typescript-virtual-container 1.5.4 → 1.5.5

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.
@@ -12,6 +12,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
12
12
  let historyIndex = null;
13
13
  let historyDraft = "";
14
14
  let cwd = userHome(authUser);
15
+ let pendingHeredoc = null;
15
16
  const shellEnv = makeDefaultEnv(authUser, hostname);
16
17
  let nanoSession = null;
17
18
  let pendingSudo = null;
@@ -22,23 +23,33 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
22
23
  };
23
24
  const commandNames = Array.from(new Set(getCommandNames())).sort();
24
25
  console.log(`[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`);
25
- // Load .bashrc if it exists
26
+ // Source login/rc files at startup
26
27
  void (async () => {
27
- const bashrcPath = `${userHome(authUser)}/.bashrc`;
28
- if (shell.vfs.exists(bashrcPath)) {
28
+ const sourceFile = async (filePath, isEnvFile = false) => {
29
+ if (!shell.vfs.exists(filePath))
30
+ return;
29
31
  try {
30
- const bashrc = shell.vfs.readFile(bashrcPath);
31
- for (const line of bashrc.split("\n")) {
32
+ const content = shell.vfs.readFile(filePath);
33
+ for (const line of content.split("\n")) {
32
34
  const l = line.trim();
33
35
  if (!l || l.startsWith("#"))
34
36
  continue;
35
- await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
37
+ if (isEnvFile) {
38
+ // /etc/environment: KEY=VALUE pairs only, no shell syntax
39
+ const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=["']?(.+?)["']?\s*$/);
40
+ if (m)
41
+ shellEnv.vars[m[1]] = m[2];
42
+ }
43
+ else {
44
+ await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
45
+ }
36
46
  }
37
47
  }
38
- catch {
39
- /* ignore bashrc errors */
40
- }
41
- }
48
+ catch { /* ignore */ }
49
+ };
50
+ await sourceFile("/etc/environment", true);
51
+ await sourceFile(`${userHome(authUser)}/.profile`);
52
+ await sourceFile(`${userHome(authUser)}/.bashrc`);
42
53
  })();
43
54
  function renderLine() {
44
55
  const prompt = buildCurrentPrompt();
@@ -283,6 +294,53 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
283
294
  nanoSession.process.stdin.write(chunk);
284
295
  return;
285
296
  }
297
+ if (pendingHeredoc) {
298
+ const hd = pendingHeredoc;
299
+ const input = chunk.toString("utf8");
300
+ for (let i = 0; i < input.length; i++) {
301
+ const ch = input[i];
302
+ if (ch === "") {
303
+ pendingHeredoc = null;
304
+ stream.write("^C\r\n");
305
+ renderLine();
306
+ return;
307
+ }
308
+ if (ch === "" || ch === "\b") {
309
+ lineBuffer = lineBuffer.slice(0, -1);
310
+ renderLine();
311
+ continue;
312
+ }
313
+ if (ch === "\r" || ch === "\n") {
314
+ const typedLine = lineBuffer;
315
+ lineBuffer = "";
316
+ cursorPos = 0;
317
+ stream.write("\r\n");
318
+ if (typedLine === hd.delimiter) {
319
+ const stdin = hd.lines.join("\n");
320
+ const cmd = hd.cmdBefore;
321
+ pendingHeredoc = null;
322
+ pushHistory(`${cmd} << ${hd.delimiter}`);
323
+ const result = await Promise.resolve(runCommand(cmd, authUser, hostname, "shell", cwd, shell, stdin, shellEnv));
324
+ if (result.stdout)
325
+ stream.write(`${toTtyLines(result.stdout)}\r\n`);
326
+ if (result.stderr)
327
+ stream.write(`${toTtyLines(result.stderr)}\r\n`);
328
+ if (result.nextCwd)
329
+ cwd = result.nextCwd;
330
+ renderLine();
331
+ return;
332
+ }
333
+ hd.lines.push(typedLine);
334
+ stream.write("> ");
335
+ continue;
336
+ }
337
+ if (ch >= " " || ch === "\t") {
338
+ lineBuffer += ch;
339
+ stream.write(ch);
340
+ }
341
+ }
342
+ return;
343
+ }
286
344
  if (pendingSudo) {
287
345
  const input = chunk.toString("utf8");
288
346
  for (let i = 0; i < input.length; i += 1) {
@@ -407,6 +465,47 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
407
465
  }
408
466
  continue;
409
467
  }
468
+ // Home: \x1b[1~ or \x1b[H
469
+ if (third === "1" && fourth === "~") {
470
+ i += 3;
471
+ cursorPos = 0;
472
+ renderLine();
473
+ continue;
474
+ }
475
+ if (third === "H") {
476
+ i += 2;
477
+ cursorPos = 0;
478
+ renderLine();
479
+ continue;
480
+ }
481
+ // End: \x1b[4~ or \x1b[F
482
+ if (third === "4" && fourth === "~") {
483
+ i += 3;
484
+ cursorPos = lineBuffer.length;
485
+ renderLine();
486
+ continue;
487
+ }
488
+ if (third === "F") {
489
+ i += 2;
490
+ cursorPos = lineBuffer.length;
491
+ renderLine();
492
+ continue;
493
+ }
494
+ }
495
+ // Home/End via \x1bO sequences (some terminals)
496
+ if (next === "O" && third) {
497
+ if (third === "H") {
498
+ i += 2;
499
+ cursorPos = 0;
500
+ renderLine();
501
+ continue;
502
+ }
503
+ if (third === "F") {
504
+ i += 2;
505
+ cursorPos = lineBuffer.length;
506
+ renderLine();
507
+ continue;
508
+ }
410
509
  }
411
510
  }
