typescript-virtual-container 1.2.9 → 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.
Files changed (281) hide show
  1. package/.vscode/settings.json +0 -1
  2. package/README.md +141 -50
  3. package/biome.json +7 -0
  4. package/dist/SSHMimic/exec.d.ts.map +1 -1
  5. package/dist/SSHMimic/executor.d.ts.map +1 -1
  6. package/dist/SSHMimic/executor.js +32 -16
  7. package/dist/SSHMimic/index.d.ts.map +1 -1
  8. package/dist/SSHMimic/index.js +20 -6
  9. package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -1
  10. package/dist/VirtualFileSystem/binaryPack.js +29 -6
  11. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem/index.js +36 -13
  13. package/dist/VirtualPackageManager/index.d.ts.map +1 -1
  14. package/dist/VirtualPackageManager/index.js +192 -43
  15. package/dist/VirtualShell/index.d.ts +10 -4
  16. package/dist/VirtualShell/index.d.ts.map +1 -1
  17. package/dist/VirtualShell/index.js +18 -7
  18. package/dist/VirtualShell/shell.d.ts.map +1 -1
  19. package/dist/VirtualShell/shell.js +3 -1
  20. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  21. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  22. package/dist/commands/adduser.d.ts +6 -0
  23. package/dist/commands/adduser.d.ts.map +1 -1
  24. package/dist/commands/adduser.js +6 -0
  25. package/dist/commands/alias.d.ts +5 -0
  26. package/dist/commands/alias.d.ts.map +1 -1
  27. package/dist/commands/alias.js +5 -0
  28. package/dist/commands/apt.d.ts +5 -0
  29. package/dist/commands/apt.d.ts.map +1 -1
  30. package/dist/commands/apt.js +32 -9
  31. package/dist/commands/awk.d.ts +11 -0
  32. package/dist/commands/awk.d.ts.map +1 -1
  33. package/dist/commands/awk.js +15 -2
  34. package/dist/commands/base64.d.ts +5 -0
  35. package/dist/commands/base64.d.ts.map +1 -1
  36. package/dist/commands/base64.js +9 -1
  37. package/dist/commands/cat.d.ts +5 -0
  38. package/dist/commands/cat.d.ts.map +1 -1
  39. package/dist/commands/cat.js +10 -2
  40. package/dist/commands/cd.d.ts +5 -0
  41. package/dist/commands/cd.d.ts.map +1 -1
  42. package/dist/commands/cd.js +5 -0
  43. package/dist/commands/chmod.d.ts +5 -0
  44. package/dist/commands/chmod.d.ts.map +1 -1
  45. package/dist/commands/chmod.js +5 -0
  46. package/dist/commands/cp.d.ts +5 -0
  47. package/dist/commands/cp.d.ts.map +1 -1
  48. package/dist/commands/cp.js +5 -0
  49. package/dist/commands/curl.d.ts +5 -0
  50. package/dist/commands/curl.d.ts.map +1 -1
  51. package/dist/commands/curl.js +34 -6
  52. package/dist/commands/cut.d.ts +5 -0
  53. package/dist/commands/cut.d.ts.map +1 -1
  54. package/dist/commands/cut.js +8 -1
  55. package/dist/commands/date.d.ts +5 -0
  56. package/dist/commands/date.d.ts.map +1 -1
  57. package/dist/commands/date.js +7 -1
  58. package/dist/commands/declare.d.ts +3 -0
  59. package/dist/commands/declare.d.ts.map +1 -0
  60. package/dist/commands/declare.js +39 -0
  61. package/dist/commands/diff.d.ts +5 -0
  62. package/dist/commands/diff.d.ts.map +1 -1
  63. package/dist/commands/diff.js +5 -0
  64. package/dist/commands/dpkg.d.ts +5 -0
  65. package/dist/commands/dpkg.d.ts.map +1 -1
  66. package/dist/commands/dpkg.js +24 -7
  67. package/dist/commands/du.d.ts.map +1 -1
  68. package/dist/commands/du.js +8 -2
  69. package/dist/commands/echo.d.ts +5 -0
  70. package/dist/commands/echo.d.ts.map +1 -1
  71. package/dist/commands/echo.js +13 -4
  72. package/dist/commands/env.d.ts +5 -0
  73. package/dist/commands/env.d.ts.map +1 -1
  74. package/dist/commands/env.js +11 -1
  75. package/dist/commands/exit.d.ts +5 -0
  76. package/dist/commands/exit.d.ts.map +1 -1
  77. package/dist/commands/exit.js +12 -2
  78. package/dist/commands/export.d.ts.map +1 -1
  79. package/dist/commands/export.js +3 -1
  80. package/dist/commands/find.d.ts +5 -0
  81. package/dist/commands/find.d.ts.map +1 -1
  82. package/dist/commands/find.js +5 -0
  83. package/dist/commands/free.d.ts +5 -0
  84. package/dist/commands/free.d.ts.map +1 -1
  85. package/dist/commands/free.js +5 -0
  86. package/dist/commands/grep.d.ts +5 -0
  87. package/dist/commands/grep.d.ts.map +1 -1
  88. package/dist/commands/grep.js +12 -2
  89. package/dist/commands/gzip.d.ts +5 -0
  90. package/dist/commands/gzip.d.ts.map +1 -1
  91. package/dist/commands/gzip.js +18 -2
  92. package/dist/commands/head.d.ts +5 -0
  93. package/dist/commands/head.d.ts.map +1 -1
  94. package/dist/commands/head.js +5 -0
  95. package/dist/commands/help.d.ts.map +1 -1
  96. package/dist/commands/help.js +98 -45
  97. package/dist/commands/history.d.ts +5 -0
  98. package/dist/commands/history.d.ts.map +1 -1
  99. package/dist/commands/history.js +5 -0
  100. package/dist/commands/hostname.d.ts +5 -0
  101. package/dist/commands/hostname.d.ts.map +1 -1
  102. package/dist/commands/hostname.js +5 -0
  103. package/dist/commands/id.d.ts.map +1 -1
  104. package/dist/commands/id.js +4 -1
  105. package/dist/commands/index.d.ts +2 -17
  106. package/dist/commands/index.d.ts.map +1 -1
  107. package/dist/commands/index.js +2 -340
  108. package/dist/commands/ls.d.ts.map +1 -1
  109. package/dist/commands/ls.js +3 -1
  110. package/dist/commands/lsb-release.d.ts.map +1 -1
  111. package/dist/commands/lsb-release.js +8 -2
  112. package/dist/commands/nano.js +1 -1
  113. package/dist/commands/neofetch.js +1 -1
  114. package/dist/commands/node.d.ts +9 -0
  115. package/dist/commands/node.d.ts.map +1 -0
  116. package/dist/commands/node.js +316 -0
  117. package/dist/commands/npm.d.ts +19 -0
  118. package/dist/commands/npm.d.ts.map +1 -0
  119. package/dist/commands/npm.js +109 -0
  120. package/dist/commands/ping.d.ts.map +1 -1
  121. package/dist/commands/ping.js +3 -1
  122. package/dist/commands/printf.d.ts +3 -0
  123. package/dist/commands/printf.d.ts.map +1 -0
  124. package/dist/commands/printf.js +113 -0
  125. package/dist/commands/ps.d.ts.map +1 -1
  126. package/dist/commands/ps.js +4 -1
  127. package/dist/commands/python.d.ts +30 -0
  128. package/dist/commands/python.d.ts.map +1 -0
  129. package/dist/commands/python.js +2058 -0
  130. package/dist/commands/read.d.ts +3 -0
  131. package/dist/commands/read.d.ts.map +1 -0
  132. package/dist/commands/read.js +34 -0
  133. package/dist/commands/registry.d.ts +8 -0
  134. package/dist/commands/registry.d.ts.map +1 -0
  135. package/dist/commands/registry.js +229 -0
  136. package/dist/commands/runtime.d.ts +6 -0
  137. package/dist/commands/runtime.d.ts.map +1 -0
  138. package/dist/commands/runtime.js +280 -0
  139. package/dist/commands/sed.d.ts.map +1 -1
  140. package/dist/commands/sed.js +11 -3
  141. package/dist/commands/set.d.ts.map +1 -1
  142. package/dist/commands/set.js +9 -3
  143. package/dist/commands/sh.d.ts.map +1 -1
  144. package/dist/commands/sh.js +57 -36
  145. package/dist/commands/shift.d.ts +5 -0
  146. package/dist/commands/shift.d.ts.map +1 -0
  147. package/dist/commands/shift.js +52 -0
  148. package/dist/commands/sleep.d.ts.map +1 -1
  149. package/dist/commands/sort.d.ts.map +1 -1
  150. package/dist/commands/sort.js +4 -2
  151. package/dist/commands/source.d.ts.map +1 -1
  152. package/dist/commands/source.js +5 -2
  153. package/dist/commands/sudo.js +1 -1
  154. package/dist/commands/tar.d.ts.map +1 -1
  155. package/dist/commands/tar.js +11 -3
  156. package/dist/commands/tee.d.ts.map +1 -1
  157. package/dist/commands/tee.js +8 -6
  158. package/dist/commands/test.d.ts.map +1 -1
  159. package/dist/commands/test.js +46 -24
  160. package/dist/commands/tr.d.ts.map +1 -1
  161. package/dist/commands/tr.js +3 -1
  162. package/dist/commands/true.d.ts +4 -0
  163. package/dist/commands/true.d.ts.map +1 -0
  164. package/dist/commands/true.js +14 -0
  165. package/dist/commands/type.d.ts.map +1 -1
  166. package/dist/commands/type.js +1 -1
  167. package/dist/commands/uname.d.ts.map +1 -1
  168. package/dist/commands/uname.js +4 -1
  169. package/dist/commands/uniq.d.ts.map +1 -1
  170. package/dist/commands/uptime.d.ts.map +1 -1
  171. package/dist/commands/uptime.js +4 -1
  172. package/dist/commands/wget.d.ts.map +1 -1
  173. package/dist/commands/wget.js +32 -7
  174. package/dist/commands/which.d.ts.map +1 -1
  175. package/dist/commands/xargs.d.ts.map +1 -1
  176. package/dist/commands/xargs.js +1 -1
  177. package/dist/index.d.ts +15 -14
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +9 -9
  180. package/dist/modules/linuxRootfs.d.ts +18 -1
  181. package/dist/modules/linuxRootfs.d.ts.map +1 -1
  182. package/dist/modules/linuxRootfs.js +160 -17
  183. package/dist/standalone-wo-sftp.d.ts +2 -0
  184. package/dist/standalone-wo-sftp.d.ts.map +1 -0
  185. package/dist/standalone-wo-sftp.js +30 -0
  186. package/dist/utils/expand.d.ts +50 -0
  187. package/dist/utils/expand.d.ts.map +1 -0
  188. package/dist/utils/expand.js +183 -0
  189. package/dist/utils/vfsDiff.d.ts +90 -0
  190. package/dist/utils/vfsDiff.d.ts.map +1 -0
  191. package/dist/utils/vfsDiff.js +177 -0
  192. package/package.json +2 -1
  193. package/src/SSHMimic/exec.ts +10 -1
  194. package/src/SSHMimic/executor.ts +104 -18
  195. package/src/SSHMimic/index.ts +49 -15
  196. package/src/VirtualFileSystem/binaryPack.ts +35 -8
  197. package/src/VirtualFileSystem/index.ts +78 -28
  198. package/src/VirtualPackageManager/index.ts +208 -49
  199. package/src/VirtualShell/index.ts +35 -7
  200. package/src/VirtualShell/shell.ts +23 -3
  201. package/src/VirtualShell/shellParser.ts +134 -36
  202. package/src/VirtualUserManager/index.ts +7 -2
  203. package/src/commands/adduser.ts +6 -0
  204. package/src/commands/alias.ts +5 -1
  205. package/src/commands/apt.ts +47 -17
  206. package/src/commands/awk.ts +20 -6
  207. package/src/commands/base64.ts +13 -2
  208. package/src/commands/cat.ts +13 -5
  209. package/src/commands/cd.ts +5 -0
  210. package/src/commands/chmod.ts +5 -0
  211. package/src/commands/cp.ts +5 -0
  212. package/src/commands/curl.ts +56 -12
  213. package/src/commands/cut.ts +8 -1
  214. package/src/commands/date.ts +7 -1
  215. package/src/commands/declare.ts +44 -0
  216. package/src/commands/diff.ts +17 -3
  217. package/src/commands/dpkg.ts +33 -11
  218. package/src/commands/du.ts +17 -5
  219. package/src/commands/echo.ts +22 -9
  220. package/src/commands/env.ts +11 -1
  221. package/src/commands/exit.ts +12 -2
  222. package/src/commands/export.ts +3 -1
  223. package/src/commands/find.ts +5 -0
  224. package/src/commands/free.ts +9 -2
  225. package/src/commands/grep.ts +12 -2
  226. package/src/commands/gzip.ts +28 -4
  227. package/src/commands/head.ts +5 -0
  228. package/src/commands/help.ts +121 -47
  229. package/src/commands/history.ts +7 -2
  230. package/src/commands/hostname.ts +5 -0
  231. package/src/commands/id.ts +4 -1
  232. package/src/commands/index.ts +9 -360
  233. package/src/commands/ls.ts +5 -3
  234. package/src/commands/lsb-release.ts +8 -2
  235. package/src/commands/nano.ts +1 -1
  236. package/src/commands/neofetch.ts +1 -1
  237. package/src/commands/node.ts +341 -0
  238. package/src/commands/npm.ts +132 -0
  239. package/src/commands/ping.ts +6 -2
  240. package/src/commands/printf.ts +112 -0
  241. package/src/commands/ps.ts +21 -9
  242. package/src/commands/python.ts +2229 -0
  243. package/src/commands/read.ts +41 -0
  244. package/src/commands/registry.ts +244 -0
  245. package/src/commands/runtime.ts +353 -0
  246. package/src/commands/sed.ts +27 -9
  247. package/src/commands/set.ts +9 -3
  248. package/src/commands/sh.ts +159 -55
  249. package/src/commands/shift.ts +53 -0
  250. package/src/commands/sleep.ts +2 -1
  251. package/src/commands/sort.ts +10 -6
  252. package/src/commands/source.ts +15 -3
  253. package/src/commands/sudo.ts +1 -1
  254. package/src/commands/tar.ts +28 -7
  255. package/src/commands/tee.ts +7 -1
  256. package/src/commands/test.ts +61 -26
  257. package/src/commands/tr.ts +3 -1
  258. package/src/commands/true.ts +17 -0
  259. package/src/commands/type.ts +6 -3
  260. package/src/commands/uname.ts +5 -1
  261. package/src/commands/uniq.ts +8 -2
  262. package/src/commands/uptime.ts +4 -1
  263. package/src/commands/wget.ts +51 -12
  264. package/src/commands/which.ts +5 -2
  265. package/src/commands/xargs.ts +11 -2
  266. package/src/index.ts +23 -24
  267. package/src/modules/linuxRootfs.ts +233 -30
  268. package/src/standalone-wo-sftp.ts +38 -0
  269. package/src/utils/expand.ts +238 -0
  270. package/src/utils/vfsDiff.ts +275 -0
  271. package/standalone-wo-sftp.js +507 -0
  272. package/standalone-wo-sftp.js.map +7 -0
  273. package/standalone.js +253 -191
  274. package/standalone.js.map +4 -4
  275. package/tests/bun-test-shim.ts +9 -1
  276. package/tests/command-helpers.test.ts +1 -5
  277. package/tests/new-features.test.ts +415 -5
  278. package/tests/parser-executor.test.ts +27 -27
  279. package/tests/sftp.test.ts +122 -42
  280. package/tests/users.test.ts +23 -5
  281. package/CHANGELOG.md +0 -150
