@gxp-dev/tools 2.0.63 → 2.0.64

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.
Files changed (182) hide show
  1. package/README.md +32 -31
  2. package/bin/gx-devtools.js +74 -54
  3. package/bin/lib/cli.js +23 -21
  4. package/bin/lib/commands/add-dependency.js +366 -325
  5. package/bin/lib/commands/assets.js +137 -139
  6. package/bin/lib/commands/build.js +169 -174
  7. package/bin/lib/commands/datastore.js +181 -183
  8. package/bin/lib/commands/dev.js +127 -131
  9. package/bin/lib/commands/extensions.js +147 -149
  10. package/bin/lib/commands/extract-config.js +73 -67
  11. package/bin/lib/commands/index.js +12 -12
  12. package/bin/lib/commands/init.js +342 -240
  13. package/bin/lib/commands/publish.js +69 -75
  14. package/bin/lib/commands/socket.js +69 -69
  15. package/bin/lib/commands/ssl.js +14 -14
  16. package/bin/lib/constants.js +10 -24
  17. package/bin/lib/tui/App.tsx +761 -705
  18. package/bin/lib/tui/components/AIPanel.tsx +191 -171
  19. package/bin/lib/tui/components/CommandInput.tsx +394 -343
  20. package/bin/lib/tui/components/GeminiPanel.tsx +175 -151
  21. package/bin/lib/tui/components/Header.tsx +23 -21
  22. package/bin/lib/tui/components/LogPanel.tsx +244 -220
  23. package/bin/lib/tui/components/TabBar.tsx +50 -48
  24. package/bin/lib/tui/components/WelcomeScreen.tsx +126 -71
  25. package/bin/lib/tui/index.tsx +37 -39
  26. package/bin/lib/tui/services/AIService.ts +518 -462
  27. package/bin/lib/tui/services/ExtensionService.ts +140 -129
  28. package/bin/lib/tui/services/GeminiService.ts +367 -337
  29. package/bin/lib/tui/services/ServiceManager.ts +344 -322
  30. package/bin/lib/tui/services/SocketService.ts +168 -168
  31. package/bin/lib/tui/services/ViteService.ts +88 -88
  32. package/bin/lib/tui/services/index.ts +47 -22
  33. package/bin/lib/utils/ai-scaffold.js +291 -280
  34. package/bin/lib/utils/extract-config.js +157 -140
  35. package/bin/lib/utils/files.js +82 -86
  36. package/bin/lib/utils/index.js +7 -7
  37. package/bin/lib/utils/paths.js +34 -34
  38. package/bin/lib/utils/prompts.js +194 -169
  39. package/bin/lib/utils/ssl.js +79 -81
  40. package/browser-extensions/README.md +0 -1
  41. package/browser-extensions/chrome/background.js +244 -237
  42. package/browser-extensions/chrome/content.js +32 -29
  43. package/browser-extensions/chrome/devtools.html +7 -7
  44. package/browser-extensions/chrome/devtools.js +19 -19
  45. package/browser-extensions/chrome/inspector.js +802 -767
  46. package/browser-extensions/chrome/manifest.json +71 -63
  47. package/browser-extensions/chrome/panel.html +674 -636
  48. package/browser-extensions/chrome/panel.js +722 -712
  49. package/browser-extensions/chrome/popup.html +586 -543
  50. package/browser-extensions/chrome/popup.js +282 -244
  51. package/browser-extensions/chrome/rules.json +1 -1
  52. package/browser-extensions/chrome/test-chrome.html +216 -136
  53. package/browser-extensions/chrome/test-mixed-content.html +284 -189
  54. package/browser-extensions/chrome/test-uri-pattern.html +221 -198
  55. package/browser-extensions/firefox/README.md +9 -6
  56. package/browser-extensions/firefox/background.js +221 -218
  57. package/browser-extensions/firefox/content.js +55 -52
  58. package/browser-extensions/firefox/debug-errors.html +386 -228
  59. package/browser-extensions/firefox/debug-https.html +153 -105
  60. package/browser-extensions/firefox/devtools.html +7 -7
  61. package/browser-extensions/firefox/devtools.js +23 -20
  62. package/browser-extensions/firefox/inspector.js +802 -767
  63. package/browser-extensions/firefox/manifest.json +68 -68
  64. package/browser-extensions/firefox/panel.html +674 -636
  65. package/browser-extensions/firefox/panel.js +722 -712
  66. package/browser-extensions/firefox/popup.html +572 -535
  67. package/browser-extensions/firefox/popup.js +281 -236
  68. package/browser-extensions/firefox/test-gramercy.html +170 -125
  69. package/browser-extensions/firefox/test-imports.html +59 -55
  70. package/browser-extensions/firefox/test-masking.html +231 -140
  71. package/browser-extensions/firefox/test-uri-pattern.html +221 -198
  72. package/dist/tui/App.d.ts +1 -1
  73. package/dist/tui/App.d.ts.map +1 -1
  74. package/dist/tui/App.js +154 -150
  75. package/dist/tui/App.js.map +1 -1
  76. package/dist/tui/components/AIPanel.d.ts.map +1 -1
  77. package/dist/tui/components/AIPanel.js +42 -35
  78. package/dist/tui/components/AIPanel.js.map +1 -1
  79. package/dist/tui/components/CommandInput.d.ts +1 -1
  80. package/dist/tui/components/CommandInput.d.ts.map +1 -1
  81. package/dist/tui/components/CommandInput.js +92 -62
  82. package/dist/tui/components/CommandInput.js.map +1 -1
  83. package/dist/tui/components/GeminiPanel.d.ts.map +1 -1
  84. package/dist/tui/components/GeminiPanel.js +37 -30
  85. package/dist/tui/components/GeminiPanel.js.map +1 -1
  86. package/dist/tui/components/Header.d.ts.map +1 -1
  87. package/dist/tui/components/Header.js +1 -1
  88. package/dist/tui/components/Header.js.map +1 -1
  89. package/dist/tui/components/LogPanel.d.ts +1 -1
  90. package/dist/tui/components/LogPanel.d.ts.map +1 -1
  91. package/dist/tui/components/LogPanel.js +26 -24
  92. package/dist/tui/components/LogPanel.js.map +1 -1
  93. package/dist/tui/components/TabBar.d.ts +2 -2
  94. package/dist/tui/components/TabBar.d.ts.map +1 -1
  95. package/dist/tui/components/TabBar.js +11 -11
  96. package/dist/tui/components/TabBar.js.map +1 -1
  97. package/dist/tui/components/WelcomeScreen.d.ts.map +1 -1
  98. package/dist/tui/components/WelcomeScreen.js +6 -6
  99. package/dist/tui/components/WelcomeScreen.js.map +1 -1
  100. package/dist/tui/index.d.ts.map +1 -1
  101. package/dist/tui/index.js +8 -8
  102. package/dist/tui/index.js.map +1 -1
  103. package/dist/tui/services/AIService.d.ts +2 -2
  104. package/dist/tui/services/AIService.d.ts.map +1 -1
  105. package/dist/tui/services/AIService.js +165 -125
  106. package/dist/tui/services/AIService.js.map +1 -1
  107. package/dist/tui/services/ExtensionService.d.ts +1 -1
  108. package/dist/tui/services/ExtensionService.d.ts.map +1 -1
  109. package/dist/tui/services/ExtensionService.js +33 -26
  110. package/dist/tui/services/ExtensionService.js.map +1 -1
  111. package/dist/tui/services/GeminiService.d.ts +1 -1
  112. package/dist/tui/services/GeminiService.d.ts.map +1 -1
  113. package/dist/tui/services/GeminiService.js +87 -76
  114. package/dist/tui/services/GeminiService.js.map +1 -1
  115. package/dist/tui/services/ServiceManager.d.ts +3 -3
  116. package/dist/tui/services/ServiceManager.d.ts.map +1 -1
  117. package/dist/tui/services/ServiceManager.js +72 -58
  118. package/dist/tui/services/ServiceManager.js.map +1 -1
  119. package/dist/tui/services/SocketService.d.ts.map +1 -1
  120. package/dist/tui/services/SocketService.js +32 -32
  121. package/dist/tui/services/SocketService.js.map +1 -1
  122. package/dist/tui/services/ViteService.d.ts.map +1 -1
  123. package/dist/tui/services/ViteService.js +26 -28
  124. package/dist/tui/services/ViteService.js.map +1 -1
  125. package/dist/tui/services/index.d.ts +6 -6
  126. package/dist/tui/services/index.d.ts.map +1 -1
  127. package/dist/tui/services/index.js +6 -6
  128. package/dist/tui/services/index.js.map +1 -1
  129. package/mcp/gxp-api-server.js +83 -81
  130. package/package.json +109 -93
  131. package/runtime/PortalContainer.vue +258 -234
  132. package/runtime/dev-tools/DevToolsModal.vue +153 -155
  133. package/runtime/dev-tools/LayoutSwitcher.vue +144 -140
  134. package/runtime/dev-tools/MockDataEditor.vue +456 -433
  135. package/runtime/dev-tools/SocketSimulator.vue +379 -371
  136. package/runtime/dev-tools/StoreInspector.vue +517 -455
  137. package/runtime/dev-tools/index.js +5 -5
  138. package/runtime/fallback-layouts/PrivateLayout.vue +2 -2
  139. package/runtime/fallback-layouts/PublicLayout.vue +2 -2
  140. package/runtime/fallback-layouts/SystemLayout.vue +2 -2
  141. package/runtime/gxpStringsPlugin.js +159 -134
  142. package/runtime/index.html +17 -19
  143. package/runtime/main.js +24 -22
  144. package/runtime/mock-api/auth-middleware.js +15 -15
  145. package/runtime/mock-api/image-generator.js +46 -46
  146. package/runtime/mock-api/index.js +55 -55
  147. package/runtime/mock-api/response-generator.js +116 -105
  148. package/runtime/mock-api/route-generator.js +107 -84
  149. package/runtime/mock-api/socket-triggers.js +94 -93
  150. package/runtime/mock-api/spec-loader.js +79 -80
  151. package/runtime/package.json +3 -0
  152. package/runtime/server.js +68 -68
  153. package/runtime/stores/gxpPortalConfigStore.js +204 -186
  154. package/runtime/stores/index.js +2 -2
  155. package/runtime/vite-inspector-plugin.js +858 -707
  156. package/runtime/vite-source-tracker-plugin.js +132 -113
  157. package/runtime/vite.config.js +191 -139
  158. package/scripts/launch-chrome.js +41 -41
  159. package/scripts/pack-chrome.js +38 -39
  160. package/socket-events/AiSessionMessageCreated.json +17 -17
  161. package/socket-events/SocialStreamPostCreated.json +23 -23
  162. package/socket-events/SocialStreamPostVariantCompleted.json +22 -22
  163. package/template/.claude/agents/gxp-developer.md +100 -99
  164. package/template/.claude/settings.json +7 -7
  165. package/template/AGENTS.md +30 -23
  166. package/template/GEMINI.md +20 -20
  167. package/template/README.md +70 -53
  168. package/template/app-manifest.json +2 -4
  169. package/template/configuration.json +10 -10
  170. package/template/default-styling.css +1 -1
  171. package/template/index.html +18 -20
  172. package/template/main.js +24 -22
  173. package/template/src/DemoPage.vue +415 -362
  174. package/template/src/Plugin.vue +76 -85
  175. package/template/src/stores/index.js +3 -3
  176. package/template/src/stores/test-data.json +164 -172
  177. package/template/theme-layouts/AdditionalStyling.css +50 -50
  178. package/template/theme-layouts/PrivateLayout.vue +8 -12
  179. package/template/theme-layouts/PublicLayout.vue +8 -12
  180. package/template/theme-layouts/SystemLayout.vue +8 -12
  181. package/template/vite.extend.js +45 -0
  182. package/template/vite.config.js +0 -409
