superpowers-manager 0.5.2 → 0.7.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 +44 -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 +32 -52
- package/dist/commands/adapter-call.js +28 -0
- package/dist/commands/install.js +26 -107
- package/dist/commands/mutation.js +7 -0
- package/dist/commands/pin.js +1 -1
- package/dist/commands/prepare.js +39 -129
- package/dist/commands/probe.js +35 -264
- package/dist/commands/track-latest.js +1 -1
- package/dist/commands/uninstall.js +16 -92
- package/dist/commands/update.js +5 -25
- package/dist/effective-selection.js +7 -9
- package/dist/git-tree.js +85 -0
- package/dist/harness-command-result.js +47 -0
- package/dist/harnesses/codex/adapter.js +231 -510
- package/dist/harnesses/codex/compatibility.js +2 -17
- package/dist/harnesses/codex/conflicts.js +2 -3
- package/dist/harnesses/codex/harness.js +18 -263
- package/dist/harnesses/codex/hooks.js +1 -1
- package/dist/harnesses/codex/json.js +1 -11
- package/dist/harnesses/codex/lifecycle.js +80 -72
- package/dist/harnesses/codex/manifest-overlay.js +23 -18
- package/dist/harnesses/codex/paths.js +14 -29
- package/dist/harnesses/codex/presentation.js +8 -37
- package/dist/harnesses/codex/publication.js +13 -37
- 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 +587 -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/python-json-format.js +0 -69
- package/dist/python-json.js +2 -27
- package/dist/python-text.js +9 -69
- 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 +39 -73
- 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/selection-state-cli.js +0 -85
- package/dist/status.js +0 -24
- package/dist/upstream-cli.js +0 -110
- package/dist/validate-generated-plugin-cli.js +0 -118
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
|
|
3
|
+
import { COMMIT_RE, SEMVER_RE } from "../../domain/refs.js";
|
|
4
|
+
import { validateSource } from "../../selection.js";
|
|
5
|
+
import { SafetyError } from "../../safety-error.js";
|
|
6
|
+
import { isOfficialSnapshotSource, requireNoSnapshotDependencies, requireSnapshotBootstrap, snapshotReceiptBinding, } from "../../snapshot-package.js";
|
|
7
|
+
const BOOTSTRAP = ".opencode/plugins/superpowers.js";
|
|
8
|
+
const BOOTSTRAP_SIZE = 5464;
|
|
9
|
+
const BOOTSTRAP_SHA256 = "a5c5e1dbb0abfbd6ec3322b724b9a7b3318bbbb3d83f9a661c56ae0ed0a3adb8";
|
|
10
|
+
function isReceiptCompatibility(value) {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
|
+
return false;
|
|
13
|
+
const compatibility = value;
|
|
14
|
+
if (typeof compatibility.reason !== "string")
|
|
15
|
+
return false;
|
|
16
|
+
if (compatibility.kind === "supported" ||
|
|
17
|
+
compatibility.kind === "experimental")
|
|
18
|
+
return typeof compatibility.generation === "string";
|
|
19
|
+
return ((compatibility.kind === "unknown" ||
|
|
20
|
+
compatibility.kind === "unsupported") &&
|
|
21
|
+
compatibility.generation === undefined);
|
|
22
|
+
}
|
|
23
|
+
export async function assessOpenCodeCompatibility(root, selection) {
|
|
24
|
+
try {
|
|
25
|
+
validateSource(selection.effectiveSource);
|
|
26
|
+
const pkg = await readArtifactObject(root, join(root, "package.json"));
|
|
27
|
+
if (pkg.name !== "superpowers" ||
|
|
28
|
+
pkg.type !== "module" ||
|
|
29
|
+
pkg.main !== BOOTSTRAP ||
|
|
30
|
+
typeof pkg.version !== "string" ||
|
|
31
|
+
!SEMVER_RE.test(pkg.version))
|
|
32
|
+
throw new Error("package metadata");
|
|
33
|
+
requireNoSnapshotDependencies(pkg);
|
|
34
|
+
await validateNativeSkill(root);
|
|
35
|
+
await requireSnapshotBootstrap(root, BOOTSTRAP, BOOTSTRAP_SIZE, BOOTSTRAP_SHA256);
|
|
36
|
+
const official = isOfficialSnapshotSource(selection.effectiveSource);
|
|
37
|
+
return {
|
|
38
|
+
kind: official ? "supported" : "experimental",
|
|
39
|
+
generation: "opencode-native-bootstrap-v1",
|
|
40
|
+
reason: official
|
|
41
|
+
? "qualified upstream-native OpenCode bootstrap"
|
|
42
|
+
: "qualified OpenCode mechanics from a custom source",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {
|
|
47
|
+
kind: "unsupported",
|
|
48
|
+
reason: `OpenCode package does not match the qualified native bootstrap profile: ${root}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function readOpenCodeReceipt(root) {
|
|
53
|
+
const path = join(root, ARTIFACT_RECEIPT);
|
|
54
|
+
try {
|
|
55
|
+
const value = await readArtifactObject(root, path);
|
|
56
|
+
if (value.schema !== 1 ||
|
|
57
|
+
value.manager !== "superpowers-manager" ||
|
|
58
|
+
value.harness !== "opencode" ||
|
|
59
|
+
typeof value.source !== "string" ||
|
|
60
|
+
typeof value.commit !== "string" ||
|
|
61
|
+
!COMMIT_RE.test(value.commit) ||
|
|
62
|
+
typeof value.digest !== "string" ||
|
|
63
|
+
!ARTIFACT_DIGEST_RE.test(value.digest) ||
|
|
64
|
+
typeof value.binding !== "string" ||
|
|
65
|
+
!ARTIFACT_DIGEST_RE.test(value.binding))
|
|
66
|
+
throw new Error("receipt fields");
|
|
67
|
+
validateSource(value.source);
|
|
68
|
+
if (snapshotReceiptBinding(value) !==
|
|
69
|
+
value.binding)
|
|
70
|
+
throw new Error("receipt binding");
|
|
71
|
+
if (!isReceiptCompatibility(value.compatibility))
|
|
72
|
+
throw new Error("receipt compatibility");
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
throw new SafetyError("opencode-package", `invalid OpenCode artifact receipt: ${path}`, { cause });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function readOpenCodePackageAssessment(root) {
|
|
80
|
+
const receipt = await readOpenCodeReceipt(root);
|
|
81
|
+
if ((await digestArtifactTree(root)) !== receipt.digest)
|
|
82
|
+
throw new SafetyError("opencode-package", `OpenCode artifact digest mismatch: ${root}`);
|
|
83
|
+
return {
|
|
84
|
+
receipt,
|
|
85
|
+
compatibility: await assessOpenCodeCompatibility(root, {
|
|
86
|
+
effectiveSource: receipt.source,
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { assertNoFollowType, canonicalizeProspectivePath, isContained, } from "../../safe-path.js";
|
|
4
|
+
function overlaps(left, right) {
|
|
5
|
+
return isContained(left, right) || isContained(right, left);
|
|
6
|
+
}
|
|
7
|
+
export async function assertOpenCodePreparationSeparate(paths) {
|
|
8
|
+
await assertNoFollowType(paths.configRoot, ["directory", "missing"]);
|
|
9
|
+
await assertNoFollowType(paths.managerRoot, ["directory", "missing"]);
|
|
10
|
+
const roots = [paths.preparedRoot, paths.installedRoot, paths.recoveryRoot];
|
|
11
|
+
const canonical = await Promise.all(roots.map(async (root) => {
|
|
12
|
+
await assertNoFollowType(root, ["directory", "missing"]);
|
|
13
|
+
return await canonicalizeProspectivePath(root);
|
|
14
|
+
}));
|
|
15
|
+
if (overlaps(canonical[0], canonical[1]) ||
|
|
16
|
+
overlaps(canonical[0], canonical[2]) ||
|
|
17
|
+
overlaps(canonical[1], canonical[2]))
|
|
18
|
+
throw new Error("storage roots overlap");
|
|
19
|
+
}
|
|
20
|
+
export function openCodePaths(env, cwd) {
|
|
21
|
+
const home = env.HOME && env.HOME.length > 0 ? env.HOME : homedir();
|
|
22
|
+
const homeDir = resolve(cwd, home);
|
|
23
|
+
const configured = env.XDG_CONFIG_HOME;
|
|
24
|
+
if (configured !== undefined &&
|
|
25
|
+
configured.length > 0 &&
|
|
26
|
+
!isAbsolute(configured))
|
|
27
|
+
throw new Error("XDG_CONFIG_HOME must be absolute");
|
|
28
|
+
const configRoot = join(configured && configured.length > 0 ? configured : join(homeDir, ".config"), "opencode");
|
|
29
|
+
const managerRoot = join(configRoot, "superpowers-manager");
|
|
30
|
+
return {
|
|
31
|
+
homeDir,
|
|
32
|
+
configRoot,
|
|
33
|
+
managerRoot,
|
|
34
|
+
preparedRoot: join(managerRoot, "prepared"),
|
|
35
|
+
installedRoot: join(managerRoot, "installed"),
|
|
36
|
+
recoveryRoot: join(managerRoot, "recovery"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { failureResult, successResult, } from "../../adapter-result.js";
|
|
2
|
+
import { createSnapshotPreparation } from "../../snapshot-prepare.js";
|
|
3
|
+
import { assertOpenCodePreparationSeparate, openCodePaths } from "./paths.js";
|
|
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
|
+
export async function validateOpenCodePreparationBeforeFetch(ctx) {
|
|
12
|
+
try {
|
|
13
|
+
const paths = openCodePaths(ctx.env ?? {}, process.cwd());
|
|
14
|
+
await assertOpenCodePreparationSeparate(paths);
|
|
15
|
+
return successResult("prepare", null, []);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return failureResult("prepare", "invalid-package", "cannot validate OpenCode artifact storage", [], []);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const preparation = createSnapshotPreparation({
|
|
22
|
+
harness: "opencode",
|
|
23
|
+
label: "OpenCode",
|
|
24
|
+
preparationLocation: openCodePreparationLocation,
|
|
25
|
+
assessCompatibility: assessOpenCodeCompatibility,
|
|
26
|
+
readAssessment: readOpenCodePackageAssessment,
|
|
27
|
+
});
|
|
28
|
+
export const prepareOpenCodeCandidate = preparation.prepareCandidate;
|
|
29
|
+
export const inspectOpenCodePrepared = preparation.inspectPrepared;
|
|
30
|
+
export const readOpenCodePrepared = preparation.readPrepared;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { displaySource } from "../../selection.js";
|
|
2
|
+
import { createSnapshotPresentation } from "../../snapshot-presentation.js";
|
|
3
|
+
import { displayPath } from "../../validator.js";
|
|
4
|
+
const renderProbe = (facts) => {
|
|
5
|
+
const fields = [
|
|
6
|
+
["harness", "opencode"],
|
|
7
|
+
["desired_commit", facts.selection.desiredCommit],
|
|
8
|
+
["upstream_source_origin", facts.selection.upstreamSourceOrigin],
|
|
9
|
+
["effective_source", displaySource(facts.selection.effectiveSource)],
|
|
10
|
+
["prepared_identity", facts.prepared.observedIdentity],
|
|
11
|
+
["installed_identity", facts.installed.observedIdentity],
|
|
12
|
+
["installation_state", facts.installed.kind],
|
|
13
|
+
["resource_state", facts.resourceState ?? "idle"],
|
|
14
|
+
["ownership", facts.ownership.presentationValue],
|
|
15
|
+
["conflicts", (facts.ownership.presentationConflicts ?? []).join("; ")],
|
|
16
|
+
["update_control", facts.control.presentationValue],
|
|
17
|
+
["compatibility", facts.compatibility.kind],
|
|
18
|
+
["compatibility_reason", facts.compatibility.reason],
|
|
19
|
+
["status", facts.status],
|
|
20
|
+
].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
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export const openCodePresentation = createSnapshotPresentation("OpenCode", renderProbe, (input) => input.receiptDigest === null && input.registration === null);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { inspectionFailure, successResult, } from "../../adapter-result.js";
|
|
2
|
+
import { displayPath } from "../../validator.js";
|
|
3
|
+
import { observeSnapshot, sameSnapshotSource, } from "../../snapshot-package.js";
|
|
4
|
+
import { classifyPathNoFollow } from "../../safe-path.js";
|
|
5
|
+
import { inspectOpenCodeDiscovery, } from "./discovery.js";
|
|
6
|
+
import { assessOpenCodeCompatibility, readOpenCodePackageAssessment, readOpenCodeReceipt, } from "./package.js";
|
|
7
|
+
import { openCodePaths } from "./paths.js";
|
|
8
|
+
function blocked(...stderr) {
|
|
9
|
+
return {
|
|
10
|
+
kind: "blocked",
|
|
11
|
+
output: { stdout: [], stderr: stderr.map((line) => displayPath(line)) },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function pathsFor(ctx) {
|
|
15
|
+
return openCodePaths(ctx.env ?? {}, process.cwd());
|
|
16
|
+
}
|
|
17
|
+
async function observeFacts(ctx) {
|
|
18
|
+
const paths = pathsFor(ctx);
|
|
19
|
+
const [discovery, snapshot, recoveryKind] = await Promise.all([
|
|
20
|
+
inspectOpenCodeDiscovery(paths, ctx.env ?? {}, process.cwd()),
|
|
21
|
+
observeSnapshot(paths.installedRoot, readOpenCodeReceipt),
|
|
22
|
+
classifyPathNoFollow(paths.recoveryRoot),
|
|
23
|
+
]);
|
|
24
|
+
return {
|
|
25
|
+
paths,
|
|
26
|
+
discovery,
|
|
27
|
+
snapshot,
|
|
28
|
+
recovery: recoveryKind === "missing" ? "absent" : "required",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function assertInspectableFacts(facts) {
|
|
32
|
+
if (facts.discovery.managedEntries.length > 1)
|
|
33
|
+
throw new Error("ambiguous Manager registration");
|
|
34
|
+
if (facts.snapshot.kind === "unverified")
|
|
35
|
+
throw new Error("unverified Manager snapshot");
|
|
36
|
+
if (facts.discovery.managedEntries.length === 1 &&
|
|
37
|
+
facts.snapshot.kind === "absent")
|
|
38
|
+
throw new Error("unverified Manager registration");
|
|
39
|
+
}
|
|
40
|
+
function mutationDecision(facts) {
|
|
41
|
+
if (facts.recovery === "required")
|
|
42
|
+
return blocked(`error: OpenCode recovery required; preserve material at ${facts.paths.recoveryRoot}`);
|
|
43
|
+
if (facts.discovery.blockedInputs.length > 0)
|
|
44
|
+
return blocked("OpenCode configuration activity could not be established read-only:", ...facts.discovery.blockedInputs.map((input) => `- ${input}`));
|
|
45
|
+
if (facts.discovery.conflicts.length > 0)
|
|
46
|
+
return blocked("Conflicting unmanaged Superpowers OpenCode resources require manual resolution:", ...facts.discovery.conflicts.map((conflict) => `- ${conflict}`), "Remove or disable each resource manually, then retry.");
|
|
47
|
+
return { kind: "allowed" };
|
|
48
|
+
}
|
|
49
|
+
function registrationFor(facts) {
|
|
50
|
+
const managed = facts.discovery.managedEntries[0];
|
|
51
|
+
return managed === undefined
|
|
52
|
+
? null
|
|
53
|
+
: {
|
|
54
|
+
observation: managed.observation,
|
|
55
|
+
entryIndex: managed.entry.index,
|
|
56
|
+
spec: managed.entry.spec,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export async function inspectOpenCodeOwnership(ctx) {
|
|
60
|
+
const operation = "inspect-opencode-ownership";
|
|
61
|
+
try {
|
|
62
|
+
const facts = await observeFacts(ctx);
|
|
63
|
+
assertInspectableFacts(facts);
|
|
64
|
+
const registration = registrationFor(facts);
|
|
65
|
+
const removalVerification = registration !== null
|
|
66
|
+
? blocked("error: the Manager OpenCode registration is still present after removal")
|
|
67
|
+
: facts.snapshot.kind === "owned"
|
|
68
|
+
? blocked("error: the Manager-owned OpenCode snapshot is still present after removal")
|
|
69
|
+
: facts.discovery.registrationUncertain
|
|
70
|
+
? blocked("error: unresolved OpenCode configuration remains after removal", ...facts.discovery.blockedInputs.map((input) => `- ${input}`))
|
|
71
|
+
: { kind: "allowed" };
|
|
72
|
+
const conflicts = [
|
|
73
|
+
...facts.discovery.conflicts,
|
|
74
|
+
...facts.discovery.blockedInputs.map((input) => `${input} is unresolved`),
|
|
75
|
+
];
|
|
76
|
+
const presentationValue = facts.snapshot.kind === "owned"
|
|
77
|
+
? registration === null
|
|
78
|
+
? `managed snapshot ${facts.snapshot.digest}`
|
|
79
|
+
: `managed ${facts.snapshot.digest}`
|
|
80
|
+
: facts.discovery.registrationUncertain
|
|
81
|
+
? "unresolved configuration"
|
|
82
|
+
: "absent";
|
|
83
|
+
return successResult(operation, {
|
|
84
|
+
installEligibility: mutationDecision(facts),
|
|
85
|
+
removalInput: {
|
|
86
|
+
installedRoot: facts.paths.installedRoot,
|
|
87
|
+
registration,
|
|
88
|
+
receiptDigest: facts.snapshot.kind === "owned"
|
|
89
|
+
? facts.snapshot.receipt.digest
|
|
90
|
+
: null,
|
|
91
|
+
},
|
|
92
|
+
removalVerification,
|
|
93
|
+
postRemovalOutput: { stdout: [], stderr: [] },
|
|
94
|
+
presentationValue,
|
|
95
|
+
presentationConflicts: conflicts,
|
|
96
|
+
}, []);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return inspectionFailure(operation, "OpenCode ownership");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function mismatch(operation, observedIdentity) {
|
|
103
|
+
return successResult(operation, { kind: "mismatch", observedIdentity }, []);
|
|
104
|
+
}
|
|
105
|
+
export async function inspectOpenCodeInstalled(selection, ctx) {
|
|
106
|
+
const operation = "inspect-opencode-installed";
|
|
107
|
+
let facts;
|
|
108
|
+
try {
|
|
109
|
+
facts = await observeFacts(ctx);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return inspectionFailure(operation, "OpenCode installed state");
|
|
113
|
+
}
|
|
114
|
+
if (facts.snapshot.kind === "absent") {
|
|
115
|
+
if (facts.discovery.managedEntries.length > 0)
|
|
116
|
+
return mismatch(operation, "registered without an installed snapshot");
|
|
117
|
+
return facts.discovery.registrationUncertain
|
|
118
|
+
? mismatch(operation, "unresolved OpenCode configuration")
|
|
119
|
+
: successResult(operation, { kind: "absent", observedIdentity: "" }, []);
|
|
120
|
+
}
|
|
121
|
+
if (facts.snapshot.kind === "unverified")
|
|
122
|
+
return mismatch(operation, "unverified installed snapshot");
|
|
123
|
+
const observed = facts.snapshot.digest;
|
|
124
|
+
if (facts.discovery.managedEntries.length > 1)
|
|
125
|
+
return inspectionFailure(operation, "OpenCode installed state");
|
|
126
|
+
if (facts.discovery.managedEntries.length !== 1 ||
|
|
127
|
+
facts.discovery.conflicts.length > 0 ||
|
|
128
|
+
facts.discovery.blockedInputs.length > 0)
|
|
129
|
+
return mismatch(operation, observed);
|
|
130
|
+
try {
|
|
131
|
+
const prepared = await readOpenCodePackageAssessment(facts.paths.preparedRoot);
|
|
132
|
+
const installedProfile = await assessOpenCodeCompatibility(facts.paths.installedRoot, { effectiveSource: facts.snapshot.receipt.source });
|
|
133
|
+
if ((prepared.compatibility.kind !== "supported" &&
|
|
134
|
+
prepared.compatibility.kind !== "experimental") ||
|
|
135
|
+
(installedProfile.kind !== "supported" &&
|
|
136
|
+
installedProfile.kind !== "experimental") ||
|
|
137
|
+
prepared.receipt.commit !== selection.desiredCommit ||
|
|
138
|
+
!sameSnapshotSource(prepared.receipt.source, selection.effectiveSource) ||
|
|
139
|
+
facts.snapshot.receipt.commit !== prepared.receipt.commit ||
|
|
140
|
+
!sameSnapshotSource(facts.snapshot.receipt.source, prepared.receipt.source) ||
|
|
141
|
+
facts.snapshot.digest !== prepared.receipt.digest)
|
|
142
|
+
return mismatch(operation, observed);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return mismatch(operation, observed);
|
|
146
|
+
}
|
|
147
|
+
return successResult(operation, { kind: "current", observedIdentity: observed }, []);
|
|
148
|
+
}
|
|
149
|
+
export async function inspectOpenCodeControl(ctx) {
|
|
150
|
+
const operation = "inspect-opencode-control";
|
|
151
|
+
try {
|
|
152
|
+
const facts = await observeFacts(ctx);
|
|
153
|
+
assertInspectableFacts(facts);
|
|
154
|
+
const mutationEligibility = mutationDecision(facts);
|
|
155
|
+
return successResult(operation, {
|
|
156
|
+
probeEligibility: { kind: "allowed" },
|
|
157
|
+
mutationEligibility,
|
|
158
|
+
presentationValue: facts.recovery === "required"
|
|
159
|
+
? `recovery required at ${facts.paths.recoveryRoot}`
|
|
160
|
+
: facts.discovery.managedEntries.length === 1
|
|
161
|
+
? "managed local registration"
|
|
162
|
+
: facts.discovery.conflicts.length > 0 ||
|
|
163
|
+
facts.discovery.blockedInputs.length > 0
|
|
164
|
+
? "unmanaged or unresolved configuration"
|
|
165
|
+
: "unregistered",
|
|
166
|
+
...(facts.recovery === "required"
|
|
167
|
+
? { recoveryState: "required" }
|
|
168
|
+
: {}),
|
|
169
|
+
}, []);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return inspectionFailure(operation, "OpenCode update control");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -1,25 +1,12 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstat } from "node:fs/promises";
|
|
3
1
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
2
|
+
import { readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
|
|
5
3
|
import { SEMVER_RE } from "../../domain/refs.js";
|
|
6
4
|
import { validateSource } from "../../selection.js";
|
|
7
|
-
|
|
8
|
-
"https://github.com/obra/superpowers",
|
|
9
|
-
"https://github.com/obra/superpowers.git",
|
|
10
|
-
"ssh://git@github.com/obra/superpowers.git",
|
|
11
|
-
"git@github.com:obra/superpowers.git",
|
|
12
|
-
]);
|
|
13
|
-
export function samePiSource(left, right) {
|
|
14
|
-
validateSource(left);
|
|
15
|
-
validateSource(right);
|
|
16
|
-
return (left === right ||
|
|
17
|
-
(OFFICIAL_SOURCES.has(left) && OFFICIAL_SOURCES.has(right)));
|
|
18
|
-
}
|
|
5
|
+
import { isOfficialSnapshotSource, requireNoSnapshotDependencies, requireSnapshotBootstrap, } from "../../snapshot-package.js";
|
|
19
6
|
export async function assessPiCompatibility(root, selection) {
|
|
20
7
|
try {
|
|
21
8
|
validateSource(selection.effectiveSource);
|
|
22
|
-
const official =
|
|
9
|
+
const official = isOfficialSnapshotSource(selection.effectiveSource);
|
|
23
10
|
const pkg = await readArtifactObject(root, join(root, "package.json"));
|
|
24
11
|
if (pkg.name !== "superpowers" ||
|
|
25
12
|
pkg.type !== "module" ||
|
|
@@ -34,28 +21,9 @@ export async function assessPiCompatibility(root, selection) {
|
|
|
34
21
|
JSON.stringify(pi.extensions) !== '["./.pi/extensions/superpowers.ts"]' ||
|
|
35
22
|
JSON.stringify(pi.skills) !== '["./skills"]')
|
|
36
23
|
throw new Error("resource declaration");
|
|
37
|
-
|
|
38
|
-
"dependencies",
|
|
39
|
-
"optionalDependencies",
|
|
40
|
-
"peerDependencies",
|
|
41
|
-
"bundleDependencies",
|
|
42
|
-
"bundledDependencies",
|
|
43
|
-
]) {
|
|
44
|
-
const value = pkg[key];
|
|
45
|
-
if (value !== undefined &&
|
|
46
|
-
(value === null ||
|
|
47
|
-
typeof value !== "object" ||
|
|
48
|
-
Object.keys(value).length !== 0))
|
|
49
|
-
throw new Error("runtime dependencies");
|
|
50
|
-
}
|
|
24
|
+
requireNoSnapshotDependencies(pkg);
|
|
51
25
|
await validateNativeSkill(root);
|
|
52
|
-
|
|
53
|
-
const bytes = await readArtifactFile(root, bootstrap, 4283);
|
|
54
|
-
if ((await lstat(bootstrap)).mode & 0o111 ||
|
|
55
|
-
bytes.length !== 4283 ||
|
|
56
|
-
createHash("sha256").update(bytes).digest("hex") !==
|
|
57
|
-
"39c27000c047f8a1399a76e032e4cd239d7bdb0be168a121f46a2ac719deaae0")
|
|
58
|
-
throw new Error("bootstrap implementation");
|
|
26
|
+
await requireSnapshotBootstrap(root, ".pi/extensions/superpowers.ts", 4283, "39c27000c047f8a1399a76e032e4cd239d7bdb0be168a121f46a2ac719deaae0");
|
|
59
27
|
return {
|
|
60
28
|
kind: official ? "supported" : "experimental",
|
|
61
29
|
generation: "pi-native-bootstrap-v1",
|
|
@@ -3,8 +3,10 @@ import { cp, lstat, mkdir, open, readFile, readdir, rm, rmdir, unlink, } from "n
|
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { failureResult, successResult, } from "../../adapter-result.js";
|
|
5
5
|
import { atomicWriteFile, beginDirectoryPublication, } from "../../atomic.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { digestArtifactTree } from "../../artifact-tree.js";
|
|
7
|
+
import { normalizeSnapshotRuntimeVersion } from "../../harness-command-result.js";
|
|
8
|
+
import { runPi } from "./native.js";
|
|
9
|
+
import { readPiPackageAssessment, readPiReceipt, } from "./package.js";
|
|
8
10
|
import { piPaths } from "./paths.js";
|
|
9
11
|
import { readPiSettings, resolveCanonicalPiLocalSource, } from "./settings.js";
|
|
10
12
|
import { inspectPiControl, inspectPiOwnership, } from "./state.js";
|
|
@@ -89,7 +91,7 @@ async function snapshot(root) {
|
|
|
89
91
|
if ((await assertNoFollowType(root, ["directory", "missing"])) === "missing")
|
|
90
92
|
return null;
|
|
91
93
|
const receipt = await readPiReceipt(root);
|
|
92
|
-
if (receipt.digest !== (await
|
|
94
|
+
if (receipt.digest !== (await digestArtifactTree(root)))
|
|
93
95
|
throw new Error("Pi snapshot changed");
|
|
94
96
|
return receipt;
|
|
95
97
|
}
|
|
@@ -347,7 +349,7 @@ export async function installPi(artifact, ctx, deps = DEFAULTS) {
|
|
|
347
349
|
const priorRegistration = await registration(paths, deps);
|
|
348
350
|
if (previous === null && priorRegistration !== null)
|
|
349
351
|
throw new Error("unowned Pi registration");
|
|
350
|
-
accepted(
|
|
352
|
+
accepted(normalizeSnapshotRuntimeVersion("Pi", await deps.run(["--version"], paths, ctx)));
|
|
351
353
|
// Recheck after the native preflight, before claiming mutation ownership.
|
|
352
354
|
await requireSnapshot(paths.installedRoot, previous);
|
|
353
355
|
if (!sameRegistration(await registration(paths, deps), priorRegistration))
|
|
@@ -449,7 +451,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
|
|
|
449
451
|
return successResult("remove-pi", null, []);
|
|
450
452
|
}
|
|
451
453
|
if (registered !== null)
|
|
452
|
-
accepted(
|
|
454
|
+
accepted(normalizeSnapshotRuntimeVersion("Pi", await deps.run(["--version"], paths, ctx)));
|
|
453
455
|
await requireSnapshot(paths.installedRoot, previous);
|
|
454
456
|
if (!sameRegistration(await registration(paths, deps), registered))
|
|
455
457
|
throw new Error("Pi registration changed");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tmpdir } from "node:os";
|
|
2
2
|
import { isAbsolute, resolve, sep } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { failureResult, } from "../../adapter-result.js";
|
|
4
|
+
import { nativeCommandResult, } from "../../harness-command-result.js";
|
|
5
5
|
import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "../../validator.js";
|
|
6
6
|
import { withWorkspace } from "../../workspace.js";
|
|
7
7
|
function selectedExecutable(env, invocationCwd) {
|
|
@@ -10,12 +10,6 @@ function selectedExecutable(env, invocationCwd) {
|
|
|
10
10
|
? resolve(invocationCwd, configured)
|
|
11
11
|
: configured;
|
|
12
12
|
}
|
|
13
|
-
function appendCaptured(log, channel, captured) {
|
|
14
|
-
log.appendBytes(channel, Buffer.from(captured.text));
|
|
15
|
-
if (captured.droppedBytes > 0) {
|
|
16
|
-
log.appendText(channel, `Pi ${channel} omitted ${captured.droppedBytes} bytes at the capture limit`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
13
|
export async function runPi(args, paths, ctx, execute = runBoundedCommand) {
|
|
20
14
|
const operation = "pi-command";
|
|
21
15
|
const env = ctx.env ?? {};
|
|
@@ -32,32 +26,7 @@ export async function runPi(args, paths, ctx, execute = runBoundedCommand) {
|
|
|
32
26
|
PI_OFFLINE: "1",
|
|
33
27
|
PI_SKIP_VERSION_CHECK: "1",
|
|
34
28
|
}, workspace, workspace);
|
|
35
|
-
|
|
36
|
-
const errno = /^[A-Z][A-Z0-9_]*$/u.test(result.errno)
|
|
37
|
-
? result.errno
|
|
38
|
-
: "UNKNOWN";
|
|
39
|
-
return failureResult(operation, "launch-failed", `cannot launch configured Pi executable (${errno})`, [], []);
|
|
40
|
-
}
|
|
41
|
-
const log = new AdapterMessageLog();
|
|
42
|
-
if (result.kind === "timedOut") {
|
|
43
|
-
appendCaptured(log, "stdout", result.stdout);
|
|
44
|
-
appendCaptured(log, "stderr", result.stderr);
|
|
45
|
-
return failureResult(operation, "timeout", `Pi command timed out after ${result.afterMs} ms`, [], log.snapshot());
|
|
46
|
-
}
|
|
47
|
-
if (result.code !== 0) {
|
|
48
|
-
appendCaptured(log, "stdout", result.stdout);
|
|
49
|
-
appendCaptured(log, "stderr", result.stderr);
|
|
50
|
-
return failureResult(operation, "nonzero-exit", result.code === null
|
|
51
|
-
? "Pi command exited without a status"
|
|
52
|
-
: `Pi command exited with status ${result.code}`, [], log.snapshot());
|
|
53
|
-
}
|
|
54
|
-
if (result.stdout.droppedBytes > 0 || result.stderr.droppedBytes > 0) {
|
|
55
|
-
appendCaptured(log, "stdout", result.stdout);
|
|
56
|
-
appendCaptured(log, "stderr", result.stderr);
|
|
57
|
-
return failureResult(operation, "output-limit", "Pi command output exceeded the capture limit", [], log.snapshot());
|
|
58
|
-
}
|
|
59
|
-
appendCaptured(log, "stderr", result.stderr);
|
|
60
|
-
return successResult(operation, { stdout: result.stdout.text }, log.snapshot());
|
|
29
|
+
return nativeCommandResult("Pi", result);
|
|
61
30
|
});
|
|
62
31
|
}
|
|
63
32
|
catch {
|
|
@@ -66,16 +35,3 @@ export async function runPi(args, paths, ctx, execute = runBoundedCommand) {
|
|
|
66
35
|
: "cannot create an isolated Pi command workspace", [], []);
|
|
67
36
|
}
|
|
68
37
|
}
|
|
69
|
-
export function normalizePiRuntimeVersion(result) {
|
|
70
|
-
if (!result.outcome.ok) {
|
|
71
|
-
return { status: result.status, outcome: result.outcome };
|
|
72
|
-
}
|
|
73
|
-
if (result.status !== 0) {
|
|
74
|
-
return failureResult("pi-runtime", "invalid-status", "Pi runtime inspection returned a nonzero status", [], result.outcome.messages);
|
|
75
|
-
}
|
|
76
|
-
const version = result.outcome.result.stdout.trim();
|
|
77
|
-
if (!SEMVER_RE.test(version)) {
|
|
78
|
-
return failureResult("pi-runtime", "invalid-version", "Pi runtime inspection returned an invalid version response", [], result.outcome.messages);
|
|
79
|
-
}
|
|
80
|
-
return successResult("pi-runtime", version, result.outcome.messages);
|
|
81
|
-
}
|