@gxp-dev/tools 2.0.11 → 2.0.13

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,877 @@
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 (CLI, API key, or gcloud)
190
+ * @returns {Promise<{available: boolean, reason?: string, method?: string}>}
191
+ */
192
+ async function checkGeminiAvailable() {
193
+ // Check for Gemini CLI first (preferred - uses logged-in account)
194
+ try {
195
+ execSync("which gemini", { stdio: "pipe" });
196
+ // Gemini CLI is installed - it handles its own auth
197
+ return { available: true, method: "cli" };
198
+ } catch (error) {
199
+ // Gemini CLI not installed
200
+ }
201
+
202
+ // Check for API key
203
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
204
+ if (apiKey) {
205
+ return { available: true, method: "api_key" };
206
+ }
207
+
208
+ // Check for gcloud CLI with auth
209
+ try {
210
+ execSync("which gcloud", { stdio: "pipe" });
211
+ const authList = execSync("gcloud auth list --format='value(account)'", {
212
+ stdio: "pipe",
213
+ }).toString();
214
+ if (authList.trim()) {
215
+ return { available: true, method: "gcloud" };
216
+ }
217
+ } catch (error) {
218
+ // gcloud not available or not authenticated
219
+ }
220
+
221
+ return {
222
+ available: false,
223
+ reason:
224
+ "Gemini requires one of:\n" +
225
+ " • Gemini CLI logged in (npm i -g @google/gemini-cli && gemini), or\n" +
226
+ " • GEMINI_API_KEY environment variable, or\n" +
227
+ " • gcloud CLI logged in (gcloud auth login)",
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Get list of available AI providers
233
+ * @returns {Promise<Array<{id: string, name: string, description: string, available: boolean, reason?: string}>>}
234
+ */
235
+ async function getAvailableProviders() {
236
+ const providers = [];
237
+
238
+ for (const [id, provider] of Object.entries(AI_PROVIDERS)) {
239
+ const status = await provider.checkAvailable();
240
+ providers.push({
241
+ id,
242
+ name: provider.name,
243
+ description: provider.description,
244
+ available: status.available,
245
+ reason: status.reason,
246
+ method: status.method,
247
+ });
248
+ }
249
+
250
+ return providers;
251
+ }
252
+
253
+ /**
254
+ * Generate scaffold using Claude CLI
255
+ * @param {string} userPrompt - User's description
256
+ * @param {string} projectName - Project name
257
+ * @param {string} description - Project description
258
+ * @returns {Promise<object|null>}
259
+ */
260
+ async function generateWithClaude(userPrompt, projectName, description) {
261
+ const fullPrompt = buildFullPrompt(userPrompt, projectName, description);
262
+
263
+ return new Promise((resolve) => {
264
+ console.log("\n🤖 Generating plugin scaffold with Claude...\n");
265
+
266
+ let output = "";
267
+ let errorOutput = "";
268
+
269
+ // Use claude CLI with --print flag to get direct output
270
+ const claude = spawn(
271
+ "claude",
272
+ ["--print", "-p", `${SCAFFOLD_SYSTEM_PROMPT}\n\n${fullPrompt}`],
273
+ {
274
+ stdio: ["pipe", "pipe", "pipe"],
275
+ shell: true,
276
+ }
277
+ );
278
+
279
+ claude.stdout.on("data", (data) => {
280
+ output += data.toString();
281
+ });
282
+
283
+ claude.stderr.on("data", (data) => {
284
+ errorOutput += data.toString();
285
+ });
286
+
287
+ claude.on("close", (code) => {
288
+ if (code !== 0) {
289
+ console.error(`❌ Claude CLI error: ${errorOutput}`);
290
+ resolve(null);
291
+ return;
292
+ }
293
+
294
+ const scaffoldData = parseAIResponse(output);
295
+ if (!scaffoldData) {
296
+ console.error("❌ Could not parse Claude response");
297
+ console.log("Raw response:", output.slice(0, 500));
298
+ resolve(null);
299
+ return;
300
+ }
301
+
302
+ if (scaffoldData.explanation) {
303
+ console.log("📝 AI Explanation:");
304
+ console.log(` ${scaffoldData.explanation}`);
305
+ console.log("");
306
+ }
307
+
308
+ resolve(scaffoldData);
309
+ });
310
+
311
+ claude.on("error", (err) => {
312
+ console.error(`❌ Failed to run Claude CLI: ${err.message}`);
313
+ resolve(null);
314
+ });
315
+ });
316
+ }
317
+
318
+ /**
319
+ * Generate scaffold using Codex CLI
320
+ * @param {string} userPrompt - User's description
321
+ * @param {string} projectName - Project name
322
+ * @param {string} description - Project description
323
+ * @returns {Promise<object|null>}
324
+ */
325
+ async function generateWithCodex(userPrompt, projectName, description) {
326
+ const fullPrompt = buildFullPrompt(userPrompt, projectName, description);
327
+
328
+ return new Promise((resolve) => {
329
+ console.log("\n🤖 Generating plugin scaffold with Codex...\n");
330
+
331
+ let output = "";
332
+ let errorOutput = "";
333
+
334
+ // Use codex CLI
335
+ const codex = spawn(
336
+ "codex",
337
+ ["--quiet", "-p", `${SCAFFOLD_SYSTEM_PROMPT}\n\n${fullPrompt}`],
338
+ {
339
+ stdio: ["pipe", "pipe", "pipe"],
340
+ shell: true,
341
+ }
342
+ );
343
+
344
+ codex.stdout.on("data", (data) => {
345
+ output += data.toString();
346
+ });
347
+
348
+ codex.stderr.on("data", (data) => {
349
+ errorOutput += data.toString();
350
+ });
351
+
352
+ codex.on("close", (code) => {
353
+ if (code !== 0) {
354
+ console.error(`❌ Codex CLI error: ${errorOutput}`);
355
+ resolve(null);
356
+ return;
357
+ }
358
+
359
+ const scaffoldData = parseAIResponse(output);
360
+ if (!scaffoldData) {
361
+ console.error("❌ Could not parse Codex response");
362
+ console.log("Raw response:", output.slice(0, 500));
363
+ resolve(null);
364
+ return;
365
+ }
366
+
367
+ if (scaffoldData.explanation) {
368
+ console.log("📝 AI Explanation:");
369
+ console.log(` ${scaffoldData.explanation}`);
370
+ console.log("");
371
+ }
372
+
373
+ resolve(scaffoldData);
374
+ });
375
+
376
+ codex.on("error", (err) => {
377
+ console.error(`❌ Failed to run Codex CLI: ${err.message}`);
378
+ resolve(null);
379
+ });
380
+ });
381
+ }
382
+
383
+ /**
384
+ * Generate scaffold using Gemini (CLI, API key, or gcloud)
385
+ * @param {string} userPrompt - User's description
386
+ * @param {string} projectName - Project name
387
+ * @param {string} description - Project description
388
+ * @param {string} method - 'cli', 'api_key', or 'gcloud'
389
+ * @returns {Promise<object|null>}
390
+ */
391
+ async function generateWithGemini(
392
+ userPrompt,
393
+ projectName,
394
+ description,
395
+ method
396
+ ) {
397
+ const fullPrompt = buildFullPrompt(userPrompt, projectName, description);
398
+
399
+ // Use the appropriate method
400
+ if (method === "cli") {
401
+ return generateWithGeminiCli(fullPrompt);
402
+ }
403
+
404
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
405
+ if (method === "api_key" && apiKey) {
406
+ return generateWithGeminiApiKey(fullPrompt, apiKey);
407
+ }
408
+
409
+ return generateWithGeminiGcloud(fullPrompt);
410
+ }
411
+
412
+ /**
413
+ * Generate using Gemini CLI
414
+ */
415
+ async function generateWithGeminiCli(fullPrompt) {
416
+ return new Promise((resolve) => {
417
+ console.log("\n🤖 Generating plugin scaffold with Gemini CLI...\n");
418
+
419
+ let output = "";
420
+ let errorOutput = "";
421
+
422
+ // Use gemini CLI with -p for prompt mode
423
+ const gemini = spawn(
424
+ "gemini",
425
+ ["-p", `${SCAFFOLD_SYSTEM_PROMPT}\n\n${fullPrompt}`],
426
+ {
427
+ stdio: ["pipe", "pipe", "pipe"],
428
+ shell: true,
429
+ }
430
+ );
431
+
432
+ gemini.stdout.on("data", (data) => {
433
+ output += data.toString();
434
+ });
435
+
436
+ gemini.stderr.on("data", (data) => {
437
+ errorOutput += data.toString();
438
+ });
439
+
440
+ gemini.on("close", (code) => {
441
+ if (code !== 0) {
442
+ console.error(`❌ Gemini CLI error: ${errorOutput}`);
443
+ resolve(null);
444
+ return;
445
+ }
446
+
447
+ const scaffoldData = parseAIResponse(output);
448
+ if (!scaffoldData) {
449
+ console.error("❌ Could not parse Gemini response");
450
+ console.log("Raw response:", output.slice(0, 500));
451
+ resolve(null);
452
+ return;
453
+ }
454
+
455
+ if (scaffoldData.explanation) {
456
+ console.log("📝 AI Explanation:");
457
+ console.log(` ${scaffoldData.explanation}`);
458
+ console.log("");
459
+ }
460
+
461
+ resolve(scaffoldData);
462
+ });
463
+
464
+ gemini.on("error", (err) => {
465
+ console.error(`❌ Failed to run Gemini CLI: ${err.message}`);
466
+ resolve(null);
467
+ });
468
+ });
469
+ }
470
+
471
+ /**
472
+ * Generate using Gemini API with API key
473
+ */
474
+ async function generateWithGeminiApiKey(fullPrompt, apiKey) {
475
+ try {
476
+ console.log("\n🤖 Generating plugin scaffold with Gemini API...\n");
477
+
478
+ const response = await fetch(
479
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
480
+ {
481
+ method: "POST",
482
+ headers: {
483
+ "Content-Type": "application/json",
484
+ },
485
+ body: JSON.stringify({
486
+ contents: [
487
+ {
488
+ role: "user",
489
+ parts: [{ text: SCAFFOLD_SYSTEM_PROMPT }, { text: fullPrompt }],
490
+ },
491
+ ],
492
+ generationConfig: {
493
+ maxOutputTokens: 8192,
494
+ temperature: 0.7,
495
+ },
496
+ }),
497
+ }
498
+ );
499
+
500
+ if (!response.ok) {
501
+ const errorText = await response.text();
502
+ console.error(`❌ Gemini API error: ${response.status}`);
503
+ console.error(errorText);
504
+ return null;
505
+ }
506
+
507
+ const data = await response.json();
508
+ const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
509
+
510
+ if (!responseText) {
511
+ console.error("❌ No response from Gemini API");
512
+ return null;
513
+ }
514
+
515
+ const scaffoldData = parseAIResponse(responseText);
516
+
517
+ if (!scaffoldData) {
518
+ console.error("❌ Could not parse AI response");
519
+ console.log("Raw response:", responseText.slice(0, 500));
520
+ return null;
521
+ }
522
+
523
+ if (scaffoldData.explanation) {
524
+ console.log("📝 AI Explanation:");
525
+ console.log(` ${scaffoldData.explanation}`);
526
+ console.log("");
527
+ }
528
+
529
+ return scaffoldData;
530
+ } catch (error) {
531
+ console.error(`❌ Gemini API failed: ${error.message}`);
532
+ return null;
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Generate using Gemini via gcloud CLI
538
+ */
539
+ async function generateWithGeminiGcloud(fullPrompt) {
540
+ return new Promise((resolve) => {
541
+ console.log(
542
+ "\n🤖 Generating plugin scaffold with Gemini (via gcloud)...\n"
543
+ );
544
+
545
+ // Get access token from gcloud
546
+ let accessToken;
547
+ try {
548
+ accessToken = execSync("gcloud auth print-access-token", {
549
+ stdio: "pipe",
550
+ })
551
+ .toString()
552
+ .trim();
553
+ } catch (error) {
554
+ console.error("❌ Failed to get gcloud access token");
555
+ resolve(null);
556
+ return;
557
+ }
558
+
559
+ // Get project ID
560
+ let projectId;
561
+ try {
562
+ projectId = execSync("gcloud config get-value project", {
563
+ stdio: "pipe",
564
+ })
565
+ .toString()
566
+ .trim();
567
+ } catch (error) {
568
+ console.error("❌ Failed to get gcloud project ID");
569
+ resolve(null);
570
+ return;
571
+ }
572
+
573
+ const requestBody = JSON.stringify({
574
+ contents: [
575
+ {
576
+ role: "user",
577
+ parts: [{ text: SCAFFOLD_SYSTEM_PROMPT }, { text: fullPrompt }],
578
+ },
579
+ ],
580
+ generationConfig: {
581
+ maxOutputTokens: 8192,
582
+ temperature: 0.7,
583
+ },
584
+ });
585
+
586
+ // Use curl to make the request (more reliable than fetch with gcloud auth)
587
+ const curl = spawn(
588
+ "curl",
589
+ [
590
+ "-s",
591
+ "-X",
592
+ "POST",
593
+ `https://us-central1-aiplatform.googleapis.com/v1/projects/${projectId}/locations/us-central1/publishers/google/models/gemini-1.5-flash:generateContent`,
594
+ "-H",
595
+ `Authorization: Bearer ${accessToken}`,
596
+ "-H",
597
+ "Content-Type: application/json",
598
+ "-d",
599
+ requestBody,
600
+ ],
601
+ { stdio: ["pipe", "pipe", "pipe"] }
602
+ );
603
+
604
+ let output = "";
605
+ let errorOutput = "";
606
+
607
+ curl.stdout.on("data", (data) => {
608
+ output += data.toString();
609
+ });
610
+
611
+ curl.stderr.on("data", (data) => {
612
+ errorOutput += data.toString();
613
+ });
614
+
615
+ curl.on("close", (code) => {
616
+ if (code !== 0) {
617
+ console.error(`❌ Gemini gcloud error: ${errorOutput}`);
618
+ resolve(null);
619
+ return;
620
+ }
621
+
622
+ try {
623
+ const data = JSON.parse(output);
624
+ const responseText =
625
+ data.candidates?.[0]?.content?.parts?.[0]?.text || "";
626
+
627
+ if (!responseText) {
628
+ console.error("❌ No response from Gemini");
629
+ resolve(null);
630
+ return;
631
+ }
632
+
633
+ const scaffoldData = parseAIResponse(responseText);
634
+
635
+ if (!scaffoldData) {
636
+ console.error("❌ Could not parse AI response");
637
+ console.log("Raw response:", responseText.slice(0, 500));
638
+ resolve(null);
639
+ return;
640
+ }
641
+
642
+ if (scaffoldData.explanation) {
643
+ console.log("📝 AI Explanation:");
644
+ console.log(` ${scaffoldData.explanation}`);
645
+ console.log("");
646
+ }
647
+
648
+ resolve(scaffoldData);
649
+ } catch (parseError) {
650
+ console.error(
651
+ `❌ Failed to parse Gemini response: ${parseError.message}`
652
+ );
653
+ resolve(null);
654
+ }
655
+ });
656
+ });
657
+ }
658
+
659
+ /**
660
+ * Build the full prompt for the AI
661
+ */
662
+ function buildFullPrompt(userPrompt, projectName, description) {
663
+ return `
664
+ Project Name: ${projectName}
665
+ Project Description: ${description || "A GxP kiosk plugin"}
666
+
667
+ User Request:
668
+ ${userPrompt}
669
+
670
+ 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.
671
+ `;
672
+ }
673
+
674
+ /**
675
+ * Parse AI response to extract structured data
676
+ * @param {string} response - Raw AI response text
677
+ * @returns {object|null} Parsed scaffold data or null if parsing fails
678
+ */
679
+ function parseAIResponse(response) {
680
+ try {
681
+ // Try to find JSON in the response
682
+ const jsonMatch = response.match(/```json\n?([\s\S]*?)\n?```/);
683
+ if (jsonMatch) {
684
+ return JSON.parse(jsonMatch[1]);
685
+ }
686
+
687
+ // Try parsing the entire response as JSON
688
+ return JSON.parse(response);
689
+ } catch (error) {
690
+ // Try to extract JSON object from response
691
+ const jsonStart = response.indexOf("{");
692
+ const jsonEnd = response.lastIndexOf("}");
693
+ if (jsonStart !== -1 && jsonEnd !== -1) {
694
+ try {
695
+ return JSON.parse(response.slice(jsonStart, jsonEnd + 1));
696
+ } catch {
697
+ // Parsing failed
698
+ }
699
+ }
700
+ }
701
+ return null;
702
+ }
703
+
704
+ /**
705
+ * Apply scaffold data to project
706
+ * @param {string} projectPath - Path to project directory
707
+ * @param {object} scaffoldData - Parsed scaffold data from AI
708
+ * @returns {object} Result with created files and updates
709
+ */
710
+ function applyScaffold(projectPath, scaffoldData) {
711
+ const result = {
712
+ filesCreated: [],
713
+ manifestUpdated: false,
714
+ errors: [],
715
+ };
716
+
717
+ // Create component files
718
+ if (scaffoldData.components && Array.isArray(scaffoldData.components)) {
719
+ for (const component of scaffoldData.components) {
720
+ if (component.path && component.content) {
721
+ try {
722
+ const filePath = path.join(projectPath, component.path);
723
+ const dirPath = path.dirname(filePath);
724
+
725
+ // Create directory if needed
726
+ if (!fs.existsSync(dirPath)) {
727
+ fs.mkdirSync(dirPath, { recursive: true });
728
+ }
729
+
730
+ // Write file
731
+ fs.writeFileSync(filePath, component.content);
732
+ result.filesCreated.push(component.path);
733
+ console.log(`✓ Created ${component.path}`);
734
+ } catch (error) {
735
+ result.errors.push(
736
+ `Failed to create ${component.path}: ${error.message}`
737
+ );
738
+ console.error(
739
+ `✗ Failed to create ${component.path}: ${error.message}`
740
+ );
741
+ }
742
+ }
743
+ }
744
+ }
745
+
746
+ // Update manifest
747
+ if (scaffoldData.manifest) {
748
+ try {
749
+ const manifestPath = path.join(projectPath, "app-manifest.json");
750
+ let manifest = {};
751
+
752
+ if (fs.existsSync(manifestPath)) {
753
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
754
+ }
755
+
756
+ // Merge strings
757
+ if (scaffoldData.manifest.strings) {
758
+ manifest.strings = manifest.strings || {};
759
+ manifest.strings.default = manifest.strings.default || {};
760
+ Object.assign(
761
+ manifest.strings.default,
762
+ scaffoldData.manifest.strings.default || scaffoldData.manifest.strings
763
+ );
764
+ }
765
+
766
+ // Merge assets
767
+ if (scaffoldData.manifest.assets) {
768
+ manifest.assets = manifest.assets || {};
769
+ Object.assign(manifest.assets, scaffoldData.manifest.assets);
770
+ }
771
+
772
+ // Merge settings
773
+ if (scaffoldData.manifest.settings) {
774
+ manifest.settings = manifest.settings || {};
775
+ Object.assign(manifest.settings, scaffoldData.manifest.settings);
776
+ }
777
+
778
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, "\t"));
779
+ result.manifestUpdated = true;
780
+ console.log("✓ Updated app-manifest.json");
781
+ } catch (error) {
782
+ result.errors.push(`Failed to update manifest: ${error.message}`);
783
+ console.error(`✗ Failed to update manifest: ${error.message}`);
784
+ }
785
+ }
786
+
787
+ return result;
788
+ }
789
+
790
+ /**
791
+ * Generate scaffold using specified provider
792
+ * @param {string} provider - Provider ID (claude, codex, gemini)
793
+ * @param {string} userPrompt - User's description of what to build
794
+ * @param {string} projectName - Name of the project
795
+ * @param {string} description - Project description
796
+ * @returns {Promise<object|null>} Scaffold data or null if generation fails
797
+ */
798
+ async function generateScaffold(
799
+ provider,
800
+ userPrompt,
801
+ projectName,
802
+ description
803
+ ) {
804
+ const providerConfig = AI_PROVIDERS[provider];
805
+ if (!providerConfig) {
806
+ console.error(`❌ Unknown AI provider: ${provider}`);
807
+ return null;
808
+ }
809
+
810
+ const status = await providerConfig.checkAvailable();
811
+ if (!status.available) {
812
+ console.error(`❌ ${providerConfig.name} is not available:`);
813
+ console.error(` ${status.reason}`);
814
+ return null;
815
+ }
816
+
817
+ return providerConfig.generate(
818
+ userPrompt,
819
+ projectName,
820
+ description,
821
+ status.method
822
+ );
823
+ }
824
+
825
+ /**
826
+ * Run AI scaffolding for a project
827
+ * @param {string} projectPath - Path to project directory
828
+ * @param {string} projectName - Name of the project
829
+ * @param {string} description - Project description
830
+ * @param {string} buildPrompt - User's build prompt
831
+ * @param {string} provider - AI provider to use (claude, codex, gemini)
832
+ * @returns {Promise<boolean>} True if scaffolding succeeded
833
+ */
834
+ async function runAIScaffolding(
835
+ projectPath,
836
+ projectName,
837
+ description,
838
+ buildPrompt,
839
+ provider = "gemini"
840
+ ) {
841
+ const scaffoldData = await generateScaffold(
842
+ provider,
843
+ buildPrompt,
844
+ projectName,
845
+ description
846
+ );
847
+
848
+ if (!scaffoldData) {
849
+ return false;
850
+ }
851
+
852
+ console.log("📦 Applying scaffold to project...\n");
853
+ const result = applyScaffold(projectPath, scaffoldData);
854
+
855
+ console.log("");
856
+ if (result.filesCreated.length > 0) {
857
+ console.log(`✅ Created ${result.filesCreated.length} file(s)`);
858
+ }
859
+ if (result.manifestUpdated) {
860
+ console.log("✅ Updated app-manifest.json");
861
+ }
862
+ if (result.errors.length > 0) {
863
+ console.log(`⚠️ ${result.errors.length} error(s) occurred`);
864
+ }
865
+
866
+ return result.errors.length === 0;
867
+ }
868
+
869
+ module.exports = {
870
+ SCAFFOLD_SYSTEM_PROMPT,
871
+ AI_PROVIDERS,
872
+ getAvailableProviders,
873
+ parseAIResponse,
874
+ applyScaffold,
875
+ generateScaffold,
876
+ runAIScaffolding,
877
+ };