@gxp-dev/tools 2.0.15 → 2.0.17

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,509 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawn, execSync } from 'child_process';
4
+ import { EventEmitter } from 'events';
5
+
6
+ // AI Provider types
7
+ export type AIProvider = 'claude' | 'codex' | 'gemini';
8
+
9
+ export interface AIProviderInfo {
10
+ id: AIProvider;
11
+ name: string;
12
+ available: boolean;
13
+ method?: string; // For gemini: 'cli', 'api_key', 'gcloud'
14
+ reason?: string;
15
+ }
16
+
17
+ // AI Configuration
18
+ export interface AIConfig {
19
+ provider: AIProvider;
20
+ systemPrompt: string;
21
+ projectContext: boolean;
22
+ maxContextTokens: number;
23
+ }
24
+
25
+ // Get the gxdev config directory
26
+ function getConfigDir(): string {
27
+ const home = process.env.HOME || process.env.USERPROFILE || '';
28
+ return path.join(home, '.gxdev');
29
+ }
30
+
31
+ // Ensure config directory exists
32
+ function ensureConfigDir(): void {
33
+ const configDir = getConfigDir();
34
+ if (!fs.existsSync(configDir)) {
35
+ fs.mkdirSync(configDir, { recursive: true });
36
+ }
37
+ }
38
+
39
+ // Get AI config file path
40
+ function getAIConfigPath(): string {
41
+ return path.join(getConfigDir(), 'ai-config.json');
42
+ }
43
+
44
+ // Load AI config
45
+ export function loadAIConfig(): AIConfig {
46
+ try {
47
+ const configPath = getAIConfigPath();
48
+ if (fs.existsSync(configPath)) {
49
+ const content = fs.readFileSync(configPath, 'utf-8');
50
+ return JSON.parse(content);
51
+ }
52
+ } catch {
53
+ // Invalid or missing config file
54
+ }
55
+ return {
56
+ provider: 'claude', // Default to Claude
57
+ systemPrompt: 'You are a helpful assistant for GxP plugin development. Help the user build Vue.js components for the GxP kiosk platform.',
58
+ projectContext: true,
59
+ maxContextTokens: 4000,
60
+ };
61
+ }
62
+
63
+ // Save AI config
64
+ export function saveAIConfig(config: AIConfig): void {
65
+ ensureConfigDir();
66
+ const configPath = getAIConfigPath();
67
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
68
+ }
69
+
70
+ // Check if a command exists
71
+ function commandExists(cmd: string): boolean {
72
+ try {
73
+ execSync(`which ${cmd}`, { stdio: 'pipe' });
74
+ return true;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ // Check available AI providers
81
+ export function getAvailableProviders(): AIProviderInfo[] {
82
+ const providers: AIProviderInfo[] = [];
83
+
84
+ // Check Claude CLI
85
+ if (commandExists('claude')) {
86
+ providers.push({
87
+ id: 'claude',
88
+ name: 'Claude',
89
+ available: true,
90
+ });
91
+ } else {
92
+ providers.push({
93
+ id: 'claude',
94
+ name: 'Claude',
95
+ available: false,
96
+ reason: 'Install: npm i -g @anthropic-ai/claude-code && claude login',
97
+ });
98
+ }
99
+
100
+ // Check Codex CLI
101
+ if (commandExists('codex')) {
102
+ providers.push({
103
+ id: 'codex',
104
+ name: 'Codex',
105
+ available: true,
106
+ });
107
+ } else {
108
+ providers.push({
109
+ id: 'codex',
110
+ name: 'Codex',
111
+ available: false,
112
+ reason: 'Install: npm i -g @openai/codex && codex auth',
113
+ });
114
+ }
115
+
116
+ // Check Gemini (CLI, API key, or gcloud)
117
+ if (commandExists('gemini')) {
118
+ providers.push({
119
+ id: 'gemini',
120
+ name: 'Gemini',
121
+ available: true,
122
+ method: 'cli',
123
+ });
124
+ } else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
125
+ providers.push({
126
+ id: 'gemini',
127
+ name: 'Gemini',
128
+ available: true,
129
+ method: 'api_key',
130
+ });
131
+ } else if (commandExists('gcloud')) {
132
+ try {
133
+ const authList = execSync("gcloud auth list --format='value(account)'", { stdio: 'pipe' }).toString();
134
+ if (authList.trim()) {
135
+ providers.push({
136
+ id: 'gemini',
137
+ name: 'Gemini',
138
+ available: true,
139
+ method: 'gcloud',
140
+ });
141
+ }
142
+ } catch {
143
+ providers.push({
144
+ id: 'gemini',
145
+ name: 'Gemini',
146
+ available: false,
147
+ reason: 'Install: npm i -g @google/gemini-cli && gemini',
148
+ });
149
+ }
150
+ } else {
151
+ providers.push({
152
+ id: 'gemini',
153
+ name: 'Gemini',
154
+ available: false,
155
+ reason: 'Install: npm i -g @google/gemini-cli && gemini',
156
+ });
157
+ }
158
+
159
+ return providers;
160
+ }
161
+
162
+ // Get provider display name with status
163
+ export function getProviderStatus(provider: AIProviderInfo): string {
164
+ if (!provider.available) {
165
+ return `${provider.name} (not available)`;
166
+ }
167
+ if (provider.method) {
168
+ switch (provider.method) {
169
+ case 'cli':
170
+ return `${provider.name} (CLI)`;
171
+ case 'api_key':
172
+ return `${provider.name} (API key)`;
173
+ case 'gcloud':
174
+ return `${provider.name} (gcloud)`;
175
+ }
176
+ }
177
+ return `${provider.name} (logged in)`;
178
+ }
179
+
180
+ // AI Service class
181
+ export class AIService extends EventEmitter {
182
+ private conversationHistory: Array<{ role: string; content: string }> = [];
183
+ private projectContext: string = '';
184
+ private currentProvider: AIProvider;
185
+ private geminiMethod?: string;
186
+
187
+ constructor() {
188
+ super();
189
+ const config = loadAIConfig();
190
+ this.currentProvider = config.provider;
191
+
192
+ // Determine gemini method if that's the current provider
193
+ const providers = getAvailableProviders();
194
+ const geminiProvider = providers.find(p => p.id === 'gemini');
195
+ if (geminiProvider?.available) {
196
+ this.geminiMethod = geminiProvider.method;
197
+ }
198
+ }
199
+
200
+ // Get current provider
201
+ getProvider(): AIProvider {
202
+ return this.currentProvider;
203
+ }
204
+
205
+ // Set current provider
206
+ setProvider(provider: AIProvider): { success: boolean; message: string } {
207
+ const providers = getAvailableProviders();
208
+ const providerInfo = providers.find(p => p.id === provider);
209
+
210
+ if (!providerInfo) {
211
+ return { success: false, message: `Unknown provider: ${provider}` };
212
+ }
213
+
214
+ if (!providerInfo.available) {
215
+ return { success: false, message: `${providerInfo.name} is not available. ${providerInfo.reason || ''}` };
216
+ }
217
+
218
+ this.currentProvider = provider;
219
+ if (provider === 'gemini') {
220
+ this.geminiMethod = providerInfo.method;
221
+ }
222
+
223
+ // Save to config
224
+ const config = loadAIConfig();
225
+ config.provider = provider;
226
+ saveAIConfig(config);
227
+
228
+ this.clearConversation();
229
+ return { success: true, message: `Switched to ${providerInfo.name}` };
230
+ }
231
+
232
+ // Check if current provider is available
233
+ isAvailable(): boolean {
234
+ const providers = getAvailableProviders();
235
+ const current = providers.find(p => p.id === this.currentProvider);
236
+ return current?.available || false;
237
+ }
238
+
239
+ // Get provider info
240
+ getProviderInfo(): AIProviderInfo | undefined {
241
+ const providers = getAvailableProviders();
242
+ return providers.find(p => p.id === this.currentProvider);
243
+ }
244
+
245
+ // Load project context
246
+ loadProjectContext(cwd: string): void {
247
+ const files = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', 'README.md', 'package.json', 'app-manifest.json'];
248
+ const contextParts: string[] = [];
249
+
250
+ for (const file of files) {
251
+ const filePath = path.join(cwd, file);
252
+ if (fs.existsSync(filePath)) {
253
+ try {
254
+ const content = fs.readFileSync(filePath, 'utf-8');
255
+ // Limit each file to 2000 chars
256
+ contextParts.push(`=== ${file} ===\n${content.slice(0, 2000)}`);
257
+ } catch {
258
+ // Skip unreadable files
259
+ }
260
+ }
261
+ }
262
+
263
+ this.projectContext = contextParts.join('\n\n');
264
+ }
265
+
266
+ // Send message using current provider
267
+ async sendMessage(message: string): Promise<string> {
268
+ const config = loadAIConfig();
269
+
270
+ // Build context
271
+ let systemContext = config.systemPrompt || '';
272
+ if (config.projectContext && this.projectContext) {
273
+ systemContext += '\n\nProject Context:\n' + this.projectContext;
274
+ }
275
+
276
+ switch (this.currentProvider) {
277
+ case 'claude':
278
+ return this.sendWithClaude(message, systemContext);
279
+ case 'codex':
280
+ return this.sendWithCodex(message, systemContext);
281
+ case 'gemini':
282
+ return this.sendWithGemini(message, systemContext);
283
+ default:
284
+ throw new Error(`Unknown provider: ${this.currentProvider}`);
285
+ }
286
+ }
287
+
288
+ // Send message with Claude CLI
289
+ private async sendWithClaude(message: string, systemContext: string): Promise<string> {
290
+ return new Promise((resolve, reject) => {
291
+ let output = '';
292
+ let errorOutput = '';
293
+
294
+ const fullPrompt = systemContext ? `${systemContext}\n\nUser: ${message}` : message;
295
+
296
+ const claude = spawn('claude', ['--print', '-p', fullPrompt], {
297
+ stdio: ['pipe', 'pipe', 'pipe'],
298
+ shell: true,
299
+ });
300
+
301
+ claude.stdout.on('data', (data) => {
302
+ output += data.toString();
303
+ });
304
+
305
+ claude.stderr.on('data', (data) => {
306
+ errorOutput += data.toString();
307
+ });
308
+
309
+ claude.on('close', (code) => {
310
+ if (code !== 0) {
311
+ reject(new Error(`Claude error: ${errorOutput || 'Unknown error'}`));
312
+ return;
313
+ }
314
+ this.conversationHistory.push({ role: 'user', content: message });
315
+ this.conversationHistory.push({ role: 'assistant', content: output });
316
+ resolve(output.trim());
317
+ });
318
+
319
+ claude.on('error', (err) => {
320
+ reject(new Error(`Failed to run Claude: ${err.message}`));
321
+ });
322
+ });
323
+ }
324
+
325
+ // Send message with Codex CLI
326
+ private async sendWithCodex(message: string, systemContext: string): Promise<string> {
327
+ return new Promise((resolve, reject) => {
328
+ let output = '';
329
+ let errorOutput = '';
330
+
331
+ const fullPrompt = systemContext ? `${systemContext}\n\nUser: ${message}` : message;
332
+
333
+ const codex = spawn('codex', ['--quiet', '-p', fullPrompt], {
334
+ stdio: ['pipe', 'pipe', 'pipe'],
335
+ shell: true,
336
+ });
337
+
338
+ codex.stdout.on('data', (data) => {
339
+ output += data.toString();
340
+ });
341
+
342
+ codex.stderr.on('data', (data) => {
343
+ errorOutput += data.toString();
344
+ });
345
+
346
+ codex.on('close', (code) => {
347
+ if (code !== 0) {
348
+ reject(new Error(`Codex error: ${errorOutput || 'Unknown error'}`));
349
+ return;
350
+ }
351
+ this.conversationHistory.push({ role: 'user', content: message });
352
+ this.conversationHistory.push({ role: 'assistant', content: output });
353
+ resolve(output.trim());
354
+ });
355
+
356
+ codex.on('error', (err) => {
357
+ reject(new Error(`Failed to run Codex: ${err.message}`));
358
+ });
359
+ });
360
+ }
361
+
362
+ // Send message with Gemini
363
+ private async sendWithGemini(message: string, systemContext: string): Promise<string> {
364
+ const fullPrompt = systemContext ? `${systemContext}\n\nUser: ${message}` : message;
365
+
366
+ if (this.geminiMethod === 'cli') {
367
+ return this.sendWithGeminiCli(fullPrompt);
368
+ } else if (this.geminiMethod === 'api_key') {
369
+ return this.sendWithGeminiApi(fullPrompt);
370
+ } else if (this.geminiMethod === 'gcloud') {
371
+ return this.sendWithGeminiGcloud(fullPrompt);
372
+ }
373
+
374
+ throw new Error('Gemini is not properly configured');
375
+ }
376
+
377
+ // Send with Gemini CLI
378
+ private async sendWithGeminiCli(prompt: string): Promise<string> {
379
+ return new Promise((resolve, reject) => {
380
+ let output = '';
381
+ let errorOutput = '';
382
+
383
+ const gemini = spawn('gemini', ['-p', prompt], {
384
+ stdio: ['pipe', 'pipe', 'pipe'],
385
+ shell: true,
386
+ });
387
+
388
+ gemini.stdout.on('data', (data) => {
389
+ output += data.toString();
390
+ });
391
+
392
+ gemini.stderr.on('data', (data) => {
393
+ errorOutput += data.toString();
394
+ });
395
+
396
+ gemini.on('close', (code) => {
397
+ if (code !== 0) {
398
+ reject(new Error(`Gemini error: ${errorOutput || 'Unknown error'}`));
399
+ return;
400
+ }
401
+ this.conversationHistory.push({ role: 'user', content: prompt });
402
+ this.conversationHistory.push({ role: 'assistant', content: output });
403
+ resolve(output.trim());
404
+ });
405
+
406
+ gemini.on('error', (err) => {
407
+ reject(new Error(`Failed to run Gemini: ${err.message}`));
408
+ });
409
+ });
410
+ }
411
+
412
+ // Send with Gemini API
413
+ private async sendWithGeminiApi(prompt: string): Promise<string> {
414
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
415
+ if (!apiKey) {
416
+ throw new Error('GEMINI_API_KEY not set');
417
+ }
418
+
419
+ const response = await fetch(
420
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
421
+ {
422
+ method: 'POST',
423
+ headers: { 'Content-Type': 'application/json' },
424
+ body: JSON.stringify({
425
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
426
+ generationConfig: { maxOutputTokens: 2048, temperature: 0.7 },
427
+ }),
428
+ }
429
+ );
430
+
431
+ if (!response.ok) {
432
+ const errorText = await response.text();
433
+ throw new Error(`Gemini API error: ${response.status} - ${errorText}`);
434
+ }
435
+
436
+ const data = await response.json() as any;
437
+ const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated.';
438
+
439
+ this.conversationHistory.push({ role: 'user', content: prompt });
440
+ this.conversationHistory.push({ role: 'assistant', content: responseText });
441
+
442
+ return responseText;
443
+ }
444
+
445
+ // Send with Gemini via gcloud
446
+ private async sendWithGeminiGcloud(prompt: string): Promise<string> {
447
+ return new Promise((resolve, reject) => {
448
+ let accessToken: string;
449
+ let projectId: string;
450
+
451
+ try {
452
+ accessToken = execSync('gcloud auth print-access-token', { stdio: 'pipe' }).toString().trim();
453
+ projectId = execSync('gcloud config get-value project', { stdio: 'pipe' }).toString().trim();
454
+ } catch (error) {
455
+ reject(new Error('Failed to get gcloud credentials'));
456
+ return;
457
+ }
458
+
459
+ const requestBody = JSON.stringify({
460
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
461
+ generationConfig: { maxOutputTokens: 2048, temperature: 0.7 },
462
+ });
463
+
464
+ const curl = spawn('curl', [
465
+ '-s', '-X', 'POST',
466
+ `https://us-central1-aiplatform.googleapis.com/v1/projects/${projectId}/locations/us-central1/publishers/google/models/gemini-1.5-flash:generateContent`,
467
+ '-H', `Authorization: Bearer ${accessToken}`,
468
+ '-H', 'Content-Type: application/json',
469
+ '-d', requestBody,
470
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
471
+
472
+ let output = '';
473
+ let errorOutput = '';
474
+
475
+ curl.stdout.on('data', (data) => { output += data.toString(); });
476
+ curl.stderr.on('data', (data) => { errorOutput += data.toString(); });
477
+
478
+ curl.on('close', (code) => {
479
+ if (code !== 0) {
480
+ reject(new Error(`Gemini gcloud error: ${errorOutput}`));
481
+ return;
482
+ }
483
+
484
+ try {
485
+ const data = JSON.parse(output);
486
+ const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated.';
487
+ this.conversationHistory.push({ role: 'user', content: prompt });
488
+ this.conversationHistory.push({ role: 'assistant', content: responseText });
489
+ resolve(responseText);
490
+ } catch (parseError) {
491
+ reject(new Error(`Failed to parse Gemini response`));
492
+ }
493
+ });
494
+ });
495
+ }
496
+
497
+ // Clear conversation history
498
+ clearConversation(): void {
499
+ this.conversationHistory = [];
500
+ }
501
+
502
+ // Get conversation history
503
+ getConversationHistory(): Array<{ role: string; content: string }> {
504
+ return [...this.conversationHistory];
505
+ }
506
+ }
507
+
508
+ // Singleton instance
509
+ export const aiService = new AIService();
@@ -11,3 +11,14 @@ export {
11
11
  saveGeminiConfig,
12
12
  clearAuthTokens
13
13
  } from './GeminiService.js';
14
+ export {
15
+ aiService,
16
+ AIService,
17
+ AIProvider,
18
+ AIProviderInfo,
19
+ AIConfig,
20
+ loadAIConfig,
21
+ saveAIConfig,
22
+ getAvailableProviders,
23
+ getProviderStatus
24
+ } from './AIService.js';
@@ -836,6 +836,22 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
836
836
  });
837
837
  return true; // Keep message channel open for async response
838
838
 
839
+ case "openDevTools":
840
+ // Open DevTools for the specified tab
841
+ // Note: Chrome doesn't have a direct API to open DevTools to a specific panel
842
+ // We inject a script that triggers the inspector and prompt user to open DevTools
843
+ if (request.tabId) {
844
+ chrome.scripting.executeScript({
845
+ target: { tabId: request.tabId },
846
+ func: () => {
847
+ // Log a message suggesting to open DevTools
848
+ console.log('%c[GxP Inspector] Inspector enabled! Press F12 or Ctrl+Shift+J to open DevTools and see the GxP Inspector panel.', 'color: #667eea; font-weight: bold; font-size: 14px;');
849
+ }
850
+ }).catch(err => console.log('[GxP DevTools] Could not inject script:', err));
851
+ }
852
+ sendResponse({ success: true });
853
+ return false;
854
+
839
855
  default:
840
856
  console.warn("[JavaScript Proxy] Unknown action:", request.action);
841
857
  sendResponse({ success: false, error: "Unknown action" });
@@ -255,18 +255,64 @@
255
255
  }
