@basou/cli 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +651 -33
- package/dist/index.js.map +1 -1
- package/dist/program.js +651 -33
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -3658,13 +3658,17 @@ import {
|
|
|
3658
3658
|
readMarkdownFile as readMarkdownFile4,
|
|
3659
3659
|
reconcileSourceRoots,
|
|
3660
3660
|
removeMarkerSection,
|
|
3661
|
+
renderAnchorStarter,
|
|
3662
|
+
renderViewPresetBlock,
|
|
3661
3663
|
renderWithMarkers as renderWithMarkers4,
|
|
3662
3664
|
resolveRepositoryRoot as resolveRepositoryRoot8,
|
|
3663
3665
|
safeSimpleGit,
|
|
3666
|
+
seedMarkers,
|
|
3664
3667
|
summarizePresetPlan,
|
|
3665
3668
|
summarizeRosterDrift,
|
|
3666
3669
|
summarizeSymlinkPlan,
|
|
3667
3670
|
summarizeWiring,
|
|
3671
|
+
summarizeWiringDrift,
|
|
3668
3672
|
unknownManifestKeys,
|
|
3669
3673
|
writeManifest as writeManifest2,
|
|
3670
3674
|
writeMarkdownFile as writeMarkdownFile5
|
|
@@ -3674,7 +3678,7 @@ var CANONICAL_FILE = "AGENTS.md";
|
|
|
3674
3678
|
function registerProjectCommand(program) {
|
|
3675
3679
|
const project = program.command("project").description("Inspect a project's declared repo roster (read-only)");
|
|
3676
3680
|
project.command("check").description(
|
|
3677
|
-
"
|
|
3681
|
+
"Surface project drift (read-only, advisory): the declared repo roster (manifest `repos`) vs the capture config (`source_roots`), AND the instruction-file wiring vs basou's native topology \u2014 a missing AGENTS.md canonical (repo or workspace view), incomplete spokes, conflicts, or collisions"
|
|
3678
3682
|
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
3679
3683
|
await runProjectCheck(opts);
|
|
3680
3684
|
});
|
|
@@ -3749,8 +3753,11 @@ function registerProjectCommand(program) {
|
|
|
3749
3753
|
project.command("new").argument("[repos...]", "Extra repo paths (besides the anchor) to seed into the roster").description(
|
|
3750
3754
|
"Scaffold a new project from scratch at the current Git repository (the anchor): create `.basou/` and seed the manifest with a candidate `repos` roster (the anchor plus any given repos, which must already be git repositories) and a `workspace.view` placeholder. Dry-run by default; pass --apply to write. Pass --no-view for a solo project. The greenfield entry point \u2014 declare visibility/language per repo afterward, then run `basou project derive --apply` to materialize the wiring"
|
|
3751
3755
|
).option("--apply", "Create `.basou/` and write the seeded manifest (default: dry-run preview)").option(
|
|
3756
|
+
"--project-name <name>",
|
|
3757
|
+
"The project's product name (a simple name \u2014 letters, digits, '.', '-', '_'): sets `project.name` and the default view path (a `<name>-workspace` sibling). Omit to fall back to the anchor directory name; use --view for a custom view path"
|
|
3758
|
+
).option(
|
|
3752
3759
|
"--view <path>",
|
|
3753
|
-
"Override the workspace view path (default: a
|
|
3760
|
+
"Override the workspace view path (default: a `<name>-workspace` sibling, where <name> is --project-name or the anchor directory name)"
|
|
3754
3761
|
).option("--no-view", "Solo project: declare no workspace view").option(
|
|
3755
3762
|
"--local-only",
|
|
3756
3763
|
"Write a .basou/ full-exclude .gitignore block (keep the trail out of version control) instead of the default ignore+commit block"
|
|
@@ -3763,10 +3770,10 @@ function registerProjectCommand(program) {
|
|
|
3763
3770
|
await runProjectDerive(opts);
|
|
3764
3771
|
});
|
|
3765
3772
|
project.command("retrofit").argument(
|
|
3766
|
-
"
|
|
3767
|
-
"The declared roster repo whose hand-authored AGENTS.md to relocate (e.g. ../foo)"
|
|
3773
|
+
"[repo]",
|
|
3774
|
+
"The declared roster repo whose hand-authored AGENTS.md to relocate (e.g. ../foo). Omit to run only the workspace view's canonical auto-migration"
|
|
3768
3775
|
).description(
|
|
3769
|
-
"Fold an existing repo's hand-authored AGENTS.md into the project topology: move the repo's regular-file `AGENTS.md` to the anchor canonical (`agents/<repo>/AGENTS.md`) and replace it with a symlink, so the prose lives at the single source of truth. Dry-run by default; pass --apply to relocate. The onboarding counterpart to `new` for a repo that already carries its own AGENTS.md \u2014 run it before `basou project derive`, which then adds the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore. Non-destructive: it refuses when the destination canonical already exists (it never clobbers it), and skips a repo whose AGENTS.md is already a symlink or absent. The anchor (`.`) is refused"
|
|
3776
|
+
"Fold an existing repo's hand-authored AGENTS.md into the project topology: move the repo's regular-file `AGENTS.md` to the anchor canonical (`agents/<repo>/AGENTS.md`) and replace it with a symlink, so the prose lives at the single source of truth. Dry-run by default; pass --apply to relocate. The onboarding counterpart to `new` for a repo that already carries its own AGENTS.md \u2014 run it before `basou project derive`, which then adds the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore. Non-destructive: it refuses when the destination canonical already exists (it never clobbers it), and skips a repo whose AGENTS.md is already a symlink or absent. The anchor (`.`) is refused. The workspace view's own canonical is auto-migrated when it is markerless prose (the generated block is prepended, the prose kept): omit the repo argument to perform that migration \u2014 a repo-argument run only reports it"
|
|
3770
3777
|
).option(
|
|
3771
3778
|
"--apply",
|
|
3772
3779
|
"Relocate the AGENTS.md to the canonical and recreate the symlink (default: dry-run preview)"
|
|
@@ -3797,18 +3804,39 @@ async function doRunProjectCheck(options, ctx) {
|
|
|
3797
3804
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project check");
|
|
3798
3805
|
const paths = basouPaths10(repositoryRoot);
|
|
3799
3806
|
const manifest = await readManifest6(paths);
|
|
3800
|
-
const
|
|
3807
|
+
const roster = summarizeRosterDrift({
|
|
3801
3808
|
...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
|
|
3802
3809
|
sourceRoots: effectiveSourceRoots(manifest)
|
|
3803
3810
|
});
|
|
3811
|
+
let wiring;
|
|
3812
|
+
const rosterRepos2 = manifest.repos ?? [];
|
|
3813
|
+
if (rosterRepos2.length > 0) {
|
|
3814
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
3815
|
+
const repoFacts = rosterRepos2.map(
|
|
3816
|
+
(entry) => gatherRepoSymlinks(repositoryRoot, anchorReal, entry)
|
|
3817
|
+
);
|
|
3818
|
+
let view = { kind: "no-view" };
|
|
3819
|
+
const viewPath = manifest.workspace.view;
|
|
3820
|
+
if (viewPath !== void 0) {
|
|
3821
|
+
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
3822
|
+
view = gatherViewSymlinks(repositoryRoot, anchorReal, rosterRepos2, viewDir);
|
|
3823
|
+
}
|
|
3824
|
+
wiring = summarizeWiringDrift({ repos: repoFacts, view });
|
|
3825
|
+
}
|
|
3826
|
+
const result = {
|
|
3827
|
+
roster,
|
|
3828
|
+
...wiring !== void 0 ? { wiring } : {},
|
|
3829
|
+
ok: roster.ok && (wiring?.ok ?? true)
|
|
3830
|
+
};
|
|
3804
3831
|
if (options.json === true) {
|
|
3805
|
-
console.log(JSON.stringify(
|
|
3832
|
+
console.log(JSON.stringify(result));
|
|
3806
3833
|
} else {
|
|
3807
|
-
console.log(renderProjectCheck(
|
|
3834
|
+
console.log(renderProjectCheck(result));
|
|
3808
3835
|
}
|
|
3809
|
-
return
|
|
3836
|
+
return result;
|
|
3810
3837
|
}
|
|
3811
|
-
function renderProjectCheck(
|
|
3838
|
+
function renderProjectCheck(result) {
|
|
3839
|
+
const summary = result.roster;
|
|
3812
3840
|
const lines = [];
|
|
3813
3841
|
lines.push("# Project composition check (declared vs captured)");
|
|
3814
3842
|
lines.push("");
|
|
@@ -3843,8 +3871,58 @@ function renderProjectCheck(summary) {
|
|
|
3843
3871
|
for (const p of summary.extra) lines.push(`- ${p}`);
|
|
3844
3872
|
lines.push("");
|
|
3845
3873
|
}
|
|
3874
|
+
const w = result.wiring;
|
|
3875
|
+
if (w !== void 0) {
|
|
3876
|
+
lines.push("## Instruction-file wiring drift (declared topology vs on-disk)");
|
|
3877
|
+
if (w.ok) {
|
|
3878
|
+
lines.push(
|
|
3879
|
+
"\u2705 Every present repo's and the view's instruction files (AGENTS.md + spokes) are wired as declared."
|
|
3880
|
+
);
|
|
3881
|
+
} else {
|
|
3882
|
+
if (w.missingCanonicals.length > 0) {
|
|
3883
|
+
lines.push(`\u26A0\uFE0F Missing instruction canonical (AGENTS.md): ${w.missingCanonicals.length}`);
|
|
3884
|
+
for (const m of w.missingCanonicals) {
|
|
3885
|
+
const where = m.target === "view" ? `view "${m.name}"` : m.target === "repo-self" ? `${m.name} (self)` : m.name;
|
|
3886
|
+
lines.push(`- ${where} \u2014 canonical AGENTS.md absent`);
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
if (w.incompleteWiring.length > 0) {
|
|
3890
|
+
lines.push(
|
|
3891
|
+
`\u26A0\uFE0F Incomplete wiring (canonical present, spokes missing): ${w.incompleteWiring.length}`
|
|
3892
|
+
);
|
|
3893
|
+
for (const i of w.incompleteWiring) {
|
|
3894
|
+
const where = i.target === "view" ? `view "${i.path}"` : i.path;
|
|
3895
|
+
lines.push(`- ${where}: ${i.files.join(", ")}`);
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
if (w.conflicts.length > 0) {
|
|
3899
|
+
lines.push(
|
|
3900
|
+
`\u26A0\uFE0F Wiring conflicts (existing file/link, left untouched): ${w.conflicts.length}`
|
|
3901
|
+
);
|
|
3902
|
+
for (const c of w.conflicts) {
|
|
3903
|
+
const where = c.target === "view" ? `view "${c.path}"` : c.path;
|
|
3904
|
+
const detail = c.reason === "mismatch" && c.actualTarget !== void 0 ? `mismatch -> ${c.actualTarget}` : c.reason;
|
|
3905
|
+
lines.push(`- ${where} ${c.file}: ${detail}`);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
if (w.collisions.length > 0) {
|
|
3909
|
+
lines.push(`\u26A0\uFE0F Canonical-name collisions (ambiguous, not wired): ${w.collisions.length}`);
|
|
3910
|
+
for (const c of w.collisions) {
|
|
3911
|
+
const who = c.view === true ? `${c.repos.join(", ")} + view` : c.repos.join(", ");
|
|
3912
|
+
lines.push(`- agents/${c.canonicalName}/AGENTS.md <- ${who}`);
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
if (w.unreachable.length > 0) {
|
|
3917
|
+
lines.push(
|
|
3918
|
+
`\u2139\uFE0F Declared repos not present on this machine (advisory \u2014 not counted as drift): ${w.unreachable.length}`
|
|
3919
|
+
);
|
|
3920
|
+
for (const u of w.unreachable) lines.push(`- ${u}`);
|
|
3921
|
+
}
|
|
3922
|
+
lines.push("");
|
|
3923
|
+
}
|
|
3846
3924
|
lines.push(
|
|
3847
|
-
"Note: read-only advisory. It
|
|
3925
|
+
"Note: read-only advisory. It shows the difference between the declaration (repos) and the capture config (source_roots), and the instruction-file wiring vs basou's native topology; it does not enforce or generate."
|
|
3848
3926
|
);
|
|
3849
3927
|
return lines.join("\n");
|
|
3850
3928
|
}
|
|
@@ -4298,6 +4376,17 @@ function inspectSymlink(filePath, expectedTarget) {
|
|
|
4298
4376
|
const actual = readlinkSync(filePath);
|
|
4299
4377
|
return actual === expectedTarget ? { state: "correct" } : { state: "mismatch", actualTarget: actual };
|
|
4300
4378
|
}
|
|
4379
|
+
function anchorCanonicalState(filePath) {
|
|
4380
|
+
let st;
|
|
4381
|
+
try {
|
|
4382
|
+
st = lstatSync(filePath);
|
|
4383
|
+
} catch (error) {
|
|
4384
|
+
if (hasErrorCode(error) && error.code === "ENOENT") return "absent";
|
|
4385
|
+
return "broken";
|
|
4386
|
+
}
|
|
4387
|
+
if (st.isSymbolicLink()) return existsSync(filePath) ? "usable" : "broken";
|
|
4388
|
+
return st.isFile() ? "usable" : "broken";
|
|
4389
|
+
}
|
|
4301
4390
|
function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
4302
4391
|
const mode = instructionMode(entry);
|
|
4303
4392
|
const isSelf = mode === "self";
|
|
@@ -4309,7 +4398,42 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4309
4398
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
4310
4399
|
}
|
|
4311
4400
|
if (real === anchorReal) {
|
|
4312
|
-
|
|
4401
|
+
const anchorCanonical = join9(real, CANONICAL_FILE);
|
|
4402
|
+
const anchorState = anchorCanonicalState(anchorCanonical);
|
|
4403
|
+
if (anchorState === "absent") {
|
|
4404
|
+
return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
|
|
4405
|
+
}
|
|
4406
|
+
if (anchorState === "broken") {
|
|
4407
|
+
return {
|
|
4408
|
+
...base,
|
|
4409
|
+
isAnchor: true,
|
|
4410
|
+
reachable: true,
|
|
4411
|
+
canonicalPresent: true,
|
|
4412
|
+
canonicalName: basename5(real),
|
|
4413
|
+
files: [{ name: CANONICAL_FILE, expectedTarget: CANONICAL_FILE, state: "blocked" }]
|
|
4414
|
+
};
|
|
4415
|
+
}
|
|
4416
|
+
const anchorFiles = expectedSymlinkTargets(
|
|
4417
|
+
real,
|
|
4418
|
+
anchorCanonical,
|
|
4419
|
+
"self"
|
|
4420
|
+
).map((spec) => {
|
|
4421
|
+
const { state, actualTarget } = inspectSymlink(join9(real, spec.name), spec.target);
|
|
4422
|
+
return {
|
|
4423
|
+
name: spec.name,
|
|
4424
|
+
expectedTarget: spec.target,
|
|
4425
|
+
state,
|
|
4426
|
+
...actualTarget !== void 0 ? { actualTarget } : {}
|
|
4427
|
+
};
|
|
4428
|
+
});
|
|
4429
|
+
return {
|
|
4430
|
+
...base,
|
|
4431
|
+
isAnchor: true,
|
|
4432
|
+
reachable: true,
|
|
4433
|
+
canonicalPresent: true,
|
|
4434
|
+
canonicalName: basename5(real),
|
|
4435
|
+
files: anchorFiles
|
|
4436
|
+
};
|
|
4313
4437
|
}
|
|
4314
4438
|
if (!existsSync(join9(real, ".git"))) {
|
|
4315
4439
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
@@ -4363,6 +4487,53 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
4363
4487
|
function failureReason(error) {
|
|
4364
4488
|
return hasErrorCode(error) ? error.code : "unknown error";
|
|
4365
4489
|
}
|
|
4490
|
+
function viewCanonicalCollision(repositoryRoot, roster, viewName) {
|
|
4491
|
+
for (const entry of roster) {
|
|
4492
|
+
let name;
|
|
4493
|
+
try {
|
|
4494
|
+
name = basename5(realpathSync(resolve7(repositoryRoot, entry.path)));
|
|
4495
|
+
} catch {
|
|
4496
|
+
name = basename5(resolve7(repositoryRoot, entry.path));
|
|
4497
|
+
}
|
|
4498
|
+
if (name === viewName) return entry.path;
|
|
4499
|
+
}
|
|
4500
|
+
return void 0;
|
|
4501
|
+
}
|
|
4502
|
+
function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
|
|
4503
|
+
const viewName = basename5(viewDir);
|
|
4504
|
+
const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
|
|
4505
|
+
if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
|
|
4506
|
+
const canonicalFile = canonicalFileFor(anchorReal, viewName);
|
|
4507
|
+
if (!existsSync(canonicalFile)) return { kind: "missing-canonical", viewName };
|
|
4508
|
+
const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
|
|
4509
|
+
(spec) => {
|
|
4510
|
+
const { state, actualTarget } = inspectSymlink(join9(viewDir, spec.name), spec.target);
|
|
4511
|
+
return {
|
|
4512
|
+
name: spec.name,
|
|
4513
|
+
expectedTarget: spec.target,
|
|
4514
|
+
state,
|
|
4515
|
+
...actualTarget !== void 0 ? { actualTarget } : {}
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
4518
|
+
);
|
|
4519
|
+
return { kind: "gathered", viewName, files };
|
|
4520
|
+
}
|
|
4521
|
+
function applyViewSymlinks(viewDir, files) {
|
|
4522
|
+
const created = [];
|
|
4523
|
+
const failed = [];
|
|
4524
|
+
for (const f of files) {
|
|
4525
|
+
if (f.state !== "missing") continue;
|
|
4526
|
+
const filePath = join9(viewDir, f.name);
|
|
4527
|
+
try {
|
|
4528
|
+
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4529
|
+
symlinkSync(f.expectedTarget, filePath);
|
|
4530
|
+
created.push(f.name);
|
|
4531
|
+
} catch (error) {
|
|
4532
|
+
failed.push({ file: f.name, message: failureReason(error) });
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
return { created, failed };
|
|
4536
|
+
}
|
|
4366
4537
|
async function doRunProjectSymlinks(options, ctx) {
|
|
4367
4538
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4368
4539
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project symlinks");
|
|
@@ -4382,11 +4553,33 @@ async function doRunProjectSymlinks(options, ctx) {
|
|
|
4382
4553
|
for (const f of failed) failures.push({ repo: plan.path, file: f.file, message: f.message });
|
|
4383
4554
|
}
|
|
4384
4555
|
}
|
|
4556
|
+
let view;
|
|
4557
|
+
let viewCreated = [];
|
|
4558
|
+
const viewFailures = [];
|
|
4559
|
+
if (roster.length > 0) {
|
|
4560
|
+
const viewPath = manifest.workspace.view;
|
|
4561
|
+
if (viewPath === void 0) {
|
|
4562
|
+
view = { kind: "no-view" };
|
|
4563
|
+
} else {
|
|
4564
|
+
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
4565
|
+
view = gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir);
|
|
4566
|
+
if (options.apply === true && view.kind === "gathered") {
|
|
4567
|
+
const { created, failed } = applyViewSymlinks(viewDir, view.files);
|
|
4568
|
+
viewCreated = created;
|
|
4569
|
+
for (const f of failed) viewFailures.push(f);
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
const viewClean = view === void 0 || view.kind === "no-view" || view.kind === "gathered" && view.files.every((f) => f.state === "correct");
|
|
4385
4574
|
const result = {
|
|
4386
4575
|
...summary,
|
|
4576
|
+
ok: summary.ok && viewClean,
|
|
4387
4577
|
hasRoster: roster.length > 0,
|
|
4388
4578
|
applied: createdCount > 0,
|
|
4389
|
-
failures
|
|
4579
|
+
failures,
|
|
4580
|
+
...view !== void 0 ? { view } : {},
|
|
4581
|
+
viewCreated,
|
|
4582
|
+
viewFailures
|
|
4390
4583
|
};
|
|
4391
4584
|
if (options.json === true) {
|
|
4392
4585
|
console.log(JSON.stringify(result));
|
|
@@ -4434,7 +4627,7 @@ function renderProjectSymlinks(result) {
|
|
|
4434
4627
|
);
|
|
4435
4628
|
} else {
|
|
4436
4629
|
lines.push(
|
|
4437
|
-
"\u2139\uFE0F No symlink needs generating, but there are conflicts / collisions / a missing canonical / unreachable repos (see below)."
|
|
4630
|
+
"\u2139\uFE0F No repo symlink needs generating, but there are conflicts / collisions / a missing canonical / unreachable repos, or the workspace view's spokes need attention (see below)."
|
|
4438
4631
|
);
|
|
4439
4632
|
}
|
|
4440
4633
|
lines.push("");
|
|
@@ -4483,11 +4676,61 @@ function renderProjectSymlinks(result) {
|
|
|
4483
4676
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
4484
4677
|
lines.push("");
|
|
4485
4678
|
}
|
|
4679
|
+
appendViewSymlinksSection(lines, result);
|
|
4486
4680
|
lines.push(
|
|
4487
4681
|
"Note: an existing file or a symlink pointing elsewhere is never overwritten; only the missing links are created (GEMINI.md is discontinued and not generated)."
|
|
4488
4682
|
);
|
|
4489
4683
|
return lines.join("\n");
|
|
4490
4684
|
}
|
|
4685
|
+
function appendViewSymlinksSection(lines, result) {
|
|
4686
|
+
const view = result.view;
|
|
4687
|
+
if (view === void 0 || view.kind === "no-view") return;
|
|
4688
|
+
lines.push("## Workspace view spokes (the view's own instruction files)");
|
|
4689
|
+
if (view.kind === "collision") {
|
|
4690
|
+
lines.push(
|
|
4691
|
+
`\u26A0\uFE0F ${view.viewName}: the view shares its canonical name with the roster repo \`${view.repoPath}\` \u2014 both would own \`agents/${view.viewName}/${CANONICAL_FILE}\`, so no view spoke is wired. Rename the view directory or the repo to disambiguate, then re-run.`
|
|
4692
|
+
);
|
|
4693
|
+
lines.push("");
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
if (view.kind === "missing-canonical") {
|
|
4697
|
+
lines.push(
|
|
4698
|
+
`\u2139\uFE0F ${view.viewName}: the view canonical \`agents/${view.viewName}/${CANONICAL_FILE}\` does not exist yet \u2014 run \`basou project preset --apply\` first (or \`basou project derive --apply\`), then re-run.`
|
|
4699
|
+
);
|
|
4700
|
+
lines.push("");
|
|
4701
|
+
return;
|
|
4702
|
+
}
|
|
4703
|
+
const failedFiles = new Set(result.viewFailures.map((f) => f.file));
|
|
4704
|
+
const missing = view.files.filter((f) => f.state === "missing");
|
|
4705
|
+
const attempted = result.viewCreated.length > 0 || result.viewFailures.length > 0;
|
|
4706
|
+
if (missing.length === 0) {
|
|
4707
|
+
lines.push(`\u2705 ${view.viewName}: the view's instruction spokes are correctly wired.`);
|
|
4708
|
+
} else if (!attempted) {
|
|
4709
|
+
lines.push(`${view.viewName}: view spokes to create (dry-run; pass --apply to write):`);
|
|
4710
|
+
for (const f of missing) lines.push(` ${f.name} -> ${f.expectedTarget}`);
|
|
4711
|
+
} else {
|
|
4712
|
+
lines.push(`${view.viewName}: view spokes created:`);
|
|
4713
|
+
for (const f of missing) {
|
|
4714
|
+
if (failedFiles.has(f.name)) continue;
|
|
4715
|
+
lines.push(` ${f.name} -> ${f.expectedTarget}`);
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4718
|
+
if (result.viewFailures.length > 0) {
|
|
4719
|
+
lines.push(` Failed:`);
|
|
4720
|
+
for (const f of result.viewFailures) lines.push(` ${f.file}: ${f.message}`);
|
|
4721
|
+
}
|
|
4722
|
+
const conflicts = view.files.filter(
|
|
4723
|
+
(f) => f.state === "mismatch" || f.state === "occupied" || f.state === "blocked"
|
|
4724
|
+
);
|
|
4725
|
+
if (conflicts.length > 0) {
|
|
4726
|
+
lines.push(` Conflicts (left untouched):`);
|
|
4727
|
+
for (const f of conflicts) {
|
|
4728
|
+
const detail = f.state === "mismatch" ? `points elsewhere (currently: ${f.actualTarget ?? "?"})` : f.state === "occupied" ? "a real file/directory" : "an uninspectable path";
|
|
4729
|
+
lines.push(` ${f.name}: ${detail}`);
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
lines.push("");
|
|
4733
|
+
}
|
|
4491
4734
|
async function runProjectWorkspace(options, ctx = {}) {
|
|
4492
4735
|
try {
|
|
4493
4736
|
await doRunProjectWorkspace(options, ctx);
|
|
@@ -4878,6 +5121,86 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
4878
5121
|
...section.kind === "ok" ? { currentBlock: section.generated } : {}
|
|
4879
5122
|
};
|
|
4880
5123
|
}
|
|
5124
|
+
function normalizeViewBlock(s) {
|
|
5125
|
+
return s.replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
5126
|
+
}
|
|
5127
|
+
function viewPresetReposFor(repositoryRoot, roster) {
|
|
5128
|
+
let anchorReal;
|
|
5129
|
+
try {
|
|
5130
|
+
anchorReal = realpathSync(repositoryRoot);
|
|
5131
|
+
} catch {
|
|
5132
|
+
anchorReal = void 0;
|
|
5133
|
+
}
|
|
5134
|
+
return roster.map((entry) => {
|
|
5135
|
+
const abs = resolve7(repositoryRoot, entry.path);
|
|
5136
|
+
let real;
|
|
5137
|
+
try {
|
|
5138
|
+
real = realpathSync(abs);
|
|
5139
|
+
} catch {
|
|
5140
|
+
real = void 0;
|
|
5141
|
+
}
|
|
5142
|
+
const name = basename5(real ?? abs);
|
|
5143
|
+
const isAnchor = real !== void 0 && anchorReal !== void 0 ? real === anchorReal : abs === repositoryRoot;
|
|
5144
|
+
return {
|
|
5145
|
+
name,
|
|
5146
|
+
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
|
|
5147
|
+
...entry.language !== void 0 ? { language: entry.language } : {},
|
|
5148
|
+
...isAnchor ? { anchor: true } : instructionMode(entry) === "self" ? { self: true } : {}
|
|
5149
|
+
};
|
|
5150
|
+
});
|
|
5151
|
+
}
|
|
5152
|
+
function gatherViewPreset(repositoryRoot, anchorReal, viewName, roster) {
|
|
5153
|
+
const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
|
|
5154
|
+
if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
|
|
5155
|
+
const desiredBlock = renderViewPresetBlock({
|
|
5156
|
+
viewName,
|
|
5157
|
+
repos: viewPresetReposFor(repositoryRoot, roster)
|
|
5158
|
+
});
|
|
5159
|
+
const canonicalFile = canonicalFileFor(anchorReal, viewName);
|
|
5160
|
+
let content;
|
|
5161
|
+
try {
|
|
5162
|
+
content = readFileSync(canonicalFile, "utf8");
|
|
5163
|
+
} catch (error) {
|
|
5164
|
+
if (hasErrorCode(error) && error.code === "ENOENT") {
|
|
5165
|
+
return {
|
|
5166
|
+
kind: "plan",
|
|
5167
|
+
action: "create",
|
|
5168
|
+
canonicalName: viewName,
|
|
5169
|
+
viewName,
|
|
5170
|
+
block: desiredBlock
|
|
5171
|
+
};
|
|
5172
|
+
}
|
|
5173
|
+
return { kind: "unreadable", canonicalName: viewName, viewName };
|
|
5174
|
+
}
|
|
5175
|
+
const section = parseMarkers(content);
|
|
5176
|
+
if (section.kind === "ok") {
|
|
5177
|
+
if (normalizeViewBlock(section.generated) === normalizeViewBlock(desiredBlock)) {
|
|
5178
|
+
return { kind: "in-sync", canonicalName: viewName, viewName };
|
|
5179
|
+
}
|
|
5180
|
+
return {
|
|
5181
|
+
kind: "plan",
|
|
5182
|
+
action: "update",
|
|
5183
|
+
canonicalName: viewName,
|
|
5184
|
+
viewName,
|
|
5185
|
+
block: desiredBlock
|
|
5186
|
+
};
|
|
5187
|
+
}
|
|
5188
|
+
return { kind: "conflict", canonicalName: viewName, viewName, reason: section.kind };
|
|
5189
|
+
}
|
|
5190
|
+
async function applyViewPreset(anchorReal, outcome) {
|
|
5191
|
+
const file = canonicalFileFor(anchorReal, outcome.canonicalName);
|
|
5192
|
+
const label = canonicalLabelFor(outcome.canonicalName);
|
|
5193
|
+
let isLink = false;
|
|
5194
|
+
try {
|
|
5195
|
+
isLink = lstatSync(file).isSymbolicLink();
|
|
5196
|
+
} catch {
|
|
5197
|
+
isLink = false;
|
|
5198
|
+
}
|
|
5199
|
+
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
5200
|
+
if (outcome.action === "create") mkdirSync(dirname3(file), { recursive: true });
|
|
5201
|
+
const existing = await readMarkdownFile4(file);
|
|
5202
|
+
await writeMarkdownFile5(file, renderWithMarkers4(existing, outcome.block, label));
|
|
5203
|
+
}
|
|
4881
5204
|
async function applyPresetPlan(anchorReal, plan) {
|
|
4882
5205
|
const file = canonicalFileFor(anchorReal, plan.canonicalName);
|
|
4883
5206
|
const label = canonicalLabelFor(plan.canonicalName);
|
|
@@ -4910,7 +5233,12 @@ async function doRunProjectPreset(options, ctx) {
|
|
|
4910
5233
|
const anchorReal = realpathSync(repositoryRoot);
|
|
4911
5234
|
const facts = [];
|
|
4912
5235
|
for (const entry of roster) facts.push(await gatherRepoPreset(repositoryRoot, anchorReal, entry));
|
|
4913
|
-
const
|
|
5236
|
+
const viewPath = manifest.workspace.view;
|
|
5237
|
+
const viewCanonicalName = roster.length > 0 && viewPath !== void 0 ? basename5(resolveViewDir(repositoryRoot, viewPath)) : void 0;
|
|
5238
|
+
const summary = summarizePresetPlan(
|
|
5239
|
+
facts,
|
|
5240
|
+
viewCanonicalName !== void 0 ? { viewCanonicalName } : void 0
|
|
5241
|
+
);
|
|
4914
5242
|
const failures = [];
|
|
4915
5243
|
let writtenCount = 0;
|
|
4916
5244
|
if (options.apply === true && summary.plans.length > 0) {
|
|
@@ -4923,11 +5251,30 @@ async function doRunProjectPreset(options, ctx) {
|
|
|
4923
5251
|
}
|
|
4924
5252
|
}
|
|
4925
5253
|
}
|
|
5254
|
+
let view;
|
|
5255
|
+
let viewApplied = false;
|
|
5256
|
+
let viewFailure;
|
|
5257
|
+
if (roster.length > 0) {
|
|
5258
|
+
view = viewCanonicalName === void 0 ? { kind: "no-view" } : gatherViewPreset(repositoryRoot, anchorReal, viewCanonicalName, roster);
|
|
5259
|
+
if (options.apply === true && view.kind === "plan") {
|
|
5260
|
+
try {
|
|
5261
|
+
await applyViewPreset(anchorReal, view);
|
|
5262
|
+
viewApplied = true;
|
|
5263
|
+
} catch (error) {
|
|
5264
|
+
viewFailure = presetFailureReason(error);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
const viewClean = view === void 0 || view.kind === "no-view" || view.kind === "in-sync";
|
|
4926
5269
|
const result = {
|
|
4927
5270
|
...summary,
|
|
5271
|
+
ok: summary.ok && viewClean,
|
|
4928
5272
|
hasRoster: roster.length > 0,
|
|
4929
5273
|
applied: writtenCount > 0,
|
|
4930
|
-
failures
|
|
5274
|
+
failures,
|
|
5275
|
+
...view !== void 0 ? { view } : {},
|
|
5276
|
+
viewApplied,
|
|
5277
|
+
...viewFailure !== void 0 ? { viewFailure } : {}
|
|
4931
5278
|
};
|
|
4932
5279
|
if (options.json === true) {
|
|
4933
5280
|
console.log(JSON.stringify(result));
|
|
@@ -4980,7 +5327,7 @@ function renderProjectPreset(result) {
|
|
|
4980
5327
|
);
|
|
4981
5328
|
} else {
|
|
4982
5329
|
lines.push(
|
|
4983
|
-
"\u2139\uFE0F No repo needs generating, but there are marker conflicts / collisions / undeclared / unreachable repos (see below)."
|
|
5330
|
+
"\u2139\uFE0F No repo needs generating, but there are marker conflicts / collisions / undeclared / unreachable repos, or the workspace view canonical needs attention (see below)."
|
|
4984
5331
|
);
|
|
4985
5332
|
}
|
|
4986
5333
|
lines.push("");
|
|
@@ -5017,10 +5364,11 @@ function renderProjectPreset(result) {
|
|
|
5017
5364
|
}
|
|
5018
5365
|
if (result.collisions.length > 0) {
|
|
5019
5366
|
lines.push(
|
|
5020
|
-
`## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-generated)`
|
|
5367
|
+
`## Canonical collisions (${result.collisions.length}) \u2014 another repo (or the workspace view) shares the same-named canonical (not auto-generated)`
|
|
5021
5368
|
);
|
|
5022
5369
|
for (const c of result.collisions) {
|
|
5023
|
-
|
|
5370
|
+
const suffix = c.view === true ? " + the workspace view (rename the view directory or the repo to disambiguate)" : "";
|
|
5371
|
+
lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}${suffix}`);
|
|
5024
5372
|
}
|
|
5025
5373
|
lines.push("");
|
|
5026
5374
|
}
|
|
@@ -5050,11 +5398,46 @@ function renderProjectPreset(result) {
|
|
|
5050
5398
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
5051
5399
|
lines.push("");
|
|
5052
5400
|
}
|
|
5401
|
+
appendViewPresetSection(lines, result);
|
|
5053
5402
|
lines.push(
|
|
5054
5403
|
"Note: only the marker region is generated; the canonical's hand-authored content (outside the markers) is preserved. The generated content is derived from the manifest declaration."
|
|
5055
5404
|
);
|
|
5056
5405
|
return lines.join("\n");
|
|
5057
5406
|
}
|
|
5407
|
+
function appendViewPresetSection(lines, result) {
|
|
5408
|
+
const view = result.view;
|
|
5409
|
+
if (view === void 0 || view.kind === "no-view") return;
|
|
5410
|
+
const canonical2 = canonicalLabelFor(view.viewName);
|
|
5411
|
+
lines.push("## Workspace view canonical (the view's own AGENTS.md)");
|
|
5412
|
+
if (view.kind === "collision") {
|
|
5413
|
+
lines.push(
|
|
5414
|
+
`\u26A0\uFE0F ${view.viewName}: shares its canonical name with the roster repo \`${view.repoPath}\` \u2014 both would own ${canonical2}, so neither side is generated. Rename the view directory or the repo to disambiguate, then re-run.`
|
|
5415
|
+
);
|
|
5416
|
+
} else if (view.kind === "in-sync") {
|
|
5417
|
+
lines.push(`\u2705 ${view.viewName}: in sync with ${canonical2} (nothing to generate).`);
|
|
5418
|
+
} else if (view.kind === "plan") {
|
|
5419
|
+
if (result.viewApplied) {
|
|
5420
|
+
lines.push(`\u2705 ${view.viewName} [${view.action}] \u2192 ${canonical2}`);
|
|
5421
|
+
} else if (result.viewFailure !== void 0) {
|
|
5422
|
+
lines.push(`- ${view.viewName} [${view.action}] \u2192 ${canonical2}: ${result.viewFailure}`);
|
|
5423
|
+
} else {
|
|
5424
|
+
lines.push(
|
|
5425
|
+
`- ${view.viewName} [${view.action}] \u2192 ${canonical2} (dry-run; pass --apply to write):`
|
|
5426
|
+
);
|
|
5427
|
+
for (const bl of view.block.split("\n")) lines.push(` ${bl}`);
|
|
5428
|
+
}
|
|
5429
|
+
} else if (view.kind === "conflict") {
|
|
5430
|
+
const detail = view.reason === "no_markers" ? "no marker region" : `malformed markers (${view.reason})`;
|
|
5431
|
+
lines.push(
|
|
5432
|
+
`\u26A0\uFE0F ${view.viewName}: ${canonical2} has ${detail}, so it is not overwritten. Add \`${GENERATED_START}\` / \`${GENERATED_END}\` where the block should go (or run \`basou project retrofit\` to prepend it, preserving your prose).`
|
|
5433
|
+
);
|
|
5434
|
+
} else {
|
|
5435
|
+
lines.push(
|
|
5436
|
+
`\u26A0\uFE0F ${view.viewName}: ${canonical2} could not be read (a directory, permissions, etc.). Resolve it by hand, then re-run.`
|
|
5437
|
+
);
|
|
5438
|
+
}
|
|
5439
|
+
lines.push("");
|
|
5440
|
+
}
|
|
5058
5441
|
async function runProjectArchive(target, options, ctx = {}) {
|
|
5059
5442
|
try {
|
|
5060
5443
|
await doRunProjectArchive(target, options, ctx);
|
|
@@ -5837,10 +6220,21 @@ async function resolveRepositoryRootForNew(cwd) {
|
|
|
5837
6220
|
throw error;
|
|
5838
6221
|
}
|
|
5839
6222
|
}
|
|
6223
|
+
function validateProjectName(name) {
|
|
6224
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
|
|
6225
|
+
throw new Error(
|
|
6226
|
+
`Invalid --project-name "${name}": use a simple name (letters, digits, '.', '-', '_'; starting with a letter or digit; no spaces or path separators). For a custom view path, use --view.`
|
|
6227
|
+
);
|
|
6228
|
+
}
|
|
6229
|
+
return name;
|
|
6230
|
+
}
|
|
5840
6231
|
async function doRunProjectNew(repos, options, ctx) {
|
|
5841
6232
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5842
6233
|
const repositoryRoot = await resolveRepositoryRootForNew(cwd);
|
|
5843
6234
|
const workspaceName = basename5(repositoryRoot);
|
|
6235
|
+
const productName = options.projectName !== void 0 ? validateProjectName(options.projectName) : void 0;
|
|
6236
|
+
const viewStem = productName ?? workspaceName;
|
|
6237
|
+
const viewOverridesProjectName = productName !== void 0 && typeof options.view === "string";
|
|
5844
6238
|
const declared = repos.map((p) => {
|
|
5845
6239
|
const abs = resolve7(cwd, p);
|
|
5846
6240
|
let real;
|
|
@@ -5865,11 +6259,15 @@ async function doRunProjectNew(repos, options, ctx) {
|
|
|
5865
6259
|
if (rel !== "." && !rosterPaths.includes(rel)) rosterPaths.push(rel);
|
|
5866
6260
|
}
|
|
5867
6261
|
const roster = rosterPaths.map((path) => ({ path }));
|
|
5868
|
-
const viewPath = options.view === false ? null : options.view ?? `../${
|
|
6262
|
+
const viewPath = options.view === false ? null : options.view ?? `../${viewStem}-workspace`;
|
|
5869
6263
|
const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
|
|
5870
6264
|
const paths = basouPaths10(repositoryRoot);
|
|
5871
6265
|
const existed = existsSync(paths.files.manifest);
|
|
5872
|
-
const manifest = createManifest2({
|
|
6266
|
+
const manifest = createManifest2({
|
|
6267
|
+
workspaceName,
|
|
6268
|
+
sourceRoots,
|
|
6269
|
+
...productName !== void 0 ? { projectName: productName } : {}
|
|
6270
|
+
});
|
|
5873
6271
|
manifest.repos = roster;
|
|
5874
6272
|
if (viewPath !== null) manifest.workspace.view = viewPath;
|
|
5875
6273
|
let applied = false;
|
|
@@ -5885,8 +6283,10 @@ async function doRunProjectNew(repos, options, ctx) {
|
|
|
5885
6283
|
}
|
|
5886
6284
|
const result = {
|
|
5887
6285
|
workspaceName,
|
|
6286
|
+
projectName: productName ?? null,
|
|
5888
6287
|
repos: roster,
|
|
5889
6288
|
view: viewPath,
|
|
6289
|
+
viewOverridesProjectName,
|
|
5890
6290
|
sourceRoots,
|
|
5891
6291
|
invalidRepos: [],
|
|
5892
6292
|
existed,
|
|
@@ -5919,11 +6319,12 @@ function renderProjectNew(result) {
|
|
|
5919
6319
|
);
|
|
5920
6320
|
lines.push("");
|
|
5921
6321
|
}
|
|
6322
|
+
const identity = result.projectName ?? result.workspaceName;
|
|
5922
6323
|
if (result.applied) {
|
|
5923
|
-
lines.push(`\u2705 Created \`.basou/\` for \`${
|
|
6324
|
+
lines.push(`\u2705 Created \`.basou/\` for \`${identity}\` and seeded the manifest:`);
|
|
5924
6325
|
} else {
|
|
5925
6326
|
lines.push(
|
|
5926
|
-
`Will create \`.basou/\` for \`${
|
|
6327
|
+
`Will create \`.basou/\` for \`${identity}\` and seed the manifest (dry-run; pass --apply to write):`
|
|
5927
6328
|
);
|
|
5928
6329
|
}
|
|
5929
6330
|
lines.push("");
|
|
@@ -5932,9 +6333,16 @@ function renderProjectNew(result) {
|
|
|
5932
6333
|
lines.push(`- ${r.path}${r.path === "." ? " (anchor)" : ""}`);
|
|
5933
6334
|
}
|
|
5934
6335
|
lines.push("");
|
|
6336
|
+
if (result.projectName !== null) {
|
|
6337
|
+
lines.push(`project name: ${result.projectName}`);
|
|
6338
|
+
lines.push("");
|
|
6339
|
+
}
|
|
5935
6340
|
lines.push(
|
|
5936
6341
|
result.view !== null ? `workspace view: ${result.view}` : "workspace view: none (solo project)"
|
|
5937
6342
|
);
|
|
6343
|
+
if (result.viewOverridesProjectName) {
|
|
6344
|
+
lines.push(" (note: --view set this path, so --project-name did not drive the view name)");
|
|
6345
|
+
}
|
|
5938
6346
|
lines.push("");
|
|
5939
6347
|
lines.push(`source_roots (${result.sourceRoots.length}):`);
|
|
5940
6348
|
for (const s of result.sourceRoots) lines.push(`- ${s}`);
|
|
@@ -5980,25 +6388,79 @@ async function doRunProjectDerive(options, ctx) {
|
|
|
5980
6388
|
const stepOpts = { apply };
|
|
5981
6389
|
console.log("# Generate project wiring in one pass (declaration \u2192 wiring)");
|
|
5982
6390
|
console.log("");
|
|
5983
|
-
console.log("## 1/
|
|
6391
|
+
console.log("## 1/6 sync source_roots (roster \u2192 capture config)");
|
|
5984
6392
|
await doRunProjectSync(stepOpts, stepCtx);
|
|
5985
6393
|
console.log("");
|
|
5986
|
-
console.log("## 2/
|
|
6394
|
+
console.log("## 2/6 generate instruction-file A preset (declaration \u2192 canonical)");
|
|
5987
6395
|
await doRunProjectPreset(stepOpts, stepCtx);
|
|
5988
6396
|
console.log("");
|
|
5989
|
-
console.log("## 3/
|
|
6397
|
+
console.log("## 3/6 seed the anchor's own AGENTS.md (greenfield only; create-only)");
|
|
6398
|
+
await doRunProjectSeedAnchor(stepOpts, stepCtx);
|
|
6399
|
+
console.log("");
|
|
6400
|
+
console.log("## 4/6 generate instruction-file symlinks (each repo \u2192 canonical)");
|
|
5990
6401
|
await doRunProjectSymlinks(stepOpts, stepCtx);
|
|
5991
6402
|
console.log("");
|
|
5992
|
-
console.log("##
|
|
6403
|
+
console.log("## 5/6 generate workspace view (aggregate the roster repos)");
|
|
5993
6404
|
await doRunProjectWorkspace(stepOpts, stepCtx);
|
|
5994
6405
|
console.log("");
|
|
5995
|
-
console.log("##
|
|
6406
|
+
console.log("## 6/6 generate .gitignore (exclude public repos' instruction files)");
|
|
5996
6407
|
await doRunProjectGitignore(stepOpts, stepCtx);
|
|
5997
6408
|
console.log("");
|
|
5998
6409
|
console.log(
|
|
5999
6410
|
apply ? "\u2705 Ran every step (each is idempotent, so a partial apply recovers on re-run)." : "\u2139\uFE0F Dry-run preview. Pass --apply to write the changes, then re-run."
|
|
6000
6411
|
);
|
|
6001
6412
|
}
|
|
6413
|
+
async function doRunProjectSeedAnchor(options, ctx) {
|
|
6414
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
6415
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
|
|
6416
|
+
const paths = basouPaths10(repositoryRoot);
|
|
6417
|
+
const manifest = await readManifest6(paths);
|
|
6418
|
+
const roster = manifest.repos ?? [];
|
|
6419
|
+
console.log("# Anchor instruction-file seed (the planning master's own AGENTS.md)");
|
|
6420
|
+
console.log("");
|
|
6421
|
+
if (roster.length === 0) {
|
|
6422
|
+
console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
|
|
6423
|
+
return;
|
|
6424
|
+
}
|
|
6425
|
+
const anchorDoc = join9(repositoryRoot, CANONICAL_FILE);
|
|
6426
|
+
if (pathPresent(anchorDoc)) {
|
|
6427
|
+
console.log(
|
|
6428
|
+
`\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
|
|
6429
|
+
);
|
|
6430
|
+
return;
|
|
6431
|
+
}
|
|
6432
|
+
const viewPath = manifest.workspace.view;
|
|
6433
|
+
const viewName = viewPath !== void 0 ? basename5(resolveViewDir(repositoryRoot, viewPath)) : void 0;
|
|
6434
|
+
const repos = viewPresetReposFor(repositoryRoot, roster);
|
|
6435
|
+
const content = renderAnchorStarter({
|
|
6436
|
+
anchorName: basename5(repositoryRoot),
|
|
6437
|
+
...manifest.project?.name !== void 0 ? { projectName: manifest.project.name } : {},
|
|
6438
|
+
...viewName !== void 0 ? { viewName } : {},
|
|
6439
|
+
repos
|
|
6440
|
+
});
|
|
6441
|
+
if (options.apply !== true) {
|
|
6442
|
+
console.log(
|
|
6443
|
+
`- ${CANONICAL_FILE} [create] \u2192 the anchor's own AGENTS.md (dry-run; pass --apply to write). A create-only starter; hand-maintain it afterward \u2014 basou never rewrites it.`
|
|
6444
|
+
);
|
|
6445
|
+
return;
|
|
6446
|
+
}
|
|
6447
|
+
try {
|
|
6448
|
+
writeFileSync(anchorDoc, content, { flag: "wx" });
|
|
6449
|
+
console.log(
|
|
6450
|
+
`\u2705 Seeded the anchor's own \`${CANONICAL_FILE}\` (create-only starter \u2014 hand-maintain it from here; basou never rewrites it).`
|
|
6451
|
+
);
|
|
6452
|
+
} catch (error) {
|
|
6453
|
+
if (hasErrorCode(error) && error.code === "EEXIST") {
|
|
6454
|
+
console.log(
|
|
6455
|
+
`\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
|
|
6456
|
+
);
|
|
6457
|
+
return;
|
|
6458
|
+
}
|
|
6459
|
+
console.log(
|
|
6460
|
+
`\u26A0\uFE0F Could not seed the anchor's \`${CANONICAL_FILE}\`: ${presetFailureReason(error)}`
|
|
6461
|
+
);
|
|
6462
|
+
}
|
|
6463
|
+
}
|
|
6002
6464
|
async function runProjectRetrofit(repo, options, ctx = {}) {
|
|
6003
6465
|
try {
|
|
6004
6466
|
await doRunProjectRetrofit(repo, options, ctx);
|
|
@@ -6037,7 +6499,7 @@ function pathPresent(p) {
|
|
|
6037
6499
|
return false;
|
|
6038
6500
|
}
|
|
6039
6501
|
}
|
|
6040
|
-
function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal) {
|
|
6502
|
+
function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal, viewCanonicalName) {
|
|
6041
6503
|
const declaredEntry = roster.find((entry) => {
|
|
6042
6504
|
const entryAbs = resolve7(repositoryRoot, entry.path);
|
|
6043
6505
|
if (argReal !== void 0) {
|
|
@@ -6061,6 +6523,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
6061
6523
|
isAnchor: false,
|
|
6062
6524
|
reachable: false,
|
|
6063
6525
|
canonicalName,
|
|
6526
|
+
...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
|
|
6064
6527
|
agentsState: "absent",
|
|
6065
6528
|
canonicalExists: false,
|
|
6066
6529
|
regularSpokes: []
|
|
@@ -6076,6 +6539,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
6076
6539
|
isAnchor,
|
|
6077
6540
|
reachable,
|
|
6078
6541
|
canonicalName,
|
|
6542
|
+
...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
|
|
6079
6543
|
agentsState: inspectAgentsState(join9(argReal, CANONICAL_FILE)),
|
|
6080
6544
|
canonicalExists: pathPresent(canonicalFile),
|
|
6081
6545
|
regularSpokes: regularFileSpokes(argReal)
|
|
@@ -6102,6 +6566,44 @@ function relocateAgentsFile(repoReal, canonicalFile) {
|
|
|
6102
6566
|
return { ok: false, message: failureReason(error), partial: true };
|
|
6103
6567
|
}
|
|
6104
6568
|
}
|
|
6569
|
+
function gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster) {
|
|
6570
|
+
if (viewPath === void 0) return { kind: "no-view" };
|
|
6571
|
+
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
6572
|
+
const viewName = basename5(viewDir);
|
|
6573
|
+
const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
|
|
6574
|
+
if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
|
|
6575
|
+
const canonicalFile = canonicalFileFor(anchorReal, viewName);
|
|
6576
|
+
let content;
|
|
6577
|
+
try {
|
|
6578
|
+
content = readFileSync(canonicalFile, "utf8");
|
|
6579
|
+
} catch (error) {
|
|
6580
|
+
if (hasErrorCode(error) && error.code === "ENOENT") return { kind: "absent", viewName };
|
|
6581
|
+
return { kind: "unreadable", viewName };
|
|
6582
|
+
}
|
|
6583
|
+
const section = parseMarkers(content);
|
|
6584
|
+
if (section.kind === "ok") return { kind: "already-marked", viewName };
|
|
6585
|
+
if (section.kind === "no_markers") {
|
|
6586
|
+
const block = renderViewPresetBlock({
|
|
6587
|
+
viewName,
|
|
6588
|
+
repos: viewPresetReposFor(repositoryRoot, roster)
|
|
6589
|
+
});
|
|
6590
|
+
return { kind: "seed", viewName, block };
|
|
6591
|
+
}
|
|
6592
|
+
return { kind: "malformed", viewName, reason: section.kind };
|
|
6593
|
+
}
|
|
6594
|
+
async function applyViewRetrofit(anchorReal, outcome) {
|
|
6595
|
+
const file = canonicalFileFor(anchorReal, outcome.viewName);
|
|
6596
|
+
const label = canonicalLabelFor(outcome.viewName);
|
|
6597
|
+
let isLink = false;
|
|
6598
|
+
try {
|
|
6599
|
+
isLink = lstatSync(file).isSymbolicLink();
|
|
6600
|
+
} catch {
|
|
6601
|
+
isLink = false;
|
|
6602
|
+
}
|
|
6603
|
+
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
6604
|
+
const existing = await readMarkdownFile4(file);
|
|
6605
|
+
await writeMarkdownFile5(file, seedMarkers(existing, outcome.block, label));
|
|
6606
|
+
}
|
|
6105
6607
|
async function doRunProjectRetrofit(repo, options, ctx) {
|
|
6106
6608
|
const cwd = ctx.cwd ?? process.cwd();
|
|
6107
6609
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project retrofit");
|
|
@@ -6109,6 +6611,36 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6109
6611
|
const manifest = await readManifest6(paths);
|
|
6110
6612
|
const roster = manifest.repos ?? [];
|
|
6111
6613
|
const anchorReal = realpathSync(repositoryRoot);
|
|
6614
|
+
const viewPath = manifest.workspace.view;
|
|
6615
|
+
if (repo === void 0) {
|
|
6616
|
+
let view2;
|
|
6617
|
+
let viewApplied = false;
|
|
6618
|
+
let viewFailure;
|
|
6619
|
+
if (roster.length > 0) {
|
|
6620
|
+
view2 = gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster);
|
|
6621
|
+
if (options.apply === true && view2.kind === "seed") {
|
|
6622
|
+
try {
|
|
6623
|
+
await applyViewRetrofit(anchorReal, view2);
|
|
6624
|
+
viewApplied = true;
|
|
6625
|
+
} catch (error) {
|
|
6626
|
+
viewFailure = presetFailureReason(error);
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
const result2 = {
|
|
6631
|
+
kind: "view-only",
|
|
6632
|
+
hasRoster: roster.length > 0,
|
|
6633
|
+
...view2 !== void 0 ? { view: view2 } : {},
|
|
6634
|
+
viewApplied,
|
|
6635
|
+
...viewFailure !== void 0 ? { viewFailure } : {}
|
|
6636
|
+
};
|
|
6637
|
+
if (options.json === true) {
|
|
6638
|
+
console.log(JSON.stringify(result2));
|
|
6639
|
+
} else {
|
|
6640
|
+
console.log(renderProjectRetrofit(result2));
|
|
6641
|
+
}
|
|
6642
|
+
return result2;
|
|
6643
|
+
}
|
|
6112
6644
|
const argAbs = resolve7(repositoryRoot, repo);
|
|
6113
6645
|
let argReal;
|
|
6114
6646
|
try {
|
|
@@ -6116,7 +6648,16 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6116
6648
|
} catch {
|
|
6117
6649
|
argReal = void 0;
|
|
6118
6650
|
}
|
|
6119
|
-
const
|
|
6651
|
+
const viewCanonicalName = viewPath !== void 0 ? basename5(resolveViewDir(repositoryRoot, viewPath)) : void 0;
|
|
6652
|
+
const facts = gatherRetrofit(
|
|
6653
|
+
repositoryRoot,
|
|
6654
|
+
anchorReal,
|
|
6655
|
+
roster,
|
|
6656
|
+
repo,
|
|
6657
|
+
argAbs,
|
|
6658
|
+
argReal,
|
|
6659
|
+
viewCanonicalName
|
|
6660
|
+
);
|
|
6120
6661
|
const plan = classifyRetrofit(facts);
|
|
6121
6662
|
let applied = false;
|
|
6122
6663
|
let failure;
|
|
@@ -6131,12 +6672,19 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6131
6672
|
partial = res.partial;
|
|
6132
6673
|
}
|
|
6133
6674
|
}
|
|
6675
|
+
let view;
|
|
6676
|
+
if (roster.length > 0) {
|
|
6677
|
+
view = gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster);
|
|
6678
|
+
}
|
|
6134
6679
|
const result = {
|
|
6680
|
+
kind: "repo",
|
|
6135
6681
|
...plan,
|
|
6136
6682
|
hasRoster: roster.length > 0,
|
|
6137
6683
|
applied,
|
|
6138
6684
|
...failure !== void 0 ? { failure } : {},
|
|
6139
|
-
...partial ? { partial } : {}
|
|
6685
|
+
...partial ? { partial } : {},
|
|
6686
|
+
...view !== void 0 ? { view } : {},
|
|
6687
|
+
viewApplied: false
|
|
6140
6688
|
};
|
|
6141
6689
|
if (options.json === true) {
|
|
6142
6690
|
console.log(JSON.stringify(result));
|
|
@@ -6145,6 +6693,47 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6145
6693
|
}
|
|
6146
6694
|
return result;
|
|
6147
6695
|
}
|
|
6696
|
+
var MARKER_PORTABILITY_NOTE = "The marker pair can later be moved anywhere in the file by hand \u2014 `basou project preset` rewrites only the region between the markers.";
|
|
6697
|
+
function appendViewRetrofitSection(lines, result) {
|
|
6698
|
+
const view = result.view;
|
|
6699
|
+
if (view === void 0 || view.kind === "no-view" || view.kind === "absent" || view.kind === "already-marked")
|
|
6700
|
+
return;
|
|
6701
|
+
const canonical2 = `agents/${view.viewName}/${CANONICAL_FILE}`;
|
|
6702
|
+
lines.push("## Workspace view canonical (auto-migrate the view's own AGENTS.md)");
|
|
6703
|
+
if (view.kind === "seed") {
|
|
6704
|
+
if (result.kind === "repo") {
|
|
6705
|
+
lines.push(
|
|
6706
|
+
`\`${canonical2}\` is markerless prose. Run \`basou project retrofit\` (no repo argument) to prepend the generated block, preserving the prose.`
|
|
6707
|
+
);
|
|
6708
|
+
lines.push(MARKER_PORTABILITY_NOTE);
|
|
6709
|
+
} else if (result.viewApplied) {
|
|
6710
|
+
lines.push(
|
|
6711
|
+
`\u2705 Prepended the generated block into \`${canonical2}\` (your hand-written prose is preserved below it).`
|
|
6712
|
+
);
|
|
6713
|
+
lines.push(MARKER_PORTABILITY_NOTE);
|
|
6714
|
+
} else if (result.viewFailure !== void 0) {
|
|
6715
|
+
lines.push(`\u26A0\uFE0F Could not seed \`${canonical2}\`: ${result.viewFailure}. Nothing was changed.`);
|
|
6716
|
+
} else {
|
|
6717
|
+
lines.push(
|
|
6718
|
+
`\`${canonical2}\` is markerless prose. Retrofit will prepend the generated block, preserving the prose (dry-run; pass --apply to write).`
|
|
6719
|
+
);
|
|
6720
|
+
lines.push(MARKER_PORTABILITY_NOTE);
|
|
6721
|
+
}
|
|
6722
|
+
} else if (view.kind === "collision") {
|
|
6723
|
+
lines.push(
|
|
6724
|
+
`\u26A0\uFE0F The view shares its canonical name with the roster repo \`${view.repoPath}\` \u2014 both would own \`${canonical2}\`, so nothing is migrated. Rename the view directory or the repo to disambiguate, then re-run.`
|
|
6725
|
+
);
|
|
6726
|
+
} else if (view.kind === "malformed") {
|
|
6727
|
+
lines.push(
|
|
6728
|
+
`\u26A0\uFE0F \`${canonical2}\` has malformed markers (${view.reason}), so it is not rewritten. Fix the \`${GENERATED_START}\` / \`${GENERATED_END}\` pair by hand, then re-run.`
|
|
6729
|
+
);
|
|
6730
|
+
} else {
|
|
6731
|
+
lines.push(
|
|
6732
|
+
`\u26A0\uFE0F \`${canonical2}\` could not be read (a directory, permissions, etc.). Resolve it by hand, then re-run.`
|
|
6733
|
+
);
|
|
6734
|
+
}
|
|
6735
|
+
lines.push("");
|
|
6736
|
+
}
|
|
6148
6737
|
function appendSpokeChecklist(lines, spokes) {
|
|
6149
6738
|
if (spokes.length === 0) return;
|
|
6150
6739
|
lines.push(
|
|
@@ -6169,6 +6758,25 @@ function renderProjectRetrofit(result) {
|
|
|
6169
6758
|
);
|
|
6170
6759
|
return lines.join("\n");
|
|
6171
6760
|
}
|
|
6761
|
+
if (result.kind === "view-only") {
|
|
6762
|
+
const view = result.view;
|
|
6763
|
+
if (view === void 0 || view.kind === "no-view") {
|
|
6764
|
+
lines.push(
|
|
6765
|
+
"\u2139\uFE0F No `workspace.view` declared \u2014 there is no view canonical to migrate. Pass a repo argument to relocate a repo's AGENTS.md instead."
|
|
6766
|
+
);
|
|
6767
|
+
} else if (view.kind === "absent") {
|
|
6768
|
+
lines.push(
|
|
6769
|
+
`\u2139\uFE0F The view canonical \`agents/${view.viewName}/${CANONICAL_FILE}\` does not exist yet \u2014 run \`basou project preset --apply\` (or \`basou project derive --apply\`) to create it; there is nothing to migrate.`
|
|
6770
|
+
);
|
|
6771
|
+
} else if (view.kind === "already-marked") {
|
|
6772
|
+
lines.push(
|
|
6773
|
+
`\u2705 \`agents/${view.viewName}/${CANONICAL_FILE}\` already has a BASOU:GENERATED region. Nothing to migrate.`
|
|
6774
|
+
);
|
|
6775
|
+
} else {
|
|
6776
|
+
appendViewRetrofitSection(lines, result);
|
|
6777
|
+
}
|
|
6778
|
+
return lines.join("\n").trimEnd();
|
|
6779
|
+
}
|
|
6172
6780
|
const canonical2 = `agents/${result.canonicalName}/${CANONICAL_FILE}`;
|
|
6173
6781
|
if (result.action === "refuse") {
|
|
6174
6782
|
if (result.reason === "not-declared") {
|
|
@@ -6191,12 +6799,18 @@ function renderProjectRetrofit(result) {
|
|
|
6191
6799
|
lines.push(
|
|
6192
6800
|
`\u26A0\uFE0F \`${result.path}/${CANONICAL_FILE}\` could not be inspected (a parent component is not a directory, a permission error, or the path is neither a regular file nor a symlink). Resolve it by hand, then re-run.`
|
|
6193
6801
|
);
|
|
6802
|
+
} else if (result.reason === "view-collision") {
|
|
6803
|
+
lines.push(
|
|
6804
|
+
`\u26A0\uFE0F \`${result.path}\` shares its canonical name with the workspace view \u2014 \`${canonical2}\` would be owned by both, so relocating would corrupt one with the other. Rename the view directory or the repo to disambiguate, then re-run.`
|
|
6805
|
+
);
|
|
6194
6806
|
} else if (result.reason === "canonical-exists") {
|
|
6195
6807
|
lines.push(
|
|
6196
6808
|
`\u26A0\uFE0F The destination canonical \`${canonical2}\` already exists. Not relocating, to avoid clobbering it. If the canonical is the source of truth, the repo's AGENTS.md is redundant (remove it, then run \`basou project symlinks\`); otherwise reconcile the two by hand.`
|
|
6197
6809
|
);
|
|
6198
6810
|
}
|
|
6199
|
-
|
|
6811
|
+
lines.push("");
|
|
6812
|
+
appendViewRetrofitSection(lines, result);
|
|
6813
|
+
return lines.join("\n").trimEnd();
|
|
6200
6814
|
}
|
|
6201
6815
|
if (result.action === "skip") {
|
|
6202
6816
|
if (result.reason === "already-symlink") {
|
|
@@ -6210,6 +6824,7 @@ function renderProjectRetrofit(result) {
|
|
|
6210
6824
|
}
|
|
6211
6825
|
lines.push("");
|
|
6212
6826
|
appendSpokeChecklist(lines, result.regularSpokes);
|
|
6827
|
+
appendViewRetrofitSection(lines, result);
|
|
6213
6828
|
return lines.join("\n").trimEnd();
|
|
6214
6829
|
}
|
|
6215
6830
|
if (result.failure !== void 0) {
|
|
@@ -6223,7 +6838,9 @@ function renderProjectRetrofit(result) {
|
|
|
6223
6838
|
} else {
|
|
6224
6839
|
lines.push("Nothing was changed. Resolve the cause and re-run.");
|
|
6225
6840
|
}
|
|
6226
|
-
|
|
6841
|
+
lines.push("");
|
|
6842
|
+
appendViewRetrofitSection(lines, result);
|
|
6843
|
+
return lines.join("\n").trimEnd();
|
|
6227
6844
|
}
|
|
6228
6845
|
if (result.applied) {
|
|
6229
6846
|
lines.push(
|
|
@@ -6238,6 +6855,7 @@ function renderProjectRetrofit(result) {
|
|
|
6238
6855
|
}
|
|
6239
6856
|
lines.push("");
|
|
6240
6857
|
appendSpokeChecklist(lines, result.regularSpokes);
|
|
6858
|
+
appendViewRetrofitSection(lines, result);
|
|
6241
6859
|
lines.push(
|
|
6242
6860
|
result.applied ? "Next: run `basou project derive --apply` to add the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore." : "After applying, run `basou project derive --apply` to finish the wiring (preset block, CLAUDE.md / Copilot spokes, .gitignore)."
|
|
6243
6861
|
);
|