superpowers-manager 0.5.2 → 0.6.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/README.md +12 -10
- package/dist/cli.js +8 -13
- package/dist/commands/install.js +2 -3
- package/dist/commands/mutation.js +7 -0
- package/dist/commands/prepare.js +5 -51
- package/dist/commands/probe.js +8 -10
- package/dist/commands/uninstall.js +2 -2
- package/dist/effective-selection.js +3 -3
- package/dist/harnesses/codex/adapter.js +259 -509
- package/dist/harnesses/codex/compatibility.js +2 -17
- package/dist/harnesses/codex/harness.js +18 -263
- package/dist/harnesses/codex/lifecycle.js +110 -54
- package/dist/harnesses/codex/manifest-overlay.js +22 -17
- package/dist/harnesses/codex/presentation.js +6 -14
- package/dist/harnesses/codex/publication.js +13 -31
- package/dist/python-json-format.js +0 -69
- package/dist/python-json.js +2 -27
- package/dist/python-text.js +9 -69
- package/dist/validator.js +35 -72
- package/package.json +1 -1
- package/dist/selection-state-cli.js +0 -85
- package/dist/upstream-cli.js +0 -110
|
@@ -3,9 +3,8 @@ import { failureResult, successResult, } from "../../adapter-result.js";
|
|
|
3
3
|
import { beginDirectoryPublication, } from "../../atomic.js";
|
|
4
4
|
import { digestArtifactTree } from "../../artifact-tree.js";
|
|
5
5
|
import { assertNoFollowType, classifyPathNoFollow } from "../../safe-path.js";
|
|
6
|
-
import {
|
|
6
|
+
import { codexInspectControl, codexInspectOwnership, codexInstallRefreshMode, codexRemove, codexReadNativeState, } from "./adapter.js";
|
|
7
7
|
import { readCodexAssessment } from "./compatibility.js";
|
|
8
|
-
import { requireManagedUpdateControl, requireNoLegacyState, } from "./lifecycle.js";
|
|
9
8
|
import { readCodexMarketplace, stageCodexMarketplace, } from "./marketplace.js";
|
|
10
9
|
import { assertCodexPreparationSeparate, codexPathFailureDetails, codexPaths, } from "./paths.js";
|
|
11
10
|
import { codexPreparationLocation } from "./prepare.js";
|
|
@@ -13,7 +12,8 @@ import { beginCodexRecovery, finishCodexRecovery, readCodexRecovery, verifyCodex
|
|
|
13
12
|
import { hasFilesystemAccessFailure, pathsEqual } from "./state.js";
|
|
14
13
|
const DEFAULTS = {
|
|
15
14
|
readNative: codexReadNativeState,
|
|
16
|
-
|
|
15
|
+
inspectOwnership: codexInspectOwnership,
|
|
16
|
+
inspectControl: codexInspectControl,
|
|
17
17
|
beginPublication: beginDirectoryPublication,
|
|
18
18
|
};
|
|
19
19
|
const REMOVAL_DEFAULTS = {
|
|
@@ -26,16 +26,6 @@ function fail(code, message, messages) {
|
|
|
26
26
|
function removalFailure(code, message, messages) {
|
|
27
27
|
return failureResult("uninstall-codex", code, message, [], messages);
|
|
28
28
|
}
|
|
29
|
-
function objectResult(result) {
|
|
30
|
-
if (result.status !== 0 ||
|
|
31
|
-
!result.outcome.ok ||
|
|
32
|
-
result.outcome.result === null ||
|
|
33
|
-
typeof result.outcome.result !== "object" ||
|
|
34
|
-
Array.isArray(result.outcome.result)) {
|
|
35
|
-
throw new Error("Codex inspection failed");
|
|
36
|
-
}
|
|
37
|
-
return result.outcome.result;
|
|
38
|
-
}
|
|
39
29
|
function acceptedNative(result) {
|
|
40
30
|
if (result.status !== 0 || !result.outcome.ok) {
|
|
41
31
|
throw new Error("Codex native observation failed");
|
|
@@ -54,26 +44,18 @@ function acceptedNative(result) {
|
|
|
54
44
|
return value;
|
|
55
45
|
}
|
|
56
46
|
function requireOwnership(result) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
requireNoLegacyState(value.identity_state).kind !== "ok" ||
|
|
60
|
-
!Array.isArray(value.conflicts) ||
|
|
61
|
-
value.conflicts.length !== 0) {
|
|
62
|
-
throw new Error("Codex ownership blocks publication");
|
|
47
|
+
if (result.status !== 0 || !result.outcome.ok) {
|
|
48
|
+
throw new Error("Codex inspection failed");
|
|
63
49
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
resources === null ||
|
|
67
|
-
Array.isArray(resources) ||
|
|
68
|
-
typeof resources.plugin !== "boolean" ||
|
|
69
|
-
typeof resources.marketplace !== "boolean") {
|
|
70
|
-
throw new Error("Codex ownership inspection is malformed");
|
|
50
|
+
if (result.outcome.result?.installEligibility?.kind !== "allowed") {
|
|
51
|
+
throw new Error("Codex ownership blocks publication");
|
|
71
52
|
}
|
|
72
53
|
}
|
|
73
54
|
function requireControl(result) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
55
|
+
if (result.status !== 0 || !result.outcome.ok) {
|
|
56
|
+
throw new Error("Codex inspection failed");
|
|
57
|
+
}
|
|
58
|
+
if (result.outcome.result?.mutationEligibility?.kind !== "allowed") {
|
|
77
59
|
throw new Error("Codex update control blocks publication");
|
|
78
60
|
}
|
|
79
61
|
}
|
|
@@ -262,10 +244,10 @@ async function observeNative(ctx, dependencies, messages) {
|
|
|
262
244
|
return acceptedNative(result);
|
|
263
245
|
}
|
|
264
246
|
async function requireEligibility(ctx, dependencies, messages) {
|
|
265
|
-
const ownership = await dependencies.
|
|
247
|
+
const ownership = await dependencies.inspectOwnership(ctx);
|
|
266
248
|
messages.push(...ownership.outcome.messages);
|
|
267
249
|
requireOwnership(ownership);
|
|
268
|
-
const control = await dependencies.
|
|
250
|
+
const control = await dependencies.inspectControl(ctx);
|
|
269
251
|
messages.push(...control.outcome.messages);
|
|
270
252
|
requireControl(control);
|
|
271
253
|
}
|
|
@@ -1,72 +1,3 @@
|
|
|
1
|
-
import { SafetyError } from "./safety-error.js";
|
|
2
|
-
/**
|
|
3
|
-
* Render a JSON number token the way CPython's `json.dump` does.
|
|
4
|
-
*
|
|
5
|
-
* Classification is by source text, matching CPython's own: a token containing
|
|
6
|
-
* `.` or `e`/`E` parses as a float, anything else as an arbitrary-width int.
|
|
7
|
-
* Integers are emitted verbatim so widths above 2^53 survive; `-0` is the only
|
|
8
|
-
* normalization JSON's grammar leaves room for, since leading zeros and a
|
|
9
|
-
* leading `+` are already forbidden.
|
|
10
|
-
*/
|
|
11
|
-
export function formatPythonNumber(raw) {
|
|
12
|
-
if (!/[.eE]/.test(raw)) {
|
|
13
|
-
// NaN/Infinity/-Infinity contain no `.` or `e`/`E`, so they would
|
|
14
|
-
// otherwise fall into the "emit verbatim" integer branch and echo back
|
|
15
|
-
// as invalid JSON. A reader running a non-standard-constants accept
|
|
16
|
-
// profile (see src/strict-json.ts) can hand this function exactly these
|
|
17
|
-
// three tokens as a raw number source, so this function must reject them
|
|
18
|
-
// itself rather than assume its caller already did. CPython's
|
|
19
|
-
// `json.dump(..., allow_nan=False)` raises ValueError for all three.
|
|
20
|
-
if (raw === "NaN" || raw === "Infinity" || raw === "-Infinity") {
|
|
21
|
-
throw new SafetyError("manifest-overlay", `JSON number out of range: ${raw}`);
|
|
22
|
-
}
|
|
23
|
-
return raw === "-0" ? "0" : raw;
|
|
24
|
-
}
|
|
25
|
-
const value = Number(raw);
|
|
26
|
-
if (!Number.isFinite(value)) {
|
|
27
|
-
throw new SafetyError("manifest-overlay", `JSON number out of range: ${raw}`);
|
|
28
|
-
}
|
|
29
|
-
return formatPythonFloat(value);
|
|
30
|
-
}
|
|
31
|
-
function formatPythonFloat(value) {
|
|
32
|
-
// toExponential() drops the sign of negative zero; CPython's repr keeps it.
|
|
33
|
-
// Note this differs from the integer -0, which normalizes to 0 — consistent,
|
|
34
|
-
// because -0 has no `.` or `e` and so classifies as an integer.
|
|
35
|
-
if (Object.is(value, -0))
|
|
36
|
-
return "-0.0";
|
|
37
|
-
// Called with no argument, toExponential() yields the shortest digit string
|
|
38
|
-
// that uniquely identifies the double — the same guarantee CPython's repr
|
|
39
|
-
// relies on. Taking digits and exponent separately is what lets us apply
|
|
40
|
-
// CPython's notation threshold instead of JavaScript's.
|
|
41
|
-
const [mantissa, exponentText] = value.toExponential().split("e");
|
|
42
|
-
const exponent = Number(exponentText);
|
|
43
|
-
const negative = mantissa.startsWith("-");
|
|
44
|
-
const digits = mantissa.replace("-", "").replace(".", "");
|
|
45
|
-
let body;
|
|
46
|
-
if (exponent >= -4 && exponent < 16) {
|
|
47
|
-
// Fixed notation, built by MOVING the decimal point through the digit
|
|
48
|
-
// string above. Do NOT route this through value.toFixed(): toExponential()
|
|
49
|
-
// has already produced the shortest digits that identify this double, and
|
|
50
|
-
// toFixed performs a second, independent rounding of the full decimal
|
|
51
|
-
// expansion. The two disagree — 1888570120608320.2 formats as
|
|
52
|
-
// ...320.3 via toFixed and ...320.2 in CPython. Measured 2026-07-31.
|
|
53
|
-
if (exponent >= digits.length - 1) {
|
|
54
|
-
body = `${digits}${"0".repeat(exponent - (digits.length - 1))}.0`;
|
|
55
|
-
}
|
|
56
|
-
else if (exponent >= 0) {
|
|
57
|
-
body = `${digits.slice(0, exponent + 1)}.${digits.slice(exponent + 1)}`;
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
body = `0.${"0".repeat(-exponent - 1)}${digits}`;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
const sign = exponent < 0 ? "-" : "+";
|
|
65
|
-
const magnitude = String(Math.abs(exponent)).padStart(2, "0");
|
|
66
|
-
body = `${mantissa.replace("-", "")}e${sign}${magnitude}`;
|
|
67
|
-
}
|
|
68
|
-
return negative ? `-${body}` : body;
|
|
69
|
-
}
|
|
70
1
|
/**
|
|
71
2
|
* Apply CPython's `ensure_ascii=True` to an already-JSON-encoded string. Input
|
|
72
3
|
* must be the output of JSON.stringify, which has already escaped quotes,
|
package/dist/python-json.js
CHANGED
|
@@ -1,29 +1,4 @@
|
|
|
1
|
+
import { escapeNonAscii } from "./python-json-format.js";
|
|
1
2
|
export function escapePythonJsonString(value) {
|
|
2
|
-
|
|
3
|
-
const shortEscapes = {
|
|
4
|
-
0x08: "\\b",
|
|
5
|
-
0x09: "\\t",
|
|
6
|
-
0x0a: "\\n",
|
|
7
|
-
0x0c: "\\f",
|
|
8
|
-
0x0d: "\\r",
|
|
9
|
-
};
|
|
10
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
11
|
-
const code = value.charCodeAt(index);
|
|
12
|
-
if (code === 0x22) {
|
|
13
|
-
escaped += '\\"';
|
|
14
|
-
}
|
|
15
|
-
else if (code === 0x5c) {
|
|
16
|
-
escaped += "\\\\";
|
|
17
|
-
}
|
|
18
|
-
else if (Object.hasOwn(shortEscapes, code)) {
|
|
19
|
-
escaped += shortEscapes[code];
|
|
20
|
-
}
|
|
21
|
-
else if (code < 0x20 || code >= 0x7f) {
|
|
22
|
-
escaped += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
escaped += value[index];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return escaped;
|
|
3
|
+
return escapeNonAscii(JSON.stringify(value).slice(1, -1));
|
|
29
4
|
}
|
package/dist/python-text.js
CHANGED
|
@@ -1,75 +1,15 @@
|
|
|
1
|
-
//
|
|
2
|
-
// CPython applies them. Deliberately excludes U+FEFF, which JavaScript's
|
|
3
|
-
// `trim()` removes and Python's `strip()` does not.
|
|
4
|
-
const PYTHON_WHITESPACE = new Set([
|
|
5
|
-
"\t",
|
|
6
|
-
"\n",
|
|
7
|
-
"\v",
|
|
8
|
-
"\f",
|
|
9
|
-
"\r",
|
|
10
|
-
" ",
|
|
11
|
-
"\x1c",
|
|
12
|
-
"\x1d",
|
|
13
|
-
"\x1e",
|
|
14
|
-
"\x1f",
|
|
15
|
-
"\x85",
|
|
16
|
-
" ",
|
|
17
|
-
" ",
|
|
18
|
-
" ",
|
|
19
|
-
" ",
|
|
20
|
-
" ",
|
|
21
|
-
" ",
|
|
22
|
-
" ",
|
|
23
|
-
" ",
|
|
24
|
-
" ",
|
|
25
|
-
" ",
|
|
26
|
-
" ",
|
|
27
|
-
" ",
|
|
28
|
-
" ",
|
|
29
|
-
"
",
|
|
30
|
-
"
",
|
|
31
|
-
" ",
|
|
32
|
-
" ",
|
|
33
|
-
" ",
|
|
34
|
-
]);
|
|
35
|
-
// CPython `str.splitlines()` boundaries. `\r\n` is handled as a pair below.
|
|
36
|
-
const PYTHON_LINE_BOUNDARIES = new Set([
|
|
37
|
-
"\n",
|
|
38
|
-
"\r",
|
|
39
|
-
"\v",
|
|
40
|
-
"\f",
|
|
41
|
-
"\x1c",
|
|
42
|
-
"\x1d",
|
|
43
|
-
"\x1e",
|
|
44
|
-
"\x85",
|
|
45
|
-
"
",
|
|
46
|
-
"
",
|
|
47
|
-
]);
|
|
1
|
+
// Match first through last non-whitespace; avoid suffix retries on internal runs.
|
|
48
2
|
export function pythonStrip(value) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
start += 1;
|
|
53
|
-
while (end > start && PYTHON_WHITESPACE.has(value[end - 1]))
|
|
54
|
-
end -= 1;
|
|
55
|
-
return value.slice(start, end);
|
|
3
|
+
return (value.match(
|
|
4
|
+
// oxlint-disable-next-line no-control-regex -- exact CPython whitespace includes C0 separators.
|
|
5
|
+
/[^\u0009-\u000d\u001c-\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028-\u2029\u202f\u205f\u3000](?:[\s\S]*[^\u0009-\u000d\u001c-\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028-\u2029\u202f\u205f\u3000])?/)?.[0] ?? "");
|
|
56
6
|
}
|
|
7
|
+
// CRLF is one boundary; a terminating boundary adds no final empty line.
|
|
57
8
|
export function pythonSplitlines(value) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const character = value[index];
|
|
63
|
-
if (!PYTHON_LINE_BOUNDARIES.has(character)) {
|
|
64
|
-
index += 1;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
lines.push(value.slice(start, index));
|
|
68
|
-
index += character === "\r" && value[index + 1] === "\n" ? 2 : 1;
|
|
69
|
-
start = index;
|
|
70
|
-
}
|
|
71
|
-
if (start < value.length)
|
|
72
|
-
lines.push(value.slice(start));
|
|
9
|
+
// oxlint-disable-next-line no-control-regex -- CPython boundaries include C0 controls.
|
|
10
|
+
const lines = value.split(/\r\n|[\n\r\v\f\u001c-\u001e\u0085\u2028\u2029]/);
|
|
11
|
+
if (lines.at(-1) === "")
|
|
12
|
+
lines.pop();
|
|
73
13
|
return lines;
|
|
74
14
|
}
|
|
75
15
|
/** Python `sorted()` orders by code point; JavaScript's default sort does not. */
|
package/dist/validator.js
CHANGED
|
@@ -2,21 +2,16 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { lstat, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { sep } from "node:path";
|
|
4
4
|
import { hasTerminalControl, pythonUnicodeEscapeBytes, } from "./adapter-result.js";
|
|
5
|
-
// The legacy SUPERPOWERS_VALIDATOR path. Its behaviour is frozen by parity, so it
|
|
6
|
-
// carries no limits at all; see the spec's D2. Bounding it is PR 12's row.
|
|
7
|
-
export const UNBOUNDED_LEGACY = { kind: "unbounded" };
|
|
8
5
|
// SUPERPOWERS_VALIDATOR_EXECUTABLE. 30s is ~270x a measured realistic validator run
|
|
9
6
|
// (0.11s, dominated by interpreter startup); 64 KiB per stream clears a
|
|
10
7
|
// one-diagnostic-line-per-file report (30 KB on a real tree) with headroom.
|
|
11
8
|
export const BOUNDED_EXECUTABLE = {
|
|
12
|
-
kind: "bounded",
|
|
13
9
|
timeoutMs: 30_000,
|
|
14
10
|
graceMs: 2_000,
|
|
15
11
|
drainMs: 200,
|
|
16
12
|
maxBytesPerStream: 64 * 1024,
|
|
17
13
|
};
|
|
18
|
-
// Retains the first `limit` bytes and counts the rest.
|
|
19
|
-
// everything, which is the unbounded policy's contract. push() always consumes the
|
|
14
|
+
// Retains the first `limit` bytes and counts the rest. push() always consumes the
|
|
20
15
|
// whole chunk: ceasing to consume would block the child on a full pipe.
|
|
21
16
|
class Sink {
|
|
22
17
|
chunks = [];
|
|
@@ -27,10 +22,6 @@ class Sink {
|
|
|
27
22
|
this.limit = limit;
|
|
28
23
|
}
|
|
29
24
|
push(chunk) {
|
|
30
|
-
if (this.limit === null) {
|
|
31
|
-
this.chunks.push(chunk);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
25
|
const room = this.limit - this.kept;
|
|
35
26
|
if (room <= 0) {
|
|
36
27
|
this.dropped += chunk.length;
|
|
@@ -42,21 +33,15 @@ class Sink {
|
|
|
42
33
|
this.dropped += chunk.length - take.length;
|
|
43
34
|
}
|
|
44
35
|
done() {
|
|
45
|
-
// Buffer.toString
|
|
46
|
-
// legacy path's current string accumulator preserves it. Measured:
|
|
47
|
-
// decode(<BOM>hi) is "hi" but toString("utf8") is "\ufeffhi". The legacy path
|
|
48
|
-
// must be byte-identical, so BOTH paths use toString.
|
|
36
|
+
// Buffer.toString preserves a leading BOM; TextDecoder would strip it.
|
|
49
37
|
return {
|
|
50
38
|
text: Buffer.concat(this.chunks).toString("utf8"),
|
|
51
39
|
droppedBytes: this.dropped,
|
|
52
40
|
};
|
|
53
41
|
}
|
|
54
42
|
}
|
|
55
|
-
// Signal the child's whole process group.
|
|
56
|
-
//
|
|
57
|
-
// leads a group of its own and -pid reaches its descendants. The LEGACY path
|
|
58
|
-
// spawns without `detached`, so its child does not lead a group and this
|
|
59
|
-
// function is not for it. Wrapped because the group is already gone in the
|
|
43
|
+
// Signal the child's whole process group. The child leads a group of its own,
|
|
44
|
+
// so -pid reaches descendants. Wrapped because the group is already gone in
|
|
60
45
|
// ordinary race between SIGTERM and SIGKILL.
|
|
61
46
|
function signalGroup(child, signal) {
|
|
62
47
|
if (child.pid === undefined)
|
|
@@ -71,9 +56,8 @@ function signalGroup(child, signal) {
|
|
|
71
56
|
export function runValidator(argv, policy, env, workspace, cwd) {
|
|
72
57
|
return new Promise((resolveRun) => {
|
|
73
58
|
const [command, ...args] = argv;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const err = new Sink(limit);
|
|
59
|
+
const out = new Sink(policy.maxBytesPerStream);
|
|
60
|
+
const err = new Sink(policy.maxBytesPerStream);
|
|
77
61
|
let settled = false;
|
|
78
62
|
// Settlement and termination are independent BY CONSTRUCTION, which is why
|
|
79
63
|
// there is no grace-timer handle to clear here: the timedOut settle is nested
|
|
@@ -113,20 +97,15 @@ export function runValidator(argv, policy, env, workspace, cwd) {
|
|
|
113
97
|
env: { ...process.env, ...env, TMPDIR: workspace },
|
|
114
98
|
cwd,
|
|
115
99
|
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
// (measured at 5279 ms against a 300 ms timeout). The LEGACY path must not
|
|
120
|
-
// acquire group leadership: that changes process and session semantics for
|
|
121
|
-
// a path whose whole contract is that it is unchanged.
|
|
122
|
-
detached: policy.kind === "bounded",
|
|
100
|
+
// The child leads its own process group so a timeout can signal the GROUP --
|
|
101
|
+
// signalling the process alone leaves descendants holding inherited pipes.
|
|
102
|
+
detached: true,
|
|
123
103
|
});
|
|
124
104
|
}
|
|
125
105
|
catch (cause) {
|
|
126
106
|
settle({
|
|
127
107
|
kind: "launchFailed",
|
|
128
108
|
errno: cause.code ?? "UNKNOWN",
|
|
129
|
-
cause,
|
|
130
109
|
});
|
|
131
110
|
return;
|
|
132
111
|
}
|
|
@@ -136,7 +115,7 @@ export function runValidator(argv, policy, env, workspace, cwd) {
|
|
|
136
115
|
child.stderr.on("data", (chunk) => err.push(chunk));
|
|
137
116
|
// Both error and close fire on a launch failure, so settle() is idempotent.
|
|
138
117
|
child.on("error", (cause) => {
|
|
139
|
-
settle({ kind: "launchFailed", errno: cause.code ?? "UNKNOWN"
|
|
118
|
+
settle({ kind: "launchFailed", errno: cause.code ?? "UNKNOWN" });
|
|
140
119
|
});
|
|
141
120
|
child.on("close", (code) => {
|
|
142
121
|
// On the timeout path, settling here would resolve BEFORE SIGKILL and the
|
|
@@ -154,12 +133,6 @@ export function runValidator(argv, policy, env, workspace, cwd) {
|
|
|
154
133
|
// may lag it indefinitely. Capture the code here or a successful validator
|
|
155
134
|
// whose descendant lingers would settle with a null code and be rejected.
|
|
156
135
|
lastCode = code;
|
|
157
|
-
// BOUNDED ONLY. The legacy path settles on `close`, exactly as it does today:
|
|
158
|
-
// a validator that backgrounds `(sleep 0.6; echo late) &` has "late" captured
|
|
159
|
-
// under `close` and lost under a 200 ms drain, and that output is captured
|
|
160
|
-
// today.
|
|
161
|
-
if (policy.kind !== "bounded")
|
|
162
|
-
return;
|
|
163
136
|
// The TIMEOUT path owns its own settlement, after the escalation has run.
|
|
164
137
|
if (timedOut)
|
|
165
138
|
return;
|
|
@@ -186,31 +159,29 @@ export function runValidator(argv, policy, env, workspace, cwd) {
|
|
|
186
159
|
});
|
|
187
160
|
}, policy.drainMs);
|
|
188
161
|
});
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
162
|
+
timeoutTimer = setTimeout(() => {
|
|
163
|
+
timedOut = true;
|
|
164
|
+
// Signal the GROUP, not the process: descendants hold the pipes. SIGTERM
|
|
165
|
+
// first, because the contract promises stderr carries the reason, and
|
|
166
|
+
// reading continues through the grace window so the child can print it.
|
|
167
|
+
signalGroup(child, "SIGTERM");
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
signalGroup(child, "SIGKILL");
|
|
170
|
+
// Settle ONLY after SIGKILL has been sent. An earlier revision settled
|
|
171
|
+
// first and left the escalation on an unref'ed timer, which never fires
|
|
172
|
+
// because the CLI exits as soon as its handler returns -- measured, the
|
|
173
|
+
// manager exited at 146 ms and the descendant survived. The wall-clock
|
|
174
|
+
// bound is therefore timeout + grace + drain, which is still a bound.
|
|
196
175
|
setTimeout(() => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
afterMs: policy.timeoutMs,
|
|
207
|
-
stdout: out.done(),
|
|
208
|
-
stderr: err.done(),
|
|
209
|
-
});
|
|
210
|
-
}, policy.drainMs);
|
|
211
|
-
}, policy.graceMs);
|
|
212
|
-
}, policy.timeoutMs);
|
|
213
|
-
}
|
|
176
|
+
settle({
|
|
177
|
+
kind: "timedOut",
|
|
178
|
+
afterMs: policy.timeoutMs,
|
|
179
|
+
stdout: out.done(),
|
|
180
|
+
stderr: err.done(),
|
|
181
|
+
});
|
|
182
|
+
}, policy.drainMs);
|
|
183
|
+
}, policy.graceMs);
|
|
184
|
+
}, policy.timeoutMs);
|
|
214
185
|
});
|
|
215
186
|
}
|
|
216
187
|
// Every probe below is best-effort BY DESIGN: spawn is the authority on whether the
|
|
@@ -297,22 +268,14 @@ const VALIDATOR_COMMANDS = new Set([
|
|
|
297
268
|
"install",
|
|
298
269
|
"update",
|
|
299
270
|
]);
|
|
300
|
-
// Legacy emptiness semantics exactly: empty string means unset, whitespace-only
|
|
301
|
-
// counts as set. No trimming — a trim would silently change legacy behaviour.
|
|
302
|
-
export function bothConfigured(env) {
|
|
303
|
-
return ((env.SUPERPOWERS_VALIDATOR || "").length > 0 &&
|
|
304
|
-
(env.SUPERPOWERS_VALIDATOR_EXECUTABLE || "").length > 0);
|
|
305
|
-
}
|
|
306
271
|
// The second exported accessor preflight derives its enforcement from. It is an
|
|
307
272
|
// accessor rather than an inline check because an accessor that under-reports what
|
|
308
273
|
// preflight enforces is the blind spot slice 2 closed.
|
|
309
274
|
export function configurationErrors(cmd, env) {
|
|
310
|
-
if (!VALIDATOR_COMMANDS.has(cmd))
|
|
311
|
-
return [];
|
|
312
|
-
if (!bothConfigured(env))
|
|
275
|
+
if (!VALIDATOR_COMMANDS.has(cmd) || !env.SUPERPOWERS_VALIDATOR)
|
|
313
276
|
return [];
|
|
314
277
|
return [
|
|
315
|
-
"SUPERPOWERS_VALIDATOR
|
|
316
|
-
"
|
|
278
|
+
"SUPERPOWERS_VALIDATOR has been removed; unset it and configure " +
|
|
279
|
+
"SUPERPOWERS_VALIDATOR_EXECUTABLE with an executable validator.",
|
|
317
280
|
];
|
|
318
281
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superpowers-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Install obra/superpowers directly from upstream for Codex and Pi. Stay on a known-good release or commit, try a branch, and upgrade when you choose.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"superpowers",
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs";
|
|
2
|
-
import { writeFile } from "node:fs/promises";
|
|
3
|
-
import { displaySource, normalizePinnedArguments, normalizeSaved, validateSource, } from "./selection.js";
|
|
4
|
-
import { readSelectionState, writeSelectionState } from "./selection-store.js";
|
|
5
|
-
import { oneLine, parseFlags, UsageError } from "./cli-arguments.js";
|
|
6
|
-
const COMMAND_FLAGS = {
|
|
7
|
-
read: ["path", "output"],
|
|
8
|
-
"write-pinned": ["path", "source", "requested-ref", "resolved-ref", "commit"],
|
|
9
|
-
"write-track-latest": ["path", "source"],
|
|
10
|
-
"validate-source": ["source"],
|
|
11
|
-
"display-source": ["source"],
|
|
12
|
-
};
|
|
13
|
-
function parseCommand(argv) {
|
|
14
|
-
const [name, ...rest] = argv;
|
|
15
|
-
if (name === undefined)
|
|
16
|
-
throw new UsageError("a subcommand is required");
|
|
17
|
-
if (!Object.hasOwn(COMMAND_FLAGS, name)) {
|
|
18
|
-
throw new UsageError(`unknown subcommand: ${name}`);
|
|
19
|
-
}
|
|
20
|
-
const command = name;
|
|
21
|
-
return { name: command, flags: parseFlags(rest, COMMAND_FLAGS[command]) };
|
|
22
|
-
}
|
|
23
|
-
async function dispatch({ name, flags }) {
|
|
24
|
-
if (name === "read") {
|
|
25
|
-
const normalized = normalizeSaved(await readSelectionState(flags.path));
|
|
26
|
-
try {
|
|
27
|
-
await writeFile(flags.output, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
28
|
-
}
|
|
29
|
-
catch (cause) {
|
|
30
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
31
|
-
throw new Error(`cannot write normalized selection output ${flags.output}: ${message}`, { cause });
|
|
32
|
-
}
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (name === "write-pinned") {
|
|
36
|
-
await writeSelectionState(flags.path, normalizePinnedArguments({
|
|
37
|
-
source: flags.source,
|
|
38
|
-
requestedRef: flags["requested-ref"],
|
|
39
|
-
resolvedRef: flags["resolved-ref"],
|
|
40
|
-
commit: flags.commit,
|
|
41
|
-
}));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (name === "write-track-latest") {
|
|
45
|
-
await writeSelectionState(flags.path, {
|
|
46
|
-
schema_version: 1,
|
|
47
|
-
mode: "track-latest",
|
|
48
|
-
source: flags.source,
|
|
49
|
-
});
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (name === "validate-source") {
|
|
53
|
-
validateSource(flags.source);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (name === "display-source") {
|
|
57
|
-
process.stdout.write(`${displaySource(flags.source)}\n`);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const unreachable = name;
|
|
61
|
-
throw new Error(`unhandled subcommand: ${String(unreachable)}`);
|
|
62
|
-
}
|
|
63
|
-
export async function runSelectionStateCli(argv) {
|
|
64
|
-
let command;
|
|
65
|
-
try {
|
|
66
|
-
command = parseCommand(argv);
|
|
67
|
-
}
|
|
68
|
-
catch (cause) {
|
|
69
|
-
process.stderr.write(`usage: selection-state-cli <subcommand> [options]\n`);
|
|
70
|
-
process.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
71
|
-
return 2;
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
await dispatch(command);
|
|
75
|
-
return 0;
|
|
76
|
-
}
|
|
77
|
-
catch (cause) {
|
|
78
|
-
process.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
79
|
-
return 1;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const entry = process.argv[1];
|
|
83
|
-
if (entry !== undefined && import.meta.filename === realpathSync(entry)) {
|
|
84
|
-
process.exitCode = await runSelectionStateCli(process.argv.slice(2));
|
|
85
|
-
}
|