@jitsusama/agentic-harness.core 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command/tokenize.js +51 -5
- package/dist/internal/quest/bash-effects.d.ts +26 -0
- package/dist/internal/quest/bash-effects.js +477 -0
- package/dist/internal/quest/bash-write.d.ts +37 -0
- package/dist/internal/quest/bash-write.js +311 -37
- package/dist/internal/quest/record-audit.d.ts +43 -0
- package/dist/internal/quest/record-audit.js +126 -0
- package/dist/internal/quest/record-gate.d.ts +36 -0
- package/dist/internal/quest/record-gate.js +101 -0
- package/dist/internal/quest/record.d.ts +112 -0
- package/dist/internal/quest/record.js +284 -0
- package/dist/internal/quest/workspace.d.ts +32 -0
- package/dist/internal/quest/workspace.js +51 -0
- package/package.json +7 -1
package/dist/command/tokenize.js
CHANGED
|
@@ -198,11 +198,20 @@ function toHeredoc(info) {
|
|
|
198
198
|
span: { start: info.index, end: info.end },
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
|
-
|
|
201
|
+
/**
|
|
202
|
+
* A word that begins with a redirect operator: an optional file
|
|
203
|
+
* descriptor or `&`, the operator, then whatever is written against it.
|
|
204
|
+
* A `<`, `>` or `(` straight after the operator is a here-string or a
|
|
205
|
+
* process substitution, which name no file.
|
|
206
|
+
*/
|
|
207
|
+
const REDIRECT = /^(\d*|&)(>>|>|<)(?![<>(])(.*)$/s;
|
|
208
|
+
/** What follows a duplication operator: a descriptor, or `-` to close. */
|
|
209
|
+
const DUPLICATION = /^&(\d+|-)$/;
|
|
202
210
|
/**
|
|
203
211
|
* Pull redirects out of a word list. A redirect operator that names
|
|
204
|
-
* a file descriptor duplication (2>&1) stands alone
|
|
205
|
-
* operator
|
|
212
|
+
* a file descriptor duplication (2>&1) stands alone. Any other
|
|
213
|
+
* operator names its target either in the same word (`2>/dev/null`)
|
|
214
|
+
* or, when written alone, in the word that follows.
|
|
206
215
|
*/
|
|
207
216
|
function extractRedirects(words) {
|
|
208
217
|
const argv = [];
|
|
@@ -214,8 +223,26 @@ function extractRedirects(words) {
|
|
|
214
223
|
argv.push(word);
|
|
215
224
|
continue;
|
|
216
225
|
}
|
|
217
|
-
const
|
|
218
|
-
|
|
226
|
+
const attached = match[3] ?? "";
|
|
227
|
+
if (DUPLICATION.test(attached)) {
|
|
228
|
+
redirects.push({ span: word.span, operator: word.text });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (attached) {
|
|
232
|
+
const operator = word.text.slice(0, word.text.length - attached.length);
|
|
233
|
+
const start = word.span.start + operator.length;
|
|
234
|
+
redirects.push({
|
|
235
|
+
span: word.span,
|
|
236
|
+
operator,
|
|
237
|
+
target: {
|
|
238
|
+
span: { start, end: word.span.end },
|
|
239
|
+
text: attached,
|
|
240
|
+
quoting: quotingOf(attached),
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const target = words[j + 1];
|
|
219
246
|
if (target) {
|
|
220
247
|
redirects.push({
|
|
221
248
|
span: { start: word.span.start, end: target.span.end },
|
|
@@ -303,6 +330,25 @@ function scanWords(source, start, end) {
|
|
|
303
330
|
}
|
|
304
331
|
return words;
|
|
305
332
|
}
|
|
333
|
+
/** The quote style of a word's text, read the way `scanWords` reads it. */
|
|
334
|
+
function quotingOf(text) {
|
|
335
|
+
let sawSingle = false;
|
|
336
|
+
let sawDouble = false;
|
|
337
|
+
let i = 0;
|
|
338
|
+
while (i < text.length) {
|
|
339
|
+
const ch = text[i];
|
|
340
|
+
if (ch === "'" || ch === '"') {
|
|
341
|
+
if (ch === "'")
|
|
342
|
+
sawSingle = true;
|
|
343
|
+
else
|
|
344
|
+
sawDouble = true;
|
|
345
|
+
i = skipQuoted(text, i);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
i++;
|
|
349
|
+
}
|
|
350
|
+
return classifyQuoting(sawSingle, sawDouble);
|
|
351
|
+
}
|
|
306
352
|
/** Classify a word's quote style from which quote kinds it used. */
|
|
307
353
|
function classifyQuoting(sawSingle, sawDouble) {
|
|
308
354
|
if (sawSingle && sawDouble)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a command does to files without a redirect: the copies, moves,
|
|
3
|
+
* links, directories, clones, downloads and unpacked archives it creates,
|
|
4
|
+
* and the files it removes.
|
|
5
|
+
*
|
|
6
|
+
* Each reader takes an unquoted argv and answers with the paths as the
|
|
7
|
+
* command names them, relative or not, so the caller can place them
|
|
8
|
+
* in the directory the command runs in. A reader reads options the way
|
|
9
|
+
* its command does, since an option's value taken for a destination is a
|
|
10
|
+
* path nobody wrote. What a command writes to standard output names no
|
|
11
|
+
* file and is left to the redirect that catches it.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The command a wrapper runs, with the wrapper and its options removed.
|
|
15
|
+
*
|
|
16
|
+
* `env` also takes the assignments before the command, and `nice -5` is
|
|
17
|
+
* the old spelling of an adjustment, so neither is read as the command.
|
|
18
|
+
*/
|
|
19
|
+
export declare function unwrap(argv: readonly string[]): string[];
|
|
20
|
+
/** The files a command creates without a redirect, as the command names them. */
|
|
21
|
+
export declare function createdBy(argv: readonly string[]): string[];
|
|
22
|
+
/**
|
|
23
|
+
* The files a command removes, as the command names them. A move removes
|
|
24
|
+
* its sources, since what was there is no longer there.
|
|
25
|
+
*/
|
|
26
|
+
export declare function removedBy(argv: readonly string[]): string[];
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a command does to files without a redirect: the copies, moves,
|
|
3
|
+
* links, directories, clones, downloads and unpacked archives it creates,
|
|
4
|
+
* and the files it removes.
|
|
5
|
+
*
|
|
6
|
+
* Each reader takes an unquoted argv and answers with the paths as the
|
|
7
|
+
* command names them, relative or not, so the caller can place them
|
|
8
|
+
* in the directory the command runs in. A reader reads options the way
|
|
9
|
+
* its command does, since an option's value taken for a destination is a
|
|
10
|
+
* path nobody wrote. What a command writes to standard output names no
|
|
11
|
+
* file and is left to the redirect that catches it.
|
|
12
|
+
*/
|
|
13
|
+
import { basename, join } from "node:path";
|
|
14
|
+
/**
|
|
15
|
+
* Read an argv's options against a grammar.
|
|
16
|
+
*
|
|
17
|
+
* Short options cluster (`-sSLo`), and a valued letter takes the rest of
|
|
18
|
+
* its word or, at the end of it, the next word. A long option takes its
|
|
19
|
+
* value after `=` or, when the grammar says it has one, as the next word.
|
|
20
|
+
* Anything after `--` is an operand.
|
|
21
|
+
*/
|
|
22
|
+
function parse(args, grammar) {
|
|
23
|
+
const operands = [];
|
|
24
|
+
const values = new Map();
|
|
25
|
+
const flags = new Set();
|
|
26
|
+
const give = (name, value) => {
|
|
27
|
+
values.set(name, [...(values.get(name) ?? []), value]);
|
|
28
|
+
};
|
|
29
|
+
for (let at = 0; at < args.length; at++) {
|
|
30
|
+
const arg = args[at] ?? "";
|
|
31
|
+
if (arg === "--") {
|
|
32
|
+
operands.push(...args.slice(at + 1));
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (arg.startsWith("--")) {
|
|
36
|
+
const equals = arg.indexOf("=");
|
|
37
|
+
if (equals > 0)
|
|
38
|
+
give(arg.slice(2, equals), arg.slice(equals + 1));
|
|
39
|
+
else if (grammar.long?.includes(arg.slice(2))) {
|
|
40
|
+
give(arg.slice(2), args[at + 1] ?? "");
|
|
41
|
+
at++;
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
flags.add(arg.slice(2));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!arg.startsWith("-") || arg === "-") {
|
|
48
|
+
operands.push(arg);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (let char = 1; char < arg.length; char++) {
|
|
52
|
+
const letter = arg[char] ?? "";
|
|
53
|
+
if (!grammar.short?.includes(letter)) {
|
|
54
|
+
flags.add(letter);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (char < arg.length - 1)
|
|
58
|
+
give(letter, arg.slice(char + 1));
|
|
59
|
+
else {
|
|
60
|
+
give(letter, args[at + 1] ?? "");
|
|
61
|
+
at++;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { operands, values, flags };
|
|
67
|
+
}
|
|
68
|
+
/** The last value an option was given, under any of its names. */
|
|
69
|
+
function last(parsed, ...names) {
|
|
70
|
+
for (const name of names) {
|
|
71
|
+
const given = parsed.values.get(name);
|
|
72
|
+
if (given?.length)
|
|
73
|
+
return given[given.length - 1];
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Whether an option was given, with or without a value. */
|
|
78
|
+
function has(parsed, ...names) {
|
|
79
|
+
return names.some((name) => parsed.flags.has(name) || parsed.values.has(name));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Where `cp`, `mv`, `install` and `ln` put their sources.
|
|
83
|
+
*
|
|
84
|
+
* Into a directory when one is named by `-t`, when the destination ends
|
|
85
|
+
* in a slash, or when there are several sources; otherwise at the
|
|
86
|
+
* destination itself, which may still be a directory the text does not
|
|
87
|
+
* reveal.
|
|
88
|
+
*/
|
|
89
|
+
function copied(sources, into, dest) {
|
|
90
|
+
if (into !== undefined)
|
|
91
|
+
return sources.map((source) => join(into, basename(source)));
|
|
92
|
+
if (dest === undefined || sources.length === 0)
|
|
93
|
+
return [];
|
|
94
|
+
if (dest.endsWith("/") || sources.length > 1) {
|
|
95
|
+
return sources.map((source) => join(dest, basename(source)));
|
|
96
|
+
}
|
|
97
|
+
return [dest];
|
|
98
|
+
}
|
|
99
|
+
/** A copier's destinations: `-t` or the last operand. */
|
|
100
|
+
function copier(grammar) {
|
|
101
|
+
return (args) => {
|
|
102
|
+
const parsed = parse(args, grammar);
|
|
103
|
+
const into = last(parsed, "t", "target-directory");
|
|
104
|
+
if (into !== undefined)
|
|
105
|
+
return copied(parsed.operands, into);
|
|
106
|
+
const sources = parsed.operands.slice(0, -1);
|
|
107
|
+
return copied(sources, undefined, parsed.operands.at(-1));
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** The name a download takes from its URL, the way curl and wget do. */
|
|
111
|
+
function remoteName(url) {
|
|
112
|
+
const path = url.replace(/[?#].*$/, "").replace(/^[a-z]+:\/\/[^/]*/i, "");
|
|
113
|
+
return basename(path) || "index.html";
|
|
114
|
+
}
|
|
115
|
+
/** A git URL's repository name, which `git clone` uses for its directory. */
|
|
116
|
+
function repositoryName(url) {
|
|
117
|
+
const trimmed = url.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
118
|
+
return trimmed.split(/[/:]/).at(-1) ?? trimmed;
|
|
119
|
+
}
|
|
120
|
+
/** A path under git's `-C` directories, which apply in order. */
|
|
121
|
+
function underGitDirectories(dirs, path) {
|
|
122
|
+
return dirs.reduceRight((inner, dir) => join(dir, inner), path);
|
|
123
|
+
}
|
|
124
|
+
const GIT_CLONE = {
|
|
125
|
+
short: "bocju",
|
|
126
|
+
long: [
|
|
127
|
+
"branch",
|
|
128
|
+
"origin",
|
|
129
|
+
"depth",
|
|
130
|
+
"reference",
|
|
131
|
+
"reference-if-able",
|
|
132
|
+
"separate-git-dir",
|
|
133
|
+
"template",
|
|
134
|
+
"config",
|
|
135
|
+
"jobs",
|
|
136
|
+
"filter",
|
|
137
|
+
"upload-pack",
|
|
138
|
+
"shallow-since",
|
|
139
|
+
"shallow-exclude",
|
|
140
|
+
"bundle-uri",
|
|
141
|
+
"server-option",
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
/** Git's destinations: the directory a clone or a new worktree makes. */
|
|
145
|
+
function git(args) {
|
|
146
|
+
const dirs = [];
|
|
147
|
+
let at = 0;
|
|
148
|
+
while (at < args.length && args[at]?.startsWith("-")) {
|
|
149
|
+
const option = args[at] ?? "";
|
|
150
|
+
if (option === "-C")
|
|
151
|
+
dirs.push(args[++at] ?? "");
|
|
152
|
+
else if (option === "-c")
|
|
153
|
+
at++;
|
|
154
|
+
at++;
|
|
155
|
+
}
|
|
156
|
+
const subcommand = args[at];
|
|
157
|
+
const rest = args.slice(at + 1);
|
|
158
|
+
if (subcommand === "clone") {
|
|
159
|
+
const [url, dir] = parse(rest, GIT_CLONE).operands;
|
|
160
|
+
if (url === undefined)
|
|
161
|
+
return [];
|
|
162
|
+
return [underGitDirectories(dirs, dir ?? repositoryName(url))];
|
|
163
|
+
}
|
|
164
|
+
if (subcommand === "worktree" && rest[0] === "add") {
|
|
165
|
+
const [path] = parse(rest.slice(1), {
|
|
166
|
+
short: "bB",
|
|
167
|
+
long: ["reason"],
|
|
168
|
+
}).operands;
|
|
169
|
+
return path === undefined ? [] : [underGitDirectories(dirs, path)];
|
|
170
|
+
}
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const CURL = {
|
|
174
|
+
short: "ACDEFHKQTUXYbcdemortuwxyz",
|
|
175
|
+
long: [
|
|
176
|
+
"output",
|
|
177
|
+
"output-dir",
|
|
178
|
+
"cookie-jar",
|
|
179
|
+
"dump-header",
|
|
180
|
+
"trace",
|
|
181
|
+
"trace-ascii",
|
|
182
|
+
"header",
|
|
183
|
+
"data",
|
|
184
|
+
"data-binary",
|
|
185
|
+
"data-raw",
|
|
186
|
+
"data-urlencode",
|
|
187
|
+
"json",
|
|
188
|
+
"form",
|
|
189
|
+
"request",
|
|
190
|
+
"user",
|
|
191
|
+
"user-agent",
|
|
192
|
+
"referer",
|
|
193
|
+
"upload-file",
|
|
194
|
+
"write-out",
|
|
195
|
+
"proxy",
|
|
196
|
+
"range",
|
|
197
|
+
"max-time",
|
|
198
|
+
"connect-timeout",
|
|
199
|
+
"retry",
|
|
200
|
+
"cookie",
|
|
201
|
+
"config",
|
|
202
|
+
"cert",
|
|
203
|
+
"key",
|
|
204
|
+
"cacert",
|
|
205
|
+
"url",
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
/** Curl's destinations: `-o`, `-O`, and the files it keeps on the side. */
|
|
209
|
+
function curl(args) {
|
|
210
|
+
const parsed = parse(args, CURL);
|
|
211
|
+
const dir = last(parsed, "output-dir");
|
|
212
|
+
const place = (name) => dir === undefined ? name : join(dir, name);
|
|
213
|
+
const found = [];
|
|
214
|
+
for (const name of ["o", "output"]) {
|
|
215
|
+
for (const file of parsed.values.get(name) ?? []) {
|
|
216
|
+
if (file !== "-")
|
|
217
|
+
found.push(place(file));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (has(parsed, "O", "remote-name", "remote-name-all")) {
|
|
221
|
+
const urls = [...parsed.operands, ...(parsed.values.get("url") ?? [])];
|
|
222
|
+
found.push(...urls.map((url) => place(remoteName(url))));
|
|
223
|
+
}
|
|
224
|
+
for (const name of [
|
|
225
|
+
"c",
|
|
226
|
+
"cookie-jar",
|
|
227
|
+
"D",
|
|
228
|
+
"dump-header",
|
|
229
|
+
"trace",
|
|
230
|
+
"trace-ascii",
|
|
231
|
+
]) {
|
|
232
|
+
for (const file of parsed.values.get(name) ?? []) {
|
|
233
|
+
if (file !== "-")
|
|
234
|
+
found.push(file);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return found;
|
|
238
|
+
}
|
|
239
|
+
const WGET = {
|
|
240
|
+
short: "OPoaetTwUiBQ",
|
|
241
|
+
long: [
|
|
242
|
+
"output-document",
|
|
243
|
+
"directory-prefix",
|
|
244
|
+
"output-file",
|
|
245
|
+
"append-output",
|
|
246
|
+
"user-agent",
|
|
247
|
+
"header",
|
|
248
|
+
"tries",
|
|
249
|
+
"timeout",
|
|
250
|
+
"wait",
|
|
251
|
+
"input-file",
|
|
252
|
+
"post-data",
|
|
253
|
+
"user",
|
|
254
|
+
"password",
|
|
255
|
+
"limit-rate",
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
/** Wget's destinations: `-O`, or each URL's name under `-P`, and its log. */
|
|
259
|
+
function wget(args) {
|
|
260
|
+
const parsed = parse(args, WGET);
|
|
261
|
+
const found = [];
|
|
262
|
+
const document = last(parsed, "O", "output-document");
|
|
263
|
+
if (document !== undefined) {
|
|
264
|
+
if (document !== "-")
|
|
265
|
+
found.push(document);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const dir = last(parsed, "P", "directory-prefix");
|
|
269
|
+
for (const url of parsed.operands) {
|
|
270
|
+
const name = remoteName(url);
|
|
271
|
+
found.push(dir === undefined ? name : join(dir, name));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const name of ["o", "output-file", "a", "append-output"]) {
|
|
275
|
+
const log = last(parsed, name);
|
|
276
|
+
if (log !== undefined)
|
|
277
|
+
found.push(log);
|
|
278
|
+
}
|
|
279
|
+
return found;
|
|
280
|
+
}
|
|
281
|
+
const TAR = {
|
|
282
|
+
short: "fCbTXK",
|
|
283
|
+
long: ["file", "directory", "files-from", "exclude-from"],
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Tar's destinations: the archive it creates or adds to, or the
|
|
287
|
+
* directory it unpacks into.
|
|
288
|
+
*
|
|
289
|
+
* The old spelling bundles its letters without a dash (`tar xzf a.tgz`),
|
|
290
|
+
* each valued letter taking the next word in turn, so it is rewritten
|
|
291
|
+
* into the dashed form before reading.
|
|
292
|
+
*/
|
|
293
|
+
function tar(args) {
|
|
294
|
+
const [first, ...rest] = args;
|
|
295
|
+
let words = args;
|
|
296
|
+
if (first !== undefined && /^[a-zA-Z]+$/.test(first)) {
|
|
297
|
+
const valued = [...first].filter((letter) => TAR.short?.includes(letter));
|
|
298
|
+
const plain = [...first].filter((letter) => !TAR.short?.includes(letter));
|
|
299
|
+
const taken = rest.slice(0, valued.length);
|
|
300
|
+
words = [
|
|
301
|
+
...(plain.length ? [`-${plain.join("")}`] : []),
|
|
302
|
+
...valued.flatMap((letter, index) => [`-${letter}`, taken[index] ?? ""]),
|
|
303
|
+
...rest.slice(valued.length),
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
const parsed = parse(words, TAR);
|
|
307
|
+
if (has(parsed, "x", "extract", "get")) {
|
|
308
|
+
return [last(parsed, "C", "directory") ?? "."];
|
|
309
|
+
}
|
|
310
|
+
if (has(parsed, "c", "r", "u", "A", "create", "append", "update")) {
|
|
311
|
+
const archive = last(parsed, "f", "file");
|
|
312
|
+
return archive === undefined || archive === "-" ? [] : [archive];
|
|
313
|
+
}
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
/** Unzip's destination: the directory it unpacks into, unless it only reads. */
|
|
317
|
+
function unzip(args) {
|
|
318
|
+
const parsed = parse(args, { short: "dP" });
|
|
319
|
+
if (has(parsed, "l", "t", "v", "p", "Z"))
|
|
320
|
+
return [];
|
|
321
|
+
return [last(parsed, "d") ?? "."];
|
|
322
|
+
}
|
|
323
|
+
/** Dd's destination, its `of=` operand. */
|
|
324
|
+
function dd(args) {
|
|
325
|
+
return args.filter((arg) => arg.startsWith("of=")).map((arg) => arg.slice(3));
|
|
326
|
+
}
|
|
327
|
+
/** Rsync's destination, its last operand, unless that is on another host. */
|
|
328
|
+
function rsync(args) {
|
|
329
|
+
const parsed = parse(args, {
|
|
330
|
+
short: "efT",
|
|
331
|
+
long: [
|
|
332
|
+
"exclude",
|
|
333
|
+
"include",
|
|
334
|
+
"exclude-from",
|
|
335
|
+
"include-from",
|
|
336
|
+
"files-from",
|
|
337
|
+
"filter",
|
|
338
|
+
"rsh",
|
|
339
|
+
"rsync-path",
|
|
340
|
+
"temp-dir",
|
|
341
|
+
"compare-dest",
|
|
342
|
+
"copy-dest",
|
|
343
|
+
"link-dest",
|
|
344
|
+
"backup-dir",
|
|
345
|
+
"suffix",
|
|
346
|
+
"chmod",
|
|
347
|
+
"chown",
|
|
348
|
+
"log-file",
|
|
349
|
+
"partial-dir",
|
|
350
|
+
"password-file",
|
|
351
|
+
"port",
|
|
352
|
+
"timeout",
|
|
353
|
+
"max-size",
|
|
354
|
+
"min-size",
|
|
355
|
+
"bwlimit",
|
|
356
|
+
"out-format",
|
|
357
|
+
],
|
|
358
|
+
});
|
|
359
|
+
if (parsed.operands.length < 2)
|
|
360
|
+
return [];
|
|
361
|
+
const dest = parsed.operands.at(-1) ?? "";
|
|
362
|
+
return /^[^/~.]*:/.test(dest) ? [] : [dest];
|
|
363
|
+
}
|
|
364
|
+
/** Every operand a command creates, once its valued options are skipped. */
|
|
365
|
+
function operandsOf(grammar) {
|
|
366
|
+
return (args) => parse(args, grammar).operands;
|
|
367
|
+
}
|
|
368
|
+
/** The readers, by command name. */
|
|
369
|
+
const CREATORS = {
|
|
370
|
+
cp: copier({ short: "tS", long: ["target-directory", "suffix"] }),
|
|
371
|
+
mv: copier({ short: "tS", long: ["target-directory", "suffix"] }),
|
|
372
|
+
install: (args) => {
|
|
373
|
+
const parsed = parse(args, {
|
|
374
|
+
short: "mogtS",
|
|
375
|
+
long: ["mode", "owner", "group"],
|
|
376
|
+
});
|
|
377
|
+
if (has(parsed, "d", "directory"))
|
|
378
|
+
return parsed.operands;
|
|
379
|
+
return copier({ short: "mogtS" })(args);
|
|
380
|
+
},
|
|
381
|
+
ln: (args) => {
|
|
382
|
+
const parsed = parse(args, {
|
|
383
|
+
short: "tS",
|
|
384
|
+
long: ["target-directory", "suffix"],
|
|
385
|
+
});
|
|
386
|
+
const [only] = parsed.operands;
|
|
387
|
+
const into = last(parsed, "t", "target-directory");
|
|
388
|
+
if (into === undefined &&
|
|
389
|
+
parsed.operands.length === 1 &&
|
|
390
|
+
only !== undefined) {
|
|
391
|
+
return [basename(only)];
|
|
392
|
+
}
|
|
393
|
+
return copier({ short: "tS", long: ["target-directory", "suffix"] })(args);
|
|
394
|
+
},
|
|
395
|
+
rsync,
|
|
396
|
+
mkdir: operandsOf({ short: "m", long: ["mode"] }),
|
|
397
|
+
touch: operandsOf({ short: "tdrA", long: ["date", "reference"] }),
|
|
398
|
+
dd,
|
|
399
|
+
git,
|
|
400
|
+
curl,
|
|
401
|
+
wget,
|
|
402
|
+
tar,
|
|
403
|
+
gtar: tar,
|
|
404
|
+
unzip,
|
|
405
|
+
};
|
|
406
|
+
/** Commands that run the command after them, and how many words they take. */
|
|
407
|
+
const WRAPPERS = {
|
|
408
|
+
nohup: {},
|
|
409
|
+
time: {},
|
|
410
|
+
command: {},
|
|
411
|
+
exec: {},
|
|
412
|
+
builtin: {},
|
|
413
|
+
nice: { short: "n" },
|
|
414
|
+
env: { short: "uCS" },
|
|
415
|
+
sudo: { short: "ugCDhprtU" },
|
|
416
|
+
};
|
|
417
|
+
/**
|
|
418
|
+
* The command a wrapper runs, with the wrapper and its options removed.
|
|
419
|
+
*
|
|
420
|
+
* `env` also takes the assignments before the command, and `nice -5` is
|
|
421
|
+
* the old spelling of an adjustment, so neither is read as the command.
|
|
422
|
+
*/
|
|
423
|
+
export function unwrap(argv) {
|
|
424
|
+
let words = [...argv];
|
|
425
|
+
for (;;) {
|
|
426
|
+
const grammar = WRAPPERS[words[0] ?? ""];
|
|
427
|
+
if (!grammar)
|
|
428
|
+
return words;
|
|
429
|
+
let at = 1;
|
|
430
|
+
while (at < words.length) {
|
|
431
|
+
const word = words[at] ?? "";
|
|
432
|
+
if (words[0] === "env" && /^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) {
|
|
433
|
+
at++;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (!word.startsWith("-") || word === "-")
|
|
437
|
+
break;
|
|
438
|
+
if (word === "--") {
|
|
439
|
+
at++;
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
const letter = word[1] ?? "";
|
|
443
|
+
const valued = word.length === 2 && grammar.short?.includes(letter);
|
|
444
|
+
at += valued ? 2 : 1;
|
|
445
|
+
}
|
|
446
|
+
words = words.slice(at);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/** The files a command creates without a redirect, as the command names them. */
|
|
450
|
+
export function createdBy(argv) {
|
|
451
|
+
const [name, ...args] = argv;
|
|
452
|
+
const reader = name === undefined ? undefined : CREATORS[name];
|
|
453
|
+
return reader ? reader(args) : [];
|
|
454
|
+
}
|
|
455
|
+
/** The readers of what a command removes, by command name. */
|
|
456
|
+
const REMOVERS = {
|
|
457
|
+
rm: operandsOf({}),
|
|
458
|
+
rmdir: operandsOf({}),
|
|
459
|
+
unlink: operandsOf({}),
|
|
460
|
+
mv: (args) => {
|
|
461
|
+
const parsed = parse(args, {
|
|
462
|
+
short: "tS",
|
|
463
|
+
long: ["target-directory", "suffix"],
|
|
464
|
+
});
|
|
465
|
+
const into = last(parsed, "t", "target-directory");
|
|
466
|
+
return into === undefined ? parsed.operands.slice(0, -1) : parsed.operands;
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* The files a command removes, as the command names them. A move removes
|
|
471
|
+
* its sources, since what was there is no longer there.
|
|
472
|
+
*/
|
|
473
|
+
export function removedBy(argv) {
|
|
474
|
+
const [name, ...args] = argv;
|
|
475
|
+
const reader = name === undefined ? undefined : REMOVERS[name];
|
|
476
|
+
return reader ? reader(args) : [];
|
|
477
|
+
}
|
|
@@ -41,6 +41,43 @@ export type BashWriteKind = "git-mutating" | "bash-write" | "read-only";
|
|
|
41
41
|
* happens to name tracked code.
|
|
42
42
|
*/
|
|
43
43
|
export declare function bashWriteTargets(command: string): string[];
|
|
44
|
+
/** Where a command runs: the session directory and the home `~` names. */
|
|
45
|
+
export interface CommandPlace {
|
|
46
|
+
readonly cwd: string;
|
|
47
|
+
readonly home: string;
|
|
48
|
+
}
|
|
49
|
+
/** The places a bash command writes to, split by whether they are known. */
|
|
50
|
+
export interface ResolvedWrites {
|
|
51
|
+
/** Absolute paths the command writes to. */
|
|
52
|
+
readonly paths: string[];
|
|
53
|
+
/** Absolute paths the command removes. */
|
|
54
|
+
readonly removed: string[];
|
|
55
|
+
/** Targets it writes to whose location the command does not say. */
|
|
56
|
+
readonly unresolved: string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve where a bash command writes, in the directory each writing
|
|
60
|
+
* command actually runs in.
|
|
61
|
+
*
|
|
62
|
+
* The session directory is only where the command starts: `cd <tree> &&
|
|
63
|
+
* cat >> x_test.go` writes inside the tree, and reading it against the
|
|
64
|
+
* session directory is how a write in a tracked tree came to be judged as
|
|
65
|
+
* one in the repository the session happened to open. So the command
|
|
66
|
+
* model is walked in order, each `cd` moving the directory for what
|
|
67
|
+
* follows, with the command's own variables filled in.
|
|
68
|
+
*
|
|
69
|
+
* A target whose location the command does not say is reported as
|
|
70
|
+
* unresolved rather than guessed: a `cd` into a command substitution, a
|
|
71
|
+
* variable from outside the command, or a relative target in a subshell
|
|
72
|
+
* or loop that changes directory. A guess judges a file nobody wrote,
|
|
73
|
+
* while an unresolved target can still be checked on disk afterwards.
|
|
74
|
+
*
|
|
75
|
+
* What a command removes is placed the same way and kept apart, since
|
|
76
|
+
* removing a document is a different question from writing one. It is
|
|
77
|
+
* only read where the command model applies; a removal inside a loop or
|
|
78
|
+
* a subshell is left to the check on disk.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveBashWrites(command: string, place: CommandPlace): ResolvedWrites;
|
|
44
81
|
/**
|
|
45
82
|
* Classify a bash command after stripping non-executable content,
|
|
46
83
|
* so quoted literals and heredoc bodies cannot raise a false
|