@epic-web/workshop-utils 6.70.0 → 6.71.1

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,38 @@ 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, {
332
+ color,
333
+ process: sidecarProcess,
334
+ command,
335
+ output,
336
+ });
315
337
  sidecarProcess.on('exit', (code, signal) => {
316
338
  sidecarProcess.stdout?.off('data', handleStdOutData);
317
339
  sidecarProcess.stderr?.off('data', handleStdErrData);
@@ -321,20 +343,73 @@ export function startSidecarProcess(name, command) {
321
343
  else {
322
344
  console.log(`${prefix} exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`);
323
345
  }
324
- sidecarProcesses.delete(name);
346
+ // Don't delete the entry so we can still access logs after exit
347
+ // Just mark it as not running by keeping the process reference
325
348
  });
326
349
  sidecarProcess.on('error', (error) => {
327
350
  console.error(`${prefix} failed to start: ${error.message}`);
328
- sidecarProcesses.delete(name);
351
+ addOutputLine('stderr', `Failed to start: ${error.message}`);
329
352
  });
330
353
  console.log(`${prefix} started`);
331
354
  }
355
+ export function getSidecarLogs(name, lineCount = 50) {
356
+ const entry = sidecarProcesses.get(name);
357
+ if (!entry)
358
+ return '';
359
+ // Get the last N lines of output
360
+ const logs = entry.output.slice(-lineCount);
361
+ return logs.map((line) => line.content).join('');
362
+ }
363
+ export async function restartSidecarProcess(name) {
364
+ if (isDeployed)
365
+ throw new Error('cannot restart sidecar processes in deployed mode');
366
+ const entry = sidecarProcesses.get(name);
367
+ if (!entry) {
368
+ console.log(`Sidecar process ${name} not found`);
369
+ return false;
370
+ }
371
+ const { command, process: proc, output: oldOutput } = entry;
372
+ // Remove the entry immediately to prevent concurrent restarts
373
+ sidecarProcesses.delete(name);
374
+ // Kill the existing process if it's still running
375
+ if (proc.exitCode === null) {
376
+ console.log(`Stopping sidecar process: ${name}`);
377
+ proc.kill();
378
+ // Wait for the process to exit
379
+ await new Promise((resolve) => {
380
+ const timeout = setTimeout(() => {
381
+ // Force kill if it doesn't exit in time
382
+ proc.kill('SIGKILL');
383
+ resolve();
384
+ }, 5000);
385
+ proc.once('exit', () => {
386
+ clearTimeout(timeout);
387
+ resolve();
388
+ });
389
+ });
390
+ }
391
+ // Start a new process with the same command
392
+ startSidecarProcess(name, command);
393
+ // Preserve logs from the old process by prepending them to the new array
394
+ // We must modify the existing array (not replace it) because event handlers
395
+ // have already captured it in their closure
396
+ const newEntry = sidecarProcesses.get(name);
397
+ if (newEntry) {
398
+ // Prepend old logs to the existing array that handlers are writing to
399
+ newEntry.output.unshift(...oldOutput);
400
+ // Trim to max entries if needed
401
+ if (newEntry.output.length > MAX_SIDECAR_LOG_ENTRIES) {
402
+ newEntry.output.splice(0, newEntry.output.length - MAX_SIDECAR_LOG_ENTRIES);
403
+ }
404
+ }
405
+ return true;
406
+ }
332
407
  export function stopSidecarProcesses() {
333
408
  if (isDeployed)
334
409
  throw new Error('cannot stop sidecar processes in deployed mode');
335
- for (const [name, proc] of sidecarProcesses.entries()) {
410
+ for (const [name, entry] of sidecarProcesses.entries()) {
336
411
  console.log(`Stopping sidecar process: ${name}`);
337
- proc.process.kill();
412
+ entry.process.kill();
338
413
  }
339
414
  sidecarProcesses.clear();
340
415
  }
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.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },