@bensandee/tooling 0.36.0 → 0.38.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/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/bin.mjs +537 -350
- package/dist/{check-Ceom_OgJ.mjs → check-hutCTIXe.mjs} +45 -18
- package/dist/docker-check/index.d.mts +17 -12
- package/dist/docker-check/index.mjs +2 -2
- package/dist/index.d.mts +2 -0
- package/package.json +10 -4
- package/tooling.schema.json +13 -2
package/dist/bin.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { d as debug, f as debugExec,
|
|
2
|
+
import { d as debug, f as debugExec, g as note, h as log, l as createRealExecutor$1, m as redact, p as isEnvVerbose, t as runDockerCheck, u as isExecSyncError } from "./check-hutCTIXe.mjs";
|
|
3
3
|
import { defineCommand, runMain } from "citty";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import { isCancel, select } from "@clack/prompts";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync,
|
|
8
|
-
import { execSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
9
9
|
import JSON5 from "json5";
|
|
10
10
|
import { parse } from "jsonc-parser";
|
|
11
11
|
import { z } from "zod";
|
|
@@ -102,6 +102,7 @@ function detectProject(targetDir) {
|
|
|
102
102
|
hasPackageJson: exists("package.json"),
|
|
103
103
|
hasTsconfig: exists("tsconfig.json"),
|
|
104
104
|
hasOxlintConfig: exists("oxlint.config.ts"),
|
|
105
|
+
oxlintExtendsRecommended: fileContains(path.join(targetDir, "oxlint.config.ts"), "@bensandee/config/oxlint/recommended"),
|
|
105
106
|
hasLegacyOxlintJson: exists(".oxlintrc.json"),
|
|
106
107
|
hasGitignore: exists(".gitignore"),
|
|
107
108
|
hasVitestConfig: exists("vitest.config.ts") || exists("vitest.config.mts"),
|
|
@@ -170,6 +171,15 @@ function detectLegacyConfigs(targetDir) {
|
|
|
170
171
|
}
|
|
171
172
|
return results;
|
|
172
173
|
}
|
|
174
|
+
/** Check whether a file exists and contains a given substring. */
|
|
175
|
+
function fileContains(filePath, substring) {
|
|
176
|
+
if (!existsSync(filePath)) return false;
|
|
177
|
+
try {
|
|
178
|
+
return readFileSync(filePath, "utf-8").includes(substring);
|
|
179
|
+
} catch (_error) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
173
183
|
/** Read and parse existing package.json, or return undefined. */
|
|
174
184
|
function readPackageJson(targetDir) {
|
|
175
185
|
const pkgPath = path.join(targetDir, "package.json");
|
|
@@ -455,6 +465,11 @@ function buildDefaultConfig(targetDir, flags) {
|
|
|
455
465
|
}
|
|
456
466
|
//#endregion
|
|
457
467
|
//#region src/utils/fs.ts
|
|
468
|
+
/** Verify that a relative path resolves within the target directory. */
|
|
469
|
+
function assertContained(targetDir, rel) {
|
|
470
|
+
const resolved = path.resolve(targetDir, rel);
|
|
471
|
+
if (!resolved.startsWith(targetDir + path.sep) && resolved !== targetDir) throw new FatalError(`Path traversal detected: ${rel} resolves outside target directory`);
|
|
472
|
+
}
|
|
458
473
|
/**
|
|
459
474
|
* Compare file content, using parsed comparison for JSON files to ignore
|
|
460
475
|
* formatting differences. Falls back to exact string comparison for JSONC
|
|
@@ -495,9 +510,18 @@ function createContext(config, confirmOverwrite) {
|
|
|
495
510
|
config,
|
|
496
511
|
targetDir: config.targetDir,
|
|
497
512
|
packageJson: pkgRaw ? parsePackageJson(pkgRaw) : void 0,
|
|
498
|
-
exists: (rel) =>
|
|
499
|
-
|
|
513
|
+
exists: (rel) => {
|
|
514
|
+
const resolved = path.resolve(config.targetDir, rel);
|
|
515
|
+
if (!resolved.startsWith(config.targetDir + path.sep) && resolved !== config.targetDir) return false;
|
|
516
|
+
return fileExists(config.targetDir, rel);
|
|
517
|
+
},
|
|
518
|
+
read: (rel) => {
|
|
519
|
+
const resolved = path.resolve(config.targetDir, rel);
|
|
520
|
+
if (!resolved.startsWith(config.targetDir + path.sep) && resolved !== config.targetDir) return;
|
|
521
|
+
return readFile(config.targetDir, rel);
|
|
522
|
+
},
|
|
500
523
|
write: (rel, content) => {
|
|
524
|
+
assertContained(config.targetDir, rel);
|
|
501
525
|
if (!rel.startsWith(".tooling-archived/")) {
|
|
502
526
|
const existing = readFile(config.targetDir, rel);
|
|
503
527
|
if (existing !== void 0 && !contentEqual(rel, existing, content)) {
|
|
@@ -509,6 +533,7 @@ function createContext(config, confirmOverwrite) {
|
|
|
509
533
|
if (rel === "package.json") ctx.packageJson = parsePackageJson(content);
|
|
510
534
|
},
|
|
511
535
|
remove: (rel) => {
|
|
536
|
+
assertContained(config.targetDir, rel);
|
|
512
537
|
const fullPath = path.join(config.targetDir, rel);
|
|
513
538
|
if (existsSync(fullPath)) rmSync(fullPath);
|
|
514
539
|
},
|
|
@@ -560,7 +585,7 @@ const DockerCheckConfigSchema = z.object({
|
|
|
560
585
|
envFile: z.string().optional().meta({ description: "Environment file for compose" }),
|
|
561
586
|
services: z.array(z.string()).optional().meta({ description: "Services to check (default: all)" }),
|
|
562
587
|
healthChecks: z.array(DeclarativeHealthCheckSchema).optional().meta({ description: "Health check definitions" }),
|
|
563
|
-
buildCommand: z.string().optional().meta({ description: "Command to build images before checking" }),
|
|
588
|
+
buildCommand: z.array(z.string()).optional().meta({ description: "Command to build images before checking (e.g. [\"pnpm\", \"image:build\"])" }),
|
|
564
589
|
buildCwd: z.string().optional().meta({ description: "Working directory for build command" }),
|
|
565
590
|
timeoutMs: z.number().int().positive().optional().meta({ description: "Overall timeout in milliseconds" }),
|
|
566
591
|
pollIntervalMs: z.number().int().positive().optional().meta({ description: "Poll interval in milliseconds" })
|
|
@@ -596,7 +621,9 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
596
621
|
dockerfile: z.string().meta({ description: "Path to Dockerfile relative to package" }),
|
|
597
622
|
context: z.string().default(".").meta({ description: "Docker build context relative to package" })
|
|
598
623
|
})).optional().meta({ description: "Docker package overrides (package name → config)" }),
|
|
599
|
-
dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" })
|
|
624
|
+
dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" }),
|
|
625
|
+
registryHost: z.string().optional().meta({ description: "Docker registry hostname (e.g. ghcr.io, code.example.com)" }),
|
|
626
|
+
registryNamespace: z.string().optional().meta({ description: "Docker registry namespace (e.g. ghcr.io/myorg, code.example.com/myorg)" })
|
|
600
627
|
});
|
|
601
628
|
/** Load saved tooling config from the target directory. Returns undefined if missing, throws on invalid. */
|
|
602
629
|
function loadToolingConfig(targetDir) {
|
|
@@ -855,14 +882,15 @@ function readRepoName(executor, cwd) {
|
|
|
855
882
|
function buildImage(executor, pkg, cwd, extraArgs) {
|
|
856
883
|
const dockerfilePath = path.resolve(cwd, pkg.docker.dockerfile);
|
|
857
884
|
const contextPath = path.resolve(cwd, pkg.docker.context);
|
|
858
|
-
|
|
859
|
-
"
|
|
860
|
-
|
|
861
|
-
|
|
885
|
+
executor.execInherit("docker", [
|
|
886
|
+
"build",
|
|
887
|
+
"-f",
|
|
888
|
+
dockerfilePath,
|
|
889
|
+
"-t",
|
|
890
|
+
`${pkg.imageName}:latest`,
|
|
862
891
|
...extraArgs,
|
|
863
892
|
contextPath
|
|
864
|
-
]
|
|
865
|
-
executor.execInherit(command);
|
|
893
|
+
]);
|
|
866
894
|
}
|
|
867
895
|
/**
|
|
868
896
|
* Detect packages with docker config in .tooling.json and build each one.
|
|
@@ -917,7 +945,13 @@ function runDockerPublish(executor, config) {
|
|
|
917
945
|
for (const pkg of packages) if (!pkg.version) throw new FatalError(`Package ${pkg.dir} has docker config but no version in package.json`);
|
|
918
946
|
if (!config.dryRun) {
|
|
919
947
|
log.info(`Logging in to ${config.registryHost}...`);
|
|
920
|
-
const loginResult = executor.exec(
|
|
948
|
+
const loginResult = executor.exec("docker", [
|
|
949
|
+
"login",
|
|
950
|
+
config.registryHost,
|
|
951
|
+
"-u",
|
|
952
|
+
config.username,
|
|
953
|
+
"--password-stdin"
|
|
954
|
+
], { input: config.password });
|
|
921
955
|
debugExec(config, "docker login", loginResult);
|
|
922
956
|
if (loginResult.exitCode !== 0) throw new FatalError(`Docker login failed: ${loginResult.stderr}`);
|
|
923
957
|
} else log.info("[dry-run] Skipping docker login");
|
|
@@ -930,12 +964,16 @@ function runDockerPublish(executor, config) {
|
|
|
930
964
|
const ref = imageRef(config.registryNamespace, pkg.imageName, tag);
|
|
931
965
|
allTags.push(ref);
|
|
932
966
|
log.info(`Tagging ${pkg.imageName} → ${ref}`);
|
|
933
|
-
const tagResult = executor.exec(
|
|
967
|
+
const tagResult = executor.exec("docker", [
|
|
968
|
+
"tag",
|
|
969
|
+
pkg.imageName,
|
|
970
|
+
ref
|
|
971
|
+
]);
|
|
934
972
|
debugExec(config, `docker tag ${pkg.imageName} ${ref}`, tagResult);
|
|
935
973
|
if (tagResult.exitCode !== 0) throw new FatalError(`docker tag failed: ${tagResult.stderr}`);
|
|
936
974
|
if (!config.dryRun) {
|
|
937
975
|
log.info(`Pushing ${ref}...`);
|
|
938
|
-
const pushResult = executor.exec(
|
|
976
|
+
const pushResult = executor.exec("docker", ["push", ref]);
|
|
939
977
|
debugExec(config, `docker push ${ref}`, pushResult);
|
|
940
978
|
if (pushResult.exitCode !== 0) throw new FatalError(`docker push failed: ${pushResult.stderr}`);
|
|
941
979
|
} else log.info(`[dry-run] Skipping push for ${ref}`);
|
|
@@ -944,7 +982,7 @@ function runDockerPublish(executor, config) {
|
|
|
944
982
|
} finally {
|
|
945
983
|
if (!config.dryRun) {
|
|
946
984
|
log.info(`Logging out from ${config.registryHost}...`);
|
|
947
|
-
executor.exec(
|
|
985
|
+
executor.exec("docker", ["logout", config.registryHost]);
|
|
948
986
|
}
|
|
949
987
|
}
|
|
950
988
|
log.info(`Published ${String(allTags.length)} image tag(s)`);
|
|
@@ -1143,7 +1181,29 @@ function computeNodeVersionYaml(ctx) {
|
|
|
1143
1181
|
}
|
|
1144
1182
|
//#endregion
|
|
1145
1183
|
//#region src/generators/publish-ci.ts
|
|
1146
|
-
|
|
1184
|
+
/**
|
|
1185
|
+
* Check whether the project has a repository URL in package.json.
|
|
1186
|
+
* When true, the CLI can derive DOCKER_REGISTRY_HOST and DOCKER_REGISTRY_NAMESPACE
|
|
1187
|
+
* at runtime, so workflows can omit those env vars.
|
|
1188
|
+
*/
|
|
1189
|
+
function hasRepositoryUrl(ctx) {
|
|
1190
|
+
const raw = ctx.read("package.json");
|
|
1191
|
+
if (!raw) return false;
|
|
1192
|
+
return parsePackageJson(raw)?.repository !== void 0;
|
|
1193
|
+
}
|
|
1194
|
+
/** Build the docker publish env block, omitting registry vars when derivable at runtime. */
|
|
1195
|
+
function dockerPublishEnv$1(registryDerivable) {
|
|
1196
|
+
return {
|
|
1197
|
+
...registryDerivable ? {} : {
|
|
1198
|
+
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
1199
|
+
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
1200
|
+
},
|
|
1201
|
+
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
1202
|
+
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
1203
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function publishSteps(nodeVersionYaml, registryDerivable) {
|
|
1147
1207
|
return [
|
|
1148
1208
|
{ step: { uses: "actions/checkout@v6" } },
|
|
1149
1209
|
{ step: { uses: "pnpm/action-setup@v5" } },
|
|
@@ -1154,13 +1214,7 @@ function publishSteps(nodeVersionYaml) {
|
|
|
1154
1214
|
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
1155
1215
|
{ step: {
|
|
1156
1216
|
name: "Publish Docker images",
|
|
1157
|
-
env:
|
|
1158
|
-
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
1159
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE"),
|
|
1160
|
-
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
1161
|
-
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
1162
|
-
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
1163
|
-
},
|
|
1217
|
+
env: dockerPublishEnv$1(registryDerivable),
|
|
1164
1218
|
run: "pnpm exec bst docker:publish"
|
|
1165
1219
|
} }
|
|
1166
1220
|
];
|
|
@@ -1203,7 +1257,7 @@ async function generateDeployCi(ctx) {
|
|
|
1203
1257
|
};
|
|
1204
1258
|
const isGitHub = ctx.config.ci === "github";
|
|
1205
1259
|
const workflowPath = isGitHub ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml";
|
|
1206
|
-
const steps = publishSteps(computeNodeVersionYaml(ctx));
|
|
1260
|
+
const steps = publishSteps(computeNodeVersionYaml(ctx), hasRepositoryUrl(ctx));
|
|
1207
1261
|
const content = buildWorkflowYaml({
|
|
1208
1262
|
ci: ctx.config.ci,
|
|
1209
1263
|
name: "Publish",
|
|
@@ -1284,8 +1338,8 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1284
1338
|
"@changesets/cli": "2.30.0",
|
|
1285
1339
|
"@release-it/bumper": "7.0.5",
|
|
1286
1340
|
"@types/node": "24.12.0",
|
|
1287
|
-
"commit-and-tag-version": "12.7.
|
|
1288
|
-
"knip": "
|
|
1341
|
+
"commit-and-tag-version": "12.7.1",
|
|
1342
|
+
"knip": "6.0.3",
|
|
1289
1343
|
"lefthook": "2.1.4",
|
|
1290
1344
|
"oxfmt": "0.41.0",
|
|
1291
1345
|
"oxlint": "1.56.0",
|
|
@@ -1300,8 +1354,8 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1300
1354
|
"@changesets/cli": "2.30.0",
|
|
1301
1355
|
"@release-it/bumper": "7.0.5",
|
|
1302
1356
|
"@types/node": "24.12.0",
|
|
1303
|
-
"commit-and-tag-version": "12.7.
|
|
1304
|
-
"knip": "
|
|
1357
|
+
"commit-and-tag-version": "12.7.1",
|
|
1358
|
+
"knip": "6.0.3",
|
|
1305
1359
|
"lefthook": "2.1.4",
|
|
1306
1360
|
"oxfmt": "0.41.0",
|
|
1307
1361
|
"oxlint": "1.56.0",
|
|
@@ -1316,8 +1370,8 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1316
1370
|
"@changesets/cli": "2.30.0",
|
|
1317
1371
|
"@release-it/bumper": "7.0.5",
|
|
1318
1372
|
"@types/node": "24.12.0",
|
|
1319
|
-
"commit-and-tag-version": "12.7.
|
|
1320
|
-
"knip": "
|
|
1373
|
+
"commit-and-tag-version": "12.7.1",
|
|
1374
|
+
"knip": "6.0.3",
|
|
1321
1375
|
"lefthook": "2.1.4",
|
|
1322
1376
|
"oxfmt": "0.41.0",
|
|
1323
1377
|
"oxlint": "1.56.0",
|
|
@@ -1332,8 +1386,8 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1332
1386
|
"@changesets/cli": "2.30.0",
|
|
1333
1387
|
"@release-it/bumper": "7.0.5",
|
|
1334
1388
|
"@types/node": "24.12.0",
|
|
1335
|
-
"commit-and-tag-version": "12.7.
|
|
1336
|
-
"knip": "
|
|
1389
|
+
"commit-and-tag-version": "12.7.1",
|
|
1390
|
+
"knip": "6.0.3",
|
|
1337
1391
|
"lefthook": "2.1.4",
|
|
1338
1392
|
"oxfmt": "0.41.0",
|
|
1339
1393
|
"oxlint": "1.56.0",
|
|
@@ -1351,8 +1405,8 @@ const ROOT_DEV_DEPS = {
|
|
|
1351
1405
|
"@changesets/cli": "2.30.0",
|
|
1352
1406
|
"@release-it/bumper": "7.0.5",
|
|
1353
1407
|
"@types/node": "24.12.0",
|
|
1354
|
-
"commit-and-tag-version": "12.7.
|
|
1355
|
-
"knip": "
|
|
1408
|
+
"commit-and-tag-version": "12.7.1",
|
|
1409
|
+
"knip": "6.0.3",
|
|
1356
1410
|
"lefthook": "2.1.4",
|
|
1357
1411
|
"oxfmt": "0.41.0",
|
|
1358
1412
|
"oxlint": "1.56.0",
|
|
@@ -1367,8 +1421,8 @@ const ROOT_DEV_DEPS = {
|
|
|
1367
1421
|
"@changesets/cli": "2.30.0",
|
|
1368
1422
|
"@release-it/bumper": "7.0.5",
|
|
1369
1423
|
"@types/node": "24.12.0",
|
|
1370
|
-
"commit-and-tag-version": "12.7.
|
|
1371
|
-
"knip": "
|
|
1424
|
+
"commit-and-tag-version": "12.7.1",
|
|
1425
|
+
"knip": "6.0.3",
|
|
1372
1426
|
"lefthook": "2.1.4",
|
|
1373
1427
|
"oxfmt": "0.41.0",
|
|
1374
1428
|
"oxlint": "1.56.0",
|
|
@@ -1383,8 +1437,8 @@ const ROOT_DEV_DEPS = {
|
|
|
1383
1437
|
"@changesets/cli": "2.30.0",
|
|
1384
1438
|
"@release-it/bumper": "7.0.5",
|
|
1385
1439
|
"@types/node": "24.12.0",
|
|
1386
|
-
"commit-and-tag-version": "12.7.
|
|
1387
|
-
"knip": "
|
|
1440
|
+
"commit-and-tag-version": "12.7.1",
|
|
1441
|
+
"knip": "6.0.3",
|
|
1388
1442
|
"lefthook": "2.1.4",
|
|
1389
1443
|
"oxfmt": "0.41.0",
|
|
1390
1444
|
"oxlint": "1.56.0",
|
|
@@ -1416,8 +1470,6 @@ function isWorkspacePackage(ctx, packageName) {
|
|
|
1416
1470
|
} catch (_error) {}
|
|
1417
1471
|
return false;
|
|
1418
1472
|
}
|
|
1419
|
-
/** Deps that should not be blindly bumped to latest (version-sensitive). */
|
|
1420
|
-
const UPDATE_EXCLUDE = new Set(["@types/node"]);
|
|
1421
1473
|
/** Add release-strategy-specific devDeps to a deps record. */
|
|
1422
1474
|
function addReleaseDeps(deps, config) {
|
|
1423
1475
|
switch (config.releaseStrategy) {
|
|
@@ -1426,8 +1478,8 @@ function addReleaseDeps(deps, config) {
|
|
|
1426
1478
|
"@changesets/cli": "2.30.0",
|
|
1427
1479
|
"@release-it/bumper": "7.0.5",
|
|
1428
1480
|
"@types/node": "24.12.0",
|
|
1429
|
-
"commit-and-tag-version": "12.7.
|
|
1430
|
-
"knip": "
|
|
1481
|
+
"commit-and-tag-version": "12.7.1",
|
|
1482
|
+
"knip": "6.0.3",
|
|
1431
1483
|
"lefthook": "2.1.4",
|
|
1432
1484
|
"oxfmt": "0.41.0",
|
|
1433
1485
|
"oxlint": "1.56.0",
|
|
@@ -1442,8 +1494,8 @@ function addReleaseDeps(deps, config) {
|
|
|
1442
1494
|
"@changesets/cli": "2.30.0",
|
|
1443
1495
|
"@release-it/bumper": "7.0.5",
|
|
1444
1496
|
"@types/node": "24.12.0",
|
|
1445
|
-
"commit-and-tag-version": "12.7.
|
|
1446
|
-
"knip": "
|
|
1497
|
+
"commit-and-tag-version": "12.7.1",
|
|
1498
|
+
"knip": "6.0.3",
|
|
1447
1499
|
"lefthook": "2.1.4",
|
|
1448
1500
|
"oxfmt": "0.41.0",
|
|
1449
1501
|
"oxlint": "1.56.0",
|
|
@@ -1460,8 +1512,8 @@ function addReleaseDeps(deps, config) {
|
|
|
1460
1512
|
"@changesets/cli": "2.30.0",
|
|
1461
1513
|
"@release-it/bumper": "7.0.5",
|
|
1462
1514
|
"@types/node": "24.12.0",
|
|
1463
|
-
"commit-and-tag-version": "12.7.
|
|
1464
|
-
"knip": "
|
|
1515
|
+
"commit-and-tag-version": "12.7.1",
|
|
1516
|
+
"knip": "6.0.3",
|
|
1465
1517
|
"lefthook": "2.1.4",
|
|
1466
1518
|
"oxfmt": "0.41.0",
|
|
1467
1519
|
"oxlint": "1.56.0",
|
|
@@ -1478,8 +1530,8 @@ function addReleaseDeps(deps, config) {
|
|
|
1478
1530
|
"@changesets/cli": "2.30.0",
|
|
1479
1531
|
"@release-it/bumper": "7.0.5",
|
|
1480
1532
|
"@types/node": "24.12.0",
|
|
1481
|
-
"commit-and-tag-version": "12.7.
|
|
1482
|
-
"knip": "
|
|
1533
|
+
"commit-and-tag-version": "12.7.1",
|
|
1534
|
+
"knip": "6.0.3",
|
|
1483
1535
|
"lefthook": "2.1.4",
|
|
1484
1536
|
"oxfmt": "0.41.0",
|
|
1485
1537
|
"oxlint": "1.56.0",
|
|
@@ -1493,47 +1545,6 @@ function addReleaseDeps(deps, config) {
|
|
|
1493
1545
|
break;
|
|
1494
1546
|
}
|
|
1495
1547
|
}
|
|
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
1548
|
async function generatePackageJson(ctx) {
|
|
1538
1549
|
const filePath = "package.json";
|
|
1539
1550
|
const existing = ctx.read(filePath);
|
|
@@ -1552,15 +1563,15 @@ async function generatePackageJson(ctx) {
|
|
|
1552
1563
|
}
|
|
1553
1564
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1554
1565
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1555
|
-
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.
|
|
1556
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1557
|
-
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.
|
|
1566
|
+
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.2";
|
|
1567
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.38.0";
|
|
1568
|
+
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.10.0";
|
|
1558
1569
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1559
1570
|
"@changesets/cli": "2.30.0",
|
|
1560
1571
|
"@release-it/bumper": "7.0.5",
|
|
1561
1572
|
"@types/node": "24.12.0",
|
|
1562
|
-
"commit-and-tag-version": "12.7.
|
|
1563
|
-
"knip": "
|
|
1573
|
+
"commit-and-tag-version": "12.7.1",
|
|
1574
|
+
"knip": "6.0.3",
|
|
1564
1575
|
"lefthook": "2.1.4",
|
|
1565
1576
|
"oxfmt": "0.41.0",
|
|
1566
1577
|
"oxlint": "1.56.0",
|
|
@@ -1575,8 +1586,8 @@ async function generatePackageJson(ctx) {
|
|
|
1575
1586
|
"@changesets/cli": "2.30.0",
|
|
1576
1587
|
"@release-it/bumper": "7.0.5",
|
|
1577
1588
|
"@types/node": "24.12.0",
|
|
1578
|
-
"commit-and-tag-version": "12.7.
|
|
1579
|
-
"knip": "
|
|
1589
|
+
"commit-and-tag-version": "12.7.1",
|
|
1590
|
+
"knip": "6.0.3",
|
|
1580
1591
|
"lefthook": "2.1.4",
|
|
1581
1592
|
"oxfmt": "0.41.0",
|
|
1582
1593
|
"oxlint": "1.56.0",
|
|
@@ -1659,8 +1670,8 @@ async function generatePackageJson(ctx) {
|
|
|
1659
1670
|
"@changesets/cli": "2.30.0",
|
|
1660
1671
|
"@release-it/bumper": "7.0.5",
|
|
1661
1672
|
"@types/node": "24.12.0",
|
|
1662
|
-
"commit-and-tag-version": "12.7.
|
|
1663
|
-
"knip": "
|
|
1673
|
+
"commit-and-tag-version": "12.7.1",
|
|
1674
|
+
"knip": "6.0.3",
|
|
1664
1675
|
"lefthook": "2.1.4",
|
|
1665
1676
|
"oxfmt": "0.41.0",
|
|
1666
1677
|
"oxlint": "1.56.0",
|
|
@@ -2405,6 +2416,7 @@ function buildSettings(ctx) {
|
|
|
2405
2416
|
`Bash(${pm} knip)`,
|
|
2406
2417
|
`Bash(${pm} knip *)`,
|
|
2407
2418
|
`Bash(${pm} check)`,
|
|
2419
|
+
`Bash(${pm} ci:check)`,
|
|
2408
2420
|
`Bash(${pm} exec oxlint *)`,
|
|
2409
2421
|
`Bash(${pm} exec knip *)`,
|
|
2410
2422
|
"Bash(git status *)",
|
|
@@ -2578,7 +2590,7 @@ async function generateClaudeSettings(ctx) {
|
|
|
2578
2590
|
}
|
|
2579
2591
|
//#endregion
|
|
2580
2592
|
//#region src/generators/release-it.ts
|
|
2581
|
-
function buildConfig$
|
|
2593
|
+
function buildConfig$3(ci, isMonorepo) {
|
|
2582
2594
|
const config = {
|
|
2583
2595
|
$schema: "https://unpkg.com/release-it/schema/release-it.json",
|
|
2584
2596
|
git: {
|
|
@@ -2606,7 +2618,7 @@ async function generateReleaseIt(ctx) {
|
|
|
2606
2618
|
action: "skipped",
|
|
2607
2619
|
description: "release-it not requested"
|
|
2608
2620
|
};
|
|
2609
|
-
const content = JSON.stringify(buildConfig$
|
|
2621
|
+
const content = JSON.stringify(buildConfig$3(ctx.config.ci, ctx.config.structure === "monorepo"), null, 2) + "\n";
|
|
2610
2622
|
const existing = ctx.read(filePath);
|
|
2611
2623
|
if (existing) {
|
|
2612
2624
|
if (contentEqual(filePath, existing, content)) return {
|
|
@@ -2629,7 +2641,7 @@ async function generateReleaseIt(ctx) {
|
|
|
2629
2641
|
}
|
|
2630
2642
|
//#endregion
|
|
2631
2643
|
//#region src/generators/changesets.ts
|
|
2632
|
-
function buildConfig$
|
|
2644
|
+
function buildConfig$2() {
|
|
2633
2645
|
return {
|
|
2634
2646
|
$schema: "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
|
2635
2647
|
changelog: "@changesets/cli/changelog",
|
|
@@ -2649,7 +2661,7 @@ async function generateChangesets(ctx) {
|
|
|
2649
2661
|
action: "skipped",
|
|
2650
2662
|
description: "Changesets not requested"
|
|
2651
2663
|
};
|
|
2652
|
-
const content = JSON.stringify(buildConfig$
|
|
2664
|
+
const content = JSON.stringify(buildConfig$2(), null, 2) + "\n";
|
|
2653
2665
|
const existing = ctx.read(filePath);
|
|
2654
2666
|
if (existing) return {
|
|
2655
2667
|
filePath,
|
|
@@ -2732,7 +2744,19 @@ function simpleReleaseCommand() {
|
|
|
2732
2744
|
"pnpm exec bst release:simple $FLAGS"
|
|
2733
2745
|
].join("\n");
|
|
2734
2746
|
}
|
|
2735
|
-
|
|
2747
|
+
/** Build the docker publish env block, omitting registry vars when derivable at runtime. */
|
|
2748
|
+
function dockerPublishEnv(registryDerivable) {
|
|
2749
|
+
return {
|
|
2750
|
+
...registryDerivable ? {} : {
|
|
2751
|
+
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
2752
|
+
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
2753
|
+
},
|
|
2754
|
+
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
2755
|
+
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
2756
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
|
|
2736
2760
|
const releaseStep = { step: {
|
|
2737
2761
|
name: "Release",
|
|
2738
2762
|
env: {
|
|
@@ -2748,13 +2772,7 @@ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
|
2748
2772
|
const dockerStep = { step: {
|
|
2749
2773
|
name: "Publish Docker images",
|
|
2750
2774
|
if: "success()",
|
|
2751
|
-
env:
|
|
2752
|
-
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
2753
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE"),
|
|
2754
|
-
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
2755
|
-
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
2756
|
-
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2757
|
-
},
|
|
2775
|
+
env: dockerPublishEnv(registryDerivable),
|
|
2758
2776
|
run: "pnpm exec bst docker:publish"
|
|
2759
2777
|
} };
|
|
2760
2778
|
return [
|
|
@@ -2763,10 +2781,10 @@ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
|
2763
2781
|
...hasDocker ? [dockerStep] : []
|
|
2764
2782
|
];
|
|
2765
2783
|
}
|
|
2766
|
-
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
2784
|
+
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
|
|
2767
2785
|
switch (strategy) {
|
|
2768
2786
|
case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
|
|
2769
|
-
case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker);
|
|
2787
|
+
case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
|
|
2770
2788
|
default: return null;
|
|
2771
2789
|
}
|
|
2772
2790
|
}
|
|
@@ -2787,7 +2805,8 @@ async function generateReleaseCi(ctx) {
|
|
|
2787
2805
|
const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
|
|
2788
2806
|
const nodeVersionYaml = computeNodeVersionYaml(ctx);
|
|
2789
2807
|
const hasDocker = hasDockerPackages(ctx);
|
|
2790
|
-
const
|
|
2808
|
+
const registryDerivable = hasRepositoryUrl(ctx);
|
|
2809
|
+
const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
|
|
2791
2810
|
if (!steps) return {
|
|
2792
2811
|
filePath,
|
|
2793
2812
|
action: "skipped",
|
|
@@ -2836,7 +2855,7 @@ function requiredCommands(formatter) {
|
|
|
2836
2855
|
}
|
|
2837
2856
|
};
|
|
2838
2857
|
}
|
|
2839
|
-
function buildConfig(formatter) {
|
|
2858
|
+
function buildConfig$1(formatter) {
|
|
2840
2859
|
return [
|
|
2841
2860
|
"pre-commit:",
|
|
2842
2861
|
" commands:",
|
|
@@ -2940,7 +2959,7 @@ function cleanPackageJson(ctx, results) {
|
|
|
2940
2959
|
}
|
|
2941
2960
|
async function generateLefthook(ctx) {
|
|
2942
2961
|
const filePath = "lefthook.yml";
|
|
2943
|
-
const content = buildConfig(ctx.config.formatter);
|
|
2962
|
+
const content = buildConfig$1(ctx.config.formatter);
|
|
2944
2963
|
const results = [];
|
|
2945
2964
|
archiveHuskyHooks(ctx, results);
|
|
2946
2965
|
archiveLintStagedConfigs(ctx, results);
|
|
@@ -3081,6 +3100,72 @@ async function generateVscodeSettings(ctx) {
|
|
|
3081
3100
|
return results;
|
|
3082
3101
|
}
|
|
3083
3102
|
//#endregion
|
|
3103
|
+
//#region src/generators/versionrc.ts
|
|
3104
|
+
function buildConfig() {
|
|
3105
|
+
return {
|
|
3106
|
+
header: "# Changelog\n\nAll notable changes to this project will be documented in this file.\n",
|
|
3107
|
+
types: [
|
|
3108
|
+
{
|
|
3109
|
+
type: "feat",
|
|
3110
|
+
section: "Features"
|
|
3111
|
+
},
|
|
3112
|
+
{
|
|
3113
|
+
type: "fix",
|
|
3114
|
+
section: "Bug Fixes"
|
|
3115
|
+
},
|
|
3116
|
+
{
|
|
3117
|
+
type: "chore",
|
|
3118
|
+
section: "Chores"
|
|
3119
|
+
},
|
|
3120
|
+
{
|
|
3121
|
+
type: "refactor",
|
|
3122
|
+
section: "Refactoring"
|
|
3123
|
+
},
|
|
3124
|
+
{
|
|
3125
|
+
type: "perf",
|
|
3126
|
+
section: "Performance"
|
|
3127
|
+
},
|
|
3128
|
+
{
|
|
3129
|
+
type: "docs",
|
|
3130
|
+
hidden: true
|
|
3131
|
+
},
|
|
3132
|
+
{
|
|
3133
|
+
type: "style",
|
|
3134
|
+
hidden: true
|
|
3135
|
+
},
|
|
3136
|
+
{
|
|
3137
|
+
type: "test",
|
|
3138
|
+
hidden: true
|
|
3139
|
+
},
|
|
3140
|
+
{
|
|
3141
|
+
type: "ci",
|
|
3142
|
+
hidden: true
|
|
3143
|
+
}
|
|
3144
|
+
],
|
|
3145
|
+
tagPrefix: "v"
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
async function generateVersionrc(ctx) {
|
|
3149
|
+
const filePath = ".versionrc.json";
|
|
3150
|
+
if (ctx.config.releaseStrategy !== "simple") return {
|
|
3151
|
+
filePath,
|
|
3152
|
+
action: "skipped",
|
|
3153
|
+
description: "simple release not requested"
|
|
3154
|
+
};
|
|
3155
|
+
if (ctx.exists(filePath)) return {
|
|
3156
|
+
filePath,
|
|
3157
|
+
action: "skipped",
|
|
3158
|
+
description: "Existing versionrc config preserved"
|
|
3159
|
+
};
|
|
3160
|
+
const content = JSON.stringify(buildConfig(), null, 2) + "\n";
|
|
3161
|
+
ctx.write(filePath, content);
|
|
3162
|
+
return {
|
|
3163
|
+
filePath,
|
|
3164
|
+
action: "created",
|
|
3165
|
+
description: "Generated .versionrc.json"
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
//#endregion
|
|
3084
3169
|
//#region src/generators/pipeline.ts
|
|
3085
3170
|
/** Run all generators sequentially and return their results. */
|
|
3086
3171
|
async function runGenerators(ctx) {
|
|
@@ -3098,6 +3183,7 @@ async function runGenerators(ctx) {
|
|
|
3098
3183
|
results.push(await generateCi(ctx));
|
|
3099
3184
|
results.push(...await generateClaudeSettings(ctx));
|
|
3100
3185
|
results.push(await generateReleaseIt(ctx));
|
|
3186
|
+
results.push(await generateVersionrc(ctx));
|
|
3101
3187
|
results.push(await generateChangesets(ctx));
|
|
3102
3188
|
results.push(await generateReleaseCi(ctx));
|
|
3103
3189
|
results.push(await generateDeployCi(ctx));
|
|
@@ -3110,173 +3196,121 @@ async function runGenerators(ctx) {
|
|
|
3110
3196
|
//#region src/generators/migrate-prompt.ts
|
|
3111
3197
|
/**
|
|
3112
3198
|
* Generate a context-aware AI migration prompt based on what the CLI did.
|
|
3113
|
-
*
|
|
3199
|
+
* Piped into Claude Code via `cat .tooling-migrate.md | claude`.
|
|
3114
3200
|
*/
|
|
3115
3201
|
function generateMigratePrompt(results, config, detected) {
|
|
3116
3202
|
const sections = [];
|
|
3117
3203
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3118
|
-
sections.push(
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
sections.push(
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3204
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.38.0 repo:sync\` on ${timestamp}_\n`);
|
|
3205
|
+
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3206
|
+
if (changed.length > 0) {
|
|
3207
|
+
const items = changed.map((r) => {
|
|
3208
|
+
const label = r.action === "archived" ? "archived" : r.action;
|
|
3209
|
+
return `- \`${r.filePath}\` (${label}) — ${r.description}`;
|
|
3210
|
+
}).join("\n");
|
|
3211
|
+
sections.push(`## Files changed\n\n${items}\n`);
|
|
3212
|
+
}
|
|
3213
|
+
const tasks = [];
|
|
3214
|
+
addLegacyToolingTasks(tasks, config, detected);
|
|
3215
|
+
addArchivedFileTasks(tasks, results);
|
|
3216
|
+
addOxlintTasks(tasks, results, detected);
|
|
3217
|
+
addMonorepoTasks(tasks, config, detected);
|
|
3218
|
+
addTestGenerationTasks(tasks, results);
|
|
3219
|
+
if (tasks.length > 0) {
|
|
3220
|
+
sections.push(`## Migration tasks\n\n${tasks.join("\n")}`);
|
|
3221
|
+
sections.push(`## Ground rules
|
|
3222
|
+
|
|
3223
|
+
Prefer fixing root causes over suppressing issues:
|
|
3224
|
+
|
|
3225
|
+
- **Lint errors**: fix the code, not disable comments
|
|
3226
|
+
- **Type errors**: add proper types, not \`any\` or \`@ts-expect-error\`
|
|
3227
|
+
- **Test failures**: fix the test or bug, don't skip/delete
|
|
3228
|
+
- **Knip findings**: remove unused code, don't add ignores
|
|
3229
|
+
|
|
3230
|
+
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.
|
|
3231
|
+
`);
|
|
3232
|
+
}
|
|
3233
|
+
sections.push(`## Verify
|
|
3234
|
+
|
|
3235
|
+
Run \`pnpm check\` and fix any errors. This runs the full pipeline:
|
|
3236
|
+
build → typecheck → lint → test → format → knip
|
|
3237
|
+
|
|
3238
|
+
When everything passes, run \`/init\` to generate a \`CLAUDE.md\` for this repository.
|
|
3239
|
+
`);
|
|
3240
|
+
return sections.join("\n");
|
|
3241
|
+
}
|
|
3242
|
+
const LEGACY_REPLACEMENTS = {
|
|
3243
|
+
eslint: "oxlint",
|
|
3244
|
+
prettier: "oxfmt",
|
|
3245
|
+
jest: "vitest",
|
|
3246
|
+
webpack: "tsdown",
|
|
3247
|
+
rollup: "tsdown"
|
|
3248
|
+
};
|
|
3249
|
+
function addLegacyToolingTasks(tasks, config, detected) {
|
|
3154
3250
|
const legacyToRemove = detected.legacyConfigs.filter((legacy) => !(legacy.tool === "prettier" && config.formatter === "prettier"));
|
|
3155
|
-
if (legacyToRemove.length
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
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("");
|
|
3191
|
-
}
|
|
3192
|
-
const oxlintWasSkipped = results.find((r) => r.filePath === "oxlint.config.ts")?.action === "skipped";
|
|
3251
|
+
if (legacyToRemove.length === 0) return;
|
|
3252
|
+
const items = legacyToRemove.map((legacy) => {
|
|
3253
|
+
const replacement = LEGACY_REPLACEMENTS[legacy.tool];
|
|
3254
|
+
const files = legacy.files.map((f) => `\`${f}\``).join(", ");
|
|
3255
|
+
const lines = [`- Remove ${legacy.tool} config files (${files}). This project now uses **${replacement}**.`, ` - Uninstall ${legacy.tool}-related packages from devDependencies`];
|
|
3256
|
+
if (legacy.tool === "eslint") lines.push(" - Migrate any custom ESLint rules that don't have oxlint equivalents");
|
|
3257
|
+
if (legacy.tool === "jest") lines.push(" - Migrate jest-specific test utilities (jest.mock, jest.fn) to vitest equivalents (vi.mock, vi.fn)");
|
|
3258
|
+
return lines.join("\n");
|
|
3259
|
+
});
|
|
3260
|
+
tasks.push(`### Remove legacy tooling\n\n${items.join("\n")}\n`);
|
|
3261
|
+
}
|
|
3262
|
+
function addArchivedFileTasks(tasks, results) {
|
|
3263
|
+
const archived = results.filter((r) => r.action === "archived");
|
|
3264
|
+
if (archived.length === 0) return;
|
|
3265
|
+
const items = archived.map((r) => `- \`${r.filePath}\` → \`.tooling-archived/${r.filePath}\``).join("\n");
|
|
3266
|
+
tasks.push(`### Review archived files
|
|
3267
|
+
|
|
3268
|
+
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:
|
|
3269
|
+
|
|
3270
|
+
${items}
|
|
3271
|
+
|
|
3272
|
+
Delete \`.tooling-archived/\` when done.
|
|
3273
|
+
`);
|
|
3274
|
+
}
|
|
3275
|
+
function addOxlintTasks(tasks, results, detected) {
|
|
3193
3276
|
if (detected.hasLegacyOxlintJson) {
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
}
|
|
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:");
|
|
3237
|
-
sections.push("");
|
|
3238
|
-
for (const r of skippedConfigs) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
3239
|
-
sections.push("");
|
|
3240
|
-
}
|
|
3241
|
-
if (results.some((r) => r.filePath === "test/example.test.ts" && r.action === "created")) {
|
|
3242
|
-
sections.push("### Generate tests");
|
|
3243
|
-
sections.push("");
|
|
3244
|
-
sections.push("A starter test was created at `test/example.test.ts`. Now:");
|
|
3245
|
-
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");
|
|
3250
|
-
sections.push("");
|
|
3251
|
-
}
|
|
3252
|
-
sections.push("## Ground rules");
|
|
3253
|
-
sections.push("");
|
|
3254
|
-
sections.push("It is OK to add new packages (e.g. `zod`, `@bensandee/common`) if they are needed to resolve errors.");
|
|
3255
|
-
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");
|
|
3278
|
-
sections.push("");
|
|
3279
|
-
return sections.join("\n");
|
|
3277
|
+
tasks.push(`### Migrate .oxlintrc.json to oxlint.config.ts
|
|
3278
|
+
|
|
3279
|
+
Compare \`.oxlintrc.json\` rules against \`@bensandee/config/oxlint/recommended\`. Port any custom rules/overrides into \`oxlint.config.ts\`, then delete \`.oxlintrc.json\`.
|
|
3280
|
+
`);
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
if (results.find((r) => r.filePath === "oxlint.config.ts")?.action === "skipped" && detected.hasOxlintConfig && !detected.oxlintExtendsRecommended) tasks.push(`### Verify oxlint.config.ts extends recommended rules
|
|
3284
|
+
|
|
3285
|
+
Ensure \`oxlint.config.ts\` imports and extends \`@bensandee/config/oxlint/recommended\`:
|
|
3286
|
+
|
|
3287
|
+
\`\`\`ts
|
|
3288
|
+
import recommended from "@bensandee/config/oxlint/recommended";
|
|
3289
|
+
import { defineConfig } from "oxlint";
|
|
3290
|
+
|
|
3291
|
+
export default defineConfig({ extends: [recommended] });
|
|
3292
|
+
\`\`\`
|
|
3293
|
+
`);
|
|
3294
|
+
}
|
|
3295
|
+
function addMonorepoTasks(tasks, config, detected) {
|
|
3296
|
+
if (config.structure !== "monorepo" || detected.hasPnpmWorkspace) return;
|
|
3297
|
+
tasks.push(`### Migrate to monorepo structure
|
|
3298
|
+
|
|
3299
|
+
This project was converted to a monorepo. Complete the migration:
|
|
3300
|
+
|
|
3301
|
+
1. Move existing source into \`packages/<name>/\`
|
|
3302
|
+
2. Split root \`package.json\` into workspace manifest + package-level \`package.json\`
|
|
3303
|
+
3. Move \`tsconfig.json\` into the package, add project references to root
|
|
3304
|
+
4. Create a package-level \`tsdown.config.ts\`
|
|
3305
|
+
5. Update import paths and build scripts
|
|
3306
|
+
`);
|
|
3307
|
+
}
|
|
3308
|
+
function addTestGenerationTasks(tasks, results) {
|
|
3309
|
+
if (!results.some((r) => r.filePath === "test/example.test.ts" && r.action === "created")) return;
|
|
3310
|
+
tasks.push(`### Generate tests
|
|
3311
|
+
|
|
3312
|
+
A starter test was created at \`test/example.test.ts\`. Review \`src/\` and create additional tests covering exported functions and edge cases.
|
|
3313
|
+
`);
|
|
3280
3314
|
}
|
|
3281
3315
|
//#endregion
|
|
3282
3316
|
//#region src/commands/repo-init.ts
|
|
@@ -3301,11 +3335,11 @@ function contextAsDockerReader(ctx) {
|
|
|
3301
3335
|
function logDetectionSummary(ctx) {
|
|
3302
3336
|
if (ctx.config.publishDocker) {
|
|
3303
3337
|
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(", ")}`);
|
|
3338
|
+
if (dockerPackages.length > 0) log.info(`Docker images to build and publish: ${dockerPackages.map((pkg) => pkg.imageName).join(", ")}`);
|
|
3305
3339
|
}
|
|
3306
3340
|
if (ctx.config.publishNpm) {
|
|
3307
3341
|
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(", ")}`);
|
|
3342
|
+
if (publishable.length > 0) log.info(`npm packages to publish: ${publishable.map((pkg) => pkg.name).join(", ")}`);
|
|
3309
3343
|
}
|
|
3310
3344
|
}
|
|
3311
3345
|
async function runInit(config, options = {}) {
|
|
@@ -3353,22 +3387,19 @@ async function runInit(config, options = {}) {
|
|
|
3353
3387
|
if (created.length > 0) summaryLines.push(`Created: ${created.map((r) => r.filePath).join(", ")}`);
|
|
3354
3388
|
if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
|
|
3355
3389
|
note(summaryLines.join("\n"), "Summary");
|
|
3356
|
-
if (!options.noPrompt) {
|
|
3357
|
-
|
|
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/"));
|
|
3390
|
+
if (!options.noPrompt) log.info(`Migration prompt written to ${promptPath}`);
|
|
3391
|
+
const packageJsonChanged = results.some((r) => r.filePath === "package.json" && (r.action === "created" || r.action === "updated"));
|
|
3361
3392
|
const hasLockfile = ctx.exists("pnpm-lock.yaml");
|
|
3362
|
-
if (
|
|
3363
|
-
log.info("
|
|
3393
|
+
if (packageJsonChanged && hasLockfile) {
|
|
3394
|
+
log.info("package.json changed — running pnpm install...");
|
|
3364
3395
|
try {
|
|
3365
|
-
execSync(
|
|
3396
|
+
execSync("pnpm install", {
|
|
3366
3397
|
cwd: config.targetDir,
|
|
3367
3398
|
stdio: "inherit",
|
|
3368
|
-
timeout:
|
|
3399
|
+
timeout: 12e4
|
|
3369
3400
|
});
|
|
3370
3401
|
} catch (_error) {
|
|
3371
|
-
log.warn("
|
|
3402
|
+
log.warn("pnpm install failed — run it manually");
|
|
3372
3403
|
}
|
|
3373
3404
|
}
|
|
3374
3405
|
if (hasChanges && ctx.exists("package.json")) try {
|
|
@@ -3379,10 +3410,10 @@ async function runInit(config, options = {}) {
|
|
|
3379
3410
|
});
|
|
3380
3411
|
} catch (_error) {}
|
|
3381
3412
|
note([
|
|
3382
|
-
"
|
|
3383
|
-
"
|
|
3384
|
-
|
|
3385
|
-
].join("\n"), "Next steps");
|
|
3413
|
+
...packageJsonChanged && !hasLockfile ? ["Run: pnpm install"] : [],
|
|
3414
|
+
"Run: pnpm check",
|
|
3415
|
+
...!options.noPrompt ? ["Run: claude \"Execute the migration steps in .tooling-migrate.md\""] : []
|
|
3416
|
+
].map((step, i) => `${i + 1}. ${step}`).join("\n"), "Next steps");
|
|
3386
3417
|
return results;
|
|
3387
3418
|
}
|
|
3388
3419
|
//#endregion
|
|
@@ -3498,15 +3529,16 @@ function lineDiff(oldText, newText) {
|
|
|
3498
3529
|
/** Create an executor that runs real commands, fetches, and reads the filesystem. */
|
|
3499
3530
|
function createRealExecutor() {
|
|
3500
3531
|
return {
|
|
3501
|
-
exec(
|
|
3532
|
+
exec(file, args, options) {
|
|
3502
3533
|
try {
|
|
3503
3534
|
return {
|
|
3504
|
-
stdout:
|
|
3535
|
+
stdout: execFileSync(file, args, {
|
|
3505
3536
|
cwd: options?.cwd,
|
|
3506
3537
|
env: {
|
|
3507
3538
|
...process.env,
|
|
3508
3539
|
...options?.env
|
|
3509
3540
|
},
|
|
3541
|
+
input: options?.input,
|
|
3510
3542
|
encoding: "utf-8",
|
|
3511
3543
|
stdio: [
|
|
3512
3544
|
"pipe",
|
|
@@ -3530,8 +3562,8 @@ function createRealExecutor() {
|
|
|
3530
3562
|
};
|
|
3531
3563
|
}
|
|
3532
3564
|
},
|
|
3533
|
-
execInherit(
|
|
3534
|
-
|
|
3565
|
+
execInherit(file, args, options) {
|
|
3566
|
+
execFileSync(file, args, {
|
|
3535
3567
|
cwd: options?.cwd,
|
|
3536
3568
|
env: options?.env ? {
|
|
3537
3569
|
...process.env,
|
|
@@ -3588,6 +3620,19 @@ function createRealExecutor() {
|
|
|
3588
3620
|
}
|
|
3589
3621
|
};
|
|
3590
3622
|
}
|
|
3623
|
+
/**
|
|
3624
|
+
* Compute sliding version tags from a semver version string.
|
|
3625
|
+
* For "1.2.3" returns ["v1", "v1.2", "latest"]. Strips prerelease suffixes.
|
|
3626
|
+
*/
|
|
3627
|
+
function computeSlidingTags(version) {
|
|
3628
|
+
const parts = (version.split("-")[0] ?? version).split(".");
|
|
3629
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) throw new FatalError(`Invalid version format "${version}". Expected semver (X.Y.Z)`);
|
|
3630
|
+
return [
|
|
3631
|
+
`v${parts[0]}`,
|
|
3632
|
+
`v${parts[0]}.${parts[1]}`,
|
|
3633
|
+
"latest"
|
|
3634
|
+
];
|
|
3635
|
+
}
|
|
3591
3636
|
/** Parse "New tag:" lines from changeset publish output. */
|
|
3592
3637
|
function parseNewTags(output) {
|
|
3593
3638
|
const tags = [];
|
|
@@ -3629,7 +3674,7 @@ const RETRY_BASE_DELAY_MS = 1e3;
|
|
|
3629
3674
|
async function responseBodyText(res) {
|
|
3630
3675
|
try {
|
|
3631
3676
|
const text = await res.text();
|
|
3632
|
-
return text.length > 500 ? text.slice(0, 500) + "…" : text;
|
|
3677
|
+
return redact(text.length > 500 ? text.slice(0, 500) + "…" : text);
|
|
3633
3678
|
} catch {
|
|
3634
3679
|
return "(could not read response body)";
|
|
3635
3680
|
}
|
|
@@ -3856,19 +3901,23 @@ async function runVersionMode(executor, config) {
|
|
|
3856
3901
|
debug(config, "Temporarily disabled changeset commit:true");
|
|
3857
3902
|
}
|
|
3858
3903
|
}
|
|
3859
|
-
const versionResult = executor.exec("pnpm changeset version", { cwd: config.cwd });
|
|
3904
|
+
const versionResult = executor.exec("pnpm", ["changeset", "version"], { cwd: config.cwd });
|
|
3860
3905
|
debugExec(config, "pnpm changeset version", versionResult);
|
|
3861
3906
|
if (originalConfig) executor.writeFile(changesetConfigPath, originalConfig);
|
|
3862
3907
|
if (versionResult.exitCode !== 0) throw new FatalError(`pnpm changeset version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr}`);
|
|
3863
|
-
debugExec(config, "pnpm install --no-frozen-lockfile", executor.exec("pnpm install --no-frozen-lockfile", { cwd: config.cwd }));
|
|
3908
|
+
debugExec(config, "pnpm install --no-frozen-lockfile", executor.exec("pnpm", ["install", "--no-frozen-lockfile"], { cwd: config.cwd }));
|
|
3864
3909
|
const { title, body } = buildPrContent(executor, config.cwd, packagesBefore);
|
|
3865
3910
|
debug(config, `PR title: ${title}`);
|
|
3866
|
-
const addResult = executor.exec("git add -A", { cwd: config.cwd });
|
|
3911
|
+
const addResult = executor.exec("git", ["add", "-A"], { cwd: config.cwd });
|
|
3867
3912
|
if (addResult.exitCode !== 0) throw new FatalError(`git add failed: ${addResult.stderr || addResult.stdout}`);
|
|
3868
3913
|
const remainingChangesets = executor.listChangesetFiles(config.cwd);
|
|
3869
3914
|
if (remainingChangesets.length > 0) log.warn(`Changeset files still present after versioning: ${remainingChangesets.join(", ")}`);
|
|
3870
3915
|
debug(config, `Changeset files after versioning: ${remainingChangesets.length > 0 ? remainingChangesets.join(", ") : "(none — all consumed)"}`);
|
|
3871
|
-
const commitResult = executor.exec("git
|
|
3916
|
+
const commitResult = executor.exec("git", [
|
|
3917
|
+
"commit",
|
|
3918
|
+
"-m",
|
|
3919
|
+
"chore: version packages"
|
|
3920
|
+
], { cwd: config.cwd });
|
|
3872
3921
|
debugExec(config, "git commit", commitResult);
|
|
3873
3922
|
if (commitResult.exitCode !== 0) {
|
|
3874
3923
|
log.info("Nothing to commit after versioning");
|
|
@@ -3884,7 +3933,12 @@ async function runVersionMode(executor, config) {
|
|
|
3884
3933
|
pr: "none"
|
|
3885
3934
|
};
|
|
3886
3935
|
}
|
|
3887
|
-
const pushResult = executor.exec(
|
|
3936
|
+
const pushResult = executor.exec("git", [
|
|
3937
|
+
"push",
|
|
3938
|
+
"origin",
|
|
3939
|
+
`HEAD:refs/heads/${BRANCH}`,
|
|
3940
|
+
"--force"
|
|
3941
|
+
], { cwd: config.cwd });
|
|
3888
3942
|
debugExec(config, "git push", pushResult);
|
|
3889
3943
|
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
3890
3944
|
const conn = {
|
|
@@ -3922,14 +3976,18 @@ async function runVersionMode(executor, config) {
|
|
|
3922
3976
|
/** Mode 2: publish to npm, push tags, and create Forgejo releases. */
|
|
3923
3977
|
async function runPublishMode(executor, config) {
|
|
3924
3978
|
log.info("No changesets — publishing packages");
|
|
3925
|
-
const publishResult = executor.exec("pnpm changeset publish", { cwd: config.cwd });
|
|
3979
|
+
const publishResult = executor.exec("pnpm", ["changeset", "publish"], { cwd: config.cwd });
|
|
3926
3980
|
debugExec(config, "pnpm changeset publish", publishResult);
|
|
3927
3981
|
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
3928
3982
|
const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
|
|
3929
3983
|
debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
|
|
3930
3984
|
const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
|
|
3931
3985
|
debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
|
|
3932
|
-
const remoteTags = parseRemoteTags(executor.exec("git
|
|
3986
|
+
const remoteTags = parseRemoteTags(executor.exec("git", [
|
|
3987
|
+
"ls-remote",
|
|
3988
|
+
"--tags",
|
|
3989
|
+
"origin"
|
|
3990
|
+
], { cwd: config.cwd }).stdout);
|
|
3933
3991
|
debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
|
|
3934
3992
|
const remoteSet = new Set(remoteTags);
|
|
3935
3993
|
const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
|
|
@@ -3963,11 +4021,19 @@ async function runPublishMode(executor, config) {
|
|
|
3963
4021
|
const errors = [];
|
|
3964
4022
|
for (const tag of allTags) try {
|
|
3965
4023
|
if (!remoteSet.has(tag)) {
|
|
3966
|
-
if (executor.exec(
|
|
3967
|
-
|
|
4024
|
+
if (executor.exec("git", [
|
|
4025
|
+
"tag",
|
|
4026
|
+
"-l",
|
|
4027
|
+
tag
|
|
4028
|
+
], { cwd: config.cwd }).stdout.trim() === "") {
|
|
4029
|
+
const tagResult = executor.exec("git", ["tag", tag], { cwd: config.cwd });
|
|
3968
4030
|
if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
3969
4031
|
}
|
|
3970
|
-
const pushTagResult = executor.exec(
|
|
4032
|
+
const pushTagResult = executor.exec("git", [
|
|
4033
|
+
"push",
|
|
4034
|
+
"origin",
|
|
4035
|
+
`refs/tags/${tag}`
|
|
4036
|
+
], { cwd: config.cwd });
|
|
3971
4037
|
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
3972
4038
|
}
|
|
3973
4039
|
if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
|
|
@@ -3980,6 +4046,31 @@ async function runPublishMode(executor, config) {
|
|
|
3980
4046
|
log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3981
4047
|
}
|
|
3982
4048
|
if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
|
|
4049
|
+
const slidingTags = allTags.flatMap((t) => {
|
|
4050
|
+
const atIndex = t.lastIndexOf("@");
|
|
4051
|
+
if (atIndex <= 0) return [];
|
|
4052
|
+
const name = t.slice(0, atIndex);
|
|
4053
|
+
return computeSlidingTags(t.slice(atIndex + 1)).map((suffix) => `${name}@${suffix}`);
|
|
4054
|
+
});
|
|
4055
|
+
if (slidingTags.length > 0) {
|
|
4056
|
+
for (const tag of slidingTags) {
|
|
4057
|
+
const tagResult = executor.exec("git", [
|
|
4058
|
+
"tag",
|
|
4059
|
+
"-f",
|
|
4060
|
+
tag
|
|
4061
|
+
], { cwd: config.cwd });
|
|
4062
|
+
if (tagResult.exitCode !== 0) log.warn(`Warning: Failed to create sliding tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4063
|
+
}
|
|
4064
|
+
const forcePushResult = executor.exec("git", [
|
|
4065
|
+
"push",
|
|
4066
|
+
"origin",
|
|
4067
|
+
...slidingTags.map((t) => `refs/tags/${t}`),
|
|
4068
|
+
"--force"
|
|
4069
|
+
], { cwd: config.cwd });
|
|
4070
|
+
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4071
|
+
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4072
|
+
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4073
|
+
}
|
|
3983
4074
|
return {
|
|
3984
4075
|
mode: "publish",
|
|
3985
4076
|
tags: allTags
|
|
@@ -4069,15 +4160,28 @@ function parseGitUrl(urlStr) {
|
|
|
4069
4160
|
function configureGitAuth(executor, conn, cwd) {
|
|
4070
4161
|
const host = conn.serverUrl.replace(/^https?:\/\//, "");
|
|
4071
4162
|
const authUrl = `https://x-access-token:${conn.token}@${host}/${conn.repository}`;
|
|
4072
|
-
executor.exec(
|
|
4163
|
+
executor.exec("git", [
|
|
4164
|
+
"remote",
|
|
4165
|
+
"set-url",
|
|
4166
|
+
"origin",
|
|
4167
|
+
authUrl
|
|
4168
|
+
], { cwd });
|
|
4073
4169
|
}
|
|
4074
4170
|
/** Configure git user.name and user.email for CI bot commits. */
|
|
4075
4171
|
function configureGitIdentity(executor, platform, cwd) {
|
|
4076
4172
|
const isGitHub = platform === "github";
|
|
4077
4173
|
const name = isGitHub ? "github-actions[bot]" : "forgejo-actions[bot]";
|
|
4078
4174
|
const email = isGitHub ? "github-actions[bot]@users.noreply.github.com" : "forgejo-actions[bot]@noreply.localhost";
|
|
4079
|
-
executor.exec(
|
|
4080
|
-
|
|
4175
|
+
executor.exec("git", [
|
|
4176
|
+
"config",
|
|
4177
|
+
"user.name",
|
|
4178
|
+
name
|
|
4179
|
+
], { cwd });
|
|
4180
|
+
executor.exec("git", [
|
|
4181
|
+
"config",
|
|
4182
|
+
"user.email",
|
|
4183
|
+
email
|
|
4184
|
+
], { cwd });
|
|
4081
4185
|
}
|
|
4082
4186
|
//#endregion
|
|
4083
4187
|
//#region src/commands/release-changesets.ts
|
|
@@ -4118,7 +4222,11 @@ function buildReleaseConfig(flags) {
|
|
|
4118
4222
|
function getCurrentBranch(executor, cwd) {
|
|
4119
4223
|
const ref = process.env["GITHUB_REF"];
|
|
4120
4224
|
if (ref?.startsWith("refs/heads/")) return ref.slice(11);
|
|
4121
|
-
return executor.exec("git
|
|
4225
|
+
return executor.exec("git", [
|
|
4226
|
+
"rev-parse",
|
|
4227
|
+
"--abbrev-ref",
|
|
4228
|
+
"HEAD"
|
|
4229
|
+
], { cwd }).stdout.trim();
|
|
4122
4230
|
}
|
|
4123
4231
|
/** Core release logic — testable with a mock executor. */
|
|
4124
4232
|
async function runRelease(config, executor) {
|
|
@@ -4189,9 +4297,15 @@ async function triggerForgejo(conn, ref, inputs) {
|
|
|
4189
4297
|
}
|
|
4190
4298
|
function triggerGitHub(ref, inputs) {
|
|
4191
4299
|
const executor = createRealExecutor();
|
|
4192
|
-
const inputFlags = Object.entries(inputs).
|
|
4193
|
-
const
|
|
4194
|
-
|
|
4300
|
+
const inputFlags = Object.entries(inputs).flatMap(([k, v]) => ["-f", `${k}=${v}`]);
|
|
4301
|
+
const result = executor.exec("gh", [
|
|
4302
|
+
"workflow",
|
|
4303
|
+
"run",
|
|
4304
|
+
"release.yml",
|
|
4305
|
+
"--ref",
|
|
4306
|
+
ref,
|
|
4307
|
+
...inputFlags
|
|
4308
|
+
], { cwd: process.cwd() });
|
|
4195
4309
|
if (result.exitCode !== 0) throw new FatalError(`Failed to trigger GitHub workflow: ${result.stderr || result.stdout || "unknown error"}`);
|
|
4196
4310
|
log.info(`Triggered release workflow on GitHub (ref: ${ref})`);
|
|
4197
4311
|
}
|
|
@@ -4256,34 +4370,39 @@ async function mergeForgejo(conn, dryRun) {
|
|
|
4256
4370
|
function mergeGitHub(dryRun) {
|
|
4257
4371
|
const executor = createRealExecutor();
|
|
4258
4372
|
if (dryRun) {
|
|
4259
|
-
const prNum = executor.exec(
|
|
4373
|
+
const prNum = executor.exec("gh", [
|
|
4374
|
+
"pr",
|
|
4375
|
+
"view",
|
|
4376
|
+
HEAD_BRANCH,
|
|
4377
|
+
"--json",
|
|
4378
|
+
"number",
|
|
4379
|
+
"--jq",
|
|
4380
|
+
".number"
|
|
4381
|
+
], { cwd: process.cwd() }).stdout.trim();
|
|
4260
4382
|
if (!prNum) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
|
|
4261
4383
|
log.info(`[dry-run] Would merge PR #${prNum} and delete branch ${HEAD_BRANCH}`);
|
|
4262
4384
|
return;
|
|
4263
4385
|
}
|
|
4264
|
-
const result = executor.exec(
|
|
4386
|
+
const result = executor.exec("gh", [
|
|
4387
|
+
"pr",
|
|
4388
|
+
"merge",
|
|
4389
|
+
HEAD_BRANCH,
|
|
4390
|
+
"--merge",
|
|
4391
|
+
"--delete-branch"
|
|
4392
|
+
], { cwd: process.cwd() });
|
|
4265
4393
|
if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
|
|
4266
4394
|
log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
|
|
4267
4395
|
}
|
|
4268
4396
|
//#endregion
|
|
4269
4397
|
//#region src/release/simple.ts
|
|
4270
|
-
/**
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
*/
|
|
4274
|
-
function computeSlidingTags(version) {
|
|
4275
|
-
const parts = (version.split("-")[0] ?? version).split(".");
|
|
4276
|
-
if (parts.length < 2 || !parts[0] || !parts[1]) throw new FatalError(`Invalid version format "${version}". Expected semver (X.Y.Z)`);
|
|
4277
|
-
return [`v${parts[0]}`, `v${parts[0]}.${parts[1]}`];
|
|
4278
|
-
}
|
|
4279
|
-
/** Build the commit-and-tag-version command with appropriate flags. */
|
|
4280
|
-
function buildCommand(config) {
|
|
4281
|
-
const args = ["pnpm exec commit-and-tag-version"];
|
|
4398
|
+
/** Build the commit-and-tag-version args. */
|
|
4399
|
+
function buildArgs(config) {
|
|
4400
|
+
const args = ["exec", "commit-and-tag-version"];
|
|
4282
4401
|
if (config.dryRun) args.push("--dry-run");
|
|
4283
4402
|
if (config.firstRelease) args.push("--first-release");
|
|
4284
|
-
if (config.releaseAs) args.push(
|
|
4285
|
-
if (config.prerelease) args.push(
|
|
4286
|
-
return args
|
|
4403
|
+
if (config.releaseAs) args.push("--release-as", config.releaseAs);
|
|
4404
|
+
if (config.prerelease) args.push("--prerelease", config.prerelease);
|
|
4405
|
+
return args;
|
|
4287
4406
|
}
|
|
4288
4407
|
/** Read the current version from package.json. */
|
|
4289
4408
|
function readVersion(executor, cwd) {
|
|
@@ -4300,14 +4419,18 @@ function platformType(config) {
|
|
|
4300
4419
|
/** Run the full commit-and-tag-version release flow. */
|
|
4301
4420
|
async function runSimpleRelease(executor, config) {
|
|
4302
4421
|
configureGitIdentity(executor, platformType(config), config.cwd);
|
|
4303
|
-
const
|
|
4304
|
-
log.info(`Running: ${
|
|
4305
|
-
const versionResult = executor.exec(
|
|
4422
|
+
const args = buildArgs(config);
|
|
4423
|
+
log.info(`Running: pnpm ${args.join(" ")}`);
|
|
4424
|
+
const versionResult = executor.exec("pnpm", args, { cwd: config.cwd });
|
|
4306
4425
|
debugExec(config, "commit-and-tag-version", versionResult);
|
|
4307
4426
|
if (versionResult.exitCode !== 0) throw new FatalError(`commit-and-tag-version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr || versionResult.stdout}`);
|
|
4308
4427
|
const version = readVersion(executor, config.cwd);
|
|
4309
4428
|
debug(config, `New version: ${version}`);
|
|
4310
|
-
const tagResult = executor.exec("git
|
|
4429
|
+
const tagResult = executor.exec("git", [
|
|
4430
|
+
"describe",
|
|
4431
|
+
"--tags",
|
|
4432
|
+
"--abbrev=0"
|
|
4433
|
+
], { cwd: config.cwd });
|
|
4311
4434
|
debugExec(config, "git describe", tagResult);
|
|
4312
4435
|
const tag = tagResult.stdout.trim();
|
|
4313
4436
|
if (!tag) throw new FatalError("Could not determine the new tag from git describe");
|
|
@@ -4331,9 +4454,18 @@ async function runSimpleRelease(executor, config) {
|
|
|
4331
4454
|
configureGitAuth(executor, config.platform.conn, config.cwd);
|
|
4332
4455
|
debug(config, "Configured git remote with RELEASE_TOKEN auth");
|
|
4333
4456
|
}
|
|
4334
|
-
const branch = executor.exec("git
|
|
4457
|
+
const branch = executor.exec("git", [
|
|
4458
|
+
"rev-parse",
|
|
4459
|
+
"--abbrev-ref",
|
|
4460
|
+
"HEAD"
|
|
4461
|
+
], { cwd: config.cwd }).stdout.trim() || "main";
|
|
4335
4462
|
debug(config, `Pushing to origin/${branch}`);
|
|
4336
|
-
const pushResult = executor.exec(
|
|
4463
|
+
const pushResult = executor.exec("git", [
|
|
4464
|
+
"push",
|
|
4465
|
+
"--follow-tags",
|
|
4466
|
+
"origin",
|
|
4467
|
+
branch
|
|
4468
|
+
], { cwd: config.cwd });
|
|
4337
4469
|
debugExec(config, "git push", pushResult);
|
|
4338
4470
|
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
4339
4471
|
pushed = true;
|
|
@@ -4343,10 +4475,19 @@ async function runSimpleRelease(executor, config) {
|
|
|
4343
4475
|
if (!config.noSlidingTags && pushed) {
|
|
4344
4476
|
slidingTags = computeSlidingTags(version);
|
|
4345
4477
|
for (const slidingTag of slidingTags) {
|
|
4346
|
-
const slidingTagResult = executor.exec(
|
|
4478
|
+
const slidingTagResult = executor.exec("git", [
|
|
4479
|
+
"tag",
|
|
4480
|
+
"-f",
|
|
4481
|
+
slidingTag
|
|
4482
|
+
], { cwd: config.cwd });
|
|
4347
4483
|
if (slidingTagResult.exitCode !== 0) throw new FatalError(`Failed to create sliding tag ${slidingTag}: ${slidingTagResult.stderr || slidingTagResult.stdout}`);
|
|
4348
4484
|
}
|
|
4349
|
-
const forcePushResult = executor.exec(
|
|
4485
|
+
const forcePushResult = executor.exec("git", [
|
|
4486
|
+
"push",
|
|
4487
|
+
"origin",
|
|
4488
|
+
...slidingTags,
|
|
4489
|
+
"--force"
|
|
4490
|
+
], { cwd: config.cwd });
|
|
4350
4491
|
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4351
4492
|
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4352
4493
|
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
@@ -4371,7 +4512,12 @@ async function createPlatformRelease(executor, config, tag) {
|
|
|
4371
4512
|
log.info(`Created Forgejo release for ${tag}`);
|
|
4372
4513
|
return true;
|
|
4373
4514
|
}
|
|
4374
|
-
const ghResult = executor.exec(
|
|
4515
|
+
const ghResult = executor.exec("gh", [
|
|
4516
|
+
"release",
|
|
4517
|
+
"create",
|
|
4518
|
+
tag,
|
|
4519
|
+
"--generate-notes"
|
|
4520
|
+
], { cwd: config.cwd });
|
|
4375
4521
|
debugExec(config, "gh release create", ghResult);
|
|
4376
4522
|
if (ghResult.exitCode !== 0) {
|
|
4377
4523
|
log.warn(`Warning: Failed to create GitHub release: ${ghResult.stderr || ghResult.stdout}`);
|
|
@@ -4609,6 +4755,45 @@ function requireEnv(name) {
|
|
|
4609
4755
|
if (!value) throw new FatalError(`Missing required environment variable: ${name}`);
|
|
4610
4756
|
return value;
|
|
4611
4757
|
}
|
|
4758
|
+
/**
|
|
4759
|
+
* Derive the registry host from a repository URL hostname.
|
|
4760
|
+
* GitHub uses ghcr.io; all other hosts (Forgejo, GitLab, etc.) use the hostname directly.
|
|
4761
|
+
*/
|
|
4762
|
+
function registryHostFromHostname(hostname) {
|
|
4763
|
+
return hostname === "github.com" ? "ghcr.io" : hostname;
|
|
4764
|
+
}
|
|
4765
|
+
/**
|
|
4766
|
+
* Resolve docker registry config using a fallback chain:
|
|
4767
|
+
* 1. Environment variables (DOCKER_REGISTRY_HOST / DOCKER_REGISTRY_NAMESPACE)
|
|
4768
|
+
* 2. .tooling.json registryHost / registryNamespace fields
|
|
4769
|
+
* 3. Derived from package.json repository URL
|
|
4770
|
+
*/
|
|
4771
|
+
function resolveRegistryConfig(cwd) {
|
|
4772
|
+
const envHost = process.env["DOCKER_REGISTRY_HOST"];
|
|
4773
|
+
const envNamespace = process.env["DOCKER_REGISTRY_NAMESPACE"];
|
|
4774
|
+
if (envHost && envNamespace) return {
|
|
4775
|
+
registryHost: envHost,
|
|
4776
|
+
registryNamespace: envNamespace
|
|
4777
|
+
};
|
|
4778
|
+
const toolingConfig = loadToolingConfig(cwd);
|
|
4779
|
+
const toolingHost = toolingConfig?.registryHost;
|
|
4780
|
+
const toolingNamespace = toolingConfig?.registryNamespace;
|
|
4781
|
+
if (toolingHost && toolingNamespace) return {
|
|
4782
|
+
registryHost: envHost ?? toolingHost,
|
|
4783
|
+
registryNamespace: envNamespace ?? toolingNamespace
|
|
4784
|
+
};
|
|
4785
|
+
const parsed = parseRepositoryUrl(cwd);
|
|
4786
|
+
if (parsed) {
|
|
4787
|
+
const derivedHost = registryHostFromHostname(parsed.hostname);
|
|
4788
|
+
const owner = parsed.repository.split("/")[0];
|
|
4789
|
+
const host = envHost ?? toolingHost ?? derivedHost;
|
|
4790
|
+
return {
|
|
4791
|
+
registryHost: host,
|
|
4792
|
+
registryNamespace: envNamespace ?? toolingNamespace ?? `${host}/${owner}`
|
|
4793
|
+
};
|
|
4794
|
+
}
|
|
4795
|
+
throw new FatalError("Could not determine docker registry. Set DOCKER_REGISTRY_HOST and DOCKER_REGISTRY_NAMESPACE, or add registryHost/registryNamespace to .tooling.json, or ensure package.json has a repository URL.");
|
|
4796
|
+
}
|
|
4612
4797
|
const publishDockerCommand = defineCommand({
|
|
4613
4798
|
meta: {
|
|
4614
4799
|
name: "docker:publish",
|
|
@@ -4625,10 +4810,12 @@ const publishDockerCommand = defineCommand({
|
|
|
4625
4810
|
}
|
|
4626
4811
|
},
|
|
4627
4812
|
async run({ args }) {
|
|
4813
|
+
const cwd = process.cwd();
|
|
4814
|
+
const { registryHost, registryNamespace } = resolveRegistryConfig(cwd);
|
|
4628
4815
|
const config = {
|
|
4629
|
-
cwd
|
|
4630
|
-
registryHost
|
|
4631
|
-
registryNamespace
|
|
4816
|
+
cwd,
|
|
4817
|
+
registryHost,
|
|
4818
|
+
registryNamespace,
|
|
4632
4819
|
username: requireEnv("DOCKER_USERNAME"),
|
|
4633
4820
|
password: requireEnv("DOCKER_PASSWORD"),
|
|
4634
4821
|
dryRun: args["dry-run"] === true,
|
|
@@ -4903,8 +5090,8 @@ function toHttpHealthChecks(checks) {
|
|
|
4903
5090
|
}
|
|
4904
5091
|
/** Write the generated check overlay to a temp file. Returns the absolute path. */
|
|
4905
5092
|
function writeTempOverlay(content) {
|
|
4906
|
-
const
|
|
4907
|
-
const filePath = path.join(
|
|
5093
|
+
const dir = mkdtempSync(path.join(tmpdir(), "tooling-check-"));
|
|
5094
|
+
const filePath = path.join(dir, "overlay.yaml");
|
|
4908
5095
|
writeFileSync(filePath, content, "utf-8");
|
|
4909
5096
|
return filePath;
|
|
4910
5097
|
}
|
|
@@ -4980,7 +5167,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
4980
5167
|
if (!result.success) throw new FatalError(`Check failed (${result.reason}): ${result.message}`);
|
|
4981
5168
|
} finally {
|
|
4982
5169
|
try {
|
|
4983
|
-
|
|
5170
|
+
rmSync(path.dirname(tempOverlayPath), { recursive: true });
|
|
4984
5171
|
} catch (_error) {}
|
|
4985
5172
|
}
|
|
4986
5173
|
}
|
|
@@ -4990,7 +5177,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
4990
5177
|
const main = defineCommand({
|
|
4991
5178
|
meta: {
|
|
4992
5179
|
name: "bst",
|
|
4993
|
-
version: "0.
|
|
5180
|
+
version: "0.38.0",
|
|
4994
5181
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
4995
5182
|
},
|
|
4996
5183
|
subCommands: {
|
|
@@ -5006,7 +5193,7 @@ const main = defineCommand({
|
|
|
5006
5193
|
"docker:check": dockerCheckCommand
|
|
5007
5194
|
}
|
|
5008
5195
|
});
|
|
5009
|
-
console.log(`@bensandee/tooling v0.
|
|
5196
|
+
console.log(`@bensandee/tooling v0.38.0`);
|
|
5010
5197
|
async function run() {
|
|
5011
5198
|
await runMain(main);
|
|
5012
5199
|
process.exit(process.exitCode ?? 0);
|