UNPKG

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