vibefast-cli 0.1.4 → 0.2.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.
Files changed (78) hide show
  1. package/AUTO-DETECT-DEPS.md +607 -0
  2. package/CHANGELOG.md +86 -0
  3. package/FINAL-PACKAGE-STRATEGY.md +583 -0
  4. package/FINAL-SIMPLE-PLAN.md +487 -0
  5. package/FLOW-DIAGRAM.md +1629 -0
  6. package/GOTCHAS-AND-RISKS.md +801 -0
  7. package/IMPLEMENTATION-COMPLETE.md +477 -0
  8. package/IMPLEMENTATION-PLAN.md +1360 -0
  9. package/PRE-PUBLISH-CHECKLIST.md +558 -0
  10. package/PRODUCTION-READINESS.md +684 -0
  11. package/PRODUCTION-TEST-RESULTS.md +465 -0
  12. package/README.md +73 -7
  13. package/READY-TO-PUBLISH.md +419 -0
  14. package/SIMPLIFIED-PLAN.md +578 -0
  15. package/TEST-SUMMARY.md +261 -0
  16. package/USER-MODIFICATIONS.md +448 -0
  17. package/cloudflare-worker/worker.js +26 -6
  18. package/dist/commands/add.d.ts.map +1 -1
  19. package/dist/commands/add.js +192 -15
  20. package/dist/commands/add.js.map +1 -1
  21. package/dist/commands/checklist.d.ts +3 -0
  22. package/dist/commands/checklist.d.ts.map +1 -0
  23. package/dist/commands/checklist.js +64 -0
  24. package/dist/commands/checklist.js.map +1 -0
  25. package/dist/commands/remove.d.ts.map +1 -1
  26. package/dist/commands/remove.js +85 -2
  27. package/dist/commands/remove.js.map +1 -1
  28. package/dist/commands/status.d.ts +3 -0
  29. package/dist/commands/status.d.ts.map +1 -0
  30. package/dist/commands/status.js +40 -0
  31. package/dist/commands/status.js.map +1 -0
  32. package/dist/core/__tests__/fsx.test.d.ts +2 -0
  33. package/dist/core/__tests__/fsx.test.d.ts.map +1 -0
  34. package/dist/core/__tests__/fsx.test.js +79 -0
  35. package/dist/core/__tests__/fsx.test.js.map +1 -0
  36. package/dist/core/__tests__/hash.test.d.ts +2 -0
  37. package/dist/core/__tests__/hash.test.d.ts.map +1 -0
  38. package/dist/core/__tests__/hash.test.js +84 -0
  39. package/dist/core/__tests__/hash.test.js.map +1 -0
  40. package/dist/core/__tests__/journal.test.js +65 -0
  41. package/dist/core/__tests__/journal.test.js.map +1 -1
  42. package/dist/core/__tests__/prompt.test.d.ts +2 -0
  43. package/dist/core/__tests__/prompt.test.d.ts.map +1 -0
  44. package/dist/core/__tests__/prompt.test.js +56 -0
  45. package/dist/core/__tests__/prompt.test.js.map +1 -0
  46. package/dist/core/fsx.d.ts +7 -1
  47. package/dist/core/fsx.d.ts.map +1 -1
  48. package/dist/core/fsx.js +18 -3
  49. package/dist/core/fsx.js.map +1 -1
  50. package/dist/core/hash.d.ts +13 -0
  51. package/dist/core/hash.d.ts.map +1 -0
  52. package/dist/core/hash.js +69 -0
  53. package/dist/core/hash.js.map +1 -0
  54. package/dist/core/journal.d.ts +10 -1
  55. package/dist/core/journal.d.ts.map +1 -1
  56. package/dist/core/journal.js +23 -1
  57. package/dist/core/journal.js.map +1 -1
  58. package/dist/core/prompt.d.ts +11 -0
  59. package/dist/core/prompt.d.ts.map +1 -0
  60. package/dist/core/prompt.js +34 -0
  61. package/dist/core/prompt.js.map +1 -0
  62. package/dist/index.js +4 -0
  63. package/dist/index.js.map +1 -1
  64. package/package.json +3 -1
  65. package/src/commands/add.ts +234 -16
  66. package/src/commands/checklist.ts +71 -0
  67. package/src/commands/remove.ts +105 -3
  68. package/src/commands/status.ts +47 -0
  69. package/src/core/__tests__/fsx.test.ts +101 -0
  70. package/src/core/__tests__/hash.test.ts +112 -0
  71. package/src/core/__tests__/journal.test.ts +76 -0
  72. package/src/core/__tests__/prompt.test.ts +72 -0
  73. package/src/core/fsx.ts +38 -5
  74. package/src/core/hash.ts +84 -0
  75. package/src/core/journal.ts +40 -2
  76. package/src/core/prompt.ts +40 -0
  77. package/src/index.ts +4 -0
  78. package/text.md +27 -0
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Prompt user for input
3
+ * Returns empty string in non-interactive environments (CI/CD)
4
+ */
5
+ export declare function promptUser(question: string): string;
6
+ /**
7
+ * Prompt user for yes/no confirmation
8
+ * Returns defaultValue in non-interactive environments
9
+ */
10
+ export declare function promptYesNo(question: string, defaultYes?: boolean): boolean;
11
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/core/prompt.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiBnD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,UAAQ,GAAG,OAAO,CAUzE"}
@@ -0,0 +1,34 @@
1
+ import readlineSync from 'readline-sync';
2
+ /**
3
+ * Prompt user for input
4
+ * Returns empty string in non-interactive environments (CI/CD)
5
+ */
6
+ export function promptUser(question) {
7
+ // Check if we're in a non-interactive environment
8
+ if (!process.stdin.isTTY) {
9
+ return '';
10
+ }
11
+ // Check for CI environment variables
12
+ const isCI = process.env.CI === 'true' ||
13
+ process.env.GITHUB_ACTIONS === 'true' ||
14
+ process.env.GITLAB_CI === 'true' ||
15
+ process.env.CIRCLECI === 'true';
16
+ if (isCI) {
17
+ return '';
18
+ }
19
+ return readlineSync.question(question);
20
+ }
21
+ /**
22
+ * Prompt user for yes/no confirmation
23
+ * Returns defaultValue in non-interactive environments
24
+ */
25
+ export function promptYesNo(question, defaultYes = false) {
26
+ const answer = promptUser(question);
27
+ // Empty answer (non-interactive or just pressed enter)
28
+ if (answer === '') {
29
+ return defaultYes;
30
+ }
31
+ const normalized = answer.toLowerCase().trim();
32
+ return normalized === 'y' || normalized === 'yes';
33
+ }
34
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/core/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,eAAe,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,kDAAkD;IAClD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACzB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,qCAAqC;IACrC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,KAAK,MAAM;QACzB,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,MAAM;QACrC,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,MAAM;QAChC,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,CAAC;IAE7C,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACzC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,UAAU,GAAG,KAAK;IAC9D,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAEpC,uDAAuD;IACvD,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAClB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IAC/C,OAAO,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,KAAK,CAAC;AACpD,CAAC"}
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ import { doctorCommand } from './commands/doctor.js';
8
8
  import { loginCommand } from './commands/login.js';
