superpowers-manager 0.6.0 → 0.7.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.
Files changed (75) hide show
  1. package/README.md +42 -26
  2. package/dist/adapter-result.js +5 -9
  3. package/dist/atomic.js +4 -14
  4. package/dist/cli-arguments.js +1 -27
  5. package/dist/cli.js +25 -40
  6. package/dist/commands/adapter-call.js +28 -0
  7. package/dist/commands/install.js +25 -105
  8. package/dist/commands/pin.js +1 -1
  9. package/dist/commands/prepare.js +37 -81
  10. package/dist/commands/probe.js +35 -262
  11. package/dist/commands/track-latest.js +1 -1
  12. package/dist/commands/uninstall.js +15 -91
  13. package/dist/commands/update.js +5 -25
  14. package/dist/effective-selection.js +4 -6
  15. package/dist/git-tree.js +85 -0
  16. package/dist/harness-command-result.js +47 -0
  17. package/dist/harnesses/codex/adapter.js +12 -41
  18. package/dist/harnesses/codex/conflicts.js +2 -3
  19. package/dist/harnesses/codex/hooks.js +1 -1
  20. package/dist/harnesses/codex/json.js +1 -11
  21. package/dist/harnesses/codex/lifecycle.js +0 -48
  22. package/dist/harnesses/codex/manifest-overlay.js +1 -1
  23. package/dist/harnesses/codex/paths.js +14 -29
  24. package/dist/harnesses/codex/presentation.js +2 -23
  25. package/dist/harnesses/codex/publication.js +0 -6
  26. package/dist/harnesses/codex/recovery.js +5 -5
  27. package/dist/harnesses/codex/stored-state.js +1 -1
  28. package/dist/harnesses/opencode/config.js +256 -0
  29. package/dist/harnesses/opencode/discovery.js +617 -0
  30. package/dist/harnesses/opencode/harness.js +37 -0
  31. package/dist/harnesses/opencode/install.js +601 -0
  32. package/dist/harnesses/opencode/native.js +57 -0
  33. package/dist/harnesses/opencode/package.js +89 -0
  34. package/dist/harnesses/opencode/paths.js +38 -0
  35. package/dist/harnesses/opencode/prepare.js +30 -0
  36. package/dist/harnesses/opencode/presentation.js +28 -0
  37. package/dist/harnesses/opencode/state.js +174 -0
  38. package/dist/harnesses/pi/compatibility.js +5 -37
  39. package/dist/harnesses/pi/install.js +7 -5
  40. package/dist/harnesses/pi/native.js +3 -47
  41. package/dist/harnesses/pi/package.js +5 -107
  42. package/dist/harnesses/pi/prepare.js +13 -104
  43. package/dist/harnesses/pi/presentation.js +23 -66
  44. package/dist/harnesses/pi/shared-skills.js +28 -39
  45. package/dist/harnesses/pi/state.js +7 -27
  46. package/dist/provenance.js +2 -2
  47. package/dist/safe-path.js +2 -2
  48. package/dist/selection-store.js +2 -22
  49. package/dist/snapshot-package.js +78 -0
  50. package/dist/snapshot-prepare.js +99 -0
  51. package/dist/snapshot-presentation.js +44 -0
  52. package/dist/validator.js +4 -1
  53. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  54. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  55. package/node_modules/jsonc-parser/README.md +364 -0
  56. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  57. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  58. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  59. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  60. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  61. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  62. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  63. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  64. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  65. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  66. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  67. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  68. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  69. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  70. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  71. package/node_modules/jsonc-parser/package.json +37 -0
  72. package/package.json +8 -4
  73. package/dist/domain/fingerprint.js +0 -4
  74. package/dist/status.js +0 -24
  75. package/dist/validate-generated-plugin-cli.js +0 -118
@@ -58,33 +58,12 @@ function fields(f) {
58
58
  { key: "update_control", label: "update control", value: f.updateControl },
59
59
  ];
60
60
  }
