superpowers-manager 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +36 -16
  2. package/dist/atomic.js +1 -3
  3. package/dist/cli-arguments.js +4 -1
  4. package/dist/cli.js +14 -43
  5. package/dist/commands/adapter-call.js +24 -0
  6. package/dist/commands/install.js +16 -63
  7. package/dist/commands/mutation.js +5 -5
  8. package/dist/commands/pin.js +10 -5
  9. package/dist/commands/prepare.js +42 -108
  10. package/dist/commands/probe.js +33 -19
  11. package/dist/commands/uninstall.js +18 -46
  12. package/dist/commands/unpin.js +2 -1
  13. package/dist/commands/update.js +8 -29
  14. package/dist/domain/refs.js +0 -3
  15. package/dist/effective-selection.js +11 -15
  16. package/dist/git-tree.js +4 -6
  17. package/dist/harness-command-result.js +33 -3
  18. package/dist/harnesses/claude-code/harness.js +39 -0
  19. package/dist/harnesses/claude-code/install.js +380 -0
  20. package/dist/harnesses/claude-code/native.js +79 -0
  21. package/dist/harnesses/claude-code/paths.js +35 -0
  22. package/dist/harnesses/claude-code/prepare.js +117 -0
  23. package/dist/harnesses/claude-code/presentation.js +25 -0
  24. package/dist/harnesses/claude-code/state.js +253 -0
  25. package/dist/harnesses/codex/adapter.js +36 -69
  26. package/dist/harnesses/codex/conflicts.js +3 -8
  27. package/dist/harnesses/codex/generated-plugin.js +4 -11
  28. package/dist/harnesses/codex/harness.js +4 -26
  29. package/dist/harnesses/codex/hooks.js +28 -27
  30. package/dist/harnesses/codex/lifecycle.js +22 -107
  31. package/dist/harnesses/codex/manifest-overlay.js +9 -27
  32. package/dist/harnesses/codex/paths.js +29 -0
  33. package/dist/harnesses/codex/prepare.js +9 -43
  34. package/dist/harnesses/codex/presentation.js +1 -1
  35. package/dist/harnesses/codex/publication.js +5 -40
  36. package/dist/harnesses/codex/state.js +1 -6
  37. package/dist/harnesses/codex/stored-state.js +1 -6
  38. package/dist/harnesses/opencode/config.js +2 -4
  39. package/dist/harnesses/opencode/discovery.js +1 -5
  40. package/dist/harnesses/opencode/install.js +12 -93
  41. package/dist/harnesses/opencode/native.js +28 -49
  42. package/dist/harnesses/opencode/package.js +11 -55
  43. package/dist/harnesses/opencode/prepare.js +2 -7
  44. package/dist/harnesses/opencode/presentation.js +2 -7
  45. package/dist/harnesses/pi/install.js +31 -120
  46. package/dist/harnesses/pi/native.js +9 -34
  47. package/dist/harnesses/pi/package.js +9 -50
  48. package/dist/harnesses/pi/prepare.js +3 -8
  49. package/dist/harnesses/pi/presentation.js +2 -7
  50. package/dist/harnesses/pi/settings.js +5 -9
  51. package/dist/harnesses/pi/shared-skills.js +23 -53
  52. package/dist/harnesses/pi/state.js +3 -10
  53. package/dist/provenance.js +3 -19
  54. package/dist/resource-lock.js +11 -16
  55. package/dist/safe-path.js +28 -1
  56. package/dist/selection.js +15 -19
  57. package/dist/snapshot-journal.js +101 -0
  58. package/dist/snapshot-package.js +62 -1
  59. package/dist/snapshot-prepare.js +9 -2
  60. package/dist/snapshot-presentation.js +9 -0
  61. package/dist/upstream.js +8 -12
  62. package/dist/validator.js +26 -23
  63. package/dist/workspace.js +29 -35
  64. package/package.json +24 -13
  65. package/dist/python-json.js +0 -4
  66. package/dist/upstream-workspace.js +0 -8
