typescript-virtual-container 1.5.11 → 1.6.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 (132) hide show
  1. package/README.md +236 -456
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/Honeypot/index.d.ts +9 -0
  4. package/dist/Honeypot/index.js +57 -0
  5. package/dist/SSHMimic/exec.d.ts +4 -0
  6. package/dist/SSHMimic/exec.js +4 -0
  7. package/dist/SSHMimic/executor.d.ts +10 -1
  8. package/dist/SSHMimic/executor.js +18 -8
  9. package/dist/SSHMimic/hostKey.d.ts +5 -0
  10. package/dist/SSHMimic/hostKey.js +5 -0
  11. package/dist/SSHMimic/loginBanner.d.ts +7 -0
  12. package/dist/SSHMimic/loginBanner.js +4 -0
  13. package/dist/SSHMimic/loginFormat.d.ts +4 -0
  14. package/dist/SSHMimic/loginFormat.js +4 -0
  15. package/dist/SSHMimic/prompt.d.ts +9 -0
  16. package/dist/SSHMimic/prompt.js +9 -0
  17. package/dist/SSHMimic/scp.d.ts +18 -0
  18. package/dist/SSHMimic/scp.js +14 -0
  19. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -3
  20. package/dist/VirtualFileSystem/binaryPack.js +32 -10
  21. package/dist/VirtualFileSystem/index.d.ts +29 -0
  22. package/dist/VirtualFileSystem/index.js +126 -5
  23. package/dist/VirtualFileSystem/internalTypes.d.ts +4 -0
  24. package/dist/VirtualFileSystem/journal.d.ts +10 -4
  25. package/dist/VirtualFileSystem/journal.js +12 -2
  26. package/dist/VirtualFileSystem/path.d.ts +23 -1
  27. package/dist/VirtualFileSystem/path.js +23 -3
  28. package/dist/VirtualPackageManager/index.js +1 -1
  29. package/dist/VirtualShell/index.d.ts +3 -0
  30. package/dist/VirtualShell/index.js +12 -3
  31. package/dist/VirtualUserManager/index.d.ts +20 -1
  32. package/dist/VirtualUserManager/index.js +52 -15
  33. package/dist/commands/bc.d.ts +5 -0
  34. package/dist/commands/bc.js +5 -0
  35. package/dist/commands/cat.js +2 -2
  36. package/dist/commands/chgrp.d.ts +7 -0
  37. package/dist/commands/chgrp.js +42 -0
  38. package/dist/commands/chown.d.ts +7 -0
  39. package/dist/commands/chown.js +79 -0
  40. package/dist/commands/cp.js +4 -3
  41. package/dist/commands/dd.d.ts +7 -0
  42. package/dist/commands/dd.js +60 -0
  43. package/dist/commands/declare.js +0 -2
  44. package/dist/commands/expr.d.ts +7 -0
  45. package/dist/commands/expr.js +63 -0
  46. package/dist/commands/fun.d.ts +5 -0
  47. package/dist/commands/fun.js +5 -1
  48. package/dist/commands/help.d.ts +5 -0
  49. package/dist/commands/help.js +5 -0
  50. package/dist/commands/helpers.d.ts +43 -0
  51. package/dist/commands/helpers.js +61 -0
  52. package/dist/commands/id.d.ts +5 -0
  53. package/dist/commands/id.js +5 -0
  54. package/dist/commands/ip.d.ts +1 -0
  55. package/dist/commands/ip.js +50 -23
  56. package/dist/commands/jobs.js +43 -9
  57. package/dist/commands/kill.d.ts +1 -0
  58. package/dist/commands/kill.js +13 -5
  59. package/dist/commands/last.js +1 -1
  60. package/dist/commands/ln.d.ts +5 -0
  61. package/dist/commands/ln.js +5 -0
  62. package/dist/commands/ls.d.ts +5 -0
  63. package/dist/commands/ls.js +19 -4
  64. package/dist/commands/lsb-release.js +1 -1
  65. package/dist/commands/man.d.ts +5 -0
  66. package/dist/commands/man.js +5 -0
  67. package/dist/commands/manuals-bundle.js +242 -0
  68. package/dist/commands/miscutils.d.ts +43 -0
  69. package/dist/commands/miscutils.js +233 -0
  70. package/dist/commands/mkdir.js +3 -2
  71. package/dist/commands/mv.js +4 -3
  72. package/dist/commands/netcat.d.ts +7 -0
  73. package/dist/commands/netcat.js +64 -0
  74. package/dist/commands/nice.d.ts +7 -0
  75. package/dist/commands/nice.js +22 -0
  76. package/dist/commands/nohup.d.ts +7 -0
  77. package/dist/commands/nohup.js +18 -0
  78. package/dist/commands/ping.d.ts +2 -1
  79. package/dist/commands/ping.js +46 -8
  80. package/dist/commands/procUtils.d.ts +13 -0
  81. package/dist/commands/procUtils.js +72 -0
  82. package/dist/commands/pwd.d.ts +5 -0
  83. package/dist/commands/pwd.js +5 -0
  84. package/dist/commands/python.js +0 -4
  85. package/dist/commands/read.js +0 -1
  86. package/dist/commands/registry.d.ts +37 -0
  87. package/dist/commands/registry.js +73 -0
  88. package/dist/commands/rm.js +3 -2
  89. package/dist/commands/runtime.d.ts +47 -1
  90. package/dist/commands/runtime.js +60 -5
  91. package/dist/commands/sh.d.ts +5 -0
  92. package/dist/commands/sh.js +5 -0
  93. package/dist/commands/stat.js +3 -2
  94. package/dist/commands/strace.js +0 -1
  95. package/dist/commands/sysinfo.d.ts +19 -0
  96. package/dist/commands/sysinfo.js +73 -0
  97. package/dist/commands/test.d.ts +5 -0
  98. package/dist/commands/test.js +5 -0
  99. package/dist/commands/textutils.d.ts +25 -0
  100. package/dist/commands/textutils.js +171 -0
  101. package/dist/commands/top.d.ts +7 -0
  102. package/dist/commands/top.js +54 -0
  103. package/dist/commands/touch.js +6 -2
  104. package/dist/commands/tr.d.ts +5 -0
  105. package/dist/commands/tr.js +5 -0
  106. package/dist/commands/w.js +1 -1
  107. package/dist/commands/which.d.ts +5 -0
  108. package/dist/commands/which.js +5 -0
  109. package/dist/index.d.ts +10 -2
  110. package/dist/index.js +4 -0
  111. package/dist/modules/VirtualNetworkManager.d.ts +54 -0
  112. package/dist/modules/VirtualNetworkManager.js +144 -0
  113. package/dist/modules/linuxRootfs.d.ts +4 -3
  114. package/dist/modules/linuxRootfs.js +115 -74
  115. package/dist/modules/neofetch.d.ts +2 -0
  116. package/dist/modules/neofetch.js +3 -2
  117. package/dist/modules/pacmanGame.d.ts +2 -0
  118. package/dist/modules/pacmanGame.js +1 -0
  119. package/dist/modules/shellInteractive.d.ts +2 -0
  120. package/dist/modules/shellInteractive.js +2 -0
  121. package/dist/modules/shellRuntime.d.ts +7 -0
  122. package/dist/modules/shellRuntime.js +6 -0
  123. package/dist/modules/webTermRenderer.js +0 -7
  124. package/dist/types/commands.d.ts +1 -1
  125. package/dist/types/vfs.d.ts +8 -0
  126. package/dist/utils/argv.d.ts +22 -3
  127. package/dist/utils/argv.js +22 -3
  128. package/dist/utils/perfLogger.d.ts +10 -2
  129. package/dist/utils/perfLogger.js +7 -14
  130. package/dist/utils/shellSession.d.ts +35 -0
  131. package/dist/utils/shellSession.js +35 -0
  132. package/package.json +1 -1
