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.
- package/README.md +9 -8
- package/agentboot.config.json +4 -1
- package/package.json +2 -2
- package/scripts/cli.ts +465 -18
- package/scripts/compile.ts +724 -75
- package/scripts/dev-sync.ts +1 -1
- package/scripts/lib/config.ts +259 -1
- package/scripts/lib/frontmatter.ts +3 -1
- package/scripts/validate.ts +12 -7
- 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/.github/ISSUE_TEMPLATE/persona-request.md +0 -62
- package/.github/ISSUE_TEMPLATE/quality-feedback.md +0 -67
- package/.github/workflows/cla.yml +0 -25
- package/.github/workflows/validate.yml +0 -49
- package/.idea/agentboot.iml +0 -9
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/CLAUDE.md +0 -230
- package/CONTRIBUTING.md +0 -168
- package/PERSONAS.md +0 -156
- package/core/instructions/baseline.instructions.md +0 -133
- package/core/instructions/security.instructions.md +0 -186
- package/core/personas/code-reviewer/SKILL.md +0 -175
- package/core/personas/security-reviewer/SKILL.md +0 -233
- package/core/personas/test-data-expert/SKILL.md +0 -234
- package/core/personas/test-generator/SKILL.md +0 -262
- package/core/traits/audit-trail.md +0 -182
- package/core/traits/confidence-signaling.md +0 -172
- package/core/traits/critical-thinking.md +0 -129
- package/core/traits/schema-awareness.md +0 -132
- package/core/traits/source-citation.md +0 -174
- package/core/traits/structured-output.md +0 -199
- package/docs/ci-cd-automation.md +0 -548
- package/docs/claude-code-reference/README.md +0 -21
- package/docs/claude-code-reference/agentboot-coverage.md +0 -484
- package/docs/claude-code-reference/feature-inventory.md +0 -906
- package/docs/cli-commands-audit.md +0 -112
- package/docs/cli-design.md +0 -924
- package/docs/concepts.md +0 -1117
- package/docs/config-schema-audit.md +0 -121
- package/docs/configuration.md +0 -645
- package/docs/delivery-methods.md +0 -758
- package/docs/developer-onboarding.md +0 -342
- package/docs/extending.md +0 -448
- package/docs/getting-started.md +0 -298
- package/docs/knowledge-layer.md +0 -464
- package/docs/marketplace.md +0 -822
- package/docs/org-connection.md +0 -570
- package/docs/plans/architecture.md +0 -2429
- package/docs/plans/design.md +0 -2018
- package/docs/plans/prd.md +0 -1862
- package/docs/plans/stack-rank.md +0 -261
- package/docs/plans/technical-spec.md +0 -2755
- package/docs/privacy-and-safety.md +0 -807
- package/docs/prompt-optimization.md +0 -1071
- package/docs/test-plan.md +0 -972
- package/docs/third-party-ecosystem.md +0 -496
- package/domains/compliance-template/README.md +0 -173
- package/domains/compliance-template/traits/compliance-aware.md +0 -228
- package/examples/enterprise/agentboot.config.json +0 -184
- package/examples/minimal/agentboot.config.json +0 -46
- package/tests/REGRESSION-PLAN.md +0 -705
- package/tests/TEST-PLAN.md +0 -111
- package/tests/cli.test.ts +0 -705
- package/tests/pipeline.test.ts +0 -608
- package/tests/validate.test.ts +0 -278
- 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
|
-
// ----
|
|
217
|
+
// ---- dev-build -----------------------------------------------------------
|
|
204
218
|
|
|
205
219
|
program
|
|
206
|
-
.command("
|
|
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("✓
|
|
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
|
|
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-]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1179
|
+
.action((key: string | undefined, value: string | undefined, _opts, cmd) => {
|
|
1180
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1027
1181
|
const cwd = process.cwd();
|
|
1028
|
-
const configPath =
|
|
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
|
// ---------------------------------------------------------------------------
|