@hominis/fireforge 0.13.2 → 0.15.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.
- package/CHANGELOG.md +85 -0
- package/README.md +20 -1
- package/dist/bin/fireforge.js +19 -5
- package/dist/src/commands/config.js +7 -1
- package/dist/src/commands/discard.js +6 -1
- package/dist/src/commands/doctor.d.ts +12 -0
- package/dist/src/commands/doctor.js +6 -1
- package/dist/src/commands/download.js +106 -7
- package/dist/src/commands/export-shared.js +7 -0
- package/dist/src/commands/export.js +5 -0
- package/dist/src/commands/furnace/apply.js +147 -47
- package/dist/src/commands/furnace/create-templates.d.ts +26 -0
- package/dist/src/commands/furnace/create-templates.js +86 -0
- package/dist/src/commands/furnace/create.js +77 -103
- package/dist/src/commands/furnace/deploy.js +20 -5
- package/dist/src/commands/furnace/diff.js +3 -1
- package/dist/src/commands/furnace/init.js +25 -7
- package/dist/src/commands/furnace/list.js +15 -7
- package/dist/src/commands/furnace/override.js +47 -15
- package/dist/src/commands/furnace/remove.js +68 -20
- package/dist/src/commands/furnace/rename.js +31 -3
- package/dist/src/commands/furnace/scan.js +8 -0
- package/dist/src/commands/furnace/validate.js +70 -7
- package/dist/src/commands/import.js +65 -11
- package/dist/src/commands/re-export.js +11 -4
- package/dist/src/commands/rebase/abort.js +26 -14
- package/dist/src/commands/rebase/confirm.d.ts +15 -2
- package/dist/src/commands/rebase/confirm.js +2 -2
- package/dist/src/commands/rebase/continue.js +39 -15
- package/dist/src/commands/rebase/index.js +2 -1
- package/dist/src/commands/rebase/patch-loop.js +90 -33
- package/dist/src/commands/register.js +13 -0
- package/dist/src/commands/resolve.js +31 -10
- package/dist/src/commands/run.js +9 -44
- package/dist/src/commands/setup-support.js +25 -7
- package/dist/src/commands/status.js +59 -8
- package/dist/src/commands/test.js +33 -7
- package/dist/src/commands/token.js +11 -1
- package/dist/src/commands/watch.js +51 -1
- package/dist/src/commands/wire.js +23 -0
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate.js +47 -1
- package/dist/src/core/furnace-apply-ftl.d.ts +33 -0
- package/dist/src/core/furnace-apply-ftl.js +102 -0
- package/dist/src/core/furnace-apply-helpers.d.ts +10 -1
- package/dist/src/core/furnace-apply-helpers.js +16 -12
- package/dist/src/core/furnace-apply.js +7 -4
- package/dist/src/core/furnace-config-tokens.d.ts +11 -0
- package/dist/src/core/furnace-config-tokens.js +28 -0
- package/dist/src/core/furnace-config.d.ts +6 -0
- package/dist/src/core/furnace-config.js +8 -1
- package/dist/src/core/furnace-constants.d.ts +20 -0
- package/dist/src/core/furnace-constants.js +32 -0
- package/dist/src/core/furnace-registration-ast.d.ts +13 -1
- package/dist/src/core/furnace-registration-ast.js +58 -25
- package/dist/src/core/furnace-registration.d.ts +28 -1
- package/dist/src/core/furnace-registration.js +98 -1
- package/dist/src/core/furnace-staleness.d.ts +17 -0
- package/dist/src/core/furnace-staleness.js +58 -0
- package/dist/src/core/furnace-validate-accessibility.js +8 -2
- package/dist/src/core/furnace-validate-helpers.d.ts +8 -0
- package/dist/src/core/furnace-validate-helpers.js +81 -0
- package/dist/src/core/furnace-validate-registration.d.ts +8 -2
- package/dist/src/core/furnace-validate-registration.js +34 -9
- package/dist/src/core/furnace-validate.js +2 -2
- package/dist/src/core/marionette-preflight.d.ts +39 -0
- package/dist/src/core/marionette-preflight.js +210 -0
- package/dist/src/core/signal-critical.d.ts +49 -0
- package/dist/src/core/signal-critical.js +80 -0
- package/dist/src/errors/download.d.ts +1 -1
- package/dist/src/errors/download.js +6 -3
- package/dist/src/types/commands/options.d.ts +6 -0
- package/dist/src/types/config.d.ts +7 -0
- package/dist/src/types/furnace.d.ts +8 -0
- package/dist/src/utils/process.d.ts +15 -2
- package/dist/src/utils/process.js +73 -0
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal-deferred critical sections.
|
|
3
|
+
*
|
|
4
|
+
* Commands that perform a compound mutation (e.g. "apply a patch to the
|
|
5
|
+
* engine, then persist progress to a session file on disk") need to finish
|
|
6
|
+
* the pair atomically with respect to SIGINT / SIGTERM. The furnace rollback
|
|
7
|
+
* mechanism is not the right tool here: rebase-style operations intentionally
|
|
8
|
+
* leave the engine mutated and only need the on-disk bookkeeping write to
|
|
9
|
+
* complete before the process exits.
|
|
10
|
+
*
|
|
11
|
+
* `runInSignalCriticalSection(fn)` wraps a short body in a registry slot.
|
|
12
|
+
* While the body runs, the CLI entry point's SIGINT / SIGTERM handlers wait
|
|
13
|
+
* for the slot to clear before calling `process.exit`, so a signal that
|
|
14
|
+
* lands mid-body is held until the body's state write finishes.
|
|
15
|
+
*
|
|
16
|
+
* This module is a pure runtime registry — it installs no signal handlers
|
|
17
|
+
* itself. The bin entry point is responsible for awaiting
|
|
18
|
+
* `waitForActiveCriticalSections` before terminating.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Runs `fn` inside a signal-deferred critical section. The CLI entry point's
|
|
22
|
+
* signal handlers `await` every active section before exiting, so a SIGINT or
|
|
23
|
+
* SIGTERM that arrives during `fn` will hold exit until `fn` returns (or
|
|
24
|
+
* rejects).
|
|
25
|
+
*
|
|
26
|
+
* `fn` should be short — anything that takes longer than the bounded wait in
|
|
27
|
+
* the bin handler (`SIGNAL_CRITICAL_SECTION_TIMEOUT_MS`) will time out and
|
|
28
|
+
* the handler will exit anyway. The intent is "guard the apply + state
|
|
29
|
+
* persist pair," not "postpone exit indefinitely."
|
|
30
|
+
*/
|
|
31
|
+
export declare function runInSignalCriticalSection<T>(label: string, fn: () => Promise<T>): Promise<T>;
|
|
32
|
+
/**
|
|
33
|
+
* Returns true while any critical section is currently running. Used by the
|
|
34
|
+
* bin entry point's signal handler to decide whether to await before exit.
|
|
35
|
+
*/
|
|
36
|
+
export declare function hasActiveCriticalSection(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Waits for every active critical section to complete or for `timeoutMs` to
|
|
39
|
+
* elapse, whichever comes first. Never rejects: a section that throws still
|
|
40
|
+
* resolves from the registry's perspective because `runInSignalCriticalSection`
|
|
41
|
+
* cleans up in `finally`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function waitForActiveCriticalSections(timeoutMs: number): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Test-only helper: clears the critical-section registry. Production code
|
|
46
|
+
* must never call this — it voids the exit-ordering guarantee for any
|
|
47
|
+
* section still in flight.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resetCriticalSectionsForTests(): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Signal-deferred critical sections.
|
|
4
|
+
*
|
|
5
|
+
* Commands that perform a compound mutation (e.g. "apply a patch to the
|
|
6
|
+
* engine, then persist progress to a session file on disk") need to finish
|
|
7
|
+
* the pair atomically with respect to SIGINT / SIGTERM. The furnace rollback
|
|
8
|
+
* mechanism is not the right tool here: rebase-style operations intentionally
|
|
9
|
+
* leave the engine mutated and only need the on-disk bookkeeping write to
|
|
10
|
+
* complete before the process exits.
|
|
11
|
+
*
|
|
12
|
+
* `runInSignalCriticalSection(fn)` wraps a short body in a registry slot.
|
|
13
|
+
* While the body runs, the CLI entry point's SIGINT / SIGTERM handlers wait
|
|
14
|
+
* for the slot to clear before calling `process.exit`, so a signal that
|
|
15
|
+
* lands mid-body is held until the body's state write finishes.
|
|
16
|
+
*
|
|
17
|
+
* This module is a pure runtime registry — it installs no signal handlers
|
|
18
|
+
* itself. The bin entry point is responsible for awaiting
|
|
19
|
+
* `waitForActiveCriticalSections` before terminating.
|
|
20
|
+
*/
|
|
21
|
+
const activeSections = new Set();
|
|
22
|
+
/**
|
|
23
|
+
* Runs `fn` inside a signal-deferred critical section. The CLI entry point's
|
|
24
|
+
* signal handlers `await` every active section before exiting, so a SIGINT or
|
|
25
|
+
* SIGTERM that arrives during `fn` will hold exit until `fn` returns (or
|
|
26
|
+
* rejects).
|
|
27
|
+
*
|
|
28
|
+
* `fn` should be short — anything that takes longer than the bounded wait in
|
|
29
|
+
* the bin handler (`SIGNAL_CRITICAL_SECTION_TIMEOUT_MS`) will time out and
|
|
30
|
+
* the handler will exit anyway. The intent is "guard the apply + state
|
|
31
|
+
* persist pair," not "postpone exit indefinitely."
|
|
32
|
+
*/
|
|
33
|
+
export async function runInSignalCriticalSection(label, fn) {
|
|
34
|
+
let resolver = () => undefined;
|
|
35
|
+
const section = {
|
|
36
|
+
label,
|
|
37
|
+
promise: new Promise((resolve) => {
|
|
38
|
+
resolver = resolve;
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
activeSections.add(section);
|
|
42
|
+
try {
|
|
43
|
+
return await fn();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
activeSections.delete(section);
|
|
47
|
+
resolver();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns true while any critical section is currently running. Used by the
|
|
52
|
+
* bin entry point's signal handler to decide whether to await before exit.
|
|
53
|
+
*/
|
|
54
|
+
export function hasActiveCriticalSection() {
|
|
55
|
+
return activeSections.size > 0;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Waits for every active critical section to complete or for `timeoutMs` to
|
|
59
|
+
* elapse, whichever comes first. Never rejects: a section that throws still
|
|
60
|
+
* resolves from the registry's perspective because `runInSignalCriticalSection`
|
|
61
|
+
* cleans up in `finally`.
|
|
62
|
+
*/
|
|
63
|
+
export async function waitForActiveCriticalSections(timeoutMs) {
|
|
64
|
+
if (activeSections.size === 0)
|
|
65
|
+
return;
|
|
66
|
+
const snapshot = [...activeSections].map((s) => s.promise);
|
|
67
|
+
await Promise.race([
|
|
68
|
+
Promise.allSettled(snapshot).then(() => undefined),
|
|
69
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Test-only helper: clears the critical-section registry. Production code
|
|
74
|
+
* must never call this — it voids the exit-ordering guarantee for any
|
|
75
|
+
* section still in flight.
|
|
76
|
+
*/
|
|
77
|
+
export function resetCriticalSectionsForTests() {
|
|
78
|
+
activeSections.clear();
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=signal-critical.js.map
|
|
@@ -37,6 +37,6 @@ export declare class EngineExistsError extends DownloadError {
|
|
|
37
37
|
*/
|
|
38
38
|
export declare class PartialEngineExistsError extends DownloadError {
|
|
39
39
|
readonly enginePath: string;
|
|
40
|
-
constructor(enginePath: string);
|
|
40
|
+
constructor(enginePath: string, cause?: Error);
|
|
41
41
|
get userMessage(): string;
|
|
42
42
|
}
|
|
@@ -80,16 +80,19 @@ export class EngineExistsError extends DownloadError {
|
|
|
80
80
|
*/
|
|
81
81
|
export class PartialEngineExistsError extends DownloadError {
|
|
82
82
|
enginePath;
|
|
83
|
-
constructor(enginePath) {
|
|
84
|
-
super(`Engine directory contains a partially initialized checkout: ${enginePath}
|
|
83
|
+
constructor(enginePath, cause) {
|
|
84
|
+
super(`Engine directory contains a partially initialized checkout: ${enginePath}`, undefined, cause);
|
|
85
85
|
this.enginePath = enginePath;
|
|
86
86
|
}
|
|
87
87
|
get userMessage() {
|
|
88
|
+
const causeMessage = this.cause instanceof Error && this.cause.message ? this.cause.message : undefined;
|
|
88
89
|
return (`Download Error: Firefox source exists, but the baseline git repository was not fully initialized.\n\n` +
|
|
89
90
|
`Path: ${this.enginePath}\n\n` +
|
|
91
|
+
(causeMessage ? `Underlying cause: ${causeMessage}\n\n` : '') +
|
|
90
92
|
'To fix this:\n' +
|
|
91
93
|
' 1. Re-run "fireforge download --force" to recreate the baseline repository\n' +
|
|
92
|
-
' 2. Or manually delete the engine/ directory before downloading again'
|
|
94
|
+
' 2. Or manually delete the engine/ directory before downloading again\n' +
|
|
95
|
+
' 3. Re-run with --verbose for the full underlying error and stack trace');
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
//# sourceMappingURL=download.js.map
|
|
@@ -178,6 +178,12 @@ export interface TestOptions {
|
|
|
178
178
|
headless?: boolean;
|
|
179
179
|
/** Run incremental UI build before testing */
|
|
180
180
|
build?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Run a marionette preflight before tests. Reports PASS/FAIL in under a
|
|
183
|
+
* minute. When test paths are supplied, a FAIL aborts before mach test is
|
|
184
|
+
* spawned. When no paths are supplied, runs the preflight only and exits.
|
|
185
|
+
*/
|
|
186
|
+
doctor?: boolean;
|
|
181
187
|
}
|
|
182
188
|
/**
|
|
183
189
|
* Options for the furnace apply command.
|
|
@@ -44,6 +44,13 @@ export interface FireForgeConfig {
|
|
|
44
44
|
wire?: WireConfig;
|
|
45
45
|
/** Patch lint configuration */
|
|
46
46
|
patchLint?: PatchLintConfig;
|
|
47
|
+
/**
|
|
48
|
+
* Project marker prefix appended to lines FireForge writes into
|
|
49
|
+
* upstream Firefox source files (e.g. the `customElements.js` tag list).
|
|
50
|
+
* `"HOMINIS"` emits a trailing ` // HOMINIS:` on each inserted line.
|
|
51
|
+
* Keeps modifications discoverable and re-applies idempotent.
|
|
52
|
+
*/
|
|
53
|
+
markerComment?: string;
|
|
47
54
|
}
|
|
48
55
|
/**
|
|
49
56
|
* Wire command configuration.
|
|
@@ -63,6 +63,14 @@ export interface FurnaceConfig {
|
|
|
63
63
|
tokenPrefix?: string;
|
|
64
64
|
/** Custom properties allowed even though they don't match tokenPrefix (e.g. ["--background-color-box"]) */
|
|
65
65
|
tokenAllowlist?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* Chrome documents scanned by the `missing-token-link` validator to confirm
|
|
68
|
+
* the tokens CSS file is `<link>`ed. Forks with multiple chrome host
|
|
69
|
+
* documents (e.g. `hominis.xhtml` beside the stock `browser.xhtml`) should
|
|
70
|
+
* list every document that may own the link. When omitted, defaults to
|
|
71
|
+
* `['browser/base/content/browser.xhtml']` — the upstream Firefox path.
|
|
72
|
+
*/
|
|
73
|
+
tokenHostDocuments?: string[];
|
|
66
74
|
/**
|
|
67
75
|
* Override the default Fluent (.ftl) base path within the engine.
|
|
68
76
|
* Defaults to `toolkit/locales/en-US/toolkit/global` when not set.
|
|
@@ -51,12 +51,23 @@ export interface StreamOptions extends ExecOptions {
|
|
|
51
51
|
export declare function execStream(command: string, args: string[], options?: StreamOptions): Promise<number>;
|
|
52
52
|
/**
|
|
53
53
|
* Executes a command and inherits stdio (shows output directly).
|
|
54
|
+
*
|
|
55
|
+
* Graceful shutdown: when the FireForge process receives SIGINT/SIGTERM, the
|
|
56
|
+
* signal is forwarded to the child as SIGTERM and a short grace timer (default
|
|
57
|
+
* 1500ms) runs before escalating to SIGKILL. A second matching signal during
|
|
58
|
+
* the grace period triggers an immediate SIGKILL — matching the usual
|
|
59
|
+
* "hit Ctrl-C again to force-quit" UX. Without this, Firefox's AsyncShutdown
|
|
60
|
+
* / profileBeforeChange blockers (which flush in-memory state to disk) can be
|
|
61
|
+
* racing the OS child-exit path, losing the last few seconds of edits.
|
|
62
|
+
*
|
|
54
63
|
* @param command - Command to execute
|
|
55
64
|
* @param args - Command arguments
|
|
56
65
|
* @param options - Execution options
|
|
57
66
|
* @returns Exit code of the process
|
|
58
67
|
*/
|
|
59
|
-
export declare function execInherit(command: string, args: string[], options?: ExecOptions
|
|
68
|
+
export declare function execInherit(command: string, args: string[], options?: ExecOptions & {
|
|
69
|
+
shutdownGraceMs?: number;
|
|
70
|
+
}): Promise<number>;
|
|
60
71
|
/**
|
|
61
72
|
* Executes a command while inheriting stdin, streaming stdout/stderr live,
|
|
62
73
|
* and capturing the emitted output for diagnostics.
|
|
@@ -65,7 +76,9 @@ export declare function execInherit(command: string, args: string[], options?: E
|
|
|
65
76
|
* @param options - Execution options
|
|
66
77
|
* @returns Execution result with stdout, stderr, and exit code
|
|
67
78
|
*/
|
|
68
|
-
export declare function execInheritCapture(command: string, args: string[], options?: ExecOptions
|
|
79
|
+
export declare function execInheritCapture(command: string, args: string[], options?: ExecOptions & {
|
|
80
|
+
shutdownGraceMs?: number;
|
|
81
|
+
}): Promise<ExecResult>;
|
|
69
82
|
/**
|
|
70
83
|
* Finds an executable in the system PATH.
|
|
71
84
|
* @param name - Name of the executable
|
|
@@ -108,6 +108,15 @@ export async function execStream(command, args, options = {}) {
|
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
110
|
* Executes a command and inherits stdio (shows output directly).
|
|
111
|
+
*
|
|
112
|
+
* Graceful shutdown: when the FireForge process receives SIGINT/SIGTERM, the
|
|
113
|
+
* signal is forwarded to the child as SIGTERM and a short grace timer (default
|
|
114
|
+
* 1500ms) runs before escalating to SIGKILL. A second matching signal during
|
|
115
|
+
* the grace period triggers an immediate SIGKILL — matching the usual
|
|
116
|
+
* "hit Ctrl-C again to force-quit" UX. Without this, Firefox's AsyncShutdown
|
|
117
|
+
* / profileBeforeChange blockers (which flush in-memory state to disk) can be
|
|
118
|
+
* racing the OS child-exit path, losing the last few seconds of edits.
|
|
119
|
+
*
|
|
111
120
|
* @param command - Command to execute
|
|
112
121
|
* @param args - Command arguments
|
|
113
122
|
* @param options - Execution options
|
|
@@ -121,14 +130,74 @@ export async function execInherit(command, args, options = {}) {
|
|
|
121
130
|
stdio: 'inherit',
|
|
122
131
|
signal: buildSignalFromTimeout(options.timeout),
|
|
123
132
|
});
|
|
133
|
+
const graceMs = options.shutdownGraceMs ?? 1500;
|
|
134
|
+
const { dispose } = installGracefulShutdownForwarder(child, graceMs);
|
|
124
135
|
child.on('error', (error) => {
|
|
136
|
+
dispose();
|
|
125
137
|
reject(error);
|
|
126
138
|
});
|
|
127
139
|
child.on('close', (code, signal) => {
|
|
140
|
+
dispose();
|
|
128
141
|
resolve(exitCodeFromClose(code, signal));
|
|
129
142
|
});
|
|
130
143
|
});
|
|
131
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Wires parent-process SIGINT/SIGTERM to a child: first signal → child.kill
|
|
147
|
+
* (SIGTERM) + grace timer; second matching signal → immediate SIGKILL; grace
|
|
148
|
+
* timer expiry → SIGKILL. Returns a `dispose()` that clears the listeners and
|
|
149
|
+
* any outstanding timer. Callers must invoke `dispose()` from both the child's
|
|
150
|
+
* `close` and `error` handlers so the process does not accumulate signal
|
|
151
|
+
* listeners across repeated spawns.
|
|
152
|
+
*/
|
|
153
|
+
function installGracefulShutdownForwarder(child, graceMs) {
|
|
154
|
+
let graceTimer;
|
|
155
|
+
const forwarded = new Set();
|
|
156
|
+
const escalate = () => {
|
|
157
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
158
|
+
return;
|
|
159
|
+
try {
|
|
160
|
+
child.kill('SIGKILL');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Child is already gone — nothing to do.
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const handleSignal = (signal) => {
|
|
167
|
+
if (forwarded.has(signal)) {
|
|
168
|
+
// Second receipt of the same signal while still running: escalate now.
|
|
169
|
+
escalate();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
forwarded.add(signal);
|
|
173
|
+
try {
|
|
174
|
+
child.kill('SIGTERM');
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// If the child can't accept SIGTERM (already dead), nothing to do.
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
graceTimer = setTimeout(escalate, graceMs);
|
|
181
|
+
graceTimer.unref();
|
|
182
|
+
};
|
|
183
|
+
const onSigint = () => {
|
|
184
|
+
handleSignal('SIGINT');
|
|
185
|
+
};
|
|
186
|
+
const onSigterm = () => {
|
|
187
|
+
handleSignal('SIGTERM');
|
|
188
|
+
};
|
|
189
|
+
process.on('SIGINT', onSigint);
|
|
190
|
+
process.on('SIGTERM', onSigterm);
|
|
191
|
+
const dispose = () => {
|
|
192
|
+
process.off('SIGINT', onSigint);
|
|
193
|
+
process.off('SIGTERM', onSigterm);
|
|
194
|
+
if (graceTimer) {
|
|
195
|
+
clearTimeout(graceTimer);
|
|
196
|
+
graceTimer = undefined;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
return { dispose };
|
|
200
|
+
}
|
|
132
201
|
/**
|
|
133
202
|
* Executes a command while inheriting stdin, streaming stdout/stderr live,
|
|
134
203
|
* and capturing the emitted output for diagnostics.
|
|
@@ -149,10 +218,14 @@ export async function execInheritCapture(command, args, options = {}) {
|
|
|
149
218
|
const err = createStreamCollector(process.stderr);
|
|
150
219
|
child.stdout.on('data', out.onData);
|
|
151
220
|
child.stderr.on('data', err.onData);
|
|
221
|
+
const graceMs = options.shutdownGraceMs ?? 1500;
|
|
222
|
+
const { dispose } = installGracefulShutdownForwarder(child, graceMs);
|
|
152
223
|
child.on('error', (error) => {
|
|
224
|
+
dispose();
|
|
153
225
|
reject(error);
|
|
154
226
|
});
|
|
155
227
|
child.on('close', (code, signal) => {
|
|
228
|
+
dispose();
|
|
156
229
|
resolve({
|
|
157
230
|
stdout: out.getText(),
|
|
158
231
|
stderr: err.getText(),
|