@enspirit/emb 0.17.0 → 0.18.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.
@@ -0,0 +1,77 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { mkdir } from 'node:fs/promises';
4
+ import { dirname, join } from 'node:path';
5
+ import * as z from 'zod';
6
+ import { AbstractOperation } from '../../../operations/index.js';
7
+ export const ComposeLogsArchiveOperationInputSchema = z
8
+ .object({
9
+ components: z
10
+ .array(z.string())
11
+ .optional()
12
+ .describe('The list of components to archive logs for (all if omitted)'),
13
+ outputDir: z
14
+ .string()
15
+ .optional()
16
+ .describe('Output directory for log files (defaults to .emb/<flavor>/logs/docker/compose)'),
17
+ timestamps: z.boolean().optional().describe('Include timestamps in logs'),
18
+ tail: z
19
+ .number()
20
+ .optional()
21
+ .describe('Number of lines to show from the end'),
22
+ })
23
+ .optional();
24
+ export class ComposeLogsArchiveOperation extends AbstractOperation {
25
+ constructor() {
26
+ super(ComposeLogsArchiveOperationInputSchema);
27
+ }
28
+ async _run(input) {
29
+ const { monorepo } = this.context;
30
+ // Determine which components to archive
31
+ const componentNames = input?.components ?? monorepo.components.map((c) => c.name);
32
+ // Validate all component names
33
+ const components = componentNames.map((name) => monorepo.component(name));
34
+ // Determine output directory
35
+ const outputDir = input?.outputDir ?? monorepo.store.join('logs/docker/compose');
36
+ // Ensure output directory exists
37
+ await mkdir(outputDir, { recursive: true });
38
+ // Archive logs for each component
39
+ const archivePromises = components.map(async (component) => {
40
+ const logPath = join(outputDir, `${component.name}.log`);
41
+ await this.archiveComponentLogs(component.name, logPath, input);
42
+ return { component: component.name, path: logPath };
43
+ });
44
+ return Promise.all(archivePromises);
45
+ }
46
+ async archiveComponentLogs(serviceName, outputPath, input) {
47
+ const { monorepo } = this.context;
48
+ // Ensure parent directory exists
49
+ await mkdir(dirname(outputPath), { recursive: true });
50
+ const cmd = 'docker';
51
+ const args = ['compose', 'logs', '--no-color'];
52
+ if (input?.timestamps) {
53
+ args.push('-t');
54
+ }
55
+ if (input?.tail !== undefined) {
56
+ args.push('--tail', String(input.tail));
57
+ }
58
+ args.push(serviceName);
59
+ return new Promise((resolve, reject) => {
60
+ const writeStream = createWriteStream(outputPath);
61
+ const child = spawn(cmd, args, {
62
+ stdio: ['ignore', 'pipe', 'pipe'],
63
+ cwd: monorepo.rootDir,
64
+ });
65
+ child.stdout?.pipe(writeStream, { end: false });
66
+ child.stderr?.pipe(writeStream, { end: false });
67
+ child.on('error', (err) => {
68
+ writeStream.end();
69
+ reject(new Error(`Failed to get logs for ${serviceName}: ${err.message}`));
70
+ });
71
+ child.on('exit', () => {
72
+ writeStream.end();
73
+ resolve();
74
+ });
75
+ });
76
+ }
77
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './ComposeDownOperation.js';
2
2
  export * from './ComposeExecOperation.js';
3
3
  export * from './ComposeExecShellOperation.js';
4
+ export * from './ComposeLogsArchiveOperation.js';
4
5
  export * from './ComposeLogsOperation.js';
5
6
  export * from './ComposePsOperation.js';
6
7
  export * from './ComposeRestartOperation.js';
@@ -1,6 +1,7 @@
1
1
  export * from './ComposeDownOperation.js';
2
2
  export * from './ComposeExecOperation.js';
3
3
  export * from './ComposeExecShellOperation.js';
4
+ export * from './ComposeLogsArchiveOperation.js';
4
5
  export * from './ComposeLogsOperation.js';
5
6
  export * from './ComposePsOperation.js';
6
7
  export * from './ComposeRestartOperation.js';
@@ -1,6 +1,12 @@
1
+ import { CommandExecError } from '../../../index.js';
1
2
  import { execa } from 'execa';
3
+ import { Readable } from 'node:stream';
2
4
  import * as z from 'zod';
3
5
  import { AbstractOperation } from '../../../operations/index.js';
6
+ // Type guard for execa error objects
7
+ function isExecaError(error) {
8
+ return error instanceof Error && 'exitCode' in error;
9
+ }
4
10
  /**
5
11
  * https://docs.docker.com/reference/api/engine/version/v1.37/#tag/Exec/operation/ContainerExec
6
12
  */
@@ -27,20 +33,44 @@ export class ExecuteLocalCommandOperation extends AbstractOperation {
27
33
  this.out = out;
28
34
  }
29
35
  async _run(input) {
30
- const proc = input.interactive
31
- ? execa(input.script, {
32
- cwd: input.workingDir,
33
- shell: true,
34
- env: input.env,
35
- stdin: 'inherit',
36
- })
37
- : execa(input.script, {
36
+ try {
37
+ if (input.interactive) {
38
+ // For interactive mode, inherit all stdio streams so the child process
39
+ // has direct access to the terminal TTY. This allows interactive CLI tools
40
+ // (like ionic, npm, etc.) to detect TTY and show prompts.
41
+ await execa(input.script, {
42
+ cwd: input.workingDir,
43
+ shell: true,
44
+ env: input.env,
45
+ stdio: 'inherit',
46
+ });
47
+ // Return an empty stream for type compatibility - the caller won't use it
48
+ // since interactive mode outputs directly to the terminal
49
+ return new Readable({
50
+ read() {
51
+ this.push(null);
52
+ },
53
+ });
54
+ }
55
+ // Non-interactive mode: capture output
56
+ const proc = execa(input.script, {
38
57
  all: true,
39
58
  cwd: input.workingDir,
40
59
  shell: true,
41
60
  env: input.env,
42
61
  });
43
- proc.all?.pipe(this.out || process.stdout);
44
- return proc.all || proc.stdout;
62
+ // With all: true, proc.all is always defined
63
+ proc.all.pipe(this.out || process.stdout);
64
+ await proc;
65
+ return proc.all;
66
+ }
67
+ catch (error) {
68
+ if (isExecaError(error)) {
69
+ const stderr = error.stderr?.trim();
70
+ const message = stderr || error.shortMessage || error.message;
71
+ throw new CommandExecError(message, error.exitCode ?? 1, error.signal);
72
+ }
73
+ throw error;
74
+ }
45
75
  }
46
76
  }