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