@basou/cli 0.25.0 → 0.26.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 +807 -162
- package/dist/index.js.map +1 -1
- package/dist/program.js +807 -162
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -3364,9 +3364,13 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
3364
3364
|
|
|
3365
3365
|
// src/commands/project.ts
|
|
3366
3366
|
import {
|
|
3367
|
+
closeSync,
|
|
3367
3368
|
existsSync,
|
|
3369
|
+
constants as fsConstants,
|
|
3370
|
+
ftruncateSync,
|
|
3368
3371
|
lstatSync,
|
|
3369
3372
|
mkdirSync,
|
|
3373
|
+
openSync,
|
|
3370
3374
|
readdirSync,
|
|
3371
3375
|
readFileSync,
|
|
3372
3376
|
readlinkSync,
|
|
@@ -3374,11 +3378,15 @@ import {
|
|
|
3374
3378
|
statSync,
|
|
3375
3379
|
symlinkSync,
|
|
3376
3380
|
unlinkSync,
|
|
3377
|
-
writeFileSync
|
|
3381
|
+
writeFileSync,
|
|
3382
|
+
writeSync
|
|
3378
3383
|
} from "fs";
|
|
3379
3384
|
import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve7 } from "path";
|
|
3380
3385
|
import {
|
|
3386
|
+
appendBasouGitignore as appendBasouGitignore2,
|
|
3381
3387
|
basouPaths as basouPaths10,
|
|
3388
|
+
createManifest as createManifest2,
|
|
3389
|
+
ensureBasouDirectory as ensureBasouDirectory2,
|
|
3382
3390
|
GENERATED_END,
|
|
3383
3391
|
GENERATED_START,
|
|
3384
3392
|
isGitNotFound,
|
|
@@ -3392,7 +3400,9 @@ import {
|
|
|
3392
3400
|
readManifest as readManifest6,
|
|
3393
3401
|
readMarkdownFile as readMarkdownFile4,
|
|
3394
3402
|
reconcileSourceRoots,
|
|
3403
|
+
removeMarkerSection,
|
|
3395
3404
|
renderWithMarkers as renderWithMarkers4,
|
|
3405
|
+
resolveRepositoryRoot as resolveRepositoryRoot8,
|
|
3396
3406
|
safeSimpleGit,
|
|
3397
3407
|
summarizePresetPlan,
|
|
3398
3408
|
summarizeRosterDrift,
|
|
@@ -3474,6 +3484,27 @@ function registerProjectCommand(program) {
|
|
|
3474
3484
|
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (oldPath, newPath, opts) => {
|
|
3475
3485
|
await runProjectRename(oldPath, newPath, opts);
|
|
3476
3486
|
});
|
|
3487
|
+
project.command("teardown").argument("<repo>", "The repo path whose basou-generated wiring to tear down (e.g. ../takuhon)").description(
|
|
3488
|
+
"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"
|
|
3489
|
+
).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) => {
|
|
3490
|
+
await runProjectTeardown(repo, opts);
|
|
3491
|
+
});
|
|
3492
|
+
project.command("new").argument("[repos...]", "Extra repo paths (besides the anchor) to seed into the roster").description(
|
|
3493
|
+
"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"
|
|
3494
|
+
).option("--apply", "Create `.basou/` and write the seeded manifest (default: dry-run preview)").option(
|
|
3495
|
+
"--view <path>",
|
|
3496
|
+
"Override the workspace view path (default: a <name>-workspace sibling)"
|
|
3497
|
+
).option("--no-view", "Solo project: declare no workspace view").option(
|
|
3498
|
+
"--local-only",
|
|
3499
|
+
"Write a .basou/ full-exclude .gitignore block (keep the trail out of version control) instead of the default ignore+commit block"
|
|
3500
|
+
).option("-f, --force", "Overwrite an existing manifest").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repos, opts) => {
|
|
3501
|
+
await runProjectNew(repos, opts);
|
|
3502
|
+
});
|
|
3503
|
+
project.command("derive").description(
|
|
3504
|
+
"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"
|
|
3505
|
+
).option("--apply", "Run every step in apply mode (default: dry-run preview)").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
3506
|
+
await runProjectDerive(opts);
|
|
3507
|
+
});
|
|
3477
3508
|
}
|
|
3478
3509
|
async function runProjectCheck(options, ctx = {}) {
|
|
3479
3510
|
try {
|
|
@@ -3489,7 +3520,7 @@ function effectiveSourceRoots(manifest) {
|
|
|
3489
3520
|
function preservedUnknownLines(fields) {
|
|
3490
3521
|
if (fields.length === 0) return [];
|
|
3491
3522
|
return [
|
|
3492
|
-
`\u2139\uFE0F
|
|
3523
|
+
`\u2139\uFE0F Preserving ${fields.length} unrecognized top-level manifest field${fields.length === 1 ? "" : "s"} (kept on write, never dropped): ${fields.join(", ")}`,
|
|
3493
3524
|
""
|
|
3494
3525
|
];
|
|
3495
3526
|
}
|
|
@@ -3511,39 +3542,41 @@ async function doRunProjectCheck(options, ctx) {
|
|
|
3511
3542
|
}
|
|
3512
3543
|
function renderProjectCheck(summary) {
|
|
3513
3544
|
const lines = [];
|
|
3514
|
-
lines.push("#
|
|
3545
|
+
lines.push("# Project composition check (declared vs captured)");
|
|
3515
3546
|
lines.push("");
|
|
3516
3547
|
if (summary.declaredCount === 0) {
|
|
3517
3548
|
lines.push(
|
|
3518
|
-
"\u2139\uFE0F repo
|
|
3549
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Running on `source_roots` alone, so there is nothing to compare the declaration against."
|
|
3519
3550
|
);
|
|
3520
3551
|
if (summary.extra.length > 0) {
|
|
3521
3552
|
lines.push("");
|
|
3522
|
-
lines.push(
|
|
3553
|
+
lines.push(`Captured source_roots (${summary.extra.length}):`);
|
|
3523
3554
|
for (const p of summary.extra) lines.push(`- ${p}`);
|
|
3524
3555
|
}
|
|
3525
3556
|
return lines.join("\n");
|
|
3526
3557
|
}
|
|
3527
3558
|
if (summary.gaps.length === 0) {
|
|
3528
3559
|
lines.push(
|
|
3529
|
-
`\u2705
|
|
3560
|
+
`\u2705 All ${summary.declaredCount} declared repo${summary.declaredCount === 1 ? " is" : "s are"} covered by the capture config (source_roots).`
|
|
3530
3561
|
);
|
|
3531
3562
|
} else {
|
|
3532
|
-
lines.push(
|
|
3563
|
+
lines.push(
|
|
3564
|
+
`\u26A0\uFE0F Declared but not captured: ${summary.gaps.length} repo${summary.gaps.length === 1 ? "" : "s"}`
|
|
3565
|
+
);
|
|
3533
3566
|
for (const g of summary.gaps) {
|
|
3534
|
-
lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 source_roots
|
|
3567
|
+
lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 not in source_roots`);
|
|
3535
3568
|
}
|
|
3536
3569
|
}
|
|
3537
3570
|
lines.push("");
|
|
3538
3571
|
if (summary.extra.length > 0) {
|
|
3539
3572
|
lines.push(
|
|
3540
|
-
`##
|
|
3573
|
+
`## Captured but undeclared (${summary.extra.length}) \u2014 the workspace view, or a missing declaration`
|
|
3541
3574
|
);
|
|
3542
3575
|
for (const p of summary.extra) lines.push(`- ${p}`);
|
|
3543
3576
|
lines.push("");
|
|
3544
3577
|
}
|
|
3545
3578
|
lines.push(
|
|
3546
|
-
"
|
|
3579
|
+
"Note: read-only advisory. It only shows the difference between the declaration (repos) and the capture config (source_roots); it does not enforce."
|
|
3547
3580
|
);
|
|
3548
3581
|
return lines.join("\n");
|
|
3549
3582
|
}
|
|
@@ -3593,29 +3626,33 @@ async function doRunProjectSync(options, ctx) {
|
|
|
3593
3626
|
}
|
|
3594
3627
|
function renderProjectSync(result) {
|
|
3595
3628
|
const lines = [];
|
|
3596
|
-
lines.push("# source_roots
|
|
3629
|
+
lines.push("# source_roots sync (declared roster \u2192 capture config)");
|
|
3597
3630
|
lines.push("");
|
|
3598
3631
|
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
3599
3632
|
if (!result.hasRoster) {
|
|
3600
3633
|
lines.push(
|
|
3601
|
-
"\u2139\uFE0F repo
|
|
3634
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). There is no declaration to sync from, so nothing changes."
|
|
3602
3635
|
);
|
|
3603
3636
|
return lines.join("\n");
|
|
3604
3637
|
}
|
|
3605
3638
|
if (result.unchanged) {
|
|
3606
|
-
lines.push("\u2705 source_roots
|
|
3639
|
+
lines.push("\u2705 source_roots already covers the entire declared roster (nothing to sync).");
|
|
3607
3640
|
return lines.join("\n");
|
|
3608
3641
|
}
|
|
3609
3642
|
if (result.applied) {
|
|
3610
|
-
lines.push(
|
|
3643
|
+
lines.push(
|
|
3644
|
+
`\u2705 Added ${result.added.length} entr${result.added.length === 1 ? "y" : "ies"} to source_roots:`
|
|
3645
|
+
);
|
|
3611
3646
|
for (const p of result.added) lines.push(`- ${p}`);
|
|
3612
3647
|
} else {
|
|
3613
3648
|
lines.push(
|
|
3614
|
-
`${result.added.length}
|
|
3649
|
+
`${result.added.length} repo${result.added.length === 1 ? " is" : "s are"} not in source_roots. To add (dry-run; pass --apply to write):`
|
|
3615
3650
|
);
|
|
3616
3651
|
for (const p of result.added) lines.push(`- ${p}`);
|
|
3617
3652
|
lines.push("");
|
|
3618
|
-
lines.push(
|
|
3653
|
+
lines.push(
|
|
3654
|
+
"Note: existing source_roots are kept; only the missing entries are appended (nothing is removed)."
|
|
3655
|
+
);
|
|
3619
3656
|
}
|
|
3620
3657
|
return lines.join("\n");
|
|
3621
3658
|
}
|
|
@@ -3675,37 +3712,41 @@ async function doRunProjectAdopt(options, ctx) {
|
|
|
3675
3712
|
}
|
|
3676
3713
|
function renderProjectAdopt(result) {
|
|
3677
3714
|
const lines = [];
|
|
3678
|
-
lines.push("# repo
|
|
3715
|
+
lines.push("# Bootstrap repo roster (source_roots \u2192 repos)");
|
|
3679
3716
|
lines.push("");
|
|
3680
3717
|
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
3681
3718
|
if (result.alreadyDeclared) {
|
|
3682
3719
|
lines.push(
|
|
3683
|
-
"\u2139\uFE0F repo
|
|
3720
|
+
"\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
3721
|
);
|
|
3685
3722
|
return lines.join("\n");
|
|
3686
3723
|
}
|
|
3687
3724
|
if (result.repos.length === 0) {
|
|
3688
|
-
lines.push("\u2139\uFE0F
|
|
3725
|
+
lines.push("\u2139\uFE0F No git repo found in source_roots (nothing to bootstrap).");
|
|
3689
3726
|
} else if (result.applied) {
|
|
3690
|
-
lines.push(
|
|
3727
|
+
lines.push(
|
|
3728
|
+
`\u2705 Wrote ${result.repos.length} repo${result.repos.length === 1 ? "" : "s"} to the repos roster:`
|
|
3729
|
+
);
|
|
3691
3730
|
for (const r of result.repos) lines.push(`- ${r.path}`);
|
|
3692
3731
|
lines.push("");
|
|
3693
3732
|
lines.push(
|
|
3694
|
-
"
|
|
3733
|
+
"Note: visibility is unset. Assign public / private / future-public to each repo manually."
|
|
3695
3734
|
);
|
|
3696
3735
|
} else {
|
|
3697
3736
|
lines.push(
|
|
3698
|
-
`${result.repos.length} repo
|
|
3737
|
+
`${result.repos.length} repo${result.repos.length === 1 ? "" : "s"} to declare in the repos roster (dry-run; pass --apply to write):`
|
|
3699
3738
|
);
|
|
3700
3739
|
for (const r of result.repos) lines.push(`- ${r.path}`);
|
|
3701
3740
|
lines.push("");
|
|
3702
|
-
lines.push("
|
|
3741
|
+
lines.push("Note: visibility is proposed unset; assign it manually after applying.");
|
|
3703
3742
|
}
|
|
3704
3743
|
if (result.excluded.length > 0) {
|
|
3705
3744
|
lines.push("");
|
|
3706
|
-
lines.push(
|
|
3745
|
+
lines.push(
|
|
3746
|
+
`## Excluded (${result.excluded.length}) \u2014 not a git repo, so not included in repos`
|
|
3747
|
+
);
|
|
3707
3748
|
for (const e of result.excluded) {
|
|
3708
|
-
const reason = e.kind === "non-repo" ? "
|
|
3749
|
+
const reason = e.kind === "non-repo" ? "not a repo (workspace view / tmp, etc.)" : "unresolvable (path does not exist)";
|
|
3709
3750
|
lines.push(`- ${e.path} \u2014 ${reason}`);
|
|
3710
3751
|
}
|
|
3711
3752
|
}
|
|
@@ -3773,50 +3814,56 @@ async function doRunProjectWiring(options, ctx) {
|
|
|
3773
3814
|
}
|
|
3774
3815
|
function renderProjectWiring(result) {
|
|
3775
3816
|
const lines = [];
|
|
3776
|
-
lines.push(
|
|
3817
|
+
lines.push(
|
|
3818
|
+
"# Instruction-file wiring check (declared roster \xD7 instruction-file presence / git tracking)"
|
|
3819
|
+
);
|
|
3777
3820
|
lines.push("");
|
|
3778
3821
|
if (!result.hasRoster) {
|
|
3779
3822
|
lines.push(
|
|
3780
|
-
"\u2139\uFE0F repo
|
|
3823
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
|
|
3781
3824
|
);
|
|
3782
3825
|
return lines.join("\n");
|
|
3783
3826
|
}
|
|
3784
3827
|
if (result.risks.length > 0) {
|
|
3785
3828
|
lines.push(
|
|
3786
|
-
`\u26A0\uFE0F
|
|
3829
|
+
`\u26A0\uFE0F Instruction files tracked by git in public-facing repos: ${result.risks.length} (canonical leak risk)`
|
|
3787
3830
|
);
|
|
3788
3831
|
for (const r of result.risks) {
|
|
3789
3832
|
lines.push(
|
|
3790
|
-
`- ${r.repo} [${r.visibility}] \u2014 ${r.file}
|
|
3833
|
+
`- ${r.repo} [${r.visibility}] \u2014 ${r.file} is tracked (it should be a gitignored symlink)`
|
|
3791
3834
|
);
|
|
3792
3835
|
}
|
|
3793
3836
|
} else if (result.ok) {
|
|
3794
|
-
lines.push(
|
|
3837
|
+
lines.push(
|
|
3838
|
+
"\u2705 No instruction file is tracked by git in a public-facing repo (no privacy risk)."
|
|
3839
|
+
);
|
|
3795
3840
|
} else {
|
|
3796
3841
|
lines.push(
|
|
3797
|
-
"\u2139\uFE0F
|
|
3842
|
+
"\u2139\uFE0F No confirmed privacy risk, but some repos are unjudgeable / unreachable (see below)."
|
|
3798
3843
|
);
|
|
3799
3844
|
}
|
|
3800
3845
|
lines.push("");
|
|
3801
3846
|
if (result.unknown.length > 0) {
|
|
3802
3847
|
lines.push(
|
|
3803
|
-
`##
|
|
3848
|
+
`## Visibility unset (${result.unknown.length}) \u2014 privacy cannot be judged. Assign visibility in the manifest repos`
|
|
3804
3849
|
);
|
|
3805
3850
|
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
3806
3851
|
lines.push("");
|
|
3807
3852
|
}
|
|
3808
3853
|
if (result.incomplete.length > 0) {
|
|
3809
|
-
lines.push(
|
|
3854
|
+
lines.push(
|
|
3855
|
+
`## Missing instruction files (${result.incomplete.length}) \u2014 to be filled by a later generation slice`
|
|
3856
|
+
);
|
|
3810
3857
|
for (const i of result.incomplete) lines.push(`- ${i.repo} \u2014 ${i.missing.join(", ")}`);
|
|
3811
3858
|
lines.push("");
|
|
3812
3859
|
}
|
|
3813
3860
|
if (result.unreachable.length > 0) {
|
|
3814
|
-
lines.push(`##
|
|
3861
|
+
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
3815
3862
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
3816
3863
|
lines.push("");
|
|
3817
3864
|
}
|
|
3818
3865
|
lines.push(
|
|
3819
|
-
"
|
|
3866
|
+
"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
3867
|
);
|
|
3821
3868
|
return lines.join("\n");
|
|
3822
3869
|
}
|
|
@@ -3895,45 +3942,47 @@ async function doRunProjectGitignore(options, ctx) {
|
|
|
3895
3942
|
}
|
|
3896
3943
|
function renderProjectGitignore(result) {
|
|
3897
3944
|
const lines = [];
|
|
3898
|
-
lines.push("# .gitignore
|
|
3945
|
+
lines.push("# .gitignore generation (exclude instruction files in public-facing repos)");
|
|
3899
3946
|
lines.push("");
|
|
3900
3947
|
if (!result.hasRoster) {
|
|
3901
3948
|
lines.push(
|
|
3902
|
-
"\u2139\uFE0F repo
|
|
3949
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
|
|
3903
3950
|
);
|
|
3904
3951
|
return lines.join("\n");
|
|
3905
3952
|
}
|
|
3906
3953
|
if (result.plans.length > 0) {
|
|
3907
|
-
const verb = result.applied ? "
|
|
3954
|
+
const verb = result.applied ? "Added to" : "To add to (dry-run; pass --apply to write)";
|
|
3908
3955
|
lines.push(
|
|
3909
|
-
`${result.applied ? "\u2705 " : ""}${result.plans.length} repo
|
|
3956
|
+
`${result.applied ? "\u2705 " : ""}${verb} the .gitignore of ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"}:`
|
|
3910
3957
|
);
|
|
3911
3958
|
for (const p of result.plans) lines.push(`- ${p.path} \u2014 ${p.toAdd.join(", ")}`);
|
|
3912
3959
|
} else if (result.ok) {
|
|
3913
|
-
lines.push(
|
|
3960
|
+
lines.push(
|
|
3961
|
+
"\u2705 Public-facing repos already exclude every instruction file in .gitignore (nothing to add)."
|
|
3962
|
+
);
|
|
3914
3963
|
} else {
|
|
3915
3964
|
lines.push(
|
|
3916
|
-
"\u2139\uFE0F
|
|
3965
|
+
"\u2139\uFE0F No public-facing repo needs an addition, but some repos are unjudgeable / unreachable (see below)."
|
|
3917
3966
|
);
|
|
3918
3967
|
}
|
|
3919
3968
|
lines.push("");
|
|
3920
3969
|
if (result.unknown.length > 0) {
|
|
3921
3970
|
lines.push(
|
|
3922
|
-
`##
|
|
3971
|
+
`## Visibility unset (${result.unknown.length}) \u2014 skipped. Assign visibility in the manifest repos`
|
|
3923
3972
|
);
|
|
3924
3973
|
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
3925
3974
|
lines.push("");
|
|
3926
3975
|
}
|
|
3927
3976
|
if (result.unreachable.length > 0) {
|
|
3928
|
-
lines.push(`##
|
|
3977
|
+
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
3929
3978
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
3930
3979
|
lines.push("");
|
|
3931
3980
|
}
|
|
3932
3981
|
lines.push(
|
|
3933
|
-
"
|
|
3982
|
+
"Note: existing .gitignore lines are kept; only the missing patterns are appended (nothing is removed). private / visibility-unset repos are skipped."
|
|
3934
3983
|
);
|
|
3935
3984
|
lines.push(
|
|
3936
|
-
"
|
|
3985
|
+
"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
3986
|
);
|
|
3938
3987
|
return lines.join("\n");
|
|
3939
3988
|
}
|
|
@@ -4061,11 +4110,11 @@ async function doRunProjectSymlinks(options, ctx) {
|
|
|
4061
4110
|
}
|
|
4062
4111
|
function renderProjectSymlinks(result) {
|
|
4063
4112
|
const lines = [];
|
|
4064
|
-
lines.push("#
|
|
4113
|
+
lines.push("# Instruction-file symlink generation (each repo \u2192 the anchor's canonical)");
|
|
4065
4114
|
lines.push("");
|
|
4066
4115
|
if (!result.hasRoster) {
|
|
4067
4116
|
lines.push(
|
|
4068
|
-
"\u2139\uFE0F repo
|
|
4117
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
|
|
4069
4118
|
);
|
|
4070
4119
|
return lines.join("\n");
|
|
4071
4120
|
}
|
|
@@ -4073,14 +4122,14 @@ function renderProjectSymlinks(result) {
|
|
|
4073
4122
|
const attempted = result.applied || result.failures.length > 0;
|
|
4074
4123
|
if (!attempted) {
|
|
4075
4124
|
lines.push(
|
|
4076
|
-
|
|
4125
|
+
`Instruction-file symlinks to create in ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"} (dry-run; pass --apply to write):`
|
|
4077
4126
|
);
|
|
4078
4127
|
for (const p of result.plans) {
|
|
4079
4128
|
lines.push(`- ${p.path}`);
|
|
4080
4129
|
for (const c of p.toCreate) lines.push(` ${c.name} -> ${c.target}`);
|
|
4081
4130
|
}
|
|
4082
4131
|
} else {
|
|
4083
|
-
const header = result.failures.length === 0 ? "\u2705
|
|
4132
|
+
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
4133
|
lines.push(header);
|
|
4085
4134
|
for (const p of result.plans) {
|
|
4086
4135
|
const failedFiles = new Set(
|
|
@@ -4093,31 +4142,35 @@ function renderProjectSymlinks(result) {
|
|
|
4093
4142
|
}
|
|
4094
4143
|
}
|
|
4095
4144
|
} else if (result.ok) {
|
|
4096
|
-
lines.push(
|
|
4145
|
+
lines.push(
|
|
4146
|
+
"\u2705 Every declared repo's instruction-file symlinks are correctly wired (nothing to generate)."
|
|
4147
|
+
);
|
|
4097
4148
|
} else {
|
|
4098
4149
|
lines.push(
|
|
4099
|
-
"\u2139\uFE0F
|
|
4150
|
+
"\u2139\uFE0F No symlink needs generating, but there are conflicts / collisions / a missing canonical / unreachable repos (see below)."
|
|
4100
4151
|
);
|
|
4101
4152
|
}
|
|
4102
4153
|
lines.push("");
|
|
4103
4154
|
if (result.failures.length > 0) {
|
|
4104
|
-
lines.push(
|
|
4155
|
+
lines.push(
|
|
4156
|
+
`## Creation failed (${result.failures.length}) \u2014 some symlinks could not be created`
|
|
4157
|
+
);
|
|
4105
4158
|
for (const f of result.failures) lines.push(`- ${f.repo} \u2014 ${f.file}: ${f.message}`);
|
|
4106
4159
|
lines.push("");
|
|
4107
4160
|
}
|
|
4108
4161
|
if (result.conflicts.length > 0) {
|
|
4109
4162
|
lines.push(
|
|
4110
|
-
`##
|
|
4163
|
+
`## Conflicts (${result.conflicts.length}) \u2014 existing entries are not overwritten. Check them manually`
|
|
4111
4164
|
);
|
|
4112
4165
|
for (const c of result.conflicts) {
|
|
4113
|
-
const detail = c.reason === "mismatch" ?
|
|
4166
|
+
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
4167
|
lines.push(`- ${c.repo} \u2014 ${c.file}: ${detail}`);
|
|
4115
4168
|
}
|
|
4116
4169
|
lines.push("");
|
|
4117
4170
|
}
|
|
4118
4171
|
if (result.collisions.length > 0) {
|
|
4119
4172
|
lines.push(
|
|
4120
|
-
`##
|
|
4173
|
+
`## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-wired)`
|
|
4121
4174
|
);
|
|
4122
4175
|
for (const c of result.collisions) {
|
|
4123
4176
|
lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
|
|
@@ -4126,18 +4179,18 @@ function renderProjectSymlinks(result) {
|
|
|
4126
4179
|
}
|
|
4127
4180
|
if (result.missingCanonical.length > 0) {
|
|
4128
4181
|
lines.push(
|
|
4129
|
-
`##
|
|
4182
|
+
`## Canonical missing (${result.missingCanonical.length}) \u2014 the anchor has no agents/<repo>/AGENTS.md, so nothing can be generated`
|
|
4130
4183
|
);
|
|
4131
4184
|
for (const p of result.missingCanonical) lines.push(`- ${p}`);
|
|
4132
4185
|
lines.push("");
|
|
4133
4186
|
}
|
|
4134
4187
|
if (result.unreachable.length > 0) {
|
|
4135
|
-
lines.push(`##
|
|
4188
|
+
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
4136
4189
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
4137
4190
|
lines.push("");
|
|
4138
4191
|
}
|
|
4139
4192
|
lines.push(
|
|
4140
|
-
"
|
|
4193
|
+
"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
4194
|
);
|
|
4142
4195
|
return lines.join("\n");
|
|
4143
4196
|
}
|
|
@@ -4239,7 +4292,7 @@ function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
|
4239
4292
|
names = readdirSync(viewDir);
|
|
4240
4293
|
} catch (error) {
|
|
4241
4294
|
if (hasErrorCode(error) && error.code === "ENOENT") return [];
|
|
4242
|
-
throw new Error("workspace view
|
|
4295
|
+
throw new Error("Cannot scan the workspace view (check the path / its type)", {
|
|
4243
4296
|
cause: error
|
|
4244
4297
|
});
|
|
4245
4298
|
}
|
|
@@ -4261,7 +4314,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
4261
4314
|
if (c === null || c.kind !== "repo") {
|
|
4262
4315
|
failed.push({
|
|
4263
4316
|
name,
|
|
4264
|
-
message: "
|
|
4317
|
+
message: "the target changed since the scan (no longer a basou-generated stray repo link; re-run)"
|
|
4265
4318
|
});
|
|
4266
4319
|
continue;
|
|
4267
4320
|
}
|
|
@@ -4350,11 +4403,11 @@ async function doRunProjectWorkspace(options, ctx) {
|
|
|
4350
4403
|
}
|
|
4351
4404
|
function renderProjectWorkspace(result) {
|
|
4352
4405
|
const lines = [];
|
|
4353
|
-
lines.push("# workspace view
|
|
4406
|
+
lines.push("# workspace view generation (aggregate the roster repos)");
|
|
4354
4407
|
lines.push("");
|
|
4355
4408
|
if (!result.hasView) {
|
|
4356
4409
|
lines.push(
|
|
4357
|
-
"\u2139\uFE0F view
|
|
4410
|
+
"\u2139\uFE0F No view declared (manifest `workspace.view`). Declare the aggregation directory, then re-run."
|
|
4358
4411
|
);
|
|
4359
4412
|
return lines.join("\n");
|
|
4360
4413
|
}
|
|
@@ -4362,12 +4415,12 @@ function renderProjectWorkspace(result) {
|
|
|
4362
4415
|
const attempted = result.applied || result.failures.length > 0;
|
|
4363
4416
|
if (!attempted) {
|
|
4364
4417
|
lines.push(
|
|
4365
|
-
|
|
4418
|
+
`Repo symlinks to create in the view: ${result.toCreate.length} (dry-run; pass --apply to write):`
|
|
4366
4419
|
);
|
|
4367
4420
|
for (const c of result.toCreate) lines.push(` ${c.name} -> ${c.target}`);
|
|
4368
4421
|
} else {
|
|
4369
4422
|
const failed = new Set(result.failures.map((f) => f.name));
|
|
4370
|
-
const header = result.failures.length === 0 ? "\u2705
|
|
4423
|
+
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
4424
|
lines.push(header);
|
|
4372
4425
|
for (const c of result.toCreate) {
|
|
4373
4426
|
if (failed.has(c.name)) continue;
|
|
@@ -4376,16 +4429,18 @@ function renderProjectWorkspace(result) {
|
|
|
4376
4429
|
}
|
|
4377
4430
|
} else if (result.ok) {
|
|
4378
4431
|
lines.push(
|
|
4379
|
-
`\u2705 view
|
|
4432
|
+
`\u2705 The view aggregates the entire declared roster (${result.correctCount} links; nothing to generate).`
|
|
4380
4433
|
);
|
|
4381
4434
|
} else {
|
|
4382
4435
|
lines.push(
|
|
4383
|
-
"\u2139\uFE0F
|
|
4436
|
+
"\u2139\uFE0F No symlink needs creating, but there are items needing attention (stray / conflict / collision / unreachable repo, see below)."
|
|
4384
4437
|
);
|
|
4385
4438
|
}
|
|
4386
4439
|
lines.push("");
|
|
4387
4440
|
if (result.failures.length > 0) {
|
|
4388
|
-
lines.push(
|
|
4441
|
+
lines.push(
|
|
4442
|
+
`## Creation failed (${result.failures.length}) \u2014 some symlinks could not be created`
|
|
4443
|
+
);
|
|
4389
4444
|
for (const f of result.failures) lines.push(`- ${f.name}: ${f.message}`);
|
|
4390
4445
|
lines.push("");
|
|
4391
4446
|
}
|
|
@@ -4393,17 +4448,17 @@ function renderProjectWorkspace(result) {
|
|
|
4393
4448
|
const attempted = result.pruned || result.pruneFailures.length > 0;
|
|
4394
4449
|
if (result.pruneWithheld) {
|
|
4395
4450
|
lines.push(
|
|
4396
|
-
`${result.toPrune.length}
|
|
4451
|
+
`${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
4452
|
);
|
|
4398
4453
|
for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
|
|
4399
4454
|
} else if (!attempted) {
|
|
4400
4455
|
lines.push(
|
|
4401
|
-
|
|
4456
|
+
`Stray repo symlinks to prune: ${result.toPrune.length} (dry-run; pass --prune to remove):`
|
|
4402
4457
|
);
|
|
4403
4458
|
for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
|
|
4404
4459
|
} else {
|
|
4405
4460
|
const failed = new Set(result.pruneFailures.map((f) => f.name));
|
|
4406
|
-
const header = result.pruneFailures.length === 0 ? "\u{1F9F9} stray repo
|
|
4461
|
+
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
4462
|
lines.push(header);
|
|
4408
4463
|
for (const p of result.toPrune) {
|
|
4409
4464
|
if (failed.has(p.name)) continue;
|
|
@@ -4414,47 +4469,47 @@ function renderProjectWorkspace(result) {
|
|
|
4414
4469
|
}
|
|
4415
4470
|
if (result.pruneFailures.length > 0) {
|
|
4416
4471
|
lines.push(
|
|
4417
|
-
`##
|
|
4472
|
+
`## Pruning failed (${result.pruneFailures.length}) \u2014 some stray symlinks could not be pruned`
|
|
4418
4473
|
);
|
|
4419
4474
|
for (const f of result.pruneFailures) lines.push(`- ${f.name}: ${f.message}`);
|
|
4420
4475
|
lines.push("");
|
|
4421
4476
|
}
|
|
4422
4477
|
if (result.conflicts.length > 0) {
|
|
4423
4478
|
lines.push(
|
|
4424
|
-
`##
|
|
4479
|
+
`## Conflicts (${result.conflicts.length}) \u2014 existing entries are not overwritten. Check them manually`
|
|
4425
4480
|
);
|
|
4426
4481
|
for (const c of result.conflicts) {
|
|
4427
|
-
const detail = c.reason === "mismatch" ?
|
|
4482
|
+
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
4483
|
lines.push(`- ${c.name}: ${detail}`);
|
|
4429
4484
|
}
|
|
4430
4485
|
lines.push("");
|
|
4431
4486
|
}
|
|
4432
4487
|
if (result.collisions.length > 0) {
|
|
4433
4488
|
lines.push(
|
|
4434
|
-
`##
|
|
4489
|
+
`## Basename collisions (${result.collisions.length}) \u2014 another repo claims the same view name (not auto-wired)`
|
|
4435
4490
|
);
|
|
4436
4491
|
for (const c of result.collisions) lines.push(`- ${c.linkName} \u2190 ${c.repos.join(", ")}`);
|
|
4437
4492
|
lines.push("");
|
|
4438
4493
|
}
|
|
4439
4494
|
if (result.unreachable.length > 0) {
|
|
4440
4495
|
lines.push(
|
|
4441
|
-
`##
|
|
4496
|
+
`## Unreachable (${result.unreachable.length}) \u2014 path unresolved, or it resolves to the view itself, so it cannot be aggregated`
|
|
4442
4497
|
);
|
|
4443
4498
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
4444
4499
|
lines.push("");
|
|
4445
4500
|
}
|
|
4446
4501
|
if (result.strayUnknown.length > 0) {
|
|
4447
4502
|
lines.push(
|
|
4448
|
-
`##
|
|
4503
|
+
`## Strays left in place (${result.strayUnknown.length}) \u2014 not confirmed to be a basou-generated repo link, so not pruned. Check them manually`
|
|
4449
4504
|
);
|
|
4450
4505
|
for (const s of result.strayUnknown) {
|
|
4451
|
-
const detail = s.reason === "broken" ? "
|
|
4506
|
+
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
4507
|
lines.push(`- ${s.name} -> ${s.target}: ${detail}`);
|
|
4453
4508
|
}
|
|
4454
4509
|
lines.push("");
|
|
4455
4510
|
}
|
|
4456
4511
|
lines.push(
|
|
4457
|
-
"
|
|
4512
|
+
"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
4513
|
);
|
|
4459
4514
|
return lines.join("\n");
|
|
4460
4515
|
}
|
|
@@ -4585,15 +4640,17 @@ async function doRunProjectPreset(options, ctx) {
|
|
|
4585
4640
|
return result;
|
|
4586
4641
|
}
|
|
4587
4642
|
function presetActionLabel(action) {
|
|
4588
|
-
return action === "create" ? "
|
|
4643
|
+
return action === "create" ? "create" : "update";
|
|
4589
4644
|
}
|
|
4590
4645
|
function renderProjectPreset(result) {
|
|
4591
4646
|
const lines = [];
|
|
4592
|
-
lines.push(
|
|
4647
|
+
lines.push(
|
|
4648
|
+
"# Instruction-file preset generation (declaration \u2192 the canonical's generated region)"
|
|
4649
|
+
);
|
|
4593
4650
|
lines.push("");
|
|
4594
4651
|
if (!result.hasRoster) {
|
|
4595
4652
|
lines.push(
|
|
4596
|
-
"\u2139\uFE0F repo
|
|
4653
|
+
"\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
|
|
4597
4654
|
);
|
|
4598
4655
|
return lines.join("\n");
|
|
4599
4656
|
}
|
|
@@ -4601,7 +4658,7 @@ function renderProjectPreset(result) {
|
|
|
4601
4658
|
const attempted = result.applied || result.failures.length > 0;
|
|
4602
4659
|
if (!attempted) {
|
|
4603
4660
|
lines.push(
|
|
4604
|
-
|
|
4661
|
+
`Preset blocks to generate in the canonical of ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"} (dry-run; pass --apply to write):`
|
|
4605
4662
|
);
|
|
4606
4663
|
for (const p of result.plans) {
|
|
4607
4664
|
lines.push(
|
|
@@ -4611,7 +4668,7 @@ function renderProjectPreset(result) {
|
|
|
4611
4668
|
}
|
|
4612
4669
|
} else {
|
|
4613
4670
|
const failed = new Set(result.failures.map((f) => f.repo));
|
|
4614
|
-
const header = result.failures.length === 0 ? "\u2705
|
|
4671
|
+
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
4672
|
lines.push(header);
|
|
4616
4673
|
for (const p of result.plans) {
|
|
4617
4674
|
if (failed.has(p.path)) continue;
|
|
@@ -4621,47 +4678,49 @@ function renderProjectPreset(result) {
|
|
|
4621
4678
|
}
|
|
4622
4679
|
}
|
|
4623
4680
|
} else if (result.ok) {
|
|
4624
|
-
lines.push(
|
|
4681
|
+
lines.push(
|
|
4682
|
+
"\u2705 Every declared repo's preset block is in sync with its canonical (nothing to generate)."
|
|
4683
|
+
);
|
|
4625
4684
|
} else {
|
|
4626
4685
|
lines.push(
|
|
4627
|
-
"\u2139\uFE0F
|
|
4686
|
+
"\u2139\uFE0F No repo needs generating, but there are marker conflicts / collisions / undeclared / unreachable repos (see below)."
|
|
4628
4687
|
);
|
|
4629
4688
|
}
|
|
4630
4689
|
lines.push("");
|
|
4631
4690
|
if (result.inSync.length > 0) {
|
|
4632
|
-
lines.push(
|
|
4691
|
+
lines.push(`In sync (${result.inSync.length}): ${result.inSync.join(", ")}`);
|
|
4633
4692
|
lines.push("");
|
|
4634
4693
|
}
|
|
4635
4694
|
if (result.failures.length > 0) {
|
|
4636
4695
|
lines.push(
|
|
4637
|
-
`##
|
|
4696
|
+
`## Write failed (${result.failures.length}) \u2014 some canonicals could not be written`
|
|
4638
4697
|
);
|
|
4639
4698
|
for (const f of result.failures) lines.push(`- ${f.repo}: ${f.message}`);
|
|
4640
4699
|
lines.push("");
|
|
4641
4700
|
}
|
|
4642
4701
|
if (result.markerConflicts.length > 0) {
|
|
4643
4702
|
lines.push(
|
|
4644
|
-
`##
|
|
4703
|
+
`## Marker conflicts (${result.markerConflicts.length}) \u2014 the canonical's markers are missing/malformed, so it is not overwritten`
|
|
4645
4704
|
);
|
|
4646
4705
|
for (const c of result.markerConflicts) {
|
|
4647
|
-
const detail = c.reason === "no_markers" ? "
|
|
4706
|
+
const detail = c.reason === "no_markers" ? "no marker region" : `malformed markers (${c.reason})`;
|
|
4648
4707
|
lines.push(`- ${c.repo}: ${detail}`);
|
|
4649
4708
|
}
|
|
4650
4709
|
lines.push(
|
|
4651
|
-
`
|
|
4710
|
+
` Fix: add these two lines where you want the preset block \u2014 \`${GENERATED_START}\` and \`${GENERATED_END}\` (absent, basou creates a fresh canonical).`
|
|
4652
4711
|
);
|
|
4653
4712
|
lines.push("");
|
|
4654
4713
|
}
|
|
4655
4714
|
if (result.unreadable.length > 0) {
|
|
4656
4715
|
lines.push(
|
|
4657
|
-
`##
|
|
4716
|
+
`## Canonical unreadable (${result.unreadable.length}) \u2014 could not be read (a directory, permissions, etc.)`
|
|
4658
4717
|
);
|
|
4659
4718
|
for (const p of result.unreadable) lines.push(`- ${p}`);
|
|
4660
4719
|
lines.push("");
|
|
4661
4720
|
}
|
|
4662
4721
|
if (result.collisions.length > 0) {
|
|
4663
4722
|
lines.push(
|
|
4664
|
-
`##
|
|
4723
|
+
`## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-generated)`
|
|
4665
4724
|
);
|
|
4666
4725
|
for (const c of result.collisions) {
|
|
4667
4726
|
lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
|
|
@@ -4670,25 +4729,25 @@ function renderProjectPreset(result) {
|
|
|
4670
4729
|
}
|
|
4671
4730
|
if (result.undeclared.length > 0) {
|
|
4672
4731
|
lines.push(
|
|
4673
|
-
`##
|
|
4732
|
+
`## Undeclared (${result.undeclared.length}) \u2014 visibility / language / publishes unset, so nothing is generated`
|
|
4674
4733
|
);
|
|
4675
4734
|
for (const p of result.undeclared) lines.push(`- ${p}`);
|
|
4676
4735
|
lines.push("");
|
|
4677
4736
|
}
|
|
4678
4737
|
if (result.anchors.length > 0) {
|
|
4679
4738
|
lines.push(
|
|
4680
|
-
`##
|
|
4739
|
+
`## Anchor (${result.anchors.length}) \u2014 its own AGENTS.md is hand-maintained, so it is skipped`
|
|
4681
4740
|
);
|
|
4682
4741
|
for (const p of result.anchors) lines.push(`- ${p}`);
|
|
4683
4742
|
lines.push("");
|
|
4684
4743
|
}
|
|
4685
4744
|
if (result.unreachable.length > 0) {
|
|
4686
|
-
lines.push(`##
|
|
4745
|
+
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
4687
4746
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
4688
4747
|
lines.push("");
|
|
4689
4748
|
}
|
|
4690
4749
|
lines.push(
|
|
4691
|
-
"
|
|
4750
|
+
"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
4751
|
);
|
|
4693
4752
|
return lines.join("\n");
|
|
4694
4753
|
}
|
|
@@ -4749,6 +4808,403 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
4749
4808
|
canonical: canonical2
|
|
4750
4809
|
};
|
|
4751
4810
|
}
|
|
4811
|
+
function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
|
|
4812
|
+
const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
4813
|
+
return expectedSymlinkTargets(repoReal, canonicalFile);
|
|
4814
|
+
}
|
|
4815
|
+
function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
4816
|
+
const filePath = join7(viewDir, name);
|
|
4817
|
+
try {
|
|
4818
|
+
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
4819
|
+
const target = readlinkSync(filePath);
|
|
4820
|
+
if (isAbsolute3(target)) return false;
|
|
4821
|
+
return realpathSync(resolve7(viewDir, target)) === repoReal;
|
|
4822
|
+
} catch {
|
|
4823
|
+
return false;
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
|
|
4827
|
+
const filePath = join7(viewDir, name);
|
|
4828
|
+
try {
|
|
4829
|
+
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
4830
|
+
const target = readlinkSync(filePath);
|
|
4831
|
+
if (isAbsolute3(target)) return false;
|
|
4832
|
+
return resolve7(viewDir, target) === expectedRepoPath;
|
|
4833
|
+
} catch {
|
|
4834
|
+
return false;
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
4838
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
4839
|
+
let repoReal;
|
|
4840
|
+
try {
|
|
4841
|
+
repoReal = realpathSync(resolve7(repositoryRoot, target));
|
|
4842
|
+
} catch {
|
|
4843
|
+
repoReal = void 0;
|
|
4844
|
+
}
|
|
4845
|
+
const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
|
|
4846
|
+
const targetAbs = resolve7(repositoryRoot, target);
|
|
4847
|
+
const canonicalName = basename4(repoReal ?? targetAbs);
|
|
4848
|
+
const roster = manifest.repos ?? [];
|
|
4849
|
+
const inRoster = roster.some((r) => {
|
|
4850
|
+
try {
|
|
4851
|
+
return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
|
|
4852
|
+
} catch {
|
|
4853
|
+
return resolve7(repositoryRoot, r.path) === targetAbs;
|
|
4854
|
+
}
|
|
4855
|
+
});
|
|
4856
|
+
const cnFold = canonicalName.toLowerCase();
|
|
4857
|
+
const canonicalShared = roster.some((r) => {
|
|
4858
|
+
let rReal = null;
|
|
4859
|
+
try {
|
|
4860
|
+
rReal = realpathSync(resolve7(repositoryRoot, r.path));
|
|
4861
|
+
} catch {
|
|
4862
|
+
rReal = null;
|
|
4863
|
+
}
|
|
4864
|
+
if (rReal !== null) {
|
|
4865
|
+
if (repoReal !== void 0 && rReal === repoReal) return false;
|
|
4866
|
+
return basename4(rReal).toLowerCase() === cnFold;
|
|
4867
|
+
}
|
|
4868
|
+
if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
|
|
4869
|
+
return basename4(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
|
|
4870
|
+
});
|
|
4871
|
+
const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
|
|
4872
|
+
const items = [];
|
|
4873
|
+
if (!isAnchor) {
|
|
4874
|
+
if (repoReal !== void 0) {
|
|
4875
|
+
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
4876
|
+
const { state, actualTarget } = inspectSymlink(join7(repoReal, spec.name), spec.target);
|
|
4877
|
+
if (state === "correct")
|
|
4878
|
+
items.push({ kind: "instruction-symlink", label: spec.name, state: "removable" });
|
|
4879
|
+
else if (state === "mismatch")
|
|
4880
|
+
items.push({
|
|
4881
|
+
kind: "instruction-symlink",
|
|
4882
|
+
label: spec.name,
|
|
4883
|
+
state: "foreign",
|
|
4884
|
+
note: `points at a different target (${actualTarget ?? "?"})`
|
|
4885
|
+
});
|
|
4886
|
+
else if (state === "occupied")
|
|
4887
|
+
items.push({
|
|
4888
|
+
kind: "instruction-symlink",
|
|
4889
|
+
label: spec.name,
|
|
4890
|
+
state: "foreign",
|
|
4891
|
+
note: "a real file, not a symlink"
|
|
4892
|
+
});
|
|
4893
|
+
else if (state === "blocked")
|
|
4894
|
+
items.push({
|
|
4895
|
+
kind: "instruction-symlink",
|
|
4896
|
+
label: spec.name,
|
|
4897
|
+
state: "blocked",
|
|
4898
|
+
note: "could not be inspected"
|
|
4899
|
+
});
|
|
4900
|
+
}
|
|
4901
|
+
let ignored;
|
|
4902
|
+
try {
|
|
4903
|
+
ignored = new Set(readGitignoreLines(join7(repoReal, ".gitignore")).map((l) => l.trim()));
|
|
4904
|
+
for (const p of INSTRUCTION_FILES) {
|
|
4905
|
+
if (ignored.has(p) || ignored.has(`/${p}`)) {
|
|
4906
|
+
items.push({
|
|
4907
|
+
kind: "gitignore",
|
|
4908
|
+
label: p,
|
|
4909
|
+
state: "manual",
|
|
4910
|
+
note: "cannot tell a basou-appended line from a hand-added one (no marker) \u2014 remove manually"
|
|
4911
|
+
});
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
} catch {
|
|
4915
|
+
items.push({
|
|
4916
|
+
kind: "gitignore",
|
|
4917
|
+
label: ".gitignore",
|
|
4918
|
+
state: "blocked",
|
|
4919
|
+
note: "could not be read"
|
|
4920
|
+
});
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
const viewPath = manifest.workspace.view;
|
|
4924
|
+
if (viewPath !== void 0) {
|
|
4925
|
+
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
4926
|
+
const linkPath = join7(viewDir, canonicalName);
|
|
4927
|
+
let isLink = false;
|
|
4928
|
+
try {
|
|
4929
|
+
isLink = lstatSync(linkPath).isSymbolicLink();
|
|
4930
|
+
} catch {
|
|
4931
|
+
isLink = false;
|
|
4932
|
+
}
|
|
4933
|
+
if (isLink) {
|
|
4934
|
+
const owned = repoReal !== void 0 ? viewLinkPointsAt(viewDir, canonicalName, repoReal) : viewLinkPointsAtPath(viewDir, canonicalName, targetAbs);
|
|
4935
|
+
if (!owned)
|
|
4936
|
+
items.push({
|
|
4937
|
+
kind: "view-symlink",
|
|
4938
|
+
label: canonicalName,
|
|
4939
|
+
state: "foreign",
|
|
4940
|
+
note: "a view link that does not point at this repo"
|
|
4941
|
+
});
|
|
4942
|
+
else if (canonicalShared)
|
|
4943
|
+
items.push({
|
|
4944
|
+
kind: "view-symlink",
|
|
4945
|
+
label: canonicalName,
|
|
4946
|
+
state: "blocked",
|
|
4947
|
+
note: collisionNote
|
|
4948
|
+
});
|
|
4949
|
+
else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
4953
|
+
const canonicalLabel = join7("agents", canonicalName, CANONICAL_FILE);
|
|
4954
|
+
let canonicalIsLink = false;
|
|
4955
|
+
try {
|
|
4956
|
+
canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
|
|
4957
|
+
} catch {
|
|
4958
|
+
canonicalIsLink = false;
|
|
4959
|
+
}
|
|
4960
|
+
if (canonicalIsLink) {
|
|
4961
|
+
items.push({
|
|
4962
|
+
kind: "canonical-block",
|
|
4963
|
+
label: canonicalLabel,
|
|
4964
|
+
state: "foreign",
|
|
4965
|
+
note: "the canonical is a symlink (not generated)"
|
|
4966
|
+
});
|
|
4967
|
+
} else if (existsSync(canonicalFile)) {
|
|
4968
|
+
let content;
|
|
4969
|
+
try {
|
|
4970
|
+
content = readFileSync(canonicalFile, "utf8");
|
|
4971
|
+
} catch {
|
|
4972
|
+
items.push({
|
|
4973
|
+
kind: "canonical-block",
|
|
4974
|
+
label: canonicalLabel,
|
|
4975
|
+
state: "blocked",
|
|
4976
|
+
note: "could not be read"
|
|
4977
|
+
});
|
|
4978
|
+
}
|
|
4979
|
+
if (content !== void 0 && content !== "") {
|
|
4980
|
+
const section = parseMarkers(content);
|
|
4981
|
+
if (section.kind === "ok" && canonicalShared) {
|
|
4982
|
+
items.push({
|
|
4983
|
+
kind: "canonical-block",
|
|
4984
|
+
label: canonicalLabel,
|
|
4985
|
+
state: "blocked",
|
|
4986
|
+
note: collisionNote
|
|
4987
|
+
});
|
|
4988
|
+
} else if (section.kind === "ok" && repoReal === void 0) {
|
|
4989
|
+
items.push({
|
|
4990
|
+
kind: "canonical-block",
|
|
4991
|
+
label: canonicalLabel,
|
|
4992
|
+
state: "manual",
|
|
4993
|
+
note: "repo could not be resolved, so ownership cannot be verified (check manually)"
|
|
4994
|
+
});
|
|
4995
|
+
} else if (section.kind === "ok") {
|
|
4996
|
+
const emptyAfter = removeMarkerSection(content, canonicalLabel).trim().length === 0;
|
|
4997
|
+
items.push({
|
|
4998
|
+
kind: "canonical-block",
|
|
4999
|
+
label: canonicalLabel,
|
|
5000
|
+
state: "removable",
|
|
5001
|
+
...emptyAfter ? {
|
|
5002
|
+
note: "the file becomes empty after the generated block is removed (a manual-delete candidate)"
|
|
5003
|
+
} : {}
|
|
5004
|
+
});
|
|
5005
|
+
} else if (section.kind === "no_markers") {
|
|
5006
|
+
items.push({
|
|
5007
|
+
kind: "canonical-block",
|
|
5008
|
+
label: canonicalLabel,
|
|
5009
|
+
state: "foreign",
|
|
5010
|
+
note: "no generated block (hand-authored only \u2014 left untouched)"
|
|
5011
|
+
});
|
|
5012
|
+
} else {
|
|
5013
|
+
items.push({
|
|
5014
|
+
kind: "canonical-block",
|
|
5015
|
+
label: canonicalLabel,
|
|
5016
|
+
state: "blocked",
|
|
5017
|
+
note: "malformed markers (fix manually)"
|
|
5018
|
+
});
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
return {
|
|
5024
|
+
target,
|
|
5025
|
+
resolved: repoReal !== void 0,
|
|
5026
|
+
repoReal: repoReal ?? null,
|
|
5027
|
+
canonicalName,
|
|
5028
|
+
isAnchor,
|
|
5029
|
+
inRoster,
|
|
5030
|
+
items,
|
|
5031
|
+
removableCount: items.filter((i) => i.state === "removable").length
|
|
5032
|
+
};
|
|
5033
|
+
}
|
|
5034
|
+
function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
5035
|
+
const removed = [];
|
|
5036
|
+
const failed = [];
|
|
5037
|
+
const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
|
|
5038
|
+
let currentRepoReal = null;
|
|
5039
|
+
try {
|
|
5040
|
+
currentRepoReal = realpathSync(resolve7(repositoryRoot, plan.target));
|
|
5041
|
+
} catch {
|
|
5042
|
+
currentRepoReal = null;
|
|
5043
|
+
}
|
|
5044
|
+
const identityOk = plan.repoReal === null ? true : currentRepoReal === plan.repoReal;
|
|
5045
|
+
const removable = plan.items.filter((i) => i.state === "removable");
|
|
5046
|
+
if (!identityOk) {
|
|
5047
|
+
for (const item of removable) changed(item.label);
|
|
5048
|
+
return { removed, failed };
|
|
5049
|
+
}
|
|
5050
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
5051
|
+
const { canonicalName, repoReal } = plan;
|
|
5052
|
+
const expectedByName = new Map(
|
|
5053
|
+
repoReal !== null ? teardownExpectedTargets(repoReal, anchorReal, canonicalName).map((s) => [s.name, s.target]) : []
|
|
5054
|
+
);
|
|
5055
|
+
for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
|
|
5056
|
+
const expected = expectedByName.get(item.label);
|
|
5057
|
+
if (repoReal === null || expected === void 0 || inspectSymlink(join7(repoReal, item.label), expected).state !== "correct") {
|
|
5058
|
+
changed(item.label);
|
|
5059
|
+
continue;
|
|
5060
|
+
}
|
|
5061
|
+
try {
|
|
5062
|
+
unlinkSync(join7(repoReal, item.label));
|
|
5063
|
+
removed.push(item.label);
|
|
5064
|
+
} catch (error) {
|
|
5065
|
+
failed.push({ label: item.label, message: failureReason(error) });
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
const viewPath = manifest.workspace.view;
|
|
5069
|
+
for (const item of removable.filter((i) => i.kind === "view-symlink")) {
|
|
5070
|
+
if (viewPath === void 0) {
|
|
5071
|
+
changed(item.label);
|
|
5072
|
+
continue;
|
|
5073
|
+
}
|
|
5074
|
+
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
5075
|
+
const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve7(repositoryRoot, plan.target));
|
|
5076
|
+
if (!owned) {
|
|
5077
|
+
changed(item.label);
|
|
5078
|
+
continue;
|
|
5079
|
+
}
|
|
5080
|
+
try {
|
|
5081
|
+
unlinkSync(join7(viewDir, item.label));
|
|
5082
|
+
removed.push(`view/${item.label}`);
|
|
5083
|
+
} catch (error) {
|
|
5084
|
+
failed.push({ label: `view/${item.label}`, message: failureReason(error) });
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
5088
|
+
for (const item of removable.filter((i) => i.kind === "canonical-block")) {
|
|
5089
|
+
const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5090
|
+
try {
|
|
5091
|
+
if (lstatSync(canonicalFile).isSymbolicLink()) {
|
|
5092
|
+
changed(item.label);
|
|
5093
|
+
continue;
|
|
5094
|
+
}
|
|
5095
|
+
} catch (error) {
|
|
5096
|
+
failed.push({ label: item.label, message: failureReason(error) });
|
|
5097
|
+
continue;
|
|
5098
|
+
}
|
|
5099
|
+
let fd;
|
|
5100
|
+
try {
|
|
5101
|
+
fd = openSync(canonicalFile, fsConstants.O_RDWR | NOFOLLOW);
|
|
5102
|
+
} catch (error) {
|
|
5103
|
+
changed(item.label);
|
|
5104
|
+
void error;
|
|
5105
|
+
continue;
|
|
5106
|
+
}
|
|
5107
|
+
try {
|
|
5108
|
+
const content = readFileSync(fd, "utf8");
|
|
5109
|
+
if (parseMarkers(content).kind !== "ok") {
|
|
5110
|
+
changed(item.label);
|
|
5111
|
+
continue;
|
|
5112
|
+
}
|
|
5113
|
+
const next = Buffer.from(removeMarkerSection(content, item.label), "utf8");
|
|
5114
|
+
ftruncateSync(fd, 0);
|
|
5115
|
+
writeSync(fd, next, 0, next.length, 0);
|
|
5116
|
+
removed.push(item.label);
|
|
5117
|
+
} catch (error) {
|
|
5118
|
+
failed.push({ label: item.label, message: failureReason(error) });
|
|
5119
|
+
} finally {
|
|
5120
|
+
closeSync(fd);
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
return { removed, failed };
|
|
5124
|
+
}
|
|
5125
|
+
function renderProjectTeardown(result) {
|
|
5126
|
+
const lines = [];
|
|
5127
|
+
lines.push(`# teardown: ${result.target}`);
|
|
5128
|
+
lines.push("");
|
|
5129
|
+
if (result.isAnchor) {
|
|
5130
|
+
lines.push("The anchor (`.`) cannot be torn down (it is the project's home).");
|
|
5131
|
+
return lines.join("\n");
|
|
5132
|
+
}
|
|
5133
|
+
if (!result.resolved) {
|
|
5134
|
+
lines.push(
|
|
5135
|
+
"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."
|
|
5136
|
+
);
|
|
5137
|
+
lines.push("");
|
|
5138
|
+
}
|
|
5139
|
+
lines.push(
|
|
5140
|
+
result.inRoster ? "Status: still a roster member (the declaration remains in the manifest)." : "Status: not a roster member (already archived)."
|
|
5141
|
+
);
|
|
5142
|
+
lines.push("");
|
|
5143
|
+
const removable = result.items.filter((i) => i.state === "removable");
|
|
5144
|
+
const manual = result.items.filter((i) => i.state === "manual");
|
|
5145
|
+
const foreign = result.items.filter((i) => i.state === "foreign");
|
|
5146
|
+
const blocked = result.items.filter((i) => i.state === "blocked");
|
|
5147
|
+
if (removable.length === 0) {
|
|
5148
|
+
lines.push("No basou-generated artifact to remove.");
|
|
5149
|
+
} else {
|
|
5150
|
+
lines.push(`To remove (${removable.length}):`);
|
|
5151
|
+
for (const i of removable)
|
|
5152
|
+
lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
|
|
5153
|
+
}
|
|
5154
|
+
if (manual.length > 0) {
|
|
5155
|
+
lines.push("");
|
|
5156
|
+
lines.push("Check manually (not auto-removed):");
|
|
5157
|
+
for (const i of manual)
|
|
5158
|
+
lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
|
|
5159
|
+
}
|
|
5160
|
+
if (foreign.length > 0) {
|
|
5161
|
+
lines.push("");
|
|
5162
|
+
lines.push("Left untouched (not basou-generated):");
|
|
5163
|
+
for (const i of foreign)
|
|
5164
|
+
lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
|
|
5165
|
+
}
|
|
5166
|
+
if (blocked.length > 0) {
|
|
5167
|
+
lines.push("");
|
|
5168
|
+
lines.push("Could not be inspected:");
|
|
5169
|
+
for (const i of blocked)
|
|
5170
|
+
lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
|
|
5171
|
+
}
|
|
5172
|
+
lines.push("");
|
|
5173
|
+
if (result.applied) {
|
|
5174
|
+
lines.push(`--apply: removed ${result.removed.length}.`);
|
|
5175
|
+
for (const r of result.removed) lines.push(` \u2713 ${r}`);
|
|
5176
|
+
if (result.failed.length > 0) {
|
|
5177
|
+
lines.push("Failed:");
|
|
5178
|
+
for (const f of result.failed) lines.push(` \u2717 ${f.label} \u2014 ${f.message}`);
|
|
5179
|
+
}
|
|
5180
|
+
} else if (removable.length > 0) {
|
|
5181
|
+
lines.push(
|
|
5182
|
+
"This is a dry-run. Pass --apply to remove (this is a destructive, irreversible operation)."
|
|
5183
|
+
);
|
|
5184
|
+
}
|
|
5185
|
+
return lines.join("\n");
|
|
5186
|
+
}
|
|
5187
|
+
async function doRunProjectTeardown(target, options, ctx = {}) {
|
|
5188
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
5189
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project teardown");
|
|
5190
|
+
const paths = basouPaths10(repositoryRoot);
|
|
5191
|
+
const manifest = await readManifest6(paths);
|
|
5192
|
+
const plan = gatherRepoTeardown(repositoryRoot, manifest, target);
|
|
5193
|
+
const willApply = options.apply === true && !plan.isAnchor && plan.removableCount > 0;
|
|
5194
|
+
const { removed, failed } = willApply ? applyRepoTeardown(repositoryRoot, manifest, plan) : { removed: [], failed: [] };
|
|
5195
|
+
const result = { ...plan, applied: willApply, removed, failed };
|
|
5196
|
+
if (options.json === true) console.log(JSON.stringify(result));
|
|
5197
|
+
else console.log(renderProjectTeardown(result));
|
|
5198
|
+
return result;
|
|
5199
|
+
}
|
|
5200
|
+
async function runProjectTeardown(target, options, ctx = {}) {
|
|
5201
|
+
try {
|
|
5202
|
+
await doRunProjectTeardown(target, options, ctx);
|
|
5203
|
+
} catch (error) {
|
|
5204
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
5205
|
+
process.exitCode = 1;
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
4752
5208
|
function omitKey(obj, key) {
|
|
4753
5209
|
const clone = { ...obj };
|
|
4754
5210
|
delete clone[key];
|
|
@@ -4818,68 +5274,74 @@ async function doRunProjectArchive(target, options, ctx) {
|
|
|
4818
5274
|
}
|
|
4819
5275
|
function renderProjectArchive(result) {
|
|
4820
5276
|
const lines = [];
|
|
4821
|
-
lines.push("# repo
|
|
5277
|
+
lines.push("# Archive a repo (fold it out of the roster)");
|
|
4822
5278
|
lines.push("");
|
|
4823
5279
|
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
4824
5280
|
if (!result.hasRoster) {
|
|
4825
|
-
lines.push("\u2139\uFE0F repo
|
|
5281
|
+
lines.push("\u2139\uFE0F No repo roster declared (manifest `repos`). There is nothing to archive.");
|
|
4826
5282
|
return lines.join("\n");
|
|
4827
5283
|
}
|
|
4828
5284
|
if (result.isAnchor) {
|
|
4829
5285
|
lines.push(
|
|
4830
|
-
`\u26A0\uFE0F \`${result.target}\`
|
|
5286
|
+
`\u26A0\uFE0F \`${result.target}\` is the anchor (the project root). The anchor cannot be archived (it is the manifest's home).`
|
|
4831
5287
|
);
|
|
4832
5288
|
return lines.join("\n");
|
|
4833
5289
|
}
|
|
4834
5290
|
if (!result.found) {
|
|
4835
|
-
lines.push(`\u2139\uFE0F \`${result.target}\`
|
|
5291
|
+
lines.push(`\u2139\uFE0F \`${result.target}\` is not declared in the roster (nothing to archive).`);
|
|
4836
5292
|
return lines.join("\n");
|
|
4837
5293
|
}
|
|
4838
5294
|
if (result.applied) {
|
|
4839
|
-
lines.push(`\u2705 \`${result.target}\`
|
|
5295
|
+
lines.push(`\u2705 Removed \`${result.target}\` from the roster.`);
|
|
4840
5296
|
} else {
|
|
4841
|
-
lines.push(
|
|
5297
|
+
lines.push(`To remove \`${result.target}\` from the roster (dry-run; pass --apply to write):`);
|
|
4842
5298
|
}
|
|
4843
5299
|
if (result.sourceRootRemoval !== void 0) {
|
|
4844
5300
|
lines.push(
|
|
4845
|
-
`-
|
|
5301
|
+
`- ${result.applied ? "Pruned" : "Will prune"} ${result.sourceRootRemoval} from source_roots (no longer captured by refresh).`
|
|
4846
5302
|
);
|
|
4847
5303
|
} else {
|
|
4848
|
-
lines.push("- source_roots
|
|
5304
|
+
lines.push("- No matching entry in source_roots (nothing to prune).");
|
|
4849
5305
|
}
|
|
4850
5306
|
if (result.reposEmptied) {
|
|
4851
5307
|
lines.push(
|
|
4852
|
-
"-
|
|
5308
|
+
"- This was the last member \u2192 the roster empties and the `repos` declaration is removed (the project is folded up)."
|
|
4853
5309
|
);
|
|
4854
5310
|
} else if (result.becomesSolo) {
|
|
4855
5311
|
lines.push(
|
|
4856
|
-
"-
|
|
5312
|
+
"- This leaves 1 repo (solo) \u2192 the workspace view is unnecessary (consider removing the view declaration / directory)."
|
|
4857
5313
|
);
|
|
4858
5314
|
}
|
|
4859
5315
|
lines.push("");
|
|
4860
5316
|
const t = result.teardown;
|
|
4861
5317
|
const items = [];
|
|
4862
|
-
if (t.viewLink) items.push("workspace view
|
|
4863
|
-
if (t.instructionFiles.length > 0)
|
|
5318
|
+
if (t.viewLink) items.push("the workspace view's symlink entry");
|
|
5319
|
+
if (t.instructionFiles.length > 0)
|
|
5320
|
+
items.push(`instruction files (${t.instructionFiles.join(", ")})`);
|
|
4864
5321
|
if (t.gitignorePatterns.length > 0)
|
|
4865
|
-
items.push(`.gitignore
|
|
4866
|
-
if (t.canonical)
|
|
5322
|
+
items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
|
|
5323
|
+
if (t.canonical)
|
|
5324
|
+
items.push(`the anchor's canonical (agents/${basename4(result.target)}/AGENTS.md)`);
|
|
4867
5325
|
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
5326
|
lines.push(
|
|
4870
|
-
"
|
|
5327
|
+
"## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
|
|
5328
|
+
);
|
|
5329
|
+
lines.push(
|
|
5330
|
+
"- The repo may already be deleted. Check manually for a leftover view symlink / instruction symlinks / .gitignore / canonical."
|
|
4871
5331
|
);
|
|
4872
5332
|
lines.push("");
|
|
4873
5333
|
} else if (items.length > 0) {
|
|
4874
|
-
lines.push(
|
|
5334
|
+
lines.push(
|
|
5335
|
+
"## Manual teardown (--apply does not touch these; remove the leftover wiring by hand)"
|
|
5336
|
+
);
|
|
4875
5337
|
for (const i of items) lines.push(`- ${i}`);
|
|
4876
5338
|
lines.push("");
|
|
4877
5339
|
} else {
|
|
4878
|
-
lines.push("repo
|
|
5340
|
+
lines.push("No repo-side wiring (view / instruction files / .gitignore / canonical) remains.");
|
|
4879
5341
|
lines.push("");
|
|
4880
5342
|
}
|
|
4881
5343
|
lines.push(
|
|
4882
|
-
"
|
|
5344
|
+
"Note: archive only changes the manifest (.basou, git-tracked, reversible). The repo, its captured history, and its on-disk wiring are not removed."
|
|
4883
5345
|
);
|
|
4884
5346
|
return lines.join("\n");
|
|
4885
5347
|
}
|
|
@@ -4965,46 +5427,46 @@ async function doRunProjectRename(oldPath, newPath, options, ctx) {
|
|
|
4965
5427
|
}
|
|
4966
5428
|
function renderProjectRename(result) {
|
|
4967
5429
|
const lines = [];
|
|
4968
|
-
lines.push("# repo
|
|
5430
|
+
lines.push("# Rename a repo (update its roster path)");
|
|
4969
5431
|
lines.push("");
|
|
4970
5432
|
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
4971
5433
|
if (!result.hasRoster) {
|
|
4972
|
-
lines.push("\u2139\uFE0F repo
|
|
5434
|
+
lines.push("\u2139\uFE0F No repo roster declared (manifest `repos`). There is nothing to rename.");
|
|
4973
5435
|
return lines.join("\n");
|
|
4974
5436
|
}
|
|
4975
5437
|
if (result.noop) {
|
|
4976
|
-
lines.push(`\u2139\uFE0F \`${result.oldTarget}\`
|
|
5438
|
+
lines.push(`\u2139\uFE0F \`${result.oldTarget}\` and \`${result.newTarget}\` are identical (no change).`);
|
|
4977
5439
|
return lines.join("\n");
|
|
4978
5440
|
}
|
|
4979
5441
|
if (result.isAnchor) {
|
|
4980
5442
|
lines.push(
|
|
4981
|
-
`\u26A0\uFE0F \`${result.oldTarget}\`
|
|
5443
|
+
`\u26A0\uFE0F \`${result.oldTarget}\` is the anchor (the project root). The anchor cannot be renamed.`
|
|
4982
5444
|
);
|
|
4983
5445
|
return lines.join("\n");
|
|
4984
5446
|
}
|
|
4985
5447
|
if (!result.found) {
|
|
4986
|
-
lines.push(`\u2139\uFE0F \`${result.oldTarget}\`
|
|
5448
|
+
lines.push(`\u2139\uFE0F \`${result.oldTarget}\` is not declared in the roster (nothing to rename).`);
|
|
4987
5449
|
return lines.join("\n");
|
|
4988
5450
|
}
|
|
4989
5451
|
if (result.collision) {
|
|
4990
5452
|
lines.push(
|
|
4991
|
-
`\u26A0\uFE0F \`${result.newTarget}\`
|
|
5453
|
+
`\u26A0\uFE0F \`${result.newTarget}\` is already declared in the roster. Not renaming, to avoid a duplicate.`
|
|
4992
5454
|
);
|
|
4993
5455
|
return lines.join("\n");
|
|
4994
5456
|
}
|
|
4995
5457
|
if (result.applied) {
|
|
4996
|
-
lines.push(`\u2705 \`${result.oldTarget}\`
|
|
5458
|
+
lines.push(`\u2705 Renamed \`${result.oldTarget}\` to \`${result.newTarget}\`.`);
|
|
4997
5459
|
} else {
|
|
4998
5460
|
lines.push(
|
|
4999
|
-
|
|
5461
|
+
`To rename \`${result.oldTarget}\` to \`${result.newTarget}\` (dry-run; pass --apply to write):`
|
|
5000
5462
|
);
|
|
5001
5463
|
}
|
|
5002
5464
|
if (result.sourceRootRenamed !== void 0) {
|
|
5003
5465
|
lines.push(
|
|
5004
|
-
`-
|
|
5466
|
+
`- ${result.applied ? "Updated" : "Will update"} ${result.sourceRootRenamed} to ${result.newTarget} in source_roots.`
|
|
5005
5467
|
);
|
|
5006
5468
|
} else {
|
|
5007
|
-
lines.push("- source_roots
|
|
5469
|
+
lines.push("- No matching entry in source_roots (nothing to update).");
|
|
5008
5470
|
}
|
|
5009
5471
|
lines.push("");
|
|
5010
5472
|
if (result.basenameChanged) {
|
|
@@ -5013,31 +5475,214 @@ function renderProjectRename(result) {
|
|
|
5013
5475
|
const items = [];
|
|
5014
5476
|
if (result.wiring.canonicalDirOld)
|
|
5015
5477
|
items.push(`anchor canonical: agents/${oldName}/ \u2192 agents/${newName}/`);
|
|
5016
|
-
if (result.wiring.viewLinkOld) items.push(`workspace view
|
|
5478
|
+
if (result.wiring.viewLinkOld) items.push(`workspace view symlink: ${oldName} \u2192 ${newName}`);
|
|
5017
5479
|
if (items.length > 0) {
|
|
5018
5480
|
lines.push(
|
|
5019
|
-
"##
|
|
5481
|
+
"## Manual rename (--apply does not touch these; the basename changed, so update them by hand)"
|
|
5020
5482
|
);
|
|
5021
5483
|
for (const i of items) lines.push(`- ${i}`);
|
|
5022
5484
|
} else {
|
|
5023
5485
|
lines.push(
|
|
5024
|
-
`basename
|
|
5486
|
+
`The basename changes ${oldName} \u2192 ${newName}, but no anchor canonical / view symlink was found.`
|
|
5025
5487
|
);
|
|
5026
5488
|
}
|
|
5027
5489
|
lines.push(
|
|
5028
|
-
"
|
|
5490
|
+
" After applying, regenerate the instruction symlinks and the view with `basou project symlinks` / `basou project workspace`."
|
|
5029
5491
|
);
|
|
5030
5492
|
} else {
|
|
5031
5493
|
lines.push(
|
|
5032
|
-
"
|
|
5494
|
+
"Note: the basename is unchanged. If you moved the repo elsewhere, regenerate the relative targets with `basou project symlinks` / `basou project workspace`."
|
|
5033
5495
|
);
|
|
5034
5496
|
}
|
|
5035
5497
|
lines.push("");
|
|
5036
5498
|
lines.push(
|
|
5037
|
-
"
|
|
5499
|
+
"Note: rename only changes the manifest (.basou, git-tracked, reversible). It does not move the repo or update the on-disk wiring."
|
|
5038
5500
|
);
|
|
5039
5501
|
return lines.join("\n");
|
|
5040
5502
|
}
|
|
5503
|
+
async function runProjectNew(repos, options, ctx = {}) {
|
|
5504
|
+
try {
|
|
5505
|
+
await doRunProjectNew(repos, options, ctx);
|
|
5506
|
+
} catch (error) {
|
|
5507
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
5508
|
+
process.exitCode = 1;
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
async function resolveRepositoryRootForNew(cwd) {
|
|
5512
|
+
try {
|
|
5513
|
+
return await resolveRepositoryRoot8(cwd);
|
|
5514
|
+
} catch (error) {
|
|
5515
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5516
|
+
throw new Error(
|
|
5517
|
+
"Not a git repository. Run 'git init' first, then re-run 'basou project new'.",
|
|
5518
|
+
{ cause: error }
|
|
5519
|
+
);
|
|
5520
|
+
}
|
|
5521
|
+
throw error;
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
async function doRunProjectNew(repos, options, ctx) {
|
|
5525
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
5526
|
+
const repositoryRoot = await resolveRepositoryRootForNew(cwd);
|
|
5527
|
+
const workspaceName = basename4(repositoryRoot);
|
|
5528
|
+
const declared = repos.map((p) => {
|
|
5529
|
+
const abs = resolve7(cwd, p);
|
|
5530
|
+
let real;
|
|
5531
|
+
try {
|
|
5532
|
+
real = realpathSync(abs);
|
|
5533
|
+
} catch {
|
|
5534
|
+
real = abs;
|
|
5535
|
+
}
|
|
5536
|
+
const rel = relative2(repositoryRoot, real);
|
|
5537
|
+
return rel === "" ? "." : rel;
|
|
5538
|
+
});
|
|
5539
|
+
const invalidRepos = declared.filter(
|
|
5540
|
+
(rel) => classifySourceRoot(repositoryRoot, rel).kind !== "repo"
|
|
5541
|
+
);
|
|
5542
|
+
if (invalidRepos.length > 0) {
|
|
5543
|
+
throw new Error(
|
|
5544
|
+
`These declared repos are not git repositories (create them with 'git init' first): ${invalidRepos.join(", ")}`
|
|
5545
|
+
);
|
|
5546
|
+
}
|
|
5547
|
+
const rosterPaths = ["."];
|
|
5548
|
+
for (const rel of declared) {
|
|
5549
|
+
if (rel !== "." && !rosterPaths.includes(rel)) rosterPaths.push(rel);
|
|
5550
|
+
}
|
|
5551
|
+
const roster = rosterPaths.map((path) => ({ path }));
|
|
5552
|
+
const viewPath = options.view === false ? null : options.view ?? `../${workspaceName}-workspace`;
|
|
5553
|
+
const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
|
|
5554
|
+
const paths = basouPaths10(repositoryRoot);
|
|
5555
|
+
const existed = existsSync(paths.files.manifest);
|
|
5556
|
+
const manifest = createManifest2({ workspaceName, sourceRoots });
|
|
5557
|
+
manifest.repos = roster;
|
|
5558
|
+
if (viewPath !== null) manifest.workspace.view = viewPath;
|
|
5559
|
+
let applied = false;
|
|
5560
|
+
if (options.apply === true) {
|
|
5561
|
+
await ensureBasouDirectory2(repositoryRoot);
|
|
5562
|
+
await writeManifest2(paths, manifest, { force: options.force === true });
|
|
5563
|
+
applied = true;
|
|
5564
|
+
try {
|
|
5565
|
+
await appendBasouGitignore2(repositoryRoot, { localOnly: options.localOnly === true });
|
|
5566
|
+
} catch (error) {
|
|
5567
|
+
renderGitignoreWarningForNew(error, isVerbose(options));
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
const result = {
|
|
5571
|
+
workspaceName,
|
|
5572
|
+
repos: roster,
|
|
5573
|
+
view: viewPath,
|
|
5574
|
+
sourceRoots,
|
|
5575
|
+
invalidRepos: [],
|
|
5576
|
+
existed,
|
|
5577
|
+
applied
|
|
5578
|
+
};
|
|
5579
|
+
if (options.json === true) {
|
|
5580
|
+
console.log(JSON.stringify(result));
|
|
5581
|
+
} else {
|
|
5582
|
+
console.log(renderProjectNew(result));
|
|
5583
|
+
}
|
|
5584
|
+
return result;
|
|
5585
|
+
}
|
|
5586
|
+
function renderGitignoreWarningForNew(error, verbose) {
|
|
5587
|
+
const baseMessage = error instanceof Error ? error.message : String(error);
|
|
5588
|
+
console.error(
|
|
5589
|
+
`Warning: Could not update .gitignore (${baseMessage}). Add Basou's default .gitignore block manually.`
|
|
5590
|
+
);
|
|
5591
|
+
if (verbose && error instanceof Error) {
|
|
5592
|
+
const label = extractCauseLabel(error);
|
|
5593
|
+
if (label !== void 0) console.error(`Caused by: ${label}`);
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
function renderProjectNew(result) {
|
|
5597
|
+
const lines = [];
|
|
5598
|
+
lines.push("# Scaffold a new project (build from a declaration)");
|
|
5599
|
+
lines.push("");
|
|
5600
|
+
if (result.existed && !result.applied) {
|
|
5601
|
+
lines.push(
|
|
5602
|
+
"\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)."
|
|
5603
|
+
);
|
|
5604
|
+
lines.push("");
|
|
5605
|
+
}
|
|
5606
|
+
if (result.applied) {
|
|
5607
|
+
lines.push(`\u2705 Created \`.basou/\` for \`${result.workspaceName}\` and seeded the manifest:`);
|
|
5608
|
+
} else {
|
|
5609
|
+
lines.push(
|
|
5610
|
+
`Will create \`.basou/\` for \`${result.workspaceName}\` and seed the manifest (dry-run; pass --apply to write):`
|
|
5611
|
+
);
|
|
5612
|
+
}
|
|
5613
|
+
lines.push("");
|
|
5614
|
+
lines.push(`repos roster (${result.repos.length}):`);
|
|
5615
|
+
for (const r of result.repos) {
|
|
5616
|
+
lines.push(`- ${r.path}${r.path === "." ? " (anchor)" : ""}`);
|
|
5617
|
+
}
|
|
5618
|
+
lines.push("");
|
|
5619
|
+
lines.push(
|
|
5620
|
+
result.view !== null ? `workspace view: ${result.view}` : "workspace view: none (solo project)"
|
|
5621
|
+
);
|
|
5622
|
+
lines.push("");
|
|
5623
|
+
lines.push(`source_roots (${result.sourceRoots.length}):`);
|
|
5624
|
+
for (const s of result.sourceRoots) lines.push(`- ${s}`);
|
|
5625
|
+
lines.push("");
|
|
5626
|
+
lines.push(
|
|
5627
|
+
"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."
|
|
5628
|
+
);
|
|
5629
|
+
if (result.applied) {
|
|
5630
|
+
lines.push(
|
|
5631
|
+
"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."
|
|
5632
|
+
);
|
|
5633
|
+
} else {
|
|
5634
|
+
lines.push(
|
|
5635
|
+
"After applying, fill in visibility / language in the manifest, then run `basou project derive`."
|
|
5636
|
+
);
|
|
5637
|
+
}
|
|
5638
|
+
return lines.join("\n");
|
|
5639
|
+
}
|
|
5640
|
+
async function runProjectDerive(options, ctx = {}) {
|
|
5641
|
+
try {
|
|
5642
|
+
await doRunProjectDerive(options, ctx);
|
|
5643
|
+
} catch (error) {
|
|
5644
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
5645
|
+
process.exitCode = 1;
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
async function doRunProjectDerive(options, ctx) {
|
|
5649
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
5650
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
|
|
5651
|
+
const paths = basouPaths10(repositoryRoot);
|
|
5652
|
+
const manifest = await readManifest6(paths);
|
|
5653
|
+
if (manifest.repos === void 0 || manifest.repos.length === 0) {
|
|
5654
|
+
console.log(
|
|
5655
|
+
"# 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)."
|
|
5656
|
+
);
|
|
5657
|
+
return;
|
|
5658
|
+
}
|
|
5659
|
+
const apply = options.apply === true;
|
|
5660
|
+
const stepCtx = {
|
|
5661
|
+
cwd: repositoryRoot,
|
|
5662
|
+
...ctx.now !== void 0 ? { now: ctx.now } : {}
|
|
5663
|
+
};
|
|
5664
|
+
const stepOpts = { apply };
|
|
5665
|
+
console.log("# Generate project wiring in one pass (declaration \u2192 wiring)");
|
|
5666
|
+
console.log("");
|
|
5667
|
+
console.log("## 1/5 sync source_roots (roster \u2192 capture config)");
|
|
5668
|
+
await doRunProjectSync(stepOpts, stepCtx);
|
|
5669
|
+
console.log("");
|
|
5670
|
+
console.log("## 2/5 generate instruction-file A preset (declaration \u2192 canonical)");
|
|
5671
|
+
await doRunProjectPreset(stepOpts, stepCtx);
|
|
5672
|
+
console.log("");
|
|
5673
|
+
console.log("## 3/5 generate instruction-file symlinks (each repo \u2192 canonical)");
|
|
5674
|
+
await doRunProjectSymlinks(stepOpts, stepCtx);
|
|
5675
|
+
console.log("");
|
|
5676
|
+
console.log("## 4/5 generate workspace view (aggregate the roster repos)");
|
|
5677
|
+
await doRunProjectWorkspace(stepOpts, stepCtx);
|
|
5678
|
+
console.log("");
|
|
5679
|
+
console.log("## 5/5 generate .gitignore (exclude public repos' instruction files)");
|
|
5680
|
+
await doRunProjectGitignore(stepOpts, stepCtx);
|
|
5681
|
+
console.log("");
|
|
5682
|
+
console.log(
|
|
5683
|
+
apply ? "\u2705 Ran every step (each is idempotent, so a partial apply recovers on re-run)." : "\u2139\uFE0F Dry-run preview. Pass --apply to write the changes, then re-run."
|
|
5684
|
+
);
|
|
5685
|
+
}
|
|
5041
5686
|
|
|
5042
5687
|
// src/commands/protocol.ts
|
|
5043
5688
|
import { readFile as readFile4 } from "fs/promises";
|
|
@@ -5046,7 +5691,7 @@ import {
|
|
|
5046
5691
|
PROTOCOL_START,
|
|
5047
5692
|
parseMarkers as parseMarkers2,
|
|
5048
5693
|
readMarkdownFile as readMarkdownFile5,
|
|
5049
|
-
removeMarkerSection
|
|
5694
|
+
removeMarkerSection as removeMarkerSection2
|
|
5050
5695
|
} from "@basou/core";
|
|
5051
5696
|
|
|
5052
5697
|
// src/lib/durable-write.ts
|
|
@@ -5338,7 +5983,7 @@ async function doRunProtocolUnsync(options) {
|
|
|
5338
5983
|
console.log("No target file; nothing to remove.");
|
|
5339
5984
|
return;
|
|
5340
5985
|
}
|
|
5341
|
-
const newBody =
|
|
5986
|
+
const newBody = removeMarkerSection2(existing, "CLAUDE.md", PROTOCOL_MARKERS);
|
|
5342
5987
|
if (newBody === existing) {
|
|
5343
5988
|
console.log("No basou:protocols block found; nothing removed.");
|
|
5344
5989
|
return;
|
|
@@ -5711,7 +6356,7 @@ import {
|
|
|
5711
6356
|
basouPaths as basouPaths12,
|
|
5712
6357
|
findErrorCode as findErrorCode10,
|
|
5713
6358
|
renderReport,
|
|
5714
|
-
resolveRepositoryRoot as
|
|
6359
|
+
resolveRepositoryRoot as resolveRepositoryRoot9,
|
|
5715
6360
|
writeMarkdownFile as writeMarkdownFile6
|
|
5716
6361
|
} from "@basou/core";
|
|
5717
6362
|
function registerReportCommand(program) {
|
|
@@ -5760,7 +6405,7 @@ async function doRunReportGenerate(options, ctx) {
|
|
|
5760
6405
|
}
|
|
5761
6406
|
async function resolveRepositoryRootForReport(cwd) {
|
|
5762
6407
|
try {
|
|
5763
|
-
return await
|
|
6408
|
+
return await resolveRepositoryRoot9(cwd);
|
|
5764
6409
|
} catch (error) {
|
|
5765
6410
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5766
6411
|
throw new Error(
|
|
@@ -5843,45 +6488,45 @@ async function doRunReviewGaps(options, ctx) {
|
|
|
5843
6488
|
return summary;
|
|
5844
6489
|
}
|
|
5845
6490
|
function relAge(iso, now) {
|
|
5846
|
-
if (iso === null) return "(
|
|
6491
|
+
if (iso === null) return "(unknown)";
|
|
5847
6492
|
const ms = now.getTime() - Date.parse(iso);
|
|
5848
|
-
if (!Number.isFinite(ms) || ms < 0) return "
|
|
6493
|
+
if (!Number.isFinite(ms) || ms < 0) return "just now";
|
|
5849
6494
|
const days = Math.floor(ms / 864e5);
|
|
5850
|
-
if (days >= 1) return `${days}
|
|
6495
|
+
if (days >= 1) return `${days}d ago`;
|
|
5851
6496
|
const hours = Math.floor(ms / 36e5);
|
|
5852
|
-
if (hours >= 1) return `${hours}
|
|
5853
|
-
return `${Math.max(1, Math.floor(ms / 6e4))}
|
|
6497
|
+
if (hours >= 1) return `${hours}h ago`;
|
|
6498
|
+
return `${Math.max(1, Math.floor(ms / 6e4))}m ago`;
|
|
5854
6499
|
}
|
|
5855
6500
|
function unitLine(u, now) {
|
|
5856
6501
|
const when = relAge(u.lastCommitAt, now);
|
|
5857
6502
|
const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
|
|
5858
6503
|
if (u.verdict === "near_unbound") {
|
|
5859
6504
|
const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
|
|
5860
|
-
return `${head} \u2014
|
|
6505
|
+
return `${head} \u2014 a nearby review exists, but the diff / changed files were not examined [${ids}]`;
|
|
5861
6506
|
}
|
|
5862
|
-
return `${head} \u2014
|
|
6507
|
+
return `${head} \u2014 no bound cross-model review`;
|
|
5863
6508
|
}
|
|
5864
6509
|
function candidateLine(u, now) {
|
|
5865
6510
|
const when = relAge(u.lastCommitAt, now);
|
|
5866
6511
|
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
|
|
6512
|
+
return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}`;
|
|
5868
6513
|
}
|
|
5869
6514
|
function renderReviewGaps(summary) {
|
|
5870
6515
|
const now = new Date(summary.generatedAt);
|
|
5871
6516
|
const lines = [];
|
|
5872
|
-
const scope = summary.scope ? summary.scope.join(", ") : "
|
|
5873
|
-
lines.push(`#
|
|
6517
|
+
const scope = summary.scope ? summary.scope.join(", ") : "all repositories";
|
|
6518
|
+
lines.push(`# Review-trail gaps (${scope})`);
|
|
5874
6519
|
lines.push("");
|
|
5875
6520
|
if (summary.gaps.length === 0) {
|
|
5876
|
-
lines.push("\u2705
|
|
6521
|
+
lines.push("\u2705 Within the captured range, no unit of work landed without a review trail.");
|
|
5877
6522
|
} else {
|
|
5878
|
-
lines.push(`\u26A0\uFE0F
|
|
6523
|
+
lines.push(`\u26A0\uFE0F Units of work that landed without a review trail: ${summary.gaps.length}`);
|
|
5879
6524
|
for (const u of summary.gaps) lines.push(unitLine(u, now));
|
|
5880
6525
|
}
|
|
5881
6526
|
lines.push("");
|
|
5882
6527
|
if (summary.candidates.length > 0) {
|
|
5883
6528
|
lines.push(
|
|
5884
|
-
`##
|
|
6529
|
+
`## To confirm (${summary.candidates.length}) \u2014 a cross-model review trace exists; confirm it actually examined this change`
|
|
5885
6530
|
);
|
|
5886
6531
|
for (const u of summary.candidates) lines.push(candidateLine(u, now));
|
|
5887
6532
|
lines.push("");
|
|
@@ -5889,19 +6534,19 @@ function renderReviewGaps(summary) {
|
|
|
5889
6534
|
if (summary.unknowns.length > 0) {
|
|
5890
6535
|
const n = summary.unknowns.reduce((sum, u) => sum + u.commitCount, 0);
|
|
5891
6536
|
lines.push(
|
|
5892
|
-
`##
|
|
6537
|
+
`## 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
6538
|
);
|
|
5894
6539
|
lines.push("");
|
|
5895
6540
|
}
|
|
5896
|
-
lines.push("##
|
|
6541
|
+
lines.push("## By repository");
|
|
5897
6542
|
for (const r of summary.repos) {
|
|
5898
6543
|
lines.push(
|
|
5899
|
-
`- ${r.repo}: ${r.units}
|
|
6544
|
+
`- ${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
6545
|
);
|
|
5901
6546
|
}
|
|
5902
6547
|
lines.push("");
|
|
5903
6548
|
lines.push(
|
|
5904
|
-
|
|
6549
|
+
`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
6550
|
);
|
|
5906
6551
|
return lines.join("\n");
|
|
5907
6552
|
}
|
|
@@ -5925,7 +6570,7 @@ import {
|
|
|
5925
6570
|
readManifest as readManifest7,
|
|
5926
6571
|
readYamlFile as readYamlFile6,
|
|
5927
6572
|
resolveClaudeCodeCommand,
|
|
5928
|
-
resolveRepositoryRoot as
|
|
6573
|
+
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
5929
6574
|
SessionSchema as SessionSchema2,
|
|
5930
6575
|
sanitizeRelatedFiles,
|
|
5931
6576
|
sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
|
|
@@ -6298,7 +6943,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
6298
6943
|
}
|
|
6299
6944
|
async function resolveRepositoryRootForRun(cwd) {
|
|
6300
6945
|
try {
|
|
6301
|
-
return await
|
|
6946
|
+
return await resolveRepositoryRoot10(cwd);
|
|
6302
6947
|
} catch (error) {
|
|
6303
6948
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
6304
6949
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
|
|
@@ -6980,7 +7625,7 @@ import {
|
|
|
6980
7625
|
basouPaths as basouPaths16,
|
|
6981
7626
|
computeWorkStats,
|
|
6982
7627
|
findErrorCode as findErrorCode12,
|
|
6983
|
-
resolveRepositoryRoot as
|
|
7628
|
+
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
6984
7629
|
} from "@basou/core";
|
|
6985
7630
|
function registerStatsCommand(program) {
|
|
6986
7631
|
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 +7728,7 @@ function formatInt(n) {
|
|
|
7083
7728
|
}
|
|
7084
7729
|
async function resolveRepositoryRootForStats(cwd) {
|
|
7085
7730
|
try {
|
|
7086
|
-
return await
|
|
7731
|
+
return await resolveRepositoryRoot11(cwd);
|
|
7087
7732
|
} catch (error) {
|
|
7088
7733
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
7089
7734
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
|
|
@@ -7111,7 +7756,7 @@ import {
|
|
|
7111
7756
|
buildStatusSnapshot,
|
|
7112
7757
|
findErrorCode as findErrorCode13,
|
|
7113
7758
|
readManifest as readManifest9,
|
|
7114
|
-
resolveRepositoryRoot as
|
|
7759
|
+
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
7115
7760
|
writeStatus
|
|
7116
7761
|
} from "@basou/core";
|
|
7117
7762
|
function registerStatusCommand(program) {
|
|
@@ -7167,7 +7812,7 @@ function renderTextStatus(s) {
|
|
|
7167
7812
|
}
|
|
7168
7813
|
async function resolveRepositoryRootForStatus(cwd) {
|
|
7169
7814
|
try {
|
|
7170
|
-
return await
|
|
7815
|
+
return await resolveRepositoryRoot12(cwd);
|
|
7171
7816
|
} catch (error) {
|
|
7172
7817
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
7173
7818
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
|
|
@@ -8304,7 +8949,7 @@ import {
|
|
|
8304
8949
|
basouPaths as basouPaths19,
|
|
8305
8950
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
8306
8951
|
findErrorCode as findErrorCode15,
|
|
8307
|
-
resolveRepositoryRoot as
|
|
8952
|
+
resolveRepositoryRoot as resolveRepositoryRoot13,
|
|
8308
8953
|
resolveSessionId as resolveSessionId5,
|
|
8309
8954
|
verifyEventsChain
|
|
8310
8955
|
} from "@basou/core";
|
|
@@ -8375,7 +9020,7 @@ function renderVerdict(row) {
|
|
|
8375
9020
|
}
|
|
8376
9021
|
async function resolveRepositoryRootForVerify(cwd) {
|
|
8377
9022
|
try {
|
|
8378
|
-
return await
|
|
9023
|
+
return await resolveRepositoryRoot13(cwd);
|
|
8379
9024
|
} catch (error) {
|
|
8380
9025
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
8381
9026
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
@@ -8405,7 +9050,7 @@ import {
|
|
|
8405
9050
|
basouPaths as basouPaths20,
|
|
8406
9051
|
findErrorCode as findErrorCode17,
|
|
8407
9052
|
readManifest as readManifest13,
|
|
8408
|
-
resolveRepositoryRoot as
|
|
9053
|
+
resolveRepositoryRoot as resolveRepositoryRoot14
|
|
8409
9054
|
} from "@basou/core";
|
|
8410
9055
|
import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
8411
9056
|
|
|
@@ -9821,7 +10466,7 @@ function waitForShutdown(signal) {
|
|
|
9821
10466
|
}
|
|
9822
10467
|
async function resolveRepositoryRootForView(cwd) {
|
|
9823
10468
|
try {
|
|
9824
|
-
return await
|
|
10469
|
+
return await resolveRepositoryRoot14(cwd);
|
|
9825
10470
|
} catch (error) {
|
|
9826
10471
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
9827
10472
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|