@raishin/vanguard-frontier-agentic 2.5.0 → 2.7.0

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.
Files changed (42) hide show
  1. package/.agents/tasks/task-dynamic-kiro-powers/2025-01-24-120000-review.md +92 -0
  2. package/.agents/tasks/task-dynamic-kiro-powers/context.json +22 -0
  3. package/.agents/tasks/task-dynamic-kiro-powers/features/FEAT-001.json +34 -0
  4. package/.agents/tasks/task-dynamic-kiro-powers/task.json +14 -0
  5. package/.agents/tasks/task-jekyll-docs-site/2025-07-17-review.md +118 -0
  6. package/.agents/tasks/task-jekyll-docs-site/context.json +30 -0
  7. package/.agents/tasks/task-jekyll-docs-site/features/FEAT-001.json +28 -0
  8. package/.agents/tasks/task-jekyll-docs-site/features/FEAT-002.json +44 -0
  9. package/.agents/tasks/task-jekyll-docs-site/task.json +14 -0
  10. package/.claude-plugin/marketplace.json +1 -1
  11. package/.claude-plugin/plugin.json +1 -1
  12. package/.cursor-plugin/plugin.json +1 -1
  13. package/.github/plugin/marketplace.json +1 -1
  14. package/README.md +2 -0
  15. package/catalog/asset-integrity.json +129 -29
  16. package/package.json +3 -1
  17. package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +3 -2
  18. package/plugins/vanguard-frontier-agentic/skills/vanguard-frontier-agentic-install/SKILL.md +37 -0
  19. package/powers/README.md +28 -10
  20. package/powers/vanguard-argocd/POWER.md +40 -0
  21. package/powers/vanguard-backstage/POWER.md +40 -0
  22. package/powers/vanguard-cert-manager/POWER.md +40 -0
  23. package/powers/vanguard-cilium/POWER.md +40 -0
  24. package/powers/vanguard-dotnet/POWER.md +41 -0
  25. package/powers/vanguard-falco/POWER.md +40 -0
  26. package/powers/vanguard-fluxcd/POWER.md +40 -0
  27. package/powers/vanguard-generic/POWER.md +40 -0
  28. package/powers/vanguard-hr/POWER.md +41 -0
  29. package/powers/vanguard-istio/POWER.md +40 -0
  30. package/powers/vanguard-kyverno/POWER.md +40 -0
  31. package/powers/vanguard-legal/POWER.md +41 -0
  32. package/powers/vanguard-marketing/POWER.md +41 -0
  33. package/powers/vanguard-multi-cloud/POWER.md +41 -0
  34. package/powers/vanguard-opentelemetry/POWER.md +40 -0
  35. package/powers/vanguard-prometheus/POWER.md +40 -0
  36. package/powers/vanguard-sigstore/POWER.md +40 -0
  37. package/scripts/export-marketplace-agents.mjs +26 -0
  38. package/scripts/generate-kiro-powers.mjs +360 -5
  39. package/scripts/install-codex-home.mjs +95 -0
  40. package/tests/test-codex-plugin-marketplace-install.test.mjs +132 -0
  41. package/tests/test-vfa-export-coverage.test.mjs +108 -0
  42. package/tests/validate-codex-marketplace.py +23 -1
