@bensandee/tooling 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -113,6 +113,7 @@ const LEGACY_PATTERNS = {
113
113
  /** Detect existing project state in the target directory. */
114
114
  function detectProject(targetDir) {
115
115
  const exists = (rel) => existsSync(path.join(targetDir, rel));
116
+ const rootPkg = readPackageJson(targetDir);
116
117
  return {
117
118
  hasPackageJson: exists("package.json"),
118
119
  hasTsconfig: exists("tsconfig.json"),
@@ -127,7 +128,8 @@ function detectProject(targetDir) {
127
128
  hasReleaseItConfig: exists(".release-it.json") || exists(".release-it.yaml") || exists(".release-it.toml"),
128
129
  hasSimpleReleaseConfig: exists(".versionrc") || exists(".versionrc.json") || exists(".versionrc.js"),
129
130
  hasChangesetsConfig: exists(".changeset/config.json"),
130
- hasRepositoryField: !!readPackageJson(targetDir)?.repository,
131
+ hasRepositoryField: !!rootPkg?.repository,
132
+ hasCommitAndTagVersion: !!rootPkg?.devDependencies?.["commit-and-tag-version"],
131
133
  legacyConfigs: detectLegacyConfigs(targetDir)
132
134
  };
133
135
  }
@@ -664,6 +666,10 @@ function ensureSchemaComment(content, ci) {
664
666
  if (content.includes("yaml-language-server")) return content;
665
667
  return FORGEJO_SCHEMA_COMMENT + content;
666
668
  }
669
+ /** Migrate content from old tooling binary name to new. */
670
+ function migrateToolingBinary(content) {
671
+ return content.replaceAll("pnpm exec tooling ", "pnpm exec bst ");
672
+ }
667
673
  /** Check if a YAML file has an opt-out comment in the first 10 lines. */
668
674
  function isToolingIgnored(content) {
669
675
  return content.split("\n", 10).some((line) => line.includes(IGNORE_PATTERN));
@@ -797,7 +803,7 @@ function ensureWorkflowConcurrency(existing, concurrency) {
797
803
  }
798
804
  }
799
805
  //#endregion
800
- //#region src/generators/deploy-ci.ts
806
+ //#region src/generators/publish-ci.ts
801
807
  /** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
802
808
  function actionsExpr$2(expr) {
803
809
  return `\${{ ${expr} }}`;
@@ -806,14 +812,14 @@ function hasEnginesNode$2(ctx) {
806
812
  return typeof ctx.packageJson?.["engines"]?.["node"] === "string";
807
813
  }
808
814
  function deployWorkflow(ci, nodeVersionYaml) {
809
- return `${workflowSchemaComment(ci)}name: Deploy
815
+ return `${workflowSchemaComment(ci)}name: Publish
810
816
  on:
811
817
  push:
812
818
  tags:
813
819
  - "v[0-9]+.[0-9]+.[0-9]+"
814
820
 
815
821
  jobs:
816
- deploy:
822
+ publish:
817
823
  runs-on: ubuntu-latest
818
824
  steps:
819
825
  - uses: actions/checkout@v4
@@ -898,48 +904,59 @@ async function generateDeployCi(ctx) {
898
904
  const nodeVersionYaml = hasEnginesNode$2(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
899
905
  const content = deployWorkflow(ctx.config.ci, nodeVersionYaml);
900
906
  if (ctx.exists(workflowPath)) {
901
- const existing = ctx.read(workflowPath);
902
- if (existing) {
903
- if (existing === content || ensureSchemaComment(existing, ctx.config.ci) === content) return {
904
- filePath: workflowPath,
905
- action: "skipped",
906
- description: "Deploy workflow already up to date"
907
- };
908
- const merged = mergeWorkflowSteps(existing, "deploy", requiredDeploySteps());
907
+ const raw = ctx.read(workflowPath);
908
+ if (raw) {
909
+ const existing = migrateToolingBinary(raw);
910
+ if (existing === content || ensureSchemaComment(existing, ctx.config.ci) === content) {
911
+ if (existing !== raw) {
912
+ ctx.write(workflowPath, ensureSchemaComment(existing, ctx.config.ci));
913
+ return {
914
+ filePath: workflowPath,
915
+ action: "updated",
916
+ description: "Migrated tooling binary name in publish workflow"
917
+ };
918
+ }
919
+ return {
920
+ filePath: workflowPath,
921
+ action: "skipped",
922
+ description: "Publish workflow already up to date"
923
+ };
924
+ }
925
+ const merged = mergeWorkflowSteps(existing, "publish", requiredDeploySteps());
909
926
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
910
927
  if (!merged.changed) {
911
- if (withComment !== existing) {
928
+ if (withComment !== raw) {
912
929
  ctx.write(workflowPath, withComment);
913
930
  return {
914
931
  filePath: workflowPath,
915
932
  action: "updated",
916
- description: "Added schema comment to deploy workflow"
933
+ description: existing !== raw ? "Migrated tooling binary name in publish workflow" : "Added schema comment to publish workflow"
917
934
  };
918
935
  }
919
936
  return {
920
937
  filePath: workflowPath,
921
938
  action: "skipped",
922
- description: "Existing deploy workflow preserved"
939
+ description: "Existing publish workflow preserved"
923
940
  };
924
941
  }
925
942
  ctx.write(workflowPath, withComment);
926
943
  return {
927
944
  filePath: workflowPath,
928
945
  action: "updated",
929
- description: "Added missing steps to deploy workflow"
946
+ description: "Added missing steps to publish workflow"
930
947
  };
931
948
  }
932
949
  return {
933
950
  filePath: workflowPath,
934
951
  action: "skipped",
935
- description: "Deploy workflow already up to date"
952
+ description: "Publish workflow already up to date"
936
953
  };
937
954
  }
938
955
  ctx.write(workflowPath, content);
939
956
  return {
940
957
  filePath: workflowPath,
941
958
  action: "created",
942
- description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions deploy workflow`
959
+ description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions publish workflow`
943
960
  };
944
961
  }
945
962
  //#endregion
@@ -969,13 +986,21 @@ const STANDARD_SCRIPTS_MONOREPO = {
969
986
  };
970
987
  /** Scripts that tooling owns — map from script name to keyword that must appear in the value. */
971
988
  const MANAGED_SCRIPTS = {
972
- check: "checks:run",
989
+ check: "bst checks:run",
973
990
  "ci:check": "pnpm check",
974
- "tooling:check": "repo:sync --check",
975
- "tooling:sync": "repo:sync",
976
- "docker:build": "docker:build",
977
- "docker:check": "docker:check"
991
+ "tooling:check": "bst repo:sync --check",
992
+ "tooling:sync": "bst repo:sync",
993
+ "trigger-release": "bst release:trigger",
994
+ "docker:build": "bst docker:build",
995
+ "docker:check": "bst docker:check"
978
996
  };
997
+ /** Check if an existing script value satisfies a managed script requirement.
998
+ * Accepts both `bst <cmd>` and `bin.mjs <cmd>` (used in the tooling repo itself). */
999
+ function matchesManagedScript(scriptValue, expectedFragment) {
1000
+ if (scriptValue.includes(expectedFragment)) return true;
1001
+ const binMjsFragment = expectedFragment.replace(/^bst /, "bin.mjs ");
1002
+ return scriptValue.includes(binMjsFragment);
1003
+ }
979
1004
  /** Deprecated scripts to remove during migration. */
980
1005
  const DEPRECATED_SCRIPTS = ["tooling:init", "tooling:update"];
981
1006
  /** DevDeps that belong in every project (single repo) or per-package (monorepo). */
@@ -1031,7 +1056,7 @@ function getAddedDevDepNames(config) {
1031
1056
  const deps = { ...ROOT_DEV_DEPS };
1032
1057
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
1033
1058
  deps["@bensandee/config"] = "0.9.1";
1034
- deps["@bensandee/tooling"] = "0.27.0";
1059
+ deps["@bensandee/tooling"] = "0.28.0";
1035
1060
  if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
1036
1061
  if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
1037
1062
  addReleaseDeps(deps, config);
@@ -1056,7 +1081,7 @@ async function generatePackageJson(ctx) {
1056
1081
  const devDeps = { ...ROOT_DEV_DEPS };
1057
1082
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1058
1083
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
1059
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.27.0";
1084
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.28.0";
1060
1085
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
1061
1086
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
1062
1087
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
@@ -1074,10 +1099,14 @@ async function generatePackageJson(ctx) {
1074
1099
  changes.push("set type: \"module\"");
1075
1100
  }
1076
1101
  const existingScripts = pkg.scripts ?? {};
1102
+ for (const [key, value] of Object.entries(existingScripts)) if (typeof value === "string" && value.includes("pnpm exec tooling ")) {
1103
+ existingScripts[key] = migrateToolingBinary(value);
1104
+ changes.push(`migrated script: ${key}`);
1105
+ }
1077
1106
  for (const [key, value] of Object.entries(allScripts)) if (!(key in existingScripts)) {
1078
1107
  existingScripts[key] = value;
1079
1108
  changes.push(`added script: ${key}`);
1080
- } else if (key in MANAGED_SCRIPTS && !existingScripts[key]?.includes(MANAGED_SCRIPTS[key])) {
1109
+ } else if (key in MANAGED_SCRIPTS && !matchesManagedScript(existingScripts[key] ?? "", MANAGED_SCRIPTS[key] ?? "")) {
1081
1110
  existingScripts[key] = value;
1082
1111
  changes.push(`updated script: ${key}`);
1083
1112
  }
@@ -2135,7 +2164,7 @@ function actionsExpr(expr) {
2135
2164
  function hasEnginesNode(ctx) {
2136
2165
  return typeof ctx.packageJson?.["engines"]?.["node"] === "string";
2137
2166
  }
2138
- function commonSteps(nodeVersionYaml, publishesNpm) {
2167
+ function commonSteps(nodeVersionYaml, publishesNpm, { build = true } = {}) {
2139
2168
  return ` - uses: actions/checkout@v4
2140
2169
  with:
2141
2170
  fetch-depth: 0
@@ -2144,8 +2173,7 @@ function commonSteps(nodeVersionYaml, publishesNpm) {
2144
2173
  with:
2145
2174
  ${nodeVersionYaml}
2146
2175
  cache: pnpm${publishesNpm ? `\n registry-url: "https://registry.npmjs.org"` : ""}
2147
- - run: pnpm install --frozen-lockfile
2148
- - run: pnpm build`;
2176
+ - run: pnpm install --frozen-lockfile${build ? `\n - run: pnpm build` : ""}`;
2149
2177
  }
2150
2178
  function releaseItWorkflow(ci, nodeVersionYaml, publishesNpm) {
2151
2179
  const isGitHub = ci === "github";
@@ -2199,7 +2227,7 @@ jobs:
2199
2227
  release:
2200
2228
  runs-on: ubuntu-latest
2201
2229
  steps:
2202
- ${commonSteps(nodeVersionYaml, publishesNpm)}${gitConfigStep}${releaseStep}
2230
+ ${commonSteps(nodeVersionYaml, publishesNpm, { build: false })}${gitConfigStep}${releaseStep}
2203
2231
  `;
2204
2232
  }
2205
2233
  /** Build the required release step for the check job (changesets). */
@@ -2263,10 +2291,10 @@ function requiredReleaseSteps(strategy, nodeVersionYaml, publishesNpm) {
2263
2291
  match: { run: "pnpm install" },
2264
2292
  step: { run: "pnpm install --frozen-lockfile" }
2265
2293
  },
2266
- {
2294
+ ...strategy !== "simple" ? [{
2267
2295
  match: { run: "build" },
2268
2296
  step: { run: "pnpm build" }
2269
- }
2297
+ }] : []
2270
2298
  ];
2271
2299
  switch (strategy) {
2272
2300
  case "release-it":
@@ -2299,18 +2327,30 @@ function buildWorkflow(strategy, ci, nodeVersionYaml, publishesNpm) {
2299
2327
  }
2300
2328
  function generateChangesetsReleaseCi(ctx, publishesNpm) {
2301
2329
  const ciPath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
2302
- const existing = ctx.read(ciPath);
2303
- if (!existing) return {
2330
+ const raw = ctx.read(ciPath);
2331
+ if (!raw) return {
2304
2332
  filePath: ciPath,
2305
2333
  action: "skipped",
2306
2334
  description: "CI workflow not found — run check generator first"
2307
2335
  };
2336
+ const existing = migrateToolingBinary(raw);
2308
2337
  const merged = mergeWorkflowSteps(existing, "check", [changesetsReleaseStep(ctx.config.ci, publishesNpm)]);
2309
- if (!merged.changed) return {
2310
- filePath: ciPath,
2311
- action: "skipped",
2312
- description: "Release step in CI workflow already up to date"
2313
- };
2338
+ if (!merged.changed) {
2339
+ if (existing !== raw) {
2340
+ const withComment = ensureSchemaComment(existing, ctx.config.ci);
2341
+ ctx.write(ciPath, withComment);
2342
+ return {
2343
+ filePath: ciPath,
2344
+ action: "updated",
2345
+ description: "Migrated tooling binary name in CI workflow"
2346
+ };
2347
+ }
2348
+ return {
2349
+ filePath: ciPath,
2350
+ action: "skipped",
2351
+ description: "Release step in CI workflow already up to date"
2352
+ };
2353
+ }
2314
2354
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
2315
2355
  ctx.write(ciPath, withComment);
2316
2356
  return {
@@ -2338,22 +2378,33 @@ async function generateReleaseCi(ctx) {
2338
2378
  description: "Release CI workflow not applicable"
2339
2379
  };
2340
2380
  if (ctx.exists(workflowPath)) {
2341
- const existing = ctx.read(workflowPath);
2342
- if (existing) {
2343
- if (existing === content || ensureSchemaComment(existing, ctx.config.ci) === content) return {
2344
- filePath: workflowPath,
2345
- action: "skipped",
2346
- description: "Release workflow already up to date"
2347
- };
2381
+ const raw = ctx.read(workflowPath);
2382
+ if (raw) {
2383
+ const existing = migrateToolingBinary(raw);
2384
+ if (existing === content || ensureSchemaComment(existing, ctx.config.ci) === content) {
2385
+ if (existing !== raw) {
2386
+ ctx.write(workflowPath, ensureSchemaComment(existing, ctx.config.ci));
2387
+ return {
2388
+ filePath: workflowPath,
2389
+ action: "updated",
2390
+ description: "Migrated tooling binary name in release workflow"
2391
+ };
2392
+ }
2393
+ return {
2394
+ filePath: workflowPath,
2395
+ action: "skipped",
2396
+ description: "Release workflow already up to date"
2397
+ };
2398
+ }
2348
2399
  const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml, publishesNpm));
2349
2400
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
2350
2401
  if (!merged.changed) {
2351
- if (withComment !== existing) {
2402
+ if (withComment !== raw) {
2352
2403
  ctx.write(workflowPath, withComment);
2353
2404
  return {
2354
2405
  filePath: workflowPath,
2355
2406
  action: "updated",
2356
- description: "Added schema comment to release workflow"
2407
+ description: existing !== raw ? "Migrated tooling binary name in release workflow" : "Added schema comment to release workflow"
2357
2408
  };
2358
2409
  }
2359
2410
  return {
@@ -2953,8 +3004,11 @@ function runDockerPublish(executor, config) {
2953
3004
  */
2954
3005
  function generateMigratePrompt(results, config, detected) {
2955
3006
  const sections = [];
3007
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2956
3008
  sections.push("# Migration Prompt");
2957
3009
  sections.push("");
3010
+ sections.push(`_Generated by \`@bensandee/tooling@0.28.0 repo:sync\` on ${timestamp}_`);
3011
+ sections.push("");
2958
3012
  sections.push("The following prompt was generated by `@bensandee/tooling repo:sync`. Paste it into Claude Code or another AI assistant to finish migrating this repository.");
2959
3013
  sections.push("");
2960
3014
  sections.push("> **Tip:** Before starting, run `/init` in Claude Code to generate a `CLAUDE.md` that gives the AI a complete picture of your repository's structure, conventions, and build commands.");
@@ -2987,6 +3041,18 @@ function generateMigratePrompt(results, config, detected) {
2987
3041
  }
2988
3042
  sections.push("## Migration tasks");
2989
3043
  sections.push("");
3044
+ if (config.releaseStrategy === "simple" && !detected.hasCommitAndTagVersion) {
3045
+ sections.push("### Add commit-and-tag-version to devDependencies");
3046
+ sections.push("");
3047
+ sections.push("The `simple` release strategy requires `commit-and-tag-version` as a root devDependency so that `pnpm exec commit-and-tag-version` resolves correctly.");
3048
+ sections.push("");
3049
+ sections.push("Run:");
3050
+ sections.push("");
3051
+ sections.push("```sh");
3052
+ sections.push("pnpm add -D -w commit-and-tag-version");
3053
+ sections.push("```");
3054
+ sections.push("");
3055
+ }
2990
3056
  if (config.releaseStrategy !== "none" && !detected.hasRepositoryField) {
2991
3057
  sections.push("### Add repository field to package.json");
2992
3058
  sections.push("");
@@ -4754,7 +4820,7 @@ const dockerCheckCommand = defineCommand({
4754
4820
  const main = defineCommand({
4755
4821
  meta: {
4756
4822
  name: "bst",
4757
- version: "0.27.0",
4823
+ version: "0.28.0",
4758
4824
  description: "Bootstrap and maintain standardized TypeScript project tooling"
4759
4825
  },
4760
4826
  subCommands: {
@@ -4770,7 +4836,7 @@ const main = defineCommand({
4770
4836
  "docker:check": dockerCheckCommand
4771
4837
  }
4772
4838
  });
4773
- console.log(`@bensandee/tooling v0.27.0`);
4839
+ console.log(`@bensandee/tooling v0.28.0`);
4774
4840
  async function run() {
4775
4841
  await runMain(main);
4776
4842
  process.exit(process.exitCode ?? 0);
package/dist/index.d.mts CHANGED
@@ -100,6 +100,8 @@ interface DetectedProjectState {
100
100
  hasChangesetsConfig: boolean;
101
101
  /** Whether package.json has a repository field (needed for release workflows) */
102
102
  hasRepositoryField: boolean;
103
+ /** Whether commit-and-tag-version is in root devDependencies (required for simple release strategy) */
104
+ hasCommitAndTagVersion: boolean;
103
105
  /** Legacy tooling configs found */
104
106
  legacyConfigs: LegacyConfig[];
105
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "bst": "./dist/bin.mjs"