UNPKG

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