@kaynooo/guard-tools 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +40 -2
- package/dist/cli.js +1891 -0
- package/package.json +44 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1891 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { dirname } from "path";
|
|
8
|
+
import process6 from "process";
|
|
9
|
+
|
|
10
|
+
// ../kit/src/args.ts
|
|
11
|
+
import process3 from "process";
|
|
12
|
+
import { parseArgs } from "util";
|
|
13
|
+
|
|
14
|
+
// ../kit/src/style.ts
|
|
15
|
+
import process2 from "process";
|
|
16
|
+
|
|
17
|
+
// ../../node_modules/.bun/@kaynooo+utils@2.31.0+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
|
|
18
|
+
function x(s) {
|
|
19
|
+
let r = s.startsWith("#") ? s.slice(1).toLowerCase() : s.toLowerCase();
|
|
20
|
+
if (!/^[a-f0-9]+$/.test(r))
|
|
21
|
+
return [0, 0, 0];
|
|
22
|
+
let t = r.length, n = (c) => Number.parseInt(c, 16);
|
|
23
|
+
if (t === 3 || t === 4) {
|
|
24
|
+
let c = n(r[0] + r[0]), u = n(r[1] + r[1]), h = n(r[2] + r[2]), g = t === 4 ? n(r[3] + r[3]) : undefined;
|
|
25
|
+
return g !== undefined ? [c, u, h, g] : [c, u, h];
|
|
26
|
+
}
|
|
27
|
+
if (t !== 6 && t !== 8)
|
|
28
|
+
throw Error(`Invalid hex color length: ${t}`);
|
|
29
|
+
let a = n(r.slice(0, 2)), b = n(r.slice(2, 4)), o = n(r.slice(4, 6)), y = t === 8 ? n(r.slice(6, 8)) : undefined;
|
|
30
|
+
if ([a, b, o].some(Number.isNaN) || y !== undefined && Number.isNaN(y))
|
|
31
|
+
throw Error(`Invalid hex color value: ${s}`);
|
|
32
|
+
return y !== undefined ? [a, b, o, y] : [a, b, o];
|
|
33
|
+
}
|
|
34
|
+
function A(s) {
|
|
35
|
+
let [r, t, n, a] = s;
|
|
36
|
+
if (a)
|
|
37
|
+
return `#${(r << 24 | t << 16 | n << 8 | Math.round((a ?? 1) * 255)).toString(16).padStart(8, "0").toUpperCase()}`;
|
|
38
|
+
return `#${(r << 16 | t << 8 | n).toString(16).padStart(6, "0").toUpperCase()}`;
|
|
39
|
+
}
|
|
40
|
+
function R(s, r, t) {
|
|
41
|
+
s = (s % 360 + 360) % 360, t /= 100;
|
|
42
|
+
let n = r * Math.min(t, 1 - t) / 100, a = (b) => {
|
|
43
|
+
let o = (b + s / 30) % 12, y = t - n * Math.max(Math.min(o - 3, 9 - o, 1), -1);
|
|
44
|
+
return Math.round(255 * y);
|
|
45
|
+
};
|
|
46
|
+
return [a(0), a(8), a(4)];
|
|
47
|
+
}
|
|
48
|
+
function k(s) {
|
|
49
|
+
let [r, t, n] = s;
|
|
50
|
+
r /= 255, t /= 255, n /= 255;
|
|
51
|
+
let a = Math.max(r, t, n), b = Math.min(r, t, n), o = 0, y = 0, c = (a + b) / 2;
|
|
52
|
+
if (a !== b) {
|
|
53
|
+
let u = a - b;
|
|
54
|
+
if (y = c > 0 && c < 1 ? u / (1 - Math.abs(2 * c - 1)) : 0, a === r)
|
|
55
|
+
o = (t - n) / u + (t < n ? 6 : 0);
|
|
56
|
+
else if (a === t)
|
|
57
|
+
o = (n - r) / u + 2;
|
|
58
|
+
else
|
|
59
|
+
o = (r - t) / u + 4;
|
|
60
|
+
o /= 6;
|
|
61
|
+
}
|
|
62
|
+
return [Math.round(o * 360), Math.round(y * 100), Math.round(c * 100)];
|
|
63
|
+
}
|
|
64
|
+
function H(s, r, t) {
|
|
65
|
+
s = (s % 360 + 360) % 360, r /= 100, t /= 100;
|
|
66
|
+
let n = (a) => {
|
|
67
|
+
let b = (a + s / 60) % 6, o = t - t * r * Math.max(Math.min(b, 4 - b, 1), 0);
|
|
68
|
+
return Math.round(255 * o);
|
|
69
|
+
};
|
|
70
|
+
return [n(5), n(3), n(1)];
|
|
71
|
+
}
|
|
72
|
+
function f(s) {
|
|
73
|
+
let [r, t, n] = s;
|
|
74
|
+
r /= 255, t /= 255, n /= 255;
|
|
75
|
+
let a = Math.max(r, t, n), b = Math.min(r, t, n), o = a - b, y = 0, c = a === 0 ? 0 : o / a, u = a;
|
|
76
|
+
if (o !== 0) {
|
|
77
|
+
if (a === r)
|
|
78
|
+
y = (t - n) / o + (t < n ? 6 : 0);
|
|
79
|
+
else if (a === t)
|
|
80
|
+
y = (n - r) / o + 2;
|
|
81
|
+
else
|
|
82
|
+
y = (r - t) / o + 4;
|
|
83
|
+
y /= 6;
|
|
84
|
+
}
|
|
85
|
+
return [Math.round(y * 360), Math.round(c * 100), Math.round(u * 100)];
|
|
86
|
+
}
|
|
87
|
+
function O(s) {
|
|
88
|
+
let r = s[0] / 255, t = s[1] / 255, n = s[2] / 255, a = 1 - Math.max(r, t, n);
|
|
89
|
+
if (a === 1)
|
|
90
|
+
return [0, 0, 0, 100];
|
|
91
|
+
let b = (1 - r - a) / (1 - a), o = (1 - t - a) / (1 - a), y = (1 - n - a) / (1 - a);
|
|
92
|
+
return [Math.round(b * 100), Math.round(o * 100), Math.round(y * 100), Math.round(a * 100)];
|
|
93
|
+
}
|
|
94
|
+
function v(s, r, t, n) {
|
|
95
|
+
return s /= 100, r /= 100, t /= 100, n /= 100, [Math.round(255 * (1 - s) * (1 - n)), Math.round(255 * (1 - r) * (1 - n)), Math.round(255 * (1 - t) * (1 - n))];
|
|
96
|
+
}
|
|
97
|
+
var m = (s) => s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
98
|
+
var p = (s) => s <= 0.0031308 ? 12.92 * s : 1.055 * s ** 0.4166666666666667 - 0.055;
|
|
99
|
+
var i = (s, r) => Math.round(s * 10 ** r) / 10 ** r;
|
|
100
|
+
function T(s) {
|
|
101
|
+
let r = m(s[0] / 255), t = m(s[1] / 255), n = m(s[2] / 255), a = Math.cbrt(0.4122214708 * r + 0.5363325363 * t + 0.0514459929 * n), b = Math.cbrt(0.2119034982 * r + 0.6806995451 * t + 0.1073969566 * n), o = Math.cbrt(0.0883024619 * r + 0.2817188376 * t + 0.6299787005 * n);
|
|
102
|
+
return [i(0.2104542553 * a + 0.793617785 * b - 0.0040720468 * o, 4), i(1.9779984951 * a - 2.428592205 * b + 0.4505937099 * o, 4), i(0.0259040371 * a + 0.7827717662 * b - 0.808675766 * o, 4)];
|
|
103
|
+
}
|
|
104
|
+
function M(s, r, t) {
|
|
105
|
+
let n = (s + 0.3963377774 * r + 0.2158037573 * t) ** 3, a = (s - 0.1055613458 * r - 0.0638541728 * t) ** 3, b = (s - 0.0894841775 * r - 1.291485548 * t) ** 3, o = p(4.0767416621 * n - 3.3077115913 * a + 0.2309699292 * b), y = p(-1.2684380046 * n + 2.6097574011 * a - 0.3413193965 * b), c = p(-0.0041960863 * n - 0.7034186147 * a + 1.707614701 * b), u = (h) => Math.min(255, Math.max(0, Math.round(h * 255)));
|
|
106
|
+
return [u(o), u(y), u(c)];
|
|
107
|
+
}
|
|
108
|
+
function d(s) {
|
|
109
|
+
let [r, t, n] = T(s), a = Math.sqrt(t * t + n * n), b = (Math.atan2(n, t) * 180 / Math.PI + 360) % 360;
|
|
110
|
+
return [i(r, 4), i(a, 4), i(b, 2)];
|
|
111
|
+
}
|
|
112
|
+
function C(s, r, t) {
|
|
113
|
+
let n = t * Math.PI / 180;
|
|
114
|
+
return M(s, r * Math.cos(n), r * Math.sin(n));
|
|
115
|
+
}
|
|
116
|
+
function l(s) {
|
|
117
|
+
let r = s / 255;
|
|
118
|
+
return r <= 0.03928 ? r / 12.92 : ((r + 0.055) / 1.055) ** 2.4;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class e {
|
|
122
|
+
r;
|
|
123
|
+
g;
|
|
124
|
+
b;
|
|
125
|
+
a;
|
|
126
|
+
constructor(s, r, t, n = 1) {
|
|
127
|
+
this.r = s, this.g = r, this.b = t, this.a = n;
|
|
128
|
+
}
|
|
129
|
+
static from(s, r) {
|
|
130
|
+
if (s === "hex") {
|
|
131
|
+
let [t, n, a, b] = x(r);
|
|
132
|
+
return new e(t, n, a, b);
|
|
133
|
+
}
|
|
134
|
+
if (s === "rgb") {
|
|
135
|
+
if (Array.isArray(r)) {
|
|
136
|
+
let [o, y, c, u] = r;
|
|
137
|
+
return new e(o, y, c, u);
|
|
138
|
+
}
|
|
139
|
+
let { r: t, g: n, b: a, a: b } = r;
|
|
140
|
+
return new e(t, n, a, b);
|
|
141
|
+
}
|
|
142
|
+
if (s === "hsl") {
|
|
143
|
+
let t, n, a, b;
|
|
144
|
+
if (Array.isArray(r))
|
|
145
|
+
[t, n, a, b] = r;
|
|
146
|
+
else
|
|
147
|
+
({ h: t, s: n, l: a, a: b } = r);
|
|
148
|
+
let [o, y, c] = R(t, n, a);
|
|
149
|
+
return new e(o, y, c, b);
|
|
150
|
+
}
|
|
151
|
+
if (s === "hsv") {
|
|
152
|
+
let t, n, a, b;
|
|
153
|
+
if (Array.isArray(r))
|
|
154
|
+
[t, n, a, b] = r;
|
|
155
|
+
else
|
|
156
|
+
({ h: t, s: n, v: a, a: b } = r);
|
|
157
|
+
let [o, y, c] = H(t, n, a);
|
|
158
|
+
return new e(o, y, c, b);
|
|
159
|
+
}
|
|
160
|
+
if (s === "cmyk") {
|
|
161
|
+
let t, n, a, b, o;
|
|
162
|
+
if (Array.isArray(r))
|
|
163
|
+
[t, n, a, b, o] = r;
|
|
164
|
+
else
|
|
165
|
+
({ c: t, m: n, y: a, k: b, a: o } = r);
|
|
166
|
+
let [y, c, u] = v(t, n, a, b);
|
|
167
|
+
return new e(y, c, u, o);
|
|
168
|
+
}
|
|
169
|
+
if (s === "oklch") {
|
|
170
|
+
let t, n, a, b;
|
|
171
|
+
if (Array.isArray(r))
|
|
172
|
+
[t, n, a, b] = r;
|
|
173
|
+
else
|
|
174
|
+
({ l: t, c: n, h: a, a: b } = r);
|
|
175
|
+
let [o, y, c] = C(t, n, a);
|
|
176
|
+
return new e(o, y, c, b);
|
|
177
|
+
}
|
|
178
|
+
if (s === "oklab") {
|
|
179
|
+
let t, n, a, b;
|
|
180
|
+
if (Array.isArray(r))
|
|
181
|
+
[t, n, a, b] = r;
|
|
182
|
+
else
|
|
183
|
+
({ l: t, a: n, b: a, alpha: b } = r);
|
|
184
|
+
let [o, y, c] = M(t, n, a);
|
|
185
|
+
return new e(o, y, c, b);
|
|
186
|
+
}
|
|
187
|
+
throw Error("Invalid input");
|
|
188
|
+
}
|
|
189
|
+
to(s) {
|
|
190
|
+
switch (s) {
|
|
191
|
+
case "rgb":
|
|
192
|
+
return [this.r, this.g, this.b];
|
|
193
|
+
case "rgba":
|
|
194
|
+
return [this.r, this.g, this.b, this.a];
|
|
195
|
+
case "hex":
|
|
196
|
+
return A([this.r, this.g, this.b]);
|
|
197
|
+
case "hexa":
|
|
198
|
+
return A([this.r, this.g, this.b, this.a]);
|
|
199
|
+
case "hsl":
|
|
200
|
+
return k([this.r, this.g, this.b]);
|
|
201
|
+
case "hsla":
|
|
202
|
+
return [...k([this.r, this.g, this.b]), this.a];
|
|
203
|
+
case "hsv":
|
|
204
|
+
return f([this.r, this.g, this.b]);
|
|
205
|
+
case "hsva":
|
|
206
|
+
return [...f([this.r, this.g, this.b]), this.a];
|
|
207
|
+
case "cmyk":
|
|
208
|
+
return O([this.r, this.g, this.b]);
|
|
209
|
+
case "cmyka":
|
|
210
|
+
return [...O([this.r, this.g, this.b]), this.a];
|
|
211
|
+
case "oklch":
|
|
212
|
+
return d([this.r, this.g, this.b]);
|
|
213
|
+
case "oklcha":
|
|
214
|
+
return [...d([this.r, this.g, this.b]), this.a];
|
|
215
|
+
case "oklab":
|
|
216
|
+
return T([this.r, this.g, this.b]);
|
|
217
|
+
case "oklaba":
|
|
218
|
+
return [...T([this.r, this.g, this.b]), this.a];
|
|
219
|
+
default:
|
|
220
|
+
return [this.r, this.g, this.b];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
getLuminance() {
|
|
224
|
+
let s = l(this.r), r = l(this.g), t = l(this.b);
|
|
225
|
+
return 0.2126 * s + 0.7152 * r + 0.0722 * t;
|
|
226
|
+
}
|
|
227
|
+
contrastRatio(s) {
|
|
228
|
+
let r = this.getLuminance(), t = s.getLuminance(), [n, a] = r >= t ? [r, t] : [t, r];
|
|
229
|
+
return (n + 0.05) / (a + 0.05);
|
|
230
|
+
}
|
|
231
|
+
bestContrastForeground() {
|
|
232
|
+
let s = new e(255, 255, 255, 1), r = new e(0, 0, 0, 1), t = this.contrastRatio(s), n = this.contrastRatio(r);
|
|
233
|
+
return t >= n ? s : r;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ../../node_modules/.bun/@kaynooo+utils@2.31.0+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-y1msbssb.js
|
|
238
|
+
var r = 39;
|
|
239
|
+
var o = 49;
|
|
240
|
+
var f2 = "\x1B[";
|
|
241
|
+
var C2 = { reset: [0, 0], bold: [1, 22], dim: [2, 22], italic: [3, 23], underline: [4, 24], inverse: [7, 27], hidden: [8, 28], strikethrough: [9, 29], black: [30, r], red: [31, r], green: [32, r], yellow: [33, r], blue: [34, r], magenta: [35, r], cyan: [36, r], white: [37, r], gray: [90, r], grey: [90, r], blackBright: [90, r], redBright: [91, r], greenBright: [92, r], yellowBright: [93, r], blueBright: [94, r], magentaBright: [95, r], cyanBright: [96, r], whiteBright: [97, r], bgBlack: [40, o], bgRed: [41, o], bgGreen: [42, o], bgYellow: [43, o], bgBlue: [44, o], bgMagenta: [45, o], bgCyan: [46, o], bgWhite: [47, o], bgBlackBright: [100, o], bgRedBright: [101, o], bgGreenBright: [102, o], bgYellowBright: [103, o], bgBlueBright: [104, o], bgMagentaBright: [105, o], bgCyanBright: [106, o], bgWhiteBright: [107, o], rgb: (i, n, a, l = "fg") => [`${l === "fg" ? 38 : 48};2;${i};${n};${a}`, l === "fg" ? r : o], hex: (i, n = "fg") => {
|
|
242
|
+
let [a, l, s] = e.from("hex", i).to("rgb");
|
|
243
|
+
return [`${n === "fg" ? 38 : 48};2;${a};${l};${s}`, n === "fg" ? r : o];
|
|
244
|
+
}, ansi256: (i, n = "fg") => [`${n === "fg" ? 38 : 48};5;${i}`, n === "fg" ? r : o] };
|
|
245
|
+
var b = new Set(["", "1", "2", "3", "true"]);
|
|
246
|
+
function h(i, n = globalThis.process?.env ?? {}) {
|
|
247
|
+
if (n.FORCE_COLOR !== undefined)
|
|
248
|
+
return b.has(n.FORCE_COLOR);
|
|
249
|
+
if (n.NO_COLOR !== undefined || n.NODE_DISABLE_COLORS !== undefined || n.TERM === "dumb")
|
|
250
|
+
return false;
|
|
251
|
+
return i?.isTTY === true;
|
|
252
|
+
}
|
|
253
|
+
function d2(i, n, a) {
|
|
254
|
+
if (!h(a.stream ?? globalThis.process?.stdout, a.env))
|
|
255
|
+
return n;
|
|
256
|
+
let l = "", s = "";
|
|
257
|
+
for (let g = 0, y = i.length;g < y; g++) {
|
|
258
|
+
let [c, p] = i[g];
|
|
259
|
+
l += `${f2}${c}m`, s = `${f2}${p}m${s}`;
|
|
260
|
+
}
|
|
261
|
+
return l + n + s;
|
|
262
|
+
}
|
|
263
|
+
function u(i, n = []) {
|
|
264
|
+
return new Proxy((l) => d2(n, l, i), { get(l, s) {
|
|
265
|
+
let g = C2[s];
|
|
266
|
+
if (Array.isArray(g))
|
|
267
|
+
return u(i, [...n, g]);
|
|
268
|
+
if (typeof g === "function")
|
|
269
|
+
return (...y) => u(i, [...n, g(...y)]);
|
|
270
|
+
throw Error(`Unknown style: ${String(s)}`);
|
|
271
|
+
} });
|
|
272
|
+
}
|
|
273
|
+
function le(i = {}) {
|
|
274
|
+
return u(i);
|
|
275
|
+
}
|
|
276
|
+
var t = le();
|
|
277
|
+
|
|
278
|
+
// ../kit/src/style.ts
|
|
279
|
+
function takesControlCodes(stream, env = process2.env) {
|
|
280
|
+
return stream.isTTY === true && env.NO_COLOR === undefined && env.NODE_DISABLE_COLORS === undefined && env.TERM !== "dumb";
|
|
281
|
+
}
|
|
282
|
+
function paint(stream, env = process2.env) {
|
|
283
|
+
return le({ stream: { isTTY: takesControlCodes(stream, env) }, env });
|
|
284
|
+
}
|
|
285
|
+
var STATUSES = {
|
|
286
|
+
passed: { symbol: "\u2713", words: ["passed", "success", "succeeded", "ok"], paint: (colors) => colors.green },
|
|
287
|
+
failed: { symbol: "\u2717", words: ["failed", "failure", "error", "errored"], paint: (colors) => colors.red },
|
|
288
|
+
running: { symbol: "\u25CF", words: ["running", "in_progress", "started"], paint: (colors) => colors.cyan },
|
|
289
|
+
queued: { symbol: "\u25CB", words: ["queued", "waiting", "pending", "created", "blocked", "scheduled"], paint: (colors) => colors.yellow },
|
|
290
|
+
skipped: { symbol: "-", words: ["skipped", "cancelled", "canceled", "manual"], paint: (colors) => colors.dim }
|
|
291
|
+
};
|
|
292
|
+
var STATUS_OF = new Map(Object.entries(STATUSES).flatMap(([status, { words }]) => words.map((word) => [word, status])));
|
|
293
|
+
|
|
294
|
+
// ../kit/src/format.ts
|
|
295
|
+
function upTo(text, separator) {
|
|
296
|
+
const end = text.indexOf(separator);
|
|
297
|
+
return end === -1 ? text : text.slice(0, end);
|
|
298
|
+
}
|
|
299
|
+
var ESCAPE = String.fromCharCode(27);
|
|
300
|
+
|
|
301
|
+
// ../kit/src/args.ts
|
|
302
|
+
function jsonOption(holds) {
|
|
303
|
+
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
class Exit extends Error {
|
|
307
|
+
code;
|
|
308
|
+
hint;
|
|
309
|
+
constructor(message, code, hint) {
|
|
310
|
+
super(message);
|
|
311
|
+
this.code = code;
|
|
312
|
+
this.hint = hint;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function fail(message, code = 2, hint) {
|
|
316
|
+
throw new Exit(message, code, hint);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
class UsageError extends Exit {
|
|
320
|
+
constructor(message) {
|
|
321
|
+
super(message, 2);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function badUsage(message) {
|
|
325
|
+
throw new UsageError(message);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
class HelpShown extends Exit {
|
|
329
|
+
constructor() {
|
|
330
|
+
super("help shown", 0);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
class Completing {
|
|
335
|
+
options;
|
|
336
|
+
constructor(options) {
|
|
337
|
+
this.options = options;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
var completion = { active: false };
|
|
341
|
+
var OPTIONAL = /\[[^\]]+\]/g;
|
|
342
|
+
var USAGE_PREFIX = /^usage:\s*/i;
|
|
343
|
+
function usageLine(usage, colors = paint(process3.stdout)) {
|
|
344
|
+
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
345
|
+
}
|
|
346
|
+
function helpText(usage, options, colors = paint(process3.stdout)) {
|
|
347
|
+
return [usageLine(usage, colors), "", colors.bold("Options:"), ...optionRows(options, colors)].join(`
|
|
348
|
+
`);
|
|
349
|
+
}
|
|
350
|
+
function optionRows(options, colors = paint(process3.stdout)) {
|
|
351
|
+
const rows = Object.entries(options).map(([name, option]) => {
|
|
352
|
+
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
353
|
+
const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
|
|
354
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description };
|
|
355
|
+
});
|
|
356
|
+
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
357
|
+
return rows.map((row) => ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
358
|
+
}
|
|
359
|
+
function parse(args, usage, options, settings = {}) {
|
|
360
|
+
const all = { ...options, help: { type: "boolean", short: "h", description: "show this help" } };
|
|
361
|
+
if (completion.active)
|
|
362
|
+
throw new Completing(all);
|
|
363
|
+
const separator = settings.rest === true ? args.indexOf("--") : -1;
|
|
364
|
+
const own = separator === -1 ? args : args.slice(0, separator);
|
|
365
|
+
const rest = separator === -1 ? undefined : args.slice(separator + 1);
|
|
366
|
+
let parsed;
|
|
367
|
+
try {
|
|
368
|
+
parsed = parseArgs({ args: own, options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
369
|
+
} catch (error) {
|
|
370
|
+
badUsage(parseError(error));
|
|
371
|
+
}
|
|
372
|
+
if (parsed.values.help === true) {
|
|
373
|
+
console.log(helpText(usage, all));
|
|
374
|
+
throw new HelpShown;
|
|
375
|
+
}
|
|
376
|
+
if (settings.positionals !== undefined)
|
|
377
|
+
checkPositionals(parsed.positionals, settings.positionals);
|
|
378
|
+
return { ...parsed, positionals: parsed.positionals, rest };
|
|
379
|
+
}
|
|
380
|
+
function checkPositionals(given, names) {
|
|
381
|
+
const variadic = names.at(-1)?.replace("]", "").endsWith("...") === true;
|
|
382
|
+
const required = names.filter((name) => !name.startsWith("[")).length;
|
|
383
|
+
if (given.length < required)
|
|
384
|
+
badUsage(`missing ${names[given.length]}`);
|
|
385
|
+
if (!variadic && given.length > names.length)
|
|
386
|
+
badUsage(`unexpected argument ${given[names.length]}`);
|
|
387
|
+
}
|
|
388
|
+
function parseError(error) {
|
|
389
|
+
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
390
|
+
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
391
|
+
}
|
|
392
|
+
async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
393
|
+
if (path === "-") {
|
|
394
|
+
if (process3.stdin.isTTY)
|
|
395
|
+
fail(noStdin);
|
|
396
|
+
return Bun.stdin.text();
|
|
397
|
+
}
|
|
398
|
+
const file = Bun.file(path);
|
|
399
|
+
if (!await file.exists())
|
|
400
|
+
fail(`no file ${path}`);
|
|
401
|
+
return file.text();
|
|
402
|
+
}
|
|
403
|
+
// ../kit/src/cli.ts
|
|
404
|
+
import process4 from "process";
|
|
405
|
+
|
|
406
|
+
// ../kit/src/text.ts
|
|
407
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
408
|
+
function distance(a, b) {
|
|
409
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
410
|
+
for (const [i, left] of [...a].entries()) {
|
|
411
|
+
const current = [i + 1];
|
|
412
|
+
for (const [j, right] of [...b].entries())
|
|
413
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
414
|
+
previous = current;
|
|
415
|
+
}
|
|
416
|
+
return previous[b.length] ?? 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ../kit/src/cli.ts
|
|
420
|
+
function closest(name, names) {
|
|
421
|
+
const started = names.find((known) => known.startsWith(name));
|
|
422
|
+
if (started !== undefined)
|
|
423
|
+
return started;
|
|
424
|
+
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
425
|
+
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
426
|
+
}
|
|
427
|
+
async function help({ path, header, commands, defaultCommand }, stream) {
|
|
428
|
+
const colors = paint(stream);
|
|
429
|
+
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
430
|
+
const prefix = path.join(" ");
|
|
431
|
+
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
432
|
+
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
433
|
+
return [
|
|
434
|
+
header(colors),
|
|
435
|
+
"",
|
|
436
|
+
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
437
|
+
"",
|
|
438
|
+
colors.bold("Commands:"),
|
|
439
|
+
...Object.entries(commands).map(([name, { summary }]) => ` ${colors.cyan(name.padEnd(width))} ${colors.dim(summary)}`),
|
|
440
|
+
...options === undefined || Object.keys(options).length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(options, colors)],
|
|
441
|
+
"",
|
|
442
|
+
`${colors.dim("Run")} ${prefix} ${colors.cyan("<command>")} --help ${colors.dim("for its options.")}`
|
|
443
|
+
].join(`
|
|
444
|
+
`);
|
|
445
|
+
}
|
|
446
|
+
function printError(prefix, message, hint, cause) {
|
|
447
|
+
const colors = paint(process4.stderr);
|
|
448
|
+
const styled = takesControlCodes(process4.stderr);
|
|
449
|
+
const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
|
|
450
|
+
const under = (label, text, tint) => text === undefined ? "" : `
|
|
451
|
+
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
452
|
+
console.error(`${head} ${message}${under("cause", cause, colors.dim)}${under("hint", hint, colors.yellow)}`);
|
|
453
|
+
}
|
|
454
|
+
function reportThrown(prefix, error, redact = (text) => text) {
|
|
455
|
+
if (error instanceof HelpShown)
|
|
456
|
+
return 0;
|
|
457
|
+
const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
|
|
458
|
+
const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
|
|
459
|
+
const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
|
|
460
|
+
printError(prefix, redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
|
|
461
|
+
return error instanceof Exit ? error.code : 1;
|
|
462
|
+
}
|
|
463
|
+
async function dispatch(level, argv, { version, redact }) {
|
|
464
|
+
const [name, ...args] = argv;
|
|
465
|
+
const prefix = level.path.join(" ");
|
|
466
|
+
if (name === undefined && level.path.length > 1) {
|
|
467
|
+
console.error(await help(level, process4.stderr));
|
|
468
|
+
return 2;
|
|
469
|
+
}
|
|
470
|
+
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
471
|
+
console.log(await help(level, process4.stdout));
|
|
472
|
+
return 0;
|
|
473
|
+
}
|
|
474
|
+
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
475
|
+
console.log(version);
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
let command = level.commands[level.aliases?.[name] ?? name];
|
|
479
|
+
let rest = args;
|
|
480
|
+
if (command === undefined && level.defaultCommand !== undefined) {
|
|
481
|
+
command = level.commands[level.defaultCommand];
|
|
482
|
+
rest = argv;
|
|
483
|
+
}
|
|
484
|
+
if (command === undefined) {
|
|
485
|
+
const meant = closest(name, Object.keys(level.commands));
|
|
486
|
+
printError(prefix, `no command ${name}`, meant === undefined ? `run ${prefix} --help for its commands` : `did you mean ${meant}?`);
|
|
487
|
+
return 2;
|
|
488
|
+
}
|
|
489
|
+
if ("commands" in command) {
|
|
490
|
+
const { summary } = command;
|
|
491
|
+
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${summary}`, commands: command.commands, aliases: command.aliases, defaultCommand: undefined }, rest, { version, redact });
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
return await command.run(rest);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
return reportThrown(`${prefix} ${name}`, error, redact);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
var COMPLETE = "__complete";
|
|
500
|
+
var FILES = "__files__";
|
|
501
|
+
async function optionsOf(run) {
|
|
502
|
+
completion.active = true;
|
|
503
|
+
try {
|
|
504
|
+
await run([]);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (error instanceof Completing)
|
|
507
|
+
return error.options;
|
|
508
|
+
} finally {
|
|
509
|
+
completion.active = false;
|
|
510
|
+
}
|
|
511
|
+
return {};
|
|
512
|
+
}
|
|
513
|
+
async function leafCandidates(run, words) {
|
|
514
|
+
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
515
|
+
return [FILES];
|
|
516
|
+
return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
|
|
517
|
+
}
|
|
518
|
+
async function candidates(commands, aliases, defaultCommand, words) {
|
|
519
|
+
const [word = "", ...rest] = words;
|
|
520
|
+
const command = commands[aliases?.[word] ?? word];
|
|
521
|
+
if (rest.length === 0 && !word.startsWith("-"))
|
|
522
|
+
return Object.entries(commands).map(([name, { summary }]) => `${name} ${summary}`);
|
|
523
|
+
if (command === undefined) {
|
|
524
|
+
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
525
|
+
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
526
|
+
}
|
|
527
|
+
return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
|
|
528
|
+
}
|
|
529
|
+
async function printCandidates(found) {
|
|
530
|
+
for (const line of await found)
|
|
531
|
+
console.log(line);
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
async function runCli(cli, argv = process4.argv.slice(2)) {
|
|
535
|
+
if (argv[0] === COMPLETE)
|
|
536
|
+
return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
|
|
537
|
+
return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${cli.about}`, commands: cli.commands, aliases: cli.aliases, defaultCommand: cli.defaultCommand }, argv, { version: cli.version, redact: cli.redact });
|
|
538
|
+
}
|
|
539
|
+
// ../kit/src/process.ts
|
|
540
|
+
import { constants } from "os";
|
|
541
|
+
async function collect(stream, name, chunks) {
|
|
542
|
+
for await (const data of stream)
|
|
543
|
+
chunks.push({ stream: name, data });
|
|
544
|
+
}
|
|
545
|
+
function exitCodeOf(code, signal) {
|
|
546
|
+
if (code !== null)
|
|
547
|
+
return code;
|
|
548
|
+
if (typeof signal === "number")
|
|
549
|
+
return 128 + signal;
|
|
550
|
+
return signal === null ? 129 : 128 + constants.signals[signal];
|
|
551
|
+
}
|
|
552
|
+
async function runCommand(argv, { cwd, env, stdin = "ignore" } = {}) {
|
|
553
|
+
const chunks = [];
|
|
554
|
+
const proc = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, ...env === undefined ? {} : { env }, stdin, stdout: "pipe", stderr: "pipe" });
|
|
555
|
+
await Promise.all([collect(proc.stdout, "stdout", chunks), collect(proc.stderr, "stderr", chunks)]);
|
|
556
|
+
await proc.exited;
|
|
557
|
+
const output = Buffer.concat(chunks.map(({ data }) => data));
|
|
558
|
+
return { code: exitCodeOf(proc.exitCode, proc.signalCode), chunks, output, text: output.toString() };
|
|
559
|
+
}
|
|
560
|
+
// package.json
|
|
561
|
+
var version = "0.2.1";
|
|
562
|
+
|
|
563
|
+
// ../kit/src/shell.ts
|
|
564
|
+
var SEPARATORS = new Set([`
|
|
565
|
+
`, ";", "|"]);
|
|
566
|
+
var PLAIN_BREAKERS = new Set(["<", ">", "(", ")", "&", "|"]);
|
|
567
|
+
var WHITESPACE = /\s/;
|
|
568
|
+
var ASSIGNMENT = /^\w+=/;
|
|
569
|
+
var TRAILING_SLASHES = /\/+$/;
|
|
570
|
+
var LEADING_TILDE = /^~(?=\/|$)/;
|
|
571
|
+
var LEADING_TABS = /^\t+/;
|
|
572
|
+
var OPENERS = /^[({]+/;
|
|
573
|
+
var CLOSERS = /[)}]+$/;
|
|
574
|
+
var HEREDOC = /^<<(-?)[ \t]*(?:'([^']*)'|"([^"]*)"|([^\s;&|<>()-][^\s;&|<>()]*))/;
|
|
575
|
+
var WRAPPERS = new Set(["command", "env", "sudo", "exec", "nohup", "time"]);
|
|
576
|
+
function separatorWidth(command, i) {
|
|
577
|
+
const pair = command.slice(i, i + 2);
|
|
578
|
+
if (pair === "&&" || pair === "||")
|
|
579
|
+
return 2;
|
|
580
|
+
const char = command[i] ?? "";
|
|
581
|
+
if (char === "&")
|
|
582
|
+
return command[i - 1] === ">" || command[i - 1] === "<" || command[i + 1] === ">" ? 0 : 1;
|
|
583
|
+
return SEPARATORS.has(char) ? 1 : 0;
|
|
584
|
+
}
|
|
585
|
+
function heredocEnd(command, newline, words) {
|
|
586
|
+
let lineStart = newline + 1;
|
|
587
|
+
for (const { word, stripsTabs } of words) {
|
|
588
|
+
while (lineStart <= command.length) {
|
|
589
|
+
const next = command.indexOf(`
|
|
590
|
+
`, lineStart);
|
|
591
|
+
const lineEnd = next === -1 ? command.length : next;
|
|
592
|
+
const line = command.slice(lineStart, lineEnd);
|
|
593
|
+
lineStart = lineEnd + 1;
|
|
594
|
+
if ((stripsTabs ? line.replace(LEADING_TABS, "") : line) === word)
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return Math.min(lineStart - 1, command.length);
|
|
599
|
+
}
|
|
600
|
+
function withoutRanges(command, start, end, ranges) {
|
|
601
|
+
let text = "";
|
|
602
|
+
let at = start;
|
|
603
|
+
for (const [from, to] of ranges) {
|
|
604
|
+
if (to <= start || from >= end)
|
|
605
|
+
continue;
|
|
606
|
+
text += command.slice(at, Math.max(at, from));
|
|
607
|
+
at = Math.max(at, to);
|
|
608
|
+
}
|
|
609
|
+
return text + command.slice(at, end);
|
|
610
|
+
}
|
|
611
|
+
function splitCommand(command) {
|
|
612
|
+
const bounds = [];
|
|
613
|
+
const bodies = [];
|
|
614
|
+
let heredocs = [];
|
|
615
|
+
let start = 0;
|
|
616
|
+
let quote = null;
|
|
617
|
+
for (let i = 0;i < command.length; i++) {
|
|
618
|
+
const char = command[i] ?? "";
|
|
619
|
+
if (char === "\\" && quote !== "'") {
|
|
620
|
+
i++;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (quote !== null) {
|
|
624
|
+
if (char === quote)
|
|
625
|
+
quote = null;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (char === "'" || char === '"') {
|
|
629
|
+
quote = char;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const heredoc = char === "<" && command[i + 1] === "<" && command[i + 2] !== "<" ? HEREDOC.exec(command.slice(i)) : null;
|
|
633
|
+
if (heredoc) {
|
|
634
|
+
heredocs.push({ word: heredoc[2] ?? heredoc[3] ?? heredoc[4] ?? "", stripsTabs: heredoc[1] === "-", isQuoted: heredoc[4] === undefined });
|
|
635
|
+
i += heredoc[0].length - 1;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (char === `
|
|
639
|
+
` && heredocs.length > 0) {
|
|
640
|
+
const end = heredocEnd(command, i, heredocs);
|
|
641
|
+
bodies.push([i, end, heredocs.some((each) => !each.isQuoted)]);
|
|
642
|
+
heredocs = [];
|
|
643
|
+
i = end - 1;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const width = separatorWidth(command, i);
|
|
647
|
+
if (width === 0)
|
|
648
|
+
continue;
|
|
649
|
+
bounds.push([start, i, command.slice(i, i + width)]);
|
|
650
|
+
i += width - 1;
|
|
651
|
+
start = i + 1;
|
|
652
|
+
}
|
|
653
|
+
bounds.push([start, command.length, ""]);
|
|
654
|
+
const ranges = bodies.map(([from, to]) => [from, to]);
|
|
655
|
+
return bounds.map(([from, to, separator]) => ({
|
|
656
|
+
text: command.slice(from, to),
|
|
657
|
+
start: from,
|
|
658
|
+
end: to,
|
|
659
|
+
command: withoutRanges(command, from, to, ranges),
|
|
660
|
+
separator,
|
|
661
|
+
expanded: bodies.filter(([body, , expands]) => expands && body >= from && body < to).map(([body, bodyEnd]) => command.slice(body, bodyEnd)).join("")
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
function isPlain(text) {
|
|
665
|
+
let quote = null;
|
|
666
|
+
for (let i = 0;i < text.length; i++) {
|
|
667
|
+
const char = text[i] ?? "";
|
|
668
|
+
const isSubstitution = char === "`" || char === "$" && text[i + 1] === "(";
|
|
669
|
+
if (char === "\\" && quote !== "'")
|
|
670
|
+
i++;
|
|
671
|
+
else if (quote !== null && char === quote)
|
|
672
|
+
quote = null;
|
|
673
|
+
else if (quote === null && (char === '"' || char === "'"))
|
|
674
|
+
quote = char;
|
|
675
|
+
else if (quote !== "'" && (isSubstitution || quote === null && PLAIN_BREAKERS.has(char)))
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
return quote === null;
|
|
679
|
+
}
|
|
680
|
+
function closingParen(text, open) {
|
|
681
|
+
let depth = 0;
|
|
682
|
+
let quote = null;
|
|
683
|
+
for (let i = open;i < text.length; i++) {
|
|
684
|
+
const char = text[i] ?? "";
|
|
685
|
+
if (char === "\\" && quote !== "'") {
|
|
686
|
+
i++;
|
|
687
|
+
} else if (quote !== null) {
|
|
688
|
+
if (char === quote)
|
|
689
|
+
quote = null;
|
|
690
|
+
} else if (char === "'" || char === '"') {
|
|
691
|
+
quote = char;
|
|
692
|
+
} else if (char === "(") {
|
|
693
|
+
depth++;
|
|
694
|
+
} else if (char === ")" && --depth === 0) {
|
|
695
|
+
return i;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return text.length;
|
|
699
|
+
}
|
|
700
|
+
function closingBackquote(text, open) {
|
|
701
|
+
for (let i = open + 1;i < text.length; i++) {
|
|
702
|
+
if (text[i] === "\\")
|
|
703
|
+
i++;
|
|
704
|
+
else if (text[i] === "`")
|
|
705
|
+
return i;
|
|
706
|
+
}
|
|
707
|
+
return text.length;
|
|
708
|
+
}
|
|
709
|
+
function bodies(text, isQuoting) {
|
|
710
|
+
const found = [];
|
|
711
|
+
let quote = null;
|
|
712
|
+
for (let i = 0;i < text.length; i++) {
|
|
713
|
+
const char = text[i] ?? "";
|
|
714
|
+
if (quote === "'") {
|
|
715
|
+
if (char === "'")
|
|
716
|
+
quote = null;
|
|
717
|
+
} else if (char === "\\") {
|
|
718
|
+
i++;
|
|
719
|
+
} else if (char === "$" && text[i + 1] === "(") {
|
|
720
|
+
const end = closingParen(text, i + 1);
|
|
721
|
+
found.push(text.slice(i + 2, end));
|
|
722
|
+
i = end;
|
|
723
|
+
} else if (char === "`") {
|
|
724
|
+
const end = closingBackquote(text, i);
|
|
725
|
+
found.push(text.slice(i + 1, end));
|
|
726
|
+
i = end;
|
|
727
|
+
} else if (isQuoting && char === '"') {
|
|
728
|
+
quote = quote === null ? char : null;
|
|
729
|
+
} else if (isQuoting && char === "'" && quote === null) {
|
|
730
|
+
quote = char;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return found;
|
|
734
|
+
}
|
|
735
|
+
function substitutions(command) {
|
|
736
|
+
return splitCommand(command).flatMap((piece) => [...bodies(piece.command, true), ...bodies(piece.expanded, false)]);
|
|
737
|
+
}
|
|
738
|
+
function words(text) {
|
|
739
|
+
const found = [];
|
|
740
|
+
let current = "";
|
|
741
|
+
let isStarted = false;
|
|
742
|
+
let isProse = false;
|
|
743
|
+
let quote = null;
|
|
744
|
+
for (let i = 0;i < text.length; i++) {
|
|
745
|
+
const char = text[i] ?? "";
|
|
746
|
+
const isContinuation = char === "\\" && quote !== "'" && text[i + 1] === `
|
|
747
|
+
`;
|
|
748
|
+
if (isContinuation)
|
|
749
|
+
i++;
|
|
750
|
+
if (quote === null && (isContinuation || WHITESPACE.test(char))) {
|
|
751
|
+
if (isStarted)
|
|
752
|
+
found.push({ text: current, isProse });
|
|
753
|
+
current = "";
|
|
754
|
+
isStarted = false;
|
|
755
|
+
isProse = false;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (isContinuation)
|
|
759
|
+
continue;
|
|
760
|
+
isStarted = true;
|
|
761
|
+
if (char === "\\" && quote !== "'")
|
|
762
|
+
current += text[++i] ?? "";
|
|
763
|
+
else if (quote !== null && char === quote)
|
|
764
|
+
quote = null;
|
|
765
|
+
else if (quote === null && (char === '"' || char === "'"))
|
|
766
|
+
quote = char;
|
|
767
|
+
else
|
|
768
|
+
current += char;
|
|
769
|
+
isProse ||= quote !== null && WHITESPACE.test(char);
|
|
770
|
+
}
|
|
771
|
+
if (isStarted)
|
|
772
|
+
found.push({ text: current, isProse });
|
|
773
|
+
return found;
|
|
774
|
+
}
|
|
775
|
+
function tokenize(text) {
|
|
776
|
+
return words(text).map((word) => word.text);
|
|
777
|
+
}
|
|
778
|
+
function parseCommand(text) {
|
|
779
|
+
const tokens = tokenize(text);
|
|
780
|
+
const assignments = [];
|
|
781
|
+
let isWrapped = false;
|
|
782
|
+
for (;; ) {
|
|
783
|
+
const first = tokens[0] ?? "";
|
|
784
|
+
if (OPENERS.test(first)) {
|
|
785
|
+
isWrapped = true;
|
|
786
|
+
const rest = first.replace(OPENERS, "");
|
|
787
|
+
if (rest === "")
|
|
788
|
+
tokens.shift();
|
|
789
|
+
else
|
|
790
|
+
tokens[0] = rest;
|
|
791
|
+
} else if (ASSIGNMENT.test(first)) {
|
|
792
|
+
assignments.push(tokens.shift() ?? "");
|
|
793
|
+
} else if (WRAPPERS.has(first)) {
|
|
794
|
+
isWrapped = true;
|
|
795
|
+
tokens.shift();
|
|
796
|
+
while (tokens[0]?.startsWith("-"))
|
|
797
|
+
tokens.shift();
|
|
798
|
+
} else {
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const last = (tokens.at(-1) ?? "").replace(CLOSERS, "");
|
|
803
|
+
if (tokens.length > 0)
|
|
804
|
+
tokens.splice(-1, 1, ...last === "" ? [] : [last]);
|
|
805
|
+
const [program = "", ...args] = tokens;
|
|
806
|
+
return { program: program.split("/").pop() ?? "", args, assignments, isWrapped };
|
|
807
|
+
}
|
|
808
|
+
function replacePieces(command, replacements) {
|
|
809
|
+
let result = command;
|
|
810
|
+
for (const [piece, text] of [...replacements].sort(([a], [b]) => b.start - a.start)) {
|
|
811
|
+
result = result.slice(0, piece.start) + text + result.slice(piece.end);
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
815
|
+
function parentDir(path) {
|
|
816
|
+
const cut = path.replace(TRAILING_SLASHES, "").lastIndexOf("/");
|
|
817
|
+
return cut <= 0 ? "/" : path.slice(0, cut);
|
|
818
|
+
}
|
|
819
|
+
function resolvePath(base, arg, home) {
|
|
820
|
+
const expanded = arg.replace(LEADING_TILDE, home);
|
|
821
|
+
const parts = [];
|
|
822
|
+
for (const part of (expanded.startsWith("/") ? expanded : `${base}/${expanded}`).split("/")) {
|
|
823
|
+
if (part === "..")
|
|
824
|
+
parts.pop();
|
|
825
|
+
else if (part !== "" && part !== ".")
|
|
826
|
+
parts.push(part);
|
|
827
|
+
}
|
|
828
|
+
return `/${parts.join("/")}`;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ../db/src/guard.ts
|
|
832
|
+
var WORD_START = /[a-z_]/i;
|
|
833
|
+
var WORD_PART = /[\w$]/;
|
|
834
|
+
var SPACE = /\s/;
|
|
835
|
+
var ESCAPE_PREFIX = /e/i;
|
|
836
|
+
var DOLLAR_TAG = /^\$(?:[a-z_]\w*)?\$/i;
|
|
837
|
+
var LOCK_KIND = /^(?:update|share|no|key)$/;
|
|
838
|
+
var DIGITS = /\d/;
|
|
839
|
+
var COMMAND_ENDS = ";";
|
|
840
|
+
var UNCLOSED = "unclosed";
|
|
841
|
+
var PRIVILEGED_SET = new Set(["role", "session", "transaction"]);
|
|
842
|
+
var READ_STARTS = new Set(["select", "with", "values", "table", "show", "explain"]);
|
|
843
|
+
var WRITE_WORDS = new Set(["insert", "update", "delete", "merge", "truncate", "drop", "alter", "create", "grant", "revoke", "into", "call", "vacuum", "lock", "refresh", "reindex", "cluster", "do"]);
|
|
844
|
+
var SIDE_EFFECT_FUNCTIONS = /^(?:nextval|setval|set_config|pg_terminate_backend|pg_cancel_backend|pg_reload_conf|pg_rotate_logfile|pg_switch_wal|pg_create_restore_point|pg_advisory_lock\w*|pg_advisory_xact_lock\w*|pg_try_advisory\w*|pg_notify|lo_\w+|dblink\w*|pg_read_file|pg_read_binary_file|pg_ls_dir|query_to_xml\w*|pg_logical_\w+|pg_replication_\w+|pg_drop_replication_slot|pg_stat_reset\w*|pg_promote|pg_file_\w+)$/;
|
|
845
|
+
function tokenize2(sql) {
|
|
846
|
+
const tokens = [];
|
|
847
|
+
let at = 0;
|
|
848
|
+
while (at < sql.length) {
|
|
849
|
+
const char = sql.charAt(at);
|
|
850
|
+
const next = sql.charAt(at + 1);
|
|
851
|
+
if (SPACE.test(char)) {
|
|
852
|
+
at++;
|
|
853
|
+
} else if (char === "-" && next === "-") {
|
|
854
|
+
const end = sql.indexOf(`
|
|
855
|
+
`, at);
|
|
856
|
+
at = end === -1 ? sql.length : end + 1;
|
|
857
|
+
} else if (char === "/" && next === "*" && sql.charAt(at + 2) === "!") {
|
|
858
|
+
at += 3;
|
|
859
|
+
while (DIGITS.test(sql.charAt(at)))
|
|
860
|
+
at++;
|
|
861
|
+
} else if (char === "/" && next === "*") {
|
|
862
|
+
at = skipComment(sql, at);
|
|
863
|
+
} else if (char === "'" || char === '"' || char === "`") {
|
|
864
|
+
const backslashes = char === "'" && ESCAPE_PREFIX.test(sql.charAt(at - 1)) && !WORD_PART.test(sql.charAt(at - 2));
|
|
865
|
+
const end = skipQuoted(sql, at, char, backslashes);
|
|
866
|
+
at = end ?? sql.length;
|
|
867
|
+
tokens.push({ kind: "punct", text: end === undefined ? UNCLOSED : char });
|
|
868
|
+
} else if (char === "$" && DOLLAR_TAG.test(sql.slice(at))) {
|
|
869
|
+
const tag = DOLLAR_TAG.exec(sql.slice(at))?.[0] ?? "$$";
|
|
870
|
+
const end = sql.indexOf(tag, at + tag.length);
|
|
871
|
+
at = end === -1 ? sql.length : end + tag.length;
|
|
872
|
+
tokens.push({ kind: "punct", text: end === -1 ? UNCLOSED : "$" });
|
|
873
|
+
} else if (WORD_START.test(char)) {
|
|
874
|
+
let end = at + 1;
|
|
875
|
+
while (end < sql.length && WORD_PART.test(sql.charAt(end)))
|
|
876
|
+
end++;
|
|
877
|
+
tokens.push({ kind: "word", text: sql.slice(at, end).toLowerCase() });
|
|
878
|
+
at = end;
|
|
879
|
+
} else {
|
|
880
|
+
tokens.push({ kind: "punct", text: char });
|
|
881
|
+
at++;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return tokens;
|
|
885
|
+
}
|
|
886
|
+
function skipComment(sql, start) {
|
|
887
|
+
let depth = 0;
|
|
888
|
+
let at = start;
|
|
889
|
+
while (at < sql.length) {
|
|
890
|
+
if (sql.startsWith("/*", at)) {
|
|
891
|
+
depth++;
|
|
892
|
+
at += 2;
|
|
893
|
+
} else if (sql.startsWith("*/", at)) {
|
|
894
|
+
depth--;
|
|
895
|
+
at += 2;
|
|
896
|
+
if (depth === 0)
|
|
897
|
+
return at;
|
|
898
|
+
} else {
|
|
899
|
+
at++;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return sql.length;
|
|
903
|
+
}
|
|
904
|
+
function skipQuoted(sql, start, quote, backslashes) {
|
|
905
|
+
let at = start + 1;
|
|
906
|
+
while (at < sql.length) {
|
|
907
|
+
const char = sql.charAt(at);
|
|
908
|
+
if (backslashes && char === "\\") {
|
|
909
|
+
at += 2;
|
|
910
|
+
} else if (char === quote) {
|
|
911
|
+
if (sql.charAt(at + 1) !== quote)
|
|
912
|
+
return at + 1;
|
|
913
|
+
at += 2;
|
|
914
|
+
} else {
|
|
915
|
+
at++;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
function splitStatements(tokens) {
|
|
921
|
+
const statements = [[]];
|
|
922
|
+
for (const token of tokens) {
|
|
923
|
+
if (token.kind === "punct" && token.text === COMMAND_ENDS)
|
|
924
|
+
statements.push([]);
|
|
925
|
+
else
|
|
926
|
+
statements.at(-1)?.push(token);
|
|
927
|
+
}
|
|
928
|
+
return statements.filter((statement) => statement.length > 0);
|
|
929
|
+
}
|
|
930
|
+
var isPunct = (token, text) => token?.kind === "punct" && token.text === text;
|
|
931
|
+
function copyReads(statement, index) {
|
|
932
|
+
let depth = 0;
|
|
933
|
+
for (const [at, token] of statement.entries()) {
|
|
934
|
+
if (at <= index)
|
|
935
|
+
continue;
|
|
936
|
+
if (isPunct(token, "("))
|
|
937
|
+
depth++;
|
|
938
|
+
else if (isPunct(token, ")"))
|
|
939
|
+
depth--;
|
|
940
|
+
else if (depth === 0 && token.kind === "word" && (token.text === "from" || token.text === "to"))
|
|
941
|
+
return token.text === "to" && (statement[at + 1]?.text === "stdout" || isPunct(statement[index - 1], "\\"));
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
function statementWrites(statement) {
|
|
946
|
+
const first = statement[0];
|
|
947
|
+
const start = isPunct(first, "\\") ? 1 : 0;
|
|
948
|
+
const reasons = [];
|
|
949
|
+
for (const [index, token] of statement.entries()) {
|
|
950
|
+
if (isPunct(token, UNCLOSED))
|
|
951
|
+
reasons.push("has a quote that never closes");
|
|
952
|
+
if (token.kind !== "word")
|
|
953
|
+
continue;
|
|
954
|
+
if (SIDE_EFFECT_FUNCTIONS.test(token.text)) {
|
|
955
|
+
reasons.push(`calls ${token.text}`);
|
|
956
|
+
} else if (isPunct(statement[index - 1], ".")) {
|
|
957
|
+
continue;
|
|
958
|
+
} else if (token.text === "set") {
|
|
959
|
+
if (index !== start || PRIVILEGED_SET.has(statement[index + 1]?.text ?? ""))
|
|
960
|
+
reasons.push("uses SET");
|
|
961
|
+
} else if (token.text === "copy") {
|
|
962
|
+
if (!copyReads(statement, index))
|
|
963
|
+
reasons.push("uses COPY");
|
|
964
|
+
} else if (WRITE_WORDS.has(token.text)) {
|
|
965
|
+
reasons.push(`uses ${token.text.toUpperCase()}`);
|
|
966
|
+
} else if (token.text === "analyze" && first?.text !== "explain") {
|
|
967
|
+
reasons.push("uses ANALYZE");
|
|
968
|
+
} else if (token.text === "for" && LOCK_KIND.test(statement[index + 1]?.text ?? "")) {
|
|
969
|
+
reasons.push("takes row locks with FOR UPDATE or SHARE");
|
|
970
|
+
} else if (token.text === "comment" && statement[index + 1]?.text === "on") {
|
|
971
|
+
reasons.push("uses COMMENT ON");
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return reasons;
|
|
975
|
+
}
|
|
976
|
+
function writes(sql) {
|
|
977
|
+
return [...new Set(splitStatements(tokenize2(sql)).flatMap(statementWrites))];
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ../db/src/clients.ts
|
|
981
|
+
var SQL_CLIENTS = new Set(["psql", "mysql", "mariadb", "pgcli", "mycli", "sqlite3"]);
|
|
982
|
+
var WRITING_TOOLS = new Set(["dropdb", "createdb", "pg_restore"]);
|
|
983
|
+
var CONSOLE_PROGRAMS = new Set(["php", "docker", "symfony", "console", "artisan"]);
|
|
984
|
+
var STATEMENT_OPTIONS = new Set(["-c", "--command", "-e", "--execute"]);
|
|
985
|
+
var FILE_OPTIONS = new Set(["-f", "--file", "<"]);
|
|
986
|
+
var PRINTERS = new Set(["echo", "printf"]);
|
|
987
|
+
var LIST_OPTIONS = new Set(["-l", "--list"]);
|
|
988
|
+
var WRITING_CONSOLE = /^(?:doctrine:(?:migrations:(?:migrate|execute|rollup)|schema:(?:update|create|drop)|database:(?:create|drop)|fixtures:load)|migrate(?::\w+)?|db:(?:seed|wipe))$/;
|
|
989
|
+
var TEST_SCOPED = /(?:^|[^a-z])test/i;
|
|
990
|
+
var SHOT_STACK = /\.worktrees\/[^/\s]+\/shot(?:\/|$)|--shot(?:-|$)/;
|
|
991
|
+
var SQL_WRITE = "Statements that write (INSERT, UPDATE, DELETE, DDL, migrations, fixtures) are handed over: list the exact command for the user to run. Only a clearly test-only database runs them directly.";
|
|
992
|
+
function hasWrite(statement) {
|
|
993
|
+
return writes(statement).length > 0;
|
|
994
|
+
}
|
|
995
|
+
function programOf(word) {
|
|
996
|
+
return word.split("/").pop() ?? "";
|
|
997
|
+
}
|
|
998
|
+
function isTestScoped(words) {
|
|
999
|
+
const client = words.findIndex((word) => SQL_CLIENTS.has(programOf(word)));
|
|
1000
|
+
return words.some((word, i) => (TEST_SCOPED.test(word) || SHOT_STACK.test(word)) && (i <= client || client === -1 || !STATEMENT_OPTIONS.has(words[i - 1] ?? "") && !FILE_OPTIONS.has(words[i - 1] ?? "")));
|
|
1001
|
+
}
|
|
1002
|
+
async function sqlViolation(command, read) {
|
|
1003
|
+
const pieces = splitCommand(command);
|
|
1004
|
+
let inShotStack = false;
|
|
1005
|
+
for (const [index, piece] of pieces.entries()) {
|
|
1006
|
+
const words = tokenize(piece.command);
|
|
1007
|
+
const { program, args } = parseCommand(piece.command);
|
|
1008
|
+
if (program === "cd")
|
|
1009
|
+
inShotStack = SHOT_STACK.test(args[0] ?? "");
|
|
1010
|
+
if (inShotStack || isTestScoped(words))
|
|
1011
|
+
continue;
|
|
1012
|
+
if (WRITING_TOOLS.has(program) && !words.some((word) => LIST_OPTIONS.has(word)))
|
|
1013
|
+
return SQL_WRITE;
|
|
1014
|
+
if (CONSOLE_PROGRAMS.has(program) && words.some((word) => WRITING_CONSOLE.test(word)))
|
|
1015
|
+
return SQL_WRITE;
|
|
1016
|
+
const client = words.findIndex((word) => SQL_CLIENTS.has(programOf(word)));
|
|
1017
|
+
if (client === -1)
|
|
1018
|
+
continue;
|
|
1019
|
+
const files = words.filter((_, i) => i > client && FILE_OPTIONS.has(words[i - 1] ?? ""));
|
|
1020
|
+
const before = index === 0 ? null : parseCommand(pieces[index - 1]?.command ?? "");
|
|
1021
|
+
const statements = [
|
|
1022
|
+
...words.filter((_, i) => i > client && STATEMENT_OPTIONS.has(words[i - 1] ?? "")),
|
|
1023
|
+
piece.text === piece.command ? "" : piece.text.slice(piece.text.indexOf(`
|
|
1024
|
+
`)),
|
|
1025
|
+
before !== null && PRINTERS.has(before.program) ? before.args.join(" ") : "",
|
|
1026
|
+
...(await Promise.all(files.map(read))).map((text) => text ?? "")
|
|
1027
|
+
];
|
|
1028
|
+
if (statements.some(hasWrite))
|
|
1029
|
+
return SQL_WRITE;
|
|
1030
|
+
}
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ../env/src/files.ts
|
|
1035
|
+
var ENV_FILE = /^\.env(?:\..+)?$/;
|
|
1036
|
+
var TEMPLATE = /\.(?:example|sample|template|dist|defaults)$/;
|
|
1037
|
+
|
|
1038
|
+
// ../env/src/guard.ts
|
|
1039
|
+
var REDIRECT = /^\d*[<>]+&?/;
|
|
1040
|
+
var ENV_DUMP = /(?:console\.\w+|JSON\.stringify|Object\.(?:entries|keys|values)|print)\(\s*process\.env\s*[,)]/;
|
|
1041
|
+
var HARMLESS = new Set(["ev", "redact", "ls", "stat", "test", "[", "[[", "touch"]);
|
|
1042
|
+
var GIT_HARMLESS = new Set(["check-ignore", "ls-files", "status"]);
|
|
1043
|
+
var ENV_PRINTERS = new Set(["env", "printenv"]);
|
|
1044
|
+
var DOCKER = new Set(["docker", "docker-compose"]);
|
|
1045
|
+
var SHELLS = new Set(["sh", "bash", "zsh", "ash"]);
|
|
1046
|
+
var DOCKER_VALUE_OPTIONS = new Set(["-f", "--file", "-p", "--project-name", "--profile", "--env-file", "--project-directory", "--ansi", "--progress", "--parallel", "-H", "--host", "-c", "--context", "--config", "-l", "--log-level"]);
|
|
1047
|
+
var EXEC_VALUE_OPTIONS = new Set(["-e", "--env", "--env-file", "-u", "--user", "-w", "--workdir", "--index", "--detach-keys"]);
|
|
1048
|
+
var CONFIG_LISTS = new Set(["--services", "--volumes", "--profiles", "--images", "--networks", "--hash", "-q", "--quiet"]);
|
|
1049
|
+
var FORMAT_OPTIONS = new Set(["-f", "--format"]);
|
|
1050
|
+
var WHOLE_FORMAT = /Env|\{\{\s*json\s+\.\s*\}\}|^json$/;
|
|
1051
|
+
var USE_EV = '.env files hold secrets that must never reach the transcript, so they are not read, edited or printed directly. Use `ev` instead: `ev list`, `ev get KEY` (masked), `ev grep <regex>` for the keys matching it, `ev show <rev>:<file>` for one at a git revision, `printf %s "$value" | ev set KEY [file]` (`--empty` for an empty value), `ev unset KEY`, `ev mv KEY --from .env --to .env.local`, `ev diff a b`, `ev missing`; and pipe output that may carry a secret through `redact`, as `docker compose exec <svc> printenv | redact` or `docker compose config | redact`. If `ev` is missing: `bun install -g @kaynooo/env-tools`.';
|
|
1052
|
+
function isEnvFile(path) {
|
|
1053
|
+
const name = path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf(":")) + 1);
|
|
1054
|
+
return (ENV_FILE.test(name) || name === ".env*") && !TEMPLATE.test(name);
|
|
1055
|
+
}
|
|
1056
|
+
function namesEnvFile(word, previous) {
|
|
1057
|
+
if (previous === "--env-file" || word.startsWith("--env-file="))
|
|
1058
|
+
return false;
|
|
1059
|
+
const bare = word.replace(REDIRECT, "");
|
|
1060
|
+
return isEnvFile(bare.includes("=") ? bare.slice(bare.indexOf("=") + 1) : bare);
|
|
1061
|
+
}
|
|
1062
|
+
function firstOperand(words, valueOptions) {
|
|
1063
|
+
for (let i = 0;i < words.length; i++) {
|
|
1064
|
+
const word = words[i] ?? "";
|
|
1065
|
+
if (valueOptions.has(word))
|
|
1066
|
+
i++;
|
|
1067
|
+
else if (!word.startsWith("-"))
|
|
1068
|
+
return i;
|
|
1069
|
+
}
|
|
1070
|
+
return words.length;
|
|
1071
|
+
}
|
|
1072
|
+
function printsEnv([program = "", ...args]) {
|
|
1073
|
+
if (SHELLS.has(program) && args[0] === "-c")
|
|
1074
|
+
return splitCommand(args[1] ?? "").some((piece) => ENV_PRINTERS.has(tokenize(piece.command)[0] ?? ""));
|
|
1075
|
+
return ENV_PRINTERS.has(program);
|
|
1076
|
+
}
|
|
1077
|
+
function dumpsContainerEnv(program, args) {
|
|
1078
|
+
if (!DOCKER.has(program))
|
|
1079
|
+
return false;
|
|
1080
|
+
const afterOptions = (words) => words.slice(firstOperand(words, DOCKER_VALUE_OPTIONS));
|
|
1081
|
+
let rest = afterOptions(args);
|
|
1082
|
+
const isCompose = program === "docker-compose" || rest[0] === "compose";
|
|
1083
|
+
if (program === "docker" && isCompose)
|
|
1084
|
+
rest = afterOptions(rest.slice(1));
|
|
1085
|
+
const [subcommand = "", ...after] = rest;
|
|
1086
|
+
if (subcommand === "exec")
|
|
1087
|
+
return printsEnv(after.slice(firstOperand(after, EXEC_VALUE_OPTIONS) + 1));
|
|
1088
|
+
if (isCompose)
|
|
1089
|
+
return subcommand === "config" && !after.some((word) => CONFIG_LISTS.has(word));
|
|
1090
|
+
if (subcommand !== "inspect" && (subcommand !== "container" || after[0] !== "inspect"))
|
|
1091
|
+
return false;
|
|
1092
|
+
const format = after.find((_, i) => FORMAT_OPTIONS.has(after[i - 1] ?? "")) ?? after.find((word) => word.startsWith("--format="))?.slice("--format=".length);
|
|
1093
|
+
return format === undefined || WHOLE_FORMAT.test(format);
|
|
1094
|
+
}
|
|
1095
|
+
function readsEnv(piece) {
|
|
1096
|
+
const { program, args } = parseCommand(piece.command);
|
|
1097
|
+
const found = words(piece.command);
|
|
1098
|
+
if (program === "" || HARMLESS.has(program) || program === "git" && GIT_HARMLESS.has(args[0] ?? ""))
|
|
1099
|
+
return false;
|
|
1100
|
+
return found.some((word, index) => !word.isProse && namesEnvFile(word.text, found[index - 1]?.text));
|
|
1101
|
+
}
|
|
1102
|
+
function touchesEnv(command) {
|
|
1103
|
+
if (ENV_DUMP.test(command))
|
|
1104
|
+
return true;
|
|
1105
|
+
if (substitutions(command).some(touchesEnv))
|
|
1106
|
+
return true;
|
|
1107
|
+
const pieces = splitCommand(command);
|
|
1108
|
+
return pieces.some((piece, index) => {
|
|
1109
|
+
if (readsEnv(piece))
|
|
1110
|
+
return true;
|
|
1111
|
+
const { program, args } = parseCommand(piece.command);
|
|
1112
|
+
if (!dumpsContainerEnv(program, args))
|
|
1113
|
+
return false;
|
|
1114
|
+
for (let at = index;pieces[at]?.separator === "|"; at++) {
|
|
1115
|
+
if (parseCommand(pieces[at + 1]?.command ?? "").program === "redact")
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
return true;
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// ../git/src/message.ts
|
|
1123
|
+
var CONVENTIONAL = /^[a-z]+(?:\([^()\n]+\))?!?: \S/;
|
|
1124
|
+
var GIT_MADE = /^(?:Merge |Revert "|fixup! |squash! |amend! )/;
|
|
1125
|
+
var TICKET = /^(?:[\w.-]+\/[\w.-]+)?#\d+ - /;
|
|
1126
|
+
var BREAKING = /^BREAKING[ -]CHANGE: /;
|
|
1127
|
+
var BREAKING_TITLE = /^[a-z]+(?:\([^()\n]+\))?!: /;
|
|
1128
|
+
var TRAILER = /^(?:co-authored-by|signed-off-by|reviewed-by|acked-by|tested-by|reported-by|suggested-by|helped-by|change-id):|^\W*generated with\b/i;
|
|
1129
|
+
var BULLET = /^\s*(?:[-*+]|\d+[.)])\s/;
|
|
1130
|
+
var SENTENCE_END = /[.!?:;)\]`'"]$/;
|
|
1131
|
+
var LOWER_START = /^\p{Ll}/u;
|
|
1132
|
+
var FORMAT = "A commit message is a `type(scope): title` line, then optionally a blank line, one or two lines on what changed and one `#<n> - <title>` line per ticket.";
|
|
1133
|
+
var TITLE = `The title is no Conventional Commit, its type and scope taken from the repository's history. ${FORMAT}`;
|
|
1134
|
+
var TITLE_LINE = `The title takes one line and a blank line follows it. ${FORMAT}`;
|
|
1135
|
+
var TRAILERS = `No trailers, ever: no Co-Authored-By, Signed-off-by or "Generated with" line, no --signoff or --trailer. ${FORMAT}`;
|
|
1136
|
+
var BULLETS = `The body names what changed in a line or two, never a bullet list narrating the diff. ${FORMAT}`;
|
|
1137
|
+
var TOO_LONG = `The body holds at most two lines besides the ticket lines; what goes beyond belongs in the reply. ${FORMAT}`;
|
|
1138
|
+
var WRAPPED = `A body sentence stays on one line however long it runs: never hard-wrap it. ${FORMAT}`;
|
|
1139
|
+
function isDescription(line) {
|
|
1140
|
+
return line !== undefined && line !== "" && !TICKET.test(line);
|
|
1141
|
+
}
|
|
1142
|
+
function commitMessageViolation(message) {
|
|
1143
|
+
const lines = message.split(`
|
|
1144
|
+
`).map((line) => line.trimEnd());
|
|
1145
|
+
while (lines[0] === "")
|
|
1146
|
+
lines.shift();
|
|
1147
|
+
while (lines.at(-1) === "")
|
|
1148
|
+
lines.pop();
|
|
1149
|
+
const [title = "", second, ...body] = lines;
|
|
1150
|
+
if (title === "")
|
|
1151
|
+
return null;
|
|
1152
|
+
if (lines.some((line) => TRAILER.test(line)))
|
|
1153
|
+
return TRAILERS;
|
|
1154
|
+
if (!CONVENTIONAL.test(title) && !GIT_MADE.test(title))
|
|
1155
|
+
return TITLE;
|
|
1156
|
+
if (second !== undefined && second !== "")
|
|
1157
|
+
return TITLE_LINE;
|
|
1158
|
+
if (BREAKING_TITLE.test(title) || body.some((line) => BREAKING.test(line)))
|
|
1159
|
+
return null;
|
|
1160
|
+
const descriptions = body.filter(isDescription);
|
|
1161
|
+
if (descriptions.some((line) => BULLET.test(line)))
|
|
1162
|
+
return BULLETS;
|
|
1163
|
+
if (descriptions.length > 2)
|
|
1164
|
+
return TOO_LONG;
|
|
1165
|
+
const isWrapped = body.some((line, i) => isDescription(line) && isDescription(body[i + 1]) && !SENTENCE_END.test(line) && LOWER_START.test(body[i + 1] ?? ""));
|
|
1166
|
+
return isWrapped ? WRAPPED : null;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// ../out/src/wrap.ts
|
|
1170
|
+
var ASSIGNMENT2 = /^\w+=/;
|
|
1171
|
+
var EXCLUDED = /(?:^|\s)(?:--json|--help|--version|-h)(?:\s|$)/;
|
|
1172
|
+
var BUN_SCRIPT = /^(?:build|check|lint|typecheck|types|test|ci|verify|compile)(?::|$)/;
|
|
1173
|
+
var BUN_TOOLS = new Set(["tsc", "tsgo", "eslint", "keslint", "vitest", "jest", "prettier", "playwright", "biome", "vue-tsc"]);
|
|
1174
|
+
var BUN_SUBCOMMANDS = new Set(["install", "i", "add", "remove", "rm", "update", "test", "check", "lint", "build"]);
|
|
1175
|
+
var STDERR_MERGE = /\s+2>&1$/;
|
|
1176
|
+
var FILTERS = new Set(["tail", "head", "grep"]);
|
|
1177
|
+
var GREP_SUMMARY = /^(?:-[a-zA-Z]*[cqlL]|--(?:count|quiet|silent|files-with(?:out)?-match(?:es)?)$)/;
|
|
1178
|
+
var PHP_VALUE_OPTIONS = new Set(["-d", "-c", "-z"]);
|
|
1179
|
+
var BUNX_VALUE_OPTIONS = new Set(["-p", "--package"]);
|
|
1180
|
+
var COMPOSE_VALUE_OPTIONS = new Set(["-f", "--file", "-p", "--project-name", "--profile", "--env-file", "--project-directory", "--ansi", "--progress", "--parallel"]);
|
|
1181
|
+
var EXEC_VALUE_OPTIONS2 = new Set(["-e", "--env", "--env-file", "-u", "--user", "-w", "--workdir", "--index", "--detach-keys", "--name", "--entrypoint", "-v", "--volume", "-p", "--publish", "-l", "--label", "--cap-add", "--cap-drop", "--pull"]);
|
|
1182
|
+
var always = () => true;
|
|
1183
|
+
var typecheck = ([sub]) => sub === "typecheck";
|
|
1184
|
+
var RULES = {
|
|
1185
|
+
bun: ([sub, script]) => sub !== undefined && (BUN_SUBCOMMANDS.has(sub) || sub === "run" && script !== undefined && BUN_SCRIPT.test(script)),
|
|
1186
|
+
bunx: (args) => {
|
|
1187
|
+
const [tool, ...rest] = afterOptions(args, BUNX_VALUE_OPTIONS);
|
|
1188
|
+
return tool !== undefined && (BUN_TOOLS.has(tool) || (tool === "nuxi" || tool === "nuxt") && typecheck(rest));
|
|
1189
|
+
},
|
|
1190
|
+
tsc: always,
|
|
1191
|
+
tsgo: always,
|
|
1192
|
+
"vue-tsc": always,
|
|
1193
|
+
nuxi: typecheck,
|
|
1194
|
+
nuxt: typecheck,
|
|
1195
|
+
eslint: always,
|
|
1196
|
+
keslint: always,
|
|
1197
|
+
vitest: always,
|
|
1198
|
+
jest: always,
|
|
1199
|
+
pytest: always,
|
|
1200
|
+
phpunit: always,
|
|
1201
|
+
paratest: always,
|
|
1202
|
+
phpstan: always,
|
|
1203
|
+
pest: always,
|
|
1204
|
+
"php-cs-fixer": always,
|
|
1205
|
+
rector: always,
|
|
1206
|
+
php: (args) => listed(afterOptions(args, PHP_VALUE_OPTIONS)),
|
|
1207
|
+
make: always,
|
|
1208
|
+
check: always,
|
|
1209
|
+
just: ([first]) => first !== undefined && !first.startsWith("-"),
|
|
1210
|
+
composer: ([sub]) => sub === "install" || sub === "update" || sub === "require",
|
|
1211
|
+
pip: ([sub]) => sub === "install",
|
|
1212
|
+
cargo: ([sub]) => ["build", "test", "check", "clippy"].includes(sub ?? ""),
|
|
1213
|
+
go: ([sub]) => ["build", "test", "vet"].includes(sub ?? ""),
|
|
1214
|
+
docker: (args) => {
|
|
1215
|
+
if (args[0] === "build")
|
|
1216
|
+
return true;
|
|
1217
|
+
if (args[0] === "exec")
|
|
1218
|
+
return listed(afterOptions(args.slice(1), EXEC_VALUE_OPTIONS2).slice(1));
|
|
1219
|
+
if (args[0] !== "compose")
|
|
1220
|
+
return false;
|
|
1221
|
+
const [sub, ...rest] = afterOptions(args.slice(1), COMPOSE_VALUE_OPTIONS);
|
|
1222
|
+
if (sub === "exec" || sub === "run")
|
|
1223
|
+
return listed(afterOptions(rest, EXEC_VALUE_OPTIONS2).slice(1));
|
|
1224
|
+
return sub === "build" || sub === "pull";
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
function afterOptions(args, valued) {
|
|
1228
|
+
let index = 0;
|
|
1229
|
+
while (args[index]?.startsWith("-") === true) {
|
|
1230
|
+
index += valued.has(args[index] ?? "") ? 2 : 1;
|
|
1231
|
+
}
|
|
1232
|
+
return args.slice(index);
|
|
1233
|
+
}
|
|
1234
|
+
function scan(command) {
|
|
1235
|
+
const segments = [];
|
|
1236
|
+
const pipes = [];
|
|
1237
|
+
let current = "";
|
|
1238
|
+
let plain = true;
|
|
1239
|
+
for (const piece of splitCommand(command)) {
|
|
1240
|
+
current += piece.text;
|
|
1241
|
+
plain &&= isPlain(piece.text) && piece.separator !== "&";
|
|
1242
|
+
if (piece.separator === "|") {
|
|
1243
|
+
pipes.push(piece.end);
|
|
1244
|
+
current += "|";
|
|
1245
|
+
plain = false;
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
segments.push(current);
|
|
1249
|
+
current = "";
|
|
1250
|
+
}
|
|
1251
|
+
return { segments: segments.map((segment) => segment.trim()).filter((segment) => segment !== ""), plain, pipes };
|
|
1252
|
+
}
|
|
1253
|
+
function isFilter(stage) {
|
|
1254
|
+
const { segments, plain } = scan(stage);
|
|
1255
|
+
const [bin, ...args] = tokenize(stage);
|
|
1256
|
+
if (!plain || segments.length !== 1 || bin === undefined || !FILTERS.has(bin))
|
|
1257
|
+
return false;
|
|
1258
|
+
return bin !== "grep" || !args.some((arg) => GREP_SUMMARY.test(arg));
|
|
1259
|
+
}
|
|
1260
|
+
function withoutFilters(command) {
|
|
1261
|
+
let rest = command;
|
|
1262
|
+
for (let at = scan(rest).pipes.at(-1);at !== undefined && isFilter(rest.slice(at + 1).trim()); at = scan(rest).pipes.at(-1))
|
|
1263
|
+
rest = rest.slice(0, at).trimEnd().replace(STDERR_MERGE, "");
|
|
1264
|
+
return rest;
|
|
1265
|
+
}
|
|
1266
|
+
function listed(words) {
|
|
1267
|
+
const start = words.findIndex((word) => !ASSIGNMENT2.test(word));
|
|
1268
|
+
const path = words[start];
|
|
1269
|
+
if (path === undefined)
|
|
1270
|
+
return false;
|
|
1271
|
+
return RULES[path.slice(path.lastIndexOf("/") + 1)]?.(words.slice(start + 1)) ?? false;
|
|
1272
|
+
}
|
|
1273
|
+
function listedSegment(segment) {
|
|
1274
|
+
return !EXCLUDED.test(segment) && listed(tokenize(segment));
|
|
1275
|
+
}
|
|
1276
|
+
function isCd(segment) {
|
|
1277
|
+
return segment === "cd" || segment.startsWith("cd ");
|
|
1278
|
+
}
|
|
1279
|
+
function shouldWrap(command) {
|
|
1280
|
+
const { segments, plain } = scan(withoutFilters(command.trim()));
|
|
1281
|
+
if (!plain || !segments.some((segment) => listedSegment(segment)))
|
|
1282
|
+
return false;
|
|
1283
|
+
return segments.every((segment) => isCd(segment) || listedSegment(segment));
|
|
1284
|
+
}
|
|
1285
|
+
function quoted(value) {
|
|
1286
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1287
|
+
}
|
|
1288
|
+
function wrap(command) {
|
|
1289
|
+
const trimmed = withoutFilters(command.trim());
|
|
1290
|
+
if (scan(trimmed).segments.length <= 1 && !ASSIGNMENT2.test(trimmed))
|
|
1291
|
+
return `out -- ${trimmed}`;
|
|
1292
|
+
return `out -- sh -c ${quoted(trimmed)}`;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// ../pkg/src/added.ts
|
|
1296
|
+
var REDIRECT2 = /^\d*[<>]/;
|
|
1297
|
+
var BARE_REDIRECT = /^\d*(?:[<>]|>>)$/;
|
|
1298
|
+
var ADDS = {
|
|
1299
|
+
bun: new Set(["add", "a", "install", "i"]),
|
|
1300
|
+
npm: new Set(["install", "i", "add", "in", "isntall"]),
|
|
1301
|
+
pnpm: new Set(["add"]),
|
|
1302
|
+
yarn: new Set(["add"])
|
|
1303
|
+
};
|
|
1304
|
+
var VALUE_FLAGS = new Set(["--cwd", "--filter", "-F", "--workspace", "--registry", "--prefix", "-C", "--dir", "--config", "--cache-dir"]);
|
|
1305
|
+
var NOT_REGISTRY = /^(?:[./~]|file:|link:|portal:|workspace:|git[+:]|github:|gitlab:|bitbucket:|https?:)|\.tgz$|^[\w.-]+\/[\w.-]+(?:#.*)?$/;
|
|
1306
|
+
var NPM_ALIAS = /^(?:@[^/]+\/)?[^@]+@npm:(.+)$/;
|
|
1307
|
+
var PUBLISH = /\b(?:ft|cli\.ts)\s+release\b|\b(?:bun|npm|pnpm|yarn)\s+publish\b|\bjust\s+(?:patch|minor|major)\b/;
|
|
1308
|
+
function addedPackages(command) {
|
|
1309
|
+
let published = false;
|
|
1310
|
+
return splitCommand(command).flatMap((piece) => {
|
|
1311
|
+
published ||= PUBLISH.test(piece.command);
|
|
1312
|
+
const { program, args } = parseCommand(piece.command);
|
|
1313
|
+
const [sub = "", ...rest] = args;
|
|
1314
|
+
if (published || !ADDS[program]?.has(sub))
|
|
1315
|
+
return [];
|
|
1316
|
+
return rest.flatMap((word, index) => {
|
|
1317
|
+
const previous = rest[index - 1] ?? "";
|
|
1318
|
+
if (word.startsWith("-") || REDIRECT2.test(word) || VALUE_FLAGS.has(previous) || BARE_REDIRECT.test(previous) || NOT_REGISTRY.test(word))
|
|
1319
|
+
return [];
|
|
1320
|
+
return [NPM_ALIAS.exec(word)?.[1] ?? word];
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// ../pkg/src/pin.ts
|
|
1326
|
+
var LOCKFILES = [
|
|
1327
|
+
["bun.lock", "bun"],
|
|
1328
|
+
["bun.lockb", "bun"],
|
|
1329
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1330
|
+
["yarn.lock", "yarn"],
|
|
1331
|
+
["package-lock.json", "npm"]
|
|
1332
|
+
];
|
|
1333
|
+
var ACTIONS = {
|
|
1334
|
+
bun: actions({ install: ["install", "i"], add: ["add", "a"], remove: ["remove", "rm"], update: ["update"] }),
|
|
1335
|
+
pnpm: actions({ install: ["install", "i"], add: ["add"], remove: ["remove", "rm"], update: ["update", "up"] }),
|
|
1336
|
+
yarn: new Map([...actions({ install: ["", "install"], add: ["add"], remove: ["remove"], update: ["upgrade"] }), ["upgrade-interactive", null], ["import", null]]),
|
|
1337
|
+
npm: actions({
|
|
1338
|
+
install: ["install", "i", "in", "ins", "inst", "insta", "instal", "isnt", "isnta", "isntal", "isntall"],
|
|
1339
|
+
add: ["add"],
|
|
1340
|
+
frozen: ["ci", "clean-install", "ic", "install-clean", "isntall-clean"],
|
|
1341
|
+
remove: ["uninstall", "unlink", "remove", "rm", "r", "un"],
|
|
1342
|
+
update: ["update", "u", "up", "upgrade", "udpate"]
|
|
1343
|
+
})
|
|
1344
|
+
};
|
|
1345
|
+
var COMMANDS = {
|
|
1346
|
+
bun: { install: "install", frozen: "install --frozen-lockfile", add: "add", remove: "remove", update: "update" },
|
|
1347
|
+
pnpm: { install: "install", frozen: "install --frozen-lockfile", add: "add", remove: "remove", update: "update" },
|
|
1348
|
+
yarn: { install: "install", add: "add", remove: "remove" },
|
|
1349
|
+
npm: { install: "install", frozen: "ci", add: "install", remove: "uninstall", update: "update" }
|
|
1350
|
+
};
|
|
1351
|
+
var OPTIONS = {
|
|
1352
|
+
bun: { dev: "--dev", exact: "--exact" },
|
|
1353
|
+
pnpm: { dev: "--save-dev", exact: "--save-exact" },
|
|
1354
|
+
yarn: { dev: "--dev", exact: "--exact" },
|
|
1355
|
+
npm: { dev: "--save-dev", exact: "--save-exact" }
|
|
1356
|
+
};
|
|
1357
|
+
var OPTION_FLAGS = new Map([
|
|
1358
|
+
["-D", "dev"],
|
|
1359
|
+
["--save-dev", "dev"],
|
|
1360
|
+
["--dev", "dev"],
|
|
1361
|
+
["-E", "exact"],
|
|
1362
|
+
["--save-exact", "exact"],
|
|
1363
|
+
["--exact", "exact"]
|
|
1364
|
+
]);
|
|
1365
|
+
var DIR_FLAGS = new Set(["--cwd", "-C", "--dir", "--prefix"]);
|
|
1366
|
+
var VALUE_FLAGS2 = new Set([...DIR_FLAGS, "--filter", "-F"]);
|
|
1367
|
+
var PLAIN_WORD = /^[\w=@./:+-]+$/;
|
|
1368
|
+
function actions(byAction) {
|
|
1369
|
+
return new Map(Object.entries(byAction).flatMap(([action, names]) => names.map((name) => [name, action])));
|
|
1370
|
+
}
|
|
1371
|
+
function isManager(program) {
|
|
1372
|
+
return Object.hasOwn(ACTIONS, program);
|
|
1373
|
+
}
|
|
1374
|
+
function translate(from, to, subcommand, rest) {
|
|
1375
|
+
const packages = rest.filter((arg) => !arg.startsWith("-"));
|
|
1376
|
+
const flags = rest.filter((arg) => arg.startsWith("-"));
|
|
1377
|
+
const listed = ACTIONS[from].get(subcommand);
|
|
1378
|
+
const action = listed === "install" && packages.length > 0 ? "add" : listed;
|
|
1379
|
+
const command = action ? COMMANDS[to][action] : undefined;
|
|
1380
|
+
if (!action || command === undefined || !packages.every((name) => PLAIN_WORD.test(name)))
|
|
1381
|
+
return null;
|
|
1382
|
+
const options = [];
|
|
1383
|
+
for (const flag of flags) {
|
|
1384
|
+
const option = OPTION_FLAGS.get(flag) ?? (from === "bun" && flag === "-d" ? "dev" : undefined);
|
|
1385
|
+
if (option === undefined || action !== "add")
|
|
1386
|
+
return null;
|
|
1387
|
+
options.push(OPTIONS[to][option]);
|
|
1388
|
+
}
|
|
1389
|
+
return [to, command, ...options, ...packages].join(" ");
|
|
1390
|
+
}
|
|
1391
|
+
function installRequests(command, cwd, home) {
|
|
1392
|
+
const requests = [];
|
|
1393
|
+
for (const piece of splitCommand(command)) {
|
|
1394
|
+
const { program, args, assignments, isWrapped } = parseCommand(piece.command);
|
|
1395
|
+
if (program === "cd" && args[0] !== undefined) {
|
|
1396
|
+
cwd = resolvePath(cwd, args[0], home);
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (!isManager(program))
|
|
1400
|
+
continue;
|
|
1401
|
+
let dir = cwd;
|
|
1402
|
+
let subcommand = "";
|
|
1403
|
+
let rest = [];
|
|
1404
|
+
let hasLeadingFlag = false;
|
|
1405
|
+
for (let i = 0;i < args.length; i++) {
|
|
1406
|
+
const arg = args[i] ?? "";
|
|
1407
|
+
const [flag = "", value] = arg.split("=", 2);
|
|
1408
|
+
if (DIR_FLAGS.has(flag) && value !== undefined && value !== "") {
|
|
1409
|
+
dir = resolvePath(cwd, value, home);
|
|
1410
|
+
} else if (VALUE_FLAGS2.has(arg)) {
|
|
1411
|
+
if (DIR_FLAGS.has(arg) && args[i + 1] !== undefined)
|
|
1412
|
+
dir = resolvePath(cwd, args[i + 1] ?? "", home);
|
|
1413
|
+
i++;
|
|
1414
|
+
} else if (!arg.startsWith("-")) {
|
|
1415
|
+
subcommand = arg;
|
|
1416
|
+
rest = args.slice(i + 1);
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
hasLeadingFlag = true;
|
|
1420
|
+
}
|
|
1421
|
+
if (ACTIONS[program].has(subcommand))
|
|
1422
|
+
requests.push({ piece, program, subcommand, rest, dir, assignments, isRewritable: !hasLeadingFlag && !isWrapped && piece.command === piece.text });
|
|
1423
|
+
}
|
|
1424
|
+
return requests;
|
|
1425
|
+
}
|
|
1426
|
+
function mismatchFor(request, pinned) {
|
|
1427
|
+
const { piece, program, subcommand, rest, isRewritable, assignments } = request;
|
|
1428
|
+
if (!pinned || pinned.managers.has(program))
|
|
1429
|
+
return null;
|
|
1430
|
+
const expected = [...pinned.managers];
|
|
1431
|
+
const only = expected.length === 1 ? expected[0] : undefined;
|
|
1432
|
+
const canRewrite = only !== undefined && isRewritable && assignments.every((word) => PLAIN_WORD.test(word));
|
|
1433
|
+
const translated = canRewrite ? translate(program, only, subcommand, rest) : null;
|
|
1434
|
+
const leading = piece.text.slice(0, piece.text.length - piece.text.trimStart().length);
|
|
1435
|
+
const trailing = piece.text.slice(piece.text.trimEnd().length);
|
|
1436
|
+
return {
|
|
1437
|
+
piece,
|
|
1438
|
+
rewrite: translated === null ? null : `${leading}${[...assignments, translated].join(" ")}${trailing}`,
|
|
1439
|
+
reason: `${pinned.dir} is pinned to ${expected.join(" or ")} by its lockfile, so \`${`${program} ${subcommand}`.trim()}\` would install with the wrong package manager.`
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// src/git.ts
|
|
1444
|
+
var GLOBAL_VALUE_OPTIONS = new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"]);
|
|
1445
|
+
var READ_ONLY_STASH = new Set(["list", "show"]);
|
|
1446
|
+
var WHOLE_TREE = new Set(["-A", "--all", "-u", "--update", "--no-ignore-removal", ".", "./", ":/", "*"]);
|
|
1447
|
+
var COMMIT_VALUE_SHORTS = new Set(["m", "F", "c", "C", "t"]);
|
|
1448
|
+
var COMMIT_VALUE_LONGS = new Set(["--message", "--file", "--reuse-message", "--reedit-message", "--template", "--author", "--date", "--fixup", "--squash", "--cleanup", "--trailer", "--pathspec-from-file"]);
|
|
1449
|
+
var SHORT_CLUSTER = /^-[A-Z]+$/i;
|
|
1450
|
+
var STAGES_ALL = /[Au]/;
|
|
1451
|
+
var ALL_TAGS = new Set(["--tags", "--follow-tags"]);
|
|
1452
|
+
var STAGE_BY_PATH = "Stage by path (`git add <path>...`, then `git status`): the tree carries unrelated work and peer sessions edit it live.";
|
|
1453
|
+
var STASH = "Never `git stash`: a pop with nothing saved applies someone else's old stash. To see how a test behaves without your change, run it at HEAD with `base -- <cmd>` (a temporary worktree, the working tree untouched), `--keep <path>` to carry the new test over, `--ref <rev>` for another commit.";
|
|
1454
|
+
var ADD_ALL = `\`git add\` over the whole tree is refused. ${STAGE_BY_PATH}`;
|
|
1455
|
+
var COMMIT_ALL = `\`git commit -a\` stages the whole tree and is refused. ${STAGE_BY_PATH}`;
|
|
1456
|
+
var PUSH = "`git push` runs only when the user asked for a push in their latest prompt: say the commit is ready and let them ask.";
|
|
1457
|
+
var TAGS_APART = "A tag goes out with its branch in one `git push --atomic origin <branch> <tag>`, never a separate `git push --tags`.";
|
|
1458
|
+
function subcommandOf(args) {
|
|
1459
|
+
for (let i = 0;i < args.length; i++) {
|
|
1460
|
+
const arg = args[i] ?? "";
|
|
1461
|
+
if (GLOBAL_VALUE_OPTIONS.has(arg))
|
|
1462
|
+
i++;
|
|
1463
|
+
else if (!arg.startsWith("-"))
|
|
1464
|
+
return { name: arg, rest: args.slice(i + 1) };
|
|
1465
|
+
}
|
|
1466
|
+
return { name: "", rest: [] };
|
|
1467
|
+
}
|
|
1468
|
+
function commitsAll(args) {
|
|
1469
|
+
for (let i = 0;i < args.length; i++) {
|
|
1470
|
+
const arg = args[i] ?? "";
|
|
1471
|
+
if (arg === "--")
|
|
1472
|
+
return false;
|
|
1473
|
+
if (arg === "--all")
|
|
1474
|
+
return true;
|
|
1475
|
+
if (COMMIT_VALUE_LONGS.has(arg)) {
|
|
1476
|
+
i++;
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (!SHORT_CLUSTER.test(arg))
|
|
1480
|
+
continue;
|
|
1481
|
+
for (const [index, flag] of [...arg.slice(1)].entries()) {
|
|
1482
|
+
if (flag === "a")
|
|
1483
|
+
return true;
|
|
1484
|
+
if (!COMMIT_VALUE_SHORTS.has(flag))
|
|
1485
|
+
continue;
|
|
1486
|
+
if (index === arg.length - 2)
|
|
1487
|
+
i++;
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return false;
|
|
1492
|
+
}
|
|
1493
|
+
function gitViolation(command, isPushAsked = false) {
|
|
1494
|
+
for (const piece of splitCommand(command)) {
|
|
1495
|
+
const { program, args } = parseCommand(piece.command);
|
|
1496
|
+
if (program !== "git")
|
|
1497
|
+
continue;
|
|
1498
|
+
const { name, rest } = subcommandOf(args);
|
|
1499
|
+
if (name === "stash" && !READ_ONLY_STASH.has(rest[0] ?? ""))
|
|
1500
|
+
return STASH;
|
|
1501
|
+
if (name === "add" && rest.some((arg) => WHOLE_TREE.has(arg) || SHORT_CLUSTER.test(arg) && STAGES_ALL.test(arg)))
|
|
1502
|
+
return ADD_ALL;
|
|
1503
|
+
if (name === "commit" && commitsAll(rest))
|
|
1504
|
+
return COMMIT_ALL;
|
|
1505
|
+
if (name === "push" && !isPushAsked)
|
|
1506
|
+
return PUSH;
|
|
1507
|
+
if (name === "push" && rest.some((arg) => ALL_TAGS.has(arg)) && !rest.includes("--atomic"))
|
|
1508
|
+
return TAGS_APART;
|
|
1509
|
+
}
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/commit.ts
|
|
1514
|
+
var REUSES = new Set(["c", "C", "--reuse-message", "--reedit-message", "--fixup", "--squash"]);
|
|
1515
|
+
var TRAILER_FLAGS = new Set(["--signoff", "--trailer"]);
|
|
1516
|
+
var LONG_VALUE = /=(.*)/s;
|
|
1517
|
+
var CAT_OPENER = /^\$\(cat <<-?[ \t]*(['"]?)([\w-]+)\1/;
|
|
1518
|
+
var HEREDOC_OPENER = /<<-?[ \t]*(['"]?)([\w-]+)\1/;
|
|
1519
|
+
var CAT_CLOSER = /^\s*(?:\)\s*)?$/;
|
|
1520
|
+
var EXPANDS = /\$[({\w]|`/;
|
|
1521
|
+
function heredocBody(text, opener) {
|
|
1522
|
+
const found = opener.exec(text);
|
|
1523
|
+
const newline = found === null ? -1 : text.indexOf(`
|
|
1524
|
+
`, found.index + found[0].length);
|
|
1525
|
+
if (found === null || newline === -1)
|
|
1526
|
+
return;
|
|
1527
|
+
const lines = text.slice(newline + 1).split(`
|
|
1528
|
+
`);
|
|
1529
|
+
const end = lines.findIndex((line) => line.trim() === found[2] || line.trim() === `${found[2]})`);
|
|
1530
|
+
return end === -1 ? undefined : { body: lines.slice(0, end).join(`
|
|
1531
|
+
`), rest: lines.slice(end + 1).join(`
|
|
1532
|
+
`) };
|
|
1533
|
+
}
|
|
1534
|
+
function sourceOf(args, text) {
|
|
1535
|
+
const source = { parts: [], file: null, stdin: null, isReused: false, hasTrailerFlag: false };
|
|
1536
|
+
const take = (option, value) => {
|
|
1537
|
+
if (option === "m" || option === "--message")
|
|
1538
|
+
source.parts.push(value);
|
|
1539
|
+
if (option === "F" || option === "--file")
|
|
1540
|
+
source.file = value;
|
|
1541
|
+
if (REUSES.has(option))
|
|
1542
|
+
source.isReused = true;
|
|
1543
|
+
if (option === "--trailer")
|
|
1544
|
+
source.hasTrailerFlag = true;
|
|
1545
|
+
};
|
|
1546
|
+
for (let i = 0;i < args.length; i++) {
|
|
1547
|
+
const arg = args[i] ?? "";
|
|
1548
|
+
if (arg === "--")
|
|
1549
|
+
break;
|
|
1550
|
+
if (arg.startsWith("--")) {
|
|
1551
|
+
const [option = "", value] = arg.split(LONG_VALUE);
|
|
1552
|
+
if (TRAILER_FLAGS.has(option))
|
|
1553
|
+
source.hasTrailerFlag = true;
|
|
1554
|
+
if (REUSES.has(option))
|
|
1555
|
+
source.isReused = true;
|
|
1556
|
+
if (value !== undefined)
|
|
1557
|
+
take(option, value);
|
|
1558
|
+
else if (COMMIT_VALUE_LONGS.has(option))
|
|
1559
|
+
take(option, args[++i] ?? "");
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
if (!SHORT_CLUSTER.test(arg))
|
|
1563
|
+
continue;
|
|
1564
|
+
for (const [index, flag] of [...arg.slice(1)].entries()) {
|
|
1565
|
+
if (flag === "s")
|
|
1566
|
+
source.hasTrailerFlag = true;
|
|
1567
|
+
if (!COMMIT_VALUE_SHORTS.has(flag))
|
|
1568
|
+
continue;
|
|
1569
|
+
take(flag, index === arg.length - 2 ? args[++i] ?? "" : arg.slice(index + 2));
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
if (source.file === "-")
|
|
1574
|
+
source.stdin = heredocBody(text, HEREDOC_OPENER)?.body ?? null;
|
|
1575
|
+
return source;
|
|
1576
|
+
}
|
|
1577
|
+
function commitSources(command) {
|
|
1578
|
+
const sources = [];
|
|
1579
|
+
for (const piece of splitCommand(command)) {
|
|
1580
|
+
const { program, args } = parseCommand(piece.command);
|
|
1581
|
+
const { name, rest } = program === "git" ? subcommandOf(args) : { name: "", rest: [] };
|
|
1582
|
+
if (name === "commit")
|
|
1583
|
+
sources.push(sourceOf(rest, piece.text));
|
|
1584
|
+
}
|
|
1585
|
+
return sources;
|
|
1586
|
+
}
|
|
1587
|
+
function inlineMessage(parts) {
|
|
1588
|
+
const texts = [];
|
|
1589
|
+
for (const part of parts) {
|
|
1590
|
+
const found = heredocBody(part, CAT_OPENER);
|
|
1591
|
+
const body = found !== undefined && CAT_CLOSER.test(found.rest) ? found.body : undefined;
|
|
1592
|
+
if (body === undefined && EXPANDS.test(part))
|
|
1593
|
+
return null;
|
|
1594
|
+
texts.push(body ?? part);
|
|
1595
|
+
}
|
|
1596
|
+
return texts.join(`
|
|
1597
|
+
|
|
1598
|
+
`);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// src/remote.ts
|
|
1602
|
+
var SHELL_PROGRAMS = new Set(["ssh", "scp", "sftp", "mosh", "autossh"]);
|
|
1603
|
+
var RSYNC_REMOTE = /^(?:[\w.-]+@)?[\w.-]{2,}:/;
|
|
1604
|
+
var SSH_URL = /^ssh:\/\//;
|
|
1605
|
+
var REMOTE = "Remote servers are off limits (no SSH, no deploys, no remote commands). Hand the user the exact command to run themselves.";
|
|
1606
|
+
function isRemote(text) {
|
|
1607
|
+
const { program, args, assignments } = parseCommand(text);
|
|
1608
|
+
if (SHELL_PROGRAMS.has(program))
|
|
1609
|
+
return true;
|
|
1610
|
+
if (program === "rsync")
|
|
1611
|
+
return args.some((arg) => !arg.startsWith("-") && RSYNC_REMOTE.test(arg));
|
|
1612
|
+
if (assignments.some((assignment) => assignment.startsWith("DOCKER_HOST=") && SSH_URL.test(assignment.slice("DOCKER_HOST=".length))))
|
|
1613
|
+
return true;
|
|
1614
|
+
return program === "docker" && tokenize(text).some((word) => SSH_URL.test(word));
|
|
1615
|
+
}
|
|
1616
|
+
function remoteViolation(command) {
|
|
1617
|
+
return splitCommand(command).some((piece) => isRemote(piece.command)) ? REMOTE : null;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/check.ts
|
|
1621
|
+
function absolute(path, cwd) {
|
|
1622
|
+
return path.startsWith("/") ? path : `${cwd}/${path}`;
|
|
1623
|
+
}
|
|
1624
|
+
function commitViolation(command, { cwd, read }) {
|
|
1625
|
+
for (const source of commitSources(command)) {
|
|
1626
|
+
if (source.hasTrailerFlag)
|
|
1627
|
+
return TRAILERS;
|
|
1628
|
+
if (source.isReused || source.file?.startsWith("/dev/") === true)
|
|
1629
|
+
continue;
|
|
1630
|
+
const { file } = source;
|
|
1631
|
+
let message = source.stdin;
|
|
1632
|
+
if (file === null)
|
|
1633
|
+
message = inlineMessage(source.parts);
|
|
1634
|
+
else if (file !== "-")
|
|
1635
|
+
message = read(absolute(file, cwd));
|
|
1636
|
+
const violation = message === null ? null : commitMessageViolation(message);
|
|
1637
|
+
if (violation !== null)
|
|
1638
|
+
return violation;
|
|
1639
|
+
}
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
function pinnedManagers(start, exists) {
|
|
1643
|
+
for (let dir = start;; dir = parentDir(dir)) {
|
|
1644
|
+
const managers = new Set(LOCKFILES.filter(([file]) => exists(`${dir}/${file}`)).map(([, manager]) => manager));
|
|
1645
|
+
if (managers.size > 0)
|
|
1646
|
+
return { dir, managers };
|
|
1647
|
+
if (dir === "/")
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function commandsRun(command) {
|
|
1652
|
+
return [command, ...substitutions(command).flatMap(commandsRun)];
|
|
1653
|
+
}
|
|
1654
|
+
function mismatches(command, { cwd, home, exists }) {
|
|
1655
|
+
return installRequests(command, cwd, home).flatMap((request) => {
|
|
1656
|
+
const mismatch = mismatchFor(request, pinnedManagers(request.dir, exists));
|
|
1657
|
+
return mismatch === null ? [] : [mismatch];
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
async function checkCommand(command, context) {
|
|
1661
|
+
const violations = [];
|
|
1662
|
+
const notes = [];
|
|
1663
|
+
const add = (rule, message) => {
|
|
1664
|
+
if (message !== null)
|
|
1665
|
+
violations.push({ rule, message });
|
|
1666
|
+
};
|
|
1667
|
+
const run = commandsRun(command);
|
|
1668
|
+
const first = (messages) => messages.find((message) => message !== null) ?? null;
|
|
1669
|
+
add("remote", first(run.map(remoteViolation)));
|
|
1670
|
+
add("git", first(run.map((each) => gitViolation(each, context.isPushAsked))));
|
|
1671
|
+
add("commit", commitViolation(command, context));
|
|
1672
|
+
add("sql", first(await Promise.all(run.map(async (each) => sqlViolation(each, async (path) => Promise.resolve(context.read(absolute(path, context.cwd))))))));
|
|
1673
|
+
add("env", touchesEnv(command) ? USE_EV : null);
|
|
1674
|
+
const found = mismatches(command, context);
|
|
1675
|
+
const refused = found.find((mismatch) => mismatch.rewrite === null || !context.canRewrite);
|
|
1676
|
+
const rewrite = refused?.rewrite ?? null;
|
|
1677
|
+
const route = rewrite === null ? "Run it with the pinned manager instead." : `Run \`${rewrite.trim()}\` instead.`;
|
|
1678
|
+
add("package-manager", refused === undefined ? null : `${refused.reason} ${route}`);
|
|
1679
|
+
let rewritten = command;
|
|
1680
|
+
if (refused === undefined && found.length > 0) {
|
|
1681
|
+
rewritten = replacePieces(command, new Map(found.map((mismatch) => [mismatch.piece, mismatch.rewrite ?? ""])));
|
|
1682
|
+
notes.push(...found.map((mismatch) => `${mismatch.reason} It ran as \`${mismatch.rewrite?.trim()}\` instead.`));
|
|
1683
|
+
}
|
|
1684
|
+
if (context.canRewrite && context.canWrap && shouldWrap(rewritten))
|
|
1685
|
+
rewritten = wrap(rewritten);
|
|
1686
|
+
return { violations, command: violations.length > 0 ? command : rewritten, notes: violations.length > 0 ? [] : notes, packages: addedPackages(command) };
|
|
1687
|
+
}
|
|
1688
|
+
function checkPath(path) {
|
|
1689
|
+
return isEnvFile(path) ? { rule: "env", message: USE_EV } : null;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// src/bins.ts
|
|
1693
|
+
var BINS = {
|
|
1694
|
+
check: "`check`: runs the repo's check, typecheck, lint and test steps and prints only what failed, each full log saved to a file; validate a change with it rather than chaining the scripts (`--only lint,test`, `--list`).",
|
|
1695
|
+
fix: "`fix`: runs the repo's fixers and formatters (its fix, lint:fix and format scripts, just fmt, go fmt, cargo fmt, ruff) and lists the files they changed.",
|
|
1696
|
+
db: '`db "<sql>"`: one read-only statement against the Postgres in DATABASE_URL (`-e KEY` for another) or, without one, the compose project\'s running Postgres service (`-s <service>`, `-d <database>`), credentials never shown, a write printed for the user to run; `db tables`, `db describe <table>`, `db dbs`.',
|
|
1697
|
+
ev: "`ev`: .env files read and edited with values masked (`ev list`, `ev get KEY`, `ev grep <regex>`, `ev show <rev>:<file>`, `ev set`, `ev diff`, `ev missing`); `ev audit [dir]` finds env files others can read or git tracks, and compose files writing secrets out.",
|
|
1698
|
+
redact: "`redact`: masks secrets in piped output, `<cmd> | redact`.",
|
|
1699
|
+
pkg: "`pkg <name>[@version]`: checks an npm package before it is added: whether the name exists or is a near miss of a popular one, the newest version at least two weeks old, licence, install scripts; `bun add`, `npm install`, `pnpm add` and `yarn add` run it first and are refused on what it flags.",
|
|
1700
|
+
stack: "`stack port <name>`: the port a service name always hashes to, for every host port a compose file publishes; `stack audit [dir]` cross-checks release.config.ts against its compose files.",
|
|
1701
|
+
ft: "`ft`: Forgejo CI runs, logs and waits, releases, issue checklists and claims, in a repo whose origin is on Forgejo.",
|
|
1702
|
+
sub: "`sub --from <text> --to <text> <file>...`: replaces text, writing nothing unless each match count is as expected (1, `-n N`, `--all`), `--regex`, `--pairs <json|->` for several in order, `--dry-run --diff`; use it instead of sed -i, a bun -e or python editor.",
|
|
1703
|
+
base: "`base [--ref <rev>] [--keep <path>...] -- <cmd>`: runs a command at HEAD (or a ref) in a temporary worktree, removed afterwards, ignored env files and node_modules/vendor linked; use it instead of git stash to see whether a test fails without your change (`--keep` the new test).",
|
|
1704
|
+
gst: "`gst [--log N]`: branch, upstream, changed counts and HEAD in one line, then the changed paths by staged, unstaged and untracked; use it instead of `git status --short` and `git log --oneline`.",
|
|
1705
|
+
gc: "`gc <title> [-m <line>]... [--ticket <line>]... -- <path>...`: commits exactly those paths, refusing with nothing staged when the message breaks a commit rule, a path has no change or other files are already staged; use it instead of chaining git add, status and commit.",
|
|
1706
|
+
waitfor: "`waitfor --file <log> --until <regex> [--fail <regex>]`, `--port <n>`, `--url <url>` (`-t 5m`): waits until the line shows, the port listens or the URL answers 2xx, exits 1 on a --fail line with the lines around it, 2 on a timeout with the last lines; use it instead of sleep and tail or polling loops.",
|
|
1707
|
+
gl: "`gl ci [ref]` waits for the GitLab pipeline covering a commit and prints each failed job's tests (exit 0/1/2), `gl runs`, `gl logs <pipeline> [-j job] [--full]`; use it instead of `glab ci` or `glab api .../pipelines/<id>/jobs` polled in a sleep loop.",
|
|
1708
|
+
out: "`out -- <cmd>`: folds long output, keeping the errors and saving the full log; run noisy builds, installs, tests and linters through it."
|
|
1709
|
+
};
|
|
1710
|
+
function binsSection(installed) {
|
|
1711
|
+
const lines = Object.entries(BINS).filter(([bin]) => installed.has(bin)).map(([, line]) => `- ${line}`);
|
|
1712
|
+
return lines.length === 0 ? undefined : `# Agent tools
|
|
1713
|
+
|
|
1714
|
+
CLIs installed on this machine for you to use over hand-rolled equivalents, \`<bin> --help\` for the options:
|
|
1715
|
+
${lines.join(`
|
|
1716
|
+
`)}`;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/hook.ts
|
|
1720
|
+
var EVENTS = ["pre-tool-use", "user-prompt-submit", "session-start"];
|
|
1721
|
+
var PUSH_WORDS = /\b(?:push|pousse)/i;
|
|
1722
|
+
var SESSION_ID = /^[\w-]+$/;
|
|
1723
|
+
var FILE_TOOLS = new Set(["Read", "Edit", "Write"]);
|
|
1724
|
+
function pushFile(sessionId, { stateDir }) {
|
|
1725
|
+
return sessionId !== undefined && SESSION_ID.test(sessionId) ? `${stateDir}/push-asked/${sessionId}` : null;
|
|
1726
|
+
}
|
|
1727
|
+
function deny(violations) {
|
|
1728
|
+
if (violations.length === 0)
|
|
1729
|
+
return null;
|
|
1730
|
+
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: violations.map((each) => each.message).join(`
|
|
1731
|
+
|
|
1732
|
+
`) } };
|
|
1733
|
+
}
|
|
1734
|
+
async function bashCall(command, input, machine) {
|
|
1735
|
+
const file = pushFile(input.session_id, machine);
|
|
1736
|
+
const cwd = input.cwd ?? machine.home;
|
|
1737
|
+
const verdict = await checkCommand(command, {
|
|
1738
|
+
cwd,
|
|
1739
|
+
home: machine.home,
|
|
1740
|
+
isPushAsked: file !== null && machine.exists(file),
|
|
1741
|
+
canRewrite: false,
|
|
1742
|
+
canWrap: false,
|
|
1743
|
+
read: machine.read,
|
|
1744
|
+
exists: machine.exists
|
|
1745
|
+
});
|
|
1746
|
+
if (verdict.violations.length > 0 || verdict.packages.length === 0 || !machine.has("pkg"))
|
|
1747
|
+
return deny(verdict.violations);
|
|
1748
|
+
const checked = await machine.run(["pkg", ...verdict.packages], cwd);
|
|
1749
|
+
return checked.code === 1 ? deny([{ rule: "pkg", message: `pkg refused what this adds, so it was not run:
|
|
1750
|
+
${checked.text.trim()}` }]) : null;
|
|
1751
|
+
}
|
|
1752
|
+
function preToolUse(input, machine) {
|
|
1753
|
+
const { tool_name: tool, tool_input: given = {} } = input;
|
|
1754
|
+
if (tool === "Bash" && typeof given.command === "string")
|
|
1755
|
+
return bashCall(given.command, input, machine);
|
|
1756
|
+
if (tool !== undefined && FILE_TOOLS.has(tool) && typeof given.file_path === "string")
|
|
1757
|
+
return deny([checkPath(given.file_path)].filter((each) => each !== null));
|
|
1758
|
+
return null;
|
|
1759
|
+
}
|
|
1760
|
+
function userPromptSubmit(input, machine) {
|
|
1761
|
+
const file = pushFile(input.session_id, machine);
|
|
1762
|
+
if (file !== null && PUSH_WORDS.test(input.prompt ?? ""))
|
|
1763
|
+
machine.write(file, "");
|
|
1764
|
+
else if (file !== null)
|
|
1765
|
+
machine.remove(file);
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
function sessionStart(machine) {
|
|
1769
|
+
const text = binsSection(new Set(Object.keys(BINS).filter(machine.has)));
|
|
1770
|
+
return text === undefined ? null : { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } };
|
|
1771
|
+
}
|
|
1772
|
+
async function hook(event, input, machine) {
|
|
1773
|
+
if (event === "pre-tool-use")
|
|
1774
|
+
return preToolUse(input, machine);
|
|
1775
|
+
return event === "user-prompt-submit" ? userPromptSubmit(input, machine) : sessionStart(machine);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// src/cli.ts
|
|
1779
|
+
var checkUsage = `guard check [options] [<command> | -]
|
|
1780
|
+
|
|
1781
|
+
Checks a shell command against the rules: remote machines, git (stash, whole-tree add and commit, push, tags), commit messages, SQL writes, env files and package managers.
|
|
1782
|
+
Prints each broken rule with the route to take instead and exits 1, prints nothing and exits 0 when the command is clean.
|
|
1783
|
+
The command is read from stdin with - or when none is given; a rule may rewrite it instead (the pinned package manager, a noisy run through out), printed as \`runs as: <command>\`.`;
|
|
1784
|
+
var checkOptions = {
|
|
1785
|
+
json: jsonOption("{violations, command, notes, packages} for a hook"),
|
|
1786
|
+
cwd: { type: "string", description: "the directory the command runs in (default: here)" },
|
|
1787
|
+
"push-asked": { type: "boolean", description: "the user asked for a push, so git push may run" },
|
|
1788
|
+
"no-wrap": { type: "boolean", description: "never route a noisy command through out" }
|
|
1789
|
+
};
|
|
1790
|
+
var pathUsage = `guard path [--json] <path>...
|
|
1791
|
+
|
|
1792
|
+
Checks the paths a file tool would read, edit or write: env files are refused.`;
|
|
1793
|
+
var pathOptions = {
|
|
1794
|
+
json: jsonOption("{violations} for a hook")
|
|
1795
|
+
};
|
|
1796
|
+
var hookUsage = `guard hook <${EVENTS.join("|")}>
|
|
1797
|
+
|
|
1798
|
+
Answers a Claude Code hook: reads the event JSON on stdin and prints the reply JSON, nothing when the call goes ahead untouched.
|
|
1799
|
+
pre-tool-use refuses a Bash call breaking a rule, an install pkg refuses, and a Read, Edit or Write of an env file; user-prompt-submit remembers whether the prompt asked for a push; session-start lists the installed agent-tools bins.
|
|
1800
|
+
A failure exits 1, which Claude Code reports without blocking the call.`;
|
|
1801
|
+
function readText(path) {
|
|
1802
|
+
try {
|
|
1803
|
+
return readFileSync(path, "utf8");
|
|
1804
|
+
} catch {
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
function report(violations) {
|
|
1809
|
+
for (const { rule, message } of violations)
|
|
1810
|
+
console.log(`${rule}: ${message}`);
|
|
1811
|
+
}
|
|
1812
|
+
async function check(args) {
|
|
1813
|
+
const parsed = parse(args, checkUsage, checkOptions);
|
|
1814
|
+
const { values, positionals } = parsed;
|
|
1815
|
+
if (positionals.length > 1)
|
|
1816
|
+
fail(`give the command as one argument, quoted
|
|
1817
|
+
|
|
1818
|
+
${checkUsage}`);
|
|
1819
|
+
const given = positionals[0] ?? "-";
|
|
1820
|
+
const command = given === "-" ? await readInput("-", { noStdin: "give the command, or pipe it on stdin" }) : given;
|
|
1821
|
+
const context = {
|
|
1822
|
+
cwd: values.cwd ?? process6.cwd(),
|
|
1823
|
+
home: homedir(),
|
|
1824
|
+
isPushAsked: values["push-asked"] === true,
|
|
1825
|
+
canRewrite: true,
|
|
1826
|
+
canWrap: values["no-wrap"] !== true && Bun.which("out") !== null,
|
|
1827
|
+
read: readText,
|
|
1828
|
+
exists: existsSync
|
|
1829
|
+
};
|
|
1830
|
+
const verdict = await checkCommand(command, context);
|
|
1831
|
+
if (values.json === true)
|
|
1832
|
+
console.log(JSON.stringify(verdict));
|
|
1833
|
+
else
|
|
1834
|
+
report(verdict.violations);
|
|
1835
|
+
if (values.json !== true && verdict.command !== command)
|
|
1836
|
+
console.log(`runs as: ${verdict.command}${verdict.notes.map((note) => `
|
|
1837
|
+
${note}`).join("")}`);
|
|
1838
|
+
return verdict.violations.length > 0 ? 1 : 0;
|
|
1839
|
+
}
|
|
1840
|
+
function path(args) {
|
|
1841
|
+
const parsed = parse(args, pathUsage, pathOptions);
|
|
1842
|
+
if (parsed.positionals.length === 0)
|
|
1843
|
+
fail(`give at least one path
|
|
1844
|
+
|
|
1845
|
+
${pathUsage}`);
|
|
1846
|
+
const violations = parsed.positionals.flatMap((each) => checkPath(each) ?? []);
|
|
1847
|
+
if (parsed.values.json === true)
|
|
1848
|
+
console.log(JSON.stringify({ violations }));
|
|
1849
|
+
else
|
|
1850
|
+
report(violations);
|
|
1851
|
+
return violations.length > 0 ? 1 : 0;
|
|
1852
|
+
}
|
|
1853
|
+
function machine() {
|
|
1854
|
+
const home = homedir();
|
|
1855
|
+
return {
|
|
1856
|
+
home,
|
|
1857
|
+
stateDir: `${process6.env.XDG_STATE_HOME ?? `${home}/.local/state`}/guard`,
|
|
1858
|
+
has: (bin) => Bun.which(bin) !== null,
|
|
1859
|
+
run: async (argv, cwd) => runCommand(argv, { cwd }),
|
|
1860
|
+
read: readText,
|
|
1861
|
+
exists: existsSync,
|
|
1862
|
+
write: (path, text) => {
|
|
1863
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1864
|
+
writeFileSync(path, text);
|
|
1865
|
+
},
|
|
1866
|
+
remove: (path) => rmSync(path, { force: true })
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
async function hookCommand(args) {
|
|
1870
|
+
const parsed = parse(args, hookUsage, {});
|
|
1871
|
+
const event = parsed.positionals[0] ?? "";
|
|
1872
|
+
if (!EVENTS.includes(event))
|
|
1873
|
+
fail(`unknown event ${event}
|
|
1874
|
+
|
|
1875
|
+
${hookUsage}`, 1);
|
|
1876
|
+
const reply = await hook(event, JSON.parse(await Bun.stdin.text()), machine());
|
|
1877
|
+
if (reply !== null)
|
|
1878
|
+
console.log(JSON.stringify(reply));
|
|
1879
|
+
return 0;
|
|
1880
|
+
}
|
|
1881
|
+
process6.exitCode = await runCli({
|
|
1882
|
+
name: "guard",
|
|
1883
|
+
version,
|
|
1884
|
+
about: "the safety rules a Claude Code Bash call is held to, each refusal naming the route",
|
|
1885
|
+
commands: {
|
|
1886
|
+
check: { run: check, summary: "check a shell command, printing each broken rule with its route" },
|
|
1887
|
+
path: { run: async (args) => Promise.resolve(path(args)), summary: "check a path a file tool would open" },
|
|
1888
|
+
hook: { run: hookCommand, summary: "answer a Claude Code hook event, JSON in and out" }
|
|
1889
|
+
},
|
|
1890
|
+
defaultCommand: "check"
|
|
1891
|
+
});
|