vibefast-cli 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { statusCommand } from './commands/status.js';
13
13
  import { checklistCommand } from './commands/checklist.js';
14
14
  import { envCommand } from './commands/env.js';
15
15
  import { healthCommand } from './commands/health.js';
16
+ import { platformCommand } from './commands/platform.js';
16
17
  import { log } from './core/log.js';
17
18
  import { showCommandHint } from './core/errors.js';
18
19
  const program = new Command();
@@ -28,6 +29,7 @@ program.addCommand(doctorCommand);
28
29
  program.addCommand(listCommand);
29
30
  program.addCommand(addCommand);
30
31
  program.addCommand(removeCommand);
32
+ program.addCommand(platformCommand);
31
33
  program.addCommand(statusCommand);
32
34
  program.addCommand(checklistCommand);
33
35
  program.addCommand(envCommand);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibefast-cli",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for installing VibeFast features into your monorepo",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { log } from '../core/log.js';
3
3
  import { withSpinner } from '../core/spinner.js';
4
- import { promptSelect, promptInput, promptConfirm } from '../core/prompts.js';
4
+ import { promptSelectAsync, promptMultiSelectAsync, promptUser, promptYesNo } from '../core/prompt.js';
5
5
  import { detectPackageManager } from '../core/detect.js';
6
6
  import simpleGit from 'simple-git';
7
7
  import { existsSync } from 'fs';
@@ -37,57 +37,85 @@ export const initCommand = new Command('init')
37
37
  process.exit(1);
38
38
  }
39
39
 
40
- // Step 1: Choose branch (always ask unless --branch provided)
41
- let branch = options.branch as 'clean' | 'main';
40
+ // Step 1: Choose starter type
41
+ let starterType: 'clean' | 'custom' = 'clean';
42
+ let selectedFeatures: string[] = [];
42
43
 
