@run0/jiki 0.1.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 (152) hide show
  1. package/dist/browser-bundle.d.ts +40 -0
  2. package/dist/builtins.d.ts +22 -0
  3. package/dist/code-transform.d.ts +7 -0
  4. package/dist/config/cdn.d.ts +13 -0
  5. package/dist/container.d.ts +101 -0
  6. package/dist/dev-server.d.ts +69 -0
  7. package/dist/errors.d.ts +19 -0
  8. package/dist/frameworks/code-transforms.d.ts +32 -0
  9. package/dist/frameworks/next-api-handler.d.ts +72 -0
  10. package/dist/frameworks/next-dev-server.d.ts +141 -0
  11. package/dist/frameworks/next-html-generator.d.ts +36 -0
  12. package/dist/frameworks/next-route-resolver.d.ts +19 -0
  13. package/dist/frameworks/next-shims.d.ts +78 -0
  14. package/dist/frameworks/remix-dev-server.d.ts +47 -0
  15. package/dist/frameworks/sveltekit-dev-server.d.ts +43 -0
  16. package/dist/frameworks/vite-dev-server.d.ts +50 -0
  17. package/dist/fs-errors.d.ts +36 -0
  18. package/dist/index.cjs +14916 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.ts +61 -0
  21. package/dist/index.mjs +14898 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/kernel.d.ts +48 -0
  24. package/dist/memfs.d.ts +144 -0
  25. package/dist/metrics.d.ts +78 -0
  26. package/dist/module-resolver.d.ts +60 -0
  27. package/dist/network-interceptor.d.ts +71 -0
  28. package/dist/npm/cache.d.ts +76 -0
  29. package/dist/npm/index.d.ts +60 -0
  30. package/dist/npm/lockfile-reader.d.ts +32 -0
  31. package/dist/npm/pnpm.d.ts +18 -0
  32. package/dist/npm/registry.d.ts +45 -0
  33. package/dist/npm/resolver.d.ts +39 -0
  34. package/dist/npm/sync-installer.d.ts +18 -0
  35. package/dist/npm/tarball.d.ts +4 -0
  36. package/dist/npm/workspaces.d.ts +46 -0
  37. package/dist/persistence.d.ts +94 -0
  38. package/dist/plugin.d.ts +156 -0
  39. package/dist/polyfills/assert.d.ts +30 -0
  40. package/dist/polyfills/child_process.d.ts +116 -0
  41. package/dist/polyfills/chokidar.d.ts +18 -0
  42. package/dist/polyfills/crypto.d.ts +49 -0
  43. package/dist/polyfills/events.d.ts +28 -0
  44. package/dist/polyfills/fs.d.ts +82 -0
  45. package/dist/polyfills/http.d.ts +147 -0
  46. package/dist/polyfills/module.d.ts +29 -0
  47. package/dist/polyfills/net.d.ts +53 -0
  48. package/dist/polyfills/os.d.ts +91 -0
  49. package/dist/polyfills/path.d.ts +96 -0
  50. package/dist/polyfills/perf_hooks.d.ts +21 -0
  51. package/dist/polyfills/process.d.ts +99 -0
  52. package/dist/polyfills/querystring.d.ts +15 -0
  53. package/dist/polyfills/readdirp.d.ts +18 -0
  54. package/dist/polyfills/readline.d.ts +32 -0
  55. package/dist/polyfills/stream.d.ts +106 -0
  56. package/dist/polyfills/stubs.d.ts +737 -0
  57. package/dist/polyfills/tty.d.ts +25 -0
  58. package/dist/polyfills/url.d.ts +41 -0
  59. package/dist/polyfills/util.d.ts +61 -0
  60. package/dist/polyfills/v8.d.ts +43 -0
  61. package/dist/polyfills/vm.d.ts +76 -0
  62. package/dist/polyfills/worker-threads.d.ts +77 -0
  63. package/dist/polyfills/ws.d.ts +32 -0
  64. package/dist/polyfills/zlib.d.ts +87 -0
  65. package/dist/runtime-helpers.d.ts +4 -0
  66. package/dist/runtime-interface.d.ts +39 -0
  67. package/dist/sandbox.d.ts +69 -0
  68. package/dist/server-bridge.d.ts +55 -0
  69. package/dist/shell-commands.d.ts +2 -0
  70. package/dist/shell.d.ts +101 -0
  71. package/dist/transpiler.d.ts +47 -0
  72. package/dist/type-checker.d.ts +57 -0
  73. package/dist/types/package-json.d.ts +17 -0
  74. package/dist/utils/binary-encoding.d.ts +4 -0
  75. package/dist/utils/hash.d.ts +6 -0
  76. package/dist/utils/safe-path.d.ts +6 -0
  77. package/dist/worker-runtime.d.ts +34 -0
  78. package/package.json +59 -0
  79. package/src/browser-bundle.ts +498 -0
  80. package/src/builtins.ts +222 -0
  81. package/src/code-transform.ts +183 -0
  82. package/src/config/cdn.ts +17 -0
  83. package/src/container.ts +343 -0
  84. package/src/dev-server.ts +322 -0
  85. package/src/errors.ts +604 -0
  86. package/src/frameworks/code-transforms.ts +667 -0
  87. package/src/frameworks/next-api-handler.ts +366 -0
  88. package/src/frameworks/next-dev-server.ts +1252 -0
  89. package/src/frameworks/next-html-generator.ts +585 -0
  90. package/src/frameworks/next-route-resolver.ts +521 -0
  91. package/src/frameworks/next-shims.ts +1084 -0
  92. package/src/frameworks/remix-dev-server.ts +163 -0
  93. package/src/frameworks/sveltekit-dev-server.ts +197 -0
  94. package/src/frameworks/vite-dev-server.ts +370 -0
  95. package/src/fs-errors.ts +118 -0
  96. package/src/index.ts +188 -0
  97. package/src/kernel.ts +381 -0
  98. package/src/memfs.ts +1006 -0
  99. package/src/metrics.ts +140 -0
  100. package/src/module-resolver.ts +511 -0
  101. package/src/network-interceptor.ts +143 -0
  102. package/src/npm/cache.ts +172 -0
  103. package/src/npm/index.ts +377 -0
  104. package/src/npm/lockfile-reader.ts +105 -0
  105. package/src/npm/pnpm.ts +108 -0
  106. package/src/npm/registry.ts +120 -0
  107. package/src/npm/resolver.ts +339 -0
  108. package/src/npm/sync-installer.ts +217 -0
  109. package/src/npm/tarball.ts +136 -0
  110. package/src/npm/workspaces.ts +255 -0
  111. package/src/persistence.ts +235 -0
  112. package/src/plugin.ts +293 -0
  113. package/src/polyfills/assert.ts +164 -0
  114. package/src/polyfills/child_process.ts +535 -0
  115. package/src/polyfills/chokidar.ts +52 -0
  116. package/src/polyfills/crypto.ts +433 -0
  117. package/src/polyfills/events.ts +178 -0
  118. package/src/polyfills/fs.ts +297 -0
  119. package/src/polyfills/http.ts +478 -0
  120. package/src/polyfills/module.ts +97 -0
  121. package/src/polyfills/net.ts +123 -0
  122. package/src/polyfills/os.ts +108 -0
  123. package/src/polyfills/path.ts +169 -0
  124. package/src/polyfills/perf_hooks.ts +30 -0
  125. package/src/polyfills/process.ts +349 -0
  126. package/src/polyfills/querystring.ts +66 -0
  127. package/src/polyfills/readdirp.ts +72 -0
  128. package/src/polyfills/readline.ts +80 -0
  129. package/src/polyfills/stream.ts +610 -0
  130. package/src/polyfills/stubs.ts +600 -0
  131. package/src/polyfills/tty.ts +43 -0
  132. package/src/polyfills/url.ts +97 -0
  133. package/src/polyfills/util.ts +173 -0
  134. package/src/polyfills/v8.ts +62 -0
  135. package/src/polyfills/vm.ts +111 -0
  136. package/src/polyfills/worker-threads.ts +189 -0
  137. package/src/polyfills/ws.ts +73 -0
  138. package/src/polyfills/zlib.ts +244 -0
  139. package/src/runtime-helpers.ts +83 -0
  140. package/src/runtime-interface.ts +46 -0
  141. package/src/sandbox.ts +178 -0
  142. package/src/server-bridge.ts +473 -0
  143. package/src/service-worker.ts +153 -0
  144. package/src/shell-commands.ts +708 -0
  145. package/src/shell.ts +795 -0
  146. package/src/transpiler.ts +282 -0
  147. package/src/type-checker.ts +241 -0
  148. package/src/types/package-json.ts +17 -0
  149. package/src/utils/binary-encoding.ts +38 -0
  150. package/src/utils/hash.ts +24 -0
  151. package/src/utils/safe-path.ts +38 -0
  152. package/src/worker-runtime.ts +42 -0
