typescript-virtual-container 1.4.1 → 1.4.3

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.
Files changed (67) hide show
  1. package/.vscode/settings.json +2 -0
  2. package/README.md +77 -36
  3. package/benchmark-virtualshell.ts +3 -11
  4. package/builds/self-standalone.js +224 -224
  5. package/builds/self-standalone.js.map +4 -4
  6. package/builds/standalone-wo-sftp.js +23 -23
  7. package/builds/standalone-wo-sftp.js.map +3 -3
  8. package/builds/standalone.js +23 -23
  9. package/builds/standalone.js.map +3 -3
  10. package/dist/VirtualFileSystem/index.d.ts +47 -0
  11. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem/index.js +159 -0
  13. package/dist/VirtualShell/index.d.ts +29 -0
  14. package/dist/VirtualShell/index.d.ts.map +1 -1
  15. package/dist/VirtualShell/index.js +29 -0
  16. package/dist/VirtualShell/shell.d.ts.map +1 -1
  17. package/dist/VirtualShell/shell.js +6 -10
  18. package/dist/VirtualShell/shellParser.js +28 -1
  19. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  20. package/dist/VirtualUserManager/index.js +4 -4
  21. package/dist/commands/export.d.ts.map +1 -1
  22. package/dist/commands/export.js +5 -3
  23. package/dist/commands/helpers.js +1 -1
  24. package/dist/commands/history.js +2 -2
  25. package/dist/commands/registry.d.ts.map +1 -1
  26. package/dist/commands/registry.js +2 -0
  27. package/dist/commands/runtime.d.ts.map +1 -1
  28. package/dist/commands/runtime.js +28 -3
  29. package/dist/commands/seq.d.ts +4 -0
  30. package/dist/commands/seq.d.ts.map +1 -0
  31. package/dist/commands/seq.js +50 -0
  32. package/dist/commands/sh.d.ts +0 -6
  33. package/dist/commands/sh.d.ts.map +1 -1
  34. package/dist/commands/sh.js +153 -10
  35. package/dist/modules/linuxRootfs.d.ts +1 -1
  36. package/dist/modules/linuxRootfs.d.ts.map +1 -1
  37. package/dist/modules/linuxRootfs.js +5 -5
  38. package/dist/self-standalone.js +149 -102
  39. package/dist/types/pipeline.d.ts +6 -0
  40. package/dist/types/pipeline.d.ts.map +1 -1
  41. package/dist/types/vfs.d.ts +15 -0
  42. package/dist/types/vfs.d.ts.map +1 -1
  43. package/dist/utils/expand.d.ts +9 -0
  44. package/dist/utils/expand.d.ts.map +1 -1
  45. package/dist/utils/expand.js +84 -2
  46. package/dist/utils/tokenize.d.ts.map +1 -1
  47. package/dist/utils/tokenize.js +40 -0
  48. package/package.json +1 -1
  49. package/src/VirtualFileSystem/index.ts +164 -1
  50. package/src/VirtualShell/index.ts +36 -0
  51. package/src/VirtualShell/shell.ts +6 -11
  52. package/src/VirtualShell/shellParser.ts +26 -1
  53. package/src/VirtualUserManager/index.ts +4 -4
  54. package/src/commands/export.ts +5 -3
  55. package/src/commands/helpers.ts +1 -1
  56. package/src/commands/history.ts +2 -2
  57. package/src/commands/registry.ts +2 -0
  58. package/src/commands/runtime.ts +30 -3
  59. package/src/commands/seq.ts +43 -0
  60. package/src/commands/sh.ts +144 -19
  61. package/src/modules/linuxRootfs.ts +6 -6
  62. package/src/self-standalone.ts +190 -141
  63. package/src/types/pipeline.ts +6 -0
  64. package/src/types/vfs.ts +17 -0
  65. package/src/utils/expand.ts +75 -2
  66. package/src/utils/tokenize.ts +20 -0
  67. package/tests/helpers.test.ts +3 -3
