@gxp-dev/tools 2.0.10 → 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.
- package/bin/lib/cli.js +42 -0
- package/bin/lib/commands/extract-config.js +186 -0
- package/bin/lib/commands/index.js +2 -0
- package/bin/lib/commands/init.js +448 -180
- package/bin/lib/tui/App.tsx +107 -0
- package/bin/lib/utils/ai-scaffold.js +806 -0
- package/bin/lib/utils/extract-config.js +468 -0
- package/bin/lib/utils/files.js +43 -2
- package/bin/lib/utils/index.js +4 -0
- package/bin/lib/utils/prompts.js +352 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +84 -0
- package/dist/tui/App.js.map +1 -1
- package/mcp/gxp-api-server.js +524 -0
- package/package.json +4 -2
- package/runtime/stores/gxpPortalConfigStore.js +9 -0
- package/template/.claude/agents/gxp-developer.md +335 -0
- package/template/.claude/settings.json +9 -0
- package/template/AGENTS.md +125 -0
- package/template/GEMINI.md +80 -0
- package/template/app-manifest.json +1 -0
|
@@ -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
|
+
};
|