@basou/cli 0.30.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 +785 -65
- package/dist/index.js.map +1 -1
- package/dist/program.js +785 -65
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3049,7 +3049,6 @@ import {
|
|
|
3049
3049
|
createManifest,
|
|
3050
3050
|
ensureBasouDirectory,
|
|
3051
3051
|
resolveRepositoryRoot as resolveRepositoryRoot7,
|
|
3052
|
-
tryRemoteUrl,
|
|
3053
3052
|
writeManifest
|
|
3054
3053
|
} from "@basou/core";
|
|
3055
3054
|
function collectValue(value, previous) {
|
|
@@ -3058,7 +3057,7 @@ function collectValue(value, previous) {
|
|
|
3058
3057
|
function registerInitCommand(program2) {
|
|
3059
3058
|
program2.command("init").description("Initialize a Basou workspace at the current Git repository root").option("--name <name>", "Workspace name (defaults to the repository directory name)").option("--project-name <name>", "Project display name").option("--project-description <description>", "Project description").option(
|
|
3060
3059
|
"--repo-url <url>",
|
|
3061
|
-
"
|
|
3060
|
+
"Deprecated and ignored (project.repository_url was removed); accepted for 0.x CLI stability, removed at 1.0"
|
|
3062
3061
|
).option(
|
|
3063
3062
|
"--source-root <path>",
|
|
3064
3063
|
"Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
|
|
@@ -3083,11 +3082,10 @@ async function doRunInit(options, ctx) {
|
|
|
3083
3082
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3084
3083
|
const repositoryRoot = await resolveRepositoryRootForInit(cwd);
|
|
3085
3084
|
const workspaceName = options.name ?? basename4(repositoryRoot);
|
|
3086
|
-
let repositoryUrl;
|
|
3087
3085
|
if (options.repoUrl !== void 0) {
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3086
|
+
console.error(
|
|
3087
|
+
"Warning: --repo-url is deprecated and ignored (project.repository_url was removed); the flag will be removed at 1.0."
|
|
3088
|
+
);
|
|
3091
3089
|
}
|
|
3092
3090
|
const sourceRoots = (options.sourceRoot ?? []).map((p) => {
|
|
3093
3091
|
const rel = relative(repositoryRoot, resolve5(cwd, p));
|
|
@@ -3098,7 +3096,6 @@ async function doRunInit(options, ctx) {
|
|
|
3098
3096
|
workspaceName,
|
|
3099
3097
|
...options.projectName !== void 0 ? { projectName: options.projectName } : {},
|
|
3100
3098
|
...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
|
|
3101
|
-
...repositoryUrl !== void 0 ? { repositoryUrl } : {},
|
|
3102
3099
|
...sourceRoots.length > 0 ? { sourceRoots } : {}
|
|
3103
3100
|
});
|
|
3104
3101
|
await writeManifest(paths, manifest, { force: options.force === true });
|
|
@@ -3661,13 +3658,17 @@ import {
|
|
|
3661
3658
|
readMarkdownFile as readMarkdownFile4,
|
|
3662
3659
|
reconcileSourceRoots,
|
|
3663
3660
|
removeMarkerSection,
|
|
3661
|
+
renderAnchorStarter,
|
|
3662
|
+
renderViewPresetBlock,
|
|
3664
3663
|
renderWithMarkers as renderWithMarkers4,
|
|
3665
3664
|
resolveRepositoryRoot as resolveRepositoryRoot8,
|
|
3666
3665
|
safeSimpleGit,
|
|
3666
|
+
seedMarkers,
|
|
3667
3667
|
summarizePresetPlan,
|
|
3668
3668
|
summarizeRosterDrift,
|
|
3669
3669
|
summarizeSymlinkPlan,
|
|
3670
3670
|
summarizeWiring,
|
|
3671
|
+
summarizeWiringDrift,
|
|
3671
3672
|
unknownManifestKeys,
|
|
3672
3673
|
writeManifest as writeManifest2,
|
|
3673
3674
|
writeMarkdownFile as writeMarkdownFile5
|
|
@@ -3677,7 +3678,7 @@ var CANONICAL_FILE = "AGENTS.md";
|
|
|
3677
3678
|
function registerProjectCommand(program2) {
|
|
3678
3679
|
const project = program2.command("project").description("Inspect a project's declared repo roster (read-only)");
|
|
3679
3680
|
project.command("check").description(
|
|
3680
|
-
"
|
|
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"
|
|
3681
3682
|
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
3682
3683
|
await runProjectCheck(opts);
|
|
3683
3684
|
});
|
|
@@ -3752,8 +3753,11 @@ function registerProjectCommand(program2) {
|
|
|
3752
3753
|
project.command("new").argument("[repos...]", "Extra repo paths (besides the anchor) to seed into the roster").description(
|
|
3753
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"
|
|
3754
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(
|
|
3755
3759
|
"--view <path>",
|
|
3756
|
-
"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)"
|
|
3757
3761
|
).option("--no-view", "Solo project: declare no workspace view").option(
|
|
3758
3762
|
"--local-only",
|
|
3759
3763
|
"Write a .basou/ full-exclude .gitignore block (keep the trail out of version control) instead of the default ignore+commit block"
|
|
@@ -3766,10 +3770,10 @@ function registerProjectCommand(program2) {
|
|
|
3766
3770
|
await runProjectDerive(opts);
|
|
3767
3771
|
});
|
|
3768
3772
|
project.command("retrofit").argument(
|
|
3769
|
-
"
|
|
3770
|
-
"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"
|
|
3771
3775
|
).description(
|
|
3772
|
-
"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"
|
|
3773
3777
|
).option(
|
|
3774
3778
|
"--apply",
|
|
3775
3779
|
"Relocate the AGENTS.md to the canonical and recreate the symlink (default: dry-run preview)"
|
|
@@ -3800,18 +3804,39 @@ async function doRunProjectCheck(options, ctx) {
|
|
|
3800
3804
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project check");
|
|
3801
3805
|
const paths = basouPaths10(repositoryRoot);
|
|
3802
3806
|
const manifest = await readManifest6(paths);
|
|
3803
|
-
const
|
|
3807
|
+
const roster = summarizeRosterDrift({
|
|
3804
3808
|
...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
|
|
3805
3809
|
sourceRoots: effectiveSourceRoots(manifest)
|
|
3806
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
|
+
};
|
|
3807
3831
|
if (options.json === true) {
|
|
3808
|
-
console.log(JSON.stringify(
|
|
3832
|
+
console.log(JSON.stringify(result));
|
|
3809
3833
|
} else {
|
|
3810
|
-
console.log(renderProjectCheck(
|
|
3834
|
+
console.log(renderProjectCheck(result));
|
|
3811
3835
|
}
|
|
3812
|
-
return
|
|
3836
|
+
return result;
|
|
3813
3837
|
}
|
|
3814
|
-
function renderProjectCheck(
|
|
3838
|
+
function renderProjectCheck(result) {
|
|
3839
|
+
const summary = result.roster;
|
|
3815
3840
|
const lines = [];
|
|
3816
3841
|
lines.push("# Project composition check (declared vs captured)");
|
|
3817
3842
|
lines.push("");
|
|
@@ -3846,8 +3871,58 @@ function renderProjectCheck(summary) {
|
|
|
3846
3871
|
for (const p of summary.extra) lines.push(`- ${p}`);
|
|
3847
3872
|
lines.push("");
|
|
3848
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
|
+
}
|
|
3849
3924
|
lines.push(
|
|
3850
|
-
"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."
|
|
3851
3926
|
);
|
|
3852
3927
|
return lines.join("\n");
|
|
3853
3928
|
}
|
|
@@ -4301,6 +4376,17 @@ function inspectSymlink(filePath, expectedTarget) {
|
|
|
4301
4376
|
const actual = readlinkSync(filePath);
|
|
4302
4377
|
return actual === expectedTarget ? { state: "correct" } : { state: "mismatch", actualTarget: actual };
|
|
4303
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
|
+
}
|
|
4304
4390
|
function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
4305
4391
|
const mode = instructionMode(entry);
|
|
4306
4392
|
const isSelf = mode === "self";
|
|
@@ -4312,7 +4398,42 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4312
4398
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
4313
4399
|
}
|
|
4314
4400
|
if (real === anchorReal) {
|
|
4315
|
-
|
|
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
|
+
};
|
|
4316
4437
|
}
|
|
4317
4438
|
if (!existsSync(join9(real, ".git"))) {
|
|
4318
4439
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
@@ -4366,6 +4487,53 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
4366
4487
|
function failureReason(error) {
|
|
4367
4488
|
return hasErrorCode(error) ? error.code : "unknown error";
|
|
4368
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
|
+
}
|
|
4369
4537
|
async function doRunProjectSymlinks(options, ctx) {
|
|
4370
4538
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4371
4539
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project symlinks");
|
|
@@ -4385,11 +4553,33 @@ async function doRunProjectSymlinks(options, ctx) {
|
|
|
4385
4553
|
for (const f of failed) failures.push({ repo: plan.path, file: f.file, message: f.message });
|
|
4386
4554
|
}
|
|
4387
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");
|
|
4388
4574
|
const result = {
|
|
4389
4575
|
...summary,
|
|
4576
|
+
ok: summary.ok && viewClean,
|
|
4390
4577
|
hasRoster: roster.length > 0,
|
|
4391
4578
|
applied: createdCount > 0,
|
|
4392
|
-
failures
|
|
4579
|
+
failures,
|
|
4580
|
+
...view !== void 0 ? { view } : {},
|
|
4581
|
+
viewCreated,
|
|
4582
|
+
viewFailures
|
|
4393
4583
|
};
|
|
4394
4584
|
if (options.json === true) {
|
|
4395
4585
|
console.log(JSON.stringify(result));
|
|
@@ -4437,7 +4627,7 @@ function renderProjectSymlinks(result) {
|
|
|
4437
4627
|
);
|
|
4438
4628
|
} else {
|
|
4439
4629
|
lines.push(
|
|
4440
|
-
"\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)."
|
|
4441
4631
|
);
|
|
4442
4632
|
}
|
|
4443
4633
|
lines.push("");
|
|
@@ -4486,11 +4676,61 @@ function renderProjectSymlinks(result) {
|
|
|
4486
4676
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
4487
4677
|
lines.push("");
|
|
4488
4678
|
}
|
|
4679
|
+
appendViewSymlinksSection(lines, result);
|
|
4489
4680
|
lines.push(
|
|
4490
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)."
|
|
4491
4682
|
);
|
|
4492
4683
|
return lines.join("\n");
|
|
4493
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
|
+
}
|
|
4494
4734
|
async function runProjectWorkspace(options, ctx = {}) {
|
|
4495
4735
|
try {
|
|
4496
4736
|
await doRunProjectWorkspace(options, ctx);
|
|
@@ -4881,6 +5121,86 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
4881
5121
|
...section.kind === "ok" ? { currentBlock: section.generated } : {}
|
|
4882
5122
|
};
|
|
4883
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
|
+
}
|
|
4884
5204
|
async function applyPresetPlan(anchorReal, plan) {
|
|
4885
5205
|
const file = canonicalFileFor(anchorReal, plan.canonicalName);
|
|
4886
5206
|
const label = canonicalLabelFor(plan.canonicalName);
|
|
@@ -4913,7 +5233,12 @@ async function doRunProjectPreset(options, ctx) {
|
|
|
4913
5233
|
const anchorReal = realpathSync(repositoryRoot);
|
|
4914
5234
|
const facts = [];
|
|
4915
5235
|
for (const entry of roster) facts.push(await gatherRepoPreset(repositoryRoot, anchorReal, entry));
|
|
4916
|
-
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
|
+
);
|
|
4917
5242
|
const failures = [];
|
|
4918
5243
|
let writtenCount = 0;
|
|
4919
5244
|
if (options.apply === true && summary.plans.length > 0) {
|
|
@@ -4926,11 +5251,30 @@ async function doRunProjectPreset(options, ctx) {
|
|
|
4926
5251
|
}
|
|
4927
5252
|
}
|
|
4928
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";
|
|
4929
5269
|
const result = {
|
|
4930
5270
|
...summary,
|
|
5271
|
+
ok: summary.ok && viewClean,
|
|
4931
5272
|
hasRoster: roster.length > 0,
|
|
4932
5273
|
applied: writtenCount > 0,
|
|
4933
|
-
failures
|
|
5274
|
+
failures,
|
|
5275
|
+
...view !== void 0 ? { view } : {},
|
|
5276
|
+
viewApplied,
|
|
5277
|
+
...viewFailure !== void 0 ? { viewFailure } : {}
|
|
4934
5278
|
};
|
|
4935
5279
|
if (options.json === true) {
|
|
4936
5280
|
console.log(JSON.stringify(result));
|
|
@@ -4983,7 +5327,7 @@ function renderProjectPreset(result) {
|
|
|
4983
5327
|
);
|
|
4984
5328
|
} else {
|
|
4985
5329
|
lines.push(
|
|
4986
|
-
"\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)."
|
|
4987
5331
|
);
|
|
4988
5332
|
}
|
|
4989
5333
|
lines.push("");
|
|
@@ -5020,10 +5364,11 @@ function renderProjectPreset(result) {
|
|
|
5020
5364
|
}
|
|
5021
5365
|
if (result.collisions.length > 0) {
|
|
5022
5366
|
lines.push(
|
|
5023
|
-
`## 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)`
|
|
5024
5368
|
);
|
|
5025
5369
|
for (const c of result.collisions) {
|
|
5026
|
-
|
|
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}`);
|
|
5027
5372
|
}
|
|
5028
5373
|
lines.push("");
|
|
5029
5374
|
}
|
|
@@ -5053,11 +5398,46 @@ function renderProjectPreset(result) {
|
|
|
5053
5398
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
5054
5399
|
lines.push("");
|
|
5055
5400
|
}
|
|
5401
|
+
appendViewPresetSection(lines, result);
|
|
5056
5402
|
lines.push(
|
|
5057
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."
|
|
5058
5404
|
);
|
|
5059
5405
|
return lines.join("\n");
|
|
5060
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
|
+
}
|
|
5061
5441
|
async function runProjectArchive(target, options, ctx = {}) {
|
|
5062
5442
|
try {
|
|
5063
5443
|
await doRunProjectArchive(target, options, ctx);
|
|
@@ -5840,10 +6220,21 @@ async function resolveRepositoryRootForNew(cwd) {
|
|
|
5840
6220
|
throw error;
|
|
5841
6221
|
}
|
|
5842
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
|
+
}
|
|
5843
6231
|
async function doRunProjectNew(repos, options, ctx) {
|
|
5844
6232
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5845
6233
|
const repositoryRoot = await resolveRepositoryRootForNew(cwd);
|
|
5846
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";
|
|
5847
6238
|
const declared = repos.map((p) => {
|
|
5848
6239
|
const abs = resolve7(cwd, p);
|
|
5849
6240
|
let real;
|
|
@@ -5868,11 +6259,15 @@ async function doRunProjectNew(repos, options, ctx) {
|
|
|
5868
6259
|
if (rel !== "." && !rosterPaths.includes(rel)) rosterPaths.push(rel);
|
|
5869
6260
|
}
|
|
5870
6261
|
const roster = rosterPaths.map((path) => ({ path }));
|
|
5871
|
-
const viewPath = options.view === false ? null : options.view ?? `../${
|
|
6262
|
+
const viewPath = options.view === false ? null : options.view ?? `../${viewStem}-workspace`;
|
|
5872
6263
|
const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
|
|
5873
6264
|
const paths = basouPaths10(repositoryRoot);
|
|
5874
6265
|
const existed = existsSync(paths.files.manifest);
|
|
5875
|
-
const manifest = createManifest2({
|
|
6266
|
+
const manifest = createManifest2({
|
|
6267
|
+
workspaceName,
|
|
6268
|
+
sourceRoots,
|
|
6269
|
+
...productName !== void 0 ? { projectName: productName } : {}
|
|
6270
|
+
});
|
|
5876
6271
|
manifest.repos = roster;
|
|
5877
6272
|
if (viewPath !== null) manifest.workspace.view = viewPath;
|
|
5878
6273
|
let applied = false;
|
|
@@ -5888,8 +6283,10 @@ async function doRunProjectNew(repos, options, ctx) {
|
|
|
5888
6283
|
}
|
|
5889
6284
|
const result = {
|
|
5890
6285
|
workspaceName,
|
|
6286
|
+
projectName: productName ?? null,
|
|
5891
6287
|
repos: roster,
|
|
5892
6288
|
view: viewPath,
|
|
6289
|
+
viewOverridesProjectName,
|
|
5893
6290
|
sourceRoots,
|
|
5894
6291
|
invalidRepos: [],
|
|
5895
6292
|
existed,
|
|
@@ -5922,11 +6319,12 @@ function renderProjectNew(result) {
|
|
|
5922
6319
|
);
|
|
5923
6320
|
lines.push("");
|
|
5924
6321
|
}
|
|
6322
|
+
const identity = result.projectName ?? result.workspaceName;
|
|
5925
6323
|
if (result.applied) {
|
|
5926
|
-
lines.push(`\u2705 Created \`.basou/\` for \`${
|
|
6324
|
+
lines.push(`\u2705 Created \`.basou/\` for \`${identity}\` and seeded the manifest:`);
|
|
5927
6325
|
} else {
|
|
5928
6326
|
lines.push(
|
|
5929
|
-
`Will create \`.basou/\` for \`${
|
|
6327
|
+
`Will create \`.basou/\` for \`${identity}\` and seed the manifest (dry-run; pass --apply to write):`
|
|
5930
6328
|
);
|
|
5931
6329
|
}
|
|
5932
6330
|
lines.push("");
|
|
@@ -5935,9 +6333,16 @@ function renderProjectNew(result) {
|
|
|
5935
6333
|
lines.push(`- ${r.path}${r.path === "." ? " (anchor)" : ""}`);
|
|
5936
6334
|
}
|
|
5937
6335
|
lines.push("");
|
|
6336
|
+
if (result.projectName !== null) {
|
|
6337
|
+
lines.push(`project name: ${result.projectName}`);
|
|
6338
|
+
lines.push("");
|
|
6339
|
+
}
|
|
5938
6340
|
lines.push(
|
|
5939
6341
|
result.view !== null ? `workspace view: ${result.view}` : "workspace view: none (solo project)"
|
|
5940
6342
|
);
|
|
6343
|
+
if (result.viewOverridesProjectName) {
|
|
6344
|
+
lines.push(" (note: --view set this path, so --project-name did not drive the view name)");
|
|
6345
|
+
}
|
|
5941
6346
|
lines.push("");
|
|
5942
6347
|
lines.push(`source_roots (${result.sourceRoots.length}):`);
|
|
5943
6348
|
for (const s of result.sourceRoots) lines.push(`- ${s}`);
|
|
@@ -5983,25 +6388,79 @@ async function doRunProjectDerive(options, ctx) {
|
|
|
5983
6388
|
const stepOpts = { apply };
|
|
5984
6389
|
console.log("# Generate project wiring in one pass (declaration \u2192 wiring)");
|
|
5985
6390
|
console.log("");
|
|
5986
|
-
console.log("## 1/
|
|
6391
|
+
console.log("## 1/6 sync source_roots (roster \u2192 capture config)");
|
|
5987
6392
|
await doRunProjectSync(stepOpts, stepCtx);
|
|
5988
6393
|
console.log("");
|
|
5989
|
-
console.log("## 2/
|
|
6394
|
+
console.log("## 2/6 generate instruction-file A preset (declaration \u2192 canonical)");
|
|
5990
6395
|
await doRunProjectPreset(stepOpts, stepCtx);
|
|
5991
6396
|
console.log("");
|
|
5992
|
-
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)");
|
|
5993
6401
|
await doRunProjectSymlinks(stepOpts, stepCtx);
|
|
5994
6402
|
console.log("");
|
|
5995
|
-
console.log("##
|
|
6403
|
+
console.log("## 5/6 generate workspace view (aggregate the roster repos)");
|
|
5996
6404
|
await doRunProjectWorkspace(stepOpts, stepCtx);
|
|
5997
6405
|
console.log("");
|
|
5998
|
-
console.log("##
|
|
6406
|
+
console.log("## 6/6 generate .gitignore (exclude public repos' instruction files)");
|
|
5999
6407
|
await doRunProjectGitignore(stepOpts, stepCtx);
|
|
6000
6408
|
console.log("");
|
|
6001
6409
|
console.log(
|
|
6002
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."
|
|
6003
6411
|
);
|
|
6004
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
|
+
}
|
|
6005
6464
|
async function runProjectRetrofit(repo, options, ctx = {}) {
|
|
6006
6465
|
try {
|
|
6007
6466
|
await doRunProjectRetrofit(repo, options, ctx);
|
|
@@ -6040,7 +6499,7 @@ function pathPresent(p) {
|
|
|
6040
6499
|
return false;
|
|
6041
6500
|
}
|
|
6042
6501
|
}
|
|
6043
|
-
function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal) {
|
|
6502
|
+
function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal, viewCanonicalName) {
|
|
6044
6503
|
const declaredEntry = roster.find((entry) => {
|
|
6045
6504
|
const entryAbs = resolve7(repositoryRoot, entry.path);
|
|
6046
6505
|
if (argReal !== void 0) {
|
|
@@ -6064,6 +6523,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
6064
6523
|
isAnchor: false,
|
|
6065
6524
|
reachable: false,
|
|
6066
6525
|
canonicalName,
|
|
6526
|
+
...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
|
|
6067
6527
|
agentsState: "absent",
|
|
6068
6528
|
canonicalExists: false,
|
|
6069
6529
|
regularSpokes: []
|
|
@@ -6079,6 +6539,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
6079
6539
|
isAnchor,
|
|
6080
6540
|
reachable,
|
|
6081
6541
|
canonicalName,
|
|
6542
|
+
...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
|
|
6082
6543
|
agentsState: inspectAgentsState(join9(argReal, CANONICAL_FILE)),
|
|
6083
6544
|
canonicalExists: pathPresent(canonicalFile),
|
|
6084
6545
|
regularSpokes: regularFileSpokes(argReal)
|
|
@@ -6105,6 +6566,44 @@ function relocateAgentsFile(repoReal, canonicalFile) {
|
|
|
6105
6566
|
return { ok: false, message: failureReason(error), partial: true };
|
|
6106
6567
|
}
|
|
6107
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
|
+
}
|
|
6108
6607
|
async function doRunProjectRetrofit(repo, options, ctx) {
|
|
6109
6608
|
const cwd = ctx.cwd ?? process.cwd();
|
|
6110
6609
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project retrofit");
|
|
@@ -6112,6 +6611,36 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6112
6611
|
const manifest = await readManifest6(paths);
|
|
6113
6612
|
const roster = manifest.repos ?? [];
|
|
6114
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
|
+
}
|
|
6115
6644
|
const argAbs = resolve7(repositoryRoot, repo);
|
|
6116
6645
|
let argReal;
|
|
6117
6646
|
try {
|
|
@@ -6119,7 +6648,16 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6119
6648
|
} catch {
|
|
6120
6649
|
argReal = void 0;
|
|
6121
6650
|
}
|
|
6122
|
-
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
|
+
);
|
|
6123
6661
|
const plan = classifyRetrofit(facts);
|
|
6124
6662
|
let applied = false;
|
|
6125
6663
|
let failure;
|
|
@@ -6134,12 +6672,19 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6134
6672
|
partial = res.partial;
|
|
6135
6673
|
}
|
|
6136
6674
|
}
|
|
6675
|
+
let view;
|
|
6676
|
+
if (roster.length > 0) {
|
|
6677
|
+
view = gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster);
|
|
6678
|
+
}
|
|
6137
6679
|
const result = {
|
|
6680
|
+
kind: "repo",
|
|
6138
6681
|
...plan,
|
|
6139
6682
|
hasRoster: roster.length > 0,
|
|
6140
6683
|
applied,
|
|
6141
6684
|
...failure !== void 0 ? { failure } : {},
|
|
6142
|
-
...partial ? { partial } : {}
|
|
6685
|
+
...partial ? { partial } : {},
|
|
6686
|
+
...view !== void 0 ? { view } : {},
|
|
6687
|
+
viewApplied: false
|
|
6143
6688
|
};
|
|
6144
6689
|
if (options.json === true) {
|
|
6145
6690
|
console.log(JSON.stringify(result));
|
|
@@ -6148,6 +6693,47 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6148
6693
|
}
|
|
6149
6694
|
return result;
|
|
6150
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
|
+
}
|
|
6151
6737
|
function appendSpokeChecklist(lines, spokes) {
|
|
6152
6738
|
if (spokes.length === 0) return;
|
|
6153
6739
|
lines.push(
|
|
@@ -6172,6 +6758,25 @@ function renderProjectRetrofit(result) {
|
|
|
6172
6758
|
);
|
|
6173
6759
|
return lines.join("\n");
|
|
6174
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
|
+
}
|
|
6175
6780
|
const canonical2 = `agents/${result.canonicalName}/${CANONICAL_FILE}`;
|
|
6176
6781
|
if (result.action === "refuse") {
|
|
6177
6782
|
if (result.reason === "not-declared") {
|
|
@@ -6194,12 +6799,18 @@ function renderProjectRetrofit(result) {
|
|
|
6194
6799
|
lines.push(
|
|
6195
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.`
|
|
6196
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
|
+
);
|
|
6197
6806
|
} else if (result.reason === "canonical-exists") {
|
|
6198
6807
|
lines.push(
|
|
6199
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.`
|
|
6200
6809
|
);
|
|
6201
6810
|
}
|
|
6202
|
-
|
|
6811
|
+
lines.push("");
|
|
6812
|
+
appendViewRetrofitSection(lines, result);
|
|
6813
|
+
return lines.join("\n").trimEnd();
|
|
6203
6814
|
}
|
|
6204
6815
|
if (result.action === "skip") {
|
|
6205
6816
|
if (result.reason === "already-symlink") {
|
|
@@ -6213,6 +6824,7 @@ function renderProjectRetrofit(result) {
|
|
|
6213
6824
|
}
|
|
6214
6825
|
lines.push("");
|
|
6215
6826
|
appendSpokeChecklist(lines, result.regularSpokes);
|
|
6827
|
+
appendViewRetrofitSection(lines, result);
|
|
6216
6828
|
return lines.join("\n").trimEnd();
|
|
6217
6829
|
}
|
|
6218
6830
|
if (result.failure !== void 0) {
|
|
@@ -6226,7 +6838,9 @@ function renderProjectRetrofit(result) {
|
|
|
6226
6838
|
} else {
|
|
6227
6839
|
lines.push("Nothing was changed. Resolve the cause and re-run.");
|
|
6228
6840
|
}
|
|
6229
|
-
|
|
6841
|
+
lines.push("");
|
|
6842
|
+
appendViewRetrofitSection(lines, result);
|
|
6843
|
+
return lines.join("\n").trimEnd();
|
|
6230
6844
|
}
|
|
6231
6845
|
if (result.applied) {
|
|
6232
6846
|
lines.push(
|
|
@@ -6241,6 +6855,7 @@ function renderProjectRetrofit(result) {
|
|
|
6241
6855
|
}
|
|
6242
6856
|
lines.push("");
|
|
6243
6857
|
appendSpokeChecklist(lines, result.regularSpokes);
|
|
6858
|
+
appendViewRetrofitSection(lines, result);
|
|
6244
6859
|
lines.push(
|
|
6245
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)."
|
|
6246
6861
|
);
|
|
@@ -6707,19 +7322,19 @@ function parseInterval(value) {
|
|
|
6707
7322
|
return seconds;
|
|
6708
7323
|
}
|
|
6709
7324
|
function abortableSleep(ms, signal) {
|
|
6710
|
-
return new Promise((
|
|
7325
|
+
return new Promise((resolve14) => {
|
|
6711
7326
|
if (signal.aborted) {
|
|
6712
|
-
|
|
7327
|
+
resolve14();
|
|
6713
7328
|
return;
|
|
6714
7329
|
}
|
|
6715
7330
|
let timer;
|
|
6716
7331
|
const onAbort = () => {
|
|
6717
7332
|
clearTimeout(timer);
|
|
6718
|
-
|
|
7333
|
+
resolve14();
|
|
6719
7334
|
};
|
|
6720
7335
|
timer = setTimeout(() => {
|
|
6721
7336
|
signal.removeEventListener("abort", onAbort);
|
|
6722
|
-
|
|
7337
|
+
resolve14();
|
|
6723
7338
|
}, ms);
|
|
6724
7339
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
6725
7340
|
});
|
|
@@ -8606,6 +9221,8 @@ async function doRunStatus(options, ctx) {
|
|
|
8606
9221
|
if (findErrorCode14(error, "ENOENT")) {
|
|
8607
9222
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
8608
9223
|
}
|
|
9224
|
+
const gateMessage = formatVersionGateMessage(error);
|
|
9225
|
+
if (gateMessage !== void 0) throw new Error(gateMessage, { cause: error });
|
|
8609
9226
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
8610
9227
|
}
|
|
8611
9228
|
const snapshot = await buildStatusSnapshot({ manifest, paths });
|
|
@@ -8637,6 +9254,18 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
8637
9254
|
throw error;
|
|
8638
9255
|
}
|
|
8639
9256
|
}
|
|
9257
|
+
function formatVersionGateMessage(error) {
|
|
9258
|
+
const issues = error.issues;
|
|
9259
|
+
if (!Array.isArray(issues)) return void 0;
|
|
9260
|
+
for (const issue of issues) {
|
|
9261
|
+
const path = issue.path;
|
|
9262
|
+
const message = issue.message;
|
|
9263
|
+
if (Array.isArray(path) && (path.includes("schema_version") || path.includes("basou_version")) && typeof message === "string" && message.startsWith("unsupported .basou format version")) {
|
|
9264
|
+
return message;
|
|
9265
|
+
}
|
|
9266
|
+
}
|
|
9267
|
+
return void 0;
|
|
9268
|
+
}
|
|
8640
9269
|
|
|
8641
9270
|
// src/commands/task.ts
|
|
8642
9271
|
import { readFile as readFile7 } from "fs/promises";
|
|
@@ -9859,7 +10488,7 @@ async function assertWorkspaceInitialized14(basouRoot) {
|
|
|
9859
10488
|
// src/commands/view.ts
|
|
9860
10489
|
import { spawn } from "child_process";
|
|
9861
10490
|
import { createHash } from "crypto";
|
|
9862
|
-
import { basename as
|
|
10491
|
+
import { basename as basename8, resolve as resolve13 } from "path";
|
|
9863
10492
|
import {
|
|
9864
10493
|
assertBasouRootSafe as assertBasouRootSafe18,
|
|
9865
10494
|
basouPaths as basouPaths21,
|
|
@@ -9997,7 +10626,7 @@ function formatSafetyReport(result) {
|
|
|
9997
10626
|
|
|
9998
10627
|
// src/lib/view-server.ts
|
|
9999
10628
|
import { createServer } from "http";
|
|
10000
|
-
import { join as join17 } from "path";
|
|
10629
|
+
import { basename as basename7, join as join17, resolve as resolve12 } from "path";
|
|
10001
10630
|
import {
|
|
10002
10631
|
computeWorkStats as computeWorkStats2,
|
|
10003
10632
|
enumerateApprovals as enumerateApprovals2,
|
|
@@ -10013,9 +10642,42 @@ import {
|
|
|
10013
10642
|
readTaskFile as readTaskFile2,
|
|
10014
10643
|
renderDecisions as renderDecisions3,
|
|
10015
10644
|
renderHandoff as renderHandoff3,
|
|
10016
|
-
summarizeOrientation
|
|
10645
|
+
summarizeOrientation,
|
|
10646
|
+
tryRemoteUrl
|
|
10017
10647
|
} from "@basou/core";
|
|
10018
10648
|
|
|
10649
|
+
// src/lib/repo-url.ts
|
|
10650
|
+
function toBrowserUrl(remote) {
|
|
10651
|
+
const raw = remote.trim();
|
|
10652
|
+
if (raw.length === 0) return null;
|
|
10653
|
+
let host;
|
|
10654
|
+
let path;
|
|
10655
|
+
if (raw.includes("://")) {
|
|
10656
|
+
let parsed;
|
|
10657
|
+
try {
|
|
10658
|
+
parsed = new URL(raw);
|
|
10659
|
+
} catch {
|
|
10660
|
+
return null;
|
|
10661
|
+
}
|
|
10662
|
+
const scheme = parsed.protocol;
|
|
10663
|
+
if (scheme !== "ssh:" && scheme !== "git:" && scheme !== "http:" && scheme !== "https:") {
|
|
10664
|
+
return null;
|
|
10665
|
+
}
|
|
10666
|
+
host = scheme === "http:" || scheme === "https:" ? parsed.host : parsed.hostname;
|
|
10667
|
+
path = parsed.pathname;
|
|
10668
|
+
} else {
|
|
10669
|
+
const match = /^[^@/\s]+@([^:/\s@]+):(.+)$/.exec(raw);
|
|
10670
|
+
if (match === null || match[1] === void 0 || match[2] === void 0) return null;
|
|
10671
|
+
host = match[1];
|
|
10672
|
+
path = match[2];
|
|
10673
|
+
}
|
|
10674
|
+
const cleanPath = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "");
|
|
10675
|
+
if (host.length === 0 || cleanPath.length === 0) return null;
|
|
10676
|
+
if (/\s/.test(host) || /\s/.test(cleanPath)) return null;
|
|
10677
|
+
if (cleanPath.split("/").some((seg) => seg === "." || seg === "..")) return null;
|
|
10678
|
+
return `https://${host}/${cleanPath}`;
|
|
10679
|
+
}
|
|
10680
|
+
|
|
10019
10681
|
// src/lib/view-ui.ts
|
|
10020
10682
|
var VIEW_HTML = `<!doctype html>
|
|
10021
10683
|
<html lang="en">
|
|
@@ -10417,9 +11079,30 @@ var VIEW_HTML = `<!doctype html>
|
|
|
10417
11079
|
card(c.approvalsPending, 'approvals pending')
|
|
10418
11080
|
]);
|
|
10419
11081
|
detail.appendChild(cards);
|
|
11082
|
+
renderRepos(detail, d.repos || []);
|
|
10420
11083
|
detail.appendChild(el('p', { class: 'muted', text: 'repo: ' + d.repoRoot }));
|
|
10421
11084
|
}).catch(fail);
|
|
10422
11085
|
}
|
|
11086
|
+
// The declared roster repos, each with a LIVE git link (derived server-side
|
|
11087
|
+
// from the repo's local git config at request time, never stored). A repo
|
|
11088
|
+
// with no remote renders as "local only". Links open in a new tab.
|
|
11089
|
+
function renderRepos(detail, repos) {
|
|
11090
|
+
if (!repos.length) return;
|
|
11091
|
+
detail.appendChild(el('h3', { text: 'Repos' }));
|
|
11092
|
+
var rows = el('div', { class: 'repos' }, []);
|
|
11093
|
+
repos.forEach(function (r) {
|
|
11094
|
+
var vis = r.visibility ? (' (' + r.visibility + ')') : '';
|
|
11095
|
+
var link = r.url
|
|
11096
|
+
? el('a', { href: r.url, target: '_blank', rel: 'noopener noreferrer', text: r.url })
|
|
11097
|
+
: el('span', { class: 'muted', text: 'local only' });
|
|
11098
|
+
rows.appendChild(el('div', { class: 'f' }, [
|
|
11099
|
+
el('strong', { text: r.name }),
|
|
11100
|
+
el('span', { class: 'muted', text: vis + ' ' }),
|
|
11101
|
+
link
|
|
11102
|
+
]));
|
|
11103
|
+
});
|
|
11104
|
+
detail.appendChild(rows);
|
|
11105
|
+
}
|
|
10423
11106
|
function card(n, label) {
|
|
10424
11107
|
return el('div', { class: 'card' }, [
|
|
10425
11108
|
el('div', { class: 'n', text: String(n) }),
|
|
@@ -10658,7 +11341,7 @@ function startViewServer(opts) {
|
|
|
10658
11341
|
};
|
|
10659
11342
|
let boundPort = port;
|
|
10660
11343
|
const getPort = () => boundPort;
|
|
10661
|
-
return new Promise((
|
|
11344
|
+
return new Promise((resolve14, reject) => {
|
|
10662
11345
|
const server = createServer((req, res) => {
|
|
10663
11346
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
10664
11347
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -10669,7 +11352,7 @@ function startViewServer(opts) {
|
|
|
10669
11352
|
const address = server.address();
|
|
10670
11353
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
10671
11354
|
server.off("error", reject);
|
|
10672
|
-
|
|
11355
|
+
resolve14({
|
|
10673
11356
|
url: `http://${host}:${boundPort}`,
|
|
10674
11357
|
port: boundPort,
|
|
10675
11358
|
close: () => closeServer(server)
|
|
@@ -10681,8 +11364,8 @@ function isAddressInfo(value) {
|
|
|
10681
11364
|
return value !== null && typeof value === "object";
|
|
10682
11365
|
}
|
|
10683
11366
|
function closeServer(server) {
|
|
10684
|
-
return new Promise((
|
|
10685
|
-
server.close(() =>
|
|
11367
|
+
return new Promise((resolve14) => {
|
|
11368
|
+
server.close(() => resolve14());
|
|
10686
11369
|
server.closeAllConnections();
|
|
10687
11370
|
});
|
|
10688
11371
|
}
|
|
@@ -10725,20 +11408,29 @@ async function handleGet(res, pathname, deps) {
|
|
|
10725
11408
|
sendError(res, 404, "Unknown workspace");
|
|
10726
11409
|
return;
|
|
10727
11410
|
}
|
|
10728
|
-
if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider)) {
|
|
11411
|
+
if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider, remoteUrlOf(deps))) {
|
|
10729
11412
|
sendError(res, 404, "Not found");
|
|
10730
11413
|
}
|
|
10731
11414
|
return;
|
|
10732
11415
|
}
|
|
10733
11416
|
if (pathname.startsWith(API_PREFIX)) {
|
|
10734
11417
|
const sub = pathname.slice(API_PREFIX.length);
|
|
10735
|
-
if (!await handleWorkspaceGet(
|
|
11418
|
+
if (!await handleWorkspaceGet(
|
|
11419
|
+
res,
|
|
11420
|
+
sub,
|
|
11421
|
+
primaryWorkspace(deps),
|
|
11422
|
+
deps.nowProvider,
|
|
11423
|
+
remoteUrlOf(deps)
|
|
11424
|
+
)) {
|
|
10736
11425
|
sendError(res, 404, "Not found");
|
|
10737
11426
|
}
|
|
10738
11427
|
return;
|
|
10739
11428
|
}
|
|
10740
11429
|
sendError(res, 404, "Not found");
|
|
10741
11430
|
}
|
|
11431
|
+
function remoteUrlOf(deps) {
|
|
11432
|
+
return deps.remoteUrlOf ?? tryRemoteUrl;
|
|
11433
|
+
}
|
|
10742
11434
|
async function handlePost(res, pathname, body, deps, runExclusive) {
|
|
10743
11435
|
const scoped = matchWsRoute(pathname);
|
|
10744
11436
|
if (scoped !== null) {
|
|
@@ -10761,9 +11453,9 @@ async function handlePost(res, pathname, body, deps, runExclusive) {
|
|
|
10761
11453
|
}
|
|
10762
11454
|
sendError(res, 404, "Not found");
|
|
10763
11455
|
}
|
|
10764
|
-
async function handleWorkspaceGet(res, sub, ws, nowProvider) {
|
|
11456
|
+
async function handleWorkspaceGet(res, sub, ws, nowProvider, resolveRemoteUrl) {
|
|
10765
11457
|
if (sub === "overview") {
|
|
10766
|
-
sendJson(res, 200, await overview(ws, nowProvider));
|
|
11458
|
+
sendJson(res, 200, await overview(ws, nowProvider, resolveRemoteUrl));
|
|
10767
11459
|
return true;
|
|
10768
11460
|
}
|
|
10769
11461
|
if (sub === "sessions") {
|
|
@@ -10896,7 +11588,7 @@ async function captureStaleness(ws, nowIso) {
|
|
|
10896
11588
|
const probe = await probeStaleness({ ctx: ws.importCtx, paths: ws.paths, nowIso });
|
|
10897
11589
|
return probe === null ? { checked: false } : { checked: true, ...probe };
|
|
10898
11590
|
}
|
|
10899
|
-
async function overview(ws, nowProvider) {
|
|
11591
|
+
async function overview(ws, nowProvider, resolveRemoteUrl) {
|
|
10900
11592
|
let manifest;
|
|
10901
11593
|
try {
|
|
10902
11594
|
manifest = await readManifest13(ws.paths);
|
|
@@ -10909,6 +11601,7 @@ async function overview(ws, nowProvider) {
|
|
|
10909
11601
|
const nowIso = nowProvider().toISOString();
|
|
10910
11602
|
const handoff = await renderHandoff3({ paths: ws.paths, nowIso });
|
|
10911
11603
|
const approvals = await enumerateApprovals2(ws.paths);
|
|
11604
|
+
const repos = await rosterRepos(ws.repoRoot, manifest, resolveRemoteUrl);
|
|
10912
11605
|
return {
|
|
10913
11606
|
initialized: true,
|
|
10914
11607
|
repoRoot: ws.repoRoot,
|
|
@@ -10926,9 +11619,26 @@ async function overview(ws, nowProvider) {
|
|
|
10926
11619
|
approvalsPending: approvals.pending.length,
|
|
10927
11620
|
approvalsResolved: approvals.resolved.length
|
|
10928
11621
|
},
|
|
11622
|
+
repos,
|
|
10929
11623
|
generatedAt: nowIso
|
|
10930
11624
|
};
|
|
10931
11625
|
}
|
|
11626
|
+
async function rosterRepos(repoRoot, manifest, resolveRemoteUrl) {
|
|
11627
|
+
const roster = manifest.repos ?? [];
|
|
11628
|
+
return Promise.all(
|
|
11629
|
+
roster.map(async (repo) => {
|
|
11630
|
+
const abs = resolve12(repoRoot, repo.path);
|
|
11631
|
+
const remote = await resolveRemoteUrl(abs);
|
|
11632
|
+
const url = remote !== void 0 ? toBrowserUrl(remote) : null;
|
|
11633
|
+
return {
|
|
11634
|
+
name: basename7(abs),
|
|
11635
|
+
path: repo.path,
|
|
11636
|
+
...url !== null ? { url } : {},
|
|
11637
|
+
...repo.visibility !== void 0 ? { visibility: repo.visibility } : {}
|
|
11638
|
+
};
|
|
11639
|
+
})
|
|
11640
|
+
);
|
|
11641
|
+
}
|
|
10932
11642
|
async function sessionsList(ws, nowProvider) {
|
|
10933
11643
|
const entries = await loadSessionEntries4(ws.paths, { now: nowProvider() });
|
|
10934
11644
|
const sessions = entries.map((entry) => ({
|
|
@@ -11173,15 +11883,20 @@ async function buildSingleDeps(ctx, cwd) {
|
|
|
11173
11883
|
const paths = basouPaths21(repositoryRoot);
|
|
11174
11884
|
await assertWorkspaceInitialized15(paths.root);
|
|
11175
11885
|
const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
|
|
11176
|
-
return {
|
|
11886
|
+
return {
|
|
11887
|
+
workspaces: [entry],
|
|
11888
|
+
mode: "single",
|
|
11889
|
+
nowProvider: nowProviderOf(ctx),
|
|
11890
|
+
...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
|
|
11891
|
+
};
|
|
11177
11892
|
}
|
|
11178
11893
|
async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
11179
|
-
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path:
|
|
11894
|
+
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve13(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
|
|
11180
11895
|
const entries = [];
|
|
11181
11896
|
const seenPath = /* @__PURE__ */ new Set();
|
|
11182
11897
|
const seenKey = /* @__PURE__ */ new Set();
|
|
11183
11898
|
for (const spec of specs) {
|
|
11184
|
-
const repoRoot =
|
|
11899
|
+
const repoRoot = resolve13(spec.path);
|
|
11185
11900
|
if (seenPath.has(repoRoot)) continue;
|
|
11186
11901
|
seenPath.add(repoRoot);
|
|
11187
11902
|
const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
|
|
@@ -11191,7 +11906,12 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
|
11191
11906
|
entries.push({ ...entry, key });
|
|
11192
11907
|
}
|
|
11193
11908
|
if (entries.length === 0) throw new Error("No workspaces to show.");
|
|
11194
|
-
return {
|
|
11909
|
+
return {
|
|
11910
|
+
workspaces: entries,
|
|
11911
|
+
mode: "portfolio",
|
|
11912
|
+
nowProvider: nowProviderOf(ctx),
|
|
11913
|
+
...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
|
|
11914
|
+
};
|
|
11195
11915
|
}
|
|
11196
11916
|
async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
11197
11917
|
const paths = basouPaths21(repoRoot);
|
|
@@ -11214,7 +11934,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
|
11214
11934
|
const notFound = error instanceof Error && error.message === "YAML file not found";
|
|
11215
11935
|
return {
|
|
11216
11936
|
key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
|
|
11217
|
-
label: labelOverride ??
|
|
11937
|
+
label: labelOverride ?? basename8(repoRoot),
|
|
11218
11938
|
paths,
|
|
11219
11939
|
repoRoot,
|
|
11220
11940
|
importCtx,
|
|
@@ -11253,7 +11973,7 @@ function openInBrowser(url, override) {
|
|
|
11253
11973
|
}
|
|
11254
11974
|
}
|
|
11255
11975
|
function waitForShutdown(signal) {
|
|
11256
|
-
return new Promise((
|
|
11976
|
+
return new Promise((resolve14) => {
|
|
11257
11977
|
const cleanup = () => {
|
|
11258
11978
|
process.off("SIGINT", onSignal);
|
|
11259
11979
|
process.off("SIGTERM", onSignal);
|
|
@@ -11261,18 +11981,18 @@ function waitForShutdown(signal) {
|
|
|
11261
11981
|
};
|
|
11262
11982
|
const onSignal = () => {
|
|
11263
11983
|
cleanup();
|
|
11264
|
-
|
|
11984
|
+
resolve14();
|
|
11265
11985
|
};
|
|
11266
11986
|
const onAbort = () => {
|
|
11267
11987
|
cleanup();
|
|
11268
|
-
|
|
11988
|
+
resolve14();
|
|
11269
11989
|
};
|
|
11270
11990
|
process.on("SIGINT", onSignal);
|
|
11271
11991
|
process.on("SIGTERM", onSignal);
|
|
11272
11992
|
if (signal !== void 0) {
|
|
11273
11993
|
if (signal.aborted) {
|
|
11274
11994
|
cleanup();
|
|
11275
|
-
|
|
11995
|
+
resolve14();
|
|
11276
11996
|
return;
|
|
11277
11997
|
}
|
|
11278
11998
|
signal.addEventListener("abort", onAbort);
|