withruntime 0.11.2 → 0.12.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 (164) hide show
  1. package/BLAXEL.md +2 -2
  2. package/COMPATIBILITY.md +74 -9
  3. package/DAYTONA.md +33 -5
  4. package/E2B.md +85 -24
  5. package/README.md +9 -8
  6. package/README.src.md +8 -7
  7. package/VERCEL.md +43 -5
  8. package/compatibility-lock.json +58 -49
  9. package/dist/billing-cli.d.ts +1 -1
  10. package/dist/billing-cli.js +77 -1
  11. package/dist/blaxel/errors.d.ts +14 -0
  12. package/dist/blaxel/errors.js +29 -0
  13. package/dist/blaxel/index.d.ts +2 -2
  14. package/dist/blaxel/index.js +4 -4
  15. package/dist/blaxel/interpreter.d.ts +2 -5
  16. package/dist/blaxel/sandbox.d.ts +14 -5
  17. package/dist/blaxel/sandbox.js +63 -7
  18. package/dist/blaxel/snapshot.d.ts +4 -0
  19. package/dist/blaxel/snapshot.js +4 -0
  20. package/dist/cli/logs.d.ts +21 -0
  21. package/dist/cli/logs.js +188 -0
  22. package/dist/cli/logs.test.d.ts +1 -0
  23. package/dist/cli/logs.test.js +272 -0
  24. package/dist/cli-extras.d.ts +3 -3
  25. package/dist/cli-extras.js +17 -12
  26. package/dist/cli-words.d.ts +10 -1
  27. package/dist/cli-words.js +18 -0
  28. package/dist/cli.d.ts +5 -1
  29. package/dist/cli.js +405 -204
  30. package/dist/client.d.ts +2 -4
  31. package/dist/client.js +27 -11
  32. package/dist/cloudflare/errors.d.ts +143 -0
  33. package/dist/cloudflare/errors.js +167 -0
  34. package/dist/cloudflare/index.d.ts +175 -6
  35. package/dist/cloudflare/index.js +416 -26
  36. package/dist/cloudflare/interpreter.d.ts +119 -0
  37. package/dist/cloudflare/interpreter.js +223 -0
  38. package/dist/cloudflare/streams.d.ts +38 -0
  39. package/dist/cloudflare/streams.js +145 -0
  40. package/dist/cloudflare/watch.d.ts +9 -0
  41. package/dist/cloudflare/watch.js +24 -0
  42. package/dist/codesandbox/index.d.ts +216 -28
  43. package/dist/codesandbox/index.js +478 -78
  44. package/dist/compat/core.d.ts +18 -0
  45. package/dist/compat/core.js +29 -0
  46. package/dist/compat/files.d.ts +1 -0
  47. package/dist/compat/files.js +25 -5
  48. package/dist/daytona/computer-use.d.ts +13 -4
  49. package/dist/daytona/computer-use.js +66 -8
  50. package/dist/daytona/daytona.d.ts +6 -0
  51. package/dist/daytona/daytona.js +30 -3
  52. package/dist/daytona/errors.d.ts +11 -0
  53. package/dist/daytona/errors.js +18 -0
  54. package/dist/daytona/filesystem.js +2 -1
  55. package/dist/daytona/image.d.ts +13 -9
  56. package/dist/daytona/image.js +18 -12
  57. package/dist/daytona/index.d.ts +1 -1
  58. package/dist/daytona/sandbox.d.ts +8 -2
  59. package/dist/daytona/sandbox.js +16 -2
  60. package/dist/domains/index.d.ts +9 -0
  61. package/dist/domains/index.js +10 -0
  62. package/dist/e2b/commands.d.ts +1 -1
  63. package/dist/e2b/commands.js +6 -7
  64. package/dist/e2b/errors.d.ts +17 -0
  65. package/dist/e2b/errors.js +40 -1
  66. package/dist/e2b/index.d.ts +2 -2
  67. package/dist/e2b/index.js +1 -1
  68. package/dist/e2b/sandbox.d.ts +27 -21
  69. package/dist/e2b/sandbox.js +34 -8
  70. package/dist/e2b/unsupported.js +1 -1
  71. package/dist/errors.d.ts +10 -1
  72. package/dist/errors.js +16 -4
  73. package/dist/freestyle/index.d.ts +285 -37
  74. package/dist/freestyle/index.js +656 -144
  75. package/dist/identity.d.ts +3 -0
  76. package/dist/identity.js +27 -0
  77. package/dist/images/index.d.ts +9 -0
  78. package/dist/images/index.js +10 -0
  79. package/dist/index.d.ts +1 -0
  80. package/dist/ips/index.d.ts +9 -0
  81. package/dist/ips/index.js +10 -0
  82. package/dist/jobs/index.d.ts +9 -0
  83. package/dist/jobs/index.js +10 -0
  84. package/dist/keys.js +2 -0
  85. package/dist/login.js +2 -0
  86. package/dist/machine.d.ts +12 -3
  87. package/dist/machine.js +20 -3
  88. package/dist/mcp.js +1 -1
  89. package/dist/modal/index.d.ts +339 -61
  90. package/dist/modal/index.js +781 -121
  91. package/dist/network-cli.d.ts +3 -1
  92. package/dist/network-cli.js +78 -36
  93. package/dist/networks/index.d.ts +11 -0
  94. package/dist/networks/index.js +10 -0
  95. package/dist/ports/index.d.ts +9 -0
  96. package/dist/ports/index.js +10 -0
  97. package/dist/product-names.d.ts +52 -0
  98. package/dist/product-names.js +164 -0
  99. package/dist/product-renames.d.ts +103 -0
  100. package/dist/product-renames.js +189 -0
  101. package/dist/products/apps.d.ts +176 -0
  102. package/dist/products/apps.js +165 -0
  103. package/dist/products/billing.d.ts +41 -0
  104. package/dist/products/billing.js +13 -0
  105. package/dist/products/browser.d.ts +10 -0
  106. package/dist/products/browsers.d.ts +71 -0
  107. package/dist/products/browsers.js +102 -0
  108. package/dist/products/builds.d.ts +126 -0
  109. package/dist/products/builds.js +221 -0
  110. package/dist/products/index.d.ts +3 -16
  111. package/dist/products/index.js +3 -14
  112. package/dist/products/jobs.d.ts +17 -0
  113. package/dist/products/jobs.js +27 -10
  114. package/dist/products/logs.d.ts +245 -0
  115. package/dist/products/logs.js +274 -0
  116. package/dist/products/logs.test.d.ts +1 -0
  117. package/dist/products/logs.test.js +343 -0
  118. package/dist/products/network-products.d.ts +38 -7
  119. package/dist/products/network-products.js +31 -18
  120. package/dist/products/observability.d.ts +3 -2
  121. package/dist/products/private-network.js +1 -1
  122. package/dist/products/queues.d.ts +149 -0
  123. package/dist/products/queues.js +213 -0
  124. package/dist/products/referrals.d.ts +21 -7
  125. package/dist/products/referrals.js +3 -3
  126. package/dist/products/secrets.d.ts +7 -3
  127. package/dist/products/secrets.js +39 -18
  128. package/dist/products/signed-requests.d.ts +21 -0
  129. package/dist/products/signed-requests.js +11 -0
  130. package/dist/products/streams.d.ts +73 -0
  131. package/dist/products/streams.js +17 -0
  132. package/dist/products/switching.d.ts +35 -7
  133. package/dist/products/switching.js +4 -3
  134. package/dist/products/volumes.d.ts +3 -0
  135. package/dist/record-cli.d.ts +23 -0
  136. package/dist/record-cli.js +308 -0
  137. package/dist/renamed-products.d.ts +38 -0
  138. package/dist/renamed-products.js +34 -0
  139. package/dist/runloop/index.d.ts +9 -2
  140. package/dist/runloop/index.js +29 -8
  141. package/dist/runloop/sdk.d.ts +131 -0
  142. package/dist/runloop/sdk.js +167 -0
  143. package/dist/sandbox.d.ts +3 -2
  144. package/dist/sandbox.js +14 -9
  145. package/dist/sandboxes/index.d.ts +11 -0
  146. package/dist/sandboxes/index.js +12 -0
  147. package/dist/snapshots/index.d.ts +9 -0
  148. package/dist/snapshots/index.js +11 -0
  149. package/dist/snapshots.d.ts +7 -4
  150. package/dist/sprites/index.d.ts +343 -31
  151. package/dist/sprites/index.js +866 -84
  152. package/dist/ssh.d.ts +1 -1
  153. package/dist/ssh.js +11 -11
  154. package/dist/terminal.d.ts +24 -0
  155. package/dist/terminal.js +32 -0
  156. package/dist/transport.d.ts +15 -1
  157. package/dist/transport.js +56 -3
  158. package/dist/tunnel.js +1 -1
  159. package/dist/types.d.ts +63 -0
  160. package/dist/vercel/sandbox.js +5 -3
  161. package/dist/vercel/snapshot.js +10 -2
  162. package/dist/volumes/index.d.ts +9 -0
  163. package/dist/volumes/index.js +10 -0
  164. package/package.json +48 -2
