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,161 @@
|
|
|
1
|
+
import { useConversation } from '@elevenlabs/react-native';
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { generateConvexHttpApiUrl } from '@/core/utils/convex-http-api';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ConversationState,
|
|
8
|
+
VoiceBotCallbacks,
|
|
9
|
+
VoiceBotConfig,
|
|
10
|
+
} from '../types';
|
|
11
|
+
|
|
12
|
+
export function useVoiceBot(
|
|
13
|
+
config: VoiceBotConfig,
|
|
14
|
+
callbacks?: VoiceBotCallbacks,
|
|
15
|
+
) {
|
|
16
|
+
const [isStarting, setIsStarting] = useState(false);
|
|
17
|
+
const [textInput, setTextInput] = useState('');
|
|
18
|
+
const tokenFetchUrl = useMemo(() => {
|
|
19
|
+
try {
|
|
20
|
+
return generateConvexHttpApiUrl('/voice/conversation-token');
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(
|
|
23
|
+
'[useVoiceBot] Failed to compute ElevenLabs token fetch URL',
|
|
24
|
+
error,
|
|
25
|
+
);
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const conversation = useConversation({
|
|
31
|
+
tokenFetchUrl,
|
|
32
|
+
onConnect: ({ conversationId }: { conversationId: string }) => {
|
|
33
|
+
console.log('✅ Connected to conversation', conversationId);
|
|
34
|
+
callbacks?.onConnect?.(conversationId);
|
|
35
|
+
},
|
|
36
|
+
onDisconnect: (details: string) => {
|
|
37
|
+
console.log('❌ Disconnected from conversation', details);
|
|
38
|
+
callbacks?.onDisconnect?.(details);
|
|
39
|
+
},
|
|
40
|
+
onError: (message: string, context?: Record<string, unknown>) => {
|
|
41
|
+
console.error('❌ Conversation error:', message, context);
|
|
42
|
+
callbacks?.onError?.(message, context);
|
|
43
|
+
},
|
|
44
|
+
onMessage: ({ message, source }) => {
|
|
45
|
+
console.log(`💬 Message from ${source}:`, message);
|
|
46
|
+
callbacks?.onMessage?.({
|
|
47
|
+
id: Date.now().toString(),
|
|
48
|
+
content: message.toString(),
|
|
49
|
+
role: source,
|
|
50
|
+
timestamp: new Date(),
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
onModeChange: ({ mode }: { mode: 'speaking' | 'listening' }) => {
|
|
54
|
+
console.log(`🔊 Mode: ${mode}`);
|
|
55
|
+
callbacks?.onModeChange?.(mode);
|
|
56
|
+
},
|
|
57
|
+
onStatusChange: ({ status }) => {
|
|
58
|
+
console.log(`📡 Status: ${status}`);
|
|
59
|
+
callbacks?.onStatusChange?.(status);
|
|
60
|
+
},
|
|
61
|
+
onCanSendFeedbackChange: ({ canSendFeedback }) => {
|
|
62
|
+
console.log(`🔊 Can send feedback: ${canSendFeedback}`);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const state: ConversationState = {
|
|
67
|
+
status: conversation.status,
|
|
68
|
+
conversationId: conversation.getId(),
|
|
69
|
+
isSpeaking: conversation.isSpeaking,
|
|
70
|
+
canSendFeedback: conversation.canSendFeedback,
|
|
71
|
+
isStarting,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const startConversation = async () => {
|
|
75
|
+
if (isStarting) return;
|
|
76
|
+
setIsStarting(true);
|
|
77
|
+
try {
|
|
78
|
+
await conversation.startSession({
|
|
79
|
+
agentId: config.agentId,
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Failed to start conversation:', error);
|
|
83
|
+
callbacks?.onError?.('Failed to start conversation', { error });
|
|
84
|
+
} finally {
|
|
85
|
+
setIsStarting(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const endConversation = async () => {
|
|
90
|
+
try {
|
|
91
|
+
// Idempotent: only end if actually connected
|
|
92
|
+
if (conversation.status === 'connected') {
|
|
93
|
+
await conversation.endSession();
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Failed to end conversation:', error);
|
|
97
|
+
callbacks?.onError?.('Failed to end conversation', { error });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const sendMessage = (message: string) => {
|
|
102
|
+
if (message.trim()) {
|
|
103
|
+
conversation.sendUserMessage(message.trim());
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const sendContextualUpdate = (context: string) => {
|
|
108
|
+
if (context.trim()) {
|
|
109
|
+
conversation.sendContextualUpdate(context.trim());
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const sendFeedback = (isPositive: boolean) => {
|
|
114
|
+
conversation.sendFeedback(isPositive);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const sendUserActivity = () => {
|
|
118
|
+
conversation.sendUserActivity();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ---- Cleanup only on UNMOUNT (avoid running on every conversation object change)
|
|
122
|
+
|
|
123
|
+
// Keep latest refs so unmount cleanup has fresh values
|
|
124
|
+
const convRef = useRef(conversation);
|
|
125
|
+
const statusRef = useRef(conversation.status);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
convRef.current = conversation;
|
|
128
|
+
statusRef.current = conversation.status;
|
|
129
|
+
}, [conversation, conversation.status]);
|
|
130
|
+
|
|
131
|
+
// Guard for React dev StrictMode double-invoke
|
|
132
|
+
const cleanedUpRef = useRef(false);
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
return () => {
|
|
136
|
+
if (cleanedUpRef.current) return;
|
|
137
|
+
cleanedUpRef.current = true;
|
|
138
|
+
|
|
139
|
+
if (statusRef.current === 'connected') {
|
|
140
|
+
console.log('🧹 Auto-ending conversation due to screen exit');
|
|
141
|
+
convRef.current.endSession().catch((error: unknown) => {
|
|
142
|
+
console.error('Failed to auto-end conversation:', error);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
state,
|
|
150
|
+
actions: {
|
|
151
|
+
startConversation,
|
|
152
|
+
endConversation,
|
|
153
|
+
sendMessage,
|
|
154
|
+
sendContextualUpdate,
|
|
155
|
+
sendFeedback,
|
|
156
|
+
sendUserActivity,
|
|
157
|
+
},
|
|
158
|
+
textInput,
|
|
159
|
+
setTextInput,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ConversationStatus, Role } from '@elevenlabs/react-native';
|
|
2
|
+
|
|
3
|
+
export interface VoiceBotConfig {
|
|
4
|
+
agentId: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ConversationState {
|
|
8
|
+
status: ConversationStatus;
|
|
9
|
+
conversationId: string | null;
|
|
10
|
+
isSpeaking: boolean;
|
|
11
|
+
canSendFeedback: boolean;
|
|
12
|
+
isStarting: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface VoiceBotMessage {
|
|
16
|
+
id: string;
|
|
17
|
+
content: string;
|
|
18
|
+
role: Role;
|
|
19
|
+
timestamp: Date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VoiceBotCallbacks {
|
|
23
|
+
onConnect?: (conversationId: string) => void;
|
|
24
|
+
onDisconnect?: (details: string) => void;
|
|
25
|
+
onError?: (message: string, context?: Record<string, unknown>) => void;
|
|
26
|
+
onMessage?: (message: VoiceBotMessage) => void;
|
|
27
|
+
onModeChange?: (mode: 'speaking' | 'listening') => void;
|
|
28
|
+
onStatusChange?: (status: ConversationStatus) => void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "voice-bot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Real-time voice conversations with AI",
|
|
5
|
+
"copy": [
|
|
6
|
+
{
|
|
7
|
+
"from": "apps/native/src/app/voice-bot",
|
|
8
|
+
"to": "apps/native/src/app/(root)/(protected)/voice-bot"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"from": "apps/native/src/features/voice-bot",
|
|
12
|
+
"to": "apps/native/src/features/voice-bot"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"nav": {
|
|
16
|
+
"href": "/(root)/(protected)/voice-bot",
|
|
17
|
+
"label": "Voice Assistant",
|
|
18
|
+
"icon": "🗣️",
|
|
19
|
+
"color": "#14B8A6"
|
|
20
|
+
},
|
|
21
|
+
"target": "native"
|
|
22
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readdir, stat, mkdir, writeFile, cp } from 'fs/promises';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
// Feature definitions
|
|
13
|
+
const features = [
|
|
14
|
+
{
|
|
15
|
+
name: 'charts',
|
|
16
|
+
label: 'Charts',
|
|
17
|
+
icon: '📊',
|
|
18
|
+
description: 'Beautiful data visualization with charts',
|
|
19
|
+
color: '#6366F1',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'chatbot',
|
|
23
|
+
label: 'AI Chatbot',
|
|
24
|
+
icon: '💬',
|
|
25
|
+
description: 'AI-powered chat assistant',
|
|
26
|
+
color: '#F43F5E',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'voice-bot',
|
|
30
|
+
label: 'Voice Assistant',
|
|
31
|
+
icon: '🗣️',
|
|
32
|
+
description: 'Real-time voice conversations with AI',
|
|
33
|
+
color: '#14B8A6',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'image-generator',
|
|
37
|
+
label: 'Image Generator',
|
|
38
|
+
icon: '🎨',
|
|
39
|
+
description: 'AI-powered image generation',
|
|
40
|
+
color: '#A855F7',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'audio-recorder',
|
|
44
|
+
label: 'Voice Notes',
|
|
45
|
+
icon: '🎤',
|
|
46
|
+
description: 'Record and manage voice notes',
|
|
47
|
+
color: '#EF4444',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'quiz',
|
|
51
|
+
label: 'Quiz',
|
|
52
|
+
icon: '📱',
|
|
53
|
+
description: 'Interactive quiz feature',
|
|
54
|
+
color: '#F97316',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'tracker-app',
|
|
58
|
+
label: 'Tracker',
|
|
59
|
+
icon: '🍎',
|
|
60
|
+
description: 'Track habits and activities',
|
|
61
|
+
color: '#06B6D4',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const monorepoPath = join(__dirname, '../../vibefast-monorepo');
|
|
66
|
+
const recipesDir = join(__dirname, '../recipes');
|
|
67
|
+
|
|
68
|
+
async function exists(path) {
|
|
69
|
+
try {
|
|
70
|
+
await stat(path);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function createRecipe(feature) {
|
|
78
|
+
console.log(`\n📦 Creating recipe for ${feature.name}...`);
|
|
79
|
+
|
|
80
|
+
const recipeDir = join(recipesDir, feature.name);
|
|
81
|
+
await mkdir(recipeDir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
// Paths to copy
|
|
84
|
+
const appPath = join(monorepoPath, 'apps/native/src/app/(root)/(protected)', feature.name);
|
|
85
|
+
const featurePath = join(monorepoPath, 'apps/native/src/features', feature.name);
|
|
86
|
+
|
|
87
|
+
const copySpecs = [];
|
|
88
|
+
|
|
89
|
+
// Check if app route exists
|
|
90
|
+
if (await exists(appPath)) {
|
|
91
|
+
const destAppPath = join(recipeDir, 'apps/native/src/app', feature.name);
|
|
92
|
+
await mkdir(dirname(destAppPath), { recursive: true });
|
|
93
|
+
await cp(appPath, destAppPath, { recursive: true });
|
|
94
|
+
copySpecs.push({
|
|
95
|
+
from: `apps/native/src/app/${feature.name}`,
|
|
96
|
+
to: `apps/native/src/app/(root)/(protected)/${feature.name}`,
|
|
97
|
+
});
|
|
98
|
+
console.log(` ✓ Copied app route`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if feature components exist
|
|
102
|
+
if (await exists(featurePath)) {
|
|
103
|
+
const destFeaturePath = join(recipeDir, 'apps/native/src/features', feature.name);
|
|
104
|
+
await mkdir(dirname(destFeaturePath), { recursive: true });
|
|
105
|
+
await cp(featurePath, destFeaturePath, { recursive: true });
|
|
106
|
+
copySpecs.push({
|
|
107
|
+
from: `apps/native/src/features/${feature.name}`,
|
|
108
|
+
to: `apps/native/src/features/${feature.name}`,
|
|
109
|
+
});
|
|
110
|
+
console.log(` ✓ Copied feature components`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (copySpecs.length === 0) {
|
|
114
|
+
console.log(` ⚠️ No files found for ${feature.name}, skipping...`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create recipe.json
|
|
119
|
+
const manifest = {
|
|
120
|
+
name: feature.name,
|
|
121
|
+
version: '1.0.0',
|
|
122
|
+
description: feature.description,
|
|
123
|
+
copy: copySpecs,
|
|
124
|
+
nav: {
|
|
125
|
+
href: `/(root)/(protected)/${feature.name}`,
|
|
126
|
+
label: feature.label,
|
|
127
|
+
icon: feature.icon,
|
|
128
|
+
color: feature.color,
|
|
129
|
+
},
|
|
130
|
+
target: 'native',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
await writeFile(
|
|
134
|
+
join(recipeDir, 'recipe.json'),
|
|
135
|
+
JSON.stringify(manifest, null, 2)
|
|
136
|
+
);
|
|
137
|
+
console.log(` ✓ Created recipe.json`);
|
|
138
|
+
|
|
139
|
+
// Create zip
|
|
140
|
+
const zipName = `${feature.name}@latest.zip`;
|
|
141
|
+
const zipPath = join(recipesDir, zipName);
|
|
142
|
+
|
|
143
|
+
await execAsync(`cd "${recipeDir}" && zip -r "../${zipName}" .`);
|
|
144
|
+
console.log(` ✓ Created ${zipName}`);
|
|
145
|
+
|
|
146
|
+
return zipPath;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function main() {
|
|
150
|
+
console.log('🚀 VibeFast Recipe Generator\n');
|
|
151
|
+
console.log(`Monorepo: ${monorepoPath}`);
|
|
152
|
+
console.log(`Output: ${recipesDir}\n`);
|
|
153
|
+
|
|
154
|
+
await mkdir(recipesDir, { recursive: true });
|
|
155
|
+
|
|
156
|
+
const created = [];
|
|
157
|
+
|
|
158
|
+
for (const feature of features) {
|
|
159
|
+
try {
|
|
160
|
+
const zipPath = await createRecipe(feature);
|
|
161
|
+
if (zipPath) {
|
|
162
|
+
created.push({ name: feature.name, zipPath });
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(` ✗ Error creating ${feature.name}:`, err.message);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log('\n✅ Recipe creation complete!\n');
|
|
170
|
+
console.log('Created recipes:');
|
|
171
|
+
created.forEach(({ name, zipPath }) => {
|
|
172
|
+
console.log(` • ${name}@latest.zip`);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
console.log('\n📤 Upload to R2 with:');
|
|
176
|
+
console.log('cd ../recipes');
|
|
177
|
+
created.forEach(({ name }) => {
|
|
178
|
+
console.log(`wrangler r2 object put vibefast-recipes/${name}@latest.zip --file=${name}@latest.zip --remote`);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
console.log('\n🧪 Test with:');
|
|
182
|
+
console.log('cd ../../vibefast-monorepo');
|
|
183
|
+
console.log('export VIBEFAST_WORKER_URL=https://vibefast-cli-worker.mzafar611.workers.dev');
|
|
184
|
+
created.forEach(({ name }) => {
|
|
185
|
+
console.log(`vf add ${name}`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,183 @@
|
|
|
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 } from '../core/fsx.js';
|
|
9
|
+
import { insertNavLinkNative, insertNavLinkWeb } from '../core/codemod.js';
|
|
10
|
+
import { join, resolve } from 'path';
|
|
11
|
+
import { ensureWithinBase } from '../core/pathGuard.js';
|
|
12
|
+
import { extractZipSafe } from '../core/archive.js';
|
|
13
|
+
|
|
14
|
+
interface RecipeManifest {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
description: string;
|
|
18
|
+
copy: Array<{ from: string; to: string }>;
|
|
19
|
+
nav: { href: string; label: string };
|
|
20
|
+
target: 'native' | 'web';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const addCommand = new Command('add')
|
|
24
|
+
.description('Add a VibeFast feature to your project')
|
|
25
|
+
.argument('<feature>', 'Feature name to install')
|
|
26
|
+
.option('--target <target>', 'Target platform (native or web)', 'native')
|
|
27
|
+
.option('--dry-run', 'Preview changes without applying')
|
|
28
|
+
.option('--force', 'Overwrite existing files')
|
|
29
|
+
.action(async (feature: string, options) => {
|
|
30
|
+
try {
|
|
31
|
+
const paths = getPaths();
|
|
32
|
+
const target = options.target as 'native' | 'web';
|
|
33
|
+
|
|
34
|
+
// Validate setup
|
|
35
|
+
const config = await validateSignature(paths.signatureFile);
|
|
36
|
+
validateTarget(target, config.targets);
|
|
37
|
+
|
|
38
|
+
// Check auth
|
|
39
|
+
const token = await getToken();
|
|
40
|
+
if (!token) {
|
|
41
|
+
log.error('Not logged in. Run "vf login --token <TOKEN>" first');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if already installed
|
|
46
|
+
const existing = await getEntry(paths.journalFile, feature, target);
|
|
47
|
+
if (existing && !options.force) {
|
|
48
|
+
log.warn(`${feature} is already installed for ${target}`);
|
|
49
|
+
log.info('Use --force to reinstall');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (options.dryRun) {
|
|
54
|
+
log.info('[DRY RUN] No changes will be made');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fetch recipe
|
|
58
|
+
log.info(`Fetching ${feature} for ${target}...`);
|
|
59
|
+
const device = await getDeviceInfo();
|
|
60
|
+
const response = await fetchRecipe({
|
|
61
|
+
token,
|
|
62
|
+
device,
|
|
63
|
+
feature,
|
|
64
|
+
target,
|
|
65
|
+
starter: { name: config.name, version: config.version },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!response.ok || (!response.signedUrl && !response.zipData)) {
|
|
69
|
+
log.error(`Failed to fetch recipe: ${response.error || 'Unknown error'}`);
|
|
70
|
+
if (response.message) {
|
|
71
|
+
log.plain(` ${response.message}`);
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Download and extract
|
|
77
|
+
log.info('Downloading recipe...');
|
|
78
|
+
const zipPath = response.zipData
|
|
79
|
+
? await downloadZip(response.zipData, true)
|
|
80
|
+
: await downloadZip(response.signedUrl!);
|
|
81
|
+
|
|
82
|
+
const extractDir = zipPath.replace('.zip', '');
|
|
83
|
+
const extractRoot = resolve(extractDir);
|
|
84
|
+
const repoRoot = resolve(paths.cwd);
|
|
85
|
+
await extractZipSafe(zipPath, extractDir);
|
|
86
|
+
|
|
87
|
+
// Read manifest
|
|
88
|
+
const manifestPath = join(extractDir, 'recipe.json');
|
|
89
|
+
const manifestContent = await readFileContent(manifestPath);
|
|
90
|
+
const manifest: RecipeManifest = JSON.parse(manifestContent);
|
|
91
|
+
if (manifest.target !== target) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Recipe target mismatch: expected ${target}, got ${manifest.target}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
log.info(`Installing ${manifest.name} v${manifest.version}...`);
|
|
98
|
+
|
|
99
|
+
// Copy files
|
|
100
|
+
const copiedFiles: string[] = [];
|
|
101
|
+
for (const copySpec of manifest.copy) {
|
|
102
|
+
const srcPath = ensureWithinBase(
|
|
103
|
+
extractRoot,
|
|
104
|
+
resolve(extractRoot, copySpec.from),
|
|
105
|
+
`Recipe file ${copySpec.from}`
|
|
106
|
+
);
|
|
107
|
+
const destPath = ensureWithinBase(
|
|
108
|
+
repoRoot,
|
|
109
|
+
resolve(repoRoot, copySpec.to),
|
|
110
|
+
`Destination ${copySpec.to}`
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
log.info(`Copying ${copySpec.from} → ${copySpec.to}`);
|
|
114
|
+
const files = await copyTree(srcPath, destPath, {
|
|
115
|
+
dryRun: options.dryRun,
|
|
116
|
+
force: options.force,
|
|
117
|
+
});
|
|
118
|
+
copiedFiles.push(...files);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Add watermark if provided
|
|
122
|
+
if (response.watermark && !options.dryRun) {
|
|
123
|
+
const { writeFileContent, readFileContent } = await import('../core/fsx.js');
|
|
124
|
+
for (const file of copiedFiles) {
|
|
125
|
+
if (file.endsWith('.ts') || file.endsWith('.tsx') || file.endsWith('.js') || file.endsWith('.jsx')) {
|
|
126
|
+
const content = await readFileContent(file);
|
|
127
|
+
const watermarked = `// vibefast license: ${response.watermark}\n${content}`;
|
|
128
|
+
await writeFileContent(file, watermarked, { force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Insert nav link
|
|
134
|
+
let navInserted = false;
|
|
135
|
+
let navHref: string | undefined;
|
|
136
|
+
let navLabel: string | undefined;
|
|
137
|
+
if (manifest.nav) {
|
|
138
|
+
log.info('Adding navigation link...');
|
|
139
|
+
const navFile = target === 'native' ? paths.nativeNavFile : paths.webNavFile;
|
|
140
|
+
const insertFn = target === 'native' ? insertNavLinkNative : insertNavLinkWeb;
|
|
141
|
+
navHref = manifest.nav.href;
|
|
142
|
+
navLabel = manifest.nav.label;
|
|
143
|
+
|
|
144
|
+
navInserted = await insertFn(navFile, manifest.nav, { dryRun: options.dryRun });
|
|
145
|
+
if (navInserted) {
|
|
146
|
+
log.success('Navigation link added');
|
|
147
|
+
} else {
|
|
148
|
+
log.info('Navigation link already exists');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Update journal
|
|
153
|
+
if (!options.dryRun) {
|
|
154
|
+
await addEntry(paths.journalFile, {
|
|
155
|
+
feature: manifest.name,
|
|
156
|
+
target: manifest.target,
|
|
157
|
+
files: copiedFiles,
|
|
158
|
+
insertedNav: navInserted,
|
|
159
|
+
navHref,
|
|
160
|
+
navLabel,
|
|
161
|
+
ts: Date.now(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
log.success(`${manifest.name} installed successfully!`);
|
|
166
|
+
log.info(`Files added: ${copiedFiles.length}`);
|
|
167
|
+
|
|
168
|
+
if (options.dryRun) {
|
|
169
|
+
log.warn('This was a dry run. Run without --dry-run to apply changes.');
|
|
170
|
+
} else {
|
|
171
|
+
log.info('Next steps:');
|
|
172
|
+
log.plain(` 1. Review the changes in your repo`);
|
|
173
|
+
log.plain(` 2. Run your dev server to test`);
|
|
174
|
+
log.plain(` 3. Navigate to the new feature`);
|
|
175
|
+
}
|
|
176
|
+
} catch (error: any) {
|
|
177
|
+
log.error(`Installation failed: ${error.message}`);
|
|
178
|
+
if (error.stack) {
|
|
179
|
+
log.plain(error.stack);
|
|
180
|
+
}
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { log } from '../core/log.js';
|
|
3
|
+
import { getToken } from '../core/auth.js';
|
|
4
|
+
import { listDevices, deactivateDevice } from '../core/http.js';
|
|
5
|
+
|
|
6
|
+
export const devicesCommand = new Command('devices')
|
|
7
|
+
.description('Manage your device activations')
|
|
8
|
+
.option('--deactivate <id>', 'Deactivate a device by ID')
|
|
9
|
+
.action(async (options) => {
|
|
10
|
+
try {
|
|
11
|
+
const token = await getToken();
|
|
12
|
+
if (!token) {
|
|
13
|
+
log.error('Not logged in. Run "vf login --token <TOKEN>" first');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (options.deactivate) {
|
|
18
|
+
await deactivateDevice(token, options.deactivate);
|
|
19
|
+
log.success(`Device ${options.deactivate} deactivated`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const response = await listDevices(token);
|
|
24
|
+
|
|
25
|
+
if (!response.devices || response.devices.length === 0) {
|
|
26
|
+
log.info('No active devices');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
log.info(`Active devices (${response.devices.length}/${response.maxDevices}):`);
|
|
31
|
+
response.devices.forEach((device: any) => {
|
|
32
|
+
log.plain(` • ${device.id} (${device.os}/${device.arch}) - ${device.lastSeen}`);
|
|
33
|
+
});
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
log.error(`Failed: ${error.message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { log } from '../core/log.js';
|
|
3
|
+
import { getPaths } from '../core/paths.js';
|
|
4
|
+
import { validateSignature } from '../core/validate.js';
|
|
5
|
+
import { exists } from '../core/fsx.js';
|
|
6
|
+
import { getToken } from '../core/auth.js';
|
|
7
|
+
|
|
8
|
+
export const doctorCommand = new Command('doctor')
|
|
9
|
+
.description('Check your VibeFast setup')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const paths = getPaths();
|
|
13
|
+
let hasErrors = false;
|
|
14
|
+
|
|
15
|
+
// Check authentication
|
|
16
|
+
log.info('Checking authentication...');
|
|
17
|
+
const token = await getToken();
|
|
18
|
+
if (!token) {
|
|
19
|
+
log.error('Not logged in');
|
|
20
|
+
log.plain(' Run: vf login --token <YOUR_TOKEN>');
|
|
21
|
+
hasErrors = true;
|
|
22
|
+
} else {
|
|
23
|
+
log.success('Authenticated');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check signature file
|
|
27
|
+
log.info('Checking repository signature...');
|
|
28
|
+
try {
|
|
29
|
+
const config = await validateSignature(paths.signatureFile);
|
|
30
|
+
log.success(`Found VibeFast repo (v${config.version})`);
|
|
31
|
+
log.info(`Available targets: ${config.targets.join(', ')}`);
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
log.error(error.message);
|
|
34
|
+
hasErrors = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check nav markers
|
|
38
|
+
log.info('Checking navigation markers...');
|
|
39
|
+
|
|
40
|
+
const nativeExists = await exists(paths.nativeNavFile);
|
|
41
|
+
if (nativeExists) {
|
|
42
|
+
log.success('Native nav file found');
|
|
43
|
+
} else {
|
|
44
|
+
log.warn('Native nav file not found');
|
|
45
|
+
log.plain(` Expected: ${paths.nativeNavFile}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const webExists = await exists(paths.webNavFile);
|
|
49
|
+
if (webExists) {
|
|
50
|
+
log.success('Web nav file found');
|
|
51
|
+
} else {
|
|
52
|
+
log.warn('Web nav file not found');
|
|
53
|
+
log.plain(` Expected: ${paths.webNavFile}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (hasErrors) {
|
|
57
|
+
log.error('Setup incomplete');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
} else {
|
|
60
|
+
log.success('All checks passed! Ready to add features.');
|
|
61
|
+
log.info('Run "vf list" to see available features');
|
|
62
|
+
}
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
log.error(`Doctor check failed: ${error.message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|