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
@@ -0,0 +1,4 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /** Generate sequences of numbers. seq LAST / seq FIRST LAST / seq FIRST INCREMENT LAST */
3
+ export declare const seqCommand: ShellModule;
4
+ //# sourceMappingURL=seq.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seq.d.ts","sourceRoot":"","sources":["../../src/commands/seq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,0FAA0F;AAC1F,eAAO,MAAM,UAAU,EAAE,WAuCxB,CAAC"}
@@ -0,0 +1,50 @@
1
+ /** Generate sequences of numbers. seq LAST / seq FIRST LAST / seq FIRST INCREMENT LAST */
2
+ export const seqCommand = {
3
+ name: "seq",
4
+ description: "Print a sequence of numbers",
5
+ category: "text",
6
+ params: ["[FIRST [INCREMENT]] LAST"],
7
+ run: ({ args }) => {
8
+ const nums = args.filter((a) => !a.startsWith("-") || /^-[\d.]/.test(a)).map(Number);
9
+ const sep = (() => { const i = args.indexOf("-s"); return i !== -1 ? (args[i + 1] ?? "\n") : "\n"; })();
10
+ const fmt = (() => { const i = args.indexOf("-f"); return i !== -1 ? (args[i + 1] ?? "%g") : null; })();
11
+ const width = args.includes("-w");
12
+ let first = 1, inc = 1, last;
13
+ if (nums.length === 1) {
14
+ last = nums[0];
15
+ }
16
+ else if (nums.length === 2) {
17
+ first = nums[0];
18
+ last = nums[1];
19
+ }
20
+ else {
21
+ first = nums[0];
22
+ inc = nums[1];
23
+ last = nums[2];
24
+ }
25
+ if (inc === 0)
26
+ return { stderr: "seq: zero increment\n", exitCode: 1 };
27
+ if ((inc > 0 && first > last) || (inc < 0 && first < last))
28
+ return { stdout: "", exitCode: 0 };
29
+ const results = [];
30
+ const maxSteps = 100000;
31
+ let steps = 0;
32
+ for (let n = first; inc > 0 ? n <= last : n >= last; n = Math.round((n + inc) * 1e10) / 1e10) {
33
+ if (++steps > maxSteps)
34
+ break;
35
+ let s;
36
+ if (fmt) {
37
+ s = fmt.replace("%g", String(n)).replace("%f", n.toFixed(6)).replace("%d", String(Math.trunc(n)));
38
+ }
39
+ else {
40
+ s = Number.isInteger(n) ? String(n) : n.toPrecision(12).replace(/\.?0+$/, "");
41
+ }
42
+ if (width) {
43
+ const maxLen = String(Math.trunc(last)).length;
44
+ s = s.padStart(maxLen, "0");
45
+ }
46
+ results.push(s);
47
+ }
48
+ return { stdout: `${results.join(sep)}\n`, exitCode: 0 };
49
+ },
50
+ };
@@ -1,9 +1,3 @@
1
1
  import type { ShellModule } from "../types/commands";
2
- /**
3
- * Execute shell scripts or commands with a minimal shell interpreter.
4
- * Supports if/elif/else, for loops, while loops, and variable expansion.
5
- * @category shell
6
- * @params ["-c <script>", "[<file>]"]
7
- */
8
2
  export declare const shCommand: ShellModule;
9
3
  //# sourceMappingURL=sh.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sh.d.ts","sourceRoot":"","sources":["../../src/commands/sh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGX,WAAW,EACX,MAAM,mBAAmB,CAAC;AA4R3B;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,WA4CvB,CAAC"}
