vibefast-cli 0.4.0 → 0.5.1

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 (89) hide show
  1. package/FEATURE-DEPENDENCY-SPEC.md +338 -0
  2. package/dist/__tests__/integration.test.d.ts +2 -0
  3. package/dist/__tests__/integration.test.d.ts.map +1 -0
  4. package/dist/__tests__/integration.test.js +219 -0
  5. package/dist/__tests__/integration.test.js.map +1 -0
  6. package/dist/__tests__/recipes.test.d.ts +2 -0
  7. package/dist/__tests__/recipes.test.d.ts.map +1 -0
  8. package/dist/__tests__/recipes.test.js +143 -0
  9. package/dist/__tests__/recipes.test.js.map +1 -0
  10. package/dist/commands/__tests__/init.test.d.ts +2 -0
  11. package/dist/commands/__tests__/init.test.d.ts.map +1 -0
  12. package/dist/commands/__tests__/init.test.js +95 -0
  13. package/dist/commands/__tests__/init.test.js.map +1 -0
  14. package/dist/commands/__tests__/platform.test.d.ts +2 -0
  15. package/dist/commands/__tests__/platform.test.d.ts.map +1 -0
  16. package/dist/commands/__tests__/platform.test.js +123 -0
  17. package/dist/commands/__tests__/platform.test.js.map +1 -0
  18. package/dist/commands/add.d.ts.map +1 -1
  19. package/dist/commands/add.js +4 -5
  20. package/dist/commands/add.js.map +1 -1
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +12 -12
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/platform.d.ts +3 -0
  25. package/dist/commands/platform.d.ts.map +1 -0
  26. package/dist/commands/platform.js +245 -0
  27. package/dist/commands/platform.js.map +1 -0
  28. package/dist/core/journal.d.ts.map +1 -1
  29. package/dist/core/journal.js +36 -19
  30. package/dist/core/journal.js.map +1 -1
  31. package/dist/core/recipes.d.ts.map +1 -1
  32. package/dist/core/recipes.js +8 -39
  33. package/dist/core/recipes.js.map +1 -1
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -1
  36. package/package.json +1 -1
  37. package/recipes/ios-widget/recipe.json +78 -0
  38. package/recipes/ios-widget/targets/widget/AppIntent.swift +46 -0
  39. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png +0 -0
  40. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png +0 -0
  41. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png +0 -0
  42. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png +0 -0
  43. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png +0 -0
  44. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png +0 -0
  45. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png +0 -0
  46. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png +0 -0
  47. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png +0 -0
  48. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png +0 -0
  49. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png +0 -0
  50. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png +0 -0
  51. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png +0 -0
  52. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png +0 -0
  53. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  54. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png +0 -0
  55. package/recipes/ios-widget/targets/widget/CalorieTrackerWidget.swift +424 -0
  56. package/recipes/ios-widget/targets/widget/HabitTrackerWidget.swift +305 -0
  57. package/recipes/ios-widget/targets/widget/Info.plist +11 -0
  58. package/recipes/ios-widget/targets/widget/WidgetLiveActivity.swift +75 -0
  59. package/recipes/ios-widget/targets/widget/expo-target.config.js +10 -0
  60. package/recipes/ios-widget/targets/widget/generated.entitlements +5 -0
  61. package/recipes/ios-widget/targets/widget/index.swift +18 -0
  62. package/recipes/ios-widget/targets/widget/widgets.swift +96 -0
  63. package/recipes/ios-widget@latest.zip +0 -0
  64. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +74 -0
  65. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +25 -0
  66. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +23 -0
  67. package/recipes/payments/apps/native/src/features/payments/README.md +200 -0
  68. package/recipes/payments/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
  69. package/recipes/payments/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
  70. package/recipes/payments/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
  71. package/recipes/payments/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
  72. package/recipes/payments/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
  73. package/recipes/payments/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
  74. package/recipes/payments/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
  75. package/recipes/payments/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
  76. package/recipes/payments/apps/native/src/features/payments/index.ts +8 -0
  77. package/recipes/payments/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
  78. package/recipes/payments/recipe.json +58 -0
  79. package/recipes/payments@latest.zip +0 -0
  80. package/src/__tests__/integration.test.ts +249 -0
  81. package/src/__tests__/recipes.test.ts +168 -0
  82. package/src/commands/__tests__/init.test.ts +112 -0
  83. package/src/commands/__tests__/platform.test.ts +141 -0
  84. package/src/commands/add.ts +4 -5
  85. package/src/commands/init.ts +14 -15
  86. package/src/commands/platform.ts +309 -0
  87. package/src/core/journal.ts +42 -25
  88. package/src/core/recipes.ts +8 -40
  89. package/src/index.ts +2 -0
