@miosa/cli 1.0.91 → 1.0.93

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 (63) hide show
  1. package/CHANGELOG.md +12 -4
  2. package/README.md +23 -60
  3. package/dist/app-advisor.js +6 -6
  4. package/dist/app-advisor.js.map +1 -1
  5. package/dist/bin/miosa.js +2 -4
  6. package/dist/bin/miosa.js.map +1 -1
  7. package/dist/client.d.ts +1 -1
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +22 -3
  10. package/dist/client.js.map +1 -1
  11. package/dist/commands/agent.d.ts +10 -10
  12. package/dist/commands/agent.js +47 -47
  13. package/dist/commands/agent.js.map +1 -1
  14. package/dist/commands/app.js +1 -1
  15. package/dist/commands/app.js.map +1 -1
  16. package/dist/commands/capabilities.d.ts.map +1 -1
  17. package/dist/commands/capabilities.js +71 -98
  18. package/dist/commands/capabilities.js.map +1 -1
  19. package/dist/commands/completion.d.ts.map +1 -1
  20. package/dist/commands/completion.js +1 -12
  21. package/dist/commands/completion.js.map +1 -1
  22. package/dist/commands/computers.d.ts.map +1 -1
  23. package/dist/commands/computers.js +95 -44
  24. package/dist/commands/computers.js.map +1 -1
  25. package/dist/commands/connectors.d.ts.map +1 -1
  26. package/dist/commands/connectors.js +1 -509
  27. package/dist/commands/connectors.js.map +1 -1
  28. package/dist/commands/deploy.d.ts.map +1 -1
  29. package/dist/commands/deploy.js +196 -150
  30. package/dist/commands/deploy.js.map +1 -1
  31. package/dist/commands/devices.d.ts.map +1 -1
  32. package/dist/commands/devices.js +53 -428
  33. package/dist/commands/devices.js.map +1 -1
  34. package/dist/commands/docker-deploy.d.ts.map +1 -1
  35. package/dist/commands/docker-deploy.js +58 -22
  36. package/dist/commands/docker-deploy.js.map +1 -1
  37. package/dist/commands/doctor.d.ts +0 -7
  38. package/dist/commands/doctor.d.ts.map +1 -1
  39. package/dist/commands/doctor.js +23 -28
  40. package/dist/commands/doctor.js.map +1 -1
  41. package/dist/commands/mcp.d.ts.map +1 -1
  42. package/dist/commands/mcp.js +8 -106
  43. package/dist/commands/mcp.js.map +1 -1
  44. package/dist/commands/run-groups.d.ts +3 -0
  45. package/dist/commands/run-groups.d.ts.map +1 -0
  46. package/dist/commands/run-groups.js +364 -0
  47. package/dist/commands/run-groups.js.map +1 -0
  48. package/dist/commands/runs.d.ts +3 -0
  49. package/dist/commands/runs.d.ts.map +1 -0
  50. package/dist/commands/runs.js +365 -0
  51. package/dist/commands/runs.js.map +1 -0
  52. package/dist/commands/sandbox.d.ts.map +1 -1
  53. package/dist/commands/sandbox.js +63 -199
  54. package/dist/commands/sandbox.js.map +1 -1
  55. package/dist/commands/status.js +1 -1
  56. package/dist/commands/status.js.map +1 -1
  57. package/dist/commands/workspaces.d.ts.map +1 -1
  58. package/dist/commands/workspaces.js +0 -47
  59. package/dist/commands/workspaces.js.map +1 -1
  60. package/dist/types.d.ts +11 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/types.js.map +1 -1
  63. package/package.json +1 -1
@@ -1,18 +1,8 @@
1
1
  import chalk from "chalk";
2
- import { readFileSync, writeFileSync } from "node:fs";
2
+ import { apiPath, client, unwrap } from "./enterprise-util.js";
3
3
  import { isJsonMode } from "../cli-env.js";
4
4
  import { renderTable } from "../ui/table.js";
5
- import { apiPath, client, unwrap } from "./enterprise-util.js";
6
5
  import { handleError } from "./util.js";
