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 +1 -1
- package/bin/agentboot.js +9 -1
- package/package.json +1 -1
- package/scripts/cli.ts +423 -4
- package/scripts/compile.ts +695 -69
- package/scripts/lib/config.ts +243 -1
- package/scripts/lib/frontmatter.ts +3 -1
- package/website/docusaurus.config.ts +117 -0
- package/website/package-lock.json +18448 -0
- package/website/package.json +47 -0
- package/website/sidebars.ts +53 -0
- package/website/src/css/custom.css +23 -0
- package/website/src/pages/index.module.css +23 -0
- package/website/src/pages/index.tsx +125 -0
- package/website/static/.nojekyll +0 -0
- package/website/static/CNAME +1 -0
- package/website/static/img/favicon.ico +0 -0
- package/website/static/img/logo.svg +1 -0
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",
|
|
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.
|
|
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
|
|
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
|
// ---------------------------------------------------------------------------
|