superpowers-manager 0.3.0 → 0.5.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 +63 -325
- package/dist/adapter.js +91 -46
- package/dist/artifact-tree.js +110 -0
- package/dist/atomic.js +103 -7
- package/dist/cli-arguments.js +37 -0
- package/dist/cli.js +97 -40
- package/dist/codex-compatibility.js +126 -0
- package/dist/codex-conflicts.js +99 -0
- package/dist/codex-harness.js +321 -0
- package/dist/codex-json.js +7 -0
- package/dist/codex-prepare.js +255 -0
- package/dist/codex-presentation.js +282 -0
- package/dist/commands/install.js +187 -173
- package/dist/commands/mutation.js +71 -0
- package/dist/commands/prepare.js +87 -188
- package/dist/commands/probe.js +191 -176
- package/dist/commands/uninstall.js +52 -139
- package/dist/commands/update.js +33 -88
- package/dist/generated-plugin.js +3 -3
- package/dist/harness-compatibility.js +13 -0
- package/dist/harness.js +1 -0
- package/dist/lifecycle.js +1 -86
- package/dist/manifest-overlay.js +2 -2
- package/dist/pi-compatibility.js +73 -0
- package/dist/pi-harness.js +63 -0
- package/dist/pi-install.js +495 -0
- package/dist/pi-native.js +81 -0
- package/dist/pi-package.js +153 -0
- package/dist/pi-paths.js +30 -0
- package/dist/pi-prepare.js +111 -0
- package/dist/pi-presentation.js +67 -0
- package/dist/pi-settings.js +171 -0
- package/dist/pi-shared-skills.js +413 -0
- package/dist/pi-state.js +311 -0
- package/dist/resource-lock.js +265 -0
- package/dist/safe-path.js +21 -5
- package/dist/strict-json.js +1 -1
- package/dist/upstream-workspace.js +8 -0
- package/dist/validator.js +3 -2
- package/package.json +19 -18
- package/bin/superpowers-manager.js +0 -16
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { failureResult, hasTerminalControl, successResult, } from "./adapter-result.js";
|
|
3
|
+
import { codexInspect, codexInstall, codexRemove, } from "./adapter.js";
|
|
4
|
+
import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./codex-prepare.js";
|
|
5
|
+
import { codexInstallReceipt, codexPresentation, } from "./codex-presentation.js";
|
|
6
|
+
import { reportLegacyState, requireManagedUpdateControl, requireNoLegacyState, verifyUninstalledResources, } from "./lifecycle.js";
|
|
7
|
+
import { SafetyError } from "./safety-error.js";
|
|
8
|
+
import { commitMatches } from "./status.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
|
+
function malformed(result, message) {
|
|
16
|
+
return failureResult(result.outcome.operation, "malformed-result", message, [], result.outcome.messages);
|
|
17
|
+
}
|
|
18
|
+
function invalidStatus(result, message) {
|
|
19
|
+
return failureResult(result.outcome.operation, "invalid-status", message, [], result.outcome.messages);
|
|
20
|
+
}
|
|
21
|
+
function resultRecord(result) {
|
|
22
|
+
const value = result.outcome.ok ? result.outcome.result : null;
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
24
|
+
? value
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
function stringField(result, key) {
|
|
28
|
+
const raw = result[key];
|
|
29
|
+
if (raw === null || raw === undefined)
|
|
30
|
+
return { ok: true, value: "" };
|
|
31
|
+
if (typeof raw === "string")
|
|
32
|
+
return { ok: true, value: raw };
|
|
33
|
+
return { ok: false };
|
|
34
|
+
}
|
|
35
|
+
function resourceFlag(result, key) {
|
|
36
|
+
const resources = result.resources;
|
|
37
|
+
const bag = typeof resources === "object" &&
|
|
38
|
+
resources !== null &&
|
|
39
|
+
!Array.isArray(resources)
|
|
40
|
+
? resources
|
|
41
|
+
: {};
|
|
42
|
+
const value = bag[key];
|
|
43
|
+
return typeof value === "boolean" ? value : null;
|
|
44
|
+
}
|
|
45
|
+
function conflictDescriptions(result) {
|
|
46
|
+
const raw = result.conflicts;
|
|
47
|
+
if (raw === null || raw === undefined)
|
|
48
|
+
return [];
|
|
49
|
+
if (!Array.isArray(raw))
|
|
50
|
+
return null;
|
|
51
|
+
const descriptions = [];
|
|
52
|
+
for (const item of raw) {
|
|
53
|
+
if (typeof item !== "string" ||
|
|
54
|
+
item.length === 0 ||
|
|
55
|
+
hasTerminalControl(item)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
descriptions.push(item);
|
|
59
|
+
}
|
|
60
|
+
return descriptions;
|
|
61
|
+
}
|
|
62
|
+
function installDecision(legacy) {
|
|
63
|
+
switch (legacy.kind) {
|
|
64
|
+
case "ok":
|
|
65
|
+
return { kind: "allowed" };
|
|
66
|
+
case "blocked":
|
|
67
|
+
return {
|
|
68
|
+
kind: "blocked",
|
|
69
|
+
output: { stdout: [], stderr: legacy.lines },
|
|
70
|
+
};
|
|
71
|
+
case "unknown":
|
|
72
|
+
return {
|
|
73
|
+
kind: "blocked",
|
|
74
|
+
output: { stdout: [], stderr: [`error: ${legacy.message}`] },
|
|
75
|
+
};
|
|
76
|
+
case "report":
|
|
77
|
+
return {
|
|
78
|
+
kind: "blocked",
|
|
79
|
+
output: {
|
|
80
|
+
stdout: [],
|
|
81
|
+
stderr: ["error: unexpected legacy report during installation"],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function normalizeCodexOwnership(result) {
|
|
87
|
+
if (!result.outcome.ok)
|
|
88
|
+
return preserveFailure(result);
|
|
89
|
+
if (result.status !== 0) {
|
|
90
|
+
return invalidStatus(result, "adapter reported a failure status for inspect --view ownership");
|
|
91
|
+
}
|
|
92
|
+
const record = resultRecord(result);
|
|
93
|
+
if (record === null) {
|
|
94
|
+
return malformed(result, "adapter returned a non-object result for inspect --view ownership");
|
|
95
|
+
}
|
|
96
|
+
const pluginPresent = resourceFlag(record, "plugin");
|
|
97
|
+
if (pluginPresent === null) {
|
|
98
|
+
return malformed(result, "expected a Boolean adapter result at resources.plugin");
|
|
99
|
+
}
|
|
100
|
+
const marketplacePresent = resourceFlag(record, "marketplace");
|
|
101
|
+
if (marketplacePresent === null) {
|
|
102
|
+
return malformed(result, "expected a Boolean adapter result at resources.marketplace");
|
|
103
|
+
}
|
|
104
|
+
const identity = stringField(record, "identity_state");
|
|
105
|
+
if (!identity.ok) {
|
|
106
|
+
return malformed(result, "adapter returned a non-string identity_state for inspect --view ownership");
|
|
107
|
+
}
|
|
108
|
+
const identityState = identity.value;
|
|
109
|
+
const conflicts = conflictDescriptions(record);
|
|
110
|
+
if (conflicts === null) {
|
|
111
|
+
return malformed(result, "expected an array of strings at conflicts");
|
|
112
|
+
}
|
|
113
|
+
const legacyEligibility = identityState.length === 0
|
|
114
|
+
? {
|
|
115
|
+
kind: "blocked",
|
|
116
|
+
output: {
|
|
117
|
+
stdout: [],
|
|
118
|
+
stderr: ["error: probe did not report adapter identity state"],
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
: installDecision(requireNoLegacyState(identityState));
|
|
122
|
+
const installEligibility = legacyEligibility.kind === "blocked" || conflicts.length === 0
|
|
123
|
+
? legacyEligibility
|
|
124
|
+
: {
|
|
125
|
+
kind: "blocked",
|
|
126
|
+
output: {
|
|
127
|
+
stdout: [],
|
|
128
|
+
stderr: [
|
|
129
|
+
"Conflicting unmanaged Superpowers Codex resources require manual resolution:",
|
|
130
|
+
...conflicts.map((conflict) => `- ${conflict}`),
|
|
131
|
+
"Remove or disable each resource manually, then retry.",
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const verification = verifyUninstalledResources(result);
|
|
136
|
+
const legacyReport = reportLegacyState(identityState);
|
|
137
|
+
let removalVerification;
|
|
138
|
+
if (!verification.ok) {
|
|
139
|
+
removalVerification = {
|
|
140
|
+
kind: "blocked",
|
|
141
|
+
output: { stdout: [], stderr: [`error: ${verification.message}`] },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
else if (legacyReport.kind === "unknown") {
|
|
145
|
+
removalVerification = {
|
|
146
|
+
kind: "blocked",
|
|
147
|
+
output: { stdout: [], stderr: [`error: ${legacyReport.message}`] },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
else if (legacyReport.kind === "blocked") {
|
|
151
|
+
removalVerification = {
|
|
152
|
+
kind: "blocked",
|
|
153
|
+
output: {
|
|
154
|
+
stdout: [],
|
|
155
|
+
stderr: ["error: unexpected legacy block after removal"],
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
removalVerification = { kind: "allowed" };
|
|
161
|
+
}
|
|
162
|
+
const postRemovalOutput = legacyReport.kind === "report"
|
|
163
|
+
? { stdout: legacyReport.lines, stderr: [] }
|
|
164
|
+
: { stdout: [], stderr: [] };
|
|
165
|
+
return successResult(result.outcome.operation, {
|
|
166
|
+
installEligibility,
|
|
167
|
+
removalInput: { pluginPresent, marketplacePresent },
|
|
168
|
+
removalVerification,
|
|
169
|
+
postRemovalOutput,
|
|
170
|
+
presentationValue: identityState,
|
|
171
|
+
presentationConflicts: conflicts,
|
|
172
|
+
}, result.outcome.messages);
|
|
173
|
+
}
|
|
174
|
+
export function normalizeCodexControl(result) {
|
|
175
|
+
if (!result.outcome.ok)
|
|
176
|
+
return preserveFailure(result);
|
|
177
|
+
if (result.status !== 0) {
|
|
178
|
+
return invalidStatus(result, "adapter reported a failure status for inspect --view update-control");
|
|
179
|
+
}
|
|
180
|
+
const record = resultRecord(result);
|
|
181
|
+
if (record === null) {
|
|
182
|
+
return malformed(result, "adapter returned a non-object result for inspect --view update-control");
|
|
183
|
+
}
|
|
184
|
+
const control = stringField(record, "update_control");
|
|
185
|
+
if (!control.ok) {
|
|
186
|
+
return malformed(result, "adapter returned a non-string update_control for inspect --view update-control");
|
|
187
|
+
}
|
|
188
|
+
const updateControl = control.value;
|
|
189
|
+
const probeEligibility = updateControl.length === 0
|
|
190
|
+
? {
|
|
191
|
+
kind: "blocked",
|
|
192
|
+
output: {
|
|
193
|
+
stdout: [],
|
|
194
|
+
stderr: [
|
|
195
|
+
"error: probe did not report adapter update-control capability",
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
: { kind: "allowed" };
|
|
200
|
+
const managed = requireManagedUpdateControl(updateControl);
|
|
201
|
+
const mutationEligibility = probeEligibility.kind === "blocked"
|
|
202
|
+
? probeEligibility
|
|
203
|
+
: managed.ok
|
|
204
|
+
? { kind: "allowed" }
|
|
205
|
+
: {
|
|
206
|
+
kind: "blocked",
|
|
207
|
+
output: {
|
|
208
|
+
stdout: [],
|
|
209
|
+
stderr: [`error: ${managed.message}`],
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
return successResult(result.outcome.operation, { probeEligibility, mutationEligibility, presentationValue: updateControl }, result.outcome.messages);
|
|
213
|
+
}
|
|
214
|
+
export function normalizeCodexInstalled(result, desiredCommit) {
|
|
215
|
+
if (!result.outcome.ok)
|
|
216
|
+
return preserveFailure(result);
|
|
217
|
+
if (result.status !== 0) {
|
|
218
|
+
return invalidStatus(result, "adapter reported a failure status for inspect --view fingerprint");
|
|
219
|
+
}
|
|
220
|
+
const record = resultRecord(result);
|
|
221
|
+
if (record === null) {
|
|
222
|
+
return malformed(result, "adapter returned a non-object result for inspect --view fingerprint");
|
|
223
|
+
}
|
|
224
|
+
const fingerprint = stringField(record, "fingerprint");
|
|
225
|
+
if (!fingerprint.ok) {
|
|
226
|
+
return malformed(result, "adapter returned a non-string fingerprint for inspect --view fingerprint");
|
|
227
|
+
}
|
|
228
|
+
const observedIdentity = fingerprint.value;
|
|
229
|
+
const installed = observedIdentity.length === 0
|
|
230
|
+
? { kind: "absent", observedIdentity: "" }
|
|
231
|
+
: commitMatches(desiredCommit, observedIdentity)
|
|
232
|
+
? { kind: "current", observedIdentity }
|
|
233
|
+
: { kind: "mismatch", observedIdentity };
|
|
234
|
+
return successResult(result.outcome.operation, installed, result.outcome.messages);
|
|
235
|
+
}
|
|
236
|
+
function safeHint(value) {
|
|
237
|
+
return typeof value === "string" && !hasTerminalControl(value) ? value : "";
|
|
238
|
+
}
|
|
239
|
+
export function normalizeCodexInstall(result) {
|
|
240
|
+
if (!result.outcome.ok)
|
|
241
|
+
return preserveFailure(result);
|
|
242
|
+
if (result.status !== 0) {
|
|
243
|
+
return invalidStatus(result, "adapter reported a failure status for install");
|
|
244
|
+
}
|
|
245
|
+
const record = resultRecord(result);
|
|
246
|
+
if (record === null) {
|
|
247
|
+
return malformed(result, "adapter returned a non-object result for install");
|
|
248
|
+
}
|
|
249
|
+
const hints = record.verification_hints;
|
|
250
|
+
const bag = typeof hints === "object" && hints !== null && !Array.isArray(hints)
|
|
251
|
+
? hints
|
|
252
|
+
: {};
|
|
253
|
+
return successResult(result.outcome.operation, codexInstallReceipt(safeHint(bag.missing), safeHint(bag.mismatch)), result.outcome.messages);
|
|
254
|
+
}
|
|
255
|
+
// The canonical payload normalizer has no context parameter. Keep the native
|
|
256
|
+
// call's defensive success/nonzero-status guard beside the concrete adapter,
|
|
257
|
+
// where the exact legacy diagnostic can still name ctx.root.
|
|
258
|
+
export function normalizeCodexInstallForContext(result, ctx) {
|
|
259
|
+
if (!result.outcome.ok)
|
|
260
|
+
return preserveFailure(result);
|
|
261
|
+
if (result.status !== 0) {
|
|
262
|
+
return invalidStatus(result, codexPresentation.callFailure("install", ctx).invalidStatus);
|
|
263
|
+
}
|
|
264
|
+
return normalizeCodexInstall(result);
|
|
265
|
+
}
|
|
266
|
+
function requirements(command, env) {
|
|
267
|
+
if (command !== "probe" &&
|
|
268
|
+
command !== "install" &&
|
|
269
|
+
command !== "update" &&
|
|
270
|
+
command !== "uninstall") {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
const executable = env.SUPERPOWERS_CODEX || "codex";
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
name: "codex",
|
|
277
|
+
executable,
|
|
278
|
+
lookup: "explicit-path-or-path",
|
|
279
|
+
missingMessage: `required command not found: ${executable} — install the Codex CLI or set SUPERPOWERS_CODEX`,
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
async function mutationRoots(ctx) {
|
|
284
|
+
const env = ctx.env ?? {};
|
|
285
|
+
const configured = env.CODEX_HOME;
|
|
286
|
+
if (configured !== undefined && configured.length > 0) {
|
|
287
|
+
return [resolve(configured)];
|
|
288
|
+
}
|
|
289
|
+
const home = env.HOME;
|
|
290
|
+
if (home === undefined || home.length === 0) {
|
|
291
|
+
throw new SafetyError("codex-harness", "cannot determine Codex state root without HOME");
|
|
292
|
+
}
|
|
293
|
+
return [join(resolve(home), ".codex")];
|
|
294
|
+
}
|
|
295
|
+
export const codexHarness = {
|
|
296
|
+
preparationLocation: codexPreparationLocation,
|
|
297
|
+
mutationRoots,
|
|
298
|
+
validatePreparationBeforeFetch: validateCodexPreparationBeforeFetch,
|
|
299
|
+
prepareCandidate: prepareCodexCandidate,
|
|
300
|
+
inspectPrepared: inspectCodexPrepared,
|
|
301
|
+
readPrepared: readCodexPrepared,
|
|
302
|
+
inspectOwnership: async (ctx) => normalizeCodexOwnership(await codexInspect("ownership", ctx)),
|
|
303
|
+
inspectUpdateControl: async (ctx) => normalizeCodexControl(await codexInspect("update-control", ctx)),
|
|
304
|
+
inspectInstalled: async (selection, ctx) => normalizeCodexInstalled(await codexInspect("fingerprint", ctx), selection.desiredCommit),
|
|
305
|
+
install: async (_artifact, ctx) => {
|
|
306
|
+
return normalizeCodexInstallForContext(await codexInstall(ctx.root, ctx), ctx);
|
|
307
|
+
},
|
|
308
|
+
remove: async (input, ctx) => {
|
|
309
|
+
const result = await codexRemove(input, ctx);
|
|
310
|
+
if (!result.outcome.ok)
|
|
311
|
+
return preserveFailure(result);
|
|
312
|
+
if (result.status !== 0) {
|
|
313
|
+
return invalidStatus(result, codexPresentation.callFailure("remove", ctx, input).invalidStatus);
|
|
314
|
+
}
|
|
315
|
+
return successResult(result.outcome.operation, null, result.outcome.messages);
|
|
316
|
+
},
|
|
317
|
+
requirements,
|
|
318
|
+
get presentation() {
|
|
319
|
+
return codexPresentation;
|
|
320
|
+
},
|
|
321
|
+
};
|
package/dist/codex-json.js
CHANGED
|
@@ -59,6 +59,13 @@ function hasTerminalControl(value) {
|
|
|
59
59
|
export function installedListingHas(raw, arrayKey, field, value) {
|
|
60
60
|
return checkedItems(raw, ACCEPT_CONSTANTS, arrayKey, field).some((item) => item[field] === value);
|
|
61
61
|
}
|
|
62
|
+
export function codexInstalledPluginsFromJson(raw) {
|
|
63
|
+
return checkedItems(raw, ACCEPT_CONSTANTS, "installed", "pluginId").map((item) => ({
|
|
64
|
+
pluginId: item.pluginId,
|
|
65
|
+
installed: typeof item.installed === "boolean" ? item.installed : null,
|
|
66
|
+
enabled: typeof item.enabled === "boolean" ? item.enabled : null,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
62
69
|
export function marketplaceRootFromJson(raw, marketplaceName) {
|
|
63
70
|
const items = checkedItems(raw, ACCEPT_CONSTANTS, "marketplaces", "name");
|
|
64
71
|
const match = items.find((item) => item.name === marketplaceName);
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { cp, mkdir, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { failureResult, successResult, } from "./adapter-result.js";
|
|
4
|
+
import { codexBuild } from "./adapter.js";
|
|
5
|
+
import { ARTIFACT_RECEIPT } from "./artifact-tree.js";
|
|
6
|
+
import { assessCodexCompatibility, readCodexAssessment, writeCodexAssessment, } from "./codex-compatibility.js";
|
|
7
|
+
import { readManifest } from "./hooks.js";
|
|
8
|
+
import { readGeneratedCommitLenient, readStrictProvenanceField, writeProvenance, } from "./provenance.js";
|
|
9
|
+
import { classifyPathNoFollow } from "./safe-path.js";
|
|
10
|
+
import { SafetyError } from "./safety-error.js";
|
|
11
|
+
import { manifestVersionForRef } from "./upstream-version.js";
|
|
12
|
+
// Order is inherited from the original prepare command; the first miss wins.
|
|
13
|
+
const REQUIRED_UPSTREAM = [
|
|
14
|
+
{ path: "skills", label: "skills/" },
|
|
15
|
+
{ path: "LICENSE", label: "LICENSE" },
|
|
16
|
+
{ path: "README.md", label: "README.md" },
|
|
17
|
+
{ path: "CODE_OF_CONDUCT.md", label: "CODE_OF_CONDUCT.md" },
|
|
18
|
+
];
|
|
19
|
+
const COPY_PATHS = [
|
|
20
|
+
"skills",
|
|
21
|
+
"assets",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"README.md",
|
|
24
|
+
"CODE_OF_CONDUCT.md",
|
|
25
|
+
];
|
|
26
|
+
const RESOLUTION_KINDS = [
|
|
27
|
+
"latest-release",
|
|
28
|
+
"tag",
|
|
29
|
+
"ref",
|
|
30
|
+
"raw-commit",
|
|
31
|
+
];
|
|
32
|
+
function unassessedCompatibility() {
|
|
33
|
+
return {
|
|
34
|
+
kind: "unknown",
|
|
35
|
+
reason: "Codex compatibility assessment is not available",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function prepareError(message, cause) {
|
|
39
|
+
return new SafetyError("prepare", message, { cause });
|
|
40
|
+
}
|
|
41
|
+
function isControlledPreparationError(cause) {
|
|
42
|
+
return (cause instanceof SafetyError &&
|
|
43
|
+
(cause.module === "prepare" ||
|
|
44
|
+
cause.module === "hooks" ||
|
|
45
|
+
cause.module === "provenance"));
|
|
46
|
+
}
|
|
47
|
+
async function pathExists(path) {
|
|
48
|
+
try {
|
|
49
|
+
await stat(path);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function regularFileExists(path) {
|
|
57
|
+
try {
|
|
58
|
+
return (await stat(path)).isFile();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function owned(message, fn) {
|
|
65
|
+
try {
|
|
66
|
+
return await fn();
|
|
67
|
+
}
|
|
68
|
+
catch (cause) {
|
|
69
|
+
throw prepareError(message, cause);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function copyPathIfPresent(source, destination) {
|
|
73
|
+
if (!(await pathExists(source)))
|
|
74
|
+
return;
|
|
75
|
+
await owned(`cannot clear candidate path: ${destination}`, () => rm(destination, { recursive: true, force: true }));
|
|
76
|
+
await owned(`cannot copy upstream path into candidate: ${source}`, () => cp(source, destination, { recursive: true, verbatimSymlinks: true }));
|
|
77
|
+
}
|
|
78
|
+
export function resolveFromCwd(value, cwd) {
|
|
79
|
+
return isAbsolute(value) ? value : resolve(cwd, value);
|
|
80
|
+
}
|
|
81
|
+
function manifestTemplate(ctx) {
|
|
82
|
+
const env = ctx.env ?? {};
|
|
83
|
+
return (env.SUPERPOWERS_MANIFEST_TEMPLATE ||
|
|
84
|
+
join(ctx.root, "plugins", "superpowers", ".codex-plugin", "plugin.template.json"));
|
|
85
|
+
}
|
|
86
|
+
export function codexPreparationLocation(ctx) {
|
|
87
|
+
const env = ctx.env ?? {};
|
|
88
|
+
return {
|
|
89
|
+
destinationRoot: resolveFromCwd(env.SUPERPOWERS_PLUGIN_ROOT || join(ctx.root, "plugins", "superpowers"), process.cwd()),
|
|
90
|
+
stagingLeaf: "superpowers",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export async function validateCodexPreparationBeforeFetch(ctx) {
|
|
94
|
+
const template = manifestTemplate(ctx);
|
|
95
|
+
if (!(await regularFileExists(template))) {
|
|
96
|
+
return failureResult("prepare", "missing-template", `missing fallback manifest template: ${template}`, [], []);
|
|
97
|
+
}
|
|
98
|
+
return successResult("prepare", null, []);
|
|
99
|
+
}
|
|
100
|
+
export async function readUpstreamManifestVersion(path) {
|
|
101
|
+
const manifest = await readManifest(path);
|
|
102
|
+
const value = manifest.version;
|
|
103
|
+
if (value === undefined || value === null)
|
|
104
|
+
return "";
|
|
105
|
+
if (typeof value !== "string") {
|
|
106
|
+
throw prepareError(`upstream manifest version is not a string: ${path}`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function asResolutionKind(value) {
|
|
111
|
+
for (const kind of RESOLUTION_KINDS) {
|
|
112
|
+
if (kind === value)
|
|
113
|
+
return kind;
|
|
114
|
+
}
|
|
115
|
+
throw prepareError(`unknown upstream resolution kind: ${value}`);
|
|
116
|
+
}
|
|
117
|
+
export async function prepareCodexCandidate(input, ctx) {
|
|
118
|
+
let upstreamManifestVersion = "";
|
|
119
|
+
let managerVersion = "";
|
|
120
|
+
try {
|
|
121
|
+
for (const required of REQUIRED_UPSTREAM) {
|
|
122
|
+
if (!(await pathExists(join(input.upstreamRoot, required.path)))) {
|
|
123
|
+
return failureResult("prepare", "missing-upstream-path", `required upstream path missing: ${required.label}`, [], []);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await owned(`cannot clear candidate root: ${input.candidateRoot}`, () => rm(input.candidateRoot, { recursive: true, force: true }));
|
|
127
|
+
await owned(`cannot create candidate root: ${input.candidateRoot}`, () => mkdir(join(input.candidateRoot, ".codex-plugin"), { recursive: true }));
|
|
128
|
+
for (const name of COPY_PATHS) {
|
|
129
|
+
await copyPathIfPresent(join(input.upstreamRoot, name), join(input.candidateRoot, name));
|
|
130
|
+
}
|
|
131
|
+
const upstreamManifest = join(input.upstreamRoot, ".codex-plugin", "plugin.json");
|
|
132
|
+
if (await regularFileExists(upstreamManifest)) {
|
|
133
|
+
upstreamManifestVersion =
|
|
134
|
+
await readUpstreamManifestVersion(upstreamManifest);
|
|
135
|
+
}
|
|
136
|
+
await writeProvenance(join(input.candidateRoot, ".superpowers-upstream.json"), {
|
|
137
|
+
source: input.selection.effectiveSource,
|
|
138
|
+
requested_ref: input.selection.requestedRef,
|
|
139
|
+
resolved_ref: input.selection.resolvedRef,
|
|
140
|
+
commit: input.selection.desiredCommit,
|
|
141
|
+
upstream_manifest_version: upstreamManifestVersion,
|
|
142
|
+
});
|
|
143
|
+
managerVersion = manifestVersionForRef({
|
|
144
|
+
requestedRef: input.selection.requestedRef,
|
|
145
|
+
resolutionKind: asResolutionKind(input.selection.resolutionKind),
|
|
146
|
+
resolvedRef: input.selection.resolvedRef,
|
|
147
|
+
commit: input.selection.desiredCommit,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (cause) {
|
|
151
|
+
// owned()/asResolutionKind(), readManifest(), and writeProvenance replace
|
|
152
|
+
// every subordinate failure with controlled preparation, hook, or
|
|
153
|
+
// provenance text. Re-emitting those owned diagnostics is therefore safe.
|
|
154
|
+
// codexBuild stays below this catch because it deliberately rethrows
|
|
155
|
+
// exceptions its native operation does not own; the shared command must
|
|
156
|
+
// hide those.
|
|
157
|
+
if (!isControlledPreparationError(cause))
|
|
158
|
+
throw cause;
|
|
159
|
+
return failureResult("prepare", "prepare-failed", cause.message, [], []);
|
|
160
|
+
}
|
|
161
|
+
const built = await codexBuild({
|
|
162
|
+
upstreamRoot: input.upstreamRoot,
|
|
163
|
+
candidateRoot: input.candidateRoot,
|
|
164
|
+
requestedRef: input.selection.requestedRef,
|
|
165
|
+
resolvedRef: input.selection.resolvedRef,
|
|
166
|
+
commit: input.selection.desiredCommit,
|
|
167
|
+
managerVersion,
|
|
168
|
+
upstreamManifestVersion,
|
|
169
|
+
fallbackManifest: manifestTemplate(ctx),
|
|
170
|
+
}, ctx);
|
|
171
|
+
if (!built.outcome.ok) {
|
|
172
|
+
return { status: built.status, outcome: built.outcome };
|
|
173
|
+
}
|
|
174
|
+
if (built.status !== 0) {
|
|
175
|
+
return failureResult("build", "invalid-status", "adapter reported failure without an error outcome", [], built.outcome.messages);
|
|
176
|
+
}
|
|
177
|
+
const compatibility = await assessCodexCompatibility(input.upstreamRoot, input.selection);
|
|
178
|
+
if (compatibility.kind === "supported") {
|
|
179
|
+
try {
|
|
180
|
+
await writeCodexAssessment(input.candidateRoot, input.selection, (await regularFileExists(join(input.upstreamRoot, ".codex-plugin/plugin.json")))
|
|
181
|
+
? "upstream"
|
|
182
|
+
: "fallback");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return failureResult("prepare", "assessment-failed", `cannot record Codex compatibility assessment: ${input.candidateRoot}`, [], built.outcome.messages);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return successResult(built.outcome.operation, {
|
|
189
|
+
root: input.candidateRoot,
|
|
190
|
+
commit: input.selection.desiredCommit,
|
|
191
|
+
compatibility,
|
|
192
|
+
identity: input.selection.desiredCommit,
|
|
193
|
+
}, built.outcome.messages);
|
|
194
|
+
}
|
|
195
|
+
export async function inspectCodexPrepared(selection, ctx) {
|
|
196
|
+
const root = codexPreparationLocation(ctx).destinationRoot;
|
|
197
|
+
const observedIdentity = await readGeneratedCommitLenient(join(root, ".superpowers-upstream.json"));
|
|
198
|
+
const compatibility = unassessedCompatibility();
|
|
199
|
+
try {
|
|
200
|
+
if ((await classifyPathNoFollow(join(root, ARTIFACT_RECEIPT))) === "missing")
|
|
201
|
+
return successResult("inspect-prepared", {
|
|
202
|
+
kind: "needs-prepare",
|
|
203
|
+
observedIdentity,
|
|
204
|
+
compatibility,
|
|
205
|
+
}, []);
|
|
206
|
+
const artifact = await readCodexAssessment(root);
|
|
207
|
+
const source = await readStrictProvenanceField(join(artifact.root, ".superpowers-upstream.json"), "source");
|
|
208
|
+
if (artifact.commit === selection.desiredCommit &&
|
|
209
|
+
source === selection.effectiveSource) {
|
|
210
|
+
if (artifact.compatibility.kind === "supported")
|
|
211
|
+
return successResult("inspect-prepared", {
|
|
212
|
+
kind: "current",
|
|
213
|
+
artifact,
|
|
214
|
+
observedIdentity,
|
|
215
|
+
compatibility: artifact.compatibility,
|
|
216
|
+
}, []);
|
|
217
|
+
return successResult("inspect-prepared", {
|
|
218
|
+
kind: "needs-prepare",
|
|
219
|
+
observedIdentity,
|
|
220
|
+
compatibility: artifact.compatibility,
|
|
221
|
+
}, []);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return failureResult("inspect-prepared", "invalid-assessment", `cannot inspect Codex prepared artifact: ${root}`, [], []);
|
|
226
|
+
}
|
|
227
|
+
return successResult("inspect-prepared", {
|
|
228
|
+
kind: "needs-prepare",
|
|
229
|
+
observedIdentity,
|
|
230
|
+
compatibility,
|
|
231
|
+
}, []);
|
|
232
|
+
}
|
|
233
|
+
export async function readCodexPrepared(ctx) {
|
|
234
|
+
let commit = "";
|
|
235
|
+
try {
|
|
236
|
+
const value = await readStrictProvenanceField(join(codexPreparationLocation(ctx).destinationRoot, ".superpowers-upstream.json"), "commit");
|
|
237
|
+
if (typeof value === "string")
|
|
238
|
+
commit = value;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// The install path treats malformed metadata as an absent commit.
|
|
242
|
+
}
|
|
243
|
+
if (commit.length === 0) {
|
|
244
|
+
return failureResult("read-prepared", "missing-commit", "generated metadata missing desired commit after prepare", [], []);
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const artifact = await readCodexAssessment(codexPreparationLocation(ctx).destinationRoot);
|
|
248
|
+
if (artifact.compatibility.kind !== "supported")
|
|
249
|
+
throw new Error("unsupported profile");
|
|
250
|
+
return successResult("read-prepared", artifact, []);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return failureResult("read-prepared", "invalid-assessment", "generated Codex compatibility assessment is missing or invalid", [], []);
|
|
254
|
+
}
|
|
255
|
+
}
|