wasm-idle 1.1.0 → 1.1.2

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 (145) hide show
  1. package/README.md +140 -37
  2. package/dist/playground/assetBridge.js +106 -31
  3. package/dist/playground/assets.d.ts +2 -2
  4. package/dist/playground/c3.js +22 -0
  5. package/dist/playground/c3Memory.d.ts +19 -0
  6. package/dist/playground/c3Memory.js +117 -0
  7. package/dist/playground/cachedClang.js +143 -34
  8. package/dist/playground/clangAssetIntegrity.js +6 -6
  9. package/dist/playground/clangWorkerProtocol.d.ts +12 -1
  10. package/dist/playground/cobol.d.ts +3 -0
  11. package/dist/playground/cobol.js +10 -1
  12. package/dist/playground/commonlispAssets.d.ts +2 -2
  13. package/dist/playground/compiledArtifactCache.d.ts +3 -1
  14. package/dist/playground/compiledArtifactCache.js +31 -5
  15. package/dist/playground/container/ContainerExplorer.svelte +307 -0
  16. package/dist/playground/container/ContainerExplorer.svelte.d.ts +14 -0
  17. package/dist/playground/container/binding.d.ts +26 -0
  18. package/dist/playground/container/binding.js +247 -0
  19. package/dist/playground/container/clang-runtime.d.ts +38 -0
  20. package/dist/playground/container/clang-runtime.js +343 -0
  21. package/dist/playground/container/client.d.ts +15 -0
  22. package/dist/playground/container/client.js +189 -0
  23. package/dist/playground/container/explorer.d.ts +31 -0
  24. package/dist/playground/container/explorer.js +112 -0
  25. package/dist/playground/container/owner.worker.js +390 -0
  26. package/dist/playground/container/protocol.d.ts +73 -0
  27. package/dist/playground/container/protocol.js +28 -0
  28. package/dist/playground/container/release.d.ts +31 -0
  29. package/dist/playground/container/release.js +612 -0
  30. package/dist/playground/container/runtime-adapter.d.ts +73 -0
  31. package/dist/playground/container/runtime-adapter.js +474 -0
  32. package/dist/playground/container/shell-control.d.ts +49 -0
  33. package/dist/playground/container/shell-control.js +303 -0
  34. package/dist/playground/container/workspace.d.ts +8 -0
  35. package/dist/playground/container/workspace.js +29 -0
  36. package/dist/playground/d.d.ts +6 -1
  37. package/dist/playground/d.js +83 -5
  38. package/dist/playground/dotnet.js +6 -8
  39. package/dist/playground/fortran.d.ts +3 -0
  40. package/dist/playground/fortran.js +10 -1
  41. package/dist/playground/go.js +14 -3
  42. package/dist/playground/goDebugInstrumentation.js +184 -54
  43. package/dist/playground/java.js +3 -2
  44. package/dist/playground/javaRuntimeErrors.d.ts +9 -0
  45. package/dist/playground/javaRuntimeErrors.js +60 -0
  46. package/dist/playground/lean4Assets.d.ts +4 -4
  47. package/dist/playground/lfortran.js +10 -0
  48. package/dist/playground/lfortranAssets.d.ts +2 -2
  49. package/dist/playground/nim.js +22 -0
  50. package/dist/playground/objectivec.d.ts +3 -0
  51. package/dist/playground/objectivec.js +11 -2
  52. package/dist/playground/options.d.ts +2 -0
  53. package/dist/playground/python.js +8 -0
  54. package/dist/playground/runtimeAssetCache.d.ts +1 -32
  55. package/dist/playground/runtimeAssetCache.js +1 -193
  56. package/dist/playground/runtimeSession.d.ts +2 -2
  57. package/dist/playground/runtimeSession.js +11 -11
  58. package/dist/playground/rust.d.ts +6 -0
  59. package/dist/playground/rust.js +103 -6
  60. package/dist/playground/staticStdinRing.d.ts +2 -1
  61. package/dist/playground/staticStdinRing.js +21 -10
  62. package/dist/playground/staticWorkerRuntime.d.ts +15 -1
  63. package/dist/playground/staticWorkerRuntime.js +119 -17
  64. package/dist/playground/tinygo.d.ts +3 -0
  65. package/dist/playground/tinygo.js +81 -6
  66. package/dist/playground/v.d.ts +3 -0
  67. package/dist/playground/v.js +10 -1
  68. package/dist/playground/wasmAwkVersion.d.ts +13 -13
  69. package/dist/playground/wasmAwkVersion.js +4 -4
  70. package/dist/playground/wasmBqnVersion.d.ts +2 -2
  71. package/dist/playground/wasmBqnVersion.js +2 -2
  72. package/dist/playground/wasmC3Version.d.ts +2 -2
  73. package/dist/playground/wasmC3Version.js +2 -2
  74. package/dist/playground/wasmClojureScriptVersion.d.ts +2 -2
  75. package/dist/playground/wasmClojureScriptVersion.js +2 -2
  76. package/dist/playground/wasmCommonLispVersion.d.ts +2 -2
  77. package/dist/playground/wasmCommonLispVersion.js +2 -2
  78. package/dist/playground/wasmDIntegrity.d.ts +5 -5
  79. package/dist/playground/wasmDIntegrity.js +5 -5
  80. package/dist/playground/wasmDVersion.d.ts +16 -1
  81. package/dist/playground/wasmDVersion.js +16 -1
  82. package/dist/playground/wasmDebugVersion.d.ts +1 -1
  83. package/dist/playground/wasmDebugVersion.js +1 -1
  84. package/dist/playground/wasmForthVersion.d.ts +2 -2
  85. package/dist/playground/wasmForthVersion.js +2 -2
  86. package/dist/playground/wasmGleamVersion.d.ts +2 -2
  87. package/dist/playground/wasmGleamVersion.js +2 -2
  88. package/dist/playground/wasmGrainVersion.d.ts +2 -2
  89. package/dist/playground/wasmGrainVersion.js +2 -2
  90. package/dist/playground/wasmJVersion.d.ts +2 -2
  91. package/dist/playground/wasmJVersion.js +2 -2
  92. package/dist/playground/wasmJanetVersion.js +2 -2
  93. package/dist/playground/wasmJuliaVersion.js +2 -2
  94. package/dist/playground/wasmLean4Version.d.ts +5 -5
  95. package/dist/playground/wasmLean4Version.js +5 -5
  96. package/dist/playground/wasmLfortranVersion.d.ts +2 -2
  97. package/dist/playground/wasmLfortranVersion.js +2 -2
  98. package/dist/playground/wasmNimVersion.d.ts +9 -9
  99. package/dist/playground/wasmNimVersion.js +6 -6
  100. package/dist/playground/wasmOcamlVersion.d.ts +2 -2
  101. package/dist/playground/wasmOcamlVersion.js +1 -1
  102. package/dist/playground/wasmPascalVersion.js +2 -2
  103. package/dist/playground/wasmPerlVersion.js +2 -2
  104. package/dist/playground/wasmPrologVersion.d.ts +2 -2
  105. package/dist/playground/wasmPrologVersion.js +2 -2
  106. package/dist/playground/wasmReScriptVersion.d.ts +2 -2
  107. package/dist/playground/wasmReScriptVersion.js +2 -2
  108. package/dist/playground/wasmRustVersion.d.ts +8 -8
  109. package/dist/playground/wasmRustVersion.js +7 -7
  110. package/dist/playground/wasmTclVersion.d.ts +4 -4
  111. package/dist/playground/wasmTclVersion.js +2 -2
  112. package/dist/playground/wasmTinyGoVersion.d.ts +14 -14
  113. package/dist/playground/wasmTinyGoVersion.js +14 -14
  114. package/dist/playground/wasmTypeScriptVersion.d.ts +5 -5
  115. package/dist/playground/wasmTypeScriptVersion.js +5 -5
  116. package/dist/playground/worker/cachedClangCompile.js +52 -4
  117. package/dist/playground/worker/cachedClangExecute.js +6 -5
  118. package/dist/playground/worker/clang.js +32 -10
  119. package/dist/playground/worker/cobol.js +2 -2
  120. package/dist/playground/worker/d.js +12 -5
  121. package/dist/playground/worker/fortran.js +2 -2
  122. package/dist/playground/worker/go.js +16 -5
  123. package/dist/playground/worker/haskell.js +10 -7
  124. package/dist/playground/worker/java.js +75 -10
  125. package/dist/playground/worker/lua.js +2 -3
  126. package/dist/playground/worker/php.js +15 -9
  127. package/dist/playground/worker/python.js +46 -9
  128. package/dist/playground/worker/pythonExecution.js +17 -1
  129. package/dist/playground/worker/pythonWorkspaceFiles.d.ts +6 -0
  130. package/dist/playground/worker/pythonWorkspaceFiles.js +26 -0
  131. package/dist/playground/worker/pythonWorkspaceImports.d.ts +4 -0
  132. package/dist/playground/worker/pythonWorkspaceImports.js +129 -0
  133. package/dist/playground/worker/r.js +13 -6
  134. package/dist/playground/worker/rust.js +34 -2
  135. package/dist/playground/worker/sqlite.js +4 -3
  136. package/dist/playground/worker/tinygo.js +11 -3
  137. package/dist/playground/worker/typescript.js +2 -3
  138. package/dist/playground/worker/v.js +2 -2
  139. package/dist/playground/worker/wat.js +2 -3
  140. package/dist/playground/worker/zig.js +20 -6
  141. package/dist/playground/workerSession.d.ts +1 -28
  142. package/dist/playground/workerSession.js +1 -164
  143. package/package.json +20 -13
  144. package/dist/playground/worker/python.runtime.cases.js +0 -374
  145. /package/dist/playground/{worker/python.runtime.cases.d.ts → container/owner.worker.d.ts} +0 -0
