@bo-agent/pwsh-local 0.0.2
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/lib/.tsbuildinfo +1 -0
- package/lib/index.d.ts +128 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +344 -0
- package/lib/index.js.map +1 -0
- package/lib/resolve.d.ts +28 -0
- package/lib/resolve.d.ts.map +1 -0
- package/lib/resolve.js +76 -0
- package/lib/resolve.js.map +1 -0
- package/lib/timeout.d.ts +61 -0
- package/lib/timeout.d.ts.map +1 -0
- package/lib/timeout.js +92 -0
- package/lib/timeout.js.map +1 -0
- package/package.json +25 -0
- package/src/index.ts +351 -0
- package/src/resolve.ts +79 -0
- package/src/timeout.ts +112 -0
- package/tsconfig.json +39 -0
package/lib/resolve.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PowerShell executable resolution, dependency-free so non-package consumers
|
|
3
|
+
* (the repository's coverage-gate probe in `vitest.config.ts`) can share the
|
|
4
|
+
* ONE resolution definition with the executor and its suites — a probe that
|
|
5
|
+
* resolved differently from the code under test could exempt a file whose
|
|
6
|
+
* suites actually run.
|
|
7
|
+
*
|
|
8
|
+
* @module @bo-agent/pwsh-local/resolve
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Well-known Windows PowerShell install locations plus PATH entries, newest
|
|
12
|
+
* first. Explicitly parameterized (env) so resolution is a pure function of
|
|
13
|
+
* its inputs on every platform.
|
|
14
|
+
* @param env - the environment to probe; defaults to the process environment.
|
|
15
|
+
* @returns candidate `pwsh` executable paths in resolution order.
|
|
16
|
+
*/
|
|
17
|
+
export declare function candidatePwshPaths(env?: NodeJS.ProcessEnv): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the pwsh executable this executor spawns.
|
|
20
|
+
* @param configured - an explicit `pwshPath` config value, trusted as-is.
|
|
21
|
+
* @param env - the environment to probe on Windows; defaults to the process environment.
|
|
22
|
+
* @param platform - the platform to resolve for; defaults to the process platform.
|
|
23
|
+
* @returns the first existing well-known location on Windows (PowerShell 7
|
|
24
|
+
* install, a PATH entry such as the Microsoft Store install, then Windows
|
|
25
|
+
* PowerShell 5.1), else `pwsh` for PATH resolution.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolvePwshPath(configured?: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string;
|
|
28
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,EAAE,CAgBjF;AAqBD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,UAAU,CAAC,EAAE,MAAM,EACnB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,MAAM,CAQR"}
|
package/lib/resolve.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PowerShell executable resolution, dependency-free so non-package consumers
|
|
3
|
+
* (the repository's coverage-gate probe in `vitest.config.ts`) can share the
|
|
4
|
+
* ONE resolution definition with the executor and its suites — a probe that
|
|
5
|
+
* resolved differently from the code under test could exempt a file whose
|
|
6
|
+
* suites actually run.
|
|
7
|
+
*
|
|
8
|
+
* @module @bo-agent/pwsh-local/resolve
|
|
9
|
+
*/
|
|
10
|
+
import { lstatSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
/**
|
|
13
|
+
* Well-known Windows PowerShell install locations plus PATH entries, newest
|
|
14
|
+
* first. Explicitly parameterized (env) so resolution is a pure function of
|
|
15
|
+
* its inputs on every platform.
|
|
16
|
+
* @param env - the environment to probe; defaults to the process environment.
|
|
17
|
+
* @returns candidate `pwsh` executable paths in resolution order.
|
|
18
|
+
*/
|
|
19
|
+
export function candidatePwshPaths(env = process.env) {
|
|
20
|
+
const programFiles = env.ProgramFiles ?? 'C:\\Program Files';
|
|
21
|
+
const systemRoot = env.SystemRoot ?? 'C:\\Windows';
|
|
22
|
+
const candidates = [
|
|
23
|
+
join(programFiles, 'PowerShell', '7', 'pwsh.exe'),
|
|
24
|
+
];
|
|
25
|
+
// Microsoft Store installs (and any user-added location) live on PATH;
|
|
26
|
+
// entries may carry surrounding quotes from `setx`-style definitions.
|
|
27
|
+
for (const entry of (env.PATH ?? '').split(';')) {
|
|
28
|
+
const trimmed = entry.trim().replace(/^"|"$/g, '');
|
|
29
|
+
if (trimmed.length === 0)
|
|
30
|
+
continue;
|
|
31
|
+
candidates.push(join(trimmed, 'pwsh.exe'));
|
|
32
|
+
}
|
|
33
|
+
// Windows PowerShell 5.1 remains the last-resort fallback on legacy hosts.
|
|
34
|
+
candidates.push(join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'));
|
|
35
|
+
return candidates;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether a candidate can be spawned. lstat opens the entry itself instead of
|
|
39
|
+
* following reparse points, so it sees the Store app execution alias where
|
|
40
|
+
* stat hits the target's ACL (EACCES); Node reports that alias as a symlink
|
|
41
|
+
* on current releases and as a plain file on older ones, and CreateProcess
|
|
42
|
+
* resolves either shape. A real directory never matches.
|
|
43
|
+
*/
|
|
44
|
+
function candidateExists(candidate) {
|
|
45
|
+
try {
|
|
46
|
+
const stat = lstatSync(candidate);
|
|
47
|
+
return stat.isFile() || stat.isSymbolicLink();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// ENOENT (the candidate vanished between listing and probing) is the only
|
|
51
|
+
// expected failure; any other error names an unspawnable path, so false
|
|
52
|
+
// is the safe answer for it too.
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the pwsh executable this executor spawns.
|
|
58
|
+
* @param configured - an explicit `pwshPath` config value, trusted as-is.
|
|
59
|
+
* @param env - the environment to probe on Windows; defaults to the process environment.
|
|
60
|
+
* @param platform - the platform to resolve for; defaults to the process platform.
|
|
61
|
+
* @returns the first existing well-known location on Windows (PowerShell 7
|
|
62
|
+
* install, a PATH entry such as the Microsoft Store install, then Windows
|
|
63
|
+
* PowerShell 5.1), else `pwsh` for PATH resolution.
|
|
64
|
+
*/
|
|
65
|
+
export function resolvePwshPath(configured, env = process.env, platform = process.platform) {
|
|
66
|
+
if (configured !== undefined && configured.length > 0)
|
|
67
|
+
return configured;
|
|
68
|
+
if (platform === 'win32') {
|
|
69
|
+
for (const candidate of candidatePwshPaths(env)) {
|
|
70
|
+
if (candidateExists(candidate))
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return 'pwsh';
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACrE,MAAM,YAAY,GAAG,GAAG,CAAC,YAAY,IAAI,mBAAmB,CAAA;IAC5D,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,IAAI,aAAa,CAAA;IAClD,MAAM,UAAU,GAAG;QACjB,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,CAAC;KAClD,CAAA;IACD,uEAAuE;IACvE,sEAAsE;IACtE,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAChD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;QAClD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,SAAQ;QAClC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAA;IAC5C,CAAC;IACD,2EAA2E;IAC3E,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAA;IAC5F,OAAO,UAAU,CAAA;AACnB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,SAAiB;IACxC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,CAAA;QACjC,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,cAAc,EAAE,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;QAC1E,wEAAwE;QACxE,iCAAiC;QACjC,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC7B,UAAmB,EACnB,MAAyB,OAAO,CAAC,GAAG,EACpC,WAA4B,OAAO,CAAC,QAAQ;IAE5C,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,UAAU,CAAA;IACxE,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,KAAK,MAAM,SAAS,IAAI,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;YAChD,IAAI,eAAe,CAAC,SAAS,CAAC;gBAAE,OAAO,SAAS,CAAA;QAClD,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/lib/timeout.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal timeout + deadline primitives used by the pwsh-local executor.
|
|
3
|
+
* Inlined here (rather than imported from a separate `@bo-agent/timeout`
|
|
4
|
+
* package) to keep the dependency surface small — same pattern as
|
|
5
|
+
* `@bo-agent/bash-local`.
|
|
6
|
+
*
|
|
7
|
+
* @module @bo-agent/pwsh-local/timeout
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Largest value `setTimeout` can express as a 32-bit signed milliseconds
|
|
11
|
+
* delay. Anything larger would wrap to a near-immediate fire. Inlined to
|
|
12
|
+
* mirror Node's `TIMEOUT_MAX` and to keep the dependency surface small.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MAX_TIMER_DELAY_MS = 2147483647;
|
|
15
|
+
/**
|
|
16
|
+
* Clamp a per-call value against a configured range. The default applies
|
|
17
|
+
* first; the maximum caps any explicit caller-supplied value.
|
|
18
|
+
* @param value - the requested value (may be undefined).
|
|
19
|
+
* @param defaultValue - the value used when none is requested.
|
|
20
|
+
* @param maxValue - the upper bound for explicit values.
|
|
21
|
+
* @param name - label prefixed to thrown errors.
|
|
22
|
+
* @returns a positive finite value within [1, maxValue].
|
|
23
|
+
* @throws Error when neither default nor requested value is a positive finite
|
|
24
|
+
* number, or when maxValue itself is invalid.
|
|
25
|
+
*/
|
|
26
|
+
export declare function clampTimeout(value: number | undefined, defaultValue: number, maxValue: number, name: string): number;
|
|
27
|
+
/** A symbol tagged on the AbortSignal's reason so the timeout reason is identifiable. */
|
|
28
|
+
export declare const TIMEOUT_REASON: unique symbol;
|
|
29
|
+
/**
|
|
30
|
+
* Combine a caller's optional AbortSignal with a millisecond timeout into a
|
|
31
|
+
* single controller. Either side aborts the controller; disposal clears the
|
|
32
|
+
* timer and removes the upstream listener, so the result is safe to discard
|
|
33
|
+
* at end of scope via the `using` declaration.
|
|
34
|
+
*/
|
|
35
|
+
export interface DeadlineHandle {
|
|
36
|
+
readonly signal: AbortSignal;
|
|
37
|
+
[Symbol.dispose](): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a deadline fused from the caller's signal and the executor's
|
|
41
|
+
* timeout. The returned controller aborts on whichever fires first; a
|
|
42
|
+
* successful command observes `signal.aborted === false`. The timeout
|
|
43
|
+
* reason is tagged so callers can tell timeout apart from upstream
|
|
44
|
+
* cancellation.
|
|
45
|
+
* @param signal - caller's upstream signal; may be undefined.
|
|
46
|
+
* @param timeoutMs - milliseconds until the timeout branch aborts.
|
|
47
|
+
* @param reason - human-readable label for the timeout reason; only used
|
|
48
|
+
* for tagging, never surfaced to the user.
|
|
49
|
+
*/
|
|
50
|
+
export declare function deadline(signal: AbortSignal | undefined, timeoutMs: number, reason: string): DeadlineHandle;
|
|
51
|
+
/**
|
|
52
|
+
* Return the tagged reason if the controller aborted because of the timeout
|
|
53
|
+
* branch (or upstream cancellation that mirrored it). Other abort reasons
|
|
54
|
+
* resolve as the caller's own cancellation; undefined means the controller
|
|
55
|
+
* is still live.
|
|
56
|
+
* @param signal - a controller's signal after a wait.
|
|
57
|
+
* @param expected - the label passed to {@link deadline}; matched against
|
|
58
|
+
* the symbol tag, so the timeout reason identifies itself.
|
|
59
|
+
*/
|
|
60
|
+
export declare function timeoutOf(signal: AbortSignal, _expected: string): symbol | undefined;
|
|
61
|
+
//# sourceMappingURL=timeout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../src/timeout.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,aAAgB,CAAA;AAE/C;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,MAAM,CASR;AAED,yFAAyF;AACzF,eAAO,MAAM,cAAc,eAAoD,CAAA;AAE/E;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,cAAc,CAwBhB;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIpF"}
|
package/lib/timeout.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal timeout + deadline primitives used by the pwsh-local executor.
|
|
3
|
+
* Inlined here (rather than imported from a separate `@bo-agent/timeout`
|
|
4
|
+
* package) to keep the dependency surface small — same pattern as
|
|
5
|
+
* `@bo-agent/bash-local`.
|
|
6
|
+
*
|
|
7
|
+
* @module @bo-agent/pwsh-local/timeout
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Largest value `setTimeout` can express as a 32-bit signed milliseconds
|
|
11
|
+
* delay. Anything larger would wrap to a near-immediate fire. Inlined to
|
|
12
|
+
* mirror Node's `TIMEOUT_MAX` and to keep the dependency surface small.
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
15
|
+
/**
|
|
16
|
+
* Clamp a per-call value against a configured range. The default applies
|
|
17
|
+
* first; the maximum caps any explicit caller-supplied value.
|
|
18
|
+
* @param value - the requested value (may be undefined).
|
|
19
|
+
* @param defaultValue - the value used when none is requested.
|
|
20
|
+
* @param maxValue - the upper bound for explicit values.
|
|
21
|
+
* @param name - label prefixed to thrown errors.
|
|
22
|
+
* @returns a positive finite value within [1, maxValue].
|
|
23
|
+
* @throws Error when neither default nor requested value is a positive finite
|
|
24
|
+
* number, or when maxValue itself is invalid.
|
|
25
|
+
*/
|
|
26
|
+
export function clampTimeout(value, defaultValue, maxValue, name) {
|
|
27
|
+
if (!Number.isFinite(maxValue) || maxValue <= 0) {
|
|
28
|
+
throw new Error(`${name}: maxValue must be a positive finite number`);
|
|
29
|
+
}
|
|
30
|
+
const candidate = value ?? defaultValue;
|
|
31
|
+
if (!Number.isFinite(candidate) || candidate <= 0) {
|
|
32
|
+
throw new Error(`${name}: value must be a positive finite number`);
|
|
33
|
+
}
|
|
34
|
+
return Math.min(candidate, maxValue);
|
|
35
|
+
}
|
|
36
|
+
/** A symbol tagged on the AbortSignal's reason so the timeout reason is identifiable. */
|
|
37
|
+
export const TIMEOUT_REASON = Symbol.for('@bo-agent/pwsh-local/TIMEOUT_REASON');
|
|
38
|
+
/**
|
|
39
|
+
* Build a deadline fused from the caller's signal and the executor's
|
|
40
|
+
* timeout. The returned controller aborts on whichever fires first; a
|
|
41
|
+
* successful command observes `signal.aborted === false`. The timeout
|
|
42
|
+
* reason is tagged so callers can tell timeout apart from upstream
|
|
43
|
+
* cancellation.
|
|
44
|
+
* @param signal - caller's upstream signal; may be undefined.
|
|
45
|
+
* @param timeoutMs - milliseconds until the timeout branch aborts.
|
|
46
|
+
* @param reason - human-readable label for the timeout reason; only used
|
|
47
|
+
* for tagging, never surfaced to the user.
|
|
48
|
+
*/
|
|
49
|
+
export function deadline(signal, timeoutMs, reason) {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
52
|
+
if (signal !== undefined) {
|
|
53
|
+
if (signal.aborted) {
|
|
54
|
+
controller.abort(signal.reason);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// `setTimeout` cannot express delays above MAX_TIMER_DELAY_MS without
|
|
61
|
+
// wrapping; cap the request so the timeout branch actually fires when
|
|
62
|
+
// promised.
|
|
63
|
+
const safeTimeout = Math.min(timeoutMs, MAX_TIMER_DELAY_MS);
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
controller.abort(TIMEOUT_REASON);
|
|
66
|
+
}, safeTimeout);
|
|
67
|
+
return {
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
[Symbol.dispose]() {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
if (signal !== undefined)
|
|
72
|
+
signal.removeEventListener('abort', onAbort);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Return the tagged reason if the controller aborted because of the timeout
|
|
78
|
+
* branch (or upstream cancellation that mirrored it). Other abort reasons
|
|
79
|
+
* resolve as the caller's own cancellation; undefined means the controller
|
|
80
|
+
* is still live.
|
|
81
|
+
* @param signal - a controller's signal after a wait.
|
|
82
|
+
* @param expected - the label passed to {@link deadline}; matched against
|
|
83
|
+
* the symbol tag, so the timeout reason identifies itself.
|
|
84
|
+
*/
|
|
85
|
+
export function timeoutOf(signal, _expected) {
|
|
86
|
+
if (!signal.aborted)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (signal.reason === TIMEOUT_REASON)
|
|
89
|
+
return TIMEOUT_REASON;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=timeout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.js","sourceRoot":"","sources":["../src/timeout.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,aAAa,CAAA;AAE/C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAyB,EACzB,YAAoB,EACpB,QAAgB,EAChB,IAAY;IAEZ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,6CAA6C,CAAC,CAAA;IACvE,CAAC;IACD,MAAM,SAAS,GAAG,KAAK,IAAI,YAAY,CAAA;IACvC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,0CAA0C,CAAC,CAAA;IACpE,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;AACtC,CAAC;AAED,yFAAyF;AACzF,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAA;AAa/E;;;;;;;;;;GAUG;AACH,MAAM,UAAU,QAAQ,CACtB,MAA+B,EAC/B,SAAiB,EACjB,MAAc;IAEd,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;IACxC,MAAM,OAAO,GAAG,GAAS,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACjC,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3D,CAAC;IACH,CAAC;IACD,sEAAsE;IACtE,sEAAsE;IACtE,YAAY;IACZ,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAA;IAC3D,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,UAAU,CAAC,KAAK,CAAC,cAAc,CAAC,CAAA;IAClC,CAAC,EAAE,WAAW,CAAC,CAAA;IACf,OAAO;QACL,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,CAAC,MAAM,CAAC,OAAO,CAAC;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACxE,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAC,MAAmB,EAAE,SAAiB;IAC9D,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,SAAS,CAAA;IACrC,IAAI,MAAM,CAAC,MAAM,KAAK,cAAc;QAAE,OAAO,cAAc,CAAA;IAC3D,OAAO,SAAS,CAAA;AAClB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bo-agent/pwsh-local",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./lib/index.js",
|
|
6
|
+
"types": "./lib/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"import": "./lib/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./resolve": {
|
|
13
|
+
"types": "./lib/resolve.d.ts",
|
|
14
|
+
"import": "./lib/resolve.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"cordis": "4.0.0-rc.10",
|
|
19
|
+
"schemastery": "3.18.0",
|
|
20
|
+
"@bo-agent/inline": "0.0.2",
|
|
21
|
+
"@bo-agent/shell": "0.0.2",
|
|
22
|
+
"@bo-agent/subprocess": "0.0.2"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local PowerShell Service Provider for the pwsh capability seam. Each
|
|
3
|
+
* command runs as `pwsh -NoLogo -NoProfile -NonInteractive -Command
|
|
4
|
+
* <command>` in a managed process spawned through `ctx.subprocess`; the
|
|
5
|
+
* executor owns command defaulting, deadlines and cause classification, the
|
|
6
|
+
* model-friendly terminal environment, and the model-facing stdout/stderr
|
|
7
|
+
* merge for background reads.
|
|
8
|
+
*
|
|
9
|
+
* The command string is passed as ONE argv element to `-Command`: PowerShell
|
|
10
|
+
* itself parses the text, and no intermediate shell exists, so there is no
|
|
11
|
+
* shell-quoting layer to escape (the `bash -c` string domain has no
|
|
12
|
+
* equivalent here). Native Win32 paths (`C:\...`) pass through unchanged.
|
|
13
|
+
*
|
|
14
|
+
* @module @bo-agent/pwsh-local
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/* jscpd:ignore-start -- this executor mirrors bash-local call-for-call by
|
|
18
|
+
design (see upstream README), so the two import the same seam surface */
|
|
19
|
+
import { Context } from 'cordis'
|
|
20
|
+
import z from 'schemastery'
|
|
21
|
+
import { ShellExecutor } from '@bo-agent/shell'
|
|
22
|
+
import type { ShellExecRequest, ShellExecSpec, ShellProcess, ShellProcessRead, ShellRunResult, CollectedOutput } from '@bo-agent/shell'
|
|
23
|
+
import type { SubprocessCollect, SubprocessHandle, SubprocessOutputReader, SubprocessSpawnSpec } from '@bo-agent/subprocess'
|
|
24
|
+
import { clampTimeout, deadline, MAX_TIMER_DELAY_MS, timeoutOf } from './timeout.ts'
|
|
25
|
+
/* jscpd:ignore-end */
|
|
26
|
+
import { resolvePwshPath } from './resolve.ts'
|
|
27
|
+
|
|
28
|
+
/* jscpd:ignore-start -- deliberate call-for-call mirror of bash-local (pwsh-tool-and-executor). */
|
|
29
|
+
/**
|
|
30
|
+
* Model-friendly environment overrides for PowerShell: disable colors and
|
|
31
|
+
* pagers that would garble tool output. `TERM=dumb` is a POSIX concept and is
|
|
32
|
+
* deliberately absent; `NO_COLOR` is honored by modern pwsh renderers.
|
|
33
|
+
*/
|
|
34
|
+
export const ENV_OVERRIDES = {
|
|
35
|
+
NO_COLOR: '1',
|
|
36
|
+
PAGER: 'cat',
|
|
37
|
+
GIT_PAGER: 'cat',
|
|
38
|
+
} as const
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* UTF-8 output pinning prepended to every command. The subprocess collector
|
|
42
|
+
* decodes output bytes as UTF-8, but Windows PowerShell 5.1 (the last-resort
|
|
43
|
+
* executable fallback) writes the console/OEM code page by default, which
|
|
44
|
+
* garbles non-ASCII output; pwsh 7 defaults to UTF-8 and is unaffected. The
|
|
45
|
+
* statements ride on line 1 after `; ` separators so PowerShell error line
|
|
46
|
+
* numbers stay accurate.
|
|
47
|
+
*
|
|
48
|
+
* The type-creation (`[System.Text.UTF8Encoding]::new(...)`) is restricted in
|
|
49
|
+
* PowerShell's Constrained Language mode (Windows AppLocker / restricted
|
|
50
|
+
* token). When this executor runs under a Windows ACL restricted token,
|
|
51
|
+
* that mode is active and the construct errors; the `LanguageMode` check
|
|
52
|
+
* gates the assignment so the rest of the user's command still runs. Full
|
|
53
|
+
* LanguageMode runs hit the assignment unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export const ENCODING_PREAMBLE =
|
|
56
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'ConstrainedLanguage') { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $OutputEncoding = [System.Text.UTF8Encoding]::new($false) }; "
|
|
57
|
+
|
|
58
|
+
/** Default SIGTERM→SIGKILL grace period (the `graceMs` config). */
|
|
59
|
+
const DEFAULT_GRACE_MS = 3_000
|
|
60
|
+
|
|
61
|
+
/** Default per-stream spill cap (the `maxSpillBytes` config). */
|
|
62
|
+
const DEFAULT_MAX_SPILL_BYTES = 64 * 1024 * 1024
|
|
63
|
+
|
|
64
|
+
/** Plugin config (all optional — `static Config` supplies the defaults). */
|
|
65
|
+
export interface Config {
|
|
66
|
+
/** Default working directory for commands (default: process.cwd()). */
|
|
67
|
+
cwd?: string
|
|
68
|
+
/** Default foreground timeout in milliseconds. */
|
|
69
|
+
timeoutMs?: number
|
|
70
|
+
/** Upper bound for per-call timeout overrides. */
|
|
71
|
+
maxTimeoutMs?: number
|
|
72
|
+
/** Per-stream in-memory output cap; overflow spills to a temp file. */
|
|
73
|
+
maxOutputBytes?: number
|
|
74
|
+
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
|
|
75
|
+
maxSpillBytes?: number
|
|
76
|
+
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
|
|
77
|
+
graceMs?: number
|
|
78
|
+
/**
|
|
79
|
+
* Explicit pwsh executable. When omitted, well-known Windows install
|
|
80
|
+
* locations and PATH entries are probed in order (PowerShell 7 install,
|
|
81
|
+
* PATH entries such as the Microsoft Store install, then Windows
|
|
82
|
+
* PowerShell 5.1), falling back to a bare `pwsh` resolved through PATH.
|
|
83
|
+
*/
|
|
84
|
+
pwshPath?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The shape after schemastery applied the defaults (cwd/pwshPath get defaults). */
|
|
88
|
+
type ResolvedConfig = Required<Omit<Config, 'cwd' | 'pwshPath'>> & Pick<Config, 'cwd' | 'pwshPath'>
|
|
89
|
+
|
|
90
|
+
// Resolution lives in its own dependency-free module so callers and the
|
|
91
|
+
// executor share one definition of which executable the package will use.
|
|
92
|
+
export { candidatePwshPaths, resolvePwshPath } from './resolve.ts'
|
|
93
|
+
|
|
94
|
+
/** Project a settled collect-mode reader into the final CollectedOutput shape. */
|
|
95
|
+
function finalOutput(reader: SubprocessOutputReader): CollectedOutput {
|
|
96
|
+
const read = reader.readFrom(0)
|
|
97
|
+
return {
|
|
98
|
+
text: read.text,
|
|
99
|
+
truncated: read.lossy,
|
|
100
|
+
...read.spillPath !== undefined ? { spillPath: read.spillPath } : {},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertPositiveFinite(name: string, value: number): void {
|
|
105
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
106
|
+
throw new Error(`pwsh-local: ${name} must be a positive finite number`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reject a resolved section this executor could not run with. The schema
|
|
112
|
+
* expresses neither "positive and finite" nor the timer bound `graceMs` has to
|
|
113
|
+
* fit, so a stored value is refused where it is written instead of failing at
|
|
114
|
+
* the next command.
|
|
115
|
+
* @param config - the resolved section, schema-valid by construction.
|
|
116
|
+
* @throws Error naming the field that cannot be used.
|
|
117
|
+
*/
|
|
118
|
+
export function assertServiceablePwshConfig(config: Config): void {
|
|
119
|
+
const resolved = config as ResolvedConfig
|
|
120
|
+
assertPositiveFinite('timeoutMs', resolved.timeoutMs)
|
|
121
|
+
assertPositiveFinite('maxTimeoutMs', resolved.maxTimeoutMs)
|
|
122
|
+
assertPositiveFinite('maxOutputBytes', resolved.maxOutputBytes)
|
|
123
|
+
assertPositiveFinite('maxSpillBytes', resolved.maxSpillBytes)
|
|
124
|
+
assertPositiveFinite('graceMs', resolved.graceMs)
|
|
125
|
+
if (resolved.graceMs > MAX_TIMER_DELAY_MS) {
|
|
126
|
+
throw new Error(`pwsh-local: graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Local PowerShell executor over `ctx.subprocess`. Bounded output, spill
|
|
132
|
+
* files, and process-tree termination are the subprocess service's mechanics;
|
|
133
|
+
* this executor supplies their configured budgets per spawn.
|
|
134
|
+
*/
|
|
135
|
+
export class PwshLocalExecutor extends ShellExecutor {
|
|
136
|
+
static inject = ['subprocess']
|
|
137
|
+
|
|
138
|
+
static Config: z<Config> = z.object({
|
|
139
|
+
cwd: z.string().default(process.cwd()),
|
|
140
|
+
timeoutMs: z.number().default(120_000),
|
|
141
|
+
maxTimeoutMs: z.number().default(600_000),
|
|
142
|
+
maxOutputBytes: z.number().default(64_000),
|
|
143
|
+
maxSpillBytes: z.number().default(DEFAULT_MAX_SPILL_BYTES),
|
|
144
|
+
graceMs: z.number().default(DEFAULT_GRACE_MS),
|
|
145
|
+
pwshPath: z.string().default(''),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
/** Validated config (schemastery applied the defaults before construction). */
|
|
149
|
+
private readonly config: ResolvedConfig
|
|
150
|
+
|
|
151
|
+
/** The pwsh executable resolved from the current config. */
|
|
152
|
+
private readonly resolvedPwshPath: string
|
|
153
|
+
|
|
154
|
+
/** The pwsh executable every command runs through. */
|
|
155
|
+
get pwshPath(): string {
|
|
156
|
+
return this.resolvedPwshPath
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
constructor(ctx: Context, config: Config) {
|
|
160
|
+
super(ctx)
|
|
161
|
+
// Schemastery fills these fields before construction; the type does not encode that step.
|
|
162
|
+
const entry = config as ResolvedConfig
|
|
163
|
+
assertServiceablePwshConfig(entry)
|
|
164
|
+
this.config = entry
|
|
165
|
+
// Default pwshPath to platform resolution: probe well-known Windows
|
|
166
|
+
// installs, then fall back to a bare `pwsh` resolved through PATH.
|
|
167
|
+
this.resolvedPwshPath = resolvePwshPath(entry.pwshPath ?? '')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolve a request into a fully-specified spec: fill `workdir` from
|
|
172
|
+
* `config.cwd` (else `process.cwd()`), and `timeoutMs` from
|
|
173
|
+
* `config.timeoutMs`, capped at `config.maxTimeoutMs`.
|
|
174
|
+
*/
|
|
175
|
+
resolve(request: ShellExecRequest): ShellExecSpec {
|
|
176
|
+
const timeoutMs = clampTimeout(
|
|
177
|
+
request.timeoutMs,
|
|
178
|
+
this.config.timeoutMs,
|
|
179
|
+
this.config.maxTimeoutMs,
|
|
180
|
+
'pwsh-local: request.timeoutMs',
|
|
181
|
+
)
|
|
182
|
+
const stdoutMaxBytes = request.stdoutMaxBytes ?? this.config.maxOutputBytes
|
|
183
|
+
assertPositiveFinite('request.stdoutMaxBytes', stdoutMaxBytes)
|
|
184
|
+
return {
|
|
185
|
+
command: request.command,
|
|
186
|
+
workdir: request.workdir ?? this.config.cwd ?? process.cwd(),
|
|
187
|
+
timeoutMs,
|
|
188
|
+
stdoutMaxBytes,
|
|
189
|
+
...request.signal ? { signal: request.signal } : {},
|
|
190
|
+
...request.stdin !== undefined ? { stdin: request.stdin } : {},
|
|
191
|
+
...request.env !== undefined ? { env: request.env } : {},
|
|
192
|
+
...request.dshEnv !== undefined ? { dshEnv: request.dshEnv } : {},
|
|
193
|
+
// Carry a sandbox policy through verbatim: this executor never
|
|
194
|
+
// confines, so the field is inert here (the seam contract) — a
|
|
195
|
+
// sandboxing subclass overrides resolve() to stamp its default instead.
|
|
196
|
+
sandboxPolicy: request.sandboxPolicy,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The pwsh invocation argv for one resolved spec — the argv-level seam a
|
|
202
|
+
* confining subclass wraps through `ctx.sandbox.confine` (the pwsh twin of
|
|
203
|
+
* bash-local's `runArgv`/`startArgv` hooks; see `@bo-agent/pwsh-sandbox`).
|
|
204
|
+
*/
|
|
205
|
+
protected argv(spec: ShellExecSpec): string[] {
|
|
206
|
+
return [this.pwshPath, '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', `${ENCODING_PREAMBLE}${spec.command}`]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Map one resolved spec plus its argv onto a fully-specified subprocess spawn. */
|
|
210
|
+
private spawnSpec(
|
|
211
|
+
spec: ShellExecSpec,
|
|
212
|
+
stdoutMaxBytes: number,
|
|
213
|
+
signal: AbortSignal | undefined,
|
|
214
|
+
argv: readonly string[],
|
|
215
|
+
): SubprocessSpawnSpec {
|
|
216
|
+
const collect = (maxBytes: number): SubprocessCollect =>
|
|
217
|
+
({ maxBytes, spill: { maxBytes: this.config.maxSpillBytes } })
|
|
218
|
+
return {
|
|
219
|
+
argv: [...argv],
|
|
220
|
+
cwd: spec.workdir,
|
|
221
|
+
stdio: {
|
|
222
|
+
stdin: spec.stdin !== undefined ? { data: spec.stdin } : 'ignore',
|
|
223
|
+
stdout: collect(stdoutMaxBytes),
|
|
224
|
+
stderr: collect(this.config.maxOutputBytes),
|
|
225
|
+
},
|
|
226
|
+
graceMs: this.config.graceMs,
|
|
227
|
+
signal,
|
|
228
|
+
env: { ...ENV_OVERRIDES, ...spec.env, ...spec.dshEnv },
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The collect-mode readers the executor itself requested (present by construction). */
|
|
233
|
+
private static collected(handle: SubprocessHandle): { stdout: SubprocessOutputReader; stderr: SubprocessOutputReader } {
|
|
234
|
+
const { stdout, stderr } = handle.collected
|
|
235
|
+
/* v8 ignore start -- collect dispositions expose both readers by the seam contract; defensive. */
|
|
236
|
+
if (stdout === undefined || stderr === undefined) {
|
|
237
|
+
throw new Error('pwsh-local: subprocess implementation dropped a requested collect stream')
|
|
238
|
+
}
|
|
239
|
+
/* v8 ignore stop */
|
|
240
|
+
return { stdout, stderr }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async run(spec: ShellExecSpec): Promise<ShellRunResult> {
|
|
244
|
+
return this.runArgv(spec, this.argv(spec))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Foreground run of an exact argv (the confining subclass re-wraps it). */
|
|
248
|
+
protected async runArgv(spec: ShellExecSpec, argv: readonly string[]): Promise<ShellRunResult> {
|
|
249
|
+
// One deadline combines timeout and upstream cancellation; disposal clears its timer.
|
|
250
|
+
using d = deadline(spec.signal, spec.timeoutMs, 'PWSH_TIMEOUT')
|
|
251
|
+
const handle = this.ctx.subprocess.spawn(this.spawnSpec(spec, spec.stdoutMaxBytes, d.signal, argv))
|
|
252
|
+
const outcome = await handle.done
|
|
253
|
+
const collected = PwshLocalExecutor.collected(handle)
|
|
254
|
+
// Only this executor's timeout reason counts as timedOut; outer deadlines count as aborts.
|
|
255
|
+
const timedOut = timeoutOf(d.signal, 'PWSH_TIMEOUT') !== undefined
|
|
256
|
+
const aborted = d.signal.aborted && !timedOut
|
|
257
|
+
return {
|
|
258
|
+
...outcome,
|
|
259
|
+
timedOut,
|
|
260
|
+
aborted,
|
|
261
|
+
timeoutMs: spec.timeoutMs,
|
|
262
|
+
stdout: finalOutput(collected.stdout),
|
|
263
|
+
stderr: finalOutput(collected.stderr),
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
start(spec: ShellExecSpec): ShellProcess {
|
|
268
|
+
return this.startArgv(spec, this.argv(spec))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Background start of an exact argv (the confining subclass re-wraps it). */
|
|
272
|
+
protected startArgv(spec: ShellExecSpec, argv: readonly string[]): ShellProcess {
|
|
273
|
+
// Background runs ignore timeoutMs; callers stop them through kill() or spec.signal.
|
|
274
|
+
const running = this.ctx.subprocess.spawn(this.spawnSpec(spec, this.config.maxOutputBytes, spec.signal, argv))
|
|
275
|
+
const collected = PwshLocalExecutor.collected(running)
|
|
276
|
+
|
|
277
|
+
// A spawn failure produces no process output, so the subprocess service has nothing
|
|
278
|
+
// to buffer; the note is delivered exactly once through the read path.
|
|
279
|
+
let spawnFailureNote: string | undefined
|
|
280
|
+
const consumeSpawnFailure = (): string => {
|
|
281
|
+
const note = spawnFailureNote ?? ''
|
|
282
|
+
spawnFailureNote = undefined
|
|
283
|
+
return note
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let stdoutOffset = 0
|
|
287
|
+
let stderrOffset = 0
|
|
288
|
+
const proc: ShellProcess = {
|
|
289
|
+
status: 'running',
|
|
290
|
+
exitCode: null,
|
|
291
|
+
signal: null,
|
|
292
|
+
done: running.done.then((outcome) => {
|
|
293
|
+
// Any signal termination is killed, including a command signaling itself.
|
|
294
|
+
if (proc.status === 'running') {
|
|
295
|
+
proc.status = spec.signal?.aborted === true || outcome.signal !== null ? 'killed' : 'completed'
|
|
296
|
+
}
|
|
297
|
+
proc.exitCode = outcome.exitCode
|
|
298
|
+
proc.signal = outcome.signal
|
|
299
|
+
this.onProcessDone(proc, collected.stderr.readFrom(0).text, false)
|
|
300
|
+
}, (error: unknown) => {
|
|
301
|
+
// Background spawn failures settle as killed and surface through the read path.
|
|
302
|
+
proc.status = 'killed'
|
|
303
|
+
spawnFailureNote = `spawn failed: ${String(error)}`
|
|
304
|
+
this.onProcessDone(proc, spawnFailureNote, true, error)
|
|
305
|
+
}),
|
|
306
|
+
readOutput: (): ShellProcessRead => {
|
|
307
|
+
const out = collected.stdout.readFrom(stdoutOffset)
|
|
308
|
+
const err = collected.stderr.readFrom(stderrOffset)
|
|
309
|
+
stdoutOffset = out.nextOffset
|
|
310
|
+
stderrOffset = err.nextOffset
|
|
311
|
+
|
|
312
|
+
// A failed spawn never produced process output, so the note and real
|
|
313
|
+
// stderr text are mutually exclusive.
|
|
314
|
+
const errText = err.text.length > 0 ? err.text : consumeSpawnFailure()
|
|
315
|
+
// Single newline between sections: stdout chunks usually end with one
|
|
316
|
+
// already; add it only when missing.
|
|
317
|
+
const separator = out.text.length > 0 && !out.text.endsWith('\n') ? '\n' : ''
|
|
318
|
+
const delta = out.text
|
|
319
|
+
+ (errText.length > 0 ? `${separator}[stderr]\n${errText}` : '')
|
|
320
|
+
return {
|
|
321
|
+
delta,
|
|
322
|
+
lossy: out.lossy || err.lossy,
|
|
323
|
+
...out.spillPath !== undefined ? { stdoutSpillPath: out.spillPath } : {},
|
|
324
|
+
...err.spillPath !== undefined ? { stderrSpillPath: err.spillPath } : {},
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
kill: (): boolean => {
|
|
328
|
+
if (proc.status !== 'running') return false
|
|
329
|
+
proc.status = 'killed'
|
|
330
|
+
running.terminate()
|
|
331
|
+
return true
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
return proc
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Settlement hook for subclasses that attach execution facts to a process.
|
|
339
|
+
* The base implementation is intentionally empty. Mirrored from
|
|
340
|
+
* `bash-local` (whose sandboxing subclass consumes the same hook); the
|
|
341
|
+
* pwsh-confining consumer is `@bo-agent/pwsh-sandbox`.
|
|
342
|
+
* @param _proc - the settled process handle.
|
|
343
|
+
* @param _stderr - the process's retained stderr tail used by subclasses for settlement classification.
|
|
344
|
+
* @param _spawnFailed - whether the spawn rejected before any process existed.
|
|
345
|
+
* @param _spawnError - the spawn rejection, when `_spawnFailed`.
|
|
346
|
+
*/
|
|
347
|
+
protected onProcessDone(_proc: ShellProcess, _stderr: string, _spawnFailed: boolean, _spawnError?: unknown): void {}
|
|
348
|
+
}
|
|
349
|
+
/* jscpd:ignore-end */
|
|
350
|
+
|
|
351
|
+
export default PwshLocalExecutor
|