@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.
- package/README.md +85 -0
- package/dist/app-manifest.d.ts +40 -0
- package/dist/app-manifest.d.ts.map +1 -1
- package/dist/app-manifest.js +168 -46
- package/dist/app-manifest.js.map +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +2 -0
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +2 -19
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +31 -11
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/env-input.d.ts +26 -0
- package/dist/commands/env-input.d.ts.map +1 -0
- package/dist/commands/env-input.js +68 -0
- package/dist/commands/env-input.js.map +1 -0
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +15 -3
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/sandbox-delete-guard.d.ts +4 -0
- package/dist/commands/sandbox-delete-guard.d.ts.map +1 -0
- package/dist/commands/sandbox-delete-guard.js +32 -0
- package/dist/commands/sandbox-delete-guard.js.map +1 -0
- package/dist/commands/sandbox-dev.d.ts +20 -0
- package/dist/commands/sandbox-dev.d.ts.map +1 -0
- package/dist/commands/sandbox-dev.js +583 -0
- package/dist/commands/sandbox-dev.js.map +1 -0
- package/dist/commands/sandbox.d.ts +5 -0
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +163 -65
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/tenant.d.ts.map +1 -1
- package/dist/commands/tenant.js +6 -5
- package/dist/commands/tenant.js.map +1 -1
- package/dist/commands/util.d.ts.map +1 -1
- package/dist/commands/util.js +28 -6
- package/dist/commands/util.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -3
- package/dist/errors.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/sandbox.js
CHANGED
|
@@ -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>",
|
|
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
|
-
|
|
427
|
-
|
|
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>",
|
|
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>",
|
|
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>
|
|
967
|
-
.description(
|
|
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
|
|
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
|
|
1062
|
+
.command("doctor [sandbox-id]")
|
|
1028
1063
|
.description("Diagnose sandbox app readiness across sandbox state, internal HTTP, public route, and TLS/edge reachability")
|
|
1029
|
-
.
|
|
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
|
|
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, {
|
|
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
|
-
{
|
|
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,
|
|
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
|
|
1587
|
-
const
|
|
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 (!
|
|
1596
|
-
const
|
|
1597
|
-
|
|
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
|
|
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(
|
|
1611
|
-
headers:
|
|
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
|
|
1638
|
-
|
|
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:
|
|
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,
|
|
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 =
|
|
2792
|
-
? `rm -rf ${shellQuote(
|
|
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;
|