@@ -1,7 +1,7 @@
1
1
  import { RuntimeError } from "../errors.js";
2
2
  import { clientFor, defaultRegion, } from "./client.js";
3
3
  import { checkEnvNames, envFileCommand, envFileLines, parseProcessLine, toRuntimePath, } from "./context.js";
4
- import { codeOf, guard, NotSupportedError, responseError, translate } from "./errors.js";
4
+ import { codeOf, guard, NotSupportedError, responseError, SandboxCreationTimeoutError, translate, } from "./errors.js";
5
5
  import { SandboxFileSystem } from "./filesystem.js";
6
6
  import { ARCHIVED, DISPLAY_NAME, EXTERNAL_ID, IDLE_PAUSE, LABEL, findByExternalId, findSandbox, LEASE_SECONDS, ownLabels, STANDBY, userLabels, whenNameFree, } from "./lookup.js";
7
7
  import { paginate } from "./pagination.js";
@@ -38,6 +38,56 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
38
38
  const DAY_MS = 86_400_000;
39
39
  /** The lease is renewed in the background once this little of it is left. */
40
40
  const RENEW_BELOW_MS = 10 * 60_000;
41
+ /** The longest creation `timeout` Blaxel accepts, in seconds. */
42
+ export const MAX_CREATION_TIMEOUT_SECONDS = 50;
43
+ /** Blaxel's own check of `timeout`, with its words. */
44
+ function creationTimeout({ timeout }) {
45
+ if (timeout === undefined)
46
+ return undefined;
47
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > MAX_CREATION_TIMEOUT_SECONDS)
48
+ throw new Error(`SandboxInstance.create: 'timeout' must be a whole number of seconds between 1 and ${MAX_CREATION_TIMEOUT_SECONDS}, got ${timeout}.`);
49
+ return timeout;
50
+ }
51
+ /** A create that must be running within `timeout` seconds of the call, the
52
+ * wait for capacity included. One that is not is deleted, so the name is
53
+ * free again, and the call rejects with SandboxCreationTimeoutError. */
54
+ async function createWithin(client, input, timeout) {
55
+ const deadline = performance.now() + timeout * 1000;
56
+ const runtime = await guard(() => whenNameFree(client, () => client.sandboxes.create({ ...input, wait: false }, { waitForCapacityMs: timeout * 1000 })));
57
+ // A sandbox the name already had is the caller's, whatever its state: it is
58
+ // answered as it is, as without a timeout, and never deleted here.
59
+ if (runtime.state === "running" || runtime.info.reused)
60
+ return runtime;
61
+ const left = deadline - performance.now();
62
+ if (left > 0)
63
+ try {
64
+ await runtime.waitFor("running", {
65
+ timeoutSeconds: Math.max(1, Math.ceil(left / 1000)),
66
+ timeoutMs: Math.ceil(left) + 5_000,
67
+ });
68
+ }
69
+ catch (error) {
70
+ if (!(error instanceof RuntimeError && error.code === "timeout")) {
71
+ await runtime.delete().catch(() => undefined);
72
+ throw translate(error);
73
+ }
74
+ }
75
+ // waitFor updated the state TypeScript narrowed above.
76
+ const state = runtime.state;
77
+ if (state === "running")
78
+ return runtime;
79
+ await runtime.delete().catch(() => undefined);
80
+ if (state !== "starting")
81
+ throw translate(new RuntimeError({
82
+ message: `Sandbox ${runtime.id} is ${runtime.state}, not running.`,
83
+ code: "start_failed",
84
+ status: 0,
85
+ hint: "Create it again; SandboxInstance.get(name) reads why it stopped.",
86
+ }));
87
+ throw new SandboxCreationTimeoutError(input.name, timeout, {
88
+ message: `It was still starting after ${timeout}s.`,
89
+ });
90
+ }
41
91
  /** The labels a new sandbox carries: the caller's, and under `blaxel/` what
42
92
  * Blaxel keeps and Runtime has no field for, so any client (the Python
43
93
  * adapter too) reads the same `spec`. */
