typescript-virtual-container 1.5.6 → 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 (43) hide show
  1. package/README.md +28 -20
  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/exit.js +1 -1
  23. package/dist/commands/find.js +1 -4
  24. package/dist/commands/helpers.d.ts +0 -20
  25. package/dist/commands/helpers.js +0 -97
  26. package/dist/commands/perl.js +1 -1
  27. package/dist/commands/python.js +5 -2
  28. package/dist/commands/registry.js +6 -1
  29. package/dist/commands/runtime.js +65 -87
  30. package/dist/commands/strace.js +1 -1
  31. package/dist/commands/tar.js +2 -2
  32. package/dist/commands/test.js +2 -2
  33. package/dist/modules/linuxRootfs.js +1 -4
  34. package/dist/modules/neofetch.js +2 -2
  35. package/dist/types/commands.d.ts +4 -0
  36. package/dist/utils/argv.d.ts +6 -0
  37. package/dist/utils/argv.js +32 -0
  38. package/dist/utils/expand.d.ts +5 -2
  39. package/dist/utils/expand.js +70 -67
  40. package/dist/utils/glob.d.ts +6 -0
  41. package/dist/utils/glob.js +34 -0
  42. package/dist/utils/tokenize.js +13 -13
  43. package/package.json +7 -6
@@ -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;
@@ -413,7 +437,7 @@ export function expandSync(input, env, lastExit = 0, home) {
413
437
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\/\/([^/}]*)\/([^}]*)\}/g, (_, name, pat, rep) => {
414
438
  const val = env[name] ?? "";
415
439
  try {
416
- return val.replace(new RegExp(pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, "."), "g"), rep);
440
+ return val.replace(shellPatToRegex(pat, "none", true, true), rep);
417
441
  }
418
442
  catch {
419
443
  return val;
@@ -423,36 +447,20 @@ export function expandSync(input, env, lastExit = 0, home) {
423
447
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\/([^/}]*)\/([^}]*)\}/g, (_, name, pat, rep) => {
424
448
  const val = env[name] ?? "";
425
449
  try {
426
- return val.replace(new RegExp(pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")), rep);
450
+ return val.replace(shellPatToRegex(pat, "none", true, false), rep);
427
451
  }
428
452
  catch {
429
453
  return val;
430
454
  }
431
455
  });
432
456
  // ${VAR##pattern} — strip longest prefix
433
- s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)##([^}]+)\}/g, (_, name, pat) => {
434
- const val = env[name] ?? "";
435
- const re = new RegExp(`^${pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}`);
436
- return val.replace(re, "");
437
- });
457
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)##([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "prefix", true), ""));
438
458
  // ${VAR#pattern} — strip shortest prefix
439
- s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)#([^}]+)\}/g, (_, name, pat) => {
440
- const val = env[name] ?? "";
441
- const re = new RegExp(`^${pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, ".")}`);
442
- return val.replace(re, "");
443
- });
459
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)#([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "prefix", false), ""));
444
460
  // ${VAR%%pattern} — strip longest suffix
445
- s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%%([^}]+)\}/g, (_, name, pat) => {
446
- const val = env[name] ?? "";
447
- const re = new RegExp(`${pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
448
- return val.replace(re, "");
449
- });
461
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%%([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "suffix", true), ""));
450
462
  // ${VAR%pattern} — strip shortest suffix
451
- s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%([^}]+)\}/g, (_, name, pat) => {
452
- const val = env[name] ?? "";
453
- const re = new RegExp(`${pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, ".")}$`);
454
- return val.replace(re, "");
455
- });
463
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "suffix", false), ""));
456
464
  // ${VAR}
457
465
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => env[name] ?? "");
458
466
  // $VAR and positional params $1 $2 ...
@@ -540,12 +548,16 @@ export async function expandAsync(input, env, lastExit, runCmd) {
540
548
  env[depthKey] = String(currentDepth);
541
549
  }
542
550
  }
543
- // ─── Glob expansion ──────────────────────────────────────────────────────────
544
- /**
545
- * Expand a glob pattern against a VirtualShell VFS.
546
- * Supports * (any chars in segment) and ** (any path).
547
- * Returns the original pattern if no matches found (bash behavior).
548
- */
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
+ }
549
561
  export function expandGlob(pattern, cwd, vfs) {
550
562
  // No glob chars → return as-is
551
563
  if (!pattern.includes('*') && !pattern.includes('?'))
@@ -567,14 +579,14 @@ function matchGlob(dir, segments, vfs) {
567
579
  // ** matches zero or more path segments
568
580
  if (seg === '**') {
569
581
  const all = walkAll(dir, vfs);
570
- return rest.length === 0 ? all : all.flatMap(d => {
571
- try {
572
- if (vfs.stat(d).type === 'directory')
573
- return matchGlob(d, rest, vfs);
574
- }
575
- catch { }
576
- return [];
577
- });
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;
578
590
  }
579
591
  let entries = [];
580
592
  try {
@@ -584,20 +596,20 @@ function matchGlob(dir, segments, vfs) {
584
596
  return [];
585
597
  }
586
598
  const re = globToRegex(seg);
587
- return entries
588
- .filter(e => !e.startsWith('.') || seg.startsWith('.'))
589
- .filter(e => re.test(e))
590
- .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;
591
604
  const full = dir === '/' ? `/${e}` : `${dir}/${e}`;
592
- if (rest.length === 0)
593
- return [full];
594
- try {
595
- if (vfs.stat(full).type === 'directory')
596
- return matchGlob(full, rest, vfs);
605
+ if (rest.length === 0) {
606
+ matched.push(full);
607
+ continue;
597
608
  }
598
- catch { }
599
- return [];
600
- });
609
+ if (nodeType(vfs, full) === 'directory')
610
+ matched.push(...matchGlob(full, rest, vfs));
611
+ }
612
+ return matched;
601
613
  }
602
614
  function walkAll(dir, vfs) {
603
615
  const results = [dir];
@@ -610,17 +622,8 @@ function walkAll(dir, vfs) {
610
622
  }
611
623
  for (const e of entries) {
612
624
  const full = dir === '/' ? `/${e}` : `${dir}/${e}`;
613
- try {
614
- if (vfs.stat(full).type === 'directory')
615
- results.push(...walkAll(full, vfs));
616
- }
617
- catch { }
625
+ if (nodeType(vfs, full) === 'directory')
626
+ results.push(...walkAll(full, vfs));
618
627
  }
619
628
  return results;
620
629
  }
621
- function globToRegex(pattern) {
622
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
623
- .replace(/\*/g, '.*')
624
- .replace(/\?/g, '.');
625
- return new RegExp(`^${escaped}$`);
626
- }
@@ -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.6",
7
+ "version": "1.5.7",
8
8
  "files": [
9
9
  "dist/",
10
10
  "README.md",
@@ -33,11 +33,11 @@
33
33
  "test": "bun run test-salve",
34
34
  "test-battery": "bun test tests/",
35
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",
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,8 +45,9 @@
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",
49
- "publish-doc": "bunx typedoc && bun build-all && cd examples && node build && cd .. && bunx gh-pages -d docs && git add docs && git commit -m 'docs: update documentation' && git push",
48
+ "build-all": "bun run build && node scripts/build-all.mjs && cd examples && node build && cd .. && cp examples/app.js docs/app.js",
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": {