@celilo/cli 0.1.5 → 0.1.6
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.
- package/drizzle/0004_caddy_hostname_list.sql +25 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +9 -2
- package/src/ansible/inventory.test.ts +3 -2
- package/src/ansible/inventory.ts +5 -1
- package/src/capabilities/public-web-helpers.test.ts +2 -2
- package/src/capabilities/public-web-publish.test.ts +34 -1
- package/src/cli/cli.test.ts +2 -2
- package/src/cli/command-registry.ts +146 -3
- package/src/cli/command-tree-parser.test.ts +1 -1
- package/src/cli/command-tree-parser.ts +9 -8
- package/src/cli/commands/hook-run.ts +15 -66
- package/src/cli/commands/module-audit.ts +14 -44
- package/src/cli/commands/module-deploy.ts +4 -1
- package/src/cli/commands/module-import-registry.test.ts +115 -0
- package/src/cli/commands/module-import.ts +106 -22
- package/src/cli/commands/module-publish.test.ts +235 -0
- package/src/cli/commands/module-publish.ts +234 -0
- package/src/cli/commands/module-remove.ts +82 -2
- package/src/cli/commands/module-search.ts +57 -0
- package/src/cli/commands/module-secret-get.ts +59 -0
- package/src/cli/commands/module-terraform-unlock.ts +57 -0
- package/src/cli/commands/module-verify.test.ts +59 -0
- package/src/cli/commands/module-verify.ts +53 -0
- package/src/cli/commands/status.ts +30 -20
- package/src/cli/commands/system-audit.test.ts +138 -0
- package/src/cli/commands/system-audit.ts +571 -0
- package/src/cli/commands/system-update.ts +391 -0
- package/src/cli/completion.ts +15 -1
- package/src/cli/fuel-gauge.ts +68 -3
- package/src/cli/generate-zsh-completion.ts +13 -3
- package/src/cli/index.ts +112 -5
- package/src/cli/parser.ts +11 -0
- package/src/cli/prompts.ts +36 -5
- package/src/cli/tui/audit-state.test.ts +246 -0
- package/src/cli/tui/audit-state.ts +525 -0
- package/src/cli/tui/audit-tui.test.tsx +135 -0
- package/src/cli/tui/audit-tui.tsx +624 -0
- package/src/cli/tui/celebration.tsx +29 -0
- package/src/cli/tui/clipboard.test.ts +94 -0
- package/src/cli/tui/clipboard.ts +101 -0
- package/src/cli/tui/icons.ts +22 -0
- package/src/cli/tui/keybar.tsx +65 -0
- package/src/cli/tui/keymap.test.ts +105 -0
- package/src/cli/tui/keymap.ts +70 -0
- package/src/cli/tui/modals/analyzing.tsx +75 -0
- package/src/cli/tui/modals/celebration.tsx +44 -0
- package/src/cli/tui/modals/reaudit-prompt.tsx +35 -0
- package/src/cli/tui/modals/remediate.tsx +44 -0
- package/src/cli/tui/modals.test.ts +137 -0
- package/src/cli/tui/mouse.test.ts +78 -0
- package/src/cli/tui/mouse.ts +114 -0
- package/src/cli/tui/panes/categories.tsx +62 -0
- package/src/cli/tui/panes/command-log.tsx +87 -0
- package/src/cli/tui/panes/detail.tsx +175 -0
- package/src/cli/tui/panes/findings.tsx +97 -0
- package/src/cli/tui/panes/summary.tsx +64 -0
- package/src/cli/tui/spawn.ts +130 -0
- package/src/cli/tui/theme.ts +42 -0
- package/src/cli/tui/wrap.test.ts +43 -0
- package/src/cli/tui/wrap.ts +45 -0
- package/src/cli/types.ts +5 -0
- package/src/db/client.ts +55 -2
- package/src/db/schema.ts +26 -17
- package/src/hooks/capability-loader.ts +133 -73
- package/src/hooks/define-hook.test.ts +9 -1
- package/src/hooks/executor.ts +22 -1
- package/src/hooks/load-hook-config.test.ts +165 -0
- package/src/hooks/load-hook-config.ts +60 -0
- package/src/hooks/logger.ts +42 -12
- package/src/hooks/run-named-hook.ts +128 -0
- package/src/hooks/types.ts +19 -0
- package/src/manifest/ensure-schema.test.ts +115 -0
- package/src/manifest/schema.ts +76 -0
- package/src/module/import.ts +20 -12
- package/src/module/packaging/build.ts +85 -16
- package/src/module/packaging/release-metadata.test.ts +103 -0
- package/src/module/packaging/release-metadata.ts +145 -0
- package/src/registry/client.test.ts +228 -0
- package/src/registry/client.ts +157 -0
- package/src/services/audit/backups.test.ts +233 -0
- package/src/services/audit/backups.ts +128 -0
- package/src/services/audit/capability-abi.test.ts +153 -0
- package/src/services/audit/capability-abi.ts +204 -0
- package/src/services/audit/cli-version.test.ts +60 -0
- package/src/services/audit/cli-version.ts +87 -0
- package/src/services/audit/health.test.ts +84 -0
- package/src/services/audit/health.ts +43 -0
- package/src/services/audit/index.test.ts +99 -0
- package/src/services/audit/index.ts +118 -0
- package/src/services/audit/machines-reachable.test.ts +87 -0
- package/src/services/audit/machines-reachable.ts +87 -0
- package/src/services/audit/module-configs.test.ts +131 -0
- package/src/services/audit/module-configs.ts +80 -0
- package/src/services/audit/module-versions.test.ts +99 -0
- package/src/services/audit/module-versions.ts +154 -0
- package/src/services/audit/schema.test.ts +68 -0
- package/src/services/audit/schema.ts +115 -0
- package/src/services/audit/secrets-decryptable.test.ts +82 -0
- package/src/services/audit/secrets-decryptable.ts +97 -0
- package/src/services/audit/services-credentials.test.ts +54 -0
- package/src/services/audit/services-credentials.ts +64 -0
- package/src/services/audit/services-reachable.test.ts +60 -0
- package/src/services/audit/services-reachable.ts +64 -0
- package/src/services/audit/terraform-plan.test.ts +127 -0
- package/src/services/audit/terraform-plan.ts +153 -0
- package/src/services/audit/types.test.ts +36 -0
- package/src/services/audit/types.ts +90 -0
- package/src/services/audit/unconfigured-modules.test.ts +48 -0
- package/src/services/audit/unconfigured-modules.ts +71 -0
- package/src/services/audit/undeployed-modules.test.ts +66 -0
- package/src/services/audit/undeployed-modules.ts +72 -0
- package/src/services/build-stream.ts +122 -122
- package/src/services/config-interview.ts +407 -2
- package/src/services/deploy-ansible.ts +73 -7
- package/src/services/deploy-preflight.ts +45 -4
- package/src/services/deploy-terraform.ts +31 -24
- package/src/services/deploy-validation.ts +167 -23
- package/src/services/ensure-interview.test.ts +245 -0
- package/src/services/health-runner.ts +110 -38
- package/src/services/module-build.ts +11 -13
- package/src/services/module-deploy.ts +370 -59
- package/src/services/ssh-key-manager.test.ts +1 -1
- package/src/services/ssh-key-manager.ts +3 -2
- package/src/services/terraform-env.ts +62 -0
- package/src/services/update/dep-graph.test.ts +214 -0
- package/src/services/update/dep-graph.ts +215 -0
- package/src/services/update/orchestrator.test.ts +463 -0
- package/src/services/update/orchestrator.ts +359 -0
- package/src/services/update/progress.ts +49 -0
- package/src/services/update/self-update.test.ts +68 -0
- package/src/services/update/self-update.ts +57 -0
- package/src/services/update/types.ts +94 -0
- package/src/templates/generator.test.ts +1 -1
- package/src/templates/generator.ts +42 -1
- package/src/test-utils/completion-harness.test.ts +1 -1
- package/src/test-utils/completion-harness.ts +4 -4
- package/src/variables/capability-self-ref.test.ts +203 -0
- package/src/variables/context.ts +49 -1
- package/src/variables/declarative-derivation.test.ts +306 -0
- package/src/variables/declarative-derivation.ts +4 -2
- package/src/variables/parser.test.ts +56 -1
- package/src/variables/parser.ts +47 -6
- package/src/variables/resolver.ts +27 -9
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { auditUndeployedModules } from './undeployed-modules';
|
|
3
|
+
|
|
4
|
+
describe('auditUndeployedModules', () => {
|
|
5
|
+
test('skips INSTALLED and VERIFIED modules', async () => {
|
|
6
|
+
const result = await auditUndeployedModules({
|
|
7
|
+
modules: [
|
|
8
|
+
{ id: 'caddy', state: 'INSTALLED' },
|
|
9
|
+
{ id: 'iptables', state: 'VERIFIED' },
|
|
10
|
+
],
|
|
11
|
+
});
|
|
12
|
+
expect(result).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('flags IMPORTED modules with actionable deploy command', async () => {
|
|
16
|
+
const result = await auditUndeployedModules({
|
|
17
|
+
modules: [{ id: 'authentik', state: 'IMPORTED' }],
|
|
18
|
+
});
|
|
19
|
+
expect(result).toHaveLength(1);
|
|
20
|
+
expect(result[0]).toMatchObject({
|
|
21
|
+
category: 'undeployed_modules',
|
|
22
|
+
severity: 'drift',
|
|
23
|
+
code: 'module_undeployed',
|
|
24
|
+
remediation: 'celilo module deploy authentik',
|
|
25
|
+
actionable: true,
|
|
26
|
+
subject: 'authentik',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('also flags VALIDATED, CONFIGURED', async () => {
|
|
31
|
+
const result = await auditUndeployedModules({
|
|
32
|
+
modules: [
|
|
33
|
+
{ id: 'a', state: 'VALIDATED' },
|
|
34
|
+
{ id: 'b', state: 'CONFIGURED' },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
expect(result).toHaveLength(2);
|
|
38
|
+
expect(result.every((f) => f.code === 'module_undeployed')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('skips transient states (DEPLOYING, GENERATING, UNINSTALLING)', async () => {
|
|
42
|
+
const result = await auditUndeployedModules({
|
|
43
|
+
modules: [
|
|
44
|
+
{ id: 'a', state: 'DEPLOYING' },
|
|
45
|
+
{ id: 'b', state: 'GENERATING' },
|
|
46
|
+
{ id: 'c', state: 'UNINSTALLING' },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
expect(result).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('ERROR state surfaces as blocked, non-actionable, with investigation guidance', async () => {
|
|
53
|
+
const result = await auditUndeployedModules({
|
|
54
|
+
modules: [{ id: 'broken', state: 'ERROR' }],
|
|
55
|
+
});
|
|
56
|
+
expect(result).toHaveLength(1);
|
|
57
|
+
expect(result[0]).toMatchObject({
|
|
58
|
+
category: 'undeployed_modules',
|
|
59
|
+
severity: 'blocked',
|
|
60
|
+
code: 'module_in_error_state',
|
|
61
|
+
actionable: false,
|
|
62
|
+
subject: 'broken',
|
|
63
|
+
});
|
|
64
|
+
expect(result[0].details).toContain('Investigate');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undeployed-modules check.
|
|
3
|
+
*
|
|
4
|
+
* Modules in the local DB whose state isn't INSTALLED or VERIFIED
|
|
5
|
+
* have been imported (and possibly configured / generated) but
|
|
6
|
+
* never made it to a running deployment. The audit surfaces them
|
|
7
|
+
* one finding per module with a runnable `celilo module deploy <id>`
|
|
8
|
+
* remediation so the user can deploy directly from the TUI.
|
|
9
|
+
*
|
|
10
|
+
* `ERROR` and `UNINSTALLING` states are reported separately — the
|
|
11
|
+
* remediation isn't simply "deploy"; the user has to investigate
|
|
12
|
+
* the prior failure first. We still flag them so they aren't
|
|
13
|
+
* forgotten.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { DriftFinding } from './types';
|
|
17
|
+
|
|
18
|
+
export interface UndeployedModule {
|
|
19
|
+
id: string;
|
|
20
|
+
/** Lifecycle state from the modules table (IMPORTED, VALIDATED, …). */
|
|
21
|
+
state: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UndeployedModulesAuditDeps {
|
|
25
|
+
modules: UndeployedModule[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const READY_STATES = new Set(['INSTALLED', 'VERIFIED']);
|
|
29
|
+
|
|
30
|
+
/** States that should NOT be flagged here — they're either deployed or in transit. */
|
|
31
|
+
const TRANSIENT_STATES = new Set(['DEPLOYING', 'GENERATING', 'UNINSTALLING']);
|
|
32
|
+
|
|
33
|
+
export async function auditUndeployedModules(
|
|
34
|
+
deps: UndeployedModulesAuditDeps,
|
|
35
|
+
): Promise<DriftFinding[]> {
|
|
36
|
+
const findings: DriftFinding[] = [];
|
|
37
|
+
|
|
38
|
+
for (const m of deps.modules) {
|
|
39
|
+
if (READY_STATES.has(m.state)) continue;
|
|
40
|
+
if (TRANSIENT_STATES.has(m.state)) continue;
|
|
41
|
+
|
|
42
|
+
if (m.state === 'ERROR') {
|
|
43
|
+
findings.push({
|
|
44
|
+
category: 'undeployed_modules',
|
|
45
|
+
severity: 'blocked',
|
|
46
|
+
code: 'module_in_error_state',
|
|
47
|
+
message: `${m.id}: previous deploy failed (state: ERROR)`,
|
|
48
|
+
details:
|
|
49
|
+
'Investigate the prior failure (check `celilo module status` and the' +
|
|
50
|
+
' module logs) before retrying — re-deploying without diagnosing the' +
|
|
51
|
+
' root cause may recreate the same broken state.',
|
|
52
|
+
// Investigatory; not actionable as a one-liner.
|
|
53
|
+
actionable: false,
|
|
54
|
+
subject: m.id,
|
|
55
|
+
remediation: `celilo module status ${m.id}`,
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
findings.push({
|
|
61
|
+
category: 'undeployed_modules',
|
|
62
|
+
severity: 'drift',
|
|
63
|
+
code: 'module_undeployed',
|
|
64
|
+
message: `${m.id}: imported but not deployed (state: ${m.state})`,
|
|
65
|
+
remediation: `celilo module deploy ${m.id}`,
|
|
66
|
+
actionable: true,
|
|
67
|
+
subject: m.id,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return findings;
|
|
72
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build Stream Executor
|
|
3
3
|
*
|
|
4
|
-
* Executes build commands with streaming output and fuel-gauge progress indicator
|
|
4
|
+
* Executes build commands with streaming output and fuel-gauge progress indicator.
|
|
5
|
+
* Interactive (TTY) mode: FuelGauge animation with scrolling preview.
|
|
6
|
+
* Non-interactive mode: raw streaming to stdout/stderr, no FuelGauge instantiated.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { spawn } from 'node:child_process';
|
|
@@ -13,9 +15,11 @@ export interface BuildStreamOptions {
|
|
|
13
15
|
cwd: string;
|
|
14
16
|
env?: Record<string, string>;
|
|
15
17
|
stdin?: string;
|
|
16
|
-
onOutput?: (
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
onOutput?: (chunk: string) => void;
|
|
19
|
+
/** Transform each output line before display. Return null to suppress. Raw output is always captured for logs. */
|
|
20
|
+
filterOutput?: (line: string) => string | null;
|
|
21
|
+
title?: string;
|
|
22
|
+
noInteractive?: boolean;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export interface BuildStreamResult {
|
|
@@ -23,179 +27,175 @@ export interface BuildStreamResult {
|
|
|
23
27
|
exitCode: number;
|
|
24
28
|
output: string;
|
|
25
29
|
error?: string;
|
|
26
|
-
backgrounded?: boolean;
|
|
30
|
+
backgrounded?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function spawnChild(options: BuildStreamOptions) {
|
|
34
|
+
const { command, args, cwd, env, stdin } = options;
|
|
35
|
+
const child = spawn(command, args, {
|
|
36
|
+
cwd,
|
|
37
|
+
env: { ...process.env, ...env },
|
|
38
|
+
stdio: [stdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
39
|
+
shell: true,
|
|
40
|
+
});
|
|
41
|
+
if (stdin && child.stdin) {
|
|
42
|
+
child.stdin.write(stdin);
|
|
43
|
+
child.stdin.end();
|
|
44
|
+
}
|
|
45
|
+
return child;
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* Features:
|
|
33
|
-
* - Cylon-style back-and-forth progress bar
|
|
34
|
-
* - Scrolling output preview (3-4 lines)
|
|
35
|
-
* - On success: clean terminal
|
|
36
|
-
* - On error: show last 8 lines of output
|
|
37
|
-
*
|
|
38
|
-
* @param options - Build execution options
|
|
39
|
-
* @returns Build result with success status and output
|
|
49
|
+
* Non-interactive path: no FuelGauge, raw streaming to stdout/stderr.
|
|
50
|
+
* onOutput is still called so callers can emit structured markers.
|
|
40
51
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
): Promise<BuildStreamResult> {
|
|
44
|
-
const {
|
|
45
|
-
command,
|
|
46
|
-
args,
|
|
47
|
-
cwd,
|
|
48
|
-
env,
|
|
49
|
-
stdin,
|
|
50
|
-
onOutput,
|
|
51
|
-
title = 'Building module',
|
|
52
|
-
noInteractive,
|
|
53
|
-
} = options;
|
|
52
|
+
function executeStreaming(options: BuildStreamOptions): Promise<BuildStreamResult> {
|
|
53
|
+
const { onOutput, filterOutput } = options;
|
|
54
54
|
|
|
55
55
|
return new Promise((resolve) => {
|
|
56
56
|
const outputLines: string[] = [];
|
|
57
57
|
const errorLines: string[] = [];
|
|
58
|
-
let backgrounded = false;
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const child = spawnChild(options);
|
|
60
|
+
|
|
61
|
+
const sigintHandler = () => {
|
|
62
|
+
if (!child.killed) child.kill('SIGINT');
|
|
63
|
+
};
|
|
64
|
+
process.on('SIGINT', sigintHandler);
|
|
65
|
+
|
|
66
|
+
if (child.stdout) {
|
|
67
|
+
child.stdout.on('data', (data: Buffer) => {
|
|
68
|
+
const text = data.toString();
|
|
69
|
+
outputLines.push(text);
|
|
70
|
+
|
|
71
|
+
if (filterOutput) {
|
|
72
|
+
for (const line of text.split('\n')) {
|
|
73
|
+
if (!line.trim()) continue;
|
|
74
|
+
const display = filterOutput(line);
|
|
75
|
+
if (display !== null) process.stdout.write(`${display}\n`);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
process.stdout.write(text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onOutput?.(text);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (child.stderr) {
|
|
86
|
+
child.stderr.on('data', (data: Buffer) => {
|
|
87
|
+
const text = data.toString();
|
|
88
|
+
errorLines.push(text);
|
|
89
|
+
process.stderr.write(text);
|
|
90
|
+
onOutput?.(text);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
child.on('close', (exitCode) => {
|
|
95
|
+
process.off('SIGINT', sigintHandler);
|
|
96
|
+
const output = outputLines.join('') + errorLines.join('');
|
|
97
|
+
if (exitCode === 0) {
|
|
98
|
+
resolve({ success: true, exitCode: 0, output });
|
|
99
|
+
} else {
|
|
100
|
+
resolve({
|
|
101
|
+
success: false,
|
|
102
|
+
exitCode: exitCode ?? 1,
|
|
103
|
+
output,
|
|
104
|
+
error: `Build exited with code ${exitCode}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
child.on('error', (error) => {
|
|
110
|
+
process.off('SIGINT', sigintHandler);
|
|
111
|
+
resolve({ success: false, exitCode: 1, output: outputLines.join(''), error: error.message });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Interactive path: FuelGauge animation with Cylon-style progress bar.
|
|
118
|
+
*/
|
|
119
|
+
function executeWithGauge(options: BuildStreamOptions): Promise<BuildStreamResult> {
|
|
120
|
+
const { onOutput, filterOutput, title = 'Building module' } = options;
|
|
121
|
+
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const outputLines: string[] = [];
|
|
124
|
+
const errorLines: string[] = [];
|
|
125
|
+
let backgrounded = false;
|
|
62
126
|
|
|
63
|
-
// Start fuel-gauge progress indicator
|
|
64
127
|
const gauge = new FuelGauge(title, {
|
|
65
|
-
skipAnimation, // Skip animation in tests/non-TTY/no-interactive
|
|
66
128
|
onBackground: () => {
|
|
67
|
-
// User pressed ESC - return control immediately
|
|
68
129
|
backgrounded = true;
|
|
69
|
-
// Clean up signal handler when backgrounding
|
|
70
130
|
process.off('SIGINT', sigintHandler);
|
|
71
|
-
resolve({
|
|
72
|
-
success: true,
|
|
73
|
-
exitCode: 0,
|
|
74
|
-
output: '',
|
|
75
|
-
backgrounded: true,
|
|
76
|
-
});
|
|
131
|
+
resolve({ success: true, exitCode: 0, output: '', backgrounded: true });
|
|
77
132
|
},
|
|
78
133
|
});
|
|
79
134
|
gauge.start();
|
|
80
135
|
|
|
81
|
-
|
|
82
|
-
// Use shell: true to support script wrappers (e.g., tfenv's terraform script)
|
|
83
|
-
const child = spawn(command, args, {
|
|
84
|
-
cwd,
|
|
85
|
-
env: { ...process.env, ...env },
|
|
86
|
-
stdio: [stdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
87
|
-
shell: true,
|
|
88
|
-
});
|
|
136
|
+
const child = spawnChild(options);
|
|
89
137
|
|
|
90
|
-
// Forward SIGINT (Ctrl+C) to child process
|
|
91
138
|
const sigintHandler = () => {
|
|
92
|
-
if (
|
|
93
|
-
child.kill('SIGINT');
|
|
94
|
-
}
|
|
139
|
+
if (!child.killed) child.kill('SIGINT');
|
|
95
140
|
};
|
|
96
141
|
process.on('SIGINT', sigintHandler);
|
|
97
142
|
|
|
98
|
-
// Pass stdin data if provided
|
|
99
|
-
if (stdin && child.stdin) {
|
|
100
|
-
child.stdin.write(stdin);
|
|
101
|
-
child.stdin.end();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Capture stdout - feed to fuel-gauge for display
|
|
105
143
|
if (child.stdout) {
|
|
106
|
-
child.stdout.on('data', (data) => {
|
|
107
|
-
if (backgrounded) return;
|
|
108
|
-
|
|
144
|
+
child.stdout.on('data', (data: Buffer) => {
|
|
145
|
+
if (backgrounded) return;
|
|
109
146
|
const text = data.toString();
|
|
110
147
|
outputLines.push(text);
|
|
111
148
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Feed lines to fuel-gauge for scrolling display
|
|
118
|
-
const lines = text.split('\n').filter((l: string) => l.trim());
|
|
119
|
-
for (const line of lines) {
|
|
120
|
-
gauge.addOutput(line);
|
|
149
|
+
for (const line of text.split('\n')) {
|
|
150
|
+
if (!line.trim()) continue;
|
|
151
|
+
const display = filterOutput ? filterOutput(line) : line;
|
|
152
|
+
if (display !== null) gauge.addOutput(display);
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
|
|
124
|
-
onOutput(text);
|
|
125
|
-
}
|
|
155
|
+
onOutput?.(text);
|
|
126
156
|
});
|
|
127
157
|
}
|
|
128
158
|
|
|
129
|
-
// Capture stderr - also feed to fuel-gauge
|
|
130
159
|
if (child.stderr) {
|
|
131
|
-
child.stderr.on('data', (data) => {
|
|
132
|
-
if (backgrounded) return;
|
|
133
|
-
|
|
160
|
+
child.stderr.on('data', (data: Buffer) => {
|
|
161
|
+
if (backgrounded) return;
|
|
134
162
|
const text = data.toString();
|
|
135
163
|
errorLines.push(text);
|
|
136
|
-
|
|
137
|
-
// In non-interactive mode, stream directly to stderr
|
|
138
|
-
if (skipAnimation) {
|
|
139
|
-
process.stderr.write(text);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Show stderr in fuel-gauge too
|
|
143
|
-
const lines = text.split('\n').filter((l: string) => l.trim());
|
|
144
|
-
for (const line of lines) {
|
|
164
|
+
for (const line of text.split('\n').filter((l: string) => l.trim())) {
|
|
145
165
|
gauge.addOutput(line);
|
|
146
166
|
}
|
|
147
|
-
|
|
148
|
-
if (onOutput) {
|
|
149
|
-
onOutput(text);
|
|
150
|
-
}
|
|
167
|
+
onOutput?.(text);
|
|
151
168
|
});
|
|
152
169
|
}
|
|
153
170
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (backgrounded) return; // Already resolved when backgrounded
|
|
157
|
-
|
|
158
|
-
// Clean up signal handler
|
|
171
|
+
child.on('close', (exitCode) => {
|
|
172
|
+
if (backgrounded) return;
|
|
159
173
|
process.off('SIGINT', sigintHandler);
|
|
160
|
-
|
|
161
|
-
const output = outputLines.join('');
|
|
162
|
-
const errorOutput = errorLines.join('');
|
|
163
|
-
|
|
174
|
+
const output = outputLines.join('') + errorLines.join('');
|
|
164
175
|
if (exitCode === 0) {
|
|
165
|
-
// Success: clean terminal, no output left behind
|
|
166
176
|
gauge.stop(true);
|
|
167
|
-
resolve({
|
|
168
|
-
success: true,
|
|
169
|
-
exitCode: exitCode || 0,
|
|
170
|
-
output: output + errorOutput,
|
|
171
|
-
});
|
|
177
|
+
resolve({ success: true, exitCode: 0, output });
|
|
172
178
|
} else {
|
|
173
|
-
// Error: show full output for debugging
|
|
174
179
|
gauge.stop(false);
|
|
175
180
|
resolve({
|
|
176
181
|
success: false,
|
|
177
|
-
exitCode: exitCode
|
|
178
|
-
output
|
|
182
|
+
exitCode: exitCode ?? 1,
|
|
183
|
+
output,
|
|
179
184
|
error: `Build exited with code ${exitCode}`,
|
|
180
185
|
});
|
|
181
186
|
}
|
|
182
187
|
});
|
|
183
188
|
|
|
184
|
-
// Handle spawn errors
|
|
185
189
|
child.on('error', (error) => {
|
|
186
|
-
if (backgrounded) return;
|
|
187
|
-
|
|
188
|
-
// Clean up signal handler
|
|
190
|
+
if (backgrounded) return;
|
|
189
191
|
process.off('SIGINT', sigintHandler);
|
|
190
|
-
|
|
191
|
-
// Error: show output for debugging
|
|
192
192
|
gauge.stop(false);
|
|
193
|
-
resolve({
|
|
194
|
-
success: false,
|
|
195
|
-
exitCode: 1,
|
|
196
|
-
output: outputLines.join(''),
|
|
197
|
-
error: error.message,
|
|
198
|
-
});
|
|
193
|
+
resolve({ success: false, exitCode: 1, output: outputLines.join(''), error: error.message });
|
|
199
194
|
});
|
|
200
195
|
});
|
|
201
196
|
}
|
|
197
|
+
|
|
198
|
+
export function executeBuildWithProgress(options: BuildStreamOptions): Promise<BuildStreamResult> {
|
|
199
|
+
const nonInteractive = !process.stdout.isTTY || !!options.noInteractive;
|
|
200
|
+
return nonInteractive ? executeStreaming(options) : executeWithGauge(options);
|
|
201
|
+
}
|