@@ -45,6 +45,8 @@ type Block =
45
45
  }
46
46
  | { type: "for"; var: string; list: string; body: string[] }
47
47
  | { type: "while"; cond: string; body: string[] }
48
+ | { type: "func"; name: string; body: string[] }
49
+ | { type: "arith"; expr: string }
48
50
  | { type: "cmd"; line: string };
49
51
 
50
52
  /** Very small shell interpreter: supports if/elif/else/fi, for/do/done, while/do/done */
@@ -58,6 +60,41 @@ function parseBlocks(lines: string[]): Block[] {
58
60
  continue;
59
61
  }
60
62
 
63
+ // Function definition: name() { or function name { or name() { body }
64
+ const funcMatchInline = line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{(.+)\}\s*$/);
65
+ const funcMatch = funcMatchInline ?? (
66
+ line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{?\s*$/) ||
67
+ line.match(/^function\s+(\w+)\s*\{?\s*$/)
68
+ );
69
+ if (funcMatch) {
70
+ const funcName = funcMatch[1]!;
71
+ const body: string[] = [];
72
+ // Inline: name() { cmd; } — single-line form
73
+ if (funcMatchInline) {
74
+ body.push(...funcMatchInline[2]!.split(";").map((s: string) => s.trim()).filter(Boolean));
75
+ blocks.push({ type: "func", name: funcName, body });
76
+ i++;
77
+ continue;
78
+ }
79
+ i++;
80
+ while (i < lines.length && lines[i]?.trim() !== "}" && i < lines.length + 1) {
81
+ const l = lines[i]!.trim().replace(/^do\s+/, "");
82
+ if (l && l !== "{") body.push(l);
83
+ i++;
84
+ }
85
+ i++; // skip closing }
86
+ blocks.push({ type: "func", name: funcName, body });
87
+ continue;
88
+ }
89
+
90
+ // (( expr )) arithmetic statement
91
+ const arithMatch = line.match(/^\(\(\s*(.+?)\s*\)\)$/);
92
+ if (arithMatch) {
93
+ blocks.push({ type: "arith", expr: arithMatch[1]! });
94
+ i++;
95
+ continue;
96
+ }
97
+
61
98
  if (line.startsWith("if ") || line === "if") {
62
99
  const cond = line
63
100
  .replace(/^if\s+/, "")
@@ -225,16 +262,34 @@ async function runBlocks(
225
262
  }
226
263
  }
227
264
 
228
- const r = await runCommand(
229
- expanded,
230
- ctx.authUser,
231
- ctx.hostname,
232
- ctx.mode,
233
- ctx.cwd,
234
- ctx.shell,
235
- undefined,
236
- ctx.env,
237
- );
265
+ const r = await (async () => {
266
+ // Check if expanded matches a registered function
267
+ const cmdName = expanded.trim().split(/\s+/)[0] ?? "";
268
+ const funcBody = ctx.env.vars[`__func_${cmdName}`];
269
+ if (funcBody) {
270
+ // Set positional params $1 $2 ... from remaining args
271
+ const funcArgs = expanded.trim().split(/\s+/).slice(1);
272
+ const savedVars = { ...ctx.env.vars };
273
+ funcArgs.forEach((a, i) => { ctx.env.vars[String(i + 1)] = a; });
274
+ ctx.env.vars["0"] = cmdName;
275
+ const funcLines = funcBody.split("\n");
276
+ const funcResult = await runBlocks(parseBlocks(funcLines), ctx);
277
+ // Restore positional params
278
+ for (let pi = 1; pi <= funcArgs.length; pi++) delete ctx.env.vars[String(pi)];
279
+ Object.assign(ctx.env.vars, { ...savedVars, ...ctx.env.vars });
280
+ return funcResult;
281
+ }
282
+ return runCommand(
283
+ expanded,
284
+ ctx.authUser,
285
+ ctx.hostname,
286
+ ctx.mode,
287
+ ctx.cwd,
288
+ ctx.shell,
289
+ undefined,
290
+ ctx.env,
291
+ );
292
+ })();
238
293
  ctx.env.lastExitCode = r.exitCode ?? 0;
239
294
  if (r.stdout) output += `${r.stdout}\n`;
240
295
  if (r.stderr) return { ...r, stdout: output.trim() };
@@ -259,6 +314,27 @@ async function runBlocks(
259
314
  if (sub.stdout) output += `${sub.stdout}\n`;
260
315
  }
261
316
  }