7
- const RUNTIME_BINARIES = {
8
- "claude-code": ["claude-code", "claude"],
9
- claude: ["claude"],
10
- codex: ["codex"],
11
- hermes: ["hermes"],
12
- osa: ["osa"],
13
- pi: ["pi"],
14
- custom: [],
15
- };
16
6
  const DEVICE_CATALOG = [
17
7
  {
18
8
  kind: "sandbox_worker",
@@ -22,25 +12,25 @@ const DEVICE_CATALOG = [
22
12
  persistence: "Use --timeout 1h or sandbox extend during active work. Use snapshot/checkpoint/fork for durable recovery points.",
23
13
  primary_commands: [
24
14
  "miosa sandbox create --template nextjs --timeout 1h --wait --json",
25
- "miosa sandbox prompt <id> --provider codex --json -- <task>",
15
+ "miosa sandbox run-agent <id> --runner codex --json -- <task>",
26
16
  "miosa sandbox exec <id> -- sh -lc '<command>'",
27
17
  "miosa sandbox write-file <id> /workspace/file ./file --json",
28
18
  "miosa sandbox publish <id> --wait --json",
29
19
  ],
30
20
  use_when: [
31
21
  "Coding agents should build inside the remote workspace.",
32
- "You need filesystem, command execution, package installs, preview ports, artifacts, or app publish.",
22
+ "You need filesystem, command execution, package installs, preview ports, files, or app publish.",
33
23
  "You want Polsia/Nebula-style virtual device behavior without a GUI desktop.",
34
24
  ],
35
25
  avoid_when: [
36
- "The task requires a full desktop browser session with VNC/CUA.",
26
+ "The task requires a full desktop browser session with VNC and desktop control.",
37
27
  "You need a long-lived production app; publish it after preview is ready.",
38
28
  ],
39
29
  },
40
30
  {
41
31
  kind: "computer",
42
32
  label: "Computer",
43
- purpose: "Durable VM/desktop device for browser automation, CUA sessions, SSH, tunnels, and persistent agent control.",
33
+ purpose: "Durable VM/desktop device for browser automation, computer control sessions, SSH, tunnels, and persistent agent control.",
44
34
  lifecycle: "Managed as a Computer. It is the correct target for GUI/browser-heavy work and remote operator sessions.",
45
35
  persistence: "Use computer checkpoints, volumes, tunnels, and agent sessions for long-lived desktop workflows.",
46
36
  primary_commands: [
@@ -52,12 +42,12 @@ const DEVICE_CATALOG = [
52
42
  ],
53
43
  use_when: [
54
44
  "The agent must use Chromium or a full desktop.",
55
- "The workflow needs login to dashboards, form filling, screenshots, or CUA-style control.",
45
+ "The workflow needs login to dashboards, form filling, screenshots, or desktop control.",
56
46
  "A human and agent need to share the same persistent machine state.",
57
47
  ],
58
48
  avoid_when: [
59
49
  "Simple code generation/build/test work fits a cheaper sandbox worker.",
60
- "The output is a production deployment; use Docker Deploy or standard deploy after build.",
50
+ "The output is a production deployment; use App Engine or standard deploy after build.",
61
51
  ],
62
52
  },
63
53
  {
@@ -82,7 +72,7 @@ const DEVICE_CATALOG = [
82
72
  },
83
73
  {
84
74
  kind: "docker_deploy_host",
85
- label: "Docker Deploy Host",
75
+ label: "App Engine Host",
86
76
  purpose: "Workspace appliance VM that runs Docker containers for durable apps published from sandboxes.",
87
77
  lifecycle: "Always-on deployment capacity. Apps are versioned releases, not interactive coding workspaces.",
88
78
  persistence: "App releases and routing are durable; use sandbox workers for edits, then publish to the host.",
@@ -97,22 +87,22 @@ const DEVICE_CATALOG = [
97
87
  ],
98
88
  avoid_when: [
99
89
  "Interactive agent work is still happening; finish in a sandbox first.",
100
- "The app needs the standard MIOSA Deploy runtime instead of Docker Deploy.",
90
+ "The app needs the standard MIOSA Deploy runtime instead of App Engine.",
101
91
  ],
102
92
  },
103
93
  ];
104
94
  const ROUTING = {
105
- build_code: "Use sandbox_worker. Run the coding agent inside it with sandbox prompt/exec and publish after HTTP 200.",
106
- browser_automation: "Use computer. It has the desktop/browser/CUA surface needed for dashboards, clicks, and visual inspection.",
95
+ build_code: "Use sandbox_worker. Run the coding agent inside it with sandbox run-agent/exec and publish after HTTP 200.",
96
+ browser_automation: "Use computer. It has the desktop/browser control surface needed for dashboards, clicks, and visual inspection.",
107
97
  durable_app: "Use docker_deploy_host after the app is ready. Publish from the sandbox into a versioned deployment.",
108
98
  local_private_context: "Use local_device only for local discovery or user-owned files, then move execution into a sandbox/computer.",
109
- massive_parallel_agents: "Use one sandbox_worker per isolated code/task shard, computers only where GUI/browser state is required, and Docker Deploy for durable outputs.",
99
+ massive_parallel_agents: "Use one sandbox_worker per isolated code/task shard, computers only where GUI/browser state is required, and App Engine for durable outputs.",
110
100
  };
111
101
  export function register(program) {
112
102
  const devices = program
113
103
  .command("devices")
114
104
  .alias("device")
115
- .description("Inspect MIOSA device types: sandboxes, computers, local devices, and Docker Deploy hosts");
105
+ .description("Inspect MIOSA device types: sandboxes, computers, local devices, and App Engine hosts");
116
106
  devices
117
107
  .command("catalog")
118
108
  .description("Print the MIOSA device catalog and routing guidance")
@@ -143,8 +133,8 @@ export function register(program) {
143
133
  .option("--json", "Output as JSON")
144
134
  .action((opts) => runDeviceAction(async () => {
145
135
  const filter = normalizeKindFilter(opts.type ?? "all");
146
- const records = unwrapList(await client().apiGet(apiPath(`/devices${queryString({ kind: apiKindFilter(filter) })}`)), ["devices"]).map(normalizeDevice);
147
- const output = { devices: records };
136
+ const { devices: records, errors } = await listDevices(filter);
137
+ const output = { devices: records, errors };
148
138
  if (isJsonMode(opts)) {
149
139
  console.log(JSON.stringify(output, null, 2));
150
140
  return;
@@ -156,303 +146,54 @@ export function register(program) {
156
146
  renderTable(records, [
157
147
  { header: "ID", key: (row) => row.id, width: 12 },
158
148
  { header: "KIND", key: (row) => row.kind },
159
- { header: "NAME", key: (row) => row.name ?? "-" },
160
- { header: "STATE", key: (row) => row.state ?? "-" },
149
+ { header: "NAME", key: (row) => row.name ?? "" },
150
+ { header: "STATE", key: (row) => row.state ?? "" },
161
151
  {
162
152
  header: "READY",
163
- key: (row) => row.ready == null ? "-" : row.ready ? "yes" : "no",
153
+ key: (row) => row.ready == null ? "" : row.ready ? "yes" : "no",
164
154
  },
165
155
  {
166
156
  header: "PERSISTENT",
167
157
  key: (row) => row.persistent == null
168
- ? "-"
158
+ ? ""
169
159
  : row.persistent
170
160
  ? "yes"
171
161
  : "no",
172
162
  },
173
- { header: "PREVIEW", key: (row) => row.preview_url ?? "-" },
163
+ { header: "PREVIEW", key: (row) => row.preview_url ?? "" },
174
164
  ]);
175
165
  }
176
- }));
177
- devices
178
- .command("show <id>")
179
- .description("Show one hosted device")
180
- .option("--json", "Output as JSON")
181
- .action((id, opts) => runDeviceAction(async () => {
182
- const raw = unwrap(await client().apiGet(apiPath(`/devices/${encodeURIComponent(id)}`)));
183
- if (isJsonMode(opts)) {
184
- console.log(JSON.stringify(raw, null, 2));
185
- return;
186
- }
187
- printObject(raw);
188
- }));
189
- devices
190
- .command("capabilities <id>")
191
- .description("Show what a device supports")
192
- .option("--json", "Output as JSON")
193
- .action((id, opts) => runDeviceAction(async () => {
194
- const raw = unwrap(await client().apiGet(apiPath(`/devices/${encodeURIComponent(id)}/capabilities`)));
195
- if (isJsonMode(opts)) {
196
- console.log(JSON.stringify(raw, null, 2));
197
- return;
166
+ if (errors.length > 0) {
167
+ console.log();
168
+ console.log(chalk.yellow("Partial inventory errors:"));
169
+ for (const error of errors) {
170
+ console.log(`- ${error.source}: ${error.message}`);
171
+ }
198
172
  }
199
- printObject(raw);
200
173
  }));
201
- devices
202
- .command("exec <id>")
203
- .description("Run a command inside a sandbox/computer device")
204
- .requiredOption("-c, --command <command>", "Command to run")
205
- .option("--timeout-ms <ms>", "Command timeout in milliseconds")
206
- .option("--cwd <path>", "Working directory")
207
- .option("--env <KEY=VALUE>", "Environment variable. Repeatable.", collect, [])
208
- .option("--json", "Output as JSON")
209
- .action((id, opts) => runDeviceAction(async () => {
210
- const raw = unwrap(await client().apiPost(apiPath(`/devices/${encodeURIComponent(id)}/exec`), {
211
- command: opts.command,
212
- timeout_ms: opts.timeoutMs ? Number.parseInt(opts.timeoutMs, 10) : undefined,
213
- cwd: opts.cwd,
214
- env: parseEnv(opts.env ?? []),
215
- }));
216
- if (isJsonMode(opts)) {
217
- console.log(JSON.stringify(raw, null, 2));
218
- return;
219
- }
220
- printObject(raw);
221
- }));
222
- devices
223
- .command("doctor <id>")
224
- .description("Run an agent-friendly health probe against a device")
225
- .option("--port <port>", "Also verify a port can be exposed")
226
- .option("--json", "Output as JSON")
227
- .action((id, opts) => runDeviceAction(async () => {
228
- const encoded = encodeURIComponent(id);
229
- const checks = [];
230
- await recordCheck(checks, "capabilities", async () => unwrap(await client().apiGet(apiPath(`/devices/${encoded}/capabilities`))));
231
- await recordCheck(checks, "exec", async () => unwrap(await client().apiPost(apiPath(`/devices/${encoded}/exec`), {
232
- command: "pwd && uname -s",
233
- timeout_ms: 30_000,
234
- })));
235
- await recordCheck(checks, "files", async () => unwrapList(await client().apiGet(apiPath(`/devices/${encoded}/files${queryString({ path: "/workspace" })}`)), ["entries", "files"]));
236
- if (opts.port) {
237
- await recordCheck(checks, "expose", async () => unwrap(await client().apiPost(apiPath(`/devices/${encoded}/expose`), {
238
- port: Number.parseInt(opts.port ?? "", 10),
239
- })));
240
- }
241
- const output = {
242
- device_id: id,
243
- ok: checks.every((check) => check.ok),
244
- checks,
245
- };
246
- if (isJsonMode(opts)) {
247
- console.log(JSON.stringify(output, null, 2));
248
- return;
249
- }
250
- console.log(chalk.bold(`Device doctor ${id}`));
251
- for (const check of checks) {
252
- const status = check.ok ? chalk.green("ok") : chalk.red("fail");
253
- console.log(`${status.padEnd(12)} ${check.name}`);
254
- if (!check.ok && check.error)
255
- console.log(chalk.red(` ${check.error}`));
256
- }
257
- }));
258
- devices
259
- .command("bootstrap <id>")
260
- .description("Prepare a device for an agent runtime and write a MIOSA runtime manifest")
261
- .requiredOption("--runtime <runtime>", "osa, claude-code, claude, codex, hermes, pi, or custom")
262
- .option("--cwd <path>", "Runtime working directory", "/workspace")
263
- .option("--connector <uid>", "Required connector UID. Repeatable.", collect, [])
264
- .option("--env <KEY=VALUE>", "Runtime env default. Repeatable.", collect, [])
265
- .option("--mcp <name=url>", "Runtime MCP server hint. Repeatable.", collect, [])
266
- .option("--install-command <command>", "Optional command to install/bootstrap the runtime")
267
- .option("--skip-probe", "Do not run a binary/runtime probe after writing manifest")
268
- .option("--json", "Output as JSON")
269
- .action((id, opts) => runDeviceAction(async () => {
270
- const runtime = normalizeRuntime(opts.runtime);
271
- const cwd = opts.cwd ?? "/workspace";
272
- const manifestPath = `${cwd.replace(/\/$/, "")}/.miosa/runtime-bootstrap.json`;
273
- const manifest = {
274
- version: 1,
275
- runtime,
276
- cwd,
277
- expected_binaries: RUNTIME_BINARIES[runtime] ?? [],
278
- connectors: opts.connector ?? [],
279
- env: parseEnv(opts.env ?? []) ?? {},
280
- mcp: parseMcp(opts.mcp ?? []),
281
- created_by: "miosa-cli",
282
- };
283
- const steps = [];
284
- await recordCheck(steps, "write_manifest", async () => unwrap(await client().apiPost(apiPath(`/devices/${encodeURIComponent(id)}/files/write`), {
285
- path: manifestPath,
286
- content: `${JSON.stringify(manifest, null, 2)}\n`,
287
- })));
288
- if (opts.installCommand) {
289
- await recordCheck(steps, "install", async () => unwrap(await client().apiPost(apiPath(`/devices/${encodeURIComponent(id)}/exec`), {
290
- command: opts.installCommand,
291
- cwd,
292
- timeout_ms: 600_000,
293
- })));
294
- }
295
- if (!opts.skipProbe) {
296
- await recordCheck(steps, "probe", async () => unwrap(await client().apiPost(apiPath(`/devices/${encodeURIComponent(id)}/exec`), {
297
- command: runtimeProbeCommand(runtime),
298
- cwd,
299
- timeout_ms: 60_000,
300
- })));
301
- }
302
- const output = {
303
- device_id: id,
304
- ok: steps.every((step) => step.ok),
305
- runtime,
306
- manifest_path: manifestPath,
307
- steps,
308
- };
309
- if (isJsonMode(opts)) {
310
- console.log(JSON.stringify(output, null, 2));
311
- return;
312
- }
313
- console.log(chalk.bold(`Bootstrapped ${runtime} on ${id}`));
314
- console.log(`${chalk.bold("manifest")} ${manifestPath}`);
315
- for (const step of steps) {
316
- const status = step.ok ? chalk.green("ok") : chalk.red("fail");
317
- console.log(`${status.padEnd(12)} ${step.name}`);
318
- if (!step.ok && step.error)
319
- console.log(chalk.red(` ${step.error}`));
320
- }
321
- }));
322
- const files = devices
323
- .command("files")
324
- .description("Read, write, and list files inside a device");
325
- files
326
- .command("list <id>")
327
- .description("List files inside a device")
328
- .option("--path <path>", "Directory path", "/workspace")
329
- .option("--json", "Output as JSON")
330
- .action((id, opts) => runDeviceAction(async () => {
331
- const raw = unwrapList(await client().apiGet(apiPath(`/devices/${encodeURIComponent(id)}/files${queryString({ path: opts.path })}`)), ["entries", "files"]);
332
- if (isJsonMode(opts)) {
333
- console.log(JSON.stringify(raw, null, 2));
334
- return;
335
- }
336
- renderTable(raw, [
337
- { header: "PATH", key: (row) => optionalString(row, "path") ?? "" },
338
- { header: "TYPE", key: (row) => optionalString(row, "type") ?? "-" },
339
- { header: "SIZE", key: (row) => String(optionalNumber(row, "size") ?? "-") },
340
- ]);
341
- }));
342
- files
343
- .command("read <id>")
344
- .description("Read a file from a device")
345
- .requiredOption("--path <path>", "File path")
346
- .option("-o, --output <path>", "Write decoded bytes to a local file")
347
- .option("--decode", "Print decoded text instead of the JSON/base64 envelope")
348
- .option("--json", "Output as JSON")
349
- .action((id, opts) => runDeviceAction(async () => {
350
- const raw = unwrap(await client().apiGet(apiPath(`/devices/${encodeURIComponent(id)}/files/read${queryString({ path: opts.path })}`)));
351
- if (opts.output) {
352
- writeFileSync(opts.output, decodeFileContent(raw));
353
- if (!isJsonMode(opts))
354
- console.log(chalk.green(`Wrote ${opts.output}`));
355
- else
356
- console.log(JSON.stringify({ output: opts.output }, null, 2));
357
- return;
358
- }
359
- if (opts.decode) {
360
- console.log(decodeFileContent(raw).toString("utf8"));
361
- return;
362
- }
363
- if (isJsonMode(opts)) {
364
- console.log(JSON.stringify(raw, null, 2));
365
- return;
366
- }
367
- printObject(raw);
368
- }));
369
- files
370
- .command("write <id>")
371
- .description("Write a file into a device")
372
- .requiredOption("--path <path>", "Destination file path")
373
- .option("--content <text>", "Text content to write")
374
- .option("--content-base64 <base64>", "Base64 content to write")
375
- .option("--file <path>", "Read local file bytes and upload as base64")
376
- .option("--json", "Output as JSON")
377
- .action((id, opts) => runDeviceAction(async () => {
378
- const body = writeFileBody(opts);
379
- const raw = unwrap(await client().apiPost(apiPath(`/devices/${encodeURIComponent(id)}/files/write`), body));
380
- if (isJsonMode(opts)) {
381
- console.log(JSON.stringify(raw, null, 2));
382
- return;
383
- }
384
- printObject(raw);
385
- }));
386
- devices
387
- .command("expose <id>")
388
- .description("Expose a device port through MIOSA routing")
389
- .requiredOption("--port <port>", "Port to expose")
390
- .option("--json", "Output as JSON")
391
- .action((id, opts) => runDeviceAction(async () => {
392
- const raw = unwrap(await client().apiPost(apiPath(`/devices/${encodeURIComponent(id)}/expose`), {
393
- port: Number.parseInt(opts.port, 10),
394
- }));
395
- if (isJsonMode(opts)) {
396
- console.log(JSON.stringify(raw, null, 2));
397
- return;
174
+ }
175
+ async function listDevices(filter) {
176
+ const records = [];
177
+ const errors = [];
178
+ if (filter === "all" || filter === "sandbox_worker") {
179
+ try {
180
+ const sandboxes = unwrapList(await client().apiGet(apiPath("/sandboxes")), ["sandboxes"]);
181
+ records.push(...sandboxes.map(normalizeSandbox));
398
182
  }
399
- printObject(raw);
400
- }));
401
- devices
402
- .command("browser <id>")
403
- .description("Return browser/desktop connection details for a computer-backed device")
404
- .option("--json", "Output as JSON")
405
- .action((id, opts) => runDeviceAction(async () => {
406
- const raw = unwrap(await client().apiGet(apiPath(`/devices/${encodeURIComponent(id)}/browser`)));
407
- if (isJsonMode(opts)) {
408
- console.log(JSON.stringify(raw, null, 2));
409
- return;
183
+ catch (err) {
184
+ errors.push(deviceListError("sandboxes", err));
410
185
  }
411
- printObject(raw);
412
- }));
413
- devices
414
- .command("pause <id>")
415
- .description("Pause a device session when supported")
416
- .option("--json", "Output as JSON")
417
- .action((id, opts) => postDeviceLifecycle(id, "pause", opts));
418
- devices
419
- .command("stop <id>")
420
- .description("Stop a device session when supported")
421
- .option("--json", "Output as JSON")
422
- .action((id, opts) => postDeviceLifecycle(id, "stop", opts));
423
- devices
424
- .command("resume <id>")
425
- .description("Resume a device session when supported")
426
- .option("--json", "Output as JSON")
427
- .action((id, opts) => postDeviceLifecycle(id, "resume", opts));
428
- devices
429
- .command("extend <id>")
430
- .description("Extend a device timeout when supported")
431
- .requiredOption("--timeout-sec <seconds>", "New timeout in seconds")
432
- .option("--json", "Output as JSON")
433
- .action((id, opts) => runDeviceAction(async () => {
434
- const raw = unwrap(await client().apiPost(apiPath(`/devices/${encodeURIComponent(id)}/extend`), {
435
- timeout_sec: Number.parseInt(opts.timeoutSec, 10),
436
- }));
437
- if (isJsonMode(opts)) {
438
- console.log(JSON.stringify(raw, null, 2));
439
- return;
186
+ }
187
+ if (filter === "all" || filter === "computer") {
188
+ try {
189
+ const computers = unwrapList(await client().apiGet(apiPath("/computers")), ["computers"]);
190
+ records.push(...computers.map(normalizeComputer));
440
191
  }
441
- printObject(raw);
442
- }));
443
- devices
444
- .command("destroy <id>")
445
- .alias("delete")
446
- .description("Destroy/delete a device when supported")
447
- .option("--json", "Output as JSON")
448
- .action((id, opts) => runDeviceAction(async () => {
449
- const raw = unwrap(await client().apiDelete(apiPath(`/devices/${encodeURIComponent(id)}`)));
450
- if (isJsonMode(opts)) {
451
- console.log(JSON.stringify(raw, null, 2));
452
- return;
192
+ catch (err) {
193
+ errors.push(deviceListError("computers", err));
453
194
  }
454
- printObject(raw);
455
- }));
195
+ }
196
+ return { devices: records, errors };
456
197
  }
457
198
  function normalizeKindFilter(value) {
458
199
  const normalized = value.trim().toLowerCase().replaceAll("-", "_");
@@ -466,19 +207,6 @@ function normalizeKindFilter(value) {
466
207
  return "computer";
467
208
  throw new Error(`Unsupported device type "${value}". Use: all, sandbox_worker, sandbox, computer`);
468
209
  }
469
- function apiKindFilter(filter) {
470
- if (filter === "all")
471
- return undefined;
472
- if (filter === "sandbox_worker")
473
- return "sandbox";
474
- return filter;
475
- }
476
- function normalizeDevice(row) {
477
- const kind = optionalString(row, "kind") ?? optionalString(row, "type") ?? "";
478
- if (kind === "computer")
479
- return normalizeComputer(row);
480
- return normalizeSandbox(row);
481
- }
482
210
  function normalizeSandbox(row) {
483
211
  return {
484
212
  id: stringField(row, "id"),
@@ -519,6 +247,14 @@ function unwrapList(payload, keys) {
519
247
  }
520
248
  return [];
521
249
  }
250
+ function deviceListError(source, err) {
251
+ const message = err instanceof Error ? err.message : String(err);
252
+ return {
253
+ source,
254
+ message,
255
+ retryable: /fetch failed|ECONNRESET|HTTP 502|other side closed|socket hang up|bad gateway/i.test(message),
256
+ };
257
+ }
522
258
  function stringField(row, key) {
523
259
  const value = row[key];
524
260
  return typeof value === "string" ? value : String(value ?? "");
@@ -536,117 +272,6 @@ function optionalNumber(row, key) {
536
272
  function isRecord(value) {
537
273
  return value !== null && typeof value === "object" && !Array.isArray(value);
538
274
  }
539
- function queryString(params) {
540
- const search = new URLSearchParams();
541
- for (const [key, value] of Object.entries(params)) {
542
- if (value !== undefined && value !== "")
543
- search.set(key, value);
544
- }
545
- const encoded = search.toString();
546
- return encoded ? `?${encoded}` : "";
547
- }
548
- function printObject(value) {
549
- if (!isRecord(value)) {
550
- console.log(String(value ?? ""));
551
- return;
552
- }
553
- for (const [key, cell] of Object.entries(value)) {
554
- const printable = cell === null || cell === undefined
555
- ? "-"
556
- : typeof cell === "object"
557
- ? JSON.stringify(cell)
558
- : String(cell);
559
- console.log(`${chalk.bold(key.padEnd(18))} ${printable}`);
560
- }
561
- }
562
- function collect(value, previous) {
563
- previous.push(value);
564
- return previous;
565
- }
566
- function parseEnv(entries) {
567
- if (entries.length === 0)
568
- return undefined;
569
- const out = {};
570
- for (const entry of entries) {
571
- const index = entry.indexOf("=");
572
- if (index <= 0)
573
- throw new Error(`Invalid --env value "${entry}". Use KEY=VALUE.`);
574
- out[entry.slice(0, index)] = entry.slice(index + 1);
575
- }
576
- return out;
577
- }
578
- function parseMcp(entries) {
579
- return entries.map((entry) => {
580
- const index = entry.indexOf("=");
581
- if (index <= 0)
582
- throw new Error(`Invalid --mcp value "${entry}". Use name=url.`);
583
- return { name: entry.slice(0, index), url: entry.slice(index + 1) };
584
- });
585
- }
586
- function normalizeRuntime(value) {
587
- const runtime = value.trim().toLowerCase();
588
- if (!(runtime in RUNTIME_BINARIES)) {
589
- throw new Error(`Unsupported runtime "${value}". Use: osa, claude-code, claude, codex, hermes, pi, custom`);
590
- }
591
- return runtime;
592
- }
593
- function runtimeProbeCommand(runtime) {
594
- const binaries = RUNTIME_BINARIES[runtime] ?? [];
595
- if (binaries.length === 0) {
596
- return "printf 'custom runtime manifest written\\n'";
597
- }
598
- const checks = binaries
599
- .map((binary) => `command -v ${shellWord(binary)} >/dev/null 2>&1`)
600
- .join(" || ");
601
- const display = binaries.join(" or ");
602
- return `${checks} && printf 'runtime available: ${display}\\n' || { printf 'runtime missing: ${display}\\n' >&2; exit 127; }`;
603
- }
604
- function shellWord(value) {
605
- if (/^[A-Za-z0-9_./:-]+$/.test(value))
606
- return value;
607
- return `'${value.replaceAll("'", "'\"'\"'")}'`;
608
- }
609
- function decodeFileContent(row) {
610
- const content = optionalString(row, "content") ?? "";
611
- const encoding = optionalString(row, "encoding") ?? "base64";
612
- return encoding === "base64"
613
- ? Buffer.from(content, "base64")
614
- : Buffer.from(content, "utf8");
615
- }
616
- function writeFileBody(opts) {
617
- const provided = [opts.content, opts.contentBase64, opts.file].filter((value) => value !== undefined);
618
- if (provided.length !== 1) {
619
- throw new Error("Use exactly one of --content, --content-base64, or --file");
620
- }
621
- return {
622
- path: opts.path,
623
- content: opts.content,
624
- content_base64: opts.contentBase64 ??
625
- (opts.file ? readFileSync(opts.file).toString("base64") : undefined),
626
- };
627
- }
628
- async function recordCheck(checks, name, fn) {
629
- try {
630
- checks.push({ name, ok: true, detail: await fn() });
631
- }
632
- catch (err) {
633
- checks.push({
634
- name,
635
- ok: false,
636
- error: err instanceof Error ? err.message : String(err),
637
- });
638
- }
639
- }
640
- function postDeviceLifecycle(id, action, opts) {
641
- return runDeviceAction(async () => {
642
- const raw = unwrap(await client().apiPost(apiPath(`/devices/${encodeURIComponent(id)}/${action}`), {}));
643
- if (isJsonMode(opts)) {
644
- console.log(JSON.stringify(raw, null, 2));
645
- return;
646
- }
647
- printObject(raw);
648
- });
649
- }
650
275
  async function runDeviceAction(fn) {
651
276
  try {
652
277
  await fn();