superpowers-manager 0.2.0 → 0.4.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 (68) hide show
  1. package/README.md +340 -66
  2. package/dist/adapter-result.js +204 -0
  3. package/dist/adapter.js +690 -0
  4. package/dist/atomic.js +142 -0
  5. package/dist/cli-arguments.js +32 -0
  6. package/dist/cli.js +313 -0
  7. package/dist/codex-harness.js +271 -0
  8. package/dist/codex-json.js +89 -0
  9. package/dist/codex-prepare.js +199 -0
  10. package/dist/codex-presentation.js +259 -0
  11. package/dist/codex-state.js +78 -0
  12. package/dist/commands/context.js +1 -0
  13. package/dist/commands/install.js +291 -0
  14. package/dist/commands/pin.js +79 -0
  15. package/dist/commands/prepare.js +420 -0
  16. package/dist/commands/probe.js +257 -0
  17. package/dist/commands/track-latest.js +44 -0
  18. package/dist/commands/uninstall.js +217 -0
  19. package/dist/commands/unpin.js +95 -0
  20. package/dist/commands/update.js +79 -0
  21. package/dist/domain/fingerprint.js +4 -0
  22. package/dist/domain/refs.js +37 -0
  23. package/dist/effective-selection.js +123 -0
  24. package/dist/generated-plugin.js +748 -0
  25. package/dist/git.js +38 -0
  26. package/dist/harness.js +1 -0
  27. package/dist/hooks.js +290 -0
  28. package/dist/lifecycle.js +135 -0
  29. package/dist/manifest-overlay.js +125 -0
  30. package/dist/provenance.js +111 -0
  31. package/dist/python-json-format.js +83 -0
  32. package/dist/python-json.js +29 -0
  33. package/dist/python-text.js +74 -0
  34. package/dist/safe-path.js +137 -0
  35. package/dist/safety-error.js +10 -0
  36. package/dist/selection-state-cli.js +85 -0
  37. package/dist/selection-store.js +172 -0
  38. package/dist/selection.js +200 -0
  39. package/dist/status.js +24 -0
  40. package/dist/strict-json.js +248 -0
  41. package/dist/upstream-cli.js +110 -0
  42. package/dist/upstream-version.js +40 -0
  43. package/dist/upstream.js +253 -0
  44. package/dist/validate-generated-plugin-cli.js +118 -0
  45. package/dist/validator.js +317 -0
  46. package/dist/workspace.js +139 -0
  47. package/package.json +25 -4
  48. package/bin/superpowers-manager.js +0 -290
  49. package/scripts/adapters/codex/adapter +0 -603
  50. package/scripts/adapters/codex/lib.sh +0 -449
  51. package/scripts/adapters/codex/validate-generated-plugin.py +0 -314
  52. package/scripts/core/adapter.sh +0 -103
  53. package/scripts/core/common.sh +0 -59
  54. package/scripts/core/lifecycle.sh +0 -141
  55. package/scripts/core/provenance.sh +0 -135
  56. package/scripts/core/selection-state.py +0 -414
  57. package/scripts/core/selection.sh +0 -168
  58. package/scripts/core/status.sh +0 -27
  59. package/scripts/core/upstream.sh +0 -280
  60. package/scripts/core/validate-adapter-response.py +0 -284
  61. package/scripts/install +0 -59
  62. package/scripts/pin +0 -51
  63. package/scripts/prepare +0 -117
  64. package/scripts/probe +0 -82
  65. package/scripts/track-latest +0 -30
  66. package/scripts/uninstall +0 -35
  67. package/scripts/unpin +0 -41
  68. package/scripts/update +0 -34
