@miosa/cli 1.0.56 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +65 -42
  3. package/dist/app-advisor.d.ts +80 -0
  4. package/dist/app-advisor.d.ts.map +1 -0
  5. package/dist/app-advisor.js +486 -0
  6. package/dist/app-advisor.js.map +1 -0
  7. package/dist/bin/miosa.js +9 -0
  8. package/dist/bin/miosa.js.map +1 -1
  9. package/dist/commands/app.d.ts +3 -0
  10. package/dist/commands/app.d.ts.map +1 -0
  11. package/dist/commands/app.js +115 -0
  12. package/dist/commands/app.js.map +1 -0
  13. package/dist/commands/capabilities.d.ts.map +1 -1
  14. package/dist/commands/capabilities.js +232 -5
  15. package/dist/commands/capabilities.js.map +1 -1
  16. package/dist/commands/command-overview.d.ts +13 -0
  17. package/dist/commands/command-overview.d.ts.map +1 -0
  18. package/dist/commands/command-overview.js +50 -0
  19. package/dist/commands/command-overview.js.map +1 -0
  20. package/dist/commands/context.d.ts +3 -0
  21. package/dist/commands/context.d.ts.map +1 -0
  22. package/dist/commands/context.js +248 -0
  23. package/dist/commands/context.js.map +1 -0
  24. package/dist/commands/databases.d.ts.map +1 -1
  25. package/dist/commands/databases.js +85 -0
  26. package/dist/commands/databases.js.map +1 -1
  27. package/dist/commands/deploy.d.ts.map +1 -1
  28. package/dist/commands/deploy.js +109 -2
  29. package/dist/commands/deploy.js.map +1 -1
  30. package/dist/commands/docker-deploy.d.ts.map +1 -1
  31. package/dist/commands/docker-deploy.js +197 -127
  32. package/dist/commands/docker-deploy.js.map +1 -1
  33. package/dist/commands/enterprise-util.d.ts +3 -1
  34. package/dist/commands/enterprise-util.d.ts.map +1 -1
  35. package/dist/commands/enterprise-util.js +25 -9
  36. package/dist/commands/enterprise-util.js.map +1 -1
  37. package/dist/commands/logs.d.ts.map +1 -1
  38. package/dist/commands/logs.js +171 -17
  39. package/dist/commands/logs.js.map +1 -1
  40. package/dist/commands/new.js +2 -2
  41. package/dist/commands/new.js.map +1 -1
  42. package/dist/commands/sandbox.d.ts.map +1 -1
  43. package/dist/commands/sandbox.js +380 -40
  44. package/dist/commands/sandbox.js.map +1 -1
  45. package/dist/commands/util.d.ts.map +1 -1
  46. package/dist/commands/util.js +6 -1
  47. package/dist/commands/util.js.map +1 -1
  48. package/dist/config.d.ts +24 -1
  49. package/dist/config.d.ts.map +1 -1
  50. package/dist/config.js +87 -0
  51. package/dist/config.js.map +1 -1
  52. package/package.json +3 -2
