wisp-agents 0.17.0 → 0.20.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/CHANGELOG.md +72 -1
- package/README.md +20 -7
- package/bin/wisp.js +4 -2
- package/package.json +2 -2
- package/server/adapters/antigravity.js +2 -1
- package/server/adapters/claude.js +2 -1
- package/server/adapters/codex.js +2 -1
- package/server/adapters/commands.js +808 -0
- package/server/adapters/cursor.js +2 -1
- package/server/adapters/grok.js +2 -1
- package/server/adapters/opencode.js +2 -1
- package/server/check.js +205 -0
- package/server/git.js +114 -2
- package/server/index.js +2 -1
- package/server/judge.js +78 -0
- package/server/machines.js +2 -2
- package/server/openapi.js +19 -2
- package/server/pipeline.js +217 -0
- package/server/quick.js +76 -0
- package/server/router.js +201 -28
- package/server/routes/auto.js +8 -1
- package/server/routes/meta.js +1 -1
- package/server/routes/settings.js +53 -4
- package/server/routes/ship.js +29 -2
- package/server/runner.js +65 -15
- package/server/scout.js +71 -0
- package/server/services/auto.js +115 -15
- package/server/services/launch.js +22 -13
- package/server/services/ship.js +107 -6
- package/server/store.js +15 -0
- package/web/css/auto.css +22 -0
- package/web/css/launcher.css +17 -0
- package/web/css/layout.css +10 -2
- package/web/css/modals.css +8 -0
- package/web/css/palette.css +22 -0
- package/web/css/ship.css +44 -0
- package/web/index.html +16 -5
- package/web/js/app.js +14 -3
- package/web/js/blocks.js +1 -1
- package/web/js/core/state.js +2 -1
- package/web/js/features/actions.js +2 -4
- package/web/js/features/auto.js +38 -7
- package/web/js/features/clones.js +79 -0
- package/web/js/features/composer.js +2 -4
- package/web/js/features/feed.js +34 -4
- package/web/js/features/launcher.js +139 -19
- package/web/js/features/live.js +5 -10
- package/web/js/features/project-list.js +99 -0
- package/web/js/features/projects.js +220 -39
- package/web/js/features/runners.js +7 -6
- package/web/js/features/settings.js +47 -7
- package/web/js/features/setup.js +3 -3
- package/web/js/features/ship.js +128 -25
- package/web/js/features/turns.js +4 -3
- package/web/js/ui/popover.js +10 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
// Turns a shell command into a short English sentence for the live ticker, e.g.
|
|
2
|
+
// `sed -n 15,50p src/a.js` → "Reading lines 15–50 of src/a.js". Best effort: a command we don't
|
|
3
|
+
// know falls back to "Running <command>", and the raw command stays in the activity detail.
|
|
4
|
+
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { clip, rel } from './common.js';
|
|
7
|
+
|
|
8
|
+
export function describeCommand(command, cwd) {
|
|
9
|
+
const raw = String(command ?? '').trim();
|
|
10
|
+
const fallback = `Running ${clip(raw, 60)}`;
|
|
11
|
+
if (!raw) return fallback;
|
|
12
|
+
let parts;
|
|
13
|
+
try {
|
|
14
|
+
parts = segments(stripHeredocs(raw)).map((pipe) => describePipeline(pipe, cwd)).filter(Boolean);
|
|
15
|
+
} catch {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
// Separators like `echo ---` or `cd x` only count when nothing better was found.
|
|
19
|
+
const strong = parts.filter((p) => !p.weak);
|
|
20
|
+
const texts = [...new Set((strong.length ? strong : parts).map((p) => p.text))];
|
|
21
|
+
if (!texts.length || (!strong.length && texts.length > 1) || (texts.length === 1 && parts.every((p) => p.unknown))) return fallback;
|
|
22
|
+
const [first, second] = texts;
|
|
23
|
+
if (!second) return clip(first, 90);
|
|
24
|
+
const more = texts.length > 2 ? ` (+${texts.length - 2} more)` : '';
|
|
25
|
+
return clip(`${first}, then ${lower(second)}${more}`, 90);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lower = (s) => s.charAt(0).toLowerCase() + s.slice(1);
|
|
29
|
+
|
|
30
|
+
// Drop heredoc bodies so their lines aren't read as commands: `cat <<EOF > f\n…\nEOF` → `cat <<EOF > f`.
|
|
31
|
+
function stripHeredocs(s) {
|
|
32
|
+
return s.replace(/(<<-?\s*(['"]?)([\w-]+)\2)([^\n]*)\n[\s\S]*?\n[ \t]*\3[ \t]*(?=\n|$)/g, '$1$4');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Split into a list of pipelines (by && || ; & newline), each a list of stages (by |), each a list
|
|
36
|
+
// of words. Quotes are honoured; $(…) and `…` stay inside the word they appear in.
|
|
37
|
+
function segments(s) {
|
|
38
|
+
const out = [];
|
|
39
|
+
let pipe = [];
|
|
40
|
+
let words = [];
|
|
41
|
+
let word = null;
|
|
42
|
+
const endWord = () => {
|
|
43
|
+
if (word !== null) words.push(word);
|
|
44
|
+
word = null;
|
|
45
|
+
};
|
|
46
|
+
const endStage = () => {
|
|
47
|
+
endWord();
|
|
48
|
+
if (words.length) pipe.push(words);
|
|
49
|
+
words = [];
|
|
50
|
+
};
|
|
51
|
+
const endPipe = () => {
|
|
52
|
+
endStage();
|
|
53
|
+
if (pipe.length) out.push(pipe);
|
|
54
|
+
pipe = [];
|
|
55
|
+
};
|
|
56
|
+
for (let i = 0; i < s.length; i++) {
|
|
57
|
+
const c = s[i];
|
|
58
|
+
if (c === "'") {
|
|
59
|
+
const j = s.indexOf("'", i + 1);
|
|
60
|
+
const end = j < 0 ? s.length : j;
|
|
61
|
+
word = (word ?? '') + s.slice(i + 1, end);
|
|
62
|
+
i = end;
|
|
63
|
+
} else if (c === '"') {
|
|
64
|
+
let j = i + 1;
|
|
65
|
+
let buf = '';
|
|
66
|
+
while (j < s.length && s[j] !== '"') {
|
|
67
|
+
if (s[j] === '\\' && j + 1 < s.length) j++;
|
|
68
|
+
buf += s[j++];
|
|
69
|
+
}
|
|
70
|
+
word = (word ?? '') + buf;
|
|
71
|
+
i = j;
|
|
72
|
+
} else if (c === '\\' && i + 1 < s.length) {
|
|
73
|
+
if (s[i + 1] !== '\n') word = (word ?? '') + s[i + 1];
|
|
74
|
+
i++;
|
|
75
|
+
} else if (c === '$' && s[i + 1] === '(') {
|
|
76
|
+
let depth = 0;
|
|
77
|
+
let j = i + 1;
|
|
78
|
+
for (; j < s.length; j++) {
|
|
79
|
+
if (s[j] === '(') depth++;
|
|
80
|
+
else if (s[j] === ')' && --depth === 0) break;
|
|
81
|
+
}
|
|
82
|
+
word = (word ?? '') + s.slice(i, j + 1);
|
|
83
|
+
i = j;
|
|
84
|
+
} else if (c === '`') {
|
|
85
|
+
const j = s.indexOf('`', i + 1);
|
|
86
|
+
const end = j < 0 ? s.length : j;
|
|
87
|
+
word = (word ?? '') + s.slice(i, end + 1);
|
|
88
|
+
i = end;
|
|
89
|
+
} else if (c === '|' && s[i + 1] !== '|') {
|
|
90
|
+
endStage();
|
|
91
|
+
} else if ((c === '&' && s[i + 1] === '&') || (c === '|' && s[i + 1] === '|')) {
|
|
92
|
+
endPipe();
|
|
93
|
+
i++;
|
|
94
|
+
} else if (c === ';' || c === '\n' || (c === '&' && s[i + 1] !== '>' && s[i - 1] !== '>')) {
|
|
95
|
+
endPipe();
|
|
96
|
+
} else if (c === '&' && s[i + 1] === '>') {
|
|
97
|
+
endWord();
|
|
98
|
+
words.push(s[i + 2] === '>' ? '&>>' : '&>');
|
|
99
|
+
i += s[i + 2] === '>' ? 2 : 1;
|
|
100
|
+
} else if (c === '>' || c === '<') {
|
|
101
|
+
// Redirects become their own word ('>', '>>', '<', '2>&1' …) so stages can read targets.
|
|
102
|
+
endWord();
|
|
103
|
+
let op = c;
|
|
104
|
+
if (/^\d$/.test(words.at(-1) ?? '') && /\s/.test(s[i - 1] ?? '') === false) op = words.pop() + op;
|
|
105
|
+
while (s[i + 1] === '>' || s[i + 1] === '&' || s[i + 1] === '<') op += s[++i];
|
|
106
|
+
if (/&$/.test(op) && /\d/.test(s[i + 1] ?? '')) op += s[++i];
|
|
107
|
+
words.push(op);
|
|
108
|
+
} else if (/\s/.test(c)) {
|
|
109
|
+
endWord();
|
|
110
|
+
} else if ((c === '(' || c === ')' || c === '{' || c === '}') && word === null) {
|
|
111
|
+
endWord(); // subshells and groups: describe what's inside
|
|
112
|
+
} else {
|
|
113
|
+
word = (word ?? '') + c;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
endPipe();
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const WRAPPERS = new Set(['sudo', 'time', 'nohup', 'command', 'exec', 'nice', 'caffeinate', 'stdbuf', 'unbuffer']);
|
|
121
|
+
const CONTROL = new Set(['then', 'else', 'elif', 'do', 'done', 'fi', 'esac', '!']);
|
|
122
|
+
|
|
123
|
+
// Peel off env assignments, wrappers and redirects. Returns { argv, out } with `out` the file a
|
|
124
|
+
// `>`/`>>` redirect writes to (ignoring /dev/null and fd duplications).
|
|
125
|
+
function parseStage(words) {
|
|
126
|
+
const argv = [];
|
|
127
|
+
let out = null;
|
|
128
|
+
let input = null;
|
|
129
|
+
for (let i = 0; i < words.length; i++) {
|
|
130
|
+
const w = words[i];
|
|
131
|
+
if (/^\d*(>>?|&>>?)$/.test(w)) {
|
|
132
|
+
const target = words[++i];
|
|
133
|
+
if (target && target !== '/dev/null') out = target;
|
|
134
|
+
} else if (/^\d*<$/.test(w)) {
|
|
135
|
+
input = words[++i];
|
|
136
|
+
} else if (/^\d*[<>]+&?\d*$/.test(w) || /^<<-?/.test(w)) {
|
|
137
|
+
if (/^<<-?$/.test(w)) i++; // heredoc delimiter
|
|
138
|
+
} else {
|
|
139
|
+
argv.push(w);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (;;) {
|
|
143
|
+
const w = argv[0];
|
|
144
|
+
if (w === undefined) break;
|
|
145
|
+
if (/^[A-Za-z_]\w*=/.test(w) || CONTROL.has(w)) argv.shift();
|
|
146
|
+
else if (w === 'env') {
|
|
147
|
+
argv.shift();
|
|
148
|
+
while (argv[0] && (/^-/.test(argv[0]) || /^[A-Za-z_]\w*=/.test(argv[0]))) argv.shift();
|
|
149
|
+
} else if (WRAPPERS.has(w)) {
|
|
150
|
+
argv.shift();
|
|
151
|
+
while (argv[0] && /^-/.test(argv[0])) argv.shift();
|
|
152
|
+
} else if (w === 'timeout' || w === 'gtimeout') {
|
|
153
|
+
argv.shift();
|
|
154
|
+
while (argv[0] && /^-/.test(argv[0])) argv.shift();
|
|
155
|
+
argv.shift(); // the duration
|
|
156
|
+
} else if (w === 'xargs') {
|
|
157
|
+
argv.shift();
|
|
158
|
+
while (argv[0] && /^-/.test(argv[0])) argv.splice(0, /^-[IdLnPsE]$/.test(argv[0]) ? 2 : 1);
|
|
159
|
+
} else break;
|
|
160
|
+
}
|
|
161
|
+
return { argv, out, input };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A pipeline is described by its first stage that says something (`git log | head` → git log).
|
|
165
|
+
function describePipeline(stages, cwd) {
|
|
166
|
+
let best = null;
|
|
167
|
+
for (const words of stages) {
|
|
168
|
+
const stage = parseStage(words);
|
|
169
|
+
if (!stage.argv.length) continue;
|
|
170
|
+
const d = describeStage(stage, cwd, stages.length > 1);
|
|
171
|
+
if (d && !d.weak && !d.unknown) return d;
|
|
172
|
+
best ??= d;
|
|
173
|
+
}
|
|
174
|
+
return best;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = (text, extra) => ({ text, ...extra });
|
|
178
|
+
const weak = (text) => ({ text, weak: true });
|
|
179
|
+
|
|
180
|
+
function describeStage({ argv, out, input }, cwd, piped) {
|
|
181
|
+
const [cmd0, ...args] = argv;
|
|
182
|
+
const cmd = path.basename(cmd0);
|
|
183
|
+
const f = (p) => short(p, cwd);
|
|
184
|
+
const files = operands(args);
|
|
185
|
+
const target = files.length ? files.map(f).slice(0, 2).join(', ') + (files.length > 2 ? ` +${files.length - 2}` : '') : '';
|
|
186
|
+
|
|
187
|
+
if (out && /^(echo|printf|cat|tee)$/.test(cmd)) return result(`Writing ${f(out)}`);
|
|
188
|
+
|
|
189
|
+
switch (cmd) {
|
|
190
|
+
case 'cd':
|
|
191
|
+
case 'pushd':
|
|
192
|
+
case 'popd':
|
|
193
|
+
return weak(args[0] ? `Going to ${f(args[0])}` : 'Changing folder');
|
|
194
|
+
case 'export':
|
|
195
|
+
case 'set':
|
|
196
|
+
case 'unset':
|
|
197
|
+
case 'source':
|
|
198
|
+
case '.':
|
|
199
|
+
case 'true':
|
|
200
|
+
case 'false':
|
|
201
|
+
case 'shift':
|
|
202
|
+
case 'local':
|
|
203
|
+
case 'read':
|
|
204
|
+
case 'trap':
|
|
205
|
+
case ':':
|
|
206
|
+
case 'for':
|
|
207
|
+
case 'case':
|
|
208
|
+
return weak('Setting up the shell');
|
|
209
|
+
case 'echo':
|
|
210
|
+
case 'printf':
|
|
211
|
+
return weak('Printing a message');
|
|
212
|
+
case 'cat':
|
|
213
|
+
case 'bat':
|
|
214
|
+
case 'less':
|
|
215
|
+
case 'more':
|
|
216
|
+
return files.length ? result(`Reading ${target}`) : piped ? weak('Reading input') : null;
|
|
217
|
+
case 'nl':
|
|
218
|
+
return files.length ? result(`Reading ${target}`) : null;
|
|
219
|
+
case 'head':
|
|
220
|
+
case 'tail': {
|
|
221
|
+
if (cmd === 'tail' && args.some((a) => /^-[a-zA-Z]*[fF]/.test(a))) return result(files.length ? `Watching ${target}` : 'Watching output');
|
|
222
|
+
const n = countArg(args);
|
|
223
|
+
if (!files.length) return weak(n ? `Taking the ${cmd === 'head' ? 'first' : 'last'} ${n} lines` : 'Trimming output');
|
|
224
|
+
return result(n ? `Reading the ${cmd === 'head' ? 'first' : 'last'} ${n} lines of ${target}` : `Reading the ${cmd === 'head' ? 'start' : 'end'} of ${target}`);
|
|
225
|
+
}
|
|
226
|
+
case 'sed':
|
|
227
|
+
case 'gsed':
|
|
228
|
+
return describeSed(args, f, piped);
|
|
229
|
+
case 'awk':
|
|
230
|
+
case 'gawk': {
|
|
231
|
+
const file = operands(args).slice(1);
|
|
232
|
+
return file.length ? result(`Reading ${f(file[0])}`) : weak('Processing text');
|
|
233
|
+
}
|
|
234
|
+
case 'grep':
|
|
235
|
+
case 'egrep':
|
|
236
|
+
case 'fgrep':
|
|
237
|
+
case 'rg':
|
|
238
|
+
case 'ag':
|
|
239
|
+
case 'ack':
|
|
240
|
+
return describeGrep(cmd, args, f, piped);
|
|
241
|
+
case 'find':
|
|
242
|
+
case 'fd': {
|
|
243
|
+
const name = valueOf(args, ['-name', '-iname', '-path', '-ipath']) ?? (cmd === 'fd' ? operands(args)[0] : null);
|
|
244
|
+
const where = cmd === 'find' ? (args[0] && !args[0].startsWith('-') ? args[0] : '') : operands(args)[1];
|
|
245
|
+
const inWhere = where && where !== '.' ? ` in ${f(where)}` : '';
|
|
246
|
+
if (args.includes('-delete')) return result(`Deleting ${name ? `${name} files` : 'files'}${inWhere}`);
|
|
247
|
+
return result(name ? `Finding ${clip(name, 40)}${inWhere}` : `Finding files${inWhere}`);
|
|
248
|
+
}
|
|
249
|
+
case 'ls':
|
|
250
|
+
case 'eza':
|
|
251
|
+
case 'exa':
|
|
252
|
+
case 'tree':
|
|
253
|
+
return result(files.length ? `Listing ${target}` : cmd === 'tree' ? 'Looking at the folder tree' : 'Listing files');
|
|
254
|
+
case 'pwd':
|
|
255
|
+
return weak('Checking the current folder');
|
|
256
|
+
case 'wc':
|
|
257
|
+
return files.length ? result(`Counting ${args.includes('-l') ? 'lines' : 'words'} in ${target}`) : weak('Counting lines');
|
|
258
|
+
case 'sort':
|
|
259
|
+
case 'uniq':
|
|
260
|
+
case 'cut':
|
|
261
|
+
case 'tr':
|
|
262
|
+
case 'column':
|
|
263
|
+
case 'fold':
|
|
264
|
+
case 'rev':
|
|
265
|
+
return files.length ? result(`Reading ${target}`) : weak('Processing text');
|
|
266
|
+
case 'jq':
|
|
267
|
+
case 'yq':
|
|
268
|
+
return result(operands(args)[1] ? `Reading ${f(operands(args)[1])}` : 'Parsing JSON', operands(args)[1] ? {} : { weak: piped });
|
|
269
|
+
case 'diff':
|
|
270
|
+
case 'cmp':
|
|
271
|
+
case 'colordiff':
|
|
272
|
+
return result(files.length >= 2 ? `Comparing ${f(files[0])} and ${f(files[1])}` : 'Comparing files');
|
|
273
|
+
case 'stat':
|
|
274
|
+
case 'file':
|
|
275
|
+
return result(`Inspecting ${target || 'a file'}`);
|
|
276
|
+
case 'du':
|
|
277
|
+
case 'df':
|
|
278
|
+
return result(`Checking disk usage${target ? ` of ${target}` : ''}`);
|
|
279
|
+
case 'mkdir':
|
|
280
|
+
return result(`Creating folder ${target}`);
|
|
281
|
+
case 'touch':
|
|
282
|
+
return result(`Creating ${target}`);
|
|
283
|
+
case 'rm':
|
|
284
|
+
case 'rmdir':
|
|
285
|
+
case 'trash':
|
|
286
|
+
return result(`Deleting ${target || 'files'}`);
|
|
287
|
+
case 'mv':
|
|
288
|
+
return result(files.length === 2 ? `Moving ${f(files[0])} to ${f(files[1])}` : `Moving ${target}`);
|
|
289
|
+
case 'cp':
|
|
290
|
+
case 'rsync':
|
|
291
|
+
return result(files.length === 2 ? `Copying ${f(files[0])} to ${f(files[1])}` : `Copying ${target}`);
|
|
292
|
+
case 'ln':
|
|
293
|
+
return result(`Linking ${target}`);
|
|
294
|
+
case 'chmod':
|
|
295
|
+
case 'chown':
|
|
296
|
+
return result(`Changing permissions of ${operands(args).slice(1).map(f).join(', ') || 'files'}`);
|
|
297
|
+
case 'tee':
|
|
298
|
+
return result(files.length ? `Writing ${target}` : 'Saving output');
|
|
299
|
+
case 'tar':
|
|
300
|
+
case 'unzip':
|
|
301
|
+
case 'gunzip':
|
|
302
|
+
case '7z':
|
|
303
|
+
return result(/^(-?[a-z]*c|zip)/.test(args[0] ?? '') && cmd === 'tar' ? 'Creating an archive' : `Extracting ${target || 'an archive'}`);
|
|
304
|
+
case 'zip':
|
|
305
|
+
return result('Creating an archive');
|
|
306
|
+
case 'curl':
|
|
307
|
+
case 'wget':
|
|
308
|
+
case 'http':
|
|
309
|
+
case 'xh': {
|
|
310
|
+
const url = args.find((a) => /^https?:\/\//.test(a) || /^(localhost|127\.0\.0\.1|[\w.-]+\.[a-z]{2,})(?:[/:]|$)/.test(a));
|
|
311
|
+
return result(url ? `Fetching ${host(url)}` : 'Fetching a URL');
|
|
312
|
+
}
|
|
313
|
+
case 'open':
|
|
314
|
+
case 'xdg-open':
|
|
315
|
+
return result(`Opening ${target || 'a file'}`);
|
|
316
|
+
case 'sleep':
|
|
317
|
+
return result(`Waiting ${args[0] ?? ''}${/^\d+$/.test(args[0] ?? '') ? 's' : ''}`.trim());
|
|
318
|
+
case 'wait':
|
|
319
|
+
return result('Waiting for background jobs');
|
|
320
|
+
case 'until':
|
|
321
|
+
case 'while': {
|
|
322
|
+
const inner = describeStage(parseStage(args), cwd, piped);
|
|
323
|
+
return result(inner && !inner.unknown ? `Waiting on: ${lower(inner.text)}` : 'Waiting for something to change');
|
|
324
|
+
}
|
|
325
|
+
case 'if':
|
|
326
|
+
case 'test':
|
|
327
|
+
case '[':
|
|
328
|
+
case '[[':
|
|
329
|
+
return weak('Checking a condition');
|
|
330
|
+
case 'ps':
|
|
331
|
+
case 'pgrep':
|
|
332
|
+
case 'top':
|
|
333
|
+
case 'htop':
|
|
334
|
+
return result('Checking running processes');
|
|
335
|
+
case 'kill':
|
|
336
|
+
case 'pkill':
|
|
337
|
+
case 'killall':
|
|
338
|
+
return result('Stopping a process');
|
|
339
|
+
case 'lsof': {
|
|
340
|
+
const port = args.find((a) => /^-?i?:\d+$/.test(a) || /^tcp:\d+$/.test(a));
|
|
341
|
+
return result(port ? `Checking what is using port ${port.replace(/\D/g, '')}` : 'Checking open files');
|
|
342
|
+
}
|
|
343
|
+
case 'which':
|
|
344
|
+
case 'whereis':
|
|
345
|
+
case 'type':
|
|
346
|
+
return result(`Checking if ${operands(args)[0] ?? 'a tool'} is installed`);
|
|
347
|
+
case 'env':
|
|
348
|
+
case 'printenv':
|
|
349
|
+
return result('Checking environment variables');
|
|
350
|
+
case 'uname':
|
|
351
|
+
case 'sw_vers':
|
|
352
|
+
case 'hostname':
|
|
353
|
+
case 'whoami':
|
|
354
|
+
case 'id':
|
|
355
|
+
return result('Checking the system');
|
|
356
|
+
case 'date':
|
|
357
|
+
return weak('Checking the time');
|
|
358
|
+
case 'git':
|
|
359
|
+
return describeGit(args, f);
|
|
360
|
+
case 'gh':
|
|
361
|
+
return describeGh(args);
|
|
362
|
+
case 'npm':
|
|
363
|
+
case 'pnpm':
|
|
364
|
+
case 'yarn':
|
|
365
|
+
case 'bun':
|
|
366
|
+
return describePackageManager(cmd, args, cwd);
|
|
367
|
+
case 'npx':
|
|
368
|
+
case 'bunx':
|
|
369
|
+
case 'pnpx':
|
|
370
|
+
return describeTool(stripFlags(args), cwd, true);
|
|
371
|
+
case 'node':
|
|
372
|
+
case 'deno':
|
|
373
|
+
case 'tsx':
|
|
374
|
+
case 'ts-node':
|
|
375
|
+
if (cmd === 'deno' && args[0] === 'test') return result('Running tests');
|
|
376
|
+
if (args.includes('--test')) return result('Running tests');
|
|
377
|
+
if (args.some((a) => a === '-e' || a === '--eval' || a === '-p' || a === '--print')) return result('Running a Node snippet');
|
|
378
|
+
if (args.includes('--version') || args.includes('-v')) return result('Checking the Node version');
|
|
379
|
+
return result(operands(args)[0] ? `Running ${f(operands(args)[0])}` : 'Running a Node script');
|
|
380
|
+
case 'python':
|
|
381
|
+
case 'python3':
|
|
382
|
+
case 'py':
|
|
383
|
+
return describePython(args, f, input);
|
|
384
|
+
case 'uv':
|
|
385
|
+
case 'poetry':
|
|
386
|
+
case 'pipenv':
|
|
387
|
+
if (/^(add|sync|install|lock)$/.test(args[0] ?? '')) return result('Installing Python packages');
|
|
388
|
+
if (args[0] === 'pip') return describePip(args.slice(1));
|
|
389
|
+
if (args[0] === 'run') return describeTool(stripFlags(args.slice(1)), cwd, false) ?? result('Running a Python script');
|
|
390
|
+
return null;
|
|
391
|
+
case 'pip':
|
|
392
|
+
case 'pip3':
|
|
393
|
+
return describePip(args);
|
|
394
|
+
case 'cargo':
|
|
395
|
+
return result(
|
|
396
|
+
{ build: 'Building the project', b: 'Building the project', test: 'Running tests', t: 'Running tests', run: 'Running the program', r: 'Running the program', check: 'Checking the code compiles', c: 'Checking the code compiles', clippy: 'Linting', fmt: 'Formatting code', add: 'Adding a dependency', install: 'Installing a crate', bench: 'Running benchmarks', doc: 'Building docs' }[args[0]] ?? `Running cargo ${args[0] ?? ''}`.trim()
|
|
397
|
+
);
|
|
398
|
+
case 'go':
|
|
399
|
+
return result(
|
|
400
|
+
{ build: 'Building the project', test: 'Running tests', run: 'Running the program', vet: 'Checking the code', fmt: 'Formatting code', get: 'Adding a dependency', install: 'Installing a Go tool', mod: args[1] === 'tidy' ? 'Tidying Go modules' : 'Managing Go modules', generate: 'Generating code' }[args[0]] ?? `Running go ${args[0] ?? ''}`.trim()
|
|
401
|
+
);
|
|
402
|
+
case 'make':
|
|
403
|
+
case 'just':
|
|
404
|
+
case 'task': {
|
|
405
|
+
const t = operands(args)[0];
|
|
406
|
+
return t ? describeTarget(t) : result(`Building with ${cmd}`);
|
|
407
|
+
}
|
|
408
|
+
case 'docker':
|
|
409
|
+
case 'podman':
|
|
410
|
+
return describeDocker(args);
|
|
411
|
+
case 'brew':
|
|
412
|
+
return result(/^(install|upgrade|reinstall)$/.test(args[0] ?? '') ? `Installing ${operands(args.slice(1)).join(', ') || 'packages'}` : args[0] === 'list' ? 'Listing installed packages' : `Running brew ${args[0] ?? ''}`.trim());
|
|
413
|
+
case 'ssh':
|
|
414
|
+
return result(`Connecting to ${operands(args)[0] ?? 'a server'}`);
|
|
415
|
+
case 'scp':
|
|
416
|
+
return result('Copying files over SSH');
|
|
417
|
+
case 'claude':
|
|
418
|
+
case 'codex':
|
|
419
|
+
case 'gemini':
|
|
420
|
+
case 'cursor-agent':
|
|
421
|
+
case 'opencode':
|
|
422
|
+
return result(`Asking ${cmd === 'cursor-agent' ? 'Cursor' : cmd.charAt(0).toUpperCase() + cmd.slice(1)}`);
|
|
423
|
+
case 'bash':
|
|
424
|
+
case 'sh':
|
|
425
|
+
case 'zsh':
|
|
426
|
+
if (args[0] === '-c' || args[0] === '-lc') return describeInner(args[1], cwd);
|
|
427
|
+
return result(operands(args)[0] ? `Running ${f(operands(args)[0])}` : 'Running a shell script');
|
|
428
|
+
default:
|
|
429
|
+
return describeTool(argv, cwd, false);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function describeInner(script, cwd) {
|
|
434
|
+
const label = describeCommand(script, cwd);
|
|
435
|
+
return label === `Running ${clip(String(script ?? '').trim(), 60)}` ? { text: label, unknown: true } : result(label);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Dev tools that run on their own or through npx / pnpm exec / uv run.
|
|
439
|
+
function describeTool(argv, cwd, viaRunner) {
|
|
440
|
+
const [cmd0, ...args] = argv;
|
|
441
|
+
if (!cmd0) return null;
|
|
442
|
+
const name = cmd0.replace(/@[^/]*$/, ''); // npx tsc@5 → tsc
|
|
443
|
+
const cmd = path.basename(name);
|
|
444
|
+
if (/^(jest|vitest|mocha|ava|tap|pytest|playwright|cypress|karma|jasmine|uvu)$/.test(cmd)) {
|
|
445
|
+
if (cmd === 'playwright' && args[0] === 'install') return result('Installing browsers for Playwright');
|
|
446
|
+
return result(args.includes('--watch') ? 'Running tests in watch mode' : 'Running tests');
|
|
447
|
+
}
|
|
448
|
+
if (cmd === 'tsc' || cmd === 'vue-tsc') return result(args.includes('--watch') || args.includes('-w') ? 'Type-checking in watch mode' : 'Type-checking');
|
|
449
|
+
if (/^(eslint|biome|oxlint|stylelint|ruff|flake8|pylint|golangci-lint|rubocop|shellcheck|standard|xo)$/.test(cmd)) {
|
|
450
|
+
if (cmd === 'biome' && args[0] === 'format') return result('Formatting code');
|
|
451
|
+
if (cmd === 'ruff' && args[0] === 'format') return result('Formatting code');
|
|
452
|
+
return result(args.includes('--fix') ? 'Fixing lint issues' : 'Linting');
|
|
453
|
+
}
|
|
454
|
+
if (/^(prettier|black|isort|gofmt|rustfmt|dprint|clang-format)$/.test(cmd)) return result(args.includes('--check') || args.includes('-c') ? 'Checking formatting' : 'Formatting code');
|
|
455
|
+
if (/^(vite|next|nuxt|astro|webpack|rollup|esbuild|parcel|tsup|turbo|nx|svelte-kit|remix)$/.test(cmd)) {
|
|
456
|
+
const sub = args.find((a) => !a.startsWith('-'));
|
|
457
|
+
if (!sub || /^(dev|serve|start|preview)$/.test(sub)) return result(cmd === 'esbuild' || cmd === 'rollup' || cmd === 'tsup' ? 'Building the project' : 'Starting the dev server');
|
|
458
|
+
if (sub === 'build') return result('Building the project');
|
|
459
|
+
if (/^(lint|test|typecheck)$/.test(sub)) return describeTarget(sub);
|
|
460
|
+
return result(`Building the project`);
|
|
461
|
+
}
|
|
462
|
+
if (/^(mypy|pyright)$/.test(cmd)) return result('Type-checking');
|
|
463
|
+
if (cmd === 'electron' || cmd === 'electron-builder' || cmd === 'electron-forge') return result(cmd === 'electron' ? 'Starting the Electron app' : 'Packaging the app');
|
|
464
|
+
if (cmd === 'serve' || cmd === 'http-server') return result('Starting a local web server');
|
|
465
|
+
if (cmd === 'prisma' || cmd === 'drizzle-kit') return result(/migrate|push/.test(args.join(' ')) ? 'Migrating the database' : 'Generating the database client');
|
|
466
|
+
if (cmd === 'create-react-app' || /^create-/.test(cmd)) return result('Scaffolding a new project');
|
|
467
|
+
if (/^\.{0,2}\//.test(cmd0) || /\.(sh|js|mjs|cjs|ts|py|rb)$/.test(cmd0)) return result(`Running ${short(cmd0, cwd)}`);
|
|
468
|
+
return { text: `Running ${viaRunner ? cmd : clip(argv.join(' '), 50)}`, unknown: !viaRunner };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// `make test`, `npm run lint`, `just build` …
|
|
472
|
+
function describeTarget(t) {
|
|
473
|
+
const n = t.replace(/^(npm|pnpm|yarn|bun):/, '');
|
|
474
|
+
if (/^(test|tests|spec|check:test)(:|$)|^test[-_]/.test(n)) return result(n === 'test' || n === 'tests' ? 'Running tests' : `Running tests (${n})`);
|
|
475
|
+
if (/^(build|compile|bundle|dist|package)(:|$)/.test(n)) return result('Building the project');
|
|
476
|
+
if (/^(lint|eslint)(:|$)/.test(n)) return result(/fix/.test(n) ? 'Fixing lint issues' : 'Linting');
|
|
477
|
+
if (/^(format|fmt|prettier)(:|$)/.test(n)) return result('Formatting code');
|
|
478
|
+
if (/^(typecheck|type-check|types|tsc|check-types)(:|$)/.test(n)) return result('Type-checking');
|
|
479
|
+
if (/^(dev|start|serve|watch|preview)(:|$)/.test(n)) return result(n === 'start' ? 'Starting the app' : 'Starting the dev server');
|
|
480
|
+
if (/^(clean)(:|$)/.test(n)) return result('Cleaning build output');
|
|
481
|
+
if (/^(install|deps|setup|bootstrap)(:|$)/.test(n)) return result('Setting up the project');
|
|
482
|
+
if (/^(release|publish|deploy)(:|$)/.test(n)) return result(n.startsWith('deploy') ? 'Deploying' : 'Publishing a release');
|
|
483
|
+
if (/^(bench|benchmark)(:|$)/.test(n)) return result('Running benchmarks');
|
|
484
|
+
if (/^(e2e|integration)(:|$)/.test(n)) return result('Running end-to-end tests');
|
|
485
|
+
if (/^(docs?)(:|$)/.test(n)) return result('Building the docs');
|
|
486
|
+
return result(`Running the ${n} script`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function describePackageManager(pm, args, cwd) {
|
|
490
|
+
const rest = stripFlags(args);
|
|
491
|
+
const [sub, ...more] = rest;
|
|
492
|
+
if (!sub) return result(pm === 'yarn' ? 'Installing dependencies' : `Running ${pm}`);
|
|
493
|
+
if (/^(i|install|ci|add)$/.test(sub)) {
|
|
494
|
+
const pkgs = operands(more);
|
|
495
|
+
return result(pkgs.length ? `Installing ${clip(pkgs.join(', '), 50)}` : 'Installing dependencies');
|
|
496
|
+
}
|
|
497
|
+
if (/^(un|uninstall|remove|rm)$/.test(sub)) return result(`Removing ${operands(more).join(', ') || 'a dependency'}`);
|
|
498
|
+
if (/^(up|update|upgrade)$/.test(sub)) return result('Updating dependencies');
|
|
499
|
+
if (/^(t|test|tst)$/.test(sub)) return result('Running tests');
|
|
500
|
+
if (sub === 'run' || sub === 'run-script') return more[0] ? describeTarget(more[0]) : result('Listing scripts');
|
|
501
|
+
if (sub === 'exec' || sub === 'dlx' || sub === 'x') return describeTool(stripFlags(more), cwd, true);
|
|
502
|
+
if (sub === 'start') return result('Starting the app');
|
|
503
|
+
if (sub === 'publish') return result('Publishing the package');
|
|
504
|
+
if (sub === 'version') return result(more[0] ? `Bumping the version (${more[0]})` : 'Bumping the version');
|
|
505
|
+
if (sub === 'pack') return result('Packing the package');
|
|
506
|
+
if (sub === 'outdated') return result('Checking for outdated dependencies');
|
|
507
|
+
if (sub === 'audit') return result('Auditing dependencies');
|
|
508
|
+
if (/^(ls|list|why|explain)$/.test(sub)) return result('Inspecting dependencies');
|
|
509
|
+
if (/^(view|info|show)$/.test(sub)) return result(`Looking up ${more[0] ?? 'a package'} on the registry`);
|
|
510
|
+
if (sub === 'init' || sub === 'create') return result('Scaffolding a new project');
|
|
511
|
+
if (sub === 'link') return result('Linking a local package');
|
|
512
|
+
if (sub === 'config') return result('Checking npm settings');
|
|
513
|
+
if (/^(-v|--version)$/.test(args[0])) return result(`Checking the ${pm} version`);
|
|
514
|
+
// `yarn build`, `pnpm lint`, `bun dev`: a script name directly.
|
|
515
|
+
if (pm !== 'npm') return describeTarget(sub);
|
|
516
|
+
return result(`Running npm ${sub}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function describePip(args) {
|
|
520
|
+
const [sub, ...more] = stripFlags(args);
|
|
521
|
+
if (sub === 'install') return result(operands(more).length && !more.includes('-r') ? `Installing ${clip(operands(more).join(', '), 50)}` : 'Installing Python packages');
|
|
522
|
+
if (sub === 'uninstall') return result('Removing Python packages');
|
|
523
|
+
if (sub === 'list' || sub === 'freeze' || sub === 'show') return result('Checking installed Python packages');
|
|
524
|
+
return result(`Running pip ${sub ?? ''}`.trim());
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function describePython(args, f, input) {
|
|
528
|
+
if (args[0] === '-c') return result('Running a Python snippet');
|
|
529
|
+
if (args[0] === '-m') {
|
|
530
|
+
const mod = args[1];
|
|
531
|
+
if (mod === 'pytest' || mod === 'unittest') return result('Running tests');
|
|
532
|
+
if (mod === 'pip') return describePip(args.slice(2));
|
|
533
|
+
if (mod === 'venv' || mod === 'virtualenv') return result('Creating a virtual environment');
|
|
534
|
+
if (mod === 'http.server') return result('Starting a local web server');
|
|
535
|
+
if (mod === 'json.tool') return result('Formatting JSON');
|
|
536
|
+
return describeTool([mod, ...args.slice(2)], '', false);
|
|
537
|
+
}
|
|
538
|
+
if (args.includes('--version') || args.includes('-V')) return result('Checking the Python version');
|
|
539
|
+
const script = operands(args.filter((a) => !/^-[IuOBESsq]+$/.test(a)))[0];
|
|
540
|
+
if (script && script !== '-') return result(`Running ${f(script)}`);
|
|
541
|
+
return result(input ? `Running a Python script on ${f(input)}` : 'Running a Python script');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function describeSed(args, f, piped) {
|
|
545
|
+
const inPlace = args.some((a) => /^-[a-zA-Z]*i/.test(a) || a.startsWith('--in-place'));
|
|
546
|
+
const scripts = [];
|
|
547
|
+
const rest = [];
|
|
548
|
+
for (let i = 0; i < args.length; i++) {
|
|
549
|
+
const a = args[i];
|
|
550
|
+
if (a === '-e' || a === '--expression') scripts.push(args[++i]);
|
|
551
|
+
else if (a === '-f') i++;
|
|
552
|
+
else if (/^-/.test(a)) continue;
|
|
553
|
+
else if (inPlace && a === '' && args[i - 1] === '-i') continue; // BSD `sed -i ''`
|
|
554
|
+
else rest.push(a);
|
|
555
|
+
}
|
|
556
|
+
if (!scripts.length && rest.length) scripts.push(rest.shift());
|
|
557
|
+
const files = rest;
|
|
558
|
+
const target = files.map(f).slice(0, 2).join(', ');
|
|
559
|
+
if (inPlace) return result(target ? `Editing ${target}` : 'Editing files');
|
|
560
|
+
const script = (scripts[0] ?? '').trim();
|
|
561
|
+
const lines = script.match(/^(\d+|\$)(?:\s*,\s*(\d+|\$))?\s*p$/);
|
|
562
|
+
if (lines && args.some((a) => /^-[a-zA-Z]*n/.test(a))) {
|
|
563
|
+
const [, a, b] = lines;
|
|
564
|
+
const span = !b ? `line ${a}` : b === '$' ? `from line ${a} on` : `lines ${a}–${b}`;
|
|
565
|
+
if (!target) return weak(`Taking ${span}`);
|
|
566
|
+
return result(b === '$' ? `Reading ${target} from line ${a} on` : `Reading ${span} of ${target}`);
|
|
567
|
+
}
|
|
568
|
+
if (target) return result(/^s(.)/.test(script) ? `Rewriting text from ${target}` : `Reading ${target}`);
|
|
569
|
+
return piped ? weak('Processing text') : result('Processing text');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function describeGrep(cmd, args, f, piped) {
|
|
573
|
+
let pattern = null;
|
|
574
|
+
const paths = [];
|
|
575
|
+
const takesValue = /^(-e|-f|-m|-A|-B|-C|-g|--glob|-t|--type|-T|--max-count|--context|--after-context|--before-context|--regexp|--include|--exclude|--exclude-dir|-d|-D)$/;
|
|
576
|
+
let files = false;
|
|
577
|
+
for (let i = 0; i < args.length; i++) {
|
|
578
|
+
const a = args[i];
|
|
579
|
+
if (a === '-e' || a === '--regexp') pattern ??= args[++i];
|
|
580
|
+
else if (a === '-l' || a === '--files' || a === '--files-with-matches') {
|
|
581
|
+
files = files || a === '--files';
|
|
582
|
+
} else if (takesValue.test(a)) i++;
|
|
583
|
+
else if (/^-/.test(a)) continue;
|
|
584
|
+
else if (pattern === null && !(cmd === 'rg' && args.includes('--files'))) pattern = a;
|
|
585
|
+
else paths.push(a);
|
|
586
|
+
}
|
|
587
|
+
if (cmd === 'rg' && args.includes('--files')) return result(paths.length ? `Listing files in ${f(paths[0])}` : 'Listing files');
|
|
588
|
+
if (pattern === null) return piped ? weak('Filtering output') : result('Searching files');
|
|
589
|
+
const where = paths.length ? ` in ${paths.map(f).slice(0, 2).join(', ')}${paths.length > 2 ? ` +${paths.length - 2}` : ''}` : '';
|
|
590
|
+
if (piped && !paths.length) return weak(`Filtering for “${clip(pattern, 40)}”`);
|
|
591
|
+
return result(`Searching for “${clip(pattern, 40)}”${where}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function describeGit(args, f) {
|
|
595
|
+
// Skip global options: git -C dir -c k=v --no-pager status
|
|
596
|
+
let i = 0;
|
|
597
|
+
while (i < args.length && args[i].startsWith('-')) i += /^(-C|-c|--git-dir|--work-tree)$/.test(args[i]) ? 2 : 1;
|
|
598
|
+
const [sub, ...rest] = args.slice(i);
|
|
599
|
+
const ops = operands(rest);
|
|
600
|
+
const has = (...flags) => flags.some((x) => rest.includes(x));
|
|
601
|
+
switch (sub) {
|
|
602
|
+
case undefined:
|
|
603
|
+
return result('Running git');
|
|
604
|
+
case 'status':
|
|
605
|
+
return result('Checking git status');
|
|
606
|
+
case 'diff':
|
|
607
|
+
return result(has('--stat', '--name-only', '--name-status', '--shortstat') ? 'Summarising changes' : has('--cached', '--staged') ? 'Reviewing staged changes' : ops.length ? `Viewing changes${ops.some((o) => o.includes('/') || o.includes('.')) && !ops.some((o) => o.includes('..')) ? ` in ${ops.map(f).slice(0, 2).join(', ')}` : ` (${clip(ops.join(' '), 30)})`}` : 'Viewing changes');
|
|
608
|
+
case 'log':
|
|
609
|
+
case 'shortlog':
|
|
610
|
+
case 'reflog':
|
|
611
|
+
return result(ops.length && !ops[0].includes('..') && /[/.]/.test(ops.at(-1)) ? `Reading the history of ${f(ops.at(-1))}` : 'Reading git history');
|
|
612
|
+
case 'show':
|
|
613
|
+
return result(ops[0] ? (ops[0].includes(':') ? `Reading ${ops[0].split(':').slice(1).join(':')} at ${ops[0].split(':')[0] || 'the index'}` : `Viewing commit ${clip(ops[0], 20)}`) : 'Viewing the last commit');
|
|
614
|
+
case 'blame':
|
|
615
|
+
return result(`Checking who changed ${ops.length ? f(ops.at(-1)) : 'the file'}`);
|
|
616
|
+
case 'add':
|
|
617
|
+
return result(has('-A', '--all', '.') || ops.includes('.') ? 'Staging all changes' : `Staging ${ops.map(f).slice(0, 2).join(', ') || 'changes'}`);
|
|
618
|
+
case 'commit': {
|
|
619
|
+
const m = valueOf(rest, ['-m', '--message']);
|
|
620
|
+
return result(has('--amend') ? 'Amending the last commit' : m ? `Committing “${clip(m.split('\n')[0], 50)}”` : 'Committing changes');
|
|
621
|
+
}
|
|
622
|
+
case 'push':
|
|
623
|
+
return result(has('--tags') ? 'Pushing tags' : ops[1] ? `Pushing ${clip(ops[1], 40)}` : 'Pushing to the remote');
|
|
624
|
+
case 'pull':
|
|
625
|
+
return result('Pulling the latest changes');
|
|
626
|
+
case 'fetch':
|
|
627
|
+
return result(ops[0] ? `Fetching from ${ops[0]}` : 'Fetching from the remote');
|
|
628
|
+
case 'clone':
|
|
629
|
+
return result(`Cloning ${ops[0] ? clip(ops[0].replace(/^https?:\/\/(www\.)?/, '').replace(/\.git$/, ''), 50) : 'a repository'}`);
|
|
630
|
+
case 'checkout':
|
|
631
|
+
case 'switch':
|
|
632
|
+
if (has('-b', '-B', '-c', '-C')) return result(`Creating branch ${valueOf(rest, ['-b', '-B', '-c', '-C']) ?? ''}`.trim());
|
|
633
|
+
if (rest.includes('--')) return result(`Discarding changes to ${operands(rest.slice(rest.indexOf('--') + 1)).map(f).join(', ') || 'files'}`);
|
|
634
|
+
return result(ops[0] ? `Switching to ${clip(ops[0], 40)}` : 'Switching branch');
|
|
635
|
+
case 'restore':
|
|
636
|
+
return result(`Restoring ${ops.map(f).slice(0, 2).join(', ') || 'files'}`);
|
|
637
|
+
case 'branch':
|
|
638
|
+
if (has('-d', '-D', '--delete')) return result(`Deleting branch ${ops[0] ?? ''}`.trim());
|
|
639
|
+
if (has('-m', '-M')) return result('Renaming a branch');
|
|
640
|
+
return result(ops[0] && !has('--list', '-a', '-r', '-v', '-vv') ? `Creating branch ${ops[0]}` : has('--show-current') ? 'Checking the current branch' : 'Listing branches');
|
|
641
|
+
case 'merge':
|
|
642
|
+
return result(has('--abort') ? 'Aborting the merge' : has('--continue') ? 'Finishing the merge' : `Merging ${ops[0] ?? 'changes'}`);
|
|
643
|
+
case 'rebase':
|
|
644
|
+
return result(has('--abort') ? 'Aborting the rebase' : has('--continue') ? 'Continuing the rebase' : `Rebasing onto ${ops[0] ?? 'upstream'}`);
|
|
645
|
+
case 'cherry-pick':
|
|
646
|
+
return result(has('--abort') ? 'Aborting the cherry-pick' : has('--continue') ? 'Continuing the cherry-pick' : `Cherry-picking ${clip(ops.join(' '), 30) || 'a commit'}`);
|
|
647
|
+
case 'revert':
|
|
648
|
+
return result(`Reverting ${ops[0] ?? 'a commit'}`);
|
|
649
|
+
case 'reset':
|
|
650
|
+
return result(has('--hard') ? 'Discarding local changes' : ops.length ? `Unstaging ${ops.map(f).slice(0, 2).join(', ')}` : 'Resetting changes');
|
|
651
|
+
case 'stash': {
|
|
652
|
+
const s = rest[0];
|
|
653
|
+
return result({ pop: 'Restoring stashed changes', apply: 'Restoring stashed changes', list: 'Listing stashes', drop: 'Dropping a stash', show: 'Viewing a stash' }[s] ?? 'Stashing changes');
|
|
654
|
+
}
|
|
655
|
+
case 'tag':
|
|
656
|
+
return result(ops.length && !has('-l', '--list') ? `Tagging ${ops[0]}` : 'Listing tags');
|
|
657
|
+
case 'remote':
|
|
658
|
+
return result(rest[0] === 'add' ? `Adding remote ${rest[1] ?? ''}`.trim() : 'Checking remotes');
|
|
659
|
+
case 'worktree':
|
|
660
|
+
return result({ add: 'Creating a worktree', remove: 'Removing a worktree', list: 'Listing worktrees', prune: 'Cleaning up worktrees' }[rest[0]] ?? 'Managing worktrees');
|
|
661
|
+
case 'rm':
|
|
662
|
+
return result(`Removing ${ops.map(f).slice(0, 2).join(', ') || 'files'} from git`);
|
|
663
|
+
case 'mv':
|
|
664
|
+
return result(ops.length === 2 ? `Renaming ${f(ops[0])} to ${f(ops[1])}` : 'Renaming files');
|
|
665
|
+
case 'grep':
|
|
666
|
+
return describeGrep('git grep', rest, f, false);
|
|
667
|
+
case 'ls-files':
|
|
668
|
+
case 'ls-tree':
|
|
669
|
+
return result('Listing tracked files');
|
|
670
|
+
case 'rev-parse':
|
|
671
|
+
case 'rev-list':
|
|
672
|
+
case 'merge-base':
|
|
673
|
+
case 'describe':
|
|
674
|
+
case 'name-rev':
|
|
675
|
+
case 'symbolic-ref':
|
|
676
|
+
case 'for-each-ref':
|
|
677
|
+
case 'cat-file':
|
|
678
|
+
return result('Inspecting the repository');
|
|
679
|
+
case 'config':
|
|
680
|
+
return result('Checking git settings');
|
|
681
|
+
case 'init':
|
|
682
|
+
return result('Creating a git repository');
|
|
683
|
+
case 'clean':
|
|
684
|
+
return result('Removing untracked files');
|
|
685
|
+
case 'bisect':
|
|
686
|
+
return result('Bisecting history');
|
|
687
|
+
case 'submodule':
|
|
688
|
+
return result('Updating submodules');
|
|
689
|
+
case 'apply':
|
|
690
|
+
case 'am':
|
|
691
|
+
return result('Applying a patch');
|
|
692
|
+
case 'format-patch':
|
|
693
|
+
return result('Exporting a patch');
|
|
694
|
+
default:
|
|
695
|
+
return result(`Running git ${sub}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function describeGh(args) {
|
|
700
|
+
const [area, action, ...rest] = args;
|
|
701
|
+
const ops = operands(rest);
|
|
702
|
+
const num = ops.find((o) => /^#?\d+$/.test(o));
|
|
703
|
+
const ref = num ? ` #${num.replace('#', '')}` : '';
|
|
704
|
+
if (area === 'pr') {
|
|
705
|
+
return result(
|
|
706
|
+
{
|
|
707
|
+
create: 'Opening a pull request',
|
|
708
|
+
view: `Viewing pull request${ref}`,
|
|
709
|
+
list: 'Listing pull requests',
|
|
710
|
+
checks: `Checking CI on pull request${ref}`,
|
|
711
|
+
merge: `Merging pull request${ref}`,
|
|
712
|
+
diff: `Viewing the diff of pull request${ref}`,
|
|
713
|
+
checkout: `Checking out pull request${ref}`,
|
|
714
|
+
co: `Checking out pull request${ref}`,
|
|
715
|
+
comment: `Commenting on pull request${ref}`,
|
|
716
|
+
review: `Reviewing pull request${ref}`,
|
|
717
|
+
edit: `Editing pull request${ref}`,
|
|
718
|
+
close: `Closing pull request${ref}`,
|
|
719
|
+
reopen: `Reopening pull request${ref}`,
|
|
720
|
+
ready: `Marking pull request${ref} ready`,
|
|
721
|
+
status: 'Checking pull request status',
|
|
722
|
+
}[action] ?? 'Working with pull requests'
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
if (area === 'issue') {
|
|
726
|
+
return result({ create: 'Opening an issue', view: `Reading issue${ref}`, list: 'Listing issues', comment: `Commenting on issue${ref}`, close: `Closing issue${ref}`, edit: `Editing issue${ref}` }[action] ?? 'Working with issues');
|
|
727
|
+
}
|
|
728
|
+
if (area === 'run') return result({ list: 'Listing CI runs', view: 'Checking a CI run', watch: 'Watching a CI run', rerun: 'Re-running CI' }[action] ?? 'Checking CI runs');
|
|
729
|
+
if (area === 'workflow') return result(action === 'run' ? 'Starting a workflow' : 'Checking workflows');
|
|
730
|
+
if (area === 'release') return result({ create: 'Publishing a GitHub release', view: 'Viewing a release', list: 'Listing releases', edit: 'Editing a release', upload: 'Uploading release files' }[action] ?? 'Working with releases');
|
|
731
|
+
if (area === 'repo') return result({ view: 'Viewing the repository', clone: 'Cloning a repository', create: 'Creating a repository', fork: 'Forking a repository' }[action] ?? 'Working with the repository');
|
|
732
|
+
if (area === 'api') return result(`Calling the GitHub API${action && !action.startsWith('-') ? ` (${clip(action.replace(/^\/?repos\/[^/]+\/[^/]+\/?/, ''), 30) || 'repo'})` : ''}`);
|
|
733
|
+
if (area === 'auth') return result('Checking GitHub sign-in');
|
|
734
|
+
if (area === 'search') return result('Searching GitHub');
|
|
735
|
+
return result(`Running gh ${area ?? ''}`.trim());
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function describeDocker(args) {
|
|
739
|
+
const [sub, action] = stripFlags(args);
|
|
740
|
+
if (sub === 'compose' || sub === 'docker-compose') return result({ up: 'Starting containers', down: 'Stopping containers', build: 'Building containers', logs: 'Reading container logs', ps: 'Listing containers', exec: 'Running a command in a container' }[action] ?? 'Managing containers');
|
|
741
|
+
return result({ build: 'Building a Docker image', run: 'Starting a container', ps: 'Listing containers', exec: 'Running a command in a container', logs: 'Reading container logs', pull: 'Pulling a Docker image', push: 'Pushing a Docker image', stop: 'Stopping a container', images: 'Listing Docker images' }[sub] ?? `Running docker ${sub ?? ''}`.trim());
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// --- argv helpers ---
|
|
745
|
+
|
|
746
|
+
// Arguments that aren't flags, skipping the value of `-n 20`-style options.
|
|
747
|
+
function operands(args) {
|
|
748
|
+
const out = [];
|
|
749
|
+
for (let i = 0; i < args.length; i++) {
|
|
750
|
+
const a = args[i];
|
|
751
|
+
if (a === '--') {
|
|
752
|
+
out.push(...args.slice(i + 1));
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
if (/^-/.test(a)) {
|
|
756
|
+
if (/^-[nABCmdeoItLX]$/.test(a) && args[i + 1] !== undefined && !/^-/.test(args[i + 1])) i++;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
out.push(a);
|
|
760
|
+
}
|
|
761
|
+
return out;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Drop leading flags (and --flag=value) so the subcommand comes first.
|
|
765
|
+
function stripFlags(args) {
|
|
766
|
+
const out = [...args];
|
|
767
|
+
while (out[0]?.startsWith('-')) out.shift();
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function valueOf(args, names) {
|
|
772
|
+
for (let i = 0; i < args.length; i++) {
|
|
773
|
+
if (names.includes(args[i])) return args[i + 1];
|
|
774
|
+
for (const n of names) if (n.startsWith('--') && args[i].startsWith(`${n}=`)) return args[i].slice(n.length + 1);
|
|
775
|
+
}
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// `head -n 20`, `head -20`, `head -n20`, `--lines=20`
|
|
780
|
+
function countArg(args) {
|
|
781
|
+
for (let i = 0; i < args.length; i++) {
|
|
782
|
+
const a = args[i];
|
|
783
|
+
if ((a === '-n' || a === '-c') && /^\+?\d+$/.test(args[i + 1] ?? '')) return args[i + 1].replace('+', '');
|
|
784
|
+
const m = a.match(/^-n?(\d+)$/) || a.match(/^--lines=(\d+)$/);
|
|
785
|
+
if (m) return m[1];
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function host(url) {
|
|
791
|
+
try {
|
|
792
|
+
const u = new URL(/^https?:/.test(url) ? url : `https://${url}`);
|
|
793
|
+
return u.hostname.replace(/^www\./, '') + (u.hostname === 'localhost' || /^127\./.test(u.hostname) ? (u.port ? `:${u.port}` : '') : '');
|
|
794
|
+
} catch {
|
|
795
|
+
return clip(url, 40);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Paths relative to the project, and long ones shortened to their tail.
|
|
800
|
+
function short(p, cwd) {
|
|
801
|
+
if (!p) return '';
|
|
802
|
+
let s = cwd ? rel(p, cwd) : p;
|
|
803
|
+
if (s.length > 48) {
|
|
804
|
+
const parts = s.split(/[\\/]/).filter(Boolean);
|
|
805
|
+
s = parts.length > 2 ? `…/${parts.slice(-2).join('/')}` : clip(s, 48);
|
|
806
|
+
}
|
|
807
|
+
return s;
|
|
808
|
+
}
|