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.
- package/README.md +36 -16
- package/dist/atomic.js +1 -3
- package/dist/cli-arguments.js +4 -1
- package/dist/cli.js +14 -43
- package/dist/commands/adapter-call.js +24 -0
- package/dist/commands/install.js +16 -63
- package/dist/commands/mutation.js +5 -5
- package/dist/commands/pin.js +10 -5
- package/dist/commands/prepare.js +42 -108
- package/dist/commands/probe.js +33 -19
- package/dist/commands/uninstall.js +18 -46
- package/dist/commands/unpin.js +2 -1
- package/dist/commands/update.js +8 -29
- package/dist/domain/refs.js +0 -3
- package/dist/effective-selection.js +11 -15
- package/dist/git-tree.js +4 -6
- package/dist/harness-command-result.js +33 -3
- package/dist/harnesses/claude-code/harness.js +39 -0
- package/dist/harnesses/claude-code/install.js +380 -0
- package/dist/harnesses/claude-code/native.js +79 -0
- package/dist/harnesses/claude-code/paths.js +35 -0
- package/dist/harnesses/claude-code/prepare.js +117 -0
- package/dist/harnesses/claude-code/presentation.js +25 -0
- package/dist/harnesses/claude-code/state.js +253 -0
- package/dist/harnesses/codex/adapter.js +36 -69
- 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 +28 -27
- package/dist/harnesses/codex/lifecycle.js +22 -107
- package/dist/harnesses/codex/manifest-overlay.js +9 -27
- package/dist/harnesses/codex/paths.js +29 -0
- package/dist/harnesses/codex/prepare.js +9 -43
- package/dist/harnesses/codex/presentation.js +1 -1
- package/dist/harnesses/codex/publication.js +5 -40
- package/dist/harnesses/codex/state.js +1 -6
- package/dist/harnesses/codex/stored-state.js +1 -6
- package/dist/harnesses/opencode/config.js +2 -4
- package/dist/harnesses/opencode/discovery.js +1 -5
- package/dist/harnesses/opencode/install.js +12 -93
- package/dist/harnesses/opencode/native.js +28 -49
- package/dist/harnesses/opencode/package.js +11 -55
- package/dist/harnesses/opencode/prepare.js +2 -7
- package/dist/harnesses/opencode/presentation.js +2 -7
- package/dist/harnesses/pi/install.js +31 -120
- package/dist/harnesses/pi/native.js +9 -34
- package/dist/harnesses/pi/package.js +9 -50
- package/dist/harnesses/pi/prepare.js +3 -8
- package/dist/harnesses/pi/presentation.js +2 -7
- package/dist/harnesses/pi/settings.js +5 -9
- package/dist/harnesses/pi/shared-skills.js +23 -53
- package/dist/harnesses/pi/state.js +3 -10
- package/dist/provenance.js +3 -19
- package/dist/resource-lock.js +11 -16
- package/dist/safe-path.js +28 -1
- package/dist/selection.js +15 -19
- package/dist/snapshot-journal.js +101 -0
- package/dist/snapshot-package.js +62 -1
- package/dist/snapshot-prepare.js +9 -2
- package/dist/snapshot-presentation.js +9 -0
- package/dist/upstream.js +8 -12
- package/dist/validator.js +26 -23
- package/dist/workspace.js +29 -35
- package/package.json +24 -13
- package/dist/python-json.js +0 -4
- 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,
|
|
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 {
|
|
@@ -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)
|
|
258
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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:
|
|
78
|
-
|
|
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;
|