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
|
@@ -1,112 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, addArtifactHashField, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
|
|
7
3
|
import { COMMIT_RE } from "../../domain/refs.js";
|
|
8
|
-
import { assertNoFollowType, assertSymlinkTargetContained, } from "../../safe-path.js";
|
|
9
4
|
import { SafetyError } from "../../safety-error.js";
|
|
10
5
|
import { validateSource } from "../../selection.js";
|
|
11
6
|
import { assessPiCompatibility } from "./compatibility.js";
|
|
12
|
-
|
|
13
|
-
const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
14
|
-
// A consistency checksum, not authentication. A coherent manual rewrite of
|
|
15
|
-
// both receipt and bytes remains possible; installed comparison must still use
|
|
16
|
-
// an independently validated candidate.
|
|
17
|
-
export function piReceiptBinding(receipt) {
|
|
18
|
-
const hash = createHash("sha256");
|
|
19
|
-
for (const field of [
|
|
20
|
-
String(receipt.schema),
|
|
21
|
-
receipt.manager,
|
|
22
|
-
receipt.harness,
|
|
23
|
-
receipt.source,
|
|
24
|
-
receipt.commit,
|
|
25
|
-
receipt.digest,
|
|
26
|
-
])
|
|
27
|
-
addArtifactHashField(hash, Buffer.from(field));
|
|
28
|
-
return hash.digest("hex");
|
|
29
|
-
}
|
|
30
|
-
async function gitBytes(repository, args) {
|
|
31
|
-
try {
|
|
32
|
-
return (await exec("git", ["--no-replace-objects", "-C", repository, ...args], {
|
|
33
|
-
encoding: null,
|
|
34
|
-
maxBuffer: 256 * 1024 * 1024,
|
|
35
|
-
})).stdout;
|
|
36
|
-
}
|
|
37
|
-
catch (cause) {
|
|
38
|
-
throw new SafetyError("pi-package", `cannot read Pi tree objects from repository: ${repository}`, { cause });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
export async function materializePiTree(repository, commit, destination) {
|
|
42
|
-
try {
|
|
43
|
-
if (!COMMIT_RE.test(commit))
|
|
44
|
-
throw new Error("commit");
|
|
45
|
-
if (!(await gitBytes(repository, ["cat-file", "-t", commit])).equals(Buffer.from("commit\n")))
|
|
46
|
-
throw new Error("not a commit");
|
|
47
|
-
await assertNoFollowType(destination, ["missing"]);
|
|
48
|
-
const bytes = await gitBytes(repository, [
|
|
49
|
-
"ls-tree",
|
|
50
|
-
"-rz",
|
|
51
|
-
"--full-tree",
|
|
52
|
-
commit,
|
|
53
|
-
]);
|
|
54
|
-
const entries = [];
|
|
55
|
-
let offset = 0;
|
|
56
|
-
while (offset < bytes.length) {
|
|
57
|
-
const end = bytes.indexOf(0, offset);
|
|
58
|
-
if (end < 0)
|
|
59
|
-
throw new Error("tree framing");
|
|
60
|
-
const record = DECODER.decode(bytes.subarray(offset, end));
|
|
61
|
-
offset = end + 1;
|
|
62
|
-
const match = /^(100644|100755|120000) blob ([a-f0-9]{40})\t([\s\S]+)$/.exec(record);
|
|
63
|
-
if (!match)
|
|
64
|
-
throw new Error("tree entry");
|
|
65
|
-
const path = match[3];
|
|
66
|
-
if (path
|
|
67
|
-
.split("/")
|
|
68
|
-
.some((part) => !part ||
|
|
69
|
-
part === "." ||
|
|
70
|
-
part === ".." ||
|
|
71
|
-
part.toLowerCase() === ".git") ||
|
|
72
|
-
isAbsolute(path) ||
|
|
73
|
-
path.includes("\\") ||
|
|
74
|
-
path.split("/")[0] === ARTIFACT_RECEIPT)
|
|
75
|
-
throw new Error("tree path");
|
|
76
|
-
entries.push({ mode: match[1], oid: match[2], path });
|
|
77
|
-
}
|
|
78
|
-
await mkdir(destination, { recursive: true });
|
|
79
|
-
for (const entry of entries) {
|
|
80
|
-
await mkdir(dirname(join(destination, entry.path)), { recursive: true });
|
|
81
|
-
}
|
|
82
|
-
for (const entry of entries.filter((entry) => entry.mode !== "120000")) {
|
|
83
|
-
const path = join(destination, entry.path);
|
|
84
|
-
await writeFile(path, await gitBytes(repository, ["cat-file", "blob", entry.oid]), { flag: "wx", mode: entry.mode === "100755" ? 0o755 : 0o644 });
|
|
85
|
-
await chmod(path, entry.mode === "100755" ? 0o755 : 0o644);
|
|
86
|
-
}
|
|
87
|
-
for (const entry of entries.filter((entry) => entry.mode === "120000")) {
|
|
88
|
-
const target = DECODER.decode(await gitBytes(repository, ["cat-file", "blob", entry.oid]));
|
|
89
|
-
const path = join(destination, entry.path);
|
|
90
|
-
const suffix = relative(resolve(destination), resolve(dirname(path), target));
|
|
91
|
-
if (!target ||
|
|
92
|
-
target.includes("\0") ||
|
|
93
|
-
isAbsolute(target) ||
|
|
94
|
-
suffix === ".." ||
|
|
95
|
-
suffix.startsWith("../") ||
|
|
96
|
-
isAbsolute(suffix))
|
|
97
|
-
throw new Error("link escape");
|
|
98
|
-
await symlink(target, path);
|
|
99
|
-
}
|
|
100
|
-
for (const entry of entries.filter((entry) => entry.mode === "120000"))
|
|
101
|
-
await assertSymlinkTargetContained(destination, join(destination, entry.path));
|
|
102
|
-
}
|
|
103
|
-
catch (cause) {
|
|
104
|
-
throw new SafetyError("pi-package", `cannot materialize Pi tree from ${repository} into ${destination}`, { cause });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
export async function digestPiTree(root) {
|
|
108
|
-
return await digestArtifactTree(root);
|
|
109
|
-
}
|
|
7
|
+
import { snapshotReceiptBinding } from "../../snapshot-package.js";
|
|
110
8
|
export async function readPiReceipt(root) {
|
|
111
9
|
const path = join(root, ARTIFACT_RECEIPT);
|
|
112
10
|
try {
|
|
@@ -123,7 +21,7 @@ export async function readPiReceipt(root) {
|
|
|
123
21
|
!ARTIFACT_DIGEST_RE.test(value.binding))
|
|
124
22
|
throw new Error("receipt fields");
|
|
125
23
|
validateSource(value.source);
|
|
126
|
-
if (
|
|
24
|
+
if (snapshotReceiptBinding(value) !== value.binding)
|
|
127
25
|
throw new Error("receipt binding");
|
|
128
26
|
const compatibility = value.compatibility;
|
|
129
27
|
if (!compatibility ||
|
|
@@ -144,7 +42,7 @@ export async function readPiReceipt(root) {
|
|
|
144
42
|
}
|
|
145
43
|
export async function readPiPackageAssessment(root) {
|
|
146
44
|
const receipt = await readPiReceipt(root);
|
|
147
|
-
if ((await
|
|
45
|
+
if ((await digestArtifactTree(root)) !== receipt.digest)
|
|
148
46
|
throw new SafetyError("pi-package", `Pi artifact digest mismatch: ${root}`);
|
|
149
47
|
const compatibility = await assessPiCompatibility(root, {
|
|
150
48
|
effectiveSource: receipt.source,
|
|
@@ -1,111 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { failureResult, successResult, } from "../../adapter-result.js";
|
|
4
|
-
import { ARTIFACT_RECEIPT } from "../../artifact-tree.js";
|
|
1
|
+
import { createSnapshotPreparation } from "../../snapshot-prepare.js";
|
|
5
2
|
import { piPaths } from "./paths.js";
|
|
6
|
-
import { assessPiCompatibility
|
|
7
|
-
import {
|
|
8
|
-
import { classifyPathNoFollow } from "../../safe-path.js";
|
|
3
|
+
import { assessPiCompatibility } from "./compatibility.js";
|
|
4
|
+
import { readPiPackageAssessment } from "./package.js";
|
|
9
5
|
export function piPreparationLocation(ctx) {
|
|
10
6
|
return {
|
|
11
7
|
destinationRoot: piPaths(ctx.env ?? {}, process.cwd()).preparedRoot,
|
|
12
8
|
stagingLeaf: "superpowers",
|
|
13
9
|
};
|
|
14
10
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
manager: "superpowers-manager",
|
|
26
|
-
harness: "pi",
|
|
27
|
-
source: input.selection.effectiveSource,
|
|
28
|
-
commit: input.selection.desiredCommit,
|
|
29
|
-
digest,
|
|
30
|
-
};
|
|
31
|
-
const receipt = {
|
|
32
|
-
...identity,
|
|
33
|
-
binding: piReceiptBinding(identity),
|
|
34
|
-
compatibility,
|
|
35
|
-
};
|
|
36
|
-
await writeFile(join(input.candidateRoot, ARTIFACT_RECEIPT), JSON.stringify(receipt) + "\n", { flag: "wx" });
|
|
37
|
-
return successResult("prepare", {
|
|
38
|
-
root: input.candidateRoot,
|
|
39
|
-
commit: receipt.commit,
|
|
40
|
-
identity: digest,
|
|
41
|
-
compatibility,
|
|
42
|
-
}, []);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return failureResult("prepare", "invalid-package", `cannot prepare Pi artifact: ${input.candidateRoot}`, [], []);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
async function readArtifactAssessment(root) {
|
|
49
|
-
const { receipt, compatibility } = await readPiPackageAssessment(root);
|
|
50
|
-
return {
|
|
51
|
-
receipt,
|
|
52
|
-
artifact: {
|
|
53
|
-
root,
|
|
54
|
-
commit: receipt.commit,
|
|
55
|
-
identity: receipt.digest,
|
|
56
|
-
compatibility,
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
async function readArtifact(root) {
|
|
61
|
-
const { artifact } = await readArtifactAssessment(root);
|
|
62
|
-
const { compatibility } = artifact;
|
|
63
|
-
if (compatibility.kind !== "supported" &&
|
|
64
|
-
compatibility.kind !== "experimental")
|
|
65
|
-
throw new Error("unsupported profile");
|
|
66
|
-
return artifact;
|
|
67
|
-
}
|
|
68
|
-
export async function inspectPiPrepared(selection, ctx) {
|
|
69
|
-
const root = piPreparationLocation(ctx).destinationRoot;
|
|
70
|
-
const unknown = {
|
|
71
|
-
kind: "unknown",
|
|
72
|
-
reason: "Pi prepared compatibility evidence is missing",
|
|
73
|
-
};
|
|
74
|
-
try {
|
|
75
|
-
if ((await classifyPathNoFollow(join(root, ARTIFACT_RECEIPT))) === "missing")
|
|
76
|
-
return successResult("inspect-prepared", { kind: "needs-prepare", observedIdentity: "", compatibility: unknown }, []);
|
|
77
|
-
const { artifact, receipt } = await readArtifactAssessment(root);
|
|
78
|
-
if (artifact.commit !== selection.desiredCommit ||
|
|
79
|
-
!samePiSource(receipt.source, selection.effectiveSource))
|
|
80
|
-
return successResult("inspect-prepared", {
|
|
81
|
-
kind: "needs-prepare",
|
|
82
|
-
observedIdentity: artifact.identity,
|
|
83
|
-
compatibility: unknown,
|
|
84
|
-
}, []);
|
|
85
|
-
if (artifact.compatibility.kind !== "supported" &&
|
|
86
|
-
artifact.compatibility.kind !== "experimental")
|
|
87
|
-
return successResult("inspect-prepared", {
|
|
88
|
-
kind: "needs-prepare",
|
|
89
|
-
observedIdentity: artifact.identity,
|
|
90
|
-
compatibility: artifact.compatibility,
|
|
91
|
-
}, []);
|
|
92
|
-
return successResult("inspect-prepared", {
|
|
93
|
-
kind: "current",
|
|
94
|
-
artifact,
|
|
95
|
-
observedIdentity: artifact.identity,
|
|
96
|
-
compatibility: artifact.compatibility,
|
|
97
|
-
}, []);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return failureResult("inspect-prepared", "invalid-package", `cannot inspect Pi prepared artifact: ${root}`, [], []);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
export async function readPiPrepared(ctx) {
|
|
104
|
-
const root = piPreparationLocation(ctx).destinationRoot;
|
|
105
|
-
try {
|
|
106
|
-
return successResult("read-prepared", await readArtifact(root), []);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return failureResult("read-prepared", "invalid-package", `cannot read Pi prepared artifact: ${root}`, [], []);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
11
|
+
const preparation = createSnapshotPreparation({
|
|
12
|
+
harness: "pi",
|
|
13
|
+
label: "Pi",
|
|
14
|
+
preparationLocation: piPreparationLocation,
|
|
15
|
+
assessCompatibility: assessPiCompatibility,
|
|
16
|
+
readAssessment: readPiPackageAssessment,
|
|
17
|
+
});
|
|
18
|
+
export const preparePiCandidate = preparation.prepareCandidate;
|
|
19
|
+
export const inspectPiPrepared = preparation.inspectPrepared;
|
|
20
|
+
export const readPiPrepared = preparation.readPrepared;
|
|
@@ -1,67 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
porcelain: fields.map(([key, value]) => `${key}=${value}\n`).join(""),
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
renderInstallVerification(_desiredCommit, receipt, inspection) {
|
|
27
|
-
if (receipt.status !== 0 || !receipt.outcome.ok)
|
|
28
|
-
return {
|
|
29
|
-
stdout: [],
|
|
30
|
-
stderr: [
|
|
31
|
-
"error: Pi activation did not return a verified installation receipt",
|
|
32
|
-
],
|
|
33
|
-
};
|
|
34
|
-
if (inspection.status !== 0 || !inspection.outcome.ok)
|
|
35
|
-
return receipt.outcome.result.missingVerificationOutput;
|
|
36
|
-
if (inspection.outcome.result.kind === "current")
|
|
37
|
-
return {
|
|
38
|
-
stdout: [
|
|
39
|
-
"Installed the frozen Superpowers Pi snapshot. Restart Pi to load it.",
|
|
40
|
-
],
|
|
41
|
-
stderr: [],
|
|
42
|
-
};
|
|
43
|
-
return inspection.outcome.result.kind === "absent"
|
|
44
|
-
? receipt.outcome.result.missingVerificationOutput
|
|
45
|
-
: receipt.outcome.result.mismatchVerificationOutput;
|
|
46
|
-
},
|
|
47
|
-
renderRemovalCompletion(_ownership, removalInput) {
|
|
48
|
-
return removalInput.receiptDigest === null &&
|
|
49
|
-
removalInput.registrationIdentity === null
|
|
50
|
-
? {
|
|
51
|
-
stdout: ["No managed Superpowers Pi installation is present."],
|
|
52
|
-
stderr: [],
|
|
53
|
-
}
|
|
54
|
-
: {
|
|
55
|
-
stdout: [
|
|
56
|
-
"Removed the managed Superpowers Pi installation. Restart Pi to load the resulting state.",
|
|
57
|
-
],
|
|
58
|
-
stderr: [],
|
|
59
|
-
};
|
|
60
|
-
},
|
|
61
|
-
callFailure(site) {
|
|
62
|
-
return {
|
|
63
|
-
unexpected: `Pi ${site} did not complete`,
|
|
64
|
-
invalidStatus: `Pi ${site} returned an invalid status`,
|
|
65
|
-
};
|
|
66
|
-
},
|
|
1
|
+
import { createSnapshotPresentation } from "../../snapshot-presentation.js";
|
|
2
|
+
const renderProbe = (facts) => {
|
|
3
|
+
const fields = [
|
|
4
|
+
["harness", "pi"],
|
|
5
|
+
["desired_commit", facts.selection.desiredCommit],
|
|
6
|
+
["prepared_identity", facts.prepared.observedIdentity],
|
|
7
|
+
["installed_identity", facts.installed.observedIdentity],
|
|
8
|
+
["installation_state", facts.installed.kind],
|
|
9
|
+
["resource_state", facts.resourceState ?? "idle"],
|
|
10
|
+
["ownership", facts.ownership.presentationValue],
|
|
11
|
+
["conflicts", (facts.ownership.presentationConflicts ?? []).join("; ")],
|
|
12
|
+
["update_control", facts.control.presentationValue],
|
|
13
|
+
["compatibility", facts.compatibility.kind],
|
|
14
|
+
["compatibility_reason", facts.compatibility.reason],
|
|
15
|
+
["status", facts.status],
|
|
16
|
+
];
|
|
17
|
+
return {
|
|
18
|
+
human: fields
|
|
19
|
+
.map(([key, value]) => `${key.replaceAll("_", " ")}: ${value || "not present"}\n`)
|
|
20
|
+
.join(""),
|
|
21
|
+
porcelain: fields.map(([key, value]) => `${key}=${value}\n`).join(""),
|
|
22
|
+
};
|
|
67
23
|
};
|
|
24
|
+
export const piPresentation = createSnapshotPresentation("Pi", renderProbe, (input) => input.receiptDigest === null && input.registrationIdentity === null);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, readlink, realpath } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { classifyPathNoFollow } from "../../safe-path.js";
|
|
4
|
+
import { classifyPathNoFollow, isContained } from "../../safe-path.js";
|
|
5
5
|
const PI_IGNORE_FILES = [".gitignore", ".ignore", ".fdignore"];
|
|
6
6
|
function toPosixPath(path) {
|
|
7
7
|
return path.split(sep).join("/");
|
|
@@ -164,6 +164,20 @@ function normalizeExactSkillPattern(pattern) {
|
|
|
164
164
|
: pattern;
|
|
165
165
|
return toPosixPath(normalized);
|
|
166
166
|
}
|
|
167
|
+
function forcedSkillActivity(path, identityRoot, controls) {
|
|
168
|
+
const identities = exactSkillIdentities(path, identityRoot);
|
|
169
|
+
for (const [prefix, activity] of [
|
|
170
|
+
["-", "disabled"],
|
|
171
|
+
["+", "active"],
|
|
172
|
+
]) {
|
|
173
|
+
const patterns = controls
|
|
174
|
+
.filter((control) => control.startsWith(prefix))
|
|
175
|
+
.map((control) => normalizeExactSkillPattern(control.slice(1)));
|
|
176
|
+
if (patterns.some((pattern) => identities.includes(pattern)))
|
|
177
|
+
return activity;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
167
181
|
function hasUnknownMinimatchSyntax(pattern) {
|
|
168
182
|
return (pattern.startsWith("#") ||
|
|
169
183
|
pattern.includes("!") ||
|
|
@@ -171,19 +185,9 @@ function hasUnknownMinimatchSyntax(pattern) {
|
|
|
171
185
|
toPosixPath(pattern).includes("//"));
|
|
172
186
|
}
|
|
173
187
|
function sharedSkillActivity(path, agentsRoot, controls) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
.map((control) => normalizeExactSkillPattern(control.slice(1)));
|
|
178
|
-
if (forceExcludes.some((pattern) => exactIdentities.includes(pattern))) {
|
|
179
|
-
return "disabled";
|
|
180
|
-
}
|
|
181
|
-
const forceIncludes = controls
|
|
182
|
-
.filter((control) => control.startsWith("+"))
|
|
183
|
-
.map((control) => normalizeExactSkillPattern(control.slice(1)));
|
|
184
|
-
if (forceIncludes.some((pattern) => exactIdentities.includes(pattern))) {
|
|
185
|
-
return "active";
|
|
186
|
-
}
|
|
188
|
+
const forced = forcedSkillActivity(path, agentsRoot, controls);
|
|
189
|
+
if (forced !== null)
|
|
190
|
+
return forced;
|
|
187
191
|
const excludePatterns = controls
|
|
188
192
|
.filter((control) => control.startsWith("!"))
|
|
189
193
|
.map((control) => control.slice(1));
|
|
@@ -233,11 +237,6 @@ function resolveTopLevelSkillPath(source, paths) {
|
|
|
233
237
|
? resolve(normalized)
|
|
234
238
|
: resolve(paths.agentDir, normalized);
|
|
235
239
|
}
|
|
236
|
-
function containedBy(root, candidate) {
|
|
237
|
-
const suffix = relative(root, candidate);
|
|
238
|
-
return (suffix === "" ||
|
|
239
|
-
(!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
|
|
240
|
-
}
|
|
241
240
|
async function hasSafeSharedSkillAncestors(path, sharedRoot) {
|
|
242
241
|
const suffix = relative(sharedRoot, dirname(path));
|
|
243
242
|
const directories = [sharedRoot];
|
|
@@ -260,19 +259,9 @@ async function hasSafeSharedSkillAncestors(path, sharedRoot) {
|
|
|
260
259
|
return true;
|
|
261
260
|
}
|
|
262
261
|
function explicitSharedSkillActivity(path, paths, controls) {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
.map((control) => normalizeExactSkillPattern(control.slice(1)));
|
|
267
|
-
if (forceExcludes.some((pattern) => exactIdentities.includes(pattern))) {
|
|
268
|
-
return "disabled";
|
|
269
|
-
}
|
|
270
|
-
const forceIncludes = controls
|
|
271
|
-
.filter((control) => control.startsWith("+"))
|
|
272
|
-
.map((control) => normalizeExactSkillPattern(control.slice(1)));
|
|
273
|
-
if (forceIncludes.some((pattern) => exactIdentities.includes(pattern))) {
|
|
274
|
-
return "active";
|
|
275
|
-
}
|
|
262
|
+
const forced = forcedSkillActivity(path, paths.agentDir, controls);
|
|
263
|
+
if (forced !== null)
|
|
264
|
+
return forced;
|
|
276
265
|
const excludePatterns = controls
|
|
277
266
|
.filter((control) => control.startsWith("!"))
|
|
278
267
|
.map((control) => control.slice(1));
|
|
@@ -321,7 +310,7 @@ async function explicitSharedSkillActivities(settings, paths) {
|
|
|
321
310
|
lexicalKind = await classifyPathNoFollow(path);
|
|
322
311
|
}
|
|
323
312
|
catch {
|
|
324
|
-
if (
|
|
313
|
+
if (isContained(sharedRoot, path) || isContained(path, sharedRoot)) {
|
|
325
314
|
indeterminate = true;
|
|
326
315
|
}
|
|
327
316
|
continue;
|
|
@@ -333,12 +322,12 @@ async function explicitSharedSkillActivities(settings, paths) {
|
|
|
333
322
|
canonicalPath = await realpath(path);
|
|
334
323
|
}
|
|
335
324
|
catch {
|
|
336
|
-
let related =
|
|
325
|
+
let related = isContained(sharedRoot, path) || isContained(path, sharedRoot);
|
|
337
326
|
if (!related && lexicalKind === "symlink") {
|
|
338
327
|
try {
|
|
339
328
|
const target = resolve(dirname(path), await readlink(path));
|
|
340
329
|
related =
|
|
341
|
-
|
|
330
|
+
isContained(sharedRoot, target) || isContained(target, sharedRoot);
|
|
342
331
|
}
|
|
343
332
|
catch {
|
|
344
333
|
// Without a relationship to the known route, preserve unrelated state.
|
|
@@ -348,10 +337,10 @@ async function explicitSharedSkillActivities(settings, paths) {
|
|
|
348
337
|
indeterminate = true;
|
|
349
338
|
continue;
|
|
350
339
|
}
|
|
351
|
-
const related =
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
340
|
+
const related = isContained(sharedRoot, path) ||
|
|
341
|
+
isContained(path, sharedRoot) ||
|
|
342
|
+
isContained(canonicalSharedRoot, canonicalPath) ||
|
|
343
|
+
isContained(canonicalPath, canonicalSharedRoot);
|
|
355
344
|
if (!related)
|
|
356
345
|
continue;
|
|
357
346
|
let canonicalKind;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { sharedPiSkillsActivity } from "./shared-skills.js";
|
|
3
|
-
import {
|
|
3
|
+
import { inspectionFailure, successResult, } from "../../adapter-result.js";
|
|
4
4
|
import { readArtifactObject } from "../../artifact-tree.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { observeSnapshot, sameSnapshotSource, } from "../../snapshot-package.js";
|
|
6
|
+
import { readPiPackageAssessment, readPiReceipt, } from "./package.js";
|
|
7
7
|
import { piPaths } from "./paths.js";
|
|
8
8
|
import { readPiSettings, resolveCanonicalPiLocalSource, resolvePiLocalSource, } from "./settings.js";
|
|
9
9
|
import { classifyPathNoFollow } from "../../safe-path.js";
|
|
@@ -111,23 +111,6 @@ async function observeSettings(paths) {
|
|
|
111
111
|
}
|
|
112
112
|
return { settings, registration };
|
|
113
113
|
}
|
|
114
|
-
async function observeSnapshot(paths) {
|
|
115
|
-
const kind = await classifyPathNoFollow(paths.installedRoot);
|
|
116
|
-
if (kind === "missing")
|
|
117
|
-
return { kind: "absent" };
|
|
118
|
-
if (kind !== "directory")
|
|
119
|
-
return { kind: "unverified" };
|
|
120
|
-
try {
|
|
121
|
-
const receipt = await readPiReceipt(paths.installedRoot);
|
|
122
|
-
const digest = await digestPiTree(paths.installedRoot);
|
|
123
|
-
return receipt.digest === digest
|
|
124
|
-
? { kind: "owned", receipt, digest }
|
|
125
|
-
: { kind: "unverified" };
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return { kind: "unverified" };
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
114
|
async function isNamedLocalSuperpowers(entry, paths, canonicalInstalledRoot) {
|
|
132
115
|
const root = resolvePiLocalSource(entry.source, paths.agentDir, paths.homeDir);
|
|
133
116
|
if (root === null ||
|
|
@@ -176,16 +159,13 @@ async function unmanagedConflicts(settings, paths) {
|
|
|
176
159
|
}
|
|
177
160
|
return [...conflicts];
|
|
178
161
|
}
|
|
179
|
-
function inspectionFailure(operation, subject) {
|
|
180
|
-
return failureResult(operation, "invalid-state", `cannot inspect ${subject}`, [], []);
|
|
181
|
-
}
|
|
182
162
|
export async function inspectPiOwnership(ctx) {
|
|
183
163
|
const operation = "inspect-pi-ownership";
|
|
184
164
|
const paths = pathsFor(ctx);
|
|
185
165
|
try {
|
|
186
166
|
const [{ settings, registration }, snapshot] = await Promise.all([
|
|
187
167
|
observeSettings(paths),
|
|
188
|
-
observeSnapshot(paths),
|
|
168
|
+
observeSnapshot(paths.installedRoot, readPiReceipt),
|
|
189
169
|
]);
|
|
190
170
|
if (snapshot.kind === "unverified") {
|
|
191
171
|
return inspectionFailure(operation, `Pi ownership at ${paths.installedRoot}`);
|
|
@@ -240,7 +220,7 @@ export async function inspectPiInstalled(selection, ctx) {
|
|
|
240
220
|
try {
|
|
241
221
|
[settings, snapshot] = await Promise.all([
|
|
242
222
|
observeSettings(paths),
|
|
243
|
-
observeSnapshot(paths),
|
|
223
|
+
observeSnapshot(paths.installedRoot, readPiReceipt),
|
|
244
224
|
]);
|
|
245
225
|
}
|
|
246
226
|
catch {
|
|
@@ -267,9 +247,9 @@ export async function inspectPiInstalled(selection, ctx) {
|
|
|
267
247
|
if ((intended.compatibility.kind !== "supported" &&
|
|
268
248
|
intended.compatibility.kind !== "experimental") ||
|
|
269
249
|
intended.receipt.commit !== selection.desiredCommit ||
|
|
270
|
-
!
|
|
250
|
+
!sameSnapshotSource(intended.receipt.source, selection.effectiveSource) ||
|
|
271
251
|
snapshot.receipt.commit !== intended.receipt.commit ||
|
|
272
|
-
!
|
|
252
|
+
!sameSnapshotSource(snapshot.receipt.source, intended.receipt.source) ||
|
|
273
253
|
snapshot.digest !== intended.receipt.digest) {
|
|
274
254
|
return mismatch;
|
|
275
255
|
}
|
package/dist/provenance.js
CHANGED
|
@@ -67,8 +67,8 @@ export function generatedMetadataPath(root) {
|
|
|
67
67
|
// Ported from
|
|
68
68
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:33-37::spw_generated_commit_or_empty`.
|
|
69
69
|
// Lenient by design: a missing or malformed generated provenance file yields
|
|
70
|
-
// "", which
|
|
71
|
-
// deny the operator the remediation path.
|
|
70
|
+
// "", which the typed prepared-state flow reports as "needs prepare".
|
|
71
|
+
// Aborting here would deny the operator the remediation path.
|
|
72
72
|
export async function generatedCommitOrEmpty(root) {
|
|
73
73
|
return readGeneratedCommitLenient(generatedMetadataPath(root));
|
|
74
74
|
}
|
package/dist/safe-path.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { lstat, readlink, realpath } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, isAbsolute, relative, resolve, sep, } from "node:path";
|
|
3
3
|
import { SafetyError } from "./safety-error.js";
|
|
4
|
-
function isErrno(value, code) {
|
|
4
|
+
export function isErrno(value, code) {
|
|
5
5
|
return (value instanceof Error &&
|
|
6
6
|
"code" in value &&
|
|
7
7
|
value.code === code);
|
|
8
8
|
}
|
|
9
|
-
function isContained(root, candidate) {
|
|
9
|
+
export function isContained(root, candidate) {
|
|
10
10
|
const suffix = relative(root, candidate);
|
|
11
11
|
return (suffix === "" ||
|
|
12
12
|
(!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
|
package/dist/selection-store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { mkdir, open } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
-
import { atomicWriteFile
|
|
4
|
+
import { atomicWriteFile } from "./atomic.js";
|
|
5
5
|
import { classifyPathNoFollow } from "./safe-path.js";
|
|
6
6
|
import { SafetyError } from "./safety-error.js";
|
|
7
7
|
import { selectionError, serializeRecord, validateRecord, } from "./selection.js";
|
|
@@ -132,19 +132,6 @@ async function ensureStateDirectory(path) {
|
|
|
132
132
|
throw selectionError(`selection state directory is not usable: ${path}`);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
-
async function finalStateDiagnostic(path, finalBytes) {
|
|
136
|
-
try {
|
|
137
|
-
const record = finalBytes === undefined
|
|
138
|
-
? await readSelectionState(path)
|
|
139
|
-
: parseRecordBytes(path, finalBytes);
|
|
140
|
-
return record === null
|
|
141
|
-
? "selection state is now absent"
|
|
142
|
-
: `selection state is now ${record.mode}`;
|
|
143
|
-
}
|
|
144
|
-
catch (cause) {
|
|
145
|
-
return `final selection state cannot be validated: ${errorText(cause)}`;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
135
|
export async function writeSelectionState(path, proposed, options = {}) {
|
|
149
136
|
await ensureStateDirectory(dirname(path));
|
|
150
137
|
// Invalid existing state must block overwrite.
|
|
@@ -160,13 +147,6 @@ export async function writeSelectionState(path, proposed, options = {}) {
|
|
|
160
147
|
});
|
|
161
148
|
}
|
|
162
149
|
catch (cause) {
|
|
163
|
-
|
|
164
|
-
? cause.details
|
|
165
|
-
: undefined;
|
|
166
|
-
if (details?.phase !== "post-replacement") {
|
|
167
|
-
throw selectionError(`cannot write selection state: ${causedErrorText(cause)}`, cause);
|
|
168
|
-
}
|
|
169
|
-
const diagnostic = await finalStateDiagnostic(path, details.finalBytes);
|
|
170
|
-
throw selectionError(`cannot complete selection state write: ${causedErrorText(cause)}; ${diagnostic}`, cause);
|
|
150
|
+
throw selectionError(`cannot write selection state: ${causedErrorText(cause)}`, cause);
|
|
171
151
|
}
|
|
172
152
|
}
|