vibefast-cli 1.1.3 → 1.2.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 (300) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +63 -169
  3. package/dist/__tests__/recipes.test.js +25 -3
  4. package/dist/__tests__/recipes.test.js.map +1 -1
  5. package/dist/commands/add.d.ts +1 -1
  6. package/dist/commands/add.d.ts.map +1 -1
  7. package/dist/commands/add.js +547 -543
  8. package/dist/commands/add.js.map +1 -1
  9. package/dist/commands/checklist.d.ts +1 -1
  10. package/dist/commands/checklist.d.ts.map +1 -1
  11. package/dist/commands/checklist.js +40 -39
  12. package/dist/commands/checklist.js.map +1 -1
  13. package/dist/commands/doctor.d.ts +1 -1
  14. package/dist/commands/doctor.js +22 -22
  15. package/dist/commands/doctor.js.map +1 -1
  16. package/dist/commands/env.d.ts +1 -1
  17. package/dist/commands/env.d.ts.map +1 -1
  18. package/dist/commands/env.js +58 -53
  19. package/dist/commands/env.js.map +1 -1
  20. package/dist/commands/health.d.ts +1 -1
  21. package/dist/commands/health.d.ts.map +1 -1
  22. package/dist/commands/health.js +101 -93
  23. package/dist/commands/health.js.map +1 -1
  24. package/dist/commands/init.d.ts +1 -1
  25. package/dist/commands/init.d.ts.map +1 -1
  26. package/dist/commands/init.js +416 -296
  27. package/dist/commands/init.js.map +1 -1
  28. package/dist/commands/remove.d.ts +1 -1
  29. package/dist/commands/remove.d.ts.map +1 -1
  30. package/dist/commands/remove.js +77 -64
  31. package/dist/commands/remove.js.map +1 -1
  32. package/dist/commands/status.d.ts +1 -1
  33. package/dist/commands/status.d.ts.map +1 -1
  34. package/dist/commands/status.js +15 -14
  35. package/dist/commands/status.js.map +1 -1
  36. package/dist/core/__tests__/detect.test.js +68 -34
  37. package/dist/core/__tests__/detect.test.js.map +1 -1
  38. package/dist/core/ast.d.ts +14 -0
  39. package/dist/core/ast.d.ts.map +1 -0
  40. package/dist/core/ast.js +239 -0
  41. package/dist/core/ast.js.map +1 -0
  42. package/dist/core/codemod.d.ts.map +1 -1
  43. package/dist/core/codemod.js +62 -44
  44. package/dist/core/codemod.js.map +1 -1
  45. package/dist/core/config.d.ts +10 -0
  46. package/dist/core/config.d.ts.map +1 -0
  47. package/dist/core/config.js +51 -0
  48. package/dist/core/config.js.map +1 -0
  49. package/dist/core/detect.d.ts +8 -2
  50. package/dist/core/detect.d.ts.map +1 -1
  51. package/dist/core/detect.js +52 -21
  52. package/dist/core/detect.js.map +1 -1
  53. package/dist/core/errors.d.ts.map +1 -1
  54. package/dist/core/errors.js +9 -8
  55. package/dist/core/errors.js.map +1 -1
  56. package/dist/core/exec.d.ts +16 -0
  57. package/dist/core/exec.d.ts.map +1 -0
  58. package/dist/core/exec.js +48 -0
  59. package/dist/core/exec.js.map +1 -0
  60. package/dist/core/manualSteps.d.ts +7 -0
  61. package/dist/core/manualSteps.d.ts.map +1 -0
  62. package/dist/core/manualSteps.js +59 -0
  63. package/dist/core/manualSteps.js.map +1 -0
  64. package/dist/core/paths.d.ts +3 -1
  65. package/dist/core/paths.d.ts.map +1 -1
  66. package/dist/core/paths.js +14 -10
  67. package/dist/core/paths.js.map +1 -1
  68. package/dist/core/spinner.d.ts +1 -1
  69. package/dist/core/spinner.d.ts.map +1 -1
  70. package/dist/core/spinner.js +38 -8
  71. package/dist/core/spinner.js.map +1 -1
  72. package/dist/core/vosk.d.ts.map +1 -1
  73. package/dist/core/vosk.js +50 -39
  74. package/dist/core/vosk.js.map +1 -1
  75. package/docs/manual-testing.md +91 -0
  76. package/package.json +6 -3
  77. package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
  78. package/recipes/audio-recorder/recipe.json +3 -3
  79. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
  80. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
  81. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
  82. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/index.ts +4 -0
  83. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
  84. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
  85. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
  86. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
  87. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
  88. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
  89. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
  90. package/recipes/audio-recorder-supabase/packages/backend/src/services/recordings.ts +369 -0
  91. package/recipes/audio-recorder-supabase/packages/backend/supabase/migrations/recordings.sql +70 -0
  92. package/recipes/audio-recorder-supabase/recipe.json +35 -0
  93. package/recipes/audio-recorder-supabase@latest.zip +0 -0
  94. package/recipes/audio-recorder@latest.zip +0 -0
  95. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +3 -3
  96. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +2 -2
  97. package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +5 -5
  98. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +3 -3
  99. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +20 -4
  100. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +7 -6
  101. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +6 -4
  102. package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +1 -1
  103. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +5 -4
  104. package/recipes/charts/recipe.json +4 -13
  105. package/recipes/charts@latest.zip +0 -0
  106. package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
  107. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +86 -86
  108. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +86 -53
  109. package/recipes/chatbot/recipe.json +26 -92
  110. package/recipes/chatbot-supabase/apps/native/src/api-client/supabase/chatbot.ts +515 -0
  111. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/app/index.tsx +257 -0
  112. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
  113. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-input-bar.tsx +485 -0
  114. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
  115. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +223 -0
  116. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
  117. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/image-preview-list.tsx +116 -0
  118. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
  119. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
  120. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
  121. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
  122. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-list.tsx +170 -0
  123. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
  124. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
  125. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
  126. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/models.ts +20 -0
  127. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
  128. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +142 -0
  129. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-config.ts +458 -0
  130. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +429 -0
  131. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
  132. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-conversation.ts +90 -0
  133. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
  134. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
  135. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +213 -0
  136. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/index.ts +86 -0
  137. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/models.ts +162 -0
  138. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/providers.ts +62 -0
  139. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/types.ts +40 -0
  140. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/file-uploader.ts +287 -0
  141. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/message-handler-service.ts +189 -0
  142. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/types/index.ts +70 -0
  143. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
  144. package/recipes/chatbot-supabase/packages/backend/src/services/conversations.ts +243 -0
  145. package/recipes/chatbot-supabase/packages/backend/src/services/messages.ts +327 -0
  146. package/recipes/chatbot-supabase/packages/backend/supabase/functions/chat-stream/index.ts +347 -0
  147. package/recipes/chatbot-supabase/packages/backend/supabase/migrations/chatbot.sql +104 -0
  148. package/recipes/chatbot-supabase/recipe.json +79 -0
  149. package/recipes/chatbot-supabase@latest.zip +0 -0
  150. package/recipes/chatbot.zip +0 -0
  151. package/recipes/chatbot@latest.zip +0 -0
  152. package/recipes/image-analysis/packages/backend/convex/imageAnalysis/index.ts +2 -2
  153. package/recipes/image-analysis/packages/backend/convex/{imageAnalysisFunctions.ts → imageAnalysis.ts} +5 -5
  154. package/recipes/image-analysis/recipe.json +15 -55
  155. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/analysis-options-screen.tsx +304 -0
  156. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/camera.tsx +221 -0
  157. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/image-capture-screen.tsx +333 -0
  158. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading-screen.tsx +214 -0
  159. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading.tsx +191 -0
  160. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/results.tsx +137 -0
  161. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/trait-details.tsx +172 -0
  162. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-analysis-data.ts +160 -0
  163. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-results-screen.ts +151 -0
  164. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-badge.tsx +77 -0
  165. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-card.tsx +75 -0
  166. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-unlocked-modal.tsx +162 -0
  167. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievements-section.tsx +44 -0
  168. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/advice-list.tsx +42 -0
  169. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/circular-progress.tsx +233 -0
  170. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/content-card.tsx +38 -0
  171. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/error-state.tsx +42 -0
  172. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/index.ts +9 -0
  173. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/loading-state.tsx +26 -0
  174. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/profile-image.tsx +60 -0
  175. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/results-header.tsx +62 -0
  176. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/score-display.tsx +54 -0
  177. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/share-options-modal.tsx +110 -0
  178. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/traits-grid.tsx +74 -0
  179. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/analysis-config.ts +80 -0
  180. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/master-analysis-config.ts +157 -0
  181. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/index.ts +1 -0
  182. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-analysis.ts +38 -0
  183. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-image-analysis.ts +208 -0
  184. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/analysis-service.ts +262 -0
  185. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/share-service.ts +176 -0
  186. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/trait-details-service.ts +289 -0
  187. package/recipes/image-analysis-supabase/packages/backend/src/services/image-analyses.ts +132 -0
  188. package/recipes/image-analysis-supabase/packages/backend/supabase/functions/analyze-image/index.ts +312 -0
  189. package/recipes/image-analysis-supabase/packages/backend/supabase/migrations/image_analysis.sql +42 -0
  190. package/recipes/image-analysis-supabase/recipe.json +57 -0
  191. package/recipes/image-analysis-supabase@latest.zip +0 -0
  192. package/recipes/image-analysis@latest.zip +0 -0
  193. package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +16 -2
  194. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +11 -5
  195. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +11 -5
  196. package/recipes/image-generator/packages/backend/convex/imageGeneration/index.ts +2 -2
  197. package/recipes/image-generator/recipe.json +16 -39
  198. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/_layout.tsx +26 -0
  199. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
  200. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/index.tsx +251 -0
  201. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/gallery-image.tsx +25 -0
  202. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
  203. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-model-selector.tsx +216 -0
  204. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
  205. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
  206. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
  207. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator.ts +103 -0
  208. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/models/models.ts +66 -0
  209. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-gallery-service.ts +96 -0
  210. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-save-service.ts +120 -0
  211. package/recipes/image-generator-supabase/packages/backend/supabase/functions/generate-image/index.ts +291 -0
  212. package/recipes/image-generator-supabase/packages/backend/supabase/migrations/image_generator.sql +71 -0
  213. package/recipes/image-generator-supabase/recipe.json +59 -0
  214. package/recipes/image-generator-supabase@latest.zip +0 -0
  215. package/recipes/image-generator@latest.zip +0 -0
  216. package/recipes/ios-widget/recipe.json +15 -24
  217. package/recipes/ios-widget@latest.zip +0 -0
  218. package/recipes/onboarding/apps/native/src/features/onboarding/analytics/index.ts +9 -0
  219. package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding-with-analytics.tsx +141 -0
  220. package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding.tsx +173 -0
  221. package/recipes/onboarding/apps/native/src/features/onboarding/config/onboarding-flow-config.ts +189 -0
  222. package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/app/index.tsx +42 -0
  223. package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/data.ts +32 -0
  224. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/app/index.tsx +43 -0
  225. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/interactive-onboarding.tsx +222 -0
  226. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/ai-tone-step.tsx +133 -0
  227. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/currency-step.tsx +165 -0
  228. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-ai-step.tsx +199 -0
  229. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-chatbot-step.tsx +154 -0
  230. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-manual-step.tsx +156 -0
  231. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-scan-step.tsx +158 -0
  232. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/main-reason-step.tsx +139 -0
  233. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/notification-step.tsx +129 -0
  234. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/overspend-step.tsx +138 -0
  235. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/personalizing-step.tsx +190 -0
  236. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/rating-step.tsx +98 -0
  237. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/reminder-step.tsx +181 -0
  238. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/safety-step.tsx +110 -0
  239. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/struggle-step.tsx +139 -0
  240. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/welcome-step.tsx +217 -0
  241. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/ui/onboarding-header.tsx +58 -0
  242. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/constants.ts +179 -0
  243. package/recipes/onboarding/apps/native/src/features/onboarding/hooks/use-onboarding-analytics.ts +323 -0
  244. package/recipes/onboarding/apps/native/src/features/onboarding/services/onboarding-analytics.ts +432 -0
  245. package/recipes/onboarding/recipe.json +15 -0
  246. package/recipes/onboarding@latest.zip +0 -0
  247. package/recipes/payments/recipe.json +28 -61
  248. package/recipes/payments-supabase/apps/native/src/features/payments/README.md +200 -0
  249. package/recipes/payments-supabase/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
  250. package/recipes/payments-supabase/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
  251. package/recipes/payments-supabase/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
  252. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
  253. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
  254. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
  255. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
  256. package/recipes/payments-supabase/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
  257. package/recipes/payments-supabase/apps/native/src/features/payments/index.ts +8 -0
  258. package/recipes/payments-supabase/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
  259. package/recipes/payments-supabase/packages/backend/src/services/payments.ts +201 -0
  260. package/recipes/payments-supabase/packages/backend/supabase/migrations/payments.sql +35 -0
  261. package/recipes/payments-supabase/recipe.json +51 -0
  262. package/recipes/payments-supabase@latest.zip +0 -0
  263. package/recipes/payments@latest.zip +0 -0
  264. package/recipes/quiz/apps/native/src/features/quiz/index.tsx +1 -2
  265. package/recipes/quiz/recipe.json +6 -9
  266. package/recipes/quiz@latest.zip +0 -0
  267. package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +1 -2
  268. package/recipes/tracker-app/recipe.json +7 -10
  269. package/recipes/tracker-app@latest.zip +0 -0
  270. package/recipes/voice-bot/recipe.json +8 -68
  271. package/recipes/voice-bot.zip +0 -0
  272. package/recipes/voice-bot@latest.zip +0 -0
  273. package/recipes/wake-word/recipe.json +10 -9
  274. package/recipes/wake-word.zip +0 -0
  275. package/recipes/wake-word@latest.zip +0 -0
  276. package/recipes/charts/apps/native/src/app/(root)/(protected)/charts/index.tsx +0 -3
  277. package/recipes/chatbot/packages/backend/convex/lib/rateLimit.ts +0 -100
  278. package/recipes/chatbot/packages/backend/convex/lib/telemetry.ts +0 -29
  279. package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +0 -0
  280. package/recipes/image-analysis/apps/native/assets/features/image-analyzer/front.jpg +0 -0
  281. package/recipes/image-analysis/apps/native/assets/features/image-analyzer/side.jpg +0 -0
  282. package/recipes/image-analysis/apps/native/assets/features/image-analyzer/threeQuarter.jpg +0 -0
  283. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/_layout.tsx +0 -5
  284. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/analysis-options.tsx +0 -50
  285. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/camera.tsx +0 -2
  286. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/index.tsx +0 -50
  287. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/loading.tsx +0 -50
  288. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/results.tsx +0 -2
  289. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/trait-details.tsx +0 -3
  290. package/recipes/image-analysis/packages/backend/convex/lib/ai/imageAnalysisAdapter.ts +0 -200
  291. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +0 -74
  292. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +0 -25
  293. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +0 -23
  294. package/recipes/quiz/apps/native/src/app/(root)/(protected)/quiz/index.tsx +0 -47
  295. package/recipes/tracker-app/apps/native/src/app/(root)/(protected)/tracker-app/index.tsx +0 -1
  296. package/recipes/voice-bot/apps/native/src/app/(root)/(protected)/voice-bot/index.tsx +0 -27
  297. package/recipes/voice-bot/packages/backend/convex/router.ts +0 -81
  298. /package/recipes/{chatbot/apps/native/src/app/(root)/(protected) → chatbot-supabase/apps/native/src/app}/chatbot/index.tsx +0 -0
  299. /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/gallery.tsx +0 -0
  300. /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/index.tsx +0 -0
