@bensandee/tooling 0.84.0 → 0.84.1
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/bin.mjs +135 -51
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -269,7 +269,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
|
|
|
269
269
|
*/
|
|
270
270
|
const envFileLoad = loadEnvFileForBin(process.argv.slice(2), {
|
|
271
271
|
name: "bst",
|
|
272
|
-
version: "0.84.
|
|
272
|
+
version: "0.84.1"
|
|
273
273
|
});
|
|
274
274
|
//#endregion
|
|
275
275
|
//#region ../cli-kit/src/logger.ts
|
|
@@ -7163,7 +7163,7 @@ function buildManagedDevDeps(ctx) {
|
|
|
7163
7163
|
const devDeps = { ...rootDevDeps(ctx.config) };
|
|
7164
7164
|
if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
|
|
7165
7165
|
if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.14.5";
|
|
7166
|
-
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.84.
|
|
7166
|
+
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.84.1";
|
|
7167
7167
|
if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.15.2";
|
|
7168
7168
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
7169
7169
|
"@changesets/cli": "3.0.3",
|
|
@@ -11950,6 +11950,10 @@ const GIT_GUIDE_BODY = [
|
|
|
11950
11950
|
"",
|
|
11951
11951
|
"Start a pull request's title with the stage it carries: `Spec:` for a spec, `Plan:` for a plan, and no stem once it carries the implementation. A draft keeps its `WIP:` marker in front (`WIP: Plan: Add retry to uploads`). When a pull request moves on to the next stage, move the stem with it. This is informational -- nothing checks it and nothing fails without it -- but it tells a reviewer at a glance what they are reading.",
|
|
11952
11952
|
"",
|
|
11953
|
+
"**Drop `WIP:` and fix the stem when the work is done.** Once the pull request carries finished work you are handing over for review -- a spec or a plan ready to approve, or an implementation whose check passes -- remove the `WIP:` marker and make the stem match what it now carries: no stem for an implementation. Do it in the same step that reports the work done, without being asked. A marker left behind tells a reviewer the work is still moving.",
|
|
11954
|
+
"",
|
|
11955
|
+
"`pr:update` never changes a pull request's draft state, so it keeps `WIP:` on a retitle. On Forgejo a draft is only that title prefix: retitle the pull request without it (`bst-fj issue:update <n> --title \"<title>\"` where `bst-fj` is installed). On GitHub, run `gh pr ready` and then edit the title.",
|
|
11956
|
+
"",
|
|
11953
11957
|
"## Merging the default branch in",
|
|
11954
11958
|
"",
|
|
11955
11959
|
"**A merge with no conflicts is not yet a merge that works.** Read the incoming diff for what your branch builds on -- a field made required, a shared schema, a size budget -- and run the full check before you push the merge. Fix what it finds in the merge commit itself, so that commit passes on its own.",
|
|
@@ -12692,7 +12696,7 @@ function reportFailures(failed, command) {
|
|
|
12692
12696
|
function generateMigratePrompt(results, config, detected) {
|
|
12693
12697
|
const sections = [];
|
|
12694
12698
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12695
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.84.
|
|
12699
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.84.1 repo:sync\` on ${timestamp}_\n`);
|
|
12696
12700
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
12697
12701
|
if (changed.length > 0) {
|
|
12698
12702
|
const items = changed.map((r) => {
|
|
@@ -27877,13 +27881,39 @@ function normalizeIssue(raw) {
|
|
|
27877
27881
|
};
|
|
27878
27882
|
}
|
|
27879
27883
|
/**
|
|
27884
|
+
* Parse the backing issue number out of a PR body's `Closes #<n>` line. Returns null when the
|
|
27885
|
+
* body carries no such line (a hand-written PR, or a body that could not be read). Lives beside
|
|
27886
|
+
* {@link toIssueSummaries}, which keeps it for every pull request a search returns.
|
|
27887
|
+
*
|
|
27888
|
+
* `Closes` only, deliberately -- narrower than the `Closes`/`Fixes`/`Resolves` vocabulary the
|
|
27889
|
+
* closing-ref machinery in `closing-refs.ts` recognizes, and it must stay
|
|
27890
|
+
* that way. A body whose *first* closing reference names some other issue is what a caller
|
|
27891
|
+
* resolves the backing issue against, and a branch name derived from that number follows it, so
|
|
27892
|
+
* every keyword added here widens the set of bodies that can be misrouted. The
|
|
27893
|
+
* cost of the narrow set is the opposite failure and the cheaper one: an adopted body already
|
|
27894
|
+
* saying `Fixes #<n>` gets no `Closes` line appended (it already closes the issue), so this
|
|
27895
|
+
* returns null and the PR is treated as having no backing issue -- which still closes its issue
|
|
27896
|
+
* on merge and still runs on the PR's own head ref, losing only the issue mapping.
|
|
27897
|
+
*/
|
|
27898
|
+
function backingIssueNumber(prBody) {
|
|
27899
|
+
const match = /Closes #(\d+)/i.exec(prBody);
|
|
27900
|
+
if (match === null) return null;
|
|
27901
|
+
const parsed = Number(match[1]);
|
|
27902
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
27903
|
+
}
|
|
27904
|
+
/**
|
|
27880
27905
|
* Normalize a raw issue list, drop pull requests (both platforms serve them
|
|
27881
27906
|
* through the issues endpoint), and strip bodies down to summaries. Shared by
|
|
27882
27907
|
* the Forgejo and GitHub list transports so the post-processing chain lives
|
|
27883
27908
|
* in exactly one place.
|
|
27884
27909
|
*/
|
|
27885
27910
|
function toIssueSummaries(raw, type = "issues") {
|
|
27886
|
-
const summaries = raw.map(normalizeIssue).map(({ body
|
|
27911
|
+
const summaries = raw.map(normalizeIssue).map(({ body, ...summary }) => {
|
|
27912
|
+
const result = summary;
|
|
27913
|
+
const backingIssue = summary.isPullRequest ? backingIssueNumber(body) : null;
|
|
27914
|
+
if (backingIssue !== null) result.backingIssue = backingIssue;
|
|
27915
|
+
return result;
|
|
27916
|
+
});
|
|
27887
27917
|
return type === "issues" ? summaries.filter((s) => !s.isPullRequest) : summaries;
|
|
27888
27918
|
}
|
|
27889
27919
|
/** Fetch a single issue by number. */
|
|
@@ -30533,6 +30563,71 @@ function writeReviewPackage(planPath, base, head, cwd, out) {
|
|
|
30533
30563
|
};
|
|
30534
30564
|
}
|
|
30535
30565
|
//#endregion
|
|
30566
|
+
//#region src/plan-workspace/task-label.ts
|
|
30567
|
+
/** The line shapes `plan:brief` and `plan:status` both read a task's number from. */
|
|
30568
|
+
const HEADING = /^(#{1,6})[ \t]+(.*)$/;
|
|
30569
|
+
const BOX = /^([ \t]*)[-*] \[[ xX]\][ \t]*(.*)$/;
|
|
30570
|
+
const LABEL = /^Task[ \t]+([0-9]+)(?![0-9])/;
|
|
30571
|
+
/** A line that opens a fence: three or more of one character, after any amount of whitespace. */
|
|
30572
|
+
const FENCE_OPEN = /^[ \t]*(`{3,}|~{3,})/;
|
|
30573
|
+
/** The same shape with nothing but whitespace after the run, which is what closes a fence. */
|
|
30574
|
+
const FENCE_CLOSE = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
|
|
30575
|
+
/** Whether a line closes the fence opened by `fenceChar` repeated `fenceLength` times. */
|
|
30576
|
+
function closesFence(text, fenceChar, fenceLength) {
|
|
30577
|
+
const run = FENCE_CLOSE.exec(text)?.[1];
|
|
30578
|
+
if (run === void 0) return false;
|
|
30579
|
+
return run[0] === fenceChar && run.length >= fenceLength;
|
|
30580
|
+
}
|
|
30581
|
+
/**
|
|
30582
|
+
* Which lines of a document a rendered markdown viewer would show as code rather than parse as
|
|
30583
|
+
* markdown: true at every fence line and every line inside one, false elsewhere. The rules are
|
|
30584
|
+
* CommonMark's, with two choices that belong to these readers:
|
|
30585
|
+
*
|
|
30586
|
+
* - Any indentation opens a fence, not the 0-3 spaces CommonMark allows, because plans nest
|
|
30587
|
+
* example fences under list items and none of the readers tracks list nesting.
|
|
30588
|
+
* - An unclosed fence runs to the end of the document, which is how a renderer shows one.
|
|
30589
|
+
*
|
|
30590
|
+
* A closer is the opener's own character repeated at least as many times, with nothing after it
|
|
30591
|
+
* but whitespace; a run of the other character, a shorter run, or a run carrying an info string is
|
|
30592
|
+
* code content and keeps the fence open.
|
|
30593
|
+
*
|
|
30594
|
+
* `stripFencedCodeBlocks` in `docs-check/links.ts` walks fences by the same rule but is a
|
|
30595
|
+
* different reader with different rules (it limits the opener's indent and tracks blockquote
|
|
30596
|
+
* depth), which is why this walk is not shared with it.
|
|
30597
|
+
*/
|
|
30598
|
+
function fencedLines(lines) {
|
|
30599
|
+
const fenced = lines.map(() => false);
|
|
30600
|
+
let fenceChar;
|
|
30601
|
+
let fenceLength = 0;
|
|
30602
|
+
for (let index = 0; index < lines.length; index++) {
|
|
30603
|
+
const text = lines[index] ?? "";
|
|
30604
|
+
if (fenceChar === void 0) {
|
|
30605
|
+
const run = FENCE_OPEN.exec(text)?.[1];
|
|
30606
|
+
if (run === void 0) continue;
|
|
30607
|
+
fenceChar = run[0];
|
|
30608
|
+
fenceLength = run.length;
|
|
30609
|
+
fenced[index] = true;
|
|
30610
|
+
continue;
|
|
30611
|
+
}
|
|
30612
|
+
fenced[index] = true;
|
|
30613
|
+
if (closesFence(text, fenceChar, fenceLength)) {
|
|
30614
|
+
fenceChar = void 0;
|
|
30615
|
+
fenceLength = 0;
|
|
30616
|
+
}
|
|
30617
|
+
}
|
|
30618
|
+
return fenced;
|
|
30619
|
+
}
|
|
30620
|
+
/**
|
|
30621
|
+
* The `<n>` of a heading or checkbox text that starts `Task <n>` (after an opening `**` or `__`),
|
|
30622
|
+
* or null. A number with a leading zero reads as null, because `plan:brief <n>` matches the digits
|
|
30623
|
+
* as written and could never find `Task 05`.
|
|
30624
|
+
*/
|
|
30625
|
+
function taskLabelNumber(text) {
|
|
30626
|
+
const digits = LABEL.exec(text.replace(/^(\*\*|__)/, ""))?.[1];
|
|
30627
|
+
if (digits === void 0 || String(Number(digits)) !== digits) return null;
|
|
30628
|
+
return Number(digits);
|
|
30629
|
+
}
|
|
30630
|
+
//#endregion
|
|
30536
30631
|
//#region src/plan-workspace/plan-boxes.ts
|
|
30537
30632
|
/**
|
|
30538
30633
|
* Read a plan document's checkboxes and the notes committed under them. One strict parser, fail-
|
|
@@ -30545,8 +30640,14 @@ function writeReviewPackage(planPath, base, head, cwd, out) {
|
|
|
30545
30640
|
* boundary matters because a batched plan is a sequence of `### <title>`
|
|
30546
30641
|
* sections and a note must never leak across one.
|
|
30547
30642
|
*
|
|
30548
|
-
*
|
|
30549
|
-
*
|
|
30643
|
+
* A line inside a fenced code block (`fencedLines`, `task-label.ts`) is invisible in every role:
|
|
30644
|
+
* a fenced box is not counted, a fenced heading does not end a block, and a fenced note or
|
|
30645
|
+
* declination is not one. The fail-safe direction holds -- an ambiguous line degrades to "not
|
|
30646
|
+
* declined".
|
|
30647
|
+
*
|
|
30648
|
+
* The agent imports this module (`run-outcome.ts`, `run-gates.ts`, `run.ts`, `plan-box-diff.ts`,
|
|
30649
|
+
* `plan-box-watch.ts`, `plan-box-walk.ts`, `plan-progress.ts`), so a change here changes how the
|
|
30650
|
+
* agent reads a plan as well as what `plan:status` reports.
|
|
30550
30651
|
*
|
|
30551
30652
|
* An unchecked box is **declined** when its block carries a line matching
|
|
30552
30653
|
* `^[ \t]*(?:[-*][ \t]+)?\*\*Declined:\*\*[ \t]*(.*)$` whose trimmed remainder is non-empty; that
|
|
@@ -30599,8 +30700,10 @@ function unwrapEmphasis(rawTitle) {
|
|
|
30599
30700
|
/** Parse every checkbox on a plan document, each with its own block already resolved. */
|
|
30600
30701
|
function parsePlanBoxes(planText) {
|
|
30601
30702
|
const lines = planText.split("\n");
|
|
30703
|
+
const fenced = fencedLines(lines);
|
|
30602
30704
|
const boxStarts = [];
|
|
30603
30705
|
for (let index = 0; index < lines.length; index++) {
|
|
30706
|
+
if (fenced[index] === true) continue;
|
|
30604
30707
|
const match = BOX_LINE_REGEX.exec(lines[index] ?? "");
|
|
30605
30708
|
if (match === null) continue;
|
|
30606
30709
|
boxStarts.push({
|
|
@@ -30612,13 +30715,17 @@ function parsePlanBoxes(planText) {
|
|
|
30612
30715
|
return boxStarts.map((box, position) => {
|
|
30613
30716
|
const nextBoxIndex = boxStarts[position + 1]?.index ?? lines.length;
|
|
30614
30717
|
let blockEnd = nextBoxIndex;
|
|
30615
|
-
for (let index = box.index + 1; index < nextBoxIndex; index++)
|
|
30616
|
-
|
|
30617
|
-
|
|
30718
|
+
for (let index = box.index + 1; index < nextBoxIndex; index++) {
|
|
30719
|
+
if (fenced[index] === true) continue;
|
|
30720
|
+
if (HEADING_REGEX.test(lines[index] ?? "")) {
|
|
30721
|
+
blockEnd = index;
|
|
30722
|
+
break;
|
|
30723
|
+
}
|
|
30618
30724
|
}
|
|
30619
30725
|
const title = unwrapEmphasis(box.rawTitle);
|
|
30620
30726
|
const retireTask = RETIRE_TASK_REGEX.test(title);
|
|
30621
30727
|
if (box.checked) return {
|
|
30728
|
+
line: box.index,
|
|
30622
30729
|
checked: true,
|
|
30623
30730
|
declined: false,
|
|
30624
30731
|
reason: null,
|
|
@@ -30628,6 +30735,7 @@ function parsePlanBoxes(planText) {
|
|
|
30628
30735
|
};
|
|
30629
30736
|
const notes = [];
|
|
30630
30737
|
for (let index = box.index + 1; index < blockEnd; index++) {
|
|
30738
|
+
if (fenced[index] === true) continue;
|
|
30631
30739
|
const noteMatch = NOTE_LINE_REGEX.exec(lines[index] ?? "");
|
|
30632
30740
|
if (noteMatch === null) continue;
|
|
30633
30741
|
const text = (noteMatch[2] ?? "").trim();
|
|
@@ -30638,6 +30746,7 @@ function parsePlanBoxes(planText) {
|
|
|
30638
30746
|
}
|
|
30639
30747
|
let reason = null;
|
|
30640
30748
|
for (let index = box.index + 1; index < blockEnd; index++) {
|
|
30749
|
+
if (fenced[index] === true) continue;
|
|
30641
30750
|
const declinedMatch = DECLINED_LINE_REGEX.exec(lines[index] ?? "");
|
|
30642
30751
|
if (declinedMatch === null) continue;
|
|
30643
30752
|
const remainder = (declinedMatch[1] ?? "").trim();
|
|
@@ -30647,6 +30756,7 @@ function parsePlanBoxes(planText) {
|
|
|
30647
30756
|
}
|
|
30648
30757
|
}
|
|
30649
30758
|
return {
|
|
30759
|
+
line: box.index,
|
|
30650
30760
|
checked: false,
|
|
30651
30761
|
declined: reason !== null,
|
|
30652
30762
|
reason,
|
|
@@ -30657,45 +30767,23 @@ function parsePlanBoxes(planText) {
|
|
|
30657
30767
|
});
|
|
30658
30768
|
}
|
|
30659
30769
|
//#endregion
|
|
30660
|
-
//#region src/plan-workspace/task-label.ts
|
|
30661
|
-
/** The line shapes `plan:brief` and `plan:status` both read a task's number from. */
|
|
30662
|
-
const FENCE = /^[ \t]*(```|~~~)/;
|
|
30663
|
-
const HEADING = /^(#{1,6})[ \t]+(.*)$/;
|
|
30664
|
-
const BOX = /^([ \t]*)[-*] \[[ xX]\][ \t]*(.*)$/;
|
|
30665
|
-
const LABEL = /^Task[ \t]+([0-9]+)(?![0-9])/;
|
|
30666
|
-
/**
|
|
30667
|
-
* The `<n>` of a heading or checkbox text that starts `Task <n>` (after an opening `**` or `__`),
|
|
30668
|
-
* or null. A number with a leading zero reads as null, because `plan:brief <n>` matches the digits
|
|
30669
|
-
* as written and could never find `Task 05`.
|
|
30670
|
-
*/
|
|
30671
|
-
function taskLabelNumber(text) {
|
|
30672
|
-
const digits = LABEL.exec(text.replace(/^(\*\*|__)/, ""))?.[1];
|
|
30673
|
-
if (digits === void 0 || String(Number(digits)) !== digits) return null;
|
|
30674
|
-
return Number(digits);
|
|
30675
|
-
}
|
|
30676
|
-
//#endregion
|
|
30677
30770
|
//#region src/plan-workspace/status.ts
|
|
30678
30771
|
const RETIRED_SEGMENT = /(^|\/)retired\//;
|
|
30679
30772
|
/**
|
|
30680
|
-
* The nearest `Task <n>` label at or before the
|
|
30681
|
-
*
|
|
30682
|
-
*
|
|
30773
|
+
* The nearest `Task <n>` label at or before the box on `line`, the box's own title winning. The
|
|
30774
|
+
* walk shares `fencedLines` with `plan:brief`, so a fenced `Task <n>` line is no more a label here
|
|
30775
|
+
* than it is a task boundary there.
|
|
30683
30776
|
*/
|
|
30684
|
-
function enclosingTaskNumber(planText,
|
|
30685
|
-
|
|
30686
|
-
|
|
30777
|
+
function enclosingTaskNumber(planText, line) {
|
|
30778
|
+
const lines = planText.split("\n");
|
|
30779
|
+
const fenced = fencedLines(lines);
|
|
30687
30780
|
let label = null;
|
|
30688
|
-
for (
|
|
30689
|
-
const
|
|
30690
|
-
if (
|
|
30691
|
-
if (
|
|
30692
|
-
|
|
30693
|
-
|
|
30694
|
-
continue;
|
|
30695
|
-
}
|
|
30696
|
-
if (inFence) continue;
|
|
30697
|
-
const text = box?.[2] ?? HEADING.exec(line)?.[2];
|
|
30698
|
-
const n = text === void 0 ? null : taskLabelNumber(text);
|
|
30781
|
+
for (let index = 0; index < lines.length; index++) {
|
|
30782
|
+
const text = lines[index] ?? "";
|
|
30783
|
+
if (index === line) return taskLabelNumber(BOX.exec(text)?.[2] ?? "") ?? label;
|
|
30784
|
+
if (fenced[index] === true) continue;
|
|
30785
|
+
const title = BOX.exec(text)?.[2] ?? HEADING.exec(text)?.[2];
|
|
30786
|
+
const n = title === void 0 ? null : taskLabelNumber(title);
|
|
30699
30787
|
if (n !== null) label = n;
|
|
30700
30788
|
}
|
|
30701
30789
|
return null;
|
|
@@ -30718,7 +30806,7 @@ function planStatus(planText, planPath) {
|
|
|
30718
30806
|
outstanding: open.length,
|
|
30719
30807
|
declined: boxes.filter((box) => box.declined).length,
|
|
30720
30808
|
next: nextBox === void 0 || nextBox.title === "" ? null : nextBox.title,
|
|
30721
|
-
nextTask: nextBox === void 0 ? null : enclosingTaskNumber(planText,
|
|
30809
|
+
nextTask: nextBox === void 0 ? null : enclosingTaskNumber(planText, nextBox.line),
|
|
30722
30810
|
onlyRetireLeft: open.length > 0 && open.every((box) => box.retireTask),
|
|
30723
30811
|
notes
|
|
30724
30812
|
};
|
|
@@ -30735,17 +30823,13 @@ function isTaskLabel(text, n) {
|
|
|
30735
30823
|
*/
|
|
30736
30824
|
function extractTask(planText, n) {
|
|
30737
30825
|
const lines = planText.split("\n");
|
|
30738
|
-
|
|
30826
|
+
const fenced = fencedLines(lines);
|
|
30739
30827
|
let start = -1;
|
|
30740
30828
|
let shape = null;
|
|
30741
30829
|
let end = lines.length;
|
|
30742
30830
|
for (let index = 0; index < lines.length; index++) {
|
|
30831
|
+
if (fenced[index] === true) continue;
|
|
30743
30832
|
const line = lines[index] ?? "";
|
|
30744
|
-
if (FENCE.test(line)) {
|
|
30745
|
-
inFence = !inFence;
|
|
30746
|
-
continue;
|
|
30747
|
-
}
|
|
30748
|
-
if (inFence) continue;
|
|
30749
30833
|
const heading = HEADING.exec(line);
|
|
30750
30834
|
const box = BOX.exec(line);
|
|
30751
30835
|
if (shape === null) {
|
|
@@ -31171,7 +31255,7 @@ setForgejoLogger(log$1);
|
|
|
31171
31255
|
* `local <sha>[-dirty]` otherwise -- so an artifact built from the working tree
|
|
31172
31256
|
* can't pass itself off as the published release whose version it carries.
|
|
31173
31257
|
*/
|
|
31174
|
-
const displayVersion = "0.84.
|
|
31258
|
+
const displayVersion = "0.84.1";
|
|
31175
31259
|
/**
|
|
31176
31260
|
* The registered `bst` subcommand map, exported so both the JSON entry path below (resolving a
|
|
31177
31261
|
* command name before citty parses anything) and the coverage test (enumerating every
|