superpowers-manager 0.5.2 → 0.7.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.
Files changed (83) hide show
  1. package/README.md +44 -26
  2. package/dist/adapter-result.js +5 -9
  3. package/dist/atomic.js +4 -14
  4. package/dist/cli-arguments.js +1 -27
  5. package/dist/cli.js +32 -52
  6. package/dist/commands/adapter-call.js +28 -0
  7. package/dist/commands/install.js +26 -107
  8. package/dist/commands/mutation.js +7 -0
  9. package/dist/commands/pin.js +1 -1
  10. package/dist/commands/prepare.js +39 -129
  11. package/dist/commands/probe.js +35 -264
  12. package/dist/commands/track-latest.js +1 -1
  13. package/dist/commands/uninstall.js +16 -92
  14. package/dist/commands/update.js +5 -25
  15. package/dist/effective-selection.js +7 -9
  16. package/dist/git-tree.js +85 -0
  17. package/dist/harness-command-result.js +47 -0
  18. package/dist/harnesses/codex/adapter.js +231 -510
  19. package/dist/harnesses/codex/compatibility.js +2 -17
  20. package/dist/harnesses/codex/conflicts.js +2 -3
  21. package/dist/harnesses/codex/harness.js +18 -263
  22. package/dist/harnesses/codex/hooks.js +1 -1
  23. package/dist/harnesses/codex/json.js +1 -11
  24. package/dist/harnesses/codex/lifecycle.js +80 -72
  25. package/dist/harnesses/codex/manifest-overlay.js +23 -18
  26. package/dist/harnesses/codex/paths.js +14 -29
  27. package/dist/harnesses/codex/presentation.js +8 -37
  28. package/dist/harnesses/codex/publication.js +13 -37
  29. package/dist/harnesses/codex/recovery.js +5 -5
  30. package/dist/harnesses/codex/stored-state.js +1 -1
  31. package/dist/harnesses/opencode/config.js +256 -0
  32. package/dist/harnesses/opencode/discovery.js +587 -0
  33. package/dist/harnesses/opencode/harness.js +37 -0
  34. package/dist/harnesses/opencode/install.js +601 -0
  35. package/dist/harnesses/opencode/native.js +57 -0
  36. package/dist/harnesses/opencode/package.js +89 -0
  37. package/dist/harnesses/opencode/paths.js +38 -0
  38. package/dist/harnesses/opencode/prepare.js +30 -0
  39. package/dist/harnesses/opencode/presentation.js +28 -0
  40. package/dist/harnesses/opencode/state.js +174 -0
  41. package/dist/harnesses/pi/compatibility.js +5 -37
  42. package/dist/harnesses/pi/install.js +7 -5
  43. package/dist/harnesses/pi/native.js +3 -47
  44. package/dist/harnesses/pi/package.js +5 -107
  45. package/dist/harnesses/pi/prepare.js +13 -104
  46. package/dist/harnesses/pi/presentation.js +23 -66
  47. package/dist/harnesses/pi/shared-skills.js +28 -39
  48. package/dist/harnesses/pi/state.js +7 -27
  49. package/dist/provenance.js +2 -2
  50. package/dist/python-json-format.js +0 -69
  51. package/dist/python-json.js +2 -27
  52. package/dist/python-text.js +9 -69
  53. package/dist/safe-path.js +2 -2
  54. package/dist/selection-store.js +2 -22
  55. package/dist/snapshot-package.js +78 -0
  56. package/dist/snapshot-prepare.js +99 -0
  57. package/dist/snapshot-presentation.js +44 -0
  58. package/dist/validator.js +39 -73
  59. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  60. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  61. package/node_modules/jsonc-parser/README.md +364 -0
  62. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  63. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  64. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  65. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  66. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  67. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  68. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  69. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  70. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  71. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  72. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  73. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  74. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  75. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  76. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  77. package/node_modules/jsonc-parser/package.json +37 -0
  78. package/package.json +8 -4
  79. package/dist/domain/fingerprint.js +0 -4
  80. package/dist/selection-state-cli.js +0 -85
  81. package/dist/status.js +0 -24
  82. package/dist/upstream-cli.js +0 -110
  83. package/dist/validate-generated-plugin-cli.js +0 -118