@@ -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 = joinCommandWords(words);
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, words.join(" "), {
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 = joinCommandWords(words);
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, words.join(" "), {
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 c = client();
514
- const result = await execSandboxRaw(c, id, "ss -ltnH 2>/dev/null || cat /proc/net/tcp /proc/net/tcp6 2>/dev/null");
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(ports, null, 2));
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")
@@ -652,6 +686,7 @@ export function register(program) {
652
686
  .option("--run-command <cmd>", "Run command for dynamic/server deployments")
653
687
  .option("--docker-deploy", "Publish onto the workspace Docker Deploy runtime instead of standard app hosting")
654
688
  .option("--domain <domain>", "Custom domain to attach")
689
+ .option("--deployment-type <type>", "Deployment runtime type: miosa_deploy, docker_deploy, dynamic, static")
655
690
  .option("--database <mode>", "none, create:postgres, postgres, or existing:<db-id>")
656
691
  .option("--port <port>", "Runtime port for dynamic deployments", parseIntegerOption)
657
692
  .option("--wait", "Wait for the production URL to answer")
@@ -669,6 +704,10 @@ export function register(program) {
669
704
  console.log(` ${chalk.bold("App")} ${result.deployment_id ?? result.app_id ?? ""}`);
670
705
  if (result.release_id)
671
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)}`);
672
711
  if (result.url)
673
712
  console.log(` ${chalk.bold("URL")} ${chalk.cyan(String(result.url))}`);
674
713
  console.log(` ${chalk.bold("Ready")} ${result.ready ? chalk.green("yes") : chalk.yellow("pending")}`);
@@ -733,6 +772,7 @@ export function register(program) {
733
772
  .description("Manage named long-running processes inside a sandbox");
734
773
  service
735
774
  .command("start <sandbox-id> <name>")
775
+ .alias("up")
736
776
  .requiredOption("--cmd <command>", "Command to start")
737
777
  .option("--cwd <path>", "Working directory inside the sandbox", "/workspace")
738
778
  .option("--port <port>", "Port served by this service", parseIntegerOption)
@@ -901,6 +941,20 @@ export function register(program) {
901
941
  }
902
942
  renderDoctorReport(report);
903
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
+ }));
904
958
  // write-file — POST /sandboxes/:id/files with base64 content.
905
959
  // If <content-or-file> is an existing local path, reads the file bytes;
906
960
  // otherwise treats the argument as literal UTF-8 text.
@@ -1624,6 +1678,13 @@ async function createSandboxCommand(sandboxId, command, opts) {
1624
1678
  status: result["status"] ?? "running",
1625
1679
  };
1626
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
+ }
1627
1688
  function serviceLogPath(name) {
1628
1689
  return `/tmp/miosa-services/${name}.log`;
1629
1690
  }
@@ -1652,6 +1713,7 @@ async function deploySandbox(localDir, opts) {
1652
1713
  manifestPort(appManifest) ??
1653
1714
  detection?.port ??
1654
1715
  5173;
1716
+ warnOnTemplatePortMismatch(opts.template ?? appManifest?.template ?? null, port, opts);
1655
1717
  const probePath = opts.probePath ?? manifestProbePath(appManifest) ?? "/";
1656
1718
  let remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
1657
1719
  const start = opts.start ??
@@ -1802,6 +1864,11 @@ async function publishSandbox(sandboxId, opts) {
1802
1864
  body["domain"] = opts.domain;
1803
1865
  if (opts.port != null)
1804
1866
  body["port"] = opts.port;
1867
+ const deploymentType = opts.dockerDeploy
1868
+ ? "docker-deploy"
1869
+ : opts.deploymentType;
1870
+ if (deploymentType)
1871
+ body["deployment_type"] = deploymentType;
1805
1872
  const database = parsePublishDatabase(opts.database);
1806
1873
  if (database !== undefined)
1807
1874
  body["database"] = database;
@@ -1822,11 +1889,29 @@ async function publishSandbox(sandboxId, opts) {
1822
1889
  extractUrl(deployment) ??
1823
1890
  stringField(data, "url") ??
1824
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;
1825
1902
  if (opts.wait && deploymentId) {
1826
1903
  deployStep(opts, "Waiting for durable deployment");
1827
1904
  const waited = await waitForDeploymentReady(c, deploymentId, opts.timeout);
1828
1905
  state = stringField(waited, "state") ?? state;
1829
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;
1830
1915
  response["state"] = state;
1831
1916
  if (url)
1832
1917
  response["url"] = url;
@@ -1852,6 +1937,8 @@ async function publishSandbox(sandboxId, opts) {
1852
1937
  version_id: versionId,
1853
1938
  url,
1854
1939
  state,
1940
+ deployment_product: deploymentProduct,
1941
+ docker_deploy_host_id: dockerDeployHostId,
1855
1942
  ready,
1856
1943
  probe,
1857
1944
  data: raw,
@@ -1950,6 +2037,188 @@ function renderDoctorReport(report) {
1950
2037
  console.log();
1951
2038
  }
1952
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
+ }
1953
2222
  async function createSandboxForDeploy(c, template, name, source) {
1954
2223
  const body = { template_id: template };
1955
2224
  if (name)
@@ -2077,7 +2346,7 @@ function shouldFallbackSandboxUpload(err) {
2077
2346
  /AGENT_UNAVAILABLE|SANDBOX_FILE_AGENT_UNAVAILABLE/i.test(err.code));
2078
2347
  }
2079
2348
  if (err instanceof Error) {
2080
- return /fetch failed|ECONNRESET|HTTP 502|AGENT_UNAVAILABLE/i.test(err.message);
2349
+ return /fetch failed|ECONNRESET|HTTP 502|AGENT_UNAVAILABLE|other side closed|socket hang up/i.test(err.message);
2081
2350
  }
2082
2351
  return false;
2083
2352
  }
@@ -2204,37 +2473,93 @@ async function execSandboxRaw(c, sandboxId, command, cwd, timeout) {
2204
2473
  body["timeout"] = timeout;
2205
2474
  return unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
2206
2475
  }
2207
- function parseListeningPorts(stdout) {
2208
- const lines = stdout.split("\n").map((l) => l.trim());
2209
- const seen = new Set();
2210
- const out = [];
2211
- // ss -ltnH columns: State Recv-Q Send-Q Local-Address:Port Peer-Address:Port
2212
- const ssLine = /^LISTEN\s+\d+\s+\d+\s+(\S+):(\d+)\s+/;
2213
- // /proc/net/tcp: sl local_address rem_address st ... (hex)
2214
- const procLine = /^\d+:\s+([0-9A-Fa-f]+):([0-9A-Fa-f]+)\s+\S+\s+([0-9A-Fa-f]+)/;
2215
- for (const line of lines) {
2216
- if (!line)
2217
- continue;
2218
- const ss = ssLine.exec(line);
2219
- if (ss && ss[2]) {
2220
- const port = Number(ss[2]);
2221
- if (!seen.has(port)) {
2222
- seen.add(port);
2223
- out.push({ port, address: ss[1] ?? "*" });
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
- }
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"];
2486
+ }
2487
+ return [];
2488
+ }
2489
+ function renderResourceMetrics(title, raw) {
2490
+ const root = unwrap(raw);
2491
+ if (!root || typeof root !== "object") {
2492
+ printValue(root, {});
2493
+ return;
2235
2494
  }
2236
- out.sort((a, b) => a.port - b.port);
2237
- return out;
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);
2238
2563
  }
2239
2564
  // Stream exec output: run in background to a log file, poll-read new bytes
2240
2565
  // until the process exits, then print the final exit code.
@@ -2524,6 +2849,15 @@ function backgroundCommand(command) {
2524
2849
  const logPath = `/tmp/miosa-bg-${Date.now()}.log`;
2525
2850
  return `nohup sh -lc ${shellQuote(command)} > ${shellQuote(logPath)} 2>&1 & echo $!`;
2526
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
+ }
2527
2861
  function collectOption(value, previous) {
2528
2862
  return [...previous, value];
2529
2863
  }
@@ -2657,6 +2991,12 @@ function str(v) {
2657
2991
  return chalk.dim("—");
2658
2992
  return String(v);
2659
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
+ }
2660
3000
  /** Apply semantic color to a sandbox status string. */
2661
3001
  function statusColor(s) {
2662
3002
  const lower = s.toLowerCase().trim();