vibefast-cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FINAL-STATUS.md +144 -0
- package/HOW-IT-WORKS.md +559 -0
- package/PLAN.md +453 -0
- package/README.md +129 -0
- package/RECIPES-READY.md +172 -0
- package/STATUS.md +199 -0
- package/SUCCESS.md +259 -0
- package/TESTING-CHECKLIST.md +450 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/64907821e2634080acce34618d2f3d4c/blobs/11f2769953c717e188062bc644da97c1fd1e4d6d0813a226ce7567dba759afab0000019a736fb8d4 +1 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite +0 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-shm +0 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-wal +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/vibefast-recipes/blobs/620e8cf7c35d9806da25dee237e1d7e8b2432bd98f755b60e2c7f08a48d2c7b90000019a73736484 +0 -0
- package/cloudflare-worker/MIGRATION.md +160 -0
- package/cloudflare-worker/QUICKSTART.md +200 -0
- package/cloudflare-worker/README.md +242 -0
- package/cloudflare-worker/generate-token.js +32 -0
- package/cloudflare-worker/mini-native@latest.zip +0 -0
- package/cloudflare-worker/setup.sh +143 -0
- package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +15 -0
- package/cloudflare-worker/test-recipe/recipe.json +16 -0
- package/cloudflare-worker/worker.js +308 -0
- package/cloudflare-worker/wrangler.toml +13 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +149 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/devices.d.ts +3 -0
- package/dist/commands/devices.d.ts.map +1 -0
- package/dist/commands/devices.js +35 -0
- package/dist/commands/devices.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +67 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +40 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +23 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +16 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +67 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/core/__tests__/journal.test.d.ts +2 -0
- package/dist/core/__tests__/journal.test.d.ts.map +1 -0
- package/dist/core/__tests__/journal.test.js +101 -0
- package/dist/core/__tests__/journal.test.js.map +1 -0
- package/dist/core/__tests__/validate.test.d.ts +2 -0
- package/dist/core/__tests__/validate.test.d.ts.map +1 -0
- package/dist/core/__tests__/validate.test.js +53 -0
- package/dist/core/__tests__/validate.test.js.map +1 -0
- package/dist/core/archive.d.ts +2 -0
- package/dist/core/archive.d.ts.map +1 -0
- package/dist/core/archive.js +59 -0
- package/dist/core/archive.js.map +1 -0
- package/dist/core/auth.d.ts +15 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +76 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/codemod.d.ts +20 -0
- package/dist/core/codemod.d.ts.map +1 -0
- package/dist/core/codemod.js +150 -0
- package/dist/core/codemod.js.map +1 -0
- package/dist/core/fsx.d.ts +12 -0
- package/dist/core/fsx.d.ts.map +1 -0
- package/dist/core/fsx.js +70 -0
- package/dist/core/fsx.js.map +1 -0
- package/dist/core/http.d.ts +30 -0
- package/dist/core/http.d.ts.map +1 -0
- package/dist/core/http.js +95 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/journal.d.ts +18 -0
- package/dist/core/journal.d.ts.map +1 -0
- package/dist/core/journal.js +34 -0
- package/dist/core/journal.js.map +1 -0
- package/dist/core/log.d.ts +8 -0
- package/dist/core/log.d.ts.map +1 -0
- package/dist/core/log.js +9 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/pathGuard.d.ts +3 -0
- package/dist/core/pathGuard.d.ts.map +1 -0
- package/dist/core/pathGuard.js +18 -0
- package/dist/core/pathGuard.js.map +1 -0
- package/dist/core/paths.d.ts +11 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +22 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/validate.d.ts +8 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +27 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/docs/decisions.md +55 -0
- package/package.json +39 -0
- package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/index.ts +4 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
- package/recipes/audio-recorder/recipe.json +22 -0
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/README.md +185 -0
- package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +223 -0
- package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +40 -0
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +143 -0
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +196 -0
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +65 -0
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +143 -0
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +246 -0
- package/recipes/charts/apps/native/src/features/charts/components/index.ts +10 -0
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +308 -0
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +180 -0
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +188 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +265 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +322 -0
- package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +183 -0
- package/recipes/charts/apps/native/src/features/charts/types/index.ts +66 -0
- package/recipes/charts/recipe.json +22 -0
- 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/app/index.tsx +302 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +469 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +246 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +115 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +173 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +20 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +143 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +664 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +359 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +79 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +207 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +86 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +162 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +62 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +40 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +238 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +180 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +60 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
- package/recipes/chatbot/recipe.json +22 -0
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
- package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/_layout.tsx +25 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +237 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/gallery-image.tsx +26 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +210 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +93 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/models/models.ts +66 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/services/image-gallery-service.ts +98 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/services/image-save-service.ts +121 -0
- package/recipes/image-generator/recipe.json +22 -0
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
- package/recipes/quiz/apps/native/src/features/quiz/components/question.tsx +67 -0
- package/recipes/quiz/apps/native/src/features/quiz/config.ts +11 -0
- package/recipes/quiz/apps/native/src/features/quiz/index.tsx +133 -0
- package/recipes/quiz/recipe.json +22 -0
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +108 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/animated-number.tsx +102 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/calorie-card.tsx +66 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/circular-progress.tsx +97 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/floating-add-button.tsx +27 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/macro-card.tsx +80 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/promo-banner.tsx +98 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/recently-logged.tsx +64 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/week-calendar.tsx +68 -0
- package/recipes/tracker-app/recipe.json +22 -0
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/upload-all.sh +32 -0
- package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +27 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/README.md +185 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/conversation-status.tsx +76 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/index.ts +4 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/message-input.tsx +98 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-bot-screen.tsx +173 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-controls.tsx +73 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/index.ts +3 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/services/index.ts +1 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/services/use-voice-bot.ts +161 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/types.ts +29 -0
- package/recipes/voice-bot/recipe.json +22 -0
- package/recipes/voice-bot@latest.zip +0 -0
- package/scripts/create-recipes.mjs +189 -0
- package/src/commands/add.ts +183 -0
- package/src/commands/devices.ts +38 -0
- package/src/commands/doctor.ts +67 -0
- package/src/commands/list.ts +45 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/logout.ts +15 -0
- package/src/commands/remove.ts +78 -0
- package/src/core/__tests__/journal.test.ts +119 -0
- package/src/core/__tests__/validate.test.ts +64 -0
- package/src/core/archive.ts +69 -0
- package/src/core/auth.ts +103 -0
- package/src/core/codemod.ts +211 -0
- package/src/core/fsx.ts +80 -0
- package/src/core/http.ts +136 -0
- package/src/core/journal.ts +64 -0
- package/src/core/log.ts +9 -0
- package/src/core/pathGuard.ts +22 -0
- package/src/core/paths.ts +33 -0
- package/src/core/validate.ts +44 -0
- package/src/index.ts +27 -0
- package/test-critical-cases.mjs +258 -0
- package/tsconfig.json +21 -0
- package/vitest.config.mts +12 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { readFileContent, writeFileContent } from './fsx.js';
|
|
2
|
+
|
|
3
|
+
const NATIVE_START = '// --- @vibefast:features:start ---';
|
|
4
|
+
const NATIVE_END = '// --- @vibefast:features:end ---';
|
|
5
|
+
const WEB_START = '{/* @vibefast:nav:start */}';
|
|
6
|
+
const WEB_END = '{/* @vibefast:nav:end */}';
|
|
7
|
+
|
|
8
|
+
function escapeTsLiteral(value: string): string {
|
|
9
|
+
return value
|
|
10
|
+
.replace(/\\/g, '\\\\')
|
|
11
|
+
.replace(/'/g, "\\'")
|
|
12
|
+
.replace(/\r/g, '\\r')
|
|
13
|
+
.replace(/\n/g, '\\n')
|
|
14
|
+
.replace(/\u2028/g, '\\u2028')
|
|
15
|
+
.replace(/\u2029/g, '\\u2029');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function escapeHtmlAttribute(value: string): string {
|
|
19
|
+
return value
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/"/g, '"')
|
|
22
|
+
.replace(/'/g, ''')
|
|
23
|
+
.replace(/</g, '<')
|
|
24
|
+
.replace(/>/g, '>');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function escapeForRegex(value: string): string {
|
|
28
|
+
return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function safeNavId(value: string): string {
|
|
32
|
+
const sanitized = value.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
33
|
+
return sanitized || 'feature';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NavLink {
|
|
37
|
+
href: string;
|
|
38
|
+
label: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function insertNavLinkNative(
|
|
42
|
+
filePath: string,
|
|
43
|
+
link: NavLink & { icon?: string; color?: string },
|
|
44
|
+
options?: { dryRun?: boolean }
|
|
45
|
+
): Promise<boolean> {
|
|
46
|
+
const content = await readFileContent(filePath);
|
|
47
|
+
|
|
48
|
+
if (!content.includes(NATIVE_START) || !content.includes(NATIVE_END)) {
|
|
49
|
+
throw new Error(`Missing navigation markers in ${filePath}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if link already exists by route
|
|
53
|
+
const routeCheck = link.href.replace('/(root)/(protected)/', '/');
|
|
54
|
+
const safeRoute = escapeTsLiteral(routeCheck);
|
|
55
|
+
if (content.includes(`route: '${safeRoute}'`)) {
|
|
56
|
+
return false; // Already exists, idempotent
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create feature object for the vibefast-features.ts file
|
|
60
|
+
const featureId = safeNavId(link.href.split('/').pop() || 'feature');
|
|
61
|
+
const navItem = ` {
|
|
62
|
+
id: '${featureId}',
|
|
63
|
+
title: '${escapeTsLiteral(link.label)}',
|
|
64
|
+
icon: '${escapeTsLiteral(link.icon || '📦')}',
|
|
65
|
+
color: '${escapeTsLiteral(link.color || '#6366F1')}',
|
|
66
|
+
description: '${escapeTsLiteral(`${link.label} feature`)}',
|
|
67
|
+
route: '${safeRoute}',
|
|
68
|
+
testID: '${featureId}-button',
|
|
69
|
+
}`;
|
|
70
|
+
|
|
71
|
+
const startIdx = content.indexOf(NATIVE_START) + NATIVE_START.length;
|
|
72
|
+
const endIdx = content.indexOf(NATIVE_END);
|
|
73
|
+
|
|
74
|
+
const before = content.slice(0, startIdx);
|
|
75
|
+
const after = content.slice(endIdx);
|
|
76
|
+
const between = content.slice(startIdx, endIdx);
|
|
77
|
+
|
|
78
|
+
// Check if there are existing features (look for opening brace)
|
|
79
|
+
const hasExistingFeatures = between.includes('{');
|
|
80
|
+
|
|
81
|
+
let newContent;
|
|
82
|
+
if (hasExistingFeatures) {
|
|
83
|
+
// Add comma after new item since there are more items
|
|
84
|
+
newContent = `${before}\n${navItem},${between}${after}`;
|
|
85
|
+
} else {
|
|
86
|
+
// First item, no comma needed
|
|
87
|
+
newContent = `${before}\n${navItem}\n ${after}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!options?.dryRun) {
|
|
91
|
+
await writeFileContent(filePath, newContent, { force: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function removeNavLinkNative(
|
|
98
|
+
filePath: string,
|
|
99
|
+
href: string,
|
|
100
|
+
options?: { dryRun?: boolean }
|
|
101
|
+
): Promise<boolean> {
|
|
102
|
+
const content = await readFileContent(filePath);
|
|
103
|
+
|
|
104
|
+
const routeCheck = href.replace('/(root)/(protected)/', '/');
|
|
105
|
+
const safeRoute = escapeTsLiteral(routeCheck);
|
|
106
|
+
if (!content.includes(`route: '${safeRoute}'`)) {
|
|
107
|
+
return false; // Not found
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Use regex to remove the entire feature object including trailing comma
|
|
111
|
+
// Match: { ... route: '/charts' ... }, or { ... route: '/charts' ... }
|
|
112
|
+
const featureObjectRegex = new RegExp(
|
|
113
|
+
`\\s*\\{[^}]*route:\\s*'${escapeForRegex(safeRoute)}'[^}]*\\},?\\s*`,
|
|
114
|
+
'gs'
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const newContent = content.replace(featureObjectRegex, '');
|
|
118
|
+
|
|
119
|
+
if (!options?.dryRun) {
|
|
120
|
+
await writeFileContent(filePath, newContent, { force: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function insertNavLinkWeb(
|
|
127
|
+
filePath: string,
|
|
128
|
+
link: NavLink,
|
|
129
|
+
options?: { dryRun?: boolean }
|
|
130
|
+
): Promise<boolean> {
|
|
131
|
+
const content = await readFileContent(filePath);
|
|
132
|
+
|
|
133
|
+
if (!content.includes(WEB_START) || !content.includes(WEB_END)) {
|
|
134
|
+
throw new Error(`Missing navigation markers in ${filePath}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if link already exists
|
|
138
|
+
const safeHref = escapeHtmlAttribute(link.href);
|
|
139
|
+
if (content.includes(`href="${safeHref}"`)) {
|
|
140
|
+
return false; // Already exists
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const navCard = ` <Link href="${safeHref}" className="card">
|
|
144
|
+
<h3>{${JSON.stringify(link.label)}}</h3>
|
|
145
|
+
</Link>`;
|
|
146
|
+
|
|
147
|
+
const startIdx = content.indexOf(WEB_START) + WEB_START.length;
|
|
148
|
+
const endIdx = content.indexOf(WEB_END);
|
|
149
|
+
|
|
150
|
+
const before = content.slice(0, startIdx);
|
|
151
|
+
const after = content.slice(endIdx);
|
|
152
|
+
const existing = content.slice(startIdx, endIdx).trim();
|
|
153
|
+
|
|
154
|
+
const newContent = existing
|
|
155
|
+
? `${before}\n${existing}\n${navCard}\n${after}`
|
|
156
|
+
: `${before}\n${navCard}\n${after}`;
|
|
157
|
+
|
|
158
|
+
if (!options?.dryRun) {
|
|
159
|
+
await writeFileContent(filePath, newContent, { force: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function removeNavLinkWeb(
|
|
166
|
+
filePath: string,
|
|
167
|
+
href: string,
|
|
168
|
+
options?: { dryRun?: boolean }
|
|
169
|
+
): Promise<boolean> {
|
|
170
|
+
const content = await readFileContent(filePath);
|
|
171
|
+
const safeHref = escapeHtmlAttribute(href);
|
|
172
|
+
|
|
173
|
+
if (!content.includes(`href="${safeHref}"`)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const lines = content.split('\n');
|
|
178
|
+
const filtered: string[] = [];
|
|
179
|
+
let skipCount = 0;
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
const line = lines[i];
|
|
183
|
+
|
|
184
|
+
if (line.includes(`href="${safeHref}"`)) {
|
|
185
|
+
let start = i;
|
|
186
|
+
while (start > 0 && !lines[start].trim().startsWith('<Link')) {
|
|
187
|
+
start--;
|
|
188
|
+
}
|
|
189
|
+
let end = i;
|
|
190
|
+
while (end < lines.length && !lines[end].trim().startsWith('</Link>')) {
|
|
191
|
+
end++;
|
|
192
|
+
}
|
|
193
|
+
skipCount = end - start + 1;
|
|
194
|
+
i = end;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (skipCount > 0) {
|
|
199
|
+
skipCount--;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
filtered.push(line);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!options?.dryRun) {
|
|
207
|
+
await writeFileContent(filePath, filtered.join('\n'), { force: true });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return true;
|
|
211
|
+
}
|
package/src/core/fsx.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile, rm, access, readdir, stat, copyFile } from 'fs/promises';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { constants } from 'fs';
|
|
4
|
+
|
|
5
|
+
export async function ensureDir(path: string): Promise<void> {
|
|
6
|
+
try {
|
|
7
|
+
await mkdir(path, { recursive: true });
|
|
8
|
+
} catch (err: any) {
|
|
9
|
+
if (err.code !== 'EEXIST') throw err;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function exists(path: string): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
await access(path, constants.F_OK);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function readFileContent(path: string): Promise<string> {
|
|
23
|
+
return readFile(path, 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function writeFileContent(
|
|
27
|
+
path: string,
|
|
28
|
+
content: string,
|
|
29
|
+
options?: { force?: boolean }
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const fileExists = await exists(path);
|
|
32
|
+
if (fileExists && !options?.force) {
|
|
33
|
+
throw new Error(`File already exists: ${path}. Use --force to overwrite.`);
|
|
34
|
+
}
|
|
35
|
+
await ensureDir(dirname(path));
|
|
36
|
+
await writeFile(path, content, 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function deleteFile(path: string): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
await rm(path, { recursive: true, force: true });
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
if (err.code !== 'ENOENT') throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function copyTree(
|
|
48
|
+
src: string,
|
|
49
|
+
dest: string,
|
|
50
|
+
options?: { dryRun?: boolean; force?: boolean }
|
|
51
|
+
): Promise<string[]> {
|
|
52
|
+
const copied: string[] = [];
|
|
53
|
+
|
|
54
|
+
async function copyRecursive(srcPath: string, destPath: string) {
|
|
55
|
+
const stats = await stat(srcPath);
|
|
56
|
+
|
|
57
|
+
if (stats.isDirectory()) {
|
|
58
|
+
if (!options?.dryRun) {
|
|
59
|
+
await ensureDir(destPath);
|
|
60
|
+
}
|
|
61
|
+
const entries = await readdir(srcPath);
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
await copyRecursive(join(srcPath, entry), join(destPath, entry));
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
const destExists = await exists(destPath);
|
|
67
|
+
if (destExists && !options?.force) {
|
|
68
|
+
throw new Error(`File exists: ${destPath}. Use --force to overwrite.`);
|
|
69
|
+
}
|
|
70
|
+
if (!options?.dryRun) {
|
|
71
|
+
await ensureDir(dirname(destPath));
|
|
72
|
+
await copyFile(srcPath, destPath);
|
|
73
|
+
}
|
|
74
|
+
copied.push(destPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await copyRecursive(src, dest);
|
|
79
|
+
return copied;
|
|
80
|
+
}
|
package/src/core/http.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
|
|
6
|
+
export interface RecipeFetchRequest {
|
|
7
|
+
token: string;
|
|
8
|
+
device: {
|
|
9
|
+
id: string;
|
|
10
|
+
os: string;
|
|
11
|
+
arch: string;
|
|
12
|
+
version: string;
|
|
13
|
+
};
|
|
14
|
+
feature: string;
|
|
15
|
+
target: string;
|
|
16
|
+
starter: {
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RecipeFetchResponse {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
signedUrl?: string;
|
|
25
|
+
zipData?: string;
|
|
26
|
+
expiresIn?: number;
|
|
27
|
+
watermark?: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const WORKER_URL = process.env.VIBEFAST_WORKER_URL || 'https://vibefast-cli-worker.mzafar611.workers.dev';
|
|
33
|
+
|
|
34
|
+
export async function fetchRecipe(request: RecipeFetchRequest): Promise<RecipeFetchResponse> {
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(`${WORKER_URL}/api/recipe/fetch`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify(request),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const error: any = await response.json().catch(() => ({ message: response.statusText }));
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
error: error.message || `HTTP ${response.status}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return response.json() as Promise<RecipeFetchResponse>;
|
|
53
|
+
} catch (err: any) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: 'Network error',
|
|
57
|
+
message: `Failed to connect to ${WORKER_URL}: ${err.message}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function downloadZip(signedUrlOrBase64: string, isBase64 = false): Promise<string> {
|
|
63
|
+
let buffer: Buffer;
|
|
64
|
+
|
|
65
|
+
if (isBase64) {
|
|
66
|
+
// Decode base64 data
|
|
67
|
+
buffer = Buffer.from(signedUrlOrBase64, 'base64');
|
|
68
|
+
} else {
|
|
69
|
+
// Download from URL
|
|
70
|
+
const response = await fetch(signedUrlOrBase64);
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`Failed to download recipe: ${response.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
77
|
+
buffer = Buffer.from(arrayBuffer);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const tempDir = join(tmpdir(), 'vibefast', randomUUID());
|
|
81
|
+
await mkdir(tempDir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
const zipPath = join(tempDir, 'recipe.zip');
|
|
84
|
+
await writeFile(zipPath, buffer);
|
|
85
|
+
|
|
86
|
+
return zipPath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function listRecipes(token: string): Promise<any> {
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(`${WORKER_URL}/api/recipes/list`, {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
headers: {
|
|
94
|
+
'Authorization': `Bearer ${token}`,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new Error(`Failed to fetch recipes: ${response.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return response.json();
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
throw new Error(`Failed to connect to ${WORKER_URL}: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function listDevices(token: string): Promise<any> {
|
|
109
|
+
const response = await fetch(`${WORKER_URL}/api/devices/list`, {
|
|
110
|
+
method: 'GET',
|
|
111
|
+
headers: {
|
|
112
|
+
'Authorization': `Bearer ${token}`,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
throw new Error(`Failed to fetch devices: ${response.statusText}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return response.json();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function deactivateDevice(token: string, deviceId: string): Promise<void> {
|
|
124
|
+
const response = await fetch(`${WORKER_URL}/api/devices/deactivate`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: {
|
|
127
|
+
'Authorization': `Bearer ${token}`,
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({ deviceId }),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`Failed to deactivate device: ${response.statusText}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileContent, writeFileContent, exists, ensureDir } from './fsx.js';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
export interface JournalEntry {
|
|
5
|
+
feature: string;
|
|
6
|
+
target: 'native' | 'web';
|
|
7
|
+
files: string[];
|
|
8
|
+
insertedNav: boolean;
|
|
9
|
+
ts: number;
|
|
10
|
+
navHref?: string;
|
|
11
|
+
navLabel?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Journal {
|
|
15
|
+
entries: JournalEntry[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function readJournal(journalPath: string): Promise<Journal> {
|
|
19
|
+
if (!(await exists(journalPath))) {
|
|
20
|
+
return { entries: [] };
|
|
21
|
+
}
|
|
22
|
+
const content = await readFileContent(journalPath);
|
|
23
|
+
return JSON.parse(content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function writeJournal(journalPath: string, journal: Journal): Promise<void> {
|
|
27
|
+
await ensureDir(dirname(journalPath));
|
|
28
|
+
await writeFileContent(journalPath, JSON.stringify(journal, null, 2), { force: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function addEntry(journalPath: string, entry: JournalEntry): Promise<void> {
|
|
32
|
+
const journal = await readJournal(journalPath);
|
|
33
|
+
// Remove existing entry for same feature+target
|
|
34
|
+
journal.entries = journal.entries.filter(
|
|
35
|
+
e => !(e.feature === entry.feature && e.target === entry.target)
|
|
36
|
+
);
|
|
37
|
+
journal.entries.push(entry);
|
|
38
|
+
await writeJournal(journalPath, journal);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function removeEntry(
|
|
42
|
+
journalPath: string,
|
|
43
|
+
feature: string,
|
|
44
|
+
target: 'native' | 'web'
|
|
45
|
+
): Promise<JournalEntry | null> {
|
|
46
|
+
const journal = await readJournal(journalPath);
|
|
47
|
+
const entry = journal.entries.find(e => e.feature === feature && e.target === target);
|
|
48
|
+
if (!entry) return null;
|
|
49
|
+
|
|
50
|
+
journal.entries = journal.entries.filter(
|
|
51
|
+
e => !(e.feature === feature && e.target === target)
|
|
52
|
+
);
|
|
53
|
+
await writeJournal(journalPath, journal);
|
|
54
|
+
return entry;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function getEntry(
|
|
58
|
+
journalPath: string,
|
|
59
|
+
feature: string,
|
|
60
|
+
target: 'native' | 'web'
|
|
61
|
+
): Promise<JournalEntry | null> {
|
|
62
|
+
const journal = await readJournal(journalPath);
|
|
63
|
+
return journal.entries.find(e => e.feature === feature && e.target === target) || null;
|
|
64
|
+
}
|
package/src/core/log.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
export const log = {
|
|
4
|
+
success: (msg: string) => console.log(pc.green('✓'), msg),
|
|
5
|
+
info: (msg: string) => console.log(pc.blue('ℹ'), msg),
|
|
6
|
+
warn: (msg: string) => console.log(pc.yellow('⚠'), msg),
|
|
7
|
+
error: (msg: string) => console.error(pc.red('✗'), msg),
|
|
8
|
+
plain: (msg: string) => console.log(msg),
|
|
9
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolve, sep } from 'path';
|
|
2
|
+
|
|
3
|
+
function withTrailingSep(path: string): string {
|
|
4
|
+
return path.endsWith(sep) ? path : `${path}${sep}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isSubPath(basePath: string, targetPath: string): boolean {
|
|
8
|
+
const base = withTrailingSep(resolve(basePath));
|
|
9
|
+
const target = resolve(targetPath);
|
|
10
|
+
return target === base.slice(0, -1) || target.startsWith(base);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ensureWithinBase(basePath: string, targetPath: string, label: string): string {
|
|
14
|
+
const base = resolve(basePath);
|
|
15
|
+
const target = resolve(targetPath);
|
|
16
|
+
|
|
17
|
+
if (!isSubPath(base, target)) {
|
|
18
|
+
throw new Error(`${label} must stay within ${base}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return target;
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { join, resolve } from 'path';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
|
|
4
|
+
export interface PathsConfig {
|
|
5
|
+
cwd: string;
|
|
6
|
+
signatureFile: string;
|
|
7
|
+
journalFile: string;
|
|
8
|
+
configDir: string;
|
|
9
|
+
nativeNavFile: string;
|
|
10
|
+
webNavFile: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getPaths(cwd: string = process.cwd()): PathsConfig {
|
|
14
|
+
return {
|
|
15
|
+
cwd,
|
|
16
|
+
signatureFile: join(cwd, '.vibefast', 'starter.json'),
|
|
17
|
+
journalFile: join(cwd, '.vibefast', 'journal.json'),
|
|
18
|
+
configDir: join(homedir(), '.vibefast'),
|
|
19
|
+
nativeNavFile: join(cwd, 'apps', 'native', 'src', 'features', 'vibefast-features.ts'),
|
|
20
|
+
webNavFile: join(cwd, 'apps', 'web', 'app', 'page.tsx'),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getConfigPath(): string {
|
|
25
|
+
const directPath = process.env.VIBEFAST_CONFIG_PATH;
|
|
26
|
+
if (directPath) {
|
|
27
|
+
return resolve(directPath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const customDir = process.env.VIBEFAST_CONFIG_DIR;
|
|
31
|
+
const baseDir = customDir ? resolve(customDir) : join(homedir(), '.vibefast');
|
|
32
|
+
return join(baseDir, 'config.json');
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileContent, exists } from './fsx.js';
|
|
2
|
+
|
|
3
|
+
export interface StarterConfig {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
targets: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function validateSignature(signaturePath: string): Promise<StarterConfig> {
|
|
10
|
+
if (!(await exists(signaturePath))) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'Not a VibeFast repo. Missing .vibefast/starter.json. Run this command from your VibeFast monorepo root.'
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const content = await readFileContent(signaturePath);
|
|
17
|
+
let config: StarterConfig;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
config = JSON.parse(content);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error('Invalid .vibefast/starter.json: not valid JSON');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (config.name !== 'vibefast') {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Invalid starter name: expected "vibefast", got "${config.name}"`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!Array.isArray(config.targets) || config.targets.length === 0) {
|
|
32
|
+
throw new Error('Invalid .vibefast/starter.json: targets must be a non-empty array');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateTarget(target: string, availableTargets: string[]): void {
|
|
39
|
+
if (!availableTargets.includes(target)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Invalid target "${target}". Available: ${availableTargets.join(', ')}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { addCommand } from './commands/add.js';
|
|
5
|
+
import { removeCommand } from './commands/remove.js';
|
|
6
|
+
import { listCommand } from './commands/list.js';
|
|
7
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
8
|
+
import { loginCommand } from './commands/login.js';
|
|
9
|
+
import { devicesCommand } from './commands/devices.js';
|
|
10
|
+
import { logoutCommand } from './commands/logout.js';
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('vf')
|
|
16
|
+
.description('VibeFast CLI - Install features into your monorepo')
|
|
17
|
+
.version('0.1.0');
|
|
18
|
+
|
|
19
|
+
program.addCommand(loginCommand);
|
|
20
|
+
program.addCommand(logoutCommand);
|
|
21
|
+
program.addCommand(devicesCommand);
|
|
22
|
+
program.addCommand(doctorCommand);
|
|
23
|
+
program.addCommand(listCommand);
|
|
24
|
+
program.addCommand(addCommand);
|
|
25
|
+
program.addCommand(removeCommand);
|
|
26
|
+
|
|
27
|
+
program.parse();
|