buninu 0.4.0 → 0.4.2

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.
@@ -7,16 +7,21 @@ import {
7
7
  execute,
8
8
  executeArgv,
9
9
  executeBunShellFallback,
10
+ isJavaScriptMode,
10
11
  needsMoreInput,
11
12
  pipelineChildState,
12
13
  } from "./shell.js";
13
14
  import {
14
15
  CommandIndex,
15
16
  FileIndex,
17
+ VariableIndex,
16
18
  completionContext,
17
19
  fitGhost,
18
20
  historyGhost,
21
+ javascriptContext,
19
22
  nextGhostChunk,
23
+ variableCompletion,
24
+ variableContext,
20
25
  } from "./completion.js";
21
26
  import { readAssetText } from "../single-exe/assetsHelper.js";
22
27
  import { buildEarlyExit, stringifyNonPrimitiveDefineValues } from "../single-exe/compiled.js";
@@ -166,6 +171,41 @@ async function interactive(state) {
166
171
  let mouseTracking = terminal && state.mouseTracking;
167
172
  const commandIndex = new CommandIndex(builtinNames());
168
173
  const fileIndex = new FileIndex();
174
+ const variableIndex = new VariableIndex();
175
+ // A `$` being typed outranks the word it sits in: `$ED` is a variable
176
+ // name, not a command or a file, wherever on the line it appears. Single
177
+ // quotes are the exception, since nothing expands inside them.
178
+ const variableMatch = (line, cursor, context) => {
179
+ if (context.quote === "'") return null;
180
+ const variable = variableContext(line, cursor);
181
+ return variable ? { variable } : null;
182
+ };
183
+ // JavaScript mode is decided on the whole source the evaluator will see,
184
+ // which on a continued line is what is pending plus what is being typed.
185
+ // `pending` is declared further down, next to the loop that fills it; it
186
+ // is always initialised by the time a keystroke can reach this.
187
+ const javascriptLine = (line) => isJavaScriptMode(pending ? `${pending}\n${line}` : line);
188
+ // What a JavaScript line offers: a shell variable name after `$.`, a path
189
+ // inside a string literal, and nothing where neither applies — shell
190
+ // word-splitting would only produce noise there.
191
+ const javascriptMatches = (line, cursor) => {
192
+ const js = javascriptContext(line, cursor);
193
+ if (js?.kind === "variable")
194
+ return {
195
+ js,
196
+ names: variableIndex.matches(state, js.prefix).map((name) => variableCompletion(js, name)),
197
+ first: variableIndex.first(state, js.prefix),
198
+ complete: (name) => variableCompletion(js, name),
199
+ };
200
+ if (js?.kind === "path")
201
+ return {
202
+ js,
203
+ names: fileIndex.matches(js.prefix, state),
204
+ first: fileIndex.first(js.prefix, state),
205
+ complete: (name) => name,
206
+ };
207
+ return null;
208
+ };
169
209
  state.history = await importedHistory(state.env);
170
210
  const history = state.history;
171
211
  // Marks everything just loaded (including whatever another session has
@@ -347,7 +387,17 @@ async function interactive(state) {
347
387
  }, onPaste) : process.stdin;
