@nimbus-sh/core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +47 -26
  2. package/dist/_shared/byte-stream.d.ts +5 -4
  3. package/dist/_shared/byte-stream.d.ts.map +1 -1
  4. package/dist/_shared/real-node-imports.d.ts +7 -0
  5. package/dist/_shared/real-node-imports.d.ts.map +1 -1
  6. package/dist/_shared/real-node-imports.js +8 -0
  7. package/dist/_shared/tarball.d.ts +23 -5
  8. package/dist/_shared/tarball.d.ts.map +1 -1
  9. package/dist/_shared/tarball.js +38 -5
  10. package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
  11. package/dist/_shared/vfs-write-ledger.js +22 -16
  12. package/dist/runtime/comment-strip.d.ts +0 -35
  13. package/dist/runtime/comment-strip.d.ts.map +1 -1
  14. package/dist/runtime/comment-strip.js +48 -20
  15. package/dist/runtime/esbuild-service.d.ts +38 -42
  16. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  17. package/dist/runtime/esbuild-service.js +494 -197
  18. package/dist/runtime/javascript-ast.d.ts.map +1 -1
  19. package/dist/runtime/javascript-ast.js +45 -7
  20. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
  21. package/dist/runtime/opentui-wasm-backend.js +18 -16
  22. package/dist/runtime/port-registry.d.ts +4 -2
  23. package/dist/runtime/port-registry.d.ts.map +1 -1
  24. package/dist/runtime/port-registry.js +60 -8
  25. package/dist/shell/stdin-adapter.d.ts +11 -0
  26. package/dist/shell/stdin-adapter.d.ts.map +1 -0
  27. package/dist/shell/stdin-adapter.js +59 -0
  28. package/dist/shell/unix-commands.d.ts +2 -1
  29. package/dist/shell/unix-commands.d.ts.map +1 -1
  30. package/dist/shell/unix-commands.js +1078 -50
  31. package/dist/substrate/lifo/commands/io/dd.js +1 -1
  32. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
  33. package/dist/substrate/lifo/commands/net/curl.js +270 -54
  34. package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
  35. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
  36. package/dist/substrate/lifo/commands/system/npm.js +26 -43
  37. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
  38. package/dist/substrate/lifo/commands/text/head.js +2 -1
  39. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
  40. package/dist/substrate/lifo/commands/text/sed.js +499 -117
  41. package/dist/substrate/lifo/commands/types.d.ts +8 -4
  42. package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
  43. package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
  44. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
  45. package/dist/substrate/lifo/node-compat/zlib.js +122 -23
  46. package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
  47. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
  48. package/dist/substrate/lifo/shell/interpreter.js +79 -29
  49. package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
  50. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
  51. package/dist/substrate/lifo/shell/pipe.js +62 -18
  52. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
  53. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
  54. package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
  55. package/dist/vfs/sqlite-vfs.d.ts +19 -0
  56. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  57. package/dist/vfs/sqlite-vfs.js +106 -23
  58. package/dist/workspace/nimbus-workspace.d.ts +15 -0
  59. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  60. package/dist/workspace/nimbus-workspace.js +20 -3
  61. package/dist/workspace/supervisor-op.d.ts +23 -0
  62. package/dist/workspace/supervisor-op.d.ts.map +1 -0
  63. package/dist/workspace/supervisor-op.js +92 -0
  64. package/package.json +2 -2
  65. package/src/_shared/byte-stream.ts +5 -4
  66. package/src/_shared/real-node-imports.ts +8 -0
  67. package/src/_shared/tarball.ts +56 -5
  68. package/src/_shared/vfs-write-ledger.ts +22 -16
  69. package/src/runtime/comment-strip.ts +42 -23
  70. package/src/runtime/esbuild-service.ts +566 -217
  71. package/src/runtime/javascript-ast.ts +40 -9
  72. package/src/runtime/opentui-wasm-backend.ts +19 -17
  73. package/src/runtime/port-registry.ts +66 -8
  74. package/src/shell/stdin-adapter.ts +58 -0
  75. package/src/shell/unix-commands.ts +1054 -50
  76. package/src/substrate/lifo/commands/io/dd.ts +2 -2
  77. package/src/substrate/lifo/commands/net/curl.ts +310 -58
  78. package/src/substrate/lifo/commands/system/npm.ts +58 -38
  79. package/src/substrate/lifo/commands/text/head.ts +2 -1
  80. package/src/substrate/lifo/commands/text/sed.ts +519 -109
  81. package/src/substrate/lifo/commands/types.ts +8 -5
  82. package/src/substrate/lifo/node-compat/zlib.ts +155 -26
  83. package/src/substrate/lifo/shell/interpreter.ts +74 -27
  84. package/src/substrate/lifo/shell/pipe.ts +65 -24
  85. package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
  86. package/src/vfs/sqlite-vfs.ts +104 -23
  87. package/src/workspace/nimbus-workspace.ts +26 -2
  88. package/src/workspace/supervisor-op.ts +117 -0
