agentboot 0.1.0 → 0.3.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 (78) hide show
  1. package/README.md +9 -8
  2. package/agentboot.config.json +4 -1
  3. package/package.json +2 -2
  4. package/scripts/cli.ts +465 -18
  5. package/scripts/compile.ts +724 -75
  6. package/scripts/dev-sync.ts +1 -1
  7. package/scripts/lib/config.ts +259 -1
  8. package/scripts/lib/frontmatter.ts +3 -1
  9. package/scripts/validate.ts +12 -7
  10. package/website/docusaurus.config.ts +117 -0
  11. package/website/package-lock.json +18448 -0
  12. package/website/package.json +47 -0
  13. package/website/sidebars.ts +53 -0
  14. package/website/src/css/custom.css +23 -0
  15. package/website/src/pages/index.module.css +23 -0
  16. package/website/src/pages/index.tsx +125 -0
  17. package/website/static/.nojekyll +0 -0
  18. package/website/static/CNAME +1 -0
  19. package/website/static/img/favicon.ico +0 -0
  20. package/website/static/img/logo.svg +1 -0
  21. package/.github/ISSUE_TEMPLATE/persona-request.md +0 -62
  22. package/.github/ISSUE_TEMPLATE/quality-feedback.md +0 -67
  23. package/.github/workflows/cla.yml +0 -25
  24. package/.github/workflows/validate.yml +0 -49
  25. package/.idea/agentboot.iml +0 -9
  26. package/.idea/misc.xml +0 -6
  27. package/.idea/modules.xml +0 -8
  28. package/.idea/vcs.xml +0 -6
  29. package/CLAUDE.md +0 -230
  30. package/CONTRIBUTING.md +0 -168
  31. package/PERSONAS.md +0 -156
  32. package/core/instructions/baseline.instructions.md +0 -133
  33. package/core/instructions/security.instructions.md +0 -186
  34. package/core/personas/code-reviewer/SKILL.md +0 -175
  35. package/core/personas/security-reviewer/SKILL.md +0 -233
  36. package/core/personas/test-data-expert/SKILL.md +0 -234
  37. package/core/personas/test-generator/SKILL.md +0 -262
  38. package/core/traits/audit-trail.md +0 -182
  39. package/core/traits/confidence-signaling.md +0 -172
  40. package/core/traits/critical-thinking.md +0 -129
  41. package/core/traits/schema-awareness.md +0 -132
  42. package/core/traits/source-citation.md +0 -174
  43. package/core/traits/structured-output.md +0 -199
  44. package/docs/ci-cd-automation.md +0 -548
  45. package/docs/claude-code-reference/README.md +0 -21
  46. package/docs/claude-code-reference/agentboot-coverage.md +0 -484
  47. package/docs/claude-code-reference/feature-inventory.md +0 -906
  48. package/docs/cli-commands-audit.md +0 -112
  49. package/docs/cli-design.md +0 -924
  50. package/docs/concepts.md +0 -1117
  51. package/docs/config-schema-audit.md +0 -121
  52. package/docs/configuration.md +0 -645
  53. package/docs/delivery-methods.md +0 -758
  54. package/docs/developer-onboarding.md +0 -342
  55. package/docs/extending.md +0 -448
  56. package/docs/getting-started.md +0 -298
  57. package/docs/knowledge-layer.md +0 -464
  58. package/docs/marketplace.md +0 -822
  59. package/docs/org-connection.md +0 -570
  60. package/docs/plans/architecture.md +0 -2429
  61. package/docs/plans/design.md +0 -2018
  62. package/docs/plans/prd.md +0 -1862
  63. package/docs/plans/stack-rank.md +0 -261
  64. package/docs/plans/technical-spec.md +0 -2755
  65. package/docs/privacy-and-safety.md +0 -807
  66. package/docs/prompt-optimization.md +0 -1071
  67. package/docs/test-plan.md +0 -972
  68. package/docs/third-party-ecosystem.md +0 -496
  69. package/domains/compliance-template/README.md +0 -173
  70. package/domains/compliance-template/traits/compliance-aware.md +0 -228
  71. package/examples/enterprise/agentboot.config.json +0 -184
  72. package/examples/minimal/agentboot.config.json +0 -46
  73. package/tests/REGRESSION-PLAN.md +0 -705
  74. package/tests/TEST-PLAN.md +0 -111
  75. package/tests/cli.test.ts +0 -705
  76. package/tests/pipeline.test.ts +0 -608
  77. package/tests/validate.test.ts +0 -278
  78. package/tsconfig.json +0 -62
package/scripts/cli.ts CHANGED
@@ -27,7 +27,7 @@ import path from "node:path";
27
27
  import fs from "node:fs";
28
28
  import chalk from "chalk";
29
29
  import { createHash } from "node:crypto";
30
- import { loadConfig } from "./lib/config.js";
30
+ import { loadConfig, type MarketplaceManifest, type MarketplaceEntry } from "./lib/config.js";
31
31
 
32
32
  // ---------------------------------------------------------------------------
33
33
  // Paths
@@ -93,6 +93,20 @@ function runScript({ script, args, verbose, quiet }: RunOptions): never {
93
93
  // Helpers
94
94
  // ---------------------------------------------------------------------------
95
95
 
96
+ /** Recursively copy a directory tree. */
97
+ function copyDirRecursive(src: string, dest: string): void {
98
+ fs.mkdirSync(dest, { recursive: true });
99
+ for (const entry of fs.readdirSync(src)) {
100
+ const srcPath = path.join(src, entry);
101
+ const destPath = path.join(dest, entry);
102
+ if (fs.statSync(srcPath).isDirectory()) {
103
+ copyDirRecursive(srcPath, destPath);
104
+ } else {
105
+ fs.copyFileSync(srcPath, destPath);
106
+ }
107
+ }
108
+ }
109
+
96
110
  /** Collect global flags that should be forwarded to scripts. */
97
111
  function collectGlobalArgs(opts: { config?: string }): string[] {
98
112
  const args: string[] = [];
@@ -140,7 +154,7 @@ program
140
154
  program
141
155
  .command("validate")
142
156
  .description("Run pre-build validation checks")
143
- .option("--strict", "treat warnings as errors")
157
+ .option("-s, --strict", "treat warnings as errors")
144
158
  .action((opts, cmd) => {
145
159
  const globalOpts = cmd.optsWithGlobals();
146
160
  const args = collectGlobalArgs({ config: globalOpts.config });
@@ -163,7 +177,7 @@ program
163
177
  .command("sync")
164
178
  .description("Distribute compiled output to target repositories")
165
179
  .option("--repos-file <path>", "path to repos.json")
166
- .option("--dry-run", "preview changes without writing")
180
+ .option("-d, --dry-run", "preview changes without writing")
167
181
  .action((opts, cmd) => {
168
182
  const globalOpts = cmd.optsWithGlobals();
169
183
  const args = collectGlobalArgs({ config: globalOpts.config });
@@ -200,10 +214,10 @@ program
200
214
  });
201
215
  });
202
216
 
203
- // ---- full-build -----------------------------------------------------------
217
+ // ---- dev-build -----------------------------------------------------------
204
218
 
205
219
  program
206
- .command("full-build")
220
+ .command("dev-build")
207
221
  .description("Run clean → validate → build → dev-sync pipeline")
208
222
  .action((_opts, cmd) => {
209
223
  const globalOpts = cmd.optsWithGlobals();
@@ -224,6 +238,10 @@ program
224
238
  ["tsx", path.join(SCRIPTS_DIR, "validate.ts"), ...baseArgs],
225
239
  { cwd: ROOT, stdio: quiet ? ["inherit", "ignore", "pipe"] : "inherit" },
226
240
  );
241
+ if (valResult.error) {
242
+ console.error(`Validation failed to start: ${valResult.error.message}`);
243
+ process.exit(1);
244
+ }
227
245
  if (valResult.status !== 0) {
228
246
  console.error("Validation failed.");
229
247
  process.exit(valResult.status ?? 1);
@@ -236,6 +254,10 @@ program
236
254
  ["tsx", path.join(SCRIPTS_DIR, "compile.ts"), ...baseArgs],
237
255
  { cwd: ROOT, stdio: quiet ? ["inherit", "ignore", "pipe"] : "inherit" },
238
256
  );
257
+ if (buildResult.error) {
258
+ console.error(`Build failed to start: ${buildResult.error.message}`);
259
+ process.exit(1);
260
+ }
239
261
  if (buildResult.status !== 0) {
240
262
  console.error("Build failed.");
241
263
  process.exit(buildResult.status ?? 1);
@@ -248,12 +270,17 @@ program
248
270
  ["tsx", path.join(SCRIPTS_DIR, "dev-sync.ts"), ...baseArgs],
249
271
  { cwd: ROOT, stdio: quiet ? ["inherit", "ignore", "pipe"] : "inherit" },
250
272
  );
273
+ if (syncResult.error) {
274
+ console.error(`Dev-sync failed to start: ${syncResult.error.message}`);
275
+ process.exit(1);
276
+ }
251
277
  if (syncResult.status !== 0) {
252
278
  console.error("Dev-sync failed.");
253
279
  process.exit(syncResult.status ?? 1);
254
280
  }
255
281
 
256
- if (!quiet) console.log("✓ full-build complete");
282
+ if (!quiet) console.log("✓ dev-build complete");
283
+ process.exit(0);
257
284
  });
258
285
 
259
286
  // ---- setup (AB-33) --------------------------------------------------------
@@ -293,7 +320,7 @@ program
293
320
  }
294
321
  } catch { /* no git, use default */ }
295
322
 
296
- console.log(chalk.cyan(` Detected org: ${orgName}`));
323
+ console.log(chalk.cyan(` Detected org: ${orgName}`) + chalk.gray(" (edit agentboot.config.json to change)"));
297
324
 
298
325
  // Scaffold config
299
326
  const configContent = JSON.stringify({
@@ -346,13 +373,13 @@ program
346
373
 
347
374
  program
348
375
  .command("add")
349
- .description("Scaffold a new persona, trait, or gotcha")
350
- .argument("<type>", "what to add: persona, trait, gotcha")
376
+ .description("Scaffold a new persona, trait, gotcha, domain, or hook")
377
+ .argument("<type>", "what to add: persona, trait, gotcha, domain, hook")
351
378
  .argument("<name>", "name for the new item (lowercase-with-hyphens)")
352
379
  .action((type: string, name: string) => {
353
380
  // Validate name format
354
- if (!/^[a-z][a-z0-9-]*$/.test(name)) {
355
- console.error(chalk.red(`Name must be lowercase alphanumeric with hyphens: got '${name}'`));
381
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
382
+ console.error(chalk.red(`Name must be 1-64 lowercase alphanumeric chars with hyphens: got '${name}'`));
356
383
  process.exit(1);
357
384
  }
358
385
 
@@ -509,8 +536,122 @@ paths:
509
536
  console.log(chalk.gray(` core/gotchas/${name}.md\n`));
510
537
  console.log(chalk.gray(` Next: Edit the paths: frontmatter and add your rules.\n`));
511
538
 
539
+ } else if (type === "domain") {
540
+ // AB-46/53: Domain layer scaffolding
541
+ const domainDir = path.join(cwd, "domains", name);
542
+ if (fs.existsSync(domainDir)) {
543
+ console.error(chalk.red(`Domain '${name}' already exists at domains/${name}/`));
544
+ process.exit(1);
545
+ }
546
+
547
+ fs.mkdirSync(path.join(domainDir, "traits"), { recursive: true });
548
+ fs.mkdirSync(path.join(domainDir, "personas"), { recursive: true });
549
+ fs.mkdirSync(path.join(domainDir, "instructions"), { recursive: true });
550
+
551
+ const domainManifest = JSON.stringify({
552
+ name,
553
+ version: "1.0.0",
554
+ description: `TODO — ${name} domain layer`,
555
+ traits: [],
556
+ personas: [],
557
+ instructions: [],
558
+ requires_core_version: ">=0.2.0",
559
+ }, null, 2);
560
+
561
+ fs.writeFileSync(path.join(domainDir, "agentboot.domain.json"), domainManifest + "\n", "utf-8");
562
+
563
+ const readmeMd = `# ${name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")} Domain
564
+
565
+ ## Purpose
566
+
567
+ <!-- Describe what this domain layer adds: compliance regime, industry standards, etc. -->
568
+
569
+ ## Activation
570
+
571
+ Add to \`agentboot.config.json\`:
572
+ \`\`\`jsonc
573
+ {
574
+ "domains": ["./domains/${name}"]
575
+ }
576
+ \`\`\`
577
+
578
+ ## Contents
579
+
580
+ - \`traits/\` — domain-specific behavioral traits
581
+ - \`personas/\` — domain-specific personas
582
+ - \`instructions/\` — domain-level always-on instructions
583
+ `;
584
+
585
+ fs.writeFileSync(path.join(domainDir, "README.md"), readmeMd, "utf-8");
586
+
587
+ console.log(chalk.bold(`\n${chalk.green("✓")} Created domain: ${name}\n`));
588
+ console.log(chalk.gray(` domains/${name}/`));
589
+ console.log(chalk.gray(` ├── agentboot.domain.json`));
590
+ console.log(chalk.gray(` ├── README.md`));
591
+ console.log(chalk.gray(` ├── traits/`));
592
+ console.log(chalk.gray(` ├── personas/`));
593
+ console.log(chalk.gray(` └── instructions/\n`));
594
+ console.log(chalk.gray(` Next: Add domain to config: "domains": ["./domains/${name}"]`));
595
+ console.log(chalk.gray(` Then: agentboot validate && agentboot build\n`));
596
+
597
+ } else if (type === "hook") {
598
+ // AB-46: Compliance hook scaffolding
599
+ const hooksDir = path.join(cwd, "hooks");
600
+ const hookPath = path.join(hooksDir, `${name}.sh`);
601
+ if (fs.existsSync(hookPath)) {
602
+ console.error(chalk.red(`Hook '${name}' already exists at hooks/${name}.sh`));
603
+ process.exit(1);
604
+ }
605
+
606
+ if (!fs.existsSync(hooksDir)) {
607
+ fs.mkdirSync(hooksDir, { recursive: true });
608
+ }
609
+
610
+ const hookScript = `#!/bin/bash
611
+ # AgentBoot compliance hook: ${name}
612
+ # Generated by \`agentboot add hook ${name}\`
613
+ #
614
+ # Hook events: PreToolUse, PostToolUse, Notification, Stop,
615
+ # SubagentStart, SubagentStop, UserPromptSubmit, SessionEnd
616
+ #
617
+ # Input: JSON on stdin with hook_event_name, agent_type, tool_name, etc.
618
+ # Output: exit 0 = pass, exit 2 = block (for PreToolUse/UserPromptSubmit)
619
+ #
620
+ # To register this hook, add to agentboot.config.json:
621
+ # "claude": {
622
+ # "hooks": {
623
+ # "<EventName>": [{
624
+ # "matcher": "",
625
+ # "hooks": [{ "type": "command", "command": "hooks/${name}.sh" }]
626
+ # }]
627
+ # }
628
+ # }
629
+
630
+ INPUT=$(cat)
631
+ EVENT_NAME=$(echo "$INPUT" | jq -r '.hook_event_name // empty')
632
+
633
+ # TODO: Add your compliance logic here
634
+ # Example: block a tool if a condition is met
635
+ # if [ "$EVENT_NAME" = "PreToolUse" ]; then
636
+ # TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
637
+ # if [ "$TOOL" = "Bash" ]; then
638
+ # echo '{"decision":"block","reason":"Bash tool is restricted by policy"}' >&2
639
+ # exit 2
640
+ # fi
641
+ # fi
642
+
643
+ exit 0
644
+ `;
645
+
646
+ fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
647
+
648
+ console.log(chalk.bold(`\n${chalk.green("✓")} Created hook: ${name}\n`));
649
+ console.log(chalk.gray(` hooks/${name}.sh\n`));
650
+ console.log(chalk.gray(` Next: Edit the hook script to add your compliance logic.`));
651
+ console.log(chalk.gray(` Then: Register in agentboot.config.json under claude.hooks\n`));
652
+
512
653
  } else {
513
- console.error(chalk.red(`Unknown type: '${type}'. Use: persona, trait, gotcha`));
654
+ console.error(chalk.red(`Unknown type: '${type}'. Use: persona, trait, gotcha, domain, hook`));
514
655
  process.exit(1);
515
656
  }
516
657
  });
@@ -636,7 +777,13 @@ program
636
777
  process.exit(1);
637
778
  }
638
779
 
639
- const config = loadConfig(configPath);
780
+ let config;
781
+ try {
782
+ config = loadConfig(configPath);
783
+ } catch (e: unknown) {
784
+ console.error(chalk.red(`Failed to parse config: ${e instanceof Error ? e.message : String(e)}`));
785
+ process.exit(1);
786
+ }
640
787
  const pkgPath = path.join(ROOT, "package.json");
641
788
  const version = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version : "unknown";
642
789
 
@@ -734,7 +881,13 @@ program
734
881
  process.exit(1);
735
882
  }
736
883
 
737
- const config = loadConfig(configPath);
884
+ let config;
885
+ try {
886
+ config = loadConfig(configPath);
887
+ } catch (e: unknown) {
888
+ console.error(chalk.red(`Failed to parse config: ${e instanceof Error ? e.message : String(e)}`));
889
+ process.exit(1);
890
+ }
738
891
  const isJson = opts.format === "json";
739
892
  if (!isJson) console.log(chalk.bold("\nAgentBoot — lint\n"));
740
893
 
@@ -752,6 +905,7 @@ program
752
905
 
753
906
  const personasDir = path.join(cwd, "core", "personas");
754
907
  const enabledPersonas = config.personas?.enabled ?? [];
908
+ const enabledTraits = config.traits?.enabled ?? [];
755
909
  const tokenBudget = config.output?.tokenBudget?.warnAt ?? 8000;
756
910
 
757
911
  // Vague language patterns
@@ -861,7 +1015,6 @@ program
861
1015
 
862
1016
  // Check for unused trait
863
1017
  const traitName = file.replace(/\.md$/, "");
864
- const enabledTraits = config.traits?.enabled ?? [];
865
1018
  if (enabledTraits.length > 0 && !enabledTraits.includes(traitName)) {
866
1019
  findings.push({ rule: "unused-trait", severity: "info", file: `core/traits/${file}`, message: `Trait not in traits.enabled list` });
867
1020
  }
@@ -921,7 +1074,7 @@ program
921
1074
  .command("uninstall")
922
1075
  .description("Remove AgentBoot managed files from a repository")
923
1076
  .option("--repo <path>", "target repository path")
924
- .option("--dry-run", "preview what would be removed")
1077
+ .option("-d, --dry-run", "preview what would be removed")
925
1078
  .action((opts) => {
926
1079
  const targetRepo = opts.repo ? path.resolve(opts.repo) : process.cwd();
927
1080
  const dryRun = opts.dryRun ?? false;
@@ -1023,9 +1176,12 @@ program
1023
1176
  .description("View configuration (read-only)")
1024
1177
  .argument("[key]", "config key (e.g., personas.enabled)")
1025
1178
  .argument("[value]", "not yet supported")
1026
- .action((key?: string, value?: string) => {
1179
+ .action((key: string | undefined, value: string | undefined, _opts, cmd) => {
1180
+ const globalOpts = cmd.optsWithGlobals();
1027
1181
  const cwd = process.cwd();
1028
- const configPath = path.join(cwd, "agentboot.config.json");
1182
+ const configPath = globalOpts.config
1183
+ ? path.resolve(globalOpts.config)
1184
+ : path.join(cwd, "agentboot.config.json");
1029
1185
 
1030
1186
  if (!fs.existsSync(configPath)) {
1031
1187
  console.error(chalk.red("No agentboot.config.json found."));
@@ -1062,6 +1218,297 @@ program
1062
1218
  process.exit(1);
1063
1219
  });
1064
1220
 
1221
+ // ---- export (AB-40) -------------------------------------------------------
1222
+
1223
+ program
1224
+ .command("export")
1225
+ .description("Export compiled output in a specific format")
1226
+ .option("--format <fmt>", "export format: plugin, marketplace, managed", "plugin")
1227
+ .option("--output <dir>", "output directory")
1228
+ .action((opts, cmd) => {
1229
+ const globalOpts = cmd.optsWithGlobals();
1230
+ const cwd = process.cwd();
1231
+ const configPath = globalOpts.config
1232
+ ? path.resolve(globalOpts.config)
1233
+ : path.join(cwd, "agentboot.config.json");
1234
+
1235
+ if (!fs.existsSync(configPath)) {
1236
+ console.error(chalk.red("No agentboot.config.json found. Run `agentboot setup`."));
1237
+ process.exit(1);
1238
+ }
1239
+
1240
+ let config;
1241
+ try {
1242
+ config = loadConfig(configPath);
1243
+ } catch (e: unknown) {
1244
+ console.error(chalk.red(`Failed to parse config: ${e instanceof Error ? e.message : String(e)}`));
1245
+ process.exit(1);
1246
+ }
1247
+
1248
+ const distPath = path.resolve(cwd, config.output?.distPath ?? "./dist");
1249
+ const format = opts.format;
1250
+
1251
+ console.log(chalk.bold(`\nAgentBoot — export (${format})\n`));
1252
+
1253
+ if (format === "plugin") {
1254
+ const pluginDir = path.join(distPath, "plugin");
1255
+ const pluginJson = path.join(pluginDir, "plugin.json");
1256
+
1257
+ if (!fs.existsSync(pluginJson)) {
1258
+ console.error(chalk.red("Plugin output not found. Run `agentboot build` first."));
1259
+ console.error(chalk.gray("Ensure 'plugin' is in personas.outputFormats or build includes claude format."));
1260
+ process.exit(1);
1261
+ }
1262
+
1263
+ const outputDir = opts.output
1264
+ ? path.resolve(opts.output)
1265
+ : path.join(cwd, ".claude-plugin");
1266
+
1267
+ // Safety: only delete existing dir if it's within cwd or contains plugin.json
1268
+ if (fs.existsSync(outputDir)) {
1269
+ const resolvedCwd = path.resolve(cwd);
1270
+ const isSafe = outputDir.startsWith(resolvedCwd + path.sep)
1271
+ || outputDir === resolvedCwd
1272
+ || fs.existsSync(path.join(outputDir, "plugin.json"));
1273
+ if (!isSafe) {
1274
+ console.error(chalk.red(` Refusing to delete ${outputDir} — not within project directory.`));
1275
+ console.error(chalk.gray(" Use a path within your project or an empty directory."));
1276
+ process.exit(1);
1277
+ }
1278
+ fs.rmSync(outputDir, { recursive: true, force: true });
1279
+ }
1280
+ fs.mkdirSync(outputDir, { recursive: true });
1281
+
1282
+ copyDirRecursive(pluginDir, outputDir);
1283
+
1284
+ // Count files
1285
+ let fileCount = 0;
1286
+ function countFiles(dir: string): void {
1287
+ for (const entry of fs.readdirSync(dir)) {
1288
+ const full = path.join(dir, entry);
1289
+ if (fs.statSync(full).isDirectory()) countFiles(full);
1290
+ else fileCount++;
1291
+ }
1292
+ }
1293
+ countFiles(outputDir);
1294
+
1295
+ console.log(chalk.green(` ✓ Exported plugin to ${path.relative(cwd, outputDir)}/`));
1296
+ console.log(chalk.gray(` ${fileCount} files (plugin.json + agents, skills, traits, hooks, rules)`));
1297
+ console.log(chalk.gray(`\n Next: agentboot publish\n`));
1298
+
1299
+ } else if (format === "managed") {
1300
+ const managedDir = path.join(distPath, "managed");
1301
+
1302
+ if (!fs.existsSync(managedDir)) {
1303
+ console.error(chalk.red("Managed settings not found. Enable managed.enabled in config and rebuild."));
1304
+ process.exit(1);
1305
+ }
1306
+
1307
+ const outputDir = opts.output
1308
+ ? path.resolve(opts.output)
1309
+ : path.join(cwd, "managed-output");
1310
+
1311
+ fs.mkdirSync(outputDir, { recursive: true });
1312
+ for (const entry of fs.readdirSync(managedDir)) {
1313
+ const srcPath = path.join(managedDir, entry);
1314
+ if (fs.statSync(srcPath).isFile()) {
1315
+ fs.copyFileSync(srcPath, path.join(outputDir, entry));
1316
+ }
1317
+ }
1318
+
1319
+ console.log(chalk.green(` ✓ Exported managed settings to ${path.relative(cwd, outputDir)}/`));
1320
+ console.log(chalk.gray(`\n Deploy via your MDM platform (Jamf, Intune, etc.)\n`));
1321
+
1322
+ } else if (format === "marketplace") {
1323
+ // Export marketplace.json scaffold
1324
+ const outputDir = opts.output ? path.resolve(opts.output) : cwd;
1325
+ const marketplacePath = path.join(outputDir, "marketplace.json");
1326
+
1327
+ if (fs.existsSync(marketplacePath)) {
1328
+ console.log(chalk.yellow(` marketplace.json already exists at ${marketplacePath}`));
1329
+ process.exit(0);
1330
+ }
1331
+
1332
+ const marketplace: MarketplaceManifest = {
1333
+ $schema: "https://agentboot.dev/schema/marketplace/v1",
1334
+ name: `${config.org}-personas`,
1335
+ description: `Agentic personas marketplace for ${config.orgDisplayName ?? config.org}`,
1336
+ maintainer: config.orgDisplayName ?? config.org,
1337
+ url: "",
1338
+ entries: [],
1339
+ };
1340
+
1341
+ fs.writeFileSync(marketplacePath, JSON.stringify(marketplace, null, 2) + "\n", "utf-8");
1342
+ console.log(chalk.green(` ✓ Created marketplace.json`));
1343
+ console.log(chalk.gray(`\n Next: agentboot publish to add entries\n`));
1344
+
1345
+ } else {
1346
+ console.error(chalk.red(`Unknown export format: '${format}'. Use: plugin, marketplace, managed`));
1347
+ process.exit(1);
1348
+ }
1349
+ });
1350
+
1351
+ // ---- publish (AB-41) ------------------------------------------------------
1352
+
1353
+ program
1354
+ .command("publish")
1355
+ .description("Publish compiled plugin to marketplace")
1356
+ .option("--marketplace <path>", "path to marketplace.json", "marketplace.json")
1357
+ .option("--bump <level>", "version bump: major, minor, patch")
1358
+ .option("-d, --dry-run", "preview changes without writing")
1359
+ .action((opts) => {
1360
+ const cwd = process.cwd();
1361
+ const dryRun = opts.dryRun ?? false;
1362
+
1363
+ console.log(chalk.bold("\nAgentBoot — publish\n"));
1364
+ if (dryRun) console.log(chalk.yellow(" DRY RUN — no files will be modified\n"));
1365
+
1366
+ // Find plugin
1367
+ const pluginJsonPath = path.join(cwd, ".claude-plugin", "plugin.json");
1368
+ const distPluginPath = path.join(cwd, "dist", "plugin", "plugin.json");
1369
+
1370
+ let pluginDir: string;
1371
+ let pluginManifest: Record<string, unknown>;
1372
+
1373
+ if (fs.existsSync(pluginJsonPath)) {
1374
+ pluginDir = path.join(cwd, ".claude-plugin");
1375
+ try {
1376
+ pluginManifest = JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"));
1377
+ } catch (e: unknown) {
1378
+ console.error(chalk.red(` Failed to parse plugin.json: ${e instanceof Error ? e.message : String(e)}`));
1379
+ process.exit(1);
1380
+ }
1381
+ } else if (fs.existsSync(distPluginPath)) {
1382
+ pluginDir = path.join(cwd, "dist", "plugin");
1383
+ try {
1384
+ pluginManifest = JSON.parse(fs.readFileSync(distPluginPath, "utf-8"));
1385
+ } catch (e: unknown) {
1386
+ console.error(chalk.red(` Failed to parse plugin.json: ${e instanceof Error ? e.message : String(e)}`));
1387
+ process.exit(1);
1388
+ }
1389
+ } else {
1390
+ console.error(chalk.red(" No plugin found. Run `agentboot export --format plugin` first."));
1391
+ process.exit(1);
1392
+ }
1393
+
1394
+ let version = (pluginManifest["version"] as string) ?? "0.0.0";
1395
+
1396
+ // B8 fix: Validate semver format before bumping
1397
+ if (!/^\d+\.\d+\.\d+$/.test(version)) {
1398
+ console.error(chalk.red(` Invalid version format: '${version}'. Expected X.Y.Z (e.g., 1.2.3)`));
1399
+ process.exit(1);
1400
+ }
1401
+
1402
+ // Version bump — B6 fix: bump BEFORE hash/copy so release gets correct version
1403
+ if (opts.bump) {
1404
+ const parts = version.split(".").map(Number);
1405
+ if (opts.bump === "major") { parts[0]!++; parts[1] = 0; parts[2] = 0; }
1406
+ else if (opts.bump === "minor") { parts[1]!++; parts[2] = 0; }
1407
+ else if (opts.bump === "patch") { parts[2]!++; }
1408
+ else {
1409
+ console.error(chalk.red(` Invalid bump level: '${opts.bump}'. Use: major, minor, patch`));
1410
+ process.exit(1);
1411
+ }
1412
+ version = parts.join(".");
1413
+ pluginManifest["version"] = version;
1414
+
1415
+ // Write bumped version to source plugin.json BEFORE hashing
1416
+ fs.writeFileSync(
1417
+ path.join(pluginDir, "plugin.json"),
1418
+ JSON.stringify(pluginManifest, null, 2) + "\n",
1419
+ "utf-8"
1420
+ );
1421
+ console.log(chalk.cyan(` Version bumped to ${version}`));
1422
+ }
1423
+
1424
+ // Path validation for version (prevent traversal via manipulated version field)
1425
+ if (/[/\\]|\.\./.test(version)) {
1426
+ console.error(chalk.red(` Version contains unsafe characters: '${version}'`));
1427
+ process.exit(1);
1428
+ }
1429
+
1430
+ // Load or create marketplace.json
1431
+ const marketplacePath = path.resolve(cwd, opts.marketplace);
1432
+ let marketplace: MarketplaceManifest;
1433
+
1434
+ if (fs.existsSync(marketplacePath)) {
1435
+ try {
1436
+ marketplace = JSON.parse(fs.readFileSync(marketplacePath, "utf-8"));
1437
+ } catch (e: unknown) {
1438
+ console.error(chalk.red(` Failed to parse marketplace.json: ${e instanceof Error ? e.message : String(e)}`));
1439
+ process.exit(1);
1440
+ }
1441
+ } else {
1442
+ console.log(chalk.yellow(` marketplace.json not found — creating at ${marketplacePath}`));
1443
+ marketplace = {
1444
+ $schema: "https://agentboot.dev/schema/marketplace/v1",
1445
+ name: (pluginManifest["name"] as string) ?? "agentboot-personas",
1446
+ description: (pluginManifest["description"] as string) ?? "",
1447
+ maintainer: (pluginManifest["author"] as string) ?? "",
1448
+ entries: [],
1449
+ };
1450
+ }
1451
+
1452
+ // Compute hash of plugin directory (now includes bumped version)
1453
+ const hash = createHash("sha256");
1454
+ function hashDir(dir: string): void {
1455
+ for (const entry of fs.readdirSync(dir).sort()) {
1456
+ const full = path.join(dir, entry);
1457
+ if (fs.statSync(full).isDirectory()) {
1458
+ hashDir(full);
1459
+ } else {
1460
+ // Include relative path in hash for integrity (not just content)
1461
+ hash.update(path.relative(pluginDir, full));
1462
+ hash.update(fs.readFileSync(full));
1463
+ }
1464
+ }
1465
+ }
1466
+ hashDir(pluginDir);
1467
+ const sha256 = hash.digest("hex");
1468
+
1469
+ // Create release entry
1470
+ const releasePath = `releases/v${version}/`;
1471
+ const entry: MarketplaceEntry = {
1472
+ type: "plugin",
1473
+ name: (pluginManifest["name"] as string) ?? "unknown",
1474
+ version,
1475
+ description: (pluginManifest["description"] as string) ?? "",
1476
+ published_at: new Date().toISOString(),
1477
+ sha256,
1478
+ path: releasePath,
1479
+ };
1480
+
1481
+ // B7 fix: Dedup by type+name+version (preserves version history)
1482
+ const existingIdx = marketplace.entries.findIndex(
1483
+ (e) => e.type === "plugin" && e.name === entry.name && e.version === entry.version
1484
+ );
1485
+ if (existingIdx >= 0) {
1486
+ marketplace.entries[existingIdx] = entry;
1487
+ } else {
1488
+ marketplace.entries.push(entry);
1489
+ }
1490
+
1491
+ if (dryRun) {
1492
+ console.log(chalk.gray(` Would write marketplace.json with entry:`));
1493
+ console.log(chalk.gray(` ${entry.name} v${entry.version} (${sha256.slice(0, 12)}...)`));
1494
+ console.log(chalk.gray(` Would copy plugin to ${releasePath}`));
1495
+ } else {
1496
+ // Write updated marketplace.json
1497
+ fs.writeFileSync(marketplacePath, JSON.stringify(marketplace, null, 2) + "\n", "utf-8");
1498
+
1499
+ // Copy plugin to releases directory (version already bumped in source)
1500
+ const releaseDir = path.resolve(cwd, releasePath);
1501
+ fs.mkdirSync(releaseDir, { recursive: true });
1502
+ copyDirRecursive(pluginDir, releaseDir);
1503
+
1504
+ console.log(chalk.green(` ✓ Published ${entry.name} v${version}`));
1505
+ console.log(chalk.gray(` SHA-256: ${sha256.slice(0, 12)}...`));
1506
+ console.log(chalk.gray(` Path: ${releasePath}`));
1507
+ }
1508
+
1509
+ console.log("");
1510
+ });
1511
+
1065
1512
  // ---------------------------------------------------------------------------
1066
1513
  // Parse
1067
1514
  // ---------------------------------------------------------------------------