buninu 0.4.3 → 0.4.6
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 +27 -0
- package/README.md +2 -1
- package/apps/bunmsh/CHANGELOG.md +34 -0
- package/apps/bunmsh/index.js +7 -0
- package/apps/bunmsh/package.json +3 -3
- package/apps/bunmsh/test/curl.test.js +1 -1
- package/apps/bunmsh/test/reference.test.js +26 -12
- package/apps/bunmsh/test/shell1.test.js +1065 -0
- package/apps/bunmsh/test/shell2.test.js +457 -0
- package/apps/bunmsh/test/shell3.test.js +741 -0
- package/apps/jsgotty/gotty.js +26 -6
- package/apps/jsgotty/package-lock.json +2 -2
- package/apps/jsgotty/package.json +2 -1
- package/apps/jsgotty/tests/websocket-origin.test.js +54 -0
- package/apps/jsmdcui/README.md +14 -0
- package/apps/jsmdcui/package.json +1 -1
- package/apps/jsmdcui/runtime/help/cdp.md +6 -0
- package/apps/jsmdcui/runtime/help/help.md +14 -0
- package/apps/jsmdcui/runtime/jsplugins/cdp/cdp-server.js +112 -34
- package/apps/jsmdcui/runtime/jsplugins/cdp/cdp.js +3 -1
- package/apps/jsmdcui/src/index.js +10 -0
- package/apps/jsmdcui/tests/cdp-security.test.js +32 -0
- package/apps/jsmdcui/tests/cdp-websocket-test.html +46 -0
- package/apps/jsmdcui/tests/demo.test.js +1 -0
- package/bin/init.js +4 -4
- package/bin/multicall.sh +7 -5
- package/package.json +3 -2
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, readlinkSync, rmSync, statSync, symlinkSync, utimesSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
bunShellFallbackArgv,
|
|
7
|
+
builtinNames,
|
|
8
|
+
createState,
|
|
9
|
+
decode,
|
|
10
|
+
execute,
|
|
11
|
+
colorProcessCommand,
|
|
12
|
+
colorProcessTable,
|
|
13
|
+
highlightShellCommand,
|
|
14
|
+
executeArgv,
|
|
15
|
+
formatProcessTable,
|
|
16
|
+
isJavaScriptMode,
|
|
17
|
+
needsMoreInput,
|
|
18
|
+
parse,
|
|
19
|
+
parsePosixProcessList,
|
|
20
|
+
parseWindowsProcessList,
|
|
21
|
+
runUnameFallback,
|
|
22
|
+
SH_COLORS,
|
|
23
|
+
taskkillFailure,
|
|
24
|
+
tokenize,
|
|
25
|
+
windowsKillCommand,
|
|
26
|
+
} from "../src/shell.js";
|
|
27
|
+
import {
|
|
28
|
+
CommandIndex,
|
|
29
|
+
FileIndex,
|
|
30
|
+
VariableIndex,
|
|
31
|
+
completionContext,
|
|
32
|
+
fitGhost,
|
|
33
|
+
firstPrefixMatch,
|
|
34
|
+
historyGhost,
|
|
35
|
+
javascriptContext,
|
|
36
|
+
nextGhostChunk,
|
|
37
|
+
prefixMatches,
|
|
38
|
+
variableCompletion,
|
|
39
|
+
variableContext,
|
|
40
|
+
} from "../src/completion.js";
|
|
41
|
+
import {
|
|
42
|
+
bunmshHistoryPath,
|
|
43
|
+
importedHistory,
|
|
44
|
+
parseBashHistory,
|
|
45
|
+
parseFishHistory,
|
|
46
|
+
readlineHistory,
|
|
47
|
+
saveBunmshHistory,
|
|
48
|
+
safeHistoryEntry,
|
|
49
|
+
} from "../src/history.js";
|
|
50
|
+
import { isLinkerPath } from "../single-exe/compiled.js";
|
|
51
|
+
import { readAssetText } from "../single-exe/assetsHelper.js";
|
|
52
|
+
import { fancyLs } from "../src/fancy-ls.js";
|
|
53
|
+
import { MOUSE_OFF, MOUSE_ON, mouseInput } from "../src/mouse.js";
|
|
54
|
+
import { canonicalEnvironment, environmentValue, homeRelativePath } from "../src/environment.js";
|
|
55
|
+
import { findIsRegularBuiltin } from "../src/find.js";
|
|
56
|
+
|
|
57
|
+
async function run(source, options = {}) {
|
|
58
|
+
const state = createState({
|
|
59
|
+
env: { HOME: "/tmp", ...options.env },
|
|
60
|
+
cwd: options.cwd ?? process.cwd(),
|
|
61
|
+
args: options.args ?? ["bunmsh"],
|
|
62
|
+
history: options.history ?? [],
|
|
63
|
+
mouseTracking: options.mouseTracking,
|
|
64
|
+
pathSearch: options.pathSearch,
|
|
65
|
+
});
|
|
66
|
+
const output = await execute(source, state, { capture: true });
|
|
67
|
+
return {
|
|
68
|
+
...output,
|
|
69
|
+
stdout: decode(output.stdout),
|
|
70
|
+
stderr: decode(output.stderr),
|
|
71
|
+
state,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
describe("CLI", () => {
|
|
75
|
+
test("shows all tab paths and marks the active tab", async () => {
|
|
76
|
+
const cwd = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
|
77
|
+
const home = cwd.slice(0, cwd.lastIndexOf("/"));
|
|
78
|
+
const shown = `~${cwd.slice(home.length)}`;
|
|
79
|
+
const proc = Bun.spawn([process.execPath, "src/main.js", "-i"], {
|
|
80
|
+
cwd,
|
|
81
|
+
env: { ...process.env, HOME: home, PS1: "[\\w] " },
|
|
82
|
+
stdin: "pipe",
|
|
83
|
+
stdout: "pipe",
|
|
84
|
+
stderr: "pipe",
|
|
85
|
+
});
|
|
86
|
+
proc.stdin.write("tab\nexit\n");
|
|
87
|
+
proc.stdin.end();
|
|
88
|
+
const [status, stdout, stderr] = await Promise.all([
|
|
89
|
+
proc.exited,
|
|
90
|
+
new Response(proc.stdout).text(),
|
|
91
|
+
new Response(proc.stderr).text(),
|
|
92
|
+
]);
|
|
93
|
+
expect(status).toBe(0);
|
|
94
|
+
expect(stdout).toBe(
|
|
95
|
+
`[${shown}] [📁 ${shown} \x1b[38;5;81m📂 ${shown}\x1b[0m] `,
|
|
96
|
+
);
|
|
97
|
+
expect(stderr).toBe("");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("adds the active tab number to the default multi-tab prompt", async () => {
|
|
101
|
+
const cwd = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
|
102
|
+
const home = cwd.slice(0, cwd.lastIndexOf("/"));
|
|
103
|
+
const shown = `~${cwd.slice(home.length)}`;
|
|
104
|
+
const env = { ...process.env, HOME: home };
|
|
105
|
+
delete env.PS1;
|
|
106
|
+
const proc = Bun.spawn([process.execPath, "src/main.js", "-i"], {
|
|
107
|
+
cwd,
|
|
108
|
+
env,
|
|
109
|
+
stdin: "pipe",
|
|
110
|
+
stdout: "pipe",
|
|
111
|
+
stderr: "pipe",
|
|
112
|
+
});
|
|
113
|
+
proc.stdin.write("tab\nexit\n");
|
|
114
|
+
proc.stdin.end();
|
|
115
|
+
const [status, stdout, stderr] = await Promise.all([
|
|
116
|
+
proc.exited,
|
|
117
|
+
new Response(proc.stdout).text(),
|
|
118
|
+
new Response(proc.stderr).text(),
|
|
119
|
+
]);
|
|
120
|
+
expect(status).toBe(0);
|
|
121
|
+
expect(stdout).toBe(
|
|
122
|
+
`📁 ${shown}\n$ 📁 ${shown} \x1b[38;5;81m📂 ${shown}\x1b[0m\n[2]$ `,
|
|
123
|
+
);
|
|
124
|
+
expect(stderr).toBe("");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("recalls saved bunmsh history with the Up arrow after startup", async () => {
|
|
128
|
+
const home = mkdtempSync(join(tmpdir(), "bunmsh-readline-history-"));
|
|
129
|
+
let transcript = "";
|
|
130
|
+
const terminal = new Bun.Terminal({
|
|
131
|
+
cols: 100,
|
|
132
|
+
rows: 30,
|
|
133
|
+
data(_terminal, data) { transcript += data.toString(); },
|
|
134
|
+
});
|
|
135
|
+
try {
|
|
136
|
+
const historyPath = join(home, ".local", "share", "bunmsh", "history");
|
|
137
|
+
mkdirSync(join(home, ".local", "share", "bunmsh"), { recursive: true });
|
|
138
|
+
await Bun.write(historyPath, `${JSON.stringify(["echo recalled-marker"])}\n`);
|
|
139
|
+
const proc = Bun.spawn({
|
|
140
|
+
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "-i"],
|
|
141
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
142
|
+
env: {
|
|
143
|
+
...process.env,
|
|
144
|
+
HOME: home,
|
|
145
|
+
PS1: "> ",
|
|
146
|
+
BUNMSH_IMPORT_HISTORY: "off",
|
|
147
|
+
},
|
|
148
|
+
terminal,
|
|
149
|
+
});
|
|
150
|
+
await Bun.sleep(150);
|
|
151
|
+
terminal.write("\x1b[A\r");
|
|
152
|
+
await Bun.sleep(100);
|
|
153
|
+
terminal.write("exit\r");
|
|
154
|
+
expect(await proc.exited).toBe(0);
|
|
155
|
+
expect(transcript).toContain("recalled-marker\r\n");
|
|
156
|
+
} finally {
|
|
157
|
+
terminal.close();
|
|
158
|
+
rmSync(home, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("enables terminal mouse reporting only through BUNMSH_MOUSE", async () => {
|
|
163
|
+
let transcript = "";
|
|
164
|
+
const terminal = new Bun.Terminal({
|
|
165
|
+
cols: 80,
|
|
166
|
+
rows: 24,
|
|
167
|
+
data(_terminal, data) { transcript += data.toString(); },
|
|
168
|
+
});
|
|
169
|
+
const proc = Bun.spawn({
|
|
170
|
+
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "-i"],
|
|
171
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
172
|
+
env: { ...process.env, PS1: "> ", BUNMSH_MOUSE: "1" },
|
|
173
|
+
terminal,
|
|
174
|
+
});
|
|
175
|
+
try {
|
|
176
|
+
await Bun.sleep(150);
|
|
177
|
+
terminal.write("exit\r");
|
|
178
|
+
expect(await proc.exited).toBe(0);
|
|
179
|
+
} finally { terminal.close(); }
|
|
180
|
+
expect(transcript).toContain(MOUSE_ON);
|
|
181
|
+
expect(transcript).toContain(MOUSE_OFF);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("--mouse enables terminal mouse reporting", async () => {
|
|
185
|
+
let transcript = "";
|
|
186
|
+
const terminal = new Bun.Terminal({
|
|
187
|
+
cols: 80,
|
|
188
|
+
rows: 24,
|
|
189
|
+
data(_terminal, data) { transcript += data.toString(); },
|
|
190
|
+
});
|
|
191
|
+
const env = { ...process.env, PS1: "> " };
|
|
192
|
+
delete env.BUNMSH_MOUSE;
|
|
193
|
+
const proc = Bun.spawn({
|
|
194
|
+
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "--mouse", "-i"],
|
|
195
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
196
|
+
env,
|
|
197
|
+
terminal,
|
|
198
|
+
});
|
|
199
|
+
try {
|
|
200
|
+
await Bun.sleep(150);
|
|
201
|
+
terminal.write("exit\r");
|
|
202
|
+
expect(await proc.exited).toBe(0);
|
|
203
|
+
} finally { terminal.close(); }
|
|
204
|
+
expect(transcript).toContain(MOUSE_ON);
|
|
205
|
+
expect(transcript).toContain(MOUSE_OFF);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("--builtin-only skips PATH lookup but keeps builtins available", () => {
|
|
209
|
+
const bun = Bun.which("bun") || process.argv0;
|
|
210
|
+
const cwd = new URL("..", import.meta.url).pathname;
|
|
211
|
+
const builtin = Bun.spawnSync({
|
|
212
|
+
cmd: [bun, "src/main.js", "--builtin-only", "-c", "printf '%s' builtin-ok"],
|
|
213
|
+
cwd,
|
|
214
|
+
});
|
|
215
|
+
expect(builtin.exitCode).toBe(0);
|
|
216
|
+
expect(builtin.stdout.toString()).toBe("builtin-ok");
|
|
217
|
+
|
|
218
|
+
const external = Bun.spawnSync({
|
|
219
|
+
cmd: [bun, "src/main.js", "--builtin-only", "-c", "sh -c 'printf external'"],
|
|
220
|
+
cwd,
|
|
221
|
+
});
|
|
222
|
+
expect(external.exitCode).toBe(127);
|
|
223
|
+
expect(external.stderr.toString()).toContain("bunmsh: sh: not found");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("tab mouse applies tracking changes immediately", async () => {
|
|
227
|
+
let transcript = "";
|
|
228
|
+
const terminal = new Bun.Terminal({
|
|
229
|
+
cols: 80,
|
|
230
|
+
rows: 24,
|
|
231
|
+
data(_terminal, data) { transcript += data.toString(); },
|
|
232
|
+
});
|
|
233
|
+
const env = { ...process.env, PS1: "> " };
|
|
234
|
+
delete env.BUNMSH_MOUSE;
|
|
235
|
+
const proc = Bun.spawn({
|
|
236
|
+
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "-i"],
|
|
237
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
238
|
+
env,
|
|
239
|
+
terminal,
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
await Bun.sleep(150);
|
|
243
|
+
terminal.write("tab mouse on\r");
|
|
244
|
+
await Bun.sleep(100);
|
|
245
|
+
terminal.write("tab mouse off\r");
|
|
246
|
+
await Bun.sleep(100);
|
|
247
|
+
terminal.write("exit\r");
|
|
248
|
+
expect(await proc.exited).toBe(0);
|
|
249
|
+
} finally { terminal.close(); }
|
|
250
|
+
expect(transcript).toContain(MOUSE_ON);
|
|
251
|
+
expect(transcript).toContain(MOUSE_OFF);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("Ctrl-T and Alt-T switch tabs without discarding the edited line", async () => {
|
|
255
|
+
let transcript = "";
|
|
256
|
+
const terminal = new Bun.Terminal({
|
|
257
|
+
cols: 100,
|
|
258
|
+
rows: 30,
|
|
259
|
+
data(_terminal, data) { transcript += data.toString(); },
|
|
260
|
+
});
|
|
261
|
+
const env = { ...process.env };
|
|
262
|
+
delete env.PS1;
|
|
263
|
+
const proc = Bun.spawn({
|
|
264
|
+
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "-i"],
|
|
265
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
266
|
+
env,
|
|
267
|
+
terminal,
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
await Bun.sleep(150);
|
|
271
|
+
terminal.write("echo shortcut-preserved");
|
|
272
|
+
terminal.write("\x14");
|
|
273
|
+
await Bun.sleep(100);
|
|
274
|
+
terminal.write("\r");
|
|
275
|
+
await Bun.sleep(100);
|
|
276
|
+
terminal.write("\x1bt");
|
|
277
|
+
await Bun.sleep(100);
|
|
278
|
+
terminal.write("exit\r");
|
|
279
|
+
expect(await proc.exited).toBe(0);
|
|
280
|
+
} finally { terminal.close(); }
|
|
281
|
+
expect(transcript).toContain("shortcut-preserved\r\n");
|
|
282
|
+
expect(transcript).toContain("[2]$ ");
|
|
283
|
+
expect(transcript).toContain("[1]$ ");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("clicking the prompt tab number creates a tab and Alt-C closes it", async () => {
|
|
287
|
+
let transcript = "";
|
|
288
|
+
let terminal;
|
|
289
|
+
terminal = new Bun.Terminal({
|
|
290
|
+
cols: 100,
|
|
291
|
+
rows: 30,
|
|
292
|
+
data(_terminal, data) {
|
|
293
|
+
const text = data.toString();
|
|
294
|
+
transcript += text;
|
|
295
|
+
if (text.includes("\x1b[6n")) terminal.write("\x1b[10;6R");
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
const env = { ...process.env };
|
|
299
|
+
delete env.PS1;
|
|
300
|
+
const proc = Bun.spawn({
|
|
301
|
+
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "--mouse", "-i"],
|
|
302
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
303
|
+
env,
|
|
304
|
+
terminal,
|
|
305
|
+
});
|
|
306
|
+
try {
|
|
307
|
+
await Bun.sleep(150);
|
|
308
|
+
terminal.write("\x14");
|
|
309
|
+
await Bun.sleep(100);
|
|
310
|
+
terminal.write("\x1b[<0;2;10M");
|
|
311
|
+
await Bun.sleep(120);
|
|
312
|
+
terminal.write("\x1bc");
|
|
313
|
+
await Bun.sleep(100);
|
|
314
|
+
terminal.write("exit\r");
|
|
315
|
+
expect(await proc.exited).toBe(0);
|
|
316
|
+
} finally { terminal.close(); }
|
|
317
|
+
expect(transcript).toContain("[3]$ ");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("clicking inside the typed line moves the cursor there instead of appending", async () => {
|
|
321
|
+
let transcript = "";
|
|
322
|
+
const terminal = new Bun.Terminal({
|
|
323
|
+
cols: 80,
|
|
324
|
+
rows: 24,
|
|
325
|
+
data(_terminal, data) {
|
|
326
|
+
const text = data.toString();
|
|
327
|
+
transcript += text;
|
|
328
|
+
// Prompt "$ " (2 cols) + "echo hello" (10 chars) = col 13, row 1
|
|
329
|
+
// (nothing has scrolled yet in a fresh session).
|
|
330
|
+
if (text.includes("\x1b[6n")) terminal.write("\x1b[1;13R");
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
const proc = Bun.spawn({
|
|
334
|
+
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "--mouse", "-i"],
|
|
335
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
336
|
+
env: { ...process.env, PS1: "$ " },
|
|
337
|
+
terminal,
|
|
338
|
+
});
|
|
339
|
+
try {
|
|
340
|
+
await Bun.sleep(150);
|
|
341
|
+
terminal.write("echo hello");
|
|
342
|
+
await Bun.sleep(100);
|
|
343
|
+
// Column 8 (1-based) lands right after "echo " (index 5 of the line),
|
|
344
|
+
// just before "hello".
|
|
345
|
+
terminal.write("\x1b[<0;8;1M");
|
|
346
|
+
await Bun.sleep(150);
|
|
347
|
+
terminal.write("X\r");
|
|
348
|
+
await Bun.sleep(150);
|
|
349
|
+
terminal.write("exit\r");
|
|
350
|
+
expect(await proc.exited).toBe(0);
|
|
351
|
+
} finally { terminal.close(); }
|
|
352
|
+
expect(transcript).toContain("Xhello\r\n");
|
|
353
|
+
expect(transcript).not.toContain("helloX\r\n");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("Alt-L, Alt-U, and Alt-P list cwd or parent without discarding the edited line", async () => {
|
|
357
|
+
const cwd = mkdtempSync(join(tmpdir(), "bunmsh-alt-l-"));
|
|
358
|
+
const child = join(cwd, "child");
|
|
359
|
+
mkdirSync(child);
|
|
360
|
+
await Bun.write(join(child, "alt-l-marker.txt"), "marker");
|
|
361
|
+
await Bun.write(join(cwd, "alt-u-parent-marker.txt"), "marker");
|
|
362
|
+
let transcript = "";
|
|
363
|
+
const terminal = new Bun.Terminal({
|
|
364
|
+
cols: 100,
|
|
365
|
+
rows: 30,
|
|
366
|
+
data(_terminal, data) { transcript += data.toString(); },
|
|
367
|
+
});
|
|
368
|
+
const proc = Bun.spawn({
|
|
369
|
+
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "-i"],
|
|
370
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
371
|
+
env: { ...process.env, PS1: "> " },
|
|
372
|
+
terminal,
|
|
373
|
+
});
|
|
374
|
+
try {
|
|
375
|
+
await Bun.sleep(150);
|
|
376
|
+
terminal.write(`cd ${child}\r`);
|
|
377
|
+
await Bun.sleep(80);
|
|
378
|
+
terminal.write("echo alt-l-preserved");
|
|
379
|
+
terminal.write("\x1bl");
|
|
380
|
+
await Bun.sleep(100);
|
|
381
|
+
terminal.write("\x1bu");
|
|
382
|
+
await Bun.sleep(100);
|
|
383
|
+
terminal.write("\x1bp");
|
|
384
|
+
await Bun.sleep(100);
|
|
385
|
+
terminal.write("\r");
|
|
386
|
+
await Bun.sleep(80);
|
|
387
|
+
terminal.write("exit\r");
|
|
388
|
+
expect(await proc.exited).toBe(0);
|
|
389
|
+
} finally {
|
|
390
|
+
terminal.close();
|
|
391
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
expect(transcript).toContain("alt-l-marker.txt");
|
|
394
|
+
expect(transcript).toContain("alt-u-parent-marker.txt");
|
|
395
|
+
expect(transcript).toContain("alt-l-preserved\r\n");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("renders ghosts and accepts history words, file paths, and commands", async () => {
|
|
399
|
+
let transcript = "";
|
|
400
|
+
const terminal = new Bun.Terminal({
|
|
401
|
+
cols: 100,
|
|
402
|
+
rows: 30,
|
|
403
|
+
data(_terminal, data) { transcript += data.toString(); },
|
|
404
|
+
});
|
|
405
|
+
const proc = Bun.spawn({
|
|
406
|
+
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "-i"],
|
|
407
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
408
|
+
env: { ...process.env, PS1: "> " },
|
|
409
|
+
terminal,
|
|
410
|
+
});
|
|
411
|
+
const send = async (text) => {
|
|
412
|
+
terminal.write(text);
|
|
413
|
+
await Bun.sleep(80);
|
|
414
|
+
};
|
|
415
|
+
try {
|
|
416
|
+
await Bun.sleep(150);
|
|
417
|
+
await send("echo hello world\r");
|
|
418
|
+
await send("echo h");
|
|
419
|
+
await send("\x1b[C");
|
|
420
|
+
await send("\r");
|
|
421
|
+
await send("echo h");
|
|
422
|
+
await send("\t");
|
|
423
|
+
await send("\r");
|
|
424
|
+
await send("basename pack");
|
|
425
|
+
await send("\x1b[C");
|
|
426
|
+
await send("\r");
|
|
427
|
+
await send("pri");
|
|
428
|
+
await send("\t");
|
|
429
|
+
await send("\r");
|
|
430
|
+
await send("exit\r");
|
|
431
|
+
expect(await proc.exited).toBe(0);
|
|
432
|
+
} finally {
|
|
433
|
+
terminal.close();
|
|
434
|
+
}
|
|
435
|
+
expect(transcript).toContain("\x1b[2mello world\x1b[0m");
|
|
436
|
+
expect(transcript).toContain("\x1b[2mage.json\x1b[0m");
|
|
437
|
+
expect(transcript).toContain("package.json\r\n");
|
|
438
|
+
expect(transcript).toContain("\x1b[2mnt\x1b[0m");
|
|
439
|
+
expect(transcript).toContain("\x1b[0Knt\r");
|
|
440
|
+
expect(transcript).not.toContain(MOUSE_ON);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe("pspa", () => {
|
|
445
|
+
test("lists every PID with its full command line", async () => {
|
|
446
|
+
const execution = await executeArgv(["builtin", "pspa"], createState(), { capture: true });
|
|
447
|
+
expect(execution.status).toBe(0);
|
|
448
|
+
const lines = decode(execution.stdout).split("\n");
|
|
449
|
+
// POSIX passes `ps -eo pid,args` through untouched, so its header is the
|
|
450
|
+
// header, and this test process has to be in the listing under itself.
|
|
451
|
+
expect(lines[0]).toMatch(/^\s*PID COMMAND$/);
|
|
452
|
+
const own = lines.find((line) => new RegExp(`^\\s*${process.pid} `).test(line));
|
|
453
|
+
expect(own).toBeDefined();
|
|
454
|
+
expect(own.length).toBeGreaterThan(String(process.pid).length + 1);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("takes no operands", async () => {
|
|
458
|
+
const execution = await executeArgv(["builtin", "pspa", "extra"], createState(), { capture: true });
|
|
459
|
+
expect(execution.status).toBe(2);
|
|
460
|
+
expect(decode(execution.stderr)).toBe("bunmsh: pspa: unexpected operand: extra\n");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("parses the Windows Win32_Process listing into sorted rows", () => {
|
|
464
|
+
// What the PowerShell one-liner writes: PID, a space, then the command
|
|
465
|
+
// line — or the image name when a system process has none.
|
|
466
|
+
const rows = parseWindowsProcessList(
|
|
467
|
+
"4 System\r\n" +
|
|
468
|
+
"9876 \"C:\\Program Files\\App\\app.exe\" --flag \"a b\"\r\n" +
|
|
469
|
+
"1200 C:\\Windows\\system32\\svchost.exe -k netsvcs\r\n" +
|
|
470
|
+
"\r\n",
|
|
471
|
+
);
|
|
472
|
+
expect(rows).toEqual([
|
|
473
|
+
{ pid: 4, args: "System" },
|
|
474
|
+
{ pid: 1200, args: "C:\\Windows\\system32\\svchost.exe -k netsvcs" },
|
|
475
|
+
{ pid: 9876, args: '"C:\\Program Files\\App\\app.exe" --flag "a b"' },
|
|
476
|
+
]);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("drops the continuation lines a multi-line command line produces", () => {
|
|
480
|
+
expect(parseWindowsProcessList("42 one\nwrapped continuation\n88 two\n")).toEqual([
|
|
481
|
+
{ pid: 42, args: "one" },
|
|
482
|
+
{ pid: 88, args: "two" },
|
|
483
|
+
]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("formats the Windows rows into the same two columns ps prints", () => {
|
|
487
|
+
expect(formatProcessTable([{ pid: 4, args: "System" }, { pid: 1200, args: "svchost.exe" }]))
|
|
488
|
+
.toBe(" PID COMMAND\n 4 System\n 1200 svchost.exe\n");
|
|
489
|
+
// The column widens past procps' five columns for a longer PID.
|
|
490
|
+
expect(formatProcessTable([{ pid: 123456, args: "app.exe" }]))
|
|
491
|
+
.toBe(" PID COMMAND\n123456 app.exe\n");
|
|
492
|
+
expect(formatProcessTable([])).toBe(" PID COMMAND\n");
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe("kill", () => {
|
|
497
|
+
test("probes with signal 0 and reports an unknown pid", async () => {
|
|
498
|
+
expect(await executeArgv(["builtin", "kill", "-0", String(process.pid)], createState(), { capture: true }))
|
|
499
|
+
.toMatchObject({ status: 0 });
|
|
500
|
+
const missing = await executeArgv(["builtin", "kill", "-0", "999999"], createState(), { capture: true });
|
|
501
|
+
expect(missing.status).toBe(1);
|
|
502
|
+
expect(decode(missing.stderr)).toContain("bunmsh: kill: 999999:");
|
|
503
|
+
expect(await executeArgv(["builtin", "kill"], createState(), { capture: true }))
|
|
504
|
+
.toMatchObject({ status: 2 });
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test.skipIf(process.platform === "win32")("signals a real process", async () => {
|
|
508
|
+
const proc = Bun.spawn({ cmd: ["sleep", "30"], stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
509
|
+
try {
|
|
510
|
+
expect(await executeArgv(["builtin", "kill", String(proc.pid)], createState(), { capture: true }))
|
|
511
|
+
.toMatchObject({ status: 0 });
|
|
512
|
+
await proc.exited;
|
|
513
|
+
expect(proc.signalCode).toBe("SIGTERM");
|
|
514
|
+
} finally { proc.kill("SIGKILL"); }
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("builds the taskkill command a Windows kill goes through", () => {
|
|
518
|
+
// /T reaches the children libuv's TerminateProcess leaves running, /F is
|
|
519
|
+
// what makes it work on a console process with no message loop.
|
|
520
|
+
expect(windowsKillCommand(1234)).toEqual(["taskkill", "/PID", "1234", "/T", "/F"]);
|
|
521
|
+
expect(windowsKillCommand("1234")).toEqual(["taskkill", "/PID", "1234", "/T", "/F"]);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("rewrites a taskkill failure as one of our own lines", () => {
|
|
525
|
+
expect(taskkillFailure(0, "SUCCESS: The process with PID 1234 has been terminated.\r\n", ""))
|
|
526
|
+
.toBeNull();
|
|
527
|
+
expect(taskkillFailure(128, "", 'ERROR: The process "1234" not found.\r\n'))
|
|
528
|
+
.toBe('The process "1234" not found.');
|
|
529
|
+
expect(taskkillFailure(1, "ERROR: Access is denied.\r\n", "")).toBe("Access is denied.");
|
|
530
|
+
expect(taskkillFailure(1, "", "")).toBe("taskkill exited with status 1");
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe("pspac", () => {
|
|
535
|
+
const wrap = (name, value) => `${SH_COLORS[name]}${value}${SH_COLORS.reset}`;
|
|
536
|
+
|
|
537
|
+
test("reads the rows back out of the plain ps listing", () => {
|
|
538
|
+
expect(parsePosixProcessList(" PID COMMAND\n 1 /init\n12771 bash -l\n\n")).toEqual([
|
|
539
|
+
{ pid: 1, args: "/init" },
|
|
540
|
+
{ pid: 12771, args: "bash -l" },
|
|
541
|
+
]);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("dims the directory and names the program the first token really is", () => {
|
|
545
|
+
const line = colorProcessCommand("/usr/bin/bash --login -l notes.txt");
|
|
546
|
+
// Colour is the only difference: the text itself is untouched.
|
|
547
|
+
expect(Bun.stripANSI(line)).toBe("/usr/bin/bash --login -l notes.txt");
|
|
548
|
+
expect(line).toContain(wrap("path", "/usr/bin/"));
|
|
549
|
+
expect(line).toContain(wrap("type", "bash"));
|
|
550
|
+
expect(line).toContain(wrap("statement", "--login"));
|
|
551
|
+
expect(line).toContain(wrap("statement", "-l"));
|
|
552
|
+
// An ordinary operand is left alone.
|
|
553
|
+
expect(line).toContain(" notes.txt");
|
|
554
|
+
// Word boundaries are micro's: the `sh` ending `script.sh` is one of
|
|
555
|
+
// sh.yaml's command names, and it is painted like one here too.
|
|
556
|
+
expect(colorProcessCommand("bun run script.sh")).toContain(wrap("type", "sh"));
|
|
557
|
+
|
|
558
|
+
// A program sh.yaml's word lists have never heard of is still the
|
|
559
|
+
// command, because here the first token is known to be one.
|
|
560
|
+
expect(colorProcessCommand("fish")).toBe(wrap("type", "fish"));
|
|
561
|
+
expect(colorProcessCommand("")).toBe("");
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("applies sh.yaml's rules to the rest of the command line", () => {
|
|
565
|
+
const source = 'if test 42 = "$HOME"; then echo ok; fi # note';
|
|
566
|
+
const line = highlightShellCommand(source);
|
|
567
|
+
expect(Bun.stripANSI(line)).toBe(source);
|
|
568
|
+
expect(line).toContain(wrap("statement", "if"));
|
|
569
|
+
expect(line).toContain(wrap("statement", "then"));
|
|
570
|
+
expect(line).toContain(wrap("constant", "42"));
|
|
571
|
+
expect(line).toContain(wrap("type", "test"));
|
|
572
|
+
expect(line).toContain(wrap("special", "="));
|
|
573
|
+
// The string is a region, so it swallows the $HOME inside it.
|
|
574
|
+
expect(line).toContain(wrap("string", '"$HOME"'));
|
|
575
|
+
expect(line).toContain(wrap("comment", "# note"));
|
|
576
|
+
expect(highlightShellCommand("run $HOME/bin ${PATH}"))
|
|
577
|
+
.toContain(wrap("identifier", "$HOME"));
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("resolves rule precedence the way micro does", () => {
|
|
581
|
+
// The flag rule is listed after the command names, so it wins the
|
|
582
|
+
// overlap: --cat is a flag, not the coreutils cat.
|
|
583
|
+
expect(highlightShellCommand("bun --cat run")).toContain(wrap("statement", "--cat"));
|
|
584
|
+
// A # inside a quoted argument does not open a comment.
|
|
585
|
+
const quoted = highlightShellCommand('echo "a # b" done');
|
|
586
|
+
expect(quoted).toContain(wrap("string", '"a # b"'));
|
|
587
|
+
expect(quoted).toContain(wrap("statement", "done"));
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("lets a kernel thread and a quoted program keep their own colour", () => {
|
|
591
|
+
expect(colorProcessCommand("[kworker/0:1]")).toBe(wrap("path", "[kworker/0:1]"));
|
|
592
|
+
const windows = colorProcessCommand('"C:\\Program Files\\App\\app.exe" --flag');
|
|
593
|
+
expect(Bun.stripANSI(windows)).toBe('"C:\\Program Files\\App\\app.exe" --flag');
|
|
594
|
+
expect(windows).toContain(wrap("string", '"C:\\Program Files\\App\\app.exe"'));
|
|
595
|
+
expect(windows).toContain(wrap("statement", "--flag"));
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("strips back to exactly what pspa prints", () => {
|
|
599
|
+
const rows = [
|
|
600
|
+
{ pid: 1, args: "/init" },
|
|
601
|
+
{ pid: 4, args: "" },
|
|
602
|
+
{ pid: 123456, args: "/usr/bin/bun run dev" },
|
|
603
|
+
];
|
|
604
|
+
expect(Bun.stripANSI(colorProcessTable(rows))).toBe(formatProcessTable(rows));
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("lists the same processes pspa does, in colour", async () => {
|
|
608
|
+
const execution = await executeArgv(["builtin", "pspac"], createState(), { capture: true });
|
|
609
|
+
expect(execution.status).toBe(0);
|
|
610
|
+
const text = decode(execution.stdout);
|
|
611
|
+
expect(text).toContain("\x1b[");
|
|
612
|
+
const lines = Bun.stripANSI(text).split("\n");
|
|
613
|
+
expect(lines[0]).toMatch(/^\s*PID COMMAND$/);
|
|
614
|
+
expect(lines.some((line) => new RegExp(`^\\s*${process.pid} `).test(line))).toBe(true);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("takes no operands", async () => {
|
|
618
|
+
const execution = await executeArgv(["builtin", "pspac", "extra"], createState(), { capture: true });
|
|
619
|
+
expect(execution.status).toBe(2);
|
|
620
|
+
expect(decode(execution.stderr)).toBe("bunmsh: pspac: unexpected operand: extra\n");
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
describe("variable completion", () => {
|
|
625
|
+
const state = { env: { HOME: "/home/user", HOSTNAME: "box", PATH: "/bin", LOCAL_ONLY: "x" } };
|
|
626
|
+
|
|
627
|
+
test("recognizes a name being typed after $, ${, and ${#", () => {
|
|
628
|
+
expect(variableContext("echo $HO")).toMatchObject({ prefix: "HO", lead: "$", brace: false });
|
|
629
|
+
expect(variableContext("echo ${HO")).toMatchObject({ prefix: "HO", lead: "${", brace: true });
|
|
630
|
+
expect(variableContext("echo ${#HO")).toMatchObject({ prefix: "HO", lead: "${#", brace: true });
|
|
631
|
+
// A bare $ is a name with nothing typed yet, not a non-match.
|
|
632
|
+
expect(variableContext("echo $")).toMatchObject({ prefix: "", lead: "$" });
|
|
633
|
+
// Position on the line does not matter: inside double quotes, on the
|
|
634
|
+
// right of an assignment, or where a command name would go.
|
|
635
|
+
expect(variableContext('echo "$HO')).toMatchObject({ prefix: "HO" });
|
|
636
|
+
expect(variableContext("X=$HO")).toMatchObject({ prefix: "HO" });
|
|
637
|
+
expect(variableContext("$ED")).toMatchObject({ prefix: "ED" });
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("leaves alone every other thing a $ can start", () => {
|
|
641
|
+
expect(variableContext("echo $(")).toBeNull();
|
|
642
|
+
expect(variableContext("echo $(l")).toBeNull();
|
|
643
|
+
expect(variableContext("echo $?")).toBeNull();
|
|
644
|
+
expect(variableContext("echo $1")).toBeNull();
|
|
645
|
+
// $$ is the pid, already complete; the second $ is not a name opening.
|
|
646
|
+
expect(variableContext("echo $$")).toBeNull();
|
|
647
|
+
expect(variableContext("echo hi")).toBeNull();
|
|
648
|
+
expect(variableContext("echo $HOME ")).toBeNull();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test("counts backslashes so an escaped dollar stays literal", () => {
|
|
652
|
+
expect(variableContext(String.raw`echo \$HO`)).toBeNull();
|
|
653
|
+
// An escaped backslash is not escaping the dollar.
|
|
654
|
+
expect(variableContext(String.raw`echo \\$HO`)).toMatchObject({ prefix: "HO" });
|
|
655
|
+
expect(variableContext(String.raw`echo \\\$HO`)).toBeNull();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("completes from the shell's own table, exported or not", () => {
|
|
659
|
+
const index = new VariableIndex();
|
|
660
|
+
expect(index.matches(state, "HO")).toEqual(["HOME", "HOSTNAME"]);
|
|
661
|
+
expect(index.first(state, "LOCAL")).toBe("LOCAL_ONLY");
|
|
662
|
+
expect(index.first(state, "ZZ")).toBeNull();
|
|
663
|
+
// An empty prefix lists everything, the way Tab on an empty word does.
|
|
664
|
+
expect(index.matches(state, "")).toEqual(["HOME", "HOSTNAME", "LOCAL_ONLY", "PATH"]);
|
|
665
|
+
expect(index.first(state, "")).toBeNull();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("picks up a name added after the index was first built", () => {
|
|
669
|
+
const index = new VariableIndex();
|
|
670
|
+
const live = { env: { ...state.env } };
|
|
671
|
+
expect(index.first(live, "TA")).toBeNull();
|
|
672
|
+
live.env.TAG = "v1";
|
|
673
|
+
expect(index.first(live, "TA")).toBe("TAG");
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test("closes the brace it was given", () => {
|
|
677
|
+
expect(variableCompletion(variableContext("echo $HO"), "HOME")).toBe("$HOME");
|
|
678
|
+
expect(variableCompletion(variableContext("echo ${HO"), "HOME")).toBe("${HOME}");
|
|
679
|
+
expect(variableCompletion(variableContext("echo ${#HO"), "HOME")).toBe("${#HOME}");
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test("reports the quote a word is inside, so nothing expands in single quotes", () => {
|
|
683
|
+
expect(completionContext("echo 'foo")).toMatchObject({ quote: "'" });
|
|
684
|
+
expect(completionContext('echo "foo')).toMatchObject({ quote: '"' });
|
|
685
|
+
expect(completionContext("echo foo")).toMatchObject({ quote: null });
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("treats $( as a command position and ) as the end of one", () => {
|
|
689
|
+
expect(completionContext("echo $(l")).toMatchObject({ command: true, prefix: "l" });
|
|
690
|
+
expect(completionContext("echo $(ls /tm")).toMatchObject({ command: false, prefix: "/tm" });
|
|
691
|
+
expect(completionContext("echo $(date) fi")).toMatchObject({ command: false, prefix: "fi" });
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
describe("javascript mode completion", () => {
|
|
696
|
+
test("decides mode by the rule the evaluator dispatches on", () => {
|
|
697
|
+
expect(isJavaScriptMode("Bun.e, 1")).toBe(true);
|
|
698
|
+
expect(isJavaScriptMode(" Bun.file('x')")).toBe(true);
|
|
699
|
+
expect(isJavaScriptMode("echo Bun.e")).toBe(false);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test("stops offering a continuation a JavaScript line could never accept", () => {
|
|
703
|
+
// A bare trailing backslash is a syntax error in JavaScript, not a
|
|
704
|
+
// request for another line, so the prompt no longer asks for one.
|
|
705
|
+
expect(needsMoreInput("Bun.e; const p = \\")).toBe(false);
|
|
706
|
+
// An unterminated string still continues: that is JavaScript's own line
|
|
707
|
+
// continuation, and the evaluator accepts it.
|
|
708
|
+
expect(needsMoreInput('Bun.e, "abc\\')).toBe(true);
|
|
709
|
+
expect(needsMoreInput("Bun.e, `abc")).toBe(true);
|
|
710
|
+
// Shell text is untouched.
|
|
711
|
+
expect(needsMoreInput("echo hi \\")).toBe(true);
|
|
712
|
+
expect(needsMoreInput('echo "abc')).toBe(true);
|
|
713
|
+
expect(needsMoreInput("echo hi")).toBe(false);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test("completes a shell variable after $. and nothing after a bare $", () => {
|
|
717
|
+
expect(javascriptContext("Bun.e, $.HO")).toMatchObject({ kind: "variable", prefix: "HO", lead: "$." });
|
|
718
|
+
expect(javascriptContext("Bun.e, $.")).toMatchObject({ kind: "variable", prefix: "" });
|
|
719
|
+
// In JavaScript a bare `$` is the table object itself; a name only
|
|
720
|
+
// starts after the dot.
|
|
721
|
+
expect(javascriptContext("Bun.e, $")).toBeNull();
|
|
722
|
+
expect(javascriptContext("Bun.e, 1 + 2")).toBeNull();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("completes a path inside an unclosed string literal", () => {
|
|
726
|
+
expect(javascriptContext('Bun.e, Bun.file("/tm')).toMatchObject({ kind: "path", prefix: "/tm", quote: '"' });
|
|
727
|
+
expect(javascriptContext("Bun.e, Bun.file('/tm")).toMatchObject({ kind: "path", prefix: "/tm", quote: "'" });
|
|
728
|
+
expect(javascriptContext("Bun.e, Bun.file(`/tm")).toMatchObject({ kind: "path", prefix: "/tm", quote: "`" });
|
|
729
|
+
// A closed string is not a path being typed.
|
|
730
|
+
expect(javascriptContext('Bun.e, Bun.file("/tmp/x")')).toBeNull();
|
|
731
|
+
// An escaped quote does not close it.
|
|
732
|
+
expect(javascriptContext('Bun.e, "it\\"s /tm')).toMatchObject({ kind: "path", prefix: 'it\\"s /tm' });
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("treats a template interpolation as code again", () => {
|
|
736
|
+
// Inside ${ } the cursor is back in JavaScript, so $. completes there.
|
|
737
|
+
expect(javascriptContext("Bun.e, `a${$.HO")).toMatchObject({ kind: "variable", prefix: "HO" });
|
|
738
|
+
// And the template resumes after the closing brace.
|
|
739
|
+
expect(javascriptContext("Bun.e, `a${b}/tm")).toMatchObject({ kind: "path", quote: "`" });
|
|
740
|
+
});
|
|
741
|
+
});
|