vibefast-cli 1.1.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +63 -169
  3. package/dist/__tests__/recipes.test.js +89 -85
  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 +576 -588
  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 +52 -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 +106 -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/imageAnalysis.ts +0 -1
  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 +90 -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 +86 -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 +72 -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/imageAnalysisFunctions.ts +0 -325
  291. package/recipes/image-analysis/packages/backend/convex/lib/ai/imageAnalysisAdapter.ts +0 -200
  292. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +0 -74
  293. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +0 -25
  294. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +0 -23
  295. package/recipes/quiz/apps/native/src/app/(root)/(protected)/quiz/index.tsx +0 -47
  296. package/recipes/tracker-app/apps/native/src/app/(root)/(protected)/tracker-app/index.tsx +0 -1
  297. package/recipes/voice-bot/apps/native/src/app/(root)/(protected)/voice-bot/index.tsx +0 -27
  298. package/recipes/voice-bot/packages/backend/convex/router.ts +0 -81
  299. /package/recipes/{chatbot/apps/native/src/app/(root)/(protected) → chatbot-supabase/apps/native/src/app}/chatbot/index.tsx +0 -0
  300. /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/gallery.tsx +0 -0
  301. /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,309 +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
- const fail = (msg) => {
344
- log.error(msg);
345
- throw new Error(msg);
346
- };
347
- // Ensure a plugins block exists; insert one if missing (before extra: or at end)
348
- const findIndex = (pattern) => {
349
- const match = pattern.exec(updated);
350
- return match ? match.index : -1;
351
- };
352
- let pluginsStart = findIndex(/plugins\s*:\s*\[/);
353
- let extraStart = findIndex(/\n\s+extra:/);
354
- if (pluginsStart === -1) {
355
- const block = ` plugins: [\n ],\n\n`;
356
- if (extraStart !== -1) {
357
- updated = `${updated.slice(0, extraStart)}${block}${updated.slice(extraStart)}`;
358
- }
359
- else {
360
- // Append before the final closing of the config object
361
- const insertPos = updated.lastIndexOf('};');
362
- updated =
363
- insertPos !== -1
364
- ? `${updated.slice(0, insertPos)}${block}${updated.slice(insertPos)}`
365
- : `${updated.trimEnd()}\n\n${block}};`;
366
- }
367
- // recompute positions
368
- pluginsStart = findIndex(/plugins\s*:\s*\[/);
369
- extraStart = findIndex(/\n\s+extra:/);
370
- }
371
- if (pluginsStart === -1) {
372
- fail('Could not locate plugins block in app.config.ts; aborting plugin auto-insert.');
373
- }
374
- if (extraStart === -1) {
375
- extraStart = updated.length;
376
- }
377
- // Boundaries for plugins block (tolerant of nested arrays)
378
- const firstBracket = updated.indexOf('[', pluginsStart);
379
- let depth = 0;
380
- let pluginsClose = -1;
381
- for (let i = firstBracket; i < updated.length; i++) {
382
- const ch = updated[i];
383
- if (ch === '[')
384
- depth += 1;
385
- if (ch === ']') {
386
- depth -= 1;
387
- if (depth === 0) {
388
- pluginsClose = i;
389
- break;
390
- }
391
- }
392
- if (i >= extraStart && extraStart !== updated.length && depth <= 0)
393
- break;
394
- }
395
- if (pluginsClose === -1) {
396
- fail('Could not locate plugins closing bracket; aborting plugin auto-insert.');
397
- }
398
- const addBlock = (block, needle) => {
399
- if (updated.includes(needle))
400
- return;
401
- updated = `${updated.slice(0, pluginsClose)} ${block}\n${updated.slice(pluginsClose)}`;
402
- };
403
- const findBlockRange = (label) => {
404
- const idx = updated.indexOf(label);
405
- if (idx === -1)
406
- return null;
407
- const startBrace = updated.indexOf('{', idx);
408
- if (startBrace === -1)
409
- return null;
410
- let depthLocal = 0;
411
- for (let i = startBrace; i < updated.length; i++) {
412
- const ch = updated[i];
413
- if (ch === '{')
414
- depthLocal += 1;
415
- if (ch === '}') {
416
- depthLocal -= 1;
417
- if (depthLocal === 0)
418
- return { start: idx, end: i + 1 };
419
- }
420
- }
421
- return null;
422
- };
423
- const patchIosBlock = () => {
424
- let iosRange = findBlockRange('ios:');
425
- // If ios block is missing, inject a minimal one before plugins.
426
- if (!iosRange) {
427
- 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: [`);
428
- iosRange = findBlockRange('ios:');
429
- }
430
- if (!iosRange) {
431
- fail('Could not locate or create ios block in app.config.ts; aborting plugin auto-insert.');
432
- }
433
- const block = updated.slice(iosRange.start, iosRange.end);
434
- let patched = block;
435
- if (!patched.includes('appleTeamId')) {
436
- patched = patched.replace(/bundleIdentifier:\s*Env\.BUNDLE_ID,?/, (m) => `${m}\n appleTeamId: Env.APPLE_TEAM_ID,`);
437
- if (patched === block) {
438
- patched = patched.replace('{', '{\n appleTeamId: Env.APPLE_TEAM_ID,');
439
- }
440
- }
441
- if (!patched.includes('com.apple.security.application-groups')) {
442
- if (patched.includes('entitlements:')) {
443
- 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 }`);
444
- }
445
- else {
446
- patched = patched.replace(/config:\s*{[^}]*},/, (m) => `${m}\n entitlements: {\n 'com.apple.security.application-groups': ['group.' + Env.BUNDLE_ID],\n },`);
447
- }
448
- }
449
- updated = updated.replace(block, patched);
450
- };
451
- if (feature === 'ios-widget') {
452
- addBlock(`[
453
- '@bacons/apple-targets',
454
- {
455
- targets: [
456
- {
457
- name: 'VibeFastWidget',
458
- type: 'widget',
459
- bundleIdentifier: \`\${Env.BUNDLE_ID}.widget\`,
460
- entitlements: {
461
- 'com.apple.security.application-groups': [
462
- 'group.' + Env.BUNDLE_ID,
463
- ],
464
- },
465
- },
466
- ],
467
- },
468
- ],`, '@bacons/apple-targets');
469
- patchIosBlock();
470
- }
471
- if (feature === 'voice-bot') {
472
- addBlock(`'@livekit/react-native-expo-plugin',`, '@livekit/react-native-expo-plugin');
473
- addBlock(`'@config-plugins/react-native-webrtc',`, '@config-plugins/react-native-webrtc');
474
- patchIosBlock();
475
- }
476
- if (feature === 'wake-word') {
477
- addBlock(`'expo-audio',`, `'expo-audio'`);
478
- addBlock(`[
479
- 'react-native-vosk',
480
- {
481
- models: ['assets/vosk-model/model-en-us'],
482
- iOSMicrophonePermission: 'We use the mic for on-device wake-phrase detection.',
483
- },
484
- ],`, 'react-native-vosk');
485
- patchIosBlock();
486
- }
487
- if (updated !== original && !options?.dryRun) {
488
- await writeFileContent(appConfigPath, updated, { force: true });
489
- log.info('Updated app.config.ts plugins for feature requirements.');
490
- }
491
- }
492
169
  // Validate setup