@@ -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.2.9",
7
+ "version": "1.3.0",
8
8
  "license": "MIT",
9
9
  "repository": {
10
10
  "type": "git",
@@ -29,6 +29,7 @@
29
29
  "build": "tsc --project tsconfig.json",
30
30
  "deploy:npm": "npm publish --access public",
31
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",
32
33
  "publish-package": "bash ./scripts/publish-package.sh",
33
34
  "standalone-build": "bunx esbuild src/standalone.ts --bundle --platform=node --target=node18 --outfile=standalone.js --tree-shaking=true --minify --sourcemap"
34
35
  },
@@ -17,7 +17,16 @@ export function runExec(
17
17
  shell: VirtualShell,
18
18
  ): void {
19
19
  Promise.resolve(
20
- runCommand(cmd, authUser, hostname, "exec", `/home/${authUser}`, shell, undefined, makeDefaultEnv(authUser, hostname)),
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) {
@@ -1,7 +1,12 @@
1
1
  import { runCommandDirect } from "../commands";
2
2
  import { resolvePath } from "../commands/helpers";
3
3
  import type { CommandMode, CommandResult, ShellEnv } from "../types/commands";
4
- import type { Pipeline, PipelineCommand, Script, Statement } from "../types/pipeline";
4
+ import type {
5
+ Pipeline,
6
+ PipelineCommand,
7
+ Script,
8
+ Statement,
9
+ } from "../types/pipeline";
5
10
  import type { VirtualShell } from "../VirtualShell";
6
11
 
7
12
  // ── Script executor (handles &&/||/;) ────────────────────────────────────────
@@ -15,17 +20,30 @@ export async function executeScript(
15
20
  shell: VirtualShell,
16
21
  env: ShellEnv,
17
22
  ): Promise<CommandResult> {
18
- if (!script.isValid) return { stderr: script.error || "Syntax error", exitCode: 1 };
23
+ if (!script.isValid)
24
+ return { stderr: script.error || "Syntax error", exitCode: 1 };
19
25
 
20
26
  let lastResult: CommandResult = { exitCode: 0 };
21
27
 
22
28
  for (const stmt of script.statements) {
23
29
  // Decide whether to run this statement based on previous op
24
- lastResult = await executePipeline(stmt.pipeline, authUser, hostname, mode, cwd, shell, env);
30
+ lastResult = await executePipeline(
31
+ stmt.pipeline,
32
+ authUser,
33
+ hostname,
34
+ mode,
35
+ cwd,
36
+ shell,
37
+ env,
38
+ );
25
39
  env.lastExitCode = lastResult.exitCode ?? 0;
26
40
 
27
41
  // Propagate session-control signals
28
- if (lastResult.closeSession || lastResult.switchUser || lastResult.nextCwd) {
42
+ if (
43
+ lastResult.closeSession ||
44
+ lastResult.switchUser ||
45
+ lastResult.nextCwd
46
+ ) {
29
47
  break;
30
48
  }
31
49
  }
@@ -48,7 +66,15 @@ export async function executeStatements(
48
66
 
49
67
  while (i < statements.length) {
50
68
  const stmt = statements[i]!;
51
- last = await executePipeline(stmt.pipeline, authUser, hostname, mode, cwd, shell, env);
69
+ last = await executePipeline(
70
+ stmt.pipeline,
71
+ authUser,
72
+ hostname,
73
+ mode,
74
+ cwd,
75
+ shell,
76
+ env,
77
+ );
52
78
  env.lastExitCode = last.exitCode ?? 0;
53
79
 
54
80
  if (last.closeSession || last.switchUser) return last;
@@ -83,7 +109,8 @@ export async function executePipeline(
83
109
  shell: VirtualShell,
84
110
  env?: ShellEnv,
85
111
  ): Promise<CommandResult> {
86
- if (!pipeline.isValid) return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
112
+ if (!pipeline.isValid)
113
+ return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
87
114
  if (pipeline.commands.length === 0) return { exitCode: 0 };
88
115
 
89
116
  const shellEnv: ShellEnv = env ?? { vars: {}, lastExitCode: 0 };
@@ -91,13 +118,23 @@ export async function executePipeline(
91
118
  if (pipeline.commands.length === 1) {
92
119
  return executeSingleCommandWithRedirections(
93
120
  pipeline.commands[0] as PipelineCommand,
94
- authUser, hostname, mode, cwd, shell, shellEnv,
121
+ authUser,
122
+ hostname,
123
+ mode,
124
+ cwd,
125
+ shell,
126
+ shellEnv,
95
127
  );
96
128
  }
97
129
 
98
130
  return executePipelineChain(
99
131
  pipeline.commands as PipelineCommand[],
100
- authUser, hostname, mode, cwd, shell, shellEnv,
132
+ authUser,
133
+ hostname,
134
+ mode,
135
+ cwd,
136
+ shell,
137
+ shellEnv,
101
138
  );
102
139
  }
103
140
 
@@ -113,25 +150,51 @@ async function executeSingleCommandWithRedirections(
113
150
  let stdin: string | undefined;
114
151
  if (cmd.inputFile) {
115
152
  const inputPath = resolvePath(cwd, cmd.inputFile);
116
- try { stdin = shell.vfs.readFile(inputPath); }
117
- catch { return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 }; }
153
+ try {
154
+ stdin = shell.vfs.readFile(inputPath);
155
+ } catch {
156
+ return {
157
+ stderr: `${cmd.inputFile}: No such file or directory`,
158
+ exitCode: 1,
159
+ };
160
+ }
118
161
  }
119
162
 
120
- const result = await runCommandDirect(cmd.name, cmd.args, authUser, hostname, mode, cwd, shell, stdin, env);
163
+ const result = await runCommandDirect(
164
+ cmd.name,
165
+ cmd.args,
166
+ authUser,
167
+ hostname,
168
+ mode,
169
+ cwd,
170
+ shell,
171
+ stdin,
172
+ env,
173
+ );
121
174
 
122
175
  if (cmd.outputFile) {
123
176
  const outputPath = resolvePath(cwd, cmd.outputFile);
124
177
  const output = result.stdout || "";
125
178
  try {
126
179
  if (cmd.appendOutput) {
127
- const existing = (() => { try { return shell.vfs.readFile(outputPath); } catch { return ""; } })();
180
+ const existing = (() => {
181
+ try {
182
+ return shell.vfs.readFile(outputPath);
183
+ } catch {
184
+ return "";
185
+ }
186
+ })();
128
187
  shell.writeFileAsUser(authUser, outputPath, existing + output);
129
188
  } else {
130
189
  shell.writeFileAsUser(authUser, outputPath, output);
131
190
  }
132
191
  return { ...result, stdout: "" };
133
192
  } catch {
134
- return { ...result, stderr: `Failed to write to ${cmd.outputFile}`, exitCode: 1 };
193
+ return {
194
+ ...result,
195
+ stderr: `Failed to write to ${cmd.outputFile}`,
196
+ exitCode: 1,
197
+ };
135
198
  }
136
199
  }
137
200
 
@@ -155,11 +218,27 @@ async function executePipelineChain(
155
218
 
156
219
  if (i === 0 && cmd.inputFile) {
157
220
  const inputPath = resolvePath(cwd, cmd.inputFile);
158
- try { currentOutput = shell.vfs.readFile(inputPath); }
159
- catch { return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 }; }
221
+ try {
222
+ currentOutput = shell.vfs.readFile(inputPath);
223
+ } catch {
224
+ return {
225
+ stderr: `${cmd.inputFile}: No such file or directory`,
226
+ exitCode: 1,
227
+ };
228
+ }
160
229
  }
161
230
 
162
- const result = await runCommandDirect(cmd.name, cmd.args, authUser, hostname, mode, cwd, shell, currentOutput, env);
231
+ const result = await runCommandDirect(
232
+ cmd.name,
233
+ cmd.args,
234
+ authUser,
235
+ hostname,
236
+ mode,
237
+ cwd,
238
+ shell,
239
+ currentOutput,
240
+ env,
241
+ );
163
242
  exitCode = result.exitCode ?? 0;
164
243
 
165
244
  if (i === commands.length - 1 && cmd.outputFile) {
@@ -167,7 +246,13 @@ async function executePipelineChain(
167
246
  const output = result.stdout || "";
168
247
  try {
169
248
  if (cmd.appendOutput) {
170
- const existing = (() => { try { return shell.vfs.readFile(outputPath); } catch { return ""; } })();
249
+ const existing = (() => {
250
+ try {
251
+ return shell.vfs.readFile(outputPath);
252
+ } catch {
253
+ return "";
254
+ }
255
+ })();
171
256
  shell.writeFileAsUser(authUser, outputPath, existing + output);
172
257
  } else {
173
258
  shell.writeFileAsUser(authUser, outputPath, output);
@@ -180,7 +265,8 @@ async function executePipelineChain(
180
265
  currentOutput = result.stdout || "";
181
266
  }
182
267
 
183
- if (result.stderr && exitCode !== 0) return { stderr: result.stderr, exitCode };
268
+ if (result.stderr && exitCode !== 0)
269
+ return { stderr: result.stderr, exitCode };
184
270
  if (result.closeSession || result.switchUser) return result;
185
271
  }
186
272
 
@@ -140,7 +140,11 @@ class SshMimic extends EventEmitter {
140
140
 
141
141
  // Rate-limit check
142
142
  if (this.isLockedOut(remoteAddress)) {
143
- this.emit("auth:failure", { username: candidateUser, remoteAddress, reason: "lockout" });
143
+ this.emit("auth:failure", {
144
+ username: candidateUser,
145
+ remoteAddress,
146
+ reason: "lockout",
147
+ });
144
148
  ctx.reject();
145
149
  return;
146
150
  }
@@ -152,7 +156,10 @@ class SshMimic extends EventEmitter {
152
156
  `User ${candidateUser} has no password set, allowing login without verification`,
153
157
  );
154
158
  authUser = candidateUser;
155
- sessionId = shell.users.registerSession(authUser, remoteAddress).id;
159
+ sessionId = shell.users.registerSession(
160
+ authUser,
161
+ remoteAddress,
162
+ ).id;
156
163
  this.recordSuccess(remoteAddress);
157
164
  this.emit("auth:success", { username: authUser, remoteAddress });
158
165
  this.ensureHomeDir(authUser);
@@ -166,7 +173,10 @@ class SshMimic extends EventEmitter {
166
173
  !shell.users.verifyPassword(candidateUser, ctx.password)
167
174
  ) {
168
175
  this.recordFailure(remoteAddress);
169
- this.emit("auth:failure", { username: candidateUser, remoteAddress });
176
+ this.emit("auth:failure", {
177
+ username: candidateUser,
178
+ remoteAddress,
179
+ });
170
180
  ctx.reject();
171
181
  return;
172
182
  }
@@ -192,13 +202,16 @@ class SshMimic extends EventEmitter {
192
202
  const incomingKey = ctx.key;
193
203
  const keyMatches = authorizedKeys.some(
194
204
  (k) =>
195
- k.algo === incomingKey.algo &&
196
- k.data.equals(incomingKey.data),
205
+ k.algo === incomingKey.algo && k.data.equals(incomingKey.data),
197
206
  );
198
207
 
199
208
  if (!keyMatches) {
200
209
  this.recordFailure(remoteAddress);
201
- this.emit("auth:failure", { username: candidateUser, remoteAddress, method: "publickey" });
210
+ this.emit("auth:failure", {
211
+ username: candidateUser,
212
+ remoteAddress,
213
+ method: "publickey",
214
+ });
202
215
  ctx.reject();
203
216
  return;
204
217
  }
@@ -206,9 +219,16 @@ class SshMimic extends EventEmitter {
206
219
  // Key matched — if this is a signature check step, accept
207
220
  if (ctx.signature) {
208
221
  authUser = candidateUser;
209
- sessionId = shell.users.registerSession(authUser, remoteAddress).id;
222
+ sessionId = shell.users.registerSession(
223
+ authUser,
224
+ remoteAddress,
225
+ ).id;
210
226
  this.recordSuccess(remoteAddress);
211
- this.emit("auth:success", { username: authUser, remoteAddress, method: "publickey" });
227
+ this.emit("auth:success", {
228
+ username: authUser,
229
+ remoteAddress,
230
+ method: "publickey",
231
+ });
212
232
  this.ensureHomeDir(authUser);
213
233
  ctx.accept();
214
234
  } else {
@@ -238,20 +258,35 @@ class SshMimic extends EventEmitter {
238
258
  acceptPty();
239
259
  });
240
260
 
241
- session.on("window-change", (_acceptChange, _rejectChange, info) => {
242
- terminalSize.cols = info?.cols ?? terminalSize.cols;
243
- terminalSize.rows = info?.rows ?? terminalSize.rows;
244
- });
261
+ session.on(
262
+ "window-change",
263
+ (_acceptChange, _rejectChange, info) => {
264
+ terminalSize.cols = info?.cols ?? terminalSize.cols;
265
+ terminalSize.rows = info?.rows ?? terminalSize.rows;
266
+ },
267
+ );
245
268
 
246
269
  session.on("shell", (acceptShell) => {
247
270
  const stream = acceptShell();
248
- shell?.startInteractiveSession(stream, authUser, sessionId, remoteAddress, terminalSize);
271
+ shell?.startInteractiveSession(
272
+ stream,
273
+ authUser,
274
+ sessionId,
275
+ remoteAddress,
276
+ terminalSize,
277
+ );
249
278
  });
250
279
 
251
280
  session.on("exec", (acceptExec, _rejectExec, info) => {
252
281
  const stream = acceptExec();
253
282
  if (stream) {
254
- runExec(stream, info.command.trim(), authUser, shell.hostname, shell);
283
+ runExec(
284
+ stream,
285
+ info.command.trim(),
286
+ authUser,
287
+ shell.hostname,
288
+ shell,
289
+ );
255
290
  }
256
291
  });
257
292
  });
@@ -293,4 +328,3 @@ class SshMimic extends EventEmitter {
293
328
 
294
329
  export { SftpMimic } from "./sftp";
295
330
  export { SshMimic };
296
-