@@ -0,0 +1,25 @@
1
+ import { displaySource } from "../../selection.js";
2
+ import { createSnapshotPresentation, probeOutput, } from "../../snapshot-presentation.js";
3
+ import { displayPath } from "../../validator.js";
4
+ const renderProbe = (facts) => {
5
+ const fields = [
6
+ ["harness", "claude-code"],
7
+ ["desired_commit", facts.selection.desiredCommit],
8
+ ["upstream_source_origin", facts.selection.upstreamSourceOrigin],
9
+ ["effective_source", displaySource(facts.selection.effectiveSource)],
10
+ ["prepared_identity", facts.prepared.observedIdentity],
11
+ ["installed_identity", facts.installed.observedIdentity],
12
+ ["installation_state", facts.installed.kind],
13
+ ["resource_state", facts.resourceState ?? "idle"],
14
+ ["ownership", facts.ownership.presentationValue],
15
+ ["conflicts", (facts.ownership.presentationConflicts ?? []).join("; ")],
16
+ ["update_control", facts.control.presentationValue],
17
+ ["compatibility", facts.compatibility.kind],
18
+ ["compatibility_reason", facts.compatibility.reason],
19
+ ["status", facts.status],
20
+ ].map(([key, value]) => [key, displayPath(value)]);
21
+ return probeOutput(fields);
22
+ };
23
+ export const claudeCodePresentation = createSnapshotPresentation("Claude Code", renderProbe, (input) => input.registration === "absent" &&
24
+ !input.pluginInstalled &&
25
+ !input.published);
@@ -0,0 +1,253 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { readArtifactFile } from "../../artifact-tree.js";
4
+ import { inspectionFailure, successResult, } from "../../adapter-result.js";
5
+ import { canonicalizeProspectivePath, classifyPathNoFollow, } from "../../safe-path.js";
6
+ import { observeSnapshot, sameSnapshotSource, } from "../../snapshot-package.js";
7
+ import { displayPath } from "../../validator.js";
8
+ import { readClaudeCodeState, runClaude, } from "./native.js";
9
+ import { assertClaudeCodeStorageSafe, claudeCodePaths, } from "./paths.js";
10
+ import { expectedClaudeCodeVersion, readClaudeCodeManifestVersion, readClaudeCodePackageAssessment, readClaudeCodeReceipt, } from "./prepare.js";
11
+ export const CLAUDE_CODE_MARKETPLACE = "superpowers-manager";
12
+ export const CLAUDE_CODE_PLUGIN_ID = "superpowers@superpowers-manager";
13
+ export const CLAUDE_CODE_MARKETPLACE_BYTES = Buffer.from(JSON.stringify({
14
+ name: CLAUDE_CODE_MARKETPLACE,
15
+ owner: { name: CLAUDE_CODE_MARKETPLACE },
16
+ plugins: [{ name: "superpowers", source: "./plugins/superpowers" }],
17
+ }) + "\n");
18
+ const SYNCED_ID = "superpowers@synced";
19
+ const DISPLAYABLE_ID = /^superpowers@[A-Za-z0-9][A-Za-z0-9._-]{0,200}$/;
20
+ function blocked(...stderr) {
21
+ return {
22
+ kind: "blocked",
23
+ output: { stdout: [], stderr: stderr.map((line) => displayPath(line)) },
24
+ };
25
+ }
26
+ function pathsFor(ctx) {
27
+ return claudeCodePaths(ctx.env ?? {}, process.cwd());
28
+ }
29
+ export async function marketplaceRegistration(native, paths) {
30
+ const entries = native.marketplaces.filter((entry) => entry.name === CLAUDE_CODE_MARKETPLACE);
31
+ if (entries.length === 0)
32
+ return "absent";
33
+ if (entries.length > 1)
34
+ throw new Error("ambiguous Claude Code marketplace");
35
+ const entry = entries[0];
36
+ if (entry.source !== "directory" || entry.path === null)
37
+ return "foreign";
38
+ return (await canonicalizeProspectivePath(entry.path)) ===
39
+ (await canonicalizeProspectivePath(paths.marketplaceRoot))
40
+ ? "owned"
41
+ : "foreign";
42
+ }
43
+ export function managerPlugin(native) {
44
+ const entries = native.plugins.filter((plugin) => plugin.id === CLAUDE_CODE_PLUGIN_ID && plugin.scope === "user");
45
+ if (entries.length > 1)
46
+ throw new Error("ambiguous Claude Code plugin");
47
+ return entries[0];
48
+ }
49
+ // An unregistered directory is removable only when every entry is part of
50
+ // the Manager's marketplace layout. Empty known directories are safe crash
51
+ // residue; a present snapshot must have a valid Manager receipt and digest.
52
+ export async function inspectClaudeCodeMarketplaceStorage(paths) {
53
+ const rootKind = await classifyPathNoFollow(paths.marketplaceRoot);
54
+ if (rootKind === "missing")
55
+ return "absent";
56
+ if (rootKind !== "directory")
57
+ return "unverified";
58
+ try {
59
+ await assertClaudeCodeStorageSafe(paths);
60
+ // Probe must present recovery before judging a crash leftover's contents.
61
+ // Mutation paths still refuse these siblings before any native call.
62
+ if ((await leftoverPublicationMaterial(paths)).length > 0)
63
+ return "recovery";
64
+ const known = (entries, allowed) => entries.every((entry) => allowed.includes(entry));
65
+ if (!known(await readdir(paths.marketplaceRoot), [
66
+ ".claude-plugin",
67
+ "plugins",
68
+ ]))
69
+ return "unverified";
70
+ const metadataRoot = join(paths.marketplaceRoot, ".claude-plugin");
71
+ if ((await classifyPathNoFollow(metadataRoot)) !== "missing") {
72
+ if (!known(await readdir(metadataRoot), ["marketplace.json"]))
73
+ return "unverified";
74
+ }
75
+ const manifestKind = await classifyPathNoFollow(paths.marketplaceManifest);
76
+ if (manifestKind !== "missing") {
77
+ if (manifestKind !== "regular-file" ||
78
+ !(await readArtifactFile(paths.marketplaceRoot, paths.marketplaceManifest, CLAUDE_CODE_MARKETPLACE_BYTES.length)).equals(CLAUDE_CODE_MARKETPLACE_BYTES))
79
+ return "unverified";
80
+ }
81
+ if ((await classifyPathNoFollow(paths.pluginsRoot)) !== "missing") {
82
+ if (!known(await readdir(paths.pluginsRoot), ["superpowers"]))
83
+ return "unverified";
84
+ if ((await classifyPathNoFollow(paths.pluginRoot)) !== "missing") {
85
+ const snapshot = await observeSnapshot(paths.pluginRoot, readClaudeCodeReceipt);
86
+ if (snapshot.kind !== "owned")
87
+ return "unverified";
88
+ }
89
+ }
90
+ return "owned";
91
+ }
92
+ catch {
93
+ return "unverified";
94
+ }
95
+ }
96
+ // A synced copy is reported as not loaded whenever another origin provides the
97
+ // same name, so it never duplicates the Manager plugin.
98
+ function conflicts(native) {
99
+ return native.plugins
100
+ .filter((plugin) => plugin.enabled &&
101
+ plugin.id.startsWith("superpowers@") &&
102
+ plugin.id !== CLAUDE_CODE_PLUGIN_ID &&
103
+ plugin.id !== SYNCED_ID)
104
+ .map((plugin) => DISPLAYABLE_ID.test(plugin.id)
105
+ ? plugin.id
106
+ : "a Claude Code plugin with a non-displayable Superpowers identity");
107
+ }
108
+ async function observeFacts(ctx, run) {
109
+ const paths = pathsFor(ctx);
110
+ await assertClaudeCodeStorageSafe(paths);
111
+ const native = await readClaudeCodeState(ctx, run);
112
+ const [registration, snapshot, marketplaceKind] = await Promise.all([
113
+ marketplaceRegistration(native, paths),
114
+ observeSnapshot(paths.pluginRoot, readClaudeCodeReceipt),
115
+ classifyPathNoFollow(paths.marketplaceRoot),
116
+ ]);
117
+ if (registration === "absent" &&
118
+ marketplaceKind !== "missing" &&
119
+ (await inspectClaudeCodeMarketplaceStorage(paths)) === "unverified")
120
+ throw new Error("unverified Claude Code marketplace storage");
121
+ return {
122
+ paths,
123
+ registration,
124
+ marketplaceSource: native.marketplaces.find((entry) => entry.name === CLAUDE_CODE_MARKETPLACE),
125
+ plugin: managerPlugin(native),
126
+ conflicts: conflicts(native),
127
+ snapshot,
128
+ published: marketplaceKind !== "missing",
129
+ };
130
+ }
131
+ function installDecision(facts) {
132
+ if (facts.registration === "foreign")
133
+ return blocked(`error: a Claude Code marketplace named ${CLAUDE_CODE_MARKETPLACE} is registered from another source; remove it manually, then retry:`, ` source: ${facts.marketplaceSource?.source ?? "unknown"}; path: ${facts.marketplaceSource?.path ?? "unknown"}`, ` claude plugin marketplace remove ${CLAUDE_CODE_MARKETPLACE}`);
134
+ if (facts.conflicts.length > 0)
135
+ return blocked("error: another Superpowers Claude Code plugin is enabled; disable it manually, then retry:", ...facts.conflicts.map((id) => DISPLAYABLE_ID.test(id) ? ` claude plugin disable ${id}` : ` ${id}`));
136
+ return { kind: "allowed" };
137
+ }
138
+ export async function inspectClaudeCodeOwnership(ctx, run = runClaude) {
139
+ const operation = "inspect-claude-code-ownership";
140
+ try {
141
+ const facts = await observeFacts(ctx, run);
142
+ if (facts.snapshot.kind === "unverified")
143
+ throw new Error("unverified Manager snapshot");
144
+ const removalVerification = facts.registration === "owned" || facts.plugin !== undefined
145
+ ? blocked("error: Claude Code still lists the Manager marketplace or plugin after removal")
146
+ : facts.published
147
+ ? blocked(`error: the Manager-owned Claude Code marketplace is still present at ${facts.paths.marketplaceRoot}`)
148
+ : { kind: "allowed" };
149
+ const presentationValue = facts.registration === "foreign"
150
+ ? "foreign marketplace registration"
151
+ : facts.snapshot.kind === "owned"
152
+ ? facts.registration === "owned"
153
+ ? `managed ${facts.snapshot.digest}`
154
+ : `managed snapshot ${facts.snapshot.digest}`
155
+ : facts.registration === "owned"
156
+ ? "managed registration without a snapshot"
157
+ : "absent";
158
+ return successResult(operation, {
159
+ installEligibility: installDecision(facts),
160
+ removalInput: {
161
+ registration: facts.registration,
162
+ pluginInstalled: facts.plugin !== undefined,
163
+ published: facts.published,
164
+ },
165
+ removalVerification,
166
+ postRemovalOutput: { stdout: [], stderr: [] },
167
+ presentationValue,
168
+ presentationConflicts: facts.conflicts,
169
+ }, []);
170
+ }
171
+ catch {
172
+ return inspectionFailure(operation, "Claude Code ownership");
173
+ }
174
+ }
175
+ export async function inspectClaudeCodeInstalled(selection, ctx, run = runClaude) {
176
+ const operation = "inspect-claude-code-installed";
177
+ let facts;
178
+ try {
179
+ facts = await observeFacts(ctx, run);
180
+ }
181
+ catch {
182
+ return inspectionFailure(operation, "Claude Code installed state");
183
+ }
184
+ const mismatch = (observedIdentity) => successResult(operation, { kind: "mismatch", observedIdentity }, []);
185
+ if (facts.snapshot.kind === "absent")
186
+ return facts.registration === "absent" &&
187
+ facts.plugin === undefined &&
188
+ !facts.published
189
+ ? successResult(operation, { kind: "absent", observedIdentity: "" }, [])
190
+ : mismatch("registered without an installed snapshot");
191
+ if (facts.snapshot.kind === "unverified")
192
+ return mismatch("unverified installed snapshot");
193
+ const observed = facts.snapshot.digest;
194
+ const plugin = facts.plugin;
195
+ if (facts.registration !== "owned" ||
196
+ plugin === undefined ||
197
+ !plugin.enabled ||
198
+ plugin.errorCount > 0 ||
199
+ facts.conflicts.length > 0)
200
+ return mismatch(observed);
201
+ try {
202
+ const prepared = await readClaudeCodePackageAssessment(facts.paths.preparedRoot);
203
+ if (prepared.compatibility.kind !== "supported" ||
204
+ prepared.receipt.commit !== selection.desiredCommit ||
205
+ !sameSnapshotSource(prepared.receipt.source, selection.effectiveSource) ||
206
+ facts.snapshot.receipt.commit !== prepared.receipt.commit ||
207
+ facts.snapshot.digest !== prepared.receipt.digest ||
208
+ plugin.version !== expectedClaudeCodeVersion(selection) ||
209
+ (await readClaudeCodeManifestVersion(facts.paths.pluginRoot)) !==
210
+ expectedClaudeCodeVersion(selection))
211
+ return mismatch(observed);
212
+ }
213
+ catch {
214
+ return mismatch(observed);
215
+ }
216
+ return successResult(operation, { kind: "current", observedIdentity: observed }, []);
217
+ }
218
+ // Filesystem-only: any `.superpowers.*` sibling is a publication backup or a
219
+ // staging directory left by an interrupted install.
220
+ export async function leftoverPublicationMaterial(paths) {
221
+ if ((await classifyPathNoFollow(paths.pluginsRoot)) === "missing")
222
+ return [];
223
+ return (await readdir(paths.pluginsRoot))
224
+ .filter((name) => name.startsWith(".superpowers."))
225
+ .sort()
226
+ .map((name) => join(paths.pluginsRoot, name));
227
+ }
228
+ export async function inspectClaudeCodeControl(ctx) {
229
+ const operation = "inspect-claude-code-control";
230
+ try {
231
+ const paths = pathsFor(ctx);
232
+ await assertClaudeCodeStorageSafe(paths);
233
+ const leftovers = await leftoverPublicationMaterial(paths);
234
+ if (leftovers.length === 0)
235
+ return successResult(operation, {
236
+ probeEligibility: { kind: "allowed" },
237
+ mutationEligibility: { kind: "allowed" },
238
+ presentationValue: "clear",
239
+ }, []);
240
+ // Shared probe requires both decisions blocked whenever recovery is
241
+ // required (`src/commands/probe.ts::function coherentControl`).
242
+ const decision = blocked("error: Claude Code recovery required; inspect and remove leftover publication material manually:", ...leftovers.map((path) => ` ${path}`));
243
+ return successResult(operation, {
244
+ probeEligibility: decision,
245
+ mutationEligibility: decision,
246
+ presentationValue: `recovery required at ${leftovers.join(", ")}`,
247
+ recoveryState: "required",
248
+ }, []);
249
+ }
250
+ catch {
251
+ return inspectionFailure(operation, "Claude Code update control");
252
+ }
253
+ }
@@ -2,8 +2,8 @@ import { Buffer } from "node:buffer";
2
2
  import { execFile } from "node:child_process";
