@basou/cli 0.26.0 → 0.28.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 +315 -9
- package/dist/index.js.map +1 -1
- package/dist/program.js +315 -9
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3365,6 +3365,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
3365
3365
|
// src/commands/project.ts
|
|
3366
3366
|
import {
|
|
3367
3367
|
closeSync,
|
|
3368
|
+
copyFileSync,
|
|
3368
3369
|
existsSync,
|
|
3369
3370
|
constants as fsConstants,
|
|
3370
3371
|
ftruncateSync,
|
|
@@ -3385,10 +3386,12 @@ import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3,
|
|
|
3385
3386
|
import {
|
|
3386
3387
|
appendBasouGitignore as appendBasouGitignore2,
|
|
3387
3388
|
basouPaths as basouPaths10,
|
|
3389
|
+
classifyRetrofit,
|
|
3388
3390
|
createManifest as createManifest2,
|
|
3389
3391
|
ensureBasouDirectory as ensureBasouDirectory2,
|
|
3390
3392
|
GENERATED_END,
|
|
3391
3393
|
GENERATED_START,
|
|
3394
|
+
instructionMode,
|
|
3392
3395
|
isGitNotFound,
|
|
3393
3396
|
parseMarkers,
|
|
3394
3397
|
pathBasename,
|
|
@@ -3505,6 +3508,17 @@ function registerProjectCommand(program2) {
|
|
|
3505
3508
|
).option("--apply", "Run every step in apply mode (default: dry-run preview)").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
3506
3509
|
await runProjectDerive(opts);
|
|
3507
3510
|
});
|
|
3511
|
+
project.command("retrofit").argument(
|
|
3512
|
+
"<repo>",
|
|
3513
|
+
"The declared roster repo whose hand-authored AGENTS.md to relocate (e.g. ../foo)"
|
|
3514
|
+
).description(
|
|
3515
|
+
"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"
|
|
3516
|
+
).option(
|
|
3517
|
+
"--apply",
|
|
3518
|
+
"Relocate the AGENTS.md to the canonical and recreate the symlink (default: dry-run preview)"
|
|
3519
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repo, opts) => {
|
|
3520
|
+
await runProjectRetrofit(repo, opts);
|
|
3521
|
+
});
|
|
3508
3522
|
}
|
|
3509
3523
|
async function runProjectCheck(options, ctx = {}) {
|
|
3510
3524
|
try {
|
|
@@ -3767,7 +3781,8 @@ async function isTrackedByGit(repoRoot, relPath) {
|
|
|
3767
3781
|
async function gatherRepoWiring(repositoryRoot, entry) {
|
|
3768
3782
|
const base = {
|
|
3769
3783
|
path: entry.path,
|
|
3770
|
-
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
|
|
3784
|
+
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
|
|
3785
|
+
...instructionMode(entry) === "self" ? { self: true } : {}
|
|
3771
3786
|
};
|
|
3772
3787
|
let real;
|
|
3773
3788
|
try {
|
|
@@ -3850,6 +3865,13 @@ function renderProjectWiring(result) {
|
|
|
3850
3865
|
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
3851
3866
|
lines.push("");
|
|
3852
3867
|
}
|
|
3868
|
+
if (result.self.length > 0) {
|
|
3869
|
+
lines.push(
|
|
3870
|
+
`## instructions: self (${result.self.length}) \u2014 committed instruction files are intentional (no leak risk)`
|
|
3871
|
+
);
|
|
3872
|
+
for (const p of result.self) lines.push(`- ${p}`);
|
|
3873
|
+
lines.push("");
|
|
3874
|
+
}
|
|
3853
3875
|
if (result.incomplete.length > 0) {
|
|
3854
3876
|
lines.push(
|
|
3855
3877
|
`## Missing instruction files (${result.incomplete.length}) \u2014 to be filled by a later generation slice`
|
|
@@ -3878,7 +3900,8 @@ async function runProjectGitignore(options, ctx = {}) {
|
|
|
3878
3900
|
function gatherRepoGitignore(repositoryRoot, entry) {
|
|
3879
3901
|
const base = {
|
|
3880
3902
|
path: entry.path,
|
|
3881
|
-
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
|
|
3903
|
+
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
|
|
3904
|
+
...instructionMode(entry) === "self" ? { self: true } : {}
|
|
3882
3905
|
};
|
|
3883
3906
|
let real;
|
|
3884
3907
|
try {
|
|
@@ -3973,6 +3996,13 @@ function renderProjectGitignore(result) {
|
|
|
3973
3996
|
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
3974
3997
|
lines.push("");
|
|
3975
3998
|
}
|
|
3999
|
+
if (result.self.length > 0) {
|
|
4000
|
+
lines.push(
|
|
4001
|
+
`## instructions: self (${result.self.length}) \u2014 skipped by design; their committed instruction files are shared, never gitignored`
|
|
4002
|
+
);
|
|
4003
|
+
for (const p of result.self) lines.push(`- ${p}`);
|
|
4004
|
+
lines.push("");
|
|
4005
|
+
}
|
|
3976
4006
|
if (result.unreachable.length > 0) {
|
|
3977
4007
|
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
3978
4008
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
@@ -3994,12 +4024,13 @@ async function runProjectSymlinks(options, ctx = {}) {
|
|
|
3994
4024
|
process.exitCode = 1;
|
|
3995
4025
|
}
|
|
3996
4026
|
}
|
|
3997
|
-
function expectedSymlinkTargets(repoDirReal, canonicalFile) {
|
|
3998
|
-
|
|
3999
|
-
{ name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) },
|
|
4027
|
+
function expectedSymlinkTargets(repoDirReal, canonicalFile, mode = "hub") {
|
|
4028
|
+
const spokes = [
|
|
4000
4029
|
{ name: "CLAUDE.md", target: CANONICAL_FILE },
|
|
4001
4030
|
{ name: ".github/copilot-instructions.md", target: `../${CANONICAL_FILE}` }
|
|
4002
4031
|
];
|
|
4032
|
+
if (mode === "self") return spokes;
|
|
4033
|
+
return [{ name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) }, ...spokes];
|
|
4003
4034
|
}
|
|
4004
4035
|
function inspectSymlink(filePath, expectedTarget) {
|
|
4005
4036
|
let isLink;
|
|
@@ -4014,7 +4045,9 @@ function inspectSymlink(filePath, expectedTarget) {
|
|
|
4014
4045
|
return actual === expectedTarget ? { state: "correct" } : { state: "mismatch", actualTarget: actual };
|
|
4015
4046
|
}
|
|
4016
4047
|
function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
4017
|
-
const
|
|
4048
|
+
const mode = instructionMode(entry);
|
|
4049
|
+
const isSelf = mode === "self";
|
|
4050
|
+
const base = { path: entry.path, ...isSelf ? { self: true } : {} };
|
|
4018
4051
|
let real;
|
|
4019
4052
|
try {
|
|
4020
4053
|
real = realpathSync(resolve7(repositoryRoot, entry.path));
|
|
@@ -4027,11 +4060,11 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4027
4060
|
if (!existsSync(join7(real, ".git"))) {
|
|
4028
4061
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
4029
4062
|
}
|
|
4030
|
-
const canonicalFile = join7(anchorReal, "agents", basename4(real), CANONICAL_FILE);
|
|
4063
|
+
const canonicalFile = isSelf ? join7(real, CANONICAL_FILE) : join7(anchorReal, "agents", basename4(real), CANONICAL_FILE);
|
|
4031
4064
|
if (!existsSync(canonicalFile)) {
|
|
4032
4065
|
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
4033
4066
|
}
|
|
4034
|
-
const files = expectedSymlinkTargets(real, canonicalFile).map(
|
|
4067
|
+
const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
|
|
4035
4068
|
(spec) => {
|
|
4036
4069
|
const { state, actualTarget } = inspectSymlink(join7(real, spec.name), spec.target);
|
|
4037
4070
|
return {
|
|
@@ -4184,6 +4217,13 @@ function renderProjectSymlinks(result) {
|
|
|
4184
4217
|
for (const p of result.missingCanonical) lines.push(`- ${p}`);
|
|
4185
4218
|
lines.push("");
|
|
4186
4219
|
}
|
|
4220
|
+
if (result.selfAgentsMissing.length > 0) {
|
|
4221
|
+
lines.push(
|
|
4222
|
+
`## AGENTS.md missing (${result.selfAgentsMissing.length}) \u2014 these \`instructions: self\` repos have no committed AGENTS.md yet; author it, then re-run to wire the spokes`
|
|
4223
|
+
);
|
|
4224
|
+
for (const p of result.selfAgentsMissing) lines.push(`- ${p}`);
|
|
4225
|
+
lines.push("");
|
|
4226
|
+
}
|
|
4187
4227
|
if (result.unreachable.length > 0) {
|
|
4188
4228
|
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
4189
4229
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
@@ -4534,6 +4574,9 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
4534
4574
|
...entry.language !== void 0 ? { language: entry.language } : {},
|
|
4535
4575
|
...entry.publishes !== void 0 ? { publishes: entry.publishes } : {}
|
|
4536
4576
|
};
|
|
4577
|
+
if (instructionMode(entry) === "self") {
|
|
4578
|
+
return { ...declared, self: true, isAnchor: false, reachable: true, canonicalPresent: false };
|
|
4579
|
+
}
|
|
4537
4580
|
let real;
|
|
4538
4581
|
try {
|
|
4539
4582
|
real = realpathSync(resolve7(repositoryRoot, entry.path));
|
|
@@ -4741,6 +4784,13 @@ function renderProjectPreset(result) {
|
|
|
4741
4784
|
for (const p of result.anchors) lines.push(`- ${p}`);
|
|
4742
4785
|
lines.push("");
|
|
4743
4786
|
}
|
|
4787
|
+
if (result.self.length > 0) {
|
|
4788
|
+
lines.push(
|
|
4789
|
+
`## instructions: self (${result.self.length}) \u2014 hands-off; their hand-authored AGENTS.md is never written by basou`
|
|
4790
|
+
);
|
|
4791
|
+
for (const p of result.self) lines.push(`- ${p}`);
|
|
4792
|
+
lines.push("");
|
|
4793
|
+
}
|
|
4744
4794
|
if (result.unreachable.length > 0) {
|
|
4745
4795
|
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
4746
4796
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
@@ -4846,13 +4896,15 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4846
4896
|
const targetAbs = resolve7(repositoryRoot, target);
|
|
4847
4897
|
const canonicalName = basename4(repoReal ?? targetAbs);
|
|
4848
4898
|
const roster = manifest.repos ?? [];
|
|
4849
|
-
const
|
|
4899
|
+
const declaredEntry = roster.find((r) => {
|
|
4850
4900
|
try {
|
|
4851
4901
|
return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
|
|
4852
4902
|
} catch {
|
|
4853
4903
|
return resolve7(repositoryRoot, r.path) === targetAbs;
|
|
4854
4904
|
}
|
|
4855
4905
|
});
|
|
4906
|
+
const inRoster = declaredEntry !== void 0;
|
|
4907
|
+
const isSelf = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
|
|
4856
4908
|
const cnFold = canonicalName.toLowerCase();
|
|
4857
4909
|
const canonicalShared = roster.some((r) => {
|
|
4858
4910
|
let rReal = null;
|
|
@@ -4874,6 +4926,16 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4874
4926
|
if (repoReal !== void 0) {
|
|
4875
4927
|
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
4876
4928
|
const { state, actualTarget } = inspectSymlink(join7(repoReal, spec.name), spec.target);
|
|
4929
|
+
if (isSelf) {
|
|
4930
|
+
if (state !== "missing")
|
|
4931
|
+
items.push({
|
|
4932
|
+
kind: "instruction-symlink",
|
|
4933
|
+
label: spec.name,
|
|
4934
|
+
state: "foreign",
|
|
4935
|
+
note: "instructions: self \u2014 committed, left untouched"
|
|
4936
|
+
});
|
|
4937
|
+
continue;
|
|
4938
|
+
}
|
|
4877
4939
|
if (state === "correct")
|
|
4878
4940
|
items.push({ kind: "instruction-symlink", label: spec.name, state: "removable" });
|
|
4879
4941
|
else if (state === "mismatch")
|
|
@@ -5683,6 +5745,250 @@ async function doRunProjectDerive(options, ctx) {
|
|
|
5683
5745
|
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."
|
|
5684
5746
|
);
|
|
5685
5747
|
}
|
|
5748
|
+
async function runProjectRetrofit(repo, options, ctx = {}) {
|
|
5749
|
+
try {
|
|
5750
|
+
await doRunProjectRetrofit(repo, options, ctx);
|
|
5751
|
+
} catch (error) {
|
|
5752
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
5753
|
+
process.exitCode = 1;
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
function inspectAgentsState(filePath) {
|
|
5757
|
+
let st;
|
|
5758
|
+
try {
|
|
5759
|
+
st = lstatSync(filePath);
|
|
5760
|
+
} catch (error) {
|
|
5761
|
+
if (hasErrorCode(error) && error.code === "ENOENT") return "absent";
|
|
5762
|
+
return "blocked";
|
|
5763
|
+
}
|
|
5764
|
+
if (st.isSymbolicLink()) return "symlink";
|
|
5765
|
+
return st.isFile() ? "regular-file" : "blocked";
|
|
5766
|
+
}
|
|
5767
|
+
function regularFileSpokes(repoReal) {
|
|
5768
|
+
const out = [];
|
|
5769
|
+
for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
|
|
5770
|
+
try {
|
|
5771
|
+
const st = lstatSync(join7(repoReal, spoke));
|
|
5772
|
+
if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
|
|
5773
|
+
} catch {
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
return out;
|
|
5777
|
+
}
|
|
5778
|
+
function pathPresent(p) {
|
|
5779
|
+
try {
|
|
5780
|
+
lstatSync(p);
|
|
5781
|
+
return true;
|
|
5782
|
+
} catch {
|
|
5783
|
+
return false;
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal) {
|
|
5787
|
+
const declaredEntry = roster.find((entry) => {
|
|
5788
|
+
const entryAbs = resolve7(repositoryRoot, entry.path);
|
|
5789
|
+
if (argReal !== void 0) {
|
|
5790
|
+
try {
|
|
5791
|
+
if (realpathSync(entryAbs) === argReal) return true;
|
|
5792
|
+
} catch {
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
return entryAbs === argAbs;
|
|
5796
|
+
});
|
|
5797
|
+
const declared = declaredEntry !== void 0;
|
|
5798
|
+
const self = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
|
|
5799
|
+
const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
|
|
5800
|
+
const path = displayRel === "" ? "." : displayRel;
|
|
5801
|
+
const canonicalName = basename4(argReal ?? argAbs);
|
|
5802
|
+
if (argReal === void 0) {
|
|
5803
|
+
return {
|
|
5804
|
+
path,
|
|
5805
|
+
declared,
|
|
5806
|
+
...self ? { self: true } : {},
|
|
5807
|
+
isAnchor: false,
|
|
5808
|
+
reachable: false,
|
|
5809
|
+
canonicalName,
|
|
5810
|
+
agentsState: "absent",
|
|
5811
|
+
canonicalExists: false,
|
|
5812
|
+
regularSpokes: []
|
|
5813
|
+
};
|
|
5814
|
+
}
|
|
5815
|
+
const isAnchor = argReal === anchorReal;
|
|
5816
|
+
const reachable = existsSync(join7(argReal, ".git"));
|
|
5817
|
+
const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5818
|
+
return {
|
|
5819
|
+
path,
|
|
5820
|
+
declared,
|
|
5821
|
+
...self ? { self: true } : {},
|
|
5822
|
+
isAnchor,
|
|
5823
|
+
reachable,
|
|
5824
|
+
canonicalName,
|
|
5825
|
+
agentsState: inspectAgentsState(join7(argReal, CANONICAL_FILE)),
|
|
5826
|
+
canonicalExists: pathPresent(canonicalFile),
|
|
5827
|
+
regularSpokes: regularFileSpokes(argReal)
|
|
5828
|
+
};
|
|
5829
|
+
}
|
|
5830
|
+
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
5831
|
+
const agentsFile = join7(repoReal, CANONICAL_FILE);
|
|
5832
|
+
try {
|
|
5833
|
+
mkdirSync(dirname2(canonicalFile), { recursive: true });
|
|
5834
|
+
} catch (error) {
|
|
5835
|
+
return { ok: false, message: failureReason(error), partial: false };
|
|
5836
|
+
}
|
|
5837
|
+
try {
|
|
5838
|
+
copyFileSync(agentsFile, canonicalFile, fsConstants.COPYFILE_EXCL);
|
|
5839
|
+
} catch (error) {
|
|
5840
|
+
const message = hasErrorCode(error) && error.code === "EEXIST" ? "canonical-exists" : failureReason(error);
|
|
5841
|
+
return { ok: false, message, partial: false };
|
|
5842
|
+
}
|
|
5843
|
+
try {
|
|
5844
|
+
unlinkSync(agentsFile);
|
|
5845
|
+
symlinkSync(relative2(repoReal, canonicalFile), agentsFile);
|
|
5846
|
+
return { ok: true };
|
|
5847
|
+
} catch (error) {
|
|
5848
|
+
return { ok: false, message: failureReason(error), partial: true };
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
async function doRunProjectRetrofit(repo, options, ctx) {
|
|
5852
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
5853
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project retrofit");
|
|
5854
|
+
const paths = basouPaths10(repositoryRoot);
|
|
5855
|
+
const manifest = await readManifest6(paths);
|
|
5856
|
+
const roster = manifest.repos ?? [];
|
|
5857
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
5858
|
+
const argAbs = resolve7(repositoryRoot, repo);
|
|
5859
|
+
let argReal;
|
|
5860
|
+
try {
|
|
5861
|
+
argReal = realpathSync(argAbs);
|
|
5862
|
+
} catch {
|
|
5863
|
+
argReal = void 0;
|
|
5864
|
+
}
|
|
5865
|
+
const facts = gatherRetrofit(repositoryRoot, anchorReal, roster, repo, argAbs, argReal);
|
|
5866
|
+
const plan = classifyRetrofit(facts);
|
|
5867
|
+
let applied = false;
|
|
5868
|
+
let failure;
|
|
5869
|
+
let partial = false;
|
|
5870
|
+
if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
|
|
5871
|
+
const canonicalFile = join7(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
|
|
5872
|
+
const res = relocateAgentsFile(argReal, canonicalFile);
|
|
5873
|
+
if (res.ok) {
|
|
5874
|
+
applied = true;
|
|
5875
|
+
} else {
|
|
5876
|
+
failure = res.message;
|
|
5877
|
+
partial = res.partial;
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
const result = {
|
|
5881
|
+
...plan,
|
|
5882
|
+
hasRoster: roster.length > 0,
|
|
5883
|
+
applied,
|
|
5884
|
+
...failure !== void 0 ? { failure } : {},
|
|
5885
|
+
...partial ? { partial } : {}
|
|
5886
|
+
};
|
|
5887
|
+
if (options.json === true) {
|
|
5888
|
+
console.log(JSON.stringify(result));
|
|
5889
|
+
} else {
|
|
5890
|
+
console.log(renderProjectRetrofit(result));
|
|
5891
|
+
}
|
|
5892
|
+
return result;
|
|
5893
|
+
}
|
|
5894
|
+
function appendSpokeChecklist(lines, spokes) {
|
|
5895
|
+
if (spokes.length === 0) return;
|
|
5896
|
+
lines.push(
|
|
5897
|
+
`## Spoke files to reconcile (${spokes.length}) \u2014 regular files that would block clean wiring`
|
|
5898
|
+
);
|
|
5899
|
+
for (const s of spokes) {
|
|
5900
|
+
lines.push(
|
|
5901
|
+
`- ${s}: a regular file. If it duplicates AGENTS.md, remove it; if it carries unique content, merge it into AGENTS.md. Then run \`basou project symlinks\`.`
|
|
5902
|
+
);
|
|
5903
|
+
}
|
|
5904
|
+
lines.push("");
|
|
5905
|
+
}
|
|
5906
|
+
function renderProjectRetrofit(result) {
|
|
5907
|
+
const lines = [];
|
|
5908
|
+
lines.push(
|
|
5909
|
+
"# Retrofit an existing AGENTS.md into the project (relocate to the anchor canonical)"
|
|
5910
|
+
);
|
|
5911
|
+
lines.push("");
|
|
5912
|
+
if (!result.hasRoster) {
|
|
5913
|
+
lines.push(
|
|
5914
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare the repo first with `basou project new` (or `basou project adopt`), then re-run."
|
|
5915
|
+
);
|
|
5916
|
+
return lines.join("\n");
|
|
5917
|
+
}
|
|
5918
|
+
const canonical2 = `agents/${result.canonicalName}/${CANONICAL_FILE}`;
|
|
5919
|
+
if (result.action === "refuse") {
|
|
5920
|
+
if (result.reason === "not-declared") {
|
|
5921
|
+
lines.push(
|
|
5922
|
+
`\u2139\uFE0F \`${result.path}\` is not declared in the roster (manifest \`repos\`). Add it first with \`basou project new\` / \`basou project adopt\`, then re-run.`
|
|
5923
|
+
);
|
|
5924
|
+
} else if (result.reason === "self") {
|
|
5925
|
+
lines.push(
|
|
5926
|
+
`\u2139\uFE0F \`${result.path}\` declares \`instructions: self\` \u2014 its \`${CANONICAL_FILE}\` is a hand-authored committed file that stays in the repo, so there is no anchor canonical to relocate it to. Retrofit does not apply (its CLAUDE.md / Copilot spokes are wired by \`basou project symlinks\`).`
|
|
5927
|
+
);
|
|
5928
|
+
} else if (result.reason === "anchor") {
|
|
5929
|
+
lines.push(
|
|
5930
|
+
`\u26A0\uFE0F \`${result.path}\` is the anchor (the project root). It owns its canonical directly \u2014 there is nothing to relocate.`
|
|
5931
|
+
);
|
|
5932
|
+
} else if (result.reason === "unreachable") {
|
|
5933
|
+
lines.push(
|
|
5934
|
+
`\u26A0\uFE0F \`${result.path}\` does not resolve to a git repository. There is nothing to retrofit.`
|
|
5935
|
+
);
|
|
5936
|
+
} else if (result.reason === "blocked") {
|
|
5937
|
+
lines.push(
|
|
5938
|
+
`\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.`
|
|
5939
|
+
);
|
|
5940
|
+
} else if (result.reason === "canonical-exists") {
|
|
5941
|
+
lines.push(
|
|
5942
|
+
`\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.`
|
|
5943
|
+
);
|
|
5944
|
+
}
|
|
5945
|
+
return lines.join("\n");
|
|
5946
|
+
}
|
|
5947
|
+
if (result.action === "skip") {
|
|
5948
|
+
if (result.reason === "already-symlink") {
|
|
5949
|
+
lines.push(
|
|
5950
|
+
`\u2705 \`${result.path}/${CANONICAL_FILE}\` is already a symlink (already wired). Nothing to retrofit.`
|
|
5951
|
+
);
|
|
5952
|
+
} else {
|
|
5953
|
+
lines.push(
|
|
5954
|
+
`\u2139\uFE0F \`${result.path}\` has no regular-file \`${CANONICAL_FILE}\` to relocate. If it should have a canonical, run \`basou project derive\` to generate one from the manifest.`
|
|
5955
|
+
);
|
|
5956
|
+
}
|
|
5957
|
+
lines.push("");
|
|
5958
|
+
appendSpokeChecklist(lines, result.regularSpokes);
|
|
5959
|
+
return lines.join("\n").trimEnd();
|
|
5960
|
+
}
|
|
5961
|
+
if (result.failure !== void 0) {
|
|
5962
|
+
lines.push(
|
|
5963
|
+
`Could not relocate \`${result.path}/${CANONICAL_FILE}\` to \`${canonical2}\`: ${result.failure}.`
|
|
5964
|
+
);
|
|
5965
|
+
if (result.partial === true) {
|
|
5966
|
+
lines.push(
|
|
5967
|
+
`The canonical \`${canonical2}\` was written, but \`${result.path}/${CANONICAL_FILE}\` may be absent. Run \`basou project symlinks --apply\` (or \`basou project derive --apply\`) to recreate the missing link, then verify.`
|
|
5968
|
+
);
|
|
5969
|
+
} else {
|
|
5970
|
+
lines.push("Nothing was changed. Resolve the cause and re-run.");
|
|
5971
|
+
}
|
|
5972
|
+
return lines.join("\n");
|
|
5973
|
+
}
|
|
5974
|
+
if (result.applied) {
|
|
5975
|
+
lines.push(
|
|
5976
|
+
`\u2705 Relocated \`${result.path}/${CANONICAL_FILE}\` to \`${canonical2}\` and left a symlink in its place.`
|
|
5977
|
+
);
|
|
5978
|
+
} else {
|
|
5979
|
+
lines.push(
|
|
5980
|
+
`To relocate \`${result.path}/${CANONICAL_FILE}\` and replace it with a symlink (dry-run; pass --apply to write):`
|
|
5981
|
+
);
|
|
5982
|
+
lines.push(` move ${result.path}/${CANONICAL_FILE} -> ${canonical2}`);
|
|
5983
|
+
lines.push(` symlink ${result.path}/${CANONICAL_FILE} -> the canonical`);
|
|
5984
|
+
}
|
|
5985
|
+
lines.push("");
|
|
5986
|
+
appendSpokeChecklist(lines, result.regularSpokes);
|
|
5987
|
+
lines.push(
|
|
5988
|
+
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)."
|
|
5989
|
+
);
|
|
5990
|
+
return lines.join("\n");
|
|
5991
|
+
}
|
|
5686
5992
|
|
|
5687
5993
|
// src/commands/protocol.ts
|
|
5688
5994
|
import { readFile as readFile4 } from "fs/promises";
|