43
- if (!options.branch) {
44
- if (options.yes) {
45
- // Default to main if --yes is used without --branch
46
- branch = 'main';
47
- log.info('Using default: full starter');
48
- } else {
49
- // Always prompt for branch selection
50
- branch = await promptSelect(
51
- 'Which starter template would you like?',
52
- [
53
- {
54
- name: 'Full',
55
- value: 'main',
56
- description: 'All features included - ready to customize',
57
- },
58
- {
59
- name: 'Clean',
60
- value: 'clean',
61
- description: 'Minimal setup - add features as you need them',
62
- },
63
- ]
64
- ) as 'clean' | 'main';
44
+ if (!options.yes) {
45
+ starterType = await promptSelectAsync(
46
+ 'How would you like to start?',
47
+ [
48
+ {
49
+ value: 'clean',
50
+ label: 'Clean',
51
+ description: 'Minimal setup - add features later with vf add',
52
+ },
53
+ {
54
+ value: 'custom',
55
+ label: 'Custom',
56
+ description: 'Choose features now - one-shot setup',
57
+ },
58
+ ]
59
+ ) as 'clean' | 'custom';
60
+
61
+ log.info(`Selected: ${starterType === 'clean' ? 'Clean starter' : 'Custom setup'}`);
62
+ log.plain('');
63
+
64
+ // If custom, let them choose features
65
+ if (starterType === 'custom') {
66
+ const { promptMultiSelectAsync } = await import('../core/prompt.js');
67
+ const { RECIPES, getRecipesByCategory } = await import('../core/recipes.js');
68
+
69
+ log.info('📦 Select features to include:');
70
+ log.plain('');
71
+
72
+ // Get native features
73
+ const nativeFeatures = getRecipesByCategory('feature').map(r => ({
74
+ value: r.name,
75
+ label: `${r.icon} ${r.name}`,
76
+ description: r.description,
77
+ }));
78
+
79
+ selectedFeatures = await promptMultiSelectAsync(
80
+ 'Select features (use space to toggle, enter to confirm):',
81
+ nativeFeatures
82
+ );
83
+
84
+ if (selectedFeatures.length > 0) {
85
+ log.info(`Selected ${selectedFeatures.length} feature(s): ${selectedFeatures.join(', ')}`);
86
+ } else {
87
+ log.info('No features selected - starting with clean setup');
88
+ }
89
+ log.plain('');
65
90
  }
91
+ } else {
92
+ log.info('Using default: Clean starter');
93
+ log.plain('');
66
94
  }
67
-
68
- log.info(`Selected: ${branch === 'main' ? 'full' : 'clean'} starter`);
69
- log.plain('');
95
+
96
+ // Always use clean branch for cloning
97
+ const branch = 'clean';
70
98
 
71
99
  // Step 1.5: Choose platforms
72
100
  let platforms: string[] = ['native', 'web'];
73
101
 
74
102
  if (!options.yes) {
75
- const platformChoice = await promptSelect(
103
+ const platformChoice = await promptSelectAsync(
76
104
  'Which platforms do you want to include?',
77
105
  [
78
106
  {
79
- name: 'Both (Native + Web)',
80
107
  value: 'both',
81
- description: 'Full monorepo with mobile and web apps',
108
+ label: 'Both (Native + Web)',
109
+ description: 'Full monorepo with mobile and web app',
82
110
  },
83
111
  {
84
- name: 'Native Only (Expo)',
85
112
  value: 'native',
113
+ label: 'Native Only (Expo)',
86
114
  description: 'Mobile app only (iOS & Android)',
87
115
  },
88
116
  {
89
- name: 'Web Only (Next.js)',
90
117
  value: 'web',
118
+ label: 'Web Only (Next.js)',
91
119
  description: 'Web app only',
92
120
  },
93
121
  ]
@@ -114,10 +142,9 @@ export const initCommand = new Command('init')
114
142
  let projectName = defaultName;
115
143
 
116
144
  if (!options.yes) {
117
- projectName = await promptInput(
118
- 'Project name:',
119
- defaultName
120
- );
145
+ projectName = await promptUser(
146
+ 'Project name: '
147
+ ) || defaultName;
121
148
  }
122
149
 
123
150
  // Validate project name
@@ -138,15 +165,13 @@ export const initCommand = new Command('init')
138
165
  const repoUrl = 'https://github.com/mzafarr/vibefast-pro.git';
139
166
 
140
167
  await withSpinner(
141
- `Cloning VibeFast monorepo (${branch === 'main' ? 'full' : 'clean'} starter)...`,
168
+ `Cloning VibeFast monorepo (clean starter)...`,
142
169
  async () => {
143
170
  const git = simpleGit();
144
171
  const cloneOptions = ['--depth', '1'];
145
172
 
146
- // Only specify branch if not main (main is default)
147
- if (branch !== 'main') {
148
- cloneOptions.unshift('--branch', branch, '--single-branch');
149
- }
173
+ // Always use clean branch
174
+ cloneOptions.unshift('--branch', 'clean', '--single-branch');
150
175
 
151
176
  try {
152
177
  await git.clone(repoUrl, projectName, cloneOptions);
@@ -274,7 +299,7 @@ export const initCommand = new Command('init')
274
299
  log.warn('pnpm is not installed');
275
300
  log.plain('');
276
301
 
277
- const shouldInstallPnpm = options.yes || await promptConfirm(
302
+ const shouldInstallPnpm = options.yes || await promptYesNo(
278
303
  'Would you like to install pnpm globally?',
279
304
  true
280
305
  );
@@ -357,7 +382,7 @@ export const initCommand = new Command('init')
357
382
  log.plain(' • Configure Convex Auth');
358
383
  log.plain('');
359
384
 
360
- const runSetup = options.yes || await promptConfirm(
385
+ const runSetup = options.yes || await promptYesNo(
361
386
  'Would you like to run the starter setup now?',
362
387
  true
363
388
  );
@@ -398,6 +423,122 @@ export const initCommand = new Command('init')
398
423
  log.plain('');
399
424
  }
400
425
 
426
+ // Step 6.5: Install selected features (if custom setup)
427
+ if (starterType === 'custom' && selectedFeatures.length > 0) {
428
+ log.plain('');
429
+ log.info('📦 Installing selected features...');
430
+ log.plain('');
431
+
432
+ // We need to be in the project directory for feature installation
433
+ process.chdir(projectPath);
434
+
435
+ for (const featureName of selectedFeatures) {
436
+ try {
437
+ log.plain('');
438
+ log.plain('━'.repeat(60));
439
+ log.info(`Installing ${featureName}...`);
440
+ log.plain('');
441
+
442
+ // Import and use the add command logic
443
+ const { getToken } = await import('../core/auth.js');
444
+ const { fetchRecipe, downloadZip } = await import('../core/http.js');
445
+ const { extractZipSafe } = await import('../core/archive.js');
446
+ const { copyTree, readFileContent } = await import('../core/fsx.js');
447
+ const { addEntry } = await import('../core/journal.js');
448
+ const { hashFiles } = await import('../core/hash.js');
449
+ const { insertNavLinkNative } = await import('../core/codemod.js');
450
+ const { getDeviceInfo } = await import('../core/auth.js');
451
+ const { validateSignature } = await import('../core/validate.js');
452
+ const { getPaths } = await import('../core/paths.js');
453
+
454
+ const paths = getPaths();
455
+ const token = await getToken();
456
+ const device = await getDeviceInfo();
457
+ const config = await validateSignature(paths.signatureFile);
458
+
459
+ // Fetch recipe
460
+ const response = await fetchRecipe({
461
+ token: token!,
462
+ device,
463
+ feature: featureName,
464
+ target: 'native',
465
+ starter: { name: config.name, version: config.version },
466
+ });
467
+
468
+ if (!response.ok || (!response.signedUrl && !response.zipData)) {
469
+ log.warn(`⚠ Could not fetch ${featureName}: ${response.error || 'Unknown error'}`);
470
+ log.info(`You can install it later with: vf add ${featureName}`);
471
+ continue;
472
+ }
473
+
474
+ // Download and extract
475
+ const zipPath = response.zipData
476
+ ? await downloadZip(response.zipData, true)
477
+ : await downloadZip(response.signedUrl!);
478
+
479
+ const extractDir = zipPath.replace('.zip', '');
480
+ await extractZipSafe(zipPath, extractDir);
481
+
482
+ // Read manifest
483
+ const manifestPath = join(extractDir, 'recipe.json');
484
+ const manifestContent = await readFileContent(manifestPath);
485
+ const manifest = JSON.parse(manifestContent);
486
+
487
+ // Copy files
488
+ const copiedFiles: string[] = [];
489
+ const { resolve: resolvePath } = await import('path');
490
+ for (const copySpec of manifest.copy) {
491
+ const srcPath = resolvePath(extractDir, copySpec.from);
492
+ const destPath = resolvePath(projectPath, copySpec.to);
493
+ const result = await copyTree(srcPath, destPath, { force: true });
494
+ copiedFiles.push(...result.files);
495
+ }
496
+
497
+ // Add navigation
498
+ let navInserted = false;
499
+ if (manifest.nav) {
500
+ navInserted = await insertNavLinkNative(paths.nativeNavFile, manifest.nav, {});
501
+ }
502
+
503
+ // Hash and journal
504
+ const fileHashes = await hashFiles(copiedFiles);
505
+ const fileEntries = Array.from(fileHashes.entries()).map(([path, hash]) => ({
506
+ path,
507
+ hash,
508
+ }));
509
+
510
+ await addEntry(paths.journalFile, {
511
+ feature: manifest.name,
512
+ target: manifest.target,
513
+ files: fileEntries,
514
+ insertedNav: navInserted,
515
+ navHref: manifest.nav?.href,
516
+ navLabel: manifest.nav?.label,
517
+ ts: Date.now(),
518
+ manifest: {
519
+ version: manifest.version,
520
+ manualSteps: manifest.manualSteps,
521
+ env: manifest.env,
522
+ },
523
+ });
524
+
525
+ log.success(`✓ ${featureName} installed successfully!`);
526
+
527
+ } catch (error: any) {
528
+ log.warn(`⚠ Failed to install ${featureName}: ${error.message}`);
529
+ log.info(`You can install it later with: vf add ${featureName}`);
530
+ }
531
+ }
532
+
533
+ // Go back to original directory
534
+ process.chdir('..');
535
+
536
+ log.plain('');
537
+ log.plain('━'.repeat(60));
538
+ log.success(`✓ Installed ${selectedFeatures.length} feature(s)`);
539
+ log.plain('');
540
+ }
541
+
401
542
  // Step 7: Login with license key (REQUIRED)
402
543
  let licenseKey = options.license;
403
544
 
@@ -425,7 +566,7 @@ export const initCommand = new Command('init')
425
566
  process.exit(1);
426
567
  }
427
568
 
428
- licenseKey = await promptInput('Enter your license key:');
569
+ licenseKey = await promptUser('Enter your license key: ');
429
570
 
430
571
  if (!licenseKey || licenseKey.trim() === '') {
431
572
  log.error('License key cannot be empty');
@@ -456,7 +597,11 @@ export const initCommand = new Command('init')
456
597
  log.plain('');
457
598
  log.success('🎉 Project initialized successfully!');
458
599
  log.plain('');
600
+ log.info(`Setup: ${starterType === 'clean' ? 'Clean' : 'Custom'}`);
459
601
  log.info(`Platforms: ${platforms.join(' + ')}`);
602
+ if (starterType === 'custom' && selectedFeatures.length > 0) {
603
+ log.info(`Features: ${selectedFeatures.join(', ')}`);
604
+ }
460
605
  log.plain('');
461
606
  log.info('Next steps:');
462
607
  log.plain(` 1. cd ${projectName}`);
@@ -0,0 +1,309 @@
1
+ import { Command } from 'commander';
2
+ import { log } from '../core/log.js';
3
+ import { withSpinner } from '../core/spinner.js';
4
+ import { promptSelectAsync } from '../core/prompt.js';
5
+ import { existsSync, rmSync } from 'fs';
6
+ import { join } from 'path';
7
+ import simpleGit from 'simple-git';
8
+
9
+ interface PlatformOptions {
10
+ yes?: boolean;
11
+ }
12
+
13
+ export const platformCommand = new Command('platform')
14
+ .description('Manage platforms in your VibeFast project')
15
+ .addCommand(
16
+ new Command('add')
17
+ .description('Add a missing platform to your project')
18
+ .option('--yes', 'Skip confirmation prompts')
19
+ .action(async (options: PlatformOptions) => {
20
+ try {
21
+ const paths = await import('../core/paths.js').then(m => m.getPaths());
22
+
23
+ // Check if we're in a VibeFast project
24
+ if (!existsSync(join(paths.cwd, '.vibefast'))) {
25
+ log.error('Not a VibeFast project. Run this from your VibeFast project root.');
26
+ process.exit(1);
27
+ }
28
+
29
+ // Check what platforms currently exist
30
+ const hasNative = existsSync(join(paths.cwd, 'apps/native'));
31
+ const hasWeb = existsSync(join(paths.cwd, 'apps/web'));
32
+
33
+ if (hasNative && hasWeb) {
34
+ log.info('✓ Both platforms already exist');
35
+ process.exit(0);
36
+ }
37
+
38
+ // Determine what can be added
39
+ const availablePlatforms = [];
40
+ if (!hasNative) availablePlatforms.push({ value: 'native', label: '📱 Native (Expo)' });
41
+ if (!hasWeb) availablePlatforms.push({ value: 'web', label: '🌐 Web (Next.js)' });
42
+
43
+ if (availablePlatforms.length === 0) {
44
+ log.info('✓ All platforms already exist');
45
+ process.exit(0);
46
+ }
47
+
48
+ log.plain('');
49
+ log.info('Available platforms to add:');
50
+ log.plain('');
51
+
52
+ const platformToAdd = await promptSelectAsync(
53
+ 'Which platform would you like to add?',
54
+ availablePlatforms
55
+ );
56
+
57
+ if (!platformToAdd) {
58
+ log.info('Cancelled');
59
+ process.exit(0);
60
+ }
61
+
62
+ log.plain('');
63
+ log.info(`Adding ${platformToAdd === 'native' ? 'Native (Expo)' : 'Web (Next.js)'} platform...`);
64
+ log.plain('');
65
+
66
+ // Clone the platform from the clean branch
67
+ const repoUrl = 'https://github.com/mzafarr/vibefast-pro.git';
68
+ const tempDir = join(paths.cwd, '.temp-platform-clone');
69
+
70
+ await withSpinner(
71
+ `Cloning ${platformToAdd} platform...`,
72
+ async () => {
73
+ const git = simpleGit();
74
+ const cloneOptions = ['--depth', '1', '--branch', 'clean', '--single-branch', '--sparse'];
75
+
76
+ try {
77
+ await git.clone(repoUrl, tempDir, cloneOptions);
78
+
79
+ // Sparse checkout only the platform we need
80
+ const sparseGit = simpleGit(tempDir);
81
+ await sparseGit.raw(['sparse-checkout', 'set', `apps/${platformToAdd}`]);
82
+ } catch (error: any) {
83
+ throw new Error(`Failed to clone platform: ${error.message}`);
84
+ }
85
+ },
86
+ {
87
+ successText: `✓ Platform cloned`,
88
+ }
89
+ );
90
+
91
+ // Copy the platform to the project
92
+ await withSpinner(
93
+ `Setting up ${platformToAdd} platform...`,
94
+ async () => {
95
+ const { copyFileSync } = await import('fs');
96
+ const { cp } = await import('fs/promises');
97
+
98
+ const sourcePath = join(tempDir, 'apps', platformToAdd);
99
+ const destPath = join(paths.cwd, 'apps', platformToAdd);
100
+
101
+ if (existsSync(sourcePath)) {
102
+ await cp(sourcePath, destPath, { recursive: true });
103
+ } else {
104
+ throw new Error(`Platform directory not found in clone`);
105
+ }
106
+
107
+ // Update package.json workspaces
108
+ const packageJsonPath = join(paths.cwd, 'package.json');
109
+ if (existsSync(packageJsonPath)) {
110
+ const { readFileSync, writeFileSync } = await import('fs');
111
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
112
+
113
+ if (packageJson.workspaces && !packageJson.workspaces.includes(`apps/${platformToAdd}`)) {
114
+ packageJson.workspaces.push(`apps/${platformToAdd}`);
115
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
116
+ }
117
+ }
118
+
119
+ // Update turbo.json
120
+ const turboJsonPath = join(paths.cwd, 'turbo.json');
121
+ if (existsSync(turboJsonPath)) {
122
+ const { readFileSync, writeFileSync } = await import('fs');
123
+ const turboJson = JSON.parse(readFileSync(turboJsonPath, 'utf-8'));
124
+
125
+ if (turboJson.pipeline) {
126
+ // Add platform-specific tasks if they don't exist
127
+ if (platformToAdd === 'native') {
128
+ if (!turboJson.pipeline['dev:native']) {
129
+ turboJson.pipeline['dev:native'] = { cache: false };
130
+ }
131
+ if (!turboJson.pipeline['native:*']) {
132
+ turboJson.pipeline['native:*'] = { cache: false };
133
+ }
134
+ } else if (platformToAdd === 'web') {
135
+ if (!turboJson.pipeline['dev:web']) {
136
+ turboJson.pipeline['dev:web'] = { cache: false };
137
+ }
138
+ if (!turboJson.pipeline['web:*']) {
139
+ turboJson.pipeline['web:*'] = { cache: false };
140
+ }
141
+ }
142
+
143
+ writeFileSync(turboJsonPath, JSON.stringify(turboJson, null, 2));
144
+ }
145
+ }
146
+
147
+ // Clean up temp directory
148
+ if (existsSync(tempDir)) {
149
+ rmSync(tempDir, { recursive: true, force: true });
150
+ }
151
+ },
152
+ {
153
+ successText: `✓ Platform setup complete`,
154
+ }
155
+ );
156
+
157
+ log.plain('');
158
+ log.success(`✓ ${platformToAdd === 'native' ? 'Native' : 'Web'} platform added successfully!`);
159
+ log.plain('');
160
+ log.info('Next steps:');
161
+ log.plain(' 1. Install dependencies:');
162
+ log.plain(' pnpm install');
163
+ log.plain('');
164
+ if (platformToAdd === 'native') {
165
+ log.plain(' 2. Start developing:');
166
+ log.plain(' pnpm dev:native');
167
+ log.plain(' pnpm native:ios');
168
+ log.plain(' pnpm native:android');
169
+ } else {
170
+ log.plain(' 2. Start developing:');
171
+ log.plain(' pnpm dev:web');
172
+ }
173
+ log.plain('');
174
+
175
+ } catch (error: any) {
176
+ log.plain('');
177
+ log.error(`Failed to add platform: ${error.message}`);
178
+ process.exit(1);
179
+ }
180
+ })
181
+ )
182
+ .addCommand(
183
+ new Command('remove')
184
+ .description('Remove a platform from your project')
185
+ .option('--yes', 'Skip confirmation prompts')
186
+ .action(async (options: PlatformOptions) => {
187
+ try {
188
+ const paths = await import('../core/paths.js').then(m => m.getPaths());
189
+
190
+ // Check if we're in a VibeFast project
191
+ if (!existsSync(join(paths.cwd, '.vibefast'))) {
192
+ log.error('Not a VibeFast project. Run this from your VibeFast project root.');
193
+ process.exit(1);
194
+ }
195
+
196
+ // Check what platforms currently exist
197
+ const hasNative = existsSync(join(paths.cwd, 'apps/native'));
198
+ const hasWeb = existsSync(join(paths.cwd, 'apps/web'));
199
+
200
+ if (!hasNative && !hasWeb) {
201
+ log.error('No platforms found to remove');
202
+ process.exit(1);
203
+ }
204
+
205
+ // Determine what can be removed
206
+ const removablePlatforms = [];
207
+ if (hasNative) removablePlatforms.push({ value: 'native', label: '📱 Native (Expo)' });
208
+ if (hasWeb) removablePlatforms.push({ value: 'web', label: '🌐 Web (Next.js)' });
209
+
210
+ if (removablePlatforms.length === 1) {
211
+ log.error('Cannot remove the only platform. You must have at least one platform.');
212
+ process.exit(1);
213
+ }
214
+
215
+ log.plain('');
216
+ log.info('Platforms you can remove:');
217
+ log.plain('');
218
+
219
+ const platformToRemove = await promptSelectAsync(
220
+ 'Which platform would you like to remove?',
221
+ removablePlatforms
222
+ );
223
+
224
+ if (!platformToRemove) {
225
+ log.info('Cancelled');
226
+ process.exit(0);
227
+ }
228
+
229
+ log.plain('');
230
+ log.warn(`⚠ This will remove the ${platformToRemove === 'native' ? 'Native' : 'Web'} platform and all its files.`);
231
+
232
+ const { promptYesNo } = await import('../core/prompt.js');
233
+ const confirmed = options.yes || await promptYesNo('Are you sure?', false);
234
+
235
+ if (!confirmed) {
236
+ log.info('Cancelled');
237
+ process.exit(0);
238
+ }
239
+
240
+ log.plain('');
241
+
242
+ await withSpinner(
243
+ `Removing ${platformToRemove} platform...`,
244
+ async () => {
245
+ const platformPath = join(paths.cwd, 'apps', platformToRemove);
246
+
247
+ if (existsSync(platformPath)) {
248
+ rmSync(platformPath, { recursive: true, force: true });
249
+ }
250
+
251
+ // Update package.json workspaces
252
+ const packageJsonPath = join(paths.cwd, 'package.json');
253
+ if (existsSync(packageJsonPath)) {
254
+ const { readFileSync, writeFileSync } = await import('fs');
255
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
256
+
257
+ if (packageJson.workspaces) {
258
+ packageJson.workspaces = packageJson.workspaces.filter(
259
+ (ws: string) => !ws.includes(`apps/${platformToRemove}`)
260
+ );
261
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
262
+ }
263
+ }
264
+
265
+ // Update turbo.json
266
+ const turboJsonPath = join(paths.cwd, 'turbo.json');
267
+ if (existsSync(turboJsonPath)) {
268
+ const { readFileSync, writeFileSync } = await import('fs');
269
+ const turboJson = JSON.parse(readFileSync(turboJsonPath, 'utf-8'));
270
+
271
+ if (turboJson.pipeline) {
272
+ const tasksToRemove: string[] = [];
273
+
274
+ if (platformToRemove === 'native') {
275
+ tasksToRemove.push('native:*', 'dev:native', 'build:native');
276
+ } else {
277
+ tasksToRemove.push('web:*', 'dev:web', 'build:web');
278
+ }
279
+
280
+ tasksToRemove.forEach(task => {
281
+ if (turboJson.pipeline[task]) {
282
+ delete turboJson.pipeline[task];
283
+ }
284
+ });
285
+
286
+ writeFileSync(turboJsonPath, JSON.stringify(turboJson, null, 2));
287
+ }
288
+ }
289
+ },
290
+ {
291
+ successText: `✓ Platform removed`,
292
+ }
293
+ );
294
+
295
+ log.plain('');
296
+ log.success(`✓ ${platformToRemove === 'native' ? 'Native' : 'Web'} platform removed successfully!`);
297
+ log.plain('');
298
+ log.info('Next steps:');
299
+ log.plain(' 1. Clean up dependencies:');
300
+ log.plain(' pnpm install');
301
+ log.plain('');
302
+
303
+ } catch (error: any) {
304
+ log.plain('');
305
+ log.error(`Failed to remove platform: ${error.message}`);
306
+ process.exit(1);
307
+ }
308
+ })
309
+ );