@@ -1,669 +1,718 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
2
- import { Box, Text, useApp, useInput, useStdout } from 'ink';
3
- import WelcomeScreen from './components/WelcomeScreen.js';
4
- import Header from './components/Header.js';
5
- import TabBar from './components/TabBar.js';
6
- import LogPanel from './components/LogPanel.js';
7
- import CommandInput from './components/CommandInput.js';
8
- import AIPanel from './components/AIPanel.js';
1
+ import React, { useState, useEffect, useCallback } from "react"
2
+ import { Box, Text, useApp, useInput, useStdout } from "ink"
3
+ import WelcomeScreen from "./components/WelcomeScreen.js"
4
+ import Header from "./components/Header.js"
5
+ import TabBar from "./components/TabBar.js"
6
+ import LogPanel from "./components/LogPanel.js"
7
+ import CommandInput from "./components/CommandInput.js"
8
+ import AIPanel from "./components/AIPanel.js"
9
9
  import {
10
- serviceManager,
11
- startVite,
12
- stopVite,
13
- startSocket,
14
- stopSocket,
15
- startExtension,
16
- stopExtension,
17
- listSocketEvents,
18
- sendSocketEvent,
19
- ServiceStatus,
20
- BrowserType,
21
- aiService,
22
- getAvailableProviders,
23
- getProviderStatus,
24
- AIProvider,
25
- } from './services/index.js';
10
+ serviceManager,
11
+ startVite,
12
+ stopVite,
13
+ startSocket,
14
+ stopSocket,
15
+ startExtension,
16
+ stopExtension,
17
+ listSocketEvents,
18
+ sendSocketEvent,
19
+ ServiceStatus,
20
+ BrowserType,
21
+ aiService,
22
+ getAvailableProviders,
23
+ getProviderStatus,
24
+ AIProvider,
25
+ } from "./services/index.js"
26
26
 
27
27
  export interface Service {
28
- id: string;
29
- name: string;
30
- status: ServiceStatus;
31
- logs: string[];
28
+ id: string
29
+ name: string
30
+ status: ServiceStatus
31
+ logs: string[]
32
32
  }
33
33
 
34
34
  interface ExtractedConfig {
35
- strings: Record<string, string>;
36
- settings: Record<string, unknown>;
37
- assets: Record<string, string>;
38
- triggerState: Record<string, unknown>;
39
- dependencies: Array<{ identifier: string; path: string; events?: Record<string, string> }>;
35
+ strings: Record<string, string>
36
+ settings: Record<string, unknown>
37
+ assets: Record<string, string>
38
+ triggerState: Record<string, unknown>
39
+ dependencies: Array<{
40
+ identifier: string
41
+ path: string
42
+ events?: Record<string, string>
43
+ }>
40
44
  }
41
45
 
42
46
  export interface AppProps {
43
- autoStart?: string[];
44
- args?: Record<string, unknown>;
47
+ autoStart?: string[]
48
+ args?: Record<string, unknown>
45
49
  }
46
50
 