493
170
  const config = await validateSignature(paths.signatureFile);
494
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
+ }
495
188
  // Check auth
496
189
  const token = await getToken();
497
190
  if (!token) {
@@ -502,10 +195,10 @@ export async function installFeature(feature, options, paths) {
502
195
  const existing = await getEntry(paths.journalFile, feature, target);
503
196
  if (existing && !options.force && !options.yes) {
504
197
  log.warn(`${feature} is already installed for ${target}`);
505
- 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);
506
199
  if (!shouldReinstall) {
507
- log.info('Skipped. Use --force to reinstall without prompting');
508
- log.plain('');
200
+ log.info("Skipped. Use --force to reinstall without prompting");
201
+ log.plain("");
509
202
  return;
510
203
  }
511
204
  // Auto-enable force mode for reinstall to avoid per-file prompts
@@ -513,20 +206,20 @@ export async function installFeature(feature, options, paths) {
513
206
  }
514
207
  else if (existing && !options.force) {
515
208
  log.warn(`${feature} is already installed for ${target}`);
516
- log.info('Use --force to reinstall');
517
- log.plain('');
209
+ log.info("Use --force to reinstall");
210
+ log.plain("");
518
211
  return;
519
212
  }
520
213
  if (options.dryRun) {
521
- log.info('[DRY RUN] No changes will be made');
214
+ log.info("[DRY RUN] No changes will be made");
522
215
  }
523
216
  // Fetch recipe
524
217
  const device = await getDeviceInfo();
525
- const response = await withSpinner(`Fetching ${feature} for ${target}...`, async () => {
218
+ const response = await withSpinner(`Fetching ${recipeName} for ${target}...`, async () => {
526
219
  return await fetchRecipe({
527
220
  token,
528
221
  device,
529
- feature,
222
+ feature: recipeName,
530
223
  target,
531
224
  starter: { name: config.name, version: config.version },
532
225
  });
@@ -537,85 +230,89 @@ export async function installFeature(feature, options, paths) {
537
230
  let extractDir = null;
538
231
  if (!response.ok || (!response.signedUrl && !response.zipData)) {
539
232
  // Remote fetch failed; try bundled fallback
540
- const error = response.error || 'Unknown error';
541
- const message = response.message || '';
542
- const localZip = await getBundledRecipeZipPath(feature);
233
+ const error = response.error || "Unknown error";
234
+ const message = response.message || "";
235
+ const localZip = await getBundledRecipeZipPath(recipeName);
543
236
  if (localZip) {
544
237
  log.info(`Remote recipe unavailable (${error}). Using bundled recipe.`);
545
238
  zipPath = localZip;
546
239
  }
547
240
  else {
548
- log.plain('');
241
+ log.plain("");
549
242
  // User-friendly error messages
550
- if (error.includes('Invalid') || error.includes('token') || error.includes('license')) {
551
- log.plain('❌ Invalid or expired license key');
552
- log.plain('');
553
- log.info('Your license key may be:');
554
- log.plain(' • Incorrect or mistyped');
555
- log.plain(' • Expired');
556
- log.plain('Revoked');
557
- log.plain('');
558
- log.info('To fix this:');
559
- log.plain(' 1. Check your license key from your purchase receipt');
560
- log.plain(' 2. Run: vf logout');
561
- log.plain(' 3. Run: vf login --token YOUR_CORRECT_TOKEN');
562
- log.plain('');
563
- log.info('Need help? Contact support@vibefast.pro');
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");
564
259
  }
565
- else if (error.includes('Device limit') || error.includes('device') || message.includes('device')) {
566
- log.plain('❌ Device limit reached');
567
- log.plain('');
568
- log.info('You have reached the maximum number of devices for your license');
569
- log.plain('');
570
- log.info('To fix this:');
571
- log.plain(' 1. Run: vf devices');
572
- log.plain(' 2. Deactivate an unused device: vf devices --deactivate <device-id>');
573
- log.plain(' 3. Try again: vf add ' + feature);
574
- log.plain('');
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("");
575
272
  if (message) {
576
273
  log.plain(`Details: ${message}`);
577
- log.plain('');
274
+ log.plain("");
578
275
  }
579
276
  }
580
- else if (error.includes('Network') || error.includes('connect')) {
581
- log.plain('❌ Network error');
582
- log.plain('');
583
- log.info('Could not connect to VibeFast servers');
584
- log.plain('');
585
- log.info('Please check:');
586
- log.plain(' • Your internet connection');
587
- log.plain(' • Firewall settings');
588
- log.plain(' • VPN configuration');
589
- 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("");
590
287
  if (message) {
591
288
  log.plain(`Details: ${message}`);
592
- log.plain('');
289
+ log.plain("");
593
290
  }
594
291
  }
595
- else if (error.includes('not found') || error.includes('404')) {
596
- log.plain('❌ Feature not found');
597
- log.plain('');
292
+ else if (error.includes("not found") || error.includes("404")) {
293
+ log.plain("❌ Feature not found");
294
+ log.plain("");
598
295
  log.info(`The feature "${feature}" does not exist or is not available for ${target}`);
599
- log.plain('');
600
- log.info('To see available features:');
601
- log.plain(' vf list');
602
- log.plain('');
296
+ log.plain("");
297
+ log.info("To see available features:");
298
+ log.plain(" vf list");
299
+ log.plain("");
603
300
  }
604
301
  else {
605
302
  // Generic error
606
303
  log.plain(`❌ ${error}`);
607
304
  if (message) {
608
- log.plain('');
305
+ log.plain("");
609
306
  log.plain(`Details: ${message}`);
610
307
  }
611
- log.plain('');
612
- log.info('If this problem persists, contact support@vibefast.pro');
308
+ log.plain("");
309
+ log.info("If this problem persists, contact support@vibefast.pro");
613
310
  }
614
- log.plain('');
311
+ log.plain("");
615
312
  process.exit(1);
616
313
  }
617
314
  }
618
- const fallbackZip = await getBundledRecipeZipPath(feature);
315
+ const fallbackZip = await getBundledRecipeZipPath(recipeName);
619
316
  let attemptedFallback = false;
620
317
  let installedManifest = null;
621
318
  let installedEnvGroups = [];
@@ -629,30 +326,30 @@ export async function installFeature(feature, options, paths) {
629
326
  try {
630
327
  // Download and extract (or use bundled zip)
631
328
  if (!zipPath) {
632
- const result = await withSpinner('Downloading and extracting recipe...', async () => {
329
+ const result = await withSpinner("Downloading and extracting recipe...", async () => {
633
330
  const zip = response.zipData
634
331
  ? await downloadZip(response.zipData, true)
635
332
  : await downloadZip(response.signedUrl);
636
- const dir = join(tmpdir(), 'vibefast', randomUUID());
333
+ const dir = join(tmpdir(), "vibefast", randomUUID());
637
334
  await extractZipSafe(zip, dir);
638
335
  return { zipPath: zip, extractDir: dir };
639
336
  }, {
640
- successText: '✓ Recipe downloaded',
337
+ successText: "✓ Recipe downloaded",
641
338
  });
642
339
  zipPath = result.zipPath;
643
340
  extractDir = result.extractDir;
644
341
  }
645
342
  else {
646
343
  // Bundled zip path - extract to temp location
647
- extractDir = join(tmpdir(), 'vibefast', randomUUID());
344
+ extractDir = join(tmpdir(), "vibefast", randomUUID());
648
345
  await extractZipSafe(zipPath, extractDir);
649
346
  }
650
347
  // Locate manifest (some archives may nest recipe.json)
651
348
  const findManifest = async (dir) => {
652
- 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 }));
653
350
  for (const entry of entries) {
654
351
  const full = join(dir, entry.name);
655
- if (entry.isFile() && entry.name === 'recipe.json')
352
+ if (entry.isFile() && entry.name === "recipe.json")
656
353
  return full;
657
354
  if (entry.isDirectory()) {
658
355
  const found = await findManifest(full);
@@ -662,7 +359,7 @@ export async function installFeature(feature, options, paths) {
662
359
  }
663
360
  return null;
664
361
  };
665
- let manifestPath = join(extractDir, 'recipe.json');
362
+ let manifestPath = join(extractDir, "recipe.json");
666
363
  if (!(await exists(manifestPath))) {
667
364
  const found = await findManifest(extractDir);
668
365
  if (!found) {
@@ -681,20 +378,22 @@ export async function installFeature(feature, options, paths) {
681
378
  if (!Array.isArray(manifest.copy)) {
682
379
  throw new Error('recipe.json is missing a valid "copy" array');
683
380
  }
684
- const extractRoot = resolve(manifestPath, '..');
381
+ const extractRoot = resolve(manifestPath, "..");
685
382
  const repoRoot = resolve(paths.cwd);
686
383
  const manifestTarget = manifest.target ??
687
- (Array.isArray(manifest.platforms) && manifest.platforms.includes('native')
688
- ? 'native'
689
- : Array.isArray(manifest.platforms) && manifest.platforms.includes('web')
690
- ? '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"
691
390
  : undefined);
692
391
  if (!manifestTarget) {
693
392
  throw new Error(`Recipe target missing for ${manifest.name}`);
694
393
  }
695
394
  manifest.target = manifestTarget;
696
395
  if (manifestTarget !== target) {
697
- throw new Error(`Recipe target mismatch: expected ${target}, got ${manifestTarget ?? 'undefined'}`);
396
+ throw new Error(`Recipe target mismatch: expected ${target}, got ${manifestTarget ?? "undefined"}`);
698
397
  }
699
398
  log.info(`Installing ${manifest.name} v${manifest.version}...`);
700
399
  const envGroups = groupEnvVars(manifest.env, paths.cwd);
@@ -718,17 +417,52 @@ export async function installFeature(feature, options, paths) {
718
417
  }
719
418
  // Show conflict warnings in dry-run mode
720
419
  if (options.dryRun && allConflicts.length > 0) {
721
- log.plain('');
420
+ log.plain("");
722
421
  log.warn(`⚠ ${allConflicts.length} file(s) will be overwritten:`);
723
- allConflicts.slice(0, 5).forEach(f => {
724
- const relativePath = f.replace(paths.cwd + '/', '');
422
+ allConflicts.slice(0, 5).forEach((f) => {
423
+ const relativePath = relative(paths.cwd, f);
725
424
  log.plain(` • ${relativePath}`);
726
425
  });
727
426
  if (allConflicts.length > 5) {
728
427
  log.plain(` ... and ${allConflicts.length - 5} more`);
729
428
  }
730
- log.warn('⚠ Make sure you have committed your changes to Git!');
731
- 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
+ }
732
466
  }
733
467
  // Show skipped files
734
468
  if (allSkipped.length > 0) {
@@ -736,9 +470,12 @@ export async function installFeature(feature, options, paths) {
736
470
  }
737
471
  // Add watermark if provided
738
472
  if (response.watermark && !options.dryRun) {
739
- const { writeFileContent, readFileContent } = await import('../core/fsx.js');
473
+ const { writeFileContent, readFileContent } = await import("../core/fsx.js");
740
474
  for (const file of copiedFiles) {
741
- 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")) {
742
479
  const content = await readFileContent(file);
743
480
  const watermarked = `// vibefast license: ${response.watermark}\n${content}`;
744
481
  await writeFileContent(file, watermarked, { force: true });
@@ -750,23 +487,27 @@ export async function installFeature(feature, options, paths) {
750
487
  let navHref;
751
488
  let navLabel;
752
489
  if (manifest.nav) {
753
- log.info('Adding navigation link...');
754
- const navFile = target === 'native' ? paths.nativeNavFile : paths.webNavFile;
755
- 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;
756
493
  navHref = manifest.nav.href;
757
494
  navLabel = manifest.nav.label;
758
- navInserted = await insertFn(navFile, manifest.nav, { dryRun: options.dryRun });
495
+ navInserted = await insertFn(navFile, manifest.nav, {
496
+ dryRun: options.dryRun,
497
+ });
759
498
  if (navInserted) {
760
- log.success('Navigation link added');
499
+ log.success("Navigation link added");
761
500
  }
762
501
  else {
763
- log.info('Navigation link already exists');
502
+ log.info("Navigation link already exists");
764
503
  }
765
504
  }
766
505
  // Hash files and update journal
767
506
  if (!options.dryRun) {
768
- log.info('Computing file hashes...');
769
- 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
+ });
770
511
  const fileEntries = Array.from(fileHashes.entries()).map(([path, hash]) => ({
771
512
  path,
772
513
  hash,
@@ -799,7 +540,9 @@ export async function installFeature(feature, options, paths) {
799
540
  break retry_install;
800
541
  }
801
542
  catch (err) {
802
- 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) {
803
546
  attemptedFallback = true;
804
547
  zipPath = fallbackZip;
805
548
  extractDir = null;
@@ -810,112 +553,149 @@ export async function installFeature(feature, options, paths) {
810
553
  }
811
554
  }
812
555
  // Auto-install dependencies
813
- if (installedManifest && installedManifest.dependencies && !options.dryRun) {
814
- log.plain('');
815
- log.warn('⚠ This feature requires additional packages');
816
- log.plain('');
817
- 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") {
818
563
  const packages = Array.from(new Set([
819
564
  ...(installedManifest.dependencies.expo ?? []),
820
565
  ...(installedManifest.dependencies.npm ?? []),
821
566
  ]));
822
567
  if (packages.length > 0) {
823
- log.info('📦 Required packages:');
824
- 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) => {
825
577
  log.plain(` • ${pkg}`);
826
578
  });
827
- log.plain('');
828
- 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)));
829
583
  if (shouldInstall) {
830
- const storeDir = join(paths.cwd, '.pnpm-store');
831
- await withSpinner('Installing packages with Expo...', async () => {
832
- const { execSync } = await import('child_process');
833
- 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");
834
587
  try {
835
- 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], {
836
590
  cwd: nativeDir,
837
- stdio: 'inherit',
591
+ stdio: "inherit",
838
592
  env: {
839
593
  ...process.env,
840
594
  PNPM_STORE_PATH: storeDir,
841
595
  },
596
+ shell: false,
842
597
  });
598
+ if (result.error) {
599
+ throw result.error;
600
+ }
843
601
  }
844
602
  catch (err) {
845
- 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.");
846
604
  if (err?.message) {
847
605
  log.warn(err.message);
848
606
  }
849
607
  }
850
608
  }, {
851
- successText: '✓ Packages installed',
609
+ successText: "✓ Packages installed",
852
610
  });
853
611
  }
854
612
  else {
855
- log.info('Install manually with:');
856
- log.plain(` pnpx expo install ${packages.join(' ')}`);
857
- log.plain('');
858
- 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");
859
617
  }
860
618
  }
861
619
  }
862
- else if (installedManifest.target === 'web' && installedManifest.dependencies.npm) {
620
+ else if (installedManifest.target === "web" &&
621
+ installedManifest.dependencies.npm) {
863
622
  const packages = installedManifest.dependencies.npm;
864
- log.info('📦 Required packages:');
865
- 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) => {
866
632
  log.plain(` • ${pkg}`);
867
633
  });
868
- log.plain('');
869
- 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)));
870
638
  if (shouldInstall) {
871
- const { detectPackageManager } = await import('../core/detect.js');
872
- const pkgManager = detectPackageManager(paths.cwd) || 'pnpm';
639
+ const { detectPackageManager } = await import("../core/detect.js");
640
+ const pkgManager = detectPackageManager(paths.cwd) || "pnpm";
873
641
  await withSpinner(`Installing packages with ${pkgManager}...`, async () => {
874
- const { execSync } = await import('child_process');
875
- const installCmd = pkgManager === 'npm'
876
- ? `npm install ${packages.join(' ')}`
877
- : `${pkgManager} add ${packages.join(' ')}`;
878
- 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, {
879
647
  cwd: paths.cwd,
880
- stdio: 'inherit',
648
+ stdio: "inherit",
649
+ shell: false,
881
650
  });
651
+ if (result.error) {
652
+ throw result.error;
653
+ }
882
654
  }, {
883
- successText: '✓ Packages installed',
655
+ successText: "✓ Packages installed",
884
656
  });
885
657
  }
886
658
  else {
887
- log.info('Install manually with:');
888
- log.plain(` pnpm add ${packages.join(' ')}`);
889
- log.plain(' OR');
890
- 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(" ")}`);
891
663
  log.plain(` OR`);
892
- log.plain(` npm install ${packages.join(' ')}`);
664
+ log.plain(` npm install ${packages.join(" ")}`);
893
665
  }
894
666
  }
895
- log.plain('');
667
+ log.plain("");
896
668
  }
897
669
  // Auto-setup Vosk model for wake-word
898
- if (installedManifest?.name === 'wake-word' && !options.dryRun) {
899
- const { setupVoskModel } = await import('../core/vosk.js');
900
- log.plain('');
901
- 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 () => {
902
674
  return await setupVoskModel(paths.cwd, { dryRun: options.dryRun });
903
675
  }, {
904
- successText: '✓ Vosk model ready',
676
+ successText: "✓ Vosk model ready",
905
677
  });
906
678
  }
907
679
  if (installedManifest?.configuration?.env) {
908
680
  await applyEnvConfiguration(paths, installedManifest.configuration.env, options);
909
681
  }
682
+ // Apply api-client exports injection
683
+ if (installedManifest?.configuration?.apiClient) {
684
+ await applyApiClientConfiguration(paths, installedManifest.configuration.apiClient, options);
685
+ }
686
+ // Apply backend exports injection
687
+ if (installedManifest?.configuration?.backend) {
688
+ await applyBackendConfiguration(paths, installedManifest.configuration.backend, options);
689
+ }
910
690
  // Apply app.config plugin inserts for certain features
911
691
  if (installedManifest?.name) {
912
- await applyAppConfigPlugins(installedManifest.name, paths, options);
692
+ await applyAppConfigPlugins(installedManifest, paths, options);
913
693
  }
914
694
  // Show manual steps with smart detection
915
695
  if (installedManifest?.manualSteps && !options.dryRun) {
916
- const { readFileContent, exists } = await import('../core/fsx.js');
917
- const { join } = await import('path');
918
- const normalize = (text) => text.replace(/\\n/g, '\n');
696
+ const { readFileContent, exists } = await import("../core/fsx.js");
697
+ const { join } = await import("path");
698
+ const normalize = (text) => text.replace(/\\n/g, "\n");
919
699
  const featureName = installedManifest?.name;
920
700
  // Check which steps might already be done
921
701
  const pendingSteps = [];
@@ -924,17 +704,20 @@ export async function installFeature(feature, options, paths) {
924
704
  let alreadyDone = false;
925
705
  const filePath = step.file ? join(paths.cwd, step.file) : null;
926
706
  // Wake-word specific checks (more lenient matching)
927
- if (featureName === 'wake-word' && step.file && filePath && (await exists(filePath))) {
707
+ if (featureName === "wake-word" &&
708
+ step.file &&
709
+ filePath &&
710
+ (await exists(filePath))) {
928
711
  const content = await readFileContent(filePath);
929
712
  const lowerTitle = step.title.toLowerCase();
930
- if (lowerTitle.includes('vosk plugin')) {
931
- if (content.includes('react-native-vosk')) {
713
+ if (lowerTitle.includes("vosk plugin")) {
714
+ if (content.includes("react-native-vosk")) {
932
715
  alreadyDone = true;
933
716
  completedSteps.push(step.title);
934
717
  }
935
718
  }
936
- else if (step.file === '.gitignore') {
937
- if (content.includes('assets/vosk-model/model-en-us')) {
719
+ else if (step.file === ".gitignore") {
720
+ if (content.includes("assets/vosk-model/model-en-us")) {
938
721
  alreadyDone = true;
939
722
  completedSteps.push(step.title);
940
723
  }
@@ -945,8 +728,8 @@ export async function installFeature(feature, options, paths) {
945
728
  if (filePath && (await exists(filePath))) {
946
729
  const content = await readFileContent(filePath);
947
730
  // Simple check: does the file contain the content to add?
948
- const contentToCheck = step.content.replace(/\s+/g, ' ').trim();
949
- const fileContentNormalized = content.replace(/\s+/g, ' ').trim();
731
+ const contentToCheck = step.content.replace(/\s+/g, " ").trim();
732
+ const fileContentNormalized = content.replace(/\s+/g, " ").trim();
950
733
  if (fileContentNormalized.includes(contentToCheck)) {
951
734
  alreadyDone = true;
952
735
  completedSteps.push(step.title);
@@ -964,30 +747,19 @@ export async function installFeature(feature, options, paths) {
964
747
  log.info(`Checklist saved to ${relPath}`);
965
748
  }
966
749
  log.info(`Run 'vf checklist ${installedManifest.name}' to see these steps again`);
967
- log.plain('');
750
+ log.plain("");
968
751
  }
969
752
  else if (completedSteps.length > 0) {
970
- log.success(`✓ Manual steps already satisfied: ${completedSteps.join(', ')}`);
971
- log.plain('');
753
+ log.success(`✓ Manual steps already satisfied: ${completedSteps.join(", ")}`);
754
+ log.plain("");
972
755
  }
973
756
  }
974
757
  if (installedEnvGroups.length > 0) {
975
- const additions = await ensureEnvVarsForGroups(installedEnvGroups, options);
976
- if (additions.length > 0) {
977
- log.plain('');
978
- const suffix = options.dryRun ? ' (dry run)' : '';
979
- log.info(`✅ Placeholder environment variables added${suffix}:`);
980
- additions.forEach(({ relativePath, added }) => {
981
- log.plain(` • ${relativePath}: ${added.join(', ')}`);
982
- });
983
- log.info('Update those files with your real API keys or secrets.');
984
- log.plain('');
985
- }
986
758
  installedEnvAttention = await reportEnvStatus(installedEnvGroups, options);
987
759
  }
988
760
  // Post-install message
989
761
  if (installedManifest?.postInstall?.message && !options.dryRun) {
990
- log.plain('');
762
+ log.plain("");
991
763
  log.info(installedManifest.postInstall.message);
992
764
  }
993
765
  // Final summary of what needs user attention
@@ -998,16 +770,39 @@ export async function installFeature(feature, options, paths) {
998
770
  }
999
771
  // Check manual steps
1000
772
  if (installedManifest?.manualSteps) {
1001
- const { readFileContent, exists } = await import('../core/fsx.js');
1002
- const { join } = await import('path');
773
+ const { readFileContent, exists } = await import("../core/fsx.js");
774
+ const { join } = await import("path");
775
+ const featureName = installedManifest.name;
1003
776
  for (const step of installedManifest.manualSteps) {
1004
777
  let alreadyDone = false;
1005
- if (step.file && step.content) {
778
+ // Wake-word: mark done if plugin/gitignore entries already exist
779
+ if (featureName === "wake-word" &&
780
+ step.file &&
781
+ step.title.toLowerCase().includes("vosk plugin")) {
1006
782
  const filePath = join(paths.cwd, step.file);
1007
783
  if (await exists(filePath)) {
1008
784
  const content = await readFileContent(filePath);
1009
- const contentToCheck = step.content.replace(/\s+/g, ' ').trim();
1010
- const fileContentNormalized = content.replace(/\s+/g, ' ').trim();
785
+ if (content.includes("react-native-vosk")) {
786
+ alreadyDone = true;
787
+ }
788
+ }
789
+ }
790
+ else if (featureName === "wake-word" &&
791
+ step.file === ".gitignore") {
792
+ const filePath = join(paths.cwd, step.file);
793
+ if (await exists(filePath)) {
794
+ const content = await readFileContent(filePath);
795
+ if (content.includes("assets/vosk-model/model-en-us")) {
796
+ alreadyDone = true;
797
+ }
798
+ }
799
+ }
800
+ if (!alreadyDone && step.file && step.content) {
801
+ const filePath = join(paths.cwd, step.file);
802
+ if (await exists(filePath)) {
803
+ const content = await readFileContent(filePath);
804
+ const contentToCheck = step.content.replace(/\s+/g, " ").trim();
805
+ const fileContentNormalized = content.replace(/\s+/g, " ").trim();
1011
806
  if (fileContentNormalized.includes(contentToCheck)) {
1012
807
  alreadyDone = true;
1013
808
  }
@@ -1020,31 +815,224 @@ export async function installFeature(feature, options, paths) {
1020
815
  }
1021
816
  // Show summary
1022
817
  if (needsAttention.length > 0) {
1023
- log.plain('');
1024
- log.plain('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
818
+ log.plain("");
819
+ log.plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1025
820
  log.warn(`⚠ ACTION REQUIRED: ${needsAttention.length} item(s) need your attention`);
1026
- log.plain('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1027
- log.plain('');
821
+ log.plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
822
+ log.plain("");
1028
823
  needsAttention.forEach((item, index) => {
1029
824
  log.plain(` ${index + 1}. ${item}`);
1030
825
  });
1031
- log.plain('');
826
+ log.plain("");
1032
827
  log.info(`💡 Run 'vf checklist ${installedManifest?.name ?? feature}' for detailed instructions`);
1033
- log.plain('');
828
+ log.plain("");
1034
829
  }
1035
830
  else {
1036
- log.plain('');
1037
- log.success('🎉 All set! No manual configuration needed.');
1038
- log.plain('');
831
+ log.plain("");
832
+ log.success("🎉 All set! No manual configuration needed.");
833
+ log.plain("");
1039
834
  }
1040
835
  }
1041
836
  if (options.dryRun) {
1042
- log.plain('');
1043
- log.warn('This was a dry run. Run without --dry-run to apply changes.');
837
+ log.plain("");
838
+ log.warn("This was a dry run. Run without --dry-run to apply changes.");
1044
839
  }
1045
840
  }
1046
841
  catch (error) {
1047
842
  throw error;
1048
843
  }
1049
844
  }
845
+ function groupEnvVars(env, cwd) {
846
+ if (!env?.length) {
847
+ return [];
848
+ }
849
+ const groups = new Map();
850
+ for (const envVar of env) {
851
+ const relativePath = envVar.file ?? "apps/native/.env.local";
852
+ const resolvedPath = ensureWithinBase(cwd, resolve(cwd, relativePath), `Env file path ${relativePath}`);
853
+ const existing = groups.get(resolvedPath);
854
+ if (existing) {
855
+ existing.vars.push(envVar);
856
+ continue;
857
+ }
858
+ groups.set(resolvedPath, {
859
+ path: resolvedPath,
860
+ relativePath,
861
+ vars: [envVar],
862
+ });
863
+ }
864
+ return [...groups.values()];
865
+ }
866
+ async function reportEnvStatus(envGroups, options) {
867
+ const attention = [];
868
+ for (const group of envGroups) {
869
+ const missing = [];
870
+ for (const envVar of group.vars) {
871
+ const hasKey = await hasEnvKey(group.path, envVar.key);
872
+ if (!hasKey) {
873
+ missing.push(envVar);
874
+ }
875
+ }
876
+ if (missing.length > 0) {
877
+ log.plain("");
878
+ log.warn(`⚠ REQUIRED ENVIRONMENT VARIABLES (${group.relativePath}):`);
879
+ log.plain("");
880
+ missing.forEach((envVar) => {
881
+ log.plain(` ${envVar.key}`);
882
+ log.plain(` ${envVar.description}`);
883
+ log.plain(` Example: ${envVar.example}`);
884
+ if (envVar.link) {
885
+ log.plain(` Get it: ${envVar.link}`);
886
+ }
887
+ log.plain("");
888
+ attention.push(`Add ${envVar.key} to ${group.relativePath}`);
889
+ });
890
+ log.info(`Add these to ${group.relativePath}`);
891
+ log.plain("");
892
+ }
893
+ }
894
+ return attention;
895
+ }
896
+ async function applyEnvConfiguration(paths, envConfig, options) {
897
+ const envPath = join(paths.cwd, "apps", "native", "env.js");
898
+ if (!(await exists(envPath))) {
899
+ return;
900
+ }
901
+ const suffix = options.dryRun ? " (dry run)" : "";
902
+ if (envConfig.constants?.length) {
903
+ const inserted = await insertEnvSnippets(envPath, ENV_CONSTANTS_START, ENV_CONSTANTS_END, envConfig.constants, options);
904
+ if (inserted) {
905
+ log.info(`Updated env.js constants${suffix}.`);
906
+ }
907
+ }
908
+ if (envConfig.client?.schema?.length) {
909
+ const inserted = await insertEnvSnippets(envPath, ENV_CLIENT_SCHEMA_START, ENV_CLIENT_SCHEMA_END, envConfig.client.schema, options);
910
+ if (inserted) {
911
+ log.info(`Updated env.js client schema definitions${suffix}.`);
912
+ }
913
+ }
914
+ if (envConfig.client?.env?.length) {
915
+ const inserted = await insertEnvSnippets(envPath, ENV_CLIENT_ENV_START, ENV_CLIENT_ENV_END, envConfig.client.env, options);
916
+ if (inserted) {
917
+ log.info(`Updated env.js client env exports${suffix}.`);
918
+ }
919
+ }
920
+ if (envConfig.buildTime?.schema?.length) {
921
+ const inserted = await insertEnvSnippets(envPath, ENV_BUILD_TIME_SCHEMA_START, ENV_BUILD_TIME_SCHEMA_END, envConfig.buildTime.schema, options);
922
+ if (inserted) {
923
+ log.info(`Updated env.js build-time schema${suffix}.`);
924
+ }
925
+ }
926
+ if (envConfig.buildTime?.env?.length) {
927
+ const inserted = await insertEnvSnippets(envPath, ENV_BUILD_TIME_ENV_START, ENV_BUILD_TIME_ENV_END, envConfig.buildTime.env, options);
928
+ if (inserted) {
929
+ log.info(`Updated env.js build-time exports${suffix}.`);
930
+ }
931
+ }
932
+ }
933
+ const API_CLIENT_EXPORTS_START = "// --- @vibefast:api-client:exports:start ---";
934
+ const API_CLIENT_EXPORTS_END = "// --- @vibefast:api-client:exports:end ---";
935
+ const BACKEND_EXPORTS_START = "// --- @vibefast:backend:exports:start ---";
936
+ const BACKEND_EXPORTS_END = "// --- @vibefast:backend:exports:end ---";
937
+ async function applyApiClientConfiguration(paths, apiClientConfig, options) {
938
+ const apiClientIndexPath = join(paths.nativeDir, "src/api-client/index.ts");
939
+ if (!(await exists(apiClientIndexPath))) {
940
+ return;
941
+ }
942
+ const suffix = options.dryRun ? " (dry run)" : "";
943
+ if (apiClientConfig.exports?.length) {
944
+ const inserted = await insertEnvSnippets(apiClientIndexPath, API_CLIENT_EXPORTS_START, API_CLIENT_EXPORTS_END, apiClientConfig.exports, options);
945
+ if (inserted) {
946
+ log.info(`Updated api-client exports${suffix}.`);
947
+ }
948
+ }
949
+ }
950
+ async function applyBackendConfiguration(paths, backendConfig, options) {
951
+ // Try to find backend index.ts in common locations
952
+ const possiblePaths = [
953
+ join(paths.cwd, "packages/backend/src/index.ts"),
954
+ join(paths.cwd, "backend/src/index.ts"),
955
+ ];
956
+ let backendIndexPath = null;
957
+ for (const p of possiblePaths) {
958
+ if (await exists(p)) {
959
+ backendIndexPath = p;
960
+ break;
961
+ }
962
+ }
963
+ if (!backendIndexPath) {
964
+ return;
965
+ }
966
+ const suffix = options.dryRun ? " (dry run)" : "";
967
+ if (backendConfig.exports?.length) {
968
+ const inserted = await insertEnvSnippets(backendIndexPath, BACKEND_EXPORTS_START, BACKEND_EXPORTS_END, backendConfig.exports, options);
969
+ if (inserted) {
970
+ log.info(`Updated backend exports${suffix}.`);
971
+ }
972
+ }
973
+ }
974
+ async function applyAppConfigPlugins(manifest, paths, options) {
975
+ const appConfigPath = join(paths.nativeDir, "app.config.ts");
976
+ if (!(await exists(appConfigPath)))
977
+ return;
978
+ // Handle plugins
979
+ if (manifest.plugins?.length) {
980
+ if (options.dryRun) {
981
+ log.info(`[DRY RUN] Would add plugins to app.config.ts: ${manifest.plugins
982
+ .map((p) => p.name)
983
+ .join(", ")}`);
984
+ }
985
+ else {
986
+ for (const plugin of manifest.plugins) {
987
+ const added = await addAppConfigPlugin(appConfigPath, plugin.name, plugin.config);
988
+ if (added) {
989
+ log.info(`Added plugin ${plugin.name} to app.config.ts`);
990
+ }
991
+ else {
992
+ log.info(`Plugin ${plugin.name} already exists in app.config.ts`);
993
+ }
994
+ }
995
+ }
996
+ }
997
+ // Handle iosConfig (for ios-widget: appleTeamId and entitlements)
998
+ if (manifest.iosConfig) {
999
+ if (options.dryRun) {
1000
+ log.info(`[DRY RUN] Would add ios config to app.config.ts`);
1001
+ }
1002
+ else {
1003
+ const iosProps = {};
1004
+ if (manifest.iosConfig.appleTeamId) {
1005
+ iosProps.appleTeamId = manifest.iosConfig.appleTeamId;
1006
+ }
1007
+ if (manifest.iosConfig.entitlements) {
1008
+ iosProps.entitlements = manifest.iosConfig.entitlements;
1009
+ }
1010
+ if (Object.keys(iosProps).length > 0) {
1011
+ const added = await addAppConfigIosProperties(appConfigPath, iosProps);
1012
+ if (added) {
1013
+ log.info(`Added ios config to app.config.ts`);
1014
+ }
1015
+ else {
1016
+ log.info(`ios config already exists in app.config.ts`);
1017
+ }
1018
+ }
1019
+ }
1020
+ }
1021
+ // Run eslint --fix to format the modified file
1022
+ if (!options.dryRun) {
1023
+ try {
1024
+ const result = spawnSync("pnpm", ["exec", "eslint", "--fix", appConfigPath], {
1025
+ cwd: paths.cwd,
1026
+ stdio: "pipe",
1027
+ encoding: "utf-8",
1028
+ });
1029
+ if (result.status === 0) {
1030
+ log.info("✓ Formatted app.config.ts");
1031
+ }
1032
+ }
1033
+ catch {
1034
+ // Silently ignore - formatting is best-effort
1035
+ }
1036
+ }
1037
+ }
1050
1038
  //# sourceMappingURL=add.js.map