typescript-virtual-container 1.5.5 → 1.5.7

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 (64) hide show
  1. package/README.md +117 -35
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/SSHMimic/index.d.ts +5 -1
  4. package/dist/SSHMimic/index.js +27 -3
  5. package/dist/SSHMimic/scp.d.ts +34 -0
  6. package/dist/SSHMimic/scp.js +285 -0
  7. package/dist/SSHMimic/sftp.d.ts +53 -3
  8. package/dist/SSHMimic/sftp.js +9 -3
  9. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
  10. package/dist/VirtualFileSystem/binaryPack.js +37 -1
  11. package/dist/VirtualFileSystem/index.d.ts +7 -0
  12. package/dist/VirtualFileSystem/index.js +67 -27
  13. package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
  14. package/dist/VirtualFileSystem/path.d.ts +5 -0
  15. package/dist/VirtualFileSystem/path.js +24 -11
  16. package/dist/VirtualPackageManager/index.d.ts +4 -2
  17. package/dist/VirtualPackageManager/index.js +24 -4
  18. package/dist/VirtualShell/index.d.ts +4 -0
  19. package/dist/VirtualShell/index.js +1 -7
  20. package/dist/VirtualShell/shell.js +40 -10
  21. package/dist/VirtualShell/shellParser.js +1 -22
  22. package/dist/commands/awk.d.ts +6 -11
  23. package/dist/commands/awk.js +462 -109
  24. package/dist/commands/bzip2.d.ts +11 -0
  25. package/dist/commands/bzip2.js +91 -0
  26. package/dist/commands/exit.js +1 -1
  27. package/dist/commands/find.d.ts +2 -2
  28. package/dist/commands/find.js +209 -37
  29. package/dist/commands/helpers.d.ts +0 -20
  30. package/dist/commands/helpers.js +0 -97
  31. package/dist/commands/lsof.d.ts +6 -0
  32. package/dist/commands/lsof.js +30 -0
  33. package/dist/commands/perl.d.ts +6 -0
  34. package/dist/commands/perl.js +76 -0
  35. package/dist/commands/python.js +5 -2
  36. package/dist/commands/registry.js +19 -1
  37. package/dist/commands/runtime.js +65 -87
  38. package/dist/commands/sed.d.ts +2 -2
  39. package/dist/commands/sed.js +216 -34
  40. package/dist/commands/sh.js +42 -0
  41. package/dist/commands/strace.d.ts +6 -0
  42. package/dist/commands/strace.js +26 -0
  43. package/dist/commands/tar.d.ts +2 -1
  44. package/dist/commands/tar.js +138 -52
  45. package/dist/commands/test.js +2 -2
  46. package/dist/commands/zip.d.ts +11 -0
  47. package/dist/commands/zip.js +232 -0
  48. package/dist/modules/linuxRootfs.js +1 -4
  49. package/dist/modules/neofetch.js +2 -2
  50. package/dist/types/commands.d.ts +4 -0
  51. package/dist/utils/argv.d.ts +6 -0
  52. package/dist/utils/argv.js +32 -0
  53. package/dist/utils/expand.d.ts +5 -2
  54. package/dist/utils/expand.js +112 -45
  55. package/dist/utils/glob.d.ts +6 -0
  56. package/dist/utils/glob.js +34 -0
  57. package/dist/utils/tokenize.js +13 -13
  58. package/package.json +9 -7
  59. package/dist/self-standalone.d.ts +0 -1
  60. package/dist/self-standalone.js +0 -444
  61. package/dist/standalone-wo-sftp.d.ts +0 -1
  62. package/dist/standalone-wo-sftp.js +0 -30
  63. package/dist/standalone.d.ts +0 -1
  64. package/dist/standalone.js +0 -61
@@ -17,6 +17,24 @@
17
17
  * $VAR simple reference
18
18
  * $((expr)) arithmetic (integer)
19
19
  */