@@ -1,72 +1,21 @@
1
- // Ports scripts/uninstall. The shell sourced common.sh, provenance.sh,
2
- // lifecycle.sh and adapter.sh; the predicates now live in src/harnesses/codex/lifecycle.ts
3
- // and the adapter arrives through ctx.adapter.
4
1
  import { tmpdir } from "node:os";
5
2
  import { oneLine } from "../cli-arguments.js";
6
3
  import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
4
+ import { GatherFailure, callAdapter, } from "./adapter-call.js";
7
5
  import { runWithMutation } from "./mutation.js";
8
6
  import { replayOutcome } from "./probe.js";
9
7
  async function invoke(call, failure, outcomes) {
10
- let result;
11
- try {
12
- result = await call();
13
- }
14
- catch {
15
- return { ok: false, message: failure.unexpected };
16
- }
17
- outcomes.push(result.outcome);
18
- const outcome = result.outcome;
19
- if (result.status !== 0 || !outcome.ok) {
20
- return {
21
- ok: false,
22
- message: outcome.ok ? failure.invalidStatus : null,
23
- };
24
- }
25
- return {
26
- ok: true,
27
- result: { status: result.status, outcome },
28
- };
29
- }
30
- // withWorkspace throws for mkdtemp failure before the callback ever runs
31
- // ("cannot create workspace",
32
- // `src/workspace.ts:120::throw new SafetyError("workspace", "cannot create workspace"`).
33
- // A bare re-throw would silently drop every outcome collected before that
34
- // point -- a narrow
35
- // DIAG-ADAPTER-01 regression the shell never had, since it replayed each
36
- // adapter response as it went rather than batching replay to the end. This
37
- // carries the outcomes collected so far alongside the original cause, so
38
- // runUninstall's catch can still replay them before reporting the cause.
39
- //
40
- // The post-success cleanup failure no longer reaches here: onCleanupFailure
41
- // below suppresses withWorkspace's throw for that case and records the
42
- // warning as data, so the computed UninstallOutcome survives it. The
43
- // outcome-carrying is still load-bearing for mkdtemp, and the shape stays
44
- // identical to src/commands/install.ts's GatherFailure.
45
- class GatherFailure extends Error {
46
- inner;
47
- outcomes;
48
- constructor(inner, outcomes) {
49
- super("uninstall gather failed");
50
- this.inner = inner;
51
- this.outcomes = outcomes;
52
- }
8
+ const result = await callAdapter(call, failure);
9
+ if (result.result !== null)
10
+ outcomes.push(result.result.outcome);
11
+ return result;
53
12
  }
54
- // Every step that can throw or fail closed, returning the outcome as data and
55
- // performing NO writes. Same shape as gatherProbe
56
- // (`src/commands/probe.ts::readonly facts: ProbeSnapshot<R>;`) and for the same
57
- // reason: a write inside this try could raise EPIPE, be caught here, and be
58
- // relabelled as a domain failure.
13
+ // Collect outcomes without writing so output failures cannot be classified as
14
+ // inspection failures. Replay collected outcomes after gathering completes.
59
15
  async function gatherUninstall(ctx) {
60
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:20-21::TMPDIR=` exported TMPDIR="$uninstall_workspace" so every
61
- // child confined its temporary files to the tree the workspace trap
62
- // removed. The AdapterContext passed to ctx.adapter below does the same.
63
16
  const parent = ctx.env.TMPDIR ?? tmpdir();
64
- // Declared OUTSIDE the withWorkspace callback (rather than inside, as an
65
- // earlier draft had it) so the catch below -- which wraps the ENTIRE
66
- // withWorkspace call, not just the callback -- can still see whatever was
67
- // collected before a workspace throw. Both `mkdtemp` failure (nothing
68
- // collected yet) and a post-success cleanup failure (everything the
69
- // callback collected) reach this same array.
17
+ // Kept outside the workspace callback so a workspace failure preserves every
18
+ // outcome collected before it.
70
19
  const outcomes = [];
71
20
  let cleanupWarning = null;
72
21
  try {
@@ -90,9 +39,8 @@ async function gatherUninstall(ctx) {
90
39
  const removed = await invoke(() => ctx.adapter.remove(removalInput, adapterContext), removeFailure, outcomes);
91
40
  if (!removed.ok)
92
41
  return failed(removed.message);
93
- // Stage 3: inspect ownership AGAIN. This overwrites the first
94
- // inspection (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:29-30::spw_verify_uninstalled_resources`), so everything below reads the
95
- // POST-uninstall state, not the pre-uninstall one read above.
42
+ // Stage 3: inspect ownership again. Everything below reads this
43
+ // post-removal state, not the inspection before removal.
96
44
  const afterFailure = ctx.adapter.presentation.callFailure("post-remove", adapterContext);