@@ -1,4 +1,4 @@
1
- import { parse, type AnyNode } from 'acorn';
1
+ import { parse, tokenizer, tokTypes, type AnyNode, type TokenType } from 'acorn';
2
2
 
3
3
  export type AstNode = AnyNode & Record<string, unknown>;
4
4
 
@@ -11,18 +11,49 @@ export function parseJavaScriptModule(source: string): AstNode {
11
11
  }
12
12
 
13
13
  export function hasTopLevelModuleSyntax(source: string): boolean {
14
- let ast: AstNode;
15
14
  try {
16
- ast = parseJavaScriptModule(source);
15
+ const tokens = tokenizer(source, {
16
+ ecmaVersion: 'latest',
17
+ sourceType: 'module',
18
+ allowHashBang: true,
19
+ });
20
+ let braceDepth = 0;
21
+ let parenDepth = 0;
22
+ let bracketDepth = 0;
23
+ let previous: TokenType | undefined;
24
+
25
+ const updateDepth = (type: TokenType): void => {
26
+ if (type === tokTypes.braceL || type === tokTypes.dollarBraceL) braceDepth++;
27
+ else if (type === tokTypes.braceR) braceDepth = Math.max(0, braceDepth - 1);
28
+ else if (type === tokTypes.parenL) parenDepth++;
29
+ else if (type === tokTypes.parenR) parenDepth = Math.max(0, parenDepth - 1);
30
+ else if (type === tokTypes.bracketL) bracketDepth++;
31
+ else if (type === tokTypes.bracketR) bracketDepth = Math.max(0, bracketDepth - 1);
32
+ };
33
+
34
+ while (true) {
35
+ const token = tokens.getToken();
36
+ const type = token.type;
37
+ if (type === tokTypes.eof) return false;
38
+ const topLevel = braceDepth === 0 && parenDepth === 0 && bracketDepth === 0;
39
+
40
+ if (topLevel && previous !== tokTypes.dot) {
41
+ if (type === tokTypes._export) return true;
42
+ if (type === tokTypes._import) {
43
+ const next = tokens.getToken();
44
+ if (next.type !== tokTypes.parenL && next.type !== tokTypes.dot) return true;
45
+ updateDepth(next.type);
46
+ previous = next.type;
47
+ continue;
48
+ }
49
+ }
50
+
51
+ updateDepth(type);
52
+ previous = type;
53
+ }
17
54
  } catch {
18
55
  return false;
19
56
  }
20
- return nodeList(ast, 'body').some((node) =>
21
- node.type === 'ImportDeclaration' ||
22
- node.type === 'ExportNamedDeclaration' ||
23
- node.type === 'ExportDefaultDeclaration' ||
24
- node.type === 'ExportAllDeclaration'
25
- );
26
57
  }
27
58
 
