@gxp-dev/tools 2.0.62 → 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 +207 -132
  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,336 +1,358 @@
1
- import { spawn, ChildProcess, execSync } from 'child_process';
2
- import { EventEmitter } from 'events';
1
+ import { spawn, ChildProcess, execSync } from "child_process"
2
+ import { EventEmitter } from "events"
3
3
 
4
- export type ServiceStatus = 'stopped' | 'starting' | 'running' | 'error';
4
+ export type ServiceStatus = "stopped" | "starting" | "running" | "error"
5
5
 
6
6
  export interface ServiceConfig {
7
- id: string;
8
- name: string;
9
- command: string;
10
- args: string[];
11
- cwd: string;
12
- env?: Record<string, string>;
7
+ id: string
8
+ name: string
9
+ command: string
10
+ args: string[]
11
+ cwd: string
12
+ env?: Record<string, string>
13
13
  }
14
14
 
15
15
  // Store configs for restart capability
16
- const serviceConfigs: Map<string, ServiceConfig> = new Map();
16
+ const serviceConfigs: Map<string, ServiceConfig> = new Map()
17
17
 
18
18
  export interface ServiceState {
19
- id: string;
20
- name: string;
21
- status: ServiceStatus;
22
- logs: string[];
23
- process?: ChildProcess;
24
- pid?: number;
25
- error?: string;
19
+ id: string
20
+ name: string
21
+ status: ServiceStatus
22
+ logs: string[]
23
+ process?: ChildProcess
24
+ pid?: number
25
+ error?: string
26
26
  }
27
27
 
