@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.
- package/dist/browser-bundle.d.ts +40 -0
- package/dist/builtins.d.ts +22 -0
- package/dist/code-transform.d.ts +7 -0
- package/dist/config/cdn.d.ts +13 -0
- package/dist/container.d.ts +101 -0
- package/dist/dev-server.d.ts +69 -0
- package/dist/errors.d.ts +19 -0
- package/dist/frameworks/code-transforms.d.ts +32 -0
- package/dist/frameworks/next-api-handler.d.ts +72 -0
- package/dist/frameworks/next-dev-server.d.ts +141 -0
- package/dist/frameworks/next-html-generator.d.ts +36 -0
- package/dist/frameworks/next-route-resolver.d.ts +19 -0
- package/dist/frameworks/next-shims.d.ts +78 -0
- package/dist/frameworks/remix-dev-server.d.ts +47 -0
- package/dist/frameworks/sveltekit-dev-server.d.ts +43 -0
- package/dist/frameworks/vite-dev-server.d.ts +50 -0
- package/dist/fs-errors.d.ts +36 -0
- package/dist/index.cjs +14916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.mjs +14898 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kernel.d.ts +48 -0
- package/dist/memfs.d.ts +144 -0
- package/dist/metrics.d.ts +78 -0
- package/dist/module-resolver.d.ts +60 -0
- package/dist/network-interceptor.d.ts +71 -0
- package/dist/npm/cache.d.ts +76 -0
- package/dist/npm/index.d.ts +60 -0
- package/dist/npm/lockfile-reader.d.ts +32 -0
- package/dist/npm/pnpm.d.ts +18 -0
- package/dist/npm/registry.d.ts +45 -0
- package/dist/npm/resolver.d.ts +39 -0
- package/dist/npm/sync-installer.d.ts +18 -0
- package/dist/npm/tarball.d.ts +4 -0
- package/dist/npm/workspaces.d.ts +46 -0
- package/dist/persistence.d.ts +94 -0
- package/dist/plugin.d.ts +156 -0
- package/dist/polyfills/assert.d.ts +30 -0
- package/dist/polyfills/child_process.d.ts +116 -0
- package/dist/polyfills/chokidar.d.ts +18 -0
- package/dist/polyfills/crypto.d.ts +49 -0
- package/dist/polyfills/events.d.ts +28 -0
- package/dist/polyfills/fs.d.ts +82 -0
- package/dist/polyfills/http.d.ts +147 -0
- package/dist/polyfills/module.d.ts +29 -0
- package/dist/polyfills/net.d.ts +53 -0
- package/dist/polyfills/os.d.ts +91 -0
- package/dist/polyfills/path.d.ts +96 -0
- package/dist/polyfills/perf_hooks.d.ts +21 -0
- package/dist/polyfills/process.d.ts +99 -0
- package/dist/polyfills/querystring.d.ts +15 -0
- package/dist/polyfills/readdirp.d.ts +18 -0
- package/dist/polyfills/readline.d.ts +32 -0
- package/dist/polyfills/stream.d.ts +106 -0
- package/dist/polyfills/stubs.d.ts +737 -0
- package/dist/polyfills/tty.d.ts +25 -0
- package/dist/polyfills/url.d.ts +41 -0
- package/dist/polyfills/util.d.ts +61 -0
- package/dist/polyfills/v8.d.ts +43 -0
- package/dist/polyfills/vm.d.ts +76 -0
- package/dist/polyfills/worker-threads.d.ts +77 -0
- package/dist/polyfills/ws.d.ts +32 -0
- package/dist/polyfills/zlib.d.ts +87 -0
- package/dist/runtime-helpers.d.ts +4 -0
- package/dist/runtime-interface.d.ts +39 -0
- package/dist/sandbox.d.ts +69 -0
- package/dist/server-bridge.d.ts +55 -0
- package/dist/shell-commands.d.ts +2 -0
- package/dist/shell.d.ts +101 -0
- package/dist/transpiler.d.ts +47 -0
- package/dist/type-checker.d.ts +57 -0
- package/dist/types/package-json.d.ts +17 -0
- package/dist/utils/binary-encoding.d.ts +4 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/safe-path.d.ts +6 -0
- package/dist/worker-runtime.d.ts +34 -0
- package/package.json +59 -0
- package/src/browser-bundle.ts +498 -0
- package/src/builtins.ts +222 -0
- package/src/code-transform.ts +183 -0
- package/src/config/cdn.ts +17 -0
- package/src/container.ts +343 -0
- package/src/dev-server.ts +322 -0
- package/src/errors.ts +604 -0
- package/src/frameworks/code-transforms.ts +667 -0
- package/src/frameworks/next-api-handler.ts +366 -0
- package/src/frameworks/next-dev-server.ts +1252 -0
- package/src/frameworks/next-html-generator.ts +585 -0
- package/src/frameworks/next-route-resolver.ts +521 -0
- package/src/frameworks/next-shims.ts +1084 -0
- package/src/frameworks/remix-dev-server.ts +163 -0
- package/src/frameworks/sveltekit-dev-server.ts +197 -0
- package/src/frameworks/vite-dev-server.ts +370 -0
- package/src/fs-errors.ts +118 -0
- package/src/index.ts +188 -0
- package/src/kernel.ts +381 -0
- package/src/memfs.ts +1006 -0
- package/src/metrics.ts +140 -0
- package/src/module-resolver.ts +511 -0
- package/src/network-interceptor.ts +143 -0
- package/src/npm/cache.ts +172 -0
- package/src/npm/index.ts +377 -0
- package/src/npm/lockfile-reader.ts +105 -0
- package/src/npm/pnpm.ts +108 -0
- package/src/npm/registry.ts +120 -0
- package/src/npm/resolver.ts +339 -0
- package/src/npm/sync-installer.ts +217 -0
- package/src/npm/tarball.ts +136 -0
- package/src/npm/workspaces.ts +255 -0
- package/src/persistence.ts +235 -0
- package/src/plugin.ts +293 -0
- package/src/polyfills/assert.ts +164 -0
- package/src/polyfills/child_process.ts +535 -0
- package/src/polyfills/chokidar.ts +52 -0
- package/src/polyfills/crypto.ts +433 -0
- package/src/polyfills/events.ts +178 -0
- package/src/polyfills/fs.ts +297 -0
- package/src/polyfills/http.ts +478 -0
- package/src/polyfills/module.ts +97 -0
- package/src/polyfills/net.ts +123 -0
- package/src/polyfills/os.ts +108 -0
- package/src/polyfills/path.ts +169 -0
- package/src/polyfills/perf_hooks.ts +30 -0
- package/src/polyfills/process.ts +349 -0
- package/src/polyfills/querystring.ts +66 -0
- package/src/polyfills/readdirp.ts +72 -0
- package/src/polyfills/readline.ts +80 -0
- package/src/polyfills/stream.ts +610 -0
- package/src/polyfills/stubs.ts +600 -0
- package/src/polyfills/tty.ts +43 -0
- package/src/polyfills/url.ts +97 -0
- package/src/polyfills/util.ts +173 -0
- package/src/polyfills/v8.ts +62 -0
- package/src/polyfills/vm.ts +111 -0
- package/src/polyfills/worker-threads.ts +189 -0
- package/src/polyfills/ws.ts +73 -0
- package/src/polyfills/zlib.ts +244 -0
- package/src/runtime-helpers.ts +83 -0
- package/src/runtime-interface.ts +46 -0
- package/src/sandbox.ts +178 -0
- package/src/server-bridge.ts +473 -0
- package/src/service-worker.ts +153 -0
- package/src/shell-commands.ts +708 -0
- package/src/shell.ts +795 -0
- package/src/transpiler.ts +282 -0
- package/src/type-checker.ts +241 -0
- package/src/types/package-json.ts +17 -0
- package/src/utils/binary-encoding.ts +38 -0
- package/src/utils/hash.ts +24 -0
- package/src/utils/safe-path.ts +38 -0
- 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
|
+
}
|