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.
Files changed (41) hide show
  1. package/README.md +63 -325
  2. package/dist/adapter.js +91 -46
  3. package/dist/artifact-tree.js +110 -0
  4. package/dist/atomic.js +103 -7
  5. package/dist/cli-arguments.js +37 -0
  6. package/dist/cli.js +97 -40
  7. package/dist/codex-compatibility.js +126 -0
  8. package/dist/codex-conflicts.js +99 -0
  9. package/dist/codex-harness.js +321 -0
  10. package/dist/codex-json.js +7 -0
  11. package/dist/codex-prepare.js +255 -0
  12. package/dist/codex-presentation.js +282 -0
  13. package/dist/commands/install.js +187 -173
  14. package/dist/commands/mutation.js +71 -0
  15. package/dist/commands/prepare.js +87 -188
  16. package/dist/commands/probe.js +191 -176
  17. package/dist/commands/uninstall.js +52 -139
  18. package/dist/commands/update.js +33 -88
  19. package/dist/generated-plugin.js +3 -3
  20. package/dist/harness-compatibility.js +13 -0
  21. package/dist/harness.js +1 -0
  22. package/dist/lifecycle.js +1 -86
  23. package/dist/manifest-overlay.js +2 -2
  24. package/dist/pi-compatibility.js +73 -0
  25. package/dist/pi-harness.js +63 -0
  26. package/dist/pi-install.js +495 -0
  27. package/dist/pi-native.js +81 -0
  28. package/dist/pi-package.js +153 -0
  29. package/dist/pi-paths.js +30 -0
  30. package/dist/pi-prepare.js +111 -0
  31. package/dist/pi-presentation.js +67 -0
  32. package/dist/pi-settings.js +171 -0
  33. package/dist/pi-shared-skills.js +413 -0
  34. package/dist/pi-state.js +311 -0
  35. package/dist/resource-lock.js +265 -0
  36. package/dist/safe-path.js +21 -5
  37. package/dist/strict-json.js +1 -1
  38. package/dist/upstream-workspace.js +8 -0
  39. package/dist/validator.js +3 -2
  40. package/package.json +19 -18
  41. package/bin/superpowers-manager.js +0 -16
package/dist/cli.js CHANGED
@@ -1,7 +1,10 @@
1
+ #!/usr/bin/env node
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
- import { runAdapter } from "./adapter.js";
4
- import { oneLine } from "./cli-arguments.js";
4
+ import { extractHarnessOptions, oneLine, UsageError } from "./cli-arguments.js";
5
+ import { codexHarness } from "./codex-harness.js";
6
+ import { piHarness } from "./pi-harness.js";
7
+ import { createResourceCoordinator } from "./resource-lock.js";
5
8
  import { runInstall } from "./commands/install.js";
6
9
  import { runPin } from "./commands/pin.js";
7
10
  import { runPrepare } from "./commands/prepare.js";