@@ -50,6 +50,12 @@ class VirtualFileSystem extends EventEmitter {
50
50
  mounts = new Map();
51
51
  /** Sorted mounts cache (longest-path-first). Rebuilt lazily on mount/unmount. */
52
52
  _sortedMounts = null;
53
+ /** Read hooks: path prefix → callback invoked before reading any file under that prefix. */
54
+ readHooks = new Map();
55
+ /** Sorted read hook prefixes (longest-first) for matching. */
56
+ _sortedReadHooks = null;
57
+ /** Re-entrancy guard for read hooks — prevents infinite loop when hook triggers another read. */
58
+ _inReadHook = false;
53
59
  /** True when running in a browser environment (no host FS access). */
54
60
  static isBrowser = typeof process === "undefined" || typeof process.versions?.node === "undefined";
55
61
  constructor(options = {}) {
@@ -85,12 +91,14 @@ class VirtualFileSystem extends EventEmitter {
85
91
  this.root = this.makeDir("", 0o755);
86
92
  }
87
93
  // ── Internal helpers ──────────────────────────────────────────────────────
88
- makeDir(name, mode) {
94
+ makeDir(name, mode, uid = 0, gid = 0) {
89
95
  const now = Date.now();
90
96
  return {
91
97
  type: "directory",
92
98
  name,
93
99
  mode,
100
+ uid,
101
+ gid,
94
102
  createdAt: now,
95
103
  updatedAt: now,
96
104
  children: Object.create(null),
@@ -98,21 +106,23 @@ class VirtualFileSystem extends EventEmitter {
98
106
  _sortedKeys: null,
99
107
  };
100
108
  }
101
- makeFile(name, content, mode, compressed) {
109
+ makeFile(name, content, mode, compressed, uid = 0, gid = 0) {
102
110
  const now = Date.now();
103
111
  return {
104
112
  type: "file",
105
113
  name,
106
114
  content,
107
115
  mode,
116
+ uid,
117
+ gid,
108
118
  compressed,
109
119
  createdAt: now,
110
120
  updatedAt: now,
111
121
  };
112
122
  }
113
- makeStub(name, content, mode) {
123
+ makeStub(name, content, mode, uid = 0, gid = 0) {
114
124
  const now = Date.now();
115
- return { type: "stub", name, stubContent: content, mode, createdAt: now, updatedAt: now };
125
+ return { type: "stub", name, stubContent: content, mode, uid, gid, createdAt: now, updatedAt: now };
116
126
  }
117
127
  /**
118
128
  * Write a lazy stub — stores content as a plain string with no Buffer allocation.
@@ -520,6 +530,43 @@ class VirtualFileSystem extends EventEmitter {
520
530
  vPath, ...opts,
521
531
  }));
522
532
  }
533
+ /**
534
+ * Register a callback that is invoked before any read under `prefix`.
535
+ * Used by /proc to refresh dynamic content on every access.
536
+ */
537
+ onBeforeRead(prefix, cb) {
538
+ const normalized = normalizePath(prefix);
539
+ this.readHooks.set(normalized, cb);
540
+ this._sortedReadHooks = [...this.readHooks.keys()].sort((a, b) => b.length - a.length);
541
+ }
542
+ /** Remove a previously registered read hook. */
543
+ offBeforeRead(prefix) {
544
+ const normalized = normalizePath(prefix);
545
+ this.readHooks.delete(normalized);
546
+ this._sortedReadHooks = [...this.readHooks.keys()].sort((a, b) => b.length - a.length);
547
+ }
548
+ /** Invoke any matching read hook for `normalizedPath`. */
549
+ _triggerReadHook(normalizedPath) {
550
+ if (this._inReadHook)
551
+ return;
552
+ if (!this._sortedReadHooks)
553
+ return;
554
+ for (const prefix of this._sortedReadHooks) {
555
+ if (normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`)) {
556
+ const cb = this.readHooks.get(prefix);
557
+ if (cb) {
558
+ this._inReadHook = true;
559
+ try {
560
+ cb();
561
+ }
562
+ finally {
563
+ this._inReadHook = false;
564
+ }
565
+ return;
566
+ }
567
+ }
568
+ }
569
+ }
523
570
  /**
524
571
  * If `targetPath` is inside a mount, return `{ hostPath, readOnly, relPath }`.
525
572
  * `relPath` is the path relative to the mount's host directory.
@@ -614,6 +661,7 @@ class VirtualFileSystem extends EventEmitter {
614
661
  return fsSync.readFileSync(m.fullHostPath, "utf8");
615
662
  }
616
663
  const normalized = normalizePath(targetPath);
664
+ this._triggerReadHook(normalized);
617
665
  const node = getNodeNormalized(this.root, normalized);
618
666
  if (node.type === "stub") {
619
667
  this.emit("file:read", { path: normalized, size: node.stubContent.length });
@@ -638,6 +686,7 @@ class VirtualFileSystem extends EventEmitter {
638
686
  return fsSync.readFileSync(m.fullHostPath);
639
687
  }
640
688
  const normalized = normalizePath(targetPath);
689
+ this._triggerReadHook(normalized);
641
690
  const node = getNodeNormalized(this.root, normalized);
642
691
  if (node.type === "stub") {
643
692
  const buf = Buffer.from(node.stubContent, "utf8");
@@ -659,8 +708,9 @@ class VirtualFileSystem extends EventEmitter {
659
708
  const m = this.resolveMount(targetPath);
660
709
  if (m)
661
710
  return fsSync.existsSync(m.fullHostPath);
711
+ const normalized = normalizePath(targetPath);
662
712
  try {
663
- getNodeNormalized(this.root, normalizePath(targetPath));
713
+ getNodeNormalized(this.root, normalized);
664
714
  return true;
665
715
  }
666
716
  catch {
@@ -673,6 +723,51 @@ class VirtualFileSystem extends EventEmitter {
673
723
  getNodeNormalized(this.root, normalized).mode = mode;
674
724
  this._journal({ op: JournalOp.CHMOD, path: normalized, mode });
675
725
  }
726
+ /** Changes ownership (uid/gid) of a file or directory. */
727
+ chown(targetPath, uid, gid) {
728
+ const normalized = normalizePath(targetPath);
729
+ const node = getNodeNormalized(this.root, normalized);
730
+ node.uid = uid;
731
+ node.gid = gid;
732
+ this._journal({ op: JournalOp.CHMOD, path: normalized, mode: node.mode });
733
+ }
734
+ /** Returns the uid and gid of a node. */
735
+ getOwner(targetPath) {
736
+ const node = getNodeNormalized(this.root, normalizePath(targetPath));
737
+ return { uid: node.uid, gid: node.gid };
738
+ }
739
+ /**
740
+ * POSIX-style access check: does `uid`/`gid` have `want` permission on `targetPath`?
741
+ * `want` is a bitmask of R_OK (4), W_OK (2), X_OK (1).
742
+ * Root (uid === 0) is granted everything except X_OK without at least one x bit set.
743
+ * Returns true when access is granted.
744
+ */
745
+ checkAccess(targetPath, uid, gid, want) {
746
+ try {
747
+ const node = getNodeNormalized(this.root, normalizePath(targetPath));
748
+ const mode = node.mode;
749
+ // Root: allowed everything except execute (at least one x bit needed)
750
+ if (uid === 0) {
751
+ if (want & 1)
752
+ return (mode & 0o111) !== 0;
753
+ return true;
754
+ }
755
+ let perm = 0;
756
+ if (uid === node.uid) {
757
+ perm = (mode >> 6) & 7; // owner bits
758
+ }
759
+ else if (gid === node.gid) {
760
+ perm = (mode >> 3) & 7; // group bits
761
+ }
762
+ else {
763
+ perm = mode & 7; // other bits
764
+ }
765
+ return (perm & want) === want;
766
+ }
767
+ catch {
768
+ return false;
769
+ }
770
+ }
676
771
  /** Returns metadata for a file or directory. */
677
772
  stat(targetPath) {
678
773
  const m = this.resolveMount(targetPath);
@@ -688,6 +783,8 @@ class VirtualFileSystem extends EventEmitter {
688
783
  name,
689
784
  path: normalizePath(targetPath),
690
785
  mode: 0o755,
786
+ uid: 0,
787
+ gid: 0,
691
788
  createdAt: hst.birthtime,
692
789
  updatedAt: now,
693
790
  childrenCount: fsSync.readdirSync(m.fullHostPath).length,
@@ -698,6 +795,8 @@ class VirtualFileSystem extends EventEmitter {
698
795
  name,
699
796
  path: normalizePath(targetPath),
700
797
  mode: m.readOnly ? 0o444 : 0o644,
798
+ uid: 0,
799
+ gid: 0,
701
800
  createdAt: hst.birthtime,
702
801
  updatedAt: now,
703
802
  compressed: false,
@@ -705,6 +804,8 @@ class VirtualFileSystem extends EventEmitter {
705
804
  };
706
805
  }
707
806
  const normalized = normalizePath(targetPath);
807
+ if (normalized.startsWith("/proc"))
808
+ this._triggerReadHook(normalized);
708
809
  const node = getNodeNormalized(this.root, normalized);
709
810
  const name = normalized === "/" ? "" : path.posix.basename(normalized);
710
811
  if (node.type === "stub") {
@@ -714,6 +815,8 @@ class VirtualFileSystem extends EventEmitter {
714
815
  name,
715
816
  path: normalized,
716
817
  mode: s.mode,
818
+ uid: s.uid,
819
+ gid: s.gid,
717
820
  createdAt: new Date(s.createdAt),
718
821
  updatedAt: new Date(s.updatedAt),
719
822
  compressed: false,
@@ -727,6 +830,8 @@ class VirtualFileSystem extends EventEmitter {
727
830
  name,
728
831
  path: normalized,
729
832
  mode: f.mode,
833
+ uid: f.uid,
834
+ gid: f.gid,
730
835
  createdAt: new Date(f.createdAt),
731
836
  updatedAt: new Date(f.updatedAt),
732
837
  compressed: f.compressed,
@@ -739,6 +844,8 @@ class VirtualFileSystem extends EventEmitter {
739
844
  name,
740
845
  path: normalized,
741
846
  mode: d.mode,
847
+ uid: d.uid,
848
+ gid: d.gid,
742
849
  createdAt: new Date(d.createdAt),
743
850
  updatedAt: new Date(d.updatedAt),
744
851
  childrenCount: d._childCount,
@@ -778,6 +885,8 @@ class VirtualFileSystem extends EventEmitter {
778
885
  }
779
886
  }
780
887
  const normalized = normalizePath(dirPath);
888
+ if (normalized.startsWith("/proc"))
889
+ this._triggerReadHook(normalized);
781
890
  const node = getNodeNormalized(this.root, normalized);
782
891
  if (node.type !== "directory") {
783
892
  throw new Error(`Cannot list '${dirPath}': not a directory.`);
@@ -873,6 +982,8 @@ class VirtualFileSystem extends EventEmitter {
873
982
  name,
874
983
  content: Buffer.from(normalizedTarget, "utf8"),
875
984
  mode: 0o120777,
985
+ uid: 0,
986
+ gid: 0,
876
987
  compressed: false,
877
988
  createdAt: Date.now(),
878
989
  updatedAt: Date.now(),
@@ -997,6 +1108,8 @@ class VirtualFileSystem extends EventEmitter {
997
1108
  type: "file",
998
1109
  name: child.name,
999
1110
  mode: child.mode,
1111
+ uid: child.uid,
1112
+ gid: child.gid,
1000
1113
  createdAt: new Date(child.createdAt).toISOString(),
1001
1114
  updatedAt: new Date(child.updatedAt).toISOString(),
1002
1115
  compressed: false,
@@ -1014,6 +1127,8 @@ class VirtualFileSystem extends EventEmitter {
1014
1127
  type: "directory",
1015
1128
  name: dir.name,
1016
1129
  mode: dir.mode,
1130
+ uid: dir.uid,
1131
+ gid: dir.gid,
1017
1132
  createdAt: new Date(dir.createdAt).toISOString(),
1018
1133
  updatedAt: new Date(dir.updatedAt).toISOString(),
1019
1134
  children,
@@ -1024,6 +1139,8 @@ class VirtualFileSystem extends EventEmitter {
1024
1139
  type: "file",
1025
1140
  name: file.name,
1026
1141
  mode: file.mode,
1142
+ uid: file.uid,
1143
+ gid: file.gid,
1027
1144
  createdAt: new Date(file.createdAt).toISOString(),
1028
1145
  updatedAt: new Date(file.updatedAt).toISOString(),
1029
1146
  compressed: file.compressed,
@@ -1061,6 +1178,8 @@ class VirtualFileSystem extends EventEmitter {
1061
1178
  type: "directory",
1062
1179
  name,
1063
1180
  mode: snap.mode,
1181
+ uid: snap.uid ?? 0,
1182
+ gid: snap.gid ?? 0,
1064
1183
  createdAt: Date.parse(snap.createdAt),
1065
1184
  updatedAt: Date.parse(snap.updatedAt),
1066
1185
  children: Object.create(null),
@@ -1074,6 +1193,8 @@ class VirtualFileSystem extends EventEmitter {
1074
1193
  type: "file",
1075
1194
  name: f.name,
1076
1195
  mode: f.mode,
1196
+ uid: f.uid ?? 0,
1197
+ gid: f.gid ?? 0,
1077
1198
  createdAt: Date.parse(f.createdAt),
1078
1199
  updatedAt: Date.parse(f.updatedAt),
1079
1200
  compressed: f.compressed,
@@ -3,6 +3,10 @@ export type InternalNode = InternalFileNode | InternalStubNode | InternalDirecto
3
3
  interface InternalBaseNode {
4
4
  name: string;
5
5
  mode: number;
6
+ /** Owner user ID (0 = root). */
7
+ uid: number;
8
+ /** Owner group ID (0 = root). */
9
+ gid: number;
6
10
  /** Unix timestamp in ms — avoids Date object overhead (~80 bytes each). */
7
11
  createdAt: number;
8
12
  /** Unix timestamp in ms. */
@@ -18,6 +18,16 @@
18
18
  * 0x06 SYMLINK — [2B target_len] [target bytes]
19
19
  */
20
20
  /** biome-ignore-all lint/style/useNamingConvention: JournalOp modes */
21
+ /**
22
+ * Journal operation type codes used in VFS write-ahead log entries.
23
+ *
24
+ * - `WRITE` — Write file content and mode.
25
+ * - `MKDIR` — Create a directory.
26
+ * - `REMOVE` — Delete a file or directory.
27
+ * - `CHMOD` — Change file mode.
28
+ * - `MOVE` — Rename / move a node.
29
+ * - `SYMLINK`— Create a symbolic link.
30
+ */
21
31
  export declare const JournalOp: {
22
32
  readonly WRITE: 1;
23
33
  readonly MKDIR: 2;
@@ -35,10 +45,6 @@ export interface JournalEntry {
35
45
  mode?: number;
36
46
  dest?: string;
37
47
  }
38
- /** Serialise one entry to a Buffer. */
39
- export declare function encodeEntry(e: JournalEntry): Buffer;
40
- /** Parse all entries from a journal Buffer. Returns empty array on corrupt data. */
41
- export declare function decodeJournal(buf: Buffer): JournalEntry[];
42
48
  /** Append a single entry to the journal file (O_APPEND, atomic write). */
43
49
  export declare function appendJournalEntry(journalPath: string, entry: JournalEntry): void;
44
50
  /** Read and decode all entries from a journal file. Returns [] if file is absent/empty. */
@@ -19,6 +19,16 @@
19
19
  */
20
20
  /** biome-ignore-all lint/style/useNamingConvention: JournalOp modes */
21
21
  import * as fsSync from "node:fs";
22
+ /**
23
+ * Journal operation type codes used in VFS write-ahead log entries.
24
+ *
25
+ * - `WRITE` — Write file content and mode.
26
+ * - `MKDIR` — Create a directory.
27
+ * - `REMOVE` — Delete a file or directory.
28
+ * - `CHMOD` — Change file mode.
29
+ * - `MOVE` — Rename / move a node.
30
+ * - `SYMLINK`— Create a symbolic link.
31
+ */
22
32
  export const JournalOp = {
23
33
  WRITE: 0x01,
24
34
  MKDIR: 0x02,
@@ -35,7 +45,7 @@ function writeString2(buf, offset, s) {
35
45
  return 2 + b.length;
36
46
  }
37
47
  /** Serialise one entry to a Buffer. */
38
- export function encodeEntry(e) {
48
+ function encodeEntry(e) {
39
49
  const pathBuf = Buffer.from(e.path, ENC);
40
50
  let payloadLen = 0;
41
51
  if (e.op === JournalOp.WRITE) {
@@ -84,7 +94,7 @@ export function encodeEntry(e) {
84
94
  return buf;
85
95
  }
86
96
  /** Parse all entries from a journal Buffer. Returns empty array on corrupt data. */
87
- export function decodeJournal(buf) {
97
+ function decodeJournal(buf) {
88
98
  const entries = [];
89
99
  let off = 0;
90
100
  try {
@@ -1,12 +1,34 @@
1
1
  import type { InternalDirectoryNode, InternalNode } from "./internalTypes";
2
+ /**
3
+ * Normalizes a VFS path by resolving `.` and `..` segments and removing trailing slash.
4
+ * Empty or blank input returns `"/"`.
5
+ * @param rawPath - The path to normalize (may be relative — leading `/` is added if missing).
6
+ * @returns The normalized absolute POSIX path.
7
+ */
2
8
  export declare function normalizePath(rawPath: string): string;
3
- export declare function splitPath(normalizedPath: string): string[];
9
+ /**
10
+ * Retrieves a node from the VFS tree by path string. Normalizes the path first.
11
+ * @param root - The root directory node of the virtual filesystem.
12
+ * @param targetPath - The path to look up (e.g. `"/usr/bin"`).
13
+ * @returns The internal node at the given path.
14
+ * @throws If any path component does not exist or an intermediate entry is not a directory.
15
+ */
4
16
  export declare function getNode(root: InternalDirectoryNode, targetPath: string): InternalNode;
5
17
  /**
6
18
  * Like getNode but skips normalization — caller must pass an already-normalized path.
7
19
  * Avoids double normalizePath() when the caller has already normalized.
8
20
  */
9
21
  export declare function getNodeNormalized(root: InternalDirectoryNode, normalized: string): InternalNode;
22
+ /**
23
+ * Resolves a path to its parent directory node and the final path component name.
24
+ * Optionally creates intermediate directories when `createIfMissing` is set.
25
+ * @param root - The root directory node of the virtual filesystem.
26
+ * @param targetPath - The path whose parent to resolve.
27
+ * @param createIfMissing - If true, missing parent directories are created via `createPath`.
28
+ * @param createPath - Callback invoked with each missing directory path to create it.
29
+ * @returns An object with the parent directory node and the final component name.
30
+ * @throws If the path is root or the resolved parent is not a directory.
31
+ */
10
32
  export declare function getParentDirectory(root: InternalDirectoryNode, targetPath: string, createIfMissing: boolean, createPath: (pathToCreate: string) => void): {
11
33
  parent: InternalDirectoryNode;
12
34
  name: string;
@@ -1,4 +1,10 @@
1
1
  import * as path from "node:path";
2
+ /**
3
+ * Normalizes a VFS path by resolving `.` and `..` segments and removing trailing slash.
4
+ * Empty or blank input returns `"/"`.
5
+ * @param rawPath - The path to normalize (may be relative — leading `/` is added if missing).
6
+ * @returns The normalized absolute POSIX path.
7
+ */
2
8
  export function normalizePath(rawPath) {
3
9
  if (!rawPath || rawPath.trim() === "") {
4
10
  return "/";
@@ -6,9 +12,13 @@ export function normalizePath(rawPath) {
6
12
  const normalized = path.posix.normalize(rawPath.startsWith("/") ? rawPath : `/${rawPath}`);
7
13
  return normalized === "" ? "/" : normalized;
8
14
  }
9
- export function splitPath(normalizedPath) {
10
- return normalizedPath.split("/").filter(Boolean);
11
- }
15
+ /**
16
+ * Retrieves a node from the VFS tree by path string. Normalizes the path first.
17
+ * @param root - The root directory node of the virtual filesystem.
18
+ * @param targetPath - The path to look up (e.g. `"/usr/bin"`).
19
+ * @returns The internal node at the given path.
20
+ * @throws If any path component does not exist or an intermediate entry is not a directory.
21
+ */
12
22
  export function getNode(root, targetPath) {
13
23
  const normalized = normalizePath(targetPath);
14
24
  return getNodeNormalized(root, normalized);
@@ -41,6 +51,16 @@ export function getNodeNormalized(root, normalized) {
41
51
  }
42
52
  return current;
43
53
  }
54
+ /**
55
+ * Resolves a path to its parent directory node and the final path component name.
56
+ * Optionally creates intermediate directories when `createIfMissing` is set.
57
+ * @param root - The root directory node of the virtual filesystem.
58
+ * @param targetPath - The path whose parent to resolve.
59
+ * @param createIfMissing - If true, missing parent directories are created via `createPath`.
60
+ * @param createPath - Callback invoked with each missing directory path to create it.
61
+ * @returns An object with the parent directory node and the final component name.
62
+ * @throws If the path is root or the resolved parent is not a directory.
63
+ */
44
64
  export function getParentDirectory(root, targetPath, createIfMissing, createPath) {
45
65
  const normalized = normalizePath(targetPath);
46
66
  if (normalized === "/") {
@@ -819,7 +819,7 @@ export class VirtualPackageManager {
819
819
  if (this.vfs.exists(filePath))
820
820
  this.vfs.remove(filePath);
821
821
  }
822
- catch { }
822
+ catch { /* best-effort cleanup */ }
823
823
  }
824
824
  // Run remove hook
825
825
  const def = this.findInRegistry(pkg.name);
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from "node:events";
2
+ import { VirtualNetworkManager } from "../modules/VirtualNetworkManager";
2
3
  import type { CommandContext, CommandResult } from "../types/commands";
3
4
  import type { ShellStream } from "../types/streams";
4
5
  import type { VfsNodeStats } from "../types/vfs";
@@ -88,6 +89,8 @@ declare class VirtualShell extends EventEmitter {
88
89
  users: VirtualUserManager;
89
90
  /** APT/dpkg package manager backed by the built-in package registry. */
90
91
  packageManager: VirtualPackageManager;
92
+ /** Virtual network stack with interfaces, routes, and ARP cache. */
93
+ network: VirtualNetworkManager;
91
94
  /** Hostname shown in the shell prompt and SSH ident string. */
92
95
  hostname: string;
93
96
  /** Distro identity strings surfaced by `uname`, `neofetch`, etc. */
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
2
2
  import { createCustomCommand, registerCommand } from "../commands/registry";
3
3
  import { runCommand } from "../commands/runtime";
4
4
  import { bootstrapLinuxRootfs, refreshProc, syncEtcPasswd, } from "../modules/linuxRootfs";
5
+ import { VirtualNetworkManager } from "../modules/VirtualNetworkManager";
5
6
  import { createPerfLogger } from "../utils/perfLogger";
6
7
  import VirtualFileSystem, {} from "../VirtualFileSystem";
7
8
  import { VirtualPackageManager } from "../VirtualPackageManager";
@@ -70,6 +71,8 @@ class VirtualShell extends EventEmitter {
70
71
  users;
71
72
  /** APT/dpkg package manager backed by the built-in package registry. */
72
73
  packageManager;
74
+ /** Virtual network stack with interfaces, routes, and ARP cache. */
75
+ network;
73
76
  /** Hostname shown in the shell prompt and SSH ident string. */
74
77
  hostname;
75
78
  /** Distro identity strings surfaced by `uname`, `neofetch`, etc. */
@@ -103,18 +106,24 @@ class VirtualShell extends EventEmitter {
103
106
  }
104
107
  this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
105
108
  this.packageManager = new VirtualPackageManager(this.vfs, this.users);
109
+ this.network = new VirtualNetworkManager();
106
110
  // Store references to avoid TypeScript "used before assigned" errors
107
111
  const vfs = this.vfs;
108
112
  const users = this.users;
109
113
  const shellProps = this.properties;
110
114
  const shellHostname = this.hostname;
111
115
  const startTime = this.startTime;
116
+ const network = this.network;
112
117
  // Initialize both VFS mirror and users, ensuring all is ready before auth
113
118
  this.initialized = (async () => {
114
119
  await vfs.restoreMirror();
115
120
  await users.initialize();
116
121
  // Bootstrap Linux rootfs (idempotent)
117
- bootstrapLinuxRootfs(vfs, users, shellHostname, shellProps, startTime);
122
+ bootstrapLinuxRootfs(vfs, users, shellHostname, shellProps, startTime, [], network);
123
+ // Register read hook: refresh /proc dynamically on every access
124
+ vfs.onBeforeRead("/proc", () => {
125
+ refreshProc(vfs, shellProps, shellHostname, startTime, users.listActiveSessions(), network);
126
+ });
118
127
  this.emit("initialized");
119
128
  })();
120
129
  }
@@ -195,7 +204,7 @@ class VirtualShell extends EventEmitter {
195
204
  * reading `/proc` files for up-to-date values.
196
205
  */
197
206
  refreshProcFs() {
198
- refreshProc(this.vfs, this.properties, this.hostname, this.startTime, this.users.listActiveSessions());
207
+ refreshProc(this.vfs, this.properties, this.hostname, this.startTime, this.users.listActiveSessions(), this.network);
199
208
  }
200
209
  /**
201
210
  * Mount a host directory into the VFS at `vPath`.
@@ -232,7 +241,7 @@ class VirtualShell extends EventEmitter {
232
241
  * whenever a session is registered or unregistered.
233
242
  */
234
243
  refreshProcSessions() {
235
- refreshProc(this.vfs, this.properties, this.hostname, this.startTime, this.users.listActiveSessions());
244
+ refreshProc(this.vfs, this.properties, this.hostname, this.startTime, this.users.listActiveSessions(), this.network);
236
245
  }
237
246
  /**
238
247
  * Syncs `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the current
@@ -7,11 +7,16 @@ import type VirtualFileSystem from "../VirtualFileSystem";
7
7
  export interface VirtualUserRecord {
8
8
  /** Unique login name. */
9
9
  username: string;
10
+ /** Numeric user ID. */
11
+ uid: number;
12
+ /** Primary group ID. */
13
+ gid: number;
10
14
  /** Per-user random salt used for password hashing. */
11
15
  salt: string;
12
16
  /** Scrypt-derived password hash in hex encoding. */
13
17
  passwordHash: string;
14
18
  }
19
+ export type ProcessStatus = "running" | "stopped" | "done";
15
20
  /** Runtime representation of a command currently executing in a session. */
16
21
  export interface VirtualProcess {
17
22
  /** Unique process identifier (auto-incremented). */
@@ -26,6 +31,10 @@ export interface VirtualProcess {
26
31
  tty: string;
27
32
  /** ISO-8601 start timestamp. */
28
33
  startedAt: string;
34
+ /** Current process state. */
35
+ status: ProcessStatus;
36
+ /** AbortController for terminating the process. */
37
+ abortController?: AbortController;
29
38
  }
30
39
  /** Runtime representation of authenticated SSH session. */
31
40
  export interface VirtualActiveSession {
@@ -61,6 +70,8 @@ export declare class VirtualUserManager extends EventEmitter {
61
70
  private readonly activeProcesses;
62
71
  private nextTty;
63
72
  private nextPid;
73
+ private nextUid;
74
+ private nextGid;
64
75
  /**
65
76
  * Creates a user manager instance backed by a virtual filesystem.
66
77
  *
@@ -213,15 +224,23 @@ export declare class VirtualUserManager extends EventEmitter {
213
224
  * @returns Array of username strings sorted alphabetically.
214
225
  */
215
226
  listUsers(): string[];
227
+ /** Returns the numeric UID for a username, or 0 if unknown. */
228
+ getUid(username: string): number;
229
+ /** Returns the primary GID for a username, or 0 if unknown. */
230
+ getGid(username: string): number;
216
231
  /**
217
232
  * Registers a running command as a virtual process.
218
233
  * Returns the assigned PID so the caller can deregister on completion.
219
234
  */
220
- registerProcess(username: string, command: string, argv: string[], tty: string): number;
235
+ registerProcess(username: string, command: string, argv: string[], tty: string, abortController?: AbortController): number;
221
236
  /** Removes a process record when the command exits. */
222
237
  unregisterProcess(pid: number): void;
238
+ /** Marks a process as done (keeps it in the table briefly for jobs/ps). */
239
+ markProcessDone(pid: number): void;
223
240
  /** Returns all currently running processes sorted by PID. */
224
241
  listProcesses(): VirtualProcess[];
242
+ /** Terminate a process by PID. Returns true if the process was found and signalled. */
243
+ killProcess(pid: number): boolean;
225
244
  private loadFromVfs;
226
245
  private loadSudoersFromVfs;
227
246
  private loadQuotasFromVfs;