withruntime 0.8.4 → 0.10.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 (132) hide show
  1. package/BLAXEL.md +4 -4
  2. package/COMPATIBILITY.md +145 -0
  3. package/DAYTONA.md +13 -7
  4. package/E2B.md +34 -20
  5. package/README.md +22 -11
  6. package/README.src.md +22 -11
  7. package/VERCEL.md +13 -3
  8. package/compatibility-lock.json +357 -0
  9. package/dist/ai-harness/index.js +2 -2
  10. package/dist/api-defaults.d.ts +17 -0
  11. package/dist/api-defaults.js +22 -0
  12. package/dist/blaxel/errors.js +1 -1
  13. package/dist/blaxel/lookup.d.ts +3 -2
  14. package/dist/blaxel/lookup.js +3 -2
  15. package/dist/blaxel/pagination.d.ts +2 -2
  16. package/dist/blaxel/pagination.js +45 -17
  17. package/dist/blaxel/preview.d.ts +3 -2
  18. package/dist/blaxel/preview.js +16 -14
  19. package/dist/blaxel/process.d.ts +1 -1
  20. package/dist/blaxel/process.js +66 -11
  21. package/dist/blaxel/sandbox.d.ts +2 -0
  22. package/dist/blaxel/sandbox.js +2 -0
  23. package/dist/blaxel/snapshot.js +3 -5
  24. package/dist/cli-words.d.ts +27 -0
  25. package/dist/cli-words.js +135 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +351 -85
  28. package/dist/client.d.ts +33 -4
  29. package/dist/client.js +47 -12
  30. package/dist/cloudflare/identity.d.ts +7 -0
  31. package/dist/cloudflare/identity.js +13 -0
  32. package/dist/cloudflare/index.d.ts +303 -0
  33. package/dist/cloudflare/index.js +592 -0
  34. package/dist/cloudflare/worker.d.ts +419 -0
  35. package/dist/cloudflare/worker.js +447 -0
  36. package/dist/codesandbox/guest-service.d.ts +31 -0
  37. package/dist/codesandbox/guest-service.js +490 -0
  38. package/dist/codesandbox/index.d.ts +386 -0
  39. package/dist/codesandbox/index.js +1347 -0
  40. package/dist/compat/core.d.ts +40 -0
  41. package/dist/compat/core.js +238 -0
  42. package/dist/compat/files.d.ts +26 -0
  43. package/dist/compat/files.js +133 -0
  44. package/dist/compat/shell.d.ts +4 -0
  45. package/dist/compat/shell.js +165 -0
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +107 -38
  48. package/dist/daytona/context.js +2 -1
  49. package/dist/daytona/daytona.js +11 -4
  50. package/dist/daytona/filesystem.d.ts +17 -4
  51. package/dist/daytona/filesystem.js +121 -38
  52. package/dist/daytona/index.d.ts +1 -1
  53. package/dist/daytona/process.d.ts +1 -1
  54. package/dist/daytona/process.js +27 -4
  55. package/dist/daytona/sandbox.d.ts +1 -1
  56. package/dist/daytona/sandbox.js +16 -7
  57. package/dist/e2b/client.js +1 -1
  58. package/dist/e2b/code-interpreter.d.ts +4 -5
  59. package/dist/e2b/code-interpreter.js +54 -56
  60. package/dist/e2b/commands.d.ts +35 -12
  61. package/dist/e2b/commands.js +169 -73
  62. package/dist/e2b/errors.js +1 -1
  63. package/dist/e2b/filesystem.d.ts +8 -6
  64. package/dist/e2b/filesystem.js +127 -28
  65. package/dist/e2b/index.d.ts +1 -0
  66. package/dist/e2b/index.js +1 -0
  67. package/dist/e2b/pty.d.ts +24 -0
  68. package/dist/e2b/pty.js +80 -0
  69. package/dist/e2b/sandbox.d.ts +6 -4
  70. package/dist/e2b/sandbox.js +59 -25
  71. package/dist/errors.d.ts +8 -0
  72. package/dist/errors.js +15 -2
  73. package/dist/file-source.d.ts +16 -0
  74. package/dist/file-source.js +162 -0
  75. package/dist/freestyle/index.d.ts +309 -0
  76. package/dist/freestyle/index.js +343 -0
  77. package/dist/index.d.ts +3 -1
  78. package/dist/login.js +16 -10
  79. package/dist/mcp.js +2 -2
  80. package/dist/modal/index.d.ts +205 -0
  81. package/dist/modal/index.js +486 -0
  82. package/dist/network-cli.js +112 -44
  83. package/dist/openai-agents/index.js +18 -71
  84. package/dist/page.d.ts +1 -1
  85. package/dist/page.js +2 -2
  86. package/dist/products/audit.d.ts +3 -0
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/interpreter.d.ts +11 -4
  90. package/dist/products/interpreter.js +33 -9
  91. package/dist/products/jobs.js +3 -2
  92. package/dist/products/limits.d.ts +10 -0
  93. package/dist/products/observability.d.ts +44 -0
  94. package/dist/products/observability.js +6 -0
  95. package/dist/products/previews.d.ts +18 -4
  96. package/dist/products/previews.js +14 -5
  97. package/dist/products/tailscale.d.ts +57 -0
  98. package/dist/products/tailscale.js +17 -0
  99. package/dist/products/volumes.d.ts +42 -0
  100. package/dist/products/volumes.js +36 -0
  101. package/dist/products/watch.d.ts +25 -4
  102. package/dist/products/watch.js +132 -18
  103. package/dist/proxy.js +3 -1
  104. package/dist/runloop/index.d.ts +326 -0
  105. package/dist/runloop/index.js +535 -0
  106. package/dist/runloop/pagination.d.ts +46 -0
  107. package/dist/runloop/pagination.js +82 -0
  108. package/dist/runloop/snapshots.d.ts +52 -0
  109. package/dist/runloop/snapshots.js +244 -0
  110. package/dist/sandbox.d.ts +76 -16
  111. package/dist/sandbox.js +696 -135
  112. package/dist/snapshots.d.ts +20 -5
  113. package/dist/snapshots.js +11 -1
  114. package/dist/sprites/index.d.ts +251 -0
  115. package/dist/sprites/index.js +588 -0
  116. package/dist/tar.d.ts +10 -1
  117. package/dist/tar.js +311 -35
  118. package/dist/tools/index.js +2 -1
  119. package/dist/transport.d.ts +23 -8
  120. package/dist/transport.js +247 -127
  121. package/dist/types.d.ts +52 -3
  122. package/dist/vercel/command.d.ts +1 -1
  123. package/dist/vercel/command.js +15 -3
  124. package/dist/vercel/errors.d.ts +2 -2
  125. package/dist/vercel/errors.js +19 -2
  126. package/dist/vercel/filesystem.d.ts +21 -18
  127. package/dist/vercel/filesystem.js +95 -48
  128. package/dist/vercel/sandbox.d.ts +8 -8
  129. package/dist/vercel/sandbox.js +159 -64
  130. package/dist/vercel/snapshot.d.ts +3 -2
  131. package/dist/vercel/snapshot.js +9 -5
  132. package/package.json +46 -3
