@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,1152 @@
1
+ import type { BuiltinFn, ShellContext } from "../shell-types";
2
+ import {
3
+ ok,
4
+ fail,
5
+ EXIT_OK,
6
+ EXIT_FAIL,
7
+ resolvePath,
8
+ parseArgs,
9
+ processEscapes,
10
+ expandCharClass,
11
+ RESET,
12
+ GREEN,
13
+ MAGENTA,
14
+ CYAN,
15
+ BOLD_RED,
16
+ } from "../shell-helpers";
17
+ import { YES_REPEAT_COUNT } from "../../constants/config";
18
+
19
+ /* ------------------------------------------------------------------ */
20
+ /* echo / printf */
21
+ /* ------------------------------------------------------------------ */
22
+
23
+ const echo: BuiltinFn = (args) => {
24
+ let noNewline = false;
25
+ let enableEscapes = false;
26
+ let start = 0;
27
+
28
+ while (start < args.length) {
29
+ const a = args[start];
30
+ if (a === "-n") {
31
+ noNewline = true;
32
+ start++;
33
+ } else if (a === "-e") {
34
+ enableEscapes = true;
35
+ start++;
36
+ } else if (a === "-E") {
37
+ enableEscapes = false;
38
+ start++;
39
+ } else if (a === "-ne" || a === "-en") {
40
+ noNewline = true;
41
+ enableEscapes = true;
42
+ start++;
43
+ } else if (a === "-nE" || a === "-En") {
44
+ noNewline = true;
45
+ start++;
46
+ } else break;
47
+ }
48
+
49
+ let output = args.slice(start).join(" ");
50
+ if (enableEscapes) output = processEscapes(output);
51
+ return ok(output + (noNewline ? "" : "\n"));
52
+ };
53
+
54
+ const printf_cmd: BuiltinFn = (args) => {
55
+ if (args.length === 0) return ok();
56
+ const fmt = args[0];
57
+ const vals = args.slice(1);
58
+ let out = "";
59
+ let vi = 0;
60
+
61
+ let i = 0;
62
+ while (i < fmt.length) {
63
+ if (fmt[i] === "\\" && i + 1 < fmt.length) {
64
+ const c = fmt[i + 1];
65
+ if (c === "n") { out += "\n"; i += 2; continue; }
66
+ if (c === "t") { out += "\t"; i += 2; continue; }
67
+ if (c === "r") { out += "\r"; i += 2; continue; }
68
+ if (c === "a") { out += "\x07"; i += 2; continue; }
69
+ if (c === "b") { out += "\b"; i += 2; continue; }
70
+ if (c === "f") { out += "\f"; i += 2; continue; }
71
+ if (c === "v") { out += "\x0b"; i += 2; continue; }
72
+ if (c === "\\") { out += "\\"; i += 2; continue; }
73
+ if (c === "0") {
74
+ let oct = "";
75
+ let j = i + 2;
76
+ while (j < fmt.length && j < i + 5 && fmt[j] >= "0" && fmt[j] <= "7")
77
+ oct += fmt[j++];
78
+ out += String.fromCharCode(parseInt(oct || "0", 8));
79
+ i = j;
80
+ continue;
81
+ }
82
+ if (c === "x") {
83
+ const hex = fmt.slice(i + 2, i + 4).match(/^[0-9a-fA-F]+/)?.[0] ?? "";
84
+ if (hex) {
85
+ out += String.fromCharCode(parseInt(hex, 16));
86
+ i += 2 + hex.length;
87
+ continue;
88
+ }
89
+ }
90
+ out += fmt[i];
91
+ i++;
92
+ continue;
93
+ }
94
+
95
+ if (fmt[i] === "%" && i + 1 < fmt.length) {
96
+ i++;
97
+ let fmtFlags = "";
98
+ while ("-+ 0#".includes(fmt[i])) fmtFlags += fmt[i++];
99
+ let width = "";
100
+ if (fmt[i] === "*") { width = vals[vi++] ?? "0"; i++; }
101
+ else { while (fmt[i] >= "0" && fmt[i] <= "9") width += fmt[i++]; }
102
+ let prec = "";
103
+ if (fmt[i] === ".") {
104
+ i++;
105
+ if (fmt[i] === "*") { prec = vals[vi++] ?? "0"; i++; }
106
+ else { while (fmt[i] >= "0" && fmt[i] <= "9") prec += fmt[i++]; }
107
+ }
108
+ const spec = fmt[i++];
109
+ const val = vals[vi++] ?? "";
110
+
111
+ if (spec === "%") { out += "%"; vi--; continue; }
112
+ if (spec === "s") {
113
+ let s = val;
114
+ if (prec) s = s.slice(0, parseInt(prec));
115
+ const w = parseInt(width) || 0;
116
+ if (fmtFlags.includes("-")) out += s.padEnd(w);
117
+ else out += s.padStart(w);
118
+ continue;
119
+ }
120
+ if (spec === "d" || spec === "i") {
121
+ const n = parseInt(val) || 0;
122
+ let s = (fmtFlags.includes("+") && n >= 0 ? "+" : "") + String(n);
123
+ if (fmtFlags.includes(" ") && n >= 0 && !fmtFlags.includes("+")) s = " " + s;
124
+ const w = parseInt(width) || 0;
125
+ const pad = fmtFlags.includes("0") && !fmtFlags.includes("-") ? "0" : " ";
126
+ if (fmtFlags.includes("-")) out += s.padEnd(w);
127
+ else if (pad === "0" && s[0] === "-") out += "-" + s.slice(1).padStart(w - 1, "0");
128
+ else out += s.padStart(w, pad);
129
+ continue;
130
+ }
131
+ if (spec === "f") {
132
+ const n = parseFloat(val) || 0;
133
+ const p = prec !== "" ? parseInt(prec) : 6;
134
+ let s = n.toFixed(p);
135
+ if (fmtFlags.includes("+") && n >= 0) s = "+" + s;
136
+ const w = parseInt(width) || 0;
137
+ if (fmtFlags.includes("-")) out += s.padEnd(w);
138
+ else out += s.padStart(w, fmtFlags.includes("0") ? "0" : " ");
139
+ continue;
140
+ }
141
+ if (spec === "x") {
142
+ const n = (parseInt(val) || 0) >>> 0;
143
+ let s = n.toString(16);
144
+ if (fmtFlags.includes("#") && n !== 0) s = "0x" + s;
145
+ out += s.padStart(parseInt(width) || 0, fmtFlags.includes("0") ? "0" : " ");
146
+ continue;
147
+ }
148
+ if (spec === "X") {
149
+ const n = (parseInt(val) || 0) >>> 0;
150
+ let s = n.toString(16).toUpperCase();
151
+ if (fmtFlags.includes("#") && n !== 0) s = "0X" + s;
152
+ out += s.padStart(parseInt(width) || 0, fmtFlags.includes("0") ? "0" : " ");
153
+ continue;
154
+ }
155
+ if (spec === "o") {
156
+ const n = (parseInt(val) || 0) >>> 0;
157
+ let s = n.toString(8);
158
+ if (fmtFlags.includes("#") && n !== 0) s = "0" + s;
159
+ out += s.padStart(parseInt(width) || 0, fmtFlags.includes("0") ? "0" : " ");
160
+ continue;
161
+ }
162
+ if (spec === "e" || spec === "E") {
163
+ const n = parseFloat(val) || 0;
164
+ const p = prec !== "" ? parseInt(prec) : 6;
165
+ let s = spec === "E" ? n.toExponential(p).toUpperCase() : n.toExponential(p);
166
+ if (fmtFlags.includes("+") && n >= 0) s = "+" + s;
167
+ out += s.padStart(parseInt(width) || 0);
168
+ continue;
169
+ }
170
+ if (spec === "g" || spec === "G") {
171
+ const n = parseFloat(val) || 0;
172
+ const p = prec !== "" ? parseInt(prec) : 6;
173
+ let s = spec === "G" ? n.toPrecision(p).toUpperCase() : n.toPrecision(p);
174
+ if (fmtFlags.includes("+") && n >= 0) s = "+" + s;
175
+ out += s.padStart(parseInt(width) || 0);
176
+ continue;
177
+ }
178
+ if (spec === "c") { out += val ? val[0] : ""; continue; }
179
+ out += "%" + spec;
180
+ vi--;
181
+ continue;
182
+ }
183
+ out += fmt[i++];
184
+ }
185
+ return ok(out);
186
+ };
187
+
188
+ /* ------------------------------------------------------------------ */
189
+ /* grep / egrep / fgrep */
190
+ /* ------------------------------------------------------------------ */
191
+
192
+ interface GrepOpts {
193
+ regex: RegExp;
194
+ highlightRe: RegExp;
195
+ patternStr: string;
196
+ ignoreCase: boolean;
197
+ invert: boolean;
198
+ countOnly: boolean;
199
+ filesOnly: boolean;
200
+ lineNumbers: boolean;
201
+ onlyMatching: boolean;
202
+ quiet: boolean;
203
+ beforeCtx: number;
204
+ afterCtx: number;
205
+ maxCount: number;
206
+ }
207
+
208
+ function grepLines(content: string, opts: GrepOpts, label?: string): string {
209
+ const {
210
+ regex, highlightRe, patternStr, ignoreCase, invert,
211
+ countOnly, filesOnly, lineNumbers, onlyMatching, quiet,
212
+ beforeCtx, afterCtx, maxCount,
213
+ } = opts;
214
+ const lines = content.split("\n");
215
+ const matchedIndices = new Set<number>();
216
+ let matchCount = 0;
217
+
218
+ for (let i = 0; i < lines.length; i++) {
219
+ if (regex.test(lines[i]) !== invert) {
220
+ matchedIndices.add(i);
221
+ matchCount++;
222
+ if (matchCount >= maxCount) break;
223
+ }
224
+ }
225
+
226
+ if (countOnly) {
227
+ return (label ? `${MAGENTA}${label}${RESET}${CYAN}:${RESET}` : "") +
228
+ matchedIndices.size + "\n";
229
+ }
230
+ if (filesOnly && matchedIndices.size > 0) {
231
+ return `${MAGENTA}${label ?? ""}${RESET}\n`;
232
+ }
233
+ if (quiet) return matchedIndices.size > 0 ? "\0" : "";
234
+
235
+ const showLines = new Set<number>();
236
+ for (const idx of matchedIndices) {
237
+ for (
238
+ let j = Math.max(0, idx - beforeCtx);
239
+ j <= Math.min(lines.length - 1, idx + afterCtx);
240
+ j++
241
+ ) {
242
+ showLines.add(j);
243
+ }
244
+ }
245
+
246
+ let out = "";
247
+ let prevShown = -2;
248
+ for (let i = 0; i < lines.length; i++) {
249
+ if (!showLines.has(i)) continue;
250
+ if (
251
+ prevShown >= 0 &&
252
+ i > prevShown + 1 &&
253
+ (beforeCtx > 0 || afterCtx > 0)
254
+ ) {
255
+ out += "--\n";
256
+ }
257
+ prevShown = i;
258
+
259
+ const isMatch = matchedIndices.has(i);
260
+ const sep = isMatch ? `${CYAN}:${RESET}` : `${CYAN}-${RESET}`;
261
+ const prefix = label ? `${MAGENTA}${label}${RESET}${sep}` : "";
262
+ const num = lineNumbers ? `${GREEN}${i + 1}${RESET}${sep}` : "";
263
+
264
+ if (onlyMatching && isMatch && !invert) {
265
+ const gRe = new RegExp(patternStr, ignoreCase ? "gi" : "g");
266
+ let m: RegExpExecArray | null;
267
+ while ((m = gRe.exec(lines[i])) !== null) {
268
+ out += `${prefix}${num}${BOLD_RED}${m[0]}${RESET}\n`;
269
+ }
270
+ } else {
271
+ const hl =
272
+ isMatch && !invert
273
+ ? lines[i].replace(highlightRe, (m) => `${BOLD_RED}${m}${RESET}`)
274
+ : lines[i];
275
+ out += `${prefix}${num}${hl}\n`;
276
+ }
277
+ }
278
+
279
+ return out;
280
+ }
281
+
282
+ function grepDirFull(
283
+ ctx: ShellContext,
284
+ dir: string,
285
+ opts: GrepOpts,
286
+ ): string {
287
+ let out = "";
288
+ try {
289
+ for (const name of ctx.volume.readdirSync(dir)) {
290
+ const full = `${dir}/${name}`;
291
+ const st = ctx.volume.statSync(full);
292
+ if (st.isDirectory()) {
293
+ out += grepDirFull(ctx, full, opts);
294
+ } else {
295
+ try {
296
+ const content = ctx.volume.readFileSync(full, "utf8");
297
+ out += grepLines(content, opts, full);
298
+ } catch {
299
+ /* skip binary/unreadable */
300
+ }
301
+ }
302
+ }
303
+ } catch {
304
+ /* skip unreadable dirs */
305
+ }
306
+ return out;
307
+ }
308
+
309
+ const grep_cmd: BuiltinFn = (args, ctx, stdin) => {
310
+ const { flags, opts: parsedOpts, positional } = parseArgs(
311
+ args,
312
+ [
313
+ "i", "v", "c", "l", "n", "r", "R", "o", "w", "x",
314
+ "E", "F", "P", "H", "h", "q", "s", "z",
315
+ ],
316
+ ["A", "B", "C", "m", "e", "f"],
317
+ );
318
+ const ignoreCase = flags.has("i");
319
+ const invert = flags.has("v");
320
+ const countOnly = flags.has("c");
321
+ const filesOnly = flags.has("l");
322
+ const lineNumbers = flags.has("n");
323
+ const recursive = flags.has("r") || flags.has("R");
324
+ const onlyMatching = flags.has("o");
325
+ const wordRegex = flags.has("w");
326
+ const lineRegex = flags.has("x");
327
+ const fixedStrings = flags.has("F");
328
+ const quiet = flags.has("q");
329
+ const suppressErrors = flags.has("s");
330
+ const afterCtx = parseInt(parsedOpts["A"] || parsedOpts["C"] || "0");
331
+ const beforeCtx = parseInt(parsedOpts["B"] || parsedOpts["C"] || "0");
332
+ const maxCount = parsedOpts["m"] ? parseInt(parsedOpts["m"]) : Infinity;
333
+
334
+ let patternStr: string;
335
+ if (parsedOpts["e"] !== undefined) {
336
+ patternStr = parsedOpts["e"];
337
+ } else if (positional.length === 0) {
338
+ return fail("grep: missing pattern\n");
339
+ } else {
340
+ patternStr = positional.shift()!;
341
+ }
342
+
343
+ if (fixedStrings)
344
+ patternStr = patternStr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
345
+ if (wordRegex) patternStr = `\\b${patternStr}\\b`;
346
+ if (lineRegex) patternStr = `^${patternStr}$`;
347
+
348
+ let regex: RegExp;
349
+ let highlightRe: RegExp;
350
+ try {
351
+ regex = new RegExp(patternStr, ignoreCase ? "im" : "m");
352
+ highlightRe = new RegExp(patternStr, ignoreCase ? "gi" : "g");
353
+ } catch {
354
+ return fail(`grep: Invalid regular expression: '${patternStr}'\n`);
355
+ }
356
+
357
+ const grepOpts: GrepOpts = {
358
+ regex, highlightRe, patternStr, ignoreCase, invert,
359
+ countOnly, filesOnly, lineNumbers, onlyMatching, quiet,
360
+ beforeCtx, afterCtx, maxCount,
361
+ };
362
+
363
+ const files = positional;
364
+
365
+ if (files.length === 0 && stdin !== undefined) {
366
+ const result = grepLines(stdin, grepOpts);
367
+ if (quiet) return result ? EXIT_OK : EXIT_FAIL;
368
+ return result ? ok(result) : EXIT_FAIL;
369
+ }
370
+
371
+ if (files.length === 0) {
372
+ return fail("grep: missing file operand\n");
373
+ }
374
+
375
+ let out = "";
376
+ const multiFile = files.length > 1 || recursive;
377
+ let anyMatch = false;
378
+
379
+ for (const file of files) {
380
+ const p = resolvePath(file, ctx.cwd);
381
+ try {
382
+ const st = ctx.volume.statSync(p);
383
+ if (st.isDirectory()) {
384
+ if (recursive) {
385
+ const r = grepDirFull(ctx, p, grepOpts);
386
+ out += r;
387
+ if (r) anyMatch = true;
388
+ } else if (!suppressErrors) {
389
+ out += `grep: ${file}: Is a directory\n`;
390
+ }
391
+ continue;
392
+ }
393
+ const content = ctx.volume.readFileSync(p, "utf8");
394
+ const result = grepLines(content, grepOpts, multiFile ? file : undefined);
395
+ out += result;
396
+ if (result) anyMatch = true;
397
+ } catch {
398
+ if (!suppressErrors)
399
+ return fail(`grep: ${file}: No such file or directory\n`);
400
+ }
401
+ }
402
+
403
+ if (quiet) return anyMatch ? EXIT_OK : EXIT_FAIL;
404
+ return anyMatch ? ok(out) : { stdout: out, stderr: "", exitCode: 1 };
405
+ };
406
+
407
+ /* ------------------------------------------------------------------ */
408
+ /* sed */
409
+ /* ------------------------------------------------------------------ */
410
+
411
+ interface SedCmd {
412
+ addr?:
413
+ | { type: "line"; n: number }
414
+ | { type: "range"; from: number; to: number }
415
+ | { type: "last" }
416
+ | { type: "regex"; re: RegExp };
417
+ type: string;
418
+ pattern?: string;
419
+ replacement?: string;
420
+ sFlags?: string;
421
+ text?: string;
422
+ printAfter?: boolean;
423
+ }
424
+
425
+ function parseSedScript(script: string): SedCmd[] | string {
426
+ const cmds: SedCmd[] = [];
427
+ const parts = script.split(/\s*;\s*|\n/).filter(Boolean);
428
+
429
+ for (const part of parts) {
430
+ let rest = part.trim();
431
+ if (!rest) continue;
432
+
433
+ let addr: SedCmd["addr"];
434
+ if (rest[0] === "$") {
435
+ addr = { type: "last" };
436
+ rest = rest.slice(1);
437
+ } else if (/^\d/.test(rest)) {
438
+ const m = rest.match(/^(\d+)(?:,(\d+|\$))?/);
439
+ if (m) {
440
+ rest = rest.slice(m[0].length);
441
+ if (m[2]) {
442
+ const to = m[2] === "$" ? Infinity : parseInt(m[2]);
443
+ addr = { type: "range", from: parseInt(m[1]), to };
444
+ } else {
445
+ addr = { type: "line", n: parseInt(m[1]) };
446
+ }
447
+ }
448
+ } else if (rest[0] === "/") {
449
+ const end = rest.indexOf("/", 1);
450
+ if (end > 0) {
451
+ const pattern = rest.slice(1, end);
452
+ try {
453
+ addr = { type: "regex", re: new RegExp(pattern) };
454
+ } catch {
455
+ return `invalid regular expression: ${pattern}`;
456
+ }
457
+ rest = rest.slice(end + 1);
458
+ }
459
+ }
460
+
461
+ const cmd = rest[0];
462
+ rest = rest.slice(1);
463
+
464
+ if (cmd === "s") {
465
+ const delim = rest[0];
466
+ if (!delim) return `unsupported expression: ${part}`;
467
+ const parts2 = rest.slice(1).split(delim);
468
+ if (parts2.length < 2) return `unsupported expression: ${part}`;
469
+ const printAfter = parts2[2]?.includes("p") ?? false;
470
+ const sFlags = (parts2[2] ?? "").replace("p", "") || undefined;
471
+ cmds.push({ addr, type: "s", pattern: parts2[0], replacement: parts2[1], sFlags, printAfter });
472
+ } else if (cmd === "d") { cmds.push({ addr, type: "d" }); }
473
+ else if (cmd === "p") { cmds.push({ addr, type: "p" }); }
474
+ else if (cmd === "q") { cmds.push({ addr, type: "q" }); }
475
+ else if (cmd === "a") { cmds.push({ addr, type: "a", text: rest.replace(/^\\?\s*/, "") }); }
476
+ else if (cmd === "i") { cmds.push({ addr, type: "i", text: rest.replace(/^\\?\s*/, "") }); }
477
+ else if (cmd === "c") { cmds.push({ addr, type: "c", text: rest.replace(/^\\?\s*/, "") }); }
478
+ else if (cmd === "y") {
479
+ const delim2 = rest[0];
480
+ const parts2 = rest.slice(1).split(delim2);
481
+ if (parts2.length < 2) return `unsupported expression: ${part}`;
482
+ cmds.push({ addr, type: "y", pattern: parts2[0], replacement: parts2[1] });
483
+ } else if (cmd === "=") { cmds.push({ addr, type: "=" }); }
484
+ else { return `unsupported command: ${cmd}`; }
485
+ }
486
+ return cmds;
487
+ }
488
+
489
+ function sedAddressMatch(
490
+ addr: SedCmd["addr"],
491
+ lineNum: number,
492
+ totalLines: number,
493
+ _line: string,
494
+ ): boolean {
495
+ if (!addr) return true;
496
+ if (addr.type === "line") return lineNum === addr.n;
497
+ if (addr.type === "range")
498
+ return lineNum >= addr.from && lineNum <= (addr.to === Infinity ? totalLines : addr.to);
499
+ if (addr.type === "last") return lineNum === totalLines;
500
+ if (addr.type === "regex") return addr.re.test(_line);
501
+ return true;
502
+ }
503
+
504
+ const sed_cmd: BuiltinFn = (args, ctx, stdin) => {
505
+ const { flags, positional } = parseArgs(args, ["i", "n", "r", "E"]);
506
+ const inPlace = flags.has("i");
507
+ const quietMode = flags.has("n");
508
+
509
+ if (positional.length === 0) return fail("sed: missing expression\n");
510
+
511
+ const expressions = positional[0];
512
+ const files = positional.slice(1);
513
+
514
+ const cmds = parseSedScript(expressions);
515
+ if (typeof cmds === "string") return fail(`sed: ${cmds}\n`);
516
+
517
+ const doSed = (content: string): string => {
518
+ const lines = content.split("\n");
519
+ let out = "";
520
+ for (let i = 0; i < lines.length; i++) {
521
+ const lineNum = i + 1;
522
+ const isLast = i === lines.length - 1;
523
+ let line = lines[i];
524
+ let suppress = quietMode;
525
+ let deleted = false;
526
+
527
+ for (const cmd of cmds) {
528
+ if (deleted) break;
529
+ if (!sedAddressMatch(cmd.addr, lineNum, lines.length, line)) continue;
530
+
531
+ switch (cmd.type) {
532
+ case "s": {
533
+ let re: RegExp;
534
+ try {
535
+ re = new RegExp(cmd.pattern!, cmd.sFlags || undefined);
536
+ } catch {
537
+ break;
538
+ }
539
+ line = line.replace(re, cmd.replacement!);
540
+ if (cmd.printAfter && re.test(lines[i])) suppress = false;
541
+ break;
542
+ }
543
+ case "d": deleted = true; break;
544
+ case "p": out += line + "\n"; break;
545
+ case "q": { if (!suppress) out += line + "\n"; return out; }
546
+ case "a": out += line + "\n" + cmd.text! + "\n"; suppress = true; break;
547
+ case "i": out += cmd.text! + "\n"; break;
548
+ case "c": out += cmd.text! + "\n"; deleted = true; break;
549
+ case "y": {
550
+ const from = cmd.pattern!;
551
+ const to = cmd.replacement!;
552
+ let result = "";
553
+ for (const ch of line) {
554
+ const idx = from.indexOf(ch);
555
+ result += idx >= 0 ? to[idx] : ch;
556
+ }
557
+ line = result;
558
+ break;
559
+ }
560
+ case "=": out += lineNum + "\n"; break;
561
+ }
562
+ }
563
+ if (!deleted && !suppress) {
564
+ out += line + (isLast && !content.endsWith("\n") ? "" : "\n");
565
+ }
566
+ }
567
+ return out;
568
+ };
569
+
570
+ if (files.length === 0 && stdin !== undefined) return ok(doSed(stdin));
571
+ if (files.length === 0) return fail("sed: missing input\n");
572
+
573
+ let out = "";
574
+ for (const file of files) {
575
+ const p = resolvePath(file, ctx.cwd);
576
+ try {
577
+ const content = ctx.volume.readFileSync(p, "utf8");
578
+ const result = doSed(content);
579
+ if (inPlace) {
580
+ ctx.volume.writeFileSync(p, result);
581
+ } else {
582
+ out += result;
583
+ }
584
+ } catch {
585
+ return fail(`sed: ${file}: No such file or directory\n`);
586
+ }
587
+ }
588
+ return ok(out);
589
+ };
590
+
591
+ /* ------------------------------------------------------------------ */
592
+ /* sort */
593
+ /* ------------------------------------------------------------------ */
594
+
595
+ const sort_cmd: BuiltinFn = (args, ctx, stdin) => {
596
+ const { flags, opts, positional } = parseArgs(
597
+ args,
598
+ ["r", "n", "u", "f", "h", "V", "b", "s"],
599
+ ["k", "t", "o"],
600
+ );
601
+ const reverse = flags.has("r");
602
+ const numeric = flags.has("n");
603
+ const unique = flags.has("u");
604
+ const ignoreCase = flags.has("f");
605
+ const humanNumeric = flags.has("h");
606
+ const versionSort = flags.has("V");
607
+ const stable = flags.has("s");
608
+ const keySpec = opts["k"];
609
+ const fieldSep = opts["t"];
610
+ const outputFile = opts["o"];
611
+
612
+ let content = stdin ?? "";
613
+ if (positional.length > 0) {
614
+ const p = resolvePath(positional[0], ctx.cwd);
615
+ try {
616
+ content = ctx.volume.readFileSync(p, "utf8");
617
+ } catch {
618
+ return fail(`sort: ${positional[0]}: No such file or directory\n`);
619
+ }
620
+ }
621
+
622
+ let lines = content.split("\n").filter(Boolean);
623
+
624
+ const getKey = (line: string): string => {
625
+ if (!keySpec) return line;
626
+ const sep = fieldSep || /\s+/;
627
+ const fields = line.split(sep);
628
+ const [startSpec, endSpec] = keySpec.split(",");
629
+ const startField = parseInt(startSpec) - 1;
630
+ const endField = endSpec ? parseInt(endSpec) - 1 : startField;
631
+ return fields
632
+ .slice(startField, endField + 1)
633
+ .join(typeof sep === "string" ? sep : " ");
634
+ };
635
+
636
+ const parseHumanSize = (s: string): number => {
637
+ const m = s.trim().match(/^([\d.]+)([KMGTPE]i?)?$/i);
638
+ if (!m) return 0;
639
+ const n = parseFloat(m[1]);
640
+ const u = (m[2] || "").toUpperCase().replace("I", "");
641
+ const mult: Record<string, number> = {
642
+ "": 1, K: 1024, M: 1048576, G: 1073741824, T: 1099511627776,
643
+ };
644
+ return n * (mult[u] || 1);
645
+ };
646
+
647
+ const compare = (a: string, b: string): number => {
648
+ let ka = getKey(a), kb = getKey(b);
649
+ if (ignoreCase) { ka = ka.toLowerCase(); kb = kb.toLowerCase(); }
650
+ if (numeric) return parseFloat(ka) - parseFloat(kb);
651
+ if (humanNumeric) return parseHumanSize(ka) - parseHumanSize(kb);
652
+ if (versionSort)
653
+ return ka.localeCompare(kb, undefined, { numeric: true, sensitivity: "base" });
654
+ return ka.localeCompare(kb);
655
+ };
656
+
657
+ if (stable) {
658
+ const indexed = lines.map((l, i) => ({ l, i }));
659
+ indexed.sort((a, b) => compare(a.l, b.l) || a.i - b.i);
660
+ lines = indexed.map((x) => x.l);
661
+ } else {
662
+ lines.sort(compare);
663
+ }
664
+ if (reverse) lines.reverse();
665
+ if (unique) {
666
+ const seen = new Set<string>();
667
+ lines = lines.filter((l) => {
668
+ const k = ignoreCase ? getKey(l).toLowerCase() : getKey(l);
669
+ if (seen.has(k)) return false;
670
+ seen.add(k);
671
+ return true;
672
+ });
673
+ }
674
+
675
+ const result = lines.join("\n") + (lines.length ? "\n" : "");
676
+ if (outputFile) {
677
+ const p = resolvePath(outputFile, ctx.cwd);
678
+ try {
679
+ ctx.volume.writeFileSync(p, result);
680
+ } catch {
681
+ /* */
682
+ }
683
+ }
684
+ return ok(result);
685
+ };
686
+
687
+ /* ------------------------------------------------------------------ */
688
+ /* uniq */
689
+ /* ------------------------------------------------------------------ */
690
+
691
+ const uniq_cmd: BuiltinFn = (args, ctx, stdin) => {
692
+ const { flags, opts, positional } = parseArgs(
693
+ args,
694
+ ["c", "d", "u", "i"],
695
+ ["f", "s", "w"],
696
+ );
697
+ const count = flags.has("c");
698
+ const dupsOnly = flags.has("d");
699
+ const uniqueOnly = flags.has("u");
700
+ const ignoreCase = flags.has("i");
701
+ const skipFields = parseInt(opts["f"] || "0");
702
+ const skipChars = parseInt(opts["s"] || "0");
703
+ const checkChars = opts["w"] ? parseInt(opts["w"]) : Infinity;
704
+
705
+ let content = stdin ?? "";
706
+ if (positional.length > 0) {
707
+ const p = resolvePath(positional[0], ctx.cwd);
708
+ try {
709
+ content = ctx.volume.readFileSync(p, "utf8");
710
+ } catch {
711
+ return fail(`uniq: ${positional[0]}: No such file or directory\n`);
712
+ }
713
+ }
714
+
715
+ const getKey = (line: string): string => {
716
+ let l = line;
717
+ if (skipFields > 0) {
718
+ const parts = l.split(/\s+/);
719
+ l = parts.slice(skipFields).join(" ");
720
+ }
721
+ if (skipChars > 0) l = l.slice(skipChars);
722
+ if (checkChars < Infinity) l = l.slice(0, checkChars);
723
+ if (ignoreCase) l = l.toLowerCase();
724
+ return l;
725
+ };
726
+
727
+ const lines = content.split("\n");
728
+ const result: string[] = [];
729
+ let prev = "";
730
+ let prevLine = "";
731
+ let prevCount = 0;
732
+
733
+ for (const line of lines) {
734
+ const key = getKey(line);
735
+ if (key === prev) {
736
+ prevCount++;
737
+ } else {
738
+ if (prevCount > 0) {
739
+ const show = dupsOnly ? prevCount > 1 : uniqueOnly ? prevCount === 1 : true;
740
+ if (show)
741
+ result.push(count ? `${String(prevCount).padStart(7)} ${prevLine}` : prevLine);
742
+ }
743
+ prev = key;
744
+ prevLine = line;
745
+ prevCount = 1;
746
+ }
747
+ }
748
+ if (prevCount > 0 && prevLine !== "") {
749
+ const show = dupsOnly ? prevCount > 1 : uniqueOnly ? prevCount === 1 : true;
750
+ if (show)
751
+ result.push(count ? `${String(prevCount).padStart(7)} ${prevLine}` : prevLine);
752
+ }
753
+
754
+ return ok(result.join("\n") + (result.length ? "\n" : ""));
755
+ };
756
+
757
+ /* ------------------------------------------------------------------ */
758
+ /* tr */
759
+ /* ------------------------------------------------------------------ */
760
+
761
+ function expandRange(s: string): string {
762
+ return s.replace(/(.)-(.)/g, (_, a: string, b: string) => {
763
+ let result = "";
764
+ const start = a.charCodeAt(0);
765
+ const end = b.charCodeAt(0);
766
+ for (let i = start; i <= end; i++) result += String.fromCharCode(i);
767
+ return result;
768
+ });
769
+ }
770
+
771
+ function squeezeDups(s: string, chars: Set<string>): string {
772
+ let out = "";
773
+ let prev = "";
774
+ for (const ch of s) {
775
+ if (ch === prev && chars.has(ch)) continue;
776
+ out += ch;
777
+ prev = ch;
778
+ }
779
+ return out;
780
+ }
781
+
782
+ function buildComplement(set: string): string {
783
+ const setChars = new Set(set);
784
+ let result = "";
785
+ for (let i = 0; i < 128; i++) {
786
+ const ch = String.fromCharCode(i);
787
+ if (!setChars.has(ch)) result += ch;
788
+ }
789
+ return result;
790
+ }
791
+
792
+ const tr_cmd: BuiltinFn = (args, _ctx, stdin) => {
793
+ const { flags, positional } = parseArgs(args, ["d", "s", "c", "C"]);
794
+ const deleteMode = flags.has("d");
795
+ const squeeze = flags.has("s");
796
+ const complement = flags.has("c") || flags.has("C");
797
+
798
+ if (positional.length === 0) return fail("tr: missing operand\n");
799
+ const content = stdin ?? "";
800
+
801
+ let set1 = expandCharClass(positional[0]);
802
+ const set2 = positional.length > 1 ? expandCharClass(positional[1]) : "";
803
+
804
+ set1 = expandRange(set1);
805
+ const expandedSet2 = set2 ? expandRange(set2) : "";
806
+
807
+ if (deleteMode) {
808
+ const chars = complement ? null : new Set(set1);
809
+ let out = "";
810
+ for (const ch of content) {
811
+ const inSet = chars ? chars.has(ch) : set1.includes(ch);
812
+ if (complement ? inSet : !inSet) out += ch;
813
+ }
814
+ if (squeeze && expandedSet2) {
815
+ const squeezeSet = new Set(expandedSet2);
816
+ out = squeezeDups(out, squeezeSet);
817
+ }
818
+ return ok(out);
819
+ }
820
+
821
+ if (positional.length < 2 && !squeeze) return fail("tr: missing operand\n");
822
+
823
+ if (squeeze && positional.length === 1) {
824
+ const squeezeSet = new Set(set1);
825
+ return ok(squeezeDups(content, squeezeSet));
826
+ }
827
+
828
+ let out = "";
829
+ const s1 = complement ? buildComplement(set1) : set1;
830
+ for (const ch of content) {
831
+ const idx = s1.indexOf(ch);
832
+ if (idx >= 0) {
833
+ const replacement =
834
+ idx < expandedSet2.length
835
+ ? expandedSet2[idx]
836
+ : expandedSet2[expandedSet2.length - 1] || ch;
837
+ out += replacement;
838
+ } else {
839
+ out += ch;
840
+ }
841
+ }
842
+
843
+ if (squeeze) {
844
+ const squeezeSet = new Set(expandedSet2);
845
+ out = squeezeDups(out, squeezeSet);
846
+ }
847
+ return ok(out);
848
+ };
849
+
850
+ /* ------------------------------------------------------------------ */
851
+ /* cut */
852
+ /* ------------------------------------------------------------------ */
853
+
854
+ function parseRangeSpec(spec: string): number[] {
855
+ const result: number[] = [];
856
+ for (const part of spec.split(",")) {
857
+ const range = part.match(/^(\d+)-(\d*)$/);
858
+ if (range) {
859
+ const start = parseInt(range[1]);
860
+ const end = range[2] ? parseInt(range[2]) : start + 100;
861
+ for (let i = start; i <= end; i++) result.push(i);
862
+ } else {
863
+ result.push(parseInt(part));
864
+ }
865
+ }
866
+ return result.filter((n) => !isNaN(n));
867
+ }
868
+
869
+ const cut_cmd: BuiltinFn = (args, ctx, stdin) => {
870
+ let delimiter = "\t";
871
+ let fields: number[] = [];
872
+ let bytes: number[] = [];
873
+ let chars: number[] = [];
874
+ let outputDelimiter: string | null = null;
875
+ const files: string[] = [];
876
+
877
+ for (let i = 0; i < args.length; i++) {
878
+ if (args[i] === "-d" && i + 1 < args.length) delimiter = args[++i];
879
+ else if (args[i] === "-f" && i + 1 < args.length)
880
+ fields = parseRangeSpec(args[++i]);
881
+ else if (args[i] === "-b" && i + 1 < args.length)
882
+ bytes = parseRangeSpec(args[++i]);
883
+ else if (args[i] === "-c" && i + 1 < args.length)
884
+ chars = parseRangeSpec(args[++i]);
885
+ else if (args[i] === "--output-delimiter" && i + 1 < args.length)
886
+ outputDelimiter = args[++i];
887
+ else if (!args[i].startsWith("-")) files.push(args[i]);
888
+ }
889
+
890
+ const outDelim = outputDelimiter ?? delimiter;
891
+
892
+ const doCut = (content: string) => {
893
+ return content
894
+ .split("\n")
895
+ .map((line) => {
896
+ if (bytes.length > 0 || chars.length > 0) {
897
+ const indices = bytes.length > 0 ? bytes : chars;
898
+ return indices.map((idx) => line[idx - 1] ?? "").join("");
899
+ }
900
+ const parts = line.split(delimiter);
901
+ return fields.map((f) => parts[f - 1] ?? "").join(outDelim);
902
+ })
903
+ .join("\n");
904
+ };
905
+
906
+ if (files.length === 0) return ok(doCut(stdin ?? ""));
907
+ let out = "";
908
+ for (const file of files) {
909
+ const p = resolvePath(file, ctx.cwd);
910
+ try {
911
+ out += doCut(ctx.volume.readFileSync(p, "utf8"));
912
+ } catch {
913
+ return fail(`cut: ${file}: No such file or directory\n`);
914
+ }
915
+ }
916
+ return ok(out);
917
+ };
918
+
919
+ /* ------------------------------------------------------------------ */
920
+ /* Small text utilities */
921
+ /* ------------------------------------------------------------------ */
922
+
923
+ const rev_cmd: BuiltinFn = (args, ctx, stdin) => {
924
+ let content = stdin ?? "";
925
+ if (args.length > 0) {
926
+ const p = resolvePath(args[0], ctx.cwd);
927
+ try {
928
+ content = ctx.volume.readFileSync(p, "utf8");
929
+ } catch {
930
+ return fail(`rev: ${args[0]}: No such file or directory\n`);
931
+ }
932
+ }
933
+ return ok(
934
+ content
935
+ .split("\n")
936
+ .map((l) => [...l].reverse().join(""))
937
+ .join("\n"),
938
+ );
939
+ };
940
+
941
+ const paste_cmd: BuiltinFn = (args, ctx, stdin) => {
942
+ const { opts, positional } = parseArgs(args, ["s"], ["d"]);
943
+ const delim = opts["d"] || "\t";
944
+
945
+ const contents: string[][] = [];
946
+ for (const file of positional) {
947
+ if (file === "-" && stdin) {
948
+ contents.push(stdin.split("\n"));
949
+ continue;
950
+ }
951
+ const p = resolvePath(file, ctx.cwd);
952
+ try {
953
+ contents.push(ctx.volume.readFileSync(p, "utf8").split("\n"));
954
+ } catch {
955
+ return fail(`paste: ${file}: No such file or directory\n`);
956
+ }
957
+ }
958
+ if (contents.length === 0 && stdin) contents.push(stdin.split("\n"));
959
+
960
+ const maxLen = Math.max(...contents.map((c) => c.length));
961
+ let out = "";
962
+ for (let i = 0; i < maxLen; i++) {
963
+ out += contents.map((c) => c[i] ?? "").join(delim) + "\n";
964
+ }
965
+ return ok(out);
966
+ };
967
+
968
+ const comm_cmd: BuiltinFn = (args, ctx) => {
969
+ const { flags, positional } = parseArgs(args, ["1", "2", "3"]);
970
+ if (positional.length < 2) return fail("comm: missing operand\n");
971
+
972
+ const readFile = (f: string) => {
973
+ const p = resolvePath(f, ctx.cwd);
974
+ return ctx.volume.readFileSync(p, "utf8").split("\n").filter(Boolean);
975
+ };
976
+
977
+ try {
978
+ const a = readFile(positional[0]);
979
+ const b = readFile(positional[1]);
980
+ let out = "";
981
+ let ai = 0, bi = 0;
982
+ while (ai < a.length || bi < b.length) {
983
+ if (ai >= a.length) {
984
+ if (!flags.has("2"))
985
+ out += "\t" + (flags.has("1") ? "" : "\t") + b[bi] + "\n";
986
+ bi++;
987
+ } else if (bi >= b.length) {
988
+ if (!flags.has("1")) out += a[ai] + "\n";
989
+ ai++;
990
+ } else if (a[ai] < b[bi]) {
991
+ if (!flags.has("1")) out += a[ai] + "\n";
992
+ ai++;
993
+ } else if (a[ai] > b[bi]) {
994
+ if (!flags.has("2"))
995
+ out += "\t" + (flags.has("1") ? "" : "\t") + b[bi] + "\n";
996
+ bi++;
997
+ } else {
998
+ if (!flags.has("3")) out += "\t\t" + a[ai] + "\n";
999
+ ai++;
1000
+ bi++;
1001
+ }
1002
+ }
1003
+ return ok(out);
1004
+ } catch (e) {
1005
+ return fail(`comm: ${e instanceof Error ? e.message : String(e)}\n`);
1006
+ }
1007
+ };
1008
+
1009
+ /* ------------------------------------------------------------------ */
1010
+ /* diff */
1011
+ /* ------------------------------------------------------------------ */
1012
+
1013
+ function simpleLCS(a: string[], b: string[]): string[] {
1014
+ const m = a.length, n = b.length;
1015
+ const dp: number[][] = Array.from({ length: m + 1 }, () =>
1016
+ new Array(n + 1).fill(0),
1017
+ );
1018
+ for (let i = 1; i <= m; i++) {
1019
+ for (let j = 1; j <= n; j++) {
1020
+ dp[i][j] =
1021
+ a[i - 1] === b[j - 1]
1022
+ ? dp[i - 1][j - 1] + 1
1023
+ : Math.max(dp[i - 1][j], dp[i][j - 1]);
1024
+ }
1025
+ }
1026
+ const result: string[] = [];
1027
+ let i = m, j = n;
1028
+ while (i > 0 && j > 0) {
1029
+ if (a[i - 1] === b[j - 1]) {
1030
+ result.unshift(a[i - 1]);
1031
+ i--;
1032
+ j--;
1033
+ } else if (dp[i - 1][j] > dp[i][j - 1]) i--;
1034
+ else j--;
1035
+ }
1036
+ return result;
1037
+ }
1038
+
1039
+ const diff_cmd: BuiltinFn = (args, ctx) => {
1040
+ const { flags, positional } = parseArgs(args, ["u", "q", "r", "N"]);
1041
+ if (positional.length < 2) return fail("diff: missing operand\n");
1042
+
1043
+ const brief = flags.has("q");
1044
+ const unified = flags.has("u");
1045
+
1046
+ const p1 = resolvePath(positional[0], ctx.cwd);
1047
+ const p2 = resolvePath(positional[1], ctx.cwd);
1048
+
1049
+ try {
1050
+ const a = ctx.volume.readFileSync(p1, "utf8").split("\n");
1051
+ const b = ctx.volume.readFileSync(p2, "utf8").split("\n");
1052
+
1053
+ if (a.join("\n") === b.join("\n")) return ok();
1054
+ if (brief)
1055
+ return {
1056
+ stdout: `Files ${positional[0]} and ${positional[1]} differ\n`,
1057
+ stderr: "",
1058
+ exitCode: 1,
1059
+ };
1060
+
1061
+ let out = "";
1062
+ if (unified) {
1063
+ out += `--- ${positional[0]}\n+++ ${positional[1]}\n`;
1064
+ out += `@@ -1,${a.length} +1,${b.length} @@\n`;
1065
+ const lcs = simpleLCS(a, b);
1066
+ let ai = 0, bi = 0, li = 0;
1067
+ while (ai < a.length || bi < b.length) {
1068
+ if (
1069
+ li < lcs.length &&
1070
+ ai < a.length && a[ai] === lcs[li] &&
1071
+ bi < b.length && b[bi] === lcs[li]
1072
+ ) {
1073
+ out += ` ${a[ai]}\n`;
1074
+ ai++; bi++; li++;
1075
+ } else if (ai < a.length && (li >= lcs.length || a[ai] !== lcs[li])) {
1076
+ out += `-${a[ai]}\n`;
1077
+ ai++;
1078
+ } else if (bi < b.length) {
1079
+ out += `+${b[bi]}\n`;
1080
+ bi++;
1081
+ }
1082
+ }
1083
+ } else {
1084
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
1085
+ if (i >= a.length) out += `> ${b[i]}\n`;
1086
+ else if (i >= b.length) out += `< ${a[i]}\n`;
1087
+ else if (a[i] !== b[i]) {
1088
+ out += `< ${a[i]}\n---\n> ${b[i]}\n`;
1089
+ }
1090
+ }
1091
+ }
1092
+
1093
+ return { stdout: out, stderr: "", exitCode: 1 };
1094
+ } catch (e) {
1095
+ return fail(`diff: ${e instanceof Error ? e.message : String(e)}\n`);
1096
+ }
1097
+ };
1098
+
1099
+ /* ------------------------------------------------------------------ */
1100
+ /* seq / yes */
1101
+ /* ------------------------------------------------------------------ */
1102
+
1103
+ const seq_cmd: BuiltinFn = (args) => {
1104
+ if (args.length === 0) return fail("seq: missing operand\n");
1105
+ let first = 1, increment = 1, last = 1;
1106
+ if (args.length === 1) {
1107
+ last = parseFloat(args[0]);
1108
+ } else if (args.length === 2) {
1109
+ first = parseFloat(args[0]);
1110
+ last = parseFloat(args[1]);
1111
+ } else {
1112
+ first = parseFloat(args[0]);
1113
+ increment = parseFloat(args[1]);
1114
+ last = parseFloat(args[2]);
1115
+ }
1116
+
1117
+ const lines: string[] = [];
1118
+ if (increment > 0) {
1119
+ for (let i = first; i <= last; i += increment) lines.push(String(i));
1120
+ } else if (increment < 0) {
1121
+ for (let i = first; i >= last; i += increment) lines.push(String(i));
1122
+ }
1123
+ return ok(lines.join("\n") + (lines.length ? "\n" : ""));
1124
+ };
1125
+
1126
+ const yes_cmd: BuiltinFn = (args) => {
1127
+ const text = args.length > 0 ? args.join(" ") : "y";
1128
+ return ok((text + "\n").repeat(YES_REPEAT_COUNT));
1129
+ };
1130
+
1131
+ /* ------------------------------------------------------------------ */
1132
+ /* Registry */
1133
+ /* ------------------------------------------------------------------ */
1134
+
1135
+ export const textProcessingCommands: [string, BuiltinFn][] = [
1136
+ ["echo", echo],
1137
+ ["printf", printf_cmd],
1138
+ ["grep", grep_cmd],
1139
+ ["egrep", grep_cmd],
1140
+ ["fgrep", grep_cmd],
1141
+ ["sed", sed_cmd],
1142
+ ["sort", sort_cmd],
1143
+ ["uniq", uniq_cmd],
1144
+ ["tr", tr_cmd],
1145
+ ["cut", cut_cmd],
1146
+ ["rev", rev_cmd],
1147
+ ["paste", paste_cmd],
1148
+ ["comm", comm_cmd],
1149
+ ["diff", diff_cmd],
1150
+ ["seq", seq_cmd],
1151
+ ["yes", yes_cmd],
1152
+ ];