20
+ import { globToRegex } from "./glob";
21
+ // Memoized shell-pattern → RegExp for ${VAR//pat/rep} etc. forms.
22
+ // Key encodes anchor/greedy options to keep separate caches per form.
23
+ const _shellPatCache = new Map();
24
+ function shellPatToRegex(pat, anchor, greedy, global = false) {
25
+ const key = `${anchor}:${greedy ? "g" : "s"}:${global ? "G" : ""}:${pat}`;
26
+ let re = _shellPatCache.get(key);
27
+ if (re)
28
+ return re;
29
+ const esc = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
30
+ const body = greedy
31
+ ? esc.replace(/\*/g, ".*").replace(/\?/g, ".")
32
+ : esc.replace(/\*/g, "[^/]*").replace(/\?/g, ".");
33
+ const src = anchor === "prefix" ? `^${body}` : anchor === "suffix" ? `${body}$` : body;
34
+ re = new RegExp(src, global ? "g" : "");
35
+ _shellPatCache.set(key, re);
36
+ return re;
37
+ }
20
38
  function tokenizeArith(expr, env) {
21
39
  const tokens = [];
22
40
  let i = 0;
@@ -193,25 +211,24 @@ export function evalArith(expr, env) {
193
211
  * Single-quoted content is passed through verbatim (POSIX sh behaviour).
194
212
  */
195
213
  function outsideSingleQuotes(input, replacer) {
214
+ // Fast path: no single quotes → apply replacer to whole string, no allocation
215
+ if (!input.includes("'"))
216
+ return replacer(input);
196
217
  const parts = [];
197
218
  let i = 0;
198
219
  while (i < input.length) {
199
220
  const sqIdx = input.indexOf("'", i);
200
221
  if (sqIdx === -1) {
201
- // No more single quotes — expand the rest
202
222
  parts.push(replacer(input.slice(i)));
203
223
  break;
204
224
  }
205
- // Expand the part before the single quote
206
225
  parts.push(replacer(input.slice(i, sqIdx)));
207
- // Find closing single quote — everything inside is literal
208
226
  const closeIdx = input.indexOf("'", sqIdx + 1);
209
227
  if (closeIdx === -1) {
210
- // Unclosed quote — treat rest as literal
211
228
  parts.push(input.slice(sqIdx));
212
229
  break;
213
230
  }
214
- parts.push(input.slice(sqIdx, closeIdx + 1)); // include quotes
231
+ parts.push(input.slice(sqIdx, closeIdx + 1));
215
232
  i = closeIdx + 1;
216
233
  }
217
234
  return parts.join("");
@@ -327,10 +344,14 @@ export function expandBraces(token) {
327
344
  return expandBracesInternal(token, 0);
328
345
  }
329
346
  function expandArithmeticChunks(input, env) {
347
+ if (!input.includes("$(("))
348
+ return input;
330
349
  let result = "";
331
350
  let index = 0;
351
+ let flush = 0;
332
352
  while (index < input.length) {
333
353
  if (input[index] === "$" && input[index + 1] === "(" && input[index + 2] === "(") {
354
+ result += input.slice(flush, index);
334
355
  let scan = index + 3;
335
356
  let depth = 0;
336
357
  while (scan < input.length) {
@@ -347,6 +368,7 @@ function expandArithmeticChunks(input, env) {
347
368
  const value = evalArith(expr, env);
348
369
  result += Number.isNaN(value) ? "0" : String(value);
349
370
  index = scan + 2;
371
+ flush = index;
350
372
  break;
351
373
  }
352
374
  }
@@ -354,16 +376,18 @@ function expandArithmeticChunks(input, env) {
354
376
  }
355
377
  if (scan >= input.length) {
356
378
  result += input.slice(index);
357
- break;
379
+ return result;
358
380
  }
359
381
  continue;
360
382
  }
361
- result += input[index];
362
383
  index++;
363
384
  }
364
- return result;
385
+ return result + input.slice(flush);
365
386
  }
366
387
  export function expandSync(input, env, lastExit = 0, home) {
388
+ // Fast path: nothing to expand (no $ and no ~ and no single quotes)
389
+ if (!input.includes("$") && !input.includes("~") && !input.includes("'"))
390
+ return input;
367
391
  const homePath = home ?? env.HOME ?? "/home/user";
368
392
  return outsideSingleQuotes(input, (chunk) => {
369
393
  let s = chunk;
@@ -377,6 +401,19 @@ export function expandSync(input, env, lastExit = 0, home) {
377
401
  s = s.replace(/\$LINENO\b/g, "1");
378
402
  // $(( arithmetic )) — must come before ${ and $VAR to avoid conflicts
379
403
  s = expandArithmeticChunks(s, env);
404
+ // ${arr[@]} and ${arr[*]} — all array elements
405
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)[@*]\}/g, (_, name) => env[name] ?? "");
406
+ // ${arr[N]} — single array element
407
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\[(\d+)\]\}/g, (_, name, idx) => env[`${name}[${idx}]`] ?? "");
408
+ // ${#arr[@]} — array length
409
+ s = s.replace(/\$\{#([A-Za-z_][A-Za-z0-9_]*)[@*]\}/g, (_, name) => {
410
+ let count = 0;
411
+ for (const k of Object.keys(env)) {
412
+ if (k.startsWith(`${name}[`))
413
+ count++;
414
+ }
415
+ return String(count);
416
+ });
380
417
  // ${#VAR} — string length
381
418
  s = s.replace(/\$\{#([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => String((env[name] ?? "").length));
382
419
  // ${VAR:-default}
@@ -389,6 +426,41 @@ export function expandSync(input, env, lastExit = 0, home) {
389
426
  });
390
427
  // ${VAR:+alternate}
391
428
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):\+([^}]*)\}/g, (_, name, alt) => env[name] !== undefined && env[name] !== "" ? alt : "");
429
+ // ${VAR:offset:len} and ${VAR:offset}
430
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):(-?\d+)(?::(\d+))?\}/g, (_, name, offset, len) => {
431
+ const val = env[name] ?? "";
432
+ const off = parseInt(offset, 10);
433
+ const start = off < 0 ? Math.max(0, val.length + off) : Math.min(off, val.length);
434
+ return len !== undefined ? val.slice(start, start + parseInt(len, 10)) : val.slice(start);
435
+ });
436
+ // ${VAR//pattern/replace} — replace all
437
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\/\/([^/}]*)\/([^}]*)\}/g, (_, name, pat, rep) => {
438
+ const val = env[name] ?? "";
439
+ try {
440
+ return val.replace(shellPatToRegex(pat, "none", true, true), rep);
441
+ }
442
+ catch {
443
+ return val;
444
+ }
445
+ });
446
+ // ${VAR/pattern/replace} — replace first
447
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\/([^/}]*)\/([^}]*)\}/g, (_, name, pat, rep) => {
448
+ const val = env[name] ?? "";
449
+ try {
450
+ return val.replace(shellPatToRegex(pat, "none", true, false), rep);
451
+ }
452
+ catch {
453
+ return val;
454
+ }
455
+ });
456
+ // ${VAR##pattern} — strip longest prefix
457
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)##([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "prefix", true), ""));
458
+ // ${VAR#pattern} — strip shortest prefix
459
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)#([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "prefix", false), ""));
460
+ // ${VAR%%pattern} — strip longest suffix
461
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%%([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "suffix", true), ""));
462
+ // ${VAR%pattern} — strip shortest suffix
463
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "suffix", false), ""));
392
464
  // ${VAR}