@@ -1,122 +1,94 @@
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 { getToken, getDeviceInfo } from '../core/auth.js';
6
- import { fetchRecipe, downloadZip } from '../core/http.js';
7
- import { addEntry, getEntry } from '../core/journal.js';
8
- import { copyTree, readFileContent, exists, writeFileContent } from '../core/fsx.js';
9
- import { addEnvVars, hasEnvKey } from '../core/env.js';
10
- import { insertEnvSnippets, insertNavLinkNative, insertNavLinkWeb, ENV_BUILD_TIME_ENV_END, ENV_BUILD_TIME_ENV_START, ENV_BUILD_TIME_SCHEMA_END, ENV_BUILD_TIME_SCHEMA_START, ENV_CLIENT_ENV_END, ENV_CLIENT_ENV_START, ENV_CLIENT_SCHEMA_END, ENV_CLIENT_SCHEMA_START, ENV_CONSTANTS_END, ENV_CONSTANTS_START, } from '../core/codemod.js';
11
- import { join, resolve, dirname, relative } from 'path';
12
- import { copyFile, mkdir } from 'fs/promises';
13
- import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
14
- import { ensureWithinBase } from '../core/pathGuard.js';
15
- import { extractZipSafe } from '../core/archive.js';
16
- import { hashFiles } from '../core/hash.js';
17
- import { promptYesNo } from '../core/prompt.js';
18
- import { detectTarget } from '../core/detect.js';
19
- import { withSpinner } from '../core/spinner.js';
20
- import { getBundledRecipeZipPath } from '../core/recipes.js';
21
- import { tmpdir } from 'os';
22
- import { randomUUID } from 'crypto';
23
- let manualStepsFilePath = null;
24
- function formatTimestampForSteps() {
25
- const now = new Date();
26
- const pad = (n) => n.toString().padStart(2, '0');
27
- return `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${now
28
- .getFullYear()
29
- .toString()
30
- .slice(-2)}`;
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 { getToken, getDeviceInfo } from "../core/auth.js";
6
+ import { fetchRecipe, downloadZip } from "../core/http.js";
7
+ import { addEntry, getEntry } from "../core/journal.js";
8
+ import { copyTree, readFileContent, exists, } from "../core/fsx.js";
9
+ import { hasEnvKey } from "../core/env.js";
10
+ import { insertEnvSnippets, insertNavLinkNative, insertNavLinkWeb, ENV_BUILD_TIME_ENV_END, ENV_BUILD_TIME_ENV_START, ENV_BUILD_TIME_SCHEMA_END, ENV_BUILD_TIME_SCHEMA_START, ENV_CLIENT_ENV_END, ENV_CLIENT_ENV_START, ENV_CLIENT_SCHEMA_END, ENV_CLIENT_SCHEMA_START, ENV_CONSTANTS_END, ENV_CONSTANTS_START, } from "../core/codemod.js";
11
+ import { join, resolve, relative } from "path";
12
+ import { ensureWithinBase } from "../core/pathGuard.js";
13
+ import { extractZipSafe } from "../core/archive.js";
14
+ import { hashFiles } from "../core/hash.js";
15
+ import { promptYesNo } from "../core/prompt.js";
16
+ import { detectTarget, detectBackendType } from "../core/detect.js";
17
+ import { withSpinner } from "../core/spinner.js";
18
+ import { getBundledRecipeZipPath } from "../core/recipes.js";
19
+ import { tmpdir } from "os";
20
+ import { randomUUID } from "crypto";
21
+ import { appendManualSteps } from "../core/manualSteps.js";
22
+ import { addAppConfigPlugin, addAppConfigIosProperties } from "../core/ast.js";
23
+ import { spawnSync } from "child_process";
24
+ /**
25
+ * Validates a package name/version string to prevent shell injection.
26
+ * Allows: letters, numbers, @, /, ., _, -, ^, ~, >, <, =, space (for version ranges)
27
+ * Disallows: shell metacharacters like ; | & $ ` etc.
28
+ */
29
+ function isValidPackageArg(arg) {
30
+ // Package names can include @scope/name@version patterns
31
+ // Version can include ^, ~, >, <, =, space for ranges
32
+ if (!arg || arg.length > 200)
33
+ return false;
34
+ // Disallow dangerous characters
35
+ if (/[;&|$`"'\\]/.test(arg))
36
+ return false;
37
+ // Disallow path traversal
38
+ if (arg.includes(".."))
39
+ return false;
40
+ // Must start with letter, @ or number
41
+ if (!/^[@a-zA-Z0-9]/.test(arg))
42
+ return false;
43
+ return true;
31
44
  }
32
- function ensureManualStepsFile(cwd) {
33
- if (manualStepsFilePath)
34
- return manualStepsFilePath;
35
- const dir = join(cwd, 'vf-manual-steps');
36
- mkdirSync(dir, { recursive: true });
37
- const base = formatTimestampForSteps();
38
- let counter = 1;
39
- let filename = `${base}-01.md`;
40
- while (existsSync(join(dir, filename))) {
41
- counter += 1;
42
- filename = `${base}-${String(counter).padStart(2, '0')}.md`;
43
- }
44
- const header = [
45
- '# VibeFast Manual Steps',
46
- '',
47
- `Generated: ${new Date().toISOString()}`,
48
- `Command: ${process.argv.join(' ')}`,
49
- '',
50
- ].join('\n');
51
- writeFileSync(join(dir, filename), header, { encoding: 'utf8' });
52
- manualStepsFilePath = join(dir, filename);
53
- return manualStepsFilePath;
54
- }
55
- function appendManualSteps(cwd, featureName, steps) {
56
- if (!steps.length)
57
- return '';
58
- const filePath = ensureManualStepsFile(cwd);
59
- const lines = [];
60
- lines.push('');
61
- lines.push(`## ${featureName}`);
62
- lines.push('');
63
- steps.forEach((step) => {
64
- lines.push(`- [ ] ${step.title}`);
65
- if (step.description) {
66
- lines.push(` - Details: ${step.description}`);
67
- }
68
- if (step.file) {
69
- lines.push(` - File: ${step.file}`);
70
- }
71
- if (step.link) {
72
- lines.push(` - Link: ${step.link}`);
73
- }
74
- lines.push('');
75
- });
76
- appendFileSync(filePath, lines.join('\n'), { encoding: 'utf8' });
77
- const relPath = relative(cwd, filePath);
78
- return relPath;
45
+ /**
46
+ * Validates all package names and returns invalid ones.
47
+ */
48
+ function getInvalidPackages(packages) {
49
+ return packages.filter((pkg) => !isValidPackageArg(pkg));
79
50
  }
80
- export const addCommand = new Command('add')
81
- .description('Add VibeFast features, UI components, or integrations')
82
- .argument('[feature]', 'Feature name to install (optional - will show interactive menu if omitted)')
83
- .option('--target <target>', 'Target platform (native or web)')
84
- .option('--dry-run', 'Preview changes without applying')
85
- .option('--force', 'Overwrite existing files without asking')
86
- .option('--yes', 'Answer yes to all prompts (for automation)')
87
- .option('--category <category>', 'Filter by category (feature, ui, integration)')
88
- .on('--help', () => {
89
- log.plain('');
90
- log.info('Examples:');
91
- log.plain(' vf add # Interactive menu');
92
- log.plain(' vf add chatbot # Add specific feature');
93
- log.plain(' vf add chatbot --dry-run # Preview changes');
94
- log.plain(' vf add chatbot --force # Force reinstall');
95
- log.plain(' vf add chatbot --yes # Skip prompts');
51
+ export const addCommand = new Command("add")
52
+ .description("Add VibeFast features or advanced UI components")
53
+ .argument("[feature]", "Feature name to install (optional - will show interactive menu if omitted)")
54
+ .option("--target <target>", "Target platform (native or web)")
55
+ .option("--dry-run", "Preview changes without applying")
56
+ .option("--force", "Overwrite existing files without asking")
57
+ .option("--yes", "Answer yes to all prompts (for automation)")
58
+ .option("--skip-install", "Skip dependency installation")
59
+ .on("--help", () => {
60
+ log.plain("");
61
+ log.info("Examples:");
62
+ log.plain(" vf add # Interactive menu");
63
+ log.plain(" vf add chatbot # Add specific feature");
64
+ log.plain(" vf add chatbot --dry-run # Preview changes");
65
+ log.plain(" vf add chatbot --force # Force reinstall");
66
+ log.plain(" vf add chatbot --yes # Skip prompts");
96
67
  })
97
68
  .action(async (feature, options) => {
98
69
  try {
99
- // Fix MaxListeners warning for batch operations
100
- process.stdin.setMaxListeners(0);
101
- process.stdout.setMaxListeners(0);
102
- const paths = getPaths();
70
+ // Fix MaxListeners warning for batch operations (reasonable limit, not unlimited)
71
+ process.stdin.setMaxListeners(50);
72
+ process.stdout.setMaxListeners(50);
73
+ const paths = await getPaths();
103
74
  // Interactive mode if no feature specified
104
75
  if (!feature) {
105
- const { RECIPES, getRecipesByCategory, formatCategory, getCategories } = await import('../core/recipes.js');
106
- const { promptSelectAsync, promptMultiSelectAsync, intro, outro } = await import('../core/prompt.js');
76
+ const { RECIPES, getRecipesByCategory, formatCategory, getCategories } = await import("../core/recipes.js");
77
+ const { promptSelectAsync, promptMultiSelectAsync, intro, outro } = await import("../core/prompt.js");
107
78
  // Check if it's a category name
108
- const categories = getCategories();
79
+ // Temporarily hide integrations from the interactive menu
80
+ const categories = getCategories().filter((cat) => cat !== "integration");
109
81
  // Show intro
110
- intro('🎯 VibeFast Feature Installer');
82
+ intro("🎯 VibeFast Feature Installer");
111
83
  // Show category selection
112
84
  const categoryChoices = categories.map((cat) => ({
113
85
  value: cat,
114
86
  label: formatCategory(cat),
115
87
  description: `Browse ${formatCategory(cat).toLowerCase()}`,
116
88
  }));
117
- const selectedCategory = await promptSelectAsync('What would you like to add?', categoryChoices);
89
+ const selectedCategory = await promptSelectAsync("What would you like to add?", categoryChoices);
118
90
  if (!selectedCategory) {
119
- outro('Cancelled');
91
+ outro("Cancelled");
120
92
  return;
121
93
  }
122
94
  // Show items in selected category
@@ -128,22 +100,22 @@ export const addCommand = new Command('add')
128
100
  }));
129
101
  const selectedRecipes = await promptMultiSelectAsync(`Select ${formatCategory(selectedCategory).toLowerCase()} to add:`, recipeChoices);
130
102
  if (selectedRecipes.length === 0) {
131
- outro('No items selected');
103
+ outro("No items selected");
132
104
  return;
133
105
  }
134
106
  // Install each selected recipe
135
107
  for (const recipeName of selectedRecipes) {
136
- log.plain('');
137
- log.plain(''.repeat(60));
108
+ log.plain("");
109
+ log.plain("".repeat(60));
138
110
  await installFeature(recipeName, options, paths);
139
111
  }
140
- outro('✨ Installation complete!');
112
+ outro("✨ Installation complete!");
141
113
  return;
142
114
  }
143
115
  // Check if feature is a category name
144
- const { getCategories, getRecipesByCategory, formatCategory } = await import('../core/recipes.js');
145
- const { promptMultiSelectAsync, intro, outro } = await import('../core/prompt.js');
146
- const categories = getCategories();
116
+ const { getCategories, getRecipesByCategory, formatCategory } = await import("../core/recipes.js");
117
+ const { promptMultiSelectAsync, intro, outro } = await import("../core/prompt.js");
118
+ const categories = getCategories().filter((cat) => cat !== "integration");
147
119
  if (categories.includes(feature)) {
148
120
  intro(`🎯 ${formatCategory(feature)}`);
149
121
  // Show items in this category
@@ -155,16 +127,16 @@ export const addCommand = new Command('add')
155
127
  }));
156
128
  const selectedRecipes = await promptMultiSelectAsync(`Select ${formatCategory(feature).toLowerCase()} to add:`, recipeChoices);
157
129
  if (selectedRecipes.length === 0) {
158
- outro('No items selected');
130
+ outro("No items selected");
159
131
  return;
160
132
  }
161
133
  // Install each selected recipe
162
134
  for (const recipeName of selectedRecipes) {
163
- log.plain('');
164
- log.plain(''.repeat(60));
135
+ log.plain("");
136
+ log.plain("".repeat(60));
165
137
  await installFeature(recipeName, options, paths);
166
138
  }
167
- outro('✨ Installation complete!');
139
+ outro("✨ Installation complete!");
168
140
  return;
169
141
  }
170
142
  // Install single feature
@@ -189,263 +161,30 @@ export async function installFeature(feature, options, paths) {
189
161
  log.info(`Auto-detected target: ${target}`);
190
162
  }
191
163
  else {
192
- log.error('Could not auto-detect target (native/web)');
193
- log.info('Please specify with --target native or --target web');
164
+ log.error("Could not auto-detect target (native/web)");
165
+ log.info("Please specify with --target native or --target web");
194
166
  process.exit(1);
195
167
  }
196
168
  }
197
- function groupEnvVars(env, cwd) {
198
- if (!env?.length) {
199
- return [];
200
- }
201
- const groups = new Map();
202
- for (const envVar of env) {
203
- const relativePath = envVar.file ?? 'apps/native/.env.local';
204
- const resolvedPath = resolve(cwd, relativePath);
205
- const existing = groups.get(resolvedPath);
206
- if (existing) {
207
- existing.vars.push(envVar);
208
- }
209
- else {
210
- groups.set(resolvedPath, {
211
- path: resolvedPath,
212
- relativePath,
213
- vars: [envVar],
214
- });
215
- }
216
- }
217
- return [...groups.values()];
218
- }
219
- async function ensureEnvFileFromExample(group, options) {
220
- if (await exists(group.path)) {
221
- return false;
222
- }
223
- const exampleCandidates = [
224
- `${group.path}.example`,
225
- join(dirname(group.path), '.env.example'),
226
- ];
227
- const examplePath = (await Promise.all(exampleCandidates.map(async (candidate) => ((await exists(candidate)) ? candidate : null)))).find(Boolean);
228
- if (!examplePath) {
229
- return false;
230
- }
231
- const exampleLabel = examplePath.startsWith(dirname(group.path))
232
- ? examplePath.slice(dirname(group.path).length + 1)
233
- : examplePath;
234
- if (options?.dryRun) {
235
- log.info(`[DRY RUN] Would create ${group.relativePath} from ${exampleLabel}`);
236
- return true;
237
- }
238
- await mkdir(dirname(group.path), { recursive: true });
239
- await copyFile(examplePath, group.path);
240
- log.info(`Created ${group.relativePath} from ${exampleLabel}`);
241
- return true;
242
- }
243
- async function ensureEnvVarsForGroups(envGroups, options) {
244
- const summary = [];
245
- for (const group of envGroups) {
246
- await ensureEnvFileFromExample(group, options);
247
- const vars = group.vars.map(envVar => ({
248
- key: envVar.key,
249
- value: envVar.value ?? envVar.example ?? '',
250
- comment: envVar.description?.replace(/\s+/g, ' ').trim() || undefined,
251
- }));
252
- const result = await addEnvVars(group.path, vars, { dryRun: options.dryRun });
253
- if (result.added.length > 0) {
254
- summary.push({
255
- relativePath: group.relativePath,
256
- added: result.added,
257
- });
258
- }
259
- }
260
- return summary;
261
- }
262
- async function reportEnvStatus(envGroups, options) {
263
- const attention = [];
264
- for (const group of envGroups) {
265
- const missing = [];
266
- const existing = [];
267
- for (const envVar of group.vars) {
268
- const hasKey = await hasEnvKey(group.path, envVar.key);
269
- if (hasKey) {
270
- existing.push(envVar.key);
271
- }
272
- else {
273
- missing.push(envVar);
274
- }
275
- }
276
- if (missing.length > 0) {
277
- log.plain('');
278
- log.warn(`⚠ REQUIRED ENVIRONMENT VARIABLES (${group.relativePath}):`);
279
- log.plain('');
280
- missing.forEach(envVar => {
281
- log.plain(` ${envVar.key}`);
282
- log.plain(` ${envVar.description}`);
283
- log.plain(` Example: ${envVar.example}`);
284
- if (envVar.link) {
285
- log.plain(` Get it: ${envVar.link}`);
286
- }
287
- log.plain('');
288
- attention.push(`Add ${envVar.key} to ${group.relativePath}`);
289
- });
290
- log.info(`Add these to ${group.relativePath}`);
291
- log.plain('');
292
- }
293
- if (existing.length > 0) {
294
- log.success(`✓ Already configured (${group.relativePath}): ${existing.join(', ')}`);
295
- log.plain('');
296
- }
297
- }
298
- return attention;
299
- }
300
- async function applyEnvConfiguration(paths, envConfig, options) {
301
- const envPath = join(paths.cwd, 'apps', 'native', 'env.js');
302
- if (!(await exists(envPath))) {
303
- return;
304
- }
305
- const suffix = options.dryRun ? ' (dry run)' : '';
306
- if (envConfig.constants?.length) {
307
- const inserted = await insertEnvSnippets(envPath, ENV_CONSTANTS_START, ENV_CONSTANTS_END, envConfig.constants, options);
308
- if (inserted) {
309
- log.info(`Updated env.js constants${suffix}.`);
310
- }
311
- }
312
- if (envConfig.client?.schema?.length) {
313
- const inserted = await insertEnvSnippets(envPath, ENV_CLIENT_SCHEMA_START, ENV_CLIENT_SCHEMA_END, envConfig.client.schema, options);
314
- if (inserted) {
315
- log.info(`Updated env.js client schema definitions${suffix}.`);
316
- }
317
- }
318
- if (envConfig.client?.env?.length) {
319
- const inserted = await insertEnvSnippets(envPath, ENV_CLIENT_ENV_START, ENV_CLIENT_ENV_END, envConfig.client.env, options);
320
- if (inserted) {
321
- log.info(`Updated env.js client env exports${suffix}.`);
322
- }
323
- }
324
- if (envConfig.buildTime?.schema?.length) {
325
- const inserted = await insertEnvSnippets(envPath, ENV_BUILD_TIME_SCHEMA_START, ENV_BUILD_TIME_SCHEMA_END, envConfig.buildTime.schema, options);
326
- if (inserted) {
327
- log.info(`Updated env.js build-time schema${suffix}.`);
328
- }
329
- }
330
- if (envConfig.buildTime?.env?.length) {
331
- const inserted = await insertEnvSnippets(envPath, ENV_BUILD_TIME_ENV_START, ENV_BUILD_TIME_ENV_END, envConfig.buildTime.env, options);
332
- if (inserted) {
333
- log.info(`Updated env.js build-time exports${suffix}.`);
334
- }
335
- }
336
- }
337
- async function applyAppConfigPlugins(feature, paths, options) {
338
- const appConfigPath = join(paths.cwd, 'apps', 'native', 'app.config.ts');
339
- if (!(await exists(appConfigPath)))
340
- return;
341
- const original = await readFileContent(appConfigPath);
342
- let updated = original;
343
- // Ensure a plugins block exists; insert one if missing (before extra: or at end)
344
- let pluginsStart = updated.indexOf('plugins: [');
345
- let extraStart = updated.indexOf('\n\n extra:');
346
- if (pluginsStart === -1) {
347
- const block = ` plugins: [\n ],\n\n`;
348
- if (extraStart !== -1) {
349
- updated = `${updated.slice(0, extraStart)}${block}${updated.slice(extraStart)}`;
350
- }
351
- else {
352
- // Append before the final closing of the config object
353
- const insertPos = updated.lastIndexOf('};');
354
- updated =
355
- insertPos !== -1
356
- ? `${updated.slice(0, insertPos)}${block}${updated.slice(insertPos)}`
357
- : `${updated.trimEnd()}\n\n${block}};`;
358
- }
359
- // recompute positions
360
- pluginsStart = updated.indexOf('plugins: [');
361
- extraStart = updated.indexOf('\n\n extra:');
362
- }
363
- if (pluginsStart === -1) {
364
- log.warn('⚠ Could not locate plugins block in app.config.ts; skipping plugin auto-insert.');
365
- return;
366
- }
367
- if (extraStart === -1) {
368
- extraStart = updated.length;
369
- }
370
- // Boundaries for plugins block
371
- const pluginsSection = updated.slice(pluginsStart, extraStart);
372
- const closeIdxLocal = pluginsSection.lastIndexOf('],');
373
- if (closeIdxLocal === -1) {
374
- log.warn('⚠ Could not locate plugins closing bracket; skipping plugin auto-insert.');
375
- return;
376
- }
377
- const pluginsClose = pluginsStart + closeIdxLocal;
378
- const addBlock = (block, needle) => {
379
- if (updated.includes(needle))
380
- return;
381
- updated = `${updated.slice(0, pluginsClose)} ${block}\n${updated.slice(pluginsClose)}`;
382
- };
383
- const patchIosBlock = () => {
384
- let iosIdx = updated.indexOf('ios:');
385
- const experimentsIdx = updated.indexOf('\n\n experiments:');
386
- // If ios block is missing, inject a minimal one before plugins.
387
- if (iosIdx === -1) {
388
- updated = updated.replace('plugins: [', `ios: {\n supportsTablet: true,\n bundleIdentifier: Env.BUNDLE_ID,\n appleTeamId: Env.APPLE_TEAM_ID,\n config: { usesNonExemptEncryption: false },\n entitlements: { 'com.apple.security.application-groups': ['group.' + Env.BUNDLE_ID] },\n },\n\n plugins: [`);
389
- iosIdx = updated.indexOf('ios:');
390
- }
391
- if (iosIdx === -1 || experimentsIdx === -1)
392
- return;
393
- const block = updated.slice(iosIdx, experimentsIdx);
394
- let patched = block;
395
- if (!patched.includes('appleTeamId')) {
396
- patched = patched.replace(/bundleIdentifier:\s*Env\.BUNDLE_ID,?/, (m) => `${m}\n appleTeamId: Env.APPLE_TEAM_ID,`);
397
- if (patched === block) {
398
- patched = patched.replace('{', '{\n appleTeamId: Env.APPLE_TEAM_ID,');
399
- }
400
- }
401
- if (!patched.includes('com.apple.security.application-groups')) {
402
- if (patched.includes('entitlements:')) {
403
- patched = patched.replace(/entitlements:\s*{([^}]*)}/s, (m, inner) => `entitlements: {\n 'com.apple.security.application-groups': ['group.' + Env.BUNDLE_ID],${inner.trim() ? `\n${inner.trimStart()}` : ''}\n }`);
404
- }
405
- else {
406
- patched = patched.replace(/config:\s*{[^}]*},/, (m) => `${m}\n entitlements: {\n 'com.apple.security.application-groups': ['group.' + Env.BUNDLE_ID],\n },`);
407
- }
408
- }
409
- updated = updated.replace(block, patched);
410
- };
411
- if (feature === 'ios-widget') {
412
- addBlock(`[
413
- '@bacons/apple-targets',
414
- {
415
- targets: [
416
- {
417
- name: 'VibeFastWidget',
418
- type: 'widget',
419
- bundleIdentifier: \`\${Env.BUNDLE_ID}.widget\`,
420
- entitlements: {
421
- 'com.apple.security.application-groups': [
422
- 'group.com.vibefast.vibefast',
423
- ],
424
- },
425
- },
426
- ],
427
- },
428
- ],`, '@bacons/apple-targets');
429
- patchIosBlock();
430
- }
431
- if (feature === 'wake-word') {
432
- addBlock(`'expo-audio',`, `'expo-audio'`);
433
- addBlock(`[
434
- 'react-native-vosk',
435
- {
436
- models: ['assets/vosk-model/model-en-us'],
437
- iOSMicrophonePermission: 'We use the mic for on-device wake-phrase detection.',
438
- },
439
- ],`, 'react-native-vosk');
440
- }
441
- if (updated !== original && !options?.dryRun) {
442
- await writeFileContent(appConfigPath, updated, { force: true });
443
- log.info('Updated app.config.ts plugins for feature requirements.');
444
- }
445
- }
446
169
  // Validate setup
