@scelar/nodepod 1.0.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 (134) hide show
  1. package/LICENSE +43 -0
  2. package/README.md +240 -0
  3. package/dist/child_process-BJOMsZje.js +8233 -0
  4. package/dist/child_process-BJOMsZje.js.map +1 -0
  5. package/dist/child_process-Cj8vOcuc.cjs +7434 -0
  6. package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
  7. package/dist/index-Cb1Cgdnd.js +35308 -0
  8. package/dist/index-Cb1Cgdnd.js.map +1 -0
  9. package/dist/index-DsMGS-xc.cjs +37195 -0
  10. package/dist/index-DsMGS-xc.cjs.map +1 -0
  11. package/dist/index.cjs +65 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +59 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +95 -0
  16. package/src/__tests__/smoke.test.ts +11 -0
  17. package/src/constants/cdn-urls.ts +18 -0
  18. package/src/constants/config.ts +236 -0
  19. package/src/cross-origin.ts +26 -0
  20. package/src/engine-factory.ts +176 -0
  21. package/src/engine-types.ts +56 -0
  22. package/src/helpers/byte-encoding.ts +39 -0
  23. package/src/helpers/digest.ts +9 -0
  24. package/src/helpers/event-loop.ts +96 -0
  25. package/src/helpers/wasm-cache.ts +133 -0
  26. package/src/iframe-sandbox.ts +141 -0
  27. package/src/index.ts +192 -0
  28. package/src/isolation-helpers.ts +148 -0
  29. package/src/memory-volume.ts +941 -0
  30. package/src/module-transformer.ts +368 -0
  31. package/src/packages/archive-extractor.ts +248 -0
  32. package/src/packages/browser-bundler.ts +284 -0
  33. package/src/packages/installer.ts +396 -0
  34. package/src/packages/registry-client.ts +131 -0
  35. package/src/packages/version-resolver.ts +411 -0
  36. package/src/polyfills/assert.ts +384 -0
  37. package/src/polyfills/async_hooks.ts +144 -0
  38. package/src/polyfills/buffer.ts +628 -0
  39. package/src/polyfills/child_process.ts +2288 -0
  40. package/src/polyfills/chokidar.ts +336 -0
  41. package/src/polyfills/cluster.ts +106 -0
  42. package/src/polyfills/console.ts +136 -0
  43. package/src/polyfills/constants.ts +123 -0
  44. package/src/polyfills/crypto.ts +885 -0
  45. package/src/polyfills/dgram.ts +87 -0
  46. package/src/polyfills/diagnostics_channel.ts +76 -0
  47. package/src/polyfills/dns.ts +134 -0
  48. package/src/polyfills/domain.ts +68 -0
  49. package/src/polyfills/esbuild.ts +854 -0
  50. package/src/polyfills/events.ts +276 -0
  51. package/src/polyfills/fs.ts +2888 -0
  52. package/src/polyfills/fsevents.ts +79 -0
  53. package/src/polyfills/http.ts +1449 -0
  54. package/src/polyfills/http2.ts +199 -0
  55. package/src/polyfills/https.ts +76 -0
  56. package/src/polyfills/inspector.ts +62 -0
  57. package/src/polyfills/lightningcss.ts +105 -0
  58. package/src/polyfills/module.ts +191 -0
  59. package/src/polyfills/net.ts +353 -0
  60. package/src/polyfills/os.ts +238 -0
  61. package/src/polyfills/path.ts +206 -0
  62. package/src/polyfills/perf_hooks.ts +102 -0
  63. package/src/polyfills/process.ts +690 -0
  64. package/src/polyfills/punycode.ts +159 -0
  65. package/src/polyfills/querystring.ts +93 -0
  66. package/src/polyfills/quic.ts +118 -0
  67. package/src/polyfills/readdirp.ts +229 -0
  68. package/src/polyfills/readline.ts +692 -0
  69. package/src/polyfills/repl.ts +134 -0
  70. package/src/polyfills/rollup.ts +119 -0
  71. package/src/polyfills/sea.ts +33 -0
  72. package/src/polyfills/sqlite.ts +78 -0
  73. package/src/polyfills/stream.ts +1620 -0
  74. package/src/polyfills/string_decoder.ts +25 -0
  75. package/src/polyfills/tailwindcss-oxide.ts +309 -0
  76. package/src/polyfills/test.ts +197 -0
  77. package/src/polyfills/timers.ts +32 -0
  78. package/src/polyfills/tls.ts +105 -0
  79. package/src/polyfills/trace_events.ts +50 -0
  80. package/src/polyfills/tty.ts +71 -0
  81. package/src/polyfills/url.ts +174 -0
  82. package/src/polyfills/util.ts +559 -0
  83. package/src/polyfills/v8.ts +126 -0
  84. package/src/polyfills/vm.ts +132 -0
  85. package/src/polyfills/volume-registry.ts +15 -0
  86. package/src/polyfills/wasi.ts +44 -0
  87. package/src/polyfills/worker_threads.ts +326 -0
  88. package/src/polyfills/ws.ts +595 -0
  89. package/src/polyfills/zlib.ts +881 -0
  90. package/src/request-proxy.ts +716 -0
  91. package/src/script-engine.ts +3375 -0
  92. package/src/sdk/nodepod-fs.ts +93 -0
  93. package/src/sdk/nodepod-process.ts +86 -0
  94. package/src/sdk/nodepod-terminal.ts +350 -0
  95. package/src/sdk/nodepod.ts +509 -0
  96. package/src/sdk/types.ts +70 -0
  97. package/src/shell/commands/bun.ts +121 -0
  98. package/src/shell/commands/directory.ts +297 -0
  99. package/src/shell/commands/file-ops.ts +525 -0
  100. package/src/shell/commands/git.ts +2142 -0
  101. package/src/shell/commands/node.ts +80 -0
  102. package/src/shell/commands/npm.ts +198 -0
  103. package/src/shell/commands/pm-types.ts +45 -0
  104. package/src/shell/commands/pnpm.ts +82 -0
  105. package/src/shell/commands/search.ts +264 -0
  106. package/src/shell/commands/shell-env.ts +352 -0
  107. package/src/shell/commands/text-processing.ts +1152 -0
  108. package/src/shell/commands/yarn.ts +84 -0
  109. package/src/shell/shell-builtins.ts +19 -0
  110. package/src/shell/shell-helpers.ts +250 -0
  111. package/src/shell/shell-interpreter.ts +514 -0
  112. package/src/shell/shell-parser.ts +429 -0
  113. package/src/shell/shell-types.ts +85 -0
  114. package/src/syntax-transforms.ts +561 -0
  115. package/src/threading/engine-worker.ts +64 -0
  116. package/src/threading/inline-worker.ts +372 -0
  117. package/src/threading/offload-types.ts +112 -0
  118. package/src/threading/offload-worker.ts +383 -0
  119. package/src/threading/offload.ts +271 -0
  120. package/src/threading/process-context.ts +92 -0
  121. package/src/threading/process-handle.ts +275 -0
  122. package/src/threading/process-manager.ts +956 -0
  123. package/src/threading/process-worker-entry.ts +854 -0
  124. package/src/threading/shared-vfs.ts +352 -0
  125. package/src/threading/sync-channel.ts +135 -0
  126. package/src/threading/task-queue.ts +177 -0
  127. package/src/threading/vfs-bridge.ts +231 -0
  128. package/src/threading/worker-pool.ts +233 -0
  129. package/src/threading/worker-protocol.ts +358 -0
  130. package/src/threading/worker-vfs.ts +218 -0
  131. package/src/types/externals.d.ts +38 -0
  132. package/src/types/fs-streams.ts +142 -0
  133. package/src/types/manifest.ts +17 -0
  134. package/src/worker-sandbox.ts +90 -0
