repo-contract 0.1.1 → 0.2.0

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0](https://github.com/MaverickCER/repo-contract/compare/repo-contract-v0.1.1...repo-contract-v0.2.0) (2026-09-03)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * defineRepoContract/runRepoContract now require spawn and env on the config (e.g. spawn: child_process.spawn, env: process.env). See ADR 0011 and README's "Supplying spawn/env" section for migration.
9
+
10
+ ### Features
11
+
12
+ * make process spawning and env access consumer-supplied capabilities ([640a961](https://github.com/MaverickCER/repo-contract/commit/640a961b65fd73511586c3b4f6274284bf5172a9))
13
+
3
14
  ## [0.1.1](https://github.com/MaverickCER/repo-contract/compare/repo-contract-v0.1.0...repo-contract-v0.1.1) (2026-09-01)
4
15
 
5
16
 
package/README.md CHANGED
@@ -92,7 +92,7 @@ npm install --save-dev yaml
92
92
 
93
93
  ### Runtime support matrix
94
94
 
95
- repo-contract spawns processes and reads `process.env` it is server/CLI-only by design, not an isomorphic/browser package.
95
+ repo-contract's checks run as spawned processes and read the ambient environment — but repo-contract never imports a process-spawning implementation or reads `process.env` itself (see [Supplying `spawn`/`env`](#supplying-spawnenv) below); it is server/CLI-only by design regardless, not an isomorphic/browser package, since the capability a consumer supplies is itself always a real Node-only spawning mechanism.
96
96
 
97
97
  | Environment | Supported |
98
98
  | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -113,9 +113,14 @@ Define your repository's standards as checks:
113
113
 
114
114
  ```ts
115
115
  // repo-contract.config.ts
116
+ import { spawn } from "node:child_process"
116
117
  import { defineRepoContract } from "repo-contract"
117
118
 
118
119
  export default defineRepoContract({
120
+ // repo-contract never spawns a process or reads process.env itself -- see
121
+ // "Supplying spawn/env" below for why, and what to pass on Windows.
122
+ spawn,
123
+ env: process.env,
119
124
  checks: {
120
125
  tests: {
121
126
  run: "npm test",
@@ -179,6 +184,49 @@ process.exitCode = verdict.passed ? 0 : 1
179
184
 
180
185
  There is no CLI, no config-file discovery magic, and no hidden state.
181
186
 
187
+ ### Supplying `spawn`/`env`
188
+
189
+ `spawn` and `env` are required fields on the config — repo-contract never imports a process-spawning implementation or reads `process.env` internally (see [ADR 0011](specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md)). You supply both, as trusted capabilities repo-contract calls with a resolved command/argv/options — it does not inspect, wrap, or sanitize them.
190
+
191
+ **macOS/Linux** — plain `node:child_process` is enough:
192
+
193
+ ```ts
194
+ import { spawn } from "node:child_process"
195
+
196
+ export default defineRepoContract({
197
+ spawn,
198
+ env: process.env,
199
+ checks: {/* ... */},
200
+ })
201
+ ```
202
+
203
+ **Windows** — most npm-installed CLI tools (`eslint`, `prettier`, `tsc`, …) resolve to `.cmd` shims, and plain `node:child_process.spawn` refuses to run those at all without `shell: true` (Node's own CVE-2024-27980 mitigation). Install [`cross-spawn`](https://www.npmjs.com/package/cross-spawn) and pass it instead — it resolves `.cmd`/`.bat` shims and quotes arguments safely for `cmd.exe`, **without** turning on shell metacharacter interpretation:
204
+
205
+ ```ts
206
+ import crossSpawn, { sync as crossSpawnSync } from "cross-spawn"
207
+
208
+ export default defineRepoContract({
209
+ spawn: crossSpawn,
210
+ env: process.env,
211
+ // Optional: lets a timed-out/aborted/Ctrl+C-killed check's full process
212
+ // tree (not just its immediate process) get cleaned up on Windows too.
213
+ killProcessTree: crossSpawnSync,
214
+ checks: {/* ... */},
215
+ })
216
+ ```
217
+
218
+ **`cross-spawn` does not mean "shell execution."** These are two independent choices:
219
+
220
+ | `Spawner` choice | `shell` option | Result |
221
+ | ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
222
+ | native `spawn` | `false` (default) | argv-only, no shell interpretation |
223
+ | `cross-spawn` | `false` (default) | Windows `.cmd`/`.bat` resolution + safe `cmd.exe` quoting, still argv-only |
224
+ | either | `true` | shell metacharacters (`&&`, `\|`, …) interpreted — a per-check or global opt-in, see [`shell`](docs/api-report/repo-contract.api.md) |
225
+
226
+ Passing `cross-spawn` fixes Windows command resolution; it does not by itself enable shell metacharacter interpretation. `check.shell` (or the config-level `shell` default) is the separate, explicit opt-in for that — see `SECURITY.md` before enabling it.
227
+
228
+ `repo-contract` doesn't ship a ready-made spawner of its own, on purpose (see ADR 0011's Alternatives) — the two snippets above are the whole integration.
229
+
182
230
  ## The model
183
231
 
184
232
  ```text
@@ -641,10 +689,13 @@ It does **not** encode your repository's definition of quality.
641
689
  Import a preset, spread it into your own `checks` record, and override whatever you need — most often `policy`:
642
690
 
643
691
  ```ts
692
+ import { spawn } from "node:child_process"
644
693
  import { defineRepoContract } from "repo-contract"
645
694
  import { format, typecheck, license } from "repo-contract/presets"
646
695
 
647
696
  export default defineRepoContract({
697
+ spawn,
698
+ env: process.env,
648
699
  checks: {
649
700
  format,
650
701
  typecheck: {
@@ -662,10 +713,13 @@ export default defineRepoContract({
662
713
  Some presets are factories because they expose options that change what gets executed:
663
714
 
664
715
  ```ts
716
+ import { spawn } from "node:child_process"
665
717
  import { defineRepoContract } from "repo-contract"
666
718
  import { lint, deadCode } from "repo-contract/presets"
667
719
 
668
720
  export default defineRepoContract({
721
+ spawn,
722
+ env: process.env,
669
723
  checks: {
670
724
  lint: lint({ path: "src" }),
671
725
  deadCode: deadCode({
@@ -1 +1 @@
1
- {"version":3,"file":"validate-config.d.ts","sourceRoot":"","sources":["../../../src/config/validate-config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAgB,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAKnE;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAwC3E;AA6QD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,GACxD,IAAI,CAoBN"}
1
+ {"version":3,"file":"validate-config.d.ts","sourceRoot":"","sources":["../../../src/config/validate-config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAgB,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAKnE;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAuE3E;AAqSD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,GACxD,IAAI,CAoBN"}
@@ -1,3 +1,4 @@
1
+ import type { SyncSpawner } from "../types.js";
1
2
  /**
2
3
  * Whether a check's process should be spawned with `detached: true`. On
3
4
  * POSIX this makes the spawned process the leader of a new process group
@@ -20,11 +21,20 @@ export declare function shouldSpawnDetached(): boolean;
20
21
  *
21
22
  * On POSIX, sends `signal` to the whole process group via the negative-pid
22
23
  * convention (requires the process to have been spawned with
23
- * `detached: true`, see `shouldSpawnDetached`). On Windows, process groups
24
+ * `detached: true`, see `shouldSpawnDetached`) -- no process spawning
25
+ * required, a single syscall via `process.kill`. On Windows, process groups
24
26
  * don't work the same way, so this shells out to `taskkill /pid <pid> /t
25
27
  * /f` -- the same technique the `tree-kill` package uses internally -- which
26
28
  * walks the system process table for descendants of `pid` regardless of how
27
- * it was spawned.
29
+ * it was spawned; that requires a synchronous spawn, which -- like every
30
+ * other process-spawning capability in this package -- is a trusted
31
+ * capability the caller supplies (`killProcessTree`, threaded from
32
+ * `RepoContractConfig.killProcessTree`), never imported internally (see
33
+ * specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md).
34
+ * When `killProcessTree` is omitted on Windows, this function is a
35
+ * documented no-op -- the caller (`spawn-check.ts`) falls back to
36
+ * terminating just the tracked child process handle directly, which needs
37
+ * no spawn at all.
28
38
  *
29
39
  * Swallows the expected best-effort-cleanup failures, but not every failure: a
30
40
  * process that has already exited (POSIX `ESRCH`, or `taskkill`'s "not found"
@@ -43,6 +53,7 @@ export declare function shouldSpawnDetached(): boolean;
43
53
  * more-cooperative alternative to fall back to here the way there is on POSIX.
44
54
  * @param pid - the pid of the tree's root process (the process group id on POSIX, since it was spawned detached)
45
55
  * @param signal - the POSIX signal to send (on Windows, ignored -- see doc comment above)
56
+ * @param killProcessTree - the consumer-supplied synchronous spawner used only on Windows; a no-op there when omitted (see doc comment above)
46
57
  */
47
- export declare function killTree(pid: number, signal: NodeJS.Signals): void;
58
+ export declare function killTree(pid: number, signal: NodeJS.Signals, killProcessTree?: SyncSpawner): void;
48
59
  //# sourceMappingURL=process-tree.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"process-tree.d.ts","sourceRoot":"","sources":["../../../src/execution/process-tree.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CA8ClE"}
1
+ {"version":3,"file":"process-tree.d.ts","sourceRoot":"","sources":["../../../src/execution/process-tree.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI,CAoDjG"}
@@ -1,4 +1,5 @@
1
1
  import type { CheckDefinition, CheckEvidence, CheckSchema, RunRepoContractOptions } from "../types.js";
2
+ import type { ExecutionCapability } from "./spawn-check.js";
2
3
  /**
3
4
  * One check's id, its original definition, and its raw execution evidence,
4
5
  * threaded together as a triple rather than three separately-keyed maps --
@@ -22,8 +23,9 @@ export declare const SELF_TERMINATE_DELAY_MS: number;
22
23
  * simply absent from the result, not present with some placeholder value.
23
24
  * @param checks - the full set of configured checks, keyed by id
24
25
  * @param concurrency - the maximum number of checks to run in parallel at once
26
+ * @param execution - the run's trusted execution capabilities (`spawn`, `env`, resolved global `shell` default), forwarded to every `spawnCheck` call
25
27
  * @param options - run options; `options.checks` restricts execution to those ids (plus their dependencies), `options.signal` cancels the whole run
26
28
  * @returns each executed check's id, definition, and raw evidence, one entry per resolved check regardless of whether it actually spawned
27
29
  */
28
- export declare function runChecks(checks: CheckSchema, concurrency: number, options?: RunRepoContractOptions): Promise<readonly CheckExecutionEntry[]>;
30
+ export declare function runChecks(checks: CheckSchema, concurrency: number, execution: ExecutionCapability, options?: RunRepoContractOptions): Promise<readonly CheckExecutionEntry[]>;
29
31
  //# sourceMappingURL=run-checks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-checks.d.ts","sourceRoot":"","sources":["../../../src/execution/run-checks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,WAAW,EACX,sBAAsB,EACvB,MAAM,aAAa,CAAA;AAOpB;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC,MAAM,EAAE,eAAe,EAAE,aAAa,CAAC,CAAA;AAYnF,eAAO,MAAM,uBAAuB,QAAgC,CAAA;AAoJpE;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,SAAS,mBAAmB,EAAE,CAAC,CAkJzC"}
1
+ {"version":3,"file":"run-checks.d.ts","sourceRoot":"","sources":["../../../src/execution/run-checks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,WAAW,EACX,sBAAsB,EACvB,MAAM,aAAa,CAAA;AAIpB,OAAO,KAAK,EAAqB,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAG9E;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC,MAAM,EAAE,eAAe,EAAE,aAAa,CAAC,CAAA;AAYnF,eAAO,MAAM,uBAAuB,QAAgC,CAAA;AAoJpE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,mBAAmB,EAC9B,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,SAAS,mBAAmB,EAAE,CAAC,CAkJzC"}
@@ -1,4 +1,22 @@
1
- import type { CheckDefinition, CheckEvidence } from "../types.js";
1
+ import type { CheckDefinition, CheckEvidence, Spawner, SyncSpawner } from "../types.js";
2
+ /**
3
+ * The trusted execution capabilities a run supplies once (see
4
+ * specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md)
5
+ * and every spawned check draws from -- `spawn`/`env` verbatim from
6
+ * `RepoContractConfig`, `shell` already resolved to its effective global
7
+ * default (`config.shell ?? false`). Internal only: not exported from
8
+ * `src/types.ts` or `src/index.ts`. Grouped into one object, rather than
9
+ * threaded as independent parameters through `run-repo-contract.ts` ->
10
+ * `run-checks.ts` -> `spawn-check.ts`, purely to keep related
11
+ * execution-capability concerns from drifting into unrelated positional
12
+ * parameters as more get added later.
13
+ */
14
+ export interface ExecutionCapability {
15
+ readonly spawn: Spawner;
16
+ readonly env: NodeJS.ProcessEnv;
17
+ readonly shell: boolean;
18
+ readonly killProcessTree?: SyncSpawner;
19
+ }
2
20
  export declare const SIGKILL_GRACE_PERIOD_MS = 2000;
3
21
  /** Handle a caller can use to forcibly terminate one in-flight check's process tree, independent of that check's own timeout/abort wiring -- used by run-checks.ts to clean up every active check when the host process itself receives SIGINT/SIGTERM. */
4
22
  export interface ActiveCheckHandle {
@@ -24,7 +42,8 @@ export interface ActiveCheckHandle {
24
42
  * @param check - the check definition to run (command, timeout, env, cwd, shell, etc.)
25
43
  * @param runSignal - the whole run's abort signal, if any; already-aborted before this is called means the check never spawns
26
44
  * @param activeHandles - the shared registry this check's kill handle is added to while running, so a host-process SIGINT/SIGTERM can terminate it
45
+ * @param execution - the run's trusted execution capabilities (`spawn`, `env`, resolved global `shell` default) -- see `ExecutionCapability`
27
46
  * @returns a fully-formed `CheckEvidence` reflecting however the process ended; this function itself never rejects
28
47
  */
29
- export declare function spawnCheck(checkId: string, check: CheckDefinition, runSignal: AbortSignal | undefined, activeHandles: Set<ActiveCheckHandle>): Promise<CheckEvidence>;
48
+ export declare function spawnCheck(checkId: string, check: CheckDefinition, runSignal: AbortSignal | undefined, activeHandles: Set<ActiveCheckHandle>, execution: ExecutionCapability): Promise<CheckEvidence>;
30
49
  //# sourceMappingURL=spawn-check.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-check.d.ts","sourceRoot":"","sources":["../../../src/execution/spawn-check.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAe,MAAM,aAAa,CAAA;AA6B9E,eAAO,MAAM,uBAAuB,OAAO,CAAA;AA6D3C,2PAA2P;AAC3P,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;CACnC;AAoHD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,eAAe,EACtB,SAAS,EAAE,WAAW,GAAG,SAAS,EAClC,aAAa,EAAE,GAAG,CAAC,iBAAiB,CAAC,GACpC,OAAO,CAAC,aAAa,CAAC,CA6PxB"}
1
+ {"version":3,"file":"spawn-check.d.ts","sourceRoot":"","sources":["../../../src/execution/spawn-check.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAe,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAIpG;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,eAAe,CAAC,EAAE,WAAW,CAAA;CACvC;AA2BD,eAAO,MAAM,uBAAuB,OAAO,CAAA;AA6D3C,2PAA2P;AAC3P,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;CACnC;AAyHD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,eAAe,EACtB,SAAS,EAAE,WAAW,GAAG,SAAS,EAClC,aAAa,EAAE,GAAG,CAAC,iBAAiB,CAAC,EACrC,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAAC,aAAa,CAAC,CA4QxB"}
@@ -9,5 +9,5 @@
9
9
  export { defineRepoContract } from "./config/define-repo-contract.js";
10
10
  export { runRepoContract } from "./run-repo-contract.js";
11
11
  export { DependencyDeclaredLaterError, InvalidCheckConfigError, InvalidRepoContractConfigError, ParserDependencyMissingError, PolicyReadFailedParseValueError, PolicyReadUnrequestedOutputError, PolicyThrewError, RepoContractError, UnknownCheckIdError, } from "./errors.js";
12
- export type { CheckDefinition, CheckDefinitionConfig, CheckEvidence, CheckSchema, CheckStatus, Evidence, OutputFormat, ParsedOutput, ParsedOutputFailure, ParsedOutputSuccess, Policy, PolicyContext, PolicyOutcome, PolicyResult, RepoContractConfig, RunRepoContractOptions, ValidatedCheckSchema, Verdict, } from "./types.js";
12
+ export type { CheckDefinition, CheckDefinitionConfig, CheckEvidence, CheckSchema, CheckStatus, Evidence, OutputFormat, ParsedOutput, ParsedOutputFailure, ParsedOutputSuccess, Policy, PolicyContext, PolicyOutcome, PolicyResult, RepoContractConfig, RunRepoContractOptions, Spawner, SyncSpawner, ValidatedCheckSchema, Verdict, } from "./types.js";
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,4BAA4B,EAC5B,+BAA+B,EAC/B,gCAAgC,EAChC,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,aAAa,CAAA;AAEpB,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,WAAW,EACX,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,MAAM,EACN,aAAa,EACb,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,sBAAsB,EACtB,oBAAoB,EACpB,OAAO,GACR,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,4BAA4B,EAC5B,+BAA+B,EAC/B,gCAAgC,EAChC,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,aAAa,CAAA;AAEpB,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,WAAW,EACX,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,MAAM,EACN,aAAa,EACb,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,sBAAsB,EACtB,OAAO,EACP,WAAW,EACX,oBAAoB,EACpB,OAAO,GACR,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"run-repo-contract.d.ts","sourceRoot":"","sources":["../../src/run-repo-contract.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,sBAAsB,EACtB,OAAO,EACR,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,eAAe,CAAC,KAAK,CAAC,OAAO,SAAS,WAAW,EAC/D,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,EACnC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAGrE"}
1
+ {"version":3,"file":"run-repo-contract.d.ts","sourceRoot":"","sources":["../../src/run-repo-contract.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,sBAAsB,EACtB,OAAO,EACR,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,eAAe,CAAC,KAAK,CAAC,OAAO,SAAS,WAAW,EAC/D,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,EACnC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAGrE"}
@@ -4,6 +4,7 @@
4
4
  * separate concepts (see specs/architecture.md) even though
5
5
  * `runRepoContract` returns both together.
6
6
  */
7
+ import type { ChildProcess, SpawnOptions, SpawnSyncOptions, SpawnSyncReturns } from "node:child_process";
7
8
  /** Output interpretation a check can explicitly request. No format requested means no parsing -- the consumer gets raw stdout/stderr only. */
8
9
  export type OutputFormat = "json" | "yaml" | "text";
9
10
  /**
@@ -202,9 +203,11 @@ export interface CheckDefinitionConfig {
202
203
  readonly run: string | readonly string[];
203
204
  /**
204
205
  * Opt into real shell execution instead of the safe argv-only default.
205
- * When `true`, `run` must be a `string` and is passed to the platform
206
- * shell as-is (via cross-spawn's own `shell` option) -- shell metacharacter
207
- * rejection does not apply. See SECURITY.md before enabling this.
206
+ * When `true` (or left unset with `RepoContractConfig.shell: true` as the
207
+ * run-wide default -- see that field), `run` must be a `string` and is
208
+ * passed to the platform shell as-is (via the supplied `Spawner`'s own
209
+ * `shell` option) -- shell metacharacter rejection does not apply. See
210
+ * SECURITY.md before enabling this.
208
211
  */
209
212
  readonly shell?: boolean;
210
213
  /** Working directory for the spawned process. Defaults to the current process's `cwd`. */
@@ -285,12 +288,76 @@ export type ValidatedCheckSchema<T> = {
285
288
  readonly dependsOn?: readonly (Exclude<keyof T, K> & string)[];
286
289
  } : never;
287
290
  };
291
+ /**
292
+ * Spawns a child process, given a resolved command, argv, and options --
293
+ * modeled directly on `node:child_process`'s own `spawn(command, args,
294
+ * options)` signature so both `node:child_process.spawn` and cross-spawn's
295
+ * exported `spawn` are valid, drop-in values with no adapter code required
296
+ * (see specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md).
297
+ * repo-contract treats whatever function is supplied as a trusted
298
+ * capability: it calls it with a resolved command/argv/options and does not
299
+ * inspect, wrap, or sanitize it -- the security properties of the spawned
300
+ * process are entirely the supplied function's own.
301
+ */
302
+ export type Spawner = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
303
+ /**
304
+ * Synchronously spawns a child process and waits for it to exit -- modeled directly on
305
+ * `node:child_process`'s own `spawnSync(command, args, options)` signature, the same
306
+ * drop-in-compatibility approach as `Spawner`. Used only for `RepoContractConfig.killProcessTree`
307
+ * (Windows process-tree cleanup via `taskkill`, which fundamentally needs to run synchronously from
308
+ * a signal-handling context that cannot `await` anything else -- see
309
+ * specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md).
310
+ * `node:child_process.spawnSync` and cross-spawn's exported `sync` are both valid, drop-in values.
311
+ */
312
+ export type SyncSpawner = (command: string, args: readonly string[], options: SpawnSyncOptions) => SpawnSyncReturns<Buffer | string>;
288
313
  /** Top-level configuration passed to `defineRepoContract`/`runRepoContract`. */
289
314
  export interface RepoContractConfig<TChecks extends CheckSchema = CheckSchema> {
290
315
  /** Every check to run, keyed by check id. */
291
316
  readonly checks: TChecks;
292
317
  /** Maximum number of checks to execute concurrently. Defaults to `os.availableParallelism()`. Must be a positive integer. */
293
318
  readonly concurrency?: number;
319
+ /**
320
+ * The trusted capability repo-contract calls to spawn every check's
321
+ * process -- e.g. `child_process.spawn` (from `"node:child_process"`) or
322
+ * cross-spawn's exported `spawn`. Required: repo-contract never imports a
323
+ * process-spawning implementation itself (see ADR 0011 above `Spawner`).
324
+ * `child_process.spawn` alone does not resolve Windows `.cmd`/`.bat`
325
+ * shims (most npm-installed CLI tools on Windows) without `shell: true`;
326
+ * pass cross-spawn instead for that correctness without enabling shell
327
+ * metacharacter interpretation -- cross-spawn is a spawn implementation
328
+ * choice, not a `shell: true` equivalent. See `shell` below.
329
+ */
330
+ readonly spawn: Spawner;
331
+ /**
332
+ * The ambient environment repo-contract treats as inheritable by each
333
+ * check whose `inheritEnv` is not `false` (the default) -- typically
334
+ * `process.env`, passed straight through by reference (never copied
335
+ * internally) so a consumer that mutates `process.env` mid-run still sees
336
+ * that reflected in later-spawned checks, exactly as if repo-contract had
337
+ * read `process.env` itself. Required: repo-contract never reads
338
+ * `process.env` internally (see ADR 0011 above `Spawner`). Typed as
339
+ * `NodeJS.ProcessEnv` so `env: process.env` needs no casting.
340
+ */
341
+ readonly env: NodeJS.ProcessEnv;
342
+ /**
343
+ * Global default for every check's own `shell` when that check doesn't
344
+ * set one itself (`check.shell ?? shell ?? false`). Defaults to `false`,
345
+ * the safe argv-only mode -- unrelated to which `spawn` is supplied; see
346
+ * `spawn`'s own doc comment for that distinction.
347
+ */
348
+ readonly shell?: boolean;
349
+ /**
350
+ * The trusted capability repo-contract calls, on Windows only, to forcibly terminate a check's
351
+ * entire process tree (not just its immediate process) on a timeout, an aborted run, or a
352
+ * host-process SIGINT/SIGTERM -- e.g. `child_process.spawnSync` (from `"node:child_process"`) or
353
+ * cross-spawn's exported `sync`. Optional, unlike `spawn`/`env`: when omitted, Windows cleanup
354
+ * falls back to terminating only the check's own immediate process (not any subprocess it spawned
355
+ * internally) -- correct for the common case, but a check that spawns its own descendants (e.g.
356
+ * `npm test` spawning the real test runner) may leave them running. POSIX cleanup never needs
357
+ * this at all (`process.kill(-pid, signal)` reaches the whole process group directly, no spawning
358
+ * required) -- see specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md.
359
+ */
360
+ readonly killProcessTree?: SyncSpawner;
294
361
  }
295
362
  /** Optional per-run controls for `runRepoContract`. */
296
363
  export interface RunRepoContractOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -1,11 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var os = require('os');
4
- var crossSpawn = require('cross-spawn');
5
4
  var string_decoder = require('string_decoder');
6
- var child_process = require('child_process');
7
-
8
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
5
 
10
6
  function _interopNamespace(e) {
11
7
  if (e && e.__esModule) return e;
@@ -26,7 +22,6 @@ function _interopNamespace(e) {
26
22
  }
27
23
 
28
24
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
29
- var crossSpawn__default = /*#__PURE__*/_interopDefault(crossSpawn);
30
25
 
31
26
  // src/config/define-repo-contract.ts
32
27
  function defineRepoContract(config) {
@@ -236,7 +231,7 @@ function validateRepoContractConfig(config) {
236
231
  if (untrusted === null || typeof untrusted !== "object") {
237
232
  throw new InvalidRepoContractConfigError("config must be an object.");
238
233
  }
239
- const { checks, concurrency } = untrusted;
234
+ const { checks, concurrency, spawn, env, shell, killProcessTree } = untrusted;
240
235
  if (checks === null || typeof checks !== "object" || Array.isArray(checks)) {
241
236
  throw new InvalidRepoContractConfigError(
242
237
  "checks must be an object mapping check id to check definition."
@@ -249,12 +244,35 @@ function validateRepoContractConfig(config) {
249
244
  );
250
245
  }
251
246
  }
247
+ if (typeof spawn !== "function") {
248
+ throw new InvalidRepoContractConfigError("spawn must be a function.");
249
+ }
250
+ validateConfigEnv(env);
251
+ if (shell !== void 0 && typeof shell !== "boolean") {
252
+ throw new InvalidRepoContractConfigError("shell must be a boolean when provided.");
253
+ }
254
+ const globalShell = shell === true;
255
+ if (killProcessTree !== void 0 && typeof killProcessTree !== "function") {
256
+ throw new InvalidRepoContractConfigError("killProcessTree must be a function when provided.");
257
+ }
252
258
  for (const [checkId, check] of Object.entries(checks)) {
253
- validateCheckDefinition(checkId, check);
259
+ validateCheckDefinition(checkId, check, globalShell);
254
260
  }
255
261
  validateDependencyGraph(checks);
256
262
  }
257
- function validateCheckDefinition(checkId, check) {
263
+ function validateConfigEnv(env) {
264
+ if (env === null || typeof env !== "object" || Array.isArray(env)) {
265
+ throw new InvalidRepoContractConfigError(
266
+ "env must be an object mapping variable name to value."
267
+ );
268
+ }
269
+ for (const [key, value] of Object.entries(env)) {
270
+ if (typeof value !== "string" && value !== void 0) {
271
+ throw new InvalidRepoContractConfigError(`env["${key}"] must be a string or undefined.`);
272
+ }
273
+ }
274
+ }
275
+ function validateCheckDefinition(checkId, check, globalShell) {
258
276
  if (/^(?:0|[1-9]\d*)$/.test(checkId)) {
259
277
  throw new InvalidCheckConfigError(
260
278
  checkId,
@@ -265,7 +283,7 @@ function validateCheckDefinition(checkId, check) {
265
283
  throw new InvalidCheckConfigError(checkId, "check definition must be an object.");
266
284
  }
267
285
  const fields = check;
268
- const usesShell = validateShell(checkId, fields.shell);
286
+ const usesShell = validateShell(checkId, fields.shell, globalShell);
269
287
  validateRun(checkId, fields.run, usesShell);
270
288
  validateCwd(checkId, fields.cwd);
271
289
  validateEnv(checkId, fields.env);
@@ -276,11 +294,11 @@ function validateCheckDefinition(checkId, check) {
276
294
  validateIsolated(checkId, fields.isolated);
277
295
  validatePolicy(checkId, fields.policy);
278
296
  }
279
- function validateShell(checkId, shell) {
297
+ function validateShell(checkId, shell, globalShell) {
280
298
  if (shell !== void 0 && typeof shell !== "boolean") {
281
299
  throw new InvalidCheckConfigError(checkId, "shell must be a boolean when provided.");
282
300
  }
283
- return shell === true;
301
+ return typeof shell === "boolean" ? shell : globalShell;
284
302
  }
285
303
  function validateRun(checkId, run, usesShell) {
286
304
  if (typeof run !== "string" && !Array.isArray(run)) {
@@ -627,18 +645,23 @@ async function runWithConcurrencyGraph(items, concurrency, dependencyIndexes, wo
627
645
  launchNext();
628
646
  });
629
647
  }
648
+
649
+ // src/execution/process-tree.ts
630
650
  function shouldSpawnDetached() {
631
651
  return process.platform !== "win32";
632
652
  }
633
653
  function isErrnoException(error) {
634
654
  return error instanceof Error && "code" in error;
635
655
  }
636
- function killTree(pid, signal) {
656
+ function killTree(pid, signal, killProcessTree) {
637
657
  if (!Number.isInteger(pid) || pid <= 0) {
638
658
  return;
639
659
  }
640
660
  if (process.platform === "win32") {
641
- const result = child_process.spawnSync("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore" });
661
+ if (killProcessTree === void 0) return;
662
+ const result = killProcessTree("taskkill", ["/pid", String(pid), "/t", "/f"], {
663
+ stdio: "ignore"
664
+ });
642
665
  if (result.error !== void 0) throw result.error;
643
666
  return;
644
667
  }
@@ -672,7 +695,7 @@ function createBoundedCollector() {
672
695
  value: () => value
673
696
  };
674
697
  }
675
- function resolveCommand(checkId, check) {
698
+ function resolveCommand(checkId, check, effectiveShell) {
676
699
  const run = check.run;
677
700
  if (typeof run !== "string") {
678
701
  const [command2, ...args2] = run;
@@ -681,7 +704,7 @@ function resolveCommand(checkId, check) {
681
704
  }
682
705
  return { command: command2, args: args2 };
683
706
  }
684
- if (check.shell === true) {
707
+ if (effectiveShell) {
685
708
  return { command: run, args: [] };
686
709
  }
687
710
  const [command, ...args] = tokenizeRunString(run, checkId);
@@ -690,10 +713,10 @@ function resolveCommand(checkId, check) {
690
713
  }
691
714
  return { command, args };
692
715
  }
693
- function buildEnv(check) {
716
+ function buildEnv(check, ambientEnv) {
694
717
  const base = {};
695
718
  if (check.inheritEnv !== false) {
696
- for (const [key, value] of Object.entries(process.env)) {
719
+ for (const [key, value] of Object.entries(ambientEnv)) {
697
720
  if (value !== void 0) base[key] = value;
698
721
  }
699
722
  }
@@ -716,13 +739,14 @@ function terminalEvidence(command, args, startedAt, status, exitCode, signal, st
716
739
  ...spawnErrorCode !== void 0 ? { spawnErrorCode } : {}
717
740
  };
718
741
  }
719
- async function spawnCheck(checkId, check, runSignal, activeHandles) {
742
+ async function spawnCheck(checkId, check, runSignal, activeHandles, execution) {
720
743
  const startedAt = /* @__PURE__ */ new Date();
721
- const { command, args } = resolveCommand(checkId, check);
744
+ const effectiveShell = check.shell ?? execution.shell;
745
+ const { command, args } = resolveCommand(checkId, check, effectiveShell);
722
746
  if (runSignal?.aborted === true) {
723
747
  return terminalEvidence(command, args, startedAt, "aborted", null, null);
724
748
  }
725
- const env = buildEnv(check);
749
+ const env = buildEnv(check, execution.env);
726
750
  let terminationReason = null;
727
751
  let timeoutHandle;
728
752
  const timeoutController = new AbortController();
@@ -737,10 +761,10 @@ async function spawnCheck(checkId, check, runSignal, activeHandles) {
737
761
  runSignal !== void 0 ? [runSignal, timeoutController.signal] : [timeoutController.signal]
738
762
  );
739
763
  return new Promise((resolve) => {
740
- const child = crossSpawn__default.default(command, args, {
764
+ const child = execution.spawn(command, args, {
741
765
  cwd: check.cwd,
742
766
  env,
743
- shell: check.shell === true,
767
+ shell: effectiveShell,
744
768
  detached: shouldSpawnDetached(),
745
769
  // Windows-only cosmetic behavior (suppresses a console window flash)
746
770
  // with no effect on stdout/stderr/exitCode/signal on any platform, and
@@ -755,7 +779,11 @@ async function spawnCheck(checkId, check, runSignal, activeHandles) {
755
779
  let hostTerminated = false;
756
780
  const bestEffortKillTree = (pid, signal) => {
757
781
  try {
758
- killTree(pid, signal);
782
+ killTree(pid, signal, execution.killProcessTree);
783
+ } catch {
784
+ }
785
+ try {
786
+ child.kill(signal);
759
787
  } catch {
760
788
  }
761
789
  };
@@ -894,7 +922,7 @@ function resolveCheckDependencies(checks, requestedChecks) {
894
922
  for (const checkId of requestedChecks) visit(checkId);
895
923
  return Object.entries(checks).filter(([checkId]) => required.has(checkId));
896
924
  }
897
- async function runChecks(checks, concurrency, options) {
925
+ async function runChecks(checks, concurrency, execution, options) {
898
926
  const entries = options?.checks ? resolveCheckDependencies(checks, options.checks) : Object.entries(checks);
899
927
  const activeHandles = /* @__PURE__ */ new Set();
900
928
  const hostAbortController = new AbortController();
@@ -915,7 +943,7 @@ async function runChecks(checks, concurrency, options) {
915
943
  )
916
944
  );
917
945
  const worker = async ([checkId, check]) => {
918
- const evidence = await spawnCheck(checkId, check, runSignal, activeHandles);
946
+ const evidence = await spawnCheck(checkId, check, runSignal, activeHandles, execution);
919
947
  return [checkId, check, evidence];
920
948
  };
921
949
  try {
@@ -1047,7 +1075,13 @@ function runRepoContract(config, options) {
1047
1075
  async function runRepoContractAfterValidation(config, options) {
1048
1076
  const concurrency = config.concurrency ?? os__namespace.availableParallelism();
1049
1077
  const startedAt = /* @__PURE__ */ new Date();
1050
- const results = await runChecks(config.checks, concurrency, options);
1078
+ const execution = {
1079
+ spawn: config.spawn,
1080
+ killProcessTree: config.killProcessTree,
1081
+ env: config.env,
1082
+ shell: config.shell ?? false
1083
+ };
1084
+ const results = await runChecks(config.checks, concurrency, execution, options);
1051
1085
  const completedAt = /* @__PURE__ */ new Date();
1052
1086
  const { evidence, entries } = await buildEvidence(results, startedAt, completedAt);
1053
1087
  const verdict = await runPolicies(entries, evidence);