61
- const NO_FACTS = {
62
- harness: "codex",
63
- requestedRef: "",
64
- resolvedRef: "",
65
- desiredCommit: "",
66
- generatedCommit: "",
67
- installedCommit: "",
68
- identityState: "",
69
- status: "",
70
- selectionOrigin: "",
71
- selectionMode: "",
72
- upstreamSourceOrigin: "",
73
- effectiveSource: "",
74
- savedMode: "",
75
- savedSource: "",
76
- savedRequestedRef: "",
77
- savedResolvedRef: "",
78
- savedCommit: "",
79
- updateControl: "",
80
- };
81
- export const PROBE_PORCELAIN_KEYS = fields(NO_FACTS).map((field) => field.key);
82
- export function formatPorcelain(f) {
61
+ function formatPorcelain(f) {
83
62
  return fields(f)
84
63
  .map((field) => `${field.key}=${field.value}\n`)
85
64
  .join("");
86
65
  }
87
- export function formatHuman(f) {
66
+ function formatHuman(f) {
88
67
  let text = fields(f)
89
68
  .map((field) => {
90
69
  const shown = field.value.length === 0 && field.absent !== undefined
@@ -298,9 +298,6 @@ async function nativeUnchangedAfterAttempt(prior, priorActiveDigest, current) {
298
298
  return (await activeDigest(current)) === priorActiveDigest;
299
299
  }
300
300
  async function rollbackPublication(pending, publication, previous, priorNative, priorActiveDigest, activationAttempted, ctx, dependencies) {
301
- if (pending.settled) {
302
- return fail("already-settled", "Codex installation transaction has already been settled", []);
303
- }
304
301
  const messages = [];
305
302
  try {
306
303
  const currentNative = await observeNative(ctx, dependencies, messages);
@@ -358,9 +355,6 @@ async function verifyActivated(pending, dependencies, ctx, messages) {
358
355
  }
359
356
  }
360
357
  async function finalizePublication(pending, publication, dependencies, ctx) {
361
- if (pending.settled) {
362
- return fail("already-settled", "Codex installation transaction has already been settled", []);
363
- }
364
358
  const messages = [];
365
359
  let verified = false;
366
360
  let backupRemoved = false;
@@ -114,7 +114,7 @@ function validateEvidenceShape(record) {
114
114
  throw new Error("incomplete old recovery evidence");
115
115
  }
116
116
  }
117
- async function decodeRecord(bytes, paths) {
117
+ function decodeRecord(bytes, paths) {
118
118
  const parsed = object(parseStrictJson(bytes, PROFILE));
119
119
  if (parsed === null)
120
120
  throw new Error("recovery journal is not an object");
@@ -182,7 +182,7 @@ async function readOwnedRecord(paths) {
182
182
  }
183
183
  const path = recordPath(paths);
184
184
  await assertNoFollowType(path, ["regular-file"]);
185
- return await decodeRecord(await readFile(path), paths);
185
+ return decodeRecord(await readFile(path), paths);
186
186
  }
187
187
  export async function readCodexRecovery(paths) {
188
188
  try {
@@ -247,11 +247,12 @@ export async function beginCodexRecovery(paths, input) {
247
247
  oldDigest: input.oldDigest,
248
248
  oldIdentity: input.oldIdentity,
249
249
  };
250
- await decodeRecord(recordBytes(record), paths);
250
+ const bytes = recordBytes(record);
251
+ decodeRecord(bytes, paths);
251
252
  const path = recordPath(paths);
252
253
  const handle = await open(path, "wx", 0o600);
253
254
  try {
254
- await handle.writeFile(recordBytes(record));
255
+ await handle.writeFile(bytes);
255
256
  await handle.sync();
256
257
  }
257
258
  finally {
@@ -311,7 +312,6 @@ async function requirePending(pending) {
311
312
  !(await readFile(path)).equals(recordBytes(pending.record))) {
312
313
  throw new Error("Codex recovery journal changed");
313
314
  }
314
- await decodeRecord(recordBytes(pending.record), pending.paths);
315
315
  }
316
316
  export async function finishCodexRecovery(pending) {
317
317
  try {
@@ -244,6 +244,6 @@ export async function readCodexStoredState(env, cwd) {
244
244
  managerPluginEnabled: manager?.enabled === true,
245
245
  legacyPluginPresent: legacy?.installed === true,
246
246
  legacyPluginEnabled: legacy?.enabled === true,
247
- installedListingJson: JSON.stringify({ installed }),
247
+ installedPlugins: installed,
248
248
  };
249
249
  }
@@ -0,0 +1,256 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { constants } from "node:fs";
3
+ import { chmod, open } from "node:fs/promises";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { applyEdits, createScanner, findNodeAtLocation, getNodeValue, parseTree, } from "jsonc-parser";
6
+ import * as jsoncParserRuntime from "jsonc-parser";
7
+ import { atomicWriteFile } from "../../atomic.js";
8
+ import { SafetyError } from "../../safety-error.js";
9
+ import { isErrno } from "../../safe-path.js";
10
+ const MAX_CONFIG_BYTES = 1024 * 1024;
11
+ const MAX_CONFIG_DEPTH = 256;
12
+ const UTF8_DECODER = new TextDecoder("utf-8", {
13
+ fatal: true,
14
+ ignoreBOM: true,
15
+ });
16
+ const SYNTAX_KIND = jsoncParserRuntime.SyntaxKind;
17
+ async function readBoundedConfigBytes(handle) {
18
+ const budget = Buffer.allocUnsafe(MAX_CONFIG_BYTES + 1);
19
+ let length = 0;
20
+ while (length < budget.length) {
21
+ const { bytesRead } = await handle.read(budget, length, budget.length - length, null);
22
+ if (bytesRead === 0)
23
+ break;
24
+ length += bytesRead;
25
+ }
26
+ if (length > MAX_CONFIG_BYTES) {
27
+ throw new Error("configuration grew past the byte limit");
28
+ }
29
+ return Buffer.from(budget.subarray(0, length));
30
+ }
31
+ function invalidConfig(path, cause) {
32
+ return new SafetyError("opencode-config", `invalid OpenCode configuration: ${path}`, cause === undefined ? {} : { cause });
33
+ }
34
+ function cannotRead(path, cause) {
35
+ return new SafetyError("opencode-config", `cannot read OpenCode configuration: ${path}`, cause === undefined ? {} : { cause });
36
+ }
37
+ function cannotRemove(path, cause) {
38
+ return new SafetyError("opencode-config", `cannot remove owned OpenCode registration: ${path}`, cause === undefined ? {} : { cause });
39
+ }
40
+ function propertyParts(property) {
41
+ const [key, value] = property.children ?? [];
42
+ if (property.type !== "property" ||
43
+ key?.type !== "string" ||
44
+ typeof key.value !== "string" ||
45
+ value === undefined) {
46
+ throw new Error("malformed property tree");
47
+ }
48
+ return [key, value];
49
+ }
50
+ function validateTree(node, depth) {
51
+ if (node.type !== "object" && node.type !== "array")
52
+ return;
53
+ if (depth > MAX_CONFIG_DEPTH)
54
+ throw new Error("configuration is too deep");
55
+ if (node.type === "object") {
56
+ const names = new Set();
57
+ for (const property of node.children ?? []) {
58
+ const [key, value] = propertyParts(property);
59
+ if (names.has(key.value))
60
+ throw new Error("duplicate key");
61
+ names.add(key.value);
62
+ validateTree(value, depth + 1);
63
+ }
64
+ return;
65
+ }
66
+ for (const child of node.children ?? [])
67
+ validateTree(child, depth + 1);
68
+ }
69
+ function configEntries(root) {
70
+ const plugin = findNodeAtLocation(root, ["plugin"]);
71
+ if (plugin === undefined)
72
+ return [];
73
+ if (plugin.type !== "array")
74
+ throw new Error("plugin must be an array");
75
+ return (plugin.children ?? []).map((entry, index) => {
76
+ if (entry.type === "string" && typeof entry.value === "string") {
77
+ return { index, spec: entry.value, options: null };
78
+ }
79
+ const tuple = entry.children ?? [];
80
+ if (entry.type !== "array" ||
81
+ tuple.length !== 2 ||
82
+ tuple[0]?.type !== "string" ||
83
+ typeof tuple[0].value !== "string" ||
84
+ tuple[1]?.type !== "object") {
85
+ throw new Error("unsupported plugin entry");
86
+ }
87
+ return {
88
+ index,
89
+ spec: tuple[0].value,
90
+ options: structuredClone(getNodeValue(tuple[1])),
91
+ };
92
+ });
93
+ }
94
+ export function parseOpenCodeConfig(text, path) {
95
+ try {
96
+ if (Buffer.byteLength(text, "utf8") > MAX_CONFIG_BYTES) {
97
+ throw new Error("configuration is too large");
98
+ }
99
+ const errors = [];
100
+ const root = parseTree(text, errors, { allowTrailingComma: true });
101
+ if (root === undefined || errors.length !== 0 || root.type !== "object") {
102
+ throw new Error("configuration must be one valid object");
103
+ }
104
+ validateTree(root, 1);
105
+ return { path, text, root, entries: configEntries(root) };
106
+ }
107
+ catch (cause) {
108
+ if (cause instanceof SafetyError)
109
+ throw cause;
110
+ throw invalidConfig(path, cause);
111
+ }
112
+ }
113
+ function sameEntries(left, right) {
114
+ return (left.length === right.length &&
115
+ left.every((entry, index) => entry.spec === right[index]?.spec &&
116
+ isDeepStrictEqual(entry.options, right[index]?.options)));
117
+ }
118
+ export function removeOpenCodeEntry(document, index) {
119
+ try {
120
+ const array = findNodeAtLocation(document.root, ["plugin"]);
121
+ const elements = array?.children ?? [];
122
+ const element = elements[index];
123
+ if (array?.type !== "array" ||
124
+ !Number.isInteger(index) ||
125
+ index < 0 ||
126
+ !element) {
127
+ throw new Error("invalid removal index");
128
+ }
129
+ const previous = elements[index - 1];
130
+ const next = elements[index + 1];
131
+ const gapStart = next || !previous
132
+ ? element.offset + element.length
133
+ : previous.offset + previous.length;
134
+ const gapEnd = next
135
+ ? next.offset
136
+ : previous
137
+ ? element.offset
138
+ : array.offset + array.length - 1;
139
+ const scanner = createScanner(document.text, true);
140
+ scanner.setPosition(gapStart);
141
+ let commaOffset;
142
+ while (scanner.getPosition() < gapEnd) {
143
+ const token = scanner.scan();
144
+ if (token === SYNTAX_KIND.EOF || scanner.getTokenOffset() >= gapEnd)
145
+ break;
146
+ if (token !== SYNTAX_KIND.CommaToken || commaOffset !== undefined) {
147
+ throw new Error("unexpected separator");
148
+ }
149
+ commaOffset = scanner.getTokenOffset();
150
+ }
151
+ if ((previous || next) && commaOffset === undefined) {
152
+ throw new Error("missing separator");
153
+ }
154
+ const edits = [
155
+ { offset: element.offset, length: element.length, content: "" },
156
+ ];
157
+ if (commaOffset !== undefined) {
158
+ edits.push({ offset: commaOffset, length: 1, content: "" });
159
+ }
160
+ const output = applyEdits(document.text, edits);
161
+ const after = parseOpenCodeConfig(output, document.path);
162
+ const expected = document.entries.filter((entry) => entry.index !== index);
163
+ if (!sameEntries(after.entries, expected)) {
164
+ throw new Error("removal changed surviving registrations");
165
+ }
166
+ return output;
167
+ }
168
+ catch (cause) {
169
+ throw cannotRemove(document.path, cause);
170
+ }
171
+ }
172
+ async function readConfigBytes(path) {
173
+ let handle;
174
+ try {
175
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
176
+ const details = await handle.stat();
177
+ if (!details.isFile() || details.size > MAX_CONFIG_BYTES) {
178
+ throw new Error("configuration is not a bounded regular file");
179
+ }
180
+ const bytes = await readBoundedConfigBytes(handle);
181
+ return {
182
+ bytes,
183
+ identity: {
184
+ dev: details.dev,
185
+ ino: details.ino,
186
+ mode: details.mode,
187
+ },
188
+ };
189
+ }
190
+ catch (cause) {
191
+ if (isErrno(cause, "ENOENT"))
192
+ return null;
193
+ throw cannotRead(path, cause);
194
+ }
195
+ finally {
196
+ await handle?.close().catch(() => { });
197
+ }
198
+ }
199
+ function decodeConfig(bytes, path) {
200
+ try {
201
+ return UTF8_DECODER.decode(bytes);
202
+ }
203
+ catch (cause) {
204
+ throw cannotRead(path, cause);
205
+ }
206
+ }
207
+ export async function readOpenCodeConfig(path) {
208
+ const observed = await readConfigBytes(path);
209
+ if (observed === null)
210
+ return null;
211
+ const text = decodeConfig(observed.bytes, path);
212
+ try {
213
+ return {
214
+ document: parseOpenCodeConfig(text, path),
215
+ bytes: observed.bytes,
216
+ identity: observed.identity,
217
+ };
218
+ }
219
+ catch (cause) {
220
+ throw cannotRead(path, cause);
221
+ }
222
+ }
223
+ function sameObservedState(left, right) {
224
+ return (left.dev === right.dev && left.ino === right.ino && left.mode === right.mode);
225
+ }
226
+ export async function removeObservedOpenCodeEntry(observation, index) {
227
+ const path = observation.document.path;
228
+ try {
229
+ const originalText = decodeConfig(observation.bytes, path);
230
+ const original = parseOpenCodeConfig(originalText, path);
231
+ const output = removeOpenCodeEntry(original, index);
232
+ const outputBytes = Buffer.from(output, "utf8");
233
+ const expected = original.entries.filter((entry) => entry.index !== index);
234
+ await atomicWriteFile(path, outputBytes, {
235
+ validate: async (temporary) => {
236
+ await chmod(temporary, observation.identity.mode & 0o7777);
237
+ const current = await readConfigBytes(path);
238
+ if (current === null ||
239
+ !sameObservedState(current.identity, observation.identity) ||
240
+ !current.bytes.equals(observation.bytes)) {
241
+ throw new Error("OpenCode configuration changed concurrently");
242
+ }
243
+ },
244
+ });
245
+ const after = await readOpenCodeConfig(path);
246
+ if (after === null ||
247
+ !after.bytes.equals(outputBytes) ||
248
+ after.identity.mode !== observation.identity.mode ||
249
+ !sameEntries(after.document.entries, expected)) {
250
+ throw new Error("published OpenCode configuration could not be verified");
251
+ }
252
+ }
253
+ catch (cause) {
254
+ throw cannotRemove(path, cause);
255
+ }
256
+ }