@bensandee/tooling 0.45.1 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +358 -424
- package/dist/index.d.mts +1 -1
- package/package.json +3 -3
package/dist/bin.mjs
CHANGED
|
@@ -4,16 +4,16 @@ 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, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
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";
|
|
12
12
|
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
13
13
|
import { isMap, isScalar, parse as parse$1, parseDocument, stringify, visit } from "yaml";
|
|
14
|
-
import { createHmac } from "node:crypto";
|
|
15
14
|
import picomatch from "picomatch";
|
|
16
15
|
import { tmpdir } from "node:os";
|
|
16
|
+
import { createHmac } from "node:crypto";
|
|
17
17
|
//#region src/types.ts
|
|
18
18
|
const LEGACY_TOOLS = [
|
|
19
19
|
"eslint",
|
|
@@ -538,7 +538,7 @@ function createContext(config, confirmOverwrite) {
|
|
|
538
538
|
remove: (rel) => {
|
|
539
539
|
assertContained(config.targetDir, rel);
|
|
540
540
|
const fullPath = path.join(config.targetDir, rel);
|
|
541
|
-
if (existsSync(fullPath)) rmSync(fullPath);
|
|
541
|
+
if (existsSync(fullPath)) rmSync(fullPath, { recursive: true });
|
|
542
542
|
},
|
|
543
543
|
confirmOverwrite
|
|
544
544
|
};
|
|
@@ -1129,13 +1129,14 @@ function normalizeWorkflow(content) {
|
|
|
1129
1129
|
const DEV_BINARY_PATTERN = /node\s+packages\/tooling-cli\/dist\/bin\.mjs/;
|
|
1130
1130
|
/**
|
|
1131
1131
|
* If the existing file uses the dev binary path (`node packages/tooling-cli/dist/bin.mjs`),
|
|
1132
|
-
* substitute it back into the generated content so we don't overwrite it with `pnpm exec bst
|
|
1132
|
+
* substitute it back into the generated content so we don't overwrite it with `pnpm exec bst`
|
|
1133
|
+
* or `pnpm dlx @bensandee/tooling`.
|
|
1133
1134
|
*/
|
|
1134
1135
|
function preserveDevBinaryPath(generated, existing) {
|
|
1135
1136
|
if (!existing) return generated;
|
|
1136
1137
|
const match = DEV_BINARY_PATTERN.exec(existing);
|
|
1137
1138
|
if (!match) return generated;
|
|
1138
|
-
return generated.replaceAll("pnpm exec bst", match[0]);
|
|
1139
|
+
return generated.replaceAll("pnpm exec bst", match[0]).replaceAll("pnpm dlx @bensandee/tooling", match[0]);
|
|
1139
1140
|
}
|
|
1140
1141
|
/** Build a complete workflow YAML string from structured options. Single source of truth for workflow files. */
|
|
1141
1142
|
function buildWorkflowYaml(options) {
|
|
@@ -1167,6 +1168,18 @@ function hasEnginesNode(ctx) {
|
|
|
1167
1168
|
function computeNodeVersionYaml(ctx) {
|
|
1168
1169
|
return hasEnginesNode(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
|
|
1169
1170
|
}
|
|
1171
|
+
/** Build the docker publish env block, omitting registry vars when derivable at runtime. */
|
|
1172
|
+
function dockerPublishEnv(registryDerivable) {
|
|
1173
|
+
return {
|
|
1174
|
+
...registryDerivable ? {} : {
|
|
1175
|
+
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
1176
|
+
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
1177
|
+
},
|
|
1178
|
+
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
1179
|
+
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
1180
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1170
1183
|
//#endregion
|
|
1171
1184
|
//#region src/generators/publish-ci.ts
|
|
1172
1185
|
/**
|
|
@@ -1179,18 +1192,6 @@ function hasRepositoryUrl(ctx) {
|
|
|
1179
1192
|
if (!raw) return false;
|
|
1180
1193
|
return parsePackageJson(raw)?.repository !== void 0;
|
|
1181
1194
|
}
|
|
1182
|
-
/** Build the docker publish env block, omitting registry vars when derivable at runtime. */
|
|
1183
|
-
function dockerPublishEnv$1(registryDerivable) {
|
|
1184
|
-
return {
|
|
1185
|
-
...registryDerivable ? {} : {
|
|
1186
|
-
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
1187
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
1188
|
-
},
|
|
1189
|
-
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
1190
|
-
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
1191
|
-
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
1195
|
function publishSteps(nodeVersionYaml, registryDerivable) {
|
|
1195
1196
|
return [
|
|
1196
1197
|
{ step: { uses: "actions/checkout@v6" } },
|
|
@@ -1202,13 +1203,13 @@ function publishSteps(nodeVersionYaml, registryDerivable) {
|
|
|
1202
1203
|
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
1203
1204
|
{ step: {
|
|
1204
1205
|
name: "Publish Docker images",
|
|
1205
|
-
env: dockerPublishEnv
|
|
1206
|
+
env: dockerPublishEnv(registryDerivable),
|
|
1206
1207
|
run: "pnpm exec bst docker:publish"
|
|
1207
1208
|
} }
|
|
1208
1209
|
];
|
|
1209
1210
|
}
|
|
1210
1211
|
const DockerMapSchema = z.object({ docker: z.record(z.string(), z.unknown()).optional() });
|
|
1211
|
-
const PostReleaseHookSchema
|
|
1212
|
+
const PostReleaseHookSchema = z.object({
|
|
1212
1213
|
postReleaseHook: z.string().optional(),
|
|
1213
1214
|
postReleaseHookSecrets: z.array(z.string()).optional()
|
|
1214
1215
|
});
|
|
@@ -1217,7 +1218,7 @@ function getPostReleaseHookCommand(ctx) {
|
|
|
1217
1218
|
const raw = ctx.read(".tooling.json");
|
|
1218
1219
|
if (!raw) return void 0;
|
|
1219
1220
|
try {
|
|
1220
|
-
const result = PostReleaseHookSchema
|
|
1221
|
+
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
1221
1222
|
if (!result.success) return void 0;
|
|
1222
1223
|
return result.data.postReleaseHook;
|
|
1223
1224
|
} catch (error) {
|
|
@@ -1230,7 +1231,7 @@ function getPostReleaseHookSecrets(ctx) {
|
|
|
1230
1231
|
const raw = ctx.read(".tooling.json");
|
|
1231
1232
|
if (!raw) return void 0;
|
|
1232
1233
|
try {
|
|
1233
|
-
const result = PostReleaseHookSchema
|
|
1234
|
+
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
1234
1235
|
if (!result.success) return void 0;
|
|
1235
1236
|
return result.data.postReleaseHookSecrets;
|
|
1236
1237
|
} catch (error) {
|
|
@@ -1584,7 +1585,7 @@ async function generatePackageJson(ctx) {
|
|
|
1584
1585
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1585
1586
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1586
1587
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.1";
|
|
1587
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1588
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.47.0";
|
|
1588
1589
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
|
|
1589
1590
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1590
1591
|
"@changesets/cli": "2.30.0",
|
|
@@ -1653,6 +1654,16 @@ async function generatePackageJson(ctx) {
|
|
|
1653
1654
|
changes.push(`updated devDependency: ${key} to ${value}`);
|
|
1654
1655
|
}
|
|
1655
1656
|
pkg.devDependencies = existingDevDeps;
|
|
1657
|
+
const existingDeps = pkg.dependencies ?? {};
|
|
1658
|
+
const commonKey = "@bensandee/common";
|
|
1659
|
+
if (commonKey in existingDeps && existingDeps[commonKey] !== "workspace:*") {
|
|
1660
|
+
const commonVersion = isWorkspacePackage(ctx, commonKey) ? "workspace:*" : "0.1.2";
|
|
1661
|
+
if (existingDeps[commonKey] !== commonVersion) {
|
|
1662
|
+
existingDeps[commonKey] = commonVersion;
|
|
1663
|
+
pkg.dependencies = existingDeps;
|
|
1664
|
+
changes.push(`updated dependency: ${commonKey} to ${commonVersion}`);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1656
1667
|
if (!pkg.repository) {
|
|
1657
1668
|
const repoUrl = detectGitRemoteUrl(ctx.targetDir);
|
|
1658
1669
|
if (repoUrl) {
|
|
@@ -2194,7 +2205,7 @@ async function generateGitignore(ctx) {
|
|
|
2194
2205
|
//#region src/generators/release-ci.ts
|
|
2195
2206
|
const DEFAULT_POST_RELEASE_COMMAND = [
|
|
2196
2207
|
"if [ -n \"$POST_RELEASE_HOOK_URL\" ]; then",
|
|
2197
|
-
" pnpm
|
|
2208
|
+
" pnpm exec bst webhook:send",
|
|
2198
2209
|
" \"$POST_RELEASE_HOOK_URL\"; fi"
|
|
2199
2210
|
].join("");
|
|
2200
2211
|
/** Build env vars for hook secrets to inject into a workflow step. */
|
|
@@ -2208,9 +2219,10 @@ function hookSecretEnv(secrets) {
|
|
|
2208
2219
|
}
|
|
2209
2220
|
/** Step that runs after release, gated on the POST_RELEASE_HOOK_URL secret being set. */
|
|
2210
2221
|
function postReleaseHookStep(opts) {
|
|
2222
|
+
const ifCondition = opts?.condition ?? (opts?.guardMain ? "github.ref == 'refs/heads/main'" : void 0);
|
|
2211
2223
|
return { step: {
|
|
2212
2224
|
name: "Post-release hook",
|
|
2213
|
-
...
|
|
2225
|
+
...ifCondition && { if: ifCondition },
|
|
2214
2226
|
env: hookSecretEnv(opts?.secrets),
|
|
2215
2227
|
run: opts?.command ?? DEFAULT_POST_RELEASE_COMMAND
|
|
2216
2228
|
} };
|
|
@@ -2282,35 +2294,25 @@ function simpleReleaseCommand() {
|
|
|
2282
2294
|
"pnpm exec bst release:simple $FLAGS"
|
|
2283
2295
|
].join("\n");
|
|
2284
2296
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
2291
|
-
},
|
|
2292
|
-
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
2293
|
-
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
2294
|
-
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2297
|
+
function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
|
|
2298
|
+
const releaseEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : {
|
|
2299
|
+
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2300
|
+
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2301
|
+
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
|
|
2295
2302
|
};
|
|
2296
|
-
|
|
2297
|
-
function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets) {
|
|
2303
|
+
const pushedIf = "steps.release.outputs.pushed == 'true'";
|
|
2298
2304
|
const releaseStep = { step: {
|
|
2305
|
+
id: "release",
|
|
2299
2306
|
name: "Release",
|
|
2300
2307
|
env: {
|
|
2301
|
-
...
|
|
2302
|
-
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2303
|
-
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2304
|
-
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
|
|
2305
|
-
},
|
|
2306
|
-
...hookSecretEnv(hookSecrets),
|
|
2308
|
+
...releaseEnv,
|
|
2307
2309
|
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2308
2310
|
},
|
|
2309
2311
|
run: simpleReleaseCommand()
|
|
2310
2312
|
} };
|
|
2311
2313
|
const dockerStep = { step: {
|
|
2312
2314
|
name: "Publish Docker images",
|
|
2313
|
-
if:
|
|
2315
|
+
if: pushedIf,
|
|
2314
2316
|
env: dockerPublishEnv(registryDerivable),
|
|
2315
2317
|
run: "pnpm exec bst docker:publish"
|
|
2316
2318
|
} };
|
|
@@ -2320,10 +2322,10 @@ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, regist
|
|
|
2320
2322
|
...hasDocker ? [dockerStep] : []
|
|
2321
2323
|
];
|
|
2322
2324
|
}
|
|
2323
|
-
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable
|
|
2325
|
+
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
|
|
2324
2326
|
switch (strategy) {
|
|
2325
2327
|
case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
|
|
2326
|
-
case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable
|
|
2328
|
+
case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
|
|
2327
2329
|
default: return null;
|
|
2328
2330
|
}
|
|
2329
2331
|
}
|
|
@@ -2346,7 +2348,7 @@ async function generateReleaseCi(ctx) {
|
|
|
2346
2348
|
const hasDocker = hasDockerPackages(ctx);
|
|
2347
2349
|
const registryDerivable = hasRepositoryUrl(ctx);
|
|
2348
2350
|
const hookSecrets = getPostReleaseHookSecrets(ctx);
|
|
2349
|
-
const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable
|
|
2351
|
+
const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
|
|
2350
2352
|
if (!steps) return {
|
|
2351
2353
|
filePath,
|
|
2352
2354
|
action: "skipped",
|
|
@@ -2356,6 +2358,11 @@ async function generateReleaseCi(ctx) {
|
|
|
2356
2358
|
command: getPostReleaseHookCommand(ctx),
|
|
2357
2359
|
secrets: hookSecrets
|
|
2358
2360
|
}));
|
|
2361
|
+
else if (ctx.config.releaseStrategy === "simple") steps.push(postReleaseHookStep({
|
|
2362
|
+
command: getPostReleaseHookCommand(ctx),
|
|
2363
|
+
secrets: hookSecrets,
|
|
2364
|
+
condition: "steps.release.outputs.pushed == 'true'"
|
|
2365
|
+
}));
|
|
2359
2366
|
const on = ctx.config.releaseStrategy === "simple" ? simpleWorkflowDispatchTrigger() : { workflow_dispatch: null };
|
|
2360
2367
|
const content = buildWorkflowYaml({
|
|
2361
2368
|
ci: ctx.config.ci,
|
|
@@ -2391,8 +2398,9 @@ async function generateReleaseCi(ctx) {
|
|
|
2391
2398
|
//#endregion
|
|
2392
2399
|
//#region src/generators/ci.ts
|
|
2393
2400
|
/** Build the release step for the check job (changesets strategy). */
|
|
2394
|
-
function changesetsReleaseStep(ci, publishesNpm
|
|
2401
|
+
function changesetsReleaseStep(ci, publishesNpm) {
|
|
2395
2402
|
if (ci === "github") return { step: {
|
|
2403
|
+
id: "release",
|
|
2396
2404
|
uses: "changesets/action@v1",
|
|
2397
2405
|
if: "github.ref == 'refs/heads/main'",
|
|
2398
2406
|
with: {
|
|
@@ -2405,6 +2413,7 @@ function changesetsReleaseStep(ci, publishesNpm, hookSecrets) {
|
|
|
2405
2413
|
}
|
|
2406
2414
|
} };
|
|
2407
2415
|
return { step: {
|
|
2416
|
+
id: "release",
|
|
2408
2417
|
name: "Release",
|
|
2409
2418
|
if: "github.ref == 'refs/heads/main'",
|
|
2410
2419
|
env: {
|
|
@@ -2412,7 +2421,6 @@ function changesetsReleaseStep(ci, publishesNpm, hookSecrets) {
|
|
|
2412
2421
|
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2413
2422
|
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
2414
2423
|
...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") },
|
|
2415
|
-
...hookSecretEnv(hookSecrets),
|
|
2416
2424
|
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2417
2425
|
},
|
|
2418
2426
|
run: "pnpm exec bst release:changesets"
|
|
@@ -2462,14 +2470,22 @@ async function generateCi(ctx) {
|
|
|
2462
2470
|
const publishesNpm = ctx.config.publishNpm === true;
|
|
2463
2471
|
const hookCommand = getPostReleaseHookCommand(ctx);
|
|
2464
2472
|
const hookSecrets = getPostReleaseHookSecrets(ctx);
|
|
2465
|
-
const
|
|
2473
|
+
const hasDocker = isChangesets && hasDockerPackages(ctx);
|
|
2474
|
+
const registryDerivable = hasDocker && hasRepositoryUrl(ctx);
|
|
2475
|
+
const publishedIf = "steps.release.outputs.published == 'true'";
|
|
2466
2476
|
const steps = [
|
|
2467
2477
|
...checkSteps(nodeVersionYaml, isChangesets && publishesNpm),
|
|
2468
|
-
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm
|
|
2469
|
-
...
|
|
2478
|
+
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm)] : [],
|
|
2479
|
+
...hasDocker ? [{ step: {
|
|
2480
|
+
name: "Publish Docker images",
|
|
2481
|
+
if: publishedIf,
|
|
2482
|
+
env: dockerPublishEnv(registryDerivable),
|
|
2483
|
+
run: "pnpm exec bst docker:publish"
|
|
2484
|
+
} }] : [],
|
|
2485
|
+
...isChangesets ? [postReleaseHookStep({
|
|
2470
2486
|
command: hookCommand,
|
|
2471
2487
|
secrets: hookSecrets,
|
|
2472
|
-
|
|
2488
|
+
condition: publishedIf
|
|
2473
2489
|
})] : []
|
|
2474
2490
|
];
|
|
2475
2491
|
const content = buildWorkflowYaml({
|
|
@@ -2718,7 +2734,7 @@ function writeIfChanged(ctx, filePath, content, label) {
|
|
|
2718
2734
|
};
|
|
2719
2735
|
}
|
|
2720
2736
|
async function generateClaudeHooks(ctx) {
|
|
2721
|
-
return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.6.3\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{existsSync as t,readFileSync as n,readdirSync as r,statSync as i}from\"node:fs\";import*as a from\"node:path\";import{dirname as o,join as s,resolve as c}from\"node:path\";import{fileURLToPath as l}from\"node:url\";import{execFileSync as u}from\"node:child_process\";Object.freeze({status:`aborted`});function d(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 f=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const p={};function m(e){return e&&Object.assign(p,e),p}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 re=Symbol(`evaluating`);function h(e,t,n){let r;Object.defineProperty(e,t,{get(){if(r!==re)return r===void 0&&(r=re,r=n()),r},set(n){Object.defineProperty(e,t,{value:n})},configurable:!0})}const ie=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function g(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ae(e){if(g(e)===!1)return!1;let t=e.constructor;if(t===void 0||typeof t!=`function`)return!0;let n=t.prototype;return!(g(n)===!1||Object.prototype.hasOwnProperty.call(n,`isPrototypeOf`)===!1)}function oe(e,t,n){let r=new e._zod.constr(t??e._zod.def);return(!t||n?.parent)&&(r._zod.parent=e),r}function _(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 se(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})},ce=d(`$ZodError`,S),C=d(`$ZodError`,S,{Parent:Error}),le=(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 f;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>x(e,a,m())));throw ie(t,i?.callee),t}return o.value})(C),ue=(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,m())));throw ie(t,i?.callee),t}return o.value})(C),w=(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 f;return a.issues.length?{success:!1,error:new(e??ce)(a.issues.map(e=>x(e,i,m())))}:{success:!0,data:a.value}})(C),T=(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,m())))}:{success:!0,data:a.value}})(C),de=e=>{let t=e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??``}}`:`[\\\\s\\\\S]*`;return RegExp(`^${t}$`)},fe=/^-?\\d+(?:\\.\\d+)?$/,pe={major:4,minor:3,patch:6},E=d(`$ZodType`,(e,t)=>{var n;e??={},e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=pe;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 f;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 f;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 f;return o.then(e=>t(e,r,a))}return t(o,r,a)}}h(e,`~standard`,()=>({validate:t=>{try{let n=w(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return T(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),me=d(`$ZodString`,(e,t)=>{E.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??de(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}}),he=d(`$ZodUnknown`,(e,t)=>{E.init(e,t),e._zod.parse=e=>e});function ge(e,t,n){e.issues.length&&t.issues.push(...y(n,e.issues)),t.value[n]=e.value}const _e=d(`$ZodArray`,(e,t)=>{E.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!Array.isArray(i))return n.issues.push({expected:`array`,code:`invalid_type`,input:i,inst:e}),n;n.value=Array(i.length);let a=[];for(let e=0;e<i.length;e++){let o=i[e],s=t.element._zod.run({value:o,issues:[]},r);s instanceof Promise?a.push(s.then(t=>ge(t,n,e))):ge(s,n,e)}return a.length?Promise.all(a).then(()=>n):n}});function D(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 ve(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=se(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(n)}}function ye(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=>D(e,n,i,t,u))):D(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 be=d(`$ZodObject`,(e,t)=>{if(E.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(()=>ve(t));h(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=g,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=>D(n,t,e,s,r))):D(i,t,e,s,r)}return i?ye(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),xe=d(`$ZodRecord`,(e,t)=>{E.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!ae(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`&&fe.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,m())),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 Se(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const Ce=d(`$ZodOptional`,(e,t)=>{E.init(e,t),e._zod.optin=`optional`,e._zod.optout=`optional`,h(e._zod,`values`,()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0),h(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=>Se(t,e.value)):Se(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var we,Te=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 Ee(){return new Te}(we=globalThis).__zod_globalRegistry??(we.__zod_globalRegistry=Ee()),globalThis.__zod_globalRegistry;function De(e,t){return new e({type:`string`,..._(t)})}function Oe(e){return new e({type:`unknown`})}const O=d(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);E.init(e,t),e.def=t,e.type=t.type,e.parse=(t,n)=>le(e,t,n,{callee:e.parse}),e.safeParse=(t,n)=>w(e,t,n),e.parseAsync=async(t,n)=>ue(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>T(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)=>oe(e,t,n),e.brand=()=>e,e.register=((t,n)=>(t.add(e,n),e)),e.apply=t=>t(e)}),ke=d(`ZodMiniString`,(e,t)=>{me.init(e,t),O.init(e,t)});function k(e){return De(ke,e)}const Ae=d(`ZodMiniUnknown`,(e,t)=>{he.init(e,t),O.init(e,t)});function A(){return Oe(Ae)}const je=d(`ZodMiniArray`,(e,t)=>{_e.init(e,t),O.init(e,t)});function Me(e,t){return new je({type:`array`,element:e,..._(t)})}const Ne=d(`ZodMiniObject`,(e,t)=>{be.init(e,t),O.init(e,t),h(e,`shape`,()=>t.shape)});function j(e,t){return new Ne({type:`object`,shape:e??{},..._(t)})}const Pe=d(`ZodMiniRecord`,(e,t)=>{xe.init(e,t),O.init(e,t)});function M(e,t,n){return new Pe({type:`record`,keyType:e,valueType:t,..._(n)})}const Fe=d(`ZodMiniOptional`,(e,t)=>{Ce.init(e,t),O.init(e,t)});function N(e){return new Fe({type:`optional`,innerType:e})}const Ie=j({session_id:k(),transcript_path:k(),cwd:k(),permission_mode:k(),hook_event_name:k(),tool_name:k(),tool_input:M(k(),A())});async function Le(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t=JSON.parse(e.join(``)),n=Ie.safeParse(t);return n.success||(process.stderr.write(`[bst-hook] invalid hook input: ${n.error.message}\\n`),process.exit(0)),n.data}const Re=j({command:k(),description:N(k())}),ze=j({file_path:k()}),Be=j({pattern:k(),path:N(k())});function Ve(e){let t=Re.safeParse(e);return t.success?t.data:null}function He(e){let t=ze.safeParse(e);return t.success?t.data:null}function Ue(e){let t=Be.safeParse(e);return t.success?t.data:null}function P(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function We(){let e=a.dirname(l(import.meta.url));return a.join(e,`bst-hook.audit.local.jsonl`)}function Ge(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let Ke=!1;function qe(t){try{let n=We();Ke||=(Ge(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 F(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 Je(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]===`&`){I(t,n,`&&`),n=``,r+=2;continue}if(o===`&`&&!(r+1<e.length&&e[r+1]===`>`)&&!(r>0&&(e[r-1]===`>`||e[r-1]===`<`))){I(t,n,`&`),n=``,r++;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){I(t,n,`||`),n=``,r+=2;continue}if(o===`|`){I(t,n,`|`),n=``,r++;continue}if(o===`;`){I(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function I(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const L=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function Ye(e,t,n=!1){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(`>(`)&&!L.has(t))return{safe:!1,reason:`process substitution`}}if(!n)for(let t=0;t<e.length;t++){let n=e[t];if(!L.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 Xe=[`.sample`,`.example`,`.template`],Ze=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function Qe(e){return Xe.some(t=>e.endsWith(t))}function R(e){return Qe(a.basename(e))?!1:Ze.test(e)}function $e(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&R(e))}const et=new Set([`status`,`log`,`diff`,`show`,`blame`,`grep`,`shortlog`,`range-diff`,`describe`,`branch`,`remote`,`fetch`,`ls-files`,`ls-tree`,`ls-remote`,`show-ref`,`for-each-ref`,`rev-parse`,`rev-list`,`cat-file`,`diff-tree`,`name-rev`,`merge-base`,`check-ignore`]),tt=new Set([`push`,`add`,`commit`,`reset`,`merge`,`rebase`,`cherry-pick`,`checkout`,`switch`,`stash`,`tag`,`revert`,`clean`,`rm`,`mv`]),nt=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]),rt=new Set([`-C`,`-c`,`--git-dir`,`--work-tree`]);function z(e){let t=e.slice(1);for(let e=0;e<t.length;e++){let n=t[e];if(rt.has(n)){e++;continue}if(!n.startsWith(`-`))return n}}function it(e){for(let t of e.slice(1))if(nt.has(t)||t===`-o`||t.startsWith(`--output`)||t.startsWith(`--exec-path=`)||t.startsWith(`--pager=`)||t.startsWith(`--upload-pack=`)||t.startsWith(`--receive-pack=`)||t.startsWith(`-o=`)||t.startsWith(`--output=`))return!1;let t=z(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return et.has(t)}function at(e){let t=z(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];if(t===`list`||t===void 0)return!1}return tt.has(t)}const ot=new Set([`push`,`pop`,`apply`,`show`]);function B(e){let t=z(e);if(!t)return!1;if(t===`add`)return!0;if(t===`checkout`)return e.slice(e.indexOf(`checkout`)+1).some(e=>e===`-b`||e===`-B`);if(t===`switch`)return!0;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===void 0?!0:ot.has(t)}return!1}const V=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(`.`)),st=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function ct(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!V.has(e))){n=e;break}return n?st.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const lt=new Set([`view`,`info`,`show`]);function ut(e){let t=e[1];return t!==void 0&<.has(t)}function dt(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 H(e,t){for(let n of e.slice(1)){if(n.startsWith(`-`)){if(n===`-t`||n.startsWith(`--target-directory`))return!1;continue}if(a.basename(n).startsWith(`.`)||n.startsWith(`~/`)||n===`~`)return!1;let e=a.resolve(t.cwd,n);if(e!==t.cwd&&!e.startsWith(t.cwd+a.sep))return!1}return!0}const ft=new Set([`-exec`,`-execdir`,`-ok`,`-okdir`,`-delete`,`-fprint`,`-fprint0`,`-fprintf`,`-fls`]);function pt(e){return!e.some(e=>ft.has(e))}function mt(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function ht(e){let t=e.findIndex(e=>a.basename(e)===`rm`);if(t===-1)return!1;let n=e.slice(t+1),r=n.filter(e=>e.startsWith(`-`)&&!e.startsWith(`--`)),i=n.filter(e=>e.startsWith(`--`)),o=r.some(e=>/[rR]/.test(e)),s=i.includes(`--recursive`);return o||s}function gt(e){return e.some(e=>e===`777`||e===`0777`)}function _t(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 vt(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 yt=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>ht(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=>_t(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>vt(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`git-write`,match:/^git$/,action:`deny`,when:e=>at(e)&&!B(e),reason:`Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.`},{name:`chmod-777`,match:/^chmod$/,action:`deny`,when:e=>gt(e),reason:`Don't use 777 permissions. Use more restrictive permissions.`},{name:`env-file-read`,match:/^(cat|head|tail|less|more|grep)$/,action:`deny`,when:e=>$e(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.\"}],U=[{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=>ut(e)},{name:`test`,match:/^test$/,action:`allow`},{name:`bracket-test`,match:/^\\[$/,action:`allow`},{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=>pt(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>mt(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>it(e)},{name:`git-safe-write`,match:/^git$/,action:`allow`,when:e=>B(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>ct(e)},{name:`cp`,match:/^cp$/,action:`allow`,when:(e,t)=>H(e,t)},{name:`mkdir`,match:/^mkdir$/,action:`allow`,when:(e,t)=>H(e,t)},{name:`touch`,match:/^touch$/,action:`allow`,when:(e,t)=>H(e,t)}],W=`.claude/hooks/bst-hook.settings.json`,bt=j({allow:Me(k())});function xt(e,t){try{return u(`git`,[`ls-files`,`--error-unmatch`,t],{cwd:e,stdio:`ignore`}),u(`git`,[`diff`,`--quiet`,`HEAD`,`--`,t],{cwd:e,stdio:`ignore`}),!0}catch{return!1}}function St(e){let r=Dt(e)??e,i=s(r,W);if(!t(i)||!xt(r,W))return[];try{let e=n(i,`utf-8`),t=JSON.parse(e),r=bt.safeParse(t);return r.success?r.data.allow.filter(e=>typeof e==`string`&&e.length>0):[]}catch{return[]}}function Ct(e,t,n){let r=n.split(/\\s+/).filter(Boolean);if(r.length===0||r[0]!==e)return!1;let i=r.slice(1),a=t.slice(1);for(let e=0;e<i.length;e++)if(e>=a.length||a[e]!==i[e])return!1;return!0}const wt=j({scripts:M(k(),A())});function G(e,t){let n=Nt(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 Tt(e,t,n){let r=kt(t,n);if(!r)return null;let i=G(e,r);return i?{...i,pkgDir:r}:null}function Et(e,t){let n=Ot(t);if(!n)return null;let r=[];for(let t of n){let n=G(e,t);n&&r.push({...n,pkgDir:t})}return r}function Dt(e){let n=c(e);for(;;){if(t(s(n,`pnpm-workspace.yaml`)))return n;let e=o(n);if(e===n)return null;n=e}}function Ot(e){let t=At(e);if(!t)return null;let n=[];for(let a of t){if(!a.endsWith(`/*`))continue;let t=c(e,a.slice(0,-2)),o;try{o=r(t)}catch{continue}for(let e of o){let r=s(t,e);try{if(!i(r).isDirectory())continue}catch{continue}n.push(r)}}return n.length>0?n:null}function kt(e,t){let n=Ot(t);if(!n)return null;for(let t of n){let n=Mt(t);if(n===e||n===`@${e}`||t.split(`/`).pop()===e||n&&(n.startsWith(`@`)?n.split(`/`)[1]:n)===e)return t}return null}function At(e){try{let t=n(s(e,`pnpm-workspace.yaml`),`utf-8`).split(`\n`),r=t.findIndex(e=>/^packages\\s*:/.test(e));if(r===-1)return null;let i=[];for(let e=r+1;e<t.length;e++){let n=t[e],r=/^\\s+-\\s+(.+)$/.exec(n);if(!r)break;i.push(r[1].trim().replace(/['\"`]/g,``))}return i.length>0?i:null}catch{return null}}const jt=j({name:k()});function Mt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=jt.safeParse(JSON.parse(t));return r.success?r.data.name:null}catch{return null}}function Nt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=wt.safeParse(JSON.parse(t));return r.success?r.data.scripts:null}catch{return null}}function Pt(e){return xt(e,`package.json`)}const Ft=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`]),It=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function Lt(e){return Ft.has(e)?!0:It.some(t=>e.startsWith(t))}function Rt(e,t){let n=F(e);if(!n.ok||n.tokens.length!==2)return null;let r=n.tokens;if(r[0]!==`cd`)return null;let i=r[1];return a.isAbsolute(i)?i:a.resolve(t.cwd,i)}function K(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=Je(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=zt(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 q(e,t){let n=e.pkgDir??t.cwd,r=t.insideResolvedScript??Pt(n)?{...t,insideResolvedScript:!0}:t;if(e.pre){let t=K(e.pre,r);if(t.action!==`allow`)return t}let i=K(e.main,r);if(i.action!==`allow`)return i;if(e.post){let t=K(e.post,r);if(t.action!==`allow`)return t}return`allow`}function zt(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=F(e);if(!r.ok)return{action:`passthrough`,rule:null,reason:r.error};let i=r.tokens;if(i.length===0)return{action:`passthrough`,rule:null,reason:null};let o=0;for(;o<i.length&&/^\\w+=/.test(i[o]);){let e=i[o].split(`=`)[0];if(Lt(e))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${e}`};o++}if(o>=i.length)return{action:`allow`,rule:`env-only`,reason:null};i=i.slice(o);let s=Ye(i,e,t.insideResolvedScript);if(!s.safe)return{action:`passthrough`,rule:null,reason:s.reason};if(!t.insideResolvedScript)for(let e=0;e<i.length;e++){let t=i[e];if(!(t===`2>&1`||t===`2>/dev/null`||t===`>/dev/null`||t===`1>/dev/null`)){if(t===`>`||t===`>>`){if(i[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=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);i=i.filter((e,t)=>!(c.has(e)||(e===`>`||e===`>>`)&&i[t+1]===`/dev/null`||e===`/dev/null`&&t>0&&(i[t-1]===`>`||i[t-1]===`>>`)));let l=a.basename(i[0]);if(l===`command`&&i.length>1)return K(i.slice(1).join(` `),{...t,depth:t.depth+1});if(l===`env`){let e=1;for(;e<i.length&&/^\\w+=/.test(i[e]);){let t=i[e].split(`=`)[0];if(Lt(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=i.length?{action:`allow`,rule:`env-only`,reason:null}:K(i.slice(e).join(` `),{...t,depth:t.depth+1})}if(l===`pnpm`)return Bt(i,t);if(l===`npm`)return Ht(i);if(l===`bash`||l===`sh`||l===`zsh`)return Gt(i,t);if(l===`xargs`)return qt(i,t);if(l===`node`&&(i.includes(`-e`)||i.includes(`-p`)||i.includes(`--eval`)||i.includes(`--print`)))return{action:`passthrough`,rule:null,reason:`node eval`};if(l===`mv`)return{action:`passthrough`,rule:null,reason:`mv`};for(let e of yt)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`deny`,rule:e.name,reason:e.reason??null};for(let e of U)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`allow`,rule:e.name,reason:null};if(t.repoAllowPatterns){for(let e of t.repoAllowPatterns)if(Ct(l,i,e))return{action:`allow`,rule:`repo:${l}`,reason:null}}return t.insideResolvedScript?{action:`allow`,rule:`resolved-script`,reason:null}:{action:`passthrough`,rule:null,reason:`no rule matched: ${l}`}}function Bt(e,t){let n=e.slice(1),r,i=-1,o=!1,s=!1,c;for(let a=0;a<n.length;a++){let l=n[a];if(l===`--filter`||l.startsWith(`--filter=`))return Ut(e,n,a,t);if(l===`-r`||l===`--recursive`){o=!0;continue}if(l===`-w`||l===`--workspace-root`){s=!0;continue}if(l===`-C`||l===`--dir`){c=n[++a];continue}if(l.startsWith(`-C=`)||l.startsWith(`--dir=`)){c=l.split(`=`,2)[1];continue}if(!(l.startsWith(`-`)&&!V.has(l))){r=l,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`}:K(e.join(` `),{...t,depth:t.depth+1})}let l;if(r===`run`){let e=n[i+1];if(!e||e.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm run without script name`};l=e}else if(!V.has(r))l=r;else{for(let n of U)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}}if(o)return Vt(l,t);if(s){let e=Dt(t.cwd);return e?J(l,{...t,cwd:e}):{action:`passthrough`,rule:null,reason:`workspace root not found`}}if(c){let e=a.resolve(t.cwd,c);return J(l,{...t,cwd:e})}return J(l,t)}function J(e,t){let n=G(e,t.cwd);if(!n)return{action:`passthrough`,rule:null,reason:`script resolution failed`};let r=q(n,{...t,depth:t.depth+1});return r===`allow`?{action:`allow`,rule:`pnpm-script:${e}`,reason:null}:r}function Vt(e,t){let n=Et(e,t.cwd);if(!n||n.length===0)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};for(let e of n){let n=q(e,{...t,depth:t.depth+1,cwd:e.pkgDir??t.cwd});if(n!==`allow`)return n}return{action:`allow`,rule:`pnpm-recursive:${e}`,reason:null}}function Ht(e){for(let t of U)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:dt(e)}}function Ut(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(`-`)&&!V.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`}:K(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(V.has(c)&&c!==`run`)return{action:`passthrough`,rule:null,reason:null};let l=Tt(c,a,r.cwd);if(!l)return{action:`passthrough`,rule:null,reason:`filtered script resolution failed`};let u=q(l,{...r,depth:r.depth+1,cwd:l.pkgDir??r.cwd});return u===`allow`?{action:`allow`,rule:`pnpm-filter:${a}:${c}`,reason:null}:u}const Wt=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function Gt(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(`-`)||Wt.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?K(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 Y=new Set([`-I`,`-d`,`-n`,`-P`,`-s`,`-L`,`-E`,`-a`,`--max-args`,`--max-procs`,`--delimiter`,`--replace`,`--max-lines`,`--max-chars`,`--arg-file`,`--process-slot-var`]),Kt=new Set([`-0`,`--null`,`-t`,`--verbose`,`-r`,`--no-run-if-empty`,`-p`,`--interactive`,`-x`,`--exit`,`--show-limits`]);function qt(e,t){let n=1;for(;n<e.length;){let t=e[n];if(!t.startsWith(`-`))break;if(Y.has(t)){n+=2;continue}let r=t.indexOf(`=`);if(r!==-1){let e=t.slice(0,r);if(Y.has(e)){n++;continue}}if(t.length>2&&Y.has(t.slice(0,2))){n++;continue}if(Kt.has(t)){n++;continue}return{action:`passthrough`,rule:null,reason:`xargs with unrecognised flags`}}return n>=e.length?{action:`passthrough`,rule:null,reason:`xargs without command`}:K(e.slice(n).join(` `),{...t,depth:t.depth+1})}const Jt=new Set([`Read`,`Glob`,`Grep`]),X=new Set([`Write`,`Edit`]),Yt=[{name:`env-file-read`,tools:Jt,action:`deny`,when:e=>R(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-write`,tools:X,action:`deny`,when:e=>R(e),reason:`Don't write to .env files. These contain secrets.`},{name:`hooks-folder-write`,tools:X,action:`deny`,when:(e,t)=>{let n=a.resolve(t.cwd,e),r=a.join(t.cwd,`.claude`,`hooks`);return n.startsWith(r+a.sep)||n===r},reason:`Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.`},{name:`claude-settings-write`,tools:X,action:`deny`,when:(e,t)=>a.resolve(t.cwd,e)===a.join(t.cwd,`.claude`,`settings.json`),reason:`Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.`},{name:`outside-cwd-write`,tools:X,action:`deny`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n!==t.cwd&&!n.startsWith(t.cwd+a.sep)},reason:`Don't write files outside the project directory.`}],Xt=[{name:`cwd-read`,tools:Jt,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}},{name:`cwd-write`,tools:X,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}}],Zt=new Set([`Read`,`Write`,`Edit`]),Qt=new Set([`Glob`,`Grep`]);function $t(e,t,n){return Zt.has(e)?He(t)?.file_path??null:Qt.has(e)?Ue(t)?.path??n:null}function en(e,t,n){let r=$t(e,t,n.cwd);if(r===null)return{action:`passthrough`,rule:null,reason:`unknown tool input shape`};for(let t of Yt)if(t.tools.has(e)&&t.when(r,n))return{action:`deny`,rule:t.name,reason:t.reason??null};for(let t of Xt)if(t.tools.has(e)&&t.when(r,n))return{action:`allow`,rule:t.name,reason:null};return{action:`passthrough`,rule:null,reason:`no file rule matched`}}const Z=await Le();Z.hook_event_name!==`PreToolUse`&&process.exit(0),new Set([`Bash`,`Read`,`Write`,`Edit`,`Glob`,`Grep`]).has(Z.tool_name)||process.exit(0);const tn=St(Z.cwd);let Q,$;if(Z.tool_name===`Bash`){let e=Ve(Z.tool_input);e||process.exit(0),Q=K(e.command,{cwd:Z.cwd,depth:0,repoAllowPatterns:tn}),$=e.command}else{let e=process.env.CLAUDE_PROJECT_DIR??Z.cwd;Q=en(Z.tool_name,Z.tool_input,{cwd:e});let t=String(Z.tool_input.file_path??Z.tool_input.path??``);$=`${Z.tool_name} ${t}`}qe({sessionId:Z.session_id,cwd:Z.cwd,toolName:Z.tool_name,command:$,decision:Q.action,rule:Q.rule,reason:Q.reason}),Q.action===`allow`?P(`allow`):Q.action===`deny`&&P(`deny`,Q.reason);export{};", "bst-hook.mjs"), writeIfChanged(ctx, GUIDE_PATH, "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that evaluates Bash commands\nand file tool operations before execution. Commands fall into three tiers:\n\n- **Denied** — blocked with a redirect message\n- **Allowed** — auto-approved, no prompt\n- **Passthrough** — falls through to Claude Code's normal permission prompt\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **rm-recursive**: Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-reset-hard**: Don't `reset --hard` on main. Create a branch first.\n- **git-write**: Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **npx-redirect**: This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\n\n### File tools (Read, Write, Edit, Glob, Grep)\n\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **hooks-folder-write**: Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\n- **claude-settings-write**: Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\n- **outside-cwd-write**: Don't write files outside the project directory.\n\n## Auto-approved commands\n\nThese run without prompting.\n\n### Bash\n\n- **Test runners**: vitest, jest\n- **Type checking**: tsc\n- **Build tools**: tsdown\n- **Linters & formatters**: oxlint, oxfmt, eslint, prettier, biome, knip\n- **Read-only commands**: cat, head, tail, grep, ls, wc, uniq, diff, echo, pwd, which, dirname, basename, realpath\n- **Package managers (read-only)**: npm (view, info, show only)\n- **Shell builtins**: test, bracket-test\n- **Navigation**: cd, true, colon\n- **Conditional commands**: find (without -exec, -delete), sort (without -o / --output)\n- **Git read operations**: git (status, log, diff, show, blame, branch, remote, fetch, rev-parse, etc.)\n- **Git safe writes**: git-safe-write (add, switch -c, checkout -b, stash push/pop/apply/show)\n- **pnpm (safe built-ins)**: pnpm (list, why, view, info, show, outdated, install (bare only))\n- **File operations (within project)**: cp (paths must be within project directory), mkdir (paths must be within project directory), touch (paths must be within project directory)\n\n### File tools\n\n- **Read/Glob/Grep**: Allowed within project directory\n- **Write/Edit**: Allowed within project directory\n\n## Passthrough\n\nEverything not listed above triggers Claude Code's normal permission prompt.\nCommon examples:\n\n- `pnpm add <pkg>`, `pnpm dlx <pkg>` (dependency changes)\n- `mv` (file moves)\n- `node`, `npx` scripts\n- Commands with output redirection (`> file`)\n- Any unrecognized command\n\n## How commands are evaluated\n\n- **Pipelines and chains**: every segment must be allowed; any deny blocks the whole command.\n- **pnpm scripts**: `pnpm <script>` resolves against `package.json` (including pre/post hooks and `--filter` workspaces) and evaluates the resolved command recursively.\n- **cd propagation**: `cd dir && cmd` evaluates `cmd` relative to the new directory.\n- **Audit log**: all decisions are logged to `.claude/hooks/bst-hook.audit.local.jsonl`.\n", "bst-hook-guide.md")];
|
|
2737
|
+
return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.6.4\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{existsSync as t,readFileSync as n,readdirSync as r,statSync as i}from\"node:fs\";import*as a from\"node:path\";import{dirname as o,join as s,resolve as c}from\"node:path\";import{fileURLToPath as l}from\"node:url\";import{execFileSync as u}from\"node:child_process\";Object.freeze({status:`aborted`});function d(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 f=class extends Error{constructor(){super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`)}};const ee={};function p(e){return e&&Object.assign(ee,e),ee}function te(e,t){return typeof t==`bigint`?t.toString():t}function ne(e){return{get value(){{let t=e();return Object.defineProperty(this,`value`,{value:t}),t}throw Error(`cached value already set`)}}}function re(e){let t=e.startsWith(`^`)?1:0,n=e.endsWith(`$`)?e.length-1:e.length;return e.slice(t,n)}const ie=Symbol(`evaluating`);function m(e,t,n){let r;Object.defineProperty(e,t,{get(){if(r!==ie)return r===void 0&&(r=ie,r=n()),r},set(n){Object.defineProperty(e,t,{value:n})},configurable:!0})}const ae=`captureStackTrace`in Error?Error.captureStackTrace:(...e)=>{};function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function oe(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 se(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 ce(e){return Object.keys(e).filter(t=>e[t]._zod.optin===`optional`&&e[t]._zod.optout===`optional`)}-Number.MAX_VALUE,Number.MAX_VALUE;function _(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 v(e,t){return t.map(t=>{var n;return(n=t).path??(n.path=[]),t.path.unshift(e),t})}function y(e){return typeof e==`string`?e:e?.message}function b(e,t,n){let r={...e,path:e.path??[]};return e.message||(r.message=y(e.inst?._zod.def?.error?.(e))??y(t?.error?.(e))??y(n.customError?.(e))??y(n.localeError?.(e))??`Invalid input`),delete r.inst,delete r.continue,t?.reportInput||delete r.input,r}const le=(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,te,2),Object.defineProperty(e,`toString`,{value:()=>e.message,enumerable:!1})},ue=d(`$ZodError`,le),x=d(`$ZodError`,le,{Parent:Error}),de=(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 f;if(o.issues.length){let t=new(i?.Err??e)(o.issues.map(e=>b(e,a,p())));throw ae(t,i?.callee),t}return o.value})(x),fe=(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=>b(e,a,p())));throw ae(t,i?.callee),t}return o.value})(x),S=(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 f;return a.issues.length?{success:!1,error:new(e??ue)(a.issues.map(e=>b(e,i,p())))}:{success:!0,data:a.value}})(x),C=(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=>b(e,i,p())))}:{success:!0,data:a.value}})(x),pe=e=>{let t=e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??``}}`:`[\\\\s\\\\S]*`;return RegExp(`^${t}$`)},me=/^-?\\d+(?:\\.\\d+)?$/,he={major:4,minor:3,patch:6},w=d(`$ZodType`,(e,t)=>{var n;e??={},e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=he;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=_(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 f;if(i||o instanceof Promise)i=(i??Promise.resolve()).then(async()=>{await o,e.issues.length!==t&&(r||=_(e,t))});else{if(e.issues.length===t)continue;r||=_(e,t)}}return i?i.then(()=>e):e},n=(n,i,a)=>{if(_(n))return n.aborted=!0,n;let o=t(i,r,a);if(o instanceof Promise){if(a.async===!1)throw new f;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 f;return o.then(e=>t(e,r,a))}return t(o,r,a)}}m(e,`~standard`,()=>({validate:t=>{try{let n=S(e,t);return n.success?{value:n.data}:{issues:n.error?.issues}}catch{return C(e,t).then(e=>e.success?{value:e.data}:{issues:e.error?.issues})}},vendor:`zod`,version:1}))}),ge=d(`$ZodString`,(e,t)=>{w.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??pe(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}}),_e=d(`$ZodUnknown`,(e,t)=>{w.init(e,t),e._zod.parse=e=>e});function ve(e,t,n){e.issues.length&&t.issues.push(...v(n,e.issues)),t.value[n]=e.value}const ye=d(`$ZodArray`,(e,t)=>{w.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!Array.isArray(i))return n.issues.push({expected:`array`,code:`invalid_type`,input:i,inst:e}),n;n.value=Array(i.length);let a=[];for(let e=0;e<i.length;e++){let o=i[e],s=t.element._zod.run({value:o,issues:[]},r);s instanceof Promise?a.push(s.then(t=>ve(t,n,e))):ve(s,n,e)}return a.length?Promise.all(a).then(()=>n):n}});function T(e,t,n,r,i){if(e.issues.length){if(i&&!(n in r))return;t.issues.push(...v(n,e.issues))}e.value===void 0?n in r&&(t.value[n]=void 0):t.value[n]=e.value}function be(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=ce(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(n)}}function xe(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=>T(e,n,i,t,u))):T(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 Se=d(`$ZodObject`,(e,t)=>{if(w.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=ne(()=>be(t));m(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=>T(n,t,e,s,r))):T(i,t,e,s,r)}return i?xe(c,s,t,o,n.value,e):c.length?Promise.all(c).then(()=>t):t}}),Ce=d(`$ZodRecord`,(e,t)=>{w.init(e,t),e._zod.parse=(n,r)=>{let i=n.value;if(!oe(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(...v(e,t.issues)),n.value[e]=t.value})):(o.issues.length&&n.issues.push(...v(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`&&me.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=>b(e,r,p())),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(...v(o,e.issues)),n.value[s.value]=e.value})):(c.issues.length&&n.issues.push(...v(o,c.issues)),n.value[s.value]=c.value)}}return a.length?Promise.all(a).then(()=>n):n}});function E(e,t){return e.issues.length&&t===void 0?{issues:[],value:void 0}:e}const we=d(`$ZodOptional`,(e,t)=>{w.init(e,t),e._zod.optin=`optional`,e._zod.optout=`optional`,m(e._zod,`values`,()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0),m(e._zod,`pattern`,()=>{let e=t.innerType._zod.pattern;return e?RegExp(`^(${re(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=>E(t,e.value)):E(r,e.value)}return e.value===void 0?e:t.innerType._zod.run(e,n)}});var D,Te=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 Ee(){return new Te}(D=globalThis).__zod_globalRegistry??(D.__zod_globalRegistry=Ee()),globalThis.__zod_globalRegistry;function De(e,t){return new e({type:`string`,...g(t)})}function Oe(e){return new e({type:`unknown`})}const O=d(`ZodMiniType`,(e,t)=>{if(!e._zod)throw Error(`Uninitialized schema in ZodMiniType.`);w.init(e,t),e.def=t,e.type=t.type,e.parse=(t,n)=>de(e,t,n,{callee:e.parse}),e.safeParse=(t,n)=>S(e,t,n),e.parseAsync=async(t,n)=>fe(e,t,n,{callee:e.parseAsync}),e.safeParseAsync=async(t,n)=>C(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)=>se(e,t,n),e.brand=()=>e,e.register=((t,n)=>(t.add(e,n),e)),e.apply=t=>t(e)}),ke=d(`ZodMiniString`,(e,t)=>{ge.init(e,t),O.init(e,t)});function k(e){return De(ke,e)}const Ae=d(`ZodMiniUnknown`,(e,t)=>{_e.init(e,t),O.init(e,t)});function je(){return Oe(Ae)}const Me=d(`ZodMiniArray`,(e,t)=>{ye.init(e,t),O.init(e,t)});function Ne(e,t){return new Me({type:`array`,element:e,...g(t)})}const Pe=d(`ZodMiniObject`,(e,t)=>{Se.init(e,t),O.init(e,t),m(e,`shape`,()=>t.shape)});function A(e,t){return new Pe({type:`object`,shape:e??{},...g(t)})}const Fe=d(`ZodMiniRecord`,(e,t)=>{Ce.init(e,t),O.init(e,t)});function j(e,t,n){return new Fe({type:`record`,keyType:e,valueType:t,...g(n)})}const Ie=d(`ZodMiniOptional`,(e,t)=>{we.init(e,t),O.init(e,t)});function M(e){return new Ie({type:`optional`,innerType:e})}const Le=A({session_id:k(),transcript_path:k(),cwd:k(),permission_mode:k(),hook_event_name:k(),tool_name:k(),tool_input:j(k(),je())});async function Re(){let e=[];for await(let t of process.stdin)e.push(typeof t==`string`?t:String(t));let t;try{t=JSON.parse(e.join(``))}catch{return{ok:!1,raw:e.join(``).slice(0,200),error:`invalid JSON`}}let n=Le.safeParse(t);return n.success?{ok:!0,data:n.data}:{ok:!1,raw:t,error:n.error.message}}const ze=A({command:k(),description:M(k())}),Be=A({file_path:k()}),Ve=A({pattern:k(),path:M(k())});function He(e){let t=ze.safeParse(e);return t.success?t.data:null}function Ue(e){let t=Be.safeParse(e);return t.success?t.data:null}function We(e){let t=Ve.safeParse(e);return t.success?t.data:null}function N(e,t){let n={hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:e,...e===`deny`&&t?{permissionDecisionReason:t}:{}}};process.stdout.write(JSON.stringify(n))}function Ge(){let e=a.dirname(l(import.meta.url));return a.join(e,`bst-hook.audit.local.jsonl`)}function Ke(t){try{if(e.statSync(t).size>1048576){let n=`${t}.1`;e.renameSync(t,n)}}catch{}}let qe=!1;function P(t){try{let n=Ge();qe||=(Ke(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 F(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 Je(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]===`&`){I(t,n,`&&`),n=``,r+=2;continue}if(o===`&`&&!(r+1<e.length&&e[r+1]===`>`)&&!(r>0&&(e[r-1]===`>`||e[r-1]===`<`))){I(t,n,`&`),n=``,r++;continue}if(o===`|`&&r+1<e.length&&e[r+1]===`|`){I(t,n,`||`),n=``,r+=2;continue}if(o===`|`){I(t,n,`|`),n=``,r++;continue}if(o===`;`){I(t,n,`;`),n=``,r++;continue}}n+=o,r++}let o=n.trim();return o&&t.push({command:o,operator:null}),t}function I(e,t,n){let r=t.trim();r&&e.push({command:r,operator:n})}const L=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);function Ye(e,t,n=!1){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(`>(`)&&!L.has(t))return{safe:!1,reason:`process substitution`}}if(!n)for(let t=0;t<e.length;t++){let n=e[t];if(!L.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 Xe=[`.sample`,`.example`,`.template`],Ze=/(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;function Qe(e){return Xe.some(t=>e.endsWith(t))}function R(e){return Qe(a.basename(e))?!1:Ze.test(e)}function $e(e){return e.slice(1).some(e=>!e.startsWith(`-`)&&R(e))}const et=new Set([`status`,`log`,`diff`,`show`,`blame`,`grep`,`shortlog`,`range-diff`,`describe`,`branch`,`remote`,`fetch`,`ls-files`,`ls-tree`,`ls-remote`,`show-ref`,`for-each-ref`,`rev-parse`,`rev-list`,`cat-file`,`diff-tree`,`name-rev`,`merge-base`,`check-ignore`]),tt=new Set([`push`,`add`,`commit`,`reset`,`merge`,`rebase`,`cherry-pick`,`checkout`,`switch`,`stash`,`tag`,`revert`,`clean`,`rm`,`mv`]),nt=new Set([`-c`,`--exec-path`,`--pager`,`--upload-pack`,`--receive-pack`]),rt=new Set([`-C`,`-c`,`--git-dir`,`--work-tree`]);function z(e){let t=e.slice(1);for(let e=0;e<t.length;e++){let n=t[e];if(rt.has(n)){e++;continue}if(!n.startsWith(`-`))return n}}function it(e){for(let t of e.slice(1))if(nt.has(t)||t===`-o`||t.startsWith(`--output`)||t.startsWith(`--exec-path=`)||t.startsWith(`--pager=`)||t.startsWith(`--upload-pack=`)||t.startsWith(`--receive-pack=`)||t.startsWith(`-o=`)||t.startsWith(`--output=`))return!1;let t=z(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===`list`||t===void 0}return et.has(t)}function at(e){let t=z(e);if(!t)return!1;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];if(t===`list`||t===void 0)return!1}return tt.has(t)}const ot=new Set([`push`,`pop`,`apply`,`show`]);function B(e){let t=z(e);if(!t)return!1;if(t===`add`)return!0;if(t===`checkout`)return e.slice(e.indexOf(`checkout`)+1).some(e=>e===`-b`||e===`-B`);if(t===`switch`)return!0;if(t===`stash`){let t=e[e.indexOf(`stash`)+1];return t===void 0?!0:ot.has(t)}return!1}const V=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(`.`)),st=new Set([`list`,`ls`,`ll`,`la`,`why`,`view`,`info`,`show`,`outdated`]);function ct(e){let t=e.slice(1),n;for(let e of t)if(!(e.startsWith(`-`)&&!V.has(e))){n=e;break}return n?st.has(n)?!0:n===`install`||n===`i`?t.slice(t.indexOf(n)+1).every(e=>e.startsWith(`-`)):!1:!1}const lt=new Set([`view`,`info`,`show`]);function ut(e){let t=e[1];return t!==void 0&<.has(t)}function dt(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 H(e,t){for(let n of e.slice(1)){if(n.startsWith(`-`)){if(n===`-t`||n.startsWith(`--target-directory`))return!1;continue}if(a.basename(n).startsWith(`.`)||n.startsWith(`~/`)||n===`~`)return!1;let e=a.resolve(t.cwd,n);if(e!==t.cwd&&!e.startsWith(t.cwd+a.sep))return!1}return!0}const ft=new Set([`-exec`,`-execdir`,`-ok`,`-okdir`,`-delete`,`-fprint`,`-fprint0`,`-fprintf`,`-fls`]);function pt(e){return!e.some(e=>ft.has(e))}function mt(e){return!e.includes(`-o`)&&!e.some(e=>e.startsWith(`--output`))}function ht(e){let t=e.findIndex(e=>a.basename(e)===`rm`);if(t===-1)return!1;let n=e.slice(t+1),r=n.filter(e=>e.startsWith(`-`)&&!e.startsWith(`--`)),i=n.filter(e=>e.startsWith(`--`)),o=r.some(e=>/[rR]/.test(e)),s=i.includes(`--recursive`);return o||s}function gt(e){return e.some(e=>e===`777`||e===`0777`)}function _t(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 vt(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 yt=[{name:`rm-recursive`,match:/^rm$/,action:`deny`,when:e=>ht(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=>_t(e),reason:\"Use `git push --force-with-lease` instead of `--force`.\"},{name:`git-reset-hard`,match:/^git$/,action:`deny`,when:e=>vt(e),reason:\"Don't `reset --hard` on main. Create a branch first.\"},{name:`git-write`,match:/^git$/,action:`deny`,when:e=>at(e)&&!B(e),reason:`Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.`},{name:`chmod-777`,match:/^chmod$/,action:`deny`,when:e=>gt(e),reason:`Don't use 777 permissions. Use more restrictive permissions.`},{name:`env-file-read`,match:/^(cat|head|tail|less|more|grep)$/,action:`deny`,when:e=>$e(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.\"}],U=[{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=>ut(e)},{name:`test`,match:/^test$/,action:`allow`},{name:`bracket-test`,match:/^\\[$/,action:`allow`},{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=>pt(e)},{name:`sort`,match:/^sort$/,action:`allow`,when:e=>mt(e)},{name:`git`,match:/^git$/,action:`allow`,when:e=>it(e)},{name:`git-safe-write`,match:/^git$/,action:`allow`,when:e=>B(e)},{name:`pnpm`,match:/^pnpm$/,action:`allow`,when:e=>ct(e)},{name:`cp`,match:/^cp$/,action:`allow`,when:(e,t)=>H(e,t)},{name:`mkdir`,match:/^mkdir$/,action:`allow`,when:(e,t)=>H(e,t)},{name:`touch`,match:/^touch$/,action:`allow`,when:(e,t)=>H(e,t)}],bt=`.claude/hooks/bst-hook.settings.json`,xt=A({allow:Ne(k())});function St(e,t){try{return u(`git`,[`ls-files`,`--error-unmatch`,t],{cwd:e,stdio:`ignore`}),u(`git`,[`diff`,`--quiet`,`HEAD`,`--`,t],{cwd:e,stdio:`ignore`}),!0}catch{return!1}}function Ct(e){let r=Ot(e)??e,i=s(r,bt);if(!t(i)||!St(r,bt))return[];try{let e=n(i,`utf-8`),t=JSON.parse(e),r=xt.safeParse(t);return r.success?r.data.allow.filter(e=>typeof e==`string`&&e.length>0):[]}catch{return[]}}function wt(e,t,n){let r=n.split(/\\s+/).filter(Boolean);if(r.length===0||r[0]!==e)return!1;let i=r.slice(1),a=t.slice(1);for(let e=0;e<i.length;e++)if(e>=a.length||a[e]!==i[e])return!1;return!0}const Tt=A({scripts:j(k(),je())});function W(e,t){let n=Pt(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 Et(e,t,n){let r=At(t,n);if(!r)return null;let i=W(e,r);return i?{...i,pkgDir:r}:null}function Dt(e,t){let n=kt(t);if(!n)return null;let r=[];for(let t of n){let n=W(e,t);n&&r.push({...n,pkgDir:t})}return r}function Ot(e){let n=c(e);for(;;){if(t(s(n,`pnpm-workspace.yaml`)))return n;let e=o(n);if(e===n)return null;n=e}}function kt(e){let t=jt(e);if(!t)return null;let n=[];for(let a of t){if(!a.endsWith(`/*`))continue;let t=c(e,a.slice(0,-2)),o;try{o=r(t)}catch{continue}for(let e of o){let r=s(t,e);try{if(!i(r).isDirectory())continue}catch{continue}n.push(r)}}return n.length>0?n:null}function At(e,t){let n=kt(t);if(!n)return null;for(let t of n){let n=Nt(t);if(n===e||n===`@${e}`||t.split(`/`).pop()===e||n&&(n.startsWith(`@`)?n.split(`/`)[1]:n)===e)return t}return null}function jt(e){try{let t=n(s(e,`pnpm-workspace.yaml`),`utf-8`).split(`\n`),r=t.findIndex(e=>/^packages\\s*:/.test(e));if(r===-1)return null;let i=[];for(let e=r+1;e<t.length;e++){let n=t[e],r=/^\\s+-\\s+(.+)$/.exec(n);if(!r)break;i.push(r[1].trim().replace(/['\"`]/g,``))}return i.length>0?i:null}catch{return null}}const Mt=A({name:k()});function Nt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=Mt.safeParse(JSON.parse(t));return r.success?r.data.name:null}catch{return null}}function Pt(e){try{let t=n(s(e,`package.json`),`utf-8`),r=Tt.safeParse(JSON.parse(t));return r.success?r.data.scripts:null}catch{return null}}function Ft(e){return St(e,`package.json`)}const It=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`]),Lt=[`GIT_CONFIG_`,`GIT_PAGER`,`GIT_SSH_COMMAND`,`GIT_EDITOR`,`GIT_EXEC_PATH`];function Rt(e){return It.has(e)?!0:Lt.some(t=>e.startsWith(t))}function zt(e,t){let n=F(e);if(!n.ok||n.tokens.length!==2)return null;let r=n.tokens;if(r[0]!==`cd`)return null;let i=r[1];return a.isAbsolute(i)?i:a.resolve(t.cwd,i)}function G(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=Je(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=zt(e.command,o);t&&(o={...o,cwd:t})}let t=Bt(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 K(e,t){let n=e.pkgDir??t.cwd,r=t.insideResolvedScript??Ft(n)?{...t,insideResolvedScript:!0}:t;if(e.pre){let t=G(e.pre,r);if(t.action!==`allow`)return t}let i=G(e.main,r);if(i.action!==`allow`)return i;if(e.post){let t=G(e.post,r);if(t.action!==`allow`)return t}return`allow`}function Bt(e,t){let n=e.trim();if(n.startsWith(`(`)&&n.endsWith(`)`))return{action:`passthrough`,rule:null,reason:`subshell`};let r=F(e);if(!r.ok)return{action:`passthrough`,rule:null,reason:r.error};let i=r.tokens;if(i.length===0)return{action:`passthrough`,rule:null,reason:null};let o=0;for(;o<i.length&&/^\\w+=/.test(i[o]);){let e=i[o].split(`=`)[0];if(Rt(e))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${e}`};o++}if(o>=i.length)return{action:`allow`,rule:`env-only`,reason:null};i=i.slice(o);let s=Ye(i,e,t.insideResolvedScript);if(!s.safe)return{action:`passthrough`,rule:null,reason:s.reason};if(!t.insideResolvedScript)for(let e=0;e<i.length;e++){let t=i[e];if(!(t===`2>&1`||t===`2>/dev/null`||t===`>/dev/null`||t===`1>/dev/null`)){if(t===`>`||t===`>>`){if(i[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=new Set([`2>&1`,`2>/dev/null`,`>/dev/null`,`1>/dev/null`]);i=i.filter((e,t)=>!(c.has(e)||(e===`>`||e===`>>`)&&i[t+1]===`/dev/null`||e===`/dev/null`&&t>0&&(i[t-1]===`>`||i[t-1]===`>>`)));let l=a.basename(i[0]);if(l===`command`&&i.length>1)return G(i.slice(1).join(` `),{...t,depth:t.depth+1});if(l===`env`){let e=1;for(;e<i.length&&/^\\w+=/.test(i[e]);){let t=i[e].split(`=`)[0];if(Rt(t))return{action:`passthrough`,rule:null,reason:`dangerous env var: ${t}`};e++}return e>=i.length?{action:`allow`,rule:`env-only`,reason:null}:G(i.slice(e).join(` `),{...t,depth:t.depth+1})}if(l===`pnpm`)return Vt(i,t);if(l===`npm`)return Ut(i);if(l===`bash`||l===`sh`||l===`zsh`)return Kt(i,t);if(l===`xargs`)return Jt(i,t);if(l===`node`&&(i.includes(`-e`)||i.includes(`-p`)||i.includes(`--eval`)||i.includes(`--print`)))return{action:`passthrough`,rule:null,reason:`node eval`};if(l===`mv`)return{action:`passthrough`,rule:null,reason:`mv`};for(let e of yt)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`deny`,rule:e.name,reason:e.reason??null};for(let e of U)if(e.match.test(l)&&(!e.when||e.when(i,t)))return{action:`allow`,rule:e.name,reason:null};if(t.repoAllowPatterns){for(let e of t.repoAllowPatterns)if(wt(l,i,e))return{action:`allow`,rule:`repo:${l}`,reason:null}}return t.insideResolvedScript?{action:`allow`,rule:`resolved-script`,reason:null}:{action:`passthrough`,rule:null,reason:`no rule matched: ${l}`}}function Vt(e,t){let n=e.slice(1),r,i=-1,o=!1,s=!1,c;for(let a=0;a<n.length;a++){let l=n[a];if(l===`--filter`||l.startsWith(`--filter=`))return Wt(e,n,a,t);if(l===`-r`||l===`--recursive`){o=!0;continue}if(l===`-w`||l===`--workspace-root`){s=!0;continue}if(l===`-C`||l===`--dir`){c=n[++a];continue}if(l.startsWith(`-C=`)||l.startsWith(`--dir=`)){c=l.split(`=`,2)[1];continue}if(!(l.startsWith(`-`)&&!V.has(l))){r=l,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`}:G(e.join(` `),{...t,depth:t.depth+1})}let l;if(r===`run`){let e=n[i+1];if(!e||e.startsWith(`-`))return{action:`passthrough`,rule:null,reason:`pnpm run without script name`};l=e}else if(!V.has(r))l=r;else{for(let n of U)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}}if(o)return Ht(l,t);if(s){let e=Ot(t.cwd);return e?q(l,{...t,cwd:e}):{action:`passthrough`,rule:null,reason:`workspace root not found`}}if(c){let e=a.resolve(t.cwd,c);return q(l,{...t,cwd:e})}return q(l,t)}function q(e,t){let n=W(e,t.cwd);if(!n)return{action:`passthrough`,rule:null,reason:`script resolution failed`};let r=K(n,{...t,depth:t.depth+1});return r===`allow`?{action:`allow`,rule:`pnpm-script:${e}`,reason:null}:r}function Ht(e,t){let n=Dt(e,t.cwd);if(!n||n.length===0)return{action:`passthrough`,rule:null,reason:`pnpm recursive`};for(let e of n){let n=K(e,{...t,depth:t.depth+1,cwd:e.pkgDir??t.cwd});if(n!==`allow`)return n}return{action:`allow`,rule:`pnpm-recursive:${e}`,reason:null}}function Ut(e){for(let t of U)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:dt(e)}}function Wt(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(`-`)&&!V.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`}:G(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(V.has(c)&&c!==`run`)return{action:`passthrough`,rule:null,reason:null};let l=Et(c,a,r.cwd);if(!l)return{action:`passthrough`,rule:null,reason:`filtered script resolution failed`};let u=K(l,{...r,depth:r.depth+1,cwd:l.pkgDir??r.cwd});return u===`allow`?{action:`allow`,rule:`pnpm-filter:${a}:${c}`,reason:null}:u}const Gt=new Set([`-i`,`--interactive`,`-l`,`--login`,`--init-file`,`--rcfile`,`--norc`,`--noprofile`,`-s`,`--stdin`]);function Kt(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(`-`)||Gt.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?G(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 J=new Set([`-I`,`-d`,`-n`,`-P`,`-s`,`-L`,`-E`,`-a`,`--max-args`,`--max-procs`,`--delimiter`,`--replace`,`--max-lines`,`--max-chars`,`--arg-file`,`--process-slot-var`]),qt=new Set([`-0`,`--null`,`-t`,`--verbose`,`-r`,`--no-run-if-empty`,`-p`,`--interactive`,`-x`,`--exit`,`--show-limits`]);function Jt(e,t){let n=1;for(;n<e.length;){let t=e[n];if(!t.startsWith(`-`))break;if(J.has(t)){n+=2;continue}let r=t.indexOf(`=`);if(r!==-1){let e=t.slice(0,r);if(J.has(e)){n++;continue}}if(t.length>2&&J.has(t.slice(0,2))){n++;continue}if(qt.has(t)){n++;continue}return{action:`passthrough`,rule:null,reason:`xargs with unrecognised flags`}}return n>=e.length?{action:`passthrough`,rule:null,reason:`xargs without command`}:G(e.slice(n).join(` `),{...t,depth:t.depth+1})}const Yt=new Set([`Read`,`Glob`,`Grep`]),Y=new Set([`Write`,`Edit`]),Xt=[{name:`env-file-read`,tools:Yt,action:`deny`,when:e=>R(e),reason:`Don't read .env files. These contain secrets that should not appear in the conversation.`},{name:`env-file-write`,tools:Y,action:`deny`,when:e=>R(e),reason:`Don't write to .env files. These contain secrets.`},{name:`hooks-folder-write`,tools:Y,action:`deny`,when:(e,t)=>{let n=a.resolve(t.cwd,e),r=a.join(t.cwd,`.claude`,`hooks`);return n.startsWith(r+a.sep)||n===r},reason:`Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.`},{name:`claude-settings-write`,tools:Y,action:`deny`,when:(e,t)=>a.resolve(t.cwd,e)===a.join(t.cwd,`.claude`,`settings.json`),reason:`Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.`},{name:`outside-cwd-write`,tools:Y,action:`deny`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n!==t.cwd&&!n.startsWith(t.cwd+a.sep)},reason:`Don't write files outside the project directory.`}],Zt=[{name:`cwd-read`,tools:Yt,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}},{name:`cwd-write`,tools:Y,action:`allow`,when:(e,t)=>{let n=a.resolve(t.cwd,e);return n===t.cwd||n.startsWith(t.cwd+a.sep)}}],Qt=new Set([`Read`,`Write`,`Edit`]),$t=new Set([`Glob`,`Grep`]);function en(e,t,n){return Qt.has(e)?Ue(t)?.file_path??null:$t.has(e)?We(t)?.path??n:null}function tn(e,t,n){let r=en(e,t,n.cwd);if(r===null)return{action:`passthrough`,rule:null,reason:`unknown tool input shape`};for(let t of Xt)if(t.tools.has(e)&&t.when(r,n))return{action:`deny`,rule:t.name,reason:t.reason??null};for(let t of Zt)if(t.tools.has(e)&&t.when(r,n))return{action:`allow`,rule:t.name,reason:null};return{action:`passthrough`,rule:null,reason:`no file rule matched`}}const X=await Re();X.ok||(process.stderr.write(`[bst-hook] invalid hook input: ${X.error}\\n`),P({sessionId:`unknown`,cwd:`unknown`,command:`<invalid input: ${X.error}>`,decision:`passthrough`,rule:null,reason:`validation failed: ${X.error}`}),process.exit(0));const Z=X.data;Z.hook_event_name!==`PreToolUse`&&process.exit(0),new Set([`Bash`,`Read`,`Write`,`Edit`,`Glob`,`Grep`]).has(Z.tool_name)||process.exit(0);const nn=Ct(Z.cwd);let Q,$;if(Z.tool_name===`Bash`){let e=He(Z.tool_input);e||(P({sessionId:Z.session_id,cwd:Z.cwd,toolName:Z.tool_name,command:`<invalid Bash input: ${JSON.stringify(Z.tool_input).slice(0,200)}>`,decision:`passthrough`,rule:null,reason:`Bash tool_input missing or invalid 'command' field`}),process.exit(0)),Q=G(e.command,{cwd:Z.cwd,depth:0,repoAllowPatterns:nn}),$=e.command}else{let e=process.env.CLAUDE_PROJECT_DIR??Z.cwd;Q=tn(Z.tool_name,Z.tool_input,{cwd:e});let t=String(Z.tool_input.file_path??Z.tool_input.path??``);$=`${Z.tool_name} ${t}`}P({sessionId:Z.session_id,cwd:Z.cwd,toolName:Z.tool_name,command:$,decision:Q.action,rule:Q.rule,reason:Q.reason}),Q.action===`allow`?N(`allow`):Q.action===`deny`&&N(`deny`,Q.reason);export{};", "bst-hook.mjs"), writeIfChanged(ctx, GUIDE_PATH, "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that evaluates Bash commands\nand file tool operations before execution. Commands fall into three tiers:\n\n- **Denied** — blocked with a redirect message\n- **Allowed** — auto-approved, no prompt\n- **Passthrough** — falls through to Claude Code's normal permission prompt\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **rm-recursive**: Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-reset-hard**: Don't `reset --hard` on main. Create a branch first.\n- **git-write**: Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **npx-redirect**: This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\n\n### File tools (Read, Write, Edit, Glob, Grep)\n\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **hooks-folder-write**: Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\n- **claude-settings-write**: Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\n- **outside-cwd-write**: Don't write files outside the project directory.\n\n## Auto-approved commands\n\nThese run without prompting.\n\n### Bash\n\n- **Test runners**: vitest, jest\n- **Type checking**: tsc\n- **Build tools**: tsdown\n- **Linters & formatters**: oxlint, oxfmt, eslint, prettier, biome, knip\n- **Read-only commands**: cat, head, tail, grep, ls, wc, uniq, diff, echo, pwd, which, dirname, basename, realpath\n- **Package managers (read-only)**: npm (view, info, show only)\n- **Shell builtins**: test, bracket-test\n- **Navigation**: cd, true, colon\n- **Conditional commands**: find (without -exec, -delete), sort (without -o / --output)\n- **Git read operations**: git (status, log, diff, show, blame, branch, remote, fetch, rev-parse, etc.)\n- **Git safe writes**: git-safe-write (add, switch -c, checkout -b, stash push/pop/apply/show)\n- **pnpm (safe built-ins)**: pnpm (list, why, view, info, show, outdated, install (bare only))\n- **File operations (within project)**: cp (paths must be within project directory), mkdir (paths must be within project directory), touch (paths must be within project directory)\n\n### File tools\n\n- **Read/Glob/Grep**: Allowed within project directory\n- **Write/Edit**: Allowed within project directory\n\n## Passthrough\n\nEverything not listed above triggers Claude Code's normal permission prompt.\nCommon examples:\n\n- `pnpm add <pkg>`, `pnpm dlx <pkg>` (dependency changes)\n- `mv` (file moves)\n- `node`, `npx` scripts\n- Commands with output redirection (`> file`)\n- Any unrecognized command\n\n## How commands are evaluated\n\n- **Pipelines and chains**: every segment must be allowed; any deny blocks the whole command.\n- **pnpm scripts**: `pnpm <script>` resolves against `package.json` (including pre/post hooks and `--filter` workspaces) and evaluates the resolved command recursively.\n- **cd propagation**: `cd dir && cmd` evaluates `cmd` relative to the new directory.\n- **Audit log**: all decisions are logged to `.claude/hooks/bst-hook.audit.local.jsonl`.\n", "bst-hook-guide.md")];
|
|
2722
2738
|
}
|
|
2723
2739
|
//#endregion
|
|
2724
2740
|
//#region src/generators/claude-settings.ts
|
|
@@ -2761,9 +2777,9 @@ function buildSettings(ctx) {
|
|
|
2761
2777
|
const allow = [];
|
|
2762
2778
|
const enabledPlugins = { "code-simplifier@claude-plugins-official": true };
|
|
2763
2779
|
const extraKnownMarketplaces = {};
|
|
2764
|
-
if (ctx.config.structure
|
|
2765
|
-
if (ctx.
|
|
2766
|
-
}
|
|
2780
|
+
if (ctx.config.structure === "monorepo") {
|
|
2781
|
+
if (getMonorepoPackages(ctx.targetDir).some((pkg) => hasWebUIDeps(pkg.dir))) enabledPlugins["frontend-design@claude-plugins-official"] = true;
|
|
2782
|
+
} else if (ctx.packageJson ? packageHasWebUIDeps(ctx.packageJson) : false) enabledPlugins["frontend-design@claude-plugins-official"] = true;
|
|
2767
2783
|
return {
|
|
2768
2784
|
permissions: {
|
|
2769
2785
|
allow,
|
|
@@ -2883,19 +2899,16 @@ async function generateClaudeSettings(ctx) {
|
|
|
2883
2899
|
const results = [];
|
|
2884
2900
|
results.push(writeOrMergeSettings(ctx, ".claude/settings.json", buildSettings(ctx)));
|
|
2885
2901
|
if (ctx.config.structure === "monorepo") for (const pkg of getMonorepoPackages(ctx.targetDir)) {
|
|
2886
|
-
if (!hasWebUIDeps(pkg.dir)) continue;
|
|
2887
2902
|
const pkgRelDir = path.relative(ctx.targetDir, pkg.dir);
|
|
2888
|
-
const
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
extraKnownMarketplaces: {}
|
|
2898
|
-
}));
|
|
2903
|
+
const claudeDir = path.join(pkgRelDir, ".claude");
|
|
2904
|
+
if (ctx.exists(claudeDir)) {
|
|
2905
|
+
ctx.remove(claudeDir);
|
|
2906
|
+
results.push({
|
|
2907
|
+
filePath: claudeDir,
|
|
2908
|
+
action: "removed",
|
|
2909
|
+
description: `Removed subpackage ${claudeDir} (plugins consolidated at root)`
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2899
2912
|
}
|
|
2900
2913
|
return results;
|
|
2901
2914
|
}
|
|
@@ -3344,7 +3357,7 @@ async function runGenerators(ctx) {
|
|
|
3344
3357
|
function generateMigratePrompt(results, config, detected) {
|
|
3345
3358
|
const sections = [];
|
|
3346
3359
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3347
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
3360
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.47.0 repo:sync\` on ${timestamp}_\n`);
|
|
3348
3361
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3349
3362
|
if (changed.length > 0) {
|
|
3350
3363
|
const items = changed.map((r) => {
|
|
@@ -3511,10 +3524,13 @@ async function runInit(config, options = {}) {
|
|
|
3511
3524
|
});
|
|
3512
3525
|
const created = results.filter((r) => r.action === "created");
|
|
3513
3526
|
const updated = results.filter((r) => r.action === "updated");
|
|
3514
|
-
const
|
|
3515
|
-
const
|
|
3527
|
+
const removed = results.filter((r) => r.action === "removed");
|
|
3528
|
+
const hasChanges = created.length > 0 || updated.length > 0 || removed.length > 0 || archivedFiles.length > 0;
|
|
3516
3529
|
const promptPath = ".tooling-migrate.md";
|
|
3517
|
-
|
|
3530
|
+
if (!options.noPrompt) {
|
|
3531
|
+
const prompt = generateMigratePrompt(results, config, detected);
|
|
3532
|
+
ctx.write(promptPath, prompt);
|
|
3533
|
+
}
|
|
3518
3534
|
if (!hasChanges && options.noPrompt) {
|
|
3519
3535
|
log.success("Repository is up to date.");
|
|
3520
3536
|
return results;
|
|
@@ -3529,6 +3545,7 @@ async function runInit(config, options = {}) {
|
|
|
3529
3545
|
const summaryLines = [];
|
|
3530
3546
|
if (created.length > 0) summaryLines.push(`Created: ${created.map((r) => r.filePath).join(", ")}`);
|
|
3531
3547
|
if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
|
|
3548
|
+
if (removed.length > 0) summaryLines.push(`Removed: ${removed.map((r) => r.filePath).join(", ")}`);
|
|
3532
3549
|
note(summaryLines.join("\n"), "Summary");
|
|
3533
3550
|
if (!options.noPrompt) log.info(`Migration prompt written to ${promptPath}`);
|
|
3534
3551
|
const packageJsonChanged = results.some((r) => r.filePath === "package.json" && (r.action === "created" || r.action === "updated"));
|
|
@@ -3606,8 +3623,8 @@ const syncCommand = defineCommand({
|
|
|
3606
3623
|
let config;
|
|
3607
3624
|
if (args.yes || !isFirstRun) {
|
|
3608
3625
|
const detected = buildDefaultConfig(targetDir, {
|
|
3609
|
-
eslintPlugin: args["eslint-plugin"] ===
|
|
3610
|
-
noCi: args["no-ci"] ===
|
|
3626
|
+
eslintPlugin: typeof args["eslint-plugin"] === "boolean" ? args["eslint-plugin"] : void 0,
|
|
3627
|
+
noCi: typeof args["no-ci"] === "boolean" ? args["no-ci"] : void 0
|
|
3611
3628
|
});
|
|
3612
3629
|
config = saved ? mergeWithSavedConfig(detected, saved) : detected;
|
|
3613
3630
|
} else config = await runInitPrompts(targetDir, saved);
|
|
@@ -3624,9 +3641,10 @@ async function runCheck(targetDir) {
|
|
|
3624
3641
|
const { ctx, pendingWrites } = createDryRunContext(saved ? mergeWithSavedConfig(detected, saved) : detected);
|
|
3625
3642
|
logDetectionSummary(ctx);
|
|
3626
3643
|
const actionable = (await runGenerators(ctx)).filter((r) => {
|
|
3644
|
+
if (r.action === "removed") return true;
|
|
3627
3645
|
if (r.action !== "created" && r.action !== "updated") return false;
|
|
3628
3646
|
const newContent = pendingWrites.get(r.filePath);
|
|
3629
|
-
if (newContent && r.action === "updated") {
|
|
3647
|
+
if (newContent !== void 0 && r.action === "updated") {
|
|
3630
3648
|
const existingPath = path.join(targetDir, r.filePath);
|
|
3631
3649
|
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
3632
3650
|
if (existing && contentEqual(r.filePath, existing, newContent)) return false;
|
|
@@ -3641,7 +3659,7 @@ async function runCheck(targetDir) {
|
|
|
3641
3659
|
for (const r of actionable) {
|
|
3642
3660
|
log.info(` ${r.action}: ${r.filePath} — ${r.description}`);
|
|
3643
3661
|
const newContent = pendingWrites.get(r.filePath);
|
|
3644
|
-
if (
|
|
3662
|
+
if (newContent === void 0) continue;
|
|
3645
3663
|
const existingPath = path.join(targetDir, r.filePath);
|
|
3646
3664
|
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
3647
3665
|
if (isBundledFile(r.filePath)) log.info(` bundled file changed (${formatBytes(existing?.length ?? 0)} → ${formatBytes(newContent.length)})`);
|
|
@@ -4251,6 +4269,111 @@ async function runVersionMode(executor, config) {
|
|
|
4251
4269
|
};
|
|
4252
4270
|
}
|
|
4253
4271
|
//#endregion
|
|
4272
|
+
//#region src/release/publish.ts
|
|
4273
|
+
/** Mode 2: publish to npm, push tags, and create Forgejo releases. */
|
|
4274
|
+
async function runPublishMode(executor, config) {
|
|
4275
|
+
log.info("No changesets — publishing packages");
|
|
4276
|
+
const publishResult = executor.exec("pnpm", ["changeset", "publish"], { cwd: config.cwd });
|
|
4277
|
+
debugExec(config, "pnpm changeset publish", publishResult);
|
|
4278
|
+
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
4279
|
+
const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
|
|
4280
|
+
debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
|
|
4281
|
+
const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
|
|
4282
|
+
debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
|
|
4283
|
+
const remoteTags = parseRemoteTags(executor.exec("git", [
|
|
4284
|
+
"ls-remote",
|
|
4285
|
+
"--tags",
|
|
4286
|
+
"origin"
|
|
4287
|
+
], { cwd: config.cwd }).stdout);
|
|
4288
|
+
debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
|
|
4289
|
+
const remoteSet = new Set(remoteTags);
|
|
4290
|
+
const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
|
|
4291
|
+
debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
|
|
4292
|
+
if (config.dryRun) {
|
|
4293
|
+
if (tagsToPush.length === 0) {
|
|
4294
|
+
log.info("No packages were published");
|
|
4295
|
+
return { mode: "none" };
|
|
4296
|
+
}
|
|
4297
|
+
log.info(`Tags to process: ${tagsToPush.join(", ")}`);
|
|
4298
|
+
log.info("[dry-run] Would push tags and create releases");
|
|
4299
|
+
return {
|
|
4300
|
+
mode: "publish",
|
|
4301
|
+
tags: tagsToPush
|
|
4302
|
+
};
|
|
4303
|
+
}
|
|
4304
|
+
const conn = {
|
|
4305
|
+
serverUrl: config.serverUrl,
|
|
4306
|
+
repository: config.repository,
|
|
4307
|
+
token: config.token
|
|
4308
|
+
};
|
|
4309
|
+
const remoteExpectedTags = expectedTags.filter((t) => remoteSet.has(t) && !tagsToPush.includes(t));
|
|
4310
|
+
const tagsWithMissingReleases = [];
|
|
4311
|
+
for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
|
|
4312
|
+
const allTags = [...tagsToPush, ...tagsWithMissingReleases];
|
|
4313
|
+
if (allTags.length === 0) {
|
|
4314
|
+
log.info("No packages were published");
|
|
4315
|
+
return { mode: "none" };
|
|
4316
|
+
}
|
|
4317
|
+
log.info(`Tags to process: ${allTags.join(", ")}`);
|
|
4318
|
+
const errors = [];
|
|
4319
|
+
for (const tag of allTags) try {
|
|
4320
|
+
if (!remoteSet.has(tag)) {
|
|
4321
|
+
if (executor.exec("git", [
|
|
4322
|
+
"tag",
|
|
4323
|
+
"-l",
|
|
4324
|
+
tag
|
|
4325
|
+
], { cwd: config.cwd }).stdout.trim() === "") {
|
|
4326
|
+
const tagResult = executor.exec("git", ["tag", tag], { cwd: config.cwd });
|
|
4327
|
+
if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4328
|
+
}
|
|
4329
|
+
const pushTagResult = executor.exec("git", [
|
|
4330
|
+
"push",
|
|
4331
|
+
"origin",
|
|
4332
|
+
`refs/tags/${tag}`
|
|
4333
|
+
], { cwd: config.cwd });
|
|
4334
|
+
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
4335
|
+
}
|
|
4336
|
+
if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
|
|
4337
|
+
else log.info(`Created release for ${tag}`);
|
|
4338
|
+
} catch (error) {
|
|
4339
|
+
errors.push({
|
|
4340
|
+
tag,
|
|
4341
|
+
error
|
|
4342
|
+
});
|
|
4343
|
+
log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4344
|
+
}
|
|
4345
|
+
if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
|
|
4346
|
+
const slidingTags = allTags.flatMap((t) => {
|
|
4347
|
+
const atIndex = t.lastIndexOf("@");
|
|
4348
|
+
if (atIndex <= 0) return [];
|
|
4349
|
+
const name = t.slice(0, atIndex);
|
|
4350
|
+
return computeSlidingTags(t.slice(atIndex + 1)).map((suffix) => `${name}@${suffix}`);
|
|
4351
|
+
});
|
|
4352
|
+
if (slidingTags.length > 0) {
|
|
4353
|
+
for (const tag of slidingTags) {
|
|
4354
|
+
const tagResult = executor.exec("git", [
|
|
4355
|
+
"tag",
|
|
4356
|
+
"-f",
|
|
4357
|
+
tag
|
|
4358
|
+
], { cwd: config.cwd });
|
|
4359
|
+
if (tagResult.exitCode !== 0) log.warn(`Warning: Failed to create sliding tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4360
|
+
}
|
|
4361
|
+
const forcePushResult = executor.exec("git", [
|
|
4362
|
+
"push",
|
|
4363
|
+
"origin",
|
|
4364
|
+
...slidingTags.map((t) => `refs/tags/${t}`),
|
|
4365
|
+
"--force"
|
|
4366
|
+
], { cwd: config.cwd });
|
|
4367
|
+
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4368
|
+
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4369
|
+
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4370
|
+
}
|
|
4371
|
+
return {
|
|
4372
|
+
mode: "publish",
|
|
4373
|
+
tags: allTags
|
|
4374
|
+
};
|
|
4375
|
+
}
|
|
4376
|
+
//#endregion
|
|
4254
4377
|
//#region src/release/connection.ts
|
|
4255
4378
|
const RepositorySchema = z.union([z.string(), z.object({ url: z.string() })]);
|
|
4256
4379
|
/**
|
|
@@ -4364,345 +4487,56 @@ function configureGitIdentity(executor, platform, cwd) {
|
|
|
4364
4487
|
], { cwd });
|
|
4365
4488
|
}
|
|
4366
4489
|
//#endregion
|
|
4367
|
-
//#region src/release
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
draft: false,
|
|
4378
|
-
prerelease: false,
|
|
4379
|
-
author: { login: actor }
|
|
4380
|
-
},
|
|
4381
|
-
repository: {
|
|
4382
|
-
full_name: options.repository,
|
|
4383
|
-
html_url: `${options.serverUrl}/${options.repository}`
|
|
4490
|
+
//#region src/commands/release-changesets.ts
|
|
4491
|
+
const releaseForgejoCommand = defineCommand({
|
|
4492
|
+
meta: {
|
|
4493
|
+
name: "release:changesets",
|
|
4494
|
+
description: "Changesets version/publish for Forgejo CI"
|
|
4495
|
+
},
|
|
4496
|
+
args: {
|
|
4497
|
+
"dry-run": {
|
|
4498
|
+
type: "boolean",
|
|
4499
|
+
description: "Skip push, API calls, and publishing side effects"
|
|
4384
4500
|
},
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4501
|
+
verbose: {
|
|
4502
|
+
type: "boolean",
|
|
4503
|
+
description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
|
|
4504
|
+
}
|
|
4505
|
+
},
|
|
4506
|
+
async run({ args }) {
|
|
4507
|
+
const result = await runRelease(buildReleaseConfig({
|
|
4508
|
+
dryRun: args["dry-run"] === true,
|
|
4509
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
4510
|
+
}), createRealExecutor());
|
|
4511
|
+
if (result.mode === "publish") {
|
|
4512
|
+
const outputFile = process.env["GITHUB_OUTPUT"];
|
|
4513
|
+
if (outputFile) try {
|
|
4514
|
+
appendFileSync(outputFile, "published=true\n");
|
|
4515
|
+
} catch (_error) {}
|
|
4516
|
+
}
|
|
4517
|
+
if (result.mode === "none") process.exitCode = 0;
|
|
4518
|
+
}
|
|
4519
|
+
});
|
|
4520
|
+
/** Build release config from environment / package.json and CLI flags. */
|
|
4521
|
+
function buildReleaseConfig(flags) {
|
|
4522
|
+
const resolved = resolveConnection(process.cwd());
|
|
4523
|
+
if (resolved.type !== "forgejo") throw new FatalError("release:changesets requires a Forgejo repository");
|
|
4393
4524
|
return {
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
draft: false,
|
|
4399
|
-
prerelease: options.prerelease ?? false,
|
|
4400
|
-
author: { login: actor }
|
|
4401
|
-
},
|
|
4402
|
-
repository: {
|
|
4403
|
-
full_name: repository,
|
|
4404
|
-
html_url: repository ? `${serverUrl}/${repository}` : ""
|
|
4405
|
-
},
|
|
4406
|
-
sender: { login: actor }
|
|
4525
|
+
...resolved.conn,
|
|
4526
|
+
cwd: process.cwd(),
|
|
4527
|
+
dryRun: flags.dryRun ?? false,
|
|
4528
|
+
verbose: flags.verbose ?? false
|
|
4407
4529
|
};
|
|
4408
4530
|
}
|
|
4409
|
-
/**
|
|
4410
|
-
function
|
|
4411
|
-
const repository = process.env["GITHUB_REPOSITORY"];
|
|
4531
|
+
/** Resolve the current branch from CI env vars or git. */
|
|
4532
|
+
function getCurrentBranch(executor, cwd) {
|
|
4412
4533
|
const ref = process.env["GITHUB_REF"];
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
release: {
|
|
4420
|
-
tag_name: tagName,
|
|
4421
|
-
name: tagName,
|
|
4422
|
-
draft: false,
|
|
4423
|
-
prerelease: false,
|
|
4424
|
-
author: { login: actor }
|
|
4425
|
-
},
|
|
4426
|
-
repository: {
|
|
4427
|
-
full_name: repository,
|
|
4428
|
-
html_url: `${serverUrl}/${repository}`
|
|
4429
|
-
},
|
|
4430
|
-
sender: { login: actor }
|
|
4431
|
-
};
|
|
4432
|
-
}
|
|
4433
|
-
/**
|
|
4434
|
-
* Compute the HMAC-SHA256 hex digest of data using the given secret.
|
|
4435
|
-
* Returns the signature in GitHub-style format: `sha256=<hex>`.
|
|
4436
|
-
*/
|
|
4437
|
-
function computeSignature(secret, data) {
|
|
4438
|
-
const hmac = createHmac("sha256", secret);
|
|
4439
|
-
hmac.update(data);
|
|
4440
|
-
return `sha256=${hmac.digest("hex")}`;
|
|
4441
|
-
}
|
|
4442
|
-
/**
|
|
4443
|
-
* POST to a webhook URL with GitHub-style HMAC-SHA256 authentication.
|
|
4444
|
-
*
|
|
4445
|
-
* Sends a JSON-serialized release payload (or empty string if no payload).
|
|
4446
|
-
* If a secret is provided, computes the HMAC-SHA256 signature of the body
|
|
4447
|
-
* and includes it in the `X-Hub-Signature-256` header.
|
|
4448
|
-
*
|
|
4449
|
-
* Never throws — returns a result object indicating success or failure.
|
|
4450
|
-
*/
|
|
4451
|
-
async function sendWebhook(options) {
|
|
4452
|
-
const { url, secret, payload, fetcher = globalThis.fetch } = options;
|
|
4453
|
-
const body = payload ? JSON.stringify(payload) : "";
|
|
4454
|
-
const headers = { "Content-Type": "application/json" };
|
|
4455
|
-
if (secret) headers["X-Hub-Signature-256"] = computeSignature(secret, body);
|
|
4456
|
-
try {
|
|
4457
|
-
const response = await fetcher(url, {
|
|
4458
|
-
method: "POST",
|
|
4459
|
-
headers,
|
|
4460
|
-
body
|
|
4461
|
-
});
|
|
4462
|
-
if (!response.ok) return {
|
|
4463
|
-
ok: false,
|
|
4464
|
-
status: response.status,
|
|
4465
|
-
error: `HTTP ${String(response.status)}: ${await response.text().catch(() => "")}`
|
|
4466
|
-
};
|
|
4467
|
-
return {
|
|
4468
|
-
ok: true,
|
|
4469
|
-
status: response.status
|
|
4470
|
-
};
|
|
4471
|
-
} catch (error) {
|
|
4472
|
-
return {
|
|
4473
|
-
ok: false,
|
|
4474
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4475
|
-
};
|
|
4476
|
-
}
|
|
4477
|
-
}
|
|
4478
|
-
/**
|
|
4479
|
-
* Send a webhook and log the result. Intended for CLI usage.
|
|
4480
|
-
* Never throws.
|
|
4481
|
-
*/
|
|
4482
|
-
async function sendWebhookWithLogging(url, secret, payload) {
|
|
4483
|
-
const result = await sendWebhook({
|
|
4484
|
-
url,
|
|
4485
|
-
secret,
|
|
4486
|
-
payload
|
|
4487
|
-
});
|
|
4488
|
-
if (result.ok) log.info("Post-release hook completed successfully");
|
|
4489
|
-
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
4490
|
-
}
|
|
4491
|
-
//#endregion
|
|
4492
|
-
//#region src/release/hook.ts
|
|
4493
|
-
const PostReleaseHookSchema = z.object({ postReleaseHook: z.string().optional() });
|
|
4494
|
-
/** Read the postReleaseHook command override from .tooling.json at runtime. */
|
|
4495
|
-
function loadHookCommand(executor, cwd) {
|
|
4496
|
-
const configPath = path.join(cwd, ".tooling.json");
|
|
4497
|
-
const raw = executor.readFile(configPath);
|
|
4498
|
-
if (!raw) return void 0;
|
|
4499
|
-
try {
|
|
4500
|
-
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
4501
|
-
if (!result.success) return void 0;
|
|
4502
|
-
return result.data.postReleaseHook;
|
|
4503
|
-
} catch (_error) {
|
|
4504
|
-
return;
|
|
4505
|
-
}
|
|
4506
|
-
}
|
|
4507
|
-
/**
|
|
4508
|
-
* Execute the post-release hook after a successful publish.
|
|
4509
|
-
*
|
|
4510
|
-
* Uses the `postReleaseHook` command from `.tooling.json` if configured,
|
|
4511
|
-
* otherwise POSTs to `POST_RELEASE_HOOK_URL` with HMAC-SHA256 authentication
|
|
4512
|
-
* using `WEBHOOK_SECRET`. Both env vars must be set by the CI workflow.
|
|
4513
|
-
*
|
|
4514
|
-
* Failures are logged as warnings — they never fail the release.
|
|
4515
|
-
*/
|
|
4516
|
-
async function runPostReleaseHook(executor, config, payload) {
|
|
4517
|
-
if (config.dryRun) {
|
|
4518
|
-
debug(config, "Skipping post-release hook (dry-run)");
|
|
4519
|
-
return;
|
|
4520
|
-
}
|
|
4521
|
-
const customCommand = loadHookCommand(executor, config.cwd);
|
|
4522
|
-
if (customCommand) {
|
|
4523
|
-
debug(config, `Running custom post-release hook: ${customCommand}`);
|
|
4524
|
-
executeHookCommand(executor, config, customCommand);
|
|
4525
|
-
return;
|
|
4526
|
-
}
|
|
4527
|
-
const hookUrl = process.env["POST_RELEASE_HOOK_URL"];
|
|
4528
|
-
if (!hookUrl) {
|
|
4529
|
-
debug(config, "No POST_RELEASE_HOOK_URL set — skipping post-release hook");
|
|
4530
|
-
return;
|
|
4531
|
-
}
|
|
4532
|
-
const hookSecret = process.env["WEBHOOK_SECRET"];
|
|
4533
|
-
debug(config, `Running post-release hook: POST ${hookUrl}`);
|
|
4534
|
-
const result = await sendWebhook({
|
|
4535
|
-
url: hookUrl,
|
|
4536
|
-
secret: hookSecret || void 0,
|
|
4537
|
-
payload,
|
|
4538
|
-
fetcher: executor.fetch.bind(executor)
|
|
4539
|
-
});
|
|
4540
|
-
if (result.ok) log.info("Post-release hook completed successfully");
|
|
4541
|
-
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
4542
|
-
}
|
|
4543
|
-
function executeHookCommand(executor, config, command) {
|
|
4544
|
-
try {
|
|
4545
|
-
const result = executor.exec("sh", ["-c", command], { cwd: config.cwd });
|
|
4546
|
-
if (result.exitCode !== 0) log.warn(`Post-release hook failed (exit code ${String(result.exitCode)}): ${result.stderr || result.stdout}`);
|
|
4547
|
-
else log.info("Post-release hook completed successfully");
|
|
4548
|
-
} catch (error) {
|
|
4549
|
-
log.warn(`Post-release hook error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4550
|
-
}
|
|
4551
|
-
}
|
|
4552
|
-
//#endregion
|
|
4553
|
-
//#region src/release/publish.ts
|
|
4554
|
-
/** Mode 2: publish to npm, push tags, and create Forgejo releases. */
|
|
4555
|
-
async function runPublishMode(executor, config) {
|
|
4556
|
-
log.info("No changesets — publishing packages");
|
|
4557
|
-
const publishResult = executor.exec("pnpm", ["changeset", "publish"], { cwd: config.cwd });
|
|
4558
|
-
debugExec(config, "pnpm changeset publish", publishResult);
|
|
4559
|
-
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
4560
|
-
const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
|
|
4561
|
-
debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
|
|
4562
|
-
const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
|
|
4563
|
-
debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
|
|
4564
|
-
const remoteTags = parseRemoteTags(executor.exec("git", [
|
|
4565
|
-
"ls-remote",
|
|
4566
|
-
"--tags",
|
|
4567
|
-
"origin"
|
|
4568
|
-
], { cwd: config.cwd }).stdout);
|
|
4569
|
-
debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
|
|
4570
|
-
const remoteSet = new Set(remoteTags);
|
|
4571
|
-
const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
|
|
4572
|
-
debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
|
|
4573
|
-
if (config.dryRun) {
|
|
4574
|
-
if (tagsToPush.length === 0) {
|
|
4575
|
-
log.info("No packages were published");
|
|
4576
|
-
return { mode: "none" };
|
|
4577
|
-
}
|
|
4578
|
-
log.info(`Tags to process: ${tagsToPush.join(", ")}`);
|
|
4579
|
-
log.info("[dry-run] Would push tags and create releases");
|
|
4580
|
-
return {
|
|
4581
|
-
mode: "publish",
|
|
4582
|
-
tags: tagsToPush
|
|
4583
|
-
};
|
|
4584
|
-
}
|
|
4585
|
-
const conn = {
|
|
4586
|
-
serverUrl: config.serverUrl,
|
|
4587
|
-
repository: config.repository,
|
|
4588
|
-
token: config.token
|
|
4589
|
-
};
|
|
4590
|
-
const remoteExpectedTags = expectedTags.filter((t) => remoteSet.has(t) && !tagsToPush.includes(t));
|
|
4591
|
-
const tagsWithMissingReleases = [];
|
|
4592
|
-
for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
|
|
4593
|
-
const allTags = [...tagsToPush, ...tagsWithMissingReleases];
|
|
4594
|
-
if (allTags.length === 0) {
|
|
4595
|
-
log.info("No packages were published");
|
|
4596
|
-
return { mode: "none" };
|
|
4597
|
-
}
|
|
4598
|
-
log.info(`Tags to process: ${allTags.join(", ")}`);
|
|
4599
|
-
const errors = [];
|
|
4600
|
-
for (const tag of allTags) try {
|
|
4601
|
-
if (!remoteSet.has(tag)) {
|
|
4602
|
-
if (executor.exec("git", [
|
|
4603
|
-
"tag",
|
|
4604
|
-
"-l",
|
|
4605
|
-
tag
|
|
4606
|
-
], { cwd: config.cwd }).stdout.trim() === "") {
|
|
4607
|
-
const tagResult = executor.exec("git", ["tag", tag], { cwd: config.cwd });
|
|
4608
|
-
if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4609
|
-
}
|
|
4610
|
-
const pushTagResult = executor.exec("git", [
|
|
4611
|
-
"push",
|
|
4612
|
-
"origin",
|
|
4613
|
-
`refs/tags/${tag}`
|
|
4614
|
-
], { cwd: config.cwd });
|
|
4615
|
-
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
4616
|
-
}
|
|
4617
|
-
if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
|
|
4618
|
-
else log.info(`Created release for ${tag}`);
|
|
4619
|
-
} catch (error) {
|
|
4620
|
-
errors.push({
|
|
4621
|
-
tag,
|
|
4622
|
-
error
|
|
4623
|
-
});
|
|
4624
|
-
log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4625
|
-
}
|
|
4626
|
-
if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
|
|
4627
|
-
const slidingTags = allTags.flatMap((t) => {
|
|
4628
|
-
const atIndex = t.lastIndexOf("@");
|
|
4629
|
-
if (atIndex <= 0) return [];
|
|
4630
|
-
const name = t.slice(0, atIndex);
|
|
4631
|
-
return computeSlidingTags(t.slice(atIndex + 1)).map((suffix) => `${name}@${suffix}`);
|
|
4632
|
-
});
|
|
4633
|
-
if (slidingTags.length > 0) {
|
|
4634
|
-
for (const tag of slidingTags) {
|
|
4635
|
-
const tagResult = executor.exec("git", [
|
|
4636
|
-
"tag",
|
|
4637
|
-
"-f",
|
|
4638
|
-
tag
|
|
4639
|
-
], { cwd: config.cwd });
|
|
4640
|
-
if (tagResult.exitCode !== 0) log.warn(`Warning: Failed to create sliding tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4641
|
-
}
|
|
4642
|
-
const forcePushResult = executor.exec("git", [
|
|
4643
|
-
"push",
|
|
4644
|
-
"origin",
|
|
4645
|
-
...slidingTags.map((t) => `refs/tags/${t}`),
|
|
4646
|
-
"--force"
|
|
4647
|
-
], { cwd: config.cwd });
|
|
4648
|
-
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4649
|
-
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4650
|
-
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4651
|
-
}
|
|
4652
|
-
await runPostReleaseHook(executor, config, buildChangesetsPayload({
|
|
4653
|
-
tags: allTags,
|
|
4654
|
-
repository: config.repository,
|
|
4655
|
-
serverUrl: config.serverUrl
|
|
4656
|
-
}));
|
|
4657
|
-
return {
|
|
4658
|
-
mode: "publish",
|
|
4659
|
-
tags: allTags
|
|
4660
|
-
};
|
|
4661
|
-
}
|
|
4662
|
-
//#endregion
|
|
4663
|
-
//#region src/commands/release-changesets.ts
|
|
4664
|
-
const releaseForgejoCommand = defineCommand({
|
|
4665
|
-
meta: {
|
|
4666
|
-
name: "release:changesets",
|
|
4667
|
-
description: "Changesets version/publish for Forgejo CI"
|
|
4668
|
-
},
|
|
4669
|
-
args: {
|
|
4670
|
-
"dry-run": {
|
|
4671
|
-
type: "boolean",
|
|
4672
|
-
description: "Skip push, API calls, and publishing side effects"
|
|
4673
|
-
},
|
|
4674
|
-
verbose: {
|
|
4675
|
-
type: "boolean",
|
|
4676
|
-
description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
|
|
4677
|
-
}
|
|
4678
|
-
},
|
|
4679
|
-
async run({ args }) {
|
|
4680
|
-
if ((await runRelease(buildReleaseConfig({
|
|
4681
|
-
dryRun: args["dry-run"] === true,
|
|
4682
|
-
verbose: args.verbose === true || isEnvVerbose()
|
|
4683
|
-
}), createRealExecutor())).mode === "none") process.exitCode = 0;
|
|
4684
|
-
}
|
|
4685
|
-
});
|
|
4686
|
-
/** Build release config from environment / package.json and CLI flags. */
|
|
4687
|
-
function buildReleaseConfig(flags) {
|
|
4688
|
-
const resolved = resolveConnection(process.cwd());
|
|
4689
|
-
if (resolved.type !== "forgejo") throw new FatalError("release:changesets requires a Forgejo repository");
|
|
4690
|
-
return {
|
|
4691
|
-
...resolved.conn,
|
|
4692
|
-
cwd: process.cwd(),
|
|
4693
|
-
dryRun: flags.dryRun ?? false,
|
|
4694
|
-
verbose: flags.verbose ?? false
|
|
4695
|
-
};
|
|
4696
|
-
}
|
|
4697
|
-
/** Resolve the current branch from CI env vars or git. */
|
|
4698
|
-
function getCurrentBranch(executor, cwd) {
|
|
4699
|
-
const ref = process.env["GITHUB_REF"];
|
|
4700
|
-
if (ref?.startsWith("refs/heads/")) return ref.slice(11);
|
|
4701
|
-
return executor.exec("git", [
|
|
4702
|
-
"rev-parse",
|
|
4703
|
-
"--abbrev-ref",
|
|
4704
|
-
"HEAD"
|
|
4705
|
-
], { cwd }).stdout.trim();
|
|
4534
|
+
if (ref?.startsWith("refs/heads/")) return ref.slice(11);
|
|
4535
|
+
return executor.exec("git", [
|
|
4536
|
+
"rev-parse",
|
|
4537
|
+
"--abbrev-ref",
|
|
4538
|
+
"HEAD"
|
|
4539
|
+
], { cwd }).stdout.trim();
|
|
4706
4540
|
}
|
|
4707
4541
|
/** Core release logic — testable with a mock executor. */
|
|
4708
4542
|
async function runRelease(config, executor) {
|
|
@@ -4970,16 +4804,6 @@ async function runSimpleRelease(executor, config) {
|
|
|
4970
4804
|
}
|
|
4971
4805
|
let releaseCreated = false;
|
|
4972
4806
|
if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
|
|
4973
|
-
if (pushed) {
|
|
4974
|
-
const conn = config.platform?.type === "forgejo" ? config.platform.conn : void 0;
|
|
4975
|
-
await runPostReleaseHook(executor, config, buildSimplePayload({
|
|
4976
|
-
tag,
|
|
4977
|
-
repository: conn?.repository,
|
|
4978
|
-
serverUrl: conn?.serverUrl,
|
|
4979
|
-
prerelease: Boolean(config.prerelease),
|
|
4980
|
-
platform: config.platform?.type
|
|
4981
|
-
}));
|
|
4982
|
-
}
|
|
4983
4807
|
return {
|
|
4984
4808
|
version,
|
|
4985
4809
|
tag,
|
|
@@ -5078,7 +4902,12 @@ const releaseSimpleCommand = defineCommand({
|
|
|
5078
4902
|
prerelease: args.prerelease,
|
|
5079
4903
|
platform
|
|
5080
4904
|
};
|
|
5081
|
-
await runSimpleRelease(createRealExecutor(), config)
|
|
4905
|
+
if ((await runSimpleRelease(createRealExecutor(), config)).pushed) {
|
|
4906
|
+
const outputFile = process.env["GITHUB_OUTPUT"];
|
|
4907
|
+
if (outputFile) try {
|
|
4908
|
+
appendFileSync(outputFile, "pushed=true\n");
|
|
4909
|
+
} catch (_error) {}
|
|
4910
|
+
}
|
|
5082
4911
|
}
|
|
5083
4912
|
});
|
|
5084
4913
|
//#endregion
|
|
@@ -5088,13 +4917,13 @@ const CHECKS = [
|
|
|
5088
4917
|
{ name: "typecheck" },
|
|
5089
4918
|
{ name: "lint" },
|
|
5090
4919
|
{ name: "test" },
|
|
5091
|
-
{ name: "docker:build" },
|
|
5092
4920
|
{
|
|
5093
4921
|
name: "format",
|
|
5094
4922
|
args: "--check"
|
|
5095
4923
|
},
|
|
5096
4924
|
{ name: "knip" },
|
|
5097
4925
|
{ name: "tooling:check" },
|
|
4926
|
+
{ name: "docker:build" },
|
|
5098
4927
|
{ name: "docker:check" }
|
|
5099
4928
|
];
|
|
5100
4929
|
/** Check if a name matches any skip pattern. Supports glob syntax via picomatch. */
|
|
@@ -6105,6 +5934,111 @@ const forgejoSecretsCommand = defineCommand({
|
|
|
6105
5934
|
}
|
|
6106
5935
|
});
|
|
6107
5936
|
//#endregion
|
|
5937
|
+
//#region src/release/webhook.ts
|
|
5938
|
+
/** Build a webhook payload from simple release context. */
|
|
5939
|
+
function buildSimplePayload(options) {
|
|
5940
|
+
const repository = options.repository ?? "";
|
|
5941
|
+
const serverUrl = options.serverUrl ?? "https://github.com";
|
|
5942
|
+
const actor = options.actor ?? ciActorName(options.platform ?? "github");
|
|
5943
|
+
return {
|
|
5944
|
+
action: "published",
|
|
5945
|
+
release: {
|
|
5946
|
+
tag_name: options.tag,
|
|
5947
|
+
name: options.tag,
|
|
5948
|
+
draft: false,
|
|
5949
|
+
prerelease: options.prerelease ?? false,
|
|
5950
|
+
author: { login: actor }
|
|
5951
|
+
},
|
|
5952
|
+
repository: {
|
|
5953
|
+
full_name: repository,
|
|
5954
|
+
html_url: repository ? `${serverUrl}/${repository}` : ""
|
|
5955
|
+
},
|
|
5956
|
+
sender: { login: actor }
|
|
5957
|
+
};
|
|
5958
|
+
}
|
|
5959
|
+
/** Build a webhook payload from GitHub Actions environment variables. */
|
|
5960
|
+
function buildPayloadFromEnv() {
|
|
5961
|
+
const repository = process.env["GITHUB_REPOSITORY"];
|
|
5962
|
+
const ref = process.env["GITHUB_REF"];
|
|
5963
|
+
const serverUrl = process.env["GITHUB_SERVER_URL"] ?? "https://github.com";
|
|
5964
|
+
const actor = process.env["GITHUB_ACTOR"] ?? "github-actions[bot]";
|
|
5965
|
+
if (!repository || !ref) return void 0;
|
|
5966
|
+
const tagName = ref.startsWith("refs/tags/") ? ref.slice(10) : ref;
|
|
5967
|
+
return {
|
|
5968
|
+
action: "published",
|
|
5969
|
+
release: {
|
|
5970
|
+
tag_name: tagName,
|
|
5971
|
+
name: tagName,
|
|
5972
|
+
draft: false,
|
|
5973
|
+
prerelease: false,
|
|
5974
|
+
author: { login: actor }
|
|
5975
|
+
},
|
|
5976
|
+
repository: {
|
|
5977
|
+
full_name: repository,
|
|
5978
|
+
html_url: `${serverUrl}/${repository}`
|
|
5979
|
+
},
|
|
5980
|
+
sender: { login: actor }
|
|
5981
|
+
};
|
|
5982
|
+
}
|
|
5983
|
+
/**
|
|
5984
|
+
* Compute the HMAC-SHA256 hex digest of data using the given secret.
|
|
5985
|
+
* Returns the signature in GitHub-style format: `sha256=<hex>`.
|
|
5986
|
+
*/
|
|
5987
|
+
function computeSignature(secret, data) {
|
|
5988
|
+
const hmac = createHmac("sha256", secret);
|
|
5989
|
+
hmac.update(data);
|
|
5990
|
+
return `sha256=${hmac.digest("hex")}`;
|
|
5991
|
+
}
|
|
5992
|
+
/**
|
|
5993
|
+
* POST to a webhook URL with GitHub-style HMAC-SHA256 authentication.
|
|
5994
|
+
*
|
|
5995
|
+
* Sends a JSON-serialized release payload (or empty string if no payload).
|
|
5996
|
+
* If a secret is provided, computes the HMAC-SHA256 signature of the body
|
|
5997
|
+
* and includes it in the `X-Hub-Signature-256` header.
|
|
5998
|
+
*
|
|
5999
|
+
* Never throws — returns a result object indicating success or failure.
|
|
6000
|
+
*/
|
|
6001
|
+
async function sendWebhook(options) {
|
|
6002
|
+
const { url, secret, payload, fetcher = globalThis.fetch } = options;
|
|
6003
|
+
const body = payload ? JSON.stringify(payload) : "";
|
|
6004
|
+
const headers = { "Content-Type": "application/json" };
|
|
6005
|
+
if (secret) headers["X-Hub-Signature-256"] = computeSignature(secret, body);
|
|
6006
|
+
try {
|
|
6007
|
+
const response = await fetcher(url, {
|
|
6008
|
+
method: "POST",
|
|
6009
|
+
headers,
|
|
6010
|
+
body
|
|
6011
|
+
});
|
|
6012
|
+
if (!response.ok) return {
|
|
6013
|
+
ok: false,
|
|
6014
|
+
status: response.status,
|
|
6015
|
+
error: `HTTP ${String(response.status)}: ${await response.text().catch(() => "")}`
|
|
6016
|
+
};
|
|
6017
|
+
return {
|
|
6018
|
+
ok: true,
|
|
6019
|
+
status: response.status
|
|
6020
|
+
};
|
|
6021
|
+
} catch (error) {
|
|
6022
|
+
return {
|
|
6023
|
+
ok: false,
|
|
6024
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6025
|
+
};
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
/**
|
|
6029
|
+
* Send a webhook and log the result. Intended for CLI usage.
|
|
6030
|
+
* Never throws.
|
|
6031
|
+
*/
|
|
6032
|
+
async function sendWebhookWithLogging(url, secret, payload) {
|
|
6033
|
+
const result = await sendWebhook({
|
|
6034
|
+
url,
|
|
6035
|
+
secret,
|
|
6036
|
+
payload
|
|
6037
|
+
});
|
|
6038
|
+
if (result.ok) log.info("Post-release hook completed successfully");
|
|
6039
|
+
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
6040
|
+
}
|
|
6041
|
+
//#endregion
|
|
6108
6042
|
//#region src/commands/webhook-send.ts
|
|
6109
6043
|
const webhookSendCommand = defineCommand({
|
|
6110
6044
|
meta: {
|
|
@@ -6155,7 +6089,7 @@ const webhookSendCommand = defineCommand({
|
|
|
6155
6089
|
const main = defineCommand({
|
|
6156
6090
|
meta: {
|
|
6157
6091
|
name: "bst",
|
|
6158
|
-
version: "0.
|
|
6092
|
+
version: "0.47.0",
|
|
6159
6093
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
6160
6094
|
},
|
|
6161
6095
|
subCommands: {
|
|
@@ -6175,7 +6109,7 @@ const main = defineCommand({
|
|
|
6175
6109
|
"webhook:send": webhookSendCommand
|
|
6176
6110
|
}
|
|
6177
6111
|
});
|
|
6178
|
-
console.log(`@bensandee/tooling v0.
|
|
6112
|
+
console.log(`@bensandee/tooling v0.47.0`);
|
|
6179
6113
|
async function run() {
|
|
6180
6114
|
await runMain(main);
|
|
6181
6115
|
process.exit(process.exitCode ?? 0);
|
package/dist/index.d.mts
CHANGED
|
@@ -57,7 +57,7 @@ interface ProjectConfig {
|
|
|
57
57
|
/** Result from a single generator: what file was written and how. */
|
|
58
58
|
interface GeneratorResult {
|
|
59
59
|
filePath: string;
|
|
60
|
-
action: "created" | "updated" | "skipped" | "archived";
|
|
60
|
+
action: "created" | "updated" | "skipped" | "archived" | "removed";
|
|
61
61
|
/** Human-readable description of what changed */
|
|
62
62
|
description: string;
|
|
63
63
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensandee/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"prettier": "3.8.1",
|
|
55
55
|
"release-it": "19.2.4",
|
|
56
56
|
"tsdown": "0.21.6",
|
|
57
|
-
"@bensandee/
|
|
58
|
-
"@bensandee/
|
|
57
|
+
"@bensandee/claude-hooks": "0.6.4",
|
|
58
|
+
"@bensandee/config": "0.10.1"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "tsdown",
|