@bensandee/tooling 0.36.0 → 0.37.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.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/dist/bin.mjs +161 -195
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -58,6 +58,26 @@ tooling checks:run --add e2e
58
58
 
59
59
  The generated `ci:check` script defaults to `pnpm check --skip 'docker:*'` since CI environments typically lack Docker support.
60
60
 
61
+ ### Custom blocks in workflows
62
+
63
+ Generated workflow files are regenerated on each `repo:sync` run. To preserve custom additions (extra steps, environment variables, etc.), wrap them in custom block markers:
64
+
65
+ ```yaml
66
+ - name: Run checks
67
+ run: pnpm ci:check
68
+ # @tooling:custom
69
+ - name: Upload coverage
70
+ uses: actions/upload-artifact@v4
71
+ with:
72
+ name: coverage
73
+ path: coverage/
74
+ # @tooling:endcustom
75
+ ```
76
+
77
+ Custom blocks are anchored to the nearest preceding non-blank line. When `repo:sync` regenerates the workflow, it extracts custom blocks from the existing file, generates fresh content, then re-inserts each block after its anchor line. If the anchor line is no longer present (e.g. a step was renamed), the block is appended at the end of the file.
78
+
79
+ To skip a workflow file entirely, add `# @bensandee/tooling:ignore` as the first line — `repo:sync` will leave the file untouched.
80
+
61
81
  ### Release management
62
82
 
63
83
  | Command | Description |
package/dist/bin.mjs CHANGED
@@ -1416,8 +1416,6 @@ function isWorkspacePackage(ctx, packageName) {
1416
1416
  } catch (_error) {}
1417
1417
  return false;
1418
1418
  }
1419
- /** Deps that should not be blindly bumped to latest (version-sensitive). */
1420
- const UPDATE_EXCLUDE = new Set(["@types/node"]);
1421
1419
  /** Add release-strategy-specific devDeps to a deps record. */
