@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { d as debug, f as debugExec, g as note, h as log, l as createRealExecutor$1, m as redact, p as isEnvVerbose, t as runDockerCheck, u as isExecSyncError } from "./check-B5DFYJ3x.mjs";
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$1 = ".tooling-archived";
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$1}/${workflowPath}`, existing);
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$1}/${workflowPath} (docker publish runs in release.yml for simple strategy)`
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.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.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.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.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.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.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.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.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.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.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.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.1";
1812
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.58.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.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.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.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.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 ? "github.ref == 'refs/heads/main'" : void 0);
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: "github.ref == 'refs/heads/main'",
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("github.ref != 'refs/heads/main'")
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
- ...nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" },
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: "pnpm ci:check"
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 enabledPlugins/extraKnownMarketplaces/hooks when empty to keep the file clean. */
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 mergedInstructions = [...settings.instructions];
4085
- let instructionChanges = 0;
4086
- for (const inst of generated.instructions) {
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
- function coverageSteps(nodeVersionYaml) {
4522
- return [
4523
- { step: {
4524
- uses: "actions/checkout@v6",
4525
- with: { "fetch-depth": 0 }
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 config = resolveCoverageConfigFromContext(ctx, ctx.config.setupVitest);
4566
- if (!config.enabled || !config.historyEnabled) return {
4567
- filePath,
4568
- action: "skipped",
4569
- description: "Coverage tracking disabled"
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: "skipped",
4598
- description: "Coverage workflow already up to date"
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: workflowPath,
4606
- action: alreadyExists ? "updated" : "created",
4607
- description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions coverage workflow`
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
- const START_MARKER = "<!-- @tooling:coverage-rule -->";
4614
- const END_MARKER = "<!-- @tooling:endcoverage-rule -->";
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 RULE_BODY = [
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
- /** Build the full managed block including markers. */
4625
- function buildBlock() {
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() === START_MARKER) {
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() === END_MARKER) return {
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 found = findBlock(existing);
4647
- if (!found) return `${existing.replace(/\n+$/, "")}\n\n${block}`;
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${block.replace(/\n+$/, "")}${tail}`;
4663
+ return `${before}\n\n${blockText.replace(/\n+$/, "")}${tail}`;
4655
4664
  }
4656
- /** Strip a coverage-rule managed block (markers + body) from CLAUDE.md content. */
4657
- function removeBlock(existing) {
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 config = resolveCoverageConfigFromContext(ctx, ctx.config.setupVitest);
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
- if (!config.enabled) {
4673
- if (!existing) return {
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
- const content = upsertBlock(STARTER_CLAUDE_MD, block);
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 coverage-rule block"
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: "Coverage-rule block already up to date"
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 coverage-rule managed block"
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.58.0 repo:sync\` on ${timestamp}_\n`);
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 cmd = check.args ? `pnpm run ${check.name} ${check.args}` : `pnpm run ${check.name}`;
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 (rootPkgRaw) {
6931
- const rootPkg = parsePackageJson(rootPkgRaw);
6932
- if (rootPkg?.name) {
6933
- const dockerPackages = detectDockerPackages(fileReader, cwd, rootPkg.name);
6934
- const composeImages = extractComposeImageNames(services);
6935
- const composeBuildDockerfiles = new Set(extractComposeBuildDockerfiles(services));
6936
- for (const pkg of dockerPackages) {
6937
- locallyBuiltImageNames.push(pkg.imageName);
6938
- const matchesImage = composeImages.some((img) => img === pkg.imageName || img.endsWith(`/${pkg.imageName}`));
6939
- const pkgDockerfileAbs = path.resolve(cwd, pkg.docker.dockerfile);
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.58.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.58.0`);
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);