superpowers-manager 0.5.2 → 0.7.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 (83) hide show
  1. package/README.md +44 -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 +32 -52
  6. package/dist/commands/adapter-call.js +28 -0
  7. package/dist/commands/install.js +26 -107
  8. package/dist/commands/mutation.js +7 -0
  9. package/dist/commands/pin.js +1 -1
  10. package/dist/commands/prepare.js +39 -129
  11. package/dist/commands/probe.js +35 -264
  12. package/dist/commands/track-latest.js +1 -1
  13. package/dist/commands/uninstall.js +16 -92
  14. package/dist/commands/update.js +5 -25
  15. package/dist/effective-selection.js +7 -9
  16. package/dist/git-tree.js +85 -0
  17. package/dist/harness-command-result.js +47 -0
  18. package/dist/harnesses/codex/adapter.js +231 -510
  19. package/dist/harnesses/codex/compatibility.js +2 -17
  20. package/dist/harnesses/codex/conflicts.js +2 -3
  21. package/dist/harnesses/codex/harness.js +18 -263
  22. package/dist/harnesses/codex/hooks.js +1 -1
  23. package/dist/harnesses/codex/json.js +1 -11
  24. package/dist/harnesses/codex/lifecycle.js +80 -72
  25. package/dist/harnesses/codex/manifest-overlay.js +23 -18
  26. package/dist/harnesses/codex/paths.js +14 -29
  27. package/dist/harnesses/codex/presentation.js +8 -37
  28. package/dist/harnesses/codex/publication.js +13 -37
  29. package/dist/harnesses/codex/recovery.js +5 -5
  30. package/dist/harnesses/codex/stored-state.js +1 -1
  31. package/dist/harnesses/opencode/config.js +256 -0
  32. package/dist/harnesses/opencode/discovery.js +587 -0
  33. package/dist/harnesses/opencode/harness.js +37 -0
  34. package/dist/harnesses/opencode/install.js +601 -0
  35. package/dist/harnesses/opencode/native.js +57 -0
  36. package/dist/harnesses/opencode/package.js +89 -0
  37. package/dist/harnesses/opencode/paths.js +38 -0
  38. package/dist/harnesses/opencode/prepare.js +30 -0
  39. package/dist/harnesses/opencode/presentation.js +28 -0
  40. package/dist/harnesses/opencode/state.js +174 -0
  41. package/dist/harnesses/pi/compatibility.js +5 -37
  42. package/dist/harnesses/pi/install.js +7 -5
  43. package/dist/harnesses/pi/native.js +3 -47
  44. package/dist/harnesses/pi/package.js +5 -107
  45. package/dist/harnesses/pi/prepare.js +13 -104
  46. package/dist/harnesses/pi/presentation.js +23 -66
  47. package/dist/harnesses/pi/shared-skills.js +28 -39
  48. package/dist/harnesses/pi/state.js +7 -27
  49. package/dist/provenance.js +2 -2
  50. package/dist/python-json-format.js +0 -69
  51. package/dist/python-json.js +2 -27
  52. package/dist/python-text.js +9 -69
  53. package/dist/safe-path.js +2 -2
  54. package/dist/selection-store.js +2 -22
  55. package/dist/snapshot-package.js +78 -0
  56. package/dist/snapshot-prepare.js +99 -0
  57. package/dist/snapshot-presentation.js +44 -0
  58. package/dist/validator.js +39 -73
  59. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  60. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  61. package/node_modules/jsonc-parser/README.md +364 -0
  62. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  63. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  64. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  65. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  66. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  67. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  68. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  69. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  70. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  71. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  72. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  73. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  74. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  75. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  76. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  77. package/node_modules/jsonc-parser/package.json +37 -0
  78. package/package.json +8 -4
  79. package/dist/domain/fingerprint.js +0 -4
  80. package/dist/selection-state-cli.js +0 -85
  81. package/dist/status.js +0 -24
  82. package/dist/upstream-cli.js +0 -110
  83. package/dist/validate-generated-plugin-cli.js +0 -118
@@ -1,5 +1,5 @@
1
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
2
- import { assertNoFollowType, canonicalizeProspectivePath, } from "../../safe-path.js";
1
+ import { join, resolve } from "node:path";
2
+ import { assertNoFollowType, canonicalizeProspectivePath, isContained, } from "../../safe-path.js";
3
3
  import { SafetyError } from "../../safety-error.js";
4
4
  export function codexHome(env, cwd) {
5
5
  const configured = env.CODEX_HOME;
@@ -26,15 +26,18 @@ export function codexPaths(env, cwd) {
26
26
  };
27
27
  }
