superpowers-manager 0.3.0 → 0.4.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/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
+ #!/usr/bin/env node
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
- import { runAdapter } from "./adapter.js";
4
4
  import { oneLine } from "./cli-arguments.js";
5
+ import { codexHarness } from "./codex-harness.js";
5
6
  import { runInstall } from "./commands/install.js";
6
7
  import { runPin } from "./commands/pin.js";
7
8
  import { runPrepare } from "./commands/prepare.js";
@@ -46,15 +47,15 @@ const IN_PROCESS_HANDLERS = {
46
47
  // the in-process path has no validator process. It remains CONDITIONAL for
47
48
  // `prepare` through commandRequirements(env) below, unchanged from slice 3.4.
48
49
  // No command requires a POSIX shell any more.
49
- const COMMAND_REQUIREMENTS = {
50
+ const SHARED_COMMAND_REQUIREMENTS = {
50
51
  pin: ["git"],
51
52
  "track-latest": [],
52
53
  unpin: [],
53
54
  prepare: ["git"],
54
- probe: ["git", "codex"],
55
- install: ["git", "codex"],
56
- update: ["git", "codex"],
57
- uninstall: ["codex"],
55
+ probe: ["git"],
56
+ install: ["git"],
57
+ update: ["git"],
58
+ uninstall: [],
58
59
  };
59
60
  // Walk upward from the bin's physical location to the directory containing
60
61
  // package.json. realpathSync first: npm/npx expose the bin through a symlink
@@ -160,40 +161,61 @@ function findTool(name, env, platform) {
160
161
  // rather than inside preflight — an accessor that under-reports what
161
162
  // preflight enforces is the blind spot slice 2 closed when it made
162
163
  // CLI-PREFLIGHT-01 derive its map from production.
163
- function commandRequirements(env) {
164
- if (!env.SUPERPOWERS_VALIDATOR)
165
- return COMMAND_REQUIREMENTS;
164
+ function sharedRequirement(name) {
166
165
  return {
167
- ...COMMAND_REQUIREMENTS,
168
- prepare: [...COMMAND_REQUIREMENTS.prepare, "python3"],
166
+ name,
167
+ executable: name,
168
+ lookup: "path",
169
+ missingMessage: `required command not found: ${name} — install ${name} and re-run`,
170
+ };
171
+ }
172
+ function commandRequirementsFor(env, adapter) {
173
+ const forCommand = (command) => {
174
+ const shared = SHARED_COMMAND_REQUIREMENTS[command].map(sharedRequirement);
175
+ if (command === "prepare" && env.SUPERPOWERS_VALIDATOR) {
176
+ shared.push(sharedRequirement("python3"));
177
+ }
178
+ return [...shared, ...adapter.requirements(command, env)];
169
179
  };
180
+ return {
181
+ pin: forCommand("pin"),
182
+ "track-latest": forCommand("track-latest"),
183
+ unpin: forCommand("unpin"),
184
+ prepare: forCommand("prepare"),
185
+ probe: forCommand("probe"),
186
+ install: forCommand("install"),
187
+ update: forCommand("update"),
188
+ uninstall: forCommand("uninstall"),
189
+ };
190
+ }
191
+ function commandRequirements(env) {
192
+ return Object.fromEntries(SUBCOMMANDS.map((command) => [
193
+ command,
194
+ commandRequirementsFor(env, codexHarness)[command].map((requirement) => requirement.name),
195
+ ]));
170
196
  }
171
197
  // Preflight; never touches Codex state. It is the union of two exported
172
198
  // accessors: configurationErrors (validator configuration) and
173
199
  // commandRequirements (tool availability), both specific to the selected
174
200
  // command. No command requires a POSIX shell: slice 4b flipped the last
175
201
  // spawned command in-process, so there is no shell to discover.
176
- function preflight(cmd, env, platform) {
202
+ function preflightFor(cmd, env, platform, adapter) {
177
203
  const errors = [...configurationErrors(cmd, env)];
178
- for (const tool of commandRequirements(env)[cmd]) {
179
- if (tool === "codex") {
180
- const codexBin = env.SUPERPOWERS_CODEX || "codex";
181
- // An explicit override may be a path rather than a PATH-resolvable name.
182
- const found = codexBin.includes(path.sep)
183
- ? fs.existsSync(codexBin)
184
- : Boolean(findTool(codexBin, env, platform));
185
- if (!found) {
186
- errors.push(`required command not found: ${codexBin} — install the Codex CLI or set SUPERPOWERS_CODEX`);
187
- }
188
- }
189
- else if (!findTool(tool, env, platform)) {
190
- errors.push(`required command not found: ${tool} — install ${tool} and re-run`);
191
- }
204
+ for (const requirement of commandRequirementsFor(env, adapter)[cmd]) {
205
+ const found = requirement.lookup === "explicit-path-or-path" &&
206
+ requirement.executable.includes(path.sep)
207
+ ? fs.existsSync(requirement.executable)
208
+ : Boolean(findTool(requirement.executable, env, platform));
209
+ if (!found)
210
+ errors.push(requirement.missingMessage);
192
211
  }
193
212
  if (errors.length)
194
213
  return { ok: false, errors };
195
214
  return { ok: true };
196
215
  }
216
+ function preflight(cmd, env, platform) {
217
+ return preflightFor(cmd, env, platform, codexHarness);
218
+ }
197
219
  function usage() {
198
220
  return [
199
221
  "usage: superpowers-manager [command] [args...]",
@@ -266,10 +288,8 @@ async function main() {
266
288
  env: process.env,
267
289
  stdout: process.stdout,
268
290
  stderr: process.stderr,
269
- // The ONLY place runAdapter is bound to a context. Every command module
270
- // reaches the adapter through this field; none imports runAdapter
271
- // itself. tests/unit/ctx-adapter-provenance.test.js gates both halves.
272
- adapter: runAdapter,
291
+ // The ONLY production binding of a concrete harness implementation.
292
+ adapter: codexHarness,
273
293
  };
274
294
  let status;
275
295
  try {
@@ -284,7 +304,7 @@ async function main() {
284
304
  }
285
305
  process.exit(status);
286
306
  }
287
- export { resolvePackageRoot, isMain, parseArgs, findTool, commandRequirements, preflight, usage, main, };
307
+ export { resolvePackageRoot, isMain, parseArgs, findTool, commandRequirements, commandRequirementsFor, preflight, preflightFor, usage, main, };
288
308
  if (isMain(import.meta.filename, process.argv[1])) {
289
309
  main().catch((cause) => {
290
310
  console.error(`error: ${oneLine(cause)}`);
@@ -0,0 +1,271 @@
1
+ import { failureResult, hasTerminalControl, successResult, } from "./adapter-result.js";
2
+ import { codexInspect, codexInstall, codexRemove, } from "./adapter.js";
3
+ import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./codex-prepare.js";
4
+ import { codexInstallReceipt, codexPresentation, } from "./codex-presentation.js";
5
+ import { reportLegacyState, requireManagedUpdateControl, requireNoLegacyState, verifyUninstalledResources, } from "./lifecycle.js";
6
+ import { commitMatches } from "./status.js";
7
+ function preserveFailure(result) {
8
+ if (result.outcome.ok) {
9
+ throw new Error("cannot preserve a successful adapter result as failure");
10
+ }
11
+ return { status: result.status, outcome: result.outcome };
12
+ }
13
+ function malformed(result, message) {
14
+ return failureResult(result.outcome.operation, "malformed-result", message, [], result.outcome.messages);
15
+ }
16
+ function invalidStatus(result, message) {
17
+ return failureResult(result.outcome.operation, "invalid-status", message, [], result.outcome.messages);
18
+ }
19
+ function resultRecord(result) {
20
+ const value = result.outcome.ok ? result.outcome.result : null;
21
+ return typeof value === "object" && value !== null && !Array.isArray(value)
22
+ ? value
23
+ : null;
24
+ }
25
+ function stringField(result, key) {
26
+ const raw = result[key];
27
+ if (raw === null || raw === undefined)
28
+ return { ok: true, value: "" };
29
+ if (typeof raw === "string")
30
+ return { ok: true, value: raw };
31
+ return { ok: false };
32
+ }
33
+ function resourceFlag(result, key) {
34
+ const resources = result.resources;
35
+ const bag = typeof resources === "object" &&
36
+ resources !== null &&
37
+ !Array.isArray(resources)
38
+ ? resources
39
+ : {};
40
+ const value = bag[key];
41
+ return typeof value === "boolean" ? value : null;
42
+ }
43
+ function installDecision(legacy) {
44
+ switch (legacy.kind) {
45
+ case "ok":
46
+ return { kind: "allowed" };
47
+ case "blocked":
48
+ return {
49
+ kind: "blocked",
50
+ output: { stdout: [], stderr: legacy.lines },
51
+ };
52
+ case "unknown":
53
+ return {
54
+ kind: "blocked",
55
+ output: { stdout: [], stderr: [`error: ${legacy.message}`] },
56
+ };
57
+ case "report":
58
+ return {
59
+ kind: "blocked",
60
+ output: {
61
+ stdout: [],
62
+ stderr: ["error: unexpected legacy report during installation"],
63
+ },
64
+ };
65
+ }
66
+ }
67
+ export function normalizeCodexOwnership(result) {
68
+ if (!result.outcome.ok)
69
+ return preserveFailure(result);
70
+ if (result.status !== 0) {
71
+ return invalidStatus(result, "adapter reported a failure status for inspect --view ownership");
72
+ }
73
+ const record = resultRecord(result);
74
+ if (record === null) {
75
+ return malformed(result, "adapter returned a non-object result for inspect --view ownership");
76
+ }
77
+ const pluginPresent = resourceFlag(record, "plugin");
78
+ if (pluginPresent === null) {
79
+ return malformed(result, "expected a Boolean adapter result at resources.plugin");
80
+ }
81
+ const marketplacePresent = resourceFlag(record, "marketplace");
82
+ if (marketplacePresent === null) {
83
+ return malformed(result, "expected a Boolean adapter result at resources.marketplace");
84
+ }
85
+ const identity = stringField(record, "identity_state");
86
+ if (!identity.ok) {
87
+ return malformed(result, "adapter returned a non-string identity_state for inspect --view ownership");
88
+ }
89
+ const identityState = identity.value;
90
+ const installEligibility = identityState.length === 0
91
+ ? {
92
+ kind: "blocked",
93
+ output: {
94
+ stdout: [],
95
+ stderr: ["error: probe did not report adapter identity state"],
96
+ },
97
+ }
98
+ : installDecision(requireNoLegacyState(identityState));
99
+ const verification = verifyUninstalledResources(result);
100
+ const legacyReport = reportLegacyState(identityState);
101
+ let removalVerification;
102
+ if (!verification.ok) {
103
+ removalVerification = {
104
+ kind: "blocked",
105
+ output: { stdout: [], stderr: [`error: ${verification.message}`] },
106
+ };
107
+ }
108
+ else if (legacyReport.kind === "unknown") {
109
+ removalVerification = {
110
+ kind: "blocked",
111
+ output: { stdout: [], stderr: [`error: ${legacyReport.message}`] },
112
+ };
113
+ }
114
+ else if (legacyReport.kind === "blocked") {
115
+ removalVerification = {
116
+ kind: "blocked",
117
+ output: {
118
+ stdout: [],
119
+ stderr: ["error: unexpected legacy block after removal"],
120
+ },
121
+ };
122
+ }
123
+ else {
124
+ removalVerification = { kind: "allowed" };
125
+ }
126
+ const postRemovalOutput = legacyReport.kind === "report"
127
+ ? { stdout: legacyReport.lines, stderr: [] }
128
+ : { stdout: [], stderr: [] };
129
+ return successResult(result.outcome.operation, {
130
+ installEligibility,
131
+ removalInput: { pluginPresent, marketplacePresent },
132
+ removalVerification,
133
+ postRemovalOutput,
134
+ presentationValue: identityState,
135
+ }, result.outcome.messages);
136
+ }
137
+ export function normalizeCodexControl(result) {
138
+ if (!result.outcome.ok)
139
+ return preserveFailure(result);
140
+ if (result.status !== 0) {
141
+ return invalidStatus(result, "adapter reported a failure status for inspect --view update-control");
142
+ }
143
+ const record = resultRecord(result);
144
+ if (record === null) {
145
+ return malformed(result, "adapter returned a non-object result for inspect --view update-control");
146
+ }
147
+ const control = stringField(record, "update_control");
148
+ if (!control.ok) {
149
+ return malformed(result, "adapter returned a non-string update_control for inspect --view update-control");
150
+ }
151
+ const updateControl = control.value;
152
+ const probeEligibility = updateControl.length === 0
153
+ ? {
154
+ kind: "blocked",
155
+ output: {
156
+ stdout: [],
157
+ stderr: [
158
+ "error: probe did not report adapter update-control capability",
159
+ ],
160
+ },
161
+ }
162
+ : { kind: "allowed" };
163
+ const managed = requireManagedUpdateControl(updateControl);
164
+ const mutationEligibility = probeEligibility.kind === "blocked"
165
+ ? probeEligibility
166
+ : managed.ok
167
+ ? { kind: "allowed" }
168
+ : {
169
+ kind: "blocked",
170
+ output: {
171
+ stdout: [],
172
+ stderr: [`error: ${managed.message}`],
173
+ },
174
+ };
175
+ return successResult(result.outcome.operation, { probeEligibility, mutationEligibility, presentationValue: updateControl }, result.outcome.messages);
176
+ }
177
+ export function normalizeCodexInstalled(result, desiredCommit) {
178
+ if (!result.outcome.ok)
179
+ return preserveFailure(result);
180
+ if (result.status !== 0) {
181
+ return invalidStatus(result, "adapter reported a failure status for inspect --view fingerprint");
182
+ }
183
+ const record = resultRecord(result);
184
+ if (record === null) {
185
+ return malformed(result, "adapter returned a non-object result for inspect --view fingerprint");
186
+ }
187
+ const fingerprint = stringField(record, "fingerprint");
188
+ if (!fingerprint.ok) {
189
+ return malformed(result, "adapter returned a non-string fingerprint for inspect --view fingerprint");
190
+ }
191
+ const observedIdentity = fingerprint.value;
192
+ const installed = observedIdentity.length === 0
193
+ ? { kind: "absent", observedIdentity: "" }
194
+ : commitMatches(desiredCommit, observedIdentity)
195
+ ? { kind: "current", observedIdentity }
196
+ : { kind: "mismatch", observedIdentity };
197
+ return successResult(result.outcome.operation, installed, result.outcome.messages);
198
+ }
199
+ function safeHint(value) {
200
+ return typeof value === "string" && !hasTerminalControl(value) ? value : "";
201
+ }
202
+ export function normalizeCodexInstall(result) {
203
+ if (!result.outcome.ok)
204
+ return preserveFailure(result);
205
+ if (result.status !== 0) {
206
+ return invalidStatus(result, "adapter reported a failure status for install");
207
+ }
208
+ const record = resultRecord(result);
209
+ if (record === null) {
210
+ return malformed(result, "adapter returned a non-object result for install");
211
+ }
212
+ const hints = record.verification_hints;
213
+ const bag = typeof hints === "object" && hints !== null && !Array.isArray(hints)
214
+ ? hints
215
+ : {};
216
+ return successResult(result.outcome.operation, codexInstallReceipt(safeHint(bag.missing), safeHint(bag.mismatch)), result.outcome.messages);
217
+ }
218
+ // The canonical payload normalizer has no context parameter. Keep the native
219
+ // call's defensive success/nonzero-status guard beside the concrete adapter,
220
+ // where the exact legacy diagnostic can still name ctx.root.
221
+ export function normalizeCodexInstallForContext(result, ctx) {
222
+ if (!result.outcome.ok)
223
+ return preserveFailure(result);
224
+ if (result.status !== 0) {
225
+ return invalidStatus(result, codexPresentation.callFailure("install", ctx).invalidStatus);
226
+ }
227
+ return normalizeCodexInstall(result);
228
+ }
229
+ function requirements(command, env) {
230
+ if (command !== "probe" &&
231
+ command !== "install" &&
232
+ command !== "update" &&
233
+ command !== "uninstall") {
234
+ return [];
235
+ }
236
+ const executable = env.SUPERPOWERS_CODEX || "codex";
237
+ return [
238
+ {
239
+ name: "codex",
240
+ executable,
241
+ lookup: "explicit-path-or-path",
242
+ missingMessage: `required command not found: ${executable} — install the Codex CLI or set SUPERPOWERS_CODEX`,
243
+ },
244
+ ];
245
+ }
246
+ export const codexHarness = {
247
+ preparationLocation: codexPreparationLocation,
248
+ validatePreparationBeforeFetch: validateCodexPreparationBeforeFetch,
249
+ prepareCandidate: prepareCodexCandidate,
250
+ inspectPrepared: inspectCodexPrepared,
251
+ readPrepared: readCodexPrepared,
252
+ inspectOwnership: async (ctx) => normalizeCodexOwnership(await codexInspect("ownership", ctx)),
253
+ inspectUpdateControl: async (ctx) => normalizeCodexControl(await codexInspect("update-control", ctx)),
254
+ inspectInstalled: async (selection, ctx) => normalizeCodexInstalled(await codexInspect("fingerprint", ctx), selection.desiredCommit),
255
+ install: async (_artifact, ctx) => {
256
+ return normalizeCodexInstallForContext(await codexInstall(ctx.root, ctx), ctx);
257
+ },
258
+ remove: async (input, ctx) => {
259
+ const result = await codexRemove(input, ctx);
260
+ if (!result.outcome.ok)
261
+ return preserveFailure(result);
262
+ if (result.status !== 0) {
263
+ return invalidStatus(result, codexPresentation.callFailure("remove", ctx, input).invalidStatus);
264
+ }
265
+ return successResult(result.outcome.operation, null, result.outcome.messages);
266
+ },
267
+ requirements,
268
+ get presentation() {
269
+ return codexPresentation;
270
+ },
271
+ };
@@ -0,0 +1,199 @@
1
+ import { cp, mkdir, rm, stat } from "node:fs/promises";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { failureResult, successResult, } from "./adapter-result.js";
4
+ import { codexBuild } from "./adapter.js";
5
+ import { commitMatches } from "./domain/fingerprint.js";
6
+ import { readManifest } from "./hooks.js";
7
+ import { generatedCommitOrEmpty, generatedMetadataPath, readStrictProvenanceField, writeProvenance, } from "./provenance.js";
8
+ import { SafetyError } from "./safety-error.js";
9
+ import { manifestVersionForRef } from "./upstream-version.js";
10
+ // Order is inherited from the original prepare command; the first miss wins.
11
+ const REQUIRED_UPSTREAM = [
12
+ { path: "skills", label: "skills/" },
13
+ { path: "LICENSE", label: "LICENSE" },
14
+ { path: "README.md", label: "README.md" },
15
+ { path: "CODE_OF_CONDUCT.md", label: "CODE_OF_CONDUCT.md" },
16
+ ];
17
+ const COPY_PATHS = [
18
+ "skills",
19
+ "assets",
20
+ "LICENSE",
21
+ "README.md",
22
+ "CODE_OF_CONDUCT.md",
23
+ ];
24
+ const RESOLUTION_KINDS = [
25
+ "latest-release",
26
+ "tag",
27
+ "ref",
28
+ "raw-commit",
29
+ ];
30
+ function prepareError(message, cause) {
31
+ return new SafetyError("prepare", message, { cause });
32
+ }
33
+ function isControlledPreparationError(cause) {
34
+ return (cause instanceof SafetyError &&
35
+ (cause.module === "prepare" ||
36
+ cause.module === "hooks" ||
37
+ cause.module === "provenance"));
38
+ }
39
+ async function pathExists(path) {
40
+ try {
41
+ await stat(path);
42
+ return true;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ async function regularFileExists(path) {
49
+ try {
50
+ return (await stat(path)).isFile();
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ async function owned(message, fn) {
57
+ try {
58
+ return await fn();
59
+ }
60
+ catch (cause) {
61
+ throw prepareError(message, cause);
62
+ }
63
+ }
64
+ async function copyPathIfPresent(source, destination) {
65
+ if (!(await pathExists(source)))
66
+ return;
67
+ await owned(`cannot clear candidate path: ${destination}`, () => rm(destination, { recursive: true, force: true }));
68
+ await owned(`cannot copy upstream path into candidate: ${source}`, () => cp(source, destination, { recursive: true, verbatimSymlinks: true }));
69
+ }
70
+ export function resolveFromCwd(value, cwd) {
71
+ return isAbsolute(value) ? value : resolve(cwd, value);
72
+ }
73
+ function manifestTemplate(ctx) {
74
+ const env = ctx.env ?? {};
75
+ return (env.SUPERPOWERS_MANIFEST_TEMPLATE ||
76
+ join(ctx.root, "plugins", "superpowers", ".codex-plugin", "plugin.template.json"));
77
+ }
78
+ export function codexPreparationLocation(ctx) {
79
+ const env = ctx.env ?? {};
80
+ return {
81
+ destinationRoot: resolveFromCwd(env.SUPERPOWERS_PLUGIN_ROOT || join(ctx.root, "plugins", "superpowers"), process.cwd()),
82
+ stagingLeaf: "superpowers",
83
+ };
84
+ }
85
+ export async function validateCodexPreparationBeforeFetch(ctx) {
86
+ const template = manifestTemplate(ctx);
87
+ if (!(await regularFileExists(template))) {
88
+ return failureResult("prepare", "missing-template", `missing fallback manifest template: ${template}`, [], []);
89
+ }
90
+ return successResult("prepare", null, []);
91
+ }
92
+ export async function readUpstreamManifestVersion(path) {
93
+ const manifest = await readManifest(path);
94
+ const value = manifest.version;
95
+ if (value === undefined || value === null)
96
+ return "";
97
+ if (typeof value !== "string") {
98
+ throw prepareError(`upstream manifest version is not a string: ${path}`);
99
+ }
100
+ return value;
101
+ }
102
+ function asResolutionKind(value) {
103
+ for (const kind of RESOLUTION_KINDS) {
104
+ if (kind === value)
105
+ return kind;
106
+ }
107
+ throw prepareError(`unknown upstream resolution kind: ${value}`);
108
+ }
109
+ export async function prepareCodexCandidate(input, ctx) {
110
+ let upstreamManifestVersion = "";
111
+ let managerVersion = "";
112
+ try {
113
+ for (const required of REQUIRED_UPSTREAM) {
114
+ if (!(await pathExists(join(input.upstreamRoot, required.path)))) {
115
+ return failureResult("prepare", "missing-upstream-path", `required upstream path missing: ${required.label}`, [], []);
116
+ }
117
+ }
118
+ await owned(`cannot clear candidate root: ${input.candidateRoot}`, () => rm(input.candidateRoot, { recursive: true, force: true }));
119
+ await owned(`cannot create candidate root: ${input.candidateRoot}`, () => mkdir(join(input.candidateRoot, ".codex-plugin"), { recursive: true }));
120
+ for (const name of COPY_PATHS) {
121
+ await copyPathIfPresent(join(input.upstreamRoot, name), join(input.candidateRoot, name));
122
+ }
123
+ const upstreamManifest = join(input.upstreamRoot, ".codex-plugin", "plugin.json");
124
+ if (await regularFileExists(upstreamManifest)) {
125
+ upstreamManifestVersion =
126
+ await readUpstreamManifestVersion(upstreamManifest);
127
+ }
128
+ await writeProvenance(join(input.candidateRoot, ".superpowers-upstream.json"), {
129
+ source: input.selection.effectiveSource,
130
+ requested_ref: input.selection.requestedRef,
131
+ resolved_ref: input.selection.resolvedRef,
132
+ commit: input.selection.desiredCommit,
133
+ upstream_manifest_version: upstreamManifestVersion,
134
+ });
135
+ managerVersion = manifestVersionForRef({
136
+ requestedRef: input.selection.requestedRef,
137
+ resolutionKind: asResolutionKind(input.selection.resolutionKind),
138
+ resolvedRef: input.selection.resolvedRef,
139
+ commit: input.selection.desiredCommit,
140
+ });
141
+ }
142
+ catch (cause) {
143
+ // owned()/asResolutionKind(), readManifest(), and writeProvenance replace
144
+ // every subordinate failure with controlled preparation, hook, or
145
+ // provenance text. Re-emitting those owned diagnostics is therefore safe.
146
+ // codexBuild stays below this catch because it deliberately rethrows
147
+ // exceptions its native operation does not own; the shared command must
148
+ // hide those.
149
+ if (!isControlledPreparationError(cause))
150
+ throw cause;
151
+ return failureResult("prepare", "prepare-failed", cause.message, [], []);
152
+ }
153
+ const built = await codexBuild({
154
+ upstreamRoot: input.upstreamRoot,
155
+ candidateRoot: input.candidateRoot,
156
+ requestedRef: input.selection.requestedRef,
157
+ resolvedRef: input.selection.resolvedRef,
158
+ commit: input.selection.desiredCommit,
159
+ managerVersion,
160
+ upstreamManifestVersion,
161
+ fallbackManifest: manifestTemplate(ctx),
162
+ }, ctx);
163
+ if (!built.outcome.ok) {
164
+ return { status: built.status, outcome: built.outcome };
165
+ }
166
+ if (built.status !== 0) {
167
+ return failureResult("build", "invalid-status", "adapter reported failure without an error outcome", [], built.outcome.messages);
168
+ }
169
+ return successResult(built.outcome.operation, { root: input.candidateRoot, commit: input.selection.desiredCommit }, built.outcome.messages);
170
+ }
171
+ export async function inspectCodexPrepared(selection, ctx) {
172
+ const observedIdentity = await generatedCommitOrEmpty(ctx.root);
173
+ if (!commitMatches(selection.desiredCommit, observedIdentity)) {
174
+ return successResult("inspect-prepared", { kind: "needs-prepare", observedIdentity }, []);
175
+ }
176
+ return successResult("inspect-prepared", {
177
+ kind: "current",
178
+ artifact: {
179
+ root: join(ctx.root, "plugins", "superpowers"),
180
+ commit: observedIdentity,
181
+ },
182
+ observedIdentity,
183
+ }, []);
184
+ }
185
+ export async function readCodexPrepared(ctx) {
186
+ let commit = "";
187
+ try {
188
+ const value = await readStrictProvenanceField(generatedMetadataPath(ctx.root), "commit");
189
+ if (typeof value === "string")
190
+ commit = value;
191
+ }
192
+ catch {
193
+ // The install path treats malformed metadata as an absent commit.
194
+ }
195
+ if (commit.length === 0) {
196
+ return failureResult("read-prepared", "missing-commit", "generated metadata missing desired commit after prepare", [], []);
197
+ }
198
+ return successResult("read-prepared", { root: join(ctx.root, "plugins", "superpowers"), commit }, []);
199
+ }