package/src/shell.ts ADDED
@@ -0,0 +1,795 @@
1
+ import { MemFS } from "./memfs";
2
+ import { Kernel } from "./kernel";
3
+ import { PackageManager } from "./npm/index";
4
+ import * as pathShim from "./polyfills/path";
5
+ import { EventEmitter } from "./polyfills/events";
6
+ import { createDefaultCommands } from "./shell-commands";
7
+
8
+ export interface ShellOptions {
9
+ cwd?: string;
10
+ env?: Record<string, string>;
11
+ onStdout?: (data: string) => void;
12
+ onStderr?: (data: string) => void;
13
+ onExit?: (code: number) => void;
14
+ pnpmPm?: PackageManager;
15
+ lazyPnpmPm?: () => PackageManager;
16
+ }
17
+
18
+ export interface ShellProcess {
19
+ stdin: { write(data: string): void };
20
+ stdout: EventEmitter;
21
+ stderr: EventEmitter;
22
+ kill(signal?: string): void;
23
+ wait(): Promise<number>;
24
+ pid: number;
25
+ }
26
+
27
+ export interface ShellResult {
28
+ stdout: string;
29
+ stderr: string;
30
+ exitCode: number;
31
+ }
32
+
33
+ export interface ShellContext {
34
+ vfs: MemFS;
35
+ runtime: Kernel;
36
+ pm: PackageManager;
37
+ pnpmPm?: PackageManager;
38
+ cwd: string;
39
+ env: Record<string, string>;
40
+ stdinData?: string;
41
+ setCwd(dir: string): void;
42
+ exec(command: string, options?: ShellOptions): Promise<ShellResult>;
43
+ write?(text: string): void;
44
+ writeErr?(text: string): void;
45
+ }
46
+
47
+ export type CommandHandler = (
48
+ args: string[],
49
+ ctx: ShellContext,
50
+ ) => ShellResult | Promise<ShellResult>;
51
+
52
+ const ok = (stdout = ""): ShellResult => ({ stdout, stderr: "", exitCode: 0 });
53
+ const fail = (stderr: string, code = 1): ShellResult => ({
54
+ stdout: "",
55
+ stderr,
56
+ exitCode: code,
57
+ });
58
+
59
+ /** Command history with up/down navigation. */
60
+ export class ShellHistory {
61
+ private entries: string[] = [];
62
+ private cursor = -1;
63
+ private maxSize: number;
64
+
65
+ constructor(maxSize = 1000) {
66
+ this.maxSize = maxSize;
67
+ }
68
+
69
+ push(command: string): void {
70
+ // Don't add duplicates of the last entry or empty commands.
71
+ if (!command.trim()) return;
72
+ if (
73
+ this.entries.length > 0 &&
74
+ this.entries[this.entries.length - 1] === command
75
+ )
76
+ return;
77
+ this.entries.push(command);
78
+ if (this.entries.length > this.maxSize) this.entries.shift();
79
+ this.cursor = this.entries.length; // reset cursor to end
80
+ }
81
+
82
+ up(): string | undefined {
83
+ if (this.cursor > 0) {
84
+ this.cursor--;
85
+ return this.entries[this.cursor];
86
+ }
87
+ return this.entries[0];
88
+ }
89
+
90
+ down(): string | undefined {
91
+ if (this.cursor < this.entries.length - 1) {
92
+ this.cursor++;
93
+ return this.entries[this.cursor];
94
+ }
95
+ this.cursor = this.entries.length;
96
+ return undefined; // past the end = empty input
97
+ }
98
+
99
+ reset(): void {
100
+ this.cursor = this.entries.length;
101
+ }
102
+
103
+ getAll(): string[] {
104
+ return [...this.entries];
105
+ }
106
+
107
+ get length(): number {
108
+ return this.entries.length;
109
+ }
110
+ }
111
+
112
+ export class Shell {
113
+ private vfs: MemFS;
114
+ private runtime: Kernel;
115
+ private packageManager: PackageManager;
116
+ private pnpmPm?: PackageManager;
117
+ private lazyPnpmPm?: () => PackageManager;
118
+ private binCache = new Map<string, string>();
119
+ private cwd: string;
120
+ private env: Record<string, string>;
121
+ private commands: Map<string, CommandHandler>;
122
+ /** Command history for this shell session. */
123
+ readonly history = new ShellHistory();
124
+ /** Background jobs spawned with `&`. */
125
+ private backgroundJobs: Array<{
126
+ id: number;
127
+ command: string;
128
+ promise: Promise<ShellResult>;
129
+ done: boolean;
130
+ result?: ShellResult;
131
+ }> = [];
132
+ private nextJobId = 1;
133
+
134
+ constructor(
135
+ vfs: MemFS,
136
+ runtime: Kernel,
137
+ packageManager: PackageManager,
138
+ options: ShellOptions = {},
139
+ ) {
140
+ this.vfs = vfs;
141
+ this.runtime = runtime;
142
+ this.packageManager = packageManager;
143
+ this.pnpmPm = options.pnpmPm;
144
+ this.lazyPnpmPm = options.lazyPnpmPm;
145
+ this.cwd = options.cwd || "/";
146
+ this.env = {
147
+ PATH: "/usr/local/bin:/usr/bin:/bin:/node_modules/.bin",
148
+ HOME: "/",
149
+ ...options.env,
150
+ };
151
+ this.commands = createDefaultCommands();
152
+ // Register built-in history/jobs/fg commands.
153
+ this.commands.set("history", (_args, ctx) => {
154
+ const lines = this.history
155
+ .getAll()
156
+ .map((cmd, i) => ` ${i + 1} ${cmd}`)
157
+ .join("\n");
158
+ return ok(lines ? lines + "\n" : "");
159
+ });
160
+ this.commands.set("jobs", (_args, ctx) => {
161
+ const lines = this.backgroundJobs
162
+ .filter(j => !j.done)
163
+ .map(j => `[${j.id}] Running ${j.command}`)
164
+ .join("\n");
165
+ return ok(lines ? lines + "\n" : "");
166
+ });
167
+ this.commands.set("fg", args => {
168
+ const id = args[0]
169
+ ? parseInt(args[0], 10)
170
+ : this.backgroundJobs.length > 0
171
+ ? this.backgroundJobs[this.backgroundJobs.length - 1].id
172
+ : 0;
173
+ const job = this.backgroundJobs.find(j => j.id === id);
174
+ if (!job) return fail(`fg: no such job: ${id}\n`);
175
+ if (job.done && job.result) return job.result;
176
+ return ok(`[${id}] ${job.command}\n`);
177
+ });
178
+ }
179
+
180
+ registerCommand(name: string, handler: CommandHandler): void {
181
+ this.commands.set(name, handler);
182
+ }
183
+
184
+ /**
185
+ * Tab completion for a partial input string.
186
+ * Returns sorted candidate completions.
187
+ */
188
+ complete(partial: string): string[] {
189
+ const trimmed = partial.trimStart();
190
+ const parts = trimmed.split(/\s+/);
191
+
192
+ if (parts.length <= 1) {
193
+ // Completing a command name
194
+ const prefix = parts[0] || "";
195
+ const candidates: string[] = [];
196
+ // Built-in / registered commands
197
+ for (const name of this.commands.keys()) {
198
+ if (name.startsWith(prefix)) candidates.push(name);
199
+ }
200
+ // Binaries in .bin
201
+ try {
202
+ const binDir = pathShim.join("/node_modules/.bin");
203
+ if (this.vfs.existsSync(binDir)) {
204
+ for (const name of this.vfs.readdirSync(binDir)) {
205
+ if (name.startsWith(prefix) && !candidates.includes(name))
206
+ candidates.push(name);
207
+ }
208
+ }
209
+ } catch {}
210
+ return candidates.sort();
211
+ }
212
+
213
+ // Completing a file path argument
214
+ const lastToken = parts[parts.length - 1];
215
+ return this.completeFilePath(lastToken);
216
+ }
217
+
218
+ private completeFilePath(partial: string): string[] {
219
+ const abs = partial.startsWith("/")
220
+ ? partial
221
+ : pathShim.resolve(this.cwd, partial);
222
+
223
+ // Try to list the directory the partial is in.
224
+ const dir = abs.endsWith("/") ? abs : pathShim.dirname(abs);
225
+ const prefix = abs.endsWith("/") ? "" : pathShim.basename(abs);
226
+
227
+ try {
228
+ const entries = this.vfs.readdirSync(dir);
229
+ return entries
230
+ .filter(e => e.startsWith(prefix))
231
+ .map(e => {
232
+ const full = pathShim.join(dir, e);
233
+ // Append / for directories
234
+ try {
235
+ if (this.vfs.statSync(full).isDirectory()) return e + "/";
236
+ } catch {}
237
+ return e;
238
+ })
239
+ .sort();
240
+ } catch {
241
+ return [];
242
+ }
243
+ }
244
+
245
+ async exec(
246
+ command: string,
247
+ options: ShellOptions = {},
248
+ ): Promise<ShellResult> {
249
+ this.history.push(command);
250
+
251
+ // Background job support: if command ends with `&`, run in background.
252
+ const trimmedCmd = command.trim();
253
+ if (trimmedCmd.endsWith("&") && !trimmedCmd.endsWith("&&")) {
254
+ const fgCmd = trimmedCmd.slice(0, -1).trim();
255
+ const jobId = this.nextJobId++;
256
+ const job = {
257
+ id: jobId,
258
+ command: fgCmd,
259
+ promise: this.exec(fgCmd, options).then(r => {
260
+ job.done = true;
261
+ job.result = r;
262
+ return r;
263
+ }),
264
+ done: false,
265
+ result: undefined as ShellResult | undefined,
266
+ };
267
+ this.backgroundJobs.push(job);
268
+ return ok(`[${jobId}] started\n`);
269
+ }
270
+
271
+ const segments = this.splitControlFlow(command);
272
+ let stdout = "";
273
+ let stderr = "";
274
+ let exitCode = 0;
275
+
276
+ let skip: "&&" | "||" | null = null;
277
+
278
+ for (const { cmd, op } of segments) {
279
+ if (!cmd) continue;
280
+
281
+ // Skip logic: propagate skip through matching operator chains
282
+ // e.g., `false && B && C` skips both B and C
283
+ // e.g., `true || B || C` skips both B and C
284
+ if (skip) {
285
+ // A ';' always breaks the skip chain (unconditional separator)
286
+ // A different operator type also breaks the chain:
287
+ // `false && B || C` — skip B (&&), but C runs (|| sees prior failure)
288
+ // `true || B && C` — skip B (||), but C runs (&& sees prior success)
289
+ if (op === ";") {
290
+ skip = null;
291
+ continue;
292
+ }
293
+ if (skip === "&&" && op === "||") {
294
+ skip = null;
295
+ continue;
296
+ }
297
+ if (skip === "||" && op === "&&") {
298
+ skip = null;
299
+ continue;
300
+ }
301
+ // Same operator type: keep skipping (propagate through chain)
302
+ continue;
303
+ }
304
+
305
+ const result = await this.execPipeline(cmd, options);
306
+ stdout += result.stdout;
307
+ stderr += result.stderr;
308
+ exitCode = result.exitCode;
309
+
310
+ // Set skip for the NEXT segment based on operator + exit code
311
+ if (op === "&&" && exitCode !== 0) {
312
+ skip = "&&";
313
+ } else if (op === "||" && exitCode === 0) {
314
+ skip = "||";
315
+ }
316
+ // ';' always continues, skip stays null
317
+ }
318
+ return { stdout, stderr, exitCode };
319
+ }
320
+
321
+ private async execPipeline(
322
+ command: string,
323
+ options: ShellOptions = {},
324
+ ): Promise<ShellResult> {
325
+ let stderr = "";
326
+ const onStderr = (data: string) => {
327
+ stderr += data;
328
+ options.onStderr?.(data);
329
+ };
330
+
331
+ const pipeSegments = command.split("|").map(s => s.trim());
332
+ let lastOutput = "";
333
+
334
+ for (let i = 0; i < pipeSegments.length; i++) {
335
+ const isLast = i === pipeSegments.length - 1;
336
+ // Only forward stdout to the caller for the last segment in the pipeline;
337
+ // intermediate segments' stdout is captured as stdin for the next segment.
338
+ let segStdout = "";
339
+ const onStdout = (data: string) => {
340
+ segStdout += data;
341
+ if (isLast) options.onStdout?.(data);
342
+ };
343
+ const result = await this.execSingle(
344
+ pipeSegments[i],
345
+ { ...options, onStdout, onStderr },
346
+ lastOutput,
347
+ );
348
+ lastOutput = result.stdout;
349
+ if (result.exitCode !== 0)
350
+ return {
351
+ stdout: isLast ? segStdout : "",
352
+ stderr,
353
+ exitCode: result.exitCode,
354
+ };
355
+ }
356
+
357
+ return { stdout: lastOutput, stderr, exitCode: 0 };
358
+ }
359
+
360
+ private splitControlFlow(
361
+ command: string,
362
+ ): Array<{ cmd: string; op: "&&" | "||" | ";" | null }> {
363
+ const segments: Array<{ cmd: string; op: "&&" | "||" | ";" | null }> = [];
364
+ let current = "";
365
+ let inQuote = "";
366
+
367
+ for (let i = 0; i < command.length; i++) {
368
+ const ch = command[i];
369
+ if (inQuote) {
370
+ current += ch;
371
+ if (ch === inQuote) inQuote = "";
372
+ continue;
373
+ }
374
+ if (ch === '"' || ch === "'") {
375
+ inQuote = ch;
376
+ current += ch;
377
+ continue;
378
+ }
379
+ if (ch === "&" && command[i + 1] === "&") {
380
+ segments.push({ cmd: current.trim(), op: "&&" });
381
+ current = "";
382
+ i++; // skip second &
383
+ continue;
384
+ }
385
+ if (ch === "|" && command[i + 1] === "|") {
386
+ segments.push({ cmd: current.trim(), op: "||" });
387
+ current = "";
388
+ i++; // skip second |
389
+ continue;
390
+ }
391
+ if (ch === ";") {
392
+ segments.push({ cmd: current.trim(), op: ";" });
393
+ current = "";
394
+ continue;
395
+ }
396
+ current += ch;
397
+ }
398
+ if (current.trim()) segments.push({ cmd: current.trim(), op: null });
399
+ return segments;
400
+ }
401
+
402
+ private parseRedirects(tokens: string[]): {
403
+ args: string[];
404
+ stdoutFile?: string;
405
+ append?: boolean;
406
+ } {
407
+ const args: string[] = [];
408
+ let stdoutFile: string | undefined;
409
+ let append = false;
410
+
411
+ for (let i = 0; i < tokens.length; i++) {
412
+ if (tokens[i] === ">>" && tokens[i + 1]) {
413
+ stdoutFile = tokens[++i];
414
+ append = true;
415
+ } else if (tokens[i] === ">" && tokens[i + 1]) {
416
+ stdoutFile = tokens[++i];
417
+ append = false;
418
+ } else if (tokens[i].endsWith(">>")) {
419
+ stdoutFile = tokens[i].slice(0, -2) || tokens[++i];
420
+ append = true;
421
+ } else {
422
+ args.push(tokens[i]);
423
+ }
424
+ }
425
+ return { args, stdoutFile, append };
426
+ }
427
+
428
+ private async execSingle(
429
+ command: string,
430
+ options: ShellOptions & {
431
+ onStdout?: (data: string) => void;
432
+ onStderr?: (data: string) => void;
433
+ },
434
+ stdinData?: string,
435
+ ): Promise<ShellResult> {
436
+ const parts = this.parseCommand(command);
437
+ if (parts.length === 0) return ok();
438
+
439
+ const redirect = this.parseRedirects(parts);
440
+ const [cmd, ...args] = redirect.args;
441
+
442
+ const resolvePnpmPm = () => this.pnpmPm ?? this.lazyPnpmPm?.();
443
+ const ctx: ShellContext = {
444
+ vfs: this.vfs,
445
+ runtime: this.runtime,
446
+ pm: this.packageManager,
447
+ get pnpmPm() {
448
+ return resolvePnpmPm();
449
+ },
450
+ cwd: this.cwd,
451
+ env: this.env,
452
+ stdinData,
453
+ setCwd: (dir: string) => {
454
+ this.cwd = dir;
455
+ },
456
+ exec: (cmd: string, opts?: ShellOptions) => this.exec(cmd, opts),
457
+ write: (text: string) => options.onStdout?.(text),
458
+ writeErr: (text: string) => options.onStderr?.(text),
459
+ };
460
+
461
+ let result: ShellResult;
462
+
463
+ const handler = this.commands.get(cmd);
464
+ if (handler) {
465
+ try {
466
+ result = await handler(args, ctx);
467
+ if (result.stderr) options.onStderr?.(result.stderr);
468
+ } catch (e) {
469
+ const errMsg = `${cmd}: ${(e as Error).message}\n`;
470
+ options.onStderr?.(errMsg);
471
+ return fail(errMsg);
472
+ }
473
+ } else {
474
+ const binPath = this.resolveBin(cmd);
475
+ if (binPath) {
476
+ try {
477
+ this.runtime.runFile(binPath);
478
+ result = ok();
479
+ } catch (e) {
480
+ const errMsg = `${cmd}: ${(e as Error).message}\n`;
481
+ options.onStderr?.(errMsg);
482
+ return fail(errMsg);
483
+ }
484
+ } else {
485
+ const errMsg = `${cmd}: command not found\n`;
486
+ options.onStderr?.(errMsg);
487
+ return { stdout: "", stderr: errMsg, exitCode: 127 };
488
+ }
489
+ }
490
+
491
+ // Handle stdout redirects
492
+ if (redirect.stdoutFile) {
493
+ const filePath = pathShim.isAbsolute(redirect.stdoutFile)
494
+ ? redirect.stdoutFile
495
+ : pathShim.resolve(this.cwd, redirect.stdoutFile);
496
+ try {
497
+ if (redirect.append) {
498
+ const existing = this.vfs.existsSync(filePath)
499
+ ? (this.vfs.readFileSync(filePath, "utf-8") as string)
500
+ : "";
501
+ this.vfs.writeFileSync(filePath, existing + result.stdout);
502
+ } else {
503
+ this.vfs.writeFileSync(filePath, result.stdout);
504
+ }
505
+ result = { ...result, stdout: "" };
506
+ } catch (e) {
507
+ const errMsg = `${(e as Error).message}\n`;
508
+ options.onStderr?.(errMsg);
509
+ return { stdout: "", stderr: errMsg, exitCode: 1 };
510
+ }
511
+ } else {
512
+ if (result.stdout) options.onStdout?.(result.stdout);
513
+ }
514
+
515
+ return result;
516
+ }
517
+
518
+ private expandVar(name: string): string {
519
+ return this.env[name] || "";
520
+ }
521
+
522
+ private parseCommand(cmd: string): string[] {
523
+ const tokens: string[] = [];
524
+ const quoted: boolean[] = []; // track whether each token was quoted
525
+ let current = "";
526
+ let wasQuoted = false;
527
+
528
+ for (let i = 0; i < cmd.length; i++) {
529
+ const ch = cmd[i];
530
+ if (wasQuoted === false && ch === "'") {
531
+ // Single quotes: no variable expansion
532
+ wasQuoted = true;
533
+ i++;
534
+ while (i < cmd.length && cmd[i] !== "'") {
535
+ current += cmd[i];
536
+ i++;
537
+ }
538
+ // i now points at closing quote (or end of string)
539
+ continue;
540
+ } else if (wasQuoted === false && ch === '"') {
541
+ // Double quotes: expand variables inside
542
+ wasQuoted = true;
543
+ i++;
544
+ while (i < cmd.length && cmd[i] !== '"') {
545
+ if (cmd[i] === "$") {
546
+ const varStr = this.consumeVariable(cmd, i);
547
+ current += varStr.value;
548
+ i = varStr.end;
549
+ } else {
550
+ current += cmd[i];
551
+ i++;
552
+ }
553
+ }
554
+ // i now points at closing quote (or end of string)
555
+ continue;
556
+ } else if (ch === " " || ch === "\t") {
557
+ if (current) {
558
+ tokens.push(current);
559
+ quoted.push(wasQuoted);
560
+ current = "";
561
+ wasQuoted = false;
562
+ }
563
+ } else if (ch === "\\" && i + 1 < cmd.length) {
564
+ current += cmd[++i];
565
+ } else if (ch === "$") {
566
+ // Unquoted variable expansion
567
+ const varStr = this.consumeVariable(cmd, i);
568
+ current += varStr.value;
569
+ i = varStr.end - 1; // -1 because for loop will i++
570
+ } else {
571
+ current += ch;
572
+ }
573
+ }
574
+ if (current) {
575
+ tokens.push(current);
576
+ quoted.push(wasQuoted);
577
+ }
578
+
579
+ // Expand globs on unquoted tokens
580
+ const result: string[] = [];
581
+ for (let i = 0; i < tokens.length; i++) {
582
+ if (
583
+ !quoted[i] &&
584
+ (tokens[i].includes("*") ||
585
+ tokens[i].includes("?") ||
586
+ tokens[i].includes("{"))
587
+ ) {
588
+ result.push(...this.expandGlob(tokens[i], this.cwd));
589
+ } else {
590
+ result.push(tokens[i]);
591
+ }
592
+ }
593
+ return result;
594
+ }
595
+
596
+ /** Consume a $VAR or ${VAR} starting at position i (pointing at '$'), returning expanded value and end index */
597
+ private consumeVariable(
598
+ cmd: string,
599
+ i: number,
600
+ ): { value: string; end: number } {
601
+ i++; // skip '$'
602
+ if (i < cmd.length && cmd[i] === "{") {
603
+ // ${VAR} syntax
604
+ i++; // skip '{'
605
+ let name = "";
606
+ while (i < cmd.length && cmd[i] !== "}") {
607
+ name += cmd[i];
608
+ i++;
609
+ }
610
+ if (i < cmd.length) i++; // skip '}'
611
+ return { value: this.expandVar(name), end: i };
612
+ } else {
613
+ // $VAR syntax — consume word characters
614
+ let name = "";
615
+ while (i < cmd.length && /\w/.test(cmd[i])) {
616
+ name += cmd[i];
617
+ i++;
618
+ }
619
+ if (!name) return { value: "$", end: i };
620
+ return { value: this.expandVar(name), end: i };
621
+ }
622
+ }
623
+
624
+ private expandGlob(pattern: string, cwd: string): string[] {
625
+ if (
626
+ !pattern.includes("*") &&
627
+ !pattern.includes("?") &&
628
+ !pattern.includes("{")
629
+ )
630
+ return [pattern];
631
+
632
+ // Brace expansion: {a,b} → expand into multiple patterns
633
+ if (pattern.includes("{")) {
634
+ const expanded = this.expandBraces(pattern);
635
+ const results: string[] = [];
636
+ for (const p of expanded) {
637
+ results.push(...this.expandGlob(p, cwd));
638
+ }
639
+ return results.length > 0 ? [...new Set(results)].sort() : [pattern];
640
+ }
641
+
642
+ // Recursive glob: ** support
643
+ if (pattern.includes("**")) {
644
+ return this.expandRecursiveGlob(pattern, cwd);
645
+ }
646
+
647
+ // Simple single-level glob
648
+ const dir = pattern.includes("/")
649
+ ? pattern.substring(0, pattern.lastIndexOf("/")) || "/"
650
+ : cwd;
651
+ const filePattern = pattern.includes("/")
652
+ ? pattern.substring(pattern.lastIndexOf("/") + 1)
653
+ : pattern;
654
+
655
+ const regexStr = filePattern
656
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
657
+ .replace(/\*/g, ".*")
658
+ .replace(/\?/g, ".");
659
+ const regex = new RegExp("^" + regexStr + "$");
660
+ try {
661
+ const entries = this.vfs.readdirSync(dir);
662
+ const matches = entries
663
+ .filter(e => regex.test(e))
664
+ .map(e => (dir === cwd ? e : `${dir}/${e}`));
665
+ return matches.length > 0 ? matches.sort() : [pattern];
666
+ } catch {
667
+ return [pattern];
668
+ }
669
+ }
670
+
671
+ /** Expand brace patterns like `{a,b}` into multiple strings. */
672
+ private expandBraces(pattern: string): string[] {
673
+ const start = pattern.indexOf("{");
674
+ if (start === -1) return [pattern];
675
+ const end = pattern.indexOf("}", start);
676
+ if (end === -1) return [pattern];
677
+
678
+ const prefix = pattern.slice(0, start);
679
+ const suffix = pattern.slice(end + 1);
680
+ const parts = pattern.slice(start + 1, end).split(",");
681
+
682
+ const results: string[] = [];
683
+ for (const part of parts) {
684
+ results.push(...this.expandBraces(prefix + part + suffix));
685
+ }
686
+ return results;
687
+ }
688
+
689
+ /** Expand `**` recursive glob patterns. */
690
+ private expandRecursiveGlob(pattern: string, cwd: string): string[] {
691
+ const isAbsolute = pattern.startsWith("/");
692
+ const base = isAbsolute ? "/" : cwd;
693
+
694
+ // Split pattern into segments
695
+ const patternSegs = pattern.split("/").filter(Boolean);
696
+
697
+ const results: string[] = [];
698
+ this.matchGlobSegments(base, patternSegs, 0, results);
699
+ return results.length > 0 ? results.sort() : [pattern];
700
+ }
701
+
702
+ private matchGlobSegments(
703
+ dir: string,
704
+ segments: string[],
705
+ segIdx: number,
706
+ results: string[],
707
+ ): void {
708
+ if (segIdx >= segments.length) return;
709
+
710
+ const seg = segments[segIdx];
711
+ const isLast = segIdx === segments.length - 1;
712
+
713
+ if (seg === "**") {
714
+ // ** matches zero or more directories
715
+ // Match zero directories: skip this segment
716
+ this.matchGlobSegments(dir, segments, segIdx + 1, results);
717
+
718
+ // Match one or more directories: recurse into each subdirectory
719
+ try {
720
+ const entries = this.vfs.readdirSync(dir);
721
+ for (const entry of entries) {
722
+ const full = dir === "/" ? `/${entry}` : `${dir}/${entry}`;
723
+ try {
724
+ if (this.vfs.statSync(full).isDirectory()) {
725
+ this.matchGlobSegments(full, segments, segIdx, results); // keep ** active
726
+ this.matchGlobSegments(full, segments, segIdx + 1, results); // move past **
727
+ }
728
+ } catch {}
729
+ }
730
+ } catch {}
731
+ return;
732
+ }
733
+
734
+ // Normal segment (may contain * or ?)
735
+ const regexStr = seg
736
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
737
+ .replace(/\*/g, ".*")
738
+ .replace(/\?/g, ".");
739
+ const regex = new RegExp("^" + regexStr + "$");
740
+
741
+ try {
742
+ const entries = this.vfs.readdirSync(dir);
743
+ for (const entry of entries) {
744
+ if (!regex.test(entry)) continue;
745
+ const full = dir === "/" ? `/${entry}` : `${dir}/${entry}`;
746
+ if (isLast) {
747
+ results.push(full);
748
+ } else {
749
+ try {
750
+ if (this.vfs.statSync(full).isDirectory()) {
751
+ this.matchGlobSegments(full, segments, segIdx + 1, results);
752
+ }
753
+ } catch {}
754
+ }
755
+ }
756
+ } catch {}
757
+ }
758
+
759
+ private resolveBin(cmd: string): string | null {
760
+ const cached = this.binCache.get(cmd);
761
+ if (cached !== undefined) return cached;
762
+ const binPath = pathShim.join("/node_modules/.bin", cmd);
763
+ if (this.vfs.existsSync(binPath)) {
764
+ this.binCache.set(cmd, binPath);
765
+ return binPath;
766
+ }
767
+ return null;
768
+ }
769
+
770
+ clearBinCache(): void {
771
+ this.binCache.clear();
772
+ }
773
+
774
+ getCwd(): string {
775
+ return this.cwd;
776
+ }
777
+ setCwd(dir: string): void {
778
+ this.cwd = dir;
779
+ }
780
+ getEnv(): Record<string, string> {
781
+ return { ...this.env };
782
+ }
783
+ setEnv(key: string, value: string): void {
784
+ this.env[key] = value;
785
+ }
786
+ }
787
+
788
+ export function createShell(
789
+ vfs: MemFS,
790
+ runtime: Kernel,
791
+ pm: PackageManager,
792
+ options?: ShellOptions,
793
+ ): Shell {
794
+ return new Shell(vfs, runtime, pm, options);
795
+ }