@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.
- package/LICENSE +43 -0
- package/README.md +240 -0
- package/dist/child_process-BJOMsZje.js +8233 -0
- package/dist/child_process-BJOMsZje.js.map +1 -0
- package/dist/child_process-Cj8vOcuc.cjs +7434 -0
- package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
- package/dist/index-Cb1Cgdnd.js +35308 -0
- package/dist/index-Cb1Cgdnd.js.map +1 -0
- package/dist/index-DsMGS-xc.cjs +37195 -0
- package/dist/index-DsMGS-xc.cjs.map +1 -0
- package/dist/index.cjs +65 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +59 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +95 -0
- package/src/__tests__/smoke.test.ts +11 -0
- package/src/constants/cdn-urls.ts +18 -0
- package/src/constants/config.ts +236 -0
- package/src/cross-origin.ts +26 -0
- package/src/engine-factory.ts +176 -0
- package/src/engine-types.ts +56 -0
- package/src/helpers/byte-encoding.ts +39 -0
- package/src/helpers/digest.ts +9 -0
- package/src/helpers/event-loop.ts +96 -0
- package/src/helpers/wasm-cache.ts +133 -0
- package/src/iframe-sandbox.ts +141 -0
- package/src/index.ts +192 -0
- package/src/isolation-helpers.ts +148 -0
- package/src/memory-volume.ts +941 -0
- package/src/module-transformer.ts +368 -0
- package/src/packages/archive-extractor.ts +248 -0
- package/src/packages/browser-bundler.ts +284 -0
- package/src/packages/installer.ts +396 -0
- package/src/packages/registry-client.ts +131 -0
- package/src/packages/version-resolver.ts +411 -0
- package/src/polyfills/assert.ts +384 -0
- package/src/polyfills/async_hooks.ts +144 -0
- package/src/polyfills/buffer.ts +628 -0
- package/src/polyfills/child_process.ts +2288 -0
- package/src/polyfills/chokidar.ts +336 -0
- package/src/polyfills/cluster.ts +106 -0
- package/src/polyfills/console.ts +136 -0
- package/src/polyfills/constants.ts +123 -0
- package/src/polyfills/crypto.ts +885 -0
- package/src/polyfills/dgram.ts +87 -0
- package/src/polyfills/diagnostics_channel.ts +76 -0
- package/src/polyfills/dns.ts +134 -0
- package/src/polyfills/domain.ts +68 -0
- package/src/polyfills/esbuild.ts +854 -0
- package/src/polyfills/events.ts +276 -0
- package/src/polyfills/fs.ts +2888 -0
- package/src/polyfills/fsevents.ts +79 -0
- package/src/polyfills/http.ts +1449 -0
- package/src/polyfills/http2.ts +199 -0
- package/src/polyfills/https.ts +76 -0
- package/src/polyfills/inspector.ts +62 -0
- package/src/polyfills/lightningcss.ts +105 -0
- package/src/polyfills/module.ts +191 -0
- package/src/polyfills/net.ts +353 -0
- package/src/polyfills/os.ts +238 -0
- package/src/polyfills/path.ts +206 -0
- package/src/polyfills/perf_hooks.ts +102 -0
- package/src/polyfills/process.ts +690 -0
- package/src/polyfills/punycode.ts +159 -0
- package/src/polyfills/querystring.ts +93 -0
- package/src/polyfills/quic.ts +118 -0
- package/src/polyfills/readdirp.ts +229 -0
- package/src/polyfills/readline.ts +692 -0
- package/src/polyfills/repl.ts +134 -0
- package/src/polyfills/rollup.ts +119 -0
- package/src/polyfills/sea.ts +33 -0
- package/src/polyfills/sqlite.ts +78 -0
- package/src/polyfills/stream.ts +1620 -0
- package/src/polyfills/string_decoder.ts +25 -0
- package/src/polyfills/tailwindcss-oxide.ts +309 -0
- package/src/polyfills/test.ts +197 -0
- package/src/polyfills/timers.ts +32 -0
- package/src/polyfills/tls.ts +105 -0
- package/src/polyfills/trace_events.ts +50 -0
- package/src/polyfills/tty.ts +71 -0
- package/src/polyfills/url.ts +174 -0
- package/src/polyfills/util.ts +559 -0
- package/src/polyfills/v8.ts +126 -0
- package/src/polyfills/vm.ts +132 -0
- package/src/polyfills/volume-registry.ts +15 -0
- package/src/polyfills/wasi.ts +44 -0
- package/src/polyfills/worker_threads.ts +326 -0
- package/src/polyfills/ws.ts +595 -0
- package/src/polyfills/zlib.ts +881 -0
- package/src/request-proxy.ts +716 -0
- package/src/script-engine.ts +3375 -0
- package/src/sdk/nodepod-fs.ts +93 -0
- package/src/sdk/nodepod-process.ts +86 -0
- package/src/sdk/nodepod-terminal.ts +350 -0
- package/src/sdk/nodepod.ts +509 -0
- package/src/sdk/types.ts +70 -0
- package/src/shell/commands/bun.ts +121 -0
- package/src/shell/commands/directory.ts +297 -0
- package/src/shell/commands/file-ops.ts +525 -0
- package/src/shell/commands/git.ts +2142 -0
- package/src/shell/commands/node.ts +80 -0
- package/src/shell/commands/npm.ts +198 -0
- package/src/shell/commands/pm-types.ts +45 -0
- package/src/shell/commands/pnpm.ts +82 -0
- package/src/shell/commands/search.ts +264 -0
- package/src/shell/commands/shell-env.ts +352 -0
- package/src/shell/commands/text-processing.ts +1152 -0
- package/src/shell/commands/yarn.ts +84 -0
- package/src/shell/shell-builtins.ts +19 -0
- package/src/shell/shell-helpers.ts +250 -0
- package/src/shell/shell-interpreter.ts +514 -0
- package/src/shell/shell-parser.ts +429 -0
- package/src/shell/shell-types.ts +85 -0
- package/src/syntax-transforms.ts +561 -0
- package/src/threading/engine-worker.ts +64 -0
- package/src/threading/inline-worker.ts +372 -0
- package/src/threading/offload-types.ts +112 -0
- package/src/threading/offload-worker.ts +383 -0
- package/src/threading/offload.ts +271 -0
- package/src/threading/process-context.ts +92 -0
- package/src/threading/process-handle.ts +275 -0
- package/src/threading/process-manager.ts +956 -0
- package/src/threading/process-worker-entry.ts +854 -0
- package/src/threading/shared-vfs.ts +352 -0
- package/src/threading/sync-channel.ts +135 -0
- package/src/threading/task-queue.ts +177 -0
- package/src/threading/vfs-bridge.ts +231 -0
- package/src/threading/worker-pool.ts +233 -0
- package/src/threading/worker-protocol.ts +358 -0
- package/src/threading/worker-vfs.ts +218 -0
- package/src/types/externals.d.ts +38 -0
- package/src/types/fs-streams.ts +142 -0
- package/src/types/manifest.ts +17 -0
- package/src/worker-sandbox.ts +90 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
// Shell interpreter: pipes, &&/||/;, redirections, variable expansion,
|
|
2
|
+
// globbing, command substitution, builtins, and registered commands.
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ShellResult,
|
|
6
|
+
ShellContext,
|
|
7
|
+
ShellCommand,
|
|
8
|
+
ListNode,
|
|
9
|
+
PipelineNode,
|
|
10
|
+
CommandNode,
|
|
11
|
+
BuiltinFn,
|
|
12
|
+
} from "./shell-types";
|
|
13
|
+
import type { MemoryVolume } from "../memory-volume";
|
|
14
|
+
import { parse, expandGlob } from "./shell-parser";
|
|
15
|
+
import { builtins } from "./shell-builtins";
|
|
16
|
+
|
|
17
|
+
/* ------------------------------------------------------------------ */
|
|
18
|
+
/* NodepodShell */
|
|
19
|
+
/* ------------------------------------------------------------------ */
|
|
20
|
+
|
|
21
|
+
// In a worker, the shell can't spawn new workers directly.
|
|
22
|
+
// This callback sends a spawn request to the main thread's ProcessManager.
|
|
23
|
+
export type SpawnChildCallback = (
|
|
24
|
+
command: string,
|
|
25
|
+
args: string[],
|
|
26
|
+
opts?: { cwd?: string; env?: Record<string, string>; stdio?: "pipe" | "inherit" },
|
|
27
|
+
) => Promise<{ pid: number; exitCode: number; stdout: string; stderr: string }>;
|
|
28
|
+
|
|
29
|
+
export class NodepodShell {
|
|
30
|
+
private volume: MemoryVolume;
|
|
31
|
+
private cwd: string;
|
|
32
|
+
private env: Record<string, string>;
|
|
33
|
+
private commands = new Map<string, ShellCommand>();
|
|
34
|
+
private lastExit = 0;
|
|
35
|
+
private aliases = new Map<string, string>();
|
|
36
|
+
// serializes concurrent exec() calls to prevent cwd save/restore races
|
|
37
|
+
private _execQueue: Promise<ShellResult> = Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
38
|
+
|
|
39
|
+
private _spawnChild: SpawnChildCallback | null = null;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
volume: MemoryVolume,
|
|
43
|
+
opts?: { cwd?: string; env?: Record<string, string> },
|
|
44
|
+
) {
|
|
45
|
+
this.volume = volume;
|
|
46
|
+
this.cwd = opts?.cwd ?? "/";
|
|
47
|
+
this.env = opts?.env ? { ...opts.env } : {};
|
|
48
|
+
this.env.PWD = this.cwd;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
registerCommand(cmd: ShellCommand): void {
|
|
52
|
+
this.commands.set(cmd.name, cmd);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setSpawnChildCallback(cb: SpawnChildCallback | null): void {
|
|
56
|
+
this._spawnChild = cb;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getSpawnChildCallback(): SpawnChildCallback | null {
|
|
60
|
+
return this._spawnChild;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getCwd(): string {
|
|
64
|
+
return this.cwd;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setCwd(cwd: string): void {
|
|
68
|
+
this.cwd = cwd;
|
|
69
|
+
this.env.PWD = cwd;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getEnv(): Record<string, string> {
|
|
73
|
+
return this.env;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async exec(
|
|
77
|
+
command: string,
|
|
78
|
+
opts?: { cwd?: string; env?: Record<string, string> },
|
|
79
|
+
): Promise<ShellResult> {
|
|
80
|
+
// serialize cwd/env-overriding calls to prevent save/restore races
|
|
81
|
+
if (opts?.cwd || opts?.env) {
|
|
82
|
+
const prev = this._execQueue;
|
|
83
|
+
let resolve!: (r: ShellResult) => void;
|
|
84
|
+
this._execQueue = new Promise<ShellResult>((r) => { resolve = r; });
|
|
85
|
+
await prev.catch(() => {});
|
|
86
|
+
try {
|
|
87
|
+
const r = await this._execInner(command, opts);
|
|
88
|
+
resolve(r);
|
|
89
|
+
return r;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const err: ShellResult = {
|
|
92
|
+
stdout: "",
|
|
93
|
+
stderr: `shell: ${e instanceof Error ? e.message : String(e)}\n`,
|
|
94
|
+
exitCode: 1,
|
|
95
|
+
};
|
|
96
|
+
resolve(err);
|
|
97
|
+
return err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return this._execInner(command, opts);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async _execInner(
|
|
104
|
+
command: string,
|
|
105
|
+
opts?: { cwd?: string; env?: Record<string, string> },
|
|
106
|
+
): Promise<ShellResult> {
|
|
107
|
+
const prevCwd = this.cwd;
|
|
108
|
+
const prevEnv = { ...this.env };
|
|
109
|
+
|
|
110
|
+
if (opts?.cwd) {
|
|
111
|
+
this.cwd = opts.cwd;
|
|
112
|
+
this.env.PWD = opts.cwd;
|
|
113
|
+
}
|
|
114
|
+
if (opts?.env) {
|
|
115
|
+
Object.assign(this.env, opts.env);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const expanded = await this.expandCommandSubstitution(command);
|
|
120
|
+
|
|
121
|
+
const ast = parse(expanded, this.env, this.lastExit);
|
|
122
|
+
return await this.execList(ast);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return {
|
|
125
|
+
stdout: "",
|
|
126
|
+
stderr: `shell: ${e instanceof Error ? e.message : String(e)}\n`,
|
|
127
|
+
exitCode: 1,
|
|
128
|
+
};
|
|
129
|
+
} finally {
|
|
130
|
+
// only restore cwd if cd didn't change it
|
|
131
|
+
if (opts?.cwd && this.cwd === opts.cwd) {
|
|
132
|
+
this.cwd = prevCwd;
|
|
133
|
+
this.env.PWD = prevCwd;
|
|
134
|
+
}
|
|
135
|
+
if (opts?.env) {
|
|
136
|
+
for (const key of Object.keys(opts.env)) {
|
|
137
|
+
if (key in prevEnv) {
|
|
138
|
+
this.env[key] = prevEnv[key];
|
|
139
|
+
} else {
|
|
140
|
+
delete this.env[key];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ---------------------------------------------------------------- */
|
|
148
|
+
/* AST execution */
|
|
149
|
+
/* ---------------------------------------------------------------- */
|
|
150
|
+
|
|
151
|
+
private async execList(list: ListNode): Promise<ShellResult> {
|
|
152
|
+
let result: ShellResult = { stdout: "", stderr: "", exitCode: 0 };
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < list.entries.length; i++) {
|
|
155
|
+
const entry = list.entries[i];
|
|
156
|
+
const pipeResult = await this.execPipeline(entry.pipeline);
|
|
157
|
+
|
|
158
|
+
result = {
|
|
159
|
+
stdout: result.stdout + pipeResult.stdout,
|
|
160
|
+
stderr: result.stderr + pipeResult.stderr,
|
|
161
|
+
exitCode: pipeResult.exitCode,
|
|
162
|
+
};
|
|
163
|
+
this.lastExit = pipeResult.exitCode;
|
|
164
|
+
|
|
165
|
+
if (entry.next === "&&" && pipeResult.exitCode !== 0) break;
|
|
166
|
+
if (entry.next === "||" && pipeResult.exitCode === 0) break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async execPipeline(pipeline: PipelineNode): Promise<ShellResult> {
|
|
173
|
+
if (pipeline.commands.length === 1) {
|
|
174
|
+
return this.execCommand(pipeline.commands[0]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let stdin: string | undefined;
|
|
178
|
+
let lastResult: ShellResult = { stdout: "", stderr: "", exitCode: 0 };
|
|
179
|
+
let allStderr = "";
|
|
180
|
+
|
|
181
|
+
for (const cmd of pipeline.commands) {
|
|
182
|
+
const result = await this.execCommand(cmd, stdin);
|
|
183
|
+
allStderr += result.stderr;
|
|
184
|
+
stdin = result.stdout;
|
|
185
|
+
lastResult = result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
stdout: lastResult.stdout,
|
|
190
|
+
stderr: allStderr,
|
|
191
|
+
exitCode: lastResult.exitCode,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async execCommand(
|
|
196
|
+
cmd: CommandNode,
|
|
197
|
+
stdin?: string,
|
|
198
|
+
): Promise<ShellResult> {
|
|
199
|
+
if (cmd.args.length === 0) {
|
|
200
|
+
for (const [k, v] of Object.entries(cmd.assignments)) {
|
|
201
|
+
this.env[k] = v;
|
|
202
|
+
}
|
|
203
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let expandedArgs: string[] = [];
|
|
207
|
+
for (const arg of cmd.args) {
|
|
208
|
+
expandedArgs.push(...expandGlob(arg, this.cwd, this.volume));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const alias = this.aliases.get(expandedArgs[0]);
|
|
212
|
+
if (alias) {
|
|
213
|
+
const aliasArgs = alias.split(/\s+/);
|
|
214
|
+
expandedArgs = [...aliasArgs, ...expandedArgs.slice(1)];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const name = expandedArgs[0];
|
|
218
|
+
const args = expandedArgs.slice(1);
|
|
219
|
+
|
|
220
|
+
if (stdin === undefined) {
|
|
221
|
+
for (const r of cmd.redirects) {
|
|
222
|
+
if (r.type === "read") {
|
|
223
|
+
const p = this.resolvePath(r.target);
|
|
224
|
+
try {
|
|
225
|
+
stdin = this.volume.readFileSync(p, "utf8");
|
|
226
|
+
} catch {
|
|
227
|
+
return {
|
|
228
|
+
stdout: "",
|
|
229
|
+
stderr: `shell: ${r.target}: No such file or directory\n`,
|
|
230
|
+
exitCode: 1,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const savedEnv: Record<string, string | undefined> = {};
|
|
238
|
+
for (const [k, v] of Object.entries(cmd.assignments)) {
|
|
239
|
+
savedEnv[k] = this.env[k];
|
|
240
|
+
this.env[k] = v;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ctx = this.buildContext();
|
|
244
|
+
let result: ShellResult;
|
|
245
|
+
|
|
246
|
+
// dispatch: builtin > registered command > PATH lookup > error
|
|
247
|
+
const builtin = builtins.get(name);
|
|
248
|
+
if (builtin) {
|
|
249
|
+
const r = builtin(args, ctx, stdin);
|
|
250
|
+
result = r instanceof Promise ? await r : r;
|
|
251
|
+
if (name === "cd") {
|
|
252
|
+
this.cwd = ctx.cwd;
|
|
253
|
+
this.env = ctx.env;
|
|
254
|
+
}
|
|
255
|
+
if (name === "export" || name === "unset") {
|
|
256
|
+
this.env = ctx.env;
|
|
257
|
+
}
|
|
258
|
+
} else if (name === "alias") {
|
|
259
|
+
result = this.handleAlias(args);
|
|
260
|
+
} else if (name === "source" || name === ".") {
|
|
261
|
+
result = await this.handleSource(args);
|
|
262
|
+
} else if (name === "history") {
|
|
263
|
+
result = { stdout: "", stderr: "", exitCode: 0 };
|
|
264
|
+
} else if (this.commands.has(name)) {
|
|
265
|
+
try {
|
|
266
|
+
result = await this.commands.get(name)!.execute(args, ctx);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
269
|
+
result = {
|
|
270
|
+
stdout: "",
|
|
271
|
+
stderr: `${name}: ${msg}\n`,
|
|
272
|
+
exitCode: 1,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
this.cwd = ctx.cwd;
|
|
276
|
+
this.env = ctx.env;
|
|
277
|
+
} else {
|
|
278
|
+
const resolvedBin = this.resolveFromPath(name);
|
|
279
|
+
if (resolvedBin && this.commands.has("node")) {
|
|
280
|
+
try {
|
|
281
|
+
result = await this.commands
|
|
282
|
+
.get("node")!
|
|
283
|
+
.execute([resolvedBin, ...args], ctx);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
286
|
+
result = {
|
|
287
|
+
stdout: "",
|
|
288
|
+
stderr: `${name}: ${msg}\n`,
|
|
289
|
+
exitCode: 1,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
this.cwd = ctx.cwd;
|
|
293
|
+
this.env = ctx.env;
|
|
294
|
+
} else {
|
|
295
|
+
result = {
|
|
296
|
+
stdout: "",
|
|
297
|
+
stderr: `${name}: command not found\n`,
|
|
298
|
+
exitCode: 127,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const [k, v] of Object.entries(savedEnv)) {
|
|
304
|
+
if (v === undefined) delete this.env[k];
|
|
305
|
+
else this.env[k] = v;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
result = await this.applyRedirects(result, cmd);
|
|
309
|
+
|
|
310
|
+
this.lastExit = result.exitCode;
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* ---------------------------------------------------------------- */
|
|
315
|
+
/* Helpers */
|
|
316
|
+
/* ---------------------------------------------------------------- */
|
|
317
|
+
|
|
318
|
+
private buildContext(): ShellContext {
|
|
319
|
+
return {
|
|
320
|
+
cwd: this.cwd,
|
|
321
|
+
env: { ...this.env },
|
|
322
|
+
volume: this.volume,
|
|
323
|
+
exec: (cmd, opts) => this.exec(cmd, opts),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private resolvePath(p: string): string {
|
|
328
|
+
if (p.startsWith("/")) return this.normalizePath(p);
|
|
329
|
+
return this.normalizePath(`${this.cwd}/${p}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Search PATH for a command. Parses .bin stubs to find the real JS entry point.
|
|
333
|
+
private resolveFromPath(name: string): string | null {
|
|
334
|
+
const pathStr = this.env.PATH || "";
|
|
335
|
+
const dirs = pathStr.split(":");
|
|
336
|
+
|
|
337
|
+
for (const dir of dirs) {
|
|
338
|
+
if (!dir) continue;
|
|
339
|
+
const candidate = `${dir}/${name}`;
|
|
340
|
+
if (!this.volume.existsSync(candidate)) continue;
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const content = this.volume.readFileSync(candidate, "utf8");
|
|
344
|
+
// bin stubs: node "/node_modules/pkg/index.js" "$@"
|
|
345
|
+
const match = content.match(/node\s+"([^"]+)"/);
|
|
346
|
+
if (match && this.volume.existsSync(match[1])) {
|
|
347
|
+
return match[1];
|
|
348
|
+
}
|
|
349
|
+
if (
|
|
350
|
+
content.startsWith("#!/") ||
|
|
351
|
+
content.startsWith("'use strict'") ||
|
|
352
|
+
content.startsWith('"use strict"') ||
|
|
353
|
+
content.startsWith("var ") ||
|
|
354
|
+
content.startsWith("const ") ||
|
|
355
|
+
content.startsWith("import ") ||
|
|
356
|
+
content.startsWith("module.")
|
|
357
|
+
) {
|
|
358
|
+
return candidate;
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
/* skip */
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private normalizePath(raw: string): string {
|
|
369
|
+
const parts = raw.split("/").filter(Boolean);
|
|
370
|
+
const stack: string[] = [];
|
|
371
|
+
for (const part of parts) {
|
|
372
|
+
if (part === "..") stack.pop();
|
|
373
|
+
else if (part !== ".") stack.push(part);
|
|
374
|
+
}
|
|
375
|
+
return "/" + stack.join("/");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private async applyRedirects(
|
|
379
|
+
result: ShellResult,
|
|
380
|
+
cmd: CommandNode,
|
|
381
|
+
): Promise<ShellResult> {
|
|
382
|
+
let { stdout, stderr, exitCode } = result;
|
|
383
|
+
|
|
384
|
+
for (const r of cmd.redirects) {
|
|
385
|
+
if (r.type === "stderr-to-stdout") {
|
|
386
|
+
stdout += stderr;
|
|
387
|
+
stderr = "";
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (r.type === "write" || r.type === "append") {
|
|
392
|
+
const p = this.resolvePath(r.target);
|
|
393
|
+
try {
|
|
394
|
+
if (r.type === "append" && this.volume.existsSync(p)) {
|
|
395
|
+
const existing = this.volume.readFileSync(p, "utf8");
|
|
396
|
+
this.volume.writeFileSync(p, existing + stdout);
|
|
397
|
+
} else {
|
|
398
|
+
const dir = this.normalizePath(p.substring(0, p.lastIndexOf("/")));
|
|
399
|
+
if (dir && dir !== "/" && !this.volume.existsSync(dir)) {
|
|
400
|
+
this.volume.mkdirSync(dir, { recursive: true });
|
|
401
|
+
}
|
|
402
|
+
this.volume.writeFileSync(p, stdout);
|
|
403
|
+
}
|
|
404
|
+
stdout = "";
|
|
405
|
+
} catch (e) {
|
|
406
|
+
stderr += `shell: ${r.target}: ${e instanceof Error ? e.message : "Cannot write"}\n`;
|
|
407
|
+
exitCode = 1;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { stdout, stderr, exitCode };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private async expandCommandSubstitution(input: string): Promise<string> {
|
|
416
|
+
let result = "";
|
|
417
|
+
let i = 0;
|
|
418
|
+
|
|
419
|
+
while (i < input.length) {
|
|
420
|
+
if (input[i] === "'") {
|
|
421
|
+
result += "'";
|
|
422
|
+
i++;
|
|
423
|
+
while (i < input.length && input[i] !== "'") {
|
|
424
|
+
result += input[i++];
|
|
425
|
+
}
|
|
426
|
+
if (i < input.length) result += input[i++];
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (input[i] === "$" && input[i + 1] === "(") {
|
|
431
|
+
i += 2;
|
|
432
|
+
let depth = 1;
|
|
433
|
+
let subCmd = "";
|
|
434
|
+
while (i < input.length && depth > 0) {
|
|
435
|
+
if (input[i] === "(") depth++;
|
|
436
|
+
if (input[i] === ")") depth--;
|
|
437
|
+
if (depth > 0) subCmd += input[i];
|
|
438
|
+
i++;
|
|
439
|
+
}
|
|
440
|
+
const subResult = await this.exec(subCmd);
|
|
441
|
+
result += subResult.stdout.replace(/\n$/, "");
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (input[i] === "`") {
|
|
446
|
+
i++;
|
|
447
|
+
let subCmd = "";
|
|
448
|
+
while (i < input.length && input[i] !== "`") {
|
|
449
|
+
subCmd += input[i++];
|
|
450
|
+
}
|
|
451
|
+
if (i < input.length) i++;
|
|
452
|
+
const subResult = await this.exec(subCmd);
|
|
453
|
+
result += subResult.stdout.replace(/\n$/, "");
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
result += input[i++];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private handleAlias(args: string[]): ShellResult {
|
|
464
|
+
if (args.length === 0) {
|
|
465
|
+
let out = "";
|
|
466
|
+
for (const [k, v] of this.aliases) out += `alias ${k}='${v}'\n`;
|
|
467
|
+
return { stdout: out, stderr: "", exitCode: 0 };
|
|
468
|
+
}
|
|
469
|
+
for (const arg of args) {
|
|
470
|
+
const eq = arg.indexOf("=");
|
|
471
|
+
if (eq > 0) {
|
|
472
|
+
let val = arg.slice(eq + 1);
|
|
473
|
+
if (
|
|
474
|
+
(val.startsWith("'") && val.endsWith("'")) ||
|
|
475
|
+
(val.startsWith('"') && val.endsWith('"'))
|
|
476
|
+
) {
|
|
477
|
+
val = val.slice(1, -1);
|
|
478
|
+
}
|
|
479
|
+
this.aliases.set(arg.slice(0, eq), val);
|
|
480
|
+
} else {
|
|
481
|
+
const val = this.aliases.get(arg);
|
|
482
|
+
if (val)
|
|
483
|
+
return { stdout: `alias ${arg}='${val}'\n`, stderr: "", exitCode: 0 };
|
|
484
|
+
return {
|
|
485
|
+
stdout: "",
|
|
486
|
+
stderr: `alias: ${arg}: not found\n`,
|
|
487
|
+
exitCode: 1,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private async handleSource(args: string[]): Promise<ShellResult> {
|
|
495
|
+
if (args.length === 0) {
|
|
496
|
+
return {
|
|
497
|
+
stdout: "",
|
|
498
|
+
stderr: "source: missing file argument\n",
|
|
499
|
+
exitCode: 1,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const p = this.resolvePath(args[0]);
|
|
503
|
+
try {
|
|
504
|
+
const content = this.volume.readFileSync(p, "utf8");
|
|
505
|
+
return this.exec(content);
|
|
506
|
+
} catch {
|
|
507
|
+
return {
|
|
508
|
+
stdout: "",
|
|
509
|
+
stderr: `source: ${args[0]}: No such file or directory\n`,
|
|
510
|
+
exitCode: 1,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|