@miosa/cli 1.0.55 → 1.0.57
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/cp.d.ts.map +1 -1
- package/dist/commands/cp.js +36 -5
- package/dist/commands/cp.js.map +1 -1
- 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 +596 -119
- 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
|
@@ -15,7 +15,7 @@ import { handleError, isJsonMode } from "./util.js";
|
|
|
15
15
|
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
|
-
import { UserError } from "../errors.js";
|
|
18
|
+
import { ApiResponseError, MiosaError, NetworkError, ServerError, UserError, } from "../errors.js";
|
|
19
19
|
export function register(program) {
|
|
20
20
|
// -------------------------------------------------------------------------
|
|
21
21
|
// sandbox / sandboxes command group — built manually to avoid subcommand
|
|
@@ -383,6 +383,9 @@ export function register(program) {
|
|
|
383
383
|
.allowExcessArguments()
|
|
384
384
|
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
385
385
|
.option("--workdir <path>", "Alias for --cwd")
|
|
386
|
+
.option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
|
|
387
|
+
.option("--command <command>", "Alias for --cmd")
|
|
388
|
+
.option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
|
|
386
389
|
.option("--env <pair>", "Environment variable KEY=VALUE. Repeatable.", collectOption, [])
|
|
387
390
|
.option("--background", "Start the command in the background and return immediately")
|
|
388
391
|
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
@@ -400,7 +403,7 @@ export function register(program) {
|
|
|
400
403
|
await postAndPrint(`/sandboxes/${enc(id)}/exec`, opts, {});
|
|
401
404
|
return;
|
|
402
405
|
}
|
|
403
|
-
const cmd =
|
|
406
|
+
const cmd = resolveSandboxCommand(words, opts);
|
|
404
407
|
const effectiveCommand = opts.background
|
|
405
408
|
? backgroundCommand(cmd)
|
|
406
409
|
: cmd;
|
|
@@ -414,7 +417,7 @@ export function register(program) {
|
|
|
414
417
|
}
|
|
415
418
|
const env = parseEnvPairs(opts.env ?? []);
|
|
416
419
|
if (opts.detached) {
|
|
417
|
-
const result = await createSandboxCommand(id,
|
|
420
|
+
const result = await createSandboxCommand(id, cmd, {
|
|
418
421
|
cwd,
|
|
419
422
|
env,
|
|
420
423
|
user: opts.user,
|
|
@@ -448,6 +451,9 @@ export function register(program) {
|
|
|
448
451
|
.allowExcessArguments()
|
|
449
452
|
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
450
453
|
.option("--workdir <path>", "Alias for --cwd")
|
|
454
|
+
.option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
|
|
455
|
+
.option("--command <command>", "Alias for --cmd")
|
|
456
|
+
.option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
|
|
451
457
|
.option("--env <pair>", "Environment variable KEY=VALUE. Repeatable.", collectOption, [])
|
|
452
458
|
.option("--background", "Start the command in the background and return immediately")
|
|
453
459
|
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
@@ -465,7 +471,7 @@ export function register(program) {
|
|
|
465
471
|
await postAndPrint(`/sandboxes/${enc(id)}/exec`, opts, {});
|
|
466
472
|
return;
|
|
467
473
|
}
|
|
468
|
-
const cmd =
|
|
474
|
+
const cmd = resolveSandboxCommand(words, opts);
|
|
469
475
|
const effectiveCommand = opts.background
|
|
470
476
|
? backgroundCommand(cmd)
|
|
471
477
|
: cmd;
|
|
@@ -479,7 +485,7 @@ export function register(program) {
|
|
|
479
485
|
}
|
|
480
486
|
const env = parseEnvPairs(opts.env ?? []);
|
|
481
487
|
if (opts.detached) {
|
|
482
|
-
const result = await createSandboxCommand(id,
|
|
488
|
+
const result = await createSandboxCommand(id, cmd, {
|
|
483
489
|
cwd,
|
|
484
490
|
env,
|
|
485
491
|
user: opts.user,
|
|
@@ -510,12 +516,10 @@ export function register(program) {
|
|
|
510
516
|
.description("List listening TCP ports inside a Sandbox")
|
|
511
517
|
.option("--json", "Output as JSON")
|
|
512
518
|
.action((id, opts) => runAction(async () => {
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
const stdout = String(result["stdout"] ?? "");
|
|
516
|
-
const ports = parseListeningPorts(stdout);
|
|
519
|
+
const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/ports`));
|
|
520
|
+
const ports = sandboxPortsFromResponse(result);
|
|
517
521
|
if (isJsonMode(opts)) {
|
|
518
|
-
console.log(JSON.stringify(
|
|
522
|
+
console.log(JSON.stringify(result, null, 2));
|
|
519
523
|
return;
|
|
520
524
|
}
|
|
521
525
|
if (ports.length === 0) {
|
|
@@ -523,6 +527,11 @@ export function register(program) {
|
|
|
523
527
|
return;
|
|
524
528
|
}
|
|
525
529
|
renderTable(ports, [
|
|
530
|
+
{
|
|
531
|
+
header: "PROTO",
|
|
532
|
+
key: (p) => p.protocol ?? "tcp",
|
|
533
|
+
width: 8,
|
|
534
|
+
},
|
|
526
535
|
{
|
|
527
536
|
header: "PORT",
|
|
528
537
|
key: "port",
|
|
@@ -533,8 +542,33 @@ export function register(program) {
|
|
|
533
542
|
key: "address",
|
|
534
543
|
width: 24,
|
|
535
544
|
},
|
|
545
|
+
{
|
|
546
|
+
header: "STATE",
|
|
547
|
+
key: (p) => p.state ?? "listen",
|
|
548
|
+
width: 10,
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
header: "PROCESS",
|
|
552
|
+
key: (p) => p.process?.name
|
|
553
|
+
? `${p.process.name}${p.process.pid ? `:${p.process.pid}` : ""}`
|
|
554
|
+
: chalk.dim("-"),
|
|
555
|
+
width: 28,
|
|
556
|
+
},
|
|
536
557
|
]);
|
|
537
558
|
}));
|
|
559
|
+
sandbox
|
|
560
|
+
.command("metrics <sandbox-id>")
|
|
561
|
+
.description("Show Sandbox resource, uptime, timeout, and readiness metrics")
|
|
562
|
+
.option("--window <window>", "Metrics window: 1h, 24h, or 7d", "1h")
|
|
563
|
+
.option("--json", "Output as JSON")
|
|
564
|
+
.action((id, opts) => runAction(async () => {
|
|
565
|
+
const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/metrics?window=${enc(opts.window)}`));
|
|
566
|
+
if (isJsonMode(opts)) {
|
|
567
|
+
console.log(JSON.stringify(result, null, 2));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
renderResourceMetrics("Sandbox metrics", result);
|
|
571
|
+
}));
|
|
538
572
|
sandbox
|
|
539
573
|
.command("deploy [local-dir]")
|
|
540
574
|
.description("Upload an app directory, start it in a sandbox, expose a preview URL, and wait for readiness")
|
|
@@ -553,21 +587,26 @@ export function register(program) {
|
|
|
553
587
|
.option("--timeout <duration>", "Wait timeout, e.g. 180s or 3m", parseDurationSec, 180)
|
|
554
588
|
.option("--probe-path <path>", "HTTP path to probe")
|
|
555
589
|
.option("--json", "Output as JSON")
|
|
556
|
-
.action((localDir = ".", opts) =>
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
590
|
+
.action(async (localDir = ".", opts) => {
|
|
591
|
+
try {
|
|
592
|
+
const result = await deploySandbox(localDir, opts);
|
|
593
|
+
if (isJsonMode(opts)) {
|
|
594
|
+
console.log(JSON.stringify(result, null, 2));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
console.log();
|
|
598
|
+
console.log(` ${chalk.bold("Sandbox")} ${result.sandbox_id}`);
|
|
599
|
+
console.log(` ${chalk.bold("Port")} ${result.port}`);
|
|
600
|
+
console.log(` ${chalk.bold("Preview")} ${chalk.cyan(result.preview_url)}`);
|
|
601
|
+
console.log(` ${chalk.bold("Ready")} ${result.preview_ready
|
|
602
|
+
? chalk.green("yes")
|
|
603
|
+
: chalk.yellow("not verified")}`);
|
|
604
|
+
console.log();
|
|
561
605
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
console.log(` ${chalk.bold("Ready")} ${result.preview_ready
|
|
567
|
-
? chalk.green("yes")
|
|
568
|
-
: chalk.yellow("not verified")}`);
|
|
569
|
-
console.log();
|
|
570
|
-
}));
|
|
606
|
+
catch (err) {
|
|
607
|
+
handleSandboxDeployError(err, opts);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
571
610
|
sandbox
|
|
572
611
|
.command("preview <sandbox-id>")
|
|
573
612
|
.description("Expose a sandbox port and optionally wait for the public preview to answer")
|
|
@@ -647,6 +686,7 @@ export function register(program) {
|
|
|
647
686
|
.option("--run-command <cmd>", "Run command for dynamic/server deployments")
|
|
648
687
|
.option("--docker-deploy", "Publish onto the workspace Docker Deploy runtime instead of standard app hosting")
|
|
649
688
|
.option("--domain <domain>", "Custom domain to attach")
|
|
689
|
+
.option("--deployment-type <type>", "Deployment runtime type: miosa_deploy, docker_deploy, dynamic, static")
|
|
650
690
|
.option("--database <mode>", "none, create:postgres, postgres, or existing:<db-id>")
|
|
651
691
|
.option("--port <port>", "Runtime port for dynamic deployments", parseIntegerOption)
|
|
652
692
|
.option("--wait", "Wait for the production URL to answer")
|
|
@@ -664,6 +704,10 @@ export function register(program) {
|
|
|
664
704
|
console.log(` ${chalk.bold("App")} ${result.deployment_id ?? result.app_id ?? ""}`);
|
|
665
705
|
if (result.release_id)
|
|
666
706
|
console.log(` ${chalk.bold("Release")} ${result.release_id}`);
|
|
707
|
+
if (result.deployment_product)
|
|
708
|
+
console.log(` ${chalk.bold("Product")} ${String(result.deployment_product)}`);
|
|
709
|
+
if (result.docker_deploy_host_id)
|
|
710
|
+
console.log(` ${chalk.bold("Docker")} ${String(result.docker_deploy_host_id)}`);
|
|
667
711
|
if (result.url)
|
|
668
712
|
console.log(` ${chalk.bold("URL")} ${chalk.cyan(String(result.url))}`);
|
|
669
713
|
console.log(` ${chalk.bold("Ready")} ${result.ready ? chalk.green("yes") : chalk.yellow("pending")}`);
|
|
@@ -728,6 +772,7 @@ export function register(program) {
|
|
|
728
772
|
.description("Manage named long-running processes inside a sandbox");
|
|
729
773
|
service
|
|
730
774
|
.command("start <sandbox-id> <name>")
|
|
775
|
+
.alias("up")
|
|
731
776
|
.requiredOption("--cmd <command>", "Command to start")
|
|
732
777
|
.option("--cwd <path>", "Working directory inside the sandbox", "/workspace")
|
|
733
778
|
.option("--port <port>", "Port served by this service", parseIntegerOption)
|
|
@@ -896,6 +941,20 @@ export function register(program) {
|
|
|
896
941
|
}
|
|
897
942
|
renderDoctorReport(report);
|
|
898
943
|
}));
|
|
944
|
+
sandbox
|
|
945
|
+
.command("recover <name-or-id>")
|
|
946
|
+
.description("Inspect a failed or partial sandbox deploy and print recovery commands")
|
|
947
|
+
.option("--port <port>", "App port to diagnose", parseIntegerOption)
|
|
948
|
+
.option("--probe-path <path>", "HTTP path to probe", "/")
|
|
949
|
+
.option("--json", "Output as JSON")
|
|
950
|
+
.action((idOrName, opts) => runAction(async () => {
|
|
951
|
+
const report = await recoverSandboxDeploy(idOrName, opts);
|
|
952
|
+
if (isJsonMode(opts)) {
|
|
953
|
+
console.log(JSON.stringify(report, null, 2));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
renderRecoverReport(report);
|
|
957
|
+
}));
|
|
899
958
|
// write-file — POST /sandboxes/:id/files with base64 content.
|
|
900
959
|
// If <content-or-file> is an existing local path, reads the file bytes;
|
|
901
960
|
// otherwise treats the argument as literal UTF-8 text.
|
|
@@ -908,8 +967,7 @@ export function register(program) {
|
|
|
908
967
|
const contentBytes = fs.existsSync(contentArg)
|
|
909
968
|
? fs.readFileSync(contentArg)
|
|
910
969
|
: Buffer.from(contentArg, "utf8");
|
|
911
|
-
const
|
|
912
|
-
const result = await fetchApiRaw(apiPath(`/sandboxes/${enc(id)}/files`), { path: remotePath, content: base64 });
|
|
970
|
+
const result = await writeBytesToSandbox(client(), id, remotePath, contentBytes);
|
|
913
971
|
if (!isJsonMode(opts)) {
|
|
914
972
|
console.log(chalk.green(`Written to ${remotePath}`));
|
|
915
973
|
}
|
|
@@ -970,10 +1028,8 @@ export function register(program) {
|
|
|
970
1028
|
console.error(chalk.red(`File not found: ${localPath}`));
|
|
971
1029
|
process.exit(1);
|
|
972
1030
|
}
|
|
973
|
-
const data = fs.readFileSync(localPath);
|
|
974
|
-
const base64 = data.toString("base64");
|
|
975
1031
|
const c = client();
|
|
976
|
-
const result = await c
|
|
1032
|
+
const result = await writeBytesToSandbox(c, id, remotePath, fs.readFileSync(localPath));
|
|
977
1033
|
if (isJsonMode(opts)) {
|
|
978
1034
|
printValue(result, opts);
|
|
979
1035
|
}
|
|
@@ -1499,6 +1555,18 @@ async function runSandboxPortForward(id, opts) {
|
|
|
1499
1555
|
process.stderr.write("\n");
|
|
1500
1556
|
server.close();
|
|
1501
1557
|
}
|
|
1558
|
+
class SandboxDeployPartialError extends Error {
|
|
1559
|
+
causeError;
|
|
1560
|
+
sandboxId;
|
|
1561
|
+
recoveryCommand;
|
|
1562
|
+
constructor(causeError, sandboxId, recoveryCommand) {
|
|
1563
|
+
super(causeError instanceof Error ? causeError.message : String(causeError));
|
|
1564
|
+
this.causeError = causeError;
|
|
1565
|
+
this.sandboxId = sandboxId;
|
|
1566
|
+
this.recoveryCommand = recoveryCommand;
|
|
1567
|
+
this.name = "SandboxDeployPartialError";
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1502
1570
|
async function showSandboxWithPreview(sandboxId, port, probePath) {
|
|
1503
1571
|
const c = client();
|
|
1504
1572
|
const sandbox = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
|
|
@@ -1610,6 +1678,13 @@ async function createSandboxCommand(sandboxId, command, opts) {
|
|
|
1610
1678
|
status: result["status"] ?? "running",
|
|
1611
1679
|
};
|
|
1612
1680
|
}
|
|
1681
|
+
function warnOnTemplatePortMismatch(template, port, opts) {
|
|
1682
|
+
if (isJsonMode(opts))
|
|
1683
|
+
return;
|
|
1684
|
+
if ((template === "nextjs" || template === "next-js") && port !== 3000) {
|
|
1685
|
+
console.error(chalk.yellow("Next.js sandbox templates default to port 3000. Use --port 3000 unless you intentionally changed the app readiness port."));
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1613
1688
|
function serviceLogPath(name) {
|
|
1614
1689
|
return `/tmp/miosa-services/${name}.log`;
|
|
1615
1690
|
}
|
|
@@ -1638,74 +1713,85 @@ async function deploySandbox(localDir, opts) {
|
|
|
1638
1713
|
manifestPort(appManifest) ??
|
|
1639
1714
|
detection?.port ??
|
|
1640
1715
|
5173;
|
|
1716
|
+
warnOnTemplatePortMismatch(opts.template ?? appManifest?.template ?? null, port, opts);
|
|
1641
1717
|
const probePath = opts.probePath ?? manifestProbePath(appManifest) ?? "/";
|
|
1642
1718
|
let remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
|
|
1643
1719
|
const start = opts.start ??
|
|
1644
1720
|
manifestStartCommand(appManifest) ??
|
|
1645
1721
|
defaultStartCommand(detection?.framework ?? appManifest?.framework, port);
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1722
|
+
let sandboxId = opts.sandbox ?? null;
|
|
1723
|
+
try {
|
|
1724
|
+
if (!sandboxId) {
|
|
1725
|
+
deployStep(opts, "Creating sandbox");
|
|
1726
|
+
sandboxId = await createSandboxForDeploy(c, opts.template ?? appManifest?.template ?? "miosa-sandbox", opts.name, {
|
|
1649
1727
|
source: opts.source,
|
|
1650
1728
|
revision: opts.revision,
|
|
1651
1729
|
depth: opts.depth,
|
|
1652
|
-
})
|
|
1653
|
-
deployStep(opts, "Waiting for sandbox");
|
|
1654
|
-
await waitForSandboxRunning(c, sandboxId, Math.min(opts.timeout, 120));
|
|
1655
|
-
if (sourceBacked) {
|
|
1656
|
-
deployStep(opts, "Waiting for source import");
|
|
1657
|
-
appManifest = await readRemoteAppManifest(c, sandboxId, opts.timeout);
|
|
1658
|
-
remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
|
|
1659
|
-
}
|
|
1660
|
-
else {
|
|
1661
|
-
deployStep(opts, "Uploading files");
|
|
1662
|
-
const archivePath = createDeployArchive(sourceDir);
|
|
1663
|
-
const remoteArchive = `/tmp/miosa-deploy-${Date.now()}.tgz`;
|
|
1664
|
-
try {
|
|
1665
|
-
await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
|
|
1666
|
-
deployStep(opts, "Extracting workspace");
|
|
1667
|
-
await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(remoteWorkdir)} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteWorkdir)}`, "/");
|
|
1730
|
+
});
|
|
1668
1731
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1732
|
+
deployStep(opts, "Waiting for sandbox");
|
|
1733
|
+
await waitForSandboxRunning(c, sandboxId, Math.min(opts.timeout, 120));
|
|
1734
|
+
if (sourceBacked) {
|
|
1735
|
+
deployStep(opts, "Waiting for source import");
|
|
1736
|
+
appManifest = await readRemoteAppManifest(c, sandboxId, opts.timeout);
|
|
1737
|
+
remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
|
|
1671
1738
|
}
|
|
1739
|
+
else {
|
|
1740
|
+
deployStep(opts, "Uploading files");
|
|
1741
|
+
const archivePath = createDeployArchive(sourceDir);
|
|
1742
|
+
const remoteArchive = `/tmp/miosa-deploy-${Date.now()}.tgz`;
|
|
1743
|
+
try {
|
|
1744
|
+
await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
|
|
1745
|
+
deployStep(opts, "Extracting workspace");
|
|
1746
|
+
await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(remoteWorkdir)} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteWorkdir)}`, "/");
|
|
1747
|
+
}
|
|
1748
|
+
finally {
|
|
1749
|
+
fs.rmSync(archivePath, { force: true });
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
const resolvedPort = opts.port ?? opts.publishPort ?? manifestPort(appManifest) ?? port;
|
|
1753
|
+
const resolvedProbePath = opts.probePath ?? manifestProbePath(appManifest) ?? probePath;
|
|
1754
|
+
const resolvedStart = opts.start ?? manifestStartCommand(appManifest) ?? start;
|
|
1755
|
+
const installCommand = opts.install === false
|
|
1756
|
+
? null
|
|
1757
|
+
: (opts.installCommand ??
|
|
1758
|
+
(appManifest?.install === false ? null : appManifest?.install) ??
|
|
1759
|
+
(sourceBacked ? "npm install" : defaultInstallCommand(sourceDir)));
|
|
1760
|
+
if (installCommand) {
|
|
1761
|
+
deployStep(opts, `Installing dependencies: ${installCommand}`);
|
|
1762
|
+
await execSandbox(c, sandboxId, installCommand, remoteWorkdir, opts.timeout);
|
|
1763
|
+
}
|
|
1764
|
+
deployStep(opts, `Starting app on port ${resolvedPort}`);
|
|
1765
|
+
await execSandbox(c, sandboxId, `fuser -k ${resolvedPort}/tcp >/dev/null 2>&1 || true; nohup sh -lc ${shellQuote(resolvedStart)} > ${shellQuote(`/tmp/miosa-app-${resolvedPort}.log`)} 2>&1 & echo $!`, remoteWorkdir);
|
|
1766
|
+
deployStep(opts, "Checking internal app readiness");
|
|
1767
|
+
const internal = await waitForInternalHttp(c, sandboxId, resolvedPort, resolvedProbePath, Math.min(opts.timeout, 60));
|
|
1768
|
+
deployStep(opts, "Creating public preview route");
|
|
1769
|
+
const exposed = await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), { port: resolvedPort, title: "app preview" });
|
|
1770
|
+
const previewUrl = extractUrl(unwrap(exposed));
|
|
1771
|
+
if (!previewUrl) {
|
|
1772
|
+
throw new UserError("Sandbox expose did not return a preview URL.");
|
|
1773
|
+
}
|
|
1774
|
+
if (opts.wait)
|
|
1775
|
+
deployStep(opts, "Checking public preview readiness");
|
|
1776
|
+
const edge = opts.wait
|
|
1777
|
+
? await waitForPublicPreview(previewUrl, resolvedProbePath, opts.timeout)
|
|
1778
|
+
: { ok: false, status: null };
|
|
1779
|
+
return {
|
|
1780
|
+
sandbox_id: sandboxId,
|
|
1781
|
+
port: resolvedPort,
|
|
1782
|
+
preview_url: previewUrl,
|
|
1783
|
+
preview_ready: edge.ok,
|
|
1784
|
+
internal_status: internal.status,
|
|
1785
|
+
edge_status: edge.status,
|
|
1786
|
+
latency_ms: edge.latency_ms ?? null,
|
|
1787
|
+
};
|
|
1672
1788
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
: (opts.installCommand ??
|
|
1679
|
-
(appManifest?.install === false ? null : appManifest?.install) ??
|
|
1680
|
-
(sourceBacked ? "npm install" : defaultInstallCommand(sourceDir)));
|
|
1681
|
-
if (installCommand) {
|
|
1682
|
-
deployStep(opts, `Installing dependencies: ${installCommand}`);
|
|
1683
|
-
await execSandbox(c, sandboxId, installCommand, remoteWorkdir, opts.timeout);
|
|
1684
|
-
}
|
|
1685
|
-
deployStep(opts, `Starting app on port ${resolvedPort}`);
|
|
1686
|
-
await execSandbox(c, sandboxId, `fuser -k ${resolvedPort}/tcp >/dev/null 2>&1 || true; nohup sh -lc ${shellQuote(resolvedStart)} > ${shellQuote(`/tmp/miosa-app-${resolvedPort}.log`)} 2>&1 & echo $!`, remoteWorkdir);
|
|
1687
|
-
deployStep(opts, "Checking internal app readiness");
|
|
1688
|
-
const internal = await waitForInternalHttp(c, sandboxId, resolvedPort, resolvedProbePath, Math.min(opts.timeout, 60));
|
|
1689
|
-
deployStep(opts, "Creating public preview route");
|
|
1690
|
-
const exposed = await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), { port: resolvedPort, title: "app preview" });
|
|
1691
|
-
const previewUrl = extractUrl(unwrap(exposed));
|
|
1692
|
-
if (!previewUrl) {
|
|
1693
|
-
throw new UserError("Sandbox expose did not return a preview URL.");
|
|
1789
|
+
catch (err) {
|
|
1790
|
+
if (sandboxId) {
|
|
1791
|
+
throw new SandboxDeployPartialError(err, sandboxId, recoveryCommandForSandboxDeploy(sandboxId, opts, localDir));
|
|
1792
|
+
}
|
|
1793
|
+
throw err;
|
|
1694
1794
|
}
|
|
1695
|
-
if (opts.wait)
|
|
1696
|
-
deployStep(opts, "Checking public preview readiness");
|
|
1697
|
-
const edge = opts.wait
|
|
1698
|
-
? await waitForPublicPreview(previewUrl, resolvedProbePath, opts.timeout)
|
|
1699
|
-
: { ok: false, status: null };
|
|
1700
|
-
return {
|
|
1701
|
-
sandbox_id: sandboxId,
|
|
1702
|
-
port: resolvedPort,
|
|
1703
|
-
preview_url: previewUrl,
|
|
1704
|
-
preview_ready: edge.ok,
|
|
1705
|
-
internal_status: internal.status,
|
|
1706
|
-
edge_status: edge.status,
|
|
1707
|
-
latency_ms: edge.latency_ms ?? null,
|
|
1708
|
-
};
|
|
1709
1795
|
}
|
|
1710
1796
|
const NON_TERMINAL_DEPLOY_STATES = new Set([
|
|
1711
1797
|
"building",
|
|
@@ -1778,6 +1864,11 @@ async function publishSandbox(sandboxId, opts) {
|
|
|
1778
1864
|
body["domain"] = opts.domain;
|
|
1779
1865
|
if (opts.port != null)
|
|
1780
1866
|
body["port"] = opts.port;
|
|
1867
|
+
const deploymentType = opts.dockerDeploy
|
|
1868
|
+
? "docker-deploy"
|
|
1869
|
+
: opts.deploymentType;
|
|
1870
|
+
if (deploymentType)
|
|
1871
|
+
body["deployment_type"] = deploymentType;
|
|
1781
1872
|
const database = parsePublishDatabase(opts.database);
|
|
1782
1873
|
if (database !== undefined)
|
|
1783
1874
|
body["database"] = database;
|
|
@@ -1798,11 +1889,29 @@ async function publishSandbox(sandboxId, opts) {
|
|
|
1798
1889
|
extractUrl(deployment) ??
|
|
1799
1890
|
stringField(data, "url") ??
|
|
1800
1891
|
null;
|
|
1892
|
+
let deploymentProduct = stringField(response, "deployment_product") ??
|
|
1893
|
+
stringField(data, "deployment_product") ??
|
|
1894
|
+
stringField(deployment, "deployment_product") ??
|
|
1895
|
+
stringField(asRecord(deployment?.["metadata"]), "deployment_product") ??
|
|
1896
|
+
null;
|
|
1897
|
+
let dockerDeployHostId = stringField(response, "docker_deploy_host_id") ??
|
|
1898
|
+
stringField(data, "docker_deploy_host_id") ??
|
|
1899
|
+
stringField(deployment, "docker_deploy_host_id") ??
|
|
1900
|
+
stringField(asRecord(deployment?.["metadata"]), "docker_deploy_host_id") ??
|
|
1901
|
+
null;
|
|
1801
1902
|
if (opts.wait && deploymentId) {
|
|
1802
1903
|
deployStep(opts, "Waiting for durable deployment");
|
|
1803
1904
|
const waited = await waitForDeploymentReady(c, deploymentId, opts.timeout);
|
|
1804
1905
|
state = stringField(waited, "state") ?? state;
|
|
1805
1906
|
url = extractUrl(waited) ?? url;
|
|
1907
|
+
deploymentProduct =
|
|
1908
|
+
stringField(waited, "deployment_product") ??
|
|
1909
|
+
stringField(asRecord(waited["metadata"]), "deployment_product") ??
|
|
1910
|
+
deploymentProduct;
|
|
1911
|
+
dockerDeployHostId =
|
|
1912
|
+
stringField(waited, "docker_deploy_host_id") ??
|
|
1913
|
+
stringField(asRecord(waited["metadata"]), "docker_deploy_host_id") ??
|
|
1914
|
+
dockerDeployHostId;
|
|
1806
1915
|
response["state"] = state;
|
|
1807
1916
|
if (url)
|
|
1808
1917
|
response["url"] = url;
|
|
@@ -1828,6 +1937,8 @@ async function publishSandbox(sandboxId, opts) {
|
|
|
1828
1937
|
version_id: versionId,
|
|
1829
1938
|
url,
|
|
1830
1939
|
state,
|
|
1940
|
+
deployment_product: deploymentProduct,
|
|
1941
|
+
docker_deploy_host_id: dockerDeployHostId,
|
|
1831
1942
|
ready,
|
|
1832
1943
|
probe,
|
|
1833
1944
|
data: raw,
|
|
@@ -1926,6 +2037,188 @@ function renderDoctorReport(report) {
|
|
|
1926
2037
|
console.log();
|
|
1927
2038
|
}
|
|
1928
2039
|
}
|
|
2040
|
+
async function recoverSandboxDeploy(idOrName, opts) {
|
|
2041
|
+
const c = client();
|
|
2042
|
+
const resolved = await resolveSandboxForRecovery(c, idOrName);
|
|
2043
|
+
if (!resolved.sandbox) {
|
|
2044
|
+
return {
|
|
2045
|
+
query: idOrName,
|
|
2046
|
+
sandbox: null,
|
|
2047
|
+
sandbox_id: null,
|
|
2048
|
+
matched_by: null,
|
|
2049
|
+
exec_ok: false,
|
|
2050
|
+
app_port: opts.port ?? null,
|
|
2051
|
+
preview_ready: null,
|
|
2052
|
+
preview_url: null,
|
|
2053
|
+
doctor: null,
|
|
2054
|
+
recommendations: [
|
|
2055
|
+
"No matching sandbox was found. Run `miosa sandbox list --json` and retry with a sandbox ID.",
|
|
2056
|
+
],
|
|
2057
|
+
commands: {
|
|
2058
|
+
list: "miosa sandbox list --json",
|
|
2059
|
+
},
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
const sandbox = resolved.sandbox;
|
|
2063
|
+
const sandboxId = str(sandbox["id"]);
|
|
2064
|
+
const name = stringOrNull(sandbox["name"]);
|
|
2065
|
+
const template = stringOrNull(sandbox["template_id"]);
|
|
2066
|
+
const port = opts.port ?? suggestedRecoveryPort(template);
|
|
2067
|
+
const execOk = await checkSandboxExec(c, sandboxId);
|
|
2068
|
+
const doctor = port != null ? await safeDoctorSandbox(sandboxId, port, opts.probePath) : null;
|
|
2069
|
+
const previewUrl = stringOrNull(doctor?.["preview_url"]);
|
|
2070
|
+
const previewReady = doctor && typeof doctor["preview_ready"] === "boolean"
|
|
2071
|
+
? Boolean(doctor["preview_ready"])
|
|
2072
|
+
: null;
|
|
2073
|
+
const recommendations = buildRecoveryRecommendations({
|
|
2074
|
+
sandbox,
|
|
2075
|
+
execOk,
|
|
2076
|
+
port,
|
|
2077
|
+
previewReady,
|
|
2078
|
+
template,
|
|
2079
|
+
});
|
|
2080
|
+
return {
|
|
2081
|
+
query: idOrName,
|
|
2082
|
+
sandbox,
|
|
2083
|
+
sandbox_id: sandboxId,
|
|
2084
|
+
matched_by: resolved.matchedBy,
|
|
2085
|
+
exec_ok: execOk,
|
|
2086
|
+
app_port: port,
|
|
2087
|
+
preview_ready: previewReady,
|
|
2088
|
+
preview_url: previewUrl,
|
|
2089
|
+
doctor,
|
|
2090
|
+
recommendations,
|
|
2091
|
+
commands: recoveryCommands(sandboxId, name, port),
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
async function resolveSandboxForRecovery(c, idOrName) {
|
|
2095
|
+
try {
|
|
2096
|
+
const direct = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(idOrName)}`)));
|
|
2097
|
+
const sandbox = asRecord(direct);
|
|
2098
|
+
if (sandbox)
|
|
2099
|
+
return { sandbox, matchedBy: "id" };
|
|
2100
|
+
}
|
|
2101
|
+
catch {
|
|
2102
|
+
// Fall through to name lookup.
|
|
2103
|
+
}
|
|
2104
|
+
const list = unwrap(await c.apiGet(apiPath("/sandboxes")));
|
|
2105
|
+
const items = Array.isArray(list)
|
|
2106
|
+
? list
|
|
2107
|
+
: Array.isArray((asRecord(list) ?? {})["data"])
|
|
2108
|
+
? (asRecord(list) ?? {})["data"]
|
|
2109
|
+
: [];
|
|
2110
|
+
const matches = items
|
|
2111
|
+
.map(asRecord)
|
|
2112
|
+
.filter((item) => Boolean(item))
|
|
2113
|
+
.filter((item) => {
|
|
2114
|
+
const name = stringOrNull(item["name"]);
|
|
2115
|
+
const id = stringOrNull(item["id"]);
|
|
2116
|
+
return name === idOrName || id?.startsWith(idOrName);
|
|
2117
|
+
})
|
|
2118
|
+
.sort((a, b) => {
|
|
2119
|
+
const aTime = Date.parse(stringOrNull(a["created_at"]) ?? "") || 0;
|
|
2120
|
+
const bTime = Date.parse(stringOrNull(b["created_at"]) ?? "") || 0;
|
|
2121
|
+
return bTime - aTime;
|
|
2122
|
+
});
|
|
2123
|
+
return { sandbox: matches[0] ?? null, matchedBy: matches[0] ? "name" : null };
|
|
2124
|
+
}
|
|
2125
|
+
async function checkSandboxExec(c, sandboxId) {
|
|
2126
|
+
try {
|
|
2127
|
+
const result = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), {
|
|
2128
|
+
command: "pwd",
|
|
2129
|
+
}));
|
|
2130
|
+
const record = asRecord(result);
|
|
2131
|
+
return Number(record?.["exit_code"] ?? 0) === 0;
|
|
2132
|
+
}
|
|
2133
|
+
catch {
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
async function safeDoctorSandbox(sandboxId, port, probePath) {
|
|
2138
|
+
try {
|
|
2139
|
+
return await doctorSandbox(sandboxId, port, probePath);
|
|
2140
|
+
}
|
|
2141
|
+
catch (err) {
|
|
2142
|
+
return {
|
|
2143
|
+
preview_ready: false,
|
|
2144
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function suggestedRecoveryPort(template) {
|
|
2149
|
+
if (template === "nextjs" || template === "next-js")
|
|
2150
|
+
return 3000;
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
2153
|
+
function buildRecoveryRecommendations(input) {
|
|
2154
|
+
const recs = [];
|
|
2155
|
+
const state = stringOrNull(input.sandbox["state"]);
|
|
2156
|
+
const ready = Boolean(input.sandbox["ready"]);
|
|
2157
|
+
if (state !== "running") {
|
|
2158
|
+
recs.push(`Sandbox state is ${state ?? "unknown"}; wait or recreate before uploading files.`);
|
|
2159
|
+
}
|
|
2160
|
+
else if (!ready) {
|
|
2161
|
+
recs.push("Sandbox exists but is not fully ready; try exec/write-file before retrying deploy.");
|
|
2162
|
+
}
|
|
2163
|
+
if (!input.execOk) {
|
|
2164
|
+
recs.push("Exec health failed; retry later or recreate the sandbox.");
|
|
2165
|
+
}
|
|
2166
|
+
else {
|
|
2167
|
+
recs.push("Exec works; if upload/deploy failed, use write-file/patch plus service up.");
|
|
2168
|
+
}
|
|
2169
|
+
if (input.template === "nextjs" && input.port !== 3000) {
|
|
2170
|
+
recs.push("Next.js templates default to port 3000; use 3000 unless you intentionally reconfigured readiness.");
|
|
2171
|
+
}
|
|
2172
|
+
if (input.port != null && input.previewReady === false) {
|
|
2173
|
+
recs.push("Preview is not ready; start the app process, then run sandbox wait.");
|
|
2174
|
+
}
|
|
2175
|
+
if (input.previewReady === true) {
|
|
2176
|
+
recs.push("Preview is ready; publish the sandbox to a durable deployment.");
|
|
2177
|
+
}
|
|
2178
|
+
return recs;
|
|
2179
|
+
}
|
|
2180
|
+
function recoveryCommands(sandboxId, name, port) {
|
|
2181
|
+
const commands = {
|
|
2182
|
+
health: `miosa sandbox exec ${sandboxId} --json -- pwd`,
|
|
2183
|
+
files: `miosa sandbox write-file ${sandboxId} /workspace/app/page.jsx ./page.jsx --json`,
|
|
2184
|
+
};
|
|
2185
|
+
if (port != null) {
|
|
2186
|
+
commands.start = `miosa sandbox service up ${sandboxId} next --cwd /workspace --port ${port} --cmd "npm run dev -- --hostname 0.0.0.0 --port ${port}" --json`;
|
|
2187
|
+
commands.wait = `miosa sandbox wait ${sandboxId} --port ${port} --timeout 180 --json`;
|
|
2188
|
+
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`;
|
|
2189
|
+
}
|
|
2190
|
+
return commands;
|
|
2191
|
+
}
|
|
2192
|
+
function renderRecoverReport(report) {
|
|
2193
|
+
if (!report.sandbox_id) {
|
|
2194
|
+
console.log(chalk.red("No matching sandbox found."));
|
|
2195
|
+
console.log(` ${report.commands["list"]}`);
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
console.log(chalk.bold("Sandbox recovery"));
|
|
2199
|
+
console.log();
|
|
2200
|
+
console.log(` ${chalk.bold("Sandbox")} ${report.sandbox_id}`);
|
|
2201
|
+
console.log(` ${chalk.bold("Matched")} ${report.matched_by}`);
|
|
2202
|
+
console.log(` ${chalk.bold("Exec")} ${report.exec_ok ? chalk.green("ok") : chalk.red("failed")}`);
|
|
2203
|
+
if (report.app_port != null) {
|
|
2204
|
+
console.log(` ${chalk.bold("Port")} ${report.app_port}`);
|
|
2205
|
+
}
|
|
2206
|
+
if (report.preview_url) {
|
|
2207
|
+
console.log(` ${chalk.bold("Preview")} ${chalk.cyan(report.preview_url)}`);
|
|
2208
|
+
}
|
|
2209
|
+
if (report.preview_ready != null) {
|
|
2210
|
+
console.log(` ${chalk.bold("Ready")} ${report.preview_ready ? chalk.green("yes") : chalk.yellow("no")}`);
|
|
2211
|
+
}
|
|
2212
|
+
console.log();
|
|
2213
|
+
for (const rec of report.recommendations) {
|
|
2214
|
+
console.log(` - ${rec}`);
|
|
2215
|
+
}
|
|
2216
|
+
console.log();
|
|
2217
|
+
console.log(chalk.bold("Next commands"));
|
|
2218
|
+
for (const [label, command] of Object.entries(report.commands)) {
|
|
2219
|
+
console.log(` ${chalk.dim(label.padEnd(7))} ${command}`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
1929
2222
|
async function createSandboxForDeploy(c, template, name, source) {
|
|
1930
2223
|
const body = { template_id: template };
|
|
1931
2224
|
if (name)
|
|
@@ -2011,10 +2304,123 @@ function createDeployArchive(sourceDir) {
|
|
|
2011
2304
|
return archivePath;
|
|
2012
2305
|
}
|
|
2013
2306
|
async function uploadFileToSandbox(c, sandboxId, localPath, remotePath) {
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2307
|
+
return writeBytesToSandbox(c, sandboxId, remotePath, fs.readFileSync(localPath));
|
|
2308
|
+
}
|
|
2309
|
+
async function writeBytesToSandbox(c, sandboxId, remotePath, bytes) {
|
|
2310
|
+
try {
|
|
2311
|
+
return await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/files`), {
|
|
2312
|
+
path: remotePath,
|
|
2313
|
+
content: bytes.toString("base64"),
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
catch (err) {
|
|
2317
|
+
if (!shouldFallbackSandboxUpload(err))
|
|
2318
|
+
throw err;
|
|
2319
|
+
await writeBytesToSandboxViaExec(c, sandboxId, remotePath, bytes);
|
|
2320
|
+
return {
|
|
2321
|
+
data: {
|
|
2322
|
+
sandbox_id: sandboxId,
|
|
2323
|
+
path: remotePath,
|
|
2324
|
+
size: bytes.length,
|
|
2325
|
+
transport: "exec_chunked_fallback",
|
|
2326
|
+
},
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
async function writeBytesToSandboxViaExec(c, sandboxId, remotePath, bytes) {
|
|
2331
|
+
const base64 = bytes.toString("base64");
|
|
2332
|
+
const base64Path = `${remotePath}.b64`;
|
|
2333
|
+
const chunkSize = 48_000;
|
|
2334
|
+
await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(path.posix.dirname(remotePath))} && rm -f ${shellQuote(remotePath)} ${shellQuote(base64Path)}`, "/");
|
|
2335
|
+
for (let offset = 0; offset < base64.length; offset += chunkSize) {
|
|
2336
|
+
const chunk = base64.slice(offset, offset + chunkSize);
|
|
2337
|
+
await execSandbox(c, sandboxId, `printf '%s' ${shellQuote(chunk)} >> ${shellQuote(base64Path)}`, "/");
|
|
2338
|
+
}
|
|
2339
|
+
await execSandbox(c, sandboxId, `base64 -d ${shellQuote(base64Path)} > ${shellQuote(remotePath)} && rm -f ${shellQuote(base64Path)}`, "/");
|
|
2340
|
+
}
|
|
2341
|
+
function shouldFallbackSandboxUpload(err) {
|
|
2342
|
+
if (err instanceof NetworkError || err instanceof ServerError)
|
|
2343
|
+
return true;
|
|
2344
|
+
if (err instanceof ApiResponseError) {
|
|
2345
|
+
return (err.retryable ||
|
|
2346
|
+
/AGENT_UNAVAILABLE|SANDBOX_FILE_AGENT_UNAVAILABLE/i.test(err.code));
|
|
2347
|
+
}
|
|
2348
|
+
if (err instanceof Error) {
|
|
2349
|
+
return /fetch failed|ECONNRESET|HTTP 502|AGENT_UNAVAILABLE|other side closed|socket hang up/i.test(err.message);
|
|
2350
|
+
}
|
|
2351
|
+
return false;
|
|
2352
|
+
}
|
|
2353
|
+
function recoveryCommandForSandboxDeploy(sandboxId, opts, localDir) {
|
|
2354
|
+
const parts = [
|
|
2355
|
+
"miosa",
|
|
2356
|
+
"sandbox",
|
|
2357
|
+
"deploy",
|
|
2358
|
+
shellQuote(localDir),
|
|
2359
|
+
"--sandbox",
|
|
2360
|
+
shellQuote(sandboxId),
|
|
2361
|
+
];
|
|
2362
|
+
if (opts.port != null)
|
|
2363
|
+
parts.push("--port", String(opts.port));
|
|
2364
|
+
if (opts.publishPort != null)
|
|
2365
|
+
parts.push("--publish-port", String(opts.publishPort));
|
|
2366
|
+
if (opts.installCommand)
|
|
2367
|
+
parts.push("--install-command", shellQuote(opts.installCommand));
|
|
2368
|
+
if (opts.install === false)
|
|
2369
|
+
parts.push("--no-install");
|
|
2370
|
+
if (opts.start)
|
|
2371
|
+
parts.push("--start", shellQuote(opts.start));
|
|
2372
|
+
if (opts.wait)
|
|
2373
|
+
parts.push("--wait");
|
|
2374
|
+
if (opts.timeout != null)
|
|
2375
|
+
parts.push("--timeout", `${opts.timeout}s`);
|
|
2376
|
+
if (opts.probePath)
|
|
2377
|
+
parts.push("--probe-path", shellQuote(opts.probePath));
|
|
2378
|
+
return parts.join(" ");
|
|
2379
|
+
}
|
|
2380
|
+
function handleSandboxDeployError(err, opts) {
|
|
2381
|
+
if (err instanceof SandboxDeployPartialError) {
|
|
2382
|
+
const cause = err.causeError;
|
|
2383
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
2384
|
+
const retryable = cause instanceof ApiResponseError
|
|
2385
|
+
? cause.retryable
|
|
2386
|
+
: shouldFallbackSandboxUpload(cause);
|
|
2387
|
+
if (isJsonMode(opts)) {
|
|
2388
|
+
console.log(JSON.stringify({
|
|
2389
|
+
ok: false,
|
|
2390
|
+
error: {
|
|
2391
|
+
code: errorCodeForDeployCause(cause),
|
|
2392
|
+
message,
|
|
2393
|
+
retryable,
|
|
2394
|
+
...(cause instanceof MiosaError && cause.requestId
|
|
2395
|
+
? { request_id: cause.requestId }
|
|
2396
|
+
: {}),
|
|
2397
|
+
},
|
|
2398
|
+
partial_resource: {
|
|
2399
|
+
type: "sandbox",
|
|
2400
|
+
id: err.sandboxId,
|
|
2401
|
+
recovery_command: err.recoveryCommand,
|
|
2402
|
+
},
|
|
2403
|
+
}, null, 2));
|
|
2404
|
+
return process.exit(1);
|
|
2405
|
+
}
|
|
2406
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
2407
|
+
console.error(chalk.yellow(`Sandbox ${err.sandboxId} exists. Retry into it with:\n ${err.recoveryCommand}`));
|
|
2408
|
+
return process.exit(1);
|
|
2409
|
+
}
|
|
2410
|
+
handleError(err);
|
|
2411
|
+
}
|
|
2412
|
+
function errorCodeForDeployCause(err) {
|
|
2413
|
+
if (err instanceof ApiResponseError)
|
|
2414
|
+
return err.code;
|
|
2415
|
+
if (err instanceof NetworkError)
|
|
2416
|
+
return "NETWORK";
|
|
2417
|
+
if (err instanceof ServerError)
|
|
2418
|
+
return "SERVER";
|
|
2419
|
+
if (err instanceof MiosaError)
|
|
2420
|
+
return err.constructor.name.toUpperCase();
|
|
2421
|
+
if (err instanceof Error && /fetch failed|ECONNRESET/i.test(err.message))
|
|
2422
|
+
return "NETWORK";
|
|
2423
|
+
return "UNEXPECTED_ERROR";
|
|
2018
2424
|
}
|
|
2019
2425
|
async function uploadDirToSandbox(sandboxId, localDir, remoteDir, opts) {
|
|
2020
2426
|
const sourceDir = path.resolve(localDir);
|
|
@@ -2067,37 +2473,93 @@ async function execSandboxRaw(c, sandboxId, command, cwd, timeout) {
|
|
|
2067
2473
|
body["timeout"] = timeout;
|
|
2068
2474
|
return unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
|
|
2069
2475
|
}
|
|
2070
|
-
function
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
continue;
|
|
2081
|
-
const ss = ssLine.exec(line);
|
|
2082
|
-
if (ss && ss[2]) {
|
|
2083
|
-
const port = Number(ss[2]);
|
|
2084
|
-
if (!seen.has(port)) {
|
|
2085
|
-
seen.add(port);
|
|
2086
|
-
out.push({ port, address: ss[1] ?? "*" });
|
|
2087
|
-
}
|
|
2088
|
-
continue;
|
|
2089
|
-
}
|
|
2090
|
-
const proc = procLine.exec(line);
|
|
2091
|
-
if (proc && proc[3] === "0A" && proc[2]) {
|
|
2092
|
-
const port = parseInt(proc[2], 16);
|
|
2093
|
-
if (!Number.isNaN(port) && !seen.has(port)) {
|
|
2094
|
-
seen.add(port);
|
|
2095
|
-
out.push({ port, address: "*" });
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2476
|
+
function sandboxPortsFromResponse(raw) {
|
|
2477
|
+
const root = unwrap(raw);
|
|
2478
|
+
if (Array.isArray(root))
|
|
2479
|
+
return root;
|
|
2480
|
+
if (root && typeof root === "object") {
|
|
2481
|
+
const row = root;
|
|
2482
|
+
if (Array.isArray(row["ports"]))
|
|
2483
|
+
return row["ports"];
|
|
2484
|
+
if (Array.isArray(row["data"]))
|
|
2485
|
+
return row["data"];
|
|
2098
2486
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2487
|
+
return [];
|
|
2488
|
+
}
|
|
2489
|
+
function renderResourceMetrics(title, raw) {
|
|
2490
|
+
const root = unwrap(raw);
|
|
2491
|
+
if (!root || typeof root !== "object") {
|
|
2492
|
+
printValue(root, {});
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
const row = root;
|
|
2496
|
+
const current = row["current"] && typeof row["current"] === "object"
|
|
2497
|
+
? row["current"]
|
|
2498
|
+
: {};
|
|
2499
|
+
printBanner({ subtitle: title });
|
|
2500
|
+
console.log(kvPanel([
|
|
2501
|
+
{ label: "resource_id", value: String(row["resource_id"] ?? row["sandbox_id"] ?? "-") },
|
|
2502
|
+
{ label: "window", value: String(row["window"] ?? "1h") },
|
|
2503
|
+
{ label: "state", value: formatState(current["state"]) },
|
|
2504
|
+
{ label: "ready", value: formatBool(current["ready"]) },
|
|
2505
|
+
{ label: "cpu", value: formatMaybe(current["cpu_count"]) },
|
|
2506
|
+
{ label: "memory", value: formatMb(current["memory_mb"]) },
|
|
2507
|
+
{ label: "disk", value: formatMb(current["disk_size_mb"]) },
|
|
2508
|
+
{ label: "uptime", value: formatSeconds(current["uptime_sec"]) },
|
|
2509
|
+
{
|
|
2510
|
+
label: "timeout_remaining",
|
|
2511
|
+
value: formatSecondsOrAlwaysOn(current["timeout_remaining_sec"]),
|
|
2512
|
+
},
|
|
2513
|
+
{ label: "node", value: formatMaybe(current["node_id"]) },
|
|
2514
|
+
{ label: "ip", value: formatMaybe(current["ip_address"]) },
|
|
2515
|
+
{ label: "boot", value: formatMs(current["boot_ms"]) },
|
|
2516
|
+
{ label: "envd_ready", value: formatMs(current["envd_ready_ms"]) },
|
|
2517
|
+
]));
|
|
2518
|
+
}
|
|
2519
|
+
function formatState(value) {
|
|
2520
|
+
const state = String(value ?? "unknown");
|
|
2521
|
+
if (["running", "active", "healthy", "ready"].includes(state)) {
|
|
2522
|
+
return chalk.green(state);
|
|
2523
|
+
}
|
|
2524
|
+
if (["provisioning", "starting", "building", "pending"].includes(state)) {
|
|
2525
|
+
return chalk.yellow(state);
|
|
2526
|
+
}
|
|
2527
|
+
if (["failed", "error", "unhealthy"].includes(state)) {
|
|
2528
|
+
return chalk.red(state);
|
|
2529
|
+
}
|
|
2530
|
+
return state;
|
|
2531
|
+
}
|
|
2532
|
+
function formatBool(value) {
|
|
2533
|
+
if (value === true)
|
|
2534
|
+
return chalk.green("true");
|
|
2535
|
+
if (value === false)
|
|
2536
|
+
return chalk.red("false");
|
|
2537
|
+
return chalk.dim("-");
|
|
2538
|
+
}
|
|
2539
|
+
function formatMaybe(value) {
|
|
2540
|
+
if (value === null || value === undefined || value === "")
|
|
2541
|
+
return chalk.dim("-");
|
|
2542
|
+
return String(value);
|
|
2543
|
+
}
|
|
2544
|
+
function formatMb(value) {
|
|
2545
|
+
if (typeof value !== "number")
|
|
2546
|
+
return formatMaybe(value);
|
|
2547
|
+
return formatBytes(value * 1024 * 1024);
|
|
2548
|
+
}
|
|
2549
|
+
function formatMs(value) {
|
|
2550
|
+
if (typeof value !== "number")
|
|
2551
|
+
return formatMaybe(value);
|
|
2552
|
+
return `${value}ms`;
|
|
2553
|
+
}
|
|
2554
|
+
function formatSeconds(value) {
|
|
2555
|
+
if (typeof value !== "number")
|
|
2556
|
+
return formatMaybe(value);
|
|
2557
|
+
return formatDuration(value * 1000);
|
|
2558
|
+
}
|
|
2559
|
+
function formatSecondsOrAlwaysOn(value) {
|
|
2560
|
+
if (value === null || value === undefined)
|
|
2561
|
+
return chalk.dim("always-on/none");
|
|
2562
|
+
return formatSeconds(value);
|
|
2101
2563
|
}
|
|
2102
2564
|
// Stream exec output: run in background to a log file, poll-read new bytes
|
|
2103
2565
|
// until the process exits, then print the final exit code.
|
|
@@ -2387,6 +2849,15 @@ function backgroundCommand(command) {
|
|
|
2387
2849
|
const logPath = `/tmp/miosa-bg-${Date.now()}.log`;
|
|
2388
2850
|
return `nohup sh -lc ${shellQuote(command)} > ${shellQuote(logPath)} 2>&1 & echo $!`;
|
|
2389
2851
|
}
|
|
2852
|
+
function resolveSandboxCommand(words, opts) {
|
|
2853
|
+
const positional = words.length === 1 ? (words[0] ?? "") : joinCommandWords(words);
|
|
2854
|
+
const cmd = opts.cmd ?? opts.command ?? positional;
|
|
2855
|
+
if (!opts.shellCmd)
|
|
2856
|
+
return cmd;
|
|
2857
|
+
if (!cmd.trim())
|
|
2858
|
+
return opts.shellCmd;
|
|
2859
|
+
return `${opts.shellCmd} ${shellQuote(cmd)}`;
|
|
2860
|
+
}
|
|
2390
2861
|
function collectOption(value, previous) {
|
|
2391
2862
|
return [...previous, value];
|
|
2392
2863
|
}
|
|
@@ -2520,6 +2991,12 @@ function str(v) {
|
|
|
2520
2991
|
return chalk.dim("—");
|
|
2521
2992
|
return String(v);
|
|
2522
2993
|
}
|
|
2994
|
+
function stringOrNull(v) {
|
|
2995
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
2996
|
+
}
|
|
2997
|
+
function shellArg(value) {
|
|
2998
|
+
return `'${value.replace(/'/g, "'\"'\"'")}'`;
|
|
2999
|
+
}
|
|
2523
3000
|
/** Apply semantic color to a sandbox status string. */
|
|
2524
3001
|
function statusColor(s) {
|
|
2525
3002
|
const lower = s.toLowerCase().trim();
|