superpowers-manager 0.7.0 → 0.7.2
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 +2 -2
- package/dist/harnesses/opencode/discovery.js +75 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,7 +124,7 @@ The [usage reference](https://github.com/j7an/superpowers-manager/blob/main/docs
|
|
|
124
124
|
|
|
125
125
|
## Requirements and platforms
|
|
126
126
|
|
|
127
|
-
The installed package requires Node >=24. Native source, tests, and packaging tooling
|
|
127
|
+
The installed package requires Node >=24. Native source, tests, and packaging tooling target latest Node 24. Every other requirement is command-specific and is checked before dispatch. This table is derived from production by `tests/bin/readme-requirements.test.ts`.
|
|
128
128
|
|
|
129
129
|
<!-- requirements:begin -->
|
|
130
130
|
|
|
@@ -143,7 +143,7 @@ The installed package requires Node >=24. Native source, tests, and packaging to
|
|
|
143
143
|
|
|
144
144
|
Generated Codex manifests preserve upstream JSON number tokens while rejecting non-finite decimal or exponent values.
|
|
145
145
|
|
|
146
|
-
macOS and Linux are tested. WSL2 is supported for the established Codex path; Pi in WSL2 and native Windows remain untested. The manager runs in-process and does not require Git Bash. The [Pi reference](https://github.com/j7an/superpowers-manager/blob/main/docs/pi.md#runtime-compatibility) explains its runtime qualification and admission checks.
|
|
146
|
+
macOS and Linux are tested. WSL2 is supported for the established Codex path; Pi in WSL2 and native Windows remain untested. The manager runs in-process and does not require Git Bash. The [Pi reference](https://github.com/j7an/superpowers-manager/blob/main/docs/pi.md#runtime-compatibility) explains its runtime qualification and admission checks. OpenCode support currently covers 1.x; OpenCode 2.x is untested and not certified as compatible (see the [OpenCode reference](https://github.com/j7an/superpowers-manager/blob/main/docs/opencode.md)).
|
|
147
147
|
|
|
148
148
|
Native test versions qualify integration mechanisms; they are not runtime allowlists, and an untested runtime is not automatically certified as compatible.
|
|
149
149
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { constants } from "node:fs";
|
|
3
4
|
import { open } from "node:fs/promises";
|
|
4
5
|
import { tmpdir, userInfo } from "node:os";
|
|
@@ -12,9 +13,7 @@ import { canonicalizeProspectivePath, classifyPathNoFollow, isContained, isErrno
|
|
|
12
13
|
import { runValidator } from "../../validator.js";
|
|
13
14
|
import { withWorkspace } from "../../workspace.js";
|
|
14
15
|
import { parseOpenCodeConfig, readOpenCodeConfig, } from "./config.js";
|
|
15
|
-
|
|
16
|
-
// its WAL with SQLite. Keep DB+WAL to one finite 16 MiB capture, without retry.
|
|
17
|
-
const MAX_ACCOUNT_FILE_BYTES = 16 * 1024 * 1024;
|
|
16
|
+
const MAX_AUTH_FILE_BYTES = 16 * 1024 * 1024;
|
|
18
17
|
const ACCOUNT_QUERY_POLICY = {
|
|
19
18
|
timeoutMs: 2_000,
|
|
20
19
|
graceMs: 200,
|
|
@@ -339,7 +338,7 @@ async function captureFile(path) {
|
|
|
339
338
|
try {
|
|
340
339
|
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
341
340
|
const stat = await handle.stat();
|
|
342
|
-
if (!stat.isFile() || stat.size >
|
|
341
|
+
if (!stat.isFile() || stat.size > MAX_AUTH_FILE_BYTES)
|
|
343
342
|
throw new Error("unsupported account database file");
|
|
344
343
|
const bytes = Buffer.allocUnsafe(stat.size);
|
|
345
344
|
let offset = 0;
|
|
@@ -360,13 +359,59 @@ async function captureFile(path) {
|
|
|
360
359
|
await handle?.close().catch(() => { });
|
|
361
360
|
}
|
|
362
361
|
}
|
|
363
|
-
function
|
|
362
|
+
async function hashFile(path, copyTo) {
|
|
363
|
+
let handle;
|
|
364
|
+
try {
|
|
365
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
366
|
+
}
|
|
367
|
+
catch (cause) {
|
|
368
|
+
if (isErrno(cause, "ENOENT"))
|
|
369
|
+
return null;
|
|
370
|
+
throw cause;
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const stat = await handle.stat();
|
|
374
|
+
if (!stat.isFile())
|
|
375
|
+
throw new Error("unsupported account database file");
|
|
376
|
+
const hash = createHash("sha256");
|
|
377
|
+
const out = copyTo === undefined
|
|
378
|
+
? undefined
|
|
379
|
+
: await open(copyTo, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
380
|
+
try {
|
|
381
|
+
for await (const chunk of handle.createReadStream({
|
|
382
|
+
autoClose: false,
|
|
383
|
+
start: 0,
|
|
384
|
+
})) {
|
|
385
|
+
hash.update(chunk);
|
|
386
|
+
for (let offset = 0; out !== undefined && offset < chunk.length;) {
|
|
387
|
+
const { bytesWritten } = await out.write(chunk, offset);
|
|
388
|
+
if (bytesWritten === 0)
|
|
389
|
+
throw new Error("account copy stalled");
|
|
390
|
+
offset += bytesWritten;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
await out?.close();
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
hash: hash.digest("hex"),
|
|
399
|
+
dev: stat.dev,
|
|
400
|
+
ino: stat.ino,
|
|
401
|
+
mode: stat.mode,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
await handle.close().catch(() => { });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function sameFile(left, right) {
|
|
364
409
|
return left === null || right === null
|
|
365
410
|
? left === right
|
|
366
411
|
: left.dev === right.dev &&
|
|
367
412
|
left.ino === right.ino &&
|
|
368
413
|
left.mode === right.mode &&
|
|
369
|
-
left.
|
|
414
|
+
left.hash === right.hash;
|
|
370
415
|
}
|
|
371
416
|
function accountDatabasePath(env, dataRoot) {
|
|
372
417
|
const configured = env.OPENCODE_DB;
|
|
@@ -381,46 +426,31 @@ async function inspectAccount(state, dataRoot) {
|
|
|
381
426
|
if (database === null)
|
|
382
427
|
return;
|
|
383
428
|
const wal = `${database}-wal`;
|
|
384
|
-
let initialDb;
|
|
385
|
-
let initialWal;
|
|
386
429
|
try {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
captureFile(wal),
|
|
390
|
-
]);
|
|
391
|
-
if (initialDb === null) {
|
|
392
|
-
if (initialWal !== null)
|
|
430
|
+
if ((await classifyPathNoFollow(database)) === "missing") {
|
|
431
|
+
if ((await classifyPathNoFollow(wal)) !== "missing")
|
|
393
432
|
throw new Error("orphan account WAL");
|
|
394
433
|
return;
|
|
395
434
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
435
|
+
// Account inspection pays a streamed full-copy cost so it never opens the
|
|
436
|
+
// native DB or its WAL with SQLite: a live read-only open rewrites -shm and
|
|
437
|
+
// creates -wal/-shm when absent. ponytail: copy time and temp space scale
|
|
438
|
+
// with DB size; query live only if that cost is measured to matter and
|
|
439
|
+
// -shm writes can be avoided.
|
|
399
440
|
const result = await withWorkspace(tmpdir(), "spw-opencode-account-", async (workspace) => {
|
|
400
441
|
const copy = join(workspace, "opencode.db");
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
await dbOut.close();
|
|
408
|
-
}
|
|
409
|
-
if (initialWal !== null) {
|
|
410
|
-
const walOut = await open(`${copy}-wal`, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
411
|
-
try {
|
|
412
|
-
await walOut.writeFile(initialWal.bytes);
|
|
413
|
-
}
|
|
414
|
-
finally {
|
|
415
|
-
await walOut.close();
|
|
416
|
-
}
|
|
417
|
-
}
|
|
442
|
+
const [initialDb, initialWal] = await Promise.all([
|
|
443
|
+
hashFile(database, copy),
|
|
444
|
+
hashFile(wal, `${copy}-wal`),
|
|
445
|
+
]);
|
|
446
|
+
if (initialDb === null)
|
|
447
|
+
throw new Error("account database moved");
|
|
418
448
|
const beforeQuery = await Promise.all([
|
|
419
|
-
|
|
420
|
-
|
|
449
|
+
hashFile(database),
|
|
450
|
+
hashFile(wal),
|
|
421
451
|
]);
|
|
422
|
-
if (!
|
|
423
|
-
!
|
|
452
|
+
if (!sameFile(initialDb, beforeQuery[0]) ||
|
|
453
|
+
!sameFile(initialWal, beforeQuery[1]))
|
|
424
454
|
throw new Error("account database moved");
|
|
425
455
|
const run = await runValidator([
|
|
426
456
|
process.execPath,
|
|
@@ -436,15 +466,15 @@ async function inspectAccount(state, dataRoot) {
|
|
|
436
466
|
run.stderr.text !== "" ||
|
|
437
467
|
(run.stdout.text !== "active" && run.stdout.text !== "absent"))
|
|
438
468
|
throw new Error("account database query failed");
|
|
469
|
+
const afterQuery = await Promise.all([
|
|
470
|
+
hashFile(database),
|
|
471
|
+
hashFile(wal),
|
|
472
|
+
]);
|
|
473
|
+
if (!sameFile(initialDb, afterQuery[0]) ||
|
|
474
|
+
!sameFile(initialWal, afterQuery[1]))
|
|
475
|
+
throw new Error("account database moved");
|
|
439
476
|
return run.stdout.text;
|
|
440
477
|
});
|
|
441
|
-
const afterQuery = await Promise.all([
|
|
442
|
-
captureFile(database),
|
|
443
|
-
captureFile(wal),
|
|
444
|
-
]);
|
|
445
|
-
if (!sameCapture(initialDb, afterQuery[0]) ||
|
|
446
|
-
!sameCapture(initialWal, afterQuery[1]))
|
|
447
|
-
throw new Error("account database moved");
|
|
448
478
|
if (result === "active")
|
|
449
479
|
addBlocked(state, "OpenCode active account remote configuration");
|
|
450
480
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superpowers-manager",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|