@@ -1,8 +1,5 @@
1
1
  import { guard, isNotFound, NotSupportedError, responseError } from "./errors.js";
2
2
  import { PREVIEW } from "./lookup.js";
3
- /** Runtime's shortest and longest token, in seconds. */
4
- const TOKEN_MIN = 60;
5
- const TOKEN_MAX = 7 * 86_400;
6
3
  function checkName(name) {
7
4
  if (!/^[A-Za-z0-9._-]{1,48}$/.test(name))
8
5
  throw new NotSupportedError(`The preview name "${name}"`, "Use 1 to 48 letters, digits, dots, dashes or underscores.");
@@ -56,19 +53,24 @@ export class SandboxPreviewTokens {
56
53
  get resourceName() {
57
54
  return this.#ctx.sandboxName;
58
55
  }
59
- /** A token that lasts until `expiresAt` (at least a minute, at most a week). */
56
+ /** A token bounded by `expiresAt`; native signing floors milliseconds and
57
+ * caps it to the caller's session. Older servers refuse without a TTL fallback. */
60
58
  async create(expiresAt) {
61
- const seconds = Math.ceil((expiresAt.getTime() - Date.now()) / 1000);
62
- if (seconds > TOKEN_MAX)
63
- throw new NotSupportedError("A preview token lasting more than a week", "Create one for at most seven days, and a new one when it runs out.");
64
- const port = this.#preview.spec.port;
65
- const got = await this.#ctx.run((runtime) => runtime.previews.get(port, Math.max(TOKEN_MIN, seconds)));
59
+ const requested = expiresAt.getTime();
60
+ if (!Number.isFinite(requested))
61
+ throw responseError(400, "A preview token needs a valid expiration date.");
62
+ const remaining = requested - Date.now();
63
+ if (remaining < 60_000 || remaining > 7 * 86_400_000)
64
+ throw new NotSupportedError("A preview token lasting less than a minute or more than a week", "Request an expiration at least one minute and at most seven days away.");
65
+ const got = await this.#ctx.run((runtime) => runtime.previews.get(this.#preview.spec.port, undefined, {
66
+ expiresAt: expiresAt.toISOString(),
67
+ }));
68
+ const actual = got.tokenExpiresAt === null ? Number.NaN : Date.parse(got.tokenExpiresAt);
69
+ if (!got.token || !Number.isFinite(actual) || actual > requested)
70
+ throw new NotSupportedError("A preview token with the requested expiration", "The server must issue a token that never outlives the requested expiration; no duration-based fallback was requested.");
66
71
  return new SandboxPreviewToken({
67
72
  metadata: { name: `token-${Date.now()}`, previewName: this.previewName },
68
- spec: {
69
- ...(got.token ? { token: got.token } : {}),
70
- ...(got.tokenExpiresAt ? { expiresAt: got.tokenExpiresAt } : {}),
71
- },
73
+ spec: { token: got.token, expiresAt: got.tokenExpiresAt },
72
74
  });
