@gxp-dev/tools 2.0.11 → 2.0.12

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.
@@ -0,0 +1,806 @@
1
+ /**
2
+ * AI Scaffolding Service
3
+ *
4
+ * Uses AI to generate plugin scaffolding based on user prompts.
5
+ * Supports multiple AI providers:
6
+ * - Claude (via claude CLI - uses logged-in account)
7
+ * - Codex (via codex CLI - uses logged-in account)
8
+ * - Gemini (via API key or gcloud CLI)
9
+ */
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+ const { spawn, execSync } = require("child_process");
14
+
15
+ // AI scaffolding prompt template
16
+ const SCAFFOLD_SYSTEM_PROMPT = `You are an expert GxP plugin developer assistant. Your task is to help create Vue.js plugin components for the GxP platform.
17
+
18
+ ## GxP Plugin Architecture
19
+
20
+ GxP plugins are Vue 3 Single File Components (SFCs) that run on kiosk displays. They use:
21
+ - Vue 3 Composition API with <script setup>
22
+ - Pinia for state management via the GxP Store
23
+ - GxP Component Kit (@gramercytech/gx-componentkit) for UI components
24
+ - gxp-string and gxp-src directives for dynamic content
25
+
26
+ ## Key Components Available
27
+
28
+ From GxP Component Kit:
29
+ - GxButton - Styled buttons with variants (primary, secondary, outline)
30
+ - GxCard - Card containers with optional header/footer
31
+ - GxInput - Form inputs with validation
32
+ - GxModal - Modal dialogs
33
+ - GxSpinner - Loading indicators
34
+ - GxAlert - Alert/notification messages
35
+ - GxBadge - Status badges
36
+ - GxAvatar - User avatars
37
+ - GxProgress - Progress bars
38
+ - GxTabs - Tab navigation
39
+ - GxAccordion - Collapsible sections
40
+
41
+ ## GxP Store Usage
42
+
43
+ \`\`\`javascript
44
+ import { useGxpStore } from '@gx-runtime/stores/gxpPortalConfigStore';
45
+
46
+ const store = useGxpStore();
47
+
48
+ // Get values
49
+ store.getString('key', 'default'); // From stringsList
50
+ store.getSetting('key', 'default'); // From pluginVars/settings
51
+ store.getAsset('key', '/fallback.jpg'); // From assetList
52
+ store.getState('key', null); // From triggerState
53
+
54
+ // Update values
55
+ store.updateString('key', 'value');
56
+ store.updateSetting('key', 'value');
57
+ store.updateAsset('key', 'url');
58
+ store.updateState('key', 'value');
59
+
60
+ // API calls
61
+ await store.apiGet('/endpoint', { params });
62
+ await store.apiPost('/endpoint', data);
63
+
64
+ // Socket events
65
+ store.listenSocket('primary', 'EventName', callback);
66
+ store.emitSocket('primary', 'event', data);
67
+ \`\`\`
68
+
69
+ ## GxP Directives
70
+
71
+ \`\`\`html
72
+ <!-- Replace text from strings -->
73
+ <h1 gxp-string="welcome_title">Default Title</h1>
74
+
75
+ <!-- Replace text from settings -->
76
+ <span gxp-string="company_name" gxp-settings>Company</span>
77
+
78
+ <!-- Replace image src from assets -->
79
+ <img gxp-src="hero_image" src="/placeholder.jpg" alt="Hero">
80
+ \`\`\`
81
+
82
+ ## Response Format
83
+
84
+ When asked to generate code, respond with a JSON object containing:
85
+ 1. "components" - Array of Vue SFC files to create
86
+ 2. "manifest" - Updates to app-manifest.json (strings, assets, settings)
87
+ 3. "explanation" - Brief explanation of what was created
88
+
89
+ Example response:
90
+ \`\`\`json
91
+ {
92
+ "components": [
93
+ {
94
+ "path": "src/views/CheckInView.vue",
95
+ "content": "<template>...</template>\\n<script setup>...</script>\\n<style scoped>...</style>"
96
+ }
97
+ ],
98
+ "manifest": {
99
+ "strings": {
100
+ "default": {
101
+ "checkin_title": "Check In",
102
+ "checkin_button": "Submit"
103
+ }
104
+ },
105
+ "assets": {
106
+ "checkin_logo": "/dev-assets/images/logo.png"
107
+ }
108
+ },
109
+ "explanation": "Created a check-in view with form inputs and validation."
110
+ }
111
+ \`\`\`
112
+
113
+ ## Important Guidelines
114
+
115
+ 1. Always use GxP Component Kit components when available
116
+ 2. Use gxp-string for all user-facing text
117
+ 3. Use gxp-src for all images
118
+ 4. Keep components focused and modular
119
+ 5. Include proper TypeScript types where beneficial
120
+ 6. Add scoped styles for component-specific CSS
121
+ 7. Use Composition API with <script setup>
122
+ 8. Handle loading and error states appropriately
123
+ `;
124
+
125
+ /**
126
+ * Available AI providers
127
+ */
128
+ const AI_PROVIDERS = {
129
+ claude: {
130
+ name: "Claude",
131
+ description: "Anthropic Claude (uses logged-in claude CLI)",
132
+ checkAvailable: checkClaudeAvailable,
133
+ generate: generateWithClaude,
134
+ },
135
+ codex: {
136
+ name: "Codex",
137
+ description: "OpenAI Codex (uses logged-in codex CLI)",
138
+ checkAvailable: checkCodexAvailable,
139
+ generate: generateWithCodex,
140
+ },
141
+ gemini: {
142
+ name: "Gemini",
143
+ description: "Google Gemini (API key or gcloud CLI)",
144
+ checkAvailable: checkGeminiAvailable,
145
+ generate: generateWithGemini,
146
+ },
147
+ };
148
+
149
+ /**
150
+ * Check if Claude CLI is available and logged in
151
+ * @returns {Promise<{available: boolean, reason?: string}>}
152
+ */
153
+ async function checkClaudeAvailable() {
154
+ try {
155
+ // Check if claude CLI exists
156
+ execSync("which claude", { stdio: "pipe" });
157
+
158
+ // Check if logged in by running a simple command
159
+ // Claude CLI doesn't have a direct "whoami" but we can check if it works
160
+ return { available: true };
161
+ } catch (error) {
162
+ return {
163
+ available: false,
164
+ reason:
165
+ "Claude CLI not installed. Install with: npm install -g @anthropic-ai/claude-code",
166
+ };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Check if Codex CLI is available and logged in
172
+ * @returns {Promise<{available: boolean, reason?: string}>}
173
+ */
174
+ async function checkCodexAvailable() {
175
+ try {
176
+ // Check if codex CLI exists
177
+ execSync("which codex", { stdio: "pipe" });
178
+ return { available: true };
179
+ } catch (error) {
180
+ return {
181
+ available: false,
182
+ reason:
183
+ "Codex CLI not installed. Install with: npm install -g @openai/codex",
184
+ };
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Check if Gemini is available (API key or gcloud)
190
+ * @returns {Promise<{available: boolean, reason?: string, method?: string}>}
191
+ */
192
+ async function checkGeminiAvailable() {
193
+ // Check for API key first
194
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
195
+ if (apiKey) {
196
+ return { available: true, method: "api_key" };
197
+ }
198
+
199
+ // Check for gcloud CLI with auth
200
+ try {
201
+ execSync("which gcloud", { stdio: "pipe" });
202
+ const authList = execSync("gcloud auth list --format='value(account)'", {
203
+ stdio: "pipe",
204
+ }).toString();
205
+ if (authList.trim()) {
206
+ return { available: true, method: "gcloud" };
207
+ }
208
+ } catch (error) {
209
+ // gcloud not available or not authenticated
210
+ }
211
+
212
+ return {
213
+ available: false,
214
+ reason:
215
+ "Gemini requires either:\n" +
216
+ " • GEMINI_API_KEY environment variable, or\n" +
217
+ " • gcloud CLI logged in (gcloud auth login)",
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Get list of available AI providers
223
+ * @returns {Promise<Array<{id: string, name: string, description: string, available: boolean, reason?: string}>>}
224
+ */
225
+ async function getAvailableProviders() {
226
+ const providers = [];
227
+
228
+ for (const [id, provider] of Object.entries(AI_PROVIDERS)) {
229
+ const status = await provider.checkAvailable();
230
+ providers.push({
231
+ id,
232
+ name: provider.name,
233
+ description: provider.description,
234
+ available: status.available,
235
+ reason: status.reason,
236
+ method: status.method,
237
+ });
238
+ }
239
+
240
+ return providers;
241
+ }
242
+
243
+ /**
244
+ * Generate scaffold using Claude CLI
245
+ * @param {string} userPrompt - User's description
246
+ * @param {string} projectName - Project name
247
+ * @param {string} description - Project description
248
+ * @returns {Promise<object|null>}
249
+ */
250
+ async function generateWithClaude(userPrompt, projectName, description) {
251
+ const fullPrompt = buildFullPrompt(userPrompt, projectName, description);
252
+
253
+ return new Promise((resolve) => {
254
+ console.log("\n🤖 Generating plugin scaffold with Claude...\n");
255
+
256
+ let output = "";
257
+ let errorOutput = "";
258
+
259
+ // Use claude CLI with --print flag to get direct output
260
+ const claude = spawn(
261
+ "claude",
262
+ ["--print", "-p", `${SCAFFOLD_SYSTEM_PROMPT}\n\n${fullPrompt}`],
263
+ {
264
+ stdio: ["pipe", "pipe", "pipe"],
265
+ shell: true,
266
+ }
267
+ );
268
+
269
+ claude.stdout.on("data", (data) => {
270
+ output += data.toString();
271
+ });
272
+
273
+ claude.stderr.on("data", (data) => {
274
+ errorOutput += data.toString();
275
+ });
276
+
277
+ claude.on("close", (code) => {
278
+ if (code !== 0) {
279
+ console.error(`❌ Claude CLI error: ${errorOutput}`);
280
+ resolve(null);
281
+ return;
282
+ }
283
+
284
+ const scaffoldData = parseAIResponse(output);
285
+ if (!scaffoldData) {
286
+ console.error("❌ Could not parse Claude response");
287
+ console.log("Raw response:", output.slice(0, 500));
288
+ resolve(null);
289
+ return;
290
+ }
291
+
292
+ if (scaffoldData.explanation) {
293
+ console.log("📝 AI Explanation:");
294
+ console.log(` ${scaffoldData.explanation}`);
295
+ console.log("");
296
+ }
297
+
298
+ resolve(scaffoldData);
299
+ });
300
+
301
+ claude.on("error", (err) => {
302
+ console.error(`❌ Failed to run Claude CLI: ${err.message}`);
303
+ resolve(null);
304
+ });
305
+ });
306
+ }
307
+
308
+ /**
309
+ * Generate scaffold using Codex CLI
310
+ * @param {string} userPrompt - User's description
311
+ * @param {string} projectName - Project name
312
+ * @param {string} description - Project description
313
+ * @returns {Promise<object|null>}
314
+ */
315
+ async function generateWithCodex(userPrompt, projectName, description) {
316
+ const fullPrompt = buildFullPrompt(userPrompt, projectName, description);
317
+
318
+ return new Promise((resolve) => {
319
+ console.log("\n🤖 Generating plugin scaffold with Codex...\n");
320
+
321
+ let output = "";
322
+ let errorOutput = "";
323
+
324
+ // Use codex CLI
325
+ const codex = spawn(
326
+ "codex",
327
+ ["--quiet", "-p", `${SCAFFOLD_SYSTEM_PROMPT}\n\n${fullPrompt}`],
328
+ {
329
+ stdio: ["pipe", "pipe", "pipe"],
330
+ shell: true,
331
+ }
332
+ );
333
+
334
+ codex.stdout.on("data", (data) => {
335
+ output += data.toString();
336
+ });
337
+
338
+ codex.stderr.on("data", (data) => {
339
+ errorOutput += data.toString();
340
+ });
341
+
342
+ codex.on("close", (code) => {
343
+ if (code !== 0) {
344
+ console.error(`❌ Codex CLI error: ${errorOutput}`);
345
+ resolve(null);
346
+ return;
347
+ }
348
+
349
+ const scaffoldData = parseAIResponse(output);
350
+ if (!scaffoldData) {
351
+ console.error("❌ Could not parse Codex response");
352
+ console.log("Raw response:", output.slice(0, 500));
353
+ resolve(null);
354
+ return;
355
+ }
356
+
357
+ if (scaffoldData.explanation) {
358
+ console.log("📝 AI Explanation:");
359
+ console.log(` ${scaffoldData.explanation}`);
360
+ console.log("");
361
+ }
362
+
363
+ resolve(scaffoldData);
364
+ });
365
+
366
+ codex.on("error", (err) => {
367
+ console.error(`❌ Failed to run Codex CLI: ${err.message}`);
368
+ resolve(null);
369
+ });
370
+ });
371
+ }
372
+
373
+ /**
374
+ * Generate scaffold using Gemini (API key or gcloud)
375
+ * @param {string} userPrompt - User's description
376
+ * @param {string} projectName - Project name
377
+ * @param {string} description - Project description
378
+ * @param {string} method - 'api_key' or 'gcloud'
379
+ * @returns {Promise<object|null>}
380
+ */
381
+ async function generateWithGemini(
382
+ userPrompt,
383
+ projectName,
384
+ description,
385
+ method
386
+ ) {
387
+ const fullPrompt = buildFullPrompt(userPrompt, projectName, description);
388
+
389
+ // Determine authentication method
390
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
391
+ const useApiKey = method === "api_key" || apiKey;
392
+
393
+ if (useApiKey && apiKey) {
394
+ return generateWithGeminiApiKey(fullPrompt, apiKey);
395
+ } else {
396
+ return generateWithGeminiGcloud(fullPrompt);
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Generate using Gemini API with API key
402
+ */
403
+ async function generateWithGeminiApiKey(fullPrompt, apiKey) {
404
+ try {
405
+ console.log("\n🤖 Generating plugin scaffold with Gemini API...\n");
406
+
407
+ const response = await fetch(
408
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
409
+ {
410
+ method: "POST",
411
+ headers: {
412
+ "Content-Type": "application/json",
413
+ },
414
+ body: JSON.stringify({
415
+ contents: [
416
+ {
417
+ role: "user",
418
+ parts: [{ text: SCAFFOLD_SYSTEM_PROMPT }, { text: fullPrompt }],
419
+ },
420
+ ],
421
+ generationConfig: {
422
+ maxOutputTokens: 8192,
423
+ temperature: 0.7,
424
+ },
425
+ }),
426
+ }
427
+ );
428
+
429
+ if (!response.ok) {
430
+ const errorText = await response.text();
431
+ console.error(`❌ Gemini API error: ${response.status}`);
432
+ console.error(errorText);
433
+ return null;
434
+ }
435
+
436
+ const data = await response.json();
437
+ const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
438
+
439
+ if (!responseText) {
440
+ console.error("❌ No response from Gemini API");
441
+ return null;
442
+ }
443
+
444
+ const scaffoldData = parseAIResponse(responseText);
445
+
446
+ if (!scaffoldData) {
447
+ console.error("❌ Could not parse AI response");
448
+ console.log("Raw response:", responseText.slice(0, 500));
449
+ return null;
450
+ }
451
+
452
+ if (scaffoldData.explanation) {
453
+ console.log("📝 AI Explanation:");
454
+ console.log(` ${scaffoldData.explanation}`);
455
+ console.log("");
456
+ }
457
+
458
+ return scaffoldData;
459
+ } catch (error) {
460
+ console.error(`❌ Gemini API failed: ${error.message}`);
461
+ return null;
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Generate using Gemini via gcloud CLI
467
+ */
468
+ async function generateWithGeminiGcloud(fullPrompt) {
469
+ return new Promise((resolve) => {
470
+ console.log(
471
+ "\n🤖 Generating plugin scaffold with Gemini (via gcloud)...\n"
472
+ );
473
+
474
+ // Get access token from gcloud
475
+ let accessToken;
476
+ try {
477
+ accessToken = execSync("gcloud auth print-access-token", {
478
+ stdio: "pipe",
479
+ })
480
+ .toString()
481
+ .trim();
482
+ } catch (error) {
483
+ console.error("❌ Failed to get gcloud access token");
484
+ resolve(null);
485
+ return;
486
+ }
487
+
488
+ // Get project ID
489
+ let projectId;
490
+ try {
491
+ projectId = execSync("gcloud config get-value project", {
492
+ stdio: "pipe",
493
+ })
494
+ .toString()
495
+ .trim();
496
+ } catch (error) {
497
+ console.error("❌ Failed to get gcloud project ID");
498
+ resolve(null);
499
+ return;
500
+ }
501
+
502
+ const requestBody = JSON.stringify({
503
+ contents: [
504
+ {
505
+ role: "user",
506
+ parts: [{ text: SCAFFOLD_SYSTEM_PROMPT }, { text: fullPrompt }],
507
+ },
508
+ ],
509
+ generationConfig: {
510
+ maxOutputTokens: 8192,
511
+ temperature: 0.7,
512
+ },
513
+ });
514
+
515
+ // Use curl to make the request (more reliable than fetch with gcloud auth)
516
+ const curl = spawn(
517
+ "curl",
518
+ [
519
+ "-s",
520
+ "-X",
521
+ "POST",
522
+ `https://us-central1-aiplatform.googleapis.com/v1/projects/${projectId}/locations/us-central1/publishers/google/models/gemini-1.5-flash:generateContent`,
523
+ "-H",
524
+ `Authorization: Bearer ${accessToken}`,
525
+ "-H",
526
+ "Content-Type: application/json",
527
+ "-d",
528
+ requestBody,
529
+ ],
530
+ { stdio: ["pipe", "pipe", "pipe"] }
531
+ );
532
+
533
+ let output = "";
534
+ let errorOutput = "";
535
+
536
+ curl.stdout.on("data", (data) => {
537
+ output += data.toString();
538
+ });
539
+
540
+ curl.stderr.on("data", (data) => {
541
+ errorOutput += data.toString();
542
+ });
543
+
544
+ curl.on("close", (code) => {
545
+ if (code !== 0) {
546
+ console.error(`❌ Gemini gcloud error: ${errorOutput}`);
547
+ resolve(null);
548
+ return;
549
+ }
550
+
551
+ try {
552
+ const data = JSON.parse(output);
553
+ const responseText =
554
+ data.candidates?.[0]?.content?.parts?.[0]?.text || "";
555
+
556
+ if (!responseText) {
557
+ console.error("❌ No response from Gemini");
558
+ resolve(null);
559
+ return;
560
+ }
561
+
562
+ const scaffoldData = parseAIResponse(responseText);
563
+
564
+ if (!scaffoldData) {
565
+ console.error("❌ Could not parse AI response");
566
+ console.log("Raw response:", responseText.slice(0, 500));
567
+ resolve(null);
568
+ return;
569
+ }
570
+
571
+ if (scaffoldData.explanation) {
572
+ console.log("📝 AI Explanation:");
573
+ console.log(` ${scaffoldData.explanation}`);
574
+ console.log("");
575
+ }
576
+
577
+ resolve(scaffoldData);
578
+ } catch (parseError) {
579
+ console.error(
580
+ `❌ Failed to parse Gemini response: ${parseError.message}`
581
+ );
582
+ resolve(null);
583
+ }
584
+ });
585
+ });
586
+ }
587
+
588
+ /**
589
+ * Build the full prompt for the AI
590
+ */
591
+ function buildFullPrompt(userPrompt, projectName, description) {
592
+ return `
593
+ Project Name: ${projectName}
594
+ Project Description: ${description || "A GxP kiosk plugin"}
595
+
596
+ User Request:
597
+ ${userPrompt}
598
+
599
+ Please generate the necessary Vue components and manifest updates for this plugin. Follow the GxP plugin architecture guidelines. Return ONLY a valid JSON object with components, manifest, and explanation fields.
600
+ `;
601
+ }
602
+
603
+ /**
604
+ * Parse AI response to extract structured data
605
+ * @param {string} response - Raw AI response text
606
+ * @returns {object|null} Parsed scaffold data or null if parsing fails
607
+ */
608
+ function parseAIResponse(response) {
609
+ try {
610
+ // Try to find JSON in the response
611
+ const jsonMatch = response.match(/```json\n?([\s\S]*?)\n?```/);
612
+ if (jsonMatch) {
613
+ return JSON.parse(jsonMatch[1]);
614
+ }
615
+
616
+ // Try parsing the entire response as JSON
617
+ return JSON.parse(response);
618
+ } catch (error) {
619
+ // Try to extract JSON object from response
620
+ const jsonStart = response.indexOf("{");
621
+ const jsonEnd = response.lastIndexOf("}");
622
+ if (jsonStart !== -1 && jsonEnd !== -1) {
623
+ try {
624
+ return JSON.parse(response.slice(jsonStart, jsonEnd + 1));
625
+ } catch {
626
+ // Parsing failed
627
+ }
628
+ }
629
+ }
630
+ return null;
631
+ }
632
+
633
+ /**
634
+ * Apply scaffold data to project
635
+ * @param {string} projectPath - Path to project directory
636
+ * @param {object} scaffoldData - Parsed scaffold data from AI
637
+ * @returns {object} Result with created files and updates
638
+ */
639
+ function applyScaffold(projectPath, scaffoldData) {
640
+ const result = {
641
+ filesCreated: [],
642
+ manifestUpdated: false,
643
+ errors: [],
644
+ };
645
+
646
+ // Create component files
647
+ if (scaffoldData.components && Array.isArray(scaffoldData.components)) {
648
+ for (const component of scaffoldData.components) {
649
+ if (component.path && component.content) {
650
+ try {
651
+ const filePath = path.join(projectPath, component.path);
652
+ const dirPath = path.dirname(filePath);
653
+
654
+ // Create directory if needed
655
+ if (!fs.existsSync(dirPath)) {
656
+ fs.mkdirSync(dirPath, { recursive: true });
657
+ }
658
+
659
+ // Write file
660
+ fs.writeFileSync(filePath, component.content);
661
+ result.filesCreated.push(component.path);
662
+ console.log(`✓ Created ${component.path}`);
663
+ } catch (error) {
664
+ result.errors.push(
665
+ `Failed to create ${component.path}: ${error.message}`
666
+ );
667
+ console.error(
668
+ `✗ Failed to create ${component.path}: ${error.message}`
669
+ );
670
+ }
671
+ }
672
+ }
673
+ }
674
+
675
+ // Update manifest
676
+ if (scaffoldData.manifest) {
677
+ try {
678
+ const manifestPath = path.join(projectPath, "app-manifest.json");
679
+ let manifest = {};
680
+
681
+ if (fs.existsSync(manifestPath)) {
682
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
683
+ }
684
+
685
+ // Merge strings
686
+ if (scaffoldData.manifest.strings) {
687
+ manifest.strings = manifest.strings || {};
688
+ manifest.strings.default = manifest.strings.default || {};
689
+ Object.assign(
690
+ manifest.strings.default,
691
+ scaffoldData.manifest.strings.default || scaffoldData.manifest.strings
692
+ );
693
+ }
694
+
695
+ // Merge assets
696
+ if (scaffoldData.manifest.assets) {
697
+ manifest.assets = manifest.assets || {};
698
+ Object.assign(manifest.assets, scaffoldData.manifest.assets);
699
+ }
700
+
701
+ // Merge settings
702
+ if (scaffoldData.manifest.settings) {
703
+ manifest.settings = manifest.settings || {};
704
+ Object.assign(manifest.settings, scaffoldData.manifest.settings);
705
+ }
706
+
707
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, "\t"));
708
+ result.manifestUpdated = true;
709
+ console.log("✓ Updated app-manifest.json");
710
+ } catch (error) {
711
+ result.errors.push(`Failed to update manifest: ${error.message}`);
712
+ console.error(`✗ Failed to update manifest: ${error.message}`);
713
+ }
714
+ }
715
+
716
+ return result;
717
+ }
718
+
719
+ /**
720
+ * Generate scaffold using specified provider
721
+ * @param {string} provider - Provider ID (claude, codex, gemini)
722
+ * @param {string} userPrompt - User's description of what to build
723
+ * @param {string} projectName - Name of the project
724
+ * @param {string} description - Project description
725
+ * @returns {Promise<object|null>} Scaffold data or null if generation fails
726
+ */
727
+ async function generateScaffold(
728
+ provider,
729
+ userPrompt,
730
+ projectName,
731
+ description
732
+ ) {
733
+ const providerConfig = AI_PROVIDERS[provider];
734
+ if (!providerConfig) {
735
+ console.error(`❌ Unknown AI provider: ${provider}`);
736
+ return null;
737
+ }
738
+
739
+ const status = await providerConfig.checkAvailable();
740
+ if (!status.available) {
741
+ console.error(`❌ ${providerConfig.name} is not available:`);
742
+ console.error(` ${status.reason}`);
743
+ return null;
744
+ }
745
+
746
+ return providerConfig.generate(
747
+ userPrompt,
748
+ projectName,
749
+ description,
750
+ status.method
751
+ );
752
+ }
753
+
754
+ /**
755
+ * Run AI scaffolding for a project
756
+ * @param {string} projectPath - Path to project directory
757
+ * @param {string} projectName - Name of the project
758
+ * @param {string} description - Project description
759
+ * @param {string} buildPrompt - User's build prompt
760
+ * @param {string} provider - AI provider to use (claude, codex, gemini)
761
+ * @returns {Promise<boolean>} True if scaffolding succeeded
762
+ */
763
+ async function runAIScaffolding(
764
+ projectPath,
765
+ projectName,
766
+ description,
767
+ buildPrompt,
768
+ provider = "gemini"
769
+ ) {
770
+ const scaffoldData = await generateScaffold(
771
+ provider,
772
+ buildPrompt,
773
+ projectName,
774
+ description
775
+ );
776
+
777
+ if (!scaffoldData) {
778
+ return false;
779
+ }
780
+
781
+ console.log("📦 Applying scaffold to project...\n");
782
+ const result = applyScaffold(projectPath, scaffoldData);
783
+
784
+ console.log("");
785
+ if (result.filesCreated.length > 0) {
786
+ console.log(`✅ Created ${result.filesCreated.length} file(s)`);
787
+ }
788
+ if (result.manifestUpdated) {
789
+ console.log("✅ Updated app-manifest.json");
790
+ }
791
+ if (result.errors.length > 0) {
792
+ console.log(`⚠️ ${result.errors.length} error(s) occurred`);
793
+ }
794
+
795
+ return result.errors.length === 0;
796
+ }
797
+
798
+ module.exports = {
799
+ SCAFFOLD_SYSTEM_PROMPT,
800
+ AI_PROVIDERS,
801
+ getAvailableProviders,
802
+ parseAIResponse,
803
+ applyScaffold,
804
+ generateScaffold,
805
+ runAIScaffolding,
806
+ };