3
3
  import { constants } from "node:fs";
4
4
  import { access, copyFile, mkdir, readFile, realpath, stat, writeFile, } from "node:fs/promises";
5
- import { delimiter, isAbsolute, join } from "node:path";
6
- import { AdapterMessageLog, failureResult, hasTerminalControl, successResult, } from "../../adapter-result.js";
5
+ import { delimiter, join } from "node:path";
6
+ import { AdapterMessageLog, failureResult, successResult, } from "../../adapter-result.js";
7
7
  import { activePluginVersionFromJson, codexInstalledPluginsFromJson, installedListingHas, marketplaceRootFromJson, } from "./json.js";
8
8
  import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./conflicts.js";
9
9
  import { oneLine } from "../../cli-arguments.js";
@@ -16,6 +16,7 @@ import { applyManifestOverlay } from "./manifest-overlay.js";
16
16
  import { readCodexBuildSource } from "../../provenance.js";
17
17
  import { codexOwnershipInspection } from "./lifecycle.js";
18
18
  import { codexInstallReceipt } from "./presentation.js";
19
+ import { isFile } from "../../safe-path.js";
19
20
  const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
20
21
  const MARKETPLACE_NAME = "superpowers-manager";
21
22
  const LEGACY_PLUGIN_ID = CODEX_LEGACY_PLUGIN_ID;
