superpowers-manager 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/install.js +16 -6
- package/dist/commands/prepare.js +1 -1
- package/dist/commands/uninstall.js +1 -1
- package/dist/harnesses/codex/adapter.js +147 -18
- package/dist/harnesses/codex/generated-plugin.js +1 -1
- package/dist/harnesses/codex/harness.js +35 -17
- package/dist/harnesses/codex/hooks.js +1 -1
- package/dist/harnesses/codex/json.js +6 -2
- package/dist/harnesses/codex/lifecycle.js +1 -1
- package/dist/harnesses/codex/marketplace.js +148 -0
- package/dist/harnesses/codex/paths.js +97 -0
- package/dist/harnesses/codex/prepare.js +34 -2
- package/dist/harnesses/codex/presentation.js +4 -3
- package/dist/harnesses/codex/publication.js +538 -0
- package/dist/harnesses/codex/recovery.js +338 -0
- package/dist/harnesses/codex/state.js +131 -0
- package/dist/harnesses/codex/stored-state.js +249 -0
- package/dist/provenance.js +3 -3
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +10 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, open, readFile, readdir, rmdir, unlink, } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { ARTIFACT_DIGEST_RE } from "../../artifact-tree.js";
|
|
5
|
+
import { assertDesignatedParentDirectory, assertNoFollowType, canonicalizeProspectivePath, } from "../../safe-path.js";
|
|
6
|
+
import { SafetyError } from "../../safety-error.js";
|
|
7
|
+
import { parseStrictJson, } from "../../strict-json.js";
|
|
8
|
+
const JOURNAL = "transaction.json";
|
|
9
|
+
const TOKEN_RE = /^[a-f0-9]{32}$/u;
|
|
10
|
+
const PROFILE = {
|
|
11
|
+
duplicateKeys: "reject",
|
|
12
|
+
nonStandardConstants: "reject",
|
|
13
|
+
integerNumbersOnly: true,
|
|
14
|
+
maxDepth: 8,
|
|
15
|
+
maxBytes: 64 * 1024,
|
|
16
|
+
};
|
|
17
|
+
const RECORD_KEYS = [
|
|
18
|
+
"schema",
|
|
19
|
+
"operation",
|
|
20
|
+
"token",
|
|
21
|
+
"marketplaceRoot",
|
|
22
|
+
"priorNative",
|
|
23
|
+
"oldDigest",
|
|
24
|
+
"oldIdentity",
|
|
25
|
+
].sort();
|
|
26
|
+
const NATIVE_KEYS = [
|
|
27
|
+
"marketplaceRoot",
|
|
28
|
+
"pluginPresent",
|
|
29
|
+
"pluginEnabled",
|
|
30
|
+
"activeVersion",
|
|
31
|
+
"activeRoot",
|
|
32
|
+
].sort();
|
|
33
|
+
const IDENTITY_KEYS = ["dev", "ino"];
|
|
34
|
+
function recoveryError(message, cause) {
|
|
35
|
+
return new SafetyError("codex-recovery", message, { cause });
|
|
36
|
+
}
|
|
37
|
+
function recordPath(paths) {
|
|
38
|
+
return join(paths.recoveryRoot, JOURNAL);
|
|
39
|
+
}
|
|
40
|
+
function stagePath(paths, token) {
|
|
41
|
+
return join(paths.managerRoot, `.stage-${token}`);
|
|
42
|
+
}
|
|
43
|
+
function backupPath(paths, token) {
|
|
44
|
+
return join(paths.managerRoot, `.marketplace.bak.${token}`);
|
|
45
|
+
}
|
|
46
|
+
function object(value) {
|
|
47
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
48
|
+
? value
|
|
49
|
+
: null;
|
|
50
|
+
}
|
|
51
|
+
function exactKeys(value, expected) {
|
|
52
|
+
const actual = Object.keys(value).sort();
|
|
53
|
+
if (actual.length !== expected.length ||
|
|
54
|
+
actual.some((key, index) => key !== expected[index])) {
|
|
55
|
+
throw new Error("unexpected recovery fields");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function nullableString(value) {
|
|
59
|
+
if (value === null || typeof value === "string")
|
|
60
|
+
return value;
|
|
61
|
+
throw new Error("invalid nullable string");
|
|
62
|
+
}
|
|
63
|
+
function identityValue(value) {
|
|
64
|
+
if (value === null)
|
|
65
|
+
return null;
|
|
66
|
+
const candidate = object(value);
|
|
67
|
+
if (candidate === null)
|
|
68
|
+
throw new Error("invalid identity");
|
|
69
|
+
exactKeys(candidate, IDENTITY_KEYS);
|
|
70
|
+
const dev = candidate.dev;
|
|
71
|
+
const ino = candidate.ino;
|
|
72
|
+
if (typeof dev !== "number" ||
|
|
73
|
+
typeof ino !== "number" ||
|
|
74
|
+
!Number.isSafeInteger(dev) ||
|
|
75
|
+
!Number.isSafeInteger(ino) ||
|
|
76
|
+
dev < 0 ||
|
|
77
|
+
ino < 0) {
|
|
78
|
+
throw new Error("invalid identity");
|
|
79
|
+
}
|
|
80
|
+
return { dev, ino };
|
|
81
|
+
}
|
|
82
|
+
function nativeValue(value) {
|
|
83
|
+
const candidate = object(value);
|
|
84
|
+
if (candidate === null)
|
|
85
|
+
throw new Error("invalid native state");
|
|
86
|
+
exactKeys(candidate, NATIVE_KEYS);
|
|
87
|
+
const marketplaceRoot = nullableString(candidate.marketplaceRoot);
|
|
88
|
+
const activeVersion = nullableString(candidate.activeVersion);
|
|
89
|
+
const activeRoot = nullableString(candidate.activeRoot);
|
|
90
|
+
if (typeof candidate.pluginPresent !== "boolean" ||
|
|
91
|
+
typeof candidate.pluginEnabled !== "boolean" ||
|
|
92
|
+
(candidate.pluginPresent === false && activeVersion !== null) ||
|
|
93
|
+
(activeVersion === null) !== (activeRoot === null)) {
|
|
94
|
+
throw new Error("invalid native state");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
marketplaceRoot,
|
|
98
|
+
pluginPresent: candidate.pluginPresent,
|
|
99
|
+
pluginEnabled: candidate.pluginEnabled,
|
|
100
|
+
activeVersion,
|
|
101
|
+
activeRoot,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function digestValue(value) {
|
|
105
|
+
if (value === null)
|
|
106
|
+
return null;
|
|
107
|
+
if (typeof value !== "string" || !ARTIFACT_DIGEST_RE.test(value)) {
|
|
108
|
+
throw new Error("invalid recovery digest");
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
function validateEvidenceShape(record) {
|
|
113
|
+
if ((record.oldDigest === null) !== (record.oldIdentity === null)) {
|
|
114
|
+
throw new Error("incomplete old recovery evidence");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function decodeRecord(bytes, paths) {
|
|
118
|
+
const parsed = object(parseStrictJson(bytes, PROFILE));
|
|
119
|
+
if (parsed === null)
|
|
120
|
+
throw new Error("recovery journal is not an object");
|
|
121
|
+
exactKeys(parsed, RECORD_KEYS);
|
|
122
|
+
if (parsed.schema !== 2)
|
|
123
|
+
throw new Error("invalid recovery schema");
|
|
124
|
+
if (parsed.operation !== "install" && parsed.operation !== "uninstall") {
|
|
125
|
+
throw new Error("invalid recovery operation");
|
|
126
|
+
}
|
|
127
|
+
if (typeof parsed.token !== "string" || !TOKEN_RE.test(parsed.token)) {
|
|
128
|
+
throw new Error("invalid recovery token");
|
|
129
|
+
}
|
|
130
|
+
if (typeof parsed.marketplaceRoot !== "string" ||
|
|
131
|
+
!isAbsolute(parsed.marketplaceRoot) ||
|
|
132
|
+
resolve(parsed.marketplaceRoot) !== parsed.marketplaceRoot) {
|
|
133
|
+
throw new Error("invalid recovery marketplace root");
|
|
134
|
+
}
|
|
135
|
+
if (parsed.marketplaceRoot !== paths.marketplaceRoot) {
|
|
136
|
+
throw new Error("foreign recovery marketplace root");
|
|
137
|
+
}
|
|
138
|
+
const record = {
|
|
139
|
+
schema: 2,
|
|
140
|
+
operation: parsed.operation,
|
|
141
|
+
token: parsed.token,
|
|
142
|
+
marketplaceRoot: parsed.marketplaceRoot,
|
|
143
|
+
priorNative: nativeValue(parsed.priorNative),
|
|
144
|
+
oldDigest: digestValue(parsed.oldDigest),
|
|
145
|
+
oldIdentity: identityValue(parsed.oldIdentity),
|
|
146
|
+
};
|
|
147
|
+
validateEvidenceShape(record);
|
|
148
|
+
return record;
|
|
149
|
+
}
|
|
150
|
+
function recordBytes(record) {
|
|
151
|
+
return Buffer.from(`${JSON.stringify(record)}\n`);
|
|
152
|
+
}
|
|
153
|
+
function sameIdentity(left, right) {
|
|
154
|
+
return (left !== null &&
|
|
155
|
+
right !== null &&
|
|
156
|
+
left.dev === right.dev &&
|
|
157
|
+
left.ino === right.ino);
|
|
158
|
+
}
|
|
159
|
+
async function identity(path) {
|
|
160
|
+
await assertNoFollowType(path, ["directory"]);
|
|
161
|
+
const details = await lstat(path);
|
|
162
|
+
return { dev: details.dev, ino: details.ino };
|
|
163
|
+
}
|
|
164
|
+
async function optionalIdentity(path) {
|
|
165
|
+
const kind = await assertNoFollowType(path, ["directory", "missing"]);
|
|
166
|
+
return kind === "missing" ? null : await identity(path);
|
|
167
|
+
}
|
|
168
|
+
async function syncDirectory(path) {
|
|
169
|
+
const handle = await open(path, "r");
|
|
170
|
+
try {
|
|
171
|
+
await handle.sync();
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await handle.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function readOwnedRecord(paths) {
|
|
178
|
+
await assertNoFollowType(paths.recoveryRoot, ["directory"]);
|
|
179
|
+
const entries = await readdir(paths.recoveryRoot);
|
|
180
|
+
if (entries.length !== 1 || entries[0] !== JOURNAL) {
|
|
181
|
+
throw new Error("unexpected Codex recovery material");
|
|
182
|
+
}
|
|
183
|
+
const path = recordPath(paths);
|
|
184
|
+
await assertNoFollowType(path, ["regular-file"]);
|
|
185
|
+
return await decodeRecord(await readFile(path), paths);
|
|
186
|
+
}
|
|
187
|
+
export async function readCodexRecovery(paths) {
|
|
188
|
+
try {
|
|
189
|
+
const kind = await assertNoFollowType(paths.recoveryRoot, [
|
|
190
|
+
"directory",
|
|
191
|
+
"missing",
|
|
192
|
+
]);
|
|
193
|
+
if (kind === "missing")
|
|
194
|
+
return null;
|
|
195
|
+
return await readOwnedRecord(paths);
|
|
196
|
+
}
|
|
197
|
+
catch (cause) {
|
|
198
|
+
throw recoveryError(`cannot inspect Codex recovery state: ${paths.recoveryRoot}`, cause);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function requireInitialSnapshot(paths, input) {
|
|
202
|
+
if (input.marketplaceRoot !== paths.marketplaceRoot) {
|
|
203
|
+
throw new Error("unexpected marketplace root");
|
|
204
|
+
}
|
|
205
|
+
await assertNoFollowType(paths.codexHome, ["directory", "missing"]);
|
|
206
|
+
await assertNoFollowType(paths.managerRoot, ["directory", "missing"]);
|
|
207
|
+
await canonicalizeProspectivePath(paths.marketplaceRoot);
|
|
208
|
+
const current = await optionalIdentity(paths.marketplaceRoot);
|
|
209
|
+
if ((current === null) !== (input.oldIdentity === null) ||
|
|
210
|
+
(current !== null && !sameIdentity(current, input.oldIdentity)) ||
|
|
211
|
+
(input.oldDigest === null) !== (input.oldIdentity === null)) {
|
|
212
|
+
throw new Error("previous marketplace changed");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export async function beginCodexRecovery(paths, input) {
|
|
216
|
+
try {
|
|
217
|
+
const homeKind = await assertNoFollowType(paths.codexHome, [
|
|
218
|
+
"directory",
|
|
219
|
+
"missing",
|
|
220
|
+
]);
|
|
221
|
+
if (homeKind === "missing") {
|
|
222
|
+
await assertDesignatedParentDirectory(paths.codexHome);
|
|
223
|
+
await mkdir(paths.codexHome, { mode: 0o700 });
|
|
224
|
+
}
|
|
225
|
+
await requireInitialSnapshot(paths, input);
|
|
226
|
+
const managerKind = await assertNoFollowType(paths.managerRoot, [
|
|
227
|
+
"directory",
|
|
228
|
+
"missing",
|
|
229
|
+
]);
|
|
230
|
+
if (managerKind === "missing")
|
|
231
|
+
await mkdir(paths.managerRoot, { mode: 0o700 });
|
|
232
|
+
if ((await assertNoFollowType(paths.recoveryRoot, [
|
|
233
|
+
"directory",
|
|
234
|
+
"missing",
|
|
235
|
+
])) !== "missing") {
|
|
236
|
+
throw new Error("unresolved Codex recovery state");
|
|
237
|
+
}
|
|
238
|
+
await mkdir(paths.recoveryRoot, { mode: 0o700 });
|
|
239
|
+
const recoveryIdentity = await identity(paths.recoveryRoot);
|
|
240
|
+
const token = randomBytes(16).toString("hex");
|
|
241
|
+
const record = {
|
|
242
|
+
schema: 2,
|
|
243
|
+
operation: input.operation,
|
|
244
|
+
token,
|
|
245
|
+
marketplaceRoot: input.marketplaceRoot,
|
|
246
|
+
priorNative: input.priorNative,
|
|
247
|
+
oldDigest: input.oldDigest,
|
|
248
|
+
oldIdentity: input.oldIdentity,
|
|
249
|
+
};
|
|
250
|
+
await decodeRecord(recordBytes(record), paths);
|
|
251
|
+
const path = recordPath(paths);
|
|
252
|
+
const handle = await open(path, "wx", 0o600);
|
|
253
|
+
try {
|
|
254
|
+
await handle.writeFile(recordBytes(record));
|
|
255
|
+
await handle.sync();
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
await handle.close();
|
|
259
|
+
}
|
|
260
|
+
await syncDirectory(paths.recoveryRoot);
|
|
261
|
+
const journal = await lstat(path);
|
|
262
|
+
return {
|
|
263
|
+
paths,
|
|
264
|
+
stage: stagePath(paths, token),
|
|
265
|
+
backup: backupPath(paths, token),
|
|
266
|
+
recoveryIdentity,
|
|
267
|
+
journalIdentity: { dev: journal.dev, ino: journal.ino },
|
|
268
|
+
record,
|
|
269
|
+
newDigest: null,
|
|
270
|
+
stageIdentity: null,
|
|
271
|
+
publishedIdentity: null,
|
|
272
|
+
settled: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
catch (cause) {
|
|
276
|
+
const suffix = cause instanceof Error &&
|
|
277
|
+
cause.message === "unresolved Codex recovery state"
|
|
278
|
+
? ": unresolved Codex recovery state"
|
|
279
|
+
: "";
|
|
280
|
+
throw recoveryError(`cannot begin Codex recovery journal${suffix}`, cause);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
export async function verifyCodexRecovery(pending) {
|
|
284
|
+
try {
|
|
285
|
+
await requirePending(pending);
|
|
286
|
+
}
|
|
287
|
+
catch (cause) {
|
|
288
|
+
const message = cause instanceof Error &&
|
|
289
|
+
(cause.message === "Codex recovery journal changed" ||
|
|
290
|
+
cause.message === "Codex recovery journal is already settled")
|
|
291
|
+
? cause.message
|
|
292
|
+
: "cannot verify Codex recovery journal";
|
|
293
|
+
throw recoveryError(message, cause);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function requirePending(pending) {
|
|
297
|
+
if (pending.settled) {
|
|
298
|
+
throw new Error("Codex recovery journal is already settled");
|
|
299
|
+
}
|
|
300
|
+
if (pending.stage !== stagePath(pending.paths, pending.record.token) ||
|
|
301
|
+
pending.backup !== backupPath(pending.paths, pending.record.token)) {
|
|
302
|
+
throw new Error("Codex recovery paths changed");
|
|
303
|
+
}
|
|
304
|
+
if (!sameIdentity(await identity(pending.paths.recoveryRoot), pending.recoveryIdentity)) {
|
|
305
|
+
throw new Error("Codex recovery directory changed");
|
|
306
|
+
}
|
|
307
|
+
const path = recordPath(pending.paths);
|
|
308
|
+
await assertNoFollowType(path, ["regular-file"]);
|
|
309
|
+
const journal = await lstat(path);
|
|
310
|
+
if (!sameIdentity({ dev: journal.dev, ino: journal.ino }, pending.journalIdentity) ||
|
|
311
|
+
!(await readFile(path)).equals(recordBytes(pending.record))) {
|
|
312
|
+
throw new Error("Codex recovery journal changed");
|
|
313
|
+
}
|
|
314
|
+
await decodeRecord(recordBytes(pending.record), pending.paths);
|
|
315
|
+
}
|
|
316
|
+
export async function finishCodexRecovery(pending) {
|
|
317
|
+
try {
|
|
318
|
+
await requirePending(pending);
|
|
319
|
+
const entries = await readdir(pending.paths.recoveryRoot);
|
|
320
|
+
if (entries.length !== 1 || entries[0] !== JOURNAL) {
|
|
321
|
+
throw new Error("unexpected Codex recovery material");
|
|
322
|
+
}
|
|
323
|
+
pending.settled = true;
|
|
324
|
+
await unlink(recordPath(pending.paths));
|
|
325
|
+
if (!sameIdentity(await identity(pending.paths.recoveryRoot), pending.recoveryIdentity)) {
|
|
326
|
+
throw new Error("Codex recovery directory changed");
|
|
327
|
+
}
|
|
328
|
+
await rmdir(pending.paths.recoveryRoot);
|
|
329
|
+
}
|
|
330
|
+
catch (cause) {
|
|
331
|
+
const message = cause instanceof Error &&
|
|
332
|
+
(cause.message === "unexpected Codex recovery material" ||
|
|
333
|
+
cause.message === "Codex recovery journal is already settled")
|
|
334
|
+
? cause.message
|
|
335
|
+
: "cannot finish Codex recovery journal";
|
|
336
|
+
throw recoveryError(message, cause);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { readFile, realpath } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { failureResult, successResult, } from "../../adapter-result.js";
|
|
4
|
+
import { digestArtifactTree, readArtifactObject } from "../../artifact-tree.js";
|
|
3
5
|
import { COMMIT_INPUT_RE } from "../../domain/refs.js";
|
|
4
6
|
import { SafetyError } from "../../safety-error.js";
|
|
5
7
|
import { parseStrictJson, } from "../../strict-json.js";
|
|
8
|
+
import { codexReadNativeState } from "./adapter.js";
|
|
9
|
+
import { readCodexAssessment } from "./compatibility.js";
|
|
10
|
+
import { readCodexMarketplace } from "./marketplace.js";
|
|
11
|
+
import { codexPaths } from "./paths.js";
|
|
12
|
+
import { classifyPathNoFollow } from "../../safe-path.js";
|
|
6
13
|
const INSTALLED_PROFILE = {
|
|
7
14
|
duplicateKeys: "last-wins",
|
|
8
15
|
nonStandardConstants: "reject",
|
|
@@ -76,3 +83,127 @@ async function comparablePath(path) {
|
|
|
76
83
|
export async function pathsEqual(a, b) {
|
|
77
84
|
return (await comparablePath(a)) === (await comparablePath(b));
|
|
78
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
|
+
function mismatch(reason, messages = [], observedIdentity = "") {
|
|
92
|
+
return successResult("inspect-codex-installed", { kind: "mismatch", observedIdentity }, [
|
|
93
|
+
...messages,
|
|
94
|
+
{ channel: "stderr", text: `Codex installation state: ${reason}` },
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
async function matchesSelection(root, selection) {
|
|
98
|
+
const provenance = await readArtifactObject(root, join(root, ".superpowers-upstream.json"));
|
|
99
|
+
return provenance.source === selection.effectiveSource;
|
|
100
|
+
}
|
|
101
|
+
export function hasFilesystemAccessFailure(cause) {
|
|
102
|
+
let current = cause;
|
|
103
|
+
for (let depth = 0; depth < 8 && current !== null && typeof current === "object"; depth += 1) {
|
|
104
|
+
const code = current.code;
|
|
105
|
+
if (typeof code === "string" && code !== "ENOENT" && code !== "ENOTDIR") {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
current = current.cause;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function activeInspectionFailure(operation, root, messages) {
|
|
113
|
+
return failureResult(operation, "inspect-failed", `cannot inspect active Codex plugin payload at ${root}`, [], messages);
|
|
114
|
+
}
|
|
115
|
+
export async function inspectCodexInstallation(selection, ctx, readNative = codexReadNativeState) {
|
|
116
|
+
const operation = "inspect-codex-installed";
|
|
117
|
+
const native = await readNative(ctx);
|
|
118
|
+
if (!native.outcome.ok)
|
|
119
|
+
return preserveFailure(native);
|
|
120
|
+
const messages = native.outcome.messages;
|
|
121
|
+
const paths = codexPaths(ctx.env ?? {}, process.cwd());
|
|
122
|
+
let marketplace;
|
|
123
|
+
try {
|
|
124
|
+
marketplace = await readCodexMarketplace(paths.marketplaceRoot);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return failureResult(operation, "inspect-failed", `cannot inspect owned Codex marketplace at ${paths.marketplaceRoot}`, [], messages);
|
|
128
|
+
}
|
|
129
|
+
const observed = native.outcome.result;
|
|
130
|
+
const observedIdentity = observed.pluginPresent && observed.activeRoot !== null
|
|
131
|
+
? await installedCommitFromRoot(observed.activeRoot)
|
|
132
|
+
: "";
|
|
133
|
+
if (!observed.pluginPresent && observed.marketplaceRoot === null) {
|
|
134
|
+
return successResult(operation, { kind: "absent", observedIdentity: "" }, messages);
|
|
135
|
+
}
|
|
136
|
+
if (observed.marketplaceRoot === null) {
|
|
137
|
+
return mismatch("Codex marketplace registration is missing", messages, observedIdentity);
|
|
138
|
+
}
|
|
139
|
+
if (!(await pathsEqual(observed.marketplaceRoot, paths.marketplaceRoot))) {
|
|
140
|
+
return mismatch("legacy Codex marketplace source", messages, observedIdentity);
|
|
141
|
+
}
|
|
142
|
+
if (marketplace === null)
|
|
143
|
+
return mismatch("durable Codex marketplace is missing", messages, observedIdentity);
|
|
144
|
+
try {
|
|
145
|
+
if (marketplace.artifact.commit !== selection.desiredCommit ||
|
|
146
|
+
marketplace.artifact.compatibility.kind !== "supported" ||
|
|
147
|
+
!(await matchesSelection(marketplace.artifact.root, selection))) {
|
|
148
|
+
return mismatch("durable Codex marketplace differs from selection", messages, observedIdentity);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return failureResult(operation, "inspect-failed", `cannot inspect owned Codex marketplace at ${paths.marketplaceRoot}`, [], messages);
|
|
153
|
+
}
|
|
154
|
+
if (!observed.pluginPresent ||
|
|
155
|
+
!observed.pluginEnabled ||
|
|
156
|
+
observed.activeRoot === null) {
|
|
157
|
+
return mismatch("Codex manager plugin needs repair", messages, observedIdentity);
|
|
158
|
+
}
|
|
159
|
+
let activeKind;
|
|
160
|
+
try {
|
|
161
|
+
activeKind = await classifyPathNoFollow(observed.activeRoot);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return activeInspectionFailure(operation, observed.activeRoot, messages);
|
|
165
|
+
}
|
|
166
|
+
if (activeKind === "missing") {
|
|
167
|
+
return mismatch("active Codex plugin payload is missing", messages, observedIdentity);
|
|
168
|
+
}
|
|
169
|
+
if (activeKind !== "directory") {
|
|
170
|
+
return activeInspectionFailure(operation, observed.activeRoot, messages);
|
|
171
|
+
}
|
|
172
|
+
let active;
|
|
173
|
+
let activeDigest;
|
|
174
|
+
try {
|
|
175
|
+
active = await readCodexAssessment(observed.activeRoot);
|
|
176
|
+
activeDigest = await digestArtifactTree(active.root);
|
|
177
|
+
}
|
|
178
|
+
catch (cause) {
|
|
179
|
+
if (hasFilesystemAccessFailure(cause)) {
|
|
180
|
+
return activeInspectionFailure(operation, observed.activeRoot, messages);
|
|
181
|
+
}
|
|
182
|
+
return mismatch("active Codex plugin payload is invalid", messages, observedIdentity);
|
|
183
|
+
}
|
|
184
|
+
let durableDigest;
|
|
185
|
+
try {
|
|
186
|
+
durableDigest = await digestArtifactTree(marketplace.artifact.root);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return failureResult(operation, "inspect-failed", `cannot inspect owned Codex marketplace at ${paths.marketplaceRoot}`, [], messages);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
if (active.commit !== selection.desiredCommit ||
|
|
193
|
+
active.compatibility.kind !== "supported" ||
|
|
194
|
+
!(await matchesSelection(active.root, selection)) ||
|
|
195
|
+
activeDigest !== durableDigest) {
|
|
196
|
+
return mismatch("active Codex plugin payload differs from durable marketplace", messages, active.commit);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (cause) {
|
|
200
|
+
if (hasFilesystemAccessFailure(cause)) {
|
|
201
|
+
return activeInspectionFailure(operation, observed.activeRoot, messages);
|
|
202
|
+
}
|
|
203
|
+
return mismatch("active Codex plugin payload is invalid", messages, observedIdentity);
|
|
204
|
+
}
|
|
205
|
+
return successResult(operation, {
|
|
206
|
+
kind: "current",
|
|
207
|
+
observedIdentity: active.commit,
|
|
208
|
+
}, messages);
|
|
209
|
+
}
|