superpowers-manager 0.8.0 → 0.9.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 +36 -16
- package/dist/atomic.js +1 -3
- package/dist/cli-arguments.js +4 -1
- package/dist/cli.js +14 -43
- package/dist/commands/adapter-call.js +24 -0
- package/dist/commands/install.js +16 -63
- package/dist/commands/mutation.js +5 -5
- package/dist/commands/pin.js +10 -5
- package/dist/commands/prepare.js +42 -108
- package/dist/commands/probe.js +33 -19
- package/dist/commands/uninstall.js +18 -46
- package/dist/commands/unpin.js +2 -1
- package/dist/commands/update.js +8 -29
- package/dist/domain/refs.js +0 -3
- package/dist/effective-selection.js +11 -15
- package/dist/git-tree.js +4 -6
- package/dist/harness-command-result.js +33 -3
- package/dist/harnesses/claude-code/harness.js +39 -0
- package/dist/harnesses/claude-code/install.js +380 -0
- package/dist/harnesses/claude-code/native.js +79 -0
- package/dist/harnesses/claude-code/paths.js +35 -0
- package/dist/harnesses/claude-code/prepare.js +117 -0
- package/dist/harnesses/claude-code/presentation.js +25 -0
- package/dist/harnesses/claude-code/state.js +253 -0
- package/dist/harnesses/codex/adapter.js +27 -68
- package/dist/harnesses/codex/conflicts.js +3 -8
- package/dist/harnesses/codex/generated-plugin.js +4 -11
- package/dist/harnesses/codex/harness.js +4 -26
- package/dist/harnesses/codex/hooks.js +11 -27
- package/dist/harnesses/codex/lifecycle.js +22 -107
- package/dist/harnesses/codex/manifest-overlay.js +9 -27
- package/dist/harnesses/codex/paths.js +29 -0
- package/dist/harnesses/codex/prepare.js +9 -43
- package/dist/harnesses/codex/presentation.js +1 -1
- package/dist/harnesses/codex/publication.js +5 -40
- package/dist/harnesses/codex/state.js +1 -6
- package/dist/harnesses/codex/stored-state.js +1 -6
- package/dist/harnesses/opencode/config.js +2 -4
- package/dist/harnesses/opencode/discovery.js +1 -5
- package/dist/harnesses/opencode/install.js +12 -93
- package/dist/harnesses/opencode/native.js +28 -49
- package/dist/harnesses/opencode/package.js +11 -55
- package/dist/harnesses/opencode/prepare.js +2 -7
- package/dist/harnesses/opencode/presentation.js +2 -7
- package/dist/harnesses/pi/install.js +31 -120
- package/dist/harnesses/pi/native.js +9 -34
- package/dist/harnesses/pi/package.js +9 -50
- package/dist/harnesses/pi/prepare.js +3 -8
- package/dist/harnesses/pi/presentation.js +2 -7
- package/dist/harnesses/pi/settings.js +5 -9
- package/dist/harnesses/pi/shared-skills.js +23 -53
- package/dist/harnesses/pi/state.js +3 -10
- package/dist/provenance.js +3 -19
- package/dist/resource-lock.js +11 -16
- package/dist/safe-path.js +28 -1
- package/dist/selection.js +15 -19
- package/dist/snapshot-journal.js +101 -0
- package/dist/snapshot-package.js +62 -1
- package/dist/snapshot-prepare.js +9 -2
- package/dist/snapshot-presentation.js +9 -0
- package/dist/upstream.js +8 -12
- package/dist/validator.js +26 -23
- package/dist/workspace.js +29 -35
- package/package.json +10 -9
- package/dist/python-json.js +0 -4
- package/dist/upstream-workspace.js +0 -8
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { copyFile, cp, mkdir, readdir, readFile, readlink,
|
|
1
|
+
import { copyFile, cp, mkdir, readdir, readFile, readlink, symlink, } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, } from "../../safe-path.js";
|
|
3
|
+
import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, isDirectory, isFile, } from "../../safe-path.js";
|
|
4
4
|
import { SafetyError } from "../../safety-error.js";
|
|
5
5
|
import { parseStrictJson } from "../../strict-json.js";
|
|
6
6
|
// Annotated, not `as const`, matching
|
|
7
|
-
// `src/provenance.ts:
|
|
7
|
+
// `src/provenance.ts:19::const PROVENANCE_STRICT_PROFILE` and
|
|
8
8
|
// `src/selection-store.ts:15::const SELECTION_JSON_PROFILE`. The annotation
|
|
9
9
|
// rejects a misspelled or unsupported profile field here rather than at the
|
|
10
10
|
// parseStrictJson call.
|
|
@@ -14,8 +14,8 @@ const MANIFEST_PROFILE = {
|
|
|
14
14
|
maxDepth: 256,
|
|
15
15
|
};
|
|
16
16
|
// Private helper, matching
|
|
17
|
-
// `src/selection.ts:
|
|
18
|
-
// `src/upstream.ts:
|
|
17
|
+
// `src/selection.ts:35::return new SafetyError("selection"` and
|
|
18
|
+
// `src/upstream.ts:36::function upstreamError`. No new exported error class:
|
|
19
19
|
// SafetyError already carries module and cause.
|
|
20
20
|
function hookError(message, cause) {
|
|
21
21
|
return new SafetyError("hooks", message, { cause });
|
|
@@ -37,22 +37,6 @@ async function classifyOwned(path, label) {
|
|
|
37
37
|
throw ownedPathError(label, path, cause);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
async function isRegularFileFollowing(path) {
|
|
41
|
-
try {
|
|
42
|
-
return (await stat(path)).isFile();
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
async function isDirectoryFollowing(path) {
|
|
49
|
-
try {
|
|
50
|
-
return (await stat(path)).isDirectory();
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
40
|
async function requireExistingContained(root, path, label) {
|
|
57
41
|
try {
|
|
58
42
|
await assertExistingContained(root, path);
|
|
@@ -108,7 +92,7 @@ function isEmptyObject(value) {
|
|
|
108
92
|
async function checkedFile(root, relativePath) {
|
|
109
93
|
const source = resolve(root, relativePath);
|
|
110
94
|
await requireExistingContained(root, source, "declared hook source");
|
|
111
|
-
if (!(await
|
|
95
|
+
if (!(await isFile(source))) {
|
|
112
96
|
throw hookError(`declared hook source is not a regular file: ${source}`);
|
|
113
97
|
}
|
|
114
98
|
return source;
|
|
@@ -132,14 +116,14 @@ export async function classifyHooks(manifest, source, upstreamRoot) {
|
|
|
132
116
|
const hooksRootPresent = (await classifyOwned(hooksRoot, "hook subtree")) !== "missing";
|
|
133
117
|
if (!declared) {
|
|
134
118
|
return {
|
|
135
|
-
copyHooksSubtree: await
|
|
119
|
+
copyHooksSubtree: await isFile(defaultConfig),
|
|
136
120
|
declaredPaths: [],
|
|
137
121
|
};
|
|
138
122
|
}
|
|
139
123
|
const hooks = manifest.hooks;
|
|
140
124
|
if (isEmptyArray(hooks)) {
|
|
141
125
|
return {
|
|
142
|
-
copyHooksSubtree: await
|
|
126
|
+
copyHooksSubtree: await isFile(defaultConfig),
|
|
143
127
|
declaredPaths: [],
|
|
144
128
|
};
|
|
145
129
|
}
|
|
@@ -179,7 +163,7 @@ async function checkedDestination(root, relativePath) {
|
|
|
179
163
|
return destination;
|
|
180
164
|
}
|
|
181
165
|
await requireExistingContained(root, destination, "declared hook destination");
|
|
182
|
-
if (!(await
|
|
166
|
+
if (!(await isFile(destination))) {
|
|
183
167
|
throw hookError(`declared hook destination is not a regular file: ${destination}`);
|
|
184
168
|
}
|
|
185
169
|
return destination;
|
|
@@ -218,7 +202,7 @@ async function validateSubtreeSymlinks(tree, containmentRoot) {
|
|
|
218
202
|
catch (cause) {
|
|
219
203
|
throw hookError(`hook subtree escapes or is broken: ${tree}`, cause);
|
|
220
204
|
}
|
|
221
|
-
if (!(await
|
|
205
|
+
if (!(await isDirectory(tree))) {
|
|
222
206
|
throw hookError(`hook subtree is not a directory: ${tree}`);
|
|
223
207
|
}
|
|
224
208
|
for (const path of await collectEntries(tree)) {
|
|
@@ -252,7 +236,7 @@ async function validateMaterializedDestination(destination, candidateRoot) {
|
|
|
252
236
|
// re-owned. Discarding it would erase why containment failed.
|
|
253
237
|
throw hookError(message, cause);
|
|
254
238
|
}
|
|
255
|
-
if (!(await
|
|
239
|
+
if (!(await isFile(destination))) {
|
|
256
240
|
throw hookError(message);
|
|
257
241
|
}
|
|
258
242
|
}
|
|
@@ -23,120 +23,35 @@ const REPORT_LINES = [
|
|
|
23
23
|
"Legacy superpowers-wrapper Codex state remains installed.",
|
|
24
24
|
"Run: npx superpowers-wrapper@0.1.1 uninstall",
|
|
25
25
|
];
|
|
26
|
-
function
|
|
27
|
-
return {
|
|
28
|
-
kind: "unknown",
|
|
29
|
-
message: `unknown adapter identity state: ${identityState}`,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:43-60::spw_require_no_legacy_state`
|
|
33
|
-
export function requireNoLegacyState(identityState) {
|
|
34
|
-
if (identityState === "neither" || identityState === "manager") {
|
|
35
|
-
return { kind: "ok" };
|
|
36
|
-
}
|
|
37
|
-
if (identityState === "legacy" || identityState === "both") {
|
|
38
|
-
return { kind: "blocked", lines: BLOCKED_LINES };
|
|
39
|
-
}
|
|
40
|
-
return unknownState(identityState);
|
|
41
|
-
}
|
|
42
|
-
// Ported from
|
|
43
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:72-85::spw_report_legacy_state`.
|
|
44
|
-
// Same enumeration, different disposition: this one reports and continues
|
|
45
|
-
// rather than blocking, so its clean arm and its legacy arm are both non-fatal.
|
|
46
|
-
export function reportLegacyState(identityState) {
|
|
47
|
-
if (identityState === "neither" || identityState === "manager") {
|
|
48
|
-
return { kind: "ok" };
|
|
49
|
-
}
|
|
50
|
-
if (identityState === "legacy" || identityState === "both") {
|
|
51
|
-
return { kind: "report", lines: REPORT_LINES };
|
|
52
|
-
}
|
|
53
|
-
return unknownState(identityState);
|
|
54
|
-
}
|
|
55
|
-
function installDecision(legacy) {
|
|
56
|
-
switch (legacy.kind) {
|
|
57
|
-
case "ok":
|
|
58
|
-
return { kind: "allowed" };
|
|
59
|
-
case "blocked":
|
|
60
|
-
return {
|
|
61
|
-
kind: "blocked",
|
|
62
|
-
output: { stdout: [], stderr: legacy.lines },
|
|
63
|
-
};
|
|
64
|
-
case "unknown":
|
|
65
|
-
return {
|
|
66
|
-
kind: "blocked",
|
|
67
|
-
output: { stdout: [], stderr: [`error: ${legacy.message}`] },
|
|
68
|
-
};
|
|
69
|
-
case "report":
|
|
70
|
-
return {
|
|
71
|
-
kind: "blocked",
|
|
72
|
-
output: {
|
|
73
|
-
stdout: [],
|
|
74
|
-
stderr: ["error: unexpected legacy report during installation"],
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
26
|
+
function blocked(stderr) {
|
|
27
|
+
return { kind: "blocked", output: { stdout: [], stderr } };
|
|
78
28
|
}
|
|
79
29
|
export function codexOwnershipInspection(identityState, removalInput, conflicts) {
|
|
30
|
+
const legacy = identityState === "legacy" || identityState === "both";
|
|
80
31
|
const { pluginPresent, marketplacePresent } = removalInput;
|
|
81
|
-
const
|
|
82
|
-
?
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
?
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"Conflicting unmanaged Superpowers Codex resources require manual resolution:",
|
|
98
|
-
...conflicts.map((conflict) => `- ${conflict}`),
|
|
99
|
-
"Remove or disable each resource manually, then retry.",
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
const legacyReport = reportLegacyState(identityState);
|
|
104
|
-
let removalVerification;
|
|
105
|
-
if (pluginPresent || marketplacePresent) {
|
|
106
|
-
const message = pluginPresent
|
|
107
|
-
? "owned plugin resource is still installed after removal"
|
|
108
|
-
: "owned marketplace resource is still registered after removal";
|
|
109
|
-
removalVerification = {
|
|
110
|
-
kind: "blocked",
|
|
111
|
-
output: { stdout: [], stderr: ["error: " + message] },
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
else if (legacyReport.kind === "unknown") {
|
|
115
|
-
removalVerification = {
|
|
116
|
-
kind: "blocked",
|
|
117
|
-
output: { stdout: [], stderr: [`error: ${legacyReport.message}`] },
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
else if (legacyReport.kind === "blocked") {
|
|
121
|
-
removalVerification = {
|
|
122
|
-
kind: "blocked",
|
|
123
|
-
output: {
|
|
124
|
-
stdout: [],
|
|
125
|
-
stderr: ["error: unexpected legacy block after removal"],
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
removalVerification = { kind: "allowed" };
|
|
131
|
-
}
|
|
132
|
-
const postRemovalOutput = legacyReport.kind === "report"
|
|
133
|
-
? { stdout: legacyReport.lines, stderr: [] }
|
|
134
|
-
: { stdout: [], stderr: [] };
|
|
32
|
+
const installEligibility = legacy
|
|
33
|
+
? blocked(BLOCKED_LINES)
|
|
34
|
+
: conflicts.length > 0
|
|
35
|
+
? blocked([
|
|
36
|
+
"Conflicting unmanaged Superpowers Codex resources require manual resolution:",
|
|
37
|
+
...conflicts.map((conflict) => `- ${conflict}`),
|
|
38
|
+
"Remove or disable each resource manually, then retry.",
|
|
39
|
+
])
|
|
40
|
+
: { kind: "allowed" };
|
|
41
|
+
const removalVerification = pluginPresent || marketplacePresent
|
|
42
|
+
? blocked([
|
|
43
|
+
pluginPresent
|
|
44
|
+
? "error: owned plugin resource is still installed after removal"
|
|
45
|
+
: "error: owned marketplace resource is still registered after removal",
|
|
46
|
+
])
|
|
47
|
+
: { kind: "allowed" };
|
|
135
48
|
return {
|
|
136
49
|
installEligibility,
|
|
137
50
|
removalInput,
|
|
138
51
|
removalVerification,
|
|
139
|
-
postRemovalOutput
|
|
52
|
+
postRemovalOutput: legacy
|
|
53
|
+
? { stdout: REPORT_LINES, stderr: [] }
|
|
54
|
+
: { stdout: [], stderr: [] },
|
|
140
55
|
presentationValue: identityState,
|
|
141
56
|
presentationConflicts: conflicts,
|
|
142
57
|
};
|
|
@@ -39,29 +39,11 @@ export function applyManifestOverlay(source, version, path) {
|
|
|
39
39
|
const entries = parsed.entries.map(([key, value]) => [key, value]);
|
|
40
40
|
setMember(entries, "version", version);
|
|
41
41
|
setMember(entries, "skills", "./skills/");
|
|
42
|
-
|
|
43
|
-
return `${emitObject(entries, 0)}\n`;
|
|
44
|
-
}
|
|
45
|
-
catch (cause) {
|
|
46
|
-
throw rewrapNumberOutOfRange(cause, path);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// emitNumber has no manifest path, so it throws a bare
|
|
50
|
-
// `JSON number out of range: ${raw}`. This rewrap adds the path without
|
|
51
|
-
// touching the local emitter's own contract, and is scoped to exactly this
|
|
52
|
-
// message so it cannot swallow or reword any other overlay throw.
|
|
53
|
-
const NUMBER_OUT_OF_RANGE_PREFIX = "JSON number out of range: ";
|
|
54
|
-
function rewrapNumberOutOfRange(cause, path) {
|
|
55
|
-
if (cause instanceof SafetyError &&
|
|
56
|
-
cause.message.startsWith(NUMBER_OUT_OF_RANGE_PREFIX)) {
|
|
57
|
-
const raw = cause.message.slice(NUMBER_OUT_OF_RANGE_PREFIX.length);
|
|
58
|
-
return new SafetyError("manifest-overlay", `JSON number out of range in ${path}: ${raw}`, { cause });
|
|
59
|
-
}
|
|
60
|
-
return cause;
|
|
42
|
+
return `${emitObject(entries, 0, path)}\n`;
|
|
61
43
|
}
|
|
62
|
-
function emitNumber(raw) {
|
|
44
|
+
function emitNumber(raw, path) {
|
|
63
45
|
if (/[.eE]/.test(raw) && !Number.isFinite(Number(raw))) {
|
|
64
|
-
throw new SafetyError("manifest-overlay", `JSON number out of range: ${raw}`);
|
|
46
|
+
throw new SafetyError("manifest-overlay", `JSON number out of range in ${path}: ${raw}`);
|
|
65
47
|
}
|
|
66
48
|
return raw;
|
|
67
49
|
}
|
|
@@ -100,7 +82,7 @@ function translate(cause, source, path) {
|
|
|
100
82
|
const column = (newlines[newlines.length - 1]?.length ?? 0) + 1;
|
|
101
83
|
return new SafetyError("manifest-overlay", `invalid manifest JSON in ${path}: line ${line} column ${column}: ${reason}`, { cause });
|
|
102
84
|
}
|
|
103
|
-
function emit(value, depth) {
|
|
85
|
+
function emit(value, depth, path) {
|
|
104
86
|
if (value === null)
|
|
105
87
|
return "null";
|
|
106
88
|
if (typeof value === "boolean")
|
|
@@ -110,21 +92,21 @@ function emit(value, depth) {
|
|
|
110
92
|
// Order matters: isRawNumber before isRawObject, and both before Array —
|
|
111
93
|
// the branded records are objects too.
|
|
112
94
|
if (isRawNumber(value))
|
|
113
|
-
return emitNumber(value.source);
|
|
95
|
+
return emitNumber(value.source, path);
|
|
114
96
|
if (isRawObject(value))
|
|
115
|
-
return emitObject(value.entries, depth);
|
|
97
|
+
return emitObject(value.entries, depth, path);
|
|
116
98
|
const inner = " ".repeat(depth + 1);
|
|
117
99
|
const outer = " ".repeat(depth);
|
|
118
100
|
if (value.length === 0)
|
|
119
101
|
return "[]";
|
|
120
|
-
const items = value.map((item) => `${inner}${emit(item, depth + 1)}`);
|
|
102
|
+
const items = value.map((item) => `${inner}${emit(item, depth + 1, path)}`);
|
|
121
103
|
return `[\n${items.join(",\n")}\n${outer}]`;
|
|
122
104
|
}
|
|
123
|
-
function emitObject(entries, depth) {
|
|
105
|
+
function emitObject(entries, depth, path) {
|
|
124
106
|
if (entries.length === 0)
|
|
125
107
|
return "{}";
|
|
126
108
|
const inner = " ".repeat(depth + 1);
|
|
127
109
|
const outer = " ".repeat(depth);
|
|
128
|
-
const members = entries.map(([key, item]) => `${inner}${escapeNonAscii(JSON.stringify(key))}: ${emit(item, depth + 1)}`);
|
|
110
|
+
const members = entries.map(([key, item]) => `${inner}${escapeNonAscii(JSON.stringify(key))}: ${emit(item, depth + 1, path)}`);
|
|
129
111
|
return `{\n${members.join(",\n")}\n${outer}}`;
|
|
130
112
|
}
|
|
@@ -51,6 +51,35 @@ async function inspectRoot(path, root) {
|
|
|
51
51
|
export function codexPathFailureDetails(cause) {
|
|
52
52
|
return cause instanceof CodexPathError ? (cause.details ?? null) : null;
|
|
53
53
|
}
|
|
54
|
+
// One mapping from a preparation-separation failure to the refusal an
|
|
55
|
+
// operation reports. Callers differ only in the code for a non-recovery
|
|
56
|
+
// refusal and in the suffix on the recovery message.
|
|
57
|
+
export function codexPathRefusal(cause, paths, preparedRoot, refusedCode, recoverySuffix) {
|
|
58
|
+
const failure = codexPathFailureDetails(cause);
|
|
59
|
+
if (failure?.kind === "inspection") {
|
|
60
|
+
if (failure.root === "recovery") {
|
|
61
|
+
return {
|
|
62
|
+
code: "recovery-required",
|
|
63
|
+
message: `cannot inspect Codex recovery state at ${paths.recoveryRoot}${recoverySuffix}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
code: refusedCode,
|
|
68
|
+
message: failure.root === "marketplace"
|
|
69
|
+
? `cannot inspect Codex marketplace storage at ${paths.marketplaceRoot}`
|
|
70
|
+
: `cannot inspect Codex preparation root at ${preparedRoot}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return failure?.kind === "overlap"
|
|
74
|
+
? {
|
|
75
|
+
code: "preparation-overlap",
|
|
76
|
+
message: `Codex preparation overlaps Codex published or recovery storage: ${preparedRoot}`,
|
|
77
|
+
}
|
|
78
|
+
: {
|
|
79
|
+
code: refusedCode,
|
|
80
|
+
message: `cannot validate Codex preparation storage separation at ${preparedRoot}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
54
83
|
export async function assertCodexPreparationSeparate(preparedRoot, paths) {
|
|
55
84
|
const recovery = await inspectRoot(paths.recoveryRoot, "recovery");
|
|
56
85
|
const marketplace = await inspectRoot(paths.marketplaceRoot, "marketplace");
|
|
@@ -6,10 +6,10 @@ import { ARTIFACT_RECEIPT } from "../../artifact-tree.js";
|
|
|
6
6
|
import { assessCodexCompatibility, readCodexAssessment, writeCodexAssessment, } from "./compatibility.js";
|
|
7
7
|
import { readManifest } from "./hooks.js";
|
|
8
8
|
import { readGeneratedCommitLenient, readStrictProvenanceField, writeProvenance, } from "../../provenance.js";
|
|
9
|
-
import { classifyPathNoFollow } from "../../safe-path.js";
|
|
9
|
+
import { classifyPathNoFollow, isFile } from "../../safe-path.js";
|
|
10
10
|
import { SafetyError } from "../../safety-error.js";
|
|
11
11
|
import { manifestVersionForRef } from "../../upstream-version.js";
|
|
12
|
-
import { assertCodexPreparationSeparate,
|
|
12
|
+
import { assertCodexPreparationSeparate, codexPathRefusal, codexPaths, } from "./paths.js";
|
|
13
13
|
import { readCodexRecovery } from "./recovery.js";
|
|
14
14
|
// Order is inherited from the original prepare command; the first miss wins.
|
|
15
15
|
const REQUIRED_UPSTREAM = [
|
|
@@ -25,12 +25,6 @@ const COPY_PATHS = [
|
|
|
25
25
|
"README.md",
|
|
26
26
|
"CODE_OF_CONDUCT.md",
|
|
27
27
|
];
|
|
28
|
-
const RESOLUTION_KINDS = [
|
|
29
|
-
"latest-release",
|
|
30
|
-
"tag",
|
|
31
|
-
"ref",
|
|
32
|
-
"raw-commit",
|
|
33
|
-
];
|
|
34
28
|
function unassessedCompatibility() {
|
|
35
29
|
return {
|
|
36
30
|
kind: "unknown",
|
|
@@ -55,14 +49,6 @@ async function pathExists(path) {
|
|
|
55
49
|
return false;
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
|
-
async function regularFileExists(path) {
|
|
59
|
-
try {
|
|
60
|
-
return (await stat(path)).isFile();
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
52
|
async function owned(message, fn) {
|
|
67
53
|
try {
|
|
68
54
|
return await fn();
|
|
@@ -102,18 +88,8 @@ export async function validateCodexPreparationBeforeFetch(ctx) {
|
|
|
102
88
|
await assertCodexPreparationSeparate(preparedRoot, paths);
|
|
103
89
|
}
|
|
104
90
|
catch (cause) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
if (failure.root === "recovery") {
|
|
108
|
-
return failureResult("prepare", "recovery-required", `cannot inspect Codex recovery state at ${paths.recoveryRoot}`, [], []);
|
|
109
|
-
}
|
|
110
|
-
return failureResult("prepare", "prepare-failed", failure.root === "marketplace"
|
|
111
|
-
? `cannot inspect Codex marketplace storage at ${paths.marketplaceRoot}`
|
|
112
|
-
: `cannot inspect Codex preparation root at ${preparedRoot}`, [], []);
|
|
113
|
-
}
|
|
114
|
-
return failureResult("prepare", failure?.kind === "overlap" ? "preparation-overlap" : "prepare-failed", failure?.kind === "overlap"
|
|
115
|
-
? `Codex preparation overlaps Codex published or recovery storage: ${preparedRoot}`
|
|
116
|
-
: `cannot validate Codex preparation storage separation at ${preparedRoot}`, [], []);
|
|
91
|
+
const refusal = codexPathRefusal(cause, paths, preparedRoot, "prepare-failed", "");
|
|
92
|
+
return failureResult("prepare", refusal.code, refusal.message, [], []);
|
|
117
93
|
}
|
|
118
94
|
try {
|
|
119
95
|
if ((await readCodexRecovery(paths)) !== null) {
|
|
@@ -124,7 +100,7 @@ export async function validateCodexPreparationBeforeFetch(ctx) {
|
|
|
124
100
|
return failureResult("prepare", "recovery-required", `cannot inspect Codex recovery state at ${paths.recoveryRoot}`, [], []);
|
|
125
101
|
}
|
|
126
102
|
const template = manifestTemplate(ctx);
|
|
127
|
-
if (!(await
|
|
103
|
+
if (!(await isFile(template))) {
|
|
128
104
|
return failureResult("prepare", "missing-template", `missing fallback manifest template: ${template}`, [], []);
|
|
129
105
|
}
|
|
130
106
|
return successResult("prepare", null, []);
|
|
@@ -139,13 +115,6 @@ export async function readUpstreamManifestVersion(path) {
|
|
|
139
115
|
}
|
|
140
116
|
return value;
|
|
141
117
|
}
|
|
142
|
-
function asResolutionKind(value) {
|
|
143
|
-
for (const kind of RESOLUTION_KINDS) {
|
|
144
|
-
if (kind === value)
|
|
145
|
-
return kind;
|
|
146
|
-
}
|
|
147
|
-
throw prepareError(`unknown upstream resolution kind: ${value}`);
|
|
148
|
-
}
|
|
149
118
|
export async function prepareCodexCandidate(input, ctx) {
|
|
150
119
|
let upstreamManifestVersion = "";
|
|
151
120
|
let managerVersion = "";
|
|
@@ -161,7 +130,7 @@ export async function prepareCodexCandidate(input, ctx) {
|
|
|
161
130
|
await copyPathIfPresent(join(input.upstreamRoot, name), join(input.candidateRoot, name));
|
|
162
131
|
}
|
|
163
132
|
const upstreamManifest = join(input.upstreamRoot, ".codex-plugin", "plugin.json");
|
|
164
|
-
if (await
|
|
133
|
+
if (await isFile(upstreamManifest)) {
|
|
165
134
|
upstreamManifestVersion =
|
|
166
135
|
await readUpstreamManifestVersion(upstreamManifest);
|
|
167
136
|
}
|
|
@@ -174,13 +143,13 @@ export async function prepareCodexCandidate(input, ctx) {
|
|
|
174
143
|
});
|
|
175
144
|
managerVersion = manifestVersionForRef({
|
|
176
145
|
requestedRef: input.selection.requestedRef,
|
|
177
|
-
resolutionKind:
|
|
146
|
+
resolutionKind: input.selection.resolutionKind,
|
|
178
147
|
resolvedRef: input.selection.resolvedRef,
|
|
179
148
|
commit: input.selection.desiredCommit,
|
|
180
149
|
});
|
|
181
150
|
}
|
|
182
151
|
catch (cause) {
|
|
183
|
-
// owned()
|
|
152
|
+
// owned(), readManifest(), and writeProvenance replace
|
|
184
153
|
// every subordinate failure with controlled preparation, hook, or
|
|
185
154
|
// provenance text. Re-emitting those owned diagnostics is therefore safe.
|
|
186
155
|
// codexBuild stays below this catch because it deliberately rethrows
|
|
@@ -203,13 +172,10 @@ export async function prepareCodexCandidate(input, ctx) {
|
|
|
203
172
|
if (!built.outcome.ok) {
|
|
204
173
|
return { status: built.status, outcome: built.outcome };
|
|
205
174
|
}
|
|
206
|
-
if (built.status !== 0) {
|
|
207
|
-
return failureResult("build", "invalid-status", "adapter reported failure without an error outcome", [], built.outcome.messages);
|
|
208
|
-
}
|
|
209
175
|
const compatibility = await assessCodexCompatibility(input.upstreamRoot, input.selection);
|
|
210
176
|
if (compatibility.kind === "supported") {
|
|
211
177
|
try {
|
|
212
|
-
await writeCodexAssessment(input.candidateRoot, input.selection, (await
|
|
178
|
+
await writeCodexAssessment(input.candidateRoot, input.selection, (await isFile(join(input.upstreamRoot, ".codex-plugin/plugin.json")))
|
|
213
179
|
? "upstream"
|
|
214
180
|
: "fallback");
|
|
215
181
|
}
|
|
@@ -90,7 +90,7 @@ function verificationOutput(kind, hint) {
|
|
|
90
90
|
return { stdout: [], stderr };
|
|
91
91
|
}
|
|
92
92
|
function safeHint(value) {
|
|
93
|
-
return
|
|
93
|
+
return hasTerminalControl(value) ? "" : value;
|
|
94
94
|
}
|
|
95
95
|
export function codexInstallReceipt(missingHint, mismatchHint) {
|
|
96
96
|
return {
|
|
@@ -3,17 +3,16 @@ 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 { codexInspectOwnership, codexInstallRefreshMode, codexRemove, codexReadNativeState, } from "./adapter.js";
|
|
7
7
|
import { readCodexAssessment } from "./compatibility.js";
|
|
8
8
|
import { readCodexMarketplace, stageCodexMarketplace, } from "./marketplace.js";
|
|
9
|
-
import { assertCodexPreparationSeparate,
|
|
9
|
+
import { assertCodexPreparationSeparate, codexPathRefusal, codexPaths, } from "./paths.js";
|
|
10
10
|
import { codexPreparationLocation } from "./prepare.js";
|
|
11
11
|
import { beginCodexRecovery, finishCodexRecovery, readCodexRecovery, verifyCodexRecovery, } from "./recovery.js";
|
|
12
12
|
import { hasFilesystemAccessFailure, pathsEqual } from "./state.js";
|
|
13
13
|
const DEFAULTS = {
|
|
14
14
|
readNative: codexReadNativeState,
|
|
15
15
|
inspectOwnership: codexInspectOwnership,
|
|
16
|
-
inspectControl: codexInspectControl,
|
|
17
16
|
beginPublication: beginDirectoryPublication,
|
|
18
17
|
};
|
|
19
18
|
const REMOVAL_DEFAULTS = {
|
|
@@ -30,18 +29,7 @@ function acceptedNative(result) {
|
|
|
30
29
|
if (result.status !== 0 || !result.outcome.ok) {
|
|
31
30
|
throw new Error("Codex native observation failed");
|
|
32
31
|
}
|
|
33
|
-
|
|
34
|
-
if (typeof value.marketplaceRoot !== "string" &&
|
|
35
|
-
value.marketplaceRoot !== null) {
|
|
36
|
-
throw new Error("Codex native observation is malformed");
|
|
37
|
-
}
|
|
38
|
-
if (typeof value.pluginPresent !== "boolean" ||
|
|
39
|
-
typeof value.pluginEnabled !== "boolean" ||
|
|
40
|
-
(typeof value.activeVersion !== "string" && value.activeVersion !== null) ||
|
|
41
|
-
(typeof value.activeRoot !== "string" && value.activeRoot !== null)) {
|
|
42
|
-
throw new Error("Codex native observation is malformed");
|
|
43
|
-
}
|
|
44
|
-
return value;
|
|
32
|
+
return result.outcome.result;
|
|
45
33
|
}
|
|
46
34
|
function requireOwnership(result) {
|
|
47
35
|
if (result.status !== 0 || !result.outcome.ok) {
|
|
@@ -51,14 +39,6 @@ function requireOwnership(result) {
|
|
|
51
39
|
throw new Error("Codex ownership blocks publication");
|
|
52
40
|
}
|
|
53
41
|
}
|
|
54
|
-
function requireControl(result) {
|
|
55
|
-
if (result.status !== 0 || !result.outcome.ok) {
|
|
56
|
-
throw new Error("Codex inspection failed");
|
|
57
|
-
}
|
|
58
|
-
if (result.outcome.result?.mutationEligibility?.kind !== "allowed") {
|
|
59
|
-
throw new Error("Codex update control blocks publication");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
42
|
function sameNative(left, right) {
|
|
63
43
|
return (left.marketplaceRoot === right.marketplaceRoot &&
|
|
64
44
|
left.pluginPresent === right.pluginPresent &&
|
|
@@ -247,9 +227,6 @@ async function requireEligibility(ctx, dependencies, messages) {
|
|
|
247
227
|
const ownership = await dependencies.inspectOwnership(ctx);
|
|
248
228
|
messages.push(...ownership.outcome.messages);
|
|
249
229
|
requireOwnership(ownership);
|
|
250
|
-
const control = await dependencies.inspectControl(ctx);
|
|
251
|
-
messages.push(...control.outcome.messages);
|
|
252
|
-
requireControl(control);
|
|
253
230
|
}
|
|
254
231
|
async function verifyPrevious(pending, previous) {
|
|
255
232
|
const current = await readCodexMarketplace(pending.paths.marketplaceRoot);
|
|
@@ -388,20 +365,8 @@ export async function installCodexMarketplace(artifact, ctx, activate, dependenc
|
|
|
388
365
|
await assertCodexPreparationSeparate(preparedRoot, paths);
|
|
389
366
|
}
|
|
390
367
|
catch (cause) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
if (failure.root === "recovery") {
|
|
394
|
-
return fail("recovery-required", `cannot inspect Codex recovery state at ${paths.recoveryRoot}; recovery is required before mutation`, messages);
|
|
395
|
-
}
|
|
396
|
-
return fail("activation-refused", failure.root === "marketplace"
|
|
397
|
-
? `cannot inspect Codex marketplace storage at ${paths.marketplaceRoot}`
|
|
398
|
-
: `cannot inspect Codex preparation root at ${preparedRoot}`, messages);
|
|
399
|
-
}
|
|
400
|
-
return fail(failure?.kind === "overlap"
|
|
401
|
-
? "preparation-overlap"
|
|
402
|
-
: "activation-refused", failure?.kind === "overlap"
|
|
403
|
-
? `Codex preparation overlaps Codex published or recovery storage: ${preparedRoot}`
|
|
404
|
-
: `cannot validate Codex preparation storage separation at ${preparedRoot}`, messages);
|
|
368
|
+
const refusal = codexPathRefusal(cause, paths, preparedRoot, "activation-refused", "; recovery is required before mutation");
|
|
369
|
+
return fail(refusal.code, refusal.message, messages);
|
|
405
370
|
}
|
|
406
371
|
let pending;
|
|
407
372
|
let publication;
|
|
@@ -83,11 +83,6 @@ async function comparablePath(path) {
|
|
|
83
83
|
export async function pathsEqual(a, b) {
|
|
84
84
|
return (await comparablePath(a)) === (await comparablePath(b));
|
|
85
85
|
}
|
|
86
|
-
function preserveFailure(result) {
|
|
87
|
-
if (result.outcome.ok)
|
|
88
|
-
throw new Error("expected adapter failure");
|
|
89
|
-
return failureResult(result.outcome.operation, result.outcome.error.code, result.outcome.error.message, result.outcome.error.hints, result.outcome.messages);
|
|
90
|
-
}
|
|
91
86
|
function mismatch(reason, messages = [], observedIdentity = "") {
|
|
92
87
|
return successResult("inspect-codex-installed", { kind: "mismatch", observedIdentity }, [
|
|
93
88
|
...messages,
|
|
@@ -116,7 +111,7 @@ export async function inspectCodexInstallation(selection, ctx, readNative = code
|
|
|
116
111
|
const operation = "inspect-codex-installed";
|
|
117
112
|
const native = await readNative(ctx);
|
|
118
113
|
if (!native.outcome.ok)
|
|
119
|
-
return
|
|
114
|
+
return { status: native.status, outcome: native.outcome };
|
|
120
115
|
const messages = native.outcome.messages;
|
|
121
116
|
const paths = codexPaths(ctx.env ?? {}, process.cwd());
|
|
122
117
|
let marketplace;
|
|
@@ -3,6 +3,7 @@ import { lstat, open, readdir } from "node:fs/promises";
|
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
4
|
import { parse, TomlDate } from "smol-toml";
|
|
5
5
|
import { SafetyError } from "../../safety-error.js";
|
|
6
|
+
import { isErrno } from "../../safe-path.js";
|
|
6
7
|
import { codexHome } from "./paths.js";
|
|
7
8
|
const CONFIG_LIMIT = 1024 * 1024;
|
|
8
9
|
const CONFIG_DEPTH_LIMIT = 64;
|
|
@@ -15,12 +16,6 @@ const MARKETPLACE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
|
15
16
|
function storedError(message, cause) {
|
|
16
17
|
return new SafetyError("codex-stored-state", message, { cause });
|
|
17
18
|
}
|
|
18
|
-
function isErrno(cause, code) {
|
|
19
|
-
return (cause !== null &&
|
|
20
|
-
typeof cause === "object" &&
|
|
21
|
-
"code" in cause &&
|
|
22
|
-
cause.code === code);
|
|
23
|
-
}
|
|
24
19
|
function table(value, name) {
|
|
25
20
|
if (value === undefined)
|
|
26
21
|
return {};
|
|
@@ -286,8 +286,7 @@ async function writeObservedOpenCodeConfig(observation, outputBytes, expected) {
|
|
|
286
286
|
export async function removeObservedOpenCodeEntry(observation, key, index) {
|
|
287
287
|
const path = observation.document.path;
|
|
288
288
|
try {
|
|
289
|
-
const
|
|
290
|
-
const original = parseOpenCodeConfig(originalText, path);
|
|
289
|
+
const original = observation.document;
|
|
291
290
|
const output = removeOpenCodeEntry(original, key, index);
|
|
292
291
|
const outputBytes = Buffer.from(output, "utf8");
|
|
293
292
|
const expected = original.entries.filter((entry) => entry.key !== key || entry.index !== index);
|
|
@@ -300,8 +299,7 @@ export async function removeObservedOpenCodeEntry(observation, key, index) {
|
|
|
300
299
|
export async function addObservedOpenCodeEntry(observation, key, spec) {
|
|
301
300
|
const path = observation.document.path;
|
|
302
301
|
try {
|
|
303
|
-
const
|
|
304
|
-
const original = parseOpenCodeConfig(originalText, path);
|
|
302
|
+
const original = observation.document;
|
|
305
303
|
const output = addOpenCodeEntry(original, key, spec);
|
|
306
304
|
const outputBytes = Buffer.from(output, "utf8");
|
|
307
305
|
const expected = parseOpenCodeConfig(output, path).entries;
|
|
@@ -163,11 +163,7 @@ async function inspectPluginEntry(observation, entry, state, allowManaged = true
|
|
|
163
163
|
const canonical = await canonicalizeProspectivePath(local);
|
|
164
164
|
if (canonical === state.installedRoot) {
|
|
165
165
|
if (allowManaged)
|
|
166
|
-
state.managedEntries.push({
|
|
167
|
-
observation,
|
|
168
|
-
entry,
|
|
169
|
-
canonicalRoot: canonical,
|
|
170
|
-
});
|
|
166
|
+
state.managedEntries.push({ observation, entry });
|
|
171
167
|
else
|
|
172
168
|
addBlocked(state, `${OPEN_CODE_UNOWNED_MANAGER_INPUT}: ${observation.document.path}`);
|
|
173
169
|
return;
|