@@ -0,0 +1,79 @@
1
+ // Ports scripts/update. The shell sourced common.sh and lifecycle.sh; the
2
+ // predicates now live in src/lifecycle.ts. Unlike install and uninstall,
3
+ // update issues no ctx.adapter call of its own: every adapter interaction
4
+ // this module needs already happens inside gatherProbe, runPrepare or
5
+ // runInstall, each of which owns its own §4.2a-conformant invoke() gate. That
6
+ // is also why update opens no temporary workspace of its own -- there is no
7
+ // mutation here for one to protect.
8
+ import { oneLine } from "../cli-arguments.js";
9
+ import { gatherProbe, replayOutcome } from "./probe.js";
10
+ import { runInstall } from "./install.js";
11
+ import { runPrepare } from "./prepare.js";
12
+ function writeOutput(output, ctx) {
13
+ for (const line of output.stdout)
14
+ ctx.stdout.write(`${line}\n`);
15
+ for (const line of output.stderr)
16
+ ctx.stderr.write(`${line}\n`);
17
+ }
18
+ export async function runUpdate(argv, ctx) {
19
+ // scripts/update never reads "$@", so extra arguments are silently
20
+ // ignored -- the same asymmetry runPrepare and runInstall document.
21
+ void argv;
22
+ let probe;
23
+ try {
24
+ probe = await gatherProbe(ctx);
25
+ }
26
+ catch (cause) {
27
+ // gatherProbe performs no writes of its own (src/commands/probe.ts), so
28
+ // this catch cannot also be reached by an EPIPE from update's own
29
+ // output -- update writes nothing before this call resolves, unlike
30
+ // install's NOTE line.
31
+ //
32
+ // This is a THIRD consumer of gatherProbe's throw channel --
33
+ // `src/commands/probe.ts:251-309::THREE exceptions, all inherited and none a regression:`'s
34
+ // runProbe catch is the first and src/commands/install.ts's runInstall catch
35
+ // is the second. Because all three wrap the identical function, runProbe's
36
+ // long comment there enumerates exactly what can reach this stream too; not
37
+ // repeated here.
38
+ ctx.stderr.write(`error: ${oneLine(cause)}\n`);
39
+ return 1;
40
+ }
41
+ // Replay first, on both paths, before any decision -- clause 1.
42
+ for (const outcome of probe.outcomes)
43
+ replayOutcome(outcome, ctx);
44
+ if (probe.status === 1) {
45
+ if (probe.message !== null) {
46
+ ctx.stderr.write(`error: ${probe.message}\n`);
47
+ }
48
+ return 1;
49
+ }
50
+ const facts = probe.facts;
51
+ if (facts.ownership.installEligibility.kind === "blocked") {
52
+ writeOutput(facts.ownership.installEligibility.output, ctx);
53
+ return 1;
54
+ }
55
+ if (facts.control.probeEligibility.kind === "blocked") {
56
+ writeOutput(facts.control.probeEligibility.output, ctx);
57
+ return 1;
58
+ }
59
+ switch (facts.status) {
60
+ case "current": {
61
+ if (facts.control.mutationEligibility.kind === "blocked") {
62
+ writeOutput(facts.control.mutationEligibility.output, ctx);
63
+ return 1;
64
+ }
65
+ const rendered = ctx.adapter.presentation.renderProbe(facts);
66
+ ctx.stdout.write(rendered.porcelain);
67
+ ctx.stdout.write(`${ctx.adapter.presentation.currentNotice}\n`);
68
+ return 0;
69
+ }
70
+ case "needs prepare": {
71
+ const prepareStatus = await runPrepare([], ctx);
72
+ if (prepareStatus !== 0)
73
+ return prepareStatus;
74
+ return await runInstall([], ctx);
75
+ }
76
+ case "needs install":
77
+ return await runInstall([], ctx);
78
+ }
79
+ }
@@ -0,0 +1,4 @@
1
+ export function commitMatches(desired, observed) {
2
+ return (observed.length > 0 &&
3
+ (observed === desired || observed === desired.slice(0, 7)));
4
+ }
@@ -0,0 +1,37 @@
1
+ const SEMVER_BASE_SOURCE = "(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)" +
2
+ "(?:-(?:(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)" +
3
+ "(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?";
4
+ export const SEMVER_BASE_RE = new RegExp(`^${SEMVER_BASE_SOURCE}$`);
5
+ export const TAG_RE = new RegExp(`^v${SEMVER_BASE_SOURCE}$`);
6
+ export const COMMIT_RE = /^[0-9a-f]{40}$/;
7
+ export const COMMIT_INPUT_RE = /^[0-9A-Fa-f]{40}$/;
8
+ export function isTagRef(value) {
9
+ return TAG_RE.test(value);
10
+ }
11
+ export function isCommit(value) {
12
+ return COMMIT_RE.test(value);
13
+ }
14
+ export function normalizeCommitInput(value) {
15
+ return COMMIT_INPUT_RE.test(value) ? value.toLowerCase() : null;
16
+ }
17
+ export function parseStableTag(tag) {
18
+ if (!TAG_RE.test(tag))
19
+ return null;
20
+ const core = tag.slice(1);
21
+ // Prerelease filter. Sound only because TAG_RE already matched: the numeric
22
+ // components cannot contain "-", so the sole way one reaches `core` is a
23
+ // prerelease suffix. Stable-tag selection must exclude those.
24
+ if (core.includes("-"))
25
+ return null;
26
+ const [major, minor, patch] = core.split(".");
27
+ return { major: BigInt(major), minor: BigInt(minor), patch: BigInt(patch) };
28
+ }
29
+ export function compareStable(a, b) {
30
+ if (a.major !== b.major)
31
+ return a.major < b.major ? -1 : 1;
32
+ if (a.minor !== b.minor)
33
+ return a.minor < b.minor ? -1 : 1;
34
+ if (a.patch !== b.patch)
35
+ return a.patch < b.patch ? -1 : 1;
36
+ return 0;
37
+ }
@@ -0,0 +1,123 @@
1
+ // `selectionError` already exists at
2
+ // `src/selection.ts:41::export function selectionError` and tags errors with
3
+ // module "selection". Import it rather than defining a second one.
4
+ import { normalizeSaved, selectionError, validateSource, } from "./selection.js";
5
+ import { readSelectionState } from "./selection-store.js";
6
+ import { readConfigRef, resolveRef } from "./upstream.js";
7
+ import { COMMIT_RE } from "./domain/refs.js";
8
+ function requireAbsolute(value, variable) {
9
+ if (!value.startsWith("/")) {
10
+ throw selectionError(`${variable} must be absolute`);
11
+ }
12
+ return value;
13
+ }
14
+ // Precedence mirrors
15
+ // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/selection.sh:4-29::spw_selection_config_dir(`.
16
+ // SUPERPOWERS_CONFIG_DIR is selected on *presence*, matching the shell's
17
+ // ${SUPERPOWERS_CONFIG_DIR+x}: an empty value takes this branch and then fails
18
+ // the absolute check, rather than falling through to XDG.
19
+ export function selectionConfigDir(env) {
20
+ const explicit = env.SUPERPOWERS_CONFIG_DIR;
21
+ if (explicit !== undefined) {
22
+ return requireAbsolute(explicit, "SUPERPOWERS_CONFIG_DIR");
23
+ }
24
+ const xdg = env.XDG_CONFIG_HOME;
25
+ if (xdg !== undefined && xdg.length > 0) {
26
+ return `${requireAbsolute(xdg, "XDG_CONFIG_HOME")}/superpowers-manager`;
27
+ }
28
+ const home = env.HOME;
29
+ if (home === undefined || home.length === 0) {
30
+ throw selectionError("HOME is required to locate selection state");
31
+ }
32
+ return `${requireAbsolute(home, "HOME")}/.config/superpowers-manager`;
33
+ }
34
+ export function selectionStatePath(env) {
35
+ return `${selectionConfigDir(env)}/selection.json`;
36
+ }
37
+ // Composes exactly what `src/selection-state-cli.ts:39::const normalized`
38
+ // composes. The shell wrote a normalized document to a mktemp file and read
39
+ // five fields back with five python3 invocations; in-process this is one call.
40
+ export async function loadSavedSelection(env) {
41
+ return normalizeSaved(await readSelectionState(selectionStatePath(env)));
42
+ }
43
+ export const UPSTREAM_URL_DEFAULT = "https://github.com/obra/superpowers";
44
+ // Ports the env > saved > package-default precedence ladder from
45
+ // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/selection.sh:88-162::spw_compute_effective_selection`.
46
+ // validateSource runs before any ref resolution -- pinned by
47
+ // tests/unit/effective-selection.test.js's "source validation precedes ref
48
+ // resolution": a credential-bearing source must fail before Git is ever
49
+ // invoked.
50
+ export async function computeEffectiveSelection(root, env) {
51
+ const saved = await loadSavedSelection(env);
52
+ let upstreamSourceOrigin;
53
+ let effectiveSource;
54
+ if (env.SUPERPOWERS_UPSTREAM_URL) {
55
+ upstreamSourceOrigin = "environment";
56
+ effectiveSource = env.SUPERPOWERS_UPSTREAM_URL;
57
+ }
58
+ else if (saved.saved_mode !== "none") {
59
+ upstreamSourceOrigin = "user-config";
60
+ effectiveSource = saved.saved_source;
61
+ }
62
+ else {
63
+ upstreamSourceOrigin = "package-default";
64
+ effectiveSource = UPSTREAM_URL_DEFAULT;
65
+ }
66
+ // Before any Git access. Pinned by tests/unit/effective-selection.test.js's
67
+ // "source validation precedes ref resolution".
68
+ validateSource(effectiveSource);
69
+ let selectionOrigin;
70
+ let selectionMode;
71
+ let requestedRef;
72
+ let usesSavedPin = false;
73
+ if (env.SUPERPOWERS_REF) {
74
+ selectionOrigin = "environment";
75
+ selectionMode = "override";
76
+ requestedRef = env.SUPERPOWERS_REF;
77
+ }
78
+ else if (saved.saved_mode === "pinned") {
79
+ selectionOrigin = "user-config";
80
+ selectionMode = "pinned";
81
+ requestedRef = saved.saved_requested_ref;
82
+ usesSavedPin = true;
83
+ }
84
+ else if (saved.saved_mode === "track-latest") {
85
+ selectionOrigin = "user-config";
86
+ selectionMode = "track-latest";
87
+ requestedRef = "latest-release";
88
+ }
89
+ else {
90
+ selectionOrigin = "package-default";
91
+ selectionMode = "default";
92
+ requestedRef = await readConfigRef(root, env);
93
+ }
94
+ if (usesSavedPin) {
95
+ return {
96
+ selectionOrigin,
97
+ selectionMode,
98
+ upstreamSourceOrigin,
99
+ effectiveSource,
100
+ requestedRef,
101
+ resolvedRef: saved.saved_resolved_ref,
102
+ desiredCommit: saved.saved_commit,
103
+ resolutionKind: COMMIT_RE.test(saved.saved_requested_ref)
104
+ ? "raw-commit"
105
+ : "tag",
106
+ saved,
107
+ };
108
+ }
109
+ const resolution = await resolveRef(effectiveSource, requestedRef);
110
+ // `Resolution` (src/upstream.ts) is { kind, ref, commit } — the
111
+ // resolved ref field is named `ref`, not `resolvedRef`.
112
+ return {
113
+ selectionOrigin,
114
+ selectionMode,
115
+ upstreamSourceOrigin,
116
+ effectiveSource,
117
+ requestedRef,
118
+ resolvedRef: resolution.ref,
119
+ desiredCommit: resolution.commit,
120
+ resolutionKind: resolution.kind,
121
+ saved,
122
+ };
123
+ }