@miosa/cli 1.0.56 → 1.0.58
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 -0
- package/README.md +65 -42
- package/dist/app-advisor.d.ts +80 -0
- package/dist/app-advisor.d.ts.map +1 -0
- package/dist/app-advisor.js +486 -0
- package/dist/app-advisor.js.map +1 -0
- package/dist/bin/miosa.js +9 -0
- package/dist/bin/miosa.js.map +1 -1
- package/dist/commands/app.d.ts +3 -0
- package/dist/commands/app.d.ts.map +1 -0
- package/dist/commands/app.js +115 -0
- package/dist/commands/app.js.map +1 -0
- package/dist/commands/capabilities.d.ts.map +1 -1
- package/dist/commands/capabilities.js +232 -5
- package/dist/commands/capabilities.js.map +1 -1
- package/dist/commands/command-overview.d.ts +13 -0
- package/dist/commands/command-overview.d.ts.map +1 -0
- package/dist/commands/command-overview.js +50 -0
- package/dist/commands/command-overview.js.map +1 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +248 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/databases.d.ts.map +1 -1
- package/dist/commands/databases.js +85 -0
- package/dist/commands/databases.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +109 -2
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/docker-deploy.d.ts.map +1 -1
- package/dist/commands/docker-deploy.js +197 -127
- package/dist/commands/docker-deploy.js.map +1 -1
- package/dist/commands/enterprise-util.d.ts +3 -1
- package/dist/commands/enterprise-util.d.ts.map +1 -1
- package/dist/commands/enterprise-util.js +25 -9
- package/dist/commands/enterprise-util.js.map +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +171 -17
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/new.js +2 -2
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +498 -51
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/util.d.ts.map +1 -1
- package/dist/commands/util.js +6 -1
- package/dist/commands/util.js.map +1 -1
- package/dist/config.d.ts +24 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +87 -0
- package/dist/config.js.map +1 -1
- package/package.json +3 -2
package/dist/commands/sandbox.js
CHANGED
|
@@ -16,6 +16,10 @@ import { renderTable } from "../ui/table.js";
|
|
|
16
16
|
import { formatDuration, hintBlock, icon, kvPanel, printBanner, printElapsed, } from "../ui/render.js";
|
|
17
17
|
import { formatBytes } from "../ui/progress.js";
|
|
18
18
|
import { ApiResponseError, MiosaError, NetworkError, ServerError, UserError, } from "../errors.js";
|
|
19
|
+
import { EXIT_USER_ERROR } from "../types.js";
|
|
20
|
+
const DEFAULT_INTERACTIVE_SANDBOX_TIMEOUT_SEC = 3_600;
|
|
21
|
+
const DEFAULT_CREATE_WAIT_TIMEOUT_SEC = 120;
|
|
22
|
+
const EXPIRING_SANDBOX_THRESHOLD_SEC = 5 * 60;
|
|
19
23
|
export function register(program) {
|
|
20
24
|
// -------------------------------------------------------------------------
|
|
21
25
|
// sandbox / sandboxes command group — built manually to avoid subcommand
|
|
@@ -207,8 +211,8 @@ export function register(program) {
|
|
|
207
211
|
body["memory_mb"] = opts.memory;
|
|
208
212
|
if (opts.disk != null)
|
|
209
213
|
body["disk_size_mb"] = opts.disk;
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
const timeoutSec = opts.timeout ?? DEFAULT_INTERACTIVE_SANDBOX_TIMEOUT_SEC;
|
|
215
|
+
body["timeout_sec"] = timeoutSec;
|
|
212
216
|
if (opts.source)
|
|
213
217
|
body["source"] = opts.source;
|
|
214
218
|
if (opts.revision)
|
|
@@ -239,7 +243,7 @@ export function register(program) {
|
|
|
239
243
|
const id = String(sb["id"] ?? "");
|
|
240
244
|
if (opts.publishPort != null && id) {
|
|
241
245
|
if (opts.wait) {
|
|
242
|
-
const preview = await waitSandboxReady(id, opts.publishPort, opts.probePath ?? "/", Math.max(opts.timeout ??
|
|
246
|
+
const preview = await waitSandboxReady(id, opts.publishPort, opts.probePath ?? "/", Math.max(Math.min(opts.timeout ?? DEFAULT_CREATE_WAIT_TIMEOUT_SEC, DEFAULT_CREATE_WAIT_TIMEOUT_SEC), 30));
|
|
243
247
|
const latest = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}`)));
|
|
244
248
|
Object.assign(sb, latest, {
|
|
245
249
|
preview,
|
|
@@ -258,7 +262,7 @@ export function register(program) {
|
|
|
258
262
|
}
|
|
259
263
|
}
|
|
260
264
|
else if (opts.wait && id) {
|
|
261
|
-
const latest = await waitForSandboxRunning(client(), id, Math.max(
|
|
265
|
+
const latest = await waitForSandboxRunning(client(), id, Math.max(Math.min(timeoutSec, DEFAULT_CREATE_WAIT_TIMEOUT_SEC), 30));
|
|
262
266
|
Object.assign(sb, latest, { ready: true });
|
|
263
267
|
}
|
|
264
268
|
if (json) {
|
|
@@ -309,7 +313,7 @@ export function register(program) {
|
|
|
309
313
|
.command("resume <sandbox-id>")
|
|
310
314
|
.description("Resume a paused Sandbox")
|
|
311
315
|
.option("--json", "Output as JSON")
|
|
312
|
-
.action((id, opts) => runAction(() =>
|
|
316
|
+
.action((id, opts) => runAction(() => resumeSandboxAndPrint(id, opts)));
|
|
313
317
|
// fork — clone from snapshot in one call (mirrors `box fork`)
|
|
314
318
|
sandbox
|
|
315
319
|
.command("fork <sandbox-id>")
|
|
@@ -373,7 +377,7 @@ export function register(program) {
|
|
|
373
377
|
}
|
|
374
378
|
if (opts.timeout != null)
|
|
375
379
|
body["timeout"] = opts.timeout;
|
|
376
|
-
await
|
|
380
|
+
await postSandboxExecAndPrint(id, opts, body);
|
|
377
381
|
}));
|
|
378
382
|
// exec — positional command arg; --data body overrides when supplied
|
|
379
383
|
addDataOption(sandbox
|
|
@@ -383,6 +387,9 @@ export function register(program) {
|
|
|
383
387
|
.allowExcessArguments()
|
|
384
388
|
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
385
389
|
.option("--workdir <path>", "Alias for --cwd")
|
|
390
|
+
.option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
|
|
391
|
+
.option("--command <command>", "Alias for --cmd")
|
|
392
|
+
.option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
|
|
386
393
|
.option("--env <pair>", "Environment variable KEY=VALUE. Repeatable.", collectOption, [])
|
|
387
394
|
.option("--background", "Start the command in the background and return immediately")
|
|
388
395
|
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
@@ -397,10 +404,10 @@ export function register(program) {
|
|
|
397
404
|
.action((id, words, opts) => runAction(async () => {
|
|
398
405
|
opts.follow = opts.follow || opts.stream;
|
|
399
406
|
if (opts.data) {
|
|
400
|
-
await
|
|
407
|
+
await postSandboxExecAndPrint(id, opts, {});
|
|
401
408
|
return;
|
|
402
409
|
}
|
|
403
|
-
const cmd =
|
|
410
|
+
const cmd = resolveSandboxCommand(words, opts);
|
|
404
411
|
const effectiveCommand = opts.background
|
|
405
412
|
? backgroundCommand(cmd)
|
|
406
413
|
: cmd;
|
|
@@ -414,7 +421,7 @@ export function register(program) {
|
|
|
414
421
|
}
|
|
415
422
|
const env = parseEnvPairs(opts.env ?? []);
|
|
416
423
|
if (opts.detached) {
|
|
417
|
-
const result = await createSandboxCommand(id,
|
|
424
|
+
const result = await createSandboxCommand(id, cmd, {
|
|
418
425
|
cwd,
|
|
419
426
|
env,
|
|
420
427
|
user: opts.user,
|
|
@@ -438,7 +445,7 @@ export function register(program) {
|
|
|
438
445
|
body["env"] = env;
|
|
439
446
|
if (opts.timeout != null)
|
|
440
447
|
body["timeout"] = opts.timeout;
|
|
441
|
-
await
|
|
448
|
+
await postSandboxExecAndPrint(id, opts, body);
|
|
442
449
|
}));
|
|
443
450
|
// run — alias for exec with identical semantics
|
|
444
451
|
addDataOption(sandbox
|
|
@@ -448,6 +455,9 @@ export function register(program) {
|
|
|
448
455
|
.allowExcessArguments()
|
|
449
456
|
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
450
457
|
.option("--workdir <path>", "Alias for --cwd")
|
|
458
|
+
.option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
|
|
459
|
+
.option("--command <command>", "Alias for --cmd")
|
|
460
|
+
.option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
|
|
451
461
|
.option("--env <pair>", "Environment variable KEY=VALUE. Repeatable.", collectOption, [])
|
|
452
462
|
.option("--background", "Start the command in the background and return immediately")
|
|
453
463
|
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
@@ -462,10 +472,10 @@ export function register(program) {
|
|
|
462
472
|
.action((id, words, opts) => runAction(async () => {
|
|
463
473
|
opts.follow = opts.follow || opts.stream;
|
|
464
474
|
if (opts.data) {
|
|
465
|
-
await
|
|
475
|
+
await postSandboxExecAndPrint(id, opts, {});
|
|
466
476
|
return;
|
|
467
477
|
}
|
|
468
|
-
const cmd =
|
|
478
|
+
const cmd = resolveSandboxCommand(words, opts);
|
|
469
479
|
const effectiveCommand = opts.background
|
|
470
480
|
? backgroundCommand(cmd)
|
|
471
481
|
: cmd;
|
|
@@ -479,7 +489,7 @@ export function register(program) {
|
|
|
479
489
|
}
|
|
480
490
|
const env = parseEnvPairs(opts.env ?? []);
|
|
481
491
|
if (opts.detached) {
|
|
482
|
-
const result = await createSandboxCommand(id,
|
|
492
|
+
const result = await createSandboxCommand(id, cmd, {
|
|
483
493
|
cwd,
|
|
484
494
|
env,
|
|
485
495
|
user: opts.user,
|
|
@@ -503,19 +513,17 @@ export function register(program) {
|
|
|
503
513
|
body["env"] = env;
|
|
504
514
|
if (opts.timeout != null)
|
|
505
515
|
body["timeout"] = opts.timeout;
|
|
506
|
-
await
|
|
516
|
+
await postSandboxExecAndPrint(id, opts, body);
|
|
507
517
|
}));
|
|
508
518
|
sandbox
|
|
509
519
|
.command("ports <sandbox-id>")
|
|
510
520
|
.description("List listening TCP ports inside a Sandbox")
|
|
511
521
|
.option("--json", "Output as JSON")
|
|
512
522
|
.action((id, opts) => runAction(async () => {
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
const stdout = String(result["stdout"] ?? "");
|
|
516
|
-
const ports = parseListeningPorts(stdout);
|
|
523
|
+
const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/ports`));
|
|
524
|
+
const ports = sandboxPortsFromResponse(result);
|
|
517
525
|
if (isJsonMode(opts)) {
|
|
518
|
-
console.log(JSON.stringify(
|
|
526
|
+
console.log(JSON.stringify(result, null, 2));
|
|
519
527
|
return;
|
|
520
528
|
}
|
|
521
529
|
if (ports.length === 0) {
|
|
@@ -523,6 +531,11 @@ export function register(program) {
|
|
|
523
531
|
return;
|
|
524
532
|
}
|
|
525
533
|
renderTable(ports, [
|
|
534
|
+
{
|
|
535
|
+
header: "PROTO",
|
|
536
|
+
key: (p) => p.protocol ?? "tcp",
|
|
537
|
+
width: 8,
|
|
538
|
+
},
|
|
526
539
|
{
|
|
527
540
|
header: "PORT",
|
|
528
541
|
key: "port",
|
|
@@ -533,8 +546,33 @@ export function register(program) {
|
|
|
533
546
|
key: "address",
|
|
534
547
|
width: 24,
|
|
535
548
|
},
|
|
549
|
+
{
|
|
550
|
+
header: "STATE",
|
|
551
|
+
key: (p) => p.state ?? "listen",
|
|
552
|
+
width: 10,
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
header: "PROCESS",
|
|
556
|
+
key: (p) => p.process?.name
|
|
557
|
+
? `${p.process.name}${p.process.pid ? `:${p.process.pid}` : ""}`
|
|
558
|
+
: chalk.dim("-"),
|
|
559
|
+
width: 28,
|
|
560
|
+
},
|
|
536
561
|
]);
|
|
537
562
|
}));
|
|
563
|
+
sandbox
|
|
564
|
+
.command("metrics <sandbox-id>")
|
|
565
|
+
.description("Show Sandbox resource, uptime, timeout, and readiness metrics")
|
|
566
|
+
.option("--window <window>", "Metrics window: 1h, 24h, or 7d", "1h")
|
|
567
|
+
.option("--json", "Output as JSON")
|
|
568
|
+
.action((id, opts) => runAction(async () => {
|
|
569
|
+
const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/metrics?window=${enc(opts.window)}`));
|
|
570
|
+
if (isJsonMode(opts)) {
|
|
571
|
+
console.log(JSON.stringify(result, null, 2));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
renderResourceMetrics("Sandbox metrics", result);
|
|
575
|
+
}));
|
|
538
576
|
sandbox
|
|
539
577
|
.command("deploy [local-dir]")
|
|
540
578
|
.description("Upload an app directory, start it in a sandbox, expose a preview URL, and wait for readiness")
|
|
@@ -652,6 +690,7 @@ export function register(program) {
|
|
|
652
690
|
.option("--run-command <cmd>", "Run command for dynamic/server deployments")
|
|
653
691
|
.option("--docker-deploy", "Publish onto the workspace Docker Deploy runtime instead of standard app hosting")
|
|
654
692
|
.option("--domain <domain>", "Custom domain to attach")
|
|
693
|
+
.option("--deployment-type <type>", "Deployment runtime type: miosa_deploy, docker_deploy, dynamic, static")
|
|
655
694
|
.option("--database <mode>", "none, create:postgres, postgres, or existing:<db-id>")
|
|
656
695
|
.option("--port <port>", "Runtime port for dynamic deployments", parseIntegerOption)
|
|
657
696
|
.option("--wait", "Wait for the production URL to answer")
|
|
@@ -669,6 +708,10 @@ export function register(program) {
|
|
|
669
708
|
console.log(` ${chalk.bold("App")} ${result.deployment_id ?? result.app_id ?? ""}`);
|
|
670
709
|
if (result.release_id)
|
|
671
710
|
console.log(` ${chalk.bold("Release")} ${result.release_id}`);
|
|
711
|
+
if (result.deployment_product)
|
|
712
|
+
console.log(` ${chalk.bold("Product")} ${String(result.deployment_product)}`);
|
|
713
|
+
if (result.docker_deploy_host_id)
|
|
714
|
+
console.log(` ${chalk.bold("Docker")} ${String(result.docker_deploy_host_id)}`);
|
|
672
715
|
if (result.url)
|
|
673
716
|
console.log(` ${chalk.bold("URL")} ${chalk.cyan(String(result.url))}`);
|
|
674
717
|
console.log(` ${chalk.bold("Ready")} ${result.ready ? chalk.green("yes") : chalk.yellow("pending")}`);
|
|
@@ -733,6 +776,7 @@ export function register(program) {
|
|
|
733
776
|
.description("Manage named long-running processes inside a sandbox");
|
|
734
777
|
service
|
|
735
778
|
.command("start <sandbox-id> <name>")
|
|
779
|
+
.alias("up")
|
|
736
780
|
.requiredOption("--cmd <command>", "Command to start")
|
|
737
781
|
.option("--cwd <path>", "Working directory inside the sandbox", "/workspace")
|
|
738
782
|
.option("--port <port>", "Port served by this service", parseIntegerOption)
|
|
@@ -901,6 +945,20 @@ export function register(program) {
|
|
|
901
945
|
}
|
|
902
946
|
renderDoctorReport(report);
|
|
903
947
|
}));
|
|
948
|
+
sandbox
|
|
949
|
+
.command("recover <name-or-id>")
|
|
950
|
+
.description("Inspect a failed or partial sandbox deploy and print recovery commands")
|
|
951
|
+
.option("--port <port>", "App port to diagnose", parseIntegerOption)
|
|
952
|
+
.option("--probe-path <path>", "HTTP path to probe", "/")
|
|
953
|
+
.option("--json", "Output as JSON")
|
|
954
|
+
.action((idOrName, opts) => runAction(async () => {
|
|
955
|
+
const report = await recoverSandboxDeploy(idOrName, opts);
|
|
956
|
+
if (isJsonMode(opts)) {
|
|
957
|
+
console.log(JSON.stringify(report, null, 2));
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
renderRecoverReport(report);
|
|
961
|
+
}));
|
|
904
962
|
// write-file — POST /sandboxes/:id/files with base64 content.
|
|
905
963
|
// If <content-or-file> is an existing local path, reads the file bytes;
|
|
906
964
|
// otherwise treats the argument as literal UTF-8 text.
|
|
@@ -1624,6 +1682,13 @@ async function createSandboxCommand(sandboxId, command, opts) {
|
|
|
1624
1682
|
status: result["status"] ?? "running",
|
|
1625
1683
|
};
|
|
1626
1684
|
}
|
|
1685
|
+
function warnOnTemplatePortMismatch(template, port, opts) {
|
|
1686
|
+
if (isJsonMode(opts))
|
|
1687
|
+
return;
|
|
1688
|
+
if ((template === "nextjs" || template === "next-js") && port !== 3000) {
|
|
1689
|
+
console.error(chalk.yellow("Next.js sandbox templates default to port 3000. Use --port 3000 unless you intentionally changed the app readiness port."));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1627
1692
|
function serviceLogPath(name) {
|
|
1628
1693
|
return `/tmp/miosa-services/${name}.log`;
|
|
1629
1694
|
}
|
|
@@ -1652,6 +1717,7 @@ async function deploySandbox(localDir, opts) {
|
|
|
1652
1717
|
manifestPort(appManifest) ??
|
|
1653
1718
|
detection?.port ??
|
|
1654
1719
|
5173;
|
|
1720
|
+
warnOnTemplatePortMismatch(opts.template ?? appManifest?.template ?? null, port, opts);
|
|
1655
1721
|
const probePath = opts.probePath ?? manifestProbePath(appManifest) ?? "/";
|
|
1656
1722
|
let remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
|
|
1657
1723
|
const start = opts.start ??
|
|
@@ -1802,6 +1868,11 @@ async function publishSandbox(sandboxId, opts) {
|
|
|
1802
1868
|
body["domain"] = opts.domain;
|
|
1803
1869
|
if (opts.port != null)
|
|
1804
1870
|
body["port"] = opts.port;
|
|
1871
|
+
const deploymentType = opts.dockerDeploy
|
|
1872
|
+
? "docker-deploy"
|
|
1873
|
+
: opts.deploymentType;
|
|
1874
|
+
if (deploymentType)
|
|
1875
|
+
body["deployment_type"] = deploymentType;
|
|
1805
1876
|
const database = parsePublishDatabase(opts.database);
|
|
1806
1877
|
if (database !== undefined)
|
|
1807
1878
|
body["database"] = database;
|
|
@@ -1822,11 +1893,29 @@ async function publishSandbox(sandboxId, opts) {
|
|
|
1822
1893
|
extractUrl(deployment) ??
|
|
1823
1894
|
stringField(data, "url") ??
|
|
1824
1895
|
null;
|
|
1896
|
+
let deploymentProduct = stringField(response, "deployment_product") ??
|
|
1897
|
+
stringField(data, "deployment_product") ??
|
|
1898
|
+
stringField(deployment, "deployment_product") ??
|
|
1899
|
+
stringField(asRecord(deployment?.["metadata"]), "deployment_product") ??
|
|
1900
|
+
null;
|
|
1901
|
+
let dockerDeployHostId = stringField(response, "docker_deploy_host_id") ??
|
|
1902
|
+
stringField(data, "docker_deploy_host_id") ??
|
|
1903
|
+
stringField(deployment, "docker_deploy_host_id") ??
|
|
1904
|
+
stringField(asRecord(deployment?.["metadata"]), "docker_deploy_host_id") ??
|
|
1905
|
+
null;
|
|
1825
1906
|
if (opts.wait && deploymentId) {
|
|
1826
1907
|
deployStep(opts, "Waiting for durable deployment");
|
|
1827
1908
|
const waited = await waitForDeploymentReady(c, deploymentId, opts.timeout);
|
|
1828
1909
|
state = stringField(waited, "state") ?? state;
|
|
1829
1910
|
url = extractUrl(waited) ?? url;
|
|
1911
|
+
deploymentProduct =
|
|
1912
|
+
stringField(waited, "deployment_product") ??
|
|
1913
|
+
stringField(asRecord(waited["metadata"]), "deployment_product") ??
|
|
1914
|
+
deploymentProduct;
|
|
1915
|
+
dockerDeployHostId =
|
|
1916
|
+
stringField(waited, "docker_deploy_host_id") ??
|
|
1917
|
+
stringField(asRecord(waited["metadata"]), "docker_deploy_host_id") ??
|
|
1918
|
+
dockerDeployHostId;
|
|
1830
1919
|
response["state"] = state;
|
|
1831
1920
|
if (url)
|
|
1832
1921
|
response["url"] = url;
|
|
@@ -1852,6 +1941,8 @@ async function publishSandbox(sandboxId, opts) {
|
|
|
1852
1941
|
version_id: versionId,
|
|
1853
1942
|
url,
|
|
1854
1943
|
state,
|
|
1944
|
+
deployment_product: deploymentProduct,
|
|
1945
|
+
docker_deploy_host_id: dockerDeployHostId,
|
|
1855
1946
|
ready,
|
|
1856
1947
|
probe,
|
|
1857
1948
|
data: raw,
|
|
@@ -1950,6 +2041,188 @@ function renderDoctorReport(report) {
|
|
|
1950
2041
|
console.log();
|
|
1951
2042
|
}
|
|
1952
2043
|
}
|
|
2044
|
+
async function recoverSandboxDeploy(idOrName, opts) {
|
|
2045
|
+
const c = client();
|
|
2046
|
+
const resolved = await resolveSandboxForRecovery(c, idOrName);
|
|
2047
|
+
if (!resolved.sandbox) {
|
|
2048
|
+
return {
|
|
2049
|
+
query: idOrName,
|
|
2050
|
+
sandbox: null,
|
|
2051
|
+
sandbox_id: null,
|
|
2052
|
+
matched_by: null,
|
|
2053
|
+
exec_ok: false,
|
|
2054
|
+
app_port: opts.port ?? null,
|
|
2055
|
+
preview_ready: null,
|
|
2056
|
+
preview_url: null,
|
|
2057
|
+
doctor: null,
|
|
2058
|
+
recommendations: [
|
|
2059
|
+
"No matching sandbox was found. Run `miosa sandbox list --json` and retry with a sandbox ID.",
|
|
2060
|
+
],
|
|
2061
|
+
commands: {
|
|
2062
|
+
list: "miosa sandbox list --json",
|
|
2063
|
+
},
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
const sandbox = resolved.sandbox;
|
|
2067
|
+
const sandboxId = str(sandbox["id"]);
|
|
2068
|
+
const name = stringOrNull(sandbox["name"]);
|
|
2069
|
+
const template = stringOrNull(sandbox["template_id"]);
|
|
2070
|
+
const port = opts.port ?? suggestedRecoveryPort(template);
|
|
2071
|
+
const execOk = await checkSandboxExec(c, sandboxId);
|
|
2072
|
+
const doctor = port != null ? await safeDoctorSandbox(sandboxId, port, opts.probePath) : null;
|
|
2073
|
+
const previewUrl = stringOrNull(doctor?.["preview_url"]);
|
|
2074
|
+
const previewReady = doctor && typeof doctor["preview_ready"] === "boolean"
|
|
2075
|
+
? Boolean(doctor["preview_ready"])
|
|
2076
|
+
: null;
|
|
2077
|
+
const recommendations = buildRecoveryRecommendations({
|
|
2078
|
+
sandbox,
|
|
2079
|
+
execOk,
|
|
2080
|
+
port,
|
|
2081
|
+
previewReady,
|
|
2082
|
+
template,
|
|
2083
|
+
});
|
|
2084
|
+
return {
|
|
2085
|
+
query: idOrName,
|
|
2086
|
+
sandbox,
|
|
2087
|
+
sandbox_id: sandboxId,
|
|
2088
|
+
matched_by: resolved.matchedBy,
|
|
2089
|
+
exec_ok: execOk,
|
|
2090
|
+
app_port: port,
|
|
2091
|
+
preview_ready: previewReady,
|
|
2092
|
+
preview_url: previewUrl,
|
|
2093
|
+
doctor,
|
|
2094
|
+
recommendations,
|
|
2095
|
+
commands: recoveryCommands(sandboxId, name, port),
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
async function resolveSandboxForRecovery(c, idOrName) {
|
|
2099
|
+
try {
|
|
2100
|
+
const direct = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(idOrName)}`)));
|
|
2101
|
+
const sandbox = asRecord(direct);
|
|
2102
|
+
if (sandbox)
|
|
2103
|
+
return { sandbox, matchedBy: "id" };
|
|
2104
|
+
}
|
|
2105
|
+
catch {
|
|
2106
|
+
// Fall through to name lookup.
|
|
2107
|
+
}
|
|
2108
|
+
const list = unwrap(await c.apiGet(apiPath("/sandboxes")));
|
|
2109
|
+
const items = Array.isArray(list)
|
|
2110
|
+
? list
|
|
2111
|
+
: Array.isArray((asRecord(list) ?? {})["data"])
|
|
2112
|
+
? (asRecord(list) ?? {})["data"]
|
|
2113
|
+
: [];
|
|
2114
|
+
const matches = items
|
|
2115
|
+
.map(asRecord)
|
|
2116
|
+
.filter((item) => Boolean(item))
|
|
2117
|
+
.filter((item) => {
|
|
2118
|
+
const name = stringOrNull(item["name"]);
|
|
2119
|
+
const id = stringOrNull(item["id"]);
|
|
2120
|
+
return name === idOrName || id?.startsWith(idOrName);
|
|
2121
|
+
})
|
|
2122
|
+
.sort((a, b) => {
|
|
2123
|
+
const aTime = Date.parse(stringOrNull(a["created_at"]) ?? "") || 0;
|
|
2124
|
+
const bTime = Date.parse(stringOrNull(b["created_at"]) ?? "") || 0;
|
|
2125
|
+
return bTime - aTime;
|
|
2126
|
+
});
|
|
2127
|
+
return { sandbox: matches[0] ?? null, matchedBy: matches[0] ? "name" : null };
|
|
2128
|
+
}
|
|
2129
|
+
async function checkSandboxExec(c, sandboxId) {
|
|
2130
|
+
try {
|
|
2131
|
+
const result = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), {
|
|
2132
|
+
command: "pwd",
|
|
2133
|
+
}));
|
|
2134
|
+
const record = asRecord(result);
|
|
2135
|
+
return Number(record?.["exit_code"] ?? 0) === 0;
|
|
2136
|
+
}
|
|
2137
|
+
catch {
|
|
2138
|
+
return false;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
async function safeDoctorSandbox(sandboxId, port, probePath) {
|
|
2142
|
+
try {
|
|
2143
|
+
return await doctorSandbox(sandboxId, port, probePath);
|
|
2144
|
+
}
|
|
2145
|
+
catch (err) {
|
|
2146
|
+
return {
|
|
2147
|
+
preview_ready: false,
|
|
2148
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
function suggestedRecoveryPort(template) {
|
|
2153
|
+
if (template === "nextjs" || template === "next-js")
|
|
2154
|
+
return 3000;
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
function buildRecoveryRecommendations(input) {
|
|
2158
|
+
const recs = [];
|
|
2159
|
+
const state = stringOrNull(input.sandbox["state"]);
|
|
2160
|
+
const ready = Boolean(input.sandbox["ready"]);
|
|
2161
|
+
if (state !== "running") {
|
|
2162
|
+
recs.push(`Sandbox state is ${state ?? "unknown"}; wait or recreate before uploading files.`);
|
|
2163
|
+
}
|
|
2164
|
+
else if (!ready) {
|
|
2165
|
+
recs.push("Sandbox exists but is not fully ready; try exec/write-file before retrying deploy.");
|
|
2166
|
+
}
|
|
2167
|
+
if (!input.execOk) {
|
|
2168
|
+
recs.push("Exec health failed; retry later or recreate the sandbox.");
|
|
2169
|
+
}
|
|
2170
|
+
else {
|
|
2171
|
+
recs.push("Exec works; if upload/deploy failed, use write-file/patch plus service up.");
|
|
2172
|
+
}
|
|
2173
|
+
if (input.template === "nextjs" && input.port !== 3000) {
|
|
2174
|
+
recs.push("Next.js templates default to port 3000; use 3000 unless you intentionally reconfigured readiness.");
|
|
2175
|
+
}
|
|
2176
|
+
if (input.port != null && input.previewReady === false) {
|
|
2177
|
+
recs.push("Preview is not ready; start the app process, then run sandbox wait.");
|
|
2178
|
+
}
|
|
2179
|
+
if (input.previewReady === true) {
|
|
2180
|
+
recs.push("Preview is ready; publish the sandbox to a durable deployment.");
|
|
2181
|
+
}
|
|
2182
|
+
return recs;
|
|
2183
|
+
}
|
|
2184
|
+
function recoveryCommands(sandboxId, name, port) {
|
|
2185
|
+
const commands = {
|
|
2186
|
+
health: `miosa sandbox exec ${sandboxId} --json -- pwd`,
|
|
2187
|
+
files: `miosa sandbox write-file ${sandboxId} /workspace/app/page.jsx ./page.jsx --json`,
|
|
2188
|
+
};
|
|
2189
|
+
if (port != null) {
|
|
2190
|
+
commands.start = `miosa sandbox service up ${sandboxId} next --cwd /workspace --port ${port} --cmd "npm run dev -- --hostname 0.0.0.0 --port ${port}" --json`;
|
|
2191
|
+
commands.wait = `miosa sandbox wait ${sandboxId} --port ${port} --timeout 180 --json`;
|
|
2192
|
+
commands.publish = `miosa sandbox publish ${sandboxId} --path /workspace --name ${shellArg(name ?? "Recovered app")} --build-command "npm run build" --run-command "npm run start" --port ${port} --wait --timeout 900s --json`;
|
|
2193
|
+
}
|
|
2194
|
+
return commands;
|
|
2195
|
+
}
|
|
2196
|
+
function renderRecoverReport(report) {
|
|
2197
|
+
if (!report.sandbox_id) {
|
|
2198
|
+
console.log(chalk.red("No matching sandbox found."));
|
|
2199
|
+
console.log(` ${report.commands["list"]}`);
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
console.log(chalk.bold("Sandbox recovery"));
|
|
2203
|
+
console.log();
|
|
2204
|
+
console.log(` ${chalk.bold("Sandbox")} ${report.sandbox_id}`);
|
|
2205
|
+
console.log(` ${chalk.bold("Matched")} ${report.matched_by}`);
|
|
2206
|
+
console.log(` ${chalk.bold("Exec")} ${report.exec_ok ? chalk.green("ok") : chalk.red("failed")}`);
|
|
2207
|
+
if (report.app_port != null) {
|
|
2208
|
+
console.log(` ${chalk.bold("Port")} ${report.app_port}`);
|
|
2209
|
+
}
|
|
2210
|
+
if (report.preview_url) {
|
|
2211
|
+
console.log(` ${chalk.bold("Preview")} ${chalk.cyan(report.preview_url)}`);
|
|
2212
|
+
}
|
|
2213
|
+
if (report.preview_ready != null) {
|
|
2214
|
+
console.log(` ${chalk.bold("Ready")} ${report.preview_ready ? chalk.green("yes") : chalk.yellow("no")}`);
|
|
2215
|
+
}
|
|
2216
|
+
console.log();
|
|
2217
|
+
for (const rec of report.recommendations) {
|
|
2218
|
+
console.log(` - ${rec}`);
|
|
2219
|
+
}
|
|
2220
|
+
console.log();
|
|
2221
|
+
console.log(chalk.bold("Next commands"));
|
|
2222
|
+
for (const [label, command] of Object.entries(report.commands)) {
|
|
2223
|
+
console.log(` ${chalk.dim(label.padEnd(7))} ${command}`);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
1953
2226
|
async function createSandboxForDeploy(c, template, name, source) {
|
|
1954
2227
|
const body = { template_id: template };
|
|
1955
2228
|
if (name)
|
|
@@ -2000,12 +2273,115 @@ async function waitForSandboxRunning(c, sandboxId, timeoutSec) {
|
|
|
2000
2273
|
if (state === "running" || state === "active")
|
|
2001
2274
|
return sandbox;
|
|
2002
2275
|
if (state === "error" || state === "failed") {
|
|
2003
|
-
throw
|
|
2276
|
+
throw sandboxStateError(sandboxId, sandbox, state);
|
|
2277
|
+
}
|
|
2278
|
+
if (state === "destroyed") {
|
|
2279
|
+
throw sandboxStateError(sandboxId, sandbox, state);
|
|
2004
2280
|
}
|
|
2005
2281
|
await sleep(1500);
|
|
2006
2282
|
}
|
|
2007
2283
|
throw new UserError(`Sandbox ${sandboxId} did not become running within ${timeoutSec}s.`);
|
|
2008
2284
|
}
|
|
2285
|
+
async function resumeSandboxAndPrint(sandboxId, opts) {
|
|
2286
|
+
try {
|
|
2287
|
+
await postAndPrint(`/sandboxes/${enc(sandboxId)}/resume`, opts, {});
|
|
2288
|
+
}
|
|
2289
|
+
catch (err) {
|
|
2290
|
+
if (err instanceof ApiResponseError && err.code === "SANDBOX_NOT_PAUSED") {
|
|
2291
|
+
throw await enrichSandboxLifecycleError(sandboxId, err);
|
|
2292
|
+
}
|
|
2293
|
+
throw err;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
async function postSandboxExecAndPrint(sandboxId, opts, body) {
|
|
2297
|
+
try {
|
|
2298
|
+
const value = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
|
|
2299
|
+
printValue(value, opts);
|
|
2300
|
+
}
|
|
2301
|
+
catch (err) {
|
|
2302
|
+
if (err instanceof ApiResponseError && err.code === "SANDBOX_NOT_RUNNING") {
|
|
2303
|
+
throw await enrichSandboxLifecycleError(sandboxId, err);
|
|
2304
|
+
}
|
|
2305
|
+
throw err;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async function enrichSandboxLifecycleError(sandboxId, err) {
|
|
2309
|
+
let sandbox = null;
|
|
2310
|
+
try {
|
|
2311
|
+
sandbox = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
|
|
2312
|
+
}
|
|
2313
|
+
catch {
|
|
2314
|
+
// Preserve the original API error when the follow-up lookup is unavailable.
|
|
2315
|
+
}
|
|
2316
|
+
if (!sandbox)
|
|
2317
|
+
return err;
|
|
2318
|
+
const state = String(sandbox["state"] ?? sandbox["status"] ?? "unknown");
|
|
2319
|
+
const details = sandboxLifecycleDetails(sandbox);
|
|
2320
|
+
const message = state === "destroyed"
|
|
2321
|
+
? `Sandbox ${sandboxId} is destroyed, not paused. It cannot be resumed.`
|
|
2322
|
+
: `${err.message}. Current sandbox state: ${state}.`;
|
|
2323
|
+
return new ApiResponseError(err.code, message, EXIT_USER_ERROR, false, sandboxRecoveryHint(sandboxId, sandbox), details, err.requestId);
|
|
2324
|
+
}
|
|
2325
|
+
function sandboxStateError(sandboxId, sandbox, state) {
|
|
2326
|
+
const reason = sandboxLastErrorReason(sandbox);
|
|
2327
|
+
const message = state === "destroyed"
|
|
2328
|
+
? `Sandbox ${sandboxId} is destroyed, not paused. It cannot be resumed.`
|
|
2329
|
+
: `Sandbox ${sandboxId} entered ${state} state${reason ? `: ${reason}` : ""}.`;
|
|
2330
|
+
return new UserError(message, sandboxRecoveryHint(sandboxId, sandbox));
|
|
2331
|
+
}
|
|
2332
|
+
function sandboxRecoveryHint(sandboxId, sandbox) {
|
|
2333
|
+
const state = String(sandbox["state"] ?? sandbox["status"] ?? "unknown");
|
|
2334
|
+
const name = String(sandbox["name"] ?? "new-sandbox");
|
|
2335
|
+
const template = String(sandbox["template_id"] ?? "nextjs");
|
|
2336
|
+
const timeoutRemainingSec = timeoutRemainingSeconds(sandbox);
|
|
2337
|
+
if (state === "running" && timeoutRemainingSec != null && timeoutRemainingSec <= EXPIRING_SANDBOX_THRESHOLD_SEC) {
|
|
2338
|
+
return `Sandbox expires soon. Extend it with: miosa sandbox extend ${sandboxId} --timeout 1h`;
|
|
2339
|
+
}
|
|
2340
|
+
if (state === "paused") {
|
|
2341
|
+
return `Resume it with: miosa sandbox resume ${sandboxId}`;
|
|
2342
|
+
}
|
|
2343
|
+
if (state === "destroyed") {
|
|
2344
|
+
return `Create a replacement with: miosa sandbox create --template ${template} --name ${shellQuote(name)} --timeout 1h --wait --json`;
|
|
2345
|
+
}
|
|
2346
|
+
if (state === "error" || state === "failed") {
|
|
2347
|
+
return `Inspect recovery with: miosa sandbox recover ${sandboxId}`;
|
|
2348
|
+
}
|
|
2349
|
+
return `Inspect it with: miosa sandbox show ${sandboxId} --json`;
|
|
2350
|
+
}
|
|
2351
|
+
function sandboxLifecycleDetails(sandbox) {
|
|
2352
|
+
return {
|
|
2353
|
+
sandbox_id: sandbox["id"],
|
|
2354
|
+
name: sandbox["name"],
|
|
2355
|
+
state: sandbox["state"] ?? sandbox["status"],
|
|
2356
|
+
timeout_sec: sandbox["timeout_sec"],
|
|
2357
|
+
timeout_remaining_ms: sandbox["timeout_remaining_ms"],
|
|
2358
|
+
destroyed_at: sandbox["destroyed_at"],
|
|
2359
|
+
last_error: sandboxLastError(sandbox),
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
function sandboxLastErrorReason(sandbox) {
|
|
2363
|
+
const lastError = sandboxLastError(sandbox);
|
|
2364
|
+
if (!lastError)
|
|
2365
|
+
return null;
|
|
2366
|
+
const reason = lastError["reason"];
|
|
2367
|
+
return typeof reason === "string" && reason.length > 0 ? reason : null;
|
|
2368
|
+
}
|
|
2369
|
+
function sandboxLastError(sandbox) {
|
|
2370
|
+
const metadata = sandbox["metadata"];
|
|
2371
|
+
if (!metadata || typeof metadata !== "object")
|
|
2372
|
+
return null;
|
|
2373
|
+
const lastError = metadata["last_error"];
|
|
2374
|
+
return lastError && typeof lastError === "object"
|
|
2375
|
+
? lastError
|
|
2376
|
+
: null;
|
|
2377
|
+
}
|
|
2378
|
+
function timeoutRemainingSeconds(sandbox) {
|
|
2379
|
+
const ms = sandbox["timeout_remaining_ms"];
|
|
2380
|
+
if (typeof ms === "number")
|
|
2381
|
+
return Math.ceil(ms / 1000);
|
|
2382
|
+
const sec = sandbox["timeout_remaining_sec"];
|
|
2383
|
+
return typeof sec === "number" ? sec : null;
|
|
2384
|
+
}
|
|
2009
2385
|
function createDeployArchive(sourceDir) {
|
|
2010
2386
|
const archivePath = path.join(os.tmpdir(), `miosa-deploy-${process.pid}-${Date.now()}.tgz`);
|
|
2011
2387
|
const result = spawnSync("tar", [
|
|
@@ -2077,7 +2453,7 @@ function shouldFallbackSandboxUpload(err) {
|
|
|
2077
2453
|
/AGENT_UNAVAILABLE|SANDBOX_FILE_AGENT_UNAVAILABLE/i.test(err.code));
|
|
2078
2454
|
}
|
|
2079
2455
|
if (err instanceof Error) {
|
|
2080
|
-
return /fetch failed|ECONNRESET|HTTP 502|AGENT_UNAVAILABLE/i.test(err.message);
|
|
2456
|
+
return /fetch failed|ECONNRESET|HTTP 502|AGENT_UNAVAILABLE|other side closed|socket hang up/i.test(err.message);
|
|
2081
2457
|
}
|
|
2082
2458
|
return false;
|
|
2083
2459
|
}
|
|
@@ -2204,37 +2580,93 @@ async function execSandboxRaw(c, sandboxId, command, cwd, timeout) {
|
|
|
2204
2580
|
body["timeout"] = timeout;
|
|
2205
2581
|
return unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
|
|
2206
2582
|
}
|
|
2207
|
-
function
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
continue;
|
|
2226
|
-
}
|
|
2227
|
-
const proc = procLine.exec(line);
|
|
2228
|
-
if (proc && proc[3] === "0A" && proc[2]) {
|
|
2229
|
-
const port = parseInt(proc[2], 16);
|
|
2230
|
-
if (!Number.isNaN(port) && !seen.has(port)) {
|
|
2231
|
-
seen.add(port);
|
|
2232
|
-
out.push({ port, address: "*" });
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2583
|
+
function sandboxPortsFromResponse(raw) {
|
|
2584
|
+
const root = unwrap(raw);
|
|
2585
|
+
if (Array.isArray(root))
|
|
2586
|
+
return root;
|
|
2587
|
+
if (root && typeof root === "object") {
|
|
2588
|
+
const row = root;
|
|
2589
|
+
if (Array.isArray(row["ports"]))
|
|
2590
|
+
return row["ports"];
|
|
2591
|
+
if (Array.isArray(row["data"]))
|
|
2592
|
+
return row["data"];
|
|
2593
|
+
}
|
|
2594
|
+
return [];
|
|
2595
|
+
}
|
|
2596
|
+
function renderResourceMetrics(title, raw) {
|
|
2597
|
+
const root = unwrap(raw);
|
|
2598
|
+
if (!root || typeof root !== "object") {
|
|
2599
|
+
printValue(root, {});
|
|
2600
|
+
return;
|
|
2235
2601
|
}
|
|
2236
|
-
|
|
2237
|
-
|
|
2602
|
+
const row = root;
|
|
2603
|
+
const current = row["current"] && typeof row["current"] === "object"
|
|
2604
|
+
? row["current"]
|
|
2605
|
+
: {};
|
|
2606
|
+
printBanner({ subtitle: title });
|
|
2607
|
+
console.log(kvPanel([
|
|
2608
|
+
{ label: "resource_id", value: String(row["resource_id"] ?? row["sandbox_id"] ?? "-") },
|
|
2609
|
+
{ label: "window", value: String(row["window"] ?? "1h") },
|
|
2610
|
+
{ label: "state", value: formatState(current["state"]) },
|
|
2611
|
+
{ label: "ready", value: formatBool(current["ready"]) },
|
|
2612
|
+
{ label: "cpu", value: formatMaybe(current["cpu_count"]) },
|
|
2613
|
+
{ label: "memory", value: formatMb(current["memory_mb"]) },
|
|
2614
|
+
{ label: "disk", value: formatMb(current["disk_size_mb"]) },
|
|
2615
|
+
{ label: "uptime", value: formatSeconds(current["uptime_sec"]) },
|
|
2616
|
+
{
|
|
2617
|
+
label: "timeout_remaining",
|
|
2618
|
+
value: formatSecondsOrAlwaysOn(current["timeout_remaining_sec"]),
|
|
2619
|
+
},
|
|
2620
|
+
{ label: "node", value: formatMaybe(current["node_id"]) },
|
|
2621
|
+
{ label: "ip", value: formatMaybe(current["ip_address"]) },
|
|
2622
|
+
{ label: "boot", value: formatMs(current["boot_ms"]) },
|
|
2623
|
+
{ label: "envd_ready", value: formatMs(current["envd_ready_ms"]) },
|
|
2624
|
+
]));
|
|
2625
|
+
}
|
|
2626
|
+
function formatState(value) {
|
|
2627
|
+
const state = String(value ?? "unknown");
|
|
2628
|
+
if (["running", "active", "healthy", "ready"].includes(state)) {
|
|
2629
|
+
return chalk.green(state);
|
|
2630
|
+
}
|
|
2631
|
+
if (["provisioning", "starting", "building", "pending"].includes(state)) {
|
|
2632
|
+
return chalk.yellow(state);
|
|
2633
|
+
}
|
|
2634
|
+
if (["failed", "error", "unhealthy"].includes(state)) {
|
|
2635
|
+
return chalk.red(state);
|
|
2636
|
+
}
|
|
2637
|
+
return state;
|
|
2638
|
+
}
|
|
2639
|
+
function formatBool(value) {
|
|
2640
|
+
if (value === true)
|
|
2641
|
+
return chalk.green("true");
|
|
2642
|
+
if (value === false)
|
|
2643
|
+
return chalk.red("false");
|
|
2644
|
+
return chalk.dim("-");
|
|
2645
|
+
}
|
|
2646
|
+
function formatMaybe(value) {
|
|
2647
|
+
if (value === null || value === undefined || value === "")
|
|
2648
|
+
return chalk.dim("-");
|
|
2649
|
+
return String(value);
|
|
2650
|
+
}
|
|
2651
|
+
function formatMb(value) {
|
|
2652
|
+
if (typeof value !== "number")
|
|
2653
|
+
return formatMaybe(value);
|
|
2654
|
+
return formatBytes(value * 1024 * 1024);
|
|
2655
|
+
}
|
|
2656
|
+
function formatMs(value) {
|
|
2657
|
+
if (typeof value !== "number")
|
|
2658
|
+
return formatMaybe(value);
|
|
2659
|
+
return `${value}ms`;
|
|
2660
|
+
}
|
|
2661
|
+
function formatSeconds(value) {
|
|
2662
|
+
if (typeof value !== "number")
|
|
2663
|
+
return formatMaybe(value);
|
|
2664
|
+
return formatDuration(value * 1000);
|
|
2665
|
+
}
|
|
2666
|
+
function formatSecondsOrAlwaysOn(value) {
|
|
2667
|
+
if (value === null || value === undefined)
|
|
2668
|
+
return chalk.dim("always-on/none");
|
|
2669
|
+
return formatSeconds(value);
|
|
2238
2670
|
}
|
|
2239
2671
|
// Stream exec output: run in background to a log file, poll-read new bytes
|
|
2240
2672
|
// until the process exits, then print the final exit code.
|
|
@@ -2524,6 +2956,15 @@ function backgroundCommand(command) {
|
|
|
2524
2956
|
const logPath = `/tmp/miosa-bg-${Date.now()}.log`;
|
|
2525
2957
|
return `nohup sh -lc ${shellQuote(command)} > ${shellQuote(logPath)} 2>&1 & echo $!`;
|
|
2526
2958
|
}
|
|
2959
|
+
function resolveSandboxCommand(words, opts) {
|
|
2960
|
+
const positional = words.length === 1 ? (words[0] ?? "") : joinCommandWords(words);
|
|
2961
|
+
const cmd = opts.cmd ?? opts.command ?? positional;
|
|
2962
|
+
if (!opts.shellCmd)
|
|
2963
|
+
return cmd;
|
|
2964
|
+
if (!cmd.trim())
|
|
2965
|
+
return opts.shellCmd;
|
|
2966
|
+
return `${opts.shellCmd} ${shellQuote(cmd)}`;
|
|
2967
|
+
}
|
|
2527
2968
|
function collectOption(value, previous) {
|
|
2528
2969
|
return [...previous, value];
|
|
2529
2970
|
}
|
|
@@ -2657,6 +3098,12 @@ function str(v) {
|
|
|
2657
3098
|
return chalk.dim("—");
|
|
2658
3099
|
return String(v);
|
|
2659
3100
|
}
|
|
3101
|
+
function stringOrNull(v) {
|
|
3102
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
3103
|
+
}
|
|
3104
|
+
function shellArg(value) {
|
|
3105
|
+
return `'${value.replace(/'/g, "'\"'\"'")}'`;
|
|
3106
|
+
}
|
|
2660
3107
|
/** Apply semantic color to a sandbox status string. */
|
|
2661
3108
|
function statusColor(s) {
|
|
2662
3109
|
const lower = s.toLowerCase().trim();
|