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,39 @@
|
|
|
1
|
+
import { displayPath } from "../../validator.js";
|
|
2
|
+
import { installClaudeCode, removeClaudeCode } from "./install.js";
|
|
3
|
+
import { claudeCodePaths } from "./paths.js";
|
|
4
|
+
import { claudeCodePreparationLocation, inspectClaudeCodePrepared, prepareClaudeCodeCandidate, readClaudeCodePrepared, validateClaudeCodePreparationBeforeFetch, } from "./prepare.js";
|
|
5
|
+
import { claudeCodePresentation } from "./presentation.js";
|
|
6
|
+
import { inspectClaudeCodeControl, inspectClaudeCodeInstalled, inspectClaudeCodeOwnership, } from "./state.js";
|
|
7
|
+
function requirements(command, env) {
|
|
8
|
+
if (command !== "probe" &&
|
|
9
|
+
command !== "install" &&
|
|
10
|
+
command !== "update" &&
|
|
11
|
+
command !== "uninstall")
|
|
12
|
+
return [];
|
|
13
|
+
const executable = env.SUPERPOWERS_CLAUDE_CODE || "claude";
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
name: "claude",
|
|
17
|
+
executable,
|
|
18
|
+
lookup: "explicit-path-or-path",
|
|
19
|
+
missingMessage: `required command not found: ${displayPath(executable)} — install Claude Code or set SUPERPOWERS_CLAUDE_CODE`,
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
export const claudeCodeHarness = {
|
|
24
|
+
preparationLocation: claudeCodePreparationLocation,
|
|
25
|
+
mutationRoots: async (ctx) => [
|
|
26
|
+
claudeCodePaths(ctx.env ?? {}, process.cwd()).configRoot,
|
|
27
|
+
],
|
|
28
|
+
validatePreparationBeforeFetch: validateClaudeCodePreparationBeforeFetch,
|
|
29
|
+
prepareCandidate: prepareClaudeCodeCandidate,
|
|
30
|
+
inspectPrepared: inspectClaudeCodePrepared,
|
|
31
|
+
readPrepared: readClaudeCodePrepared,
|
|
32
|
+
inspectOwnership: inspectClaudeCodeOwnership,
|
|
33
|
+
inspectUpdateControl: inspectClaudeCodeControl,
|
|
34
|
+
inspectInstalled: inspectClaudeCodeInstalled,
|
|
35
|
+
install: installClaudeCode,
|
|
36
|
+
remove: removeClaudeCode,
|
|
37
|
+
requirements,
|
|
38
|
+
presentation: claudeCodePresentation,
|
|
39
|
+
};
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { cp, mkdir, mkdtemp, rm, rmdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { readArtifactFile } from "../../artifact-tree.js";
|
|
4
|
+
import { failureResult, successResult, } from "../../adapter-result.js";
|
|
5
|
+
import { atomicWriteFile, beginDirectoryPublication, } from "../../atomic.js";
|
|
6
|
+
import { assertNoFollowType, classifyPathNoFollow, isErrno, } from "../../safe-path.js";
|
|
7
|
+
import { displayPath } from "../../validator.js";
|
|
8
|
+
import { readClaudeCodeState, runClaude, } from "./native.js";
|
|
9
|
+
import { assertClaudeCodeStorageSafe, claudeCodePaths, } from "./paths.js";
|
|
10
|
+
import { readClaudeCodeManifestVersion, readClaudeCodePackageAssessment, } from "./prepare.js";
|
|
11
|
+
import { CLAUDE_CODE_MARKETPLACE, CLAUDE_CODE_MARKETPLACE_BYTES, CLAUDE_CODE_PLUGIN_ID, inspectClaudeCodeMarketplaceStorage, leftoverPublicationMaterial, managerPlugin, marketplaceRegistration, } from "./state.js";
|
|
12
|
+
const DEFAULTS = {
|
|
13
|
+
run: runClaude,
|
|
14
|
+
beginPublication: beginDirectoryPublication,
|
|
15
|
+
};
|
|
16
|
+
// Carries only hand-written text; anything else becomes a generic message.
|
|
17
|
+
class ClaudeCodeStep extends Error {
|
|
18
|
+
}
|
|
19
|
+
async function native(run, args, ctx) {
|
|
20
|
+
const result = await run(args, ctx);
|
|
21
|
+
if (!result.outcome.ok || result.status !== 0) {
|
|
22
|
+
const reported = result.outcome.ok
|
|
23
|
+
? undefined
|
|
24
|
+
: /^Claude Code command exited with status ([0-9]{1,3})$/.exec(result.outcome.error.message)?.[1];
|
|
25
|
+
const exitStatus = reported !== undefined && Number(reported) <= 255
|
|
26
|
+
? ` (exit status ${reported})`
|
|
27
|
+
: "";
|
|
28
|
+
throw new ClaudeCodeStep(`claude ${displayPath(args.join(" "))} did not complete${exitStatus}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const MANIFEST_LIMIT = 64 * 1024;
|
|
32
|
+
async function readMarketplaceManifest(paths) {
|
|
33
|
+
const kind = await classifyPathNoFollow(paths.marketplaceManifest);
|
|
34
|
+
if (kind === "missing")
|
|
35
|
+
return null;
|
|
36
|
+
if (kind !== "regular-file")
|
|
37
|
+
throw new Error("unverified Claude Code marketplace manifest");
|
|
38
|
+
return await readArtifactFile(paths.marketplaceRoot, paths.marketplaceManifest, MANIFEST_LIMIT);
|
|
39
|
+
}
|
|
40
|
+
async function createDirectoryIfMissing(path) {
|
|
41
|
+
if ((await classifyPathNoFollow(path)) !== "missing")
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
await mkdir(path);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
if (isErrno(cause, "EEXIST"))
|
|
49
|
+
return false;
|
|
50
|
+
throw cause;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function writeMarketplaceManifest(paths) {
|
|
54
|
+
const path = paths.marketplaceManifest;
|
|
55
|
+
if ((await readMarketplaceManifest(paths))?.equals(CLAUDE_CODE_MARKETPLACE_BYTES))
|
|
56
|
+
return;
|
|
57
|
+
await atomicWriteFile(path, CLAUDE_CODE_MARKETPLACE_BYTES, {
|
|
58
|
+
validate: async () => {
|
|
59
|
+
await assertNoFollowType(path, ["regular-file", "missing"]);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function removeEmptyDirectory(path) {
|
|
64
|
+
try {
|
|
65
|
+
await rmdir(path);
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
if (!isErrno(cause, "ENOENT"))
|
|
69
|
+
throw cause;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Restore only bytes and directories this invocation changed. An unexpected
|
|
73
|
+
// replacement or extra entry remains in place and makes rollback fail closed.
|
|
74
|
+
async function restoreMarketplace(paths, before) {
|
|
75
|
+
const current = await readMarketplaceManifest(paths);
|
|
76
|
+
if (before.manifest === null) {
|
|
77
|
+
if (current !== null) {
|
|
78
|
+
if (!current.equals(CLAUDE_CODE_MARKETPLACE_BYTES))
|
|
79
|
+
throw new Error("Claude Code marketplace manifest changed");
|
|
80
|
+
await unlink(paths.marketplaceManifest);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (current === null) {
|
|
84
|
+
throw new Error("Claude Code marketplace manifest disappeared");
|
|
85
|
+
}
|
|
86
|
+
else if (!current.equals(before.manifest)) {
|
|
87
|
+
if (!current.equals(CLAUDE_CODE_MARKETPLACE_BYTES))
|
|
88
|
+
throw new Error("Claude Code marketplace manifest changed");
|
|
89
|
+
await atomicWriteFile(paths.marketplaceManifest, before.manifest, {
|
|
90
|
+
validate: async () => {
|
|
91
|
+
await assertNoFollowType(paths.marketplaceManifest, ["regular-file"]);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (before.createdPlugins)
|
|
96
|
+
await removeEmptyDirectory(paths.pluginsRoot);
|
|
97
|
+
if (before.createdMetadata)
|
|
98
|
+
await removeEmptyDirectory(join(paths.marketplaceRoot, ".claude-plugin"));
|
|
99
|
+
if (before.createdRoot)
|
|
100
|
+
await removeEmptyDirectory(paths.marketplaceRoot);
|
|
101
|
+
}
|
|
102
|
+
async function removeVerifiedMarketplace(paths) {
|
|
103
|
+
const storage = await inspectClaudeCodeMarketplaceStorage(paths);
|
|
104
|
+
if (storage === "absent")
|
|
105
|
+
return;
|
|
106
|
+
if (storage !== "owned")
|
|
107
|
+
throw new Error("unverified Claude Code marketplace storage");
|
|
108
|
+
if ((await classifyPathNoFollow(paths.pluginRoot)) === "directory")
|
|
109
|
+
await rm(paths.pluginRoot, { recursive: true, force: false });
|
|
110
|
+
if ((await classifyPathNoFollow(paths.marketplaceManifest)) === "regular-file")
|
|
111
|
+
await unlink(paths.marketplaceManifest);
|
|
112
|
+
for (const path of [
|
|
113
|
+
paths.pluginsRoot,
|
|
114
|
+
join(paths.marketplaceRoot, ".claude-plugin"),
|
|
115
|
+
paths.marketplaceRoot,
|
|
116
|
+
])
|
|
117
|
+
await removeEmptyDirectory(path);
|
|
118
|
+
}
|
|
119
|
+
// Undo native steps relative to what step 1 observed, after the snapshot has
|
|
120
|
+
// already been restored so a rerun `plugin update` records the restored version.
|
|
121
|
+
async function restoreNative(start, refreshed, paths, run, ctx) {
|
|
122
|
+
const now = await readClaudeCodeState(ctx, run);
|
|
123
|
+
const scope = ["--scope", "user"];
|
|
124
|
+
if (start.registration === "absent") {
|
|
125
|
+
if ((await marketplaceRegistration(now, paths)) === "owned")
|
|
126
|
+
await native(run, ["plugin", "marketplace", "remove", CLAUDE_CODE_MARKETPLACE], ctx);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const plugin = managerPlugin(now);
|
|
130
|
+
if (start.plugin === undefined) {
|
|
131
|
+
if (plugin !== undefined)
|
|
132
|
+
await native(run, ["plugin", "uninstall", CLAUDE_CODE_PLUGIN_ID, ...scope], ctx);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!start.plugin.enabled && plugin?.enabled === true)
|
|
136
|
+
await native(run, ["plugin", "disable", CLAUDE_CODE_PLUGIN_ID, ...scope], ctx);
|
|
137
|
+
if (refreshed)
|
|
138
|
+
await native(run, ["plugin", "update", CLAUDE_CODE_PLUGIN_ID, ...scope], ctx);
|
|
139
|
+
}
|
|
140
|
+
async function settle(operation, failure, action) {
|
|
141
|
+
try {
|
|
142
|
+
await action();
|
|
143
|
+
return successResult(operation, null, []);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return failureResult(operation, "settlement-failed", failure, [], []);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export async function installClaudeCode(artifact, ctx, deps = DEFAULTS) {
|
|
150
|
+
const operation = "install-claude-code";
|
|
151
|
+
const paths = claudeCodePaths(ctx.env ?? {}, process.cwd());
|
|
152
|
+
const scope = ["--scope", "user"];
|
|
153
|
+
let start;
|
|
154
|
+
let stageContainer;
|
|
155
|
+
let publication;
|
|
156
|
+
let publicationAttempted = false;
|
|
157
|
+
let refreshed = false;
|
|
158
|
+
let marketplaceBefore;
|
|
159
|
+
try {
|
|
160
|
+
await assertClaudeCodeStorageSafe(paths);
|
|
161
|
+
if (artifact.root !== paths.preparedRoot)
|
|
162
|
+
throw new ClaudeCodeStep("unexpected Claude Code prepared root");
|
|
163
|
+
const assessment = await readClaudeCodePackageAssessment(artifact.root);
|
|
164
|
+
if (assessment.receipt.digest !== artifact.identity ||
|
|
165
|
+
assessment.receipt.commit !== artifact.commit ||
|
|
166
|
+
assessment.compatibility.kind !== "supported")
|
|
167
|
+
throw new ClaudeCodeStep("the Claude Code prepared artifact changed before activation");
|
|
168
|
+
const leftovers = await leftoverPublicationMaterial(paths);
|
|
169
|
+
if (leftovers.length > 0)
|
|
170
|
+
throw new ClaudeCodeStep(`Claude Code recovery required; inspect leftover publication material at ${displayPath(leftovers[0])}`);
|
|
171
|
+
const before = await readClaudeCodeState(ctx, deps.run);
|
|
172
|
+
const observed = {
|
|
173
|
+
registration: await marketplaceRegistration(before, paths),
|
|
174
|
+
plugin: managerPlugin(before),
|
|
175
|
+
};
|
|
176
|
+
if (observed.registration === "foreign")
|
|
177
|
+
throw new ClaudeCodeStep(`a foreign ${CLAUDE_CODE_MARKETPLACE} Claude Code marketplace is registered`);
|
|
178
|
+
if (observed.registration === "absent") {
|
|
179
|
+
const storage = await inspectClaudeCodeMarketplaceStorage(paths);
|
|
180
|
+
if (storage === "recovery")
|
|
181
|
+
throw new ClaudeCodeStep(`Claude Code recovery required; inspect leftover publication material in ${displayPath(paths.pluginsRoot)}`);
|
|
182
|
+
if (storage === "unverified")
|
|
183
|
+
throw new ClaudeCodeStep(`unverified Claude Code marketplace storage at ${displayPath(paths.marketplaceRoot)}`);
|
|
184
|
+
}
|
|
185
|
+
const conflicting = before.plugins.find((plugin) => plugin.enabled &&
|
|
186
|
+
plugin.id.startsWith("superpowers@") &&
|
|
187
|
+
plugin.id !== CLAUDE_CODE_PLUGIN_ID &&
|
|
188
|
+
plugin.id !== "superpowers@synced");
|
|
189
|
+
if (conflicting !== undefined) {
|
|
190
|
+
const action = /^superpowers@[A-Za-z0-9][A-Za-z0-9._-]{0,200}$/.test(conflicting.id)
|
|
191
|
+
? `; run claude plugin disable ${conflicting.id} before retrying`
|
|
192
|
+
: "; inspect and disable the competing plugin before retrying";
|
|
193
|
+
throw new ClaudeCodeStep(`another Superpowers Claude Code plugin is enabled${action}`);
|
|
194
|
+
}
|
|
195
|
+
start = observed;
|
|
196
|
+
const storageBefore = {
|
|
197
|
+
manifest: await readMarketplaceManifest(paths),
|
|
198
|
+
createdRoot: false,
|
|
199
|
+
createdMetadata: false,
|
|
200
|
+
createdPlugins: false,
|
|
201
|
+
};
|
|
202
|
+
marketplaceBefore = storageBefore;
|
|
203
|
+
storageBefore.createdRoot = await createDirectoryIfMissing(paths.marketplaceRoot);
|
|
204
|
+
storageBefore.createdMetadata = await createDirectoryIfMissing(join(paths.marketplaceRoot, ".claude-plugin"));
|
|
205
|
+
storageBefore.createdPlugins = await createDirectoryIfMissing(paths.pluginsRoot);
|
|
206
|
+
await assertClaudeCodeStorageSafe(paths);
|
|
207
|
+
await writeMarketplaceManifest(paths);
|
|
208
|
+
stageContainer = await mkdtemp(join(paths.pluginsRoot, `.superpowers.stage.${process.pid}.`));
|
|
209
|
+
const stage = join(stageContainer, "superpowers");
|
|
210
|
+
await cp(artifact.root, stage, {
|
|
211
|
+
recursive: true,
|
|
212
|
+
verbatimSymlinks: true,
|
|
213
|
+
force: false,
|
|
214
|
+
errorOnExist: true,
|
|
215
|
+
});
|
|
216
|
+
if ((await readClaudeCodePackageAssessment(stage)).receipt.digest !==
|
|
217
|
+
assessment.receipt.digest)
|
|
218
|
+
throw new ClaudeCodeStep("the staged Claude Code artifact changed");
|
|
219
|
+
publicationAttempted = true;
|
|
220
|
+
publication = await deps.beginPublication(stage, paths.pluginRoot);
|
|
221
|
+
await rm(stageContainer, { recursive: true, force: true });
|
|
222
|
+
stageContainer = undefined;
|
|
223
|
+
if (start.registration === "absent")
|
|
224
|
+
await native(deps.run, ["plugin", "marketplace", "add", paths.marketplaceRoot], ctx);
|
|
225
|
+
if (start.plugin === undefined) {
|
|
226
|
+
await native(deps.run, ["plugin", "install", CLAUDE_CODE_PLUGIN_ID, ...scope], ctx);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
if (!start.plugin.enabled)
|
|
230
|
+
await native(deps.run, ["plugin", "enable", CLAUDE_CODE_PLUGIN_ID, ...scope], ctx);
|
|
231
|
+
// `plugin list` reports the version recorded at install, so a swapped
|
|
232
|
+
// snapshot leaves it stale until `plugin update` recomputes it.
|
|
233
|
+
if (start.plugin.version !==
|
|
234
|
+
(await readClaudeCodeManifestVersion(paths.pluginRoot))) {
|
|
235
|
+
refreshed = true;
|
|
236
|
+
await native(deps.run, ["plugin", "update", CLAUDE_CODE_PLUGIN_ID, ...scope], ctx);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const published = publication;
|
|
240
|
+
const begun = start;
|
|
241
|
+
const wasRefreshed = refreshed;
|
|
242
|
+
const previousMarketplace = marketplaceBefore;
|
|
243
|
+
const backup = displayPath(published.backup ?? paths.pluginsRoot);
|
|
244
|
+
return successResult(operation, {
|
|
245
|
+
missingVerificationOutput: {
|
|
246
|
+
stdout: [],
|
|
247
|
+
stderr: [
|
|
248
|
+
"error: the Manager Claude Code plugin is not listed after activation",
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
mismatchVerificationOutput: {
|
|
252
|
+
stdout: [],
|
|
253
|
+
stderr: [
|
|
254
|
+
"error: the installed Claude Code plugin does not match the prepared artifact; run `claude plugin list` to inspect load errors",
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
transaction: {
|
|
258
|
+
finalize: () => settle(operation, `Claude Code activation succeeded but backup cleanup failed at ${backup}`, () => published.finalize()),
|
|
259
|
+
rollback: () => settle(operation, `Claude Code rollback did not complete; the previous snapshot may not be restored — inspect ${backup}`, async () => {
|
|
260
|
+
await published.rollback();
|
|
261
|
+
await restoreNative(begun, wasRefreshed, paths, deps.run, ctx);
|
|
262
|
+
await restoreMarketplace(paths, previousMarketplace);
|
|
263
|
+
}),
|
|
264
|
+
},
|
|
265
|
+
}, []);
|
|
266
|
+
}
|
|
267
|
+
catch (cause) {
|
|
268
|
+
const message = cause instanceof ClaudeCodeStep
|
|
269
|
+
? cause.message
|
|
270
|
+
: "cannot activate the Manager-owned Claude Code installation";
|
|
271
|
+
let restored = !publicationAttempted || publication !== undefined;
|
|
272
|
+
if (stageContainer !== undefined) {
|
|
273
|
+
try {
|
|
274
|
+
await rm(stageContainer, { recursive: true, force: true });
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
restored = false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
let snapshotRestored = !publicationAttempted || publication !== undefined;
|
|
281
|
+
if (publication !== undefined) {
|
|
282
|
+
try {
|
|
283
|
+
await publication.rollback();
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
restored = false;
|
|
287
|
+
snapshotRestored = false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (snapshotRestored && publication !== undefined && start !== undefined) {
|
|
291
|
+
try {
|
|
292
|
+
await restoreNative(start, refreshed, paths, deps.run, ctx);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
restored = false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (restored && marketplaceBefore !== undefined) {
|
|
299
|
+
try {
|
|
300
|
+
await restoreMarketplace(paths, marketplaceBefore);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
restored = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
let recoveryPath = marketplaceBefore?.createdRoot
|
|
307
|
+
? paths.marketplaceRoot
|
|
308
|
+
: paths.pluginsRoot;
|
|
309
|
+
if (!restored) {
|
|
310
|
+
try {
|
|
311
|
+
const leftovers = await leftoverPublicationMaterial(paths);
|
|
312
|
+
recoveryPath =
|
|
313
|
+
leftovers.find((path) => basename(path).startsWith(".superpowers.bak.")) ??
|
|
314
|
+
leftovers[0] ??
|
|
315
|
+
recoveryPath;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// The parent path still gives the operator a bounded inspection target.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return failureResult(operation, "install-failed", restored
|
|
322
|
+
? message
|
|
323
|
+
: `${message}; the previous Claude Code state may not be restored — inspect ${displayPath(recoveryPath)}`, [], []);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export async function removeClaudeCode(input, ctx, deps = DEFAULTS) {
|
|
327
|
+
const operation = "remove-claude-code";
|
|
328
|
+
const paths = claudeCodePaths(ctx.env ?? {}, process.cwd());
|
|
329
|
+
if (input.registration === "foreign")
|
|
330
|
+
return failureResult(operation, "foreign-marketplace", `refusing to remove a foreign ${CLAUDE_CODE_MARKETPLACE} Claude Code marketplace; remove it manually`, [], []);
|
|
331
|
+
try {
|
|
332
|
+
await assertClaudeCodeStorageSafe(paths);
|
|
333
|
+
// Shared uninstall never consults update control, and deleting the
|
|
334
|
+
// marketplace would destroy retained recovery material.
|
|
335
|
+
if ((await leftoverPublicationMaterial(paths)).length > 0)
|
|
336
|
+
return failureResult(operation, "recovery-required", `Claude Code recovery required; inspect and remove leftover publication material in ${displayPath(paths.pluginsRoot)} before uninstalling`, [], []);
|
|
337
|
+
const current = await readClaudeCodeState(ctx, deps.run);
|
|
338
|
+
const registration = await marketplaceRegistration(current, paths);
|
|
339
|
+
if (registration === "foreign")
|
|
340
|
+
return failureResult(operation, "foreign-marketplace", `refusing to remove a foreign ${CLAUDE_CODE_MARKETPLACE} Claude Code marketplace; remove it manually`, [], []);
|
|
341
|
+
const pluginInstalled = managerPlugin(current) !== undefined;
|
|
342
|
+
const published = (await classifyPathNoFollow(paths.marketplaceRoot)) !== "missing";
|
|
343
|
+
if (registration !== input.registration ||
|
|
344
|
+
pluginInstalled !== input.pluginInstalled ||
|
|
345
|
+
published !== input.published)
|
|
346
|
+
return failureResult(operation, "stale-ownership", "Claude Code marketplace or plugin state changed before removal; inspect it and retry", [], []);
|
|
347
|
+
if (published) {
|
|
348
|
+
const storage = await inspectClaudeCodeMarketplaceStorage(paths);
|
|
349
|
+
if (storage !== "owned")
|
|
350
|
+
return failureResult(operation, storage === "recovery"
|
|
351
|
+
? "recovery-required"
|
|
352
|
+
: "unverified-marketplace", storage === "recovery"
|
|
353
|
+
? `Claude Code recovery required; inspect and remove leftover publication material in ${displayPath(paths.pluginsRoot)} before uninstalling`
|
|
354
|
+
: `refusing to remove unverified Claude Code marketplace storage at ${displayPath(paths.marketplaceRoot)}`, [], []);
|
|
355
|
+
}
|
|
356
|
+
if (input.registration === "owned")
|
|
357
|
+
await native(deps.run, ["plugin", "marketplace", "remove", CLAUDE_CODE_MARKETPLACE], ctx);
|
|
358
|
+
else if (input.pluginInstalled)
|
|
359
|
+
await native(deps.run, ["plugin", "uninstall", CLAUDE_CODE_PLUGIN_ID, "--scope", "user"], ctx);
|
|
360
|
+
if (input.registration === "owned" || input.pluginInstalled) {
|
|
361
|
+
const after = await readClaudeCodeState(ctx, deps.run);
|
|
362
|
+
if ((await marketplaceRegistration(after, paths)) !== "absent" ||
|
|
363
|
+
managerPlugin(after) !== undefined)
|
|
364
|
+
return failureResult(operation, "removal-unverified", "Claude Code still lists the Manager marketplace or plugin after removal", [], []);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (cause) {
|
|
368
|
+
return failureResult(operation, "remove-failed", cause instanceof ClaudeCodeStep
|
|
369
|
+
? cause.message
|
|
370
|
+
: "cannot remove the Manager-owned Claude Code registration", [], []);
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
if (input.published)
|
|
374
|
+
await removeVerifiedMarketplace(paths);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return failureResult(operation, "cleanup-pending", `removed the Claude Code registration; cleanup pending at ${displayPath(paths.marketplaceRoot)}`, [], []);
|
|
378
|
+
}
|
|
379
|
+
return successResult(operation, null, []);
|
|
380
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {} from "../../adapter-result.js";
|
|
2
|
+
import { runIsolatedNative, } from "../../harness-command-result.js";
|
|
3
|
+
import { SafetyError } from "../../safety-error.js";
|
|
4
|
+
import { parseStrictJson } from "../../strict-json.js";
|
|
5
|
+
import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "../../validator.js";
|
|
6
|
+
// Native list commands include the caller's project and local scope.
|
|
7
|
+
export async function runClaude(args, ctx, execute = runBoundedCommand) {
|
|
8
|
+
return await runIsolatedNative("Claude Code", (ctx.env ?? {}).SUPERPOWERS_CLAUDE_CODE || "claude", ctx, async (executable, workspace, env, invocationCwd) => await execute([executable, ...args], BOUNDED_EXECUTABLE, { ...env, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" }, workspace, invocationCwd));
|
|
9
|
+
}
|
|
10
|
+
function listEntries(stdout, subject) {
|
|
11
|
+
let value;
|
|
12
|
+
try {
|
|
13
|
+
value = parseStrictJson(stdout.trim(), {
|
|
14
|
+
duplicateKeys: "reject",
|
|
15
|
+
nonStandardConstants: "reject",
|
|
16
|
+
maxDepth: 64,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (cause) {
|
|
20
|
+
throw new SafetyError("claude-code-native", `invalid ${subject}`, {
|
|
21
|
+
cause,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (!Array.isArray(value))
|
|
25
|
+
throw new SafetyError("claude-code-native", `invalid ${subject}`);
|
|
26
|
+
return value.map((entry) => {
|
|
27
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry))
|
|
28
|
+
throw new SafetyError("claude-code-native", `invalid ${subject}`);
|
|
29
|
+
return entry;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function parsePluginList(stdout) {
|
|
33
|
+
return listEntries(stdout, "Claude Code plugin list").map((entry) => {
|
|
34
|
+
const { id, version, scope, enabled, installPath, errors } = entry;
|
|
35
|
+
if (typeof id !== "string" ||
|
|
36
|
+
typeof version !== "string" ||
|
|
37
|
+
typeof scope !== "string" ||
|
|
38
|
+
typeof enabled !== "boolean" ||
|
|
39
|
+
typeof installPath !== "string" ||
|
|
40
|
+
(errors !== undefined && !Array.isArray(errors)))
|
|
41
|
+
throw new SafetyError("claude-code-native", "invalid Claude Code plugin list");
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
version,
|
|
45
|
+
scope,
|
|
46
|
+
enabled,
|
|
47
|
+
installPath,
|
|
48
|
+
errorCount: errors === undefined ? 0 : errors.length,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function parseMarketplaceList(stdout) {
|
|
53
|
+
return listEntries(stdout, "Claude Code marketplace list").map((entry) => {
|
|
54
|
+
const { name, source, path } = entry;
|
|
55
|
+
if (typeof name !== "string" ||
|
|
56
|
+
typeof source !== "string" ||
|
|
57
|
+
(path !== undefined && typeof path !== "string"))
|
|
58
|
+
throw new SafetyError("claude-code-native", "invalid Claude Code marketplace list");
|
|
59
|
+
return { name, source, path: path ?? null };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export async function readClaudeCodeState(ctx, run = runClaude) {
|
|
63
|
+
try {
|
|
64
|
+
const plugins = await run(["plugin", "list", "--json"], ctx);
|
|
65
|
+
const marketplaces = await run(["plugin", "marketplace", "list", "--json"], ctx);
|
|
66
|
+
if (!plugins.outcome.ok ||
|
|
67
|
+
plugins.status !== 0 ||
|
|
68
|
+
!marketplaces.outcome.ok ||
|
|
69
|
+
marketplaces.status !== 0)
|
|
70
|
+
throw new Error("native command");
|
|
71
|
+
return {
|
|
72
|
+
plugins: parsePluginList(plugins.outcome.result.stdout),
|
|
73
|
+
marketplaces: parseMarketplaceList(marketplaces.outcome.result.stdout),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
throw new SafetyError("claude-code-native", "cannot inspect Claude Code plugin state", { cause });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { assertNoFollowType } from "../../safe-path.js";
|
|
4
|
+
export function claudeCodePaths(env, cwd) {
|
|
5
|
+
const configured = env.CLAUDE_CONFIG_DIR;
|
|
6
|
+
const home = env.HOME && env.HOME.length > 0 ? env.HOME : homedir();
|
|
7
|
+
const configRoot = configured !== undefined && configured.length > 0
|
|
8
|
+
? resolve(cwd, configured)
|
|
9
|
+
: join(resolve(cwd, home), ".claude");
|
|
10
|
+
const managerRoot = join(configRoot, "superpowers-manager");
|
|
11
|
+
const marketplaceRoot = join(managerRoot, "marketplace");
|
|
12
|
+
const pluginsRoot = join(marketplaceRoot, "plugins");
|
|
13
|
+
return {
|
|
14
|
+
configRoot,
|
|
15
|
+
managerRoot,
|
|
16
|
+
preparedRoot: join(managerRoot, "prepared"),
|
|
17
|
+
marketplaceRoot,
|
|
18
|
+
marketplaceManifest: join(marketplaceRoot, ".claude-plugin", "marketplace.json"),
|
|
19
|
+
pluginsRoot,
|
|
20
|
+
pluginRoot: join(pluginsRoot, "superpowers"),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Each owned directory is a real directory or absent; a symlink anywhere in
|
|
24
|
+
// the chain could redirect publication outside the config root.
|
|
25
|
+
export async function assertClaudeCodeStorageSafe(paths) {
|
|
26
|
+
for (const path of [
|
|
27
|
+
paths.configRoot,
|
|
28
|
+
paths.managerRoot,
|
|
29
|
+
paths.preparedRoot,
|
|
30
|
+
paths.marketplaceRoot,
|
|
31
|
+
join(paths.marketplaceRoot, ".claude-plugin"),
|
|
32
|
+
paths.pluginsRoot,
|
|
33
|
+
])
|
|
34
|
+
await assertNoFollowType(path, ["directory", "missing"]);
|
|
35
|
+
}
|
|
@@ -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;
|