UNPKG

31 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.StartCommand = void 0;
4const cli_framework_1 = require("@ionic/cli-framework");
5const string_1 = require("@ionic/cli-framework/utils/string");
6const utils_fs_1 = require("@ionic/utils-fs");
7const utils_terminal_1 = require("@ionic/utils-terminal");
8const chalk = require("chalk");
9const Debug = require("debug");
10const path = require("path");
11const constants_1 = require("../constants");
12const color_1 = require("../lib/color");
13const command_1 = require("../lib/command");
14const errors_1 = require("../lib/errors");
15const executor_1 = require("../lib/executor");
16const project_1 = require("../lib/project");
17const session_1 = require("../lib/session");
18const shell_1 = require("../lib/shell");
19const start_1 = require("../lib/start");
20const emoji_1 = require("../lib/utils/emoji");
21const http_1 = require("../lib/utils/http");
22const debug = Debug('ionic:commands:start');
23class StartCommand extends command_1.Command {
24 constructor() {
25 super(...arguments);
26 this.canRemoveExisting = false;
27 }
28 async getMetadata() {
29 return {
30 name: 'start',
31 type: 'global',
32 summary: 'Create a new project',
33 description: `
34This command creates a working Ionic app. It installs dependencies for you and sets up your project.
35
36Running ${color_1.input('ionic start')} without any arguments will prompt you for information about your new project.
37
38The first argument is your app's ${color_1.input('name')}. Don't worry--you can always change this later. The ${color_1.input('--project-id')} is generated from ${color_1.input('name')} unless explicitly specified.
39
40The second argument is the ${color_1.input('template')} from which to generate your app. You can list all templates with the ${color_1.input('--list')} option. You can also specify a git repository URL for ${color_1.input('template')}, in which case the existing project will be cloned.
41
42Use the ${color_1.input('--type')} option to start projects using older versions of Ionic. For example, you can start an Ionic 3 project with ${color_1.input('--type=ionic-angular')}. Use ${color_1.input('--list')} to see all project types and templates.
43 `,
44 exampleCommands: [
45 '',
46 '--list',
47 'myApp',
48 'myApp blank',
49 'myApp tabs --cordova',
50 'myApp tabs --capacitor',
51 'myApp super --type=ionic-angular',
52 'myApp blank --type=ionic1',
53 'cordovaApp tabs --cordova',
54 '"My App" blank',
55 '"Conference App" https://github.com/ionic-team/ionic-conference-app',
56 ],
57 inputs: [
58 {
59 name: 'name',
60 summary: `The name of your new project (e.g. ${color_1.input('myApp')}, ${color_1.input('"My App"')})`,
61 validators: [cli_framework_1.validators.required],
62 },
63 {
64 name: 'template',
65 summary: `The starter template to use (e.g. ${['blank', 'tabs'].map(t => color_1.input(t)).join(', ')}; use ${color_1.input('--list')} to see all)`,
66 validators: [cli_framework_1.validators.required],
67 },
68 ],
69 options: [
70 {
71 name: 'list',
72 summary: 'List available starter templates',
73 type: Boolean,
74 aliases: ['l'],
75 },
76 {
77 name: 'type',
78 summary: `Type of project to start (e.g. ${start_1.getStarterProjectTypes().map(type => color_1.input(type)).join(', ')})`,
79 type: String,
80 },
81 {
82 name: 'cordova',
83 summary: 'Include Cordova integration',
84 type: Boolean,
85 },
86 {
87 name: 'capacitor',
88 summary: 'Include Capacitor integration',
89 type: Boolean,
90 groups: ["experimental" /* EXPERIMENTAL */],
91 },
92 {
93 name: 'deps',
94 summary: 'Do not install npm/yarn dependencies',
95 type: Boolean,
96 default: true,
97 groups: ["advanced" /* ADVANCED */],
98 },
99 {
100 name: 'git',
101 summary: 'Do not initialize a git repo',
102 type: Boolean,
103 default: true,
104 groups: ["advanced" /* ADVANCED */],
105 },
106 {
107 name: 'link',
108 summary: 'Connect your new app to Ionic',
109 type: Boolean,
110 groups: ["advanced" /* ADVANCED */],
111 },
112 {
113 name: 'id',
114 summary: 'Specify an Ionic App ID to link',
115 },
116 {
117 name: 'project-id',
118 summary: 'Specify a slug for your app (used for the directory name and package name)',
119 groups: ["advanced" /* ADVANCED */],
120 spec: { value: 'slug' },
121 },
122 {
123 name: 'package-id',
124 summary: 'Specify the bundle ID/application ID for your app (reverse-DNS notation)',
125 groups: ["advanced" /* ADVANCED */],
126 spec: { value: 'id' },
127 },
128 {
129 name: 'start-id',
130 summary: 'Used by the Ionic app start experience to generate an associated app locally',
131 groups: ["hidden" /* HIDDEN */],
132 spec: { value: 'id' },
133 },
134 {
135 name: 'tag',
136 summary: `Specify a tag to use for the starters (e.g. ${['latest', 'testing', 'next'].map(t => color_1.input(t)).join(', ')})`,
137 default: 'latest',
138 groups: ["hidden" /* HIDDEN */],
139 },
140 ],
141 };
142 }
143 async startIdStart(inputs, options) {
144 const startId = options['start-id'];
145 const wizardApiUrl = process.env.START_WIZARD_URL_BASE || `https://ionicframework.com`;
146 const { req } = await http_1.createRequest('GET', `${wizardApiUrl}/api/v1/wizard/app/${startId}`, this.env.config.getHTTPConfig());
147 const error = (e) => {
148 this.env.log.error(`No such app ${chalk.bold(startId)}. This app configuration may have expired. Please retry at https://ionicframework.com/start`);
149 if (e) {
150 throw e;
151 }
152 };
153 let data;
154 try {
155 const ret = await req;
156 if (ret.status !== 200) {
157 return error();
158 }
159 data = (await req).body;
160 if (!data) {
161 return error();
162 }
163 }
164 catch (e) {
165 return error(e);
166 }
167 let projectDir = string_1.slugify(data.name);
168 if (inputs.length === 1) {
169 projectDir = inputs[0];
170 }
171 await this.checkForExisting(projectDir);
172 inputs.push(data.name);
173 inputs.push(data.template);
174 await this.startIdConvert(startId);
175 const appIconBuffer = data.appIcon ?
176 Buffer.from(data.appIcon.replace(/^data:image\/\w+;base64,/, ''), 'base64') :
177 undefined;
178 const splashBuffer = data.appSplash ?
179 Buffer.from(data.appSplash.replace(/^data:image\/\w+;base64,/, ''), 'base64') :
180 undefined;
181 this.schema = {
182 cloned: false,
183 name: data.name,
184 type: data.type,
185 template: data.template,
186 projectId: string_1.slugify(data.name),
187 projectDir,
188 packageId: data['package-id'],
189 appflowId: undefined,
190 appIcon: appIconBuffer,
191 splash: splashBuffer,
192 themeColor: data.theme,
193 };
194 }
195 async startIdConvert(id) {
196 const wizardApiUrl = process.env.START_WIZARD_URL_BASE || `https://ionicframework.com`;
197 if (!wizardApiUrl) {
198 return;
199 }
200 const { req } = await http_1.createRequest('POST', `${wizardApiUrl}/api/v1/wizard/app/${id}/start`, this.env.config.getHTTPConfig());
201 try {
202 await req;
203 }
204 catch (e) {
205 this.env.log.warn(`Unable to set app flag on server: ${e.message}`);
206 }
207 }
208 async preRun(inputs, options) {
209 const { promptToLogin } = await Promise.resolve().then(() => require('../lib/session'));
210 start_1.verifyOptions(options, this.env);
211 const appflowId = options['id'] ? String(options['id']) : undefined;
212 if (appflowId) {
213 if (!this.env.session.isLoggedIn()) {
214 await promptToLogin(this.env);
215 }
216 }
217 // The start wizard pre-populates all arguments for the CLI
218 if (options['start-id']) {
219 await this.startIdStart(inputs, options);
220 return;
221 }
222 const projectType = string_1.isValidURL(inputs[1]) ? 'custom' : options['type'] ? String(options['type']) : await this.getProjectType();
223 if (options['cordova']) {
224 const { checkForUnsupportedProject } = await Promise.resolve().then(() => require('../lib/integrations/cordova/utils'));
225 try {
226 await checkForUnsupportedProject(projectType);
227 }
228 catch (e) {
229 this.env.log.error(e.message);
230 options['cordova'] = false;
231 }
232 }
233 if (!inputs[0]) {
234 if (appflowId) {
235 const { AppClient } = await Promise.resolve().then(() => require('../lib/app'));
236 const token = await this.env.session.getUserToken();
237 const appClient = new AppClient(token, this.env);
238 const tasks = this.createTaskChain();
239 tasks.next(`Looking up app ${color_1.input(appflowId)}`);
240 const app = await appClient.load(appflowId);
241 // TODO: can ask to clone via repo_url
242 tasks.end();
243 this.env.log.info(`Using ${color_1.strong(app.name)} for ${color_1.input('name')} and ${color_1.strong(app.slug)} for ${color_1.input('--project-id')}.`);
244 inputs[0] = app.name;
245 options['project-id'] = app.slug;
246 }
247 else {
248 if (this.env.flags.interactive) {
249 this.env.log.nl();
250 this.env.log.msg(`${color_1.strong(`Every great app needs a name! ${emoji_1.emoji('😍', '')}`)}\n` +
251 `Please enter the full name of your app. You can change this at any time. To bypass this prompt next time, supply ${color_1.input('name')}, the first argument to ${color_1.input('ionic start')}.\n\n`);
252 }
253 const name = await this.env.prompt({
254 type: 'input',
255 name: 'name',
256 message: 'Project name:',
257 validate: v => cli_framework_1.validators.required(v),
258 });
259 inputs[0] = name;
260 }
261 }
262 if (!inputs[1]) {
263 if (this.env.flags.interactive) {
264 this.env.log.nl();
265 this.env.log.msg(`${color_1.strong(`Let's pick the perfect starter template! ${emoji_1.emoji('💪', '')}`)}\n` +
266 `Starter templates are ready-to-go Ionic apps that come packed with everything you need to build your app. To bypass this prompt next time, supply ${color_1.input('template')}, the second argument to ${color_1.input('ionic start')}.\n\n`);
267 }
268 const template = await this.env.prompt({
269 type: 'list',
270 name: 'template',
271 message: 'Starter template:',
272 choices: () => {
273 const starterTemplateList = start_1.STARTER_TEMPLATES.filter(st => st.projectType === projectType);
274 const cols = utils_terminal_1.columnar(starterTemplateList.map(({ name, description }) => [color_1.input(name), description || '']), constants_1.COLUMNAR_OPTIONS).split('\n');
275 if (starterTemplateList.length === 0) {
276 throw new errors_1.FatalException(`No starter templates found for project type: ${color_1.input(projectType)}.`);
277 }
278 return starterTemplateList.map((starter, i) => {
279 return {
280 name: cols[i],
281 short: starter.name,
282 value: starter.name,
283 };
284 });
285 },
286 });
287 inputs[1] = template;
288 }
289 const starterTemplate = start_1.STARTER_TEMPLATES.find(t => t.name === inputs[1] && t.projectType === projectType);
290 if (starterTemplate && starterTemplate.type === 'repo') {
291 inputs[1] = starterTemplate.repo;
292 }
293 const cloned = string_1.isValidURL(inputs[1]);
294 if (this.project && this.project.details.context === 'app') {
295 const confirm = await this.env.prompt({
296 type: 'confirm',
297 name: 'confirm',
298 message: 'You are already in an Ionic project directory. Do you really want to start another project here?',
299 default: false,
300 });
301 if (!confirm) {
302 this.env.log.info('Not starting project within existing project.');
303 throw new errors_1.FatalException();
304 }
305 }
306 await this.validateProjectType(projectType);
307 if (cloned) {
308 if (!options['git']) {
309 this.env.log.warn(`The ${color_1.input('--no-git')} option has no effect when cloning apps. Git must be used.`);
310 }
311 options['git'] = true;
312 }
313 if (options['v1'] || options['v2']) {
314 throw new errors_1.FatalException(`The ${color_1.input('--v1')} and ${color_1.input('--v2')} flags have been removed.\n` +
315 `Use the ${color_1.input('--type')} option. (see ${color_1.input('ionic start --help')})`);
316 }
317 if (options['app-name']) {
318 this.env.log.warn(`The ${color_1.input('--app-name')} option has been removed. Use the ${color_1.input('name')} argument with double quotes: e.g. ${color_1.input('ionic start "My App"')}`);
319 }
320 if (options['display-name']) {
321 this.env.log.warn(`The ${color_1.input('--display-name')} option has been removed. Use the ${color_1.input('name')} argument with double quotes: e.g. ${color_1.input('ionic start "My App"')}`);
322 }
323 if (options['bundle-id']) {
324 this.env.log.warn(`The ${color_1.input('--bundle-id')} option has been deprecated. Please use ${color_1.input('--package-id')}.`);
325 options['package-id'] = options['bundle-id'];
326 }
327 let projectId = options['project-id'] ? String(options['project-id']) : undefined;
328 if (projectId) {
329 await this.validateProjectId(projectId);
330 }
331 else {
332 projectId = options['project-id'] = project_1.isValidProjectId(inputs[0]) ? inputs[0] : string_1.slugify(inputs[0]);
333 }
334 const projectDir = path.resolve(projectId);
335 const packageId = options['package-id'] ? String(options['package-id']) : undefined;
336 if (projectId) {
337 await this.checkForExisting(projectDir);
338 }
339 if (cloned) {
340 this.schema = {
341 cloned: true,
342 url: inputs[1],
343 projectId,
344 projectDir,
345 };
346 }
347 else {
348 this.schema = {
349 cloned: false,
350 name: inputs[0],
351 type: projectType,
352 template: inputs[1],
353 projectId,
354 projectDir,
355 packageId,
356 appflowId,
357 themeColor: undefined,
358 };
359 }
360 }
361 async getProjectType() {
362 if (this.env.flags.interactive) {
363 this.env.log.nl();
364 this.env.log.msg(`${color_1.strong(`Pick a framework! ${emoji_1.emoji('😁', '')}`)}\n\n` +
365 `Please select the JavaScript framework to use for your new app. To bypass this prompt next time, supply a value for the ${color_1.input('--type')} option.\n\n`);
366 }
367 const frameworkChoice = await this.env.prompt({
368 type: 'list',
369 name: 'frameworks',
370 message: 'Framework:',
371 default: 'angular',
372 choices: () => {
373 const cols = utils_terminal_1.columnar(start_1.SUPPORTED_FRAMEWORKS.map(({ name, description }) => [color_1.input(name), description]), constants_1.COLUMNAR_OPTIONS).split('\n');
374 return start_1.SUPPORTED_FRAMEWORKS.map((starterTemplate, i) => {
375 return {
376 name: cols[i],
377 short: starterTemplate.name,
378 value: starterTemplate.type,
379 };
380 });
381 },
382 });
383 return frameworkChoice;
384 }
385 async run(inputs, options, runinfo) {
386 const { pkgManagerArgs } = await Promise.resolve().then(() => require('../lib/utils/npm'));
387 const { getTopLevel, isGitInstalled } = await Promise.resolve().then(() => require('../lib/git'));
388 if (!this.schema) {
389 throw new errors_1.FatalException(`Invalid start schema: cannot start app.`);
390 }
391 const { projectId, projectDir, packageId, appflowId } = this.schema;
392 const tag = options['tag'] ? String(options['tag']) : 'latest';
393 let linkConfirmed = typeof appflowId === 'string';
394 const gitDesired = options['git'] ? true : false;
395 const gitInstalled = await isGitInstalled(this.env);
396 const gitTopLevel = await getTopLevel(this.env);
397 let gitIntegration = gitDesired && gitInstalled && !gitTopLevel ? true : false;
398 if (!gitInstalled) {
399 const installationDocs = `See installation docs for git: ${color_1.strong('https://git-scm.com/book/en/v2/Getting-Started-Installing-Git')}`;
400 if (appflowId) {
401 throw new errors_1.FatalException(`Git CLI not found on your PATH.\n` +
402 `Git must be installed to connect this app to Ionic. ${installationDocs}`);
403 }
404 if (this.schema.cloned) {
405 throw new errors_1.FatalException(`Git CLI not found on your PATH.\n` +
406 `Git must be installed to clone apps with ${color_1.input('ionic start')}. ${installationDocs}`);
407 }
408 }
409 if (gitTopLevel && !this.schema.cloned) {
410 this.env.log.info(`Existing git project found (${color_1.strong(gitTopLevel)}). Git operations are disabled.`);
411 }
412 const tasks = this.createTaskChain();
413 tasks.next(`Preparing directory ${color_1.input(utils_terminal_1.prettyPath(projectDir))}`);
414 if (this.canRemoveExisting) {
415 await utils_fs_1.remove(projectDir);
416 }
417 await utils_fs_1.mkdir(projectDir);
418 tasks.end();
419 if (this.schema.cloned) {
420 await this.env.shell.run('git', ['clone', this.schema.url, projectDir, '--progress'], { stdio: 'inherit' });
421 }
422 else {
423 const starterTemplate = await this.findStarterTemplate(this.schema.template, this.schema.type, tag);
424 await this.downloadStarterTemplate(projectDir, starterTemplate);
425 }
426 let project;
427 if (this.project && this.project.details.context === 'multiapp' && !this.schema.cloned) {
428 // We're in a multi-app setup, so the new config file isn't wanted.
429 await utils_fs_1.unlink(path.resolve(projectDir, constants_1.PROJECT_FILE));
430 project = await project_1.createProjectFromDetails({ context: 'multiapp', configPath: path.resolve(this.project.rootDirectory, constants_1.PROJECT_FILE), id: projectId, type: this.schema.type, errors: [] }, this.env);
431 project.config.set('type', this.schema.type);
432 project.config.set('root', path.relative(this.project.rootDirectory, projectDir));
433 }
434 else {
435 project = await project_1.createProjectFromDirectory(projectDir, { _: [] }, this.env, { logErrors: false });
436 }
437 // start is weird, once the project directory is created, it becomes a
438 // "project" command and so we replace the `Project` instance that was
439 // autogenerated when the CLI booted up. This has worked thus far?
440 this.namespace.root.project = project;
441 if (!this.project) {
442 throw new errors_1.FatalException('Error while loading project.');
443 }
444 this.env.shell.alterPath = p => shell_1.prependNodeModulesBinToPath(projectDir, p);
445 if (!this.schema.cloned) {
446 if (this.schema.type === 'react') {
447 options['capacitor'] = true;
448 }
449 if (options['cordova']) {
450 const { confirmCordovaUsage } = await Promise.resolve().then(() => require('../lib/integrations/cordova/utils'));
451 const confirm = await confirmCordovaUsage(this.env);
452 if (confirm) {
453 await executor_1.runCommand(runinfo, ['integrations', 'enable', 'cordova', '--quiet']);
454 }
455 else {
456 options['cordova'] = false;
457 }
458 }
459 if (options['capacitor'] === null && !options['cordova']) {
460 const confirm = await this.env.prompt({
461 type: 'confirm',
462 name: 'confirm',
463 message: 'Integrate your new app with Capacitor to target native iOS and Android?',
464 default: false,
465 });
466 if (confirm) {
467 options['capacitor'] = true;
468 }
469 }
470 if (options['capacitor']) {
471 await executor_1.runCommand(runinfo, ['integrations', 'enable', 'capacitor', '--quiet', '--', this.schema.name, packageId ? packageId : 'io.ionic.starter']);
472 }
473 await this.project.personalize({
474 name: this.schema.name,
475 projectId,
476 packageId,
477 appIcon: this.schema.appIcon,
478 splash: this.schema.splash,
479 themeColor: this.schema.themeColor,
480 });
481 this.env.log.nl();
482 }
483 const shellOptions = { cwd: projectDir, stdio: 'inherit' };
484 if (options['deps']) {
485 this.env.log.msg('Installing dependencies may take several minutes.');
486 this.env.log.rawmsg(start_1.getAdvertisement());
487 const [installer, ...installerArgs] = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install' });
488 await this.env.shell.run(installer, installerArgs, shellOptions);
489 }
490 if (!this.schema.cloned) {
491 if (gitIntegration) {
492 try {
493 await this.env.shell.run('git', ['init'], shellOptions); // TODO: use initializeRepo()?
494 }
495 catch (e) {
496 this.env.log.warn('Error encountered during repo initialization. Disabling further git operations.');
497 gitIntegration = false;
498 }
499 }
500 // Prompt to create account
501 if (!this.env.session.isLoggedIn()) {
502 await session_1.promptToSignup(this.env);
503 }
504 if (options['link']) {
505 const cmdArgs = ['link'];
506 if (appflowId) {
507 cmdArgs.push(appflowId);
508 }
509 cmdArgs.push('--name', this.schema.name);
510 await executor_1.runCommand(runinfo, cmdArgs);
511 linkConfirmed = true;
512 }
513 const manifestPath = path.resolve(projectDir, 'ionic.starter.json');
514 const manifest = await this.loadManifest(manifestPath);
515 if (manifest) {
516 await utils_fs_1.unlink(manifestPath);
517 }
518 if (gitIntegration) {
519 try {
520 await this.env.shell.run('git', ['add', '-A'], shellOptions);
521 await this.env.shell.run('git', ['commit', '-m', 'Initial commit', '--no-gpg-sign'], shellOptions);
522 }
523 catch (e) {
524 this.env.log.warn('Error encountered during commit. Disabling further git operations.');
525 gitIntegration = false;
526 }
527 }
528 if (manifest) {
529 await this.performManifestOps(manifest);
530 }
531 }
532 this.env.log.nl();
533 await this.showNextSteps(projectDir, this.schema.cloned, linkConfirmed, !options['cordova']);
534 }
535 async checkForExisting(projectDir) {
536 const projectExists = await utils_fs_1.pathExists(projectDir);
537 if (projectExists) {
538 const confirm = await this.env.prompt({
539 type: 'confirm',
540 name: 'confirm',
541 message: `${color_1.input(utils_terminal_1.prettyPath(projectDir))} exists. ${color_1.failure('Overwrite?')}`,
542 default: false,
543 });
544 if (!confirm) {
545 this.env.log.msg(`Not erasing existing project in ${color_1.input(utils_terminal_1.prettyPath(projectDir))}.`);
546 throw new errors_1.FatalException();
547 }
548 this.canRemoveExisting = confirm;
549 }
550 }
551 async findStarterTemplate(template, type, tag) {
552 const starterTemplate = start_1.STARTER_TEMPLATES.find(t => t.projectType === type && t.name === template);
553 if (starterTemplate && starterTemplate.type === 'managed') {
554 return {
555 ...starterTemplate,
556 archive: `${start_1.STARTER_BASE_URL}/${tag === 'latest' ? '' : `${tag}/`}${starterTemplate.id}.tar.gz`,
557 };
558 }
559 const tasks = this.createTaskChain();
560 tasks.next('Looking up starter');
561 const starterList = await start_1.getStarterList(this.env.config, tag);
562 const starter = starterList.starters.find(t => t.type === type && t.name === template);
563 if (starter) {
564 tasks.end();
565 return {
566 name: starter.name,
567 projectType: starter.type,
568 archive: `${start_1.STARTER_BASE_URL}/${tag === 'latest' ? '' : `${tag}/`}${starter.id}.tar.gz`,
569 };
570 }
571 else {
572 throw new errors_1.FatalException(`Unable to find starter template for ${color_1.input(template)}\n` +
573 `If this is not a typo, please make sure it is a valid starter template within the starters repo: ${color_1.strong('https://github.com/ionic-team/starters')}`);
574 }
575 }
576 async validateProjectType(type) {
577 const projectTypes = start_1.getStarterProjectTypes();
578 if (!['custom', ...projectTypes].includes(type)) {
579 throw new errors_1.FatalException(`${color_1.input(type)} is not a valid project type.\n` +
580 `Please choose a different ${color_1.input('--type')}. Use ${color_1.input('ionic start --list')} to list all available starter templates.`);
581 }
582 }
583 async validateProjectId(projectId) {
584 if (!project_1.isValidProjectId(projectId)) {
585 throw new errors_1.FatalException(`${color_1.input(projectId)} is not a valid package or directory name.\n` +
586 `Please choose a different ${color_1.input('--project-id')}. Alphanumeric characters are always safe.`);
587 }
588 }
589 async loadManifest(manifestPath) {
590 try {
591 return await start_1.readStarterManifest(manifestPath);
592 }
593 catch (e) {
594 debug(`Error with manifest file ${color_1.strong(utils_terminal_1.prettyPath(manifestPath))}: ${e}`);
595 }
596 }
597 async performManifestOps(manifest) {
598 if (manifest.welcome) {
599 this.env.log.nl();
600 this.env.log.msg(`${color_1.strong('Starter Welcome')}:`);
601 this.env.log.msg(manifest.welcome);
602 }
603 }
604 async downloadStarterTemplate(projectDir, starterTemplate) {
605 const { createRequest, download } = await Promise.resolve().then(() => require('../lib/utils/http'));
606 const { tar } = await Promise.resolve().then(() => require('../lib/utils/archive'));
607 const tasks = this.createTaskChain();
608 const task = tasks.next(`Downloading and extracting ${color_1.input(starterTemplate.name.toString())} starter`);
609 debug('Tar extraction created for %s', projectDir);
610 const ws = tar.extract({ cwd: projectDir });
611 const { req } = await createRequest('GET', starterTemplate.archive, this.env.config.getHTTPConfig());
612 await download(req, ws, { progress: (loaded, total) => task.progress(loaded, total) });
613 tasks.end();
614 }
615 async showNextSteps(projectDir, cloned, linkConfirmed, isCapacitor) {
616 const cordovaResCommand = isCapacitor ? 'cordova-res --skip-config --copy' : 'cordova-res';
617 const steps = [
618 `Go to your ${cloned ? 'cloned' : 'new'} project: ${color_1.input(`cd ${utils_terminal_1.prettyPath(projectDir)}`)}`,
619 `Run ${color_1.input('ionic serve')} within the app directory to see your app in the browser`,
620 isCapacitor ?
621 `Run ${color_1.input('ionic capacitor add')} to add a native iOS or Android project using Capacitor` :
622 `Run ${color_1.input('ionic cordova platform add')} to add a native iOS or Android project using Cordova`,
623 `Generate your app icon and splash screens using ${color_1.input(cordovaResCommand)}`,
624 `Explore the Ionic docs for components, tutorials, and more: ${color_1.strong('https://ion.link/docs')}`,
625 `Building an enterprise app? Ionic has Enterprise Support and Features: ${color_1.strong('https://ion.link/enterprise-edition')}`,
626 ];
627 if (linkConfirmed) {
628 steps.push(`Push your code to Ionic Appflow to perform real-time updates, and more: ${color_1.input('git push ionic master')}`);
629 }
630 this.env.log.msg(`${color_1.strong('Your Ionic app is ready! Follow these next steps')}:\n${steps.map(s => ` - ${s}`).join('\n')}`);
631 }
632}
633exports.StartCommand = StartCommand;