@bensandee/tooling 0.57.4 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,6 +78,28 @@ Custom blocks are anchored to the nearest preceding non-blank line. When `repo:s
78
78
 
79
79
  To skip a workflow file entirely, add `# @bensandee/tooling:ignore` as the first line — `repo:sync` will leave the file untouched.
80
80
 
81
+ ### pnpm-workspace.yaml
82
+
83
+ `repo:sync` manages `pnpm-workspace.yaml` with a narrow contract — mandated keys, a few seeded-on-creation keys, and everything else passed through verbatim.
84
+
85
+ **Mandated** (drift fails `repo:sync --check`):
86
+
87
+ - `blockExoticSubdeps: true`
88
+ - `minimumReleaseAge` — any non-negative number (`0` is valid)
89
+
90
+ **Seeded on creation only** (free to change or remove afterward):
91
+
92
+ - `minimumReleaseAgeExclude: ["@bensandee/*"]`
93
+ - `packages: ["packages/*"]` (monorepos only)
94
+
95
+ All other keys — `allowBuilds`, custom entries — are preserved verbatim through `repo:sync`. The generator emits the file for single-package repos too, not just monorepos.
96
+
97
+ Opt out entirely with `"pnpmWorkspace": false` in `.tooling.json`; the generator then never inspects or writes the file.
98
+
99
+ ### packageManager force-bump
100
+
101
+ `repo:sync` rewrites `package.json#packageManager` when an existing value is `pnpm@<N>.x` with `N < 11`, setting it to the tooling's pinned pnpm version (currently `11.1.1`). This is delivered through `repo:sync` rather than Renovate because the mandated `pnpm-workspace.yaml` keys (`blockExoticSubdeps`) require pnpm 11+. Within-major bumps (`pnpm@11.x → 11.y`) remain Renovate's job; non-pnpm package managers (yarn, npm) are out of scope.
102
+
81
103
  ### Repository setup
82
104
 
83
105
  | Command | Description |
@@ -350,6 +372,7 @@ Available override fields:
350
372
  | `releaseStrategy` | string | Detected from existing config, else monorepo: `"changesets"`, single: `"simple"` |
351
373
  | `projectType` | string | Auto-detected from `package.json` deps |
352
374
  | `detectPackageTypes` | boolean | `true` |
375
+ | `pnpmWorkspace` | `false` | Set to `false` to opt out of `pnpm-workspace.yaml` management |
353
376
 
354
377
  ## Debug logging
355
378
 
package/dist/bin.mjs CHANGED
@@ -39,7 +39,8 @@ const PackageJsonSchema = z.looseObject({
39
39
  types: z.string().optional(),
40
40
  typings: z.string().optional(),
41
41
  engines: StringRecord.optional(),
42
- repository: z.union([z.string(), z.looseObject({ url: z.string() })]).optional()
42
+ repository: z.union([z.string(), z.looseObject({ url: z.string() })]).optional(),
43
+ packageManager: z.string().optional()
43
44
  });
