prae-cli 0.9.2 → 0.10.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/lib/bin.js +2 -6
- package/lib/{checkpoint-C_q_Rpa5.js → checkpoint-BLOGdWFu.js} +1 -1
- package/lib/cli-BlWTUnGJ.js +6 -0
- package/lib/{cli-B_B6-bUZ.js → cli-C_6dosaw.js} +629 -534
- package/lib/docs/pilot-claude-code.md +192 -0
- package/lib/hook-DxLgNtc4.js +693 -0
- package/lib/index.js +4 -4
- package/lib/init-BPlBJvA-.js +272 -0
- package/lib/{instructions-ya0LVnZy.js → instructions-fCJ4j7ce.js} +2 -1
- package/lib/{mcp-BCXbpqu4.js → mcp-DdEDvTvR.js} +1 -1
- package/lib/playground.js +717 -109
- package/lib/prae-baseline.prae.json +144 -1
- package/lib/{receipt-check-DQK_gVPc.js → receipt-check-2F4zLEW5.js} +1 -1
- package/lib/src-2iJy5MZc.js +3 -0
- package/lib/src-CZYvvEn5.js +1691 -0
- package/lib/{src-LA7NdLKb.js → src-CuiWC80o.js} +104 -25
- package/lib/{test-run-CCDgJ0Dp.js → test-run-BGsK-inZ.js} +1 -1
- package/lib/views-C7DD-Oif.js +1396 -0
- package/package.json +6 -6
- package/lib/evaluate-BqLxhYiP.js +0 -499
- package/lib/src-7X6dO7gC.js +0 -3
- package/lib/views-D4UEfbzt.js +0 -1505
- /package/lib/{ocsf-DhVGbBUw.js → ocsf-BwWyM7hc.js} +0 -0
- /package/lib/{src-CBGZVvt7.js → src-BoBCmiwj.js} +0 -0
- /package/lib/{src-d4c_n6wd.js → src-XukJfuml.js} +0 -0
package/lib/playground.js
CHANGED
|
@@ -2,124 +2,676 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
//#region ../kernel/src/command.ts
|
|
5
|
-
/** Wrappers whose first operand is the program
|
|
6
|
-
const WRAPPERS = new
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
/** Wrappers whose first operand is the program, and their options that take a value. */
|
|
6
|
+
const WRAPPERS = new Map(Object.entries({
|
|
7
|
+
sudo: [
|
|
8
|
+
"-u",
|
|
9
|
+
"-g",
|
|
10
|
+
"-p",
|
|
11
|
+
"-h",
|
|
12
|
+
"-C",
|
|
13
|
+
"-r",
|
|
14
|
+
"-t",
|
|
15
|
+
"-U",
|
|
16
|
+
"-D",
|
|
17
|
+
"-T",
|
|
18
|
+
"-R"
|
|
19
|
+
],
|
|
20
|
+
doas: ["-u", "-C"],
|
|
21
|
+
env: [
|
|
22
|
+
"-u",
|
|
23
|
+
"-C",
|
|
24
|
+
"-P",
|
|
25
|
+
"--unset",
|
|
26
|
+
"--chdir"
|
|
27
|
+
],
|
|
28
|
+
command: [],
|
|
29
|
+
builtin: [],
|
|
30
|
+
exec: ["-a"],
|
|
31
|
+
nohup: [],
|
|
32
|
+
time: ["-f", "-o"],
|
|
33
|
+
nice: ["-n"],
|
|
34
|
+
timeout: [
|
|
35
|
+
"-s",
|
|
36
|
+
"-k",
|
|
37
|
+
"--signal",
|
|
38
|
+
"--kill-after"
|
|
39
|
+
],
|
|
40
|
+
setsid: [],
|
|
41
|
+
stdbuf: [
|
|
42
|
+
"-i",
|
|
43
|
+
"-o",
|
|
44
|
+
"-e"
|
|
45
|
+
],
|
|
46
|
+
xargs: [
|
|
47
|
+
"-I",
|
|
48
|
+
"-n",
|
|
49
|
+
"-P",
|
|
50
|
+
"-L",
|
|
51
|
+
"-s",
|
|
52
|
+
"-d",
|
|
53
|
+
"-E",
|
|
54
|
+
"-a",
|
|
55
|
+
"--arg-file"
|
|
56
|
+
]
|
|
57
|
+
}).map(([name, options]) => [name, new Set(options)]));
|
|
58
|
+
const SHELLS = new Set([
|
|
59
|
+
"sh",
|
|
60
|
+
"bash",
|
|
61
|
+
"zsh",
|
|
62
|
+
"dash",
|
|
63
|
+
"ksh",
|
|
64
|
+
"mksh",
|
|
65
|
+
"ash"
|
|
13
66
|
]);
|
|
14
|
-
/**
|
|
15
|
-
const
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"-P",
|
|
27
|
-
"-L",
|
|
28
|
-
"-s"
|
|
67
|
+
/** Words that open or continue a compound command, not a program. */
|
|
68
|
+
const RESERVED = new Set([
|
|
69
|
+
"!",
|
|
70
|
+
"if",
|
|
71
|
+
"then",
|
|
72
|
+
"elif",
|
|
73
|
+
"else",
|
|
74
|
+
"fi",
|
|
75
|
+
"while",
|
|
76
|
+
"until",
|
|
77
|
+
"do",
|
|
78
|
+
"done"
|
|
29
79
|
]);
|
|
30
|
-
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]
|
|
80
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*\+?=/;
|
|
81
|
+
/** Scripts inside scripts (`bash -c`, `eval`, backticks, here-documents). */
|
|
82
|
+
const MAX_DEPTH = 8;
|
|
83
|
+
/** Brackets inside one script. */
|
|
84
|
+
const MAX_NESTING = 64;
|
|
85
|
+
/** A part of the line that cannot be read with confidence. */
|
|
86
|
+
var Undecomposable = class extends Error {};
|
|
87
|
+
const basename = (word) => (word.split("/").pop() ?? word).toLowerCase();
|
|
88
|
+
const isBlank = (c) => c === " " || c === " " || c === "\r";
|
|
89
|
+
const isSpecial = (c) => /^[0-9@*#?$!-]$/.test(c);
|
|
90
|
+
const expands = (w) => /[*?]|\[.*\]|\{.*(,|\.\.).*\}/.test(w.bare);
|
|
31
91
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* Splitting on `|` before parsing quotes is the classic mistake — it turns
|
|
35
|
-
* `echo "a | b"` into two segments — so quoting and separators are read
|
|
36
|
-
* together.
|
|
92
|
+
* Read one script into `out`. `expansionsOnly` reads it as the body of an
|
|
93
|
+
* unquoted here-document: text, in which only substitutions run.
|
|
37
94
|
*/
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
let
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
95
|
+
function read(src, out, depth, expansionsOnly = false) {
|
|
96
|
+
if (depth > MAX_DEPTH) throw new Undecomposable(`scripts nested more than ${MAX_DEPTH} deep`);
|
|
97
|
+
let at = 0;
|
|
98
|
+
let nesting = 0;
|
|
99
|
+
/** Commands here read their standard input from a pipe. */
|
|
100
|
+
let piped = false;
|
|
101
|
+
const pending = [];
|
|
102
|
+
const ch = (k = 0) => src[at + k] ?? "";
|
|
103
|
+
const ahead = (s) => src.startsWith(s, at);
|
|
104
|
+
const ends = (k) => {
|
|
105
|
+
const c = ch(k);
|
|
106
|
+
return c === "" || isBlank(c) || c === "\n" || ";&|()<>".includes(c);
|
|
46
107
|
};
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
tokens = [];
|
|
108
|
+
const deeper = () => {
|
|
109
|
+
nesting += 1;
|
|
110
|
+
if (nesting > MAX_NESTING) throw new Undecomposable(`brackets nested more than ${MAX_NESTING} deep`);
|
|
51
111
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
112
|
+
const fresh = () => ({
|
|
113
|
+
text: "",
|
|
114
|
+
literal: true,
|
|
115
|
+
quoted: false,
|
|
116
|
+
bare: ""
|
|
117
|
+
});
|
|
118
|
+
/** A script inside this one: its unreadable parts are recorded, not fatal. */
|
|
119
|
+
const nested = (text, onlyExpansions = false) => {
|
|
120
|
+
try {
|
|
121
|
+
read(text, out, depth + 1, onlyExpansions);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (!(error instanceof Undecomposable)) throw error;
|
|
124
|
+
out.undecomposable.push(error.message);
|
|
58
125
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
126
|
+
};
|
|
127
|
+
function blanks() {
|
|
128
|
+
for (;;) {
|
|
129
|
+
const c = ch();
|
|
130
|
+
if (isBlank(c)) at += 1;
|
|
131
|
+
else if (c === "\\" && ch(1) === "\n") at += 2;
|
|
132
|
+
else if (c === "#") while (at < src.length && ch() !== "\n") at += 1;
|
|
133
|
+
else return;
|
|
62
134
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
135
|
+
}
|
|
136
|
+
/** One unquoted newline, then the here-document bodies it starts. */
|
|
137
|
+
function newline() {
|
|
138
|
+
at += 1;
|
|
139
|
+
for (let doc = pending.shift(); doc; doc = pending.shift()) body(doc);
|
|
140
|
+
}
|
|
141
|
+
function body(doc) {
|
|
142
|
+
let text = "";
|
|
143
|
+
while (at < src.length) {
|
|
144
|
+
const stop = src.indexOf("\n", at);
|
|
145
|
+
const end = stop === -1 ? src.length : stop;
|
|
146
|
+
const line = src.slice(at, end);
|
|
147
|
+
at = Math.min(end + 1, src.length);
|
|
148
|
+
if ((doc.strip ? line.replace(/^\t+/, "") : line) === doc.delim) break;
|
|
149
|
+
text += `${line}\n`;
|
|
66
150
|
}
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
151
|
+
if (doc.feed.shell !== void 0) if (!doc.quoted && /[$`\\]/.test(text)) out.undecomposable.push(`a here-document with expansions is run by ${doc.feed.shell}`);
|
|
152
|
+
else nested(text);
|
|
153
|
+
else if (!doc.quoted) nested(text, true);
|
|
154
|
+
}
|
|
155
|
+
function list$1(end) {
|
|
156
|
+
for (;;) {
|
|
157
|
+
blanks();
|
|
158
|
+
const c = ch();
|
|
159
|
+
if (c === "") {
|
|
160
|
+
if (end === void 0) return;
|
|
161
|
+
throw new Undecomposable(end === ")" ? "an unclosed `(`" : "an unclosed `{`");
|
|
162
|
+
}
|
|
163
|
+
if (c === "\n") newline();
|
|
164
|
+
else if (c === ";") {
|
|
165
|
+
if (ch(1) === ";") throw new Undecomposable("`;;` outside a case statement");
|
|
166
|
+
at += 1;
|
|
167
|
+
} else if (c === "&" && ch(1) !== ">") at += ch(1) === "&" ? 2 : 1;
|
|
168
|
+
else if (c === "|" && ch(1) === "|") at += 2;
|
|
169
|
+
else if (c === "|") throw new Undecomposable("a pipe with nothing before it");
|
|
170
|
+
else if (c === ")") {
|
|
171
|
+
if (end !== ")") throw new Undecomposable("an unbalanced `)`");
|
|
172
|
+
at += 1;
|
|
173
|
+
return;
|
|
174
|
+
} else if (end === "}" && c === "}" && ends(1)) {
|
|
175
|
+
at += 1;
|
|
176
|
+
return;
|
|
177
|
+
} else pipeline();
|
|
71
178
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
179
|
+
}
|
|
180
|
+
function pipeline() {
|
|
181
|
+
const names = [];
|
|
182
|
+
for (let position = 0;; position += 1) {
|
|
183
|
+
names.push(...command(position > 0 || piped));
|
|
184
|
+
blanks();
|
|
185
|
+
if (ch() !== "|" || ch(1) === "|") break;
|
|
186
|
+
at += ch(1) === "&" ? 2 : 1;
|
|
187
|
+
for (blanks(); ch() === "\n"; blanks()) newline();
|
|
75
188
|
}
|
|
76
|
-
|
|
189
|
+
if (names.length > 0) out.pipelines.push(names.join("|"));
|
|
77
190
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
191
|
+
function command(fromPipe) {
|
|
192
|
+
blanks();
|
|
193
|
+
if (ahead("((") && arithmetic()) {
|
|
194
|
+
trailing();
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
if (ch() === "(") {
|
|
198
|
+
at += 1;
|
|
199
|
+
return compound(")", fromPipe);
|
|
200
|
+
}
|
|
201
|
+
if (ch() === "{" && (isBlank(ch(1)) || ch(1) === "\n")) {
|
|
202
|
+
at += 1;
|
|
203
|
+
return compound("}", fromPipe);
|
|
204
|
+
}
|
|
205
|
+
return simple(fromPipe);
|
|
206
|
+
}
|
|
207
|
+
function compound(end, fromPipe) {
|
|
208
|
+
deeper();
|
|
209
|
+
const before = out.segments.length;
|
|
210
|
+
const outer = piped;
|
|
211
|
+
piped = fromPipe;
|
|
212
|
+
list$1(end);
|
|
213
|
+
piped = outer;
|
|
214
|
+
nesting -= 1;
|
|
215
|
+
trailing();
|
|
216
|
+
return out.segments.slice(before).map((s) => s.name);
|
|
217
|
+
}
|
|
218
|
+
/** Redirections after a compound command; anything else is not shell. */
|
|
219
|
+
function trailing() {
|
|
220
|
+
blanks();
|
|
221
|
+
while (redirection() !== void 0) blanks();
|
|
222
|
+
if (!ends(0) && ch() !== "}") throw new Undecomposable("a word after a compound command");
|
|
223
|
+
}
|
|
224
|
+
function simple(fromPipe) {
|
|
225
|
+
const mark = out.segments.length;
|
|
226
|
+
const words = [];
|
|
227
|
+
const redirects = [];
|
|
228
|
+
const feed = {};
|
|
229
|
+
let runsNothing = false;
|
|
230
|
+
let conditional = false;
|
|
231
|
+
for (;;) {
|
|
232
|
+
blanks();
|
|
233
|
+
const c = ch();
|
|
234
|
+
if (conditional) {
|
|
235
|
+
if (c === "") throw new Undecomposable("an unclosed `[[`");
|
|
236
|
+
if (c === "\n") {
|
|
237
|
+
newline();
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (ahead("]]") && ends(2)) {
|
|
241
|
+
at += 2;
|
|
242
|
+
conditional = false;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const op = /^(&&|\|\||[()<>!])/.exec(src.slice(at, at + 2));
|
|
246
|
+
if (op !== null) {
|
|
247
|
+
at += op[0].length;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (c === "" || c === "\n" || c === ";" || c === ")" || c === "|") break;
|
|
252
|
+
if (c === "&" && ch(1) !== ">") break;
|
|
253
|
+
if (c === "(") {
|
|
254
|
+
if (ahead("((")) {
|
|
255
|
+
if (arithmetic()) continue;
|
|
256
|
+
throw new Undecomposable("an unexpected `(`");
|
|
257
|
+
}
|
|
258
|
+
if (words.length === 1) {
|
|
259
|
+
at += 1;
|
|
260
|
+
blanks();
|
|
261
|
+
if (ch() === ")") {
|
|
262
|
+
at += 1;
|
|
263
|
+
for (blanks(); ch() === "\n"; blanks()) newline();
|
|
264
|
+
return command(fromPipe);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
throw new Undecomposable("an unexpected `(`");
|
|
268
|
+
}
|
|
269
|
+
const redirected = redirection(feed);
|
|
270
|
+
if (redirected !== void 0) {
|
|
271
|
+
redirects.push(...redirected);
|
|
91
272
|
continue;
|
|
92
273
|
}
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
|
|
274
|
+
const w = word();
|
|
275
|
+
if (w === void 0) throw new Undecomposable("an unexpected character");
|
|
276
|
+
if (words.length === 0 && w.literal && !w.quoted) {
|
|
277
|
+
if (RESERVED.has(w.text)) return command(fromPipe);
|
|
278
|
+
if (w.text === "case" || w.text === "coproc") throw new Undecomposable(`a \`${w.text}\` statement`);
|
|
279
|
+
if (w.text === "function") {
|
|
280
|
+
blanks();
|
|
281
|
+
word();
|
|
282
|
+
blanks();
|
|
283
|
+
if (ahead("()")) at += 2;
|
|
284
|
+
for (blanks(); ch() === "\n"; blanks()) newline();
|
|
285
|
+
return command(fromPipe);
|
|
286
|
+
}
|
|
287
|
+
if (w.text === "for" || w.text === "select") runsNothing = true;
|
|
288
|
+
if (w.text === "[[") {
|
|
289
|
+
runsNothing = true;
|
|
290
|
+
conditional = true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
words.push(w);
|
|
294
|
+
}
|
|
295
|
+
if (runsNothing || words.length === 0) return [];
|
|
296
|
+
const name = resolve(words, redirects, [], false, mark, feed, fromPipe);
|
|
297
|
+
return name === void 0 ? [] : [name];
|
|
298
|
+
}
|
|
299
|
+
/** A redirection at `at`: the operator and its target, as tokens. */
|
|
300
|
+
function redirection(feed) {
|
|
301
|
+
if ((ch() === "<" || ch() === ">") && ch(1) === "(") return void 0;
|
|
302
|
+
const m = /^(\d*)(<<<|<<-|<<|<>|<&|>>|>\||>&|&>>|&>|<|>)/.exec(src.slice(at, at + 12));
|
|
303
|
+
if (m === null) return void 0;
|
|
304
|
+
at += m[0].length;
|
|
305
|
+
blanks();
|
|
306
|
+
const target = word();
|
|
307
|
+
if (target === void 0) throw new Undecomposable("a redirection with no target");
|
|
308
|
+
const op = m[2];
|
|
309
|
+
if (op === "<<" || op === "<<-") pending.push({
|
|
310
|
+
delim: target.text,
|
|
311
|
+
strip: op === "<<-",
|
|
312
|
+
quoted: target.quoted,
|
|
313
|
+
feed: feed ?? {}
|
|
314
|
+
});
|
|
315
|
+
else if (op === "<<<" && feed !== void 0) feed.herestring = target;
|
|
316
|
+
if (op === ">&" || op === "<&") return [`${m[0]}${target.text}`];
|
|
317
|
+
return [m[0], target.text];
|
|
318
|
+
}
|
|
319
|
+
/** One word at `at`, quotes and expansions read; undefined at an operator. */
|
|
320
|
+
function word() {
|
|
321
|
+
const w = fresh();
|
|
322
|
+
const start$1 = at;
|
|
323
|
+
for (;;) {
|
|
324
|
+
const c = ch();
|
|
325
|
+
if (c === "" || isBlank(c) || c === "\n" || ";&|()".includes(c)) break;
|
|
326
|
+
if (c === "<" || c === ">") {
|
|
327
|
+
if (ch(1) !== "(") break;
|
|
328
|
+
const from = at;
|
|
329
|
+
at += 2;
|
|
330
|
+
substitution();
|
|
331
|
+
w.text += src.slice(from, at);
|
|
332
|
+
w.literal = false;
|
|
333
|
+
} else if (c === "\\") {
|
|
334
|
+
if (ch(1) !== "\n") {
|
|
335
|
+
w.text += ch(1);
|
|
336
|
+
w.quoted = true;
|
|
337
|
+
}
|
|
338
|
+
at += 2;
|
|
339
|
+
} else if (c === "'") {
|
|
340
|
+
const end = src.indexOf("'", at + 1);
|
|
341
|
+
if (end === -1) throw new Undecomposable("an unbalanced single quote");
|
|
342
|
+
w.text += src.slice(at + 1, end);
|
|
343
|
+
w.quoted = true;
|
|
344
|
+
at = end + 1;
|
|
345
|
+
} else if (c === "$" && ch(1) === "'") ansi(w);
|
|
346
|
+
else if (c === "$" && ch(1) === "\"") {
|
|
347
|
+
at += 1;
|
|
348
|
+
double(w, true);
|
|
349
|
+
} else if (c === "\"") double(w, true);
|
|
350
|
+
else if (c === "`") backtick(w, false);
|
|
351
|
+
else if (c === "$") dollar(w);
|
|
352
|
+
else {
|
|
353
|
+
w.text += c;
|
|
354
|
+
w.bare += c;
|
|
355
|
+
at += 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return at === start$1 ? void 0 : w;
|
|
359
|
+
}
|
|
360
|
+
/** `"…"` (or, unclosed, a here-document body): only `$` and backticks are live. */
|
|
361
|
+
function double(w, closed) {
|
|
362
|
+
w.quoted = true;
|
|
363
|
+
if (closed) at += 1;
|
|
364
|
+
for (;;) {
|
|
365
|
+
const c = ch();
|
|
366
|
+
if (c === "") {
|
|
367
|
+
if (closed) throw new Undecomposable("an unbalanced double quote");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (c === "\"" && closed) {
|
|
371
|
+
at += 1;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const n = ch(1);
|
|
375
|
+
if (c === "\\" && n !== "" && (n === "\n" || "$`\"\\".includes(n))) {
|
|
376
|
+
if (n !== "\n") w.text += n;
|
|
377
|
+
at += 2;
|
|
378
|
+
} else if (c === "`") backtick(w, closed);
|
|
379
|
+
else if (c === "$") dollar(w);
|
|
380
|
+
else {
|
|
381
|
+
w.text += c;
|
|
96
382
|
at += 1;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** `$'…'`: escapes can spell anything, so one with escapes is not a literal. */
|
|
387
|
+
function ansi(w) {
|
|
388
|
+
let k = at + 2;
|
|
389
|
+
for (;;) {
|
|
390
|
+
const c = src[k];
|
|
391
|
+
if (c === void 0) throw new Undecomposable("an unbalanced `$'` quote");
|
|
392
|
+
if (c === "'") break;
|
|
393
|
+
if (c === "\\") {
|
|
394
|
+
w.literal = false;
|
|
395
|
+
w.text += c + (src[k + 1] ?? "");
|
|
396
|
+
k += 2;
|
|
397
|
+
} else {
|
|
398
|
+
w.text += c;
|
|
399
|
+
k += 1;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
w.quoted = true;
|
|
403
|
+
at = k + 1;
|
|
404
|
+
}
|
|
405
|
+
/** `$(…)`, `$((…))`, `${…}`, `$NAME`; a lone `$` is a dollar sign. */
|
|
406
|
+
function dollar(w) {
|
|
407
|
+
const from = at;
|
|
408
|
+
if (ahead("$(")) {
|
|
409
|
+
if (!(ahead("$((") && arithmetic())) {
|
|
410
|
+
at += 2;
|
|
411
|
+
substitution();
|
|
412
|
+
}
|
|
413
|
+
} else if (ahead("${")) {
|
|
414
|
+
at += 2;
|
|
415
|
+
parameter();
|
|
416
|
+
} else if (/[A-Za-z_]/.test(ch(1))) {
|
|
417
|
+
at += 1;
|
|
418
|
+
while (/[A-Za-z0-9_]/.test(ch())) at += 1;
|
|
419
|
+
} else if (isSpecial(ch(1))) at += 2;
|
|
420
|
+
else {
|
|
421
|
+
w.text += "$";
|
|
422
|
+
w.bare += "$";
|
|
423
|
+
at += 1;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
w.text += src.slice(from, at);
|
|
427
|
+
w.literal = false;
|
|
428
|
+
}
|
|
429
|
+
/** After `$(`, `<(` or `>(`: a list up to its `)`, with its own stdin. */
|
|
430
|
+
function substitution() {
|
|
431
|
+
deeper();
|
|
432
|
+
const outer = piped;
|
|
433
|
+
piped = false;
|
|
434
|
+
list$1(")");
|
|
435
|
+
piped = outer;
|
|
436
|
+
nesting -= 1;
|
|
437
|
+
}
|
|
438
|
+
/** After `${`: up to the matching `}`; a default value can run a command. */
|
|
439
|
+
function parameter() {
|
|
440
|
+
deeper();
|
|
441
|
+
const sink = fresh();
|
|
442
|
+
for (;;) {
|
|
443
|
+
const c = ch();
|
|
444
|
+
if (c === "") throw new Undecomposable("an unclosed `${`");
|
|
445
|
+
if (c === "}") break;
|
|
446
|
+
if (c === "\\") at += 2;
|
|
447
|
+
else if (c === "'") {
|
|
448
|
+
const end = src.indexOf("'", at + 1);
|
|
449
|
+
if (end === -1) throw new Undecomposable("an unbalanced single quote");
|
|
450
|
+
at = end + 1;
|
|
451
|
+
} else if (c === "\"") double(sink, true);
|
|
452
|
+
else if (c === "`") backtick(sink, false);
|
|
453
|
+
else if (c === "$") dollar(sink);
|
|
454
|
+
else at += 1;
|
|
455
|
+
}
|
|
456
|
+
at += 1;
|
|
457
|
+
nesting -= 1;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Where `((…))` / `$((…))` at `at` ends, if bash reads it as arithmetic:
|
|
461
|
+
* only when the `)` matching the second `(` is followed at once by `)`.
|
|
462
|
+
* Otherwise bash reads `(` `(` — nested subshells, or `$(` and a subshell —
|
|
463
|
+
* and `((rm -rf /) )` runs rm. Quotes and backticks are skipped whole;
|
|
464
|
+
* anything unclosed is not arithmetic, so it is read the stricter way.
|
|
465
|
+
*/
|
|
466
|
+
function arithmeticEnd() {
|
|
467
|
+
let depth$1 = 1;
|
|
468
|
+
for (let k = at + (ch() === "$" ? 3 : 2); k < src.length; k += 1) {
|
|
469
|
+
const c = src[k];
|
|
470
|
+
if (c === "\\") k += 1;
|
|
471
|
+
else if (c === "'" || c === "\"" || c === "`") {
|
|
472
|
+
let end = k + 1;
|
|
473
|
+
while (end < src.length && src[end] !== c) {
|
|
474
|
+
if (src[end] === "\\" && c !== "'") end += 1;
|
|
475
|
+
end += 1;
|
|
101
476
|
}
|
|
477
|
+
if (end >= src.length) return void 0;
|
|
478
|
+
k = end;
|
|
479
|
+
} else if (c === "(") depth$1 += 1;
|
|
480
|
+
else if (c === ")") {
|
|
481
|
+
depth$1 -= 1;
|
|
482
|
+
if (depth$1 === 0) return src[k + 1] === ")" ? k + 2 : void 0;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* `((…))` or `$((…))` read as arithmetic, in which a substitution still
|
|
488
|
+
* runs; false, and nothing read, when bash would not read it so.
|
|
489
|
+
*/
|
|
490
|
+
function arithmetic() {
|
|
491
|
+
const end = arithmeticEnd();
|
|
492
|
+
if (end === void 0) return false;
|
|
493
|
+
deeper();
|
|
494
|
+
at += ch() === "$" ? 3 : 2;
|
|
495
|
+
const sink = fresh();
|
|
496
|
+
while (at < end) {
|
|
497
|
+
const c = ch();
|
|
498
|
+
if (c === "$") dollar(sink);
|
|
499
|
+
else if (c === "`") backtick(sink, false);
|
|
500
|
+
else at += 1;
|
|
501
|
+
}
|
|
502
|
+
if (at !== end) throw new Undecomposable("an ambiguous `((`");
|
|
503
|
+
nesting -= 1;
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
/** `` `…` ``: its text, unescaped, is a script of its own. */
|
|
507
|
+
function backtick(w, inDouble) {
|
|
508
|
+
let inner = "";
|
|
509
|
+
let k = at + 1;
|
|
510
|
+
for (;;) {
|
|
511
|
+
const c = src[k];
|
|
512
|
+
if (c === void 0) throw new Undecomposable("an unbalanced backtick");
|
|
513
|
+
if (c === "`") break;
|
|
514
|
+
const n = src[k + 1] ?? "";
|
|
515
|
+
const escaped = "`$\\".includes(n) || inDouble && n === "\"";
|
|
516
|
+
if (c === "\\" && n !== "" && escaped) {
|
|
517
|
+
inner += n;
|
|
518
|
+
k += 2;
|
|
519
|
+
} else {
|
|
520
|
+
inner += c;
|
|
521
|
+
k += 1;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
w.text += src.slice(at, k + 1);
|
|
525
|
+
w.literal = false;
|
|
526
|
+
at = k + 1;
|
|
527
|
+
nested(inner);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Words to the program they run, inserted at `insertAt` so a command comes
|
|
531
|
+
* before the substitutions in its words; then whatever that program runs in
|
|
532
|
+
* turn (`bash -c`, `eval`, `find -exec`).
|
|
533
|
+
*/
|
|
534
|
+
function resolve(words, redirects, seen, open, insertAt, feed, fromPipe = false) {
|
|
535
|
+
const wrappers = [...seen];
|
|
536
|
+
let openEnded = open;
|
|
537
|
+
let replace;
|
|
538
|
+
let k = 0;
|
|
539
|
+
while (k < words.length) {
|
|
540
|
+
const w = words[k];
|
|
541
|
+
if (ASSIGNMENT.test(w.text)) {
|
|
542
|
+
k += 1;
|
|
102
543
|
continue;
|
|
103
544
|
}
|
|
104
|
-
break;
|
|
545
|
+
if (!w.literal || expands(w)) break;
|
|
546
|
+
const base = basename(w.text);
|
|
547
|
+
const options = WRAPPERS.get(base);
|
|
548
|
+
if (options === void 0) break;
|
|
549
|
+
wrappers.push(base);
|
|
550
|
+
if (base === "xargs") openEnded = true;
|
|
551
|
+
k += 1;
|
|
552
|
+
while (k < words.length) {
|
|
553
|
+
const option = words[k].text;
|
|
554
|
+
if (option === "--") {
|
|
555
|
+
k += 1;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
if (!option.startsWith("-")) break;
|
|
559
|
+
if (base === "env" && /^(-S|--split-string)/.test(option)) {
|
|
560
|
+
out.undecomposable.push("`env -S` splits a string into a command");
|
|
561
|
+
return insert(insertAt, {
|
|
562
|
+
name: base,
|
|
563
|
+
tokens: [],
|
|
564
|
+
wrappers
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
if (base === "xargs" && option.startsWith("-I")) replace = option.length > 2 ? option.slice(2) : words[k + 1]?.text;
|
|
568
|
+
k += options.has(option) ? 2 : 1;
|
|
569
|
+
}
|
|
570
|
+
if (base === "timeout") k += 1;
|
|
105
571
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
572
|
+
const program = words[k];
|
|
573
|
+
const rest = words.slice(k + 1);
|
|
574
|
+
const tokens = [...rest.map((w) => w.text), ...redirects];
|
|
575
|
+
const last = wrappers.at(-1);
|
|
576
|
+
if (program === void 0) return last === void 0 || wrappers.length === seen.length ? void 0 : insert(insertAt, {
|
|
577
|
+
name: last,
|
|
578
|
+
tokens: [...redirects],
|
|
579
|
+
wrappers
|
|
580
|
+
});
|
|
581
|
+
if (!program.literal || expands(program) || openEnded && (program.text.includes("{}") || replace !== void 0 && replace !== "" && program.text.includes(replace))) {
|
|
582
|
+
out.undecomposable.push("the program word is only known at run time");
|
|
583
|
+
if (last !== void 0) insert(insertAt, {
|
|
110
584
|
name: last,
|
|
111
|
-
tokens: [],
|
|
585
|
+
tokens: [program.text, ...tokens],
|
|
112
586
|
wrappers
|
|
113
587
|
});
|
|
114
|
-
|
|
588
|
+
return;
|
|
115
589
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
590
|
+
const name = basename(program.text);
|
|
591
|
+
insert(insertAt, {
|
|
592
|
+
name,
|
|
593
|
+
tokens,
|
|
594
|
+
wrappers,
|
|
595
|
+
...openEnded ? { openEnded: true } : {}
|
|
120
596
|
});
|
|
597
|
+
if (SHELLS.has(name)) shell(name, rest, feed, fromPipe);
|
|
598
|
+
else if (name === "eval") if (rest.every((w) => w.literal)) nested(rest.map((w) => w.text).join(" "));
|
|
599
|
+
else out.undecomposable.push("`eval` of a string that is not a literal");
|
|
600
|
+
else if (name === "source" || name === ".") {
|
|
601
|
+
if (rest[0] !== void 0 && !rest[0].literal) out.undecomposable.push(`\`${name}\` of a file only known at run time`);
|
|
602
|
+
} else if (name === "find") for (let i = 0; i < rest.length; i += 1) {
|
|
603
|
+
const flag = rest[i].text;
|
|
604
|
+
if (![
|
|
605
|
+
"-exec",
|
|
606
|
+
"-execdir",
|
|
607
|
+
"-ok",
|
|
608
|
+
"-okdir"
|
|
609
|
+
].includes(flag)) continue;
|
|
610
|
+
let end = i + 1;
|
|
611
|
+
while (end < rest.length && !/^[;+]$/.test(rest[end].text)) end += 1;
|
|
612
|
+
resolve(rest.slice(i + 1, end), [], [...wrappers, "find"], true, out.segments.length);
|
|
613
|
+
i = end;
|
|
614
|
+
}
|
|
615
|
+
return name;
|
|
616
|
+
}
|
|
617
|
+
function insert(where, segment) {
|
|
618
|
+
out.segments.splice(where, 0, segment);
|
|
619
|
+
return segment.name;
|
|
620
|
+
}
|
|
621
|
+
/** What a shell program runs: a `-c` string, a script file, or its stdin. */
|
|
622
|
+
function shell(name, args, feed, fromPipe) {
|
|
623
|
+
let inline = false;
|
|
624
|
+
let stdin = false;
|
|
625
|
+
let k = 0;
|
|
626
|
+
while (k < args.length) {
|
|
627
|
+
const option = args[k].text;
|
|
628
|
+
if (option === "--" || option === "-") {
|
|
629
|
+
k += 1;
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
if (option === "--rcfile" || option === "--init-file") k += 2;
|
|
633
|
+
else if (option.startsWith("--")) k += 1;
|
|
634
|
+
else if (/^[-+][A-Za-z]+$/.test(option)) {
|
|
635
|
+
if (option.startsWith("-") && option.includes("c")) inline = true;
|
|
636
|
+
if (option.includes("s")) stdin = true;
|
|
637
|
+
k += option.includes("o") ? 2 : 1;
|
|
638
|
+
} else break;
|
|
639
|
+
}
|
|
640
|
+
const operand = args[k];
|
|
641
|
+
if (inline) {
|
|
642
|
+
if (operand === void 0) return;
|
|
643
|
+
if (operand.literal) nested(operand.text);
|
|
644
|
+
else out.undecomposable.push(`\`${name} -c\` runs a string that is not a literal`);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (operand !== void 0 && !stdin) {
|
|
648
|
+
if (!operand.literal) out.undecomposable.push(`\`${name}\` runs a script only known at run time`);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (fromPipe) out.undecomposable.push(`\`${name}\` runs commands read from a pipe`);
|
|
652
|
+
if (feed === void 0) return;
|
|
653
|
+
feed.shell = `\`${name}\``;
|
|
654
|
+
const string = feed.herestring;
|
|
655
|
+
if (string === void 0) return;
|
|
656
|
+
if (string.literal) nested(string.text);
|
|
657
|
+
else out.undecomposable.push(`a here-string with expansions is run by \`${name}\``);
|
|
658
|
+
}
|
|
659
|
+
if (expansionsOnly) double(fresh(), false);
|
|
660
|
+
else list$1(void 0);
|
|
661
|
+
}
|
|
662
|
+
/** Read a command line into every command it would run, and what could not be read. */
|
|
663
|
+
function decompose(command) {
|
|
664
|
+
const out = {
|
|
665
|
+
segments: [],
|
|
666
|
+
pipelines: [],
|
|
667
|
+
undecomposable: []
|
|
668
|
+
};
|
|
669
|
+
try {
|
|
670
|
+
read(command, out, 0);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
out.undecomposable.push(error instanceof Undecomposable ? error.message : "the line could not be read");
|
|
121
673
|
}
|
|
122
|
-
return
|
|
674
|
+
return out;
|
|
123
675
|
}
|
|
124
676
|
/** `git*` matches `git`, `github`; bare strings match exactly. */
|
|
125
677
|
function matches$1(pattern, value) {
|
|
@@ -144,17 +696,19 @@
|
|
|
144
696
|
}
|
|
145
697
|
const list = (v) => v === void 0 ? [] : typeof v === "string" ? [v] : v;
|
|
146
698
|
/**
|
|
147
|
-
*
|
|
699
|
+
* Every present field holds on what was read. With `open`, a segment whose
|
|
700
|
+
* arguments arrive at run time (`xargs`, `find -exec`) satisfies any token
|
|
701
|
+
* condition, because those arguments could.
|
|
148
702
|
*/
|
|
149
|
-
function
|
|
150
|
-
const parts = segments
|
|
703
|
+
function holds(match, read$1, open) {
|
|
704
|
+
const parts = read$1.segments;
|
|
151
705
|
if (parts.length === 0) return false;
|
|
152
706
|
const names = list(match.name);
|
|
153
707
|
const args = list(match.token);
|
|
154
708
|
const anyArgs = list(match.anyToken);
|
|
155
709
|
const pipes = list(match.pipedTo);
|
|
156
710
|
const shapes = list(match.pipeline);
|
|
157
|
-
const argsHold = (segment) => tokensHold(args, segment.tokens) && (anyArgs.length === 0 || anyArgs.some((wanted) => segment.tokens.some((token) => matches$1(wanted, token))));
|
|
711
|
+
const argsHold = (segment) => open && segment.openEnded === true || tokensHold(args, segment.tokens) && (anyArgs.length === 0 || anyArgs.some((wanted) => segment.tokens.some((token) => matches$1(wanted, token))));
|
|
158
712
|
if (names.length > 0) {
|
|
159
713
|
const hit = parts.filter((segment) => names.some((pattern) => matches$1(pattern, segment.name) || segment.wrappers.some((wrapper) => matches$1(pattern, wrapper))));
|
|
160
714
|
if (hit.length === 0) return false;
|
|
@@ -166,11 +720,32 @@
|
|
|
166
720
|
if (!parts.slice(1).some((segment) => pipes.some((pattern) => matches$1(pattern, segment.name)))) return false;
|
|
167
721
|
}
|
|
168
722
|
if (shapes.length > 0) {
|
|
169
|
-
const
|
|
170
|
-
if (!shapes.some((pattern) => globMatches(pattern, signature))) return false;
|
|
723
|
+
const signatures = [parts.map((segment) => segment.name).join("|"), ...read$1.pipelines];
|
|
724
|
+
if (!shapes.some((pattern) => signatures.some((signature) => globMatches(pattern, signature)))) return false;
|
|
171
725
|
}
|
|
172
726
|
return true;
|
|
173
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* Judge one command line against one matcher, on what could be read. Every
|
|
730
|
+
* present field must hold.
|
|
731
|
+
*/
|
|
732
|
+
function commandMatches(match, command) {
|
|
733
|
+
return holds(match, typeof command === "string" ? decompose(command) : command, false);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Why a matcher that did not hold on what was read might still hold on what
|
|
737
|
+
* runs: part of the line could not be read, or a matched program takes
|
|
738
|
+
* arguments only known at run time. Undefined when there is no such reason.
|
|
739
|
+
* A `deny` or `ask` rule matches on it, conservatively (`evaluate.ts`).
|
|
740
|
+
*/
|
|
741
|
+
function commandUnread(match, command) {
|
|
742
|
+
const read$1 = typeof command === "string" ? decompose(command) : command;
|
|
743
|
+
if (read$1.undecomposable.length > 0) return [...new Set(read$1.undecomposable)].join("; ");
|
|
744
|
+
if (!holds(match, read$1, true)) return void 0;
|
|
745
|
+
const open = read$1.segments.find((s) => s.openEnded === true);
|
|
746
|
+
const from = open?.wrappers.includes("xargs") ? "xargs" : "find -exec";
|
|
747
|
+
return `the arguments \`${open?.name ?? "?"}\` gets from ${from} are only known at run time`;
|
|
748
|
+
}
|
|
174
749
|
|
|
175
750
|
//#endregion
|
|
176
751
|
//#region ../kernel/src/path.ts
|
|
@@ -383,15 +958,25 @@
|
|
|
383
958
|
}
|
|
384
959
|
return true;
|
|
385
960
|
}
|
|
386
|
-
|
|
961
|
+
/**
|
|
962
|
+
* Whether a rule's matcher holds on a call: `false`, or a hit — carrying a
|
|
963
|
+
* note when the hit is conservative. `strict` rules (deny, ask) match a shell
|
|
964
|
+
* line they cannot fully read, because what was not read could be what they
|
|
965
|
+
* refuse; the note says what that was. Weaker rules never match on a guess.
|
|
966
|
+
*/
|
|
967
|
+
function matches(when, facts, strict, shell) {
|
|
968
|
+
let note;
|
|
387
969
|
if (!listed(facts.tool, when.tool)) return false;
|
|
388
970
|
if (!listed(facts.capability, when.capability)) return false;
|
|
389
971
|
if (!listed(facts.plugin, when.plugin)) return false;
|
|
390
972
|
if (!listed(facts.agent, when.agent)) return false;
|
|
391
973
|
if (when.command !== void 0) {
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
394
|
-
if (!commandMatches(when.command,
|
|
974
|
+
const line = shell();
|
|
975
|
+
if (line === void 0) return false;
|
|
976
|
+
if (!commandMatches(when.command, line)) {
|
|
977
|
+
note = strict ? commandUnread(when.command, line) : void 0;
|
|
978
|
+
if (note === void 0) return false;
|
|
979
|
+
}
|
|
395
980
|
}
|
|
396
981
|
if (when.path !== void 0) {
|
|
397
982
|
const raw = pathArgOf(facts.args, when.path.arg);
|
|
@@ -403,7 +988,10 @@
|
|
|
403
988
|
const raw = facts.args[when.host.arg ?? "url"];
|
|
404
989
|
if (typeof raw !== "string") return false;
|
|
405
990
|
const host = hostOf(raw);
|
|
406
|
-
if (host === void 0)
|
|
991
|
+
if (host === void 0) {
|
|
992
|
+
if (when.host.notIn === void 0) return false;
|
|
993
|
+
return note === void 0 ? {} : { note };
|
|
994
|
+
}
|
|
407
995
|
if (when.host.in !== void 0 && !hostIn(host, when.host.in)) return false;
|
|
408
996
|
if (when.host.notIn !== void 0 && hostIn(host, when.host.notIn)) return false;
|
|
409
997
|
}
|
|
@@ -420,7 +1008,7 @@
|
|
|
420
1008
|
const wanted = when.secrets.types;
|
|
421
1009
|
if ((facts.secrets ?? []).filter((finding) => finding.confidence >= floor && (wanted === void 0 || wanted.includes(finding.type))).length < (when.secrets.atLeast ?? 1)) return false;
|
|
422
1010
|
}
|
|
423
|
-
return
|
|
1011
|
+
return note === void 0 ? {} : { note };
|
|
424
1012
|
}
|
|
425
1013
|
function step(rule, effect) {
|
|
426
1014
|
return {
|
|
@@ -445,10 +1033,26 @@
|
|
|
445
1033
|
*/
|
|
446
1034
|
function evaluate(pack, facts) {
|
|
447
1035
|
const hits = [];
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1036
|
+
let line = null;
|
|
1037
|
+
const shell = () => {
|
|
1038
|
+
if (line === null) {
|
|
1039
|
+
const raw = facts.args.command ?? facts.args.cmd ?? facts.args.script;
|
|
1040
|
+
line = typeof raw === "string" ? decompose(raw) : void 0;
|
|
1041
|
+
}
|
|
1042
|
+
return line;
|
|
1043
|
+
};
|
|
1044
|
+
for (const rule of pack.rules) {
|
|
1045
|
+
const strict = rule.effect.kind === "deny" || rule.effect.kind === "ask";
|
|
1046
|
+
const hit = matches(rule.when, facts, strict, shell);
|
|
1047
|
+
if (hit === false) continue;
|
|
1048
|
+
hits.push({
|
|
1049
|
+
rule,
|
|
1050
|
+
effect: hit.note === void 0 ? rule.effect : {
|
|
1051
|
+
...rule.effect,
|
|
1052
|
+
because: `${rule.effect.because} (could not decompose: ${hit.note})`
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
452
1056
|
if (hits.length === 0) return {
|
|
453
1057
|
kind: "allow",
|
|
454
1058
|
trace: [],
|
|
@@ -984,6 +1588,7 @@
|
|
|
984
1588
|
"version",
|
|
985
1589
|
"description",
|
|
986
1590
|
"owner",
|
|
1591
|
+
"expires_at",
|
|
987
1592
|
"baseline",
|
|
988
1593
|
"rules"
|
|
989
1594
|
]);
|
|
@@ -992,6 +1597,8 @@
|
|
|
992
1597
|
const version = check.text(".version", raw.version);
|
|
993
1598
|
const description = raw.description === void 0 ? void 0 : check.text(".description", raw.description);
|
|
994
1599
|
const owner = raw.owner === void 0 ? void 0 : check.text(".owner", raw.owner);
|
|
1600
|
+
const expires = raw.expires_at === void 0 ? void 0 : check.text(".expires_at", raw.expires_at);
|
|
1601
|
+
if (expires !== void 0 && (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/.test(expires) || !Number.isFinite(Date.parse(expires)))) check.fail(".expires_at", "expected an ISO 8601 time with a zone, e.g. 2026-10-05T12:00:00Z");
|
|
995
1602
|
let baseline;
|
|
996
1603
|
if (raw.baseline !== void 0) if (!isObject(raw.baseline)) check.fail(".baseline", "expected an object");
|
|
997
1604
|
else {
|
|
@@ -1030,6 +1637,7 @@
|
|
|
1030
1637
|
version,
|
|
1031
1638
|
...description === void 0 ? {} : { description },
|
|
1032
1639
|
...owner === void 0 ? {} : { owner },
|
|
1640
|
+
...expires === void 0 ? {} : { expires_at: expires },
|
|
1033
1641
|
...baseline === void 0 ? {} : { baseline },
|
|
1034
1642
|
rules
|
|
1035
1643
|
}
|