superpowers-manager 0.7.3 → 0.8.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.
@@ -34,13 +34,14 @@ export function nativeCommandResult(label, result) {
34
34
  appendCaptured(log, label, "stderr", result.stderr);
35
35
  return successResult(operation, { stdout: result.stdout.text }, log.snapshot());
36
36
  }
37
- export function normalizeSnapshotRuntimeVersion(label, result) {
37
+ export function normalizeSnapshotRuntimeVersion(label, result, strip) {
38
38
  if (!result.outcome.ok)
39
39
  return { status: result.status, outcome: result.outcome };
40
40
  const operation = `${label.toLowerCase()}-runtime`;
41
41
  if (result.status !== 0)
42
42
  return failureResult(operation, "invalid-status", `${label} runtime inspection returned a nonzero status`, [], result.outcome.messages);
43
- const version = result.outcome.result.stdout.trim();
43
+ const reported = result.outcome.result.stdout.trim();
44
+ const version = strip === undefined ? reported : reported.replace(strip, "");
44
45
  if (!SEMVER_RE.test(version))
45
46
  return failureResult(operation, "invalid-version", `${label} runtime inspection returned an invalid version response`, [], result.outcome.messages);
46
47
  return successResult(operation, version, result.outcome.messages);
@@ -2,7 +2,7 @@ import { Buffer } from "node:buffer";
2
2
  import { constants } from "node:fs";
3
3
  import { chmod, open } from "node:fs/promises";
4
4
  import { isDeepStrictEqual } from "node:util";
5
- import { applyEdits, createScanner, findNodeAtLocation, getNodeValue, parseTree, } from "jsonc-parser";
5
+ import { applyEdits, createScanner, findNodeAtLocation, getNodeValue, modify, parseTree, } from "jsonc-parser";
6
6
  import * as jsoncParserRuntime from "jsonc-parser";
7
7
  import { atomicWriteFile } from "../../atomic.js";
8
8
  import { SafetyError } from "../../safety-error.js";
@@ -14,6 +14,7 @@ const UTF8_DECODER = new TextDecoder("utf-8", {
14
14
  ignoreBOM: true,
15
15
  });
16
16
  const SYNTAX_KIND = jsoncParserRuntime.SyntaxKind;
17
+ const CONFIG_KEYS = ["plugin", "plugins"];
17
18
  async function readBoundedConfigBytes(handle) {
18
19
  const budget = Buffer.allocUnsafe(MAX_CONFIG_BYTES + 1);
19
20
  let length = 0;
@@ -37,6 +38,9 @@ function cannotRead(path, cause) {
37
38
  function cannotRemove(path, cause) {
38
39
  return new SafetyError("opencode-config", `cannot remove owned OpenCode registration: ${path}`, cause === undefined ? {} : { cause });
39
40
  }
41
+ function cannotRegister(path, cause) {
42
+ return new SafetyError("opencode-config", `cannot register owned OpenCode registration: ${path}`, cause === undefined ? {} : { cause });
43
+ }
40
44
  function propertyParts(property) {
41
45
  const [key, value] = property.children ?? [];
42
46
  if (property.type !== "property" ||
@@ -67,28 +71,31 @@ function validateTree(node, depth) {
67
71
  validateTree(child, depth + 1);
68
72
  }
69
73
  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
- };
74
+ return CONFIG_KEYS.flatMap((key) => {
75
+ const array = findNodeAtLocation(root, [key]);
76
+ if (array === undefined)
77
+ return [];
78
+ if (array.type !== "array")
79
+ throw new Error(`${key} must be an array`);
80
+ return (array.children ?? []).map((entry, index) => {
81
+ if (entry.type === "string" && typeof entry.value === "string") {
82
+ return { key, index, spec: entry.value, options: null };
83
+ }
84
+ const tuple = entry.children ?? [];
85
+ if (entry.type !== "array" ||
86
+ tuple.length !== 2 ||
87
+ tuple[0]?.type !== "string" ||
88
+ typeof tuple[0].value !== "string" ||
89
+ tuple[1]?.type !== "object") {
90
+ throw new Error("unsupported plugin entry");
91
+ }
92
+ return {
93
+ key,
94
+ index,
95
+ spec: tuple[0].value,
96
+ options: structuredClone(getNodeValue(tuple[1])),
97
+ };
98
+ });
92
99
  });
93
100
  }
94
101
  export function parseOpenCodeConfig(text, path) {
@@ -112,12 +119,13 @@ export function parseOpenCodeConfig(text, path) {
112
119
  }
113
120
  function sameEntries(left, right) {
114
121
  return (left.length === right.length &&
115
- left.every((entry, index) => entry.spec === right[index]?.spec &&
122
+ left.every((entry, index) => entry.key === right[index]?.key &&
123
+ entry.spec === right[index]?.spec &&
116
124
  isDeepStrictEqual(entry.options, right[index]?.options)));
117
125
  }
118
- export function removeOpenCodeEntry(document, index) {
126
+ export function removeOpenCodeEntry(document, key, index) {
119
127
  try {
120
- const array = findNodeAtLocation(document.root, ["plugin"]);
128
+ const array = findNodeAtLocation(document.root, [key]);
121
129
  const elements = array?.children ?? [];
122
130
  const element = elements[index];
123
131
  if (array?.type !== "array" ||
@@ -159,7 +167,7 @@ export function removeOpenCodeEntry(document, index) {
159
167
  }
160
168
  const output = applyEdits(document.text, edits);
161
169
  const after = parseOpenCodeConfig(output, document.path);
162
- const expected = document.entries.filter((entry) => entry.index !== index);
170
+ const expected = document.entries.filter((entry) => entry.key !== key || entry.index !== index);
163
171
  if (!sameEntries(after.entries, expected)) {
164
172
  throw new Error("removal changed surviving registrations");
165
173
  }
@@ -169,6 +177,37 @@ export function removeOpenCodeEntry(document, index) {
169
177
  throw cannotRemove(document.path, cause);
170
178
  }
171
179
  }
180
+ export function addOpenCodeEntry(document, key, spec) {
181
+ try {
182
+ if (spec.trim().length === 0)
183
+ throw new Error("empty registration spec");
184
+ const array = findNodeAtLocation(document.root, [key]);
185
+ if (array !== undefined && array.type !== "array") {
186
+ throw new Error(`${key} must be an array`);
187
+ }
188
+ const position = array?.children?.length ?? 0;
189
+ const edits = modify(document.text, [key, position], spec, {
190
+ isArrayInsertion: true,
191
+ });
192
+ const output = applyEdits(document.text, edits);
193
+ const after = parseOpenCodeConfig(output, document.path);
194
+ const expected = [
195
+ ...document.entries,
196
+ { key, index: position, spec, options: null },
197
+ ].sort((left, right) => left.key === right.key
198
+ ? left.index - right.index
199
+ : left.key < right.key
200
+ ? -1
201
+ : 1);
202
+ if (!sameEntries(after.entries, expected)) {
203
+ throw new Error("registration changed surviving registrations");
204
+ }
205
+ return output;
206
+ }
207
+ catch (cause) {
208
+ throw cannotRegister(document.path, cause);
209
+ }
210
+ }
172
211
  async function readConfigBytes(path) {
173
212
  let handle;
174
213
  try {
@@ -223,34 +262,52 @@ export async function readOpenCodeConfig(path) {
223
262
  function sameObservedState(left, right) {
224
263
  return (left.dev === right.dev && left.ino === right.ino && left.mode === right.mode);
225
264
  }
226
- export async function removeObservedOpenCodeEntry(observation, index) {
265
+ async function writeObservedOpenCodeConfig(observation, outputBytes, expected) {
266
+ const path = observation.document.path;
267
+ await atomicWriteFile(path, outputBytes, {
268
+ validate: async (temporary) => {
269
+ await chmod(temporary, observation.identity.mode & 0o7777);
270
+ const current = await readConfigBytes(path);
271
+ if (current === null ||
272
+ !sameObservedState(current.identity, observation.identity) ||
273
+ !current.bytes.equals(observation.bytes)) {
274
+ throw new Error("OpenCode configuration changed concurrently");
275
+ }
276
+ },
277
+ });
278
+ const after = await readOpenCodeConfig(path);
279
+ if (after === null ||
280
+ !after.bytes.equals(outputBytes) ||
281
+ after.identity.mode !== observation.identity.mode ||
282
+ !sameEntries(after.document.entries, expected)) {
283
+ throw new Error("published OpenCode configuration could not be verified");
284
+ }
285
+ }
286
+ export async function removeObservedOpenCodeEntry(observation, key, index) {
227
287
  const path = observation.document.path;
228
288
  try {
229
289
  const originalText = decodeConfig(observation.bytes, path);
230
290
  const original = parseOpenCodeConfig(originalText, path);
231
- const output = removeOpenCodeEntry(original, index);
291
+ const output = removeOpenCodeEntry(original, key, index);
232
292
  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
- }
293
+ const expected = original.entries.filter((entry) => entry.key !== key || entry.index !== index);
294
+ await writeObservedOpenCodeConfig(observation, outputBytes, expected);
252
295
  }
253
296
  catch (cause) {
254
297
  throw cannotRemove(path, cause);
255
298
  }
256
299
  }
300
+ export async function addObservedOpenCodeEntry(observation, key, spec) {
301
+ const path = observation.document.path;
302
+ try {
303
+ const originalText = decodeConfig(observation.bytes, path);
304
+ const original = parseOpenCodeConfig(originalText, path);
305
+ const output = addOpenCodeEntry(original, key, spec);
306
+ const outputBytes = Buffer.from(output, "utf8");
307
+ const expected = parseOpenCodeConfig(output, path).entries;
308
+ await writeObservedOpenCodeConfig(observation, outputBytes, expected);
309
+ }
310
+ catch (cause) {
311
+ throw cannotRegister(path, cause);
312
+ }
313
+ }
@@ -151,11 +151,11 @@ function isKnownPluginFile(path) {
151
151
  }
152
152
  async function inspectPluginEntry(observation, entry, state, allowManaged = true, active = true) {
153
153
  if (entry.spec.trim().length === 0) {
154
- addBlocked(state, `${observation.document.path} plugin[${entry.index}]`);
154
+ addBlocked(state, `${observation.document.path} ${entry.key}[${entry.index}]`);
155
155
  return;
156
156
  }
157
157
  if (containsSubstitution(entry.spec)) {
158
- addBlocked(state, `${observation.document.path} plugin[${entry.index}]`);
158
+ addBlocked(state, `${observation.document.path} ${entry.key}[${entry.index}]`);
159
159
  return;
160
160
  }
161
161
  const local = localPluginPath(entry.spec, observation.document.path, state);
@@ -175,14 +175,14 @@ async function inspectPluginEntry(observation, entry, state, allowManaged = true
175
175
  }
176
176
  if (!active)
177
177
  return;
178
- if (inspectUpstreamSpec(entry.spec, `${observation.document.path} plugin[${entry.index}]`, state))
178
+ if (inspectUpstreamSpec(entry.spec, `${observation.document.path} ${entry.key}[${entry.index}]`, state))
179
179
  return;
180
180
  if (local === null)
181
181
  return;
182
182
  const kind = await classifyPathNoFollow(local);
183
183
  if (kind === "missing") {
184
184
  if (/^superpowers(?:\.(?:js|ts))?$/u.test(basename(local)))
185
- addBlocked(state, `${observation.document.path} plugin[${entry.index}]`);
185
+ addBlocked(state, `${observation.document.path} ${entry.key}[${entry.index}]`);
186
186
  return;
187
187
  }
188
188
  if (isKnownPluginFile(local)) {
@@ -190,7 +190,7 @@ async function inspectPluginEntry(observation, entry, state, allowManaged = true
190
190
  return;
191
191
  }
192
192
  if (kind !== "directory" && kind !== "symlink") {
193
- addBlocked(state, `${observation.document.path} plugin[${entry.index}]`);
193
+ addBlocked(state, `${observation.document.path} ${entry.key}[${entry.index}]`);
194
194
  return;
195
195
  }
196
196
  try {
@@ -200,7 +200,7 @@ async function inspectPluginEntry(observation, entry, state, allowManaged = true
200
200
  }
201
201
  catch {
202
202
  if (basename(local) === "superpowers")
203
- addBlocked(state, `${observation.document.path} plugin[${entry.index}]`);
203
+ addBlocked(state, `${observation.document.path} ${entry.key}[${entry.index}]`);
204
204
  }
205
205
  }
206
206
  async function inspectSkillFile(path, label, state, requireKnownIdentity = false) {
@@ -268,7 +268,7 @@ async function inspectDocument(observation, state) {
268
268
  await inspectConfigFields(effective.root, source, state);
269
269
  const active = !truthy(state.env.OPENCODE_PURE);
270
270
  for (const entry of effective.entries) {
271
- const original = observation.document.entries[entry.index];
271
+ const original = observation.document.entries.find((candidate) => candidate.key === entry.key && candidate.index === entry.index);
272
272
  const stable = original !== undefined && original.spec === entry.spec;
273
273
  await inspectPluginEntry(observation, stable ? original : entry, state, stable && isNativeWriterOrigin(observation, state), active);
274
274
  }
@@ -565,8 +565,8 @@ export async function inspectOpenCodeDiscovery(paths, env, cwd) {
565
565
  for (const entry of inline.entries) {
566
566
  if (entry.spec.trim().length === 0 ||
567
567
  containsSubstitution(entry.spec))
568
- addBlocked(state, `OPENCODE_CONFIG_CONTENT plugin[${entry.index}]`);
569
- else if (inspectUpstreamSpec(entry.spec, `OPENCODE_CONFIG_CONTENT plugin[${entry.index}]`, state)) {
568
+ addBlocked(state, `OPENCODE_CONFIG_CONTENT ${entry.key}[${entry.index}]`);
569
+ else if (inspectUpstreamSpec(entry.spec, `OPENCODE_CONFIG_CONTENT ${entry.key}[${entry.index}]`, state)) {
570
570
  continue;
571
571
  }
572
572
  else {
@@ -575,7 +575,7 @@ export async function inspectOpenCodeDiscovery(paths, env, cwd) {
575
575
  const canonical = await canonicalizeProspectivePath(local);
576
576
  addBlocked(state, canonical === state.installedRoot
577
577
  ? `${OPEN_CODE_UNOWNED_MANAGER_INPUT}: OPENCODE_CONFIG_CONTENT`
578
- : `OPENCODE_CONFIG_CONTENT plugin[${entry.index}]`);
578
+ : `OPENCODE_CONFIG_CONTENT ${entry.key}[${entry.index}]`);
579
579
  }
580
580
  }
581
581
  }
@@ -6,7 +6,7 @@ import { failureResult, successResult, } from "../../adapter-result.js";
6
6
  import { digestArtifactTree } from "../../artifact-tree.js";
7
7
  import { atomicWriteFile, beginDirectoryPublication, } from "../../atomic.js";
8
8
  import { assertNoFollowType, canonicalizeProspectivePath, classifyPathNoFollow, } from "../../safe-path.js";
9
- import { removeObservedOpenCodeEntry } from "./config.js";
9
+ import { addObservedOpenCodeEntry, removeObservedOpenCodeEntry, } from "./config.js";
10
10
  import { inspectOpenCodeDiscovery, OPEN_CODE_PURE_MODE_INPUT, } from "./discovery.js";
11
11
  import { normalizeSnapshotRuntimeVersion } from "../../harness-command-result.js";
12
12
  import { runOpenCode } from "./native.js";
@@ -18,6 +18,8 @@ const DEFAULTS = {
18
18
  run: runOpenCode,
19
19
  beginPublication: beginDirectoryPublication,
20
20
  };
21
+ // V1 prints a bare semver ("1.18.31"); V2 prints "opencode v2.0.10".
22
+ const OPEN_CODE_VERSION_PREFIX = /^opencode\s+v/i;
21
23
  // The journal can contain data from two independently bounded 1 MiB receipts
22
24
  // plus one bounded 1 MiB config. Six bytes of JSON output per input byte covers
23
25
  // control escaping; counting the registration spec independently brings that
@@ -122,13 +124,14 @@ function registrationRecord(registration) {
122
124
  return null;
123
125
  const observation = registration.observation;
124
126
  const node = findNodeAtLocation(observation.document.root, [
125
- "plugin",
127
+ registration.entry.key,
126
128
  registration.entry.index,
127
129
  ]);
128
130
  if (node === undefined)
129
131
  throw new Error("OpenCode registration syntax changed");
130
132
  return {
131
133
  configPath: observation.document.path,
134
+ configKey: registration.entry.key,
132
135
  entryIndex: registration.entry.index,
133
136
  spec: registration.entry.spec,
134
137
  optionsDigest: createHash("sha256")
@@ -148,6 +151,7 @@ function sameRegistration(registration, expected) {
148
151
  ? registration === undefined
149
152
  : registration !== undefined &&
150
153
  registration.observation.document.path === expected.configPath &&
154
+ registration.entry.key === expected.configKey &&
151
155
  registration.entry.spec === expected.spec &&
152
156
  createHash("sha256")
153
157
  .update(JSON.stringify(registration.entry.options))
@@ -157,6 +161,7 @@ function registrationInputSame(input, current) {
157
161
  return input === null
158
162
  ? current === null
159
163
  : current !== null &&
164
+ input.key === current.key &&
160
165
  input.entryIndex === current.entryIndex &&
161
166
  input.spec === current.spec &&
162
167
  input.observation.document.path === current.observation.document.path &&
@@ -348,7 +353,7 @@ async function rollbackOpenCodePublication(pending, publication) {
348
353
  const current = observed.managedEntries[0];
349
354
  if (current === undefined)
350
355
  throw new Error("OpenCode created registration missing");
351
- await removeObservedOpenCodeEntry(current.observation, current.entry.index);
356
+ await removeObservedOpenCodeEntry(current.observation, current.entry.key, current.entry.index);
352
357
  observed = await discovery(pending.paths, pending.ctx);
353
358
  requireNoOwnedActivation(observed);
354
359
  }
@@ -416,7 +421,13 @@ export async function installOpenCode(artifact, ctx, deps = DEFAULTS) {
416
421
  let priorRegistration = registrationRecord(observed.managedEntries[0]);
417
422
  if (previous === null && priorRegistration !== null)
418
423
  throw new Error("unowned OpenCode registration");
419
- accepted(normalizeSnapshotRuntimeVersion("OpenCode", await deps.run(["--version"], paths, ctx)));
424
+ const runtimeVersion = accepted(normalizeSnapshotRuntimeVersion("OpenCode", await deps.run(["--version"], paths, ctx), OPEN_CODE_VERSION_PREFIX));
425
+ const major = Number.parseInt(runtimeVersion, 10);
426
+ if (major !== 1 && major !== 2)
427
+ return fail("unsupported-runtime", `unsupported OpenCode major version ${major}`);
428
+ if (major === 2 &&
429
+ assessment.compatibility.generation !== "opencode-native-bootstrap-v2")
430
+ return fail("unsupported-artifact", "the selected upstream ref does not support OpenCode 2; select a ref that ships the V2 entrypoint");
420
431
  await requireSnapshot(paths.installedRoot, previous);
421
432
  observed = await requireStableActivationRegistration(paths, ctx, priorRegistration);
422
433
  priorRegistration = registrationRecord(observed.managedEntries[0]);
@@ -443,16 +454,40 @@ export async function installOpenCode(artifact, ctx, deps = DEFAULTS) {
443
454
  if (priorRegistration === null) {
444
455
  await phase(pending, "registering");
445
456
  observed = await requireStableActivationRegistration(paths, ctx, null);
446
- const native = await deps
447
- .run(["plugin", pending.canonicalRoot, "--global"], paths, ctx)
448
- .catch(() => fail("native-failed", "OpenCode native installation failed"));
457
+ let native = null;
458
+ if (major === 1) {
459
+ native = await deps
460
+ .run(["plugin", pending.canonicalRoot, "--global"], paths, ctx)
461
+ .catch(() => fail("native-failed", "OpenCode native installation failed"));
462
+ }
463
+ else {
464
+ const jsonc = join(paths.configRoot, "opencode.jsonc");
465
+ const json = join(paths.configRoot, "opencode.json");
466
+ let target = observed.documents.find((document) => document.document.path === jsonc) ??
467
+ observed.documents.find((document) => document.document.path === json);
468
+ if (target === undefined) {
469
+ await atomicWriteFile(json, Buffer.from("{}\n"), {
470
+ validate: async () => {
471
+ await assertNoFollowType(paths.configRoot, ["directory"]);
472
+ await assertNoFollowType(jsonc, ["missing"]);
473
+ await assertNoFollowType(json, ["missing"]);
474
+ },
475
+ });
476
+ observed = await requireStableActivationRegistration(paths, ctx, null);
477
+ target = observed.documents.find((document) => document.document.path === json);
478
+ }
479
+ if (target === undefined)
480
+ throw new Error("no OpenCode configuration to register in");
481
+ await addObservedOpenCodeEntry(target, "plugins", pending.canonicalRoot);
482
+ }
449
483
  observed = await discovery(paths, ctx);
450
484
  requireSafeActivationDiscovery(observed);
451
485
  const created = registrationRecord(observed.managedEntries[0]);
452
486
  if (created === null || created.spec !== pending.canonicalRoot)
453
487
  throw new Error("OpenCode activation unverified");
454
488
  await phase(pending, "registered", { createdRegistration: created });
455
- accepted(native);
489
+ if (native !== null)
490
+ accepted(native);
456
491
  }
457
492
  await requirePublication(pending);
458
493
  observed = await discovery(paths, ctx);
@@ -524,6 +559,7 @@ export async function removeOpenCode(input, ctx, deps = DEFAULTS) {
524
559
  ? null
525
560
  : {
526
561
  observation: registered.observation,
562
+ key: registered.entry.key,
527
563
  entryIndex: registered.entry.index,
528
564
  spec: registered.entry.spec,
529
565
  }))
@@ -546,7 +582,7 @@ export async function removeOpenCode(input, ctx, deps = DEFAULTS) {
546
582
  if (!sameRegistration(observed.managedEntries[0], pending.journal.priorRegistration))
547
583
  throw new Error("OpenCode registration changed");
548
584
  if (registered !== undefined) {
549
- await removeObservedOpenCodeEntry(registered.observation, registered.entry.index);
585
+ await removeObservedOpenCodeEntry(registered.observation, registered.entry.key, registered.entry.index);
550
586
  observed = await discovery(paths, ctx);
551
587
  requireNoOwnedActivation(observed);
552
588
  deregistered = true;
@@ -1,23 +1,35 @@
1
1
  import { join } from "node:path";
2
2
  import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
3
3
  import { COMMIT_RE, SEMVER_RE } from "../../domain/refs.js";
4
+ import { assertNoFollowType } from "../../safe-path.js";
4
5
  import { validateSource } from "../../selection.js";
5
6
  import { SafetyError } from "../../safety-error.js";
6
7
  import { isOfficialSnapshotSource, requireNoSnapshotDependencies, requireSnapshotBootstrap, snapshotReceiptBinding, } from "../../snapshot-package.js";
7
8
  const BOOTSTRAP = ".opencode/plugins/superpowers.js";
8
- // Exact-byte allowlist: every upstream bootstrap change needs re-qualification
9
- // in the native container before its bytes are added here. Revisit the policy
10
- // if upstream churn makes that a per-release chore.
11
- const QUALIFIED_BOOTSTRAPS = [
12
- // obra/superpowers v6.0.0-v6.3.0
9
+ const ENTRYPOINT = "index.js";
10
+ // Exact-byte allowlist, matched as a unit: a bootstrap and the V2 entrypoint
11
+ // that shipped with it were qualified together in the native container. Adding
12
+ // either half alone would admit a combination no container ever ran. Every
13
+ // upstream change needs re-qualification before its bytes are added here.
14
+ const QUALIFIED_PROFILES = [
13
15
  {
14
- size: 5464,
15
- sha256: "a5c5e1dbb0abfbd6ec3322b724b9a7b3318bbbb3d83f9a661c56ae0ed0a3adb8",
16
+ // obra/superpowers v6.0.0-v6.3.0 — no V2 entrypoint shipped
17
+ bootstrap: {
18
+ size: 5464,
19
+ sha256: "a5c5e1dbb0abfbd6ec3322b724b9a7b3318bbbb3d83f9a661c56ae0ed0a3adb8",
20
+ },
21
+ entrypoint: null,
16
22
  },
17
- // obra/superpowers v6.4.1
18
23
  {
19
- size: 17617,
20
- sha256: "c979fe5a9fd6fddc9bc9730b34b25989f9d53939eed7d594c4564f6e47495f26",
24
+ // obra/superpowers v6.4.1
25
+ bootstrap: {
26
+ size: 17617,
27
+ sha256: "c979fe5a9fd6fddc9bc9730b34b25989f9d53939eed7d594c4564f6e47495f26",
28
+ },
29
+ entrypoint: {
30
+ size: 536,
31
+ sha256: "f0132fd5339befeb99ba903b1619fd9530969e5b4a1ea8cecf0b0a7f0213a099",
32
+ },
21
33
  },
22
34
  ];
23
35
  function isReceiptCompatibility(value) {
@@ -45,11 +57,21 @@ export async function assessOpenCodeCompatibility(root, selection) {
45
57
  throw new Error("package metadata");
46
58
  requireNoSnapshotDependencies(pkg);
47
59
  await validateNativeSkill(root);
48
- await Promise.any(QUALIFIED_BOOTSTRAPS.map(({ size, sha256 }) => requireSnapshotBootstrap(root, BOOTSTRAP, size, sha256)));
60
+ const matched = await Promise.any(QUALIFIED_PROFILES.map(async (profile) => {
61
+ await requireSnapshotBootstrap(root, BOOTSTRAP, profile.bootstrap.size, profile.bootstrap.sha256);
62
+ if (profile.entrypoint === null)
63
+ await assertNoFollowType(join(root, ENTRYPOINT), ["missing"]);
64
+ else
65
+ await requireSnapshotBootstrap(root, ENTRYPOINT, profile.entrypoint.size, profile.entrypoint.sha256);
66
+ return profile;
67
+ }));
68
+ const generation = matched.entrypoint === null
69
+ ? "opencode-native-bootstrap-v1"
70
+ : "opencode-native-bootstrap-v2";
49
71
  const official = isOfficialSnapshotSource(selection.effectiveSource);
50
72
  return {
51
73
  kind: official ? "supported" : "experimental",
52
- generation: "opencode-native-bootstrap-v1",
74
+ generation,
53
75
  reason: official
54
76
  ? "qualified upstream-native OpenCode bootstrap"
55
77
  : "qualified OpenCode mechanics from a custom source",
@@ -10,6 +10,7 @@ const renderProbe = (facts) => {
10
10
  ["prepared_identity", facts.prepared.observedIdentity],
11
11
  ["installed_identity", facts.installed.observedIdentity],
12
12
  ["installation_state", facts.installed.kind],
13
+ ["registration_key", facts.ownership.removalInput.registration?.key ?? ""],
13
14
  ["resource_state", facts.resourceState ?? "idle"],
14
15
  ["ownership", facts.ownership.presentationValue],
15
16
  ["conflicts", (facts.ownership.presentationConflicts ?? []).join("; ")],
@@ -52,6 +52,7 @@ function registrationFor(facts) {
52
52
  ? null
53
53
  : {
54
54
  observation: managed.observation,
55
+ key: managed.entry.key,
55
56
  entryIndex: managed.entry.index,
56
57
  spec: managed.entry.spec,
57
58
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superpowers-manager",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Install obra/superpowers directly from upstream for Codex, Pi, and OpenCode. Stay on a known-good release or commit, try a branch, and upgrade when you choose.",
5
5
  "keywords": [
6
6
  "superpowers",