agentboot 0.4.3 → 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 +94 -22
- 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 +339 -72
- 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
|
@@ -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
|
|
@@ -694,15 +694,16 @@ program
|
|
|
694
694
|
}
|
|
695
695
|
if (!isJson) console.log(chalk.bold("\nAgentBoot — doctor\n"));
|
|
696
696
|
const cwd = process.cwd();
|
|
697
|
-
let
|
|
697
|
+
let issuesFound = 0;
|
|
698
|
+
let issuesFixed = 0;
|
|
698
699
|
|
|
699
700
|
interface DoctorCheck { name: string; status: "ok" | "fail" | "warn"; message: string; fixable?: boolean; fixed?: boolean }
|
|
700
701
|
const checks: DoctorCheck[] = [];
|
|
701
702
|
|
|
702
703
|
function ok(msg: string) { checks.push({ name: msg, status: "ok", message: msg }); if (!isJson) console.log(` ${chalk.green("✓")} ${msg}`); }
|
|
703
|
-
function fail(msg: string, fixable = false) {
|
|
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)") : ""}`); }
|
|
704
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)") : ""}`); }
|
|
705
|
-
function fixed(msg: string) { checks.push({ name: msg, status: "ok", message: msg, fixed: true }); if (!isJson) console.log(` ${chalk.green("✓")} ${msg} ${chalk.cyan(dryRun ? "(would fix)" : "(fixed)")}`); }
|
|
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)")}`); }
|
|
706
707
|
|
|
707
708
|
// 1. Environment
|
|
708
709
|
if (!isJson) console.log(chalk.cyan("Environment"));
|
|
@@ -733,6 +734,12 @@ program
|
|
|
733
734
|
const config = loadConfig(configPath);
|
|
734
735
|
ok(`Config parses successfully (org: ${config.org})`);
|
|
735
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
|
+
|
|
736
743
|
// Helper: generate a minimal SKILL.md scaffold
|
|
737
744
|
function scaffoldSkillMd(name: string): string {
|
|
738
745
|
return [
|
|
@@ -756,6 +763,7 @@ program
|
|
|
756
763
|
const enabledPersonas = config.personas?.enabled ?? [];
|
|
757
764
|
const personasDir = path.join(cwd, "core", "personas");
|
|
758
765
|
let personaIssues = 0;
|
|
766
|
+
let personasScaffolded = 0;
|
|
759
767
|
for (const p of enabledPersonas) {
|
|
760
768
|
const pDir = path.join(personasDir, p);
|
|
761
769
|
if (!fs.existsSync(pDir)) {
|
|
@@ -766,6 +774,7 @@ program
|
|
|
766
774
|
const personaConfig = { traits: config.traits?.enabled ?? [] };
|
|
767
775
|
fs.writeFileSync(path.join(pDir, "persona.config.json"), JSON.stringify(personaConfig, null, 2) + "\n", "utf-8");
|
|
768
776
|
}
|
|
777
|
+
personasScaffolded++;
|
|
769
778
|
fixed(`Scaffolded persona: ${p}`);
|
|
770
779
|
} else {
|
|
771
780
|
personaIssues++; fail(`Persona not found: ${p}`, true);
|
|
@@ -775,18 +784,24 @@ program
|
|
|
775
784
|
if (!dryRun) {
|
|
776
785
|
fs.writeFileSync(path.join(pDir, "SKILL.md"), scaffoldSkillMd(p), "utf-8");
|
|
777
786
|
}
|
|
787
|
+
personasScaffolded++;
|
|
778
788
|
fixed(`Created missing SKILL.md for: ${p}`);
|
|
779
789
|
} else {
|
|
780
790
|
personaIssues++; fail(`Missing SKILL.md: ${p}`, true);
|
|
781
791
|
}
|
|
782
792
|
}
|
|
783
793
|
}
|
|
784
|
-
if (personaIssues === 0
|
|
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)`);
|
|
798
|
+
}
|
|
785
799
|
|
|
786
800
|
// Check traits
|
|
787
801
|
const enabledTraits = config.traits?.enabled ?? [];
|
|
788
802
|
const traitsDir = path.join(cwd, "core", "traits");
|
|
789
803
|
let traitIssues = 0;
|
|
804
|
+
let traitsScaffolded = 0;
|
|
790
805
|
for (const t of enabledTraits) {
|
|
791
806
|
if (!fs.existsSync(path.join(traitsDir, `${t}.md`))) {
|
|
792
807
|
if (fixMode) {
|
|
@@ -795,13 +810,18 @@ program
|
|
|
795
810
|
const traitContent = `# ${t}\n\nTODO: Define this trait.\n`;
|
|
796
811
|
fs.writeFileSync(path.join(traitsDir, `${t}.md`), traitContent, "utf-8");
|
|
797
812
|
}
|
|
813
|
+
traitsScaffolded++;
|
|
798
814
|
fixed(`Created missing trait: ${t}.md`);
|
|
799
815
|
} else {
|
|
800
816
|
traitIssues++; fail(`Trait not found: ${t}`, true);
|
|
801
817
|
}
|
|
802
818
|
}
|
|
803
819
|
}
|
|
804
|
-
if (traitIssues === 0
|
|
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
|
+
}
|
|
805
825
|
|
|
806
826
|
// Check core directories
|
|
807
827
|
const coreDirs = ["core/personas", "core/traits", "core/instructions", "core/gotchas"];
|
|
@@ -865,14 +885,16 @@ program
|
|
|
865
885
|
|
|
866
886
|
if (!isJson) console.log("");
|
|
867
887
|
|
|
888
|
+
const issuesRemaining = issuesFound - issuesFixed;
|
|
889
|
+
|
|
868
890
|
if (isJson) {
|
|
869
|
-
console.log(JSON.stringify({ issues, checks }, null, 2));
|
|
870
|
-
process.exit(
|
|
891
|
+
console.log(JSON.stringify({ issues: issuesRemaining, issuesFound, issuesFixed, checks }, null, 2));
|
|
892
|
+
process.exit(issuesRemaining > 0 ? 1 : 0);
|
|
871
893
|
}
|
|
872
894
|
|
|
873
|
-
if (
|
|
895
|
+
if (issuesRemaining > 0) {
|
|
874
896
|
const fixableCount = checks.filter(c => c.fixable && !c.fixed).length;
|
|
875
|
-
console.log(chalk.bold(chalk.red(`✗ ${
|
|
897
|
+
console.log(chalk.bold(chalk.red(`✗ ${issuesRemaining} issue${issuesRemaining !== 1 ? "s" : ""} found`)));
|
|
876
898
|
if (fixableCount > 0) {
|
|
877
899
|
console.log(chalk.gray(` ${fixableCount} fixable — run \`agentboot doctor --fix\`\n`));
|
|
878
900
|
} else {
|
|
@@ -880,9 +902,8 @@ program
|
|
|
880
902
|
}
|
|
881
903
|
process.exit(1);
|
|
882
904
|
} else {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
console.log(chalk.bold(chalk.green(`✓ All checks passed (${fixedCount} issue${fixedCount !== 1 ? "s" : ""} ${dryRun ? "would be " : ""}fixed)\n`)));
|
|
905
|
+
if (issuesFixed > 0) {
|
|
906
|
+
console.log(chalk.bold(chalk.green(`✓ All checks passed (${issuesFixed} issue${issuesFixed !== 1 ? "s" : ""} ${dryRun ? "would be " : ""}fixed)\n`)));
|
|
886
907
|
} else {
|
|
887
908
|
console.log(chalk.bold(chalk.green("✓ All checks passed\n")));
|
|
888
909
|
}
|
|
@@ -1374,9 +1395,9 @@ program
|
|
|
1374
1395
|
|
|
1375
1396
|
program
|
|
1376
1397
|
.command("config")
|
|
1377
|
-
.description("
|
|
1378
|
-
.argument("[key]", "config key (e.g., personas.enabled)")
|
|
1379
|
-
.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)")
|
|
1380
1401
|
.action((key: string | undefined, value: string | undefined, _opts, cmd) => {
|
|
1381
1402
|
const globalOpts = cmd.optsWithGlobals();
|
|
1382
1403
|
const cwd = process.cwd();
|
|
@@ -1413,10 +1434,61 @@ program
|
|
|
1413
1434
|
process.exit(0);
|
|
1414
1435
|
}
|
|
1415
1436
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
+
}
|
|
1420
1492
|
});
|
|
1421
1493
|
|
|
1422
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, scanPath };
|
|
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);
|
|
333
489
|
|
|
334
|
-
//
|
|
335
|
-
|
|
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);
|
|
336
493
|
|
|
337
|
-
|
|
494
|
+
// Step 1.2: Org detection — slug (machine identifier) and display name (human label)
|
|
495
|
+
let orgSlug = opts.org ?? detection.gitOrg ?? null;
|
|
496
|
+
|
|
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,51 +669,76 @@ 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
|
+
}
|
|
498
740
|
|
|
499
|
-
//
|
|
741
|
+
// Remote status
|
|
500
742
|
let hubHasRemote = false;
|
|
501
743
|
try {
|
|
502
744
|
const remoteResult = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
@@ -507,22 +749,47 @@ async function path1CreateHub(cwd: string, opts: InstallOptions, detection: Dete
|
|
|
507
749
|
hubHasRemote = remoteResult.status === 0 && !!remoteResult.stdout?.trim();
|
|
508
750
|
} catch { /* no git or no remote */ }
|
|
509
751
|
|
|
510
|
-
|
|
511
|
-
|
|
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++;
|
|
512
781
|
|
|
513
782
|
if (!hubHasRemote) {
|
|
514
|
-
console.log(chalk.
|
|
515
|
-
|
|
516
|
-
" Everything works locally. When your org is ready:\n\n" +
|
|
517
|
-
" gh repo create <org>/personas --source . --private --push\n"
|
|
518
|
-
));
|
|
783
|
+
console.log(chalk.gray(` ${step}. Push when ready: gh repo create ${orgSlug}/personas --source . --private --push`));
|
|
784
|
+
step++;
|
|
519
785
|
}
|
|
520
786
|
|
|
521
|
-
|
|
522
|
-
console.log(chalk.
|
|
523
|
-
console.log(chalk.gray("
|
|
524
|
-
console.log(chalk.gray("
|
|
525
|
-
console.log(chalk.gray("
|
|
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("");
|
|
526
793
|
}
|
|
527
794
|
|
|
528
795
|
// ---------------------------------------------------------------------------
|
|
@@ -537,7 +804,7 @@ async function path2ConnectToHub(cwd: string, opts: InstallOptions, detection: D
|
|
|
537
804
|
console.log(chalk.gray("\n Looking for your org's personas repo...\n"));
|
|
538
805
|
|
|
539
806
|
// Check siblings
|
|
540
|
-
const siblings =
|
|
807
|
+
const siblings = scanNearby(cwd);
|
|
541
808
|
const hubSiblings = siblings.filter(s => s.type === "hub");
|
|
542
809
|
|
|
543
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>
|