create-agdf 0.13.4 → 0.13.6
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 +7 -1
- package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/.codex-plugin/plugin.json +2 -3
- package/generated/plugins/agdf/hooks/session-start.sh +31 -0
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +30 -9
- package/generated/plugins/agdf/runtime/agdf-local.js +8 -1
- package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +30 -9
- package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/local-validator.js +116 -31
- package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +206 -0
- package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
- package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
- package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +64 -19
- package/generated/plugins/agdf/submission/openai/availability.md +1 -1
- package/generated/plugins/agdf/submission/openai/capability-matrix.json +1 -1
- package/generated/plugins/agdf/submission/openai/release-notes.md +1 -1
- package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
- package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +30 -9
- package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
- package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +1 -1
- package/generated/submissions/openai/agdf/submission/openai/inventory.json +9 -9
- package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
- package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
- package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
- package/generated/submissions/openai/agdf/submission/openai/release-notes.md +1 -1
- package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
- package/lib/cli/application.js +4 -3
- package/lib/fs-swap.js +26 -0
- package/lib/installers/local-development.js +202 -0
- package/lib/installers/local-marketplace.js +181 -36
- package/lib/installers/opencode.js +17 -2
- package/lib/installers/plugin-installers.js +86 -22
- package/lib/lifecycle/operations.js +9 -5
- package/lib/lifecycle/status.js +10 -2
- package/lib/public-plugin/builder.js +5 -4
- package/lib/runtime/local-validator.js +116 -31
- package/lib/runtime/plugin-provenance.js +206 -0
- package/package.json +6 -2
|
@@ -6,6 +6,7 @@ import process from "node:process";
|
|
|
6
6
|
import { generatedRoot, pluginDefinition } from "../cli/runtime-context.js";
|
|
7
7
|
import { evaluateOpenCodeRepositoryActivation } from "./opencode-activation.js";
|
|
8
8
|
import { resolveLocalValidator } from "../runtime/local-validator.js";
|
|
9
|
+
import { validateLocalOpenCodePackageSource } from "./local-development.js";
|
|
9
10
|
|
|
10
11
|
const contractModules = ["gate-transition.md", "interaction.md", "modes.md", "quality.md", "context-graph.md", "control-scaffold.md", "closeout.md"];
|
|
11
12
|
const openCodeSkillNames = pluginDefinition.skillSet.map((skill) => pluginDefinition.opencode.skillPrefix + skill.slug);
|
|
@@ -36,7 +37,15 @@ export function defaultOpenCodeConfigDir() {
|
|
|
36
37
|
return process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export function
|
|
40
|
+
export function resolveOpenCodeInstallPackageSource(packageSource) {
|
|
41
|
+
const local = packageSource ? validateLocalOpenCodePackageSource(packageSource) : null;
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
local,
|
|
44
|
+
specifier: local?.specifier ?? `${pluginDefinition.opencode.npmPackage}@${pluginDefinition.version}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function installOpenCodeGlobalPlugin(configDir, dependencies = {}) {
|
|
40
49
|
try {
|
|
41
50
|
assertGlobalOpenCodeSurfaceWritable(configDir);
|
|
42
51
|
} catch (error) {
|
|
@@ -97,8 +106,11 @@ export function installOpenCodeGlobalPlugin(configDir) {
|
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
mkdirSync(configDir, { recursive: true });
|
|
109
|
+
let localPackageSource = null;
|
|
100
110
|
try {
|
|
101
|
-
const
|
|
111
|
+
const packageSource = resolveOpenCodeInstallPackageSource(dependencies.packageSource);
|
|
112
|
+
localPackageSource = packageSource.local;
|
|
113
|
+
const packageSpecifier = packageSource.specifier;
|
|
102
114
|
const invocation = openCodeNpmInvocation(["install", "--silent", "--save-prod", "--save-exact", packageSpecifier]);
|
|
103
115
|
execFileSync(invocation.executable, invocation.args, {
|
|
104
116
|
cwd: configDir,
|
|
@@ -123,6 +135,9 @@ export function installOpenCodeGlobalPlugin(configDir) {
|
|
|
123
135
|
added: !alreadyInstalled,
|
|
124
136
|
transition: openCodePackageTransition(previousPackage, installedPackage),
|
|
125
137
|
sdk_alignment: sdkAlignment,
|
|
138
|
+
package_source: localPackageSource
|
|
139
|
+
? { kind: "local_checkout", path: localPackageSource.tarball, digest: localPackageSource.digest, version: localPackageSource.version }
|
|
140
|
+
: { kind: "registry", path: "", digest: "", version: pluginDefinition.version },
|
|
126
141
|
};
|
|
127
142
|
}
|
|
128
143
|
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { pluginDefinition } from "../cli/runtime-context.js";
|
|
3
|
-
import { classifyMarketplaceList, prepareLocalMarketplace } from "./local-marketplace.js";
|
|
3
|
+
import { CODEX_REGISTRATION_REVISION, classifyMarketplaceList, inspectLocalMarketplaceProjection, isCodexLocalInstallVersion, prepareLocalMarketplace } from "./local-marketplace.js";
|
|
4
4
|
|
|
5
5
|
export function installCodexGlobalPlugin({ exec = execFileSync, prepare = prepareLocalMarketplace, dataRoot } = {}) {
|
|
6
6
|
const expectedVersion = pluginDefinition.version;
|
|
7
7
|
const nativeOutput = [];
|
|
8
|
-
const transaction = prepare({ expectedVersion, ...(dataRoot ? { dataRoot } : {}) });
|
|
9
|
-
const
|
|
8
|
+
const transaction = prepare({ expectedVersion, codexRegistrationRevision: CODEX_REGISTRATION_REVISION, ...(dataRoot ? { dataRoot } : {}) });
|
|
9
|
+
const expectedInstallVersion = transaction.codexInstallVersion ?? expectedVersion;
|
|
10
|
+
const migration = { state: "unknown", source: "", addedLocal: false, removedLegacy: false, refreshOwnedLocal: false, removedOwnedLocal: false };
|
|
10
11
|
try {
|
|
11
12
|
const marketplaceOutput = runPluginPhase(exec, "codex", ["plugin", "marketplace", "list", "--json"], "marketplace", captureOptions());
|
|
12
|
-
migrateMarketplace({ surface: "codex", exec, output: marketplaceOutput, root: transaction.root, nativeOutput, migration });
|
|
13
|
-
nativeOutput.push(runPluginPhase(exec, "codex", ["plugin", "add", "agdf", "--
|
|
13
|
+
migrateMarketplace({ surface: "codex", exec, output: marketplaceOutput, root: transaction.root, nativeOutput, migration, refreshOwnedLocal: transaction.changed === true });
|
|
14
|
+
nativeOutput.push(runPluginPhase(exec, "codex", ["plugin", "add", "agdf@agdf", "--json"], "plugin_operation", captureOptions()));
|
|
14
15
|
const listOutput = runPluginPhase(exec, "codex", ["plugin", "list"], "verification", captureOptions());
|
|
15
16
|
const installedVersion = pluginVersionFromList(listOutput, "agdf@agdf");
|
|
16
|
-
if (installedVersion !==
|
|
17
|
-
throw lifecycleAdapterError("version", versionMismatchMessage("Codex", "agdf@agdf",
|
|
17
|
+
if (installedVersion !== expectedInstallVersion) {
|
|
18
|
+
throw lifecycleAdapterError("version", versionMismatchMessage("Codex", "agdf@agdf", expectedInstallVersion, installedVersion, "npx --yes @agdf/cli@latest codex"));
|
|
18
19
|
}
|
|
19
20
|
transaction.commit();
|
|
20
21
|
return {
|
|
21
|
-
surface: "codex", operation: migration.state === "owned_local_current" ? "update" : "install", expectedVersion, installedVersion, verificationStatus: "healthy",
|
|
22
|
-
evidence: [
|
|
22
|
+
surface: "codex", operation: migration.state === "owned_local_current" ? "update" : "install", expectedVersion: expectedInstallVersion, canonicalVersion: expectedVersion, installedVersion, verificationStatus: "healthy",
|
|
23
|
+
evidence: [
|
|
24
|
+
"durable_local_marketplace",
|
|
25
|
+
`marketplace:${migration.state}`,
|
|
26
|
+
"codex plugin list",
|
|
27
|
+
...(transaction.pluginRoot ? [`staged_plugin_root:${transaction.pluginRoot}`, "staged_installation_provenance:matched"] : []),
|
|
28
|
+
...(transaction.sourceDigest ? [`source_digest:${transaction.sourceDigest}`] : []),
|
|
29
|
+
...(transaction.digest ? [`plugin_digest:${transaction.digest}`] : []),
|
|
30
|
+
...(expectedInstallVersion === expectedVersion ? [] : [`canonical_version:${expectedVersion}`, `local_install_version:${expectedInstallVersion}`]),
|
|
31
|
+
],
|
|
23
32
|
nativeOutput: nativeOutput.filter(Boolean).map(String),
|
|
24
33
|
};
|
|
25
34
|
} catch (error) {
|
|
@@ -39,7 +48,12 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
|
|
|
39
48
|
nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", "marketplace", "update", "agdf"], "marketplace", captureOptions()));
|
|
40
49
|
const beforeList = runPluginPhase(exec, "claude", ["plugin", "list"], "verification", captureOptions());
|
|
41
50
|
const alreadyInstalled = pluginListHasPlugin(beforeList, "agdf@agdf");
|
|
42
|
-
|
|
51
|
+
// `claude plugin update` keeps the cached copy when the version is unchanged, so a
|
|
52
|
+
// same-version local source change never reaches the host; reinstall replaces the content.
|
|
53
|
+
if (alreadyInstalled) {
|
|
54
|
+
nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", "uninstall", "agdf@agdf"], "plugin_operation", captureOptions()));
|
|
55
|
+
}
|
|
56
|
+
nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", "install", "agdf@agdf"], "plugin_operation", captureOptions()));
|
|
43
57
|
const afterList = runPluginPhase(exec, "claude", ["plugin", "list"], "verification", captureOptions());
|
|
44
58
|
const installedVersion = pluginVersionFromList(afterList, "agdf@agdf");
|
|
45
59
|
if (installedVersion && installedVersion !== expectedVersion) {
|
|
@@ -52,7 +66,15 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
|
|
|
52
66
|
expectedVersion,
|
|
53
67
|
installedVersion,
|
|
54
68
|
verificationStatus: installedVersion ? "healthy" : "degraded",
|
|
55
|
-
evidence: [
|
|
69
|
+
evidence: [
|
|
70
|
+
"durable_local_marketplace",
|
|
71
|
+
`marketplace:${migration.state}`,
|
|
72
|
+
"claude plugin list",
|
|
73
|
+
...(transaction.pluginRoot ? [`staged_plugin_root:${transaction.pluginRoot}`, "staged_installation_provenance:matched"] : []),
|
|
74
|
+
...(transaction.sourceDigest ? [`source_digest:${transaction.sourceDigest}`] : []),
|
|
75
|
+
...(transaction.digest ? [`plugin_digest:${transaction.digest}`] : []),
|
|
76
|
+
...(installedVersion ? [] : ["host_did_not_expose_version"]),
|
|
77
|
+
],
|
|
56
78
|
nativeOutput: nativeOutput.filter(Boolean).map(String),
|
|
57
79
|
};
|
|
58
80
|
} catch (error) {
|
|
@@ -61,13 +83,21 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
|
|
|
61
83
|
}
|
|
62
84
|
}
|
|
63
85
|
|
|
64
|
-
function migrateMarketplace({ surface, exec, output, root, nativeOutput, migration }) {
|
|
86
|
+
function migrateMarketplace({ surface, exec, output, root, nativeOutput, migration, refreshOwnedLocal = false }) {
|
|
65
87
|
const classification = classifyMarketplaceList(surface, output, root);
|
|
66
88
|
Object.assign(migration, classification);
|
|
67
89
|
if (["conflict", "unknown"].includes(classification.state)) {
|
|
68
90
|
throw lifecycleAdapterError("marketplace", `Refusing to replace non-AGDF marketplace registration agdf (${classification.reason || classification.state}).`, classification);
|
|
69
91
|
}
|
|
70
92
|
const executable = surface === "claude" ? "claude" : "codex";
|
|
93
|
+
if (surface === "codex" && classification.state === "owned_local_current" && refreshOwnedLocal) {
|
|
94
|
+
migration.refreshOwnedLocal = true;
|
|
95
|
+
nativeOutput.push(runPluginPhase(exec, executable, ["plugin", "marketplace", "remove", "agdf", "--json"], "marketplace", captureOptions()));
|
|
96
|
+
migration.removedOwnedLocal = true;
|
|
97
|
+
nativeOutput.push(runPluginPhase(exec, executable, ["plugin", "marketplace", "add", root, "--json"], "marketplace", captureOptions()));
|
|
98
|
+
migration.addedLocal = true;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
71
101
|
if (classification.state === "legacy_github") {
|
|
72
102
|
const args = surface === "claude"
|
|
73
103
|
? ["plugin", "marketplace", "remove", "agdf", "--scope", "user"]
|
|
@@ -91,10 +121,23 @@ function recoverMarketplace({ surface, exec, migration, transaction, error }) {
|
|
|
91
121
|
try {
|
|
92
122
|
exec(executable, args, captureOptions());
|
|
93
123
|
recovery.push({ args, status: "restored" });
|
|
124
|
+
return true;
|
|
94
125
|
} catch (rollbackError) {
|
|
95
126
|
recovery.push({ args, status: "failed", message: commandErrorText(rollbackError) });
|
|
127
|
+
return false;
|
|
96
128
|
}
|
|
97
129
|
};
|
|
130
|
+
if (migration?.refreshOwnedLocal) {
|
|
131
|
+
const refreshedRegistrationRemoved = migration.addedLocal
|
|
132
|
+
? attempt(["plugin", "marketplace", "remove", "agdf", "--json"])
|
|
133
|
+
: true;
|
|
134
|
+
rollbackMarketplaceFilesystem(transaction, recovery);
|
|
135
|
+
if (migration.removedOwnedLocal && refreshedRegistrationRemoved) {
|
|
136
|
+
attempt(["plugin", "marketplace", "add", migration.source || transaction.root, "--json"]);
|
|
137
|
+
}
|
|
138
|
+
error.evidence = { ...(error.evidence ?? {}), rollback: recovery };
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
98
141
|
if (migration?.addedLocal) {
|
|
99
142
|
attempt(surface === "claude"
|
|
100
143
|
? ["plugin", "marketplace", "remove", "agdf", "--scope", "user"]
|
|
@@ -105,32 +148,44 @@ function recoverMarketplace({ surface, exec, migration, transaction, error }) {
|
|
|
105
148
|
? ["plugin", "marketplace", "add", migration.source, "--scope", "user"]
|
|
106
149
|
: ["plugin", "marketplace", "add", migration.source, "--json"]);
|
|
107
150
|
}
|
|
151
|
+
rollbackMarketplaceFilesystem(transaction, recovery);
|
|
152
|
+
error.evidence = { ...(error.evidence ?? {}), rollback: recovery };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function rollbackMarketplaceFilesystem(transaction, recovery) {
|
|
108
156
|
try {
|
|
109
157
|
transaction.rollback();
|
|
110
158
|
recovery.push({ filesystem: transaction.root, status: "restored" });
|
|
111
159
|
} catch (rollbackError) {
|
|
112
160
|
recovery.push({ filesystem: transaction.root, status: "failed", message: rollbackError.message });
|
|
113
161
|
}
|
|
114
|
-
error.evidence = { ...(error.evidence ?? {}), rollback: recovery };
|
|
115
162
|
}
|
|
116
163
|
|
|
117
164
|
function captureOptions() {
|
|
118
165
|
return { encoding: "utf8", stdio: "pipe" };
|
|
119
166
|
}
|
|
120
167
|
|
|
121
|
-
export function inspectPluginSurface(surface, exec = execFileSync) {
|
|
168
|
+
export function inspectPluginSurface(surface, exec = execFileSync, options = {}) {
|
|
122
169
|
const executable = surface === "claude" ? "claude" : "codex";
|
|
123
170
|
const pluginId = "agdf@agdf";
|
|
124
171
|
try {
|
|
125
172
|
const output = exec(executable, ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
|
|
126
173
|
const installed = pluginListHasPlugin(output, pluginId);
|
|
127
174
|
const version = installed ? pluginVersionFromList(output, pluginId) : "";
|
|
175
|
+
let localDevelopmentVersion = false;
|
|
176
|
+
if (surface === "codex" && isCodexLocalInstallVersion(pluginDefinition.version, version)) {
|
|
177
|
+
try {
|
|
178
|
+
localDevelopmentVersion = inspectLocalMarketplaceProjection(options)?.codexInstallVersion === version;
|
|
179
|
+
} catch {
|
|
180
|
+
localDevelopmentVersion = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
128
183
|
return {
|
|
129
|
-
status: !installed ? "not_installed" : version === pluginDefinition.version ? "healthy" : "degraded",
|
|
184
|
+
status: !installed ? "not_installed" : version === pluginDefinition.version || localDevelopmentVersion ? "healthy" : "degraded",
|
|
130
185
|
surface,
|
|
131
186
|
version: version || null,
|
|
132
187
|
expected_version: pluginDefinition.version,
|
|
133
|
-
evidence: [`${executable} plugin list`, ...(installed && !version ? ["host_did_not_expose_version"] : [])],
|
|
188
|
+
evidence: [`${executable} plugin list`, ...(localDevelopmentVersion ? [`canonical_version:${pluginDefinition.version}`, `local_install_version:${version}`] : []), ...(installed && !version ? ["host_did_not_expose_version"] : [])],
|
|
134
189
|
};
|
|
135
190
|
} catch (error) {
|
|
136
191
|
return { status: "unknown", surface, version: null, expected_version: pluginDefinition.version, evidence: [commandErrorText(error)] };
|
|
@@ -167,14 +222,23 @@ export function pluginListHasPlugin(output, pluginId) {
|
|
|
167
222
|
.some((line) => line.includes(pluginId));
|
|
168
223
|
}
|
|
169
224
|
|
|
225
|
+
const VERSION_PATTERN = "(\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?)";
|
|
226
|
+
|
|
170
227
|
export function pluginVersionFromList(output, pluginId) {
|
|
171
228
|
const escapedPluginId = pluginId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
229
|
+
const lines = output.split(/\r?\n/);
|
|
230
|
+
const entryIndex = lines.findIndex((entry) => new RegExp(`(^|\\s)${escapedPluginId}(\\s|$)`).test(entry));
|
|
231
|
+
if (entryIndex < 0) return "";
|
|
232
|
+
const sameLine = lines[entryIndex].match(new RegExp(`\\b${VERSION_PATTERN}\\b`));
|
|
233
|
+
if (sameLine) return sameLine[1];
|
|
234
|
+
// Current `claude plugin list` prints the plugin id and its `Version:` on separate
|
|
235
|
+
// lines; scan the entry's block until the next plugin id line.
|
|
236
|
+
for (let index = entryIndex + 1; index < lines.length; index += 1) {
|
|
237
|
+
if (/\S+@\S+/.test(lines[index])) break;
|
|
238
|
+
const blockMatch = lines[index].match(new RegExp(`\\bVersion:?\\s*${VERSION_PATTERN}\\b`, "i"));
|
|
239
|
+
if (blockMatch) return blockMatch[1];
|
|
240
|
+
}
|
|
241
|
+
return "";
|
|
178
242
|
}
|
|
179
243
|
|
|
180
244
|
function versionMismatchMessage(surface, pluginId, expectedVersion, installedVersion, correctiveCommand) {
|
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { pluginDefinition } from "../cli/runtime-context.js";
|
|
5
5
|
import { evaluateOpenCodeStatus, openCodeNpmInvocation } from "../installers/opencode.js";
|
|
6
6
|
import { inspectPluginSurface } from "../installers/plugin-installers.js";
|
|
7
|
+
import { inspectGeneratedRepositoryMarketplace } from "../runtime/plugin-provenance.js";
|
|
7
8
|
|
|
8
9
|
const CODEX_DISABLE_MARKER = "# AGDF-OWNED-REPOSITORY-PLUGIN-STATE";
|
|
9
10
|
|
|
@@ -12,8 +13,10 @@ function pluginSection(content, selector) {
|
|
|
12
13
|
return content.match(new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`))?.[0] ?? "";
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function repositorySelector(targetDir) {
|
|
16
|
-
|
|
16
|
+
function repositorySelector(targetDir, config = "") {
|
|
17
|
+
const repository = inspectGeneratedRepositoryMarketplace(targetDir);
|
|
18
|
+
if (repository.status === "matched") return repository.selector;
|
|
19
|
+
return pluginSection(config, "agdf@agdf-repo") ? "agdf@agdf-repo" : "agdf@agdf";
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function planRepositoryDisable(targetDir, surface) {
|
|
@@ -22,7 +25,7 @@ export function planRepositoryDisable(targetDir, surface) {
|
|
|
22
25
|
}
|
|
23
26
|
const path = join(targetDir, ".codex", "config.toml");
|
|
24
27
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
25
|
-
const selector = repositorySelector(targetDir);
|
|
28
|
+
const selector = repositorySelector(targetDir, existing);
|
|
26
29
|
const escaped = JSON.stringify(selector).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
30
|
const sectionPattern = new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`);
|
|
28
31
|
const currentSection = pluginSection(existing, selector);
|
|
@@ -49,8 +52,9 @@ export function planRepositoryDisable(targetDir, surface) {
|
|
|
49
52
|
export function verifyRepositoryDisabled(targetDir) {
|
|
50
53
|
const path = join(targetDir, ".codex", "config.toml");
|
|
51
54
|
if (!existsSync(path)) return { status: "failed", evidence: [`missing:${path}`] };
|
|
52
|
-
const
|
|
53
|
-
const
|
|
55
|
+
const config = readFileSync(path, "utf8");
|
|
56
|
+
const selector = repositorySelector(targetDir, config);
|
|
57
|
+
const section = pluginSection(config, selector);
|
|
54
58
|
const matches = [...section.matchAll(/^enabled\s*=\s*(true|false)\s*$/gm)];
|
|
55
59
|
return matches.length === 1 && matches[0][1] === "false"
|
|
56
60
|
? { status: "healthy", evidence: [`${path}:${selector}:enabled=false`] }
|
package/lib/lifecycle/status.js
CHANGED
|
@@ -5,6 +5,7 @@ import { evaluateDoctor } from "../control-evaluation/doctor.js";
|
|
|
5
5
|
import { evaluateGateCheck } from "../control-evaluation/gate-check.js";
|
|
6
6
|
import { evaluateOpenCodeStatus } from "../installers/opencode.js";
|
|
7
7
|
import { inspectPluginSurface } from "../installers/plugin-installers.js";
|
|
8
|
+
import { inspectGeneratedRepositoryMarketplace } from "../runtime/plugin-provenance.js";
|
|
8
9
|
|
|
9
10
|
function deliveryStatus(targetDir, selection, dependencies) {
|
|
10
11
|
const controlDir = join(targetDir, ".agdf", "control");
|
|
@@ -29,7 +30,8 @@ function deliveryStatus(targetDir, selection, dependencies) {
|
|
|
29
30
|
|
|
30
31
|
function repositoryStatus(targetDir, surface) {
|
|
31
32
|
if (surface === "codex") {
|
|
32
|
-
const
|
|
33
|
+
const repository = inspectGeneratedRepositoryMarketplace(targetDir);
|
|
34
|
+
const marketplace = repository.marketplacePath;
|
|
33
35
|
const disabled = join(targetDir, ".codex", "config.toml");
|
|
34
36
|
const config = existsSync(disabled) ? readFileSync(disabled, "utf8") : "";
|
|
35
37
|
const isDisabled = ["agdf@agdf-repo", "agdf@agdf"].some((selector) => {
|
|
@@ -37,7 +39,11 @@ function repositoryStatus(targetDir, surface) {
|
|
|
37
39
|
const section = config.match(new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`))?.[0] ?? "";
|
|
38
40
|
return /^enabled\s*=\s*false\s*$/m.test(section);
|
|
39
41
|
});
|
|
40
|
-
return {
|
|
42
|
+
return {
|
|
43
|
+
status: isDisabled ? "disabled" : repository.status === "matched" ? "active" : repository.status === "invalid" ? "degraded" : "not_configured",
|
|
44
|
+
scope: "repository",
|
|
45
|
+
evidence: [marketplace, ...(repository.reason ? [repository.reason] : [])],
|
|
46
|
+
};
|
|
41
47
|
}
|
|
42
48
|
if (surface === "opencode") {
|
|
43
49
|
const instructions = join(targetDir, ".opencode", pluginDefinition.opencode.instructionsFileName);
|
|
@@ -97,6 +103,8 @@ export function evaluateGeneralStatus(targetDir, options = {}, dependencies = {}
|
|
|
97
103
|
? `Run npx --yes @agdf/cli@latest ${surface} to install or repair AGDF.`
|
|
98
104
|
: repository.status === "disabled"
|
|
99
105
|
? "Restart the host to apply the repository-local disable; the healthy global AGDF installation remains available."
|
|
106
|
+
: repository.status === "degraded"
|
|
107
|
+
? "The repository marketplace is incomplete or invalid; use the supported repository scaffold or the healthy durable global AGDF installation."
|
|
100
108
|
: repository.status === "not_configured"
|
|
101
109
|
? `Run npx --yes @agdf/cli@latest ${surface === "codex" ? "codex-repo" : `${surface}-repo`} in this repository.`
|
|
102
110
|
: delivery.status === "blocked"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync,
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { renameSyncWithRetry } from "../fs-swap.js";
|
|
4
5
|
import { assertPublicPluginContract, loadJson } from "./contract.js";
|
|
5
6
|
import { renderCodexPluginManifest } from "./manifest.js";
|
|
6
7
|
import { createReadinessReport, renderReadinessReport } from "./report.js";
|
|
@@ -66,11 +67,11 @@ export function buildPublicPluginCandidate({ repoRoot, outputRoot }) {
|
|
|
66
67
|
const backupRoot = `${outputRoot}.previous`;
|
|
67
68
|
if (existsSync(backupRoot)) throw new Error(`Refusing candidate swap while recovery backup exists: ${backupRoot}`);
|
|
68
69
|
const hadPrevious = existsSync(outputRoot);
|
|
69
|
-
if (hadPrevious)
|
|
70
|
+
if (hadPrevious) renameSyncWithRetry(outputRoot, backupRoot);
|
|
70
71
|
try {
|
|
71
|
-
|
|
72
|
+
renameSyncWithRetry(temporaryRoot, outputRoot);
|
|
72
73
|
} catch (error) {
|
|
73
|
-
if (hadPrevious && existsSync(backupRoot) && !existsSync(outputRoot))
|
|
74
|
+
if (hadPrevious && existsSync(backupRoot) && !existsSync(outputRoot)) renameSyncWithRetry(backupRoot, outputRoot);
|
|
74
75
|
throw error;
|
|
75
76
|
}
|
|
76
77
|
if (hadPrevious) rmSync(backupRoot, { recursive: true, force: true });
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync,
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
3
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
4
|
import process from "node:process";
|
|
5
|
+
import {
|
|
6
|
+
INSTALLATION_PROVENANCE_FILE,
|
|
7
|
+
digestDirectory,
|
|
8
|
+
inspectGeneratedRepositoryMarketplace,
|
|
9
|
+
inspectInstallationProvenance,
|
|
10
|
+
validateDistributionProfiles,
|
|
11
|
+
} from "./plugin-provenance.js";
|
|
6
12
|
|
|
7
13
|
function readJson(path) {
|
|
8
14
|
try {
|
|
@@ -12,27 +18,12 @@ function readJson(path) {
|
|
|
12
18
|
}
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
function visit(directory) {
|
|
18
|
-
for (const name of readdirSync(directory).sort()) {
|
|
19
|
-
const path = join(directory, name);
|
|
20
|
-
const stats = statSync(path);
|
|
21
|
-
if (stats.isDirectory()) visit(path);
|
|
22
|
-
else if (stats.isFile()) files.push(path);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
visit(root);
|
|
26
|
-
const hash = createHash("sha256");
|
|
27
|
-
for (const path of files) {
|
|
28
|
-
hash.update(relative(root, path).replaceAll("\\", "/"));
|
|
29
|
-
hash.update("\0");
|
|
30
|
-
hash.update(readFileSync(path));
|
|
31
|
-
hash.update("\0");
|
|
32
|
-
}
|
|
33
|
-
return hash.digest("hex");
|
|
21
|
+
function canonicalPath(path) {
|
|
22
|
+
try { return realpathSync(path); } catch { return resolve(path); }
|
|
34
23
|
}
|
|
35
24
|
|
|
25
|
+
export { digestDirectory };
|
|
26
|
+
|
|
36
27
|
function envelope(machineValidation, options, evidence = {}) {
|
|
37
28
|
return {
|
|
38
29
|
schema_version: "1",
|
|
@@ -42,6 +33,14 @@ function envelope(machineValidation, options, evidence = {}) {
|
|
|
42
33
|
observed_version: evidence.observedVersion ?? null,
|
|
43
34
|
source: evidence.source ?? null,
|
|
44
35
|
registry_access: false,
|
|
36
|
+
distribution_profile: evidence.distributionProfile ?? null,
|
|
37
|
+
evidence_plane: evidence.evidencePlane ?? null,
|
|
38
|
+
canonical_version: evidence.canonicalVersion ?? options.expectedVersion ?? null,
|
|
39
|
+
plugin_version: evidence.pluginVersion ?? null,
|
|
40
|
+
runtime_digest: evidence.runtimeDigest ?? null,
|
|
41
|
+
source_digest: evidence.sourceDigest ?? null,
|
|
42
|
+
plugin_root: evidence.pluginRoot ?? null,
|
|
43
|
+
provenance_status: evidence.provenanceStatus ?? null,
|
|
45
44
|
...(evidence.reason ? { reason: evidence.reason } : {}),
|
|
46
45
|
};
|
|
47
46
|
}
|
|
@@ -51,17 +50,17 @@ export function resolveLocalValidator(options) {
|
|
|
51
50
|
const packageManifest = readJson(join(options.ownedPackageRoot, "package.json"));
|
|
52
51
|
const observedVersion = packageManifest?.version ?? null;
|
|
53
52
|
if (!packageManifest) {
|
|
54
|
-
return { envelope: envelope("unavailable", options, { source: "config_local_package", reason: "package_missing" }) };
|
|
53
|
+
return { envelope: envelope("unavailable", options, { source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local", reason: "package_missing" }) };
|
|
55
54
|
}
|
|
56
55
|
if (observedVersion !== options.expectedVersion) {
|
|
57
|
-
return { envelope: envelope("version_mismatch", options, { observedVersion, source: "config_local_package" }) };
|
|
56
|
+
return { envelope: envelope("version_mismatch", options, { observedVersion, source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local" }) };
|
|
58
57
|
}
|
|
59
58
|
const entrypoint = join(options.ownedPackageRoot, "bin", "create-agdf.js");
|
|
60
59
|
if (!existsSync(entrypoint)) {
|
|
61
|
-
return { envelope: envelope("unavailable", options, { observedVersion, source: "config_local_package", reason: "entrypoint_missing" }) };
|
|
60
|
+
return { envelope: envelope("unavailable", options, { observedVersion, source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local", reason: "entrypoint_missing" }) };
|
|
62
61
|
}
|
|
63
62
|
return {
|
|
64
|
-
envelope: envelope("owned_version_matched", options, { observedVersion, source: "config_local_package" }),
|
|
63
|
+
envelope: envelope("owned_version_matched", options, { observedVersion, source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local", provenanceStatus: "matched" }),
|
|
65
64
|
executable: process.execPath,
|
|
66
65
|
prefixArgs: [entrypoint],
|
|
67
66
|
};
|
|
@@ -73,30 +72,116 @@ export function resolveLocalValidator(options) {
|
|
|
73
72
|
return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", reason: "manifest_invalid" }) };
|
|
74
73
|
}
|
|
75
74
|
if (manifest) {
|
|
75
|
+
const pluginRoot = canonicalPath(options.pluginRoot ?? join(options.runtimeRoot, ".."));
|
|
76
|
+
if (options.expectedPluginRoot && canonicalPath(options.expectedPluginRoot) !== pluginRoot) {
|
|
77
|
+
return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", pluginRoot, evidencePlane: "loaded_session", distributionProfile: "runtime-plugin", provenanceStatus: "mismatch", reason: "plugin_root_mismatch" }) };
|
|
78
|
+
}
|
|
79
|
+
const definition = readJson(join(pluginRoot, "meta", "agdf-plugin.definition.json"));
|
|
80
|
+
const profile = validateDistributionProfiles(definition);
|
|
81
|
+
if (profile.status !== "matched") {
|
|
82
|
+
return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", pluginRoot, distributionProfile: "runtime-plugin", provenanceStatus: "invalid", reason: "profile_invalid" }) };
|
|
83
|
+
}
|
|
84
|
+
const codexManifest = readJson(join(pluginRoot, ".codex-plugin", "plugin.json"));
|
|
85
|
+
const claudeManifest = readJson(join(pluginRoot, ".claude-plugin", "plugin.json"));
|
|
86
|
+
const pluginVersion = codexManifest?.version ?? null;
|
|
87
|
+
if (!codexManifest || !claudeManifest || claudeManifest.version !== definition.version) {
|
|
88
|
+
return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, distributionProfile: "runtime-plugin", provenanceStatus: "invalid", reason: "manifest_invalid" }) };
|
|
89
|
+
}
|
|
76
90
|
const packageRoot = join(options.runtimeRoot, "create-agdf");
|
|
77
91
|
if (!existsSync(packageRoot)) {
|
|
78
|
-
return { envelope: envelope("unavailable", options, { source: "plugin_bundle", reason: "
|
|
92
|
+
return { envelope: envelope("unavailable", options, { source: "plugin_bundle", pluginRoot, pluginVersion, distributionProfile: "runtime-plugin", reason: "runtime_missing" }) };
|
|
79
93
|
}
|
|
80
94
|
const packageManifest = readJson(join(packageRoot, "package.json"));
|
|
81
95
|
const observedVersion = packageManifest?.version ?? manifest.version ?? null;
|
|
82
96
|
if (observedVersion !== options.expectedVersion || manifest.version !== options.expectedVersion) {
|
|
83
|
-
return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle" }) };
|
|
97
|
+
return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, distributionProfile: "runtime-plugin" }) };
|
|
84
98
|
}
|
|
85
99
|
let observedDigest = null;
|
|
86
100
|
try { observedDigest = digestDirectory(packageRoot); } catch {}
|
|
87
101
|
if (!observedDigest || manifest.digest !== observedDigest) {
|
|
88
|
-
return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", reason: "
|
|
102
|
+
return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, runtimeDigest: observedDigest, distributionProfile: "runtime-plugin", reason: "runtime_digest_mismatch" }) };
|
|
89
103
|
}
|
|
90
104
|
const entrypoint = resolve(options.runtimeRoot, manifest.entrypoint ?? "");
|
|
91
105
|
const entrypointRelative = relative(options.runtimeRoot, entrypoint);
|
|
92
106
|
if (!manifest.entrypoint || entrypointRelative === ".." || entrypointRelative.startsWith(`..${sep}`) || isAbsolute(entrypointRelative)) {
|
|
93
|
-
return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", reason: "invalid_entrypoint" }) };
|
|
107
|
+
return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, runtimeDigest: observedDigest, distributionProfile: "runtime-plugin", reason: "invalid_entrypoint" }) };
|
|
94
108
|
}
|
|
95
109
|
if (!existsSync(entrypoint)) {
|
|
96
|
-
return { envelope: envelope("unavailable", options, { observedVersion, source: "plugin_bundle", reason: "entrypoint_missing" }) };
|
|
110
|
+
return { envelope: envelope("unavailable", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, runtimeDigest: observedDigest, distributionProfile: "runtime-plugin", reason: "entrypoint_missing" }) };
|
|
111
|
+
}
|
|
112
|
+
const installed = existsSync(join(pluginRoot, INSTALLATION_PROVENANCE_FILE));
|
|
113
|
+
let provenanceStatus = "not_applicable";
|
|
114
|
+
let sourceDigest = null;
|
|
115
|
+
let evidencePlane = "loaded_session";
|
|
116
|
+
if (installed) {
|
|
117
|
+
const provenance = inspectInstallationProvenance(pluginRoot, {
|
|
118
|
+
definition,
|
|
119
|
+
runtimeManifest: manifest,
|
|
120
|
+
pluginVersion,
|
|
121
|
+
});
|
|
122
|
+
provenanceStatus = provenance.status;
|
|
123
|
+
sourceDigest = provenance.marker?.source_digest ?? null;
|
|
124
|
+
if (provenance.status !== "matched") {
|
|
125
|
+
return { envelope: envelope("version_mismatch", options, {
|
|
126
|
+
observedVersion,
|
|
127
|
+
source: "plugin_bundle",
|
|
128
|
+
pluginRoot,
|
|
129
|
+
pluginVersion,
|
|
130
|
+
canonicalVersion: definition.version,
|
|
131
|
+
runtimeDigest: observedDigest,
|
|
132
|
+
sourceDigest,
|
|
133
|
+
distributionProfile: "runtime-plugin",
|
|
134
|
+
evidencePlane: "loaded_session",
|
|
135
|
+
provenanceStatus,
|
|
136
|
+
reason: provenance.reason,
|
|
137
|
+
}) };
|
|
138
|
+
}
|
|
139
|
+
} else if (existsSync(join(pluginRoot, ".agdf-local-install.json"))) {
|
|
140
|
+
return { envelope: envelope("unavailable", options, {
|
|
141
|
+
observedVersion,
|
|
142
|
+
source: "plugin_bundle",
|
|
143
|
+
pluginRoot,
|
|
144
|
+
pluginVersion,
|
|
145
|
+
canonicalVersion: definition.version,
|
|
146
|
+
runtimeDigest: observedDigest,
|
|
147
|
+
distributionProfile: "runtime-plugin",
|
|
148
|
+
evidencePlane: "loaded_session",
|
|
149
|
+
provenanceStatus: "missing",
|
|
150
|
+
reason: "installation_provenance_missing",
|
|
151
|
+
}) };
|
|
152
|
+
} else {
|
|
153
|
+
const generatedRepository = inspectGeneratedRepositoryMarketplace(resolve(pluginRoot, "..", ".."));
|
|
154
|
+
if (pluginVersion !== definition.version
|
|
155
|
+
|| generatedRepository.status !== "matched"
|
|
156
|
+
|| canonicalPath(generatedRepository.pluginRoot) !== pluginRoot) {
|
|
157
|
+
return { envelope: envelope("unavailable", options, {
|
|
158
|
+
observedVersion,
|
|
159
|
+
source: "plugin_bundle",
|
|
160
|
+
pluginRoot,
|
|
161
|
+
pluginVersion,
|
|
162
|
+
canonicalVersion: definition.version,
|
|
163
|
+
runtimeDigest: observedDigest,
|
|
164
|
+
distributionProfile: "runtime-plugin",
|
|
165
|
+
evidencePlane: "installed_plugin_root",
|
|
166
|
+
provenanceStatus: "missing",
|
|
167
|
+
reason: "installation_provenance_missing",
|
|
168
|
+
}) };
|
|
169
|
+
}
|
|
170
|
+
evidencePlane = "generated_bundle";
|
|
97
171
|
}
|
|
98
172
|
return {
|
|
99
|
-
envelope: envelope("owned_version_matched", options, {
|
|
173
|
+
envelope: envelope("owned_version_matched", options, {
|
|
174
|
+
observedVersion,
|
|
175
|
+
source: "plugin_bundle",
|
|
176
|
+
pluginRoot,
|
|
177
|
+
pluginVersion,
|
|
178
|
+
canonicalVersion: definition.version,
|
|
179
|
+
runtimeDigest: observedDigest,
|
|
180
|
+
sourceDigest,
|
|
181
|
+
distributionProfile: "runtime-plugin",
|
|
182
|
+
evidencePlane,
|
|
183
|
+
provenanceStatus,
|
|
184
|
+
}),
|
|
100
185
|
executable: process.execPath,
|
|
101
186
|
prefixArgs: [entrypoint],
|
|
102
187
|
};
|