tollgate-cli 0.2.1 → 0.2.4
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/README.md +20 -1
- package/dist/tollgate.mjs +646 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,13 @@ Your team approves before your coding agent does something you can't undo.
|
|
|
11
11
|
|
|
12
12
|
Every other command runs untouched; the hook adds about 50 ms.
|
|
13
13
|
|
|
14
|
+
It also gates what an agent could do instead of running the command:
|
|
15
|
+
|
|
16
|
+
- **File edits** to Tollgate's own setup (`.claude/settings*.json`, `.tollgate.json`, the token), deploy and CI files (`.github/workflows/`, `.gitlab-ci.yml`, `vercel`/`netlify`/`fly`/`railway` config) and `.env` files. The card shows the change; `.env` values are hidden.
|
|
17
|
+
- **MCP tools** whose names delete, deploy, publish, merge, refund, charge, transfer or run SQL (`mcp__github__merge_pull_request`, `mcp__supabase__execute_sql`).
|
|
18
|
+
|
|
19
|
+
An approval covers that exact edit or call only. Custom presets take `"tool": "edit"` or `"tool": "mcp"`; edit patterns match `edit <path>` (`write <path>` for a new file), MCP patterns match `mcp__server__tool {json arguments}`.
|
|
20
|
+
|
|
14
21
|
## Set up
|
|
15
22
|
|
|
16
23
|
1. In your team's Tollgate room, open **Settings › Repos › Link a repo** and copy the command it shows.
|
|
@@ -34,9 +41,19 @@ Every other command runs untouched; the hook adds about 50 ms.
|
|
|
34
41
|
| `.claude/settings.local.json` | No (added to `.gitignore`): your personal hook registration |
|
|
35
42
|
| `~/.config/tollgate/` | Never in a repo: your token (file mode 0600) and the hook bundle |
|
|
36
43
|
|
|
44
|
+
## What the hook can see through
|
|
45
|
+
|
|
46
|
+
Agents rarely type the risky command itself. The hook looks at what a command really runs:
|
|
47
|
+
|
|
48
|
+
- **Package scripts:** `pnpm db:deploy`, `npm run release`, `yarn deploy`, `bun run x` are looked up in the nearest `package.json` (with `pre`/`post` scripts and scripts that call other scripts), so the card shows _pnpm db:deploy → prisma migrate deploy_ with the migration SQL.
|
|
49
|
+
- **Lists and wrappers:** every part of `a && b; c | d` is checked on its own, so `npm publish && echo --dry-run` is still a publish. Prefixes like `VAR=x`, `env`, `sudo`, `npx`, `pnpm exec`, `dotenv --` and literal `bash -c '…'`, `eval '…'`, `( … )` and `$( … )` are looked inside.
|
|
50
|
+
- **Code nobody can read first:** `curl … | sh`, `base64 -d | sh`, `bash <(curl …)` and commands built from a variable (`eval "$CMD"`) are gated as _opaque_.
|
|
51
|
+
|
|
52
|
+
Tollgate is not a shell parser and not a sandbox. It does **not** read Makefiles (`make deploy`), shell files (`./deploy.sh`), npm lifecycle scripts like `prepublishOnly`, or workspace scripts selected with `--filter`. It guards against an agent's mistakes, not against someone determined to get around it on their own machine.
|
|
53
|
+
|
|
37
54
|
## What leaves your machine
|
|
38
55
|
|
|
39
|
-
Only for commands that match a preset: the command and working directory after **local redaction** (known token formats, `KEY=value` secrets, passwords in URLs, secret flags, and every value from your `.env*` files), Claude Code's one-line description of the command (shown to approvers as the agent's reason), your git branch, the last commit subject, and for migrations the newest migration's SQL (capped at 2 KB). The card shows how many values were hidden. The server redacts everything again.
|
|
56
|
+
Only for commands that match a preset: the command and working directory after **local redaction** (known token formats, `KEY=value` secrets, passwords in URLs, secret flags, and every value from your `.env*` files), Claude Code's one-line description of the command (shown to approvers as the agent's reason), your git branch, the last commit subject, and for migrations the newest migration's SQL (capped at 2 KB). For a gated edit, the path and the change (capped at 80 lines); for a gated MCP call, the tool name and its arguments, redacted the same way. The card shows how many values were hidden. The server redacts everything again.
|
|
40
57
|
|
|
41
58
|
## Commands
|
|
42
59
|
|
|
@@ -48,6 +65,8 @@ Only for commands that match a preset: the command and working directory after *
|
|
|
48
65
|
| `unlink` | Forget this repo's token on this machine (revoke it in the web app) |
|
|
49
66
|
| `hook` | The hook itself; Claude Code calls it with JSON on stdin |
|
|
50
67
|
|
|
68
|
+
After upgrading from 0.2.3 or earlier, run `init` again: it re-registers the hook for file edits and MCP tools.
|
|
69
|
+
|
|
51
70
|
## Safety notes
|
|
52
71
|
|
|
53
72
|
- **Fails closed.** If the server can't be reached, a risky command is blocked, with a message saying why.
|
package/dist/tollgate.mjs
CHANGED
|
@@ -7,7 +7,8 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/main.ts
|
|
9
9
|
import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
10
|
-
import {
|
|
10
|
+
import { homedir as homedir2 } from "node:os";
|
|
11
|
+
import { join as join3, resolve as resolve2 } from "node:path";
|
|
11
12
|
import { parseArgs } from "node:util";
|
|
12
13
|
|
|
13
14
|
// ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/external.js
|
|
@@ -845,14 +846,14 @@ function promiseAllObject(promisesObj) {
|
|
|
845
846
|
}
|
|
846
847
|
function randomString(length = 10) {
|
|
847
848
|
const chars = "abcdefghijklmnopqrstuvwxyz";
|
|
848
|
-
let
|
|
849
|
+
let str2 = "";
|
|
849
850
|
for (let i = 0; i < length; i++) {
|
|
850
|
-
|
|
851
|
+
str2 += chars[Math.floor(Math.random() * chars.length)];
|
|
851
852
|
}
|
|
852
|
-
return
|
|
853
|
+
return str2;
|
|
853
854
|
}
|
|
854
|
-
function esc(
|
|
855
|
-
return JSON.stringify(
|
|
855
|
+
function esc(str2) {
|
|
856
|
+
return JSON.stringify(str2);
|
|
856
857
|
}
|
|
857
858
|
function slugify(input2) {
|
|
858
859
|
return input2.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -966,8 +967,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
|
|
|
966
967
|
"symbol",
|
|
967
968
|
"undefined"
|
|
968
969
|
]);
|
|
969
|
-
function escapeRegex(
|
|
970
|
-
return
|
|
970
|
+
function escapeRegex(str2) {
|
|
971
|
+
return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
971
972
|
}
|
|
972
973
|
function clone(inst, def, params) {
|
|
973
974
|
const cl = new inst._zod.constr(def ?? inst._zod.def);
|
|
@@ -1233,13 +1234,13 @@ function getSizableOrigin(input2) {
|
|
|
1233
1234
|
return "unknown";
|
|
1234
1235
|
}
|
|
1235
1236
|
var highSurrogate = /[\uD800-\uDBFF]/;
|
|
1236
|
-
function codePointLength(
|
|
1237
|
-
const units =
|
|
1238
|
-
if (!highSurrogate.test(
|
|
1237
|
+
function codePointLength(str2) {
|
|
1238
|
+
const units = str2.length;
|
|
1239
|
+
if (!highSurrogate.test(str2))
|
|
1239
1240
|
return units;
|
|
1240
1241
|
let count = units;
|
|
1241
1242
|
for (let i = 0; i < units - 1; i++) {
|
|
1242
|
-
if ((
|
|
1243
|
+
if ((str2.charCodeAt(i) & 64512) === 55296 && (str2.charCodeAt(i + 1) & 64512) === 56320) {
|
|
1243
1244
|
count--;
|
|
1244
1245
|
i++;
|
|
1245
1246
|
}
|
|
@@ -1750,14 +1751,14 @@ function toDotPath(_path) {
|
|
|
1750
1751
|
return segs.join("");
|
|
1751
1752
|
}
|
|
1752
1753
|
function prettifyError(error62) {
|
|
1753
|
-
const
|
|
1754
|
+
const lines2 = [];
|
|
1754
1755
|
const issues = [...error62.issues].sort((a, b) => (a.path ?? []).length - (b.path ?? []).length);
|
|
1755
1756
|
for (const issue2 of issues) {
|
|
1756
|
-
|
|
1757
|
+
lines2.push(`\u2716 ${issue2.message}`);
|
|
1757
1758
|
if (issue2.path?.length)
|
|
1758
|
-
|
|
1759
|
+
lines2.push(` \u2192 at ${toDotPath(issue2.path)}`);
|
|
1759
1760
|
}
|
|
1760
|
-
return
|
|
1761
|
+
return lines2.join("\n");
|
|
1761
1762
|
}
|
|
1762
1763
|
|
|
1763
1764
|
// ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/parse.js
|
|
@@ -2592,9 +2593,9 @@ var Doc = class {
|
|
|
2592
2593
|
return;
|
|
2593
2594
|
}
|
|
2594
2595
|
const content = arg;
|
|
2595
|
-
const
|
|
2596
|
-
const minIndent = Math.min(...
|
|
2597
|
-
const dedented =
|
|
2596
|
+
const lines2 = content.split("\n").filter((x) => x);
|
|
2597
|
+
const minIndent = Math.min(...lines2.map((x) => x.length - x.trimStart().length));
|
|
2598
|
+
const dedented = lines2.map((x) => x.slice(minIndent)).map((x) => " ".repeat(this.indent * 2) + x);
|
|
2598
2599
|
for (const line of dedented) {
|
|
2599
2600
|
this.content.push(line);
|
|
2600
2601
|
}
|
|
@@ -5077,7 +5078,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
5077
5078
|
var NONE = 0;
|
|
5078
5079
|
var ASSUMED = 1;
|
|
5079
5080
|
var PROVEN = 2;
|
|
5080
|
-
function isRecursive(inst, stack,
|
|
5081
|
+
function isRecursive(inst, stack, resolve3) {
|
|
5081
5082
|
const cached2 = recursive.get(inst);
|
|
5082
5083
|
if (cached2 !== void 0)
|
|
5083
5084
|
return cached2 ? PROVEN : NONE;
|
|
@@ -5087,7 +5088,7 @@ function isRecursive(inst, stack, resolve2) {
|
|
|
5087
5088
|
let result = NONE;
|
|
5088
5089
|
const check2 = (child) => {
|
|
5089
5090
|
if (result !== PROVEN && child?._zod) {
|
|
5090
|
-
const answer = isRecursive(child, stack,
|
|
5091
|
+
const answer = isRecursive(child, stack, resolve3);
|
|
5091
5092
|
if (answer > result)
|
|
5092
5093
|
result = answer;
|
|
5093
5094
|
}
|
|
@@ -5098,7 +5099,7 @@ function isRecursive(inst, stack, resolve2) {
|
|
|
5098
5099
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
5099
5100
|
if (spread && !desc.enumerable)
|
|
5100
5101
|
continue;
|
|
5101
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
5102
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
|
|
5102
5103
|
if (child > answer)
|
|
5103
5104
|
answer = child;
|
|
5104
5105
|
}
|
|
@@ -5162,7 +5163,7 @@ function isRecursive(inst, stack, resolve2) {
|
|
|
5162
5163
|
break;
|
|
5163
5164
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
5164
5165
|
case "lazy": {
|
|
5165
|
-
const inner = def._cachedInner ?? (
|
|
5166
|
+
const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
|
|
5166
5167
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
5167
5168
|
break;
|
|
5168
5169
|
}
|
|
@@ -5242,14 +5243,14 @@ var memo = {
|
|
|
5242
5243
|
var _a3;
|
|
5243
5244
|
(_a3 = inst._zod).deferred ?? (_a3.deferred = []);
|
|
5244
5245
|
inst._zod.deferred.push(() => {
|
|
5245
|
-
const
|
|
5246
|
+
const base2 = inst._zod.parse;
|
|
5246
5247
|
const wrapped = (payload, ctx) => {
|
|
5247
5248
|
if (ctx.direction !== "backward" && isBackEdge(ctx, payload.value))
|
|
5248
5249
|
throw new $ZodCyclicError();
|
|
5249
|
-
return
|
|
5250
|
+
return base2(payload, ctx);
|
|
5250
5251
|
};
|
|
5251
5252
|
inst._zod.parse = wrapped;
|
|
5252
|
-
if (inst._zod.run ===
|
|
5253
|
+
if (inst._zod.run === base2)
|
|
5253
5254
|
inst._zod.run = wrapped;
|
|
5254
5255
|
});
|
|
5255
5256
|
},
|
|
@@ -5261,15 +5262,15 @@ var memo = {
|
|
|
5261
5262
|
let lastBucket;
|
|
5262
5263
|
(_a3 = inst._zod).deferred ?? (_a3.deferred = []);
|
|
5263
5264
|
inst._zod.deferred.push(() => {
|
|
5264
|
-
const
|
|
5265
|
+
const base2 = inst._zod.parse;
|
|
5265
5266
|
const wrapped = (payload, ctx) => {
|
|
5266
5267
|
if (isRecursiveInst === void 0) {
|
|
5267
5268
|
const walked = isRecursive(inst, /* @__PURE__ */ new Set(), false);
|
|
5268
5269
|
if (walked === NONE) {
|
|
5269
|
-
inst._zod.parse =
|
|
5270
|
+
inst._zod.parse = base2;
|
|
5270
5271
|
if (inst._zod.run === wrapped)
|
|
5271
|
-
inst._zod.run =
|
|
5272
|
-
return
|
|
5272
|
+
inst._zod.run = base2;
|
|
5273
|
+
return base2(payload, ctx);
|
|
5273
5274
|
}
|
|
5274
5275
|
if (walked === PROVEN || rechecked)
|
|
5275
5276
|
isRecursiveInst = true;
|
|
@@ -5278,7 +5279,7 @@ var memo = {
|
|
|
5278
5279
|
}
|
|
5279
5280
|
const input2 = payload.value;
|
|
5280
5281
|
if (!isRef(input2))
|
|
5281
|
-
return
|
|
5282
|
+
return base2(payload, ctx);
|
|
5282
5283
|
let state = ctx[STATE];
|
|
5283
5284
|
if (!state) {
|
|
5284
5285
|
state = { buckets: /* @__PURE__ */ new WeakMap(), backEdges: void 0 };
|
|
@@ -5307,7 +5308,7 @@ var memo = {
|
|
|
5307
5308
|
}
|
|
5308
5309
|
handoff = bucket;
|
|
5309
5310
|
const depth = open.length;
|
|
5310
|
-
const result =
|
|
5311
|
+
const result = base2(payload, ctx);
|
|
5311
5312
|
handoff = void 0;
|
|
5312
5313
|
const entry = open.length > depth ? open.pop() : void 0;
|
|
5313
5314
|
if (result instanceof Promise) {
|
|
@@ -5322,7 +5323,7 @@ var memo = {
|
|
|
5322
5323
|
return result;
|
|
5323
5324
|
};
|
|
5324
5325
|
inst._zod.parse = wrapped;
|
|
5325
|
-
if (inst._zod.run ===
|
|
5326
|
+
if (inst._zod.run === base2)
|
|
5326
5327
|
inst._zod.run = wrapped;
|
|
5327
5328
|
});
|
|
5328
5329
|
}
|
|
@@ -13231,9 +13232,9 @@ function codePointLengthVar(doc, ctx, accessor, inDoubt) {
|
|
|
13231
13232
|
doc.write(`const ${v} = typeof ${accessor} === "string" && ${inDoubt} ? ${cpLen}(${accessor}) : ${accessor}.length;`);
|
|
13232
13233
|
return v;
|
|
13233
13234
|
}
|
|
13234
|
-
function numericOperand(value,
|
|
13235
|
+
function numericOperand(value, label2) {
|
|
13235
13236
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
13236
|
-
throw new ZodCompileUnsupportedError(`${
|
|
13237
|
+
throw new ZodCompileUnsupportedError(`${label2} bound of type ${typeof value}`);
|
|
13237
13238
|
}
|
|
13238
13239
|
return `${value}`;
|
|
13239
13240
|
}
|
|
@@ -19727,8 +19728,347 @@ function date4(params) {
|
|
|
19727
19728
|
return _coercedDate(ZodDate, params);
|
|
19728
19729
|
}
|
|
19729
19730
|
|
|
19731
|
+
// ../../features/gates/shared/expand.ts
|
|
19732
|
+
var MAX_DEPTH = 4;
|
|
19733
|
+
var MAX_SEGMENTS = 64;
|
|
19734
|
+
var MAX_LABEL = 80;
|
|
19735
|
+
function scan(text, onChar) {
|
|
19736
|
+
const state = { quote: null, depth: 0, backtick: false };
|
|
19737
|
+
for (let i = 0; i < text.length; i++) {
|
|
19738
|
+
const ch = text[i];
|
|
19739
|
+
const skip = onChar(i, state);
|
|
19740
|
+
if (typeof skip === "number") {
|
|
19741
|
+
i = skip;
|
|
19742
|
+
continue;
|
|
19743
|
+
}
|
|
19744
|
+
if (state.quote === "'") {
|
|
19745
|
+
if (ch === "'") state.quote = null;
|
|
19746
|
+
continue;
|
|
19747
|
+
}
|
|
19748
|
+
if (ch === "\\") {
|
|
19749
|
+
i += 1;
|
|
19750
|
+
continue;
|
|
19751
|
+
}
|
|
19752
|
+
if (state.quote === '"') {
|
|
19753
|
+
if (ch === '"') state.quote = null;
|
|
19754
|
+
else if (ch === "$" && text[i + 1] === "(") {
|
|
19755
|
+
state.depth += 1;
|
|
19756
|
+
i += 1;
|
|
19757
|
+
} else if (ch === ")" && state.depth > 0) state.depth -= 1;
|
|
19758
|
+
continue;
|
|
19759
|
+
}
|
|
19760
|
+
if (ch === "'" || ch === '"') state.quote = ch;
|
|
19761
|
+
else if (ch === "`") state.backtick = !state.backtick;
|
|
19762
|
+
else if (ch === "(") state.depth += 1;
|
|
19763
|
+
else if (ch === ")" && state.depth > 0) state.depth -= 1;
|
|
19764
|
+
}
|
|
19765
|
+
}
|
|
19766
|
+
var atTop = (s) => s.quote === null && s.depth === 0 && !s.backtick;
|
|
19767
|
+
function splitList(text) {
|
|
19768
|
+
const parts = [];
|
|
19769
|
+
let start = 0;
|
|
19770
|
+
scan(text, (i, s) => {
|
|
19771
|
+
if (!atTop(s)) return;
|
|
19772
|
+
const ch = text[i];
|
|
19773
|
+
const next = text[i + 1];
|
|
19774
|
+
const prev = text[i - 1];
|
|
19775
|
+
if (ch === "#" && (i === 0 || /\s/.test(prev))) {
|
|
19776
|
+
parts.push(text.slice(start, i));
|
|
19777
|
+
const end = text.indexOf("\n", i);
|
|
19778
|
+
start = end === -1 ? text.length : end + 1;
|
|
19779
|
+
return end === -1 ? text.length : end;
|
|
19780
|
+
}
|
|
19781
|
+
if (ch === ";" || ch === "\n") {
|
|
19782
|
+
parts.push(text.slice(start, i));
|
|
19783
|
+
start = i + 1;
|
|
19784
|
+
} else if (ch === "&" && next === "&" || ch === "|" && next === "|") {
|
|
19785
|
+
parts.push(text.slice(start, i));
|
|
19786
|
+
start = i + 2;
|
|
19787
|
+
return i + 1;
|
|
19788
|
+
} else if (ch === "&" && prev !== ">" && prev !== "<" && prev !== "|" && next !== ">") {
|
|
19789
|
+
parts.push(text.slice(start, i));
|
|
19790
|
+
start = i + 1;
|
|
19791
|
+
}
|
|
19792
|
+
});
|
|
19793
|
+
parts.push(text.slice(start));
|
|
19794
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
19795
|
+
}
|
|
19796
|
+
function splitPipeline(text) {
|
|
19797
|
+
const parts = [];
|
|
19798
|
+
let start = 0;
|
|
19799
|
+
scan(text, (i, s) => {
|
|
19800
|
+
if (!atTop(s) || text[i] !== "|") return;
|
|
19801
|
+
parts.push(text.slice(start, i));
|
|
19802
|
+
const skipAmp = text[i + 1] === "&";
|
|
19803
|
+
start = i + (skipAmp ? 2 : 1);
|
|
19804
|
+
return skipAmp ? i + 1 : void 0;
|
|
19805
|
+
});
|
|
19806
|
+
parts.push(text.slice(start));
|
|
19807
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
19808
|
+
}
|
|
19809
|
+
function shellWords(text) {
|
|
19810
|
+
const words = [];
|
|
19811
|
+
let current = "";
|
|
19812
|
+
let inWord = false;
|
|
19813
|
+
let quote = null;
|
|
19814
|
+
let depth = 0;
|
|
19815
|
+
let backtick = false;
|
|
19816
|
+
for (let i = 0; i < text.length; i++) {
|
|
19817
|
+
const ch = text[i];
|
|
19818
|
+
if (quote === "'") {
|
|
19819
|
+
if (ch === "'") quote = null;
|
|
19820
|
+
else current += ch;
|
|
19821
|
+
continue;
|
|
19822
|
+
}
|
|
19823
|
+
if (ch === "\\" && i + 1 < text.length) {
|
|
19824
|
+
current += text[i + 1];
|
|
19825
|
+
inWord = true;
|
|
19826
|
+
i += 1;
|
|
19827
|
+
continue;
|
|
19828
|
+
}
|
|
19829
|
+
if (quote === '"') {
|
|
19830
|
+
if (ch === '"' && depth === 0) quote = null;
|
|
19831
|
+
else {
|
|
19832
|
+
if (ch === "$" && text[i + 1] === "(") depth += 1;
|
|
19833
|
+
else if (ch === ")" && depth > 0) depth -= 1;
|
|
19834
|
+
current += ch;
|
|
19835
|
+
}
|
|
19836
|
+
continue;
|
|
19837
|
+
}
|
|
19838
|
+
if (depth > 0 || backtick) {
|
|
19839
|
+
if (ch === "(") depth += 1;
|
|
19840
|
+
else if (ch === ")") depth -= 1;
|
|
19841
|
+
else if (ch === "`") backtick = false;
|
|
19842
|
+
current += ch;
|
|
19843
|
+
continue;
|
|
19844
|
+
}
|
|
19845
|
+
if (/\s/.test(ch)) {
|
|
19846
|
+
if (inWord) words.push(current);
|
|
19847
|
+
current = "";
|
|
19848
|
+
inWord = false;
|
|
19849
|
+
continue;
|
|
19850
|
+
}
|
|
19851
|
+
inWord = true;
|
|
19852
|
+
if (ch === "'" || ch === '"') quote = ch;
|
|
19853
|
+
else if (ch === "$" && text[i + 1] === "(") {
|
|
19854
|
+
depth = 1;
|
|
19855
|
+
current += "$(";
|
|
19856
|
+
i += 1;
|
|
19857
|
+
} else if (ch === "`") {
|
|
19858
|
+
backtick = true;
|
|
19859
|
+
current += ch;
|
|
19860
|
+
} else current += ch;
|
|
19861
|
+
}
|
|
19862
|
+
if (inWord) words.push(current);
|
|
19863
|
+
return words;
|
|
19864
|
+
}
|
|
19865
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
19866
|
+
var base = (word) => word.slice(word.lastIndexOf("/") + 1);
|
|
19867
|
+
function skipFlags(words, withValue = /* @__PURE__ */ new Set()) {
|
|
19868
|
+
let i = 0;
|
|
19869
|
+
while (i < words.length && words[i].startsWith("-") && words[i] !== "--") {
|
|
19870
|
+
i += withValue.has(words[i]) ? 2 : 1;
|
|
19871
|
+
}
|
|
19872
|
+
if (words[i] === "--") i += 1;
|
|
19873
|
+
return words.slice(i);
|
|
19874
|
+
}
|
|
19875
|
+
var SUDO_VALUE = /* @__PURE__ */ new Set(["-u", "-g", "-C", "-D", "-h", "-p", "-r", "-t", "-U", "-T"]);
|
|
19876
|
+
var NPX_VALUE = /* @__PURE__ */ new Set(["-p", "--package", "-c", "--call"]);
|
|
19877
|
+
var ENV_VALUE = /* @__PURE__ */ new Set(["-u", "--unset", "-C", "--chdir", "-S", "--split-string"]);
|
|
19878
|
+
function stripPrefixes(input2) {
|
|
19879
|
+
let words = input2;
|
|
19880
|
+
for (let guard = 0; guard < 16 && words.length > 0; guard++) {
|
|
19881
|
+
const first = base(words[0]);
|
|
19882
|
+
const second = words[1];
|
|
19883
|
+
let next = null;
|
|
19884
|
+
if (ASSIGNMENT.test(words[0])) next = words.slice(1);
|
|
19885
|
+
else if (first === "env") {
|
|
19886
|
+
next = skipFlags(words.slice(1), ENV_VALUE);
|
|
19887
|
+
while (next.length > 0 && ASSIGNMENT.test(next[0])) next = next.slice(1);
|
|
19888
|
+
} else if (first === "sudo" || first === "doas") next = skipFlags(words.slice(1), SUDO_VALUE);
|
|
19889
|
+
else if (["time", "nohup", "command", "exec", "builtin"].includes(first)) {
|
|
19890
|
+
next = skipFlags(words.slice(1));
|
|
19891
|
+
} else if (first === "nice") next = skipFlags(words.slice(1), /* @__PURE__ */ new Set(["-n"]));
|
|
19892
|
+
else if (first === "npx" || first === "bunx") next = skipFlags(words.slice(1), NPX_VALUE);
|
|
19893
|
+
else if (first === "pnpm" && (second === "exec" || second === "dlx")) {
|
|
19894
|
+
next = skipFlags(words.slice(2));
|
|
19895
|
+
} else if (first === "yarn" && (second === "dlx" || second === "exec")) {
|
|
19896
|
+
next = skipFlags(words.slice(2));
|
|
19897
|
+
} else if (first === "npm" && (second === "exec" || second === "x")) {
|
|
19898
|
+
next = skipFlags(words.slice(2), NPX_VALUE);
|
|
19899
|
+
} else if (first === "dotenv" || first === "dotenvx") {
|
|
19900
|
+
const dash = words.indexOf("--");
|
|
19901
|
+
next = dash === -1 ? skipFlags(words.slice(1), /* @__PURE__ */ new Set(["-e", "-f", "-v", "-c"])) : words.slice(dash + 1);
|
|
19902
|
+
if (first === "dotenvx" && next[0] === "run")
|
|
19903
|
+
next = skipFlags(next.slice(1), /* @__PURE__ */ new Set(["-f"]));
|
|
19904
|
+
} else if (first === "cross-env") {
|
|
19905
|
+
next = words.slice(1);
|
|
19906
|
+
while (next.length > 0 && ASSIGNMENT.test(next[0])) next = next.slice(1);
|
|
19907
|
+
}
|
|
19908
|
+
if (next === null) break;
|
|
19909
|
+
words = next;
|
|
19910
|
+
}
|
|
19911
|
+
return words;
|
|
19912
|
+
}
|
|
19913
|
+
var SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
19914
|
+
function innerScript(words) {
|
|
19915
|
+
if (words.length === 0) return null;
|
|
19916
|
+
const first = base(words[0]);
|
|
19917
|
+
if (SHELLS.has(first)) {
|
|
19918
|
+
const flag = words.findIndex((word, i) => i > 0 && /^-[A-Za-z]*c[A-Za-z]*$/.test(word));
|
|
19919
|
+
return flag !== -1 && words[flag + 1] !== void 0 ? words[flag + 1] : null;
|
|
19920
|
+
}
|
|
19921
|
+
if (first === "eval") return words.slice(1).join(" ") || null;
|
|
19922
|
+
return null;
|
|
19923
|
+
}
|
|
19924
|
+
function substitutions(text) {
|
|
19925
|
+
const found = [];
|
|
19926
|
+
for (let i = 0; i < text.length; i++) {
|
|
19927
|
+
if (text[i] === "'") {
|
|
19928
|
+
const end = text.indexOf("'", i + 1);
|
|
19929
|
+
if (end === -1) break;
|
|
19930
|
+
i = end;
|
|
19931
|
+
continue;
|
|
19932
|
+
}
|
|
19933
|
+
if (text[i] === "$" && text[i + 1] === "(" && text[i + 2] !== "(") {
|
|
19934
|
+
let depth = 1;
|
|
19935
|
+
let j = i + 2;
|
|
19936
|
+
for (; j < text.length && depth > 0; j++) {
|
|
19937
|
+
if (text[j] === "(") depth += 1;
|
|
19938
|
+
else if (text[j] === ")") depth -= 1;
|
|
19939
|
+
}
|
|
19940
|
+
found.push(text.slice(i + 2, j - 1));
|
|
19941
|
+
i = j - 1;
|
|
19942
|
+
} else if (text[i] === "`") {
|
|
19943
|
+
const end = text.indexOf("`", i + 1);
|
|
19944
|
+
if (end === -1) break;
|
|
19945
|
+
found.push(text.slice(i + 1, end));
|
|
19946
|
+
i = end;
|
|
19947
|
+
}
|
|
19948
|
+
}
|
|
19949
|
+
return found.filter((inner) => inner.trim());
|
|
19950
|
+
}
|
|
19951
|
+
var PNPM_BUILTINS = new Set(
|
|
19952
|
+
"add i install update up upgrade remove rm uninstall un link ln unlink import rebuild rb prune fetch patch patch-commit exec dlx create publish pack audit list ls outdated why store root bin config c get set env init setup server deploy licenses doctor approve-builds ignored-builds self-update cat-file cat-index find-hash recursive multi m help".split(
|
|
19953
|
+
" "
|
|
19954
|
+
)
|
|
19955
|
+
);
|
|
19956
|
+
var YARN_BUILTINS = new Set(
|
|
19957
|
+
"add install remove upgrade up publish init info why dlx exec workspace workspaces config cache link unlink pack version global import npm plugin set constraints dedupe explain node rebuild search stage unplug patch patch-commit login logout owner tag team audit autoclean check generate-lock-entry help licenses list outdated policies upgrade-interactive".split(
|
|
19958
|
+
" "
|
|
19959
|
+
)
|
|
19960
|
+
);
|
|
19961
|
+
var RUNNER_VALUE = /* @__PURE__ */ new Set([
|
|
19962
|
+
"-C",
|
|
19963
|
+
"--dir",
|
|
19964
|
+
"--prefix",
|
|
19965
|
+
"--filter",
|
|
19966
|
+
"-F",
|
|
19967
|
+
"-w",
|
|
19968
|
+
"--workspace",
|
|
19969
|
+
"--cwd"
|
|
19970
|
+
]);
|
|
19971
|
+
var NPM_LIFECYCLE = {
|
|
19972
|
+
test: "test",
|
|
19973
|
+
t: "test",
|
|
19974
|
+
tst: "test",
|
|
19975
|
+
start: "start",
|
|
19976
|
+
stop: "stop",
|
|
19977
|
+
restart: "restart"
|
|
19978
|
+
};
|
|
19979
|
+
function scriptName(words, scripts) {
|
|
19980
|
+
if (words.length < 2) return null;
|
|
19981
|
+
const tool = base(words[0]);
|
|
19982
|
+
const rest = skipFlags(words.slice(1), RUNNER_VALUE);
|
|
19983
|
+
const sub = rest[0];
|
|
19984
|
+
if (!sub) return null;
|
|
19985
|
+
const after = skipFlags(rest.slice(1), RUNNER_VALUE)[0] ?? null;
|
|
19986
|
+
if (tool === "npm") {
|
|
19987
|
+
if (["run", "run-script", "rum", "urn"].includes(sub)) return after;
|
|
19988
|
+
return NPM_LIFECYCLE[sub] ?? null;
|
|
19989
|
+
}
|
|
19990
|
+
if (tool === "pnpm") {
|
|
19991
|
+
if (sub === "run" || sub === "run-script") return after;
|
|
19992
|
+
return PNPM_BUILTINS.has(sub) ? null : sub;
|
|
19993
|
+
}
|
|
19994
|
+
if (tool === "yarn") {
|
|
19995
|
+
if (sub === "run") return after;
|
|
19996
|
+
return YARN_BUILTINS.has(sub) ? null : sub;
|
|
19997
|
+
}
|
|
19998
|
+
if (tool === "bun") {
|
|
19999
|
+
if (sub === "run") return after;
|
|
20000
|
+
return scripts && sub in scripts ? sub : null;
|
|
20001
|
+
}
|
|
20002
|
+
return null;
|
|
20003
|
+
}
|
|
20004
|
+
function label(words) {
|
|
20005
|
+
const text = words.join(" ");
|
|
20006
|
+
return text.length > MAX_LABEL ? `${text.slice(0, MAX_LABEL - 1)}\u2026` : text;
|
|
20007
|
+
}
|
|
20008
|
+
function expandCommand(command, options) {
|
|
20009
|
+
const maxDepth = options.maxDepth ?? MAX_DEPTH;
|
|
20010
|
+
const maxSegments = options.maxSegments ?? MAX_SEGMENTS;
|
|
20011
|
+
const segments = [];
|
|
20012
|
+
let scripts;
|
|
20013
|
+
const loadScripts = () => {
|
|
20014
|
+
if (scripts === void 0) {
|
|
20015
|
+
try {
|
|
20016
|
+
scripts = options.readScripts?.(options.cwd) ?? null;
|
|
20017
|
+
} catch {
|
|
20018
|
+
scripts = null;
|
|
20019
|
+
}
|
|
20020
|
+
}
|
|
20021
|
+
return scripts;
|
|
20022
|
+
};
|
|
20023
|
+
const visit2 = (text, via, depth, seen) => {
|
|
20024
|
+
for (const pipeline of splitList(text)) {
|
|
20025
|
+
if (segments.length >= maxSegments) return;
|
|
20026
|
+
const group = /^\(([\s\S]*)\)$/.exec(pipeline) ?? /^\{([\s\S]*?);?\s*\}$/.exec(pipeline);
|
|
20027
|
+
if (group) {
|
|
20028
|
+
if (depth < maxDepth) visit2(group[1], via, depth + 1, seen);
|
|
20029
|
+
continue;
|
|
20030
|
+
}
|
|
20031
|
+
const commands = splitPipeline(pipeline).map((part) => stripPrefixes(shellWords(part)));
|
|
20032
|
+
const flat = commands.map((words) => words.join(" ")).filter(Boolean).join(" | ");
|
|
20033
|
+
if (flat) segments.push({ text: flat, via });
|
|
20034
|
+
if (depth >= maxDepth) continue;
|
|
20035
|
+
for (const inner of substitutions(pipeline)) {
|
|
20036
|
+
visit2(inner, [...via, "$(\u2026)"], depth + 1, seen);
|
|
20037
|
+
}
|
|
20038
|
+
for (const words of commands) {
|
|
20039
|
+
const script = innerScript(words);
|
|
20040
|
+
if (script) {
|
|
20041
|
+
const wrapper = base(words[0]) === "eval" ? "eval" : `${base(words[0])} -c`;
|
|
20042
|
+
visit2(script, [...via, wrapper], depth + 1, seen);
|
|
20043
|
+
}
|
|
20044
|
+
const name = scriptName(words, loadScripts() ?? null);
|
|
20045
|
+
const all = loadScripts();
|
|
20046
|
+
if (!name || !all || seen.has(name) || !(name in all)) continue;
|
|
20047
|
+
const nextSeen = new Set(seen).add(name);
|
|
20048
|
+
const from = [...via, label(words)];
|
|
20049
|
+
for (const key of [`pre${name}`, name, `post${name}`]) {
|
|
20050
|
+
const body = all[key];
|
|
20051
|
+
if (typeof body === "string" && body.trim()) {
|
|
20052
|
+
visit2(body.slice(0, 2e3), from, depth + 1, nextSeen);
|
|
20053
|
+
}
|
|
20054
|
+
}
|
|
20055
|
+
}
|
|
20056
|
+
}
|
|
20057
|
+
};
|
|
20058
|
+
visit2(command, [], 0, /* @__PURE__ */ new Set());
|
|
20059
|
+
return segments;
|
|
20060
|
+
}
|
|
20061
|
+
|
|
19730
20062
|
// ../../features/gates/shared/presets.ts
|
|
20063
|
+
var GATE_TOOLS = ["bash", "edit", "mcp"];
|
|
20064
|
+
var TOOL_NOUN = {
|
|
20065
|
+
bash: "command",
|
|
20066
|
+
edit: "edit",
|
|
20067
|
+
mcp: "tool call"
|
|
20068
|
+
};
|
|
19731
20069
|
var presetSchema = external_exports.object({
|
|
20070
|
+
// Defaults to "bash", so presets written before file and MCP gates keep their meaning.
|
|
20071
|
+
tool: external_exports.enum(GATE_TOOLS).optional(),
|
|
19732
20072
|
name: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,39}$/).min(1),
|
|
19733
20073
|
patterns: external_exports.array(external_exports.string().min(1).max(300)).min(1).max(20),
|
|
19734
20074
|
// A match is skipped when any of these also matches (e.g. --dry-run changes nothing).
|
|
@@ -19786,7 +20126,8 @@ var DEFAULT_PRESETS = [
|
|
|
19786
20126
|
{
|
|
19787
20127
|
name: "publish",
|
|
19788
20128
|
patterns: [
|
|
19789
|
-
|
|
20129
|
+
// Flags (and their values) may sit between the tool and "publish": pnpm --filter cli publish.
|
|
20130
|
+
"\\b(npm|pnpm|yarn)\\b[^;&|\\n]*\\s+publish(\\s|$)",
|
|
19790
20131
|
"\\bcargo\\s+publish\\b",
|
|
19791
20132
|
"\\btwine\\s+upload\\b"
|
|
19792
20133
|
],
|
|
@@ -19797,8 +20138,8 @@ var DEFAULT_PRESETS = [
|
|
|
19797
20138
|
{
|
|
19798
20139
|
name: "infra",
|
|
19799
20140
|
patterns: [
|
|
19800
|
-
"\\bterraform\\s+(apply|destroy)\\
|
|
19801
|
-
"\\bpulumi\\s+(up|destroy)\\
|
|
20141
|
+
"\\bterraform\\b[^;&|\\n]*\\s+(apply|destroy)(\\s|$)",
|
|
20142
|
+
"\\bpulumi\\b[^;&|\\n]*\\s+(up|destroy)(\\s|$)",
|
|
19802
20143
|
"\\baws\\s+.*\\bdelete-[a-z-]+\\b",
|
|
19803
20144
|
"\\baws\\s+s3\\s+(rm|rb)\\b",
|
|
19804
20145
|
"\\bgcloud\\s+.*\\bdelete\\b"
|
|
@@ -19806,6 +20147,21 @@ var DEFAULT_PRESETS = [
|
|
|
19806
20147
|
unless: [DRY_RUN],
|
|
19807
20148
|
quorum: 2
|
|
19808
20149
|
},
|
|
20150
|
+
{
|
|
20151
|
+
// Code Tollgate can't read before it runs: fetched from the network, decoded, or built
|
|
20152
|
+
// from a variable at run time. A person should look before it runs.
|
|
20153
|
+
name: "opaque",
|
|
20154
|
+
patterns: [
|
|
20155
|
+
"\\b(curl|wget)\\b[^|;&]*\\|\\s*(sudo(\\s+-\\S+)*\\s+)?(env\\s+)?(ba|z|da|k)?sh\\b",
|
|
20156
|
+
"\\bbase64\\s+(-d|-D|--decode)\\b[^|;&]*\\|\\s*(sudo\\s+)?(ba|z|da|k)?sh\\b",
|
|
20157
|
+
"\\b(ba|z|da|k)?sh\\s+<\\(",
|
|
20158
|
+
"(^|[\\s;&|])(source|\\.)\\s+<\\(",
|
|
20159
|
+
"(^|[\\s;&|])eval\\s+[\"']?[$`]",
|
|
20160
|
+
"\\b(ba|z|da|k)?sh(\\s+-\\w+)*\\s+-\\w*c\\s+[\"']?[$`]"
|
|
20161
|
+
],
|
|
20162
|
+
env: "other",
|
|
20163
|
+
quorum: 1
|
|
20164
|
+
},
|
|
19809
20165
|
{
|
|
19810
20166
|
name: "destructive-fs",
|
|
19811
20167
|
patterns: [
|
|
@@ -19814,6 +20170,52 @@ var DEFAULT_PRESETS = [
|
|
|
19814
20170
|
env: "other",
|
|
19815
20171
|
quorum: 1,
|
|
19816
20172
|
outsideRepoOnly: true
|
|
20173
|
+
},
|
|
20174
|
+
// File edits are matched as "edit <path>" or "write <path>", the path relative to the repo
|
|
20175
|
+
// (or ~/… outside it).
|
|
20176
|
+
{
|
|
20177
|
+
// An agent that may not run a command could otherwise switch the guard off: the hook's
|
|
20178
|
+
// registration, the repo's presets, or this machine's Tollgate token.
|
|
20179
|
+
name: "guard-config",
|
|
20180
|
+
tool: "edit",
|
|
20181
|
+
patterns: [
|
|
20182
|
+
"(^|[\\s/])\\.claude/settings(\\.local)?\\.json$",
|
|
20183
|
+
"(^|[\\s/])\\.tollgate\\.json$",
|
|
20184
|
+
"(^|[\\s/])\\.config/tollgate/"
|
|
20185
|
+
],
|
|
20186
|
+
env: "other",
|
|
20187
|
+
quorum: 1
|
|
20188
|
+
},
|
|
20189
|
+
{
|
|
20190
|
+
// Where a deploy is defined: a change here ships with the next push, gate or not.
|
|
20191
|
+
name: "ci-config",
|
|
20192
|
+
tool: "edit",
|
|
20193
|
+
patterns: [
|
|
20194
|
+
"(^|[\\s/])\\.github/workflows/",
|
|
20195
|
+
"(^|[\\s/])\\.gitlab-ci\\.ya?ml$",
|
|
20196
|
+
"(^|[\\s/])(vercel|netlify)\\.(json|toml)$",
|
|
20197
|
+
"(^|[\\s/])(fly|railway)\\.(json|toml)$"
|
|
20198
|
+
],
|
|
20199
|
+
quorum: 1,
|
|
20200
|
+
prodQuorum: 2
|
|
20201
|
+
},
|
|
20202
|
+
{
|
|
20203
|
+
name: "env-file",
|
|
20204
|
+
tool: "edit",
|
|
20205
|
+
patterns: ["(^|[\\s/])\\.env(\\.[^/\\s]+)?$"],
|
|
20206
|
+
unless: ["\\.env\\.(example|sample|template|defaults)$"],
|
|
20207
|
+
quorum: 1
|
|
20208
|
+
},
|
|
20209
|
+
{
|
|
20210
|
+
// MCP calls are matched as "mcp__server__tool {json args}". Tool names that move money,
|
|
20211
|
+
// ship, merge or destroy, whichever server they come from.
|
|
20212
|
+
name: "mcp-risky",
|
|
20213
|
+
tool: "mcp",
|
|
20214
|
+
patterns: [
|
|
20215
|
+
"^mcp__\\S+?__(\\S*_)?(delete|drop|destroy|deploy|publish|merge|refund|charge|payout|transfer|execute_sql|apply_migration)(_\\S*)?(\\s|$)"
|
|
20216
|
+
],
|
|
20217
|
+
quorum: 1,
|
|
20218
|
+
prodQuorum: 2
|
|
19817
20219
|
}
|
|
19818
20220
|
];
|
|
19819
20221
|
var PROD_RE = /(^|[\s=:/_-])(prod|production)($|[\s.:/_-])|--prod\b/i;
|
|
@@ -19834,8 +20236,8 @@ function compile2(source) {
|
|
|
19834
20236
|
}
|
|
19835
20237
|
return regexCache.get(source) ?? null;
|
|
19836
20238
|
}
|
|
19837
|
-
function resolvePath(
|
|
19838
|
-
const parts = (target.startsWith("/") ? target : `${
|
|
20239
|
+
function resolvePath(base2, target) {
|
|
20240
|
+
const parts = (target.startsWith("/") ? target : `${base2}/${target}`).split("/");
|
|
19839
20241
|
const out = [];
|
|
19840
20242
|
for (const part of parts) {
|
|
19841
20243
|
if (part === "" || part === ".") continue;
|
|
@@ -19861,20 +20263,62 @@ function rmTargetsOutsideRepo(command, cwd, repoRoot) {
|
|
|
19861
20263
|
}
|
|
19862
20264
|
return false;
|
|
19863
20265
|
}
|
|
20266
|
+
function envOf(tool, text) {
|
|
20267
|
+
if (tool === "edit") return detectEnv(text.replace(/\./g, "/"));
|
|
20268
|
+
if (tool === "mcp") return detectEnv(text.replace(/["{},]/g, " "));
|
|
20269
|
+
return detectEnv(text);
|
|
20270
|
+
}
|
|
19864
20271
|
function matchPreset(presets, input2) {
|
|
20272
|
+
const tool = input2.tool ?? "bash";
|
|
19865
20273
|
for (const preset of presets) {
|
|
20274
|
+
if ((preset.tool ?? "bash") !== tool) continue;
|
|
19866
20275
|
const hit = preset.patterns.some((source) => compile2(source)?.test(input2.command) ?? false);
|
|
19867
20276
|
if (!hit) continue;
|
|
19868
20277
|
if (preset.unless?.some((source) => compile2(source)?.test(input2.command) ?? false)) continue;
|
|
19869
20278
|
if (preset.outsideRepoOnly && !rmTargetsOutsideRepo(input2.command, input2.cwd, input2.repoRoot)) {
|
|
19870
20279
|
continue;
|
|
19871
20280
|
}
|
|
19872
|
-
const env = preset.env ??
|
|
20281
|
+
const env = preset.env ?? envOf(tool, input2.command);
|
|
19873
20282
|
const quorum = env === "prod" ? Math.max(preset.quorum, preset.prodQuorum ?? 0) : preset.quorum;
|
|
19874
20283
|
return { preset, env, quorum };
|
|
19875
20284
|
}
|
|
19876
20285
|
return null;
|
|
19877
20286
|
}
|
|
20287
|
+
function withDefaultPresets(presets) {
|
|
20288
|
+
const defaults = new Map(DEFAULT_PRESETS.map((preset) => [preset.name, preset]));
|
|
20289
|
+
const names = new Set(presets.map((preset) => preset.name));
|
|
20290
|
+
const merged = presets.map((preset) => {
|
|
20291
|
+
const builtIn = defaults.get(preset.name);
|
|
20292
|
+
return builtIn ? {
|
|
20293
|
+
...preset,
|
|
20294
|
+
tool: builtIn.tool,
|
|
20295
|
+
patterns: [.../* @__PURE__ */ new Set([...preset.patterns, ...builtIn.patterns])]
|
|
20296
|
+
} : preset;
|
|
20297
|
+
});
|
|
20298
|
+
return [...merged, ...DEFAULT_PRESETS.filter((preset) => !names.has(preset.name))];
|
|
20299
|
+
}
|
|
20300
|
+
var ENV_RANK = { prod: 2, staging: 1, other: 0 };
|
|
20301
|
+
function matchCommand(presets, input2) {
|
|
20302
|
+
const tool = input2.tool ?? "bash";
|
|
20303
|
+
const pieces = tool === "bash" ? [
|
|
20304
|
+
{ text: input2.command, via: [] },
|
|
20305
|
+
...expandCommand(input2.command, { cwd: input2.cwd, readScripts: input2.readScripts }),
|
|
20306
|
+
...input2.extra ?? []
|
|
20307
|
+
] : [{ text: input2.command, via: [] }];
|
|
20308
|
+
let best = null;
|
|
20309
|
+
for (const piece of pieces) {
|
|
20310
|
+
const match = matchPreset(presets, {
|
|
20311
|
+
command: piece.text,
|
|
20312
|
+
cwd: input2.cwd,
|
|
20313
|
+
repoRoot: input2.repoRoot,
|
|
20314
|
+
tool
|
|
20315
|
+
});
|
|
20316
|
+
if (!match) continue;
|
|
20317
|
+
const better = best === null || match.quorum > best.quorum || match.quorum === best.quorum && ENV_RANK[match.env] > ENV_RANK[best.env] || match.quorum === best.quorum && match.env === best.env && best.via.length === 0 && piece.via.length > 0 && best.preset.name === match.preset.name;
|
|
20318
|
+
if (better) best = { ...match, segment: piece.text, via: piece.via };
|
|
20319
|
+
}
|
|
20320
|
+
return best;
|
|
20321
|
+
}
|
|
19878
20322
|
|
|
19879
20323
|
// src/api.ts
|
|
19880
20324
|
var REQUEST_TIMEOUT_MS = 5e3;
|
|
@@ -19954,6 +20398,28 @@ function findRepoRoot(cwd) {
|
|
|
19954
20398
|
dir = parent;
|
|
19955
20399
|
}
|
|
19956
20400
|
}
|
|
20401
|
+
function readScripts(cwd, repoRoot) {
|
|
20402
|
+
let dir = cwd;
|
|
20403
|
+
for (; ; ) {
|
|
20404
|
+
const file2 = join(dir, "package.json");
|
|
20405
|
+
if (existsSync(file2)) {
|
|
20406
|
+
try {
|
|
20407
|
+
const scripts = JSON.parse(readFileSync(file2, "utf8")).scripts;
|
|
20408
|
+
if (!scripts || typeof scripts !== "object") return null;
|
|
20409
|
+
return Object.fromEntries(
|
|
20410
|
+
Object.entries(scripts).filter(
|
|
20411
|
+
(entry) => typeof entry[1] === "string"
|
|
20412
|
+
)
|
|
20413
|
+
);
|
|
20414
|
+
} catch {
|
|
20415
|
+
return null;
|
|
20416
|
+
}
|
|
20417
|
+
}
|
|
20418
|
+
const parent = dirname(dir);
|
|
20419
|
+
if (dir === repoRoot || parent === dir || !dir.startsWith(repoRoot)) return null;
|
|
20420
|
+
dir = parent;
|
|
20421
|
+
}
|
|
20422
|
+
}
|
|
19957
20423
|
function readConfigFile(repoRoot) {
|
|
19958
20424
|
const raw = JSON.parse(readFileSync(join(repoRoot, CONFIG_FILE), "utf8"));
|
|
19959
20425
|
return configSchema.parse(raw);
|
|
@@ -20106,6 +20572,8 @@ function commandHash(input2) {
|
|
|
20106
20572
|
}
|
|
20107
20573
|
var createGateSchema = external_exports.object({
|
|
20108
20574
|
preset: external_exports.string().min(1).max(40),
|
|
20575
|
+
// CLI 0.2.4+: what was stopped. Older hooks only send Bash commands.
|
|
20576
|
+
tool: external_exports.enum(GATE_TOOLS).default("bash"),
|
|
20109
20577
|
quorum: external_exports.number().int().min(1).max(9).optional(),
|
|
20110
20578
|
env: external_exports.enum(["prod", "staging", "other"]).optional(),
|
|
20111
20579
|
command: external_exports.string().min(1).max(4e3),
|
|
@@ -20116,9 +20584,16 @@ var createGateSchema = external_exports.object({
|
|
|
20116
20584
|
context: external_exports.string().max(2048).default(""),
|
|
20117
20585
|
// Claude Code's own one-line description of the Bash call ("Apply pending migrations").
|
|
20118
20586
|
intent: external_exports.string().max(300).nullable().optional(),
|
|
20587
|
+
// What a wrapper or package script really runs ("pnpm db:deploy" -> "prisma migrate deploy"),
|
|
20588
|
+
// redacted by the hook. The server matches it again; it can only raise a gate, never lower it.
|
|
20589
|
+
via: external_exports.array(
|
|
20590
|
+
external_exports.object({ from: external_exports.string().min(1).max(300), runs: external_exports.string().min(1).max(1e3) }).strict()
|
|
20591
|
+
).max(5).optional(),
|
|
20119
20592
|
redactions: external_exports.number().int().min(0).max(1e4).default(0),
|
|
20120
20593
|
cmdHash: external_exports.string().regex(SHA256_RE),
|
|
20121
|
-
sessionId: external_exports.string().max(200).optional()
|
|
20594
|
+
sessionId: external_exports.string().max(200).optional(),
|
|
20595
|
+
// How long this hook waits before parking the gate (CLI 0.2.3+), for the countdown.
|
|
20596
|
+
waitSec: external_exports.number().int().min(5).max(600).optional()
|
|
20122
20597
|
}).strict();
|
|
20123
20598
|
var consumeGrantSchema = external_exports.object({ cmdHash: external_exports.string().regex(SHA256_RE) }).strict();
|
|
20124
20599
|
|
|
@@ -20206,32 +20681,116 @@ function truncate(text, maxChars) {
|
|
|
20206
20681
|
return `${text.slice(0, Math.max(0, maxChars - 15))}\u2026 [truncated]`;
|
|
20207
20682
|
}
|
|
20208
20683
|
|
|
20684
|
+
// src/tool-call.ts
|
|
20685
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
20686
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
20687
|
+
var MAX_DETAIL_LINES = 80;
|
|
20688
|
+
var str = (value) => typeof value === "string" ? value : "";
|
|
20689
|
+
function displayPath(file2, cwd, repoRoot, home) {
|
|
20690
|
+
const absolute = resolve(cwd, file2);
|
|
20691
|
+
const inside = (base2) => {
|
|
20692
|
+
const rel = relative(base2, absolute);
|
|
20693
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel.split(sep).join("/") : null;
|
|
20694
|
+
};
|
|
20695
|
+
const inRepo = inside(repoRoot);
|
|
20696
|
+
if (inRepo) return inRepo;
|
|
20697
|
+
const inHome = inside(home);
|
|
20698
|
+
return inHome ? `~/${inHome}` : absolute.split(sep).join("/");
|
|
20699
|
+
}
|
|
20700
|
+
function lines(prefix, text) {
|
|
20701
|
+
return text === "" ? [] : text.split("\n").map((line) => `${prefix}${line}`);
|
|
20702
|
+
}
|
|
20703
|
+
function hideEnvValues(text) {
|
|
20704
|
+
return text.replace(/^([+-] \s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_.]*\s*=).*$/gm, "$1[REDACTED]");
|
|
20705
|
+
}
|
|
20706
|
+
function editDetail(toolName, input2) {
|
|
20707
|
+
let out;
|
|
20708
|
+
if (toolName === "Write") {
|
|
20709
|
+
out = lines("+ ", str(input2.content));
|
|
20710
|
+
} else if (toolName === "NotebookEdit") {
|
|
20711
|
+
out = input2.edit_mode === "delete" ? ["(deletes a notebook cell)"] : lines("+ ", str(input2.new_source));
|
|
20712
|
+
} else {
|
|
20713
|
+
const edits = toolName === "MultiEdit" && Array.isArray(input2.edits) ? input2.edits : [input2];
|
|
20714
|
+
out = edits.flatMap((edit, index) => [
|
|
20715
|
+
...index > 0 ? [""] : [],
|
|
20716
|
+
...lines("- ", str(edit.old_string)),
|
|
20717
|
+
...lines("+ ", str(edit.new_string)),
|
|
20718
|
+
...edit.replace_all === true ? ["(every occurrence in the file)"] : []
|
|
20719
|
+
]);
|
|
20720
|
+
}
|
|
20721
|
+
const shown = out.slice(0, MAX_DETAIL_LINES);
|
|
20722
|
+
if (out.length > shown.length) shown.push(`\u2026 ${out.length - shown.length} more lines`);
|
|
20723
|
+
return shown.join("\n");
|
|
20724
|
+
}
|
|
20725
|
+
function readToolCall(toolName, rawInput, where) {
|
|
20726
|
+
const input2 = rawInput && typeof rawInput === "object" ? rawInput : {};
|
|
20727
|
+
const hashSource = `${toolName}
|
|
20728
|
+
${JSON.stringify(input2)}`;
|
|
20729
|
+
if (EDIT_TOOLS.has(toolName)) {
|
|
20730
|
+
const file2 = str(input2.file_path) || str(input2.notebook_path);
|
|
20731
|
+
if (!file2) return null;
|
|
20732
|
+
const path = displayPath(file2, where.cwd, where.repoRoot, where.home);
|
|
20733
|
+
const detail = editDetail(toolName, input2);
|
|
20734
|
+
const isEnv = /(^|\/)\.env[^/]*$/.test(path);
|
|
20735
|
+
return {
|
|
20736
|
+
tool: "edit",
|
|
20737
|
+
subject: `${toolName === "Write" ? "write" : "edit"} ${path}`,
|
|
20738
|
+
hashSource,
|
|
20739
|
+
detail: isEnv ? hideEnvValues(detail) : detail
|
|
20740
|
+
};
|
|
20741
|
+
}
|
|
20742
|
+
if (toolName.startsWith("mcp__")) {
|
|
20743
|
+
const args = JSON.stringify(input2);
|
|
20744
|
+
return {
|
|
20745
|
+
tool: "mcp",
|
|
20746
|
+
subject: args === "{}" ? toolName : `${toolName} ${args}`,
|
|
20747
|
+
hashSource,
|
|
20748
|
+
detail: ""
|
|
20749
|
+
};
|
|
20750
|
+
}
|
|
20751
|
+
return null;
|
|
20752
|
+
}
|
|
20753
|
+
|
|
20209
20754
|
// src/hook.ts
|
|
20210
20755
|
var POLL_MS = 2e3;
|
|
20211
20756
|
var PARK_MARGIN_SEC = 15;
|
|
20212
|
-
function denyMessages(gate) {
|
|
20757
|
+
function denyMessages(gate, tool = "bash") {
|
|
20213
20758
|
const who = gate.decidedBy || "a teammate";
|
|
20759
|
+
const noun = TOOL_NOUN[tool];
|
|
20760
|
+
const question = tool === "bash" ? `${who} asked a question in TG-${gate.code} before deciding: "${gate.question}" Answer it specifically and honestly in the description of your next Bash call, then run the same command again so the team can decide. If the answer changes your plan, change the command too.` : `${who} asked a question in TG-${gate.code} before deciding: "${gate.question}" Answer it to the user, then make the same ${noun} again so the team can decide. If the answer changes your plan, change the ${noun} too.`;
|
|
20214
20761
|
return {
|
|
20215
|
-
denied: gate.question ?
|
|
20216
|
-
parked: `Parked as TG-${gate.code}: nobody on the team decided in time. Continue with other work that doesn't need this
|
|
20762
|
+
denied: gate.question ? question : `Denied by ${who} in TG-${gate.code}: ${gate.denyReason ?? "no reason given"}. Do not retry this ${noun} as is. Change your plan to address the reason, then propose the new ${noun}.`,
|
|
20763
|
+
parked: `Parked as TG-${gate.code}: nobody on the team decided in time. Continue with other work that doesn't need this ${noun}. Retry the exact same ${noun} later; once the team approves TG-${gate.code} it passes immediately (${gate.url}).`,
|
|
20217
20764
|
expired: `TG-${gate.code} expired without approval. Ask the user whether to raise it again.`
|
|
20218
20765
|
};
|
|
20219
20766
|
}
|
|
20220
|
-
function notLinkedMessage() {
|
|
20221
|
-
return
|
|
20767
|
+
function notLinkedMessage(tool = "bash") {
|
|
20768
|
+
return `Tollgate blocked this ${TOOL_NOUN[tool]} because this repo is not linked on this machine. Ask the user to run: npx tollgate-cli init`;
|
|
20222
20769
|
}
|
|
20223
|
-
function failClosedMessage(server, error62) {
|
|
20770
|
+
function failClosedMessage(server, error62, tool = "bash") {
|
|
20224
20771
|
const detail = error62 instanceof Error ? error62.message : String(error62);
|
|
20225
|
-
return `Tollgate could not reach ${server} (${detail}), so this risky
|
|
20772
|
+
return `Tollgate could not reach ${server} (${detail}), so this risky ${TOOL_NOUN[tool]} was blocked to be safe. Tell the user; do not try to work around it.`;
|
|
20226
20773
|
}
|
|
20227
20774
|
async function decide(input2, deps) {
|
|
20228
|
-
|
|
20229
|
-
const
|
|
20775
|
+
const toolName = input2.tool_name ?? "";
|
|
20776
|
+
const isBash = toolName === "Bash";
|
|
20777
|
+
if (!isBash && !EDIT_TOOLS.has(toolName) && !toolName.startsWith("mcp__")) {
|
|
20778
|
+
return { kind: "none" };
|
|
20779
|
+
}
|
|
20780
|
+
const command = isBash && typeof input2.tool_input?.command === "string" ? input2.tool_input.command : "";
|
|
20230
20781
|
const cwd = input2.cwd || process.cwd();
|
|
20231
|
-
if (!command.trim()) return { kind: "none" };
|
|
20782
|
+
if (isBash && !command.trim()) return { kind: "none" };
|
|
20232
20783
|
const config2 = deps.loadConfig(cwd);
|
|
20233
20784
|
if (!config2) return { kind: "none", note: "no .tollgate.json" };
|
|
20234
|
-
const
|
|
20785
|
+
const call = isBash ? { tool: "bash", subject: command, hashSource: command, detail: "" } : readToolCall(toolName, input2.tool_input, { cwd, repoRoot: config2.repoRoot, home: deps.home });
|
|
20786
|
+
if (!call) return { kind: "none" };
|
|
20787
|
+
const match = matchCommand(withDefaultPresets(config2.presets), {
|
|
20788
|
+
command: call.subject,
|
|
20789
|
+
cwd,
|
|
20790
|
+
repoRoot: config2.repoRoot,
|
|
20791
|
+
tool: call.tool,
|
|
20792
|
+
readScripts: isBash && deps.readScripts ? (dir) => deps.readScripts(dir, config2.repoRoot) : void 0
|
|
20793
|
+
});
|
|
20235
20794
|
if (!match) return { kind: "none" };
|
|
20236
20795
|
if (deps.env.TOLLGATE_BYPASS === "1") {
|
|
20237
20796
|
deps.audit("bypass", { preset: match.preset.name, repo: config2.repo });
|
|
@@ -20240,14 +20799,14 @@ async function decide(input2, deps) {
|
|
|
20240
20799
|
if (config2.error) {
|
|
20241
20800
|
return {
|
|
20242
20801
|
kind: "deny",
|
|
20243
|
-
reason: `Tollgate blocked this
|
|
20802
|
+
reason: `Tollgate blocked this ${TOOL_NOUN[call.tool]} because .tollgate.json is invalid (${config2.error}). Ask the user to fix it or run: npx tollgate-cli init`
|
|
20244
20803
|
};
|
|
20245
20804
|
}
|
|
20246
20805
|
const token = deps.loadToken(config2.repoRoot);
|
|
20247
|
-
if (!token) return { kind: "deny", reason: notLinkedMessage() };
|
|
20806
|
+
if (!token) return { kind: "deny", reason: notLinkedMessage(call.tool) };
|
|
20248
20807
|
const started = deps.now();
|
|
20249
20808
|
const deadline = started + Math.max(5, config2.hookTimeoutSec - PARK_MARGIN_SEC) * 1e3;
|
|
20250
|
-
const cmdHash = commandHash({ repo: config2.repo, cwd, command });
|
|
20809
|
+
const cmdHash = commandHash({ repo: config2.repo, cwd, command: call.hashSource });
|
|
20251
20810
|
const api = deps.api(config2, token);
|
|
20252
20811
|
try {
|
|
20253
20812
|
const grant = await api.consumeGrant(cmdHash);
|
|
@@ -20255,14 +20814,24 @@ async function decide(input2, deps) {
|
|
|
20255
20814
|
return grant.mode === "delegate" ? { kind: "allow", reason: "Approved earlier by the team (single-use grant)." } : { kind: "none", note: "grant redeemed" };
|
|
20256
20815
|
}
|
|
20257
20816
|
const secrets = deps.envSecrets(config2.repoRoot);
|
|
20258
|
-
const redactedCommand = redact(
|
|
20817
|
+
const redactedCommand = redact(call.subject, secrets);
|
|
20259
20818
|
const redactedCwd = redact(cwd, secrets);
|
|
20260
|
-
const context = redact(
|
|
20819
|
+
const context = redact(
|
|
20820
|
+
isBash ? deps.context(`${command}
|
|
20821
|
+
${match.segment}`, config2.repoRoot) : call.detail,
|
|
20822
|
+
secrets
|
|
20823
|
+
);
|
|
20824
|
+
const via = match.via.length > 0 ? {
|
|
20825
|
+
from: redact(match.via.join(" \u2192 "), secrets),
|
|
20826
|
+
runs: redact(match.segment, secrets)
|
|
20827
|
+
} : null;
|
|
20261
20828
|
const branch = deps.gitBranch(config2.repoRoot);
|
|
20262
|
-
const description = input2.tool_input?.description;
|
|
20829
|
+
const description = isBash ? input2.tool_input?.description : void 0;
|
|
20263
20830
|
const intent = typeof description === "string" && description.trim() ? redact(description.trim(), secrets) : null;
|
|
20264
20831
|
let gate = await api.createGate({
|
|
20265
20832
|
preset: match.preset.name,
|
|
20833
|
+
// Only sent when it isn't a command, so servers from before file gates still accept it.
|
|
20834
|
+
...call.tool === "bash" ? {} : { tool: call.tool },
|
|
20266
20835
|
quorum: match.quorum,
|
|
20267
20836
|
env: match.env,
|
|
20268
20837
|
command: truncate(redactedCommand.text, 4e3),
|
|
@@ -20270,9 +20839,11 @@ async function decide(input2, deps) {
|
|
|
20270
20839
|
branch: branch ? truncate(redact(branch, secrets).text, 200) : null,
|
|
20271
20840
|
context: truncate(context.text, 2048),
|
|
20272
20841
|
intent: intent ? truncate(intent.text, 300) : null,
|
|
20273
|
-
|
|
20842
|
+
via: via ? [{ from: truncate(via.from.text, 300), runs: truncate(via.runs.text, 1e3) }] : void 0,
|
|
20843
|
+
redactions: redactedCommand.count + redactedCwd.count + context.count + (intent?.count ?? 0) + (via ? via.from.count + via.runs.count : 0),
|
|
20274
20844
|
cmdHash,
|
|
20275
|
-
sessionId: input2.session_id
|
|
20845
|
+
sessionId: input2.session_id,
|
|
20846
|
+
waitSec: Math.round((deadline - started) / 1e3)
|
|
20276
20847
|
});
|
|
20277
20848
|
deps.progress(
|
|
20278
20849
|
`Tollgate: waiting on your team \xB7 TG-${gate.code} (0/${gate.quorum}) \xB7 ${gate.url}`
|
|
@@ -20281,18 +20852,19 @@ async function decide(input2, deps) {
|
|
|
20281
20852
|
if (gate.status === "approved") {
|
|
20282
20853
|
return gate.mode === "delegate" ? { kind: "allow", reason: `Approved by the team in TG-${gate.code}.` } : { kind: "none", note: `TG-${gate.code} approved` };
|
|
20283
20854
|
}
|
|
20284
|
-
|
|
20285
|
-
if (gate.status === "
|
|
20286
|
-
if (gate.status === "
|
|
20855
|
+
const messages = denyMessages(gate, call.tool);
|
|
20856
|
+
if (gate.status === "denied") return { kind: "deny", reason: messages.denied };
|
|
20857
|
+
if (gate.status === "expired") return { kind: "deny", reason: messages.expired };
|
|
20858
|
+
if (gate.status === "parked") return { kind: "deny", reason: messages.parked };
|
|
20287
20859
|
if (deps.now() + POLL_MS >= deadline) {
|
|
20288
20860
|
await api.park(gate.id).catch(() => void 0);
|
|
20289
|
-
return { kind: "deny", reason:
|
|
20861
|
+
return { kind: "deny", reason: messages.parked };
|
|
20290
20862
|
}
|
|
20291
20863
|
await deps.sleep(POLL_MS);
|
|
20292
20864
|
gate = await api.getGate(gate.id).catch(() => gate);
|
|
20293
20865
|
}
|
|
20294
20866
|
} catch (error62) {
|
|
20295
|
-
return { kind: "deny", reason: failClosedMessage(config2.server, error62) };
|
|
20867
|
+
return { kind: "deny", reason: failClosedMessage(config2.server, error62, call.tool) };
|
|
20296
20868
|
}
|
|
20297
20869
|
}
|
|
20298
20870
|
function renderDecision(decision) {
|
|
@@ -20310,6 +20882,7 @@ function renderDecision(decision) {
|
|
|
20310
20882
|
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
20311
20883
|
import { join as join2 } from "node:path";
|
|
20312
20884
|
var SETTINGS_FILE = join2(".claude", "settings.local.json");
|
|
20885
|
+
var HOOK_MATCHER = "Bash|Edit|Write|MultiEdit|NotebookEdit|mcp__.*";
|
|
20313
20886
|
function isTollgateHook(command) {
|
|
20314
20887
|
return /tollgate/.test(command) && /\bhook\b/.test(command);
|
|
20315
20888
|
}
|
|
@@ -20322,7 +20895,7 @@ function mergeHookSettings(existing, hookCommand, timeoutSec) {
|
|
|
20322
20895
|
hooks.PreToolUse = [
|
|
20323
20896
|
...kept,
|
|
20324
20897
|
{
|
|
20325
|
-
matcher:
|
|
20898
|
+
matcher: HOOK_MATCHER,
|
|
20326
20899
|
hooks: [
|
|
20327
20900
|
{
|
|
20328
20901
|
type: "command",
|
|
@@ -20388,7 +20961,7 @@ function writeRepoFiles(input2) {
|
|
|
20388
20961
|
}
|
|
20389
20962
|
|
|
20390
20963
|
// src/main.ts
|
|
20391
|
-
var HELP = `tollgate ${"0.2.
|
|
20964
|
+
var HELP = `tollgate ${"0.2.4"}: your team approves risky agent commands before they run
|
|
20392
20965
|
|
|
20393
20966
|
Usage:
|
|
20394
20967
|
npx tollgate-cli init --token <tg_\u2026> [--server <url>] [--repo <owner/name>]
|
|
@@ -20405,7 +20978,9 @@ function hookDeps() {
|
|
|
20405
20978
|
envSecrets,
|
|
20406
20979
|
gitBranch,
|
|
20407
20980
|
context: buildContext,
|
|
20981
|
+
readScripts,
|
|
20408
20982
|
env: process.env,
|
|
20983
|
+
home: homedir2(),
|
|
20409
20984
|
now: () => Date.now(),
|
|
20410
20985
|
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
20411
20986
|
audit: (event, detail) => {
|
|
@@ -20472,7 +21047,7 @@ async function runInit(argv) {
|
|
|
20472
21047
|
dir: { type: "string" }
|
|
20473
21048
|
}
|
|
20474
21049
|
});
|
|
20475
|
-
const repoRoot =
|
|
21050
|
+
const repoRoot = resolve2(values.dir ?? process.cwd());
|
|
20476
21051
|
const token = values.token ?? process.env.TOLLGATE_TOKEN;
|
|
20477
21052
|
const server = (values.server ?? process.env.TOLLGATE_SERVER ?? "").replace(/\/+$/, "");
|
|
20478
21053
|
if (!token || !server) {
|
|
@@ -20482,7 +21057,7 @@ async function runInit(argv) {
|
|
|
20482
21057
|
}
|
|
20483
21058
|
const who = await whoAmI(server, token);
|
|
20484
21059
|
const repo = values.repo ?? who.repo.name ?? gitRepoName(repoRoot) ?? "repo";
|
|
20485
|
-
const bundle = installBundle(
|
|
21060
|
+
const bundle = installBundle(resolve2(process.argv[1] ?? ""), tollgateHome());
|
|
20486
21061
|
const tokenFile2 = saveToken(repoRoot, { token, server, repo });
|
|
20487
21062
|
const files = writeRepoFiles({ repoRoot, server, repo, hookCommand: hookCommandFor(bundle) });
|
|
20488
21063
|
process.stdout.write(
|
|
@@ -20572,7 +21147,7 @@ async function main() {
|
|
|
20572
21147
|
return runUnlink();
|
|
20573
21148
|
case "--version":
|
|
20574
21149
|
case "-v":
|
|
20575
|
-
process.stdout.write(`${"0.2.
|
|
21150
|
+
process.stdout.write(`${"0.2.4"}
|
|
20576
21151
|
`);
|
|
20577
21152
|
return;
|
|
20578
21153
|
default:
|