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
|
@@ -1,57 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
3
2
|
import { mkdir } from "node:fs/promises";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import {} from "../../adapter-result.js";
|
|
4
|
+
import { runIsolatedNative, } from "../../harness-command-result.js";
|
|
6
5
|
import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "../../validator.js";
|
|
7
|
-
import { withWorkspace } from "../../workspace.js";
|
|
8
6
|
const OPEN_CODE_EXECUTABLE = {
|
|
9
7
|
...BOUNDED_EXECUTABLE,
|
|
10
8
|
inheritEnvironment: false,
|
|
11
9
|
};
|
|
12
|
-
function selectedExecutable(env, invocationCwd) {
|
|
13
|
-
const configured = env.SUPERPOWERS_OPENCODE || "opencode";
|
|
14
|
-
return configured.includes(sep) && !isAbsolute(configured)
|
|
15
|
-
? resolve(invocationCwd, configured)
|
|
16
|
-
: configured;
|
|
17
|
-
}
|
|
18
10
|
export async function runOpenCode(args, paths, ctx, execute = runBoundedCommand) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
OPENCODE_PURE: "1",
|
|
45
|
-
OPENCODE_TEST_MANAGED_CONFIG_DIR: managed,
|
|
46
|
-
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
47
|
-
GIT_CONFIG_NOSYSTEM: "1",
|
|
48
|
-
}, temporary, workspace);
|
|
49
|
-
return nativeCommandResult("OpenCode", result);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return failureResult(operation, "workspace-failed", entered
|
|
54
|
-
? "cannot complete OpenCode command in its isolated workspace"
|
|
55
|
-
: "cannot create an isolated OpenCode command workspace", [], []);
|
|
56
|
-
}
|
|
11
|
+
return await runIsolatedNative("OpenCode", (ctx.env ?? {}).SUPERPOWERS_OPENCODE || "opencode", ctx, async (executable, workspace, env) => {
|
|
12
|
+
const home = resolve(workspace, "home");
|
|
13
|
+
const data = resolve(workspace, "data");
|
|
14
|
+
const cache = resolve(workspace, "cache");
|
|
15
|
+
const state = resolve(workspace, "state");
|
|
16
|
+
const temporary = resolve(workspace, "tmp");
|
|
17
|
+
const managed = resolve(workspace, "managed");
|
|
18
|
+
await Promise.all([home, data, cache, state, temporary, managed].map((path) => mkdir(path)));
|
|
19
|
+
return await execute([executable, ...args], OPEN_CODE_EXECUTABLE, {
|
|
20
|
+
PATH: env.PATH,
|
|
21
|
+
HOME: home,
|
|
22
|
+
XDG_CONFIG_HOME: dirname(paths.configRoot),
|
|
23
|
+
XDG_DATA_HOME: data,
|
|
24
|
+
XDG_CACHE_HOME: cache,
|
|
25
|
+
XDG_STATE_HOME: state,
|
|
26
|
+
TMPDIR: temporary,
|
|
27
|
+
OPENCODE_DISABLE_AUTOUPDATE: "1",
|
|
28
|
+
OPENCODE_DISABLE_MODELS_FETCH: "1",
|
|
29
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
|
|
30
|
+
OPENCODE_PURE: "1",
|
|
31
|
+
OPENCODE_TEST_MANAGED_CONFIG_DIR: managed,
|
|
32
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
33
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
34
|
+
}, temporary, workspace);
|
|
35
|
+
});
|
|
57
36
|
}
|
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
|
|
3
|
+
import { SEMVER_RE } from "../../domain/refs.js";
|
|
4
|
+
import { assertNoFollowType } from "../../safe-path.js";
|
|
4
5
|
import { validateSource } from "../../selection.js";
|
|
5
|
-
import {
|
|
6
|
-
import { isOfficialSnapshotSource, requireNoSnapshotDependencies, requireSnapshotBootstrap, snapshotReceiptBinding, } from "../../snapshot-package.js";
|
|
6
|
+
import { createSnapshotReceipts, isOfficialSnapshotSource, requireNoSnapshotDependencies, requireSnapshotBootstrap, } from "../../snapshot-package.js";
|
|
7
7
|
const BOOTSTRAP = ".opencode/plugins/superpowers.js";
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const ENTRYPOINT = "index.js";
|
|
9
|
+
// Exact-byte allowlist, matched as a unit: a bootstrap and the V2 entrypoint
|
|
10
|
+
// that shipped with it were qualified together in the native container. Adding
|
|
11
|
+
// either half alone would admit a combination no container ever ran. Every
|
|
12
|
+
// upstream change needs re-qualification before its bytes are added here.
|
|
13
|
+
const QUALIFIED_PROFILES = [
|
|
13
14
|
{
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
// obra/superpowers v6.0.0-v6.3.0 — no V2 entrypoint shipped
|
|
16
|
+
bootstrap: {
|
|
17
|
+
size: 5464,
|
|
18
|
+
sha256: "a5c5e1dbb0abfbd6ec3322b724b9a7b3318bbbb3d83f9a661c56ae0ed0a3adb8",
|
|
19
|
+
},
|
|
20
|
+
entrypoint: null,
|
|
16
21
|
},
|
|
17
|
-
// obra/superpowers v6.4.1
|
|
18
22
|
{
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
// obra/superpowers v6.4.1
|
|
24
|
+
bootstrap: {
|
|
25
|
+
size: 17617,
|
|
26
|
+
sha256: "c979fe5a9fd6fddc9bc9730b34b25989f9d53939eed7d594c4564f6e47495f26",
|
|
27
|
+
},
|
|
28
|
+
entrypoint: {
|
|
29
|
+
size: 536,
|
|
30
|
+
sha256: "f0132fd5339befeb99ba903b1619fd9530969e5b4a1ea8cecf0b0a7f0213a099",
|
|
31
|
+
},
|
|
21
32
|
},
|
|
22
33
|
];
|
|
23
|
-
function isReceiptCompatibility(value) {
|
|
24
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
25
|
-
return false;
|
|
26
|
-
const compatibility = value;
|
|
27
|
-
if (typeof compatibility.reason !== "string")
|
|
28
|
-
return false;
|
|
29
|
-
if (compatibility.kind === "supported" ||
|
|
30
|
-
compatibility.kind === "experimental")
|
|
31
|
-
return typeof compatibility.generation === "string";
|
|
32
|
-
return ((compatibility.kind === "unknown" ||
|
|
33
|
-
compatibility.kind === "unsupported") &&
|
|
34
|
-
compatibility.generation === undefined);
|
|
35
|
-
}
|
|
36
34
|
export async function assessOpenCodeCompatibility(root, selection) {
|
|
37
35
|
try {
|
|
38
36
|
validateSource(selection.effectiveSource);
|
|
@@ -45,11 +43,21 @@ export async function assessOpenCodeCompatibility(root, selection) {
|
|
|
45
43
|
throw new Error("package metadata");
|
|
46
44
|
requireNoSnapshotDependencies(pkg);
|
|
47
45
|
await validateNativeSkill(root);
|
|
48
|
-
await Promise.any(
|
|
46
|
+
const matched = await Promise.any(QUALIFIED_PROFILES.map(async (profile) => {
|
|
47
|
+
await requireSnapshotBootstrap(root, BOOTSTRAP, profile.bootstrap.size, profile.bootstrap.sha256);
|
|
48
|
+
if (profile.entrypoint === null)
|
|
49
|
+
await assertNoFollowType(join(root, ENTRYPOINT), ["missing"]);
|
|
50
|
+
else
|
|
51
|
+
await requireSnapshotBootstrap(root, ENTRYPOINT, profile.entrypoint.size, profile.entrypoint.sha256);
|
|
52
|
+
return profile;
|
|
53
|
+
}));
|
|
54
|
+
const generation = matched.entrypoint === null
|
|
55
|
+
? "opencode-native-bootstrap-v1"
|
|
56
|
+
: "opencode-native-bootstrap-v2";
|
|
49
57
|
const official = isOfficialSnapshotSource(selection.effectiveSource);
|
|
50
58
|
return {
|
|
51
59
|
kind: official ? "supported" : "experimental",
|
|
52
|
-
generation
|
|
60
|
+
generation,
|
|
53
61
|
reason: official
|
|
54
62
|
? "qualified upstream-native OpenCode bootstrap"
|
|
55
63
|
: "qualified OpenCode mechanics from a custom source",
|
|
@@ -62,41 +70,11 @@ export async function assessOpenCodeCompatibility(root, selection) {
|
|
|
62
70
|
};
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
typeof value.commit !== "string" ||
|
|
74
|
-
!COMMIT_RE.test(value.commit) ||
|
|
75
|
-
typeof value.digest !== "string" ||
|
|
76
|
-
!ARTIFACT_DIGEST_RE.test(value.digest) ||
|
|
77
|
-
typeof value.binding !== "string" ||
|
|
78
|
-
!ARTIFACT_DIGEST_RE.test(value.binding))
|
|
79
|
-
throw new Error("receipt fields");
|
|
80
|
-
validateSource(value.source);
|
|
81
|
-
if (snapshotReceiptBinding(value) !==
|
|
82
|
-
value.binding)
|
|
83
|
-
throw new Error("receipt binding");
|
|
84
|
-
if (!isReceiptCompatibility(value.compatibility))
|
|
85
|
-
throw new Error("receipt compatibility");
|
|
86
|
-
return value;
|
|
87
|
-
}
|
|
88
|
-
catch (cause) {
|
|
89
|
-
throw new SafetyError("opencode-package", `invalid OpenCode artifact receipt: ${path}`, { cause });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
export async function readOpenCodePackageAssessment(root) {
|
|
93
|
-
const receipt = await readOpenCodeReceipt(root);
|
|
94
|
-
if ((await digestArtifactTree(root)) !== receipt.digest)
|
|
95
|
-
throw new SafetyError("opencode-package", `OpenCode artifact digest mismatch: ${root}`);
|
|
96
|
-
return {
|
|
97
|
-
receipt,
|
|
98
|
-
compatibility: await assessOpenCodeCompatibility(root, {
|
|
99
|
-
effectiveSource: receipt.source,
|
|
100
|
-
}),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
73
|
+
const receipts = createSnapshotReceipts({
|
|
74
|
+
harness: "opencode",
|
|
75
|
+
label: "OpenCode",
|
|
76
|
+
strictGeneration: true,
|
|
77
|
+
assessCompatibility: assessOpenCodeCompatibility,
|
|
78
|
+
});
|
|
79
|
+
export const readOpenCodeReceipt = receipts.readReceipt;
|
|
80
|
+
export const readOpenCodePackageAssessment = receipts.readAssessment;
|
|
@@ -2,12 +2,6 @@ import { failureResult, successResult, } from "../../adapter-result.js";
|
|
|
2
2
|
import { createSnapshotPreparation } from "../../snapshot-prepare.js";
|
|
3
3
|
import { assertOpenCodePreparationSeparate, openCodePaths } from "./paths.js";
|
|
4
4
|
import { assessOpenCodeCompatibility, readOpenCodePackageAssessment, } from "./package.js";
|
|
5
|
-
export function openCodePreparationLocation(ctx) {
|
|
6
|
-
return {
|
|
7
|
-
destinationRoot: openCodePaths(ctx.env ?? {}, process.cwd()).preparedRoot,
|
|
8
|
-
stagingLeaf: "superpowers",
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
5
|
export async function validateOpenCodePreparationBeforeFetch(ctx) {
|
|
12
6
|
try {
|
|
13
7
|
const paths = openCodePaths(ctx.env ?? {}, process.cwd());
|
|
@@ -21,10 +15,11 @@ export async function validateOpenCodePreparationBeforeFetch(ctx) {
|
|
|
21
15
|
const preparation = createSnapshotPreparation({
|
|
22
16
|
harness: "opencode",
|
|
23
17
|
label: "OpenCode",
|
|
24
|
-
|
|
18
|
+
paths: openCodePaths,
|
|
25
19
|
assessCompatibility: assessOpenCodeCompatibility,
|
|
26
20
|
readAssessment: readOpenCodePackageAssessment,
|
|
27
21
|
});
|
|
22
|
+
export const openCodePreparationLocation = preparation.preparationLocation;
|
|
28
23
|
export const prepareOpenCodeCandidate = preparation.prepareCandidate;
|
|
29
24
|
export const inspectOpenCodePrepared = preparation.inspectPrepared;
|
|
30
25
|
export const readOpenCodePrepared = preparation.readPrepared;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { displaySource } from "../../selection.js";
|
|
2
|
-
import { createSnapshotPresentation } from "../../snapshot-presentation.js";
|
|
2
|
+
import { createSnapshotPresentation, probeOutput, } from "../../snapshot-presentation.js";
|
|
3
3
|
import { displayPath } from "../../validator.js";
|
|
4
4
|
const renderProbe = (facts) => {
|
|
5
5
|
const fields = [
|
|
@@ -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("; ")],
|
|
@@ -18,11 +19,6 @@ const renderProbe = (facts) => {
|
|
|
18
19
|
["compatibility_reason", facts.compatibility.reason],
|
|
19
20
|
["status", facts.status],
|
|
20
21
|
].map(([key, value]) => [key, displayPath(value)]);
|
|
21
|
-
return
|
|
22
|
-
human: fields
|
|
23
|
-
.map(([key, value]) => `${key.replaceAll("_", " ")}: ${value || "not present"}\n`)
|
|
24
|
-
.join(""),
|
|
25
|
-
porcelain: fields.map(([key, value]) => `${key}=${value}\n`).join(""),
|
|
26
|
-
};
|
|
22
|
+
return probeOutput(fields);
|
|
27
23
|
};
|
|
28
24
|
export const openCodePresentation = createSnapshotPresentation("OpenCode", renderProbe, (input) => input.receiptDigest === null && input.registration === null);
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { cp,
|
|
3
|
-
import { basename, dirname, join } from "node:path";
|
|
2
|
+
import { cp, mkdir, readdir, rm } from "node:fs/promises";
|
|
4
3
|
import { failureResult, successResult, } from "../../adapter-result.js";
|
|
5
|
-
import {
|
|
4
|
+
import { beginDirectoryPublication, } from "../../atomic.js";
|
|
6
5
|
import { digestArtifactTree } from "../../artifact-tree.js";
|
|
7
6
|
import { normalizeSnapshotRuntimeVersion } from "../../harness-command-result.js";
|
|
8
7
|
import { runPi } from "./native.js";
|
|
9
8
|
import { readPiPackageAssessment, readPiReceipt, } from "./package.js";
|
|
10
9
|
import { piPaths } from "./paths.js";
|
|
11
|
-
import { readPiSettings
|
|
10
|
+
import { readPiSettings } from "./settings.js";
|
|
12
11
|
import { inspectPiControl, inspectPiOwnership, } from "./state.js";
|
|
13
12
|
import { assertNoFollowType, canonicalizeProspectivePath, classifyPathNoFollow, } from "../../safe-path.js";
|
|
13
|
+
import { createJournal, createSnapshotJournal, hasRecovery, identity, journalPath, publicationPaths, requireNoRecovery, } from "../../snapshot-journal.js";
|
|
14
14
|
const DEFAULTS = {
|
|
15
15
|
run: runPi,
|
|
16
16
|
beginPublication: beginDirectoryPublication,
|
|
17
|
-
readSettings: readPiSettings,
|
|
18
17
|
};
|
|
19
18
|
function fail(code, message) {
|
|
20
19
|
return failureResult("pi-lifecycle", code, message, [], []);
|
|
@@ -24,20 +23,6 @@ function accepted(result) {
|
|
|
24
23
|
throw new Error("Pi operation did not succeed");
|
|
25
24
|
return result.outcome.result;
|
|
26
25
|
}
|
|
27
|
-
function sameIdentity(a, b) {
|
|
28
|
-
return a.dev === b.dev && a.ino === b.ino;
|
|
29
|
-
}
|
|
30
|
-
async function identity(path) {
|
|
31
|
-
await assertNoFollowType(path, ["directory"]);
|
|
32
|
-
return await lstat(path);
|
|
33
|
-
}
|
|
34
|
-
async function requireIdentity(path, expected) {
|
|
35
|
-
if (!sameIdentity(await identity(path), expected))
|
|
36
|
-
throw new Error("Pi directory changed");
|
|
37
|
-
}
|
|
38
|
-
function journalPath(p) {
|
|
39
|
-
return join(p.paths.recoveryRoot, "transaction.json");
|
|
40
|
-
}
|
|
41
26
|
function journalBytes(journal) {
|
|
42
27
|
return Buffer.from(JSON.stringify(journal) + "\n");
|
|
43
28
|
}
|
|
@@ -47,38 +32,15 @@ async function validatePaths(paths) {
|
|
|
47
32
|
await assertNoFollowType(paths.installedRoot, ["directory", "missing"]);
|
|
48
33
|
return await canonicalizeProspectivePath(paths.installedRoot);
|
|
49
34
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await directory.close();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async function hasRecovery(paths) {
|
|
63
|
-
try {
|
|
64
|
-
return (await classifyPathNoFollow(paths.recoveryRoot)) !== "missing";
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
async function registration(paths, deps) {
|
|
71
|
-
const settings = await deps.readSettings(paths.settingsFile, paths.homeDir);
|
|
72
|
-
const canonicalRoot = await canonicalizeProspectivePath(paths.installedRoot);
|
|
73
|
-
const matches = [];
|
|
74
|
-
for (const entry of settings.packages) {
|
|
75
|
-
if ((await resolveCanonicalPiLocalSource(entry.source, paths.agentDir, paths.homeDir)) === canonicalRoot) {
|
|
76
|
-
matches.push(entry);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (matches.length > 1)
|
|
80
|
-
throw new Error("duplicate Pi registration");
|
|
81
|
-
return matches[0] ?? null;
|
|
35
|
+
const { requireIdentity, requireJournal, phase, retireJournal } = createSnapshotJournal({
|
|
36
|
+
label: "Pi",
|
|
37
|
+
validatePaths,
|
|
38
|
+
serialize: journalBytes,
|
|
39
|
+
readCap: 64 * 1024,
|
|
40
|
+
});
|
|
41
|
+
async function registration(paths) {
|
|
42
|
+
return (await readPiSettings(paths.settingsFile, paths.homeDir))
|
|
43
|
+
.managerRegistration;
|
|
82
44
|
}
|
|
83
45
|
function sameRegistration(a, b) {
|
|
84
46
|
return a === null
|
|
@@ -115,39 +77,6 @@ async function requireSnapshot(root, expected) {
|
|
|
115
77
|
if (!sameArtifact(await snapshot(root), expected))
|
|
116
78
|
throw new Error("Pi artifact changed");
|
|
117
79
|
}
|
|
118
|
-
async function requireJournal(p) {
|
|
119
|
-
if ((await validatePaths(p.paths)) !== p.canonicalRoot ||
|
|
120
|
-
p.journal.installedRoot !== p.canonicalRoot ||
|
|
121
|
-
!/^[a-f0-9]{32}$/u.test(p.journal.token))
|
|
122
|
-
throw new Error("Pi recovery root changed");
|
|
123
|
-
await requireIdentity(p.paths.recoveryRoot, p.recoveryIdentity);
|
|
124
|
-
const path = journalPath(p);
|
|
125
|
-
await assertNoFollowType(path, ["regular-file"]);
|
|
126
|
-
const stat = await lstat(path);
|
|
127
|
-
if (!sameIdentity(stat, p.journalIdentity) ||
|
|
128
|
-
stat.size > 64 * 1024 ||
|
|
129
|
-
!(await readFile(path)).equals(journalBytes(p.journal)))
|
|
130
|
-
throw new Error("Pi recovery journal changed");
|
|
131
|
-
if (p.backup !==
|
|
132
|
-
join(dirname(p.canonicalRoot), `.${basename(p.canonicalRoot)}.bak.${p.journal.token}`) ||
|
|
133
|
-
p.stage !==
|
|
134
|
-
join(dirname(p.canonicalRoot), `.${basename(p.canonicalRoot)}.stage.${p.journal.token}`))
|
|
135
|
-
throw new Error("Pi recovery paths changed");
|
|
136
|
-
}
|
|
137
|
-
async function phase(p, next, changes = {}) {
|
|
138
|
-
await requireJournal(p);
|
|
139
|
-
const journal = { ...p.journal, ...changes, phase: next };
|
|
140
|
-
const bytes = journalBytes(journal);
|
|
141
|
-
await atomicWriteFile(journalPath(p), bytes, {
|
|
142
|
-
validate: async (temporary) => {
|
|
143
|
-
if (!(await readFile(temporary)).equals(bytes))
|
|
144
|
-
throw new Error("Pi journal write changed");
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
await syncDirectoryStrict(p.paths.recoveryRoot);
|
|
148
|
-
p.journal = journal;
|
|
149
|
-
p.journalIdentity = await lstat(journalPath(p));
|
|
150
|
-
}
|
|
151
80
|
async function beginJournal(paths, oldArtifact, newArtifact, priorRegistration, ctx, deps) {
|
|
152
81
|
const canonicalRoot = await validatePaths(paths);
|
|
153
82
|
await mkdir(paths.managerRoot, { recursive: true });
|
|
@@ -166,38 +95,20 @@ async function beginJournal(paths, oldArtifact, newArtifact, priorRegistration,
|
|
|
166
95
|
createdRegistration: null,
|
|
167
96
|
phase: newArtifact === null ? "removing" : "staging",
|
|
168
97
|
};
|
|
169
|
-
const path = join(paths.recoveryRoot, "transaction.json");
|
|
170
|
-
const handle = await open(path, "wx", 0o600);
|
|
171
|
-
try {
|
|
172
|
-
await handle.writeFile(journalBytes(journal));
|
|
173
|
-
await handle.sync();
|
|
174
|
-
}
|
|
175
|
-
finally {
|
|
176
|
-
await handle.close();
|
|
177
|
-
}
|
|
178
98
|
// The first journal is durable before either a staging tree or backup exists.
|
|
179
|
-
await
|
|
99
|
+
const journalIdentity = await createJournal(paths.recoveryRoot, journalBytes(journal));
|
|
180
100
|
return {
|
|
181
101
|
paths,
|
|
182
102
|
canonicalRoot,
|
|
183
103
|
recoveryIdentity,
|
|
184
|
-
|
|
185
|
-
stage: join(dirname(canonicalRoot), `.${basename(canonicalRoot)}.stage.${token}`),
|
|
104
|
+
...publicationPaths(canonicalRoot, token),
|
|
186
105
|
ctx,
|
|
187
106
|
deps,
|
|
188
107
|
journal,
|
|
189
|
-
journalIdentity
|
|
108
|
+
journalIdentity,
|
|
190
109
|
settled: false,
|
|
191
110
|
};
|
|
192
111
|
}
|
|
193
|
-
async function retireJournal(p) {
|
|
194
|
-
await requireJournal(p);
|
|
195
|
-
if ((await readdir(p.paths.recoveryRoot)).some((name) => name !== "transaction.json"))
|
|
196
|
-
throw new Error("unknown Pi recovery material");
|
|
197
|
-
await unlink(journalPath(p));
|
|
198
|
-
await requireIdentity(p.paths.recoveryRoot, p.recoveryIdentity);
|
|
199
|
-
await rmdir(p.paths.recoveryRoot);
|
|
200
|
-
}
|
|
201
112
|
async function journalRetirementGuidance(p) {
|
|
202
113
|
try {
|
|
203
114
|
const kind = await classifyPathNoFollow(p.paths.recoveryRoot);
|
|
@@ -243,7 +154,7 @@ async function finalizePiPublication(p, publication) {
|
|
|
243
154
|
let publicationCleanupCompleted = false;
|
|
244
155
|
try {
|
|
245
156
|
await requirePublication(p);
|
|
246
|
-
const current = await registration(p.paths
|
|
157
|
+
const current = await registration(p.paths);
|
|
247
158
|
const expectedRegistration = p.journal.priorRegistration ?? p.journal.createdRegistration;
|
|
248
159
|
if (expectedRegistration === null ||
|
|
249
160
|
!sameRegistration(current, expectedRegistration) ||
|
|
@@ -273,7 +184,7 @@ async function rollbackPiPublication(p, publication) {
|
|
|
273
184
|
let retirementStarted = false;
|
|
274
185
|
try {
|
|
275
186
|
await requirePublication(p);
|
|
276
|
-
const current = await registration(p.paths
|
|
187
|
+
const current = await registration(p.paths);
|
|
277
188
|
if (p.journal.priorRegistration !== null) {
|
|
278
189
|
if (!sameRegistration(current, p.journal.priorRegistration))
|
|
279
190
|
throw new Error("prior Pi registration changed");
|
|
@@ -288,13 +199,13 @@ async function rollbackPiPublication(p, publication) {
|
|
|
288
199
|
await p.deps
|
|
289
200
|
.run(["remove", p.canonicalRoot, "--no-approve"], p.paths, p.ctx)
|
|
290
201
|
.catch(() => undefined);
|
|
291
|
-
if ((await registration(p.paths
|
|
202
|
+
if ((await registration(p.paths)) !== null)
|
|
292
203
|
throw new Error("Pi deregistration unverified");
|
|
293
204
|
}
|
|
294
205
|
await requirePublication(p);
|
|
295
206
|
await publication.rollback();
|
|
296
207
|
await requireSnapshot(p.canonicalRoot, p.journal.oldArtifact);
|
|
297
|
-
if (!sameRegistration(await registration(p.paths
|
|
208
|
+
if (!sameRegistration(await registration(p.paths), p.journal.priorRegistration))
|
|
298
209
|
throw new Error("Pi registration restoration unverified");
|
|
299
210
|
await phase(p, "restored");
|
|
300
211
|
retirementStarted = true;
|
|
@@ -312,7 +223,7 @@ async function recoverBeforePublication(p) {
|
|
|
312
223
|
await requireJournal(p);
|
|
313
224
|
await requireSnapshot(p.canonicalRoot, p.journal.oldArtifact);
|
|
314
225
|
await assertNoFollowType(p.backup, ["missing"]);
|
|
315
|
-
if (!sameRegistration(await registration(p.paths
|
|
226
|
+
if (!sameRegistration(await registration(p.paths), p.journal.priorRegistration))
|
|
316
227
|
return false;
|
|
317
228
|
if ((await assertNoFollowType(p.stage, ["directory", "missing"])) !==
|
|
318
229
|
"missing") {
|
|
@@ -346,13 +257,13 @@ export async function installPi(artifact, ctx, deps = DEFAULTS) {
|
|
|
346
257
|
throw new Error("Pi prepared artifact changed");
|
|
347
258
|
await requireActivationEligibility(ctx);
|
|
348
259
|
const previous = await snapshot(paths.installedRoot);
|
|
349
|
-
const priorRegistration = await registration(paths
|
|
260
|
+
const priorRegistration = await registration(paths);
|
|
350
261
|
if (previous === null && priorRegistration !== null)
|
|
351
262
|
throw new Error("unowned Pi registration");
|
|
352
263
|
accepted(normalizeSnapshotRuntimeVersion("Pi", await deps.run(["--version"], paths, ctx)));
|
|
353
264
|
// Recheck after the native preflight, before claiming mutation ownership.
|
|
354
265
|
await requireSnapshot(paths.installedRoot, previous);
|
|
355
|
-
if (!sameRegistration(await registration(paths
|
|
266
|
+
if (!sameRegistration(await registration(paths), priorRegistration))
|
|
356
267
|
throw new Error("Pi registration changed");
|
|
357
268
|
pending = await beginJournal(paths, previous, assessment.receipt, priorRegistration, ctx, deps);
|
|
358
269
|
await mkdir(pending.stage);
|
|
@@ -371,20 +282,20 @@ export async function installPi(artifact, ctx, deps = DEFAULTS) {
|
|
|
371
282
|
await requireIdentity(pending.stage, pending.stageIdentity);
|
|
372
283
|
await requireActivationEligibility(ctx);
|
|
373
284
|
await requireSnapshot(paths.installedRoot, previous);
|
|
374
|
-
if (!sameRegistration(await registration(paths
|
|
285
|
+
if (!sameRegistration(await registration(paths), priorRegistration))
|
|
375
286
|
throw new Error("Pi registration changed");
|
|
376
287
|
publication = await deps.beginPublication(pending.stage, pending.canonicalRoot, { backupPath: pending.backup });
|
|
377
288
|
pending.publishedIdentity = pending.stageIdentity;
|
|
378
289
|
await phase(pending, "published");
|
|
379
290
|
if (priorRegistration === null) {
|
|
380
291
|
await phase(pending, "registering");
|
|
381
|
-
if ((await registration(paths
|
|
292
|
+
if ((await registration(paths)) !== null)
|
|
382
293
|
throw new Error("Pi registration changed");
|
|
383
294
|
const result = await deps
|
|
384
295
|
.run(["install", pending.canonicalRoot, "--no-approve"], paths, ctx)
|
|
385
296
|
.catch(() => fail("native-failed", "Pi native installation failed"));
|
|
386
297
|
// Even an unsuccessful command may have changed settings. Read them first.
|
|
387
|
-
const registered = await registration(paths
|
|
298
|
+
const registered = await registration(paths);
|
|
388
299
|
if (registered === null || registered.resourceState !== "enabled")
|
|
389
300
|
throw new Error("Pi activation unverified");
|
|
390
301
|
await phase(pending, "registered", {
|
|
@@ -393,7 +304,7 @@ export async function installPi(artifact, ctx, deps = DEFAULTS) {
|
|
|
393
304
|
accepted(result);
|
|
394
305
|
}
|
|
395
306
|
await requirePublication(pending);
|
|
396
|
-
const registered = await registration(paths
|
|
307
|
+
const registered = await registration(paths);
|
|
397
308
|
if (registered === null || registered.resourceState !== "enabled")
|
|
398
309
|
throw new Error("Pi activation unverified");
|
|
399
310
|
await phase(pending, "ready");
|
|
@@ -444,7 +355,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
|
|
|
444
355
|
input.receiptDigest !== current.receiptDigest)
|
|
445
356
|
throw new Error("stale Pi removal input");
|
|
446
357
|
const previous = await snapshot(paths.installedRoot);
|
|
447
|
-
const registered = await registration(paths
|
|
358
|
+
const registered = await registration(paths);
|
|
448
359
|
if (previous === null) {
|
|
449
360
|
if (registered !== null)
|
|
450
361
|
throw new Error("unowned Pi registration");
|
|
@@ -453,7 +364,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
|
|
|
453
364
|
if (registered !== null)
|
|
454
365
|
accepted(normalizeSnapshotRuntimeVersion("Pi", await deps.run(["--version"], paths, ctx)));
|
|
455
366
|
await requireSnapshot(paths.installedRoot, previous);
|
|
456
|
-
if (!sameRegistration(await registration(paths
|
|
367
|
+
if (!sameRegistration(await registration(paths), registered))
|
|
457
368
|
throw new Error("Pi registration changed");
|
|
458
369
|
pending = await beginJournal(paths, previous, null, registered, ctx, deps);
|
|
459
370
|
const installedIdentity = await identity(paths.installedRoot);
|
|
@@ -461,7 +372,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
|
|
|
461
372
|
const result = await deps
|
|
462
373
|
.run(["remove", pending.canonicalRoot, "--no-approve"], paths, ctx)
|
|
463
374
|
.catch(() => fail("native-failed", "Pi native removal failed"));
|
|
464
|
-
const after = await registration(paths
|
|
375
|
+
const after = await registration(paths);
|
|
465
376
|
if (after !== null) {
|
|
466
377
|
if (sameRegistration(after, registered)) {
|
|
467
378
|
await requireSnapshot(paths.installedRoot, previous);
|
|
@@ -480,7 +391,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
|
|
|
480
391
|
await phase(pending, "deregistered");
|
|
481
392
|
await requireIdentity(paths.installedRoot, installedIdentity);
|
|
482
393
|
await requireSnapshot(paths.installedRoot, previous);
|
|
483
|
-
if ((await registration(paths
|
|
394
|
+
if ((await registration(paths)) !== null)
|
|
484
395
|
throw new Error("Pi registration returned");
|
|
485
396
|
await rm(paths.installedRoot, { recursive: true });
|
|
486
397
|
await requireSnapshot(paths.installedRoot, null);
|
|
@@ -1,37 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { failureResult, } from "../../adapter-result.js";
|
|
4
|
-
import { nativeCommandResult, } from "../../harness-command-result.js";
|
|
1
|
+
import {} from "../../adapter-result.js";
|
|
2
|
+
import { runIsolatedNative, } from "../../harness-command-result.js";
|
|
5
3
|
import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "../../validator.js";
|
|
6
|
-
import { withWorkspace } from "../../workspace.js";
|
|
7
|
-
function selectedExecutable(env, invocationCwd) {
|
|
8
|
-
const configured = env.SUPERPOWERS_PI || "pi";
|
|
9
|
-
return configured.includes(sep) && !isAbsolute(configured)
|
|
10
|
-
? resolve(invocationCwd, configured)
|
|
11
|
-
: configured;
|
|
12
|
-
}
|
|
13
4
|
export async function runPi(args, paths, ctx, execute = runBoundedCommand) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
entered = true;
|
|
22
|
-
const result = await execute([executable, ...args], BOUNDED_EXECUTABLE, {
|
|
23
|
-
...env,
|
|
24
|
-
HOME: paths.homeDir,
|
|
25
|
-
PI_CODING_AGENT_DIR: paths.agentDir,
|
|
26
|
-
PI_OFFLINE: "1",
|
|
27
|
-
PI_SKIP_VERSION_CHECK: "1",
|
|
28
|
-
}, workspace, workspace);
|
|
29
|
-
return nativeCommandResult("Pi", result);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return failureResult(operation, "workspace-failed", entered
|
|
34
|
-
? "cannot complete Pi command in its isolated workspace"
|
|
35
|
-
: "cannot create an isolated Pi command workspace", [], []);
|
|
36
|
-
}
|
|
5
|
+
return await runIsolatedNative("Pi", (ctx.env ?? {}).SUPERPOWERS_PI || "pi", ctx, async (executable, workspace, env) => await execute([executable, ...args], BOUNDED_EXECUTABLE, {
|
|
6
|
+
...env,
|
|
7
|
+
HOME: paths.homeDir,
|
|
8
|
+
PI_CODING_AGENT_DIR: paths.agentDir,
|
|
9
|
+
PI_OFFLINE: "1",
|
|
10
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
11
|
+
}, workspace, workspace));
|
|
37
12
|
}
|