vibefast-cli 0.1.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 (250) hide show
  1. package/FINAL-STATUS.md +144 -0
  2. package/HOW-IT-WORKS.md +559 -0
  3. package/PLAN.md +453 -0
  4. package/README.md +129 -0
  5. package/RECIPES-READY.md +172 -0
  6. package/STATUS.md +199 -0
  7. package/SUCCESS.md +259 -0
  8. package/TESTING-CHECKLIST.md +450 -0
  9. package/cloudflare-worker/.wrangler/state/v3/kv/64907821e2634080acce34618d2f3d4c/blobs/11f2769953c717e188062bc644da97c1fd1e4d6d0813a226ce7567dba759afab0000019a736fb8d4 +1 -0
  10. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite +0 -0
  11. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-shm +0 -0
  12. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-wal +0 -0
  13. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
  14. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
  15. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
  16. package/cloudflare-worker/.wrangler/state/v3/r2/vibefast-recipes/blobs/620e8cf7c35d9806da25dee237e1d7e8b2432bd98f755b60e2c7f08a48d2c7b90000019a73736484 +0 -0
  17. package/cloudflare-worker/MIGRATION.md +160 -0
  18. package/cloudflare-worker/QUICKSTART.md +200 -0
  19. package/cloudflare-worker/README.md +242 -0
  20. package/cloudflare-worker/generate-token.js +32 -0
  21. package/cloudflare-worker/mini-native@latest.zip +0 -0
  22. package/cloudflare-worker/setup.sh +143 -0
  23. package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +15 -0
  24. package/cloudflare-worker/test-recipe/recipe.json +16 -0
  25. package/cloudflare-worker/worker.js +308 -0
  26. package/cloudflare-worker/wrangler.toml +13 -0
  27. package/dist/commands/add.d.ts +3 -0
  28. package/dist/commands/add.d.ts.map +1 -0
  29. package/dist/commands/add.js +149 -0
  30. package/dist/commands/add.js.map +1 -0
  31. package/dist/commands/devices.d.ts +3 -0
  32. package/dist/commands/devices.d.ts.map +1 -0
  33. package/dist/commands/devices.js +35 -0
  34. package/dist/commands/devices.js.map +1 -0
  35. package/dist/commands/doctor.d.ts +3 -0
  36. package/dist/commands/doctor.d.ts.map +1 -0
  37. package/dist/commands/doctor.js +67 -0
  38. package/dist/commands/doctor.js.map +1 -0
  39. package/dist/commands/list.d.ts +3 -0
  40. package/dist/commands/list.d.ts.map +1 -0
  41. package/dist/commands/list.js +40 -0
  42. package/dist/commands/list.js.map +1 -0
  43. package/dist/commands/login.d.ts +3 -0
  44. package/dist/commands/login.d.ts.map +1 -0
  45. package/dist/commands/login.js +23 -0
  46. package/dist/commands/login.js.map +1 -0
  47. package/dist/commands/logout.d.ts +3 -0
  48. package/dist/commands/logout.d.ts.map +1 -0
  49. package/dist/commands/logout.js +16 -0
  50. package/dist/commands/logout.js.map +1 -0
  51. package/dist/commands/remove.d.ts +3 -0
  52. package/dist/commands/remove.d.ts.map +1 -0
  53. package/dist/commands/remove.js +67 -0
  54. package/dist/commands/remove.js.map +1 -0
  55. package/dist/core/__tests__/journal.test.d.ts +2 -0
  56. package/dist/core/__tests__/journal.test.d.ts.map +1 -0
  57. package/dist/core/__tests__/journal.test.js +101 -0
  58. package/dist/core/__tests__/journal.test.js.map +1 -0
  59. package/dist/core/__tests__/validate.test.d.ts +2 -0
  60. package/dist/core/__tests__/validate.test.d.ts.map +1 -0
  61. package/dist/core/__tests__/validate.test.js +53 -0
  62. package/dist/core/__tests__/validate.test.js.map +1 -0
  63. package/dist/core/archive.d.ts +2 -0
  64. package/dist/core/archive.d.ts.map +1 -0
  65. package/dist/core/archive.js +59 -0
  66. package/dist/core/archive.js.map +1 -0
  67. package/dist/core/auth.d.ts +15 -0
  68. package/dist/core/auth.d.ts.map +1 -0
  69. package/dist/core/auth.js +76 -0
  70. package/dist/core/auth.js.map +1 -0
  71. package/dist/core/codemod.d.ts +20 -0
  72. package/dist/core/codemod.d.ts.map +1 -0
  73. package/dist/core/codemod.js +150 -0
  74. package/dist/core/codemod.js.map +1 -0
  75. package/dist/core/fsx.d.ts +12 -0
  76. package/dist/core/fsx.d.ts.map +1 -0
  77. package/dist/core/fsx.js +70 -0
  78. package/dist/core/fsx.js.map +1 -0
  79. package/dist/core/http.d.ts +30 -0
  80. package/dist/core/http.d.ts.map +1 -0
  81. package/dist/core/http.js +95 -0
  82. package/dist/core/http.js.map +1 -0
  83. package/dist/core/journal.d.ts +18 -0
  84. package/dist/core/journal.d.ts.map +1 -0
  85. package/dist/core/journal.js +34 -0
  86. package/dist/core/journal.js.map +1 -0
  87. package/dist/core/log.d.ts +8 -0
  88. package/dist/core/log.d.ts.map +1 -0
  89. package/dist/core/log.js +9 -0
  90. package/dist/core/log.js.map +1 -0
  91. package/dist/core/pathGuard.d.ts +3 -0
  92. package/dist/core/pathGuard.d.ts.map +1 -0
  93. package/dist/core/pathGuard.js +18 -0
  94. package/dist/core/pathGuard.js.map +1 -0
  95. package/dist/core/paths.d.ts +11 -0
  96. package/dist/core/paths.d.ts.map +1 -0
  97. package/dist/core/paths.js +22 -0
  98. package/dist/core/paths.js.map +1 -0
  99. package/dist/core/validate.d.ts +8 -0
  100. package/dist/core/validate.d.ts.map +1 -0
  101. package/dist/core/validate.js +27 -0
  102. package/dist/core/validate.js.map +1 -0
  103. package/dist/index.d.ts +3 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +23 -0
  106. package/dist/index.js.map +1 -0
  107. package/docs/decisions.md +55 -0
  108. package/package.json +39 -0
  109. package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
  110. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
  111. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
  112. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
  113. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/index.ts +4 -0
  114. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
  115. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
  116. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
  117. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
  118. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
  119. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
  120. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
  121. package/recipes/audio-recorder/recipe.json +22 -0
  122. package/recipes/audio-recorder@latest.zip +0 -0
  123. package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
  124. package/recipes/charts/apps/native/src/features/charts/README.md +185 -0
  125. package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +223 -0
  126. package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +40 -0
  127. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +143 -0
  128. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +196 -0
  129. package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +65 -0
  130. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +143 -0
  131. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +246 -0
  132. package/recipes/charts/apps/native/src/features/charts/components/index.ts +10 -0
  133. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +308 -0
  134. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +180 -0
  135. package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +188 -0
  136. package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +265 -0
  137. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +322 -0
  138. package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +183 -0
  139. package/recipes/charts/apps/native/src/features/charts/types/index.ts +66 -0
  140. package/recipes/charts/recipe.json +22 -0
  141. package/recipes/charts@latest.zip +0 -0
  142. package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
  143. package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +302 -0
  144. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
  145. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +469 -0
  146. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
  147. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +246 -0
  148. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
  149. package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +115 -0
  150. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
  151. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
  152. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
  153. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
  154. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +173 -0
  155. package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
  156. package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
  157. package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
  158. package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +20 -0
  159. package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
  160. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +143 -0
  161. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +664 -0
  162. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +359 -0
  163. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
  164. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +79 -0
  165. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
  166. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
  167. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +207 -0
  168. package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +86 -0
  169. package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +162 -0
  170. package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +62 -0
  171. package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +40 -0
  172. package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +238 -0
  173. package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +180 -0
  174. package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +60 -0
  175. package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
  176. package/recipes/chatbot/recipe.json +22 -0
  177. package/recipes/chatbot@latest.zip +0 -0
  178. package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
  179. package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
  180. package/recipes/image-generator/apps/native/src/features/image-generator/app/_layout.tsx +25 -0
  181. package/recipes/image-generator/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
  182. package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +237 -0
  183. package/recipes/image-generator/apps/native/src/features/image-generator/components/gallery-image.tsx +26 -0
  184. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
  185. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +210 -0
  186. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
  187. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
  188. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
  189. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +93 -0
  190. package/recipes/image-generator/apps/native/src/features/image-generator/models/models.ts +66 -0
  191. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-gallery-service.ts +98 -0
  192. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-save-service.ts +121 -0
  193. package/recipes/image-generator/recipe.json +22 -0
  194. package/recipes/image-generator@latest.zip +0 -0
  195. package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
  196. package/recipes/quiz/apps/native/src/features/quiz/components/question.tsx +67 -0
  197. package/recipes/quiz/apps/native/src/features/quiz/config.ts +11 -0
  198. package/recipes/quiz/apps/native/src/features/quiz/index.tsx +133 -0
  199. package/recipes/quiz/recipe.json +22 -0
  200. package/recipes/quiz@latest.zip +0 -0
  201. package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
  202. package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +108 -0
  203. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/animated-number.tsx +102 -0
  204. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/calorie-card.tsx +66 -0
  205. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/circular-progress.tsx +97 -0
  206. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/floating-add-button.tsx +27 -0
  207. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/macro-card.tsx +80 -0
  208. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/promo-banner.tsx +98 -0
  209. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/recently-logged.tsx +64 -0
  210. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/week-calendar.tsx +68 -0
  211. package/recipes/tracker-app/recipe.json +22 -0
  212. package/recipes/tracker-app@latest.zip +0 -0
  213. package/recipes/upload-all.sh +32 -0
  214. package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +27 -0
  215. package/recipes/voice-bot/apps/native/src/features/voice-bot/README.md +185 -0
  216. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/conversation-status.tsx +76 -0
  217. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/index.ts +4 -0
  218. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/message-input.tsx +98 -0
  219. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-bot-screen.tsx +173 -0
  220. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-controls.tsx +73 -0
  221. package/recipes/voice-bot/apps/native/src/features/voice-bot/index.ts +3 -0
  222. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/index.ts +1 -0
  223. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/use-voice-bot.ts +161 -0
  224. package/recipes/voice-bot/apps/native/src/features/voice-bot/types.ts +29 -0
  225. package/recipes/voice-bot/recipe.json +22 -0
  226. package/recipes/voice-bot@latest.zip +0 -0
  227. package/scripts/create-recipes.mjs +189 -0
  228. package/src/commands/add.ts +183 -0
  229. package/src/commands/devices.ts +38 -0
  230. package/src/commands/doctor.ts +67 -0
  231. package/src/commands/list.ts +45 -0
  232. package/src/commands/login.ts +24 -0
  233. package/src/commands/logout.ts +15 -0
  234. package/src/commands/remove.ts +78 -0
  235. package/src/core/__tests__/journal.test.ts +119 -0
  236. package/src/core/__tests__/validate.test.ts +64 -0
  237. package/src/core/archive.ts +69 -0
  238. package/src/core/auth.ts +103 -0
  239. package/src/core/codemod.ts +211 -0
  240. package/src/core/fsx.ts +80 -0
  241. package/src/core/http.ts +136 -0
  242. package/src/core/journal.ts +64 -0
  243. package/src/core/log.ts +9 -0
  244. package/src/core/pathGuard.ts +22 -0
  245. package/src/core/paths.ts +33 -0
  246. package/src/core/validate.ts +44 -0
  247. package/src/index.ts +27 -0
  248. package/test-critical-cases.mjs +258 -0
  249. package/tsconfig.json +21 -0
  250. package/vitest.config.mts +12 -0
