create-agdf 0.9.0 → 0.9.3

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.
@@ -0,0 +1,419 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import process from "node:process";
6
+ import { generatedRoot, pluginDefinition } from "../cli/runtime-context.js";
7
+
8
+ const contractModules = ["gate-transition.md", "interaction.md", "modes.md", "quality.md", "context-graph.md", "control-scaffold.md", "closeout.md"];
9
+ const openCodeSkillNames = pluginDefinition.skillSet.map((skill) => pluginDefinition.opencode.skillPrefix + skill.slug);
10
+ const globalOpenCodeSkillNames = pluginDefinition.skillSet.map((skill) => pluginDefinition.opencode.globalSkillPrefix + skill.slug);
11
+ const globalOpenCodeSkillOwnershipMarker = "<!-- AGDF-GLOBAL-SKILL: ";
12
+ const globalOpenCodeInstructionsOwnershipMarker = "<!-- AGDF-GLOBAL-INSTRUCTIONS -->";
13
+ const globalOpenCodeRuntimeContractOwnershipMarker = "<!-- AGDF-GLOBAL-RUNTIME-CONTRACT -->";
14
+ const testNpmCliPath = process.env.NODE_ENV === "test" ? process.env.AGDF_TEST_NPM_CLI_PATH || "" : "";
15
+ const npmCommand = testNpmCliPath ? process.execPath : process.platform === "win32" ? process.execPath : "npm";
16
+ const npmPrefixArgs = testNpmCliPath ? [testNpmCliPath] : process.platform === "win32" ? [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")] : [];
17
+
18
+ export function defaultOpenCodeConfigDir() {
19
+ return process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
20
+ }
21
+
22
+ export function installOpenCodeGlobalPlugin(configDir) {
23
+ assertGlobalOpenCodeSurfaceWritable(configDir);
24
+ const previousPackage = resolveOpenCodePackage(configDir);
25
+ const configPath = join(configDir, "opencode.json");
26
+ let config = {};
27
+
28
+ if (existsSync(configPath)) {
29
+ try {
30
+ config = JSON.parse(readFileSync(configPath, "utf8"));
31
+ } catch {
32
+ throw new Error(`Refusing to update unreadable OpenCode config: ${configPath}`);
33
+ }
34
+ }
35
+
36
+ if (config.plugin !== undefined && !Array.isArray(config.plugin)) {
37
+ throw new Error(`Refusing to update OpenCode config with non-array plugin field: ${configPath}`);
38
+ }
39
+
40
+ const plugins = [...(config.plugin ?? [])];
41
+ const alreadyInstalled = plugins.includes(pluginDefinition.opencode.npmPackage);
42
+ if (!alreadyInstalled) {
43
+ plugins.push(pluginDefinition.opencode.npmPackage);
44
+ }
45
+
46
+ const nextConfig = {
47
+ "$schema": config.$schema ?? "https://opencode.ai/config.json",
48
+ ...config,
49
+ plugin: plugins,
50
+ };
51
+
52
+ if (nextConfig.instructions !== undefined && !Array.isArray(nextConfig.instructions)) {
53
+ throw new Error(`Refusing to update OpenCode config with non-array instructions field: ${configPath}`);
54
+ }
55
+ nextConfig.instructions = [...(nextConfig.instructions ?? [])];
56
+ if (!nextConfig.instructions.includes("AGDF.md")) nextConfig.instructions.push("AGDF.md");
57
+
58
+ if (nextConfig.permission === undefined) {
59
+ nextConfig.permission = { question: "allow", skill: { "agdf-*": "allow" } };
60
+ } else if (nextConfig.permission && typeof nextConfig.permission === "object" && !Array.isArray(nextConfig.permission)) {
61
+ nextConfig.permission = { ...nextConfig.permission };
62
+ if (nextConfig.permission.question === undefined) nextConfig.permission.question = "allow";
63
+ if (nextConfig.permission.skill === undefined) {
64
+ nextConfig.permission.skill = { "agdf-*": "allow" };
65
+ } else if (nextConfig.permission.skill && typeof nextConfig.permission.skill === "object" && !Array.isArray(nextConfig.permission.skill)) {
66
+ nextConfig.permission.skill = { ...nextConfig.permission.skill };
67
+ if (nextConfig.permission.skill["agdf-*"] === undefined) nextConfig.permission.skill["agdf-*"] = "allow";
68
+ }
69
+ }
70
+
71
+ mkdirSync(configDir, { recursive: true });
72
+ try {
73
+ const packageSpecifier = `${pluginDefinition.opencode.npmPackage}@${pluginDefinition.version}`;
74
+ execFileSync(npmCommand, [...npmPrefixArgs, "install", "--silent", "--save-prod", "--save-exact", packageSpecifier], {
75
+ cwd: configDir,
76
+ stdio: "pipe",
77
+ });
78
+ } catch (error) {
79
+ throw new Error(`Failed to install ${pluginDefinition.opencode.npmPackage} into the OpenCode config directory: ${(error.stderr || error.message).toString().trim()}`);
80
+ }
81
+ writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
82
+ const installedPackage = resolveOpenCodePackage(configDir);
83
+
84
+ return {
85
+ configPath,
86
+ added: !alreadyInstalled,
87
+ transition: openCodePackageTransition(previousPackage, installedPackage),
88
+ };
89
+ }
90
+
91
+ function globalOpenCodeConfigPaths(configDir) {
92
+ return {
93
+ instructions: join(configDir, pluginDefinition.opencode.instructionsFileName),
94
+ runtimeContract: join(configDir, pluginDefinition.opencode.runtimeContractFileName),
95
+ contracts: join(configDir, "contracts"),
96
+ skills: join(configDir, "skills"),
97
+ };
98
+ }
99
+
100
+ function globalOpenCodeOwnershipMarkerIsValid(content, marker, placement) {
101
+ const lines = content.split(/\r?\n/);
102
+ if (placement === "first-line") return lines[0] === marker;
103
+ const frontmatterEnd = lines.findIndex((line, index) => index > 0 && line === "---");
104
+ return frontmatterEnd >= 0 && lines[frontmatterEnd + 1] === marker;
105
+ }
106
+
107
+ function assertGlobalOpenCodeFileWritable(path, marker, placement) {
108
+ if (!existsSync(path)) return;
109
+ const existing = readFileSync(path, "utf8");
110
+ if (!globalOpenCodeOwnershipMarkerIsValid(existing, marker, placement)) {
111
+ throw new Error(`Refusing to overwrite unowned global OpenCode file: ${path}`);
112
+ }
113
+ }
114
+
115
+ function assertGlobalOpenCodeSurfaceWritable(configDir) {
116
+ const paths = globalOpenCodeConfigPaths(configDir);
117
+ assertGlobalOpenCodeFileWritable(paths.instructions, globalOpenCodeInstructionsOwnershipMarker, "first-line");
118
+ assertGlobalOpenCodeFileWritable(paths.runtimeContract, globalOpenCodeRuntimeContractOwnershipMarker, "first-line");
119
+ for (const moduleName of contractModules) {
120
+ assertGlobalOpenCodeFileWritable(
121
+ join(paths.contracts, moduleName),
122
+ globalOpenCodeRuntimeContractOwnershipMarker,
123
+ "first-line",
124
+ );
125
+ }
126
+ for (const skillName of globalOpenCodeSkillNames) {
127
+ assertGlobalOpenCodeFileWritable(
128
+ join(paths.skills, skillName, "SKILL.md"),
129
+ `${globalOpenCodeSkillOwnershipMarker}${skillName} -->`,
130
+ "after-frontmatter",
131
+ );
132
+ }
133
+ }
134
+
135
+ function writeOwnedGlobalOpenCodeFile(path, content, marker, placement) {
136
+ if (existsSync(path)) {
137
+ assertGlobalOpenCodeFileWritable(path, marker, placement);
138
+ }
139
+ mkdirSync(dirname(path), { recursive: true });
140
+ writeFileSync(path, content, "utf8");
141
+ }
142
+
143
+ function globalOpenCodeBoundary() {
144
+ return [
145
+ "## Global OpenCode Surface Boundary",
146
+ "",
147
+ "This skill is globally discoverable, but global plugin or skill presence is not repository governance activation.",
148
+ "Before applying AGDF gates, later artefacts or implementation guidance, inspect the current repository for `.opencode/AGDF.md`, `.opencode/skills/agdf-gate-check/SKILL.md` and `.agdf/control/`.",
149
+ "If the repository surface is absent, stop and direct the user to `npx --yes @agdf/cli@latest opencode-repo` in this repository.",
150
+ "When the repository surface exists, its local instructions, native skills and `.agdf/control/` state are authoritative.",
151
+ "",
152
+ ].join("\n");
153
+ }
154
+
155
+ function toGlobalOpenCodeContent(content) {
156
+ let next = content;
157
+ for (const skill of pluginDefinition.skillSet) {
158
+ const localName = `${pluginDefinition.opencode.skillPrefix}${skill.slug}`;
159
+ const globalName = `${pluginDefinition.opencode.globalSkillPrefix}${skill.slug}`;
160
+ next = next.replaceAll(localName, globalName);
161
+ }
162
+ return next;
163
+ }
164
+
165
+ export function installOpenCodeGlobalSurface(configDir) {
166
+ const paths = globalOpenCodeConfigPaths(configDir);
167
+ const generatedOpenCodeRoot = join(generatedRoot, ".opencode");
168
+ const generatedInstructions = readFileSync(join(generatedOpenCodeRoot, pluginDefinition.opencode.instructionsFileName), "utf8");
169
+ const generatedRuntimeContract = readFileSync(join(generatedOpenCodeRoot, pluginDefinition.opencode.runtimeContractFileName), "utf8");
170
+ const globalInstructions = [
171
+ globalOpenCodeInstructionsOwnershipMarker,
172
+ "# AGDF Global OpenCode instructions",
173
+ "",
174
+ "AGDF native skills are globally discoverable through OpenCode.",
175
+ "",
176
+ globalOpenCodeBoundary(),
177
+ "The repository-local `.opencode/AGDF.md`, `.opencode/skills/` and `.agdf/control/` files remain the source of truth for active governance.",
178
+ "",
179
+ toGlobalOpenCodeContent(generatedInstructions),
180
+ ].join("\n");
181
+ const globalRuntimeContract = `${globalOpenCodeRuntimeContractOwnershipMarker}\n${generatedRuntimeContract}`;
182
+
183
+ writeOwnedGlobalOpenCodeFile(paths.instructions, globalInstructions, globalOpenCodeInstructionsOwnershipMarker, "first-line");
184
+ writeOwnedGlobalOpenCodeFile(paths.runtimeContract, globalRuntimeContract, globalOpenCodeRuntimeContractOwnershipMarker, "first-line");
185
+ for (const moduleName of contractModules) {
186
+ const generatedModule = readFileSync(join(generatedOpenCodeRoot, "contracts", moduleName), "utf8");
187
+ writeOwnedGlobalOpenCodeFile(
188
+ join(paths.contracts, moduleName),
189
+ `${globalOpenCodeRuntimeContractOwnershipMarker}\n${generatedModule}`,
190
+ globalOpenCodeRuntimeContractOwnershipMarker,
191
+ "first-line",
192
+ );
193
+ }
194
+
195
+ for (let index = 0; index < openCodeSkillNames.length; index += 1) {
196
+ const sourceName = openCodeSkillNames[index];
197
+ const skillName = globalOpenCodeSkillNames[index];
198
+ const sourcePath = join(generatedOpenCodeRoot, "skills", sourceName, "SKILL.md");
199
+ const sourceContent = readFileSync(sourcePath, "utf8");
200
+ const marker = `${globalOpenCodeSkillOwnershipMarker}${skillName} -->`;
201
+ const content = toGlobalOpenCodeContent(sourceContent).replace(/^(---[\s\S]*?\n---\n)/, `$1${marker}\n\n${globalOpenCodeBoundary()}`);
202
+ writeOwnedGlobalOpenCodeFile(join(paths.skills, skillName, "SKILL.md"), content, marker, "after-frontmatter");
203
+ }
204
+
205
+ return {
206
+ instructions: paths.instructions,
207
+ runtimeContract: paths.runtimeContract,
208
+ contracts: paths.contracts,
209
+ skills: paths.skills,
210
+ };
211
+ }
212
+
213
+ function readOpenCodeConfig(configPath) {
214
+ if (!existsSync(configPath)) return { exists: false, parseError: "", config: {} };
215
+ try {
216
+ return { exists: true, parseError: "", config: JSON.parse(readFileSync(configPath, "utf8")) };
217
+ } catch (error) {
218
+ return { exists: true, parseError: error.message, config: {} };
219
+ }
220
+ }
221
+
222
+ function isOpenCodeRepositorySurfacePresent(targetDir) {
223
+ return existsSync(join(targetDir, ".opencode", pluginDefinition.opencode.instructionsFileName))
224
+ && existsSync(join(targetDir, ".opencode", "skills", `${pluginDefinition.opencode.skillPrefix}gate-check`, "SKILL.md"));
225
+ }
226
+
227
+ function resolveOpenCodePackage(configDir) {
228
+ const packageName = pluginDefinition.opencode.npmPackage;
229
+ try {
230
+ const resolvedPath = execFileSync(process.execPath, ["-e", `process.stdout.write(require.resolve(${JSON.stringify(packageName)}))`], {
231
+ cwd: configDir,
232
+ encoding: "utf8",
233
+ stdio: "pipe",
234
+ });
235
+ let installedVersion = "";
236
+ try {
237
+ const packageManifest = JSON.parse(readFileSync(join(dirname(resolvedPath), "package.json"), "utf8"));
238
+ installedVersion = typeof packageManifest.version === "string" && packageManifest.version.trim() ? packageManifest.version.trim() : "";
239
+ } catch {
240
+ installedVersion = "";
241
+ }
242
+ return {
243
+ loadable: true,
244
+ path: resolvedPath,
245
+ installed_version: installedVersion,
246
+ error: "",
247
+ };
248
+ } catch (error) {
249
+ return {
250
+ loadable: false,
251
+ path: "",
252
+ installed_version: "",
253
+ error: (error.stderr || error.message || "package not resolvable").toString().trim(),
254
+ };
255
+ }
256
+ }
257
+
258
+ function openCodePackageVersionStatus(packageState) {
259
+ if (!packageState.loadable) return "unloadable";
260
+ if (!packageState.installed_version) return "unknown";
261
+ return packageState.installed_version === pluginDefinition.version ? "current" : "outdated";
262
+ }
263
+
264
+ function openCodePackageTransition(previousPackage, installedPackage) {
265
+ const previousVersion = previousPackage.loadable ? previousPackage.installed_version : "";
266
+ const installedVersion = installedPackage.loadable ? installedPackage.installed_version : "";
267
+ if (!installedVersion || (previousPackage.loadable && !previousVersion)) {
268
+ return {
269
+ previous_version: previousVersion,
270
+ installed_version: installedVersion,
271
+ status: "unknown",
272
+ };
273
+ }
274
+ if (!previousPackage.loadable) {
275
+ return {
276
+ previous_version: "",
277
+ installed_version: installedVersion,
278
+ status: "installed",
279
+ };
280
+ }
281
+ return {
282
+ previous_version: previousVersion,
283
+ installed_version: installedVersion,
284
+ status: previousVersion === installedVersion ? "unchanged" : "updated",
285
+ };
286
+ }
287
+
288
+ function evaluateGlobalOpenCodeSurface(configDir) {
289
+ const paths = globalOpenCodeConfigPaths(configDir);
290
+ const skillCount = globalOpenCodeSkillNames.filter((skillName) => existsSync(join(paths.skills, skillName, "SKILL.md"))).length;
291
+ const contractCount = contractModules.filter((moduleName) => existsSync(join(paths.contracts, moduleName))).length;
292
+ return {
293
+ path: paths.skills,
294
+ instructions: paths.instructions,
295
+ runtime_contract: paths.runtimeContract,
296
+ contracts: paths.contracts,
297
+ expected_skill_count: globalOpenCodeSkillNames.length,
298
+ skill_count: skillCount,
299
+ expected_contract_count: contractModules.length,
300
+ contract_count: contractCount,
301
+ present: existsSync(paths.skills),
302
+ complete: existsSync(paths.instructions)
303
+ && existsSync(paths.runtimeContract)
304
+ && contractCount === contractModules.length
305
+ && skillCount === openCodeSkillNames.length,
306
+ };
307
+ }
308
+
309
+ export function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeConfigDir(), transition = null) {
310
+ const configPath = join(configDir, "opencode.json");
311
+ const configState = readOpenCodeConfig(configPath);
312
+ const plugins = Array.isArray(configState.config.plugin) ? configState.config.plugin : [];
313
+ const globalConfigured = plugins.includes(pluginDefinition.opencode.npmPackage);
314
+ const packageState = resolveOpenCodePackage(configDir);
315
+ const packageVersionStatus = openCodePackageVersionStatus(packageState);
316
+ const globalNativeSurface = evaluateGlobalOpenCodeSurface(configDir);
317
+ const sessionSignals = {
318
+ active: process.env.AGDF_PLUGIN_ACTIVE === "1",
319
+ version: process.env.AGDF_PLUGIN_VERSION || "",
320
+ control_dir: process.env.AGDF_CONTROL_DIR || "",
321
+ repository_surface: process.env.AGDF_OPENCODE_REPOSITORY_SURFACE === "1",
322
+ };
323
+ const repositorySurface = isOpenCodeRepositorySurfacePresent(targetDir);
324
+ const gateCheckSkillPath = join(targetDir, ".opencode", "skills", `${pluginDefinition.opencode.skillPrefix}gate-check`, "SKILL.md");
325
+
326
+ const findings = [];
327
+ if (!configState.exists) findings.push("OpenCode global config not found.");
328
+ if (configState.parseError) findings.push(`OpenCode global config is not valid JSON: ${configState.parseError}`);
329
+ if (!globalConfigured) findings.push(`OpenCode global config does not include ${pluginDefinition.opencode.npmPackage}.`);
330
+ if (!packageState.loadable) findings.push(`${pluginDefinition.opencode.npmPackage} is not loadable from the OpenCode config directory.`);
331
+ if (packageVersionStatus === "outdated") findings.push(`${pluginDefinition.opencode.npmPackage} version ${packageState.installed_version} is outdated; expected ${pluginDefinition.version}.`);
332
+ if (packageVersionStatus === "unknown" && packageState.loadable) findings.push(`${pluginDefinition.opencode.npmPackage} is loadable but its installed version is unknown.`);
333
+ if (!globalNativeSurface.complete) {
334
+ findings.push(
335
+ `Global OpenCode native surface is incomplete (${globalNativeSurface.skill_count}/${globalNativeSurface.expected_skill_count} skills; ${globalNativeSurface.contract_count}/${globalNativeSurface.expected_contract_count} contract modules).`,
336
+ );
337
+ }
338
+ if (!sessionSignals.active) findings.push("No active AGDF OpenCode session signal is visible in this process.");
339
+ if (!repositorySurface) findings.push("Current repository does not contain the AGDF OpenCode surface.");
340
+
341
+ return {
342
+ schema_version: "1",
343
+ status: globalConfigured && packageState.loadable ? "configured" : "not_configured",
344
+ global_config: {
345
+ path: configPath,
346
+ exists: configState.exists,
347
+ parse_error: configState.parseError,
348
+ plugin_configured: globalConfigured,
349
+ },
350
+ package: {
351
+ name: pluginDefinition.opencode.npmPackage,
352
+ loadable: packageState.loadable,
353
+ resolved_path: packageState.path,
354
+ installed_version: packageState.installed_version || null,
355
+ expected_version: pluginDefinition.version,
356
+ version_status: packageVersionStatus,
357
+ ...(transition ? { transition } : {}),
358
+ error: packageState.error,
359
+ },
360
+ global_native_surface: globalNativeSurface,
361
+ session: sessionSignals,
362
+ repository_surface: {
363
+ path: targetDir,
364
+ present: repositorySurface,
365
+ instructions: join(targetDir, ".opencode", pluginDefinition.opencode.instructionsFileName),
366
+ gate_check_agent: gateCheckSkillPath,
367
+ gate_check_skill: gateCheckSkillPath,
368
+ },
369
+ visible_entrypoint: repositorySurface ? `${pluginDefinition.opencode.skillPrefix}gate-check (native skill)` : "none until opencode-repo is installed for this repository",
370
+ findings,
371
+ next_step: packageVersionStatus !== "current"
372
+ ? "Run npx --yes @agdf/cli@latest opencode to install or repair the OpenCode package version."
373
+ : !globalNativeSurface.complete
374
+ ? "Run npx --yes @agdf/cli@latest opencode to install or repair the global native OpenCode skill surface."
375
+ : repositorySurface
376
+ ? `Restart OpenCode if needed, then load ${pluginDefinition.opencode.skillPrefix}gate-check through the native skill tool for new build/change intent.`
377
+ : "Run npx --yes @agdf/cli@latest opencode-repo in repositories where AGDF governance should be active and reviewable.",
378
+ };
379
+ }
380
+
381
+ export function printOpenCodeStatus(report, json, io = console) {
382
+ if (json) {
383
+ io.log(JSON.stringify(report, null, 2));
384
+ return;
385
+ }
386
+
387
+ io.log("AGDF OpenCode status");
388
+ io.log(`Status: ${report.status}`);
389
+ io.log(`Global config: ${report.global_config.plugin_configured ? "configured" : "missing"} (${report.global_config.path})`);
390
+ if (report.global_config.parse_error) io.log(`Config parse error: ${report.global_config.parse_error}`);
391
+ io.log(`Package loadable: ${report.package.loadable ? "yes" : "no"}`);
392
+ if (report.package.resolved_path) io.log(`Package path: ${report.package.resolved_path}`);
393
+ io.log(`Package version: ${report.package.installed_version || "unknown"}`);
394
+ io.log(`Expected version: ${report.package.expected_version}`);
395
+ io.log(`Version status: ${report.package.version_status}`);
396
+ if (report.package.transition?.status === "updated") {
397
+ io.log(`Version transition: ${report.package.transition.previous_version} -> ${report.package.transition.installed_version}`);
398
+ } else if (report.package.transition?.status === "installed") {
399
+ io.log(`Version transition: new install (${report.package.transition.installed_version || "unknown"})`);
400
+ } else if (report.package.transition?.status === "unchanged") {
401
+ io.log(`Version transition: unchanged (${report.package.transition.installed_version})`);
402
+ } else if (report.package.transition?.status === "unknown") {
403
+ io.log("Version transition: unknown");
404
+ }
405
+ io.log(`Global native skills: ${report.global_native_surface.complete ? "complete" : "incomplete"} (${report.global_native_surface.skill_count}/${report.global_native_surface.expected_skill_count})`);
406
+ io.log(`Global skill path: ${report.global_native_surface.path}`);
407
+ io.log(`Session active signal: ${report.session.active ? "yes" : "no"}`);
408
+ if (report.session.version) io.log(`Session plugin version: ${report.session.version}`);
409
+ if (report.session.control_dir) io.log(`Session control dir: ${report.session.control_dir}`);
410
+ io.log(`Repository surface: ${report.repository_surface.present ? "present" : "missing"}`);
411
+ io.log(`Visible entrypoint: ${report.visible_entrypoint}`);
412
+ io.log(`Next step: ${report.next_step}`);
413
+
414
+ if (report.findings.length > 0) {
415
+ io.log("");
416
+ io.log("Findings:");
417
+ for (const finding of report.findings) io.log(`- ${finding}`);
418
+ }
419
+ }
@@ -0,0 +1,70 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { pluginDefinition } from "../cli/runtime-context.js";
3
+
4
+ export function installCodexGlobalPlugin({ exec = execFileSync, io = console } = {}) {
5
+ const expectedVersion = pluginDefinition.version;
6
+
7
+ try {
8
+ exec("codex", ["plugin", "marketplace", "add", "arndtgold/ai-native-governance-delivery-framework"], { stdio: "inherit" });
9
+ exec("codex", ["plugin", "marketplace", "upgrade", "agdf"], { stdio: "inherit" });
10
+ exec("codex", ["plugin", "add", "agdf", "--marketplace", "agdf"], { stdio: "inherit" });
11
+ const listOutput = exec("codex", ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
12
+ const installedVersion = pluginVersionFromList(listOutput, "agdf@agdf");
13
+ if (installedVersion !== expectedVersion) {
14
+ throw new Error(versionMismatchMessage("Codex", "agdf@agdf", expectedVersion, installedVersion, "codex plugin marketplace upgrade agdf && codex plugin add agdf --marketplace agdf"));
15
+ }
16
+ io.log(`AGDF Codex plugin version verified: ${installedVersion}.`);
17
+ } catch (error) {
18
+ if (error.message?.startsWith("AGDF Codex plugin version mismatch")) throw error;
19
+ throw new Error(`Failed to install the AGDF Codex plugin. Make sure the Codex CLI is installed and available on PATH, then rerun this command. ${commandErrorText(error)}`.trim());
20
+ }
21
+ }
22
+
23
+ export function installClaudeGlobalPlugin({ exec = execFileSync, io = console } = {}) {
24
+ const expectedVersion = pluginDefinition.version;
25
+
26
+ try {
27
+ exec("claude", ["plugin", "marketplace", "add", "arndtgold/ai-native-governance-delivery-framework"], { stdio: "inherit" });
28
+ exec("claude", ["plugin", "marketplace", "update", "agdf"], { stdio: "inherit" });
29
+ const beforeList = exec("claude", ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
30
+ const alreadyInstalled = pluginListHasPlugin(beforeList, "agdf@agdf");
31
+ exec("claude", ["plugin", alreadyInstalled ? "update" : "install", "agdf@agdf"], { stdio: "inherit" });
32
+ const afterList = exec("claude", ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
33
+ const installedVersion = pluginVersionFromList(afterList, "agdf@agdf");
34
+ if (installedVersion) {
35
+ if (installedVersion !== expectedVersion) {
36
+ throw new Error(versionMismatchMessage("Claude Code", "agdf@agdf", expectedVersion, installedVersion, "claude plugin marketplace update agdf && claude plugin update agdf@agdf"));
37
+ }
38
+ io.log(`AGDF Claude Code plugin version verified: ${installedVersion}.`);
39
+ } else {
40
+ io.log("AGDF Claude Code plugin installed or updated. Claude Code did not expose a plugin version in `claude plugin list`; verify with `claude plugin list` after restart if needed.");
41
+ }
42
+ } catch (error) {
43
+ if (error.message?.startsWith("AGDF Claude Code plugin version mismatch")) throw error;
44
+ throw new Error(`Failed to install the AGDF Claude Code plugin. Make sure the Claude Code CLI is installed and available on PATH, then rerun this command. ${commandErrorText(error)}`.trim());
45
+ }
46
+ }
47
+
48
+ function commandErrorText(error) {
49
+ return (error.stderr || error.stdout || error.message || "").toString().trim();
50
+ }
51
+
52
+ export function pluginListHasPlugin(output, pluginId) {
53
+ return output
54
+ .split(/\r?\n/)
55
+ .some((line) => line.includes(pluginId));
56
+ }
57
+
58
+ export function pluginVersionFromList(output, pluginId) {
59
+ const escapedPluginId = pluginId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
60
+ const line = output
61
+ .split(/\r?\n/)
62
+ .find((entry) => new RegExp(`(^|\\s)${escapedPluginId}(\\s|$)`).test(entry));
63
+ if (!line) return "";
64
+ const versionMatch = line.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
65
+ return versionMatch?.[1] ?? "";
66
+ }
67
+
68
+ function versionMismatchMessage(surface, pluginId, expectedVersion, installedVersion, correctiveCommand) {
69
+ return `AGDF ${surface} plugin version mismatch for ${pluginId}: expected ${expectedVersion}, observed ${installedVersion || "unknown"}. Refresh with: ${correctiveCommand}`;
70
+ }