agentboot 0.4.2 → 0.4.4
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 +2 -2
- package/package.json +2 -2
- package/scripts/cli.ts +211 -29
- package/scripts/compile.ts +8 -3
- package/scripts/lib/config.ts +8 -3
- package/scripts/lib/import.ts +1 -1
- package/scripts/lib/install.ts +353 -66
- package/scripts/sync.ts +10 -4
- package/scripts/validate.ts +10 -6
- package/website/src/pages/index.tsx +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# AgentBoot
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Bootstrap your agentic development teams.**
|
|
4
4
|
|
|
5
|
-
AgentBoot is a build tool that compiles AI agent personas and distributes them across your organization's repositories. Define once, deploy everywhere.
|
|
5
|
+
AgentBoot is a build tool that compiles AI agent personas and distributes them across your organization's repositories. Define once, deploy everywhere. Convention over configuration for AI agent governance.
|
|
6
6
|
|
|
7
7
|
## The Problem
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentboot",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.4",
|
|
4
|
+
"description": "Bootstrap your agentic development teams. Convention over configuration for AI agent governance.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Mike Saavedra <mike@agentboot.dev>",
|
|
7
7
|
"homepage": "https://agentboot.dev",
|
package/scripts/cli.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* agentboot sync [--repos-file path] [--dry-run]
|
|
13
13
|
* agentboot install [--hub] [--connect] [--org name] [--path dir]
|
|
14
14
|
* agentboot add <type> <name>
|
|
15
|
-
* agentboot doctor [--format text|json]
|
|
15
|
+
* agentboot doctor [--fix] [--dry-run] [--format text|json]
|
|
16
16
|
* agentboot status [--format text|json]
|
|
17
17
|
* agentboot lint [--persona name] [--severity level] [--format text|json]
|
|
18
18
|
* agentboot uninstall [--repo path] [--dry-run]
|
|
@@ -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, type MarketplaceManifest, type MarketplaceEntry } from "./lib/config.js";
|
|
30
|
+
import { loadConfig, stripJsoncComments, type MarketplaceManifest, type MarketplaceEntry } from "./lib/config.js";
|
|
31
31
|
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
// Paths
|
|
@@ -615,12 +615,12 @@ Add to \`agentboot.config.json\`:
|
|
|
615
615
|
# }
|
|
616
616
|
|
|
617
617
|
INPUT=$(cat)
|
|
618
|
-
EVENT_NAME=$(
|
|
618
|
+
EVENT_NAME=$(printf '%s' "$INPUT" | jq -r '.hook_event_name // empty')
|
|
619
619
|
|
|
620
620
|
# TODO: Add your compliance logic here
|
|
621
621
|
# Example: block a tool if a condition is met
|
|
622
622
|
# if [ "$EVENT_NAME" = "PreToolUse" ]; then
|
|
623
|
-
# TOOL=$(
|
|
623
|
+
# TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
|
|
624
624
|
# if [ "$TOOL" = "Bash" ]; then
|
|
625
625
|
# echo '{"decision":"block","reason":"Bash tool is restricted by policy"}' >&2
|
|
626
626
|
# exit 2
|
|
@@ -682,19 +682,28 @@ program
|
|
|
682
682
|
.command("doctor")
|
|
683
683
|
.description("Check environment and diagnose configuration issues")
|
|
684
684
|
.option("--format <fmt>", "output format: text, json", "text")
|
|
685
|
+
.option("--fix", "auto-fix issues that can be resolved automatically")
|
|
686
|
+
.option("--dry-run", "show what --fix would do without making changes")
|
|
685
687
|
.action((opts, cmd) => {
|
|
686
688
|
const globalOpts = cmd.optsWithGlobals();
|
|
687
689
|
const isJson = opts.format === "json";
|
|
690
|
+
const fixMode = opts.fix === true;
|
|
691
|
+
const dryRun = opts.dryRun === true;
|
|
692
|
+
if (dryRun && !fixMode && !isJson) {
|
|
693
|
+
console.log(chalk.yellow("Note: --dry-run has no effect without --fix\n"));
|
|
694
|
+
}
|
|
688
695
|
if (!isJson) console.log(chalk.bold("\nAgentBoot — doctor\n"));
|
|
689
696
|
const cwd = process.cwd();
|
|
690
|
-
let
|
|
697
|
+
let issuesFound = 0;
|
|
698
|
+
let issuesFixed = 0;
|
|
691
699
|
|
|
692
|
-
interface DoctorCheck { name: string; status: "ok" | "fail" | "warn"; message: string }
|
|
700
|
+
interface DoctorCheck { name: string; status: "ok" | "fail" | "warn"; message: string; fixable?: boolean; fixed?: boolean }
|
|
693
701
|
const checks: DoctorCheck[] = [];
|
|
694
702
|
|
|
695
703
|
function ok(msg: string) { checks.push({ name: msg, status: "ok", message: msg }); if (!isJson) console.log(` ${chalk.green("✓")} ${msg}`); }
|
|
696
|
-
function fail(msg: string) {
|
|
697
|
-
function warn(msg: string) { checks.push({ name: msg, status: "warn", message: msg }); if (!isJson) console.log(` ${chalk.yellow("⚠")} ${msg}`); }
|
|
704
|
+
function fail(msg: string, fixable = false) { issuesFound++; checks.push({ name: msg, status: "fail", message: msg, fixable }); if (!isJson) console.log(` ${chalk.red("✗")} ${msg}${fixable && !fixMode ? chalk.gray(" (fixable with --fix)") : ""}`); }
|
|
705
|
+
function warn(msg: string, fixable = false) { checks.push({ name: msg, status: "warn", message: msg, fixable }); if (!isJson) console.log(` ${chalk.yellow("⚠")} ${msg}${fixable && !fixMode ? chalk.gray(" (fixable with --fix)") : ""}`); }
|
|
706
|
+
function fixed(msg: string) { issuesFound++; issuesFixed++; checks.push({ name: msg, status: "ok", message: msg, fixed: true }); if (!isJson) console.log(` ${chalk.green("✓")} ${msg} ${chalk.cyan(dryRun ? "(would fix)" : "(fixed)")}`); }
|
|
698
707
|
|
|
699
708
|
// 1. Environment
|
|
700
709
|
if (!isJson) console.log(chalk.cyan("Environment"));
|
|
@@ -725,36 +734,146 @@ program
|
|
|
725
734
|
const config = loadConfig(configPath);
|
|
726
735
|
ok(`Config parses successfully (org: ${config.org})`);
|
|
727
736
|
|
|
737
|
+
// Check for orgDisplayName
|
|
738
|
+
if (!config.orgDisplayName || config.orgDisplayName === config.org) {
|
|
739
|
+
warn(`orgDisplayName not set — compiled output will use "${config.org}" as the display name`);
|
|
740
|
+
if (!isJson) console.log(chalk.gray(` Set it with: agentboot config orgDisplayName "Your Org Name"`));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Helper: generate a minimal SKILL.md scaffold
|
|
744
|
+
function scaffoldSkillMd(name: string): string {
|
|
745
|
+
return [
|
|
746
|
+
"---",
|
|
747
|
+
`id: ${name}`,
|
|
748
|
+
`name: ${name}`,
|
|
749
|
+
"version: 0.1.0",
|
|
750
|
+
"---",
|
|
751
|
+
"",
|
|
752
|
+
`# ${name}`,
|
|
753
|
+
"",
|
|
754
|
+
"<!-- traits:start -->",
|
|
755
|
+
"<!-- traits:end -->",
|
|
756
|
+
"",
|
|
757
|
+
"TODO: Define this persona.",
|
|
758
|
+
"",
|
|
759
|
+
].join("\n");
|
|
760
|
+
}
|
|
761
|
+
|
|
728
762
|
// Check personas
|
|
729
763
|
const enabledPersonas = config.personas?.enabled ?? [];
|
|
730
764
|
const personasDir = path.join(cwd, "core", "personas");
|
|
731
765
|
let personaIssues = 0;
|
|
766
|
+
let personasScaffolded = 0;
|
|
732
767
|
for (const p of enabledPersonas) {
|
|
733
768
|
const pDir = path.join(personasDir, p);
|
|
734
|
-
if (!fs.existsSync(pDir)) {
|
|
735
|
-
|
|
769
|
+
if (!fs.existsSync(pDir)) {
|
|
770
|
+
if (fixMode) {
|
|
771
|
+
if (!dryRun) {
|
|
772
|
+
fs.mkdirSync(pDir, { recursive: true });
|
|
773
|
+
fs.writeFileSync(path.join(pDir, "SKILL.md"), scaffoldSkillMd(p), "utf-8");
|
|
774
|
+
const personaConfig = { traits: config.traits?.enabled ?? [] };
|
|
775
|
+
fs.writeFileSync(path.join(pDir, "persona.config.json"), JSON.stringify(personaConfig, null, 2) + "\n", "utf-8");
|
|
776
|
+
}
|
|
777
|
+
personasScaffolded++;
|
|
778
|
+
fixed(`Scaffolded persona: ${p}`);
|
|
779
|
+
} else {
|
|
780
|
+
personaIssues++; fail(`Persona not found: ${p}`, true);
|
|
781
|
+
}
|
|
782
|
+
} else if (!fs.existsSync(path.join(pDir, "SKILL.md"))) {
|
|
783
|
+
if (fixMode) {
|
|
784
|
+
if (!dryRun) {
|
|
785
|
+
fs.writeFileSync(path.join(pDir, "SKILL.md"), scaffoldSkillMd(p), "utf-8");
|
|
786
|
+
}
|
|
787
|
+
personasScaffolded++;
|
|
788
|
+
fixed(`Created missing SKILL.md for: ${p}`);
|
|
789
|
+
} else {
|
|
790
|
+
personaIssues++; fail(`Missing SKILL.md: ${p}`, true);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (personaIssues === 0 && personasScaffolded === 0) {
|
|
795
|
+
ok(`All ${enabledPersonas.length} enabled personas found`);
|
|
796
|
+
} else if (personaIssues === 0 && personasScaffolded > 0) {
|
|
797
|
+
ok(`All ${enabledPersonas.length} enabled personas found (${personasScaffolded} scaffolded)`);
|
|
736
798
|
}
|
|
737
|
-
if (personaIssues === 0) ok(`All ${enabledPersonas.length} enabled personas found`);
|
|
738
799
|
|
|
739
800
|
// Check traits
|
|
740
801
|
const enabledTraits = config.traits?.enabled ?? [];
|
|
741
802
|
const traitsDir = path.join(cwd, "core", "traits");
|
|
742
803
|
let traitIssues = 0;
|
|
804
|
+
let traitsScaffolded = 0;
|
|
743
805
|
for (const t of enabledTraits) {
|
|
744
|
-
if (!fs.existsSync(path.join(traitsDir, `${t}.md`))) {
|
|
806
|
+
if (!fs.existsSync(path.join(traitsDir, `${t}.md`))) {
|
|
807
|
+
if (fixMode) {
|
|
808
|
+
if (!dryRun) {
|
|
809
|
+
fs.mkdirSync(traitsDir, { recursive: true });
|
|
810
|
+
const traitContent = `# ${t}\n\nTODO: Define this trait.\n`;
|
|
811
|
+
fs.writeFileSync(path.join(traitsDir, `${t}.md`), traitContent, "utf-8");
|
|
812
|
+
}
|
|
813
|
+
traitsScaffolded++;
|
|
814
|
+
fixed(`Created missing trait: ${t}.md`);
|
|
815
|
+
} else {
|
|
816
|
+
traitIssues++; fail(`Trait not found: ${t}`, true);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (traitIssues === 0 && traitsScaffolded === 0) {
|
|
821
|
+
ok(`All ${enabledTraits.length} enabled traits found`);
|
|
822
|
+
} else if (traitIssues === 0 && traitsScaffolded > 0) {
|
|
823
|
+
ok(`All ${enabledTraits.length} enabled traits found (${traitsScaffolded} scaffolded)`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Check core directories
|
|
827
|
+
const coreDirs = ["core/personas", "core/traits", "core/instructions", "core/gotchas"];
|
|
828
|
+
for (const dir of coreDirs) {
|
|
829
|
+
const fullDir = path.join(cwd, dir);
|
|
830
|
+
if (!fs.existsSync(fullDir)) {
|
|
831
|
+
if (fixMode) {
|
|
832
|
+
if (!dryRun) fs.mkdirSync(fullDir, { recursive: true });
|
|
833
|
+
fixed(`Created missing directory: ${dir}/`);
|
|
834
|
+
} else {
|
|
835
|
+
warn(`Missing directory: ${dir}/`, true);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
745
838
|
}
|
|
746
|
-
if (traitIssues === 0) ok(`All ${enabledTraits.length} enabled traits found`);
|
|
747
839
|
|
|
748
840
|
// Check repos.json
|
|
749
841
|
const reposPath = config.sync?.repos ?? "./repos.json";
|
|
750
842
|
const fullReposPath = path.resolve(path.dirname(configPath), reposPath);
|
|
751
|
-
if (fs.existsSync(fullReposPath))
|
|
752
|
-
|
|
843
|
+
if (fs.existsSync(fullReposPath)) {
|
|
844
|
+
ok(`repos.json found`);
|
|
845
|
+
} else if (fixMode) {
|
|
846
|
+
if (!dryRun) fs.writeFileSync(fullReposPath, "[]\n", "utf-8");
|
|
847
|
+
fixed(`Created empty repos.json`);
|
|
848
|
+
} else {
|
|
849
|
+
warn(`repos.json not found at ${reposPath}`, true);
|
|
850
|
+
}
|
|
753
851
|
|
|
754
852
|
// Check dist/
|
|
755
853
|
const distPath = path.resolve(cwd, config.output?.distPath ?? "./dist");
|
|
756
|
-
if (fs.existsSync(distPath))
|
|
757
|
-
|
|
854
|
+
if (fs.existsSync(distPath)) {
|
|
855
|
+
ok(`dist/ exists (built)`);
|
|
856
|
+
} else if (fixMode) {
|
|
857
|
+
if (!isJson) console.log(` ${chalk.cyan("→")} Building dist/...`);
|
|
858
|
+
if (!dryRun) {
|
|
859
|
+
const compileScript = path.join(SCRIPTS_DIR, "compile.ts");
|
|
860
|
+
const tsx = path.join(ROOT, "node_modules", ".bin", "tsx");
|
|
861
|
+
const buildResult = spawnSync(tsx, [compileScript], {
|
|
862
|
+
cwd,
|
|
863
|
+
encoding: "utf-8",
|
|
864
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
865
|
+
});
|
|
866
|
+
if (buildResult.status === 0) {
|
|
867
|
+
fixed(`Built dist/`);
|
|
868
|
+
} else {
|
|
869
|
+
fail(`Build failed: ${buildResult.stderr?.trim() ?? "unknown error"}`);
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
fixed(`Would run \`agentboot build\``);
|
|
873
|
+
}
|
|
874
|
+
} else {
|
|
875
|
+
warn(`dist/ not found — run \`agentboot build\``, true);
|
|
876
|
+
}
|
|
758
877
|
|
|
759
878
|
} catch (e: unknown) {
|
|
760
879
|
fail(`Config parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -766,16 +885,28 @@ program
|
|
|
766
885
|
|
|
767
886
|
if (!isJson) console.log("");
|
|
768
887
|
|
|
888
|
+
const issuesRemaining = issuesFound - issuesFixed;
|
|
889
|
+
|
|
769
890
|
if (isJson) {
|
|
770
|
-
console.log(JSON.stringify({ issues, checks }, null, 2));
|
|
771
|
-
process.exit(
|
|
891
|
+
console.log(JSON.stringify({ issues: issuesRemaining, issuesFound, issuesFixed, checks }, null, 2));
|
|
892
|
+
process.exit(issuesRemaining > 0 ? 1 : 0);
|
|
772
893
|
}
|
|
773
894
|
|
|
774
|
-
if (
|
|
775
|
-
|
|
895
|
+
if (issuesRemaining > 0) {
|
|
896
|
+
const fixableCount = checks.filter(c => c.fixable && !c.fixed).length;
|
|
897
|
+
console.log(chalk.bold(chalk.red(`✗ ${issuesRemaining} issue${issuesRemaining !== 1 ? "s" : ""} found`)));
|
|
898
|
+
if (fixableCount > 0) {
|
|
899
|
+
console.log(chalk.gray(` ${fixableCount} fixable — run \`agentboot doctor --fix\`\n`));
|
|
900
|
+
} else {
|
|
901
|
+
console.log("");
|
|
902
|
+
}
|
|
776
903
|
process.exit(1);
|
|
777
904
|
} else {
|
|
778
|
-
|
|
905
|
+
if (issuesFixed > 0) {
|
|
906
|
+
console.log(chalk.bold(chalk.green(`✓ All checks passed (${issuesFixed} issue${issuesFixed !== 1 ? "s" : ""} ${dryRun ? "would be " : ""}fixed)\n`)));
|
|
907
|
+
} else {
|
|
908
|
+
console.log(chalk.bold(chalk.green("✓ All checks passed\n")));
|
|
909
|
+
}
|
|
779
910
|
}
|
|
780
911
|
});
|
|
781
912
|
|
|
@@ -1264,9 +1395,9 @@ program
|
|
|
1264
1395
|
|
|
1265
1396
|
program
|
|
1266
1397
|
.command("config")
|
|
1267
|
-
.description("
|
|
1268
|
-
.argument("[key]", "config key (e.g., personas.enabled)")
|
|
1269
|
-
.argument("[value]", "
|
|
1398
|
+
.description("Read or write configuration values")
|
|
1399
|
+
.argument("[key]", "config key (e.g., org, orgDisplayName, personas.enabled)")
|
|
1400
|
+
.argument("[value]", "value to set (strings only — edit agentboot.config.json for complex values)")
|
|
1270
1401
|
.action((key: string | undefined, value: string | undefined, _opts, cmd) => {
|
|
1271
1402
|
const globalOpts = cmd.optsWithGlobals();
|
|
1272
1403
|
const cwd = process.cwd();
|
|
@@ -1303,10 +1434,61 @@ program
|
|
|
1303
1434
|
process.exit(0);
|
|
1304
1435
|
}
|
|
1305
1436
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1437
|
+
// Write a config value
|
|
1438
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
1439
|
+
|
|
1440
|
+
// Detect JSONC comments — writing back would destroy them
|
|
1441
|
+
const stripped = stripJsoncComments(raw);
|
|
1442
|
+
if (stripped !== raw) {
|
|
1443
|
+
console.error(chalk.red("Config file contains comments (JSONC)."));
|
|
1444
|
+
console.error(chalk.gray(" Writing would remove all comments. Edit the file directly:\n"));
|
|
1445
|
+
console.error(chalk.gray(` ${configPath}\n`));
|
|
1446
|
+
process.exit(1);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
let config: Record<string, unknown>;
|
|
1450
|
+
try {
|
|
1451
|
+
config = JSON.parse(stripped);
|
|
1452
|
+
} catch {
|
|
1453
|
+
console.error(chalk.red("Failed to parse config for writing."));
|
|
1454
|
+
process.exit(1);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const keys = key.split(".");
|
|
1458
|
+
let target: Record<string, unknown> = config;
|
|
1459
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
1460
|
+
const k = keys[i]!;
|
|
1461
|
+
if (target[k] === undefined) {
|
|
1462
|
+
// Auto-create intermediate objects
|
|
1463
|
+
target[k] = {};
|
|
1464
|
+
target = target[k] as Record<string, unknown>;
|
|
1465
|
+
} else if (typeof target[k] === "object" && !Array.isArray(target[k]) && target[k] !== null) {
|
|
1466
|
+
target = target[k] as Record<string, unknown>;
|
|
1467
|
+
} else {
|
|
1468
|
+
console.error(chalk.red(`Cannot write to ${key}: "${k}" exists but is ${typeof target[k]}, not an object.`));
|
|
1469
|
+
console.error(chalk.gray(" Edit agentboot.config.json directly.\n"));
|
|
1470
|
+
process.exit(1);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const finalKey = keys[keys.length - 1]!;
|
|
1475
|
+
const oldValue = target[finalKey];
|
|
1476
|
+
|
|
1477
|
+
// Guard against overwriting non-string values (arrays, objects, numbers, booleans)
|
|
1478
|
+
if (oldValue !== undefined && typeof oldValue !== "string") {
|
|
1479
|
+
console.error(chalk.red(`Cannot overwrite ${key}: existing value is ${typeof oldValue}, not a string.`));
|
|
1480
|
+
console.error(chalk.gray(" Edit agentboot.config.json directly for non-string values.\n"));
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
target[finalKey] = value;
|
|
1485
|
+
|
|
1486
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1487
|
+
if (oldValue !== undefined) {
|
|
1488
|
+
console.log(chalk.green(` ${key}: ${JSON.stringify(oldValue)} → ${JSON.stringify(value)}`));
|
|
1489
|
+
} else {
|
|
1490
|
+
console.log(chalk.green(` ${key}: ${JSON.stringify(value)} (added)`));
|
|
1491
|
+
}
|
|
1310
1492
|
});
|
|
1311
1493
|
|
|
1312
1494
|
// ---- export (AB-40) -------------------------------------------------------
|
package/scripts/compile.ts
CHANGED
|
@@ -1043,10 +1043,15 @@ exit 0
|
|
|
1043
1043
|
const includeDevId = config.telemetry?.includeDevId ?? false;
|
|
1044
1044
|
|
|
1045
1045
|
let devIdBlock = "";
|
|
1046
|
-
if (includeDevId === "hashed") {
|
|
1046
|
+
if (includeDevId === "hashed" || includeDevId === "email") {
|
|
1047
|
+
if (includeDevId === "email") {
|
|
1048
|
+
log(chalk.yellow(` ⚠ telemetry.includeDevId "email" now defaults to hashed for privacy.`));
|
|
1049
|
+
log(chalk.yellow(` Use "email-raw" to explicitly include raw emails (not recommended).`));
|
|
1050
|
+
}
|
|
1047
1051
|
devIdBlock = `DEV_ID=$(git config user.email 2>/dev/null | shasum -a 256 | cut -d' ' -f1)`;
|
|
1048
|
-
} else if (includeDevId === "email") {
|
|
1049
|
-
log(chalk.yellow(` ⚠ telemetry.includeDevId is "email" — raw emails will be in telemetry logs
|
|
1052
|
+
} else if (includeDevId === "email-raw") {
|
|
1053
|
+
log(chalk.yellow(` ⚠ telemetry.includeDevId is "email-raw" — raw emails will be in telemetry logs.`));
|
|
1054
|
+
log(chalk.yellow(` Consider "hashed" for privacy compliance (GDPR, data minimization).`));
|
|
1050
1055
|
devIdBlock = `DEV_ID=$(git config user.email 2>/dev/null || echo "unknown")`;
|
|
1051
1056
|
} else {
|
|
1052
1057
|
devIdBlock = `DEV_ID=""`;
|
package/scripts/lib/config.ts
CHANGED
|
@@ -171,7 +171,7 @@ export interface TelemetryConfig {
|
|
|
171
171
|
enabled?: boolean;
|
|
172
172
|
/** How to identify developers in telemetry.
|
|
173
173
|
* false = no developer ID, "hashed" = SHA-256 of email, "email" = raw email. */
|
|
174
|
-
includeDevId?: false | "hashed" | "email";
|
|
174
|
+
includeDevId?: false | "hashed" | "email" | "email-raw";
|
|
175
175
|
/** Path to NDJSON log file. Default: ~/.agentboot/telemetry.ndjson */
|
|
176
176
|
logPath?: string;
|
|
177
177
|
/** Never include raw prompt content in telemetry. Design invariant. */
|
|
@@ -396,10 +396,15 @@ export function loadConfig(configPath: string): AgentBootConfig {
|
|
|
396
396
|
["sync.repos", parsed.sync?.repos],
|
|
397
397
|
["output.distPath", parsed.output?.distPath],
|
|
398
398
|
["personas.customDir", parsed.personas?.customDir],
|
|
399
|
+
["telemetry.logPath", parsed.telemetry?.logPath],
|
|
399
400
|
];
|
|
400
401
|
for (const [fieldName, value] of pathFields) {
|
|
401
|
-
if (typeof value === "string"
|
|
402
|
-
|
|
402
|
+
if (typeof value === "string") {
|
|
403
|
+
// Check for .. path traversal (normalized for both separators)
|
|
404
|
+
const normalized = value.replace(/\\/g, "/");
|
|
405
|
+
if (normalized.split("/").includes("..")) {
|
|
406
|
+
throw new Error(`"${fieldName}" must not contain ".." path segments`);
|
|
407
|
+
}
|
|
403
408
|
}
|
|
404
409
|
}
|
|
405
410
|
|
package/scripts/lib/import.ts
CHANGED
|
@@ -917,7 +917,7 @@ function analyzeOverlap(
|
|
|
917
917
|
return matches;
|
|
918
918
|
}
|
|
919
919
|
|
|
920
|
-
export { analyzeOverlap, normalizeContent, jaccardSimilarity };
|
|
920
|
+
export { analyzeOverlap, normalizeContent, jaccardSimilarity, scanPath, applyPlan, buildClassificationPrompt, ALLOWED_CLASSIFICATION_DIRS };
|
|
921
921
|
|
|
922
922
|
// ---------------------------------------------------------------------------
|
|
923
923
|
// Hub finder
|
package/scripts/lib/install.ts
CHANGED
|
@@ -142,13 +142,21 @@ function detectGhAuthenticated(): boolean {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
145
|
+
* Scan nearby directories for agentboot.config.json or .claude/.
|
|
146
|
+
* Checks: the parent directory itself, then sibling directories.
|
|
147
|
+
* This supports both sibling layouts (hub next to spokes) and parent layouts
|
|
148
|
+
* (hub is the parent directory containing spoke repos).
|
|
147
149
|
*/
|
|
148
|
-
function
|
|
150
|
+
export function scanNearby(cwd: string): Array<{ path: string; type: "hub" | "claude" }> {
|
|
149
151
|
const parent = path.dirname(cwd);
|
|
150
152
|
const results: Array<{ path: string; type: "hub" | "claude" }> = [];
|
|
151
153
|
|
|
154
|
+
// Check parent directory itself (supports hub-as-parent layout)
|
|
155
|
+
if (fs.existsSync(path.join(parent, "agentboot.config.json"))) {
|
|
156
|
+
results.push({ path: parent, type: "hub" });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check sibling directories
|
|
152
160
|
try {
|
|
153
161
|
for (const entry of fs.readdirSync(parent)) {
|
|
154
162
|
const siblingPath = path.join(parent, entry);
|
|
@@ -206,11 +214,11 @@ function searchGitHubOrg(org: string): string | null {
|
|
|
206
214
|
// Scaffold helpers
|
|
207
215
|
// ---------------------------------------------------------------------------
|
|
208
216
|
|
|
209
|
-
export function scaffoldHub(targetDir: string,
|
|
217
|
+
export function scaffoldHub(targetDir: string, orgSlug: string, orgDisplayName?: string): void {
|
|
210
218
|
// agentboot.config.json
|
|
211
219
|
const configContent = JSON.stringify({
|
|
212
|
-
org:
|
|
213
|
-
orgDisplayName:
|
|
220
|
+
org: orgSlug,
|
|
221
|
+
orgDisplayName: orgDisplayName ?? orgSlug,
|
|
214
222
|
groups: {},
|
|
215
223
|
personas: {
|
|
216
224
|
enabled: ["code-reviewer", "security-reviewer", "test-generator", "test-data-expert"],
|
|
@@ -246,6 +254,11 @@ export function scaffoldHub(targetDir: string, orgName: string): void {
|
|
|
246
254
|
|
|
247
255
|
function runBuild(hubDir: string): boolean {
|
|
248
256
|
console.log(chalk.cyan("\n Compiling personas..."));
|
|
257
|
+
console.log(chalk.gray(
|
|
258
|
+
" This reads your traits and personas from core/, composes them, and\n" +
|
|
259
|
+
" writes compiled output to dist/. The dist/ folder is what gets\n" +
|
|
260
|
+
" deployed to your repos.\n"
|
|
261
|
+
));
|
|
249
262
|
const result = spawnSync("agentboot", ["build"], {
|
|
250
263
|
cwd: hubDir,
|
|
251
264
|
encoding: "utf-8",
|
|
@@ -253,10 +266,10 @@ function runBuild(hubDir: string): boolean {
|
|
|
253
266
|
});
|
|
254
267
|
|
|
255
268
|
if (result.status === 0) {
|
|
256
|
-
console.log(chalk.green("
|
|
269
|
+
console.log(chalk.green(" Build complete."));
|
|
257
270
|
return true;
|
|
258
271
|
} else {
|
|
259
|
-
console.log(chalk.yellow(" Build
|
|
272
|
+
console.log(chalk.yellow(" Build did not complete — you can run `agentboot build` later."));
|
|
260
273
|
return false;
|
|
261
274
|
}
|
|
262
275
|
}
|
|
@@ -270,6 +283,150 @@ function runSync(hubDir: string): boolean {
|
|
|
270
283
|
return result.status === 0;
|
|
271
284
|
}
|
|
272
285
|
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Hub target validation
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Validate and potentially adjust the hub target directory.
|
|
292
|
+
*
|
|
293
|
+
* If the target directory exists and looks like it already has content (a git
|
|
294
|
+
* repo, source files, etc.), we don't want to scaffold hub files into it —
|
|
295
|
+
* that would pollute an existing project. Instead, offer to create a `personas`
|
|
296
|
+
* subdirectory.
|
|
297
|
+
*/
|
|
298
|
+
async function validateHubTarget(initialDir: string): Promise<string> {
|
|
299
|
+
let hubDir = initialDir;
|
|
300
|
+
|
|
301
|
+
// eslint-disable-next-line no-constant-condition
|
|
302
|
+
while (true) {
|
|
303
|
+
// If the directory doesn't exist, it will be created fresh — no issue.
|
|
304
|
+
if (!fs.existsSync(hubDir)) {
|
|
305
|
+
fs.mkdirSync(hubDir, { recursive: true });
|
|
306
|
+
return hubDir;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// If it already has agentboot.config.json, it's already a hub — bail.
|
|
310
|
+
if (fs.existsSync(path.join(hubDir, "agentboot.config.json"))) {
|
|
311
|
+
console.log(chalk.yellow("\n This directory already has agentboot.config.json."));
|
|
312
|
+
console.log(chalk.gray(" Run `agentboot doctor` to check your configuration.\n"));
|
|
313
|
+
throw new AgentBootError(0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Check if the directory has existing content that suggests it's not an
|
|
317
|
+
// empty directory intended for a new personas repo.
|
|
318
|
+
const entries = fs.readdirSync(hubDir).filter(e => !e.startsWith(".") || e === ".git");
|
|
319
|
+
const hasGit = fs.existsSync(path.join(hubDir, ".git"));
|
|
320
|
+
const hasPackageJson = fs.existsSync(path.join(hubDir, "package.json"));
|
|
321
|
+
const hasSrc = fs.existsSync(path.join(hubDir, "src"));
|
|
322
|
+
|
|
323
|
+
const hasExistingContent = entries.length > 0 && (hasGit || hasPackageJson || hasSrc);
|
|
324
|
+
|
|
325
|
+
if (!hasExistingContent) {
|
|
326
|
+
// Empty or near-empty directory — fine to use directly.
|
|
327
|
+
return hubDir;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// The directory has content. Warn and offer alternatives.
|
|
331
|
+
const dirName = path.basename(hubDir);
|
|
332
|
+
const personasPath = path.join(hubDir, "personas");
|
|
333
|
+
|
|
334
|
+
console.log(chalk.yellow(
|
|
335
|
+
`\n "${dirName}" already has content (${entries.length} items).` +
|
|
336
|
+
`\n Scaffolding here would mix persona source code with existing files.\n`
|
|
337
|
+
));
|
|
338
|
+
|
|
339
|
+
const choice = await select({
|
|
340
|
+
message: "Where should the personas repo live?",
|
|
341
|
+
choices: [
|
|
342
|
+
{ name: `Create ${personasPath} (recommended)`, value: "sub" },
|
|
343
|
+
{ name: "Choose a different location", value: "custom" },
|
|
344
|
+
{ name: `Use ${hubDir} anyway (not recommended)`, value: "here" },
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (choice === "sub") {
|
|
349
|
+
if (!fs.existsSync(personasPath)) {
|
|
350
|
+
fs.mkdirSync(personasPath, { recursive: true });
|
|
351
|
+
}
|
|
352
|
+
return personasPath;
|
|
353
|
+
} else if (choice === "custom") {
|
|
354
|
+
const customPath = await input({
|
|
355
|
+
message: "Path for the personas repo:",
|
|
356
|
+
default: personasPath,
|
|
357
|
+
});
|
|
358
|
+
hubDir = path.resolve(customPath);
|
|
359
|
+
continue; // re-validate the new target
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// "here" — user insists, proceed with original path
|
|
363
|
+
return hubDir;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Nudge toward the convention of naming the hub repo "personas".
|
|
369
|
+
*
|
|
370
|
+
* This is an educational moment, not a gate. The user can proceed with any
|
|
371
|
+
* name — but we explain why "personas" is the convention and what they gain
|
|
372
|
+
* by following it.
|
|
373
|
+
*/
|
|
374
|
+
async function nudgePersonasConvention(hubDir: string): Promise<string> {
|
|
375
|
+
const dirName = path.basename(hubDir);
|
|
376
|
+
|
|
377
|
+
// Already named "personas" — nothing to do.
|
|
378
|
+
if (dirName === "personas") return hubDir;
|
|
379
|
+
|
|
380
|
+
console.log(chalk.cyan(
|
|
381
|
+
`\n Convention: name this repo "personas"\n\n`
|
|
382
|
+
) + chalk.gray(
|
|
383
|
+
` AgentBoot follows a convention-over-configuration philosophy. When every\n` +
|
|
384
|
+
` org names their hub repo "personas", several things work automatically:\n\n` +
|
|
385
|
+
` - \`agentboot install\` auto-discovers it by scanning for "personas" in\n` +
|
|
386
|
+
` your GitHub org and sibling directories\n` +
|
|
387
|
+
` - New team members know where to look without being told\n` +
|
|
388
|
+
` - Docs, examples, and community answers all reference the same path\n` +
|
|
389
|
+
` - \`gh repo clone <org>/personas\` works across every AgentBoot org\n\n` +
|
|
390
|
+
` You chose "${dirName}" — that works fine. This is a recommendation,\n` +
|
|
391
|
+
` not a requirement.\n`
|
|
392
|
+
));
|
|
393
|
+
|
|
394
|
+
const choice = await select({
|
|
395
|
+
message: `Keep "${dirName}" or rename to "personas"?`,
|
|
396
|
+
choices: [
|
|
397
|
+
{ name: `Rename to ${path.join(path.dirname(hubDir), "personas")} (recommended)`, value: "rename" },
|
|
398
|
+
{ name: `Keep "${dirName}"`, value: "keep" },
|
|
399
|
+
],
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (choice === "rename") {
|
|
403
|
+
const personasDir = path.join(path.dirname(hubDir), "personas");
|
|
404
|
+
if (fs.existsSync(personasDir)) {
|
|
405
|
+
console.log(chalk.yellow(` ${personasDir} already exists. Keeping "${dirName}".`));
|
|
406
|
+
return hubDir;
|
|
407
|
+
}
|
|
408
|
+
// If the original dir was just created (empty), rename it.
|
|
409
|
+
// If it had content, we can't rename safely — keep it.
|
|
410
|
+
try {
|
|
411
|
+
const entries = fs.readdirSync(hubDir);
|
|
412
|
+
if (entries.length === 0) {
|
|
413
|
+
fs.rmdirSync(hubDir);
|
|
414
|
+
fs.mkdirSync(personasDir, { recursive: true });
|
|
415
|
+
return personasDir;
|
|
416
|
+
} else {
|
|
417
|
+
// Directory has content (from scaffold or prior step) — rename via fs.rename
|
|
418
|
+
fs.renameSync(hubDir, personasDir);
|
|
419
|
+
return personasDir;
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
console.log(chalk.yellow(` Could not rename. Keeping "${dirName}".`));
|
|
423
|
+
return hubDir;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return hubDir;
|
|
428
|
+
}
|
|
429
|
+
|
|
273
430
|
// ---------------------------------------------------------------------------
|
|
274
431
|
// Path 1: Create a new personas repo (Architect)
|
|
275
432
|
// ---------------------------------------------------------------------------
|
|
@@ -326,15 +483,18 @@ async function path1CreateHub(cwd: string, opts: InstallOptions, detection: Dete
|
|
|
326
483
|
}
|
|
327
484
|
}
|
|
328
485
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
486
|
+
// Validate target directory — if it exists and has content, offer to create
|
|
487
|
+
// a personas subdirectory instead of scaffolding into an existing repo.
|
|
488
|
+
hubDir = await validateHubTarget(hubDir);
|
|
489
|
+
|
|
490
|
+
// Nudge toward the "personas" naming convention if the user chose a
|
|
491
|
+
// different name. This is educational, not enforced.
|
|
492
|
+
hubDir = await nudgePersonasConvention(hubDir);
|
|
333
493
|
|
|
334
|
-
// Step 1.2: Org detection
|
|
335
|
-
let
|
|
494
|
+
// Step 1.2: Org detection — slug (machine identifier) and display name (human label)
|
|
495
|
+
let orgSlug = opts.org ?? detection.gitOrg ?? null;
|
|
336
496
|
|
|
337
|
-
if (!
|
|
497
|
+
if (!orgSlug) {
|
|
338
498
|
// Try detecting from the hub dir's git remote
|
|
339
499
|
try {
|
|
340
500
|
const gitResult = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
@@ -344,25 +504,39 @@ async function path1CreateHub(cwd: string, opts: InstallOptions, detection: Dete
|
|
|
344
504
|
});
|
|
345
505
|
if (gitResult.stdout) {
|
|
346
506
|
const match = gitResult.stdout.trim().match(/[/:]([\w.-]+)\//);
|
|
347
|
-
if (match)
|
|
507
|
+
if (match) orgSlug = match[1]!;
|
|
348
508
|
}
|
|
349
509
|
} catch { /* no git */ }
|
|
350
510
|
}
|
|
351
511
|
|
|
352
|
-
if (
|
|
512
|
+
if (orgSlug) {
|
|
353
513
|
const useDetected = await confirm({
|
|
354
|
-
message: `Use "${
|
|
514
|
+
message: `Use "${orgSlug}" as your org identifier?`,
|
|
355
515
|
default: true,
|
|
356
516
|
});
|
|
357
517
|
if (!useDetected) {
|
|
358
|
-
|
|
518
|
+
orgSlug = await input({ message: "Org identifier (lowercase, used in package names and paths):" });
|
|
359
519
|
}
|
|
360
520
|
} else {
|
|
361
|
-
|
|
362
|
-
message: "Org
|
|
521
|
+
orgSlug = await input({
|
|
522
|
+
message: "Org identifier (GitHub org, username, or slug — lowercase, no spaces):",
|
|
363
523
|
});
|
|
364
524
|
}
|
|
365
525
|
|
|
526
|
+
// Normalize slug: lowercase, replace spaces with hyphens
|
|
527
|
+
orgSlug = orgSlug.toLowerCase().replace(/\s+/g, "-");
|
|
528
|
+
|
|
529
|
+
// Derive a default display name from the slug
|
|
530
|
+
const defaultDisplayName = orgSlug
|
|
531
|
+
.split(/[-_]/)
|
|
532
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
533
|
+
.join(" ");
|
|
534
|
+
|
|
535
|
+
const orgDisplayName = await input({
|
|
536
|
+
message: "Org display name (shown in compiled output):",
|
|
537
|
+
default: defaultDisplayName,
|
|
538
|
+
});
|
|
539
|
+
|
|
366
540
|
// Step 1.3: Scan for existing content nearby (with permission)
|
|
367
541
|
const shouldScan = await confirm({
|
|
368
542
|
message: "Scan nearby directories for existing AI agent content?",
|
|
@@ -370,7 +544,7 @@ async function path1CreateHub(cwd: string, opts: InstallOptions, detection: Dete
|
|
|
370
544
|
});
|
|
371
545
|
|
|
372
546
|
if (shouldScan) {
|
|
373
|
-
const siblings =
|
|
547
|
+
const siblings = scanNearby(hubDir !== cwd ? cwd : hubDir);
|
|
374
548
|
const claudeSiblings = siblings.filter(s => s.type === "claude");
|
|
375
549
|
|
|
376
550
|
if (claudeSiblings.length > 0) {
|
|
@@ -390,9 +564,9 @@ async function path1CreateHub(cwd: string, opts: InstallOptions, detection: Dete
|
|
|
390
564
|
}
|
|
391
565
|
|
|
392
566
|
// Step 1.4: Scaffold
|
|
393
|
-
console.log(chalk.bold(`\n Creating personas repo for ${
|
|
567
|
+
console.log(chalk.bold(`\n Creating personas repo for ${orgDisplayName}...\n`));
|
|
394
568
|
|
|
395
|
-
scaffoldHub(hubDir,
|
|
569
|
+
scaffoldHub(hubDir, orgSlug, orgDisplayName);
|
|
396
570
|
|
|
397
571
|
console.log(chalk.green(" Source code:"));
|
|
398
572
|
console.log(chalk.gray(" core/personas/ 4 personas (code-reviewer, security-reviewer, ...)"));
|
|
@@ -400,29 +574,72 @@ async function path1CreateHub(cwd: string, opts: InstallOptions, detection: Dete
|
|
|
400
574
|
console.log(chalk.gray(" core/instructions/ 2 always-on instruction sets"));
|
|
401
575
|
console.log(chalk.gray(" core/gotchas/ (empty — add domain knowledge here)"));
|
|
402
576
|
console.log(chalk.green("\n Build configuration:"));
|
|
403
|
-
console.log(chalk.gray(` agentboot.config.json org: "${
|
|
577
|
+
console.log(chalk.gray(` agentboot.config.json org: "${orgSlug}", displayName: "${orgDisplayName}"`));
|
|
404
578
|
console.log(chalk.gray(" repos.json (empty — register your repos here)"));
|
|
405
579
|
|
|
406
|
-
// Step 1.5:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
580
|
+
// Step 1.5: Build
|
|
581
|
+
//
|
|
582
|
+
// AgentBoot is a build tool. The personas repo contains source code (traits,
|
|
583
|
+
// personas, instructions) that gets compiled into deployable output. This is
|
|
584
|
+
// like compiling TypeScript to JavaScript — the source is what you edit, the
|
|
585
|
+
// output is what gets deployed.
|
|
586
|
+
//
|
|
587
|
+
// If the user is running `agentboot install`, then agentboot is already
|
|
588
|
+
// available (globally or via npx). We can always attempt a build.
|
|
589
|
+
|
|
590
|
+
let buildSucceeded = false;
|
|
591
|
+
const shouldBuild = await confirm({
|
|
592
|
+
message: "Compile personas now? (builds the deployable output)",
|
|
593
|
+
default: true,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
if (shouldBuild) {
|
|
597
|
+
buildSucceeded = runBuild(hubDir);
|
|
410
598
|
} else {
|
|
411
|
-
console.log(chalk.gray(
|
|
599
|
+
console.log(chalk.gray(
|
|
600
|
+
"\n You can compile later by running:\n\n" +
|
|
601
|
+
` cd ${hubDir}\n` +
|
|
602
|
+
" agentboot build\n"
|
|
603
|
+
));
|
|
412
604
|
}
|
|
413
605
|
|
|
414
606
|
// Step 1.6: Register first repo (optional)
|
|
607
|
+
//
|
|
608
|
+
// A "target repo" is any codebase where you want AI agent governance.
|
|
609
|
+
// Registering it adds it to repos.json — the list of repos that receive
|
|
610
|
+
// compiled personas when you run `agentboot sync`.
|
|
611
|
+
//
|
|
612
|
+
// The personas repo and target repos can be anywhere on your filesystem.
|
|
613
|
+
// They don't need to be siblings or in the same parent directory.
|
|
614
|
+
|
|
615
|
+
let registeredRepo = false;
|
|
616
|
+
let registeredRepoName = "";
|
|
617
|
+
let registeredRepoPath = "";
|
|
618
|
+
|
|
619
|
+
console.log(chalk.bold("\n Register a target repo\n"));
|
|
620
|
+
console.log(chalk.gray(
|
|
621
|
+
" A target repo is any codebase where you want AgentBoot personas deployed.\n" +
|
|
622
|
+
" It can be anywhere on your filesystem — it does not need to be next to\n" +
|
|
623
|
+
" this personas repo.\n"
|
|
624
|
+
));
|
|
625
|
+
|
|
415
626
|
const registerRepo = await confirm({
|
|
416
627
|
message: "Register your first target repo now?",
|
|
417
628
|
default: true,
|
|
418
629
|
});
|
|
419
630
|
|
|
420
631
|
if (registerRepo) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
632
|
+
let promptOpts: { message: string; default?: string };
|
|
633
|
+
if (detection.looksLikeCodeRepo) {
|
|
634
|
+
promptOpts = {
|
|
635
|
+
message: `Path to target repo (absolute or relative):`,
|
|
636
|
+
default: cwd,
|
|
637
|
+
};
|
|
638
|
+
} else {
|
|
639
|
+
promptOpts = { message: "Path to target repo (absolute or relative):" };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const repoPathInput = await input(promptOpts);
|
|
426
643
|
const repoPath = path.resolve(repoPathInput);
|
|
427
644
|
|
|
428
645
|
if (!fs.existsSync(repoPath)) {
|
|
@@ -452,57 +669,127 @@ async function path1CreateHub(cwd: string, opts: InstallOptions, detection: Dete
|
|
|
452
669
|
repos.push({ path: repoPath, label: repoName });
|
|
453
670
|
fs.writeFileSync(reposJsonPath, JSON.stringify(repos, null, 2) + "\n", "utf-8");
|
|
454
671
|
console.log(chalk.green(`\n Added ${repoName} to repos.json.`));
|
|
672
|
+
registeredRepo = true;
|
|
673
|
+
registeredRepoName = repoName;
|
|
674
|
+
registeredRepoPath = repoPath;
|
|
455
675
|
|
|
456
676
|
// Check for existing .claude/ content
|
|
457
677
|
if (fs.existsSync(path.join(repoPath, ".claude"))) {
|
|
458
678
|
console.log(chalk.gray(
|
|
459
|
-
|
|
460
|
-
`
|
|
461
|
-
` agentboot uninstall
|
|
679
|
+
`\n This repo has existing .claude/ content. On first sync, AgentBoot\n` +
|
|
680
|
+
` will archive it to .claude/.agentboot-archive/ before deploying.\n` +
|
|
681
|
+
` You can restore the original content anytime with: agentboot uninstall`
|
|
462
682
|
));
|
|
463
683
|
}
|
|
464
684
|
|
|
465
|
-
// Offer to sync
|
|
466
|
-
if (!opts.noSync &&
|
|
685
|
+
// Offer to sync — only if build succeeded (dist/ exists)
|
|
686
|
+
if (!opts.noSync && buildSucceeded && fs.existsSync(path.join(hubDir, "dist"))) {
|
|
687
|
+
console.log(chalk.gray(
|
|
688
|
+
`\n Sync deploys the compiled personas to ${repoName}'s .claude/ directory.\n` +
|
|
689
|
+
` This writes files locally — it does not commit or push. You review\n` +
|
|
690
|
+
` the output before committing.`
|
|
691
|
+
));
|
|
692
|
+
|
|
467
693
|
const shouldSync = await confirm({
|
|
468
|
-
message:
|
|
694
|
+
message: `Deploy personas to ${repoName} now?`,
|
|
469
695
|
default: true,
|
|
470
696
|
});
|
|
471
697
|
|
|
472
698
|
if (shouldSync) {
|
|
473
699
|
console.log(chalk.cyan("\n Syncing..."));
|
|
474
700
|
if (runSync(hubDir)) {
|
|
475
|
-
console.log(chalk.green(
|
|
701
|
+
console.log(chalk.green(`\n Personas deployed to ${repoPath}/.claude/`));
|
|
702
|
+
console.log(chalk.gray(
|
|
703
|
+
`\n To activate them, commit the .claude/ directory:\n\n` +
|
|
704
|
+
` cd ${repoPath}\n` +
|
|
705
|
+
` git add .claude/\n` +
|
|
706
|
+
` git commit -m "chore: deploy AgentBoot personas"\n\n` +
|
|
707
|
+
` Then open Claude Code in that repo and try: /review-code`
|
|
708
|
+
));
|
|
476
709
|
}
|
|
477
710
|
}
|
|
711
|
+
} else if (!buildSucceeded) {
|
|
712
|
+
console.log(chalk.gray(
|
|
713
|
+
`\n Repo registered. To deploy personas, build first:\n\n` +
|
|
714
|
+
` cd ${hubDir}\n` +
|
|
715
|
+
` agentboot build && agentboot sync`
|
|
716
|
+
));
|
|
478
717
|
}
|
|
479
718
|
}
|
|
480
719
|
}
|
|
481
720
|
|
|
482
|
-
// Step 1.7:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
));
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
));
|
|
494
|
-
console.log(chalk.gray(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
721
|
+
// Step 1.7: Summary and next steps
|
|
722
|
+
//
|
|
723
|
+
// Context-aware: the summary reflects what actually happened during install,
|
|
724
|
+
// so the user knows exactly where they are and what to do next.
|
|
725
|
+
|
|
726
|
+
console.log(chalk.bold("\n ─────────────────────────────────────────────"));
|
|
727
|
+
console.log(chalk.bold(`\n ${chalk.green("✓")} AgentBoot setup complete\n`));
|
|
728
|
+
|
|
729
|
+
// What was created
|
|
730
|
+
console.log(chalk.cyan(" What was created:\n"));
|
|
731
|
+
console.log(chalk.gray(` Personas repo: ${hubDir}`));
|
|
732
|
+
console.log(chalk.gray(` Config: ${hubDir}/agentboot.config.json`));
|
|
733
|
+
console.log(chalk.gray(` Org: ${orgSlug} (${orgDisplayName})`));
|
|
734
|
+
if (buildSucceeded) {
|
|
735
|
+
console.log(chalk.gray(` Compiled output: ${hubDir}/dist/`));
|
|
736
|
+
}
|
|
737
|
+
if (registeredRepo) {
|
|
738
|
+
console.log(chalk.gray(` Target repo: ${registeredRepoPath} (${registeredRepoName})`));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Remote status
|
|
742
|
+
let hubHasRemote = false;
|
|
743
|
+
try {
|
|
744
|
+
const remoteResult = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
745
|
+
cwd: hubDir,
|
|
746
|
+
encoding: "utf-8",
|
|
747
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
748
|
+
});
|
|
749
|
+
hubHasRemote = remoteResult.status === 0 && !!remoteResult.stdout?.trim();
|
|
750
|
+
} catch { /* no git or no remote */ }
|
|
751
|
+
|
|
752
|
+
if (!hubHasRemote) {
|
|
753
|
+
console.log(chalk.gray(" Remote: none (local only — fine for evaluation)"));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Context-aware next steps
|
|
757
|
+
console.log(chalk.cyan("\n What to do next:\n"));
|
|
758
|
+
|
|
759
|
+
let step = 1;
|
|
760
|
+
|
|
761
|
+
if (!buildSucceeded) {
|
|
762
|
+
console.log(chalk.gray(` ${step}. Build personas: cd ${hubDir} && agentboot build`));
|
|
763
|
+
step++;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (!registeredRepo) {
|
|
767
|
+
console.log(chalk.gray(` ${step}. Register a repo: agentboot install (from your code repo)`));
|
|
768
|
+
console.log(chalk.gray(` Or edit: ${hubDir}/repos.json`));
|
|
769
|
+
step++;
|
|
770
|
+
} else if (buildSucceeded && !fs.existsSync(path.join(registeredRepoPath, ".claude", ".agentboot-manifest.json"))) {
|
|
771
|
+
console.log(chalk.gray(` ${step}. Deploy personas: cd ${hubDir} && agentboot sync`));
|
|
772
|
+
step++;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
console.log(chalk.gray(` ${step}. Try it out: Open your repo in Claude Code and run /review-code`));
|
|
776
|
+
step++;
|
|
777
|
+
console.log(chalk.gray(` ${step}. Customize: Edit personas in ${hubDir}/core/personas/`));
|
|
778
|
+
step++;
|
|
779
|
+
console.log(chalk.gray(` ${step}. Import existing: agentboot import --path <dir>`));
|
|
780
|
+
step++;
|
|
781
|
+
|
|
782
|
+
if (!hubHasRemote) {
|
|
783
|
+
console.log(chalk.gray(` ${step}. Push when ready: gh repo create ${orgSlug}/personas --source . --private --push`));
|
|
784
|
+
step++;
|
|
785
|
+
}
|
|
498
786
|
|
|
499
|
-
//
|
|
500
|
-
console.log(chalk.
|
|
501
|
-
console.log(chalk.gray("
|
|
502
|
-
console.log(chalk.gray("
|
|
503
|
-
console.log(chalk.gray("
|
|
504
|
-
console.log(
|
|
505
|
-
console.log(chalk.gray(" 4. Set up CI: agentboot validate --strict in your pipeline\n"));
|
|
787
|
+
// Governance — brief, not a wall
|
|
788
|
+
console.log(chalk.cyan("\n Governance tips:\n"));
|
|
789
|
+
console.log(chalk.gray(" - Enable branch protection on main (persona changes deserve review)"));
|
|
790
|
+
console.log(chalk.gray(" - Add `agentboot validate --strict` to CI"));
|
|
791
|
+
console.log(chalk.gray(" - Encourage developers to contribute — they know the prompts best"));
|
|
792
|
+
console.log("");
|
|
506
793
|
}
|
|
507
794
|
|
|
508
795
|
// ---------------------------------------------------------------------------
|
|
@@ -517,7 +804,7 @@ async function path2ConnectToHub(cwd: string, opts: InstallOptions, detection: D
|
|
|
517
804
|
console.log(chalk.gray("\n Looking for your org's personas repo...\n"));
|
|
518
805
|
|
|
519
806
|
// Check siblings
|
|
520
|
-
const siblings =
|
|
807
|
+
const siblings = scanNearby(cwd);
|
|
521
808
|
const hubSiblings = siblings.filter(s => s.type === "hub");
|
|
522
809
|
|
|
523
810
|
if (hubSiblings.length === 1) {
|
package/scripts/sync.ts
CHANGED
|
@@ -595,15 +595,21 @@ function validateRepoEntry(entry: RepoEntry, config: AgentBootConfig): string[]
|
|
|
595
595
|
);
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
-
// Validate repo path safety
|
|
598
|
+
// Validate repo path safety — resolve symlinks to check the real target
|
|
599
599
|
const resolvedPath = path.resolve(entry.path);
|
|
600
|
+
let realPath = resolvedPath;
|
|
601
|
+
try {
|
|
602
|
+
if (fs.existsSync(resolvedPath)) {
|
|
603
|
+
realPath = fs.realpathSync(resolvedPath);
|
|
604
|
+
}
|
|
605
|
+
} catch { /* permission denied or broken symlink — use resolved path */ }
|
|
600
606
|
const dangerousPaths = ["/", "/etc", "/usr", "/var", "/tmp", "/home", "/root", "/bin", "/sbin", "/lib", "/opt"];
|
|
601
|
-
if (dangerousPaths.includes(
|
|
607
|
+
if (dangerousPaths.includes(realPath)) {
|
|
602
608
|
errors.push(
|
|
603
|
-
`[${label}] Repo path "${
|
|
609
|
+
`[${label}] Repo path "${realPath}" resolves to a system directory — refusing to sync`
|
|
604
610
|
);
|
|
605
611
|
}
|
|
606
|
-
if (fs.existsSync(
|
|
612
|
+
if (fs.existsSync(realPath) && !fs.existsSync(path.join(realPath, ".git"))) {
|
|
607
613
|
// Warn but don't block — temp dirs in tests and some workflows don't have .git
|
|
608
614
|
console.warn(
|
|
609
615
|
chalk.yellow(` ⚠ [${label}] Repo path "${resolvedPath}" has no .git directory — is this a git repo?`)
|
package/scripts/validate.ts
CHANGED
|
@@ -298,7 +298,7 @@ function checkSkillFrontmatter(config: AgentBootConfig, configDir: string): Chec
|
|
|
298
298
|
* Detect regex patterns likely to cause catastrophic backtracking.
|
|
299
299
|
* Rejects patterns with nested quantifiers like (a+)+, (a*)*b, etc.
|
|
300
300
|
*/
|
|
301
|
-
function isUnsafeRegex(pattern: string): boolean {
|
|
301
|
+
export function isUnsafeRegex(pattern: string): boolean {
|
|
302
302
|
// Reject patterns longer than 200 chars
|
|
303
303
|
if (pattern.length > 200) return true;
|
|
304
304
|
// Reject nested quantifiers: (x+)+, (x*)+, (x+)*, (x{n,})+, etc.
|
|
@@ -308,7 +308,7 @@ function isUnsafeRegex(pattern: string): boolean {
|
|
|
308
308
|
return false;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
function buildSecretPatterns(config: AgentBootConfig): RegExp[] {
|
|
311
|
+
export function buildSecretPatterns(config: AgentBootConfig): RegExp[] {
|
|
312
312
|
const configPatterns: RegExp[] = [];
|
|
313
313
|
for (const p of config.validation?.secretPatterns ?? []) {
|
|
314
314
|
if (isUnsafeRegex(p)) {
|
|
@@ -438,7 +438,11 @@ async function main(): Promise<void> {
|
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
main()
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
// Only run main() when executed directly, not when imported for testing
|
|
442
|
+
const isDirectRun = process.argv[1]?.includes("validate");
|
|
443
|
+
if (isDirectRun) {
|
|
444
|
+
main().catch((err: unknown) => {
|
|
445
|
+
console.error(chalk.red("Unexpected error:"), err);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
@@ -75,7 +75,7 @@ function Features() {
|
|
|
75
75
|
/>
|
|
76
76
|
<Feature
|
|
77
77
|
title="Convention Over Configuration"
|
|
78
|
-
description="Sensible defaults for everything. Edit one config file.
|
|
78
|
+
description="Sensible defaults for everything. Edit one config file. Bootstrap your agentic development teams."
|
|
79
79
|
/>
|
|
80
80
|
</div>
|
|
81
81
|
</section>
|