@@ -212,6 +212,239 @@ const PROVIDERS = {
212
212
 
213
213
  const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
214
214
 
215
+ // --- Dynamic provider discovery and derivation ---
216
+
217
+ /** Special-case display name mappings for providers not in PROVIDERS. */
218
+ const DISPLAY_NAME_OVERRIDES = {
219
+ dotnet: ".NET",
220
+ hr: "HR",
221
+ fluxcd: "FluxCD",
222
+ argocd: "ArgoCD",
223
+ opentelemetry: "OpenTelemetry",
224
+ "cert-manager": "Cert-Manager",
225
+ "multi-cloud": "Multi-Cloud",
226
+ };
227
+
228
+ /** Pre-authored keyword sets for derived providers. */
229
+ const DERIVED_KEYWORDS = {
230
+ argocd: ["argocd", "gitops", "progressive-delivery", "application-sync"],
231
+ dotnet: ["dotnet", "csharp", "aspnet-core", "ef-core", "nuget"],
232
+ marketing: ["marketing-governance", "consent-compliance", "advertising-fairness", "email-authentication"],
233
+ hr: ["hr-governance", "employment-risk", "compensation-equity", "recruiting"],
234
+ legal: ["legal-risk", "contract-review", "privacy-compliance", "regulatory"],
235
+ generic: ["test-quality", "ci-pipeline", "helm-chart", "manifest-review"],
236
+ "multi-cloud": ["finops", "cloud-pricing", "cost-optimization", "reserved-instances"],
237
+ backstage: ["backstage", "scaffolder", "software-templates", "developer-portal"],
238
+ "cert-manager": ["cert-manager", "x509", "certificate-lifecycle", "pki"],
239
+ cilium: ["cilium", "network-policy", "ebpf", "cluster-mesh"],
240
+ falco: ["falco", "runtime-threat", "syscall-rules", "container-security"],
241
+ fluxcd: ["fluxcd", "gitops", "kustomization", "helm-release"],
242
+ istio: ["istio", "service-mesh", "ambient-mesh", "mtls"],
243
+ kyverno: ["kyverno", "admission-policy", "cluster-policy", "policy-enforcement"],
244
+ opentelemetry: ["opentelemetry", "otel-collector", "tracing", "observability-pipeline"],
245
+ prometheus: ["prometheus", "alertmanager", "metrics-cardinality", "scrape-config"],
246
+ sigstore: ["sigstore", "cosign", "supply-chain-integrity", "image-signing"],
247
+ };
248
+
249
+ /**
250
+ * Discover all unique providers from the catalog where at least one agent
251
+ * has 'kiro' in its harnesses array.
252
+ */
253
+ function discoverKiroProviders() {
254
+ const providers = new Set();
255
+ for (const entry of catalog) {
256
+ if (
257
+ entry.type === "agent" &&
258
+ Array.isArray(entry.harnesses) &&
259
+ entry.harnesses.includes("kiro")
260
+ ) {
261
+ providers.add(entry.provider);
262
+ }
263
+ }
264
+ return [...providers].sort();
265
+ }
266
+
267
+ /**
268
+ * Title-case a provider name, handling special cases.
269
+ */
270
+ function titleCaseProvider(provider) {
271
+ if (DISPLAY_NAME_OVERRIDES[provider]) return DISPLAY_NAME_OVERRIDES[provider];
272
+ return provider
273
+ .split("-")
274
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
275
+ .join("-");
276
+ }
277
+
278
+ /**
279
+ * Derive a topic summary from agent IDs for description generation.
280
+ */
281
+ function deriveTopics(entries) {
282
+ const topics = entries
283
+ .map((e) => e.id)
284
+ .filter((id) => !id.endsWith("-maestro-agent"))
285
+ .map((id) => {
286
+ // Strip provider prefix and -agent / -review-agent suffix
287
+ let topic = id
288
+ .replace(/-review-agent$/, "")
289
+ .replace(/-agent$/, "")
290
+ .replace(/-run-agent$/, "");
291
+ // Remove known provider prefixes
292
+ const prefixes = [
293
+ "dotnet-", "hr-", "legal-", "marketing-", "finops-",
294
+ "argocd-", "backstage-", "cert-manager-", "cilium-",
295
+ "falco-", "fluxcd-", "istio-", "kyverno-",
296
+ "opentelemetry-", "prometheus-", "sigstore-",
297
+ ];
298
+ for (const pfx of prefixes) {
299
+ if (topic.startsWith(pfx)) {
300
+ topic = topic.slice(pfx.length);
301
+ break;
302
+ }
303
+ }
304
+ return topic.replace(/-/g, " ");
305
+ })
306
+ .slice(0, 4);
307
+ return topics.join(", ");
308
+ }
309
+
310
+ /**
311
+ * Auto-generate steering content for a provider NOT in the hardcoded
312
+ * PROVIDERS object.
313
+ */
314
+ function deriveProviderConfig(provider, catalogEntries) {
315
+ const displayLabel = titleCaseProvider(provider);
316
+ const displayName = `Vanguard Frontier \u2014 ${displayLabel}`;
317
+
318
+ const entries = catalogEntries.filter(
319
+ (e) => e.type === "agent" && e.provider === provider,
320
+ );
321
+ const maestro = entries.find((e) => e.id.endsWith("-maestro-agent"));
322
+ const liveGuards = entries.filter((e) => /-live-/.test(e.id));
323
+
324
+ // Build description (max 3 sentences)
325
+ let description;
326
+ if (maestro && entries.length > 2) {
327
+ const topics = deriveTopics(entries);
328
+ description = `Curated ${displayLabel} agents for ${topics}. Routes via ${maestro.id} to specialist agents based on task scope. Static review only; no live mutations.`;
329
+ } else if (entries.length === 1) {
330
+ // Single agent, no maestro
331
+ const summary = entries[0].summary || "";
332
+ // Split into sentences - skip "Agent for <id>." prefix if present
333
+ const sentences = summary.split(/(?<=[.!?])\s/);
334
+ let useSentence = sentences[0] || "";
335
+ if (/^Agent for\s/i.test(useSentence) && sentences.length > 1) {
336
+ useSentence = sentences[1];
337
+ }
338
+ // Remove trailing period for reassembly
339
+ useSentence = useSentence.replace(/\.$/, "");
340
+ // Strip leading "Static review of" / "Review" prefix to avoid doubling
341
+ let core = useSentence
342
+ .replace(/^Static,?\s+evidence-gated\s+review\s+of\s+/i, "")
343
+ .replace(/^Static\s+review\s+of\s+/i, "")
344
+ .replace(/^Review\s+(a\s+)?/i, "");
345
+ // Truncate if too long, respecting word boundaries
346
+ if (core.length > 120) {
347
+ const truncated = core.substring(0, 117);
348
+ const lastSpace = truncated.lastIndexOf(" ");
349
+ core = (lastSpace > 0 ? truncated.substring(0, lastSpace) : truncated) + "...";
350
+ }
351
+ const sep = core.endsWith("...") ? " " : ". ";
352
+ description = `Reviews ${core.charAt(0).toLowerCase() + core.slice(1)}${sep}Static review only; no live mutations.`;
353
+ } else {
354
+ // Multiple agents, no maestro
355
+ const topics = deriveTopics(entries);
356
+ description = `Curated ${displayLabel} review agents covering ${topics}. Reference agents directly under agents/${provider}/. Static review only; no live mutations.`;
357
+ }
358
+
359
+ // Keywords
360
+ const keywords = DERIVED_KEYWORDS[provider] || [
361
+ provider,
362
+ "static-review",
363
+ "configuration-audit",
364
+ "best-practices",
365
+ ];
366
+
367
+ // Invariants
368
+ const invariants = [];
369
+ if (liveGuards.length > 0) {
370
+ invariants.push(
371
+ `Live-guard agents (${provider}-live-*) must never be auto-dispatched; require explicit approval and rollback plan.`,
372
+ );
373
+ }
374
+ if (maestro) {
375
+ invariants.push(
376
+ `Route all tasks through ${maestro.id} for proper classification and dispatch.`,
377
+ );
378
+ }
379
+ invariants.push(
380
+ "Static review only -- agents analyze configuration and provide findings without mutating live systems.",
381
+ );
382
+ // Add domain-specific invariants
383
+ if (provider === "dotnet") {
384
+ invariants.push("Review covers language runtime, frameworks, data access, testing, and supply-chain integrity.");
385
+ } else if (provider === "hr") {
386
+ invariants.push("All findings must respect employee privacy and data-minimization principles.");
387
+ } else if (provider === "legal") {
388
+ invariants.push("Agents provide risk-flagging only; output is not legal advice and does not create attorney-client privilege.");
389
+ } else if (provider === "marketing") {
390
+ invariants.push("Review covers consent, privacy, fairness, and regulatory compliance for marketing systems.");
391
+ } else if (provider === "multi-cloud") {
392
+ invariants.push("Cost recommendations are estimates based on public pricing; verify against actual billing before acting.");
393
+ } else if (provider === "generic") {
394
+ invariants.push("Agents are provider-agnostic and focus on CI, Helm, manifest, and test-quality patterns.");
395
+ } else if (provider === "argocd") {
396
+ invariants.push("Sync and rollout strategies must be validated against the target cluster GitOps workflow.");
397
+ } else if (provider === "backstage") {
398
+ invariants.push("Template parameters and scaffolder actions must be reviewed for injection and secret-exposure risks.");
399
+ } else if (provider === "cert-manager") {
400
+ invariants.push("Certificate renewal windows and issuer trust chains must be validated before any policy change.");
401
+ } else if (provider === "cilium") {
402
+ invariants.push("Network policies must be reviewed for unintended traffic blocking across namespaces and cluster-mesh endpoints.");
403
+ } else if (provider === "falco") {
404
+ invariants.push("Rule changes must be evaluated for false-positive rate impact on production alerting.");
405
+ } else if (provider === "fluxcd") {
406
+ invariants.push("Kustomization and HelmRelease reconciliation intervals must align with the GitOps change cadence.");
407
+ } else if (provider === "istio") {
408
+ invariants.push("Service mesh policies affect traffic routing cluster-wide; review blast radius before changes.");
409
+ } else if (provider === "kyverno") {
410
+ invariants.push("Cluster-scoped policies can reject legitimate workloads; validate against existing deployments before applying.");
411
+ } else if (provider === "opentelemetry") {
412
+ invariants.push("Collector pipeline changes affect observability for all instrumented services; review cardinality impact.");
413
+ } else if (provider === "prometheus") {
414
+ invariants.push("Alerting rule and scrape config changes affect monitoring coverage; review for metric-name collisions.");
415
+ } else if (provider === "sigstore") {
416
+ invariants.push("Supply-chain policy changes can block valid deployments; verify cosign keyless trust roots before enforcement.");
417
+ }
418
+
419
+ return { displayName, description, keywords, invariants };
420
+ }
421
+
422
+ /**
423
+ * Build a merged map combining hand-authored PROVIDERS with auto-derived
424
+ * entries for all kiro-enabled providers in the catalog.
425
+ */
426
+ function buildMergedProviders() {
427
+ const kiroProviders = discoverKiroProviders();
428
+ const merged = {};
429
+
430
+ for (const provider of kiroProviders) {
431
+ if (PROVIDERS[provider]) {
432
+ merged[provider] = PROVIDERS[provider];
433
+ } else {
434
+ merged[provider] = deriveProviderConfig(provider, catalog);
435
+ }
436
+ }
437
+
438
+ // Sort alphabetically for deterministic output
439
+ const sorted = {};
440
+ for (const key of Object.keys(merged).sort()) {
441
+ sorted[key] = merged[key];
442
+ }
443
+ return sorted;
444
+ }
445
+
446
+ const allProviders = buildMergedProviders();
447
+
215
448
  function summarize(provider) {
216
449
  const entries = catalog.filter(
217
450
  (e) => e.type === "agent" && e.provider === provider,
@@ -254,7 +487,9 @@ function renderPower(provider, cfg) {
254
487
 
255
488
  const adapterNote =
256
489
  kiroAvailable === total
257
- ? `All ${total} agents in this provider ship a Kiro adapter (\`harnesses/kiro-ide.agent.md\`, \`kiro-cli.agent.json\`).`
490
+ ? total === 1
491
+ ? `The single agent in this provider ships a Kiro adapter (\`harnesses/kiro-ide.agent.md\`, \`kiro-cli.agent.json\`).`
492
+ : `All ${total} agents in this provider ship a Kiro adapter (\`harnesses/kiro-ide.agent.md\`, \`kiro-cli.agent.json\`).`
258
493
  : kiroAvailable === 0
259
494
  ? `This provider's ${total} agents do not yet ship Kiro adapters — this Power supplies steering content only. Use \`npx vfa-export-agents --platform kiro --provider ${provider}\` from the npm package once Kiro adapters land.`
260
495
  : `${kiroAvailable} of ${total} agents in this provider ship a Kiro adapter; the rest provide steering context only.`;
@@ -273,7 +508,9 @@ function renderPower(provider, cfg) {
273
508
  "",
274
509
  maestroLine,
275
510
  "",
276
- "Use the maestro as the entry point: classify the task, then dispatch to one specialist or a parallel team of specialists. Never have the maestro itself execute a live mutation.",
511
+ maestro
512
+ ? "Use the maestro as the entry point: classify the task, then dispatch to one specialist or a parallel team of specialists. Never have the maestro itself execute a live mutation."
513
+ : "Reference agents directly from agents/" + provider + "/ without maestro-based routing.",
277
514
  "",
278
515
  "## Live-guard agents (gate_mode only)",
279
516
  "",
@@ -299,10 +536,114 @@ function renderPower(provider, cfg) {
299
536
  return frontmatter + body;
300
537
  }
301
538
 
539
+ function renderReadme() {
540
+ const providerKeys = Object.keys(allProviders);
541
+ const count = providerKeys.length;
542
+ const tree = providerKeys
543
+ .map((p, i) => {
544
+ const prefix = i < count - 1 ? "├──" : "└──";
545
+ return `${prefix} vanguard-${p}/POWER.md`;
546
+ })
547
+ .join("\n");
548
+
549
+ return `# \`powers/\` — Kiro Powers
550
+
551
+ This directory holds **${count} Kiro Powers** for \`vanguard-frontier-agentic\`, one
552
+ per cloud/platform/IaC provider. Each Power is a directory containing a
553
+ \`POWER.md\` file with strict-5 frontmatter and steering content.
554
+
555
+ ## What's in here
556
+
557
+ \`\`\`
558
+ powers/
559
+ ${tree}
560
+ \`\`\`
561
+
562
+ Each \`POWER.md\` declares:
563
+
564
+ - **Frontmatter (strict-5):** \`name\`, \`displayName\`, \`description\` (≤ 3
565
+ sentences), \`keywords\` (specific, non-broad), \`author\`. **No other fields
566
+ permitted** by Kiro spec.
567
+ - **Body steering:** when to engage, routing pattern (\`<provider>-maestro\`),
568
+ live-mutation discipline, provider-specific invariants (e.g. MLPS 2.0 for
569
+ Alibaba/Huawei, Enterprise Project vs IAM scope for Huawei, account-ID
570
+ /region confirmation for AWS).
571
+
572
+ ## How users install
573
+
574
+ Kiro Powers don't have a one-command marketplace install — the Powers panel
575
+ is per-Power directory add. Users clone the repo and add each Power they
576
+ need via the Kiro UI:
577
+
578
+ \`\`\`bash
579
+ # 1. Clone the repo
580
+ git clone https://github.com/Raishin/vanguard-frontier-agentic
581
+ cd vanguard-frontier-agentic
582
+ \`\`\`
583
+
584
+ \`\`\`text
585
+ 2. In Kiro:
586
+ Open the Powers panel → "Add Custom Power" → "Local Directory"
587
+ Paste the absolute path to the Power(s) you need:
588
+ /absolute/path/to/vanguard-frontier-agentic/powers/vanguard-aws
589
+ /absolute/path/to/vanguard-frontier-agentic/powers/vanguard-kubernetes
590
+ Repeat for each provider you work with.
591
+ \`\`\`
592
+
593
+ ## How to update
594
+
595
+ \`\`\`bash
596
+ # Regenerate the ${count} Powers from catalog/agents.json + per-provider config:
597
+ npm run kiro-powers:write
598
+
599
+ # Then verify everything is in sync:
600
+ npm run validate:kiro-powers
601
+ \`\`\`
602
+
603
+ The \`validate\` chain runs \`validate:kiro-powers\` automatically. The
604
+ validator enforces:
605
+
606
+ - strict-5 frontmatter (any extra field fails)
607
+ - lowercase kebab-case names
608
+ - name matches directory name
609
+ - description ≤ 3 sentences (decimal-aware — "MLPS 2.0" doesn't count as a
610
+ sentence break)
611
+ - non-empty keywords list, no broad terms (\`cloud\`, \`devops\`, \`code\`,
612
+ \`agent\`, \`ml\`, etc.) per Kiro's anti-false-activation guidance
613
+ - generator in sync (\`--check\`)
614
+
615
+ ## Schema references (official Kiro docs)
616
+
617
+ - **Kiro Powers repo:** <https://github.com/kirodotdev/powers>
618
+ - **POWER.md frontmatter spec:**
619
+ <https://github.com/kirodotdev/powers/blob/main/power-builder/POWER.md>
620
+ - **Interactive power builder:**
621
+ <https://github.com/kirodotdev/powers/blob/main/power-builder/steering/interactive.md>
622
+ - **Testing a power locally:**
623
+ <https://github.com/kirodotdev/powers/blob/main/power-builder/steering/testing.md>
624
+ - **Kiro IDE:** <https://kiro.dev/>
625
+
626
+ ## Design notes
627
+
628
+ - **One Power per provider, not one mega-Power** — Kiro docs warn that
629
+ broad keywords trigger false activations across unrelated tasks. One
630
+ narrowly-scoped Power per provider keeps activation precise:
631
+ \`vanguard-alibaba\` activates on Alibaba Cloud work only; \`vanguard-aws\`
632
+ never activates on Azure questions.
633
+ - **Hetzner and Contabo Powers exist** even though their agents don't yet
634
+ ship Kiro adapter files (their \`harnesses: [codex, claude-code]\`). Powers
635
+ are steering-first — the steering content stands alone. When their Kiro
636
+ adapter files land, the Powers will gain agent-routing as well.
637
+ - **No \`version\`, \`repository\`, \`license\`, or \`tags\`** — Kiro spec
638
+ explicitly forbids these fields in frontmatter. The validator fails on
639
+ any extra field.
640
+ `;
641
+ }
642
+
302
643
  const errors = [];
303
644
  const written = [];
304
645
 
305
- for (const [provider, cfg] of Object.entries(PROVIDERS)) {
646
+ for (const [provider, cfg] of Object.entries(allProviders)) {
306
647
  const dir = join(powersRoot, `vanguard-${provider}`);
307
648
  const file = join(dir, "POWER.md");
308
649
  const next = renderPower(provider, cfg);
@@ -322,15 +663,29 @@ for (const [provider, cfg] of Object.entries(PROVIDERS)) {
322
663
  }
323
664
  }
324
665
 
666
+ // Generate/check README.md
667
+ const readmePath = join(powersRoot, "README.md");
668
+ const nextReadme = renderReadme();
669
+ if (check) {
670
+ if (!existsSync(readmePath)) {
671
+ errors.push(`${relative(repoRoot, readmePath)} is missing`);
672
+ } else if (readFileSync(readmePath, "utf8") !== nextReadme) {
673
+ errors.push(`${relative(repoRoot, readmePath)} is stale; run npm run kiro-powers:write`);
674
+ }
675
+ } else {
676
+ writeFileSync(readmePath, nextReadme);
677
+ written.push(relative(repoRoot, readmePath));
678
+ }
679
+
325
680
  if (check) {
326
681
  if (errors.length) {
327
682
  errors.forEach((e) => console.error(`ERROR: ${e}`));
328
683
  process.exit(1);
329
684
  }
330
685
  console.log(
331
- `OK: ${Object.keys(PROVIDERS).length} Kiro Powers are in sync`,
686
+ `OK: ${Object.keys(allProviders).length} Kiro Powers are in sync`,
332
687
  );
333
688
  } else {
334
- console.log(`OK: wrote ${written.length} Kiro Powers`);
689
+ console.log(`OK: wrote ${written.length} Kiro Powers (+ README.md)`);
335
690
  written.forEach((f) => console.log(` ${f}`));
336
691
  }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reliable two-stage installer for Vanguard Frontier Agentic on Codex.
4
+ *
5
+ * Stage 1: register/refresh the Codex plugin marketplace.
6
+ * Stage 2: export all Codex-capable agents and companion skills into a Codex home.
7
+ */
8
+
9
+ import { spawnSync } from "node:child_process";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const exporter = path.join(repoRoot, "scripts", "export-marketplace-agents.mjs");
16
+
17
+ const args = process.argv.slice(2);
18
+ const opts = {
19
+ marketplace: "Raishin/vanguard-frontier-agentic",
20
+ repo: os.homedir(),
21
+ force: true,
22
+ skipMarketplace: false,
23
+ dryRun: false,
24
+ };
25
+
26
+ function usage(exitCode = 0) {
27
+ const out = exitCode === 0 ? console.log : console.error;
28
+ out(`Usage: node scripts/install-codex-home.mjs [options]\n\nOptions:\n --marketplace <source> Codex marketplace source (default: Raishin/vanguard-frontier-agentic)\n --repo <path> Target home/repo path whose .codex folder receives agents/skills (default: $HOME)\n --dry-run Do not write agents/skills; pass --dry-run to exporter\n --skip-marketplace Skip codex plugin marketplace add/upgrade\n --no-force Do not pass --force to exporter\n -h, --help Show this help\n`);
29
+ process.exit(exitCode);
30
+ }
31
+
32
+ for (let i = 0; i < args.length; i++) {
33
+ const arg = args[i];
34
+ if (arg === "-h" || arg === "--help") usage(0);
35
+ if (arg === "--marketplace") {
36
+ const val = args[++i];
37
+ if (!val || val.startsWith("-")) { console.error("--marketplace requires a non-flag value"); usage(1); }
38
+ opts.marketplace = val;
39
+ } else if (arg === "--repo") {
40
+ const val = args[++i];
41
+ if (!val || val.startsWith("-")) { console.error("--repo requires a non-flag value"); usage(1); }
42
+ opts.repo = val;
43
+ }
44
+ else if (arg === "--dry-run") opts.dryRun = true;
45
+ else if (arg === "--skip-marketplace") opts.skipMarketplace = true;
46
+ else if (arg === "--no-force") opts.force = false;
47
+ else {
48
+ console.error(`Unknown option: ${arg}`);
49
+ usage(1);
50
+ }
51
+ }
52
+
53
+ if (!opts.marketplace) {
54
+ console.error("--marketplace cannot be empty");
55
+ process.exit(1);
56
+ }
57
+ if (!opts.repo) {
58
+ console.error("--repo cannot be empty");
59
+ process.exit(1);
60
+ }
61
+
62
+ function run(label, command, commandArgs, options = {}) {
63
+ console.error(`\n[${label}] ${command} ${commandArgs.join(" ")}`);
64
+ const result = spawnSync(command, commandArgs, {
65
+ cwd: repoRoot,
66
+ stdio: "inherit",
67
+ ...options,
68
+ });
69
+ if (result.error) {
70
+ console.error(`[${label}] failed to start: ${result.error.message}`);
71
+ process.exit(1);
72
+ }
73
+ if (result.status !== 0) {
74
+ console.error(`[${label}] exited ${result.status}`);
75
+ process.exit(result.status ?? 1);
76
+ }
77
+ }
78
+
79
+ if (!opts.skipMarketplace) {
80
+ run("marketplace-add", "codex", ["plugin", "marketplace", "add", opts.marketplace]);
81
+ const marketplaceName = opts.marketplace
82
+ .split("/").pop()
83
+ ?.replace(/\.git$/, "")
84
+ ?.replace(/@.+$/, "");
85
+ if (marketplaceName) {
86
+ run("marketplace-upgrade", "codex", ["plugin", "marketplace", "upgrade", marketplaceName]);
87
+ }
88
+ }
89
+
90
+ const exportArgs = ["--platform", "codex", "--all", "--repo", opts.repo];
91
+ if (opts.force) exportArgs.push("--force");
92
+ if (opts.dryRun) exportArgs.push("--dry-run");
93
+ run("export-agents-and-skills", process.execPath, [exporter, ...exportArgs]);
94
+
95
+ console.error("\nOK: two-stage Codex install completed");
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Optional E2E check for the real Codex marketplace-add command.
4
+ *
5
+ * This test is intentionally opt-in because it runs the installed `codex` CLI
6
+ * and may hit the network when CODEX_PLUGIN_MARKETPLACE_SOURCE is a GitHub
7
+ * shorthand. It uses an isolated CODEX_HOME and never writes to ~/.codex.
8
+ *
9
+ * What it proves:
10
+ * - `codex plugin marketplace add <source>` exits successfully.
11
+ * - Codex tracks the marketplace in the isolated CODEX_HOME/config.toml.
12
+ * - Codex materializes the marketplace source under CODEX_HOME/.tmp/marketplaces.
13
+ * - The materialized marketplace contains the repo's Codex marketplace and plugin manifests.
14
+ *
15
+ * What it does NOT prove:
16
+ * - It does not prove a plugin was installed into CODEX_HOME/plugins/cache/... .
17
+ * OpenAI docs describe that as plugin installation through a marketplace;
18
+ * the current CLI command under test is marketplace-add, not plugin-install.
19
+ *
20
+ * Run:
21
+ * RUN_CODEX_PLUGIN_MARKETPLACE_E2E=1 node tests/test-codex-plugin-marketplace-install.test.mjs
22
+ *
23
+ * Optional override:
24
+ * CODEX_PLUGIN_MARKETPLACE_SOURCE=Raishin/vanguard-frontier-agentic@main \
25
+ * RUN_CODEX_PLUGIN_MARKETPLACE_E2E=1 node tests/test-codex-plugin-marketplace-install.test.mjs
26
+ *
27
+ * Strict cache assertion, expected to fail for marketplace-add-only on the
28
+ * current Codex CLI unless a separate plugin install path populates cache:
29
+ * EXPECT_CODEX_PLUGIN_CACHE=1 RUN_CODEX_PLUGIN_MARKETPLACE_E2E=1 \
30
+ * node tests/test-codex-plugin-marketplace-install.test.mjs
31
+ */
32
+
33
+ import { spawnSync } from "node:child_process";
34
+ import fs from "node:fs";
35
+ import os from "node:os";
36
+ import path from "node:path";
37
+
38
+ const enabled = process.env.RUN_CODEX_PLUGIN_MARKETPLACE_E2E === "1";
39
+ if (!enabled) {
40
+ console.log("SKIP codex marketplace E2E; set RUN_CODEX_PLUGIN_MARKETPLACE_E2E=1 to run it");
41
+ process.exit(0);
42
+ }
43
+
44
+ const source = process.env.CODEX_PLUGIN_MARKETPLACE_SOURCE || "Raishin/vanguard-frontier-agentic";
45
+ const marketplaceName = process.env.CODEX_PLUGIN_MARKETPLACE_NAME || "vanguard-frontier-agentic";
46
+ const expectPluginCache = process.env.EXPECT_CODEX_PLUGIN_CACHE === "1";
47
+ const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), "vfa-codex-home-"));
48
+
49
+ let failures = 0;
50
+ const ok = (msg) => console.log(`OK ${msg}`);
51
+ const fail = (msg) => {
52
+ console.log(`FAIL ${msg}`);
53
+ failures += 1;
54
+ };
55
+
56
+ function exists(rel) {
57
+ return fs.existsSync(path.join(codexHome, rel));
58
+ }
59
+
60
+ try {
61
+ const result = spawnSync(
62
+ "codex",
63
+ ["plugin", "marketplace", "add", source],
64
+ {
65
+ encoding: "utf8",
66
+ env: {
67
+ ...process.env,
68
+ CODEX_HOME: codexHome,
69
+ },
70
+ timeout: 120000,
71
+ },
72
+ );
73
+
74
+ if (result.error?.code === "ENOENT") {
75
+ console.log("SKIP codex marketplace E2E; `codex` executable not found on PATH");
76
+ process.exit(0);
77
+ }
78
+ if (result.signal === "SIGTERM") {
79
+ fail("codex marketplace add timed out after 120s");
80
+ }
81
+ if (result.status === 0) {
82
+ ok(`codex plugin marketplace add ${source} exits 0`);
83
+ } else {
84
+ fail(`codex marketplace add exited ${result.status}; stderr=${(result.stderr || "").slice(0, 1000)}`);
85
+ }
86
+
87
+ const configPath = path.join(codexHome, "config.toml");
88
+ const config = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
89
+ if (config.includes(`[marketplaces.${marketplaceName}]`)) {
90
+ ok(`config.toml tracks marketplace ${marketplaceName}`);
91
+ } else {
92
+ fail(`config.toml missing [marketplaces.${marketplaceName}]`);
93
+ }
94
+
95
+ const installedRoot = path.join(codexHome, ".tmp", "marketplaces", marketplaceName);
96
+ if (fs.existsSync(installedRoot)) {
97
+ ok(`marketplace source materialized at ${installedRoot}`);
98
+ } else {
99
+ fail(`marketplace source missing at ${installedRoot}`);
100
+ }
101
+
102
+ const requiredFiles = [
103
+ `.tmp/marketplaces/${marketplaceName}/.agents/plugins/marketplace.json`,
104
+ `.tmp/marketplaces/${marketplaceName}/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json`,
105
+ `.tmp/marketplaces/${marketplaceName}/plugins/cross-platform-agent-template/.codex-plugin/plugin.json`,
106
+ ];
107
+ for (const rel of requiredFiles) {
108
+ if (exists(rel)) ok(`${rel} exists`);
109
+ else fail(`${rel} missing`);
110
+ }
111
+
112
+ const cacheRoot = path.join(codexHome, "plugins", "cache", marketplaceName);
113
+ if (fs.existsSync(cacheRoot)) {
114
+ ok(`plugin cache exists at ${cacheRoot}`);
115
+ } else if (expectPluginCache) {
116
+ fail(`plugin cache missing at ${cacheRoot}`);
117
+ } else {
118
+ console.log(`INFO plugin cache not created by marketplace-add alone: ${cacheRoot}`);
119
+ }
120
+ } finally {
121
+ if (process.env.KEEP_CODEX_MARKETPLACE_E2E_HOME !== "1") {
122
+ fs.rmSync(codexHome, { recursive: true, force: true });
123
+ } else {
124
+ console.log(`INFO kept isolated CODEX_HOME at ${codexHome}`);
125
+ }
126
+ }
127
+
128
+ if (failures > 0) {
129
+ console.error(`\n${failures} check(s) failed`);
130
+ process.exit(1);
131
+ }
132
+ console.log("\nOK: codex marketplace add E2E checks passed");