@basou/cli 0.25.0 → 0.27.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 +1056 -162
- package/dist/index.js.map +1 -1
- package/dist/program.js +1056 -162
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -3364,9 +3364,14 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
3364
3364
|
|
|
3365
3365
|
// src/commands/project.ts
|
|
3366
3366
|
import {
|
|
3367
|
+
closeSync,
|
|
3368
|
+
copyFileSync,
|
|
3367
3369
|
existsSync,
|
|
3370
|
+
constants as fsConstants,
|
|
3371
|
+
ftruncateSync,
|
|
3368
3372
|
lstatSync,
|
|
3369
3373
|
mkdirSync,
|
|
3374
|
+
openSync,
|
|
3370
3375
|
readdirSync,
|
|
3371
3376
|
readFileSync,
|
|
3372
3377
|
readlinkSync,
|
|
@@ -3374,11 +3379,16 @@ import {
|
|
|
3374
3379
|
statSync,
|
|
3375
3380
|
symlinkSync,
|
|
3376
3381
|
unlinkSync,
|
|
3377
|
-
writeFileSync
|
|
3382
|
+
writeFileSync,
|
|
3383
|
+
writeSync
|
|
3378
3384
|
} from "fs";
|
|
3379
3385
|
import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve7 } from "path";
|
|
3380
3386
|
import {
|
|
3387
|
+
appendBasouGitignore as appendBasouGitignore2,
|
|
3381
3388
|
basouPaths as basouPaths10,
|
|
3389
|
+
classifyRetrofit,
|
|
3390
|
+
createManifest as createManifest2,
|
|
3391
|
+
ensureBasouDirectory as ensureBasouDirectory2,
|
|
3382
3392
|
GENERATED_END,
|
|
3383
3393
|
GENERATED_START,
|
|
3384
3394
|
isGitNotFound,
|
|
@@ -3392,7 +3402,9 @@ import {
|
|
|
3392
3402
|
readManifest as readManifest6,
|
|
3393
3403
|
readMarkdownFile as readMarkdownFile4,
|
|
3394
3404
|
reconcileSourceRoots,
|
|
3405
|
+
removeMarkerSection,
|
|
3395
3406
|
renderWithMarkers as renderWithMarkers4,
|
|
3407
|
+
resolveRepositoryRoot as resolveRepositoryRoot8,
|
|
3396
3408
|
safeSimpleGit,
|
|
3397
3409
|
summarizePresetPlan,
|
|
3398
3410
|
summarizeRosterDrift,
|
|
@@ -3474,6 +3486,38 @@ function registerProjectCommand(program) {
|
|
|
3474
3486
|
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (oldPath, newPath, opts) => {
|
|
3475
3487
|
await runProjectRename(oldPath, newPath, opts);
|
|
3476
3488
|
});
|
|
3489
|
+
project.command("teardown").argument("<repo>", "The repo path whose basou-generated wiring to tear down (e.g. ../takuhon)").description(
|
|
3490
|
+
"Remove the basou-generated wiring for one repo: its instruction symlinks (AGENTS.md / CLAUDE.md / copilot), its `.gitignore` patterns, its workspace view symlink, and the generated block in the anchor's canonical. Dry-run by default (a classified plan: removable / foreign / blocked); pass --apply to remove ONLY the verified-basou artifacts, re-checking each just before it acts \u2014 a real file, a foreign symlink, or hand-authored canonical prose is never touched. This is the destructive counterpart to `archive` (which only drops the manifest declaration): archive first, then teardown to clean the on-disk wiring. The anchor (`.`) is refused. Not reversible \u2014 the manifest is git-tracked but the removed symlinks/lines are not"
|
|
3491
|
+
).option("--apply", "Remove the verified-basou artifacts (default: dry-run classified preview)").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repo, opts) => {
|
|
3492
|
+
await runProjectTeardown(repo, opts);
|
|
3493
|
+
});
|
|
3494
|
+
project.command("new").argument("[repos...]", "Extra repo paths (besides the anchor) to seed into the roster").description(
|
|
3495
|
+
"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"
|
|
3496
|
+
).option("--apply", "Create `.basou/` and write the seeded manifest (default: dry-run preview)").option(
|
|
3497
|
+
"--view <path>",
|
|
3498
|
+
"Override the workspace view path (default: a <name>-workspace sibling)"
|
|
3499
|
+
).option("--no-view", "Solo project: declare no workspace view").option(
|
|
3500
|
+
"--local-only",
|
|
3501
|
+
"Write a .basou/ full-exclude .gitignore block (keep the trail out of version control) instead of the default ignore+commit block"
|
|
3502
|
+
).option("-f, --force", "Overwrite an existing manifest").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repos, opts) => {
|
|
3503
|
+
await runProjectNew(repos, opts);
|
|
3504
|
+
});
|
|
3505
|
+
project.command("derive").description(
|
|
3506
|
+
"Materialize a project's full wiring from the declared manifest: sync `source_roots` to the roster, generate each repo's canonical preset block, its instruction-file symlinks, the workspace view, and each public repo's .gitignore \u2014 in dependency order. Dry-run by default; pass --apply to write. The greenfield counterpart to `new` (run after declaring visibility/language) and a one-shot maintenance pass. Re-runnable: each step is idempotent, so a partial apply recovers on a second run"
|
|
3507
|
+
).option("--apply", "Run every step in apply mode (default: dry-run preview)").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
3508
|
+
await runProjectDerive(opts);
|
|
3509
|
+
});
|
|
3510
|
+
project.command("retrofit").argument(
|
|
3511
|
+
"<repo>",
|
|
3512
|
+
"The declared roster repo whose hand-authored AGENTS.md to relocate (e.g. ../foo)"
|
|
3513
|
+
).description(
|
|
3514
|
+
"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"
|
|
3515
|
+
).option(
|
|
3516
|
+
"--apply",
|
|
3517
|
+
"Relocate the AGENTS.md to the canonical and recreate the symlink (default: dry-run preview)"
|
|
3518
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repo, opts) => {
|
|
3519
|
+
await runProjectRetrofit(repo, opts);
|
|
3520
|
+
});
|
|
3477
3521
|
}
|
|
3478
3522
|
async function runProjectCheck(options, ctx = {}) {
|
|
3479
3523
|
try {
|
|
@@ -3489,7 +3533,7 @@ function effectiveSourceRoots(manifest) {
|
|
|
3489
3533
|
function preservedUnknownLines(fields) {
|
|
3490
3534
|
if (fields.length === 0) return [];
|
|
3491
3535
|
return [
|
|
3492
|
-
`\u2139\uFE0F
|
|
3536
|
+
`\u2139\uFE0F Preserving ${fields.length} unrecognized top-level manifest field${fields.length === 1 ? "" : "s"} (kept on write, never dropped): ${fields.join(", ")}`,
|
|
3493
3537
|
""
|
|
3494
3538
|
];
|
|
3495
3539
|
}
|
|
@@ -3511,39 +3555,41 @@ async function doRunProjectCheck(options, ctx) {
|
|
|
3511
3555
|
}
|
|
3512
3556
|
function renderProjectCheck(summary) {
|
|
3513
3557
|
const lines = [];
|
|
3514
|
-
lines.push("#
|
|
3558
|
+
lines.push("# Project composition check (declared vs captured)");
|
|
3515
3559
|
lines.push("");
|
|
3516
3560
|
if (summary.declaredCount === 0) {
|
|
3517
3561
|
lines.push(
|
|
3518
|
-
"\u2139\uFE0F repo
|
|
3562
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Running on `source_roots` alone, so there is nothing to compare the declaration against."
|
|
3519
3563
|
);
|
|
3520
3564
|
if (summary.extra.length > 0) {
|
|
3521
3565
|
lines.push("");
|
|
3522
|
-
lines.push(
|
|
3566
|
+
lines.push(`Captured source_roots (${summary.extra.length}):`);
|
|
3523
3567
|
for (const p of summary.extra) lines.push(`- ${p}`);
|
|
3524
3568
|
}
|
|
3525
3569
|
return lines.join("\n");
|
|
3526
3570
|
}
|
|
3527
3571
|
if (summary.gaps.length === 0) {
|
|
3528
3572
|
lines.push(
|
|
3529
|
-
`\u2705
|
|
3573
|
+
`\u2705 All ${summary.declaredCount} declared repo${summary.declaredCount === 1 ? " is" : "s are"} covered by the capture config (source_roots).`
|
|
3530
3574
|
);
|
|
3531
3575
|
} else {
|
|
3532
|
-
lines.push(
|
|
3576
|
+
lines.push(
|
|
3577
|
+
`\u26A0\uFE0F Declared but not captured: ${summary.gaps.length} repo${summary.gaps.length === 1 ? "" : "s"}`
|
|
3578
|
+
);
|
|
3533
3579
|
for (const g of summary.gaps) {
|
|
3534
|
-
lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 source_roots
|
|
3580
|
+
lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 not in source_roots`);
|
|
3535
3581
|
}
|
|
3536
3582
|
}
|
|
3537
3583
|
lines.push("");
|
|
3538
3584
|
if (summary.extra.length > 0) {
|
|
3539
3585
|
lines.push(
|
|
3540
|
-
`##
|
|
3586
|
+
`## Captured but undeclared (${summary.extra.length}) \u2014 the workspace view, or a missing declaration`
|
|
3541
3587
|
);
|
|
3542
3588
|
for (const p of summary.extra) lines.push(`- ${p}`);
|
|
3543
3589
|
lines.push("");
|
|
3544
3590
|
}
|
|
3545
3591
|
lines.push(
|
|
3546
|
-
"
|
|
3592
|
+
"Note: read-only advisory. It only shows the difference between the declaration (repos) and the capture config (source_roots); it does not enforce."
|
|
3547
3593
|
);
|
|
3548
3594
|
return lines.join("\n");
|
|
3549
3595
|
}
|
|
@@ -3593,29 +3639,33 @@ async function doRunProjectSync(options, ctx) {
|
|
|
3593
3639
|
}
|
|
3594
3640
|
function renderProjectSync(result) {
|
|
3595
3641
|
const lines = [];
|
|
3596
|
-
lines.push("# source_roots
|
|
3642
|
+
lines.push("# source_roots sync (declared roster \u2192 capture config)");
|
|
3597
3643
|
lines.push("");
|
|
3598
3644
|
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
3599
3645
|
if (!result.hasRoster) {
|
|
3600
3646
|
lines.push(
|
|
3601
|
-
"\u2139\uFE0F repo
|
|
3647
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). There is no declaration to sync from, so nothing changes."
|
|
3602
3648
|
);
|
|
3603
3649
|
return lines.join("\n");
|
|
3604
3650
|
}
|
|
3605
3651
|
if (result.unchanged) {
|
|
3606
|
-
lines.push("\u2705 source_roots
|
|
3652
|
+
lines.push("\u2705 source_roots already covers the entire declared roster (nothing to sync).");
|
|
3607
3653
|
return lines.join("\n");
|
|
3608
3654
|
}
|
|
3609
3655
|
if (result.applied) {
|
|
3610
|
-
lines.push(
|
|
3656
|
+
lines.push(
|
|
3657
|
+
`\u2705 Added ${result.added.length} entr${result.added.length === 1 ? "y" : "ies"} to source_roots:`
|
|
3658
|
+
);
|
|
3611
3659
|
for (const p of result.added) lines.push(`- ${p}`);
|
|
3612
3660
|
} else {
|
|
3613
3661
|
lines.push(
|
|
3614
|
-
`${result.added.length}
|
|
3662
|
+
`${result.added.length} repo${result.added.length === 1 ? " is" : "s are"} not in source_roots. To add (dry-run; pass --apply to write):`
|
|
3615
3663
|
);
|
|
3616
3664
|
for (const p of result.added) lines.push(`- ${p}`);
|
|
3617
3665
|
lines.push("");
|
|
3618
|
-
lines.push(
|
|
3666
|
+
lines.push(
|
|
3667
|
+
"Note: existing source_roots are kept; only the missing entries are appended (nothing is removed)."
|
|
3668
|
+
);
|
|
3619
3669
|
}
|
|
3620
3670
|
return lines.join("\n");
|
|
3621
3671
|
}
|
|
@@ -3675,37 +3725,41 @@ async function doRunProjectAdopt(options, ctx) {
|
|
|
3675
3725
|
}
|
|
3676
3726
|
function renderProjectAdopt(result) {
|
|
3677
3727
|
const lines = [];
|
|
3678
|
-
lines.push("# repo
|
|
3728
|
+
lines.push("# Bootstrap repo roster (source_roots \u2192 repos)");
|
|
3679
3729
|
lines.push("");
|
|
3680
3730
|
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
3681
3731
|
if (result.alreadyDeclared) {
|
|
3682
3732
|
lines.push(
|
|
3683
|
-
"\u2139\uFE0F repo
|
|
3733
|
+
"\u2139\uFE0F A repo roster (manifest `repos`) is already declared. adopt is a one-time bootstrap, so it writes nothing. Use `project check` / `project sync` for ongoing maintenance."
|
|
3684
3734
|
);
|
|
3685
3735
|
return lines.join("\n");
|
|
3686
3736
|
}
|
|
3687
3737
|
if (result.repos.length === 0) {
|
|
3688
|
-
lines.push("\u2139\uFE0F
|
|
3738
|
+
lines.push("\u2139\uFE0F No git repo found in source_roots (nothing to bootstrap).");
|
|
3689
3739
|
} else if (result.applied) {
|
|
3690
|
-
lines.push(
|
|
3740
|
+
lines.push(
|
|
3741
|
+
`\u2705 Wrote ${result.repos.length} repo${result.repos.length === 1 ? "" : "s"} to the repos roster:`
|
|
3742
|
+
);
|
|
3691
3743
|
for (const r of result.repos) lines.push(`- ${r.path}`);
|
|
3692
3744
|
lines.push("");
|
|
3693
3745
|
lines.push(
|
|
3694
|
-
"
|
|
3746
|
+
"Note: visibility is unset. Assign public / private / future-public to each repo manually."
|
|
3695
3747
|
);
|
|
3696
3748
|
} else {
|
|
3697
3749
|
lines.push(
|
|
3698
|
-
`${result.repos.length} repo
|
|
3750
|
+
`${result.repos.length} repo${result.repos.length === 1 ? "" : "s"} to declare in the repos roster (dry-run; pass --apply to write):`
|
|
3699
3751
|
);
|
|
3700
3752
|
for (const r of result.repos) lines.push(`- ${r.path}`);
|
|
3701
3753
|
lines.push("");
|
|
3702
|
-
lines.push("
|
|
3754
|
+
lines.push("Note: visibility is proposed unset; assign it manually after applying.");
|
|
3703
3755
|
}
|
|
3704
3756
|
if (result.excluded.length > 0) {
|
|
3705
3757
|
lines.push("");
|
|
3706
|
-
lines.push(
|
|
3758
|
+
lines.push(
|
|
3759
|
+
`## Excluded (${result.excluded.length}) \u2014 not a git repo, so not included in repos`
|
|
3760
|
+
);
|
|
3707
3761
|
for (const e of result.excluded) {
|
|
3708
|
-
const reason = e.kind === "non-repo" ? "
|
|
3762
|
+
const reason = e.kind === "non-repo" ? "not a repo (workspace view / tmp, etc.)" : "unresolvable (path does not exist)";
|
|
3709
3763
|
lines.push(`- ${e.path} \u2014 ${reason}`);
|
|
3710
3764
|
}
|
|
3711
3765
|
}
|
|
@@ -3773,50 +3827,56 @@ async function doRunProjectWiring(options, ctx) {
|
|
|
3773
3827
|
}
|
|
3774
3828
|
function renderProjectWiring(result) {
|
|
3775
3829
|
const lines = [];
|
|
3776
|
-
lines.push(
|
|
3830
|
+
lines.push(
|
|
3831
|
+
"# Instruction-file wiring check (declared roster \xD7 instruction-file presence / git tracking)"
|
|
3832
|
+
);
|
|
3777
3833
|
lines.push("");
|
|
3778
3834
|
if (!result.hasRoster) {
|
|
3779
3835
|
lines.push(
|
|
3780
|
-
"\u2139\uFE0F repo
|
|
3836
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
|
|
3781
3837
|
);
|
|
3782
3838
|
return lines.join("\n");
|
|
3783
3839
|
}
|
|
3784
3840
|
if (result.risks.length > 0) {
|
|
3785
3841
|
lines.push(
|
|
3786
|
-
`\u26A0\uFE0F
|
|
3842
|
+
`\u26A0\uFE0F Instruction files tracked by git in public-facing repos: ${result.risks.length} (canonical leak risk)`
|
|
3787
3843
|
);
|
|
3788
3844
|
for (const r of result.risks) {
|
|
3789
3845
|
lines.push(
|
|
3790
|
-
`- ${r.repo} [${r.visibility}] \u2014 ${r.file}
|
|
3846
|
+
`- ${r.repo} [${r.visibility}] \u2014 ${r.file} is tracked (it should be a gitignored symlink)`
|
|
3791
3847
|
);
|
|
3792
3848
|
}
|
|
3793
3849
|
} else if (result.ok) {
|
|
3794
|
-
lines.push(
|
|
3850
|
+
lines.push(
|
|
3851
|
+
"\u2705 No instruction file is tracked by git in a public-facing repo (no privacy risk)."
|
|
3852
|
+
);
|
|
3795
3853
|
} else {
|
|
3796
3854
|
lines.push(
|
|
3797
|
-
"\u2139\uFE0F
|
|
3855
|
+
"\u2139\uFE0F No confirmed privacy risk, but some repos are unjudgeable / unreachable (see below)."
|
|
3798
3856
|
);
|
|
3799
3857
|
}
|
|
3800
3858
|
lines.push("");
|
|
3801
3859
|
if (result.unknown.length > 0) {
|
|
3802
3860
|
lines.push(
|
|
3803
|
-
`##
|
|
3861
|
+
`## Visibility unset (${result.unknown.length}) \u2014 privacy cannot be judged. Assign visibility in the manifest repos`
|
|
3804
3862
|
);
|
|
3805
3863
|
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
3806
3864
|
lines.push("");
|
|
3807
3865
|
}
|
|
3808
3866
|
if (result.incomplete.length > 0) {
|
|
3809
|
-
lines.push(
|
|
3867
|
+
lines.push(
|
|
3868
|
+
`## Missing instruction files (${result.incomplete.length}) \u2014 to be filled by a later generation slice`
|
|
3869
|
+
);
|
|
3810
3870
|
for (const i of result.incomplete) lines.push(`- ${i.repo} \u2014 ${i.missing.join(", ")}`);
|
|
3811
3871
|
lines.push("");
|
|
3812
3872
|
}
|
|
3813
3873
|
if (result.unreachable.length > 0) {
|
|
3814
|
-
lines.push(`##
|
|
3874
|
+
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
3815
3875
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
3816
3876
|
lines.push("");
|
|
3817
3877
|
}
|
|
3818
3878
|
lines.push(
|
|
3819
|
-
"
|
|
3879
|
+
"Note: read-only advisory. It only shows instruction-file presence and git-tracking status; it neither generates nor enforces (for the .basou footprint, use `basou view --check`)."
|
|
3820
3880
|
);
|
|
3821
3881
|
return lines.join("\n");
|
|
3822
3882
|
}
|
|
@@ -3895,45 +3955,47 @@ async function doRunProjectGitignore(options, ctx) {
|
|
|
3895
3955
|
}
|
|
3896
3956
|
function renderProjectGitignore(result) {
|
|
3897
3957
|
const lines = [];
|
|
3898
|
-
lines.push("# .gitignore
|
|
3958
|
+
lines.push("# .gitignore generation (exclude instruction files in public-facing repos)");
|
|
3899
3959
|
lines.push("");
|
|
3900
3960
|
if (!result.hasRoster) {
|
|
3901
3961
|
lines.push(
|
|
3902
|
-
"\u2139\uFE0F repo
|
|
3962
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
|
|
3903
3963
|
);
|
|
3904
3964
|
return lines.join("\n");
|
|
3905
3965
|
}
|
|
3906
3966
|
if (result.plans.length > 0) {
|
|
3907
|
-
const verb = result.applied ? "
|
|
3967
|
+
const verb = result.applied ? "Added to" : "To add to (dry-run; pass --apply to write)";
|
|
3908
3968
|
lines.push(
|
|
3909
|
-
`${result.applied ? "\u2705 " : ""}${result.plans.length} repo
|
|
3969
|
+
`${result.applied ? "\u2705 " : ""}${verb} the .gitignore of ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"}:`
|
|
3910
3970
|
);
|
|
3911
3971
|
for (const p of result.plans) lines.push(`- ${p.path} \u2014 ${p.toAdd.join(", ")}`);
|
|
3912
3972
|
} else if (result.ok) {
|
|
3913
|
-
lines.push(
|
|
3973
|
+
lines.push(
|
|
3974
|
+
"\u2705 Public-facing repos already exclude every instruction file in .gitignore (nothing to add)."
|
|
3975
|
+
);
|
|
3914
3976
|
} else {
|
|
3915
3977
|
lines.push(
|
|
3916
|
-
"\u2139\uFE0F
|
|
3978
|
+
"\u2139\uFE0F No public-facing repo needs an addition, but some repos are unjudgeable / unreachable (see below)."
|
|
3917
3979
|
);
|
|
3918
3980
|
}
|
|
3919
3981
|
lines.push("");
|
|
3920
3982
|
if (result.unknown.length > 0) {
|
|
3921
3983
|
lines.push(
|
|
3922
|
-
`##
|
|
3984
|
+
`## Visibility unset (${result.unknown.length}) \u2014 skipped. Assign visibility in the manifest repos`
|
|
3923
3985
|
);
|
|
3924
3986
|
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
3925
3987
|
lines.push("");
|
|
3926
3988
|
}
|
|
3927
3989
|
if (result.unreachable.length > 0) {
|
|
3928
|
-
lines.push(`##
|
|
3990
|
+
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
3929
3991
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
3930
3992
|
lines.push("");
|
|
3931
3993
|
}
|
|
3932
3994
|
lines.push(
|
|
3933
|
-
"
|
|
3995
|
+
"Note: existing .gitignore lines are kept; only the missing patterns are appended (nothing is removed). private / visibility-unset repos are skipped."
|
|
3934
3996
|
);
|
|
3935
3997
|
lines.push(
|
|
3936
|
-
"
|
|
3998
|
+
"Note: appending to .gitignore does not untrack files already tracked by git. Detect tracked instruction files with `basou project wiring` and remove them with `git rm --cached <file>`."
|
|
3937
3999
|
);
|
|
3938
4000
|
return lines.join("\n");
|
|
3939
4001
|
}
|
|
@@ -4061,11 +4123,11 @@ async function doRunProjectSymlinks(options, ctx) {
|
|
|
4061
4123
|
}
|
|
4062
4124
|
function renderProjectSymlinks(result) {
|
|
4063
4125
|
const lines = [];
|
|
4064
|
-
lines.push("#
|
|
4126
|
+
lines.push("# Instruction-file symlink generation (each repo \u2192 the anchor's canonical)");
|
|
4065
4127
|
lines.push("");
|
|
4066
4128
|
if (!result.hasRoster) {
|
|
4067
4129
|
lines.push(
|
|
4068
|
-
"\u2139\uFE0F repo
|
|
4130
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
|
|
4069
4131
|
);
|
|
4070
4132
|
return lines.join("\n");
|
|
4071
4133
|
}
|
|
@@ -4073,14 +4135,14 @@ function renderProjectSymlinks(result) {
|
|
|
4073
4135
|
const attempted = result.applied || result.failures.length > 0;
|
|
4074
4136
|
if (!attempted) {
|
|
4075
4137
|
lines.push(
|
|
4076
|
-
|
|
4138
|
+
`Instruction-file symlinks to create in ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"} (dry-run; pass --apply to write):`
|
|
4077
4139
|
);
|
|
4078
4140
|
for (const p of result.plans) {
|
|
4079
4141
|
lines.push(`- ${p.path}`);
|
|
4080
4142
|
for (const c of p.toCreate) lines.push(` ${c.name} -> ${c.target}`);
|
|
4081
4143
|
}
|
|
4082
4144
|
} else {
|
|
4083
|
-
const header = result.failures.length === 0 ? "\u2705
|
|
4145
|
+
const header = result.failures.length === 0 ? "\u2705 Created instruction-file symlinks:" : result.applied ? "Created instruction-file symlinks (some failed, see below):" : "Could not create instruction-file symlinks (see below):";
|
|
4084
4146
|
lines.push(header);
|
|
4085
4147
|
for (const p of result.plans) {
|
|
4086
4148
|
const failedFiles = new Set(
|
|
@@ -4093,31 +4155,35 @@ function renderProjectSymlinks(result) {
|
|
|
4093
4155
|
}
|
|
4094
4156
|
}
|
|
4095
4157
|
} else if (result.ok) {
|
|
4096
|
-
lines.push(
|
|
4158
|
+
lines.push(
|
|
4159
|
+
"\u2705 Every declared repo's instruction-file symlinks are correctly wired (nothing to generate)."
|
|
4160
|
+
);
|
|
4097
4161
|
} else {
|
|
4098
4162
|
lines.push(
|
|
4099
|
-
"\u2139\uFE0F
|
|
4163
|
+
"\u2139\uFE0F No symlink needs generating, but there are conflicts / collisions / a missing canonical / unreachable repos (see below)."
|
|
4100
4164
|
);
|
|
4101
4165
|
}
|
|
4102
4166
|
lines.push("");
|
|
4103
4167
|
if (result.failures.length > 0) {
|
|
4104
|
-
lines.push(
|
|
4168
|
+
lines.push(
|
|
4169
|
+
`## Creation failed (${result.failures.length}) \u2014 some symlinks could not be created`
|
|
4170
|
+
);
|
|
4105
4171
|
for (const f of result.failures) lines.push(`- ${f.repo} \u2014 ${f.file}: ${f.message}`);
|
|
4106
4172
|
lines.push("");
|
|
4107
4173
|
}
|
|
4108
4174
|
if (result.conflicts.length > 0) {
|
|
4109
4175
|
lines.push(
|
|
4110
|
-
`##
|
|
4176
|
+
`## Conflicts (${result.conflicts.length}) \u2014 existing entries are not overwritten. Check them manually`
|
|
4111
4177
|
);
|
|
4112
4178
|
for (const c of result.conflicts) {
|
|
4113
|
-
const detail = c.reason === "mismatch" ?
|
|
4179
|
+
const detail = c.reason === "mismatch" ? `a symlink pointing elsewhere (currently: ${c.actualTarget ?? "?"})` : c.reason === "occupied" ? "a real file/directory, not a symlink" : "an uninspectable path (a parent component is not a directory, etc.)";
|
|
4114
4180
|
lines.push(`- ${c.repo} \u2014 ${c.file}: ${detail}`);
|
|
4115
4181
|
}
|
|
4116
4182
|
lines.push("");
|
|
4117
4183
|
}
|
|
4118
4184
|
if (result.collisions.length > 0) {
|
|
4119
4185
|
lines.push(
|
|
4120
|
-
`##
|
|
4186
|
+
`## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-wired)`
|
|
4121
4187
|
);
|
|
4122
4188
|
for (const c of result.collisions) {
|
|
4123
4189
|
lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
|
|
@@ -4126,18 +4192,18 @@ function renderProjectSymlinks(result) {
|
|
|
4126
4192
|
}
|
|
4127
4193
|
if (result.missingCanonical.length > 0) {
|
|
4128
4194
|
lines.push(
|
|
4129
|
-
`##
|
|
4195
|
+
`## Canonical missing (${result.missingCanonical.length}) \u2014 the anchor has no agents/<repo>/AGENTS.md, so nothing can be generated`
|
|
4130
4196
|
);
|
|
4131
4197
|
for (const p of result.missingCanonical) lines.push(`- ${p}`);
|
|
4132
4198
|
lines.push("");
|
|
4133
4199
|
}
|
|
4134
4200
|
if (result.unreachable.length > 0) {
|
|
4135
|
-
lines.push(`##
|
|
4201
|
+
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
4136
4202
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
4137
4203
|
lines.push("");
|
|
4138
4204
|
}
|
|
4139
4205
|
lines.push(
|
|
4140
|
-
"
|
|
4206
|
+
"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)."
|
|
4141
4207
|
);
|
|
4142
4208
|
return lines.join("\n");
|
|
4143
4209
|
}
|
|
@@ -4239,7 +4305,7 @@ function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
|
4239
4305
|
names = readdirSync(viewDir);
|
|
4240
4306
|
} catch (error) {
|
|
4241
4307
|
if (hasErrorCode(error) && error.code === "ENOENT") return [];
|
|
4242
|
-
throw new Error("workspace view
|
|
4308
|
+
throw new Error("Cannot scan the workspace view (check the path / its type)", {
|
|
4243
4309
|
cause: error
|
|
4244
4310
|
});
|
|
4245
4311
|
}
|
|
@@ -4261,7 +4327,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
4261
4327
|
if (c === null || c.kind !== "repo") {
|
|
4262
4328
|
failed.push({
|
|
4263
4329
|
name,
|
|
4264
|
-
message: "
|
|
4330
|
+
message: "the target changed since the scan (no longer a basou-generated stray repo link; re-run)"
|
|
4265
4331
|
});
|
|
4266
4332
|
continue;
|
|
4267
4333
|
}
|
|
@@ -4350,11 +4416,11 @@ async function doRunProjectWorkspace(options, ctx) {
|
|
|
4350
4416
|
}
|
|
4351
4417
|
function renderProjectWorkspace(result) {
|
|
4352
4418
|
const lines = [];
|
|
4353
|
-
lines.push("# workspace view
|
|
4419
|
+
lines.push("# workspace view generation (aggregate the roster repos)");
|
|
4354
4420
|
lines.push("");
|
|
4355
4421
|
if (!result.hasView) {
|
|
4356
4422
|
lines.push(
|
|
4357
|
-
"\u2139\uFE0F view
|
|
4423
|
+
"\u2139\uFE0F No view declared (manifest `workspace.view`). Declare the aggregation directory, then re-run."
|
|
4358
4424
|
);
|
|
4359
4425
|
return lines.join("\n");
|
|
4360
4426
|
}
|
|
@@ -4362,12 +4428,12 @@ function renderProjectWorkspace(result) {
|
|
|
4362
4428
|
const attempted = result.applied || result.failures.length > 0;
|
|
4363
4429
|
if (!attempted) {
|
|
4364
4430
|
lines.push(
|
|
4365
|
-
|
|
4431
|
+
`Repo symlinks to create in the view: ${result.toCreate.length} (dry-run; pass --apply to write):`
|
|
4366
4432
|
);
|
|
4367
4433
|
for (const c of result.toCreate) lines.push(` ${c.name} -> ${c.target}`);
|
|
4368
4434
|
} else {
|
|
4369
4435
|
const failed = new Set(result.failures.map((f) => f.name));
|
|
4370
|
-
const header = result.failures.length === 0 ? "\u2705
|
|
4436
|
+
const header = result.failures.length === 0 ? "\u2705 Created repo symlinks in the view:" : result.applied ? "Created repo symlinks in the view (some failed, see below):" : "Could not create repo symlinks in the view (see below):";
|
|
4371
4437
|
lines.push(header);
|
|
4372
4438
|
for (const c of result.toCreate) {
|
|
4373
4439
|
if (failed.has(c.name)) continue;
|
|
@@ -4376,16 +4442,18 @@ function renderProjectWorkspace(result) {
|
|
|
4376
4442
|
}
|
|
4377
4443
|
} else if (result.ok) {
|
|
4378
4444
|
lines.push(
|
|
4379
|
-
`\u2705 view
|
|
4445
|
+
`\u2705 The view aggregates the entire declared roster (${result.correctCount} links; nothing to generate).`
|
|
4380
4446
|
);
|
|
4381
4447
|
} else {
|
|
4382
4448
|
lines.push(
|
|
4383
|
-
"\u2139\uFE0F
|
|
4449
|
+
"\u2139\uFE0F No symlink needs creating, but there are items needing attention (stray / conflict / collision / unreachable repo, see below)."
|
|
4384
4450
|
);
|
|
4385
4451
|
}
|
|
4386
4452
|
lines.push("");
|
|
4387
4453
|
if (result.failures.length > 0) {
|
|
4388
|
-
lines.push(
|
|
4454
|
+
lines.push(
|
|
4455
|
+
`## Creation failed (${result.failures.length}) \u2014 some symlinks could not be created`
|
|
4456
|
+
);
|
|
4389
4457
|
for (const f of result.failures) lines.push(`- ${f.name}: ${f.message}`);
|
|
4390
4458
|
lines.push("");
|
|
4391
4459
|
}
|
|
@@ -4393,17 +4461,17 @@ function renderProjectWorkspace(result) {
|
|
|
4393
4461
|
const attempted = result.pruned || result.pruneFailures.length > 0;
|
|
4394
4462
|
if (result.pruneWithheld) {
|
|
4395
4463
|
lines.push(
|
|
4396
|
-
`${result.toPrune.length}
|
|
4464
|
+
`${result.toPrune.length} stray repo symlink${result.toPrune.length === 1 ? "" : "s"} were due to be pruned, but pruning was withheld because some repos are unreachable (an unreachable repo's link cannot be told apart from a stray; resolve or archive the repos below, then re-run):`
|
|
4397
4465
|
);
|
|
4398
4466
|
for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
|
|
4399
4467
|
} else if (!attempted) {
|
|
4400
4468
|
lines.push(
|
|
4401
|
-
|
|
4469
|
+
`Stray repo symlinks to prune: ${result.toPrune.length} (dry-run; pass --prune to remove):`
|
|
4402
4470
|
);
|
|
4403
4471
|
for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
|
|
4404
4472
|
} else {
|
|
4405
4473
|
const failed = new Set(result.pruneFailures.map((f) => f.name));
|
|
4406
|
-
const header = result.pruneFailures.length === 0 ? "\u{1F9F9} stray repo
|
|
4474
|
+
const header = result.pruneFailures.length === 0 ? "\u{1F9F9} Pruned stray repo symlinks:" : result.pruned ? "Pruned stray repo symlinks (some failed, see below):" : "Could not prune stray repo symlinks (see below):";
|
|
4407
4475
|
lines.push(header);
|
|
4408
4476
|
for (const p of result.toPrune) {
|
|
4409
4477
|
if (failed.has(p.name)) continue;
|
|
@@ -4414,47 +4482,47 @@ function renderProjectWorkspace(result) {
|
|
|
4414
4482
|
}
|
|
4415
4483
|
if (result.pruneFailures.length > 0) {
|
|
4416
4484
|
lines.push(
|
|
4417
|
-
`##
|
|
4485
|
+
`## Pruning failed (${result.pruneFailures.length}) \u2014 some stray symlinks could not be pruned`
|
|
4418
4486
|
);
|
|
4419
4487
|
for (const f of result.pruneFailures) lines.push(`- ${f.name}: ${f.message}`);
|
|
4420
4488
|
lines.push("");
|
|
4421
4489
|
}
|
|
4422
4490
|
if (result.conflicts.length > 0) {
|
|
4423
4491
|
lines.push(
|
|
4424
|
-
`##
|
|
4492
|
+
`## Conflicts (${result.conflicts.length}) \u2014 existing entries are not overwritten. Check them manually`
|
|
4425
4493
|
);
|
|
4426
4494
|
for (const c of result.conflicts) {
|
|
4427
|
-
const detail = c.reason === "mismatch" ?
|
|
4495
|
+
const detail = c.reason === "mismatch" ? `a symlink pointing elsewhere (currently: ${c.actualTarget ?? "?"})` : c.reason === "occupied" ? "a real file/directory, not a symlink" : "an uninspectable path (a parent component is not a directory, etc.)";
|
|
4428
4496
|
lines.push(`- ${c.name}: ${detail}`);
|
|
4429
4497
|
}
|
|
4430
4498
|
lines.push("");
|
|
4431
4499
|
}
|
|
4432
4500
|
if (result.collisions.length > 0) {
|
|
4433
4501
|
lines.push(
|
|
4434
|
-
`##
|
|
4502
|
+
`## Basename collisions (${result.collisions.length}) \u2014 another repo claims the same view name (not auto-wired)`
|
|
4435
4503
|
);
|
|
4436
4504
|
for (const c of result.collisions) lines.push(`- ${c.linkName} \u2190 ${c.repos.join(", ")}`);
|
|
4437
4505
|
lines.push("");
|
|
4438
4506
|
}
|
|
4439
4507
|
if (result.unreachable.length > 0) {
|
|
4440
4508
|
lines.push(
|
|
4441
|
-
`##
|
|
4509
|
+
`## Unreachable (${result.unreachable.length}) \u2014 path unresolved, or it resolves to the view itself, so it cannot be aggregated`
|
|
4442
4510
|
);
|
|
4443
4511
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
4444
4512
|
lines.push("");
|
|
4445
4513
|
}
|
|
4446
4514
|
if (result.strayUnknown.length > 0) {
|
|
4447
4515
|
lines.push(
|
|
4448
|
-
`##
|
|
4516
|
+
`## Strays left in place (${result.strayUnknown.length}) \u2014 not confirmed to be a basou-generated repo link, so not pruned. Check them manually`
|
|
4449
4517
|
);
|
|
4450
4518
|
for (const s of result.strayUnknown) {
|
|
4451
|
-
const detail = s.reason === "broken" ? "
|
|
4519
|
+
const detail = s.reason === "broken" ? "broken link (target does not resolve)" : s.reason === "non-repo" ? "non-git-repo target (a file, or a directory without .git)" : "absolute-path target (basou generates relative links only)";
|
|
4452
4520
|
lines.push(`- ${s.name} -> ${s.target}: ${detail}`);
|
|
4453
4521
|
}
|
|
4454
4522
|
lines.push("");
|
|
4455
4523
|
}
|
|
4456
4524
|
lines.push(
|
|
4457
|
-
"
|
|
4525
|
+
"Note: creation (--apply) never overwrites an existing entry. Stray repo links are pruned with --prune (only the symlink is removed, never the referenced repo). A stray not confirmed to be basou-generated (broken / non-repo / absolute path) is left in place."
|
|
4458
4526
|
);
|
|
4459
4527
|
return lines.join("\n");
|
|
4460
4528
|
}
|
|
@@ -4585,15 +4653,17 @@ async function doRunProjectPreset(options, ctx) {
|
|
|
4585
4653
|
return result;
|
|
4586
4654
|
}
|
|
4587
4655
|
function presetActionLabel(action) {
|
|
4588
|
-
return action === "create" ? "
|
|
4656
|
+
return action === "create" ? "create" : "update";
|
|
4589
4657
|
}
|
|
4590
4658
|
function renderProjectPreset(result) {
|
|
4591
4659
|
const lines = [];
|
|
4592
|
-
lines.push(
|
|
4660
|
+
lines.push(
|
|
4661
|
+
"# Instruction-file preset generation (declaration \u2192 the canonical's generated region)"
|
|
4662
|
+
);
|
|
4593
4663
|
lines.push("");
|
|
4594
4664
|
if (!result.hasRoster) {
|
|
4595
4665
|
lines.push(
|
|
4596
|
-
"\u2139\uFE0F repo
|
|
4666
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
|
|
4597
4667
|
);
|
|
4598
4668
|
return lines.join("\n");
|
|
4599
4669
|
}
|
|
@@ -4601,7 +4671,7 @@ function renderProjectPreset(result) {
|
|
|
4601
4671
|
const attempted = result.applied || result.failures.length > 0;
|
|
4602
4672
|
if (!attempted) {
|
|
4603
4673
|
lines.push(
|
|
4604
|
-
|
|
4674
|
+
`Preset blocks to generate in the canonical of ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"} (dry-run; pass --apply to write):`
|
|
4605
4675
|
);
|
|
4606
4676
|
for (const p of result.plans) {
|
|
4607
4677
|
lines.push(
|
|
@@ -4611,7 +4681,7 @@ function renderProjectPreset(result) {
|
|
|
4611
4681
|
}
|
|
4612
4682
|
} else {
|
|
4613
4683
|
const failed = new Set(result.failures.map((f) => f.repo));
|
|
4614
|
-
const header = result.failures.length === 0 ? "\u2705
|
|
4684
|
+
const header = result.failures.length === 0 ? "\u2705 Generated preset blocks in the canonical:" : result.applied ? "Generated preset blocks (some failed, see below):" : "Could not generate preset blocks (see below):";
|
|
4615
4685
|
lines.push(header);
|
|
4616
4686
|
for (const p of result.plans) {
|
|
4617
4687
|
if (failed.has(p.path)) continue;
|
|
@@ -4621,47 +4691,49 @@ function renderProjectPreset(result) {
|
|
|
4621
4691
|
}
|
|
4622
4692
|
}
|
|
4623
4693
|
} else if (result.ok) {
|
|
4624
|
-
lines.push(
|
|
4694
|
+
lines.push(
|
|
4695
|
+
"\u2705 Every declared repo's preset block is in sync with its canonical (nothing to generate)."
|
|
4696
|
+
);
|
|
4625
4697
|
} else {
|
|
4626
4698
|
lines.push(
|
|
4627
|
-
"\u2139\uFE0F
|
|
4699
|
+
"\u2139\uFE0F No repo needs generating, but there are marker conflicts / collisions / undeclared / unreachable repos (see below)."
|
|
4628
4700
|
);
|
|
4629
4701
|
}
|
|
4630
4702
|
lines.push("");
|
|
4631
4703
|
if (result.inSync.length > 0) {
|
|
4632
|
-
lines.push(
|
|
4704
|
+
lines.push(`In sync (${result.inSync.length}): ${result.inSync.join(", ")}`);
|
|
4633
4705
|
lines.push("");
|
|
4634
4706
|
}
|
|
4635
4707
|
if (result.failures.length > 0) {
|
|
4636
4708
|
lines.push(
|
|
4637
|
-
`##
|
|
4709
|
+
`## Write failed (${result.failures.length}) \u2014 some canonicals could not be written`
|
|
4638
4710
|
);
|
|
4639
4711
|
for (const f of result.failures) lines.push(`- ${f.repo}: ${f.message}`);
|
|
4640
4712
|
lines.push("");
|
|
4641
4713
|
}
|
|
4642
4714
|
if (result.markerConflicts.length > 0) {
|
|
4643
4715
|
lines.push(
|
|
4644
|
-
`##
|
|
4716
|
+
`## Marker conflicts (${result.markerConflicts.length}) \u2014 the canonical's markers are missing/malformed, so it is not overwritten`
|
|
4645
4717
|
);
|
|
4646
4718
|
for (const c of result.markerConflicts) {
|
|
4647
|
-
const detail = c.reason === "no_markers" ? "
|
|
4719
|
+
const detail = c.reason === "no_markers" ? "no marker region" : `malformed markers (${c.reason})`;
|
|
4648
4720
|
lines.push(`- ${c.repo}: ${detail}`);
|
|
4649
4721
|
}
|
|
4650
4722
|
lines.push(
|
|
4651
|
-
`
|
|
4723
|
+
` Fix: add these two lines where you want the preset block \u2014 \`${GENERATED_START}\` and \`${GENERATED_END}\` (absent, basou creates a fresh canonical).`
|
|
4652
4724
|
);
|
|
4653
4725
|
lines.push("");
|
|
4654
4726
|
}
|
|
4655
4727
|
if (result.unreadable.length > 0) {
|
|
4656
4728
|
lines.push(
|
|
4657
|
-
`##
|
|
4729
|
+
`## Canonical unreadable (${result.unreadable.length}) \u2014 could not be read (a directory, permissions, etc.)`
|
|
4658
4730
|
);
|
|
4659
4731
|
for (const p of result.unreadable) lines.push(`- ${p}`);
|
|
4660
4732
|
lines.push("");
|
|
4661
4733
|
}
|
|
4662
4734
|
if (result.collisions.length > 0) {
|
|
4663
4735
|
lines.push(
|
|
4664
|
-
`##
|
|
4736
|
+
`## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-generated)`
|
|
4665
4737
|
);
|
|
4666
4738
|
for (const c of result.collisions) {
|
|
4667
4739
|
lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
|
|
@@ -4670,25 +4742,25 @@ function renderProjectPreset(result) {
|
|
|
4670
4742
|
}
|
|
4671
4743
|
if (result.undeclared.length > 0) {
|
|
4672
4744
|
lines.push(
|
|
4673
|
-
`##
|
|
4745
|
+
`## Undeclared (${result.undeclared.length}) \u2014 visibility / language / publishes unset, so nothing is generated`
|
|
4674
4746
|
);
|
|
4675
4747
|
for (const p of result.undeclared) lines.push(`- ${p}`);
|
|
4676
4748
|
lines.push("");
|
|
4677
4749
|
}
|
|
4678
4750
|
if (result.anchors.length > 0) {
|
|
4679
4751
|
lines.push(
|
|
4680
|
-
`##
|
|
4752
|
+
`## Anchor (${result.anchors.length}) \u2014 its own AGENTS.md is hand-maintained, so it is skipped`
|
|
4681
4753
|
);
|
|
4682
4754
|
for (const p of result.anchors) lines.push(`- ${p}`);
|
|
4683
4755
|
lines.push("");
|
|
4684
4756
|
}
|
|
4685
4757
|
if (result.unreachable.length > 0) {
|
|
4686
|
-
lines.push(`##
|
|
4758
|
+
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
4687
4759
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
4688
4760
|
lines.push("");
|
|
4689
4761
|
}
|
|
4690
4762
|
lines.push(
|
|
4691
|
-
"
|
|
4763
|
+
"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."
|
|
4692
4764
|
);
|
|
4693
4765
|
return lines.join("\n");
|
|
4694
4766
|
}
|
|
@@ -4749,6 +4821,403 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
4749
4821
|
canonical: canonical2
|
|
4750
4822
|
};
|
|
4751
4823
|
}
|
|
4824
|
+
function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
|
|
4825
|
+
const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
4826
|
+
return expectedSymlinkTargets(repoReal, canonicalFile);
|
|
4827
|
+
}
|
|
4828
|
+
function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
4829
|
+
const filePath = join7(viewDir, name);
|
|
4830
|
+
try {
|
|
4831
|
+
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
4832
|
+
const target = readlinkSync(filePath);
|
|
4833
|
+
if (isAbsolute3(target)) return false;
|
|
4834
|
+
return realpathSync(resolve7(viewDir, target)) === repoReal;
|
|
4835
|
+
} catch {
|
|
4836
|
+
return false;
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
|
|
4840
|
+
const filePath = join7(viewDir, name);
|
|
4841
|
+
try {
|
|
4842
|
+
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
4843
|
+
const target = readlinkSync(filePath);
|
|
4844
|
+
if (isAbsolute3(target)) return false;
|
|
4845
|
+
return resolve7(viewDir, target) === expectedRepoPath;
|
|
4846
|
+
} catch {
|
|
4847
|
+
return false;
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
4851
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
4852
|
+
let repoReal;
|
|
4853
|
+
try {
|
|
4854
|
+
repoReal = realpathSync(resolve7(repositoryRoot, target));
|
|
4855
|
+
} catch {
|
|
4856
|
+
repoReal = void 0;
|
|
4857
|
+
}
|
|
4858
|
+
const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
|
|
4859
|
+
const targetAbs = resolve7(repositoryRoot, target);
|
|
4860
|
+
const canonicalName = basename4(repoReal ?? targetAbs);
|
|
4861
|
+
const roster = manifest.repos ?? [];
|
|
4862
|
+
const inRoster = roster.some((r) => {
|
|
4863
|
+
try {
|
|
4864
|
+
return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
|
|
4865
|
+
} catch {
|
|
4866
|
+
return resolve7(repositoryRoot, r.path) === targetAbs;
|
|
4867
|
+
}
|
|
4868
|
+
});
|
|
4869
|
+
const cnFold = canonicalName.toLowerCase();
|
|
4870
|
+
const canonicalShared = roster.some((r) => {
|
|
4871
|
+
let rReal = null;
|
|
4872
|
+
try {
|
|
4873
|
+
rReal = realpathSync(resolve7(repositoryRoot, r.path));
|
|
4874
|
+
} catch {
|
|
4875
|
+
rReal = null;
|
|
4876
|
+
}
|
|
4877
|
+
if (rReal !== null) {
|
|
4878
|
+
if (repoReal !== void 0 && rReal === repoReal) return false;
|
|
4879
|
+
return basename4(rReal).toLowerCase() === cnFold;
|
|
4880
|
+
}
|
|
4881
|
+
if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
|
|
4882
|
+
return basename4(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
|
|
4883
|
+
});
|
|
4884
|
+
const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
|
|
4885
|
+
const items = [];
|
|
4886
|
+
if (!isAnchor) {
|
|
4887
|
+
if (repoReal !== void 0) {
|
|
4888
|
+
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
4889
|
+
const { state, actualTarget } = inspectSymlink(join7(repoReal, spec.name), spec.target);
|
|
4890
|
+
if (state === "correct")
|
|
4891
|
+
items.push({ kind: "instruction-symlink", label: spec.name, state: "removable" });
|
|
4892
|
+
else if (state === "mismatch")
|
|
4893
|
+
items.push({
|
|
4894
|
+
kind: "instruction-symlink",
|
|
4895
|
+
label: spec.name,
|
|
4896
|
+
state: "foreign",
|
|
4897
|
+
note: `points at a different target (${actualTarget ?? "?"})`
|
|
4898
|
+
});
|
|
4899
|
+
else if (state === "occupied")
|
|
4900
|
+
items.push({
|
|
4901
|
+
kind: "instruction-symlink",
|
|
4902
|
+
label: spec.name,
|
|
4903
|
+
state: "foreign",
|
|
4904
|
+
note: "a real file, not a symlink"
|
|
4905
|
+
});
|
|
4906
|
+
else if (state === "blocked")
|
|
4907
|
+
items.push({
|
|
4908
|
+
kind: "instruction-symlink",
|
|
4909
|
+
label: spec.name,
|
|
4910
|
+
state: "blocked",
|
|
4911
|
+
note: "could not be inspected"
|
|
4912
|
+
});
|
|
4913
|
+
}
|
|
4914
|
+
let ignored;
|
|
4915
|
+
try {
|
|
4916
|
+
ignored = new Set(readGitignoreLines(join7(repoReal, ".gitignore")).map((l) => l.trim()));
|
|
4917
|
+
for (const p of INSTRUCTION_FILES) {
|
|
4918
|
+
if (ignored.has(p) || ignored.has(`/${p}`)) {
|
|
4919
|
+
items.push({
|
|
4920
|
+
kind: "gitignore",
|
|
4921
|
+
label: p,
|
|
4922
|
+
state: "manual",
|
|
4923
|
+
note: "cannot tell a basou-appended line from a hand-added one (no marker) \u2014 remove manually"
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
} catch {
|
|
4928
|
+
items.push({
|
|
4929
|
+
kind: "gitignore",
|
|
4930
|
+
label: ".gitignore",
|
|
4931
|
+
state: "blocked",
|
|
4932
|
+
note: "could not be read"
|
|
4933
|
+
});
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
const viewPath = manifest.workspace.view;
|
|
4937
|
+
if (viewPath !== void 0) {
|
|
4938
|
+
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
4939
|
+
const linkPath = join7(viewDir, canonicalName);
|
|
4940
|
+
let isLink = false;
|
|
4941
|
+
try {
|
|
4942
|
+
isLink = lstatSync(linkPath).isSymbolicLink();
|
|
4943
|
+
} catch {
|
|
4944
|
+
isLink = false;
|
|
4945
|
+
}
|
|
4946
|
+
if (isLink) {
|
|
4947
|
+
const owned = repoReal !== void 0 ? viewLinkPointsAt(viewDir, canonicalName, repoReal) : viewLinkPointsAtPath(viewDir, canonicalName, targetAbs);
|
|
4948
|
+
if (!owned)
|
|
4949
|
+
items.push({
|
|
4950
|
+
kind: "view-symlink",
|
|
4951
|
+
label: canonicalName,
|
|
4952
|
+
state: "foreign",
|
|
4953
|
+
note: "a view link that does not point at this repo"
|
|
4954
|
+
});
|
|
4955
|
+
else if (canonicalShared)
|
|
4956
|
+
items.push({
|
|
4957
|
+
kind: "view-symlink",
|
|
4958
|
+
label: canonicalName,
|
|
4959
|
+
state: "blocked",
|
|
4960
|
+
note: collisionNote
|
|
4961
|
+
});
|
|
4962
|
+
else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
4966
|
+
const canonicalLabel = join7("agents", canonicalName, CANONICAL_FILE);
|
|
4967
|
+
let canonicalIsLink = false;
|
|
4968
|
+
try {
|
|
4969
|
+
canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
|
|
4970
|
+
} catch {
|
|
4971
|
+
canonicalIsLink = false;
|
|
4972
|
+
}
|
|
4973
|
+
if (canonicalIsLink) {
|
|
4974
|
+
items.push({
|
|
4975
|
+
kind: "canonical-block",
|
|
4976
|
+
label: canonicalLabel,
|
|
4977
|
+
state: "foreign",
|
|
4978
|
+
note: "the canonical is a symlink (not generated)"
|
|
4979
|
+
});
|
|
4980
|
+
} else if (existsSync(canonicalFile)) {
|
|
4981
|
+
let content;
|
|
4982
|
+
try {
|
|
4983
|
+
content = readFileSync(canonicalFile, "utf8");
|
|
4984
|
+
} catch {
|
|
4985
|
+
items.push({
|
|
4986
|
+
kind: "canonical-block",
|
|
4987
|
+
label: canonicalLabel,
|
|
4988
|
+
state: "blocked",
|
|
4989
|
+
note: "could not be read"
|
|
4990
|
+
});
|
|
4991
|
+
}
|
|
4992
|
+
if (content !== void 0 && content !== "") {
|
|
4993
|
+
const section = parseMarkers(content);
|
|
4994
|
+
if (section.kind === "ok" && canonicalShared) {
|
|
4995
|
+
items.push({
|
|
4996
|
+
kind: "canonical-block",
|
|
4997
|
+
label: canonicalLabel,
|
|
4998
|
+
state: "blocked",
|
|
4999
|
+
note: collisionNote
|
|
5000
|
+
});
|
|
5001
|
+
} else if (section.kind === "ok" && repoReal === void 0) {
|
|
5002
|
+
items.push({
|
|
5003
|
+
kind: "canonical-block",
|
|
5004
|
+
label: canonicalLabel,
|
|
5005
|
+
state: "manual",
|
|
5006
|
+
note: "repo could not be resolved, so ownership cannot be verified (check manually)"
|
|
5007
|
+
});
|
|
5008
|
+
} else if (section.kind === "ok") {
|
|
5009
|
+
const emptyAfter = removeMarkerSection(content, canonicalLabel).trim().length === 0;
|
|
5010
|
+
items.push({
|
|
5011
|
+
kind: "canonical-block",
|
|
5012
|
+
label: canonicalLabel,
|
|
5013
|
+
state: "removable",
|
|
5014
|
+
...emptyAfter ? {
|
|
5015
|
+
note: "the file becomes empty after the generated block is removed (a manual-delete candidate)"
|
|
5016
|
+
} : {}
|
|
5017
|
+
});
|
|
5018
|
+
} else if (section.kind === "no_markers") {
|
|
5019
|
+
items.push({
|
|
5020
|
+
kind: "canonical-block",
|
|
5021
|
+
label: canonicalLabel,
|
|
5022
|
+
state: "foreign",
|
|
5023
|
+
note: "no generated block (hand-authored only \u2014 left untouched)"
|
|
5024
|
+
});
|
|
5025
|
+
} else {
|
|
5026
|
+
items.push({
|
|
5027
|
+
kind: "canonical-block",
|
|
5028
|
+
label: canonicalLabel,
|
|
5029
|
+
state: "blocked",
|
|
5030
|
+
note: "malformed markers (fix manually)"
|
|
5031
|
+
});
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
return {
|
|
5037
|
+
target,
|
|
5038
|
+
resolved: repoReal !== void 0,
|
|
5039
|
+
repoReal: repoReal ?? null,
|
|
5040
|
+
canonicalName,
|
|
5041
|
+
isAnchor,
|
|
5042
|
+
inRoster,
|
|
5043
|
+
items,
|
|
5044
|
+
removableCount: items.filter((i) => i.state === "removable").length
|
|
5045
|
+
};
|
|
5046
|
+
}
|
|
5047
|
+
function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
5048
|
+
const removed = [];
|
|
5049
|
+
const failed = [];
|
|
5050
|
+
const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
|
|
5051
|
+
let currentRepoReal = null;
|
|
5052
|
+
try {
|
|
5053
|
+
currentRepoReal = realpathSync(resolve7(repositoryRoot, plan.target));
|
|
5054
|
+
} catch {
|
|
5055
|
+
currentRepoReal = null;
|
|
5056
|
+
}
|
|
5057
|
+
const identityOk = plan.repoReal === null ? true : currentRepoReal === plan.repoReal;
|
|
5058
|
+
const removable = plan.items.filter((i) => i.state === "removable");
|
|
5059
|
+
if (!identityOk) {
|
|
5060
|
+
for (const item of removable) changed(item.label);
|
|
5061
|
+
return { removed, failed };
|
|
5062
|
+
}
|
|
5063
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
5064
|
+
const { canonicalName, repoReal } = plan;
|
|
5065
|
+
const expectedByName = new Map(
|
|
5066
|
+
repoReal !== null ? teardownExpectedTargets(repoReal, anchorReal, canonicalName).map((s) => [s.name, s.target]) : []
|
|
5067
|
+
);
|
|
5068
|
+
for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
|
|
5069
|
+
const expected = expectedByName.get(item.label);
|
|
5070
|
+
if (repoReal === null || expected === void 0 || inspectSymlink(join7(repoReal, item.label), expected).state !== "correct") {
|
|
5071
|
+
changed(item.label);
|
|
5072
|
+
continue;
|
|
5073
|
+
}
|
|
5074
|
+
try {
|
|
5075
|
+
unlinkSync(join7(repoReal, item.label));
|
|
5076
|
+
removed.push(item.label);
|
|
5077
|
+
} catch (error) {
|
|
5078
|
+
failed.push({ label: item.label, message: failureReason(error) });
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
const viewPath = manifest.workspace.view;
|
|
5082
|
+
for (const item of removable.filter((i) => i.kind === "view-symlink")) {
|
|
5083
|
+
if (viewPath === void 0) {
|
|
5084
|
+
changed(item.label);
|
|
5085
|
+
continue;
|
|
5086
|
+
}
|
|
5087
|
+
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
5088
|
+
const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve7(repositoryRoot, plan.target));
|
|
5089
|
+
if (!owned) {
|
|
5090
|
+
changed(item.label);
|
|
5091
|
+
continue;
|
|
5092
|
+
}
|
|
5093
|
+
try {
|
|
5094
|
+
unlinkSync(join7(viewDir, item.label));
|
|
5095
|
+
removed.push(`view/${item.label}`);
|
|
5096
|
+
} catch (error) {
|
|
5097
|
+
failed.push({ label: `view/${item.label}`, message: failureReason(error) });
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
5101
|
+
for (const item of removable.filter((i) => i.kind === "canonical-block")) {
|
|
5102
|
+
const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5103
|
+
try {
|
|
5104
|
+
if (lstatSync(canonicalFile).isSymbolicLink()) {
|
|
5105
|
+
changed(item.label);
|
|
5106
|
+
continue;
|
|
5107
|
+
}
|
|
5108
|
+
} catch (error) {
|
|
5109
|
+
failed.push({ label: item.label, message: failureReason(error) });
|
|
5110
|
+
continue;
|
|
5111
|
+
}
|
|
5112
|
+
let fd;
|
|
5113
|
+
try {
|
|
5114
|
+
fd = openSync(canonicalFile, fsConstants.O_RDWR | NOFOLLOW);
|
|
5115
|
+
} catch (error) {
|
|
5116
|
+
changed(item.label);
|
|
5117
|
+
void error;
|
|
5118
|
+
continue;
|
|
5119
|
+
}
|
|
5120
|
+
try {
|
|
5121
|
+
const content = readFileSync(fd, "utf8");
|
|
5122
|
+
if (parseMarkers(content).kind !== "ok") {
|
|
5123
|
+
changed(item.label);
|
|
5124
|
+
continue;
|
|
5125
|
+
}
|
|
5126
|
+
const next = Buffer.from(removeMarkerSection(content, item.label), "utf8");
|
|
5127
|
+
ftruncateSync(fd, 0);
|
|
5128
|
+
writeSync(fd, next, 0, next.length, 0);
|
|
5129
|
+
removed.push(item.label);
|
|
5130
|
+
} catch (error) {
|
|
5131
|
+
failed.push({ label: item.label, message: failureReason(error) });
|
|
5132
|
+
} finally {
|
|
5133
|
+
closeSync(fd);
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
return { removed, failed };
|
|
5137
|
+
}
|
|
5138
|
+
function renderProjectTeardown(result) {
|
|
5139
|
+
const lines = [];
|
|
5140
|
+
lines.push(`# teardown: ${result.target}`);
|
|
5141
|
+
lines.push("");
|
|
5142
|
+
if (result.isAnchor) {
|
|
5143
|
+
lines.push("The anchor (`.`) cannot be torn down (it is the project's home).");
|
|
5144
|
+
return lines.join("\n");
|
|
5145
|
+
}
|
|
5146
|
+
if (!result.resolved) {
|
|
5147
|
+
lines.push(
|
|
5148
|
+
"Note: the repo path could not be resolved (already deleted?). The in-repo wiring cannot be inspected, so only the view link / canonical are in scope."
|
|
5149
|
+
);
|
|
5150
|
+
lines.push("");
|
|
5151
|
+
}
|
|
5152
|
+
lines.push(
|
|
5153
|
+
result.inRoster ? "Status: still a roster member (the declaration remains in the manifest)." : "Status: not a roster member (already archived)."
|
|
5154
|
+
);
|
|
5155
|
+
lines.push("");
|
|
5156
|
+
const removable = result.items.filter((i) => i.state === "removable");
|
|
5157
|
+
const manual = result.items.filter((i) => i.state === "manual");
|
|
5158
|
+
const foreign = result.items.filter((i) => i.state === "foreign");
|
|
5159
|
+
const blocked = result.items.filter((i) => i.state === "blocked");
|
|
5160
|
+
if (removable.length === 0) {
|
|
5161
|
+
lines.push("No basou-generated artifact to remove.");
|
|
5162
|
+
} else {
|
|
5163
|
+
lines.push(`To remove (${removable.length}):`);
|
|
5164
|
+
for (const i of removable)
|
|
5165
|
+
lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
|
|
5166
|
+
}
|
|
5167
|
+
if (manual.length > 0) {
|
|
5168
|
+
lines.push("");
|
|
5169
|
+
lines.push("Check manually (not auto-removed):");
|
|
5170
|
+
for (const i of manual)
|
|
5171
|
+
lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
|
|
5172
|
+
}
|
|
5173
|
+
if (foreign.length > 0) {
|
|
5174
|
+
lines.push("");
|
|
5175
|
+
lines.push("Left untouched (not basou-generated):");
|
|
5176
|
+
for (const i of foreign)
|
|
5177
|
+
lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
|
|
5178
|
+
}
|
|
5179
|
+
if (blocked.length > 0) {
|
|
5180
|
+
lines.push("");
|
|
5181
|
+
lines.push("Could not be inspected:");
|
|
5182
|
+
for (const i of blocked)
|
|
5183
|
+
lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
|
|
5184
|
+
}
|
|
5185
|
+
lines.push("");
|
|
5186
|
+
if (result.applied) {
|
|
5187
|
+
lines.push(`--apply: removed ${result.removed.length}.`);
|
|
5188
|
+
for (const r of result.removed) lines.push(` \u2713 ${r}`);
|
|
5189
|
+
if (result.failed.length > 0) {
|
|
5190
|
+
lines.push("Failed:");
|
|
5191
|
+
for (const f of result.failed) lines.push(` \u2717 ${f.label} \u2014 ${f.message}`);
|
|
5192
|
+
}
|
|
5193
|
+
} else if (removable.length > 0) {
|
|
5194
|
+
lines.push(
|
|
5195
|
+
"This is a dry-run. Pass --apply to remove (this is a destructive, irreversible operation)."
|
|
5196
|
+
);
|
|
5197
|
+
}
|
|
5198
|
+
return lines.join("\n");
|
|
5199
|
+
}
|
|
5200
|
+
async function doRunProjectTeardown(target, options, ctx = {}) {
|
|
5201
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
5202
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project teardown");
|
|
5203
|
+
const paths = basouPaths10(repositoryRoot);
|
|
5204
|
+
const manifest = await readManifest6(paths);
|
|
5205
|
+
const plan = gatherRepoTeardown(repositoryRoot, manifest, target);
|
|
5206
|
+
const willApply = options.apply === true && !plan.isAnchor && plan.removableCount > 0;
|
|
5207
|
+
const { removed, failed } = willApply ? applyRepoTeardown(repositoryRoot, manifest, plan) : { removed: [], failed: [] };
|
|
5208
|
+
const result = { ...plan, applied: willApply, removed, failed };
|
|
5209
|
+
if (options.json === true) console.log(JSON.stringify(result));
|
|
5210
|
+
else console.log(renderProjectTeardown(result));
|
|
5211
|
+
return result;
|
|
5212
|
+
}
|
|
5213
|
+
async function runProjectTeardown(target, options, ctx = {}) {
|
|
5214
|
+
try {
|
|
5215
|
+
await doRunProjectTeardown(target, options, ctx);
|
|
5216
|
+
} catch (error) {
|
|
5217
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
5218
|
+
process.exitCode = 1;
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
4752
5221
|
function omitKey(obj, key) {
|
|
4753
5222
|
const clone = { ...obj };
|
|
4754
5223
|
delete clone[key];
|
|
@@ -4818,68 +5287,74 @@ async function doRunProjectArchive(target, options, ctx) {
|
|
|
4818
5287
|
}
|
|
4819
5288
|
function renderProjectArchive(result) {
|
|
4820
5289
|
const lines = [];
|
|
4821
|
-
lines.push("# repo
|
|
5290
|
+
lines.push("# Archive a repo (fold it out of the roster)");
|
|
4822
5291
|
lines.push("");
|
|
4823
5292
|
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
4824
5293
|
if (!result.hasRoster) {
|
|
4825
|
-
lines.push("\u2139\uFE0F repo
|
|
5294
|
+
lines.push("\u2139\uFE0F No repo roster declared (manifest `repos`). There is nothing to archive.");
|
|
4826
5295
|
return lines.join("\n");
|
|
4827
5296
|
}
|
|
4828
5297
|
if (result.isAnchor) {
|
|
4829
5298
|
lines.push(
|
|
4830
|
-
`\u26A0\uFE0F \`${result.target}\`
|
|
5299
|
+
`\u26A0\uFE0F \`${result.target}\` is the anchor (the project root). The anchor cannot be archived (it is the manifest's home).`
|
|
4831
5300
|
);
|
|
4832
5301
|
return lines.join("\n");
|
|
4833
5302
|
}
|
|
4834
5303
|
if (!result.found) {
|
|
4835
|
-
lines.push(`\u2139\uFE0F \`${result.target}\`
|
|
5304
|
+
lines.push(`\u2139\uFE0F \`${result.target}\` is not declared in the roster (nothing to archive).`);
|
|
4836
5305
|
return lines.join("\n");
|
|
4837
5306
|
}
|
|
4838
5307
|
if (result.applied) {
|
|
4839
|
-
lines.push(`\u2705 \`${result.target}\`
|
|
5308
|
+
lines.push(`\u2705 Removed \`${result.target}\` from the roster.`);
|
|
4840
5309
|
} else {
|
|
4841
|
-
lines.push(
|
|
5310
|
+
lines.push(`To remove \`${result.target}\` from the roster (dry-run; pass --apply to write):`);
|
|
4842
5311
|
}
|
|
4843
5312
|
if (result.sourceRootRemoval !== void 0) {
|
|
4844
5313
|
lines.push(
|
|
4845
|
-
`-
|
|
5314
|
+
`- ${result.applied ? "Pruned" : "Will prune"} ${result.sourceRootRemoval} from source_roots (no longer captured by refresh).`
|
|
4846
5315
|
);
|
|
4847
5316
|
} else {
|
|
4848
|
-
lines.push("- source_roots
|
|
5317
|
+
lines.push("- No matching entry in source_roots (nothing to prune).");
|
|
4849
5318
|
}
|
|
4850
5319
|
if (result.reposEmptied) {
|
|
4851
5320
|
lines.push(
|
|
4852
|
-
"-
|
|
5321
|
+
"- This was the last member \u2192 the roster empties and the `repos` declaration is removed (the project is folded up)."
|
|
4853
5322
|
);
|
|
4854
5323
|
} else if (result.becomesSolo) {
|
|
4855
5324
|
lines.push(
|
|
4856
|
-
"-
|
|
5325
|
+
"- This leaves 1 repo (solo) \u2192 the workspace view is unnecessary (consider removing the view declaration / directory)."
|
|
4857
5326
|
);
|
|
4858
5327
|
}
|
|
4859
5328
|
lines.push("");
|
|
4860
5329
|
const t = result.teardown;
|
|
4861
5330
|
const items = [];
|
|
4862
|
-
if (t.viewLink) items.push("workspace view
|
|
4863
|
-
if (t.instructionFiles.length > 0)
|
|
5331
|
+
if (t.viewLink) items.push("the workspace view's symlink entry");
|
|
5332
|
+
if (t.instructionFiles.length > 0)
|
|
5333
|
+
items.push(`instruction files (${t.instructionFiles.join(", ")})`);
|
|
4864
5334
|
if (t.gitignorePatterns.length > 0)
|
|
4865
|
-
items.push(`.gitignore
|
|
4866
|
-
if (t.canonical)
|
|
5335
|
+
items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
|
|
5336
|
+
if (t.canonical)
|
|
5337
|
+
items.push(`the anchor's canonical (agents/${basename4(result.target)}/AGENTS.md)`);
|
|
4867
5338
|
if (!t.inspected) {
|
|
4868
|
-
lines.push("## \u624B\u52D5 teardown(repo \u304C\u30C7\u30A3\u30B9\u30AF\u4E0A\u306B\u89E3\u6C7A\u3067\u304D\u306A\u3044\u305F\u3081\u672A\u691C\u67FB)");
|
|
4869
5339
|
lines.push(
|
|
4870
|
-
"
|
|
5340
|
+
"## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
|
|
5341
|
+
);
|
|
5342
|
+
lines.push(
|
|
5343
|
+
"- The repo may already be deleted. Check manually for a leftover view symlink / instruction symlinks / .gitignore / canonical."
|
|
4871
5344
|
);
|
|
4872
5345
|
lines.push("");
|
|
4873
5346
|
} else if (items.length > 0) {
|
|
4874
|
-
lines.push(
|
|
5347
|
+
lines.push(
|
|
5348
|
+
"## Manual teardown (--apply does not touch these; remove the leftover wiring by hand)"
|
|
5349
|
+
);
|
|
4875
5350
|
for (const i of items) lines.push(`- ${i}`);
|
|
4876
5351
|
lines.push("");
|
|
4877
5352
|
} else {
|
|
4878
|
-
lines.push("repo
|
|
5353
|
+
lines.push("No repo-side wiring (view / instruction files / .gitignore / canonical) remains.");
|
|
4879
5354
|
lines.push("");
|
|
4880
5355
|
}
|
|
4881
5356
|
lines.push(
|
|
4882
|
-
"
|
|
5357
|
+
"Note: archive only changes the manifest (.basou, git-tracked, reversible). The repo, its captured history, and its on-disk wiring are not removed."
|
|
4883
5358
|
);
|
|
4884
5359
|
return lines.join("\n");
|
|
4885
5360
|
}
|
|
@@ -4965,46 +5440,46 @@ async function doRunProjectRename(oldPath, newPath, options, ctx) {
|
|
|
4965
5440
|
}
|
|
4966
5441
|
function renderProjectRename(result) {
|
|
4967
5442
|
const lines = [];
|
|
4968
|
-
lines.push("# repo
|
|
5443
|
+
lines.push("# Rename a repo (update its roster path)");
|
|
4969
5444
|
lines.push("");
|
|
4970
5445
|
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
4971
5446
|
if (!result.hasRoster) {
|
|
4972
|
-
lines.push("\u2139\uFE0F repo
|
|
5447
|
+
lines.push("\u2139\uFE0F No repo roster declared (manifest `repos`). There is nothing to rename.");
|
|
4973
5448
|
return lines.join("\n");
|
|
4974
5449
|
}
|
|
4975
5450
|
if (result.noop) {
|
|
4976
|
-
lines.push(`\u2139\uFE0F \`${result.oldTarget}\`
|
|
5451
|
+
lines.push(`\u2139\uFE0F \`${result.oldTarget}\` and \`${result.newTarget}\` are identical (no change).`);
|
|
4977
5452
|
return lines.join("\n");
|
|
4978
5453
|
}
|
|
4979
5454
|
if (result.isAnchor) {
|
|
4980
5455
|
lines.push(
|
|
4981
|
-
`\u26A0\uFE0F \`${result.oldTarget}\`
|
|
5456
|
+
`\u26A0\uFE0F \`${result.oldTarget}\` is the anchor (the project root). The anchor cannot be renamed.`
|
|
4982
5457
|
);
|
|
4983
5458
|
return lines.join("\n");
|
|
4984
5459
|
}
|
|
4985
5460
|
if (!result.found) {
|
|
4986
|
-
lines.push(`\u2139\uFE0F \`${result.oldTarget}\`
|
|
5461
|
+
lines.push(`\u2139\uFE0F \`${result.oldTarget}\` is not declared in the roster (nothing to rename).`);
|
|
4987
5462
|
return lines.join("\n");
|
|
4988
5463
|
}
|
|
4989
5464
|
if (result.collision) {
|
|
4990
5465
|
lines.push(
|
|
4991
|
-
`\u26A0\uFE0F \`${result.newTarget}\`
|
|
5466
|
+
`\u26A0\uFE0F \`${result.newTarget}\` is already declared in the roster. Not renaming, to avoid a duplicate.`
|
|
4992
5467
|
);
|
|
4993
5468
|
return lines.join("\n");
|
|
4994
5469
|
}
|
|
4995
5470
|
if (result.applied) {
|
|
4996
|
-
lines.push(`\u2705 \`${result.oldTarget}\`
|
|
5471
|
+
lines.push(`\u2705 Renamed \`${result.oldTarget}\` to \`${result.newTarget}\`.`);
|
|
4997
5472
|
} else {
|
|
4998
5473
|
lines.push(
|
|
4999
|
-
|
|
5474
|
+
`To rename \`${result.oldTarget}\` to \`${result.newTarget}\` (dry-run; pass --apply to write):`
|
|
5000
5475
|
);
|
|
5001
5476
|
}
|
|
5002
5477
|
if (result.sourceRootRenamed !== void 0) {
|
|
5003
5478
|
lines.push(
|
|
5004
|
-
`-
|
|
5479
|
+
`- ${result.applied ? "Updated" : "Will update"} ${result.sourceRootRenamed} to ${result.newTarget} in source_roots.`
|
|
5005
5480
|
);
|
|
5006
5481
|
} else {
|
|
5007
|
-
lines.push("- source_roots
|
|
5482
|
+
lines.push("- No matching entry in source_roots (nothing to update).");
|
|
5008
5483
|
}
|
|
5009
5484
|
lines.push("");
|
|
5010
5485
|
if (result.basenameChanged) {
|
|
@@ -5013,28 +5488,447 @@ function renderProjectRename(result) {
|
|
|
5013
5488
|
const items = [];
|
|
5014
5489
|
if (result.wiring.canonicalDirOld)
|
|
5015
5490
|
items.push(`anchor canonical: agents/${oldName}/ \u2192 agents/${newName}/`);
|
|
5016
|
-
if (result.wiring.viewLinkOld) items.push(`workspace view
|
|
5491
|
+
if (result.wiring.viewLinkOld) items.push(`workspace view symlink: ${oldName} \u2192 ${newName}`);
|
|
5017
5492
|
if (items.length > 0) {
|
|
5018
5493
|
lines.push(
|
|
5019
|
-
"##
|
|
5494
|
+
"## Manual rename (--apply does not touch these; the basename changed, so update them by hand)"
|
|
5020
5495
|
);
|
|
5021
5496
|
for (const i of items) lines.push(`- ${i}`);
|
|
5022
5497
|
} else {
|
|
5023
5498
|
lines.push(
|
|
5024
|
-
`basename
|
|
5499
|
+
`The basename changes ${oldName} \u2192 ${newName}, but no anchor canonical / view symlink was found.`
|
|
5025
5500
|
);
|
|
5026
5501
|
}
|
|
5027
5502
|
lines.push(
|
|
5028
|
-
"
|
|
5503
|
+
" After applying, regenerate the instruction symlinks and the view with `basou project symlinks` / `basou project workspace`."
|
|
5029
5504
|
);
|
|
5030
5505
|
} else {
|
|
5031
5506
|
lines.push(
|
|
5032
|
-
"
|
|
5507
|
+
"Note: the basename is unchanged. If you moved the repo elsewhere, regenerate the relative targets with `basou project symlinks` / `basou project workspace`."
|
|
5033
5508
|
);
|
|
5034
5509
|
}
|
|
5035
5510
|
lines.push("");
|
|
5036
5511
|
lines.push(
|
|
5037
|
-
"
|
|
5512
|
+
"Note: rename only changes the manifest (.basou, git-tracked, reversible). It does not move the repo or update the on-disk wiring."
|
|
5513
|
+
);
|
|
5514
|
+
return lines.join("\n");
|
|
5515
|
+
}
|
|
5516
|
+
async function runProjectNew(repos, options, ctx = {}) {
|
|
5517
|
+
try {
|
|
5518
|
+
await doRunProjectNew(repos, options, ctx);
|
|
5519
|
+
} catch (error) {
|
|
5520
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
5521
|
+
process.exitCode = 1;
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
async function resolveRepositoryRootForNew(cwd) {
|
|
5525
|
+
try {
|
|
5526
|
+
return await resolveRepositoryRoot8(cwd);
|
|
5527
|
+
} catch (error) {
|
|
5528
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5529
|
+
throw new Error(
|
|
5530
|
+
"Not a git repository. Run 'git init' first, then re-run 'basou project new'.",
|
|
5531
|
+
{ cause: error }
|
|
5532
|
+
);
|
|
5533
|
+
}
|
|
5534
|
+
throw error;
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
async function doRunProjectNew(repos, options, ctx) {
|
|
5538
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
5539
|
+
const repositoryRoot = await resolveRepositoryRootForNew(cwd);
|
|
5540
|
+
const workspaceName = basename4(repositoryRoot);
|
|
5541
|
+
const declared = repos.map((p) => {
|
|
5542
|
+
const abs = resolve7(cwd, p);
|
|
5543
|
+
let real;
|
|
5544
|
+
try {
|
|
5545
|
+
real = realpathSync(abs);
|
|
5546
|
+
} catch {
|
|
5547
|
+
real = abs;
|
|
5548
|
+
}
|
|
5549
|
+
const rel = relative2(repositoryRoot, real);
|
|
5550
|
+
return rel === "" ? "." : rel;
|
|
5551
|
+
});
|
|
5552
|
+
const invalidRepos = declared.filter(
|
|
5553
|
+
(rel) => classifySourceRoot(repositoryRoot, rel).kind !== "repo"
|
|
5554
|
+
);
|
|
5555
|
+
if (invalidRepos.length > 0) {
|
|
5556
|
+
throw new Error(
|
|
5557
|
+
`These declared repos are not git repositories (create them with 'git init' first): ${invalidRepos.join(", ")}`
|
|
5558
|
+
);
|
|
5559
|
+
}
|
|
5560
|
+
const rosterPaths = ["."];
|
|
5561
|
+
for (const rel of declared) {
|
|
5562
|
+
if (rel !== "." && !rosterPaths.includes(rel)) rosterPaths.push(rel);
|
|
5563
|
+
}
|
|
5564
|
+
const roster = rosterPaths.map((path) => ({ path }));
|
|
5565
|
+
const viewPath = options.view === false ? null : options.view ?? `../${workspaceName}-workspace`;
|
|
5566
|
+
const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
|
|
5567
|
+
const paths = basouPaths10(repositoryRoot);
|
|
5568
|
+
const existed = existsSync(paths.files.manifest);
|
|
5569
|
+
const manifest = createManifest2({ workspaceName, sourceRoots });
|
|
5570
|
+
manifest.repos = roster;
|
|
5571
|
+
if (viewPath !== null) manifest.workspace.view = viewPath;
|
|
5572
|
+
let applied = false;
|
|
5573
|
+
if (options.apply === true) {
|
|
5574
|
+
await ensureBasouDirectory2(repositoryRoot);
|
|
5575
|
+
await writeManifest2(paths, manifest, { force: options.force === true });
|
|
5576
|
+
applied = true;
|
|
5577
|
+
try {
|
|
5578
|
+
await appendBasouGitignore2(repositoryRoot, { localOnly: options.localOnly === true });
|
|
5579
|
+
} catch (error) {
|
|
5580
|
+
renderGitignoreWarningForNew(error, isVerbose(options));
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
const result = {
|
|
5584
|
+
workspaceName,
|
|
5585
|
+
repos: roster,
|
|
5586
|
+
view: viewPath,
|
|
5587
|
+
sourceRoots,
|
|
5588
|
+
invalidRepos: [],
|
|
5589
|
+
existed,
|
|
5590
|
+
applied
|
|
5591
|
+
};
|
|
5592
|
+
if (options.json === true) {
|
|
5593
|
+
console.log(JSON.stringify(result));
|
|
5594
|
+
} else {
|
|
5595
|
+
console.log(renderProjectNew(result));
|
|
5596
|
+
}
|
|
5597
|
+
return result;
|
|
5598
|
+
}
|
|
5599
|
+
function renderGitignoreWarningForNew(error, verbose) {
|
|
5600
|
+
const baseMessage = error instanceof Error ? error.message : String(error);
|
|
5601
|
+
console.error(
|
|
5602
|
+
`Warning: Could not update .gitignore (${baseMessage}). Add Basou's default .gitignore block manually.`
|
|
5603
|
+
);
|
|
5604
|
+
if (verbose && error instanceof Error) {
|
|
5605
|
+
const label = extractCauseLabel(error);
|
|
5606
|
+
if (label !== void 0) console.error(`Caused by: ${label}`);
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
function renderProjectNew(result) {
|
|
5610
|
+
const lines = [];
|
|
5611
|
+
lines.push("# Scaffold a new project (build from a declaration)");
|
|
5612
|
+
lines.push("");
|
|
5613
|
+
if (result.existed && !result.applied) {
|
|
5614
|
+
lines.push(
|
|
5615
|
+
"\u26A0\uFE0F This anchor already has a `.basou/manifest.yaml`. Overwriting it requires --force (nothing is written by default, so an existing declaration is not lost)."
|
|
5616
|
+
);
|
|
5617
|
+
lines.push("");
|
|
5618
|
+
}
|
|
5619
|
+
if (result.applied) {
|
|
5620
|
+
lines.push(`\u2705 Created \`.basou/\` for \`${result.workspaceName}\` and seeded the manifest:`);
|
|
5621
|
+
} else {
|
|
5622
|
+
lines.push(
|
|
5623
|
+
`Will create \`.basou/\` for \`${result.workspaceName}\` and seed the manifest (dry-run; pass --apply to write):`
|
|
5624
|
+
);
|
|
5625
|
+
}
|
|
5626
|
+
lines.push("");
|
|
5627
|
+
lines.push(`repos roster (${result.repos.length}):`);
|
|
5628
|
+
for (const r of result.repos) {
|
|
5629
|
+
lines.push(`- ${r.path}${r.path === "." ? " (anchor)" : ""}`);
|
|
5630
|
+
}
|
|
5631
|
+
lines.push("");
|
|
5632
|
+
lines.push(
|
|
5633
|
+
result.view !== null ? `workspace view: ${result.view}` : "workspace view: none (solo project)"
|
|
5634
|
+
);
|
|
5635
|
+
lines.push("");
|
|
5636
|
+
lines.push(`source_roots (${result.sourceRoots.length}):`);
|
|
5637
|
+
for (const s of result.sourceRoots) lines.push(`- ${s}`);
|
|
5638
|
+
lines.push("");
|
|
5639
|
+
lines.push(
|
|
5640
|
+
"Note: visibility / language are unset. Assign them to each repo manually. Basou does not create git repos; a declared repo must already be `git init`-ed."
|
|
5641
|
+
);
|
|
5642
|
+
if (result.applied) {
|
|
5643
|
+
lines.push(
|
|
5644
|
+
"Next: fill in visibility / language for each repo in `.basou/manifest.yaml`, then run `basou project derive --apply` to generate the wiring in one pass."
|
|
5645
|
+
);
|
|
5646
|
+
} else {
|
|
5647
|
+
lines.push(
|
|
5648
|
+
"After applying, fill in visibility / language in the manifest, then run `basou project derive`."
|
|
5649
|
+
);
|
|
5650
|
+
}
|
|
5651
|
+
return lines.join("\n");
|
|
5652
|
+
}
|
|
5653
|
+
async function runProjectDerive(options, ctx = {}) {
|
|
5654
|
+
try {
|
|
5655
|
+
await doRunProjectDerive(options, ctx);
|
|
5656
|
+
} catch (error) {
|
|
5657
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
5658
|
+
process.exitCode = 1;
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
async function doRunProjectDerive(options, ctx) {
|
|
5662
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
5663
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
|
|
5664
|
+
const paths = basouPaths10(repositoryRoot);
|
|
5665
|
+
const manifest = await readManifest6(paths);
|
|
5666
|
+
if (manifest.repos === void 0 || manifest.repos.length === 0) {
|
|
5667
|
+
console.log(
|
|
5668
|
+
"# Generate project wiring in one pass (declaration \u2192 wiring)\n\n\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one first with `basou project new` (new project) or `basou project adopt` (bootstrap from existing source_roots)."
|
|
5669
|
+
);
|
|
5670
|
+
return;
|
|
5671
|
+
}
|
|
5672
|
+
const apply = options.apply === true;
|
|
5673
|
+
const stepCtx = {
|
|
5674
|
+
cwd: repositoryRoot,
|
|
5675
|
+
...ctx.now !== void 0 ? { now: ctx.now } : {}
|
|
5676
|
+
};
|
|
5677
|
+
const stepOpts = { apply };
|
|
5678
|
+
console.log("# Generate project wiring in one pass (declaration \u2192 wiring)");
|
|
5679
|
+
console.log("");
|
|
5680
|
+
console.log("## 1/5 sync source_roots (roster \u2192 capture config)");
|
|
5681
|
+
await doRunProjectSync(stepOpts, stepCtx);
|
|
5682
|
+
console.log("");
|
|
5683
|
+
console.log("## 2/5 generate instruction-file A preset (declaration \u2192 canonical)");
|
|
5684
|
+
await doRunProjectPreset(stepOpts, stepCtx);
|
|
5685
|
+
console.log("");
|
|
5686
|
+
console.log("## 3/5 generate instruction-file symlinks (each repo \u2192 canonical)");
|
|
5687
|
+
await doRunProjectSymlinks(stepOpts, stepCtx);
|
|
5688
|
+
console.log("");
|
|
5689
|
+
console.log("## 4/5 generate workspace view (aggregate the roster repos)");
|
|
5690
|
+
await doRunProjectWorkspace(stepOpts, stepCtx);
|
|
5691
|
+
console.log("");
|
|
5692
|
+
console.log("## 5/5 generate .gitignore (exclude public repos' instruction files)");
|
|
5693
|
+
await doRunProjectGitignore(stepOpts, stepCtx);
|
|
5694
|
+
console.log("");
|
|
5695
|
+
console.log(
|
|
5696
|
+
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."
|
|
5697
|
+
);
|
|
5698
|
+
}
|
|
5699
|
+
async function runProjectRetrofit(repo, options, ctx = {}) {
|
|
5700
|
+
try {
|
|
5701
|
+
await doRunProjectRetrofit(repo, options, ctx);
|
|
5702
|
+
} catch (error) {
|
|
5703
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
5704
|
+
process.exitCode = 1;
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
function inspectAgentsState(filePath) {
|
|
5708
|
+
let st;
|
|
5709
|
+
try {
|
|
5710
|
+
st = lstatSync(filePath);
|
|
5711
|
+
} catch (error) {
|
|
5712
|
+
if (hasErrorCode(error) && error.code === "ENOENT") return "absent";
|
|
5713
|
+
return "blocked";
|
|
5714
|
+
}
|
|
5715
|
+
if (st.isSymbolicLink()) return "symlink";
|
|
5716
|
+
return st.isFile() ? "regular-file" : "blocked";
|
|
5717
|
+
}
|
|
5718
|
+
function regularFileSpokes(repoReal) {
|
|
5719
|
+
const out = [];
|
|
5720
|
+
for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
|
|
5721
|
+
try {
|
|
5722
|
+
const st = lstatSync(join7(repoReal, spoke));
|
|
5723
|
+
if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
|
|
5724
|
+
} catch {
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
return out;
|
|
5728
|
+
}
|
|
5729
|
+
function pathPresent(p) {
|
|
5730
|
+
try {
|
|
5731
|
+
lstatSync(p);
|
|
5732
|
+
return true;
|
|
5733
|
+
} catch {
|
|
5734
|
+
return false;
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal) {
|
|
5738
|
+
const declared = roster.some((entry) => {
|
|
5739
|
+
const entryAbs = resolve7(repositoryRoot, entry.path);
|
|
5740
|
+
if (argReal !== void 0) {
|
|
5741
|
+
try {
|
|
5742
|
+
if (realpathSync(entryAbs) === argReal) return true;
|
|
5743
|
+
} catch {
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
return entryAbs === argAbs;
|
|
5747
|
+
});
|
|
5748
|
+
const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
|
|
5749
|
+
const path = displayRel === "" ? "." : displayRel;
|
|
5750
|
+
const canonicalName = basename4(argReal ?? argAbs);
|
|
5751
|
+
if (argReal === void 0) {
|
|
5752
|
+
return {
|
|
5753
|
+
path,
|
|
5754
|
+
declared,
|
|
5755
|
+
isAnchor: false,
|
|
5756
|
+
reachable: false,
|
|
5757
|
+
canonicalName,
|
|
5758
|
+
agentsState: "absent",
|
|
5759
|
+
canonicalExists: false,
|
|
5760
|
+
regularSpokes: []
|
|
5761
|
+
};
|
|
5762
|
+
}
|
|
5763
|
+
const isAnchor = argReal === anchorReal;
|
|
5764
|
+
const reachable = existsSync(join7(argReal, ".git"));
|
|
5765
|
+
const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5766
|
+
return {
|
|
5767
|
+
path,
|
|
5768
|
+
declared,
|
|
5769
|
+
isAnchor,
|
|
5770
|
+
reachable,
|
|
5771
|
+
canonicalName,
|
|
5772
|
+
agentsState: inspectAgentsState(join7(argReal, CANONICAL_FILE)),
|
|
5773
|
+
canonicalExists: pathPresent(canonicalFile),
|
|
5774
|
+
regularSpokes: regularFileSpokes(argReal)
|
|
5775
|
+
};
|
|
5776
|
+
}
|
|
5777
|
+
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
5778
|
+
const agentsFile = join7(repoReal, CANONICAL_FILE);
|
|
5779
|
+
try {
|
|
5780
|
+
mkdirSync(dirname2(canonicalFile), { recursive: true });
|
|
5781
|
+
} catch (error) {
|
|
5782
|
+
return { ok: false, message: failureReason(error), partial: false };
|
|
5783
|
+
}
|
|
5784
|
+
try {
|
|
5785
|
+
copyFileSync(agentsFile, canonicalFile, fsConstants.COPYFILE_EXCL);
|
|
5786
|
+
} catch (error) {
|
|
5787
|
+
const message = hasErrorCode(error) && error.code === "EEXIST" ? "canonical-exists" : failureReason(error);
|
|
5788
|
+
return { ok: false, message, partial: false };
|
|
5789
|
+
}
|
|
5790
|
+
try {
|
|
5791
|
+
unlinkSync(agentsFile);
|
|
5792
|
+
symlinkSync(relative2(repoReal, canonicalFile), agentsFile);
|
|
5793
|
+
return { ok: true };
|
|
5794
|
+
} catch (error) {
|
|
5795
|
+
return { ok: false, message: failureReason(error), partial: true };
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
async function doRunProjectRetrofit(repo, options, ctx) {
|
|
5799
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
5800
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project retrofit");
|
|
5801
|
+
const paths = basouPaths10(repositoryRoot);
|
|
5802
|
+
const manifest = await readManifest6(paths);
|
|
5803
|
+
const roster = manifest.repos ?? [];
|
|
5804
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
5805
|
+
const argAbs = resolve7(repositoryRoot, repo);
|
|
5806
|
+
let argReal;
|
|
5807
|
+
try {
|
|
5808
|
+
argReal = realpathSync(argAbs);
|
|
5809
|
+
} catch {
|
|
5810
|
+
argReal = void 0;
|
|
5811
|
+
}
|
|
5812
|
+
const facts = gatherRetrofit(repositoryRoot, anchorReal, roster, repo, argAbs, argReal);
|
|
5813
|
+
const plan = classifyRetrofit(facts);
|
|
5814
|
+
let applied = false;
|
|
5815
|
+
let failure;
|
|
5816
|
+
let partial = false;
|
|
5817
|
+
if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
|
|
5818
|
+
const canonicalFile = join7(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
|
|
5819
|
+
const res = relocateAgentsFile(argReal, canonicalFile);
|
|
5820
|
+
if (res.ok) {
|
|
5821
|
+
applied = true;
|
|
5822
|
+
} else {
|
|
5823
|
+
failure = res.message;
|
|
5824
|
+
partial = res.partial;
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
const result = {
|
|
5828
|
+
...plan,
|
|
5829
|
+
hasRoster: roster.length > 0,
|
|
5830
|
+
applied,
|
|
5831
|
+
...failure !== void 0 ? { failure } : {},
|
|
5832
|
+
...partial ? { partial } : {}
|
|
5833
|
+
};
|
|
5834
|
+
if (options.json === true) {
|
|
5835
|
+
console.log(JSON.stringify(result));
|
|
5836
|
+
} else {
|
|
5837
|
+
console.log(renderProjectRetrofit(result));
|
|
5838
|
+
}
|
|
5839
|
+
return result;
|
|
5840
|
+
}
|
|
5841
|
+
function appendSpokeChecklist(lines, spokes) {
|
|
5842
|
+
if (spokes.length === 0) return;
|
|
5843
|
+
lines.push(
|
|
5844
|
+
`## Spoke files to reconcile (${spokes.length}) \u2014 regular files that would block clean wiring`
|
|
5845
|
+
);
|
|
5846
|
+
for (const s of spokes) {
|
|
5847
|
+
lines.push(
|
|
5848
|
+
`- ${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\`.`
|
|
5849
|
+
);
|
|
5850
|
+
}
|
|
5851
|
+
lines.push("");
|
|
5852
|
+
}
|
|
5853
|
+
function renderProjectRetrofit(result) {
|
|
5854
|
+
const lines = [];
|
|
5855
|
+
lines.push(
|
|
5856
|
+
"# Retrofit an existing AGENTS.md into the project (relocate to the anchor canonical)"
|
|
5857
|
+
);
|
|
5858
|
+
lines.push("");
|
|
5859
|
+
if (!result.hasRoster) {
|
|
5860
|
+
lines.push(
|
|
5861
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare the repo first with `basou project new` (or `basou project adopt`), then re-run."
|
|
5862
|
+
);
|
|
5863
|
+
return lines.join("\n");
|
|
5864
|
+
}
|
|
5865
|
+
const canonical2 = `agents/${result.canonicalName}/${CANONICAL_FILE}`;
|
|
5866
|
+
if (result.action === "refuse") {
|
|
5867
|
+
if (result.reason === "not-declared") {
|
|
5868
|
+
lines.push(
|
|
5869
|
+
`\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.`
|
|
5870
|
+
);
|
|
5871
|
+
} else if (result.reason === "anchor") {
|
|
5872
|
+
lines.push(
|
|
5873
|
+
`\u26A0\uFE0F \`${result.path}\` is the anchor (the project root). It owns its canonical directly \u2014 there is nothing to relocate.`
|
|
5874
|
+
);
|
|
5875
|
+
} else if (result.reason === "unreachable") {
|
|
5876
|
+
lines.push(
|
|
5877
|
+
`\u26A0\uFE0F \`${result.path}\` does not resolve to a git repository. There is nothing to retrofit.`
|
|
5878
|
+
);
|
|
5879
|
+
} else if (result.reason === "blocked") {
|
|
5880
|
+
lines.push(
|
|
5881
|
+
`\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.`
|
|
5882
|
+
);
|
|
5883
|
+
} else if (result.reason === "canonical-exists") {
|
|
5884
|
+
lines.push(
|
|
5885
|
+
`\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.`
|
|
5886
|
+
);
|
|
5887
|
+
}
|
|
5888
|
+
return lines.join("\n");
|
|
5889
|
+
}
|
|
5890
|
+
if (result.action === "skip") {
|
|
5891
|
+
if (result.reason === "already-symlink") {
|
|
5892
|
+
lines.push(
|
|
5893
|
+
`\u2705 \`${result.path}/${CANONICAL_FILE}\` is already a symlink (already wired). Nothing to retrofit.`
|
|
5894
|
+
);
|
|
5895
|
+
} else {
|
|
5896
|
+
lines.push(
|
|
5897
|
+
`\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.`
|
|
5898
|
+
);
|
|
5899
|
+
}
|
|
5900
|
+
lines.push("");
|
|
5901
|
+
appendSpokeChecklist(lines, result.regularSpokes);
|
|
5902
|
+
return lines.join("\n").trimEnd();
|
|
5903
|
+
}
|
|
5904
|
+
if (result.failure !== void 0) {
|
|
5905
|
+
lines.push(
|
|
5906
|
+
`Could not relocate \`${result.path}/${CANONICAL_FILE}\` to \`${canonical2}\`: ${result.failure}.`
|
|
5907
|
+
);
|
|
5908
|
+
if (result.partial === true) {
|
|
5909
|
+
lines.push(
|
|
5910
|
+
`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.`
|
|
5911
|
+
);
|
|
5912
|
+
} else {
|
|
5913
|
+
lines.push("Nothing was changed. Resolve the cause and re-run.");
|
|
5914
|
+
}
|
|
5915
|
+
return lines.join("\n");
|
|
5916
|
+
}
|
|
5917
|
+
if (result.applied) {
|
|
5918
|
+
lines.push(
|
|
5919
|
+
`\u2705 Relocated \`${result.path}/${CANONICAL_FILE}\` to \`${canonical2}\` and left a symlink in its place.`
|
|
5920
|
+
);
|
|
5921
|
+
} else {
|
|
5922
|
+
lines.push(
|
|
5923
|
+
`To relocate \`${result.path}/${CANONICAL_FILE}\` and replace it with a symlink (dry-run; pass --apply to write):`
|
|
5924
|
+
);
|
|
5925
|
+
lines.push(` move ${result.path}/${CANONICAL_FILE} -> ${canonical2}`);
|
|
5926
|
+
lines.push(` symlink ${result.path}/${CANONICAL_FILE} -> the canonical`);
|
|
5927
|
+
}
|
|
5928
|
+
lines.push("");
|
|
5929
|
+
appendSpokeChecklist(lines, result.regularSpokes);
|
|
5930
|
+
lines.push(
|
|
5931
|
+
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)."
|
|
5038
5932
|
);
|
|
5039
5933
|
return lines.join("\n");
|
|
5040
5934
|
}
|
|
@@ -5046,7 +5940,7 @@ import {
|
|
|
5046
5940
|
PROTOCOL_START,
|
|
5047
5941
|
parseMarkers as parseMarkers2,
|
|
5048
5942
|
readMarkdownFile as readMarkdownFile5,
|
|
5049
|
-
removeMarkerSection
|
|
5943
|
+
removeMarkerSection as removeMarkerSection2
|
|
5050
5944
|
} from "@basou/core";
|
|
5051
5945
|
|
|
5052
5946
|
// src/lib/durable-write.ts
|
|
@@ -5338,7 +6232,7 @@ async function doRunProtocolUnsync(options) {
|
|
|
5338
6232
|
console.log("No target file; nothing to remove.");
|
|
5339
6233
|
return;
|
|
5340
6234
|
}
|
|
5341
|
-
const newBody =
|
|
6235
|
+
const newBody = removeMarkerSection2(existing, "CLAUDE.md", PROTOCOL_MARKERS);
|
|
5342
6236
|
if (newBody === existing) {
|
|
5343
6237
|
console.log("No basou:protocols block found; nothing removed.");
|
|
5344
6238
|
return;
|
|
@@ -5711,7 +6605,7 @@ import {
|
|
|
5711
6605
|
basouPaths as basouPaths12,
|
|
5712
6606
|
findErrorCode as findErrorCode10,
|
|
5713
6607
|
renderReport,
|
|
5714
|
-
resolveRepositoryRoot as
|
|
6608
|
+
resolveRepositoryRoot as resolveRepositoryRoot9,
|
|
5715
6609
|
writeMarkdownFile as writeMarkdownFile6
|
|
5716
6610
|
} from "@basou/core";
|
|
5717
6611
|
function registerReportCommand(program) {
|
|
@@ -5760,7 +6654,7 @@ async function doRunReportGenerate(options, ctx) {
|
|
|
5760
6654
|
}
|
|
5761
6655
|
async function resolveRepositoryRootForReport(cwd) {
|
|
5762
6656
|
try {
|
|
5763
|
-
return await
|
|
6657
|
+
return await resolveRepositoryRoot9(cwd);
|
|
5764
6658
|
} catch (error) {
|
|
5765
6659
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5766
6660
|
throw new Error(
|
|
@@ -5843,45 +6737,45 @@ async function doRunReviewGaps(options, ctx) {
|
|
|
5843
6737
|
return summary;
|
|
5844
6738
|
}
|
|
5845
6739
|
function relAge(iso, now) {
|
|
5846
|
-
if (iso === null) return "(
|
|
6740
|
+
if (iso === null) return "(unknown)";
|
|
5847
6741
|
const ms = now.getTime() - Date.parse(iso);
|
|
5848
|
-
if (!Number.isFinite(ms) || ms < 0) return "
|
|
6742
|
+
if (!Number.isFinite(ms) || ms < 0) return "just now";
|
|
5849
6743
|
const days = Math.floor(ms / 864e5);
|
|
5850
|
-
if (days >= 1) return `${days}
|
|
6744
|
+
if (days >= 1) return `${days}d ago`;
|
|
5851
6745
|
const hours = Math.floor(ms / 36e5);
|
|
5852
|
-
if (hours >= 1) return `${hours}
|
|
5853
|
-
return `${Math.max(1, Math.floor(ms / 6e4))}
|
|
6746
|
+
if (hours >= 1) return `${hours}h ago`;
|
|
6747
|
+
return `${Math.max(1, Math.floor(ms / 6e4))}m ago`;
|
|
5854
6748
|
}
|
|
5855
6749
|
function unitLine(u, now) {
|
|
5856
6750
|
const when = relAge(u.lastCommitAt, now);
|
|
5857
6751
|
const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
|
|
5858
6752
|
if (u.verdict === "near_unbound") {
|
|
5859
6753
|
const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
|
|
5860
|
-
return `${head} \u2014
|
|
6754
|
+
return `${head} \u2014 a nearby review exists, but the diff / changed files were not examined [${ids}]`;
|
|
5861
6755
|
}
|
|
5862
|
-
return `${head} \u2014
|
|
6756
|
+
return `${head} \u2014 no bound cross-model review`;
|
|
5863
6757
|
}
|
|
5864
6758
|
function candidateLine(u, now) {
|
|
5865
6759
|
const when = relAge(u.lastCommitAt, now);
|
|
5866
6760
|
const cite = u.reviews.map((r) => `${r.sessionId.slice(0, 14)}${r.examinedDiff ? "(diff)" : ""}`).join(", ");
|
|
5867
|
-
return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014
|
|
6761
|
+
return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}`;
|
|
5868
6762
|
}
|
|
5869
6763
|
function renderReviewGaps(summary) {
|
|
5870
6764
|
const now = new Date(summary.generatedAt);
|
|
5871
6765
|
const lines = [];
|
|
5872
|
-
const scope = summary.scope ? summary.scope.join(", ") : "
|
|
5873
|
-
lines.push(`#
|
|
6766
|
+
const scope = summary.scope ? summary.scope.join(", ") : "all repositories";
|
|
6767
|
+
lines.push(`# Review-trail gaps (${scope})`);
|
|
5874
6768
|
lines.push("");
|
|
5875
6769
|
if (summary.gaps.length === 0) {
|
|
5876
|
-
lines.push("\u2705
|
|
6770
|
+
lines.push("\u2705 Within the captured range, no unit of work landed without a review trail.");
|
|
5877
6771
|
} else {
|
|
5878
|
-
lines.push(`\u26A0\uFE0F
|
|
6772
|
+
lines.push(`\u26A0\uFE0F Units of work that landed without a review trail: ${summary.gaps.length}`);
|
|
5879
6773
|
for (const u of summary.gaps) lines.push(unitLine(u, now));
|
|
5880
6774
|
}
|
|
5881
6775
|
lines.push("");
|
|
5882
6776
|
if (summary.candidates.length > 0) {
|
|
5883
6777
|
lines.push(
|
|
5884
|
-
`##
|
|
6778
|
+
`## To confirm (${summary.candidates.length}) \u2014 a cross-model review trace exists; confirm it actually examined this change`
|
|
5885
6779
|
);
|
|
5886
6780
|
for (const u of summary.candidates) lines.push(candidateLine(u, now));
|
|
5887
6781
|
lines.push("");
|
|
@@ -5889,19 +6783,19 @@ function renderReviewGaps(summary) {
|
|
|
5889
6783
|
if (summary.unknowns.length > 0) {
|
|
5890
6784
|
const n = summary.unknowns.reduce((sum, u) => sum + u.commitCount, 0);
|
|
5891
6785
|
lines.push(
|
|
5892
|
-
`##
|
|
6786
|
+
`## Undeterminable (${summary.unknowns.length} unit${summary.unknowns.length === 1 ? "" : "s"} / ${n} commit${n === 1 ? "" : "s"}) \u2014 repo or timestamp could not be derived from capture; verdict withheld (not a clear)`
|
|
5893
6787
|
);
|
|
5894
6788
|
lines.push("");
|
|
5895
6789
|
}
|
|
5896
|
-
lines.push("##
|
|
6790
|
+
lines.push("## By repository");
|
|
5897
6791
|
for (const r of summary.repos) {
|
|
5898
6792
|
lines.push(
|
|
5899
|
-
`- ${r.repo}: ${r.units}
|
|
6793
|
+
`- ${r.repo}: ${r.units} unit${r.units === 1 ? "" : "s"} (no trail ${r.omissionUnits} / nearby only ${r.nearUnboundUnits} / to confirm ${r.candidateUnits}${r.unknownUnits > 0 ? ` / unknown ${r.unknownUnits}` : ""})`
|
|
5900
6794
|
);
|
|
5901
6795
|
}
|
|
5902
6796
|
lines.push("");
|
|
5903
6797
|
lines.push(
|
|
5904
|
-
|
|
6798
|
+
`Note: read-only advisory. Only captured commits are in scope (newest captured commit: ${summary.newestCommitAt === null ? "none" : relAge(summary.newestCommitAt, now)}). It never auto-judges that a review "happened", and temporal proximity alone is not a pass. It does not enforce.`
|
|
5905
6799
|
);
|
|
5906
6800
|
return lines.join("\n");
|
|
5907
6801
|
}
|
|
@@ -5925,7 +6819,7 @@ import {
|
|
|
5925
6819
|
readManifest as readManifest7,
|
|
5926
6820
|
readYamlFile as readYamlFile6,
|
|
5927
6821
|
resolveClaudeCodeCommand,
|
|
5928
|
-
resolveRepositoryRoot as
|
|
6822
|
+
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
5929
6823
|
SessionSchema as SessionSchema2,
|
|
5930
6824
|
sanitizeRelatedFiles,
|
|
5931
6825
|
sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
|
|
@@ -6298,7 +7192,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
6298
7192
|
}
|
|
6299
7193
|
async function resolveRepositoryRootForRun(cwd) {
|
|
6300
7194
|
try {
|
|
6301
|
-
return await
|
|
7195
|
+
return await resolveRepositoryRoot10(cwd);
|
|
6302
7196
|
} catch (error) {
|
|
6303
7197
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
6304
7198
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
|
|
@@ -6980,7 +7874,7 @@ import {
|
|
|
6980
7874
|
basouPaths as basouPaths16,
|
|
6981
7875
|
computeWorkStats,
|
|
6982
7876
|
findErrorCode as findErrorCode12,
|
|
6983
|
-
resolveRepositoryRoot as
|
|
7877
|
+
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
6984
7878
|
} from "@basou/core";
|
|
6985
7879
|
function registerStatsCommand(program) {
|
|
6986
7880
|
program.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
@@ -7083,7 +7977,7 @@ function formatInt(n) {
|
|
|
7083
7977
|
}
|
|
7084
7978
|
async function resolveRepositoryRootForStats(cwd) {
|
|
7085
7979
|
try {
|
|
7086
|
-
return await
|
|
7980
|
+
return await resolveRepositoryRoot11(cwd);
|
|
7087
7981
|
} catch (error) {
|
|
7088
7982
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
7089
7983
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
|
|
@@ -7111,7 +8005,7 @@ import {
|
|
|
7111
8005
|
buildStatusSnapshot,
|
|
7112
8006
|
findErrorCode as findErrorCode13,
|
|
7113
8007
|
readManifest as readManifest9,
|
|
7114
|
-
resolveRepositoryRoot as
|
|
8008
|
+
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
7115
8009
|
writeStatus
|
|
7116
8010
|
} from "@basou/core";
|
|
7117
8011
|
function registerStatusCommand(program) {
|
|
@@ -7167,7 +8061,7 @@ function renderTextStatus(s) {
|
|
|
7167
8061
|
}
|
|
7168
8062
|
async function resolveRepositoryRootForStatus(cwd) {
|
|
7169
8063
|
try {
|
|
7170
|
-
return await
|
|
8064
|
+
return await resolveRepositoryRoot12(cwd);
|
|
7171
8065
|
} catch (error) {
|
|
7172
8066
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
7173
8067
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
|
|
@@ -8304,7 +9198,7 @@ import {
|
|
|
8304
9198
|
basouPaths as basouPaths19,
|
|
8305
9199
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
8306
9200
|
findErrorCode as findErrorCode15,
|
|
8307
|
-
resolveRepositoryRoot as
|
|
9201
|
+
resolveRepositoryRoot as resolveRepositoryRoot13,
|
|
8308
9202
|
resolveSessionId as resolveSessionId5,
|
|
8309
9203
|
verifyEventsChain
|
|
8310
9204
|
} from "@basou/core";
|
|
@@ -8375,7 +9269,7 @@ function renderVerdict(row) {
|
|
|
8375
9269
|
}
|
|
8376
9270
|
async function resolveRepositoryRootForVerify(cwd) {
|
|
8377
9271
|
try {
|
|
8378
|
-
return await
|
|
9272
|
+
return await resolveRepositoryRoot13(cwd);
|
|
8379
9273
|
} catch (error) {
|
|
8380
9274
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
8381
9275
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
@@ -8405,7 +9299,7 @@ import {
|
|
|
8405
9299
|
basouPaths as basouPaths20,
|
|
8406
9300
|
findErrorCode as findErrorCode17,
|
|
8407
9301
|
readManifest as readManifest13,
|
|
8408
|
-
resolveRepositoryRoot as
|
|
9302
|
+
resolveRepositoryRoot as resolveRepositoryRoot14
|
|
8409
9303
|
} from "@basou/core";
|
|
8410
9304
|
import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
8411
9305
|
|
|
@@ -9821,7 +10715,7 @@ function waitForShutdown(signal) {
|
|
|
9821
10715
|
}
|
|
9822
10716
|
async function resolveRepositoryRootForView(cwd) {
|
|
9823
10717
|
try {
|
|
9824
|
-
return await
|
|
10718
|
+
return await resolveRepositoryRoot14(cwd);
|
|
9825
10719
|
} catch (error) {
|
|
9826
10720
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
9827
10721
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|