@miosa/cli 1.0.54 → 1.0.56

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.
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgDzC,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0iE/C"}
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsDzC,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8iE/C"}
@@ -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
@@ -553,21 +553,26 @@ export function register(program) {
553
553
  .option("--timeout <duration>", "Wait timeout, e.g. 180s or 3m", parseDurationSec, 180)
554
554
  .option("--probe-path <path>", "HTTP path to probe")
555
555
  .option("--json", "Output as JSON")
556
- .action((localDir = ".", opts) => runAction(async () => {
557
- const result = await deploySandbox(localDir, opts);
558
- if (isJsonMode(opts)) {
559
- console.log(JSON.stringify(result, null, 2));
560
- return;
556
+ .action(async (localDir = ".", opts) => {
557
+ try {
558
+ const result = await deploySandbox(localDir, opts);
559
+ if (isJsonMode(opts)) {
560
+ console.log(JSON.stringify(result, null, 2));
561
+ return;
562
+ }
563
+ console.log();
564
+ console.log(` ${chalk.bold("Sandbox")} ${result.sandbox_id}`);
565
+ console.log(` ${chalk.bold("Port")} ${result.port}`);
566
+ console.log(` ${chalk.bold("Preview")} ${chalk.cyan(result.preview_url)}`);
567
+ console.log(` ${chalk.bold("Ready")} ${result.preview_ready
568
+ ? chalk.green("yes")
569
+ : chalk.yellow("not verified")}`);
570
+ console.log();
561
571
  }
562
- console.log();
563
- console.log(` ${chalk.bold("Sandbox")} ${result.sandbox_id}`);
564
- console.log(` ${chalk.bold("Port")} ${result.port}`);
565
- console.log(` ${chalk.bold("Preview")} ${chalk.cyan(result.preview_url)}`);
566
- console.log(` ${chalk.bold("Ready")} ${result.preview_ready
567
- ? chalk.green("yes")
568
- : chalk.yellow("not verified")}`);
569
- console.log();
570
- }));
572
+ catch (err) {
573
+ handleSandboxDeployError(err, opts);
574
+ }
575
+ });
571
576
  sandbox
572
577
  .command("preview <sandbox-id>")
573
578
  .description("Expose a sandbox port and optionally wait for the public preview to answer")
@@ -908,8 +913,7 @@ export function register(program) {
908
913
  const contentBytes = fs.existsSync(contentArg)
909
914
  ? fs.readFileSync(contentArg)
910
915
  : Buffer.from(contentArg, "utf8");
911
- const base64 = contentBytes.toString("base64");
912
- const result = await fetchApiRaw(apiPath(`/sandboxes/${enc(id)}/files`), { path: remotePath, content: base64 });
916
+ const result = await writeBytesToSandbox(client(), id, remotePath, contentBytes);
913
917
  if (!isJsonMode(opts)) {
914
918
  console.log(chalk.green(`Written to ${remotePath}`));
915
919
  }
@@ -970,10 +974,8 @@ export function register(program) {
970
974
  console.error(chalk.red(`File not found: ${localPath}`));
971
975
  process.exit(1);
972
976
  }
973
- const data = fs.readFileSync(localPath);
974
- const base64 = data.toString("base64");
975
977
  const c = client();
976
- const result = await c.apiPost(apiPath(`/sandboxes/${enc(id)}/files`), { path: remotePath, content: base64 });
978
+ const result = await writeBytesToSandbox(c, id, remotePath, fs.readFileSync(localPath));
977
979
  if (isJsonMode(opts)) {
978
980
  printValue(result, opts);
979
981
  }
@@ -1499,6 +1501,18 @@ async function runSandboxPortForward(id, opts) {
1499
1501
  process.stderr.write("\n");
1500
1502
  server.close();
1501
1503
  }
1504
+ class SandboxDeployPartialError extends Error {
1505
+ causeError;
1506
+ sandboxId;
1507
+ recoveryCommand;
1508
+ constructor(causeError, sandboxId, recoveryCommand) {
1509
+ super(causeError instanceof Error ? causeError.message : String(causeError));
1510
+ this.causeError = causeError;
1511
+ this.sandboxId = sandboxId;
1512
+ this.recoveryCommand = recoveryCommand;
1513
+ this.name = "SandboxDeployPartialError";
1514
+ }
1515
+ }
1502
1516
  async function showSandboxWithPreview(sandboxId, port, probePath) {
1503
1517
  const c = client();
1504
1518
  const sandbox = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
@@ -1643,69 +1657,79 @@ async function deploySandbox(localDir, opts) {
1643
1657
  const start = opts.start ??
1644
1658
  manifestStartCommand(appManifest) ??
1645
1659
  defaultStartCommand(detection?.framework ?? appManifest?.framework, port);
1646
- const sandboxId = opts.sandbox ??
1647
- (deployStep(opts, "Creating sandbox"),
1648
- await createSandboxForDeploy(c, opts.template ?? appManifest?.template ?? "miosa-sandbox", opts.name, {
1660
+ let sandboxId = opts.sandbox ?? null;
1661
+ try {
1662
+ if (!sandboxId) {
1663
+ deployStep(opts, "Creating sandbox");
1664
+ sandboxId = await createSandboxForDeploy(c, opts.template ?? appManifest?.template ?? "miosa-sandbox", opts.name, {
1649
1665
  source: opts.source,
1650
1666
  revision: opts.revision,
1651
1667
  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)}`, "/");
1668
+ });
1668
1669
  }
1669
- finally {
1670
- fs.rmSync(archivePath, { force: true });
1670
+ deployStep(opts, "Waiting for sandbox");
1671
+ await waitForSandboxRunning(c, sandboxId, Math.min(opts.timeout, 120));
1672
+ if (sourceBacked) {
1673
+ deployStep(opts, "Waiting for source import");
1674
+ appManifest = await readRemoteAppManifest(c, sandboxId, opts.timeout);
1675
+ remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
1671
1676
  }
1677
+ else {
1678
+ deployStep(opts, "Uploading files");
1679
+ const archivePath = createDeployArchive(sourceDir);
1680
+ const remoteArchive = `/tmp/miosa-deploy-${Date.now()}.tgz`;
1681
+ try {
1682
+ await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
1683
+ deployStep(opts, "Extracting workspace");
1684
+ await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(remoteWorkdir)} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteWorkdir)}`, "/");
1685
+ }
1686
+ finally {
1687
+ fs.rmSync(archivePath, { force: true });
1688
+ }
1689
+ }
1690
+ const resolvedPort = opts.port ?? opts.publishPort ?? manifestPort(appManifest) ?? port;
1691
+ const resolvedProbePath = opts.probePath ?? manifestProbePath(appManifest) ?? probePath;
1692
+ const resolvedStart = opts.start ?? manifestStartCommand(appManifest) ?? start;
1693
+ const installCommand = opts.install === false
1694
+ ? null
1695
+ : (opts.installCommand ??
1696
+ (appManifest?.install === false ? null : appManifest?.install) ??
1697
+ (sourceBacked ? "npm install" : defaultInstallCommand(sourceDir)));
1698
+ if (installCommand) {
1699
+ deployStep(opts, `Installing dependencies: ${installCommand}`);
1700
+ await execSandbox(c, sandboxId, installCommand, remoteWorkdir, opts.timeout);
1701
+ }
1702
+ deployStep(opts, `Starting app on port ${resolvedPort}`);
1703
+ 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);
1704
+ deployStep(opts, "Checking internal app readiness");
1705
+ const internal = await waitForInternalHttp(c, sandboxId, resolvedPort, resolvedProbePath, Math.min(opts.timeout, 60));
1706
+ deployStep(opts, "Creating public preview route");
1707
+ const exposed = await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), { port: resolvedPort, title: "app preview" });
1708
+ const previewUrl = extractUrl(unwrap(exposed));
1709
+ if (!previewUrl) {
1710
+ throw new UserError("Sandbox expose did not return a preview URL.");
1711
+ }
1712
+ if (opts.wait)
1713
+ deployStep(opts, "Checking public preview readiness");
1714
+ const edge = opts.wait
1715
+ ? await waitForPublicPreview(previewUrl, resolvedProbePath, opts.timeout)
1716
+ : { ok: false, status: null };
1717
+ return {
1718
+ sandbox_id: sandboxId,
1719
+ port: resolvedPort,
1720
+ preview_url: previewUrl,
1721
+ preview_ready: edge.ok,
1722
+ internal_status: internal.status,
1723
+ edge_status: edge.status,
1724
+ latency_ms: edge.latency_ms ?? null,
1725
+ };
1672
1726
  }
1673
- const resolvedPort = opts.port ?? opts.publishPort ?? manifestPort(appManifest) ?? port;
1674
- const resolvedProbePath = opts.probePath ?? manifestProbePath(appManifest) ?? probePath;
1675
- const resolvedStart = opts.start ?? manifestStartCommand(appManifest) ?? start;
1676
- const installCommand = opts.install === false
1677
- ? null
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.");
1727
+ catch (err) {
1728
+ if (sandboxId) {
1729
+ throw new SandboxDeployPartialError(err, sandboxId, recoveryCommandForSandboxDeploy(sandboxId, opts, localDir));
1730
+ }
1731
+ throw err;
1694
1732
  }
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
1733
  }
1710
1734
  const NON_TERMINAL_DEPLOY_STATES = new Set([
1711
1735
  "building",
@@ -2011,10 +2035,123 @@ function createDeployArchive(sourceDir) {
2011
2035
  return archivePath;
2012
2036
  }
2013
2037
  async function uploadFileToSandbox(c, sandboxId, localPath, remotePath) {
2014
- await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/files`), {
2015
- path: remotePath,
2016
- content: fs.readFileSync(localPath).toString("base64"),
2017
- });
2038
+ return writeBytesToSandbox(c, sandboxId, remotePath, fs.readFileSync(localPath));
2039
+ }
2040
+ async function writeBytesToSandbox(c, sandboxId, remotePath, bytes) {
2041
+ try {
2042
+ return await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/files`), {
2043
+ path: remotePath,
2044
+ content: bytes.toString("base64"),
2045
+ });
2046
+ }
2047
+ catch (err) {
2048
+ if (!shouldFallbackSandboxUpload(err))
2049
+ throw err;
2050
+ await writeBytesToSandboxViaExec(c, sandboxId, remotePath, bytes);
2051
+ return {
2052
+ data: {
2053
+ sandbox_id: sandboxId,
2054
+ path: remotePath,
2055
+ size: bytes.length,
2056
+ transport: "exec_chunked_fallback",
2057
+ },
2058
+ };
2059
+ }
2060
+ }
2061
+ async function writeBytesToSandboxViaExec(c, sandboxId, remotePath, bytes) {
2062
+ const base64 = bytes.toString("base64");
2063
+ const base64Path = `${remotePath}.b64`;
2064
+ const chunkSize = 48_000;
2065
+ await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(path.posix.dirname(remotePath))} && rm -f ${shellQuote(remotePath)} ${shellQuote(base64Path)}`, "/");
2066
+ for (let offset = 0; offset < base64.length; offset += chunkSize) {
2067
+ const chunk = base64.slice(offset, offset + chunkSize);
2068
+ await execSandbox(c, sandboxId, `printf '%s' ${shellQuote(chunk)} >> ${shellQuote(base64Path)}`, "/");
2069
+ }
2070
+ await execSandbox(c, sandboxId, `base64 -d ${shellQuote(base64Path)} > ${shellQuote(remotePath)} && rm -f ${shellQuote(base64Path)}`, "/");
2071
+ }
2072
+ function shouldFallbackSandboxUpload(err) {
2073
+ if (err instanceof NetworkError || err instanceof ServerError)
2074
+ return true;
2075
+ if (err instanceof ApiResponseError) {
2076
+ return (err.retryable ||
2077
+ /AGENT_UNAVAILABLE|SANDBOX_FILE_AGENT_UNAVAILABLE/i.test(err.code));
2078
+ }
2079
+ if (err instanceof Error) {
2080
+ return /fetch failed|ECONNRESET|HTTP 502|AGENT_UNAVAILABLE/i.test(err.message);
2081
+ }
2082
+ return false;
2083
+ }
2084
+ function recoveryCommandForSandboxDeploy(sandboxId, opts, localDir) {
2085
+ const parts = [
2086
+ "miosa",
2087
+ "sandbox",
2088
+ "deploy",
2089
+ shellQuote(localDir),
2090
+ "--sandbox",
2091
+ shellQuote(sandboxId),
2092
+ ];
2093
+ if (opts.port != null)
2094
+ parts.push("--port", String(opts.port));
2095
+ if (opts.publishPort != null)
2096
+ parts.push("--publish-port", String(opts.publishPort));
2097
+ if (opts.installCommand)
2098
+ parts.push("--install-command", shellQuote(opts.installCommand));
2099
+ if (opts.install === false)
2100
+ parts.push("--no-install");
2101
+ if (opts.start)
2102
+ parts.push("--start", shellQuote(opts.start));
2103
+ if (opts.wait)
2104
+ parts.push("--wait");
2105
+ if (opts.timeout != null)
2106
+ parts.push("--timeout", `${opts.timeout}s`);
2107
+ if (opts.probePath)
2108
+ parts.push("--probe-path", shellQuote(opts.probePath));
2109
+ return parts.join(" ");
2110
+ }
2111
+ function handleSandboxDeployError(err, opts) {
2112
+ if (err instanceof SandboxDeployPartialError) {
2113
+ const cause = err.causeError;
2114
+ const message = cause instanceof Error ? cause.message : String(cause);
2115
+ const retryable = cause instanceof ApiResponseError
2116
+ ? cause.retryable
2117
+ : shouldFallbackSandboxUpload(cause);
2118
+ if (isJsonMode(opts)) {
2119
+ console.log(JSON.stringify({
2120
+ ok: false,
2121
+ error: {
2122
+ code: errorCodeForDeployCause(cause),
2123
+ message,
2124
+ retryable,
2125
+ ...(cause instanceof MiosaError && cause.requestId
2126
+ ? { request_id: cause.requestId }
2127
+ : {}),
2128
+ },
2129
+ partial_resource: {
2130
+ type: "sandbox",
2131
+ id: err.sandboxId,
2132
+ recovery_command: err.recoveryCommand,
2133
+ },
2134
+ }, null, 2));
2135
+ return process.exit(1);
2136
+ }
2137
+ console.error(chalk.red(`Error: ${message}`));
2138
+ console.error(chalk.yellow(`Sandbox ${err.sandboxId} exists. Retry into it with:\n ${err.recoveryCommand}`));
2139
+ return process.exit(1);
2140
+ }
2141
+ handleError(err);
2142
+ }
2143
+ function errorCodeForDeployCause(err) {
2144
+ if (err instanceof ApiResponseError)
2145
+ return err.code;
2146
+ if (err instanceof NetworkError)
2147
+ return "NETWORK";
2148
+ if (err instanceof ServerError)
2149
+ return "SERVER";
2150
+ if (err instanceof MiosaError)
2151
+ return err.constructor.name.toUpperCase();
2152
+ if (err instanceof Error && /fetch failed|ECONNRESET/i.test(err.message))
2153
+ return "NETWORK";
2154
+ return "UNEXPECTED_ERROR";
2018
2155
  }
2019
2156
  async function uploadDirToSandbox(sandboxId, localDir, remoteDir, opts) {
2020
2157
  const sourceDir = path.resolve(localDir);