28
28
  export class ServiceManager extends EventEmitter {
29
- private services: Map<string, ServiceState> = new Map();
30
- private maxLogLines = 1000;
31
- private cleanupRegistered = false;
32
-
33
- constructor() {
34
- super();
35
- this.registerCleanupHandlers();
36
- }
37
-
38
- // Register cleanup handlers to kill all services on exit
39
- private registerCleanupHandlers(): void {
40
- if (this.cleanupRegistered) return;
41
- this.cleanupRegistered = true;
42
-
43
- const cleanup = () => {
44
- this.forceStopAll();
45
- };
46
-
47
- // Handle various exit scenarios
48
- process.on('exit', cleanup);
49
- process.on('SIGINT', cleanup);
50
- process.on('SIGTERM', cleanup);
51
- process.on('SIGHUP', cleanup);
52
- process.on('beforeExit', cleanup);
53
-
54
- // Handle uncaught exceptions
55
- process.on('uncaughtException', (err) => {
56
- console.error('Uncaught exception:', err);
57
- cleanup();
58
- process.exit(1);
59
- });
60
- }
61
-
62
- getService(id: string): ServiceState | undefined {
63
- return this.services.get(id);
64
- }
65
-
66
- getAllServices(): ServiceState[] {
67
- return Array.from(this.services.values());
68
- }
69
-
70
- isRunning(id: string): boolean {
71
- const service = this.services.get(id);
72
- return service?.status === 'running' || service?.status === 'starting';
73
- }
74
-
75
- start(config: ServiceConfig): ServiceState {
76
- // Store config for restart capability
77
- serviceConfigs.set(config.id, config);
78
-
79
- // Check if already running
80
- const existing = this.services.get(config.id);
81
- if (existing && (existing.status === 'running' || existing.status === 'starting')) {
82
- this.addLog(config.id, `[${config.name}] Already running`);
83
- return existing;
84
- }
85
-
86
- // Create or reset service state
87
- const state: ServiceState = {
88
- id: config.id,
89
- name: config.name,
90
- status: 'starting',
91
- logs: existing?.logs || [],
92
- };
93
- this.services.set(config.id, state);
94
-
95
- this.addLog(config.id, `[${config.name}] Starting...`);
96
- this.emit('statusChange', config.id, 'starting');
97
-
98
- try {
99
- // Spawn the process with environment variables to prevent stdin access
100
- // CI=true disables interactive prompts in many tools
101
- // These prevent child processes from trying to use raw stdin mode
102
- // detached: true creates a new process group that doesn't share the parent's tty
103
- const proc = spawn(config.command, config.args, {
104
- cwd: config.cwd,
105
- env: {
106
- ...process.env,
107
- ...config.env,
108
- FORCE_COLOR: '1',
109
- CI: 'true',
110
- },
111
- shell: true,
112
- stdio: ['ignore', 'pipe', 'pipe'],
113
- detached: true,
114
- });
115
-
116
- state.process = proc;
117
- state.pid = proc.pid;
118
-
119
- // Handle stdout
120
- proc.stdout?.on('data', (data: Buffer) => {
121
- const lines = data.toString().split('\n').filter(line => line.trim());
122
- lines.forEach(line => {
123
- this.addLog(config.id, line);
124
- });
125
-
126
- // Detect when service is ready
127
- const output = data.toString();
128
- if (state.status === 'starting') {
129
- // Vite ready indicators
130
- if (output.includes('ready in') || output.includes('Local:') || output.includes('VITE')) {
131
- state.status = 'running';
132
- this.emit('statusChange', config.id, 'running');
133
- }
134
- // Socket.IO ready indicator
135
- if (output.includes('Socket.IO server') || output.includes('listening on port')) {
136
- state.status = 'running';
137
- this.emit('statusChange', config.id, 'running');
138
- }
139
- }
140
- });
141
-
142
- // Handle stderr
143
- proc.stderr?.on('data', (data: Buffer) => {
144
- const lines = data.toString().split('\n').filter(line => line.trim());
145
- lines.forEach(line => {
146
- this.addLog(config.id, `[stderr] ${line}`);
147
- });
148
- });
149
-
150
- // Handle process exit
151
- proc.on('close', (code) => {
152
- state.process = undefined;
153
- if (code === 0 || code === null) {
154
- state.status = 'stopped';
155
- this.addLog(config.id, `[${config.name}] Stopped`);
156
- } else {
157
- state.status = 'error';
158
- state.error = `Process exited with code ${code}`;
159
- this.addLog(config.id, `[${config.name}] Error: exited with code ${code}`);
160
- }
161
- this.emit('statusChange', config.id, state.status);
162
- });
163
-
164
- // Handle spawn errors
165
- proc.on('error', (err) => {
166
- state.status = 'error';
167
- state.error = err.message;
168
- state.process = undefined;
169
- this.addLog(config.id, `[${config.name}] Error: ${err.message}`);
170
- this.emit('statusChange', config.id, 'error');
171
- });
172
-
173
- // Set running after a short delay if no ready message detected
174
- setTimeout(() => {
175
- if (state.status === 'starting' && state.process) {
176
- state.status = 'running';
177
- this.emit('statusChange', config.id, 'running');
178
- }
179
- }, 3000);
180
-
181
- } catch (err) {
182
- state.status = 'error';
183
- state.error = err instanceof Error ? err.message : 'Unknown error';
184
- this.addLog(config.id, `[${config.name}] Failed to start: ${state.error}`);
185
- this.emit('statusChange', config.id, 'error');
186
- }
187
-
188
- return state;
189
- }
190
-
191
- stop(id: string): boolean {
192
- const service = this.services.get(id);
193
- if (!service || !service.process) {
194
- return false;
195
- }
196
-
197
- this.addLog(id, `[${service.name}] Stopping...`);
198
-
199
- // Kill the process tree
200
- try {
201
- process.kill(-service.process.pid!, 'SIGTERM');
202
- } catch {
203
- // Process group kill failed, try direct kill
204
- service.process.kill('SIGTERM');
205
- }
206
-
207
- // Force kill after timeout
208
- setTimeout(() => {
209
- if (service.process && !service.process.killed) {
210
- try {
211
- process.kill(-service.process.pid!, 'SIGKILL');
212
- } catch {
213
- service.process.kill('SIGKILL');
214
- }
215
- }
216
- }, 2000);
217
-
218
- return true;
219
- }
220
-
221
- stopAll(): void {
222
- for (const [id] of this.services) {
223
- this.stop(id);
224
- }
225
- }
226
-
227
- // Force stop all services synchronously - used during process exit
228
- forceStopAll(): void {
229
- for (const [id, service] of this.services) {
230
- if (service.pid) {
231
- try {
232
- // Try to kill the process group first
233
- process.kill(-service.pid, 'SIGKILL');
234
- } catch {
235
- // Process group kill failed, try direct kill
236
- try {
237
- process.kill(service.pid, 'SIGKILL');
238
- } catch {
239
- // Process may already be dead
240
- }
241
- }
242
- }
243
- if (service.process && !service.process.killed) {
244
- try {
245
- service.process.kill('SIGKILL');
246
- } catch {
247
- // Ignore errors
248
- }
249
- }
250
- }
251
-
252
- // Also try to kill any orphaned vite/nodemon processes using lsof on common ports
253
- try {
254
- // Kill processes on typical dev ports synchronously
255
- execSync('lsof -ti :3060 | xargs kill -9 2>/dev/null || true', { stdio: 'ignore' });
256
- execSync('lsof -ti :3069 | xargs kill -9 2>/dev/null || true', { stdio: 'ignore' });
257
- } catch {
258
- // Ignore errors - best effort cleanup
259
- }
260
- }
261
-
262
- restart(id: string): boolean {
263
- const config = serviceConfigs.get(id);
264
- if (!config) {
265
- return false;
266
- }
267
-
268
- const service = this.services.get(id);
269
- if (service?.process) {
270
- // Stop first, then restart after process exits
271
- this.addLog(id, `[${config.name}] Restarting...`);
272
-
273
- const onExit = () => {
274
- // Small delay to ensure cleanup
275
- setTimeout(() => {
276
- this.start(config);
277
- }, 500);
278
- };
279
-
280
- // Listen for process exit once
281
- service.process.once('close', onExit);
282
-
283
- // Kill the process
284
- try {
285
- process.kill(-service.process.pid!, 'SIGTERM');
286
- } catch {
287
- service.process.kill('SIGTERM');
288
- }
289
-
290
- // Force kill after timeout
291
- setTimeout(() => {
292
- if (service.process && !service.process.killed) {
293
- try {
294
- process.kill(-service.process.pid!, 'SIGKILL');
295
- } catch {
296
- service.process.kill('SIGKILL');
297
- }
298
- }
299
- }, 2000);
300
- } else {
301
- // Not running, just start
302
- this.start(config);
303
- }
304
-
305
- return true;
306
- }
307
-
308
- getConfig(id: string): ServiceConfig | undefined {
309
- return serviceConfigs.get(id);
310
- }
311
-
312
- clearLogs(id: string): void {
313
- const service = this.services.get(id);
314
- if (service) {
315
- service.logs = [];
316
- this.emit('logsCleared', id);
317
- }
318
- }
319
-
320
- private addLog(id: string, message: string): void {
321
- const service = this.services.get(id);
322
- if (!service) return;
323
-
324
- service.logs.push(message);
325
-
326
- // Trim logs if too many
327
- if (service.logs.length > this.maxLogLines) {
328
- service.logs = service.logs.slice(-this.maxLogLines);
329
- }
330
-
331
- this.emit('log', id, message);
332
- }
29
+ private services: Map<string, ServiceState> = new Map()
30
+ private maxLogLines = 1000
31
+ private cleanupRegistered = false
32
+
33
+ constructor() {
34
+ super()
35
+ this.registerCleanupHandlers()
36
+ }
37
+
38
+ // Register cleanup handlers to kill all services on exit
39
+ private registerCleanupHandlers(): void {
40
+ if (this.cleanupRegistered) return
41
+ this.cleanupRegistered = true
42
+
43
+ const cleanup = () => {
44
+ this.forceStopAll()
45
+ }
46
+
47
+ // Handle various exit scenarios
48
+ process.on("exit", cleanup)
49
+ process.on("SIGINT", cleanup)
50
+ process.on("SIGTERM", cleanup)
51
+ process.on("SIGHUP", cleanup)
52
+ process.on("beforeExit", cleanup)
53
+
54
+ // Handle uncaught exceptions
55
+ process.on("uncaughtException", (err) => {
56
+ console.error("Uncaught exception:", err)
57
+ cleanup()
58
+ process.exit(1)
59
+ })
60
+ }
61
+
62
+ getService(id: string): ServiceState | undefined {
63
+ return this.services.get(id)
64
+ }
65
+
66
+ getAllServices(): ServiceState[] {
67
+ return Array.from(this.services.values())
68
+ }
69
+
70
+ isRunning(id: string): boolean {
71
+ const service = this.services.get(id)
72
+ return service?.status === "running" || service?.status === "starting"
73
+ }
74
+
75
+ start(config: ServiceConfig): ServiceState {
76
+ // Store config for restart capability
77
+ serviceConfigs.set(config.id, config)
78
+
79
+ // Check if already running
80
+ const existing = this.services.get(config.id)
81
+ if (
82
+ existing &&
83
+ (existing.status === "running" || existing.status === "starting")
84
+ ) {
85
+ this.addLog(config.id, `[${config.name}] Already running`)
86
+ return existing
87
+ }
88
+
89
+ // Create or reset service state
90
+ const state: ServiceState = {
91
+ id: config.id,
92
+ name: config.name,
93
+ status: "starting",
94
+ logs: existing?.logs || [],
95
+ }
96
+ this.services.set(config.id, state)
97
+
98
+ this.addLog(config.id, `[${config.name}] Starting...`)
99
+ this.emit("statusChange", config.id, "starting")
100
+
101
+ try {
102
+ // Spawn the process with environment variables to prevent stdin access
103
+ // CI=true disables interactive prompts in many tools
104
+ // These prevent child processes from trying to use raw stdin mode
105
+ // detached: true creates a new process group that doesn't share the parent's tty
106
+ const proc = spawn(config.command, config.args, {
107
+ cwd: config.cwd,
108
+ env: {
109
+ ...process.env,
110
+ ...config.env,
111
+ FORCE_COLOR: "1",
112
+ CI: "true",
113
+ },
114
+ shell: true,
115
+ stdio: ["ignore", "pipe", "pipe"],
116
+ detached: true,
117
+ })
118
+
119
+ state.process = proc
120
+ state.pid = proc.pid
121
+
122
+ // Handle stdout
123
+ proc.stdout?.on("data", (data: Buffer) => {
124
+ const lines = data
125
+ .toString()
126
+ .split("\n")
127
+ .filter((line) => line.trim())
128
+ lines.forEach((line) => {
129
+ this.addLog(config.id, line)
130
+ })
131
+
132
+ // Detect when service is ready
133
+ const output = data.toString()
134
+ if (state.status === "starting") {
135
+ // Vite ready indicators
136
+ if (
137
+ output.includes("ready in") ||
138
+ output.includes("Local:") ||
139
+ output.includes("VITE")
140
+ ) {
141
+ state.status = "running"
142
+ this.emit("statusChange", config.id, "running")
143
+ }
144
+ // Socket.IO ready indicator
145
+ if (
146
+ output.includes("Socket.IO server") ||
147
+ output.includes("listening on port")
148
+ ) {
149
+ state.status = "running"
150
+ this.emit("statusChange", config.id, "running")
151
+ }
152
+ }
153
+ })
154
+
155
+ // Handle stderr
156
+ proc.stderr?.on("data", (data: Buffer) => {
157
+ const lines = data
158
+ .toString()
159
+ .split("\n")
160
+ .filter((line) => line.trim())
161
+ lines.forEach((line) => {
162
+ this.addLog(config.id, `[stderr] ${line}`)
163
+ })
164
+ })
165
+
166
+ // Handle process exit
167
+ proc.on("close", (code) => {
168
+ state.process = undefined
169
+ if (code === 0 || code === null) {
170
+ state.status = "stopped"
171
+ this.addLog(config.id, `[${config.name}] Stopped`)
172
+ } else {
173
+ state.status = "error"
174
+ state.error = `Process exited with code ${code}`
175
+ this.addLog(
176
+ config.id,
177
+ `[${config.name}] Error: exited with code ${code}`,
178
+ )
179
+ }
180
+ this.emit("statusChange", config.id, state.status)
181
+ })
182
+
183
+ // Handle spawn errors
184
+ proc.on("error", (err) => {
185
+ state.status = "error"
186
+ state.error = err.message
187
+ state.process = undefined
188
+ this.addLog(config.id, `[${config.name}] Error: ${err.message}`)
189
+ this.emit("statusChange", config.id, "error")
190
+ })
191
+
192
+ // Set running after a short delay if no ready message detected
193
+ setTimeout(() => {
194
+ if (state.status === "starting" && state.process) {
195
+ state.status = "running"
196
+ this.emit("statusChange", config.id, "running")
197
+ }
198
+ }, 3000)
199
+ } catch (err) {
200
+ state.status = "error"
201
+ state.error = err instanceof Error ? err.message : "Unknown error"
202
+ this.addLog(config.id, `[${config.name}] Failed to start: ${state.error}`)
203
+ this.emit("statusChange", config.id, "error")
204
+ }
205
+
206
+ return state
207
+ }
208
+
209
+ stop(id: string): boolean {
210
+ const service = this.services.get(id)
211
+ if (!service || !service.process) {
212
+ return false
213
+ }
214
+
215
+ this.addLog(id, `[${service.name}] Stopping...`)
216
+
217
+ // Kill the process tree
218
+ try {
219
+ process.kill(-service.process.pid!, "SIGTERM")
220
+ } catch {
221
+ // Process group kill failed, try direct kill
222
+ service.process.kill("SIGTERM")
223
+ }
224
+
225
+ // Force kill after timeout
226
+ setTimeout(() => {
227
+ if (service.process && !service.process.killed) {
228
+ try {
229
+ process.kill(-service.process.pid!, "SIGKILL")
230
+ } catch {
231
+ service.process.kill("SIGKILL")
232
+ }
233
+ }
234
+ }, 2000)
235
+
236
+ return true
237
+ }
238
+
239
+ stopAll(): void {
240
+ for (const [id] of this.services) {
241
+ this.stop(id)
242
+ }
243
+ }
244
+
245
+ // Force stop all services synchronously - used during process exit
246
+ forceStopAll(): void {
247
+ for (const [id, service] of this.services) {
248
+ if (service.pid) {
249
+ try {
250
+ // Try to kill the process group first
251
+ process.kill(-service.pid, "SIGKILL")
252
+ } catch {
253
+ // Process group kill failed, try direct kill
254
+ try {
255
+ process.kill(service.pid, "SIGKILL")
256
+ } catch {
257
+ // Process may already be dead
258
+ }
259
+ }
260
+ }
261
+ if (service.process && !service.process.killed) {
262
+ try {
263
+ service.process.kill("SIGKILL")
264
+ } catch {
265
+ // Ignore errors
266
+ }
267
+ }
268
+ }
269
+
270
+ // Also try to kill any orphaned vite/nodemon processes using lsof on common ports
271
+ try {
272
+ // Kill processes on typical dev ports synchronously
273
+ execSync("lsof -ti :3060 | xargs kill -9 2>/dev/null || true", {
274
+ stdio: "ignore",
275
+ })
276
+ execSync("lsof -ti :3069 | xargs kill -9 2>/dev/null || true", {
277
+ stdio: "ignore",
278
+ })
279
+ } catch {
280
+ // Ignore errors - best effort cleanup
281
+ }
282
+ }
283
+
284
+ restart(id: string): boolean {
285
+ const config = serviceConfigs.get(id)
286
+ if (!config) {
287
+ return false
288
+ }
289
+
290
+ const service = this.services.get(id)
291
+ if (service?.process) {
292
+ // Stop first, then restart after process exits
293
+ this.addLog(id, `[${config.name}] Restarting...`)
294
+
295
+ const onExit = () => {
296
+ // Small delay to ensure cleanup
297
+ setTimeout(() => {
298
+ this.start(config)
299
+ }, 500)
300
+ }
301
+
302
+ // Listen for process exit once
303
+ service.process.once("close", onExit)
304
+
305
+ // Kill the process
306
+ try {
307
+ process.kill(-service.process.pid!, "SIGTERM")
308
+ } catch {
309
+ service.process.kill("SIGTERM")
310
+ }
311
+
312
+ // Force kill after timeout
313
+ setTimeout(() => {
314
+ if (service.process && !service.process.killed) {
315
+ try {
316
+ process.kill(-service.process.pid!, "SIGKILL")
317
+ } catch {
318
+ service.process.kill("SIGKILL")
319
+ }
320
+ }
321
+ }, 2000)
322
+ } else {
323
+ // Not running, just start
324
+ this.start(config)
325
+ }
326
+
327
+ return true
328
+ }
329
+
330
+ getConfig(id: string): ServiceConfig | undefined {
331
+ return serviceConfigs.get(id)
332
+ }
333
+
334
+ clearLogs(id: string): void {
335
+ const service = this.services.get(id)
336
+ if (service) {
337
+ service.logs = []
338
+ this.emit("logsCleared", id)
339
+ }
340
+ }
341
+
342
+ private addLog(id: string, message: string): void {
343
+ const service = this.services.get(id)
344
+ if (!service) return
345
+
346
+ service.logs.push(message)
347
+
348
+ // Trim logs if too many
349
+ if (service.logs.length > this.maxLogLines) {
350
+ service.logs = service.logs.slice(-this.maxLogLines)
351
+ }
352
+
353
+ this.emit("log", id, message)
354
+ }
333
355
  }
334
356
 
335
357
  // Singleton instance
336
- export const serviceManager = new ServiceManager();
358
+ export const serviceManager = new ServiceManager()