@@ -0,0 +1,73 @@
1
+ import { type ProgressLike, type WorkspaceFile } from '@wasm-idle/core';
2
+ import { type Container, type ContainerProcess, type LiveCommandAdapter, type MemoryFileSystem } from '@wasm-idle/container';
3
+ import { createPlaygroundBinding } from '../index';
4
+ import type { SandboxRuntimeAssets } from '../sandbox';
5
+ import type { SandboxExecutionOptions } from '../options';
6
+ import type { ContainerWorkerEvent, ContainerWorkspaceEntry } from './protocol';
7
+ interface Job {
8
+ id: string;
9
+ language: string;
10
+ method: 'load' | 'prepare' | 'run';
11
+ requestId: number;
12
+ code: string;
13
+ log: boolean;
14
+ args: string[];
15
+ options: SandboxExecutionOptions;
16
+ signal: AbortSignal;
17
+ progress?: ProgressLike;
18
+ result?: boolean | string;
19
+ error?: unknown;
20
+ process?: ContainerProcess;
21
+ writer?: WritableStreamDefaultWriter<Uint8Array>;
22
+ output?: (data: string) => void;
23
+ readyForInput?: boolean;
24
+ }
25
+ interface RuntimeAdapterOptions {
26
+ runtimeAssets: SandboxRuntimeAssets;
27
+ emit(event: ContainerWorkerEvent): void;
28
+ /** Only a trusted local app factory is used in contract tests. */
29
+ createBinding?: typeof createPlaygroundBinding;
30
+ }
31
+ /** Existing real engines keep their private runtime FS; their source snapshot is the common VFS. */
32
+ export declare class WebsiteRuntimeAdapter {
33
+ #private;
34
+ private readonly settings;
35
+ readonly adapter: LiveCommandAdapter;
36
+ readonly commands: {
37
+ "website-playground-engine": {
38
+ adapter: string;
39
+ bytes: Uint8Array<ArrayBuffer>;
40
+ argv0: string;
41
+ };
42
+ };
43
+ readonly filesystemProfile = "shared-source-snapshot; runtime-private-files-not-exported";
44
+ constructor(settings: RuntimeAdapterOptions);
45
+ attach(container: Container): void;
46
+ get fs(): MemoryFileSystem;
47
+ directory(language: string): string;
48
+ private session;
49
+ syncWorkspace(language: string, files: readonly WorkspaceFile[], expectedBaseFiles?: readonly WorkspaceFile[]): void;
50
+ readWorkspace(language: string): WorkspaceFile[];
51
+ /** Enumerate the common namespace without copying file bodies or following links. */
52
+ listWorkspaceFiles(): ContainerWorkspaceEntry[];
53
+ inputs(language: string, code: string, options: SandboxExecutionOptions): {
54
+ code: string;
55
+ options: SandboxExecutionOptions;
56
+ };
57
+ execute(method: Job['method'], language: string, code: string | undefined, log: boolean | undefined, args: string[] | undefined, options: SandboxExecutionOptions | undefined, signal: AbortSignal, progress?: ProgressLike, requestId?: number): Promise<string | boolean | undefined>;
58
+ private flushInput;
59
+ write(language: string, value: string): Promise<void>;
60
+ eof(language: string): Promise<void>;
61
+ stop(language: string): Promise<void>;
62
+ clear(language: string): Promise<void>;
63
+ restart(language: string): Promise<void>;
64
+ debug(language: string, method: string, args: unknown[]): Promise<any>;
65
+ inspect(): {
66
+ language: string;
67
+ pid: number | undefined;
68
+ phase: "load" | "run" | "prepare";
69
+ filesystem: string;
70
+ }[];
71
+ dispose(): Promise<void>;
72
+ }
73
+ export {};
@@ -0,0 +1,474 @@
1
+ import { normalizeLanguageId, normalizeWorkspacePath, validateWorkspaceFiles } from '@wasm-idle/core';
2
+ import { ContainerError } from '@wasm-idle/container';
3
+ import { createPlaygroundBinding } from '../index';
4
+ const COMMAND = 'website-playground-engine';
5
+ const ADAPTER = 'website-playground-host-v1';
6
+ const encoder = new TextEncoder();
7
+ const decoder = new TextDecoder();
8
+ const defaultSource = (language) => ({
9
+ C: 'main.c',
10
+ CPP: 'main.cpp',
11
+ PYTHON: 'main.py',
12
+ PYTHON3: 'main.py',
13
+ JAVA: 'Main.java',
14
+ RUST: 'main.rs',
15
+ GO: 'main.go',
16
+ JAVASCRIPT: 'main.js',
17
+ TYPESCRIPT: 'main.ts'
18
+ })[language] ?? 'main.txt';
19
+ /** Existing real engines keep their private runtime FS; their source snapshot is the common VFS. */
20
+ export class WebsiteRuntimeAdapter {
21
+ settings;
22
+ adapter;
23
+ commands = {
24
+ [COMMAND]: { adapter: ADAPTER, bytes: new Uint8Array(), argv0: COMMAND }
25
+ };
26
+ filesystemProfile = 'shared-source-snapshot; runtime-private-files-not-exported';
27
+ #container;
28
+ #sessions = new Map();
29
+ #jobs = new Map();
30
+ #published = new Map();
31
+ #nextJob = 0;
32
+ #disposed = false;
33
+ constructor(settings) {
34
+ this.settings = settings;
35
+ this.adapter = {
36
+ id: ADAPTER,
37
+ filesystem: 'live',
38
+ start: async (request) => {
39
+ const job = this.#jobs.get(request.invocation.args[0]);
40
+ if (!job ||
41
+ request.invocation.args.length !== 1 ||
42
+ request.fs !== this.fs ||
43
+ request.invocation.cwd !== this.directory(job.language))
44
+ throw new ContainerError('INVALID_ARGUMENT', 'Unknown application runtime job');
45
+ let output, errors;
46
+ let closed = false, bytes = 0;
47
+ const stdout = new ReadableStream({
48
+ start(controller) {
49
+ output = controller;
50
+ }
51
+ });
52
+ const stderr = new ReadableStream({
53
+ start(controller) {
54
+ errors = controller;
55
+ }
56
+ });
57
+ const session = this.session(job.language);
58
+ job.output = (data) => {
59
+ if (closed || request.signal.aborted)
60
+ return;
61
+ const chunk = encoder.encode(data);
62
+ bytes += chunk.byteLength;
63
+ if (bytes > request.limits.maxOutputBytes) {
64
+ job.error = new ContainerError('RESOURCE_LIMIT', 'Runtime output exceeds the container limit');
65
+ void this.stop(job.language).catch(() => { });
66
+ return;
67
+ }
68
+ if (chunk.byteLength)
69
+ output.enqueue(chunk);
70
+ };
71
+ const abort = () => {
72
+ void session.sandbox.then((sandbox) => sandbox.terminate()).catch(() => { });
73
+ };
74
+ request.signal.addEventListener('abort', abort, { once: true });
75
+ return {
76
+ stdin: new WritableStream({
77
+ write: (chunk) => this.write(job.language, decoder.decode(chunk, { stream: true })),
78
+ close: () => (job.method === 'run' ? this.eof(job.language) : undefined)
79
+ }),
80
+ stdout,
81
+ stderr,
82
+ run: async () => {
83
+ try {
84
+ const sandbox = await session.sandbox;
85
+ request.signal.throwIfAborted();
86
+ const options = { ...job.options, signal: request.signal };
87
+ if (job.method === 'load') {
88
+ await sandbox.load(job.code, job.log, job.args, options, job.progress);
89
+ job.result = true;
90
+ }
91
+ else {
92
+ const running = sandbox.run(job.code, job.method === 'prepare', job.log, job.progress, job.args, options);
93
+ if (job.method === 'run') {
94
+ job.readyForInput = true;
95
+ await this.flushInput(session, sandbox);
96
+ }
97
+ job.result = await running;
98
+ }
99
+ if (job.error)
100
+ throw job.error;
101
+ return { code: job.result === false ? 1 : 0 };
102
+ }
103
+ catch (error) {
104
+ job.error = error;
105
+ throw error;
106
+ }
107
+ },
108
+ dispose: async () => {
109
+ if (closed)
110
+ return;
111
+ closed = true;
112
+ request.signal.removeEventListener('abort', abort);
113
+ job.readyForInput = false;
114
+ if (request.signal.aborted)
115
+ await session.sandbox
116
+ .then((sandbox) => sandbox.terminate())
117
+ .catch(() => { });
118
+ output.close();
119
+ errors.close();
120
+ }
121
+ };
122
+ }
123
+ };
124
+ }
125
+ attach(container) {
126
+ if (this.#container)
127
+ throw new Error('Runtime adapter is already attached');
128
+ this.#container = container;
129
+ }
130
+ get fs() {
131
+ if (!this.#container)
132
+ throw new Error('Container not attached');
133
+ return this.#container.fs;
134
+ }
135
+ directory(language) {
136
+ if (typeof language !== 'string' ||
137
+ !/^[A-Z][A-Z0-9_]{0,63}$/u.test(language) ||
138
+ !normalizeLanguageId(language))
139
+ throw new TypeError('Unsupported workspace language');
140
+ const path = `/workspace/${language}`;
141
+ this.fs.mkdir(path, { recursive: true });
142
+ return path;
143
+ }
144
+ session(language) {
145
+ if (this.#disposed)
146
+ throw new Error('Runtime owner disposed');
147
+ let session = this.#sessions.get(language);
148
+ if (session)
149
+ return session;
150
+ const binding = (this.settings.createBinding ?? createPlaygroundBinding)(this.settings.runtimeAssets);
151
+ session = {
152
+ binding,
153
+ sandbox: undefined,
154
+ pendingInput: [],
155
+ pendingBytes: 0,
156
+ pendingEof: false
157
+ };
158
+ const owner = session;
159
+ session.sandbox = binding.load(language).then((value) => {
160
+ const sandbox = value;
161
+ sandbox.output = (data) => owner.active?.output?.(data);
162
+ for (const [property, name] of [
163
+ ['ondebug', 'debug'],
164
+ ['oncompilerdiagnostic', 'diagnostic'],
165
+ ['image', 'image']
166
+ ])
167
+ sandbox[property] = (data) => this.settings.emit({
168
+ language,
169
+ name,
170
+ data,
171
+ requestId: owner.active?.requestId
172
+ });
173
+ return sandbox;
174
+ });
175
+ void session.sandbox.catch(() => { });
176
+ this.#sessions.set(language, session);
177
+ return session;
178
+ }
179
+ syncWorkspace(language, files, expectedBaseFiles) {
180
+ const path = this.directory(language);
181
+ const validated = validateWorkspaceFiles(files, {
182
+ maxFiles: 2048,
183
+ maxFileBytes: 16 * 1024 * 1024,
184
+ maxTotalBytes: 64 * 1024 * 1024,
185
+ caseSensitive: true
186
+ });
187
+ const next = new Set(validated.map((file) => file.path));
188
+ const asBytes = (content) => typeof content === 'string' ? encoder.encode(content) : content;
189
+ const equal = (left, right) => left === undefined || right === undefined
190
+ ? left === right
191
+ : left.length === right.length &&
192
+ left.every((byte, index) => byte === right[index]);
193
+ const current = (name) => {
194
+ try {
195
+ return this.fs.readFile(name, { root: path, cwd: path });
196
+ }
197
+ catch (error) {
198
+ if (error.code !== 'ENOENT')
199
+ throw error;
200
+ return undefined;
201
+ }
202
+ };
203
+ const base = expectedBaseFiles === undefined
204
+ ? undefined
205
+ : new Map(validateWorkspaceFiles(expectedBaseFiles, {
206
+ maxFiles: 2048,
207
+ maxFileBytes: 16 * 1024 * 1024,
208
+ maxTotalBytes: 64 * 1024 * 1024,
209
+ caseSensitive: true
210
+ }).map((file) => [file.path, asBytes(file.content)]));
211
+ const previousPaths = base?.keys() ?? this.#published.get(language) ?? [];
212
+ const deletions = [...previousPaths].filter((previous) => !next.has(previous));
213
+ // Compare all touched files before publishing even the first edit. Shell-created
214
+ // paths outside the editor's baseline are retained unless explicitly supplied.
215
+ if (base) {
216
+ for (const file of validated) {
217
+ const before = base.get(file.path), after = asBytes(file.content), present = current(file.path);
218
+ if (!equal(before, after) && !equal(present, before) && !equal(present, after))
219
+ throw new ContainerError('FILESYSTEM_CONFLICT', `Workspace changed while editing ${file.path}`);
220
+ }
221
+ for (const file of deletions)
222
+ if (current(file) !== undefined && !equal(current(file), base.get(file)))
223
+ throw new ContainerError('FILESYSTEM_CONFLICT', `Workspace changed before deleting ${file}`);
224
+ }
225
+ for (const previous of deletions) {
226
+ try {
227
+ this.fs.unlink(previous, { root: path, cwd: path });
228
+ }
229
+ catch (error) {
230
+ if (error.code !== 'ENOENT')
231
+ throw error;
232
+ }
233
+ }
234
+ for (const file of validated) {
235
+ // An unchanged editor buffer must not overwrite a newer shell revision.
236
+ if (base?.has(file.path) && equal(base.get(file.path), asBytes(file.content)))
237
+ continue;
238
+ const parent = file.path.split('/').slice(0, -1).join('/');
239
+ if (parent)
240
+ this.fs.mkdir(parent, { recursive: true, root: path, cwd: path });
241
+ const bytes = typeof file.content === 'string' ? encoder.encode(file.content) : file.content;
242
+ let previous;
243
+ try {
244
+ previous = this.fs.readFile(file.path, { root: path, cwd: path });
245
+ }
246
+ catch (error) {
247
+ if (error.code !== 'ENOENT')
248
+ throw error;
249
+ }
250
+ if (previous?.length === bytes.length &&
251
+ previous.every((byte, index) => byte === bytes[index]))
252
+ continue;
253
+ this.fs.writeFile(file.path, bytes, { root: path, cwd: path });
254
+ }
255
+ this.#published.set(language, next);
256
+ }
257
+ readWorkspace(language) {
258
+ const root = this.directory(language), files = [];
259
+ const visit = (path) => {
260
+ for (const name of this.fs.readdir(path || '.', { root, cwd: root })) {
261
+ const relative = path ? `${path}/${name}` : name;
262
+ const stat = this.fs.lstat(relative, { root, cwd: root });
263
+ if (stat.type === 'directory')
264
+ visit(relative);
265
+ else if (stat.type === 'file')
266
+ files.push({
267
+ path: relative,
268
+ content: this.fs.readFile(relative, { root, cwd: root })
269
+ });
270
+ }
271
+ };
272
+ visit('');
273
+ return files;
274
+ }
275
+ /** Enumerate the common namespace without copying file bodies or following links. */
276
+ listWorkspaceFiles() {
277
+ const entries = [];
278
+ const options = { root: '/workspace', cwd: '/workspace' };
279
+ const visit = (parent) => {
280
+ for (const name of this.fs.readdir(parent || '.', options)) {
281
+ const path = parent ? `${parent}/${name}` : name;
282
+ const stat = this.fs.lstat(path, options);
283
+ if (stat.type !== 'file' && stat.type !== 'directory' && stat.type !== 'symlink')
284
+ continue;
285
+ entries.push({ path, type: stat.type, size: stat.size });
286
+ if (stat.type === 'directory')
287
+ visit(path);
288
+ }
289
+ };
290
+ visit('');
291
+ return entries;
292
+ }
293
+ inputs(language, code, options) {
294
+ const root = this.directory(language);
295
+ const activePath = normalizeWorkspacePath(options.activePath ?? defaultSource(language));
296
+ try {
297
+ this.fs.stat(activePath, { root, cwd: root });
298
+ }
299
+ catch (error) {
300
+ if (error.code !== 'ENOENT')
301
+ throw error;
302
+ this.syncWorkspace(language, [
303
+ ...new Map([...(options.workspaceFiles ?? []), { path: activePath, content: code }].map((entry) => [entry.path, entry])).values()
304
+ ]);
305
+ }
306
+ const files = this.readWorkspace(language);
307
+ return {
308
+ code: new TextDecoder('utf-8', { fatal: true }).decode(this.fs.readFile(activePath, { root, cwd: root })),
309
+ options: {
310
+ ...options,
311
+ activePath,
312
+ workspaceFiles: files
313
+ }
314
+ };
315
+ }
316
+ async execute(method, language, code = '', log = true, args = [], options = {}, signal, progress, requestId = 0) {
317
+ const session = this.session(language);
318
+ if (session.active)
319
+ throw new ContainerError('RESOURCE_LIMIT', `${language} already has an active runtime job`);
320
+ const source = this.inputs(language, code, options);
321
+ const job = {
322
+ id: String(++this.#nextJob),
323
+ language,
324
+ method,
325
+ code: source.code,
326
+ log,
327
+ args: [...args],
328
+ options: source.options,
329
+ signal,
330
+ progress,
331
+ requestId
332
+ };
333
+ session.active = job;
334
+ this.#jobs.set(job.id, job);
335
+ try {
336
+ const process = await this.#container.spawn(COMMAND, [job.id], {
337
+ cwd: this.directory(language),
338
+ env: options.env,
339
+ signal
340
+ });
341
+ job.process = process;
342
+ const drain = async (stream) => {
343
+ const reader = stream.getReader(), text = new TextDecoder();
344
+ try {
345
+ for (;;) {
346
+ const { value, done } = await reader.read();
347
+ if (done)
348
+ break;
349
+ const data = text.decode(value, { stream: true });
350
+ if (data)
351
+ this.settings.emit({ language, name: 'output', data, requestId });
352
+ }
353
+ const tail = text.decode();
354
+ if (tail)
355
+ this.settings.emit({ language, name: 'output', data: tail, requestId });
356
+ }
357
+ finally {
358
+ reader.releaseLock();
359
+ }
360
+ };
361
+ const [result] = await Promise.all([
362
+ process.exited,
363
+ drain(process.stdout),
364
+ drain(process.stderr)
365
+ ]);
366
+ if (signal.aborted)
367
+ throw signal.reason ?? new DOMException('Runtime cancelled', 'AbortError');
368
+ if (job.error)
369
+ throw job.error;
370
+ if (result.reason !== 'exited')
371
+ throw new Error(`Runtime ended with ${result.reason}`);
372
+ return method === 'load' ? undefined : (job.result ?? result.code === 0);
373
+ }
374
+ finally {
375
+ if (session.active === job)
376
+ session.active = undefined;
377
+ this.#jobs.delete(job.id);
378
+ }
379
+ }
380
+ async flushInput(session, sandbox) {
381
+ if (!session.active?.readyForInput)
382
+ return;
383
+ for (const value of session.pendingInput.splice(0))
384
+ sandbox.write?.(value);
385
+ session.pendingBytes = 0;
386
+ if (session.pendingEof) {
387
+ session.pendingEof = false;
388
+ sandbox.eof();
389
+ }
390
+ }
391
+ async write(language, value) {
392
+ if (typeof value !== 'string')
393
+ throw new TypeError('Runtime input must be text');
394
+ const session = this.session(language);
395
+ if (session.pendingEof)
396
+ throw new Error('Runtime input is closed');
397
+ const size = encoder.encode(value).byteLength;
398
+ if (session.pendingBytes + size > 8 * 1024 * 1024)
399
+ throw new Error('Pending runtime input exceeds the container limit');
400
+ session.pendingInput.push(value);
401
+ session.pendingBytes += size;
402
+ if (session.active?.readyForInput)
403
+ await this.flushInput(session, await session.sandbox);
404
+ }
405
+ async eof(language) {
406
+ const session = this.session(language);
407
+ session.pendingEof = true;
408
+ if (session.active?.readyForInput)
409
+ await this.flushInput(session, await session.sandbox);
410
+ }
411
+ async stop(language) {
412
+ const session = this.#sessions.get(language);
413
+ if (!session)
414
+ return;
415
+ session.pendingInput.length = 0;
416
+ session.pendingBytes = 0;
417
+ session.pendingEof = false;
418
+ await Promise.all([
419
+ session.active?.process?.terminate(),
420
+ session.sandbox.then((sandbox) => sandbox.terminate()).catch(() => { })
421
+ ]);
422
+ }
423
+ async clear(language) {
424
+ await (await this.session(language).sandbox).clear();
425
+ }
426
+ async restart(language) {
427
+ await this.stop(language);
428
+ const session = this.#sessions.get(language);
429
+ this.#sessions.delete(language);
430
+ await session?.binding.dispose?.();
431
+ }
432
+ async debug(language, method, args) {
433
+ const allowed = new Set([
434
+ 'debugCommand',
435
+ 'debugPause',
436
+ 'setBreakpoints',
437
+ 'debugEvaluate',
438
+ 'debugVariables',
439
+ 'debugScopes',
440
+ 'debugFrameScopes',
441
+ 'debugFrameName',
442
+ 'debugReadMemory',
443
+ 'debugWriteMemory',
444
+ 'debugDataBreakpointInfo',
445
+ 'debugSetDataBreakpoints'
446
+ ]);
447
+ if (!allowed.has(method))
448
+ throw new TypeError('Unknown debugger method');
449
+ const sandbox = await this.session(language).sandbox;
450
+ const handler = sandbox[method];
451
+ if (typeof handler !== 'function')
452
+ throw new Error(`The ${language} runtime does not support ${method}`);
453
+ return await handler.apply(sandbox, args);
454
+ }
455
+ inspect() {
456
+ return [...this.#jobs.values()].map((job) => ({
457
+ language: job.language,
458
+ pid: job.process?.pid,
459
+ phase: job.method,
460
+ filesystem: this.filesystemProfile
461
+ }));
462
+ }
463
+ async dispose() {
464
+ if (this.#disposed)
465
+ return;
466
+ this.#disposed = true;
467
+ await Promise.allSettled([...this.#sessions.values()].map(async (session) => {
468
+ await session.active?.process?.terminate();
469
+ await session.binding.dispose?.();
470
+ }));
471
+ this.#sessions.clear();
472
+ this.#jobs.clear();
473
+ }
474
+ }
@@ -0,0 +1,49 @@
1
+ import type { CommandDefinition, HostCommandInvocation, LiveCommandAdapter, MemoryFileSystem } from '@wasm-idle/container';
2
+ export declare const SHELL_CONTROL_COMMAND = "wasm-idle-shell-control";
3
+ export declare const SHELL_CONTROL_FUNCTION_ENV = "BASH_FUNC___wasm_idle_poll%%";
4
+ export declare const SHELL_CONTROL_ROUTE = "/usr/local/bin/wasm-idle-shell-control";
5
+ export declare const SHELL_CONTROL_MOUNT = "/usr/share/wasm-idle-shell-control";
6
+ export declare const SHELL_CONTROL_RCFILE = "/usr/share/wasm-idle-shell-control/rcfile";
7
+ export declare const SHELL_CONTROL_REQUEST = "/tmp/.wasm-idle-shell-control-request";
8
+ export interface ShellControlInspection {
9
+ readonly ready: boolean;
10
+ readonly disposed: boolean;
11
+ /** Last cwd confirmed by the actual shell host invocation, or the host's initial cwd. */
12
+ readonly cwd: string;
13
+ readonly pendingCwd: string | null;
14
+ readonly generation: number;
15
+ readonly appliedGeneration: number;
16
+ readonly error: string | null;
17
+ }
18
+ interface ShellControlOptions {
19
+ languages: readonly string[];
20
+ initialDirectory?: string;
21
+ onChange?: (inspection: ShellControlInspection) => void;
22
+ /** Trusted deterministic protocol fixture input; never read from guest messages. */
23
+ session?: string;
24
+ }
25
+ /** Host directory requests are read only at the producer's real primary-prompt boundary. */
26
+ export declare function createShellControl(options: ShellControlOptions): Readonly<{
27
+ adapter: LiveCommandAdapter;
28
+ commands: Readonly<Record<string, CommandDefinition>>;
29
+ route: Readonly<{
30
+ path: "/usr/local/bin/wasm-idle-shell-control";
31
+ command: "wasm-idle-shell-control";
32
+ }>;
33
+ setup(fs: MemoryFileSystem): Readonly<{
34
+ mountPath: "/usr/share/wasm-idle-shell-control";
35
+ rcfilePath: "/usr/share/wasm-idle-shell-control/rcfile";
36
+ requestPath: "/tmp/.wasm-idle-shell-control-request";
37
+ args: readonly string[];
38
+ env: Readonly<{
39
+ WASM_IDLE_SHELL_CONTROL: "1";
40
+ INPUTRC: "/tmp/.wasm-idle-shell-inputrc";
41
+ "BASH_FUNC___wasm_idle_poll%%": `() ${string}`;
42
+ }>;
43
+ }>;
44
+ requestDirectory(language: string): Promise<void>;
45
+ onInvocation(invocation: HostCommandInvocation): number | undefined;
46
+ inspect: () => ShellControlInspection;
47
+ dispose(): void;
48
+ }>;
49
+ export {};