97
45
  const after = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), afterFailure, outcomes);
98
46
  if (!after.ok)
@@ -125,13 +73,13 @@ async function gatherUninstall(ctx) {
125
73
  catch (cause) {
126
74
  // Reachable only for mkdtemp failure, with nothing collected yet: this
127
75
  // callback never throws -- every ctx.adapter throw is already caught
128
- // inside invoke(), and presenceFlag/verifyUninstalledResources/
76
+ // inside invoke(), and presenceFlag/
129
77
  // reportLegacyState are pure (src/harnesses/codex/lifecycle.ts's header comment) -- so a
130
78
  // post-success cleanup failure is handled by onCleanupFailure above and
131
79
  // cannot reach here. Wrapping with `outcomes` anyway keeps the class
132
80
  // total over its declared contract rather than assuming the callback's
133
81
  // purity at the throw site.
134
- throw new GatherFailure(cause, outcomes);
82
+ throw new GatherFailure("uninstall gather failed", cause, outcomes);
135
83
  }
136
84
  }
137
85
  export async function runUninstall(argv, ctx) {
@@ -147,30 +95,8 @@ async function performUninstall(argv, ctx) {
147
95
  run = await gatherUninstall(ctx);
148
96
  }
149
97
  catch (cause) {
150
- // gatherUninstall throws exactly one shape: GatherFailure, wrapping
151
- // withWorkspace's "cannot create workspace" SafetyError
152
- // (`src/workspace.ts:120::throw new SafetyError("workspace", "cannot create workspace"`),
153
- // alongside whatever outcomes were collected before that throw -- none,
154
- // for that cause. Replaying first, before
155
- // reporting the cause, keeps the arm honest for any outcome-bearing
156
- // throw the class is declared to carry (DIAG-ADAPTER-01). The
157
- // post-success cleanup failure no longer arrives here: gatherUninstall's
158
- // onCleanupFailure records it as `cleanupWarning` and the computed
159
- // outcome survives, which is what `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:34-35::complete` did. The
160
- // `instanceof` guard is defensive rather than load-bearing --
161
- // gatherUninstall's own catch is the only thing that can throw here, and
162
- // it always wraps -- but this catch does not assume that invariant
163
- // blindly.
164
- //
165
- // A cause outside ctx.adapter's AdapterFailure guard
166
- // (`src/harnesses/codex/adapter.ts:1081::async function runCodexOperation<T = JsonValue>(`) does NOT
167
- // reach here: invoke() catches it inside gatherUninstall and converts it
168
- // to a hand-written message carried as UninstallOutcome data, exactly as
169
- // src/commands/probe.ts's inspect() does for the same cause.
170
- //
171
- // gatherUninstall performs no writes of its own, so this catch cannot
172
- // also be reached by an EPIPE from uninstall's own output -- every write
173
- // below runs only after this try/catch has resolved.
98
+ // Replay collected outcomes before reporting a gather failure. The
99
+ // cleanup reporter retains post-success cleanup failures as data.
174
100
  const outcomes = cause instanceof GatherFailure ? cause.outcomes : [];
175
101
  for (const outcome of outcomes)
176
102
  replayOutcome(outcome, ctx);
@@ -179,9 +105,7 @@ async function performUninstall(argv, ctx) {
179
105
  return 1;
180
106
  }
181
107
  const { outcome, cleanupWarning } = run;
182
- // Replay first, on both paths: the shell validator replayed every
183
- // response's messages whether or not that response was a failure
184
- // (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/validate-adapter-response.py:268::replay(messages)`).
108
+ // Replay before reporting a command-level result on either path.
185
109
  for (const each of outcome.outcomes)
186
110
  replayOutcome(each, ctx);
187
111
  let status;
@@ -1,21 +1,10 @@
1
- // Ports scripts/update. The shell sourced common.sh and lifecycle.sh; the
2
- // predicates now live in src/harnesses/codex/lifecycle.ts. Unlike install and uninstall,
3
- // update issues no ctx.adapter call of its own: every adapter interaction
4
- // this module needs already happens inside gatherProbe, runPrepare or
5
- // runInstall, each of which owns its own §4.2a-conformant invoke() gate. That
6
- // is also why update opens no temporary workspace of its own -- there is no
7
- // mutation here for one to protect.
1
+ // Update delegates adapter calls to probe, prepare, and install. Those
2
+ // mutating commands own their workspaces, so update opens none itself.
8
3
  import { oneLine } from "../cli-arguments.js";
4
+ import { runInstall, writeOutput } from "./install.js";
9
5
  import { gatherProbe, replayOutcome } from "./probe.js";
10
- import { runInstall } from "./install.js";
11
6
  import { runPrepare } from "./prepare.js";
12
7
  import { runWithMutation } from "./mutation.js";
13
- function writeOutput(output, ctx) {
14
- for (const line of output.stdout)
15
- ctx.stdout.write(`${line}\n`);
16
- for (const line of output.stderr)
17
- ctx.stderr.write(`${line}\n`);
18
- }
19
8
  export async function runUpdate(argv, ctx) {
20
9
  return await runWithMutation("update", ctx, async (scoped) => performUpdate(argv, scoped));
21
10
  }
@@ -28,17 +17,8 @@ async function performUpdate(argv, ctx) {
28
17
  probe = await gatherProbe(ctx);
29
18
  }
30
19
  catch (cause) {
31
- // gatherProbe performs no writes of its own (src/commands/probe.ts), so
32
- // this catch cannot also be reached by an EPIPE from update's own
33
- // output -- update writes nothing before this call resolves, unlike
34
- // install's NOTE line.
35
- //
36
- // This is a THIRD consumer of gatherProbe's throw channel --
37
- // `src/commands/probe.ts:469-527::THREE exceptions, all inherited and none a regression:`'s
38
- // runProbe catch is the first and src/commands/install.ts's runInstall catch
39
- // is the second. Because all three wrap the identical function, runProbe's
40
- // long comment there enumerates exactly what can reach this stream too; not
41
- // repeated here.
20
+ // Gathering does not write, so this catch cannot misclassify an output
21
+ // failure. oneLine() bounds any inherited diagnostic to one line.
42
22
  ctx.stderr.write(`error: ${oneLine(cause)}\n`);
43
23
  return 1;
44
24
  }
@@ -34,19 +34,18 @@ export function selectionConfigDir(env) {
34
34
  export function selectionStatePath(env) {
35
35
  return `${selectionConfigDir(env)}/selection.json`;
36
36
  }
37
- // Composes exactly what `src/selection-state-cli.ts:39::const normalized`
38
- // composes. The shell wrote a normalized document to a mktemp file and read
39
- // five fields back with five python3 invocations; in-process this is one call.
37
+ // Normalizes the record returned by readSelectionState directly. The shell
38
+ // wrote a normalized document to a mktemp file and read five fields back with
39
+ // five python3 invocations; in-process this is one call.
40
40
  export async function loadSavedSelection(env) {
41
41
  return normalizeSaved(await readSelectionState(selectionStatePath(env)));
42
42
  }
43
43
  export const UPSTREAM_URL_DEFAULT = "https://github.com/obra/superpowers";
44
44
  // Ports the env > saved > package-default precedence ladder from
45
45
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/selection.sh:88-162::spw_compute_effective_selection`.
46
- // validateSource runs before any ref resolution -- pinned by
47
- // tests/unit/effective-selection.test.js's "source validation precedes ref
48
- // resolution": a credential-bearing source must fail before Git is ever
49
- // invoked.
46
+ // validateSource runs before any ref resolution. A credential-bearing source
47
+ // must fail before Git is invoked; the resolver-log check lives in
48
+ // `tests/baseline/selection-location.test.ts::SEL-PRECEDENCE-VALIDATE-01 invalid saved state stops resolution`.
50
49
  export async function computeEffectiveSelection(root, env) {
51
50
  const saved = await loadSavedSelection(env);
52
51
  let upstreamSourceOrigin;
@@ -63,8 +62,7 @@ export async function computeEffectiveSelection(root, env) {
63
62
  upstreamSourceOrigin = "package-default";
64
63
  effectiveSource = UPSTREAM_URL_DEFAULT;
65
64
  }
66
- // Before any Git access. Pinned by tests/unit/effective-selection.test.js's
67
- // "source validation precedes ref resolution".
65
+ // Before any Git access; the selection-location test above checks the log.
68
66
  validateSource(effectiveSource);
69
67
  let selectionOrigin;
70
68
  let selectionMode;
@@ -0,0 +1,85 @@
1
+ import { execFile } from "node:child_process";
2
+ import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { ARTIFACT_RECEIPT } from "./artifact-tree.js";
6
+ import { COMMIT_RE } from "./domain/refs.js";
7
+ import { assertNoFollowType, assertSymlinkTargetContained, } from "./safe-path.js";
8
+ import { SafetyError } from "./safety-error.js";
9
+ const exec = promisify(execFile);
10
+ const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
11
+ async function gitBytes(repository, args) {
12
+ try {
13
+ return (await exec("git", ["--no-replace-objects", "-C", repository, ...args], {
14
+ encoding: null,
15
+ maxBuffer: 256 * 1024 * 1024,
16
+ })).stdout;
17
+ }
18
+ catch (cause) {
19
+ throw new SafetyError("git-tree", `cannot read Git tree objects from repository: ${repository}`, { cause });
20
+ }
21
+ }
22
+ export async function materializeGitTree(repository, commit, destination) {
23
+ try {
24
+ if (!COMMIT_RE.test(commit))
25
+ throw new Error("commit");
26
+ if (!(await gitBytes(repository, ["cat-file", "-t", commit])).equals(Buffer.from("commit\n")))
27
+ throw new Error("not a commit");
28
+ await assertNoFollowType(destination, ["missing"]);
29
+ const bytes = await gitBytes(repository, [
30
+ "ls-tree",
31
+ "-rz",
32
+ "--full-tree",
33
+ commit,
34
+ ]);
35
+ const entries = [];
36
+ let offset = 0;
37
+ while (offset < bytes.length) {
38
+ const end = bytes.indexOf(0, offset);
39
+ if (end < 0)
40
+ throw new Error("tree framing");
41
+ const record = DECODER.decode(bytes.subarray(offset, end));
42
+ offset = end + 1;
43
+ const match = /^(100644|100755|120000) blob ([a-f0-9]{40})\t([\s\S]+)$/.exec(record);
44
+ if (!match)
45
+ throw new Error("tree entry");
46
+ const path = match[3];
47
+ if (path
48
+ .split("/")
49
+ .some((part) => !part ||
50
+ part === "." ||
51
+ part === ".." ||
52
+ part.toLowerCase() === ".git") ||
53
+ isAbsolute(path) ||
54
+ path.includes("\\") ||
55
+ path.split("/")[0] === ARTIFACT_RECEIPT)
56
+ throw new Error("tree path");
57
+ entries.push({ mode: match[1], oid: match[2], path });
58
+ }
59
+ await mkdir(destination, { recursive: true });
60
+ for (const entry of entries)
61
+ await mkdir(dirname(join(destination, entry.path)), { recursive: true });
62
+ for (const entry of entries.filter((entry) => entry.mode !== "120000")) {
63
+ const path = join(destination, entry.path), mode = entry.mode === "100755" ? 0o755 : 0o644;
64
+ await writeFile(path, await gitBytes(repository, ["cat-file", "blob", entry.oid]), { flag: "wx", mode });
65
+ await chmod(path, mode);
66
+ }
67
+ for (const entry of entries.filter((entry) => entry.mode === "120000")) {
68
+ const target = DECODER.decode(await gitBytes(repository, ["cat-file", "blob", entry.oid]));
69
+ const path = join(destination, entry.path), suffix = relative(resolve(destination), resolve(dirname(path), target));
70
+ if (!target ||
71
+ target.includes("\0") ||
72
+ isAbsolute(target) ||
73
+ suffix === ".." ||
74
+ suffix.startsWith("../") ||
75
+ isAbsolute(suffix))
76
+ throw new Error("link escape");
77
+ await symlink(target, path);
78
+ }
79
+ for (const entry of entries.filter((entry) => entry.mode === "120000"))
80
+ await assertSymlinkTargetContained(destination, join(destination, entry.path));
81
+ }
82
+ catch (cause) {
83
+ throw new SafetyError("git-tree", `cannot materialize Git tree from ${repository} into ${destination}`, { cause });
84
+ }
85
+ }
@@ -0,0 +1,47 @@
1
+ import { AdapterMessageLog, failureResult, successResult, } from "./adapter-result.js";
2
+ import { SEMVER_RE } from "./domain/refs.js";
3
+ function appendCaptured(log, label, channel, captured) {
4
+ log.appendBytes(channel, Buffer.from(captured.text));
5
+ if (captured.droppedBytes > 0)
6
+ log.appendText(channel, `${label} ${channel} omitted ${captured.droppedBytes} bytes at the capture limit`);
7
+ }
8
+ export function nativeCommandResult(label, result) {
9
+ const operation = `${label.toLowerCase()}-command`;
10
+ if (result.kind === "launchFailed") {
11
+ const errno = /^[A-Z][A-Z0-9_]*$/u.test(result.errno)
12
+ ? result.errno
13
+ : "UNKNOWN";
14
+ return failureResult(operation, "launch-failed", `cannot launch configured ${label} executable (${errno})`, [], []);
15
+ }
16
+ const log = new AdapterMessageLog();
17
+ if (result.kind === "timedOut") {
18
+ appendCaptured(log, label, "stdout", result.stdout);
19
+ appendCaptured(log, label, "stderr", result.stderr);
20
+ return failureResult(operation, "timeout", `${label} command timed out after ${result.afterMs} ms`, [], log.snapshot());
21
+ }
22
+ if (result.code !== 0) {
23
+ appendCaptured(log, label, "stdout", result.stdout);
24
+ appendCaptured(log, label, "stderr", result.stderr);
25
+ return failureResult(operation, "nonzero-exit", result.code === null
26
+ ? `${label} command exited without a status`
27
+ : `${label} command exited with status ${result.code}`, [], log.snapshot());
28
+ }
29
+ if (result.stdout.droppedBytes > 0 || result.stderr.droppedBytes > 0) {
30
+ appendCaptured(log, label, "stdout", result.stdout);
31
+ appendCaptured(log, label, "stderr", result.stderr);
32
+ return failureResult(operation, "output-limit", `${label} command output exceeded the capture limit`, [], log.snapshot());
33
+ }
34
+ appendCaptured(log, label, "stderr", result.stderr);
35
+ return successResult(operation, { stdout: result.stdout.text }, log.snapshot());
36
+ }
37
+ export function normalizeSnapshotRuntimeVersion(label, result) {
38
+ if (!result.outcome.ok)
39
+ return { status: result.status, outcome: result.outcome };
40
+ const operation = `${label.toLowerCase()}-runtime`;
41
+ if (result.status !== 0)
42
+ return failureResult(operation, "invalid-status", `${label} runtime inspection returned a nonzero status`, [], result.outcome.messages);
43
+ const version = result.outcome.result.stdout.trim();
44
+ if (!SEMVER_RE.test(version))
45
+ return failureResult(operation, "invalid-version", `${label} runtime inspection returned an invalid version response`, [], result.outcome.messages);
46
+ return successResult(operation, version, result.outcome.messages);
47
+ }