447
170
  const config = await validateSignature(paths.signatureFile);
448
171
  validateTarget(target, config.targets);
172
+ // Detect backend type for recipe selection
173
+ const backend = await detectBackendType(paths.cwd);
174
+ log.info(`Target: ${target}, Backend: ${backend}`);
175
+ // For Supabase projects, try feature-supabase recipe first
176
+ // This allows backend-specific recipes while maintaining backwards compatibility
177
+ let recipeName = feature;
178
+ if (backend === "supabase") {
179
+ // Check if supabase-specific bundled recipe exists
180
+ const supabaseRecipe = `${feature}-supabase`;
181
+ const supabaseZipPath = await getBundledRecipeZipPath(supabaseRecipe);
182
+ if (supabaseZipPath) {
183
+ recipeName = supabaseRecipe;
184
+ log.info(`Using Supabase-specific recipe: ${recipeName}`);
185
+ }
186
+ // Otherwise fall back to base recipe (which may work for both backends)
187
+ }
449
188
  // Check auth
450
189
  const token = await getToken();
451
190
  if (!token) {
@@ -456,10 +195,10 @@ export async function installFeature(feature, options, paths) {
456
195
  const existing = await getEntry(paths.journalFile, feature, target);
457
196
  if (existing && !options.force && !options.yes) {
458
197
  log.warn(`${feature} is already installed for ${target}`);
459
- const shouldReinstall = await promptYesNo('Reinstall and overwrite existing files? (y/N) ', false);
198
+ const shouldReinstall = await promptYesNo("Reinstall and overwrite existing files? (y/N) ", false);
460
199
  if (!shouldReinstall) {
461
- log.info('Skipped. Use --force to reinstall without prompting');
462
- log.plain('');
200
+ log.info("Skipped. Use --force to reinstall without prompting");
201
+ log.plain("");
463
202
  return;
464
203
  }
465
204
  // Auto-enable force mode for reinstall to avoid per-file prompts
@@ -467,20 +206,20 @@ export async function installFeature(feature, options, paths) {
467
206
  }
468
207
  else if (existing && !options.force) {
469
208
  log.warn(`${feature} is already installed for ${target}`);
470
- log.info('Use --force to reinstall');
471
- log.plain('');
209
+ log.info("Use --force to reinstall");
210
+ log.plain("");
472
211
  return;
473
212
  }
474
213
  if (options.dryRun) {
475
- log.info('[DRY RUN] No changes will be made');
214
+ log.info("[DRY RUN] No changes will be made");
476
215
  }
477
216
  // Fetch recipe
478
217
  const device = await getDeviceInfo();
479
- const response = await withSpinner(`Fetching ${feature} for ${target}...`, async () => {
218
+ const response = await withSpinner(`Fetching ${recipeName} for ${target}...`, async () => {
480
219
  return await fetchRecipe({
481
220
  token,
482
221
  device,
483
- feature,
222
+ feature: recipeName,
484
223
  target,
485
224
  starter: { name: config.name, version: config.version },
486
225
  });
@@ -491,85 +230,89 @@ export async function installFeature(feature, options, paths) {
491
230
  let extractDir = null;
492
231
  if (!response.ok || (!response.signedUrl && !response.zipData)) {
493
232
  // Remote fetch failed; try bundled fallback
494
- const error = response.error || 'Unknown error';
495
- const message = response.message || '';
496
- const localZip = await getBundledRecipeZipPath(feature);
233
+ const error = response.error || "Unknown error";
234
+ const message = response.message || "";
235
+ const localZip = await getBundledRecipeZipPath(recipeName);
497
236
  if (localZip) {
498
237
  log.info(`Remote recipe unavailable (${error}). Using bundled recipe.`);
499
238
  zipPath = localZip;
500
239
  }
501
240
  else {
502
- log.plain('');
241
+ log.plain("");
503
242
  // User-friendly error messages
504
- if (error.includes('Invalid') || error.includes('token') || error.includes('license')) {
505
- log.plain('❌ Invalid or expired license key');
506
- log.plain('');
507
- log.info('Your license key may be:');
508
- log.plain(' • Incorrect or mistyped');
509
- log.plain(' • Expired');
510
- log.plain('Revoked');
511
- log.plain('');
512
- log.info('To fix this:');
513
- log.plain(' 1. Check your license key from your purchase receipt');
514
- log.plain(' 2. Run: vf logout');
515
- log.plain(' 3. Run: vf login --token YOUR_CORRECT_TOKEN');
516
- log.plain('');
517
- log.info('Need help? Contact support@vibefast.pro');
518
- }
519
- else if (error.includes('Device limit') || error.includes('device') || message.includes('device')) {
520
- log.plain('❌ Device limit reached');
521
- log.plain('');
522
- log.info('You have reached the maximum number of devices for your license');
523
- log.plain('');
524
- log.info('To fix this:');
525
- log.plain(' 1. Run: vf devices');
526
- log.plain(' 2. Deactivate an unused device: vf devices --deactivate <device-id>');
527
- log.plain(' 3. Try again: vf add ' + feature);
528
- log.plain('');
243
+ if (error.includes("Invalid") ||
244
+ error.includes("token") ||
245
+ error.includes("license")) {
246
+ log.plain("❌ Invalid or expired license key");
247
+ log.plain("");
248
+ log.info("Your license key may be:");
249
+ log.plain("Incorrect or mistyped");
250
+ log.plain(" • Expired");
251
+ log.plain(" • Revoked");
252
+ log.plain("");
253
+ log.info("To fix this:");
254
+ log.plain(" 1. Check your license key from your purchase receipt");
255
+ log.plain(" 2. Run: vf logout");
256
+ log.plain(" 3. Run: vf login --token YOUR_CORRECT_TOKEN");
257
+ log.plain("");
258
+ log.info("Need help? Contact support@vibefast.pro");
259
+ }
260
+ else if (error.includes("Device limit") ||
261
+ error.includes("device") ||
262
+ message.includes("device")) {
263
+ log.plain("❌ Device limit reached");
264
+ log.plain("");
265
+ log.info("You have reached the maximum number of devices for your license");
266
+ log.plain("");
267
+ log.info("To fix this:");
268
+ log.plain(" 1. Run: vf devices");
269
+ log.plain(" 2. Deactivate an unused device: vf devices --deactivate <device-id>");
270
+ log.plain(" 3. Try again: vf add " + feature);
271
+ log.plain("");
529
272
  if (message) {
530
273
  log.plain(`Details: ${message}`);
531
- log.plain('');
274
+ log.plain("");
532
275
  }
533
276
  }
534
- else if (error.includes('Network') || error.includes('connect')) {
535
- log.plain('❌ Network error');
536
- log.plain('');
537
- log.info('Could not connect to VibeFast servers');
538
- log.plain('');
539
- log.info('Please check:');
540
- log.plain(' • Your internet connection');
541
- log.plain(' • Firewall settings');
542
- log.plain(' • VPN configuration');
543
- log.plain('');
277
+ else if (error.includes("Network") || error.includes("connect")) {
278
+ log.plain("❌ Network error");
279
+ log.plain("");
280
+ log.info("Could not connect to VibeFast servers");
281
+ log.plain("");
282
+ log.info("Please check:");
283
+ log.plain(" • Your internet connection");
284
+ log.plain(" • Firewall settings");
285
+ log.plain(" • VPN configuration");
286
+ log.plain("");
544
287
  if (message) {
545
288
  log.plain(`Details: ${message}`);
546
- log.plain('');
289
+ log.plain("");
547
290
  }
548
291
  }
549
- else if (error.includes('not found') || error.includes('404')) {
550
- log.plain('❌ Feature not found');
551
- log.plain('');
292
+ else if (error.includes("not found") || error.includes("404")) {
293
+ log.plain("❌ Feature not found");
294
+ log.plain("");
552
295
  log.info(`The feature "${feature}" does not exist or is not available for ${target}`);
553
- log.plain('');
554
- log.info('To see available features:');
555
- log.plain(' vf list');
556
- log.plain('');
296
+ log.plain("");
297
+ log.info("To see available features:");
298
+ log.plain(" vf list");
299
+ log.plain("");
557
300
  }
558
301
  else {
559
302
  // Generic error
560
303
  log.plain(`❌ ${error}`);
561
304
  if (message) {
562
- log.plain('');
305
+ log.plain("");
563
306
  log.plain(`Details: ${message}`);
564
307
  }
565
- log.plain('');
566
- log.info('If this problem persists, contact support@vibefast.pro');
308
+ log.plain("");
309
+ log.info("If this problem persists, contact support@vibefast.pro");
567
310
  }
568
- log.plain('');
311
+ log.plain("");
569
312
  process.exit(1);
570
313
  }
571
314
  }
572
- const fallbackZip = await getBundledRecipeZipPath(feature);
315
+ const fallbackZip = await getBundledRecipeZipPath(recipeName);
573
316
  let attemptedFallback = false;
574
317
  let installedManifest = null;
575
318
  let installedEnvGroups = [];
@@ -583,30 +326,30 @@ export async function installFeature(feature, options, paths) {
583
326
  try {
584
327
  // Download and extract (or use bundled zip)
585
328
  if (!zipPath) {
586
- const result = await withSpinner('Downloading and extracting recipe...', async () => {
329
+ const result = await withSpinner("Downloading and extracting recipe...", async () => {
587
330
  const zip = response.zipData
588
331
  ? await downloadZip(response.zipData, true)
589
332
  : await downloadZip(response.signedUrl);
590
- const dir = join(tmpdir(), 'vibefast', randomUUID());
333
+ const dir = join(tmpdir(), "vibefast", randomUUID());
591
334
  await extractZipSafe(zip, dir);
592
335
  return { zipPath: zip, extractDir: dir };
593
336
  }, {
594
- successText: '✓ Recipe downloaded',
337
+ successText: "✓ Recipe downloaded",
595
338
  });
596
339
  zipPath = result.zipPath;
597
340
  extractDir = result.extractDir;
598
341
  }
599
342
  else {
600
343
  // Bundled zip path - extract to temp location
601
- extractDir = join(tmpdir(), 'vibefast', randomUUID());
344
+ extractDir = join(tmpdir(), "vibefast", randomUUID());
602
345
  await extractZipSafe(zipPath, extractDir);
603
346
  }
604
347
  // Locate manifest (some archives may nest recipe.json)
605
348
  const findManifest = async (dir) => {
606
- const entries = await import('fs/promises').then((m) => m.readdir(dir, { withFileTypes: true }));
349
+ const entries = await import("fs/promises").then((m) => m.readdir(dir, { withFileTypes: true }));
607
350
  for (const entry of entries) {
608
351
  const full = join(dir, entry.name);
609
- if (entry.isFile() && entry.name === 'recipe.json')
352
+ if (entry.isFile() && entry.name === "recipe.json")
610
353
  return full;
611
354
  if (entry.isDirectory()) {
612
355
  const found = await findManifest(full);
@@ -616,7 +359,7 @@ export async function installFeature(feature, options, paths) {
616
359
  }
617
360
  return null;
618
361
  };
619
- let manifestPath = join(extractDir, 'recipe.json');
362
+ let manifestPath = join(extractDir, "recipe.json");
620
363
  if (!(await exists(manifestPath))) {
621
364
  const found = await findManifest(extractDir);
622
365
  if (!found) {
@@ -635,20 +378,22 @@ export async function installFeature(feature, options, paths) {
635
378
  if (!Array.isArray(manifest.copy)) {
636
379
  throw new Error('recipe.json is missing a valid "copy" array');
637
380
  }
638
- const extractRoot = resolve(manifestPath, '..');
381
+ const extractRoot = resolve(manifestPath, "..");
639
382
  const repoRoot = resolve(paths.cwd);
640
383
  const manifestTarget = manifest.target ??
641
- (Array.isArray(manifest.platforms) && manifest.platforms.includes('native')
642
- ? 'native'
643
- : Array.isArray(manifest.platforms) && manifest.platforms.includes('web')
644
- ? 'web'
384
+ (Array.isArray(manifest.platforms) &&
385
+ manifest.platforms.includes("native")
386
+ ? "native"
387
+ : Array.isArray(manifest.platforms) &&
388
+ manifest.platforms.includes("web")
389
+ ? "web"
645
390
  : undefined);
646
391
  if (!manifestTarget) {
647
392
  throw new Error(`Recipe target missing for ${manifest.name}`);
648
393
  }
649
394
  manifest.target = manifestTarget;
650
395
  if (manifestTarget !== target) {
651
- throw new Error(`Recipe target mismatch: expected ${target}, got ${manifestTarget ?? 'undefined'}`);
396
+ throw new Error(`Recipe target mismatch: expected ${target}, got ${manifestTarget ?? "undefined"}`);
652
397
  }
653
398
  log.info(`Installing ${manifest.name} v${manifest.version}...`);
654
399
  const envGroups = groupEnvVars(manifest.env, paths.cwd);
@@ -672,17 +417,52 @@ export async function installFeature(feature, options, paths) {
672
417
  }
673
418
  // Show conflict warnings in dry-run mode
674
419
  if (options.dryRun && allConflicts.length > 0) {
675
- log.plain('');
420
+ log.plain("");
676
421
  log.warn(`⚠ ${allConflicts.length} file(s) will be overwritten:`);
677
- allConflicts.slice(0, 5).forEach(f => {
678
- const relativePath = f.replace(paths.cwd + '/', '');
422
+ allConflicts.slice(0, 5).forEach((f) => {
423
+ const relativePath = relative(paths.cwd, f);
679
424
  log.plain(` • ${relativePath}`);
680
425
  });
681
426
  if (allConflicts.length > 5) {
682
427
  log.plain(` ... and ${allConflicts.length - 5} more`);
683
428
  }
684
- log.warn('⚠ Make sure you have committed your changes to Git!');
685
- log.plain('');
429
+ log.warn("⚠ Make sure you have committed your changes to Git!");
430
+ log.plain("");
431
+ }
432
+ // Handle Supabase migrations (rename with timestamp)
433
+ if (backend === "supabase") {
434
+ const { rename } = await import("fs/promises");
435
+ const { basename, dirname, join } = await import("path");
436
+ for (const file of copiedFiles) {
437
+ if (file.includes("/supabase/migrations/") &&
438
+ file.endsWith(".sql")) {
439
+ const name = basename(file);
440
+ // Check if already timestamped (14 digits at start)
441
+ if (!/^\d{14}_/.test(name)) {
442
+ // Use base timestamp + index to prevent collisions when multiple migrations installed
443
+ const baseTimestamp = Date.now();
444
+ const migrationIndex = copiedFiles
445
+ .filter((f) => f.includes("/supabase/migrations/") && f.endsWith(".sql"))
446
+ .indexOf(file);
447
+ const timestamp = `${baseTimestamp}${String(migrationIndex).padStart(4, "0")}`;
448
+ const newName = `${timestamp}_${name}`;
449
+ const newPath = join(dirname(file), newName);
450
+ try {
451
+ await rename(file, newPath);
452
+ // Update file in copiedFiles list so hashing works correctly
453
+ const index = copiedFiles.indexOf(file);
454
+ if (index !== -1) {
455
+ copiedFiles[index] = newPath;
456
+ }
457
+ log.info(`📦 Timestamped migration: ${newName}`);
458
+ log.plain(" (Run 'supabase db push' to apply)");
459
+ }
460
+ catch (err) {
461
+ log.warn(`Failed to rename migration ${name}: ${err}`);
462
+ }
463
+ }
464
+ }
465
+ }
686
466
  }
687
467
  // Show skipped files
688
468
  if (allSkipped.length > 0) {
@@ -690,9 +470,12 @@ export async function installFeature(feature, options, paths) {
690
470
  }
691
471
  // Add watermark if provided
692
472
  if (response.watermark && !options.dryRun) {
693
- const { writeFileContent, readFileContent } = await import('../core/fsx.js');
473
+ const { writeFileContent, readFileContent } = await import("../core/fsx.js");
694
474
  for (const file of copiedFiles) {
695
- if (file.endsWith('.ts') || file.endsWith('.tsx') || file.endsWith('.js') || file.endsWith('.jsx')) {
475
+ if (file.endsWith(".ts") ||
476
+ file.endsWith(".tsx") ||
477
+ file.endsWith(".js") ||
478
+ file.endsWith(".jsx")) {
696
479
  const content = await readFileContent(file);
697
480
  const watermarked = `// vibefast license: ${response.watermark}\n${content}`;
698
481
  await writeFileContent(file, watermarked, { force: true });
@@ -704,23 +487,27 @@ export async function installFeature(feature, options, paths) {
704
487
  let navHref;
705
488
  let navLabel;
706
489
  if (manifest.nav) {
707
- log.info('Adding navigation link...');
708
- const navFile = target === 'native' ? paths.nativeNavFile : paths.webNavFile;
709
- const insertFn = target === 'native' ? insertNavLinkNative : insertNavLinkWeb;
490
+ log.info("Adding navigation link...");
491
+ const navFile = target === "native" ? paths.nativeNavFile : paths.webNavFile;
492
+ const insertFn = target === "native" ? insertNavLinkNative : insertNavLinkWeb;
710
493
  navHref = manifest.nav.href;
711
494
  navLabel = manifest.nav.label;
712
- navInserted = await insertFn(navFile, manifest.nav, { dryRun: options.dryRun });
495
+ navInserted = await insertFn(navFile, manifest.nav, {
496
+ dryRun: options.dryRun,
497
+ });
713
498
  if (navInserted) {
714
- log.success('Navigation link added');
499
+ log.success("Navigation link added");
715
500
  }
716
501
  else {
717
- log.info('Navigation link already exists');
502
+ log.info("Navigation link already exists");
718
503
  }
719
504
  }
720
505
  // Hash files and update journal
721
506
  if (!options.dryRun) {
722
- log.info('Computing file hashes...');
723
- const fileHashes = await hashFiles(copiedFiles, { showProgress: copiedFiles.length > 20 });
507
+ log.info("Computing file hashes...");
508
+ const fileHashes = await hashFiles(copiedFiles, {
509
+ showProgress: copiedFiles.length > 20,
510
+ });
724
511
  const fileEntries = Array.from(fileHashes.entries()).map(([path, hash]) => ({
725
512
  path,
726
513
  hash,
@@ -753,7 +540,9 @@ export async function installFeature(feature, options, paths) {
753
540
  break retry_install;
754
541
  }
755
542
  catch (err) {
756
- if (!attemptedFallback && fallbackZip) {
543
+ // Don't retry for nav marker errors - bundled recipe will have same problem
544
+ const isNavMarkerError = err.message?.includes("Missing navigation markers");
545
+ if (!attemptedFallback && fallbackZip && !isNavMarkerError) {
757
546
  attemptedFallback = true;
758
547
  zipPath = fallbackZip;
759
548
  extractDir = null;
@@ -764,112 +553,145 @@ export async function installFeature(feature, options, paths) {
764
553
  }
765
554
  }
766
555
  // Auto-install dependencies
767
- if (installedManifest && installedManifest.dependencies && !options.dryRun) {
768
- log.plain('');
769
- log.warn('⚠ This feature requires additional packages');
770
- log.plain('');
771
- if (installedManifest.target === 'native') {
556
+ if (installedManifest &&
557
+ installedManifest.dependencies &&
558
+ !options.dryRun) {
559
+ log.plain("");
560
+ log.warn("⚠ This feature requires additional packages");
561
+ log.plain("");
562
+ if (installedManifest.target === "native") {
772
563
  const packages = Array.from(new Set([
773
564
  ...(installedManifest.dependencies.expo ?? []),
774
565
  ...(installedManifest.dependencies.npm ?? []),
775
566
  ]));
776
567
  if (packages.length > 0) {
777
- log.info('📦 Required packages:');
778
- packages.forEach(pkg => {
568
+ // Security: Validate package names before shell execution
569
+ const invalidPkgs = getInvalidPackages(packages);
570
+ if (invalidPkgs.length > 0) {
571
+ log.error(`Invalid package names detected: ${invalidPkgs.join(", ")}`);
572
+ log.info("Install manually with: pnpx expo install <packages>");
573
+ return;
574
+ }
575
+ log.info("📦 Required packages:");
576
+ packages.forEach((pkg) => {
779
577
  log.plain(` • ${pkg}`);
780
578
  });
781
- log.plain('');
782
- const shouldInstall = options.yes || await promptYesNo('Would you like to install these packages now? (Y/n) ', true);
579
+ log.plain("");
580
+ const shouldInstall = !options.skipInstall &&
581
+ (options.yes ||
582
+ (await promptYesNo("Would you like to install these packages now? (Y/n) ", true)));
783
583
  if (shouldInstall) {
784
- const storeDir = join(paths.cwd, '.pnpm-store');
785
- await withSpinner('Installing packages with Expo...', async () => {
786
- const { execSync } = await import('child_process');
787
- const nativeDir = join(paths.cwd, 'apps', 'native');
584
+ const storeDir = join(paths.cwd, ".pnpm-store");
585
+ await withSpinner("Installing packages with Expo...", async () => {
586
+ const nativeDir = join(paths.cwd, "apps", "native");
788
587
  try {
789
- execSync(`pnpx expo install ${packages.join(' ')}`, {
588
+ // Use spawnSync with args array for safety (no shell interpolation)
589
+ const result = spawnSync("pnpx", ["expo", "install", ...packages], {
790
590
  cwd: nativeDir,
791
- stdio: 'inherit',
591
+ stdio: "inherit",
792
592
  env: {
793
593
  ...process.env,
794
594
  PNPM_STORE_PATH: storeDir,
795
595
  },
596
+ shell: false,
796
597
  });
598
+ if (result.error) {
599
+ throw result.error;
600
+ }
797
601
  }
798
602
  catch (err) {
799
- log.warn('⚠ Expo install reported an error (packages may still be installed). Verify dependencies and app.config.ts plugin additions.');
603
+ log.warn("⚠ Expo install reported an error (packages may still be installed). Verify dependencies and app.config.ts plugin additions.");
800
604
  if (err?.message) {
801
605
  log.warn(err.message);
802
606
  }
803
607
  }
804
608
  }, {
805
- successText: '✓ Packages installed',
609
+ successText: "✓ Packages installed",
806
610
  });
807
611
  }
808
612
  else {
809
- log.info('Install manually with:');
810
- log.plain(` pnpx expo install ${packages.join(' ')}`);
811
- log.plain('');
812
- log.info('💡 Expo will automatically pick compatible versions');
613
+ log.info("Install manually with:");
614
+ log.plain(` pnpx expo install ${packages.join(" ")}`);
615
+ log.plain("");
616
+ log.info("💡 Expo will automatically pick compatible versions");
813
617
  }
814
618
  }
815
619
  }
816
- else if (installedManifest.target === 'web' && installedManifest.dependencies.npm) {
620
+ else if (installedManifest.target === "web" &&
621
+ installedManifest.dependencies.npm) {
817
622
  const packages = installedManifest.dependencies.npm;
818
- log.info('📦 Required packages:');
819
- packages.forEach(pkg => {
623
+ // Security: Validate package names before shell execution
624
+ const invalidPkgs = getInvalidPackages(packages);
625
+ if (invalidPkgs.length > 0) {
626
+ log.error(`Invalid package names detected: ${invalidPkgs.join(", ")}`);
627
+ log.info("Install manually with: pnpm add <packages>");
628
+ return;
629
+ }
630
+ log.info("📦 Required packages:");
631
+ packages.forEach((pkg) => {
820
632
  log.plain(` • ${pkg}`);
821
633
  });
822
- log.plain('');
823
- const shouldInstall = options.yes || await promptYesNo('Would you like to install these packages now? (Y/n) ', true);
634
+ log.plain("");
635
+ const shouldInstall = !options.skipInstall &&
636
+ (options.yes ||
637
+ (await promptYesNo("Would you like to install these packages now? (Y/n) ", true)));
824
638
  if (shouldInstall) {
825
- const { detectPackageManager } = await import('../core/detect.js');
826
- const pkgManager = detectPackageManager(paths.cwd) || 'pnpm';
639
+ const { detectPackageManager } = await import("../core/detect.js");
640
+ const pkgManager = detectPackageManager(paths.cwd) || "pnpm";
827
641
  await withSpinner(`Installing packages with ${pkgManager}...`, async () => {
828
- const { execSync } = await import('child_process');
829
- const installCmd = pkgManager === 'npm'
830
- ? `npm install ${packages.join(' ')}`
831
- : `${pkgManager} add ${packages.join(' ')}`;
832
- execSync(installCmd, {
642
+ // Use spawnSync with args array for safety (no shell interpolation)
643
+ const args = pkgManager === "npm"
644
+ ? ["install", ...packages]
645
+ : ["add", ...packages];
646
+ const result = spawnSync(pkgManager, args, {
833
647
  cwd: paths.cwd,
834
- stdio: 'inherit',
648
+ stdio: "inherit",
649
+ shell: false,
835
650
  });
651
+ if (result.error) {
652
+ throw result.error;
653
+ }
836
654
  }, {
837
- successText: '✓ Packages installed',
655
+ successText: "✓ Packages installed",
838
656
  });
839
657
  }
840
658
  else {
841
- log.info('Install manually with:');
842
- log.plain(` pnpm add ${packages.join(' ')}`);
843
- log.plain(' OR');
844
- log.plain(` yarn add ${packages.join(' ')}`);
659
+ log.info("Install manually with:");
660
+ log.plain(` pnpm add ${packages.join(" ")}`);
661
+ log.plain(" OR");
662
+ log.plain(` yarn add ${packages.join(" ")}`);
845
663
  log.plain(` OR`);
846
- log.plain(` npm install ${packages.join(' ')}`);
664
+ log.plain(` npm install ${packages.join(" ")}`);
847
665
  }
848
666
  }
849
- log.plain('');
667
+ log.plain("");
850
668
  }
851
669
  // Auto-setup Vosk model for wake-word
852
- if (installedManifest?.name === 'wake-word' && !options.dryRun) {
853
- const { setupVoskModel } = await import('../core/vosk.js');
854
- log.plain('');
855
- await withSpinner('Setting up Vosk speech model...', async () => {
670
+ if (installedManifest?.name === "wake-word" && !options.dryRun) {
671
+ const { setupVoskModel } = await import("../core/vosk.js");
672
+ log.plain("");
673
+ await withSpinner("Setting up Vosk speech model...", async () => {
856
674
  return await setupVoskModel(paths.cwd, { dryRun: options.dryRun });
857
675
  }, {
858
- successText: '✓ Vosk model ready',
676
+ successText: "✓ Vosk model ready",
859
677
  });
860
678
  }
861
679
  if (installedManifest?.configuration?.env) {
862
680
  await applyEnvConfiguration(paths, installedManifest.configuration.env, options);
863
681
  }
682
+ // Apply api-client exports injection
683
+ if (installedManifest?.configuration?.apiClient) {
684
+ await applyApiClientConfiguration(paths, installedManifest.configuration.apiClient, options);
685
+ }
864
686
  // Apply app.config plugin inserts for certain features
865
687
  if (installedManifest?.name) {
866
- await applyAppConfigPlugins(installedManifest.name, paths, options);
688
+ await applyAppConfigPlugins(installedManifest, paths, options);
867
689
  }
868
690
  // Show manual steps with smart detection
869
691
  if (installedManifest?.manualSteps && !options.dryRun) {
870
- const { readFileContent, exists } = await import('../core/fsx.js');
871
- const { join } = await import('path');
872
- const normalize = (text) => text.replace(/\\n/g, '\n');
692
+ const { readFileContent, exists } = await import("../core/fsx.js");
693
+ const { join } = await import("path");
694
+ const normalize = (text) => text.replace(/\\n/g, "\n");
873
695
  const featureName = installedManifest?.name;
874
696
  // Check which steps might already be done
875
697
  const pendingSteps = [];
@@ -878,17 +700,20 @@ export async function installFeature(feature, options, paths) {
878
700
  let alreadyDone = false;
879
701
  const filePath = step.file ? join(paths.cwd, step.file) : null;
880
702
  // Wake-word specific checks (more lenient matching)
881
- if (featureName === 'wake-word' && step.file && filePath && (await exists(filePath))) {
703
+ if (featureName === "wake-word" &&
704
+ step.file &&
705
+ filePath &&
706
+ (await exists(filePath))) {
882
707
  const content = await readFileContent(filePath);
883
708
  const lowerTitle = step.title.toLowerCase();
884
- if (lowerTitle.includes('vosk plugin')) {
885
- if (content.includes('react-native-vosk')) {
709
+ if (lowerTitle.includes("vosk plugin")) {
710
+ if (content.includes("react-native-vosk")) {
886
711
  alreadyDone = true;
887
712
  completedSteps.push(step.title);
888
713
  }
889
714
  }
890
- else if (step.file === '.gitignore') {
891
- if (content.includes('assets/vosk-model/model-en-us')) {
715
+ else if (step.file === ".gitignore") {
716
+ if (content.includes("assets/vosk-model/model-en-us")) {
892
717
  alreadyDone = true;
893
718
  completedSteps.push(step.title);
894
719
  }
@@ -899,8 +724,8 @@ export async function installFeature(feature, options, paths) {
899
724
  if (filePath && (await exists(filePath))) {
900
725
  const content = await readFileContent(filePath);
901
726
  // Simple check: does the file contain the content to add?
902
- const contentToCheck = step.content.replace(/\s+/g, ' ').trim();
903
- const fileContentNormalized = content.replace(/\s+/g, ' ').trim();
727
+ const contentToCheck = step.content.replace(/\s+/g, " ").trim();
728
+ const fileContentNormalized = content.replace(/\s+/g, " ").trim();
904
729
  if (fileContentNormalized.includes(contentToCheck)) {
905
730
  alreadyDone = true;
906
731
  completedSteps.push(step.title);
@@ -918,30 +743,19 @@ export async function installFeature(feature, options, paths) {
918
743
  log.info(`Checklist saved to ${relPath}`);
919
744
  }
920
745
  log.info(`Run 'vf checklist ${installedManifest.name}' to see these steps again`);
921
- log.plain('');
746
+ log.plain("");
922
747
  }
923
748
  else if (completedSteps.length > 0) {
924
- log.success(`✓ Manual steps already satisfied: ${completedSteps.join(', ')}`);
925
- log.plain('');
749
+ log.success(`✓ Manual steps already satisfied: ${completedSteps.join(", ")}`);
750
+ log.plain("");
926
751
  }
927
752
  }
928
753
  if (installedEnvGroups.length > 0) {
929
- const additions = await ensureEnvVarsForGroups(installedEnvGroups, options);
930
- if (additions.length > 0) {
931
- log.plain('');
932
- const suffix = options.dryRun ? ' (dry run)' : '';
933
- log.info(`✅ Placeholder environment variables added${suffix}:`);
934
- additions.forEach(({ relativePath, added }) => {
935
- log.plain(` • ${relativePath}: ${added.join(', ')}`);
936
- });
937
- log.info('Update those files with your real API keys or secrets.');
938
- log.plain('');
939
- }
940
754
  installedEnvAttention = await reportEnvStatus(installedEnvGroups, options);
941
755
  }
942
756
  // Post-install message
943
757
  if (installedManifest?.postInstall?.message && !options.dryRun) {
944
- log.plain('');
758
+ log.plain("");
945
759
  log.info(installedManifest.postInstall.message);
946
760
  }
947
761
  // Final summary of what needs user attention
@@ -952,16 +766,39 @@ export async function installFeature(feature, options, paths) {
952
766
  }
953
767
  // Check manual steps
954
768
  if (installedManifest?.manualSteps) {
955
- const { readFileContent, exists } = await import('../core/fsx.js');
956
- const { join } = await import('path');
769
+ const { readFileContent, exists } = await import("../core/fsx.js");
770
+ const { join } = await import("path");
771
+ const featureName = installedManifest.name;
957
772
  for (const step of installedManifest.manualSteps) {
958
773
  let alreadyDone = false;
959
- if (step.file && step.content) {
774
+ // Wake-word: mark done if plugin/gitignore entries already exist
775
+ if (featureName === "wake-word" &&
776
+ step.file &&
777
+ step.title.toLowerCase().includes("vosk plugin")) {
778
+ const filePath = join(paths.cwd, step.file);
779
+ if (await exists(filePath)) {
780
+ const content = await readFileContent(filePath);
781
+ if (content.includes("react-native-vosk")) {
782
+ alreadyDone = true;
783
+ }
784
+ }
785
+ }
786
+ else if (featureName === "wake-word" &&
787
+ step.file === ".gitignore") {
788
+ const filePath = join(paths.cwd, step.file);
789
+ if (await exists(filePath)) {
790
+ const content = await readFileContent(filePath);
791
+ if (content.includes("assets/vosk-model/model-en-us")) {
792
+ alreadyDone = true;
793
+ }
794
+ }
795
+ }
796
+ if (!alreadyDone && step.file && step.content) {
960
797
  const filePath = join(paths.cwd, step.file);
961
798
  if (await exists(filePath)) {
962
799
  const content = await readFileContent(filePath);
963
- const contentToCheck = step.content.replace(/\s+/g, ' ').trim();
964
- const fileContentNormalized = content.replace(/\s+/g, ' ').trim();
800
+ const contentToCheck = step.content.replace(/\s+/g, " ").trim();
801
+ const fileContentNormalized = content.replace(/\s+/g, " ").trim();
965
802
  if (fileContentNormalized.includes(contentToCheck)) {
966
803
  alreadyDone = true;
967
804
  }
@@ -974,31 +811,198 @@ export async function installFeature(feature, options, paths) {
974
811
  }
975
812
  // Show summary
976
813
  if (needsAttention.length > 0) {
977
- log.plain('');
978
- log.plain('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
814
+ log.plain("");
815
+ log.plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
979
816
  log.warn(`⚠ ACTION REQUIRED: ${needsAttention.length} item(s) need your attention`);
980
- log.plain('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
981
- log.plain('');
817
+ log.plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
818
+ log.plain("");
982
819
  needsAttention.forEach((item, index) => {
983
820
  log.plain(` ${index + 1}. ${item}`);
984
821
  });
985
- log.plain('');
822
+ log.plain("");
986
823
  log.info(`💡 Run 'vf checklist ${installedManifest?.name ?? feature}' for detailed instructions`);
987
- log.plain('');
824
+ log.plain("");
988
825
  }
989
826
  else {
990
- log.plain('');
991
- log.success('🎉 All set! No manual configuration needed.');
992
- log.plain('');
827
+ log.plain("");
828
+ log.success("🎉 All set! No manual configuration needed.");
829
+ log.plain("");
993
830
  }
994
831
  }
995
832
  if (options.dryRun) {
996
- log.plain('');
997
- log.warn('This was a dry run. Run without --dry-run to apply changes.');
833
+ log.plain("");
834
+ log.warn("This was a dry run. Run without --dry-run to apply changes.");
998
835
  }
999
836
  }
1000
837
  catch (error) {
1001
838
  throw error;
1002
839
  }
1003
840
  }
841
+ function groupEnvVars(env, cwd) {
842
+ if (!env?.length) {
843
+ return [];
844
+ }
845
+ const groups = new Map();
846
+ for (const envVar of env) {
847
+ const relativePath = envVar.file ?? "apps/native/.env.local";
848
+ const resolvedPath = ensureWithinBase(cwd, resolve(cwd, relativePath), `Env file path ${relativePath}`);
849
+ const existing = groups.get(resolvedPath);
850
+ if (existing) {
851
+ existing.vars.push(envVar);
852
+ continue;
853
+ }
854
+ groups.set(resolvedPath, {
855
+ path: resolvedPath,
856
+ relativePath,
857
+ vars: [envVar],
858
+ });
859
+ }
860
+ return [...groups.values()];
861
+ }
862
+ async function reportEnvStatus(envGroups, options) {
863
+ const attention = [];
864
+ for (const group of envGroups) {
865
+ const missing = [];
866
+ for (const envVar of group.vars) {
867
+ const hasKey = await hasEnvKey(group.path, envVar.key);
868
+ if (!hasKey) {
869
+ missing.push(envVar);
870
+ }
871
+ }
872
+ if (missing.length > 0) {
873
+ log.plain("");
874
+ log.warn(`⚠ REQUIRED ENVIRONMENT VARIABLES (${group.relativePath}):`);
875
+ log.plain("");
876
+ missing.forEach((envVar) => {
877
+ log.plain(` ${envVar.key}`);
878
+ log.plain(` ${envVar.description}`);
879
+ log.plain(` Example: ${envVar.example}`);
880
+ if (envVar.link) {
881
+ log.plain(` Get it: ${envVar.link}`);
882
+ }
883
+ log.plain("");
884
+ attention.push(`Add ${envVar.key} to ${group.relativePath}`);
885
+ });
886
+ log.info(`Add these to ${group.relativePath}`);
887
+ log.plain("");
888
+ }
889
+ }
890
+ return attention;
891
+ }
892
+ async function applyEnvConfiguration(paths, envConfig, options) {
893
+ const envPath = join(paths.cwd, "apps", "native", "env.js");
894
+ if (!(await exists(envPath))) {
895
+ return;
896
+ }
897
+ const suffix = options.dryRun ? " (dry run)" : "";
898
+ if (envConfig.constants?.length) {
899
+ const inserted = await insertEnvSnippets(envPath, ENV_CONSTANTS_START, ENV_CONSTANTS_END, envConfig.constants, options);
900
+ if (inserted) {
901
+ log.info(`Updated env.js constants${suffix}.`);
902
+ }
903
+ }
904
+ if (envConfig.client?.schema?.length) {
905
+ const inserted = await insertEnvSnippets(envPath, ENV_CLIENT_SCHEMA_START, ENV_CLIENT_SCHEMA_END, envConfig.client.schema, options);
906
+ if (inserted) {
907
+ log.info(`Updated env.js client schema definitions${suffix}.`);
908
+ }
909
+ }
910
+ if (envConfig.client?.env?.length) {
911
+ const inserted = await insertEnvSnippets(envPath, ENV_CLIENT_ENV_START, ENV_CLIENT_ENV_END, envConfig.client.env, options);
912
+ if (inserted) {
913
+ log.info(`Updated env.js client env exports${suffix}.`);
914
+ }
915
+ }
916
+ if (envConfig.buildTime?.schema?.length) {
917
+ const inserted = await insertEnvSnippets(envPath, ENV_BUILD_TIME_SCHEMA_START, ENV_BUILD_TIME_SCHEMA_END, envConfig.buildTime.schema, options);
918
+ if (inserted) {
919
+ log.info(`Updated env.js build-time schema${suffix}.`);
920
+ }
921
+ }
922
+ if (envConfig.buildTime?.env?.length) {
923
+ const inserted = await insertEnvSnippets(envPath, ENV_BUILD_TIME_ENV_START, ENV_BUILD_TIME_ENV_END, envConfig.buildTime.env, options);
924
+ if (inserted) {
925
+ log.info(`Updated env.js build-time exports${suffix}.`);
926
+ }
927
+ }
928
+ }
929
+ const API_CLIENT_EXPORTS_START = "// --- @vibefast:api-client:exports:start ---";
930
+ const API_CLIENT_EXPORTS_END = "// --- @vibefast:api-client:exports:end ---";
931
+ async function applyApiClientConfiguration(paths, apiClientConfig, options) {
932
+ const apiClientIndexPath = join(paths.nativeDir, "src/api-client/index.ts");
933
+ if (!(await exists(apiClientIndexPath))) {
934
+ return;
935
+ }
936
+ const suffix = options.dryRun ? " (dry run)" : "";
937
+ if (apiClientConfig.exports?.length) {
938
+ const inserted = await insertEnvSnippets(apiClientIndexPath, API_CLIENT_EXPORTS_START, API_CLIENT_EXPORTS_END, apiClientConfig.exports, options);
939
+ if (inserted) {
940
+ log.info(`Updated api-client exports${suffix}.`);
941
+ }
942
+ }
943
+ }
944
+ async function applyAppConfigPlugins(manifest, paths, options) {
945
+ const appConfigPath = join(paths.nativeDir, "app.config.ts");
946
+ if (!(await exists(appConfigPath)))
947
+ return;
948
+ // Handle plugins
949
+ if (manifest.plugins?.length) {
950
+ if (options.dryRun) {
951
+ log.info(`[DRY RUN] Would add plugins to app.config.ts: ${manifest.plugins
952
+ .map((p) => p.name)
953
+ .join(", ")}`);
954
+ }
955
+ else {
956
+ for (const plugin of manifest.plugins) {
957
+ const added = await addAppConfigPlugin(appConfigPath, plugin.name, plugin.config);
958
+ if (added) {
959
+ log.info(`Added plugin ${plugin.name} to app.config.ts`);
960
+ }
961
+ else {
962
+ log.info(`Plugin ${plugin.name} already exists in app.config.ts`);
963
+ }
964
+ }
965
+ }
966
+ }
967
+ // Handle iosConfig (for ios-widget: appleTeamId and entitlements)
968
+ if (manifest.iosConfig) {
969
+ if (options.dryRun) {
970
+ log.info(`[DRY RUN] Would add ios config to app.config.ts`);
971
+ }
972
+ else {
973
+ const iosProps = {};
974
+ if (manifest.iosConfig.appleTeamId) {
975
+ iosProps.appleTeamId = manifest.iosConfig.appleTeamId;
976
+ }
977
+ if (manifest.iosConfig.entitlements) {
978
+ iosProps.entitlements = manifest.iosConfig.entitlements;
979
+ }
980
+ if (Object.keys(iosProps).length > 0) {
981
+ const added = await addAppConfigIosProperties(appConfigPath, iosProps);
982
+ if (added) {
983
+ log.info(`Added ios config to app.config.ts`);
984
+ }
985
+ else {
986
+ log.info(`ios config already exists in app.config.ts`);
987
+ }
988
+ }
989
+ }
990
+ }
991
+ // Run eslint --fix to format the modified file
992
+ if (!options.dryRun) {
993
+ try {
994
+ const result = spawnSync("pnpm", ["exec", "eslint", "--fix", appConfigPath], {
995
+ cwd: paths.cwd,
996
+ stdio: "pipe",
997
+ encoding: "utf-8",
998
+ });
999
+ if (result.status === 0) {
1000
+ log.info("✓ Formatted app.config.ts");
1001
+ }
1002
+ }
1003
+ catch {
1004
+ // Silently ignore - formatting is best-effort
1005
+ }
1006
+ }
1007
+ }
1004
1008
  //# sourceMappingURL=add.js.map