@miosa/cli 1.0.94 → 1.1.0
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 +144 -5
- 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/bin/miosa.js +1 -0
- package/dist/bin/miosa.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -5
- package/dist/client.js.map +1 -1
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.d.ts.map +1 -0
- package/dist/commands/cloud.js +192 -0
- package/dist/commands/cloud.js.map +1 -0
- 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/databases.d.ts.map +1 -1
- package/dist/commands/databases.js +3 -2
- package/dist/commands/databases.js.map +1 -1
- package/dist/commands/db.js +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/desktop.d.ts.map +1 -1
- package/dist/commands/desktop.js +2 -1
- package/dist/commands/desktop.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/mcp.js +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 +247 -87
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/shell.d.ts.map +1 -1
- package/dist/commands/shell.js +3 -2
- package/dist/commands/shell.js.map +1 -1
- package/dist/commands/storage.d.ts.map +1 -1
- package/dist/commands/storage.js +3 -2
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/templates.d.ts.map +1 -1
- package/dist/commands/templates.js +94 -0
- package/dist/commands/templates.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/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +4 -3
package/dist/commands/sandbox.js
CHANGED
|
@@ -11,15 +11,25 @@ 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;
|
|
26
|
+
const SANDBOX_SIZE_CONTRACTS = {
|
|
27
|
+
xs: { cpu: 1, memory: 2_048, disk: 10_240 },
|
|
28
|
+
small: { cpu: 2, memory: 4_096, disk: 10_240 },
|
|
29
|
+
medium: { cpu: 4, memory: 8_192, disk: 20_480 },
|
|
30
|
+
large: { cpu: 8, memory: 16_384, disk: 40_960 },
|
|
31
|
+
xl: { cpu: 16, memory: 32_768, disk: 81_920 },
|
|
32
|
+
};
|
|
23
33
|
export function register(program) {
|
|
24
34
|
// -------------------------------------------------------------------------
|
|
25
35
|
// sandbox / sandboxes command group — built manually to avoid subcommand
|
|
@@ -30,6 +40,7 @@ export function register(program) {
|
|
|
30
40
|
.command("sandbox")
|
|
31
41
|
.alias("sandboxes")
|
|
32
42
|
.description("Manage Sandboxes — lightweight code-only Computers (Firecracker microVMs without a desktop)");
|
|
43
|
+
registerSandboxDevCommands(sandbox);
|
|
33
44
|
// list
|
|
34
45
|
sandbox
|
|
35
46
|
.command("list")
|
|
@@ -63,7 +74,7 @@ export function register(program) {
|
|
|
63
74
|
{ header: "NAME", key: "name" },
|
|
64
75
|
{
|
|
65
76
|
header: "STATUS",
|
|
66
|
-
key: "
|
|
77
|
+
key: "state",
|
|
67
78
|
color: (val) => statusColor(val.trim()),
|
|
68
79
|
},
|
|
69
80
|
{
|
|
@@ -112,7 +123,7 @@ export function register(program) {
|
|
|
112
123
|
{ label: "Name", value: str(sb["name"]) },
|
|
113
124
|
{
|
|
114
125
|
label: "Status",
|
|
115
|
-
value: statusColor(str(sb["
|
|
126
|
+
value: statusColor(str(sb["state"])),
|
|
116
127
|
},
|
|
117
128
|
];
|
|
118
129
|
if (sb["template_id"]) {
|
|
@@ -170,10 +181,13 @@ export function register(program) {
|
|
|
170
181
|
.description("Create a new Sandbox")
|
|
171
182
|
.option("--template <template>", "Template / image ID (default: miosa-sandbox)")
|
|
172
183
|
.option("--name <name>", "Human-readable name for the Sandbox")
|
|
184
|
+
.option("--size <size>", "Named size: xs, small, medium, large, or xl (default: small)", parseSandboxSize)
|
|
173
185
|
.option("--cpu <n>", "vCPU count", parseIntegerOption)
|
|
174
186
|
.option("--memory <size>", "Memory size, e.g. 4096mb or 4gb", parseSizeMb)
|
|
175
187
|
.option("--disk <size>", "Disk size, e.g. 10240mb or 10gb", parseSizeMb)
|
|
176
188
|
.option("--timeout <duration>", "Wall-clock timeout, e.g. 300s, 1h", parseDurationSec)
|
|
189
|
+
.option("--idle-timeout <duration>", "Idle timeout before pause; 0 disables it (default: 0)", parseDurationSec)
|
|
190
|
+
.option("--idempotency-key <key>", "Retry-safe create key retained by the service for 24 hours")
|
|
177
191
|
.option("--publish-port <port>", "Expose this port after create", parseIntegerOption)
|
|
178
192
|
.option("--wait", "Wait for sandbox running and published port readiness")
|
|
179
193
|
.option("--probe-path <path>", "HTTP path to probe when --publish-port is set", "/")
|
|
@@ -182,6 +196,9 @@ export function register(program) {
|
|
|
182
196
|
.option("--depth <n>", "Git clone depth when --source git: is used", parseIntegerOption)
|
|
183
197
|
.option("--snapshot <id>", "Create from a sandbox snapshot")
|
|
184
198
|
.option("--workspace <id-or-slug>", "Workspace ID/slug")
|
|
199
|
+
.option("--external-workspace <id>", "White-label workspace/customer ID for billing attribution")
|
|
200
|
+
.option("--external-user <id>", "White-label user ID for billing attribution")
|
|
201
|
+
.option("--external-project <id>", "White-label project ID for billing attribution")
|
|
185
202
|
.option("--agent-profile <id>", "Agent runtime profile ID to mount into the sandbox")
|
|
186
203
|
.option("--skip-agent-profile", "Do not apply the tenant/workspace default agent runtime profile")
|
|
187
204
|
.option("--network-policy <policy>", "Network policy: allow-all or deny-all")
|
|
@@ -192,6 +209,12 @@ export function register(program) {
|
|
|
192
209
|
.option("--non-persistent", "Discard filesystem state on timeout instead of pausing for resume")
|
|
193
210
|
.option("--auto-start", "Seed and start the template app after the sandbox reaches running"))
|
|
194
211
|
.option("--json", "Output as JSON")
|
|
212
|
+
.addHelpText("after", `
|
|
213
|
+
Note:
|
|
214
|
+
White-label preview domains require external attribution at creation.
|
|
215
|
+
Pass --external-workspace / --external-user / --external-project when the
|
|
216
|
+
sandbox will be exposed on a white-label preview domain.
|
|
217
|
+
`)
|
|
195
218
|
.action((opts) => runAction(async () => {
|
|
196
219
|
const t0 = Date.now();
|
|
197
220
|
const json = !!isJsonMode(opts) || process.env["MIOSA_JSON"] === "1";
|
|
@@ -204,19 +227,21 @@ export function register(program) {
|
|
|
204
227
|
renderCreateSuccess(raw, Date.now() - t0);
|
|
205
228
|
return;
|
|
206
229
|
}
|
|
207
|
-
const body = {
|
|
208
|
-
|
|
209
|
-
|
|
230
|
+
const body = {
|
|
231
|
+
template_id: opts.template ?? "miosa-sandbox",
|
|
232
|
+
};
|
|
210
233
|
if (opts.name)
|
|
211
234
|
body["name"] = opts.name;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
body["
|
|
216
|
-
|
|
217
|
-
body["disk_size_mb"] =
|
|
235
|
+
const resources = resolveSandboxResources(opts);
|
|
236
|
+
body["size"] = resources.size;
|
|
237
|
+
if (resources.legacy) {
|
|
238
|
+
body["cpu_count"] = resources.legacy.cpu;
|
|
239
|
+
body["memory_mb"] = resources.legacy.memory;
|
|
240
|
+
body["disk_size_mb"] = resources.legacy.disk;
|
|
241
|
+
}
|
|
218
242
|
const timeoutSec = opts.timeout ?? DEFAULT_INTERACTIVE_SANDBOX_TIMEOUT_SEC;
|
|
219
243
|
body["timeout_sec"] = timeoutSec;
|
|
244
|
+
body["idle_timeout_sec"] = opts.idleTimeout ?? 0;
|
|
220
245
|
if (opts.source)
|
|
221
246
|
body["source"] = opts.source;
|
|
222
247
|
if (opts.revision)
|
|
@@ -236,6 +261,15 @@ export function register(program) {
|
|
|
236
261
|
process.env["MIOSA_WORKSPACE"];
|
|
237
262
|
if (workspace)
|
|
238
263
|
body["workspace_id"] = workspace;
|
|
264
|
+
if (opts.externalWorkspace) {
|
|
265
|
+
body["external_workspace_id"] = opts.externalWorkspace;
|
|
266
|
+
}
|
|
267
|
+
if (opts.externalUser) {
|
|
268
|
+
body["external_user_id"] = opts.externalUser;
|
|
269
|
+
}
|
|
270
|
+
if (opts.externalProject) {
|
|
271
|
+
body["external_project_id"] = opts.externalProject;
|
|
272
|
+
}
|
|
239
273
|
const networkPolicy = buildNetworkPolicy(opts);
|
|
240
274
|
if (networkPolicy) {
|
|
241
275
|
body["metadata"] = {
|
|
@@ -249,7 +283,9 @@ export function register(program) {
|
|
|
249
283
|
body["persistent"] = opts.nonPersistent ? false : true;
|
|
250
284
|
if (opts.autoStart)
|
|
251
285
|
body["auto_start"] = true;
|
|
252
|
-
const raw = unwrap(await client().apiPost(apiPath("/sandboxes"), body
|
|
286
|
+
const raw = unwrap(await client().apiPost(apiPath("/sandboxes"), body, opts.idempotencyKey
|
|
287
|
+
? { "Idempotency-Key": opts.idempotencyKey }
|
|
288
|
+
: undefined));
|
|
253
289
|
const sb = (raw ?? {});
|
|
254
290
|
const id = String(sb["id"] ?? "");
|
|
255
291
|
if (opts.publishPort != null && id) {
|
|
@@ -285,14 +321,16 @@ export function register(program) {
|
|
|
285
321
|
// delete
|
|
286
322
|
sandbox
|
|
287
323
|
.command("delete <sandbox-id>")
|
|
288
|
-
.description("
|
|
324
|
+
.description("Permanently delete a Sandbox (legacy API extension)")
|
|
325
|
+
.requiredOption("--force", "Confirm permanent filesystem deletion")
|
|
289
326
|
.option("--json", "Output as JSON")
|
|
290
327
|
.action((id, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}`, opts)));
|
|
291
328
|
// destroy / rm — aliases for delete
|
|
292
329
|
sandbox
|
|
293
330
|
.command("destroy <sandbox-id>")
|
|
294
331
|
.alias("rm")
|
|
295
|
-
.description("
|
|
332
|
+
.description("Permanently destroy a Sandbox (legacy API extension)")
|
|
333
|
+
.requiredOption("--force", "Confirm permanent filesystem deletion")
|
|
296
334
|
.option("--json", "Output as JSON")
|
|
297
335
|
.action((id, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}`, opts)));
|
|
298
336
|
// action — lifecycle operations: pause, resume
|
|
@@ -307,6 +345,11 @@ export function register(program) {
|
|
|
307
345
|
}
|
|
308
346
|
await postAndPrint(`/sandboxes/${enc(id)}/${enc(action)}`, opts, {});
|
|
309
347
|
}));
|
|
348
|
+
sandbox
|
|
349
|
+
.command("pause <sandbox-id>")
|
|
350
|
+
.description("Pause a running persistent Sandbox and preserve its workspace")
|
|
351
|
+
.option("--json", "Output as JSON")
|
|
352
|
+
.action((id, opts) => runAction(() => postAndPrint(`/sandboxes/${enc(id)}/pause`, opts, {})));
|
|
310
353
|
// stop — snapshot + pause (mirrors `box stop`)
|
|
311
354
|
sandbox
|
|
312
355
|
.command("stop <sandbox-id>")
|
|
@@ -314,7 +357,7 @@ export function register(program) {
|
|
|
314
357
|
.option("--no-snapshot", "Deprecated compatibility flag; stop still preserves state server-side")
|
|
315
358
|
.option("--json", "Output as JSON")
|
|
316
359
|
.action((id, opts) => runAction(async () => {
|
|
317
|
-
const stopped = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/
|
|
360
|
+
const stopped = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/pause`), {}));
|
|
318
361
|
if (isJsonMode(opts)) {
|
|
319
362
|
console.log(JSON.stringify(stopped, null, 2));
|
|
320
363
|
return;
|
|
@@ -335,19 +378,27 @@ export function register(program) {
|
|
|
335
378
|
sandbox
|
|
336
379
|
.command("resume <sandbox-id>")
|
|
337
380
|
.description("Resume a paused Sandbox")
|
|
381
|
+
.option("--idempotency-key <key>", "Retry-safe lifecycle key sent as Idempotency-Key")
|
|
338
382
|
.option("--json", "Output as JSON")
|
|
339
383
|
.action((id, opts) => runAction(() => resumeSandboxAndPrint(id, opts)));
|
|
340
384
|
// fork — clone from snapshot in one call (mirrors `box fork`)
|
|
341
385
|
sandbox
|
|
342
386
|
.command("fork <sandbox-id>")
|
|
343
|
-
.description("
|
|
344
|
-
.option("--
|
|
387
|
+
.description("Snapshot and fork a running Sandbox into a new Sandbox")
|
|
388
|
+
.option("--template <template>", "Template or immutable image override")
|
|
389
|
+
.option("--timeout <duration>", "Fork timeout", parseDurationSec)
|
|
390
|
+
.option("--idempotency-key <key>", "Retry-safe fork key sent as Idempotency-Key")
|
|
345
391
|
.option("--json", "Output as JSON")
|
|
346
392
|
.action((id, opts) => runAction(async () => {
|
|
347
393
|
const body = {};
|
|
348
|
-
if (opts.
|
|
349
|
-
body["
|
|
350
|
-
|
|
394
|
+
if (opts.template)
|
|
395
|
+
body["template_id"] = opts.template;
|
|
396
|
+
if (opts.timeout != null)
|
|
397
|
+
body["timeout_sec"] = opts.timeout;
|
|
398
|
+
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/fork`), body, opts.idempotencyKey
|
|
399
|
+
? { "Idempotency-Key": opts.idempotencyKey }
|
|
400
|
+
: undefined));
|
|
401
|
+
printValue(result, opts);
|
|
351
402
|
}));
|
|
352
403
|
// desktop — open the Sandbox's web desktop URL (mirrors `box desktop`).
|
|
353
404
|
// Sandboxes are headless by default; only templates that expose a desktop
|
|
@@ -385,7 +436,9 @@ export function register(program) {
|
|
|
385
436
|
.option("--connector <uid>", "MIOSA Connect connector UID to preflight before running the agent")
|
|
386
437
|
.option("--preflight", "Verify the Sandbox has the requested provider connector before exec")
|
|
387
438
|
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
388
|
-
.option("--env <KEY=VALUE>",
|
|
439
|
+
.option("--env <KEY=VALUE>", `Environment variable for the Agent Run. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
|
|
440
|
+
.option("--env-file <path>", ENV_FILE_OPTION_HELP)
|
|
441
|
+
.option("--env-stdin", ENV_STDIN_OPTION_HELP)
|
|
389
442
|
.option("--agent-profile <id>", "Agent runtime profile ID")
|
|
390
443
|
.option("--skip-agent-profile", "Do not apply the default agent runtime profile")
|
|
391
444
|
.option("--external-workspace <id>", "White-label workspace/customer ID for billing attribution")
|
|
@@ -423,8 +476,9 @@ export function register(program) {
|
|
|
423
476
|
body["model"] = opts.model;
|
|
424
477
|
if (opts.cwd)
|
|
425
478
|
body["cwd"] = opts.cwd;
|
|
426
|
-
|
|
427
|
-
|
|
479
|
+
const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
|
|
480
|
+
if (Object.keys(env).length > 0) {
|
|
481
|
+
body["env"] = env;
|
|
428
482
|
}
|
|
429
483
|
if (opts.agentProfile) {
|
|
430
484
|
body["agent_runtime_profile_id"] = opts.agentProfile;
|
|
@@ -479,7 +533,9 @@ export function register(program) {
|
|
|
479
533
|
.option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
|
|
480
534
|
.option("--command <command>", "Alias for --cmd")
|
|
481
535
|
.option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
|
|
482
|
-
.option("--env <pair>",
|
|
536
|
+
.option("--env <pair>", `Environment variable KEY=VALUE. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
|
|
537
|
+
.option("--env-file <path>", ENV_FILE_OPTION_HELP)
|
|
538
|
+
.option("--env-stdin", ENV_STDIN_OPTION_HELP)
|
|
483
539
|
.option("--background", "Start the command in the background and return immediately")
|
|
484
540
|
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
485
541
|
.option("--follow", "Stream command output until it exits (alias --stream)")
|
|
@@ -508,7 +564,7 @@ export function register(program) {
|
|
|
508
564
|
body["cwd"] = cwd;
|
|
509
565
|
body["dir"] = cwd;
|
|
510
566
|
}
|
|
511
|
-
const env = parseEnvPairs(opts.env ?? []);
|
|
567
|
+
const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
|
|
512
568
|
if (opts.detached) {
|
|
513
569
|
const result = await createSandboxCommand(id, cmd, {
|
|
514
570
|
cwd,
|
|
@@ -547,7 +603,9 @@ export function register(program) {
|
|
|
547
603
|
.option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
|
|
548
604
|
.option("--command <command>", "Alias for --cmd")
|
|
549
605
|
.option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
|
|
550
|
-
.option("--env <pair>",
|
|
606
|
+
.option("--env <pair>", `Environment variable KEY=VALUE. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
|
|
607
|
+
.option("--env-file <path>", ENV_FILE_OPTION_HELP)
|
|
608
|
+
.option("--env-stdin", ENV_STDIN_OPTION_HELP)
|
|
551
609
|
.option("--background", "Start the command in the background and return immediately")
|
|
552
610
|
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
553
611
|
.option("--follow", "Stream command output until it exits (alias --stream)")
|
|
@@ -576,7 +634,7 @@ export function register(program) {
|
|
|
576
634
|
body["cwd"] = cwd;
|
|
577
635
|
body["dir"] = cwd;
|
|
578
636
|
}
|
|
579
|
-
const env = parseEnvPairs(opts.env ?? []);
|
|
637
|
+
const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
|
|
580
638
|
if (opts.detached) {
|
|
581
639
|
const result = await createSandboxCommand(id, cmd, {
|
|
582
640
|
cwd,
|
|
@@ -963,11 +1021,17 @@ export function register(program) {
|
|
|
963
1021
|
]);
|
|
964
1022
|
}));
|
|
965
1023
|
env
|
|
966
|
-
.command("set <sandbox-id>
|
|
967
|
-
.description(
|
|
1024
|
+
.command("set <sandbox-id> [pairs...]")
|
|
1025
|
+
.description(`Set encrypted sandbox env vars as KEY=VALUE. ${ENV_INLINE_SHELL_WARNING}`)
|
|
1026
|
+
.option("--env-file <path>", ENV_FILE_OPTION_HELP)
|
|
1027
|
+
.option("--env-stdin", ENV_STDIN_OPTION_HELP)
|
|
968
1028
|
.option("--json", "Output as JSON")
|
|
969
1029
|
.action((id, pairs, opts) => runAction(async () => {
|
|
970
|
-
const
|
|
1030
|
+
const values = await resolveEnvInputs(parseEnvPairs(pairs ?? []), opts);
|
|
1031
|
+
if (Object.keys(values).length === 0) {
|
|
1032
|
+
throw new UserError("No env vars provided.", "Pass KEY=VALUE pairs, --env-file <path>, or --env-stdin.");
|
|
1033
|
+
}
|
|
1034
|
+
const vars = Object.entries(values).map(([key, value]) => ({
|
|
971
1035
|
key,
|
|
972
1036
|
value,
|
|
973
1037
|
}));
|
|
@@ -1024,12 +1088,34 @@ export function register(program) {
|
|
|
1024
1088
|
console.log(chalk.green(`Attached database ${databaseId} to sandbox ${id}.`));
|
|
1025
1089
|
}));
|
|
1026
1090
|
sandbox
|
|
1027
|
-
.command("doctor
|
|
1091
|
+
.command("doctor [sandbox-id]")
|
|
1028
1092
|
.description("Diagnose sandbox app readiness across sandbox state, internal HTTP, public route, and TLS/edge reachability")
|
|
1029
|
-
.
|
|
1093
|
+
.option("--port <port>", "Port inside the sandbox to check", parseIntegerOption)
|
|
1030
1094
|
.option("--probe-path <path>", "HTTP path to probe", "/")
|
|
1095
|
+
.option("--full", "Inspect the complete canonical developer contract")
|
|
1096
|
+
.option("--dir <path>", "Project directory for --full", ".")
|
|
1031
1097
|
.option("--json", "Output as JSON")
|
|
1032
1098
|
.action((id, opts) => runAction(async () => {
|
|
1099
|
+
if (opts.full) {
|
|
1100
|
+
const report = await runFullSandboxDoctor(opts.dir, id);
|
|
1101
|
+
if (!report.ok)
|
|
1102
|
+
process.exitCode = 1;
|
|
1103
|
+
if (isJsonMode(opts)) {
|
|
1104
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
console.log();
|
|
1108
|
+
console.log(chalk.bold("Sandbox Doctor Full"));
|
|
1109
|
+
console.log();
|
|
1110
|
+
for (const check of report.checks) {
|
|
1111
|
+
console.log(` ${check.ok ? chalk.green("ok") : chalk.red("fail")} ${check.id}: ${check.message}`);
|
|
1112
|
+
}
|
|
1113
|
+
console.log();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (!id || opts.port == null) {
|
|
1117
|
+
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.");
|
|
1118
|
+
}
|
|
1033
1119
|
const report = await doctorSandbox(id, opts.port, opts.probePath);
|
|
1034
1120
|
if (isJsonMode(opts)) {
|
|
1035
1121
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -1082,7 +1168,7 @@ export function register(program) {
|
|
|
1082
1168
|
.option("--json", "Output raw JSON response")
|
|
1083
1169
|
.action(async (id, remotePath, opts) => {
|
|
1084
1170
|
try {
|
|
1085
|
-
const result = await
|
|
1171
|
+
const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/files/read?path=${enc(remotePath)}`));
|
|
1086
1172
|
if (isJsonMode(opts)) {
|
|
1087
1173
|
console.log(JSON.stringify(typeof result === "string" ? { content: result } : result, null, 2));
|
|
1088
1174
|
return;
|
|
@@ -1142,11 +1228,14 @@ export function register(program) {
|
|
|
1142
1228
|
.command("upload-dir <sandbox-id> <local-dir> <remote-dir>")
|
|
1143
1229
|
.description("Upload a local directory into a Sandbox")
|
|
1144
1230
|
.option("--delete", "Delete the remote directory contents before extracting")
|
|
1231
|
+
.option("--force", "Skip the --delete confirmation prompt")
|
|
1232
|
+
.option("--yes", "Alias for --force")
|
|
1145
1233
|
.option("--json", "Output as JSON")
|
|
1146
1234
|
.action(async (id, localDir, remoteDir, opts) => {
|
|
1147
1235
|
try {
|
|
1148
1236
|
const result = await uploadDirToSandbox(id, localDir, remoteDir, {
|
|
1149
1237
|
delete: !!opts.delete,
|
|
1238
|
+
force: !!(opts.force || opts.yes),
|
|
1150
1239
|
});
|
|
1151
1240
|
if (isJsonMode(opts)) {
|
|
1152
1241
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1163,11 +1252,14 @@ export function register(program) {
|
|
|
1163
1252
|
.description("Sync a local directory into a sandbox")
|
|
1164
1253
|
.requiredOption("--sandbox <id>", "Sandbox ID")
|
|
1165
1254
|
.option("--delete", "Delete the remote directory contents before extracting")
|
|
1255
|
+
.option("--force", "Skip the --delete confirmation prompt")
|
|
1256
|
+
.option("--yes", "Alias for --force")
|
|
1166
1257
|
.option("--json", "Output as JSON")
|
|
1167
1258
|
.action(async (localDir, remoteDir, opts) => {
|
|
1168
1259
|
try {
|
|
1169
1260
|
const result = await uploadDirToSandbox(opts.sandbox, localDir, remoteDir, {
|
|
1170
1261
|
delete: !!opts.delete,
|
|
1262
|
+
force: !!(opts.force || opts.yes),
|
|
1171
1263
|
});
|
|
1172
1264
|
if (isJsonMode(opts)) {
|
|
1173
1265
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1184,6 +1276,8 @@ export function register(program) {
|
|
|
1184
1276
|
.alias("copy")
|
|
1185
1277
|
.description("Copy a local file or directory into a sandbox, e.g. ./app/. sbx_123:/workspace")
|
|
1186
1278
|
.option("--delete", "Delete the remote directory contents before extracting directories")
|
|
1279
|
+
.option("--force", "Skip the --delete confirmation prompt")
|
|
1280
|
+
.option("--yes", "Alias for --force")
|
|
1187
1281
|
.option("--json", "Output as JSON")
|
|
1188
1282
|
.action(async (source, target, opts) => {
|
|
1189
1283
|
try {
|
|
@@ -1204,7 +1298,10 @@ export function register(program) {
|
|
|
1204
1298
|
const local = source.endsWith("/.") ? source.slice(0, -2) : source;
|
|
1205
1299
|
const stat = fs.statSync(local);
|
|
1206
1300
|
if (stat.isDirectory()) {
|
|
1207
|
-
const result = await uploadDirToSandbox(parsed.sandboxId, local, parsed.remotePath, {
|
|
1301
|
+
const result = await uploadDirToSandbox(parsed.sandboxId, local, parsed.remotePath, {
|
|
1302
|
+
delete: !!opts.delete,
|
|
1303
|
+
force: !!(opts.force || opts.yes),
|
|
1304
|
+
});
|
|
1208
1305
|
if (isJsonMode(opts)) {
|
|
1209
1306
|
console.log(JSON.stringify(result, null, 2));
|
|
1210
1307
|
return;
|
|
@@ -1314,7 +1411,10 @@ export function register(program) {
|
|
|
1314
1411
|
console.log(kvPanel([
|
|
1315
1412
|
{ label: "Export", value: chalk.bold(str(exportData["id"])) },
|
|
1316
1413
|
{ label: "Sandbox", value: id },
|
|
1317
|
-
{
|
|
1414
|
+
{
|
|
1415
|
+
label: "Status",
|
|
1416
|
+
value: statusColor(str(exportData["status"])),
|
|
1417
|
+
},
|
|
1318
1418
|
{
|
|
1319
1419
|
label: "Archive",
|
|
1320
1420
|
value: str(exportData["archive_download_url"]),
|
|
@@ -1555,7 +1655,7 @@ function normalizeConnectorMode(value) {
|
|
|
1555
1655
|
// 4. Spawn `ssh` pointing at localhost:<port> with the key and user.
|
|
1556
1656
|
// 5. On ssh exit, close the TCP server and exit.
|
|
1557
1657
|
const SANDBOX_KEY_PATH = path.join(os.homedir(), ".ssh", "miosa_sandbox_ed25519");
|
|
1558
|
-
async function ensureSandboxSshKey(id,
|
|
1658
|
+
async function ensureSandboxSshKey(id, config) {
|
|
1559
1659
|
if (!fs.existsSync(SANDBOX_KEY_PATH)) {
|
|
1560
1660
|
console.log(chalk.dim("Generating SSH keypair for MIOSA sandbox access..."));
|
|
1561
1661
|
fs.mkdirSync(path.join(os.homedir(), ".ssh"), { recursive: true });
|
|
@@ -1583,22 +1683,54 @@ async function ensureSandboxSshKey(id, apiKey, endpoint) {
|
|
|
1583
1683
|
// already exist from a previous sandbox, but a fresh sandbox still needs it
|
|
1584
1684
|
// installed in authorized_keys before SSH auth can succeed.
|
|
1585
1685
|
const pubKey = fs.readFileSync(`${SANDBOX_KEY_PATH}.pub`, "utf8").trim();
|
|
1586
|
-
const
|
|
1587
|
-
const
|
|
1686
|
+
const endpoint = config.endpoint.replace(/\/$/, "");
|
|
1687
|
+
const response = await fetch(`${endpoint}${apiPath(`/sandboxes/${encodeURIComponent(id)}/ssh-keys`)}`, {
|
|
1588
1688
|
method: "POST",
|
|
1589
1689
|
headers: {
|
|
1690
|
+
...sandboxTransportHeaders(config),
|
|
1590
1691
|
"Content-Type": "application/json",
|
|
1591
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1592
1692
|
},
|
|
1593
1693
|
body: JSON.stringify({ public_key: pubKey }),
|
|
1594
1694
|
});
|
|
1595
|
-
if (!
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1695
|
+
if (!response.ok) {
|
|
1696
|
+
const rawBody = await response.text();
|
|
1697
|
+
let body = {};
|
|
1698
|
+
try {
|
|
1699
|
+
body = JSON.parse(rawBody);
|
|
1700
|
+
}
|
|
1701
|
+
catch {
|
|
1702
|
+
body = { message: rawBody || `HTTP ${response.status}` };
|
|
1703
|
+
}
|
|
1704
|
+
throw mapHttpError(response.status, body, rawBody, response.headers.get("x-request-id"));
|
|
1598
1705
|
}
|
|
1599
1706
|
console.log(chalk.green("SSH key registered."));
|
|
1600
1707
|
}
|
|
1601
|
-
function
|
|
1708
|
+
function sandboxTransportHeaders(config) {
|
|
1709
|
+
if (!config.api_key)
|
|
1710
|
+
throw new Error("Not authenticated. Run: miosa login");
|
|
1711
|
+
const headers = {
|
|
1712
|
+
Authorization: `Bearer ${String(config.api_key)}`,
|
|
1713
|
+
};
|
|
1714
|
+
if (config.tenant)
|
|
1715
|
+
headers["X-MIOSA-Tenant"] = config.tenant;
|
|
1716
|
+
if (config.workspace)
|
|
1717
|
+
headers["X-MIOSA-Workspace"] = config.workspace;
|
|
1718
|
+
return headers;
|
|
1719
|
+
}
|
|
1720
|
+
export function buildSandboxWebSocketRequest(config, id) {
|
|
1721
|
+
const base = config.endpoint.replace(/\/$/, "");
|
|
1722
|
+
const wsBase = base.replace(/^https?/, (protocol) => protocol === "https" ? "wss" : "ws");
|
|
1723
|
+
const url = new URL(`${wsBase}/api/v1/sandboxes/${encodeURIComponent(id)}/ssh-tunnel`);
|
|
1724
|
+
const headers = sandboxTransportHeaders(config);
|
|
1725
|
+
if (config.tenant) {
|
|
1726
|
+
url.searchParams.set("tenant", config.tenant);
|
|
1727
|
+
}
|
|
1728
|
+
if (config.workspace) {
|
|
1729
|
+
url.searchParams.set("workspace", config.workspace);
|
|
1730
|
+
}
|
|
1731
|
+
return { url: url.toString(), headers };
|
|
1732
|
+
}
|
|
1733
|
+
function bridgeSandboxWs(socket, request) {
|
|
1602
1734
|
let closed = false;
|
|
1603
1735
|
function cleanup() {
|
|
1604
1736
|
if (closed)
|
|
@@ -1607,8 +1739,8 @@ function bridgeSandboxWs(socket, wsUrl, apiKey) {
|
|
|
1607
1739
|
if (!socket.destroyed)
|
|
1608
1740
|
socket.destroy();
|
|
1609
1741
|
}
|
|
1610
|
-
const ws = new WebSocket(
|
|
1611
|
-
headers:
|
|
1742
|
+
const ws = new WebSocket(request.url, {
|
|
1743
|
+
headers: request.headers,
|
|
1612
1744
|
});
|
|
1613
1745
|
ws.on("open", () => {
|
|
1614
1746
|
socket.on("data", (chunk) => {
|
|
@@ -1634,14 +1766,8 @@ function bridgeSandboxWs(socket, wsUrl, apiKey) {
|
|
|
1634
1766
|
}
|
|
1635
1767
|
async function runSandboxSsh(id, opts) {
|
|
1636
1768
|
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);
|
|
1769
|
+
const wsRequest = buildSandboxWebSocketRequest(config, id);
|
|
1770
|
+
await ensureSandboxSshKey(id, config);
|
|
1645
1771
|
// Pick a free local port
|
|
1646
1772
|
const localPort = opts.port ?? (await pickFreePort());
|
|
1647
1773
|
const user = opts.user ?? "root";
|
|
@@ -1650,13 +1776,13 @@ async function runSandboxSsh(id, opts) {
|
|
|
1650
1776
|
sandbox_id: id,
|
|
1651
1777
|
local_port: localPort,
|
|
1652
1778
|
user,
|
|
1653
|
-
ws_url:
|
|
1779
|
+
ws_url: wsRequest.url,
|
|
1654
1780
|
key_path: SANDBOX_KEY_PATH,
|
|
1655
1781
|
}));
|
|
1656
1782
|
return;
|
|
1657
1783
|
}
|
|
1658
1784
|
const server = createServer((socket) => {
|
|
1659
|
-
bridgeSandboxWs(socket,
|
|
1785
|
+
bridgeSandboxWs(socket, wsRequest);
|
|
1660
1786
|
});
|
|
1661
1787
|
await new Promise((resolve, reject) => {
|
|
1662
1788
|
server.on("error", reject);
|
|
@@ -2196,7 +2322,7 @@ function parsePublishDatabase(value) {
|
|
|
2196
2322
|
normalized === "postgresql" ||
|
|
2197
2323
|
normalized === "create:postgres" ||
|
|
2198
2324
|
normalized === "create:postgresql") {
|
|
2199
|
-
return { engine: "postgresql", engine_version: "
|
|
2325
|
+
return { engine: "postgresql", engine_version: "16" };
|
|
2200
2326
|
}
|
|
2201
2327
|
if (normalized.startsWith("existing:")) {
|
|
2202
2328
|
return { existing_database_id: value.slice("existing:".length) };
|
|
@@ -2506,7 +2632,10 @@ async function waitForSandboxRunning(c, sandboxId, timeoutSec) {
|
|
|
2506
2632
|
}
|
|
2507
2633
|
async function resumeSandboxAndPrint(sandboxId, opts) {
|
|
2508
2634
|
try {
|
|
2509
|
-
await
|
|
2635
|
+
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/resume`), {}, opts.idempotencyKey
|
|
2636
|
+
? { "Idempotency-Key": opts.idempotencyKey }
|
|
2637
|
+
: undefined));
|
|
2638
|
+
printValue(result, opts);
|
|
2510
2639
|
}
|
|
2511
2640
|
catch (err) {
|
|
2512
2641
|
if (err instanceof ApiResponseError && err.code === "SANDBOX_NOT_PAUSED") {
|
|
@@ -2783,13 +2912,16 @@ async function uploadDirToSandbox(sandboxId, localDir, remoteDir, opts) {
|
|
|
2783
2912
|
if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) {
|
|
2784
2913
|
throw new UserError(`Local directory not found: ${sourceDir}`);
|
|
2785
2914
|
}
|
|
2915
|
+
const deleteTarget = opts.delete
|
|
2916
|
+
? await confirmRemoteDeleteDir(sandboxId, remoteDir, !!opts.force)
|
|
2917
|
+
: null;
|
|
2786
2918
|
const c = client();
|
|
2787
2919
|
const archivePath = createDeployArchive(sourceDir);
|
|
2788
2920
|
const remoteArchive = `/tmp/miosa-upload-${Date.now()}.tgz`;
|
|
2789
2921
|
try {
|
|
2790
2922
|
await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
|
|
2791
|
-
const clean =
|
|
2792
|
-
? `rm -rf ${shellQuote(
|
|
2923
|
+
const clean = deleteTarget != null
|
|
2924
|
+
? `rm -rf ${shellQuote(deleteTarget)} && mkdir -p ${shellQuote(deleteTarget)}`
|
|
2793
2925
|
: `mkdir -p ${shellQuote(remoteDir)}`;
|
|
2794
2926
|
await execSandbox(c, sandboxId, `${clean} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteDir)} && rm -f ${shellQuote(remoteArchive)}`, "/");
|
|
2795
2927
|
}
|
|
@@ -2803,6 +2935,31 @@ async function uploadDirToSandbox(sandboxId, localDir, remoteDir, opts) {
|
|
|
2803
2935
|
files_label: path.basename(sourceDir) || sourceDir,
|
|
2804
2936
|
};
|
|
2805
2937
|
}
|
|
2938
|
+
// --delete runs `rm -rf` inside the sandbox. Refuse protected roots, then
|
|
2939
|
+
// require an explicit confirmation (interactive prompt, or --force/--yes)
|
|
2940
|
+
// before anything is wiped. Returns the normalized remote dir to delete.
|
|
2941
|
+
async function confirmRemoteDeleteDir(sandboxId, remoteDir, force) {
|
|
2942
|
+
const target = assertDeletableRemoteDir(remoteDir);
|
|
2943
|
+
if (force)
|
|
2944
|
+
return target;
|
|
2945
|
+
if (!process.stdin.isTTY) {
|
|
2946
|
+
throw new UserError(`--delete will permanently wipe ${target} on sandbox ${sandboxId}.`, "Non-interactive session: re-run with --force (or --yes) to confirm the wipe.");
|
|
2947
|
+
}
|
|
2948
|
+
console.log(chalk.yellow(`--delete will permanently wipe sandbox ${sandboxId}:${target} before uploading.`));
|
|
2949
|
+
const { default: inquirer } = await import("inquirer");
|
|
2950
|
+
const { confirmed } = await inquirer.prompt([
|
|
2951
|
+
{
|
|
2952
|
+
type: "confirm",
|
|
2953
|
+
name: "confirmed",
|
|
2954
|
+
message: `Wipe ${sandboxId}:${target}?`,
|
|
2955
|
+
default: false,
|
|
2956
|
+
},
|
|
2957
|
+
]);
|
|
2958
|
+
if (!confirmed) {
|
|
2959
|
+
throw new UserError("Cancelled - nothing was deleted or uploaded.");
|
|
2960
|
+
}
|
|
2961
|
+
return target;
|
|
2962
|
+
}
|
|
2806
2963
|
async function execSandbox(c, sandboxId, command, cwd, timeout) {
|
|
2807
2964
|
const body = { command: commandInCwd(command, cwd) };
|
|
2808
2965
|
if (cwd) {
|
|
@@ -3170,33 +3327,6 @@ async function readSandboxFile(c, sandboxId, remotePath) {
|
|
|
3170
3327
|
}
|
|
3171
3328
|
throw new UserError(`Could not read sandbox file: ${remotePath}`);
|
|
3172
3329
|
}
|
|
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
3330
|
function commandInCwd(command, cwd) {
|
|
3201
3331
|
if (!cwd)
|
|
3202
3332
|
return command;
|
|
@@ -3289,6 +3419,36 @@ function parseIntegerOption(value) {
|
|
|
3289
3419
|
throw new UserError(`Invalid integer: ${value}`);
|
|
3290
3420
|
return n;
|
|
3291
3421
|
}
|
|
3422
|
+
function parseSandboxSize(value) {
|
|
3423
|
+
const size = String(value).trim().toLowerCase();
|
|
3424
|
+
if (size in SANDBOX_SIZE_CONTRACTS)
|
|
3425
|
+
return size;
|
|
3426
|
+
throw new UserError(`Invalid sandbox size: ${value}. Expected xs, small, medium, large, or xl.`);
|
|
3427
|
+
}
|
|
3428
|
+
function resolveSandboxResources(opts) {
|
|
3429
|
+
const legacyValues = [opts.cpu, opts.memory, opts.disk];
|
|
3430
|
+
const legacyCount = legacyValues.filter((value) => value != null).length;
|
|
3431
|
+
if (legacyCount === 0)
|
|
3432
|
+
return { size: opts.size ?? "small" };
|
|
3433
|
+
if (legacyCount !== legacyValues.length) {
|
|
3434
|
+
throw new UserError("Legacy resource overrides require --cpu, --memory, and --disk together. Prefer --size.");
|
|
3435
|
+
}
|
|
3436
|
+
const legacy = {
|
|
3437
|
+
cpu: opts.cpu,
|
|
3438
|
+
memory: opts.memory,
|
|
3439
|
+
disk: opts.disk,
|
|
3440
|
+
};
|
|
3441
|
+
const matchingSize = Object.entries(SANDBOX_SIZE_CONTRACTS).find(([, contract]) => contract.cpu === legacy.cpu &&
|
|
3442
|
+
contract.memory === legacy.memory &&
|
|
3443
|
+
contract.disk === legacy.disk)?.[0];
|
|
3444
|
+
if (!matchingSize) {
|
|
3445
|
+
throw new UserError("Legacy --cpu/--memory/--disk values must exactly match a named sandbox size. Prefer --size.");
|
|
3446
|
+
}
|
|
3447
|
+
if (opts.size && opts.size !== matchingSize) {
|
|
3448
|
+
throw new UserError(`Legacy resource overrides match ${matchingSize}, not requested size ${opts.size}.`);
|
|
3449
|
+
}
|
|
3450
|
+
return { size: matchingSize, legacy };
|
|
3451
|
+
}
|
|
3292
3452
|
function parseSizeMb(value) {
|
|
3293
3453
|
const match = String(value)
|
|
3294
3454
|
.trim()
|