@bensandee/tooling 0.37.0 → 0.39.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/dist/bin.mjs +636 -308
- 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 +11 -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",
|
|
@@ -1424,8 +1478,8 @@ function addReleaseDeps(deps, config) {
|
|
|
1424
1478
|
"@changesets/cli": "2.30.0",
|
|
1425
1479
|
"@release-it/bumper": "7.0.5",
|
|
1426
1480
|
"@types/node": "24.12.0",
|
|
1427
|
-
"commit-and-tag-version": "12.7.
|
|
1428
|
-
"knip": "
|
|
1481
|
+
"commit-and-tag-version": "12.7.1",
|
|
1482
|
+
"knip": "6.0.3",
|
|
1429
1483
|
"lefthook": "2.1.4",
|
|
1430
1484
|
"oxfmt": "0.41.0",
|
|
1431
1485
|
"oxlint": "1.56.0",
|
|
@@ -1440,8 +1494,8 @@ function addReleaseDeps(deps, config) {
|
|
|
1440
1494
|
"@changesets/cli": "2.30.0",
|
|
1441
1495
|
"@release-it/bumper": "7.0.5",
|
|
1442
1496
|
"@types/node": "24.12.0",
|
|
1443
|
-
"commit-and-tag-version": "12.7.
|
|
1444
|
-
"knip": "
|
|
1497
|
+
"commit-and-tag-version": "12.7.1",
|
|
1498
|
+
"knip": "6.0.3",
|
|
1445
1499
|
"lefthook": "2.1.4",
|
|
1446
1500
|
"oxfmt": "0.41.0",
|
|
1447
1501
|
"oxlint": "1.56.0",
|
|
@@ -1458,8 +1512,8 @@ function addReleaseDeps(deps, config) {
|
|
|
1458
1512
|
"@changesets/cli": "2.30.0",
|
|
1459
1513
|
"@release-it/bumper": "7.0.5",
|
|
1460
1514
|
"@types/node": "24.12.0",
|
|
1461
|
-
"commit-and-tag-version": "12.7.
|
|
1462
|
-
"knip": "
|
|
1515
|
+
"commit-and-tag-version": "12.7.1",
|
|
1516
|
+
"knip": "6.0.3",
|
|
1463
1517
|
"lefthook": "2.1.4",
|
|
1464
1518
|
"oxfmt": "0.41.0",
|
|
1465
1519
|
"oxlint": "1.56.0",
|
|
@@ -1476,8 +1530,8 @@ function addReleaseDeps(deps, config) {
|
|
|
1476
1530
|
"@changesets/cli": "2.30.0",
|
|
1477
1531
|
"@release-it/bumper": "7.0.5",
|
|
1478
1532
|
"@types/node": "24.12.0",
|
|
1479
|
-
"commit-and-tag-version": "12.7.
|
|
1480
|
-
"knip": "
|
|
1533
|
+
"commit-and-tag-version": "12.7.1",
|
|
1534
|
+
"knip": "6.0.3",
|
|
1481
1535
|
"lefthook": "2.1.4",
|
|
1482
1536
|
"oxfmt": "0.41.0",
|
|
1483
1537
|
"oxlint": "1.56.0",
|
|
@@ -1509,15 +1563,15 @@ async function generatePackageJson(ctx) {
|
|
|
1509
1563
|
}
|
|
1510
1564
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1511
1565
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1512
|
-
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.
|
|
1513
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1514
|
-
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.3";
|
|
1567
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.39.0";
|
|
1568
|
+
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.11.0";
|
|
1515
1569
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1516
1570
|
"@changesets/cli": "2.30.0",
|
|
1517
1571
|
"@release-it/bumper": "7.0.5",
|
|
1518
1572
|
"@types/node": "24.12.0",
|
|
1519
|
-
"commit-and-tag-version": "12.7.
|
|
1520
|
-
"knip": "
|
|
1573
|
+
"commit-and-tag-version": "12.7.1",
|
|
1574
|
+
"knip": "6.0.3",
|
|
1521
1575
|
"lefthook": "2.1.4",
|
|
1522
1576
|
"oxfmt": "0.41.0",
|
|
1523
1577
|
"oxlint": "1.56.0",
|
|
@@ -1532,8 +1586,8 @@ async function generatePackageJson(ctx) {
|
|
|
1532
1586
|
"@changesets/cli": "2.30.0",
|
|
1533
1587
|
"@release-it/bumper": "7.0.5",
|
|
1534
1588
|
"@types/node": "24.12.0",
|
|
1535
|
-
"commit-and-tag-version": "12.7.
|
|
1536
|
-
"knip": "
|
|
1589
|
+
"commit-and-tag-version": "12.7.1",
|
|
1590
|
+
"knip": "6.0.3",
|
|
1537
1591
|
"lefthook": "2.1.4",
|
|
1538
1592
|
"oxfmt": "0.41.0",
|
|
1539
1593
|
"oxlint": "1.56.0",
|
|
@@ -1616,8 +1670,8 @@ async function generatePackageJson(ctx) {
|
|
|
1616
1670
|
"@changesets/cli": "2.30.0",
|
|
1617
1671
|
"@release-it/bumper": "7.0.5",
|
|
1618
1672
|
"@types/node": "24.12.0",
|
|
1619
|
-
"commit-and-tag-version": "12.7.
|
|
1620
|
-
"knip": "
|
|
1673
|
+
"commit-and-tag-version": "12.7.1",
|
|
1674
|
+
"knip": "6.0.3",
|
|
1621
1675
|
"lefthook": "2.1.4",
|
|
1622
1676
|
"oxfmt": "0.41.0",
|
|
1623
1677
|
"oxlint": "1.56.0",
|
|
@@ -1878,6 +1932,7 @@ import { defineConfig } from "oxlint";
|
|
|
1878
1932
|
|
|
1879
1933
|
export default defineConfig({
|
|
1880
1934
|
extends: [recommended],
|
|
1935
|
+
ignorePatterns: [".claude/hooks/bst-hook.mjs"],
|
|
1881
1936
|
});
|
|
1882
1937
|
`;
|
|
1883
1938
|
const CONFIG_PRESET_ONLY = `import { presetRules } from "@bensandee/config/oxlint";
|
|
@@ -1885,6 +1940,7 @@ import { defineConfig } from "oxlint";
|
|
|
1885
1940
|
|
|
1886
1941
|
export default defineConfig({
|
|
1887
1942
|
rules: presetRules,
|
|
1943
|
+
ignorePatterns: [".claude/hooks/bst-hook.mjs"],
|
|
1888
1944
|
});
|
|
1889
1945
|
`;
|
|
1890
1946
|
async function generateOxlint(ctx) {
|
|
@@ -1912,7 +1968,10 @@ async function generateOxlint(ctx) {
|
|
|
1912
1968
|
}
|
|
1913
1969
|
//#endregion
|
|
1914
1970
|
//#region src/generators/formatter.ts
|
|
1915
|
-
const OXFMT_CONFIG = `{
|
|
1971
|
+
const OXFMT_CONFIG = `{
|
|
1972
|
+
"ignorePatterns": [".claude"]
|
|
1973
|
+
}
|
|
1974
|
+
`;
|
|
1916
1975
|
const PRETTIER_CONFIG = `{
|
|
1917
1976
|
"semi": true,
|
|
1918
1977
|
"singleQuote": false,
|
|
@@ -2001,7 +2060,11 @@ const REQUIRED_ENTRIES = [
|
|
|
2001
2060
|
"!.env.example"
|
|
2002
2061
|
];
|
|
2003
2062
|
/** Tooling-specific entries added during init/update but not required for repo:sync --check. */
|
|
2004
|
-
const OPTIONAL_ENTRIES = [
|
|
2063
|
+
const OPTIONAL_ENTRIES = [
|
|
2064
|
+
".tooling-migrate.md",
|
|
2065
|
+
".tooling-archived/",
|
|
2066
|
+
".claude/hooks/bst-hook.audit.local.jsonl"
|
|
2067
|
+
];
|
|
2005
2068
|
const ALL_ENTRIES = [...REQUIRED_ENTRIES, ...OPTIONAL_ENTRIES];
|
|
2006
2069
|
/** Normalize a gitignore entry for comparison: strip leading `/` and trailing `/`. */
|
|
2007
2070
|
function normalizeEntry(entry) {
|
|
@@ -2160,12 +2223,13 @@ const KNIP_CONFIG_SINGLE = `import type { KnipConfig } from "knip";
|
|
|
2160
2223
|
export default {
|
|
2161
2224
|
entry: ["src/index.ts"],
|
|
2162
2225
|
project: ["src/**/*.ts"],
|
|
2163
|
-
ignore: ["dist/**"],
|
|
2226
|
+
ignore: ["dist/**", ".claude/hooks/**"],
|
|
2164
2227
|
} satisfies KnipConfig;
|
|
2165
2228
|
`;
|
|
2166
2229
|
const KNIP_CONFIG_MONOREPO = `import type { KnipConfig } from "knip";
|
|
2167
2230
|
|
|
2168
2231
|
export default {
|
|
2232
|
+
ignore: [".claude/hooks/**"],
|
|
2169
2233
|
workspaces: {
|
|
2170
2234
|
".": {
|
|
2171
2235
|
entry: [],
|
|
@@ -2296,7 +2360,33 @@ async function generatePnpmWorkspace(ctx) {
|
|
|
2296
2360
|
};
|
|
2297
2361
|
}
|
|
2298
2362
|
//#endregion
|
|
2363
|
+
//#region src/generators/claude-hooks.ts
|
|
2364
|
+
const HOOK_PATH = ".claude/hooks/bst-hook.mjs";
|
|
2365
|
+
async function generateClaudeHooks(ctx) {
|
|
2366
|
+
const content = "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.1.0\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync — do not edit manually.\n */\nimport*as e from\"node:fs\";import{readFileSync as t,readdirSync as n,statSync as r}from\"node:fs\";import*as i from\"node:path\";import{join as a,resolve as o}from\"node:path\";import{fileURLToPath as s}from\"node:url\";Object.freeze({status:`aborted`});function c(e,t,n){function r(n,r){if(n._zod||Object.defineProperty(n,`_zod`,{value:{def:r,constr:o,traits:new Set},enumerable:!1}),n._zod.traits.has(e))return;n._zod.traits.add(e),t(n,r);let i=o.prototype,a=Object.keys(i);for(let e=0;e<a.length;e++){let t=a[e];t in n||(n[t]=i[t].bind(n))}}let i=n?.Parent??Object;class a extends i{}Object.defineProperty(a,`name`,{value:e});function o(e){var t;let i=n?.Parent?new a:this;r(i,e),(t=i._zod).deferred??(t.deferred=[]);for(let e of i._zod.deferred)e();return i}return Object.defineProperty(o,`init`,{value:r}),Object.defineProperty(o,Symbol.hasInstance,{value:t=>n?.Parent&&t instanceof n.Parent?!0:t?._zod?.traits?.has(e)}),Object.defineProperty(o,`name`,{value:e}),o}var l=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const u={};function d(e){return e&&Object.assign(u,e),u}function ee(e,t){return typeof t==`bigint`?t.toString():t}function te(e){return{get value(){{let t=e();return Object.defineProperty(this,`value`,{value:t}),t}throw Error(`cached value already set`)}}}function ne(e){let t=e.startsWith(`^`)?1:0,n=e.endsWith(`$`)?e.length-1:e.length;return e.slice(t,n)}const f=Symbol(`evaluating`);function p(e,t,n){let r;Object.defineProperty(e,t,{get(){if(r!==f)return r===void 0&&(r=f,r=n()),r},set(n){Object.defineProperty(e,t,{value:n})},configurable:!0})}const m=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function re(e){if(h(e)===!1)return!1;let t=e.constructor;if(t===void 0||typeof t!=`function`)return!0;let n=t.prototype;return!(h(n)===!1||Object.prototype.hasOwnProperty.call(n,`isPrototypeOf`)===!1)}function ie(e,t,n){let r=new e._zod.constr(t??e._zod.def);return(!t||n?.parent)&&(r._zod.parent=e),r}function g(e){let t=e;if(!t)return{};if(typeof t==`string`)return{error:()=>t};if(t?.message!==void 0){if(t?.error!==void 0)throw Error(\"Cannot specify both `message` and `error` params\");t.error=t.message}return delete t.message,typeof t.error==`string`?{...t,error:()=>t.error}:t}function _(e){return Object.keys(e).filter(t=>e[t]._zod.optin===`optional`&&e[t]._zod.optout===`optional`)}-Number.MAX_VALUE,Number.MAX_VALUE;function v(e,t=0){if(e.aborted===!0)return!0;for(let n=t;n<e.issues.length;n++)if(e.issues[n]?.continue!==!0)return!0;return!1}function y(e,t){return t.map(t=>{var n;return(n=t).path??(n.path=[]),t.path.unshift(e),t})}function b(e){return typeof e==`string`?e:e?.message}function x(e,t,n){let r={...e,path:e.path??[]};return e.message||(r.message=b(e.inst?._zod.def?.error?.(e))??b(t?.error?.(e))??b(n.customError?.(e))??b(n.localeError?.(e))??`Invalid input`),delete r.inst,delete r.continue,t?.reportInput||delete r.input,r}const S=(e,t)=>{e.name=`$ZodError`,Object.defineProperty(e,`_zod`,{value:e._zod,enumerable:!1}),Object.defineProperty(e,`issues`,{value:t,enumerable:!1}),e.message=JSON.stringify(t,ee,2),Object.defineProperty(e,`toString`,{value:()=>e.message,enumerable:!1})},ae=c(`$ZodError`,S),C=c(`$ZodError`,S,{Parent:Error}),w=(e=>(t,n,r,i)=>{let a=r?Object.assign(r,{async:!1}):{async:!1},o=t._zod.run({value:n,issues:[]},a);if(o instanceof Promise)throw new l;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>x(e,a,d())));throw m(t,i?.callee),t}return o.value})(C),oe=(e=>async(t,n,r,i)=>{let a=r?Object.assign(r,{async:!0}):{async:!0},o=t._zod.run({value:n,issues:[]},a);if(o instanceof Promise&&(o=await o),o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>x(e,a,d())));throw m(t,i?.callee),t}return o.value})(C),T=(e=>(t,n,r)=>{let i=r?{...r,async:!1}:{async:!1},a=t._zod.run({value:n,issues:[]},i);if(a instanceof Promise)throw new l;return a.issues.length?{success:!1,error:new(e??ae)(a.issues.map(e=>x(e,i,d())))}:{success:!0,data:a.value}})(C),E=(e=>async(t,n,r)=>{let i=r?Object.assign(r,{async:!0}):{async:!0},a=t._zod.run({value:n,issues:[]},i);return a instanceof Promise&&(a=await a),a.issues.length?{success:!1,error:new e(a.issues.map(e=>x(e,i,d())))}:{success:!0,data:a.value}})(C),se=e=>{let t=e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??``}}`:`[\\\\s\\\\S]*`;return RegExp(`^${t}$`)},ce=/^-?\\d+(?:\\.\\d+)?$/,le={major:4,minor:3,patch:6},D=c(`$ZodType`,(e,t)=>{var n;e??={},e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=le;let r=[...e._zod.def.checks??[]];e._zod.traits.has(`$ZodCheck`)&&r.unshift(e);for(let t of r)for(let n of t._zod.onattach)n(e);if(r.length===0)(n=e._zod).deferred??(n.deferred=[]),e._zod.deferred?.push(()=>{e._zod.run=e._zod.parse});else{let t=(e,t,n)=>{let r=v(e),i;for(let a of t){if(a._zod.def.when){if(!a._zod.def.when(e))continue}else if(r)continue;let t=e.issues.length,o=a._zod.check(e);if(o instanceof Promise&&n?.async===!1)throw new l;if(i||o instanceof Promise)i=(i??Promise.resolve()).then(async()=>{await o,e.issues.length!==t&&(r||=v(e,t))});else{if(e.issues.length===t)continue;r||=v(e,t)}}return i?i.then(()=>e):e},n=(n,i,a)=>{if(v(n))return n.aborted=!0,n;let o=t(i,r,a);if(o instanceof Promise){if(a.async===!1)throw new l;return o.then(t=>e._zod.parse(t,a))}return e._zod.parse(o,a)};e._zod.run=(i,a)=>{if(a.skipChecks)return e._zod.parse(i,a);if(a.direction===`backward`){let t=e._zod.parse({value:i.value,issues:[]},{...a,skipChecks:!0});return t instanceof Promise?t.then(e=>n(e,i,a)):n(t,i,a)}let o=e._zod.parse(i,a);if(o instanceof Promise){if(a.async===!1)throw new l;return o.then(e=>t(e,r,a))}return t(o,r,a)}}p(e,`~standard`,()=>({validate:t=>{try{let n=T(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return E(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),ue=c(`$ZodString`,(e,t)=>{D.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??se(e._zod.bag),e._zod.parse=(n,r)=>{if(t.coerce)try{n.value=String(n.value)}catch{}return typeof n.value==`string`||n.issues.push({expected:`string`,code:`invalid_type`,input:n.value,inst:e}),n}}),de=c(`$ZodUnknown`,(e,t)=>{D.init(e,t),e._zod.parse=e=>e});function O(e,t,n,r,i){if(e.issues.length){if(i&&!(n in r))return;t.issues.push(...y(n,e.issues))}e.value===void 0?n in r&&(t.value[n]=void 0):t.value[n]=e.value}function fe(e){let t=Object.keys(e.shape);for(let n of t)if(!e.shape?.[n]?._zod?.traits?.has(`$ZodType`))throw Error(`Invalid element at key \"${n}\": expected a Zod schema`);let n=_(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(n)}}function pe(e,t,n,r,i,a){let o=[],s=i.keySet,c=i.catchall._zod,l=c.def.type,u=c.optout===`optional`;for(let i in t){if(s.has(i))continue;if(l===`never`){o.push(i);continue}let a=c.run({value:t[i],issues:[]},r);a instanceof Promise?e.push(a.then(e=>O(e,n,i,t,u))):O(a,n,i,t,u)}return o.length&&n.issues.push({code:`unrecognized_keys`,keys:o,input:t,inst:a}),e.length?Promise.all(e).then(()=>n):n}const me=c(`$ZodObject`,(e,t)=>{if(D.init(e,t),!Object.getOwnPropertyDescriptor(t,`shape`)?.get){let e=t.shape;Object.defineProperty(t,`shape`,{get:()=>{let n={...e};return Object.defineProperty(t,`shape`,{value:n}),n}})}let n=te(()=>fe(t));p(e._zod,`propValues`,()=>{let e=t.shape,n={};for(let t in e){let r=e[t]._zod;if(r.values){n[t]??(n[t]=new Set);for(let e of r.values)n[t].add(e)}}return n});let r=h,i=t.catchall,a;e._zod.parse=(t,o)=>{a??=n.value;let s=t.value;if(!r(s))return t.issues.push({expected:`object`,code:`invalid_type`,input:s,inst:e}),t;t.value={};let c=[],l=a.shape;for(let e of a.keys){let n=l[e],r=n._zod.optout===`optional`,i=n._zod.run({value:s[e],issues:[]},o);i instanceof Promise?c.push(i.then(n=>O(n,t,e,s,r))):O(i,t,e,s,r)}return i?pe(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),he=c(`$ZodRecord`,(e,t)=>{D.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!re(i))return n.issues.push({expected:`record`,code:`invalid_type`,input:i,inst:e}),n;let a=[],o=t.keyType._zod.values;if(o){n.value={};let s=new Set;for(let e of o)if(typeof e==`string`||typeof e==`number`||typeof e==`symbol`){s.add(typeof e==`number`?e.toString():e);let o=t.valueType._zod.run({value:i[e],issues:[]},r);o instanceof Promise?a.push(o.then(t=>{t.issues.length&&n.issues.push(...y(e,t.issues)),n.value[e]=t.value})):(o.issues.length&&n.issues.push(...y(e,o.issues)),n.value[e]=o.value)}let c;for(let e in i)s.has(e)||(c??=[],c.push(e));c&&c.length>0&&n.issues.push({code:`unrecognized_keys`,input:i,inst:e,keys:c})}else{n.value={};for(let o of Reflect.ownKeys(i)){if(o===`__proto__`)continue;let s=t.keyType._zod.run({value:o,issues:[]},r);if(s instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);if(typeof o==`string`&&ce.test(o)&&s.issues.length){let e=t.keyType._zod.run({value:Number(o),issues:[]},r);if(e instanceof Promise)throw Error(`Async schemas not supported in object keys currently`);e.issues.length===0&&(s=e)}if(s.issues.length){t.mode===`loose`?n.value[o]=i[o]:n.issues.push({code:`invalid_key`,origin:`record`,issues:s.issues.map(e=>x(e,r,d())),input:o,path:[o],inst:e});continue}let c=t.valueType._zod.run({value:i[o],issues:[]},r);c instanceof Promise?a.push(c.then(e=>{e.issues.length&&n.issues.push(...y(o,e.issues)),n.value[s.value]=e.value})):(c.issues.length&&n.issues.push(...y(o,c.issues)),n.value[s.value]=c.value)}}return a.length?Promise.all(a).then(()=>n):n}});function k(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const ge=c(`$ZodOptional`,(e,t)=>{D.init(e,t),e._zod.optin=`optional`,e._zod.optout=`optional`,p(e._zod,`values`,()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0),p(e._zod,`pattern`,()=>{let e=t.innerType._zod.pattern;return e?RegExp(`^(${ne(e.source)})?$`):void 0}),e._zod.parse=(e,n)=>{if(t.innerType._zod.optin===`optional`){let r=t.innerType._zod.run(e,n);return r instanceof Promise?r.then(t=>k(t,e.value)):k(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var A,j=class{constructor(){this._map=new WeakMap,this._idmap=new Map}add(e,...t){let n=t[0];return this._map.set(e,n),n&&typeof n==`object`&&`id`in n&&this._idmap.set(n.id,e),this}clear(){return this._map=new WeakMap,this._idmap=new Map,this}remove(e){let t=this._map.get(e);return t&&typeof t==`object`&&`id`in t&&this._idmap.delete(t.id),this._map.delete(e),this}get(e){let t=e._zod.parent;if(t){let n={...this.get(t)??{}};delete n.id;let r={...n,...this._map.get(e)};return Object.keys(r).length?r:void 0}return this._map.get(e)}has(e){return this._map.has(e)}};function M(){return new j}(A=globalThis).__zod_globalRegistry??(A.__zod_globalRegistry=M()),globalThis.__zod_globalRegistry;function N(e,t){return new e({type:`string`,...g(t)})}function P(e){return new e({type:`unknown`})}const F=c(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);D.init(e,t),e.def=t,e.type=t.type,e.parse=(t,n)=>w(e,t,n,{callee:e.parse}),e.safeParse=(t,n)=>T(e,t,n),e.parseAsync=async(t,n)=>oe(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>E(e,t,n),e.check=(...n)=>e.clone({...t,checks:[...t.checks??[],...n.map(e=>typeof e==`function`?{_zod:{check:e,def:{check:`custom`},onattach:[]}}:e)]},{parent:!0}),e.with=e.check,e.clone=(t,n)=>ie(e,t,n),e.brand=()=>e,e.register=((t,n)=>(t.add(e,n),e)),e.apply=t=>t(e)}),I=c(`ZodMiniString`,(e,t)=>{ue.init(e,t),F.init(e,t)});function L(e){return N(I,e)}const _e=c(`ZodMiniUnknown`,(e,t)=>{de.init(e,t),F.init(e,t)});function ve(){return P(_e)}const ye=c(`ZodMiniObject`,(e,t)=>{me.init(e,t),F.init(e,t),p(e,`shape`,()=>t.shape)});function R(e,t){return new ye({type:`object`,shape:e??{},...g(t)})}const be=c(`ZodMiniRecord`,(e,t)=>{he.init(e,t),F.init(e,t)});function xe(e,t,n){return new be({type:`record`,keyType:e,valueType:t,...g(n)})}const Se=c(`ZodMiniOptional`,(e,t)=>{ge.init(e,t),F.init(e,t)});function Ce(e){return new Se({type:`optional`,innerType:e})}const we=R({session_id:L(),transcript_path:L(),cwd:L(),permission_mode:L(),hook_event_name:L(),tool_name:L(),tool_input:R({command:L(),description:Ce(L())})});async function Te(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t=JSON.parse(e.join(``)),n=we.safeParse(t);return n.success||(process.stderr.write(`[bst-hook] invalid hook input: ${n.error.message}\\n`),process.exit(0)),n.data}function z(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function Ee(){let e=i.dirname(s(import.meta.url));return i.join(e,`bst-hook.audit.local.jsonl`)}function De(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let Oe=!1;function ke(t){try{let n=Ee();Oe||=(De(n),!0);let r={timestamp:new Date().toISOString(),...t};e.appendFileSync(n,JSON.stringify(r)+`\n`)}catch(e){process.stderr.write(`[bst-hook] audit write failed: ${e instanceof Error?e.message:String(e)}\\n`)}}function B(e){let t=[],n=``,r=0,i=!1;for(;r<e.length;){let a=e[r];if(a===`'`){for(i=!0,r++;r<e.length&&e[r]!==`'`;)n+=e[r],r++;if(r>=e.length)return{ok:!1,error:`unterminated single quote`};r++}else if(a===`\"`){for(i=!0,r++;r<e.length&&e[r]!==`\"`;){if(e[r]===`\\\\`&&r+1<e.length){let t=e[r+1];if(t===`\"`||t===`\\\\`||t===`$`||t===\"`\"){n+=t,r+=2;continue}}n+=e[r],r++}if(r>=e.length)return{ok:!1,error:`unterminated double quote`};r++}else a===`\\\\`&&r+1<e.length?(i=!0,n+=e[r+1],r+=2):a===` `||a===` `?(i&&=(t.push(n),n=``,!1),r++):(i=!0,n+=a,r++)}return i&&t.push(n),{ok:!0,tokens:t}}function Ae(e){if(!e||!e.trim())return[];let t=[],n=``,r=0,i=!1,a=!1;for(;r<e.length;){let o=e[r];if(o===`'`&&!a){i=!i,n+=o,r++;continue}if(o===`\"`&&!i){a=!a,n+=o,r++;continue}if(o===`\\\\`&&!i&&r+1<e.length){n+=o+e[r+1],r+=2;continue}if(!i&&!a){if(o===`&`&&r+1<e.length&&e[r+1]===`&`){V(t,n,`&&`),n=``,r+=2;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){V(t,n,`||`),n=``,r+=2;continue}if(o===`|`){V(t,n,`|`),n=``,r++;continue}if(o===`;`){V(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function V(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const H=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function je(e,t){if(/[\\n\\r]/.test(t))return{safe:!1,reason:`unquoted newline in command`};for(let t of e){if(t===\"`\")return{safe:!1,reason:`backtick command substitution`};if(t.includes(`$(`))return{safe:!1,reason:`dollar command substitution`};if(t.includes(\"`\"))return{safe:!1,reason:`backtick command substitution`};if(t.includes(`<(`)||t.includes(`>(`)&&!H.has(t))return{safe:!1,reason:`process substitution`}}for(let t=0;t<e.length;t++){let n=e[t];if(!H.has(n)){if(n.includes(`<`)&&!n.includes(`<(`))return{safe:!1,reason:`input redirect`};if(n.includes(`>`)){if(n.endsWith(`>/dev/null`)||n.match(/^(&>>|&>|>>|>|1>|2>)$/)&&e[t+1]===`/dev/null`)continue;return{safe:!1,reason:`output redirect to file`}}}}return{safe:!0}}const Me=new Set([`status`,`log`,`diff`,`show`,`branch`,`remote`,`fetch`,`rev-parse`]),Ne=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]);function Pe(e){for(let t of e.slice(1))if(Ne.has(t)||t.startsWith(`--exec-path=`)||t.startsWith(`--pager=`)||t.startsWith(`--upload-pack=`)||t.startsWith(`--receive-pack=`))return!1;for(let t of e.slice(1))if(!t.startsWith(`-`)){if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return Me.has(t)}return!1}const U=new Set(`install.i.add.remove.rm.uninstall.un.update.up.upgrade.list.ls.ll.la.why.view.info.show.outdated.audit.licenses.init.create.link.unlink.pack.publish.config.set.get.store.cache.rebuild.rb.prune.dedupe.dlx.exec.run.patch.patch-commit.recursive.-r`.split(`.`)),Fe=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function Ie(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!U.has(e))){n=e;break}return n?Fe.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const Le=new Set([`view`,`info`,`show`]);function Re(e){let t=e[1];return t!==void 0&&Le.has(t)}function ze(e){let t=e[1],n=`This project uses pnpm.`;return t?t===`install`||t===`i`||t===`ci`?e.slice(2).some(e=>!e.startsWith(`-`))?`${n} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`:`${n} Use \\`pnpm install\\` instead of npm.`:t===`add`?`${n} Use \\`pnpm add <pkg>\\` instead of npm.`:t===`uninstall`||t===`remove`||t===`rm`||t===`un`||t===`r`?`${n} Use \\`pnpm remove <pkg>\\` instead of npm.`:t===`run`||t===`run-script`?`${n} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`:t===`test`||t===`t`||t===`tst`?`${n} Use \\`pnpm test\\` instead of npm.`:t===`start`?`${n} Use \\`pnpm start\\` instead of npm.`:t===`stop`?`${n} Use \\`pnpm stop\\` instead of npm.`:t===`restart`?`${n} Use \\`pnpm restart\\` instead of npm.`:t===`exec`?`${n} Use \\`pnpm exec <cmd>\\` instead of npm.`:t===`list`||t===`ls`||t===`ll`||t===`la`?`${n} Use \\`pnpm list\\` instead of npm.`:t===`why`?`${n} Use \\`pnpm why\\` instead of npm.`:t===`outdated`?`${n} Use \\`pnpm outdated\\` instead of npm.`:t===`audit`?`${n} Use \\`pnpm audit\\` instead of npm.`:t===`update`||t===`up`||t===`upgrade`?`${n} Use \\`pnpm update\\` instead of npm.`:t===`init`?`${n} Use \\`pnpm init\\` instead of npm.`:t===`publish`?`${n} Use \\`pnpm publish\\` instead of npm.`:t===`link`?`${n} Use \\`pnpm link\\` instead of npm.`:t===`unlink`?`${n} Use \\`pnpm unlink\\` instead of npm.`:t===`pack`?`${n} Use \\`pnpm pack\\` instead of npm.`:t===`cache`?`${n} Use \\`pnpm store\\` instead of \\`npm cache\\`.`:t===`config`||t===`set`||t===`get`?`${n} Use \\`pnpm config\\` instead of npm.`:`${n} Use the equivalent pnpm command instead of npm.`:`${n} Use \\`pnpm\\` instead of npm.`}function W(e,t){for(let n of e.slice(1)){if(n.startsWith(`-`))continue;if(i.basename(n).startsWith(`.`)||n.startsWith(`~/`)||n===`~`)return!1;let e=i.resolve(t.cwd,n);if(e!==t.cwd&&!e.startsWith(t.cwd+i.sep))return!1}return!0}const Be=new Set([`-exec`,`-execdir`,`-ok`,`-delete`]);function Ve(e){return!e.some(e=>Be.has(e))}function He(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function Ue(e){let t=e.findIndex(e=>i.basename(e)===`rm`);if(t===-1)return!1;let n=e.slice(t+1),r=n.filter(e=>e.startsWith(`-`)&&!e.startsWith(`--`)),a=n.filter(e=>e.startsWith(`--`)),o=r.some(e=>/[rR]/.test(e)),s=a.includes(`--recursive`);return o||s}function We(e){return e.some(e=>e===`777`||e===`0777`)}const Ge=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function G(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&Ge.test(e))}function Ke(e){let t=e.indexOf(`push`);if(t===-1)return!1;let n=e.slice(t+1);return n.some(e=>e===`--force`||e===`-f`&&!n.includes(`--force-with-lease`))}function qe(e){if(!(e.includes(`reset`)&&e.includes(`--hard`)))return!1;if(e.some(e=>e===`main`||e===`master`))return!0;let t=e.filter(e=>e!==`git`&&e!==`reset`&&!e.startsWith(`-`));return t.length===0||t.includes(`HEAD`)}const Je=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>Ue(e),reason:\"Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\"},{name:`sudo`,match:/^sudo$/,action:`deny`,reason:`Don't use sudo. If you need elevated permissions, ask me to do it manually.`},{name:`git-force-push`,match:/^git$/,action:`deny`,when:e=>Ke(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>qe(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`chmod-777`,match:/^chmod$/,action:`deny`,when:e=>We(e),reason:`Don't use 777 permissions. Use more restrictive permissions.`},{name:`env-file-cat`,match:/^cat$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-head`,match:/^head$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-tail`,match:/^tail$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-less`,match:/^less$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-more`,match:/^more$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-grep`,match:/^grep$/,action:`deny`,when:e=>G(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`npx-redirect`,match:/^npx$/,action:`deny`,reason:\"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"}],K=[{name:`vitest`,match:/^vitest$/,action:`allow`},{name:`jest`,match:/^jest$/,action:`allow`},{name:`tsc`,match:/^tsc$/,action:`allow`},{name:`tsdown`,match:/^tsdown$/,action:`allow`},{name:`oxlint`,match:/^oxlint$/,action:`allow`},{name:`oxfmt`,match:/^oxfmt$/,action:`allow`},{name:`eslint`,match:/^eslint$/,action:`allow`},{name:`prettier`,match:/^prettier$/,action:`allow`},{name:`biome`,match:/^biome$/,action:`allow`},{name:`knip`,match:/^knip$/,action:`allow`},{name:`cat`,match:/^cat$/,action:`allow`},{name:`head`,match:/^head$/,action:`allow`},{name:`tail`,match:/^tail$/,action:`allow`},{name:`grep`,match:/^grep$/,action:`allow`},{name:`ls`,match:/^ls$/,action:`allow`},{name:`wc`,match:/^wc$/,action:`allow`},{name:`uniq`,match:/^uniq$/,action:`allow`},{name:`diff`,match:/^diff$/,action:`allow`},{name:`echo`,match:/^echo$/,action:`allow`},{name:`pwd`,match:/^pwd$/,action:`allow`},{name:`which`,match:/^which$/,action:`allow`},{name:`dirname`,match:/^dirname$/,action:`allow`},{name:`basename`,match:/^basename$/,action:`allow`},{name:`realpath`,match:/^realpath$/,action:`allow`},{name:`npm`,match:/^npm$/,action:`allow`,when:e=>Re(e)},{name:`cd`,match:/^cd$/,action:`allow`},{name:`true`,match:/^true$/,action:`allow`},{name:`colon`,match:/^:$/,action:`allow`},{name:`find`,match:/^find$/,action:`allow`,when:e=>Ve(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>He(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>Pe(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>Ie(e)},{name:`cp`,match:/^cp$/,action:`allow`,when:(e,t)=>W(e,t)},{name:`mkdir`,match:/^mkdir$/,action:`allow`,when:(e,t)=>W(e,t)},{name:`touch`,match:/^touch$/,action:`allow`,when:(e,t)=>W(e,t)}],Ye=R({scripts:xe(L(),ve())});function q(e,t){let n=et(t);if(!n)return null;let r=n[e];if(typeof r!=`string`)return null;let i=n[`pre${e}`],a=n[`post${e}`];return{pre:typeof i==`string`?i:null,main:r,post:typeof a==`string`?a:null}}function Xe(e,t,n){let r=Ze(t,n);if(!r)return null;let i=q(e,r);return i?{...i,pkgDir:r}:null}function Ze(e,t){let i=Qe(t);if(!i)return null;for(let s of i){if(!s.endsWith(`/*`))continue;let i=o(t,s.slice(0,-2)),c;try{c=n(i)}catch{continue}for(let t of c){let n=a(i,t);try{if(!r(n).isDirectory())continue}catch{continue}let o=J(n);if(o===e||o===`@${e}`||t===e||o&&(o.startsWith(`@`)?o.split(`/`)[1]:o)===e)return n}}return null}function Qe(e){try{let n=t(a(e,`pnpm-workspace.yaml`),`utf-8`).split(`\n`),r=n.findIndex(e=>/^packages\\s*:/.test(e));if(r===-1)return null;let i=[];for(let e=r+1;e<n.length;e++){let t=n[e],r=/^\\s+-\\s+(.+)$/.exec(t);if(!r)break;i.push(r[1].trim().replace(/['\"`]/g,``))}return i.length>0?i:null}catch{return null}}const $e=R({name:L()});function J(e){try{let n=t(a(e,`package.json`),`utf-8`),r=$e.safeParse(JSON.parse(n));return r.success?r.data.name:null}catch{return null}}function et(e){try{let n=t(a(e,`package.json`),`utf-8`),r=Ye.safeParse(JSON.parse(n));return r.success?r.data.scripts:null}catch{return null}}const tt=new Set([`LD_PRELOAD`,`LD_LIBRARY_PATH`,`DYLD_INSERT_LIBRARIES`,`NODE_OPTIONS`,`PYTHONPATH`,`PYTHONSTARTUP`,`PERL5OPT`,`PERL5LIB`,`RUBYOPT`,`RUBYLIB`,`BASH_ENV`,`ENV`,`npm_config_prefix`,`npm_config_registry`,`EDITOR`,`VISUAL`]),nt=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function Y(e){return tt.has(e)?!0:nt.some(t=>e.startsWith(t))}function rt(e,t){let n=B(e);if(!n.ok||n.tokens.length!==2)return null;let r=n.tokens;if(r[0]!==`cd`)return null;let a=r[1];return i.isAbsolute(a)?a:i.resolve(t.cwd,a)}function X(e,t){if(t.depth>=5)return{action:`passthrough`,rule:null,reason:`max evaluation depth exceeded`};let n=e.trim();if(!n)return{action:`passthrough`,rule:null,reason:null};if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=Ae(n);if(r.length===0)return{action:`passthrough`,rule:null,reason:null};let i=null,a=null,o=t;for(let e of r){if(e.operator===`&&`){let t=rt(e.command,o);t&&(o={...o,cwd:t})}let t=it(e.command,o);if(t.action===`deny`)return t;t.action===`passthrough`&&(i||=t),t.action===`allow`&&(a=t)}return i||(r.length===1&&a?a:{action:`allow`,rule:`all-segments`,reason:null})}function it(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=B(e);if(!r.ok)return{action:`passthrough`,rule:null,reason:r.error};let a=r.tokens;if(a.length===0)return{action:`passthrough`,rule:null,reason:null};let o=0;for(;o<a.length&&/^\\w+=/.test(a[o]);){let e=a[o].split(`=`)[0];if(Y(e))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${e}`};o++}if(o>=a.length)return{action:`allow`,rule:`env-only`,reason:null};a=a.slice(o);let s=je(a,e);if(!s.safe)return{action:`passthrough`,rule:null,reason:s.reason};for(let e=0;e<a.length;e++){let t=a[e];if(!(t===`2>&1`||t===`2>/dev/null`||t===`>/dev/null`||t===`1>/dev/null`)){if(t===`>`||t===`>>`){if(a[e+1]===`/dev/null`)continue;return{action:`passthrough`,rule:null,reason:`output redirect to file`}}if(t===`<`)return{action:`passthrough`,rule:null,reason:`input redirect`}}}let c=i.basename(a[0]);if(c===`command`&&a.length>1)return X(a.slice(1).join(` `),{...t,depth:t.depth+1});if(c===`env`){let e=1;for(;e<a.length&&/^\\w+=/.test(a[e]);){let t=a[e].split(`=`)[0];if(Y(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=a.length?{action:`allow`,rule:`env-only`,reason:null}:X(a.slice(e).join(` `),{...t,depth:t.depth+1})}if(c===`pnpm`)return at(a,t);if(c===`npm`)return ot(a);if(c===`bash`||c===`sh`||c===`zsh`)return lt(a,t);if(c===`node`&&(a.includes(`-e`)||a.includes(`-p`)||a.includes(`--eval`)||a.includes(`--print`)))return{action:`passthrough`,rule:null,reason:`node eval`};if(c===`mv`)return{action:`passthrough`,rule:null,reason:`mv`};for(let e of Je)if(e.match.test(c)&&(!e.when||e.when(a,t)))return{action:`deny`,rule:e.name,reason:e.reason??null};for(let e of K)if(e.match.test(c)&&(!e.when||e.when(a,t)))return{action:`allow`,rule:e.name,reason:null};return{action:`passthrough`,rule:null,reason:null}}function at(e,t){let n=e.slice(1),r,i=-1;for(let a=0;a<n.length;a++){let o=n[a];if(o===`--filter`||o.startsWith(`--filter=`))return st(e,n,a,t);if(o===`-r`||o===`--recursive`)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};if(!(o.startsWith(`-`)&&!U.has(o))){r=o,i=a;break}}if(!r)return{action:`passthrough`,rule:null,reason:null};if(r===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(r===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(r===`exec`){let e=n.slice(i+1);return e.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:X(e.join(` `),{...t,depth:t.depth+1})}if(r===`run`){let e=n[i+1];return!e||e.startsWith(`-`)?{action:`passthrough`,rule:null,reason:`pnpm run without script name`}:Z(e,t)}if(U.has(r)){for(let n of K)if(n.match.test(`pnpm`)&&(!n.when||n.when(e,t)))return{action:`allow`,rule:n.name,reason:null};return{action:`passthrough`,rule:null,reason:null}}return Z(r,t)}function Z(e,t){let n=q(e,t.cwd);if(!n)return{action:`passthrough`,rule:null,reason:`script resolution failed`};let r={...t,depth:t.depth+1};if(n.pre){let e=X(n.pre,r);if(e.action!==`allow`)return e}let i=X(n.main,r);if(i.action!==`allow`)return i;if(n.post){let e=X(n.post,r);if(e.action!==`allow`)return e}return{action:`allow`,rule:`pnpm-script:${e}`,reason:null}}function ot(e){for(let t of K)if(t.match.test(`npm`)&&(!t.when||t.when(e,{cwd:``,depth:0})))return{action:`allow`,rule:t.name,reason:null};return{action:`deny`,rule:`npm-redirect`,reason:ze(e)}}function st(e,t,n,r){let i=t[n],a,o;if(i.startsWith(`--filter=`))a=i.slice(9),o=n+1;else{if(a=t[n+1],!a||a.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm --filter without package`};o=n+2}if(!a)return{action:`passthrough`,rule:null,reason:`pnpm --filter without package`};let s;for(let e=o;e<t.length;e++){let n=t[e];if(!(n.startsWith(`-`)&&!U.has(n))){s=n;break}}if(!s)return{action:`passthrough`,rule:null,reason:`pnpm --filter without subcommand`};if(s===`install`||s===`i`)return t.slice(t.indexOf(s)+1).every(e=>e.startsWith(`-`))?{action:`allow`,rule:`pnpm`,reason:null}:{action:`passthrough`,rule:null,reason:`pnpm --filter install with package arg`};if(s===`dlx`)return{action:`passthrough`,rule:null,reason:`pnpm dlx`};if(s===`add`)return{action:`passthrough`,rule:null,reason:`pnpm add`};if(new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`,`licenses`,`audit`]).has(s))return{action:`allow`,rule:`pnpm`,reason:null};if(s===`exec`){let e=t.indexOf(`exec`,o),n=t.slice(e+1);return n.length===0?{action:`passthrough`,rule:null,reason:`bare pnpm exec`}:X(n.join(` `),{...r,depth:r.depth+1})}let c=s===`run`?t[t.indexOf(`run`,o)+1]:s;if(!c||c.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm --filter run without script name`};if(U.has(c)&&c!==`run`)return{action:`passthrough`,rule:null,reason:null};let l=Xe(c,a,r.cwd);if(!l)return{action:`passthrough`,rule:null,reason:`filtered script resolution failed`};let u={...r,depth:r.depth+1,cwd:l.pkgDir??r.cwd};if(l.pre){let e=X(l.pre,u);if(e.action!==`allow`)return e}let d=X(l.main,u);if(d.action!==`allow`)return d;if(l.post){let e=X(l.post,u);if(e.action!==`allow`)return e}return{action:`allow`,rule:`pnpm-filter:${a}:${c}`,reason:null}}const ct=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function lt(e,t){let n=e.indexOf(`-c`);if(n===-1)return{action:`passthrough`,rule:null,reason:`shell without -c`};for(let t=1;t<e.length;t++){if(t===n||t===n+1)continue;let r=e[t];if(r.startsWith(`-`)||ct.has(r))return{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}}let r=e[n+1];return r?e.length>n+2?{action:`passthrough`,rule:null,reason:`shell -c with positional args`}:n===1?X(r,{...t,depth:t.depth+1}):{action:`passthrough`,rule:null,reason:`shell -c with extra flags`}:{action:`passthrough`,rule:null,reason:`shell -c without command`}}const Q=await Te();(Q.hook_event_name!==`PreToolUse`||Q.tool_name!==`Bash`)&&process.exit(0);const $=X(Q.tool_input.command,{cwd:Q.cwd,depth:0});ke({sessionId:Q.session_id,command:Q.tool_input.command,decision:$.action,rule:$.rule,reason:$.reason}),$.action===`allow`?z(`allow`):$.action===`deny`&&z(`deny`,$.reason);export{};";
|
|
2367
|
+
const existing = ctx.read(HOOK_PATH);
|
|
2368
|
+
if (existing === content) return {
|
|
2369
|
+
filePath: HOOK_PATH,
|
|
2370
|
+
action: "skipped",
|
|
2371
|
+
description: "Hook is up to date"
|
|
2372
|
+
};
|
|
2373
|
+
ctx.write(HOOK_PATH, content);
|
|
2374
|
+
return {
|
|
2375
|
+
filePath: HOOK_PATH,
|
|
2376
|
+
action: existing ? "updated" : "created",
|
|
2377
|
+
description: existing ? "Updated bst-hook.mjs" : "Generated bst-hook.mjs"
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
//#endregion
|
|
2299
2381
|
//#region src/generators/claude-settings.ts
|
|
2382
|
+
const HookEntrySchema = z.object({
|
|
2383
|
+
type: z.string(),
|
|
2384
|
+
command: z.string()
|
|
2385
|
+
});
|
|
2386
|
+
const HookMatcherSchema = z.object({
|
|
2387
|
+
matcher: z.string(),
|
|
2388
|
+
hooks: z.array(HookEntrySchema).default([])
|
|
2389
|
+
});
|
|
2300
2390
|
const ClaudeSettingsSchema = z.object({
|
|
2301
2391
|
permissions: z.object({
|
|
2302
2392
|
allow: z.array(z.string()).default([]),
|
|
@@ -2305,6 +2395,7 @@ const ClaudeSettingsSchema = z.object({
|
|
|
2305
2395
|
allow: [],
|
|
2306
2396
|
deny: []
|
|
2307
2397
|
}),
|
|
2398
|
+
hooks: z.record(z.string(), z.array(HookMatcherSchema)).default({}),
|
|
2308
2399
|
instructions: z.array(z.string()).default([]),
|
|
2309
2400
|
enabledPlugins: z.record(z.string(), z.boolean()).default({}),
|
|
2310
2401
|
extraKnownMarketplaces: z.record(z.string(), z.record(z.string(), z.unknown())).default({})
|
|
@@ -2326,64 +2417,11 @@ function parseClaudeSettings(raw) {
|
|
|
2326
2417
|
function buildSettings(ctx) {
|
|
2327
2418
|
const pm = "pnpm";
|
|
2328
2419
|
const allow = [
|
|
2329
|
-
`Bash(${pm} install)`,
|
|
2330
|
-
`Bash(${pm} install *)`,
|
|
2331
|
-
`Bash(${pm} add *)`,
|
|
2332
|
-
`Bash(${pm} update *)`,
|
|
2333
|
-
`Bash(${pm} view *)`,
|
|
2334
|
-
`Bash(${pm} why *)`,
|
|
2335
|
-
`Bash(${pm} list)`,
|
|
2336
|
-
`Bash(${pm} list *)`,
|
|
2337
|
-
`Bash(${pm} ls)`,
|
|
2338
|
-
`Bash(${pm} ls *)`,
|
|
2339
|
-
"Bash(npm view *)",
|
|
2340
|
-
"Bash(npm info *)",
|
|
2341
|
-
"Bash(npm show *)",
|
|
2342
|
-
`Bash(${pm} build)`,
|
|
2343
|
-
`Bash(${pm} -r build)`,
|
|
2344
|
-
`Bash(${pm} dev)`,
|
|
2345
|
-
`Bash(${pm} test)`,
|
|
2346
|
-
`Bash(${pm} test *)`,
|
|
2347
|
-
`Bash(${pm} -r test)`,
|
|
2348
|
-
`Bash(${pm} vitest)`,
|
|
2349
|
-
`Bash(${pm} vitest *)`,
|
|
2350
|
-
`Bash(${pm} exec vitest)`,
|
|
2351
|
-
`Bash(${pm} exec vitest *)`,
|
|
2352
|
-
`Bash(${pm} exec oxfmt)`,
|
|
2353
|
-
`Bash(${pm} exec oxfmt *)`,
|
|
2354
|
-
`Bash(${pm} typecheck)`,
|
|
2355
|
-
`Bash(${pm} -r --parallel run typecheck)`,
|
|
2356
|
-
`Bash(${pm} tsc *)`,
|
|
2357
|
-
`Bash(${pm} exec tsc *)`,
|
|
2358
|
-
`Bash(${pm} lint)`,
|
|
2359
|
-
`Bash(${pm} lint *)`,
|
|
2360
|
-
`Bash(${pm} format)`,
|
|
2361
|
-
`Bash(${pm} format *)`,
|
|
2362
|
-
`Bash(${pm} knip)`,
|
|
2363
|
-
`Bash(${pm} knip *)`,
|
|
2364
|
-
`Bash(${pm} check)`,
|
|
2365
|
-
`Bash(${pm} ci:check)`,
|
|
2366
|
-
`Bash(${pm} exec oxlint *)`,
|
|
2367
|
-
`Bash(${pm} exec knip *)`,
|
|
2368
|
-
"Bash(git status *)",
|
|
2369
|
-
"Bash(git log *)",
|
|
2370
|
-
"Bash(git diff *)",
|
|
2371
|
-
"Bash(git branch *)",
|
|
2372
|
-
"Bash(git show *)",
|
|
2373
|
-
"Bash(git rev-parse *)",
|
|
2374
|
-
"Bash(ls *)",
|
|
2375
|
-
"Bash(cat *)",
|
|
2376
|
-
"Bash(head *)",
|
|
2377
|
-
"Bash(tail *)",
|
|
2378
|
-
"Bash(wc *)",
|
|
2379
2420
|
"Bash(test *)",
|
|
2380
2421
|
"Bash([ *)",
|
|
2381
|
-
"Bash(
|
|
2382
|
-
"Bash(which *)",
|
|
2383
|
-
"Bash(node -e *)",
|
|
2384
|
-
"Bash(node -p *)"
|
|
2422
|
+
"Bash(git -C * diff HEAD -- *)"
|
|
2385
2423
|
];
|
|
2386
|
-
if (ctx.config.structure === "monorepo") allow.push(`Bash(${pm}
|
|
2424
|
+
if (ctx.config.structure === "monorepo") allow.push(`Bash(${pm} -r *)`);
|
|
2387
2425
|
const enabledPlugins = { "code-simplifier@claude-plugins-official": true };
|
|
2388
2426
|
const extraKnownMarketplaces = {};
|
|
2389
2427
|
if (ctx.config.structure !== "monorepo") {
|
|
@@ -2393,7 +2431,6 @@ function buildSettings(ctx) {
|
|
|
2393
2431
|
permissions: {
|
|
2394
2432
|
allow,
|
|
2395
2433
|
deny: [
|
|
2396
|
-
"Bash(npx *)",
|
|
2397
2434
|
"Bash(git push *)",
|
|
2398
2435
|
"Bash(git push)",
|
|
2399
2436
|
"Bash(git add *)",
|
|
@@ -2412,35 +2449,19 @@ function buildSettings(ctx) {
|
|
|
2412
2449
|
"Bash(git clean *)",
|
|
2413
2450
|
"Bash(git rm *)",
|
|
2414
2451
|
"Bash(git mv *)",
|
|
2415
|
-
"Bash(rm -rf *)",
|
|
2416
|
-
"Bash(cat *.env)",
|
|
2417
|
-
"Bash(cat *.env.*)",
|
|
2418
|
-
"Bash(cat .env)",
|
|
2419
|
-
"Bash(cat .env.*)",
|
|
2420
|
-
"Bash(head *.env)",
|
|
2421
|
-
"Bash(head *.env.*)",
|
|
2422
|
-
"Bash(head .env)",
|
|
2423
|
-
"Bash(head .env.*)",
|
|
2424
|
-
"Bash(tail *.env)",
|
|
2425
|
-
"Bash(tail *.env.*)",
|
|
2426
|
-
"Bash(tail .env)",
|
|
2427
|
-
"Bash(tail .env.*)",
|
|
2428
|
-
"Bash(less *.env)",
|
|
2429
|
-
"Bash(less *.env.*)",
|
|
2430
|
-
"Bash(less .env)",
|
|
2431
|
-
"Bash(less .env.*)",
|
|
2432
|
-
"Bash(more *.env)",
|
|
2433
|
-
"Bash(more *.env.*)",
|
|
2434
|
-
"Bash(more .env)",
|
|
2435
|
-
"Bash(more .env.*)",
|
|
2436
|
-
"Bash(grep * .env)",
|
|
2437
|
-
"Bash(grep * .env.*)",
|
|
2438
2452
|
"Read(.env)",
|
|
2439
2453
|
"Read(.env.*)",
|
|
2440
2454
|
"Read(*.env)",
|
|
2441
2455
|
"Read(*.env.*)"
|
|
2442
2456
|
]
|
|
2443
2457
|
},
|
|
2458
|
+
hooks: { PreToolUse: [{
|
|
2459
|
+
matcher: "Bash",
|
|
2460
|
+
hooks: [{
|
|
2461
|
+
type: "command",
|
|
2462
|
+
command: `node "$CLAUDE_PROJECT_DIR/.claude/hooks/bst-hook.mjs"`
|
|
2463
|
+
}]
|
|
2464
|
+
}] },
|
|
2444
2465
|
instructions: [
|
|
2445
2466
|
"Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
|
|
2446
2467
|
"No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
|
|
@@ -2451,10 +2472,16 @@ function buildSettings(ctx) {
|
|
|
2451
2472
|
extraKnownMarketplaces
|
|
2452
2473
|
};
|
|
2453
2474
|
}
|
|
2454
|
-
|
|
2475
|
+
const BST_HOOK_MARKER = "bst-hook.mjs";
|
|
2476
|
+
/** Check if a hook entry list already contains the bst-hook command. */
|
|
2477
|
+
function hasBstHook(matchers) {
|
|
2478
|
+
return matchers.some((m) => m.matcher === "Bash" && m.hooks.some((h) => h.command.includes(BST_HOOK_MARKER)));
|
|
2479
|
+
}
|
|
2480
|
+
/** Remove enabledPlugins/extraKnownMarketplaces/hooks when empty to keep the file clean. */
|
|
2455
2481
|
function serializeSettings$1(settings) {
|
|
2456
|
-
const { enabledPlugins, extraKnownMarketplaces, ...rest } = settings;
|
|
2482
|
+
const { enabledPlugins, extraKnownMarketplaces, hooks, ...rest } = settings;
|
|
2457
2483
|
const out = { ...rest };
|
|
2484
|
+
if (Object.keys(hooks).length > 0) out["hooks"] = hooks;
|
|
2458
2485
|
if (Object.keys(enabledPlugins).length > 0) out["enabledPlugins"] = enabledPlugins;
|
|
2459
2486
|
if (Object.keys(extraKnownMarketplaces).length > 0) out["extraKnownMarketplaces"] = extraKnownMarketplaces;
|
|
2460
2487
|
return JSON.stringify(out, null, 2) + "\n";
|
|
@@ -2480,9 +2507,18 @@ function writeOrMergeSettings(ctx, filePath, generated) {
|
|
|
2480
2507
|
else mergedInstructions.push(inst);
|
|
2481
2508
|
instructionChanges++;
|
|
2482
2509
|
}
|
|
2510
|
+
let hookChanges = 0;
|
|
2511
|
+
const mergedHooks = { ...settings.hooks };
|
|
2512
|
+
for (const [event, generatedMatchers] of Object.entries(generated.hooks)) {
|
|
2513
|
+
const existingMatchers = mergedHooks[event] ?? [];
|
|
2514
|
+
if (!hasBstHook(existingMatchers)) {
|
|
2515
|
+
mergedHooks[event] = [...existingMatchers, ...generatedMatchers];
|
|
2516
|
+
hookChanges++;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2483
2519
|
const missingPlugins = Object.entries(generated.enabledPlugins).filter(([key]) => !(key in settings.enabledPlugins));
|
|
2484
2520
|
const missingMarketplaces = Object.entries(generated.extraKnownMarketplaces).filter(([key]) => !(key in settings.extraKnownMarketplaces));
|
|
2485
|
-
const changed = missingAllow.length + missingDeny.length + instructionChanges + missingPlugins.length + missingMarketplaces.length;
|
|
2521
|
+
const changed = missingAllow.length + missingDeny.length + hookChanges + instructionChanges + missingPlugins.length + missingMarketplaces.length;
|
|
2486
2522
|
if (changed === 0) return {
|
|
2487
2523
|
filePath,
|
|
2488
2524
|
action: "skipped",
|
|
@@ -2492,6 +2528,7 @@ function writeOrMergeSettings(ctx, filePath, generated) {
|
|
|
2492
2528
|
allow: [...settings.permissions.allow, ...missingAllow],
|
|
2493
2529
|
deny: [...settings.permissions.deny, ...missingDeny]
|
|
2494
2530
|
};
|
|
2531
|
+
if (Object.keys(mergedHooks).length > 0) rawJson["hooks"] = mergedHooks;
|
|
2495
2532
|
rawJson["instructions"] = mergedInstructions;
|
|
2496
2533
|
const updatedPlugins = { ...settings.enabledPlugins };
|
|
2497
2534
|
for (const [key, value] of missingPlugins) updatedPlugins[key] = value;
|
|
@@ -2527,6 +2564,7 @@ async function generateClaudeSettings(ctx) {
|
|
|
2527
2564
|
allow: [],
|
|
2528
2565
|
deny: []
|
|
2529
2566
|
},
|
|
2567
|
+
hooks: {},
|
|
2530
2568
|
instructions: [],
|
|
2531
2569
|
enabledPlugins: { "frontend-design@claude-plugins-official": true },
|
|
2532
2570
|
extraKnownMarketplaces: {}
|
|
@@ -2690,7 +2728,19 @@ function simpleReleaseCommand() {
|
|
|
2690
2728
|
"pnpm exec bst release:simple $FLAGS"
|
|
2691
2729
|
].join("\n");
|
|
2692
2730
|
}
|
|
2693
|
-
|
|
2731
|
+
/** Build the docker publish env block, omitting registry vars when derivable at runtime. */
|
|
2732
|
+
function dockerPublishEnv(registryDerivable) {
|
|
2733
|
+
return {
|
|
2734
|
+
...registryDerivable ? {} : {
|
|
2735
|
+
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
2736
|
+
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
2737
|
+
},
|
|
2738
|
+
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
2739
|
+
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
2740
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
|
|
2694
2744
|
const releaseStep = { step: {
|
|
2695
2745
|
name: "Release",
|
|
2696
2746
|
env: {
|
|
@@ -2706,13 +2756,7 @@ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
|
2706
2756
|
const dockerStep = { step: {
|
|
2707
2757
|
name: "Publish Docker images",
|
|
2708
2758
|
if: "success()",
|
|
2709
|
-
env:
|
|
2710
|
-
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
2711
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE"),
|
|
2712
|
-
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
2713
|
-
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
2714
|
-
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2715
|
-
},
|
|
2759
|
+
env: dockerPublishEnv(registryDerivable),
|
|
2716
2760
|
run: "pnpm exec bst docker:publish"
|
|
2717
2761
|
} };
|
|
2718
2762
|
return [
|
|
@@ -2721,10 +2765,10 @@ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
|
2721
2765
|
...hasDocker ? [dockerStep] : []
|
|
2722
2766
|
];
|
|
2723
2767
|
}
|
|
2724
|
-
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
2768
|
+
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
|
|
2725
2769
|
switch (strategy) {
|
|
2726
2770
|
case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
|
|
2727
|
-
case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker);
|
|
2771
|
+
case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
|
|
2728
2772
|
default: return null;
|
|
2729
2773
|
}
|
|
2730
2774
|
}
|
|
@@ -2745,7 +2789,8 @@ async function generateReleaseCi(ctx) {
|
|
|
2745
2789
|
const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
|
|
2746
2790
|
const nodeVersionYaml = computeNodeVersionYaml(ctx);
|
|
2747
2791
|
const hasDocker = hasDockerPackages(ctx);
|
|
2748
|
-
const
|
|
2792
|
+
const registryDerivable = hasRepositoryUrl(ctx);
|
|
2793
|
+
const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
|
|
2749
2794
|
if (!steps) return {
|
|
2750
2795
|
filePath,
|
|
2751
2796
|
action: "skipped",
|
|
@@ -3120,6 +3165,7 @@ async function runGenerators(ctx) {
|
|
|
3120
3165
|
results.push(await generateKnip(ctx));
|
|
3121
3166
|
results.push(await generateRenovate(ctx));
|
|
3122
3167
|
results.push(await generateCi(ctx));
|
|
3168
|
+
results.push(await generateClaudeHooks(ctx));
|
|
3123
3169
|
results.push(...await generateClaudeSettings(ctx));
|
|
3124
3170
|
results.push(await generateReleaseIt(ctx));
|
|
3125
3171
|
results.push(await generateVersionrc(ctx));
|
|
@@ -3140,112 +3186,116 @@ async function runGenerators(ctx) {
|
|
|
3140
3186
|
function generateMigratePrompt(results, config, detected) {
|
|
3141
3187
|
const sections = [];
|
|
3142
3188
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3143
|
-
sections.push(
|
|
3144
|
-
sections.push("");
|
|
3145
|
-
sections.push(`_Generated by \`@bensandee/tooling@0.37.0 repo:sync\` on ${timestamp}_`);
|
|
3146
|
-
sections.push("");
|
|
3189
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.39.0 repo:sync\` on ${timestamp}_\n`);
|
|
3147
3190
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3148
3191
|
if (changed.length > 0) {
|
|
3149
|
-
|
|
3150
|
-
sections.push("");
|
|
3151
|
-
for (const r of changed) {
|
|
3192
|
+
const items = changed.map((r) => {
|
|
3152
3193
|
const label = r.action === "archived" ? "archived" : r.action;
|
|
3153
|
-
|
|
3154
|
-
}
|
|
3155
|
-
sections.push(
|
|
3194
|
+
return `- \`${r.filePath}\` (${label}) — ${r.description}`;
|
|
3195
|
+
}).join("\n");
|
|
3196
|
+
sections.push(`## Files changed\n\n${items}\n`);
|
|
3156
3197
|
}
|
|
3157
3198
|
const tasks = [];
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3199
|
+
addLegacyToolingTasks(tasks, config, detected);
|
|
3200
|
+
addArchivedFileTasks(tasks, results);
|
|
3201
|
+
addOxlintTasks(tasks, results, detected);
|
|
3202
|
+
addMonorepoTasks(tasks, config, detected);
|
|
3203
|
+
addTestGenerationTasks(tasks, results);
|
|
3204
|
+
if (tasks.length > 0) {
|
|
3205
|
+
sections.push(`## Migration tasks\n\n${tasks.join("\n")}`);
|
|
3206
|
+
sections.push(`## Ground rules
|
|
3207
|
+
|
|
3208
|
+
Prefer fixing root causes over suppressing issues:
|
|
3209
|
+
|
|
3210
|
+
- **Lint errors**: fix the code, not disable comments
|
|
3211
|
+
- **Type errors**: add proper types, not \`any\` or \`@ts-expect-error\`
|
|
3212
|
+
- **Test failures**: fix the test or bug, don't skip/delete
|
|
3213
|
+
- **Knip findings**: remove unused code, don't add ignores
|
|
3214
|
+
|
|
3215
|
+
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.
|
|
3216
|
+
`);
|
|
3176
3217
|
}
|
|
3218
|
+
sections.push(`## Verify
|
|
3219
|
+
|
|
3220
|
+
Run \`pnpm check\` and fix any errors. This runs the full pipeline:
|
|
3221
|
+
build → typecheck → lint → test → format → knip
|
|
3222
|
+
|
|
3223
|
+
When everything passes, run \`/init\` to generate a \`CLAUDE.md\` for this repository.
|
|
3224
|
+
`);
|
|
3225
|
+
return sections.join("\n");
|
|
3226
|
+
}
|
|
3227
|
+
const LEGACY_REPLACEMENTS = {
|
|
3228
|
+
eslint: "oxlint",
|
|
3229
|
+
prettier: "oxfmt",
|
|
3230
|
+
jest: "vitest",
|
|
3231
|
+
webpack: "tsdown",
|
|
3232
|
+
rollup: "tsdown"
|
|
3233
|
+
};
|
|
3234
|
+
function addLegacyToolingTasks(tasks, config, detected) {
|
|
3235
|
+
const legacyToRemove = detected.legacyConfigs.filter((legacy) => !(legacy.tool === "prettier" && config.formatter === "prettier"));
|
|
3236
|
+
if (legacyToRemove.length === 0) return;
|
|
3237
|
+
const items = legacyToRemove.map((legacy) => {
|
|
3238
|
+
const replacement = LEGACY_REPLACEMENTS[legacy.tool];
|
|
3239
|
+
const files = legacy.files.map((f) => `\`${f}\``).join(", ");
|
|
3240
|
+
const lines = [`- Remove ${legacy.tool} config files (${files}). This project now uses **${replacement}**.`, ` - Uninstall ${legacy.tool}-related packages from devDependencies`];
|
|
3241
|
+
if (legacy.tool === "eslint") lines.push(" - Migrate any custom ESLint rules that don't have oxlint equivalents");
|
|
3242
|
+
if (legacy.tool === "jest") lines.push(" - Migrate jest-specific test utilities (jest.mock, jest.fn) to vitest equivalents (vi.mock, vi.fn)");
|
|
3243
|
+
return lines.join("\n");
|
|
3244
|
+
});
|
|
3245
|
+
tasks.push(`### Remove legacy tooling\n\n${items.join("\n")}\n`);
|
|
3246
|
+
}
|
|
3247
|
+
function addArchivedFileTasks(tasks, results) {
|
|
3177
3248
|
const archived = results.filter((r) => r.action === "archived");
|
|
3178
|
-
if (archived.length
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3249
|
+
if (archived.length === 0) return;
|
|
3250
|
+
const items = archived.map((r) => `- \`${r.filePath}\` → \`.tooling-archived/${r.filePath}\``).join("\n");
|
|
3251
|
+
tasks.push(`### Review archived files
|
|
3252
|
+
|
|
3253
|
+
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:
|
|
3254
|
+
|
|
3255
|
+
${items}
|
|
3256
|
+
|
|
3257
|
+
Delete \`.tooling-archived/\` when done.
|
|
3258
|
+
`);
|
|
3259
|
+
}
|
|
3260
|
+
function addOxlintTasks(tasks, results, detected) {
|
|
3189
3261
|
if (detected.hasLegacyOxlintJson) {
|
|
3190
|
-
tasks.push(
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
tasks.push("### Verify oxlint.config.ts extends recommended rules");
|
|
3196
|
-
tasks.push("");
|
|
3197
|
-
tasks.push("Ensure `oxlint.config.ts` imports and extends `@bensandee/config/oxlint/recommended`:");
|
|
3198
|
-
tasks.push("");
|
|
3199
|
-
tasks.push("```ts");
|
|
3200
|
-
tasks.push("import recommended from \"@bensandee/config/oxlint/recommended\";");
|
|
3201
|
-
tasks.push("import { defineConfig } from \"oxlint\";");
|
|
3202
|
-
tasks.push("");
|
|
3203
|
-
tasks.push("export default defineConfig({ extends: [recommended] });");
|
|
3204
|
-
tasks.push("```");
|
|
3205
|
-
tasks.push("");
|
|
3206
|
-
}
|
|
3207
|
-
if (config.structure === "monorepo" && !detected.hasPnpmWorkspace) {
|
|
3208
|
-
tasks.push("### Migrate to monorepo structure");
|
|
3209
|
-
tasks.push("");
|
|
3210
|
-
tasks.push("This project was converted to a monorepo. Complete the migration:");
|
|
3211
|
-
tasks.push("");
|
|
3212
|
-
tasks.push("1. Move existing source into `packages/<name>/`");
|
|
3213
|
-
tasks.push("2. Split root `package.json` into workspace manifest + package-level `package.json`");
|
|
3214
|
-
tasks.push("3. Move `tsconfig.json` into the package, add project references to root");
|
|
3215
|
-
tasks.push("4. Create a package-level `tsdown.config.ts`");
|
|
3216
|
-
tasks.push("5. Update import paths and build scripts");
|
|
3217
|
-
tasks.push("");
|
|
3218
|
-
}
|
|
3219
|
-
if (results.some((r) => r.filePath === "test/example.test.ts" && r.action === "created")) {
|
|
3220
|
-
tasks.push("### Generate tests");
|
|
3221
|
-
tasks.push("");
|
|
3222
|
-
tasks.push("A starter test was created at `test/example.test.ts`. Review `src/` and create additional tests covering exported functions and edge cases.");
|
|
3223
|
-
tasks.push("");
|
|
3262
|
+
tasks.push(`### Migrate .oxlintrc.json to oxlint.config.ts
|
|
3263
|
+
|
|
3264
|
+
Compare \`.oxlintrc.json\` rules against \`@bensandee/config/oxlint/recommended\`. Port any custom rules/overrides into \`oxlint.config.ts\`, then delete \`.oxlintrc.json\`.
|
|
3265
|
+
`);
|
|
3266
|
+
return;
|
|
3224
3267
|
}
|
|
3225
|
-
if (
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3268
|
+
if (results.find((r) => r.filePath === "oxlint.config.ts")?.action === "skipped" && detected.hasOxlintConfig && !detected.oxlintExtendsRecommended) tasks.push(`### Verify oxlint.config.ts extends recommended rules
|
|
3269
|
+
|
|
3270
|
+
Ensure \`oxlint.config.ts\` imports and extends \`@bensandee/config/oxlint/recommended\`:
|
|
3271
|
+
|
|
3272
|
+
\`\`\`ts
|
|
3273
|
+
import recommended from "@bensandee/config/oxlint/recommended";
|
|
3274
|
+
import { defineConfig } from "oxlint";
|
|
3275
|
+
|
|
3276
|
+
export default defineConfig({ extends: [recommended] });
|
|
3277
|
+
\`\`\`
|
|
3278
|
+
`);
|
|
3279
|
+
}
|
|
3280
|
+
function addMonorepoTasks(tasks, config, detected) {
|
|
3281
|
+
if (config.structure !== "monorepo" || detected.hasPnpmWorkspace) return;
|
|
3282
|
+
tasks.push(`### Migrate to monorepo structure
|
|
3283
|
+
|
|
3284
|
+
This project was converted to a monorepo. Complete the migration:
|
|
3285
|
+
|
|
3286
|
+
1. Move existing source into \`packages/<name>/\`
|
|
3287
|
+
2. Split root \`package.json\` into workspace manifest + package-level \`package.json\`
|
|
3288
|
+
3. Move \`tsconfig.json\` into the package, add project references to root
|
|
3289
|
+
4. Create a package-level \`tsdown.config.ts\`
|
|
3290
|
+
5. Update import paths and build scripts
|
|
3291
|
+
`);
|
|
3292
|
+
}
|
|
3293
|
+
function addTestGenerationTasks(tasks, results) {
|
|
3294
|
+
if (!results.some((r) => r.filePath === "test/example.test.ts" && r.action === "created")) return;
|
|
3295
|
+
tasks.push(`### Generate tests
|
|
3296
|
+
|
|
3297
|
+
A starter test was created at \`test/example.test.ts\`. Review \`src/\` and create additional tests covering exported functions and edge cases.
|
|
3298
|
+
`);
|
|
3249
3299
|
}
|
|
3250
3300
|
//#endregion
|
|
3251
3301
|
//#region src/commands/repo-init.ts
|
|
@@ -3436,7 +3486,8 @@ async function runCheck(targetDir) {
|
|
|
3436
3486
|
if (!newContent) continue;
|
|
3437
3487
|
const existingPath = path.join(targetDir, r.filePath);
|
|
3438
3488
|
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
3439
|
-
if (
|
|
3489
|
+
if (isBundledFile(r.filePath)) log.info(` bundled file changed (${formatBytes(existing?.length ?? 0)} → ${formatBytes(newContent.length)})`);
|
|
3490
|
+
else if (!existing) {
|
|
3440
3491
|
const lineCount = newContent.split("\n").length - 1;
|
|
3441
3492
|
log.info(` + ${lineCount} new lines`);
|
|
3442
3493
|
} else {
|
|
@@ -3446,6 +3497,14 @@ async function runCheck(targetDir) {
|
|
|
3446
3497
|
}
|
|
3447
3498
|
return 1;
|
|
3448
3499
|
}
|
|
3500
|
+
/** Files that are minified/bundled and produce unreadable diffs. */
|
|
3501
|
+
function isBundledFile(filePath) {
|
|
3502
|
+
return filePath.endsWith("bst-hook.mjs");
|
|
3503
|
+
}
|
|
3504
|
+
function formatBytes(bytes) {
|
|
3505
|
+
if (bytes < 1024) return `${String(bytes)} B`;
|
|
3506
|
+
return `${(bytes / 1024).toFixed(1)} kB`;
|
|
3507
|
+
}
|
|
3449
3508
|
const normalize = (line) => line.trimEnd();
|
|
3450
3509
|
function lineDiff(oldText, newText) {
|
|
3451
3510
|
const oldLines = oldText.split("\n").map(normalize);
|
|
@@ -3464,15 +3523,16 @@ function lineDiff(oldText, newText) {
|
|
|
3464
3523
|
/** Create an executor that runs real commands, fetches, and reads the filesystem. */
|
|
3465
3524
|
function createRealExecutor() {
|
|
3466
3525
|
return {
|
|
3467
|
-
exec(
|
|
3526
|
+
exec(file, args, options) {
|
|
3468
3527
|
try {
|
|
3469
3528
|
return {
|
|
3470
|
-
stdout:
|
|
3529
|
+
stdout: execFileSync(file, args, {
|
|
3471
3530
|
cwd: options?.cwd,
|
|
3472
3531
|
env: {
|
|
3473
3532
|
...process.env,
|
|
3474
3533
|
...options?.env
|
|
3475
3534
|
},
|
|
3535
|
+
input: options?.input,
|
|
3476
3536
|
encoding: "utf-8",
|
|
3477
3537
|
stdio: [
|
|
3478
3538
|
"pipe",
|
|
@@ -3496,8 +3556,8 @@ function createRealExecutor() {
|
|
|
3496
3556
|
};
|
|
3497
3557
|
}
|
|
3498
3558
|
},
|
|
3499
|
-
execInherit(
|
|
3500
|
-
|
|
3559
|
+
execInherit(file, args, options) {
|
|
3560
|
+
execFileSync(file, args, {
|
|
3501
3561
|
cwd: options?.cwd,
|
|
3502
3562
|
env: options?.env ? {
|
|
3503
3563
|
...process.env,
|
|
@@ -3554,6 +3614,19 @@ function createRealExecutor() {
|
|
|
3554
3614
|
}
|
|
3555
3615
|
};
|
|
3556
3616
|
}
|
|
3617
|
+
/**
|
|
3618
|
+
* Compute sliding version tags from a semver version string.
|
|
3619
|
+
* For "1.2.3" returns ["v1", "v1.2", "latest"]. Strips prerelease suffixes.
|
|
3620
|
+
*/
|
|
3621
|
+
function computeSlidingTags(version) {
|
|
3622
|
+
const parts = (version.split("-")[0] ?? version).split(".");
|
|
3623
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) throw new FatalError(`Invalid version format "${version}". Expected semver (X.Y.Z)`);
|
|
3624
|
+
return [
|
|
3625
|
+
`v${parts[0]}`,
|
|
3626
|
+
`v${parts[0]}.${parts[1]}`,
|
|
3627
|
+
"latest"
|
|
3628
|
+
];
|
|
3629
|
+
}
|
|
3557
3630
|
/** Parse "New tag:" lines from changeset publish output. */
|
|
3558
3631
|
function parseNewTags(output) {
|
|
3559
3632
|
const tags = [];
|
|
@@ -3595,7 +3668,7 @@ const RETRY_BASE_DELAY_MS = 1e3;
|
|
|
3595
3668
|
async function responseBodyText(res) {
|
|
3596
3669
|
try {
|
|
3597
3670
|
const text = await res.text();
|
|
3598
|
-
return text.length > 500 ? text.slice(0, 500) + "…" : text;
|
|
3671
|
+
return redact(text.length > 500 ? text.slice(0, 500) + "…" : text);
|
|
3599
3672
|
} catch {
|
|
3600
3673
|
return "(could not read response body)";
|
|
3601
3674
|
}
|
|
@@ -3739,6 +3812,30 @@ async function ensureRelease(executor, conn, tag) {
|
|
|
3739
3812
|
//#endregion
|
|
3740
3813
|
//#region src/release/version.ts
|
|
3741
3814
|
const BRANCH = "changeset-release/main";
|
|
3815
|
+
/** Pattern matching a dateless version heading like `## 1.2.3` or `## 1.2.3-beta.0`. */
|
|
3816
|
+
const DATELESS_VERSION_RE = /^## (\d+\.\d+\.\d+(?:-[^\s)]+)?)$/gm;
|
|
3817
|
+
/** Format today's date as YYYY-MM-DD. */
|
|
3818
|
+
function todayIso() {
|
|
3819
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3820
|
+
}
|
|
3821
|
+
/** Add today's date to dateless version headings in a changelog string. */
|
|
3822
|
+
function stampDates(content, date) {
|
|
3823
|
+
return content.replace(DATELESS_VERSION_RE, `## $1 (${date})`);
|
|
3824
|
+
}
|
|
3825
|
+
/**
|
|
3826
|
+
* Stamp release dates on CHANGELOGs for packages that were just versioned.
|
|
3827
|
+
* Only modifies headings that don't already have a date (idempotent).
|
|
3828
|
+
*/
|
|
3829
|
+
function stampChangelogDates(executor, cwd, changedDirs, isMonorepo) {
|
|
3830
|
+
const date = todayIso();
|
|
3831
|
+
const changelogPaths = isMonorepo ? changedDirs.map((dir) => path.join(cwd, "packages", dir, "CHANGELOG.md")) : [path.join(cwd, "CHANGELOG.md")];
|
|
3832
|
+
for (const changelogPath of changelogPaths) {
|
|
3833
|
+
const content = executor.readFile(changelogPath);
|
|
3834
|
+
if (!content) continue;
|
|
3835
|
+
const updated = stampDates(content, date);
|
|
3836
|
+
if (updated !== content) executor.writeFile(changelogPath, updated);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3742
3839
|
/** Extract the latest changelog entry (content between first and second `## ` heading). */
|
|
3743
3840
|
function extractLatestEntry(changelog) {
|
|
3744
3841
|
const lines = changelog.split("\n");
|
|
@@ -3822,19 +3919,29 @@ async function runVersionMode(executor, config) {
|
|
|
3822
3919
|
debug(config, "Temporarily disabled changeset commit:true");
|
|
3823
3920
|
}
|
|
3824
3921
|
}
|
|
3825
|
-
const versionResult = executor.exec("pnpm changeset version", { cwd: config.cwd });
|
|
3922
|
+
const versionResult = executor.exec("pnpm", ["changeset", "version"], { cwd: config.cwd });
|
|
3826
3923
|
debugExec(config, "pnpm changeset version", versionResult);
|
|
3827
3924
|
if (originalConfig) executor.writeFile(changesetConfigPath, originalConfig);
|
|
3828
3925
|
if (versionResult.exitCode !== 0) throw new FatalError(`pnpm changeset version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr}`);
|
|
3829
|
-
|
|
3926
|
+
const packagesAfterVersion = executor.listWorkspacePackages(config.cwd);
|
|
3927
|
+
if (packagesBefore.length > 0 || packagesAfterVersion.length > 0) {
|
|
3928
|
+
const beforeMap = new Map(packagesBefore.map((pkg) => [pkg.name, pkg.version]));
|
|
3929
|
+
const changedDirs = packagesAfterVersion.filter((pkg) => beforeMap.get(pkg.name) !== pkg.version).map((pkg) => pkg.dir);
|
|
3930
|
+
stampChangelogDates(executor, config.cwd, changedDirs, true);
|
|
3931
|
+
} else stampChangelogDates(executor, config.cwd, [], false);
|
|
3932
|
+
debugExec(config, "pnpm install --no-frozen-lockfile", executor.exec("pnpm", ["install", "--no-frozen-lockfile"], { cwd: config.cwd }));
|
|
3830
3933
|
const { title, body } = buildPrContent(executor, config.cwd, packagesBefore);
|
|
3831
3934
|
debug(config, `PR title: ${title}`);
|
|
3832
|
-
const addResult = executor.exec("git add -A", { cwd: config.cwd });
|
|
3935
|
+
const addResult = executor.exec("git", ["add", "-A"], { cwd: config.cwd });
|
|
3833
3936
|
if (addResult.exitCode !== 0) throw new FatalError(`git add failed: ${addResult.stderr || addResult.stdout}`);
|
|
3834
3937
|
const remainingChangesets = executor.listChangesetFiles(config.cwd);
|
|
3835
3938
|
if (remainingChangesets.length > 0) log.warn(`Changeset files still present after versioning: ${remainingChangesets.join(", ")}`);
|
|
3836
3939
|
debug(config, `Changeset files after versioning: ${remainingChangesets.length > 0 ? remainingChangesets.join(", ") : "(none — all consumed)"}`);
|
|
3837
|
-
const commitResult = executor.exec("git
|
|
3940
|
+
const commitResult = executor.exec("git", [
|
|
3941
|
+
"commit",
|
|
3942
|
+
"-m",
|
|
3943
|
+
"chore: version packages"
|
|
3944
|
+
], { cwd: config.cwd });
|
|
3838
3945
|
debugExec(config, "git commit", commitResult);
|
|
3839
3946
|
if (commitResult.exitCode !== 0) {
|
|
3840
3947
|
log.info("Nothing to commit after versioning");
|
|
@@ -3850,7 +3957,12 @@ async function runVersionMode(executor, config) {
|
|
|
3850
3957
|
pr: "none"
|
|
3851
3958
|
};
|
|
3852
3959
|
}
|
|
3853
|
-
const pushResult = executor.exec(
|
|
3960
|
+
const pushResult = executor.exec("git", [
|
|
3961
|
+
"push",
|
|
3962
|
+
"origin",
|
|
3963
|
+
`HEAD:refs/heads/${BRANCH}`,
|
|
3964
|
+
"--force"
|
|
3965
|
+
], { cwd: config.cwd });
|
|
3854
3966
|
debugExec(config, "git push", pushResult);
|
|
3855
3967
|
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
3856
3968
|
const conn = {
|
|
@@ -3888,14 +4000,18 @@ async function runVersionMode(executor, config) {
|
|
|
3888
4000
|
/** Mode 2: publish to npm, push tags, and create Forgejo releases. */
|
|
3889
4001
|
async function runPublishMode(executor, config) {
|
|
3890
4002
|
log.info("No changesets — publishing packages");
|
|
3891
|
-
const publishResult = executor.exec("pnpm changeset publish", { cwd: config.cwd });
|
|
4003
|
+
const publishResult = executor.exec("pnpm", ["changeset", "publish"], { cwd: config.cwd });
|
|
3892
4004
|
debugExec(config, "pnpm changeset publish", publishResult);
|
|
3893
4005
|
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
3894
4006
|
const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
|
|
3895
4007
|
debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
|
|
3896
4008
|
const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
|
|
3897
4009
|
debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
|
|
3898
|
-
const remoteTags = parseRemoteTags(executor.exec("git
|
|
4010
|
+
const remoteTags = parseRemoteTags(executor.exec("git", [
|
|
4011
|
+
"ls-remote",
|
|
4012
|
+
"--tags",
|
|
4013
|
+
"origin"
|
|
4014
|
+
], { cwd: config.cwd }).stdout);
|
|
3899
4015
|
debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
|
|
3900
4016
|
const remoteSet = new Set(remoteTags);
|
|
3901
4017
|
const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
|
|
@@ -3929,11 +4045,19 @@ async function runPublishMode(executor, config) {
|
|
|
3929
4045
|
const errors = [];
|
|
3930
4046
|
for (const tag of allTags) try {
|
|
3931
4047
|
if (!remoteSet.has(tag)) {
|
|
3932
|
-
if (executor.exec(
|
|
3933
|
-
|
|
4048
|
+
if (executor.exec("git", [
|
|
4049
|
+
"tag",
|
|
4050
|
+
"-l",
|
|
4051
|
+
tag
|
|
4052
|
+
], { cwd: config.cwd }).stdout.trim() === "") {
|
|
4053
|
+
const tagResult = executor.exec("git", ["tag", tag], { cwd: config.cwd });
|
|
3934
4054
|
if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
3935
4055
|
}
|
|
3936
|
-
const pushTagResult = executor.exec(
|
|
4056
|
+
const pushTagResult = executor.exec("git", [
|
|
4057
|
+
"push",
|
|
4058
|
+
"origin",
|
|
4059
|
+
`refs/tags/${tag}`
|
|
4060
|
+
], { cwd: config.cwd });
|
|
3937
4061
|
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
3938
4062
|
}
|
|
3939
4063
|
if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
|
|
@@ -3946,6 +4070,31 @@ async function runPublishMode(executor, config) {
|
|
|
3946
4070
|
log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3947
4071
|
}
|
|
3948
4072
|
if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
|
|
4073
|
+
const slidingTags = allTags.flatMap((t) => {
|
|
4074
|
+
const atIndex = t.lastIndexOf("@");
|
|
4075
|
+
if (atIndex <= 0) return [];
|
|
4076
|
+
const name = t.slice(0, atIndex);
|
|
4077
|
+
return computeSlidingTags(t.slice(atIndex + 1)).map((suffix) => `${name}@${suffix}`);
|
|
4078
|
+
});
|
|
4079
|
+
if (slidingTags.length > 0) {
|
|
4080
|
+
for (const tag of slidingTags) {
|
|
4081
|
+
const tagResult = executor.exec("git", [
|
|
4082
|
+
"tag",
|
|
4083
|
+
"-f",
|
|
4084
|
+
tag
|
|
4085
|
+
], { cwd: config.cwd });
|
|
4086
|
+
if (tagResult.exitCode !== 0) log.warn(`Warning: Failed to create sliding tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4087
|
+
}
|
|
4088
|
+
const forcePushResult = executor.exec("git", [
|
|
4089
|
+
"push",
|
|
4090
|
+
"origin",
|
|
4091
|
+
...slidingTags.map((t) => `refs/tags/${t}`),
|
|
4092
|
+
"--force"
|
|
4093
|
+
], { cwd: config.cwd });
|
|
4094
|
+
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4095
|
+
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4096
|
+
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4097
|
+
}
|
|
3949
4098
|
return {
|
|
3950
4099
|
mode: "publish",
|
|
3951
4100
|
tags: allTags
|
|
@@ -4035,15 +4184,28 @@ function parseGitUrl(urlStr) {
|
|
|
4035
4184
|
function configureGitAuth(executor, conn, cwd) {
|
|
4036
4185
|
const host = conn.serverUrl.replace(/^https?:\/\//, "");
|
|
4037
4186
|
const authUrl = `https://x-access-token:${conn.token}@${host}/${conn.repository}`;
|
|
4038
|
-
executor.exec(
|
|
4187
|
+
executor.exec("git", [
|
|
4188
|
+
"remote",
|
|
4189
|
+
"set-url",
|
|
4190
|
+
"origin",
|
|
4191
|
+
authUrl
|
|
4192
|
+
], { cwd });
|
|
4039
4193
|
}
|
|
4040
4194
|
/** Configure git user.name and user.email for CI bot commits. */
|
|
4041
4195
|
function configureGitIdentity(executor, platform, cwd) {
|
|
4042
4196
|
const isGitHub = platform === "github";
|
|
4043
4197
|
const name = isGitHub ? "github-actions[bot]" : "forgejo-actions[bot]";
|
|
4044
4198
|
const email = isGitHub ? "github-actions[bot]@users.noreply.github.com" : "forgejo-actions[bot]@noreply.localhost";
|
|
4045
|
-
executor.exec(
|
|
4046
|
-
|
|
4199
|
+
executor.exec("git", [
|
|
4200
|
+
"config",
|
|
4201
|
+
"user.name",
|
|
4202
|
+
name
|
|
4203
|
+
], { cwd });
|
|
4204
|
+
executor.exec("git", [
|
|
4205
|
+
"config",
|
|
4206
|
+
"user.email",
|
|
4207
|
+
email
|
|
4208
|
+
], { cwd });
|
|
4047
4209
|
}
|
|
4048
4210
|
//#endregion
|
|
4049
4211
|
//#region src/commands/release-changesets.ts
|
|
@@ -4084,7 +4246,11 @@ function buildReleaseConfig(flags) {
|
|
|
4084
4246
|
function getCurrentBranch(executor, cwd) {
|
|
4085
4247
|
const ref = process.env["GITHUB_REF"];
|
|
4086
4248
|
if (ref?.startsWith("refs/heads/")) return ref.slice(11);
|
|
4087
|
-
return executor.exec("git
|
|
4249
|
+
return executor.exec("git", [
|
|
4250
|
+
"rev-parse",
|
|
4251
|
+
"--abbrev-ref",
|
|
4252
|
+
"HEAD"
|
|
4253
|
+
], { cwd }).stdout.trim();
|
|
4088
4254
|
}
|
|
4089
4255
|
/** Core release logic — testable with a mock executor. */
|
|
4090
4256
|
async function runRelease(config, executor) {
|
|
@@ -4155,9 +4321,15 @@ async function triggerForgejo(conn, ref, inputs) {
|
|
|
4155
4321
|
}
|
|
4156
4322
|
function triggerGitHub(ref, inputs) {
|
|
4157
4323
|
const executor = createRealExecutor();
|
|
4158
|
-
const inputFlags = Object.entries(inputs).
|
|
4159
|
-
const
|
|
4160
|
-
|
|
4324
|
+
const inputFlags = Object.entries(inputs).flatMap(([k, v]) => ["-f", `${k}=${v}`]);
|
|
4325
|
+
const result = executor.exec("gh", [
|
|
4326
|
+
"workflow",
|
|
4327
|
+
"run",
|
|
4328
|
+
"release.yml",
|
|
4329
|
+
"--ref",
|
|
4330
|
+
ref,
|
|
4331
|
+
...inputFlags
|
|
4332
|
+
], { cwd: process.cwd() });
|
|
4161
4333
|
if (result.exitCode !== 0) throw new FatalError(`Failed to trigger GitHub workflow: ${result.stderr || result.stdout || "unknown error"}`);
|
|
4162
4334
|
log.info(`Triggered release workflow on GitHub (ref: ${ref})`);
|
|
4163
4335
|
}
|
|
@@ -4222,34 +4394,39 @@ async function mergeForgejo(conn, dryRun) {
|
|
|
4222
4394
|
function mergeGitHub(dryRun) {
|
|
4223
4395
|
const executor = createRealExecutor();
|
|
4224
4396
|
if (dryRun) {
|
|
4225
|
-
const prNum = executor.exec(
|
|
4397
|
+
const prNum = executor.exec("gh", [
|
|
4398
|
+
"pr",
|
|
4399
|
+
"view",
|
|
4400
|
+
HEAD_BRANCH,
|
|
4401
|
+
"--json",
|
|
4402
|
+
"number",
|
|
4403
|
+
"--jq",
|
|
4404
|
+
".number"
|
|
4405
|
+
], { cwd: process.cwd() }).stdout.trim();
|
|
4226
4406
|
if (!prNum) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
|
|
4227
4407
|
log.info(`[dry-run] Would merge PR #${prNum} and delete branch ${HEAD_BRANCH}`);
|
|
4228
4408
|
return;
|
|
4229
4409
|
}
|
|
4230
|
-
const result = executor.exec(
|
|
4410
|
+
const result = executor.exec("gh", [
|
|
4411
|
+
"pr",
|
|
4412
|
+
"merge",
|
|
4413
|
+
HEAD_BRANCH,
|
|
4414
|
+
"--merge",
|
|
4415
|
+
"--delete-branch"
|
|
4416
|
+
], { cwd: process.cwd() });
|
|
4231
4417
|
if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
|
|
4232
4418
|
log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
|
|
4233
4419
|
}
|
|
4234
4420
|
//#endregion
|
|
4235
4421
|
//#region src/release/simple.ts
|
|
4236
|
-
/**
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
*/
|
|
4240
|
-
function computeSlidingTags(version) {
|
|
4241
|
-
const parts = (version.split("-")[0] ?? version).split(".");
|
|
4242
|
-
if (parts.length < 2 || !parts[0] || !parts[1]) throw new FatalError(`Invalid version format "${version}". Expected semver (X.Y.Z)`);
|
|
4243
|
-
return [`v${parts[0]}`, `v${parts[0]}.${parts[1]}`];
|
|
4244
|
-
}
|
|
4245
|
-
/** Build the commit-and-tag-version command with appropriate flags. */
|
|
4246
|
-
function buildCommand(config) {
|
|
4247
|
-
const args = ["pnpm exec commit-and-tag-version"];
|
|
4422
|
+
/** Build the commit-and-tag-version args. */
|
|
4423
|
+
function buildArgs(config) {
|
|
4424
|
+
const args = ["exec", "commit-and-tag-version"];
|
|
4248
4425
|
if (config.dryRun) args.push("--dry-run");
|
|
4249
4426
|
if (config.firstRelease) args.push("--first-release");
|
|
4250
|
-
if (config.releaseAs) args.push(
|
|
4251
|
-
if (config.prerelease) args.push(
|
|
4252
|
-
return args
|
|
4427
|
+
if (config.releaseAs) args.push("--release-as", config.releaseAs);
|
|
4428
|
+
if (config.prerelease) args.push("--prerelease", config.prerelease);
|
|
4429
|
+
return args;
|
|
4253
4430
|
}
|
|
4254
4431
|
/** Read the current version from package.json. */
|
|
4255
4432
|
function readVersion(executor, cwd) {
|
|
@@ -4266,14 +4443,18 @@ function platformType(config) {
|
|
|
4266
4443
|
/** Run the full commit-and-tag-version release flow. */
|
|
4267
4444
|
async function runSimpleRelease(executor, config) {
|
|
4268
4445
|
configureGitIdentity(executor, platformType(config), config.cwd);
|
|
4269
|
-
const
|
|
4270
|
-
log.info(`Running: ${
|
|
4271
|
-
const versionResult = executor.exec(
|
|
4446
|
+
const args = buildArgs(config);
|
|
4447
|
+
log.info(`Running: pnpm ${args.join(" ")}`);
|
|
4448
|
+
const versionResult = executor.exec("pnpm", args, { cwd: config.cwd });
|
|
4272
4449
|
debugExec(config, "commit-and-tag-version", versionResult);
|
|
4273
4450
|
if (versionResult.exitCode !== 0) throw new FatalError(`commit-and-tag-version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr || versionResult.stdout}`);
|
|
4274
4451
|
const version = readVersion(executor, config.cwd);
|
|
4275
4452
|
debug(config, `New version: ${version}`);
|
|
4276
|
-
const tagResult = executor.exec("git
|
|
4453
|
+
const tagResult = executor.exec("git", [
|
|
4454
|
+
"describe",
|
|
4455
|
+
"--tags",
|
|
4456
|
+
"--abbrev=0"
|
|
4457
|
+
], { cwd: config.cwd });
|
|
4277
4458
|
debugExec(config, "git describe", tagResult);
|
|
4278
4459
|
const tag = tagResult.stdout.trim();
|
|
4279
4460
|
if (!tag) throw new FatalError("Could not determine the new tag from git describe");
|
|
@@ -4297,9 +4478,18 @@ async function runSimpleRelease(executor, config) {
|
|
|
4297
4478
|
configureGitAuth(executor, config.platform.conn, config.cwd);
|
|
4298
4479
|
debug(config, "Configured git remote with RELEASE_TOKEN auth");
|
|
4299
4480
|
}
|
|
4300
|
-
const branch = executor.exec("git
|
|
4481
|
+
const branch = executor.exec("git", [
|
|
4482
|
+
"rev-parse",
|
|
4483
|
+
"--abbrev-ref",
|
|
4484
|
+
"HEAD"
|
|
4485
|
+
], { cwd: config.cwd }).stdout.trim() || "main";
|
|
4301
4486
|
debug(config, `Pushing to origin/${branch}`);
|
|
4302
|
-
const pushResult = executor.exec(
|
|
4487
|
+
const pushResult = executor.exec("git", [
|
|
4488
|
+
"push",
|
|
4489
|
+
"--follow-tags",
|
|
4490
|
+
"origin",
|
|
4491
|
+
branch
|
|
4492
|
+
], { cwd: config.cwd });
|
|
4303
4493
|
debugExec(config, "git push", pushResult);
|
|
4304
4494
|
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
4305
4495
|
pushed = true;
|
|
@@ -4309,10 +4499,19 @@ async function runSimpleRelease(executor, config) {
|
|
|
4309
4499
|
if (!config.noSlidingTags && pushed) {
|
|
4310
4500
|
slidingTags = computeSlidingTags(version);
|
|
4311
4501
|
for (const slidingTag of slidingTags) {
|
|
4312
|
-
const slidingTagResult = executor.exec(
|
|
4502
|
+
const slidingTagResult = executor.exec("git", [
|
|
4503
|
+
"tag",
|
|
4504
|
+
"-f",
|
|
4505
|
+
slidingTag
|
|
4506
|
+
], { cwd: config.cwd });
|
|
4313
4507
|
if (slidingTagResult.exitCode !== 0) throw new FatalError(`Failed to create sliding tag ${slidingTag}: ${slidingTagResult.stderr || slidingTagResult.stdout}`);
|
|
4314
4508
|
}
|
|
4315
|
-
const forcePushResult = executor.exec(
|
|
4509
|
+
const forcePushResult = executor.exec("git", [
|
|
4510
|
+
"push",
|
|
4511
|
+
"origin",
|
|
4512
|
+
...slidingTags,
|
|
4513
|
+
"--force"
|
|
4514
|
+
], { cwd: config.cwd });
|
|
4316
4515
|
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4317
4516
|
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4318
4517
|
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
@@ -4337,7 +4536,12 @@ async function createPlatformRelease(executor, config, tag) {
|
|
|
4337
4536
|
log.info(`Created Forgejo release for ${tag}`);
|
|
4338
4537
|
return true;
|
|
4339
4538
|
}
|
|
4340
|
-
const ghResult = executor.exec(
|
|
4539
|
+
const ghResult = executor.exec("gh", [
|
|
4540
|
+
"release",
|
|
4541
|
+
"create",
|
|
4542
|
+
tag,
|
|
4543
|
+
"--generate-notes"
|
|
4544
|
+
], { cwd: config.cwd });
|
|
4341
4545
|
debugExec(config, "gh release create", ghResult);
|
|
4342
4546
|
if (ghResult.exitCode !== 0) {
|
|
4343
4547
|
log.warn(`Warning: Failed to create GitHub release: ${ghResult.stderr || ghResult.stdout}`);
|
|
@@ -4575,6 +4779,45 @@ function requireEnv(name) {
|
|
|
4575
4779
|
if (!value) throw new FatalError(`Missing required environment variable: ${name}`);
|
|
4576
4780
|
return value;
|
|
4577
4781
|
}
|
|
4782
|
+
/**
|
|
4783
|
+
* Derive the registry host from a repository URL hostname.
|
|
4784
|
+
* GitHub uses ghcr.io; all other hosts (Forgejo, GitLab, etc.) use the hostname directly.
|
|
4785
|
+
*/
|
|
4786
|
+
function registryHostFromHostname(hostname) {
|
|
4787
|
+
return hostname === "github.com" ? "ghcr.io" : hostname;
|
|
4788
|
+
}
|
|
4789
|
+
/**
|
|
4790
|
+
* Resolve docker registry config using a fallback chain:
|
|
4791
|
+
* 1. Environment variables (DOCKER_REGISTRY_HOST / DOCKER_REGISTRY_NAMESPACE)
|
|
4792
|
+
* 2. .tooling.json registryHost / registryNamespace fields
|
|
4793
|
+
* 3. Derived from package.json repository URL
|
|
4794
|
+
*/
|
|
4795
|
+
function resolveRegistryConfig(cwd) {
|
|
4796
|
+
const envHost = process.env["DOCKER_REGISTRY_HOST"];
|
|
4797
|
+
const envNamespace = process.env["DOCKER_REGISTRY_NAMESPACE"];
|
|
4798
|
+
if (envHost && envNamespace) return {
|
|
4799
|
+
registryHost: envHost,
|
|
4800
|
+
registryNamespace: envNamespace
|
|
4801
|
+
};
|
|
4802
|
+
const toolingConfig = loadToolingConfig(cwd);
|
|
4803
|
+
const toolingHost = toolingConfig?.registryHost;
|
|
4804
|
+
const toolingNamespace = toolingConfig?.registryNamespace;
|
|
4805
|
+
if (toolingHost && toolingNamespace) return {
|
|
4806
|
+
registryHost: envHost ?? toolingHost,
|
|
4807
|
+
registryNamespace: envNamespace ?? toolingNamespace
|
|
4808
|
+
};
|
|
4809
|
+
const parsed = parseRepositoryUrl(cwd);
|
|
4810
|
+
if (parsed) {
|
|
4811
|
+
const derivedHost = registryHostFromHostname(parsed.hostname);
|
|
4812
|
+
const owner = parsed.repository.split("/")[0];
|
|
4813
|
+
const host = envHost ?? toolingHost ?? derivedHost;
|
|
4814
|
+
return {
|
|
4815
|
+
registryHost: host,
|
|
4816
|
+
registryNamespace: envNamespace ?? toolingNamespace ?? `${host}/${owner}`
|
|
4817
|
+
};
|
|
4818
|
+
}
|
|
4819
|
+
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.");
|
|
4820
|
+
}
|
|
4578
4821
|
const publishDockerCommand = defineCommand({
|
|
4579
4822
|
meta: {
|
|
4580
4823
|
name: "docker:publish",
|
|
@@ -4591,10 +4834,12 @@ const publishDockerCommand = defineCommand({
|
|
|
4591
4834
|
}
|
|
4592
4835
|
},
|
|
4593
4836
|
async run({ args }) {
|
|
4837
|
+
const cwd = process.cwd();
|
|
4838
|
+
const { registryHost, registryNamespace } = resolveRegistryConfig(cwd);
|
|
4594
4839
|
const config = {
|
|
4595
|
-
cwd
|
|
4596
|
-
registryHost
|
|
4597
|
-
registryNamespace
|
|
4840
|
+
cwd,
|
|
4841
|
+
registryHost,
|
|
4842
|
+
registryNamespace,
|
|
4598
4843
|
username: requireEnv("DOCKER_USERNAME"),
|
|
4599
4844
|
password: requireEnv("DOCKER_PASSWORD"),
|
|
4600
4845
|
dryRun: args["dry-run"] === true,
|
|
@@ -4869,8 +5114,8 @@ function toHttpHealthChecks(checks) {
|
|
|
4869
5114
|
}
|
|
4870
5115
|
/** Write the generated check overlay to a temp file. Returns the absolute path. */
|
|
4871
5116
|
function writeTempOverlay(content) {
|
|
4872
|
-
const
|
|
4873
|
-
const filePath = path.join(
|
|
5117
|
+
const dir = mkdtempSync(path.join(tmpdir(), "tooling-check-"));
|
|
5118
|
+
const filePath = path.join(dir, "overlay.yaml");
|
|
4874
5119
|
writeFileSync(filePath, content, "utf-8");
|
|
4875
5120
|
return filePath;
|
|
4876
5121
|
}
|
|
@@ -4946,17 +5191,99 @@ const dockerCheckCommand = defineCommand({
|
|
|
4946
5191
|
if (!result.success) throw new FatalError(`Check failed (${result.reason}): ${result.message}`);
|
|
4947
5192
|
} finally {
|
|
4948
5193
|
try {
|
|
4949
|
-
|
|
5194
|
+
rmSync(path.dirname(tempOverlayPath), { recursive: true });
|
|
4950
5195
|
} catch (_error) {}
|
|
4951
5196
|
}
|
|
4952
5197
|
}
|
|
4953
5198
|
});
|
|
4954
5199
|
//#endregion
|
|
5200
|
+
//#region src/commands/hooks-audit.ts
|
|
5201
|
+
const AuditEntrySchema = z.object({
|
|
5202
|
+
timestamp: z.string(),
|
|
5203
|
+
sessionId: z.string(),
|
|
5204
|
+
command: z.string(),
|
|
5205
|
+
decision: z.enum([
|
|
5206
|
+
"allow",
|
|
5207
|
+
"deny",
|
|
5208
|
+
"passthrough"
|
|
5209
|
+
]),
|
|
5210
|
+
rule: z.string().nullable(),
|
|
5211
|
+
reason: z.string().nullable()
|
|
5212
|
+
});
|
|
5213
|
+
function parseAuditLog(logPath) {
|
|
5214
|
+
if (!existsSync(logPath)) return [];
|
|
5215
|
+
const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
|
|
5216
|
+
const entries = [];
|
|
5217
|
+
for (const line of lines) try {
|
|
5218
|
+
const result = AuditEntrySchema.safeParse(JSON.parse(line));
|
|
5219
|
+
if (result.success) entries.push(result.data);
|
|
5220
|
+
} catch (_error) {}
|
|
5221
|
+
return entries;
|
|
5222
|
+
}
|
|
5223
|
+
const hooksAuditCommand = defineCommand({
|
|
5224
|
+
meta: {
|
|
5225
|
+
name: "hooks:audit",
|
|
5226
|
+
description: "Summarize the bst-hook audit log"
|
|
5227
|
+
},
|
|
5228
|
+
args: {
|
|
5229
|
+
dir: {
|
|
5230
|
+
type: "positional",
|
|
5231
|
+
description: "Project directory (default: current directory)",
|
|
5232
|
+
required: false
|
|
5233
|
+
},
|
|
5234
|
+
tail: {
|
|
5235
|
+
type: "string",
|
|
5236
|
+
description: "Show the last N entries (default: 20)"
|
|
5237
|
+
},
|
|
5238
|
+
decision: {
|
|
5239
|
+
type: "string",
|
|
5240
|
+
description: "Filter by decision: allow, deny, or passthrough"
|
|
5241
|
+
},
|
|
5242
|
+
json: {
|
|
5243
|
+
type: "boolean",
|
|
5244
|
+
description: "Output raw JSON lines instead of formatted summary"
|
|
5245
|
+
}
|
|
5246
|
+
},
|
|
5247
|
+
async run({ args }) {
|
|
5248
|
+
const targetDir = path.resolve(args.dir ?? ".");
|
|
5249
|
+
const logPath = path.join(targetDir, ".claude/hooks/bst-hook.audit.local.jsonl");
|
|
5250
|
+
let entries = parseAuditLog(logPath);
|
|
5251
|
+
if (entries.length === 0) {
|
|
5252
|
+
log.info("No audit log entries found.");
|
|
5253
|
+
return;
|
|
5254
|
+
}
|
|
5255
|
+
const decisionFilter = args.decision;
|
|
5256
|
+
if (decisionFilter) entries = entries.filter((e) => e.decision === decisionFilter);
|
|
5257
|
+
const tailCount = args.tail ? Number.parseInt(args.tail, 10) : 20;
|
|
5258
|
+
if (entries.length > tailCount) entries = entries.slice(-tailCount);
|
|
5259
|
+
if (args.json === true) {
|
|
5260
|
+
for (const entry of entries) console.log(JSON.stringify(entry));
|
|
5261
|
+
return;
|
|
5262
|
+
}
|
|
5263
|
+
const allEntries = parseAuditLog(logPath);
|
|
5264
|
+
const counts = {
|
|
5265
|
+
allow: 0,
|
|
5266
|
+
deny: 0,
|
|
5267
|
+
passthrough: 0
|
|
5268
|
+
};
|
|
5269
|
+
for (const e of allEntries) counts[e.decision]++;
|
|
5270
|
+
log.info(`Audit log: ${String(allEntries.length)} entries — ${String(counts.allow)} allow, ${String(counts.deny)} deny, ${String(counts.passthrough)} passthrough`);
|
|
5271
|
+
log.info("");
|
|
5272
|
+
for (const entry of entries) {
|
|
5273
|
+
const ts = entry.timestamp.replace("T", " ").replace(/\.\d+Z$/, "");
|
|
5274
|
+
const icon = entry.decision === "allow" ? "+" : entry.decision === "deny" ? "x" : "?";
|
|
5275
|
+
const rule = entry.rule ? ` [${entry.rule}]` : "";
|
|
5276
|
+
const reason = entry.reason ? ` — ${entry.reason}` : "";
|
|
5277
|
+
log.info(` ${icon} ${ts} ${entry.decision.padEnd(11)} ${entry.command}${rule}${reason}`);
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
});
|
|
5281
|
+
//#endregion
|
|
4955
5282
|
//#region src/bin.ts
|
|
4956
5283
|
const main = defineCommand({
|
|
4957
5284
|
meta: {
|
|
4958
5285
|
name: "bst",
|
|
4959
|
-
version: "0.
|
|
5286
|
+
version: "0.39.0",
|
|
4960
5287
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
4961
5288
|
},
|
|
4962
5289
|
subCommands: {
|
|
@@ -4969,10 +5296,11 @@ const main = defineCommand({
|
|
|
4969
5296
|
"release:simple": releaseSimpleCommand,
|
|
4970
5297
|
"docker:publish": publishDockerCommand,
|
|
4971
5298
|
"docker:build": dockerBuildCommand,
|
|
4972
|
-
"docker:check": dockerCheckCommand
|
|
5299
|
+
"docker:check": dockerCheckCommand,
|
|
5300
|
+
"hooks:audit": hooksAuditCommand
|
|
4973
5301
|
}
|
|
4974
5302
|
});
|
|
4975
|
-
console.log(`@bensandee/tooling v0.
|
|
5303
|
+
console.log(`@bensandee/tooling v0.39.0`);
|
|
4976
5304
|
async function run() {
|
|
4977
5305
|
await runMain(main);
|
|
4978
5306
|
process.exit(process.exitCode ?? 0);
|