@@ -212,7 +262,7 @@ async function resolveImage(client, image) {
212
262
  if (found && found.state === "ready")
213
263
  return { image: found.id };
214
264
  const feature = `The image ${image}, which is not a Runtime image`;
215
- const alternative = `Build it as a Runtime image named ${name}: \`npx withruntime image build --dockerfile Dockerfile --name ${name} -t ${name}:${tag}\`. The code can keep "${image}": the adapter starts from ${name}:${tag}.`;
265
+ const alternative = `Build it as a Runtime image named ${name}: \`npx withruntime images build --dockerfile Dockerfile --name ${name} -t ${name}:${tag}\`. The code can keep "${image}": the adapter starts from ${name}:${tag}.`;
216
266
  throw new NotSupportedError(feature, alternative, `${feature}, is not supported on Runtime. ${alternative}`);
217
267
  }
218
268
  /** Blaxel volumes (by name) as Runtime volumes. */
@@ -228,7 +278,7 @@ async function resolveVolumes(client, volumes) {
228
278
  return Promise.all(volumes.map(async (volume) => {
229
279
  const found = (await guard(() => client.volumes.list({ name: volume.name }))).data[0];
230
280
  if (!found)
231
- throw new NotSupportedError(`The volume ${volume.name}, which is not a Runtime volume`, `Create it first: runtime.volumes.create({ name: "${volume.name}" }) or \`npx withruntime volume create --name ${volume.name}\`.`);
281
+ throw new NotSupportedError(`The volume ${volume.name}, which is not a Runtime volume`, `Create it first: runtime.volumes.create({ name: "${volume.name}" }) or \`npx withruntime volumes create --name ${volume.name}\`.`);
232
282
  return { volumeId: found.id, path: toRuntimePath(volume.mountPath) };
233
283
  }));
234
284
  }
