@bensandee/tooling 0.6.2 → 0.7.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 +301 -98
- package/package.json +4 -3
package/dist/bin.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
|
|
|
7
7
|
import JSON5 from "json5";
|
|
8
8
|
import { parse } from "jsonc-parser";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
10
11
|
|
|
11
12
|
//#region src/types.ts
|
|
12
13
|
/** Default CI platform when not explicitly chosen. */
|
|
@@ -431,6 +432,32 @@ function createContext(config, confirmOverwrite) {
|
|
|
431
432
|
archivedFiles
|
|
432
433
|
};
|
|
433
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Create a read-only GeneratorContext for dry-run checks.
|
|
437
|
+
* Reads from the real filesystem but captures writes instead of applying them.
|
|
438
|
+
* confirmOverwrite always returns "overwrite" to surface all potential changes.
|
|
439
|
+
*/
|
|
440
|
+
function createDryRunContext(config) {
|
|
441
|
+
const pkgRaw = readFile(config.targetDir, "package.json");
|
|
442
|
+
const pendingWrites = /* @__PURE__ */ new Map();
|
|
443
|
+
const shadow = /* @__PURE__ */ new Map();
|
|
444
|
+
return {
|
|
445
|
+
ctx: {
|
|
446
|
+
config,
|
|
447
|
+
targetDir: config.targetDir,
|
|
448
|
+
packageJson: pkgRaw ? parsePackageJson(pkgRaw) : void 0,
|
|
449
|
+
exists: (rel) => shadow.has(rel) || fileExists(config.targetDir, rel),
|
|
450
|
+
read: (rel) => shadow.get(rel) ?? readFile(config.targetDir, rel),
|
|
451
|
+
write: (rel, content) => {
|
|
452
|
+
pendingWrites.set(rel, content);
|
|
453
|
+
shadow.set(rel, content);
|
|
454
|
+
},
|
|
455
|
+
remove: () => {},
|
|
456
|
+
confirmOverwrite: async () => "overwrite"
|
|
457
|
+
},
|
|
458
|
+
pendingWrites
|
|
459
|
+
};
|
|
460
|
+
}
|
|
434
461
|
|
|
435
462
|
//#endregion
|
|
436
463
|
//#region src/generators/package-json.ts
|
|
@@ -772,6 +799,11 @@ async function generateTsconfig(ctx) {
|
|
|
772
799
|
exclude: ["node_modules", "dist"]
|
|
773
800
|
};
|
|
774
801
|
if (existing) {
|
|
802
|
+
if (existing.includes("// @bensandee/tooling:ignore")) return [{
|
|
803
|
+
filePath,
|
|
804
|
+
action: "skipped",
|
|
805
|
+
description: "Ignored via tooling:ignore comment"
|
|
806
|
+
}];
|
|
775
807
|
const parsed = parseTsconfig(existing);
|
|
776
808
|
const changes = [];
|
|
777
809
|
if (!parsed.extends) {
|
|
@@ -843,6 +875,14 @@ function generateMonorepoPackageTsconfigs(ctx) {
|
|
|
843
875
|
const projectType = detectProjectType(pkg.dir);
|
|
844
876
|
const extendsValue = `@bensandee/config/tsconfig/${projectType}`;
|
|
845
877
|
if (existing) {
|
|
878
|
+
if (existing.includes("// @bensandee/tooling:ignore")) {
|
|
879
|
+
results.push({
|
|
880
|
+
filePath,
|
|
881
|
+
action: "skipped",
|
|
882
|
+
description: "Ignored via tooling:ignore comment"
|
|
883
|
+
});
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
846
886
|
const parsed = parseTsconfig(existing);
|
|
847
887
|
const changes = [];
|
|
848
888
|
if (parsed.extends !== extendsValue) {
|
|
@@ -918,7 +958,12 @@ async function generateVitest(ctx) {
|
|
|
918
958
|
description: "Monorepo: vitest config belongs in individual packages"
|
|
919
959
|
}];
|
|
920
960
|
const configPath = "vitest.config.ts";
|
|
921
|
-
if (ctx.exists(configPath)) if (
|
|
961
|
+
if (ctx.exists(configPath)) if (ctx.read(configPath) === VITEST_CONFIG) results.push({
|
|
962
|
+
filePath: configPath,
|
|
963
|
+
action: "skipped",
|
|
964
|
+
description: "Config already up to date"
|
|
965
|
+
});
|
|
966
|
+
else if (await ctx.confirmOverwrite(configPath) === "skip") results.push({
|
|
922
967
|
filePath: configPath,
|
|
923
968
|
action: "skipped",
|
|
924
969
|
description: "Existing config preserved"
|
|
@@ -972,7 +1017,7 @@ async function generateOxlint(ctx) {
|
|
|
972
1017
|
const content = ctx.config.useEslintPlugin ? CONFIG_WITH_LINT_RULES : CONFIG_PRESET_ONLY;
|
|
973
1018
|
const existing = ctx.read(filePath);
|
|
974
1019
|
if (existing) {
|
|
975
|
-
if (existing === content) return {
|
|
1020
|
+
if (existing === content || existing.includes("@bensandee/config/oxlint")) return {
|
|
976
1021
|
filePath,
|
|
977
1022
|
action: "skipped",
|
|
978
1023
|
description: "Already configured"
|
|
@@ -1008,8 +1053,13 @@ async function generateFormatter(ctx) {
|
|
|
1008
1053
|
}
|
|
1009
1054
|
async function generateOxfmt(ctx) {
|
|
1010
1055
|
const filePath = ".oxfmtrc.json";
|
|
1011
|
-
const
|
|
1012
|
-
if (
|
|
1056
|
+
const existing = ctx.read(filePath);
|
|
1057
|
+
if (existing) {
|
|
1058
|
+
if (existing === OXFMT_CONFIG) return {
|
|
1059
|
+
filePath,
|
|
1060
|
+
action: "skipped",
|
|
1061
|
+
description: "Already configured"
|
|
1062
|
+
};
|
|
1013
1063
|
if (await ctx.confirmOverwrite(filePath) === "skip") return {
|
|
1014
1064
|
filePath,
|
|
1015
1065
|
action: "skipped",
|
|
@@ -1019,7 +1069,7 @@ async function generateOxfmt(ctx) {
|
|
|
1019
1069
|
ctx.write(filePath, OXFMT_CONFIG);
|
|
1020
1070
|
return {
|
|
1021
1071
|
filePath,
|
|
1022
|
-
action:
|
|
1072
|
+
action: existing ? "updated" : "created",
|
|
1023
1073
|
description: "Generated .oxfmtrc.json"
|
|
1024
1074
|
};
|
|
1025
1075
|
}
|
|
@@ -1053,7 +1103,13 @@ async function generateTsdown(ctx) {
|
|
|
1053
1103
|
action: "skipped",
|
|
1054
1104
|
description: "Monorepo: tsdown config belongs in individual packages"
|
|
1055
1105
|
};
|
|
1056
|
-
|
|
1106
|
+
const existing = ctx.read(filePath);
|
|
1107
|
+
if (existing) {
|
|
1108
|
+
if (existing === TSDOWN_CONFIG) return {
|
|
1109
|
+
filePath,
|
|
1110
|
+
action: "skipped",
|
|
1111
|
+
description: "Already configured"
|
|
1112
|
+
};
|
|
1057
1113
|
if (await ctx.confirmOverwrite(filePath) === "skip") return {
|
|
1058
1114
|
filePath,
|
|
1059
1115
|
action: "skipped",
|
|
@@ -1063,23 +1119,25 @@ async function generateTsdown(ctx) {
|
|
|
1063
1119
|
ctx.write(filePath, TSDOWN_CONFIG);
|
|
1064
1120
|
return {
|
|
1065
1121
|
filePath,
|
|
1066
|
-
action: "created",
|
|
1122
|
+
action: existing ? "updated" : "created",
|
|
1067
1123
|
description: "Generated tsdown.config.ts"
|
|
1068
1124
|
};
|
|
1069
1125
|
}
|
|
1070
1126
|
|
|
1071
1127
|
//#endregion
|
|
1072
1128
|
//#region src/generators/gitignore.ts
|
|
1073
|
-
|
|
1129
|
+
/** Entries that every project should have — repo:check flags these as missing. */
|
|
1130
|
+
const REQUIRED_ENTRIES = [
|
|
1074
1131
|
"node_modules/",
|
|
1075
1132
|
"dist/",
|
|
1076
1133
|
"*.tsbuildinfo",
|
|
1077
1134
|
".env",
|
|
1078
1135
|
".env.*",
|
|
1079
|
-
"!.env.example"
|
|
1080
|
-
".tooling-migrate.md",
|
|
1081
|
-
".tooling-archived/"
|
|
1136
|
+
"!.env.example"
|
|
1082
1137
|
];
|
|
1138
|
+
/** Tooling-specific entries added during init/update but not required for repo:check. */
|
|
1139
|
+
const OPTIONAL_ENTRIES = [".tooling-migrate.md", ".tooling-archived/"];
|
|
1140
|
+
const ALL_ENTRIES = [...REQUIRED_ENTRIES, ...OPTIONAL_ENTRIES];
|
|
1083
1141
|
/** Normalize a gitignore entry for comparison: strip leading `/` and trailing `/`. */
|
|
1084
1142
|
function normalizeEntry(entry) {
|
|
1085
1143
|
let s = entry.trim();
|
|
@@ -1092,21 +1150,27 @@ async function generateGitignore(ctx) {
|
|
|
1092
1150
|
const existing = ctx.read(filePath);
|
|
1093
1151
|
if (existing) {
|
|
1094
1152
|
const existingNormalized = new Set(existing.split("\n").map(normalizeEntry).filter((line) => line.length > 0));
|
|
1095
|
-
const missing =
|
|
1153
|
+
const missing = ALL_ENTRIES.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
1096
1154
|
if (missing.length === 0) return {
|
|
1097
1155
|
filePath,
|
|
1098
1156
|
action: "skipped",
|
|
1099
1157
|
description: "Already has all standard entries"
|
|
1100
1158
|
};
|
|
1159
|
+
const missingRequired = REQUIRED_ENTRIES.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
1101
1160
|
const updated = existing.trimEnd() + "\n\n# Added by @bensandee/tooling\n" + missing.join("\n") + "\n";
|
|
1102
1161
|
ctx.write(filePath, updated);
|
|
1162
|
+
if (missingRequired.length === 0) return {
|
|
1163
|
+
filePath,
|
|
1164
|
+
action: "skipped",
|
|
1165
|
+
description: "Only optional entries missing"
|
|
1166
|
+
};
|
|
1103
1167
|
return {
|
|
1104
1168
|
filePath,
|
|
1105
1169
|
action: "updated",
|
|
1106
1170
|
description: `Appended ${String(missing.length)} missing entries`
|
|
1107
1171
|
};
|
|
1108
1172
|
}
|
|
1109
|
-
ctx.write(filePath,
|
|
1173
|
+
ctx.write(filePath, ALL_ENTRIES.join("\n") + "\n");
|
|
1110
1174
|
return {
|
|
1111
1175
|
filePath,
|
|
1112
1176
|
action: "created",
|
|
@@ -1117,7 +1181,9 @@ async function generateGitignore(ctx) {
|
|
|
1117
1181
|
//#endregion
|
|
1118
1182
|
//#region src/generators/ci.ts
|
|
1119
1183
|
function hasEnginesNode$1(ctx) {
|
|
1120
|
-
|
|
1184
|
+
const raw = ctx.read("package.json");
|
|
1185
|
+
if (!raw) return false;
|
|
1186
|
+
return typeof parsePackageJson(raw)?.engines?.["node"] === "string";
|
|
1121
1187
|
}
|
|
1122
1188
|
function ciWorkflow(isMonorepo, nodeVersionYaml) {
|
|
1123
1189
|
return `name: CI
|
|
@@ -1144,6 +1210,7 @@ jobs:
|
|
|
1144
1210
|
- run: ${isMonorepo ? "pnpm -r test" : "pnpm test"}
|
|
1145
1211
|
- run: pnpm format --check
|
|
1146
1212
|
- run: pnpm knip
|
|
1213
|
+
- run: pnpm exec tooling repo:check
|
|
1147
1214
|
`;
|
|
1148
1215
|
}
|
|
1149
1216
|
async function generateCi(ctx) {
|
|
@@ -1158,10 +1225,20 @@ async function generateCi(ctx) {
|
|
|
1158
1225
|
const filePath = isGitHub ? ".github/workflows/check.yml" : ".forgejo/workflows/check.yml";
|
|
1159
1226
|
const content = ciWorkflow(isMonorepo, nodeVersionYaml);
|
|
1160
1227
|
if (ctx.exists(filePath)) {
|
|
1161
|
-
|
|
1228
|
+
const existing = ctx.read(filePath);
|
|
1229
|
+
if (existing && !existing.includes("repo:check")) {
|
|
1230
|
+
const patched = existing.trimEnd() + "\n - run: pnpm exec tooling repo:check\n";
|
|
1231
|
+
ctx.write(filePath, patched);
|
|
1232
|
+
return {
|
|
1233
|
+
filePath,
|
|
1234
|
+
action: "updated",
|
|
1235
|
+
description: "Added `pnpm exec tooling repo:check` step to CI workflow"
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1162
1239
|
filePath,
|
|
1163
1240
|
action: "skipped",
|
|
1164
|
-
description: "
|
|
1241
|
+
description: "CI workflow already up to date"
|
|
1165
1242
|
};
|
|
1166
1243
|
}
|
|
1167
1244
|
ctx.write(filePath, content);
|
|
@@ -1529,18 +1606,11 @@ async function generateChangesets(ctx) {
|
|
|
1529
1606
|
};
|
|
1530
1607
|
const content = JSON.stringify(buildConfig$1(), null, 2) + "\n";
|
|
1531
1608
|
const existing = ctx.read(filePath);
|
|
1532
|
-
if (existing) {
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
};
|
|
1538
|
-
if (await ctx.confirmOverwrite(filePath) === "skip") return {
|
|
1539
|
-
filePath,
|
|
1540
|
-
action: "skipped",
|
|
1541
|
-
description: "Existing changesets config preserved"
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1609
|
+
if (existing) return {
|
|
1610
|
+
filePath,
|
|
1611
|
+
action: "skipped",
|
|
1612
|
+
description: "Existing changesets config preserved"
|
|
1613
|
+
};
|
|
1544
1614
|
ctx.write(filePath, content);
|
|
1545
1615
|
return {
|
|
1546
1616
|
filePath,
|
|
@@ -1706,13 +1776,11 @@ async function generateReleaseCi(ctx) {
|
|
|
1706
1776
|
action: "skipped",
|
|
1707
1777
|
description: "Release CI workflow not applicable"
|
|
1708
1778
|
};
|
|
1709
|
-
if (ctx.exists(workflowPath)) {
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
};
|
|
1715
|
-
}
|
|
1779
|
+
if (ctx.exists(workflowPath)) return {
|
|
1780
|
+
filePath: workflowPath,
|
|
1781
|
+
action: "skipped",
|
|
1782
|
+
description: "Existing release workflow preserved"
|
|
1783
|
+
};
|
|
1716
1784
|
ctx.write(workflowPath, content);
|
|
1717
1785
|
return {
|
|
1718
1786
|
filePath: workflowPath,
|
|
@@ -1833,43 +1901,127 @@ async function generateLefthook(ctx) {
|
|
|
1833
1901
|
archiveLintStagedConfigs(ctx, results);
|
|
1834
1902
|
cleanPackageJson(ctx, results);
|
|
1835
1903
|
const existingPath = LEFTHOOK_CONFIG_PATHS.find((p) => ctx.exists(p));
|
|
1836
|
-
if (existingPath
|
|
1837
|
-
const existing = ctx.read(filePath);
|
|
1838
|
-
if (existing) {
|
|
1839
|
-
if (existing === content) {
|
|
1840
|
-
results.push({
|
|
1841
|
-
filePath,
|
|
1842
|
-
action: "skipped",
|
|
1843
|
-
description: "Already configured"
|
|
1844
|
-
});
|
|
1845
|
-
return results;
|
|
1846
|
-
}
|
|
1847
|
-
if (await ctx.confirmOverwrite(filePath) === "skip") {
|
|
1848
|
-
results.push({
|
|
1849
|
-
filePath,
|
|
1850
|
-
action: "skipped",
|
|
1851
|
-
description: "Existing lefthook config preserved"
|
|
1852
|
-
});
|
|
1853
|
-
return results;
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
} else if (existingPath) {
|
|
1904
|
+
if (existingPath) {
|
|
1857
1905
|
results.push({
|
|
1858
1906
|
filePath: existingPath,
|
|
1859
1907
|
action: "skipped",
|
|
1860
|
-
description:
|
|
1908
|
+
description: "Existing lefthook config preserved"
|
|
1861
1909
|
});
|
|
1862
1910
|
return results;
|
|
1863
1911
|
}
|
|
1864
1912
|
ctx.write(filePath, content);
|
|
1865
1913
|
results.push({
|
|
1866
1914
|
filePath,
|
|
1867
|
-
action:
|
|
1915
|
+
action: "created",
|
|
1868
1916
|
description: "Generated lefthook pre-commit config"
|
|
1869
1917
|
});
|
|
1870
1918
|
return results;
|
|
1871
1919
|
}
|
|
1872
1920
|
|
|
1921
|
+
//#endregion
|
|
1922
|
+
//#region src/generators/pipeline.ts
|
|
1923
|
+
/** Run all generators sequentially and return their results. */
|
|
1924
|
+
async function runGenerators(ctx) {
|
|
1925
|
+
const results = [];
|
|
1926
|
+
results.push(await generatePackageJson(ctx));
|
|
1927
|
+
results.push(await generatePnpmWorkspace(ctx));
|
|
1928
|
+
results.push(...await generateTsconfig(ctx));
|
|
1929
|
+
results.push(await generateTsdown(ctx));
|
|
1930
|
+
results.push(await generateOxlint(ctx));
|
|
1931
|
+
results.push(await generateFormatter(ctx));
|
|
1932
|
+
results.push(...await generateLefthook(ctx));
|
|
1933
|
+
results.push(await generateGitignore(ctx));
|
|
1934
|
+
results.push(await generateKnip(ctx));
|
|
1935
|
+
results.push(await generateRenovate(ctx));
|
|
1936
|
+
results.push(await generateCi(ctx));
|
|
1937
|
+
results.push(await generateClaudeSettings(ctx));
|
|
1938
|
+
results.push(await generateReleaseIt(ctx));
|
|
1939
|
+
results.push(await generateChangesets(ctx));
|
|
1940
|
+
results.push(await generateReleaseCi(ctx));
|
|
1941
|
+
results.push(...await generateVitest(ctx));
|
|
1942
|
+
return results;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
//#endregion
|
|
1946
|
+
//#region src/utils/tooling-config.ts
|
|
1947
|
+
const CONFIG_FILE = ".tooling.json";
|
|
1948
|
+
const ToolingConfigSchema = z.object({
|
|
1949
|
+
structure: z.enum(["single", "monorepo"]).optional(),
|
|
1950
|
+
useEslintPlugin: z.boolean().optional(),
|
|
1951
|
+
formatter: z.enum(["oxfmt", "prettier"]).optional(),
|
|
1952
|
+
setupVitest: z.boolean().optional(),
|
|
1953
|
+
ci: z.enum([
|
|
1954
|
+
"github",
|
|
1955
|
+
"forgejo",
|
|
1956
|
+
"none"
|
|
1957
|
+
]).optional(),
|
|
1958
|
+
setupRenovate: z.boolean().optional(),
|
|
1959
|
+
releaseStrategy: z.enum([
|
|
1960
|
+
"release-it",
|
|
1961
|
+
"commit-and-tag-version",
|
|
1962
|
+
"changesets",
|
|
1963
|
+
"none"
|
|
1964
|
+
]).optional(),
|
|
1965
|
+
projectType: z.enum([
|
|
1966
|
+
"default",
|
|
1967
|
+
"node",
|
|
1968
|
+
"react",
|
|
1969
|
+
"library"
|
|
1970
|
+
]).optional(),
|
|
1971
|
+
detectPackageTypes: z.boolean().optional()
|
|
1972
|
+
});
|
|
1973
|
+
/** Load saved tooling config from the target directory. Returns undefined if missing or invalid. */
|
|
1974
|
+
function loadToolingConfig(targetDir) {
|
|
1975
|
+
const fullPath = path.join(targetDir, CONFIG_FILE);
|
|
1976
|
+
if (!existsSync(fullPath)) return void 0;
|
|
1977
|
+
try {
|
|
1978
|
+
const raw = readFileSync(fullPath, "utf-8");
|
|
1979
|
+
const result = ToolingConfigSchema.safeParse(JSON.parse(raw));
|
|
1980
|
+
return result.success ? result.data : void 0;
|
|
1981
|
+
} catch {
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
/** Save the user's config choices to .tooling.json via the generator context. */
|
|
1986
|
+
function saveToolingConfig(ctx, config) {
|
|
1987
|
+
const saved = {
|
|
1988
|
+
structure: config.structure,
|
|
1989
|
+
useEslintPlugin: config.useEslintPlugin,
|
|
1990
|
+
formatter: config.formatter,
|
|
1991
|
+
setupVitest: config.setupVitest,
|
|
1992
|
+
ci: config.ci,
|
|
1993
|
+
setupRenovate: config.setupRenovate,
|
|
1994
|
+
releaseStrategy: config.releaseStrategy,
|
|
1995
|
+
projectType: config.projectType,
|
|
1996
|
+
detectPackageTypes: config.detectPackageTypes
|
|
1997
|
+
};
|
|
1998
|
+
const content = JSON.stringify(saved, null, 2) + "\n";
|
|
1999
|
+
const existed = ctx.exists(CONFIG_FILE);
|
|
2000
|
+
ctx.write(CONFIG_FILE, content);
|
|
2001
|
+
return {
|
|
2002
|
+
filePath: CONFIG_FILE,
|
|
2003
|
+
action: existed ? "updated" : "created",
|
|
2004
|
+
description: "Saved tooling configuration"
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
/** Merge saved config over detected defaults. Saved values win when present. */
|
|
2008
|
+
function mergeWithSavedConfig(detected, saved) {
|
|
2009
|
+
return {
|
|
2010
|
+
name: detected.name,
|
|
2011
|
+
isNew: detected.isNew,
|
|
2012
|
+
targetDir: detected.targetDir,
|
|
2013
|
+
structure: saved.structure ?? detected.structure,
|
|
2014
|
+
useEslintPlugin: saved.useEslintPlugin ?? detected.useEslintPlugin,
|
|
2015
|
+
formatter: saved.formatter ?? detected.formatter,
|
|
2016
|
+
setupVitest: saved.setupVitest ?? detected.setupVitest,
|
|
2017
|
+
ci: saved.ci ?? detected.ci,
|
|
2018
|
+
setupRenovate: saved.setupRenovate ?? detected.setupRenovate,
|
|
2019
|
+
releaseStrategy: saved.releaseStrategy ?? detected.releaseStrategy,
|
|
2020
|
+
projectType: saved.projectType ?? detected.projectType,
|
|
2021
|
+
detectPackageTypes: saved.detectPackageTypes ?? detected.detectPackageTypes
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1873
2025
|
//#endregion
|
|
1874
2026
|
//#region src/commands/repo-init.ts
|
|
1875
2027
|
const initCommand = defineCommand({
|
|
@@ -1903,10 +2055,14 @@ const initCommand = defineCommand({
|
|
|
1903
2055
|
},
|
|
1904
2056
|
async run({ args }) {
|
|
1905
2057
|
const targetDir = path.resolve(args.dir ?? ".");
|
|
1906
|
-
await runInit(args.yes ?
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2058
|
+
await runInit(args.yes ? (() => {
|
|
2059
|
+
const saved = loadToolingConfig(targetDir);
|
|
2060
|
+
const detected = buildDefaultConfig(targetDir, {
|
|
2061
|
+
eslintPlugin: args["eslint-plugin"] === true ? true : void 0,
|
|
2062
|
+
noCi: args["no-ci"] === true ? true : void 0
|
|
2063
|
+
});
|
|
2064
|
+
return saved ? mergeWithSavedConfig(detected, saved) : detected;
|
|
2065
|
+
})() : await runInitPrompts(targetDir), args["no-prompt"] === true ? { noPrompt: true } : {});
|
|
1910
2066
|
}
|
|
1911
2067
|
});
|
|
1912
2068
|
async function runInit(config, options = {}) {
|
|
@@ -1928,25 +2084,9 @@ async function runInit(config, options = {}) {
|
|
|
1928
2084
|
if (p.isCancel(result)) return "skip";
|
|
1929
2085
|
return result;
|
|
1930
2086
|
}));
|
|
1931
|
-
const results = [];
|
|
1932
2087
|
s.start("Generating configuration files...");
|
|
1933
|
-
results
|
|
1934
|
-
results.push(
|
|
1935
|
-
results.push(...await generateTsconfig(ctx));
|
|
1936
|
-
results.push(await generateTsdown(ctx));
|
|
1937
|
-
results.push(await generateOxlint(ctx));
|
|
1938
|
-
results.push(await generateFormatter(ctx));
|
|
1939
|
-
results.push(...await generateLefthook(ctx));
|
|
1940
|
-
results.push(await generateGitignore(ctx));
|
|
1941
|
-
results.push(await generateKnip(ctx));
|
|
1942
|
-
results.push(await generateRenovate(ctx));
|
|
1943
|
-
results.push(await generateCi(ctx));
|
|
1944
|
-
results.push(await generateClaudeSettings(ctx));
|
|
1945
|
-
results.push(await generateReleaseIt(ctx));
|
|
1946
|
-
results.push(await generateChangesets(ctx));
|
|
1947
|
-
results.push(await generateReleaseCi(ctx));
|
|
1948
|
-
const vitestResults = await generateVitest(ctx);
|
|
1949
|
-
results.push(...vitestResults);
|
|
2088
|
+
const results = await runGenerators(ctx);
|
|
2089
|
+
results.push(saveToolingConfig(ctx, config));
|
|
1950
2090
|
const alreadyArchived = new Set(results.filter((r) => r.action === "archived").map((r) => r.filePath));
|
|
1951
2091
|
for (const rel of archivedFiles) if (!alreadyArchived.has(rel)) results.push({
|
|
1952
2092
|
filePath: rel,
|
|
@@ -1994,7 +2134,7 @@ async function runInit(config, options = {}) {
|
|
|
1994
2134
|
const updateCommand = defineCommand({
|
|
1995
2135
|
meta: {
|
|
1996
2136
|
name: "repo:update",
|
|
1997
|
-
description: "
|
|
2137
|
+
description: "Update managed config and add missing files"
|
|
1998
2138
|
},
|
|
1999
2139
|
args: { dir: {
|
|
2000
2140
|
type: "positional",
|
|
@@ -2006,26 +2146,73 @@ const updateCommand = defineCommand({
|
|
|
2006
2146
|
}
|
|
2007
2147
|
});
|
|
2008
2148
|
async function runUpdate(targetDir) {
|
|
2009
|
-
|
|
2149
|
+
const saved = loadToolingConfig(targetDir);
|
|
2150
|
+
const detected = buildDefaultConfig(targetDir, {});
|
|
2151
|
+
if (!saved) p.log.warn("No .tooling.json found — using detected defaults. Run `tooling repo:init` to save your preferences.");
|
|
2152
|
+
return runInit(saved ? mergeWithSavedConfig(detected, saved) : detected, {
|
|
2010
2153
|
noPrompt: true,
|
|
2011
|
-
confirmOverwrite: async () => "
|
|
2154
|
+
confirmOverwrite: async () => "overwrite"
|
|
2012
2155
|
});
|
|
2013
2156
|
}
|
|
2014
2157
|
|
|
2015
2158
|
//#endregion
|
|
2016
|
-
//#region src/
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2159
|
+
//#region src/commands/repo-check.ts
|
|
2160
|
+
const checkCommand = defineCommand({
|
|
2161
|
+
meta: {
|
|
2162
|
+
name: "repo:check",
|
|
2163
|
+
description: "Check repo for tooling drift (dry-run, CI-friendly)"
|
|
2164
|
+
},
|
|
2165
|
+
args: { dir: {
|
|
2166
|
+
type: "positional",
|
|
2167
|
+
description: "Target directory (default: current directory)",
|
|
2168
|
+
required: false
|
|
2169
|
+
} },
|
|
2170
|
+
async run({ args }) {
|
|
2171
|
+
const exitCode = await runCheck(path.resolve(args.dir ?? "."));
|
|
2172
|
+
process.exitCode = exitCode;
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
async function runCheck(targetDir) {
|
|
2176
|
+
const saved = loadToolingConfig(targetDir);
|
|
2177
|
+
const detected = buildDefaultConfig(targetDir, {});
|
|
2178
|
+
if (!saved) p.log.warn("No .tooling.json found — using detected defaults. Run `tooling repo:init` to save your preferences.");
|
|
2179
|
+
const { ctx, pendingWrites } = createDryRunContext(saved ? mergeWithSavedConfig(detected, saved) : detected);
|
|
2180
|
+
const actionable = (await runGenerators(ctx)).filter((r) => r.action === "created" || r.action === "updated");
|
|
2181
|
+
if (actionable.length === 0) {
|
|
2182
|
+
p.log.success("Repository is up to date.");
|
|
2183
|
+
return 0;
|
|
2184
|
+
}
|
|
2185
|
+
p.log.warn(`${actionable.length} file(s) would be changed by repo:update`);
|
|
2186
|
+
for (const r of actionable) {
|
|
2187
|
+
p.log.info(` ${r.action}: ${r.filePath} — ${r.description}`);
|
|
2188
|
+
const newContent = pendingWrites.get(r.filePath);
|
|
2189
|
+
if (!newContent) continue;
|
|
2190
|
+
const existingPath = path.join(targetDir, r.filePath);
|
|
2191
|
+
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
2192
|
+
if (!existing) {
|
|
2193
|
+
const lineCount = newContent.split("\n").length - 1;
|
|
2194
|
+
p.log.info(` + ${lineCount} new lines`);
|
|
2195
|
+
} else {
|
|
2196
|
+
const diff = lineDiff(existing, newContent);
|
|
2197
|
+
if (diff.length > 0) for (const line of diff) p.log.info(` ${line}`);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return 1;
|
|
2201
|
+
}
|
|
2202
|
+
const normalize = (line) => line.trimEnd();
|
|
2203
|
+
/** Produce a compact line-level diff summary, ignoring whitespace-only differences. */
|
|
2204
|
+
function lineDiff(oldText, newText) {
|
|
2205
|
+
const oldLines = oldText.split("\n").map(normalize);
|
|
2206
|
+
const newLines = newText.split("\n").map(normalize);
|
|
2207
|
+
const oldSet = new Set(oldLines);
|
|
2208
|
+
const newSet = new Set(newLines);
|
|
2209
|
+
const removed = oldLines.filter((l) => l.trim() !== "" && !newSet.has(l));
|
|
2210
|
+
const added = newLines.filter((l) => l.trim() !== "" && !oldSet.has(l));
|
|
2211
|
+
const lines = [];
|
|
2212
|
+
for (const l of removed) lines.push(`- ${l.trim()}`);
|
|
2213
|
+
for (const l of added) lines.push(`+ ${l.trim()}`);
|
|
2214
|
+
return lines;
|
|
2215
|
+
}
|
|
2029
2216
|
|
|
2030
2217
|
//#endregion
|
|
2031
2218
|
//#region src/utils/exec.ts
|
|
@@ -2104,6 +2291,10 @@ function createRealExecutor() {
|
|
|
2104
2291
|
} catch {
|
|
2105
2292
|
return null;
|
|
2106
2293
|
}
|
|
2294
|
+
},
|
|
2295
|
+
writeFile(filePath, content) {
|
|
2296
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2297
|
+
writeFileSync(filePath, content);
|
|
2107
2298
|
}
|
|
2108
2299
|
};
|
|
2109
2300
|
}
|
|
@@ -2319,8 +2510,19 @@ async function runVersionMode(executor, config) {
|
|
|
2319
2510
|
p.log.info("Changesets detected — versioning packages");
|
|
2320
2511
|
const packagesBefore = executor.listWorkspacePackages(config.cwd);
|
|
2321
2512
|
debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
|
|
2513
|
+
const changesetConfigPath = path.join(config.cwd, ".changeset", "config.json");
|
|
2514
|
+
const originalConfig = executor.readFile(changesetConfigPath);
|
|
2515
|
+
if (originalConfig) {
|
|
2516
|
+
const parsed = JSON.parse(originalConfig);
|
|
2517
|
+
if (parsed.commit) {
|
|
2518
|
+
parsed.commit = false;
|
|
2519
|
+
executor.writeFile(changesetConfigPath, JSON.stringify(parsed, null, 2) + "\n");
|
|
2520
|
+
debug(config, "Temporarily disabled changeset commit:true");
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2322
2523
|
const versionResult = executor.exec("pnpm changeset version", { cwd: config.cwd });
|
|
2323
2524
|
debugExec(config, "pnpm changeset version", versionResult);
|
|
2525
|
+
if (originalConfig) executor.writeFile(changesetConfigPath, originalConfig);
|
|
2324
2526
|
if (versionResult.exitCode !== 0) throw new FatalError(`pnpm changeset version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr}`);
|
|
2325
2527
|
debugExec(config, "pnpm install --no-frozen-lockfile", executor.exec("pnpm install --no-frozen-lockfile", { cwd: config.cwd }));
|
|
2326
2528
|
const { title, body } = buildPrContent(executor, config.cwd, packagesBefore);
|
|
@@ -2701,19 +2903,20 @@ function mergeGitHub(dryRun) {
|
|
|
2701
2903
|
const main = defineCommand({
|
|
2702
2904
|
meta: {
|
|
2703
2905
|
name: "tooling",
|
|
2704
|
-
version: "0.
|
|
2906
|
+
version: "0.7.0",
|
|
2705
2907
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
2706
2908
|
},
|
|
2707
2909
|
subCommands: {
|
|
2708
2910
|
"repo:init": initCommand,
|
|
2709
2911
|
"repo:update": updateCommand,
|
|
2912
|
+
"repo:check": checkCommand,
|
|
2710
2913
|
"release:changesets": releaseForgejoCommand,
|
|
2711
2914
|
"release:trigger": releaseTriggerCommand,
|
|
2712
2915
|
"release:create-forgejo-release": createForgejoReleaseCommand,
|
|
2713
2916
|
"release:merge": releaseMergeCommand
|
|
2714
2917
|
}
|
|
2715
2918
|
});
|
|
2716
|
-
console.log(`@bensandee/tooling v0.
|
|
2919
|
+
console.log(`@bensandee/tooling v0.7.0`);
|
|
2717
2920
|
runMain(main);
|
|
2718
2921
|
|
|
2719
2922
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensandee/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tooling": "./dist/bin.mjs"
|
|
@@ -25,14 +25,15 @@
|
|
|
25
25
|
"citty": "^0.2.1",
|
|
26
26
|
"json5": "^2.2.3",
|
|
27
27
|
"jsonc-parser": "^3.3.1",
|
|
28
|
-
"zod": "^4.3.6"
|
|
28
|
+
"zod": "^4.3.6",
|
|
29
|
+
"@bensandee/common": "0.1.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/node": "24.10.11",
|
|
32
33
|
"tsdown": "0.20.3",
|
|
33
34
|
"typescript": "5.9.3",
|
|
34
35
|
"vitest": "4.0.18",
|
|
35
|
-
"@bensandee/config": "0.6.
|
|
36
|
+
"@bensandee/config": "0.6.2"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "tsdown",
|