348
388
  const completer = (line) => {
349
389
  commandIndex.refreshIfChanged(state);
390
+ if (javascriptLine(line)) {
391
+ const found = javascriptMatches(line, line.length);
392
+ return found?.names.length ? [found.names, found.js.text] : [[], line];
393
+ }
350
394
  const context = completionContext(line);
395
+ const found = variableMatch(line, line.length, context);
396
+ if (found) {
397
+ const names = variableIndex.matches(state, found.variable.prefix);
398
+ if (names.length)
399
+ return [names.map((name) => variableCompletion(found.variable, name)), found.variable.text];
400
+ }
351
401
  if (context.command) {
352
402
  return [
353
403
  context.prefix.includes("/")
@@ -460,15 +510,39 @@ async function interactive(state) {
460
510
  let renderTask = null;
461
511
  const currentGhost = () => {
462
512
  if (readline.cursor !== readline.line.length) return null;
513
+ if (javascriptLine(readline.line)) {
514
+ const found = javascriptMatches(readline.line, readline.cursor);
515
+ // An empty name matches everything, so it ghosts nothing; Tab still
516
+ // lists it all, the way it does for an empty word in shell text.
517
+ if (found?.js.prefix && found.first)
518
+ return found.complete(found.first).slice(found.js.text.length);
519
+ return historyGhost(history, readline.line);
520
+ }
463
521
  const context = completionContext(readline.line, readline.cursor);
522
+ const found = variableMatch(readline.line, readline.cursor, context);
523
+ // An empty name would match every variable there is, so a bare `$`
524
+ // ghosts nothing; Tab still lists them all, the way it does for files.
525
+ const variableGhost = () => {
526
+ if (!found?.variable.prefix) return null;
527
+ const name = variableIndex.first(state, found.variable.prefix);
528
+ return name
529
+ ? variableCompletion(found.variable, name).slice(found.variable.text.length)
530
+ : null;
531
+ };
464
532
  if (context.command) {
533
+ const ghost = variableGhost();
534
+ if (ghost) return ghost;
465
535
  const match = context.prefix.includes("/")
466
536
  ? fileIndex.first(context.prefix, state)
467
537
  : commandIndex.first(context.prefix);
468
538
  return match ? match.slice(context.prefix.length) : null;
469
539
  }
540
+ // History still wins where it applies: it completes the whole line,
541
+ // which reaches further than one variable name.
470
542
  const fromHistory = historyGhost(history, readline.line);
471
543
  if (fromHistory) return fromHistory;
544
+ const ghost = variableGhost();
545
+ if (ghost) return ghost;
472
546
  const match = fileIndex.first(context.prefix, state);
473
547
  return match ? match.slice(context.prefix.length) : null;
474
548
  };
@@ -44,13 +44,14 @@ import { findIsRegularBuiltin, runFind } from "./find.js";
44
44
  import { main as startFileServer, parseServeArguments, waitForInterrupt } from "../serve.js";
45
45
  import { main as catFancy } from "./cat-fancy.js";
46
46
  import { parseCatOperands } from "./cat-operands.js";
47
+ import { runCurl } from "./curl.js";
47
48
 
48
49
  const encoder = new TextEncoder();
49
50
  const decoder = new TextDecoder();
50
51
  const DEFAULT_ALIASES = {
51
52
  ls: ["ls", "--color=auto"],
52
- diff: ["diff", "--color=auto"],
53
53
  grep: ["grep", "--color=auto"],
54
+ diff: ["diff", "--color"],
54
55
  };
55
56
  const DEFAULT_COMMAND_PATH = process.platform === "win32"
56
57
  ? (environmentValue(process.env, "PATH") ?? "")
@@ -1574,6 +1575,19 @@ const builtins = {
1574
1575
  let status = 0, stderr = "";
1575
1576
  for (; i < argv.length; i++) {
1576
1577
  const pid = Number(argv[i]);
1578
+ // Signal 0 asks whether a PID can be signalled without touching it,
1579
+ // which taskkill has no way to express — and which process.kill does
1580
+ // answer on Windows. Everything else goes through taskkill there.
1581
+ if (process.platform === "win32" && signal !== 0) {
1582
+ if (!Number.isInteger(pid) || pid <= 0) {
1583
+ status = 1;
1584
+ stderr += `bunmsh: kill: ${argv[i]}: arguments must be process ids\n`;
1585
+ continue;
1586
+ }
1587
+ const failure = await runWindowsKill(pid);
1588
+ if (failure) { status = 1; stderr += `bunmsh: kill: ${argv[i]}: ${failure}\n`; }
1589
+ continue;
1590
+ }
1577
1591
  try { process.kill(pid, signal); }
1578
1592
  catch (error) { status = 1; stderr += `bunmsh: kill: ${argv[i]}: ${error.message}\n`; }
1579
1593
  }
@@ -1907,6 +1921,38 @@ async function runTrFallback(argv, _state, input) {
1907
1921
  return result(0, output);
1908
1922
  }
1909
1923
 
1924
+ // `curl` only ever runs when the system has none, so it inherits the
1925
+ // streaming rules the other fallbacks use: write straight through when the
1926
+ // bytes are headed for the terminal or a pipeline stage, buffer when the
1927
+ // caller is capturing them.
1928
+ async function runCurlFallback(argv, state, input, context = {}) {
1929
+ const streamStdout = state.pipelineChild ||
1930
+ (!context.captureStdout && !context.options?.stdoutSink);
1931
+ const streamStderr = state.pipelineChild || !context.captureStderr;
1932
+ const stdout = [];
1933
+ const stderr = [];
1934
+ const io = {
1935
+ cwd: nativePath(state.cwd),
1936
+ env: state.env,
1937
+ stdoutIsTTY: streamStdout && Boolean(process.stdout.isTTY),
1938
+ readStdin: () => readFallbackInput(input),
1939
+ async writeStdout(chunk) {
1940
+ const data = bytes(chunk);
1941
+ if (streamStdout) await writeStream(process.stdout, data); else stdout.push(data);
1942
+ },
1943
+ async writeStderr(text) {
1944
+ const data = bytes(text);
1945
+ if (streamStderr) await writeStream(process.stderr, data); else stderr.push(data);
1946
+ },
1947
+ };
1948
+ try {
1949
+ const status = await runCurl(argv, io);
1950
+ return result(status, concatBytes(stdout), concatBytes(stderr));
1951
+ } catch (error) {
1952
+ return result(2, concatBytes(stdout), concatBytes([...stderr, bytes(`curl: ${error.message}\n`)]));
1953
+ }
1954
+ }
1955
+
1910
1956
  async function runTeeFallback(argv, state, input) {
1911
1957
  let append = false, i = 1;
1912
1958
  if (argv[i] === "-a") append = true, i++;
@@ -2342,6 +2388,282 @@ export function runUnameFallback(argv, system = {}) {
2342
2388
  return result(0, `${[..."snrvmp"].filter((flag) => flags.has(flag)).map((flag) => values[flag]).join(" ")}\n`);
2343
2389
  }
2344
2390
 
2391
+ // Windows has no `ps`, so the same two columns come out of Win32_Process.
2392
+ // bun-taskmgr reads `Select-Object ProcessId, CommandLine`, but that is
2393
+ // formatted as a console table, which truncates a long command line with an
2394
+ // ellipsis — exactly the part pspa exists to show. Building the line inside
2395
+ // PowerShell instead keeps it whole, and falls back to the image name for a
2396
+ // system process whose CommandLine is null.
2397
+ const PSPA_WINDOWS_QUERY = "Get-CimInstance Win32_Process | ForEach-Object { " +
2398
+ "$_.ProcessId.ToString() + ' ' + $(if ($_.CommandLine) { $_.CommandLine } else { $_.Name }) }";
2399
+
2400
+ export function parseWindowsProcessList(text) {
2401
+ const rows = [];
2402
+ for (const line of text.split(/\r?\n/)) {
2403
+ const match = /^\s*(\d+) ?(.*)$/.exec(line);
2404
+ // A command line containing a newline arrives as continuation lines with
2405
+ // no PID of their own. There is nothing to attribute them to, so they are
2406
+ // dropped rather than guessed at.
2407
+ if (match) rows.push({ pid: Number(match[1]), args: match[2].trim() });
2408
+ }
2409
+ return rows.sort((first, second) => first.pid - second.pid);
2410
+ }
2411
+
2412
+ // Laid out like `ps -eo pid,args`, so both platforms print the same shape:
2413
+ // a right-aligned PID column at least as wide as procps' own, then the
2414
+ // command line.
2415
+ export function formatProcessTable(rows) {
2416
+ const width = rows.reduce((widest, row) => Math.max(widest, String(row.pid).length), 5);
2417
+ const lines = [`${"PID".padStart(width)} COMMAND`];
2418
+ for (const row of rows)
2419
+ lines.push(`${String(row.pid).padStart(width)} ${row.args}`.trimEnd());
2420
+ return `${lines.join("\n")}\n`;
2421
+ }
2422
+
2423
+ // Windows has no signals to deliver. libuv turns SIGTERM, SIGINT, and
2424
+ // SIGKILL into an unconditional TerminateProcess, refuses every other name
2425
+ // with ENOSYS, and in no case touches the target's children. `taskkill`
2426
+ // reaches all of it, and `/T /F` is the form bun-taskmgr verified on
2427
+ // Windows 11, so that is what a terminating signal becomes here.
2428
+ export function windowsKillCommand(pid) {
2429
+ return ["taskkill", "/PID", String(pid), "/T", "/F"];
2430
+ }
2431
+
2432
+ // taskkill reports its own failures in prose ("ERROR: The process "123" not
2433
+ // found."). Strip its prefix so the line reads as one of ours.
2434
+ export function taskkillFailure(status, stdout, stderr) {
2435
+ if (status === 0) return null;
2436
+ const message = (stderr.trim() || stdout.trim()).replace(/^ERROR:\s*/i, "");
2437
+ return message || `taskkill exited with status ${status}`;
2438
+ }
2439
+
2440
+ async function runWindowsKill(pid) {
2441
+ let proc;
2442
+ try {
2443
+ proc = Bun.spawn({
2444
+ cmd: windowsKillCommand(pid),
2445
+ stdin: "ignore",
2446
+ stdout: "pipe",
2447
+ stderr: "pipe",
2448
+ });
2449
+ } catch (error) { return `taskkill: ${error.message}`; }
2450
+ const [status, stdout, stderr] = await Promise.all([
2451
+ proc.exited,
2452
+ new Response(proc.stdout).text(),
2453
+ new Response(proc.stderr).text(),
2454
+ ]);
2455
+ return taskkillFailure(status, stdout, stderr);
2456
+ }
2457
+
2458
+ // `pspa` answers the question a shell with no process viewer keeps running
2459
+ // into: which PID belongs to which command line. POSIX hands that to
2460
+ // `ps -eo pid,args` and passes the output straight through — reading it over
2461
+ // a pipe is also what stops procps from cutting long command lines at the
2462
+ // terminal width, which it does when its stdout is a terminal.
2463
+ // Rows out of `ps`, for the callers that render the listing themselves
2464
+ // rather than passing it through.
2465
+ export function parsePosixProcessList(text) {
2466
+ const rows = [];
2467
+ for (const line of text.split(/\r?\n/)) {
2468
+ if (/^\s*PID\s+COMMAND\s*$/.test(line)) continue;
2469
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
2470
+ if (match) rows.push({ pid: Number(match[1]), args: match[2].trimEnd() });
2471
+ }
2472
+ return rows;
2473
+ }
2474
+
2475
+ async function queryProcessList(name, state) {
2476
+ const windows = process.platform === "win32";
2477
+ const command = windows
2478
+ ? ["powershell", "-NoProfile", "-Command", PSPA_WINDOWS_QUERY]
2479
+ : ["ps", "-eo", "pid,args"];
2480
+ let proc;
2481
+ try {
2482
+ proc = Bun.spawn({
2483
+ cmd: command,
2484
+ cwd: nativePath(state.cwd),
2485
+ env: state.env,
2486
+ stdin: "ignore",
2487
+ stdout: "pipe",
2488
+ stderr: "pipe",
2489
+ });
2490
+ } catch (error) {
2491
+ return { failure: result(127, "", `bunmsh: ${name}: ${command[0]}: ${error.message}\n`) };
2492
+ }
2493
+ const [status, stdout, stderr] = await Promise.all([
2494
+ proc.exited,
2495
+ new Response(proc.stdout).arrayBuffer(),
2496
+ new Response(proc.stderr).text(),
2497
+ ]);
2498
+ if (status !== 0) {
2499
+ return {
2500
+ failure: result(status, "",
2501
+ stderr || `bunmsh: ${name}: ${command[0]}: exited with status ${status}\n`),
2502
+ };
2503
+ }
2504
+ return { windows, output: new Uint8Array(stdout), stderr };
2505
+ }
2506
+
2507
+ async function runPspaFallback(argv, state) {
2508
+ if (argv.length > 1)
2509
+ return result(2, "", `bunmsh: pspa: unexpected operand: ${argv[1]}\n`);
2510
+ const query = await queryProcessList("pspa", state);
2511
+ if (query.failure) return query.failure;
2512
+ return query.windows
2513
+ ? result(0, formatProcessTable(parseWindowsProcessList(decoder.decode(query.output))), query.stderr)
2514
+ : result(0, query.output, query.stderr);
2515
+ }
2516
+
2517
+ // The COMMAND column of a process listing is a shell command line, so it is
2518
+ // highlighted as one. The rules are micro's `syntax/sh.yaml` and the colours
2519
+ // are its `colorschemes/monokai.micro` colour-links, which is also the
2520
+ // palette catfancy already uses — so a command line reads the same here, in
2521
+ // a previewed script, and in the editor.
2522
+ export const SH_COLORS = {
2523
+ comment: Bun.color("#75715E", "ansi-16m"),
2524
+ identifier: Bun.color("#66D9EF", "ansi-16m"),
2525
+ constant: Bun.color("#AE81FF", "ansi-16m"),
2526
+ string: Bun.color("#E6DB74", "ansi-16m"),
2527
+ statement: Bun.color("#F92672", "ansi-16m"),
2528
+ type: Bun.color("#66D9EF", "ansi-16m"),
2529
+ special: Bun.color("#A6E22E", "ansi-16m"),
2530
+ path: "\x1b[2m",
2531
+ header: "\x1b[1m",
2532
+ reset: "\x1b[0m",
2533
+ };
2534
+
2535
+ // sh.yaml's rules, in its order. A later rule paints over an earlier one,
2536
+ // which is how micro resolves them too: that is why the flag rule beats the
2537
+ // command names it overlaps, and why `$var` ends up an identifier rather
2538
+ // than the `special` its `$` matched first.
2539
+ const SH_RULES = [
2540
+ { name: "constant", pattern: /\b[0-9]+\b/g },
2541
+ { name: "statement", pattern: /\b(break|case|continue|do|done|elif|else|esac|exec|exit|fi|for|function|if|in|return|select|then|trap|until|wait|while)\b/g },
2542
+ { name: "special", pattern: /[`$<>!=&~^{}();\][]+/g },
2543
+ { name: "type", pattern: /\b(cd|command|echo|eval|export|getopts|let|local|read|set|shift|time|umask|unset)\b/g },
2544
+ { name: "type", pattern: /\b((g|ig)?awk|bash|dash|find|getopt|\w{0,4}grep|kill|killall|\w{0,4}less|make|pkill|sed|sh|tar)\b/g },
2545
+ { name: "type", pattern: /\b(base64|basename|cat|chcon|chgrp|chmod|chown|chroot|cksum|comm|cp|csplit|cut|date|dd|df|dir|dircolors|dirname|du|env|expand|expr|factor|false|fmt|fold|head|hostid|id|install|join|link|ln|logname|ls|md5sum|mkdir|mkfifo|mknod|mktemp|mv|nice|nl|nohup|nproc|numfmt|od|paste|pathchk|pinky|pr|printenv|printf|ptx|pwd|readlink|realpath|rm|rmdir|runcon|seq|(sha1|sha224|sha256|sha384|sha512)sum|shred|shuf|sleep|sort|split|stat|stdbuf|stty|sum|sync|tac|tail|tee|test|time|timeout|touch|tr|true|truncate|tsort|tty|uname|unexpand|uniq|unlink|users|vdir|wc|who|whoami|yes)\b/g },
2546
+ { name: "statement", pattern: /(\s|^)(--?[A-Za-z0-9][\w-]*)/g, group: 2 },
2547
+ { name: "identifier", pattern: /\$\{[\w:!%&=+#~@*^$?, .\-/[\]]+\}/g },
2548
+ { name: "identifier", pattern: /\$([0-9!#@*$?-]|[A-Za-z_]\w*)/g },
2549
+ ];
2550
+
2551
+ // Quoted strings and comments are regions in sh.yaml, not patterns: they
2552
+ // swallow whatever is inside them. Finding their spans first is what keeps
2553
+ // a `#` inside a quoted argument from turning the rest of the line into a
2554
+ // comment, and a quote inside a comment from opening a string.
2555
+ function shellRegions(text) {
2556
+ const regions = [];
2557
+ for (let i = 0; i < text.length; i++) {
2558
+ const ch = text[i];
2559
+ if (ch === '"' || ch === "'") {
2560
+ let end = i + 1;
2561
+ while (end < text.length) {
2562
+ if (text[end] === "\\") { end += 2; continue; }
2563
+ if (text[end] === ch) { end++; break; }
2564
+ end++;
2565
+ }
2566
+ end = Math.min(end, text.length);
2567
+ regions.push({ start: i, end, name: "string" });
2568
+ i = end - 1;
2569
+ continue;
2570
+ }
2571
+ if (ch === "#" && (i === 0 || /\s/.test(text[i - 1]))) {
2572
+ regions.push({ start: i, end: text.length, name: "comment" });
2573
+ break;
2574
+ }
2575
+ }
2576
+ return regions;
2577
+ }
2578
+
2579
+ // `base` paints runs before any rule runs, so a rule matching inside one
2580
+ // still wins — the same layering micro applies between a scheme's default
2581
+ // and its rules.
2582
+ export function highlightShellCommand(text, base = []) {
2583
+ const painted = new Array(text.length).fill(null);
2584
+ for (const run of base)
2585
+ for (let i = run.start; i < Math.min(run.end, text.length); i++) painted[i] = run.name;
2586
+ const regions = shellRegions(text);
2587
+ const inRegion = new Array(text.length).fill(false);
2588
+ for (const region of regions)
2589
+ for (let i = region.start; i < region.end; i++) inRegion[i] = true;
2590
+ for (const rule of SH_RULES) {
2591
+ for (const match of text.matchAll(rule.pattern)) {
2592
+ const value = rule.group ? match[rule.group] : match[0];
2593
+ if (!value) continue;
2594
+ const start = match.index + (rule.group ? match[0].indexOf(value) : 0);
2595
+ for (let i = start; i < start + value.length; i++)
2596
+ if (!inRegion[i]) painted[i] = rule.name;
2597
+ }
2598
+ }
2599
+ for (const region of regions)
2600
+ for (let i = region.start; i < region.end; i++) painted[i] = region.name;
2601
+
2602
+ let output = "";
2603
+ for (let i = 0; i < text.length;) {
2604
+ const name = painted[i];
2605
+ let end = i;
2606
+ while (end < text.length && painted[end] === name) end++;
2607
+ const chunk = text.slice(i, end);
2608
+ output += name ? `${SH_COLORS[name]}${chunk}${SH_COLORS.reset}` : chunk;
2609
+ i = end;
2610
+ }
2611
+ return output;
2612
+ }
2613
+
2614
+ // Two additions to what sh.yaml describes, because a process listing is not
2615
+ // a script. The directory in front of the program is dimmed: that is the
2616
+ // part an absolute path buries the answer in, and `ps` prints a lot of it.
2617
+ // And the program's own name is painted `type` whether or not it is one of
2618
+ // the names sh.yaml lists — the file has to guess a command from a word
2619
+ // list, while here the first token is known to be one.
2620
+ // Both are base layers, so a rule still paints over them, and a quoted
2621
+ // program keeps the string colour a quoted string gets everywhere else.
2622
+ // A kernel thread — `[kworker/0:1]`, one bracketed token with no program,
2623
+ // path, or arguments in it — recedes whole instead of being taken apart.
2624
+ export function colorProcessCommand(args) {
2625
+ if (!args) return "";
2626
+ if (/^\[.+\]$/.test(args)) return `${SH_COLORS.path}${args}${SH_COLORS.reset}`;
2627
+ const end = args.indexOf(" ");
2628
+ const program = end < 0 ? args.length : end;
2629
+ const separator = Math.max(
2630
+ args.lastIndexOf("/", program - 1),
2631
+ args.lastIndexOf("\\", program - 1),
2632
+ );
2633
+ const base = separator < 0
2634
+ ? [{ start: 0, end: program, name: "type" }]
2635
+ : [
2636
+ { start: 0, end: separator + 1, name: "path" },
2637
+ { start: separator + 1, end: program, name: "type" },
2638
+ ];
2639
+ return highlightShellCommand(args, base);
2640
+ }
2641
+
2642
+ export function colorProcessTable(rows) {
2643
+ // A PID is a number, and sh.yaml paints a number `constant`.
2644
+ const { constant: PID, header: HEADER, reset: RESET } = SH_COLORS;
2645
+ const width = rows.reduce((widest, row) => Math.max(widest, String(row.pid).length), 5);
2646
+ const lines = [`${HEADER}${"PID".padStart(width)} COMMAND${RESET}`];
2647
+ // Nothing but colour separates this from formatProcessTable's output, so
2648
+ // a row with no command line keeps the same trimmed tail that one has.
2649
+ for (const row of rows)
2650
+ lines.push(`${PID}${String(row.pid).padStart(width)}${RESET}` +
2651
+ (row.args ? ` ${colorProcessCommand(row.args)}` : ""));
2652
+ return `${lines.join("\n")}\n`;
2653
+ }
2654
+
2655
+ // `pspac` is `pspa` coloured. Like catfancy it always colours rather than
2656
+ // sniffing for a terminal, so the plain listing stays one command away.
2657
+ async function runPspacFallback(argv, state) {
2658
+ if (argv.length > 1)
2659
+ return result(2, "", `bunmsh: pspac: unexpected operand: ${argv[1]}\n`);
2660
+ const query = await queryProcessList("pspac", state);
2661
+ if (query.failure) return query.failure;
2662
+ const text = decoder.decode(query.output);
2663
+ const rows = query.windows ? parseWindowsProcessList(text) : parsePosixProcessList(text);
2664
+ return result(0, colorProcessTable(rows), query.stderr);
2665
+ }
2666
+
2345
2667
  const fallbackBuiltins = {
2346
2668
  basename: async (argv) => {
2347
2669
  const i = argv[1] === "--" ? 2 : 1;
@@ -2377,6 +2699,7 @@ const fallbackBuiltins = {
2377
2699
  return result(output.status, output.stdout, output.stderr);
2378
2700
  },
2379
2701
  cp: runBunShellCpFallback,
2702
+ curl: runCurlFallback,
2380
2703
  head: runHeadFallback,
2381
2704
  tail: (argv, state, input) => runTextFilter(argv, state, input, "tail"),
2382
2705
  wc: runWcFallback,
@@ -2396,6 +2719,8 @@ const fallbackBuiltins = {
2396
2719
  ln: async (argv, state) => runLnFallback(argv, state),
2397
2720
  chmod: async (argv, state) => runChmodFallback(argv, state),
2398
2721
  uname: async (argv) => runUnameFallback(argv),
2722
+ pspa: runPspaFallback,
2723
+ pspac: runPspacFallback,
2399
2724
  bunmsh: runBunmshFallback,
2400
2725
  bun: runBunFallback,
2401
2726
  serve: async (argv, state) => {
@@ -2675,13 +3000,26 @@ function parseCompoundScript(source) {
2675
3000
  // ShellSyntaxError whose message starts with "unterminated". An interactive
2676
3001
  // front end can use this, mirroring mksh's PS2 continuation prompt, to tell
2677
3002
  // "still typing this command" apart from a real syntax error.
3003
+ export function isJavaScriptMode(source) {
3004
+ return source.trimStart().startsWith("Bun.");
3005
+ }
3006
+
2678
3007
  export function needsMoreInput(source) {
2679
3008
  source = normalizeSource(source);
2680
3009
  try {
2681
3010
  if (hasCompoundSyntax(source)) parseCompoundScript(source);
2682
3011
  else tokenize(source, { strict: true });
2683
3012
  } catch (error) {
2684
- return error instanceof ShellSyntaxError && error.message.startsWith("unterminated");
3013
+ if (!(error instanceof ShellSyntaxError && error.message.startsWith("unterminated")))
3014
+ return false;
3015
+ // JavaScript has no line-continuation backslash outside a string
3016
+ // literal: there it is a syntax error, not a request for another line,
3017
+ // so asking for one only leads the typing somewhere that cannot parse.
3018
+ // An unterminated quote still continues, because that is JavaScript's
3019
+ // own line continuation and the evaluator does accept it.
3020
+ if (isJavaScriptMode(source) && error.message === "unterminated line continuation")
3021
+ return false;
3022
+ return true;
2685
3023
  }
2686
3024
  return false;
2687
3025
  }
@@ -3394,12 +3732,15 @@ export async function execute(source, state = createState(), io = {}) {
3394
3732
  return execution;
3395
3733
  }
3396
3734
  const rawSource = source.trimStart();
3397
- if (rawSource.startsWith("Bun.")) {
3735
+ if (isJavaScriptMode(rawSource)) {
3398
3736
  let execution;
3399
3737
  const previousCwd = process.cwd();
3400
3738
  try {
3401
3739
  try {
3402
3740
  process.chdir(nativePath(state.cwd));
3741
+
3742
+ const $ = state.env
3743
+
3403
3744
  const value = await eval(rawSource);
3404
3745
  execution = result(0, value === undefined ? "" : `${formatValue(value)}\n`);
3405
3746
  } finally {
@@ -3416,11 +3757,11 @@ export async function execute(source, state = createState(), io = {}) {
3416
3757
  return execution;
3417
3758
  }
3418
3759
  const lines = source.match(/.*(?:\n|$)/g)?.filter(Boolean) ?? [];
3419
- if (lines.some((line) => line.trimStart().startsWith("Bun."))) {
3760
+ if (lines.some((line) => isJavaScriptMode(line))) {
3420
3761
  const parts = [];
3421
3762
  let shellSource = "";
3422
3763
  for (const line of lines) {
3423
- if (!line.trimStart().startsWith("Bun.")) {
3764
+ if (!isJavaScriptMode(line)) {
3424
3765
  shellSource += line;
3425
3766
  continue;
3426
3767
  }