@@ -0,0 +1,45 @@
1
+ import { Command } from 'commander';
2
+ import { log } from '../core/log.js';
3
+ import { getToken } from '../core/auth.js';
4
+ import { listRecipes } from '../core/http.js';
5
+
6
+ export const listCommand = new Command('list')
7
+ .description('List available VibeFast features')
8
+ .action(async () => {
9
+ try {
10
+ const token = await getToken();
11
+ if (!token) {
12
+ log.error('Not logged in. Run "vf login --token <TOKEN>" first');
13
+ process.exit(1);
14
+ }
15
+
16
+ const response = await listRecipes(token);
17
+
18
+ if (!response.recipes || response.recipes.length === 0) {
19
+ log.info('No recipes available');
20
+ return;
21
+ }
22
+
23
+ const native = response.recipes.filter((r: any) => r.target === 'native');
24
+ const web = response.recipes.filter((r: any) => r.target === 'web');
25
+
26
+ if (native.length > 0) {
27
+ log.info('Native (Expo) features:');
28
+ native.forEach((recipe: any) => {
29
+ log.plain(` • ${recipe.name} - ${recipe.description}`);
30
+ });
31
+ }
32
+
33
+ if (web.length > 0) {
34
+ log.info('\nWeb (Next.js) features:');
35
+ web.forEach((recipe: any) => {
36
+ log.plain(` • ${recipe.name} - ${recipe.description}`);
37
+ });
38
+ }
39
+
40
+ log.plain('\nInstall with: vf add <feature-name>');
41
+ } catch (error: any) {
42
+ log.error(`Failed to list recipes: ${error.message}`);
43
+ process.exit(1);
44
+ }
45
+ });
@@ -0,0 +1,24 @@
1
+ import { Command } from 'commander';
2
+ import { log } from '../core/log.js';
3
+ import { saveToken } from '../core/auth.js';
4
+
5
+ export const loginCommand = new Command('login')
6
+ .description('Authenticate with your VibeFast token')
7
+ .requiredOption('--token <token>', 'Your VibeFast license token')
8
+ .action(async (options) => {
9
+ try {
10
+ const token = options.token.trim();
11
+
12
+ if (!token || token.length < 10) {
13
+ log.error('Invalid token format');
14
+ process.exit(1);
15
+ }
16
+
17
+ await saveToken(token);
18
+ log.success('Token saved successfully');
19
+ log.info('Run "vf doctor" to verify your setup');
20
+ } catch (error: any) {
21
+ log.error(`Login failed: ${error.message}`);
22
+ process.exit(1);
23
+ }
24
+ });
@@ -0,0 +1,15 @@
1
+ import { Command } from 'commander';
2
+ import { log } from '../core/log.js';
3
+ import { clearToken } from '../core/auth.js';
4
+
5
+ export const logoutCommand = new Command('logout')
6
+ .description('Clear your stored VibeFast token')
7
+ .action(async () => {
8
+ try {
9
+ await clearToken();
10
+ log.success('Token cleared');
11
+ } catch (error: any) {
12
+ log.error(`Logout failed: ${error.message}`);
13
+ process.exit(1);
14
+ }
15
+ });
@@ -0,0 +1,78 @@
1
+ import { Command } from 'commander';
2
+ import { log } from '../core/log.js';
3
+ import { getPaths } from '../core/paths.js';
4
+ import { validateSignature, validateTarget } from '../core/validate.js';
5
+ import { getEntry, removeEntry } from '../core/journal.js';
6
+ import { deleteFile } from '../core/fsx.js';
7
+ import { removeNavLinkNative, removeNavLinkWeb } from '../core/codemod.js';
8
+
9
+ export const removeCommand = new Command('remove')
10
+ .description('Remove a VibeFast feature from your project')
11
+ .argument('<feature>', 'Feature name to remove')
12
+ .option('--target <target>', 'Target platform (native or web)', 'native')
13
+ .option('--dry-run', 'Preview changes without applying')
14
+ .action(async (feature: string, options) => {
15
+ try {
16
+ const paths = getPaths();
17
+ const target = options.target as 'native' | 'web';
18
+
19
+ // Validate setup
20
+ const config = await validateSignature(paths.signatureFile);
21
+ validateTarget(target, config.targets);
22
+
23
+ if (options.dryRun) {
24
+ log.info('[DRY RUN] No changes will be made');
25
+ }
26
+
27
+ // Get journal entry (do not mutate journal yet)
28
+ const entry = await getEntry(paths.journalFile, feature, target);
29
+
30
+ if (!entry) {
31
+ log.warn(`${feature} is not installed for ${target}`);
32
+ log.info('Run "vf list" to see installed features');
33
+ return;
34
+ }
35
+
36
+ log.info(`Removing ${feature} from ${target}...`);
37
+
38
+ // Delete files
39
+ for (const file of entry.files) {
40
+ log.info(`Deleting ${file}`);
41
+ if (!options.dryRun) {
42
+ await deleteFile(file);
43
+ }
44
+ }
45
+
46
+ // Remove nav link
47
+ if (entry.insertedNav) {
48
+ log.info('Removing navigation link...');
49
+ const navFile = target === 'native' ? paths.nativeNavFile : paths.webNavFile;
50
+ const removeFn = target === 'native' ? removeNavLinkNative : removeNavLinkWeb;
51
+
52
+ const href = entry.navHref
53
+ ? entry.navHref
54
+ : target === 'native'
55
+ ? `/(root)/(protected)/${feature}`
56
+ : `/${feature}`;
57
+
58
+ if (!options.dryRun) {
59
+ await removeFn(navFile, href);
60
+ }
61
+ log.success('Navigation link removed');
62
+ }
63
+
64
+ if (!options.dryRun) {
65
+ await removeEntry(paths.journalFile, entry.feature, entry.target);
66
+ }
67
+
68
+ log.success(`${feature} removed successfully!`);
69
+ log.info(`Files deleted: ${entry.files.length}`);
70
+
71
+ if (options.dryRun) {
72
+ log.warn('This was a dry run. Run without --dry-run to apply changes.');
73
+ }
74
+ } catch (error: any) {
75
+ log.error(`Removal failed: ${error.message}`);
76
+ process.exit(1);
77
+ }
78
+ });
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { readJournal, writeJournal, addEntry, removeEntry, getEntry } from '../journal.js';
3
+ import { deleteFile } from '../fsx.js';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ import { randomUUID } from 'crypto';
7
+
8
+ describe('journal', () => {
9
+ let testDir: string;
10
+ let journalPath: string;
11
+
12
+ beforeEach(async () => {
13
+ testDir = join(tmpdir(), 'vibefast-test', randomUUID());
14
+ journalPath = join(testDir, '.vibefast', 'journal.json');
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await deleteFile(testDir);
19
+ });
20
+
21
+ it('should return empty journal if file missing', async () => {
22
+ const journal = await readJournal(journalPath);
23
+ expect(journal.entries).toEqual([]);
24
+ });
25
+
26
+ it('should write and read journal', async () => {
27
+ const journal = {
28
+ entries: [
29
+ {
30
+ feature: 'test',
31
+ target: 'native' as const,
32
+ files: ['/test/file.ts'],
33
+ insertedNav: true,
34
+ ts: Date.now(),
35
+ },
36
+ ],
37
+ };
38
+
39
+ await writeJournal(journalPath, journal);
40
+ const read = await readJournal(journalPath);
41
+ expect(read.entries).toHaveLength(1);
42
+ expect(read.entries[0].feature).toBe('test');
43
+ });
44
+
45
+ it('should add entry', async () => {
46
+ const entry = {
47
+ feature: 'mini-native',
48
+ target: 'native' as const,
49
+ files: ['/apps/native/src/app/mini/index.tsx'],
50
+ insertedNav: true,
51
+ ts: Date.now(),
52
+ };
53
+
54
+ await addEntry(journalPath, entry);
55
+ const journal = await readJournal(journalPath);
56
+ expect(journal.entries).toHaveLength(1);
57
+ expect(journal.entries[0].feature).toBe('mini-native');
58
+ });
59
+
60
+ it('should replace existing entry for same feature+target', async () => {
61
+ const entry1 = {
62
+ feature: 'test',
63
+ target: 'native' as const,
64
+ files: ['/file1.ts'],
65
+ insertedNav: true,
66
+ ts: 1000,
67
+ };
68
+
69
+ const entry2 = {
70
+ feature: 'test',
71
+ target: 'native' as const,
72
+ files: ['/file2.ts'],
73
+ insertedNav: false,
74
+ ts: 2000,
75
+ };
76
+
77
+ await addEntry(journalPath, entry1);
78
+ await addEntry(journalPath, entry2);
79
+
80
+ const journal = await readJournal(journalPath);
81
+ expect(journal.entries).toHaveLength(1);
82
+ expect(journal.entries[0].ts).toBe(2000);
83
+ });
84
+
85
+ it('should remove entry', async () => {
86
+ const entry = {
87
+ feature: 'test',
88
+ target: 'native' as const,
89
+ files: ['/file.ts'],
90
+ insertedNav: true,
91
+ ts: Date.now(),
92
+ };
93
+
94
+ await addEntry(journalPath, entry);
95
+ const removed = await removeEntry(journalPath, 'test', 'native');
96
+
97
+ expect(removed).not.toBeNull();
98
+ expect(removed?.feature).toBe('test');
99
+
100
+ const journal = await readJournal(journalPath);
101
+ expect(journal.entries).toHaveLength(0);
102
+ });
103
+
104
+ it('should get entry', async () => {
105
+ const entry = {
106
+ feature: 'test',
107
+ target: 'native' as const,
108
+ files: ['/file.ts'],
109
+ insertedNav: true,
110
+ ts: Date.now(),
111
+ };
112
+
113
+ await addEntry(journalPath, entry);
114
+ const found = await getEntry(journalPath, 'test', 'native');
115
+
116
+ expect(found).not.toBeNull();
117
+ expect(found?.feature).toBe('test');
118
+ });
119
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { validateSignature, validateTarget } from '../validate.js';
3
+ import { writeFileContent, deleteFile } from '../fsx.js';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ import { randomUUID } from 'crypto';
7
+
8
+ describe('validate', () => {
9
+ let testDir: string;
10
+ let signaturePath: string;
11
+
12
+ beforeEach(async () => {
13
+ testDir = join(tmpdir(), 'vibefast-test', randomUUID());
14
+ signaturePath = join(testDir, '.vibefast', 'starter.json');
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await deleteFile(testDir);
19
+ });
20
+
21
+ describe('validateSignature', () => {
22
+ it('should validate correct signature', async () => {
23
+ const config = {
24
+ name: 'vibefast',
25
+ version: '1.0.0',
26
+ targets: ['native', 'web'],
27
+ };
28
+ await writeFileContent(signaturePath, JSON.stringify(config));
29
+
30
+ const result = await validateSignature(signaturePath);
31
+ expect(result.name).toBe('vibefast');
32
+ expect(result.version).toBe('1.0.0');
33
+ expect(result.targets).toEqual(['native', 'web']);
34
+ });
35
+
36
+ it('should throw if file missing', async () => {
37
+ await expect(validateSignature(signaturePath)).rejects.toThrow('Not a VibeFast repo');
38
+ });
39
+
40
+ it('should throw if name is wrong', async () => {
41
+ const config = { name: 'wrong', version: '1.0.0', targets: ['native'] };
42
+ await writeFileContent(signaturePath, JSON.stringify(config));
43
+
44
+ await expect(validateSignature(signaturePath)).rejects.toThrow('Invalid starter name');
45
+ });
46
+
47
+ it('should throw if targets empty', async () => {
48
+ const config = { name: 'vibefast', version: '1.0.0', targets: [] };
49
+ await writeFileContent(signaturePath, JSON.stringify(config));
50
+
51
+ await expect(validateSignature(signaturePath)).rejects.toThrow('targets must be a non-empty array');
52
+ });
53
+ });
54
+
55
+ describe('validateTarget', () => {
56
+ it('should accept valid target', () => {
57
+ expect(() => validateTarget('native', ['native', 'web'])).not.toThrow();
58
+ });
59
+
60
+ it('should reject invalid target', () => {
61
+ expect(() => validateTarget('invalid', ['native', 'web'])).toThrow('Invalid target');
62
+ });
63
+ });
64
+ });
@@ -0,0 +1,69 @@
1
+ import yauzl from 'yauzl';
2
+ import { ensureDir, deleteFile } from './fsx.js';
3
+ import { dirname, resolve } from 'path';
4
+ import { pipeline } from 'stream/promises';
5
+ import { createWriteStream } from 'fs';
6
+ import { ensureWithinBase } from './pathGuard.js';
7
+
8
+ function isSymlink(entry: yauzl.Entry): boolean {
9
+ // Upper 4 bits of external attributes encode the file type on Unix
10
+ const fileType = (entry.externalFileAttributes >>> 28) & 0xf;
11
+ return fileType === 0xa;
12
+ }
13
+
14
+ export async function extractZipSafe(zipPath: string, destinationDir: string): Promise<void> {
15
+ const destRoot = resolve(destinationDir);
16
+ await deleteFile(destRoot);
17
+ await ensureDir(destRoot);
18
+
19
+ await new Promise<void>((resolvePromise, rejectPromise) => {
20
+ yauzl.open(zipPath, { lazyEntries: true }, (openErr, zipfile) => {
21
+ if (openErr || !zipfile) {
22
+ rejectPromise(openErr || new Error('Unable to read zip archive'));
23
+ return;
24
+ }
25
+
26
+ const bail = (err: Error) => {
27
+ zipfile.close();
28
+ rejectPromise(err);
29
+ };
30
+
31
+ zipfile.readEntry();
32
+
33
+ zipfile.on('entry', (entry: yauzl.Entry) => {
34
+ const normalizedName = entry.fileName.replace(/\\/g, '/');
35
+ const targetPath = ensureWithinBase(destRoot, resolve(destRoot, normalizedName), `Archive entry ${entry.fileName}`);
36
+
37
+ if (/\/$/.test(normalizedName)) {
38
+ ensureDir(targetPath)
39
+ .then(() => zipfile.readEntry())
40
+ .catch(bail);
41
+ return;
42
+ }
43
+
44
+ if (isSymlink(entry)) {
45
+ bail(new Error(`Archive entry ${entry.fileName} is a symlink, which is not supported`));
46
+ return;
47
+ }
48
+
49
+ zipfile.openReadStream(entry, (streamErr, readStream) => {
50
+ if (streamErr || !readStream) {
51
+ bail(streamErr || new Error(`Failed to read ${entry.fileName}`));
52
+ return;
53
+ }
54
+
55
+ ensureDir(dirname(targetPath))
56
+ .then(() => pipeline(readStream, createWriteStream(targetPath)))
57
+ .then(() => zipfile.readEntry())
58
+ .catch(bail);
59
+ });
60
+ });
61
+
62
+ zipfile.on('end', () => {
63
+ zipfile.close();
64
+ resolvePromise();
65
+ });
66
+ zipfile.on('error', bail);
67
+ });
68
+ });
69
+ }
@@ -0,0 +1,103 @@
1
+ import { readFileContent, writeFileContent, ensureDir, exists } from './fsx.js';
2
+ import { getConfigPath } from './paths.js';
3
+ import { randomUUID } from 'crypto';
4
+ import { hostname, userInfo, platform, arch } from 'os';
5
+ import { dirname } from 'path';
6
+
7
+ export interface AuthConfig {
8
+ token?: string;
9
+ deviceId?: string;
10
+ }
11
+
12
+ export interface DeviceInfo {
13
+ id: string;
14
+ os: string;
15
+ arch: string;
16
+ version: string;
17
+ }
18
+
19
+ export async function saveToken(token: string): Promise<void> {
20
+ const configPath = getConfigPath();
21
+ await ensureDir(dirname(configPath));
22
+
23
+ let config: AuthConfig = {};
24
+ if (await exists(configPath)) {
25
+ const content = await readFileContent(configPath);
26
+ config = JSON.parse(content);
27
+ }
28
+
29
+ config.token = token;
30
+
31
+ // Generate device ID if not exists
32
+ if (!config.deviceId) {
33
+ config.deviceId = generateDeviceId();
34
+ }
35
+
36
+ await writeFileContent(configPath, JSON.stringify(config, null, 2), { force: true });
37
+ }
38
+
39
+ export async function getToken(): Promise<string | null> {
40
+ const configPath = getConfigPath();
41
+ if (!(await exists(configPath))) {
42
+ return null;
43
+ }
44
+
45
+ const content = await readFileContent(configPath);
46
+ const config: AuthConfig = JSON.parse(content);
47
+ return config.token || null;
48
+ }
49
+
50
+ export async function clearToken(): Promise<void> {
51
+ const configPath = getConfigPath();
52
+ if (await exists(configPath)) {
53
+ const content = await readFileContent(configPath);
54
+ const config: AuthConfig = JSON.parse(content);
55
+ delete config.token;
56
+ await writeFileContent(configPath, JSON.stringify(config, null, 2), { force: true });
57
+ }
58
+ }
59
+
60
+ export async function getDeviceInfo(): Promise<DeviceInfo> {
61
+ const configPath = getConfigPath();
62
+ let deviceId: string;
63
+
64
+ if (await exists(configPath)) {
65
+ const content = await readFileContent(configPath);
66
+ const config: AuthConfig = JSON.parse(content);
67
+ deviceId = config.deviceId || generateDeviceId();
68
+
69
+ if (!config.deviceId) {
70
+ config.deviceId = deviceId;
71
+ await writeFileContent(configPath, JSON.stringify(config, null, 2), { force: true });
72
+ }
73
+ } else {
74
+ deviceId = generateDeviceId();
75
+ await ensureDir(dirname(configPath));
76
+ await writeFileContent(
77
+ configPath,
78
+ JSON.stringify({ deviceId }, null, 2),
79
+ { force: true }
80
+ );
81
+ }
82
+
83
+ return {
84
+ id: deviceId,
85
+ os: platform(),
86
+ arch: arch(),
87
+ version: '0.1.0',
88
+ };
89
+ }
90
+
91
+ function generateDeviceId(): string {
92
+ try {
93
+ const user = userInfo().username;
94
+ const host = hostname();
95
+ const os = platform();
96
+ const uuid = randomUUID();
97
+
98
+ // Create a stable but unique device ID
99
+ return `${user}-${host}-${os}-${uuid}`.slice(0, 64);
100
+ } catch {
101
+ return randomUUID();
102
+ }
103
+ }