superpowers-manager 0.4.0 → 0.5.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 +76 -517
- package/dist/artifact-tree.js +111 -0
- package/dist/atomic.js +103 -7
- package/dist/cli-arguments.js +37 -0
- package/dist/cli.js +49 -12
- package/dist/commands/install.js +116 -17
- package/dist/commands/mutation.js +71 -0
- package/dist/commands/prepare.js +23 -17
- package/dist/commands/probe.js +164 -14
- package/dist/commands/uninstall.js +9 -5
- package/dist/commands/update.js +6 -2
- package/dist/domain/refs.js +4 -0
- package/dist/harness-compatibility.js +13 -0
- package/dist/{adapter.js → harnesses/codex/adapter.js} +17 -13
- package/dist/harnesses/codex/compatibility.js +126 -0
- package/dist/harnesses/codex/conflicts.js +99 -0
- package/dist/{generated-plugin.js → harnesses/codex/generated-plugin.js} +7 -73
- package/dist/{codex-harness.js → harnesses/codex/harness.js} +55 -5
- package/dist/{hooks.js → harnesses/codex/hooks.js} +6 -6
- package/dist/{codex-json.js → harnesses/codex/json.js} +9 -2
- package/dist/{lifecycle.js → harnesses/codex/lifecycle.js} +2 -2
- package/dist/{manifest-overlay.js → harnesses/codex/manifest-overlay.js} +5 -5
- package/dist/{codex-prepare.js → harnesses/codex/prepare.js} +72 -16
- package/dist/{codex-presentation.js → harnesses/codex/presentation.js} +27 -4
- package/dist/{codex-state.js → harnesses/codex/state.js} +3 -3
- package/dist/harnesses/pi/compatibility.js +73 -0
- package/dist/harnesses/pi/harness.js +63 -0
- package/dist/harnesses/pi/install.js +495 -0
- package/dist/harnesses/pi/native.js +81 -0
- package/dist/harnesses/pi/package.js +153 -0
- package/dist/harnesses/pi/paths.js +30 -0
- package/dist/harnesses/pi/prepare.js +111 -0
- package/dist/harnesses/pi/presentation.js +67 -0
- package/dist/harnesses/pi/settings.js +171 -0
- package/dist/harnesses/pi/shared-skills.js +413 -0
- package/dist/harnesses/pi/state.js +311 -0
- package/dist/python-text.js +16 -0
- package/dist/resource-lock.js +265 -0
- package/dist/safe-path.js +21 -5
- package/dist/skill-validation.js +52 -0
- package/dist/upstream-workspace.js +8 -0
- package/dist/validate-generated-plugin-cli.js +1 -1
- package/dist/validator.js +2 -1
- package/package.json +12 -8
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { validateNativeSkill } from "../../artifact-tree.js";
|
|
3
|
+
import { classifyPathNoFollow, assertExistingContained, } from "../../safe-path.js";
|
|
4
|
+
import { classifyHooks, readManifest } from "./hooks.js";
|
|
5
|
+
import { SEMVER_RE } from "../../domain/refs.js";
|
|
6
|
+
import { validateSource } from "../../selection.js";
|
|
7
|
+
import { writeFile } from "node:fs/promises";
|
|
8
|
+
import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
|
|
9
|
+
import { COMMIT_RE } from "../../domain/refs.js";
|
|
10
|
+
import { validateGeneratedPlugin } from "./generated-plugin.js";
|
|
11
|
+
import { SafetyError } from "../../safety-error.js";
|
|
12
|
+
export async function writeCodexAssessment(root, selection, manifestSource) {
|
|
13
|
+
const receipt = {
|
|
14
|
+
schema: 1,
|
|
15
|
+
manager: "superpowers-manager",
|
|
16
|
+
harness: "codex",
|
|
17
|
+
source: selection.effectiveSource,
|
|
18
|
+
commit: selection.desiredCommit,
|
|
19
|
+
generation: "codex-native-skills-v1",
|
|
20
|
+
manifestSource,
|
|
21
|
+
digest: await digestArtifactTree(root),
|
|
22
|
+
};
|
|
23
|
+
await writeFile(join(root, ARTIFACT_RECEIPT), JSON.stringify(receipt) + "\n", { flag: "wx" });
|
|
24
|
+
}
|
|
25
|
+
export async function readCodexAssessment(root) {
|
|
26
|
+
try {
|
|
27
|
+
const receipt = await readArtifactObject(root, join(root, ARTIFACT_RECEIPT));
|
|
28
|
+
const provenance = await readArtifactObject(root, join(root, ".superpowers-upstream.json"));
|
|
29
|
+
if (receipt.schema !== 1 ||
|
|
30
|
+
receipt.manager !== "superpowers-manager" ||
|
|
31
|
+
receipt.harness !== "codex" ||
|
|
32
|
+
receipt.generation !== "codex-native-skills-v1" ||
|
|
33
|
+
typeof receipt.source !== "string" ||
|
|
34
|
+
typeof receipt.commit !== "string" ||
|
|
35
|
+
!COMMIT_RE.test(receipt.commit) ||
|
|
36
|
+
typeof receipt.digest !== "string" ||
|
|
37
|
+
!ARTIFACT_DIGEST_RE.test(receipt.digest) ||
|
|
38
|
+
(receipt.manifestSource !== "upstream" &&
|
|
39
|
+
receipt.manifestSource !== "fallback") ||
|
|
40
|
+
receipt.source !== provenance.source ||
|
|
41
|
+
receipt.commit !== provenance.commit)
|
|
42
|
+
throw new Error("assessment fields");
|
|
43
|
+
validateSource(receipt.source);
|
|
44
|
+
if ((await digestArtifactTree(root)) !== receipt.digest)
|
|
45
|
+
throw new Error("assessment digest");
|
|
46
|
+
const manifest = await readManifest(join(root, ".codex-plugin/plugin.json"));
|
|
47
|
+
if (typeof manifest.version !== "string" ||
|
|
48
|
+
typeof provenance.requested_ref !== "string" ||
|
|
49
|
+
typeof provenance.resolved_ref !== "string" ||
|
|
50
|
+
typeof provenance.upstream_manifest_version !== "string")
|
|
51
|
+
throw new Error("provenance fields");
|
|
52
|
+
const errors = await validateGeneratedPlugin({
|
|
53
|
+
pluginRoot: root,
|
|
54
|
+
source: receipt.source,
|
|
55
|
+
commit: receipt.commit,
|
|
56
|
+
requestedRef: provenance.requested_ref,
|
|
57
|
+
resolvedRef: provenance.resolved_ref,
|
|
58
|
+
upstreamManifestVersion: provenance.upstream_manifest_version,
|
|
59
|
+
manifestVersion: manifest.version,
|
|
60
|
+
manifestSource: receipt.manifestSource,
|
|
61
|
+
});
|
|
62
|
+
if (errors.length)
|
|
63
|
+
throw new Error("generated resources");
|
|
64
|
+
const selection = {
|
|
65
|
+
effectiveSource: receipt.source,
|
|
66
|
+
desiredCommit: receipt.commit,
|
|
67
|
+
requestedRef: provenance.requested_ref,
|
|
68
|
+
resolvedRef: provenance.resolved_ref,
|
|
69
|
+
resolutionKind: "raw-commit",
|
|
70
|
+
selectionOrigin: "package-default",
|
|
71
|
+
selectionMode: "default",
|
|
72
|
+
upstreamSourceOrigin: "package-default",
|
|
73
|
+
saved: {
|
|
74
|
+
saved_mode: "none",
|
|
75
|
+
saved_source: "",
|
|
76
|
+
saved_requested_ref: "",
|
|
77
|
+
saved_resolved_ref: "",
|
|
78
|
+
saved_commit: "",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const compatibility = await assessCodexCompatibility(root, selection);
|
|
82
|
+
return {
|
|
83
|
+
root,
|
|
84
|
+
commit: receipt.commit,
|
|
85
|
+
identity: receipt.commit,
|
|
86
|
+
compatibility,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (cause) {
|
|
90
|
+
throw new SafetyError("codex-compatibility", `invalid Codex prepared assessment: ${root}`, { cause });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export async function assessCodexCompatibility(root, selection) {
|
|
94
|
+
try {
|
|
95
|
+
validateSource(selection.effectiveSource);
|
|
96
|
+
await validateNativeSkill(root);
|
|
97
|
+
for (const path of [
|
|
98
|
+
".codex/superpowers-codex",
|
|
99
|
+
".codex/superpowers-bootstrap.md",
|
|
100
|
+
]) {
|
|
101
|
+
if ((await classifyPathNoFollow(join(root, path))) !== "missing")
|
|
102
|
+
throw new Error("legacy bootstrap");
|
|
103
|
+
}
|
|
104
|
+
const path = join(root, ".codex-plugin/plugin.json");
|
|
105
|
+
if ((await classifyPathNoFollow(path)) !== "missing") {
|
|
106
|
+
await assertExistingContained(root, path);
|
|
107
|
+
const manifest = await readManifest(path);
|
|
108
|
+
if (manifest.name !== "superpowers" ||
|
|
109
|
+
typeof manifest.version !== "string" ||
|
|
110
|
+
!SEMVER_RE.test(manifest.version))
|
|
111
|
+
throw new Error("manifest");
|
|
112
|
+
await classifyHooks(manifest, "upstream", root);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
kind: "supported",
|
|
116
|
+
generation: "codex-native-skills-v1",
|
|
117
|
+
reason: "native Codex skill discovery",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return {
|
|
122
|
+
kind: "unsupported",
|
|
123
|
+
reason: `Codex package does not provide compatible native skill discovery: ${root}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { codexInstalledPluginsFromJson, } from "./json.js";
|
|
4
|
+
export const CODEX_MANAGER_PLUGIN_ID = "superpowers@superpowers-manager";
|
|
5
|
+
export const CODEX_LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
|
|
6
|
+
const SUPERPOWERS_PLUGIN_PREFIX = "superpowers@";
|
|
7
|
+
const DISPLAYABLE_PLUGIN_ID = /^superpowers@[A-Za-z0-9][A-Za-z0-9._-]*/;
|
|
8
|
+
const NATIVE_ROUTE = ".agents/skills/superpowers";
|
|
9
|
+
const NATIVE_ROUTE_CONFLICT = `native Codex skills route ~/${NATIVE_ROUTE} has indeterminate activity`;
|
|
10
|
+
function isMissingPath(cause) {
|
|
11
|
+
if (cause === null || typeof cause !== "object" || !("code" in cause)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return cause.code === "ENOENT" || cause.code === "ENOTDIR";
|
|
15
|
+
}
|
|
16
|
+
function isUnmanagedPluginId(pluginId) {
|
|
17
|
+
if (pluginId === CODEX_MANAGER_PLUGIN_ID ||
|
|
18
|
+
pluginId === CODEX_LEGACY_PLUGIN_ID) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return (pluginId.startsWith(SUPERPOWERS_PLUGIN_PREFIX) &&
|
|
22
|
+
pluginId.length > SUPERPOWERS_PLUGIN_PREFIX.length);
|
|
23
|
+
}
|
|
24
|
+
function displayablePluginId(pluginId) {
|
|
25
|
+
return pluginId.length <= 256 &&
|
|
26
|
+
DISPLAYABLE_PLUGIN_ID.exec(pluginId)?.[0] === pluginId
|
|
27
|
+
? pluginId
|
|
28
|
+
: "";
|
|
29
|
+
}
|
|
30
|
+
function unmanagedPluginConflict(plugin) {
|
|
31
|
+
if (!isUnmanagedPluginId(plugin.pluginId))
|
|
32
|
+
return "";
|
|
33
|
+
const displayId = displayablePluginId(plugin.pluginId);
|
|
34
|
+
const subject = displayId.length > 0
|
|
35
|
+
? `Codex plugin ${displayId}`
|
|
36
|
+
: "Codex plugin with a non-displayable Superpowers identity";
|
|
37
|
+
if (plugin.installed === true && plugin.enabled === true) {
|
|
38
|
+
return `active ${subject}`;
|
|
39
|
+
}
|
|
40
|
+
if ((plugin.installed === true && plugin.enabled === false) ||
|
|
41
|
+
(plugin.installed === false && plugin.enabled === false)) {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
return `${subject} has indeterminate activity`;
|
|
45
|
+
}
|
|
46
|
+
async function nativeRouteConflict(ctx) {
|
|
47
|
+
const home = ctx.env?.HOME;
|
|
48
|
+
if (home === undefined || home.length === 0)
|
|
49
|
+
return NATIVE_ROUTE_CONFLICT;
|
|
50
|
+
const route = join(home, ".agents", "skills", "superpowers");
|
|
51
|
+
const asset = join(route, "using-superpowers", "SKILL.md");
|
|
52
|
+
let routeIsSymlink;
|
|
53
|
+
try {
|
|
54
|
+
routeIsSymlink = (await lstat(route)).isSymbolicLink();
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
return isMissingPath(cause) ? "" : NATIVE_ROUTE_CONFLICT;
|
|
58
|
+
}
|
|
59
|
+
if (!routeIsSymlink) {
|
|
60
|
+
try {
|
|
61
|
+
await lstat(asset);
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
return isMissingPath(cause) ? "" : NATIVE_ROUTE_CONFLICT;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const resolvedAsset = await realpath(asset);
|
|
69
|
+
if (!(await stat(resolvedAsset)).isFile())
|
|
70
|
+
return NATIVE_ROUTE_CONFLICT;
|
|
71
|
+
const contents = await readFile(resolvedAsset, "utf8");
|
|
72
|
+
const lines = contents.split(/\r?\n/u);
|
|
73
|
+
if (lines[0] !== "---")
|
|
74
|
+
return "";
|
|
75
|
+
const end = lines.indexOf("---", 1);
|
|
76
|
+
if (end < 0 ||
|
|
77
|
+
!lines
|
|
78
|
+
.slice(1, end)
|
|
79
|
+
.some((line) => /^name:[ \t]+using-superpowers[ \t]*$/u.test(line))) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return NATIVE_ROUTE_CONFLICT;
|
|
85
|
+
}
|
|
86
|
+
return NATIVE_ROUTE_CONFLICT;
|
|
87
|
+
}
|
|
88
|
+
export async function inspectCodexConflicts(ctx, installedListing) {
|
|
89
|
+
const conflicts = new Set();
|
|
90
|
+
for (const plugin of codexInstalledPluginsFromJson(installedListing)) {
|
|
91
|
+
const conflict = unmanagedPluginConflict(plugin);
|
|
92
|
+
if (conflict.length > 0)
|
|
93
|
+
conflicts.add(conflict);
|
|
94
|
+
}
|
|
95
|
+
const nativeConflict = await nativeRouteConflict(ctx);
|
|
96
|
+
if (nativeConflict.length > 0)
|
|
97
|
+
conflicts.add(nativeConflict);
|
|
98
|
+
return [...conflicts];
|
|
99
|
+
}
|
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import { lstat, readdir, readFile, readlink, stat } from "node:fs/promises";
|
|
2
1
|
import { homedir } from "node:os";
|
|
3
2
|
import { posix } from "node:path";
|
|
4
|
-
import { COMMIT_RE } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
lstat,
|
|
9
|
-
stat,
|
|
10
|
-
readdir,
|
|
11
|
-
readlink,
|
|
12
|
-
readFile,
|
|
13
|
-
};
|
|
14
|
-
// The Python's own SEMVER_RE (:15-22). Deliberately not SEMVER_BASE_SOURCE
|
|
15
|
-
// from src/domain/refs.ts, which omits the `+build` component; widening the
|
|
16
|
-
// shared constant would change TAG_RE and is out of scope.
|
|
17
|
-
const SEMVER_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
3
|
+
import { COMMIT_RE, SEMVER_RE } from "../../domain/refs.js";
|
|
4
|
+
import { compareByCodePoint, pythonStrip } from "../../python-text.js";
|
|
5
|
+
import { DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "../../skill-validation.js";
|
|
6
|
+
import { parseStrictJson } from "../../strict-json.js";
|
|
18
7
|
const MANIFEST_JSON_PROFILE = {
|
|
19
8
|
duplicateKeys: "last-wins",
|
|
20
9
|
nonStandardConstants: "reject",
|
|
@@ -443,61 +432,6 @@ const REQUIRED_FILES = [
|
|
|
443
432
|
"README.md",
|
|
444
433
|
"CODE_OF_CONDUCT.md",
|
|
445
434
|
];
|
|
446
|
-
/** Python `sorted()` orders by code point; JavaScript's default sort does not. */
|
|
447
|
-
function compareByCodePoint(left, right) {
|
|
448
|
-
// `Array.from` splits by code point, exactly as spreading would; oxlint's
|
|
449
|
-
// `no-misused-spread` rejects the spread form, and grapheme segmentation is
|
|
450
|
-
// the wrong unit here — Python compares code points.
|
|
451
|
-
const leftPoints = Array.from(left);
|
|
452
|
-
const rightPoints = Array.from(right);
|
|
453
|
-
const shared = Math.min(leftPoints.length, rightPoints.length);
|
|
454
|
-
for (let index = 0; index < shared; index += 1) {
|
|
455
|
-
const a = leftPoints[index].codePointAt(0);
|
|
456
|
-
const b = rightPoints[index].codePointAt(0);
|
|
457
|
-
if (a !== b)
|
|
458
|
-
return a - b;
|
|
459
|
-
}
|
|
460
|
-
return leftPoints.length - rightPoints.length;
|
|
461
|
-
}
|
|
462
|
-
async function validateSkillFrontmatter(skillMd, skillName, errors, deps) {
|
|
463
|
-
let contents;
|
|
464
|
-
try {
|
|
465
|
-
contents = STRICT_DECODER.decode(await deps.readFile(skillMd));
|
|
466
|
-
}
|
|
467
|
-
catch {
|
|
468
|
-
errors.push(`skill \`${skillName}\` has unreadable UTF-8 \`SKILL.md\``);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
if (contents === "") {
|
|
472
|
-
errors.push(`skill \`${skillName}\` has empty \`SKILL.md\``);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
const lines = pythonSplitlines(contents);
|
|
476
|
-
if (lines.length === 0 || lines[0] !== "---") {
|
|
477
|
-
errors.push(`skill \`${skillName}\` must start with \`---\``);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const closingIndex = lines.indexOf("---", 1);
|
|
481
|
-
if (closingIndex === -1) {
|
|
482
|
-
errors.push(`skill \`${skillName}\` frontmatter is not closed`);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
const frontmatter = lines.slice(1, closingIndex);
|
|
486
|
-
for (const key of ["name", "description"]) {
|
|
487
|
-
const matches = frontmatter.filter((line) => line.startsWith(`${key}:`));
|
|
488
|
-
if (matches.length !== 1) {
|
|
489
|
-
errors.push(`skill \`${skillName}\` frontmatter must contain exactly one top-level \`${key}:\``);
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
const value = pythonStrip(matches[0].slice(matches[0].indexOf(":") + 1));
|
|
493
|
-
if (value === "" ||
|
|
494
|
-
value === "''" ||
|
|
495
|
-
value === '""' ||
|
|
496
|
-
value.startsWith("#")) {
|
|
497
|
-
errors.push(`skill \`${skillName}\` frontmatter field \`${key}\` must be non-empty`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
435
|
async function validateHookSubtree(pluginRoot, hooksRoot, errors, deps) {
|
|
502
436
|
let resolvedRoot;
|
|
503
437
|
try {
|
|
@@ -513,9 +447,9 @@ async function validateHookSubtree(pluginRoot, hooksRoot, errors, deps) {
|
|
|
513
447
|
isLink = (await inspectLink(path, deps)) === "symlink";
|
|
514
448
|
}
|
|
515
449
|
catch {
|
|
516
|
-
// The `src/generated-plugin.ts:
|
|
517
|
-
// The three probes sharing the *subtree* string are `src/generated-plugin.ts:
|
|
518
|
-
// `src/generated-plugin.ts:
|
|
450
|
+
// The `src/harnesses/codex/generated-plugin.ts:583::isLink =` probe reaches the first catch bounded by `src/harnesses/codex/generated-plugin.ts:592::let rawTarget`; the `src/harnesses/codex/generated-plugin.ts:594::rawTarget = decodePathBytes` readlink reaches the same site text.
|
|
451
|
+
// The three probes sharing the *subtree* string are `src/harnesses/codex/generated-plugin.ts:627::resolvedDirectory =`, `src/harnesses/codex/generated-plugin.ts:637::children =`, and
|
|
452
|
+
// `src/harnesses/codex/generated-plugin.ts:646::deps, true)) === "directory"`, not this one.
|
|
519
453
|
errors.push(`generated hook symlink could not be inspected: ${path}`);
|
|
520
454
|
return false;
|
|
521
455
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { failureResult, hasTerminalControl, successResult, } from "../../adapter-result.js";
|
|
2
3
|
import { codexInspect, codexInstall, codexRemove, } from "./adapter.js";
|
|
3
|
-
import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./
|
|
4
|
-
import { codexInstallReceipt, codexPresentation
|
|
4
|
+
import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./prepare.js";
|
|
5
|
+
import { codexInstallReceipt, codexPresentation } from "./presentation.js";
|
|
5
6
|
import { reportLegacyState, requireManagedUpdateControl, requireNoLegacyState, verifyUninstalledResources, } from "./lifecycle.js";
|
|
6
|
-
import {
|
|
7
|
+
import { SafetyError } from "../../safety-error.js";
|
|
8
|
+
import { commitMatches } from "../../status.js";
|
|
7
9
|
function preserveFailure(result) {
|
|
8
10
|
if (result.outcome.ok) {
|
|
9
11
|
throw new Error("cannot preserve a successful adapter result as failure");
|
|
@@ -40,6 +42,23 @@ function resourceFlag(result, key) {
|
|
|
40
42
|
const value = bag[key];
|
|
41
43
|
return typeof value === "boolean" ? value : null;
|
|
42
44
|
}
|
|
45
|
+
function conflictDescriptions(result) {
|
|
46
|
+
const raw = result.conflicts;
|
|
47
|
+
if (raw === null || raw === undefined)
|
|
48
|
+
return [];
|
|
49
|
+
if (!Array.isArray(raw))
|
|
50
|
+
return null;
|
|
51
|
+
const descriptions = [];
|
|
52
|
+
for (const item of raw) {
|
|
53
|
+
if (typeof item !== "string" ||
|
|
54
|
+
item.length === 0 ||
|
|
55
|
+
hasTerminalControl(item)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
descriptions.push(item);
|
|
59
|
+
}
|
|
60
|
+
return descriptions;
|
|
61
|
+
}
|
|
43
62
|
function installDecision(legacy) {
|
|
44
63
|
switch (legacy.kind) {
|
|
45
64
|
case "ok":
|
|
@@ -87,7 +106,11 @@ export function normalizeCodexOwnership(result) {
|
|
|
87
106
|
return malformed(result, "adapter returned a non-string identity_state for inspect --view ownership");
|
|
88
107
|
}
|
|
89
108
|
const identityState = identity.value;
|
|
90
|
-
const
|
|
109
|
+
const conflicts = conflictDescriptions(record);
|
|
110
|
+
if (conflicts === null) {
|
|
111
|
+
return malformed(result, "expected an array of strings at conflicts");
|
|
112
|
+
}
|
|
113
|
+
const legacyEligibility = identityState.length === 0
|
|
91
114
|
? {
|
|
92
115
|
kind: "blocked",
|
|
93
116
|
output: {
|
|
@@ -96,6 +119,19 @@ export function normalizeCodexOwnership(result) {
|
|
|
96
119
|
},
|
|
97
120
|
}
|
|
98
121
|
: installDecision(requireNoLegacyState(identityState));
|
|
122
|
+
const installEligibility = legacyEligibility.kind === "blocked" || conflicts.length === 0
|
|
123
|
+
? legacyEligibility
|
|
124
|
+
: {
|
|
125
|
+
kind: "blocked",
|
|
126
|
+
output: {
|
|
127
|
+
stdout: [],
|
|
128
|
+
stderr: [
|
|
129
|
+
"Conflicting unmanaged Superpowers Codex resources require manual resolution:",
|
|
130
|
+
...conflicts.map((conflict) => `- ${conflict}`),
|
|
131
|
+
"Remove or disable each resource manually, then retry.",
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
};
|
|
99
135
|
const verification = verifyUninstalledResources(result);
|
|
100
136
|
const legacyReport = reportLegacyState(identityState);
|
|
101
137
|
let removalVerification;
|
|
@@ -132,6 +168,7 @@ export function normalizeCodexOwnership(result) {
|
|
|
132
168
|
removalVerification,
|
|
133
169
|
postRemovalOutput,
|
|
134
170
|
presentationValue: identityState,
|
|
171
|
+
presentationConflicts: conflicts,
|
|
135
172
|
}, result.outcome.messages);
|
|
136
173
|
}
|
|
137
174
|
export function normalizeCodexControl(result) {
|
|
@@ -243,8 +280,21 @@ function requirements(command, env) {
|
|
|
243
280
|
},
|
|
244
281
|
];
|
|
245
282
|
}
|
|
283
|
+
async function mutationRoots(ctx) {
|
|
284
|
+
const env = ctx.env ?? {};
|
|
285
|
+
const configured = env.CODEX_HOME;
|
|
286
|
+
if (configured !== undefined && configured.length > 0) {
|
|
287
|
+
return [resolve(configured)];
|
|
288
|
+
}
|
|
289
|
+
const home = env.HOME;
|
|
290
|
+
if (home === undefined || home.length === 0) {
|
|
291
|
+
throw new SafetyError("codex-harness", "cannot determine Codex state root without HOME");
|
|
292
|
+
}
|
|
293
|
+
return [join(resolve(home), ".codex")];
|
|
294
|
+
}
|
|
246
295
|
export const codexHarness = {
|
|
247
296
|
preparationLocation: codexPreparationLocation,
|
|
297
|
+
mutationRoots,
|
|
248
298
|
validatePreparationBeforeFetch: validateCodexPreparationBeforeFetch,
|
|
249
299
|
prepareCandidate: prepareCodexCandidate,
|
|
250
300
|
inspectPrepared: inspectCodexPrepared,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { copyFile, cp, mkdir, readdir, readFile, readlink, stat, symlink, } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, } from "
|
|
4
|
-
import { SafetyError } from "
|
|
5
|
-
import { parseStrictJson } from "
|
|
3
|
+
import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, } from "../../safe-path.js";
|
|
4
|
+
import { SafetyError } from "../../safety-error.js";
|
|
5
|
+
import { parseStrictJson } from "../../strict-json.js";
|
|
6
6
|
// Annotated, not `as const`, matching
|
|
7
7
|
// `src/provenance.ts:20::export const PROVENANCE_STRICT_PROFILE` and
|
|
8
8
|
// `src/selection-store.ts:19::const SELECTION_JSON_PROFILE`. The annotation
|
|
@@ -77,11 +77,11 @@ export async function readManifest(path) {
|
|
|
77
77
|
catch {
|
|
78
78
|
// Deliberately drops the cause on both branches. `detail(cause)` on the
|
|
79
79
|
// read branch surfaced a raw errno (`ENOENT: … open '<path>'`), which the
|
|
80
|
-
// `hook classification failed:` site in src/adapter.ts re-emits onto the
|
|
80
|
+
// `hook classification failed:` site in src/harnesses/codex/adapter.ts re-emits onto the
|
|
81
81
|
// terminal these commands write to; the parse branch surfaced
|
|
82
|
-
// strict-json's own wording under a prefix src/manifest-overlay.ts also
|
|
82
|
+
// strict-json's own wording under a prefix src/harnesses/codex/manifest-overlay.ts also
|
|
83
83
|
// uses with CPython wording.
|
|
84
|
-
// Same text as the `cannot read manifest JSON in` site in src/adapter.ts.
|
|
84
|
+
// Same text as the `cannot read manifest JSON in` site in src/harnesses/codex/adapter.ts.
|
|
85
85
|
throw hookError(`cannot read manifest JSON in ${path}`);
|
|
86
86
|
}
|
|
87
87
|
let parsed;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SafetyError } from "
|
|
2
|
-
import { parseStrictJson, } from "
|
|
1
|
+
import { SafetyError } from "../../safety-error.js";
|
|
2
|
+
import { parseStrictJson, } from "../../strict-json.js";
|
|
3
3
|
const ACCEPT_CONSTANTS = {
|
|
4
4
|
duplicateKeys: "last-wins",
|
|
5
5
|
nonStandardConstants: "accept",
|
|
@@ -59,6 +59,13 @@ function hasTerminalControl(value) {
|
|
|
59
59
|
export function installedListingHas(raw, arrayKey, field, value) {
|
|
60
60
|
return checkedItems(raw, ACCEPT_CONSTANTS, arrayKey, field).some((item) => item[field] === value);
|
|
61
61
|
}
|
|
62
|
+
export function codexInstalledPluginsFromJson(raw) {
|
|
63
|
+
return checkedItems(raw, ACCEPT_CONSTANTS, "installed", "pluginId").map((item) => ({
|
|
64
|
+
pluginId: item.pluginId,
|
|
65
|
+
installed: typeof item.installed === "boolean" ? item.installed : null,
|
|
66
|
+
enabled: typeof item.enabled === "boolean" ? item.enabled : null,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
62
69
|
export function marketplaceRootFromJson(raw, marketplaceName) {
|
|
63
70
|
const items = checkedItems(raw, ACCEPT_CONSTANTS, "marketplaces", "name");
|
|
64
71
|
const match = items.find((item) => item.name === marketplaceName);
|
|
@@ -52,7 +52,7 @@ export function reportLegacyState(identityState) {
|
|
|
52
52
|
}
|
|
53
53
|
return unknownState(identityState);
|
|
54
54
|
}
|
|
55
|
-
export { verifyInstalledFingerprint, } from "./
|
|
55
|
+
export { verifyInstalledFingerprint, } from "./presentation.js";
|
|
56
56
|
// Ported from
|
|
57
57
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:62-70::spw_require_managed_update_control`.
|
|
58
58
|
// Both refusals reached spw_die in the shell, so both carry the `error: ` prefix
|
|
@@ -101,7 +101,7 @@ export function verifyUninstalledResources(inspectResult) {
|
|
|
101
101
|
// rather than getting its own message.
|
|
102
102
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/adapter.sh:70::expected`
|
|
103
103
|
// emits "expected Boolean adapter result at resources.plugin" for input {} —
|
|
104
|
-
// the input tests/unit/lifecycle.test.
|
|
104
|
+
// the input tests/unit/harnesses/codex/lifecycle.test.ts's "a non-object resources falls
|
|
105
105
|
// through to the Boolean message" exercises — so a distinct "not an object"
|
|
106
106
|
// message here would be a port-only divergence. Parity, not divergence. Spec
|
|
107
107
|
// §6.2.3 item 3.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { escapeNonAscii, formatPythonNumber } from "
|
|
2
|
-
import { SafetyError } from "
|
|
3
|
-
import { isRawNumber, isRawObject, parseStrictJsonPreservingNumbers, } from "
|
|
1
|
+
import { escapeNonAscii, formatPythonNumber, } from "../../python-json-format.js";
|
|
2
|
+
import { SafetyError } from "../../safety-error.js";
|
|
3
|
+
import { isRawNumber, isRawObject, parseStrictJsonPreservingNumbers, } from "../../strict-json.js";
|
|
4
4
|
const OVERLAY_PROFILE = {
|
|
5
5
|
duplicateKeys: "last-wins",
|
|
6
6
|
nonStandardConstants: "reject",
|
|
@@ -12,7 +12,7 @@ const OVERLAY_PROFILE = {
|
|
|
12
12
|
* Byte-for-byte equivalent to the CPython implementation this replaces:
|
|
13
13
|
* `json.dump(data, f, indent=2, allow_nan=False)` plus a trailing newline.
|
|
14
14
|
* The byte-parity evidence lives in
|
|
15
|
-
* `tests/baseline/manifest-overlay-parity.test.
|
|
15
|
+
* `tests/baseline/harnesses/codex/manifest-overlay-parity.test.ts` (tracked as
|
|
16
16
|
* `MANIFEST-READER-OVERLAY-01` in `docs/baseline/traceability.md`) against
|
|
17
17
|
* the fixtures under `tests/fixtures/baseline/overlay-parity/`. Known,
|
|
18
18
|
* deliberate divergences from strict byte parity are recorded in
|
|
@@ -69,7 +69,7 @@ function setMember(entries, key, value) {
|
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* Map strict-json's diagnostics onto the CPython wording the callers assert.
|
|
72
|
-
* Repository policy at `AGENTS.md:
|
|
72
|
+
* Repository policy at `AGENTS.md:92::most diagnostics are asserted as`
|
|
73
73
|
* freezes most of these diagnostics as complete-string contracts.
|
|
74
74
|
*/
|
|
75
75
|
function translate(cause, source, path) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { cp, mkdir, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { failureResult, successResult, } from "
|
|
3
|
+
import { failureResult, successResult, } from "../../adapter-result.js";
|
|
4
4
|
import { codexBuild } from "./adapter.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ARTIFACT_RECEIPT } from "../../artifact-tree.js";
|
|
6
|
+
import { assessCodexCompatibility, readCodexAssessment, writeCodexAssessment, } from "./compatibility.js";
|
|
6
7
|
import { readManifest } from "./hooks.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { readGeneratedCommitLenient, readStrictProvenanceField, writeProvenance, } from "../../provenance.js";
|
|
9
|
+
import { classifyPathNoFollow } from "../../safe-path.js";
|
|
10
|
+
import { SafetyError } from "../../safety-error.js";
|
|
11
|
+
import { manifestVersionForRef } from "../../upstream-version.js";
|
|
10
12
|
// Order is inherited from the original prepare command; the first miss wins.
|
|
11
13
|
const REQUIRED_UPSTREAM = [
|
|
12
14
|
{ path: "skills", label: "skills/" },
|
|
@@ -27,6 +29,12 @@ const RESOLUTION_KINDS = [
|
|
|
27
29
|
"ref",
|
|
28
30
|
"raw-commit",
|
|
29
31
|
];
|
|
32
|
+
function unassessedCompatibility() {
|
|
33
|
+
return {
|
|
34
|
+
kind: "unknown",
|
|
35
|
+
reason: "Codex compatibility assessment is not available",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
30
38
|
function prepareError(message, cause) {
|
|
31
39
|
return new SafetyError("prepare", message, { cause });
|
|
32
40
|
}
|
|
@@ -166,26 +174,66 @@ export async function prepareCodexCandidate(input, ctx) {
|
|
|
166
174
|
if (built.status !== 0) {
|
|
167
175
|
return failureResult("build", "invalid-status", "adapter reported failure without an error outcome", [], built.outcome.messages);
|
|
168
176
|
}
|
|
169
|
-
|
|
177
|
+
const compatibility = await assessCodexCompatibility(input.upstreamRoot, input.selection);
|
|
178
|
+
if (compatibility.kind === "supported") {
|
|
179
|
+
try {
|
|
180
|
+
await writeCodexAssessment(input.candidateRoot, input.selection, (await regularFileExists(join(input.upstreamRoot, ".codex-plugin/plugin.json")))
|
|
181
|
+
? "upstream"
|
|
182
|
+
: "fallback");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return failureResult("prepare", "assessment-failed", `cannot record Codex compatibility assessment: ${input.candidateRoot}`, [], built.outcome.messages);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return successResult(built.outcome.operation, {
|
|
189
|
+
root: input.candidateRoot,
|
|
190
|
+
commit: input.selection.desiredCommit,
|
|
191
|
+
compatibility,
|
|
192
|
+
identity: input.selection.desiredCommit,
|
|
193
|
+
}, built.outcome.messages);
|
|
170
194
|
}
|
|
171
195
|
export async function inspectCodexPrepared(selection, ctx) {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
196
|
+
const root = codexPreparationLocation(ctx).destinationRoot;
|
|
197
|
+
const observedIdentity = await readGeneratedCommitLenient(join(root, ".superpowers-upstream.json"));
|
|
198
|
+
const compatibility = unassessedCompatibility();
|
|
199
|
+
try {
|
|
200
|
+
if ((await classifyPathNoFollow(join(root, ARTIFACT_RECEIPT))) === "missing")
|
|
201
|
+
return successResult("inspect-prepared", {
|
|
202
|
+
kind: "needs-prepare",
|
|
203
|
+
observedIdentity,
|
|
204
|
+
compatibility,
|
|
205
|
+
}, []);
|
|
206
|
+
const artifact = await readCodexAssessment(root);
|
|
207
|
+
const source = await readStrictProvenanceField(join(artifact.root, ".superpowers-upstream.json"), "source");
|
|
208
|
+
if (artifact.commit === selection.desiredCommit &&
|
|
209
|
+
source === selection.effectiveSource) {
|
|
210
|
+
if (artifact.compatibility.kind === "supported")
|
|
211
|
+
return successResult("inspect-prepared", {
|
|
212
|
+
kind: "current",
|
|
213
|
+
artifact,
|
|
214
|
+
observedIdentity,
|
|
215
|
+
compatibility: artifact.compatibility,
|
|
216
|
+
}, []);
|
|
217
|
+
return successResult("inspect-prepared", {
|
|
218
|
+
kind: "needs-prepare",
|
|
219
|
+
observedIdentity,
|
|
220
|
+
compatibility: artifact.compatibility,
|
|
221
|
+
}, []);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return failureResult("inspect-prepared", "invalid-assessment", `cannot inspect Codex prepared artifact: ${root}`, [], []);
|
|
175
226
|
}
|
|
176
227
|
return successResult("inspect-prepared", {
|
|
177
|
-
kind: "
|
|
178
|
-
artifact: {
|
|
179
|
-
root: join(ctx.root, "plugins", "superpowers"),
|
|
180
|
-
commit: observedIdentity,
|
|
181
|
-
},
|
|
228
|
+
kind: "needs-prepare",
|
|
182
229
|
observedIdentity,
|
|
230
|
+
compatibility,
|
|
183
231
|
}, []);
|
|
184
232
|
}
|
|
185
233
|
export async function readCodexPrepared(ctx) {
|
|
186
234
|
let commit = "";
|
|
187
235
|
try {
|
|
188
|
-
const value = await readStrictProvenanceField(
|
|
236
|
+
const value = await readStrictProvenanceField(join(codexPreparationLocation(ctx).destinationRoot, ".superpowers-upstream.json"), "commit");
|
|
189
237
|
if (typeof value === "string")
|
|
190
238
|
commit = value;
|
|
191
239
|
}
|
|
@@ -195,5 +243,13 @@ export async function readCodexPrepared(ctx) {
|
|
|
195
243
|
if (commit.length === 0) {
|
|
196
244
|
return failureResult("read-prepared", "missing-commit", "generated metadata missing desired commit after prepare", [], []);
|
|
197
245
|
}
|
|
198
|
-
|
|
246
|
+
try {
|
|
247
|
+
const artifact = await readCodexAssessment(codexPreparationLocation(ctx).destinationRoot);
|
|
248
|
+
if (artifact.compatibility.kind !== "supported")
|
|
249
|
+
throw new Error("unsupported profile");
|
|
250
|
+
return successResult("read-prepared", artifact, []);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return failureResult("read-prepared", "invalid-assessment", "generated Codex compatibility assessment is missing or invalid", [], []);
|
|
254
|
+
}
|
|
199
255
|
}
|