28
59
  export function nodeList(node: AstNode, key: string): AstNode[] {
@@ -211,18 +211,17 @@ export class OpenTUIWasmBackend {
211
211
  * semantics), so each `dlopen`'d symbol call frees what was allocated before
212
212
  * it. Without this a per-frame `rgbaPtr(color)` would leak linear memory.
213
213
  *
214
- * `view` is the source ArrayBufferView when the caller passed a writable one:
215
- * native FFI `ptr(view)` yields a live pointer into the view's storage, so
216
- * after the symbol call the view must reflect anything Zig wrote into it. Many
217
- * FFIRenderLib wrappers materialize OUT-buffers this way `getCursorState`,
218
- * `getTerminalCapabilities`, `getRenderStats`, the span-feed's
219
- * `streamDrainSpans(ptr(outBuffer))`, every `editBufferGet*`/`editorViewGet*`
220
- * so the claimed-scratch release copies these back before freeing.
214
+ * `view` is a writable byte view over the caller's original storage.
215
+ * Native `ptr(value)` exposes live memory for both ArrayBuffer and typed-array
216
+ * inputs, so every transient allocation copies back after the symbol call.
217
+ * OpenTUI's struct getters use bare ArrayBuffers (`LogicalCursorStruct`,
218
+ * `VisualCursorStruct`, stats, terminal capabilities), while span-feed drains
219
+ * use typed arrays. Both forms must observe Zig's writes.
221
220
  */
222
221
  readonly #pendingPtrScratch: Array<{
223
222
  offset: number;
224
223
  size: number;
225
- view: ArrayBufferView | null;
224
+ view: ArrayBufferView;
226
225
  }> = [];
227
226
 
228
227
  /**
@@ -336,12 +335,10 @@ export class OpenTUIWasmBackend {
336
335
  // allocation as transient scratch that the next symbol call frees (see
337
336
  // `#bindSymbol`). This keeps a per-frame `rgbaPtr(color)` loop leak-free.
338
337
  //
339
- // A writable ArrayBufferView is also an OUT-buffer here: native `ptr(view)` is
340
- // a live pointer into the view's storage, so after the call the view must
341
- // reflect Zig's writes. We record the source view so the claimed-scratch
342
- // release copies it back (the span-feed `streamDrainSpans(ptr(drainBuffer))`
343
- // and every FFIRenderLib `ptr(outBuffer)`/`ptr(cursorBuffer)`/`ptr(statsBuffer)`
344
- // getter depend on this). Read-only ArrayBuffers carry a null view (copy-in only).
338
+ // Every ArrayBuffer and ArrayBufferView is potentially an OUT-buffer:
339
+ // native `ptr(value)` is a live pointer into the same storage. Copy the
340
+ // arena bytes back after the call so bare struct buffers and typed-array
341
+ // windows have identical semantics.
345
342
  ptr(value: ArrayBuffer | ArrayBufferView): OpenTUIPointer {
346
343
  const { bytes, byteOffset, byteLength } = viewBytes(value);
347
344
  const size = byteLength === 0 ? ARENA_ALIGN : byteLength;
@@ -352,7 +349,7 @@ export class OpenTUIWasmBackend {
352
349
  this.#pendingPtrScratch.push({
353
350
  offset,
354
351
  size,
355
- view: ArrayBuffer.isView(value) ? value : null,
352
+ view: ArrayBuffer.isView(value) ? value : new Uint8Array(value),
356
353
  });
357
354
  return offset;
358
355
  }
@@ -513,8 +510,13 @@ export class OpenTUIWasmBackend {
513
510
  for (const s of scratch) scratchBytes += s.size;
514
511
  diag.rec(name, this.#exports.memory.buffer.byteLength - memBefore, marshaled, scratchBytes);
515
512
  }
516
- if (returns === 'u64' || returns === 'i64') {
517
- return typeof result === 'bigint' ? result : BigInt(result as number);
513
+ if (returns === 'u64') {
514
+ const value = typeof result === 'bigint' ? result : BigInt(result as number);
515
+ return BigInt.asUintN(64, value);
516
+ }
517
+ if (returns === 'i64') {
518
+ const value = typeof result === 'bigint' ? result : BigInt(result as number);
519
+ return BigInt.asIntN(64, value);
518
520
  }
519
521
  return result;
520
522
  };
@@ -23,8 +23,9 @@
23
23
  * `request.method` and application headers mirror the outer request,
24
24
  * Nimbus credentials/internal headers are removed, and
25
25
  * `request.body` is a ReadableStream (or null for GET/HEAD) that
26
- * the facet can consume once. The returned Response is returned
27
- * to the outer fetch as-is; its body is streamed directly.
26
+ * the facet can consume once. The returned Response is streamed
27
+ * straight back, with one normalization: the hop speaks identity,
28
+ * so a compressed body is decoded here. See `decodeContentCoding`.
28
29
  */