28
28
  function overlaps(a, b) {
29
- const inside = (root, leaf) => {
30
- const suffix = relative(root, leaf);
31
- return (suffix === "" ||
32
- (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
33
- };
34
- return inside(a, b) || inside(b, a);
29
+ return isContained(a, b) || isContained(b, a);
30
+ }
31
+ class CodexPathError extends SafetyError {
32
+ constructor(message, details, cause) {
33
+ super("codex-paths", message, {
34
+ details,
35
+ ...(cause === undefined ? {} : { cause }),
36
+ });
37
+ }
35
38
  }
36
39
  function separationError() {
37
- return new SafetyError("codex-paths", "preparation overlaps Codex published or recovery storage", { details: { kind: "overlap" } });
40
+ return new CodexPathError("preparation overlaps Codex published or recovery storage", { kind: "overlap" });
38
41
  }
39
42
  async function inspectRoot(path, root) {
40
43
  try {
@@ -42,29 +45,11 @@ async function inspectRoot(path, root) {
42
45
  return await canonicalizeProspectivePath(path);
43
46
  }
44
47
  catch (cause) {
45
- throw new SafetyError("codex-paths", `cannot inspect ${root} root: ${path}`, { cause, details: { kind: "inspection", root, path } });
48
+ throw new CodexPathError(`cannot inspect ${root} root: ${path}`, { kind: "inspection", root, path }, cause);
46
49
  }
47
50
  }
48
51
  export function codexPathFailureDetails(cause) {
49
- if (!(cause instanceof SafetyError) || cause.module !== "codex-paths") {
50
- return null;
51
- }
52
- const details = cause.details;
53
- if (details === null || typeof details !== "object" || !("kind" in details)) {
54
- return null;
55
- }
56
- if (details.kind === "overlap")
57
- return { kind: "overlap" };
58
- if (details.kind !== "inspection" ||
59
- !("root" in details) ||
60
- !("path" in details) ||
61
- (details.root !== "preparation" &&
62
- details.root !== "marketplace" &&
63
- details.root !== "recovery") ||
64
- typeof details.path !== "string") {
65
- return null;
66
- }
67
- return { kind: "inspection", root: details.root, path: details.path };
52
+ return cause instanceof CodexPathError ? (cause.details ?? null) : null;
68
53
  }
69
54
  export async function assertCodexPreparationSeparate(preparedRoot, paths) {
70
55
  const recovery = await inspectRoot(paths.recoveryRoot, "recovery");
@@ -1,5 +1,5 @@
1
- import { normalizeCodexInstall, normalizeCodexInstalled } from "./harness.js";
2
1
  import { displaySource } from "../../selection.js";
2
+ import { hasTerminalControl } from "../../adapter-result.js";
3
3
  // One ordered table drives both formats, preserving the public human and
4
4
  // porcelain field order exposed through the harness presentation.
5
5
  function fields(f) {
@@ -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
@@ -110,23 +89,15 @@ function verificationOutput(kind, hint) {
110
89
  stderr.push(`hint: ${hint}`);
111
90
  return { stdout: [], stderr };
112
91
  }
92
+ function safeHint(value) {
93
+ return typeof value === "string" && !hasTerminalControl(value) ? value : "";
94
+ }
113
95
  export function codexInstallReceipt(missingHint, mismatchHint) {
114
96
  return {
115
- missingVerificationOutput: verificationOutput("missing", missingHint),
116
- mismatchVerificationOutput: verificationOutput("mismatch", mismatchHint),
97
+ missingVerificationOutput: verificationOutput("missing", safeHint(missingHint)),
98
+ mismatchVerificationOutput: verificationOutput("mismatch", safeHint(mismatchHint)),
117
99
  };
118
100
  }
119
- // Compatibility export target for src/harnesses/codex/lifecycle.ts. Delegate to the production
120
- // normalizers and renderer so the retained verdict cannot drift from commands.
121
- export function verifyInstalledFingerprint(desiredCommit, installResult, inspectResult) {
122
- const receipt = normalizeCodexInstall(installResult);
123
- const inspection = normalizeCodexInstalled(inspectResult, desiredCommit);
124
- const output = codexPresentation.renderInstallVerification(desiredCommit, receipt, inspection);
125
- const ok = inspection.status === 0 &&
126
- inspection.outcome.ok &&
127
- inspection.outcome.result.kind === "current";
128
- return { ok, stdout: output.stdout, stderr: output.stderr };
129
- }
130
101
  function probeFacts(facts) {
131
102
  const selection = facts.selection;
132
103
  const saved = selection.saved;
@@ -3,9 +3,8 @@ import { failureResult, successResult, } from "../../adapter-result.js";
3
3
  import { beginDirectoryPublication, } from "../../atomic.js";
4
4
  import { digestArtifactTree } from "../../artifact-tree.js";
5
5
  import { assertNoFollowType, classifyPathNoFollow } from "../../safe-path.js";
6
- import { codexInspect, codexInstallRefreshMode, codexRemove, codexReadNativeState, } from "./adapter.js";
6
+ import { codexInspectControl, codexInspectOwnership, codexInstallRefreshMode, codexRemove, codexReadNativeState, } from "./adapter.js";
7
7
  import { readCodexAssessment } from "./compatibility.js";
8
- import { requireManagedUpdateControl, requireNoLegacyState, } from "./lifecycle.js";
9
8
  import { readCodexMarketplace, stageCodexMarketplace, } from "./marketplace.js";
10
9
  import { assertCodexPreparationSeparate, codexPathFailureDetails, codexPaths, } from "./paths.js";
11
10
  import { codexPreparationLocation } from "./prepare.js";
@@ -13,7 +12,8 @@ import { beginCodexRecovery, finishCodexRecovery, readCodexRecovery, verifyCodex
13
12
  import { hasFilesystemAccessFailure, pathsEqual } from "./state.js";
14
13
  const DEFAULTS = {
15
14
  readNative: codexReadNativeState,
16
- inspectNative: codexInspect,
15
+ inspectOwnership: codexInspectOwnership,
16
+ inspectControl: codexInspectControl,
17
17
  beginPublication: beginDirectoryPublication,
18
18
  };
19
19
  const REMOVAL_DEFAULTS = {
@@ -26,16 +26,6 @@ function fail(code, message, messages) {
26
26
  function removalFailure(code, message, messages) {
27
27
  return failureResult("uninstall-codex", code, message, [], messages);
28
28
  }
29
- function objectResult(result) {
30
- if (result.status !== 0 ||
31
- !result.outcome.ok ||
32
- result.outcome.result === null ||
33
- typeof result.outcome.result !== "object" ||
34
- Array.isArray(result.outcome.result)) {
35
- throw new Error("Codex inspection failed");
36
- }
37
- return result.outcome.result;
38
- }
39
29
  function acceptedNative(result) {
40
30
  if (result.status !== 0 || !result.outcome.ok) {
41
31
  throw new Error("Codex native observation failed");
@@ -54,26 +44,18 @@ function acceptedNative(result) {
54
44
  return value;
55
45
  }
56
46
  function requireOwnership(result) {
57
- const value = objectResult(result);
58
- if (typeof value.identity_state !== "string" ||
59
- requireNoLegacyState(value.identity_state).kind !== "ok" ||
60
- !Array.isArray(value.conflicts) ||
61
- value.conflicts.length !== 0) {
62
- throw new Error("Codex ownership blocks publication");
47
+ if (result.status !== 0 || !result.outcome.ok) {
48
+ throw new Error("Codex inspection failed");
63
49
  }
64
- const resources = value.resources;
65
- if (typeof resources !== "object" ||
66
- resources === null ||
67
- Array.isArray(resources) ||
68
- typeof resources.plugin !== "boolean" ||
69
- typeof resources.marketplace !== "boolean") {
70
- throw new Error("Codex ownership inspection is malformed");
50
+ if (result.outcome.result?.installEligibility?.kind !== "allowed") {
51
+ throw new Error("Codex ownership blocks publication");
71
52
  }
72
53
  }
73
54
  function requireControl(result) {
74
- const value = objectResult(result);
75
- if (typeof value.update_control !== "string" ||
76
- !requireManagedUpdateControl(value.update_control).ok) {
55
+ if (result.status !== 0 || !result.outcome.ok) {
56
+ throw new Error("Codex inspection failed");
57
+ }
58
+ if (result.outcome.result?.mutationEligibility?.kind !== "allowed") {
77
59
  throw new Error("Codex update control blocks publication");
78
60
  }
79
61
  }
@@ -262,10 +244,10 @@ async function observeNative(ctx, dependencies, messages) {
262
244
  return acceptedNative(result);
263
245
  }
264
246
  async function requireEligibility(ctx, dependencies, messages) {
265
- const ownership = await dependencies.inspectNative("ownership", ctx);
247
+ const ownership = await dependencies.inspectOwnership(ctx);
266
248
  messages.push(...ownership.outcome.messages);
267
249
  requireOwnership(ownership);
268
- const control = await dependencies.inspectNative("update-control", ctx);
250
+ const control = await dependencies.inspectControl(ctx);
269
251
  messages.push(...control.outcome.messages);
270
252
  requireControl(control);
271
253
  }
@@ -316,9 +298,6 @@ async function nativeUnchangedAfterAttempt(prior, priorActiveDigest, current) {
316
298
  return (await activeDigest(current)) === priorActiveDigest;
317
299
  }
318
300
  async function rollbackPublication(pending, publication, previous, priorNative, priorActiveDigest, activationAttempted, ctx, dependencies) {
319
- if (pending.settled) {
320
- return fail("already-settled", "Codex installation transaction has already been settled", []);
321
- }
322
301
  const messages = [];
323
302
  try {
324
303
  const currentNative = await observeNative(ctx, dependencies, messages);
@@ -376,9 +355,6 @@ async function verifyActivated(pending, dependencies, ctx, messages) {
376
355
  }
377
356
  }
378
357
  async function finalizePublication(pending, publication, dependencies, ctx) {
379
- if (pending.settled) {
380
- return fail("already-settled", "Codex installation transaction has already been settled", []);
381
- }
382
358
  const messages = [];
383
359
  let verified = false;
384
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
+ }