@bensandee/tooling 0.35.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.
- package/README.md +20 -0
- package/dist/bin.mjs +240 -199
- 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
|
@@ -956,7 +956,76 @@ function runDockerPublish(executor, config) {
|
|
|
956
956
|
//#endregion
|
|
957
957
|
//#region src/utils/yaml-merge.ts
|
|
958
958
|
const IGNORE_PATTERN = "@bensandee/tooling:ignore";
|
|
959
|
+
const CUSTOM_START = "# @tooling:custom";
|
|
960
|
+
const CUSTOM_END = "# @tooling:endcustom";
|
|
959
961
|
const FORGEJO_SCHEMA_COMMENT = "# yaml-language-server: $schema=../../.vscode/forgejo-workflow.schema.json\n";
|
|
962
|
+
/** Extract custom blocks from a workflow file, recording their anchor lines. */
|
|
963
|
+
function extractCustomBlocks(content) {
|
|
964
|
+
const lines = content.split("\n");
|
|
965
|
+
const blocks = [];
|
|
966
|
+
let i = 0;
|
|
967
|
+
while (i < lines.length) if (lines[i].trim() === CUSTOM_START) {
|
|
968
|
+
let anchor = "";
|
|
969
|
+
for (let j = i - 1; j >= 0; j--) if (lines[j].trim() !== "") {
|
|
970
|
+
anchor = lines[j].trim();
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
const blockLines = [lines[i]];
|
|
974
|
+
i++;
|
|
975
|
+
while (i < lines.length && lines[i].trim() !== CUSTOM_END) {
|
|
976
|
+
blockLines.push(lines[i]);
|
|
977
|
+
i++;
|
|
978
|
+
}
|
|
979
|
+
if (i < lines.length) {
|
|
980
|
+
blockLines.push(lines[i]);
|
|
981
|
+
i++;
|
|
982
|
+
}
|
|
983
|
+
blocks.push({
|
|
984
|
+
anchor,
|
|
985
|
+
lines: blockLines
|
|
986
|
+
});
|
|
987
|
+
} else i++;
|
|
988
|
+
return blocks;
|
|
989
|
+
}
|
|
990
|
+
/** Remove custom blocks (markers + content) from a workflow string. */
|
|
991
|
+
function stripCustomBlocks(content) {
|
|
992
|
+
const lines = content.split("\n");
|
|
993
|
+
const result = [];
|
|
994
|
+
let inCustom = false;
|
|
995
|
+
for (const line of lines) {
|
|
996
|
+
if (line.trim() === CUSTOM_START) {
|
|
997
|
+
inCustom = true;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
if (inCustom && line.trim() === CUSTOM_END) {
|
|
1001
|
+
inCustom = false;
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (!inCustom) result.push(line);
|
|
1005
|
+
}
|
|
1006
|
+
return result.join("\n");
|
|
1007
|
+
}
|
|
1008
|
+
/** Insert previously extracted custom blocks into freshly generated content, anchored by preceding line. */
|
|
1009
|
+
function insertCustomBlocks(generated, blocks) {
|
|
1010
|
+
if (blocks.length === 0) return generated;
|
|
1011
|
+
const lines = generated.split("\n");
|
|
1012
|
+
const insertions = [];
|
|
1013
|
+
for (const block of blocks) {
|
|
1014
|
+
let insertAfter = -1;
|
|
1015
|
+
for (let i = 0; i < lines.length; i++) if (lines[i].trim() === block.anchor) {
|
|
1016
|
+
insertAfter = i;
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
if (insertAfter < 0) insertAfter = lines.length - 1;
|
|
1020
|
+
insertions.push({
|
|
1021
|
+
afterIndex: insertAfter,
|
|
1022
|
+
block
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
insertions.sort((a, b) => b.afterIndex - a.afterIndex);
|
|
1026
|
+
for (const { afterIndex, block } of insertions) lines.splice(afterIndex + 1, 0, ...block.lines);
|
|
1027
|
+
return lines.join("\n");
|
|
1028
|
+
}
|
|
960
1029
|
/** Prepend the Forgejo schema comment if it's not already present. No-op for GitHub. */
|
|
961
1030
|
function ensureSchemaComment(content, ci) {
|
|
962
1031
|
if (ci !== "forgejo") return content;
|
|
@@ -1014,7 +1083,7 @@ function mergeLefthookCommands(existing, requiredCommands) {
|
|
|
1014
1083
|
* don't cause unnecessary overwrites.
|
|
1015
1084
|
*/
|
|
1016
1085
|
function normalizeWorkflow(content) {
|
|
1017
|
-
const doc = parseDocument(content.replaceAll(/node\s+packages\/tooling-cli\/dist\/bin\.mjs/g, "pnpm exec bst"));
|
|
1086
|
+
const doc = parseDocument(stripCustomBlocks(content).replaceAll(/node\s+packages\/tooling-cli\/dist\/bin\.mjs/g, "pnpm exec bst"));
|
|
1018
1087
|
doc.comment = null;
|
|
1019
1088
|
doc.commentBefore = null;
|
|
1020
1089
|
visit(doc, {
|
|
@@ -1156,7 +1225,9 @@ async function generateDeployCi(ctx) {
|
|
|
1156
1225
|
description: "Publish workflow already up to date"
|
|
1157
1226
|
};
|
|
1158
1227
|
}
|
|
1159
|
-
|
|
1228
|
+
const withDevPath = preserveDevBinaryPath(content, existing);
|
|
1229
|
+
const final = existing ? insertCustomBlocks(withDevPath, extractCustomBlocks(existing)) : withDevPath;
|
|
1230
|
+
ctx.write(workflowPath, final);
|
|
1160
1231
|
return {
|
|
1161
1232
|
filePath: workflowPath,
|
|
1162
1233
|
action: alreadyExists ? "updated" : "created",
|
|
@@ -1345,8 +1416,6 @@ function isWorkspacePackage(ctx, packageName) {
|
|
|
1345
1416
|
} catch (_error) {}
|
|
1346
1417
|
return false;
|
|
1347
1418
|
}
|
|
1348
|
-
/** Deps that should not be blindly bumped to latest (version-sensitive). */
|
|
1349
|
-
const UPDATE_EXCLUDE = new Set(["@types/node"]);
|
|
1350
1419
|
/** Add release-strategy-specific devDeps to a deps record. */
|
|
1351
1420
|
function addReleaseDeps(deps, config) {
|
|
1352
1421
|
switch (config.releaseStrategy) {
|
|
@@ -1422,47 +1491,6 @@ function addReleaseDeps(deps, config) {
|
|
|
1422
1491
|
break;
|
|
1423
1492
|
}
|
|
1424
1493
|
}
|
|
1425
|
-
/** Returns the list of pinned devDependency names that the tool would add for a given config. */
|
|
1426
|
-
function getAddedDevDepNames(config) {
|
|
1427
|
-
const deps = { ...ROOT_DEV_DEPS };
|
|
1428
|
-
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
1429
|
-
deps["@bensandee/config"] = "0.9.1";
|
|
1430
|
-
deps["@bensandee/tooling"] = "0.35.0";
|
|
1431
|
-
if (config.formatter === "oxfmt") deps["oxfmt"] = {
|
|
1432
|
-
"@changesets/cli": "2.30.0",
|
|
1433
|
-
"@release-it/bumper": "7.0.5",
|
|
1434
|
-
"@types/node": "24.12.0",
|
|
1435
|
-
"commit-and-tag-version": "12.7.0",
|
|
1436
|
-
"knip": "5.87.0",
|
|
1437
|
-
"lefthook": "2.1.4",
|
|
1438
|
-
"oxfmt": "0.41.0",
|
|
1439
|
-
"oxlint": "1.56.0",
|
|
1440
|
-
"prettier": "3.8.1",
|
|
1441
|
-
"release-it": "19.2.4",
|
|
1442
|
-
"tsdown": "0.21.4",
|
|
1443
|
-
"typescript": "5.9.3",
|
|
1444
|
-
"vitest": "4.1.0",
|
|
1445
|
-
"pnpm": "10.32.1"
|
|
1446
|
-
}["oxfmt"];
|
|
1447
|
-
if (config.formatter === "prettier") deps["prettier"] = {
|
|
1448
|
-
"@changesets/cli": "2.30.0",
|
|
1449
|
-
"@release-it/bumper": "7.0.5",
|
|
1450
|
-
"@types/node": "24.12.0",
|
|
1451
|
-
"commit-and-tag-version": "12.7.0",
|
|
1452
|
-
"knip": "5.87.0",
|
|
1453
|
-
"lefthook": "2.1.4",
|
|
1454
|
-
"oxfmt": "0.41.0",
|
|
1455
|
-
"oxlint": "1.56.0",
|
|
1456
|
-
"prettier": "3.8.1",
|
|
1457
|
-
"release-it": "19.2.4",
|
|
1458
|
-
"tsdown": "0.21.4",
|
|
1459
|
-
"typescript": "5.9.3",
|
|
1460
|
-
"vitest": "4.1.0",
|
|
1461
|
-
"pnpm": "10.32.1"
|
|
1462
|
-
}["prettier"];
|
|
1463
|
-
addReleaseDeps(deps, config);
|
|
1464
|
-
return Object.keys(deps).filter((name) => !UPDATE_EXCLUDE.has(name));
|
|
1465
|
-
}
|
|
1466
1494
|
async function generatePackageJson(ctx) {
|
|
1467
1495
|
const filePath = "package.json";
|
|
1468
1496
|
const existing = ctx.read(filePath);
|
|
@@ -1482,7 +1510,7 @@ async function generatePackageJson(ctx) {
|
|
|
1482
1510
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1483
1511
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1484
1512
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
|
|
1485
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1513
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.37.0";
|
|
1486
1514
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
1487
1515
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1488
1516
|
"@changesets/cli": "2.30.0",
|
|
@@ -2116,7 +2144,9 @@ async function generateCi(ctx) {
|
|
|
2116
2144
|
description: "CI workflow already up to date"
|
|
2117
2145
|
};
|
|
2118
2146
|
}
|
|
2119
|
-
|
|
2147
|
+
const withDevPath = preserveDevBinaryPath(content, existing);
|
|
2148
|
+
const final = existing ? insertCustomBlocks(withDevPath, extractCustomBlocks(existing)) : withDevPath;
|
|
2149
|
+
ctx.write(filePath, final);
|
|
2120
2150
|
return {
|
|
2121
2151
|
filePath,
|
|
2122
2152
|
action: alreadyExists ? "updated" : "created",
|
|
@@ -2332,6 +2362,7 @@ function buildSettings(ctx) {
|
|
|
2332
2362
|
`Bash(${pm} knip)`,
|
|
2333
2363
|
`Bash(${pm} knip *)`,
|
|
2334
2364
|
`Bash(${pm} check)`,
|
|
2365
|
+
`Bash(${pm} ci:check)`,
|
|
2335
2366
|
`Bash(${pm} exec oxlint *)`,
|
|
2336
2367
|
`Bash(${pm} exec knip *)`,
|
|
2337
2368
|
"Bash(git status *)",
|
|
@@ -2505,7 +2536,7 @@ async function generateClaudeSettings(ctx) {
|
|
|
2505
2536
|
}
|
|
2506
2537
|
//#endregion
|
|
2507
2538
|
//#region src/generators/release-it.ts
|
|
2508
|
-
function buildConfig$
|
|
2539
|
+
function buildConfig$3(ci, isMonorepo) {
|
|
2509
2540
|
const config = {
|
|
2510
2541
|
$schema: "https://unpkg.com/release-it/schema/release-it.json",
|
|
2511
2542
|
git: {
|
|
@@ -2533,7 +2564,7 @@ async function generateReleaseIt(ctx) {
|
|
|
2533
2564
|
action: "skipped",
|
|
2534
2565
|
description: "release-it not requested"
|
|
2535
2566
|
};
|
|
2536
|
-
const content = JSON.stringify(buildConfig$
|
|
2567
|
+
const content = JSON.stringify(buildConfig$3(ctx.config.ci, ctx.config.structure === "monorepo"), null, 2) + "\n";
|
|
2537
2568
|
const existing = ctx.read(filePath);
|
|
2538
2569
|
if (existing) {
|
|
2539
2570
|
if (contentEqual(filePath, existing, content)) return {
|
|
@@ -2556,7 +2587,7 @@ async function generateReleaseIt(ctx) {
|
|
|
2556
2587
|
}
|
|
2557
2588
|
//#endregion
|
|
2558
2589
|
//#region src/generators/changesets.ts
|
|
2559
|
-
function buildConfig$
|
|
2590
|
+
function buildConfig$2() {
|
|
2560
2591
|
return {
|
|
2561
2592
|
$schema: "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
|
2562
2593
|
changelog: "@changesets/cli/changelog",
|
|
@@ -2576,7 +2607,7 @@ async function generateChangesets(ctx) {
|
|
|
2576
2607
|
action: "skipped",
|
|
2577
2608
|
description: "Changesets not requested"
|
|
2578
2609
|
};
|
|
2579
|
-
const content = JSON.stringify(buildConfig$
|
|
2610
|
+
const content = JSON.stringify(buildConfig$2(), null, 2) + "\n";
|
|
2580
2611
|
const existing = ctx.read(filePath);
|
|
2581
2612
|
if (existing) return {
|
|
2582
2613
|
filePath,
|
|
@@ -2743,7 +2774,9 @@ async function generateReleaseCi(ctx) {
|
|
|
2743
2774
|
description: "Release workflow already up to date"
|
|
2744
2775
|
};
|
|
2745
2776
|
}
|
|
2746
|
-
|
|
2777
|
+
const withDevPath = preserveDevBinaryPath(content, existing);
|
|
2778
|
+
const final = existing ? insertCustomBlocks(withDevPath, extractCustomBlocks(existing)) : withDevPath;
|
|
2779
|
+
ctx.write(workflowPath, final);
|
|
2747
2780
|
return {
|
|
2748
2781
|
filePath: workflowPath,
|
|
2749
2782
|
action: alreadyExists ? "updated" : "created",
|
|
@@ -2761,7 +2794,7 @@ function requiredCommands(formatter) {
|
|
|
2761
2794
|
}
|
|
2762
2795
|
};
|
|
2763
2796
|
}
|
|
2764
|
-
function buildConfig(formatter) {
|
|
2797
|
+
function buildConfig$1(formatter) {
|
|
2765
2798
|
return [
|
|
2766
2799
|
"pre-commit:",
|
|
2767
2800
|
" commands:",
|
|
@@ -2865,7 +2898,7 @@ function cleanPackageJson(ctx, results) {
|
|
|
2865
2898
|
}
|
|
2866
2899
|
async function generateLefthook(ctx) {
|
|
2867
2900
|
const filePath = "lefthook.yml";
|
|
2868
|
-
const content = buildConfig(ctx.config.formatter);
|
|
2901
|
+
const content = buildConfig$1(ctx.config.formatter);
|
|
2869
2902
|
const results = [];
|
|
2870
2903
|
archiveHuskyHooks(ctx, results);
|
|
2871
2904
|
archiveLintStagedConfigs(ctx, results);
|
|
@@ -3006,6 +3039,72 @@ async function generateVscodeSettings(ctx) {
|
|
|
3006
3039
|
return results;
|
|
3007
3040
|
}
|
|
3008
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
|
|
3009
3108
|
//#region src/generators/pipeline.ts
|
|
3010
3109
|
/** Run all generators sequentially and return their results. */
|
|
3011
3110
|
async function runGenerators(ctx) {
|
|
@@ -3023,6 +3122,7 @@ async function runGenerators(ctx) {
|
|
|
3023
3122
|
results.push(await generateCi(ctx));
|
|
3024
3123
|
results.push(...await generateClaudeSettings(ctx));
|
|
3025
3124
|
results.push(await generateReleaseIt(ctx));
|
|
3125
|
+
results.push(await generateVersionrc(ctx));
|
|
3026
3126
|
results.push(await generateChangesets(ctx));
|
|
3027
3127
|
results.push(await generateReleaseCi(ctx));
|
|
3028
3128
|
results.push(await generateDeployCi(ctx));
|
|
@@ -3035,51 +3135,30 @@ async function runGenerators(ctx) {
|
|
|
3035
3135
|
//#region src/generators/migrate-prompt.ts
|
|
3036
3136
|
/**
|
|
3037
3137
|
* Generate a context-aware AI migration prompt based on what the CLI did.
|
|
3038
|
-
*
|
|
3138
|
+
* Piped into Claude Code via `cat .tooling-migrate.md | claude`.
|
|
3039
3139
|
*/
|
|
3040
3140
|
function generateMigratePrompt(results, config, detected) {
|
|
3041
3141
|
const sections = [];
|
|
3042
3142
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3043
3143
|
sections.push("# Migration Prompt");
|
|
3044
3144
|
sections.push("");
|
|
3045
|
-
sections.push(`_Generated by \`@bensandee/tooling@0.
|
|
3046
|
-
sections.push("");
|
|
3047
|
-
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.");
|
|
3048
|
-
sections.push("");
|
|
3049
|
-
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.");
|
|
3145
|
+
sections.push(`_Generated by \`@bensandee/tooling@0.37.0 repo:sync\` on ${timestamp}_`);
|
|
3050
3146
|
sections.push("");
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
const updated = results.filter((r) => r.action === "updated");
|
|
3055
|
-
const skipped = results.filter((r) => r.action === "skipped");
|
|
3056
|
-
const archived = results.filter((r) => r.action === "archived");
|
|
3057
|
-
if (created.length > 0) {
|
|
3058
|
-
sections.push("**Created:**");
|
|
3059
|
-
for (const r of created) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
3060
|
-
sections.push("");
|
|
3061
|
-
}
|
|
3062
|
-
if (updated.length > 0) {
|
|
3063
|
-
sections.push("**Updated:**");
|
|
3064
|
-
for (const r of updated) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
3065
|
-
sections.push("");
|
|
3066
|
-
}
|
|
3067
|
-
if (archived.length > 0) {
|
|
3068
|
-
sections.push("**Archived:**");
|
|
3069
|
-
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");
|
|
3070
3150
|
sections.push("");
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3151
|
+
for (const r of changed) {
|
|
3152
|
+
const label = r.action === "archived" ? "archived" : r.action;
|
|
3153
|
+
sections.push(`- \`${r.filePath}\` (${label}) — ${r.description}`);
|
|
3154
|
+
}
|
|
3075
3155
|
sections.push("");
|
|
3076
3156
|
}
|
|
3077
|
-
|
|
3078
|
-
sections.push("");
|
|
3157
|
+
const tasks = [];
|
|
3079
3158
|
const legacyToRemove = detected.legacyConfigs.filter((legacy) => !(legacy.tool === "prettier" && config.formatter === "prettier"));
|
|
3080
3159
|
if (legacyToRemove.length > 0) {
|
|
3081
|
-
|
|
3082
|
-
|
|
3160
|
+
tasks.push("### Remove legacy tooling");
|
|
3161
|
+
tasks.push("");
|
|
3083
3162
|
for (const legacy of legacyToRemove) {
|
|
3084
3163
|
const replacement = {
|
|
3085
3164
|
eslint: "oxlint",
|
|
@@ -3088,118 +3167,83 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
3088
3167
|
webpack: "tsdown",
|
|
3089
3168
|
rollup: "tsdown"
|
|
3090
3169
|
}[legacy.tool];
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
if (legacy.tool === "eslint")
|
|
3094
|
-
if (legacy.tool === "jest")
|
|
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)");
|
|
3095
3174
|
}
|
|
3096
|
-
|
|
3175
|
+
tasks.push("");
|
|
3097
3176
|
}
|
|
3177
|
+
const archived = results.filter((r) => r.action === "archived");
|
|
3098
3178
|
if (archived.length > 0) {
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
for (const r of archived)
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
sections.push("- **Lint configs**: enabled plugin categories (e.g. `jsx-a11y`, `import`, `react`, `nextjs`), custom `plugins` or `overrides`, file-scoped rule blocks");
|
|
3108
|
-
sections.push("- **TypeScript configs**: compiler features like `jsx`, `paths`, `baseUrl`, or `references` that affect build behavior");
|
|
3109
|
-
sections.push("- **Other configs**: feature flags, custom presets, or integrations that go beyond the default template");
|
|
3110
|
-
sections.push("");
|
|
3111
|
-
sections.push("If the old config had capabilities the new one lacks, port them into the new file. Then:");
|
|
3112
|
-
sections.push("");
|
|
3113
|
-
sections.push("1. If the project previously used `husky` and `lint-staged`, remove them from `devDependencies`");
|
|
3114
|
-
sections.push("2. Delete the `.tooling-archived/` directory when migration is complete");
|
|
3115
|
-
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("");
|
|
3116
3187
|
}
|
|
3117
3188
|
const oxlintWasSkipped = results.find((r) => r.filePath === "oxlint.config.ts")?.action === "skipped";
|
|
3118
3189
|
if (detected.hasLegacyOxlintJson) {
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
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.");
|
|
3124
|
-
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`.");
|
|
3125
|
-
sections.push("3. Delete `.oxlintrc.json`.");
|
|
3126
|
-
sections.push("4. Run `pnpm lint` to verify the new config works correctly.");
|
|
3127
|
-
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("");
|
|
3128
3194
|
} else if (oxlintWasSkipped && detected.hasOxlintConfig) {
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
sections.push(" ```");
|
|
3141
|
-
sections.push("3. If it uses a different pattern, update it to extend the recommended config while preserving any project-specific customizations.");
|
|
3142
|
-
sections.push("4. Run `pnpm lint` to verify the config works correctly.");
|
|
3143
|
-
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("");
|
|
3144
3206
|
}
|
|
3145
3207
|
if (config.structure === "monorepo" && !detected.hasPnpmWorkspace) {
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
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("");
|
|
3156
3218
|
}
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
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");
|
|
3162
3227
|
sections.push("");
|
|
3163
|
-
|
|
3228
|
+
sections.push(...tasks);
|
|
3229
|
+
sections.push("## Ground rules");
|
|
3164
3230
|
sections.push("");
|
|
3165
|
-
|
|
3166
|
-
if (results.some((r) => r.filePath === "test/example.test.ts" && r.action === "created")) {
|
|
3167
|
-
sections.push("### Generate tests");
|
|
3231
|
+
sections.push("Prefer fixing root causes over suppressing issues:");
|
|
3168
3232
|
sections.push("");
|
|
3169
|
-
sections.push("
|
|
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");
|
|
3170
3237
|
sections.push("");
|
|
3171
|
-
sections.push("
|
|
3172
|
-
sections.push("2. Create additional test files following the starter test's patterns (import style, describe/it structure)");
|
|
3173
|
-
sections.push("3. Focus on edge cases and core business logic");
|
|
3174
|
-
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.");
|
|
3175
3239
|
sections.push("");
|
|
3176
3240
|
}
|
|
3177
|
-
sections.push("##
|
|
3178
|
-
sections.push("");
|
|
3179
|
-
sections.push("It is OK to add new packages (e.g. `zod`, `@bensandee/common`) if they are needed to resolve errors.");
|
|
3180
|
-
sections.push("");
|
|
3181
|
-
sections.push("When resolving errors from the checklist below, prefer fixing the root cause over suppressing the issue. For example:");
|
|
3182
|
-
sections.push("");
|
|
3183
|
-
sections.push("- **Lint errors**: fix the code rather than adding disable comments or rule exceptions");
|
|
3184
|
-
sections.push("- **Test failures**: update the test or fix the underlying bug rather than skipping or deleting the test");
|
|
3185
|
-
sections.push("- **Knip findings**: remove genuinely unused code/exports/dependencies rather than adding ignores to `knip.config.ts`");
|
|
3186
|
-
sections.push("- **Type errors**: add proper types rather than using `any` or `@ts-expect-error`");
|
|
3241
|
+
sections.push("## Verify");
|
|
3187
3242
|
sections.push("");
|
|
3188
|
-
sections.push("
|
|
3243
|
+
sections.push("Run `pnpm check` and fix any errors. This runs the full pipeline:");
|
|
3244
|
+
sections.push("build → typecheck → lint → test → format → knip");
|
|
3189
3245
|
sections.push("");
|
|
3190
|
-
sections.push("
|
|
3191
|
-
sections.push("");
|
|
3192
|
-
sections.push("Run each of these commands and fix any errors before moving on:");
|
|
3193
|
-
sections.push("");
|
|
3194
|
-
sections.push("1. `pnpm install`");
|
|
3195
|
-
const updateCmd = `pnpm update --latest ${getAddedDevDepNames(config).join(" ")}`;
|
|
3196
|
-
sections.push(`2. \`${updateCmd}\` — bump added dependencies to their latest versions`);
|
|
3197
|
-
sections.push("3. `pnpm typecheck` — fix any type errors");
|
|
3198
|
-
sections.push("4. `pnpm build` — fix any build errors");
|
|
3199
|
-
sections.push("5. `pnpm test` — fix any test failures");
|
|
3200
|
-
sections.push("6. `pnpm lint` — fix the code to satisfy lint rules");
|
|
3201
|
-
sections.push("7. `pnpm knip` — remove unused exports, dependencies, and dead code");
|
|
3202
|
-
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.");
|
|
3203
3247
|
sections.push("");
|
|
3204
3248
|
return sections.join("\n");
|
|
3205
3249
|
}
|
|
@@ -3226,11 +3270,11 @@ function contextAsDockerReader(ctx) {
|
|
|
3226
3270
|
function logDetectionSummary(ctx) {
|
|
3227
3271
|
if (ctx.config.publishDocker) {
|
|
3228
3272
|
const dockerPackages = detectDockerPackages(contextAsDockerReader(ctx), ctx.targetDir, ctx.config.name);
|
|
3229
|
-
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(", ")}`);
|
|
3230
3274
|
}
|
|
3231
3275
|
if (ctx.config.publishNpm) {
|
|
3232
3276
|
const publishable = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson);
|
|
3233
|
-
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(", ")}`);
|
|
3234
3278
|
}
|
|
3235
3279
|
}
|
|
3236
3280
|
async function runInit(config, options = {}) {
|
|
@@ -3278,22 +3322,19 @@ async function runInit(config, options = {}) {
|
|
|
3278
3322
|
if (created.length > 0) summaryLines.push(`Created: ${created.map((r) => r.filePath).join(", ")}`);
|
|
3279
3323
|
if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
|
|
3280
3324
|
note(summaryLines.join("\n"), "Summary");
|
|
3281
|
-
if (!options.noPrompt) {
|
|
3282
|
-
|
|
3283
|
-
log.info("In Claude Code, run: \"Execute the steps in .tooling-migrate.md\"");
|
|
3284
|
-
}
|
|
3285
|
-
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"));
|
|
3286
3327
|
const hasLockfile = ctx.exists("pnpm-lock.yaml");
|
|
3287
|
-
if (
|
|
3288
|
-
log.info("
|
|
3328
|
+
if (packageJsonChanged && hasLockfile) {
|
|
3329
|
+
log.info("package.json changed — running pnpm install...");
|
|
3289
3330
|
try {
|
|
3290
|
-
execSync(
|
|
3331
|
+
execSync("pnpm install", {
|
|
3291
3332
|
cwd: config.targetDir,
|
|
3292
3333
|
stdio: "inherit",
|
|
3293
|
-
timeout:
|
|
3334
|
+
timeout: 12e4
|
|
3294
3335
|
});
|
|
3295
3336
|
} catch (_error) {
|
|
3296
|
-
log.warn("
|
|
3337
|
+
log.warn("pnpm install failed — run it manually");
|
|
3297
3338
|
}
|
|
3298
3339
|
}
|
|
3299
3340
|
if (hasChanges && ctx.exists("package.json")) try {
|
|
@@ -3304,10 +3345,10 @@ async function runInit(config, options = {}) {
|
|
|
3304
3345
|
});
|
|
3305
3346
|
} catch (_error) {}
|
|
3306
3347
|
note([
|
|
3307
|
-
"
|
|
3308
|
-
"
|
|
3309
|
-
|
|
3310
|
-
].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");
|
|
3311
3352
|
return results;
|
|
3312
3353
|
}
|
|
3313
3354
|
//#endregion
|
|
@@ -4915,7 +4956,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
4915
4956
|
const main = defineCommand({
|
|
4916
4957
|
meta: {
|
|
4917
4958
|
name: "bst",
|
|
4918
|
-
version: "0.
|
|
4959
|
+
version: "0.37.0",
|
|
4919
4960
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
4920
4961
|
},
|
|
4921
4962
|
subCommands: {
|
|
@@ -4931,7 +4972,7 @@ const main = defineCommand({
|
|
|
4931
4972
|
"docker:check": dockerCheckCommand
|
|
4932
4973
|
}
|
|
4933
4974
|
});
|
|
4934
|
-
console.log(`@bensandee/tooling v0.
|
|
4975
|
+
console.log(`@bensandee/tooling v0.37.0`);
|
|
4935
4976
|
async function run() {
|
|
4936
4977
|
await runMain(main);
|
|
4937
4978
|
process.exit(process.exitCode ?? 0);
|