29
30
 
30
31
  import { sanitizeUntrustedHeaders } from '../_shared/untrusted-request.js';
@@ -48,7 +49,60 @@ export interface PortEntry {
48
49
  capability: string;
49
50
  }
50
51
 
51
- function createPortCapability(): string {
52
+ /**
53
+ * Content codings this hop can undo. `DecompressionStream` decodes exactly
54
+ * these; brotli and zstd have no decoder in the runtime, so a body in one of
55
+ * those cannot be repaired here.
56
+ */
57
+ const DECODABLE_CONTENT_CODINGS = new Map<string, 'gzip' | 'deflate'>([
58
+ ['gzip', 'gzip'],
59
+ ['x-gzip', 'gzip'],
60
+ ['deflate', 'deflate'],
61
+ ]);
62
+
63
+ /**
64
+ * Hand back a port target's response with its bytes and its headers in
65
+ * agreement.
66
+ *
67
+ * A `Response` a facet builds always holds an identity body. The runtime
68
+ * treats the bytes a `Response` is constructed from as unencoded and runs its
69
+ * own content negotiation on the way out: it drops a `Content-Encoding` the
70
+ * client did not ask for, and compresses again when the client did. Either
71
+ * way a guest server's `Content-Encoding` is a label with nothing behind it,
72
+ * and the browser renders compressed bytes as text.
73
+ *
74
+ * The forwarded request asks the target for `identity`, so this decode covers
75
+ * the server that compresses whatever the client said. A coding with no
76
+ * decoder answers 502: an honest failure beats a page of mojibake.
77
+ */
78
+ function decodeContentCoding(response: Response, port: number): Response {
79
+ const coding = response.headers.get('Content-Encoding')?.trim().toLowerCase();
80
+ if (!coding || coding === 'identity' || response.body === null) return response;
81
+
82
+ const format = DECODABLE_CONTENT_CODINGS.get(coding);
83
+ if (!format) {
84
+ void response.body.cancel().catch(() => {});
85
+ return new Response(
86
+ JSON.stringify({
87
+ error: `port proxy: target answered Content-Encoding "${coding}", which this hop cannot decode`,
88
+ port,
89
+ }),
90
+ { status: 502, headers: { 'Content-Type': 'application/json' } },
91
+ );
92
+ }
93
+
94
+ const headers = new Headers(response.headers);
95
+ headers.delete('Content-Encoding');
96
+ // The decoded body has a different length; the Response re-derives it.
97
+ headers.delete('Content-Length');
98
+ return new Response(response.body.pipeThrough(new DecompressionStream(format)), {
99
+ status: response.status,
100
+ statusText: response.statusText,
101
+ headers,
102
+ });
103
+ }
104
+
105
+ export function createPortCapability(): string {
52
106
  const bytes = crypto.getRandomValues(new Uint8Array(12));
53
107
  return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
54
108
  }
@@ -262,6 +316,10 @@ export class PortRegistry {
262
316
  if (authorization) headers.set('authorization', authorization);
263
317
 
264
318
  headers.set('X-Nimbus-Port', String(port));
319
+ // A Response cannot carry an encoded body across this hop, so the
320
+ // target is asked for the one coding that survives it. See
321
+ // `decodeContentCoding` for the server that compresses anyway.
322
+ headers.set('Accept-Encoding', 'identity');
265
323
  // `duplex: 'half'` is required by workerd when body is a
266
324
  // ReadableStream — otherwise `new Request(…)` throws. It's not
267
325
  // part of the published @cloudflare/workers-types RequestInit,
@@ -305,12 +363,12 @@ export class PortRegistry {
305
363
  );
306
364
  }
307
365
 
308
- // Return the facet's Response as-is. Body is streamed; headers,
309
- // status, and status-text pass through unchanged. We do NOT
310
- // inject Access-Control-Allow-Origin — a port proxy forwards
311
- // whatever CORS policy the user's HTTP server chose (audit C3
366
+ // Stream the facet's Response back. Status, status-text, and every
367
+ // header pass through; only a content coding the hop cannot carry is
368
+ // undone. We do NOT inject Access-Control-Allow-Origin — a port proxy
369
+ // forwards whatever CORS policy the user's HTTP server chose (audit C3
312
370
  // discourages gratuitous wildcards on non-static routes).
313
- return response;
371
+ return decodeContentCoding(response, port);
314
372
  } catch (error: unknown) {
315
373
  // Server-side triage — users see only the 502 body, operators
316
374
  // see the full error + stack in Worker logs.
@@ -0,0 +1,58 @@
1
+ import type { CommandInputStream } from '../substrate/lifo/commands/types.js';
2
+
3
+ /**
4
+ * One consuming source behind the full reader contract, byte-accurate:
5
+ * the text is encoded once and every offset is a byte offset, so
6
+ * readBytes(1) over `é` yields exactly one UTF-8 byte per call and mixed
7
+ * text/byte consumption never diverges. Worker pure-builtin adapters hold
8
+ * stdin as a plain string; without this they can only offer readAll, and
9
+ * streaming commands see an empty pipe.
10
+ */
11
+ export function staticStdinReader(text: string): CommandInputStream {
12
+ const bytes = new TextEncoder().encode(text);
13
+ const decoder = new TextDecoder('utf-8');
14
+ let offset = 0;
15
+
16
+ const pull = (max: number): Uint8Array | null => {
17
+ if (offset >= bytes.length) return null;
18
+ const end = Math.min(offset + max, bytes.length);
19
+ const chunk = bytes.subarray(offset, end);
20
+ offset = end;
21
+ return chunk;
22
+ };
23
+
24
+ return {
25
+ readBytes: async (maxLength: number) => {
26
+ if (maxLength <= 0) return new Uint8Array(0);
27
+ return pull(maxLength);
28
+ },
29
+ read: async () => {
30
+ while (offset < bytes.length) {
31
+ const end = Math.min(offset + 65536, bytes.length);
32
+ const text2 = decoder.decode(bytes.subarray(offset, end), { stream: true });
33
+ offset = end;
34
+ if (text2.length > 0) return text2;
35
+ }
36
+ const tail = decoder.decode();
37
+ return tail.length > 0 ? tail : null;
38
+ },
39
+ readAll: async () => {
40
+ if (offset >= bytes.length) return '';
41
+ const out = decoder.decode(bytes.subarray(offset));
42
+ offset = bytes.length;
43
+ return out;
44
+ },
45
+ readLine: async () => {
46
+ if (offset >= bytes.length) return null;
47
+ const newline = bytes.indexOf(0x0a, offset);
48
+ if (newline === -1) {
49
+ const line = decoder.decode(bytes.subarray(offset));
50
+ offset = bytes.length;
51
+ return line;
52
+ }
53
+ const line = decoder.decode(bytes.subarray(offset, newline));
54
+ offset = newline + 1;
55
+ return line;
56
+ },
57
+ };
58
+ }