superpowers-manager 0.7.3 → 0.9.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/README.md +36 -16
- package/dist/atomic.js +1 -3
- package/dist/cli-arguments.js +4 -1
- package/dist/cli.js +14 -43
- package/dist/commands/adapter-call.js +24 -0
- package/dist/commands/install.js +16 -63
- package/dist/commands/mutation.js +5 -5
- package/dist/commands/pin.js +10 -5
- package/dist/commands/prepare.js +42 -108
- package/dist/commands/probe.js +33 -19
- package/dist/commands/uninstall.js +18 -46
- package/dist/commands/unpin.js +2 -1
- package/dist/commands/update.js +8 -29
- package/dist/domain/refs.js +0 -3
- package/dist/effective-selection.js +11 -15
- package/dist/git-tree.js +4 -6
- package/dist/harness-command-result.js +36 -5
- package/dist/harnesses/claude-code/harness.js +39 -0
- package/dist/harnesses/claude-code/install.js +380 -0
- package/dist/harnesses/claude-code/native.js +79 -0
- package/dist/harnesses/claude-code/paths.js +35 -0
- package/dist/harnesses/claude-code/prepare.js +117 -0
- package/dist/harnesses/claude-code/presentation.js +25 -0
- package/dist/harnesses/claude-code/state.js +253 -0
- package/dist/harnesses/codex/adapter.js +27 -68
- package/dist/harnesses/codex/conflicts.js +3 -8
- package/dist/harnesses/codex/generated-plugin.js +4 -11
- package/dist/harnesses/codex/harness.js +4 -26
- package/dist/harnesses/codex/hooks.js +11 -27
- package/dist/harnesses/codex/lifecycle.js +22 -107
- package/dist/harnesses/codex/manifest-overlay.js +9 -27
- package/dist/harnesses/codex/paths.js +29 -0
- package/dist/harnesses/codex/prepare.js +9 -43
- package/dist/harnesses/codex/presentation.js +1 -1
- package/dist/harnesses/codex/publication.js +5 -40
- package/dist/harnesses/codex/state.js +1 -6
- package/dist/harnesses/codex/stored-state.js +1 -6
- package/dist/harnesses/opencode/config.js +105 -50
- package/dist/harnesses/opencode/discovery.js +11 -15
- package/dist/harnesses/opencode/install.js +57 -102
- package/dist/harnesses/opencode/native.js +28 -49
- package/dist/harnesses/opencode/package.js +45 -67
- package/dist/harnesses/opencode/prepare.js +2 -7
- package/dist/harnesses/opencode/presentation.js +3 -7
- package/dist/harnesses/opencode/state.js +1 -0
- package/dist/harnesses/pi/install.js +31 -120
- package/dist/harnesses/pi/native.js +9 -34
- package/dist/harnesses/pi/package.js +9 -50
- package/dist/harnesses/pi/prepare.js +3 -8
- package/dist/harnesses/pi/presentation.js +2 -7
- package/dist/harnesses/pi/settings.js +5 -9
- package/dist/harnesses/pi/shared-skills.js +23 -53
- package/dist/harnesses/pi/state.js +3 -10
- package/dist/provenance.js +3 -19
- package/dist/resource-lock.js +11 -16
- package/dist/safe-path.js +28 -1
- package/dist/selection.js +15 -19
- package/dist/snapshot-journal.js +101 -0
- package/dist/snapshot-package.js +62 -1
- package/dist/snapshot-prepare.js +9 -2
- package/dist/snapshot-presentation.js +9 -0
- package/dist/upstream.js +8 -12
- package/dist/validator.js +26 -23
- package/dist/workspace.js +29 -35
- package/package.json +10 -9
- package/dist/python-json.js +0 -4
- package/dist/upstream-workspace.js +0 -8
|
@@ -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,50 @@ 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
|
-
const
|
|
230
|
-
const
|
|
231
|
-
const output = removeOpenCodeEntry(original, index);
|
|
289
|
+
const original = observation.document;
|
|
290
|
+
const output = removeOpenCodeEntry(original, key, index);
|
|
232
291
|
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
|
-
}
|
|
292
|
+
const expected = original.entries.filter((entry) => entry.key !== key || entry.index !== index);
|
|
293
|
+
await writeObservedOpenCodeConfig(observation, outputBytes, expected);
|
|
252
294
|
}
|
|
253
295
|
catch (cause) {
|
|
254
296
|
throw cannotRemove(path, cause);
|
|
255
297
|
}
|
|
256
298
|
}
|
|
299
|
+
export async function addObservedOpenCodeEntry(observation, key, spec) {
|
|
300
|
+
const path = observation.document.path;
|
|
301
|
+
try {
|
|
302
|
+
const original = observation.document;
|
|
303
|
+
const output = addOpenCodeEntry(original, key, spec);
|
|
304
|
+
const outputBytes = Buffer.from(output, "utf8");
|
|
305
|
+
const expected = parseOpenCodeConfig(output, path).entries;
|
|
306
|
+
await writeObservedOpenCodeConfig(observation, outputBytes, expected);
|
|
307
|
+
}
|
|
308
|
+
catch (cause) {
|
|
309
|
+
throw cannotRegister(path, cause);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -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);
|
|
@@ -163,11 +163,7 @@ async function inspectPluginEntry(observation, entry, state, allowManaged = true
|
|
|
163
163
|
const canonical = await canonicalizeProspectivePath(local);
|
|
164
164
|
if (canonical === state.installedRoot) {
|
|
165
165
|
if (allowManaged)
|
|
166
|
-
state.managedEntries.push({
|
|
167
|
-
observation,
|
|
168
|
-
entry,
|
|
169
|
-
canonicalRoot: canonical,
|
|
170
|
-
});
|
|
166
|
+
state.managedEntries.push({ observation, entry });
|
|
171
167
|
else
|
|
172
168
|
addBlocked(state, `${OPEN_CODE_UNOWNED_MANAGER_INPUT}: ${observation.document.path}`);
|
|
173
169
|
return;
|
|
@@ -175,14 +171,14 @@ async function inspectPluginEntry(observation, entry, state, allowManaged = true
|
|
|
175
171
|
}
|
|
176
172
|
if (!active)
|
|
177
173
|
return;
|
|
178
|
-
if (inspectUpstreamSpec(entry.spec, `${observation.document.path}
|
|
174
|
+
if (inspectUpstreamSpec(entry.spec, `${observation.document.path} ${entry.key}[${entry.index}]`, state))
|
|
179
175
|
return;
|
|
180
176
|
if (local === null)
|
|
181
177
|
return;
|
|
182
178
|
const kind = await classifyPathNoFollow(local);
|
|
183
179
|
if (kind === "missing") {
|
|
184
180
|
if (/^superpowers(?:\.(?:js|ts))?$/u.test(basename(local)))
|
|
185
|
-
addBlocked(state, `${observation.document.path}
|
|
181
|
+
addBlocked(state, `${observation.document.path} ${entry.key}[${entry.index}]`);
|
|
186
182
|
return;
|
|
187
183
|
}
|
|
188
184
|
if (isKnownPluginFile(local)) {
|
|
@@ -190,7 +186,7 @@ async function inspectPluginEntry(observation, entry, state, allowManaged = true
|
|
|
190
186
|
return;
|
|
191
187
|
}
|
|
192
188
|
if (kind !== "directory" && kind !== "symlink") {
|
|
193
|
-
addBlocked(state, `${observation.document.path}
|
|
189
|
+
addBlocked(state, `${observation.document.path} ${entry.key}[${entry.index}]`);
|
|
194
190
|
return;
|
|
195
191
|
}
|
|
196
192
|
try {
|
|
@@ -200,7 +196,7 @@ async function inspectPluginEntry(observation, entry, state, allowManaged = true
|
|
|
200
196
|
}
|
|
201
197
|
catch {
|
|
202
198
|
if (basename(local) === "superpowers")
|
|
203
|
-
addBlocked(state, `${observation.document.path}
|
|
199
|
+
addBlocked(state, `${observation.document.path} ${entry.key}[${entry.index}]`);
|
|
204
200
|
}
|
|
205
201
|
}
|
|
206
202
|
async function inspectSkillFile(path, label, state, requireKnownIdentity = false) {
|
|
@@ -268,7 +264,7 @@ async function inspectDocument(observation, state) {
|
|
|
268
264
|
await inspectConfigFields(effective.root, source, state);
|
|
269
265
|
const active = !truthy(state.env.OPENCODE_PURE);
|
|
270
266
|
for (const entry of effective.entries) {
|
|
271
|
-
const original = observation.document.entries
|
|
267
|
+
const original = observation.document.entries.find((candidate) => candidate.key === entry.key && candidate.index === entry.index);
|
|
272
268
|
const stable = original !== undefined && original.spec === entry.spec;
|
|
273
269
|
await inspectPluginEntry(observation, stable ? original : entry, state, stable && isNativeWriterOrigin(observation, state), active);
|
|
274
270
|
}
|
|
@@ -565,8 +561,8 @@ export async function inspectOpenCodeDiscovery(paths, env, cwd) {
|
|
|
565
561
|
for (const entry of inline.entries) {
|
|
566
562
|
if (entry.spec.trim().length === 0 ||
|
|
567
563
|
containsSubstitution(entry.spec))
|
|
568
|
-
addBlocked(state, `OPENCODE_CONFIG_CONTENT
|
|
569
|
-
else if (inspectUpstreamSpec(entry.spec, `OPENCODE_CONFIG_CONTENT
|
|
564
|
+
addBlocked(state, `OPENCODE_CONFIG_CONTENT ${entry.key}[${entry.index}]`);
|
|
565
|
+
else if (inspectUpstreamSpec(entry.spec, `OPENCODE_CONFIG_CONTENT ${entry.key}[${entry.index}]`, state)) {
|
|
570
566
|
continue;
|
|
571
567
|
}
|
|
572
568
|
else {
|
|
@@ -575,7 +571,7 @@ export async function inspectOpenCodeDiscovery(paths, env, cwd) {
|
|
|
575
571
|
const canonical = await canonicalizeProspectivePath(local);
|
|
576
572
|
addBlocked(state, canonical === state.installedRoot
|
|
577
573
|
? `${OPEN_CODE_UNOWNED_MANAGER_INPUT}: OPENCODE_CONFIG_CONTENT`
|
|
578
|
-
: `OPENCODE_CONFIG_CONTENT
|
|
574
|
+
: `OPENCODE_CONFIG_CONTENT ${entry.key}[${entry.index}]`);
|
|
579
575
|
}
|
|
580
576
|
}
|
|
581
577
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { cp,
|
|
3
|
-
import {
|
|
2
|
+
import { cp, mkdir, readdir, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
4
|
import { findNodeAtLocation } from "jsonc-parser";
|
|
5
5
|
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";
|
|
@@ -14,10 +14,13 @@ import { readOpenCodePackageAssessment, readOpenCodeReceipt, } from "./package.j
|
|
|
14
14
|
import { assertOpenCodePreparationSeparate, openCodePaths, } from "./paths.js";
|
|
15
15
|
import { inspectOpenCodeControl, inspectOpenCodeOwnership, } from "./state.js";
|
|
16
16
|
import { displayPath } from "../../validator.js";
|
|
17
|
+
import { createJournal, createSnapshotJournal, hasRecovery, identity, journalPath, publicationPaths, requireNoRecovery, } from "../../snapshot-journal.js";
|
|
17
18
|
const DEFAULTS = {
|
|
18
19
|
run: runOpenCode,
|
|
19
20
|
beginPublication: beginDirectoryPublication,
|
|
20
21
|
};
|
|
22
|
+
// V1 prints a bare semver ("1.18.31"); V2 prints "opencode v2.0.10".
|
|
23
|
+
const OPEN_CODE_VERSION_PREFIX = /^opencode\s+v/i;
|
|
21
24
|
// The journal can contain data from two independently bounded 1 MiB receipts
|
|
22
25
|
// plus one bounded 1 MiB config. Six bytes of JSON output per input byte covers
|
|
23
26
|
// control escaping; counting the registration spec independently brings that
|
|
@@ -33,20 +36,6 @@ function accepted(result) {
|
|
|
33
36
|
throw new Error("OpenCode operation did not succeed");
|
|
34
37
|
return result.outcome.result;
|
|
35
38
|
}
|
|
36
|
-
function sameIdentity(left, right) {
|
|
37
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
38
|
-
}
|
|
39
|
-
async function identity(path) {
|
|
40
|
-
await assertNoFollowType(path, ["directory"]);
|
|
41
|
-
return await lstat(path);
|
|
42
|
-
}
|
|
43
|
-
async function requireIdentity(path, expected) {
|
|
44
|
-
if (!sameIdentity(await identity(path), expected))
|
|
45
|
-
throw new Error("OpenCode directory changed");
|
|
46
|
-
}
|
|
47
|
-
function journalPath(pending) {
|
|
48
|
-
return join(pending.paths.recoveryRoot, "transaction.json");
|
|
49
|
-
}
|
|
50
39
|
function journalBytes(journal) {
|
|
51
40
|
const bytes = Buffer.from(`${JSON.stringify(journal)}\n`);
|
|
52
41
|
if (bytes.length > MAX_JOURNAL_BYTES)
|
|
@@ -55,31 +44,14 @@ function journalBytes(journal) {
|
|
|
55
44
|
}
|
|
56
45
|
async function validatePaths(paths) {
|
|
57
46
|
await assertOpenCodePreparationSeparate(paths);
|
|
58
|
-
await assertNoFollowType(paths.configRoot, ["directory", "missing"]);
|
|
59
|
-
await assertNoFollowType(paths.managerRoot, ["directory", "missing"]);
|
|
60
|
-
await assertNoFollowType(paths.installedRoot, ["directory", "missing"]);
|
|
61
47
|
return await canonicalizeProspectivePath(paths.installedRoot);
|
|
62
48
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async function syncDirectoryStrict(path) {
|
|
75
|
-
const directory = await open(path, "r");
|
|
76
|
-
try {
|
|
77
|
-
await directory.sync();
|
|
78
|
-
}
|
|
79
|
-
finally {
|
|
80
|
-
await directory.close();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
49
|
+
const { requireIdentity, requireJournal, phase, retireJournal } = createSnapshotJournal({
|
|
50
|
+
label: "OpenCode",
|
|
51
|
+
validatePaths,
|
|
52
|
+
serialize: journalBytes,
|
|
53
|
+
readCap: MAX_JOURNAL_BYTES,
|
|
54
|
+
});
|
|
83
55
|
function artifactSame(left, right) {
|
|
84
56
|
return left === null
|
|
85
57
|
? right === null
|
|
@@ -122,13 +94,14 @@ function registrationRecord(registration) {
|
|
|
122
94
|
return null;
|
|
123
95
|
const observation = registration.observation;
|
|
124
96
|
const node = findNodeAtLocation(observation.document.root, [
|
|
125
|
-
|
|
97
|
+
registration.entry.key,
|
|
126
98
|
registration.entry.index,
|
|
127
99
|
]);
|
|
128
100
|
if (node === undefined)
|
|
129
101
|
throw new Error("OpenCode registration syntax changed");
|
|
130
102
|
return {
|
|
131
103
|
configPath: observation.document.path,
|
|
104
|
+
configKey: registration.entry.key,
|
|
132
105
|
entryIndex: registration.entry.index,
|
|
133
106
|
spec: registration.entry.spec,
|
|
134
107
|
optionsDigest: createHash("sha256")
|
|
@@ -148,6 +121,7 @@ function sameRegistration(registration, expected) {
|
|
|
148
121
|
? registration === undefined
|
|
149
122
|
: registration !== undefined &&
|
|
150
123
|
registration.observation.document.path === expected.configPath &&
|
|
124
|
+
registration.entry.key === expected.configKey &&
|
|
151
125
|
registration.entry.spec === expected.spec &&
|
|
152
126
|
createHash("sha256")
|
|
153
127
|
.update(JSON.stringify(registration.entry.options))
|
|
@@ -157,6 +131,7 @@ function registrationInputSame(input, current) {
|
|
|
157
131
|
return input === null
|
|
158
132
|
? current === null
|
|
159
133
|
: current !== null &&
|
|
134
|
+
input.key === current.key &&
|
|
160
135
|
input.entryIndex === current.entryIndex &&
|
|
161
136
|
input.spec === current.spec &&
|
|
162
137
|
input.observation.document.path === current.observation.document.path &&
|
|
@@ -195,39 +170,6 @@ async function requireActivationEligibility(ctx) {
|
|
|
195
170
|
control.mutationEligibility.kind !== "allowed")
|
|
196
171
|
throw new Error("OpenCode ownership or control blocked");
|
|
197
172
|
}
|
|
198
|
-
async function requireJournal(pending) {
|
|
199
|
-
if ((await validatePaths(pending.paths)) !== pending.canonicalRoot ||
|
|
200
|
-
pending.journal.installedRoot !== pending.canonicalRoot ||
|
|
201
|
-
!/^[a-f0-9]{32}$/u.test(pending.journal.token))
|
|
202
|
-
throw new Error("OpenCode recovery root changed");
|
|
203
|
-
await requireIdentity(pending.paths.recoveryRoot, pending.recoveryIdentity);
|
|
204
|
-
const path = journalPath(pending);
|
|
205
|
-
await assertNoFollowType(path, ["regular-file"]);
|
|
206
|
-
const observed = await lstat(path);
|
|
207
|
-
if (!sameIdentity(observed, pending.journalIdentity) ||
|
|
208
|
-
observed.size > MAX_JOURNAL_BYTES ||
|
|
209
|
-
!(await readFile(path)).equals(journalBytes(pending.journal)))
|
|
210
|
-
throw new Error("OpenCode recovery journal changed");
|
|
211
|
-
if (pending.backup !==
|
|
212
|
-
join(dirname(pending.canonicalRoot), `.${basename(pending.canonicalRoot)}.bak.${pending.journal.token}`) ||
|
|
213
|
-
pending.stage !==
|
|
214
|
-
join(dirname(pending.canonicalRoot), `.${basename(pending.canonicalRoot)}.stage.${pending.journal.token}`))
|
|
215
|
-
throw new Error("OpenCode recovery paths changed");
|
|
216
|
-
}
|
|
217
|
-
async function phase(pending, next, changes = {}) {
|
|
218
|
-
await requireJournal(pending);
|
|
219
|
-
const journal = { ...pending.journal, ...changes, phase: next };
|
|
220
|
-
const bytes = journalBytes(journal);
|
|
221
|
-
await atomicWriteFile(journalPath(pending), bytes, {
|
|
222
|
-
validate: async (temporary) => {
|
|
223
|
-
if (!(await readFile(temporary)).equals(bytes))
|
|
224
|
-
throw new Error("OpenCode journal write changed");
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
await syncDirectoryStrict(pending.paths.recoveryRoot);
|
|
228
|
-
pending.journal = journal;
|
|
229
|
-
pending.journalIdentity = await lstat(journalPath(pending));
|
|
230
|
-
}
|
|
231
173
|
async function beginJournal(paths, oldArtifact, newArtifact, priorRegistration, ctx) {
|
|
232
174
|
const canonicalRoot = await validatePaths(paths);
|
|
233
175
|
await requireNoRecovery(paths);
|
|
@@ -248,36 +190,18 @@ async function beginJournal(paths, oldArtifact, newArtifact, priorRegistration,
|
|
|
248
190
|
await mkdir(paths.managerRoot, { recursive: true });
|
|
249
191
|
await mkdir(paths.recoveryRoot, { mode: 0o700 });
|
|
250
192
|
const recoveryIdentity = await identity(paths.recoveryRoot);
|
|
251
|
-
const
|
|
252
|
-
const handle = await open(path, "wx", 0o600);
|
|
253
|
-
try {
|
|
254
|
-
await handle.writeFile(bytes);
|
|
255
|
-
await handle.sync();
|
|
256
|
-
}
|
|
257
|
-
finally {
|
|
258
|
-
await handle.close();
|
|
259
|
-
}
|
|
260
|
-
await syncDirectoryStrict(paths.recoveryRoot);
|
|
193
|
+
const journalIdentity = await createJournal(paths.recoveryRoot, bytes);
|
|
261
194
|
return {
|
|
262
195
|
paths,
|
|
263
196
|
canonicalRoot,
|
|
264
197
|
recoveryIdentity,
|
|
265
|
-
|
|
266
|
-
stage: join(dirname(canonicalRoot), `.${basename(canonicalRoot)}.stage.${token}`),
|
|
198
|
+
...publicationPaths(canonicalRoot, token),
|
|
267
199
|
ctx,
|
|
268
200
|
journal,
|
|
269
|
-
journalIdentity
|
|
201
|
+
journalIdentity,
|
|
270
202
|
settled: false,
|
|
271
203
|
};
|
|
272
204
|
}
|
|
273
|
-
async function retireJournal(pending) {
|
|
274
|
-
await requireJournal(pending);
|
|
275
|
-
if ((await readdir(pending.paths.recoveryRoot)).some((name) => name !== "transaction.json"))
|
|
276
|
-
throw new Error("unknown OpenCode recovery material");
|
|
277
|
-
await unlink(journalPath(pending));
|
|
278
|
-
await requireIdentity(pending.paths.recoveryRoot, pending.recoveryIdentity);
|
|
279
|
-
await rmdir(pending.paths.recoveryRoot);
|
|
280
|
-
}
|
|
281
205
|
async function recoveryGuidance(pending) {
|
|
282
206
|
try {
|
|
283
207
|
const kind = await classifyPathNoFollow(pending.paths.recoveryRoot);
|
|
@@ -348,7 +272,7 @@ async function rollbackOpenCodePublication(pending, publication) {
|
|
|
348
272
|
const current = observed.managedEntries[0];
|
|
349
273
|
if (current === undefined)
|
|
350
274
|
throw new Error("OpenCode created registration missing");
|
|
351
|
-
await removeObservedOpenCodeEntry(current.observation, current.entry.index);
|
|
275
|
+
await removeObservedOpenCodeEntry(current.observation, current.entry.key, current.entry.index);
|
|
352
276
|
observed = await discovery(pending.paths, pending.ctx);
|
|
353
277
|
requireNoOwnedActivation(observed);
|
|
354
278
|
}
|
|
@@ -416,7 +340,13 @@ export async function installOpenCode(artifact, ctx, deps = DEFAULTS) {
|
|
|
416
340
|
let priorRegistration = registrationRecord(observed.managedEntries[0]);
|
|
417
341
|
if (previous === null && priorRegistration !== null)
|
|
418
342
|
throw new Error("unowned OpenCode registration");
|
|
419
|
-
accepted(normalizeSnapshotRuntimeVersion("OpenCode", await deps.run(["--version"], paths, ctx)));
|
|
343
|
+
const runtimeVersion = accepted(normalizeSnapshotRuntimeVersion("OpenCode", await deps.run(["--version"], paths, ctx), OPEN_CODE_VERSION_PREFIX));
|
|
344
|
+
const major = Number.parseInt(runtimeVersion, 10);
|
|
345
|
+
if (major !== 1 && major !== 2)
|
|
346
|
+
return fail("unsupported-runtime", `unsupported OpenCode major version ${major}`);
|
|
347
|
+
if (major === 2 &&
|
|
348
|
+
assessment.compatibility.generation !== "opencode-native-bootstrap-v2")
|
|
349
|
+
return fail("unsupported-artifact", "the selected upstream ref does not support OpenCode 2; select a ref that ships the V2 entrypoint");
|
|
420
350
|
await requireSnapshot(paths.installedRoot, previous);
|
|
421
351
|
observed = await requireStableActivationRegistration(paths, ctx, priorRegistration);
|
|
422
352
|
priorRegistration = registrationRecord(observed.managedEntries[0]);
|
|
@@ -443,16 +373,40 @@ export async function installOpenCode(artifact, ctx, deps = DEFAULTS) {
|
|
|
443
373
|
if (priorRegistration === null) {
|
|
444
374
|
await phase(pending, "registering");
|
|
445
375
|
observed = await requireStableActivationRegistration(paths, ctx, null);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
376
|
+
let native = null;
|
|
377
|
+
if (major === 1) {
|
|
378
|
+
native = await deps
|
|
379
|
+
.run(["plugin", pending.canonicalRoot, "--global"], paths, ctx)
|
|
380
|
+
.catch(() => fail("native-failed", "OpenCode native installation failed"));
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const jsonc = join(paths.configRoot, "opencode.jsonc");
|
|
384
|
+
const json = join(paths.configRoot, "opencode.json");
|
|
385
|
+
let target = observed.documents.find((document) => document.document.path === jsonc) ??
|
|
386
|
+
observed.documents.find((document) => document.document.path === json);
|
|
387
|
+
if (target === undefined) {
|
|
388
|
+
await atomicWriteFile(json, Buffer.from("{}\n"), {
|
|
389
|
+
validate: async () => {
|
|
390
|
+
await assertNoFollowType(paths.configRoot, ["directory"]);
|
|
391
|
+
await assertNoFollowType(jsonc, ["missing"]);
|
|
392
|
+
await assertNoFollowType(json, ["missing"]);
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
observed = await requireStableActivationRegistration(paths, ctx, null);
|
|
396
|
+
target = observed.documents.find((document) => document.document.path === json);
|
|
397
|
+
}
|
|
398
|
+
if (target === undefined)
|
|
399
|
+
throw new Error("no OpenCode configuration to register in");
|
|
400
|
+
await addObservedOpenCodeEntry(target, "plugins", pending.canonicalRoot);
|
|
401
|
+
}
|
|
449
402
|
observed = await discovery(paths, ctx);
|
|
450
403
|
requireSafeActivationDiscovery(observed);
|
|
451
404
|
const created = registrationRecord(observed.managedEntries[0]);
|
|
452
405
|
if (created === null || created.spec !== pending.canonicalRoot)
|
|
453
406
|
throw new Error("OpenCode activation unverified");
|
|
454
407
|
await phase(pending, "registered", { createdRegistration: created });
|
|
455
|
-
|
|
408
|
+
if (native !== null)
|
|
409
|
+
accepted(native);
|
|
456
410
|
}
|
|
457
411
|
await requirePublication(pending);
|
|
458
412
|
observed = await discovery(paths, ctx);
|
|
@@ -524,6 +478,7 @@ export async function removeOpenCode(input, ctx, deps = DEFAULTS) {
|
|
|
524
478
|
? null
|
|
525
479
|
: {
|
|
526
480
|
observation: registered.observation,
|
|
481
|
+
key: registered.entry.key,
|
|
527
482
|
entryIndex: registered.entry.index,
|
|
528
483
|
spec: registered.entry.spec,
|
|
529
484
|
}))
|
|
@@ -546,7 +501,7 @@ export async function removeOpenCode(input, ctx, deps = DEFAULTS) {
|
|
|
546
501
|
if (!sameRegistration(observed.managedEntries[0], pending.journal.priorRegistration))
|
|
547
502
|
throw new Error("OpenCode registration changed");
|
|
548
503
|
if (registered !== undefined) {
|
|
549
|
-
await removeObservedOpenCodeEntry(registered.observation, registered.entry.index);
|
|
504
|
+
await removeObservedOpenCodeEntry(registered.observation, registered.entry.key, registered.entry.index);
|
|
550
505
|
observed = await discovery(paths, ctx);
|
|
551
506
|
requireNoOwnedActivation(observed);
|
|
552
507
|
deregistered = true;
|