@@ -110,7 +111,7 @@ export function mapCodexLaunchFailure(cause, codexBin) {
110
111
  signal: null,
111
112
  stdout: Buffer.alloc(0),
112
113
  // Trailing newline matches how a real process writes stderr;
113
- // `src/adapter-result.ts:133::appendBytes` splits on newlines and terminates the
114
+ // `src/adapter-result.ts:132::appendBytes` splits on newlines and terminates the
114
115
  // final chunk at end-of-buffer either way.
115
116
  stderr: Buffer.from(`cannot launch Codex command ${codexBin}${detail}\n`, "utf8"),
116
117
  };
@@ -160,42 +161,14 @@ async function requireCodex(codexBin, env) {
160
161
  fail("command-not-found", `required Codex command not found: ${codexBin}`);
161
162
  }
162
163
  }
163
- async function directoryExists(path) {
164
- try {
165
- return (await stat(path)).isDirectory();
166
- }
167
- catch {
168
- return false;
169
- }
170
- }
171
- async function fileExists(path) {
172
- try {
173
- return (await stat(path)).isFile();
174
- }
175
- catch {
176
- return false;
177
- }
178
- }
179
164
  async function runBuild(input, env, log) {
180
165
  const { upstreamRoot, candidateRoot, fallbackManifest } = input;
181
- if (!isAbsolute(upstreamRoot)) {
182
- fail("invalid-arguments", "--upstream-root must be an absolute path");
183
- }
184
- if (!isAbsolute(candidateRoot)) {
185
- fail("invalid-arguments", "--candidate-root must be an absolute path");
186
- }
187
- if (!(await directoryExists(upstreamRoot))) {
188
- fail("invalid-arguments", `upstream root not found: ${upstreamRoot}`);
189
- }
190
- if (!(await directoryExists(candidateRoot))) {
191
- fail("invalid-arguments", `candidate root not found: ${candidateRoot}`);
192
- }
193
- if (!(await fileExists(fallbackManifest))) {
166
+ if (!(await isFile(fallbackManifest))) {
194
167
  fail("invalid-arguments", `fallback manifest not found: ${fallbackManifest}`);
195
168
  }
196
169
  const candidateManifest = join(candidateRoot, ".codex-plugin/plugin.json");
197
170
  const upstreamManifest = join(upstreamRoot, ".codex-plugin/plugin.json");
198
- const manifestSource = (await fileExists(upstreamManifest))
171
+ const manifestSource = (await isFile(upstreamManifest))
199
172
  ? "upstream"
200
173
  : "fallback";
201
174
  try {
@@ -209,12 +182,20 @@ async function runBuild(input, env, log) {
209
182
  ? "cannot copy upstream manifest into candidate"
210
183
  : "cannot copy fallback manifest into candidate");
211
184
  }
212
- let plan;
213
185
  let sourceRoot;
214
186
  let realCandidateRoot;
215
187
  try {
216
188
  sourceRoot = await realpath(upstreamRoot);
217
189
  realCandidateRoot = await realpath(candidateRoot);
190
+ }
191
+ catch {
192
+ // Kept out of the classification catch below, which re-emits its cause:
193
+ // a raw realpath error would carry Node's wording onto stderr.
194
+ log.appendText("stderr", `cannot resolve hook roots: ${upstreamRoot}, ${candidateRoot}`);
195
+ fail("build-failed", "failed to prepare upstream Codex hooks");
196
+ }
197
+ let plan;
198
+ try {
218
199
  const manifest = await readManifest(candidateManifest);
219
200
  plan = await classifyHooks(manifest, manifestSource, sourceRoot);
220
201
  }
@@ -254,8 +235,8 @@ async function runBuild(input, env, log) {
254
235
  // applyManifestOverlay's own messages already name the manifest
255
236
  // path — three are the frozen CPython wording, and the fourth (the
256
237
  // numeric-overflow diagnostic, which has no CPython oracle wording
257
- // to match) now carries the path via its own rewrap in
258
- // src/harnesses/codex/manifest-overlay.ts. Emit as-is, with no added prefix — a
238
+ // to match) carries the path from src/harnesses/codex/manifest-overlay.ts's
239
+ // emitter. Emit as-is, with no added prefix — a
259
240
  // prefix here would double up the path these messages already
260
241
  // name.
261
242
  log.appendText("stderr", oneLine(cause));
@@ -309,12 +290,6 @@ async function runBuild(input, env, log) {
309
290
  return {};
310
291
  }
311
292
  async function runInstall(packageRoot, env, log) {
312
- if (!isAbsolute(packageRoot)) {
313
- fail("invalid-arguments", "--package-root must be an absolute path");
314
- }
315
- if (!(await directoryExists(packageRoot))) {
316
- fail("invalid-arguments", `package root not found: ${packageRoot}`);
317
- }
318
293
  const codexBin = env.SUPERPOWERS_CODEX || "codex";
319
294
  const refreshMode = codexInstallRefreshMode(env);
320
295
  await requireCodex(codexBin, env);
@@ -392,11 +367,6 @@ async function runUninstall(input, env, log) {
392
367
  return {};
393
368
  }
394
369
  function ownershipFromResources(pluginPresent, marketplacePresent, legacyPresent, conflicts) {
395
- if (conflicts.some((item) => typeof item !== "string" ||
396
- item.length === 0 ||
397
- hasTerminalControl(item))) {
398
- fail("malformed-result", "expected an array of strings at conflicts");
399
- }
400
370
  const managerPresent = pluginPresent || marketplacePresent;
401
371
  const identityState = managerPresent
402
372
  ? legacyPresent
@@ -407,14 +377,27 @@ function ownershipFromResources(pluginPresent, marketplacePresent, legacyPresent
407
377
  : "neither";
408
378
  return codexOwnershipInspection(identityState, { pluginPresent, marketplacePresent }, conflicts);
409
379
  }
380
+ async function ownershipAfterListingFailure(context, env, message) {
381
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", message);
382
+ const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
383
+ return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
384
+ }
385
+ async function nativeAfterListingFailure(env, message) {
386
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", message);
387
+ return {
388
+ marketplaceRoot: stored.managerMarketplaceRoot,
389
+ pluginPresent: stored.managerPluginPresent,
390
+ pluginEnabled: stored.managerPluginEnabled,
391
+ activeVersion: null,
392
+ activeRoot: null,
393
+ };
394
+ }
410
395
  async function runOwnership(context, env, log) {
411
396
  const codexBin = env.SUPERPOWERS_CODEX || "codex";
412
397
  await requireCodex(codexBin, env);
413
398
  const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
414
399
  if (commandFailed(plugins)) {
415
- const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
416
- const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
417
- return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
400
+ return await ownershipAfterListingFailure(context, env, `cannot list Codex plugins via '${codexBin} plugin list --json'`);
418
401
  }
419
402
  let managerPlugin;
420
403
  let legacyPlugin;
@@ -430,9 +413,7 @@ async function runOwnership(context, env, log) {
430
413
  }
431
414
  const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
432
415
  if (commandFailed(marketplaces)) {
433
- const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
434
- const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
435
- return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
416
+ return await ownershipAfterListingFailure(context, env, `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
436
417
  }
437
418
  let managerMarketplace;
438
419
  let legacyMarketplace;
@@ -484,14 +465,7 @@ export function codexReadNativeState(context) {
484
465
  await requireCodex(codexBin, env);
485
466
  const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
486
467
  if (commandFailed(plugins)) {
487
- const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
488
- return {
489
- marketplaceRoot: stored.managerMarketplaceRoot,
490
- pluginPresent: stored.managerPluginPresent,
491
- pluginEnabled: stored.managerPluginEnabled,
492
- activeVersion: null,
493
- activeRoot: null,
494
- };
468
+ return await nativeAfterListingFailure(env, `cannot list Codex plugins via '${codexBin} plugin list --json'`);
495
469
  }
496
470
  let manager;
497
471
  let activeVersion;
@@ -512,14 +486,7 @@ export function codexReadNativeState(context) {
512
486
  }
513
487
  const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
514
488
  if (commandFailed(marketplaces)) {
515
- const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
516
- return {
517
- marketplaceRoot: stored.managerMarketplaceRoot,
518
- pluginPresent: stored.managerPluginPresent,
519
- pluginEnabled: stored.managerPluginEnabled,
520
- activeVersion: null,
521
- activeRoot: null,
522
- };
489
+ return await nativeAfterListingFailure(env, `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
523
490
  }
524
491
  let marketplaceRoot;
525
492
  try {
@@ -1,17 +1,12 @@
1
1
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { isAbsenceError } from "../../safe-path.js";
3
4
  export const CODEX_MANAGER_PLUGIN_ID = "superpowers@superpowers-manager";
4
5
  export const CODEX_LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
5
6
  const SUPERPOWERS_PLUGIN_PREFIX = "superpowers@";
6
7
  const DISPLAYABLE_PLUGIN_ID = /^superpowers@[A-Za-z0-9][A-Za-z0-9._-]*/;
7
8
  const NATIVE_ROUTE = ".agents/skills/superpowers";
8
9
  const NATIVE_ROUTE_CONFLICT = `native Codex skills route ~/${NATIVE_ROUTE} has indeterminate activity`;
9
- function isMissingPath(cause) {
10
- if (cause === null || typeof cause !== "object" || !("code" in cause)) {
11
- return false;
12
- }
13
- return cause.code === "ENOENT" || cause.code === "ENOTDIR";
14
- }
15
10
  function isUnmanagedPluginId(pluginId) {
16
11
  if (pluginId === CODEX_MANAGER_PLUGIN_ID ||
17
12
  pluginId === CODEX_LEGACY_PLUGIN_ID) {
@@ -53,14 +48,14 @@ async function nativeRouteConflict(ctx) {
53
48
  routeIsSymlink = (await lstat(route)).isSymbolicLink();
54
49
  }
55
50
  catch (cause) {
56
- return isMissingPath(cause) ? "" : NATIVE_ROUTE_CONFLICT;
51
+ return isAbsenceError(cause) ? "" : NATIVE_ROUTE_CONFLICT;
57
52
  }
58
53
  if (!routeIsSymlink) {
59
54
  try {
60
55
  await lstat(asset);
61
56
  }
62
57
  catch (cause) {
63
- return isMissingPath(cause) ? "" : NATIVE_ROUTE_CONFLICT;
58
+ return isAbsenceError(cause) ? "" : NATIVE_ROUTE_CONFLICT;
64
59
  }
65
60
  }
66
61
  try {
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import { posix } from "node:path";
3
3
  import { COMMIT_RE, SEMVER_RE } from "../../domain/refs.js";
4
4
  import { compareByCodePoint, pythonStrip } from "../../python-text.js";
5
+ import { isAbsenceError } from "../../safe-path.js";
5
6
  import { DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "../../skill-validation.js";
6
7
  import { parseStrictJson } from "../../strict-json.js";
7
8
  const MANIFEST_JSON_PROFILE = {
@@ -18,14 +19,6 @@ class InspectionFailure extends Error {
18
19
  /** Any error from a directory enumeration, absence-like included. */
19
20
  class EnumerationFailure extends Error {
20
21
  }
21
- /**
22
- * `ENOENT`/`ENOTDIR` mean *missing*; every other error rejects. This is the
23
- * only place the absence set is defined.
24
- */
25
- function isAbsenceError(cause) {
26
- const code = cause?.code;
27
- return code === "ENOENT" || code === "ENOTDIR";
28
- }
29
22
  const STRICT_DECODER = new TextDecoder("utf-8", {
30
23
  fatal: true,
31
24
  ignoreBOM: true,
@@ -447,9 +440,9 @@ async function validateHookSubtree(pluginRoot, hooksRoot, errors, deps) {
447
440
  isLink = (await inspectLink(path, deps)) === "symlink";
448
441
  }
449
442
  catch {
450
- // The `src/harnesses/codex/generated-plugin.ts:583::isLink =` probe reaches the first catch bounded by `src/harnesses/codex/generated-plugin.ts:592::let rawTarget`; the `src/harnesses/codex/generated-plugin.ts:594::rawTarget = decodePathBytes` readlink reaches the same site text.
451
- // The three probes sharing the *subtree* string are `src/harnesses/codex/generated-plugin.ts:627::resolvedDirectory =`, `src/harnesses/codex/generated-plugin.ts:637::children =`, and
452
- // `src/harnesses/codex/generated-plugin.ts:646::deps, true)) === "directory"`, not this one.
443
+ // The `src/harnesses/codex/generated-plugin.ts:575::isLink =` probe reaches the first catch bounded by `src/harnesses/codex/generated-plugin.ts:584::let rawTarget`; the `src/harnesses/codex/generated-plugin.ts:586::rawTarget = decodePathBytes` readlink reaches the same site text.
444
+ // The three probes sharing the *subtree* string are `src/harnesses/codex/generated-plugin.ts:619::resolvedDirectory =`, `src/harnesses/codex/generated-plugin.ts:629::children =`, and
445
+ // `src/harnesses/codex/generated-plugin.ts:638::deps, true)) === "directory"`, not this one.
453
446
  errors.push(`generated hook symlink could not be inspected: ${path}`);
454
447
  return false;
455
448
  }
@@ -1,4 +1,4 @@
1
- import { failureResult, successResult, } from "../../adapter-result.js";
1
+ import { successResult, } from "../../adapter-result.js";
2
2
  import { codexInspectControl, codexInspectOwnership, codexInstall, } from "./adapter.js";
3
3
  import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./prepare.js";
4
4
  import { codexPresentation } from "./presentation.js";
@@ -6,17 +6,6 @@ import { codexHome, codexPaths } from "./paths.js";
6
6
  import { installCodexMarketplace, removeCodexMarketplace, } from "./publication.js";
7
7
  import { readCodexRecovery } from "./recovery.js";
8
8
  import { inspectCodexInstallation } from "./state.js";
9
- function preserveFailure(result) {
10
- if (result.outcome.ok) {
11
- throw new Error("cannot preserve a successful adapter result as failure");
12
- }
13
- return { status: result.status, outcome: result.outcome };
14
- }
15
- export function rejectSuccessfulNonzeroStatus(result, message) {
16
- if (!result.outcome.ok || result.status === 0)
17
- return result;
18
- return failureResult(result.outcome.operation, "invalid-status", message, [], result.outcome.messages);
19
- }
20
9
  function requirements(command, env) {
21
10
  if (command !== "probe" &&
22
11
  command !== "install" &&
@@ -38,7 +27,7 @@ async function mutationRoots(ctx) {
38
27
  return [codexHome(ctx.env ?? {}, process.cwd())];
39
28
  }
40
29
  async function inspectCodexUpdateControl(ctx) {
41
- const result = rejectSuccessfulNonzeroStatus(await codexInspectControl(ctx), "adapter reported a failure status for inspect --view update-control");
30
+ const result = await codexInspectControl(ctx);
42
31
  if (!result.outcome.ok)
43
32
  return result;
44
33
  const paths = codexPaths(ctx.env ?? {}, process.cwd());
@@ -74,19 +63,8 @@ export const codexHarness = {
74
63
  inspectOwnership: codexInspectOwnership,
75
64
  inspectUpdateControl: inspectCodexUpdateControl,
76
65
  inspectInstalled: inspectCodexInstallation,
77
- install: async (artifact, ctx) => installCodexMarketplace(artifact, ctx, async (root, context) => {
78
- const result = await codexInstall(root, context);
79
- return rejectSuccessfulNonzeroStatus(result, codexPresentation.callFailure("install", context).invalidStatus);
80
- }),
81
- remove: async (input, ctx) => {
82
- const result = await removeCodexMarketplace(input, ctx);
83
- if (!result.outcome.ok)
84
- return preserveFailure(result);
85
- if (result.status !== 0) {
86
- return rejectSuccessfulNonzeroStatus(result, codexPresentation.callFailure("remove", ctx, input).invalidStatus);
87
- }
88
- return successResult(result.outcome.operation, null, result.outcome.messages);
89
- },
66
+ install: (artifact, ctx) => installCodexMarketplace(artifact, ctx, codexInstall),
67
+ remove: removeCodexMarketplace,
90
68
  requirements,
91
69
  get presentation() {
92
70
  return codexPresentation;