typescript-virtual-container 1.2.8 → 1.3.0
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.
- package/.vscode/settings.json +0 -1
- package/README.md +462 -44
- package/biome.json +7 -0
- package/dist/SSHMimic/exec.d.ts.map +1 -1
- package/dist/SSHMimic/executor.d.ts.map +1 -1
- package/dist/SSHMimic/executor.js +35 -21
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +20 -6
- package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -1
- package/dist/VirtualFileSystem/binaryPack.js +29 -6
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +36 -13
- package/dist/VirtualPackageManager/index.d.ts +202 -0
- package/dist/VirtualPackageManager/index.d.ts.map +1 -0
- package/dist/VirtualPackageManager/index.js +825 -0
- package/dist/VirtualShell/index.d.ts +93 -12
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +95 -13
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +3 -1
- package/dist/VirtualShell/shellParser.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.d.ts +52 -20
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +54 -20
- package/dist/commands/adduser.d.ts +6 -0
- package/dist/commands/adduser.d.ts.map +1 -1
- package/dist/commands/adduser.js +6 -0
- package/dist/commands/alias.d.ts +9 -0
- package/dist/commands/alias.d.ts.map +1 -0
- package/dist/commands/alias.js +63 -0
- package/dist/commands/apt.d.ts +9 -0
- package/dist/commands/apt.d.ts.map +1 -0
- package/dist/commands/apt.js +205 -0
- package/dist/commands/awk.d.ts +11 -0
- package/dist/commands/awk.d.ts.map +1 -1
- package/dist/commands/awk.js +15 -2
- package/dist/commands/base64.d.ts +5 -0
- package/dist/commands/base64.d.ts.map +1 -1
- package/dist/commands/base64.js +9 -1
- package/dist/commands/cat.d.ts +5 -0
- package/dist/commands/cat.d.ts.map +1 -1
- package/dist/commands/cat.js +35 -8
- package/dist/commands/cd.d.ts +5 -0
- package/dist/commands/cd.d.ts.map +1 -1
- package/dist/commands/cd.js +5 -0
- package/dist/commands/chmod.d.ts +5 -0
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +57 -3
- package/dist/commands/command-helpers.d.ts +78 -4
- package/dist/commands/command-helpers.d.ts.map +1 -1
- package/dist/commands/command-helpers.js +78 -4
- package/dist/commands/cp.d.ts +5 -0
- package/dist/commands/cp.d.ts.map +1 -1
- package/dist/commands/cp.js +5 -0
- package/dist/commands/curl.d.ts +5 -0
- package/dist/commands/curl.d.ts.map +1 -1
- package/dist/commands/curl.js +106 -26
- package/dist/commands/cut.d.ts +5 -0
- package/dist/commands/cut.d.ts.map +1 -1
- package/dist/commands/cut.js +8 -1
- package/dist/commands/date.d.ts +5 -0
- package/dist/commands/date.d.ts.map +1 -1
- package/dist/commands/date.js +7 -1
- package/dist/commands/declare.d.ts +3 -0
- package/dist/commands/declare.d.ts.map +1 -0
- package/dist/commands/declare.js +39 -0
- package/dist/commands/diff.d.ts +5 -0
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +5 -0
- package/dist/commands/dpkg.d.ts +9 -0
- package/dist/commands/dpkg.d.ts.map +1 -0
- package/dist/commands/dpkg.js +161 -0
- package/dist/commands/du.d.ts.map +1 -1
- package/dist/commands/du.js +8 -2
- package/dist/commands/echo.d.ts +5 -0
- package/dist/commands/echo.d.ts.map +1 -1
- package/dist/commands/echo.js +33 -12
- package/dist/commands/env.d.ts +5 -0
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +11 -1
- package/dist/commands/exit.d.ts +5 -0
- package/dist/commands/exit.d.ts.map +1 -1
- package/dist/commands/exit.js +12 -2
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +3 -1
- package/dist/commands/find.d.ts +5 -0
- package/dist/commands/find.d.ts.map +1 -1
- package/dist/commands/find.js +5 -0
- package/dist/commands/free.d.ts +8 -0
- package/dist/commands/free.d.ts.map +1 -0
- package/dist/commands/free.js +43 -0
- package/dist/commands/grep.d.ts +5 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +12 -2
- package/dist/commands/gzip.d.ts +5 -0
- package/dist/commands/gzip.d.ts.map +1 -1
- package/dist/commands/gzip.js +18 -2
- package/dist/commands/head.d.ts +5 -0
- package/dist/commands/head.d.ts.map +1 -1
- package/dist/commands/head.js +5 -0
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +98 -45
- package/dist/commands/helpers.d.ts +3 -0
- package/dist/commands/helpers.d.ts.map +1 -1
- package/dist/commands/helpers.js +3 -0
- package/dist/commands/history.d.ts +8 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +26 -0
- package/dist/commands/hostname.d.ts +5 -0
- package/dist/commands/hostname.d.ts.map +1 -1
- package/dist/commands/hostname.js +5 -0
- package/dist/commands/id.d.ts.map +1 -1
- package/dist/commands/id.js +4 -1
- package/dist/commands/index.d.ts +2 -10
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -231
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +6 -3
- package/dist/commands/lsb-release.d.ts +3 -0
- package/dist/commands/lsb-release.d.ts.map +1 -0
- package/dist/commands/lsb-release.js +56 -0
- package/dist/commands/man.d.ts +3 -0
- package/dist/commands/man.d.ts.map +1 -0
- package/dist/commands/man.js +155 -0
- package/dist/commands/nano.js +1 -1
- package/dist/commands/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +6 -1
- package/dist/commands/node.d.ts +9 -0
- package/dist/commands/node.d.ts.map +1 -0
- package/dist/commands/node.js +316 -0
- package/dist/commands/npm.d.ts +19 -0
- package/dist/commands/npm.d.ts.map +1 -0
- package/dist/commands/npm.js +109 -0
- package/dist/commands/ping.d.ts.map +1 -1
- package/dist/commands/ping.js +7 -2
- package/dist/commands/printf.d.ts +3 -0
- package/dist/commands/printf.d.ts.map +1 -0
- package/dist/commands/printf.js +113 -0
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +30 -6
- package/dist/commands/python.d.ts +30 -0
- package/dist/commands/python.d.ts.map +1 -0
- package/dist/commands/python.js +2058 -0
- package/dist/commands/read.d.ts +3 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +34 -0
- package/dist/commands/registry.d.ts +8 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +229 -0
- package/dist/commands/runtime.d.ts +6 -0
- package/dist/commands/runtime.d.ts.map +1 -0
- package/dist/commands/runtime.js +280 -0
- package/dist/commands/sed.d.ts.map +1 -1
- package/dist/commands/sed.js +11 -3
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +9 -3
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +69 -30
- package/dist/commands/shift.d.ts +5 -0
- package/dist/commands/shift.d.ts.map +1 -0
- package/dist/commands/shift.js +52 -0
- package/dist/commands/sleep.d.ts.map +1 -1
- package/dist/commands/sort.d.ts.map +1 -1
- package/dist/commands/sort.js +4 -2
- package/dist/commands/source.d.ts +3 -0
- package/dist/commands/source.d.ts.map +1 -0
- package/dist/commands/source.js +34 -0
- package/dist/commands/sudo.js +1 -1
- package/dist/commands/tar.d.ts.map +1 -1
- package/dist/commands/tar.js +11 -3
- package/dist/commands/tee.d.ts.map +1 -1
- package/dist/commands/tee.js +8 -6
- package/dist/commands/test.d.ts +3 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +114 -0
- package/dist/commands/tr.d.ts.map +1 -1
- package/dist/commands/tr.js +3 -1
- package/dist/commands/true.d.ts +4 -0
- package/dist/commands/true.d.ts.map +1 -0
- package/dist/commands/true.js +14 -0
- package/dist/commands/type.d.ts +3 -0
- package/dist/commands/type.d.ts.map +1 -0
- package/dist/commands/type.js +34 -0
- package/dist/commands/uname.d.ts.map +1 -1
- package/dist/commands/uname.js +4 -1
- package/dist/commands/uniq.d.ts.map +1 -1
- package/dist/commands/uptime.d.ts +3 -0
- package/dist/commands/uptime.d.ts.map +1 -0
- package/dist/commands/uptime.js +43 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +92 -96
- package/dist/commands/which.d.ts +3 -0
- package/dist/commands/which.d.ts.map +1 -0
- package/dist/commands/which.js +32 -0
- package/dist/commands/xargs.d.ts.map +1 -1
- package/dist/commands/xargs.js +1 -1
- package/dist/index.d.ts +15 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/modules/linuxRootfs.d.ts +41 -0
- package/dist/modules/linuxRootfs.d.ts.map +1 -0
- package/dist/modules/linuxRootfs.js +440 -0
- package/dist/modules/neofetch.d.ts.map +1 -1
- package/dist/modules/neofetch.js +1 -0
- package/dist/standalone-wo-sftp.d.ts +2 -0
- package/dist/standalone-wo-sftp.d.ts.map +1 -0
- package/dist/standalone-wo-sftp.js +30 -0
- package/dist/utils/expand.d.ts +50 -0
- package/dist/utils/expand.d.ts.map +1 -0
- package/dist/utils/expand.js +183 -0
- package/dist/utils/vfsDiff.d.ts +90 -0
- package/dist/utils/vfsDiff.d.ts.map +1 -0
- package/dist/utils/vfsDiff.js +177 -0
- package/package.json +3 -1
- package/src/SSHMimic/exec.ts +10 -1
- package/src/SSHMimic/executor.ts +105 -21
- package/src/SSHMimic/index.ts +49 -15
- package/src/VirtualFileSystem/binaryPack.ts +35 -8
- package/src/VirtualFileSystem/index.ts +78 -28
- package/src/VirtualPackageManager/index.ts +979 -0
- package/src/VirtualShell/index.ts +133 -14
- package/src/VirtualShell/shell.ts +23 -3
- package/src/VirtualShell/shellParser.ts +134 -36
- package/src/VirtualUserManager/index.ts +62 -22
- package/src/commands/adduser.ts +6 -0
- package/src/commands/alias.ts +64 -0
- package/src/commands/apt.ts +228 -0
- package/src/commands/awk.ts +20 -6
- package/src/commands/base64.ts +13 -2
- package/src/commands/cat.ts +40 -8
- package/src/commands/cd.ts +5 -0
- package/src/commands/chmod.ts +53 -3
- package/src/commands/command-helpers.ts +78 -4
- package/src/commands/cp.ts +5 -0
- package/src/commands/curl.ts +118 -33
- package/src/commands/cut.ts +8 -1
- package/src/commands/date.ts +7 -1
- package/src/commands/declare.ts +44 -0
- package/src/commands/diff.ts +17 -3
- package/src/commands/dpkg.ts +180 -0
- package/src/commands/du.ts +17 -5
- package/src/commands/echo.ts +41 -12
- package/src/commands/env.ts +11 -1
- package/src/commands/exit.ts +12 -2
- package/src/commands/export.ts +3 -1
- package/src/commands/find.ts +5 -0
- package/src/commands/free.ts +47 -0
- package/src/commands/grep.ts +12 -2
- package/src/commands/gzip.ts +28 -4
- package/src/commands/head.ts +5 -0
- package/src/commands/help.ts +121 -47
- package/src/commands/helpers.ts +8 -0
- package/src/commands/history.ts +34 -0
- package/src/commands/hostname.ts +5 -0
- package/src/commands/id.ts +4 -1
- package/src/commands/index.ts +9 -255
- package/src/commands/ls.ts +6 -3
- package/src/commands/lsb-release.ts +58 -0
- package/src/commands/man.ts +166 -0
- package/src/commands/nano.ts +1 -1
- package/src/commands/neofetch.ts +6 -1
- package/src/commands/node.ts +341 -0
- package/src/commands/npm.ts +132 -0
- package/src/commands/ping.ts +10 -3
- package/src/commands/printf.ts +112 -0
- package/src/commands/ps.ts +40 -6
- package/src/commands/python.ts +2229 -0
- package/src/commands/read.ts +41 -0
- package/src/commands/registry.ts +244 -0
- package/src/commands/runtime.ts +353 -0
- package/src/commands/sed.ts +27 -9
- package/src/commands/set.ts +9 -3
- package/src/commands/sh.ts +170 -44
- package/src/commands/shift.ts +53 -0
- package/src/commands/sleep.ts +2 -1
- package/src/commands/sort.ts +10 -6
- package/src/commands/source.ts +47 -0
- package/src/commands/sudo.ts +1 -1
- package/src/commands/tar.ts +28 -7
- package/src/commands/tee.ts +7 -1
- package/src/commands/test.ts +135 -0
- package/src/commands/tr.ts +3 -1
- package/src/commands/true.ts +17 -0
- package/src/commands/type.ts +43 -0
- package/src/commands/uname.ts +5 -1
- package/src/commands/uniq.ts +8 -2
- package/src/commands/uptime.ts +49 -0
- package/src/commands/wget.ts +105 -119
- package/src/commands/which.ts +37 -0
- package/src/commands/xargs.ts +11 -2
- package/src/index.ts +27 -18
- package/src/modules/linuxRootfs.ts +642 -0
- package/src/modules/neofetch.ts +1 -0
- package/src/standalone-wo-sftp.ts +38 -0
- package/src/utils/expand.ts +238 -0
- package/src/utils/vfsDiff.ts +275 -0
- package/standalone-wo-sftp.js +507 -0
- package/standalone-wo-sftp.js.map +7 -0
- package/standalone.js +486 -109
- package/standalone.js.map +4 -4
- package/tests/bun-test-shim.ts +9 -1
- package/tests/command-helpers.test.ts +1 -5
- package/tests/new-features.test.ts +1036 -0
- package/tests/parser-executor.test.ts +27 -27
- package/tests/sftp.test.ts +122 -42
- package/tests/users.test.ts +23 -5
- package/CHANGELOG.md +0 -150
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* expand.ts
|
|
3
|
+
*
|
|
4
|
+
* Centralised shell variable and expression expansion.
|
|
5
|
+
* Used by `runCommand` (index.ts), `echo`, and `sh.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Handles (in order):
|
|
8
|
+
* ~ tilde to $HOME
|
|
9
|
+
* $? last exit code
|
|
10
|
+
* $$ mock PID
|
|
11
|
+
* $# argument count (0 outside scripts)
|
|
12
|
+
* ${#VAR} string length
|
|
13
|
+
* ${VAR:-def} default if unset/empty
|
|
14
|
+
* ${VAR:=def} assign default if unset/empty
|
|
15
|
+
* ${VAR:+val} alternate value if set
|
|
16
|
+
* ${VAR} simple braced reference
|
|
17
|
+
* $VAR simple reference
|
|
18
|
+
* $((expr)) arithmetic (integer)
|
|
19
|
+
*/
|
|
20
|
+
// ─── arithmetic evaluator ────────────────────────────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* Evaluate a simple integer arithmetic expression.
|
|
23
|
+
* Supports: + - * / % ** unary- ( )
|
|
24
|
+
* Variables are resolved from `env` before evaluation.
|
|
25
|
+
* Returns NaN on syntax error.
|
|
26
|
+
*/
|
|
27
|
+
export function evalArith(expr, env) {
|
|
28
|
+
// Substitute variable names before evaluating
|
|
29
|
+
const substituted = expr.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g, (_, name) => {
|
|
30
|
+
const val = env[name];
|
|
31
|
+
return val !== undefined && val !== "" ? val : "0";
|
|
32
|
+
});
|
|
33
|
+
// Whitelist: only digits, operators, spaces, parens
|
|
34
|
+
if (!/^[\d\s+\-*/%()^!&|<>=,. ]+$/.test(substituted))
|
|
35
|
+
return NaN;
|
|
36
|
+
try {
|
|
37
|
+
// Use Function constructor for safe subset (no identifiers remain)
|
|
38
|
+
// eslint-disable-next-line no-new-func
|
|
39
|
+
const result = Function(`"use strict"; return (${substituted.replace(/\*\*/g, "**")});`)();
|
|
40
|
+
return typeof result === "number" ? Math.trunc(result) : NaN;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return NaN;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ─── synchronous expansion ───────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Apply a replacer only to portions of `input` that are NOT inside single quotes.
|
|
49
|
+
* Single-quoted content is passed through verbatim (POSIX sh behaviour).
|
|
50
|
+
*/
|
|
51
|
+
function outsideSingleQuotes(input, replacer) {
|
|
52
|
+
const parts = [];
|
|
53
|
+
let i = 0;
|
|
54
|
+
while (i < input.length) {
|
|
55
|
+
const sqIdx = input.indexOf("'", i);
|
|
56
|
+
if (sqIdx === -1) {
|
|
57
|
+
// No more single quotes — expand the rest
|
|
58
|
+
parts.push(replacer(input.slice(i)));
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
// Expand the part before the single quote
|
|
62
|
+
parts.push(replacer(input.slice(i, sqIdx)));
|
|
63
|
+
// Find closing single quote — everything inside is literal
|
|
64
|
+
const closeIdx = input.indexOf("'", sqIdx + 1);
|
|
65
|
+
if (closeIdx === -1) {
|
|
66
|
+
// Unclosed quote — treat rest as literal
|
|
67
|
+
parts.push(input.slice(sqIdx));
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
parts.push(input.slice(sqIdx, closeIdx + 1)); // include quotes
|
|
71
|
+
i = closeIdx + 1;
|
|
72
|
+
}
|
|
73
|
+
return parts.join("");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Expand all shell variable and expression forms synchronously.
|
|
77
|
+
* Does NOT handle `$(cmd)` — that requires async; see `expandAsync`.
|
|
78
|
+
* Content inside single quotes is left verbatim per POSIX sh rules.
|
|
79
|
+
*
|
|
80
|
+
* @param input Raw string possibly containing `$VAR`, `${...}`, `$((...))`.
|
|
81
|
+
* @param env Current session env vars.
|
|
82
|
+
* @param lastExit Last command exit code (for `$?`).
|
|
83
|
+
* @param home Home directory path (for `~`).
|
|
84
|
+
*/
|
|
85
|
+
export function expandSync(input, env, lastExit = 0, home) {
|
|
86
|
+
const homePath = home ?? env.HOME ?? "/home/user";
|
|
87
|
+
return outsideSingleQuotes(input, (chunk) => {
|
|
88
|
+
let s = chunk;
|
|
89
|
+
// Tilde expansion — only at start of token or after `:` or whitespace
|
|
90
|
+
s = s.replace(/(^|[\s:])~(\/|$)/g, (_, pre, post) => `${pre}${homePath}${post}`);
|
|
91
|
+
// $? $$ $#
|
|
92
|
+
s = s.replace(/\$\?/g, String(lastExit));
|
|
93
|
+
s = s.replace(/\$\$/g, "1");
|
|
94
|
+
s = s.replace(/\$#/g, "0");
|
|
95
|
+
// $(( arithmetic )) — must come before ${ and $VAR to avoid conflicts
|
|
96
|
+
s = s.replace(/\$\(\(([^)]+(?:\([^)]*\)[^)]*)*)\)\)/g, (_, expr) => {
|
|
97
|
+
const result = evalArith(expr, env);
|
|
98
|
+
return Number.isNaN(result) ? "0" : String(result);
|
|
99
|
+
});
|
|
100
|
+
// ${#VAR} — string length
|
|
101
|
+
s = s.replace(/\$\{#([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => String((env[name] ?? "").length));
|
|
102
|
+
// ${VAR:-default}
|
|
103
|
+
s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):-([^}]*)\}/g, (_, name, def) => env[name] !== undefined && env[name] !== "" ? env[name] : def);
|
|
104
|
+
// ${VAR:=default} — also assigns to env
|
|
105
|
+
s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):=([^}]*)\}/g, (_, name, def) => {
|
|
106
|
+
if (env[name] === undefined || env[name] === "")
|
|
107
|
+
env[name] = def;
|
|
108
|
+
return env[name];
|
|
109
|
+
});
|
|
110
|
+
// ${VAR:+alternate}
|
|
111
|
+
s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):\+([^}]*)\}/g, (_, name, alt) => env[name] !== undefined && env[name] !== "" ? alt : "");
|
|
112
|
+
// ${VAR}
|
|
113
|
+
s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => env[name] ?? "");
|
|
114
|
+
// $VAR
|
|
115
|
+
s = s.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) => env[name] ?? "");
|
|
116
|
+
return s;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// ─── async expansion (includes $(cmd)) ──────────────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* Expand all shell forms including `$(cmd)` command substitution.
|
|
122
|
+
*
|
|
123
|
+
* Processes `$(...)` blocks depth-first, respecting single-quote boundaries.
|
|
124
|
+
* Then delegates to `expandSync` for the remaining forms.
|
|
125
|
+
*
|
|
126
|
+
* @param input Raw string.
|
|
127
|
+
* @param env Current session env vars.
|
|
128
|
+
* @param lastExit Last exit code.
|
|
129
|
+
* @param runCmd Async callback to execute a command and return its stdout.
|
|
130
|
+
*/
|
|
131
|
+
export async function expandAsync(input, env, lastExit, runCmd) {
|
|
132
|
+
// $(cmd) substitution — skip content inside single quotes
|
|
133
|
+
if (input.includes("$(")) {
|
|
134
|
+
let result = "";
|
|
135
|
+
let inSingle = false;
|
|
136
|
+
let i = 0;
|
|
137
|
+
while (i < input.length) {
|
|
138
|
+
const ch = input[i];
|
|
139
|
+
if (ch === "'" && !inSingle) {
|
|
140
|
+
inSingle = true;
|
|
141
|
+
result += ch;
|
|
142
|
+
i++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ch === "'" && inSingle) {
|
|
146
|
+
inSingle = false;
|
|
147
|
+
result += ch;
|
|
148
|
+
i++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!inSingle && ch === "$" && input[i + 1] === "(") {
|
|
152
|
+
// $((expr)) arithmetic — NOT a $(cmd) substitution, skip it
|
|
153
|
+
if (input[i + 2] === "(") {
|
|
154
|
+
result += ch;
|
|
155
|
+
i++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// Find matching ) with depth tracking
|
|
159
|
+
let depth = 0;
|
|
160
|
+
let j = i + 1;
|
|
161
|
+
while (j < input.length) {
|
|
162
|
+
if (input[j] === "(")
|
|
163
|
+
depth++;
|
|
164
|
+
else if (input[j] === ")") {
|
|
165
|
+
depth--;
|
|
166
|
+
if (depth === 0)
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
j++;
|
|
170
|
+
}
|
|
171
|
+
const sub = input.slice(i + 2, j).trim();
|
|
172
|
+
const out = (await runCmd(sub)).replace(/\n$/, "");
|
|
173
|
+
result += out;
|
|
174
|
+
i = j + 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
result += ch;
|
|
178
|
+
i++;
|
|
179
|
+
}
|
|
180
|
+
input = result;
|
|
181
|
+
}
|
|
182
|
+
return expandSync(input, env, lastExit);
|
|
183
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vfsDiff.ts
|
|
3
|
+
*
|
|
4
|
+
* Snapshot diff tooling for `VirtualFileSystem`.
|
|
5
|
+
*
|
|
6
|
+
* Compares two VFS snapshots and returns structured diff results suitable
|
|
7
|
+
* for test assertions, audit logging, and deployment verification.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { diffSnapshots, formatDiff } from "typescript-virtual-container/utils/vfsDiff";
|
|
12
|
+
*
|
|
13
|
+
* const before = shell.vfs.toSnapshot();
|
|
14
|
+
* await client.exec("npm install && mkdir -p /app");
|
|
15
|
+
* const after = shell.vfs.toSnapshot();
|
|
16
|
+
*
|
|
17
|
+
* const diff = diffSnapshots(before, after);
|
|
18
|
+
* console.log(formatDiff(diff));
|
|
19
|
+
*
|
|
20
|
+
* // Test assertions
|
|
21
|
+
* expect(diff.added).toContain("/app");
|
|
22
|
+
* expect(diff.modified).toContain("/etc/hosts");
|
|
23
|
+
* expect(diff.removed).not.toContain("/tmp/needed-file");
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
import type { VfsSnapshot } from "../types/vfs";
|
|
27
|
+
/** A single changed file entry in a diff result. */
|
|
28
|
+
export interface VfsDiffEntry {
|
|
29
|
+
/** Absolute VFS path of the changed node. */
|
|
30
|
+
path: string;
|
|
31
|
+
/** Node type — `"file"` or `"directory"`. */
|
|
32
|
+
type: "file" | "directory";
|
|
33
|
+
}
|
|
34
|
+
/** A modified file entry — includes before/after content for files. */
|
|
35
|
+
export interface VfsDiffModified extends VfsDiffEntry {
|
|
36
|
+
type: "file";
|
|
37
|
+
/** Content before the change (decoded from base64). */
|
|
38
|
+
before: string;
|
|
39
|
+
/** Content after the change (decoded from base64). */
|
|
40
|
+
after: string;
|
|
41
|
+
}
|
|
42
|
+
/** Full result of a snapshot diff operation. */
|
|
43
|
+
export interface VfsDiff {
|
|
44
|
+
/** Paths present in `after` but not in `before`. */
|
|
45
|
+
added: VfsDiffEntry[];
|
|
46
|
+
/** Paths present in `before` but not in `after`. */
|
|
47
|
+
removed: VfsDiffEntry[];
|
|
48
|
+
/** Files whose content or mode changed between snapshots. */
|
|
49
|
+
modified: VfsDiffModified[];
|
|
50
|
+
/** True when there are no differences. */
|
|
51
|
+
clean: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Compute the diff between two VFS snapshots.
|
|
55
|
+
*
|
|
56
|
+
* @param before Snapshot taken before the operation.
|
|
57
|
+
* @param after Snapshot taken after the operation.
|
|
58
|
+
* @param options Optional filtering options.
|
|
59
|
+
* @returns A structured `VfsDiff` result.
|
|
60
|
+
*/
|
|
61
|
+
export declare function diffSnapshots(before: VfsSnapshot, after: VfsSnapshot, options?: {
|
|
62
|
+
/** Glob-style path prefixes to ignore (e.g. `["/proc", "/var/log"]`). */
|
|
63
|
+
ignore?: string[];
|
|
64
|
+
}): VfsDiff;
|
|
65
|
+
/**
|
|
66
|
+
* Format a `VfsDiff` as a human-readable string similar to `git diff --stat`.
|
|
67
|
+
*
|
|
68
|
+
* @param diff Result from `diffSnapshots`.
|
|
69
|
+
* @param options Formatting options.
|
|
70
|
+
*/
|
|
71
|
+
export declare function formatDiff(diff: VfsDiff, options?: {
|
|
72
|
+
/** Show file content changes inline. Default: false. */
|
|
73
|
+
showContent?: boolean;
|
|
74
|
+
/** Max chars of content to show per change. Default: 120. */
|
|
75
|
+
maxContentChars?: number;
|
|
76
|
+
}): string;
|
|
77
|
+
/**
|
|
78
|
+
* Assert that a diff contains specific paths, throwing on mismatch.
|
|
79
|
+
* Designed for use in test suites.
|
|
80
|
+
*
|
|
81
|
+
* @param diff Result from `diffSnapshots`.
|
|
82
|
+
* @param expect Expected paths in each category.
|
|
83
|
+
* @throws When any expectation is not met.
|
|
84
|
+
*/
|
|
85
|
+
export declare function assertDiff(diff: VfsDiff, expect: {
|
|
86
|
+
added?: string[];
|
|
87
|
+
removed?: string[];
|
|
88
|
+
modified?: string[];
|
|
89
|
+
}): void;
|
|
90
|
+
//# sourceMappingURL=vfsDiff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vfsDiff.d.ts","sourceRoot":"","sources":["../../src/utils/vfsDiff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EACX,WAAW,EAIX,MAAM,cAAc,CAAC;AAItB,oDAAoD;AACpD,MAAM,WAAW,YAAY;IAC5B,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC3B;AAED,uEAAuE;AACvE,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;CACd;AAED,gDAAgD;AAChD,MAAM,WAAW,OAAO;IACvB,oDAAoD;IACpD,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,oDAAoD;IACpD,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,6DAA6D;IAC7D,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,0CAA0C;IAC1C,KAAK,EAAE,OAAO,CAAC;CACf;AA4BD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,WAAW,EAClB,OAAO,GAAE;IACR,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACb,GACJ,OAAO,CAsET;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACzB,IAAI,EAAE,OAAO,EACb,OAAO,GAAE;IACR,wDAAwD;IACxD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAC;CACpB,GACJ,MAAM,CAsCR;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACzB,IAAI,EAAE,OAAO,EACb,MAAM,EAAE;IACP,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB,GACC,IAAI,CA4BN"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vfsDiff.ts
|
|
3
|
+
*
|
|
4
|
+
* Snapshot diff tooling for `VirtualFileSystem`.
|
|
5
|
+
*
|
|
6
|
+
* Compares two VFS snapshots and returns structured diff results suitable
|
|
7
|
+
* for test assertions, audit logging, and deployment verification.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { diffSnapshots, formatDiff } from "typescript-virtual-container/utils/vfsDiff";
|
|
12
|
+
*
|
|
13
|
+
* const before = shell.vfs.toSnapshot();
|
|
14
|
+
* await client.exec("npm install && mkdir -p /app");
|
|
15
|
+
* const after = shell.vfs.toSnapshot();
|
|
16
|
+
*
|
|
17
|
+
* const diff = diffSnapshots(before, after);
|
|
18
|
+
* console.log(formatDiff(diff));
|
|
19
|
+
*
|
|
20
|
+
* // Test assertions
|
|
21
|
+
* expect(diff.added).toContain("/app");
|
|
22
|
+
* expect(diff.modified).toContain("/etc/hosts");
|
|
23
|
+
* expect(diff.removed).not.toContain("/tmp/needed-file");
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
// ─── internal helpers ─────────────────────────────────────────────────────────
|
|
27
|
+
function flattenSnapshot(node, prefix, out) {
|
|
28
|
+
const path = prefix === "" ? "/" : prefix;
|
|
29
|
+
out.set(path, node);
|
|
30
|
+
if (node.type === "directory") {
|
|
31
|
+
for (const child of node.children ?? []) {
|
|
32
|
+
flattenSnapshot(child, `${prefix}/${child.name}`, out);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function decodeContent(node) {
|
|
37
|
+
try {
|
|
38
|
+
return Buffer.from(node.contentBase64, "base64").toString("utf8");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return node.contentBase64;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ─── public API ───────────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Compute the diff between two VFS snapshots.
|
|
47
|
+
*
|
|
48
|
+
* @param before Snapshot taken before the operation.
|
|
49
|
+
* @param after Snapshot taken after the operation.
|
|
50
|
+
* @param options Optional filtering options.
|
|
51
|
+
* @returns A structured `VfsDiff` result.
|
|
52
|
+
*/
|
|
53
|
+
export function diffSnapshots(before, after, options = {}) {
|
|
54
|
+
const ignorePrefixes = options.ignore ?? [];
|
|
55
|
+
const shouldIgnore = (path) => ignorePrefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
|
56
|
+
const beforeMap = new Map();
|
|
57
|
+
const afterMap = new Map();
|
|
58
|
+
flattenSnapshot(before.root, "", beforeMap);
|
|
59
|
+
flattenSnapshot(after.root, "", afterMap);
|
|
60
|
+
const added = [];
|
|
61
|
+
const removed = [];
|
|
62
|
+
const modified = [];
|
|
63
|
+
// Added — in after, not in before
|
|
64
|
+
for (const [path, node] of afterMap) {
|
|
65
|
+
if (shouldIgnore(path))
|
|
66
|
+
continue;
|
|
67
|
+
if (!beforeMap.has(path)) {
|
|
68
|
+
added.push({ path, type: node.type });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Removed — in before, not in after
|
|
72
|
+
for (const [path, node] of beforeMap) {
|
|
73
|
+
if (shouldIgnore(path))
|
|
74
|
+
continue;
|
|
75
|
+
if (!afterMap.has(path)) {
|
|
76
|
+
removed.push({ path, type: node.type });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Modified — in both, but content or mode changed
|
|
80
|
+
for (const [path, afterNode] of afterMap) {
|
|
81
|
+
if (shouldIgnore(path))
|
|
82
|
+
continue;
|
|
83
|
+
const beforeNode = beforeMap.get(path);
|
|
84
|
+
if (!beforeNode)
|
|
85
|
+
continue; // already in added
|
|
86
|
+
if (afterNode.type !== beforeNode.type)
|
|
87
|
+
continue; // type change = add+remove
|
|
88
|
+
if (afterNode.type === "file" && beforeNode.type === "file") {
|
|
89
|
+
const beforeContent = decodeContent(beforeNode);
|
|
90
|
+
const afterContent = decodeContent(afterNode);
|
|
91
|
+
const modeChanged = afterNode.mode !== beforeNode.mode;
|
|
92
|
+
if (beforeContent !== afterContent || modeChanged) {
|
|
93
|
+
modified.push({
|
|
94
|
+
path,
|
|
95
|
+
type: "file",
|
|
96
|
+
before: beforeContent,
|
|
97
|
+
after: afterContent,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Sort all arrays for determinism
|
|
103
|
+
const sortByPath = (a, b) => a.path.localeCompare(b.path);
|
|
104
|
+
added.sort(sortByPath);
|
|
105
|
+
removed.sort(sortByPath);
|
|
106
|
+
modified.sort(sortByPath);
|
|
107
|
+
return {
|
|
108
|
+
added,
|
|
109
|
+
removed,
|
|
110
|
+
modified,
|
|
111
|
+
clean: added.length === 0 && removed.length === 0 && modified.length === 0,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Format a `VfsDiff` as a human-readable string similar to `git diff --stat`.
|
|
116
|
+
*
|
|
117
|
+
* @param diff Result from `diffSnapshots`.
|
|
118
|
+
* @param options Formatting options.
|
|
119
|
+
*/
|
|
120
|
+
export function formatDiff(diff, options = {}) {
|
|
121
|
+
if (diff.clean)
|
|
122
|
+
return "(no changes)";
|
|
123
|
+
const { showContent = false, maxContentChars = 120 } = options;
|
|
124
|
+
const lines = [];
|
|
125
|
+
for (const entry of diff.added) {
|
|
126
|
+
lines.push(`+ ${entry.path} [${entry.type}]`);
|
|
127
|
+
}
|
|
128
|
+
for (const entry of diff.removed) {
|
|
129
|
+
lines.push(`- ${entry.path} [${entry.type}]`);
|
|
130
|
+
}
|
|
131
|
+
for (const entry of diff.modified) {
|
|
132
|
+
lines.push(`~ ${entry.path} [modified]`);
|
|
133
|
+
if (showContent) {
|
|
134
|
+
const before = entry.before.slice(0, maxContentChars);
|
|
135
|
+
const after = entry.after.slice(0, maxContentChars);
|
|
136
|
+
lines.push(` before: ${JSON.stringify(before)}${entry.before.length > maxContentChars ? "…" : ""}`);
|
|
137
|
+
lines.push(` after: ${JSON.stringify(after)}${entry.after.length > maxContentChars ? "…" : ""}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const summary = [
|
|
141
|
+
diff.added.length > 0 ? `${diff.added.length} added` : "",
|
|
142
|
+
diff.removed.length > 0 ? `${diff.removed.length} removed` : "",
|
|
143
|
+
diff.modified.length > 0 ? `${diff.modified.length} modified` : "",
|
|
144
|
+
]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join(", ");
|
|
147
|
+
lines.push(`\n${summary}`);
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Assert that a diff contains specific paths, throwing on mismatch.
|
|
152
|
+
* Designed for use in test suites.
|
|
153
|
+
*
|
|
154
|
+
* @param diff Result from `diffSnapshots`.
|
|
155
|
+
* @param expect Expected paths in each category.
|
|
156
|
+
* @throws When any expectation is not met.
|
|
157
|
+
*/
|
|
158
|
+
export function assertDiff(diff, expect) {
|
|
159
|
+
const addedPaths = diff.added.map((e) => e.path);
|
|
160
|
+
const removedPaths = diff.removed.map((e) => e.path);
|
|
161
|
+
const modifiedPaths = diff.modified.map((e) => e.path);
|
|
162
|
+
for (const path of expect.added ?? []) {
|
|
163
|
+
if (!addedPaths.includes(path)) {
|
|
164
|
+
throw new Error(`assertDiff: expected "${path}" to be added, but it was not.\nAdded: ${JSON.stringify(addedPaths)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const path of expect.removed ?? []) {
|
|
168
|
+
if (!removedPaths.includes(path)) {
|
|
169
|
+
throw new Error(`assertDiff: expected "${path}" to be removed, but it was not.\nRemoved: ${JSON.stringify(removedPaths)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const path of expect.modified ?? []) {
|
|
173
|
+
if (!modifiedPaths.includes(path)) {
|
|
174
|
+
throw new Error(`assertDiff: expected "${path}" to be modified, but it was not.\nModified: ${JSON.stringify(modifiedPaths)}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
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.
|
|
7
|
+
"version": "1.3.0",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -25,9 +25,11 @@
|
|
|
25
25
|
"lint": "bunx --bun @biomejs/biome lint ./src",
|
|
26
26
|
"lint:write": "bunx --bun @biomejs/biome lint --write ./src",
|
|
27
27
|
"test": "bunx --bun @biomejs/biome test ./src",
|
|
28
|
+
"test-battery": "bun test tests/",
|
|
28
29
|
"build": "tsc --project tsconfig.json",
|
|
29
30
|
"deploy:npm": "npm publish --access public",
|
|
30
31
|
"bench": "rm -rf .benchmark-shells/ && bun benchmark-virtualshell.ts",
|
|
32
|
+
"standalone-build:wo-sftp": "bunx esbuild src/standalone-wo-sftp.ts --bundle --platform=node --target=node18 --outfile=standalone-wo-sftp.js --tree-shaking=true --minify --sourcemap",
|
|
31
33
|
"publish-package": "bash ./scripts/publish-package.sh",
|
|
32
34
|
"standalone-build": "bunx esbuild src/standalone.ts --bundle --platform=node --target=node18 --outfile=standalone.js --tree-shaking=true --minify --sourcemap"
|
|
33
35
|
},
|
package/src/SSHMimic/exec.ts
CHANGED
|
@@ -17,7 +17,16 @@ export function runExec(
|
|
|
17
17
|
shell: VirtualShell,
|
|
18
18
|
): void {
|
|
19
19
|
Promise.resolve(
|
|
20
|
-
runCommand(
|
|
20
|
+
runCommand(
|
|
21
|
+
cmd,
|
|
22
|
+
authUser,
|
|
23
|
+
hostname,
|
|
24
|
+
"exec",
|
|
25
|
+
`/home/${authUser}`,
|
|
26
|
+
shell,
|
|
27
|
+
undefined,
|
|
28
|
+
makeDefaultEnv(authUser, hostname),
|
|
29
|
+
),
|
|
21
30
|
)
|
|
22
31
|
.then((result) => {
|
|
23
32
|
if (result.stdout) {
|