superpowers-manager 0.7.2 → 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.
- package/dist/harness-command-result.js +3 -2
- package/dist/harnesses/opencode/config.js +105 -48
- package/dist/harnesses/opencode/discovery.js +10 -10
- package/dist/harnesses/opencode/install.js +45 -9
- package/dist/harnesses/opencode/package.js +39 -4
- package/dist/harnesses/opencode/presentation.js +1 -0
- package/dist/harnesses/opencode/state.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
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, [
|
|
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
|
-
|
|
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
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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
|
|
569
|
-
else if (inspectUpstreamSpec(entry.spec, `OPENCODE_CONFIG_CONTENT
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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,12 +1,37 @@
|
|
|
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
|
-
const
|
|
9
|
-
|
|
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 = [
|
|
15
|
+
{
|
|
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,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
// obra/superpowers v6.4.1
|
|
25
|
+
bootstrap: {
|
|
26
|
+
size: 17617,
|
|
27
|
+
sha256: "c979fe5a9fd6fddc9bc9730b34b25989f9d53939eed7d594c4564f6e47495f26",
|
|
28
|
+
},
|
|
29
|
+
entrypoint: {
|
|
30
|
+
size: 536,
|
|
31
|
+
sha256: "f0132fd5339befeb99ba903b1619fd9530969e5b4a1ea8cecf0b0a7f0213a099",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
10
35
|
function isReceiptCompatibility(value) {
|
|
11
36
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
37
|
return false;
|
|
@@ -32,11 +57,21 @@ export async function assessOpenCodeCompatibility(root, selection) {
|
|
|
32
57
|
throw new Error("package metadata");
|
|
33
58
|
requireNoSnapshotDependencies(pkg);
|
|
34
59
|
await validateNativeSkill(root);
|
|
35
|
-
await
|
|
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";
|
|
36
71
|
const official = isOfficialSnapshotSource(selection.effectiveSource);
|
|
37
72
|
return {
|
|
38
73
|
kind: official ? "supported" : "experimental",
|
|
39
|
-
generation
|
|
74
|
+
generation,
|
|
40
75
|
reason: official
|
|
41
76
|
? "qualified upstream-native OpenCode bootstrap"
|
|
42
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("; ")],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superpowers-manager",
|
|
3
|
-
"version": "0.
|
|
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",
|