@@ -525,7 +575,9 @@ export class SandboxInstance {
525
575
  * image (Runtime's stock image) and 4096 MB, with vCPUs at one per 2048 MB.
526
576
  * Funding is left to Runtime: the included usage first, then prepaid credit. A name another live sandbox holds is refused
527
577
  * with a 409, as in Blaxel. */
528
- static async create(sandbox, { createIfNotExist = false } = {}) {
578
+ static async create(sandbox, options = {}) {
579
+ const { createIfNotExist = false, safe = false } = options;
580
+ const timeout = creationTimeout(options);
529
581
  const config = configOf(sandbox);
530
582
  const region = config.region || defaultRegion();
531
583
  checkRegion(region);
@@ -559,7 +611,9 @@ export class SandboxInstance {
559
611
  ...(createIfNotExist && config.name ? { getOrCreate: true } : {}),
560
612
  ...config.withruntime?.create,
561
613
  };
562
- const runtime = await guard(() => whenNameFree(client, () => client.sandboxes.create(input)));
614
+ const runtime = timeout === undefined
615
+ ? await guard(() => whenNameFree(client, () => client.sandboxes.create(input)))
616
+ : await createWithin(client, input, timeout);
563
617
  const asked = {
564
618
  ...(Object.keys(envs).length ? { envs } : {}),
565
619
  ...(config.network ? { network: config.network } : {}),
@@ -571,6 +625,8 @@ export class SandboxInstance {
571
625
  const instance = new this({ runtime, client, asked });
572
626
  try {
573
627
  await instance.#setUp(envs, lifetime.days);
628
+ if (safe)
629
+ await instance.fs.ls("/");
574
630
  }
575
631
  catch (error) {
576
632
  await runtime.stop({ wait: false }).catch(() => undefined);
@@ -580,8 +636,8 @@ export class SandboxInstance {
580
636
  }
581
637
  /** The named sandbox, or a new one when no live sandbox has the name.
582
638
  * Runtime settles a race between two callers: both get the same sandbox. */
583
- static async createIfNotExists(sandbox) {
584
- return this.create(sandbox, { createIfNotExist: true });
639
+ static async createIfNotExists(sandbox, options = {}) {
640
+ return this.create(sandbox, { ...options, createIfNotExist: true });
585
641
  }
586
642
  /** The live sandbox with this name (or Runtime id). It is not woken: the
587
643
  * next call wakes it. */
@@ -71,6 +71,10 @@ export declare function appendEnvs(runtime: RuntimeSandbox, envs: Record<string,
71
71
  export declare function startFromSnapshot(client: Runtime, snapshotId: string, name: string, labels: Record<string, string>, envs: Record<string, string>): Promise<RuntimeSandbox>;
72
72
  declare function refuseApplication(options?: {
73
73
  targetType?: string;
74
+ port?: number;
75
+ traffic?: number;
76
+ customDomain?: string;
77
+ prefix?: string;
74
78
  }): void;
75
79
  /** A snapshot, as Blaxel's Snapshot class. */
76
80
  export declare class Snapshot {
@@ -63,6 +63,10 @@ export async function startFromSnapshot(client, snapshotId, name, labels, envs)
63
63
  function refuseApplication(options = {}) {
64
64
  if (options.targetType === "application")
65
65
  throw new NotSupportedError("Forking into an application", "Fork into a sandbox and share its port with sandbox.previews.create.");
66
+ // Blaxel's application-fork routing: a sandbox fork has no use for them.
67
+ for (const key of ["port", "traffic", "customDomain", "prefix"])
68
+ if (options[key] !== undefined)
69
+ throw new NotSupportedError(`A fork's ${key}`, "Fork without it, then share a port of the copy with sandbox.previews.create({ metadata: { name }, spec: { port } }).");
66
70
  }
67
71
  /** A snapshot, as Blaxel's Snapshot class. */
68
72
  export class Snapshot {
@@ -0,0 +1,21 @@
1
+ import type { ObservabilityClient } from "../products/logs.js";
2
+ type Product = "logs" | "traces" | "alerts";
3
+ type Args = {
4
+ positional: string[];
5
+ flags: Map<string, string[]>;
6
+ };
7
+ type Out = {
8
+ json: boolean;
9
+ tty?: boolean;
10
+ drain?: () => Promise<void>;
11
+ write(text: string): void;
12
+ error(text: string): void;
13
+ };
14
+ export declare const LOGS_HELP = "runtime logs tail <selector|app> [--since 1h] [--grep text] [--json] [--raw]\nruntime logs query '<LogQL>' [--from <time>] [--to <time>] [--limit <n>]\nruntime logs labels | values <label>\nruntime logs views ls|get|create|update|delete [id] [--body '<JSON>']\nruntime traces get <id> | search '<TraceQL>' | tags\nruntime traces views ls|get|create|update|delete [id] [--body '<JSON>']\nruntime logs ingest --body '<OTLP JSON>'\nruntime traces ingest --body '<OTLP JSON>'\nruntime alerts ls|get|create|update|delete|test|silence [id]\n create/update: --body '<JSON: name, labels, query, condition, for, actions>'\n silence: --until <RFC3339> [--reason <text>]\nEvery command takes --json. Writes take --idempotency-key <key>.\nOn a terminal, control characters in logs show as \\uXXXX; --raw prints them as written.";
15
+ /** Same parsed argument and output ports as the existing CLI. No registration
16
+ * occurs here: the launch owner supplies a client only after launch. */
17
+ export declare function logsProductCommand(product: Product, argv: string[], args: Args, out: Out, client: () => Promise<ObservabilityClient>, options?: {
18
+ signal?: AbortSignal;
19
+ now?: number;
20
+ }): Promise<number>;
21
+ export {};
@@ -0,0 +1,188 @@
1
+ import { RuntimeError } from "../errors.js";
2
+ import { terminalSafe } from "../terminal.js";
3
+ export const LOGS_HELP = `runtime logs tail <selector|app> [--since 1h] [--grep text] [--json] [--raw]
4
+ runtime logs query '<LogQL>' [--from <time>] [--to <time>] [--limit <n>]
5
+ runtime logs labels | values <label>
6
+ runtime logs views ls|get|create|update|delete [id] [--body '<JSON>']
7
+ runtime traces get <id> | search '<TraceQL>' | tags
8
+ runtime traces views ls|get|create|update|delete [id] [--body '<JSON>']
9
+ runtime logs ingest --body '<OTLP JSON>'
10
+ runtime traces ingest --body '<OTLP JSON>'
11
+ runtime alerts ls|get|create|update|delete|test|silence [id]
12
+ create/update: --body '<JSON: name, labels, query, condition, for, actions>'
13
+ silence: --until <RFC3339> [--reason <text>]
14
+ Every command takes --json. Writes take --idempotency-key <key>.
15
+ On a terminal, control characters in logs show as \\uXXXX; --raw prints them as written.`;
16
+ const usage = (message) => new RuntimeError({ code: "usage", status: 0, message, hint: LOGS_HELP });
17
+ function since(text, now) {
18
+ const match = /^(\d+)(s|m|h|d)$/.exec(text);
19
+ if (!match)
20
+ throw usage("Use --since with seconds, minutes, hours or days, such as 1h.");
21
+ const factors = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
22
+ const at = now - Number(match[1]) * factors[match[2]];
23
+ if (!Number.isSafeInteger(at) || at < 0)
24
+ throw usage("--since is outside the supported time range.");
25
+ return new Date(at).toISOString();
26
+ }
27
+ /** Same parsed argument and output ports as the existing CLI. No registration
28
+ * occurs here: the launch owner supplies a client only after launch. */
29
+ export async function logsProductCommand(product, argv, args, out, client, options = {}) {
30
+ const verb = argv[0];
31
+ const rest = args.positional.slice(1);
32
+ const flag = (name) => args.flags.get(name)?.at(-1);
33
+ const json = out.json || flag("json") === "true";
34
+ // On a terminal, what customers' programs wrote is shown escaped, JSON
35
+ // included (an escape is still valid JSON); --raw prints it as it is.
36
+ const safe = (text) => (out.tty && flag("raw") !== "true" ? terminalSafe(text) : text);
37
+ const lines = (text) => text.split("\n").map(safe).join("\n");
38
+ const print = (value, text) => out.write(json ? safe(JSON.stringify(value)) : lines(text ?? JSON.stringify(value, null, 2)));
39
+ const need = (value, name) => {
40
+ if (!value)
41
+ throw usage(`Give ${name}.`);
42
+ return value;
43
+ };
44
+ const body = () => {
45
+ try {
46
+ const value = JSON.parse(need(flag("body"), "--body with a JSON object"));
47
+ if (!value || typeof value !== "object" || Array.isArray(value))
48
+ throw usage("--body must be a JSON object.");
49
+ return value;
50
+ }
51
+ catch (error) {
52
+ if (error instanceof RuntimeError)
53
+ throw error;
54
+ throw usage("--body is not valid JSON.");
55
+ }
56
+ };
57
+ const request = {
58
+ ...(options.signal ? { signal: options.signal } : {}),
59
+ ...(flag("idempotency-key") ? { idempotencyKey: flag("idempotency-key") } : {}),
60
+ };
61
+ if (!verb || verb === "help" || verb === "--help") {
62
+ print({ usage: LOGS_HELP }, LOGS_HELP);
63
+ return 0;
64
+ }
65
+ const runtime = await client();
66
+ if ((product === "logs" || product === "traces") && verb === "views") {
67
+ const views = runtime[product].views;
68
+ const action = need(rest[0], "a views command");
69
+ if (action === "ls") {
70
+ const result = await views.list({}, request);
71
+ print({ data: result.data, nextCursor: result.nextCursor });
72
+ }
73
+ else if (action === "create")
74
+ print(await views.create(body(), request));
75
+ else {
76
+ const id = need(rest[1], "a view id");
77
+ if (action === "get")
78
+ print(await views.get(id, request));
79
+ else if (action === "update")
80
+ print(await views.update(id, body(), request));
81
+ else if (action === "delete") {
82
+ await views.delete(id, request);
83
+ print({ deleted: id });
84
+ }
85
+ else
86
+ throw usage(`Unknown views command: ${action}.`);
87
+ }
88
+ return 0;
89
+ }
90
+ if ((product === "logs" || product === "traces") && verb === "ingest") {
91
+ const input = body();
92
+ if (product === "logs") {
93
+ if (!Array.isArray(input.resourceLogs))
94
+ throw usage("OTLP logs need resourceLogs.");
95
+ const resourceLogs = input.resourceLogs;
96
+ print(await runtime.logs.ingest({ resourceLogs }, request));
97
+ }
98
+ else {
99
+ if (!Array.isArray(input.resourceSpans))
100
+ throw usage("OTLP spans need resourceSpans.");
101
+ const resourceSpans = input.resourceSpans;
102
+ print(await runtime.traces.ingest({ resourceSpans }, request));
103
+ }
104
+ return 0;
105
+ }
106
+ if (product === "logs") {
107
+ if (verb === "tail") {
108
+ const selector = need(rest[0], "a selector or app name");
109
+ let query = selector.trim().startsWith("{") ? selector : `{app=${JSON.stringify(selector)}}`;
110
+ if (flag("grep") !== undefined)
111
+ query += ` |= ${JSON.stringify(flag("grep"))}`;
112
+ for await (const event of runtime.logs.tail(query, {
113
+ ...request,
114
+ ...(flag("since") ? { start: since(flag("since"), options.now ?? Date.now()) } : {}),
115
+ })) {
116
+ if (json)
117
+ out.write(safe(JSON.stringify(event)));
118
+ else if (event.gap) {
119
+ const text = safe(`Logs expired between ${event.from} and ${event.to}; continuing from retained logs.`);
120
+ out.write(out.tty ? `\x1b[2m${text}\x1b[0m` : text);
121
+ }
122
+ else {
123
+ const time = safe(event.time);
124
+ const text = safe(typeof event.body === "string" ? event.body : JSON.stringify(event.body));
125
+ out.write(`${out.tty ? `\x1b[2m${time}\x1b[0m` : time} ${safe(event.severityText)} ${text}`);
126
+ }
127
+ await out.drain?.();
128
+ }
129
+ }
130
+ else if (verb === "query") {
131
+ const limit = flag("limit") === undefined ? undefined : Number(flag("limit"));
132
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1))
133
+ throw usage("--limit must be a positive integer.");
134
+ print(await runtime.logs.queryRange({
135
+ query: need(rest[0], "a LogQL query"),
136
+ ...(flag("from") ? { start: flag("from") } : {}),
137
+ ...(flag("to") ? { end: flag("to") } : {}),
138
+ ...(limit !== undefined ? { limit } : {}),
139
+ }, request));
140
+ }
141
+ else if (verb === "labels")
142
+ print(await runtime.logs.labels({}, request));
143
+ else if (verb === "values")
144
+ print(await runtime.logs.labelValues(need(rest[0], "a label name"), {}, request));
145
+ else
146
+ throw usage(`Unknown logs command: ${verb}.`);
147
+ }
148
+ else if (product === "traces") {
149
+ if (verb === "get")
150
+ print(await runtime.traces.get(need(rest[0], "a trace id"), request));
151
+ else if (verb === "search")
152
+ print(await runtime.traces.search(need(rest[0], "a TraceQL query"), request));
153
+ else if (verb === "tags")
154
+ print(await runtime.traces.tags(request));
155
+ else
156
+ throw usage(`Unknown traces command: ${verb}.`);
157
+ }
158
+ else {
159
+ if (verb === "ls") {
160
+ const result = await runtime.alerts.list({}, request);
161
+ print({ data: result.data, nextCursor: result.nextCursor });
162
+ }
163
+ else if (verb === "create")
164
+ print(await runtime.alerts.create(body(), request));
165
+ else {
166
+ const id = need(rest[0], "an alert id");
167
+ if (verb === "get")
168
+ print(await runtime.alerts.get(id, request));
169
+ else if (verb === "update")
170
+ print(await runtime.alerts.update(id, body(), request));
171
+ else if (verb === "delete") {
172
+ await runtime.alerts.delete(id, request);
173
+ print({ deleted: id });
174
+ }
175
+ else if (verb === "test")
176
+ print(await runtime.alerts.test(id, request));
177
+ else if (verb === "silence") {
178
+ const until = need(flag("until"), "--until with an RFC3339 time");
179
+ if (!/T.*(?:Z|[+-]\d\d:\d\d)$/.test(until) || !Number.isFinite(Date.parse(until)))
180
+ throw usage("--until must be an RFC3339 time with timezone.");
181
+ print(await runtime.alerts.silence(id, { until, ...(flag("reason") ? { reason: flag("reason") } : {}) }, request));
182
+ }
183
+ else
184
+ throw usage(`Unknown alerts command: ${verb}.`);
185
+ }
186
+ }
187
+ return 0;
188
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,272 @@
1
+ import { expect, test } from "bun:test";
2
+ import { logsProductCommand } from "./logs.js";
3
+ import { terminalSafe } from "../terminal.js";
4
+ import { logs, traces, alerts } from "../products/logs.js";
5
+ import { Transport } from "../transport.js";
6
+ import { InvalidRequestError } from "../errors.js";
7
+ const input = {
8
+ name: "errors",
9
+ labels: {},
10
+ query: '{app="api"}',
11
+ condition: "above 5",
12
+ for: "5m",
13
+ actions: [],
14
+ };
15
+ const cases = [
16
+ ["logs", ["query", "{}"], { from: "1", to: "2", limit: "5" }, "GET", "/v1/logs/query_range"],
17
+ ["logs", ["labels"], {}, "GET", "/v1/logs/labels"],
18
+ ["logs", ["values", "app"], {}, "GET", "/v1/logs/labels/app/values"],
19
+ ["logs", ["ingest"], { body: '{"resourceLogs":[]}' }, "POST", "/v1/logs"],
20
+ ["traces", ["ingest"], { body: '{"resourceSpans":[]}' }, "POST", "/v1/traces"],
21
+ ["traces", ["get", "id"], {}, "GET", "/v1/traces/id"],
22
+ ["traces", ["search", "{}"], {}, "GET", "/v1/traces:search"],
23
+ ["traces", ["tags"], {}, "GET", "/v1/traces/tags"],
24
+ ["alerts", ["test", "id"], {}, "POST", "/v1/alerts/id/test"],
25
+ [
26
+ "alerts",
27
+ ["silence", "id"],
28
+ { until: "2026-10-07T00:00:00Z", reason: "maintenance" },
29
+ "POST",
30
+ "/v1/alerts/id/silence",
31
+ ],
32
+ ];
33
+ for (const product of ["logs", "traces", "alerts"])
34
+ for (const verb of ["ls", "get", "create", "update", "delete"]) {
35
+ const collection = `/v1/${product}${product === "alerts" ? "" : "/views"}`;
36
+ cases.push([
37
+ product,
38
+ [
39
+ ...(product === "alerts" ? [] : ["views"]),
40
+ verb,
41
+ ...(["get", "update", "delete"].includes(verb) ? ["id"] : []),
42
+ ],
43
+ ["create", "update"].includes(verb) ? { body: JSON.stringify(input) } : {},
44
+ { ls: "GET", get: "GET", create: "POST", update: "PUT", delete: "DELETE" }[verb],
45
+ collection + (["get", "update", "delete"].includes(verb) ? "/id" : ""),
46
+ ]);
47
+ }
48
+ function setup() {
49
+ const requests = [];
50
+ const transport = new Transport({
51
+ apiKey: "fake",
52
+ baseUrl: "https://fake.test",
53
+ maxRetries: 0,
54
+ fetch: (async (url, init) => {
55
+ requests.push({ url: new URL(url instanceof Request ? url.url : url), init: init ?? {} });
56
+ return Response.json({ ...input, id: "id", data: [], nextCursor: null, marker: "ok" });
57
+ }),
58
+ });
59
+ return {
60
+ client: { logs: logs(transport), traces: traces(transport), alerts: alerts(transport) },
61
+ requests,
62
+ };
63
+ }
64
+ for (const [product, positional, flags, method, path] of cases)
65
+ for (const mode of ["tty", "pipe", "json"])
66
+ test(`${product} ${positional.join(" ")} ${mode}`, async () => {
67
+ const { client, requests } = setup(), output = [];
68
+ const result = await logsProductCommand(product, positional, {
69
+ positional,
70
+ flags: new Map(Object.entries({ ...flags, "idempotency-key": "retry-key" }).map(([key, value]) => [
71
+ key,
72
+ [value],
73
+ ])),
74
+ }, {
75
+ json: mode === "json",
76
+ tty: mode === "tty",
77
+ write: (text) => output.push(text),
78
+ error: () => {
79
+ throw new Error("unexpected stderr");
80
+ },
81
+ }, async () => client);
82
+ expect(result).toBe(0);
83
+ expect(requests).toHaveLength(1);
84
+ expect(requests[0].url.pathname).toBe(path);
85
+ expect(requests[0].init.method).toBe(method);
86
+ expect(output).toHaveLength(1);
87
+ expect(output[0]).not.toContain("\x1b");
88
+ const printed = JSON.parse(output[0]);
89
+ if (positional.includes("delete"))
90
+ expect(printed).toEqual({ deleted: "id" });
91
+ else if (positional.includes("ls"))
92
+ expect(printed).toEqual({ data: [], nextCursor: null });
93
+ else
94
+ expect(printed.marker).toBe("ok");
95
+ if (method !== "GET")
96
+ expect(new Headers(requests[0].init.headers).get("idempotency-key")).toBe("retry-key");
97
+ if (positional[0] === "query")
98
+ expect(Object.fromEntries(requests[0].url.searchParams)).toEqual({
99
+ query: "{}",
100
+ start: "1",
101
+ end: "2",
102
+ limit: "5",
103
+ });
104
+ });
105
+ for (const mode of ["tty", "pipe", "json"])
106
+ test(`tail ${mode}, selector escaping, gap and backpressure`, async () => {
107
+ const { client } = setup();
108
+ const output = [];
109
+ let drained = 0;
110
+ const events = [
111
+ {
112
+ time: "12:00",
113
+ timeUnixNano: "1",
114
+ severity: 9,
115
+ severityText: "INFO",
116
+ labels: {},
117
+ attributes: {},
118
+ traceId: null,
119
+ spanId: null,
120
+ body: "line\x1b[2J\nnext",
121
+ cursor: "one",
122
+ },
123
+ { gap: true, from: "old", to: "kept" },
124
+ ];
125
+ let received;
126
+ client.logs.tail = async function* (query, options) {
127
+ received = { query, options };
128
+ yield* events;
129
+ };
130
+ const positional = ["tail", 'app"name'];
131
+ await logsProductCommand("logs", positional, {
132
+ positional,
133
+ flags: new Map([
134
+ ["since", ["1h"]],
135
+ ["grep", ['a"b']],
136
+ ]),
137
+ }, {
138
+ json: mode === "json",
139
+ tty: mode === "tty",
140
+ write: (text) => output.push(text),
141
+ error() { },
142
+ async drain() {
143
+ drained++;
144
+ },
145
+ }, async () => client, { now: Date.parse("2026-10-06T12:00:00Z") });
146
+ expect(received).toEqual({
147
+ query: '{app="app\\"name"} |= "a\\"b"',
148
+ options: { start: "2026-10-06T11:00:00.000Z" },
149
+ });
150
+ expect(drained).toBe(2);
151
+ if (mode === "json")
152
+ expect(output.map((text) => JSON.parse(text))).toEqual(events);
153
+ else {
154
+ if (mode === "tty")
155
+ expect(output[0]).toContain("line\\u001b[2J\\u000anext");
156
+ else
157
+ expect(output[0]).toContain("line\u001b[2J\nnext");
158
+ expect(output[1]).toContain("Logs expired");
159
+ expect(output[1].includes("\x1b[2m")).toBe(mode === "tty");
160
+ }
161
+ });
162
+ test("tail survives an actual fake-fetch disconnect through the SDK", async () => {
163
+ let calls = 0;
164
+ const cursors = [], output = [];
165
+ const abort = new AbortController();
166
+ const transport = new Transport({
167
+ apiKey: "fake",
168
+ baseUrl: "https://fake.test",
169
+ fetch: (async (input) => {
170
+ cursors.push(new URL(input instanceof Request ? input.url : input).searchParams.get("cursor"));
171
+ const cursor = String(++calls);
172
+ return new Response(`id: ${cursor}\ndata: ${JSON.stringify({ time: "now", timeUnixNano: "1", labels: {}, body: "same", severityText: "INFO" })}\n\n`, { headers: { "content-type": "text/event-stream" } });
173
+ }),
174
+ });
175
+ const client = { logs: logs(transport), traces: traces(transport), alerts: alerts(transport) };
176
+ const run = logsProductCommand("logs", ["tail", "{}"], { positional: ["tail", "{}"], flags: new Map() }, {
177
+ json: true,
178
+ write(text) {
179
+ output.push(text);
180
+ if (output.length === 2)
181
+ abort.abort(new Error("done"));
182
+ },
183
+ error() { },
184
+ }, async () => client, { signal: abort.signal });
185
+ await expect(run).rejects.toThrow("done");
186
+ expect(cursors).toEqual([null, "1"]);
187
+ expect(output.map((text) => JSON.parse(text).cursor)).toEqual(["1", "2"]);
188
+ });
189
+ for (const [product, positional, flags] of [
190
+ ["logs", ["query", "{}"], { limit: "0" }],
191
+ ["logs", ["query"], {}],
192
+ ["logs", ["tail", "api"], { since: "forever" }],
193
+ ["alerts", ["create"], { body: "[1]" }],
194
+ ["alerts", ["create"], { body: "broken" }],
195
+ ["alerts", ["silence", "id"], { until: "tomorrow" }],
196
+ ["logs", ["ingest"], { body: "{}" }],
197
+ ["traces", ["ingest"], { body: "{}" }],
198
+ ["alerts", ["oops", "id"], {}],
199
+ ])
200
+ test(`refuses ${positional.join(" ")} ${JSON.stringify(flags)}`, async () => {
201
+ const { client, requests } = setup();
202
+ await expect(logsProductCommand(product, positional, { positional, flags: new Map(Object.entries(flags).map(([key, value]) => [key, [value]])) }, { json: true, write() { }, error() { } }, async () => client)).rejects.toMatchObject({ code: "usage", hint: expect.any(String) });
203
+ expect(requests).toHaveLength(0);
204
+ });
205
+ test("help needs no credentials", async () => {
206
+ const output = [];
207
+ await logsProductCommand("logs", ["help"], { positional: ["help"], flags: new Map() }, { json: false, write: (text) => output.push(text), error() { } }, async () => {
208
+ throw new Error("credentials requested");
209
+ });
210
+ expect(output[0]).toContain("\nruntime logs query");
211
+ });
212
+ test("typed failures reach the main CLI's JSON error renderer unchanged", async () => {
213
+ const { client } = setup();
214
+ const failure = new InvalidRequestError({
215
+ code: "query_limit",
216
+ message: "Too broad",
217
+ status: 400,
218
+ hint: "Narrow the time range.",
219
+ });
220
+ client.logs.queryRange = async () => {
221
+ throw failure;
222
+ };
223
+ await expect(logsProductCommand("logs", ["query", "{}"], { positional: ["query", "{}"], flags: new Map() }, {
224
+ json: true,
225
+ write() {
226
+ throw new Error("Do not print success on failure");
227
+ },
228
+ error() { },
229
+ }, async () => client)).rejects.toBe(failure);
230
+ });
231
+ test("human JSON keeps formatting but escapes C1 terminal controls", async () => {
232
+ const { client } = setup();
233
+ const output = [];
234
+ client.traces.get = async () => ({ resourceSpans: [], marker: "\u009b2J" });
235
+ await logsProductCommand("traces", ["get", "id"], { positional: ["get", "id"], flags: new Map() }, { json: false, tty: true, write: (text) => output.push(text), error() { } }, async () => client);
236
+ expect(output[0]).not.toContain("\u009b");
237
+ expect(output[0]).toContain("\\u009b");
238
+ expect(output[0]).toContain("\n");
239
+ expect(JSON.parse(output[0]).marker).toBe("\u009b2J");
240
+ });
241
+ test("a terminal shows what customers' programs wrote, escaped, and nothing acts", async () => {
242
+ // Clipboard (OSC 52), a forged link (OSC 8), screen clear, the one-byte C1
243
+ // CSI and a right-to-left override; tab stays.
244
+ const hostile = "a\tb \x1b]52;c;c2VjcmV0\x07 \x1b]8;;https://evil.test\x07ok\x1b]8;;\x07 \x1b[2J \u009b2J \u202eevil";
245
+ const shown = terminalSafe(hostile);
246
+ // eslint-disable-next-line no-control-regex -- the test looks for control characters
247
+ expect(shown).not.toMatch(/[\x00-\x08\x0a-\x1f\x7f-\x9f\u202e]/);
248
+ expect(shown).toContain("a\tb");
249
+ expect(shown).toContain("\\u001b]52;c;c2VjcmV0\\u0007");
250
+ expect(shown).toContain("\\u009b2J \\u202eevil");
251
+ const run = async (tty, raw, json) => {
252
+ const { client } = setup();
253
+ client.logs.tail = async function* () {
254
+ yield { cursor: "c", time: "t", severityText: "INFO", body: hostile, labels: {} };
255
+ };
256
+ const output = [];
257
+ const flags = new Map(raw ? [["raw", ["true"]]] : []);
258
+ await logsProductCommand("logs", ["tail", "{}"], { positional: ["tail", "{}"], flags }, { json, tty, write: (text) => output.push(text), error() { } }, async () => client);
259
+ return output.join("\n");
260
+ };
261
+ for (const json of [false, true]) {
262
+ const onTerminal = await run(true, false, json);
263
+ expect(onTerminal).not.toContain("\x1b]");
264
+ expect(onTerminal).not.toContain("\u009b");
265
+ expect(onTerminal).not.toContain("\u202e");
266
+ if (json)
267
+ expect(JSON.parse(onTerminal).body).toBe(hostile);
268
+ // A pipe, or --raw on a terminal, gets exactly what was written.
269
+ expect(await run(false, false, json)).toContain(json ? JSON.stringify(hostile) : hostile);
270
+ expect(await run(true, true, json)).toContain(json ? JSON.stringify(hostile) : hostile);
271
+ }
272
+ });
@@ -22,9 +22,9 @@ export declare function lostOutput(result: {
22
22
  stdoutTruncated?: boolean;
23
23
  stderrTruncated?: boolean;
24
24
  }, id: string): string | undefined;
25
- /** Feeds `runtime sandbox shell` from standard input. When piped input ends,
25
+ /** Feeds `runtime sandboxes shell` from standard input. When piped input ends,
26
26
  * the shell is told so the way a terminal says it, Ctrl-D, after a newline
27
- * if the last line had none: `echo ls | runtime sandbox shell <id>` runs `ls`
27
+ * if the last line had none: `echo ls | runtime sandboxes shell <id>` runs `ls`
28
28
  * and exits, as ssh does, where it used to wait forever (25 September 2026).
29
29
  * A keyboard never ends, so a terminal's input is left alone. Returns the
30
30
  * function that unhooks it. */
@@ -33,7 +33,7 @@ export declare function pipeShellInput(stdin: NodeJS.ReadableStream & {
33
33
  }, terminal: {
34
34
  write(data: Uint8Array | string): void;
35
35
  }): () => void;
36
- /** `runtime snapshot get`, as `runtime sandbox get` reads: one fact a line.
36
+ /** `runtime snapshots get`, as `runtime sandboxes get` reads: one fact a line.
37
37
  * It printed raw JSON until 25 September 2026; `--json` still does. */
38
38
  export declare function describeSnapshot(s: Snapshot, table: (rows: string[][]) => string): string;
39
39
  export {};