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,111 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { COMMIT_INPUT_RE } from "./domain/refs.js";
4
+ import { escapePythonJsonString } from "./python-json.js";
5
+ import { SafetyError } from "./safety-error.js";
6
+ import { parseStrictJson, } from "./strict-json.js";
7
+ export const PROVENANCE_STRICT_PROFILE = {
8
+ duplicateKeys: "last-wins",
9
+ nonStandardConstants: "reject",
10
+ maxDepth: 256,
11
+ };
12
+ export const PROVENANCE_LENIENT_PROFILE = {
13
+ duplicateKeys: "last-wins",
14
+ nonStandardConstants: "reject",
15
+ };
16
+ export const PROVENANCE_CODEX_SOURCE_PROFILE = {
17
+ duplicateKeys: "last-wins",
18
+ nonStandardConstants: "accept",
19
+ };
20
+ function asObject(value) {
21
+ return value !== null && typeof value === "object" && !Array.isArray(value)
22
+ ? value
23
+ : undefined;
24
+ }
25
+ export async function readStrictProvenanceField(path, dottedKey) {
26
+ try {
27
+ const parsed = parseStrictJson(await readFile(path), PROVENANCE_STRICT_PROFILE);
28
+ let current = parsed;
29
+ if (asObject(current) === undefined) {
30
+ throw new SafetyError("provenance", `provenance value must be an object: ${path}`);
31
+ }
32
+ for (const part of dottedKey.split(".")) {
33
+ const object = current === undefined ? undefined : asObject(current);
34
+ if (object === undefined || !Object.hasOwn(object, part))
35
+ return undefined;
36
+ current = object[part];
37
+ }
38
+ return current === null ? undefined : current;
39
+ }
40
+ catch (cause) {
41
+ if (cause instanceof SafetyError && cause.module === "provenance") {
42
+ throw cause;
43
+ }
44
+ throw new SafetyError("provenance", `cannot read strict provenance field from ${path}`, { cause });
45
+ }
46
+ }
47
+ export async function readGeneratedCommitLenient(path) {
48
+ try {
49
+ const parsed = parseStrictJson(await readFile(path), PROVENANCE_LENIENT_PROFILE);
50
+ const object = asObject(parsed);
51
+ const commit = object?.commit;
52
+ return typeof commit === "string" && COMMIT_INPUT_RE.test(commit)
53
+ ? commit
54
+ : "";
55
+ }
56
+ catch {
57
+ return "";
58
+ }
59
+ }
60
+ // Ported from
61
+ // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:28-31::spw_generated_metadata_path(`.
62
+ // The path the generated tree's provenance lives at, relative to a package
63
+ // root.
64
+ export function generatedMetadataPath(root) {
65
+ return join(root, "plugins", "superpowers", ".superpowers-upstream.json");
66
+ }
67
+ // Ported from
68
+ // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:33-37::spw_generated_commit_or_empty`.
69
+ // Lenient by design: a missing or malformed generated provenance file yields
70
+ // "", which `statusForCommits` reads as "needs prepare". Aborting here would
71
+ // deny the operator the remediation path.
72
+ export async function generatedCommitOrEmpty(root) {
73
+ return readGeneratedCommitLenient(generatedMetadataPath(root));
74
+ }
75
+ export async function readCodexBuildSource(path) {
76
+ try {
77
+ const parsed = parseStrictJson(await readFile(path), PROVENANCE_CODEX_SOURCE_PROFILE);
78
+ const source = asObject(parsed)?.source;
79
+ if (typeof source !== "string" || source.length === 0) {
80
+ throw new SafetyError("provenance", `invalid Codex source in ${path}`);
81
+ }
82
+ return source;
83
+ }
84
+ catch (cause) {
85
+ if (cause instanceof SafetyError && cause.module === "provenance") {
86
+ throw cause;
87
+ }
88
+ throw new SafetyError("provenance", `cannot read Codex source from ${path}`, { cause });
89
+ }
90
+ }
91
+ const PROVENANCE_KEYS = [
92
+ "source",
93
+ "requested_ref",
94
+ "resolved_ref",
95
+ "commit",
96
+ "upstream_manifest_version",
97
+ ];
98
+ export function serializeProvenance(record) {
99
+ const lines = PROVENANCE_KEYS.map((key) => ` "${key}": "${escapePythonJsonString(record[key])}"`);
100
+ return `{\n${lines.join(",\n")}\n}\n`;
101
+ }
102
+ export async function writeProvenance(path, record) {
103
+ try {
104
+ await writeFile(path, serializeProvenance(record), "utf8");
105
+ }
106
+ catch (cause) {
107
+ throw new SafetyError("provenance", `cannot write provenance ${path}`, {
108
+ cause,
109
+ });
110
+ }
111
+ }
@@ -0,0 +1,83 @@
1
+ import { SafetyError } from "./safety-error.js";
2
+ /**
3
+ * Render a JSON number token the way CPython's `json.dump` does.
4
+ *
5
+ * Classification is by source text, matching CPython's own: a token containing
6
+ * `.` or `e`/`E` parses as a float, anything else as an arbitrary-width int.
7
+ * Integers are emitted verbatim so widths above 2^53 survive; `-0` is the only
8
+ * normalization JSON's grammar leaves room for, since leading zeros and a
9
+ * leading `+` are already forbidden.
10
+ */
11
+ export function formatPythonNumber(raw) {
12
+ if (!/[.eE]/.test(raw)) {
13
+ // NaN/Infinity/-Infinity contain no `.` or `e`/`E`, so they would
14
+ // otherwise fall into the "emit verbatim" integer branch and echo back
15
+ // as invalid JSON. A reader running a non-standard-constants accept
16
+ // profile (see src/strict-json.ts) can hand this function exactly these
17
+ // three tokens as a raw number source, so this function must reject them
18
+ // itself rather than assume its caller already did. CPython's
19
+ // `json.dump(..., allow_nan=False)` raises ValueError for all three.
20
+ if (raw === "NaN" || raw === "Infinity" || raw === "-Infinity") {
21
+ throw new SafetyError("manifest-overlay", `JSON number out of range: ${raw}`);
22
+ }
23
+ return raw === "-0" ? "0" : raw;
24
+ }
25
+ const value = Number(raw);
26
+ if (!Number.isFinite(value)) {
27
+ throw new SafetyError("manifest-overlay", `JSON number out of range: ${raw}`);
28
+ }
29
+ return formatPythonFloat(value);
30
+ }
31
+ function formatPythonFloat(value) {
32
+ // toExponential() drops the sign of negative zero; CPython's repr keeps it.
33
+ // Note this differs from the integer -0, which normalizes to 0 — consistent,
34
+ // because -0 has no `.` or `e` and so classifies as an integer.
35
+ if (Object.is(value, -0))
36
+ return "-0.0";
37
+ // Called with no argument, toExponential() yields the shortest digit string
38
+ // that uniquely identifies the double — the same guarantee CPython's repr
39
+ // relies on. Taking digits and exponent separately is what lets us apply
40
+ // CPython's notation threshold instead of JavaScript's.
41
+ const [mantissa, exponentText] = value.toExponential().split("e");
42
+ const exponent = Number(exponentText);
43
+ const negative = mantissa.startsWith("-");
44
+ const digits = mantissa.replace("-", "").replace(".", "");
45
+ let body;
46
+ if (exponent >= -4 && exponent < 16) {
47
+ // Fixed notation, built by MOVING the decimal point through the digit
48
+ // string above. Do NOT route this through value.toFixed(): toExponential()
49
+ // has already produced the shortest digits that identify this double, and
50
+ // toFixed performs a second, independent rounding of the full decimal
51
+ // expansion. The two disagree — 1888570120608320.2 formats as
52
+ // ...320.3 via toFixed and ...320.2 in CPython. Measured 2026-07-31.
53
+ if (exponent >= digits.length - 1) {
54
+ body = `${digits}${"0".repeat(exponent - (digits.length - 1))}.0`;
55
+ }
56
+ else if (exponent >= 0) {
57
+ body = `${digits.slice(0, exponent + 1)}.${digits.slice(exponent + 1)}`;
58
+ }
59
+ else {
60
+ body = `0.${"0".repeat(-exponent - 1)}${digits}`;
61
+ }
62
+ }
63
+ else {
64
+ const sign = exponent < 0 ? "-" : "+";
65
+ const magnitude = String(Math.abs(exponent)).padStart(2, "0");
66
+ body = `${mantissa.replace("-", "")}e${sign}${magnitude}`;
67
+ }
68
+ return negative ? `-${body}` : body;
69
+ }
70
+ /**
71
+ * Apply CPython's `ensure_ascii=True` to an already-JSON-encoded string. Input
72
+ * must be the output of JSON.stringify, which has already escaped quotes,
73
+ * backslashes, and control characters; this only replaces the remainder.
74
+ * Astral characters are already surrogate pairs in a JavaScript string, so the
75
+ * per-code-unit replacement emits the pair CPython emits.
76
+ *
77
+ * The range starts at U+007F, not U+0080: CPython escapes DEL while
78
+ * JSON.stringify leaves it literal, and U+007E is "~" in both. Below U+0020
79
+ * the two already agree. Measured 2026-07-31.
80
+ */
81
+ export function escapeNonAscii(text) {
82
+ return text.replace(/[\u007f-\uffff]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
83
+ }
@@ -0,0 +1,29 @@
1
+ export function escapePythonJsonString(value) {
2
+ let escaped = "";
3
+ const shortEscapes = {
4
+ 0x08: "\\b",
5
+ 0x09: "\\t",
6
+ 0x0a: "\\n",
7
+ 0x0c: "\\f",
8
+ 0x0d: "\\r",
9
+ };
10
+ for (let index = 0; index < value.length; index += 1) {
11
+ const code = value.charCodeAt(index);
12
+ if (code === 0x22) {
13
+ escaped += '\\"';
14
+ }
15
+ else if (code === 0x5c) {
16
+ escaped += "\\\\";
17
+ }
18
+ else if (Object.hasOwn(shortEscapes, code)) {
19
+ escaped += shortEscapes[code];
20
+ }
21
+ else if (code < 0x20 || code >= 0x7f) {
22
+ escaped += `\\u${code.toString(16).padStart(4, "0")}`;
23
+ }
24
+ else {
25
+ escaped += value[index];
26
+ }
27
+ }
28
+ return escaped;
29
+ }
@@ -0,0 +1,74 @@
1
+ // CPython `str.strip()` whitespace, from Unicode's Bidi/White_Space tables as
2
+ // CPython applies them. Deliberately excludes U+FEFF, which JavaScript's
3
+ // `trim()` removes and Python's `strip()` does not.
4
+ const PYTHON_WHITESPACE = new Set([
5
+ "\t",
6
+ "\n",
7
+ "\v",
8
+ "\f",
9
+ "\r",
10
+ " ",
11
+ "\x1c",
12
+ "\x1d",
13
+ "\x1e",
14
+ "\x1f",
15
+ "\x85",
16
+ " ",
17
+ " ",
18
+ " ",
19
+ " ",
20
+ " ",
21
+ " ",
22
+ " ",
23
+ " ",
24
+ " ",
25
+ " ",
26
+ " ",
27
+ " ",
28
+ " ",
29
+ "
",
30
+ "
",
31
+ " ",
32
+ " ",
33
+ " ",
34
+ ]);
35
+ // CPython `str.splitlines()` boundaries. `\r\n` is handled as a pair below.
36
+ const PYTHON_LINE_BOUNDARIES = new Set([
37
+ "\n",
38
+ "\r",
39
+ "\v",
40
+ "\f",
41
+ "\x1c",
42
+ "\x1d",
43
+ "\x1e",
44
+ "\x85",
45
+ "
",
46
+ "
",
47
+ ]);
48
+ export function pythonStrip(value) {
49
+ let start = 0;
50
+ let end = value.length;
51
+ while (start < end && PYTHON_WHITESPACE.has(value[start]))
52
+ start += 1;
53
+ while (end > start && PYTHON_WHITESPACE.has(value[end - 1]))
54
+ end -= 1;
55
+ return value.slice(start, end);
56
+ }
57
+ export function pythonSplitlines(value) {
58
+ const lines = [];
59
+ let start = 0;
60
+ let index = 0;
61
+ while (index < value.length) {
62
+ const character = value[index];
63
+ if (!PYTHON_LINE_BOUNDARIES.has(character)) {
64
+ index += 1;
65
+ continue;
66
+ }
67
+ lines.push(value.slice(start, index));
68
+ index += character === "\r" && value[index + 1] === "\n" ? 2 : 1;
69
+ start = index;
70
+ }
71
+ if (start < value.length)
72
+ lines.push(value.slice(start));
73
+ return lines;
74
+ }
@@ -0,0 +1,137 @@
1
+ import { lstat, readlink, realpath } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, relative, resolve, sep, } from "node:path";
3
+ import { SafetyError } from "./safety-error.js";
4
+ function isErrno(value, code) {
5
+ return (value instanceof Error &&
6
+ "code" in value &&
7
+ value.code === code);
8
+ }
9
+ function isContained(root, candidate) {
10
+ const suffix = relative(root, candidate);
11
+ return (suffix === "" ||
12
+ (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
13
+ }
14
+ function requireContained(root, candidate) {
15
+ if (!isContained(root, candidate)) {
16
+ throw new SafetyError("safe-path", `path escapes containment root: ${candidate}`);
17
+ }
18
+ }
19
+ export async function classifyPathNoFollow(path) {
20
+ try {
21
+ const info = await lstat(path);
22
+ if (info.isSymbolicLink())
23
+ return "symlink";
24
+ if (info.isFile())
25
+ return "regular-file";
26
+ if (info.isDirectory())
27
+ return "directory";
28
+ return "other";
29
+ }
30
+ catch (cause) {
31
+ if (isErrno(cause, "ENOENT"))
32
+ return "missing";
33
+ throw new SafetyError("safe-path", `cannot inspect path: ${path}`, {
34
+ cause,
35
+ });
36
+ }
37
+ }
38
+ export async function assertNoFollowType(path, allowed) {
39
+ const actual = await classifyPathNoFollow(path);
40
+ if (!allowed.includes(actual)) {
41
+ throw new SafetyError("safe-path", `path has disallowed type ${actual}: ${path}`);
42
+ }
43
+ return actual;
44
+ }
45
+ export async function assertDesignatedParentDirectory(target) {
46
+ const parent = dirname(resolve(target));
47
+ const actual = await classifyPathNoFollow(parent);
48
+ if (actual !== "missing" && actual !== "directory") {
49
+ throw new SafetyError("safe-path", `designated parent must be a directory without following symlinks: ${parent}`);
50
+ }
51
+ }
52
+ export async function assertExistingContained(root, candidate) {
53
+ const lexicalRoot = resolve(root);
54
+ const lexicalCandidate = resolve(candidate);
55
+ requireContained(lexicalRoot, lexicalCandidate);
56
+ try {
57
+ const [resolvedRoot, resolvedCandidate] = await Promise.all([
58
+ realpath(lexicalRoot),
59
+ realpath(lexicalCandidate),
60
+ ]);
61
+ requireContained(resolvedRoot, resolvedCandidate);
62
+ return lexicalCandidate;
63
+ }
64
+ catch (cause) {
65
+ if (cause instanceof SafetyError)
66
+ throw cause;
67
+ throw new SafetyError("safe-path", `cannot resolve existing path: ${lexicalCandidate}`, { cause });
68
+ }
69
+ }
70
+ export async function assertProspectiveContained(root, candidate) {
71
+ const lexicalRoot = resolve(root);
72
+ const lexicalCandidate = resolve(candidate);
73
+ requireContained(lexicalRoot, lexicalCandidate);
74
+ try {
75
+ const resolvedRoot = await realpath(lexicalRoot);
76
+ const missing = [];
77
+ const visitedCursors = new Set();
78
+ let cursor = lexicalCandidate;
79
+ let resolvedAncestor;
80
+ for (;;) {
81
+ if (visitedCursors.has(cursor)) {
82
+ throw new SafetyError("safe-path", `prospective path resolution repeats cursor: ${cursor}`);
83
+ }
84
+ visitedCursors.add(cursor);
85
+ try {
86
+ resolvedAncestor = await realpath(cursor);
87
+ break;
88
+ }
89
+ catch (cause) {
90
+ if (!isErrno(cause, "ENOENT"))
91
+ throw cause;
92
+ let symlinkTarget;
93
+ try {
94
+ if ((await lstat(cursor)).isSymbolicLink()) {
95
+ symlinkTarget = await readlink(cursor);
96
+ }
97
+ }
98
+ catch (inspectionCause) {
99
+ if (!isErrno(inspectionCause, "ENOENT"))
100
+ throw inspectionCause;
101
+ }
102
+ if (symlinkTarget !== undefined) {
103
+ cursor = resolve(await realpath(dirname(cursor)), symlinkTarget);
104
+ continue;
105
+ }
106
+ const parent = dirname(cursor);
107
+ if (parent === cursor)
108
+ throw cause;
109
+ missing.unshift(basename(cursor));
110
+ cursor = parent;
111
+ }
112
+ }
113
+ const resolvedCandidate = resolve(resolvedAncestor, ...missing);
114
+ requireContained(resolvedRoot, resolvedCandidate);
115
+ return lexicalCandidate;
116
+ }
117
+ catch (cause) {
118
+ if (cause instanceof SafetyError)
119
+ throw cause;
120
+ throw new SafetyError("safe-path", `cannot resolve prospective path: ${lexicalCandidate}`, { cause });
121
+ }
122
+ }
123
+ export async function assertSymlinkTargetContained(root, candidate) {
124
+ await assertNoFollowType(candidate, ["symlink"]);
125
+ try {
126
+ const [resolvedRoot, resolvedTarget] = await Promise.all([
127
+ realpath(resolve(root)),
128
+ realpath(resolve(candidate)),
129
+ ]);
130
+ requireContained(resolvedRoot, resolvedTarget);
131
+ }
132
+ catch (cause) {
133
+ if (cause instanceof SafetyError)
134
+ throw cause;
135
+ throw new SafetyError("safe-path", `cannot resolve symlink target: ${candidate}`, { cause });
136
+ }
137
+ }
@@ -0,0 +1,10 @@
1
+ export class SafetyError extends Error {
2
+ module;
3
+ details;
4
+ constructor(module, message, options = {}) {
5
+ super(message, { cause: options.cause });
6
+ this.name = "SafetyError";
7
+ this.module = module;
8
+ this.details = options.details;
9
+ }
10
+ }
@@ -0,0 +1,85 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { displaySource, normalizePinnedArguments, normalizeSaved, validateSource, } from "./selection.js";
4
+ import { readSelectionState, writeSelectionState } from "./selection-store.js";
5
+ import { oneLine, parseFlags, UsageError } from "./cli-arguments.js";
6
+ const COMMAND_FLAGS = {
7
+ read: ["path", "output"],
8
+ "write-pinned": ["path", "source", "requested-ref", "resolved-ref", "commit"],
9
+ "write-track-latest": ["path", "source"],
10
+ "validate-source": ["source"],
11
+ "display-source": ["source"],
12
+ };
13
+ function parseCommand(argv) {
14
+ const [name, ...rest] = argv;
15
+ if (name === undefined)
16
+ throw new UsageError("a subcommand is required");
17
+ if (!Object.hasOwn(COMMAND_FLAGS, name)) {
18
+ throw new UsageError(`unknown subcommand: ${name}`);
19
+ }
20
+ const command = name;
21
+ return { name: command, flags: parseFlags(rest, COMMAND_FLAGS[command]) };
22
+ }
23
+ async function dispatch({ name, flags }) {
24
+ if (name === "read") {
25
+ const normalized = normalizeSaved(await readSelectionState(flags.path));
26
+ try {
27
+ await writeFile(flags.output, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
28
+ }
29
+ catch (cause) {
30
+ const message = cause instanceof Error ? cause.message : String(cause);
31
+ throw new Error(`cannot write normalized selection output ${flags.output}: ${message}`, { cause });
32
+ }
33
+ return;
34
+ }
35
+ if (name === "write-pinned") {
36
+ await writeSelectionState(flags.path, normalizePinnedArguments({
37
+ source: flags.source,
38
+ requestedRef: flags["requested-ref"],
39
+ resolvedRef: flags["resolved-ref"],
40
+ commit: flags.commit,
41
+ }));
42
+ return;
43
+ }
44
+ if (name === "write-track-latest") {
45
+ await writeSelectionState(flags.path, {
46
+ schema_version: 1,
47
+ mode: "track-latest",
48
+ source: flags.source,
49
+ });
50
+ return;
51
+ }
52
+ if (name === "validate-source") {
53
+ validateSource(flags.source);
54
+ return;
55
+ }
56
+ if (name === "display-source") {
57
+ process.stdout.write(`${displaySource(flags.source)}\n`);
58
+ return;
59
+ }
60
+ const unreachable = name;
61
+ throw new Error(`unhandled subcommand: ${String(unreachable)}`);
62
+ }
63
+ export async function runSelectionStateCli(argv) {
64
+ let command;
65
+ try {
66
+ command = parseCommand(argv);
67
+ }
68
+ catch (cause) {
69
+ process.stderr.write(`usage: selection-state-cli <subcommand> [options]\n`);
70
+ process.stderr.write(`error: ${oneLine(cause)}\n`);
71
+ return 2;
72
+ }
73
+ try {
74
+ await dispatch(command);
75
+ return 0;
76
+ }
77
+ catch (cause) {
78
+ process.stderr.write(`error: ${oneLine(cause)}\n`);
79
+ return 1;
80
+ }
81
+ }
82
+ const entry = process.argv[1];
83
+ if (entry !== undefined && import.meta.filename === realpathSync(entry)) {
84
+ process.exitCode = await runSelectionStateCli(process.argv.slice(2));
85
+ }