@nimbus-sh/worker 0.1.5 → 0.1.6

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 (135) hide show
  1. package/README.md +37 -0
  2. package/dist/_shared/crypto.d.ts +15 -0
  3. package/dist/_shared/crypto.d.ts.map +1 -0
  4. package/dist/_shared/crypto.js +122 -0
  5. package/dist/facets/manager.d.ts +24 -0
  6. package/dist/facets/manager.d.ts.map +1 -1
  7. package/dist/facets/manager.js +279 -70
  8. package/dist/facets/process.d.ts +11 -2
  9. package/dist/facets/process.d.ts.map +1 -1
  10. package/dist/facets/process.js +187 -21
  11. package/dist/facets/vite-dev-server.d.ts +2 -0
  12. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  13. package/dist/facets/vite-dev-server.js +39 -12
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -0
  17. package/dist/loaders/child-process/spawn-facet.d.ts +1 -3
  18. package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -1
  19. package/dist/loaders/child-process/spawn-facet.js +1 -12
  20. package/dist/loaders/child-process/spawn-pool.d.ts +1 -1
  21. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  22. package/dist/loaders/child-process/spawn-pool.js +1 -6
  23. package/dist/npm/installer.d.ts.map +1 -1
  24. package/dist/npm/installer.js +47 -27
  25. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  26. package/dist/npm/pre-bundle-facet.js +2 -1
  27. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  28. package/dist/npm/resolve-one-facet.js +41 -13
  29. package/dist/npm/resolver.d.ts.map +1 -1
  30. package/dist/npm/resolver.js +70 -35
  31. package/dist/router/index.d.ts.map +1 -1
  32. package/dist/router/index.js +45 -21
  33. package/dist/router/remote-api.d.ts.map +1 -1
  34. package/dist/router/remote-api.js +29 -8
  35. package/dist/runtime/barrel-synthesizer.d.ts +7 -0
  36. package/dist/runtime/barrel-synthesizer.d.ts.map +1 -1
  37. package/dist/runtime/barrel-synthesizer.js +11 -0
  38. package/dist/runtime/clang-runner.d.ts.map +1 -1
  39. package/dist/runtime/clang-runner.js +1 -8
  40. package/dist/runtime/esbuild-service.d.ts +12 -5
  41. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  42. package/dist/runtime/esbuild-service.js +28 -14
  43. package/dist/runtime/node-runner.d.ts +2 -0
  44. package/dist/runtime/node-runner.d.ts.map +1 -1
  45. package/dist/runtime/node-shims.d.ts +4 -2
  46. package/dist/runtime/node-shims.d.ts.map +1 -1
  47. package/dist/runtime/node-shims.js +404 -55
  48. package/dist/runtime/os-contracts.d.ts +128 -0
  49. package/dist/runtime/os-contracts.d.ts.map +1 -0
  50. package/dist/runtime/os-contracts.js +1 -0
  51. package/dist/runtime/package-manager.d.ts.map +1 -1
  52. package/dist/runtime/package-manager.js +32 -7
  53. package/dist/runtime/port-registry.d.ts +5 -0
  54. package/dist/runtime/port-registry.d.ts.map +1 -1
  55. package/dist/runtime/port-registry.js +22 -1
  56. package/dist/runtime/pyodide-runtime-assets.d.ts +19 -0
  57. package/dist/runtime/pyodide-runtime-assets.d.ts.map +1 -0
  58. package/dist/runtime/pyodide-runtime-assets.js +44 -0
  59. package/dist/runtime/python-repl.d.ts +3 -1
  60. package/dist/runtime/python-repl.d.ts.map +1 -1
  61. package/dist/runtime/python-repl.js +11 -29
  62. package/dist/runtime/python-runner.d.ts +8 -9
  63. package/dist/runtime/python-runner.d.ts.map +1 -1
  64. package/dist/runtime/python-runner.js +897 -61
  65. package/dist/runtime/python-socket-shim.d.ts +11 -0
  66. package/dist/runtime/python-socket-shim.d.ts.map +1 -0
  67. package/dist/runtime/python-socket-shim.js +526 -0
  68. package/dist/runtime/require-resolver.d.ts.map +1 -1
  69. package/dist/runtime/require-resolver.js +2 -11
  70. package/dist/runtime/ruby-gems.d.ts +25 -0
  71. package/dist/runtime/ruby-gems.d.ts.map +1 -0
  72. package/dist/runtime/ruby-gems.js +582 -0
  73. package/dist/runtime/ruby-runner.d.ts +2 -4
  74. package/dist/runtime/ruby-runner.d.ts.map +1 -1
  75. package/dist/runtime/ruby-runner.js +335 -34
  76. package/dist/runtime/runtime-catalog.d.ts +16 -4
  77. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  78. package/dist/runtime/runtime-catalog.js +21 -10
  79. package/dist/runtime/runtime-registry.d.ts +3 -0
  80. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  81. package/dist/runtime/runtime-registry.js +4 -0
  82. package/dist/runtime/sqlite-runtime-fs-bridge.d.ts +43 -0
  83. package/dist/runtime/sqlite-runtime-fs-bridge.d.ts.map +1 -0
  84. package/dist/runtime/sqlite-runtime-fs-bridge.js +224 -0
  85. package/dist/runtime/static-server.d.ts +3 -0
  86. package/dist/runtime/static-server.d.ts.map +1 -0
  87. package/dist/runtime/static-server.js +121 -0
  88. package/dist/runtime/vfs-snapshot.d.ts +57 -0
  89. package/dist/runtime/vfs-snapshot.d.ts.map +1 -0
  90. package/dist/runtime/vfs-snapshot.js +141 -0
  91. package/dist/runtime/virtual-socket-kernel.d.ts +16 -0
  92. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -0
  93. package/dist/runtime/virtual-socket-kernel.js +360 -0
  94. package/dist/runtime/wasi-instance.d.ts +6 -3
  95. package/dist/runtime/wasi-instance.d.ts.map +1 -1
  96. package/dist/runtime/wasm-runner.d.ts +1 -25
  97. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  98. package/dist/runtime/wasm-runner.js +1 -160
  99. package/dist/session/agent.d.ts +43 -0
  100. package/dist/session/agent.d.ts.map +1 -0
  101. package/dist/session/agent.js +1252 -0
  102. package/dist/session/init.d.ts.map +1 -1
  103. package/dist/session/init.js +15 -16
  104. package/dist/session/nimbus-session.d.ts +13 -2
  105. package/dist/session/nimbus-session.d.ts.map +1 -1
  106. package/dist/session/nimbus-session.js +55 -7
  107. package/dist/session/programmatic.d.ts +10 -0
  108. package/dist/session/programmatic.d.ts.map +1 -1
  109. package/dist/session/programmatic.js +121 -0
  110. package/dist/session/routes.d.ts.map +1 -1
  111. package/dist/session/routes.js +19 -1
  112. package/dist/session/rpc.d.ts +11 -0
  113. package/dist/session/rpc.d.ts.map +1 -1
  114. package/dist/session/rpc.js +56 -43
  115. package/dist/session/supervisor-rpc.d.ts +15 -0
  116. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  117. package/dist/session/supervisor-rpc.js +32 -0
  118. package/dist/session/ws.d.ts +4 -3
  119. package/dist/session/ws.d.ts.map +1 -1
  120. package/dist/session/ws.js +11 -1
  121. package/dist/shell/unix-commands.js +12 -16
  122. package/dist/vfs/path.d.ts +8 -4
  123. package/dist/vfs/path.d.ts.map +1 -1
  124. package/dist/vfs/path.js +16 -4
  125. package/dist/vfs/sqlite-vfs.d.ts +2 -0
  126. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  127. package/dist/vfs/sqlite-vfs.js +11 -0
  128. package/dist/vfs/symlink-registry.d.ts +7 -1
  129. package/dist/vfs/symlink-registry.d.ts.map +1 -1
  130. package/dist/vfs/symlink-registry.js +16 -11
  131. package/package.json +4 -1
  132. package/public/index.html +240 -0
  133. package/public/s/index.html +1172 -146
  134. package/runtime-contracts/pyodide-workerd-adapter.json +18 -0
  135. package/scripts/bundle-runtime.mjs +99 -24