1
+ {"version":3,"file":"sh.d.ts","sourceRoot":"","sources":["../../src/commands/sh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGX,WAAW,EACX,MAAM,mBAAmB,CAAC;AAqa3B,eAAO,MAAM,SAAS,EAAE,WAsCvB,CAAC"}
@@ -19,6 +19,38 @@ function parseBlocks(lines) {
19
19
  i++;
20
20
  continue;
21
21
  }
22
+ // Function definition: name() { or function name { or name() { body }
23
+ const funcMatchInline = line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{(.+)\}\s*$/);
24
+ const funcMatch = funcMatchInline ?? (line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{?\s*$/) ||
25
+ line.match(/^function\s+(\w+)\s*\{?\s*$/));
26
+ if (funcMatch) {
27
+ const funcName = funcMatch[1];
28
+ const body = [];
29
+ // Inline: name() { cmd; } — single-line form
30
+ if (funcMatchInline) {
31
+ body.push(...funcMatchInline[2].split(";").map((s) => s.trim()).filter(Boolean));
32
+ blocks.push({ type: "func", name: funcName, body });
33
+ i++;
34
+ continue;
35
+ }
36
+ i++;
37
+ while (i < lines.length && lines[i]?.trim() !== "}" && i < lines.length + 1) {
38
+ const l = lines[i].trim().replace(/^do\s+/, "");
39
+ if (l && l !== "{")
40
+ body.push(l);
41
+ i++;
42
+ }
43
+ i++; // skip closing }
44
+ blocks.push({ type: "func", name: funcName, body });
45
+ continue;
46
+ }
47
+ // (( expr )) arithmetic statement
48
+ const arithMatch = line.match(/^\(\(\s*(.+?)\s*\)\)$/);
49
+ if (arithMatch) {
50
+ blocks.push({ type: "arith", expr: arithMatch[1] });
51
+ i++;
52
+ continue;
53
+ }
22
54
  if (line.startsWith("if ") || line === "if") {
23
55
  const cond = line
24
56
  .replace(/^if\s+/, "")
@@ -174,7 +206,26 @@ async function runBlocks(blocks, ctx) {
174
206
  continue;
175
207
  }
176
208
  }
177
- const r = await runCommand(expanded, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
209
+ const r = await (async () => {
210
+ // Check if expanded matches a registered function
211
+ const cmdName = expanded.trim().split(/\s+/)[0] ?? "";
212
+ const funcBody = ctx.env.vars[`__func_${cmdName}`];
213
+ if (funcBody) {
214
+ // Set positional params $1 $2 ... from remaining args
215
+ const funcArgs = expanded.trim().split(/\s+/).slice(1);
216
+ const savedVars = { ...ctx.env.vars };
217
+ funcArgs.forEach((a, i) => { ctx.env.vars[String(i + 1)] = a; });
218
+ ctx.env.vars["0"] = cmdName;
219
+ const funcLines = funcBody.split("\n");
220
+ const funcResult = await runBlocks(parseBlocks(funcLines), ctx);
221
+ // Restore positional params
222
+ for (let pi = 1; pi <= funcArgs.length; pi++)
223
+ delete ctx.env.vars[String(pi)];
224
+ Object.assign(ctx.env.vars, { ...savedVars, ...ctx.env.vars });
225
+ return funcResult;
226
+ }
227
+ return runCommand(expanded, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
228
+ })();
178
229
  ctx.env.lastExitCode = r.exitCode ?? 0;
179
230
  if (r.stdout)
180
231
  output += `${r.stdout}\n`;
@@ -207,9 +258,35 @@ async function runBlocks(blocks, ctx) {
207
258
  }
208
259
  }
209
260
  }
261
+ else if (block.type === "func") {
262
+ // Register function in env vars as __func_<name>=<body>
263
+ ctx.env.vars[`__func_${block.name}`] = block.body.join("\n");
264
+ }
265
+ else if (block.type === "arith") {
266
+ // (( expr )) — evaluate arithmetic, update vars
267
+ const { expandSync } = await import("../utils/expand");
268
+ const expr = expandSync(block.expr, ctx.env.vars, ctx.env.lastExitCode);
269
+ // Handle i++ / i-- / i+=N / i-=N
270
+ const incMatch = expr.match(/^(\w+)\s*(\+\+|--)$/);
271
+ if (incMatch) {
272
+ const val = parseInt(ctx.env.vars[incMatch[1]] ?? "0", 10);
273
+ ctx.env.vars[incMatch[1]] = String(incMatch[2] === "++" ? val + 1 : val - 1);
274
+ }
275
+ else {
276
+ const assignMatch = expr.match(/^(\w+)\s*([+\-*/])=\s*(.+)$/);
277
+ if (assignMatch) {
278
+ const lhs = parseInt(ctx.env.vars[assignMatch[1]] ?? "0", 10);
279
+ const rhs = parseInt(assignMatch[3], 10);
280
+ const ops = { "+": lhs + rhs, "-": lhs - rhs, "*": lhs * rhs, "/": Math.floor(lhs / rhs) };
281
+ ctx.env.vars[assignMatch[1]] = String(ops[assignMatch[2]] ?? lhs);
282
+ }
283
+ }
284
+ }
210
285
  else if (block.type === "for") {
211
286
  const listExpanded = await expandVars(block.list, ctx.env.vars, ctx.env.lastExitCode, ctx);
212
- const items = listExpanded.trim().split(/\s+/);
287
+ // Apply brace expansion to each token in the list
288
+ const { expandBraces } = await import("../utils/expand");
289
+ const items = listExpanded.trim().split(/\s+/).flatMap(expandBraces);
213
290
  for (const item of items) {
214
291
  ctx.env.vars[block.var] = item;
215
292
  const sub = await runBlocks(parseBlocks(block.body), ctx);
@@ -239,6 +316,78 @@ async function runBlocks(blocks, ctx) {
239
316
  * @category shell
240
317
  * @params ["-c <script>", "[<file>]"]
241
318
  */
319
+ /**
320
+ * Split a sh script into logical lines, respecting:
321
+ * - `{...}` braces (function bodies)
322
+ * - Newlines and semicolons at depth 0 only
323
+ */
324
+ function splitShScript(script) {
325
+ const lines = [];
326
+ let current = "";
327
+ let depth = 0;
328
+ let inSingleQ = false;
329
+ let inDoubleQ = false;
330
+ let i = 0;
331
+ while (i < script.length) {
332
+ const ch = script[i];
333
+ if (!inSingleQ && !inDoubleQ) {
334
+ if (ch === "'") {
335
+ inSingleQ = true;
336
+ current += ch;
337
+ i++;
338
+ continue;
339
+ }
340
+ if (ch === '"') {
341
+ inDoubleQ = true;
342
+ current += ch;
343
+ i++;
344
+ continue;
345
+ }
346
+ if (ch === "{") {
347
+ depth++;
348
+ current += ch;
349
+ i++;
350
+ continue;
351
+ }
352
+ if (ch === "}") {
353
+ depth--;
354
+ current += ch;
355
+ i++;
356
+ // At depth 0, closing } ends the function body line
357
+ if (depth === 0) {
358
+ const t = current.trim();
359
+ if (t)
360
+ lines.push(t);
361
+ current = "";
362
+ // Skip trailing ; or whitespace
363
+ while (i < script.length && (script[i] === ";" || script[i] === " "))
364
+ i++;
365
+ }
366
+ continue;
367
+ }
368
+ if (depth === 0 && (ch === ";" || ch === "\n")) {
369
+ const t = current.trim();
370
+ if (t && !t.startsWith("#"))
371
+ lines.push(t);
372
+ current = "";
373
+ i++;
374
+ continue;
375
+ }
376
+ }
377
+ else if (inSingleQ && ch === "'") {
378
+ inSingleQ = false;
379
+ }
380
+ else if (inDoubleQ && ch === '"') {
381
+ inDoubleQ = false;
382
+ }
383
+ current += ch;
384
+ i++;
385
+ }
386
+ const t = current.trim();
387
+ if (t && !t.startsWith("#"))
388
+ lines.push(t);
389
+ return lines;
390
+ }
242
391
  export const shCommand = {
243
392
  name: "sh",
244
393
  aliases: ["bash"],
@@ -252,10 +401,7 @@ export const shCommand = {
252
401
  const script = args[args.indexOf("-c") + 1] ?? "";
253
402
  if (!script)
254
403
  return { stderr: "sh: -c requires a script", exitCode: 1 };
255
- const lines = script
256
- .split(/[;\n]/)
257
- .map((l) => l.trim())
258
- .filter((l) => l && !l.startsWith("#"));
404
+ const lines = splitShScript(script);
259
405
  const blocks = parseBlocks(lines);
260
406
  return runBlocks(blocks, ctx);
261
407
  }
@@ -269,10 +415,7 @@ export const shCommand = {
269
415
  exitCode: 1,
270
416
  };
271
417
  const content = shell.vfs.readFile(p);
272
- const lines = content
273
- .split("\n")
274
- .map((l) => l.trim())
275
- .filter((l) => l && !l.startsWith("#"));
418
+ const lines = splitShScript(content);
276
419
  const blocks = parseBlocks(lines);
277
420
  return runBlocks(blocks, ctx);
278
421
  }
@@ -6,8 +6,8 @@
6
6
  * Called once during VirtualShell initialization. Idempotent — skips
7
7
  * paths that already exist so FS-mode snapshots survive restarts.
8
8
  */
9
- import type { ShellProperties } from "../VirtualShell";
10
9
  import type VirtualFileSystem from "../VirtualFileSystem";
10
+ import type { ShellProperties } from "../VirtualShell";
11
11
  import type { VirtualUserManager } from "../VirtualUserManager";
12
12
  /**
13
13
  * Sync `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the
@@ -1 +1 @@
1
- {"version":3,"file":"linuxRootfs.d.ts","sourceRoot":"","sources":["../../src/modules/linuxRootfs.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAgHhE;;;;;GAKG;AACH,wBAAgB,aAAa,CAC5B,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,GACvB,IAAI,CAsCN;AAgED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAC1B,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,oBAAoB,EAAE,GAC/D,IAAI,CAmJN;AAuND;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,EACzB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,eAAe,EACtB,cAAc,EAAE,MAAM,GACpB,IAAI,CAYN"}
1
+ {"version":3,"file":"linuxRootfs.d.ts","sourceRoot":"","sources":["../../src/modules/linuxRootfs.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE;;;;;;GAMG;AAGH,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAgHhE;;;;;GAKG;AACH,wBAAgB,aAAa,CAC5B,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,GACvB,IAAI,CAsCN;AAgED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAC1B,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,oBAAoB,EAAE,GAC/D,IAAI,CAmJN;AAuND;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,EACzB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,eAAe,EACtB,cAAc,EAAE,MAAM,GACpB,IAAI,CAYN"}
@@ -394,18 +394,18 @@ function bootstrapTmp(vfs) {
394
394
  // ─── /root ────────────────────────────────────────────────────────────────────
395
395
  function bootstrapRoot(vfs) {
396
396
  ensureDir(vfs, "/root", 0o700);
397
- ensureFile(vfs, "/root/.bashrc", `${[
397
+ ensureFile(vfs, "/home/root/.bashrc", `${[
398
398
  "# root .bashrc",
399
399
  "export PS1='\\[\\033[0;31m\\]\\u@\\h\\[\\033[0m\\]:\\[\\033[0;34m\\]\\w\\[\\033[0m\\]# '",
400
400
  "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
401
401
  "alias ll='ls -la'",
402
402
  "alias la='ls -A'",
403
403
  ].join("\n")}\n`);
404
- ensureFile(vfs, "/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
404
+ ensureFile(vfs, "/home/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
405
405
  // Fix: /home/root should map to /root for root user
406
- if (!vfs.exists("/home/root")) {
407
- vfs.symlink("/root", "/home/root");
408
- }
406
+ // if (!vfs.exists("/home/root")) {
407
+ // vfs.symlink("/root", "/home/root");
408
+ // }
409
409
  }
410
410
  // ─── /opt + /srv + /mnt + /media ─────────────────────────────────────────────
411
411
  function bootstrapMisc(vfs) {