@@ -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';
@@ -42,17 +42,17 @@ export const initCommand = new Command('init')
42
42
  let selectedFeatures: string[] = [];
43
43
 
44
44
  if (!options.yes) {
45
- starterType = await promptSelect(
45
+ starterType = await promptSelectAsync(
46
46
  'How would you like to start?',
47
47
  [
48
48
  {
49
- name: 'Clean',
50
49
  value: 'clean',
50
+ label: 'Clean',
51
51
  description: 'Minimal setup - add features later with vf add',
52
52
  },
53
53
  {
54
- name: 'Custom',
55
54
  value: 'custom',
55
+ label: 'Custom',
56
56
  description: 'Choose features now - one-shot setup',
57
57
  },
58
58
  ]
@@ -100,22 +100,22 @@ export const initCommand = new Command('init')
100
100
  let platforms: string[] = ['native', 'web'];
101
101
 
102
102
  if (!options.yes) {
103
- const platformChoice = await promptSelect(
103
+ const platformChoice = await promptSelectAsync(
104
104
  'Which platforms do you want to include?',
105
105
  [
106
106
  {
107
- name: 'Both (Native + Web)',
108
107
  value: 'both',
108
+ label: 'Both (Native + Web)',
109
109
  description: 'Full monorepo with mobile and web app',
110
110
  },
111
111
  {
112
- name: 'Native Only (Expo)',
113
112
  value: 'native',
113
+ label: 'Native Only (Expo)',
114
114
  description: 'Mobile app only (iOS & Android)',
115
115
  },
116
116
  {
117
- name: 'Web Only (Next.js)',
118
117
  value: 'web',
118
+ label: 'Web Only (Next.js)',
119
119
  description: 'Web app only',
120
120
  },
121
121
  ]
@@ -142,10 +142,9 @@ export const initCommand = new Command('init')
142
142
  let projectName = defaultName;
143
143
 
144
144
  if (!options.yes) {
145
- projectName = await promptInput(
146
- 'Project name:',
147
- defaultName
148
- );
145
+ projectName = await promptUser(
146
+ 'Project name: '
147
+ ) || defaultName;
149
148
  }
150
149
 
151
150
  // Validate project name
@@ -300,7 +299,7 @@ export const initCommand = new Command('init')
300
299
  log.warn('pnpm is not installed');
301
300
  log.plain('');
302
301
 
303
- const shouldInstallPnpm = options.yes || await promptConfirm(
302
+ const shouldInstallPnpm = options.yes || await promptYesNo(
304
303
  'Would you like to install pnpm globally?',
305
304
  true
306
305
  );
@@ -383,7 +382,7 @@ export const initCommand = new Command('init')
383
382
  log.plain(' • Configure Convex Auth');
384
383
  log.plain('');
385
384
 
386
- const runSetup = options.yes || await promptConfirm(
385
+ const runSetup = options.yes || await promptYesNo(
387
386
  'Would you like to run the starter setup now?',
388
387
  true
389
388
  );
@@ -567,7 +566,7 @@ export const initCommand = new Command('init')
567
566
  process.exit(1);
568
567
  }
569
568
 
570
- licenseKey = await promptInput('Enter your license key:');
569
+ licenseKey = await promptUser('Enter your license key: ');
571
570
 
572
571
  if (!licenseKey || licenseKey.trim() === '') {
573
572
  log.error('License key cannot be empty');
@@ -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
+ );
@@ -30,35 +30,52 @@ export async function readJournal(journalPath: string): Promise<Journal> {
30
30
  if (!(await exists(journalPath))) {
31
31
  return { entries: [] };
32
32
  }
33
- const content = await readFileContent(journalPath);
34
- const journal = JSON.parse(content) as Journal;
35
33
 
36
- // Migrate old format to new format
37
- let needsMigration = false;
38
-
39
- for (const entry of journal.entries) {
40
- if (entry.files.length > 0 && typeof entry.files[0] === 'string') {
41
- needsMigration = true;
42
- // Old format: array of strings
43
- const oldFiles = entry.files as string[];
44
-
45
- // Convert to new format with hashes
46
- const newFiles: FileEntry[] = [];
47
- for (const filePath of oldFiles) {
48
- const hash = await hashFile(filePath);
49
- newFiles.push({ path: filePath, hash });
34
+ try {
35
+ const content = await readFileContent(journalPath);
36
+ const journal = JSON.parse(content) as Journal;
37
+
38
+ // Ensure entries is an array
39
+ if (!journal.entries || !Array.isArray(journal.entries)) {
40
+ console.warn('Invalid journal format, resetting to empty journal');
41
+ return { entries: [] };
42
+ }
43
+
44
+ // Migrate old format to new format
45
+ let needsMigration = false;
46
+
47
+ for (const entry of journal.entries) {
48
+ if (entry.files && entry.files.length > 0 && typeof entry.files[0] === 'string') {
49
+ needsMigration = true;
50
+ // Old format: array of strings
51
+ const oldFiles = entry.files as string[];
52
+
53
+ // Convert to new format with hashes
54
+ const newFiles: FileEntry[] = [];
55
+ for (const filePath of oldFiles) {
56
+ try {
57
+ const hash = await hashFile(filePath);
58
+ newFiles.push({ path: filePath, hash });
59
+ } catch (err) {
60
+ // File might not exist anymore, skip it
61
+ console.warn(`Could not hash file ${filePath}, skipping`);
62
+ }
63
+ }
64
+
65
+ entry.files = newFiles;
50
66
  }
51
-
52
- entry.files = newFiles;
53
67
  }
68
+
69
+ // Save migrated journal
70
+ if (needsMigration) {
71
+ await writeJournal(journalPath, journal);
72
+ }
73
+
74
+ return journal;
75
+ } catch (err) {
76
+ console.warn('Failed to read journal, resetting to empty journal:', err);
77
+ return { entries: [] };
54
78
  }
55
-
56
- // Save migrated journal
57
- if (needsMigration) {
58
- await writeJournal(journalPath, journal);
59
- }
60
-
61
- return journal;
62
79
  }
63
80
 
64
81
  export async function writeJournal(journalPath: string, journal: Journal): Promise<void> {
@@ -70,49 +70,17 @@ export const RECIPES: RecipeInfo[] = [
70
70
  description: 'Wake word detection with Vosk',
71
71
  icon: '🎙️',
72
72
  },
73
-
74
- // Advanced UI Components
75
73
  {
76
- name: 'glowing-button',
77
- category: 'advanced-ui',
78
- description: 'Animated glowing button with shimmer effect',
79
- icon: '',
80
- },
81
- {
82
- name: 'animated-switch',
83
- category: 'advanced-ui',
84
- description: 'Smooth animated toggle switch',
85
- icon: '🎚️',
86
- },
87
- {
88
- name: 'animated-chip',
89
- category: 'advanced-ui',
90
- description: 'Animated chip/tag component with press effects',
91
- icon: '🏷️',
92
- },
93
- {
94
- name: 'number-stepper',
95
- category: 'advanced-ui',
96
- description: 'Number input with +/- buttons',
97
- icon: '🔢',
98
- },
99
- {
100
- name: 'progress-circle',
101
- category: 'advanced-ui',
102
- description: 'Circular progress indicator with animation',
103
- icon: '⭕',
104
- },
105
- {
106
- name: 'swipe-slider',
107
- category: 'advanced-ui',
108
- description: 'Interactive swipe-to-confirm slider',
109
- icon: '👆',
74
+ name: 'ios-widget',
75
+ category: 'feature',
76
+ description: 'iOS Home Screen widgets with Live Activities',
77
+ icon: '📱',
110
78
  },
111
79
  {
112
- name: 'timeline',
113
- category: 'advanced-ui',
114
- description: 'Activity timeline view for feeds',
115
- icon: '📅',
80
+ name: 'payments',
81
+ category: 'feature',
82
+ description: 'In-app purchases and subscriptions with RevenueCat',
83
+ icon: '💳',
116
84
  },
117
85
  ];
118
86
 
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { statusCommand } from './commands/status.js';
14
14
  import { checklistCommand } from './commands/checklist.js';
15
15
  import { envCommand } from './commands/env.js';
16
16
  import { healthCommand } from './commands/health.js';
17
+ import { platformCommand } from './commands/platform.js';
17
18
  import { log } from './core/log.js';
18
19
  import { showCommandHint } from './core/errors.js';
19
20
 
@@ -32,6 +33,7 @@ program.addCommand(doctorCommand);
32
33
  program.addCommand(listCommand);
33
34
  program.addCommand(addCommand);
34
35
  program.addCommand(removeCommand);
36
+ program.addCommand(platformCommand);
35
37
  program.addCommand(statusCommand);
36
38
  program.addCommand(checklistCommand);
37
39
  program.addCommand(envCommand);