vibefast-cli 1.1.5 → 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.
- package/CHANGELOG.md +32 -0
- package/README.md +63 -169
- package/dist/commands/add.d.ts +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +547 -589
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/checklist.d.ts +1 -1
- package/dist/commands/checklist.d.ts.map +1 -1
- package/dist/commands/checklist.js +40 -39
- package/dist/commands/checklist.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +22 -22
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/env.d.ts +1 -1
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +58 -53
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/health.d.ts +1 -1
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +101 -93
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +416 -296
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/remove.d.ts +1 -1
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +77 -64
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +15 -14
- package/dist/commands/status.js.map +1 -1
- package/dist/core/__tests__/detect.test.js +68 -34
- package/dist/core/__tests__/detect.test.js.map +1 -1
- package/dist/core/ast.d.ts +14 -0
- package/dist/core/ast.d.ts.map +1 -0
- package/dist/core/ast.js +239 -0
- package/dist/core/ast.js.map +1 -0
- package/dist/core/codemod.d.ts.map +1 -1
- package/dist/core/codemod.js +62 -44
- package/dist/core/codemod.js.map +1 -1
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +51 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/detect.d.ts +8 -2
- package/dist/core/detect.d.ts.map +1 -1
- package/dist/core/detect.js +52 -21
- package/dist/core/detect.js.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +9 -8
- package/dist/core/errors.js.map +1 -1
- package/dist/core/exec.d.ts +16 -0
- package/dist/core/exec.d.ts.map +1 -0
- package/dist/core/exec.js +48 -0
- package/dist/core/exec.js.map +1 -0
- package/dist/core/manualSteps.d.ts +7 -0
- package/dist/core/manualSteps.d.ts.map +1 -0
- package/dist/core/manualSteps.js +59 -0
- package/dist/core/manualSteps.js.map +1 -0
- package/dist/core/paths.d.ts +3 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +14 -10
- package/dist/core/paths.js.map +1 -1
- package/dist/core/spinner.d.ts +1 -1
- package/dist/core/spinner.d.ts.map +1 -1
- package/dist/core/spinner.js +38 -8
- package/dist/core/spinner.js.map +1 -1
- package/dist/core/vosk.d.ts.map +1 -1
- package/dist/core/vosk.js +50 -39
- package/dist/core/vosk.js.map +1 -1
- package/docs/manual-testing.md +91 -0
- package/package.json +6 -3
- package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
- package/recipes/audio-recorder/recipe.json +3 -3
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/index.ts +4 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
- package/recipes/audio-recorder-supabase/packages/backend/src/services/recordings.ts +369 -0
- package/recipes/audio-recorder-supabase/packages/backend/supabase/migrations/recordings.sql +70 -0
- package/recipes/audio-recorder-supabase/recipe.json +35 -0
- package/recipes/audio-recorder-supabase@latest.zip +0 -0
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +3 -3
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +2 -2
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +5 -5
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +3 -3
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +20 -4
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +7 -6
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +6 -4
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +1 -1
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +5 -4
- package/recipes/charts/recipe.json +4 -13
- package/recipes/charts@latest.zip +0 -0
- package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +86 -86
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +86 -53
- package/recipes/chatbot/recipe.json +26 -92
- package/recipes/chatbot-supabase/apps/native/src/api-client/supabase/chatbot.ts +515 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/app/index.tsx +257 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-input-bar.tsx +485 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +223 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/image-preview-list.tsx +116 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-list.tsx +170 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/models.ts +20 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +142 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-config.ts +458 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +429 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-conversation.ts +90 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +213 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/index.ts +86 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/models.ts +162 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/providers.ts +62 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/types.ts +40 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/file-uploader.ts +287 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/message-handler-service.ts +189 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/types/index.ts +70 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
- package/recipes/chatbot-supabase/packages/backend/src/services/conversations.ts +243 -0
- package/recipes/chatbot-supabase/packages/backend/src/services/messages.ts +327 -0
- package/recipes/chatbot-supabase/packages/backend/supabase/functions/chat-stream/index.ts +347 -0
- package/recipes/chatbot-supabase/packages/backend/supabase/migrations/chatbot.sql +104 -0
- package/recipes/chatbot-supabase/recipe.json +79 -0
- package/recipes/chatbot-supabase@latest.zip +0 -0
- package/recipes/chatbot.zip +0 -0
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-analysis/packages/backend/convex/imageAnalysis/index.ts +2 -2
- package/recipes/image-analysis/packages/backend/convex/imageAnalysis.ts +0 -1
- package/recipes/image-analysis/recipe.json +15 -55
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/analysis-options-screen.tsx +304 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/camera.tsx +221 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/image-capture-screen.tsx +333 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading-screen.tsx +214 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading.tsx +191 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/results.tsx +137 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/trait-details.tsx +172 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-analysis-data.ts +160 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-results-screen.ts +151 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-badge.tsx +77 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-card.tsx +75 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-unlocked-modal.tsx +162 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievements-section.tsx +44 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/advice-list.tsx +42 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/circular-progress.tsx +233 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/content-card.tsx +38 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/error-state.tsx +42 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/index.ts +9 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/loading-state.tsx +26 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/profile-image.tsx +60 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/results-header.tsx +62 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/score-display.tsx +54 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/share-options-modal.tsx +110 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/traits-grid.tsx +74 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/analysis-config.ts +80 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/master-analysis-config.ts +157 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/index.ts +1 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-analysis.ts +38 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-image-analysis.ts +208 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/analysis-service.ts +262 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/share-service.ts +176 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/trait-details-service.ts +289 -0
- package/recipes/image-analysis-supabase/packages/backend/src/services/image-analyses.ts +132 -0
- package/recipes/image-analysis-supabase/packages/backend/supabase/functions/analyze-image/index.ts +312 -0
- package/recipes/image-analysis-supabase/packages/backend/supabase/migrations/image_analysis.sql +42 -0
- package/recipes/image-analysis-supabase/recipe.json +57 -0
- package/recipes/image-analysis-supabase@latest.zip +0 -0
- package/recipes/image-analysis@latest.zip +0 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +16 -2
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +11 -5
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +11 -5
- package/recipes/image-generator/packages/backend/convex/imageGeneration/index.ts +2 -2
- package/recipes/image-generator/recipe.json +16 -39
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/_layout.tsx +26 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/index.tsx +251 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/gallery-image.tsx +25 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-model-selector.tsx +216 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator.ts +103 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/models/models.ts +66 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-gallery-service.ts +96 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-save-service.ts +120 -0
- package/recipes/image-generator-supabase/packages/backend/supabase/functions/generate-image/index.ts +291 -0
- package/recipes/image-generator-supabase/packages/backend/supabase/migrations/image_generator.sql +71 -0
- package/recipes/image-generator-supabase/recipe.json +59 -0
- package/recipes/image-generator-supabase@latest.zip +0 -0
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/ios-widget/recipe.json +15 -24
- package/recipes/ios-widget@latest.zip +0 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/analytics/index.ts +9 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding-with-analytics.tsx +141 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding.tsx +173 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/config/onboarding-flow-config.ts +189 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/app/index.tsx +42 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/data.ts +32 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/app/index.tsx +43 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/interactive-onboarding.tsx +222 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/ai-tone-step.tsx +133 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/currency-step.tsx +165 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-ai-step.tsx +199 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-chatbot-step.tsx +154 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-manual-step.tsx +156 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-scan-step.tsx +158 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/main-reason-step.tsx +139 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/notification-step.tsx +129 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/overspend-step.tsx +138 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/personalizing-step.tsx +190 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/rating-step.tsx +98 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/reminder-step.tsx +181 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/safety-step.tsx +110 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/struggle-step.tsx +139 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/welcome-step.tsx +217 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/ui/onboarding-header.tsx +58 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/constants.ts +179 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/hooks/use-onboarding-analytics.ts +323 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/services/onboarding-analytics.ts +432 -0
- package/recipes/onboarding/recipe.json +15 -0
- package/recipes/onboarding@latest.zip +0 -0
- package/recipes/payments/recipe.json +28 -61
- package/recipes/payments-supabase/apps/native/src/features/payments/README.md +200 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/index.ts +8 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
- package/recipes/payments-supabase/packages/backend/src/services/payments.ts +201 -0
- package/recipes/payments-supabase/packages/backend/supabase/migrations/payments.sql +35 -0
- package/recipes/payments-supabase/recipe.json +51 -0
- package/recipes/payments-supabase@latest.zip +0 -0
- package/recipes/payments@latest.zip +0 -0
- package/recipes/quiz/apps/native/src/features/quiz/index.tsx +1 -2
- package/recipes/quiz/recipe.json +6 -9
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +1 -2
- package/recipes/tracker-app/recipe.json +7 -10
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/voice-bot/recipe.json +8 -68
- package/recipes/voice-bot.zip +0 -0
- package/recipes/voice-bot@latest.zip +0 -0
- package/recipes/wake-word/recipe.json +10 -9
- package/recipes/wake-word.zip +0 -0
- package/recipes/wake-word@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/(root)/(protected)/charts/index.tsx +0 -3
- package/recipes/chatbot/packages/backend/convex/lib/rateLimit.ts +0 -100
- package/recipes/chatbot/packages/backend/convex/lib/telemetry.ts +0 -29
- package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +0 -0
- package/recipes/image-analysis/apps/native/assets/features/image-analyzer/front.jpg +0 -0
- package/recipes/image-analysis/apps/native/assets/features/image-analyzer/side.jpg +0 -0
- package/recipes/image-analysis/apps/native/assets/features/image-analyzer/threeQuarter.jpg +0 -0
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/_layout.tsx +0 -5
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/analysis-options.tsx +0 -50
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/camera.tsx +0 -2
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/index.tsx +0 -50
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/loading.tsx +0 -50
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/results.tsx +0 -2
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/trait-details.tsx +0 -3
- package/recipes/image-analysis/packages/backend/convex/imageAnalysisFunctions.ts +0 -325
- package/recipes/image-analysis/packages/backend/convex/lib/ai/imageAnalysisAdapter.ts +0 -200
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +0 -74
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +0 -25
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +0 -23
- package/recipes/quiz/apps/native/src/app/(root)/(protected)/quiz/index.tsx +0 -47
- package/recipes/tracker-app/apps/native/src/app/(root)/(protected)/tracker-app/index.tsx +0 -1
- package/recipes/voice-bot/apps/native/src/app/(root)/(protected)/voice-bot/index.tsx +0 -27
- package/recipes/voice-bot/packages/backend/convex/router.ts +0 -81
- /package/recipes/{chatbot/apps/native/src/app/(root)/(protected) → chatbot-supabase/apps/native/src/app}/chatbot/index.tsx +0 -0
- /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/gallery.tsx +0 -0
- /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/index.tsx +0 -0
package/dist/commands/add.js
CHANGED
|
@@ -1,122 +1,94 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import { log } from
|
|
3
|
-
import { getPaths } from
|
|
4
|
-
import { validateSignature, validateTarget } from
|
|
5
|
-
import { getToken, getDeviceInfo } from
|
|
6
|
-
import { fetchRecipe, downloadZip } from
|
|
7
|
-
import { addEntry, getEntry } from
|
|
8
|
-
import { copyTree, readFileContent, exists,
|
|
9
|
-
import {
|
|
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
|
|
11
|
-
import { join, resolve,
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
81
|
-
.description(
|
|
82
|
-
.argument(
|
|
83
|
-
.option(
|
|
84
|
-
.option(
|
|
85
|
-
.option(
|
|
86
|
-
.option(
|
|
87
|
-
.option(
|
|
88
|
-
.on(
|
|
89
|
-
log.plain(
|
|
90
|
-
log.info(
|
|
91
|
-
log.plain(
|
|
92
|
-
log.plain(
|
|
93
|
-
log.plain(
|
|
94
|
-
log.plain(
|
|
95
|
-
log.plain(
|
|
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(
|
|
101
|
-
process.stdout.setMaxListeners(
|
|
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(
|
|
106
|
-
const { promptSelectAsync, promptMultiSelectAsync, intro, outro } = await import(
|
|
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
|
-
|
|
79
|
+
// Temporarily hide integrations from the interactive menu
|
|
80
|
+
const categories = getCategories().filter((cat) => cat !== "integration");
|
|
109
81
|
// Show intro
|
|
110
|
-
intro(
|
|
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(
|
|
89
|
+
const selectedCategory = await promptSelectAsync("What would you like to add?", categoryChoices);
|
|
118
90
|
if (!selectedCategory) {
|
|
119
|
-
outro(
|
|
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(
|
|
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(
|
|
108
|
+
log.plain("");
|
|
109
|
+
log.plain("━".repeat(60));
|
|
138
110
|
await installFeature(recipeName, options, paths);
|
|
139
111
|
}
|
|
140
|
-
outro(
|
|
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(
|
|
145
|
-
const { promptMultiSelectAsync, intro, outro } = await import(
|
|
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(
|
|
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(
|
|
135
|
+
log.plain("");
|
|
136
|
+
log.plain("━".repeat(60));
|
|
165
137
|
await installFeature(recipeName, options, paths);
|
|
166
138
|
}
|
|
167
|
-
outro(
|
|
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(
|
|
193
|
-
log.info(
|
|
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(
|
|
198
|
+
const shouldReinstall = await promptYesNo("Reinstall and overwrite existing files? (y/N) ", false);
|
|
506
199
|
if (!shouldReinstall) {
|
|
507
|
-
log.info(
|
|
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(
|
|
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(
|
|
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 ${
|
|
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 ||
|
|
541
|
-
const message = response.message ||
|
|
542
|
-
const localZip = await getBundledRecipeZipPath(
|
|
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(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
log.
|
|
554
|
-
log.plain(
|
|
555
|
-
log.
|
|
556
|
-
log.plain(
|
|
557
|
-
log.plain(
|
|
558
|
-
log.
|
|
559
|
-
log.plain(
|
|
560
|
-
log.
|
|
561
|
-
log.plain(
|
|
562
|
-
log.plain(
|
|
563
|
-
log.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
log.
|
|
571
|
-
log.plain(
|
|
572
|
-
log.
|
|
573
|
-
log.plain(
|
|
574
|
-
log.
|
|
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("");
|
|
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(
|
|
581
|
-
log.plain(
|
|
582
|
-
log.plain(
|
|
583
|
-
log.info(
|
|
584
|
-
log.plain(
|
|
585
|
-
log.info(
|
|
586
|
-
log.plain(
|
|
587
|
-
log.plain(
|
|
588
|
-
log.plain(
|
|
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(
|
|
596
|
-
log.plain(
|
|
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(
|
|
601
|
-
log.plain(
|
|
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(
|
|
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(
|
|
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(
|
|
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(),
|
|
333
|
+
const dir = join(tmpdir(), "vibefast", randomUUID());
|
|
637
334
|
await extractZipSafe(zip, dir);
|
|
638
335
|
return { zipPath: zip, extractDir: dir };
|
|
639
336
|
}, {
|
|
640
|
-
successText:
|
|
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(),
|
|
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(
|
|
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 ===
|
|
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,
|
|
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) &&
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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 ??
|
|
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 =
|
|
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(
|
|
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(
|
|
473
|
+
const { writeFileContent, readFileContent } = await import("../core/fsx.js");
|
|
740
474
|
for (const file of copiedFiles) {
|
|
741
|
-
if (file.endsWith(
|
|
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(
|
|
754
|
-
const navFile = target ===
|
|
755
|
-
const insertFn = target ===
|
|
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, {
|
|
495
|
+
navInserted = await insertFn(navFile, manifest.nav, {
|
|
496
|
+
dryRun: options.dryRun,
|
|
497
|
+
});
|
|
759
498
|
if (navInserted) {
|
|
760
|
-
log.success(
|
|
499
|
+
log.success("Navigation link added");
|
|
761
500
|
}
|
|
762
501
|
else {
|
|
763
|
-
log.info(
|
|
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(
|
|
769
|
-
const fileHashes = await hashFiles(copiedFiles, {
|
|
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
|
-
|
|
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,145 @@ export async function installFeature(feature, options, paths) {
|
|
|
810
553
|
}
|
|
811
554
|
}
|
|
812
555
|
// Auto-install dependencies
|
|
813
|
-
if (installedManifest &&
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
log.plain(
|
|
817
|
-
|
|
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
|
-
|
|
824
|
-
|
|
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.
|
|
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,
|
|
831
|
-
await withSpinner(
|
|
832
|
-
const
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
609
|
+
successText: "✓ Packages installed",
|
|
852
610
|
});
|
|
853
611
|
}
|
|
854
612
|
else {
|
|
855
|
-
log.info(
|
|
856
|
-
log.plain(` pnpx expo install ${packages.join(
|
|
857
|
-
log.plain(
|
|
858
|
-
log.info(
|
|
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 ===
|
|
620
|
+
else if (installedManifest.target === "web" &&
|
|
621
|
+
installedManifest.dependencies.npm) {
|
|
863
622
|
const packages = installedManifest.dependencies.npm;
|
|
864
|
-
|
|
865
|
-
|
|
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.
|
|
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(
|
|
872
|
-
const pkgManager = detectPackageManager(paths.cwd) ||
|
|
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
|
-
|
|
875
|
-
const
|
|
876
|
-
?
|
|
877
|
-
:
|
|
878
|
-
|
|
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:
|
|
648
|
+
stdio: "inherit",
|
|
649
|
+
shell: false,
|
|
881
650
|
});
|
|
651
|
+
if (result.error) {
|
|
652
|
+
throw result.error;
|
|
653
|
+
}
|
|
882
654
|
}, {
|
|
883
|
-
successText:
|
|
655
|
+
successText: "✓ Packages installed",
|
|
884
656
|
});
|
|
885
657
|
}
|
|
886
658
|
else {
|
|
887
|
-
log.info(
|
|
888
|
-
log.plain(` pnpm add ${packages.join(
|
|
889
|
-
log.plain(
|
|
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 ===
|
|
899
|
-
const { setupVoskModel } = await import(
|
|
900
|
-
log.plain(
|
|
901
|
-
await withSpinner(
|
|
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:
|
|
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
|
+
}
|
|
910
686
|
// Apply app.config plugin inserts for certain features
|
|
911
687
|
if (installedManifest?.name) {
|
|
912
|
-
await applyAppConfigPlugins(installedManifest
|
|
688
|
+
await applyAppConfigPlugins(installedManifest, paths, options);
|
|
913
689
|
}
|
|
914
690
|
// Show manual steps with smart detection
|
|
915
691
|
if (installedManifest?.manualSteps && !options.dryRun) {
|
|
916
|
-
const { readFileContent, exists } = await import(
|
|
917
|
-
const { join } = await import(
|
|
918
|
-
const normalize = (text) => text.replace(/\\n/g,
|
|
692
|
+
const { readFileContent, exists } = await import("../core/fsx.js");
|
|
693
|
+
const { join } = await import("path");
|
|
694
|
+
const normalize = (text) => text.replace(/\\n/g, "\n");
|
|
919
695
|
const featureName = installedManifest?.name;
|
|
920
696
|
// Check which steps might already be done
|
|
921
697
|
const pendingSteps = [];
|
|
@@ -924,17 +700,20 @@ export async function installFeature(feature, options, paths) {
|
|
|
924
700
|
let alreadyDone = false;
|
|
925
701
|
const filePath = step.file ? join(paths.cwd, step.file) : null;
|
|
926
702
|
// Wake-word specific checks (more lenient matching)
|
|
927
|
-
if (featureName ===
|
|
703
|
+
if (featureName === "wake-word" &&
|
|
704
|
+
step.file &&
|
|
705
|
+
filePath &&
|
|
706
|
+
(await exists(filePath))) {
|
|
928
707
|
const content = await readFileContent(filePath);
|
|
929
708
|
const lowerTitle = step.title.toLowerCase();
|
|
930
|
-
if (lowerTitle.includes(
|
|
931
|
-
if (content.includes(
|
|
709
|
+
if (lowerTitle.includes("vosk plugin")) {
|
|
710
|
+
if (content.includes("react-native-vosk")) {
|
|
932
711
|
alreadyDone = true;
|
|
933
712
|
completedSteps.push(step.title);
|
|
934
713
|
}
|
|
935
714
|
}
|
|
936
|
-
else if (step.file ===
|
|
937
|
-
if (content.includes(
|
|
715
|
+
else if (step.file === ".gitignore") {
|
|
716
|
+
if (content.includes("assets/vosk-model/model-en-us")) {
|
|
938
717
|
alreadyDone = true;
|
|
939
718
|
completedSteps.push(step.title);
|
|
940
719
|
}
|
|
@@ -945,8 +724,8 @@ export async function installFeature(feature, options, paths) {
|
|
|
945
724
|
if (filePath && (await exists(filePath))) {
|
|
946
725
|
const content = await readFileContent(filePath);
|
|
947
726
|
// Simple check: does the file contain the content to add?
|
|
948
|
-
const contentToCheck = step.content.replace(/\s+/g,
|
|
949
|
-
const fileContentNormalized = content.replace(/\s+/g,
|
|
727
|
+
const contentToCheck = step.content.replace(/\s+/g, " ").trim();
|
|
728
|
+
const fileContentNormalized = content.replace(/\s+/g, " ").trim();
|
|
950
729
|
if (fileContentNormalized.includes(contentToCheck)) {
|
|
951
730
|
alreadyDone = true;
|
|
952
731
|
completedSteps.push(step.title);
|
|
@@ -964,30 +743,19 @@ export async function installFeature(feature, options, paths) {
|
|
|
964
743
|
log.info(`Checklist saved to ${relPath}`);
|
|
965
744
|
}
|
|
966
745
|
log.info(`Run 'vf checklist ${installedManifest.name}' to see these steps again`);
|
|
967
|
-
log.plain(
|
|
746
|
+
log.plain("");
|
|
968
747
|
}
|
|
969
748
|
else if (completedSteps.length > 0) {
|
|
970
|
-
log.success(`✓ Manual steps already satisfied: ${completedSteps.join(
|
|
971
|
-
log.plain(
|
|
749
|
+
log.success(`✓ Manual steps already satisfied: ${completedSteps.join(", ")}`);
|
|
750
|
+
log.plain("");
|
|
972
751
|
}
|
|
973
752
|
}
|
|
974
753
|
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
754
|
installedEnvAttention = await reportEnvStatus(installedEnvGroups, options);
|
|
987
755
|
}
|
|
988
756
|
// Post-install message
|
|
989
757
|
if (installedManifest?.postInstall?.message && !options.dryRun) {
|
|
990
|
-
log.plain(
|
|
758
|
+
log.plain("");
|
|
991
759
|
log.info(installedManifest.postInstall.message);
|
|
992
760
|
}
|
|
993
761
|
// Final summary of what needs user attention
|
|
@@ -998,16 +766,39 @@ export async function installFeature(feature, options, paths) {
|
|
|
998
766
|
}
|
|
999
767
|
// Check manual steps
|
|
1000
768
|
if (installedManifest?.manualSteps) {
|
|
1001
|
-
const { readFileContent, exists } = await import(
|
|
1002
|
-
const { join } = await import(
|
|
769
|
+
const { readFileContent, exists } = await import("../core/fsx.js");
|
|
770
|
+
const { join } = await import("path");
|
|
771
|
+
const featureName = installedManifest.name;
|
|
1003
772
|
for (const step of installedManifest.manualSteps) {
|
|
1004
773
|
let alreadyDone = false;
|
|
1005
|
-
if
|
|
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") {
|
|
1006
788
|
const filePath = join(paths.cwd, step.file);
|
|
1007
789
|
if (await exists(filePath)) {
|
|
1008
790
|
const content = await readFileContent(filePath);
|
|
1009
|
-
|
|
1010
|
-
|
|
791
|
+
if (content.includes("assets/vosk-model/model-en-us")) {
|
|
792
|
+
alreadyDone = true;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (!alreadyDone && step.file && step.content) {
|
|
797
|
+
const filePath = join(paths.cwd, step.file);
|
|
798
|
+
if (await exists(filePath)) {
|
|
799
|
+
const content = await readFileContent(filePath);
|
|
800
|
+
const contentToCheck = step.content.replace(/\s+/g, " ").trim();
|
|
801
|
+
const fileContentNormalized = content.replace(/\s+/g, " ").trim();
|
|
1011
802
|
if (fileContentNormalized.includes(contentToCheck)) {
|
|
1012
803
|
alreadyDone = true;
|
|
1013
804
|
}
|
|
@@ -1020,31 +811,198 @@ export async function installFeature(feature, options, paths) {
|
|
|
1020
811
|
}
|
|
1021
812
|
// Show summary
|
|
1022
813
|
if (needsAttention.length > 0) {
|
|
1023
|
-
log.plain(
|
|
1024
|
-
log.plain(
|
|
814
|
+
log.plain("");
|
|
815
|
+
log.plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
1025
816
|
log.warn(`⚠ ACTION REQUIRED: ${needsAttention.length} item(s) need your attention`);
|
|
1026
|
-
log.plain(
|
|
1027
|
-
log.plain(
|
|
817
|
+
log.plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
818
|
+
log.plain("");
|
|
1028
819
|
needsAttention.forEach((item, index) => {
|
|
1029
820
|
log.plain(` ${index + 1}. ${item}`);
|
|
1030
821
|
});
|
|
1031
|
-
log.plain(
|
|
822
|
+
log.plain("");
|
|
1032
823
|
log.info(`💡 Run 'vf checklist ${installedManifest?.name ?? feature}' for detailed instructions`);
|
|
1033
|
-
log.plain(
|
|
824
|
+
log.plain("");
|
|
1034
825
|
}
|
|
1035
826
|
else {
|
|
1036
|
-
log.plain(
|
|
1037
|
-
log.success(
|
|
1038
|
-
log.plain(
|
|
827
|
+
log.plain("");
|
|
828
|
+
log.success("🎉 All set! No manual configuration needed.");
|
|
829
|
+
log.plain("");
|
|
1039
830
|
}
|
|
1040
831
|
}
|
|
1041
832
|
if (options.dryRun) {
|
|
1042
|
-
log.plain(
|
|
1043
|
-
log.warn(
|
|
833
|
+
log.plain("");
|
|
834
|
+
log.warn("This was a dry run. Run without --dry-run to apply changes.");
|
|
1044
835
|
}
|
|
1045
836
|
}
|
|
1046
837
|
catch (error) {
|
|
1047
838
|
throw error;
|
|
1048
839
|
}
|
|
1049
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
|
+
}
|
|
1050
1008
|
//# sourceMappingURL=add.js.map
|