superpowers-manager 0.7.3 → 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 +36 -5
- 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 +105 -50
- package/dist/harnesses/opencode/discovery.js +11 -15
- package/dist/harnesses/opencode/install.js +57 -102
- package/dist/harnesses/opencode/native.js +28 -49
- package/dist/harnesses/opencode/package.js +45 -67
- package/dist/harnesses/opencode/prepare.js +2 -7
- package/dist/harnesses/opencode/presentation.js +3 -7
- package/dist/harnesses/opencode/state.js +1 -0
- 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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { applyEdits, modify } from "jsonc-parser";
|
|
4
|
+
import { failureResult, successResult, } from "../../adapter-result.js";
|
|
5
|
+
import { readArtifactFile, readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
|
|
6
|
+
import { SEMVER_RE } from "../../domain/refs.js";
|
|
7
|
+
import { classifyPathNoFollow } from "../../safe-path.js";
|
|
8
|
+
import { SafetyError } from "../../safety-error.js";
|
|
9
|
+
import { validateSource } from "../../selection.js";
|
|
10
|
+
import { createSnapshotReceipts, } from "../../snapshot-package.js";
|
|
11
|
+
import { createSnapshotPreparation } from "../../snapshot-prepare.js";
|
|
12
|
+
import { manifestVersionForRef } from "../../upstream-version.js";
|
|
13
|
+
import { assertClaudeCodeStorageSafe, claudeCodePaths } from "./paths.js";
|
|
14
|
+
const CLAUDE_CODE_MANIFEST = ".claude-plugin/plugin.json";
|
|
15
|
+
const GENERATION = "claude-code-native-plugin-v1";
|
|
16
|
+
async function assessClaudeCodeCompatibility(root, selection) {
|
|
17
|
+
try {
|
|
18
|
+
validateSource(selection.effectiveSource);
|
|
19
|
+
const manifest = await readArtifactObject(root, join(root, CLAUDE_CODE_MANIFEST));
|
|
20
|
+
if (manifest.name !== "superpowers" ||
|
|
21
|
+
typeof manifest.version !== "string" ||
|
|
22
|
+
!SEMVER_RE.test(manifest.version))
|
|
23
|
+
throw new Error("manifest");
|
|
24
|
+
await validateNativeSkill(root);
|
|
25
|
+
const hooks = join(root, "hooks", "hooks.json");
|
|
26
|
+
if ((await classifyPathNoFollow(hooks)) !== "missing")
|
|
27
|
+
await readArtifactObject(root, hooks);
|
|
28
|
+
return {
|
|
29
|
+
kind: "supported",
|
|
30
|
+
generation: GENERATION,
|
|
31
|
+
reason: "native Claude Code plugin",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {
|
|
36
|
+
kind: "unsupported",
|
|
37
|
+
reason: `Claude Code package does not provide a native Claude Code plugin: ${root}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// The ref-aware manifest version for this selection. Two selections of the
|
|
42
|
+
// same commit (for example a tag and the raw SHA) yield different versions.
|
|
43
|
+
export function expectedClaudeCodeVersion(selection) {
|
|
44
|
+
return manifestVersionForRef({
|
|
45
|
+
requestedRef: selection.requestedRef,
|
|
46
|
+
resolutionKind: selection.resolutionKind,
|
|
47
|
+
resolvedRef: selection.resolvedRef,
|
|
48
|
+
commit: selection.desiredCommit,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// A targeted edit of the one value: every other upstream byte, including
|
|
52
|
+
// formatting and unknown fields, is preserved.
|
|
53
|
+
async function rewriteClaudeCodeVersion(root, selection) {
|
|
54
|
+
const version = expectedClaudeCodeVersion(selection);
|
|
55
|
+
const path = join(root, CLAUDE_CODE_MANIFEST);
|
|
56
|
+
await readArtifactObject(root, path);
|
|
57
|
+
const text = (await readArtifactFile(root, path)).toString("utf8");
|
|
58
|
+
await writeFile(path, applyEdits(text, modify(text, ["version"], version, {})));
|
|
59
|
+
}
|
|
60
|
+
const receipts = createSnapshotReceipts({
|
|
61
|
+
harness: "claude-code",
|
|
62
|
+
label: "Claude Code",
|
|
63
|
+
strictGeneration: true,
|
|
64
|
+
assessCompatibility: assessClaudeCodeCompatibility,
|
|
65
|
+
});
|
|
66
|
+
export const readClaudeCodeReceipt = receipts.readReceipt;
|
|
67
|
+
export const readClaudeCodePackageAssessment = receipts.readAssessment;
|
|
68
|
+
export async function readClaudeCodeManifestVersion(root) {
|
|
69
|
+
const manifest = await readArtifactObject(root, join(root, CLAUDE_CODE_MANIFEST));
|
|
70
|
+
if (typeof manifest.version !== "string")
|
|
71
|
+
throw new SafetyError("claude-code-package", `Claude Code manifest has no version: ${root}`);
|
|
72
|
+
return manifest.version;
|
|
73
|
+
}
|
|
74
|
+
export async function validateClaudeCodePreparationBeforeFetch(ctx) {
|
|
75
|
+
try {
|
|
76
|
+
await assertClaudeCodeStorageSafe(claudeCodePaths(ctx.env ?? {}, process.cwd()));
|
|
77
|
+
return successResult("prepare", null, []);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return failureResult("prepare", "invalid-package", "cannot validate Claude Code artifact storage", [], []);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const preparation = createSnapshotPreparation({
|
|
84
|
+
harness: "claude-code",
|
|
85
|
+
label: "Claude Code",
|
|
86
|
+
paths: claudeCodePaths,
|
|
87
|
+
assessCompatibility: assessClaudeCodeCompatibility,
|
|
88
|
+
readAssessment: readClaudeCodePackageAssessment,
|
|
89
|
+
rewrite: rewriteClaudeCodeVersion,
|
|
90
|
+
});
|
|
91
|
+
export const claudeCodePreparationLocation = preparation.preparationLocation;
|
|
92
|
+
export const prepareClaudeCodeCandidate = preparation.prepareCandidate;
|
|
93
|
+
// The shared check compares only commit and source, so a same-commit ref
|
|
94
|
+
// change would otherwise leave a prepared tree carrying the old version.
|
|
95
|
+
export async function inspectClaudeCodePrepared(selection, ctx) {
|
|
96
|
+
const result = await preparation.inspectPrepared(selection, ctx);
|
|
97
|
+
if (!result.outcome.ok || result.outcome.result.kind !== "current")
|
|
98
|
+
return result;
|
|
99
|
+
const root = result.outcome.result.artifact.root;
|
|
100
|
+
try {
|
|
101
|
+
if ((await readClaudeCodeManifestVersion(root)) ===
|
|
102
|
+
expectedClaudeCodeVersion(selection))
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return failureResult("inspect-prepared", "invalid-package", `cannot inspect Claude Code prepared artifact: ${root}`, [], []);
|
|
107
|
+
}
|
|
108
|
+
return successResult("inspect-prepared", {
|
|
109
|
+
kind: "needs-prepare",
|
|
110
|
+
observedIdentity: result.outcome.result.observedIdentity,
|
|
111
|
+
compatibility: {
|
|
112
|
+
kind: "unknown",
|
|
113
|
+
reason: "Claude Code prepared version does not match the selection",
|
|
114
|
+
},
|
|
115
|
+
}, []);
|
|
116
|
+
}
|
|
117
|
+
export const readClaudeCodePrepared = preparation.readPrepared;
|
|
@@ -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,
|
|
6
|
-
import { AdapterMessageLog, failureResult,
|
|
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:
|
|
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 (!
|
|
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
|
|
171
|
+
const manifestSource = (await isFile(upstreamManifest))
|
|
199
172
|
? "upstream"
|
|
200
173
|
: "fallback";
|
|
201
174
|
try {
|
|
@@ -254,8 +227,8 @@ async function runBuild(input, env, log) {
|
|
|
254
227
|
// applyManifestOverlay's own messages already name the manifest
|
|
255
228
|
// path — three are the frozen CPython wording, and the fourth (the
|
|
256
229
|
// numeric-overflow diagnostic, which has no CPython oracle wording
|
|
257
|
-
// to match)
|
|
258
|
-
//
|
|
230
|
+
// to match) carries the path from src/harnesses/codex/manifest-overlay.ts's
|
|
231
|
+
// emitter. Emit as-is, with no added prefix — a
|
|
259
232
|
// prefix here would double up the path these messages already
|
|
260
233
|
// name.
|
|
261
234
|
log.appendText("stderr", oneLine(cause));
|
|
@@ -309,12 +282,6 @@ async function runBuild(input, env, log) {
|
|
|
309
282
|
return {};
|
|
310
283
|
}
|
|
311
284
|
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
285
|
const codexBin = env.SUPERPOWERS_CODEX || "codex";
|
|
319
286
|
const refreshMode = codexInstallRefreshMode(env);
|
|
320
287
|
await requireCodex(codexBin, env);
|
|
@@ -392,11 +359,6 @@ async function runUninstall(input, env, log) {
|
|
|
392
359
|
return {};
|
|
393
360
|
}
|
|
394
361
|
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
362
|
const managerPresent = pluginPresent || marketplacePresent;
|
|
401
363
|
const identityState = managerPresent
|
|
402
364
|
? legacyPresent
|
|
@@ -407,14 +369,27 @@ function ownershipFromResources(pluginPresent, marketplacePresent, legacyPresent
|
|
|
407
369
|
: "neither";
|
|
408
370
|
return codexOwnershipInspection(identityState, { pluginPresent, marketplacePresent }, conflicts);
|
|
409
371
|
}
|
|
372
|
+
async function ownershipAfterListingFailure(context, env, message) {
|
|
373
|
+
const stored = await storedStateAfterListingFailure(env, "inspect-failed", message);
|
|
374
|
+
const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
|
|
375
|
+
return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
|
|
376
|
+
}
|
|
377
|
+
async function nativeAfterListingFailure(env, message) {
|
|
378
|
+
const stored = await storedStateAfterListingFailure(env, "inspect-failed", message);
|
|
379
|
+
return {
|
|
380
|
+
marketplaceRoot: stored.managerMarketplaceRoot,
|
|
381
|
+
pluginPresent: stored.managerPluginPresent,
|
|
382
|
+
pluginEnabled: stored.managerPluginEnabled,
|
|
383
|
+
activeVersion: null,
|
|
384
|
+
activeRoot: null,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
410
387
|
async function runOwnership(context, env, log) {
|
|
411
388
|
const codexBin = env.SUPERPOWERS_CODEX || "codex";
|
|
412
389
|
await requireCodex(codexBin, env);
|
|
413
390
|
const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
|
|
414
391
|
if (commandFailed(plugins)) {
|
|
415
|
-
|
|
416
|
-
const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
|
|
417
|
-
return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
|
|
392
|
+
return await ownershipAfterListingFailure(context, env, `cannot list Codex plugins via '${codexBin} plugin list --json'`);
|
|
418
393
|
}
|
|
419
394
|
let managerPlugin;
|
|
420
395
|
let legacyPlugin;
|
|
@@ -430,9 +405,7 @@ async function runOwnership(context, env, log) {
|
|
|
430
405
|
}
|
|
431
406
|
const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
|
|
432
407
|
if (commandFailed(marketplaces)) {
|
|
433
|
-
|
|
434
|
-
const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
|
|
435
|
-
return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
|
|
408
|
+
return await ownershipAfterListingFailure(context, env, `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
|
|
436
409
|
}
|
|
437
410
|
let managerMarketplace;
|
|
438
411
|
let legacyMarketplace;
|
|
@@ -484,14 +457,7 @@ export function codexReadNativeState(context) {
|
|
|
484
457
|
await requireCodex(codexBin, env);
|
|
485
458
|
const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
|
|
486
459
|
if (commandFailed(plugins)) {
|
|
487
|
-
|
|
488
|
-
return {
|
|
489
|
-
marketplaceRoot: stored.managerMarketplaceRoot,
|
|
490
|
-
pluginPresent: stored.managerPluginPresent,
|
|
491
|
-
pluginEnabled: stored.managerPluginEnabled,
|
|
492
|
-
activeVersion: null,
|
|
493
|
-
activeRoot: null,
|
|
494
|
-
};
|
|
460
|
+
return await nativeAfterListingFailure(env, `cannot list Codex plugins via '${codexBin} plugin list --json'`);
|
|
495
461
|
}
|
|
496
462
|
let manager;
|
|
497
463
|
let activeVersion;
|
|
@@ -512,14 +478,7 @@ export function codexReadNativeState(context) {
|
|
|
512
478
|
}
|
|
513
479
|
const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
|
|
514
480
|
if (commandFailed(marketplaces)) {
|
|
515
|
-
|
|
516
|
-
return {
|
|
517
|
-
marketplaceRoot: stored.managerMarketplaceRoot,
|
|
518
|
-
pluginPresent: stored.managerPluginPresent,
|
|
519
|
-
pluginEnabled: stored.managerPluginEnabled,
|
|
520
|
-
activeVersion: null,
|
|
521
|
-
activeRoot: null,
|
|
522
|
-
};
|
|
481
|
+
return await nativeAfterListingFailure(env, `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
|
|
523
482
|
}
|
|
524
483
|
let marketplaceRoot;
|
|
525
484
|
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
|
|
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
|
|
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:
|
|
451
|
-
// The three probes sharing the *subtree* string are `src/harnesses/codex/generated-plugin.ts:
|
|
452
|
-
// `src/harnesses/codex/generated-plugin.ts:
|
|
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
|
}
|