@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.
- package/CHANGELOG.md +12 -4
- package/README.md +23 -60
- package/dist/app-advisor.js +6 -6
- package/dist/app-advisor.js.map +1 -1
- package/dist/bin/miosa.js +2 -4
- package/dist/bin/miosa.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +22 -3
- package/dist/client.js.map +1 -1
- package/dist/commands/agent.d.ts +10 -10
- package/dist/commands/agent.js +47 -47
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/app.js +1 -1
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/capabilities.d.ts.map +1 -1
- package/dist/commands/capabilities.js +71 -98
- package/dist/commands/capabilities.js.map +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +1 -12
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/computers.d.ts.map +1 -1
- package/dist/commands/computers.js +95 -44
- package/dist/commands/computers.js.map +1 -1
- package/dist/commands/connectors.d.ts.map +1 -1
- package/dist/commands/connectors.js +1 -509
- package/dist/commands/connectors.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +196 -150
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/devices.d.ts.map +1 -1
- package/dist/commands/devices.js +53 -428
- package/dist/commands/devices.js.map +1 -1
- package/dist/commands/docker-deploy.d.ts.map +1 -1
- package/dist/commands/docker-deploy.js +58 -22
- package/dist/commands/docker-deploy.js.map +1 -1
- package/dist/commands/doctor.d.ts +0 -7
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +23 -28
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +8 -106
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/run-groups.d.ts +3 -0
- package/dist/commands/run-groups.d.ts.map +1 -0
- package/dist/commands/run-groups.js +364 -0
- package/dist/commands/run-groups.js.map +1 -0
- package/dist/commands/runs.d.ts +3 -0
- package/dist/commands/runs.d.ts.map +1 -0
- package/dist/commands/runs.js +365 -0
- package/dist/commands/runs.js.map +1 -0
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +63 -199
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/workspaces.d.ts.map +1 -1
- package/dist/commands/workspaces.js +0 -47
- package/dist/commands/workspaces.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/devices.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
106
|
-
browser_automation: "Use computer. It has the desktop/browser
|
|
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
|
|
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
|
|
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 =
|
|
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 ? "
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|