44
45
  const TsconfigSchema = z.looseObject({
45
46
  extends: z.union([z.string(), z.array(z.string())]).optional(),
@@ -644,7 +645,8 @@ const ToolingConfigSchema = z.strictObject({
644
645
  releaseAssets: z.array(ReleaseAssetSchema).optional().meta({ description: "Custom assets to build and upload to the release" }),
645
646
  postReleaseHook: z.string().optional().meta({ description: "Override the post-release hook command (default: POST to POST_RELEASE_HOOK_URL with HMAC-SHA256 auth)" }),
646
647
  postReleaseHookSecrets: z.array(z.string()).optional().meta({ description: "Additional secret names to inject as env vars in the post-release hook step (e.g. ['WEBHOOK_SECRET'])" }),
647
- coverage: z.union([z.literal(false), CoverageConfigSchema]).optional().meta({ description: "Coverage tracking configuration. Set to false to disable; omit for default-on (when a vitest config is present)." })
648
+ coverage: z.union([z.literal(false), CoverageConfigSchema]).optional().meta({ description: "Coverage tracking configuration. Set to false to disable; omit for default-on (when a vitest config is present)." }),
649
+ pnpmWorkspace: z.literal(false).optional().meta({ description: "Set to false to opt out of pnpm-workspace.yaml management (skips both generation and the repo:sync --check validation)." })
648
650
  });
649
651
  /** Format a Zod issue as a human-readable bullet: location + plain-English explanation. */
650
652
  function formatIssue(issue) {
@@ -1591,7 +1593,7 @@ const PER_PACKAGE_DEV_DEPS = {
1591
1593
  "tsdown": "0.22.0",
1592
1594
  "typescript": "6.0.3",
1593
1595
  "vitest": "4.1.5",
1594
- "pnpm": "11.0.9"
1596
+ "pnpm": "11.1.1"
1595
1597
  }["@types/node"],
1596
1598
  tsdown: {
1597
1599
  "@changesets/cli": "2.31.0",
@@ -1607,7 +1609,7 @@ const PER_PACKAGE_DEV_DEPS = {
1607
1609
  "tsdown": "0.22.0",
1608
1610
  "typescript": "6.0.3",
1609
1611
  "vitest": "4.1.5",
1610
- "pnpm": "11.0.9"
1612
+ "pnpm": "11.1.1"
1611
1613
  }["tsdown"],
1612
1614
  typescript: {
1613
1615
  "@changesets/cli": "2.31.0",
@@ -1623,7 +1625,7 @@ const PER_PACKAGE_DEV_DEPS = {
1623
1625
  "tsdown": "0.22.0",
1624
1626
  "typescript": "6.0.3",
1625
1627
  "vitest": "4.1.5",
1626
- "pnpm": "11.0.9"
1628
+ "pnpm": "11.1.1"
1627
1629
  }["typescript"],
1628
1630
  vitest: {
1629
1631
  "@changesets/cli": "2.31.0",
@@ -1639,7 +1641,7 @@ const PER_PACKAGE_DEV_DEPS = {
1639
1641
  "tsdown": "0.22.0",
1640
1642
  "typescript": "6.0.3",
1641
1643
  "vitest": "4.1.5",
1642
- "pnpm": "11.0.9"
1644
+ "pnpm": "11.1.1"
1643
1645
  }["vitest"]
1644
1646
  };
1645
1647
  /** DevDeps that belong at the root regardless of structure. */
@@ -1658,7 +1660,7 @@ const ROOT_DEV_DEPS = {
1658
1660
  "tsdown": "0.22.0",
1659
1661
  "typescript": "6.0.3",
1660
1662
  "vitest": "4.1.5",
1661
- "pnpm": "11.0.9"
1663
+ "pnpm": "11.1.1"
1662
1664
  }["knip"],
1663
1665
  lefthook: {
1664
1666
  "@changesets/cli": "2.31.0",
@@ -1674,7 +1676,7 @@ const ROOT_DEV_DEPS = {
1674
1676
  "tsdown": "0.22.0",
1675
1677
  "typescript": "6.0.3",
1676
1678
  "vitest": "4.1.5",
1677
- "pnpm": "11.0.9"
1679
+ "pnpm": "11.1.1"
1678
1680
  }["lefthook"],
1679
1681
  oxlint: {
1680
1682
  "@changesets/cli": "2.31.0",
@@ -1690,7 +1692,7 @@ const ROOT_DEV_DEPS = {
1690
1692
  "tsdown": "0.22.0",
1691
1693
  "typescript": "6.0.3",
1692
1694
  "vitest": "4.1.5",
1693
- "pnpm": "11.0.9"
1695
+ "pnpm": "11.1.1"
1694
1696
  }["oxlint"]
1695
1697
  };
1696
1698
  /**
@@ -1731,7 +1733,7 @@ function addReleaseDeps(deps, config) {
1731
1733
  "tsdown": "0.22.0",
1732
1734
  "typescript": "6.0.3",
1733
1735
  "vitest": "4.1.5",
1734
- "pnpm": "11.0.9"
1736
+ "pnpm": "11.1.1"
1735
1737
  }["release-it"];
1736
1738
  if (config.structure === "monorepo") deps["@release-it/bumper"] = {
1737
1739
  "@changesets/cli": "2.31.0",
@@ -1747,7 +1749,7 @@ function addReleaseDeps(deps, config) {
1747
1749
  "tsdown": "0.22.0",
1748
1750
  "typescript": "6.0.3",
1749
1751
  "vitest": "4.1.5",
1750
- "pnpm": "11.0.9"
1752
+ "pnpm": "11.1.1"
1751
1753
  }["@release-it/bumper"];
1752
1754
  break;
1753
1755
  case "simple":
@@ -1765,7 +1767,7 @@ function addReleaseDeps(deps, config) {
1765
1767
  "tsdown": "0.22.0",
1766
1768
  "typescript": "6.0.3",
1767
1769
  "vitest": "4.1.5",
1768
- "pnpm": "11.0.9"
1770
+ "pnpm": "11.1.1"
1769
1771
  }["commit-and-tag-version"];
1770
1772
  break;
1771
1773
  case "changesets":
@@ -1783,7 +1785,7 @@ function addReleaseDeps(deps, config) {
1783
1785
  "tsdown": "0.22.0",
1784
1786
  "typescript": "6.0.3",
1785
1787
  "vitest": "4.1.5",
1786
- "pnpm": "11.0.9"
1788
+ "pnpm": "11.1.1"
1787
1789
  }["@changesets/cli"];
1788
1790
  break;
1789
1791
  }
@@ -1806,8 +1808,8 @@ async function generatePackageJson(ctx) {
1806
1808
  }
1807
1809
  const devDeps = { ...ROOT_DEV_DEPS };
1808
1810
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1809
- devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.11.0";
1810
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.57.4";
1811
+ devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.11.1";
1812
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.58.0";
1811
1813
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.13.0";
1812
1814
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1813
1815
  "@changesets/cli": "2.31.0",
@@ -1823,7 +1825,7 @@ async function generatePackageJson(ctx) {
1823
1825
  "tsdown": "0.22.0",
1824
1826
  "typescript": "6.0.3",
1825
1827
  "vitest": "4.1.5",
1826
- "pnpm": "11.0.9"
1828
+ "pnpm": "11.1.1"
1827
1829
  }["oxfmt"];
1828
1830
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
1829
1831
  "@changesets/cli": "2.31.0",
@@ -1839,7 +1841,7 @@ async function generatePackageJson(ctx) {
1839
1841
  "tsdown": "0.22.0",
1840
1842
  "typescript": "6.0.3",
1841
1843
  "vitest": "4.1.5",
1842
- "pnpm": "11.0.9"
1844
+ "pnpm": "11.1.1"
1843
1845
  }["prettier"];
1844
1846
  addReleaseDeps(devDeps, ctx.config);
1845
1847
  if (existing) {
@@ -1897,6 +1899,36 @@ async function generatePackageJson(ctx) {
1897
1899
  pkg["engines"] = { node: ">=24.13.0" };
1898
1900
  changes.push("set engines.node >= 24.13.0");
1899
1901
  }
1902
+ const pinnedPnpm = {
1903
+ "@changesets/cli": "2.31.0",
1904
+ "@release-it/bumper": "7.0.5",
1905
+ "@types/node": "24.12.3",
1906
+ "commit-and-tag-version": "12.7.1",
1907
+ "knip": "6.12.1",
1908
+ "lefthook": "2.1.6",
1909
+ "oxfmt": "0.48.0",
1910
+ "oxlint": "1.63.0",
1911
+ "prettier": "3.8.3",
1912
+ "release-it": "20.0.1",
1913
+ "tsdown": "0.22.0",
1914
+ "typescript": "6.0.3",
1915
+ "vitest": "4.1.5",
1916
+ "pnpm": "11.1.1"
1917
+ }["pnpm"];
1918
+ const desiredPm = pinnedPnpm ? `pnpm@${pinnedPnpm}` : void 0;
1919
+ if (desiredPm) {
1920
+ const currentPm = pkg.packageManager;
1921
+ if (!currentPm) {
1922
+ pkg.packageManager = desiredPm;
1923
+ changes.push(`set packageManager: ${desiredPm}`);
1924
+ } else {
1925
+ const match = /^pnpm@(\d+)\./.exec(currentPm);
1926
+ if (match && Number(match[1]) < 11) {
1927
+ pkg.packageManager = desiredPm;
1928
+ changes.push(`bumped packageManager: ${currentPm} → ${desiredPm}`);
1929
+ }
1930
+ }
1931
+ }
1900
1932
  if (changes.length === 0) return {
1901
1933
  filePath,
1902
1934
  action: "skipped",
@@ -1933,7 +1965,7 @@ async function generatePackageJson(ctx) {
1933
1965
  "tsdown": "0.22.0",
1934
1966
  "typescript": "6.0.3",
1935
1967
  "vitest": "4.1.5",
1936
- "pnpm": "11.0.9"
1968
+ "pnpm": "11.1.1"
1937
1969
  }["pnpm"]}`
1938
1970
  };
1939
1971
  ctx.write(filePath, JSON.stringify(pkg, null, 2) + "\n");
@@ -3842,26 +3874,92 @@ async function generateRenovate(ctx) {
3842
3874
  }
3843
3875
  //#endregion
3844
3876
  //#region src/generators/pnpm-workspace.ts
3845
- const WORKSPACE_YAML = `packages:
3877
+ const FILE_PATH = "pnpm-workspace.yaml";
3878
+ const DEFAULT_MIN_RELEASE_AGE = 1440;
3879
+ const FRESH_MONOREPO = `packages:
3846
3880
  - "packages/*"
3881
+
3882
+ blockExoticSubdeps: true
3883
+
3884
+ minimumReleaseAge: ${DEFAULT_MIN_RELEASE_AGE}
3885
+ minimumReleaseAgeExclude:
3886
+ - "@bensandee/*"
3847
3887
  `;
3888
+ const FRESH_SINGLE = `blockExoticSubdeps: true
3889
+
3890
+ minimumReleaseAge: ${DEFAULT_MIN_RELEASE_AGE}
3891
+ minimumReleaseAgeExclude:
3892
+ - "@bensandee/*"
3893
+ `;
3894
+ /** Minimal projection of `.tooling.json` needed by this generator. */
3895
+ const PnpmWorkspaceOptOutSchema = z.object({ pnpmWorkspace: z.literal(false).optional() });
3896
+ function isPnpmWorkspaceDisabled(ctx) {
3897
+ const raw = ctx.read(".tooling.json");
3898
+ if (!raw) return false;
3899
+ try {
3900
+ const result = PnpmWorkspaceOptOutSchema.safeParse(JSON.parse(raw));
3901
+ return result.success && result.data.pnpmWorkspace === false;
3902
+ } catch {
3903
+ return false;
3904
+ }
3905
+ }
3906
+ /**
3907
+ * Apply managed-key contract to an existing doc. Returns the list of changes made.
3908
+ *
3909
+ * Mandated:
3910
+ * - `blockExoticSubdeps: true`
3911
+ * - `minimumReleaseAge`: any non-negative number; seeded to 1440 if missing/invalid.
3912
+ *
3913
+ * Not enforced (left alone after creation):
3914
+ * - `minimumReleaseAgeExclude`
3915
+ * - everything else (`packages`, `allowBuilds`, etc.)
3916
+ */
3917
+ function applyManagedKeys(doc) {
3918
+ const changes = [];
3919
+ if (doc.get("blockExoticSubdeps") !== true) {
3920
+ doc.set("blockExoticSubdeps", true);
3921
+ changes.push("set blockExoticSubdeps: true");
3922
+ }
3923
+ const minAge = doc.get("minimumReleaseAge");
3924
+ if (typeof minAge !== "number" || !Number.isFinite(minAge) || minAge < 0) {
3925
+ doc.set("minimumReleaseAge", DEFAULT_MIN_RELEASE_AGE);
3926
+ changes.push(`set minimumReleaseAge: ${DEFAULT_MIN_RELEASE_AGE}`);
3927
+ }
3928
+ return changes;
3929
+ }
3848
3930
  async function generatePnpmWorkspace(ctx) {
3849
- const filePath = "pnpm-workspace.yaml";
3850
- if (ctx.config.structure !== "monorepo") return {
3851
- filePath,
3931
+ if (isPnpmWorkspaceDisabled(ctx)) return {
3932
+ filePath: FILE_PATH,
3852
3933
  action: "skipped",
3853
- description: "Not a monorepo"
3934
+ description: "pnpmWorkspace: false in .tooling.json"
3854
3935
  };
3855
- if (ctx.exists(filePath)) return {
3856
- filePath,
3936
+ const isMonorepo = ctx.config.structure === "monorepo";
3937
+ const existing = ctx.read(FILE_PATH);
3938
+ if (existing === void 0) {
3939
+ ctx.write(FILE_PATH, isMonorepo ? FRESH_MONOREPO : FRESH_SINGLE);
3940
+ return {
3941
+ filePath: FILE_PATH,
3942
+ action: "created",
3943
+ description: "Generated pnpm-workspace.yaml"
3944
+ };
3945
+ }
3946
+ const doc = parseDocument(existing);
3947
+ if (doc.errors.length > 0) return {
3948
+ filePath: FILE_PATH,
3857
3949
  action: "skipped",
3858
- description: "pnpm-workspace.yaml already exists"
3950
+ description: `Could not parse existing pnpm-workspace.yaml: ${doc.errors[0]?.message ?? "unknown error"}`
3859
3951
  };
3860
- ctx.write(filePath, WORKSPACE_YAML);
3952
+ const changes = applyManagedKeys(doc);
3953
+ if (changes.length === 0) return {
3954
+ filePath: FILE_PATH,
3955
+ action: "skipped",
3956
+ description: "Already up to spec"
3957
+ };
3958
+ ctx.write(FILE_PATH, doc.toString());
3861
3959
  return {
3862
- filePath,
3863
- action: "created",
3864
- description: "Generated pnpm-workspace.yaml"
3960
+ filePath: FILE_PATH,
3961
+ action: "updated",
3962
+ description: changes.join(", ")
3865
3963
  };
3866
3964
  }
3867
3965
  //#endregion
@@ -4718,7 +4816,7 @@ async function runGenerators(ctx) {
4718
4816
  function generateMigratePrompt(results, config, detected) {
4719
4817
  const sections = [];
4720
4818
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4721
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.57.4 repo:sync\` on ${timestamp}_\n`);
4819
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.58.0 repo:sync\` on ${timestamp}_\n`);
4722
4820
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
4723
4821
  if (changed.length > 0) {
4724
4822
  const items = changed.map((r) => {
@@ -8173,7 +8271,7 @@ const out = (msg) => console.log(msg);
8173
8271
  const main = defineCommand({
8174
8272
  meta: {
8175
8273
  name: "bst",
8176
- version: "0.57.4",
8274
+ version: "0.58.0",
8177
8275
  description: "Bootstrap and maintain standardized TypeScript project tooling"
8178
8276
  },
8179
8277
  subCommands: {
@@ -8245,7 +8343,7 @@ const main = defineCommand({
8245
8343
  })
8246
8344
  }
8247
8345
  });
8248
- console.log(`@bensandee/tooling v0.57.4`);
8346
+ console.log(`@bensandee/tooling v0.58.0`);
8249
8347
  async function run() {
8250
8348
  await runMain(main);
8251
8349
  process.exit(process.exitCode ?? 0);
package/dist/index.d.mts CHANGED
@@ -18,6 +18,7 @@ declare const PackageJsonSchema: z.ZodObject<{
18
18
  repository: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
19
19
  url: z.ZodString;
20
20
  }, z.core.$loose>]>>;
21
+ packageManager: z.ZodOptional<z.ZodString>;
21
22
  }, z.core.$loose>;
22
23
  type PackageJson = z.infer<typeof PackageJsonSchema>;
23
24
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.57.4",
3
+ "version": "0.58.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -55,7 +55,7 @@
55
55
  "release-it": "20.0.1",
56
56
  "tsdown": "0.22.0",
57
57
  "@bensandee/claude-hooks": "0.7.4",
58
- "@bensandee/config": "0.11.0"
58
+ "@bensandee/config": "0.11.1"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown",
@@ -258,6 +258,11 @@
258
258
  "additionalProperties": false
259
259
  }
260
260
  ]
261
+ },
262
+ "pnpmWorkspace": {
263
+ "description": "Set to false to opt out of pnpm-workspace.yaml management (skips both generation and the repo:sync --check validation).",
264
+ "type": "boolean",
265
+ "const": false
261
266
  }
262
267
  },
263
268
  "additionalProperties": false