novac 2.0.1 → 2.2.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 (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
@@ -0,0 +1,2691 @@
1
+ /**
2
+ * tea's F — Virtual Filesystem (v2.0.0)
3
+ *
4
+ * Features:
5
+ * - POSIX-style path mode (default)
6
+ * - Windows-style path mode: mode: 'win' → C:\Users\...
7
+ * - URL-style path mode: mode: 'url' → http://host/path/...
8
+ * - Snapshots saved to ./snaps/ (not /tmp)
9
+ * - Mount appends into /mnt/ and fixes duplicate-segment collisions
10
+ * - Real watcher events: 'change', 'rename', 'unlink', 'mkdir', 'write'
11
+ * - Permission enforcement (uid/gid/mode checks)
12
+ * - exec: shell interpreter (sh-compatible lexer + parser + evaluator)
13
+ * - ENV variables, PATH lookup, pipelines, redirections, &&/||/;
14
+ * - Built-ins: cd, echo, export, unset, env, pwd, alias, type, which, exit
15
+ * - Executable files in bin/, she-bang (#!) support
16
+ * - REPL: tf.repl() → interactive shell session object
17
+ * - TeaBin: custom binary executable format
18
+ * - Registers: R0–R15, SP, PC, FLAGS
19
+ * - Opcodes: MOV, ADD, SUB, MUL, DIV, MOD, AND, OR, XOR, NOT, SHL, SHR,
20
+ * CMP, JMP, JE, JNE, JL, JG, JLE, JGE, CALL, RET, PUSH, POP,
21
+ * LOAD, STORE, ALLOC, FREE, IN, OUT, HALT, NOP, INT
22
+ * - Heap memory (Uint8Array), stack, syscall interrupts
23
+ *
24
+ * Architecture:
25
+ * The entire FS is one JS object:
26
+ * {
27
+ * meta: { created, label, version },
28
+ * tree: { "/": { type:"dir", mode:0o755, uid:0, gid:0, atime, mtime, ctime, children:{} }, ... },
29
+ * inodeCounter: 3
30
+ * }
31
+ *
32
+ * Path resolution always starts from "/" (or drive root in win mode).
33
+ * Hard links, symlinks, permissions, mounts all supported.
34
+ */
35
+
36
+ "use strict";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Constants
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const S_IFREG = 0o100000;
43
+ const S_IFDIR = 0o040000;
44
+ const S_IFLNK = 0o120000;
45
+
46
+ const S_IRUSR = 0o400; const S_IWUSR = 0o200; const S_IXUSR = 0o100;
47
+ const S_IRGRP = 0o040; const S_IWGRP = 0o020; const S_IXGRP = 0o010;
48
+ const S_IROTH = 0o004; const S_IWOTH = 0o002; const S_IXOTH = 0o001;
49
+
50
+ const O_RDONLY = 0;
51
+ const O_WRONLY = 1;
52
+ const O_RDWR = 2;
53
+ const O_CREAT = 64;
54
+ const O_EXCL = 128;
55
+ const O_TRUNC = 512;
56
+ const O_APPEND = 1024;
57
+
58
+ const COPYFILE_EXCL = 1;
59
+ const VERSION = "2.0.0";
60
+ const SNAP_DIR = "./snaps";
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Errors
64
+ // ---------------------------------------------------------------------------
65
+
66
+ class TFError extends Error {
67
+ constructor(code, message, path) {
68
+ super(message + (path ? ` '${path}'` : ""));
69
+ this.code = code; this.path = path; this.name = "TFError";
70
+ }
71
+ }
72
+
73
+ function err(code, path) {
74
+ const messages = {
75
+ ENOENT: "no such file or directory", EEXIST: "file already exists",
76
+ ENOTDIR: "not a directory", EISDIR: "is a directory",
77
+ ENOTEMPTY: "directory not empty", EACCES: "permission denied",
78
+ EBADF: "bad file descriptor", EINVAL: "invalid argument",
79
+ ELOOP: "too many levels of symbolic links", EPERM: "operation not permitted",
80
+ ENOTSUP: "operation not supported", ENOTEXEC: "exec format error",
81
+ ENOMEM: "out of memory", EFAULT: "bad address",
82
+ };
83
+ return new TFError(code, messages[code] || code, path);
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Path mode adapters
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * PathAdapter: normalises between posix / win / url path representations.
92
+ * Internally we always use posix "/"-separated paths.
93
+ * The adapter converts user-supplied paths → internal, and internal → display.
94
+ */
95
+ class PathAdapter {
96
+ constructor(mode = "posix") {
97
+ this.mode = mode;
98
+ }
99
+
100
+ // Convert external path (user-supplied) to internal posix path
101
+ toInternal(p) {
102
+ if (typeof p !== "string") throw err("EINVAL");
103
+ switch (this.mode) {
104
+ case "win": return this._winToInternal(p);
105
+ case "url": return this._urlToInternal(p);
106
+ default: return p; // posix: pass through
107
+ }
108
+ }
109
+
110
+ // Convert internal posix path to external display form
111
+ toExternal(p) {
112
+ switch (this.mode) {
113
+ case "win": return this._internalToWin(p);
114
+ case "url": return this._internalToUrl(p);
115
+ default: return p;
116
+ }
117
+ }
118
+
119
+ _winToInternal(p) {
120
+ // "C:\Users\foo\bar" → "/C/Users/foo/bar"
121
+ // "C:/" → "/C"
122
+ // "\foo\bar" → "/foo/bar"
123
+ // "foo\bar" → "/foo/bar"
124
+ let s = p.replace(/\\/g, "/");
125
+ const driveMatch = s.match(/^([A-Za-z]):(\/.*)?$/);
126
+ if (driveMatch) {
127
+ const drive = driveMatch[1].toUpperCase();
128
+ const rest = driveMatch[2] || "/";
129
+ s = "/" + drive + rest;
130
+ } else if (!s.startsWith("/")) {
131
+ s = "/" + s;
132
+ }
133
+ return s;
134
+ }
135
+
136
+ _internalToWin(p) {
137
+ // "/C/Users/foo" → "C:\Users\foo"
138
+ const m = p.match(/^\/([A-Z])(\/.*)?$/);
139
+ if (m) {
140
+ return m[1] + ":" + (m[2] || "\\").replace(/\//g, "\\");
141
+ }
142
+ return p.replace(/\//g, "\\");
143
+ }
144
+
145
+ _urlToInternal(p) {
146
+ // "http://hostname/path/to/file" → "/http/hostname/path/to/file"
147
+ // "https://example.com/a/b" → "/https/example.com/a/b"
148
+ // "/path" → "/path"
149
+ const m = p.match(/^([a-zA-Z][a-zA-Z0-9+\-.]*):\/\/([^/]*)(\/.*)?$/);
150
+ if (m) {
151
+ const scheme = m[1].toLowerCase();
152
+ const host = m[2];
153
+ const rest = m[3] || "/";
154
+ return "/" + scheme + "/" + host + rest;
155
+ }
156
+ return p.startsWith("/") ? p : "/" + p;
157
+ }
158
+
159
+ _internalToUrl(p) {
160
+ // "/http/example.com/a/b" → "http://example.com/a/b"
161
+ const m = p.match(/^\/([a-z][a-z0-9+\-.]*)\/([^/]+)(\/.*)?$/);
162
+ if (m) {
163
+ return m[1] + "://" + m[2] + (m[3] || "/");
164
+ }
165
+ return p;
166
+ }
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Utilities
171
+ // ---------------------------------------------------------------------------
172
+
173
+ function now() { return new Date().toISOString(); }
174
+
175
+ function normPath(p) {
176
+ if (typeof p !== "string") throw err("EINVAL");
177
+ const abs = p.startsWith("/") ? p : "/" + p;
178
+ const parts = abs.split("/").filter(Boolean);
179
+ const stack = [];
180
+ for (const part of parts) {
181
+ if (part === ".") continue;
182
+ if (part === "..") stack.pop();
183
+ else stack.push(part);
184
+ }
185
+ return "/" + stack.join("/");
186
+ }
187
+
188
+ function splitPath(p) {
189
+ const n = normPath(p);
190
+ if (n === "/") return { dir: "/", base: "" };
191
+ const idx = n.lastIndexOf("/");
192
+ return { dir: idx === 0 ? "/" : n.slice(0, idx), base: n.slice(idx + 1) };
193
+ }
194
+
195
+ function matchGlob(pattern, str) {
196
+ const re = pattern
197
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
198
+ .replace(/\*\*/g, "§DOUBLE§").replace(/\*/g, "[^/]*")
199
+ .replace(/§DOUBLE§/g, ".*").replace(/\?/g, "[^/]");
200
+ return new RegExp("^" + re + "$").test(str);
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Simple EventEmitter
205
+ // ---------------------------------------------------------------------------
206
+
207
+ class EventEmitter {
208
+ constructor() { this._events = {}; }
209
+ on(ev, fn) { (this._events[ev] = this._events[ev] || []).push(fn); return this; }
210
+ off(ev, fn) { if (this._events[ev]) this._events[ev] = this._events[ev].filter(f => f !== fn); return this; }
211
+ emit(ev, ...args) { (this._events[ev] || []).slice().forEach(fn => { try { fn(...args); } catch(_) {} }); }
212
+ once(ev, fn) {
213
+ const w = (...args) => { fn(...args); this.off(ev, w); };
214
+ return this.on(ev, w);
215
+ }
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Stats
220
+ // ---------------------------------------------------------------------------
221
+
222
+ class Stats {
223
+ constructor(node, nodeId) {
224
+ this.dev = 1;
225
+ this.ino = parseInt(nodeId.replace(/\D/g, ""), 10) || 0;
226
+ this.mode = (node.mode & 0o7777) | (node.type === "dir" ? S_IFDIR : node.type === "symlink" ? S_IFLNK : S_IFREG);
227
+ this.nlink = node.nlink || 1;
228
+ this.uid = node.uid || 0;
229
+ this.gid = node.gid || 0;
230
+ this.rdev = 0;
231
+ this.size = node.type === "file" ? (node.data || "").length : node.type === "symlink" ? (node.link || "").length : 4096;
232
+ this.blksize = 4096;
233
+ this.blocks = Math.ceil(this.size / 512);
234
+ this.atimeMs = new Date(node.atime || node.ctime).getTime();
235
+ this.mtimeMs = new Date(node.mtime || node.ctime).getTime();
236
+ this.ctimeMs = new Date(node.ctime).getTime();
237
+ this.birthtimeMs = new Date(node.ctime).getTime();
238
+ this.atime = new Date(this.atimeMs); this.mtime = new Date(this.mtimeMs);
239
+ this.ctime = new Date(this.ctimeMs); this.birthtime = new Date(this.birthtimeMs);
240
+ this._type = node.type;
241
+ }
242
+ isFile() { return this._type === "file"; }
243
+ isDirectory() { return this._type === "dir"; }
244
+ isSymbolicLink() { return this._type === "symlink"; }
245
+ isBlockDevice() { return false; }
246
+ isCharacterDevice() { return false; }
247
+ isFIFO() { return false; }
248
+ isSocket() { return false; }
249
+ }
250
+
251
+ // ===========================================================================
252
+ // TeaBin — Custom Binary Executable Format + VM
253
+ // ===========================================================================
254
+
255
+ /**
256
+ * Binary format (little-endian):
257
+ * Magic: 0x54 0x45 0x41 0x42 ("TEAB")
258
+ * Version: 1 byte
259
+ * Data segment size: 4 bytes (uint32)
260
+ * Data segment: N bytes
261
+ * Code segment size: 4 bytes (uint32)
262
+ * Code segment: each instruction = 6 bytes:
263
+ * [opcode:1][mode:1][arg0:2][arg1:2] (uint16 args)
264
+ *
265
+ * Registers: R0–R15 (index 0–15), SP (16), PC (17)
266
+ * FLAGS: ZF (zero), SF (sign), OF (overflow), CF (carry)
267
+ */
268
+
269
+ const OP = {
270
+ NOP: 0x00,
271
+ // Data movement
272
+ MOV: 0x01, // MOV dst_reg, src (reg or imm)
273
+ LOAD: 0x02, // LOAD dst_reg, addr_reg — load word from memory[addr]
274
+ STORE: 0x03, // STORE addr_reg, src_reg — store word to memory[addr]
275
+ PUSH: 0x04, // PUSH src_reg
276
+ POP: 0x05, // POP dst_reg
277
+ // Arithmetic
278
+ ADD: 0x10,
279
+ SUB: 0x11,
280
+ MUL: 0x12,
281
+ DIV: 0x13,
282
+ MOD: 0x14,
283
+ // Bitwise
284
+ AND: 0x20,
285
+ OR: 0x21,
286
+ XOR: 0x22,
287
+ NOT: 0x23,
288
+ SHL: 0x24,
289
+ SHR: 0x25,
290
+ // Compare / branch
291
+ CMP: 0x30, // CMP reg, reg/imm — sets FLAGS
292
+ JMP: 0x31, // JMP addr (imm = instruction index)
293
+ JE: 0x32, // Jump if ZF
294
+ JNE: 0x33,
295
+ JL: 0x34, // Jump if SF != OF
296
+ JG: 0x35, // Jump if ZF=0 and SF=OF
297
+ JLE: 0x36,
298
+ JGE: 0x37,
299
+ // Subroutines
300
+ CALL: 0x40, // CALL addr — pushes PC, jumps
301
+ RET: 0x41, // RET — pops PC
302
+ // Memory management
303
+ ALLOC: 0x50, // ALLOC dst_reg, size_reg — malloc(size) → pointer in dst
304
+ FREE: 0x51, // FREE addr_reg
305
+ // I/O
306
+ IN: 0x60, // IN dst_reg, port
307
+ OUT: 0x61, // OUT port, src_reg
308
+ // System
309
+ INT: 0x70, // INT vector — software interrupt / syscall
310
+ HALT: 0xFF,
311
+ };
312
+
313
+ // Arg mode flags (encoded in 'mode' byte of instruction)
314
+ const ARG_REG = 0; // argument is register index
315
+ const ARG_IMM = 1; // argument is immediate value
316
+ // mode byte: bits[0]=arg0 is imm, bits[1]=arg1 is imm
317
+
318
+ const REG_COUNT = 18; // R0–R15 + SP(16) + PC(17)
319
+ const SP_IDX = 16;
320
+ const PC_IDX = 17;
321
+ const HEAP_SIZE = 1024 * 64; // 64 KB
322
+
323
+ class TeaBinVM {
324
+ constructor(opts = {}) {
325
+ this.regs = new Int32Array(REG_COUNT); // registers
326
+ this.mem = new Uint8Array(HEAP_SIZE); // flat heap
327
+ this.stack = []; // JS-array stack (separate from heap)
328
+ this.flags = { ZF: false, SF: false, OF: false, CF: false };
329
+ this._heapAllocs = new Map(); // ptr → size
330
+ this._heapTop = 0;
331
+ this.halted = false;
332
+ this.exitCode = 0;
333
+ this._ports = opts.ports || {}; // port → {read, write} handlers
334
+ this._intHandlers = opts.interrupts || {}; // vector → fn(vm)
335
+ this.stdout = [];
336
+ this.stdin = [];
337
+ this._maxCycles = opts.maxCycles || 1_000_000;
338
+ }
339
+
340
+ /** Load a TEAB binary (ArrayBuffer or Uint8Array) */
341
+ load(binary) {
342
+ const buf = binary instanceof Uint8Array ? binary : new Uint8Array(binary);
343
+ let off = 0;
344
+
345
+ // Magic
346
+ if (buf[0] !== 0x54 || buf[1] !== 0x45 || buf[2] !== 0x41 || buf[3] !== 0x42)
347
+ throw new Error("Invalid TEAB magic");
348
+ off = 4;
349
+
350
+ const _version = buf[off++]; // reserved
351
+
352
+ // Data segment
353
+ const dataSize = buf[off] | (buf[off+1]<<8) | (buf[off+2]<<16) | (buf[off+3]<<24);
354
+ off += 4;
355
+ this.mem.set(buf.subarray(off, off + dataSize), 0);
356
+ this._heapTop = dataSize;
357
+ off += dataSize;
358
+
359
+ // Code segment
360
+ const codeSize = buf[off] | (buf[off+1]<<8) | (buf[off+2]<<16) | (buf[off+3]<<24);
361
+ off += 4;
362
+ this._instructions = [];
363
+ for (let i = 0; i < codeSize; i += 6) {
364
+ const opcode = buf[off + i];
365
+ const mode = buf[off + i + 1];
366
+ const arg0 = buf[off + i + 2] | (buf[off + i + 3] << 8);
367
+ const arg1 = buf[off + i + 4] | (buf[off + i + 5] << 8);
368
+ this._instructions.push({ opcode, mode, arg0, arg1 });
369
+ }
370
+ this.regs[SP_IDX] = HEAP_SIZE - 1; // stack grows down in heap (but we use JS stack)
371
+ this.regs[PC_IDX] = 0;
372
+ return this;
373
+ }
374
+
375
+ /** Build a TEAB binary from an assembly-like source string */
376
+ static assemble(src) {
377
+ const lines = src.split(/\n/).map(l => l.replace(/;.*/, "").trim()).filter(Boolean);
378
+ const labels = {};
379
+ // First pass: collect labels
380
+ let instrIdx = 0;
381
+ for (const line of lines) {
382
+ if (line.endsWith(":")) { labels[line.slice(0, -1)] = instrIdx; continue; }
383
+ if (line.startsWith(".data")) continue;
384
+ instrIdx++;
385
+ }
386
+
387
+ const instructions = [];
388
+ const dataBytes = [];
389
+ let inData = false;
390
+ const dataLabels = {};
391
+
392
+ for (const line of lines) {
393
+ if (line.startsWith(".data")) { inData = true; continue; }
394
+ if (line.startsWith(".code")) { inData = false; continue; }
395
+ if (line.endsWith(":")) continue; // label
396
+
397
+ if (inData) {
398
+ // .data label "string" or .data label 0x...
399
+ const m = line.match(/^(\w+)\s+"(.*)"$/) || line.match(/^(\w+)\s+(.+)$/);
400
+ if (m) {
401
+ dataLabels[m[1]] = dataBytes.length;
402
+ const val = m[2];
403
+ if (val.startsWith('"')) {
404
+ for (const ch of val.slice(1, -1)) dataBytes.push(ch.charCodeAt(0));
405
+ dataBytes.push(0); // null terminate
406
+ } else {
407
+ const n = parseInt(val, 0);
408
+ dataBytes.push(n & 0xFF, (n>>8)&0xFF, (n>>16)&0xFF, (n>>24)&0xFF);
409
+ }
410
+ }
411
+ continue;
412
+ }
413
+
414
+ const parts = line.split(/[\s,]+/);
415
+ const mnemo = parts[0].toUpperCase();
416
+ const opcode = OP[mnemo];
417
+ if (opcode === undefined) throw new Error(`Unknown opcode: ${mnemo}`);
418
+
419
+ const parseArg = (s) => {
420
+ if (!s) return { val: 0, imm: false };
421
+ if (/^R\d+$/i.test(s)) return { val: parseInt(s.slice(1)), imm: false };
422
+ if (s.toUpperCase() === "SP") return { val: SP_IDX, imm: false };
423
+ if (s.toUpperCase() === "PC") return { val: PC_IDX, imm: false };
424
+ if (labels[s] !== undefined) return { val: labels[s], imm: true };
425
+ if (dataLabels[s] !== undefined) return { val: dataLabels[s], imm: true };
426
+ return { val: parseInt(s, 0) & 0xFFFF, imm: true };
427
+ };
428
+
429
+ const a0 = parseArg(parts[1]);
430
+ const a1 = parseArg(parts[2]);
431
+ const mode = (a0.imm ? 1 : 0) | (a1.imm ? 2 : 0);
432
+ instructions.push({ opcode, mode, arg0: a0.val & 0xFFFF, arg1: a1.val & 0xFFFF });
433
+ }
434
+
435
+ // Encode
436
+ const dataSize = dataBytes.length;
437
+ const codeSize = instructions.length * 6;
438
+ const total = 4 + 1 + 4 + dataSize + 4 + codeSize;
439
+ const buf = new Uint8Array(total);
440
+ let off = 0;
441
+ buf[off++]=0x54; buf[off++]=0x45; buf[off++]=0x41; buf[off++]=0x42; // TEAB
442
+ buf[off++]=1; // version
443
+ buf[off++]=dataSize&0xFF; buf[off++]=(dataSize>>8)&0xFF;
444
+ buf[off++]=(dataSize>>16)&0xFF; buf[off++]=(dataSize>>24)&0xFF;
445
+ for (const b of dataBytes) buf[off++]=b;
446
+ buf[off++]=codeSize&0xFF; buf[off++]=(codeSize>>8)&0xFF;
447
+ buf[off++]=(codeSize>>16)&0xFF; buf[off++]=(codeSize>>24)&0xFF;
448
+ for (const ins of instructions) {
449
+ buf[off++]=ins.opcode; buf[off++]=ins.mode;
450
+ buf[off++]=ins.arg0&0xFF; buf[off++]=(ins.arg0>>8)&0xFF;
451
+ buf[off++]=ins.arg1&0xFF; buf[off++]=(ins.arg1>>8)&0xFF;
452
+ }
453
+ return buf;
454
+ }
455
+
456
+ _getReg(idx) { return this.regs[idx] | 0; }
457
+ _setReg(idx, val) {
458
+ this.regs[idx] = val | 0;
459
+ if (idx === PC_IDX) return;
460
+ this.flags.ZF = (val === 0);
461
+ this.flags.SF = (val < 0);
462
+ }
463
+
464
+ _arg(val, isImm) { return isImm ? (val | 0) : this._getReg(val); }
465
+
466
+ _memReadWord(addr) {
467
+ addr = addr & 0xFFFF;
468
+ return this.mem[addr] | (this.mem[addr+1]<<8) | (this.mem[addr+2]<<16) | (this.mem[addr+3]<<24);
469
+ }
470
+ _memWriteWord(addr, val) {
471
+ addr = addr & 0xFFFF;
472
+ this.mem[addr]=val&0xFF; this.mem[addr+1]=(val>>8)&0xFF;
473
+ this.mem[addr+2]=(val>>16)&0xFF; this.mem[addr+3]=(val>>24)&0xFF;
474
+ }
475
+
476
+ _alloc(size) {
477
+ const ptr = this._heapTop;
478
+ if (ptr + size > HEAP_SIZE) throw err("ENOMEM");
479
+ this._heapAllocs.set(ptr, size);
480
+ this._heapTop += size;
481
+ return ptr;
482
+ }
483
+
484
+ _free(ptr) { this._heapAllocs.delete(ptr); }
485
+
486
+ _setFlags(val) {
487
+ this.flags.ZF = (val === 0);
488
+ this.flags.SF = (val < 0);
489
+ }
490
+
491
+ run() {
492
+ let cycles = 0;
493
+ const ins = this._instructions;
494
+ while (!this.halted && this.regs[PC_IDX] < ins.length && cycles < this._maxCycles) {
495
+ const pc = this.regs[PC_IDX]++;
496
+ const { opcode, mode, arg0, arg1 } = ins[pc];
497
+ const imm0 = (mode & 1) !== 0;
498
+ const imm1 = (mode & 2) !== 0;
499
+ const v0 = this._arg(arg0, imm0);
500
+ const v1 = this._arg(arg1, imm1);
501
+
502
+ switch (opcode) {
503
+ case OP.NOP: break;
504
+ case OP.HALT: this.halted = true; this.exitCode = v0; break;
505
+
506
+ case OP.MOV: this._setReg(arg0, v1); break;
507
+
508
+ case OP.LOAD: {
509
+ const word = this._memReadWord(v1);
510
+ this._setReg(arg0, word);
511
+ break;
512
+ }
513
+ case OP.STORE: this._memWriteWord(v0, v1); break;
514
+
515
+ case OP.PUSH:
516
+ this.stack.push(v0);
517
+ break;
518
+ case OP.POP:
519
+ if (this.stack.length === 0) throw new Error("Stack underflow");
520
+ this._setReg(arg0, this.stack.pop());
521
+ break;
522
+
523
+ case OP.ADD: { const r = (v0 + v1)|0; this._setFlags(r); this.flags.CF = (v0+v1) > 0x7FFFFFFF; this._setReg(arg0, r); break; }
524
+ case OP.SUB: { const r = (v0 - v1)|0; this._setFlags(r); this._setReg(arg0, r); break; }
525
+ case OP.MUL: { const r = Math.imul(v0, v1); this._setFlags(r); this._setReg(arg0, r); break; }
526
+ case OP.DIV: { if (v1===0) throw new Error("Division by zero"); const r=(v0/v1)|0; this._setFlags(r); this._setReg(arg0, r); break; }
527
+ case OP.MOD: { if (v1===0) throw new Error("Division by zero"); const r=v0%v1; this._setFlags(r); this._setReg(arg0, r); break; }
528
+
529
+ case OP.AND: { const r=v0&v1; this._setFlags(r); this._setReg(arg0, r); break; }
530
+ case OP.OR: { const r=v0|v1; this._setFlags(r); this._setReg(arg0, r); break; }
531
+ case OP.XOR: { const r=v0^v1; this._setFlags(r); this._setReg(arg0, r); break; }
532
+ case OP.NOT: { const r=~v0; this._setFlags(r); this._setReg(arg0, r); break; }
533
+ case OP.SHL: { const r=(v0<<v1)|0; this._setFlags(r); this._setReg(arg0, r); break; }
534
+ case OP.SHR: { const r=(v0>>v1)|0; this._setFlags(r); this._setReg(arg0, r); break; }
535
+
536
+ case OP.CMP: {
537
+ const r = (v0 - v1)|0;
538
+ this.flags.ZF = (r === 0);
539
+ this.flags.SF = (r < 0);
540
+ this.flags.OF = ((v0 >= 0 && v1 < 0 && r < 0) || (v0 < 0 && v1 >= 0 && r >= 0));
541
+ this.flags.CF = (v0 < v1);
542
+ break;
543
+ }
544
+
545
+ case OP.JMP: this.regs[PC_IDX] = v0; break;
546
+ case OP.JE: if (this.flags.ZF) this.regs[PC_IDX] = v0; break;
547
+ case OP.JNE: if (!this.flags.ZF) this.regs[PC_IDX] = v0; break;
548
+ case OP.JL: if (this.flags.SF !== this.flags.OF) this.regs[PC_IDX] = v0; break;
549
+ case OP.JG: if (!this.flags.ZF && this.flags.SF === this.flags.OF) this.regs[PC_IDX] = v0; break;
550
+ case OP.JLE: if (this.flags.ZF || this.flags.SF !== this.flags.OF) this.regs[PC_IDX] = v0; break;
551
+ case OP.JGE: if (this.flags.SF === this.flags.OF) this.regs[PC_IDX] = v0; break;
552
+
553
+ case OP.CALL:
554
+ this.stack.push(this.regs[PC_IDX]);
555
+ this.regs[PC_IDX] = v0;
556
+ break;
557
+ case OP.RET:
558
+ if (this.stack.length === 0) { this.halted = true; break; }
559
+ this.regs[PC_IDX] = this.stack.pop();
560
+ break;
561
+
562
+ case OP.ALLOC: {
563
+ const ptr = this._alloc(v1);
564
+ this._setReg(arg0, ptr);
565
+ break;
566
+ }
567
+ case OP.FREE: this._free(v0); break;
568
+
569
+ case OP.IN: {
570
+ const port = v1;
571
+ const handler = this._ports[port];
572
+ const val = handler ? handler.read() : (this.stdin.shift() || 0);
573
+ this._setReg(arg0, val);
574
+ break;
575
+ }
576
+ case OP.OUT: {
577
+ const port = v0;
578
+ const val = v1;
579
+ const handler = this._ports[port];
580
+ if (handler) { handler.write(val); }
581
+ else { this.stdout.push(val); }
582
+ break;
583
+ }
584
+
585
+ case OP.INT: {
586
+ const vec = v0;
587
+ const handler = this._intHandlers[vec];
588
+ if (handler) handler(this);
589
+ // Syscall table for INT 0x80 (Linux-style)
590
+ else if (vec === 0x80) this._syscall();
591
+ break;
592
+ }
593
+
594
+ default:
595
+ throw new Error(`Unknown opcode: 0x${opcode.toString(16)}`);
596
+ }
597
+ cycles++;
598
+ }
599
+ if (cycles >= this._maxCycles) throw new Error("CPU cycle limit exceeded");
600
+ return this.exitCode;
601
+ }
602
+
603
+ _syscall() {
604
+ // R0 = syscall number, R1–R5 = args
605
+ const num = this._getReg(0);
606
+ switch (num) {
607
+ case 1: // write(fd, buf_ptr, len) → writes string to stdout
608
+ {
609
+ const ptr = this._getReg(2);
610
+ const len = this._getReg(3);
611
+ let s = "";
612
+ for (let i = 0; i < len; i++) s += String.fromCharCode(this.mem[(ptr + i) & 0xFFFF] || 0);
613
+ this.stdout.push(s);
614
+ this._setReg(0, len);
615
+ break;
616
+ }
617
+ case 60: // exit(code)
618
+ this.exitCode = this._getReg(1);
619
+ this.halted = true;
620
+ break;
621
+ default:
622
+ this._setReg(0, -1); // ENOSYS
623
+ }
624
+ }
625
+
626
+ /** Read a null-terminated string from memory at addr */
627
+ readString(addr) {
628
+ let s = "";
629
+ let i = addr;
630
+ while (i < HEAP_SIZE && this.mem[i] !== 0) s += String.fromCharCode(this.mem[i++]);
631
+ return s;
632
+ }
633
+ }
634
+
635
+ // ===========================================================================
636
+ // Shell (sh interpreter)
637
+ // ===========================================================================
638
+
639
+ /**
640
+ * Sh — a minimal but real sh interpreter
641
+ * Supports: variables, ENV, export, PATH lookup, pipelines (|), redirections
642
+ * (>, >>, <), background (&), &&, ||, ;, subshells $(), quoting, globs,
643
+ * built-ins, #! scripts, and TeaBin executables.
644
+ */
645
+
646
+ class Sh {
647
+ constructor(fs, opts = {}) {
648
+ this._fs = fs;
649
+ this.env = Object.assign({
650
+ PATH: "/bin:/usr/bin:/usr/local/bin",
651
+ HOME: "/root",
652
+ USER: "root",
653
+ SHELL: "/bin/sh",
654
+ PWD: "/",
655
+ PS1: "$ ",
656
+ IFS: " \t\n",
657
+ }, opts.env || {});
658
+ this.aliases = {};
659
+ this._exitCode = 0;
660
+ this._uid = opts.uid !== undefined ? opts.uid : 0;
661
+ this._gid = opts.gid !== undefined ? opts.gid : 0;
662
+ }
663
+
664
+ // -- Public API -----------------------------------------------------------
665
+
666
+ /** Execute a shell script string. Returns { stdout, stderr, code }. */
667
+ exec(script, opts = {}) {
668
+ const ctx = {
669
+ stdin: opts.stdin || [],
670
+ stdout: opts.stdout || [],
671
+ stderr: opts.stderr || [],
672
+ vars: Object.assign({}, this.env),
673
+ };
674
+ try {
675
+ const tokens = this._lex(script);
676
+ const ast = this._parse(tokens);
677
+ this._execList(ast, ctx);
678
+ } catch(e) {
679
+ if (e && e._exit) { this._exitCode = e.code; return { stdout: ctx.stdout.join(""), stderr: ctx.stderr.join(""), code: e.code }; }
680
+ ctx.stderr.push(e.message + "\n");
681
+ this._exitCode = 1;
682
+ }
683
+ return {
684
+ stdout: ctx.stdout.join(""),
685
+ stderr: ctx.stderr.join(""),
686
+ code: this._exitCode,
687
+ };
688
+ }
689
+
690
+ // -- Lexer ----------------------------------------------------------------
691
+
692
+ _lex(src) {
693
+ const tokens = [];
694
+ let i = 0;
695
+ while (i < src.length) {
696
+ const ch = src[i];
697
+ // skip whitespace (but not newlines — they're separators)
698
+ if (ch === " " || ch === "\t") { i++; continue; }
699
+ if (ch === "\n" || ch === ";") { tokens.push({ type: "SEP" }); i++; continue; }
700
+ if (ch === "#") { while (i < src.length && src[i] !== "\n") i++; continue; }
701
+ if (ch === "&" && src[i+1] === "&") { tokens.push({ type: "AND" }); i+=2; continue; }
702
+ if (ch === "|" && src[i+1] === "|") { tokens.push({ type: "OR" }); i+=2; continue; }
703
+ if (ch === "|") { tokens.push({ type: "PIPE" }); i++; continue; }
704
+ if (ch === "&") { tokens.push({ type: "BG" }); i++; continue; }
705
+ if (ch === ">") {
706
+ if (src[i+1] === ">") { tokens.push({ type: "REDIR_APPEND" }); i+=2; }
707
+ else { tokens.push({ type: "REDIR_OUT" }); i++; }
708
+ continue;
709
+ }
710
+ if (ch === "<") { tokens.push({ type: "REDIR_IN" }); i++; continue; }
711
+ if (ch === "(") { tokens.push({ type: "LPAREN" }); i++; continue; }
712
+ if (ch === ")") { tokens.push({ type: "RPAREN" }); i++; continue; }
713
+ if (ch === "{") { tokens.push({ type: "LBRACE" }); i++; continue; }
714
+ if (ch === "}") { tokens.push({ type: "RBRACE" }); i++; continue; }
715
+
716
+ // String / word
717
+ let word = "";
718
+ while (i < src.length) {
719
+ const c = src[i];
720
+ if (" \t\n;|&<>(){}".includes(c)) break;
721
+ if (c === "\\") { i++; word += src[i] || ""; i++; continue; }
722
+ if (c === "'") {
723
+ i++;
724
+ while (i < src.length && src[i] !== "'") word += src[i++];
725
+ i++;
726
+ continue;
727
+ }
728
+ if (c === '"') {
729
+ i++;
730
+ while (i < src.length && src[i] !== '"') {
731
+ if (src[i] === "\\" && '"$`\\'.includes(src[i+1])) { i++; word += src[i++]; }
732
+ else word += src[i++];
733
+ }
734
+ i++;
735
+ continue;
736
+ }
737
+ word += c; i++;
738
+ }
739
+ if (word) tokens.push({ type: "WORD", value: word });
740
+ }
741
+ return tokens;
742
+ }
743
+
744
+ // -- Parser ---------------------------------------------------------------
745
+ // Produces a list of command nodes
746
+
747
+ _parse(tokens) {
748
+ // Flatten into a list of "statements" separated by SEP/AND/OR/PIPE/BG
749
+ return this._parseList(tokens, 0).nodes;
750
+ }
751
+
752
+ _parseList(tokens, pos) {
753
+ const nodes = [];
754
+ while (pos < tokens.length) {
755
+ const { node, pos: newPos } = this._parseCmd(tokens, pos);
756
+ pos = newPos;
757
+ if (node) nodes.push(node);
758
+ if (pos >= tokens.length) break;
759
+ const sep = tokens[pos];
760
+ if (sep.type === "RPAREN" || sep.type === "RBRACE") break;
761
+ if (sep.type === "SEP") { nodes.push({ type: "SEP" }); pos++; continue; }
762
+ if (sep.type === "AND") { nodes.push({ type: "AND" }); pos++; continue; }
763
+ if (sep.type === "OR") { nodes.push({ type: "OR" }); pos++; continue; }
764
+ if (sep.type === "PIPE") { nodes.push({ type: "PIPE" }); pos++; continue; }
765
+ if (sep.type === "BG") { nodes.push({ type: "BG" }); pos++; continue; }
766
+ break;
767
+ }
768
+ return { nodes, pos };
769
+ }
770
+
771
+ _parseCmd(tokens, pos) {
772
+ const words = [];
773
+ const redirs = [];
774
+ while (pos < tokens.length) {
775
+ const tok = tokens[pos];
776
+ if (["SEP","AND","OR","PIPE","BG","RPAREN","RBRACE"].includes(tok.type)) break;
777
+ if (tok.type === "REDIR_OUT" || tok.type === "REDIR_APPEND" || tok.type === "REDIR_IN") {
778
+ const kind = tok.type;
779
+ pos++;
780
+ const file = tokens[pos];
781
+ if (!file || file.type !== "WORD") throw new Error("sh: expected filename after redirect");
782
+ redirs.push({ kind, file: file.value });
783
+ pos++;
784
+ continue;
785
+ }
786
+ if (tok.type === "LPAREN") {
787
+ // subshell
788
+ pos++;
789
+ const { nodes, pos: endPos } = this._parseList(tokens, pos);
790
+ pos = endPos + 1; // skip RPAREN
791
+ return { node: { type: "SUBSHELL", body: nodes }, pos };
792
+ }
793
+ if (tok.type === "WORD") { words.push(tok.value); pos++; continue; }
794
+ pos++;
795
+ }
796
+ if (words.length === 0 && redirs.length === 0) return { node: null, pos };
797
+ return { node: { type: "CMD", words, redirs }, pos };
798
+ }
799
+
800
+ // -- Executor -------------------------------------------------------------
801
+
802
+ _execList(nodes, ctx) {
803
+ let i = 0;
804
+ while (i < nodes.length) {
805
+ const node = nodes[i];
806
+ if (!node || node.type === "SEP") { i++; continue; }
807
+
808
+ // Collect a pipeline
809
+ const pipeline = [node];
810
+ i++;
811
+ while (i < nodes.length && nodes[i] && nodes[i].type === "PIPE") {
812
+ i++; // skip PIPE
813
+ if (i < nodes.length && nodes[i] && nodes[i].type !== "SEP")
814
+ pipeline.push(nodes[i++]);
815
+ }
816
+
817
+ this._execPipeline(pipeline, ctx);
818
+
819
+ // Check && / ||
820
+ if (i < nodes.length && nodes[i]) {
821
+ if (nodes[i].type === "AND") { i++; if (this._exitCode !== 0) { while (i < nodes.length && nodes[i].type !== "SEP") i++; } continue; }
822
+ if (nodes[i].type === "OR") { i++; if (this._exitCode === 0) { while (i < nodes.length && nodes[i].type !== "SEP") i++; } continue; }
823
+ }
824
+ }
825
+ }
826
+
827
+ _execPipeline(pipeline, ctx) {
828
+ if (pipeline.length === 1) {
829
+ this._execNode(pipeline[0], ctx);
830
+ return;
831
+ }
832
+ // Build piped buffers
833
+ let prevOut = ctx.stdin;
834
+ for (let i = 0; i < pipeline.length; i++) {
835
+ const isLast = (i === pipeline.length - 1);
836
+ const pipeOut = isLast ? ctx.stdout : [];
837
+ const pipeCtx = {
838
+ stdin: prevOut,
839
+ stdout: pipeOut,
840
+ stderr: ctx.stderr,
841
+ vars: ctx.vars,
842
+ };
843
+ this._execNode(pipeline[i], pipeCtx);
844
+ prevOut = pipeOut;
845
+ }
846
+ }
847
+
848
+ _execNode(node, ctx) {
849
+ if (!node) return;
850
+ if (node.type === "SEP") return;
851
+ if (node.type === "SUBSHELL") {
852
+ const subCtx = { stdin: [...ctx.stdin], stdout: ctx.stdout, stderr: ctx.stderr, vars: { ...ctx.vars } };
853
+ this._execList(node.body, subCtx);
854
+ return;
855
+ }
856
+ if (node.type === "CMD") {
857
+ // Expand vars and perform globbing
858
+ const words = node.words.map(w => this._expand(w, ctx));
859
+ const expanded = [];
860
+ for (const w of words) {
861
+ const globs = this._glob(w, ctx.vars.PWD || "/");
862
+ expanded.push(...globs);
863
+ }
864
+ if (expanded.length === 0) return;
865
+
866
+ // Handle redirections
867
+ let stdin = ctx.stdin;
868
+ let stdout = ctx.stdout;
869
+ for (const r of node.redirs) {
870
+ const file = this._expand(r.file, ctx);
871
+ if (r.kind === "REDIR_IN") {
872
+ try { stdin = [this._fs.readFile(file, "utf8")]; } catch(_) { stdin = []; }
873
+ } else if (r.kind === "REDIR_OUT") {
874
+ const buf = [];
875
+ stdout = buf;
876
+ // after exec, write buf to file
877
+ const flush = () => this._fs.writeFile(file, buf.join(""));
878
+ ctx._flushes = ctx._flushes || [];
879
+ ctx._flushes.push(flush);
880
+ } else if (r.kind === "REDIR_APPEND") {
881
+ const buf = [];
882
+ stdout = buf;
883
+ const flush = () => this._fs.appendFile(file, buf.join(""));
884
+ ctx._flushes = ctx._flushes || [];
885
+ ctx._flushes.push(flush);
886
+ }
887
+ }
888
+
889
+ const cmdCtx = { stdin, stdout, stderr: ctx.stderr, vars: ctx.vars };
890
+ this._runCommand(expanded[0], expanded.slice(1), cmdCtx);
891
+
892
+ // Flush redirections
893
+ for (const f of (ctx._flushes || [])) f();
894
+ ctx._flushes = [];
895
+ return;
896
+ }
897
+ }
898
+
899
+ _expand(word, ctx) {
900
+ // $VAR, ${VAR}, $?, $$
901
+ return word.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*|\?|\$|\d+)/g, (_, a, b) => {
902
+ const name = a || b;
903
+ if (name === "?") return String(this._exitCode);
904
+ if (name === "$") return "$$";
905
+ return ctx.vars[name] !== undefined ? ctx.vars[name] : "";
906
+ });
907
+ }
908
+
909
+ _glob(pattern, cwd) {
910
+ if (!/[*?]/.test(pattern)) return [pattern];
911
+ try {
912
+ const hits = this._fs.glob(pattern, { cwd });
913
+ return hits.length ? hits : [pattern];
914
+ } catch(_) { return [pattern]; }
915
+ }
916
+
917
+ _runCommand(cmd, args, ctx) {
918
+ // Alias expansion
919
+ if (this.aliases[cmd]) {
920
+ const aliased = this._lex(this.aliases[cmd] + " " + args.join(" "));
921
+ const ast = this._parse(aliased);
922
+ this._execList(ast, ctx);
923
+ return;
924
+ }
925
+
926
+ // Variable assignment (FOO=bar)
927
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(cmd)) {
928
+ const eq = cmd.indexOf("=");
929
+ const key = cmd.slice(0, eq);
930
+ const val = this._expand(cmd.slice(eq + 1), ctx);
931
+ ctx.vars[key] = val;
932
+ this.env[key] = val;
933
+ this._exitCode = 0;
934
+ return;
935
+ }
936
+
937
+ // Built-ins
938
+ if (this._builtin(cmd, args, ctx)) return;
939
+
940
+ // PATH lookup
941
+ const bin = this._findBin(cmd, ctx.vars.PATH || this.env.PATH);
942
+ if (!bin) {
943
+ ctx.stderr.push(`sh: ${cmd}: command not found\n`);
944
+ this._exitCode = 127;
945
+ return;
946
+ }
947
+
948
+ this._execFile(bin, [cmd, ...args], ctx);
949
+ }
950
+
951
+ _findBin(name, PATH) {
952
+ if (name.startsWith("/") || name.startsWith("./") || name.startsWith("../")) {
953
+ return this._fs.exists(name) ? name : null;
954
+ }
955
+ for (const dir of (PATH || "").split(":")) {
956
+ const p = dir.endsWith("/") ? dir + name : dir + "/" + name;
957
+ if (this._fs.exists(p)) return p;
958
+ }
959
+ return null;
960
+ }
961
+
962
+ _execFile(path, argv, ctx) {
963
+ let data;
964
+ try {
965
+ data = this._fs.readFile(path, "utf8");
966
+ } catch(e) {
967
+ ctx.stderr.push(`sh: ${path}: ${e.message}\n`);
968
+ this._exitCode = 126;
969
+ return;
970
+ }
971
+
972
+ // Check execute permission
973
+ try {
974
+ const st = this._fs.stat(path);
975
+ const canExec = (st.uid === this._uid && (st.mode & S_IXUSR)) ||
976
+ (st.gid === this._gid && (st.mode & S_IXGRP)) ||
977
+ (this._uid === 0) ||
978
+ (st.mode & S_IXOTH);
979
+ if (!canExec) { ctx.stderr.push(`sh: ${path}: permission denied\n`); this._exitCode = 126; return; }
980
+ } catch(_) {}
981
+
982
+ // She-bang
983
+ if (data.startsWith("#!")) {
984
+ const firstLine = data.split("\n")[0].slice(2).trim();
985
+ const interp = firstLine.split(/\s+/);
986
+ this._runCommand(interp[0], [...interp.slice(1), path, ...argv.slice(1)], ctx);
987
+ return;
988
+ }
989
+
990
+ // TEAB binary magic (base64-encoded in the file data, or raw bytes check)
991
+ if (data.startsWith("TEAB") || (data.charCodeAt(0) === 0x54 && data.charCodeAt(1) === 0x45)) {
992
+ this._execTeaBin(data, argv, ctx);
993
+ return;
994
+ }
995
+
996
+ // Treat as shell script
997
+ const saved = { ...this.env };
998
+ ctx.vars["0"] = argv[0];
999
+ argv.slice(1).forEach((a, i) => { ctx.vars[String(i+1)] = a; });
1000
+ this.exec(data, { stdin: ctx.stdin, stdout: ctx.stdout, stderr: ctx.stderr });
1001
+ Object.assign(this.env, saved);
1002
+ }
1003
+
1004
+ _execTeaBin(data, argv, ctx) {
1005
+ try {
1006
+ // Encode file data as Uint8Array (assuming it's stored as a binary string)
1007
+ const bytes = new Uint8Array(data.length);
1008
+ for (let i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i) & 0xFF;
1009
+ const vm = new TeaBinVM({
1010
+ interrupts: {
1011
+ 0x80: (vm) => {
1012
+ // Write syscall: R0=1 → stdout
1013
+ if (vm._getReg(0) === 1) {
1014
+ const ptr = vm._getReg(2);
1015
+ const len = vm._getReg(3);
1016
+ let s = "";
1017
+ for (let i = 0; i < len; i++) s += String.fromCharCode(vm.mem[(ptr+i)&0xFFFF]);
1018
+ ctx.stdout.push(s);
1019
+ vm._setReg(0, len);
1020
+ }
1021
+ }
1022
+ }
1023
+ });
1024
+ vm.load(bytes);
1025
+ this._exitCode = vm.run();
1026
+ for (const o of vm.stdout) ctx.stdout.push(typeof o === "number" ? String.fromCharCode(o) : String(o));
1027
+ } catch(e) {
1028
+ ctx.stderr.push(`${argv[0]}: ${e.message}\n`);
1029
+ this._exitCode = 1;
1030
+ }
1031
+ }
1032
+
1033
+ _builtin(cmd, args, ctx) {
1034
+ switch (cmd) {
1035
+ case "echo": {
1036
+ let newline = true, i = 0;
1037
+ if (args[0] === "-n") { newline = false; i = 1; }
1038
+ ctx.stdout.push(args.slice(i).join(" ") + (newline ? "\n" : ""));
1039
+ this._exitCode = 0;
1040
+ return true;
1041
+ }
1042
+ case "printf": {
1043
+ let fmt = args[0] || "";
1044
+ let ai = 1;
1045
+ const out = fmt.replace(/%[diouxXeEfgGsqcb%]/g, (m) => {
1046
+ if (m === "%%") return "%";
1047
+ const a = args[ai++] || "";
1048
+ if (m === "%d" || m === "%i") return String(parseInt(a,10)||0);
1049
+ if (m === "%s") return a;
1050
+ if (m === "%x") return (parseInt(a,10)||0).toString(16);
1051
+ if (m === "%o") return (parseInt(a,10)||0).toString(8);
1052
+ return a;
1053
+ }).replace(/\\n/g,"\n").replace(/\\t/g,"\t").replace(/\\r/g,"\r");
1054
+ ctx.stdout.push(out);
1055
+ this._exitCode = 0;
1056
+ return true;
1057
+ }
1058
+ case "cd": {
1059
+ const target = args[0] || ctx.vars.HOME || "/";
1060
+ const p = target.startsWith("/") ? target : (ctx.vars.PWD || "/") + "/" + target;
1061
+ const norm = normPath(p);
1062
+ if (!this._fs.exists(norm)) {
1063
+ ctx.stderr.push(`cd: ${target}: No such file or directory\n`);
1064
+ this._exitCode = 1;
1065
+ } else {
1066
+ ctx.vars.PWD = norm;
1067
+ this.env.PWD = norm;
1068
+ this._exitCode = 0;
1069
+ }
1070
+ return true;
1071
+ }
1072
+ case "pwd": { ctx.stdout.push((ctx.vars.PWD || "/") + "\n"); this._exitCode = 0; return true; }
1073
+ case "export": {
1074
+ for (const a of args) {
1075
+ if (a.includes("=")) {
1076
+ const [k, ...v] = a.split("=");
1077
+ ctx.vars[k] = v.join("=");
1078
+ this.env[k] = ctx.vars[k];
1079
+ } else {
1080
+ this.env[a] = ctx.vars[a] || "";
1081
+ }
1082
+ }
1083
+ this._exitCode = 0;
1084
+ return true;
1085
+ }
1086
+ case "unset": { for (const a of args) { delete ctx.vars[a]; delete this.env[a]; } this._exitCode = 0; return true; }
1087
+ case "env":
1088
+ case "printenv": {
1089
+ if (args.length) { ctx.stdout.push((ctx.vars[args[0]] || "") + "\n"); }
1090
+ else { for (const [k,v] of Object.entries(ctx.vars)) ctx.stdout.push(`${k}=${v}\n`); }
1091
+ this._exitCode = 0; return true;
1092
+ }
1093
+ case "set": {
1094
+ for (const [k,v] of Object.entries(ctx.vars)) ctx.stdout.push(`${k}=${v}\n`);
1095
+ this._exitCode = 0; return true;
1096
+ }
1097
+ case "alias": {
1098
+ if (args.length === 0) {
1099
+ for (const [k,v] of Object.entries(this.aliases)) ctx.stdout.push(`alias ${k}='${v}'\n`);
1100
+ } else {
1101
+ for (const a of args) {
1102
+ const eq = a.indexOf("=");
1103
+ if (eq < 0) { ctx.stdout.push(`alias ${a}='${this.aliases[a] || ""}'\n`); }
1104
+ else { this.aliases[a.slice(0, eq)] = a.slice(eq + 1).replace(/^'|'$/g, ""); }
1105
+ }
1106
+ }
1107
+ this._exitCode = 0; return true;
1108
+ }
1109
+ case "unalias": { for (const a of args) delete this.aliases[a]; this._exitCode = 0; return true; }
1110
+ case "type":
1111
+ case "which": {
1112
+ for (const a of args) {
1113
+ if (this.aliases[a]) { ctx.stdout.push(`${a} is aliased to '${this.aliases[a]}'\n`); continue; }
1114
+ if (["echo","printf","cd","pwd","export","unset","env","alias","type","which","true","false","exit","read","test","[","source",".","sleep","kill"].includes(a)) {
1115
+ ctx.stdout.push(`${a} is a shell builtin\n`); continue;
1116
+ }
1117
+ const bin = this._findBin(a, ctx.vars.PATH);
1118
+ if (bin) ctx.stdout.push(bin + "\n");
1119
+ else ctx.stderr.push(`${a}: not found\n`);
1120
+ }
1121
+ this._exitCode = 0; return true;
1122
+ }
1123
+ case "true": { this._exitCode = 0; return true; }
1124
+ case "false": { this._exitCode = 1; return true; }
1125
+ case "exit": { throw { _exit: true, code: parseInt(args[0], 10) || 0 }; }
1126
+ case "source":
1127
+ case ".": {
1128
+ const path = args[0];
1129
+ if (!path) { ctx.stderr.push("source: filename argument required\n"); this._exitCode = 1; return true; }
1130
+ try {
1131
+ const script = this._fs.readFile(path, "utf8");
1132
+ const r = this.exec(script, { stdin: ctx.stdin, stdout: ctx.stdout, stderr: ctx.stderr });
1133
+ this._exitCode = r.code;
1134
+ } catch(e) { ctx.stderr.push(`source: ${path}: ${e.message}\n`); this._exitCode = 1; }
1135
+ return true;
1136
+ }
1137
+ case "read": {
1138
+ const varName = args[0] || "REPLY";
1139
+ const line = (ctx.stdin.shift() || "").replace(/\n$/, "");
1140
+ ctx.vars[varName] = line;
1141
+ this._exitCode = 0; return true;
1142
+ }
1143
+ case "test":
1144
+ case "[": {
1145
+ // Basic test expressions
1146
+ const a = args.slice(0, args[args.length-1] === "]" ? -1 : undefined);
1147
+ this._exitCode = this._test(a, ctx) ? 0 : 1;
1148
+ return true;
1149
+ }
1150
+ case "sleep": {
1151
+ // no-op in virtual fs
1152
+ this._exitCode = 0; return true;
1153
+ }
1154
+ case "cat": {
1155
+ if (args.length === 0) {
1156
+ ctx.stdout.push(...ctx.stdin); this._exitCode = 0; return true;
1157
+ }
1158
+ for (const f of args) {
1159
+ try { ctx.stdout.push(this._fs.readFile(f, "utf8")); }
1160
+ catch(e) { ctx.stderr.push(`cat: ${f}: ${e.message}\n`); this._exitCode = 1; return true; }
1161
+ }
1162
+ this._exitCode = 0; return true;
1163
+ }
1164
+ case "ls": {
1165
+ const dir = args.find(a => !a.startsWith("-")) || ctx.vars.PWD || "/";
1166
+ const showHidden = args.includes("-a") || args.includes("-la") || args.includes("-al");
1167
+ const long = args.includes("-l") || args.includes("-la") || args.includes("-al");
1168
+ try {
1169
+ const entries = this._fs.readdir(dir, { withFileTypes: true });
1170
+ for (const e of entries) {
1171
+ if (!showHidden && e.name.startsWith(".")) continue;
1172
+ if (long) {
1173
+ const st = this._fs.stat(dir === "/" ? "/" + e.name : dir + "/" + e.name);
1174
+ const type = e.isDirectory() ? "d" : e.isSymbolicLink() ? "l" : "-";
1175
+ const mode = this._formatMode(st.mode);
1176
+ ctx.stdout.push(`${type}${mode} ${st.nlink} ${st.uid} ${st.gid} ${String(st.size).padStart(6)} ${e.name}\n`);
1177
+ } else {
1178
+ ctx.stdout.push(e.name + " ");
1179
+ }
1180
+ }
1181
+ if (!long) ctx.stdout.push("\n");
1182
+ this._exitCode = 0;
1183
+ } catch(e) { ctx.stderr.push(`ls: ${dir}: ${e.message}\n`); this._exitCode = 1; }
1184
+ return true;
1185
+ }
1186
+ case "mkdir": {
1187
+ const recursive = args.includes("-p");
1188
+ const dirs = args.filter(a => !a.startsWith("-"));
1189
+ for (const d of dirs) {
1190
+ try { this._fs.mkdir(d, { recursive }); }
1191
+ catch(e) { ctx.stderr.push(`mkdir: ${d}: ${e.message}\n`); this._exitCode = 1; return true; }
1192
+ }
1193
+ this._exitCode = 0; return true;
1194
+ }
1195
+ case "rm": {
1196
+ const recursive = args.includes("-r") || args.includes("-rf") || args.includes("-fr");
1197
+ const force = args.includes("-f") || args.includes("-rf") || args.includes("-fr");
1198
+ const files = args.filter(a => !a.startsWith("-"));
1199
+ for (const f of files) {
1200
+ try { this._fs.rm(f, { recursive, force }); }
1201
+ catch(e) { if (!force) { ctx.stderr.push(`rm: ${f}: ${e.message}\n`); this._exitCode = 1; return true; } }
1202
+ }
1203
+ this._exitCode = 0; return true;
1204
+ }
1205
+ case "cp": {
1206
+ const recursive = args.includes("-r") || args.includes("-R");
1207
+ const paths = args.filter(a => !a.startsWith("-"));
1208
+ if (paths.length < 2) { ctx.stderr.push("cp: missing file operand\n"); this._exitCode = 1; return true; }
1209
+ try { this._fs.cp(paths[0], paths[1], { recursive }); this._exitCode = 0; }
1210
+ catch(e) { ctx.stderr.push(`cp: ${e.message}\n`); this._exitCode = 1; }
1211
+ return true;
1212
+ }
1213
+ case "mv": {
1214
+ if (args.length < 2) { ctx.stderr.push("mv: missing file operand\n"); this._exitCode = 1; return true; }
1215
+ try { this._fs.rename(args[0], args[1]); this._exitCode = 0; }
1216
+ catch(e) { ctx.stderr.push(`mv: ${e.message}\n`); this._exitCode = 1; }
1217
+ return true;
1218
+ }
1219
+ case "chmod": {
1220
+ if (args.length < 2) { ctx.stderr.push("chmod: missing operand\n"); this._exitCode = 1; return true; }
1221
+ const mode = parseInt(args[0], 8);
1222
+ try { this._fs.chmod(args[1], mode); this._exitCode = 0; }
1223
+ catch(e) { ctx.stderr.push(`chmod: ${e.message}\n`); this._exitCode = 1; }
1224
+ return true;
1225
+ }
1226
+ case "touch": {
1227
+ for (const f of args) {
1228
+ try {
1229
+ if (!this._fs.exists(f)) this._fs.writeFile(f, "");
1230
+ else this._fs.utimes(f, new Date(), new Date());
1231
+ } catch(e) { ctx.stderr.push(`touch: ${f}: ${e.message}\n`); this._exitCode = 1; return true; }
1232
+ }
1233
+ this._exitCode = 0; return true;
1234
+ }
1235
+ case "grep": {
1236
+ const pattern = args[0];
1237
+ const files = args.slice(1);
1238
+ if (!pattern) { ctx.stderr.push("grep: usage: grep PATTERN [FILE...]\n"); this._exitCode = 1; return true; }
1239
+ const re = new RegExp(pattern);
1240
+ let found = false;
1241
+ const search = (text, label) => {
1242
+ for (const line of text.split("\n")) {
1243
+ if (re.test(line)) { ctx.stdout.push((files.length > 1 ? label + ":" : "") + line + "\n"); found = true; }
1244
+ }
1245
+ };
1246
+ if (files.length === 0) { search(ctx.stdin.join(""), "stdin"); }
1247
+ else {
1248
+ for (const f of files) {
1249
+ try { search(this._fs.readFile(f, "utf8"), f); }
1250
+ catch(e) { ctx.stderr.push(`grep: ${f}: ${e.message}\n`); }
1251
+ }
1252
+ }
1253
+ this._exitCode = found ? 0 : 1; return true;
1254
+ }
1255
+ case "find": {
1256
+ const path = args[0] || ctx.vars.PWD || "/";
1257
+ const opts = {};
1258
+ for (let i = 1; i < args.length; i++) {
1259
+ if (args[i] === "-type" && args[i+1]) { opts.type = args[++i]; }
1260
+ if (args[i] === "-name" && args[i+1]) { opts.name = args[++i]; }
1261
+ }
1262
+ try {
1263
+ const results = this._fs.find(path, opts);
1264
+ for (const r of results) ctx.stdout.push(r + "\n");
1265
+ this._exitCode = 0;
1266
+ } catch(e) { ctx.stderr.push(`find: ${e.message}\n`); this._exitCode = 1; }
1267
+ return true;
1268
+ }
1269
+ case "wc": {
1270
+ const text = args.length > 0 ? this._fs.readFile(args[0], "utf8") : ctx.stdin.join("");
1271
+ const lines = (text.match(/\n/g) || []).length;
1272
+ const words = text.split(/\s+/).filter(Boolean).length;
1273
+ const chars = text.length;
1274
+ ctx.stdout.push(` ${lines} ${words} ${chars}\n`);
1275
+ this._exitCode = 0; return true;
1276
+ }
1277
+ case "head": {
1278
+ const n = args.includes("-n") ? parseInt(args[args.indexOf("-n")+1]) : 10;
1279
+ const src = args.find(a => !a.startsWith("-")) || null;
1280
+ const text = src ? this._fs.readFile(src, "utf8") : ctx.stdin.join("");
1281
+ ctx.stdout.push(text.split("\n").slice(0, n).join("\n") + "\n");
1282
+ this._exitCode = 0; return true;
1283
+ }
1284
+ case "tail": {
1285
+ const n = args.includes("-n") ? parseInt(args[args.indexOf("-n")+1]) : 10;
1286
+ const src = args.find(a => !a.startsWith("-")) || null;
1287
+ const text = src ? this._fs.readFile(src, "utf8") : ctx.stdin.join("");
1288
+ const lines = text.split("\n");
1289
+ ctx.stdout.push(lines.slice(-n).join("\n") + "\n");
1290
+ this._exitCode = 0; return true;
1291
+ }
1292
+ case "sort": {
1293
+ const text = ctx.stdin.join("");
1294
+ const sorted = text.split("\n").filter(Boolean).sort().join("\n") + "\n";
1295
+ ctx.stdout.push(sorted); this._exitCode = 0; return true;
1296
+ }
1297
+ case "uniq": {
1298
+ const text = ctx.stdin.join("");
1299
+ const lines = text.split("\n");
1300
+ const out = [lines[0]];
1301
+ for (let i = 1; i < lines.length; i++) if (lines[i] !== lines[i-1]) out.push(lines[i]);
1302
+ ctx.stdout.push(out.join("\n") + "\n"); this._exitCode = 0; return true;
1303
+ }
1304
+ case "tr": {
1305
+ const [set1, set2] = args;
1306
+ let text = ctx.stdin.join("");
1307
+ if (set1 && set2) {
1308
+ for (let i = 0; i < Math.min(set1.length, set2.length); i++)
1309
+ text = text.split(set1[i]).join(set2[i]);
1310
+ }
1311
+ ctx.stdout.push(text); this._exitCode = 0; return true;
1312
+ }
1313
+ case "sed": {
1314
+ // Basic s/pat/rep/g support
1315
+ const expr = args[0] || "";
1316
+ const src = args[1] || null;
1317
+ let text = src ? this._fs.readFile(src, "utf8") : ctx.stdin.join("");
1318
+ const m = expr.match(/^s\/(.+)\/(.*)\/([gim]*)$/);
1319
+ if (m) {
1320
+ const re = new RegExp(m[1], m[3] || "");
1321
+ text = text.replace(re, m[2]);
1322
+ }
1323
+ ctx.stdout.push(text); this._exitCode = 0; return true;
1324
+ }
1325
+ case "awk": {
1326
+ // Very minimal: print $N support
1327
+ const prog = args[0] || "{ print }";
1328
+ const text = ctx.stdin.join("");
1329
+ for (const line of text.split("\n").filter(Boolean)) {
1330
+ const fields = line.split(/\s+/);
1331
+ const out = prog
1332
+ .replace(/\$(\d+)/g, (_, n) => fields[parseInt(n)-1] || "")
1333
+ .replace(/\$0/g, line)
1334
+ .replace(/print\s*/g, "");
1335
+ ctx.stdout.push(out.trim() + "\n");
1336
+ }
1337
+ this._exitCode = 0; return true;
1338
+ }
1339
+ default: return false;
1340
+ }
1341
+ }
1342
+
1343
+ _test(args, ctx) {
1344
+ if (args.length === 0) return false;
1345
+ if (args[0] === "!" ) return !this._test(args.slice(1), ctx);
1346
+ if (args[1] === "-a") return this._test(args.slice(0,1), ctx) && this._test(args.slice(2), ctx);
1347
+ if (args[1] === "-o") return this._test(args.slice(0,1), ctx) || this._test(args.slice(2), ctx);
1348
+ if (args[0] === "-f") return this._fs.exists(args[1]) && this._fs.stat(args[1]).isFile();
1349
+ if (args[0] === "-d") return this._fs.exists(args[1]) && this._fs.stat(args[1]).isDirectory();
1350
+ if (args[0] === "-e") return this._fs.exists(args[1]);
1351
+ if (args[0] === "-n") return (args[1] || "").length > 0;
1352
+ if (args[0] === "-z") return (args[1] || "").length === 0;
1353
+ if (args[1] === "=") return args[0] === args[2];
1354
+ if (args[1] === "!=") return args[0] !== args[2];
1355
+ if (args[1] === "-eq") return parseInt(args[0]) === parseInt(args[2]);
1356
+ if (args[1] === "-ne") return parseInt(args[0]) !== parseInt(args[2]);
1357
+ if (args[1] === "-lt") return parseInt(args[0]) < parseInt(args[2]);
1358
+ if (args[1] === "-le") return parseInt(args[0]) <= parseInt(args[2]);
1359
+ if (args[1] === "-gt") return parseInt(args[0]) > parseInt(args[2]);
1360
+ if (args[1] === "-ge") return parseInt(args[0]) >= parseInt(args[2]);
1361
+ return args[0].length > 0;
1362
+ }
1363
+
1364
+ _formatMode(mode) {
1365
+ const bits = "rwxrwxrwx";
1366
+ return bits.split("").map((b, i) => (mode & (1 << (8-i))) ? b : "-").join("");
1367
+ }
1368
+ }
1369
+
1370
+ // ===========================================================================
1371
+ // REPL
1372
+ // ===========================================================================
1373
+
1374
+ class ShRepl extends EventEmitter {
1375
+ constructor(sh) {
1376
+ super();
1377
+ this._sh = sh;
1378
+ this._history = [];
1379
+ this._histIdx = -1;
1380
+ this.running = true;
1381
+ }
1382
+
1383
+ /** Feed a line of input, returns { stdout, stderr, code } */
1384
+ line(input) {
1385
+ if (!this.running) return { stdout: "", stderr: "Session closed\n", code: 1 };
1386
+ this._history.push(input);
1387
+ this._histIdx = this._history.length;
1388
+ const result = this._sh.exec(input);
1389
+ this.emit("result", result);
1390
+ return result;
1391
+ }
1392
+
1393
+ historyUp() { if (this._histIdx > 0) this._histIdx--; return this._history[this._histIdx] || ""; }
1394
+ historyDown() { if (this._histIdx < this._history.length) this._histIdx++; return this._history[this._histIdx] || ""; }
1395
+ close() { this.running = false; this.emit("close"); }
1396
+ prompt() { return this._sh.env.PS1 || "$ "; }
1397
+ }
1398
+
1399
+ // ===========================================================================
1400
+ // Main class: TeaF
1401
+ // ===========================================================================
1402
+
1403
+ class TeaF extends EventEmitter {
1404
+ /**
1405
+ * @param {Object} opts
1406
+ * @param {string} opts.label
1407
+ * @param {number} opts.umask
1408
+ * @param {string} opts.mode - 'posix' (default) | 'win' | 'url'
1409
+ * @param {number} opts.uid - current uid for permission checks (0 = root)
1410
+ * @param {number} opts.gid
1411
+ * @param {boolean} opts.enforcePermissions
1412
+ */
1413
+ constructor(opts = {}) {
1414
+ super();
1415
+ this._db = null;
1416
+ this._fds = new Map();
1417
+ this._fdCounter = 3;
1418
+ this._watchers = new Map();
1419
+ this._mounts = new Map();
1420
+ this._umask = opts.umask !== undefined ? opts.umask : 0o022;
1421
+ this._uid = opts.uid !== undefined ? opts.uid : 0;
1422
+ this._gid = opts.gid !== undefined ? opts.gid : 0;
1423
+ this._enforcePerms = opts.enforcePermissions !== false; // default true
1424
+ this._pathAdapter = new PathAdapter(opts.mode || "posix");
1425
+ this._sh = null; // lazy-init shell
1426
+ this._init(opts.label || "tea/f");
1427
+ }
1428
+
1429
+ // -------------------------------------------------------------------------
1430
+ // Internal bootstrap
1431
+ // -------------------------------------------------------------------------
1432
+
1433
+ _init(label) {
1434
+ const t = now();
1435
+ this._db = {
1436
+ meta: { created: t, label, version: VERSION },
1437
+ tree: {
1438
+ "/": { type: "dir", mode: 0o755, uid: 0, gid: 0, atime: t, mtime: t, ctime: t, nlink: 2, children: {} },
1439
+ },
1440
+ inodeCounter: 1,
1441
+ };
1442
+ // Create standard directories
1443
+ for (const d of ["bin", "etc", "usr", "usr/bin", "usr/local", "usr/local/bin", "home", "root", "tmp", "var", "lib", "mnt", "proc", "dev"]) {
1444
+ this._mkdirOne("/" + d, 0o755, ...this._parentAndBase("/" + d));
1445
+ }
1446
+ }
1447
+
1448
+ _parentAndBase(p) {
1449
+ const { dir, base } = splitPath(p);
1450
+ const parentId = this._resolve(dir);
1451
+ return [parentId, base];
1452
+ }
1453
+
1454
+ _newId() { this._db.inodeCounter++; return `node_${this._db.inodeCounter}`; }
1455
+
1456
+ // -------------------------------------------------------------------------
1457
+ // Path translation (mode-aware)
1458
+ // -------------------------------------------------------------------------
1459
+
1460
+ _p(path) {
1461
+ const internal = this._pathAdapter.toInternal(path);
1462
+ return normPath(internal);
1463
+ }
1464
+
1465
+ _ext(path) { return this._pathAdapter.toExternal(path); }
1466
+
1467
+ // -------------------------------------------------------------------------
1468
+ // Permission checking
1469
+ // -------------------------------------------------------------------------
1470
+
1471
+ _checkPerm(node, need) {
1472
+ if (!this._enforcePerms || this._uid === 0) return; // root bypasses
1473
+ const mode = node.mode & 0o777;
1474
+ let allowed = mode & 0o007; // other
1475
+ if (node.gid === this._gid) allowed |= (mode >> 3) & 0o007;
1476
+ if (node.uid === this._uid) allowed |= (mode >> 6) & 0o007;
1477
+ // need: 4=read, 2=write, 1=exec
1478
+ if ((allowed & need) !== need) throw err("EACCES");
1479
+ }
1480
+
1481
+ _checkRead(node) { this._checkPerm(node, 4); }
1482
+ _checkWrite(node) { this._checkPerm(node, 2); }
1483
+ _checkExec(node) { this._checkPerm(node, 1); }
1484
+
1485
+ // -------------------------------------------------------------------------
1486
+ // Node resolution
1487
+ // -------------------------------------------------------------------------
1488
+
1489
+ _resolve(path, { followSymlinks = true, maxLinks = 40 } = {}) {
1490
+ const p = normPath(typeof path === "string" ? path : "/");
1491
+ if (p === "/") return "/";
1492
+ const parts = p.split("/").filter(Boolean);
1493
+ let cur = "/";
1494
+ for (let i = 0; i < parts.length; i++) {
1495
+ // Check for mount point at current position
1496
+ const curPath = "/" + parts.slice(0, i).join("/") || "/";
1497
+ if (this._mounts.has(curPath)) {
1498
+ // Delegate to the mounted fs
1499
+ const mounted = this._mounts.get(curPath);
1500
+ const subPath = "/" + parts.slice(i).join("/");
1501
+ return "__mount__:" + curPath + ":" + subPath;
1502
+ }
1503
+
1504
+ const node = this._db.tree[cur];
1505
+ if (!node) throw err("ENOENT", path);
1506
+ if (node.type !== "dir") throw err("ENOTDIR", path);
1507
+ this._checkExec(node);
1508
+ const childId = node.children[parts[i]];
1509
+ if (!childId) throw err("ENOENT", path);
1510
+ const childNode = this._db.tree[childId];
1511
+ if (!childNode) throw err("ENOENT", path);
1512
+ if (childNode.type === "symlink" && (followSymlinks || i < parts.length - 1)) {
1513
+ if (maxLinks <= 0) throw err("ELOOP", path);
1514
+ const target = normPath(childNode.link.startsWith("/")
1515
+ ? childNode.link
1516
+ : "/" + parts.slice(0, i).join("/") + "/" + childNode.link);
1517
+ cur = this._resolve(target, { followSymlinks: true, maxLinks: maxLinks - 1 });
1518
+ } else {
1519
+ cur = childId;
1520
+ }
1521
+ }
1522
+ return cur;
1523
+ }
1524
+
1525
+ _isMountRef(id) { return typeof id === "string" && id.startsWith("__mount__:"); }
1526
+
1527
+ _mountedOp(id, method, args) {
1528
+ const [, mountPath, subPath] = id.split(":");
1529
+ const mountedFs = this._mounts.get(mountPath);
1530
+ if (!mountedFs) throw err("ENOENT");
1531
+ return mountedFs[method](subPath, ...args);
1532
+ }
1533
+
1534
+ _node(nodeId) { return this._db.tree[nodeId]; }
1535
+
1536
+ _getNode(path, opts) {
1537
+ const normP = this._p(path);
1538
+ const id = this._resolve(normP, opts);
1539
+ if (this._isMountRef(id)) {
1540
+ const [, , subPath] = id.split(":");
1541
+ const [, mountPath] = id.split(":");
1542
+ const mountedFs = this._mounts.get(mountPath);
1543
+ return { id, node: { type: "mount", _fs: mountedFs, _sub: subPath } };
1544
+ }
1545
+ const node = this._db.tree[id];
1546
+ if (!node) throw err("ENOENT", path);
1547
+ return { id, node };
1548
+ }
1549
+
1550
+ _touch(nodeId, fields = ["atime", "mtime", "ctime"]) {
1551
+ if (this._isMountRef(nodeId)) return;
1552
+ const n = now();
1553
+ const node = this._db.tree[nodeId];
1554
+ if (!node) return;
1555
+ for (const f of fields) node[f] = n;
1556
+ }
1557
+
1558
+ // -------------------------------------------------------------------------
1559
+ // Watcher notification (real events)
1560
+ // -------------------------------------------------------------------------
1561
+
1562
+ _notify(nodeId, eventType, filePath) {
1563
+ for (const [, w] of this._watchers) {
1564
+ // Check if this nodeId matches watcher's watched node or is a descendant
1565
+ const matches = (w.id === nodeId) || this._isDescendantById(nodeId, w.id);
1566
+ if (matches) {
1567
+ try { w.emitter.emit(eventType, eventType, filePath || w.path); } catch(_) {}
1568
+ if (w.listener) try { w.listener(eventType, filePath || w.path); } catch(_) {}
1569
+ }
1570
+ }
1571
+ // Also fire top-level FS events
1572
+ this.emit(eventType, filePath || nodeId);
1573
+ }
1574
+
1575
+ _isDescendantById(childId, ancestorId) {
1576
+ if (childId === ancestorId) return false;
1577
+ // Walk all dirs to see if ancestorId is a parent
1578
+ const ancestorNode = this._db.tree[ancestorId];
1579
+ if (!ancestorNode || ancestorNode.type !== "dir") return false;
1580
+ for (const cid of Object.values(ancestorNode.children)) {
1581
+ if (cid === childId) return true;
1582
+ if (this._isDescendantById(childId, cid)) return true;
1583
+ }
1584
+ return false;
1585
+ }
1586
+
1587
+ // -------------------------------------------------------------------------
1588
+ // Path utilities
1589
+ // -------------------------------------------------------------------------
1590
+
1591
+ join(...parts) { return this._ext(normPath(parts.map(p => this._p(p)).join("/"))); }
1592
+ resolve(...parts) { return this._ext(normPath(parts.map(p => this._p(p)).join("/"))); }
1593
+ dirname(path) { return this._ext(splitPath(this._p(path)).dir); }
1594
+ basename(path, ext) {
1595
+ let b = splitPath(this._p(path)).base || "/";
1596
+ if (ext && b.endsWith(ext)) b = b.slice(0, -ext.length);
1597
+ return b;
1598
+ }
1599
+ extname(path) { const b = this.basename(path); const dot = b.lastIndexOf("."); return dot > 0 ? b.slice(dot) : ""; }
1600
+ relative(from, to) {
1601
+ const f = this._p(from).split("/").filter(Boolean);
1602
+ const t = this._p(to).split("/").filter(Boolean);
1603
+ let i = 0;
1604
+ while (i < f.length && i < t.length && f[i] === t[i]) i++;
1605
+ return [...f.slice(i).map(() => ".."), ...t.slice(i)].join("/") || ".";
1606
+ }
1607
+
1608
+ // -------------------------------------------------------------------------
1609
+ // exists / access / stat
1610
+ // -------------------------------------------------------------------------
1611
+
1612
+ exists(path) { try { this._resolve(this._p(path)); return true; } catch(_) { return false; } }
1613
+
1614
+ access(path, mode = 0) {
1615
+ const { node } = this._getNode(path);
1616
+ if (mode !== 0) this._checkPerm(node, mode);
1617
+ return true;
1618
+ }
1619
+
1620
+ stat(path) {
1621
+ const p = this._p(path);
1622
+ const { id, node } = this._getNode(p, { followSymlinks: true });
1623
+ if (node.type === "mount") return node._fs.stat(node._sub);
1624
+ this._checkRead(node);
1625
+ this._touch(id, ["atime"]);
1626
+ return new Stats(node, id);
1627
+ }
1628
+
1629
+ lstat(path) {
1630
+ const p = this._p(path);
1631
+ const { id, node } = this._getNode(p, { followSymlinks: false });
1632
+ if (node.type === "mount") return node._fs.lstat(node._sub);
1633
+ return new Stats(node, id);
1634
+ }
1635
+
1636
+ fstat(fd) {
1637
+ const handle = this._fds.get(fd);
1638
+ if (!handle) throw err("EBADF");
1639
+ const node = this._db.tree[handle.nodeId];
1640
+ if (!node) throw err("EBADF");
1641
+ return new Stats(node, handle.nodeId);
1642
+ }
1643
+
1644
+ // -------------------------------------------------------------------------
1645
+ // mkdir / rmdir / readdir
1646
+ // -------------------------------------------------------------------------
1647
+
1648
+ mkdir(path, opts = {}) {
1649
+ const mode = (typeof opts === "number" ? opts : (opts.mode || (0o777 & ~this._umask)));
1650
+ const recursive = opts.recursive || false;
1651
+ const p = this._p(path);
1652
+
1653
+ if (recursive) {
1654
+ const parts = p.split("/").filter(Boolean);
1655
+ let cur = "/";
1656
+ for (let i = 0; i < parts.length; i++) {
1657
+ const parentNode = this._db.tree[cur];
1658
+ if (!parentNode.children[parts[i]]) {
1659
+ const partial = "/" + parts.slice(0, i + 1).join("/");
1660
+ const { dir: pdir, base: pbase } = splitPath(partial);
1661
+ const parentId = this._resolve(pdir);
1662
+ this._mkdirOne(partial, mode, parentId, pbase);
1663
+ }
1664
+ cur = this._db.tree[cur].children[parts[i]];
1665
+ }
1666
+ return;
1667
+ }
1668
+
1669
+ const { dir, base } = splitPath(p);
1670
+ if (!base) throw err("EEXIST", path);
1671
+ const { id: parentId, node: parentNode } = this._getNode(dir, { followSymlinks: true });
1672
+ if (parentNode.type !== "dir") throw err("ENOTDIR", dir);
1673
+ this._checkWrite(parentNode);
1674
+ if (parentNode.children[base]) throw err("EEXIST", path);
1675
+ this._mkdirOne(p, mode, parentId, base);
1676
+ }
1677
+
1678
+ _mkdirOne(path, mode, parentId, base) {
1679
+ const t = now();
1680
+ const id = this._newId();
1681
+ this._db.tree[id] = { type: "dir", mode, uid: this._uid, gid: this._gid, atime: t, mtime: t, ctime: t, nlink: 2, children: {} };
1682
+ this._db.tree[parentId].children[base] = id;
1683
+ this._touch(parentId, ["mtime", "ctime"]);
1684
+ this._notify(id, "mkdir", path);
1685
+ }
1686
+
1687
+ rmdir(path, opts = {}) {
1688
+ const p = this._p(path);
1689
+ const { id, node } = this._getNode(p, { followSymlinks: true });
1690
+ if (node.type !== "dir") throw err("ENOTDIR", path);
1691
+ if (opts.recursive) { this._rmRecursive(p); return; }
1692
+ if (Object.keys(node.children).length > 0) throw err("ENOTEMPTY", path);
1693
+ const { dir, base } = splitPath(p);
1694
+ const { id: parentId, node: parentNode } = this._getNode(dir);
1695
+ this._checkWrite(parentNode);
1696
+ delete this._db.tree[parentId].children[base];
1697
+ delete this._db.tree[id];
1698
+ this._touch(parentId, ["mtime", "ctime"]);
1699
+ this._notify(parentId, "rmdir", path);
1700
+ }
1701
+
1702
+ readdir(path, opts = {}) {
1703
+ const { id, node } = this._getNode(path, { followSymlinks: true });
1704
+ if (node.type === "mount") return node._fs.readdir(node._sub, opts);
1705
+ if (node.type !== "dir") throw err("ENOTDIR", path);
1706
+ this._checkRead(node);
1707
+ this._touch(id, ["atime"]);
1708
+ const names = Object.keys(node.children);
1709
+ if (opts.withFileTypes) {
1710
+ return names.map(name => ({
1711
+ name,
1712
+ isFile: () => this._db.tree[node.children[name]]?.type === "file",
1713
+ isDirectory: () => this._db.tree[node.children[name]]?.type === "dir",
1714
+ isSymbolicLink:() => this._db.tree[node.children[name]]?.type === "symlink",
1715
+ }));
1716
+ }
1717
+ return names;
1718
+ }
1719
+
1720
+ // -------------------------------------------------------------------------
1721
+ // readFile / writeFile / appendFile / truncate
1722
+ // -------------------------------------------------------------------------
1723
+
1724
+ readFile(path, opts = {}) {
1725
+ const encoding = typeof opts === "string" ? opts : opts.encoding;
1726
+ const { id, node } = this._getNode(path, { followSymlinks: true });
1727
+ if (node.type === "mount") return node._fs.readFile(node._sub, opts);
1728
+ if (node.type === "dir") throw err("EISDIR", path);
1729
+ this._checkRead(node);
1730
+ this._touch(id, ["atime"]);
1731
+ const data = node.data || "";
1732
+ return encoding ? data : (typeof Buffer !== "undefined" ? Buffer.from(data) : data);
1733
+ }
1734
+
1735
+ writeFile(path, data, opts = {}) {
1736
+ const mode = typeof opts === "object" ? (opts.mode || (0o666 & ~this._umask)) : (0o666 & ~this._umask);
1737
+ const flag = typeof opts === "object" ? (opts.flag || "w") : "w";
1738
+ const p = this._p(path);
1739
+ const str = (data !== null && data !== undefined)
1740
+ ? (typeof data === "object" && data.toString ? data.toString() : String(data))
1741
+ : "";
1742
+
1743
+ let id, node;
1744
+ try {
1745
+ ({ id, node } = this._getNode(p, { followSymlinks: true }));
1746
+ if (node.type === "mount") { node._fs.writeFile(node._sub, data, opts); return; }
1747
+ if (node.type === "dir") throw err("EISDIR", path);
1748
+ if (flag === "wx" || flag === "ax") throw err("EEXIST", path);
1749
+ this._checkWrite(node);
1750
+ } catch(e) {
1751
+ if (e.code !== "ENOENT") throw e;
1752
+ const { dir, base } = splitPath(p);
1753
+ const { id: parentId, node: parentNode } = this._getNode(dir, { followSymlinks: true });
1754
+ if (parentNode.type !== "dir") throw err("ENOTDIR", dir);
1755
+ this._checkWrite(parentNode);
1756
+ id = this._newId();
1757
+ const t = now();
1758
+ this._db.tree[id] = { type: "file", mode, uid: this._uid, gid: this._gid, atime: t, mtime: t, ctime: t, nlink: 1, data: "" };
1759
+ this._db.tree[parentId].children[base] = id;
1760
+ this._touch(parentId, ["mtime", "ctime"]);
1761
+ node = this._db.tree[id];
1762
+ }
1763
+
1764
+ node.data = (flag === "a" || flag === "ax") ? (node.data || "") + str : str;
1765
+ this._touch(id, ["mtime", "ctime"]);
1766
+ this._notify(id, "write", p);
1767
+ }
1768
+
1769
+ appendFile(path, data, opts = {}) {
1770
+ const o = typeof opts === "string" ? { encoding: opts } : opts;
1771
+ this.writeFile(path, data, { ...o, flag: "a" });
1772
+ }
1773
+
1774
+ truncate(path, len = 0) {
1775
+ const { id, node } = this._getNode(path, { followSymlinks: true });
1776
+ if (node.type !== "file") throw err("EINVAL", path);
1777
+ this._checkWrite(node);
1778
+ const d = node.data || "";
1779
+ node.data = d.length >= len ? d.slice(0, len) : d.padEnd(len, "\0");
1780
+ this._touch(id, ["mtime", "ctime"]);
1781
+ }
1782
+
1783
+ ftruncate(fd, len = 0) {
1784
+ const handle = this._fds.get(fd);
1785
+ if (!handle) throw err("EBADF");
1786
+ const node = this._db.tree[handle.nodeId];
1787
+ if (!node || node.type !== "file") throw err("EBADF");
1788
+ if (!handle.writable) throw err("EBADF");
1789
+ const d = node.data || "";
1790
+ node.data = d.length >= len ? d.slice(0, len) : d.padEnd(len, "\0");
1791
+ this._touch(handle.nodeId, ["mtime", "ctime"]);
1792
+ }
1793
+
1794
+ // -------------------------------------------------------------------------
1795
+ // rename / unlink / rm
1796
+ // -------------------------------------------------------------------------
1797
+
1798
+ rename(src, dst) {
1799
+ const sp = this._p(src); const dp = this._p(dst);
1800
+ if (sp === dp) return;
1801
+ const { id: srcId } = this._getNode(sp, { followSymlinks: false });
1802
+ const { dir: srcDir, base: srcBase } = splitPath(sp);
1803
+ const { dir: dstDir, base: dstBase } = splitPath(dp);
1804
+ const { id: srcParentId, node: srcParentNode } = this._getNode(srcDir);
1805
+ this._checkWrite(srcParentNode);
1806
+ const { id: dstParentId, node: dstParentNode } = this._getNode(dstDir);
1807
+ this._checkWrite(dstParentNode);
1808
+ if (dstParentNode.type !== "dir") throw err("ENOTDIR", dstDir);
1809
+ if (dstParentNode.children[dstBase]) {
1810
+ const existId = dstParentNode.children[dstBase];
1811
+ const existNode = this._db.tree[existId];
1812
+ if (existNode.type === "dir" && Object.keys(existNode.children).length > 0) throw err("ENOTEMPTY", dst);
1813
+ if (existNode.type === "dir" && this._db.tree[srcId].type !== "dir") throw err("EISDIR", dst);
1814
+ delete this._db.tree[existId];
1815
+ }
1816
+ dstParentNode.children[dstBase] = srcId;
1817
+ delete this._db.tree[srcParentId].children[srcBase];
1818
+ this._touch(srcParentId, ["mtime", "ctime"]);
1819
+ this._touch(dstParentId, ["mtime", "ctime"]);
1820
+ this._touch(srcId, ["ctime"]);
1821
+ this._notify(srcId, "rename", dp);
1822
+ }
1823
+
1824
+ unlink(path) {
1825
+ const p = this._p(path);
1826
+ const { id, node } = this._getNode(p, { followSymlinks: false });
1827
+ if (node.type === "dir") throw err("EISDIR", path);
1828
+ const { dir, base } = splitPath(p);
1829
+ const { id: parentId, node: parentNode } = this._getNode(dir);
1830
+ this._checkWrite(parentNode);
1831
+ node.nlink = (node.nlink || 1) - 1;
1832
+ delete this._db.tree[parentId].children[base];
1833
+ if (node.nlink <= 0) delete this._db.tree[id];
1834
+ this._touch(parentId, ["mtime", "ctime"]);
1835
+ this._notify(parentId, "unlink", p);
1836
+ }
1837
+
1838
+ rm(path, opts = {}) {
1839
+ const p = this._p(path);
1840
+ try {
1841
+ const { node } = this._getNode(p, { followSymlinks: false });
1842
+ if (node.type === "dir") {
1843
+ if (!opts.recursive) throw err("EISDIR", path);
1844
+ this._rmRecursive(p);
1845
+ } else {
1846
+ this.unlink(p);
1847
+ }
1848
+ } catch(e) {
1849
+ if (opts.force && e.code === "ENOENT") return;
1850
+ throw e;
1851
+ }
1852
+ }
1853
+
1854
+ _rmRecursive(path) {
1855
+ const p = normPath(path);
1856
+ const { id, node } = this._getNode(p, { followSymlinks: false });
1857
+ if (node.type === "dir") {
1858
+ for (const child of Object.keys(node.children))
1859
+ this._rmRecursive(p === "/" ? "/" + child : p + "/" + child);
1860
+ if (p !== "/") {
1861
+ const { dir, base } = splitPath(p);
1862
+ const { id: parentId } = this._getNode(dir);
1863
+ delete this._db.tree[parentId].children[base];
1864
+ delete this._db.tree[id];
1865
+ this._touch(parentId, ["mtime", "ctime"]);
1866
+ }
1867
+ } else {
1868
+ this.unlink(p);
1869
+ }
1870
+ }
1871
+
1872
+ // -------------------------------------------------------------------------
1873
+ // Links
1874
+ // -------------------------------------------------------------------------
1875
+
1876
+ link(existingPath, newPath) {
1877
+ const { id } = this._getNode(existingPath, { followSymlinks: true });
1878
+ const node = this._db.tree[id];
1879
+ if (node.type === "dir") throw err("EPERM", existingPath);
1880
+ const dp = this._p(newPath);
1881
+ const { dir, base } = splitPath(dp);
1882
+ const { id: parentId, node: parentNode } = this._getNode(dir);
1883
+ this._checkWrite(parentNode);
1884
+ if (parentNode.type !== "dir") throw err("ENOTDIR", dir);
1885
+ if (parentNode.children[base]) throw err("EEXIST", newPath);
1886
+ parentNode.children[base] = id;
1887
+ node.nlink = (node.nlink || 1) + 1;
1888
+ this._touch(id, ["ctime"]);
1889
+ this._touch(parentId, ["mtime", "ctime"]);
1890
+ }
1891
+
1892
+ symlink(target, path) {
1893
+ const p = this._p(path);
1894
+ const { dir, base } = splitPath(p);
1895
+ const { id: parentId, node: parentNode } = this._getNode(dir);
1896
+ this._checkWrite(parentNode);
1897
+ if (parentNode.type !== "dir") throw err("ENOTDIR", dir);
1898
+ if (parentNode.children[base]) throw err("EEXIST", path);
1899
+ const t = now();
1900
+ const id = this._newId();
1901
+ this._db.tree[id] = { type: "symlink", mode: 0o777, uid: this._uid, gid: this._gid, atime: t, mtime: t, ctime: t, nlink: 1, link: target };
1902
+ parentNode.children[base] = id;
1903
+ this._touch(parentId, ["mtime", "ctime"]);
1904
+ }
1905
+
1906
+ readlink(path) {
1907
+ const { node } = this._getNode(path, { followSymlinks: false });
1908
+ if (node.type !== "symlink") throw err("EINVAL", path);
1909
+ return node.link;
1910
+ }
1911
+
1912
+ realpath(path) { return this._ext(normPath(this._p(path))); }
1913
+
1914
+ // -------------------------------------------------------------------------
1915
+ // Permissions / ownership / timestamps
1916
+ // -------------------------------------------------------------------------
1917
+
1918
+ chmod(path, mode) {
1919
+ const { id, node } = this._getNode(path, { followSymlinks: true });
1920
+ if (this._enforcePerms && this._uid !== 0 && node.uid !== this._uid) throw err("EPERM", path);
1921
+ node.mode = mode;
1922
+ this._touch(id, ["ctime"]);
1923
+ }
1924
+ lchmod(path, mode) {
1925
+ const { id, node } = this._getNode(path, { followSymlinks: false });
1926
+ if (this._enforcePerms && this._uid !== 0 && node.uid !== this._uid) throw err("EPERM", path);
1927
+ node.mode = mode; this._touch(id, ["ctime"]);
1928
+ }
1929
+ fchmod(fd, mode) {
1930
+ const handle = this._fds.get(fd);
1931
+ if (!handle) throw err("EBADF");
1932
+ const node = this._db.tree[handle.nodeId];
1933
+ if (!node) throw err("EBADF");
1934
+ if (this._enforcePerms && this._uid !== 0 && node.uid !== this._uid) throw err("EPERM");
1935
+ node.mode = mode; this._touch(handle.nodeId, ["ctime"]);
1936
+ }
1937
+
1938
+ chown(path, uid, gid) {
1939
+ const { id, node } = this._getNode(path, { followSymlinks: true });
1940
+ if (this._enforcePerms && this._uid !== 0) throw err("EPERM", path);
1941
+ node.uid = uid; node.gid = gid; this._touch(id, ["ctime"]);
1942
+ }
1943
+ lchown(path, uid, gid) {
1944
+ const { id, node } = this._getNode(path, { followSymlinks: false });
1945
+ if (this._enforcePerms && this._uid !== 0) throw err("EPERM", path);
1946
+ node.uid = uid; node.gid = gid; this._touch(id, ["ctime"]);
1947
+ }
1948
+ fchown(fd, uid, gid) {
1949
+ const handle = this._fds.get(fd);
1950
+ if (!handle) throw err("EBADF");
1951
+ const node = this._db.tree[handle.nodeId];
1952
+ if (!node) throw err("EBADF");
1953
+ if (this._enforcePerms && this._uid !== 0) throw err("EPERM");
1954
+ node.uid = uid; node.gid = gid; this._touch(handle.nodeId, ["ctime"]);
1955
+ }
1956
+
1957
+ utimes(path, atime, mtime) {
1958
+ const { id, node } = this._getNode(path, { followSymlinks: true });
1959
+ this._checkWrite(node);
1960
+ node.atime = new Date(atime).toISOString();
1961
+ node.mtime = new Date(mtime).toISOString();
1962
+ node.ctime = now();
1963
+ }
1964
+
1965
+ // -------------------------------------------------------------------------
1966
+ // File descriptors
1967
+ // -------------------------------------------------------------------------
1968
+
1969
+ open(path, flags, mode) {
1970
+ const p = this._p(path);
1971
+ const flagStr = typeof flags === "string" ? flags : String(flags);
1972
+ const isCreate = flagStr.includes("w") || flagStr.includes("a") || (typeof flags === "number" && flags & O_CREAT);
1973
+ const isExcl = flagStr.includes("x") || (typeof flags === "number" && flags & O_EXCL);
1974
+ const isAppend = flagStr.includes("a") || (typeof flags === "number" && flags & O_APPEND);
1975
+ const isTrunc = (flagStr.includes("w") && !isAppend) || (typeof flags === "number" && flags & O_TRUNC);
1976
+ const isWrite = flagStr.includes("w") || flagStr.includes("a") || flagStr.includes("+") || (typeof flags === "number" && (flags & (O_WRONLY | O_RDWR)));
1977
+ const isRead = !flagStr.includes("w") || flagStr.includes("+") || flagStr.includes("r") || (typeof flags === "number" && !(flags & O_WRONLY));
1978
+
1979
+ let nodeId;
1980
+ try {
1981
+ nodeId = this._resolve(p);
1982
+ if (isExcl) throw err("EEXIST", path);
1983
+ const node = this._db.tree[nodeId];
1984
+ if (isRead) this._checkRead(node);
1985
+ if (isWrite) this._checkWrite(node);
1986
+ if (isTrunc) { node.data = ""; this._touch(nodeId, ["mtime", "ctime"]); }
1987
+ } catch(e) {
1988
+ if (e.code !== "ENOENT" || !isCreate) throw e;
1989
+ this.writeFile(p, "", { mode: mode || (0o666 & ~this._umask) });
1990
+ nodeId = this._resolve(p);
1991
+ }
1992
+
1993
+ const fd = this._fdCounter++;
1994
+ this._fds.set(fd, {
1995
+ nodeId, flags: flagStr,
1996
+ pos: isAppend ? (this._db.tree[nodeId].data || "").length : 0,
1997
+ readable: isRead, writable: isWrite, append: isAppend,
1998
+ });
1999
+ return fd;
2000
+ }
2001
+
2002
+ close(fd) { if (!this._fds.has(fd)) throw err("EBADF"); this._fds.delete(fd); }
2003
+
2004
+ read(fd, buffer, offset = 0, length, position = null) {
2005
+ const handle = this._fds.get(fd);
2006
+ if (!handle || !handle.readable) throw err("EBADF");
2007
+ const node = this._db.tree[handle.nodeId];
2008
+ if (!node || node.type !== "file") throw err("EBADF");
2009
+ const len = length !== undefined ? length : (buffer ? buffer.length : 0);
2010
+ const pos = position !== null ? position : handle.pos;
2011
+ const data = node.data || "";
2012
+ const chunk = data.slice(pos, pos + len);
2013
+ const bytesRead = chunk.length;
2014
+ if (typeof buffer === "object" && buffer !== null && !Array.isArray(buffer)) {
2015
+ for (let i = 0; i < bytesRead; i++) buffer[offset + i] = chunk.charCodeAt(i);
2016
+ }
2017
+ if (position === null) handle.pos += bytesRead;
2018
+ this._touch(handle.nodeId, ["atime"]);
2019
+ return { bytesRead, buffer: chunk };
2020
+ }
2021
+
2022
+ write(fd, buffer, offset = 0, length, position = null) {
2023
+ const handle = this._fds.get(fd);
2024
+ if (!handle || !handle.writable) throw err("EBADF");
2025
+ const node = this._db.tree[handle.nodeId];
2026
+ if (!node || node.type !== "file") throw err("EBADF");
2027
+ const str = typeof buffer === "string"
2028
+ ? buffer.slice(offset, length != null ? offset + length : undefined)
2029
+ : (typeof buffer === "object" && buffer.toString ? buffer.toString().slice(offset, length != null ? offset + length : undefined) : String(buffer));
2030
+ const pos = handle.append ? (node.data || "").length : (position !== null ? position : handle.pos);
2031
+ const d = node.data || "";
2032
+ node.data = d.slice(0, pos) + str + d.slice(pos + str.length);
2033
+ const bytesWritten = str.length;
2034
+ if (!handle.append && position === null) handle.pos += bytesWritten;
2035
+ this._touch(handle.nodeId, ["mtime", "ctime"]);
2036
+ this._notify(handle.nodeId, "write", null);
2037
+ return { bytesWritten };
2038
+ }
2039
+
2040
+ fsync(fd) { if (!this._fds.has(fd)) throw err("EBADF"); }
2041
+ fdatasync(fd) { if (!this._fds.has(fd)) throw err("EBADF"); }
2042
+
2043
+ // -------------------------------------------------------------------------
2044
+ // Streams
2045
+ // -------------------------------------------------------------------------
2046
+
2047
+ createReadStream(path, opts = {}) {
2048
+ const self = this; const em = new EventEmitter();
2049
+ const start = opts.start || 0; const end = opts.end;
2050
+ em.pipe = (dest) => {
2051
+ try {
2052
+ const { node } = self._getNode(path, { followSymlinks: true });
2053
+ if (node.type !== "file") { em.emit("error", err("EISDIR", path)); return dest; }
2054
+ const data = (node.data || "").slice(start, end != null ? end + 1 : undefined);
2055
+ em.emit("data", data); em.emit("end");
2056
+ if (dest && typeof dest.write === "function") dest.write(data);
2057
+ } catch(e) { em.emit("error", e); }
2058
+ return dest;
2059
+ };
2060
+ setTimeout(() => em.pipe(null), 0);
2061
+ return em;
2062
+ }
2063
+
2064
+ createWriteStream(path, opts = {}) {
2065
+ const self = this; const flag = opts.flags || "w"; const em = new EventEmitter();
2066
+ em.write = (chunk) => { self.appendFile(path, chunk); return true; };
2067
+ em.end = (chunk) => { if (chunk) em.write(chunk); em.emit("finish"); em.emit("close"); };
2068
+ if (flag === "w") try { self.writeFile(path, "", opts); } catch(_) {}
2069
+ return em;
2070
+ }
2071
+
2072
+ // -------------------------------------------------------------------------
2073
+ // copyFile / cp
2074
+ // -------------------------------------------------------------------------
2075
+
2076
+ copyFile(src, dst, flags = 0) {
2077
+ const excl = flags & COPYFILE_EXCL;
2078
+ if (excl && this.exists(dst)) throw err("EEXIST", dst);
2079
+ const { node: srcNode } = this._getNode(src, { followSymlinks: true });
2080
+ this._checkRead(srcNode);
2081
+ if (srcNode.type === "dir") throw err("EISDIR", src);
2082
+ this.writeFile(dst, srcNode.data || "");
2083
+ }
2084
+
2085
+ cp(src, dst, opts = {}) {
2086
+ const sp = this._p(src); const dp = this._p(dst);
2087
+ const { node: srcNode } = this._getNode(sp, { followSymlinks: true });
2088
+ if (srcNode.type === "file") { this.copyFile(sp, dp, opts.errorOnExist ? COPYFILE_EXCL : 0); return; }
2089
+ if (srcNode.type === "dir") {
2090
+ if (!opts.recursive) throw err("EISDIR", src);
2091
+ if (!this.exists(dp)) this.mkdir(dp, { recursive: true });
2092
+ for (const child of Object.keys(srcNode.children)) this.cp(`${sp}/${child}`, `${dp}/${child}`, opts);
2093
+ }
2094
+ }
2095
+
2096
+ // -------------------------------------------------------------------------
2097
+ // Watchers (real EventEmitter-based with actual events)
2098
+ // -------------------------------------------------------------------------
2099
+
2100
+ watch(path, opts = {}, listener) {
2101
+ if (typeof opts === "function") { listener = opts; opts = {}; }
2102
+ const p = this._p(path);
2103
+ const id = this._resolve(p);
2104
+ const watcher = new EventEmitter();
2105
+ const key = Symbol();
2106
+ this._watchers.set(key, { id, path: p, emitter: watcher, listener: listener || null });
2107
+ watcher.close = () => { this._watchers.delete(key); watcher.emit("close"); };
2108
+ return watcher;
2109
+ }
2110
+
2111
+ watchFile(path, opts = {}, listener) {
2112
+ if (typeof opts === "function") { listener = opts; opts = {}; }
2113
+ const p = this._p(path);
2114
+ let lastStat;
2115
+ try { lastStat = this.stat(p); } catch(_) { lastStat = null; }
2116
+ const key = Symbol();
2117
+ const watcher = { path: p, listener: listener || (() => {}), stop: () => this._watchers.delete(key), id: null, emitter: new EventEmitter() };
2118
+ // Hook into notify
2119
+ this._watchers.set(key, watcher);
2120
+ return { stop: watcher.stop };
2121
+ }
2122
+
2123
+ unwatchFile(path) {
2124
+ const p = this._p(path);
2125
+ for (const [k, v] of this._watchers) { if (v.path === p) this._watchers.delete(k); }
2126
+ }
2127
+
2128
+ // -------------------------------------------------------------------------
2129
+ // Glob / find
2130
+ // -------------------------------------------------------------------------
2131
+
2132
+ glob(pattern, opts = {}) {
2133
+ const root = normPath(this._p(opts.cwd || "/"));
2134
+ const results = [];
2135
+ const search = (dir) => {
2136
+ let node;
2137
+ try { ({ node } = this._getNode(dir, { followSymlinks: true })); } catch(_) { return; }
2138
+ if (node.type !== "dir") return;
2139
+ for (const child of Object.keys(node.children)) {
2140
+ const childPath = dir === "/" ? "/" + child : dir + "/" + child;
2141
+ const rel = this.relative(root, childPath);
2142
+ if (matchGlob(pattern, rel) || matchGlob(pattern, child)) results.push(this._ext(childPath));
2143
+ search(childPath);
2144
+ }
2145
+ };
2146
+ search(root);
2147
+ return results;
2148
+ }
2149
+
2150
+ find(path, opts = {}) {
2151
+ const results = [];
2152
+ const typeFilter = opts.type; const nameFilter = opts.name;
2153
+ const search = (p) => {
2154
+ let node;
2155
+ try { ({ node } = this._getNode(p, { followSymlinks: false })); } catch(_) { return; }
2156
+ const typeMatch = !typeFilter || (typeFilter === "f" && node.type === "file") || (typeFilter === "d" && node.type === "dir") || (typeFilter === "l" && node.type === "symlink");
2157
+ const nameMatch = !nameFilter || matchGlob(nameFilter, this.basename(p));
2158
+ if (typeMatch && nameMatch) results.push(this._ext(p));
2159
+ if (node.type === "dir") {
2160
+ for (const child of Object.keys(node.children)) search(p === "/" ? "/" + child : p + "/" + child);
2161
+ }
2162
+ };
2163
+ search(normPath(this._p(path)));
2164
+ return results;
2165
+ }
2166
+
2167
+ // -------------------------------------------------------------------------
2168
+ // df / du / tree
2169
+ // -------------------------------------------------------------------------
2170
+
2171
+ df() {
2172
+ let files = 0, dirs = 0, symlinks = 0, totalBytes = 0;
2173
+ for (const [, node] of Object.entries(this._db.tree)) {
2174
+ if (node.type === "file") { files++; totalBytes += (node.data || "").length; }
2175
+ if (node.type === "dir") dirs++;
2176
+ if (node.type === "symlink") symlinks++;
2177
+ }
2178
+ return { files, dirs, symlinks, totalBytes, nodes: Object.keys(this._db.tree).length };
2179
+ }
2180
+
2181
+ du(path) {
2182
+ const p = normPath(this._p(path));
2183
+ let bytes = 0;
2184
+ const measure = (pt) => {
2185
+ let node;
2186
+ try { ({ node } = this._getNode(pt, { followSymlinks: false })); } catch(_) { return; }
2187
+ if (node.type === "file") bytes += (node.data || "").length;
2188
+ if (node.type === "dir") for (const child of Object.keys(node.children)) measure(pt === "/" ? "/" + child : pt + "/" + child);
2189
+ };
2190
+ measure(p);
2191
+ return bytes;
2192
+ }
2193
+
2194
+ tree(path = "/", opts = {}) {
2195
+ const indent = opts._indent || "";
2196
+ const isLast = opts._isLast !== false;
2197
+ const p = normPath(this._p(path));
2198
+ const name = this.basename(p) || "/";
2199
+ const connector = indent === "" ? "" : (isLast ? "└── " : "├── ");
2200
+ let out = indent + connector + name + "\n";
2201
+ let node;
2202
+ try { ({ node } = this._getNode(p, { followSymlinks: false })); } catch(_) { return out; }
2203
+ if (node.type === "symlink") return indent + connector + name + " -> " + node.link + "\n";
2204
+ if (node.type === "dir") {
2205
+ const children = Object.keys(node.children);
2206
+ children.forEach((child, i) => {
2207
+ const childPath = p === "/" ? "/" + child : p + "/" + child;
2208
+ const last = i === children.length - 1;
2209
+ const newIndent = indent + (isLast || indent === "" ? " " : "│ ");
2210
+ out += this.tree(childPath, { _indent: newIndent, _isLast: last });
2211
+ });
2212
+ }
2213
+ return out;
2214
+ }
2215
+
2216
+ // -------------------------------------------------------------------------
2217
+ // Mount / unmount (properly appends into /mnt/)
2218
+ // -------------------------------------------------------------------------
2219
+
2220
+ /**
2221
+ * Mount another TeaF instance at path (defaults to /mnt/<label>).
2222
+ * Handles duplicate-segment collisions by appending a counter.
2223
+ * Always creates mount point inside /mnt/ if a relative name is given.
2224
+ */
2225
+ mount(mountPath, otherTF) {
2226
+ if (!(otherTF instanceof TeaF)) throw new Error("mount: must provide another TeaF instance");
2227
+ let p;
2228
+ if (mountPath.startsWith("/")) {
2229
+ p = normPath(mountPath);
2230
+ } else {
2231
+ // Treat as a name inside /mnt/
2232
+ // Avoid collision with existing /mnt/ children
2233
+ const mntNode = this._db.tree[this._resolve("/mnt")];
2234
+ let name = mountPath;
2235
+ let counter = 0;
2236
+ while (mntNode && mntNode.children[name]) {
2237
+ counter++;
2238
+ name = mountPath + "_" + counter;
2239
+ }
2240
+ p = "/mnt/" + name;
2241
+ }
2242
+
2243
+ // Ensure path exists
2244
+ if (!this.exists(p)) this.mkdir(p, { recursive: true });
2245
+ this._mounts.set(p, otherTF);
2246
+ this.emit("mount", p);
2247
+ return p;
2248
+ }
2249
+
2250
+ unmount(path) {
2251
+ const p = path.startsWith("/") ? normPath(path) : "/mnt/" + path;
2252
+ if (!this._mounts.has(p)) throw new Error("unmount: not a mount point: " + p);
2253
+ this._mounts.delete(p);
2254
+ this.emit("unmount", p);
2255
+ }
2256
+
2257
+ // -------------------------------------------------------------------------
2258
+ // exec — run sh command/script in this FS
2259
+ // -------------------------------------------------------------------------
2260
+
2261
+ /**
2262
+ * Execute a shell command string in this filesystem.
2263
+ * @param {string} cmd - command or sh script
2264
+ * @param {Object} opts - { env, stdin, cwd, uid, gid }
2265
+ * @returns {{ stdout: string, stderr: string, code: number }}
2266
+ */
2267
+ exec(cmd, opts = {}) {
2268
+ if (!this._sh) {
2269
+ this._sh = new Sh(this, { uid: this._uid, gid: this._gid });
2270
+ }
2271
+ if (opts.env) Object.assign(this._sh.env, opts.env);
2272
+ if (opts.cwd) this._sh.env.PWD = normPath(this._p(opts.cwd));
2273
+ if (opts.uid !== undefined) this._sh._uid = opts.uid;
2274
+ if (opts.gid !== undefined) this._sh._gid = opts.gid;
2275
+ return this._sh.exec(cmd, { stdin: opts.stdin || [], stdout: opts.stdout, stderr: opts.stderr });
2276
+ }
2277
+
2278
+ /** Get or create the shell instance */
2279
+ get sh() {
2280
+ if (!this._sh) this._sh = new Sh(this, { uid: this._uid, gid: this._gid });
2281
+ return this._sh;
2282
+ }
2283
+
2284
+ /**
2285
+ * Create an interactive REPL session for this FS.
2286
+ * @returns {ShRepl}
2287
+ */
2288
+ repl(opts = {}) {
2289
+ const sh = new Sh(this, { uid: this._uid, gid: this._gid, env: opts.env });
2290
+ return new ShRepl(sh);
2291
+ }
2292
+
2293
+ // -------------------------------------------------------------------------
2294
+ // TeaBin helpers (install binary onto filesystem)
2295
+ // -------------------------------------------------------------------------
2296
+
2297
+ /**
2298
+ * Assemble TeaBin source and install as an executable at path.
2299
+ * @param {string} path - e.g. "/bin/hello"
2300
+ * @param {string} src - assembly source
2301
+ */
2302
+ installBin(path, src) {
2303
+ const bytes = TeaBinVM.assemble(src);
2304
+ // Store as a binary string in the file
2305
+ let str = "";
2306
+ for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
2307
+ this.writeFile(path, str);
2308
+ this.chmod(path, 0o755);
2309
+ }
2310
+
2311
+ /**
2312
+ * Run a TeaBin binary file from the filesystem.
2313
+ * @param {string} path
2314
+ * @param {Object} opts - { ports, interrupts, maxCycles }
2315
+ * @returns {{ stdout: string, exitCode: number }}
2316
+ */
2317
+ runBin(path, opts = {}) {
2318
+ const data = this.readFile(path, "utf8");
2319
+ const bytes = new Uint8Array(data.length);
2320
+ for (let i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i) & 0xFF;
2321
+ const vm = new TeaBinVM(opts);
2322
+ vm.load(bytes);
2323
+ const exitCode = vm.run();
2324
+ const stdout = vm.stdout.map(o => typeof o === "number" ? String.fromCharCode(o) : String(o)).join("");
2325
+ return { stdout, exitCode };
2326
+ }
2327
+
2328
+ // -------------------------------------------------------------------------
2329
+ // JSON persistence (snaps/ directory)
2330
+ // -------------------------------------------------------------------------
2331
+
2332
+ toJSON() { return JSON.stringify(this._db, null, 2); }
2333
+
2334
+ fromJSON(json) {
2335
+ const data = typeof json === "string" ? JSON.parse(json) : json;
2336
+ if (!data.tree || !data.meta) throw new Error("Invalid tea/f snapshot");
2337
+ this._db = data;
2338
+ return this;
2339
+ }
2340
+
2341
+ /**
2342
+ * Save snapshot to ./snaps/<filename>.json (Node.js only).
2343
+ * @param {string} name - just the base name (no path needed), e.g. "my-fs"
2344
+ * or a full path starting with /
2345
+ */
2346
+ saveSnapshot(name) {
2347
+ if (typeof require === "undefined") throw new Error("saveSnapshot requires Node.js");
2348
+ const fs = require("fs");
2349
+ const path = require("path");
2350
+
2351
+ let filePath;
2352
+ if (name.startsWith("/") || name.includes("\\") || name.includes(":")) {
2353
+ filePath = name; // absolute path given
2354
+ } else {
2355
+ // Ensure ./snaps/ exists
2356
+ if (!fs.existsSync(SNAP_DIR)) fs.mkdirSync(SNAP_DIR, { recursive: true });
2357
+ const base = name.endsWith(".json") ? name : name + ".json";
2358
+ filePath = path.join(SNAP_DIR, base);
2359
+ }
2360
+ fs.writeFileSync(filePath, this.toJSON(), "utf8");
2361
+ return filePath;
2362
+ }
2363
+
2364
+ /** Alias kept for compatibility */
2365
+ saveToFile(filePath) { return this.saveSnapshot(filePath); }
2366
+
2367
+ /**
2368
+ * Load snapshot from ./snaps/<name>.json or an absolute path (Node.js only).
2369
+ */
2370
+ loadSnapshot(name) {
2371
+ if (typeof require === "undefined") throw new Error("loadSnapshot requires Node.js");
2372
+ const fs = require("fs");
2373
+ const path = require("path");
2374
+ let filePath;
2375
+ // If it looks like a full/relative OS path (contains path sep or starts with . or /), use as-is
2376
+ if (name.startsWith("/") || name.startsWith("./") || name.startsWith("../") ||
2377
+ name.includes("\\") || name.includes(":") || name.includes(path.sep)) {
2378
+ filePath = name;
2379
+ } else {
2380
+ const base = name.endsWith(".json") ? name : name + ".json";
2381
+ filePath = path.join(SNAP_DIR, base);
2382
+ }
2383
+ const json = fs.readFileSync(filePath, "utf8");
2384
+ return this.fromJSON(json);
2385
+ }
2386
+
2387
+ /** Alias */
2388
+ loadFromFile(filePath) { return this.loadSnapshot(filePath); }
2389
+
2390
+ // -------------------------------------------------------------------------
2391
+ // Async wrappers
2392
+ // -------------------------------------------------------------------------
2393
+
2394
+ get promises() {
2395
+ const wrap = (method) => (...args) => {
2396
+ try { return Promise.resolve(this[method](...args)); }
2397
+ catch(e) { return Promise.reject(e); }
2398
+ };
2399
+ return {
2400
+ readFile: wrap("readFile"), writeFile: wrap("writeFile"),
2401
+ appendFile: wrap("appendFile"), truncate: wrap("truncate"),
2402
+ mkdir: wrap("mkdir"), rmdir: wrap("rmdir"), readdir: wrap("readdir"),
2403
+ stat: wrap("stat"), lstat: wrap("lstat"), rename: wrap("rename"),
2404
+ unlink: wrap("unlink"), rm: wrap("rm"), link: wrap("link"),
2405
+ symlink: wrap("symlink"), readlink: wrap("readlink"), realpath: wrap("realpath"),
2406
+ chmod: wrap("chmod"), lchmod: wrap("lchmod"), chown: wrap("chown"),
2407
+ lchown: wrap("lchown"), utimes: wrap("utimes"), copyFile: wrap("copyFile"),
2408
+ cp: wrap("cp"), open: wrap("open"), access: wrap("access"),
2409
+ exec: wrap("exec"),
2410
+ };
2411
+ }
2412
+ }
2413
+
2414
+ // ===========================================================================
2415
+ // Factory
2416
+ // ===========================================================================
2417
+
2418
+ function createFS(opts) { return new TeaF(opts); }
2419
+ createFS.TeaF = TeaF;
2420
+ createFS.Stats = Stats;
2421
+ createFS.TFError = TFError;
2422
+ createFS.TeaBinVM = TeaBinVM;
2423
+ createFS.Sh = Sh;
2424
+ createFS.ShRepl = ShRepl;
2425
+ createFS.PathAdapter = PathAdapter;
2426
+ createFS.COPYFILE_EXCL = COPYFILE_EXCL;
2427
+ createFS.O_RDONLY = O_RDONLY;
2428
+ createFS.O_WRONLY = O_WRONLY;
2429
+ createFS.O_RDWR = O_RDWR;
2430
+ createFS.O_CREAT = O_CREAT;
2431
+ createFS.O_EXCL = O_EXCL;
2432
+ createFS.O_TRUNC = O_TRUNC;
2433
+ createFS.O_APPEND = O_APPEND;
2434
+ createFS.OP = OP;
2435
+ createFS.VERSION = VERSION;
2436
+
2437
+ if (typeof module !== "undefined" && module.exports) module.exports = { kitdef: createFS };
2438
+ if (typeof window !== "undefined") window.tf = createFS;
2439
+
2440
+ // ===========================================================================
2441
+ // Self-test
2442
+ // ===========================================================================
2443
+
2444
+ if (typeof process !== "undefined" && process.argv[1] && process.argv[1].endsWith("tea-f.js")) {
2445
+ const tf = createFS({ label: "test-fs", enforcePermissions: false });
2446
+ const assert = (cond, msg) => { if (!cond) throw new Error("FAIL: " + msg); };
2447
+ console.log("=== tea's F v2 — self-test ===\n");
2448
+
2449
+ // mkdir / exists
2450
+ tf.mkdir("/etc/config", { recursive: true }); // /etc pre-exists from _init
2451
+ tf.mkdir("/usr/local/bin", { recursive: true });
2452
+ assert(tf.exists("/etc/config"), "mkdir /etc/config");
2453
+ assert(tf.exists("/usr/local/bin"), "mkdir recursive");
2454
+ console.log("✓ mkdir + exists");
2455
+
2456
+ // writeFile / readFile
2457
+ tf.writeFile("/etc/hosts", "127.0.0.1 localhost\n");
2458
+ assert(tf.readFile("/etc/hosts", "utf8") === "127.0.0.1 localhost\n", "writeFile/readFile");
2459
+ console.log("✓ writeFile / readFile");
2460
+
2461
+ // appendFile
2462
+ tf.appendFile("/etc/hosts", "::1 localhost\n");
2463
+ assert(tf.readFile("/etc/hosts", "utf8").includes("::1"), "appendFile");
2464
+ console.log("✓ appendFile");
2465
+
2466
+ // watcher events
2467
+ const watched = [];
2468
+ const watcher = tf.watch("/etc");
2469
+ watcher.on("write", (ev, f) => watched.push(ev));
2470
+ tf.writeFile("/etc/test-watch", "hello");
2471
+ assert(watched.length > 0, "watcher received event");
2472
+ watcher.close();
2473
+ console.log("✓ watcher real events");
2474
+
2475
+ // stat
2476
+ const s = tf.stat("/etc/hosts");
2477
+ assert(s.isFile(), "stat isFile");
2478
+ assert(s.size > 0, "stat size");
2479
+ console.log("✓ stat");
2480
+
2481
+ // symlink / readlink / lstat
2482
+ tf.symlink("/etc/hosts", "/etc/hosts.bak");
2483
+ const ls = tf.lstat("/etc/hosts.bak");
2484
+ assert(ls.isSymbolicLink(), "lstat symlink");
2485
+ assert(tf.readlink("/etc/hosts.bak") === "/etc/hosts", "readlink");
2486
+ console.log("✓ symlink / readlink / lstat");
2487
+
2488
+ // hard link
2489
+ tf.link("/etc/hosts", "/etc/hosts2");
2490
+ assert(tf.readFile("/etc/hosts2", "utf8") === tf.readFile("/etc/hosts", "utf8"), "hard link");
2491
+ console.log("✓ hard link");
2492
+
2493
+ // rename
2494
+ tf.rename("/etc/hosts2", "/etc/hosts.old");
2495
+ assert(!tf.exists("/etc/hosts2"), "rename src gone");
2496
+ assert(tf.exists("/etc/hosts.old"), "rename dst exists");
2497
+ console.log("✓ rename");
2498
+
2499
+ // unlink
2500
+ tf.unlink("/etc/hosts.old");
2501
+ assert(!tf.exists("/etc/hosts.old"), "unlink");
2502
+ console.log("✓ unlink");
2503
+
2504
+ // readdir
2505
+ const entries = tf.readdir("/etc");
2506
+ assert(entries.includes("hosts"), "readdir includes hosts");
2507
+ console.log("✓ readdir");
2508
+
2509
+ // chmod / chown
2510
+ tf.chmod("/etc/hosts", 0o644);
2511
+ assert(tf.stat("/etc/hosts").mode & 0o644, "chmod");
2512
+ tf.chown("/etc/hosts", 1000, 1000);
2513
+ assert(tf.stat("/etc/hosts").uid === 1000, "chown uid");
2514
+ console.log("✓ chmod / chown");
2515
+
2516
+ // truncate
2517
+ tf.truncate("/etc/hosts", 5);
2518
+ assert(tf.readFile("/etc/hosts", "utf8").length === 5, "truncate");
2519
+ console.log("✓ truncate");
2520
+
2521
+ // File descriptors
2522
+ tf.writeFile("/tmp/test.txt", "hello world");
2523
+ const fd = tf.open("/tmp/test.txt", "r");
2524
+ const { bytesRead } = tf.read(fd, new Uint8Array(5), 0, 5, 0);
2525
+ assert(bytesRead === 5, "read bytesRead");
2526
+ tf.close(fd);
2527
+ const fd2 = tf.open("/tmp/test2.txt", "w");
2528
+ tf.write(fd2, "abc");
2529
+ tf.close(fd2);
2530
+ assert(tf.readFile("/tmp/test2.txt", "utf8") === "abc", "write via fd");
2531
+ console.log("✓ open / read / write / close");
2532
+
2533
+ // copyFile / cp recursive
2534
+ tf.writeFile("/etc/hosts", "127.0.0.1 localhost\n");
2535
+ tf.copyFile("/etc/hosts", "/etc/hosts.copy");
2536
+ assert(tf.exists("/etc/hosts.copy"), "copyFile");
2537
+ tf.mkdir("/backup");
2538
+ tf.cp("/etc", "/backup/etc", { recursive: true });
2539
+ assert(tf.exists("/backup/etc/hosts"), "cp recursive");
2540
+ tf.rm("/backup", { recursive: true });
2541
+ assert(!tf.exists("/backup"), "rm recursive");
2542
+ console.log("✓ copyFile / cp / rm");
2543
+
2544
+ // glob / find / path helpers
2545
+ tf.writeFile("/usr/local/bin/node", "#!/usr/bin/env node\n");
2546
+ tf.writeFile("/usr/local/bin/npm", "#!/usr/bin/env node\n");
2547
+ const hits = tf.glob("**/bin/*");
2548
+ assert(hits.length >= 2, "glob **");
2549
+ const found = tf.find("/usr", { type: "f" });
2550
+ assert(found.length >= 2, "find type:f");
2551
+ assert(tf.join("/usr", "local", "bin") === "/usr/local/bin", "join");
2552
+ assert(tf.dirname("/usr/local/bin") === "/usr/local", "dirname");
2553
+ assert(tf.basename("/usr/local/bin/node") === "node", "basename");
2554
+ assert(tf.extname("/etc/hosts.copy") === ".copy", "extname");
2555
+ assert(tf.relative("/usr", "/usr/local/bin") === "local/bin", "relative");
2556
+ console.log("✓ glob / find / path helpers");
2557
+
2558
+ // df / du / tree
2559
+ const dfStats = tf.df();
2560
+ assert(dfStats.files > 0, "df files");
2561
+ const usage = tf.du("/etc");
2562
+ assert(usage >= 0, "du");
2563
+ const treeStr = tf.tree("/");
2564
+ assert(treeStr.includes("etc"), "tree includes etc");
2565
+ console.log("✓ df / du / tree");
2566
+
2567
+ // Win mode
2568
+ const tfWin = createFS({ mode: "win", label: "win-test" });
2569
+ tfWin.mkdir("C:\\Users\\Alice", { recursive: true });
2570
+ tfWin.writeFile("C:\\Users\\Alice\\notes.txt", "hello windows");
2571
+ assert(tfWin.readFile("C:\\Users\\Alice\\notes.txt", "utf8") === "hello windows", "win mode");
2572
+ console.log("✓ win mode (C:\\...)");
2573
+
2574
+ // URL mode
2575
+ const tfUrl = createFS({ mode: "url", label: "url-test" });
2576
+ tfUrl.mkdir("http://example.com/api/v1", { recursive: true });
2577
+ tfUrl.writeFile("http://example.com/api/v1/data.json", '{"ok":true}');
2578
+ assert(tfUrl.readFile("http://example.com/api/v1/data.json", "utf8") === '{"ok":true}', "url mode read");
2579
+ console.log("✓ url mode (http://...)");
2580
+
2581
+ // Mount with /mnt/ appending and duplicate prevention
2582
+ const tf2 = createFS({ label: "sub-fs" });
2583
+ tf2.writeFile("/hello.txt", "from mounted fs");
2584
+ const mountAt = tf.mount("sub", tf2);
2585
+ assert(mountAt === "/mnt/sub", "mount path");
2586
+ // Mount duplicate
2587
+ const tf3 = createFS({ label: "sub-fs-2" });
2588
+ const mountAt2 = tf.mount("sub", tf3);
2589
+ assert(mountAt2 === "/mnt/sub_1", "mount collision resolved");
2590
+ console.log("✓ mount (into /mnt/, collision-safe)");
2591
+
2592
+ // Shell exec
2593
+ tf.writeFile("/etc/motd", "Welcome to tea/f!\n");
2594
+ const r1 = tf.exec("cat /etc/motd");
2595
+ assert(r1.stdout.includes("Welcome"), "exec cat");
2596
+ const r2 = tf.exec("echo hello world");
2597
+ assert(r2.stdout.trim() === "hello world", "exec echo");
2598
+ const r3 = tf.exec("ls /etc");
2599
+ assert(r3.stdout.includes("hosts"), "exec ls");
2600
+ const r4 = tf.exec("echo foo > /tmp/redir.txt && cat /tmp/redir.txt");
2601
+ assert(tf.readFile("/tmp/redir.txt", "utf8").trim() === "foo", "exec redirect");
2602
+ const r5 = tf.exec("echo a | grep a");
2603
+ assert(r5.stdout.trim() === "a", "exec pipe+grep");
2604
+ console.log("✓ shell exec (cat, echo, ls, redirect, pipe)");
2605
+
2606
+ // REPL
2607
+ const repl = tf.repl();
2608
+ const res = repl.line("echo repl-works");
2609
+ assert(res.stdout.trim() === "repl-works", "repl line");
2610
+ repl.close();
2611
+ console.log("✓ repl");
2612
+
2613
+ // TeaBin VM — assemble + run simple program
2614
+ const asmSrc = `
2615
+ .code
2616
+ MOV R0, 42
2617
+ OUT 0, R0
2618
+ HALT 0
2619
+ `;
2620
+ const bytes = TeaBinVM.assemble(asmSrc);
2621
+ const vm = new TeaBinVM();
2622
+ vm.load(bytes);
2623
+ vm.run();
2624
+ assert(vm.stdout[0] === 42, "TeaBin VM OUT");
2625
+ console.log("✓ TeaBin VM (assemble + run + OUT)");
2626
+
2627
+ // installBin + runBin
2628
+ const helloBinSrc = `
2629
+ .data
2630
+ msg "Hi from bin!\n"
2631
+ .code
2632
+ MOV R2, 0
2633
+ MOV R3, 14
2634
+ MOV R0, 1
2635
+ INT 0x80
2636
+ HALT 0
2637
+ `;
2638
+ tf.installBin("/bin/hello", helloBinSrc);
2639
+ assert(tf.exists("/bin/hello"), "installBin");
2640
+ const binResult = tf.runBin("/bin/hello");
2641
+ assert(binResult.stdout.includes("Hi"), "runBin stdout");
2642
+ console.log("✓ installBin / runBin");
2643
+
2644
+ // JSON round-trip
2645
+ const json = tf.toJSON();
2646
+ const tf4 = createFS();
2647
+ tf4.fromJSON(json);
2648
+ assert(tf4.exists("/etc/hosts"), "JSON round-trip");
2649
+ console.log("✓ toJSON / fromJSON");
2650
+
2651
+ // saveSnapshot / loadSnapshot
2652
+ const snapPath = tf.saveSnapshot("test-snap");
2653
+ const tf5 = createFS();
2654
+ tf5.loadSnapshot(snapPath);
2655
+ assert(tf5.exists("/usr/local/bin/node"), "loadSnapshot");
2656
+ // Cleanup
2657
+ try { require("fs").unlinkSync(snapPath); require("fs").rmdirSync(SNAP_DIR); } catch(_) {}
2658
+ console.log("✓ saveSnapshot / loadSnapshot (./snaps/)");
2659
+
2660
+ // Permission enforcement
2661
+ const tfPerms = createFS({ uid: 0, gid: 0, enforcePermissions: true });
2662
+ // As root: create a home dir for uid 1000 and a secret file
2663
+ tfPerms.mkdir("/home/alice", { recursive: true });
2664
+ tfPerms.chown("/home/alice", 1000, 1000);
2665
+ tfPerms.chmod("/home/alice", 0o700);
2666
+ tfPerms.writeFile("/home/alice/secret.txt", "private");
2667
+ tfPerms.chown("/home/alice/secret.txt", 1000, 1000);
2668
+ tfPerms.chmod("/home/alice/secret.txt", 0o600);
2669
+ // Now act as uid 1000 — owner can read
2670
+ tfPerms._uid = 1000; tfPerms._gid = 1000;
2671
+ try {
2672
+ tfPerms.readFile("/home/alice/secret.txt", "utf8");
2673
+ console.log("✓ permission: owner can read 0o600 file");
2674
+ } catch(e) { throw new Error("FAIL: owner should read own file: " + e.message); }
2675
+ // Act as uid 2000 — should be denied
2676
+ tfPerms._uid = 2000; tfPerms._gid = 2000;
2677
+ let denied = false;
2678
+ try { tfPerms.readFile("/home/alice/secret.txt", "utf8"); }
2679
+ catch(e) { denied = e.code === "EACCES"; }
2680
+ assert(denied, "permission: other user denied reading 0o600 file");
2681
+ console.log("✓ permission enforcement");
2682
+
2683
+ tf.promises.readFile("/usr/local/bin/node", "utf8").then(v => {
2684
+ assert(v.includes("node"), "promises.readFile");
2685
+ console.log("✓ promises API");
2686
+ console.log("\n=== all tests passed ===\n");
2687
+ console.log("Filesystem snapshot:\n");
2688
+ console.log(tf.tree("/"));
2689
+ console.log("df:", tf.df());
2690
+ });
2691
+ }