317
+ } else if (block.type === "func") {
318
+ // Register function in env vars as __func_<name>=<body>
319
+ ctx.env.vars[`__func_${block.name}`] = block.body.join("\n");
320
+ } else if (block.type === "arith") {
321
+ // (( expr )) — evaluate arithmetic, update vars
322
+ const { expandSync } = await import("../utils/expand");
323
+ const expr = expandSync(block.expr, ctx.env.vars, ctx.env.lastExitCode);
324
+ // Handle i++ / i-- / i+=N / i-=N
325
+ const incMatch = expr.match(/^(\w+)\s*(\+\+|--)$/);
326
+ if (incMatch) {
327
+ const val = parseInt(ctx.env.vars[incMatch[1]!] ?? "0", 10);
328
+ ctx.env.vars[incMatch[1]!] = String(incMatch[2] === "++" ? val + 1 : val - 1);
329
+ } else {
330
+ const assignMatch = expr.match(/^(\w+)\s*([+\-*/])=\s*(.+)$/);
331
+ if (assignMatch) {
332
+ const lhs = parseInt(ctx.env.vars[assignMatch[1]!] ?? "0", 10);
333
+ const rhs = parseInt(assignMatch[3]!, 10);
334
+ const ops: Record<string, number> = { "+": lhs + rhs, "-": lhs - rhs, "*": lhs * rhs, "/": Math.floor(lhs / rhs) };
335
+ ctx.env.vars[assignMatch[1]!] = String(ops[assignMatch[2]!] ?? lhs);
336
+ }
337
+ }
262
338
  } else if (block.type === "for") {
263
339
  const listExpanded = await expandVars(
264
340
  block.list,
@@ -266,7 +342,9 @@ async function runBlocks(
266
342
  ctx.env.lastExitCode,
267
343
  ctx,
268
344
  );
269
- const items = listExpanded.trim().split(/\s+/);
345
+ // Apply brace expansion to each token in the list
346
+ const { expandBraces } = await import("../utils/expand");
347
+ const items = listExpanded.trim().split(/\s+/).flatMap(expandBraces);
270
348
  for (const item of items) {
271
349
  ctx.env.vars[block.var] = item;
272
350
  const sub = await runBlocks(parseBlocks(block.body), ctx);
@@ -292,6 +370,59 @@ async function runBlocks(
292
370
  * @category shell
293
371
  * @params ["-c <script>", "[<file>]"]
294
372
  */
373
+
374
+ /**
375
+ * Split a sh script into logical lines, respecting:
376
+ * - `{...}` braces (function bodies)
377
+ * - Newlines and semicolons at depth 0 only
378
+ */
379
+ function splitShScript(script: string): string[] {
380
+ const lines: string[] = [];
381
+ let current = "";
382
+ let depth = 0;
383
+ let inSingleQ = false;
384
+ let inDoubleQ = false;
385
+ let i = 0;
386
+ while (i < script.length) {
387
+ const ch = script[i]!;
388
+ if (!inSingleQ && !inDoubleQ) {
389
+ if (ch === "'") { inSingleQ = true; current += ch; i++; continue; }
390
+ if (ch === '"') { inDoubleQ = true; current += ch; i++; continue; }
391
+ if (ch === "{") { depth++; current += ch; i++; continue; }
392
+ if (ch === "}") {
393
+ depth--;
394
+ current += ch;
395
+ i++;
396
+ // At depth 0, closing } ends the function body line
397
+ if (depth === 0) {
398
+ const t = current.trim();
399
+ if (t) lines.push(t);
400
+ current = "";
401
+ // Skip trailing ; or whitespace
402
+ while (i < script.length && (script[i] === ";" || script[i] === " ")) i++;
403
+ }
404
+ continue;
405
+ }
406
+ if (depth === 0 && (ch === ";" || ch === "\n")) {
407
+ const t = current.trim();
408
+ if (t && !t.startsWith("#")) lines.push(t);
409
+ current = "";
410
+ i++;
411
+ continue;
412
+ }
413
+ } else if (inSingleQ && ch === "'") {
414
+ inSingleQ = false;
415
+ } else if (inDoubleQ && ch === '"') {
416
+ inDoubleQ = false;
417
+ }
418
+ current += ch;
419
+ i++;
420
+ }
421
+ const t = current.trim();
422
+ if (t && !t.startsWith("#")) lines.push(t);
423
+ return lines;
424
+ }
425
+
295
426
  export const shCommand: ShellModule = {
296
427
  name: "sh",
297
428
  aliases: ["bash"],
@@ -305,10 +436,7 @@ export const shCommand: ShellModule = {
305
436
  if (ifFlag(args, "-c")) {
306
437
  const script = args[args.indexOf("-c") + 1] ?? "";
307
438
  if (!script) return { stderr: "sh: -c requires a script", exitCode: 1 };
308
- const lines = script
309
- .split(/[;\n]/)
310
- .map((l) => l.trim())
311
- .filter((l) => l && !l.startsWith("#"));
439
+ const lines = splitShScript(script);
312
440
  const blocks = parseBlocks(lines);
313
441
  return runBlocks(blocks, ctx);
314
442
  }
@@ -323,10 +451,7 @@ export const shCommand: ShellModule = {
323
451
  exitCode: 1,
324
452
  };
325
453
  const content = shell.vfs.readFile(p);
326
- const lines = content
327
- .split("\n")
328
- .map((l) => l.trim())
329
- .filter((l) => l && !l.startsWith("#"));
454
+ const lines = splitShScript(content);
330
455
  const blocks = parseBlocks(lines);
331
456
  return runBlocks(blocks, ctx);
332
457
  }
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  import * as os from "node:os";
11
- import type { ShellProperties } from "../VirtualShell";
12
11
  import type VirtualFileSystem from "../VirtualFileSystem";
12
+ import type { ShellProperties } from "../VirtualShell";
13
13
  import type { VirtualUserManager } from "../VirtualUserManager";
14
14
 
15
15
  // ─── helpers ────────────────────────────────────────────────────────────────
@@ -584,7 +584,7 @@ function bootstrapRoot(vfs: VirtualFileSystem): void {
584
584
  ensureDir(vfs, "/root", 0o700);
585
585
  ensureFile(
586
586
  vfs,
587
- "/root/.bashrc",
587
+ "/home/root/.bashrc",
588
588
  `${[
589
589
  "# root .bashrc",
590
590
  "export PS1='\\[\\033[0;31m\\]\\u@\\h\\[\\033[0m\\]:\\[\\033[0;34m\\]\\w\\[\\033[0m\\]# '",
@@ -593,11 +593,11 @@ function bootstrapRoot(vfs: VirtualFileSystem): void {
593
593
  "alias la='ls -A'",
594
594
  ].join("\n")}\n`,
595
595
  );
596
- ensureFile(vfs, "/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
596
+ ensureFile(vfs, "/home/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
597
597
  // Fix: /home/root should map to /root for root user
598
- if (!vfs.exists("/home/root")) {
599
- vfs.symlink("/root", "/home/root");
600
- }
598
+ // if (!vfs.exists("/home/root")) {
599
+ // vfs.symlink("/root", "/home/root");
600
+ // }
601
601
  }
602
602
 
603
603
  // ─── /opt + /srv + /mnt + /media ─────────────────────────────────────────────