@kaynooo/env-tools 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -2
- package/dist/ev.js +1542 -0
- package/dist/redact.js +1055 -0
- package/package.json +37 -4
- package/src/dotenv.ts +152 -0
package/dist/ev.js
ADDED
|
@@ -0,0 +1,1542 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/ev.ts
|
|
5
|
+
import process8 from "process";
|
|
6
|
+
|
|
7
|
+
// ../kit/src/args.ts
|
|
8
|
+
import process4 from "process";
|
|
9
|
+
import { parseArgs } from "util";
|
|
10
|
+
|
|
11
|
+
// ../kit/src/format.ts
|
|
12
|
+
import process3 from "process";
|
|
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
|
+
function truncate(text, width) {
|
|
300
|
+
return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
301
|
+
}
|
|
302
|
+
var LINE_BREAKS = /\s*\n\s*/g;
|
|
303
|
+
var ESCAPE = String.fromCharCode(27);
|
|
304
|
+
function textWidth(text) {
|
|
305
|
+
return Bun.stringWidth(text);
|
|
306
|
+
}
|
|
307
|
+
function table(rows, { cellWidth, header = false, align = [], fit } = {}) {
|
|
308
|
+
const cells = cellWidth === undefined ? rows : rows.map((row) => row.map((cell) => truncate(cell.replaceAll(LINE_BREAKS, " "), cellWidth)));
|
|
309
|
+
const widths = [];
|
|
310
|
+
for (const row of cells) {
|
|
311
|
+
for (const [index, cell] of row.entries())
|
|
312
|
+
widths[index] = Math.max(widths[index] ?? 0, textWidth(cell));
|
|
313
|
+
}
|
|
314
|
+
const shown = fit === undefined ? cells : narrow(cells, widths, fit);
|
|
315
|
+
const colors = paint(process3.stdout);
|
|
316
|
+
const lines = shown.map((row) => row.map((cell, index) => {
|
|
317
|
+
const width = widths[index] ?? 0;
|
|
318
|
+
const gap = " ".repeat(Math.max(0, width - textWidth(cell)));
|
|
319
|
+
if (align[index] === "right")
|
|
320
|
+
return `${gap}${cell}`;
|
|
321
|
+
return index === row.length - 1 ? cell : `${cell}${gap}`;
|
|
322
|
+
}).join(" ").trimEnd());
|
|
323
|
+
return lines.map((line, index) => header && index === 0 ? colors.bold(line) : line).join(`
|
|
324
|
+
`);
|
|
325
|
+
}
|
|
326
|
+
function narrow(rows, widths, limit) {
|
|
327
|
+
const used = () => widths.reduce((sum, width) => sum + width, 0) + 2 * Math.max(0, widths.length - 1);
|
|
328
|
+
const cuttable = (index) => rows.every((row) => !(row[index] ?? "").includes(ESCAPE));
|
|
329
|
+
const floor = 8;
|
|
330
|
+
while (used() > limit) {
|
|
331
|
+
let widest = -1;
|
|
332
|
+
for (const [index, width] of widths.entries()) {
|
|
333
|
+
if (cuttable(index) && width > floor && width > (widths[widest] ?? 0))
|
|
334
|
+
widest = index;
|
|
335
|
+
}
|
|
336
|
+
if (widest === -1)
|
|
337
|
+
break;
|
|
338
|
+
widths[widest] = Math.max(floor, (widths[widest] ?? 0) - (used() - limit));
|
|
339
|
+
}
|
|
340
|
+
return rows.map((row) => row.map((cell, index) => textWidth(cell) > (widths[index] ?? 0) ? truncate(cell, widths[index] ?? 0) : cell));
|
|
341
|
+
}
|
|
342
|
+
function printJson(value) {
|
|
343
|
+
console.log(JSON.stringify(value, undefined, 2));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ../kit/src/args.ts
|
|
347
|
+
function jsonOption(holds) {
|
|
348
|
+
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
class Exit extends Error {
|
|
352
|
+
code;
|
|
353
|
+
hint;
|
|
354
|
+
constructor(message, code, hint) {
|
|
355
|
+
super(message);
|
|
356
|
+
this.code = code;
|
|
357
|
+
this.hint = hint;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function fail(message, code = 2, hint) {
|
|
361
|
+
throw new Exit(message, code, hint);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
class UsageError extends Exit {
|
|
365
|
+
constructor(message) {
|
|
366
|
+
super(message, 2);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function badUsage(message) {
|
|
370
|
+
throw new UsageError(message);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
class HelpShown extends Exit {
|
|
374
|
+
constructor() {
|
|
375
|
+
super("help shown", 0);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
class Completing {
|
|
380
|
+
options;
|
|
381
|
+
constructor(options) {
|
|
382
|
+
this.options = options;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
var completion = { active: false };
|
|
386
|
+
var OPTIONAL = /\[[^\]]+\]/g;
|
|
387
|
+
var USAGE_PREFIX = /^usage:\s*/i;
|
|
388
|
+
var FULL_STOP = /\.$/;
|
|
389
|
+
function usageLine(usage, colors = paint(process4.stdout)) {
|
|
390
|
+
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
391
|
+
}
|
|
392
|
+
function helpText(usage, options, colors = paint(process4.stdout), { examples = [], replaces } = {}) {
|
|
393
|
+
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
394
|
+
`);
|
|
395
|
+
}
|
|
396
|
+
function replacesRows(replaces, colors = paint(process4.stdout)) {
|
|
397
|
+
return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
|
|
398
|
+
}
|
|
399
|
+
function wantsHelpJson(env = process4.env) {
|
|
400
|
+
return env.KIT_HELP === "json";
|
|
401
|
+
}
|
|
402
|
+
function description(usage, { examples = [], replaces } = {}) {
|
|
403
|
+
const lines = usage.replace(USAGE_PREFIX, "").split(`
|
|
404
|
+
`);
|
|
405
|
+
const first = lines[0];
|
|
406
|
+
const colon = first.indexOf(": ");
|
|
407
|
+
const prose = lines[lines.indexOf("") + 1] ?? "";
|
|
408
|
+
const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
|
|
409
|
+
return {
|
|
410
|
+
synopsis: colon === -1 ? first : first.slice(0, colon),
|
|
411
|
+
summary: colon === -1 ? fallback : first.slice(colon + 2),
|
|
412
|
+
...replaces === undefined ? {} : { replaces },
|
|
413
|
+
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function exampleRows(examples, colors = paint(process4.stdout)) {
|
|
417
|
+
if (examples.length === 0)
|
|
418
|
+
return [];
|
|
419
|
+
return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
|
|
420
|
+
}
|
|
421
|
+
function optionRows(options, colors = paint(process4.stdout)) {
|
|
422
|
+
const rows = Object.entries(options).map(([name, option]) => {
|
|
423
|
+
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
424
|
+
const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
|
|
425
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description };
|
|
426
|
+
});
|
|
427
|
+
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
428
|
+
return rows.map((row) => ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
429
|
+
}
|
|
430
|
+
var OUTPUT_OPTIONS = {
|
|
431
|
+
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
432
|
+
plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
|
|
433
|
+
};
|
|
434
|
+
function applyOutput(color, plain, env = process4.env) {
|
|
435
|
+
const when = plain === true ? "never" : color ?? "auto";
|
|
436
|
+
if (when === "auto")
|
|
437
|
+
return;
|
|
438
|
+
if (when !== "always" && when !== "never")
|
|
439
|
+
badUsage(`--color takes auto, always or never, not ${when}`);
|
|
440
|
+
if (when === "always") {
|
|
441
|
+
env.FORCE_COLOR = "1";
|
|
442
|
+
delete env.NO_COLOR;
|
|
443
|
+
} else {
|
|
444
|
+
env.NO_COLOR = "1";
|
|
445
|
+
delete env.FORCE_COLOR;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function parse(args, usage, options, settings = {}) {
|
|
449
|
+
const output = Object.fromEntries(Object.entries(OUTPUT_OPTIONS).filter(([name]) => !(name in options)));
|
|
450
|
+
const all = { ...options, ...output, help: { type: "boolean", short: "h", description: "show this help" } };
|
|
451
|
+
if (completion.active)
|
|
452
|
+
throw new Completing(all);
|
|
453
|
+
const separator = settings.rest === true ? args.indexOf("--") : -1;
|
|
454
|
+
const own = separator === -1 ? args : args.slice(0, separator);
|
|
455
|
+
const rest = separator === -1 ? undefined : args.slice(separator + 1);
|
|
456
|
+
let parsed;
|
|
457
|
+
try {
|
|
458
|
+
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
459
|
+
} catch (error) {
|
|
460
|
+
badUsage(parseError(error));
|
|
461
|
+
}
|
|
462
|
+
const values = parsed.values;
|
|
463
|
+
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
464
|
+
if (values.help === true) {
|
|
465
|
+
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, all, undefined, settings));
|
|
466
|
+
throw new HelpShown;
|
|
467
|
+
}
|
|
468
|
+
if (settings.positionals !== undefined)
|
|
469
|
+
checkPositionals(parsed.positionals, settings.positionals);
|
|
470
|
+
return { ...parsed, positionals: parsed.positionals, rest };
|
|
471
|
+
}
|
|
472
|
+
function checkPositionals(given, names) {
|
|
473
|
+
const variadic = names.at(-1)?.replace("]", "").endsWith("...") === true;
|
|
474
|
+
const required = names.filter((name) => !name.startsWith("[")).length;
|
|
475
|
+
if (given.length < required)
|
|
476
|
+
badUsage(`missing ${names[given.length]}`);
|
|
477
|
+
if (!variadic && given.length > names.length)
|
|
478
|
+
badUsage(`unexpected argument ${given[names.length]}`);
|
|
479
|
+
}
|
|
480
|
+
function joinDashValues(args, options) {
|
|
481
|
+
const longOf = new Map(Object.entries(options).flatMap(([name, option]) => option.short === undefined ? [[`--${name}`, name]] : [[`--${name}`, name], [`-${option.short}`, name]]));
|
|
482
|
+
const isOption = (arg) => arg === "--" || longOf.has(arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg);
|
|
483
|
+
const joined = [];
|
|
484
|
+
for (let i = 0;i < args.length; i++) {
|
|
485
|
+
const arg = args[i] ?? "";
|
|
486
|
+
if (arg === "--")
|
|
487
|
+
return [...joined, ...args.slice(i)];
|
|
488
|
+
const name = longOf.get(arg);
|
|
489
|
+
const next = args[i + 1];
|
|
490
|
+
if (name !== undefined && options[name]?.type === "string" && next !== undefined && next.startsWith("-") && next !== "-" && !isOption(next)) {
|
|
491
|
+
joined.push(`--${name}=${next}`);
|
|
492
|
+
i++;
|
|
493
|
+
} else {
|
|
494
|
+
joined.push(arg);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return joined;
|
|
498
|
+
}
|
|
499
|
+
function parseError(error) {
|
|
500
|
+
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
501
|
+
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
502
|
+
}
|
|
503
|
+
async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
504
|
+
if (path === "-") {
|
|
505
|
+
if (process4.stdin.isTTY)
|
|
506
|
+
fail(noStdin);
|
|
507
|
+
return Bun.stdin.text();
|
|
508
|
+
}
|
|
509
|
+
const file = Bun.file(path);
|
|
510
|
+
if (!await file.exists())
|
|
511
|
+
fail(`no file ${path}`);
|
|
512
|
+
return file.text();
|
|
513
|
+
}
|
|
514
|
+
// ../kit/src/cli.ts
|
|
515
|
+
import process5 from "process";
|
|
516
|
+
|
|
517
|
+
// ../kit/src/text.ts
|
|
518
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
519
|
+
function distance(a, b) {
|
|
520
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
521
|
+
for (const [i, left] of [...a].entries()) {
|
|
522
|
+
const current = [i + 1];
|
|
523
|
+
for (const [j, right] of [...b].entries())
|
|
524
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
525
|
+
previous = current;
|
|
526
|
+
}
|
|
527
|
+
return previous[b.length] ?? 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ../kit/src/cli.ts
|
|
531
|
+
var LEVEL_OPTIONS = {
|
|
532
|
+
...OUTPUT_OPTIONS,
|
|
533
|
+
help: { type: "boolean", short: "h", description: "show this help" }
|
|
534
|
+
};
|
|
535
|
+
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
536
|
+
function commandRows(commands, colors) {
|
|
537
|
+
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
538
|
+
const groups = new Map([["Commands:", []]]);
|
|
539
|
+
for (const [name, { summary, group }] of Object.entries(commands)) {
|
|
540
|
+
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
541
|
+
const rows = groups.get(heading) ?? [];
|
|
542
|
+
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summary)}`);
|
|
543
|
+
groups.set(heading, rows);
|
|
544
|
+
}
|
|
545
|
+
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
546
|
+
}
|
|
547
|
+
function takeOutputFlags(argv) {
|
|
548
|
+
let index = 0;
|
|
549
|
+
for (;index < argv.length; index++) {
|
|
550
|
+
const word = argv[index];
|
|
551
|
+
if (word === "--plain")
|
|
552
|
+
applyOutput(undefined, true);
|
|
553
|
+
else if (word.startsWith("--color="))
|
|
554
|
+
applyOutput(word.slice("--color=".length), undefined);
|
|
555
|
+
else if (word === "--color" && argv[index + 1] !== undefined)
|
|
556
|
+
applyOutput(argv[++index], undefined);
|
|
557
|
+
else
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
return argv.slice(index);
|
|
561
|
+
}
|
|
562
|
+
function closest(name, names) {
|
|
563
|
+
const started = names.find((known) => known.startsWith(name));
|
|
564
|
+
if (started !== undefined)
|
|
565
|
+
return started;
|
|
566
|
+
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
567
|
+
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
568
|
+
}
|
|
569
|
+
async function help({ path, header, summary, commands, defaultCommand, examples, replaces }, stream) {
|
|
570
|
+
const colors = paint(stream);
|
|
571
|
+
const prefix = path.join(" ");
|
|
572
|
+
if (wantsHelpJson())
|
|
573
|
+
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
574
|
+
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
575
|
+
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
576
|
+
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
577
|
+
return [
|
|
578
|
+
header(colors),
|
|
579
|
+
"",
|
|
580
|
+
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
581
|
+
...replacesRows(replaces, colors),
|
|
582
|
+
"",
|
|
583
|
+
...commandRows(commands, colors),
|
|
584
|
+
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
585
|
+
"",
|
|
586
|
+
colors.bold("Options:"),
|
|
587
|
+
...optionRows(path.length === 1 ? TOP_OPTIONS : LEVEL_OPTIONS, colors),
|
|
588
|
+
...exampleRows(examples ?? [], colors),
|
|
589
|
+
"",
|
|
590
|
+
`${colors.dim("Run")} ${prefix} ${colors.cyan("<command>")} --help ${colors.dim("for its options.")}`
|
|
591
|
+
].join(`
|
|
592
|
+
`);
|
|
593
|
+
}
|
|
594
|
+
function printError(prefix, message, hint, cause) {
|
|
595
|
+
const colors = paint(process5.stderr);
|
|
596
|
+
const styled = takesControlCodes(process5.stderr);
|
|
597
|
+
const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
|
|
598
|
+
const under = (label, text, tint) => text === undefined ? "" : `
|
|
599
|
+
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
600
|
+
console.error(`${head} ${message}${under("cause", cause, colors.dim)}${under("hint", hint, colors.yellow)}`);
|
|
601
|
+
}
|
|
602
|
+
function printJsonError(message, hint, cause) {
|
|
603
|
+
console.error(JSON.stringify({ error: message, ...hint === undefined ? {} : { hint }, ...cause === undefined ? {} : { cause } }));
|
|
604
|
+
}
|
|
605
|
+
function wantsJson(argv) {
|
|
606
|
+
const end = argv.indexOf("--");
|
|
607
|
+
return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
|
|
608
|
+
}
|
|
609
|
+
function reportThrown(prefix, error, redact = (text) => text, json = false) {
|
|
610
|
+
if (error instanceof HelpShown)
|
|
611
|
+
return 0;
|
|
612
|
+
const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
|
|
613
|
+
const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
|
|
614
|
+
const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
|
|
615
|
+
const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
|
|
616
|
+
report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
|
|
617
|
+
return error instanceof Exit ? error.code : 1;
|
|
618
|
+
}
|
|
619
|
+
async function dispatch(level, typed, { version, redact }) {
|
|
620
|
+
const prefix = level.path.join(" ");
|
|
621
|
+
let argv;
|
|
622
|
+
try {
|
|
623
|
+
argv = takeOutputFlags(typed);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
return reportThrown(prefix, error);
|
|
626
|
+
}
|
|
627
|
+
const [name, ...args] = argv;
|
|
628
|
+
if (name === undefined && level.path.length > 1) {
|
|
629
|
+
console.error(await help(level, process5.stderr));
|
|
630
|
+
return 2;
|
|
631
|
+
}
|
|
632
|
+
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
633
|
+
console.log(await help(level, process5.stdout));
|
|
634
|
+
return 0;
|
|
635
|
+
}
|
|
636
|
+
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
637
|
+
console.log(version);
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
let command = level.commands[level.aliases?.[name] ?? name];
|
|
641
|
+
let rest = args;
|
|
642
|
+
if (command === undefined && level.defaultCommand !== undefined) {
|
|
643
|
+
command = level.commands[level.defaultCommand];
|
|
644
|
+
rest = argv;
|
|
645
|
+
}
|
|
646
|
+
if (command === undefined) {
|
|
647
|
+
const meant = closest(name, Object.keys(level.commands));
|
|
648
|
+
printError(prefix, `no command ${name}`, meant === undefined ? `run ${prefix} --help for its commands` : `did you mean ${meant}?`);
|
|
649
|
+
return 2;
|
|
650
|
+
}
|
|
651
|
+
if ("commands" in command) {
|
|
652
|
+
const { summary } = command;
|
|
653
|
+
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${summary}`, summary, commands: command.commands, aliases: command.aliases, defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
return await command.run(rest);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
var COMPLETE = "__complete";
|
|
662
|
+
var FILES = "__files__";
|
|
663
|
+
async function optionsOf(run) {
|
|
664
|
+
completion.active = true;
|
|
665
|
+
try {
|
|
666
|
+
await run([]);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
if (error instanceof Completing)
|
|
669
|
+
return error.options;
|
|
670
|
+
} finally {
|
|
671
|
+
completion.active = false;
|
|
672
|
+
}
|
|
673
|
+
return {};
|
|
674
|
+
}
|
|
675
|
+
async function leafCandidates(run, words) {
|
|
676
|
+
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
677
|
+
return [FILES];
|
|
678
|
+
return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
|
|
679
|
+
}
|
|
680
|
+
async function candidates(commands, aliases, defaultCommand, words) {
|
|
681
|
+
const [word = "", ...rest] = words;
|
|
682
|
+
const command = commands[aliases?.[word] ?? word];
|
|
683
|
+
if (rest.length === 0 && !word.startsWith("-"))
|
|
684
|
+
return Object.entries(commands).map(([name, { summary }]) => `${name} ${summary}`);
|
|
685
|
+
if (command === undefined) {
|
|
686
|
+
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
687
|
+
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
688
|
+
}
|
|
689
|
+
return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
|
|
690
|
+
}
|
|
691
|
+
async function printCandidates(found) {
|
|
692
|
+
for (const line of await found)
|
|
693
|
+
console.log(line);
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
696
|
+
async function runCli(cli, argv = process5.argv.slice(2)) {
|
|
697
|
+
if (argv[0] === COMPLETE)
|
|
698
|
+
return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
|
|
699
|
+
return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${cli.about}`, summary: cli.about, commands: cli.commands, aliases: cli.aliases, defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
|
|
700
|
+
}
|
|
701
|
+
async function runMain({ name, version, run, redact }, argv = process5.argv.slice(2)) {
|
|
702
|
+
if (argv[0] === COMPLETE)
|
|
703
|
+
return printCandidates(leafCandidates(run, argv.slice(1)));
|
|
704
|
+
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
|
|
705
|
+
console.log(version);
|
|
706
|
+
return 0;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
return await run(argv);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
return reportThrown(name, error, redact, wantsJson(argv));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// package.json
|
|
715
|
+
var version = "0.5.0";
|
|
716
|
+
|
|
717
|
+
// src/commands/audit.ts
|
|
718
|
+
import { stat as stat3 } from "fs/promises";
|
|
719
|
+
import { relative, resolve } from "path";
|
|
720
|
+
import process7 from "process";
|
|
721
|
+
|
|
722
|
+
// src/audit.ts
|
|
723
|
+
import { readdir as readdir2, stat } from "fs/promises";
|
|
724
|
+
import { dirname, join } from "path";
|
|
725
|
+
var {$ } = globalThis.Bun;
|
|
726
|
+
|
|
727
|
+
// src/files.ts
|
|
728
|
+
import { readdir } from "fs/promises";
|
|
729
|
+
|
|
730
|
+
// src/dotenv.ts
|
|
731
|
+
var ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i;
|
|
732
|
+
var EXPORTED = /^\s*export\s/;
|
|
733
|
+
var INLINE_COMMENT = /\s+#.*$/;
|
|
734
|
+
var CARRIAGE_RETURN = /\r$/;
|
|
735
|
+
var ESCAPES = { n: `
|
|
736
|
+
`, r: "\r", t: "\t", '"': '"', "\\": "\\" };
|
|
737
|
+
var NEEDS_QUOTES = /[\s#"'\\$`]/;
|
|
738
|
+
function closingQuote(text, quote) {
|
|
739
|
+
for (let index = 0;index < text.length; index++) {
|
|
740
|
+
if (quote === '"' && text[index] === "\\")
|
|
741
|
+
index++;
|
|
742
|
+
else if (text[index] === quote)
|
|
743
|
+
return index;
|
|
744
|
+
}
|
|
745
|
+
return -1;
|
|
746
|
+
}
|
|
747
|
+
function unescapeDouble(text) {
|
|
748
|
+
let out = "";
|
|
749
|
+
for (let index = 0;index < text.length; index++) {
|
|
750
|
+
const next = text[index + 1];
|
|
751
|
+
if (text[index] === "\\" && next !== undefined && next in ESCAPES) {
|
|
752
|
+
out += ESCAPES[next];
|
|
753
|
+
index++;
|
|
754
|
+
} else {
|
|
755
|
+
out += text[index];
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return out;
|
|
759
|
+
}
|
|
760
|
+
function parseDotenv(text) {
|
|
761
|
+
const physical = text.split(`
|
|
762
|
+
`);
|
|
763
|
+
const lines = [];
|
|
764
|
+
for (let index = 0;index < physical.length; index++) {
|
|
765
|
+
const line = physical[index] ?? "";
|
|
766
|
+
const match = ASSIGNMENT.exec(line.replace(CARRIAGE_RETURN, ""));
|
|
767
|
+
if (match === null) {
|
|
768
|
+
lines.push({ raw: line });
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const [, key = "", rest = ""] = match;
|
|
772
|
+
const start = rest.trimStart();
|
|
773
|
+
const quote = start[0];
|
|
774
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") {
|
|
775
|
+
lines.push({ raw: line, key, value: start.replace(INLINE_COMMENT, "").trim() });
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
let body = start.slice(1);
|
|
779
|
+
let raw = line;
|
|
780
|
+
let end = closingQuote(body, quote);
|
|
781
|
+
while (end === -1 && index + 1 < physical.length) {
|
|
782
|
+
index++;
|
|
783
|
+
body += `
|
|
784
|
+
${(physical[index] ?? "").replace(CARRIAGE_RETURN, "")}`;
|
|
785
|
+
raw += `
|
|
786
|
+
${physical[index] ?? ""}`;
|
|
787
|
+
end = closingQuote(body, quote);
|
|
788
|
+
}
|
|
789
|
+
const inner = end === -1 ? body : body.slice(0, end);
|
|
790
|
+
lines.push({ raw, key, value: quote === '"' ? unescapeDouble(inner) : inner });
|
|
791
|
+
}
|
|
792
|
+
return { lines };
|
|
793
|
+
}
|
|
794
|
+
function serializeDotenv(file) {
|
|
795
|
+
return file.lines.map((line) => line.raw).join(`
|
|
796
|
+
`);
|
|
797
|
+
}
|
|
798
|
+
function entries(file) {
|
|
799
|
+
const map = new Map;
|
|
800
|
+
for (const line of file.lines) {
|
|
801
|
+
if (line.key !== undefined)
|
|
802
|
+
map.set(line.key, line.value ?? "");
|
|
803
|
+
}
|
|
804
|
+
return map;
|
|
805
|
+
}
|
|
806
|
+
function formatAssignment(key, value) {
|
|
807
|
+
if (value === "" || !NEEDS_QUOTES.test(value))
|
|
808
|
+
return `${key}=${value}`;
|
|
809
|
+
const escaped = value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("\r", "\\r").replaceAll(`
|
|
810
|
+
`, "\\n");
|
|
811
|
+
return `${key}="${escaped}"`;
|
|
812
|
+
}
|
|
813
|
+
function setLine(file, key, value, raw) {
|
|
814
|
+
const index = file.lines.findLastIndex((line) => line.key === key);
|
|
815
|
+
const existing = file.lines[index];
|
|
816
|
+
if (existing !== undefined) {
|
|
817
|
+
const exported = EXPORTED.test(existing.raw) ? "export " : "";
|
|
818
|
+
file.lines[index] = { raw: raw ?? `${exported}${formatAssignment(key, value)}`, key, value };
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const line = { raw: raw ?? formatAssignment(key, value), key, value };
|
|
822
|
+
const last = file.lines.at(-1);
|
|
823
|
+
if (last === undefined)
|
|
824
|
+
file.lines.push(line, { raw: "" });
|
|
825
|
+
else if (last.raw === "")
|
|
826
|
+
file.lines.splice(-1, 0, line);
|
|
827
|
+
else
|
|
828
|
+
file.lines.push(line, { raw: "" });
|
|
829
|
+
}
|
|
830
|
+
function removeKey(file, key) {
|
|
831
|
+
const before = file.lines.length;
|
|
832
|
+
file.lines = file.lines.filter((line) => line.key !== key);
|
|
833
|
+
return before - file.lines.length;
|
|
834
|
+
}
|
|
835
|
+
function lastLine(file, key) {
|
|
836
|
+
return file.lines.findLast((line) => line.key === key);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/mask.ts
|
|
840
|
+
var MIN_KNOWN_LENGTH = 6;
|
|
841
|
+
var SECRET_KEY = /TOKEN|SECRET|PASSWORD|KEY|PRIVATE|DSN/i;
|
|
842
|
+
var URL_PASSWORD = /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]*:([^\s@/]+)@/dgi;
|
|
843
|
+
var ESCAPE2 = /[.*+?^${}()|[\]\\]/g;
|
|
844
|
+
var TOKEN_FORMATS = [
|
|
845
|
+
["GitHub token", /\b(?:gh[pousr]_|github_pat_)\w{20,}/g],
|
|
846
|
+
["GitLab token", /\bglpat-[\w-]{20,}/g],
|
|
847
|
+
["API key", /\bsk-[\w-]{20,}/g],
|
|
848
|
+
["Slack token", /\bxox[abprs]-[\w-]{10,}/g],
|
|
849
|
+
["JWT", /\beyJ[\w-]+\.[\w-]+\.[\w-]+/g]
|
|
850
|
+
];
|
|
851
|
+
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
852
|
+
var PEM_END = /-----END [A-Z ]*PRIVATE KEY-----/;
|
|
853
|
+
function isSecretKey(key) {
|
|
854
|
+
return SECRET_KEY.test(key);
|
|
855
|
+
}
|
|
856
|
+
function hint(value) {
|
|
857
|
+
const shown = Math.min(3, Math.floor(value.length / 4));
|
|
858
|
+
return `${value.slice(0, shown)}***(${value.length})`;
|
|
859
|
+
}
|
|
860
|
+
function knownSecrets(pairs) {
|
|
861
|
+
const keys = new Map;
|
|
862
|
+
for (const [key, value] of pairs) {
|
|
863
|
+
if (value.length >= MIN_KNOWN_LENGTH && !keys.has(value))
|
|
864
|
+
keys.set(value, key);
|
|
865
|
+
}
|
|
866
|
+
if (keys.size === 0)
|
|
867
|
+
return { pattern: undefined, keys };
|
|
868
|
+
const alternatives = [...keys.keys()].sort((a, b) => b.length - a.length).map((value) => value.replace(ESCAPE2, "\\$&"));
|
|
869
|
+
return { pattern: new RegExp(`(?<!\\w)(?:${alternatives.join("|")})(?!\\w)`, "g"), keys };
|
|
870
|
+
}
|
|
871
|
+
function urlPasswords(text) {
|
|
872
|
+
return [...text.matchAll(URL_PASSWORD)].flatMap((url) => url[1] ?? []);
|
|
873
|
+
}
|
|
874
|
+
function findSecrets(line, known) {
|
|
875
|
+
const found = [];
|
|
876
|
+
for (const url of line.matchAll(URL_PASSWORD)) {
|
|
877
|
+
const [start, end] = url.indices?.[1] ?? [];
|
|
878
|
+
if (start !== undefined && end !== undefined)
|
|
879
|
+
found.push({ start, end, kind: "URL password", mask: "***" });
|
|
880
|
+
}
|
|
881
|
+
for (const [kind, pattern] of TOKEN_FORMATS) {
|
|
882
|
+
for (const token of line.matchAll(pattern))
|
|
883
|
+
found.push({ start: token.index, end: token.index + token[0].length, kind, mask: hint(token[0]) });
|
|
884
|
+
}
|
|
885
|
+
if (known?.pattern !== undefined) {
|
|
886
|
+
for (const value of line.matchAll(known.pattern))
|
|
887
|
+
found.push({ start: value.index, end: value.index + value[0].length, kind: `value of ${known.keys.get(value[0])}`, mask: hint(value[0]) });
|
|
888
|
+
}
|
|
889
|
+
found.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
890
|
+
const kept = [];
|
|
891
|
+
for (const match of found) {
|
|
892
|
+
if (match.start >= (kept.at(-1)?.end ?? 0))
|
|
893
|
+
kept.push(match);
|
|
894
|
+
}
|
|
895
|
+
return kept;
|
|
896
|
+
}
|
|
897
|
+
function maskLine(line, known) {
|
|
898
|
+
let out = "";
|
|
899
|
+
let at = 0;
|
|
900
|
+
for (const match of findSecrets(line, known)) {
|
|
901
|
+
out += line.slice(at, match.start) + match.mask;
|
|
902
|
+
at = match.end;
|
|
903
|
+
}
|
|
904
|
+
return out + line.slice(at);
|
|
905
|
+
}
|
|
906
|
+
function maskValue(key, value) {
|
|
907
|
+
if (value === "")
|
|
908
|
+
return value;
|
|
909
|
+
return isSecretKey(key) || PEM_BEGIN.test(value) ? hint(value) : maskLine(value);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/files.ts
|
|
913
|
+
var ENV_FILE = /^\.env(?:\..+)?$/;
|
|
914
|
+
var TEMPLATE = /\.(?:example|sample|template|dist|defaults)$/;
|
|
915
|
+
async function envFiles(folder = process.cwd()) {
|
|
916
|
+
const found = await readdir(folder, { withFileTypes: true });
|
|
917
|
+
return found.filter((entry) => entry.isFile() && ENV_FILE.test(entry.name)).map((entry) => entry.name).sort();
|
|
918
|
+
}
|
|
919
|
+
async function readEnv(path) {
|
|
920
|
+
const file = Bun.file(path);
|
|
921
|
+
if (!await file.exists())
|
|
922
|
+
fail(`${path} does not exist`);
|
|
923
|
+
return parseDotenv(await file.text());
|
|
924
|
+
}
|
|
925
|
+
async function writeEnv(path, file) {
|
|
926
|
+
await Bun.write(path, serializeDotenv(file));
|
|
927
|
+
}
|
|
928
|
+
async function loadKnown(folder = process.cwd()) {
|
|
929
|
+
const pairs = [];
|
|
930
|
+
for (const name of await envFiles(folder)) {
|
|
931
|
+
for (const [key, value] of entries(parseDotenv(await Bun.file(`${folder}/${name}`).text()))) {
|
|
932
|
+
if (isSecretKey(key))
|
|
933
|
+
pairs.push([key, value]);
|
|
934
|
+
else
|
|
935
|
+
pairs.push(...urlPasswords(value).map((password) => [key, password]));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return knownSecrets(pairs);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/audit.ts
|
|
942
|
+
var MAX_DEPTH = 5;
|
|
943
|
+
var SKIPPED = new Set(["node_modules", ".git", "vendor"]);
|
|
944
|
+
var COMPOSE_FILE = /^(?:docker-)?compose[\w.-]*\.ya?ml$/;
|
|
945
|
+
var ENTRY = /^\s*(?:-\s*)?["']?([A-Z0-9_]+)["']?\s*[:=]\s*["']?([^"'\s#]+)/;
|
|
946
|
+
var SECRET_NAME = /(?:PASSWORD|PASSWD|SECRET|TOKEN|KEY)$/;
|
|
947
|
+
var FLAG = /^(?:[01]|yes|no|true|false)$/i;
|
|
948
|
+
function isInlineSecret(name, value) {
|
|
949
|
+
if (value === "" || value.startsWith("$") || value.startsWith("/run/secrets/") || FLAG.test(value))
|
|
950
|
+
return false;
|
|
951
|
+
return SECRET_NAME.test(name) || urlPasswords(value).some((password) => !password.startsWith("$"));
|
|
952
|
+
}
|
|
953
|
+
function inlineSecrets(compose) {
|
|
954
|
+
const names = compose.split(`
|
|
955
|
+
`).flatMap((line) => {
|
|
956
|
+
const [, name = "", value = ""] = ENTRY.exec(line) ?? [];
|
|
957
|
+
return isInlineSecret(name, value) ? [name] : [];
|
|
958
|
+
});
|
|
959
|
+
return [...new Set(names)];
|
|
960
|
+
}
|
|
961
|
+
async function openToOthers(path) {
|
|
962
|
+
if (((await stat(path)).mode & 4) === 0)
|
|
963
|
+
return false;
|
|
964
|
+
for (let dir = dirname(path);dir !== "/"; dir = dirname(dir)) {
|
|
965
|
+
if (((await stat(dir)).mode & 1) === 0)
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
return true;
|
|
969
|
+
}
|
|
970
|
+
async function tracked(path) {
|
|
971
|
+
return (await $`git ls-files --error-unmatch -- ${path}`.cwd(dirname(path)).quiet().nothrow()).exitCode === 0;
|
|
972
|
+
}
|
|
973
|
+
async function* candidates2(root, unread, depth = 0) {
|
|
974
|
+
let found;
|
|
975
|
+
try {
|
|
976
|
+
found = await readdir2(root, { withFileTypes: true });
|
|
977
|
+
} catch {
|
|
978
|
+
unread.push(root);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
for (const entry of found) {
|
|
982
|
+
const path = join(root, entry.name);
|
|
983
|
+
if (entry.isDirectory() && depth < MAX_DEPTH && !SKIPPED.has(entry.name))
|
|
984
|
+
yield* candidates2(path, unread, depth + 1);
|
|
985
|
+
else if (entry.isFile() && !TEMPLATE.test(entry.name) && (ENV_FILE.test(entry.name) || COMPOSE_FILE.test(entry.name)))
|
|
986
|
+
yield path;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async function audit(roots) {
|
|
990
|
+
const exposed = [];
|
|
991
|
+
const unread = [];
|
|
992
|
+
for (const root of roots) {
|
|
993
|
+
for await (const path of candidates2(root, unread)) {
|
|
994
|
+
const readable = await openToOthers(path);
|
|
995
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
996
|
+
if (ENV_FILE.test(name)) {
|
|
997
|
+
const isTracked = await tracked(path);
|
|
998
|
+
if (readable || isTracked)
|
|
999
|
+
exposed.push({ path, readable, tracked: isTracked });
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const secrets = readable ? inlineSecrets(await Bun.file(path).text()) : [];
|
|
1003
|
+
if (secrets.length > 0)
|
|
1004
|
+
exposed.push({ path, readable, tracked: false, secrets });
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return { exposed: exposed.sort((a, b) => a.path.localeCompare(b.path)), unread };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// src/fix.ts
|
|
1011
|
+
import { chmod, chown, rm, stat as stat2, writeFile } from "fs/promises";
|
|
1012
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1013
|
+
var MAP_ENTRY = /^(\s*)(["']?)([A-Z0-9_]+)\2(\s*:)(.*)$/;
|
|
1014
|
+
var LIST_ENTRY = /^(\s*-[ \t]*)(["']?)([A-Z0-9_]+)=(.*)$/;
|
|
1015
|
+
var TRAILING = /^\s*(?:#.*)?$/;
|
|
1016
|
+
var PLAIN_COMMENT = /\s+#.*$/;
|
|
1017
|
+
var NOT_PLAIN = /^[|>&*![{]/;
|
|
1018
|
+
var ENV_PLAIN = /^[^\s#"'\\$`]*$/;
|
|
1019
|
+
function quoted(text, quote) {
|
|
1020
|
+
let value = "";
|
|
1021
|
+
for (let index = 0;index < text.length; index++) {
|
|
1022
|
+
const char = text[index] ?? "";
|
|
1023
|
+
const next = text[index + 1];
|
|
1024
|
+
if (quote === "'" && char === "'" && next === "'") {
|
|
1025
|
+
value += "'";
|
|
1026
|
+
index++;
|
|
1027
|
+
} else if (quote === '"' && char === "\\") {
|
|
1028
|
+
if (next !== "\\" && next !== '"')
|
|
1029
|
+
return;
|
|
1030
|
+
value += next;
|
|
1031
|
+
index++;
|
|
1032
|
+
} else if (char === quote) {
|
|
1033
|
+
return { value, after: text.slice(index + 1) };
|
|
1034
|
+
} else {
|
|
1035
|
+
value += char;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
function plain(text) {
|
|
1041
|
+
const value = text.replace(PLAIN_COMMENT, "").trimEnd();
|
|
1042
|
+
return { value, after: text.slice(value.length) };
|
|
1043
|
+
}
|
|
1044
|
+
function composeEntry(line) {
|
|
1045
|
+
const map = MAP_ENTRY.exec(line);
|
|
1046
|
+
if (map !== null) {
|
|
1047
|
+
const [, indent = "", keyQuote = "", key = "", colon = "", spaced = ""] = map;
|
|
1048
|
+
const rest = spaced.trimStart();
|
|
1049
|
+
const separator = colon + spaced.slice(0, spaced.length - rest.length);
|
|
1050
|
+
const quote = rest[0] === '"' || rest[0] === "'" ? rest[0] : "";
|
|
1051
|
+
const scalar = quote === "" ? plain(rest) : quoted(rest.slice(1), quote);
|
|
1052
|
+
if (scalar === undefined || !TRAILING.test(scalar.after))
|
|
1053
|
+
return { key, value: rest, unmovable: "its value is written in a form ev does not rewrite" };
|
|
1054
|
+
if (quote === "" && NOT_PLAIN.test(scalar.value))
|
|
1055
|
+
return { key, value: scalar.value, unmovable: "its value is a block, anchor, alias, tag or collection" };
|
|
1056
|
+
return { key, value: scalar.value, swap: (ref) => `${indent}${keyQuote}${key}${keyQuote}${separator}${quote}${ref}${quote}${scalar.after}` };
|
|
1057
|
+
}
|
|
1058
|
+
const list = LIST_ENTRY.exec(line);
|
|
1059
|
+
if (list === null)
|
|
1060
|
+
return;
|
|
1061
|
+
const [, dash = "", quote = "", key = "", rest = ""] = list;
|
|
1062
|
+
const scalar = quote === "" ? plain(rest) : quoted(rest, quote);
|
|
1063
|
+
if (scalar === undefined || !TRAILING.test(scalar.after))
|
|
1064
|
+
return { key, value: rest, unmovable: "its value is written in a form ev does not rewrite" };
|
|
1065
|
+
return { key, value: scalar.value, swap: (ref) => `${dash}${quote}${key}=${ref}${quote}${scalar.after}` };
|
|
1066
|
+
}
|
|
1067
|
+
function envLine(key, value) {
|
|
1068
|
+
if (ENV_PLAIN.test(value))
|
|
1069
|
+
return `${key}=${value}`;
|
|
1070
|
+
return value.includes("'") || value.includes(`
|
|
1071
|
+
`) ? undefined : `${key}='${value}'`;
|
|
1072
|
+
}
|
|
1073
|
+
async function resolved(path) {
|
|
1074
|
+
const run = Bun.spawn(["docker", "compose", "-f", path, "config", "--format", "json"], { cwd: dirname2(path), stdout: "pipe", stderr: "ignore", stdin: "ignore" });
|
|
1075
|
+
const [out, code] = await Promise.all([new Response(run.stdout).text(), run.exited]);
|
|
1076
|
+
return code === 0 ? out : undefined;
|
|
1077
|
+
}
|
|
1078
|
+
function gainedBy(before, after, key) {
|
|
1079
|
+
const services = (config) => Object.entries(JSON.parse(config).services ?? {});
|
|
1080
|
+
const had = new Set(services(before).filter(([, service]) => service.environment?.[key] !== undefined).map(([name]) => name));
|
|
1081
|
+
return services(after).filter(([name, service]) => service.environment?.[key] !== undefined && !had.has(name)).map(([name]) => name);
|
|
1082
|
+
}
|
|
1083
|
+
async function shutOthersOut(path) {
|
|
1084
|
+
try {
|
|
1085
|
+
await chmod(path, (await stat2(path)).mode & 504);
|
|
1086
|
+
return true;
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
if (error.code === "EPERM")
|
|
1089
|
+
return false;
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async function fixCompose(path) {
|
|
1094
|
+
const env = join2(dirname2(path), ".env");
|
|
1095
|
+
const compose = await Bun.file(path).text();
|
|
1096
|
+
const envBefore = await Bun.file(env).exists() ? await Bun.file(env).text() : undefined;
|
|
1097
|
+
const dotenv = parseDotenv(envBefore ?? "");
|
|
1098
|
+
const known = entries(dotenv);
|
|
1099
|
+
const fix = { path, env, moved: [], left: [], open: [] };
|
|
1100
|
+
const lines = compose.split(`
|
|
1101
|
+
`).map((line) => {
|
|
1102
|
+
const entry = composeEntry(line);
|
|
1103
|
+
if (entry === undefined || !isInlineSecret(entry.key, entry.value))
|
|
1104
|
+
return line;
|
|
1105
|
+
const { key } = entry;
|
|
1106
|
+
if ("unmovable" in entry) {
|
|
1107
|
+
fix.left.push({ key, why: entry.unmovable });
|
|
1108
|
+
return line;
|
|
1109
|
+
}
|
|
1110
|
+
if (entry.value.replaceAll("$$", "").includes("$")) {
|
|
1111
|
+
fix.left.push({ key, why: "its value reads another variable" });
|
|
1112
|
+
return line;
|
|
1113
|
+
}
|
|
1114
|
+
const value = entry.value.replaceAll("$$", "$");
|
|
1115
|
+
const existing = known.get(key);
|
|
1116
|
+
if (existing === undefined) {
|
|
1117
|
+
const raw = envLine(key, value);
|
|
1118
|
+
if (raw === undefined) {
|
|
1119
|
+
fix.left.push({ key, why: "its value holds a quote or line break a .env cannot keep literal" });
|
|
1120
|
+
return line;
|
|
1121
|
+
}
|
|
1122
|
+
setLine(dotenv, key, value, raw);
|
|
1123
|
+
known.set(key, value);
|
|
1124
|
+
} else if (existing !== value) {
|
|
1125
|
+
fix.left.push({ key, why: `${env.slice(env.lastIndexOf("/") + 1)} already sets it to another value` });
|
|
1126
|
+
return line;
|
|
1127
|
+
}
|
|
1128
|
+
if (!fix.moved.includes(key))
|
|
1129
|
+
fix.moved.push(key);
|
|
1130
|
+
return entry.swap(`\${${key}}`);
|
|
1131
|
+
});
|
|
1132
|
+
if (fix.moved.length > 0)
|
|
1133
|
+
await move(fix, compose, lines.join(`
|
|
1134
|
+
`), envBefore, serializeDotenv(dotenv));
|
|
1135
|
+
for (const file of [path, env]) {
|
|
1136
|
+
if (await Bun.file(file).exists() && !await shutOthersOut(file))
|
|
1137
|
+
fix.open.push(file);
|
|
1138
|
+
}
|
|
1139
|
+
return fix;
|
|
1140
|
+
}
|
|
1141
|
+
async function move(fix, compose, composeAfter, envBefore, envAfter) {
|
|
1142
|
+
const { path, env } = fix;
|
|
1143
|
+
const docker = Bun.which("docker") !== null;
|
|
1144
|
+
const before = docker ? await resolved(path) : undefined;
|
|
1145
|
+
if (envBefore === undefined) {
|
|
1146
|
+
await writeFile(env, envAfter, { mode: 384 });
|
|
1147
|
+
const { uid, gid } = await stat2(path);
|
|
1148
|
+
try {
|
|
1149
|
+
await chown(env, uid, gid);
|
|
1150
|
+
} catch {}
|
|
1151
|
+
} else {
|
|
1152
|
+
await writeFile(env, envAfter);
|
|
1153
|
+
}
|
|
1154
|
+
await writeFile(path, composeAfter);
|
|
1155
|
+
if (!docker) {
|
|
1156
|
+
fix.check = "no docker";
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (before === undefined) {
|
|
1160
|
+
fix.check = "fails before";
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const after = await resolved(path);
|
|
1164
|
+
if (after === before) {
|
|
1165
|
+
fix.check = "same";
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
await writeFile(path, compose);
|
|
1169
|
+
if (envBefore === undefined)
|
|
1170
|
+
await rm(env);
|
|
1171
|
+
else
|
|
1172
|
+
await writeFile(env, envBefore);
|
|
1173
|
+
fix.check = "rolled back";
|
|
1174
|
+
fix.left.push(...fix.moved.map((key) => {
|
|
1175
|
+
const gained = after === undefined ? [] : gainedBy(before, after, key);
|
|
1176
|
+
return { key, why: gained.length > 0 ? `${gained.join(", ")} ${gained.length > 1 ? "load" : "loads"} the .env through env_file and would gain it, so it was put back` : "docker compose config resolved differently once moved, so it was put back" };
|
|
1177
|
+
}));
|
|
1178
|
+
fix.moved = [];
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/commands/audit.ts
|
|
1182
|
+
var usage = `ev audit [dir...] [--fix]
|
|
1183
|
+
|
|
1184
|
+
Lists the .env files any account on the machine can read or git tracks, and the compose files any account can read that write a secret out (a secret-named value or a URL password) instead of reading it from a .env, never printing a value.
|
|
1185
|
+
Looks under each folder (the current one by default) down to 5 levels, skipping node_modules, vendor and .git, and leaves .example, .sample, .template and .dist files out. A folder it cannot read is named on stderr.
|
|
1186
|
+
With --fix, moves each secret a listed compose file writes out into the .env beside it as \${KEY}, puts it back when \`docker compose config\` resolves differently after, and shuts other accounts out of both files and of every listed .env; a key the .env already sets to another value stays put.
|
|
1187
|
+
Exits 0 when it finds none (with --fix, when none is left), 1 when it finds one, 2 on a usage error.`;
|
|
1188
|
+
var options = {
|
|
1189
|
+
json: jsonOption("the findings and the folders it could not read"),
|
|
1190
|
+
fix: { type: "boolean", description: "move inline compose secrets into a .env and shut other accounts out" }
|
|
1191
|
+
};
|
|
1192
|
+
var SHELL_SAFE = /^[\w./-]+$/;
|
|
1193
|
+
var MAX_UNREAD_LISTED = 3;
|
|
1194
|
+
var CHECKS = {
|
|
1195
|
+
same: "docker compose config resolves the same",
|
|
1196
|
+
"rolled back": "docker compose config resolved differently, so everything was put back",
|
|
1197
|
+
"no docker": "not checked against docker compose config: docker is not installed",
|
|
1198
|
+
"fails before": "not checked against docker compose config: it already failed on the file"
|
|
1199
|
+
};
|
|
1200
|
+
async function isFolder(path) {
|
|
1201
|
+
try {
|
|
1202
|
+
return (await stat3(path)).isDirectory();
|
|
1203
|
+
} catch {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function quoted2(path) {
|
|
1208
|
+
return SHELL_SAFE.test(path) ? path : `'${path.replaceAll("'", "'\\''")}'`;
|
|
1209
|
+
}
|
|
1210
|
+
function shown(path) {
|
|
1211
|
+
return path.startsWith(`${process7.cwd()}/`) ? relative(process7.cwd(), path) : path;
|
|
1212
|
+
}
|
|
1213
|
+
async function fixAll(exposed, json) {
|
|
1214
|
+
const fixed = [];
|
|
1215
|
+
let left = false;
|
|
1216
|
+
for (const { path, readable, tracked, secrets } of exposed) {
|
|
1217
|
+
const fix = secrets === undefined ? undefined : await fixCompose(path);
|
|
1218
|
+
const open = fix?.open ?? (readable && !await shutOthersOut(path) ? [path] : []);
|
|
1219
|
+
if (fix !== undefined)
|
|
1220
|
+
fixed.push(fix);
|
|
1221
|
+
left ||= tracked || open.length > 0 || (fix?.left.length ?? 0) > 0;
|
|
1222
|
+
if (json)
|
|
1223
|
+
continue;
|
|
1224
|
+
const lines = [
|
|
1225
|
+
...fix === undefined || fix.moved.length === 0 ? [] : [`moved ${fix.moved.join(", ")} into ${shown(fix.env)}`],
|
|
1226
|
+
...fix?.left.map(({ key, why }) => `left ${key} in place: ${why}`) ?? [],
|
|
1227
|
+
...fix?.check === undefined ? [] : [CHECKS[fix.check]],
|
|
1228
|
+
...open.map((file) => `another account owns ${file === path ? "it" : shown(file)}, so it stays readable: sudo chmod o-rwx ${quoted2(shown(file))}`),
|
|
1229
|
+
...readable && !open.includes(path) ? ["now unreadable by other accounts"] : [],
|
|
1230
|
+
...tracked ? [`tracked by git: git rm --cached ${quoted2(shown(path))}, then ignore it`] : []
|
|
1231
|
+
];
|
|
1232
|
+
console.log(`${shown(path)}
|
|
1233
|
+
${lines.join(`
|
|
1234
|
+
`)}`);
|
|
1235
|
+
}
|
|
1236
|
+
return { fixed, left };
|
|
1237
|
+
}
|
|
1238
|
+
async function audit2(args) {
|
|
1239
|
+
const parsed = parse(args, usage, options);
|
|
1240
|
+
const roots = (parsed.positionals.length > 0 ? parsed.positionals : ["."]).map((dir) => resolve(dir));
|
|
1241
|
+
for (const dir of roots) {
|
|
1242
|
+
if (!await isFolder(dir))
|
|
1243
|
+
fail(`${dir} is not a folder`);
|
|
1244
|
+
}
|
|
1245
|
+
const { exposed, unread } = await audit(roots);
|
|
1246
|
+
const json = parsed.values.json === true;
|
|
1247
|
+
if (!json && unread.length > MAX_UNREAD_LISTED) {
|
|
1248
|
+
console.error(`could not read ${unread.length} folders, so they were not audited (--json lists them)`);
|
|
1249
|
+
} else if (!json) {
|
|
1250
|
+
for (const dir of unread)
|
|
1251
|
+
console.error(`could not read ${dir}, so it was not audited`);
|
|
1252
|
+
}
|
|
1253
|
+
if (parsed.values.fix === true && (json || exposed.length > 0)) {
|
|
1254
|
+
const { fixed, left } = await fixAll(exposed, json);
|
|
1255
|
+
if (json)
|
|
1256
|
+
printJson({ fixed, exposed: (await audit(roots)).exposed, unread });
|
|
1257
|
+
return left ? 1 : 0;
|
|
1258
|
+
}
|
|
1259
|
+
if (json) {
|
|
1260
|
+
printJson({ exposed, unread });
|
|
1261
|
+
return exposed.length > 0 ? 1 : 0;
|
|
1262
|
+
}
|
|
1263
|
+
if (exposed.length === 0) {
|
|
1264
|
+
console.log("no exposed .env file nor compose file writing a secret out");
|
|
1265
|
+
return 0;
|
|
1266
|
+
}
|
|
1267
|
+
const uid = process7.getuid?.();
|
|
1268
|
+
for (const { path, readable, tracked, secrets } of exposed) {
|
|
1269
|
+
const sudo = uid !== undefined && uid !== 0 && (await stat3(path)).uid !== uid ? "sudo " : "";
|
|
1270
|
+
const problems = [
|
|
1271
|
+
...secrets === undefined ? [] : [`writes out ${secrets.join(", ")}: ev audit --fix moves the values into a .env beside it`],
|
|
1272
|
+
...readable ? [`readable by any account: ${sudo}chmod o-rwx ${quoted2(shown(path))}`] : [],
|
|
1273
|
+
...tracked ? [`tracked by git: git rm --cached ${quoted2(shown(path))}, then ignore it`] : []
|
|
1274
|
+
];
|
|
1275
|
+
console.log(`${shown(path)}
|
|
1276
|
+
${problems.join(`
|
|
1277
|
+
`)}`);
|
|
1278
|
+
}
|
|
1279
|
+
return 1;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/commands/compare.ts
|
|
1283
|
+
var diffUsage = `ev diff <a> <b>
|
|
1284
|
+
|
|
1285
|
+
Names the keys set in one file and not the other, never their values.
|
|
1286
|
+
|
|
1287
|
+
Exits 0 when both set the same keys, 1 when they differ, 2 on a usage error.`;
|
|
1288
|
+
var missingUsage = `ev missing [file]
|
|
1289
|
+
|
|
1290
|
+
Names the keys the reference file sets that the file (.env by default) does not.
|
|
1291
|
+
|
|
1292
|
+
Exits 0 when none is missing, 1 when some are, 2 on a usage error.`;
|
|
1293
|
+
async function keys(path) {
|
|
1294
|
+
return new Set(entries(await readEnv(path)).keys());
|
|
1295
|
+
}
|
|
1296
|
+
async function diff(args) {
|
|
1297
|
+
const parsed = parse(args, diffUsage, {});
|
|
1298
|
+
const [a, b] = parsed.positionals;
|
|
1299
|
+
if (a === undefined || b === undefined)
|
|
1300
|
+
fail(`needs two files
|
|
1301
|
+
|
|
1302
|
+
${diffUsage}`);
|
|
1303
|
+
const left = await keys(a);
|
|
1304
|
+
const right = await keys(b);
|
|
1305
|
+
const lines = [
|
|
1306
|
+
...[...left].filter((key) => !right.has(key)).map((key) => `only in ${a}: ${key}`),
|
|
1307
|
+
...[...right].filter((key) => !left.has(key)).map((key) => `only in ${b}: ${key}`)
|
|
1308
|
+
];
|
|
1309
|
+
if (lines.length === 0)
|
|
1310
|
+
return 0;
|
|
1311
|
+
console.log(lines.join(`
|
|
1312
|
+
`));
|
|
1313
|
+
return 1;
|
|
1314
|
+
}
|
|
1315
|
+
async function missing(args) {
|
|
1316
|
+
const parsed = parse(args, missingUsage, { against: { type: "string", description: "the reference file (.env.example by default)" } });
|
|
1317
|
+
const [file = ".env"] = parsed.positionals;
|
|
1318
|
+
const against = parsed.values.against ?? ".env.example";
|
|
1319
|
+
const have = await keys(file);
|
|
1320
|
+
const absent = [...await keys(against)].filter((key) => !have.has(key));
|
|
1321
|
+
if (absent.length === 0)
|
|
1322
|
+
return 0;
|
|
1323
|
+
console.log(absent.map((key) => `${key} (set in ${against}, not in ${file})`).join(`
|
|
1324
|
+
`));
|
|
1325
|
+
return 1;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// src/commands/edit.ts
|
|
1329
|
+
var DEFAULT_FILE = ".env";
|
|
1330
|
+
var TRAILING_NEWLINE = /\r?\n$/;
|
|
1331
|
+
var setUsage = `ev set KEY [file]
|
|
1332
|
+
|
|
1333
|
+
Sets KEY in the file (.env by default, created when missing), replacing its line in place or appending one. The value is read from stdin, or from -F <file>, so it never appears in a command line; --empty sets it to an empty value.
|
|
1334
|
+
|
|
1335
|
+
Exits 0 when it wrote the key, 1 when it could not, 2 on a usage error.`;
|
|
1336
|
+
var unsetUsage = `ev unset KEY [file]
|
|
1337
|
+
|
|
1338
|
+
Removes every line setting KEY from the file (.env by default), leaving comments and other lines as they are.
|
|
1339
|
+
|
|
1340
|
+
Exits 0 when it removed the key, 1 when the file does not set it, 2 on a usage error.`;
|
|
1341
|
+
var mvUsage = `ev mv KEY --from <file> --to <file>
|
|
1342
|
+
|
|
1343
|
+
Moves KEY's line from one file to another, as written. Refuses to replace a key the target already sets unless --force.
|
|
1344
|
+
|
|
1345
|
+
Exits 0 when it moved the key, 1 when the source lacks it or the target already sets it, 2 on a usage error.`;
|
|
1346
|
+
async function readValue(from) {
|
|
1347
|
+
const input = await readInput(from ?? "-", { noStdin: "pipe the value on stdin or give -F <file>, it is never taken from the command line" });
|
|
1348
|
+
if (input === "")
|
|
1349
|
+
fail("the value is empty, nothing was written: --empty sets an empty value");
|
|
1350
|
+
return input.replace(TRAILING_NEWLINE, "");
|
|
1351
|
+
}
|
|
1352
|
+
async function readOrEmpty(path) {
|
|
1353
|
+
return await Bun.file(path).exists() ? readEnv(path) : { lines: [] };
|
|
1354
|
+
}
|
|
1355
|
+
async function set(args) {
|
|
1356
|
+
const parsed = parse(args, setUsage, {
|
|
1357
|
+
file: { type: "string", short: "F", description: "read the value from this file instead of stdin" },
|
|
1358
|
+
empty: { type: "boolean", description: "set an empty value, reading nothing" }
|
|
1359
|
+
});
|
|
1360
|
+
const [key, target = DEFAULT_FILE] = parsed.positionals;
|
|
1361
|
+
if (key === undefined)
|
|
1362
|
+
fail(`missing KEY
|
|
1363
|
+
|
|
1364
|
+
${setUsage}`);
|
|
1365
|
+
if (parsed.values.empty === true && parsed.values.file !== undefined)
|
|
1366
|
+
fail("--empty and -F exclude each other");
|
|
1367
|
+
const value = parsed.values.empty === true ? "" : await readValue(parsed.values.file);
|
|
1368
|
+
const file = await readOrEmpty(target);
|
|
1369
|
+
setLine(file, key, value);
|
|
1370
|
+
await writeEnv(target, file);
|
|
1371
|
+
console.log(`${target}: ${key}=${maskValue(key, value).replaceAll(`
|
|
1372
|
+
`, "\\n")}`);
|
|
1373
|
+
return 0;
|
|
1374
|
+
}
|
|
1375
|
+
async function unset(args) {
|
|
1376
|
+
const parsed = parse(args, unsetUsage, {});
|
|
1377
|
+
const [key, target = DEFAULT_FILE] = parsed.positionals;
|
|
1378
|
+
if (key === undefined)
|
|
1379
|
+
fail(`missing KEY
|
|
1380
|
+
|
|
1381
|
+
${unsetUsage}`);
|
|
1382
|
+
const file = await readEnv(target);
|
|
1383
|
+
if (removeKey(file, key) === 0)
|
|
1384
|
+
fail(`${key} is not set in ${target}`, 1);
|
|
1385
|
+
await writeEnv(target, file);
|
|
1386
|
+
console.log(`${target}: removed ${key}`);
|
|
1387
|
+
return 0;
|
|
1388
|
+
}
|
|
1389
|
+
async function mv(args) {
|
|
1390
|
+
const parsed = parse(args, mvUsage, {
|
|
1391
|
+
from: { type: "string", description: "the file to take KEY from" },
|
|
1392
|
+
to: { type: "string", description: "the file to put KEY in, created when missing" },
|
|
1393
|
+
force: { type: "boolean", description: "replace KEY when the target already sets it" }
|
|
1394
|
+
});
|
|
1395
|
+
const [key] = parsed.positionals;
|
|
1396
|
+
const { from, to, force } = parsed.values;
|
|
1397
|
+
if (key === undefined || from === undefined || to === undefined)
|
|
1398
|
+
fail(`needs KEY, --from and --to
|
|
1399
|
+
|
|
1400
|
+
${mvUsage}`);
|
|
1401
|
+
if (from === to)
|
|
1402
|
+
fail("--from and --to are the same file");
|
|
1403
|
+
const source = await readEnv(from);
|
|
1404
|
+
const line = lastLine(source, key);
|
|
1405
|
+
if (line === undefined)
|
|
1406
|
+
fail(`${key} is not set in ${from}`, 1);
|
|
1407
|
+
const target = await readOrEmpty(to);
|
|
1408
|
+
if (entries(target).has(key) && force !== true)
|
|
1409
|
+
fail(`${to} already sets ${key}, --force replaces it`, 1);
|
|
1410
|
+
setLine(target, key, line.value ?? "", line.raw);
|
|
1411
|
+
await writeEnv(to, target);
|
|
1412
|
+
removeKey(source, key);
|
|
1413
|
+
await writeEnv(from, source);
|
|
1414
|
+
console.log(`${key}: ${from} -> ${to}`);
|
|
1415
|
+
return 0;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/commands/read.ts
|
|
1419
|
+
var {$: $2 } = globalThis.Bun;
|
|
1420
|
+
var listUsage = `ev list [file]
|
|
1421
|
+
|
|
1422
|
+
Prints every key of the folder's .env* files, or of one file, with secret values masked: a few leading characters and the length under a key named like TOKEN, SECRET, PASSWORD, KEY, PRIVATE or DSN, and the password of any URL.
|
|
1423
|
+
|
|
1424
|
+
Exits 0 when it printed them, 1 when there is no .env* file, 2 on a usage error.`;
|
|
1425
|
+
var getUsage = `ev get KEY [file]
|
|
1426
|
+
|
|
1427
|
+
Prints the masked value of one key, from every .env* file that sets it or from one file.
|
|
1428
|
+
|
|
1429
|
+
Exits 0 when the key is set, 1 when it is not set anywhere, 2 on a usage error.`;
|
|
1430
|
+
function display(key, value) {
|
|
1431
|
+
return maskValue(key, value).replaceAll(`
|
|
1432
|
+
`, "\\n").replaceAll("\r", "\\r");
|
|
1433
|
+
}
|
|
1434
|
+
async function list(args) {
|
|
1435
|
+
const parsed = parse(args, listUsage, {});
|
|
1436
|
+
const [file] = parsed.positionals;
|
|
1437
|
+
const names = file === undefined ? await envFiles() : [file];
|
|
1438
|
+
if (names.length === 0)
|
|
1439
|
+
fail("no .env* file in this folder", 1);
|
|
1440
|
+
const rows = [];
|
|
1441
|
+
for (const name of names) {
|
|
1442
|
+
for (const [key, value] of entries(await readEnv(name)))
|
|
1443
|
+
rows.push([name, key, display(key, value)]);
|
|
1444
|
+
}
|
|
1445
|
+
if (rows.length > 0)
|
|
1446
|
+
console.log(table(rows));
|
|
1447
|
+
return 0;
|
|
1448
|
+
}
|
|
1449
|
+
async function get(args) {
|
|
1450
|
+
const parsed = parse(args, getUsage, {});
|
|
1451
|
+
const [key, file] = parsed.positionals;
|
|
1452
|
+
if (key === undefined)
|
|
1453
|
+
fail(`missing KEY
|
|
1454
|
+
|
|
1455
|
+
${getUsage}`);
|
|
1456
|
+
const names = file === undefined ? await envFiles() : [file];
|
|
1457
|
+
const rows = [];
|
|
1458
|
+
for (const name of names) {
|
|
1459
|
+
const value = entries(await readEnv(name)).get(key);
|
|
1460
|
+
if (value !== undefined)
|
|
1461
|
+
rows.push([name, display(key, value)]);
|
|
1462
|
+
}
|
|
1463
|
+
if (rows.length === 0)
|
|
1464
|
+
fail(`${key} is not set in ${file ?? "any .env* file"}`, 1);
|
|
1465
|
+
console.log(file === undefined ? table(rows) : rows[0]?.[1]);
|
|
1466
|
+
return 0;
|
|
1467
|
+
}
|
|
1468
|
+
var grepUsage = `ev grep [-i] <regex> [file...]
|
|
1469
|
+
|
|
1470
|
+
Prints the keys matching the regular expression, from the folder's .env* files or the files given, with their values masked as ev list does. The regex is matched against the key only, never the value: \`ev grep '^DATABASE_'\` instead of \`grep ^DATABASE_ .env\`.
|
|
1471
|
+
|
|
1472
|
+
Exits 0 when a key matches, 1 when none does, 2 on a usage error.`;
|
|
1473
|
+
var showUsage = `ev show <rev>:<file>
|
|
1474
|
+
|
|
1475
|
+
Prints every key of an env file as a git revision holds it, values masked as ev list does: \`ev show HEAD~1:api/.env\` instead of \`git show HEAD~1:api/.env\`.
|
|
1476
|
+
|
|
1477
|
+
Exits 0 when it printed them, 1 when git cannot show the file, 2 on a usage error.`;
|
|
1478
|
+
var REVISION = /^([^:]+):(.+)$/;
|
|
1479
|
+
async function grep(args) {
|
|
1480
|
+
const parsed = parse(args, grepUsage, { "ignore-case": { type: "boolean", short: "i", description: "match the key whatever its case" } });
|
|
1481
|
+
const [source, ...files] = parsed.positionals;
|
|
1482
|
+
if (source === undefined)
|
|
1483
|
+
fail(`missing the regex
|
|
1484
|
+
|
|
1485
|
+
${grepUsage}`);
|
|
1486
|
+
let pattern;
|
|
1487
|
+
try {
|
|
1488
|
+
pattern = new RegExp(source, parsed.values["ignore-case"] === true ? "i" : "");
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
fail(`${source} is no valid regular expression: ${error instanceof Error ? error.message : String(error)}`);
|
|
1491
|
+
}
|
|
1492
|
+
const names = files.length === 0 ? await envFiles() : files;
|
|
1493
|
+
const rows = [];
|
|
1494
|
+
for (const name of names) {
|
|
1495
|
+
for (const [key, value] of entries(await readEnv(name))) {
|
|
1496
|
+
if (pattern.test(key))
|
|
1497
|
+
rows.push([name, key, display(key, value)]);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (rows.length === 0)
|
|
1501
|
+
fail(`no key matches ${source} in ${names.join(", ") || "any .env* file"}`, 1);
|
|
1502
|
+
console.log(table(rows));
|
|
1503
|
+
return 0;
|
|
1504
|
+
}
|
|
1505
|
+
async function show(args) {
|
|
1506
|
+
const parsed = parse(args, showUsage, {});
|
|
1507
|
+
const [target] = parsed.positionals;
|
|
1508
|
+
const [, revision, path] = REVISION.exec(target ?? "") ?? [];
|
|
1509
|
+
if (revision === undefined || path === undefined)
|
|
1510
|
+
fail(`give <rev>:<file>
|
|
1511
|
+
|
|
1512
|
+
${showUsage}`);
|
|
1513
|
+
const shown = await $2`git show ${`${revision}:${path}`}`.quiet().nothrow();
|
|
1514
|
+
if (shown.exitCode !== 0)
|
|
1515
|
+
fail(`git show ${revision}:${path} failed: ${shown.stderr.toString().trim()}`, 1);
|
|
1516
|
+
const rows = [...entries(parseDotenv(shown.stdout.toString()))].map(([key, value]) => [key, display(key, value)]);
|
|
1517
|
+
if (rows.length > 0)
|
|
1518
|
+
console.log(table(rows));
|
|
1519
|
+
return 0;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/ev.ts
|
|
1523
|
+
process8.exitCode = await runCli({
|
|
1524
|
+
name: "ev",
|
|
1525
|
+
version,
|
|
1526
|
+
about: ".env files listed and edited with secrets masked",
|
|
1527
|
+
replaces: "cat .env, grep on env files and editing them by hand",
|
|
1528
|
+
examples: ["ev list", ["ev get DATABASE_URL", "one key, its value masked"]],
|
|
1529
|
+
commands: {
|
|
1530
|
+
list: { run: list, summary: "every key of the .env* files, secrets masked" },
|
|
1531
|
+
get: { run: get, summary: "one key's masked value" },
|
|
1532
|
+
grep: { run: grep, summary: "the keys matching a regex, values masked" },
|
|
1533
|
+
show: { run: show, summary: "an env file at a git revision, values masked" },
|
|
1534
|
+
set: { run: set, summary: "set a key, the value read from stdin or -F" },
|
|
1535
|
+
unset: { run: unset, summary: "remove a key from a file" },
|
|
1536
|
+
mv: { run: mv, summary: "move a key from one file to another" },
|
|
1537
|
+
diff: { run: diff, summary: "keys set in one file and not the other" },
|
|
1538
|
+
missing: { run: missing, summary: "keys of .env.example that .env lacks" },
|
|
1539
|
+
audit: { run: audit2, summary: ".env files others can read or git tracks, compose files writing secrets out" }
|
|
1540
|
+
},
|
|
1541
|
+
aliases: { ls: "list" }
|
|
1542
|
+
});
|