1422
1420
  function addReleaseDeps(deps, config) {
1423
1421
  switch (config.releaseStrategy) {
@@ -1493,47 +1491,6 @@ function addReleaseDeps(deps, config) {
1493
1491
  break;
1494
1492
  }
1495
1493
  }
1496
- /** Returns the list of pinned devDependency names that the tool would add for a given config. */
1497
- function getAddedDevDepNames(config) {
1498
- const deps = { ...ROOT_DEV_DEPS };
1499
- if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
1500
- deps["@bensandee/config"] = "0.9.1";
1501
- deps["@bensandee/tooling"] = "0.36.0";
1502
- if (config.formatter === "oxfmt") deps["oxfmt"] = {
1503
- "@changesets/cli": "2.30.0",
1504
- "@release-it/bumper": "7.0.5",
1505
- "@types/node": "24.12.0",
1506
- "commit-and-tag-version": "12.7.0",
1507
- "knip": "5.87.0",
1508
- "lefthook": "2.1.4",
1509
- "oxfmt": "0.41.0",
1510
- "oxlint": "1.56.0",
1511
- "prettier": "3.8.1",
1512
- "release-it": "19.2.4",
1513
- "tsdown": "0.21.4",
1514
- "typescript": "5.9.3",
1515
- "vitest": "4.1.0",
1516
- "pnpm": "10.32.1"
1517
- }["oxfmt"];
1518
- if (config.formatter === "prettier") deps["prettier"] = {
1519
- "@changesets/cli": "2.30.0",
1520
- "@release-it/bumper": "7.0.5",
1521
- "@types/node": "24.12.0",
1522
- "commit-and-tag-version": "12.7.0",
1523
- "knip": "5.87.0",
1524
- "lefthook": "2.1.4",
1525
- "oxfmt": "0.41.0",
1526
- "oxlint": "1.56.0",
1527
- "prettier": "3.8.1",
1528
- "release-it": "19.2.4",
1529
- "tsdown": "0.21.4",
1530
- "typescript": "5.9.3",
1531
- "vitest": "4.1.0",
1532
- "pnpm": "10.32.1"
1533
- }["prettier"];
1534
- addReleaseDeps(deps, config);
1535
- return Object.keys(deps).filter((name) => !UPDATE_EXCLUDE.has(name));
1536
- }
1537
1494
  async function generatePackageJson(ctx) {
1538
1495
  const filePath = "package.json";
1539
1496
  const existing = ctx.read(filePath);
@@ -1553,7 +1510,7 @@ async function generatePackageJson(ctx) {
1553
1510
  const devDeps = { ...ROOT_DEV_DEPS };
1554
1511
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1555
1512
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
1556
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.36.0";
1513
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.37.0";
1557
1514
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
1558
1515
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1559
1516
  "@changesets/cli": "2.30.0",
@@ -2405,6 +2362,7 @@ function buildSettings(ctx) {
2405
2362
  `Bash(${pm} knip)`,
2406
2363
  `Bash(${pm} knip *)`,
2407
2364
  `Bash(${pm} check)`,
2365
+ `Bash(${pm} ci:check)`,
2408
2366
  `Bash(${pm} exec oxlint *)`,
2409
2367
  `Bash(${pm} exec knip *)`,
2410
2368
  "Bash(git status *)",
@@ -2578,7 +2536,7 @@ async function generateClaudeSettings(ctx) {
2578
2536
  }
2579
2537
  //#endregion
2580
2538
  //#region src/generators/release-it.ts
2581
- function buildConfig$2(ci, isMonorepo) {
2539
+ function buildConfig$3(ci, isMonorepo) {
2582
2540
  const config = {
2583
2541
  $schema: "https://unpkg.com/release-it/schema/release-it.json",
2584
2542
  git: {
@@ -2606,7 +2564,7 @@ async function generateReleaseIt(ctx) {
2606
2564
  action: "skipped",
2607
2565
  description: "release-it not requested"
2608
2566
  };
2609
- const content = JSON.stringify(buildConfig$2(ctx.config.ci, ctx.config.structure === "monorepo"), null, 2) + "\n";
2567
+ const content = JSON.stringify(buildConfig$3(ctx.config.ci, ctx.config.structure === "monorepo"), null, 2) + "\n";
2610
2568
  const existing = ctx.read(filePath);
2611
2569
  if (existing) {
2612
2570
  if (contentEqual(filePath, existing, content)) return {
@@ -2629,7 +2587,7 @@ async function generateReleaseIt(ctx) {
2629
2587
  }
2630
2588
  //#endregion
2631
2589
  //#region src/generators/changesets.ts
2632
- function buildConfig$1() {
2590
+ function buildConfig$2() {
2633
2591
  return {
2634
2592
  $schema: "https://unpkg.com/@changesets/config@3.1.1/schema.json",
2635
2593
  changelog: "@changesets/cli/changelog",
@@ -2649,7 +2607,7 @@ async function generateChangesets(ctx) {
2649
2607
  action: "skipped",
2650
2608
  description: "Changesets not requested"
2651
2609
  };
2652
- const content = JSON.stringify(buildConfig$1(), null, 2) + "\n";
2610
+ const content = JSON.stringify(buildConfig$2(), null, 2) + "\n";
2653
2611
  const existing = ctx.read(filePath);
2654
2612
  if (existing) return {
2655
2613
  filePath,
@@ -2836,7 +2794,7 @@ function requiredCommands(formatter) {
2836
2794
  }
2837
2795
  };
2838
2796
  }
2839
- function buildConfig(formatter) {
2797
+ function buildConfig$1(formatter) {
2840
2798
  return [
2841
2799
  "pre-commit:",
2842
2800
  " commands:",
@@ -2940,7 +2898,7 @@ function cleanPackageJson(ctx, results) {
2940
2898
  }
2941
2899
  async function generateLefthook(ctx) {
2942
2900
  const filePath = "lefthook.yml";
2943
- const content = buildConfig(ctx.config.formatter);
2901
+ const content = buildConfig$1(ctx.config.formatter);
2944
2902
  const results = [];
2945
2903
  archiveHuskyHooks(ctx, results);
2946
2904
  archiveLintStagedConfigs(ctx, results);
@@ -3081,6 +3039,72 @@ async function generateVscodeSettings(ctx) {
3081
3039
  return results;
3082
3040
  }
3083
3041
  //#endregion
3042
+ //#region src/generators/versionrc.ts
3043
+ function buildConfig() {
3044
+ return {
3045
+ header: "# Changelog\n\nAll notable changes to this project will be documented in this file.\n",
3046
+ types: [
3047
+ {
3048
+ type: "feat",
3049
+ section: "Features"
3050
+ },
3051
+ {
3052
+ type: "fix",
3053
+ section: "Bug Fixes"
3054
+ },
3055
+ {
3056
+ type: "chore",
3057
+ section: "Chores"
3058
+ },
3059
+ {
3060
+ type: "refactor",
3061
+ section: "Refactoring"
3062
+ },
3063
+ {
3064
+ type: "perf",
3065
+ section: "Performance"
3066
+ },
3067
+ {
3068
+ type: "docs",
3069
+ hidden: true
3070
+ },
3071
+ {
3072
+ type: "style",
3073
+ hidden: true
3074
+ },
3075
+ {
3076
+ type: "test",
3077
+ hidden: true
3078
+ },
3079
+ {
3080
+ type: "ci",
3081
+ hidden: true
3082
+ }
3083
+ ],
3084
+ tagPrefix: "v"
3085
+ };
3086
+ }
3087
+ async function generateVersionrc(ctx) {
3088
+ const filePath = ".versionrc.json";
3089
+ if (ctx.config.releaseStrategy !== "simple") return {
3090
+ filePath,
3091
+ action: "skipped",
3092
+ description: "simple release not requested"
3093
+ };
3094
+ if (ctx.exists(filePath)) return {
3095
+ filePath,
3096
+ action: "skipped",
3097
+ description: "Existing versionrc config preserved"
3098
+ };
3099
+ const content = JSON.stringify(buildConfig(), null, 2) + "\n";
3100
+ ctx.write(filePath, content);
3101
+ return {
3102
+ filePath,
3103
+ action: "created",
3104
+ description: "Generated .versionrc.json"
3105
+ };
3106
+ }
3107
+ //#endregion
3084
3108
  //#region src/generators/pipeline.ts
3085
3109
  /** Run all generators sequentially and return their results. */
3086
3110
  async function runGenerators(ctx) {
@@ -3098,6 +3122,7 @@ async function runGenerators(ctx) {
3098
3122
  results.push(await generateCi(ctx));
3099
3123
  results.push(...await generateClaudeSettings(ctx));
3100
3124
  results.push(await generateReleaseIt(ctx));
3125
+ results.push(await generateVersionrc(ctx));
3101
3126
  results.push(await generateChangesets(ctx));
3102
3127
  results.push(await generateReleaseCi(ctx));
3103
3128
  results.push(await generateDeployCi(ctx));
@@ -3110,51 +3135,30 @@ async function runGenerators(ctx) {
3110
3135
  //#region src/generators/migrate-prompt.ts
3111
3136
  /**
3112
3137
  * Generate a context-aware AI migration prompt based on what the CLI did.
3113
- * This prompt can be pasted into Claude Code (or similar) to finish the migration.
3138
+ * Piped into Claude Code via `cat .tooling-migrate.md | claude`.
3114
3139
  */
3115
3140
  function generateMigratePrompt(results, config, detected) {
3116
3141
  const sections = [];
3117
3142
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3118
3143
  sections.push("# Migration Prompt");
3119
3144
  sections.push("");
3120
- sections.push(`_Generated by \`@bensandee/tooling@0.36.0 repo:sync\` on ${timestamp}_`);
3121
- sections.push("");
3122
- 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.");
3145
+ sections.push(`_Generated by \`@bensandee/tooling@0.37.0 repo:sync\` on ${timestamp}_`);
3123
3146
  sections.push("");
3124
- 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.");
3125
- sections.push("");
3126
- sections.push("## What was changed");
3127
- sections.push("");
3128
- const created = results.filter((r) => r.action === "created");
3129
- const updated = results.filter((r) => r.action === "updated");
3130
- const skipped = results.filter((r) => r.action === "skipped");
3131
- const archived = results.filter((r) => r.action === "archived");
3132
- if (created.length > 0) {
3133
- sections.push("**Created:**");
3134
- for (const r of created) sections.push(`- \`${r.filePath}\` — ${r.description}`);
3135
- sections.push("");
3136
- }
3137
- if (updated.length > 0) {
3138
- sections.push("**Updated:**");
3139
- for (const r of updated) sections.push(`- \`${r.filePath}\` — ${r.description}`);
3140
- sections.push("");
3141
- }
3142
- if (archived.length > 0) {
3143
- sections.push("**Archived:**");
3144
- for (const r of archived) sections.push(`- \`${r.filePath}\` — ${r.description}`);
3147
+ const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
3148
+ if (changed.length > 0) {
3149
+ sections.push("## Files changed");
3145
3150
  sections.push("");
3146
- }
3147
- if (skipped.length > 0) {
3148
- sections.push("**Skipped (review these):**");
3149
- for (const r of skipped) sections.push(`- \`${r.filePath}\` — ${r.description}`);
3151
+ for (const r of changed) {
3152
+ const label = r.action === "archived" ? "archived" : r.action;
3153
+ sections.push(`- \`${r.filePath}\` (${label}) — ${r.description}`);
3154
+ }
3150
3155
  sections.push("");
3151
3156
  }
3152
- sections.push("## Migration tasks");
3153
- sections.push("");
3157
+ const tasks = [];
3154
3158
  const legacyToRemove = detected.legacyConfigs.filter((legacy) => !(legacy.tool === "prettier" && config.formatter === "prettier"));
3155
3159
  if (legacyToRemove.length > 0) {
3156
- sections.push("### Remove legacy tooling");
3157
- sections.push("");
3160
+ tasks.push("### Remove legacy tooling");
3161
+ tasks.push("");
3158
3162
  for (const legacy of legacyToRemove) {
3159
3163
  const replacement = {
3160
3164
  eslint: "oxlint",
@@ -3163,118 +3167,83 @@ function generateMigratePrompt(results, config, detected) {
3163
3167
  webpack: "tsdown",
3164
3168
  rollup: "tsdown"
3165
3169
  }[legacy.tool];
3166
- sections.push(`- Remove ${legacy.tool} config files (${legacy.files.map((f) => `\`${f}\``).join(", ")}). This project now uses **${replacement}**.`);
3167
- sections.push(` - Uninstall ${legacy.tool}-related packages from devDependencies`);
3168
- if (legacy.tool === "eslint") sections.push(" - Migrate any custom ESLint rules that don't have oxlint equivalents");
3169
- if (legacy.tool === "jest") sections.push(" - Migrate any jest-specific test utilities (jest.mock, jest.fn) to vitest equivalents (vi.mock, vi.fn)");
3170
+ tasks.push(`- Remove ${legacy.tool} config files (${legacy.files.map((f) => `\`${f}\``).join(", ")}). This project now uses **${replacement}**.`);
3171
+ tasks.push(` - Uninstall ${legacy.tool}-related packages from devDependencies`);
3172
+ if (legacy.tool === "eslint") tasks.push(" - Migrate any custom ESLint rules that don't have oxlint equivalents");
3173
+ if (legacy.tool === "jest") tasks.push(" - Migrate jest-specific test utilities (jest.mock, jest.fn) to vitest equivalents (vi.mock, vi.fn)");
3170
3174
  }
3171
- sections.push("");
3175
+ tasks.push("");
3172
3176
  }
3177
+ const archived = results.filter((r) => r.action === "archived");
3173
3178
  if (archived.length > 0) {
3174
- sections.push("### Review archived files");
3175
- sections.push("");
3176
- sections.push("The following files were modified or replaced. The originals have been saved to `.tooling-archived/`:");
3177
- sections.push("");
3178
- for (const r of archived) sections.push(`- \`${r.filePath}\` → \`.tooling-archived/${r.filePath}\``);
3179
- sections.push("");
3180
- sections.push("For each archived file, **diff the old version against the new one** and look for features, categories, or modules that were enabled in the original but are missing from the replacement. Focus on broad capability gaps rather than individual rule strictness (in general, being stricter is fine). Examples of what to look for:");
3181
- sections.push("");
3182
- sections.push("- **Lint configs**: enabled plugin categories (e.g. `jsx-a11y`, `import`, `react`, `nextjs`), custom `plugins` or `overrides`, file-scoped rule blocks");
3183
- sections.push("- **TypeScript configs**: compiler features like `jsx`, `paths`, `baseUrl`, or `references` that affect build behavior");
3184
- sections.push("- **Other configs**: feature flags, custom presets, or integrations that go beyond the default template");
3185
- sections.push("");
3186
- sections.push("If the old config had capabilities the new one lacks, port them into the new file. Then:");
3187
- sections.push("");
3188
- sections.push("1. If the project previously used `husky` and `lint-staged`, remove them from `devDependencies`");
3189
- sections.push("2. Delete the `.tooling-archived/` directory when migration is complete");
3190
- sections.push("");
3179
+ tasks.push("### Review archived files");
3180
+ tasks.push("");
3181
+ tasks.push("The originals have been saved to `.tooling-archived/`. For each, diff the old version against the new one and port any capabilities the new file lacks:");
3182
+ tasks.push("");
3183
+ for (const r of archived) tasks.push(`- \`${r.filePath}\` → \`.tooling-archived/${r.filePath}\``);
3184
+ tasks.push("");
3185
+ tasks.push("If the project previously used `husky` and `lint-staged`, remove them from `devDependencies`. Delete `.tooling-archived/` when done.");
3186
+ tasks.push("");
3191
3187
  }
3192
3188
  const oxlintWasSkipped = results.find((r) => r.filePath === "oxlint.config.ts")?.action === "skipped";
3193
3189
  if (detected.hasLegacyOxlintJson) {
3194
- sections.push("### Migrate .oxlintrc.json to oxlint.config.ts");
3195
- sections.push("");
3196
- sections.push("A new `oxlint.config.ts` has been generated using `defineConfig` from the `oxlint` package. The existing `.oxlintrc.json` needs to be migrated:");
3197
- sections.push("");
3198
- sections.push("1. Read `.oxlintrc.json` and compare its `rules` against the rules provided by `@bensandee/config/oxlint/recommended` (check `node_modules/@bensandee/config`). Most standard rules are already included in the recommended config.");
3199
- sections.push("2. If there are any custom rules, overrides, settings, or `jsPlugins` not covered by the recommended config, add them to `oxlint.config.ts` alongside the `extends`.");
3200
- sections.push("3. Delete `.oxlintrc.json`.");
3201
- sections.push("4. Run `pnpm lint` to verify the new config works correctly.");
3202
- sections.push("");
3190
+ tasks.push("### Migrate .oxlintrc.json to oxlint.config.ts");
3191
+ tasks.push("");
3192
+ tasks.push("Compare `.oxlintrc.json` rules against `@bensandee/config/oxlint/recommended`. Port any custom rules/overrides into `oxlint.config.ts`, then delete `.oxlintrc.json`.");
3193
+ tasks.push("");
3203
3194
  } else if (oxlintWasSkipped && detected.hasOxlintConfig) {
3204
- sections.push("### Verify oxlint.config.ts includes recommended rules");
3205
- sections.push("");
3206
- sections.push("The existing `oxlint.config.ts` was kept as-is. Verify that it extends the recommended config from `@bensandee/config/oxlint`:");
3207
- sections.push("");
3208
- sections.push("1. Open `oxlint.config.ts` and check that it imports and extends `@bensandee/config/oxlint/recommended`.");
3209
- sections.push("2. The expected pattern is:");
3210
- sections.push(" ```ts");
3211
- sections.push(" import recommended from \"@bensandee/config/oxlint/recommended\";");
3212
- sections.push(" import { defineConfig } from \"oxlint\";");
3213
- sections.push("");
3214
- sections.push(" export default defineConfig({ extends: [recommended] });");
3215
- sections.push(" ```");
3216
- sections.push("3. If it uses a different pattern, update it to extend the recommended config while preserving any project-specific customizations.");
3217
- sections.push("4. Run `pnpm lint` to verify the config works correctly.");
3218
- sections.push("");
3195
+ tasks.push("### Verify oxlint.config.ts extends recommended rules");
3196
+ tasks.push("");
3197
+ tasks.push("Ensure `oxlint.config.ts` imports and extends `@bensandee/config/oxlint/recommended`:");
3198
+ tasks.push("");
3199
+ tasks.push("```ts");
3200
+ tasks.push("import recommended from \"@bensandee/config/oxlint/recommended\";");
3201
+ tasks.push("import { defineConfig } from \"oxlint\";");
3202
+ tasks.push("");
3203
+ tasks.push("export default defineConfig({ extends: [recommended] });");
3204
+ tasks.push("```");
3205
+ tasks.push("");
3219
3206
  }
3220
3207
  if (config.structure === "monorepo" && !detected.hasPnpmWorkspace) {
3221
- sections.push("### Migrate to monorepo structure");
3222
- sections.push("");
3223
- sections.push("This project was converted from a single repo to a monorepo. Complete the migration:");
3224
- sections.push("");
3225
- sections.push("1. Move existing source into `packages/<name>/` (using the existing package name)");
3226
- sections.push("2. Split the root `package.json` into a root workspace manifest + package-level `package.json`");
3227
- sections.push("3. Move the existing `tsconfig.json` into the package and update the root tsconfig with project references");
3228
- sections.push("4. Create a package-level `tsdown.config.ts` in the new package");
3229
- sections.push("5. Update any import paths or build scripts affected by the move");
3230
- sections.push("");
3208
+ tasks.push("### Migrate to monorepo structure");
3209
+ tasks.push("");
3210
+ tasks.push("This project was converted to a monorepo. Complete the migration:");
3211
+ tasks.push("");
3212
+ tasks.push("1. Move existing source into `packages/<name>/`");
3213
+ tasks.push("2. Split root `package.json` into workspace manifest + package-level `package.json`");
3214
+ tasks.push("3. Move `tsconfig.json` into the package, add project references to root");
3215
+ tasks.push("4. Create a package-level `tsdown.config.ts`");
3216
+ tasks.push("5. Update import paths and build scripts");
3217
+ tasks.push("");
3231
3218
  }
3232
- const skippedConfigs = skipped.filter((r) => r.filePath !== "ci" && r.description !== "Not a monorepo");
3233
- if (skippedConfigs.length > 0) {
3234
- sections.push("### Review skipped files");
3235
- sections.push("");
3236
- sections.push("The following files were left unchanged. Review them for compatibility:");
3219
+ if (results.some((r) => r.filePath === "test/example.test.ts" && r.action === "created")) {
3220
+ tasks.push("### Generate tests");
3221
+ tasks.push("");
3222
+ tasks.push("A starter test was created at `test/example.test.ts`. Review `src/` and create additional tests covering exported functions and edge cases.");
3223
+ tasks.push("");
3224
+ }
3225
+ if (tasks.length > 0) {
3226
+ sections.push("## Migration tasks");
3237
3227
  sections.push("");
3238
- for (const r of skippedConfigs) sections.push(`- \`${r.filePath}\` — ${r.description}`);
3228
+ sections.push(...tasks);
3229
+ sections.push("## Ground rules");
3239
3230
  sections.push("");
3240
- }
3241
- if (results.some((r) => r.filePath === "test/example.test.ts" && r.action === "created")) {
3242
- sections.push("### Generate tests");
3231
+ sections.push("Prefer fixing root causes over suppressing issues:");
3243
3232
  sections.push("");
3244
- sections.push("A starter test was created at `test/example.test.ts`. Now:");
3233
+ sections.push("- **Lint errors**: fix the code, not disable comments");
3234
+ sections.push("- **Type errors**: add proper types, not `any` or `@ts-expect-error`");
3235
+ sections.push("- **Test failures**: fix the test or bug, don't skip/delete");
3236
+ sections.push("- **Knip findings**: remove unused code, don't add ignores");
3245
3237
  sections.push("");
3246
- sections.push("1. Review the existing source code in `src/`");
3247
- sections.push("2. Create additional test files following the starter test's patterns (import style, describe/it structure)");
3248
- sections.push("3. Focus on edge cases and core business logic");
3249
- sections.push("4. Aim for meaningful coverage of exported functions and key code paths");
3238
+ sections.push("It is OK to add new packages (e.g. `zod`, `@bensandee/common`) to resolve errors. Only suppress an issue with a comment explaining why the fix is not feasible.");
3250
3239
  sections.push("");
3251
3240
  }
3252
- sections.push("## Ground rules");
3241
+ sections.push("## Verify");
3253
3242
  sections.push("");
3254
- sections.push("It is OK to add new packages (e.g. `zod`, `@bensandee/common`) if they are needed to resolve errors.");
3243
+ sections.push("Run `pnpm check` and fix any errors. This runs the full pipeline:");
3244
+ sections.push("build → typecheck → lint → test → format → knip");
3255
3245
  sections.push("");
3256
- sections.push("When resolving errors from the checklist below, prefer fixing the root cause over suppressing the issue. For example:");
3257
- sections.push("");
3258
- sections.push("- **Lint errors**: fix the code rather than adding disable comments or rule exceptions");
3259
- sections.push("- **Test failures**: update the test or fix the underlying bug rather than skipping or deleting the test");
3260
- sections.push("- **Knip findings**: remove genuinely unused code/exports/dependencies rather than adding ignores to `knip.config.ts`");
3261
- sections.push("- **Type errors**: add proper types rather than using `any` or `@ts-expect-error`");
3262
- sections.push("");
3263
- sections.push("Only suppress an issue if there is a clear, documented reason why the fix is not feasible (e.g. a third-party type mismatch). Leave a comment explaining why.");
3264
- sections.push("");
3265
- sections.push("## Verification checklist");
3266
- sections.push("");
3267
- sections.push("Run each of these commands and fix any errors before moving on:");
3268
- sections.push("");
3269
- sections.push("1. `pnpm install`");
3270
- const updateCmd = `pnpm update --latest ${getAddedDevDepNames(config).join(" ")}`;
3271
- sections.push(`2. \`${updateCmd}\` — bump added dependencies to their latest versions`);
3272
- sections.push("3. `pnpm typecheck` — fix any type errors");
3273
- sections.push("4. `pnpm build` — fix any build errors");
3274
- sections.push("5. `pnpm test` — fix any test failures");
3275
- sections.push("6. `pnpm lint` — fix the code to satisfy lint rules");
3276
- sections.push("7. `pnpm knip` — remove unused exports, dependencies, and dead code");
3277
- sections.push("8. `pnpm format` — fix any formatting issues");
3246
+ sections.push("When everything passes, run `/init` to generate a `CLAUDE.md` for this repository.");
3278
3247
  sections.push("");
3279
3248
  return sections.join("\n");
3280
3249
  }
@@ -3301,11 +3270,11 @@ function contextAsDockerReader(ctx) {
3301
3270
  function logDetectionSummary(ctx) {
3302
3271
  if (ctx.config.publishDocker) {
3303
3272
  const dockerPackages = detectDockerPackages(contextAsDockerReader(ctx), ctx.targetDir, ctx.config.name);
3304
- if (dockerPackages.length > 0) log.info(`Docker images: ${dockerPackages.map((pkg) => pkg.imageName).join(", ")}`);
3273
+ if (dockerPackages.length > 0) log.info(`Docker images to build and publish: ${dockerPackages.map((pkg) => pkg.imageName).join(", ")}`);
3305
3274
  }
3306
3275
  if (ctx.config.publishNpm) {
3307
3276
  const publishable = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson);
3308
- if (publishable.length > 0) log.info(`npm packages: ${publishable.map((pkg) => pkg.name).join(", ")}`);
3277
+ if (publishable.length > 0) log.info(`npm packages to publish: ${publishable.map((pkg) => pkg.name).join(", ")}`);
3309
3278
  }
3310
3279
  }
3311
3280
  async function runInit(config, options = {}) {
@@ -3353,22 +3322,19 @@ async function runInit(config, options = {}) {
3353
3322
  if (created.length > 0) summaryLines.push(`Created: ${created.map((r) => r.filePath).join(", ")}`);
3354
3323
  if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
3355
3324
  note(summaryLines.join("\n"), "Summary");
3356
- if (!options.noPrompt) {
3357
- log.info(`Migration prompt written to ${promptPath}`);
3358
- log.info("In Claude Code, run: \"Execute the steps in .tooling-migrate.md\"");
3359
- }
3360
- const bensandeeDeps = getAddedDevDepNames(config).filter((name) => name.startsWith("@bensandee/"));
3325
+ if (!options.noPrompt) log.info(`Migration prompt written to ${promptPath}`);
3326
+ const packageJsonChanged = results.some((r) => r.filePath === "package.json" && (r.action === "created" || r.action === "updated"));
3361
3327
  const hasLockfile = ctx.exists("pnpm-lock.yaml");
3362
- if (bensandeeDeps.length > 0 && hasLockfile) {
3363
- log.info("Updating @bensandee/* packages...");
3328
+ if (packageJsonChanged && hasLockfile) {
3329
+ log.info("package.json changed — running pnpm install...");
3364
3330
  try {
3365
- execSync(`pnpm update --latest ${bensandeeDeps.join(" ")}`, {
3331
+ execSync("pnpm install", {
3366
3332
  cwd: config.targetDir,
3367
3333
  stdio: "inherit",
3368
- timeout: 6e4
3334
+ timeout: 12e4
3369
3335
  });
3370
3336
  } catch (_error) {
3371
- log.warn("Could not update @bensandee/* packages — run pnpm install manually");
3337
+ log.warn("pnpm install failed — run it manually");
3372
3338
  }
3373
3339
  }
3374
3340
  if (hasChanges && ctx.exists("package.json")) try {
@@ -3379,10 +3345,10 @@ async function runInit(config, options = {}) {
3379
3345
  });
3380
3346
  } catch (_error) {}
3381
3347
  note([
3382
- "1. Run: pnpm install",
3383
- "2. Run: pnpm check",
3384
- ...options.noPrompt ? [] : ["3. In Claude Code, run: \"Execute the steps in .tooling-migrate.md\""]
3385
- ].join("\n"), "Next steps");
3348
+ ...packageJsonChanged && !hasLockfile ? ["Run: pnpm install"] : [],
3349
+ "Run: pnpm check",
3350
+ ...!options.noPrompt ? ["Run: claude \"Execute the migration steps in .tooling-migrate.md\""] : []
3351
+ ].map((step, i) => `${i + 1}. ${step}`).join("\n"), "Next steps");
3386
3352
  return results;
3387
3353
  }
3388
3354
  //#endregion
@@ -4990,7 +4956,7 @@ const dockerCheckCommand = defineCommand({
4990
4956
  const main = defineCommand({
4991
4957
  meta: {
4992
4958
  name: "bst",
4993
- version: "0.36.0",
4959
+ version: "0.37.0",
4994
4960
  description: "Bootstrap and maintain standardized TypeScript project tooling"
4995
4961
  },
4996
4962
  subCommands: {
@@ -5006,7 +4972,7 @@ const main = defineCommand({
5006
4972
  "docker:check": dockerCheckCommand
5007
4973
  }
5008
4974
  });
5009
- console.log(`@bensandee/tooling v0.36.0`);
4975
+ console.log(`@bensandee/tooling v0.37.0`);
5010
4976
  async function run() {
5011
4977
  await runMain(main);
5012
4978
  process.exit(process.exitCode ?? 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "bst": "./dist/bin.mjs"