@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/src/resolve.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
import { lstatSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Well-known Windows PowerShell install locations plus PATH entries, newest
|
|
16
|
+
* first. Explicitly parameterized (env) so resolution is a pure function of
|
|
17
|
+
* its inputs on every platform.
|
|
18
|
+
* @param env - the environment to probe; defaults to the process environment.
|
|
19
|
+
* @returns candidate `pwsh` executable paths in resolution order.
|
|
20
|
+
*/
|
|
21
|
+
export function candidatePwshPaths(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
22
|
+
const programFiles = env.ProgramFiles ?? 'C:\\Program Files'
|
|
23
|
+
const systemRoot = env.SystemRoot ?? 'C:\\Windows'
|
|
24
|
+
const candidates = [
|
|
25
|
+
join(programFiles, 'PowerShell', '7', 'pwsh.exe'),
|
|
26
|
+
]
|
|
27
|
+
// Microsoft Store installs (and any user-added location) live on PATH;
|
|
28
|
+
// entries may carry surrounding quotes from `setx`-style definitions.
|
|
29
|
+
for (const entry of (env.PATH ?? '').split(';')) {
|
|
30
|
+
const trimmed = entry.trim().replace(/^"|"$/g, '')
|
|
31
|
+
if (trimmed.length === 0) continue
|
|
32
|
+
candidates.push(join(trimmed, 'pwsh.exe'))
|
|
33
|
+
}
|
|
34
|
+
// Windows PowerShell 5.1 remains the last-resort fallback on legacy hosts.
|
|
35
|
+
candidates.push(join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'))
|
|
36
|
+
return candidates
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether a candidate can be spawned. lstat opens the entry itself instead of
|
|
41
|
+
* following reparse points, so it sees the Store app execution alias where
|
|
42
|
+
* stat hits the target's ACL (EACCES); Node reports that alias as a symlink
|
|
43
|
+
* on current releases and as a plain file on older ones, and CreateProcess
|
|
44
|
+
* resolves either shape. A real directory never matches.
|
|
45
|
+
*/
|
|
46
|
+
function candidateExists(candidate: string): boolean {
|
|
47
|
+
try {
|
|
48
|
+
const stat = lstatSync(candidate)
|
|
49
|
+
return stat.isFile() || stat.isSymbolicLink()
|
|
50
|
+
} catch {
|
|
51
|
+
// ENOENT (the candidate vanished between listing and probing) is the only
|
|
52
|
+
// expected failure; any other error names an unspawnable path, so false
|
|
53
|
+
// is the safe answer for it too.
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the pwsh executable this executor spawns.
|
|
60
|
+
* @param configured - an explicit `pwshPath` config value, trusted as-is.
|
|
61
|
+
* @param env - the environment to probe on Windows; defaults to the process environment.
|
|
62
|
+
* @param platform - the platform to resolve for; defaults to the process platform.
|
|
63
|
+
* @returns the first existing well-known location on Windows (PowerShell 7
|
|
64
|
+
* install, a PATH entry such as the Microsoft Store install, then Windows
|
|
65
|
+
* PowerShell 5.1), else `pwsh` for PATH resolution.
|
|
66
|
+
*/
|
|
67
|
+
export function resolvePwshPath(
|
|
68
|
+
configured?: string,
|
|
69
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
70
|
+
platform: NodeJS.Platform = process.platform,
|
|
71
|
+
): string {
|
|
72
|
+
if (configured !== undefined && configured.length > 0) return configured
|
|
73
|
+
if (platform === 'win32') {
|
|
74
|
+
for (const candidate of candidatePwshPaths(env)) {
|
|
75
|
+
if (candidateExists(candidate)) return candidate
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return 'pwsh'
|
|
79
|
+
}
|
package/src/timeout.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* Largest value `setTimeout` can express as a 32-bit signed milliseconds
|
|
12
|
+
* delay. Anything larger would wrap to a near-immediate fire. Inlined to
|
|
13
|
+
* mirror Node's `TIMEOUT_MAX` and to keep the dependency surface small.
|
|
14
|
+
*/
|
|
15
|
+
export const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Clamp a per-call value against a configured range. The default applies
|
|
19
|
+
* first; the maximum caps any explicit caller-supplied value.
|
|
20
|
+
* @param value - the requested value (may be undefined).
|
|
21
|
+
* @param defaultValue - the value used when none is requested.
|
|
22
|
+
* @param maxValue - the upper bound for explicit values.
|
|
23
|
+
* @param name - label prefixed to thrown errors.
|
|
24
|
+
* @returns a positive finite value within [1, maxValue].
|
|
25
|
+
* @throws Error when neither default nor requested value is a positive finite
|
|
26
|
+
* number, or when maxValue itself is invalid.
|
|
27
|
+
*/
|
|
28
|
+
export function clampTimeout(
|
|
29
|
+
value: number | undefined,
|
|
30
|
+
defaultValue: number,
|
|
31
|
+
maxValue: number,
|
|
32
|
+
name: string,
|
|
33
|
+
): number {
|
|
34
|
+
if (!Number.isFinite(maxValue) || maxValue <= 0) {
|
|
35
|
+
throw new Error(`${name}: maxValue must be a positive finite number`)
|
|
36
|
+
}
|
|
37
|
+
const candidate = value ?? defaultValue
|
|
38
|
+
if (!Number.isFinite(candidate) || candidate <= 0) {
|
|
39
|
+
throw new Error(`${name}: value must be a positive finite number`)
|
|
40
|
+
}
|
|
41
|
+
return Math.min(candidate, maxValue)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A symbol tagged on the AbortSignal's reason so the timeout reason is identifiable. */
|
|
45
|
+
export const TIMEOUT_REASON = Symbol.for('@bo-agent/pwsh-local/TIMEOUT_REASON')
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Combine a caller's optional AbortSignal with a millisecond timeout into a
|
|
49
|
+
* single controller. Either side aborts the controller; disposal clears the
|
|
50
|
+
* timer and removes the upstream listener, so the result is safe to discard
|
|
51
|
+
* at end of scope via the `using` declaration.
|
|
52
|
+
*/
|
|
53
|
+
export interface DeadlineHandle {
|
|
54
|
+
readonly signal: AbortSignal
|
|
55
|
+
[Symbol.dispose](): void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build a deadline fused from the caller's signal and the executor's
|
|
60
|
+
* timeout. The returned controller aborts on whichever fires first; a
|
|
61
|
+
* successful command observes `signal.aborted === false`. The timeout
|
|
62
|
+
* reason is tagged so callers can tell timeout apart from upstream
|
|
63
|
+
* cancellation.
|
|
64
|
+
* @param signal - caller's upstream signal; may be undefined.
|
|
65
|
+
* @param timeoutMs - milliseconds until the timeout branch aborts.
|
|
66
|
+
* @param reason - human-readable label for the timeout reason; only used
|
|
67
|
+
* for tagging, never surfaced to the user.
|
|
68
|
+
*/
|
|
69
|
+
export function deadline(
|
|
70
|
+
signal: AbortSignal | undefined,
|
|
71
|
+
timeoutMs: number,
|
|
72
|
+
reason: string,
|
|
73
|
+
): DeadlineHandle {
|
|
74
|
+
const controller = new AbortController()
|
|
75
|
+
const onAbort = (): void => controller.abort(signal?.reason)
|
|
76
|
+
if (signal !== undefined) {
|
|
77
|
+
if (signal.aborted) {
|
|
78
|
+
controller.abort(signal.reason)
|
|
79
|
+
} else {
|
|
80
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// `setTimeout` cannot express delays above MAX_TIMER_DELAY_MS without
|
|
84
|
+
// wrapping; cap the request so the timeout branch actually fires when
|
|
85
|
+
// promised.
|
|
86
|
+
const safeTimeout = Math.min(timeoutMs, MAX_TIMER_DELAY_MS)
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
controller.abort(TIMEOUT_REASON)
|
|
89
|
+
}, safeTimeout)
|
|
90
|
+
return {
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
[Symbol.dispose](): void {
|
|
93
|
+
clearTimeout(timer)
|
|
94
|
+
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Return the tagged reason if the controller aborted because of the timeout
|
|
101
|
+
* branch (or upstream cancellation that mirrored it). Other abort reasons
|
|
102
|
+
* resolve as the caller's own cancellation; undefined means the controller
|
|
103
|
+
* is still live.
|
|
104
|
+
* @param signal - a controller's signal after a wait.
|
|
105
|
+
* @param expected - the label passed to {@link deadline}; matched against
|
|
106
|
+
* the symbol tag, so the timeout reason identifies itself.
|
|
107
|
+
*/
|
|
108
|
+
export function timeoutOf(signal: AbortSignal, _expected: string): symbol | undefined {
|
|
109
|
+
if (!signal.aborted) return undefined
|
|
110
|
+
if (signal.reason === TIMEOUT_REASON) return TIMEOUT_REASON
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"composite": true,
|
|
10
|
+
"incremental": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"rewriteRelativeImportExtensions": true,
|
|
15
|
+
"verbatimModuleSyntax": false,
|
|
16
|
+
"strict": true,
|
|
17
|
+
"noUncheckedIndexedAccess": true,
|
|
18
|
+
"exactOptionalPropertyTypes": true,
|
|
19
|
+
"noImplicitOverride": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUnusedLocals": false,
|
|
22
|
+
"noUnusedParameters": false,
|
|
23
|
+
"types": ["node"],
|
|
24
|
+
"outDir": "lib",
|
|
25
|
+
"rootDir": "src",
|
|
26
|
+
"tsBuildInfoFile": "lib/.tsbuildinfo",
|
|
27
|
+
"paths": {
|
|
28
|
+
"@bo-agent/inline": ["../inline/src"],
|
|
29
|
+
"@bo-agent/shell": ["../shell/src"],
|
|
30
|
+
"@bo-agent/subprocess": ["../subprocess/src"]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"include": ["src"],
|
|
34
|
+
"references": [
|
|
35
|
+
{ "path": "../inline" },
|
|
36
|
+
{ "path": "../subprocess" },
|
|
37
|
+
{ "path": "../shell" }
|
|
38
|
+
]
|
|
39
|
+
}
|