47
51
  export default function App({ autoStart, args }: AppProps) {
48
- const { exit } = useApp();
49
- const { stdout } = useStdout();
50
- const [showAIPanel, setShowAIPanel] = useState(false);
51
- const [services, setServices] = useState<Service[]>([]);
52
- const [activeTab, setActiveTab] = useState(0);
53
- const [suggestionRows, setSuggestionRows] = useState(0);
54
-
55
- // Get terminal height for full screen
56
- const terminalHeight = stdout?.rows || 24;
57
-
58
- // Stable callback for suggestion row changes to prevent unnecessary re-renders
59
- const handleSuggestionsChange = useCallback((count: number) => {
60
- setSuggestionRows(count);
61
- }, []);
62
-
63
- // Sync services from ServiceManager
64
- const syncServices = useCallback(() => {
65
- const managerServices = serviceManager.getAllServices();
66
- setServices(managerServices.map(s => ({
67
- id: s.id,
68
- name: s.name,
69
- status: s.status,
70
- logs: s.logs,
71
- })));
72
- }, []);
73
-
74
- // Set up ServiceManager event listeners
75
- useEffect(() => {
76
- const onLog = (id: string, message: string) => {
77
- setServices(prev => prev.map(s =>
78
- s.id === id ? { ...s, logs: [...s.logs, message] } : s
79
- ));
80
- };
81
-
82
- const onStatusChange = (id: string, status: ServiceStatus) => {
83
- setServices(prev => prev.map(s =>
84
- s.id === id ? { ...s, status } : s
85
- ));
86
- };
87
-
88
- const onLogsCleared = (id: string) => {
89
- setServices(prev => prev.map(s =>
90
- s.id === id ? { ...s, logs: [] } : s
91
- ));
92
- };
93
-
94
- serviceManager.on('log', onLog);
95
- serviceManager.on('statusChange', onStatusChange);
96
- serviceManager.on('logsCleared', onLogsCleared);
97
-
98
- return () => {
99
- serviceManager.off('log', onLog);
100
- serviceManager.off('statusChange', onStatusChange);
101
- serviceManager.off('logsCleared', onLogsCleared);
102
- };
103
- }, []);
104
-
105
- // Cleanup on exit
106
- useEffect(() => {
107
- const cleanup = () => {
108
- serviceManager.forceStopAll();
109
- };
110
-
111
- process.on('SIGINT', cleanup);
112
- process.on('SIGTERM', cleanup);
113
-
114
- return () => {
115
- cleanup();
116
- process.off('SIGINT', cleanup);
117
- process.off('SIGTERM', cleanup);
118
- };
119
- }, []);
120
-
121
- // Handle keyboard shortcuts
122
- useInput((input, key) => {
123
- // Ctrl+C to exit
124
- if (key.ctrl && input === 'c') {
125
- serviceManager.forceStopAll();
126
- exit();
127
- return;
128
- }
129
-
130
- // Ctrl+L to clear current log
131
- if (key.ctrl && input === 'l') {
132
- if (services[activeTab]) {
133
- serviceManager.clearLogs(services[activeTab].id);
134
- }
135
- return;
136
- }
137
-
138
- // Ctrl+K to stop current service
139
- if (key.ctrl && input === 'k') {
140
- if (services[activeTab] && services[activeTab].id !== 'system') {
141
- stopService(services[activeTab].id);
142
- }
143
- return;
144
- }
145
-
146
- // Left/Right arrow to switch tabs (Tab is reserved for command autocomplete)
147
- if (key.leftArrow && services.length > 0) {
148
- setActiveTab((activeTab - 1 + services.length) % services.length);
149
- return;
150
- }
151
- if (key.rightArrow && services.length > 0) {
152
- setActiveTab((activeTab + 1) % services.length);
153
- return;
154
- }
155
-
156
- // Ctrl+1-9 or Cmd+1-9 to switch tabs directly
157
- if ((key.ctrl || key.meta) && /^[1-9]$/.test(input)) {
158
- const tabIndex = parseInt(input) - 1;
159
- if (tabIndex < services.length) {
160
- setActiveTab(tabIndex);
161
- }
162
- return;
163
- }
164
- });
165
-
166
- // Handle auto-start commands
167
- useEffect(() => {
168
- if (autoStart?.length) {
169
- setTimeout(() => {
170
- autoStart.forEach(cmd => {
171
- handleCommand(`/${cmd}`);
172
- });
173
- }, 100);
174
- }
175
- }, []);
176
-
177
- const handleCommand = (input: string) => {
178
- const trimmed = input.trim();
179
- if (!trimmed.startsWith('/')) return;
180
-
181
- const parts = trimmed.slice(1).split(' ');
182
- const command = parts[0];
183
- const cmdArgs = parts.slice(1);
184
-
185
- switch (command) {
186
- case 'help':
187
- addSystemLog(getHelpText());
188
- break;
189
-
190
- case 'dev':
191
- startDevServer(cmdArgs);
192
- break;
193
-
194
- case 'socket':
195
- if (cmdArgs[0] === 'send') {
196
- handleSocketSend(cmdArgs.slice(1));
197
- } else if (cmdArgs[0] === 'list') {
198
- handleSocketList();
199
- } else {
200
- const socketWithMock = cmdArgs.includes('--with-mock') || args?.withMock === true;
201
- startSocketServer(socketWithMock);
202
- }
203
- break;
204
-
205
- case 'mock':
206
- // Shorthand for /socket --with-mock
207
- startSocketServer(true);
208
- break;
209
-
210
- case 'ext':
211
- const browser = cmdArgs[0] || 'chrome';
212
- launchExtension(browser);
213
- break;
214
-
215
- case 'stop':
216
- stopService(cmdArgs[0]);
217
- break;
218
-
219
- case 'restart':
220
- restartService(cmdArgs[0]);
221
- break;
222
-
223
- case 'clear':
224
- if (services[activeTab]) {
225
- serviceManager.clearLogs(services[activeTab].id);
226
- }
227
- break;
228
-
229
- case 'quit':
230
- case 'exit':
231
- serviceManager.forceStopAll();
232
- exit();
233
- break;
234
-
235
- case 'ai':
236
- handleAICommand(cmdArgs);
237
- break;
238
-
239
- case 'extract-config':
240
- case 'extract':
241
- handleExtractConfig(cmdArgs);
242
- break;
243
-
244
- case 'add-dependency':
245
- handleAddDependency(cmdArgs);
246
- break;
247
-
248
- default:
249
- addSystemLog(`Unknown command: ${command}. Type /help for available commands.`);
250
- }
251
- };
252
-
253
- const startDevServer = (cmdArgs: string[]) => {
254
- const noHttps = cmdArgs.includes('--no-https') || args?.noHttps === true;
255
- const noSocket = cmdArgs.includes('--no-socket') || args?.noSocket === true;
256
- const withMock = cmdArgs.includes('--with-mock') || args?.withMock === true;
257
- const withFirefox = cmdArgs.includes('--firefox') || args?.firefox === true;
258
- const withChrome = cmdArgs.includes('--chrome') || args?.chrome === true;
259
-
260
- // Determine port from env or default
261
- const port = process.env.NODE_PORT || 3060;
262
- const useHttps = !noHttps;
263
-
264
- // Socket server starts by default unless --no-socket is passed
265
- const shouldStartSocket = !noSocket;
266
-
267
- // Check if already running
268
- if (serviceManager.isRunning('vite')) {
269
- addSystemLog('Vite dev server is already running.');
270
- // Switch to vite tab
271
- const viteIdx = services.findIndex(s => s.id === 'vite');
272
- if (viteIdx >= 0) setActiveTab(viteIdx);
273
- return;
274
- }
275
-
276
- startVite({ noHttps });
277
-
278
- // Also start socket server based on flags/env (with mock if requested)
279
- if (shouldStartSocket && !serviceManager.isRunning('socket')) {
280
- startSocket({ withMock });
281
- }
282
-
283
- // Launch browser extensions if requested (pass URL options)
284
- if (withFirefox && !serviceManager.isRunning('ext-firefox')) {
285
- startExtension({ browser: 'firefox', useHttps, port });
286
- }
287
- if (withChrome && !serviceManager.isRunning('ext-chrome')) {
288
- startExtension({ browser: 'chrome', useHttps, port });
289
- }
290
-
291
- // Sync and switch to the new vite tab
292
- const updatedServices = serviceManager.getAllServices();
293
- const viteIdx = updatedServices.findIndex(s => s.id === 'vite');
294
- setServices(updatedServices.map(s => ({
295
- id: s.id,
296
- name: s.name,
297
- status: s.status,
298
- logs: s.logs,
299
- })));
300
- setActiveTab(viteIdx >= 0 ? viteIdx : Math.max(0, updatedServices.length - 1));
301
- };
302
-
303
- const startSocketServer = (withMock: boolean = false) => {
304
- if (serviceManager.isRunning('socket')) {
305
- addSystemLog('Socket.IO server is already running.');
306
- const socketIdx = services.findIndex(s => s.id === 'socket');
307
- if (socketIdx >= 0) setActiveTab(socketIdx);
308
- return;
309
- }
310
-
311
- startSocket({ withMock });
312
-
313
- // Sync and switch to the new socket tab
314
- const updatedServices = serviceManager.getAllServices();
315
- const socketIdx = updatedServices.findIndex(s => s.id === 'socket');
316
- setServices(updatedServices.map(s => ({
317
- id: s.id,
318
- name: s.name,
319
- status: s.status,
320
- logs: s.logs,
321
- })));
322
- setActiveTab(socketIdx >= 0 ? socketIdx : Math.max(0, updatedServices.length - 1));
323
- };
324
-
325
- const launchExtension = (browser: string) => {
326
- const browserType = browser.toLowerCase() as BrowserType;
327
- if (browserType !== 'chrome' && browserType !== 'firefox') {
328
- addSystemLog(`Invalid browser: ${browser}. Use 'chrome' or 'firefox'.`);
329
- return;
330
- }
331
-
332
- const serviceId = `ext-${browserType}`;
333
- if (serviceManager.isRunning(serviceId)) {
334
- addSystemLog(`${browser} extension is already running.`);
335
- const extIdx = services.findIndex(s => s.id === serviceId);
336
- if (extIdx >= 0) setActiveTab(extIdx);
337
- return;
338
- }
339
-
340
- startExtension({ browser: browserType });
341
-
342
- // Sync and switch to the new extension tab
343
- const updatedServices = serviceManager.getAllServices();
344
- const extIdx = updatedServices.findIndex(s => s.id === serviceId);
345
- setServices(updatedServices.map(s => ({
346
- id: s.id,
347
- name: s.name,
348
- status: s.status,
349
- logs: s.logs,
350
- })));
351
- setActiveTab(extIdx >= 0 ? extIdx : Math.max(0, updatedServices.length - 1));
352
- };
353
-
354
- const stopService = (serviceId?: string) => {
355
- const targetId = serviceId || services[activeTab]?.id;
356
- if (!targetId) {
357
- addSystemLog('No service specified. Usage: /stop <service-id>');
358
- return;
359
- }
360
-
361
- if (targetId === 'vite') {
362
- stopVite();
363
- } else if (targetId === 'socket') {
364
- stopSocket();
365
- } else if (targetId.startsWith('ext-')) {
366
- const browser = targetId.replace('ext-', '') as BrowserType;
367
- stopExtension(browser);
368
- } else {
369
- serviceManager.stop(targetId);
370
- }
371
-
372
- syncServices();
373
- };
374
-
375
- const restartService = (serviceId?: string) => {
376
- const targetId = serviceId || services[activeTab]?.id;
377
- if (!targetId) {
378
- addSystemLog('No service to restart. Usage: /restart [service-id]');
379
- return;
380
- }
381
-
382
- if (targetId === 'system') {
383
- addSystemLog('Cannot restart the system service.');
384
- return;
385
- }
386
-
387
- const success = serviceManager.restart(targetId);
388
- if (!success) {
389
- addSystemLog(`Cannot restart "${targetId}". Service config not found.`);
390
- }
391
- syncServices();
392
- };
393
-
394
- const handleAICommand = async (cmdArgs: string[]) => {
395
- const subCommand = cmdArgs[0];
396
-
397
- switch (subCommand) {
398
- case 'model':
399
- // Set or show current AI provider
400
- const providerArg = cmdArgs[1] as AIProvider | undefined;
401
- if (providerArg) {
402
- const result = aiService.setProvider(providerArg);
403
- if (result.success) {
404
- addSystemLog(`✅ ${result.message}`);
405
- } else {
406
- addSystemLog(`❌ ${result.message}`);
407
- }
408
- } else {
409
- // Show current provider and available providers
410
- const current = aiService.getProviderInfo();
411
- const providers = getAvailableProviders();
412
- let message = `Current AI provider: ${current ? getProviderStatus(current) : 'None'}\n\nAvailable providers:`;
413
- for (const p of providers) {
414
- const status = p.available ? getProviderStatus(p) : `${p.name} (not available)`;
415
- const marker = p.id === current?.id ? ' ← current' : '';
416
- message += `\n ${p.id}: ${status}${marker}`;
417
- if (!p.available && p.reason) {
418
- message += `\n ${p.reason}`;
419
- }
420
- }
421
- message += '\n\nUsage: /ai model <claude|codex|gemini>';
422
- addSystemLog(message);
423
- }
424
- break;
425
-
426
- case 'status':
427
- // Show detailed status of all providers
428
- const providers = getAvailableProviders();
429
- const currentProvider = aiService.getProvider();
430
- let statusMsg = 'AI Provider Status:\n';
431
- for (const p of providers) {
432
- const icon = p.available ? '✅' : '❌';
433
- const current = p.id === currentProvider ? ' (current)' : '';
434
- statusMsg += `\n ${icon} ${getProviderStatus(p)}${current}`;
435
- if (!p.available && p.reason) {
436
- statusMsg += `\n ${p.reason}`;
437
- }
438
- }
439
- addSystemLog(statusMsg);
440
- break;
441
-
442
- case 'ask':
443
- // Quick question without opening panel
444
- const question = cmdArgs.slice(1).join(' ');
445
- if (!question) {
446
- addSystemLog('Usage: /ai ask <your question>');
447
- return;
448
- }
449
- if (!aiService.isAvailable()) {
450
- addSystemLog(`Current provider (${aiService.getProvider()}) is not available. Run /ai model to select a different provider.`);
451
- return;
452
- }
453
- const providerName = aiService.getProviderInfo()?.name || 'AI';
454
- addSystemLog(`Asking ${providerName}: ${question}`);
455
- try {
456
- aiService.loadProjectContext(process.cwd());
457
- const response = await aiService.sendMessage(question);
458
- addSystemLog(`${providerName}: ${response}`);
459
- } catch (err) {
460
- addSystemLog(`Error: ${err instanceof Error ? err.message : 'Unknown error'}`);
461
- }
462
- break;
463
-
464
- case 'clear':
465
- aiService.clearConversation();
466
- addSystemLog('Conversation history cleared.');
467
- break;
468
-
469
- case 'chat':
470
- default:
471
- // Open AI chat panel
472
- if (!aiService.isAvailable()) {
473
- addSystemLog(`Current provider (${aiService.getProvider()}) is not available. Run /ai model to select a different provider.`);
474
- return;
475
- }
476
- setShowAIPanel(true);
477
- }
478
- };
479
-
480
- const addSystemLog = (message: string) => {
481
- // Use functional update to properly handle rapid successive calls
482
- setServices(prev => {
483
- const existingSystem = prev.find(s => s.id === 'system');
484
- if (existingSystem) {
485
- // Add message to existing system service
486
- return prev.map(s =>
487
- s.id === 'system' ? { ...s, logs: [...s.logs, message] } : s
488
- );
489
- } else {
490
- // Create new system service with the message
491
- const newService: Service = {
492
- id: 'system',
493
- name: 'System',
494
- status: 'running',
495
- logs: [message],
496
- };
497
- return [...prev, newService];
498
- }
499
- });
500
-
501
- // Switch to system tab
502
- setTimeout(() => {
503
- setServices(current => {
504
- const sysIdx = current.findIndex(s => s.id === 'system');
505
- if (sysIdx >= 0) setActiveTab(sysIdx);
506
- return current; // Don't modify, just read
507
- });
508
- }, 50);
509
- };
510
-
511
- const handleSocketSend = async (eventArgs: string[]) => {
512
- if (!eventArgs.length) {
513
- addSystemLog('Usage: /socket send <event-name> [identifier]');
514
- return;
515
- }
516
-
517
- const eventName = eventArgs[0];
518
- const identifier = eventArgs[1];
519
-
520
- addSystemLog(`Sending socket event: ${eventName}...`);
521
-
522
- const result = await sendSocketEvent(eventName, identifier);
523
- if (result.success) {
524
- addSystemLog(`✅ ${result.message}`);
525
- } else {
526
- addSystemLog(`❌ ${result.message}`);
527
- }
528
- };
529
-
530
- const handleSocketList = () => {
531
- const events = listSocketEvents();
532
- if (events.length === 0) {
533
- addSystemLog('No socket events found. Check your socket-events directory.');
534
- return;
535
- }
536
-
537
- let message = 'Available socket events:\n';
538
- for (const event of events) {
539
- message += `\n ${event.name}\n`;
540
- message += ` Event: ${event.event}\n`;
541
- message += ` Channel: ${event.channel}`;
542
- }
543
- message += '\n\nUsage: /socket send <event-name> [identifier]';
544
- addSystemLog(message);
545
- };
546
-
547
- const handleExtractConfig = async (cmdArgs: string[]) => {
548
- const dryRun = cmdArgs.includes('--dry-run') || cmdArgs.includes('-d');
549
- const overwrite = cmdArgs.includes('--overwrite') || cmdArgs.includes('-o');
550
-
551
- addSystemLog('Scanning source files for GxP configuration...');
552
-
553
- try {
554
- // Use dynamic imports for ES modules
555
- const path = await import('path');
556
- const fs = await import('fs');
557
- const url = await import('url');
558
- const { createRequire } = await import('module');
559
-
560
- // Get the directory of this file and resolve to the utils directory
561
- const __filename = url.fileURLToPath(import.meta.url);
562
- const __dirname = path.dirname(__filename);
563
-
564
- // The compiled JS is in dist/tui/, utils is in bin/lib/utils/
565
- // From dist/tui/ we need to go up to package root, then into bin/lib/utils/
566
- const packageRoot = path.resolve(__dirname, '..', '..');
567
- const utilsPath = path.join(packageRoot, 'bin', 'lib', 'utils', 'extract-config.js');
568
-
569
- // Create a require function to load CommonJS modules
570
- const requireCjs = createRequire(import.meta.url);
571
- const extractConfigUtils = requireCjs(utilsPath) as {
572
- extractConfigFromSource: (srcDir: string) => ExtractedConfig;
573
- mergeConfig: (existing: Record<string, unknown>, extracted: ExtractedConfig, options: { overwrite: boolean }) => Record<string, unknown>;
574
- generateSummary: (config: ExtractedConfig) => string;
575
- };
576
-
577
- const projectPath = process.cwd();
578
- const srcDir = path.join(projectPath, 'src');
579
- const manifestPath = path.join(projectPath, 'app-manifest.json');
580
-
581
- // Check if src directory exists
582
- if (!fs.existsSync(srcDir)) {
583
- addSystemLog('Source directory not found: src/');
584
- return;
585
- }
586
-
587
- // Extract configuration
588
- const extractedConfig = extractConfigUtils.extractConfigFromSource(srcDir);
589
- const summary = extractConfigUtils.generateSummary(extractedConfig);
590
- addSystemLog(summary);
591
-
592
- // Count total items
593
- const totalItems =
594
- Object.keys(extractedConfig.strings).length +
595
- Object.keys(extractedConfig.settings).length +
596
- Object.keys(extractedConfig.assets).length +
597
- Object.keys(extractedConfig.triggerState).length +
598
- extractedConfig.dependencies.length;
599
-
600
- if (totalItems === 0) {
601
- addSystemLog('No GxP configuration found in source files.');
602
- return;
603
- }
604
-
605
- if (dryRun) {
606
- addSystemLog('Dry run mode - no changes made.');
607
- addSystemLog('Run /extract-config without --dry-run to apply changes.');
608
- return;
609
- }
610
-
611
- // Load or create manifest
612
- let existingManifest: Record<string, unknown> = {};
613
- if (fs.existsSync(manifestPath)) {
614
- try {
615
- existingManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
616
- } catch {
617
- addSystemLog('Could not parse existing manifest, creating new one.');
618
- existingManifest = getDefaultManifest();
619
- }
620
- } else {
621
- addSystemLog('Creating new app-manifest.json');
622
- existingManifest = getDefaultManifest();
623
- }
624
-
625
- // Merge and write
626
- const mergedManifest = extractConfigUtils.mergeConfig(existingManifest, extractedConfig, { overwrite });
627
- fs.writeFileSync(manifestPath, JSON.stringify(mergedManifest, null, '\t'));
628
- addSystemLog('Updated app-manifest.json');
629
- } catch (err) {
630
- addSystemLog(`Error: ${err instanceof Error ? err.message : 'Unknown error'}`);
631
- }
632
- };
633
-
634
- const handleAddDependency = async (cmdArgs: string[]) => {
635
- // The add-dependency wizard requires interactive terminal access (raw stdin)
636
- // which conflicts with Ink's own stdin handling. Run it in a separate terminal.
637
- const envFlag = cmdArgs.find(a => a === '-e' || a === '--env');
638
- const envVal = envFlag ? cmdArgs[cmdArgs.indexOf(envFlag) + 1] : '';
639
- const cmd = `gxdev add-dependency${envVal ? ` -e ${envVal}` : ''}`;
640
-
641
- addSystemLog('');
642
- addSystemLog('The Add Dependency wizard requires interactive terminal access.');
643
- addSystemLog('Run this command in a separate terminal:');
644
- addSystemLog('');
645
- addSystemLog(` \x1B[36m${cmd}\x1B[0m`);
646
- addSystemLog('');
647
- };
648
-
649
- const getDefaultManifest = () => ({
650
- name: 'GxToolkit',
651
- version: '1.0.0',
652
- description: 'GxToolkit Plugin',
653
- manifest_version: 3,
654
- asset_dir: '/src/assets/',
655
- configurationFile: 'configuration.json',
656
- appInstructionsFile: 'app-instructions.md',
657
- defaultStylingFile: 'default-styling.css',
658
- settings: {},
659
- strings: { default: {} },
660
- assets: {},
661
- triggerState: {},
662
- dependencies: [],
663
- permissions: [],
664
- });
665
-
666
- const getHelpText = () => `
52
+ const { exit } = useApp()
53
+ const { stdout } = useStdout()
54
+ const [showAIPanel, setShowAIPanel] = useState(false)
55
+ const [services, setServices] = useState<Service[]>([])
56
+ const [activeTab, setActiveTab] = useState(0)
57
+ const [suggestionRows, setSuggestionRows] = useState(0)
58
+
59
+ // Get terminal height for full screen
60
+ const terminalHeight = stdout?.rows || 24
61
+
62
+ // Stable callback for suggestion row changes to prevent unnecessary re-renders
63
+ const handleSuggestionsChange = useCallback((count: number) => {
64
+ setSuggestionRows(count)
65
+ }, [])
66
+
67
+ // Sync services from ServiceManager
68
+ const syncServices = useCallback(() => {
69
+ const managerServices = serviceManager.getAllServices()
70
+ setServices(
71
+ managerServices.map((s) => ({
72
+ id: s.id,
73
+ name: s.name,
74
+ status: s.status,
75
+ logs: s.logs,
76
+ })),
77
+ )
78
+ }, [])
79
+
80
+ // Set up ServiceManager event listeners
81
+ useEffect(() => {
82
+ const onLog = (id: string, message: string) => {
83
+ setServices((prev) =>
84
+ prev.map((s) =>
85
+ s.id === id ? { ...s, logs: [...s.logs, message] } : s,
86
+ ),
87
+ )
88
+ }
89
+
90
+ const onStatusChange = (id: string, status: ServiceStatus) => {
91
+ setServices((prev) =>
92
+ prev.map((s) => (s.id === id ? { ...s, status } : s)),
93
+ )
94
+ }
95
+
96
+ const onLogsCleared = (id: string) => {
97
+ setServices((prev) =>
98
+ prev.map((s) => (s.id === id ? { ...s, logs: [] } : s)),
99
+ )
100
+ }
101
+
102
+ serviceManager.on("log", onLog)
103
+ serviceManager.on("statusChange", onStatusChange)
104
+ serviceManager.on("logsCleared", onLogsCleared)
105
+
106
+ return () => {
107
+ serviceManager.off("log", onLog)
108
+ serviceManager.off("statusChange", onStatusChange)
109
+ serviceManager.off("logsCleared", onLogsCleared)
110
+ }
111
+ }, [])
112
+
113
+ // Cleanup on exit
114
+ useEffect(() => {
115
+ const cleanup = () => {
116
+ serviceManager.forceStopAll()
117
+ }
118
+
119
+ process.on("SIGINT", cleanup)
120
+ process.on("SIGTERM", cleanup)
121
+
122
+ return () => {
123
+ cleanup()
124
+ process.off("SIGINT", cleanup)
125
+ process.off("SIGTERM", cleanup)
126
+ }
127
+ }, [])
128
+
129
+ // Handle keyboard shortcuts
130
+ useInput((input, key) => {
131
+ // Ctrl+C to exit
132
+ if (key.ctrl && input === "c") {
133
+ serviceManager.forceStopAll()
134
+ exit()
135
+ return
136
+ }
137
+
138
+ // Ctrl+L to clear current log
139
+ if (key.ctrl && input === "l") {
140
+ if (services[activeTab]) {
141
+ serviceManager.clearLogs(services[activeTab].id)
142
+ }
143
+ return
144
+ }
145
+
146
+ // Ctrl+K to stop current service
147
+ if (key.ctrl && input === "k") {
148
+ if (services[activeTab] && services[activeTab].id !== "system") {
149
+ stopService(services[activeTab].id)
150
+ }
151
+ return
152
+ }
153
+
154
+ // Left/Right arrow to switch tabs (Tab is reserved for command autocomplete)
155
+ if (key.leftArrow && services.length > 0) {
156
+ setActiveTab((activeTab - 1 + services.length) % services.length)
157
+ return
158
+ }
159
+ if (key.rightArrow && services.length > 0) {
160
+ setActiveTab((activeTab + 1) % services.length)
161
+ return
162
+ }
163
+
164
+ // Ctrl+1-9 or Cmd+1-9 to switch tabs directly
165
+ if ((key.ctrl || key.meta) && /^[1-9]$/.test(input)) {
166
+ const tabIndex = parseInt(input) - 1
167
+ if (tabIndex < services.length) {
168
+ setActiveTab(tabIndex)
169
+ }
170
+ return
171
+ }
172
+ })
173
+
174
+ // Handle auto-start commands
175
+ useEffect(() => {
176
+ if (autoStart?.length) {
177
+ setTimeout(() => {
178
+ autoStart.forEach((cmd) => {
179
+ handleCommand(`/${cmd}`)
180
+ })
181
+ }, 100)
182
+ }
183
+ }, [])
184
+
185
+ const handleCommand = (input: string) => {
186
+ const trimmed = input.trim()
187
+ if (!trimmed.startsWith("/")) return
188
+
189
+ const parts = trimmed.slice(1).split(" ")
190
+ const command = parts[0]
191
+ const cmdArgs = parts.slice(1)
192
+
193
+ switch (command) {
194
+ case "help":
195
+ addSystemLog(getHelpText())
196
+ break
197
+
198
+ case "dev":
199
+ startDevServer(cmdArgs)
200
+ break
201
+
202
+ case "socket":
203
+ if (cmdArgs[0] === "send") {
204
+ handleSocketSend(cmdArgs.slice(1))
205
+ } else if (cmdArgs[0] === "list") {
206
+ handleSocketList()
207
+ } else {
208
+ const socketWithMock =
209
+ cmdArgs.includes("--with-mock") || args?.withMock === true
210
+ startSocketServer(socketWithMock)
211
+ }
212
+ break
213
+
214
+ case "mock":
215
+ // Shorthand for /socket --with-mock
216
+ startSocketServer(true)
217
+ break
218
+
219
+ case "ext":
220
+ const browser = cmdArgs[0] || "chrome"
221
+ launchExtension(browser)
222
+ break
223
+
224
+ case "stop":
225
+ stopService(cmdArgs[0])
226
+ break
227
+
228
+ case "restart":
229
+ restartService(cmdArgs[0])
230
+ break
231
+
232
+ case "clear":
233
+ if (services[activeTab]) {
234
+ serviceManager.clearLogs(services[activeTab].id)
235
+ }
236
+ break
237
+
238
+ case "quit":
239
+ case "exit":
240
+ serviceManager.forceStopAll()
241
+ exit()
242
+ break
243
+
244
+ case "ai":
245
+ handleAICommand(cmdArgs)
246
+ break
247
+
248
+ case "extract-config":
249
+ case "extract":
250
+ handleExtractConfig(cmdArgs)
251
+ break
252
+
253
+ case "add-dependency":
254
+ handleAddDependency(cmdArgs)
255
+ break
256
+
257
+ default:
258
+ addSystemLog(
259
+ `Unknown command: ${command}. Type /help for available commands.`,
260
+ )
261
+ }
262
+ }
263
+
264
+ const startDevServer = (cmdArgs: string[]) => {
265
+ const noHttps = cmdArgs.includes("--no-https") || args?.noHttps === true
266
+ const noSocket = cmdArgs.includes("--no-socket") || args?.noSocket === true
267
+ const withMock = cmdArgs.includes("--with-mock") || args?.withMock === true
268
+ const withFirefox = cmdArgs.includes("--firefox") || args?.firefox === true
269
+ const withChrome = cmdArgs.includes("--chrome") || args?.chrome === true
270
+
271
+ // Determine port from env or default
272
+ const port = process.env.NODE_PORT || 3060
273
+ const useHttps = !noHttps
274
+
275
+ // Socket server starts by default unless --no-socket is passed
276
+ const shouldStartSocket = !noSocket
277
+
278
+ // Check if already running
279
+ if (serviceManager.isRunning("vite")) {
280
+ addSystemLog("Vite dev server is already running.")
281
+ // Switch to vite tab
282
+ const viteIdx = services.findIndex((s) => s.id === "vite")
283
+ if (viteIdx >= 0) setActiveTab(viteIdx)
284
+ return
285
+ }
286
+
287
+ startVite({ noHttps })
288
+
289
+ // Also start socket server based on flags/env (with mock if requested)
290
+ if (shouldStartSocket && !serviceManager.isRunning("socket")) {
291
+ startSocket({ withMock })
292
+ }
293
+
294
+ // Launch browser extensions if requested (pass URL options)
295
+ if (withFirefox && !serviceManager.isRunning("ext-firefox")) {
296
+ startExtension({ browser: "firefox", useHttps, port })
297
+ }
298
+ if (withChrome && !serviceManager.isRunning("ext-chrome")) {
299
+ startExtension({ browser: "chrome", useHttps, port })
300
+ }
301
+
302
+ // Sync and switch to the new vite tab
303
+ const updatedServices = serviceManager.getAllServices()
304
+ const viteIdx = updatedServices.findIndex((s) => s.id === "vite")
305
+ setServices(
306
+ updatedServices.map((s) => ({
307
+ id: s.id,
308
+ name: s.name,
309
+ status: s.status,
310
+ logs: s.logs,
311
+ })),
312
+ )
313
+ setActiveTab(
314
+ viteIdx >= 0 ? viteIdx : Math.max(0, updatedServices.length - 1),
315
+ )
316
+ }
317
+
318
+ const startSocketServer = (withMock: boolean = false) => {
319
+ if (serviceManager.isRunning("socket")) {
320
+ addSystemLog("Socket.IO server is already running.")
321
+ const socketIdx = services.findIndex((s) => s.id === "socket")
322
+ if (socketIdx >= 0) setActiveTab(socketIdx)
323
+ return
324
+ }
325
+
326
+ startSocket({ withMock })
327
+
328
+ // Sync and switch to the new socket tab
329
+ const updatedServices = serviceManager.getAllServices()
330
+ const socketIdx = updatedServices.findIndex((s) => s.id === "socket")
331
+ setServices(
332
+ updatedServices.map((s) => ({
333
+ id: s.id,
334
+ name: s.name,
335
+ status: s.status,
336
+ logs: s.logs,
337
+ })),
338
+ )
339
+ setActiveTab(
340
+ socketIdx >= 0 ? socketIdx : Math.max(0, updatedServices.length - 1),
341
+ )
342
+ }
343
+
344
+ const launchExtension = (browser: string) => {
345
+ const browserType = browser.toLowerCase() as BrowserType
346
+ if (browserType !== "chrome" && browserType !== "firefox") {
347
+ addSystemLog(`Invalid browser: ${browser}. Use 'chrome' or 'firefox'.`)
348
+ return
349
+ }
350
+
351
+ const serviceId = `ext-${browserType}`
352
+ if (serviceManager.isRunning(serviceId)) {
353
+ addSystemLog(`${browser} extension is already running.`)
354
+ const extIdx = services.findIndex((s) => s.id === serviceId)
355
+ if (extIdx >= 0) setActiveTab(extIdx)
356
+ return
357
+ }
358
+
359
+ startExtension({ browser: browserType })
360
+
361
+ // Sync and switch to the new extension tab
362
+ const updatedServices = serviceManager.getAllServices()
363
+ const extIdx = updatedServices.findIndex((s) => s.id === serviceId)
364
+ setServices(
365
+ updatedServices.map((s) => ({
366
+ id: s.id,
367
+ name: s.name,
368
+ status: s.status,
369
+ logs: s.logs,
370
+ })),
371
+ )
372
+ setActiveTab(extIdx >= 0 ? extIdx : Math.max(0, updatedServices.length - 1))
373
+ }
374
+
375
+ const stopService = (serviceId?: string) => {
376
+ const targetId = serviceId || services[activeTab]?.id
377
+ if (!targetId) {
378
+ addSystemLog("No service specified. Usage: /stop <service-id>")
379
+ return
380
+ }
381
+
382
+ if (targetId === "vite") {
383
+ stopVite()
384
+ } else if (targetId === "socket") {
385
+ stopSocket()
386
+ } else if (targetId.startsWith("ext-")) {
387
+ const browser = targetId.replace("ext-", "") as BrowserType
388
+ stopExtension(browser)
389
+ } else {
390
+ serviceManager.stop(targetId)
391
+ }
392
+
393
+ syncServices()
394
+ }
395
+
396
+ const restartService = (serviceId?: string) => {
397
+ const targetId = serviceId || services[activeTab]?.id
398
+ if (!targetId) {
399
+ addSystemLog("No service to restart. Usage: /restart [service-id]")
400
+ return
401
+ }
402
+
403
+ if (targetId === "system") {
404
+ addSystemLog("Cannot restart the system service.")
405
+ return
406
+ }
407
+
408
+ const success = serviceManager.restart(targetId)
409
+ if (!success) {
410
+ addSystemLog(`Cannot restart "${targetId}". Service config not found.`)
411
+ }
412
+ syncServices()
413
+ }
414
+
415
+ const handleAICommand = async (cmdArgs: string[]) => {
416
+ const subCommand = cmdArgs[0]
417
+
418
+ switch (subCommand) {
419
+ case "model":
420
+ // Set or show current AI provider
421
+ const providerArg = cmdArgs[1] as AIProvider | undefined
422
+ if (providerArg) {
423
+ const result = aiService.setProvider(providerArg)
424
+ if (result.success) {
425
+ addSystemLog(`✅ ${result.message}`)
426
+ } else {
427
+ addSystemLog(`❌ ${result.message}`)
428
+ }
429
+ } else {
430
+ // Show current provider and available providers
431
+ const current = aiService.getProviderInfo()
432
+ const providers = getAvailableProviders()
433
+ let message = `Current AI provider: ${current ? getProviderStatus(current) : "None"}\n\nAvailable providers:`
434
+ for (const p of providers) {
435
+ const status = p.available
436
+ ? getProviderStatus(p)
437
+ : `${p.name} (not available)`
438
+ const marker = p.id === current?.id ? " ← current" : ""
439
+ message += `\n ${p.id}: ${status}${marker}`
440
+ if (!p.available && p.reason) {
441
+ message += `\n ${p.reason}`
442
+ }
443
+ }
444
+ message += "\n\nUsage: /ai model <claude|codex|gemini>"
445
+ addSystemLog(message)
446
+ }
447
+ break
448
+
449
+ case "status":
450
+ // Show detailed status of all providers
451
+ const providers = getAvailableProviders()
452
+ const currentProvider = aiService.getProvider()
453
+ let statusMsg = "AI Provider Status:\n"
454
+ for (const p of providers) {
455
+ const icon = p.available ? "✅" : "❌"
456
+ const current = p.id === currentProvider ? " (current)" : ""
457
+ statusMsg += `\n ${icon} ${getProviderStatus(p)}${current}`
458
+ if (!p.available && p.reason) {
459
+ statusMsg += `\n ${p.reason}`
460
+ }
461
+ }
462
+ addSystemLog(statusMsg)
463
+ break
464
+
465
+ case "ask":
466
+ // Quick question without opening panel
467
+ const question = cmdArgs.slice(1).join(" ")
468
+ if (!question) {
469
+ addSystemLog("Usage: /ai ask <your question>")
470
+ return
471
+ }
472
+ if (!aiService.isAvailable()) {
473
+ addSystemLog(
474
+ `Current provider (${aiService.getProvider()}) is not available. Run /ai model to select a different provider.`,
475
+ )
476
+ return
477
+ }
478
+ const providerName = aiService.getProviderInfo()?.name || "AI"
479
+ addSystemLog(`Asking ${providerName}: ${question}`)
480
+ try {
481
+ aiService.loadProjectContext(process.cwd())
482
+ const response = await aiService.sendMessage(question)
483
+ addSystemLog(`${providerName}: ${response}`)
484
+ } catch (err) {
485
+ addSystemLog(
486
+ `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
487
+ )
488
+ }
489
+ break
490
+
491
+ case "clear":
492
+ aiService.clearConversation()
493
+ addSystemLog("Conversation history cleared.")
494
+ break
495
+
496
+ case "chat":
497
+ default:
498
+ // Open AI chat panel
499
+ if (!aiService.isAvailable()) {
500
+ addSystemLog(
501
+ `Current provider (${aiService.getProvider()}) is not available. Run /ai model to select a different provider.`,
502
+ )
503
+ return
504
+ }
505
+ setShowAIPanel(true)
506
+ }
507
+ }
508
+
509
+ const addSystemLog = (message: string) => {
510
+ // Use functional update to properly handle rapid successive calls
511
+ setServices((prev) => {
512
+ const existingSystem = prev.find((s) => s.id === "system")
513
+ if (existingSystem) {
514
+ // Add message to existing system service
515
+ return prev.map((s) =>
516
+ s.id === "system" ? { ...s, logs: [...s.logs, message] } : s,
517
+ )
518
+ } else {
519
+ // Create new system service with the message
520
+ const newService: Service = {
521
+ id: "system",
522
+ name: "System",
523
+ status: "running",
524
+ logs: [message],
525
+ }
526
+ return [...prev, newService]
527
+ }
528
+ })
529
+
530
+ // Switch to system tab
531
+ setTimeout(() => {
532
+ setServices((current) => {
533
+ const sysIdx = current.findIndex((s) => s.id === "system")
534
+ if (sysIdx >= 0) setActiveTab(sysIdx)
535
+ return current // Don't modify, just read
536
+ })
537
+ }, 50)
538
+ }
539
+
540
+ const handleSocketSend = async (eventArgs: string[]) => {
541
+ if (!eventArgs.length) {
542
+ addSystemLog("Usage: /socket send <event-name> [identifier]")
543
+ return
544
+ }
545
+
546
+ const eventName = eventArgs[0]
547
+ const identifier = eventArgs[1]
548
+
549
+ addSystemLog(`Sending socket event: ${eventName}...`)
550
+
551
+ const result = await sendSocketEvent(eventName, identifier)
552
+ if (result.success) {
553
+ addSystemLog(`✅ ${result.message}`)
554
+ } else {
555
+ addSystemLog(`❌ ${result.message}`)
556
+ }
557
+ }
558
+
559
+ const handleSocketList = () => {
560
+ const events = listSocketEvents()
561
+ if (events.length === 0) {
562
+ addSystemLog(
563
+ "No socket events found. Check your socket-events directory.",
564
+ )
565
+ return
566
+ }
567
+
568
+ let message = "Available socket events:\n"
569
+ for (const event of events) {
570
+ message += `\n ${event.name}\n`
571
+ message += ` Event: ${event.event}\n`
572
+ message += ` Channel: ${event.channel}`
573
+ }
574
+ message += "\n\nUsage: /socket send <event-name> [identifier]"
575
+ addSystemLog(message)
576
+ }
577
+
578
+ const handleExtractConfig = async (cmdArgs: string[]) => {
579
+ const dryRun = cmdArgs.includes("--dry-run") || cmdArgs.includes("-d")
580
+ const overwrite = cmdArgs.includes("--overwrite") || cmdArgs.includes("-o")
581
+
582
+ addSystemLog("Scanning source files for GxP configuration...")
583
+
584
+ try {
585
+ // Use dynamic imports for ES modules
586
+ const path = await import("path")
587
+ const fs = await import("fs")
588
+ const url = await import("url")
589
+ const { createRequire } = await import("module")
590
+
591
+ // Get the directory of this file and resolve to the utils directory
592
+ const __filename = url.fileURLToPath(import.meta.url)
593
+ const __dirname = path.dirname(__filename)
594
+
595
+ // The compiled JS is in dist/tui/, utils is in bin/lib/utils/
596
+ // From dist/tui/ we need to go up to package root, then into bin/lib/utils/
597
+ const packageRoot = path.resolve(__dirname, "..", "..")
598
+ const utilsPath = path.join(
599
+ packageRoot,
600
+ "bin",
601
+ "lib",
602
+ "utils",
603
+ "extract-config.js",
604
+ )
605
+
606
+ // Create a require function to load CommonJS modules
607
+ const requireCjs = createRequire(import.meta.url)
608
+ const extractConfigUtils = requireCjs(utilsPath) as {
609
+ extractConfigFromSource: (srcDir: string) => ExtractedConfig
610
+ mergeConfig: (
611
+ existing: Record<string, unknown>,
612
+ extracted: ExtractedConfig,
613
+ options: { overwrite: boolean },
614
+ ) => Record<string, unknown>
615
+ generateSummary: (config: ExtractedConfig) => string
616
+ }
617
+
618
+ const projectPath = process.cwd()
619
+ const srcDir = path.join(projectPath, "src")
620
+ const manifestPath = path.join(projectPath, "app-manifest.json")
621
+
622
+ // Check if src directory exists
623
+ if (!fs.existsSync(srcDir)) {
624
+ addSystemLog("Source directory not found: src/")
625
+ return
626
+ }
627
+
628
+ // Extract configuration
629
+ const extractedConfig = extractConfigUtils.extractConfigFromSource(srcDir)
630
+ const summary = extractConfigUtils.generateSummary(extractedConfig)
631
+ addSystemLog(summary)
632
+
633
+ // Count total items
634
+ const totalItems =
635
+ Object.keys(extractedConfig.strings).length +
636
+ Object.keys(extractedConfig.settings).length +
637
+ Object.keys(extractedConfig.assets).length +
638
+ Object.keys(extractedConfig.triggerState).length +
639
+ extractedConfig.dependencies.length
640
+
641
+ if (totalItems === 0) {
642
+ addSystemLog("No GxP configuration found in source files.")
643
+ return
644
+ }
645
+
646
+ if (dryRun) {
647
+ addSystemLog("Dry run mode - no changes made.")
648
+ addSystemLog("Run /extract-config without --dry-run to apply changes.")
649
+ return
650
+ }
651
+
652
+ // Load or create manifest
653
+ let existingManifest: Record<string, unknown> = {}
654
+ if (fs.existsSync(manifestPath)) {
655
+ try {
656
+ existingManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
657
+ } catch {
658
+ addSystemLog("Could not parse existing manifest, creating new one.")
659
+ existingManifest = getDefaultManifest()
660
+ }
661
+ } else {
662
+ addSystemLog("Creating new app-manifest.json")
663
+ existingManifest = getDefaultManifest()
664
+ }
665
+
666
+ // Merge and write
667
+ const mergedManifest = extractConfigUtils.mergeConfig(
668
+ existingManifest,
669
+ extractedConfig,
670
+ { overwrite },
671
+ )
672
+ fs.writeFileSync(manifestPath, JSON.stringify(mergedManifest, null, "\t"))
673
+ addSystemLog("Updated app-manifest.json")
674
+ } catch (err) {
675
+ addSystemLog(
676
+ `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
677
+ )
678
+ }
679
+ }
680
+
681
+ const handleAddDependency = async (cmdArgs: string[]) => {
682
+ // The add-dependency wizard requires interactive terminal access (raw stdin)
683
+ // which conflicts with Ink's own stdin handling. Run it in a separate terminal.
684
+ const envFlag = cmdArgs.find((a) => a === "-e" || a === "--env")
685
+ const envVal = envFlag ? cmdArgs[cmdArgs.indexOf(envFlag) + 1] : ""
686
+ const cmd = `gxdev add-dependency${envVal ? ` -e ${envVal}` : ""}`
687
+
688
+ addSystemLog("")
689
+ addSystemLog(
690
+ "The Add Dependency wizard requires interactive terminal access.",
691
+ )
692
+ addSystemLog("Run this command in a separate terminal:")
693
+ addSystemLog("")
694
+ addSystemLog(` \x1B[36m${cmd}\x1B[0m`)
695
+ addSystemLog("")
696
+ }
697
+
698
+ const getDefaultManifest = () => ({
699
+ name: "GxToolkit",
700
+ version: "1.0.0",
701
+ description: "GxToolkit Plugin",
702
+ manifest_version: 3,
703
+ asset_dir: "/src/assets/",
704
+ configurationFile: "configuration.json",
705
+ appInstructionsFile: "app-instructions.md",
706
+ defaultStylingFile: "default-styling.css",
707
+ settings: {},
708
+ strings: { default: {} },
709
+ assets: {},
710
+ triggerState: {},
711
+ dependencies: [],
712
+ permissions: [],
713
+ })
714
+
715
+ const getHelpText = () => `
667
716
  Available commands:
668
717
 
669
718
  Development Server:
@@ -720,55 +769,62 @@ Keyboard shortcuts:
720
769
  Tab Autocomplete command
721
770
  ↑/↓ Navigate suggestions or command history
722
771
  Esc Clear input
723
- `;
724
-
725
- // Show AI panel
726
- if (showAIPanel) {
727
- return (
728
- <AIPanel
729
- onClose={() => setShowAIPanel(false)}
730
- onLog={addSystemLog}
731
- />
732
- );
733
- }
734
-
735
- const currentService = services[activeTab];
736
-
737
- // Calculate log panel height to make room for suggestions
738
- // Fixed elements: Header (3), TabBar (1), Log border (2), Input (3), Hints (1) = 10 rows minimum
739
- const fixedRows = 10;
740
- const availableForLog = terminalHeight - fixedRows - suggestionRows;
741
- const logPanelHeight = Math.max(3, availableForLog); // Minimum 3 rows for log panel
742
-
743
- return (
744
- <Box flexDirection="column" height={terminalHeight}>
745
- <Header projectName={process.cwd().split('/').pop() || 'gxdev'} />
746
-
747
- {services.length > 0 && (
748
- <TabBar
749
- services={services}
750
- activeTab={activeTab}
751
- onTabChange={setActiveTab}
752
- />
753
- )}
754
-
755
- <Box height={logPanelHeight} flexDirection="column" borderStyle="single" borderColor="gray" overflow="hidden">
756
- {currentService ? (
757
- <LogPanel logs={currentService.logs} maxHeight={logPanelHeight} />
758
- ) : (
759
- <WelcomeScreen />
760
- )}
761
- </Box>
762
-
763
- <CommandInput
764
- onSubmit={handleCommand}
765
- activeService={currentService ? {
766
- id: currentService.id,
767
- name: currentService.name,
768
- status: currentService.status
769
- } : null}
770
- onSuggestionsChange={handleSuggestionsChange}
771
- />
772
- </Box>
773
- );
772
+ `
773
+
774
+ // Show AI panel
775
+ if (showAIPanel) {
776
+ return (
777
+ <AIPanel onClose={() => setShowAIPanel(false)} onLog={addSystemLog} />
778
+ )
779
+ }
780
+
781
+ const currentService = services[activeTab]
782
+
783
+ // Calculate log panel height to make room for suggestions
784
+ // Fixed elements: Header (3), TabBar (1), Log border (2), Input (3), Hints (1) = 10 rows minimum
785
+ const fixedRows = 10
786
+ const availableForLog = terminalHeight - fixedRows - suggestionRows
787
+ const logPanelHeight = Math.max(3, availableForLog) // Minimum 3 rows for log panel
788
+
789
+ return (
790
+ <Box flexDirection="column" height={terminalHeight}>
791
+ <Header projectName={process.cwd().split("/").pop() || "gxdev"} />
792
+
793
+ {services.length > 0 && (
794
+ <TabBar
795
+ services={services}
796
+ activeTab={activeTab}
797
+ onTabChange={setActiveTab}
798
+ />
799
+ )}
800
+
801
+ <Box
802
+ height={logPanelHeight}
803
+ flexDirection="column"
804
+ borderStyle="single"
805
+ borderColor="gray"
806
+ overflow="hidden"
807
+ >
808
+ {currentService ? (
809
+ <LogPanel logs={currentService.logs} maxHeight={logPanelHeight} />
810
+ ) : (
811
+ <WelcomeScreen />
812
+ )}
813
+ </Box>
814
+
815
+ <CommandInput
816
+ onSubmit={handleCommand}
817
+ activeService={
818
+ currentService
819
+ ? {
820
+ id: currentService.id,
821
+ name: currentService.name,
822
+ status: currentService.status,
823
+ }
824
+ : null
825
+ }
826
+ onSuggestionsChange={handleSuggestionsChange}
827
+ />
828
+ </Box>
829
+ )
774
830
  }