superpowers-manager 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -26
- package/dist/adapter-result.js +5 -9
- package/dist/atomic.js +4 -14
- package/dist/cli-arguments.js +1 -27
- package/dist/cli.js +25 -40
- package/dist/commands/adapter-call.js +28 -0
- package/dist/commands/install.js +25 -105
- package/dist/commands/pin.js +1 -1
- package/dist/commands/prepare.js +37 -81
- package/dist/commands/probe.js +35 -262
- package/dist/commands/track-latest.js +1 -1
- package/dist/commands/uninstall.js +15 -91
- package/dist/commands/update.js +5 -25
- package/dist/effective-selection.js +4 -6
- package/dist/git-tree.js +85 -0
- package/dist/harness-command-result.js +47 -0
- package/dist/harnesses/codex/adapter.js +12 -41
- package/dist/harnesses/codex/conflicts.js +2 -3
- package/dist/harnesses/codex/hooks.js +1 -1
- package/dist/harnesses/codex/json.js +1 -11
- package/dist/harnesses/codex/lifecycle.js +0 -48
- package/dist/harnesses/codex/manifest-overlay.js +1 -1
- package/dist/harnesses/codex/paths.js +14 -29
- package/dist/harnesses/codex/presentation.js +2 -23
- package/dist/harnesses/codex/publication.js +0 -6
- package/dist/harnesses/codex/recovery.js +5 -5
- package/dist/harnesses/codex/stored-state.js +1 -1
- package/dist/harnesses/opencode/config.js +256 -0
- package/dist/harnesses/opencode/discovery.js +617 -0
- package/dist/harnesses/opencode/harness.js +37 -0
- package/dist/harnesses/opencode/install.js +601 -0
- package/dist/harnesses/opencode/native.js +57 -0
- package/dist/harnesses/opencode/package.js +89 -0
- package/dist/harnesses/opencode/paths.js +38 -0
- package/dist/harnesses/opencode/prepare.js +30 -0
- package/dist/harnesses/opencode/presentation.js +28 -0
- package/dist/harnesses/opencode/state.js +174 -0
- package/dist/harnesses/pi/compatibility.js +5 -37
- package/dist/harnesses/pi/install.js +7 -5
- package/dist/harnesses/pi/native.js +3 -47
- package/dist/harnesses/pi/package.js +5 -107
- package/dist/harnesses/pi/prepare.js +13 -104
- package/dist/harnesses/pi/presentation.js +23 -66
- package/dist/harnesses/pi/shared-skills.js +28 -39
- package/dist/harnesses/pi/state.js +7 -27
- package/dist/provenance.js +2 -2
- package/dist/safe-path.js +2 -2
- package/dist/selection-store.js +2 -22
- package/dist/snapshot-package.js +78 -0
- package/dist/snapshot-prepare.js +99 -0
- package/dist/snapshot-presentation.js +44 -0
- package/dist/validator.js +4 -1
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +8 -4
- package/dist/domain/fingerprint.js +0 -4
- package/dist/status.js +0 -24
- package/dist/validate-generated-plugin-cli.js +0 -118
|
@@ -58,33 +58,12 @@ function fields(f) {
|
|
|
58
58
|
{ key: "update_control", label: "update control", value: f.updateControl },
|
|
59
59
|
];
|
|
60
60
|
}
|
|
61
|
-
|
|
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
|
-
|
|
66
|
+
function formatHuman(f) {
|
|
88
67
|
let text = fields(f)
|
|
89
68
|
.map((field) => {
|
|
90
69
|
const shown = field.value.length === 0 && field.absent !== undefined
|
|
@@ -298,9 +298,6 @@ async function nativeUnchangedAfterAttempt(prior, priorActiveDigest, current) {
|
|
|
298
298
|
return (await activeDigest(current)) === priorActiveDigest;
|
|
299
299
|
}
|
|
300
300
|
async function rollbackPublication(pending, publication, previous, priorNative, priorActiveDigest, activationAttempted, ctx, dependencies) {
|
|
301
|
-
if (pending.settled) {
|
|
302
|
-
return fail("already-settled", "Codex installation transaction has already been settled", []);
|
|
303
|
-
}
|
|
304
301
|
const messages = [];
|
|
305
302
|
try {
|
|
306
303
|
const currentNative = await observeNative(ctx, dependencies, messages);
|
|
@@ -358,9 +355,6 @@ async function verifyActivated(pending, dependencies, ctx, messages) {
|
|
|
358
355
|
}
|
|
359
356
|
}
|
|
360
357
|
async function finalizePublication(pending, publication, dependencies, ctx) {
|
|
361
|
-
if (pending.settled) {
|
|
362
|
-
return fail("already-settled", "Codex installation transaction has already been settled", []);
|
|
363
|
-
}
|
|
364
358
|
const messages = [];
|
|
365
359
|
let verified = false;
|
|
366
360
|
let backupRemoved = false;
|
|
@@ -114,7 +114,7 @@ function validateEvidenceShape(record) {
|
|
|
114
114
|
throw new Error("incomplete old recovery evidence");
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|