@miosa/cli 1.0.94 → 1.0.95

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 (48) hide show
  1. package/README.md +85 -0
  2. package/dist/app-manifest.d.ts +40 -0
  3. package/dist/app-manifest.d.ts.map +1 -1
  4. package/dist/app-manifest.js +168 -46
  5. package/dist/app-manifest.js.map +1 -1
  6. package/dist/commands/completion.d.ts.map +1 -1
  7. package/dist/commands/completion.js +2 -0
  8. package/dist/commands/completion.js.map +1 -1
  9. package/dist/commands/context.d.ts.map +1 -1
  10. package/dist/commands/context.js +2 -19
  11. package/dist/commands/context.js.map +1 -1
  12. package/dist/commands/deploy.d.ts.map +1 -1
  13. package/dist/commands/deploy.js +31 -11
  14. package/dist/commands/deploy.js.map +1 -1
  15. package/dist/commands/env-input.d.ts +26 -0
  16. package/dist/commands/env-input.d.ts.map +1 -0
  17. package/dist/commands/env-input.js +68 -0
  18. package/dist/commands/env-input.js.map +1 -0
  19. package/dist/commands/env.d.ts.map +1 -1
  20. package/dist/commands/env.js +15 -3
  21. package/dist/commands/env.js.map +1 -1
  22. package/dist/commands/sandbox-delete-guard.d.ts +4 -0
  23. package/dist/commands/sandbox-delete-guard.d.ts.map +1 -0
  24. package/dist/commands/sandbox-delete-guard.js +32 -0
  25. package/dist/commands/sandbox-delete-guard.js.map +1 -0
  26. package/dist/commands/sandbox-dev.d.ts +20 -0
  27. package/dist/commands/sandbox-dev.d.ts.map +1 -0
  28. package/dist/commands/sandbox-dev.js +583 -0
  29. package/dist/commands/sandbox-dev.js.map +1 -0
  30. package/dist/commands/sandbox.d.ts +5 -0
  31. package/dist/commands/sandbox.d.ts.map +1 -1
  32. package/dist/commands/sandbox.js +163 -65
  33. package/dist/commands/sandbox.js.map +1 -1
  34. package/dist/commands/tenant.d.ts.map +1 -1
  35. package/dist/commands/tenant.js +6 -5
  36. package/dist/commands/tenant.js.map +1 -1
  37. package/dist/commands/util.d.ts.map +1 -1
  38. package/dist/commands/util.js +28 -6
  39. package/dist/commands/util.js.map +1 -1
  40. package/dist/config.d.ts +2 -0
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/config.js +17 -0
  43. package/dist/config.js.map +1 -1
  44. package/dist/errors.d.ts +1 -1
  45. package/dist/errors.d.ts.map +1 -1
  46. package/dist/errors.js +3 -3
  47. package/dist/errors.js.map +1 -1
  48. package/package.json +1 -1
