@miosa/cli 1.3.2 → 1.3.3
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 +9 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +59 -0
- package/dist/client.js.map +1 -1
- package/dist/commands/enterprise-util.d.ts +1 -1
- package/dist/commands/enterprise-util.d.ts.map +1 -1
- package/dist/commands/enterprise-util.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +7 -1
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +113 -11
- package/dist/commands/sandbox.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/sandbox.js
CHANGED
|
@@ -3,13 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import { createServer } from "node:net";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import * as http from "node:http";
|
|
7
8
|
import * as https from "node:https";
|
|
8
9
|
import chalk from "chalk";
|
|
9
10
|
import WebSocket from "ws";
|
|
10
11
|
import { loadAppManifest, manifestPort, manifestProbePath, manifestStartCommand, parseAppManifest, } from "../app-manifest.js";
|
|
11
12
|
import { detectFramework } from "../framework-detector.js";
|
|
12
|
-
import { addDataOption, client, apiPath, deleteAndPrint, enc, getAndPrint, postAndPrint, printValue, runAction, unwrap, } from "./enterprise-util.js";
|
|
13
|
+
import { addDataOption, client, apiPath, deleteAndPrint, enc, getAndPrint, parseData, postAndPrint, printValue, runAction, unwrap, } from "./enterprise-util.js";
|
|
13
14
|
import { loadConfig } from "../config.js";
|
|
14
15
|
import { assertDeletableRemoteDir } from "./sandbox-delete-guard.js";
|
|
15
16
|
import { ENV_FILE_OPTION_HELP, ENV_INLINE_SHELL_WARNING, ENV_STDIN_OPTION_HELP, resolveEnvInputs, } from "./env-input.js";
|
|
@@ -23,6 +24,57 @@ import { registerSandboxDevCommands, runFullSandboxDoctor, } from "./sandbox-dev
|
|
|
23
24
|
const DEFAULT_INTERACTIVE_SANDBOX_TIMEOUT_SEC = 3_600;
|
|
24
25
|
const DEFAULT_CREATE_WAIT_TIMEOUT_SEC = 120;
|
|
25
26
|
const EXPIRING_SANDBOX_THRESHOLD_SEC = 5 * 60;
|
|
27
|
+
// How long, after a sandbox reaches `running`, to keep polling for
|
|
28
|
+
// command-readiness before `--wait` gives up.
|
|
29
|
+
const COMMAND_READY_WAIT_TIMEOUT_SEC = 60;
|
|
30
|
+
const COMMAND_READY_POLL_INTERVAL_MS = 500;
|
|
31
|
+
// Platform readiness components that gate command execution. Mirrors the
|
|
32
|
+
// server's `Engine.Sandbox.Readiness` platform set: `exec` returns
|
|
33
|
+
// `409 SANDBOX_NOT_COMMAND_READY` until all of these report `ready`. Excludes
|
|
34
|
+
// the template `application_probe` (the workload), which is started BY exec.
|
|
35
|
+
const PLATFORM_READINESS_COMPONENTS = [
|
|
36
|
+
"command_agent",
|
|
37
|
+
"process_control",
|
|
38
|
+
"resource_contract",
|
|
39
|
+
"clock",
|
|
40
|
+
"identity",
|
|
41
|
+
"durability",
|
|
42
|
+
"filesystem",
|
|
43
|
+
"internal_probe",
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* True when a sandbox is command-ready — `exec` will succeed rather than return
|
|
47
|
+
* `409 SANDBOX_NOT_COMMAND_READY`. Accepts a readiness payload
|
|
48
|
+
* (`GET /sandboxes/:id/readiness`) or a sandbox record. Mirrors the server's
|
|
49
|
+
* `command_path_ready?/1`: the aggregate `ready` flag short-circuits, otherwise
|
|
50
|
+
* every platform component must report `ready`.
|
|
51
|
+
*/
|
|
52
|
+
function isCommandReady(payload) {
|
|
53
|
+
if (!payload || typeof payload !== "object")
|
|
54
|
+
return false;
|
|
55
|
+
const p = payload;
|
|
56
|
+
if (p["ready"] === true)
|
|
57
|
+
return true;
|
|
58
|
+
const readiness = p["readiness"];
|
|
59
|
+
const components = (readiness?.["components"] ??
|
|
60
|
+
p["components"] ??
|
|
61
|
+
p["readiness_components"]);
|
|
62
|
+
if (!components || typeof components !== "object")
|
|
63
|
+
return false;
|
|
64
|
+
return PLATFORM_READINESS_COMPONENTS.every((name) => {
|
|
65
|
+
const comp = components[name];
|
|
66
|
+
const status = typeof comp === "string" ? comp : comp?.status;
|
|
67
|
+
return status === "ready";
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Generate a create Idempotency-Key. Generated once per logical `create` and
|
|
72
|
+
* re-sent unchanged on every retry (see `MiosaClient.apiPostWithRetry`), so a
|
|
73
|
+
* retried create dedups to the same sandbox instead of billing a duplicate VM.
|
|
74
|
+
*/
|
|
75
|
+
function generateIdempotencyKey() {
|
|
76
|
+
return randomUUID();
|
|
77
|
+
}
|
|
26
78
|
const SANDBOX_SIZE_CONTRACTS = {
|
|
27
79
|
xs: { cpu: 1, memory: 2_048, disk: 10_240 },
|
|
28
80
|
small: { cpu: 2, memory: 4_096, disk: 10_240 },
|
|
@@ -285,12 +337,15 @@ Note:
|
|
|
285
337
|
const t0 = Date.now();
|
|
286
338
|
const json = !!isJsonMode(opts) || process.env["MIOSA_JSON"] === "1";
|
|
287
339
|
if (opts.data) {
|
|
340
|
+
// Raw --data/--input/--file body — still idempotent and retry-safe.
|
|
341
|
+
const idempotencyKey = opts.idempotencyKey ?? generateIdempotencyKey();
|
|
342
|
+
const dataBody = parseData(opts.data, opts.input, opts.file) ?? {};
|
|
343
|
+
const value = unwrap(await client().apiPostWithRetry(apiPath("/sandboxes"), dataBody, { "Idempotency-Key": idempotencyKey }));
|
|
288
344
|
if (json) {
|
|
289
|
-
|
|
345
|
+
printValue(value, opts);
|
|
290
346
|
return;
|
|
291
347
|
}
|
|
292
|
-
|
|
293
|
-
renderCreateSuccess(raw, Date.now() - t0);
|
|
348
|
+
renderCreateSuccess(value, Date.now() - t0);
|
|
294
349
|
return;
|
|
295
350
|
}
|
|
296
351
|
const body = {
|
|
@@ -350,9 +405,13 @@ Note:
|
|
|
350
405
|
body["persistent"] = opts.nonPersistent ? false : true;
|
|
351
406
|
if (opts.autoStart)
|
|
352
407
|
body["auto_start"] = true;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
408
|
+
// Auto-generate an Idempotency-Key when none is supplied, and retry
|
|
409
|
+
// on 429/5xx with the SAME key so a retried create dedups to one
|
|
410
|
+
// sandbox instead of billing a duplicate.
|
|
411
|
+
const idempotencyKey = opts.idempotencyKey ?? generateIdempotencyKey();
|
|
412
|
+
const raw = unwrap(await client().apiPostWithRetry(apiPath("/sandboxes"), body, {
|
|
413
|
+
"Idempotency-Key": idempotencyKey,
|
|
414
|
+
}));
|
|
356
415
|
const sb = (raw ?? {});
|
|
357
416
|
const id = String(sb["id"] ?? "");
|
|
358
417
|
if (opts.publishPort != null && id) {
|
|
@@ -376,7 +435,9 @@ Note:
|
|
|
376
435
|
}
|
|
377
436
|
}
|
|
378
437
|
else if (opts.wait && id) {
|
|
379
|
-
const latest = await waitForSandboxRunning(client(), id, Math.max(Math.min(timeoutSec, DEFAULT_CREATE_WAIT_TIMEOUT_SEC), 30)
|
|
438
|
+
const latest = await waitForSandboxRunning(client(), id, Math.max(Math.min(timeoutSec, DEFAULT_CREATE_WAIT_TIMEOUT_SEC), 30),
|
|
439
|
+
// create --wait must guarantee exec works before returning.
|
|
440
|
+
true);
|
|
380
441
|
Object.assign(sb, latest, { ready: true });
|
|
381
442
|
}
|
|
382
443
|
if (json) {
|
|
@@ -2046,7 +2107,9 @@ async function waitSandboxReady(sandboxId, port, probePath, timeoutSec) {
|
|
|
2046
2107
|
};
|
|
2047
2108
|
}
|
|
2048
2109
|
async function waitSandboxVmReady(sandboxId, timeoutSec) {
|
|
2049
|
-
|
|
2110
|
+
// `sandbox wait` reports readiness for use — confirm command-readiness so a
|
|
2111
|
+
// successful wait guarantees exec works.
|
|
2112
|
+
const sandbox = await waitForSandboxRunning(client(), sandboxId, timeoutSec, true);
|
|
2050
2113
|
return {
|
|
2051
2114
|
...sandbox,
|
|
2052
2115
|
sandbox_id: sandboxId,
|
|
@@ -2680,13 +2743,24 @@ async function readRemoteAppManifest(c, sandboxId, timeoutSec) {
|
|
|
2680
2743
|
}
|
|
2681
2744
|
return null;
|
|
2682
2745
|
}
|
|
2683
|
-
async function waitForSandboxRunning(c, sandboxId, timeoutSec
|
|
2746
|
+
async function waitForSandboxRunning(c, sandboxId, timeoutSec,
|
|
2747
|
+
// Off by default so existing callers (e.g. `sandbox deploy`, which has its
|
|
2748
|
+
// own downstream recovery) keep their behavior. The explicit user-facing
|
|
2749
|
+
// waits (`create --wait`, `sandbox wait`) opt in.
|
|
2750
|
+
requireCommandReady = false) {
|
|
2684
2751
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
2685
2752
|
while (Date.now() < deadline) {
|
|
2686
2753
|
const sandbox = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
|
|
2687
2754
|
const state = String(sandbox["state"] ?? sandbox["status"] ?? "").toLowerCase();
|
|
2688
|
-
if (state === "running" || state === "active")
|
|
2755
|
+
if (state === "running" || state === "active") {
|
|
2756
|
+
// A running VM is not proof that exec will work: the command agent may
|
|
2757
|
+
// not be attached yet (409 SANDBOX_NOT_COMMAND_READY). Confirm
|
|
2758
|
+
// command-readiness so `--wait` only reports success once exec works.
|
|
2759
|
+
if (requireCommandReady) {
|
|
2760
|
+
await confirmCommandReady(c, sandboxId, deadline);
|
|
2761
|
+
}
|
|
2689
2762
|
return sandbox;
|
|
2763
|
+
}
|
|
2690
2764
|
if (state === "error" || state === "failed") {
|
|
2691
2765
|
throw sandboxStateError(sandboxId, sandbox, state);
|
|
2692
2766
|
}
|
|
@@ -2697,6 +2771,34 @@ async function waitForSandboxRunning(c, sandboxId, timeoutSec) {
|
|
|
2697
2771
|
}
|
|
2698
2772
|
throw new UserError(`Sandbox ${sandboxId} did not become running within ${timeoutSec}s.`);
|
|
2699
2773
|
}
|
|
2774
|
+
/**
|
|
2775
|
+
* After a sandbox reaches `running`, poll `GET /sandboxes/:id/readiness` until
|
|
2776
|
+
* it is command-ready (exec will succeed). Bounded by the running-wait
|
|
2777
|
+
* `deadline`, extended by a command-ready grace window. A missing readiness
|
|
2778
|
+
* endpoint (`NOT_FOUND`, older servers) is tolerated — return without blocking.
|
|
2779
|
+
* Throws when command-readiness is not reached before the deadline.
|
|
2780
|
+
*/
|
|
2781
|
+
async function confirmCommandReady(c, sandboxId, runningDeadline) {
|
|
2782
|
+
const deadline = Math.max(runningDeadline, Date.now() + COMMAND_READY_WAIT_TIMEOUT_SEC * 1000);
|
|
2783
|
+
for (;;) {
|
|
2784
|
+
try {
|
|
2785
|
+
const readiness = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}/readiness`)));
|
|
2786
|
+
if (isCommandReady(readiness))
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
catch (err) {
|
|
2790
|
+
// Older servers may not expose the readiness endpoint. Don't block the
|
|
2791
|
+
// create — treat a missing endpoint as best-effort success.
|
|
2792
|
+
if (err instanceof ApiResponseError && err.code === "NOT_FOUND")
|
|
2793
|
+
return;
|
|
2794
|
+
// Other transient errors: keep polling until the deadline.
|
|
2795
|
+
}
|
|
2796
|
+
if (Date.now() >= deadline) {
|
|
2797
|
+
throw new UserError(`Sandbox ${sandboxId} reached running but its command agent did not attach in time; exec may return SANDBOX_NOT_COMMAND_READY.`, "Retry the command, or poll `miosa sandbox get <id>` before exec.");
|
|
2798
|
+
}
|
|
2799
|
+
await sleep(COMMAND_READY_POLL_INTERVAL_MS);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2700
2802
|
async function resumeSandboxAndPrint(sandboxId, opts) {
|
|
2701
2803
|
try {
|
|
2702
2804
|
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/resume`), {}, opts.idempotencyKey
|