@bensandee/tooling 0.58.0 → 0.59.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 +223 -194
- package/dist/docker-check/index.d.mts +121 -1
- package/dist/docker-check/index.mjs +2 -2
- package/dist/index.d.mts +1 -118
- package/dist/{check-B5DFYJ3x.mjs → project-name-CAVRoqNy.mjs} +172 -2
- package/dist/types-DtwkblrK.d.mts +119 -0
- package/package.json +2 -2
package/dist/bin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { d as
|
|
2
|
+
import { _ as log, d as isExecSyncError, f as lintComposeFiles, g as redact, h as isEnvVerbose, m as debugExec, n as runDockerCheck, p as debug, t as deriveProjectName, u as createRealExecutor$1, v as note } from "./project-name-CAVRoqNy.mjs";
|
|
3
3
|
import { defineCommand, runMain } from "citty";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import { isCancel, select } from "@clack/prompts";
|
|
@@ -10,7 +10,7 @@ import JSON5 from "json5";
|
|
|
10
10
|
import { parse } from "jsonc-parser";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
13
|
-
import { isMap, isScalar, parse as parse$1, parseDocument, stringify, visit } from "yaml";
|
|
13
|
+
import { isMap, isScalar, isSeq, parse as parse$1, parseDocument, stringify, visit } from "yaml";
|
|
14
14
|
import picomatch from "picomatch";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { createHmac } from "node:crypto";
|
|
@@ -1330,6 +1330,11 @@ function buildWorkflowYaml(options) {
|
|
|
1330
1330
|
function actionsExpr(expr) {
|
|
1331
1331
|
return `\${{ ${expr} }}`;
|
|
1332
1332
|
}
|
|
1333
|
+
const MAIN_BRANCH_REF = "refs/heads/main";
|
|
1334
|
+
/** `if:` condition that gates a step to push events on the default branch. */
|
|
1335
|
+
const IS_MAIN_BRANCH = `github.ref == '${MAIN_BRANCH_REF}'`;
|
|
1336
|
+
/** Negated form for concurrency `cancel-in-progress` and similar uses. */
|
|
1337
|
+
const IS_NOT_MAIN_BRANCH = `github.ref != '${MAIN_BRANCH_REF}'`;
|
|
1333
1338
|
function hasEnginesNode(ctx) {
|
|
1334
1339
|
const raw = ctx.read("package.json");
|
|
1335
1340
|
if (!raw) return false;
|
|
@@ -1473,7 +1478,7 @@ function hasReleaseAssets(ctx) {
|
|
|
1473
1478
|
return false;
|
|
1474
1479
|
}
|
|
1475
1480
|
}
|
|
1476
|
-
const ARCHIVE_DIR$
|
|
1481
|
+
const ARCHIVE_DIR$2 = ".tooling-archived";
|
|
1477
1482
|
async function generateDeployCi(ctx) {
|
|
1478
1483
|
const filePath = "deploy-ci";
|
|
1479
1484
|
if (!ctx.config.publishDocker || ctx.config.ci === "none") return {
|
|
@@ -1486,12 +1491,12 @@ async function generateDeployCi(ctx) {
|
|
|
1486
1491
|
if (ctx.config.releaseStrategy === "simple") {
|
|
1487
1492
|
const existing = ctx.read(workflowPath);
|
|
1488
1493
|
if (existing !== void 0) {
|
|
1489
|
-
ctx.write(`${ARCHIVE_DIR$
|
|
1494
|
+
ctx.write(`${ARCHIVE_DIR$2}/${workflowPath}`, existing);
|
|
1490
1495
|
ctx.remove(workflowPath);
|
|
1491
1496
|
return {
|
|
1492
1497
|
filePath: workflowPath,
|
|
1493
1498
|
action: "archived",
|
|
1494
|
-
description: `Moved to ${ARCHIVE_DIR$
|
|
1499
|
+
description: `Moved to ${ARCHIVE_DIR$2}/${workflowPath} (docker publish runs in release.yml for simple strategy)`
|
|
1495
1500
|
};
|
|
1496
1501
|
}
|
|
1497
1502
|
return {
|
|
@@ -1593,7 +1598,7 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1593
1598
|
"tsdown": "0.22.0",
|
|
1594
1599
|
"typescript": "6.0.3",
|
|
1595
1600
|
"vitest": "4.1.5",
|
|
1596
|
-
"pnpm": "11.1.
|
|
1601
|
+
"pnpm": "11.1.2"
|
|
1597
1602
|
}["@types/node"],
|
|
1598
1603
|
tsdown: {
|
|
1599
1604
|
"@changesets/cli": "2.31.0",
|
|
@@ -1609,7 +1614,7 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1609
1614
|
"tsdown": "0.22.0",
|
|
1610
1615
|
"typescript": "6.0.3",
|
|
1611
1616
|
"vitest": "4.1.5",
|
|
1612
|
-
"pnpm": "11.1.
|
|
1617
|
+
"pnpm": "11.1.2"
|
|
1613
1618
|
}["tsdown"],
|
|
1614
1619
|
typescript: {
|
|
1615
1620
|
"@changesets/cli": "2.31.0",
|
|
@@ -1625,7 +1630,7 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1625
1630
|
"tsdown": "0.22.0",
|
|
1626
1631
|
"typescript": "6.0.3",
|
|
1627
1632
|
"vitest": "4.1.5",
|
|
1628
|
-
"pnpm": "11.1.
|
|
1633
|
+
"pnpm": "11.1.2"
|
|
1629
1634
|
}["typescript"],
|
|
1630
1635
|
vitest: {
|
|
1631
1636
|
"@changesets/cli": "2.31.0",
|
|
@@ -1641,7 +1646,7 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1641
1646
|
"tsdown": "0.22.0",
|
|
1642
1647
|
"typescript": "6.0.3",
|
|
1643
1648
|
"vitest": "4.1.5",
|
|
1644
|
-
"pnpm": "11.1.
|
|
1649
|
+
"pnpm": "11.1.2"
|
|
1645
1650
|
}["vitest"]
|
|
1646
1651
|
};
|
|
1647
1652
|
/** DevDeps that belong at the root regardless of structure. */
|
|
@@ -1660,7 +1665,7 @@ const ROOT_DEV_DEPS = {
|
|
|
1660
1665
|
"tsdown": "0.22.0",
|
|
1661
1666
|
"typescript": "6.0.3",
|
|
1662
1667
|
"vitest": "4.1.5",
|
|
1663
|
-
"pnpm": "11.1.
|
|
1668
|
+
"pnpm": "11.1.2"
|
|
1664
1669
|
}["knip"],
|
|
1665
1670
|
lefthook: {
|
|
1666
1671
|
"@changesets/cli": "2.31.0",
|
|
@@ -1676,7 +1681,7 @@ const ROOT_DEV_DEPS = {
|
|
|
1676
1681
|
"tsdown": "0.22.0",
|
|
1677
1682
|
"typescript": "6.0.3",
|
|
1678
1683
|
"vitest": "4.1.5",
|
|
1679
|
-
"pnpm": "11.1.
|
|
1684
|
+
"pnpm": "11.1.2"
|
|
1680
1685
|
}["lefthook"],
|
|
1681
1686
|
oxlint: {
|
|
1682
1687
|
"@changesets/cli": "2.31.0",
|
|
@@ -1692,7 +1697,7 @@ const ROOT_DEV_DEPS = {
|
|
|
1692
1697
|
"tsdown": "0.22.0",
|
|
1693
1698
|
"typescript": "6.0.3",
|
|
1694
1699
|
"vitest": "4.1.5",
|
|
1695
|
-
"pnpm": "11.1.
|
|
1700
|
+
"pnpm": "11.1.2"
|
|
1696
1701
|
}["oxlint"]
|
|
1697
1702
|
};
|
|
1698
1703
|
/**
|
|
@@ -1733,7 +1738,7 @@ function addReleaseDeps(deps, config) {
|
|
|
1733
1738
|
"tsdown": "0.22.0",
|
|
1734
1739
|
"typescript": "6.0.3",
|
|
1735
1740
|
"vitest": "4.1.5",
|
|
1736
|
-
"pnpm": "11.1.
|
|
1741
|
+
"pnpm": "11.1.2"
|
|
1737
1742
|
}["release-it"];
|
|
1738
1743
|
if (config.structure === "monorepo") deps["@release-it/bumper"] = {
|
|
1739
1744
|
"@changesets/cli": "2.31.0",
|
|
@@ -1749,7 +1754,7 @@ function addReleaseDeps(deps, config) {
|
|
|
1749
1754
|
"tsdown": "0.22.0",
|
|
1750
1755
|
"typescript": "6.0.3",
|
|
1751
1756
|
"vitest": "4.1.5",
|
|
1752
|
-
"pnpm": "11.1.
|
|
1757
|
+
"pnpm": "11.1.2"
|
|
1753
1758
|
}["@release-it/bumper"];
|
|
1754
1759
|
break;
|
|
1755
1760
|
case "simple":
|
|
@@ -1767,7 +1772,7 @@ function addReleaseDeps(deps, config) {
|
|
|
1767
1772
|
"tsdown": "0.22.0",
|
|
1768
1773
|
"typescript": "6.0.3",
|
|
1769
1774
|
"vitest": "4.1.5",
|
|
1770
|
-
"pnpm": "11.1.
|
|
1775
|
+
"pnpm": "11.1.2"
|
|
1771
1776
|
}["commit-and-tag-version"];
|
|
1772
1777
|
break;
|
|
1773
1778
|
case "changesets":
|
|
@@ -1785,7 +1790,7 @@ function addReleaseDeps(deps, config) {
|
|
|
1785
1790
|
"tsdown": "0.22.0",
|
|
1786
1791
|
"typescript": "6.0.3",
|
|
1787
1792
|
"vitest": "4.1.5",
|
|
1788
|
-
"pnpm": "11.1.
|
|
1793
|
+
"pnpm": "11.1.2"
|
|
1789
1794
|
}["@changesets/cli"];
|
|
1790
1795
|
break;
|
|
1791
1796
|
}
|
|
@@ -1808,8 +1813,8 @@ async function generatePackageJson(ctx) {
|
|
|
1808
1813
|
}
|
|
1809
1814
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1810
1815
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1811
|
-
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.11.
|
|
1812
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1816
|
+
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.11.2";
|
|
1817
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.59.1";
|
|
1813
1818
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.13.0";
|
|
1814
1819
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1815
1820
|
"@changesets/cli": "2.31.0",
|
|
@@ -1825,7 +1830,7 @@ async function generatePackageJson(ctx) {
|
|
|
1825
1830
|
"tsdown": "0.22.0",
|
|
1826
1831
|
"typescript": "6.0.3",
|
|
1827
1832
|
"vitest": "4.1.5",
|
|
1828
|
-
"pnpm": "11.1.
|
|
1833
|
+
"pnpm": "11.1.2"
|
|
1829
1834
|
}["oxfmt"];
|
|
1830
1835
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
|
|
1831
1836
|
"@changesets/cli": "2.31.0",
|
|
@@ -1841,7 +1846,7 @@ async function generatePackageJson(ctx) {
|
|
|
1841
1846
|
"tsdown": "0.22.0",
|
|
1842
1847
|
"typescript": "6.0.3",
|
|
1843
1848
|
"vitest": "4.1.5",
|
|
1844
|
-
"pnpm": "11.1.
|
|
1849
|
+
"pnpm": "11.1.2"
|
|
1845
1850
|
}["prettier"];
|
|
1846
1851
|
addReleaseDeps(devDeps, ctx.config);
|
|
1847
1852
|
if (existing) {
|
|
@@ -1913,7 +1918,7 @@ async function generatePackageJson(ctx) {
|
|
|
1913
1918
|
"tsdown": "0.22.0",
|
|
1914
1919
|
"typescript": "6.0.3",
|
|
1915
1920
|
"vitest": "4.1.5",
|
|
1916
|
-
"pnpm": "11.1.
|
|
1921
|
+
"pnpm": "11.1.2"
|
|
1917
1922
|
}["pnpm"];
|
|
1918
1923
|
const desiredPm = pinnedPnpm ? `pnpm@${pinnedPnpm}` : void 0;
|
|
1919
1924
|
if (desiredPm) {
|
|
@@ -1965,7 +1970,7 @@ async function generatePackageJson(ctx) {
|
|
|
1965
1970
|
"tsdown": "0.22.0",
|
|
1966
1971
|
"typescript": "6.0.3",
|
|
1967
1972
|
"vitest": "4.1.5",
|
|
1968
|
-
"pnpm": "11.1.
|
|
1973
|
+
"pnpm": "11.1.2"
|
|
1969
1974
|
}["pnpm"]}`
|
|
1970
1975
|
};
|
|
1971
1976
|
ctx.write(filePath, JSON.stringify(pkg, null, 2) + "\n");
|
|
@@ -3385,7 +3390,7 @@ function hookSecretEnv(secrets) {
|
|
|
3385
3390
|
}
|
|
3386
3391
|
/** Step that runs after release with post-release webhook env wired up. */
|
|
3387
3392
|
function postReleaseHookStep(opts) {
|
|
3388
|
-
const ifCondition = opts?.condition ?? (opts?.guardMain ?
|
|
3393
|
+
const ifCondition = opts?.condition ?? (opts?.guardMain ? IS_MAIN_BRANCH : void 0);
|
|
3389
3394
|
return { step: {
|
|
3390
3395
|
name: "Post-release hook",
|
|
3391
3396
|
...ifCondition && { if: ifCondition },
|
|
@@ -3566,12 +3571,31 @@ async function generateReleaseCi(ctx) {
|
|
|
3566
3571
|
}
|
|
3567
3572
|
//#endregion
|
|
3568
3573
|
//#region src/generators/ci.ts
|
|
3574
|
+
function coverageHistorySteps() {
|
|
3575
|
+
return [
|
|
3576
|
+
{ step: {
|
|
3577
|
+
name: "Print coverage summary",
|
|
3578
|
+
if: IS_MAIN_BRANCH,
|
|
3579
|
+
run: "pnpm exec bst coverage:summary"
|
|
3580
|
+
} },
|
|
3581
|
+
{ step: {
|
|
3582
|
+
name: "Append record to bst/coverage-metrics branch",
|
|
3583
|
+
if: IS_MAIN_BRANCH,
|
|
3584
|
+
run: "pnpm exec bst coverage:record"
|
|
3585
|
+
} },
|
|
3586
|
+
{ step: {
|
|
3587
|
+
name: "Show recent coverage trend",
|
|
3588
|
+
if: IS_MAIN_BRANCH,
|
|
3589
|
+
run: "pnpm exec bst coverage:status --limit 10"
|
|
3590
|
+
} }
|
|
3591
|
+
];
|
|
3592
|
+
}
|
|
3569
3593
|
/** Build the release step for the check job (changesets strategy). */
|
|
3570
3594
|
function changesetsReleaseStep(ci, publishesNpm) {
|
|
3571
3595
|
if (ci === "github") return { step: {
|
|
3572
3596
|
id: "release",
|
|
3573
3597
|
uses: "changesets/action@v1",
|
|
3574
|
-
if:
|
|
3598
|
+
if: IS_MAIN_BRANCH,
|
|
3575
3599
|
with: {
|
|
3576
3600
|
publish: "pnpm changeset publish",
|
|
3577
3601
|
version: "pnpm changeset version"
|
|
@@ -3596,9 +3620,11 @@ function changesetsReleaseStep(ci, publishesNpm) {
|
|
|
3596
3620
|
}
|
|
3597
3621
|
const CI_CONCURRENCY = {
|
|
3598
3622
|
group: `ci-${actionsExpr("github.ref")}`,
|
|
3599
|
-
"cancel-in-progress": actionsExpr(
|
|
3623
|
+
"cancel-in-progress": actionsExpr(IS_NOT_MAIN_BRANCH)
|
|
3600
3624
|
};
|
|
3601
|
-
function checkSteps(nodeVersionYaml, publishesNpm) {
|
|
3625
|
+
function checkSteps(nodeVersionYaml, publishesNpm, withCoverage) {
|
|
3626
|
+
const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
|
|
3627
|
+
const checkCommand = withCoverage ? "pnpm ci:check --with-coverage" : "pnpm ci:check";
|
|
3602
3628
|
return [
|
|
3603
3629
|
{ step: {
|
|
3604
3630
|
uses: "actions/checkout@v6",
|
|
@@ -3608,7 +3634,7 @@ function checkSteps(nodeVersionYaml, publishesNpm) {
|
|
|
3608
3634
|
{ step: {
|
|
3609
3635
|
uses: "actions/setup-node@v6",
|
|
3610
3636
|
with: {
|
|
3611
|
-
...
|
|
3637
|
+
...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
3612
3638
|
cache: "pnpm",
|
|
3613
3639
|
...publishesNpm && { "registry-url": actionsExpr("vars.NPM_REGISTRY_URL || 'https://registry.npmjs.org'") }
|
|
3614
3640
|
}
|
|
@@ -3616,7 +3642,7 @@ function checkSteps(nodeVersionYaml, publishesNpm) {
|
|
|
3616
3642
|
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
3617
3643
|
{ step: {
|
|
3618
3644
|
name: "Run all checks",
|
|
3619
|
-
run:
|
|
3645
|
+
run: checkCommand
|
|
3620
3646
|
} }
|
|
3621
3647
|
];
|
|
3622
3648
|
}
|
|
@@ -3637,6 +3663,8 @@ async function generateCi(ctx) {
|
|
|
3637
3663
|
const publishesNpm = ctx.config.publishNpm === true;
|
|
3638
3664
|
const hookCommand = getPostReleaseHookCommand(ctx);
|
|
3639
3665
|
const hookSecrets = getPostReleaseHookSecrets(ctx);
|
|
3666
|
+
const coverage = resolveCoverageConfigFromContext(ctx, ctx.config.setupVitest);
|
|
3667
|
+
const emitCoverageSteps = coverage.historyEnabled;
|
|
3640
3668
|
const hasDocker = isChangesets && hasDockerPackages(ctx);
|
|
3641
3669
|
const hasAssets = isChangesets && hasReleaseAssets(ctx);
|
|
3642
3670
|
const registryDerivable = hasDocker && hasRepositoryUrl(ctx);
|
|
@@ -3649,7 +3677,8 @@ async function generateCi(ctx) {
|
|
|
3649
3677
|
releaseEnv["RELEASE_TOKEN"] = actionsExpr("secrets.RELEASE_TOKEN");
|
|
3650
3678
|
}
|
|
3651
3679
|
const steps = [
|
|
3652
|
-
...checkSteps(nodeVersionYaml, isChangesets && publishesNpm),
|
|
3680
|
+
...checkSteps(nodeVersionYaml, isChangesets && publishesNpm, coverage.enabled),
|
|
3681
|
+
...emitCoverageSteps ? coverageHistorySteps() : [],
|
|
3653
3682
|
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm)] : [],
|
|
3654
3683
|
...hasAssets ? [{ step: {
|
|
3655
3684
|
name: "Upload Release Assets",
|
|
@@ -3679,6 +3708,7 @@ async function generateCi(ctx) {
|
|
|
3679
3708
|
},
|
|
3680
3709
|
pull_request: null
|
|
3681
3710
|
},
|
|
3711
|
+
...emitCoverageSteps && { permissions: { contents: "write" } },
|
|
3682
3712
|
enableEmailNotifications: isForgejo,
|
|
3683
3713
|
...isChangesets && { concurrency: CI_CONCURRENCY },
|
|
3684
3714
|
jobName: "check",
|
|
@@ -3876,6 +3906,7 @@ async function generateRenovate(ctx) {
|
|
|
3876
3906
|
//#region src/generators/pnpm-workspace.ts
|
|
3877
3907
|
const FILE_PATH = "pnpm-workspace.yaml";
|
|
3878
3908
|
const DEFAULT_MIN_RELEASE_AGE = 1440;
|
|
3909
|
+
const BENSANDEE_EXCLUDE_PATTERN = "@bensandee/*";
|
|
3879
3910
|
const FRESH_MONOREPO = `packages:
|
|
3880
3911
|
- "packages/*"
|
|
3881
3912
|
|
|
@@ -3909,9 +3940,11 @@ function isPnpmWorkspaceDisabled(ctx) {
|
|
|
3909
3940
|
* Mandated:
|
|
3910
3941
|
* - `blockExoticSubdeps: true`
|
|
3911
3942
|
* - `minimumReleaseAge`: any non-negative number; seeded to 1440 if missing/invalid.
|
|
3943
|
+
* - `minimumReleaseAgeExclude`: must contain `"@bensandee/*"` so this monorepo's
|
|
3944
|
+
* own packages can be installed immediately after publish. Other entries are
|
|
3945
|
+
* preserved.
|
|
3912
3946
|
*
|
|
3913
3947
|
* Not enforced (left alone after creation):
|
|
3914
|
-
* - `minimumReleaseAgeExclude`
|
|
3915
3948
|
* - everything else (`packages`, `allowBuilds`, etc.)
|
|
3916
3949
|
*/
|
|
3917
3950
|
function applyManagedKeys(doc) {
|
|
@@ -3925,6 +3958,14 @@ function applyManagedKeys(doc) {
|
|
|
3925
3958
|
doc.set("minimumReleaseAge", DEFAULT_MIN_RELEASE_AGE);
|
|
3926
3959
|
changes.push(`set minimumReleaseAge: ${DEFAULT_MIN_RELEASE_AGE}`);
|
|
3927
3960
|
}
|
|
3961
|
+
const excludeNode = doc.get("minimumReleaseAgeExclude");
|
|
3962
|
+
if (!isSeq(excludeNode)) {
|
|
3963
|
+
doc.set("minimumReleaseAgeExclude", [BENSANDEE_EXCLUDE_PATTERN]);
|
|
3964
|
+
changes.push(`set minimumReleaseAgeExclude: ["${BENSANDEE_EXCLUDE_PATTERN}"]`);
|
|
3965
|
+
} else if (!excludeNode.items.some((item) => isScalar(item) && item.value === BENSANDEE_EXCLUDE_PATTERN)) {
|
|
3966
|
+
excludeNode.add(BENSANDEE_EXCLUDE_PATTERN);
|
|
3967
|
+
changes.push(`added "${BENSANDEE_EXCLUDE_PATTERN}" to minimumReleaseAgeExclude`);
|
|
3968
|
+
}
|
|
3928
3969
|
return changes;
|
|
3929
3970
|
}
|
|
3930
3971
|
async function generatePnpmWorkspace(ctx) {
|
|
@@ -4020,6 +4061,22 @@ function parseClaudeSettings(raw) {
|
|
|
4020
4061
|
return;
|
|
4021
4062
|
}
|
|
4022
4063
|
}
|
|
4064
|
+
/**
|
|
4065
|
+
* Instructions previously emitted to `.claude/settings.json#instructions`.
|
|
4066
|
+
* Migrated to a managed block in CLAUDE.md (see `claude-md.ts`); on sync we
|
|
4067
|
+
* strip any existing entry that matches (by either-direction prefix, mirroring
|
|
4068
|
+
* the upgrade-in-place semantics the merge logic used to apply on add).
|
|
4069
|
+
*/
|
|
4070
|
+
const MIGRATED_INSTRUCTIONS = [
|
|
4071
|
+
"Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
|
|
4072
|
+
"No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
|
|
4073
|
+
"Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option. For no-empty-function: add a `{ /* no-op */ }` comment body instead of a disable comment.",
|
|
4074
|
+
"Prefer extensionless imports; if an extension is required, use .ts over .js.",
|
|
4075
|
+
"The bst-hook manages Bash/file permissions. See .claude/hooks/bst-hook-guide.md for auto-approved, denied, and passthrough commands."
|
|
4076
|
+
];
|
|
4077
|
+
function isMigratedInstruction(s) {
|
|
4078
|
+
return MIGRATED_INSTRUCTIONS.some((c) => s.startsWith(c) || c.startsWith(s));
|
|
4079
|
+
}
|
|
4023
4080
|
function buildSettings(ctx) {
|
|
4024
4081
|
const allow = [];
|
|
4025
4082
|
const enabledPlugins = { "code-simplifier@claude-plugins-official": true };
|
|
@@ -4039,13 +4096,7 @@ function buildSettings(ctx) {
|
|
|
4039
4096
|
command: `node "$(git rev-parse --show-toplevel)/.claude/hooks/bst-hook.mjs"`
|
|
4040
4097
|
}]
|
|
4041
4098
|
}] },
|
|
4042
|
-
instructions: [
|
|
4043
|
-
"Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
|
|
4044
|
-
"No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
|
|
4045
|
-
"Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option. For no-empty-function: add a `{ /* no-op */ }` comment body instead of a disable comment.",
|
|
4046
|
-
"Prefer extensionless imports; if an extension is required, use .ts over .js.",
|
|
4047
|
-
"The bst-hook manages Bash/file permissions. See .claude/hooks/bst-hook-guide.md for auto-approved, denied, and passthrough commands."
|
|
4048
|
-
],
|
|
4099
|
+
instructions: [],
|
|
4049
4100
|
enabledPlugins,
|
|
4050
4101
|
extraKnownMarketplaces
|
|
4051
4102
|
};
|
|
@@ -4060,11 +4111,12 @@ function removeObsoleteBstHookEntries(matchers, consolidatedMatcher) {
|
|
|
4060
4111
|
return false;
|
|
4061
4112
|
});
|
|
4062
4113
|
}
|
|
4063
|
-
/** Remove
|
|
4114
|
+
/** Remove empty optional fields to keep the file clean. */
|
|
4064
4115
|
function serializeSettings$1(settings) {
|
|
4065
|
-
const { enabledPlugins, extraKnownMarketplaces, hooks, ...rest } = settings;
|
|
4116
|
+
const { enabledPlugins, extraKnownMarketplaces, hooks, instructions, ...rest } = settings;
|
|
4066
4117
|
const out = { ...rest };
|
|
4067
4118
|
if (Object.keys(hooks).length > 0) out["hooks"] = hooks;
|
|
4119
|
+
if (instructions.length > 0) out["instructions"] = instructions;
|
|
4068
4120
|
if (Object.keys(enabledPlugins).length > 0) out["enabledPlugins"] = enabledPlugins;
|
|
4069
4121
|
if (Object.keys(extraKnownMarketplaces).length > 0) out["extraKnownMarketplaces"] = extraKnownMarketplaces;
|
|
4070
4122
|
return JSON.stringify(out, null, 2) + "\n";
|
|
@@ -4081,15 +4133,9 @@ function writeOrMergeSettings(ctx, filePath, generated) {
|
|
|
4081
4133
|
const { settings, rawJson } = parsed;
|
|
4082
4134
|
const missingAllow = generated.permissions.allow.filter((rule) => !settings.permissions.allow.includes(rule));
|
|
4083
4135
|
const missingDeny = generated.permissions.deny.filter((rule) => !settings.permissions.deny.includes(rule));
|
|
4084
|
-
const
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
if (mergedInstructions.includes(inst)) continue;
|
|
4088
|
-
const prefixIdx = mergedInstructions.findIndex((e) => inst.startsWith(e) || e.startsWith(inst));
|
|
4089
|
-
if (prefixIdx !== -1) mergedInstructions[prefixIdx] = inst;
|
|
4090
|
-
else mergedInstructions.push(inst);
|
|
4091
|
-
instructionChanges++;
|
|
4092
|
-
}
|
|
4136
|
+
const filteredInstructions = settings.instructions.filter((s) => !isMigratedInstruction(s));
|
|
4137
|
+
const instructionChanges = settings.instructions.length - filteredInstructions.length;
|
|
4138
|
+
const mergedInstructions = filteredInstructions;
|
|
4093
4139
|
let hookChanges = 0;
|
|
4094
4140
|
const mergedHooks = { ...settings.hooks };
|
|
4095
4141
|
for (const [event, generatedMatchers] of Object.entries(generated.hooks)) {
|
|
@@ -4130,7 +4176,8 @@ function writeOrMergeSettings(ctx, filePath, generated) {
|
|
|
4130
4176
|
deny: [...settings.permissions.deny, ...missingDeny]
|
|
4131
4177
|
};
|
|
4132
4178
|
if (Object.keys(mergedHooks).length > 0) rawJson["hooks"] = mergedHooks;
|
|
4133
|
-
rawJson["instructions"] = mergedInstructions;
|
|
4179
|
+
if (mergedInstructions.length > 0) rawJson["instructions"] = mergedInstructions;
|
|
4180
|
+
else delete rawJson["instructions"];
|
|
4134
4181
|
const updatedPlugins = { ...settings.enabledPlugins };
|
|
4135
4182
|
for (const [key, value] of missingPlugins) updatedPlugins[key] = value;
|
|
4136
4183
|
const updatedMarketplaces = { ...settings.extraKnownMarketplaces };
|
|
@@ -4284,7 +4331,7 @@ function buildConfig$1(formatter) {
|
|
|
4284
4331
|
""
|
|
4285
4332
|
].join("\n");
|
|
4286
4333
|
}
|
|
4287
|
-
const ARCHIVE_DIR = ".tooling-archived";
|
|
4334
|
+
const ARCHIVE_DIR$1 = ".tooling-archived";
|
|
4288
4335
|
/** Common client-side git hooks that husky may have configured. */
|
|
4289
4336
|
const HUSKY_HOOK_NAMES = [
|
|
4290
4337
|
"pre-commit",
|
|
@@ -4315,12 +4362,12 @@ function archiveHuskyHooks(ctx, results) {
|
|
|
4315
4362
|
const huskyPath = `.husky/${hook}`;
|
|
4316
4363
|
const existing = ctx.read(huskyPath);
|
|
4317
4364
|
if (existing !== void 0) {
|
|
4318
|
-
ctx.write(`${ARCHIVE_DIR}/${huskyPath}`, existing);
|
|
4365
|
+
ctx.write(`${ARCHIVE_DIR$1}/${huskyPath}`, existing);
|
|
4319
4366
|
ctx.remove(huskyPath);
|
|
4320
4367
|
results.push({
|
|
4321
4368
|
filePath: huskyPath,
|
|
4322
4369
|
action: "archived",
|
|
4323
|
-
description: `Moved to ${ARCHIVE_DIR}/${huskyPath}`
|
|
4370
|
+
description: `Moved to ${ARCHIVE_DIR$1}/${huskyPath}`
|
|
4324
4371
|
});
|
|
4325
4372
|
found = true;
|
|
4326
4373
|
}
|
|
@@ -4333,12 +4380,12 @@ function archiveLintStagedConfigs(ctx, results) {
|
|
|
4333
4380
|
for (const lsPath of LINT_STAGED_CONFIG_PATHS) {
|
|
4334
4381
|
const existing = ctx.read(lsPath);
|
|
4335
4382
|
if (existing !== void 0) {
|
|
4336
|
-
ctx.write(`${ARCHIVE_DIR}/${lsPath}`, existing);
|
|
4383
|
+
ctx.write(`${ARCHIVE_DIR$1}/${lsPath}`, existing);
|
|
4337
4384
|
ctx.remove(lsPath);
|
|
4338
4385
|
results.push({
|
|
4339
4386
|
filePath: lsPath,
|
|
4340
4387
|
action: "archived",
|
|
4341
|
-
description: `Moved to ${ARCHIVE_DIR}/${lsPath}`
|
|
4388
|
+
description: `Moved to ${ARCHIVE_DIR$1}/${lsPath}`
|
|
4342
4389
|
});
|
|
4343
4390
|
found = true;
|
|
4344
4391
|
}
|
|
@@ -4518,43 +4565,12 @@ async function generateVscodeSettings(ctx) {
|
|
|
4518
4565
|
}
|
|
4519
4566
|
//#endregion
|
|
4520
4567
|
//#region src/generators/coverage-ci.ts
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
{ step: { uses: "pnpm/action-setup@v5" } },
|
|
4528
|
-
{ step: {
|
|
4529
|
-
uses: "actions/setup-node@v6",
|
|
4530
|
-
with: {
|
|
4531
|
-
...nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
4532
|
-
cache: "pnpm"
|
|
4533
|
-
}
|
|
4534
|
-
} },
|
|
4535
|
-
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
4536
|
-
{ step: {
|
|
4537
|
-
name: "Build",
|
|
4538
|
-
run: "pnpm build"
|
|
4539
|
-
} },
|
|
4540
|
-
{ step: {
|
|
4541
|
-
name: "Run tests with coverage",
|
|
4542
|
-
run: "pnpm test:coverage"
|
|
4543
|
-
} },
|
|
4544
|
-
{ step: {
|
|
4545
|
-
name: "Print coverage summary",
|
|
4546
|
-
run: "pnpm exec bst coverage:summary"
|
|
4547
|
-
} },
|
|
4548
|
-
{ step: {
|
|
4549
|
-
name: "Append record to bst/coverage-metrics branch",
|
|
4550
|
-
run: "pnpm exec bst coverage:record"
|
|
4551
|
-
} },
|
|
4552
|
-
{ step: {
|
|
4553
|
-
name: "Show recent coverage trend",
|
|
4554
|
-
run: "pnpm exec bst coverage:status --limit 10"
|
|
4555
|
-
} }
|
|
4556
|
-
];
|
|
4557
|
-
}
|
|
4568
|
+
const ARCHIVE_DIR = ".tooling-archived";
|
|
4569
|
+
/**
|
|
4570
|
+
* Cleanup-only generator: coverage steps now run inline in ci.yml (see
|
|
4571
|
+
* generateCi). Any legacy coverage.yml left over from a previous version is
|
|
4572
|
+
* archived to .tooling-archived/ on the next repo:sync.
|
|
4573
|
+
*/
|
|
4558
4574
|
async function generateCoverageCi(ctx) {
|
|
4559
4575
|
const filePath = "coverage-ci";
|
|
4560
4576
|
if (ctx.config.ci === "none") return {
|
|
@@ -4562,100 +4578,92 @@ async function generateCoverageCi(ctx) {
|
|
|
4562
4578
|
action: "skipped",
|
|
4563
4579
|
description: "CI workflow not requested"
|
|
4564
4580
|
};
|
|
4565
|
-
const
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
const isGitHub = ctx.config.ci === "github";
|
|
4572
|
-
const isForgejo = !isGitHub;
|
|
4573
|
-
const workflowPath = isGitHub ? ".github/workflows/coverage.yml" : ".forgejo/workflows/coverage.yml";
|
|
4574
|
-
const nodeVersionYaml = computeNodeVersionYaml(ctx);
|
|
4575
|
-
const content = buildWorkflowYaml({
|
|
4576
|
-
ci: ctx.config.ci,
|
|
4577
|
-
name: "Coverage",
|
|
4578
|
-
on: { push: {
|
|
4579
|
-
branches: ["main"],
|
|
4580
|
-
"tags-ignore": ["**"]
|
|
4581
|
-
} },
|
|
4582
|
-
permissions: { contents: "write" },
|
|
4583
|
-
enableEmailNotifications: isForgejo,
|
|
4584
|
-
jobName: "coverage",
|
|
4585
|
-
steps: coverageSteps(nodeVersionYaml)
|
|
4586
|
-
});
|
|
4587
|
-
const alreadyExists = ctx.exists(workflowPath);
|
|
4588
|
-
const existing = alreadyExists ? ctx.read(workflowPath) : void 0;
|
|
4589
|
-
if (existing) {
|
|
4590
|
-
if (isToolingIgnored(existing)) return {
|
|
4591
|
-
filePath: workflowPath,
|
|
4592
|
-
action: "skipped",
|
|
4593
|
-
description: "Coverage workflow has ignore comment"
|
|
4594
|
-
};
|
|
4595
|
-
if (normalizeWorkflow(existing) === normalizeWorkflow(content)) return {
|
|
4581
|
+
const workflowPath = ctx.config.ci === "github" ? ".github/workflows/coverage.yml" : ".forgejo/workflows/coverage.yml";
|
|
4582
|
+
const existing = ctx.read(workflowPath);
|
|
4583
|
+
if (existing !== void 0) {
|
|
4584
|
+
ctx.write(`${ARCHIVE_DIR}/${workflowPath}`, existing);
|
|
4585
|
+
ctx.remove(workflowPath);
|
|
4586
|
+
return {
|
|
4596
4587
|
filePath: workflowPath,
|
|
4597
|
-
action: "
|
|
4598
|
-
description:
|
|
4588
|
+
action: "archived",
|
|
4589
|
+
description: `Moved to ${ARCHIVE_DIR}/${workflowPath} (coverage now runs inline in ci.yml)`
|
|
4599
4590
|
};
|
|
4600
4591
|
}
|
|
4601
|
-
const withPinnedActions = preservePinnedActions(preserveDevBinaryPath(content, existing), existing);
|
|
4602
|
-
const final = existing ? insertCustomBlocks(withPinnedActions, extractCustomBlocks(existing)) : withPinnedActions;
|
|
4603
|
-
ctx.write(workflowPath, final);
|
|
4604
4592
|
return {
|
|
4605
|
-
filePath
|
|
4606
|
-
action:
|
|
4607
|
-
description:
|
|
4593
|
+
filePath,
|
|
4594
|
+
action: "skipped",
|
|
4595
|
+
description: "Coverage runs inline in ci.yml"
|
|
4608
4596
|
};
|
|
4609
4597
|
}
|
|
4610
4598
|
//#endregion
|
|
4611
4599
|
//#region src/generators/claude-md.ts
|
|
4612
4600
|
const FILE = "CLAUDE.md";
|
|
4613
|
-
|
|
4614
|
-
|
|
4601
|
+
/**
|
|
4602
|
+
* General agent rules previously emitted as the `instructions` array in
|
|
4603
|
+
* `.claude/settings.json`. Migrated here so CLAUDE.md is the single source
|
|
4604
|
+
* for human-readable agent guidance.
|
|
4605
|
+
*
|
|
4606
|
+
* Also includes the plan-lifecycle convention so `docs/plan-*.md` files get
|
|
4607
|
+
* retired consistently when they ship.
|
|
4608
|
+
*/
|
|
4609
|
+
const AGENT_RULES_BODY = [
|
|
4610
|
+
"### Agent rules",
|
|
4611
|
+
"",
|
|
4612
|
+
"- Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
|
|
4613
|
+
"- No typecasts (`as`/`any`). Use zod schemas, type guards, or narrowing instead.",
|
|
4614
|
+
"- Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option. For `no-empty-function`: add a `{ /* no-op */ }` comment body instead of a disable comment.",
|
|
4615
|
+
"- Prefer extensionless imports; if an extension is required, use `.ts` over `.js`.",
|
|
4616
|
+
"- The bst-hook manages Bash/file permissions. See `.claude/hooks/bst-hook-guide.md` for auto-approved, denied, and passthrough commands.",
|
|
4617
|
+
"",
|
|
4618
|
+
"#### Plan lifecycle",
|
|
4619
|
+
"",
|
|
4620
|
+
"When a plan under `docs/` (e.g. `docs/plan-foo.md`) is fully implemented, retire it:",
|
|
4621
|
+
"",
|
|
4622
|
+
"1. Move the file into `docs/completed/`.",
|
|
4623
|
+
"2. Rename so the `plan-` prefix becomes `completed-` — `docs/plan-foo.md` → `docs/completed/completed-foo.md`. Don't keep both prefixes (no `completed-plan-foo.md`).",
|
|
4624
|
+
"3. Update the plan body: change the `Status:` line to `Status: **completed YYYY-MM-DD**` using today's date, and add a short \"Shipped:\" paragraph naming what landed and where the post-implementation truth lives (CLAUDE.md, source files, etc.).",
|
|
4625
|
+
"4. Fix any relative links inside the moved file that broke from the move (e.g. `[todo.md](todo.md)` → `[todo.md](../todo.md)`).",
|
|
4626
|
+
"5. Confirm no stale references to the old path remain elsewhere in the repo."
|
|
4627
|
+
].join("\n");
|
|
4615
4628
|
/**
|
|
4616
4629
|
* The coverage-baseline rule that the agent must consult before declaring a
|
|
4617
4630
|
* task complete. Kept terse — the linked plan and `--help` carry the long form.
|
|
4618
4631
|
*/
|
|
4619
|
-
const
|
|
4632
|
+
const COVERAGE_RULE_BODY = [
|
|
4620
4633
|
"### Coverage baseline",
|
|
4621
4634
|
"",
|
|
4622
4635
|
"Before declaring a task complete that touched files under `packages/*/src/` (or `src/` for single-package repos), run `pnpm test:coverage` then `pnpm exec bst coverage:check`. If any of the four overall totals (lines/statements/branches/functions) dropped by more than the configured tolerance, add tests until parity is restored — or, if a touched file is genuinely not worth covering, exclude it via `coverage.exclude` in `vitest.config.ts` and explain why in the commit message. If all four are flat-or-up, run `pnpm exec bst coverage:check --update-baseline` and stage `coverage-baseline.json` with the change. Skip this gate for docs-only, config-only, dependency-bump, or test-only changes."
|
|
4623
4636
|
].join("\n");
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
return `${START_MARKER}\n\n${RULE_BODY}\n\n${END_MARKER}\n`;
|
|
4637
|
+
function buildBlockText(block) {
|
|
4638
|
+
return `${block.startMarker}\n\n${block.body}\n\n${block.endMarker}\n`;
|
|
4627
4639
|
}
|
|
4628
|
-
function findBlock(existing) {
|
|
4640
|
+
function findBlock(existing, block) {
|
|
4629
4641
|
const lines = existing.split("\n");
|
|
4630
4642
|
let startLine = -1;
|
|
4631
|
-
for (let i = 0; i < lines.length; i++) if (lines[i]?.trim() ===
|
|
4643
|
+
for (let i = 0; i < lines.length; i++) if (lines[i]?.trim() === block.startMarker) {
|
|
4632
4644
|
startLine = i;
|
|
4633
4645
|
break;
|
|
4634
4646
|
}
|
|
4635
4647
|
if (startLine === -1) return void 0;
|
|
4636
|
-
for (let i = startLine + 1; i < lines.length; i++) if (lines[i]?.trim() ===
|
|
4648
|
+
for (let i = startLine + 1; i < lines.length; i++) if (lines[i]?.trim() === block.endMarker) return {
|
|
4637
4649
|
startLine,
|
|
4638
4650
|
endLine: i
|
|
4639
4651
|
};
|
|
4640
4652
|
}
|
|
4641
|
-
/**
|
|
4642
|
-
* Replace any existing `<!-- @tooling:coverage-rule -->...<!-- @tooling:endcoverage-rule -->`
|
|
4643
|
-
* block in `existing` with `block`, or append it at the end if no block is found.
|
|
4644
|
-
*/
|
|
4645
4653
|
function upsertBlock(existing, block) {
|
|
4646
|
-
const
|
|
4647
|
-
|
|
4654
|
+
const blockText = buildBlockText(block);
|
|
4655
|
+
const found = findBlock(existing, block);
|
|
4656
|
+
if (!found) return `${existing.replace(/\n+$/, "")}\n\n${blockText}`;
|
|
4648
4657
|
const lines = existing.split("\n");
|
|
4649
4658
|
const beforeLines = lines.slice(0, found.startLine);
|
|
4650
4659
|
const afterLines = lines.slice(found.endLine + 1);
|
|
4651
4660
|
const before = beforeLines.join("\n").replace(/\n+$/, "");
|
|
4652
4661
|
const after = afterLines.join("\n").replace(/^\n+/, "");
|
|
4653
4662
|
const tail = after.length > 0 ? `\n\n${after}` : "\n";
|
|
4654
|
-
return `${before}\n\n${
|
|
4663
|
+
return `${before}\n\n${blockText.replace(/\n+$/, "")}${tail}`;
|
|
4655
4664
|
}
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
const found = findBlock(existing);
|
|
4665
|
+
function removeBlock(existing, block) {
|
|
4666
|
+
const found = findBlock(existing, block);
|
|
4659
4667
|
if (!found) return existing;
|
|
4660
4668
|
const lines = existing.split("\n");
|
|
4661
4669
|
const beforeLines = lines.slice(0, found.startLine);
|
|
@@ -4665,50 +4673,46 @@ function removeBlock(existing) {
|
|
|
4665
4673
|
if (after.length === 0) return `${before}\n`;
|
|
4666
4674
|
return `${before}\n\n${after}`;
|
|
4667
4675
|
}
|
|
4676
|
+
function applyBlock(existing, block) {
|
|
4677
|
+
return block.enabled ? upsertBlock(existing, block) : removeBlock(existing, block);
|
|
4678
|
+
}
|
|
4668
4679
|
const STARTER_CLAUDE_MD = "# CLAUDE.md\n\nProject guidance for Claude Code.\n";
|
|
4669
4680
|
async function generateClaudeMd(ctx) {
|
|
4670
|
-
const
|
|
4681
|
+
const coverageConfig = resolveCoverageConfigFromContext(ctx, ctx.config.setupVitest);
|
|
4682
|
+
const blocks = [{
|
|
4683
|
+
name: "agent-rules",
|
|
4684
|
+
startMarker: "<!-- @tooling:agent-rules -->",
|
|
4685
|
+
endMarker: "<!-- @tooling:endagent-rules -->",
|
|
4686
|
+
body: AGENT_RULES_BODY,
|
|
4687
|
+
enabled: true
|
|
4688
|
+
}, {
|
|
4689
|
+
name: "coverage-rule",
|
|
4690
|
+
startMarker: "<!-- @tooling:coverage-rule -->",
|
|
4691
|
+
endMarker: "<!-- @tooling:endcoverage-rule -->",
|
|
4692
|
+
body: COVERAGE_RULE_BODY,
|
|
4693
|
+
enabled: coverageConfig.enabled
|
|
4694
|
+
}];
|
|
4671
4695
|
const existing = ctx.read(FILE);
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
filePath: FILE,
|
|
4675
|
-
action: "skipped",
|
|
4676
|
-
description: "Coverage disabled; no CLAUDE.md to manage"
|
|
4677
|
-
};
|
|
4678
|
-
if (!existing.includes(START_MARKER)) return {
|
|
4679
|
-
filePath: FILE,
|
|
4680
|
-
action: "skipped",
|
|
4681
|
-
description: "Coverage disabled; no managed block to remove"
|
|
4682
|
-
};
|
|
4683
|
-
const updated = removeBlock(existing);
|
|
4684
|
-
ctx.write(FILE, updated);
|
|
4685
|
-
return {
|
|
4686
|
-
filePath: FILE,
|
|
4687
|
-
action: "updated",
|
|
4688
|
-
description: "Removed coverage-rule managed block"
|
|
4689
|
-
};
|
|
4690
|
-
}
|
|
4691
|
-
const block = buildBlock();
|
|
4696
|
+
let updated = existing ?? STARTER_CLAUDE_MD;
|
|
4697
|
+
for (const block of blocks) updated = applyBlock(updated, block);
|
|
4692
4698
|
if (!existing) {
|
|
4693
|
-
|
|
4694
|
-
ctx.write(FILE, content);
|
|
4699
|
+
ctx.write(FILE, updated);
|
|
4695
4700
|
return {
|
|
4696
4701
|
filePath: FILE,
|
|
4697
4702
|
action: "created",
|
|
4698
|
-
description: "Generated CLAUDE.md with
|
|
4703
|
+
description: "Generated CLAUDE.md with managed rule blocks"
|
|
4699
4704
|
};
|
|
4700
4705
|
}
|
|
4701
|
-
const updated = upsertBlock(existing, block);
|
|
4702
4706
|
if (updated === existing) return {
|
|
4703
4707
|
filePath: FILE,
|
|
4704
4708
|
action: "skipped",
|
|
4705
|
-
description: "
|
|
4709
|
+
description: "Managed rule blocks already up to date"
|
|
4706
4710
|
};
|
|
4707
4711
|
ctx.write(FILE, updated);
|
|
4708
4712
|
return {
|
|
4709
4713
|
filePath: FILE,
|
|
4710
4714
|
action: "updated",
|
|
4711
|
-
description: "Updated
|
|
4715
|
+
description: "Updated managed rule blocks"
|
|
4712
4716
|
};
|
|
4713
4717
|
}
|
|
4714
4718
|
//#endregion
|
|
@@ -4816,7 +4820,7 @@ async function runGenerators(ctx) {
|
|
|
4816
4820
|
function generateMigratePrompt(results, config, detected) {
|
|
4817
4821
|
const sections = [];
|
|
4818
4822
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4819
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
4823
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.59.1 repo:sync\` on ${timestamp}_\n`);
|
|
4820
4824
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
4821
4825
|
if (changed.length > 0) {
|
|
4822
4826
|
const items = changed.map((r) => {
|
|
@@ -5083,6 +5087,10 @@ const syncCommand = defineCommand({
|
|
|
5083
5087
|
return;
|
|
5084
5088
|
}
|
|
5085
5089
|
const saved = loadToolingConfig(targetDir);
|
|
5090
|
+
if (runComposeLintGate(targetDir, saved) > 0) {
|
|
5091
|
+
process.exitCode = 1;
|
|
5092
|
+
return;
|
|
5093
|
+
}
|
|
5086
5094
|
const isFirstRun = !saved;
|
|
5087
5095
|
let config;
|
|
5088
5096
|
if (args.yes || !isFirstRun) {
|
|
@@ -5101,6 +5109,7 @@ const syncCommand = defineCommand({
|
|
|
5101
5109
|
/** Run sync in check mode: dry-run drift detection. */
|
|
5102
5110
|
async function runCheck(targetDir) {
|
|
5103
5111
|
const saved = loadToolingConfig(targetDir);
|
|
5112
|
+
if (runComposeLintGate(targetDir, saved) > 0) return 1;
|
|
5104
5113
|
const detected = buildDefaultConfig(targetDir, {});
|
|
5105
5114
|
const { ctx, pendingWrites } = createDryRunContext(saved ? mergeWithSavedConfig(detected, saved) : detected);
|
|
5106
5115
|
logDetectionSummary(ctx);
|
|
@@ -5137,6 +5146,16 @@ async function runCheck(targetDir) {
|
|
|
5137
5146
|
}
|
|
5138
5147
|
return 1;
|
|
5139
5148
|
}
|
|
5149
|
+
/**
|
|
5150
|
+
* Lint compose files before any generator or apply step runs. Returns the number
|
|
5151
|
+
* of issues found (0 = clean). Each issue is emitted via `log.error` so the
|
|
5152
|
+
* user sees the full message and the suggested fix.
|
|
5153
|
+
*/
|
|
5154
|
+
function runComposeLintGate(targetDir, config) {
|
|
5155
|
+
const issues = lintComposeFiles(targetDir, config);
|
|
5156
|
+
for (const issue of issues) log.error(issue.message);
|
|
5157
|
+
return issues.length;
|
|
5158
|
+
}
|
|
5140
5159
|
/** Files that are minified/bundled and produce unreadable diffs. */
|
|
5141
5160
|
function isBundledFile(filePath) {
|
|
5142
5161
|
return filePath.endsWith("bst-hook.mjs");
|
|
@@ -6392,6 +6411,9 @@ function runRunChecks(targetDir, options = {}) {
|
|
|
6392
6411
|
const allChecks = [...CHECKS, ...add.map((name) => ({ name }))];
|
|
6393
6412
|
const failures = [];
|
|
6394
6413
|
const notDefined = [];
|
|
6414
|
+
const withCoverage = options.withCoverage ?? false;
|
|
6415
|
+
const useCoverageScript = withCoverage && definedScripts.has("test:coverage");
|
|
6416
|
+
if (withCoverage && !useCoverageScript) log.warn("--with-coverage requested but test:coverage script is not defined; falling back to test");
|
|
6395
6417
|
for (const check of allChecks) {
|
|
6396
6418
|
if (shouldSkip(check.name, skip)) continue;
|
|
6397
6419
|
if (!definedScripts.has(check.name)) {
|
|
@@ -6401,7 +6423,8 @@ function runRunChecks(targetDir, options = {}) {
|
|
|
6401
6423
|
} else notDefined.push(check.name);
|
|
6402
6424
|
continue;
|
|
6403
6425
|
}
|
|
6404
|
-
const
|
|
6426
|
+
const scriptName = check.name === "test" && useCoverageScript ? "test:coverage" : check.name;
|
|
6427
|
+
const cmd = check.args ? `pnpm run ${scriptName} ${check.args}` : `pnpm run ${scriptName}`;
|
|
6405
6428
|
debug(vc, `Running: ${cmd} (in ${targetDir})`);
|
|
6406
6429
|
reporter.groupStart(check.name);
|
|
6407
6430
|
const start = Date.now();
|
|
@@ -6454,6 +6477,11 @@ const runChecksCommand = defineCommand({
|
|
|
6454
6477
|
type: "boolean",
|
|
6455
6478
|
description: "Emit detailed debug logging",
|
|
6456
6479
|
required: false
|
|
6480
|
+
},
|
|
6481
|
+
"with-coverage": {
|
|
6482
|
+
type: "boolean",
|
|
6483
|
+
description: "Run the test step with coverage (uses pnpm run test:coverage when defined)",
|
|
6484
|
+
required: false
|
|
6457
6485
|
}
|
|
6458
6486
|
},
|
|
6459
6487
|
run({ args }) {
|
|
@@ -6461,7 +6489,8 @@ const runChecksCommand = defineCommand({
|
|
|
6461
6489
|
skip: args.skip ? new Set(args.skip.split(",").map((s) => s.trim())) : void 0,
|
|
6462
6490
|
add: args.add ? args.add.split(",").map((s) => s.trim()) : void 0,
|
|
6463
6491
|
failFast: args["fail-fast"] ? true : void 0,
|
|
6464
|
-
verbose: args.verbose === true || isEnvVerbose()
|
|
6492
|
+
verbose: args.verbose === true || isEnvVerbose(),
|
|
6493
|
+
withCoverage: args["with-coverage"] === true
|
|
6465
6494
|
});
|
|
6466
6495
|
process.exitCode = exitCode;
|
|
6467
6496
|
}
|
|
@@ -6926,20 +6955,19 @@ const dockerCheckCommand = defineCommand({
|
|
|
6926
6955
|
const services = parseComposeServices(composeCwd, defaults.composeFiles);
|
|
6927
6956
|
const fileReader = createFileReader();
|
|
6928
6957
|
const rootPkgRaw = fileReader.readFile(path.join(cwd, "package.json"));
|
|
6958
|
+
const rootPkg = rootPkgRaw ? parsePackageJson(rootPkgRaw) : void 0;
|
|
6959
|
+
const projectName = deriveProjectName(rootPkg?.name ?? path.basename(cwd));
|
|
6929
6960
|
const locallyBuiltImageNames = [];
|
|
6930
|
-
if (
|
|
6931
|
-
const
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
const matchesBuild = composeBuildDockerfiles.has(pkgDockerfileAbs);
|
|
6941
|
-
if (!matchesImage && !matchesBuild) log.warn(`Docker package "${pkg.dir}" (image: ${pkg.imageName}) is not referenced in any compose service.`);
|
|
6942
|
-
}
|
|
6961
|
+
if (rootPkg?.name) {
|
|
6962
|
+
const dockerPackages = detectDockerPackages(fileReader, cwd, rootPkg.name);
|
|
6963
|
+
const composeImages = extractComposeImageNames(services);
|
|
6964
|
+
const composeBuildDockerfiles = new Set(extractComposeBuildDockerfiles(services));
|
|
6965
|
+
for (const pkg of dockerPackages) {
|
|
6966
|
+
locallyBuiltImageNames.push(pkg.imageName);
|
|
6967
|
+
const matchesImage = composeImages.some((img) => img === pkg.imageName || img.endsWith(`/${pkg.imageName}`));
|
|
6968
|
+
const pkgDockerfileAbs = path.resolve(cwd, pkg.docker.dockerfile);
|
|
6969
|
+
const matchesBuild = composeBuildDockerfiles.has(pkgDockerfileAbs);
|
|
6970
|
+
if (!matchesImage && !matchesBuild) log.warn(`Docker package "${pkg.dir}" (image: ${pkg.imageName}) is not referenced in any compose service.`);
|
|
6943
6971
|
}
|
|
6944
6972
|
}
|
|
6945
6973
|
const tempOverlayPath = writeTempOverlay(generateCheckOverlay(services, locallyBuiltImageNames));
|
|
@@ -6953,6 +6981,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
6953
6981
|
compose: {
|
|
6954
6982
|
cwd: composeCwd,
|
|
6955
6983
|
composeFiles,
|
|
6984
|
+
projectName,
|
|
6956
6985
|
envFile: defaults.envFile,
|
|
6957
6986
|
services: defaults.services
|
|
6958
6987
|
},
|
|
@@ -8271,7 +8300,7 @@ const out = (msg) => console.log(msg);
|
|
|
8271
8300
|
const main = defineCommand({
|
|
8272
8301
|
meta: {
|
|
8273
8302
|
name: "bst",
|
|
8274
|
-
version: "0.
|
|
8303
|
+
version: "0.59.1",
|
|
8275
8304
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
8276
8305
|
},
|
|
8277
8306
|
subCommands: {
|
|
@@ -8343,7 +8372,7 @@ const main = defineCommand({
|
|
|
8343
8372
|
})
|
|
8344
8373
|
}
|
|
8345
8374
|
});
|
|
8346
|
-
console.log(`@bensandee/tooling v0.
|
|
8375
|
+
console.log(`@bensandee/tooling v0.59.1`);
|
|
8347
8376
|
async function run() {
|
|
8348
8377
|
await runMain(main);
|
|
8349
8378
|
process.exit(process.exitCode ?? 0);
|