393
465
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => env[name] ?? "");
394
466
  // $VAR and positional params $1 $2 ...
@@ -476,12 +548,16 @@ export async function expandAsync(input, env, lastExit, runCmd) {
476
548
  env[depthKey] = String(currentDepth);
477
549
  }
478
550
  }
479
- // ─── Glob expansion ──────────────────────────────────────────────────────────
480
- /**
481
- * Expand a glob pattern against a VirtualShell VFS.
482
- * Supports * (any chars in segment) and ** (any path).
483
- * Returns the original pattern if no matches found (bash behavior).
484
- */
551
+ function nodeType(vfs, p) {
552
+ if (vfs.statType)
553
+ return vfs.statType(p);
554
+ try {
555
+ return vfs.stat(p).type;
556
+ }
557
+ catch {
558
+ return null;
559
+ }
560
+ }
485
561
  export function expandGlob(pattern, cwd, vfs) {
486
562
  // No glob chars → return as-is
487
563
  if (!pattern.includes('*') && !pattern.includes('?'))
@@ -503,14 +579,14 @@ function matchGlob(dir, segments, vfs) {
503
579
  // ** matches zero or more path segments
504
580
  if (seg === '**') {
505
581
  const all = walkAll(dir, vfs);
506
- return rest.length === 0 ? all : all.flatMap(d => {
507
- try {
508
- if (vfs.stat(d).type === 'directory')
509
- return matchGlob(d, rest, vfs);
510
- }
511
- catch { }
512
- return [];
513
- });
582
+ if (rest.length === 0)
583
+ return all;
584
+ const out = [];
585
+ for (const d of all) {
586
+ if (nodeType(vfs, d) === 'directory')
587
+ out.push(...matchGlob(d, rest, vfs));
588
+ }
589
+ return out;
514
590
  }
515
591
  let entries = [];
516
592
  try {
@@ -520,20 +596,20 @@ function matchGlob(dir, segments, vfs) {
520
596
  return [];
521
597
  }
522
598
  const re = globToRegex(seg);
523
- return entries
524
- .filter(e => !e.startsWith('.') || seg.startsWith('.'))
525
- .filter(e => re.test(e))
526
- .flatMap(e => {
599
+ const showHidden = seg.startsWith('.');
600
+ const matched = [];
601
+ for (const e of entries) {
602
+ if ((!showHidden && e.startsWith('.')) || !re.test(e))
603
+ continue;
527
604
  const full = dir === '/' ? `/${e}` : `${dir}/${e}`;
528
- if (rest.length === 0)
529
- return [full];
530
- try {
531
- if (vfs.stat(full).type === 'directory')
532
- return matchGlob(full, rest, vfs);
605
+ if (rest.length === 0) {
606
+ matched.push(full);
607
+ continue;
533
608
  }
534
- catch { }
535
- return [];
536
- });
609
+ if (nodeType(vfs, full) === 'directory')
610
+ matched.push(...matchGlob(full, rest, vfs));
611
+ }
612
+ return matched;
537
613
  }
538
614
  function walkAll(dir, vfs) {
539
615
  const results = [dir];
@@ -546,17 +622,8 @@ function walkAll(dir, vfs) {
546
622
  }
547
623
  for (const e of entries) {
548
624
  const full = dir === '/' ? `/${e}` : `${dir}/${e}`;
549
- try {
550
- if (vfs.stat(full).type === 'directory')
551
- results.push(...walkAll(full, vfs));
552
- }
553
- catch { }
625
+ if (nodeType(vfs, full) === 'directory')
626
+ results.push(...walkAll(full, vfs));
554
627
  }
555
628
  return results;
556
629
  }
557
- function globToRegex(pattern) {
558
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
559
- .replace(/\*/g, '.*')
560
- .replace(/\?/g, '.');
561
- return new RegExp(`^${escaped}$`);
562
- }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Convert a shell glob pattern to a RegExp.
3
+ * Supports: * (any chars), ? (one char), [...] (char class), flags (e.g. "i").
4
+ * Results are memoized — same pattern+flags returns the cached instance.
5
+ */
6
+ export declare function globToRegex(pattern: string, flags?: string): RegExp;
@@ -0,0 +1,34 @@
1
+ const _globCache = new Map();
2
+ /**
3
+ * Convert a shell glob pattern to a RegExp.
4
+ * Supports: * (any chars), ? (one char), [...] (char class), flags (e.g. "i").
5
+ * Results are memoized — same pattern+flags returns the cached instance.
6
+ */
7
+ export function globToRegex(pattern, flags = "") {
8
+ const key = `${flags}:${pattern}`;
9
+ const cached = _globCache.get(key);
10
+ if (cached)
11
+ return cached;
12
+ let re = "^";
13
+ for (let i = 0; i < pattern.length; i++) {
14
+ const c = pattern[i];
15
+ if (c === "*")
16
+ re += ".*";
17
+ else if (c === "?")
18
+ re += ".";
19
+ else if (c === "[") {
20
+ const close = pattern.indexOf("]", i + 1);
21
+ if (close === -1)
22
+ re += "\\[";
23
+ else {
24
+ re += `[${pattern.slice(i + 1, close)}]`;
25
+ i = close;
26
+ }
27
+ }
28
+ else
29
+ re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
30
+ }
31
+ const result = new RegExp(`${re}$`, flags);
32
+ _globCache.set(key, result);
33
+ return result;
34
+ }
@@ -51,9 +51,11 @@ export function tokenizeCommand(input) {
51
51
  continue;
52
52
  }
53
53
  // Handle 2>&1, 2>>, 2>, >&, >>
54
- if (!inQ && ch === "2" && (next === ">")) {
55
- const rest = input.slice(i + 1);
56
- if (rest.startsWith(">>&1") || rest.startsWith(">> &1")) {
54
+ if (!inQ && ch === "2" && next === ">") {
55
+ const c2 = input[i + 2];
56
+ const c3 = input[i + 3];
57
+ const c4 = input[i + 4];
58
+ if (c2 === ">" && c3 === "&" && c4 === "1") {
57
59
  if (current) {
58
60
  tokens.push(current);
59
61
  current = "";
@@ -62,7 +64,7 @@ export function tokenizeCommand(input) {
62
64
  i += 5;
63
65
  continue;
64
66
  }
65
- if (rest.startsWith(">&1")) {
67
+ if (c2 === "&" && c3 === "1") {
66
68
  if (current) {
67
69
  tokens.push(current);
68
70
  current = "";
@@ -71,7 +73,7 @@ export function tokenizeCommand(input) {
71
73
  i += 4;
72
74
  continue;
73
75
  }
74
- if (rest.startsWith(">>")) {
76
+ if (c2 === ">") {
75
77
  if (current) {
76
78
  tokens.push(current);
77
79
  current = "";
@@ -80,15 +82,13 @@ export function tokenizeCommand(input) {
80
82
  i += 3;
81
83
  continue;
82
84
  }
83
- if (rest.startsWith(">")) {
84
- if (current) {
85
- tokens.push(current);
86
- current = "";
87
- }
88
- tokens.push("2>");
89
- i += 2;
90
- continue;
85
+ if (current) {
86
+ tokens.push(current);
87
+ current = "";
91
88
  }
89
+ tokens.push("2>");
90
+ i += 2;
91
+ continue;
92
92
  }
93
93
  if ((ch === ">" || ch === "<") && !inQ) {
94
94
  if (current) {
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.5",
7
+ "version": "1.5.7",
8
8
  "files": [
9
9
  "dist/",
10
10
  "README.md",
@@ -32,12 +32,12 @@
32
32
  "lint:write": "bunx --bun @biomejs/biome lint --write ./src",
33
33
  "test": "bun run test-salve",
34
34
  "test-battery": "bun test tests/",
35
- "test-salve": "for f in tests/*.test.ts; do echo \"\\n\ud83e\uddea Testing $f...\"; bun test \"$f\" --timeout 10000; sleep 0.25; done",
36
- "build": "tsc --project tsconfig.json",
35
+ "test-salve": "for f in tests/*.test.ts; do echo \"\\n🧪 Testing $f...\"; bun test \"$f\" --timeout 10000; sleep 0.25; done",
36
+ "build": "tsc --project tsconfig.json && rm -f dist/*standalone*",
37
37
  "deploy:npm": "bun publish --access public",
38
38
  "bench": "rm -rf .benchmark-shells/ && bun benchmark-virtualshell.ts",
39
- "standalone-build:wo-sftp": "bunx esbuild src/standalone-wo-sftp.ts --bundle --platform=node --target=node18 --outfile=builds/standalone-wo-sftp.js --tree-shaking=true --minify --banner:js='#!/usr/bin/env node'",
40
- "web-build": "bunx esbuild src/web.ts --bundle --platform=browser --format=esm --target=es2020 --outfile=builds/web.min.js --tree-shaking=true --minify",
39
+ "benchmark": "bun benchmark-virtualshell.ts > benchmark-results.txt",
40
+ "web-build": "bunx esbuild src/web.ts --bundle --platform=browser --format=esm --target=es2020 --outfile=builds/web.min.js --tree-shaking=true --minify",
41
41
  "web-build-iife": "bunx esbuild src/web.ts --bundle --platform=browser --format=iife --target=es2020 --outfile=builds/web-iife.min.js --tree-shaking=true --minify --global-name=WebShellLib",
42
42
  "example-build": "bun run web-build && cp builds/web.min.js examples/web.min.js",
43
43
  "example-serve": "cd examples && bun server.js",
@@ -45,14 +45,15 @@
45
45
  "publish-package": "bash ./scripts/publish-package.sh",
46
46
  "self-standalone-build": "node scripts/build-all.mjs",
47
47
  "standalone-build": "bunx esbuild src/standalone.ts --bundle --platform=node --target=node18 --outfile=builds/standalone.cjs --tree-shaking=true --minify --banner:js='#!/usr/bin/env node'",
48
- "build-all": "node scripts/build-all.mjs",
48
+ "build-all": "bun run build && node scripts/build-all.mjs && cd examples && node build && cd .. && cp examples/app.js docs/app.js",
49
49
  "publish-doc": "bunx typedoc && bun build-all && bunx gh-pages -d docs && git add docs && git commit -m 'docs: update documentation' && git push",
50
+ "publish-doc-app": "bun build-all && bunx gh-pages -d docs && git add docs/app.js && git commit -m 'docs: update web terminal app' && git push",
50
51
  "generate-manuals": "node scripts/generate-manuals-bundle.mjs"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@biomejs/biome": "^2.4.15",
54
55
  "@types/bun": "^1.3.14",
55
- "@types/node": "^25.7.0",
56
+ "@types/node": "^25.8.0",
56
57
  "@types/ssh2": "^1.15.5",
57
58
  "esbuild": "^0.28.0",
58
59
  "gh-pages": "^6.3.0",
@@ -63,6 +64,7 @@
63
64
  "typescript": "^5"
64
65
  },
65
66
  "dependencies": {
67
+ "fflate": "^0.8.2",
66
68
  "ssh2": "^1.17.0"
67
69
  }
68
70
  }
@@ -1 +0,0 @@
1
- export {};