@nimbus-sh/core 0.1.0 → 0.3.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/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/runtime/bash-runner.d.ts +63 -0
- package/dist/runtime/bash-runner.d.ts.map +1 -0
- package/dist/runtime/bash-runner.generated.d.ts +14 -0
- package/dist/runtime/bash-runner.generated.d.ts.map +1 -0
- package/dist/runtime/bash-runner.generated.js +13 -0
- package/dist/runtime/bash-runner.js +290 -0
- package/dist/runtime/cpython-runner.d.ts +86 -0
- package/dist/runtime/cpython-runner.d.ts.map +1 -0
- package/dist/runtime/cpython-runner.js +425 -0
- package/dist/runtime/facet-host.d.ts +159 -0
- package/dist/runtime/facet-host.d.ts.map +1 -0
- package/dist/runtime/facet-host.js +22 -0
- package/dist/runtime/installed-runtimes.d.ts +99 -0
- package/dist/runtime/installed-runtimes.d.ts.map +1 -0
- package/dist/runtime/installed-runtimes.js +162 -0
- package/dist/runtime/local-facet-host.d.ts +38 -0
- package/dist/runtime/local-facet-host.d.ts.map +1 -0
- package/dist/runtime/local-facet-host.js +171 -0
- package/dist/runtime/python-pip.d.ts +38 -0
- package/dist/runtime/python-pip.d.ts.map +1 -0
- package/dist/runtime/python-pip.js +1063 -0
- package/dist/runtime/runtime-manifest.d.ts +86 -0
- package/dist/runtime/runtime-manifest.d.ts.map +1 -0
- package/dist/runtime/runtime-manifest.js +72 -0
- package/dist/runtime/runtime-package.d.ts +63 -0
- package/dist/runtime/runtime-package.d.ts.map +1 -0
- package/dist/runtime/runtime-package.js +66 -0
- package/dist/runtime/runtime-registry.d.ts +162 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -0
- package/dist/runtime/runtime-registry.js +363 -0
- package/dist/runtime/vfs-snapshot.d.ts.map +1 -1
- package/dist/runtime/vfs-snapshot.js +15 -1
- package/dist/runtime/vfs-supervisor.d.ts +22 -0
- package/dist/runtime/vfs-supervisor.d.ts.map +1 -0
- package/dist/runtime/vfs-supervisor.js +65 -0
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +14 -0
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -0
- package/dist/runtime/virtual-socket-kernel.generated.js +13 -0
- package/dist/runtime/wasm-runner.d.ts +80 -0
- package/dist/runtime/wasm-runner.d.ts.map +1 -0
- package/dist/runtime/wasm-runner.js +686 -0
- package/dist/workspace/nimbus-workspace.d.ts +116 -20
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +238 -45
- package/package.json +4 -2
- package/src/index.ts +16 -0
- package/src/runtime/bash-runner.generated.ts +14 -0
- package/src/runtime/bash-runner.ts +347 -0
- package/src/runtime/cpython-runner.ts +504 -0
- package/src/runtime/facet-host.ts +170 -0
- package/src/runtime/installed-runtimes.ts +235 -0
- package/src/runtime/local-facet-host.ts +205 -0
- package/src/runtime/python-pip.ts +1211 -0
- package/src/runtime/runtime-manifest.ts +155 -0
- package/src/runtime/runtime-package.ts +114 -0
- package/src/runtime/runtime-registry.ts +511 -0
- package/src/runtime/vfs-snapshot.ts +15 -1
- package/src/runtime/vfs-supervisor.ts +67 -0
- package/src/runtime/virtual-socket-kernel.generated.ts +14 -0
- package/src/runtime/wasm-runner.ts +835 -0
- package/src/workspace/nimbus-workspace.ts +349 -54
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-registry.ts — shared shell-command factory for runtime
|
|
3
|
+
* dispatchers (node, bun, and future native-WASM / Python / Ruby /
|
|
4
|
+
* AssemblyScript runtimes).
|
|
5
|
+
*
|
|
6
|
+
* Why this exists
|
|
7
|
+
* ───────────────
|
|
8
|
+
* `node` and `bun` shell-command handlers in src/session/init.ts
|
|
9
|
+
* shared ~85% of their code: argv parsing for --version / --help /
|
|
10
|
+
* -e / script-path, VFS lookup, shebang strip, esbuild transform for
|
|
11
|
+
* .ts/.tsx/.jsx, dispatch to the runner. The duplication had drifted
|
|
12
|
+
* — only `node` had the primitive #1 nodeFlagSpan fix
|
|
13
|
+
* (init.ts:233-243), only `node` had primitive-#2 binSpawn ctx
|
|
14
|
+
* propagation (init.ts:391-403), only `bun` had install / run
|
|
15
|
+
* subcommand routing.
|
|
16
|
+
*
|
|
17
|
+
* `buildRuntimeHandler` returns a single shell-handler function that
|
|
18
|
+
* encodes the shared contract. Per-runtime variation is supplied
|
|
19
|
+
* via the `RuntimeSpec` parameter:
|
|
20
|
+
*
|
|
21
|
+
* - name + version + helpText
|
|
22
|
+
* - run(): runner fn (runFresh / runBunScript / wasm-runner)
|
|
23
|
+
* - subcommands: optional map of `<verb> → handler` for
|
|
24
|
+
* bun-style `bun install`, `bun run` (node has none today)
|
|
25
|
+
* - transform(): optional code rewriter (bun prepends BUN_SHIM_PREAMBLE)
|
|
26
|
+
* - supportsBinSpawn: true for node (the .bin handler propagates
|
|
27
|
+
* a callerPid); other runtimes use a plain spawn flow.
|
|
28
|
+
*
|
|
29
|
+
* Anti-requirements observed
|
|
30
|
+
* ──────────────────────────
|
|
31
|
+
* - NO setTimeout / NO retry / NO defensive-catch added.
|
|
32
|
+
* - NO behavioral change vs the pre-refactor handlers — every
|
|
33
|
+
* runtime-specific quirk is preserved exactly.
|
|
34
|
+
* - Per-runtime test parity: existing runtime-primitives probes
|
|
35
|
+
* (#1 npx / #2 .bin) and runtime-pkg probes (G1-G4) MUST still
|
|
36
|
+
* pass against the refactored handlers — the contract is
|
|
37
|
+
* observable behaviour, not implementation shape.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import type { SqliteVFS } from '../vfs/sqlite-vfs.js';
|
|
41
|
+
import { normalizeVfsPath, resolveVfsPath, vfsPathExtension } from '../vfs/path.js';
|
|
42
|
+
import { CRED_KERNEL, type VfsCred } from './os-contracts.js';
|
|
43
|
+
import type { EsbuildService } from './esbuild-service.js';
|
|
44
|
+
import { parseFacetBundleProfile, type FacetBundleProfile } from './bundle-profile.js';
|
|
45
|
+
import { bindImportMetaResolve, importMetaDefines } from './import-meta-transform.js';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result shape that runtime-registry expects from a runner. Mirrors
|
|
49
|
+
* the existing RunFreshResult / RunBunResult shapes — kept narrow so
|
|
50
|
+
* future runtimes don't have to plumb runtime-internal state.
|
|
51
|
+
*/
|
|
52
|
+
export interface RuntimeRunResult {
|
|
53
|
+
exitCode: number;
|
|
54
|
+
stdout: string;
|
|
55
|
+
stderr: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Options the handler passes to the runner. Mirrors RunFreshOpts.
|
|
60
|
+
*/
|
|
61
|
+
export interface RuntimeRunOpts {
|
|
62
|
+
argv: string[];
|
|
63
|
+
env: Record<string, string> | undefined;
|
|
64
|
+
cwd: string | undefined;
|
|
65
|
+
filename: string;
|
|
66
|
+
dirname: string;
|
|
67
|
+
command: string;
|
|
68
|
+
/** Primitive #1/G4 hooks. node-runner consumes these; other
|
|
69
|
+
* runtimes ignore them safely. */
|
|
70
|
+
skipSpawn?: boolean;
|
|
71
|
+
callerPid?: number;
|
|
72
|
+
/** Capture stdout/stderr in the result instead of streaming to the
|
|
73
|
+
* terminal supervisor. Used by child_process pipe semantics. */
|
|
74
|
+
captureOutput?: boolean;
|
|
75
|
+
forceLongRunning?: boolean;
|
|
76
|
+
attachedTty?: boolean;
|
|
77
|
+
bundleProfile?: FacetBundleProfile;
|
|
78
|
+
/** Invoking process credentials for credential-bound runtime snapshots. */
|
|
79
|
+
cred?: VfsCred;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Extensions probed when a target names no exact file, in Node's order. */
|
|
83
|
+
const SCRIPT_RESOLUTION_CANDIDATES = ['.js', '.ts', '.tsx', '.mjs', '.jsx', '/index.js', '/index.ts'];
|
|
84
|
+
|
|
85
|
+
/** The VFS surface script resolution needs. */
|
|
86
|
+
export interface ScriptResolutionFs {
|
|
87
|
+
isFile(path: string): boolean;
|
|
88
|
+
readFileString(path: string): string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a runtime target — `./cli.ts`, `sub/x`, `.`, or a bare name — to a
|
|
93
|
+
* canonical VFS key, or null when nothing runnable sits there.
|
|
94
|
+
*
|
|
95
|
+
* A directory never resolves to itself: it falls through to the index
|
|
96
|
+
* candidates, so `bun ./tools` finds `tools/index.js` the way real bun does
|
|
97
|
+
* rather than trying to read the directory as source.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveRuntimeScriptPath(
|
|
100
|
+
fs: ScriptResolutionFs,
|
|
101
|
+
cwd: string,
|
|
102
|
+
target: string,
|
|
103
|
+
opts?: { preferModuleField?: boolean },
|
|
104
|
+
): string | null {
|
|
105
|
+
const base = normalizeVfsPath(cwd || '/home/user');
|
|
106
|
+
let resolved: string;
|
|
107
|
+
if (target === '.' || target === './') {
|
|
108
|
+
// `node .` / `bun .` — the package's declared entry point.
|
|
109
|
+
let main = 'index.js';
|
|
110
|
+
try {
|
|
111
|
+
const pkg = JSON.parse(fs.readFileString(`${base}/package.json`));
|
|
112
|
+
main = (opts?.preferModuleField ? pkg.module : undefined) || pkg.main || 'index.js';
|
|
113
|
+
} catch { /* no readable package.json — index.js */ }
|
|
114
|
+
resolved = resolveVfsPath(main, base);
|
|
115
|
+
} else {
|
|
116
|
+
resolved = resolveVfsPath(target, base);
|
|
117
|
+
}
|
|
118
|
+
if (fs.isFile(resolved)) return resolved;
|
|
119
|
+
for (const candidate of SCRIPT_RESOLUTION_CANDIDATES) {
|
|
120
|
+
if (fs.isFile(resolved + candidate)) return resolved + candidate;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A subcommand handler. `runAsRuntime` re-enters the standard flow — flag
|
|
127
|
+
* span, script resolution, transform, exec — with a rewritten argv, as if
|
|
128
|
+
* the verb had never been typed. `bun run <file>` uses it to hand a path
|
|
129
|
+
* to the very same execution path `bun <file>` takes, rather than growing
|
|
130
|
+
* a second one.
|
|
131
|
+
*/
|
|
132
|
+
export type RuntimeSubcommand = (
|
|
133
|
+
ctx: any,
|
|
134
|
+
registry: ShellRegistry,
|
|
135
|
+
runAsRuntime: (args: string[]) => Promise<number>,
|
|
136
|
+
) => Promise<number>;
|
|
137
|
+
|
|
138
|
+
export interface RuntimeSpec {
|
|
139
|
+
/** Shell-command name: 'node' / 'bun' / 'wasm-runner' / 'python'. */
|
|
140
|
+
name: string;
|
|
141
|
+
/** Output of `<name> --version`. Includes the leading 'v' if the
|
|
142
|
+
* runtime convention does (Node: 'v20.0.0'; Bun: '1.1.42'). */
|
|
143
|
+
version: string;
|
|
144
|
+
/** Multi-line help text for `<name> --help`. */
|
|
145
|
+
helpText: string;
|
|
146
|
+
/**
|
|
147
|
+
* Runner function. Closes over whatever substrate the runtime executes on —
|
|
148
|
+
* a FacetManager for node and bun, a {@link ./facet-host.js FacetHost} for
|
|
149
|
+
* wasm-runner — because this factory never inspects it. It used to travel
|
|
150
|
+
* through here as a first parameter, which is the only thing that tied the
|
|
151
|
+
* shared handler to a Durable Object.
|
|
152
|
+
*/
|
|
153
|
+
run(code: string, opts: RuntimeRunOpts): Promise<RuntimeRunResult>;
|
|
154
|
+
/**
|
|
155
|
+
* Subcommand router. When the first positional arg is a key in
|
|
156
|
+
* this map, the handler is invoked instead of the standard
|
|
157
|
+
* script-execution flow. Used by `bun install`, `bun run <script>`.
|
|
158
|
+
*/
|
|
159
|
+
subcommands?: Record<string, RuntimeSubcommand>;
|
|
160
|
+
/**
|
|
161
|
+
* When true, the runtime treats the args list as a binary file
|
|
162
|
+
* path (NOT a JS script). Used by `wasm-runner` — the args[0] is a
|
|
163
|
+
* .wasm path, args[1+] are the function name + integer args.
|
|
164
|
+
* The handler skips the read-and-transform-script flow and calls
|
|
165
|
+
* `run()` with a synthetic empty `code` — runtimes that set this
|
|
166
|
+
* flag implement the actual bytes-load inside their runner.
|
|
167
|
+
*/
|
|
168
|
+
bypassesScriptRead?: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* Primitive #1 / G4 — when true, the script-execution branch
|
|
171
|
+
* propagates `ctx.__nimbusBinSpawn` into RuntimeRunOpts. Only
|
|
172
|
+
* `node` enables this; bun's runFresh chain doesn't share PID
|
|
173
|
+
* state with the .bin handler today. Future runtimes set this
|
|
174
|
+
* iff they share the runFresh contract.
|
|
175
|
+
*/
|
|
176
|
+
supportsBinSpawn?: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Minimal registry shape we depend on. Avoids importing the full vendored
|
|
181
|
+
* shell registry type tree when the runtime path only needs resolve().
|
|
182
|
+
*/
|
|
183
|
+
export interface ShellRegistry {
|
|
184
|
+
resolve(name: string): Promise<any> | any;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build a shell-handler function for a runtime. The returned function
|
|
189
|
+
* is the value passed to `registry.register('<name>', handler)`.
|
|
190
|
+
*
|
|
191
|
+
* Captures `vfs`, `getEsbuild` (for lazy init) + the spec. The same factory is used for every runtime; the only
|
|
192
|
+
* runtime-specific code lives in `spec`.
|
|
193
|
+
*/
|
|
194
|
+
export function buildRuntimeHandler(
|
|
195
|
+
spec: RuntimeSpec,
|
|
196
|
+
ctx0: {
|
|
197
|
+
vfs: SqliteVFS;
|
|
198
|
+
/** Lazy esbuild initialiser. Called once per first .ts/.tsx/.jsx
|
|
199
|
+
* invocation — the host owns the init lifecycle, including whether
|
|
200
|
+
* the module is loaded eagerly or on this call. */
|
|
201
|
+
getEsbuild(): EsbuildService | Promise<EsbuildService>;
|
|
202
|
+
registry: ShellRegistry;
|
|
203
|
+
},
|
|
204
|
+
): (ctx: any) => Promise<number> {
|
|
205
|
+
const { vfs, getEsbuild, registry } = ctx0;
|
|
206
|
+
const fs = vfs.as(CRED_KERNEL);
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The standard invocation: flag span, --version/--help/-e, then the
|
|
210
|
+
* script-path flow. Subcommand verbs are NOT considered here — the
|
|
211
|
+
* caller has already consumed them — so a verb handler can delegate
|
|
212
|
+
* back in with a rewritten argv without re-triggering itself.
|
|
213
|
+
*/
|
|
214
|
+
async function runtimeInvocation(ctx: any, args: string[]): Promise<number> {
|
|
215
|
+
const name = spec.name;
|
|
216
|
+
const nimbusCtx = ctx as {
|
|
217
|
+
__nimbusCaptureOutput?: unknown;
|
|
218
|
+
__nimbusBundleProfile?: unknown;
|
|
219
|
+
__nimbusBinSpawn?: {
|
|
220
|
+
callerPid?: number;
|
|
221
|
+
command?: string;
|
|
222
|
+
forceLongRunning?: boolean;
|
|
223
|
+
attachedTty?: boolean;
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
// A facet-hosted runtime streams its output to the session terminal over
|
|
227
|
+
// the supervisor RPC and hands the shell an empty string. That is live and
|
|
228
|
+
// cheap, and it is only correct while the process's stdout IS the terminal:
|
|
229
|
+
// the stream bypasses the shell's stdout chain, so the moment fd 1 or fd 2
|
|
230
|
+
// is a file, a pipe, a command substitution, or the capture sink of a
|
|
231
|
+
// programmatic exec, the bytes have to come back in the result and be
|
|
232
|
+
// written through ctx.stdout instead. A context with no fd table of its own
|
|
233
|
+
// — the child_process broker synthesizes one — says so directly.
|
|
234
|
+
const captureOutput = !!nimbusCtx.__nimbusCaptureOutput
|
|
235
|
+
|| ctx.isFdTerminal?.(1) === false
|
|
236
|
+
|| ctx.isFdTerminal?.(2) === false;
|
|
237
|
+
const bundleProfile = parseFacetBundleProfile(nimbusCtx.__nimbusBundleProfile);
|
|
238
|
+
|
|
239
|
+
// ── Flag-span computation (primitive #1) ──
|
|
240
|
+
//
|
|
241
|
+
// Real-Node only treats args UP TO the first non-flag token as
|
|
242
|
+
// CLI flags. Pre-refactor, version/help/eval scanned the entire
|
|
243
|
+
// args array, breaking `node /path/to/tsc --version` (the user's
|
|
244
|
+
// --version was misinterpreted as a node flag).
|
|
245
|
+
let flagSpan = 0;
|
|
246
|
+
while (flagSpan < args.length && args[flagSpan].startsWith('-')) {
|
|
247
|
+
flagSpan++;
|
|
248
|
+
const prev = args[flagSpan - 1];
|
|
249
|
+
// -e / --eval consumes one value; advance past it.
|
|
250
|
+
if ((prev === '-e' || prev === '--eval') && flagSpan < args.length) {
|
|
251
|
+
flagSpan++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const flagSlice = args.slice(0, flagSpan);
|
|
255
|
+
|
|
256
|
+
// ── --version ──
|
|
257
|
+
if (flagSlice.includes('-v') || flagSlice.includes('--version')) {
|
|
258
|
+
ctx.stdout.write(spec.version + '\n');
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── --help ──
|
|
263
|
+
if (flagSlice.includes('--help') || flagSlice.includes('-h')) {
|
|
264
|
+
ctx.stdout.write(spec.helpText);
|
|
265
|
+
if (!spec.helpText.endsWith('\n')) ctx.stdout.write('\n');
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── -e / --eval ──
|
|
270
|
+
const evalIdx = flagSlice.indexOf('-e') !== -1
|
|
271
|
+
? flagSlice.indexOf('-e')
|
|
272
|
+
: flagSlice.indexOf('--eval');
|
|
273
|
+
if (evalIdx !== -1) {
|
|
274
|
+
const code = args[evalIdx + 1];
|
|
275
|
+
if (!code) {
|
|
276
|
+
ctx.stderr.write(`${name}: -e requires an argument\n`);
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
const result = await spec.run(code, {
|
|
280
|
+
cred: ctx.cred,
|
|
281
|
+
argv: args.slice(evalIdx + 2),
|
|
282
|
+
env: ctx.env,
|
|
283
|
+
cwd: ctx.cwd,
|
|
284
|
+
filename: '<eval>',
|
|
285
|
+
dirname: ctx.cwd || '/home/user',
|
|
286
|
+
command: `${name} -e ...`,
|
|
287
|
+
...(captureOutput ? { captureOutput: true } : {}),
|
|
288
|
+
...(bundleProfile ? { bundleProfile } : {}),
|
|
289
|
+
});
|
|
290
|
+
if (result.stdout) ctx.stdout.write(result.stdout);
|
|
291
|
+
if (result.stderr) ctx.stderr.write(result.stderr);
|
|
292
|
+
return result.exitCode;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── script path (or .wasm path for bypassesScriptRead) ──
|
|
296
|
+
const scriptIdx = flagSpan;
|
|
297
|
+
const scriptPath = args[scriptIdx];
|
|
298
|
+
if (!scriptPath) {
|
|
299
|
+
ctx.stderr.write(
|
|
300
|
+
`${name}: REPL not supported. Use ${name} -e "code" or ${name} script.js\n`,
|
|
301
|
+
);
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── bypassesScriptRead branch (wasm-runner) ──
|
|
306
|
+
//
|
|
307
|
+
// The runner takes the path AS-IS (it's a .wasm, not JS source).
|
|
308
|
+
// We don't read or transform here; the runner reads the bytes
|
|
309
|
+
// and instantiates them. `.` resolution and extension probing are
|
|
310
|
+
// meaningless for a .wasm target and stay out of this branch.
|
|
311
|
+
if (spec.bypassesScriptRead) {
|
|
312
|
+
const filename = '/' + resolveVfsPath(scriptPath, ctx.cwd || '/home/user');
|
|
313
|
+
const dirname = filename.includes('/')
|
|
314
|
+
? filename.substring(0, filename.lastIndexOf('/'))
|
|
315
|
+
: '/';
|
|
316
|
+
// `args.slice(scriptIdx + 1)` are the runner's user args (e.g.
|
|
317
|
+
// [exportName, intArg1, intArg2, ...] for wasm-runner).
|
|
318
|
+
const result = await spec.run('', {
|
|
319
|
+
cred: ctx.cred,
|
|
320
|
+
argv: args.slice(scriptIdx + 1),
|
|
321
|
+
env: ctx.env,
|
|
322
|
+
cwd: ctx.cwd,
|
|
323
|
+
filename,
|
|
324
|
+
dirname,
|
|
325
|
+
command: `${name} ${args.slice(0, scriptIdx + 1).join(' ')}`,
|
|
326
|
+
...(captureOutput ? { captureOutput: true } : {}),
|
|
327
|
+
...(bundleProfile ? { bundleProfile } : {}),
|
|
328
|
+
});
|
|
329
|
+
if (result.stdout) ctx.stdout.write(result.stdout);
|
|
330
|
+
if (result.stderr) ctx.stderr.write(result.stderr);
|
|
331
|
+
return result.exitCode;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Resolve against cwd: `.` → the package entry, then extension probing.
|
|
335
|
+
const resolvedPath = resolveRuntimeScriptPath(fs, ctx.cwd || '/home/user', scriptPath, {
|
|
336
|
+
// bun prefers .module over .main when both exist; node uses .main.
|
|
337
|
+
preferModuleField: name === 'bun',
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
let code: string | null = null;
|
|
341
|
+
if (resolvedPath !== null) {
|
|
342
|
+
try {
|
|
343
|
+
code = fs.readFileString(resolvedPath);
|
|
344
|
+
} catch { /* unreadable — reported below */ }
|
|
345
|
+
}
|
|
346
|
+
if (resolvedPath === null || code === null) {
|
|
347
|
+
ctx.stderr.write(`${name}: cannot find module '${scriptPath}'\n`);
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Shebang strip (primitive #1).
|
|
352
|
+
if (code.startsWith('#!')) {
|
|
353
|
+
const nl = code.indexOf('\n');
|
|
354
|
+
code = nl >= 0 ? code.substring(nl + 1) : '';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── ESM-source detection (primitive: type:module entry scripts) ──
|
|
358
|
+
//
|
|
359
|
+
// Nimbus's facet pre-compile loop wraps every entry script in
|
|
360
|
+
// `new Function(...)` which runs it as CJS. A real `node script.js`
|
|
361
|
+
// dispatch honours the nearest package.json's `"type"` field
|
|
362
|
+
// (and the file extension) to decide whether to parse as ESM:
|
|
363
|
+
//
|
|
364
|
+
// - .mjs → always ESM
|
|
365
|
+
// - .cjs → always CJS
|
|
366
|
+
// - .js → ESM iff nearest package.json has "type": "module"
|
|
367
|
+
// - no extension → same rule as .js. Node allows an extensionless
|
|
368
|
+
// main entry and resolves its format from the
|
|
369
|
+
// package type, and that is the shape of nearly
|
|
370
|
+
// every npm `bin` script (typescript's `bin/tsc`,
|
|
371
|
+
// and the `node_modules/.bin/<cli>` target the bin
|
|
372
|
+
// dispatcher hands us).
|
|
373
|
+
//
|
|
374
|
+
// Without this, every modern ESM-only npm initialiser
|
|
375
|
+
// (create-vite, create-astro, create-svelte, modern create-*)
|
|
376
|
+
// crashes immediately with "Cannot use import statement outside
|
|
377
|
+
// a module" because their bin entry is `index.js` and the
|
|
378
|
+
// package.json declares `type: module`.
|
|
379
|
+
//
|
|
380
|
+
// We transform to CJS (format: 'cjs') so the facet's `new
|
|
381
|
+
// Function()` runs it as a CJS module body — same path that the
|
|
382
|
+
// bundle's `transformEsmInBundle` (W3.5 Fix B) takes for
|
|
383
|
+
// sub-module ESM files. esbuild's CJS output emits __require /
|
|
384
|
+
// module.exports / exports.X so the facet's pre-compile loop
|
|
385
|
+
// sees ordinary CJS source.
|
|
386
|
+
function nearestPackageTypeIsModule(absPath: string): boolean {
|
|
387
|
+
// Walk up dirs looking for the nearest package.json. First one
|
|
388
|
+
// wins (Node spec); we do NOT consult ancestors past it.
|
|
389
|
+
let dir = absPath.replace(/^\/+/, '');
|
|
390
|
+
const slash = dir.lastIndexOf('/');
|
|
391
|
+
dir = slash > 0 ? dir.substring(0, slash) : '';
|
|
392
|
+
const visited = new Set<string>();
|
|
393
|
+
while (dir && !visited.has(dir)) {
|
|
394
|
+
visited.add(dir);
|
|
395
|
+
const pj = dir + '/package.json';
|
|
396
|
+
if (fs.exists(pj)) {
|
|
397
|
+
try {
|
|
398
|
+
const pkg = JSON.parse(fs.readFileString(pj));
|
|
399
|
+
return pkg && pkg.type === 'module';
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const last = dir.lastIndexOf('/');
|
|
405
|
+
if (last <= 0) break;
|
|
406
|
+
dir = dir.substring(0, last);
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const scriptExt = vfsPathExtension(resolvedPath);
|
|
412
|
+
const needsEsmTransform =
|
|
413
|
+
scriptExt === '.mjs' ||
|
|
414
|
+
((scriptExt === '.js' || scriptExt === '') && nearestPackageTypeIsModule(resolvedPath));
|
|
415
|
+
|
|
416
|
+
// esbuild transform for TypeScript / TSX / JSX (both node and bun)
|
|
417
|
+
// AND for ESM entry scripts (primitive ESM-detect).
|
|
418
|
+
if (
|
|
419
|
+
scriptExt === '.ts' ||
|
|
420
|
+
scriptExt === '.tsx' ||
|
|
421
|
+
scriptExt === '.jsx' ||
|
|
422
|
+
needsEsmTransform
|
|
423
|
+
) {
|
|
424
|
+
try {
|
|
425
|
+
const eb = await getEsbuild();
|
|
426
|
+
const loader =
|
|
427
|
+
scriptExt === '.tsx' ? 'tsx' :
|
|
428
|
+
scriptExt === '.jsx' ? 'jsx' :
|
|
429
|
+
scriptExt === '.ts' ? 'ts' :
|
|
430
|
+
'js';
|
|
431
|
+
// Substitute `import.meta.url` at compile-time so esbuild's
|
|
432
|
+
// CJS output doesn't reduce it to `undefined` (its default
|
|
433
|
+
// for unknown import.meta references). The substitution
|
|
434
|
+
// value is a real `file://<absolute-path>` URL — exactly
|
|
435
|
+
// what real Node returns when running this script. Tools
|
|
436
|
+
// that compute `fileURLToPath(import.meta.url)` (create-vite,
|
|
437
|
+
// most modern CLIs) then resolve relative paths against
|
|
438
|
+
// the actual script location.
|
|
439
|
+
//
|
|
440
|
+
// Without this, `create-vite` does
|
|
441
|
+
// r(import.meta.url) → fileURLToPath(undefined) → throws
|
|
442
|
+
// → falls into a different code path
|
|
443
|
+
// → readdirSync(wrong-template-dir) returns []
|
|
444
|
+
// → "Scaffolding..." but writes no files.
|
|
445
|
+
const absUrl = 'file:///' + resolvedPath.replace(/^\/+/, '');
|
|
446
|
+
const transformed = await eb.transform(code, {
|
|
447
|
+
loader,
|
|
448
|
+
format: 'cjs',
|
|
449
|
+
define: importMetaDefines(absUrl),
|
|
450
|
+
});
|
|
451
|
+
code = bindImportMetaResolve(transformed.code, absUrl);
|
|
452
|
+
} catch (e: any) {
|
|
453
|
+
ctx.stderr.write(`${name}: transform error for ${scriptPath}: ${e?.message}\n`);
|
|
454
|
+
return 1;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const filename = '/' + resolvedPath;
|
|
459
|
+
const dirname = filename.includes('/')
|
|
460
|
+
? filename.substring(0, filename.lastIndexOf('/'))
|
|
461
|
+
: '/';
|
|
462
|
+
|
|
463
|
+
// Primitive #1 / G4 — propagate bin-spawn ctx if the runtime
|
|
464
|
+
// supports it (currently node only).
|
|
465
|
+
const binSpawn = spec.supportsBinSpawn ? nimbusCtx.__nimbusBinSpawn : undefined;
|
|
466
|
+
|
|
467
|
+
const leadingFlags = args.slice(0, scriptIdx);
|
|
468
|
+
const result = await spec.run(code, {
|
|
469
|
+
cred: ctx.cred,
|
|
470
|
+
argv: [...leadingFlags, filename, ...args.slice(scriptIdx + 1)],
|
|
471
|
+
env: ctx.env,
|
|
472
|
+
cwd: ctx.cwd,
|
|
473
|
+
filename,
|
|
474
|
+
dirname,
|
|
475
|
+
command:
|
|
476
|
+
binSpawn?.command || `${name} ${args.slice(0, scriptIdx + 1).join(' ')}`,
|
|
477
|
+
...(binSpawn ? {
|
|
478
|
+
skipSpawn: true,
|
|
479
|
+
callerPid: binSpawn.callerPid,
|
|
480
|
+
forceLongRunning: binSpawn.forceLongRunning === true,
|
|
481
|
+
attachedTty: binSpawn.attachedTty === true,
|
|
482
|
+
} : {}),
|
|
483
|
+
...(captureOutput ? { captureOutput: true } : {}),
|
|
484
|
+
...(bundleProfile ? { bundleProfile } : {}),
|
|
485
|
+
});
|
|
486
|
+
if (result.stdout) ctx.stdout.write(result.stdout);
|
|
487
|
+
if (result.stderr) ctx.stderr.write(result.stderr);
|
|
488
|
+
return result.exitCode;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return async function runtimeHandler(ctx: any): Promise<number> {
|
|
492
|
+
const args: string[] = ctx.args || [];
|
|
493
|
+
|
|
494
|
+
// ── Subcommand dispatch ──
|
|
495
|
+
//
|
|
496
|
+
// BEFORE flag-span computation: subcommands like `bun install`
|
|
497
|
+
// have their first positional arg as the verb, NOT a node-style
|
|
498
|
+
// flag. A verb owns the whole invocation, but it may hand a
|
|
499
|
+
// rewritten argv back to the standard flow — that is how
|
|
500
|
+
// `bun run <file>` reaches the same execution path as `bun <file>`.
|
|
501
|
+
if (spec.subcommands && args.length > 0 && spec.subcommands[args[0]]) {
|
|
502
|
+
return spec.subcommands[args[0]](
|
|
503
|
+
ctx,
|
|
504
|
+
registry,
|
|
505
|
+
(rewritten: string[]) => runtimeInvocation(ctx, rewritten),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return runtimeInvocation(ctx, args);
|
|
510
|
+
};
|
|
511
|
+
}
|
|
@@ -198,7 +198,21 @@ export function snapshotVfs(
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
return {
|
|
201
|
-
snapshot: {
|
|
201
|
+
snapshot: {
|
|
202
|
+
root,
|
|
203
|
+
roots,
|
|
204
|
+
preopens: [],
|
|
205
|
+
files,
|
|
206
|
+
dirs: Array.from(dirsSet).sort(),
|
|
207
|
+
modes,
|
|
208
|
+
// Only when the walk hid nothing. The walk either finishes or returns an
|
|
209
|
+
// error above, so a snapshot taken with no skip list HAS listed each root
|
|
210
|
+
// exhaustively — which is what lets a guest treat a path it does not find
|
|
211
|
+
// as absent instead of asking for it. With the default skip list it has
|
|
212
|
+
// not, and claiming otherwise would make node_modules invisible rather
|
|
213
|
+
// than merely unseeded.
|
|
214
|
+
...(skipSubdirs.size === 0 ? { enumeratedRoots: roots } : {}),
|
|
215
|
+
},
|
|
202
216
|
bytes: totalBytes,
|
|
203
217
|
files: fileCount,
|
|
204
218
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vfs-supervisor.ts — the session's syscall capability, served in place.
|
|
3
|
+
*
|
|
4
|
+
* The WASI layer treats its seed as a CACHE over the session filesystem and
|
|
5
|
+
* reaches the real thing through a {@link WasiSupervisorStub}. In a Durable
|
|
6
|
+
* Object that stub is an RPC handle minted for a pid, because the facet is a
|
|
7
|
+
* different isolate; in the caller's own isolate the filesystem is right here
|
|
8
|
+
* and the credential is already on the view.
|
|
9
|
+
*
|
|
10
|
+
* The methods are `async` because the shim's contract is, not because anything
|
|
11
|
+
* here waits. That is what makes this usable on a host with no JSPI: every
|
|
12
|
+
* mutation is queued and drained OUTSIDE the guest (`__wasiDrainPersist`, which
|
|
13
|
+
* a runner awaits after the program returns), so a promise there costs nothing.
|
|
14
|
+
* A READ is different — its promise would have to suspend the guest mid-syscall
|
|
15
|
+
* — which is why a host without parking must seed the filesystem completely and
|
|
16
|
+
* never reach the read paths at all. See FacetHost.parking.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { CredentialedVfs } from '../vfs/sqlite-vfs.js';
|
|
20
|
+
import type { WasiStatResult, WasiSupervisorStub } from './wasi/types.js';
|
|
21
|
+
|
|
22
|
+
/** Serve the WASI syscall surface directly from `vfs`, as its own credential. */
|
|
23
|
+
export function vfsSupervisor(vfs: CredentialedVfs): WasiSupervisorStub {
|
|
24
|
+
const key = (path: string): string => path.replace(/^\/+/, '');
|
|
25
|
+
return {
|
|
26
|
+
async fsReadRange(vfsPath, offset, length) {
|
|
27
|
+
return vfs.readRange(key(vfsPath), offset, length);
|
|
28
|
+
},
|
|
29
|
+
async fsRevision(root) {
|
|
30
|
+
return vfs.revision(key(root));
|
|
31
|
+
},
|
|
32
|
+
async stat(vfsPath): Promise<WasiStatResult | null> {
|
|
33
|
+
const path = key(vfsPath);
|
|
34
|
+
if (!vfs.exists(path)) return null;
|
|
35
|
+
const stat = vfs.lstat(path);
|
|
36
|
+
return {
|
|
37
|
+
type: stat.type === 'directory' ? 'directory' : stat.type === 'symlink' ? 'symlink' : 'file',
|
|
38
|
+
size: stat.size,
|
|
39
|
+
mtime: stat.mtime,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
async writeFile(vfsPath, bytes) {
|
|
43
|
+
const path = key(vfsPath);
|
|
44
|
+
const parent = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '';
|
|
45
|
+
if (parent && !vfs.exists(parent)) vfs.mkdir(parent, { recursive: true });
|
|
46
|
+
vfs.writeFile(path, bytes);
|
|
47
|
+
},
|
|
48
|
+
async unlink(vfsPath) {
|
|
49
|
+
vfs.unlink(key(vfsPath));
|
|
50
|
+
},
|
|
51
|
+
async mkdir(vfsPath) {
|
|
52
|
+
vfs.mkdir(key(vfsPath), { recursive: true });
|
|
53
|
+
},
|
|
54
|
+
async rmdir(vfsPath) {
|
|
55
|
+
vfs.rmdir(key(vfsPath));
|
|
56
|
+
},
|
|
57
|
+
async rename(from, to) {
|
|
58
|
+
vfs.rename(key(from), key(to));
|
|
59
|
+
},
|
|
60
|
+
async symlink(target, vfsPath) {
|
|
61
|
+
vfs.symlink(target, key(vfsPath));
|
|
62
|
+
},
|
|
63
|
+
async utimes(vfsPath, atimeMs, mtimeMs) {
|
|
64
|
+
vfs.utimes(key(vfsPath), atimeMs, mtimeMs);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|