@celilo/cli 0.1.5 → 0.1.7

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 (145) hide show
  1. package/drizzle/0004_caddy_hostname_list.sql +25 -0
  2. package/drizzle/meta/_journal.json +14 -0
  3. package/package.json +9 -2
  4. package/src/ansible/inventory.test.ts +3 -2
  5. package/src/ansible/inventory.ts +5 -1
  6. package/src/capabilities/public-web-helpers.test.ts +2 -2
  7. package/src/capabilities/public-web-publish.test.ts +34 -1
  8. package/src/cli/cli.test.ts +2 -2
  9. package/src/cli/command-registry.ts +146 -3
  10. package/src/cli/command-tree-parser.test.ts +1 -1
  11. package/src/cli/command-tree-parser.ts +9 -8
  12. package/src/cli/commands/hook-run.ts +15 -66
  13. package/src/cli/commands/module-audit.ts +14 -44
  14. package/src/cli/commands/module-deploy.ts +4 -1
  15. package/src/cli/commands/module-import-registry.test.ts +115 -0
  16. package/src/cli/commands/module-import.ts +106 -22
  17. package/src/cli/commands/module-publish.test.ts +235 -0
  18. package/src/cli/commands/module-publish.ts +234 -0
  19. package/src/cli/commands/module-remove.ts +82 -2
  20. package/src/cli/commands/module-search.ts +57 -0
  21. package/src/cli/commands/module-secret-get.ts +59 -0
  22. package/src/cli/commands/module-terraform-unlock.ts +57 -0
  23. package/src/cli/commands/module-verify.test.ts +59 -0
  24. package/src/cli/commands/module-verify.ts +53 -0
  25. package/src/cli/commands/status.ts +30 -20
  26. package/src/cli/commands/system-audit.test.ts +138 -0
  27. package/src/cli/commands/system-audit.ts +571 -0
  28. package/src/cli/commands/system-update.ts +391 -0
  29. package/src/cli/completion.ts +15 -1
  30. package/src/cli/fuel-gauge.ts +68 -3
  31. package/src/cli/generate-zsh-completion.ts +13 -3
  32. package/src/cli/index.ts +112 -5
  33. package/src/cli/parser.ts +11 -0
  34. package/src/cli/prompts.ts +36 -5
  35. package/src/cli/tui/audit-state.test.ts +246 -0
  36. package/src/cli/tui/audit-state.ts +525 -0
  37. package/src/cli/tui/audit-tui.test.tsx +135 -0
  38. package/src/cli/tui/audit-tui.tsx +624 -0
  39. package/src/cli/tui/celebration.tsx +29 -0
  40. package/src/cli/tui/clipboard.test.ts +94 -0
  41. package/src/cli/tui/clipboard.ts +101 -0
  42. package/src/cli/tui/icons.ts +22 -0
  43. package/src/cli/tui/keybar.tsx +65 -0
  44. package/src/cli/tui/keymap.test.ts +105 -0
  45. package/src/cli/tui/keymap.ts +70 -0
  46. package/src/cli/tui/modals/analyzing.tsx +75 -0
  47. package/src/cli/tui/modals/celebration.tsx +44 -0
  48. package/src/cli/tui/modals/reaudit-prompt.tsx +35 -0
  49. package/src/cli/tui/modals/remediate.tsx +44 -0
  50. package/src/cli/tui/modals.test.ts +137 -0
  51. package/src/cli/tui/mouse.test.ts +78 -0
  52. package/src/cli/tui/mouse.ts +114 -0
  53. package/src/cli/tui/panes/categories.tsx +62 -0
  54. package/src/cli/tui/panes/command-log.tsx +87 -0
  55. package/src/cli/tui/panes/detail.tsx +175 -0
  56. package/src/cli/tui/panes/findings.tsx +97 -0
  57. package/src/cli/tui/panes/summary.tsx +64 -0
  58. package/src/cli/tui/spawn.ts +130 -0
  59. package/src/cli/tui/theme.ts +42 -0
  60. package/src/cli/tui/wrap.test.ts +43 -0
  61. package/src/cli/tui/wrap.ts +45 -0
  62. package/src/cli/types.ts +5 -0
  63. package/src/db/client.ts +55 -2
  64. package/src/db/schema.ts +26 -17
  65. package/src/hooks/capability-loader.ts +133 -73
  66. package/src/hooks/define-hook.test.ts +9 -1
  67. package/src/hooks/executor.ts +22 -1
  68. package/src/hooks/load-hook-config.test.ts +165 -0
  69. package/src/hooks/load-hook-config.ts +60 -0
  70. package/src/hooks/logger.ts +42 -12
  71. package/src/hooks/run-named-hook.ts +128 -0
  72. package/src/hooks/types.ts +19 -0
  73. package/src/manifest/ensure-schema.test.ts +115 -0
  74. package/src/manifest/schema.ts +76 -0
  75. package/src/module/import.ts +20 -12
  76. package/src/module/packaging/build.ts +85 -16
  77. package/src/module/packaging/release-metadata.test.ts +103 -0
  78. package/src/module/packaging/release-metadata.ts +145 -0
  79. package/src/registry/client.test.ts +228 -0
  80. package/src/registry/client.ts +157 -0
  81. package/src/services/audit/backups.test.ts +233 -0
  82. package/src/services/audit/backups.ts +128 -0
  83. package/src/services/audit/capability-abi.test.ts +153 -0
  84. package/src/services/audit/capability-abi.ts +204 -0
  85. package/src/services/audit/cli-version.test.ts +60 -0
  86. package/src/services/audit/cli-version.ts +87 -0
  87. package/src/services/audit/health.test.ts +84 -0
  88. package/src/services/audit/health.ts +43 -0
  89. package/src/services/audit/index.test.ts +99 -0
  90. package/src/services/audit/index.ts +118 -0
  91. package/src/services/audit/machines-reachable.test.ts +87 -0
  92. package/src/services/audit/machines-reachable.ts +87 -0
  93. package/src/services/audit/module-configs.test.ts +131 -0
  94. package/src/services/audit/module-configs.ts +80 -0
  95. package/src/services/audit/module-versions.test.ts +99 -0
  96. package/src/services/audit/module-versions.ts +154 -0
  97. package/src/services/audit/schema.test.ts +68 -0
  98. package/src/services/audit/schema.ts +115 -0
  99. package/src/services/audit/secrets-decryptable.test.ts +82 -0
  100. package/src/services/audit/secrets-decryptable.ts +97 -0
  101. package/src/services/audit/services-credentials.test.ts +54 -0
  102. package/src/services/audit/services-credentials.ts +64 -0
  103. package/src/services/audit/services-reachable.test.ts +60 -0
  104. package/src/services/audit/services-reachable.ts +64 -0
  105. package/src/services/audit/terraform-plan.test.ts +127 -0
  106. package/src/services/audit/terraform-plan.ts +153 -0
  107. package/src/services/audit/types.test.ts +36 -0
  108. package/src/services/audit/types.ts +90 -0
  109. package/src/services/audit/unconfigured-modules.test.ts +48 -0
  110. package/src/services/audit/unconfigured-modules.ts +71 -0
  111. package/src/services/audit/undeployed-modules.test.ts +66 -0
  112. package/src/services/audit/undeployed-modules.ts +72 -0
  113. package/src/services/build-stream.ts +122 -122
  114. package/src/services/config-interview.ts +407 -2
  115. package/src/services/deploy-ansible.ts +73 -7
  116. package/src/services/deploy-preflight.ts +45 -4
  117. package/src/services/deploy-terraform.ts +31 -24
  118. package/src/services/deploy-validation.ts +167 -23
  119. package/src/services/ensure-interview.test.ts +245 -0
  120. package/src/services/health-runner.ts +110 -38
  121. package/src/services/module-build.ts +11 -13
  122. package/src/services/module-deploy.ts +370 -59
  123. package/src/services/ssh-key-manager.test.ts +1 -1
  124. package/src/services/ssh-key-manager.ts +3 -2
  125. package/src/services/terraform-env.ts +62 -0
  126. package/src/services/update/dep-graph.test.ts +214 -0
  127. package/src/services/update/dep-graph.ts +215 -0
  128. package/src/services/update/orchestrator.test.ts +463 -0
  129. package/src/services/update/orchestrator.ts +359 -0
  130. package/src/services/update/progress.ts +49 -0
  131. package/src/services/update/self-update.test.ts +68 -0
  132. package/src/services/update/self-update.ts +57 -0
  133. package/src/services/update/types.ts +94 -0
  134. package/src/templates/generator.test.ts +1 -1
  135. package/src/templates/generator.ts +42 -1
  136. package/src/test-utils/completion-harness.test.ts +1 -1
  137. package/src/test-utils/completion-harness.ts +4 -4
  138. package/src/variables/capability-self-ref.test.ts +203 -0
  139. package/src/variables/context.ts +49 -1
  140. package/src/variables/declarative-derivation.test.ts +306 -0
  141. package/src/variables/declarative-derivation.ts +4 -2
  142. package/src/variables/parser.test.ts +56 -1
  143. package/src/variables/parser.ts +47 -6
  144. package/src/variables/resolver.ts +27 -9
  145. 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?: (line: string) => void;
17
- title?: string; // Custom title for progress indicator (defaults to "Building module")
18
- noInteractive?: boolean; // Skip animation, stream output directly
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; // True if user pressed ESC to background
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
- * Execute build command with fuel-gauge progress indicator
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
- export async function executeBuildWithProgress(
42
- options: BuildStreamOptions,
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
- // Skip animation in tests/non-TTY or when --no-interactive
61
- const skipAnimation = !process.stdout.isTTY || !!noInteractive;
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
- // Spawn child process
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 (child && !child.killed) {
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; // Don't process output after backgrounding
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
- // In non-interactive mode, stream directly to stdout
113
- if (skipAnimation) {
114
- process.stdout.write(text);
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
- if (onOutput) {
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; // Don't process output after backgrounding
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
- // Handle completion
155
- child.on('close', (exitCode: number | null) => {
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 || 1,
178
- output: output + errorOutput,
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; // Already resolved when backgrounded
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
+ }