@@ -0,0 +1,525 @@
1
+ import type { BuiltinFn, ShellContext } from "../shell-types";
2
+ import {
3
+ ok,
4
+ fail,
5
+ resolvePath,
6
+ parseArgs,
7
+ pathModule,
8
+ } from "../shell-helpers";
9
+
10
+ /* ------------------------------------------------------------------ */
11
+ /* Helpers */
12
+ /* ------------------------------------------------------------------ */
13
+
14
+ function formatCat(
15
+ content: string,
16
+ numberAll: boolean,
17
+ numberNonBlank: boolean,
18
+ squeeze: boolean,
19
+ showEnds: boolean,
20
+ showTabs: boolean,
21
+ ): string {
22
+ let lines = content.split("\n");
23
+ if (squeeze) {
24
+ const squeezed: string[] = [];
25
+ let prevBlank = false;
26
+ for (const line of lines) {
27
+ const blank = line.length === 0;
28
+ if (blank && prevBlank) continue;
29
+ squeezed.push(line);
30
+ prevBlank = blank;
31
+ }
32
+ lines = squeezed;
33
+ }
34
+ let lineNum = 1;
35
+ const result = lines.map((line, idx) => {
36
+ let l = line;
37
+ if (showTabs) l = l.replace(/\t/g, "^I");
38
+ if (showEnds && idx < lines.length - 1) l += "$";
39
+ if (numberNonBlank) {
40
+ if (line.length > 0) l = `${String(lineNum++).padStart(6)}\t${l}`;
41
+ } else if (numberAll) {
42
+ l = `${String(lineNum++).padStart(6)}\t${l}`;
43
+ }
44
+ return l;
45
+ });
46
+ return result.join("\n");
47
+ }
48
+
49
+ function copyTree(ctx: ShellContext, src: string, dst: string): void {
50
+ ctx.volume.mkdirSync(dst, { recursive: true });
51
+ for (const name of ctx.volume.readdirSync(src)) {
52
+ const s = `${src}/${name}`;
53
+ const d = `${dst}/${name}`;
54
+ const st = ctx.volume.statSync(s);
55
+ if (st.isDirectory()) {
56
+ copyTree(ctx, s, d);
57
+ } else {
58
+ ctx.volume.writeFileSync(d, ctx.volume.readFileSync(s));
59
+ }
60
+ }
61
+ }
62
+
63
+ function removeTree(ctx: ShellContext, dir: string): void {
64
+ for (const name of ctx.volume.readdirSync(dir)) {
65
+ const full = `${dir}/${name}`;
66
+ const st = ctx.volume.statSync(full);
67
+ if (st.isDirectory()) removeTree(ctx, full);
68
+ else ctx.volume.unlinkSync(full);
69
+ }
70
+ ctx.volume.rmdirSync(dir);
71
+ }
72
+
73
+ /* ------------------------------------------------------------------ */
74
+ /* Commands */
75
+ /* ------------------------------------------------------------------ */
76
+
77
+ const cat: BuiltinFn = (args, ctx, stdin) => {
78
+ const { flags, positional } = parseArgs(args, [
79
+ "n",
80
+ "b",
81
+ "s",
82
+ "E",
83
+ "T",
84
+ "A",
85
+ "e",
86
+ "t",
87
+ "v",
88
+ ]);
89
+ const numberAll = flags.has("n") || flags.has("A");
90
+ const numberNonBlank = flags.has("b");
91
+ const squeeze = flags.has("s");
92
+ const showEnds = flags.has("E") || flags.has("A") || flags.has("e");
93
+ const showTabs = flags.has("T") || flags.has("A") || flags.has("t");
94
+
95
+ if (positional.length === 0 && stdin !== undefined) {
96
+ return ok(
97
+ formatCat(stdin, numberAll, numberNonBlank, squeeze, showEnds, showTabs),
98
+ );
99
+ }
100
+ if (positional.length === 0) return fail("cat: missing operand\n");
101
+
102
+ let out = "";
103
+ for (const file of positional) {
104
+ if (file === "-" && stdin !== undefined) {
105
+ out += formatCat(
106
+ stdin,
107
+ numberAll,
108
+ numberNonBlank,
109
+ squeeze,
110
+ showEnds,
111
+ showTabs,
112
+ );
113
+ continue;
114
+ }
115
+ const p = resolvePath(file, ctx.cwd);
116
+ try {
117
+ const content = ctx.volume.readFileSync(p, "utf8");
118
+ out += formatCat(
119
+ content,
120
+ numberAll,
121
+ numberNonBlank,
122
+ squeeze,
123
+ showEnds,
124
+ showTabs,
125
+ );
126
+ } catch {
127
+ return fail(`cat: ${file}: No such file or directory\n`);
128
+ }
129
+ }
130
+ return ok(out);
131
+ };
132
+
133
+ const head: BuiltinFn = (args, ctx, stdin) => {
134
+ let n = 10;
135
+ let byteMode = false;
136
+ let bytes = 0;
137
+ const files: string[] = [];
138
+ for (let i = 0; i < args.length; i++) {
139
+ if (args[i] === "-n" && i + 1 < args.length) {
140
+ n = parseInt(args[++i], 10) || 10;
141
+ } else if (args[i] === "-c" && i + 1 < args.length) {
142
+ bytes = parseInt(args[++i], 10) || 0;
143
+ byteMode = true;
144
+ } else if (args[i].startsWith("-") && /^\d+$/.test(args[i].slice(1))) {
145
+ n = parseInt(args[i].slice(1), 10);
146
+ } else if (!args[i].startsWith("-")) {
147
+ files.push(args[i]);
148
+ }
149
+ }
150
+
151
+ const doHead = (content: string) => {
152
+ if (byteMode) return content.slice(0, bytes);
153
+ return content.split("\n").slice(0, n).join("\n") + "\n";
154
+ };
155
+
156
+ if (files.length === 0 && stdin !== undefined) return ok(doHead(stdin));
157
+ if (files.length === 0) return fail("head: missing operand\n");
158
+
159
+ let out = "";
160
+ for (const file of files) {
161
+ const p = resolvePath(file, ctx.cwd);
162
+ try {
163
+ const content = ctx.volume.readFileSync(p, "utf8");
164
+ if (files.length > 1) out += `==> ${file} <==\n`;
165
+ out += doHead(content);
166
+ } catch {
167
+ return fail(`head: ${file}: No such file or directory\n`);
168
+ }
169
+ }
170
+ return ok(out);
171
+ };
172
+
173
+ const tail: BuiltinFn = (args, ctx, stdin) => {
174
+ let n = 10;
175
+ let byteMode = false;
176
+ let bytes = 0;
177
+ const files: string[] = [];
178
+ for (let i = 0; i < args.length; i++) {
179
+ if (args[i] === "-n" && i + 1 < args.length) {
180
+ n = parseInt(args[++i], 10) || 10;
181
+ } else if (args[i] === "-c" && i + 1 < args.length) {
182
+ bytes = parseInt(args[++i], 10) || 0;
183
+ byteMode = true;
184
+ } else if (args[i] === "-f") {
185
+ // -f (follow) can't work in VFS, ignore
186
+ } else if (args[i].startsWith("-") && /^\d+$/.test(args[i].slice(1))) {
187
+ n = parseInt(args[i].slice(1), 10);
188
+ } else if (!args[i].startsWith("-")) {
189
+ files.push(args[i]);
190
+ }
191
+ }
192
+
193
+ const doTail = (content: string) => {
194
+ if (byteMode) return content.slice(-bytes);
195
+ const lines = content.split("\n");
196
+ const start = Math.max(
197
+ 0,
198
+ lines.length - n - (content.endsWith("\n") ? 1 : 0),
199
+ );
200
+ return lines.slice(start).join("\n");
201
+ };
202
+
203
+ if (files.length === 0 && stdin !== undefined) return ok(doTail(stdin));
204
+ if (files.length === 0) return fail("tail: missing operand\n");
205
+
206
+ let out = "";
207
+ for (const file of files) {
208
+ const p = resolvePath(file, ctx.cwd);
209
+ try {
210
+ const content = ctx.volume.readFileSync(p, "utf8");
211
+ if (files.length > 1) out += `==> ${file} <==\n`;
212
+ out += doTail(content);
213
+ } catch {
214
+ return fail(`tail: ${file}: No such file or directory\n`);
215
+ }
216
+ }
217
+ return ok(out);
218
+ };
219
+
220
+ const touch: BuiltinFn = (args, ctx) => {
221
+ if (args.length === 0) return fail("touch: missing operand\n");
222
+ for (const file of args) {
223
+ if (file.startsWith("-")) continue;
224
+ const p = resolvePath(file, ctx.cwd);
225
+ if (!ctx.volume.existsSync(p)) {
226
+ ctx.volume.writeFileSync(p, "");
227
+ }
228
+ }
229
+ return ok();
230
+ };
231
+
232
+ const cpCmd: BuiltinFn = (args, ctx) => {
233
+ const { flags, positional } = parseArgs(args, ["r", "R", "f", "n", "v"]);
234
+ const recursive = flags.has("r") || flags.has("R") || flags.has("recursive");
235
+ const verbose = flags.has("v");
236
+
237
+ if (positional.length < 2) return fail("cp: missing operand\n");
238
+
239
+ const dest = positional[positional.length - 1];
240
+ const sources = positional.slice(0, -1);
241
+ const dstPath = resolvePath(dest, ctx.cwd);
242
+ let out = "";
243
+
244
+ for (const src of sources) {
245
+ const srcPath = resolvePath(src, ctx.cwd);
246
+ try {
247
+ const st = ctx.volume.statSync(srcPath);
248
+ if (st.isDirectory()) {
249
+ if (!recursive)
250
+ return fail(`cp: -r not specified; omitting directory '${src}'\n`);
251
+ copyTree(ctx, srcPath, dstPath);
252
+ if (verbose) out += `'${src}' -> '${dest}'\n`;
253
+ } else {
254
+ let destFinal = dstPath;
255
+ if (ctx.volume.existsSync(dstPath)) {
256
+ try {
257
+ if (ctx.volume.statSync(dstPath).isDirectory()) {
258
+ destFinal = `${dstPath}/${pathModule.basename(srcPath)}`;
259
+ }
260
+ } catch {
261
+ /* */
262
+ }
263
+ }
264
+ ctx.volume.writeFileSync(destFinal, ctx.volume.readFileSync(srcPath));
265
+ if (verbose) out += `'${src}' -> '${dest}'\n`;
266
+ }
267
+ } catch {
268
+ return fail(`cp: cannot stat '${src}': No such file or directory\n`);
269
+ }
270
+ }
271
+ return ok(out);
272
+ };
273
+
274
+ const mv: BuiltinFn = (args, ctx) => {
275
+ const { flags, positional } = parseArgs(args, ["f", "n", "v"]);
276
+ const verbose = flags.has("v");
277
+ if (positional.length < 2) return fail("mv: missing operand\n");
278
+
279
+ const dest = positional[positional.length - 1];
280
+ const sources = positional.slice(0, -1);
281
+ const dstPath = resolvePath(dest, ctx.cwd);
282
+ let out = "";
283
+
284
+ for (const src of sources) {
285
+ const srcPath = resolvePath(src, ctx.cwd);
286
+ try {
287
+ let destFinal = dstPath;
288
+ if (ctx.volume.existsSync(dstPath)) {
289
+ try {
290
+ if (ctx.volume.statSync(dstPath).isDirectory()) {
291
+ destFinal = `${dstPath}/${pathModule.basename(srcPath)}`;
292
+ }
293
+ } catch {
294
+ /* */
295
+ }
296
+ }
297
+ ctx.volume.renameSync(srcPath, destFinal);
298
+ if (verbose) out += `renamed '${src}' -> '${dest}'\n`;
299
+ } catch {
300
+ return fail(
301
+ `mv: cannot move '${src}' to '${dest}': No such file or directory\n`,
302
+ );
303
+ }
304
+ }
305
+ return ok(out);
306
+ };
307
+
308
+ const rm: BuiltinFn = (args, ctx) => {
309
+ const { flags, positional } = parseArgs(args, ["r", "R", "f", "v"]);
310
+ const recursive = flags.has("r") || flags.has("R") || flags.has("recursive");
311
+ const force = flags.has("f") || flags.has("force");
312
+ const verbose = flags.has("v");
313
+
314
+ if (positional.length === 0 && !force) return fail("rm: missing operand\n");
315
+
316
+ let out = "";
317
+ for (const target of positional) {
318
+ const p = resolvePath(target, ctx.cwd);
319
+ if (!ctx.volume.existsSync(p)) {
320
+ if (force) continue;
321
+ return fail(`rm: cannot remove '${target}': No such file or directory\n`);
322
+ }
323
+ const st = ctx.volume.statSync(p);
324
+ if (st.isDirectory()) {
325
+ if (!recursive)
326
+ return fail(`rm: cannot remove '${target}': Is a directory\n`);
327
+ removeTree(ctx, p);
328
+ if (verbose) out += `removed directory '${target}'\n`;
329
+ } else {
330
+ ctx.volume.unlinkSync(p);
331
+ if (verbose) out += `removed '${target}'\n`;
332
+ }
333
+ }
334
+ return ok(out);
335
+ };
336
+
337
+ const mkdir_cmd: BuiltinFn = (args, ctx) => {
338
+ const { flags, positional } = parseArgs(args, ["p", "v"]);
339
+ const recursive = flags.has("p");
340
+ const verbose = flags.has("v");
341
+
342
+ if (positional.length === 0) return fail("mkdir: missing operand\n");
343
+
344
+ let out = "";
345
+ for (const dir of positional) {
346
+ const p = resolvePath(dir, ctx.cwd);
347
+ try {
348
+ ctx.volume.mkdirSync(p, { recursive });
349
+ if (verbose) out += `mkdir: created directory '${dir}'\n`;
350
+ } catch (e) {
351
+ if (!recursive)
352
+ return fail(
353
+ `mkdir: cannot create directory '${dir}': ${e instanceof Error ? e.message : String(e)}\n`,
354
+ );
355
+ }
356
+ }
357
+ return ok(out);
358
+ };
359
+
360
+ const rmdir_cmd: BuiltinFn = (args, ctx) => {
361
+ const { flags, positional } = parseArgs(args, ["p", "v"]);
362
+ const parents = flags.has("p");
363
+ const verbose = flags.has("v");
364
+
365
+ if (positional.length === 0) return fail("rmdir: missing operand\n");
366
+
367
+ let out = "";
368
+ for (const dir of positional) {
369
+ let p = resolvePath(dir, ctx.cwd);
370
+ try {
371
+ ctx.volume.rmdirSync(p);
372
+ if (verbose) out += `rmdir: removing directory, '${dir}'\n`;
373
+ if (parents) {
374
+ while (p !== "/") {
375
+ p = pathModule.dirname(p);
376
+ if (p === "/") break;
377
+ try {
378
+ ctx.volume.rmdirSync(p);
379
+ } catch {
380
+ break;
381
+ }
382
+ }
383
+ }
384
+ } catch {
385
+ return fail(
386
+ `rmdir: failed to remove '${dir}': Directory not empty or not found\n`,
387
+ );
388
+ }
389
+ }
390
+ return ok(out);
391
+ };
392
+
393
+ const chmod: BuiltinFn = (args, _ctx) => {
394
+ if (args.length < 2) return fail("chmod: missing operand\n");
395
+ return ok(); // no-op in VFS
396
+ };
397
+
398
+ const wc: BuiltinFn = (args, ctx, stdin) => {
399
+ const { flags, positional } = parseArgs(args, ["l", "w", "c", "m", "L"]);
400
+ const showLines = flags.has("l");
401
+ const showWords = flags.has("w");
402
+ const showBytes = flags.has("c");
403
+ const showChars = flags.has("m");
404
+ const showMaxLine = flags.has("L");
405
+ const showAll =
406
+ !showLines && !showWords && !showBytes && !showChars && !showMaxLine;
407
+
408
+ const doWc = (content: string, label?: string) => {
409
+ const lines = content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
410
+ const words = content.split(/\s+/).filter(Boolean).length;
411
+ const bytes = new TextEncoder().encode(content).length;
412
+ const chars = [...content].length;
413
+ const maxLine = content
414
+ .split("\n")
415
+ .reduce((mx, l) => Math.max(mx, l.length), 0);
416
+
417
+ const parts: string[] = [];
418
+ if (showAll || showLines) parts.push(String(lines).padStart(7));
419
+ if (showAll || showWords) parts.push(String(words).padStart(7));
420
+ if (showChars) parts.push(String(chars).padStart(7));
421
+ if (showAll || showBytes) parts.push(String(bytes).padStart(7));
422
+ if (showMaxLine) parts.push(String(maxLine).padStart(7));
423
+
424
+ const suffix = label ? ` ${label}` : "";
425
+ return parts.join("") + suffix + "\n";
426
+ };
427
+
428
+ if (positional.length === 0 && stdin !== undefined) return ok(doWc(stdin));
429
+ if (positional.length === 0) return fail("wc: missing operand\n");
430
+
431
+ let out = "";
432
+ let totalLines = 0,
433
+ totalWords = 0,
434
+ totalBytes = 0;
435
+ for (const file of positional) {
436
+ const p = resolvePath(file, ctx.cwd);
437
+ try {
438
+ const content = ctx.volume.readFileSync(p, "utf8");
439
+ out += doWc(content, file);
440
+ totalLines +=
441
+ content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
442
+ totalWords += content.split(/\s+/).filter(Boolean).length;
443
+ totalBytes += new TextEncoder().encode(content).length;
444
+ } catch {
445
+ return fail(`wc: ${file}: No such file or directory\n`);
446
+ }
447
+ }
448
+ if (positional.length > 1) {
449
+ const parts: string[] = [];
450
+ if (showAll || showLines) parts.push(String(totalLines).padStart(7));
451
+ if (showAll || showWords) parts.push(String(totalWords).padStart(7));
452
+ if (showAll || showBytes) parts.push(String(totalBytes).padStart(7));
453
+ out += parts.join("") + " total\n";
454
+ }
455
+ return ok(out);
456
+ };
457
+
458
+ const tee: BuiltinFn = (args, ctx, stdin) => {
459
+ const { flags, positional } = parseArgs(args, ["a"]);
460
+ const append = flags.has("a");
461
+ const content = stdin ?? "";
462
+
463
+ for (const file of positional) {
464
+ const p = resolvePath(file, ctx.cwd);
465
+ if (append && ctx.volume.existsSync(p)) {
466
+ const existing = ctx.volume.readFileSync(p, "utf8");
467
+ ctx.volume.writeFileSync(p, existing + content);
468
+ } else {
469
+ ctx.volume.writeFileSync(p, content);
470
+ }
471
+ }
472
+ return ok(content);
473
+ };
474
+
475
+ const readlink_cmd: BuiltinFn = (args, ctx) => {
476
+ const { positional } = parseArgs(args, ["f", "e", "m", "n", "q", "z"]);
477
+ if (positional.length === 0) return fail("readlink: missing operand\n");
478
+ const p = resolvePath(positional[0], ctx.cwd);
479
+ return ok(p + "\n");
480
+ };
481
+
482
+ const ln_cmd: BuiltinFn = (args, ctx) => {
483
+ const { positional } = parseArgs(args, ["s", "f"]);
484
+ if (positional.length < 2) return fail("ln: missing operand\n");
485
+ const src = resolvePath(positional[0], ctx.cwd);
486
+ const dst = resolvePath(positional[1], ctx.cwd);
487
+ try {
488
+ const content = ctx.volume.readFileSync(src);
489
+ ctx.volume.writeFileSync(dst, content);
490
+ return ok();
491
+ } catch {
492
+ return fail(
493
+ `ln: cannot create link '${positional[1]}': source not found\n`,
494
+ );
495
+ }
496
+ };
497
+
498
+ const writeFile: BuiltinFn = (args, ctx) => {
499
+ if (args.length < 2) return fail("write: missing arguments\n");
500
+ const path = resolvePath(args[0], ctx.cwd);
501
+ ctx.volume.writeFileSync(path, args.slice(1).join(" "));
502
+ return ok();
503
+ };
504
+
505
+ /* ------------------------------------------------------------------ */
506
+ /* Registry */
507
+ /* ------------------------------------------------------------------ */
508
+
509
+ export const fileOpsCommands: [string, BuiltinFn][] = [
510
+ ["cat", cat],
511
+ ["head", head],
512
+ ["tail", tail],
513
+ ["touch", touch],
514
+ ["cp", cpCmd],
515
+ ["mv", mv],
516
+ ["rm", rm],
517
+ ["mkdir", mkdir_cmd],
518
+ ["rmdir", rmdir_cmd],
519
+ ["chmod", chmod],
520
+ ["wc", wc],
521
+ ["tee", tee],
522
+ ["ln", ln_cmd],
523
+ ["readlink", readlink_cmd],
524
+ ["write", writeFile],
525
+ ];