@@ -46,15 +49,15 @@ const IN_PROCESS_HANDLERS = {
46
49
  // the in-process path has no validator process. It remains CONDITIONAL for
47
50
  // `prepare` through commandRequirements(env) below, unchanged from slice 3.4.
48
51
  // No command requires a POSIX shell any more.
49
- const COMMAND_REQUIREMENTS = {
52
+ const SHARED_COMMAND_REQUIREMENTS = {
50
53
  pin: ["git"],
51
54
  "track-latest": [],
52
55
  unpin: [],
53
56
  prepare: ["git"],
54
- probe: ["git", "codex"],
55
- install: ["git", "codex"],
56
- update: ["git", "codex"],
57
- uninstall: ["codex"],
57
+ probe: ["git"],
58
+ install: ["git"],
59
+ update: ["git"],
60
+ uninstall: [],
58
61
  };
59
62
  // Walk upward from the bin's physical location to the directory containing
60
63
  // package.json. realpathSync first: npm/npx expose the bin through a symlink
@@ -84,14 +87,28 @@ function isMain(moduleFilename, argvPath) {
84
87
  function parseArgs(argv) {
85
88
  const first = argv[0];
86
89
  if (argv.length === 0)
87
- return { kind: "run", cmd: "update", args: [] };
90
+ return {
91
+ kind: "run",
92
+ cmd: "update",
93
+ args: [],
94
+ options: { harness: "codex", allowExperimental: false },
95
+ };
88
96
  if (first === "--help" || first === "-h")
89
97
  return { kind: "help" };
90
98
  if (first === "--version")
91
99
  return { kind: "version" };
92
100
  if (first && SUBCOMMANDS.includes(first)) {
93
101
  const command = first;
94
- const args = argv.slice(1);
102
+ let extracted;
103
+ try {
104
+ extracted = extractHarnessOptions(command, argv.slice(1));
105
+ }
106
+ catch (cause) {
107
+ if (cause instanceof UsageError)
108
+ return { kind: "usage-error", message: cause.message };
109
+ throw cause;
110
+ }
111
+ const { args, options } = extracted;
95
112
  if (command === "pin" && args.length !== 1) {
96
113
  return {
97
114
  kind: "usage-error",
@@ -127,7 +144,7 @@ function parseArgs(argv) {
127
144
  message: "usage: superpowers-manager probe [--porcelain]",
128
145
  };
129
146
  }
130
- return { kind: "run", cmd: command, args };
147
+ return { kind: "run", cmd: command, args, options };
131
148
  }
132
149
  return { kind: "usage-error", message: `unknown subcommand: ${first}` };
133
150
  }
@@ -160,40 +177,61 @@ function findTool(name, env, platform) {
160
177
  // rather than inside preflight — an accessor that under-reports what
161
178
  // preflight enforces is the blind spot slice 2 closed when it made
162
179
  // CLI-PREFLIGHT-01 derive its map from production.
163
- function commandRequirements(env) {
164
- if (!env.SUPERPOWERS_VALIDATOR)
165
- return COMMAND_REQUIREMENTS;
180
+ function sharedRequirement(name) {
181
+ return {
182
+ name,
183
+ executable: name,
184
+ lookup: "path",
185
+ missingMessage: `required command not found: ${name} — install ${name} and re-run`,
186
+ };
187
+ }
188
+ function commandRequirementsFor(env, adapter) {
189
+ const forCommand = (command) => {
190
+ const shared = SHARED_COMMAND_REQUIREMENTS[command].map(sharedRequirement);
191
+ if (command === "prepare" && env.SUPERPOWERS_VALIDATOR) {
192
+ shared.push(sharedRequirement("python3"));
193
+ }
194
+ return [...shared, ...adapter.requirements(command, env)];
195
+ };
166
196
  return {
167
- ...COMMAND_REQUIREMENTS,
168
- prepare: [...COMMAND_REQUIREMENTS.prepare, "python3"],
197
+ pin: forCommand("pin"),
198
+ "track-latest": forCommand("track-latest"),
199
+ unpin: forCommand("unpin"),
200
+ prepare: forCommand("prepare"),
201
+ probe: forCommand("probe"),
202
+ install: forCommand("install"),
203
+ update: forCommand("update"),
204
+ uninstall: forCommand("uninstall"),
169
205
  };
170
206
  }
207
+ function commandRequirements(env) {
208
+ return Object.fromEntries(SUBCOMMANDS.map((command) => [
209
+ command,
210
+ commandRequirementsFor(env, codexHarness)[command].map((requirement) => requirement.name),
211
+ ]));
212
+ }
171
213
  // Preflight; never touches Codex state. It is the union of two exported
172
214
  // accessors: configurationErrors (validator configuration) and
173
215
  // commandRequirements (tool availability), both specific to the selected
174
216
  // command. No command requires a POSIX shell: slice 4b flipped the last
175
217
  // spawned command in-process, so there is no shell to discover.
176
- function preflight(cmd, env, platform) {
218
+ function preflightFor(cmd, env, platform, adapter) {
177
219
  const errors = [...configurationErrors(cmd, env)];
178
- for (const tool of commandRequirements(env)[cmd]) {
179
- if (tool === "codex") {
180
- const codexBin = env.SUPERPOWERS_CODEX || "codex";
181
- // An explicit override may be a path rather than a PATH-resolvable name.
182
- const found = codexBin.includes(path.sep)
183
- ? fs.existsSync(codexBin)
184
- : Boolean(findTool(codexBin, env, platform));
185
- if (!found) {
186
- errors.push(`required command not found: ${codexBin} — install the Codex CLI or set SUPERPOWERS_CODEX`);
187
- }
188
- }
189
- else if (!findTool(tool, env, platform)) {
190
- errors.push(`required command not found: ${tool} — install ${tool} and re-run`);
191
- }
220
+ for (const requirement of commandRequirementsFor(env, adapter)[cmd]) {
221
+ const found = requirement.lookup === "explicit-path-or-path" &&
222
+ requirement.executable.includes(path.sep)
223
+ ? fs.existsSync(requirement.executable)
224
+ : Boolean(findTool(requirement.executable, env, platform));
225
+ if (!found)
226
+ errors.push(requirement.missingMessage);
192
227
  }
193
228
  if (errors.length)
194
229
  return { ok: false, errors };
195
230
  return { ok: true };
196
231
  }
232
+ function preflight(cmd, env, platform) {
233
+ return preflightFor(cmd, env, platform, codexHarness);
234
+ }
197
235
  function usage() {
198
236
  return [
199
237
  "usage: superpowers-manager [command] [args...]",
@@ -210,6 +248,14 @@ function usage() {
210
248
  " update probe, then prepare/install only if needed (default when no subcommand)",
211
249
  " uninstall remove the manager plugin and marketplace from Codex",
212
250
  "",
251
+ "Target one invocation (default: codex; selection commands are shared):",
252
+ " probe --harness pi [--porcelain]",
253
+ " prepare --harness pi",
254
+ " install --harness pi [--allow-experimental]",
255
+ " update --harness pi [--allow-experimental]",
256
+ " uninstall --harness pi",
257
+ " --harness=codex and --harness=pi are also accepted after the command.",
258
+ "",
213
259
  "Environment overrides (used by in-process commands): SUPERPOWERS_REF,",
214
260
  "SUPERPOWERS_UPSTREAM_URL, SUPERPOWERS_CODEX, SUPERPOWERS_CACHE_DIR,",
215
261
  "SUPERPOWERS_CONFIG_DIR, XDG_CONFIG_HOME,",
@@ -242,11 +288,22 @@ async function main() {
242
288
  console.error(usage());
243
289
  process.exit(2);
244
290
  }
245
- const pf = preflight(parsed.cmd, process.env, process.platform);
291
+ const status = parsed.options.harness === "pi"
292
+ ? await dispatch(piHarness, parsed)
293
+ : await dispatch(codexHarness, parsed);
294
+ process.exit(status);
295
+ }
296
+ async function dispatch(adapter, parsed) {
297
+ const root = resolvePackageRoot(import.meta.filename);
298
+ if (!root) {
299
+ console.error("error: cannot resolve the superpowers-manager package root");
300
+ return 1;
301
+ }
302
+ const pf = preflightFor(parsed.cmd, process.env, process.platform, adapter);
246
303
  if (!pf.ok) {
247
304
  for (const e of pf.errors)
248
305
  console.error(`error: ${e}`);
249
- process.exit(1);
306
+ return 1;
250
307
  }
251
308
  // Dispatch is no longer a branch: slice 4b flipped the last spawned command,
252
309
  // so every subcommand runs here, and slice 6 deleted the DISPATCH table whose
@@ -259,17 +316,17 @@ async function main() {
259
316
  const handler = IN_PROCESS_HANDLERS[parsed.cmd];
260
317
  if (!handler) {
261
318
  console.error(`error: no in-process handler registered for: ${parsed.cmd}`);
262
- process.exit(1);
319
+ return 1;
263
320
  }
264
321
  const ctx = {
265
322
  root,
266
323
  env: process.env,
267
324
  stdout: process.stdout,
268
325
  stderr: process.stderr,
269
- // The ONLY place runAdapter is bound to a context. Every command module
270
- // reaches the adapter through this field; none imports runAdapter
271
- // itself. tests/unit/ctx-adapter-provenance.test.js gates both halves.
272
- adapter: runAdapter,
326
+ options: parsed.options,
327
+ coordination: createResourceCoordinator(),
328
+ // The ONLY production binding of a concrete harness implementation.
329
+ adapter,
273
330
  };
274
331
  let status;
275
332
  try {
@@ -280,11 +337,11 @@ async function main() {
280
337
  // failures and returns a status code (see src/commands/unpin.ts). This
281
338
  // re-emits a subordinate module's own diagnostic if one somehow escapes.
282
339
  console.error(`error: ${oneLine(cause)}`);
283
- process.exit(1);
340
+ return 1;
284
341
  }
285
- process.exit(status);
342
+ return status;
286
343
  }
287
- export { resolvePackageRoot, isMain, parseArgs, findTool, commandRequirements, preflight, usage, main, };
344
+ export { resolvePackageRoot, isMain, parseArgs, findTool, commandRequirements, commandRequirementsFor, preflight, preflightFor, usage, dispatch, main, };
288
345
  if (isMain(import.meta.filename, process.argv[1])) {
289
346
  main().catch((cause) => {
290
347
  console.error(`error: ${oneLine(cause)}`);
@@ -0,0 +1,126 @@
1
+ import { join } from "node:path";
2
+ import { validateNativeSkill } from "./artifact-tree.js";
3
+ import { classifyPathNoFollow, assertExistingContained } from "./safe-path.js";
4
+ import { classifyHooks, readManifest } from "./hooks.js";
5
+ import { SEMVER_RE } from "./generated-plugin.js";
6
+ import { validateSource } from "./selection.js";
7
+ import { writeFile } from "node:fs/promises";
8
+ import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "./artifact-tree.js";
9
+ import { COMMIT_RE } from "./domain/refs.js";
10
+ import { validateGeneratedPlugin } from "./generated-plugin.js";
11
+ import { SafetyError } from "./safety-error.js";
12
+ export async function writeCodexAssessment(root, selection, manifestSource) {
13
+ const receipt = {
14
+ schema: 1,
15
+ manager: "superpowers-manager",
16
+ harness: "codex",
17
+ source: selection.effectiveSource,
18
+ commit: selection.desiredCommit,
19
+ generation: "codex-native-skills-v1",
20
+ manifestSource,
21
+ digest: await digestArtifactTree(root),
22
+ };
23
+ await writeFile(join(root, ARTIFACT_RECEIPT), JSON.stringify(receipt) + "\n", { flag: "wx" });
24
+ }
25
+ export async function readCodexAssessment(root) {
26
+ try {
27
+ const receipt = await readArtifactObject(root, join(root, ARTIFACT_RECEIPT));
28
+ const provenance = await readArtifactObject(root, join(root, ".superpowers-upstream.json"));
29
+ if (receipt.schema !== 1 ||
30
+ receipt.manager !== "superpowers-manager" ||
31
+ receipt.harness !== "codex" ||
32
+ receipt.generation !== "codex-native-skills-v1" ||
33
+ typeof receipt.source !== "string" ||
34
+ typeof receipt.commit !== "string" ||
35
+ !COMMIT_RE.test(receipt.commit) ||
36
+ typeof receipt.digest !== "string" ||
37
+ !ARTIFACT_DIGEST_RE.test(receipt.digest) ||
38
+ (receipt.manifestSource !== "upstream" &&
39
+ receipt.manifestSource !== "fallback") ||
40
+ receipt.source !== provenance.source ||
41
+ receipt.commit !== provenance.commit)
42
+ throw new Error("assessment fields");
43
+ validateSource(receipt.source);
44
+ if ((await digestArtifactTree(root)) !== receipt.digest)
45
+ throw new Error("assessment digest");
46
+ const manifest = await readManifest(join(root, ".codex-plugin/plugin.json"));
47
+ if (typeof manifest.version !== "string" ||
48
+ typeof provenance.requested_ref !== "string" ||
49
+ typeof provenance.resolved_ref !== "string" ||
50
+ typeof provenance.upstream_manifest_version !== "string")
51
+ throw new Error("provenance fields");
52
+ const errors = await validateGeneratedPlugin({
53
+ pluginRoot: root,
54
+ source: receipt.source,
55
+ commit: receipt.commit,
56
+ requestedRef: provenance.requested_ref,
57
+ resolvedRef: provenance.resolved_ref,
58
+ upstreamManifestVersion: provenance.upstream_manifest_version,
59
+ manifestVersion: manifest.version,
60
+ manifestSource: receipt.manifestSource,
61
+ });
62
+ if (errors.length)
63
+ throw new Error("generated resources");
64
+ const selection = {
65
+ effectiveSource: receipt.source,
66
+ desiredCommit: receipt.commit,
67
+ requestedRef: provenance.requested_ref,
68
+ resolvedRef: provenance.resolved_ref,
69
+ resolutionKind: "raw-commit",
70
+ selectionOrigin: "package-default",
71
+ selectionMode: "default",
72
+ upstreamSourceOrigin: "package-default",
73
+ saved: {
74
+ saved_mode: "none",
75
+ saved_source: "",
76
+ saved_requested_ref: "",
77
+ saved_resolved_ref: "",
78
+ saved_commit: "",
79
+ },
80
+ };
81
+ const compatibility = await assessCodexCompatibility(root, selection);
82
+ return {
83
+ root,
84
+ commit: receipt.commit,
85
+ identity: receipt.commit,
86
+ compatibility,
87
+ };
88
+ }
89
+ catch (cause) {
90
+ throw new SafetyError("codex-compatibility", `invalid Codex prepared assessment: ${root}`, { cause });
91
+ }
92
+ }
93
+ export async function assessCodexCompatibility(root, selection) {
94
+ try {
95
+ validateSource(selection.effectiveSource);
96
+ await validateNativeSkill(root);
97
+ for (const path of [
98
+ ".codex/superpowers-codex",
99
+ ".codex/superpowers-bootstrap.md",
100
+ ]) {
101
+ if ((await classifyPathNoFollow(join(root, path))) !== "missing")
102
+ throw new Error("legacy bootstrap");
103
+ }
104
+ const path = join(root, ".codex-plugin/plugin.json");
105
+ if ((await classifyPathNoFollow(path)) !== "missing") {
106
+ await assertExistingContained(root, path);
107
+ const manifest = await readManifest(path);
108
+ if (manifest.name !== "superpowers" ||
109
+ typeof manifest.version !== "string" ||
110
+ !SEMVER_RE.test(manifest.version))
111
+ throw new Error("manifest");
112
+ await classifyHooks(manifest, "upstream", root);
113
+ }
114
+ return {
115
+ kind: "supported",
116
+ generation: "codex-native-skills-v1",
117
+ reason: "native Codex skill discovery",
118
+ };
119
+ }
120
+ catch {
121
+ return {
122
+ kind: "unsupported",
123
+ reason: `Codex package does not provide compatible native skill discovery: ${root}`,
124
+ };
125
+ }
126
+ }
@@ -0,0 +1,99 @@
1
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { codexInstalledPluginsFromJson, } from "./codex-json.js";
4
+ export const CODEX_MANAGER_PLUGIN_ID = "superpowers@superpowers-manager";
5
+ export const CODEX_LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
6
+ const SUPERPOWERS_PLUGIN_PREFIX = "superpowers@";
7
+ const DISPLAYABLE_PLUGIN_ID = /^superpowers@[A-Za-z0-9][A-Za-z0-9._-]*/;
8
+ const NATIVE_ROUTE = ".agents/skills/superpowers";
9
+ const NATIVE_ROUTE_CONFLICT = `native Codex skills route ~/${NATIVE_ROUTE} has indeterminate activity`;
10
+ function isMissingPath(cause) {
11
+ if (cause === null || typeof cause !== "object" || !("code" in cause)) {
12
+ return false;
13
+ }
14
+ return cause.code === "ENOENT" || cause.code === "ENOTDIR";
15
+ }
16
+ function isUnmanagedPluginId(pluginId) {
17
+ if (pluginId === CODEX_MANAGER_PLUGIN_ID ||
18
+ pluginId === CODEX_LEGACY_PLUGIN_ID) {
19
+ return false;
20
+ }
21
+ return (pluginId.startsWith(SUPERPOWERS_PLUGIN_PREFIX) &&
22
+ pluginId.length > SUPERPOWERS_PLUGIN_PREFIX.length);
23
+ }
24
+ function displayablePluginId(pluginId) {
25
+ return pluginId.length <= 256 &&
26
+ DISPLAYABLE_PLUGIN_ID.exec(pluginId)?.[0] === pluginId
27
+ ? pluginId
28
+ : "";
29
+ }
30
+ function unmanagedPluginConflict(plugin) {
31
+ if (!isUnmanagedPluginId(plugin.pluginId))
32
+ return "";
33
+ const displayId = displayablePluginId(plugin.pluginId);
34
+ const subject = displayId.length > 0
35
+ ? `Codex plugin ${displayId}`
36
+ : "Codex plugin with a non-displayable Superpowers identity";
37
+ if (plugin.installed === true && plugin.enabled === true) {
38
+ return `active ${subject}`;
39
+ }
40
+ if ((plugin.installed === true && plugin.enabled === false) ||
41
+ (plugin.installed === false && plugin.enabled === false)) {
42
+ return "";
43
+ }
44
+ return `${subject} has indeterminate activity`;
45
+ }
46
+ async function nativeRouteConflict(ctx) {
47
+ const home = ctx.env?.HOME;
48
+ if (home === undefined || home.length === 0)
49
+ return NATIVE_ROUTE_CONFLICT;
50
+ const route = join(home, ".agents", "skills", "superpowers");
51
+ const asset = join(route, "using-superpowers", "SKILL.md");
52
+ let routeIsSymlink;
53
+ try {
54
+ routeIsSymlink = (await lstat(route)).isSymbolicLink();
55
+ }
56
+ catch (cause) {
57
+ return isMissingPath(cause) ? "" : NATIVE_ROUTE_CONFLICT;
58
+ }
59
+ if (!routeIsSymlink) {
60
+ try {
61
+ await lstat(asset);
62
+ }
63
+ catch (cause) {
64
+ return isMissingPath(cause) ? "" : NATIVE_ROUTE_CONFLICT;
65
+ }
66
+ }
67
+ try {
68
+ const resolvedAsset = await realpath(asset);
69
+ if (!(await stat(resolvedAsset)).isFile())
70
+ return NATIVE_ROUTE_CONFLICT;
71
+ const contents = await readFile(resolvedAsset, "utf8");
72
+ const lines = contents.split(/\r?\n/u);
73
+ if (lines[0] !== "---")
74
+ return "";
75
+ const end = lines.indexOf("---", 1);
76
+ if (end < 0 ||
77
+ !lines
78
+ .slice(1, end)
79
+ .some((line) => /^name:[ \t]+using-superpowers[ \t]*$/u.test(line))) {
80
+ return "";
81
+ }
82
+ }
83
+ catch {
84
+ return NATIVE_ROUTE_CONFLICT;
85
+ }
86
+ return NATIVE_ROUTE_CONFLICT;
87
+ }
88
+ export async function inspectCodexConflicts(ctx, installedListing) {
89
+ const conflicts = new Set();
90
+ for (const plugin of codexInstalledPluginsFromJson(installedListing)) {
91
+ const conflict = unmanagedPluginConflict(plugin);
92
+ if (conflict.length > 0)
93
+ conflicts.add(conflict);
94
+ }
95
+ const nativeConflict = await nativeRouteConflict(ctx);
96
+ if (nativeConflict.length > 0)
97
+ conflicts.add(nativeConflict);
98
+ return [...conflicts];
99
+ }