@@ -24,6 +24,7 @@
24
24
  * Loop guard: `resolveSymlinkChain` follows at most 40 hops (matches
25
25
  * POSIX SYMLOOP_MAX).
26
26
  */
27
+ import { normalizeVfsPath } from './path.js';
27
28
  const REGISTRY_PATH = '.nimbus-symlinks.json';
28
29
  export class SymlinkRegistry {
29
30
  vfs;
@@ -62,9 +63,9 @@ export class SymlinkRegistry {
62
63
  }
63
64
  catch { /* fail-soft */ }
64
65
  }
65
- /** Normalize a path: strip leading slashes (VFS internal convention). */
66
+ /** Normalize a path to the VFS internal key convention. */
66
67
  norm(p) {
67
- return p.replace(/^\/+/, '').replace(/\/+$/, '');
68
+ return normalizeVfsPath(p);
68
69
  }
69
70
  /** Create or replace a symlink. Target is stored verbatim (can be
70
71
  * absolute or relative — interpretation happens at resolve time). */
@@ -112,15 +113,7 @@ export class SymlinkRegistry {
112
113
  else {
113
114
  // Relative target: resolve from symlink's parent dir.
114
115
  const parent = cur.includes('/') ? cur.substring(0, cur.lastIndexOf('/')) : '';
115
- const parts = (parent + '/' + target).split('/');
116
- const out = [];
117
- for (const s of parts) {
118
- if (s === '..')
119
- out.pop();
120
- else if (s !== '.' && s !== '')
121
- out.push(s);
122
- }
123
- cur = out.join('/');
116
+ cur = normalizeVfsPath(`${parent}/${target}`);
124
117
  }
125
118
  }