412
511
  if (ch === "\u0003") {
@@ -418,13 +517,61 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
418
517
  renderLine();
419
518
  continue;
420
519
  }
520
+ if (ch === "\u0001") {
521
+ cursorPos = 0;
522
+ renderLine();
523
+ continue;
524
+ } // Ctrl+A
525
+ if (ch === "\u0005") {
526
+ cursorPos = lineBuffer.length;
527
+ renderLine();
528
+ continue;
529
+ } // Ctrl+E
530
+ if (ch === "\u000b") {
531
+ lineBuffer = lineBuffer.slice(0, cursorPos);
532
+ renderLine();
533
+ continue;
534
+ } // Ctrl+K
535
+ if (ch === "\u0015") {
536
+ lineBuffer = lineBuffer.slice(cursorPos);
537
+ cursorPos = 0;
538
+ renderLine();
539
+ continue;
540
+ } // Ctrl+U
541
+ if (ch === "\u0017") { // Ctrl+W — kill word backward
542
+ let wStart = cursorPos;
543
+ while (wStart > 0 && lineBuffer[wStart - 1] === " ")
544
+ wStart--;
545
+ while (wStart > 0 && lineBuffer[wStart - 1] !== " ")
546
+ wStart--;
547
+ lineBuffer = lineBuffer.slice(0, wStart) + lineBuffer.slice(cursorPos);
548
+ cursorPos = wStart;
549
+ renderLine();
550
+ continue;
551
+ }
421
552
  if (ch === "\r" || ch === "\n") {
422
- const line = lineBuffer.trim();
553
+ let line = lineBuffer.trim();
423
554
  lineBuffer = "";
424
555
  cursorPos = 0;
425
556
  historyIndex = null;
426
557
  historyDraft = "";
427
558
  stream.write("\r\n");
559
+ // !! history expansion
560
+ if (line === "!!" || line.startsWith("!! ") || /\s!!$/.test(line) || / !! /.test(line)) {
561
+ const lastCmd = history.length > 0 ? history[history.length - 1] : "";
562
+ line = line === "!!" ? lastCmd : line.replace(/!!/g, lastCmd);
563
+ }
564
+ else if (/(?:^|\s)!!/.test(line)) {
565
+ const lastCmd = history.length > 0 ? history[history.length - 1] : "";
566
+ line = line.replace(/!!/g, lastCmd);
567
+ }
568
+ // Heredoc detection: cmd << DELIM
569
+ const heredocMatch = line.match(/^(.*?)\s*<<-?\s*['"`]?([A-Za-z_][A-Za-z0-9_]*)['"`]?\s*$/);
570
+ if (heredocMatch && line.length > 0) {
571
+ pendingHeredoc = { delimiter: heredocMatch[2], lines: [], cmdBefore: heredocMatch[1].trim() || "cat" };
572
+ stream.write("> ");
573
+ continue;
574
+ }
428
575
  if (line.length > 0) {
429
576
  const result = await Promise.resolve(runCommand(line, authUser, hostname, "shell", cwd, shell, undefined, shellEnv));
430
577
  pushHistory(line);
@@ -0,0 +1,2 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ export declare const bcCommand: ShellModule;
@@ -0,0 +1,28 @@
1
+ import { evalArith } from "../utils/expand";
2
+ export const bcCommand = {
3
+ name: "bc",
4
+ description: "Arbitrary precision calculator language",
5
+ category: "system",
6
+ params: ["[expression]"],
7
+ run: ({ args, stdin }) => {
8
+ const input = (stdin ?? args.join(" ")).trim();
9
+ if (!input)
10
+ return { stdout: "", exitCode: 0 };
11
+ const results = [];
12
+ for (const line of input.split("\n")) {
13
+ const expr = line.trim();
14
+ if (!expr || expr.startsWith("#"))
15
+ continue;
16
+ // Strip trailing semicolons
17
+ const cleaned = expr.replace(/;+$/, "").trim();
18
+ const val = evalArith(cleaned, {});
19
+ if (!Number.isNaN(val)) {
20
+ results.push(String(val));
21
+ }
22
+ else {
23
+ return { stderr: `bc: syntax error on line: ${expr}`, exitCode: 1 };
24
+ }
25
+ }
26
+ return { stdout: results.join("\n"), exitCode: 0 };
27
+ },
28
+ };
@@ -4,4 +4,4 @@ import type { ShellModule } from "../types/commands";
4
4
  * @category network
5
5
  * @params ["<object> <command>"]
6
6
  */
7
- export declare const ifconfigCommand: ShellModule;
7
+ export declare const ipCommand: ShellModule;
@@ -21,8 +21,8 @@ const LINK_OUTPUT = `1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state
21
21
  * @category network
22
22
  * @params ["<object> <command>"]
23
23
  */
24
- export const ifconfigCommand = {
25
- name: "ifconfig",
24
+ export const ipCommand = {
25
+ name: "ip",
26
26
  description: "Show/manipulate routing, network devices, interfaces",
27
27
  category: "network",
28
28
  params: ["<object> <command>"],
@@ -30,7 +30,7 @@ export const ifconfigCommand = {
30
30
  const obj = args[0]?.toLowerCase();
31
31
  const cmd = args[1]?.toLowerCase() ?? "show";
32
32
  if (!obj) {
33
- return { stderr: "Usage: ifconfig [interface]", exitCode: 1 };
33
+ return { stderr: "Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }\nOBJECT := { link | addr | route | neigh }", exitCode: 1 };
34
34
  }
35
35
  if (obj === "addr" || obj === "address" || obj === "a") {
36
36
  return { stdout: ADDR_OUTPUT, exitCode: 0 };
@@ -0,0 +1,4 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ export declare const jobsCommand: ShellModule;
3
+ export declare const bgCommand: ShellModule;
4
+ export declare const fgCommand: ShellModule;
@@ -0,0 +1,27 @@
1
+ export const jobsCommand = {
2
+ name: "jobs",
3
+ description: "List active jobs",
4
+ category: "shell",
5
+ params: [],
6
+ run: () => ({ stdout: "", exitCode: 0 }),
7
+ };
8
+ export const bgCommand = {
9
+ name: "bg",
10
+ description: "Resume a suspended job in the background",
11
+ category: "shell",
12
+ params: ["[%jobspec]"],
13
+ run: ({ args }) => ({
14
+ stderr: `bg: ${args[0] ?? "%1"}: no such job`,
15
+ exitCode: 1,
16
+ }),
17
+ };
18
+ export const fgCommand = {
19
+ name: "fg",
20
+ description: "Resume a suspended job in the foreground",
21
+ category: "shell",
22
+ params: ["[%jobspec]"],
23
+ run: ({ args }) => ({
24
+ stderr: `fg: ${args[0] ?? "%1"}: no such job`,
25
+ exitCode: 1,
26
+ }),
27
+ };
@@ -4,6 +4,7 @@ import { aptCacheCommand, aptCommand } from "./apt";
4
4
  import { awkCommand } from "./awk";
5
5
  import { base64Command } from "./base64";
6
6
  import { basenameCommand, dirnameCommand } from "./basename";
7
+ import { bcCommand } from "./bc";
7
8
  import { catCommand } from "./cat";
8
9
  import { cdCommand } from "./cd";
9
10
  import { chmodCommand } from "./chmod";
@@ -35,7 +36,8 @@ import { historyCommand } from "./history";
35
36
  import { hostnameCommand } from "./hostname";
36
37
  import { htopCommand } from "./htop";
37
38
  import { idCommand } from "./id";
38
- import { ifconfigCommand } from "./ifconfig";
39
+ import { ipCommand } from "./ip";
40
+ import { bgCommand, fgCommand, jobsCommand } from "./jobs";
39
41
  import { killCommand } from "./kill";
40
42
  import { dmesgCommand, lastCommand } from "./last";
41
43
  import { lnCommand, readlinkCommand } from "./ln";
@@ -81,13 +83,13 @@ import { unameCommand } from "./uname";
81
83
  import { uniqCommand } from "./uniq";
82
84
  import { unsetCommand } from "./unset";
83
85
  import { uptimeCommand } from "./uptime";
86
+ import { wCommand } from "./w";
84
87
  import { wcCommand } from "./wc";
85
88
  import { wgetCommand } from "./wget";
86
89
  import { whichCommand } from "./which";
87
90
  import { whoCommand } from "./who";
88
91
  import { whoamiCommand } from "./whoami";
89
92
  import { xargsCommand } from "./xargs";
90
- import { wCommand } from "./w";
91
93
  const BASE_COMMANDS = [
92
94
  // Navigation
93
95
  pwdCommand,
@@ -159,7 +161,7 @@ const BASE_COMMANDS = [
159
161
  sttyCommand,
160
162
  lastCommand,
161
163
  dmesgCommand,
162
- ifconfigCommand,
164
+ ipCommand,
163
165
  yesCommand,
164
166
  fortuneCommand,
165
167
  cowsayCommand,
@@ -184,6 +186,10 @@ const BASE_COMMANDS = [
184
186
  dpkgCommand,
185
187
  dpkgQueryCommand,
186
188
  // Shell (extended)
189
+ jobsCommand,
190
+ bgCommand,
191
+ fgCommand,
192
+ bcCommand,
187
193
  whichCommand,
188
194
  typeCommand,
189
195
  manCommand,
@@ -15,10 +15,11 @@ export function makeDefaultEnv(authUser, hostname) {
15
15
  HOME: userHome(authUser),
16
16
  USER: authUser,
17
17
  LOGNAME: authUser,
18
- SHELL: "/bin/sh",
18
+ SHELL: "/bin/bash",
19
19
  TERM: "xterm-256color",
20
20
  HOSTNAME: hostname,
21
21
  PS1: "\\u@\\h:\\w\\$ ",
22
+ "0": "/bin/bash",
22
23
  },
23
24
  lastExitCode: 0,
24
25
  };
@@ -11,11 +11,31 @@ export const setCommand = {
11
11
  run: ({ args, env }) => {
12
12
  if (args.length === 0) {
13
13
  const out = Object.entries(env.vars)
14
+ .filter(([k]) => !k.startsWith("__"))
14
15
  .map(([k, v]) => `${k}=${v}`)
15
16
  .join("\n");
16
17
  return { stdout: out, exitCode: 0 };
17
18
  }
18
19
  for (const arg of args) {
20
+ const flagMatch = arg.match(/^([+-])([a-zA-Z]+)$/);
21
+ if (flagMatch) {
22
+ const on = flagMatch[1] === "-";
23
+ for (const flag of flagMatch[2]) {
24
+ if (flag === "e") {
25
+ if (on)
26
+ env.vars.__errexit = "1";
27
+ else
28
+ delete env.vars.__errexit;
29
+ }
30
+ if (flag === "x") {
31
+ if (on)
32
+ env.vars.__xtrace = "1";
33
+ else
34
+ delete env.vars.__xtrace;
35
+ }
36
+ }
37
+ continue;
38
+ }
19
39
  if (arg.includes("=")) {
20
40
  const eq = arg.indexOf("=");
21
41
  env.vars[arg.slice(0, eq)] = arg.slice(eq + 1);
@@ -125,6 +125,43 @@ function parseBlocks(lines) {
125
125
  }
126
126
  blocks.push({ type: "while", cond, body });
127
127
  }
128
+ else if (line.startsWith("case ") && line.endsWith(" in") || line.match(/^case\s+.+\s+in$/)) {
129
+ const caseExpr = line.replace(/^case\s+/, "").replace(/\s+in$/, "").trim();
130
+ const patterns = [];
131
+ i++;
132
+ while (i < lines.length && lines[i]?.trim() !== "esac") {
133
+ const pl = lines[i].trim();
134
+ if (!pl || pl === "esac") {
135
+ i++;
136
+ continue;
137
+ }
138
+ // pattern) or pattern1|pattern2)
139
+ const patMatch = pl.match(/^(.+?)\)\s*(.*)$/);
140
+ if (patMatch) {
141
+ const pat = patMatch[1].trim();
142
+ const body = [];
143
+ if (patMatch[2]?.trim() && patMatch[2].trim() !== ";;") {
144
+ body.push(patMatch[2].trim());
145
+ }
146
+ i++;
147
+ while (i < lines.length) {
148
+ const bl = lines[i].trim();
149
+ if (bl === ";;" || bl === "esac")
150
+ break;
151
+ if (bl)
152
+ body.push(bl);
153
+ i++;
154
+ }
155
+ if (lines[i]?.trim() === ";;")
156
+ i++; // skip ;;
157
+ patterns.push({ pattern: pat, body });
158
+ }
159
+ else {
160
+ i++;
161
+ }
162
+ }
163
+ blocks.push({ type: "case", expr: caseExpr, patterns });
164
+ }
128
165
  else {
129
166
  blocks.push({ type: "cmd", line });
130
167
  }
@@ -189,9 +226,12 @@ async function evalCondition(cond, ctx) {
189
226
  async function runBlocks(blocks, ctx) {
190
227
  let lastResult = { exitCode: 0 };
191
228
  let output = "";
229
+ let traceOutput = "";
192
230
  for (const block of blocks) {
193
231
  if (block.type === "cmd") {
194
232
  const expanded = await expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode, ctx);
233
+ if (ctx.env.vars.__xtrace)
234
+ traceOutput += `+ ${expanded}\n`;
195
235
  // Bare VAR=val assignment(s) — handle before dispatching to runCommand
196
236
  const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)/;
197
237
  const tokens = expanded.trim().split(/\s+/);
@@ -231,6 +271,8 @@ async function runBlocks(blocks, ctx) {
231
271
  output += `${r.stdout}\n`;
232
272
  if (r.stderr)
233
273
  return { ...r, stdout: output.trim() };
274
+ if (ctx.env.vars.__errexit && (r.exitCode ?? 0) !== 0)
275
+ return { ...r, stdout: output.trim() };
234
276
  lastResult = r;
235
277
  }
236
278
  else if (block.type === "if") {
@@ -311,8 +353,34 @@ async function runBlocks(blocks, ctx) {
311
353
  iterations++;
312
354
  }
313
355
  }
356
+ else if (block.type === "case") {
357
+ const expanded = await expandVars(block.expr, ctx.env.vars, ctx.env.lastExitCode, ctx);
358
+ for (const pat of block.patterns) {
359
+ const alts = pat.pattern.split("|").map((p) => p.trim());
360
+ const matched = alts.some((p) => {
361
+ if (p === "*")
362
+ return true;
363
+ if (p.includes("*") || p.includes("?")) {
364
+ const re = new RegExp(`^${p.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
365
+ return re.test(expanded);
366
+ }
367
+ return p === expanded;
368
+ });
369
+ if (matched) {
370
+ const sub = await runBlocks(parseBlocks(pat.body), ctx);
371
+ if (sub.stdout)
372
+ output += `${sub.stdout}\n`;
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ }
378
+ const finalStdout = output.trim() || lastResult.stdout;
379
+ if (traceOutput) {
380
+ const traceStderr = (lastResult.stderr ? `${lastResult.stderr}\n` : "") + traceOutput.trim();
381
+ return { ...lastResult, stdout: finalStdout, stderr: traceStderr || lastResult.stderr };
314
382
  }
315
- return { ...lastResult, stdout: output.trim() || lastResult.stdout };
383
+ return { ...lastResult, stdout: finalStdout };
316
384
  }
317
385
  /**
318
386
  * Execute shell scripts or commands with a minimal shell interpreter.
@@ -369,10 +369,12 @@ export function expandSync(input, env, lastExit = 0, home) {
369
369
  let s = chunk;
370
370
  // Tilde expansion — only at start of token or after `:` or whitespace
371
371
  s = s.replace(/(^|[\s:])~(\/|$)/g, (_, pre, post) => `${pre}${homePath}${post}`);
372
- // $? $$ $#
372
+ // $? $$ $# $RANDOM $LINENO
373
373
  s = s.replace(/\$\?/g, String(lastExit));
374
374
  s = s.replace(/\$\$/g, "1");
375
375
  s = s.replace(/\$#/g, "0");
376
+ s = s.replace(/\$RANDOM\b/g, () => String(Math.floor(Math.random() * 32768)));
377
+ s = s.replace(/\$LINENO\b/g, "1");
376
378
  // $(( arithmetic )) — must come before ${ and $VAR to avoid conflicts
377
379
  s = expandArithmeticChunks(s, env);
378
380
  // ${#VAR} — string length
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
- "version": "1.5.4",
7
+ "version": "1.5.5",
8
8
  "files": [
9
9
  "dist/",
10
10
  "README.md",