256
256
 
257
257
  /* Inspector section */
258
- .inspector-row {
258
+ .inspector-card {
259
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
260
+ border-radius: 8px;
261
+ padding: 14px;
262
+ margin-top: 16px;
263
+ cursor: pointer;
264
+ transition: all 0.2s;
265
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
266
+ }
267
+
268
+ .inspector-card:hover {
269
+ transform: translateY(-1px);
270
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
271
+ }
272
+
273
+ .inspector-card-header {
259
274
  display: flex;
260
275
  align-items: center;
261
- gap: 10px;
262
- padding: 10px 0;
263
- border-top: 1px solid #e9ecef;
264
- margin-top: 8px;
276
+ justify-content: space-between;
277
+ margin-bottom: 8px;
278
+ }
279
+
280
+ .inspector-card-title {
281
+ font-size: 13px;
282
+ font-weight: 600;
283
+ color: white;
284
+ }
285
+
286
+ .inspector-badge {
287
+ background: rgba(255, 255, 255, 0.2);
288
+ padding: 3px 8px;
289
+ border-radius: 10px;
290
+ font-size: 10px;
291
+ font-weight: 600;
292
+ color: white;
293
+ }
294
+
295
+ .inspector-badge.enabled {
296
+ background: rgba(40, 167, 69, 0.8);
265
297
  }
266
298
 
267
- .inspector-toggle {
268
- padding: 4px 10px;
299
+ .inspector-card-desc {
269
300
  font-size: 11px;
301
+ color: rgba(255, 255, 255, 0.85);
302
+ margin-bottom: 8px;
303
+ }
304
+
305
+ .inspector-card-hint {
306
+ font-size: 10px;
307
+ color: rgba(255, 255, 255, 0.6);
308
+ }
309
+
310
+ .inspector-card-hint kbd {
311
+ background: rgba(255, 255, 255, 0.2);
312
+ color: white;
313
+ padding: 2px 5px;
314
+ border-radius: 3px;
315
+ font-size: 9px;
270
316
  }
271
317
 
272
318
  kbd {
@@ -407,6 +453,16 @@
407
453
  <div class="help-text">RegEx pattern to match JavaScript file URLs</div>
408
454
  </div>
409
455
  </div>
456
+
457
+ <!-- Component Inspector Card -->
458
+ <div id="inspectorCard" class="inspector-card">
459
+ <div class="inspector-card-header">
460
+ <span class="inspector-card-title">Component Inspector</span>
461
+ <span id="inspectorBadge" class="inspector-badge">OFF</span>
462
+ </div>
463
+ <div class="inspector-card-desc">Click to open DevTools and inspect Vue components</div>
464
+ <div class="inspector-card-hint">Or press <kbd>Ctrl+Shift+I</kbd> on the page</div>
465
+ </div>
410
466
  </div>
411
467
 
412
468
  <!-- CSS Tab -->
@@ -472,18 +528,6 @@
472
528
  <label for="maskingMode">URL Masking Mode</label>
473
529
  </div>
474
530
  </div>
475
-
476
- <div class="settings-section">
477
- <div class="settings-title">Component Inspector</div>
478
- <div style="display: flex; align-items: center; gap: 10px;">
479
- <button id="inspectorToggle" class="toggle-button inspector-toggle">
480
- <div class="status-dot"></div>
481
- <span id="inspectorText">OFF</span>
482
- </button>
483
- <span style="font-size: 11px; color: #6c757d;">or press <kbd>Ctrl+Shift+I</kbd></span>
484
- </div>
485
- <div class="help-text" style="margin-top: 8px;">Inspect Vue components and extract hardcoded strings</div>
486
- </div>
487
531
  </div>
488
532
  </div>
489
533