@bensandee/tooling 0.9.0 → 0.10.1
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 +310 -89
- package/package.json +2 -1
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 { isMap, isSeq, parseDocument } from "yaml";
|
|
10
11
|
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
11
12
|
//#region src/types.ts
|
|
12
13
|
/** Default CI platform when not explicitly chosen. */
|
|
@@ -414,6 +415,19 @@ function buildDefaultConfig(targetDir, flags) {
|
|
|
414
415
|
}
|
|
415
416
|
//#endregion
|
|
416
417
|
//#region src/utils/fs.ts
|
|
418
|
+
/**
|
|
419
|
+
* Compare file content, using parsed comparison for JSON files to ignore
|
|
420
|
+
* formatting differences. Falls back to exact string comparison for JSONC
|
|
421
|
+
* files (with comments/trailing commas) that fail JSON.parse.
|
|
422
|
+
*/
|
|
423
|
+
function contentEqual(relativePath, a, b) {
|
|
424
|
+
if (relativePath.endsWith(".json")) try {
|
|
425
|
+
return JSON.stringify(JSON.parse(a)) === JSON.stringify(JSON.parse(b));
|
|
426
|
+
} catch {
|
|
427
|
+
return a === b;
|
|
428
|
+
}
|
|
429
|
+
return a === b;
|
|
430
|
+
}
|
|
417
431
|
/** Check whether a file exists at the given path. */
|
|
418
432
|
function fileExists(targetDir, relativePath) {
|
|
419
433
|
return existsSync(path.join(targetDir, relativePath));
|
|
@@ -446,7 +460,7 @@ function createContext(config, confirmOverwrite) {
|
|
|
446
460
|
write: (rel, content) => {
|
|
447
461
|
if (!rel.startsWith(".tooling-archived/")) {
|
|
448
462
|
const existing = readFile(config.targetDir, rel);
|
|
449
|
-
if (existing !== void 0 && existing
|
|
463
|
+
if (existing !== void 0 && !contentEqual(rel, existing, content)) {
|
|
450
464
|
writeFile(config.targetDir, `.tooling-archived/${rel}`, existing);
|
|
451
465
|
archivedFiles.push(rel);
|
|
452
466
|
}
|
|
@@ -567,7 +581,7 @@ function getAddedDevDepNames(config) {
|
|
|
567
581
|
const deps = { ...ROOT_DEV_DEPS };
|
|
568
582
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
569
583
|
deps["@bensandee/config"] = "0.7.1";
|
|
570
|
-
deps["@bensandee/tooling"] = "0.
|
|
584
|
+
deps["@bensandee/tooling"] = "0.10.1";
|
|
571
585
|
if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
|
|
572
586
|
if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
|
|
573
587
|
addReleaseDeps(deps, config);
|
|
@@ -588,7 +602,7 @@ async function generatePackageJson(ctx) {
|
|
|
588
602
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
589
603
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
590
604
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.7.1";
|
|
591
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
605
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.10.1";
|
|
592
606
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.0";
|
|
593
607
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
|
|
594
608
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
|
|
@@ -1228,6 +1242,106 @@ async function generateGitignore(ctx) {
|
|
|
1228
1242
|
};
|
|
1229
1243
|
}
|
|
1230
1244
|
//#endregion
|
|
1245
|
+
//#region src/utils/yaml-merge.ts
|
|
1246
|
+
const IGNORE_PATTERN = "@bensandee/tooling:ignore";
|
|
1247
|
+
const FORGEJO_SCHEMA_COMMENT = "# yaml-language-server: $schema=../../.vscode/forgejo-workflow.schema.json\n";
|
|
1248
|
+
/** Returns a yaml-language-server schema comment for Forgejo workflows, empty string otherwise. */
|
|
1249
|
+
function workflowSchemaComment(ci) {
|
|
1250
|
+
return ci === "forgejo" ? FORGEJO_SCHEMA_COMMENT : "";
|
|
1251
|
+
}
|
|
1252
|
+
/** Check if a YAML file has an opt-out comment in the first 10 lines. */
|
|
1253
|
+
function isToolingIgnored(content) {
|
|
1254
|
+
return content.split("\n", 10).some((line) => line.includes(IGNORE_PATTERN));
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Ensure required commands exist under `pre-commit.commands` in a lefthook config.
|
|
1258
|
+
* Only adds missing commands — never modifies existing ones.
|
|
1259
|
+
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1260
|
+
*/
|
|
1261
|
+
function mergeLefthookCommands(existing, requiredCommands) {
|
|
1262
|
+
if (isToolingIgnored(existing)) return {
|
|
1263
|
+
content: existing,
|
|
1264
|
+
changed: false
|
|
1265
|
+
};
|
|
1266
|
+
try {
|
|
1267
|
+
const doc = parseDocument(existing);
|
|
1268
|
+
let changed = false;
|
|
1269
|
+
if (!doc.hasIn(["pre-commit", "commands"])) {
|
|
1270
|
+
doc.setIn(["pre-commit", "commands"], requiredCommands);
|
|
1271
|
+
return {
|
|
1272
|
+
content: doc.toString(),
|
|
1273
|
+
changed: true
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
const commands = doc.getIn(["pre-commit", "commands"]);
|
|
1277
|
+
if (!isMap(commands)) return {
|
|
1278
|
+
content: existing,
|
|
1279
|
+
changed: false
|
|
1280
|
+
};
|
|
1281
|
+
for (const [name, config] of Object.entries(requiredCommands)) if (!commands.has(name)) {
|
|
1282
|
+
commands.set(name, config);
|
|
1283
|
+
changed = true;
|
|
1284
|
+
}
|
|
1285
|
+
return {
|
|
1286
|
+
content: changed ? doc.toString() : existing,
|
|
1287
|
+
changed
|
|
1288
|
+
};
|
|
1289
|
+
} catch {
|
|
1290
|
+
return {
|
|
1291
|
+
content: existing,
|
|
1292
|
+
changed: false
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Ensure required steps exist in a workflow job's steps array.
|
|
1298
|
+
* Only adds missing steps at the end — never modifies existing ones.
|
|
1299
|
+
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1300
|
+
*/
|
|
1301
|
+
function mergeWorkflowSteps(existing, jobName, requiredSteps) {
|
|
1302
|
+
if (isToolingIgnored(existing)) return {
|
|
1303
|
+
content: existing,
|
|
1304
|
+
changed: false
|
|
1305
|
+
};
|
|
1306
|
+
try {
|
|
1307
|
+
const doc = parseDocument(existing);
|
|
1308
|
+
const steps = doc.getIn([
|
|
1309
|
+
"jobs",
|
|
1310
|
+
jobName,
|
|
1311
|
+
"steps"
|
|
1312
|
+
]);
|
|
1313
|
+
if (!isSeq(steps)) return {
|
|
1314
|
+
content: existing,
|
|
1315
|
+
changed: false
|
|
1316
|
+
};
|
|
1317
|
+
let changed = false;
|
|
1318
|
+
for (const { match, step } of requiredSteps) if (!steps.items.some((item) => {
|
|
1319
|
+
if (!isMap(item)) return false;
|
|
1320
|
+
if (match.run) {
|
|
1321
|
+
const run = item.get("run");
|
|
1322
|
+
return typeof run === "string" && run.includes(match.run);
|
|
1323
|
+
}
|
|
1324
|
+
if (match.uses) {
|
|
1325
|
+
const uses = item.get("uses");
|
|
1326
|
+
return typeof uses === "string" && uses.startsWith(match.uses);
|
|
1327
|
+
}
|
|
1328
|
+
return false;
|
|
1329
|
+
})) {
|
|
1330
|
+
steps.add(doc.createNode(step));
|
|
1331
|
+
changed = true;
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
content: changed ? doc.toString() : existing,
|
|
1335
|
+
changed
|
|
1336
|
+
};
|
|
1337
|
+
} catch {
|
|
1338
|
+
return {
|
|
1339
|
+
content: existing,
|
|
1340
|
+
changed: false
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
//#endregion
|
|
1231
1345
|
//#region src/generators/ci.ts
|
|
1232
1346
|
function hasEnginesNode$1(ctx) {
|
|
1233
1347
|
const raw = ctx.read("package.json");
|
|
@@ -1235,8 +1349,12 @@ function hasEnginesNode$1(ctx) {
|
|
|
1235
1349
|
return typeof parsePackageJson(raw)?.engines?.["node"] === "string";
|
|
1236
1350
|
}
|
|
1237
1351
|
function ciWorkflow(isMonorepo, nodeVersionYaml, isForgejo) {
|
|
1238
|
-
|
|
1239
|
-
|
|
1352
|
+
const buildCmd = isMonorepo ? "pnpm -r build" : "pnpm build";
|
|
1353
|
+
const testCmd = isMonorepo ? "pnpm -r test" : "pnpm test";
|
|
1354
|
+
const typecheckCmd = isMonorepo ? "pnpm -r --parallel run typecheck" : "pnpm typecheck";
|
|
1355
|
+
const emailNotifications = isForgejo ? "\nenable-email-notifications: true\n" : "";
|
|
1356
|
+
return `${workflowSchemaComment(isForgejo ? "forgejo" : "github")}name: CI
|
|
1357
|
+
${emailNotifications}on:
|
|
1240
1358
|
push:
|
|
1241
1359
|
branches: [main]
|
|
1242
1360
|
pull_request:
|
|
@@ -1253,32 +1371,71 @@ jobs:
|
|
|
1253
1371
|
${nodeVersionYaml}
|
|
1254
1372
|
cache: pnpm
|
|
1255
1373
|
- run: pnpm install --frozen-lockfile
|
|
1256
|
-
- run: ${
|
|
1374
|
+
- run: ${typecheckCmd}
|
|
1257
1375
|
- run: pnpm lint
|
|
1258
|
-
- run: ${
|
|
1259
|
-
- run: ${
|
|
1376
|
+
- run: ${buildCmd}
|
|
1377
|
+
- run: ${testCmd}
|
|
1260
1378
|
- run: pnpm format --check
|
|
1261
1379
|
- run: pnpm knip
|
|
1262
1380
|
- run: pnpm exec tooling repo:check
|
|
1263
1381
|
`;
|
|
1264
1382
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1383
|
+
function requiredCheckSteps(isMonorepo, nodeVersionYaml) {
|
|
1384
|
+
const buildCmd = isMonorepo ? "pnpm -r build" : "pnpm build";
|
|
1385
|
+
const testCmd = isMonorepo ? "pnpm -r test" : "pnpm test";
|
|
1386
|
+
const typecheckCmd = isMonorepo ? "pnpm -r --parallel run typecheck" : "pnpm typecheck";
|
|
1387
|
+
return [
|
|
1388
|
+
{
|
|
1389
|
+
match: { uses: "actions/checkout" },
|
|
1390
|
+
step: { uses: "actions/checkout@v4" }
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
match: { uses: "pnpm/action-setup" },
|
|
1394
|
+
step: { uses: "pnpm/action-setup@v4" }
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
match: { uses: "actions/setup-node" },
|
|
1398
|
+
step: {
|
|
1399
|
+
uses: "actions/setup-node@v4",
|
|
1400
|
+
with: {
|
|
1401
|
+
...nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
1402
|
+
cache: "pnpm"
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
match: { run: "pnpm install" },
|
|
1408
|
+
step: { run: "pnpm install --frozen-lockfile" }
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
match: { run: "typecheck" },
|
|
1412
|
+
step: { run: typecheckCmd }
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
match: { run: "lint" },
|
|
1416
|
+
step: { run: "pnpm lint" }
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1419
|
+
match: { run: "build" },
|
|
1420
|
+
step: { run: buildCmd }
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
match: { run: "test" },
|
|
1424
|
+
step: { run: testCmd }
|
|
1425
|
+
},
|
|
1426
|
+
{
|
|
1427
|
+
match: { run: "format" },
|
|
1428
|
+
step: { run: "pnpm format --check" }
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
match: { run: "knip" },
|
|
1432
|
+
step: { run: "pnpm knip" }
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
match: { run: "repo:check" },
|
|
1436
|
+
step: { run: "pnpm exec tooling repo:check" }
|
|
1437
|
+
}
|
|
1438
|
+
];
|
|
1282
1439
|
}
|
|
1283
1440
|
async function generateCi(ctx) {
|
|
1284
1441
|
if (ctx.config.ci === "none") return {
|
|
@@ -1293,14 +1450,14 @@ async function generateCi(ctx) {
|
|
|
1293
1450
|
const content = ciWorkflow(isMonorepo, nodeVersionYaml, !isGitHub);
|
|
1294
1451
|
if (ctx.exists(filePath)) {
|
|
1295
1452
|
const existing = ctx.read(filePath);
|
|
1296
|
-
if (existing
|
|
1297
|
-
const
|
|
1298
|
-
if (
|
|
1299
|
-
ctx.write(filePath,
|
|
1453
|
+
if (existing) {
|
|
1454
|
+
const merged = mergeWorkflowSteps(existing, "check", requiredCheckSteps(isMonorepo, nodeVersionYaml));
|
|
1455
|
+
if (merged.changed) {
|
|
1456
|
+
ctx.write(filePath, merged.content);
|
|
1300
1457
|
return {
|
|
1301
1458
|
filePath,
|
|
1302
1459
|
action: "updated",
|
|
1303
|
-
description: "Added
|
|
1460
|
+
description: "Added missing steps to CI workflow"
|
|
1304
1461
|
};
|
|
1305
1462
|
}
|
|
1306
1463
|
}
|
|
@@ -1489,6 +1646,7 @@ function buildSettings(ctx) {
|
|
|
1489
1646
|
`Bash(${pm} add *)`,
|
|
1490
1647
|
`Bash(${pm} update *)`,
|
|
1491
1648
|
`Bash(${pm} view *)`,
|
|
1649
|
+
`Bash(${pm} why *)`,
|
|
1492
1650
|
`Bash(${pm} list)`,
|
|
1493
1651
|
`Bash(${pm} list *)`,
|
|
1494
1652
|
`Bash(${pm} ls)`,
|
|
@@ -1706,7 +1864,7 @@ async function generateReleaseIt(ctx) {
|
|
|
1706
1864
|
const content = JSON.stringify(buildConfig$2(ctx.config.ci, ctx.config.structure === "monorepo"), null, 2) + "\n";
|
|
1707
1865
|
const existing = ctx.read(filePath);
|
|
1708
1866
|
if (existing) {
|
|
1709
|
-
if (existing
|
|
1867
|
+
if (contentEqual(filePath, existing, content)) return {
|
|
1710
1868
|
filePath,
|
|
1711
1869
|
action: "skipped",
|
|
1712
1870
|
description: "Already configured"
|
|
@@ -1785,7 +1943,7 @@ permissions:
|
|
|
1785
1943
|
contents: write
|
|
1786
1944
|
` : "";
|
|
1787
1945
|
const tokenEnv = isGitHub ? `GITHUB_TOKEN: \${{ github.token }}` : `FORGEJO_TOKEN: \${{ secrets.FORGEJO_TOKEN }}`;
|
|
1788
|
-
return
|
|
1946
|
+
return `${workflowSchemaComment(ci)}name: Release
|
|
1789
1947
|
on:
|
|
1790
1948
|
workflow_dispatch:
|
|
1791
1949
|
${permissions}
|
|
@@ -1834,7 +1992,7 @@ permissions:
|
|
|
1834
1992
|
TAG=$(git describe --tags --abbrev=0)
|
|
1835
1993
|
pnpm publish --no-git-checks
|
|
1836
1994
|
pnpm exec tooling release:create-forgejo-release --tag "$TAG"`;
|
|
1837
|
-
return
|
|
1995
|
+
return `${workflowSchemaComment(ci)}name: Release
|
|
1838
1996
|
on:
|
|
1839
1997
|
workflow_dispatch:
|
|
1840
1998
|
${permissions}
|
|
@@ -1846,7 +2004,7 @@ ${commonSteps(nodeVersionYaml)}${gitConfigStep}${releaseStep}
|
|
|
1846
2004
|
`;
|
|
1847
2005
|
}
|
|
1848
2006
|
function changesetsWorkflow(ci, nodeVersionYaml) {
|
|
1849
|
-
if (ci === "github") return
|
|
2007
|
+
if (ci === "github") return `${workflowSchemaComment(ci)}name: Release
|
|
1850
2008
|
on:
|
|
1851
2009
|
push:
|
|
1852
2010
|
branches:
|
|
@@ -1869,7 +2027,7 @@ ${commonSteps(nodeVersionYaml)}
|
|
|
1869
2027
|
GITHUB_TOKEN: \${{ github.token }}
|
|
1870
2028
|
NPM_TOKEN: \${{ secrets.NPM_TOKEN }}
|
|
1871
2029
|
`;
|
|
1872
|
-
return
|
|
2030
|
+
return `${workflowSchemaComment(ci)}name: Release
|
|
1873
2031
|
on:
|
|
1874
2032
|
push:
|
|
1875
2033
|
branches:
|
|
@@ -1893,6 +2051,62 @@ ${commonSteps(nodeVersionYaml)}
|
|
|
1893
2051
|
run: pnpm exec tooling release:changesets
|
|
1894
2052
|
`;
|
|
1895
2053
|
}
|
|
2054
|
+
function requiredReleaseSteps(strategy, nodeVersionYaml) {
|
|
2055
|
+
const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
|
|
2056
|
+
const steps = [
|
|
2057
|
+
{
|
|
2058
|
+
match: { uses: "actions/checkout" },
|
|
2059
|
+
step: {
|
|
2060
|
+
uses: "actions/checkout@v4",
|
|
2061
|
+
with: { "fetch-depth": 0 }
|
|
2062
|
+
}
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
match: { uses: "pnpm/action-setup" },
|
|
2066
|
+
step: { uses: "pnpm/action-setup@v4" }
|
|
2067
|
+
},
|
|
2068
|
+
{
|
|
2069
|
+
match: { uses: "actions/setup-node" },
|
|
2070
|
+
step: {
|
|
2071
|
+
uses: "actions/setup-node@v4",
|
|
2072
|
+
with: {
|
|
2073
|
+
...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
2074
|
+
cache: "pnpm",
|
|
2075
|
+
"registry-url": "https://registry.npmjs.org"
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
},
|
|
2079
|
+
{
|
|
2080
|
+
match: { run: "pnpm install" },
|
|
2081
|
+
step: { run: "pnpm install --frozen-lockfile" }
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
match: { run: "build" },
|
|
2085
|
+
step: { run: "pnpm build" }
|
|
2086
|
+
}
|
|
2087
|
+
];
|
|
2088
|
+
switch (strategy) {
|
|
2089
|
+
case "release-it":
|
|
2090
|
+
steps.push({
|
|
2091
|
+
match: { run: "release-it" },
|
|
2092
|
+
step: { run: "pnpm release-it --ci" }
|
|
2093
|
+
});
|
|
2094
|
+
break;
|
|
2095
|
+
case "commit-and-tag-version":
|
|
2096
|
+
steps.push({
|
|
2097
|
+
match: { run: "commit-and-tag-version" },
|
|
2098
|
+
step: { run: "pnpm exec commit-and-tag-version" }
|
|
2099
|
+
});
|
|
2100
|
+
break;
|
|
2101
|
+
case "changesets":
|
|
2102
|
+
steps.push({
|
|
2103
|
+
match: { run: "changeset" },
|
|
2104
|
+
step: { run: "pnpm exec tooling release:changesets" }
|
|
2105
|
+
});
|
|
2106
|
+
break;
|
|
2107
|
+
}
|
|
2108
|
+
return steps;
|
|
2109
|
+
}
|
|
1896
2110
|
function buildWorkflow(strategy, ci, nodeVersionYaml) {
|
|
1897
2111
|
switch (strategy) {
|
|
1898
2112
|
case "release-it": return releaseItWorkflow(ci, nodeVersionYaml);
|
|
@@ -1917,11 +2131,25 @@ async function generateReleaseCi(ctx) {
|
|
|
1917
2131
|
action: "skipped",
|
|
1918
2132
|
description: "Release CI workflow not applicable"
|
|
1919
2133
|
};
|
|
1920
|
-
if (ctx.exists(workflowPath))
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2134
|
+
if (ctx.exists(workflowPath)) {
|
|
2135
|
+
const existing = ctx.read(workflowPath);
|
|
2136
|
+
if (existing) {
|
|
2137
|
+
const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml));
|
|
2138
|
+
if (merged.changed) {
|
|
2139
|
+
ctx.write(workflowPath, merged.content);
|
|
2140
|
+
return {
|
|
2141
|
+
filePath: workflowPath,
|
|
2142
|
+
action: "updated",
|
|
2143
|
+
description: "Added missing steps to release workflow"
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return {
|
|
2148
|
+
filePath: workflowPath,
|
|
2149
|
+
action: "skipped",
|
|
2150
|
+
description: "Release workflow already up to date"
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
1925
2153
|
ctx.write(workflowPath, content);
|
|
1926
2154
|
return {
|
|
1927
2155
|
filePath: workflowPath,
|
|
@@ -1931,6 +2159,15 @@ async function generateReleaseCi(ctx) {
|
|
|
1931
2159
|
}
|
|
1932
2160
|
//#endregion
|
|
1933
2161
|
//#region src/generators/lefthook.ts
|
|
2162
|
+
function requiredCommands(formatter) {
|
|
2163
|
+
return {
|
|
2164
|
+
lint: { run: "pnpm exec oxlint {staged_files}" },
|
|
2165
|
+
format: {
|
|
2166
|
+
run: formatter === "prettier" ? "pnpm exec prettier --write {staged_files}" : "pnpm exec oxfmt --no-error-on-unmatched-pattern {staged_files}",
|
|
2167
|
+
stage_fixed: true
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
1934
2171
|
function buildConfig(formatter) {
|
|
1935
2172
|
return [
|
|
1936
2173
|
"pre-commit:",
|
|
@@ -2042,10 +2279,25 @@ async function generateLefthook(ctx) {
|
|
|
2042
2279
|
cleanPackageJson(ctx, results);
|
|
2043
2280
|
const existingPath = LEFTHOOK_CONFIG_PATHS.find((p) => ctx.exists(p));
|
|
2044
2281
|
if (existingPath) {
|
|
2045
|
-
|
|
2282
|
+
const existing = ctx.read(existingPath);
|
|
2283
|
+
if (existing) {
|
|
2284
|
+
const merged = mergeLefthookCommands(existing, requiredCommands(ctx.config.formatter));
|
|
2285
|
+
if (merged.changed) {
|
|
2286
|
+
ctx.write(existingPath, merged.content);
|
|
2287
|
+
results.push({
|
|
2288
|
+
filePath: existingPath,
|
|
2289
|
+
action: "updated",
|
|
2290
|
+
description: "Added missing pre-commit commands"
|
|
2291
|
+
});
|
|
2292
|
+
} else results.push({
|
|
2293
|
+
filePath: existingPath,
|
|
2294
|
+
action: "skipped",
|
|
2295
|
+
description: "Lefthook config already up to date"
|
|
2296
|
+
});
|
|
2297
|
+
} else results.push({
|
|
2046
2298
|
filePath: existingPath,
|
|
2047
2299
|
action: "skipped",
|
|
2048
|
-
description: "
|
|
2300
|
+
description: "Could not read existing lefthook config"
|
|
2049
2301
|
});
|
|
2050
2302
|
return results;
|
|
2051
2303
|
}
|
|
@@ -2062,22 +2314,13 @@ async function generateLefthook(ctx) {
|
|
|
2062
2314
|
const SCHEMA_NPM_PATH = "@bensandee/config/schemas/forgejo-workflow.schema.json";
|
|
2063
2315
|
const SCHEMA_LOCAL_PATH = ".vscode/forgejo-workflow.schema.json";
|
|
2064
2316
|
const SETTINGS_PATH = ".vscode/settings.json";
|
|
2065
|
-
const SCHEMA_GLOB = ".forgejo/workflows/*.yml";
|
|
2317
|
+
const SCHEMA_GLOB = ".forgejo/workflows/*.{yml,yaml}";
|
|
2066
2318
|
const VscodeSettingsSchema = z.object({ "yaml.schemas": z.record(z.string(), z.unknown()).default({}) }).passthrough();
|
|
2067
|
-
const FullWorkspaceFileSchema = z.object({ settings: z.record(z.string(), z.unknown()).default({}) }).passthrough();
|
|
2068
2319
|
function readSchemaFromNodeModules(targetDir) {
|
|
2069
2320
|
const candidate = path.join(targetDir, "node_modules", SCHEMA_NPM_PATH);
|
|
2070
2321
|
if (!existsSync(candidate)) return void 0;
|
|
2071
2322
|
return readFileSync(candidate, "utf-8");
|
|
2072
2323
|
}
|
|
2073
|
-
/** Find a *.code-workspace file in the target directory. */
|
|
2074
|
-
function findWorkspaceFile(targetDir) {
|
|
2075
|
-
try {
|
|
2076
|
-
return readdirSync(targetDir).find((e) => e.endsWith(".code-workspace"));
|
|
2077
|
-
} catch {
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
2324
|
function serializeSettings(settings) {
|
|
2082
2325
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
2083
2326
|
}
|
|
@@ -2099,36 +2342,6 @@ function mergeYamlSchemas(settings) {
|
|
|
2099
2342
|
changed: true
|
|
2100
2343
|
};
|
|
2101
2344
|
}
|
|
2102
|
-
function writeSchemaToWorkspaceFile(ctx, wsFileName) {
|
|
2103
|
-
const raw = ctx.read(wsFileName);
|
|
2104
|
-
if (!raw) return {
|
|
2105
|
-
filePath: wsFileName,
|
|
2106
|
-
action: "skipped",
|
|
2107
|
-
description: "Could not read workspace file"
|
|
2108
|
-
};
|
|
2109
|
-
const fullParsed = FullWorkspaceFileSchema.safeParse(parse(raw));
|
|
2110
|
-
if (!fullParsed.success) return {
|
|
2111
|
-
filePath: wsFileName,
|
|
2112
|
-
action: "skipped",
|
|
2113
|
-
description: "Could not parse workspace file"
|
|
2114
|
-
};
|
|
2115
|
-
const { merged, changed } = mergeYamlSchemas(fullParsed.data.settings);
|
|
2116
|
-
if (!changed) return {
|
|
2117
|
-
filePath: wsFileName,
|
|
2118
|
-
action: "skipped",
|
|
2119
|
-
description: "Already has Forgejo schema mapping"
|
|
2120
|
-
};
|
|
2121
|
-
const updated = {
|
|
2122
|
-
...fullParsed.data,
|
|
2123
|
-
settings: merged
|
|
2124
|
-
};
|
|
2125
|
-
ctx.write(wsFileName, JSON.stringify(updated, null, 2) + "\n");
|
|
2126
|
-
return {
|
|
2127
|
-
filePath: wsFileName,
|
|
2128
|
-
action: "updated",
|
|
2129
|
-
description: "Added Forgejo workflow schema mapping to workspace settings"
|
|
2130
|
-
};
|
|
2131
|
-
}
|
|
2132
2345
|
function writeSchemaToSettings(ctx) {
|
|
2133
2346
|
if (ctx.exists(SETTINGS_PATH)) {
|
|
2134
2347
|
const raw = ctx.read(SETTINGS_PATH);
|
|
@@ -2183,7 +2396,7 @@ async function generateVscodeSettings(ctx) {
|
|
|
2183
2396
|
return results;
|
|
2184
2397
|
}
|
|
2185
2398
|
const existingSchema = ctx.read(SCHEMA_LOCAL_PATH);
|
|
2186
|
-
if (existingSchema
|
|
2399
|
+
if (existingSchema !== void 0 && contentEqual(SCHEMA_LOCAL_PATH, existingSchema, schemaContent)) results.push({
|
|
2187
2400
|
filePath: SCHEMA_LOCAL_PATH,
|
|
2188
2401
|
action: "skipped",
|
|
2189
2402
|
description: "Schema already up to date"
|
|
@@ -2196,8 +2409,7 @@ async function generateVscodeSettings(ctx) {
|
|
|
2196
2409
|
description: "Copied Forgejo workflow schema from @bensandee/config"
|
|
2197
2410
|
});
|
|
2198
2411
|
}
|
|
2199
|
-
|
|
2200
|
-
results.push(wsFile ? writeSchemaToWorkspaceFile(ctx, wsFile) : writeSchemaToSettings(ctx));
|
|
2412
|
+
results.push(writeSchemaToSettings(ctx));
|
|
2201
2413
|
return results;
|
|
2202
2414
|
}
|
|
2203
2415
|
//#endregion
|
|
@@ -2279,7 +2491,7 @@ function saveToolingConfig(ctx, config) {
|
|
|
2279
2491
|
};
|
|
2280
2492
|
const content = JSON.stringify(saved, null, 2) + "\n";
|
|
2281
2493
|
const existing = ctx.exists(CONFIG_FILE) ? ctx.read(CONFIG_FILE) : void 0;
|
|
2282
|
-
if (existing
|
|
2494
|
+
if (existing !== void 0 && contentEqual(CONFIG_FILE, existing, content)) return {
|
|
2283
2495
|
filePath: CONFIG_FILE,
|
|
2284
2496
|
action: "skipped",
|
|
2285
2497
|
description: "Already up to date"
|
|
@@ -2482,7 +2694,16 @@ async function runCheck(targetDir) {
|
|
|
2482
2694
|
return 1;
|
|
2483
2695
|
}
|
|
2484
2696
|
const { ctx, pendingWrites } = createDryRunContext(mergeWithSavedConfig(buildDefaultConfig(targetDir, {}), saved));
|
|
2485
|
-
const actionable = (await runGenerators(ctx)).filter((r) =>
|
|
2697
|
+
const actionable = (await runGenerators(ctx)).filter((r) => {
|
|
2698
|
+
if (r.action !== "created" && r.action !== "updated") return false;
|
|
2699
|
+
const newContent = pendingWrites.get(r.filePath);
|
|
2700
|
+
if (newContent && r.action === "updated") {
|
|
2701
|
+
const existingPath = path.join(targetDir, r.filePath);
|
|
2702
|
+
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
2703
|
+
if (existing && contentEqual(r.filePath, existing, newContent)) return false;
|
|
2704
|
+
}
|
|
2705
|
+
return true;
|
|
2706
|
+
});
|
|
2486
2707
|
if (actionable.length === 0) {
|
|
2487
2708
|
p.log.success("Repository is up to date.");
|
|
2488
2709
|
return 0;
|
|
@@ -3207,7 +3428,7 @@ function mergeGitHub(dryRun) {
|
|
|
3207
3428
|
const main = defineCommand({
|
|
3208
3429
|
meta: {
|
|
3209
3430
|
name: "tooling",
|
|
3210
|
-
version: "0.
|
|
3431
|
+
version: "0.10.1",
|
|
3211
3432
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
3212
3433
|
},
|
|
3213
3434
|
subCommands: {
|
|
@@ -3220,7 +3441,7 @@ const main = defineCommand({
|
|
|
3220
3441
|
"release:merge": releaseMergeCommand
|
|
3221
3442
|
}
|
|
3222
3443
|
});
|
|
3223
|
-
console.log(`@bensandee/tooling v0.
|
|
3444
|
+
console.log(`@bensandee/tooling v0.10.1`);
|
|
3224
3445
|
runMain(main);
|
|
3225
3446
|
//#endregion
|
|
3226
3447
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensandee/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tooling": "./dist/bin.mjs"
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"citty": "^0.2.1",
|
|
26
26
|
"json5": "^2.2.3",
|
|
27
27
|
"jsonc-parser": "^3.3.1",
|
|
28
|
+
"yaml": "^2.8.2",
|
|
28
29
|
"zod": "^4.3.6",
|
|
29
30
|
"@bensandee/common": "0.1.0"
|
|
30
31
|
},
|