@epic-web/workshop-utils 6.70.0 → 6.71.0

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.
@@ -7,10 +7,18 @@ type DevProcessesMap = Map<string, {
7
7
  process: ChildProcess;
8
8
  port: number;
9
9
  }>;
10
- type SidecarProcessesMap = Map<string, {
10
+ type SidecarOutputLine = {
11
+ type: 'stdout' | 'stderr';
12
+ content: string;
13
+ timestamp: number;
14
+ };
15
+ type SidecarProcessEntry = {
11
16
  color: (typeof colors)[number];
12
17
  process: ChildProcess;
13
- }>;
18
+ command: string;
19
+ output: Array<SidecarOutputLine>;
20
+ };
21
+ type SidecarProcessesMap = Map<string, SidecarProcessEntry>;
14
22
  type OutputLine = {
15
23
  type: 'stdout' | 'stderr';
16
24
  content: string;
@@ -78,6 +86,8 @@ export declare function getProcesses(): {
78
86
  };
79
87
  export declare function startSidecarProcesses(processes: Record<string, string>): void;
80
88
  export declare function startSidecarProcess(name: string, command: string): void;
89
+ export declare function getSidecarLogs(name: string, lineCount?: number): string;
90
+ export declare function restartSidecarProcess(name: string): Promise<boolean>;
81
91
  export declare function stopSidecarProcesses(): void;
82
92
  export declare function closeProcess(key: string): Promise<void>;
83
93
  export declare function stopPort(port: string | number): Promise<void>;
@@ -274,14 +274,21 @@ export function startSidecarProcesses(processes) {
274
274
  startSidecarProcess(name, command);
275
275
  }
276
276
  }
277
+ // Maximum number of log entries to keep per sidecar process
278
+ const MAX_SIDECAR_LOG_ENTRIES = 1000;
277
279
  export function startSidecarProcess(name, command) {
278
280
  if (isDeployed)
279
281
  throw new Error('cannot run sidecar processes in deployed mode');
280
282
  // if the process is already running, don't start it again
281
- if (sidecarProcesses.has(name)) {
283
+ const existingEntry = sidecarProcesses.get(name);
284
+ if (existingEntry && existingEntry.process.exitCode === null) {
282
285
  console.log(`Sidecar process ${name} is already running`);
283
286
  return;
284
287
  }
288
+ // If there's an old exited entry, clean it up first
289
+ if (existingEntry) {
290
+ sidecarProcesses.delete(name);
291
+ }
285
292
  const color = getNextAvailableColor();
286
293
  // Spawn the command using shell to handle complex commands properly
287
294
  const workshopRoot = getEnv().EPICSHOP_CONTEXT_CWD;
@@ -295,23 +302,33 @@ export function startSidecarProcess(name, command) {
295
302
  },
296
303
  });
297
304
  const prefix = chalk[color](`[${name}]`);