126
119
  // ELOOP
@@ -135,3 +128,15 @@ export class SymlinkRegistry {
135
128
  return out;
136
129
  }
137
130
  }
131
+ /**
132
+ * Return the session-wide registry for a VFS instance. The registry has an
133
+ * in-memory cache, so all runtime surfaces that share one SqliteVFS must also
134
+ * share the registry instance to avoid stale symlink reads.
135
+ */
136
+ export function getSymlinkRegistry(vfs) {
137
+ const owner = vfs;
138
+ if (!owner.__nimbus_symlink_registry) {
139
+ owner.__nimbus_symlink_registry = new SymlinkRegistry(vfs);
140
+ }
141
+ return owner.__nimbus_symlink_registry;
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbus-sh/worker",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Nimbus runtime package for Cloudflare Workers, re-exported publicly through @nimbus-sh/sdk/worker.",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,6 +46,7 @@
46
46
  "dist",
47
47
  "public",
48
48
  "scripts",
49
+ "runtime-contracts",
49
50
  "README.md",
50
51
  "LICENSE",
51
52
  "NOTICE.md"
@@ -61,7 +62,9 @@
61
62
  "typecheck": "tsc --noEmit"
62
63
  },
63
64
  "dependencies": {
65
+ "@ai-sdk/openai-compatible": "^2.0.41",
64
66
  "@lifo-sh/core": "^0.5.5",
67
+ "ai": "^6.0.182",
65
68
  "clean-git-ref": "^2.0.1",
66
69
  "crc-32": "^1.2.2",
67
70
  "diff": "^9.0.0",
package/public/index.html CHANGED
@@ -259,6 +259,187 @@
259
259
  .term-info { color: var(--accent-2); }
260
260
  .term-dim { color: var(--muted); }
261
261
 
262
+ /* ─────────────────────────────── SDK ─────────────────────────────── */
263
+ .sdk {
264
+ padding: 28px 0 92px;
265
+ border-top: 1px solid var(--border);
266
+ }
267
+ .sdk-layout {
268
+ display: grid;
269
+ grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
270
+ gap: 34px;
271
+ align-items: center;
272
+ }
273
+ .section-kicker {
274
+ display: inline-flex;
275
+ align-items: center;
276
+ gap: 8px;
277
+ margin-bottom: 14px;
278
+ color: var(--accent);
279
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
280
+ font-size: 12px;
281
+ letter-spacing: 0.04em;
282
+ text-transform: uppercase;
283
+ }
284
+ .section-kicker::before {
285
+ content: "";
286
+ width: 8px;
287
+ height: 8px;
288
+ border-radius: 50%;
289
+ background: var(--accent);
290
+ box-shadow: 0 0 4px rgba(100, 255, 218, 0.65);
291
+ }
292
+ .sdk h2 {
293
+ max-width: 460px;
294
+ font-size: 30px;
295
+ line-height: 1.15;
296
+ letter-spacing: -0.02em;
297
+ margin-bottom: 16px;
298
+ }
299
+ .sdk-copy > p {
300
+ max-width: 470px;
301
+ color: var(--fg-dim);
302
+ font-size: 15px;
303
+ line-height: 1.65;
304
+ margin-bottom: 26px;
305
+ }
306
+ .sdk-actions {
307
+ display: flex;
308
+ gap: 12px;
309
+ flex-wrap: wrap;
310
+ }
311
+ .sdk-panel {
312
+ min-width: 0;
313
+ overflow: hidden;
314
+ border: 1px solid var(--border);
315
+ border-radius: 8px;
316
+ background: var(--code-bg);
317
+ box-shadow: 0 30px 80px -18px rgba(0, 0, 0, 0.62);
318
+ }
319
+ .sdk-panel-bar {
320
+ display: grid;
321
+ grid-template-columns: auto 1fr auto;
322
+ align-items: center;
323
+ gap: 14px;
324
+ padding: 10px 14px;
325
+ background: #161b22;
326
+ border-bottom: 1px solid var(--border);
327
+ }
328
+ .sdk-dots {
329
+ display: flex;
330
+ gap: 7px;
331
+ }
332
+ .sdk-dots span {
333
+ width: 11px;
334
+ height: 11px;
335
+ border-radius: 50%;
336
+ }
337
+ .sdk-dots .r { background: #ff5f57; }
338
+ .sdk-dots .y { background: #febc2e; }
339
+ .sdk-dots .g { background: #28c940; }
340
+ .sdk-panel-title {
341
+ min-width: 0;
342
+ color: var(--muted);
343
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
344
+ font-size: 12px;
345
+ text-align: center;
346
+ overflow: hidden;
347
+ text-overflow: ellipsis;
348
+ white-space: nowrap;
349
+ }
350
+ .sdk-file-pill {
351
+ padding: 3px 8px;
352
+ border: 1px solid var(--border);
353
+ border-radius: 6px;
354
+ color: var(--fg-dim);
355
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
356
+ font-size: 11px;
357
+ background: rgba(88, 166, 255, 0.08);
358
+ }
359
+ .sdk-install {
360
+ display: flex;
361
+ align-items: center;
362
+ gap: 12px;
363
+ padding: 11px 16px;
364
+ border-bottom: 1px solid var(--border);
365
+ color: var(--muted);
366
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
367
+ font-size: 12px;
368
+ }
369
+ .sdk-install span {
370
+ color: var(--muted-2);
371
+ }
372
+ .sdk-install code { color: var(--fg-dim); }
373
+ .sdk-code-wrap {
374
+ overflow-x: auto;
375
+ scrollbar-color: #303846 #0b0f14;
376
+ scrollbar-width: thin;
377
+ }
378
+ .sdk-code-wrap::-webkit-scrollbar {
379
+ height: 8px;
380
+ }
381
+ .sdk-code-wrap::-webkit-scrollbar-track {
382
+ background: #0b0f14;
383
+ }
384
+ .sdk-code-wrap::-webkit-scrollbar-thumb {
385
+ background: #303846;
386
+ border: 2px solid #0b0f14;
387
+ border-radius: 999px;
388
+ }
389
+ .sdk-code {
390
+ margin: 0;
391
+ padding: 18px 0 18px 0;
392
+ min-width: 580px;
393
+ list-style: none;
394
+ counter-reset: sdk-line;
395
+ color: var(--fg-dim);
396
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
397
+ font-size: 13px;
398
+ line-height: 1.68;
399
+ }
400
+ .sdk-code li {
401
+ counter-increment: sdk-line;
402
+ display: grid;
403
+ grid-template-columns: 44px 1fr;
404
+ gap: 14px;
405
+ padding: 0 20px 0 0;
406
+ white-space: pre;
407
+ }
408
+ .sdk-code li code {
409
+ min-width: 0;
410
+ }
411
+ .sdk-code li::before {
412
+ content: counter(sdk-line);
413
+ color: var(--muted-2);
414
+ text-align: right;
415
+ user-select: none;
416
+ }
417
+ .code-dim { color: var(--muted-2); }
418
+ .code-key { color: #ffb86c; }
419
+ .code-fn { color: var(--accent-2); }
420
+ .code-str { color: #a5d6ff; }
421
+ .code-var { color: var(--accent); }
422
+ .code-prop { color: #d2a8ff; }
423
+ .sdk-output {
424
+ display: flex;
425
+ justify-content: space-between;
426
+ gap: 14px;
427
+ padding: 13px 16px;
428
+ border-top: 1px solid var(--border);
429
+ background: #0b0f14;
430
+ color: var(--muted);
431
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
432
+ font-size: 12px;
433
+ }
434
+ .sdk-output strong {
435
+ color: var(--accent);
436
+ font-weight: 600;
437
+ }
438
+ .sdk-output span {
439
+ min-width: 0;
440
+ overflow-wrap: anywhere;
441
+ }
442
+
262
443
  /* ─────────────────────────── Feature grid ─────────────────────────── */
263
444
  .features { padding: 32px 0 96px; border-top: 1px solid var(--border); }
264
445
  .features h2 {
@@ -318,6 +499,14 @@
318
499
  .badges li { font-size: 11px; padding: 4px 10px; }
319
500
  .btn { height: 42px; padding: 0 18px; font-size: 13.5px; }
320
501
  .terminal-body { font-size: 12px; padding: 16px 18px; }
502
+ .sdk { padding: 24px 0 72px; }
503
+ .sdk-layout { grid-template-columns: 1fr; gap: 28px; }
504
+ .sdk h2 { font-size: 25px; }
505
+ .sdk-code { font-size: 12px; min-width: 560px; }
506
+ .sdk-panel-bar { grid-template-columns: auto minmax(0, 1fr) auto; }
507
+ .sdk-file-pill { padding: 3px 6px; }
508
+ .sdk-install { align-items: flex-start; flex-direction: column; gap: 4px; }
509
+ .sdk-output { flex-direction: column; gap: 6px; }
321
510
  }
322
511
 
323
512
  /* Reduced motion */
@@ -333,6 +522,7 @@
333
522
  <span>nimbus</span>
334
523
  </a>
335
524
  <nav>
525
+ <a href="#sdk">SDK</a>
336
526
  <a href="#features">Features</a>
337
527
  <a class="btn-nav" href="https://github.com/AshishKumar4/Nimbus" target="_blank" rel="noopener">
338
528
  <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
@@ -403,6 +593,56 @@
403
593
  </div>
404
594
  </section>
405
595
 
596
+ <!-- SDK -->
597
+ <section class="sdk" id="sdk">
598
+ <div class="sdk-layout">
599
+ <div class="sdk-copy">
600
+ <span class="section-kicker">Programmatic SDK</span>
601
+ <h2>Run sandboxes from your own code.</h2>
602
+ <p>Create a persistent Nimbus workspace, write a file, start a server, and hand the preview URL back to your user.</p>
603
+ <div class="sdk-actions">
604
+ <a class="btn btn-primary" href="https://github.com/AshishKumar4/Nimbus#embed-nimbus-in-your-workers-project" target="_blank" rel="noopener">Read SDK docs</a>
605
+ <a class="btn btn-ghost" href="https://www.npmjs.com/package/@nimbus-sh/sdk" target="_blank" rel="noopener">npm package</a>
606
+ </div>
607
+ </div>
608
+
609
+ <figure class="sdk-panel" aria-label="Nimbus SDK TypeScript example">
610
+ <div class="sdk-panel-bar">
611
+ <div class="sdk-dots" aria-hidden="true"><span class="r"></span><span class="y"></span><span class="g"></span></div>
612
+ <figcaption class="sdk-panel-title">sdk-demo.ts</figcaption>
613
+ <span class="sdk-file-pill">TypeScript</span>
614
+ </div>
615
+ <div class="sdk-install">
616
+ <span>install</span>
617
+ <code>npm install @nimbus-sh/sdk</code>
618
+ </div>
619
+ <div class="sdk-code-wrap">
620
+ <ol class="sdk-code">
621
+ <li><code><span class="code-key">import</span> { Nimbus } <span class="code-key">from</span> <span class="code-str">'@nimbus-sh/sdk'</span>;</code></li>
622
+ <li><code></code></li>
623
+ <li><code><span class="code-key">export default</span> {</code></li>
624
+ <li><code> <span class="code-key">async</span> <span class="code-fn">fetch</span>(req: Request, env: Env) {</code></li>
625
+ <li><code> <span class="code-key">const</span> <span class="code-var">endpoint</span> = <span class="code-key">new</span> URL(req.url).origin;</code></li>
626
+ <li><code> <span class="code-key">const</span> <span class="code-var">nimbus</span> = Nimbus.<span class="code-fn">fromEnv</span>(env, { endpoint });</code></li>
627
+ <li><code> <span class="code-key">const</span> <span class="code-var">box</span> = <span class="code-var">nimbus</span>.<span class="code-fn">sandbox</span>(<span class="code-str">'demo-app'</span>);</code></li>
628
+ <li><code></code></li>
629
+ <li><code> <span class="code-key">await</span> <span class="code-var">box</span>.<span class="code-prop">files</span>.<span class="code-fn">write</span>(<span class="code-str">'/home/user/app.js'</span>, serverSource);</code></li>
630
+ <li><code> <span class="code-key">await</span> <span class="code-var">box</span>.<span class="code-fn">startProcess</span>(<span class="code-str">'PORT=3000 node app.js'</span>);</code></li>
631
+ <li><code> <span class="code-key">const</span> <span class="code-var">preview</span> = <span class="code-key">await</span> <span class="code-var">box</span>.<span class="code-prop">ports</span>.<span class="code-fn">expose</span>(3000);</code></li>
632
+ <li><code></code></li>
633
+ <li><code> <span class="code-key">return</span> Response.<span class="code-fn">json</span>({ previewUrl: preview.url });</code></li>
634
+ <li><code> },</code></li>
635
+ <li><code>};</code></li>
636
+ </ol>
637
+ </div>
638
+ <div class="sdk-output" aria-label="SDK result">
639
+ <span><strong>preview</strong> /s/demo-app/port/3000/</span>
640
+ <span><strong>runtime</strong> node</span>
641
+ </div>
642
+ </figure>
643
+ </div>
644
+ </section>
645
+
406
646
  <!-- FEATURES -->
407
647
  <section class="features" id="features">
408
648
  <h2>Everything a dev environment needs. <span class="accent">Nothing you don&rsquo;t.</span></h2>