73
75
  }
74
76
  list() {
@@ -193,7 +195,7 @@ export class SandboxPreviews {
193
195
  const found = await this.#ctx.run((runtime) => runtime.previews.get(port));
194
196
  return new SandboxPreview(modelOf(found, previewName, this.sandboxName), this.#ctx);
195
197
  }
196
- /** Stops sharing the port; open connections close within seconds. */
198
+ /** Stops sharing the port; its open connections are closed before this returns. */
197
199
  async delete(previewName) {
198
200
  const port = this.#portOf(previewName);
199
201
  if (port === undefined)
@@ -87,7 +87,7 @@ export declare class SandboxProcess {
87
87
  * process running; with it, once the process ends, with its output. */
88
88
  exec(request: ProcessRequest | ProcessRequestWithLog): Promise<PostProcessResponse | ProcessResponseWithLog>;
89
89
  /** The process, with the output it has written so far. */
90
- get(identifier: string, _options?: {
90
+ get(identifier: string, options?: {
91
91
  signal?: AbortSignal;
92
92
  retry?: boolean;
93
93
  }): Promise<GetProcessByIdentifierResponse>;
@@ -152,14 +152,17 @@ export class SandboxProcess {
152
152
  }
153
153
  /** A call that found the sandbox paused (autoWake off) is made again once,
154
154
  * after a wake: it never started, so it cannot run twice. */
155
- async #retryPaused(work) {
155
+ async #retryPaused(work, signal) {
156
+ signal?.throwIfAborted();
156
157
  try {
157
158
  return await work();
158
159
  }
159
160
  catch (error) {
161
+ signal?.throwIfAborted();
160
162
  if (codeOf(error) !== "sandbox_paused")
161
163
  throw translate(error);
162
164
  await this.#ctx.wake();
165
+ signal?.throwIfAborted();
163
166
  return guard(work);
164
167
  }
165
168
  }
@@ -182,6 +185,8 @@ export class SandboxProcess {
182
185
  const startedAt = new Date().toISOString();
183
186
  const run = async () => {
184
187
  for await (const event of runtime.execStream(line, { ...options, signal: abort.signal })) {
188
+ if (event.type === "truncated")
189
+ throw responseError(502, "Some command output is no longer available.");
185
190
  if (event.type === "start") {
186
191
  id = event.processId;
187
192
  this.#remember(id, meta);
@@ -279,16 +284,20 @@ export class SandboxProcess {
279
284
  };
280
285
  }
281
286
  /** A process by pid or name: the registry first, then Runtime's list. */
282
- async #find(identifier) {
287
+ async #find(identifier, signal) {
288
+ signal?.throwIfAborted();
283
289
  const runtime = await this.#ctx.live();
290
+ signal?.throwIfAborted();
284
291
  const registry = this.#registry;
285
292
  const known = registry.byName.get(identifier) ?? (registry.meta.has(identifier) ? identifier : undefined);
286
293
  if (known !== undefined) {
287
- const process = await this.#retryPaused(() => runtime.processes.get(known));
294
+ const process = await this.#retryPaused(() => runtime.processes.get(known, { signal }), signal);
295
+ signal?.throwIfAborted();
288
296
  registry.processes.set(known, process);
289
297
  return { id: known, info: process.info, process };
290
298
  }
291
- const all = await this.#retryPaused(() => runtime.processes.list());
299
+ const all = await this.#retryPaused(() => runtime.processes.list({ signal }), signal);
300
+ signal?.throwIfAborted();
292
301
  let found;
293
302
  for (const info of all) {
294
303
  const parsed = parseProcessLine(info.command);
@@ -321,15 +330,20 @@ export class SandboxProcess {
321
330
  const running = found.info.state === "running";
322
331
  if (running && !untilExit && found.info.outputBytes <= found.info.firstOffset)
323
332
  return out;
333
+ signal?.throwIfAborted();
324
334
  const runtime = await this.#ctx.live();
335
+ signal?.throwIfAborted();
325
336
  const abort = new AbortController();
326
337
  const forward = () => abort.abort();
338
+ signal?.throwIfAborted();
327
339
  signal?.addEventListener("abort", forward, { once: true });
328
340
  try {
329
341
  for await (const event of runtime.processes.follow(found.id, {
330
342
  cursor: 0,
331
343
  signal: abort.signal,
332
344
  })) {
345
+ if (event.type === "truncated")
346
+ throw responseError(502, "Some command output is no longer available.");
333
347
  if (event.type === "stdout" || event.type === "stderr") {
334
348
  out[event.type] += event.data;
335
349
  out.logs += event.data;
@@ -362,10 +376,32 @@ export class SandboxProcess {
362
376
  return out;
363
377
  }
364
378
  /** The process, with the output it has written so far. */
365
- async get(identifier, _options = {}) {
366
- const found = await this.#find(identifier);
367
- const out = await this.#output(found, false);
368
- return this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
379
+ async get(identifier, options = {}) {
380
+ options.signal?.throwIfAborted();
381
+ const signal = options.signal;
382
+ let onAbort;
383
+ const interrupted = new Promise((_, reject) => {
384
+ onAbort = () => reject(new Error("Process read was interrupted.", { cause: signal?.reason }));
385
+ signal?.addEventListener("abort", onAbort, { once: true });
386
+ });
387
+ try {
388
+ const reading = (async () => {
389
+ const found = await this.#find(identifier, signal);
390
+ const out = await this.#output(found, false, signal);
391
+ signal?.throwIfAborted();
392
+ return this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
393
+ })();
394
+ return await Promise.race([reading, interrupted]);
395
+ }
396
+ catch (error) {
397
+ if (signal?.aborted)
398
+ throw signal.reason ?? error;
399
+ throw error;
400
+ }
401
+ finally {
402
+ if (onAbort)
403
+ signal?.removeEventListener("abort", onAbort);
404
+ }
369
405
  }
370
406
  /** Waits for the process to end (at most `maxWait` ms, -1 for no limit) and
371
407
  * answers it with its output. The process is never stopped by the wait. */
@@ -381,12 +417,27 @@ export class SandboxProcess {
381
417
  throw late();
382
418
  const abort = new AbortController();
383
419
  const forward = () => abort.abort(signal?.reason);
420
+ signal?.throwIfAborted();
384
421
  signal?.addEventListener("abort", forward, { once: true });
385
422
  const timer = maxWait === -1 ? undefined : setTimeout(() => abort.abort(late()), maxWait);
423
+ let onAbort;
424
+ const interrupted = new Promise((_, reject) => {
425
+ onAbort = () => {
426
+ const reason = abort.signal.reason;
427
+ reject(reason instanceof Error
428
+ ? reason
429
+ : new Error("Process wait was interrupted.", { cause: reason }));
430
+ };
431
+ abort.signal.addEventListener("abort", onAbort, { once: true });
432
+ });
386
433
  try {
387
- const found = await this.#find(identifier);
388
- const out = await this.#output(found, true, abort.signal);
389
- return await this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
434
+ const result = (async () => {
435
+ const found = await this.#find(identifier, abort.signal);
436
+ const out = await this.#output(found, true, abort.signal);
437
+ abort.signal.throwIfAborted();
438
+ return await this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
439
+ })();
440
+ return await Promise.race([result, interrupted]);
390
441
  }
391
442
  catch (error) {
392
443
  if (abort.signal.aborted)
@@ -395,6 +446,8 @@ export class SandboxProcess {
395
446
  }
396
447
  finally {
397
448
  clearTimeout(timer);
449
+ if (onAbort)
450
+ abort.signal.removeEventListener("abort", onAbort);
398
451
  signal?.removeEventListener("abort", forward);
399
452
  }
400
453
  }
@@ -476,6 +529,8 @@ export class SandboxProcess {
476
529
  cursor: 0,
477
530
  signal: abort.signal,
478
531
  })) {
532
+ if (event.type === "truncated")
533
+ throw responseError(502, "Some command output is no longer available.");
479
534
  if (event.type === "stdout" || event.type === "stderr") {
480
535
  const lines = (partial[event.type] + event.data).split(/\r?\n/);
481
536
  partial[event.type] = lines.pop();
@@ -32,6 +32,8 @@ export type SandboxForkOptions = {
32
32
  /** A snapshot's id or, of this sandbox's snapshots, its name. */
33
33
  snapshotId?: string;
34
34
  envs?: Env[];
35
+ /** Blaxel's fork override; Runtime cannot enforce this override during a fork. */
36
+ lifecycle?: SandboxLifecycle;
35
37
  };
36
38
  export type SandboxArchiveOptions = {
37
39
  wait?: boolean;
@@ -750,6 +750,8 @@ export class SandboxInstance {
750
750
  * (files, memory and running processes), or started from a snapshot with
751
751
  * `snapshotId`. `envs` are added over this sandbox's. */
752
752
  async fork(targetName, options = {}) {
753
+ if (options.lifecycle !== undefined)
754
+ throw new NotSupportedError("Overriding lifecycle during a fork", "Omit lifecycle to use the existing Runtime fork behavior.");
753
755
  refuseApplication(options);
754
756
  const envs = envRecord(options.envs);
755
757
  checkEnvNames(Object.keys(envs));
@@ -22,13 +22,11 @@ export function snapshotModel(snapshot, sandboxName) {
22
22
  }
23
23
  /** Envs as a record; a nameless one is refused. */
24
24
  export function envRecord(envs) {
25
- const out = {};
26
- for (const env of envs ?? []) {
25
+ return Object.fromEntries((envs ?? []).map((env) => {
27
26
  if (!env.name)
28
27
  throw responseError(400, "every env needs a name");
29
- out[env.name] = env.value ?? "";
30
- }
31
- return out;
28
+ return [env.name, env.value ?? ""];
29
+ }));
32
30
  }
33
31
  /** Appends envs to a sandbox's env file: they win over the ones it has. */
34
32
  export async function appendEnvs(runtime, envs) {
@@ -0,0 +1,27 @@
1
+ import type { RuntimeError } from "./errors.js";
2
+ import type { SandboxMetrics } from "./products/observability.js";
3
+ /** A refusal of what a command sent, reworded for the command line: every
4
+ * issue named by its option, since the CLI never prints details.issues, and
5
+ * a hint that names the options rather than the request's fields. Anything
6
+ * else is left as the API said it. */
7
+ export declare function cliRefusal(error: RuntimeError): {
8
+ message: string;
9
+ hint?: string;
10
+ };
11
+ /** A resource read as one fact a line, as `sandbox get` prints: a list
12
+ * joined, a nested record as its own facts, nothing as `-`. */
13
+ export declare function describeRecord(record: Record<string, unknown>, table: (rows: string[][]) => string): string;
14
+ /** The newest CPU reading the metrics hold, and how old it is. `latest` is
15
+ * the last sample, which can carry no CPU figure (the first after a wake has
16
+ * nothing to compare with), while the points before it do; "no reading"
17
+ * above a sparkline of readings contradicted itself. */
18
+ export declare function newestCpu(m: SandboxMetrics, now?: number): {
19
+ percent: number;
20
+ cores: number | null;
21
+ ago: string | null;
22
+ } | null;
23
+ /** The lines of the top-level help about one command, ` feedback ...` and
24
+ * the lines that continue it, or null when it has none. `runtime feedback
25
+ * help` filed "help" as feedback, and `runtime support help` sent it to
26
+ * support, until 30 September 2026. */
27
+ export declare function topicHelp(help: string, command: string): string | null;
@@ -0,0 +1,135 @@
1
+ /* What the CLI says in its own words where the API's would mislead: the
2
+ option a refusal is about rather than the request field it was sent as,
3
+ one fact a line for a resource read, and the newest CPU reading a
4
+ sandbox's metrics hold. Kept out of cli.ts, which many hands edit. */
5
+ /** The request fields a command line sets, by the option that sets them.
6
+ * An amount in microdollars is typed in dollars. */
7
+ const OPTIONS = {
8
+ name: { flag: "--name" },
9
+ labels: { flag: "--label" },
10
+ env: { flag: "--env" },
11
+ region: { flag: "--region" },
12
+ vcpu: { flag: "--vcpu" },
13
+ memoryMiB: { flag: "--memory" },
14
+ diskMiB: { flag: "--disk" },
15
+ cpu: { flag: "--cpu" },
16
+ cpuFloorMillis: { flag: "--cpu-floor" },
17
+ timeoutSeconds: { flag: "--timeout" },
18
+ onLeaseEnd: { flag: "--on-timeout" },
19
+ idlePauseSeconds: { flag: "--idle-pause" },
20
+ autoWake: { flag: "--no-auto-wake / --auto-wake" },
21
+ persistent: { flag: "--persistent" },
22
+ funding: { flag: "--trial / --paid" },
23
+ image: { flag: "--image" },
24
+ snapshot: { flag: "--snapshot" },
25
+ volumes: { flag: "--volume" },
26
+ network: { flag: "--allow / --deny / --connect / --no-internet" },
27
+ maxCostMicros: { flag: "--max-cost", micros: true },
28
+ maxTotalCostMicros: { flag: "--max-total-cost", micros: true },
29
+ };
30
+ const usd = (micros) => `$${(micros / 1e6).toLocaleString("en-US", { maximumFractionDigits: 6 })}`;
31
+ /** One issue the API found in a request, in the command line's words:
32
+ * `body.timeoutSeconds must be at least 60` is `--timeout must be at least 60`. */
33
+ function issueWords(path, message) {
34
+ const field = /^(?:\$\.)?body\.([A-Za-z]+)/.exec(path)?.[1];
35
+ const option = field && Object.hasOwn(OPTIONS, field) ? OPTIONS[field] : undefined;
36
+ if (!option) {
37
+ const where = path.replace(/^\$\.?/, "").replace(/^(body|query|path)\./, "") || "the request";
38
+ return `${where} ${message}`;
39
+ }
40
+ const said = option.micros ? message.replace(/-?\d+(?:\.\d+)?/g, (n) => usd(Number(n))) : message;
41
+ return `${option.flag} ${said}`;
42
+ }
43
+ /** A refusal of what a command sent, reworded for the command line: every
44
+ * issue named by its option, since the CLI never prints details.issues, and
45
+ * a hint that names the options rather than the request's fields. Anything
46
+ * else is left as the API said it. */
47
+ export function cliRefusal(error) {
48
+ const issues = error.details?.issues;
49
+ if (error.code === "invalid_request" && Array.isArray(issues) && issues.length) {
50
+ const lines = issues
51
+ .filter((i) => typeof i.path === "string" && typeof i.message === "string")
52
+ .map((i) => issueWords(i.path, i.message));
53
+ if (lines.length)
54
+ return {
55
+ message: lines.length === 1 ? `${lines[0]}.` : lines.map((l) => `\n ${l}`).join(""),
56
+ hint: "Change what is named above and run the command again.",
57
+ };
58
+ }
59
+ const field = typeof error.details?.field === "string" ? error.details.field : "";
60
+ const option = Object.hasOwn(OPTIONS, field) ? OPTIONS[field] : undefined;
61
+ if (option && !option.micros && error.message.includes(field)) {
62
+ const named = new RegExp(`\\b${field}\\b`, "g");
63
+ return {
64
+ message: error.message.replace(named, option.flag),
65
+ ...(error.hint ? { hint: error.hint.replace(named, option.flag) } : {}),
66
+ };
67
+ }
68
+ return { message: error.message, ...(error.hint ? { hint: error.hint } : {}) };
69
+ }
70
+ /** A resource read as one fact a line, as `sandbox get` prints: a list
71
+ * joined, a nested record as its own facts, nothing as `-`. */
72
+ export function describeRecord(record, table) {
73
+ const rows = [];
74
+ const cell = (value) => {
75
+ if (value === null || value === undefined || value === "")
76
+ return "-";
77
+ if (Array.isArray(value))
78
+ return value.length
79
+ ? value.map((v) => (v && typeof v === "object" ? JSON.stringify(v) : String(v))).join(", ")
80
+ : "-";
81
+ // A string as it is; a number, a boolean or a record as JSON writes it.
82
+ return typeof value === "string" ? value : JSON.stringify(value);
83
+ };
84
+ const add = (prefix, value) => {
85
+ for (const [key, v] of Object.entries(value)) {
86
+ const name = prefix ? `${prefix}.${key}` : key;
87
+ if (v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length <= 8)
88
+ add(name, v);
89
+ else
90
+ rows.push([name, cell(v)]);
91
+ }
92
+ };
93
+ add("", record);
94
+ return table(rows);
95
+ }
96
+ /** The newest CPU reading the metrics hold, and how old it is. `latest` is
97
+ * the last sample, which can carry no CPU figure (the first after a wake has
98
+ * nothing to compare with), while the points before it do; "no reading"
99
+ * above a sparkline of readings contradicted itself. */
100
+ export function newestCpu(m, now = Date.now()) {
101
+ if (m.latest?.cpuPercent != null)
102
+ return { percent: m.latest.cpuPercent, cores: m.latest.cpuCores, ago: null };
103
+ for (let i = m.points.length - 1; i >= 0; i--) {
104
+ const p = m.points[i];
105
+ if (p.cpuPercent == null)
106
+ continue;
107
+ const seconds = Math.max(0, Math.round((now - Date.parse(p.at)) / 1000));
108
+ const ago = seconds < 90
109
+ ? `${seconds} s ago`
110
+ : seconds < 5400
111
+ ? `${Math.round(seconds / 60)} min ago`
112
+ : `${Math.round(seconds / 3600)} h ago`;
113
+ return { percent: p.cpuPercent, cores: p.cpuCores, ago };
114
+ }
115
+ return null;
116
+ }
117
+ /** The lines of the top-level help about one command, ` feedback ...` and
118
+ * the lines that continue it, or null when it has none. `runtime feedback
119
+ * help` filed "help" as feedback, and `runtime support help` sent it to
120
+ * support, until 30 September 2026. */
121
+ export function topicHelp(help, command) {
122
+ const lines = help.split("\n");
123
+ const out = [];
124
+ let inside = false;
125
+ for (const line of lines) {
126
+ const starts = line.startsWith(` ${command} `) || line === ` ${command}`;
127
+ if (starts)
128
+ inside = true;
129
+ else if (inside && !/^ {3,}\S/.test(line))
130
+ inside = false;
131
+ if (inside)
132
+ out.push(line);
133
+ }
134
+ return out.length ? out.join("\n") : null;
135
+ }
package/dist/cli.d.ts CHANGED
@@ -7,6 +7,8 @@ type Out = {
7
7
  json: boolean;
8
8
  write: (text: string) => void;
9
9
  error: (text: string) => void;
10
+ /** Wait when an output pipe cannot accept another page yet. */
11
+ drain?: () => Promise<void>;
10
12
  };
11
13
  type Args = {
12
14
  positional: string[];