typescript-virtual-container 1.2.7 → 1.2.9
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/README.md +457 -42
- package/dist/SSHMimic/executor.js +3 -5
- package/dist/VirtualFileSystem/binaryPack.d.ts +49 -0
- package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -0
- package/dist/VirtualFileSystem/binaryPack.js +193 -0
- package/dist/VirtualFileSystem/index.d.ts +7 -5
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +20 -9
- package/dist/VirtualPackageManager/index.d.ts +202 -0
- package/dist/VirtualPackageManager/index.d.ts.map +1 -0
- package/dist/VirtualPackageManager/index.js +676 -0
- package/dist/VirtualShell/index.d.ts +87 -12
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +83 -12
- 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/alias.d.ts +4 -0
- package/dist/commands/alias.d.ts.map +1 -0
- package/dist/commands/alias.js +58 -0
- package/dist/commands/apt.d.ts +4 -0
- package/dist/commands/apt.d.ts.map +1 -0
- package/dist/commands/apt.js +182 -0
- package/dist/commands/cat.d.ts.map +1 -1
- package/dist/commands/cat.js +27 -8
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +52 -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/curl.d.ts.map +1 -1
- package/dist/commands/curl.js +81 -29
- package/dist/commands/dpkg.d.ts +4 -0
- package/dist/commands/dpkg.d.ts.map +1 -0
- package/dist/commands/dpkg.js +144 -0
- package/dist/commands/echo.d.ts.map +1 -1
- package/dist/commands/echo.js +24 -12
- package/dist/commands/free.d.ts +3 -0
- package/dist/commands/free.d.ts.map +1 -0
- package/dist/commands/free.js +38 -0
- 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 +3 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +21 -0
- package/dist/commands/index.d.ts +8 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +120 -11
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +4 -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 +50 -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/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +5 -0
- package/dist/commands/ping.d.ts.map +1 -1
- package/dist/commands/ping.js +5 -2
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +27 -6
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +29 -11
- package/dist/commands/source.d.ts +3 -0
- package/dist/commands/source.d.ts.map +1 -0
- package/dist/commands/source.js +31 -0
- package/dist/commands/test.d.ts +3 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +92 -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/uptime.d.ts +3 -0
- package/dist/commands/uptime.d.ts.map +1 -0
- package/dist/commands/uptime.js +40 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +71 -100
- 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/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/modules/linuxRootfs.d.ts +24 -0
- package/dist/modules/linuxRootfs.d.ts.map +1 -0
- package/dist/modules/linuxRootfs.js +297 -0
- package/dist/modules/neofetch.d.ts.map +1 -1
- package/dist/modules/neofetch.js +1 -0
- package/dist/standalone.js +4 -1
- package/package.json +2 -1
- package/src/SSHMimic/executor.ts +3 -5
- package/src/VirtualFileSystem/binaryPack.ts +219 -0
- package/src/VirtualFileSystem/index.ts +21 -11
- package/src/VirtualPackageManager/index.ts +820 -0
- package/src/VirtualShell/index.ts +104 -13
- package/src/VirtualUserManager/index.ts +55 -20
- package/src/commands/alias.ts +60 -0
- package/src/commands/apt.ts +198 -0
- package/src/commands/cat.ts +32 -8
- package/src/commands/chmod.ts +48 -3
- package/src/commands/command-helpers.ts +78 -4
- package/src/commands/curl.ts +78 -37
- package/src/commands/dpkg.ts +158 -0
- package/src/commands/echo.ts +30 -14
- package/src/commands/free.ts +40 -0
- package/src/commands/helpers.ts +8 -0
- package/src/commands/history.ts +29 -0
- package/src/commands/index.ts +116 -11
- package/src/commands/ls.ts +5 -4
- package/src/commands/lsb-release.ts +52 -0
- package/src/commands/man.ts +166 -0
- package/src/commands/neofetch.ts +5 -0
- package/src/commands/ping.ts +5 -2
- package/src/commands/ps.ts +28 -6
- package/src/commands/sh.ts +33 -11
- package/src/commands/source.ts +35 -0
- package/src/commands/test.ts +100 -0
- package/src/commands/type.ts +40 -0
- package/src/commands/uptime.ts +46 -0
- package/src/commands/wget.ts +70 -123
- package/src/commands/which.ts +34 -0
- package/src/index.ts +10 -0
- package/src/modules/linuxRootfs.ts +439 -0
- package/src/modules/neofetch.ts +1 -0
- package/src/standalone.ts +4 -1
- package/standalone.js +418 -103
- package/standalone.js.map +4 -4
- package/tests/new-features.test.ts +626 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* linuxRootfs.ts
|
|
3
|
+
*
|
|
4
|
+
* Bootstraps a realistic Linux directory hierarchy in the VFS.
|
|
5
|
+
* Called once during VirtualShell initialization. Idempotent — skips
|
|
6
|
+
* paths that already exist so FS-mode snapshots survive restarts.
|
|
7
|
+
*/
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
10
|
+
function ensureDir(vfs, path, mode = 0o755) {
|
|
11
|
+
if (!vfs.exists(path))
|
|
12
|
+
vfs.mkdir(path, mode);
|
|
13
|
+
}
|
|
14
|
+
function ensureFile(vfs, path, content, mode = 0o644) {
|
|
15
|
+
if (!vfs.exists(path))
|
|
16
|
+
vfs.writeFile(path, content, { mode });
|
|
17
|
+
}
|
|
18
|
+
// ─── /etc ────────────────────────────────────────────────────────────────────
|
|
19
|
+
function bootstrapEtc(vfs, hostname, props) {
|
|
20
|
+
ensureDir(vfs, "/etc");
|
|
21
|
+
// os-release — authoritative distro identity used by neofetch, lsb_release
|
|
22
|
+
ensureFile(vfs, "/etc/os-release", `${[
|
|
23
|
+
`NAME="Fortune GNU/Linux"`,
|
|
24
|
+
`PRETTY_NAME="${props.os}"`,
|
|
25
|
+
`ID=fortune`,
|
|
26
|
+
`ID_LIKE=debian`,
|
|
27
|
+
`HOME_URL="https://github.com/itsrealfortune/typescript-virtual-container"`,
|
|
28
|
+
`VERSION_CODENAME=aurora`,
|
|
29
|
+
`VERSION_ID="1.0"`,
|
|
30
|
+
].join("\n")}\n`);
|
|
31
|
+
ensureFile(vfs, "/etc/debian_version", "12.0\n");
|
|
32
|
+
ensureFile(vfs, "/etc/hostname", `${hostname}\n`);
|
|
33
|
+
ensureFile(vfs, "/etc/shells", "/bin/sh\n/bin/bash\n/usr/bin/bash\n");
|
|
34
|
+
ensureFile(vfs, "/etc/profile", `${[
|
|
35
|
+
"export PATH=/usr/local/bin:/usr/bin:/bin",
|
|
36
|
+
"export PS1='\\u@\\h:\\w\\$ '",
|
|
37
|
+
].join("\n")}\n`);
|
|
38
|
+
ensureFile(vfs, "/etc/issue", `Fortune GNU/Linux 1.0 \\n \\l\n`);
|
|
39
|
+
ensureFile(vfs, "/etc/motd", [
|
|
40
|
+
"",
|
|
41
|
+
`Welcome to ${props.os}`,
|
|
42
|
+
`Kernel: ${props.kernel}`,
|
|
43
|
+
"",
|
|
44
|
+
].join("\n"));
|
|
45
|
+
// APT sources
|
|
46
|
+
ensureDir(vfs, "/etc/apt");
|
|
47
|
+
ensureDir(vfs, "/etc/apt/sources.list.d");
|
|
48
|
+
ensureFile(vfs, "/etc/apt/sources.list", `${[
|
|
49
|
+
"# Fortune GNU/Linux package sources",
|
|
50
|
+
"deb [virtual] fortune://packages.fortune.local aurora main contrib",
|
|
51
|
+
"deb [virtual] fortune://security.fortune.local aurora-security main",
|
|
52
|
+
].join("\n")}\n`);
|
|
53
|
+
// network stubs
|
|
54
|
+
ensureDir(vfs, "/etc/network");
|
|
55
|
+
ensureFile(vfs, "/etc/network/interfaces", `${[
|
|
56
|
+
"auto lo",
|
|
57
|
+
"iface lo inet loopback",
|
|
58
|
+
"",
|
|
59
|
+
"auto eth0",
|
|
60
|
+
"iface eth0 inet dhcp",
|
|
61
|
+
].join("\n")}\n`);
|
|
62
|
+
ensureFile(vfs, "/etc/resolv.conf", "nameserver 1.1.1.1\nnameserver 8.8.8.8\n");
|
|
63
|
+
ensureFile(vfs, "/etc/hosts", `${[
|
|
64
|
+
"127.0.0.1 localhost",
|
|
65
|
+
`127.0.1.1 ${hostname}`,
|
|
66
|
+
"::1 localhost ip6-localhost ip6-loopback",
|
|
67
|
+
].join("\n")}\n`);
|
|
68
|
+
ensureDir(vfs, "/etc/cron.d");
|
|
69
|
+
ensureDir(vfs, "/etc/init.d");
|
|
70
|
+
ensureDir(vfs, "/etc/systemd");
|
|
71
|
+
ensureDir(vfs, "/etc/systemd/system");
|
|
72
|
+
}
|
|
73
|
+
// ─── /etc/passwd + /etc/group + /etc/shadow ─────────────────────────────────
|
|
74
|
+
export function syncEtcPasswd(vfs, users) {
|
|
75
|
+
const userList = users.listUsers();
|
|
76
|
+
const passwdLines = [
|
|
77
|
+
"root:x:0:0:root:/root:/bin/bash",
|
|
78
|
+
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin",
|
|
79
|
+
"www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin",
|
|
80
|
+
"nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin",
|
|
81
|
+
];
|
|
82
|
+
let uid = 1000;
|
|
83
|
+
for (const u of userList) {
|
|
84
|
+
if (u === "root")
|
|
85
|
+
continue;
|
|
86
|
+
passwdLines.push(`${u}:x:${uid}:${uid}::/home/${u}:/bin/bash`);
|
|
87
|
+
uid++;
|
|
88
|
+
}
|
|
89
|
+
vfs.writeFile("/etc/passwd", `${passwdLines.join("\n")}\n`);
|
|
90
|
+
const groupLines = [
|
|
91
|
+
"root:x:0:",
|
|
92
|
+
"daemon:x:1:",
|
|
93
|
+
`sudo:x:27:${userList.filter((u) => users.isSudoer(u)).join(",")}`,
|
|
94
|
+
`users:x:100:${userList.filter((u) => u !== "root").join(",")}`,
|
|
95
|
+
"nogroup:x:65534:",
|
|
96
|
+
];
|
|
97
|
+
vfs.writeFile("/etc/group", `${groupLines.join("\n")}\n`);
|
|
98
|
+
// shadow — fake hashes, never real
|
|
99
|
+
const shadowLines = [
|
|
100
|
+
"root:*:19000:0:99999:7:::",
|
|
101
|
+
"daemon:*:19000:0:99999:7:::",
|
|
102
|
+
];
|
|
103
|
+
for (const u of userList) {
|
|
104
|
+
if (u === "root")
|
|
105
|
+
continue;
|
|
106
|
+
shadowLines.push(`${u}:!:19000:0:99999:7:::`);
|
|
107
|
+
}
|
|
108
|
+
vfs.writeFile("/etc/shadow", `${shadowLines.join("\n")}\n`, { mode: 0o640 });
|
|
109
|
+
}
|
|
110
|
+
// ─── /proc ───────────────────────────────────────────────────────────────────
|
|
111
|
+
export function refreshProc(vfs, props, hostname, shellStartTime) {
|
|
112
|
+
ensureDir(vfs, "/proc");
|
|
113
|
+
const uptimeSec = Math.floor((Date.now() - shellStartTime) / 1000);
|
|
114
|
+
vfs.writeFile("/proc/uptime", `${uptimeSec}.00 ${Math.floor(uptimeSec * 0.9)}.00\n`);
|
|
115
|
+
const totalMemKb = Math.floor(os.totalmem() / 1024);
|
|
116
|
+
const freeMemKb = Math.floor(os.freemem() / 1024);
|
|
117
|
+
const availMemKb = Math.floor(freeMemKb * 0.95);
|
|
118
|
+
vfs.writeFile("/proc/meminfo", `${[
|
|
119
|
+
`MemTotal: ${String(totalMemKb).padStart(10)} kB`,
|
|
120
|
+
`MemFree: ${String(freeMemKb).padStart(10)} kB`,
|
|
121
|
+
`MemAvailable: ${String(availMemKb).padStart(10)} kB`,
|
|
122
|
+
`Buffers: ${String(Math.floor(totalMemKb * 0.02)).padStart(10)} kB`,
|
|
123
|
+
`Cached: ${String(Math.floor(totalMemKb * 0.15)).padStart(10)} kB`,
|
|
124
|
+
`SwapTotal: ${String(Math.floor(totalMemKb * 0.5)).padStart(10)} kB`,
|
|
125
|
+
`SwapFree: ${String(Math.floor(totalMemKb * 0.5)).padStart(10)} kB`,
|
|
126
|
+
].join("\n")}\n`);
|
|
127
|
+
const cpus = os.cpus();
|
|
128
|
+
const cpuLines = [];
|
|
129
|
+
for (let i = 0; i < cpus.length; i++) {
|
|
130
|
+
const c = cpus[i];
|
|
131
|
+
if (!c)
|
|
132
|
+
continue;
|
|
133
|
+
const mhz = (c.speed).toFixed(3);
|
|
134
|
+
cpuLines.push(`processor\t: ${i}`, `model name\t: ${c.model}`, `cpu MHz\t\t: ${mhz}`, `cache size\t: 8192 KB`, "");
|
|
135
|
+
}
|
|
136
|
+
vfs.writeFile("/proc/cpuinfo", `${cpuLines.join("\n")}\n`);
|
|
137
|
+
vfs.writeFile("/proc/version", `Linux version ${props.kernel} (fortune@build) (gcc version 12.2.0) #1 SMP\n`);
|
|
138
|
+
vfs.writeFile("/proc/hostname", `${hostname}\n`);
|
|
139
|
+
// /proc/loadavg
|
|
140
|
+
const load = (Math.random() * 0.5).toFixed(2);
|
|
141
|
+
vfs.writeFile("/proc/loadavg", `${load} ${load} ${load} 1/1 1\n`);
|
|
142
|
+
// /proc/net stubs
|
|
143
|
+
ensureDir(vfs, "/proc/net");
|
|
144
|
+
ensureFile(vfs, "/proc/net/dev", `${[
|
|
145
|
+
"Inter-| Receive | Transmit",
|
|
146
|
+
" face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed",
|
|
147
|
+
" lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
|
|
148
|
+
" eth0: 131072 1024 0 0 0 0 0 0 65536 512 0 0 0 0 0 0",
|
|
149
|
+
].join("\n")}\n`);
|
|
150
|
+
}
|
|
151
|
+
// ─── /sys ─────────────────────────────────────────────────────────────────────
|
|
152
|
+
function bootstrapSys(vfs, props) {
|
|
153
|
+
ensureDir(vfs, "/sys");
|
|
154
|
+
ensureDir(vfs, "/sys/devices");
|
|
155
|
+
ensureDir(vfs, "/sys/devices/virtual");
|
|
156
|
+
ensureDir(vfs, "/sys/devices/virtual/dmi");
|
|
157
|
+
ensureDir(vfs, "/sys/devices/virtual/dmi/id");
|
|
158
|
+
ensureFile(vfs, "/sys/devices/virtual/dmi/id/sys_vendor", "Fortune Systems\n");
|
|
159
|
+
ensureFile(vfs, "/sys/devices/virtual/dmi/id/product_name", "VirtualContainer v1\n");
|
|
160
|
+
ensureFile(vfs, "/sys/devices/virtual/dmi/id/board_name", "fortune-board\n");
|
|
161
|
+
ensureDir(vfs, "/sys/class");
|
|
162
|
+
ensureDir(vfs, "/sys/class/net");
|
|
163
|
+
ensureDir(vfs, "/sys/kernel");
|
|
164
|
+
ensureFile(vfs, "/sys/kernel/hostname", "fortune-vm\n");
|
|
165
|
+
ensureFile(vfs, "/sys/kernel/osrelease", `${props.kernel}\n`);
|
|
166
|
+
ensureFile(vfs, "/sys/kernel/ostype", "Linux\n");
|
|
167
|
+
}
|
|
168
|
+
// ─── /dev ─────────────────────────────────────────────────────────────────────
|
|
169
|
+
function bootstrapDev(vfs) {
|
|
170
|
+
ensureDir(vfs, "/dev");
|
|
171
|
+
ensureFile(vfs, "/dev/null", "", 0o666);
|
|
172
|
+
ensureFile(vfs, "/dev/zero", "", 0o666);
|
|
173
|
+
ensureFile(vfs, "/dev/random", "", 0o444);
|
|
174
|
+
ensureFile(vfs, "/dev/urandom", "", 0o444);
|
|
175
|
+
ensureDir(vfs, "/dev/pts");
|
|
176
|
+
ensureDir(vfs, "/dev/shm");
|
|
177
|
+
}
|
|
178
|
+
// ─── /usr ─────────────────────────────────────────────────────────────────────
|
|
179
|
+
function bootstrapUsr(vfs) {
|
|
180
|
+
ensureDir(vfs, "/usr");
|
|
181
|
+
ensureDir(vfs, "/usr/bin");
|
|
182
|
+
ensureDir(vfs, "/usr/sbin");
|
|
183
|
+
ensureDir(vfs, "/usr/local");
|
|
184
|
+
ensureDir(vfs, "/usr/local/bin");
|
|
185
|
+
ensureDir(vfs, "/usr/local/lib");
|
|
186
|
+
ensureDir(vfs, "/usr/local/share");
|
|
187
|
+
ensureDir(vfs, "/usr/share");
|
|
188
|
+
ensureDir(vfs, "/usr/share/doc");
|
|
189
|
+
ensureDir(vfs, "/usr/share/man");
|
|
190
|
+
ensureDir(vfs, "/usr/share/man/man1");
|
|
191
|
+
ensureDir(vfs, "/usr/lib");
|
|
192
|
+
// Stub binaries so `which` can find built-in commands
|
|
193
|
+
const builtins = [
|
|
194
|
+
"sh", "bash", "ls", "cat", "echo", "grep", "find", "sort",
|
|
195
|
+
"head", "tail", "cut", "tr", "sed", "awk", "wc", "tee",
|
|
196
|
+
"tar", "gzip", "gunzip", "touch", "mkdir", "rm", "mv", "cp",
|
|
197
|
+
"chmod", "ln", "pwd", "env", "date", "sleep", "id", "whoami",
|
|
198
|
+
"hostname", "uname", "ps", "kill", "df", "du", "curl", "wget",
|
|
199
|
+
"nano", "diff", "uniq", "xargs", "base64",
|
|
200
|
+
];
|
|
201
|
+
for (const bin of builtins) {
|
|
202
|
+
ensureFile(vfs, `/usr/bin/${bin}`, `#!/bin/sh\nexec builtin ${bin} "$@"\n`, 0o755);
|
|
203
|
+
}
|
|
204
|
+
// lsb_release script
|
|
205
|
+
ensureFile(vfs, "/usr/bin/lsb_release", "#!/bin/sh\nexec lsb_release \"$@\"\n", 0o755);
|
|
206
|
+
}
|
|
207
|
+
// ─── /var ─────────────────────────────────────────────────────────────────────
|
|
208
|
+
function bootstrapVar(vfs) {
|
|
209
|
+
ensureDir(vfs, "/var");
|
|
210
|
+
ensureDir(vfs, "/var/log");
|
|
211
|
+
ensureDir(vfs, "/var/tmp");
|
|
212
|
+
ensureDir(vfs, "/var/run");
|
|
213
|
+
ensureDir(vfs, "/var/cache");
|
|
214
|
+
ensureDir(vfs, "/var/cache/apt");
|
|
215
|
+
ensureDir(vfs, "/var/cache/apt/archives");
|
|
216
|
+
ensureDir(vfs, "/var/lib");
|
|
217
|
+
ensureDir(vfs, "/var/lib/apt");
|
|
218
|
+
ensureDir(vfs, "/var/lib/apt/lists");
|
|
219
|
+
ensureDir(vfs, "/var/lib/dpkg");
|
|
220
|
+
ensureDir(vfs, "/var/lib/dpkg/info");
|
|
221
|
+
// dpkg status — starts empty, apt install populates it
|
|
222
|
+
ensureFile(vfs, "/var/lib/dpkg/status", "");
|
|
223
|
+
ensureFile(vfs, "/var/lib/dpkg/available", "");
|
|
224
|
+
// syslog stub
|
|
225
|
+
ensureFile(vfs, "/var/log/syslog", `${new Date().toUTCString()} fortune kernel: Virtual container started\n`);
|
|
226
|
+
ensureFile(vfs, "/var/log/auth.log", "");
|
|
227
|
+
ensureFile(vfs, "/var/log/dpkg.log", "");
|
|
228
|
+
ensureFile(vfs, "/var/log/apt/history.log", "");
|
|
229
|
+
ensureFile(vfs, "/var/log/apt/term.log", "");
|
|
230
|
+
}
|
|
231
|
+
// ─── /bin + /sbin symlinks ────────────────────────────────────────────────────
|
|
232
|
+
function bootstrapBin(vfs) {
|
|
233
|
+
// On modern Debian/Ubuntu /bin is a symlink to /usr/bin
|
|
234
|
+
if (!vfs.exists("/bin")) {
|
|
235
|
+
vfs.symlink("/usr/bin", "/bin");
|
|
236
|
+
}
|
|
237
|
+
if (!vfs.exists("/sbin")) {
|
|
238
|
+
vfs.symlink("/usr/sbin", "/sbin");
|
|
239
|
+
}
|
|
240
|
+
if (!vfs.exists("/lib")) {
|
|
241
|
+
ensureDir(vfs, "/lib");
|
|
242
|
+
}
|
|
243
|
+
if (!vfs.exists("/lib64")) {
|
|
244
|
+
ensureDir(vfs, "/lib64");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// ─── /tmp ─────────────────────────────────────────────────────────────────────
|
|
248
|
+
function bootstrapTmp(vfs) {
|
|
249
|
+
ensureDir(vfs, "/tmp", 0o1777);
|
|
250
|
+
}
|
|
251
|
+
// ─── /root ────────────────────────────────────────────────────────────────────
|
|
252
|
+
function bootstrapRoot(vfs) {
|
|
253
|
+
ensureDir(vfs, "/root", 0o700);
|
|
254
|
+
ensureFile(vfs, "/root/.bashrc", `${[
|
|
255
|
+
"# root .bashrc",
|
|
256
|
+
"export PS1='\\[\\033[0;31m\\]\\u@\\h\\[\\033[0m\\]:\\[\\033[0;34m\\]\\w\\[\\033[0m\\]# '",
|
|
257
|
+
"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
258
|
+
"alias ll='ls -la'",
|
|
259
|
+
"alias la='ls -A'",
|
|
260
|
+
].join("\n")}\n`);
|
|
261
|
+
ensureFile(vfs, "/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
|
|
262
|
+
// Fix: /home/root should map to /root for root user
|
|
263
|
+
if (!vfs.exists("/home/root")) {
|
|
264
|
+
vfs.symlink("/root", "/home/root");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// ─── /opt + /srv + /mnt + /media ─────────────────────────────────────────────
|
|
268
|
+
function bootstrapMisc(vfs) {
|
|
269
|
+
ensureDir(vfs, "/opt");
|
|
270
|
+
ensureDir(vfs, "/srv");
|
|
271
|
+
ensureDir(vfs, "/mnt");
|
|
272
|
+
ensureDir(vfs, "/media");
|
|
273
|
+
}
|
|
274
|
+
// ─── main entry point ─────────────────────────────────────────────────────────
|
|
275
|
+
/**
|
|
276
|
+
* Bootstraps the full Linux rootfs hierarchy in the VFS.
|
|
277
|
+
* Safe to call multiple times — idempotent.
|
|
278
|
+
*
|
|
279
|
+
* @param vfs Target virtual filesystem.
|
|
280
|
+
* @param users User manager (for /etc/passwd sync).
|
|
281
|
+
* @param hostname Virtual hostname.
|
|
282
|
+
* @param props Shell properties (kernel, os, arch).
|
|
283
|
+
* @param shellStartTime Unix ms of shell creation (for uptime).
|
|
284
|
+
*/
|
|
285
|
+
export function bootstrapLinuxRootfs(vfs, users, hostname, props, shellStartTime) {
|
|
286
|
+
bootstrapEtc(vfs, hostname, props);
|
|
287
|
+
bootstrapSys(vfs, props);
|
|
288
|
+
bootstrapDev(vfs);
|
|
289
|
+
bootstrapUsr(vfs);
|
|
290
|
+
bootstrapVar(vfs);
|
|
291
|
+
bootstrapBin(vfs);
|
|
292
|
+
bootstrapTmp(vfs);
|
|
293
|
+
bootstrapRoot(vfs);
|
|
294
|
+
bootstrapMisc(vfs);
|
|
295
|
+
refreshProc(vfs, props, hostname, shellStartTime);
|
|
296
|
+
syncEtcPasswd(vfs, users);
|
|
297
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"neofetch.d.ts","sourceRoot":"","sources":["../../src/modules/neofetch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAsGvD,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAkKD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"neofetch.d.ts","sourceRoot":"","sources":["../../src/modules/neofetch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAsGvD,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAkKD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CA2E9D"}
|
package/dist/modules/neofetch.js
CHANGED
|
@@ -255,6 +255,7 @@ export function buildNeofetchOutput(info) {
|
|
|
255
255
|
`Kernel: ${fields.kernel}`,
|
|
256
256
|
`Uptime: ${uptime}`,
|
|
257
257
|
// `Packages: ${fields.packages}`,
|
|
258
|
+
`Packages: ${fields.packages}`,
|
|
258
259
|
`Shell: ${fields.shell}`,
|
|
259
260
|
// `Shell Props: ${fields.shellProps}`,
|
|
260
261
|
`Resolution: ${fields.resolution}`,
|
package/dist/standalone.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { VirtualSftpServer, VirtualShell, VirtualSshServer } from ".";
|
|
2
2
|
const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
|
|
3
|
-
const virtualShell = new VirtualShell(hostname
|
|
3
|
+
const virtualShell = new VirtualShell(hostname, undefined, {
|
|
4
|
+
mode: "fs",
|
|
5
|
+
snapshotPath: ".vfs",
|
|
6
|
+
});
|
|
4
7
|
virtualShell.addCommand("demo", [], () => {
|
|
5
8
|
return {
|
|
6
9
|
stdout: "This is a demo command. It does nothing useful.",
|
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.2.
|
|
7
|
+
"version": "1.2.9",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -25,6 +25,7 @@
|
|
|
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",
|
package/src/SSHMimic/executor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runCommandDirect } from "../commands";
|
|
2
2
|
import { resolvePath } from "../commands/helpers";
|
|
3
3
|
import type { CommandMode, CommandResult, ShellEnv } from "../types/commands";
|
|
4
4
|
import type { Pipeline, PipelineCommand, Script, Statement } from "../types/pipeline";
|
|
@@ -117,8 +117,7 @@ async function executeSingleCommandWithRedirections(
|
|
|
117
117
|
catch { return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 }; }
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const
|
|
121
|
-
const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, stdin, env);
|
|
120
|
+
const result = await runCommandDirect(cmd.name, cmd.args, authUser, hostname, mode, cwd, shell, stdin, env);
|
|
122
121
|
|
|
123
122
|
if (cmd.outputFile) {
|
|
124
123
|
const outputPath = resolvePath(cwd, cmd.outputFile);
|
|
@@ -160,8 +159,7 @@ async function executePipelineChain(
|
|
|
160
159
|
catch { return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 }; }
|
|
161
160
|
}
|
|
162
161
|
|
|
163
|
-
const
|
|
164
|
-
const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, currentOutput, env);
|
|
162
|
+
const result = await runCommandDirect(cmd.name, cmd.args, authUser, hostname, mode, cwd, shell, currentOutput, env);
|
|
165
163
|
exitCode = result.exitCode ?? 0;
|
|
166
164
|
|
|
167
165
|
if (i === commands.length - 1 && cmd.outputFile) {
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary serialisation format for VirtualFileSystem snapshots.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the JSON+base64 approach. No external dependencies.
|
|
5
|
+
*
|
|
6
|
+
* Wire format (little-endian throughout):
|
|
7
|
+
*
|
|
8
|
+
* File header:
|
|
9
|
+
* [4] magic = 0x56 0x46 0x53 0x21 ("VFS!")
|
|
10
|
+
* [1] version = 0x01
|
|
11
|
+
*
|
|
12
|
+
* Node (recursive):
|
|
13
|
+
* [1] type = 0x01 (file) | 0x02 (directory)
|
|
14
|
+
* [2] name length (uint16)
|
|
15
|
+
* [N] name bytes (utf8)
|
|
16
|
+
* [4] mode (uint32)
|
|
17
|
+
* [8] createdAt ms (float64)
|
|
18
|
+
* [8] updatedAt ms (float64)
|
|
19
|
+
*
|
|
20
|
+
* File node extra:
|
|
21
|
+
* [1] compressed flag (0x00 | 0x01)
|
|
22
|
+
* [4] content length (uint32)
|
|
23
|
+
* [N] content bytes (raw — no base64)
|
|
24
|
+
*
|
|
25
|
+
* Directory node extra:
|
|
26
|
+
* [4] children count (uint32)
|
|
27
|
+
* [N] children nodes (recursive)
|
|
28
|
+
*
|
|
29
|
+
* Total overhead vs JSON+base64 for 1 MB of file data:
|
|
30
|
+
* JSON+base64 : ~1.37 MB (base64 33% bloat) + JSON string wrapping
|
|
31
|
+
* Binary pack : ~1.00 MB + ~40 bytes/node header → ~27% smaller, no string parsing
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { InternalDirectoryNode, InternalFileNode, InternalNode } from "./internalTypes";
|
|
35
|
+
|
|
36
|
+
const MAGIC = Buffer.from([0x56, 0x46, 0x53, 0x21]); // "VFS!"
|
|
37
|
+
const VERSION = 0x01;
|
|
38
|
+
const TYPE_FILE = 0x01;
|
|
39
|
+
const TYPE_DIR = 0x02;
|
|
40
|
+
|
|
41
|
+
// ── Encoder ───────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
class Encoder {
|
|
44
|
+
private chunks: Buffer[] = [];
|
|
45
|
+
|
|
46
|
+
write(buf: Buffer): void { this.chunks.push(buf); }
|
|
47
|
+
|
|
48
|
+
writeUint8(n: number): void {
|
|
49
|
+
const b = Buffer.allocUnsafe(1);
|
|
50
|
+
b.writeUInt8(n, 0);
|
|
51
|
+
this.chunks.push(b);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
writeUint16(n: number): void {
|
|
55
|
+
const b = Buffer.allocUnsafe(2);
|
|
56
|
+
b.writeUInt16LE(n, 0);
|
|
57
|
+
this.chunks.push(b);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
writeUint32(n: number): void {
|
|
61
|
+
const b = Buffer.allocUnsafe(4);
|
|
62
|
+
b.writeUInt32LE(n, 0);
|
|
63
|
+
this.chunks.push(b);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
writeFloat64(n: number): void {
|
|
67
|
+
const b = Buffer.allocUnsafe(8);
|
|
68
|
+
b.writeDoubleBE(n, 0);
|
|
69
|
+
this.chunks.push(b);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
writeString(s: string): void {
|
|
73
|
+
const encoded = Buffer.from(s, "utf8");
|
|
74
|
+
this.writeUint16(encoded.length);
|
|
75
|
+
this.chunks.push(encoded);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
writeBytes(bytes: Buffer): void {
|
|
79
|
+
this.writeUint32(bytes.length);
|
|
80
|
+
this.chunks.push(bytes);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toBuffer(): Buffer { return Buffer.concat(this.chunks); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function encodeNode(enc: Encoder, node: InternalNode): void {
|
|
87
|
+
if (node.type === "file") {
|
|
88
|
+
const f = node as InternalFileNode;
|
|
89
|
+
enc.writeUint8(TYPE_FILE);
|
|
90
|
+
enc.writeString(f.name);
|
|
91
|
+
enc.writeUint32(f.mode);
|
|
92
|
+
enc.writeFloat64(f.createdAt.getTime());
|
|
93
|
+
enc.writeFloat64(f.updatedAt.getTime());
|
|
94
|
+
enc.writeUint8(f.compressed ? 0x01 : 0x00);
|
|
95
|
+
enc.writeBytes(f.content);
|
|
96
|
+
} else {
|
|
97
|
+
const d = node as InternalDirectoryNode;
|
|
98
|
+
enc.writeUint8(TYPE_DIR);
|
|
99
|
+
enc.writeString(d.name);
|
|
100
|
+
enc.writeUint32(d.mode);
|
|
101
|
+
enc.writeFloat64(d.createdAt.getTime());
|
|
102
|
+
enc.writeFloat64(d.updatedAt.getTime());
|
|
103
|
+
const children = Array.from(d.children.values());
|
|
104
|
+
enc.writeUint32(children.length);
|
|
105
|
+
for (const child of children) encodeNode(enc, child);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Serialise an in-memory VFS root to a compact binary Buffer.
|
|
111
|
+
* No base64, no JSON. ~27% smaller than the JSON+base64 format for typical VFS trees.
|
|
112
|
+
*/
|
|
113
|
+
export function encodeVfs(root: InternalDirectoryNode): Buffer {
|
|
114
|
+
const enc = new Encoder();
|
|
115
|
+
enc.write(MAGIC);
|
|
116
|
+
enc.writeUint8(VERSION);
|
|
117
|
+
encodeNode(enc, root);
|
|
118
|
+
return enc.toBuffer();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Decoder ───────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
class Decoder {
|
|
124
|
+
private pos = 0;
|
|
125
|
+
constructor(private readonly buf: Buffer) {}
|
|
126
|
+
|
|
127
|
+
readUint8(): number { return this.buf.readUInt8(this.pos++); }
|
|
128
|
+
|
|
129
|
+
readUint16(): number {
|
|
130
|
+
const v = this.buf.readUInt16LE(this.pos);
|
|
131
|
+
this.pos += 2;
|
|
132
|
+
return v;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
readUint32(): number {
|
|
136
|
+
const v = this.buf.readUInt32LE(this.pos);
|
|
137
|
+
this.pos += 4;
|
|
138
|
+
return v;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
readFloat64(): number {
|
|
142
|
+
const v = this.buf.readDoubleBE(this.pos);
|
|
143
|
+
this.pos += 8;
|
|
144
|
+
return v;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
readString(): string {
|
|
148
|
+
const len = this.readUint16();
|
|
149
|
+
const s = this.buf.toString("utf8", this.pos, this.pos + len);
|
|
150
|
+
this.pos += len;
|
|
151
|
+
return s;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
readBytes(): Buffer {
|
|
155
|
+
const len = this.readUint32();
|
|
156
|
+
const b = this.buf.slice(this.pos, this.pos + len);
|
|
157
|
+
this.pos += len;
|
|
158
|
+
return b;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
remaining(): number { return this.buf.length - this.pos; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function decodeNode(dec: Decoder): InternalNode {
|
|
165
|
+
const type = dec.readUint8();
|
|
166
|
+
const name = dec.readString();
|
|
167
|
+
const mode = dec.readUint32();
|
|
168
|
+
const createdAt = new Date(dec.readFloat64());
|
|
169
|
+
const updatedAt = new Date(dec.readFloat64());
|
|
170
|
+
|
|
171
|
+
if (type === TYPE_FILE) {
|
|
172
|
+
const compressed = dec.readUint8() === 0x01;
|
|
173
|
+
const content = dec.readBytes();
|
|
174
|
+
return { type: "file", name, mode, createdAt, updatedAt, compressed, content } satisfies InternalFileNode;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (type === TYPE_DIR) {
|
|
178
|
+
const count = dec.readUint32();
|
|
179
|
+
const children = new Map<string, InternalNode>();
|
|
180
|
+
for (let i = 0; i < count; i++) {
|
|
181
|
+
const child = decodeNode(dec);
|
|
182
|
+
children.set(child.name, child);
|
|
183
|
+
}
|
|
184
|
+
return { type: "directory", name, mode, createdAt, updatedAt, children } satisfies InternalDirectoryNode;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
throw new Error(`[VFS binary] Unknown node type: 0x${type.toString(16)}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Deserialise a binary Buffer produced by {@link encodeVfs} back into an
|
|
192
|
+
* InternalDirectoryNode tree. Throws on magic/version mismatch or truncation.
|
|
193
|
+
*/
|
|
194
|
+
export function decodeVfs(buf: Buffer): InternalDirectoryNode {
|
|
195
|
+
if (buf.length < 5) throw new Error("[VFS binary] Buffer too short");
|
|
196
|
+
|
|
197
|
+
const magic = buf.slice(0, 4);
|
|
198
|
+
if (!magic.equals(MAGIC)) {
|
|
199
|
+
throw new Error("[VFS binary] Invalid magic — not a VFS binary snapshot");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const dec = new Decoder(buf);
|
|
203
|
+
// skip magic (4) + version (1)
|
|
204
|
+
for (let i = 0; i < 5; i++) dec.readUint8();
|
|
205
|
+
|
|
206
|
+
const root = decodeNode(dec);
|
|
207
|
+
if (root.type !== "directory") {
|
|
208
|
+
throw new Error("[VFS binary] Root node must be a directory");
|
|
209
|
+
}
|
|
210
|
+
return root as InternalDirectoryNode;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns true if `buf` looks like a VFS binary snapshot (starts with magic bytes).
|
|
215
|
+
* Used to auto-detect format when loading from disk.
|
|
216
|
+
*/
|
|
217
|
+
export function isBinarySnapshot(buf: Buffer): boolean {
|
|
218
|
+
return buf.length >= 4 && buf.slice(0, 4).equals(MAGIC);
|
|
219
|
+
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
InternalFileNode,
|
|
8
8
|
InternalNode,
|
|
9
9
|
} from "./internalTypes";
|
|
10
|
+
import { decodeVfs, encodeVfs, isBinarySnapshot } from "./binaryPack";
|
|
10
11
|
import { getNode, getParentDirectory, normalizePath } from "./path";
|
|
11
12
|
import type {
|
|
12
13
|
RemoveOptions,
|
|
@@ -24,8 +25,8 @@ import type {
|
|
|
24
25
|
* "memory" — pure in-memory, no disk I/O (default).
|
|
25
26
|
*
|
|
26
27
|
* "fs" — mirrors the VFS tree to a directory on the host filesystem.
|
|
27
|
-
* `snapshotPath` must be set to the directory where the
|
|
28
|
-
* snapshot file will be read/written.
|
|
28
|
+
* `snapshotPath` must be set to the directory where the binary
|
|
29
|
+
* snapshot file will be read/written (`vfs-snapshot.vfsb`).
|
|
29
30
|
*/
|
|
30
31
|
export type VfsPersistenceMode = "memory" | "fs";
|
|
31
32
|
|
|
@@ -53,7 +54,7 @@ export interface VfsOptions {
|
|
|
53
54
|
* **Memory mode** (default) — all state lives in a fast recursive tree.
|
|
54
55
|
* Use `toSnapshot()` / `fromSnapshot()` / `importSnapshot()` for serialisation.
|
|
55
56
|
*
|
|
56
|
-
* **FS mode** — same in-memory tree, but `restoreMirror()` loads a
|
|
57
|
+
* **FS mode** — same in-memory tree, but `restoreMirror()` loads a binary
|
|
57
58
|
* snapshot from disk and `flushMirror()` writes it back. This gives you
|
|
58
59
|
* persistent VFS state across process restarts without any real POSIX filesystem
|
|
59
60
|
* semantics leaking through.
|
|
@@ -86,7 +87,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
86
87
|
}
|
|
87
88
|
this.snapshotFile = path.resolve(
|
|
88
89
|
options.snapshotPath,
|
|
89
|
-
"vfs-snapshot.
|
|
90
|
+
"vfs-snapshot.vfsb",
|
|
90
91
|
);
|
|
91
92
|
} else {
|
|
92
93
|
this.snapshotFile = null;
|
|
@@ -151,7 +152,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
151
152
|
// ── Persistence ───────────────────────────────────────────────────────────
|
|
152
153
|
|
|
153
154
|
/**
|
|
154
|
-
* In `"fs"` mode: reads the
|
|
155
|
+
* In `"fs"` mode: reads the binary snapshot (`vfs-snapshot.vfsb`) from disk.
|
|
156
|
+
* Automatically falls back to legacy JSON format for backward compatibility.
|
|
155
157
|
* Silently succeeds when the snapshot file does not exist yet.
|
|
156
158
|
*
|
|
157
159
|
* In `"memory"` mode: no-op (kept for API compatibility).
|
|
@@ -162,9 +164,16 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
162
164
|
if (!fsSync.existsSync(this.snapshotFile)) return;
|
|
163
165
|
|
|
164
166
|
try {
|
|
165
|
-
const raw = fsSync.readFileSync(this.snapshotFile
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
const raw = fsSync.readFileSync(this.snapshotFile);
|
|
168
|
+
if (isBinarySnapshot(raw)) {
|
|
169
|
+
// Fast binary format (current)
|
|
170
|
+
this.root = decodeVfs(raw);
|
|
171
|
+
} else {
|
|
172
|
+
// Legacy JSON fallback — auto-migrates on next flushMirror()
|
|
173
|
+
const snapshot: VfsSnapshot = JSON.parse(raw.toString("utf8"));
|
|
174
|
+
this.root = this.deserializeDir(snapshot.root, "");
|
|
175
|
+
console.info("[VirtualFileSystem] Migrating legacy JSON snapshot to binary format.");
|
|
176
|
+
}
|
|
168
177
|
this.emit("snapshot:restore", { path: this.snapshotFile });
|
|
169
178
|
} catch (err) {
|
|
170
179
|
// Corrupt or unreadable snapshot — start fresh and warn
|
|
@@ -176,7 +185,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
176
185
|
}
|
|
177
186
|
|
|
178
187
|
/**
|
|
179
|
-
* In `"fs"` mode: serialises the in-memory tree to a
|
|
188
|
+
* In `"fs"` mode: serialises the in-memory tree to a binary snapshot on disk
|
|
189
|
+
* (`vfs-snapshot.vfsb`). ~27% smaller and significantly faster than JSON+base64.
|
|
180
190
|
* The directory is created if it does not exist.
|
|
181
191
|
*
|
|
182
192
|
* In `"memory"` mode: emits `"mirror:flush"` and returns (no disk write).
|
|
@@ -189,8 +199,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
189
199
|
|
|
190
200
|
const dir = path.dirname(this.snapshotFile);
|
|
191
201
|
fsSync.mkdirSync(dir, { recursive: true });
|
|
192
|
-
const
|
|
193
|
-
fsSync.writeFileSync(this.snapshotFile,
|
|
202
|
+
const binary = encodeVfs(this.root);
|
|
203
|
+
fsSync.writeFileSync(this.snapshotFile, binary);
|
|
194
204
|
this.emit("mirror:flush", { path: this.snapshotFile });
|
|
195
205
|
}
|
|
196
206
|
|