superpowers-manager 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/install.js +16 -6
- package/dist/commands/prepare.js +1 -1
- package/dist/commands/uninstall.js +1 -1
- package/dist/harnesses/codex/adapter.js +147 -18
- package/dist/harnesses/codex/generated-plugin.js +1 -1
- package/dist/harnesses/codex/harness.js +35 -17
- package/dist/harnesses/codex/hooks.js +1 -1
- package/dist/harnesses/codex/json.js +6 -2
- package/dist/harnesses/codex/lifecycle.js +1 -1
- package/dist/harnesses/codex/marketplace.js +148 -0
- package/dist/harnesses/codex/paths.js +97 -0
- package/dist/harnesses/codex/prepare.js +34 -2
- package/dist/harnesses/codex/presentation.js +4 -3
- package/dist/harnesses/codex/publication.js +538 -0
- package/dist/harnesses/codex/recovery.js +338 -0
- package/dist/harnesses/codex/state.js +131 -0
- package/dist/harnesses/codex/stored-state.js +249 -0
- package/dist/provenance.js +3 -3
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Codex installs the Manager-owned marketplace plugin after preparation and
|
|
|
56
56
|
validation. Review upstream hook definitions with Codex's `/hooks` flow before
|
|
57
57
|
trusting or running them; updates can change a referenced script or surface a
|
|
58
58
|
new definition. The manager neither creates hooks nor changes Codex trust state.
|
|
59
|
-
See the [Codex reference](https://github.com/j7an/superpowers-manager/blob/main/docs/codex.md).
|
|
59
|
+
See the [Codex reference](https://github.com/j7an/superpowers-manager/blob/main/docs/codex.md#durable-storage-migration-and-recovery) for durable storage, migration, and recovery.
|
|
60
60
|
|
|
61
61
|
### Pi
|
|
62
62
|
|
package/dist/cli.js
CHANGED
|
@@ -341,7 +341,7 @@ async function dispatch(adapter, parsed) {
|
|
|
341
341
|
}
|
|
342
342
|
return status;
|
|
343
343
|
}
|
|
344
|
-
export { resolvePackageRoot, isMain, parseArgs,
|
|
344
|
+
export { resolvePackageRoot, isMain, parseArgs, commandRequirements, commandRequirementsFor, preflight, usage, main, };
|
|
345
345
|
if (isMain(import.meta.filename, process.argv[1])) {
|
|
346
346
|
main().catch((cause) => {
|
|
347
347
|
console.error(`error: ${oneLine(cause)}`);
|
package/dist/commands/install.js
CHANGED
|
@@ -190,18 +190,28 @@ async function gatherInstallStages(ctx, selection, artifact) {
|
|
|
190
190
|
const restoration = restored.ok
|
|
191
191
|
? `installed state after rollback: ${restored.result.outcome.result.kind}; identity=${restored.result.outcome.result.observedIdentity}`
|
|
192
192
|
: "installed state after rollback could not be inspected";
|
|
193
|
-
const
|
|
193
|
+
const verificationOutput = inspection === null
|
|
194
|
+
? null
|
|
195
|
+
: ctx.adapter.presentation.renderInstallVerification(selection.desiredCommit, install.result, inspection);
|
|
196
|
+
const inspectionMessage = verificationOutput === null && !inspected.ok
|
|
197
|
+
? inspected.message
|
|
198
|
+
: null;
|
|
199
|
+
const settlementMessage = !settlement.ok
|
|
194
200
|
? settlement.message
|
|
195
|
-
:
|
|
196
|
-
? inspected.message
|
|
197
|
-
: "installation could not be verified";
|
|
201
|
+
: null;
|
|
198
202
|
return {
|
|
199
203
|
kind: "verified",
|
|
200
204
|
outcomes,
|
|
201
205
|
status: 1,
|
|
202
|
-
stdout: [],
|
|
206
|
+
stdout: verificationOutput?.stdout ?? [],
|
|
203
207
|
stderr: [
|
|
204
|
-
...(
|
|
208
|
+
...(verificationOutput?.stderr ?? []),
|
|
209
|
+
...(inspectionMessage === null
|
|
210
|
+
? []
|
|
211
|
+
: [`error: ${inspectionMessage}`]),
|
|
212
|
+
...(settlementMessage === null
|
|
213
|
+
? []
|
|
214
|
+
: [`error: ${settlementMessage}`]),
|
|
205
215
|
restoration,
|
|
206
216
|
],
|
|
207
217
|
};
|
package/dist/commands/prepare.js
CHANGED
|
@@ -418,7 +418,7 @@ async function performPrepare(argv, ctx) {
|
|
|
418
418
|
// completed before cleanup ran, so it is not being reported as unverified
|
|
419
419
|
// -- but something did still go wrong, and AGENTS.md's fail-closed rule
|
|
420
420
|
// extends to it. Mirrors
|
|
421
|
-
// `src/commands/install.ts:
|
|
421
|
+
// `src/commands/install.ts:562-570::if (cleanupWarning`.
|
|
422
422
|
ctx.stderr.write(`error: ${cleanupWarning}\n`);
|
|
423
423
|
return 1;
|
|
424
424
|
}
|
|
@@ -163,7 +163,7 @@ async function performUninstall(argv, ctx) {
|
|
|
163
163
|
// blindly.
|
|
164
164
|
//
|
|
165
165
|
// A cause outside ctx.adapter's AdapterFailure guard
|
|
166
|
-
// (`src/harnesses/codex/adapter.ts:
|
|
166
|
+
// (`src/harnesses/codex/adapter.ts:1081::async function runCodexOperation<T = JsonValue>(`) does NOT
|
|
167
167
|
// reach here: invoke() catches it inside gatherUninstall and converts it
|
|
168
168
|
// to a hand-written message carried as UninstallOutcome data, exactly as
|
|
169
169
|
// src/commands/probe.ts's inspect() does for the same cause.
|
|
@@ -5,10 +5,12 @@ import { access, copyFile, mkdir, readFile, realpath, stat, writeFile, } from "n
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
7
7
|
import { AdapterMessageLog, failureResult, successResult, } from "../../adapter-result.js";
|
|
8
|
-
import { activePluginVersionFromJson, installedListingHas, marketplaceRootFromJson, } from "./json.js";
|
|
8
|
+
import { activePluginVersionFromJson, codexInstalledPluginsFromJson, installedListingHas, marketplaceRootFromJson, } from "./json.js";
|
|
9
9
|
import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./conflicts.js";
|
|
10
10
|
import { oneLine } from "../../cli-arguments.js";
|
|
11
11
|
import { installedCommitFromRoot, installedRootForVersion, pathsEqual, } from "./state.js";
|
|
12
|
+
import { codexHome } from "./paths.js";
|
|
13
|
+
import { readCodexStoredState } from "./stored-state.js";
|
|
12
14
|
import { validateGeneratedPlugin } from "./generated-plugin.js";
|
|
13
15
|
import { classifyHooks, materializeHooks, readManifest, } from "./hooks.js";
|
|
14
16
|
import { applyManifestOverlay } from "./manifest-overlay.js";
|
|
@@ -19,6 +21,12 @@ const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
|
|
|
19
21
|
const MARKETPLACE_NAME = "superpowers-manager";
|
|
20
22
|
const LEGACY_PLUGIN_ID = CODEX_LEGACY_PLUGIN_ID;
|
|
21
23
|
const LEGACY_MARKETPLACE_NAME = "superpowers-wrapper";
|
|
24
|
+
export function codexInstallRefreshMode(env) {
|
|
25
|
+
const refreshMode = env.SUPERPOWERS_INSTALL_REFRESH_MODE || "add-only";
|
|
26
|
+
return refreshMode === "add-only" || refreshMode === "remove-add"
|
|
27
|
+
? refreshMode
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
22
30
|
class AdapterFailure extends Error {
|
|
23
31
|
code;
|
|
24
32
|
hints;
|
|
@@ -95,6 +103,14 @@ export { runCommand as runCommandForTest };
|
|
|
95
103
|
function commandFailed(result) {
|
|
96
104
|
return result.status !== 0;
|
|
97
105
|
}
|
|
106
|
+
async function storedStateAfterListingFailure(env, code, message) {
|
|
107
|
+
try {
|
|
108
|
+
return await readCodexStoredState(env, process.cwd());
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
fail(code, message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
98
114
|
// Exported only so its unit test can reach it. No integration test can cover
|
|
99
115
|
// it: on Linux glibc, `execvp`'s ENOEXEC falls back to `/bin/sh`, so the spawn
|
|
100
116
|
// still succeeds, and every other candidate errno is already peeled off by an
|
|
@@ -383,25 +399,27 @@ async function runInstall(packageRoot, env, log) {
|
|
|
383
399
|
fail("invalid-arguments", `package root not found: ${packageRoot}`);
|
|
384
400
|
}
|
|
385
401
|
const codexBin = env.SUPERPOWERS_CODEX || "codex";
|
|
386
|
-
const refreshMode = env
|
|
402
|
+
const refreshMode = codexInstallRefreshMode(env);
|
|
387
403
|
await requireCodex(codexBin, env);
|
|
388
|
-
if (refreshMode
|
|
389
|
-
fail("invalid-arguments", `unsupported SUPERPOWERS_INSTALL_REFRESH_MODE: ${
|
|
404
|
+
if (refreshMode === null) {
|
|
405
|
+
fail("invalid-arguments", `unsupported SUPERPOWERS_INSTALL_REFRESH_MODE: ${env.SUPERPOWERS_INSTALL_REFRESH_MODE}`);
|
|
390
406
|
}
|
|
391
407
|
let entered = false;
|
|
392
408
|
try {
|
|
393
409
|
return await withWorkspace(tmpdir(), "superpowers-manager.adapter-install.", async () => {
|
|
394
410
|
entered = true;
|
|
395
411
|
const marketplaceList = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
|
|
396
|
-
if (commandFailed(marketplaceList)) {
|
|
397
|
-
fail("install-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
|
|
398
|
-
}
|
|
399
412
|
let registeredRoot;
|
|
400
|
-
|
|
401
|
-
registeredRoot =
|
|
413
|
+
if (commandFailed(marketplaceList)) {
|
|
414
|
+
registeredRoot = (await storedStateAfterListingFailure(env, "install-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`)).managerMarketplaceRoot;
|
|
402
415
|
}
|
|
403
|
-
|
|
404
|
-
|
|
416
|
+
else {
|
|
417
|
+
try {
|
|
418
|
+
registeredRoot = marketplaceRootFromJson(marketplaceList.stdout, MARKETPLACE_NAME);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
fail("install-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
|
|
422
|
+
}
|
|
405
423
|
}
|
|
406
424
|
if (registeredRoot.length === 0) {
|
|
407
425
|
const added = await mutationCommand(log, codexBin, ["plugin", "marketplace", "add", packageRoot], env);
|
|
@@ -543,16 +561,34 @@ async function runInspect(view, context, env, log) {
|
|
|
543
561
|
entered = true;
|
|
544
562
|
const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
|
|
545
563
|
if (commandFailed(plugins)) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
564
|
+
const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
|
|
565
|
+
const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedListingJson);
|
|
566
|
+
const managerPresent = stored.managerPluginPresent ||
|
|
567
|
+
stored.managerMarketplaceRoot.length > 0;
|
|
568
|
+
const legacyPresent = stored.legacyPluginPresent ||
|
|
569
|
+
stored.legacyMarketplaceRoot !== null;
|
|
570
|
+
return {
|
|
571
|
+
view: "ownership",
|
|
572
|
+
resources: {
|
|
573
|
+
plugin: stored.managerPluginPresent,
|
|
574
|
+
marketplace: true,
|
|
575
|
+
},
|
|
576
|
+
legacy_resources: {
|
|
577
|
+
plugin: stored.legacyPluginPresent,
|
|
578
|
+
marketplace: stored.legacyMarketplaceRoot !== null,
|
|
579
|
+
},
|
|
580
|
+
identity_state: managerPresent
|
|
581
|
+
? legacyPresent
|
|
582
|
+
? "both"
|
|
583
|
+
: "manager"
|
|
584
|
+
: legacyPresent
|
|
585
|
+
? "legacy"
|
|
586
|
+
: "neither",
|
|
587
|
+
conflicts: [...conflicts],
|
|
588
|
+
};
|
|
551
589
|
}
|
|
552
590
|
let managerPlugin;
|
|
553
591
|
let legacyPlugin;
|
|
554
|
-
let managerMarketplace;
|
|
555
|
-
let legacyMarketplace;
|
|
556
592
|
let conflicts;
|
|
557
593
|
try {
|
|
558
594
|
managerPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", PLUGIN_ID);
|
|
@@ -562,6 +598,35 @@ async function runInspect(view, context, env, log) {
|
|
|
562
598
|
catch {
|
|
563
599
|
fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
|
|
564
600
|
}
|
|
601
|
+
const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
|
|
602
|
+
if (commandFailed(marketplaces)) {
|
|
603
|
+
const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
|
|
604
|
+
const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedListingJson);
|
|
605
|
+
const managerPresent = true;
|
|
606
|
+
const legacyPresent = stored.legacyPluginPresent ||
|
|
607
|
+
stored.legacyMarketplaceRoot !== null;
|
|
608
|
+
return {
|
|
609
|
+
view: "ownership",
|
|
610
|
+
resources: {
|
|
611
|
+
plugin: stored.managerPluginPresent,
|
|
612
|
+
marketplace: true,
|
|
613
|
+
},
|
|
614
|
+
legacy_resources: {
|
|
615
|
+
plugin: stored.legacyPluginPresent,
|
|
616
|
+
marketplace: stored.legacyMarketplaceRoot !== null,
|
|
617
|
+
},
|
|
618
|
+
identity_state: managerPresent
|
|
619
|
+
? legacyPresent
|
|
620
|
+
? "both"
|
|
621
|
+
: "manager"
|
|
622
|
+
: legacyPresent
|
|
623
|
+
? "legacy"
|
|
624
|
+
: "neither",
|
|
625
|
+
conflicts: [...conflicts],
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
let managerMarketplace;
|
|
629
|
+
let legacyMarketplace;
|
|
565
630
|
try {
|
|
566
631
|
managerMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", MARKETPLACE_NAME);
|
|
567
632
|
legacyMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", LEGACY_MARKETPLACE_NAME);
|
|
@@ -628,6 +693,70 @@ export function codexRemove(input, context) {
|
|
|
628
693
|
export function codexInspect(view, context) {
|
|
629
694
|
return runCodexOperation("inspect", context, (env, log) => runInspect(view, context, env, log));
|
|
630
695
|
}
|
|
696
|
+
export function codexReadNativeState(context) {
|
|
697
|
+
return runCodexOperation("native-state", context, async (env, log) => {
|
|
698
|
+
const codexBin = env.SUPERPOWERS_CODEX || "codex";
|
|
699
|
+
await requireCodex(codexBin, env);
|
|
700
|
+
const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
|
|
701
|
+
if (commandFailed(plugins)) {
|
|
702
|
+
const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
|
|
703
|
+
return {
|
|
704
|
+
marketplaceRoot: stored.managerMarketplaceRoot,
|
|
705
|
+
pluginPresent: stored.managerPluginPresent,
|
|
706
|
+
pluginEnabled: stored.managerPluginEnabled,
|
|
707
|
+
activeVersion: null,
|
|
708
|
+
activeRoot: null,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
let manager;
|
|
712
|
+
let activeVersion;
|
|
713
|
+
try {
|
|
714
|
+
const managers = codexInstalledPluginsFromJson(plugins.stdout).filter((plugin) => plugin.pluginId === PLUGIN_ID);
|
|
715
|
+
if (managers.length > 1)
|
|
716
|
+
fail("inspect-failed", "Codex manager plugin appears more than once");
|
|
717
|
+
manager = managers[0];
|
|
718
|
+
activeVersion =
|
|
719
|
+
manager === undefined
|
|
720
|
+
? ""
|
|
721
|
+
: activePluginVersionFromJson(plugins.stdout, PLUGIN_ID);
|
|
722
|
+
}
|
|
723
|
+
catch (cause) {
|
|
724
|
+
if (cause instanceof AdapterFailure)
|
|
725
|
+
throw cause;
|
|
726
|
+
fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
|
|
727
|
+
}
|
|
728
|
+
const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
|
|
729
|
+
if (commandFailed(marketplaces)) {
|
|
730
|
+
const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
|
|
731
|
+
return {
|
|
732
|
+
marketplaceRoot: stored.managerMarketplaceRoot,
|
|
733
|
+
pluginPresent: stored.managerPluginPresent,
|
|
734
|
+
pluginEnabled: stored.managerPluginEnabled,
|
|
735
|
+
activeVersion: null,
|
|
736
|
+
activeRoot: null,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
let marketplaceRoot;
|
|
740
|
+
try {
|
|
741
|
+
marketplaceRoot = marketplaceRootFromJson(marketplaces.stdout, MARKETPLACE_NAME, true);
|
|
742
|
+
}
|
|
743
|
+
catch (cause) {
|
|
744
|
+
if (cause instanceof AdapterFailure)
|
|
745
|
+
throw cause;
|
|
746
|
+
fail("inspect-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
|
|
747
|
+
}
|
|
748
|
+
const searchRoot = env.SUPERPOWERS_INSTALLED_SEARCH_ROOT || codexHome(env, process.cwd());
|
|
749
|
+
return {
|
|
750
|
+
marketplaceRoot: marketplaceRoot || null,
|
|
751
|
+
pluginPresent: manager !== undefined,
|
|
752
|
+
pluginEnabled: manager?.enabled === true,
|
|
753
|
+
activeVersion: activeVersion || null,
|
|
754
|
+
activeRoot: activeVersion
|
|
755
|
+
? installedRootForVersion(searchRoot, MARKETPLACE_NAME, "superpowers", activeVersion)
|
|
756
|
+
: null,
|
|
757
|
+
};
|
|
758
|
+
});
|
|
759
|
+
}
|
|
631
760
|
export async function runAdapter(argv, context) {
|
|
632
761
|
const rawOperation = argv[0];
|
|
633
762
|
const operation = rawOperation || "adapter";
|
|
@@ -39,7 +39,7 @@ function decodePathBytes(bytes) {
|
|
|
39
39
|
* syscall, so a string carrying one would open a different file than the
|
|
40
40
|
* caller named. CPython raises `UnicodeEncodeError` instead.
|
|
41
41
|
*/
|
|
42
|
-
|
|
42
|
+
function hasUnpairedSurrogate(value) {
|
|
43
43
|
for (let index = 0; index < value.length; index += 1) {
|
|
44
44
|
const unit = value.charCodeAt(index);
|
|
45
45
|
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { join, resolve } from "node:path";
|
|
2
1
|
import { failureResult, hasTerminalControl, successResult, } from "../../adapter-result.js";
|
|
3
|
-
import { codexInspect, codexInstall,
|
|
2
|
+
import { codexInspect, codexInstall, } from "./adapter.js";
|
|
4
3
|
import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./prepare.js";
|
|
5
4
|
import { codexInstallReceipt, codexPresentation } from "./presentation.js";
|
|
6
5
|
import { reportLegacyState, requireManagedUpdateControl, requireNoLegacyState, verifyUninstalledResources, } from "./lifecycle.js";
|
|
7
|
-
import { SafetyError } from "../../safety-error.js";
|
|
8
6
|
import { commitMatches } from "../../status.js";
|
|
7
|
+
import { codexHome, codexPaths } from "./paths.js";
|
|
8
|
+
import { installCodexMarketplace, removeCodexMarketplace, } from "./publication.js";
|
|
9
|
+
import { readCodexRecovery } from "./recovery.js";
|
|
10
|
+
import { inspectCodexInstallation } from "./state.js";
|
|
9
11
|
function preserveFailure(result) {
|
|
10
12
|
if (result.outcome.ok) {
|
|
11
13
|
throw new Error("cannot preserve a successful adapter result as failure");
|
|
@@ -281,16 +283,34 @@ function requirements(command, env) {
|
|
|
281
283
|
];
|
|
282
284
|
}
|
|
283
285
|
async function mutationRoots(ctx) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
return [codexHome(ctx.env ?? {}, process.cwd())];
|
|
287
|
+
}
|
|
288
|
+
async function inspectCodexUpdateControl(ctx) {
|
|
289
|
+
const normalized = normalizeCodexControl(await codexInspect("update-control", ctx));
|
|
290
|
+
if (!normalized.outcome.ok)
|
|
291
|
+
return normalized;
|
|
292
|
+
const paths = codexPaths(ctx.env ?? {}, process.cwd());
|
|
293
|
+
let diagnostic = null;
|
|
294
|
+
try {
|
|
295
|
+
if ((await readCodexRecovery(paths)) !== null) {
|
|
296
|
+
diagnostic = `Codex recovery required; preserve material at ${paths.recoveryRoot} for manual resolution`;
|
|
297
|
+
}
|
|
288
298
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
throw new SafetyError("codex-harness", "cannot determine Codex state root without HOME");
|
|
299
|
+
catch {
|
|
300
|
+
diagnostic = `cannot inspect Codex recovery state at ${paths.recoveryRoot}`;
|
|
292
301
|
}
|
|
293
|
-
|
|
302
|
+
if (diagnostic === null)
|
|
303
|
+
return normalized;
|
|
304
|
+
const decision = {
|
|
305
|
+
kind: "blocked",
|
|
306
|
+
output: { stdout: [], stderr: [`error: ${diagnostic}`] },
|
|
307
|
+
};
|
|
308
|
+
return successResult(normalized.outcome.operation, {
|
|
309
|
+
probeEligibility: decision,
|
|
310
|
+
mutationEligibility: decision,
|
|
311
|
+
presentationValue: diagnostic,
|
|
312
|
+
recoveryState: "required",
|
|
313
|
+
}, normalized.outcome.messages);
|
|
294
314
|
}
|
|
295
315
|
export const codexHarness = {
|
|
296
316
|
preparationLocation: codexPreparationLocation,
|
|
@@ -300,13 +320,11 @@ export const codexHarness = {
|
|
|
300
320
|
inspectPrepared: inspectCodexPrepared,
|
|
301
321
|
readPrepared: readCodexPrepared,
|
|
302
322
|
inspectOwnership: async (ctx) => normalizeCodexOwnership(await codexInspect("ownership", ctx)),
|
|
303
|
-
inspectUpdateControl:
|
|
304
|
-
inspectInstalled:
|
|
305
|
-
install: async (
|
|
306
|
-
return normalizeCodexInstallForContext(await codexInstall(ctx.root, ctx), ctx);
|
|
307
|
-
},
|
|
323
|
+
inspectUpdateControl: inspectCodexUpdateControl,
|
|
324
|
+
inspectInstalled: inspectCodexInstallation,
|
|
325
|
+
install: async (artifact, ctx) => installCodexMarketplace(artifact, ctx, async (root, context) => normalizeCodexInstallForContext(await codexInstall(root, context), context)),
|
|
308
326
|
remove: async (input, ctx) => {
|
|
309
|
-
const result = await
|
|
327
|
+
const result = await removeCodexMarketplace(input, ctx);
|
|
310
328
|
if (!result.outcome.ok)
|
|
311
329
|
return preserveFailure(result);
|
|
312
330
|
if (result.status !== 0) {
|
|
@@ -4,7 +4,7 @@ import { assertExistingContained, assertProspectiveContained, classifyPathNoFoll
|
|
|
4
4
|
import { SafetyError } from "../../safety-error.js";
|
|
5
5
|
import { parseStrictJson } from "../../strict-json.js";
|
|
6
6
|
// Annotated, not `as const`, matching
|
|
7
|
-
// `src/provenance.ts:20::
|
|
7
|
+
// `src/provenance.ts:20::const PROVENANCE_STRICT_PROFILE` and
|
|
8
8
|
// `src/selection-store.ts:19::const SELECTION_JSON_PROFILE`. The annotation
|
|
9
9
|
// rejects a misspelled or unsupported profile field here rather than at the
|
|
10
10
|
// parseStrictJson call.
|
|
@@ -66,9 +66,13 @@ export function codexInstalledPluginsFromJson(raw) {
|
|
|
66
66
|
enabled: typeof item.enabled === "boolean" ? item.enabled : null,
|
|
67
67
|
}));
|
|
68
68
|
}
|
|
69
|
-
export function marketplaceRootFromJson(raw, marketplaceName) {
|
|
69
|
+
export function marketplaceRootFromJson(raw, marketplaceName, requireUnique = false) {
|
|
70
70
|
const items = checkedItems(raw, ACCEPT_CONSTANTS, "marketplaces", "name");
|
|
71
|
-
const
|
|
71
|
+
const matches = items.filter((item) => item.name === marketplaceName);
|
|
72
|
+
if (requireUnique && matches.length > 1) {
|
|
73
|
+
fail("marketplace appears more than once");
|
|
74
|
+
}
|
|
75
|
+
const match = matches[0];
|
|
72
76
|
if (match === undefined)
|
|
73
77
|
return "";
|
|
74
78
|
if (typeof match.root !== "string" || match.root.length === 0) {
|
|
@@ -52,7 +52,7 @@ export function reportLegacyState(identityState) {
|
|
|
52
52
|
}
|
|
53
53
|
return unknownState(identityState);
|
|
54
54
|
}
|
|
55
|
-
export { verifyInstalledFingerprint
|
|
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
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { copyFile, cp, lstat, mkdir, readdir, rm, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
|
|
4
|
+
import { assertNoFollowType, classifyPathNoFollow } from "../../safe-path.js";
|
|
5
|
+
import { SafetyError } from "../../safety-error.js";
|
|
6
|
+
import { readCodexAssessment } from "./compatibility.js";
|
|
7
|
+
const MARKETPLACE_PATH = ".agents/plugins/marketplace.json";
|
|
8
|
+
const PLUGIN_PATH = "plugins/superpowers";
|
|
9
|
+
function inspectionError(root, cause) {
|
|
10
|
+
return new SafetyError("codex-marketplace", `cannot inspect owned Codex marketplace: ${root}`, { cause });
|
|
11
|
+
}
|
|
12
|
+
function validateMarketplace(value) {
|
|
13
|
+
const plugins = value.plugins;
|
|
14
|
+
if (!Array.isArray(plugins) || plugins.length !== 1) {
|
|
15
|
+
throw new Error("marketplace plugins");
|
|
16
|
+
}
|
|
17
|
+
const plugin = plugins[0];
|
|
18
|
+
if (plugin === null || typeof plugin !== "object" || Array.isArray(plugin)) {
|
|
19
|
+
throw new Error("marketplace plugin");
|
|
20
|
+
}
|
|
21
|
+
const source = plugin.source;
|
|
22
|
+
if (plugin.name !== "superpowers" ||
|
|
23
|
+
source === null ||
|
|
24
|
+
typeof source !== "object" ||
|
|
25
|
+
Array.isArray(source) ||
|
|
26
|
+
source.source !== "local" ||
|
|
27
|
+
source.path !== "./plugins/superpowers") {
|
|
28
|
+
throw new Error("marketplace source");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function requireEntries(root, expected) {
|
|
32
|
+
const actual = await readdir(root);
|
|
33
|
+
if (actual.length !== expected.length ||
|
|
34
|
+
actual.some((entry) => !expected.includes(entry))) {
|
|
35
|
+
throw new Error("tree entries");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function readMarketplace(root) {
|
|
39
|
+
try {
|
|
40
|
+
await assertNoFollowType(root, ["directory"]);
|
|
41
|
+
const before = await lstat(root);
|
|
42
|
+
await requireEntries(root, [".agents", "plugins", ARTIFACT_RECEIPT]);
|
|
43
|
+
await assertNoFollowType(join(root, ".agents"), ["directory"]);
|
|
44
|
+
await assertNoFollowType(join(root, "plugins"), ["directory"]);
|
|
45
|
+
await assertNoFollowType(join(root, ARTIFACT_RECEIPT), ["regular-file"]);
|
|
46
|
+
await requireEntries(join(root, ".agents"), ["plugins"]);
|
|
47
|
+
await requireEntries(join(root, ".agents", "plugins"), [
|
|
48
|
+
"marketplace.json",
|
|
49
|
+
]);
|
|
50
|
+
await requireEntries(join(root, "plugins"), ["superpowers"]);
|
|
51
|
+
const pluginRoot = join(root, PLUGIN_PATH);
|
|
52
|
+
await assertNoFollowType(pluginRoot, ["directory"]);
|
|
53
|
+
validateMarketplace(await readArtifactObject(root, join(root, MARKETPLACE_PATH)));
|
|
54
|
+
const receipt = await readArtifactObject(root, join(root, ARTIFACT_RECEIPT));
|
|
55
|
+
if (receipt.schema !== 1 ||
|
|
56
|
+
receipt.manager !== "superpowers-manager" ||
|
|
57
|
+
receipt.harness !== "codex" ||
|
|
58
|
+
receipt.generation !== "codex-marketplace-v1" ||
|
|
59
|
+
typeof receipt.digest !== "string" ||
|
|
60
|
+
!ARTIFACT_DIGEST_RE.test(receipt.digest)) {
|
|
61
|
+
throw new Error("receipt");
|
|
62
|
+
}
|
|
63
|
+
const digest = await digestArtifactTree(root);
|
|
64
|
+
if (digest !== receipt.digest)
|
|
65
|
+
throw new Error("digest");
|
|
66
|
+
const artifact = await readCodexAssessment(pluginRoot);
|
|
67
|
+
const after = await lstat(root);
|
|
68
|
+
if (before.dev !== after.dev || before.ino !== after.ino) {
|
|
69
|
+
throw new Error("root changed");
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
root,
|
|
73
|
+
dev: before.dev,
|
|
74
|
+
ino: before.ino,
|
|
75
|
+
digest,
|
|
76
|
+
artifact,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (cause) {
|
|
80
|
+
throw inspectionError(root, cause);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function removeOwnedCandidate(root, identity) {
|
|
84
|
+
try {
|
|
85
|
+
await assertNoFollowType(root, ["directory"]);
|
|
86
|
+
const current = await lstat(root);
|
|
87
|
+
if (current.dev !== identity.dev || current.ino !== identity.ino)
|
|
88
|
+
return;
|
|
89
|
+
await rm(root, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// A candidate that cannot still be proven as ours must remain untouched.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function stageCodexMarketplace(artifact, packageRoot, candidateRoot) {
|
|
96
|
+
let candidateIdentity = null;
|
|
97
|
+
try {
|
|
98
|
+
const assessed = await readCodexAssessment(artifact.root);
|
|
99
|
+
if (assessed.commit !== artifact.commit)
|
|
100
|
+
throw new Error("artifact commit");
|
|
101
|
+
const sourceDigest = await digestArtifactTree(artifact.root);
|
|
102
|
+
await assertNoFollowType(candidateRoot, ["missing"]);
|
|
103
|
+
await mkdir(candidateRoot, { mode: 0o700 });
|
|
104
|
+
const candidate = await lstat(candidateRoot);
|
|
105
|
+
candidateIdentity = { dev: candidate.dev, ino: candidate.ino };
|
|
106
|
+
await mkdir(join(candidateRoot, ".agents", "plugins"), { recursive: true });
|
|
107
|
+
await mkdir(join(candidateRoot, "plugins"), { recursive: true });
|
|
108
|
+
await copyFile(join(packageRoot, MARKETPLACE_PATH), join(candidateRoot, MARKETPLACE_PATH), 0);
|
|
109
|
+
await cp(artifact.root, join(candidateRoot, PLUGIN_PATH), {
|
|
110
|
+
dereference: false,
|
|
111
|
+
errorOnExist: true,
|
|
112
|
+
force: false,
|
|
113
|
+
recursive: true,
|
|
114
|
+
verbatimSymlinks: true,
|
|
115
|
+
});
|
|
116
|
+
const copied = await readCodexAssessment(join(candidateRoot, PLUGIN_PATH));
|
|
117
|
+
if (copied.commit !== artifact.commit ||
|
|
118
|
+
(await digestArtifactTree(copied.root)) !== sourceDigest) {
|
|
119
|
+
throw new Error("copied artifact");
|
|
120
|
+
}
|
|
121
|
+
validateMarketplace(await readArtifactObject(candidateRoot, join(candidateRoot, MARKETPLACE_PATH)));
|
|
122
|
+
const receipt = {
|
|
123
|
+
schema: 1,
|
|
124
|
+
manager: "superpowers-manager",
|
|
125
|
+
harness: "codex",
|
|
126
|
+
generation: "codex-marketplace-v1",
|
|
127
|
+
digest: await digestArtifactTree(candidateRoot),
|
|
128
|
+
};
|
|
129
|
+
await writeFile(join(candidateRoot, ARTIFACT_RECEIPT), JSON.stringify(receipt) + "\n", { flag: "wx" });
|
|
130
|
+
return await readMarketplace(candidateRoot);
|
|
131
|
+
}
|
|
132
|
+
catch (cause) {
|
|
133
|
+
if (candidateIdentity !== null) {
|
|
134
|
+
await removeOwnedCandidate(candidateRoot, candidateIdentity);
|
|
135
|
+
}
|
|
136
|
+
throw new SafetyError("codex-marketplace", `cannot stage owned Codex marketplace: ${candidateRoot}`, { cause });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function readCodexMarketplace(root) {
|
|
140
|
+
try {
|
|
141
|
+
if ((await classifyPathNoFollow(root)) === "missing")
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
catch (cause) {
|
|
145
|
+
throw inspectionError(root, cause);
|
|
146
|
+
}
|
|
147
|
+
return await readMarketplace(root);
|
|
148
|
+
}
|