9
9
  import { devicesCommand } from './commands/devices.js';
10
10
  import { logoutCommand } from './commands/logout.js';
11
+ import { statusCommand } from './commands/status.js';
12
+ import { checklistCommand } from './commands/checklist.js';
11
13
  const program = new Command();
12
14
  program
13
15
  .name('vf')
@@ -20,5 +22,7 @@ program.addCommand(doctorCommand);
20
22
  program.addCommand(listCommand);
21
23
  program.addCommand(addCommand);
22
24
  program.addCommand(removeCommand);
25
+ program.addCommand(statusCommand);
26
+ program.addCommand(checklistCommand);
23
27
  program.parse();
24
28
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,IAAI,CAAC;KACV,WAAW,CAAC,oDAAoD,CAAC;KACjE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC,CAAC;AAEnC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AACjC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAElC,OAAO,CAAC,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,IAAI,CAAC;KACV,WAAW,CAAC,oDAAoD,CAAC;KACjE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC,CAAC;AAEnC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AACjC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;AAErC,OAAO,CAAC,KAAK,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibefast-cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for installing VibeFast features into your monorepo",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,10 +24,12 @@
24
24
  "dependencies": {
25
25
  "commander": "^12.0.0",
26
26
  "picocolors": "^1.0.0",
27
+ "readline-sync": "^1.4.10",
27
28
  "yauzl": "^2.10.0"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@types/node": "^20.11.0",
32
+ "@types/readline-sync": "^1.4.8",
31
33
  "@types/yauzl": "^2.10.3",
32
34
  "tsx": "^4.7.0",
33
35
  "typescript": "^5.3.3",
@@ -4,20 +4,42 @@ import { getPaths } from '../core/paths.js';
4
4
  import { validateSignature, validateTarget } from '../core/validate.js';
5
5
  import { getToken, getDeviceInfo } from '../core/auth.js';
6
6
  import { fetchRecipe, downloadZip } from '../core/http.js';
7
- import { addEntry, getEntry } from '../core/journal.js';
7
+ import { addEntry, getEntry, FileEntry } from '../core/journal.js';
8
8
  import { copyTree, readFileContent } from '../core/fsx.js';
9
9
  import { insertNavLinkNative, insertNavLinkWeb } from '../core/codemod.js';
10
10
  import { join, resolve } from 'path';
11
11
  import { ensureWithinBase } from '../core/pathGuard.js';
12
12
  import { extractZipSafe } from '../core/archive.js';
13
+ import { hashFiles } from '../core/hash.js';
14
+ import { promptYesNo } from '../core/prompt.js';
13
15
 
14
16
  interface RecipeManifest {
15
17
  name: string;
16
18
  version: string;
17
19
  description: string;
18
20
  copy: Array<{ from: string; to: string }>;
19
- nav: { href: string; label: string };
21
+ nav?: { href: string; label: string };
20
22
  target: 'native' | 'web';
23
+ dependencies?: {
24
+ expo?: string[];
25
+ npm?: string[];
26
+ };
27
+ env?: Array<{
28
+ key: string;
29
+ description: string;
30
+ example: string;
31
+ link?: string;
32
+ }>;
33
+ manualSteps?: Array<{
34
+ title: string;
35
+ description: string;
36
+ link?: string;
37
+ file?: string;
38
+ content?: string;
39
+ }>;
40
+ postInstall?: {
41
+ message?: string;
42
+ };
21
43
  }
22
44
 
23
45
  export const addCommand = new Command('add')
@@ -25,7 +47,8 @@ export const addCommand = new Command('add')
25
47
  .argument('<feature>', 'Feature name to install')
26
48
  .option('--target <target>', 'Target platform (native or web)', 'native')
27
49
  .option('--dry-run', 'Preview changes without applying')
28
- .option('--force', 'Overwrite existing files')
50
+ .option('--force', 'Overwrite existing files without asking')
51
+ .option('--yes', 'Answer yes to all prompts (for automation)')
29
52
  .action(async (feature: string, options) => {
30
53
  try {
31
54
  const paths = getPaths();
@@ -66,10 +89,76 @@ export const addCommand = new Command('add')
66
89
  });
67
90
 
68
91
  if (!response.ok || (!response.signedUrl && !response.zipData)) {
69
- log.error(`Failed to fetch recipe: ${response.error || 'Unknown error'}`);
70
- if (response.message) {
71
- log.plain(` ${response.message}`);
92
+ const error = response.error || 'Unknown error';
93
+ const message = response.message || '';
94
+
95
+ log.plain('');
96
+ log.error('Failed to fetch recipe');
97
+ log.plain('');
98
+
99
+ // User-friendly error messages
100
+ if (error.includes('Invalid') || error.includes('token') || error.includes('license')) {
101
+ log.plain('❌ Invalid or expired license key');
102
+ log.plain('');
103
+ log.info('Your license key may be:');
104
+ log.plain(' • Incorrect or mistyped');
105
+ log.plain(' • Expired');
106
+ log.plain(' • Revoked');
107
+ log.plain('');
108
+ log.info('To fix this:');
109
+ log.plain(' 1. Check your license key from your purchase receipt');
110
+ log.plain(' 2. Run: vf logout');
111
+ log.plain(' 3. Run: vf login --token YOUR_CORRECT_TOKEN');
112
+ log.plain('');
113
+ log.info('Need help? Contact support@vibefast.pro');
114
+ } else if (error.includes('Device limit') || error.includes('device') || message.includes('device')) {
115
+ log.plain('❌ Device limit reached');
116
+ log.plain('');
117
+ log.info('You have reached the maximum number of devices for your license');
118
+ log.plain('');
119
+ log.info('To fix this:');
120
+ log.plain(' 1. Run: vf devices');
121
+ log.plain(' 2. Deactivate an unused device: vf devices --deactivate <device-id>');
122
+ log.plain(' 3. Try again: vf add ' + feature);
123
+ log.plain('');
124
+ if (message) {
125
+ log.plain(`Details: ${message}`);
126
+ log.plain('');
127
+ }
128
+ } else if (error.includes('Network') || error.includes('connect')) {
129
+ log.plain('❌ Network error');
130
+ log.plain('');
131
+ log.info('Could not connect to VibeFast servers');
132
+ log.plain('');
133
+ log.info('Please check:');
134
+ log.plain(' • Your internet connection');
135
+ log.plain(' • Firewall settings');
136
+ log.plain(' • VPN configuration');
137
+ log.plain('');
138
+ if (message) {
139
+ log.plain(`Details: ${message}`);
140
+ log.plain('');
141
+ }
142
+ } else if (error.includes('not found') || error.includes('404')) {
143
+ log.plain('❌ Feature not found');
144
+ log.plain('');
145
+ log.info(`The feature "${feature}" does not exist or is not available for ${target}`);
146
+ log.plain('');
147
+ log.info('To see available features:');
148
+ log.plain(' vf list');
149
+ log.plain('');
150
+ } else {
151
+ // Generic error
152
+ log.plain(`❌ ${error}`);
153
+ if (message) {
154
+ log.plain('');
155
+ log.plain(`Details: ${message}`);
156
+ }
157
+ log.plain('');
158
+ log.info('If this problem persists, contact support@vibefast.pro');
72
159
  }
160
+
161
+ log.plain('');
73
162
  process.exit(1);
74
163
  }
75
164
 
@@ -96,8 +185,11 @@ export const addCommand = new Command('add')
96
185
 
97
186
  log.info(`Installing ${manifest.name} v${manifest.version}...`);
98
187
 
99
- // Copy files
188
+ // Copy files with interactive confirmation
100
189
  const copiedFiles: string[] = [];
190
+ const allConflicts: string[] = [];
191
+ const allSkipped: string[] = [];
192
+
101
193
  for (const copySpec of manifest.copy) {
102
194
  const srcPath = ensureWithinBase(
103
195
  extractRoot,
@@ -111,11 +203,35 @@ export const addCommand = new Command('add')
111
203
  );
112
204
 
113
205
  log.info(`Copying ${copySpec.from} → ${copySpec.to}`);
114
- const files = await copyTree(srcPath, destPath, {
206
+ const result = await copyTree(srcPath, destPath, {
115
207
  dryRun: options.dryRun,
116
208
  force: options.force,
209
+ interactive: !options.force && !options.dryRun && !options.yes,
117
210
  });
118
- copiedFiles.push(...files);
211
+
212
+ copiedFiles.push(...result.files);
213
+ allConflicts.push(...result.conflicts);
214
+ allSkipped.push(...result.skipped);
215
+ }
216
+
217
+ // Show conflict warnings in dry-run mode
218
+ if (options.dryRun && allConflicts.length > 0) {
219
+ log.plain('');
220
+ log.warn(`⚠ ${allConflicts.length} file(s) will be overwritten:`);
221
+ allConflicts.slice(0, 5).forEach(f => {
222
+ const relativePath = f.replace(paths.cwd + '/', '');
223
+ log.plain(` • ${relativePath}`);
224
+ });
225
+ if (allConflicts.length > 5) {
226
+ log.plain(` ... and ${allConflicts.length - 5} more`);
227
+ }
228
+ log.warn('⚠ Make sure you have committed your changes to Git!');
229
+ log.plain('');
230
+ }
231
+
232
+ // Show skipped files
233
+ if (allSkipped.length > 0) {
234
+ log.info(`ℹ Skipped ${allSkipped.length} file(s) (you chose not to overwrite)`);
119
235
  }
120
236
 
121
237
  // Add watermark if provided
@@ -149,29 +265,131 @@ export const addCommand = new Command('add')
149
265
  }
150
266
  }
151
267
 
152
- // Update journal
268
+ // Hash files and update journal
153
269
  if (!options.dryRun) {
270
+ log.info('Computing file hashes...');
271
+ const fileHashes = await hashFiles(copiedFiles, { showProgress: copiedFiles.length > 20 });
272
+
273
+ const fileEntries: FileEntry[] = Array.from(fileHashes.entries()).map(([path, hash]) => ({
274
+ path,
275
+ hash,
276
+ }));
277
+
154
278
  await addEntry(paths.journalFile, {
155
279
  feature: manifest.name,
156
280
  target: manifest.target,
157
- files: copiedFiles,
281
+ files: fileEntries,
158
282
  insertedNav: navInserted,
159
283
  navHref,
160
284
  navLabel,
161
285
  ts: Date.now(),
286
+ manifest: {
287
+ version: manifest.version,
288
+ manualSteps: manifest.manualSteps,
289
+ env: manifest.env,
290
+ },
162
291
  });
163
292
  }
164
293
 
165
294
  log.success(`${manifest.name} installed successfully!`);
166
295
  log.info(`Files added: ${copiedFiles.length}`);
167
296
 
297
+ // Show required packages
298
+ if (manifest.dependencies) {
299
+ log.plain('');
300
+ log.warn('⚠ This feature requires additional packages');
301
+ log.plain('');
302
+
303
+ if (manifest.target === 'native' && manifest.dependencies.expo) {
304
+ const packages = manifest.dependencies.expo;
305
+
306
+ log.info('📦 Required packages:');
307
+ packages.forEach(pkg => {
308
+ log.plain(` • ${pkg}`);
309
+ });
310
+ log.plain('');
311
+
312
+ log.info('Install with:');
313
+ log.plain(` npx expo install ${packages.join(' ')}`);
314
+ log.plain('');
315
+ log.info('💡 Expo will automatically pick compatible versions');
316
+
317
+ } else if (manifest.target === 'web' && manifest.dependencies.npm) {
318
+ const packages = manifest.dependencies.npm;
319
+
320
+ log.info('📦 Required packages:');
321
+ packages.forEach(pkg => {
322
+ log.plain(` • ${pkg}`);
323
+ });
324
+ log.plain('');
325
+
326
+ log.info('Install with:');
327
+ log.plain(` pnpm add ${packages.join(' ')}`);
328
+ log.plain(' OR');
329
+ log.plain(` yarn add ${packages.join(' ')}`);
330
+ log.plain(` OR`);
331
+ log.plain(` npm install ${packages.join(' ')}`);
332
+ }
333
+
334
+ log.plain('');
335
+ }
336
+
337
+ // Show manual steps
338
+ if (manifest.manualSteps && !options.dryRun) {
339
+ log.plain('');
340
+ log.warn('⚠ MANUAL STEPS REQUIRED:');
341
+ log.plain('');
342
+ log.plain('This feature requires some manual configuration:');
343
+ log.plain('');
344
+
345
+ manifest.manualSteps.forEach((step, index) => {
346
+ log.plain(`Step ${index + 1}: ${step.title}`);
347
+ log.plain(` ${step.description}`);
348
+ if (step.link) {
349
+ log.plain(` 🔗 ${step.link}`);
350
+ }
351
+ if (step.file) {
352
+ log.plain(` 📝 File: ${step.file}`);
353
+ }
354
+ if (step.content) {
355
+ log.plain(` Add: ${step.content}`);
356
+ }
357
+ log.plain('');
358
+ });
359
+
360
+ log.info(`💡 Run 'vf checklist ${manifest.name}' to see these steps again`);
361
+ log.plain('');
362
+ }
363
+
364
+ // Show environment variables
365
+ if (manifest.env && !options.dryRun) {
366
+ log.plain('');
367
+ log.warn('⚠ REQUIRED ENVIRONMENT VARIABLES:');
368
+ log.plain('');
369
+
370
+ manifest.env.forEach(envVar => {
371
+ log.plain(` ${envVar.key}`);
372
+ log.plain(` ${envVar.description}`);
373
+ log.plain(` Example: ${envVar.example}`);
374
+ if (envVar.link) {
375
+ log.plain(` Get it: ${envVar.link}`);
376
+ }
377
+ log.plain('');
378
+ });
379
+
380
+ log.info('Add these to your .env file');
381
+ log.plain('');
382
+ }
383
+
384
+ // Post-install message
385
+ if (manifest.postInstall?.message && !options.dryRun) {
386
+ log.plain('');
387
+ log.info(manifest.postInstall.message);
388
+ }
389
+
168
390
  if (options.dryRun) {
391
+ log.plain('');
169
392
  log.warn('This was a dry run. Run without --dry-run to apply changes.');
170
- } else {
171
- log.info('Next steps:');
172
- log.plain(` 1. Review the changes in your repo`);
173
- log.plain(` 2. Run your dev server to test`);
174
- log.plain(` 3. Navigate to the new feature`);
175
393
  }
176
394
  } catch (error: any) {
177
395
  log.error(`Installation failed: ${error.message}`);
@@ -0,0 +1,71 @@
1
+ import { Command } from 'commander';
2
+ import { log } from '../core/log.js';
3
+ import { getPaths } from '../core/paths.js';
4
+ import { getEntry } from '../core/journal.js';
5
+
6
+ export const checklistCommand = new Command('checklist')
7
+ .description('Show manual setup steps for an installed feature')
8
+ .argument('<feature>', 'Feature name')
9
+ .option('--target <target>', 'Target platform (native or web)', 'native')
10
+ .action(async (feature: string, options) => {
11
+ try {
12
+ const paths = getPaths();
13
+ const target = options.target as 'native' | 'web';
14
+
15
+ // Check if feature is installed
16
+ const entry = await getEntry(paths.journalFile, feature, target);
17
+ if (!entry) {
18
+ log.error(`${feature} is not installed for ${target}`);
19
+ log.info('Run "vf status" to see installed features');
20
+ process.exit(1);
21
+ }
22
+
23
+ // Check if feature has manual steps
24
+ if (!entry.manifest?.manualSteps || entry.manifest.manualSteps.length === 0) {
25
+ log.info(`${feature} has no manual setup steps`);
26
+ log.success('This feature is ready to use!');
27
+ return;
28
+ }
29
+
30
+ // Display manual steps
31
+ log.info(`Manual setup steps for ${feature}:`);
32
+ log.plain('');
33
+
34
+ entry.manifest.manualSteps.forEach((step: any, index: number) => {
35
+ log.plain(`Step ${index + 1}: ${step.title}`);
36
+ log.plain(` ${step.description}`);
37
+ if (step.link) {
38
+ log.plain(` 🔗 ${step.link}`);
39
+ }
40
+ if (step.file) {
41
+ log.plain(` 📝 File: ${step.file}`);
42
+ }
43
+ if (step.content) {
44
+ log.plain(` Add: ${step.content}`);
45
+ }
46
+ log.plain('');
47
+ });
48
+
49
+ // Display environment variables if any
50
+ if (entry.manifest.env && entry.manifest.env.length > 0) {
51
+ log.warn('⚠ REQUIRED ENVIRONMENT VARIABLES:');
52
+ log.plain('');
53
+
54
+ entry.manifest.env.forEach((envVar: any) => {
55
+ log.plain(` ${envVar.key}`);
56
+ log.plain(` ${envVar.description}`);
57
+ log.plain(` Example: ${envVar.example}`);
58
+ if (envVar.link) {
59
+ log.plain(` Get it: ${envVar.link}`);
60
+ }
61
+ log.plain('');
62
+ });
63
+
64
+ log.info('Add these to your .env file');
65
+ }
66
+
67
+ } catch (error: any) {
68
+ log.error(`Failed to show checklist: ${error.message}`);
69
+ process.exit(1);
70
+ }
71
+ });
@@ -2,15 +2,19 @@ import { Command } from 'commander';
2
2
  import { log } from '../core/log.js';
3
3
  import { getPaths } from '../core/paths.js';
4
4
  import { validateSignature, validateTarget } from '../core/validate.js';
5
- import { getEntry, removeEntry } from '../core/journal.js';
5
+ import { getEntry, removeEntry, FileEntry } from '../core/journal.js';
6
6
  import { deleteFile } from '../core/fsx.js';
7
7
  import { removeNavLinkNative, removeNavLinkWeb } from '../core/codemod.js';
8
+ import { hashFile } from '../core/hash.js';
9
+ import { promptYesNo } from '../core/prompt.js';
8
10
 
9
11
  export const removeCommand = new Command('remove')
10
12
  .description('Remove a VibeFast feature from your project')
11
13
  .argument('<feature>', 'Feature name to remove')
12
14
  .option('--target <target>', 'Target platform (native or web)', 'native')
13
15
  .option('--dry-run', 'Preview changes without applying')
16
+ .option('--force', 'Skip modification check')
17
+ .option('--yes', 'Answer yes to all prompts')
14
18
  .action(async (feature: string, options) => {
15
19
  try {
16
20
  const paths = getPaths();
@@ -35,8 +39,99 @@ export const removeCommand = new Command('remove')
35
39
 
36
40
  log.info(`Removing ${feature} from ${target}...`);
37
41
 
42
+ // Check for modifications (unless --force)
43
+ if (!options.force && !options.dryRun) {
44
+ log.info('Checking for file modifications...');
45
+
46
+ const modifiedFiles: Array<{ path: string; status: 'modified' | 'deleted' }> = [];
47
+
48
+ // Get file paths from entry
49
+ const filePaths = Array.isArray(entry.files) && entry.files.length > 0
50
+ ? typeof entry.files[0] === 'string'
51
+ ? entry.files as string[]
52
+ : (entry.files as FileEntry[]).map(f => f.path)
53
+ : [];
54
+
55
+ const fileHashes = Array.isArray(entry.files) && entry.files.length > 0 && typeof entry.files[0] !== 'string'
56
+ ? new Map((entry.files as FileEntry[]).map(f => [f.path, f.hash]))
57
+ : new Map<string, string>();
58
+
59
+ for (const filePath of filePaths) {
60
+ const originalHash = fileHashes.get(filePath);
61
+
62
+ // Skip if we don't have original hash (old journal format)
63
+ if (!originalHash) continue;
64
+
65
+ const currentHash = await hashFile(filePath);
66
+
67
+ if (currentHash === '') {
68
+ // File was deleted by user
69
+ modifiedFiles.push({ path: filePath, status: 'deleted' });
70
+ } else if (currentHash !== originalHash) {
71
+ // File was modified by user
72
+ modifiedFiles.push({ path: filePath, status: 'modified' });
73
+ }
74
+ }
75
+
76
+ // Warn about modifications
77
+ if (modifiedFiles.length > 0) {
78
+ log.plain('');
79
+ log.warn('⚠ WARNING: The following files were changed:');
80
+ log.plain('');
81
+
82
+ const modified = modifiedFiles.filter(f => f.status === 'modified');
83
+ const deleted = modifiedFiles.filter(f => f.status === 'deleted');
84
+
85
+ if (modified.length > 0) {
86
+ log.plain(' Modified by you:');
87
+ modified.slice(0, 10).forEach(f => {
88
+ const relativePath = f.path.replace(paths.cwd + '/', '');
89
+ log.plain(` • ${relativePath}`);
90
+ });
91
+ if (modified.length > 10) {
92
+ log.plain(` ... and ${modified.length - 10} more`);
93
+ }
94
+ }
95
+
96
+ if (deleted.length > 0) {
97
+ log.plain('');
98
+ log.plain(' Already deleted:');
99
+ deleted.slice(0, 5).forEach(f => {
100
+ const relativePath = f.path.replace(paths.cwd + '/', '');
101
+ log.plain(` • ${relativePath}`);
102
+ });
103
+ if (deleted.length > 5) {
104
+ log.plain(` ... and ${deleted.length - 5} more`);
105
+ }
106
+ }
107
+
108
+ log.plain('');
109
+ log.warn('⚠ Your changes will be LOST if you continue!');
110
+ log.info('💡 Make sure you have committed to Git.');
111
+ log.plain('');
112
+
113
+ if (!options.yes) {
114
+ const shouldContinue = promptYesNo(
115
+ 'Continue with removal? (y/N): ',
116
+ false
117
+ );
118
+
119
+ if (!shouldContinue) {
120
+ log.info('Removal cancelled');
121
+ return;
122
+ }
123
+ }
124
+ }
125
+ }
126
+
38
127
  // Delete files
39
- for (const file of entry.files) {
128
+ const filePaths = Array.isArray(entry.files) && entry.files.length > 0
129
+ ? typeof entry.files[0] === 'string'
130
+ ? entry.files as string[]
131
+ : (entry.files as FileEntry[]).map(f => f.path)
132
+ : [];
133
+
134
+ for (const file of filePaths) {
40
135
  log.info(`Deleting ${file}`);
41
136
  if (!options.dryRun) {
42
137
  await deleteFile(file);
@@ -66,7 +161,14 @@ export const removeCommand = new Command('remove')
66
161
  }
67
162
 
68
163
  log.success(`${feature} removed successfully!`);
69
- log.info(`Files deleted: ${entry.files.length}`);
164
+
165
+ const fileCount = Array.isArray(entry.files) && entry.files.length > 0
166
+ ? typeof entry.files[0] === 'string'
167
+ ? entry.files.length
168
+ : (entry.files as FileEntry[]).length
169
+ : 0;
170
+
171
+ log.info(`Files deleted: ${fileCount}`);
70
172
 
71
173
  if (options.dryRun) {
72
174
  log.warn('This was a dry run. Run without --dry-run to apply changes.');
@@ -0,0 +1,47 @@
1
+ import { Command } from 'commander';
2
+ import { log } from '../core/log.js';
3
+ import { getPaths } from '../core/paths.js';
4
+ import { readJournal, FileEntry } from '../core/journal.js';
5
+
6
+ export const statusCommand = new Command('status')
7
+ .description('Show installed features and their status')
8
+ .action(async () => {
9
+ try {
10
+ const paths = getPaths();
11
+ const journal = await readJournal(paths.journalFile);
12
+
13
+ if (journal.entries.length === 0) {
14
+ log.info('No features installed yet');
15
+ log.info('Run "vf list" to see available features');
16
+ return;
17
+ }
18
+
19
+ log.info('Installed features:');
20
+ log.plain('');
21
+
22
+ for (const entry of journal.entries) {
23
+ const version = entry.manifest?.version || 'unknown';
24
+ const fileCount = Array.isArray(entry.files) && entry.files.length > 0
25
+ ? typeof entry.files[0] === 'string'
26
+ ? entry.files.length
27
+ : (entry.files as FileEntry[]).length
28
+ : 0;
29
+
30
+ log.plain(` ✓ ${entry.feature} (v${version}) - ${entry.target}`);
31
+ log.plain(` Files: ${fileCount}`);
32
+ log.plain(` Installed: ${new Date(entry.ts).toLocaleDateString()}`);
33
+
34
+ if (entry.manifest?.manualSteps && entry.manifest.manualSteps.length > 0) {
35
+ log.plain(` ⚠ Has manual setup steps (run: vf checklist ${entry.feature})`);
36
+ }
37
+
38
+ log.plain('');
39
+ }
40
+
41
+ log.info(`Total: ${journal.entries.length} feature(s) installed`);
42
+
43
+ } catch (error: any) {
44
+ log.error(`Failed to show status: ${error.message}`);
45
+ process.exit(1);
46
+ }
47
+ });