@@ -11,12 +11,15 @@ import { loadAppManifest, manifestPort, manifestProbePath, manifestStartCommand,
11
11
  import { detectFramework } from "../framework-detector.js";
12
12
  import { addDataOption, client, apiPath, deleteAndPrint, enc, getAndPrint, postAndPrint, printValue, runAction, unwrap, } from "./enterprise-util.js";
13
13
  import { loadConfig } from "../config.js";
14
+ import { assertDeletableRemoteDir } from "./sandbox-delete-guard.js";
15
+ import { ENV_FILE_OPTION_HELP, ENV_INLINE_SHELL_WARNING, ENV_STDIN_OPTION_HELP, resolveEnvInputs, } from "./env-input.js";
14
16
  import { handleError, isJsonMode } from "./util.js";
15
17
  import { renderTable } from "../ui/table.js";
16
18
  import { formatDuration, hintBlock, icon, kvPanel, printBanner, printElapsed, } from "../ui/render.js";
17
19
  import { formatBytes } from "../ui/progress.js";
18
- import { ApiResponseError, MiosaError, NetworkError, ServerError, UserError, } from "../errors.js";
20
+ import { ApiResponseError, MiosaError, NetworkError, ServerError, UserError, mapHttpError, } from "../errors.js";
19
21
  import { EXIT_USER_ERROR } from "../types.js";
22
+ import { registerSandboxDevCommands, runFullSandboxDoctor, } from "./sandbox-dev.js";
20
23
  const DEFAULT_INTERACTIVE_SANDBOX_TIMEOUT_SEC = 3_600;
21
24
  const DEFAULT_CREATE_WAIT_TIMEOUT_SEC = 120;
22
25
  const EXPIRING_SANDBOX_THRESHOLD_SEC = 5 * 60;
@@ -30,6 +33,7 @@ export function register(program) {
30
33
  .command("sandbox")
31
34
  .alias("sandboxes")
32
35
  .description("Manage Sandboxes — lightweight code-only Computers (Firecracker microVMs without a desktop)");
36
+ registerSandboxDevCommands(sandbox);
33
37
  // list
34
38
  sandbox
35
39
  .command("list")
@@ -182,6 +186,9 @@ export function register(program) {
182
186
  .option("--depth <n>", "Git clone depth when --source git: is used", parseIntegerOption)
183
187
  .option("--snapshot <id>", "Create from a sandbox snapshot")
184
188
  .option("--workspace <id-or-slug>", "Workspace ID/slug")
189
+ .option("--external-workspace <id>", "White-label workspace/customer ID for billing attribution")
190
+ .option("--external-user <id>", "White-label user ID for billing attribution")
191
+ .option("--external-project <id>", "White-label project ID for billing attribution")
185
192
  .option("--agent-profile <id>", "Agent runtime profile ID to mount into the sandbox")
186
193
  .option("--skip-agent-profile", "Do not apply the tenant/workspace default agent runtime profile")
187
194
  .option("--network-policy <policy>", "Network policy: allow-all or deny-all")
@@ -192,6 +199,12 @@ export function register(program) {
192
199
  .option("--non-persistent", "Discard filesystem state on timeout instead of pausing for resume")
193
200
  .option("--auto-start", "Seed and start the template app after the sandbox reaches running"))
194
201
  .option("--json", "Output as JSON")
202
+ .addHelpText("after", `
203
+ Note:
204
+ White-label preview domains require external attribution at creation.
205
+ Pass --external-workspace / --external-user / --external-project when the
206
+ sandbox will be exposed on a white-label preview domain.
207
+ `)
195
208
  .action((opts) => runAction(async () => {
196
209
  const t0 = Date.now();
197
210
  const json = !!isJsonMode(opts) || process.env["MIOSA_JSON"] === "1";
@@ -236,6 +249,15 @@ export function register(program) {
236
249
  process.env["MIOSA_WORKSPACE"];
237
250
  if (workspace)
238
251
  body["workspace_id"] = workspace;
252
+ if (opts.externalWorkspace) {
253
+ body["external_workspace_id"] = opts.externalWorkspace;
254
+ }
255
+ if (opts.externalUser) {
256
+ body["external_user_id"] = opts.externalUser;
257
+ }
258
+ if (opts.externalProject) {
259
+ body["external_project_id"] = opts.externalProject;
260
+ }
239
261
  const networkPolicy = buildNetworkPolicy(opts);
240
262
  if (networkPolicy) {
241
263
  body["metadata"] = {
@@ -385,7 +407,9 @@ export function register(program) {
385
407
  .option("--connector <uid>", "MIOSA Connect connector UID to preflight before running the agent")
386
408
  .option("--preflight", "Verify the Sandbox has the requested provider connector before exec")
387
409
  .option("--cwd <path>", "Working directory inside the Sandbox")
388
- .option("--env <KEY=VALUE>", "Environment variable for the run. Repeatable.", collectOption, [])
410
+ .option("--env <KEY=VALUE>", `Environment variable for the Agent Run. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
411
+ .option("--env-file <path>", ENV_FILE_OPTION_HELP)
412
+ .option("--env-stdin", ENV_STDIN_OPTION_HELP)
389
413
  .option("--agent-profile <id>", "Agent runtime profile ID")
390
414
  .option("--skip-agent-profile", "Do not apply the default agent runtime profile")
391
415
  .option("--external-workspace <id>", "White-label workspace/customer ID for billing attribution")
@@ -423,8 +447,9 @@ export function register(program) {
423
447
  body["model"] = opts.model;
424
448
  if (opts.cwd)
425
449
  body["cwd"] = opts.cwd;
426
- if (opts.env && opts.env.length > 0) {
427
- body["env"] = parseEnvPairs(opts.env);
450
+ const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
451
+ if (Object.keys(env).length > 0) {
452
+ body["env"] = env;
428
453
  }
429
454
  if (opts.agentProfile) {
430
455
  body["agent_runtime_profile_id"] = opts.agentProfile;
@@ -479,7 +504,9 @@ export function register(program) {
479
504
  .option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
480
505
  .option("--command <command>", "Alias for --cmd")
481
506
  .option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
482
- .option("--env <pair>", "Environment variable KEY=VALUE. Repeatable.", collectOption, [])
507
+ .option("--env <pair>", `Environment variable KEY=VALUE. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
508
+ .option("--env-file <path>", ENV_FILE_OPTION_HELP)
509
+ .option("--env-stdin", ENV_STDIN_OPTION_HELP)
483
510
  .option("--background", "Start the command in the background and return immediately")
484
511
  .option("--detached", "Create a durable backend command and return command_id immediately")
485
512
  .option("--follow", "Stream command output until it exits (alias --stream)")
@@ -508,7 +535,7 @@ export function register(program) {
508
535
  body["cwd"] = cwd;
509
536
  body["dir"] = cwd;
510
537
  }
511
- const env = parseEnvPairs(opts.env ?? []);
538
+ const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
512
539
  if (opts.detached) {
513
540
  const result = await createSandboxCommand(id, cmd, {
514
541
  cwd,
@@ -547,7 +574,9 @@ export function register(program) {
547
574
  .option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
548
575
  .option("--command <command>", "Alias for --cmd")
549
576
  .option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
550
- .option("--env <pair>", "Environment variable KEY=VALUE. Repeatable.", collectOption, [])
577
+ .option("--env <pair>", `Environment variable KEY=VALUE. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
578
+ .option("--env-file <path>", ENV_FILE_OPTION_HELP)
579
+ .option("--env-stdin", ENV_STDIN_OPTION_HELP)
551
580
  .option("--background", "Start the command in the background and return immediately")
552
581
  .option("--detached", "Create a durable backend command and return command_id immediately")
553
582
  .option("--follow", "Stream command output until it exits (alias --stream)")
@@ -576,7 +605,7 @@ export function register(program) {
576
605
  body["cwd"] = cwd;
577
606
  body["dir"] = cwd;
578
607
  }
579
- const env = parseEnvPairs(opts.env ?? []);
608
+ const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
580
609
  if (opts.detached) {
581
610
  const result = await createSandboxCommand(id, cmd, {
582
611
  cwd,
@@ -963,11 +992,17 @@ export function register(program) {
963
992
  ]);
964
993
  }));
965
994
  env
966
- .command("set <sandbox-id> <pairs...>")
967
- .description("Set encrypted sandbox env vars as KEY=VALUE")
995
+ .command("set <sandbox-id> [pairs...]")
996
+ .description(`Set encrypted sandbox env vars as KEY=VALUE. ${ENV_INLINE_SHELL_WARNING}`)
997
+ .option("--env-file <path>", ENV_FILE_OPTION_HELP)
998
+ .option("--env-stdin", ENV_STDIN_OPTION_HELP)
968
999
  .option("--json", "Output as JSON")
969
1000
  .action((id, pairs, opts) => runAction(async () => {
970
- const vars = Object.entries(parseEnvPairs(pairs)).map(([key, value]) => ({
1001
+ const values = await resolveEnvInputs(parseEnvPairs(pairs ?? []), opts);
1002
+ if (Object.keys(values).length === 0) {
1003
+ throw new UserError("No env vars provided.", "Pass KEY=VALUE pairs, --env-file <path>, or --env-stdin.");
1004
+ }
1005
+ const vars = Object.entries(values).map(([key, value]) => ({
971
1006
  key,
972
1007
  value,
973
1008
  }));
@@ -1024,12 +1059,34 @@ export function register(program) {
1024
1059
  console.log(chalk.green(`Attached database ${databaseId} to sandbox ${id}.`));
1025
1060
  }));
1026
1061
  sandbox
1027
- .command("doctor <sandbox-id>")
1062
+ .command("doctor [sandbox-id]")
1028
1063
  .description("Diagnose sandbox app readiness across sandbox state, internal HTTP, public route, and TLS/edge reachability")
1029
- .requiredOption("--port <port>", "Port inside the sandbox to check", parseIntegerOption)
1064
+ .option("--port <port>", "Port inside the sandbox to check", parseIntegerOption)
1030
1065
  .option("--probe-path <path>", "HTTP path to probe", "/")
1066
+ .option("--full", "Inspect the complete canonical developer contract")
1067
+ .option("--dir <path>", "Project directory for --full", ".")
1031
1068
  .option("--json", "Output as JSON")
1032
1069
  .action((id, opts) => runAction(async () => {
1070
+ if (opts.full) {
1071
+ const report = await runFullSandboxDoctor(opts.dir, id);
1072
+ if (!report.ok)
1073
+ process.exitCode = 1;
1074
+ if (isJsonMode(opts)) {
1075
+ console.log(JSON.stringify(report, null, 2));
1076
+ return;
1077
+ }
1078
+ console.log();
1079
+ console.log(chalk.bold("Sandbox Doctor Full"));
1080
+ console.log();
1081
+ for (const check of report.checks) {
1082
+ console.log(` ${check.ok ? chalk.green("ok") : chalk.red("fail")} ${check.id}: ${check.message}`);
1083
+ }
1084
+ console.log();
1085
+ return;
1086
+ }
1087
+ if (!id || opts.port == null) {
1088
+ throw new UserError("Sandbox ID and --port are required unless --full is used.", "Use miosa sandbox doctor <sandbox-id> --port <port>, or miosa sandbox doctor --full.");
1089
+ }
1033
1090
  const report = await doctorSandbox(id, opts.port, opts.probePath);
1034
1091
  if (isJsonMode(opts)) {
1035
1092
  console.log(JSON.stringify(report, null, 2));
@@ -1082,7 +1139,7 @@ export function register(program) {
1082
1139
  .option("--json", "Output raw JSON response")
1083
1140
  .action(async (id, remotePath, opts) => {
1084
1141
  try {
1085
- const result = await fetchApiRaw(apiPath(`/sandboxes/${enc(id)}/files/read?path=${enc(remotePath)}`));
1142
+ const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/files/read?path=${enc(remotePath)}`));
1086
1143
  if (isJsonMode(opts)) {
1087
1144
  console.log(JSON.stringify(typeof result === "string" ? { content: result } : result, null, 2));
1088
1145
  return;
@@ -1142,11 +1199,14 @@ export function register(program) {
1142
1199
  .command("upload-dir <sandbox-id> <local-dir> <remote-dir>")
1143
1200
  .description("Upload a local directory into a Sandbox")
1144
1201
  .option("--delete", "Delete the remote directory contents before extracting")
1202
+ .option("--force", "Skip the --delete confirmation prompt")
1203
+ .option("--yes", "Alias for --force")
1145
1204
  .option("--json", "Output as JSON")
1146
1205
  .action(async (id, localDir, remoteDir, opts) => {
1147
1206
  try {
1148
1207
  const result = await uploadDirToSandbox(id, localDir, remoteDir, {
1149
1208
  delete: !!opts.delete,
1209
+ force: !!(opts.force || opts.yes),
1150
1210
  });
1151
1211
  if (isJsonMode(opts)) {
1152
1212
  console.log(JSON.stringify(result, null, 2));
@@ -1163,11 +1223,14 @@ export function register(program) {
1163
1223
  .description("Sync a local directory into a sandbox")
1164
1224
  .requiredOption("--sandbox <id>", "Sandbox ID")
1165
1225
  .option("--delete", "Delete the remote directory contents before extracting")
1226
+ .option("--force", "Skip the --delete confirmation prompt")
1227
+ .option("--yes", "Alias for --force")
1166
1228
  .option("--json", "Output as JSON")
1167
1229
  .action(async (localDir, remoteDir, opts) => {
1168
1230
  try {
1169
1231
  const result = await uploadDirToSandbox(opts.sandbox, localDir, remoteDir, {
1170
1232
  delete: !!opts.delete,
1233
+ force: !!(opts.force || opts.yes),
1171
1234
  });
1172
1235
  if (isJsonMode(opts)) {
1173
1236
  console.log(JSON.stringify(result, null, 2));
@@ -1184,6 +1247,8 @@ export function register(program) {
1184
1247
  .alias("copy")
1185
1248
  .description("Copy a local file or directory into a sandbox, e.g. ./app/. sbx_123:/workspace")
1186
1249
  .option("--delete", "Delete the remote directory contents before extracting directories")
1250
+ .option("--force", "Skip the --delete confirmation prompt")
1251
+ .option("--yes", "Alias for --force")
1187
1252
  .option("--json", "Output as JSON")
1188
1253
  .action(async (source, target, opts) => {
1189
1254
  try {
@@ -1204,7 +1269,10 @@ export function register(program) {
1204
1269
  const local = source.endsWith("/.") ? source.slice(0, -2) : source;
1205
1270
  const stat = fs.statSync(local);
1206
1271
  if (stat.isDirectory()) {
1207
- const result = await uploadDirToSandbox(parsed.sandboxId, local, parsed.remotePath, { delete: !!opts.delete });
1272
+ const result = await uploadDirToSandbox(parsed.sandboxId, local, parsed.remotePath, {
1273
+ delete: !!opts.delete,
1274
+ force: !!(opts.force || opts.yes),
1275
+ });
1208
1276
  if (isJsonMode(opts)) {
1209
1277
  console.log(JSON.stringify(result, null, 2));
1210
1278
  return;
@@ -1314,7 +1382,10 @@ export function register(program) {
1314
1382
  console.log(kvPanel([
1315
1383
  { label: "Export", value: chalk.bold(str(exportData["id"])) },
1316
1384
  { label: "Sandbox", value: id },
1317
- { label: "Status", value: statusColor(str(exportData["status"])) },
1385
+ {
1386
+ label: "Status",
1387
+ value: statusColor(str(exportData["status"])),
1388
+ },
1318
1389
  {
1319
1390
  label: "Archive",
1320
1391
  value: str(exportData["archive_download_url"]),
@@ -1555,7 +1626,7 @@ function normalizeConnectorMode(value) {
1555
1626
  // 4. Spawn `ssh` pointing at localhost:<port> with the key and user.
1556
1627
  // 5. On ssh exit, close the TCP server and exit.
1557
1628
  const SANDBOX_KEY_PATH = path.join(os.homedir(), ".ssh", "miosa_sandbox_ed25519");
1558
- async function ensureSandboxSshKey(id, apiKey, endpoint) {
1629
+ async function ensureSandboxSshKey(id, config) {
1559
1630
  if (!fs.existsSync(SANDBOX_KEY_PATH)) {
1560
1631
  console.log(chalk.dim("Generating SSH keypair for MIOSA sandbox access..."));
1561
1632
  fs.mkdirSync(path.join(os.homedir(), ".ssh"), { recursive: true });
@@ -1583,22 +1654,54 @@ async function ensureSandboxSshKey(id, apiKey, endpoint) {
1583
1654
  // already exist from a previous sandbox, but a fresh sandbox still needs it
1584
1655
  // installed in authorized_keys before SSH auth can succeed.
1585
1656
  const pubKey = fs.readFileSync(`${SANDBOX_KEY_PATH}.pub`, "utf8").trim();
1586
- const base = endpoint.replace(/\/$/, "");
1587
- const res = await fetch(`${base}/api/v1/sandboxes/${encodeURIComponent(id)}/ssh-keys`, {
1657
+ const endpoint = config.endpoint.replace(/\/$/, "");
1658
+ const response = await fetch(`${endpoint}${apiPath(`/sandboxes/${encodeURIComponent(id)}/ssh-keys`)}`, {
1588
1659
  method: "POST",
1589
1660
  headers: {
1661
+ ...sandboxTransportHeaders(config),
1590
1662
  "Content-Type": "application/json",
1591
- Authorization: `Bearer ${apiKey}`,
1592
1663
  },
1593
1664
  body: JSON.stringify({ public_key: pubKey }),
1594
1665
  });
1595
- if (!res.ok) {
1596
- const body = await res.text();
1597
- throw new Error(`Failed to register SSH key: ${res.status} ${body}`);
1666
+ if (!response.ok) {
1667
+ const rawBody = await response.text();
1668
+ let body = {};
1669
+ try {
1670
+ body = JSON.parse(rawBody);
1671
+ }
1672
+ catch {
1673
+ body = { message: rawBody || `HTTP ${response.status}` };
1674
+ }
1675
+ throw mapHttpError(response.status, body, rawBody, response.headers.get("x-request-id"));
1598
1676
  }
1599
1677
  console.log(chalk.green("SSH key registered."));
1600
1678
  }
1601
- function bridgeSandboxWs(socket, wsUrl, apiKey) {
1679
+ function sandboxTransportHeaders(config) {
1680
+ if (!config.api_key)
1681
+ throw new Error("Not authenticated. Run: miosa login");
1682
+ const headers = {
1683
+ Authorization: `Bearer ${String(config.api_key)}`,
1684
+ };
1685
+ if (config.tenant)
1686
+ headers["X-MIOSA-Tenant"] = config.tenant;
1687
+ if (config.workspace)
1688
+ headers["X-MIOSA-Workspace"] = config.workspace;
1689
+ return headers;
1690
+ }
1691
+ export function buildSandboxWebSocketRequest(config, id) {
1692
+ const base = config.endpoint.replace(/\/$/, "");
1693
+ const wsBase = base.replace(/^https?/, (protocol) => protocol === "https" ? "wss" : "ws");
1694
+ const url = new URL(`${wsBase}/api/v1/sandboxes/${encodeURIComponent(id)}/ssh-tunnel`);
1695
+ const headers = sandboxTransportHeaders(config);
1696
+ if (config.tenant) {
1697
+ url.searchParams.set("tenant", config.tenant);
1698
+ }
1699
+ if (config.workspace) {
1700
+ url.searchParams.set("workspace", config.workspace);
1701
+ }
1702
+ return { url: url.toString(), headers };
1703
+ }
1704
+ function bridgeSandboxWs(socket, request) {
1602
1705
  let closed = false;
1603
1706
  function cleanup() {
1604
1707
  if (closed)
@@ -1607,8 +1710,8 @@ function bridgeSandboxWs(socket, wsUrl, apiKey) {
1607
1710
  if (!socket.destroyed)
1608
1711
  socket.destroy();
1609
1712
  }
1610
- const ws = new WebSocket(wsUrl, {
1611
- headers: { Authorization: `Bearer ${apiKey}` },
1713
+ const ws = new WebSocket(request.url, {
1714
+ headers: request.headers,
1612
1715
  });
1613
1716
  ws.on("open", () => {
1614
1717
  socket.on("data", (chunk) => {
@@ -1634,14 +1737,8 @@ function bridgeSandboxWs(socket, wsUrl, apiKey) {
1634
1737
  }
1635
1738
  async function runSandboxSsh(id, opts) {
1636
1739
  const config = loadConfig();
1637
- const apiKey = config.api_key;
1638
- if (!apiKey)
1639
- throw new Error("Not authenticated. Run: miosa login");
1640
- const endpoint = config.endpoint ?? "https://api.miosa.ai";
1641
- const base = endpoint.replace(/\/$/, "");
1642
- const wsBase = base.replace(/^https?/, (p) => (p === "https" ? "wss" : "ws"));
1643
- const wsUrl = `${wsBase}/api/v1/sandboxes/${encodeURIComponent(id)}/ssh-tunnel`;
1644
- await ensureSandboxSshKey(id, String(apiKey), endpoint);
1740
+ const wsRequest = buildSandboxWebSocketRequest(config, id);
1741
+ await ensureSandboxSshKey(id, config);
1645
1742
  // Pick a free local port
1646
1743
  const localPort = opts.port ?? (await pickFreePort());
1647
1744
  const user = opts.user ?? "root";
@@ -1650,13 +1747,13 @@ async function runSandboxSsh(id, opts) {
1650
1747
  sandbox_id: id,
1651
1748
  local_port: localPort,
1652
1749
  user,
1653
- ws_url: wsUrl,
1750
+ ws_url: wsRequest.url,
1654
1751
  key_path: SANDBOX_KEY_PATH,
1655
1752
  }));
1656
1753
  return;
1657
1754
  }
1658
1755
  const server = createServer((socket) => {
1659
- bridgeSandboxWs(socket, wsUrl, String(apiKey));
1756
+ bridgeSandboxWs(socket, wsRequest);
1660
1757
  });
1661
1758
  await new Promise((resolve, reject) => {
1662
1759
  server.on("error", reject);
@@ -2783,13 +2880,16 @@ async function uploadDirToSandbox(sandboxId, localDir, remoteDir, opts) {
2783
2880
  if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) {
2784
2881
  throw new UserError(`Local directory not found: ${sourceDir}`);
2785
2882
  }
2883
+ const deleteTarget = opts.delete
2884
+ ? await confirmRemoteDeleteDir(sandboxId, remoteDir, !!opts.force)
2885
+ : null;
2786
2886
  const c = client();
2787
2887
  const archivePath = createDeployArchive(sourceDir);
2788
2888
  const remoteArchive = `/tmp/miosa-upload-${Date.now()}.tgz`;
2789
2889
  try {
2790
2890
  await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
2791
- const clean = opts.delete
2792
- ? `rm -rf ${shellQuote(remoteDir)} && mkdir -p ${shellQuote(remoteDir)}`
2891
+ const clean = deleteTarget != null
2892
+ ? `rm -rf ${shellQuote(deleteTarget)} && mkdir -p ${shellQuote(deleteTarget)}`
2793
2893
  : `mkdir -p ${shellQuote(remoteDir)}`;
2794
2894
  await execSandbox(c, sandboxId, `${clean} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteDir)} && rm -f ${shellQuote(remoteArchive)}`, "/");
2795
2895
  }
@@ -2803,6 +2903,31 @@ async function uploadDirToSandbox(sandboxId, localDir, remoteDir, opts) {
2803
2903
  files_label: path.basename(sourceDir) || sourceDir,
2804
2904
  };
2805
2905
  }
2906
+ // --delete runs `rm -rf` inside the sandbox. Refuse protected roots, then
2907
+ // require an explicit confirmation (interactive prompt, or --force/--yes)
2908
+ // before anything is wiped. Returns the normalized remote dir to delete.
2909
+ async function confirmRemoteDeleteDir(sandboxId, remoteDir, force) {
2910
+ const target = assertDeletableRemoteDir(remoteDir);
2911
+ if (force)
2912
+ return target;
2913
+ if (!process.stdin.isTTY) {
2914
+ throw new UserError(`--delete will permanently wipe ${target} on sandbox ${sandboxId}.`, "Non-interactive session: re-run with --force (or --yes) to confirm the wipe.");
2915
+ }
2916
+ console.log(chalk.yellow(`--delete will permanently wipe sandbox ${sandboxId}:${target} before uploading.`));
2917
+ const { default: inquirer } = await import("inquirer");
2918
+ const { confirmed } = await inquirer.prompt([
2919
+ {
2920
+ type: "confirm",
2921
+ name: "confirmed",
2922
+ message: `Wipe ${sandboxId}:${target}?`,
2923
+ default: false,
2924
+ },
2925
+ ]);
2926
+ if (!confirmed) {
2927
+ throw new UserError("Cancelled - nothing was deleted or uploaded.");
2928
+ }
2929
+ return target;
2930
+ }
2806
2931
  async function execSandbox(c, sandboxId, command, cwd, timeout) {
2807
2932
  const body = { command: commandInCwd(command, cwd) };
2808
2933
  if (cwd) {
@@ -3170,33 +3295,6 @@ async function readSandboxFile(c, sandboxId, remotePath) {
3170
3295
  }
3171
3296
  throw new UserError(`Could not read sandbox file: ${remotePath}`);
3172
3297
  }
3173
- async function fetchApiRaw(path, body) {
3174
- const config = loadConfig();
3175
- const apiKey = config.api_key;
3176
- if (!apiKey)
3177
- throw new UserError("Not authenticated. Run: miosa login");
3178
- const endpoint = (config.endpoint ?? "https://api.miosa.ai").replace(/\/$/, "");
3179
- const res = await fetch(`${endpoint}${path}`, {
3180
- method: body === undefined ? "GET" : "POST",
3181
- headers: {
3182
- Authorization: `Bearer ${String(apiKey)}`,
3183
- Accept: "application/json, text/plain, */*",
3184
- "Content-Type": "application/json",
3185
- "User-Agent": "@miosa/cli",
3186
- },
3187
- body: body === undefined ? undefined : JSON.stringify(body),
3188
- });
3189
- const text = await res.text();
3190
- if (!res.ok) {
3191
- throw new UserError(`Server error (${res.status}): HTTP ${res.status}`, text || res.statusText);
3192
- }
3193
- try {
3194
- return JSON.parse(text);
3195
- }
3196
- catch {
3197
- return text;
3198
- }
3199
- }
3200
3298
  function commandInCwd(command, cwd) {
3201
3299
  if (!cwd)
3202
3300
  return command;