305
+ const output = [];
306
+ function addOutputLine(type, content) {
307
+ output.push({ type, content, timestamp: Date.now() });
308
+ // Keep only the last MAX_SIDECAR_LOG_ENTRIES entries
309
+ if (output.length > MAX_SIDECAR_LOG_ENTRIES) {
310
+ output.shift();
311
+ }
312
+ }
298
313
  function handleStdOutData(data) {
299
- console.log(data
300
- .toString('utf-8')
314
+ const content = data.toString('utf-8');
315
+ addOutputLine('stdout', content);
316
+ console.log(content
301
317
  .split('\n')
302
318
  .map((line) => `${prefix} ${line}`)
303
319
  .join('\n'));
304
320
  }
305
321
  sidecarProcess.stdout?.on('data', handleStdOutData);
306
322
  function handleStdErrData(data) {
307
- console.error(data
308
- .toString('utf-8')
323
+ const content = data.toString('utf-8');
324
+ addOutputLine('stderr', content);
325
+ console.error(content
309
326
  .split('\n')
310
327
  .map((line) => `${prefix} ${line}`)
311
328
  .join('\n'));
312
329
  }
313
330
  sidecarProcess.stderr?.on('data', handleStdErrData);
314
- sidecarProcesses.set(name, { color, process: sidecarProcess });
331
+ sidecarProcesses.set(name, { color, process: sidecarProcess, command, output });
315
332
  sidecarProcess.on('exit', (code, signal) => {
316
333
  sidecarProcess.stdout?.off('data', handleStdOutData);
317
334
  sidecarProcess.stderr?.off('data', handleStdErrData);
@@ -321,20 +338,73 @@ export function startSidecarProcess(name, command) {
321
338
  else {
322
339
  console.log(`${prefix} exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`);
323
340
  }
324
- sidecarProcesses.delete(name);
341
+ // Don't delete the entry so we can still access logs after exit
342
+ // Just mark it as not running by keeping the process reference
325
343
  });
326
344
  sidecarProcess.on('error', (error) => {
327
345
  console.error(`${prefix} failed to start: ${error.message}`);
328
- sidecarProcesses.delete(name);
346
+ addOutputLine('stderr', `Failed to start: ${error.message}`);
329
347
  });
330
348
  console.log(`${prefix} started`);
331
349
  }
350
+ export function getSidecarLogs(name, lineCount = 50) {
351
+ const entry = sidecarProcesses.get(name);
352
+ if (!entry)
353
+ return '';
354
+ // Get the last N lines of output
355
+ const logs = entry.output.slice(-lineCount);
356
+ return logs.map((line) => line.content).join('');
357
+ }
358
+ export async function restartSidecarProcess(name) {
359
+ if (isDeployed)
360
+ throw new Error('cannot restart sidecar processes in deployed mode');
361
+ const entry = sidecarProcesses.get(name);
362
+ if (!entry) {
363
+ console.log(`Sidecar process ${name} not found`);
364
+ return false;
365
+ }
366
+ const { command, process: proc, output: oldOutput } = entry;
367
+ // Remove the entry immediately to prevent concurrent restarts
368
+ sidecarProcesses.delete(name);
369
+ // Kill the existing process if it's still running
370
+ if (proc.exitCode === null) {
371
+ console.log(`Stopping sidecar process: ${name}`);
372
+ proc.kill();
373
+ // Wait for the process to exit
374
+ await new Promise((resolve) => {
375
+ const timeout = setTimeout(() => {
376
+ // Force kill if it doesn't exit in time
377
+ proc.kill('SIGKILL');
378
+ resolve();
379
+ }, 5000);
380
+ proc.once('exit', () => {
381
+ clearTimeout(timeout);
382
+ resolve();
383
+ });
384
+ });
385
+ }
386
+ // Start a new process with the same command
387
+ startSidecarProcess(name, command);
388
+ // Preserve logs from the old process by prepending them to the new array
389
+ // We must modify the existing array (not replace it) because event handlers
390
+ // have already captured it in their closure
391
+ const newEntry = sidecarProcesses.get(name);
392
+ if (newEntry) {
393
+ // Prepend old logs to the existing array that handlers are writing to
394
+ newEntry.output.unshift(...oldOutput);
395
+ // Trim to max entries if needed
396
+ if (newEntry.output.length > MAX_SIDECAR_LOG_ENTRIES) {
397
+ newEntry.output.splice(0, newEntry.output.length - MAX_SIDECAR_LOG_ENTRIES);
398
+ }
399
+ }
400
+ return true;
401
+ }
332
402
  export function stopSidecarProcesses() {
333
403
  if (isDeployed)
334
404
  throw new Error('cannot stop sidecar processes in deployed mode');
335
- for (const [name, proc] of sidecarProcesses.entries()) {
405
+ for (const [name, entry] of sidecarProcesses.entries()) {
336
406
  console.log(`Stopping sidecar process: ${name}`);
337
- proc.process.kill();
407
+ entry.process.kill();
338
408
  }
339
409
  sidecarProcesses.clear();
340
410
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-utils",
3
- "version": "6.70.0",
3
+ "version": "6.71.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },