agentboot 0.2.0 → 0.3.1

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 CHANGED
@@ -166,7 +166,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
166
166
 
167
167
  ## License
168
168
 
169
- Apache 2.0 — see [LICENSE](LICENSE).
169
+ Apache 2.0 — see [LICENSE](LICENSE). "AgentBoot" is a trademark of Michel Saavedra — see [TRADEMARK.md](TRADEMARK.md).
170
170
 
171
171
  ---
172
172
 
package/bin/agentboot.js CHANGED
@@ -3,14 +3,22 @@
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, join } from "node:path";
6
+ import { createRequire } from "node:module";
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = dirname(__filename);
9
10
  const cli = join(__dirname, "..", "scripts", "cli.ts");
10
11
 
12
+ // Resolve tsx from this package's own node_modules, not the user's cwd.
13
+ // When installed globally (npm -g, brew), Node resolves bare specifiers
14
+ // from cwd which fails. Using createRequire anchors resolution to this file.
15
+ const require = createRequire(import.meta.url);
16
+ const tsxPkgDir = dirname(require.resolve("tsx/package.json"));
17
+ const tsxEntry = join(tsxPkgDir, "dist", "esm", "index.mjs");
18
+
11
19
  const result = spawnSync(
12
20
  process.execPath,
13
- ["--import", "tsx", cli, ...process.argv.slice(2)],
21
+ ["--import", `file://${tsxEntry}`, cli, ...process.argv.slice(2)],
14
22
  { stdio: "inherit", env: { ...process.env } }
15
23
  );
16
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentboot",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Convention over configuration for agentic development teams. The Spring Boot of Claude Code governance.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Mike Saavedra <mike@agentboot.dev>",
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[] = [];
@@ -359,8 +373,8 @@ program
359
373
 
360
374
  program
361
375
  .command("add")
362
- .description("Scaffold a new persona, trait, or gotcha")
363
- .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")
364
378
  .argument("<name>", "name for the new item (lowercase-with-hyphens)")
365
379
  .action((type: string, name: string) => {
366
380
  // Validate name format
@@ -522,8 +536,122 @@ paths:
522
536
  console.log(chalk.gray(` core/gotchas/${name}.md\n`));
523
537
  console.log(chalk.gray(` Next: Edit the paths: frontmatter and add your rules.\n`));
524
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
+
525
653
  } else {
526
- 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`));
527
655
  process.exit(1);
528
656
  }
529
657
  });
@@ -1090,6 +1218,297 @@ program
1090
1218
  process.exit(1);
1091
1219
  });
1092
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
+
1093
1512
  // ---------------------------------------------------------------------------
1094
1513
  // Parse
1095
1514
  // ---------------------------------------------------------------------------