@miosa/cli 1.3.4 → 2.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 +20 -835
- package/bin/miosa.js +75 -0
- package/lib/archive.js +88 -0
- package/lib/checksums.js +29 -0
- package/lib/platforms.js +31 -0
- package/lib/resolve.js +47 -0
- package/package.json +26 -41
- package/scripts/postinstall.js +68 -0
- package/CHANGELOG.md +0 -110
- package/dist/action-authority.d.ts +0 -106
- package/dist/action-authority.d.ts.map +0 -1
- package/dist/action-authority.js +0 -116
- package/dist/action-authority.js.map +0 -1
- package/dist/app-advisor.d.ts +0 -80
- package/dist/app-advisor.d.ts.map +0 -1
- package/dist/app-advisor.js +0 -486
- package/dist/app-advisor.js.map +0 -1
- package/dist/app-contract.d.ts +0 -92
- package/dist/app-contract.d.ts.map +0 -1
- package/dist/app-contract.js +0 -218
- package/dist/app-contract.js.map +0 -1
- package/dist/app-document.d.ts +0 -23
- package/dist/app-document.d.ts.map +0 -1
- package/dist/app-document.js +0 -180
- package/dist/app-document.js.map +0 -1
- package/dist/app-manifest.d.ts +0 -140
- package/dist/app-manifest.d.ts.map +0 -1
- package/dist/app-manifest.js +0 -377
- package/dist/app-manifest.js.map +0 -1
- package/dist/app-operation.d.ts +0 -23
- package/dist/app-operation.d.ts.map +0 -1
- package/dist/app-operation.js +0 -49
- package/dist/app-operation.js.map +0 -1
- package/dist/app-release.d.ts +0 -132
- package/dist/app-release.d.ts.map +0 -1
- package/dist/app-release.js +0 -294
- package/dist/app-release.js.map +0 -1
- package/dist/bin/miosa.d.ts +0 -3
- package/dist/bin/miosa.d.ts.map +0 -1
- package/dist/bin/miosa.js +0 -288
- package/dist/bin/miosa.js.map +0 -1
- package/dist/cli-env.d.ts +0 -12
- package/dist/cli-env.d.ts.map +0 -1
- package/dist/cli-env.js +0 -34
- package/dist/cli-env.js.map +0 -1
- package/dist/client.d.ts +0 -155
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -865
- package/dist/client.js.map +0 -1
- package/dist/commands/actions.d.ts +0 -3
- package/dist/commands/actions.d.ts.map +0 -1
- package/dist/commands/actions.js +0 -222
- package/dist/commands/actions.js.map +0 -1
- package/dist/commands/agent-runtime-profiles.d.ts +0 -3
- package/dist/commands/agent-runtime-profiles.d.ts.map +0 -1
- package/dist/commands/agent-runtime-profiles.js +0 -198
- package/dist/commands/agent-runtime-profiles.js.map +0 -1
- package/dist/commands/agent.d.ts +0 -18
- package/dist/commands/agent.d.ts.map +0 -1
- package/dist/commands/agent.js +0 -737
- package/dist/commands/agent.js.map +0 -1
- package/dist/commands/alerts.d.ts +0 -3
- package/dist/commands/alerts.d.ts.map +0 -1
- package/dist/commands/alerts.js +0 -41
- package/dist/commands/alerts.js.map +0 -1
- package/dist/commands/api-keys.d.ts +0 -3
- package/dist/commands/api-keys.d.ts.map +0 -1
- package/dist/commands/api-keys.js +0 -160
- package/dist/commands/api-keys.js.map +0 -1
- package/dist/commands/api-resource.d.ts +0 -20
- package/dist/commands/api-resource.d.ts.map +0 -1
- package/dist/commands/api-resource.js +0 -120
- package/dist/commands/api-resource.js.map +0 -1
- package/dist/commands/app.d.ts +0 -38
- package/dist/commands/app.d.ts.map +0 -1
- package/dist/commands/app.js +0 -1182
- package/dist/commands/app.js.map +0 -1
- package/dist/commands/apps.d.ts +0 -3
- package/dist/commands/apps.d.ts.map +0 -1
- package/dist/commands/apps.js +0 -218
- package/dist/commands/apps.js.map +0 -1
- package/dist/commands/audit.d.ts +0 -3
- package/dist/commands/audit.d.ts.map +0 -1
- package/dist/commands/audit.js +0 -25
- package/dist/commands/audit.js.map +0 -1
- package/dist/commands/auth.d.ts +0 -3
- package/dist/commands/auth.d.ts.map +0 -1
- package/dist/commands/auth.js +0 -282
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/backups.d.ts +0 -3
- package/dist/commands/backups.d.ts.map +0 -1
- package/dist/commands/backups.js +0 -23
- package/dist/commands/backups.js.map +0 -1
- package/dist/commands/billing.d.ts +0 -3
- package/dist/commands/billing.d.ts.map +0 -1
- package/dist/commands/billing.js +0 -208
- package/dist/commands/billing.js.map +0 -1
- package/dist/commands/builds.d.ts +0 -3
- package/dist/commands/builds.d.ts.map +0 -1
- package/dist/commands/builds.js +0 -198
- package/dist/commands/builds.js.map +0 -1
- package/dist/commands/capabilities.d.ts +0 -3
- package/dist/commands/capabilities.d.ts.map +0 -1
- package/dist/commands/capabilities.js +0 -1002
- package/dist/commands/capabilities.js.map +0 -1
- package/dist/commands/checkpoints.d.ts +0 -3
- package/dist/commands/checkpoints.d.ts.map +0 -1
- package/dist/commands/checkpoints.js +0 -193
- package/dist/commands/checkpoints.js.map +0 -1
- package/dist/commands/cleanup.d.ts +0 -3
- package/dist/commands/cleanup.d.ts.map +0 -1
- package/dist/commands/cleanup.js +0 -65
- package/dist/commands/cleanup.js.map +0 -1
- package/dist/commands/cloud.d.ts +0 -3
- package/dist/commands/cloud.d.ts.map +0 -1
- package/dist/commands/cloud.js +0 -192
- package/dist/commands/cloud.js.map +0 -1
- package/dist/commands/command-overview.d.ts +0 -13
- package/dist/commands/command-overview.d.ts.map +0 -1
- package/dist/commands/command-overview.js +0 -50
- package/dist/commands/command-overview.js.map +0 -1
- package/dist/commands/completion.d.ts +0 -3
- package/dist/commands/completion.d.ts.map +0 -1
- package/dist/commands/completion.js +0 -183
- package/dist/commands/completion.js.map +0 -1
- package/dist/commands/computers.d.ts +0 -3
- package/dist/commands/computers.d.ts.map +0 -1
- package/dist/commands/computers.js +0 -706
- package/dist/commands/computers.js.map +0 -1
- package/dist/commands/config.d.ts +0 -3
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -114
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/connect.d.ts +0 -6
- package/dist/commands/connect.d.ts.map +0 -1
- package/dist/commands/connect.js +0 -127
- package/dist/commands/connect.js.map +0 -1
- package/dist/commands/connectors.d.ts +0 -3
- package/dist/commands/connectors.d.ts.map +0 -1
- package/dist/commands/connectors.js +0 -172
- package/dist/commands/connectors.js.map +0 -1
- package/dist/commands/containers.d.ts +0 -3
- package/dist/commands/containers.d.ts.map +0 -1
- package/dist/commands/containers.js +0 -20
- package/dist/commands/containers.js.map +0 -1
- package/dist/commands/context.d.ts +0 -3
- package/dist/commands/context.d.ts.map +0 -1
- package/dist/commands/context.js +0 -233
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/cp.d.ts +0 -3
- package/dist/commands/cp.d.ts.map +0 -1
- package/dist/commands/cp.js +0 -319
- package/dist/commands/cp.js.map +0 -1
- package/dist/commands/cron.d.ts +0 -3
- package/dist/commands/cron.d.ts.map +0 -1
- package/dist/commands/cron.js +0 -65
- package/dist/commands/cron.js.map +0 -1
- package/dist/commands/dashboard.d.ts +0 -13
- package/dist/commands/dashboard.d.ts.map +0 -1
- package/dist/commands/dashboard.js +0 -54
- package/dist/commands/dashboard.js.map +0 -1
- package/dist/commands/databases.d.ts +0 -3
- package/dist/commands/databases.d.ts.map +0 -1
- package/dist/commands/databases.js +0 -714
- package/dist/commands/databases.js.map +0 -1
- package/dist/commands/db.d.ts +0 -3
- package/dist/commands/db.d.ts.map +0 -1
- package/dist/commands/db.js +0 -385
- package/dist/commands/db.js.map +0 -1
- package/dist/commands/deploy.d.ts +0 -3
- package/dist/commands/deploy.d.ts.map +0 -1
- package/dist/commands/deploy.js +0 -1234
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/desktop.d.ts +0 -3
- package/dist/commands/desktop.d.ts.map +0 -1
- package/dist/commands/desktop.js +0 -277
- package/dist/commands/desktop.js.map +0 -1
- package/dist/commands/dev.d.ts +0 -3
- package/dist/commands/dev.d.ts.map +0 -1
- package/dist/commands/dev.js +0 -245
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/devices.d.ts +0 -3
- package/dist/commands/devices.d.ts.map +0 -1
- package/dist/commands/devices.js +0 -279
- package/dist/commands/devices.js.map +0 -1
- package/dist/commands/docker-deploy.d.ts +0 -3
- package/dist/commands/docker-deploy.d.ts.map +0 -1
- package/dist/commands/docker-deploy.js +0 -659
- package/dist/commands/docker-deploy.js.map +0 -1
- package/dist/commands/doctor.d.ts +0 -69
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -596
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/domains.d.ts +0 -3
- package/dist/commands/domains.d.ts.map +0 -1
- package/dist/commands/domains.js +0 -307
- package/dist/commands/domains.js.map +0 -1
- package/dist/commands/enterprise-util.d.ts +0 -39
- package/dist/commands/enterprise-util.d.ts.map +0 -1
- package/dist/commands/enterprise-util.js +0 -217
- package/dist/commands/enterprise-util.js.map +0 -1
- package/dist/commands/env-input.d.ts +0 -26
- package/dist/commands/env-input.d.ts.map +0 -1
- package/dist/commands/env-input.js +0 -68
- package/dist/commands/env-input.js.map +0 -1
- package/dist/commands/env.d.ts +0 -3
- package/dist/commands/env.d.ts.map +0 -1
- package/dist/commands/env.js +0 -189
- package/dist/commands/env.js.map +0 -1
- package/dist/commands/exec.d.ts +0 -3
- package/dist/commands/exec.d.ts.map +0 -1
- package/dist/commands/exec.js +0 -110
- package/dist/commands/exec.js.map +0 -1
- package/dist/commands/forge.d.ts +0 -3
- package/dist/commands/forge.d.ts.map +0 -1
- package/dist/commands/forge.js +0 -254
- package/dist/commands/forge.js.map +0 -1
- package/dist/commands/functions.d.ts +0 -3
- package/dist/commands/functions.d.ts.map +0 -1
- package/dist/commands/functions.js +0 -47
- package/dist/commands/functions.js.map +0 -1
- package/dist/commands/gha-runners.d.ts +0 -3
- package/dist/commands/gha-runners.d.ts.map +0 -1
- package/dist/commands/gha-runners.js +0 -33
- package/dist/commands/gha-runners.js.map +0 -1
- package/dist/commands/groups.d.ts +0 -3
- package/dist/commands/groups.d.ts.map +0 -1
- package/dist/commands/groups.js +0 -38
- package/dist/commands/groups.js.map +0 -1
- package/dist/commands/host.d.ts +0 -3
- package/dist/commands/host.d.ts.map +0 -1
- package/dist/commands/host.js +0 -74
- package/dist/commands/host.js.map +0 -1
- package/dist/commands/hosts.d.ts +0 -7
- package/dist/commands/hosts.d.ts.map +0 -1
- package/dist/commands/hosts.js +0 -91
- package/dist/commands/hosts.js.map +0 -1
- package/dist/commands/link.d.ts +0 -8
- package/dist/commands/link.d.ts.map +0 -1
- package/dist/commands/link.js +0 -124
- package/dist/commands/link.js.map +0 -1
- package/dist/commands/login.d.ts +0 -10
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js +0 -263
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts +0 -3
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js +0 -43
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/logs.d.ts +0 -3
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/logs.js +0 -401
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/ls.d.ts +0 -3
- package/dist/commands/ls.d.ts.map +0 -1
- package/dist/commands/ls.js +0 -121
- package/dist/commands/ls.js.map +0 -1
- package/dist/commands/mcp.d.ts +0 -46
- package/dist/commands/mcp.d.ts.map +0 -1
- package/dist/commands/mcp.js +0 -3413
- package/dist/commands/mcp.js.map +0 -1
- package/dist/commands/menu.d.ts +0 -29
- package/dist/commands/menu.d.ts.map +0 -1
- package/dist/commands/menu.js +0 -147
- package/dist/commands/menu.js.map +0 -1
- package/dist/commands/meshes.d.ts +0 -3
- package/dist/commands/meshes.d.ts.map +0 -1
- package/dist/commands/meshes.js +0 -27
- package/dist/commands/meshes.js.map +0 -1
- package/dist/commands/network-policy.d.ts +0 -3
- package/dist/commands/network-policy.d.ts.map +0 -1
- package/dist/commands/network-policy.js +0 -155
- package/dist/commands/network-policy.js.map +0 -1
- package/dist/commands/new.d.ts +0 -3
- package/dist/commands/new.d.ts.map +0 -1
- package/dist/commands/new.js +0 -296
- package/dist/commands/new.js.map +0 -1
- package/dist/commands/opencomputers.d.ts +0 -10
- package/dist/commands/opencomputers.d.ts.map +0 -1
- package/dist/commands/opencomputers.js +0 -25
- package/dist/commands/opencomputers.js.map +0 -1
- package/dist/commands/operating-contract.d.ts +0 -3
- package/dist/commands/operating-contract.d.ts.map +0 -1
- package/dist/commands/operating-contract.js +0 -651
- package/dist/commands/operating-contract.js.map +0 -1
- package/dist/commands/project.d.ts +0 -4
- package/dist/commands/project.d.ts.map +0 -1
- package/dist/commands/project.js +0 -25
- package/dist/commands/project.js.map +0 -1
- package/dist/commands/pull.d.ts +0 -14
- package/dist/commands/pull.d.ts.map +0 -1
- package/dist/commands/pull.js +0 -156
- package/dist/commands/pull.js.map +0 -1
- package/dist/commands/regions.d.ts +0 -3
- package/dist/commands/regions.d.ts.map +0 -1
- package/dist/commands/regions.js +0 -103
- package/dist/commands/regions.js.map +0 -1
- package/dist/commands/releases.d.ts +0 -3
- package/dist/commands/releases.d.ts.map +0 -1
- package/dist/commands/releases.js +0 -365
- package/dist/commands/releases.js.map +0 -1
- package/dist/commands/rm.d.ts +0 -3
- package/dist/commands/rm.d.ts.map +0 -1
- package/dist/commands/rm.js +0 -42
- package/dist/commands/rm.js.map +0 -1
- package/dist/commands/rollback.d.ts +0 -3
- package/dist/commands/rollback.d.ts.map +0 -1
- package/dist/commands/rollback.js +0 -62
- package/dist/commands/rollback.js.map +0 -1
- package/dist/commands/run-groups.d.ts +0 -3
- package/dist/commands/run-groups.d.ts.map +0 -1
- package/dist/commands/run-groups.js +0 -364
- package/dist/commands/run-groups.js.map +0 -1
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands/run.js +0 -130
- package/dist/commands/run.js.map +0 -1
- package/dist/commands/runs.d.ts +0 -3
- package/dist/commands/runs.d.ts.map +0 -1
- package/dist/commands/runs.js +0 -365
- package/dist/commands/runs.js.map +0 -1
- package/dist/commands/runtime-env.d.ts +0 -3
- package/dist/commands/runtime-env.d.ts.map +0 -1
- package/dist/commands/runtime-env.js +0 -114
- package/dist/commands/runtime-env.js.map +0 -1
- package/dist/commands/sandbox-delete-guard.d.ts +0 -4
- package/dist/commands/sandbox-delete-guard.d.ts.map +0 -1
- package/dist/commands/sandbox-delete-guard.js +0 -32
- package/dist/commands/sandbox-delete-guard.js.map +0 -1
- package/dist/commands/sandbox-dev.d.ts +0 -20
- package/dist/commands/sandbox-dev.d.ts.map +0 -1
- package/dist/commands/sandbox-dev.js +0 -583
- package/dist/commands/sandbox-dev.js.map +0 -1
- package/dist/commands/sandbox.d.ts +0 -36
- package/dist/commands/sandbox.d.ts.map +0 -1
- package/dist/commands/sandbox.js +0 -3796
- package/dist/commands/sandbox.js.map +0 -1
- package/dist/commands/scale.d.ts +0 -3
- package/dist/commands/scale.d.ts.map +0 -1
- package/dist/commands/scale.js +0 -61
- package/dist/commands/scale.js.map +0 -1
- package/dist/commands/schedules.d.ts +0 -3
- package/dist/commands/schedules.d.ts.map +0 -1
- package/dist/commands/schedules.js +0 -37
- package/dist/commands/schedules.js.map +0 -1
- package/dist/commands/secrets.d.ts +0 -3
- package/dist/commands/secrets.d.ts.map +0 -1
- package/dist/commands/secrets.js +0 -200
- package/dist/commands/secrets.js.map +0 -1
- package/dist/commands/services.d.ts +0 -3
- package/dist/commands/services.d.ts.map +0 -1
- package/dist/commands/services.js +0 -70
- package/dist/commands/services.js.map +0 -1
- package/dist/commands/shell.d.ts +0 -16
- package/dist/commands/shell.d.ts.map +0 -1
- package/dist/commands/shell.js +0 -555
- package/dist/commands/shell.js.map +0 -1
- package/dist/commands/snapshot.d.ts +0 -10
- package/dist/commands/snapshot.d.ts.map +0 -1
- package/dist/commands/snapshot.js +0 -181
- package/dist/commands/snapshot.js.map +0 -1
- package/dist/commands/ssh.d.ts +0 -3
- package/dist/commands/ssh.d.ts.map +0 -1
- package/dist/commands/ssh.js +0 -93
- package/dist/commands/ssh.js.map +0 -1
- package/dist/commands/status.d.ts +0 -3
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js +0 -302
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/storage.d.ts +0 -3
- package/dist/commands/storage.d.ts.map +0 -1
- package/dist/commands/storage.js +0 -456
- package/dist/commands/storage.js.map +0 -1
- package/dist/commands/teams.d.ts +0 -3
- package/dist/commands/teams.d.ts.map +0 -1
- package/dist/commands/teams.js +0 -169
- package/dist/commands/teams.js.map +0 -1
- package/dist/commands/templates.d.ts +0 -3
- package/dist/commands/templates.d.ts.map +0 -1
- package/dist/commands/templates.js +0 -690
- package/dist/commands/templates.js.map +0 -1
- package/dist/commands/tenant.d.ts +0 -3
- package/dist/commands/tenant.d.ts.map +0 -1
- package/dist/commands/tenant.js +0 -95
- package/dist/commands/tenant.js.map +0 -1
- package/dist/commands/tunnel.d.ts +0 -3
- package/dist/commands/tunnel.d.ts.map +0 -1
- package/dist/commands/tunnel.js +0 -418
- package/dist/commands/tunnel.js.map +0 -1
- package/dist/commands/up.d.ts +0 -14
- package/dist/commands/up.d.ts.map +0 -1
- package/dist/commands/up.js +0 -720
- package/dist/commands/up.js.map +0 -1
- package/dist/commands/update.d.ts +0 -3
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js +0 -166
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/util.d.ts +0 -25
- package/dist/commands/util.d.ts.map +0 -1
- package/dist/commands/util.js +0 -199
- package/dist/commands/util.js.map +0 -1
- package/dist/commands/volumes.d.ts +0 -3
- package/dist/commands/volumes.d.ts.map +0 -1
- package/dist/commands/volumes.js +0 -196
- package/dist/commands/volumes.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -6
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js +0 -414
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/webhooks.d.ts +0 -3
- package/dist/commands/webhooks.d.ts.map +0 -1
- package/dist/commands/webhooks.js +0 -23
- package/dist/commands/webhooks.js.map +0 -1
- package/dist/commands/whoami.d.ts +0 -3
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js +0 -156
- package/dist/commands/whoami.js.map +0 -1
- package/dist/commands/workspaces.d.ts +0 -3
- package/dist/commands/workspaces.d.ts.map +0 -1
- package/dist/commands/workspaces.js +0 -147
- package/dist/commands/workspaces.js.map +0 -1
- package/dist/config.d.ts +0 -54
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -275
- package/dist/config.js.map +0 -1
- package/dist/errors.d.ts +0 -30
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -92
- package/dist/errors.js.map +0 -1
- package/dist/framework-detector.d.ts +0 -22
- package/dist/framework-detector.d.ts.map +0 -1
- package/dist/framework-detector.js +0 -373
- package/dist/framework-detector.js.map +0 -1
- package/dist/generated/action-capabilities.d.ts +0 -826
- package/dist/generated/action-capabilities.d.ts.map +0 -1
- package/dist/generated/action-capabilities.js +0 -1035
- package/dist/generated/action-capabilities.js.map +0 -1
- package/dist/net-diagnosis.d.ts +0 -87
- package/dist/net-diagnosis.d.ts.map +0 -1
- package/dist/net-diagnosis.js +0 -408
- package/dist/net-diagnosis.js.map +0 -1
- package/dist/pty/raw-mode.d.ts +0 -7
- package/dist/pty/raw-mode.d.ts.map +0 -1
- package/dist/pty/raw-mode.js +0 -22
- package/dist/pty/raw-mode.js.map +0 -1
- package/dist/pty/ws-pty-client.d.ts +0 -13
- package/dist/pty/ws-pty-client.d.ts.map +0 -1
- package/dist/pty/ws-pty-client.js +0 -73
- package/dist/pty/ws-pty-client.js.map +0 -1
- package/dist/tui/dashboard.d.ts +0 -35
- package/dist/tui/dashboard.d.ts.map +0 -1
- package/dist/tui/dashboard.js +0 -191
- package/dist/tui/dashboard.js.map +0 -1
- package/dist/tui/logs.d.ts +0 -26
- package/dist/tui/logs.d.ts.map +0 -1
- package/dist/tui/logs.js +0 -276
- package/dist/tui/logs.js.map +0 -1
- package/dist/types.d.ts +0 -362
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -16
- package/dist/types.js.map +0 -1
- package/dist/ui/picker.d.ts +0 -41
- package/dist/ui/picker.d.ts.map +0 -1
- package/dist/ui/picker.js +0 -77
- package/dist/ui/picker.js.map +0 -1
- package/dist/ui/progress.d.ts +0 -10
- package/dist/ui/progress.d.ts.map +0 -1
- package/dist/ui/progress.js +0 -36
- package/dist/ui/progress.js.map +0 -1
- package/dist/ui/render.d.ts +0 -91
- package/dist/ui/render.d.ts.map +0 -1
- package/dist/ui/render.js +0 -138
- package/dist/ui/render.js.map +0 -1
- package/dist/ui/spinner.d.ts +0 -4
- package/dist/ui/spinner.d.ts.map +0 -1
- package/dist/ui/spinner.js +0 -7
- package/dist/ui/spinner.js.map +0 -1
- package/dist/ui/table.d.ts +0 -8
- package/dist/ui/table.d.ts.map +0 -1
- package/dist/ui/table.js +0 -46
- package/dist/ui/table.js.map +0 -1
- package/dist/update-check.d.ts +0 -13
- package/dist/update-check.d.ts.map +0 -1
- package/dist/update-check.js +0 -94
- package/dist/update-check.js.map +0 -1
- package/dist/version.d.ts +0 -3
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -5
- package/dist/version.js.map +0 -1
package/dist/commands/sandbox.js
DELETED
|
@@ -1,3796 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
-
import { createServer } from "node:net";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import * as http from "node:http";
|
|
8
|
-
import * as https from "node:https";
|
|
9
|
-
import chalk from "chalk";
|
|
10
|
-
import WebSocket from "ws";
|
|
11
|
-
import { loadAppManifest, manifestPort, manifestProbePath, manifestStartCommand, parseAppManifest, } from "../app-manifest.js";
|
|
12
|
-
import { detectFramework } from "../framework-detector.js";
|
|
13
|
-
import { addDataOption, client, apiPath, deleteAndPrint, enc, getAndPrint, parseData, postAndPrint, printValue, runAction, unwrap, } from "./enterprise-util.js";
|
|
14
|
-
import { loadConfig } from "../config.js";
|
|
15
|
-
import { assertDeletableRemoteDir } from "./sandbox-delete-guard.js";
|
|
16
|
-
import { ENV_FILE_OPTION_HELP, ENV_INLINE_SHELL_WARNING, ENV_STDIN_OPTION_HELP, resolveEnvInputs, } from "./env-input.js";
|
|
17
|
-
import { handleError, isJsonMode, printJson } from "./util.js";
|
|
18
|
-
import { renderTable } from "../ui/table.js";
|
|
19
|
-
import { formatDuration, hintBlock, icon, kvPanel, printBanner, printElapsed, } from "../ui/render.js";
|
|
20
|
-
import { formatBytes } from "../ui/progress.js";
|
|
21
|
-
import { ApiResponseError, MiosaError, NetworkError, ServerError, UserError, mapHttpError, } from "../errors.js";
|
|
22
|
-
import { EXIT_USER_ERROR } from "../types.js";
|
|
23
|
-
import { registerSandboxDevCommands, runFullSandboxDoctor, } from "./sandbox-dev.js";
|
|
24
|
-
const DEFAULT_INTERACTIVE_SANDBOX_TIMEOUT_SEC = 3_600;
|
|
25
|
-
const DEFAULT_CREATE_WAIT_TIMEOUT_SEC = 120;
|
|
26
|
-
const EXPIRING_SANDBOX_THRESHOLD_SEC = 5 * 60;
|
|
27
|
-
// How long, after a sandbox reaches `running`, to keep polling for
|
|
28
|
-
// command-readiness before `--wait` gives up.
|
|
29
|
-
const COMMAND_READY_WAIT_TIMEOUT_SEC = 60;
|
|
30
|
-
const COMMAND_READY_POLL_INTERVAL_MS = 500;
|
|
31
|
-
// Platform readiness components that gate command execution. Mirrors the
|
|
32
|
-
// server's `Engine.Sandbox.Readiness` platform set: `exec` returns
|
|
33
|
-
// `409 SANDBOX_NOT_COMMAND_READY` until all of these report `ready`. Excludes
|
|
34
|
-
// the template `application_probe` (the workload), which is started BY exec.
|
|
35
|
-
const PLATFORM_READINESS_COMPONENTS = [
|
|
36
|
-
"command_agent",
|
|
37
|
-
"process_control",
|
|
38
|
-
"resource_contract",
|
|
39
|
-
"clock",
|
|
40
|
-
"identity",
|
|
41
|
-
"durability",
|
|
42
|
-
"filesystem",
|
|
43
|
-
"internal_probe",
|
|
44
|
-
];
|
|
45
|
-
/**
|
|
46
|
-
* True when a sandbox is command-ready — `exec` will succeed rather than return
|
|
47
|
-
* `409 SANDBOX_NOT_COMMAND_READY`. Accepts a readiness payload
|
|
48
|
-
* (`GET /sandboxes/:id/readiness`) or a sandbox record. Mirrors the server's
|
|
49
|
-
* `command_path_ready?/1`: the aggregate `ready` flag short-circuits, otherwise
|
|
50
|
-
* every platform component must report `ready`.
|
|
51
|
-
*/
|
|
52
|
-
function isCommandReady(payload) {
|
|
53
|
-
if (!payload || typeof payload !== "object")
|
|
54
|
-
return false;
|
|
55
|
-
const p = payload;
|
|
56
|
-
if (p["ready"] === true)
|
|
57
|
-
return true;
|
|
58
|
-
const readiness = p["readiness"];
|
|
59
|
-
const components = (readiness?.["components"] ??
|
|
60
|
-
p["components"] ??
|
|
61
|
-
p["readiness_components"]);
|
|
62
|
-
if (!components || typeof components !== "object")
|
|
63
|
-
return false;
|
|
64
|
-
return PLATFORM_READINESS_COMPONENTS.every((name) => {
|
|
65
|
-
const comp = components[name];
|
|
66
|
-
const status = typeof comp === "string" ? comp : comp?.status;
|
|
67
|
-
return status === "ready";
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Generate a create Idempotency-Key. Generated once per logical `create` and
|
|
72
|
-
* re-sent unchanged on every retry (see `MiosaClient.apiPostWithRetry`), so a
|
|
73
|
-
* retried create dedups to the same sandbox instead of billing a duplicate VM.
|
|
74
|
-
*/
|
|
75
|
-
function generateIdempotencyKey() {
|
|
76
|
-
return randomUUID();
|
|
77
|
-
}
|
|
78
|
-
const SANDBOX_SIZE_CONTRACTS = {
|
|
79
|
-
xs: { cpu: 1, memory: 2_048, disk: 10_240 },
|
|
80
|
-
small: { cpu: 2, memory: 4_096, disk: 10_240 },
|
|
81
|
-
medium: { cpu: 4, memory: 8_192, disk: 20_480 },
|
|
82
|
-
large: { cpu: 8, memory: 16_384, disk: 40_960 },
|
|
83
|
-
xl: { cpu: 16, memory: 32_768, disk: 81_920 },
|
|
84
|
-
};
|
|
85
|
-
export function register(program) {
|
|
86
|
-
// -------------------------------------------------------------------------
|
|
87
|
-
// sandbox / sandboxes command group — built manually to avoid subcommand
|
|
88
|
-
// conflicts that arise when using the resourceCommands helper alongside
|
|
89
|
-
// custom create/exec/file subcommands.
|
|
90
|
-
// -------------------------------------------------------------------------
|
|
91
|
-
const sandbox = program
|
|
92
|
-
.command("sandbox")
|
|
93
|
-
.alias("sandboxes")
|
|
94
|
-
.description("Manage Sandboxes — lightweight code-only Computers (Firecracker microVMs without a desktop)");
|
|
95
|
-
registerSandboxDevCommands(sandbox);
|
|
96
|
-
// migrate — point a sandbox at a different template version.
|
|
97
|
-
//
|
|
98
|
-
// PREVIEW BY DEFAULT. Migrating replaces the sandbox's root filesystem, so
|
|
99
|
-
// the destructive path is behind an explicit --confirm rather than being what
|
|
100
|
-
// happens when you forget a flag. Ross/HackerAI asked for exactly this shape:
|
|
101
|
-
// a preview of what is preserved, explicit confirmation, the original kept
|
|
102
|
-
// until the replacement is ready, and rollback.
|
|
103
|
-
sandbox
|
|
104
|
-
.command("migrate <id>")
|
|
105
|
-
.description("Preview (default) or perform moving a Sandbox to another template version")
|
|
106
|
-
.requiredOption("--template <ref>", "Target version: v3, <template>@v3, a build id, or 'latest' (latest USABLE version)")
|
|
107
|
-
.option("--confirm", "Actually perform the migration. Without this, nothing is changed.")
|
|
108
|
-
.option("--json", "Output raw JSON")
|
|
109
|
-
.action(async (id, opts) => {
|
|
110
|
-
try {
|
|
111
|
-
const json = isJsonMode(opts);
|
|
112
|
-
const body = {
|
|
113
|
-
template_version: opts.template,
|
|
114
|
-
confirm: Boolean(opts.confirm),
|
|
115
|
-
};
|
|
116
|
-
const res = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/migrate`), body));
|
|
117
|
-
if (json) {
|
|
118
|
-
printJson(res);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const plan = (res["plan"] ?? res);
|
|
122
|
-
const from = (plan["from"] ?? {});
|
|
123
|
-
const to = (plan["to"] ?? {});
|
|
124
|
-
console.log();
|
|
125
|
-
console.log(` ${chalk.bold("Migration")} ${chalk.dim(str(from["image_id"]))} ${chalk.dim("->")} ${chalk.bold(`v${str(to["version"])}`)} ${chalk.dim(str(to["image_id"]))}`);
|
|
126
|
-
const preserved = (plan["preserved"] ?? []);
|
|
127
|
-
const lost = (plan["lost"] ?? []);
|
|
128
|
-
if (preserved.length > 0) {
|
|
129
|
-
console.log();
|
|
130
|
-
console.log(` ${chalk.green("Preserved")}`);
|
|
131
|
-
for (const p of preserved) {
|
|
132
|
-
console.log(` ${icon.ok} ${str(p["item"])} ${chalk.dim(str(p["detail"]))}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// Printed with the same weight as the preserved list, deliberately.
|
|
136
|
-
// A preview that only shows survivors invites you to assume the rest
|
|
137
|
-
// survives too.
|
|
138
|
-
if (lost.length > 0) {
|
|
139
|
-
console.log();
|
|
140
|
-
console.log(` ${chalk.red("NOT preserved")}`);
|
|
141
|
-
for (const l of lost) {
|
|
142
|
-
console.log(` ${icon.fail} ${str(l["item"])} ${chalk.dim(str(l["detail"]))}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const warnings = (plan["warnings"] ?? []);
|
|
146
|
-
for (const w of warnings) {
|
|
147
|
-
console.log();
|
|
148
|
-
console.log(` ${chalk.yellow("Warning:")} ${w}`);
|
|
149
|
-
}
|
|
150
|
-
console.log();
|
|
151
|
-
if (!opts.confirm) {
|
|
152
|
-
console.log(` ${chalk.dim("Nothing has changed. Re-run with --confirm to perform this migration.")}`);
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
console.log(` ${chalk.dim(str(res["note"]) || "the original Sandbox is preserved until the replacement is ready")}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
handleError(err);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
// list
|
|
163
|
-
sandbox
|
|
164
|
-
.command("list")
|
|
165
|
-
.description("List all Sandboxes")
|
|
166
|
-
.option("--state <state>", "Filter by state (running, paused, …)")
|
|
167
|
-
.option("--json", "Output as JSON")
|
|
168
|
-
.action((opts) => runAction(async () => {
|
|
169
|
-
const qs = opts.state ? `?state=${enc(opts.state)}` : "";
|
|
170
|
-
if (isJsonMode(opts)) {
|
|
171
|
-
await getAndPrint(`/sandboxes${qs}`, opts);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const raw = unwrap(await client().apiGet(apiPath(`/sandboxes${qs}`)));
|
|
175
|
-
const items = Array.isArray(raw)
|
|
176
|
-
? raw
|
|
177
|
-
: [];
|
|
178
|
-
console.log();
|
|
179
|
-
console.log(` ${icon.info} ${chalk.bold(String(items.length))} ${chalk.dim("sandbox(es)")}`);
|
|
180
|
-
console.log();
|
|
181
|
-
if (items.length === 0) {
|
|
182
|
-
console.log(kvPanel([
|
|
183
|
-
{ label: "Sandboxes", value: chalk.dim("0 — none created yet") },
|
|
184
|
-
]));
|
|
185
|
-
console.log();
|
|
186
|
-
console.log(hintBlock("Try", ["miosa sandbox create"]));
|
|
187
|
-
console.log();
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
renderTable(items, [
|
|
191
|
-
{ header: "ID", key: "id" },
|
|
192
|
-
{ header: "NAME", key: "name" },
|
|
193
|
-
{
|
|
194
|
-
header: "STATUS",
|
|
195
|
-
key: "state",
|
|
196
|
-
color: (val) => statusColor(val.trim()),
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
header: "TEMPLATE",
|
|
200
|
-
key: "template_id",
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
header: "CREATED",
|
|
204
|
-
key: "created_at",
|
|
205
|
-
},
|
|
206
|
-
]);
|
|
207
|
-
console.log();
|
|
208
|
-
console.log(hintBlock("Try", [
|
|
209
|
-
"miosa sandbox show <id>",
|
|
210
|
-
"miosa sandbox exec <id> --command ...",
|
|
211
|
-
"miosa sandbox create",
|
|
212
|
-
]));
|
|
213
|
-
console.log();
|
|
214
|
-
}));
|
|
215
|
-
// ls — muscle-memory alias for list
|
|
216
|
-
sandbox
|
|
217
|
-
.command("ls")
|
|
218
|
-
.description("Alias for list")
|
|
219
|
-
.option("--json", "Output as JSON")
|
|
220
|
-
.action((opts) => runAction(() => getAndPrint("/sandboxes", opts)));
|
|
221
|
-
// show — canonical resourceCommands-style name
|
|
222
|
-
sandbox
|
|
223
|
-
.command("show <sandbox-id>")
|
|
224
|
-
.description("Show a Sandbox by ID")
|
|
225
|
-
.option("--port <port>", "Include live preview readiness for this port", parseIntegerOption)
|
|
226
|
-
.option("--probe-path <path>", "HTTP path to probe when --port is set", "/")
|
|
227
|
-
.option("--json", "Output as JSON")
|
|
228
|
-
.action((id, opts) => runAction(async () => {
|
|
229
|
-
if (isJsonMode(opts)) {
|
|
230
|
-
const data = opts.port != null
|
|
231
|
-
? await showSandboxWithPreview(id, opts.port, opts.probePath)
|
|
232
|
-
: unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}`)));
|
|
233
|
-
console.log(JSON.stringify(data, null, 2));
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const raw = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}`)));
|
|
237
|
-
const sb = (raw ?? {});
|
|
238
|
-
printBanner({ subtitle: "Sandbox" });
|
|
239
|
-
const rows = [
|
|
240
|
-
{ label: "ID", value: chalk.bold(str(sb["id"])), icon: icon.info },
|
|
241
|
-
{ label: "Name", value: str(sb["name"]) },
|
|
242
|
-
{
|
|
243
|
-
label: "Status",
|
|
244
|
-
value: statusColor(str(sb["state"])),
|
|
245
|
-
},
|
|
246
|
-
];
|
|
247
|
-
if (sb["template_id"]) {
|
|
248
|
-
rows.push({ label: "Template", value: str(sb["template_id"]) });
|
|
249
|
-
}
|
|
250
|
-
if (sb["cpu_count"] != null) {
|
|
251
|
-
rows.push({ label: "CPU", value: str(sb["cpu_count"]) });
|
|
252
|
-
}
|
|
253
|
-
if (sb["memory_mb"] != null) {
|
|
254
|
-
rows.push({
|
|
255
|
-
label: "Memory",
|
|
256
|
-
value: `${str(sb["memory_mb"])} MB`,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
if (sb["timeout_sec"] != null) {
|
|
260
|
-
rows.push({
|
|
261
|
-
label: "Timeout",
|
|
262
|
-
value: `${str(sb["timeout_sec"])} sec`,
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
if (sb["ip_address"]) {
|
|
266
|
-
rows.push({ label: "IP", value: str(sb["ip_address"]) });
|
|
267
|
-
}
|
|
268
|
-
if (sb["public_url"]) {
|
|
269
|
-
rows.push({
|
|
270
|
-
label: "URL",
|
|
271
|
-
value: chalk.cyan(str(sb["public_url"])),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
if (sb["created_at"]) {
|
|
275
|
-
rows.push({
|
|
276
|
-
label: "Created",
|
|
277
|
-
value: chalk.dim(str(sb["created_at"])),
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
console.log(kvPanel(rows));
|
|
281
|
-
console.log();
|
|
282
|
-
warnIfExpiringSoon(sb, str(sb["id"]));
|
|
283
|
-
console.log(hintBlock("Try", [
|
|
284
|
-
`miosa sandbox exec ${str(sb["id"])} --command ...`,
|
|
285
|
-
`miosa sandbox preview ${str(sb["id"])} --port 5173 --wait`,
|
|
286
|
-
`miosa sandbox destroy ${str(sb["id"])}`,
|
|
287
|
-
]));
|
|
288
|
-
console.log();
|
|
289
|
-
}));
|
|
290
|
-
// get — matches documented form: miosa sandboxes get <id>
|
|
291
|
-
sandbox
|
|
292
|
-
.command("get <sandbox-id>")
|
|
293
|
-
.description("Show a Sandbox by ID (alias for show)")
|
|
294
|
-
.option("--json", "Output as JSON")
|
|
295
|
-
.action((id, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}`, opts)));
|
|
296
|
-
// create — typed flags for common options; --data JSON overrides all flags
|
|
297
|
-
addDataOption(sandbox
|
|
298
|
-
.command("create")
|
|
299
|
-
.description("Create a new Sandbox")
|
|
300
|
-
.option("--template <template>", "Template / image ID (default: miosa-sandbox)")
|
|
301
|
-
.option("--name <name>", "Human-readable name for the Sandbox")
|
|
302
|
-
.option("--size <size>", "Named size: xs, small, medium, large, or xl (default: small)", parseSandboxSize)
|
|
303
|
-
.option("--cpu <n>", "vCPU count", parseIntegerOption)
|
|
304
|
-
.option("--memory <size>", "Memory size, e.g. 4096mb or 4gb", parseSizeMb)
|
|
305
|
-
.option("--disk <size>", "Disk size, e.g. 10240mb or 10gb", parseSizeMb)
|
|
306
|
-
.option("--timeout <duration>", "Wall-clock timeout, e.g. 300s, 1h", parseDurationSec)
|
|
307
|
-
.option("--idle-timeout <duration>", "Idle timeout before pause; 0 disables it (default: 0)", parseDurationSec)
|
|
308
|
-
.option("--idempotency-key <key>", "Retry-safe create key retained by the service for 24 hours")
|
|
309
|
-
.option("--publish-port <port>", "Expose this port after create", parseIntegerOption)
|
|
310
|
-
.option("--wait", "Wait for sandbox running and published port readiness")
|
|
311
|
-
.option("--probe-path <path>", "HTTP path to probe when --publish-port is set", "/")
|
|
312
|
-
.option("--source <source>", "Source: git:https://..., tarball:https://..., or snapshot:<id>")
|
|
313
|
-
.option("--revision <revision>", "Git revision/branch when --source git: is used")
|
|
314
|
-
.option("--depth <n>", "Git clone depth when --source git: is used", parseIntegerOption)
|
|
315
|
-
.option("--snapshot <id>", "Create from a sandbox snapshot")
|
|
316
|
-
.option("--workspace <id-or-slug>", "Workspace ID/slug")
|
|
317
|
-
.option("--external-workspace <id>", "White-label workspace/customer ID for billing attribution")
|
|
318
|
-
.option("--external-user <id>", "White-label user ID for billing attribution")
|
|
319
|
-
.option("--external-project <id>", "White-label project ID for billing attribution")
|
|
320
|
-
.option("--agent-profile <id>", "Agent runtime profile ID to mount into the sandbox")
|
|
321
|
-
.option("--skip-agent-profile", "Do not apply the tenant/workspace default agent runtime profile")
|
|
322
|
-
.option("--network-policy <policy>", "Network policy: allow-all or deny-all")
|
|
323
|
-
.option("--allowed-domain <domain>", "Allowed egress domain. Repeatable.", collectOption, [])
|
|
324
|
-
.option("--allowed-cidr <cidr>", "Allowed egress CIDR. Repeatable.", collectOption, [])
|
|
325
|
-
.option("--denied-cidr <cidr>", "Denied egress CIDR. Repeatable.", collectOption, [])
|
|
326
|
-
.option("--always-on", "Keep the sandbox running until explicitly stopped or destroyed")
|
|
327
|
-
.option("--non-persistent", "Discard filesystem state on timeout instead of pausing for resume")
|
|
328
|
-
.option("--auto-start", "Seed and start the template app after the sandbox reaches running"))
|
|
329
|
-
.option("--json", "Output as JSON")
|
|
330
|
-
.addHelpText("after", `
|
|
331
|
-
Note:
|
|
332
|
-
White-label preview domains require external attribution at creation.
|
|
333
|
-
Pass --external-workspace / --external-user / --external-project when the
|
|
334
|
-
sandbox will be exposed on a white-label preview domain.
|
|
335
|
-
`)
|
|
336
|
-
.action((opts) => runAction(async () => {
|
|
337
|
-
const t0 = Date.now();
|
|
338
|
-
const json = !!isJsonMode(opts) || process.env["MIOSA_JSON"] === "1";
|
|
339
|
-
if (opts.data) {
|
|
340
|
-
// Raw --data/--input/--file body — still idempotent and retry-safe.
|
|
341
|
-
const idempotencyKey = opts.idempotencyKey ?? generateIdempotencyKey();
|
|
342
|
-
const dataBody = parseData(opts.data, opts.input, opts.file) ?? {};
|
|
343
|
-
const value = unwrap(await client().apiPostWithRetry(apiPath("/sandboxes"), dataBody, { "Idempotency-Key": idempotencyKey }));
|
|
344
|
-
if (json) {
|
|
345
|
-
printValue(value, opts);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
renderCreateSuccess(value, Date.now() - t0);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
const body = {
|
|
352
|
-
template_id: opts.template ?? "miosa-sandbox",
|
|
353
|
-
};
|
|
354
|
-
if (opts.name)
|
|
355
|
-
body["name"] = opts.name;
|
|
356
|
-
const resources = resolveSandboxResources(opts);
|
|
357
|
-
if (resources.size)
|
|
358
|
-
body["size"] = resources.size;
|
|
359
|
-
if (resources.legacy) {
|
|
360
|
-
body["cpu_count"] = resources.legacy.cpu;
|
|
361
|
-
body["memory_mb"] = resources.legacy.memory;
|
|
362
|
-
body["disk_size_mb"] = resources.legacy.disk;
|
|
363
|
-
}
|
|
364
|
-
const timeoutSec = opts.timeout ?? DEFAULT_INTERACTIVE_SANDBOX_TIMEOUT_SEC;
|
|
365
|
-
body["timeout_sec"] = timeoutSec;
|
|
366
|
-
body["idle_timeout_sec"] = opts.idleTimeout ?? 0;
|
|
367
|
-
if (opts.source)
|
|
368
|
-
body["source"] = opts.source;
|
|
369
|
-
if (opts.revision)
|
|
370
|
-
body["revision"] = opts.revision;
|
|
371
|
-
if (opts.depth != null)
|
|
372
|
-
body["depth"] = opts.depth;
|
|
373
|
-
if (opts.snapshot)
|
|
374
|
-
body["snapshot_id"] = opts.snapshot;
|
|
375
|
-
if (opts.agentProfile) {
|
|
376
|
-
body["agent_runtime_profile_id"] = opts.agentProfile;
|
|
377
|
-
}
|
|
378
|
-
if (opts.skipAgentProfile) {
|
|
379
|
-
body["skip_agent_runtime_profile"] = true;
|
|
380
|
-
}
|
|
381
|
-
const workspace = opts.workspace ??
|
|
382
|
-
program.opts().workspace ??
|
|
383
|
-
process.env["MIOSA_WORKSPACE"];
|
|
384
|
-
if (workspace)
|
|
385
|
-
body["workspace_id"] = workspace;
|
|
386
|
-
if (opts.externalWorkspace) {
|
|
387
|
-
body["external_workspace_id"] = opts.externalWorkspace;
|
|
388
|
-
}
|
|
389
|
-
if (opts.externalUser) {
|
|
390
|
-
body["external_user_id"] = opts.externalUser;
|
|
391
|
-
}
|
|
392
|
-
if (opts.externalProject) {
|
|
393
|
-
body["external_project_id"] = opts.externalProject;
|
|
394
|
-
}
|
|
395
|
-
const networkPolicy = buildNetworkPolicy(opts);
|
|
396
|
-
if (networkPolicy) {
|
|
397
|
-
body["metadata"] = {
|
|
398
|
-
...(body["metadata"] ??
|
|
399
|
-
{}),
|
|
400
|
-
network_policy: networkPolicy,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
if (opts.alwaysOn)
|
|
404
|
-
body["always_on"] = true;
|
|
405
|
-
body["persistent"] = opts.nonPersistent ? false : true;
|
|
406
|
-
if (opts.autoStart)
|
|
407
|
-
body["auto_start"] = true;
|
|
408
|
-
// Auto-generate an Idempotency-Key when none is supplied, and retry
|
|
409
|
-
// on 429/5xx with the SAME key so a retried create dedups to one
|
|
410
|
-
// sandbox instead of billing a duplicate.
|
|
411
|
-
const idempotencyKey = opts.idempotencyKey ?? generateIdempotencyKey();
|
|
412
|
-
const raw = unwrap(await client().apiPostWithRetry(apiPath("/sandboxes"), body, {
|
|
413
|
-
"Idempotency-Key": idempotencyKey,
|
|
414
|
-
}));
|
|
415
|
-
const sb = (raw ?? {});
|
|
416
|
-
const id = String(sb["id"] ?? "");
|
|
417
|
-
if (opts.publishPort != null && id) {
|
|
418
|
-
if (opts.wait) {
|
|
419
|
-
const preview = await waitSandboxReady(id, opts.publishPort, opts.probePath ?? "/", Math.max(Math.min(opts.timeout ?? DEFAULT_CREATE_WAIT_TIMEOUT_SEC, DEFAULT_CREATE_WAIT_TIMEOUT_SEC), 30));
|
|
420
|
-
const latest = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}`)));
|
|
421
|
-
Object.assign(sb, latest, {
|
|
422
|
-
preview,
|
|
423
|
-
preview_url: preview.url,
|
|
424
|
-
ready: preview.ready,
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
const preview = await previewSandbox(id, opts.publishPort, {
|
|
429
|
-
wait: false,
|
|
430
|
-
timeout: 1,
|
|
431
|
-
probePath: opts.probePath ?? "/",
|
|
432
|
-
});
|
|
433
|
-
sb["preview"] = preview;
|
|
434
|
-
sb["preview_url"] = preview.url;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
else if (opts.wait && id) {
|
|
438
|
-
const latest = await waitForSandboxRunning(client(), id, Math.max(Math.min(timeoutSec, DEFAULT_CREATE_WAIT_TIMEOUT_SEC), 30),
|
|
439
|
-
// create --wait must guarantee exec works before returning.
|
|
440
|
-
true);
|
|
441
|
-
Object.assign(sb, latest, { ready: true });
|
|
442
|
-
}
|
|
443
|
-
if (json) {
|
|
444
|
-
console.log(JSON.stringify(sb, null, 2));
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
renderCreateSuccess(sb, Date.now() - t0);
|
|
448
|
-
}));
|
|
449
|
-
// delete
|
|
450
|
-
sandbox
|
|
451
|
-
.command("delete <sandbox-id>")
|
|
452
|
-
.description("Permanently delete a Sandbox (legacy API extension)")
|
|
453
|
-
.requiredOption("--force", "Confirm permanent filesystem deletion")
|
|
454
|
-
.option("--json", "Output as JSON")
|
|
455
|
-
.action((id, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}`, opts)));
|
|
456
|
-
// destroy / rm — aliases for delete
|
|
457
|
-
sandbox
|
|
458
|
-
.command("destroy <sandbox-id>")
|
|
459
|
-
.alias("rm")
|
|
460
|
-
.description("Permanently destroy a Sandbox (legacy API extension)")
|
|
461
|
-
.requiredOption("--force", "Confirm permanent filesystem deletion")
|
|
462
|
-
.option("--json", "Output as JSON")
|
|
463
|
-
.action((id, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}`, opts)));
|
|
464
|
-
// action — lifecycle operations: pause, resume
|
|
465
|
-
addDataOption(sandbox
|
|
466
|
-
.command("action <sandbox-id> <action>")
|
|
467
|
-
.description("Run a lifecycle action: pause, resume"))
|
|
468
|
-
.option("--json", "Output as JSON")
|
|
469
|
-
.action((id, action, opts) => runAction(async () => {
|
|
470
|
-
const allowed = ["pause", "resume"];
|
|
471
|
-
if (!allowed.includes(action)) {
|
|
472
|
-
throw new Error(`Unsupported action "${action}". Use: ${allowed.join(", ")}`);
|
|
473
|
-
}
|
|
474
|
-
await postAndPrint(`/sandboxes/${enc(id)}/${enc(action)}`, opts, {});
|
|
475
|
-
}));
|
|
476
|
-
sandbox
|
|
477
|
-
.command("pause <sandbox-id>")
|
|
478
|
-
.description("Pause a running persistent Sandbox and preserve its workspace")
|
|
479
|
-
.option("--json", "Output as JSON")
|
|
480
|
-
.action((id, opts) => runAction(() => postAndPrint(`/sandboxes/${enc(id)}/pause`, opts, {})));
|
|
481
|
-
// stop — snapshot + pause (mirrors `box stop`)
|
|
482
|
-
sandbox
|
|
483
|
-
.command("stop <sandbox-id>")
|
|
484
|
-
.description("Stop a persistent Sandbox session and preserve its filesystem")
|
|
485
|
-
.option("--no-snapshot", "Deprecated compatibility flag; stop still preserves state server-side")
|
|
486
|
-
.option("--json", "Output as JSON")
|
|
487
|
-
.action((id, opts) => runAction(async () => {
|
|
488
|
-
const stopped = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/stop`), {}));
|
|
489
|
-
if (isJsonMode(opts)) {
|
|
490
|
-
console.log(JSON.stringify(stopped, null, 2));
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
const row = (stopped ?? {});
|
|
494
|
-
console.log(chalk.green("Sandbox stopped"));
|
|
495
|
-
console.log(kvPanel([
|
|
496
|
-
{ label: "ID", value: str(row["id"] ?? id) },
|
|
497
|
-
{
|
|
498
|
-
label: "State",
|
|
499
|
-
value: statusColor(str(row["state"] ?? "paused")),
|
|
500
|
-
},
|
|
501
|
-
{ label: "Resume", value: `miosa sandbox resume ${id}` },
|
|
502
|
-
{ label: "Destroy", value: `miosa sandbox destroy ${id}` },
|
|
503
|
-
]));
|
|
504
|
-
}));
|
|
505
|
-
// resume — direct shortcut (mirrors `box resume`)
|
|
506
|
-
sandbox
|
|
507
|
-
.command("resume <sandbox-id>")
|
|
508
|
-
.description("Resume a paused Sandbox")
|
|
509
|
-
.option("--idempotency-key <key>", "Retry-safe lifecycle key sent as Idempotency-Key")
|
|
510
|
-
.option("--json", "Output as JSON")
|
|
511
|
-
.action((id, opts) => runAction(() => resumeSandboxAndPrint(id, opts)));
|
|
512
|
-
// fork — clone from snapshot in one call (mirrors `box fork`)
|
|
513
|
-
sandbox
|
|
514
|
-
.command("fork <sandbox-id>")
|
|
515
|
-
.description("Snapshot and fork a running Sandbox into a new Sandbox")
|
|
516
|
-
.option("--template <template>", "Template or immutable image override")
|
|
517
|
-
.option("--timeout <duration>", "Fork timeout", parseDurationSec)
|
|
518
|
-
.option("--idempotency-key <key>", "Retry-safe fork key sent as Idempotency-Key")
|
|
519
|
-
.option("--json", "Output as JSON")
|
|
520
|
-
.action((id, opts) => runAction(async () => {
|
|
521
|
-
const body = {};
|
|
522
|
-
if (opts.template)
|
|
523
|
-
body["template_id"] = opts.template;
|
|
524
|
-
if (opts.timeout != null)
|
|
525
|
-
body["timeout_sec"] = opts.timeout;
|
|
526
|
-
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/fork`), body, opts.idempotencyKey
|
|
527
|
-
? { "Idempotency-Key": opts.idempotencyKey }
|
|
528
|
-
: undefined));
|
|
529
|
-
printValue(result, opts);
|
|
530
|
-
}));
|
|
531
|
-
// desktop — open the Sandbox's web desktop URL (mirrors `box desktop`).
|
|
532
|
-
// Sandboxes are headless by default; only templates that expose a desktop
|
|
533
|
-
// port (e.g. `miosa-sandbox` w/ Kasm) emit a `preview_url`. We surface that.
|
|
534
|
-
sandbox
|
|
535
|
-
.command("desktop <sandbox-id>")
|
|
536
|
-
.description("Print the Sandbox's web desktop URL (when the template exposes one)")
|
|
537
|
-
.option("--json", "Output as JSON")
|
|
538
|
-
.action((id, opts) => runAction(async () => {
|
|
539
|
-
if (isJsonMode(opts)) {
|
|
540
|
-
await getAndPrint(`/sandboxes/${enc(id)}`, opts);
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
const sb = (await getAndPrint(`/sandboxes/${enc(id)}`, {
|
|
544
|
-
json: true,
|
|
545
|
-
}));
|
|
546
|
-
if (sb?.preview_url) {
|
|
547
|
-
console.log(sb.preview_url);
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
console.error(chalk.yellow("No desktop URL on this Sandbox. Use `miosa desktop open <computer-id>` for Computers, or expose a port via `miosa sandbox` for headless previews."));
|
|
551
|
-
process.exitCode = 1;
|
|
552
|
-
}
|
|
553
|
-
}));
|
|
554
|
-
registerSandboxConnectorCommands(sandbox);
|
|
555
|
-
// run-agent - invoke an in-Sandbox AI runner CLI.
|
|
556
|
-
// Implemented through Runs so callers get a stable run response while
|
|
557
|
-
// execution still happens inside the remote filesystem.
|
|
558
|
-
sandbox
|
|
559
|
-
.command("run-agent <sandbox-id> <instruction...>")
|
|
560
|
-
.description("Run an in-Sandbox AI agent runtime with the given instruction")
|
|
561
|
-
.option("--runner <name>", "Runner: claude (default), claude-code, codex, pi, hermes, osa, custom")
|
|
562
|
-
.option("--runtime-command <command>", "Executable command for --runner custom, e.g. 'hermes-agent run'")
|
|
563
|
-
.option("--model <name>", "Provider-specific model name")
|
|
564
|
-
.option("--connector <uid>", "MIOSA Connect connector UID to preflight before running the agent")
|
|
565
|
-
.option("--preflight", "Verify the Sandbox has the requested provider connector before exec")
|
|
566
|
-
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
567
|
-
.option("--env <KEY=VALUE>", `Environment variable for the Agent Run. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
|
|
568
|
-
.option("--env-file <path>", ENV_FILE_OPTION_HELP)
|
|
569
|
-
.option("--env-stdin", ENV_STDIN_OPTION_HELP)
|
|
570
|
-
.option("--agent-profile <id>", "Agent runtime profile ID")
|
|
571
|
-
.option("--skip-agent-profile", "Do not apply the default agent runtime profile")
|
|
572
|
-
.option("--external-workspace <id>", "White-label workspace/customer ID for billing attribution")
|
|
573
|
-
.option("--external-user <id>", "White-label user ID for billing attribution")
|
|
574
|
-
.option("--external-project <id>", "White-label project ID for billing attribution")
|
|
575
|
-
.option("--timeout <sec>", "Exec timeout in seconds", parseIntegerOption)
|
|
576
|
-
.option("--json", "Output as JSON")
|
|
577
|
-
.action((id, words, opts) => runAction(async () => {
|
|
578
|
-
const runner = opts.runner ?? "claude";
|
|
579
|
-
if (!isSupportedRunAgentRunner(runner)) {
|
|
580
|
-
const allowedRunners = supportedRunAgentRunners();
|
|
581
|
-
throw new Error(`Unsupported runner "${runner}". Use: ${allowedRunners.join(", ")}`);
|
|
582
|
-
}
|
|
583
|
-
if (opts.runtimeCommand && runner !== "custom") {
|
|
584
|
-
throw new Error("--runtime-command can only be used with --runner custom");
|
|
585
|
-
}
|
|
586
|
-
if (opts.connector || opts.preflight) {
|
|
587
|
-
await preflightSandboxConnector(id, {
|
|
588
|
-
provider: runner,
|
|
589
|
-
connector: opts.connector,
|
|
590
|
-
model: opts.model,
|
|
591
|
-
cwd: opts.cwd,
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
const instruction = words.join(" ");
|
|
595
|
-
const body = {
|
|
596
|
-
target_kind: "sandbox",
|
|
597
|
-
target_id: id,
|
|
598
|
-
runner,
|
|
599
|
-
instruction,
|
|
600
|
-
};
|
|
601
|
-
if (opts.runtimeCommand)
|
|
602
|
-
body["command"] = opts.runtimeCommand;
|
|
603
|
-
if (opts.model)
|
|
604
|
-
body["model"] = opts.model;
|
|
605
|
-
if (opts.cwd)
|
|
606
|
-
body["cwd"] = opts.cwd;
|
|
607
|
-
const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
|
|
608
|
-
if (Object.keys(env).length > 0) {
|
|
609
|
-
body["env"] = env;
|
|
610
|
-
}
|
|
611
|
-
if (opts.agentProfile) {
|
|
612
|
-
body["agent_runtime_profile_id"] = opts.agentProfile;
|
|
613
|
-
}
|
|
614
|
-
if (opts.skipAgentProfile) {
|
|
615
|
-
body["skip_agent_runtime_profile"] = true;
|
|
616
|
-
}
|
|
617
|
-
if (opts.externalWorkspace) {
|
|
618
|
-
body["external_workspace_id"] = opts.externalWorkspace;
|
|
619
|
-
}
|
|
620
|
-
if (opts.externalUser) {
|
|
621
|
-
body["external_user_id"] = opts.externalUser;
|
|
622
|
-
}
|
|
623
|
-
if (opts.externalProject) {
|
|
624
|
-
body["external_project_id"] = opts.externalProject;
|
|
625
|
-
}
|
|
626
|
-
if (opts.timeout != null)
|
|
627
|
-
body["timeout"] = opts.timeout;
|
|
628
|
-
const run = unwrap(await client().apiPost(apiPath("/runs"), body));
|
|
629
|
-
if (isJsonMode(opts)) {
|
|
630
|
-
console.log(JSON.stringify(run, null, 2));
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
console.log();
|
|
634
|
-
console.log(kvPanel([
|
|
635
|
-
{ label: "Run", value: chalk.bold(str(run["id"])) },
|
|
636
|
-
{ label: "Target", value: `${str(run["target_kind"])} ${id}` },
|
|
637
|
-
{ label: "Runner", value: str(run["runner"]) },
|
|
638
|
-
{ label: "Status", value: statusColor(str(run["status"])) },
|
|
639
|
-
{ label: "Exit", value: str(run["exit_code"]) },
|
|
640
|
-
]));
|
|
641
|
-
const output = str(run["output"]).trim();
|
|
642
|
-
const stderr = str(run["stderr"]).trim();
|
|
643
|
-
if (output) {
|
|
644
|
-
console.log();
|
|
645
|
-
console.log(output);
|
|
646
|
-
}
|
|
647
|
-
if (stderr) {
|
|
648
|
-
console.error();
|
|
649
|
-
console.error(chalk.red(stderr));
|
|
650
|
-
}
|
|
651
|
-
console.log();
|
|
652
|
-
}));
|
|
653
|
-
// exec — positional command arg; --data body overrides when supplied
|
|
654
|
-
addDataOption(sandbox
|
|
655
|
-
.command("exec <sandbox-id> [command...]")
|
|
656
|
-
.description('Run a command inside a Sandbox (positional args joined as shell command). Use `--` before the command to pass flags, e.g. `sandbox exec <id> -- bash -c "cd x && y"`.')
|
|
657
|
-
.allowUnknownOption()
|
|
658
|
-
.allowExcessArguments()
|
|
659
|
-
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
660
|
-
.option("--workdir <path>", "Alias for --cwd")
|
|
661
|
-
.option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
|
|
662
|
-
.option("--command <command>", "Alias for --cmd")
|
|
663
|
-
.option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
|
|
664
|
-
.option("--env <pair>", `Environment variable KEY=VALUE. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
|
|
665
|
-
.option("--env-file <path>", ENV_FILE_OPTION_HELP)
|
|
666
|
-
.option("--env-stdin", ENV_STDIN_OPTION_HELP)
|
|
667
|
-
.option("--background", "Start the command in the background and return immediately")
|
|
668
|
-
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
669
|
-
.option("--follow", "Stream command output until it exits (alias --stream)")
|
|
670
|
-
.option("--stream", "Alias for --follow")
|
|
671
|
-
.option("--user <user>", "Run command as user")
|
|
672
|
-
.option("--sudo", "Run command through sudo")
|
|
673
|
-
.option("--tty", "Request TTY metadata for command resource")
|
|
674
|
-
.option("--interactive", "Request interactive metadata for command resource")
|
|
675
|
-
.option("--timeout <sec>", "Exec timeout in seconds", parseIntegerOption))
|
|
676
|
-
.option("--json", "Output as JSON")
|
|
677
|
-
.action((id, words, opts) => runAction(async () => {
|
|
678
|
-
opts.follow = opts.follow || opts.stream;
|
|
679
|
-
if (opts.data) {
|
|
680
|
-
await postSandboxExecAndPrint(id, opts, {});
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
const cmd = resolveSandboxCommand(words, opts);
|
|
684
|
-
const effectiveCommand = opts.background
|
|
685
|
-
? backgroundCommand(cmd)
|
|
686
|
-
: cmd;
|
|
687
|
-
const cwd = opts.cwd ?? opts.workdir;
|
|
688
|
-
const body = cmd
|
|
689
|
-
? { command: commandInCwd(effectiveCommand, cwd) }
|
|
690
|
-
: {};
|
|
691
|
-
if (cwd) {
|
|
692
|
-
body["cwd"] = cwd;
|
|
693
|
-
body["dir"] = cwd;
|
|
694
|
-
}
|
|
695
|
-
const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
|
|
696
|
-
if (opts.detached) {
|
|
697
|
-
const result = await createSandboxCommand(id, cmd, {
|
|
698
|
-
cwd,
|
|
699
|
-
env,
|
|
700
|
-
user: opts.user,
|
|
701
|
-
sudo: !!opts.sudo,
|
|
702
|
-
tty: !!opts.tty,
|
|
703
|
-
interactive: !!opts.interactive,
|
|
704
|
-
timeout: opts.timeout,
|
|
705
|
-
});
|
|
706
|
-
if (isJsonMode(opts)) {
|
|
707
|
-
console.log(JSON.stringify(result, null, 2));
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
console.log(String(result["id"] ?? result["command_id"] ?? ""));
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
if (opts.follow) {
|
|
714
|
-
await runFollowExec(id, cmd, cwd, env, opts.timeout, opts);
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
if (Object.keys(env).length > 0)
|
|
718
|
-
body["env"] = env;
|
|
719
|
-
if (opts.timeout != null)
|
|
720
|
-
body["timeout"] = opts.timeout;
|
|
721
|
-
await postSandboxExecAndPrint(id, opts, body);
|
|
722
|
-
}));
|
|
723
|
-
// run — alias for exec with identical semantics
|
|
724
|
-
addDataOption(sandbox
|
|
725
|
-
.command("run <sandbox-id> [command...]")
|
|
726
|
-
.description("Run a command inside a Sandbox (alias for exec). Use `--` before the command to pass flags.")
|
|
727
|
-
.allowUnknownOption()
|
|
728
|
-
.allowExcessArguments()
|
|
729
|
-
.option("--cwd <path>", "Working directory inside the Sandbox")
|
|
730
|
-
.option("--workdir <path>", "Alias for --cwd")
|
|
731
|
-
.option("--cmd <command>", "Explicit command string to run; avoids CLI parsing command flags")
|
|
732
|
-
.option("--command <command>", "Alias for --cmd")
|
|
733
|
-
.option("--shell-cmd <shell>", "Run --cmd through this shell, e.g. 'bash -lc'")
|
|
734
|
-
.option("--env <pair>", `Environment variable KEY=VALUE. Repeatable. ${ENV_INLINE_SHELL_WARNING}`, collectOption, [])
|
|
735
|
-
.option("--env-file <path>", ENV_FILE_OPTION_HELP)
|
|
736
|
-
.option("--env-stdin", ENV_STDIN_OPTION_HELP)
|
|
737
|
-
.option("--background", "Start the command in the background and return immediately")
|
|
738
|
-
.option("--detached", "Create a durable backend command and return command_id immediately")
|
|
739
|
-
.option("--follow", "Stream command output until it exits (alias --stream)")
|
|
740
|
-
.option("--stream", "Alias for --follow")
|
|
741
|
-
.option("--user <user>", "Run command as user")
|
|
742
|
-
.option("--sudo", "Run command through sudo")
|
|
743
|
-
.option("--tty", "Request TTY metadata for command resource")
|
|
744
|
-
.option("--interactive", "Request interactive metadata for command resource")
|
|
745
|
-
.option("--timeout <sec>", "Exec timeout in seconds", parseIntegerOption))
|
|
746
|
-
.option("--json", "Output as JSON")
|
|
747
|
-
.action((id, words, opts) => runAction(async () => {
|
|
748
|
-
opts.follow = opts.follow || opts.stream;
|
|
749
|
-
if (opts.data) {
|
|
750
|
-
await postSandboxExecAndPrint(id, opts, {});
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
const cmd = resolveSandboxCommand(words, opts);
|
|
754
|
-
const effectiveCommand = opts.background
|
|
755
|
-
? backgroundCommand(cmd)
|
|
756
|
-
: cmd;
|
|
757
|
-
const cwd = opts.cwd ?? opts.workdir;
|
|
758
|
-
const body = cmd
|
|
759
|
-
? { command: commandInCwd(effectiveCommand, cwd) }
|
|
760
|
-
: {};
|
|
761
|
-
if (cwd) {
|
|
762
|
-
body["cwd"] = cwd;
|
|
763
|
-
body["dir"] = cwd;
|
|
764
|
-
}
|
|
765
|
-
const env = await resolveEnvInputs(parseEnvPairs(opts.env ?? []), opts);
|
|
766
|
-
if (opts.detached) {
|
|
767
|
-
const result = await createSandboxCommand(id, cmd, {
|
|
768
|
-
cwd,
|
|
769
|
-
env,
|
|
770
|
-
user: opts.user,
|
|
771
|
-
sudo: !!opts.sudo,
|
|
772
|
-
tty: !!opts.tty,
|
|
773
|
-
interactive: !!opts.interactive,
|
|
774
|
-
timeout: opts.timeout,
|
|
775
|
-
});
|
|
776
|
-
if (isJsonMode(opts)) {
|
|
777
|
-
console.log(JSON.stringify(result, null, 2));
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
console.log(String(result["id"] ?? result["command_id"] ?? ""));
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
if (opts.follow) {
|
|
784
|
-
await runFollowExec(id, cmd, cwd, env, opts.timeout, opts);
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
if (Object.keys(env).length > 0)
|
|
788
|
-
body["env"] = env;
|
|
789
|
-
if (opts.timeout != null)
|
|
790
|
-
body["timeout"] = opts.timeout;
|
|
791
|
-
await postSandboxExecAndPrint(id, opts, body);
|
|
792
|
-
}));
|
|
793
|
-
sandbox
|
|
794
|
-
.command("ports <sandbox-id>")
|
|
795
|
-
.description("List listening TCP ports inside a Sandbox")
|
|
796
|
-
.option("--json", "Output as JSON")
|
|
797
|
-
.action((id, opts) => runAction(async () => {
|
|
798
|
-
const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/ports`));
|
|
799
|
-
const ports = sandboxPortsFromResponse(result);
|
|
800
|
-
if (isJsonMode(opts)) {
|
|
801
|
-
console.log(JSON.stringify(result, null, 2));
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
if (ports.length === 0) {
|
|
805
|
-
console.log(chalk.dim("(no listening ports)"));
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
renderTable(ports, [
|
|
809
|
-
{
|
|
810
|
-
header: "PROTO",
|
|
811
|
-
key: (p) => p.protocol ?? "tcp",
|
|
812
|
-
width: 8,
|
|
813
|
-
},
|
|
814
|
-
{
|
|
815
|
-
header: "PORT",
|
|
816
|
-
key: "port",
|
|
817
|
-
width: 8,
|
|
818
|
-
},
|
|
819
|
-
{
|
|
820
|
-
header: "ADDRESS",
|
|
821
|
-
key: "address",
|
|
822
|
-
width: 24,
|
|
823
|
-
},
|
|
824
|
-
{
|
|
825
|
-
header: "STATE",
|
|
826
|
-
key: (p) => p.state ?? "listen",
|
|
827
|
-
width: 10,
|
|
828
|
-
},
|
|
829
|
-
{
|
|
830
|
-
header: "PROCESS",
|
|
831
|
-
key: (p) => p.process?.name
|
|
832
|
-
? `${p.process.name}${p.process.pid ? `:${p.process.pid}` : ""}`
|
|
833
|
-
: chalk.dim("-"),
|
|
834
|
-
width: 28,
|
|
835
|
-
},
|
|
836
|
-
]);
|
|
837
|
-
}));
|
|
838
|
-
sandbox
|
|
839
|
-
.command("metrics <sandbox-id>")
|
|
840
|
-
.description("Show Sandbox resource, uptime, timeout, and readiness metrics")
|
|
841
|
-
.option("--window <window>", "Metrics window: 1h, 24h, or 7d", "1h")
|
|
842
|
-
.option("--json", "Output as JSON")
|
|
843
|
-
.action((id, opts) => runAction(async () => {
|
|
844
|
-
const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/metrics?window=${enc(opts.window)}`));
|
|
845
|
-
if (isJsonMode(opts)) {
|
|
846
|
-
console.log(JSON.stringify(result, null, 2));
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
renderResourceMetrics("Sandbox metrics", result);
|
|
850
|
-
}));
|
|
851
|
-
sandbox
|
|
852
|
-
.command("deploy [local-dir]")
|
|
853
|
-
.description("Upload an app directory, start it in a sandbox, expose a preview URL, and wait for readiness")
|
|
854
|
-
.option("--sandbox <id>", "Existing sandbox ID. Creates one when omitted")
|
|
855
|
-
.option("--template <template>", "Template for new sandbox", "miosa-sandbox")
|
|
856
|
-
.option("--name <name>", "Name for a new sandbox")
|
|
857
|
-
.option("--port <port>", "Preview port", parseIntegerOption)
|
|
858
|
-
.option("--publish-port <port>", "Alias for --port", parseIntegerOption)
|
|
859
|
-
.option("--start <command>", "Start command to run inside /workspace")
|
|
860
|
-
.option("--install-command <command>", "Install command to run before start")
|
|
861
|
-
.option("--no-install", "Skip automatic dependency install")
|
|
862
|
-
.option("--source <source>", "Source: git:https://... or tarball:https://... for repo-backed preview deploy")
|
|
863
|
-
.option("--revision <revision>", "Git revision/branch for --source git:...")
|
|
864
|
-
.option("--depth <n>", "Git clone depth for --source git:...", parseIntegerOption)
|
|
865
|
-
.option("--wait", "Wait until the public preview returns a good HTTP status")
|
|
866
|
-
.option("--timeout <duration>", "Wait timeout, e.g. 180s or 3m", parseDurationSec, 180)
|
|
867
|
-
.option("--probe-path <path>", "HTTP path to probe")
|
|
868
|
-
.option("--json", "Output as JSON")
|
|
869
|
-
.action(async (localDir = ".", opts) => {
|
|
870
|
-
try {
|
|
871
|
-
const result = await deploySandbox(localDir, opts);
|
|
872
|
-
if (isJsonMode(opts)) {
|
|
873
|
-
console.log(JSON.stringify(result, null, 2));
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
console.log();
|
|
877
|
-
console.log(` ${chalk.bold("Sandbox")} ${result.sandbox_id}`);
|
|
878
|
-
console.log(` ${chalk.bold("Port")} ${result.port}`);
|
|
879
|
-
console.log(` ${chalk.bold("Preview")} ${chalk.cyan(result.preview_url)}`);
|
|
880
|
-
console.log(` ${chalk.bold("Ready")} ${result.preview_ready
|
|
881
|
-
? chalk.green("yes")
|
|
882
|
-
: chalk.yellow("not verified")}`);
|
|
883
|
-
console.log();
|
|
884
|
-
}
|
|
885
|
-
catch (err) {
|
|
886
|
-
handleSandboxDeployError(err, opts);
|
|
887
|
-
}
|
|
888
|
-
});
|
|
889
|
-
sandbox
|
|
890
|
-
.command("preview <sandbox-id>")
|
|
891
|
-
.description("Expose a sandbox port and optionally wait for the public preview to answer")
|
|
892
|
-
.requiredOption("--port <port>", "Port inside the sandbox to expose", parseIntegerOption)
|
|
893
|
-
.option("--wait", "Wait until the public URL returns a good HTTP status")
|
|
894
|
-
.option("--timeout <sec>", "Wait timeout in seconds", parseDurationSec, 120)
|
|
895
|
-
.option("--probe-path <path>", "HTTP path to probe", "/")
|
|
896
|
-
.option("--json", "Output as JSON")
|
|
897
|
-
.action((id, opts) => runAction(async () => {
|
|
898
|
-
const result = await previewSandbox(id, opts.port, {
|
|
899
|
-
wait: !!opts.wait,
|
|
900
|
-
timeout: opts.timeout,
|
|
901
|
-
probePath: opts.probePath,
|
|
902
|
-
});
|
|
903
|
-
if (isJsonMode(opts)) {
|
|
904
|
-
console.log(JSON.stringify(result, null, 2));
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
console.log(result.url);
|
|
908
|
-
if (!result.ready) {
|
|
909
|
-
console.error(chalk.yellow(`Preview route created but not verified yet (${result.error ?? result.status ?? "pending"}).`));
|
|
910
|
-
}
|
|
911
|
-
}));
|
|
912
|
-
sandbox
|
|
913
|
-
.command("wait <sandbox-id>")
|
|
914
|
-
.description("Wait for sandbox VM readiness; pass --port to also verify app preview readiness")
|
|
915
|
-
.option("--port <port>", "Port inside the sandbox to check", parseIntegerOption)
|
|
916
|
-
.option("--url", "Print only the ready public preview URL")
|
|
917
|
-
.option("--timeout <sec>", "Wait timeout in seconds", parseDurationSec, 120)
|
|
918
|
-
.option("--probe-path <path>", "HTTP path to probe", "/")
|
|
919
|
-
.option("--json", "Output as JSON")
|
|
920
|
-
.action((id, opts) => runAction(async () => {
|
|
921
|
-
const result = opts.port == null
|
|
922
|
-
? await waitSandboxVmReady(id, opts.timeout)
|
|
923
|
-
: await waitSandboxReady(id, opts.port, opts.probePath, opts.timeout);
|
|
924
|
-
if (opts.url && result.url) {
|
|
925
|
-
console.log(result.url);
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
if (isJsonMode(opts)) {
|
|
929
|
-
console.log(JSON.stringify(result, null, 2));
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
console.log(chalk.green("Ready"));
|
|
933
|
-
if (result.url)
|
|
934
|
-
console.log(chalk.cyan(result.url));
|
|
935
|
-
}));
|
|
936
|
-
sandbox
|
|
937
|
-
.command("domain <sandbox-id> <port>")
|
|
938
|
-
.description("Return the public preview domain/URL for a sandbox port")
|
|
939
|
-
.option("--wait", "Wait until the public URL is externally ready")
|
|
940
|
-
.option("--timeout <duration>", "Wait timeout", parseDurationSec, 120)
|
|
941
|
-
.option("--probe-path <path>", "HTTP path to probe", "/")
|
|
942
|
-
.option("--json", "Output as JSON")
|
|
943
|
-
.action((id, portText, opts) => runAction(async () => {
|
|
944
|
-
const port = Number(portText);
|
|
945
|
-
if (!Number.isInteger(port))
|
|
946
|
-
throw new UserError(`Invalid port: ${portText}`);
|
|
947
|
-
const result = await previewSandbox(id, port, {
|
|
948
|
-
wait: !!opts.wait,
|
|
949
|
-
timeout: opts.timeout,
|
|
950
|
-
probePath: opts.probePath,
|
|
951
|
-
});
|
|
952
|
-
const data = { port, ...result };
|
|
953
|
-
if (isJsonMode(opts)) {
|
|
954
|
-
console.log(JSON.stringify(data, null, 2));
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
console.log(result.url);
|
|
958
|
-
}));
|
|
959
|
-
sandbox
|
|
960
|
-
.command("publish <sandbox-id>")
|
|
961
|
-
.description("Publish a sandbox workspace to durable MIOSA Deploy hosting")
|
|
962
|
-
.option("--path <path>", "Path inside the sandbox to publish", "/workspace")
|
|
963
|
-
.option("--app <id>", "Existing app/deployment ID to publish a new release to")
|
|
964
|
-
.option("--name <name>", "Name for a new durable app")
|
|
965
|
-
.option("--slug <slug>", "Production slug for a new durable app")
|
|
966
|
-
.option("--environment <name>", "Target environment label", "production")
|
|
967
|
-
.option("--build-command <cmd>", "Build command to run before publishing")
|
|
968
|
-
.option("--run-command <cmd>", "Run command for dynamic/server deployments")
|
|
969
|
-
.option("--docker-deploy", "Publish onto the workspace App Engine runtime instead of standard app hosting")
|
|
970
|
-
.option("--domain <domain>", "Custom domain to attach")
|
|
971
|
-
.option("--deployment-type <type>", "Deployment runtime type: miosa_deploy, docker_deploy, dynamic, static")
|
|
972
|
-
.option("--database <mode>", "none, create:postgres, postgres, or existing:<db-id>")
|
|
973
|
-
.option("--port <port>", "Runtime port for dynamic deployments", parseIntegerOption)
|
|
974
|
-
.option("--wait", "Wait for the production URL to answer")
|
|
975
|
-
.option("--no-wait", "Return immediately without waiting (this is the default)")
|
|
976
|
-
.option("--timeout <duration>", "Wait timeout", parseDurationSec, 600)
|
|
977
|
-
.option("--json", "Output as JSON")
|
|
978
|
-
.action((id, opts) => runAction(async () => {
|
|
979
|
-
const result = await publishSandbox(id, opts);
|
|
980
|
-
if (isJsonMode(opts)) {
|
|
981
|
-
console.log(JSON.stringify(result, null, 2));
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
console.log();
|
|
985
|
-
console.log(` ${chalk.bold("Type")} deployment`);
|
|
986
|
-
console.log(` ${chalk.bold("App")} ${result.deployment_id ?? result.app_id ?? ""}`);
|
|
987
|
-
if (result.release_id)
|
|
988
|
-
console.log(` ${chalk.bold("Release")} ${result.release_id}`);
|
|
989
|
-
if (result.deployment_product)
|
|
990
|
-
console.log(` ${chalk.bold("Product")} ${String(result.deployment_product)}`);
|
|
991
|
-
if (result.docker_deploy_host_id)
|
|
992
|
-
console.log(` ${chalk.bold("Docker")} ${String(result.docker_deploy_host_id)}`);
|
|
993
|
-
if (result.url)
|
|
994
|
-
console.log(` ${chalk.bold("URL")} ${chalk.cyan(String(result.url))}`);
|
|
995
|
-
console.log(` ${chalk.bold("Ready")} ${result.ready ? chalk.green("yes") : chalk.yellow("pending")}`);
|
|
996
|
-
console.log();
|
|
997
|
-
}));
|
|
998
|
-
const command = sandbox
|
|
999
|
-
.command("command")
|
|
1000
|
-
.description("Inspect and control detached sandbox commands");
|
|
1001
|
-
command
|
|
1002
|
-
.command("get <sandbox-id> <command-id>")
|
|
1003
|
-
.option("--json", "Output as JSON")
|
|
1004
|
-
.action((id, commandId, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/commands/${enc(commandId)}`, opts)));
|
|
1005
|
-
command
|
|
1006
|
-
.command("logs <sandbox-id> <command-id>")
|
|
1007
|
-
.option("--follow", "Follow logs (currently returns tailed logs when backend SSE is unavailable)")
|
|
1008
|
-
.option("--tail <n>", "Number of lines", parseIntegerOption, 200)
|
|
1009
|
-
.option("--json", "Output as JSON")
|
|
1010
|
-
.action((id, commandId, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/commands/${enc(commandId)}/logs?tail=${opts.tail}`, opts)));
|
|
1011
|
-
command
|
|
1012
|
-
.command("kill <sandbox-id> <command-id>")
|
|
1013
|
-
.option("--json", "Output as JSON")
|
|
1014
|
-
.action((id, commandId, opts) => runAction(() => postAndPrint(`/sandboxes/${enc(id)}/commands/${enc(commandId)}/kill`, opts, {})));
|
|
1015
|
-
sandbox
|
|
1016
|
-
.command("extend <sandbox-id>")
|
|
1017
|
-
.description("Extend or replace a sandbox timeout")
|
|
1018
|
-
.requiredOption("--timeout <duration>", "New timeout, e.g. 2h", parseDurationSec)
|
|
1019
|
-
.option("--json", "Output as JSON")
|
|
1020
|
-
.action((id, opts) => runAction(() => postAndPrint(`/sandboxes/${enc(id)}/extend`, opts, {
|
|
1021
|
-
timeout_sec: opts.timeout,
|
|
1022
|
-
})));
|
|
1023
|
-
sandbox
|
|
1024
|
-
.command("usage <sandbox-id>")
|
|
1025
|
-
.description("Show sandbox runtime, rough resource usage, and estimated cost")
|
|
1026
|
-
.option("--json", "Output as JSON")
|
|
1027
|
-
.action((id, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/usage`, opts)));
|
|
1028
|
-
const config = sandbox
|
|
1029
|
-
.command("config")
|
|
1030
|
-
.description("Configure sandbox settings");
|
|
1031
|
-
config
|
|
1032
|
-
.command("network-policy <sandbox-id>")
|
|
1033
|
-
.description("Store egress network policy metadata for a sandbox")
|
|
1034
|
-
.option("--network-policy <policy>", "Network policy: allow-all or deny-all")
|
|
1035
|
-
.option("--allowed-domain <domain>", "Allowed egress domain. Repeatable.", collectOption, [])
|
|
1036
|
-
.option("--allowed-cidr <cidr>", "Allowed egress CIDR. Repeatable.", collectOption, [])
|
|
1037
|
-
.option("--denied-cidr <cidr>", "Denied egress CIDR. Repeatable.", collectOption, [])
|
|
1038
|
-
.option("--json", "Output as JSON")
|
|
1039
|
-
.action((id, opts) => runAction(async () => {
|
|
1040
|
-
const policy = buildNetworkPolicy(opts);
|
|
1041
|
-
if (!policy)
|
|
1042
|
-
throw new UserError("No network policy options provided.");
|
|
1043
|
-
const result = unwrap(await client().apiPatch(apiPath(`/sandboxes/${enc(id)}`), {
|
|
1044
|
-
metadata: { network_policy: policy },
|
|
1045
|
-
}));
|
|
1046
|
-
if (isJsonMode(opts)) {
|
|
1047
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
console.log(chalk.green("Network policy updated."));
|
|
1051
|
-
}));
|
|
1052
|
-
const service = sandbox
|
|
1053
|
-
.command("service")
|
|
1054
|
-
.description("Manage named long-running processes inside a sandbox");
|
|
1055
|
-
service
|
|
1056
|
-
.command("start <sandbox-id> <name>")
|
|
1057
|
-
.alias("up")
|
|
1058
|
-
.requiredOption("--cmd <command>", "Command to start")
|
|
1059
|
-
.option("--cwd <path>", "Working directory inside the sandbox", "/workspace")
|
|
1060
|
-
.option("--port <port>", "Port served by this service", parseIntegerOption)
|
|
1061
|
-
.option("--json", "Output as JSON")
|
|
1062
|
-
.action((id, name, opts) => runAction(async () => {
|
|
1063
|
-
const result = await startSandboxService(id, name, opts);
|
|
1064
|
-
if (isJsonMode(opts)) {
|
|
1065
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
console.log(chalk.green(`Started ${name}`));
|
|
1069
|
-
if (result.preview_url)
|
|
1070
|
-
console.log(chalk.cyan(String(result.preview_url)));
|
|
1071
|
-
}));
|
|
1072
|
-
service
|
|
1073
|
-
.command("restart <sandbox-id> <name>")
|
|
1074
|
-
.option("--cmd <command>", "Ignored when backend service metadata already exists")
|
|
1075
|
-
.option("--cwd <path>", "Working directory inside the sandbox", "/workspace")
|
|
1076
|
-
.option("--port <port>", "Port served by this service", parseIntegerOption)
|
|
1077
|
-
.option("--json", "Output as JSON")
|
|
1078
|
-
.action((id, name, opts) => runAction(async () => {
|
|
1079
|
-
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/services/${enc(name)}/restart`), {}));
|
|
1080
|
-
if (isJsonMode(opts)) {
|
|
1081
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
console.log(chalk.green(`Restarted ${name}`));
|
|
1085
|
-
if (result.preview_url)
|
|
1086
|
-
console.log(chalk.cyan(String(result.preview_url)));
|
|
1087
|
-
}));
|
|
1088
|
-
service
|
|
1089
|
-
.command("status <sandbox-id> <name>")
|
|
1090
|
-
.option("--json", "Output as JSON")
|
|
1091
|
-
.action((id, name, opts) => runAction(async () => {
|
|
1092
|
-
const result = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}/services/${enc(name)}`)));
|
|
1093
|
-
const status = String(result["status"] ?? "unknown");
|
|
1094
|
-
if (isJsonMode(opts)) {
|
|
1095
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
console.log(status === "running" ? chalk.green(status) : chalk.yellow(status));
|
|
1099
|
-
}));
|
|
1100
|
-
service
|
|
1101
|
-
.command("logs <sandbox-id> <name>")
|
|
1102
|
-
.option("--lines <n>", "Number of log lines", parseIntegerOption, 100)
|
|
1103
|
-
.option("--json", "Output as JSON")
|
|
1104
|
-
.action((id, name, opts) => runAction(async () => {
|
|
1105
|
-
const result = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}/services/${enc(name)}/logs?tail=${Number(opts.lines) || 100}`)));
|
|
1106
|
-
if (isJsonMode(opts)) {
|
|
1107
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
const lines = Array.isArray(result["lines"]) ? result["lines"] : [];
|
|
1111
|
-
process.stdout.write(lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
1112
|
-
}));
|
|
1113
|
-
const env = sandbox
|
|
1114
|
-
.command("env")
|
|
1115
|
-
.description("Manage encrypted environment variables for a sandbox");
|
|
1116
|
-
env
|
|
1117
|
-
.command("list <sandbox-id>")
|
|
1118
|
-
.description("List sandbox env var names and masked previews")
|
|
1119
|
-
.option("--json", "Output as JSON")
|
|
1120
|
-
.action((id, opts) => runAction(async () => {
|
|
1121
|
-
const result = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}/env`)));
|
|
1122
|
-
if (isJsonMode(opts)) {
|
|
1123
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
const rows = Array.isArray(result)
|
|
1127
|
-
? result
|
|
1128
|
-
: [];
|
|
1129
|
-
if (rows.length === 0) {
|
|
1130
|
-
console.log(chalk.dim("No sandbox env vars."));
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
renderTable(rows, [
|
|
1134
|
-
{
|
|
1135
|
-
header: "NAME",
|
|
1136
|
-
key: "name",
|
|
1137
|
-
width: 32,
|
|
1138
|
-
},
|
|
1139
|
-
{
|
|
1140
|
-
header: "VALUE",
|
|
1141
|
-
key: "preview",
|
|
1142
|
-
width: 24,
|
|
1143
|
-
},
|
|
1144
|
-
{
|
|
1145
|
-
header: "UPDATED",
|
|
1146
|
-
key: "updated_at",
|
|
1147
|
-
width: 28,
|
|
1148
|
-
},
|
|
1149
|
-
]);
|
|
1150
|
-
}));
|
|
1151
|
-
env
|
|
1152
|
-
.command("set <sandbox-id> [pairs...]")
|
|
1153
|
-
.description(`Set encrypted sandbox env vars as KEY=VALUE. ${ENV_INLINE_SHELL_WARNING}`)
|
|
1154
|
-
.option("--env-file <path>", ENV_FILE_OPTION_HELP)
|
|
1155
|
-
.option("--env-stdin", ENV_STDIN_OPTION_HELP)
|
|
1156
|
-
.option("--json", "Output as JSON")
|
|
1157
|
-
.action((id, pairs, opts) => runAction(async () => {
|
|
1158
|
-
const values = await resolveEnvInputs(parseEnvPairs(pairs ?? []), opts);
|
|
1159
|
-
if (Object.keys(values).length === 0) {
|
|
1160
|
-
throw new UserError("No env vars provided.", "Pass KEY=VALUE pairs, --env-file <path>, or --env-stdin.");
|
|
1161
|
-
}
|
|
1162
|
-
const vars = Object.entries(values).map(([key, value]) => ({
|
|
1163
|
-
key,
|
|
1164
|
-
value,
|
|
1165
|
-
}));
|
|
1166
|
-
const result = unwrap(await client().apiPut(apiPath(`/sandboxes/${enc(id)}/env`), {
|
|
1167
|
-
vars,
|
|
1168
|
-
}));
|
|
1169
|
-
if (isJsonMode(opts)) {
|
|
1170
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
console.log(chalk.green(`Set ${vars.length} sandbox env var(s).`));
|
|
1174
|
-
}));
|
|
1175
|
-
env
|
|
1176
|
-
.command("delete <sandbox-id> <key>")
|
|
1177
|
-
.alias("unset")
|
|
1178
|
-
.description("Delete an encrypted sandbox env var")
|
|
1179
|
-
.option("--json", "Output as JSON")
|
|
1180
|
-
.action((id, key, opts) => runAction(async () => {
|
|
1181
|
-
const result = unwrap(await client().apiDelete(apiPath(`/sandboxes/${enc(id)}/env/${enc(key)}`)));
|
|
1182
|
-
if (isJsonMode(opts)) {
|
|
1183
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
console.log(chalk.green(`Deleted ${key}.`));
|
|
1187
|
-
}));
|
|
1188
|
-
env
|
|
1189
|
-
.command("sync <sandbox-id>")
|
|
1190
|
-
.description("Sync encrypted sandbox env vars into the running VM")
|
|
1191
|
-
.option("--json", "Output as JSON")
|
|
1192
|
-
.action((id, opts) => runAction(async () => {
|
|
1193
|
-
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/env/sync`), {}));
|
|
1194
|
-
if (isJsonMode(opts)) {
|
|
1195
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
const status = result && typeof result === "object" && "status" in result
|
|
1199
|
-
? String(result["status"])
|
|
1200
|
-
: "ok";
|
|
1201
|
-
console.log(chalk.green(`Sandbox env sync ${status}.`));
|
|
1202
|
-
}));
|
|
1203
|
-
const sandboxDb = sandbox
|
|
1204
|
-
.command("db")
|
|
1205
|
-
.description("Attach managed databases to sandboxes");
|
|
1206
|
-
sandboxDb
|
|
1207
|
-
.command("attach <sandbox-id> <database-id>")
|
|
1208
|
-
.description("Attach a managed database and persist DATABASE_URL env vars")
|
|
1209
|
-
.option("--json", "Output as JSON")
|
|
1210
|
-
.action((id, databaseId, opts) => runAction(async () => {
|
|
1211
|
-
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/database`), { database_id: databaseId }));
|
|
1212
|
-
if (isJsonMode(opts)) {
|
|
1213
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
console.log(chalk.green(`Attached database ${databaseId} to sandbox ${id}.`));
|
|
1217
|
-
}));
|
|
1218
|
-
sandbox
|
|
1219
|
-
.command("doctor [sandbox-id]")
|
|
1220
|
-
.description("Diagnose sandbox app readiness across sandbox state, internal HTTP, public route, and TLS/edge reachability")
|
|
1221
|
-
.option("--port <port>", "Port inside the sandbox to check", parseIntegerOption)
|
|
1222
|
-
.option("--probe-path <path>", "HTTP path to probe", "/")
|
|
1223
|
-
.option("--full", "Inspect the complete canonical developer contract")
|
|
1224
|
-
.option("--dir <path>", "Project directory for --full", ".")
|
|
1225
|
-
.option("--json", "Output as JSON")
|
|
1226
|
-
.action((id, opts) => runAction(async () => {
|
|
1227
|
-
if (opts.full) {
|
|
1228
|
-
const report = await runFullSandboxDoctor(opts.dir, id);
|
|
1229
|
-
if (!report.ok)
|
|
1230
|
-
process.exitCode = 1;
|
|
1231
|
-
if (isJsonMode(opts)) {
|
|
1232
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
console.log();
|
|
1236
|
-
console.log(chalk.bold("Sandbox Doctor Full"));
|
|
1237
|
-
console.log();
|
|
1238
|
-
for (const check of report.checks) {
|
|
1239
|
-
console.log(` ${check.ok ? chalk.green("ok") : chalk.red("fail")} ${check.id}: ${check.message}`);
|
|
1240
|
-
}
|
|
1241
|
-
console.log();
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
if (!id || opts.port == null) {
|
|
1245
|
-
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.");
|
|
1246
|
-
}
|
|
1247
|
-
const report = await doctorSandbox(id, opts.port, opts.probePath);
|
|
1248
|
-
if (isJsonMode(opts)) {
|
|
1249
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1250
|
-
return;
|
|
1251
|
-
}
|
|
1252
|
-
renderDoctorReport(report);
|
|
1253
|
-
}));
|
|
1254
|
-
sandbox
|
|
1255
|
-
.command("recover <name-or-id>")
|
|
1256
|
-
.description("Inspect a failed or partial sandbox deploy and print recovery commands")
|
|
1257
|
-
.option("--port <port>", "App port to diagnose", parseIntegerOption)
|
|
1258
|
-
.option("--probe-path <path>", "HTTP path to probe", "/")
|
|
1259
|
-
.option("--json", "Output as JSON")
|
|
1260
|
-
.action((idOrName, opts) => runAction(async () => {
|
|
1261
|
-
const report = await recoverSandboxDeploy(idOrName, opts);
|
|
1262
|
-
if (isJsonMode(opts)) {
|
|
1263
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
renderRecoverReport(report);
|
|
1267
|
-
}));
|
|
1268
|
-
// write-file — POST /sandboxes/:id/files with base64 content.
|
|
1269
|
-
// If <content-or-file> is an existing local path, reads the file bytes;
|
|
1270
|
-
// otherwise treats the argument as literal UTF-8 text.
|
|
1271
|
-
sandbox
|
|
1272
|
-
.command("write-file <sandbox-id> <remote-path> <content-or-file>")
|
|
1273
|
-
.description("Write a file inside a Sandbox (pass literal text or a local file path)")
|
|
1274
|
-
.option("--json", "Output as JSON")
|
|
1275
|
-
.action(async (id, remotePath, contentArg, opts) => {
|
|
1276
|
-
try {
|
|
1277
|
-
const contentBytes = fs.existsSync(contentArg)
|
|
1278
|
-
? fs.readFileSync(contentArg)
|
|
1279
|
-
: Buffer.from(contentArg, "utf8");
|
|
1280
|
-
const result = await writeBytesToSandbox(client(), id, remotePath, contentBytes);
|
|
1281
|
-
if (!isJsonMode(opts)) {
|
|
1282
|
-
console.log(chalk.green(`Written to ${remotePath}`));
|
|
1283
|
-
}
|
|
1284
|
-
else {
|
|
1285
|
-
printValue(typeof result === "string" ? { content: result } : result, opts);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
catch (err) {
|
|
1289
|
-
handleError(err);
|
|
1290
|
-
}
|
|
1291
|
-
});
|
|
1292
|
-
// read-file — GET /sandboxes/:id/files/:path, decode base64 content and print.
|
|
1293
|
-
sandbox
|
|
1294
|
-
.command("read-file <sandbox-id> <remote-path>")
|
|
1295
|
-
.description("Read a file from inside a Sandbox and print its contents")
|
|
1296
|
-
.option("--json", "Output raw JSON response")
|
|
1297
|
-
.action(async (id, remotePath, opts) => {
|
|
1298
|
-
try {
|
|
1299
|
-
const result = await client().apiGet(apiPath(`/sandboxes/${enc(id)}/files/read?path=${enc(remotePath)}`));
|
|
1300
|
-
if (isJsonMode(opts)) {
|
|
1301
|
-
console.log(JSON.stringify(typeof result === "string" ? { content: result } : result, null, 2));
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
const data = result !== null &&
|
|
1305
|
-
typeof result === "object" &&
|
|
1306
|
-
!Array.isArray(result)
|
|
1307
|
-
? (result["data"] ?? result)
|
|
1308
|
-
: result;
|
|
1309
|
-
const contentVal = data !== null &&
|
|
1310
|
-
typeof data === "object" &&
|
|
1311
|
-
!Array.isArray(data) &&
|
|
1312
|
-
"content" in data &&
|
|
1313
|
-
typeof data["content"] === "string"
|
|
1314
|
-
? data["content"]
|
|
1315
|
-
: null;
|
|
1316
|
-
if (contentVal !== null) {
|
|
1317
|
-
process.stdout.write(contentVal);
|
|
1318
|
-
}
|
|
1319
|
-
else if (typeof data === "string") {
|
|
1320
|
-
process.stdout.write(data);
|
|
1321
|
-
}
|
|
1322
|
-
else {
|
|
1323
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
catch (err) {
|
|
1327
|
-
handleError(err);
|
|
1328
|
-
}
|
|
1329
|
-
});
|
|
1330
|
-
// upload — read local binary file, POST to /sandboxes/:id/files as base64.
|
|
1331
|
-
sandbox
|
|
1332
|
-
.command("upload <sandbox-id> <local-path> <remote-path>")
|
|
1333
|
-
.description("Upload a local file into a Sandbox")
|
|
1334
|
-
.option("--json", "Output as JSON")
|
|
1335
|
-
.action(async (id, localPath, remotePath, opts) => {
|
|
1336
|
-
try {
|
|
1337
|
-
if (!fs.existsSync(localPath)) {
|
|
1338
|
-
console.error(chalk.red(`File not found: ${localPath}`));
|
|
1339
|
-
process.exit(1);
|
|
1340
|
-
}
|
|
1341
|
-
const c = client();
|
|
1342
|
-
const result = await writeBytesToSandbox(c, id, remotePath, fs.readFileSync(localPath));
|
|
1343
|
-
if (isJsonMode(opts)) {
|
|
1344
|
-
printValue(result, opts);
|
|
1345
|
-
}
|
|
1346
|
-
else {
|
|
1347
|
-
const filename = path.basename(localPath);
|
|
1348
|
-
console.log(chalk.green(`Uploaded ${filename} → ${remotePath}`));
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
catch (err) {
|
|
1352
|
-
handleError(err);
|
|
1353
|
-
}
|
|
1354
|
-
});
|
|
1355
|
-
sandbox
|
|
1356
|
-
.command("upload-dir <sandbox-id> <local-dir> <remote-dir>")
|
|
1357
|
-
.description("Upload a local directory into a Sandbox")
|
|
1358
|
-
.option("--delete", "Delete the remote directory contents before extracting")
|
|
1359
|
-
.option("--force", "Skip the --delete confirmation prompt")
|
|
1360
|
-
.option("--yes", "Alias for --force")
|
|
1361
|
-
.option("--json", "Output as JSON")
|
|
1362
|
-
.action(async (id, localDir, remoteDir, opts) => {
|
|
1363
|
-
try {
|
|
1364
|
-
const result = await uploadDirToSandbox(id, localDir, remoteDir, {
|
|
1365
|
-
delete: !!opts.delete,
|
|
1366
|
-
force: !!(opts.force || opts.yes),
|
|
1367
|
-
});
|
|
1368
|
-
if (isJsonMode(opts)) {
|
|
1369
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
1372
|
-
console.log(chalk.green(`Uploaded ${result.files_label} → ${remoteDir}`));
|
|
1373
|
-
}
|
|
1374
|
-
catch (err) {
|
|
1375
|
-
handleError(err);
|
|
1376
|
-
}
|
|
1377
|
-
});
|
|
1378
|
-
sandbox
|
|
1379
|
-
.command("sync <local-dir> <remote-dir>")
|
|
1380
|
-
.description("Sync a local directory into a sandbox")
|
|
1381
|
-
.requiredOption("--sandbox <id>", "Sandbox ID")
|
|
1382
|
-
.option("--delete", "Delete the remote directory contents before extracting")
|
|
1383
|
-
.option("--force", "Skip the --delete confirmation prompt")
|
|
1384
|
-
.option("--yes", "Alias for --force")
|
|
1385
|
-
.option("--json", "Output as JSON")
|
|
1386
|
-
.action(async (localDir, remoteDir, opts) => {
|
|
1387
|
-
try {
|
|
1388
|
-
const result = await uploadDirToSandbox(opts.sandbox, localDir, remoteDir, {
|
|
1389
|
-
delete: !!opts.delete,
|
|
1390
|
-
force: !!(opts.force || opts.yes),
|
|
1391
|
-
});
|
|
1392
|
-
if (isJsonMode(opts)) {
|
|
1393
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
console.log(chalk.green(`Synced ${result.files_label} → ${remoteDir}`));
|
|
1397
|
-
}
|
|
1398
|
-
catch (err) {
|
|
1399
|
-
handleError(err);
|
|
1400
|
-
}
|
|
1401
|
-
});
|
|
1402
|
-
sandbox
|
|
1403
|
-
.command("cp <source> <target>")
|
|
1404
|
-
.alias("copy")
|
|
1405
|
-
.description("Copy a local file or directory into a sandbox, e.g. ./app/. sbx_123:/workspace")
|
|
1406
|
-
.option("--delete", "Delete the remote directory contents before extracting directories")
|
|
1407
|
-
.option("--force", "Skip the --delete confirmation prompt")
|
|
1408
|
-
.option("--yes", "Alias for --force")
|
|
1409
|
-
.option("--json", "Output as JSON")
|
|
1410
|
-
.action(async (source, target, opts) => {
|
|
1411
|
-
try {
|
|
1412
|
-
if (isSandboxTarget(source) && !isSandboxTarget(target)) {
|
|
1413
|
-
const parsedSource = parseSandboxTarget(source);
|
|
1414
|
-
const result = await downloadSandboxPath(parsedSource.sandboxId, parsedSource.remotePath, target);
|
|
1415
|
-
if (isJsonMode(opts)) {
|
|
1416
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1417
|
-
return;
|
|
1418
|
-
}
|
|
1419
|
-
console.log(chalk.green(`Copied ${source} → ${target}`));
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
if (isSandboxTarget(source) || !isSandboxTarget(target)) {
|
|
1423
|
-
throw new UserError("Invalid copy direction.", "Use local → sandbox (`./app/. sbx_123:/workspace`) or sandbox → local (`sbx_123:/workspace/dist ./dist`).");
|
|
1424
|
-
}
|
|
1425
|
-
const parsed = parseSandboxTarget(target);
|
|
1426
|
-
const local = source.endsWith("/.") ? source.slice(0, -2) : source;
|
|
1427
|
-
const stat = fs.statSync(local);
|
|
1428
|
-
if (stat.isDirectory()) {
|
|
1429
|
-
const result = await uploadDirToSandbox(parsed.sandboxId, local, parsed.remotePath, {
|
|
1430
|
-
delete: !!opts.delete,
|
|
1431
|
-
force: !!(opts.force || opts.yes),
|
|
1432
|
-
});
|
|
1433
|
-
if (isJsonMode(opts)) {
|
|
1434
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
console.log(chalk.green(`Copied ${result.files_label} → ${target}`));
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
const c = client();
|
|
1441
|
-
await uploadFileToSandbox(c, parsed.sandboxId, local, parsed.remotePath);
|
|
1442
|
-
if (isJsonMode(opts)) {
|
|
1443
|
-
console.log(JSON.stringify({
|
|
1444
|
-
sandbox_id: parsed.sandboxId,
|
|
1445
|
-
local_path: path.resolve(local),
|
|
1446
|
-
remote_path: parsed.remotePath,
|
|
1447
|
-
}, null, 2));
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
console.log(chalk.green(`Copied ${path.basename(local)} → ${target}`));
|
|
1451
|
-
}
|
|
1452
|
-
catch (err) {
|
|
1453
|
-
handleError(err);
|
|
1454
|
-
}
|
|
1455
|
-
});
|
|
1456
|
-
// download — GET /sandboxes/:id/files/:path; write to --output or stdout.
|
|
1457
|
-
sandbox
|
|
1458
|
-
.command("download <sandbox-id> <remote-path>")
|
|
1459
|
-
.description("Download a file from a Sandbox to a local path or stdout")
|
|
1460
|
-
.option("--output <file>", "Write to this local file instead of stdout")
|
|
1461
|
-
.option("--json", "Output raw JSON response")
|
|
1462
|
-
.action(async (id, remotePath, opts) => {
|
|
1463
|
-
try {
|
|
1464
|
-
const encoded = enc(remotePath.replace(/^\//, ""));
|
|
1465
|
-
const c = client();
|
|
1466
|
-
const bytes = await c.apiGetBinary(apiPath(`/sandboxes/${enc(id)}/files/${encoded}`));
|
|
1467
|
-
if (opts.output) {
|
|
1468
|
-
fs.writeFileSync(opts.output, bytes);
|
|
1469
|
-
if (isJsonMode(opts)) {
|
|
1470
|
-
console.log(JSON.stringify({
|
|
1471
|
-
sandbox_id: id,
|
|
1472
|
-
remote_path: remotePath,
|
|
1473
|
-
output: opts.output,
|
|
1474
|
-
bytes: bytes.length,
|
|
1475
|
-
}, null, 2));
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
console.log(chalk.green(`Downloaded ${remotePath} → ${opts.output}`));
|
|
1479
|
-
}
|
|
1480
|
-
else {
|
|
1481
|
-
if (isJsonMode(opts)) {
|
|
1482
|
-
console.log(JSON.stringify({
|
|
1483
|
-
sandbox_id: id,
|
|
1484
|
-
remote_path: remotePath,
|
|
1485
|
-
bytes: bytes.length,
|
|
1486
|
-
content_base64: bytes.toString("base64"),
|
|
1487
|
-
}, null, 2));
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
process.stdout.write(bytes);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
catch (err) {
|
|
1494
|
-
handleError(err);
|
|
1495
|
-
}
|
|
1496
|
-
});
|
|
1497
|
-
sandbox
|
|
1498
|
-
.command("export <sandbox-id> <remote-paths...>")
|
|
1499
|
-
.description("Create a portable export for sandbox-generated files; optionally download it")
|
|
1500
|
-
.option("--label <label>", "Human-readable export label")
|
|
1501
|
-
.option("--filename <name>", "Filename to use when downloading")
|
|
1502
|
-
.option("--output <file>", "Download the file/archive to this local path")
|
|
1503
|
-
.option("--json", "Output as JSON")
|
|
1504
|
-
.action(async (id, remotePaths, opts) => {
|
|
1505
|
-
try {
|
|
1506
|
-
if (remotePaths.length === 0) {
|
|
1507
|
-
throw new UserError("At least one remote path is required.");
|
|
1508
|
-
}
|
|
1509
|
-
const body = remotePaths.length === 1
|
|
1510
|
-
? { path: remotePaths[0] }
|
|
1511
|
-
: { paths: remotePaths };
|
|
1512
|
-
if (opts.label)
|
|
1513
|
-
body["label"] = opts.label;
|
|
1514
|
-
if (opts.filename)
|
|
1515
|
-
body["filename"] = opts.filename;
|
|
1516
|
-
const exportData = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/exports`), body));
|
|
1517
|
-
if (opts.output) {
|
|
1518
|
-
const query = new URLSearchParams();
|
|
1519
|
-
if (remotePaths.length === 1) {
|
|
1520
|
-
query.set("path", remotePaths[0] ?? "");
|
|
1521
|
-
}
|
|
1522
|
-
else {
|
|
1523
|
-
for (const remotePath of remotePaths) {
|
|
1524
|
-
query.append("paths[]", remotePath);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
if (opts.filename)
|
|
1528
|
-
query.set("filename", opts.filename);
|
|
1529
|
-
const bytes = await client().apiGetBinary(apiPath(`/sandboxes/${enc(id)}/exports/download?${query.toString()}`));
|
|
1530
|
-
fs.writeFileSync(opts.output, bytes);
|
|
1531
|
-
}
|
|
1532
|
-
if (isJsonMode(opts)) {
|
|
1533
|
-
console.log(JSON.stringify(opts.output
|
|
1534
|
-
? { ...exportData, downloaded_to: opts.output }
|
|
1535
|
-
: exportData, null, 2));
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
console.log();
|
|
1539
|
-
console.log(kvPanel([
|
|
1540
|
-
{ label: "Export", value: chalk.bold(str(exportData["id"])) },
|
|
1541
|
-
{ label: "Sandbox", value: id },
|
|
1542
|
-
{
|
|
1543
|
-
label: "Status",
|
|
1544
|
-
value: statusColor(str(exportData["status"])),
|
|
1545
|
-
},
|
|
1546
|
-
{
|
|
1547
|
-
label: "Archive",
|
|
1548
|
-
value: str(exportData["archive_download_url"]),
|
|
1549
|
-
},
|
|
1550
|
-
...(opts.output
|
|
1551
|
-
? [{ label: "Downloaded", value: opts.output }]
|
|
1552
|
-
: []),
|
|
1553
|
-
]));
|
|
1554
|
-
const files = Array.isArray(exportData["files"])
|
|
1555
|
-
? exportData["files"]
|
|
1556
|
-
: [];
|
|
1557
|
-
if (files.length > 0) {
|
|
1558
|
-
console.log();
|
|
1559
|
-
renderTable(files, [
|
|
1560
|
-
{ header: "PATH", key: "path" },
|
|
1561
|
-
{
|
|
1562
|
-
header: "DOWNLOAD URL",
|
|
1563
|
-
key: "download_url",
|
|
1564
|
-
},
|
|
1565
|
-
]);
|
|
1566
|
-
}
|
|
1567
|
-
console.log();
|
|
1568
|
-
}
|
|
1569
|
-
catch (err) {
|
|
1570
|
-
handleError(err);
|
|
1571
|
-
}
|
|
1572
|
-
});
|
|
1573
|
-
// logs — GET /sandboxes/:id/logs
|
|
1574
|
-
sandbox
|
|
1575
|
-
.command("logs <sandbox-id>")
|
|
1576
|
-
.description("Show Sandbox logs")
|
|
1577
|
-
.option("--lines <n>", "Number of log lines to fetch", parseIntegerOption)
|
|
1578
|
-
.option("--json", "Output as JSON")
|
|
1579
|
-
.action((id, opts) => runAction(async () => {
|
|
1580
|
-
const qs = opts.lines != null ? `?lines=${opts.lines}` : "";
|
|
1581
|
-
await getAndPrint(`/sandboxes/${enc(id)}/logs${qs}`, opts);
|
|
1582
|
-
}));
|
|
1583
|
-
// ssh — WS tunnel → local listener → spawn system ssh client
|
|
1584
|
-
sandbox
|
|
1585
|
-
.command("ssh <sandbox-id>")
|
|
1586
|
-
.description("Open an SSH session into a running Sandbox via the platform WS tunnel")
|
|
1587
|
-
.option("-p, --port <n>", "Local port for the tunnel listener", parseIntegerOption)
|
|
1588
|
-
.option("-l, --user <name>", "SSH user (default: root)")
|
|
1589
|
-
.option("--no-spawn", "Print connection info instead of spawning the ssh client")
|
|
1590
|
-
.option("--json", "Output as JSON")
|
|
1591
|
-
.action(async (id, opts) => {
|
|
1592
|
-
try {
|
|
1593
|
-
await runSandboxSsh(id, opts);
|
|
1594
|
-
}
|
|
1595
|
-
catch (err) {
|
|
1596
|
-
handleError(err);
|
|
1597
|
-
}
|
|
1598
|
-
});
|
|
1599
|
-
sandbox
|
|
1600
|
-
.command("shell <sandbox-id>")
|
|
1601
|
-
.description("Open an interactive shell into a running Sandbox")
|
|
1602
|
-
.option("-p, --port <n>", "Local port for the tunnel listener", parseIntegerOption)
|
|
1603
|
-
.option("-l, --user <name>", "SSH user (default: root)")
|
|
1604
|
-
.option("--json", "Output connection info as JSON")
|
|
1605
|
-
.action(async (id, opts) => {
|
|
1606
|
-
try {
|
|
1607
|
-
await runSandboxSsh(id, { ...opts, spawn: !isJsonMode(opts) });
|
|
1608
|
-
}
|
|
1609
|
-
catch (err) {
|
|
1610
|
-
handleError(err);
|
|
1611
|
-
}
|
|
1612
|
-
});
|
|
1613
|
-
// port-forward — TCP listener → WS tunnel → sandbox port
|
|
1614
|
-
sandbox
|
|
1615
|
-
.command("port-forward <sandbox-id>")
|
|
1616
|
-
.alias("forward")
|
|
1617
|
-
.description([
|
|
1618
|
-
"Forward a local TCP port to a port inside a Sandbox via the platform WS tunnel.",
|
|
1619
|
-
"",
|
|
1620
|
-
"Examples:",
|
|
1621
|
-
" miosa sandbox port-forward sbx_123 --remote 5173",
|
|
1622
|
-
" miosa sandbox port-forward sbx_123 --remote 5432 --local 15432",
|
|
1623
|
-
].join("\n"))
|
|
1624
|
-
.requiredOption("--remote <port>", "Port inside the sandbox to reach", parseIntegerOption)
|
|
1625
|
-
.option("--local <port>", "Local port to listen on (default = remote port)", parseIntegerOption)
|
|
1626
|
-
.option("--wait", "Probe the local forwarded URL before returning readiness")
|
|
1627
|
-
.option("--timeout <sec>", "Wait timeout in seconds", parseDurationSec, 30)
|
|
1628
|
-
.option("--probe-path <path>", "HTTP path to probe when --wait is set", "/")
|
|
1629
|
-
.option("--json", "Output as JSON")
|
|
1630
|
-
.action(async (id, opts) => {
|
|
1631
|
-
try {
|
|
1632
|
-
await runSandboxPortForward(id, {
|
|
1633
|
-
remotePort: opts.remote,
|
|
1634
|
-
localPort: opts.local ?? opts.remote,
|
|
1635
|
-
wait: !!opts.wait,
|
|
1636
|
-
timeoutSec: opts.timeout,
|
|
1637
|
-
probePath: opts.probePath,
|
|
1638
|
-
json: !!isJsonMode(opts),
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
catch (err) {
|
|
1642
|
-
handleError(err);
|
|
1643
|
-
}
|
|
1644
|
-
});
|
|
1645
|
-
// checkpoint — POST /sandboxes/:id/snapshots
|
|
1646
|
-
addDataOption(sandbox
|
|
1647
|
-
.command("checkpoint <sandbox-id>")
|
|
1648
|
-
.description("Create a Checkpoint (memory state snapshot) of a Sandbox"))
|
|
1649
|
-
.option("--json", "Output as JSON")
|
|
1650
|
-
.action((id, opts) => runAction(() => postAndPrint(`/sandboxes/${enc(id)}/snapshots`, opts, {})));
|
|
1651
|
-
sandbox
|
|
1652
|
-
.command("snapshot <sandbox-id>")
|
|
1653
|
-
.description("Create a Sandbox snapshot")
|
|
1654
|
-
.option("--name <name>", "Human-readable snapshot name/comment")
|
|
1655
|
-
.option("--comment <comment>", "Snapshot comment")
|
|
1656
|
-
.option("--stop", "Pause the sandbox after snapshot creation")
|
|
1657
|
-
.option("--expiration <duration>", "Requested retention duration, e.g. 14d")
|
|
1658
|
-
.option("--json", "Output as JSON")
|
|
1659
|
-
.action((id, opts) => runAction(async () => {
|
|
1660
|
-
const body = {};
|
|
1661
|
-
const comment = opts.comment ?? opts.name;
|
|
1662
|
-
if (comment)
|
|
1663
|
-
body["comment"] = comment;
|
|
1664
|
-
if (opts.expiration)
|
|
1665
|
-
body["expiration"] = opts.expiration;
|
|
1666
|
-
const snapshot = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/snapshots`), body));
|
|
1667
|
-
if (opts.stop) {
|
|
1668
|
-
await client().apiPost(apiPath(`/sandboxes/${enc(id)}/pause`), {});
|
|
1669
|
-
}
|
|
1670
|
-
if (isJsonMode(opts)) {
|
|
1671
|
-
console.log(JSON.stringify({ snapshot, stopped: !!opts.stop }, null, 2));
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
|
-
const snap = (snapshot ?? {});
|
|
1675
|
-
console.log(chalk.green("Snapshot requested"));
|
|
1676
|
-
if (snap["id"])
|
|
1677
|
-
console.log(String(snap["id"]));
|
|
1678
|
-
if (opts.stop)
|
|
1679
|
-
console.log(chalk.dim("Sandbox pause requested."));
|
|
1680
|
-
}));
|
|
1681
|
-
const snapshots = sandbox
|
|
1682
|
-
.command("snapshots")
|
|
1683
|
-
.description("List, inspect, and delete Sandbox snapshots");
|
|
1684
|
-
snapshots
|
|
1685
|
-
.command("list <sandbox-id>")
|
|
1686
|
-
.description("List snapshots for a sandbox")
|
|
1687
|
-
.option("--json", "Output as JSON")
|
|
1688
|
-
.action((id, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/snapshots`, opts)));
|
|
1689
|
-
snapshots
|
|
1690
|
-
.command("get <sandbox-id> <snapshot-id>")
|
|
1691
|
-
.description("Get a sandbox snapshot")
|
|
1692
|
-
.option("--json", "Output as JSON")
|
|
1693
|
-
.action((id, snapshotId, opts) => runAction(() => getAndPrint(`/sandboxes/${enc(id)}/snapshots/${enc(snapshotId)}`, opts)));
|
|
1694
|
-
snapshots
|
|
1695
|
-
.command("delete <sandbox-id> <snapshot-id>")
|
|
1696
|
-
.alias("rm")
|
|
1697
|
-
.description("Delete a sandbox snapshot")
|
|
1698
|
-
.option("--json", "Output as JSON")
|
|
1699
|
-
.action((id, snapshotId, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}/snapshots/${enc(snapshotId)}`, opts)));
|
|
1700
|
-
// delete-checkpoint / delete-snapshot
|
|
1701
|
-
sandbox
|
|
1702
|
-
.command("delete-checkpoint <sandbox-id> <checkpoint-id>")
|
|
1703
|
-
.alias("delete-snapshot")
|
|
1704
|
-
.description("Delete a Sandbox Checkpoint")
|
|
1705
|
-
.option("--json", "Output as JSON")
|
|
1706
|
-
.action((id, sid, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}/snapshots/${enc(sid)}`, opts)));
|
|
1707
|
-
// -------------------------------------------------------------------------
|
|
1708
|
-
// sandbox-templates — top-level command group for template management
|
|
1709
|
-
// -------------------------------------------------------------------------
|
|
1710
|
-
const templates = program
|
|
1711
|
-
.command("sandbox-templates")
|
|
1712
|
-
.description("Manage Sandbox templates");
|
|
1713
|
-
templates
|
|
1714
|
-
.command("list")
|
|
1715
|
-
.description("List all available Sandbox templates")
|
|
1716
|
-
.option("--include-aliases", "Include template aliases in the response")
|
|
1717
|
-
.option("--json", "Output as JSON")
|
|
1718
|
-
.action((opts) => runAction(async () => {
|
|
1719
|
-
const qs = opts.includeAliases ? "?include_aliases=true" : "";
|
|
1720
|
-
await getAndPrint(`/sandbox-templates${qs}`, opts);
|
|
1721
|
-
}));
|
|
1722
|
-
templates
|
|
1723
|
-
.command("get <template-id>")
|
|
1724
|
-
.description("Show a Sandbox template by ID or slug")
|
|
1725
|
-
.option("--json", "Output as JSON")
|
|
1726
|
-
.action((id, opts) => runAction(() => getAndPrint(`/sandbox-templates/${enc(id)}`, opts)));
|
|
1727
|
-
}
|
|
1728
|
-
function registerSandboxConnectorCommands(sandbox) {
|
|
1729
|
-
const connectors = sandbox
|
|
1730
|
-
.command("connectors")
|
|
1731
|
-
.alias("providers")
|
|
1732
|
-
.description("Manage brokered provider connector bindings for a Sandbox");
|
|
1733
|
-
connectors
|
|
1734
|
-
.command("list <sandbox-id>")
|
|
1735
|
-
.description("List provider connectors bound to a Sandbox")
|
|
1736
|
-
.option("--json", "Output as JSON")
|
|
1737
|
-
.action((id, opts) => runAction(async () => {
|
|
1738
|
-
const raw = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}/connectors`)));
|
|
1739
|
-
printValue(raw, opts);
|
|
1740
|
-
}));
|
|
1741
|
-
connectors
|
|
1742
|
-
.command("attach <sandbox-id> <connector>")
|
|
1743
|
-
.description("Attach a connector to a Sandbox as brokered env")
|
|
1744
|
-
.requiredOption("--env <name>", "Environment variable name, e.g. ANTHROPIC_API_KEY")
|
|
1745
|
-
.option("--mode <mode>", "brokered-env or plain-env", "brokered-env")
|
|
1746
|
-
.option("--installation-id <id>", "Provider installation ID")
|
|
1747
|
-
.option("--json", "Output as JSON")
|
|
1748
|
-
.action((id, connector, opts) => runAction(async () => {
|
|
1749
|
-
const body = {
|
|
1750
|
-
connector,
|
|
1751
|
-
env_name: opts.env,
|
|
1752
|
-
mode: normalizeConnectorMode(opts.mode ?? "brokered-env"),
|
|
1753
|
-
installation_id: opts.installationId,
|
|
1754
|
-
};
|
|
1755
|
-
const raw = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/connectors`), body));
|
|
1756
|
-
printValue(raw, opts);
|
|
1757
|
-
}));
|
|
1758
|
-
connectors
|
|
1759
|
-
.command("detach <sandbox-id> <binding-id-or-connector>")
|
|
1760
|
-
.description("Detach a connector binding from a Sandbox")
|
|
1761
|
-
.option("--json", "Output as JSON")
|
|
1762
|
-
.action((id, binding, opts) => runAction(() => deleteAndPrint(`/sandboxes/${enc(id)}/connectors/${enc(binding)}`, opts)));
|
|
1763
|
-
connectors
|
|
1764
|
-
.command("sync <sandbox-id>")
|
|
1765
|
-
.description("Sync connector placeholder env vars into a running Sandbox")
|
|
1766
|
-
.option("--json", "Output as JSON")
|
|
1767
|
-
.action((id, opts) => runAction(async () => {
|
|
1768
|
-
const raw = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/connectors/sync`), {}));
|
|
1769
|
-
printValue(raw, opts);
|
|
1770
|
-
}));
|
|
1771
|
-
}
|
|
1772
|
-
function normalizeConnectorMode(value) {
|
|
1773
|
-
return value.replaceAll("_", "-");
|
|
1774
|
-
}
|
|
1775
|
-
// ── sandbox ssh implementation ─────────────────────────────────────────────
|
|
1776
|
-
//
|
|
1777
|
-
// Protocol:
|
|
1778
|
-
// 1. Ensure the user has an SSH keypair at ~/.ssh/miosa_sandbox_ed25519.
|
|
1779
|
-
// If not, generate one and register it via POST /sandboxes/:id/ssh-keys.
|
|
1780
|
-
// 2. Open a local TCP server on a random (or chosen) port.
|
|
1781
|
-
// 3. Each accepted connection is bridged bidirectionally to the platform WS
|
|
1782
|
-
// endpoint wss://<api>/api/v1/sandboxes/:id/ssh-tunnel.
|
|
1783
|
-
// 4. Spawn `ssh` pointing at localhost:<port> with the key and user.
|
|
1784
|
-
// 5. On ssh exit, close the TCP server and exit.
|
|
1785
|
-
const SANDBOX_KEY_PATH = path.join(os.homedir(), ".ssh", "miosa_sandbox_ed25519");
|
|
1786
|
-
async function ensureSandboxSshKey(id, config) {
|
|
1787
|
-
if (!fs.existsSync(SANDBOX_KEY_PATH)) {
|
|
1788
|
-
console.log(chalk.dim("Generating SSH keypair for MIOSA sandbox access..."));
|
|
1789
|
-
fs.mkdirSync(path.join(os.homedir(), ".ssh"), { recursive: true });
|
|
1790
|
-
await new Promise((resolve, reject) => {
|
|
1791
|
-
const child = spawn("ssh-keygen", [
|
|
1792
|
-
"-t",
|
|
1793
|
-
"ed25519",
|
|
1794
|
-
"-f",
|
|
1795
|
-
SANDBOX_KEY_PATH,
|
|
1796
|
-
"-N",
|
|
1797
|
-
"",
|
|
1798
|
-
"-C",
|
|
1799
|
-
"miosa-sandbox",
|
|
1800
|
-
], { stdio: "pipe" });
|
|
1801
|
-
child.on("close", (code) => {
|
|
1802
|
-
if (code === 0)
|
|
1803
|
-
resolve();
|
|
1804
|
-
else
|
|
1805
|
-
reject(new Error(`ssh-keygen exited with code ${code}`));
|
|
1806
|
-
});
|
|
1807
|
-
child.on("error", reject);
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
// Register the public key with every target sandbox. The local key can
|
|
1811
|
-
// already exist from a previous sandbox, but a fresh sandbox still needs it
|
|
1812
|
-
// installed in authorized_keys before SSH auth can succeed.
|
|
1813
|
-
const pubKey = fs.readFileSync(`${SANDBOX_KEY_PATH}.pub`, "utf8").trim();
|
|
1814
|
-
const endpoint = config.endpoint.replace(/\/$/, "");
|
|
1815
|
-
const response = await fetch(`${endpoint}${apiPath(`/sandboxes/${encodeURIComponent(id)}/ssh-keys`)}`, {
|
|
1816
|
-
method: "POST",
|
|
1817
|
-
headers: {
|
|
1818
|
-
...sandboxTransportHeaders(config),
|
|
1819
|
-
"Content-Type": "application/json",
|
|
1820
|
-
},
|
|
1821
|
-
body: JSON.stringify({ public_key: pubKey }),
|
|
1822
|
-
});
|
|
1823
|
-
if (!response.ok) {
|
|
1824
|
-
const rawBody = await response.text();
|
|
1825
|
-
let body = {};
|
|
1826
|
-
try {
|
|
1827
|
-
body = JSON.parse(rawBody);
|
|
1828
|
-
}
|
|
1829
|
-
catch {
|
|
1830
|
-
body = { message: rawBody || `HTTP ${response.status}` };
|
|
1831
|
-
}
|
|
1832
|
-
throw mapHttpError(response.status, body, rawBody, response.headers.get("x-request-id"));
|
|
1833
|
-
}
|
|
1834
|
-
console.log(chalk.green("SSH key registered."));
|
|
1835
|
-
}
|
|
1836
|
-
function sandboxTransportHeaders(config) {
|
|
1837
|
-
if (!config.api_key)
|
|
1838
|
-
throw new Error("Not authenticated. Run: miosa login");
|
|
1839
|
-
const headers = {
|
|
1840
|
-
Authorization: `Bearer ${String(config.api_key)}`,
|
|
1841
|
-
};
|
|
1842
|
-
if (config.tenant)
|
|
1843
|
-
headers["X-MIOSA-Tenant"] = config.tenant;
|
|
1844
|
-
if (config.workspace)
|
|
1845
|
-
headers["X-MIOSA-Workspace"] = config.workspace;
|
|
1846
|
-
return headers;
|
|
1847
|
-
}
|
|
1848
|
-
export function buildSandboxWebSocketRequest(config, id) {
|
|
1849
|
-
const base = config.endpoint.replace(/\/$/, "");
|
|
1850
|
-
const wsBase = base.replace(/^https?/, (protocol) => protocol === "https" ? "wss" : "ws");
|
|
1851
|
-
const url = new URL(`${wsBase}/api/v1/sandboxes/${encodeURIComponent(id)}/ssh-tunnel`);
|
|
1852
|
-
const headers = sandboxTransportHeaders(config);
|
|
1853
|
-
if (config.tenant) {
|
|
1854
|
-
url.searchParams.set("tenant", config.tenant);
|
|
1855
|
-
}
|
|
1856
|
-
if (config.workspace) {
|
|
1857
|
-
url.searchParams.set("workspace", config.workspace);
|
|
1858
|
-
}
|
|
1859
|
-
return { url: url.toString(), headers };
|
|
1860
|
-
}
|
|
1861
|
-
function bridgeSandboxWs(socket, request) {
|
|
1862
|
-
let closed = false;
|
|
1863
|
-
function cleanup() {
|
|
1864
|
-
if (closed)
|
|
1865
|
-
return;
|
|
1866
|
-
closed = true;
|
|
1867
|
-
if (!socket.destroyed)
|
|
1868
|
-
socket.destroy();
|
|
1869
|
-
}
|
|
1870
|
-
const ws = new WebSocket(request.url, {
|
|
1871
|
-
headers: request.headers,
|
|
1872
|
-
});
|
|
1873
|
-
ws.on("open", () => {
|
|
1874
|
-
socket.on("data", (chunk) => {
|
|
1875
|
-
if (ws.readyState === WebSocket.OPEN)
|
|
1876
|
-
ws.send(chunk);
|
|
1877
|
-
});
|
|
1878
|
-
socket.on("close", () => {
|
|
1879
|
-
if (ws.readyState === WebSocket.OPEN)
|
|
1880
|
-
ws.close();
|
|
1881
|
-
});
|
|
1882
|
-
socket.on("error", () => ws.close());
|
|
1883
|
-
});
|
|
1884
|
-
ws.on("message", (data) => {
|
|
1885
|
-
if (!socket.destroyed) {
|
|
1886
|
-
socket.write(typeof data === "string" ? Buffer.from(data) : data);
|
|
1887
|
-
}
|
|
1888
|
-
});
|
|
1889
|
-
ws.on("close", cleanup);
|
|
1890
|
-
ws.on("error", (err) => {
|
|
1891
|
-
process.stderr.write(`\r\nWS error for sandbox tunnel: ${err.message}\r\n`);
|
|
1892
|
-
cleanup();
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
async function runSandboxSsh(id, opts) {
|
|
1896
|
-
const config = loadConfig();
|
|
1897
|
-
const wsRequest = buildSandboxWebSocketRequest(config, id);
|
|
1898
|
-
await ensureSandboxSshKey(id, config);
|
|
1899
|
-
// Pick a free local port
|
|
1900
|
-
const localPort = opts.port ?? (await pickFreePort());
|
|
1901
|
-
const user = opts.user ?? "root";
|
|
1902
|
-
if (isJsonMode(opts)) {
|
|
1903
|
-
console.log(JSON.stringify({
|
|
1904
|
-
sandbox_id: id,
|
|
1905
|
-
local_port: localPort,
|
|
1906
|
-
user,
|
|
1907
|
-
ws_url: wsRequest.url,
|
|
1908
|
-
key_path: SANDBOX_KEY_PATH,
|
|
1909
|
-
}));
|
|
1910
|
-
return;
|
|
1911
|
-
}
|
|
1912
|
-
const server = createServer((socket) => {
|
|
1913
|
-
bridgeSandboxWs(socket, wsRequest);
|
|
1914
|
-
});
|
|
1915
|
-
await new Promise((resolve, reject) => {
|
|
1916
|
-
server.on("error", reject);
|
|
1917
|
-
server.listen(localPort, "127.0.0.1", resolve);
|
|
1918
|
-
});
|
|
1919
|
-
console.log(chalk.green("Tunnel ready.") +
|
|
1920
|
-
chalk.dim(` 127.0.0.1:${localPort} → sandbox ${id}`));
|
|
1921
|
-
if (opts.spawn === false) {
|
|
1922
|
-
console.log(chalk.dim(`Connect manually: ssh -i ${SANDBOX_KEY_PATH} -p ${localPort} ${user}@127.0.0.1`));
|
|
1923
|
-
console.log(chalk.dim("Press Ctrl+C to close."));
|
|
1924
|
-
await waitForSignal();
|
|
1925
|
-
server.close();
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
const sshArgs = [
|
|
1929
|
-
"-i",
|
|
1930
|
-
SANDBOX_KEY_PATH,
|
|
1931
|
-
"-p",
|
|
1932
|
-
String(localPort),
|
|
1933
|
-
"-o",
|
|
1934
|
-
"StrictHostKeyChecking=no",
|
|
1935
|
-
"-o",
|
|
1936
|
-
"UserKnownHostsFile=/dev/null",
|
|
1937
|
-
"-o",
|
|
1938
|
-
"LogLevel=ERROR",
|
|
1939
|
-
`${user}@127.0.0.1`,
|
|
1940
|
-
];
|
|
1941
|
-
const sshProc = spawn("ssh", sshArgs, { stdio: "inherit" });
|
|
1942
|
-
const exitCode = await new Promise((resolve) => {
|
|
1943
|
-
sshProc.on("close", (code) => resolve(code ?? 1));
|
|
1944
|
-
sshProc.on("error", (err) => {
|
|
1945
|
-
console.error(chalk.red(`Failed to spawn ssh: ${err.message}`));
|
|
1946
|
-
resolve(1);
|
|
1947
|
-
});
|
|
1948
|
-
});
|
|
1949
|
-
server.close();
|
|
1950
|
-
process.exit(exitCode);
|
|
1951
|
-
}
|
|
1952
|
-
async function runSandboxPortForward(id, opts) {
|
|
1953
|
-
const preview = await previewSandbox(id, opts.remotePort, {
|
|
1954
|
-
wait: true,
|
|
1955
|
-
timeout: opts.timeoutSec,
|
|
1956
|
-
probePath: opts.probePath,
|
|
1957
|
-
});
|
|
1958
|
-
const upstreamBase = new URL(preview.url);
|
|
1959
|
-
const stats = { active: 0, total: 0, bytesIn: 0, bytesOut: 0 };
|
|
1960
|
-
const server = http.createServer((req, res) => {
|
|
1961
|
-
stats.active++;
|
|
1962
|
-
stats.total++;
|
|
1963
|
-
const target = new URL(req.url ?? "/", upstreamBase);
|
|
1964
|
-
const headers = { ...req.headers, host: target.host };
|
|
1965
|
-
const mod = target.protocol === "https:" ? https : http;
|
|
1966
|
-
const proxyReq = mod.request(target, { method: req.method, headers }, (proxyRes) => {
|
|
1967
|
-
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
1968
|
-
proxyRes.on("data", (chunk) => {
|
|
1969
|
-
stats.bytesOut += chunk.length;
|
|
1970
|
-
});
|
|
1971
|
-
proxyRes.pipe(res);
|
|
1972
|
-
});
|
|
1973
|
-
req.on("data", (chunk) => {
|
|
1974
|
-
stats.bytesIn += chunk.length;
|
|
1975
|
-
});
|
|
1976
|
-
proxyReq.on("error", (err) => {
|
|
1977
|
-
if (!res.headersSent)
|
|
1978
|
-
res.writeHead(502, { "content-type": "text/plain" });
|
|
1979
|
-
res.end(`MIOSA port-forward upstream error: ${err.message}`);
|
|
1980
|
-
});
|
|
1981
|
-
res.on("close", () => {
|
|
1982
|
-
stats.active = Math.max(0, stats.active - 1);
|
|
1983
|
-
});
|
|
1984
|
-
req.pipe(proxyReq);
|
|
1985
|
-
});
|
|
1986
|
-
await new Promise((resolve, reject) => {
|
|
1987
|
-
server.on("error", (err) => {
|
|
1988
|
-
if (err.code === "EADDRINUSE") {
|
|
1989
|
-
reject(new Error(`Port ${opts.localPort} is already in use. Choose another with --local.`));
|
|
1990
|
-
}
|
|
1991
|
-
else {
|
|
1992
|
-
reject(err);
|
|
1993
|
-
}
|
|
1994
|
-
});
|
|
1995
|
-
server.listen(opts.localPort, "127.0.0.1", resolve);
|
|
1996
|
-
});
|
|
1997
|
-
let localProbe = null;
|
|
1998
|
-
if (opts.wait) {
|
|
1999
|
-
localProbe = await waitForLocalHttp(opts.localPort, opts.probePath, opts.timeoutSec);
|
|
2000
|
-
}
|
|
2001
|
-
if (isJsonMode(opts)) {
|
|
2002
|
-
console.log(JSON.stringify({
|
|
2003
|
-
sandbox_id: id,
|
|
2004
|
-
remote_port: opts.remotePort,
|
|
2005
|
-
local_port: opts.localPort,
|
|
2006
|
-
upstream_url: preview.url,
|
|
2007
|
-
ready: localProbe?.ok ?? !opts.wait,
|
|
2008
|
-
status: localProbe?.status ?? null,
|
|
2009
|
-
latency_ms: localProbe?.latency_ms ?? null,
|
|
2010
|
-
}));
|
|
2011
|
-
}
|
|
2012
|
-
else {
|
|
2013
|
-
console.log(`${chalk.green(opts.wait ? "Forwarding ready" : "Forwarding")} ${chalk.cyan(`localhost:${opts.localPort}`)} ${chalk.dim("→")} sandbox ${chalk.cyan(id)}:${chalk.bold(String(opts.remotePort))}`);
|
|
2014
|
-
console.log(chalk.dim(`Upstream: ${preview.url}`));
|
|
2015
|
-
console.log(chalk.dim("Press Ctrl+C to close.\n"));
|
|
2016
|
-
}
|
|
2017
|
-
const ticker = setInterval(() => {
|
|
2018
|
-
if (isJsonMode(opts) || stats.active === 0)
|
|
2019
|
-
return;
|
|
2020
|
-
process.stderr.write(chalk.dim(`\r[${new Date().toLocaleTimeString()}] connections: ${stats.active} ↑ ${formatBytes(stats.bytesIn)} ↓ ${formatBytes(stats.bytesOut)} `));
|
|
2021
|
-
}, 5_000);
|
|
2022
|
-
await waitForSignal();
|
|
2023
|
-
clearInterval(ticker);
|
|
2024
|
-
process.stderr.write("\n");
|
|
2025
|
-
server.close();
|
|
2026
|
-
}
|
|
2027
|
-
class SandboxDeployPartialError extends Error {
|
|
2028
|
-
causeError;
|
|
2029
|
-
sandboxId;
|
|
2030
|
-
recoveryCommand;
|
|
2031
|
-
constructor(causeError, sandboxId, recoveryCommand) {
|
|
2032
|
-
super(causeError instanceof Error ? causeError.message : String(causeError));
|
|
2033
|
-
this.causeError = causeError;
|
|
2034
|
-
this.sandboxId = sandboxId;
|
|
2035
|
-
this.recoveryCommand = recoveryCommand;
|
|
2036
|
-
this.name = "SandboxDeployPartialError";
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
async function showSandboxWithPreview(sandboxId, port, probePath) {
|
|
2040
|
-
const c = client();
|
|
2041
|
-
const sandbox = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
|
|
2042
|
-
let preview = null;
|
|
2043
|
-
try {
|
|
2044
|
-
preview = await previewSandbox(sandboxId, port, {
|
|
2045
|
-
wait: false,
|
|
2046
|
-
timeout: 1,
|
|
2047
|
-
probePath,
|
|
2048
|
-
});
|
|
2049
|
-
}
|
|
2050
|
-
catch (err) {
|
|
2051
|
-
preview = {
|
|
2052
|
-
url: "",
|
|
2053
|
-
ready: false,
|
|
2054
|
-
status: null,
|
|
2055
|
-
latency_ms: null,
|
|
2056
|
-
error: err instanceof Error ? err.message : String(err),
|
|
2057
|
-
};
|
|
2058
|
-
}
|
|
2059
|
-
return {
|
|
2060
|
-
...sandbox,
|
|
2061
|
-
ready: preview?.ready ?? false,
|
|
2062
|
-
preview: {
|
|
2063
|
-
url: preview?.url || null,
|
|
2064
|
-
port,
|
|
2065
|
-
route_ready: Boolean(preview?.url),
|
|
2066
|
-
tls_ready: preview?.ready ?? false,
|
|
2067
|
-
last_status: preview?.status ?? null,
|
|
2068
|
-
latency_ms: preview?.latency_ms ?? null,
|
|
2069
|
-
error: preview?.error,
|
|
2070
|
-
},
|
|
2071
|
-
};
|
|
2072
|
-
}
|
|
2073
|
-
async function previewSandbox(sandboxId, port, opts) {
|
|
2074
|
-
const c = client();
|
|
2075
|
-
const exposed = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), {
|
|
2076
|
-
port,
|
|
2077
|
-
title: "app preview",
|
|
2078
|
-
}));
|
|
2079
|
-
const url = extractUrl(exposed);
|
|
2080
|
-
if (!url)
|
|
2081
|
-
throw new UserError("Sandbox expose did not return a preview URL.");
|
|
2082
|
-
const edge = opts.wait
|
|
2083
|
-
? await waitForPublicPreview(url, opts.probePath, opts.timeout)
|
|
2084
|
-
: await probePublicPreview(url, opts.probePath);
|
|
2085
|
-
return {
|
|
2086
|
-
url,
|
|
2087
|
-
ready: edge.ok,
|
|
2088
|
-
status: edge.status,
|
|
2089
|
-
latency_ms: edge.latency_ms ?? null,
|
|
2090
|
-
error: edge.error,
|
|
2091
|
-
};
|
|
2092
|
-
}
|
|
2093
|
-
async function waitSandboxReady(sandboxId, port, probePath, timeoutSec) {
|
|
2094
|
-
const c = client();
|
|
2095
|
-
await waitForSandboxRunning(c, sandboxId, Math.min(timeoutSec, 120));
|
|
2096
|
-
const internal = await waitForInternalHttp(c, sandboxId, port, probePath, Math.min(timeoutSec, 60));
|
|
2097
|
-
const preview = await previewSandbox(sandboxId, port, {
|
|
2098
|
-
wait: true,
|
|
2099
|
-
timeout: timeoutSec,
|
|
2100
|
-
probePath,
|
|
2101
|
-
});
|
|
2102
|
-
return {
|
|
2103
|
-
sandbox_id: sandboxId,
|
|
2104
|
-
port,
|
|
2105
|
-
internal_status: internal.status,
|
|
2106
|
-
...preview,
|
|
2107
|
-
};
|
|
2108
|
-
}
|
|
2109
|
-
async function waitSandboxVmReady(sandboxId, timeoutSec) {
|
|
2110
|
-
// `sandbox wait` reports readiness for use — confirm command-readiness so a
|
|
2111
|
-
// successful wait guarantees exec works.
|
|
2112
|
-
const sandbox = await waitForSandboxRunning(client(), sandboxId, timeoutSec, true);
|
|
2113
|
-
return {
|
|
2114
|
-
...sandbox,
|
|
2115
|
-
sandbox_id: sandboxId,
|
|
2116
|
-
ready: true,
|
|
2117
|
-
url: null,
|
|
2118
|
-
};
|
|
2119
|
-
}
|
|
2120
|
-
async function startSandboxService(sandboxId, name, opts) {
|
|
2121
|
-
validateServiceName(name);
|
|
2122
|
-
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/services`), {
|
|
2123
|
-
name,
|
|
2124
|
-
command: opts.cmd,
|
|
2125
|
-
cwd: opts.cwd,
|
|
2126
|
-
}));
|
|
2127
|
-
let previewUrl = null;
|
|
2128
|
-
if (opts.port != null) {
|
|
2129
|
-
const preview = await previewSandbox(sandboxId, opts.port, {
|
|
2130
|
-
wait: false,
|
|
2131
|
-
timeout: 1,
|
|
2132
|
-
probePath: "/",
|
|
2133
|
-
});
|
|
2134
|
-
previewUrl = preview.url;
|
|
2135
|
-
}
|
|
2136
|
-
return {
|
|
2137
|
-
...result,
|
|
2138
|
-
port: opts.port ?? null,
|
|
2139
|
-
preview_url: previewUrl,
|
|
2140
|
-
};
|
|
2141
|
-
}
|
|
2142
|
-
async function createSandboxCommand(sandboxId, command, opts) {
|
|
2143
|
-
if (!command.trim())
|
|
2144
|
-
throw new UserError("Command is required.");
|
|
2145
|
-
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/commands`), {
|
|
2146
|
-
command,
|
|
2147
|
-
cwd: opts.cwd,
|
|
2148
|
-
env: opts.env,
|
|
2149
|
-
user: opts.user,
|
|
2150
|
-
sudo: opts.sudo,
|
|
2151
|
-
tty: opts.tty,
|
|
2152
|
-
interactive: opts.interactive,
|
|
2153
|
-
timeout: opts.timeout,
|
|
2154
|
-
}));
|
|
2155
|
-
return {
|
|
2156
|
-
...result,
|
|
2157
|
-
command_id: result["id"],
|
|
2158
|
-
status: result["status"] ?? "running",
|
|
2159
|
-
};
|
|
2160
|
-
}
|
|
2161
|
-
function warnOnTemplatePortMismatch(template, port, opts) {
|
|
2162
|
-
if (isJsonMode(opts))
|
|
2163
|
-
return;
|
|
2164
|
-
if ((template === "nextjs" || template === "next-js") && port !== 3000) {
|
|
2165
|
-
console.error(chalk.yellow("Next.js sandbox templates default to port 3000. Use --port 3000 unless you intentionally changed the app readiness port."));
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
function serviceLogPath(name) {
|
|
2169
|
-
return `/tmp/miosa-services/${name}.log`;
|
|
2170
|
-
}
|
|
2171
|
-
function servicePidPath(name) {
|
|
2172
|
-
return `/tmp/miosa-services/${name}.pid`;
|
|
2173
|
-
}
|
|
2174
|
-
function validateServiceName(name) {
|
|
2175
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(name)) {
|
|
2176
|
-
throw new UserError(`Invalid service name: ${name}`, "Use only letters, numbers, dot, dash, and underscore.");
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
export async function deploySandbox(localDir, opts) {
|
|
2180
|
-
const sourceDir = path.resolve(localDir);
|
|
2181
|
-
const sourceBacked = !!opts.source;
|
|
2182
|
-
if (!sourceBacked &&
|
|
2183
|
-
(!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory())) {
|
|
2184
|
-
throw new UserError(`Local directory not found: ${sourceDir}`);
|
|
2185
|
-
}
|
|
2186
|
-
const c = client();
|
|
2187
|
-
let appManifest = sourceBacked
|
|
2188
|
-
? null
|
|
2189
|
-
: (loadAppManifest(sourceDir)?.manifest ?? null);
|
|
2190
|
-
const detection = sourceBacked ? null : detectFramework(sourceDir);
|
|
2191
|
-
const port = opts.port ??
|
|
2192
|
-
opts.publishPort ??
|
|
2193
|
-
manifestPort(appManifest) ??
|
|
2194
|
-
detection?.port ??
|
|
2195
|
-
5173;
|
|
2196
|
-
warnOnTemplatePortMismatch(opts.template ?? appManifest?.template ?? null, port, opts);
|
|
2197
|
-
const probePath = opts.probePath ?? manifestProbePath(appManifest) ?? "/";
|
|
2198
|
-
let remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
|
|
2199
|
-
const start = opts.start ??
|
|
2200
|
-
manifestStartCommand(appManifest) ??
|
|
2201
|
-
defaultStartCommand(detection?.framework ?? appManifest?.framework, port);
|
|
2202
|
-
let sandboxId = opts.sandbox ?? null;
|
|
2203
|
-
try {
|
|
2204
|
-
if (!sandboxId) {
|
|
2205
|
-
deployStep(opts, "Creating sandbox");
|
|
2206
|
-
sandboxId = await createSandboxForDeploy(c, opts.template ?? appManifest?.template ?? "miosa-sandbox", opts.name, {
|
|
2207
|
-
source: opts.source,
|
|
2208
|
-
revision: opts.revision,
|
|
2209
|
-
depth: opts.depth,
|
|
2210
|
-
});
|
|
2211
|
-
}
|
|
2212
|
-
deployStep(opts, "Waiting for sandbox");
|
|
2213
|
-
await waitForSandboxRunning(c, sandboxId, Math.min(opts.timeout, 120));
|
|
2214
|
-
if (sourceBacked) {
|
|
2215
|
-
deployStep(opts, "Waiting for source import");
|
|
2216
|
-
appManifest = await readRemoteAppManifest(c, sandboxId, opts.timeout);
|
|
2217
|
-
remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
|
|
2218
|
-
}
|
|
2219
|
-
else {
|
|
2220
|
-
deployStep(opts, "Uploading files");
|
|
2221
|
-
const archivePath = createDeployArchive(sourceDir);
|
|
2222
|
-
const remoteArchive = `/tmp/miosa-deploy-${Date.now()}.tgz`;
|
|
2223
|
-
try {
|
|
2224
|
-
await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
|
|
2225
|
-
deployStep(opts, "Extracting workspace");
|
|
2226
|
-
await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(remoteWorkdir)} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteWorkdir)}`, "/");
|
|
2227
|
-
}
|
|
2228
|
-
finally {
|
|
2229
|
-
fs.rmSync(archivePath, { force: true });
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
const resolvedPort = opts.port ?? opts.publishPort ?? manifestPort(appManifest) ?? port;
|
|
2233
|
-
const resolvedProbePath = opts.probePath ?? manifestProbePath(appManifest) ?? probePath;
|
|
2234
|
-
const resolvedStart = opts.start ?? manifestStartCommand(appManifest) ?? start;
|
|
2235
|
-
const installCommand = opts.install === false
|
|
2236
|
-
? null
|
|
2237
|
-
: (opts.installCommand ??
|
|
2238
|
-
(appManifest?.install === false ? null : appManifest?.install) ??
|
|
2239
|
-
(sourceBacked ? "npm install" : defaultInstallCommand(sourceDir)));
|
|
2240
|
-
if (installCommand) {
|
|
2241
|
-
deployStep(opts, `Installing dependencies: ${installCommand}`);
|
|
2242
|
-
await execSandbox(c, sandboxId, installCommand, remoteWorkdir, opts.timeout);
|
|
2243
|
-
}
|
|
2244
|
-
deployStep(opts, `Starting app on port ${resolvedPort}`);
|
|
2245
|
-
await execSandbox(c, sandboxId, `fuser -k ${resolvedPort}/tcp >/dev/null 2>&1 || true; nohup sh -lc ${shellQuote(resolvedStart)} > ${shellQuote(`/tmp/miosa-app-${resolvedPort}.log`)} 2>&1 & echo $!`, remoteWorkdir);
|
|
2246
|
-
deployStep(opts, "Checking internal app readiness");
|
|
2247
|
-
const internal = await waitForInternalHttp(c, sandboxId, resolvedPort, resolvedProbePath, Math.min(opts.timeout, 60));
|
|
2248
|
-
deployStep(opts, "Creating public preview route");
|
|
2249
|
-
const exposed = await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), { port: resolvedPort, title: "app preview" });
|
|
2250
|
-
const previewUrl = extractUrl(unwrap(exposed));
|
|
2251
|
-
if (!previewUrl) {
|
|
2252
|
-
throw new UserError("Sandbox expose did not return a preview URL.");
|
|
2253
|
-
}
|
|
2254
|
-
if (opts.wait)
|
|
2255
|
-
deployStep(opts, "Checking public preview readiness");
|
|
2256
|
-
const edge = opts.wait
|
|
2257
|
-
? await waitForPublicPreview(previewUrl, resolvedProbePath, opts.timeout)
|
|
2258
|
-
: { ok: false, status: null };
|
|
2259
|
-
return {
|
|
2260
|
-
sandbox_id: sandboxId,
|
|
2261
|
-
port: resolvedPort,
|
|
2262
|
-
preview_url: previewUrl,
|
|
2263
|
-
preview_ready: edge.ok,
|
|
2264
|
-
internal_status: internal.status,
|
|
2265
|
-
edge_status: edge.status,
|
|
2266
|
-
latency_ms: edge.latency_ms ?? null,
|
|
2267
|
-
};
|
|
2268
|
-
}
|
|
2269
|
-
catch (err) {
|
|
2270
|
-
if (sandboxId) {
|
|
2271
|
-
throw new SandboxDeployPartialError(err, sandboxId, recoveryCommandForSandboxDeploy(sandboxId, opts, localDir));
|
|
2272
|
-
}
|
|
2273
|
-
throw err;
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
const NON_TERMINAL_DEPLOY_STATES = new Set([
|
|
2277
|
-
"building",
|
|
2278
|
-
"pending",
|
|
2279
|
-
"queued",
|
|
2280
|
-
"deploying",
|
|
2281
|
-
]);
|
|
2282
|
-
// Bug 9: look for an existing non-terminal deployment with a matching name so
|
|
2283
|
-
// retries attach a new release instead of creating a duplicate app. Defensive:
|
|
2284
|
-
// returns null (fall back to create) if the list call fails or finds nothing.
|
|
2285
|
-
async function findExistingDeploymentByName(c, name) {
|
|
2286
|
-
try {
|
|
2287
|
-
const raw = unwrap(await c.apiGet(apiPath("/deployments")));
|
|
2288
|
-
const items = Array.isArray(raw)
|
|
2289
|
-
? raw
|
|
2290
|
-
: Array.isArray((asRecord(raw) ?? {})["data"])
|
|
2291
|
-
? (asRecord(raw) ?? {})["data"]
|
|
2292
|
-
: [];
|
|
2293
|
-
for (const item of items) {
|
|
2294
|
-
const rec = asRecord(item);
|
|
2295
|
-
if (!rec)
|
|
2296
|
-
continue;
|
|
2297
|
-
if (stringField(rec, "name") !== name)
|
|
2298
|
-
continue;
|
|
2299
|
-
const state = (stringField(rec, "state") ?? "").toLowerCase();
|
|
2300
|
-
const id = stringField(rec, "id");
|
|
2301
|
-
if (id && NON_TERMINAL_DEPLOY_STATES.has(state)) {
|
|
2302
|
-
return { id, state };
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
return null;
|
|
2306
|
-
}
|
|
2307
|
-
catch {
|
|
2308
|
-
return null;
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
async function publishSandbox(sandboxId, opts) {
|
|
2312
|
-
const c = client();
|
|
2313
|
-
const body = {
|
|
2314
|
-
output_path: opts.path,
|
|
2315
|
-
path: opts.path,
|
|
2316
|
-
environment: opts.environment,
|
|
2317
|
-
metadata: opts.dockerDeploy
|
|
2318
|
-
? { environment: opts.environment, deployment_product: "docker_deploy" }
|
|
2319
|
-
: { environment: opts.environment },
|
|
2320
|
-
};
|
|
2321
|
-
if (opts.dockerDeploy)
|
|
2322
|
-
body["deployment_type"] = "docker-deploy";
|
|
2323
|
-
if (opts.app)
|
|
2324
|
-
body["deployment_id"] = opts.app;
|
|
2325
|
-
// Bug 9: avoid creating a duplicate deployment on retry. When publishing by
|
|
2326
|
-
// name (no explicit app id), reuse an existing non-terminal deployment with
|
|
2327
|
-
// the same name by attaching a new release to it.
|
|
2328
|
-
if (opts.name && !opts.app) {
|
|
2329
|
-
const existing = await findExistingDeploymentByName(c, opts.name);
|
|
2330
|
-
if (existing) {
|
|
2331
|
-
body["deployment_id"] = existing.id;
|
|
2332
|
-
deployStep(opts, chalk.dim(`Attaching to existing deployment ${existing.id} (${existing.state})`));
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
if (opts.name)
|
|
2336
|
-
body["name"] = opts.name;
|
|
2337
|
-
if (opts.slug)
|
|
2338
|
-
body["slug"] = opts.slug;
|
|
2339
|
-
if (opts.buildCommand)
|
|
2340
|
-
body["build_command"] = opts.buildCommand;
|
|
2341
|
-
if (opts.runCommand)
|
|
2342
|
-
body["run_command"] = opts.runCommand;
|
|
2343
|
-
if (opts.domain)
|
|
2344
|
-
body["domain"] = opts.domain;
|
|
2345
|
-
if (opts.port != null)
|
|
2346
|
-
body["port"] = opts.port;
|
|
2347
|
-
const deploymentType = opts.dockerDeploy
|
|
2348
|
-
? "docker-deploy"
|
|
2349
|
-
: opts.deploymentType;
|
|
2350
|
-
if (deploymentType)
|
|
2351
|
-
body["deployment_type"] = deploymentType;
|
|
2352
|
-
const database = parsePublishDatabase(opts.database);
|
|
2353
|
-
if (database !== undefined)
|
|
2354
|
-
body["database"] = database;
|
|
2355
|
-
deployStep(opts, "Publishing sandbox workspace to durable deployment");
|
|
2356
|
-
const raw = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/publish`), body));
|
|
2357
|
-
const response = asRecord(raw) ?? {};
|
|
2358
|
-
const data = asRecord(response["data"]) ?? response;
|
|
2359
|
-
const deployment = asRecord(data["deployment"]);
|
|
2360
|
-
const version = asRecord(data["version"]);
|
|
2361
|
-
const release = asRecord(data["release"]);
|
|
2362
|
-
const deploymentId = stringField(response, "deployment_id") ??
|
|
2363
|
-
stringField(deployment, "id") ??
|
|
2364
|
-
null;
|
|
2365
|
-
const versionId = stringField(response, "version_id") ?? stringField(version, "id") ?? null;
|
|
2366
|
-
const releaseId = stringField(response, "release_id") ?? stringField(release, "id") ?? null;
|
|
2367
|
-
let state = stringField(response, "state") ?? stringField(deployment, "state") ?? null;
|
|
2368
|
-
let url = stringField(response, "url") ??
|
|
2369
|
-
extractUrl(deployment) ??
|
|
2370
|
-
stringField(data, "url") ??
|
|
2371
|
-
null;
|
|
2372
|
-
let deploymentProduct = stringField(response, "deployment_product") ??
|
|
2373
|
-
stringField(data, "deployment_product") ??
|
|
2374
|
-
stringField(deployment, "deployment_product") ??
|
|
2375
|
-
stringField(asRecord(deployment?.["metadata"]), "deployment_product") ??
|
|
2376
|
-
null;
|
|
2377
|
-
let dockerDeployHostId = stringField(response, "docker_deploy_host_id") ??
|
|
2378
|
-
stringField(data, "docker_deploy_host_id") ??
|
|
2379
|
-
stringField(deployment, "docker_deploy_host_id") ??
|
|
2380
|
-
stringField(asRecord(deployment?.["metadata"]), "docker_deploy_host_id") ??
|
|
2381
|
-
null;
|
|
2382
|
-
if (opts.wait && deploymentId) {
|
|
2383
|
-
deployStep(opts, "Waiting for durable deployment");
|
|
2384
|
-
const waited = await waitForDeploymentReady(c, deploymentId, opts.timeout);
|
|
2385
|
-
state = stringField(waited, "state") ?? state;
|
|
2386
|
-
url = extractUrl(waited) ?? url;
|
|
2387
|
-
deploymentProduct =
|
|
2388
|
-
stringField(waited, "deployment_product") ??
|
|
2389
|
-
stringField(asRecord(waited["metadata"]), "deployment_product") ??
|
|
2390
|
-
deploymentProduct;
|
|
2391
|
-
dockerDeployHostId =
|
|
2392
|
-
stringField(waited, "docker_deploy_host_id") ??
|
|
2393
|
-
stringField(asRecord(waited["metadata"]), "docker_deploy_host_id") ??
|
|
2394
|
-
dockerDeployHostId;
|
|
2395
|
-
response["state"] = state;
|
|
2396
|
-
if (url)
|
|
2397
|
-
response["url"] = url;
|
|
2398
|
-
data["deployment"] = waited;
|
|
2399
|
-
data["promotion_pending"] = false;
|
|
2400
|
-
data["app_consistency_pending"] = false;
|
|
2401
|
-
if (url)
|
|
2402
|
-
data["url"] = url;
|
|
2403
|
-
}
|
|
2404
|
-
let probe = null;
|
|
2405
|
-
if (opts.wait && url) {
|
|
2406
|
-
deployStep(opts, "Checking production URL readiness");
|
|
2407
|
-
probe = await waitForPublicPreview(url, "/", opts.timeout);
|
|
2408
|
-
}
|
|
2409
|
-
const ready = (state === null || ["running", "active"].includes(state.toLowerCase())) &&
|
|
2410
|
-
(opts.wait ? probe?.ok !== false : true);
|
|
2411
|
-
return {
|
|
2412
|
-
type: "deployment",
|
|
2413
|
-
sandbox_id: sandboxId,
|
|
2414
|
-
deployment_id: deploymentId,
|
|
2415
|
-
app_id: deploymentId,
|
|
2416
|
-
release_id: releaseId,
|
|
2417
|
-
version_id: versionId,
|
|
2418
|
-
url,
|
|
2419
|
-
state,
|
|
2420
|
-
deployment_product: deploymentProduct,
|
|
2421
|
-
docker_deploy_host_id: dockerDeployHostId,
|
|
2422
|
-
ready,
|
|
2423
|
-
probe,
|
|
2424
|
-
data: raw,
|
|
2425
|
-
};
|
|
2426
|
-
}
|
|
2427
|
-
async function waitForDeploymentReady(c, deploymentId, timeoutSec) {
|
|
2428
|
-
const deadline = Date.now() + timeoutSec * 1000;
|
|
2429
|
-
let last = null;
|
|
2430
|
-
while (Date.now() < deadline) {
|
|
2431
|
-
const value = unwrap(await c.apiGet(apiPath(`/deployments/${enc(deploymentId)}`)));
|
|
2432
|
-
const deployment = asRecord(value) ?? {};
|
|
2433
|
-
last = deployment;
|
|
2434
|
-
const state = String(deployment["state"] ?? "").toLowerCase();
|
|
2435
|
-
if (state === "running" || state === "active")
|
|
2436
|
-
return deployment;
|
|
2437
|
-
if (state === "failed" || state === "error") {
|
|
2438
|
-
throw new UserError(`Deployment ${deploymentId} entered ${state} state.`, String(deployment["error_message"] ?? deployment["error"] ?? ""));
|
|
2439
|
-
}
|
|
2440
|
-
await sleep(2000);
|
|
2441
|
-
}
|
|
2442
|
-
const lastState = last ? String(last["state"] ?? "unknown") : "unknown";
|
|
2443
|
-
throw new UserError(`Deployment still building after ${timeoutSec}s — it may still finish. Re-check with \`miosa sandbox show ${deploymentId}\` or \`miosa deploy logs\`.`, `Last state: ${lastState}`);
|
|
2444
|
-
}
|
|
2445
|
-
function parsePublishDatabase(value) {
|
|
2446
|
-
if (!value)
|
|
2447
|
-
return undefined;
|
|
2448
|
-
const normalized = value.toLowerCase();
|
|
2449
|
-
if (normalized === "none")
|
|
2450
|
-
return false;
|
|
2451
|
-
if (normalized === "postgres" ||
|
|
2452
|
-
normalized === "postgresql" ||
|
|
2453
|
-
normalized === "create:postgres" ||
|
|
2454
|
-
normalized === "create:postgresql") {
|
|
2455
|
-
return { engine: "postgresql", engine_version: "16" };
|
|
2456
|
-
}
|
|
2457
|
-
if (normalized.startsWith("existing:")) {
|
|
2458
|
-
return { existing_database_id: value.slice("existing:".length) };
|
|
2459
|
-
}
|
|
2460
|
-
return value;
|
|
2461
|
-
}
|
|
2462
|
-
function deployStep(opts, label) {
|
|
2463
|
-
if (!isJsonMode(opts))
|
|
2464
|
-
console.error(chalk.dim(`→ ${label}`));
|
|
2465
|
-
}
|
|
2466
|
-
async function doctorSandbox(sandboxId, port, probePath) {
|
|
2467
|
-
const c = client();
|
|
2468
|
-
const sandbox = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
|
|
2469
|
-
const internal = await probeInternalHttp(c, sandboxId, port, probePath);
|
|
2470
|
-
let exposeData = {};
|
|
2471
|
-
try {
|
|
2472
|
-
exposeData = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), {
|
|
2473
|
-
port,
|
|
2474
|
-
title: "doctor probe",
|
|
2475
|
-
}));
|
|
2476
|
-
}
|
|
2477
|
-
catch (err) {
|
|
2478
|
-
exposeData = { error: err instanceof Error ? err.message : String(err) };
|
|
2479
|
-
}
|
|
2480
|
-
const previewUrl = extractUrl(exposeData);
|
|
2481
|
-
const edge = previewUrl
|
|
2482
|
-
? await probePublicPreview(previewUrl, probePath)
|
|
2483
|
-
: { ok: false, status: null, error: "No preview URL returned" };
|
|
2484
|
-
return {
|
|
2485
|
-
sandbox_id: sandboxId,
|
|
2486
|
-
sandbox_state: sandbox["state"] ?? sandbox["status"] ?? "unknown",
|
|
2487
|
-
process: internal.ok ? "listening" : "not_ready",
|
|
2488
|
-
route: previewUrl ? "created" : "missing",
|
|
2489
|
-
tls: edge.error && /tls|certificate|ssl/i.test(edge.error)
|
|
2490
|
-
? "not_ready"
|
|
2491
|
-
: previewUrl
|
|
2492
|
-
? "checked"
|
|
2493
|
-
: "unknown",
|
|
2494
|
-
internal_probe: internal,
|
|
2495
|
-
edge_probe: edge,
|
|
2496
|
-
preview_ready: edge.ok,
|
|
2497
|
-
preview_url: previewUrl,
|
|
2498
|
-
expose: exposeData,
|
|
2499
|
-
};
|
|
2500
|
-
}
|
|
2501
|
-
function renderDoctorReport(report) {
|
|
2502
|
-
console.log();
|
|
2503
|
-
console.log(chalk.bold("Sandbox Doctor"));
|
|
2504
|
-
console.log();
|
|
2505
|
-
console.log(` ${chalk.bold("Sandbox")} ${report["sandbox_id"]}`);
|
|
2506
|
-
console.log(` ${chalk.bold("State")} ${report["sandbox_state"]}`);
|
|
2507
|
-
console.log(` ${chalk.bold("Process")} ${report["process"]}`);
|
|
2508
|
-
console.log(` ${chalk.bold("Route")} ${report["route"]}`);
|
|
2509
|
-
console.log(` ${chalk.bold("TLS/edge")} ${report["tls"]}`);
|
|
2510
|
-
console.log(` ${chalk.bold("Preview ready")} ${report["preview_ready"] ? chalk.green("yes") : chalk.red("no")}`);
|
|
2511
|
-
if (report["preview_url"]) {
|
|
2512
|
-
console.log(` ${chalk.bold("Preview URL")} ${chalk.cyan(String(report["preview_url"]))}`);
|
|
2513
|
-
}
|
|
2514
|
-
console.log();
|
|
2515
|
-
if (!report["preview_ready"]) {
|
|
2516
|
-
console.log(chalk.yellow(" Preview is not externally ready yet. Internal app health and edge probe details are in --json output."));
|
|
2517
|
-
console.log();
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
async function recoverSandboxDeploy(idOrName, opts) {
|
|
2521
|
-
const c = client();
|
|
2522
|
-
const resolved = await resolveSandboxForRecovery(c, idOrName);
|
|
2523
|
-
if (!resolved.sandbox) {
|
|
2524
|
-
return {
|
|
2525
|
-
query: idOrName,
|
|
2526
|
-
sandbox: null,
|
|
2527
|
-
sandbox_id: null,
|
|
2528
|
-
matched_by: null,
|
|
2529
|
-
exec_ok: false,
|
|
2530
|
-
app_port: opts.port ?? null,
|
|
2531
|
-
preview_ready: null,
|
|
2532
|
-
preview_url: null,
|
|
2533
|
-
doctor: null,
|
|
2534
|
-
recommendations: [
|
|
2535
|
-
"No matching sandbox was found. Run `miosa sandbox list --json` and retry with a sandbox ID.",
|
|
2536
|
-
],
|
|
2537
|
-
commands: {
|
|
2538
|
-
list: "miosa sandbox list --json",
|
|
2539
|
-
},
|
|
2540
|
-
};
|
|
2541
|
-
}
|
|
2542
|
-
const sandbox = resolved.sandbox;
|
|
2543
|
-
const sandboxId = str(sandbox["id"]);
|
|
2544
|
-
const name = stringOrNull(sandbox["name"]);
|
|
2545
|
-
const template = stringOrNull(sandbox["template_id"]);
|
|
2546
|
-
const port = opts.port ?? suggestedRecoveryPort(template);
|
|
2547
|
-
const execOk = await checkSandboxExec(c, sandboxId);
|
|
2548
|
-
const doctor = port != null
|
|
2549
|
-
? await safeDoctorSandbox(sandboxId, port, opts.probePath)
|
|
2550
|
-
: null;
|
|
2551
|
-
const previewUrl = stringOrNull(doctor?.["preview_url"]);
|
|
2552
|
-
const previewReady = doctor && typeof doctor["preview_ready"] === "boolean"
|
|
2553
|
-
? Boolean(doctor["preview_ready"])
|
|
2554
|
-
: null;
|
|
2555
|
-
const recommendations = buildRecoveryRecommendations({
|
|
2556
|
-
sandbox,
|
|
2557
|
-
execOk,
|
|
2558
|
-
port,
|
|
2559
|
-
previewReady,
|
|
2560
|
-
template,
|
|
2561
|
-
});
|
|
2562
|
-
return {
|
|
2563
|
-
query: idOrName,
|
|
2564
|
-
sandbox,
|
|
2565
|
-
sandbox_id: sandboxId,
|
|
2566
|
-
matched_by: resolved.matchedBy,
|
|
2567
|
-
exec_ok: execOk,
|
|
2568
|
-
app_port: port,
|
|
2569
|
-
preview_ready: previewReady,
|
|
2570
|
-
preview_url: previewUrl,
|
|
2571
|
-
doctor,
|
|
2572
|
-
recommendations,
|
|
2573
|
-
commands: recoveryCommands(sandboxId, name, port),
|
|
2574
|
-
};
|
|
2575
|
-
}
|
|
2576
|
-
async function resolveSandboxForRecovery(c, idOrName) {
|
|
2577
|
-
try {
|
|
2578
|
-
const direct = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(idOrName)}`)));
|
|
2579
|
-
const sandbox = asRecord(direct);
|
|
2580
|
-
if (sandbox)
|
|
2581
|
-
return { sandbox, matchedBy: "id" };
|
|
2582
|
-
}
|
|
2583
|
-
catch {
|
|
2584
|
-
// Fall through to name lookup.
|
|
2585
|
-
}
|
|
2586
|
-
const list = unwrap(await c.apiGet(apiPath("/sandboxes")));
|
|
2587
|
-
const items = Array.isArray(list)
|
|
2588
|
-
? list
|
|
2589
|
-
: Array.isArray((asRecord(list) ?? {})["data"])
|
|
2590
|
-
? (asRecord(list) ?? {})["data"]
|
|
2591
|
-
: [];
|
|
2592
|
-
const matches = items
|
|
2593
|
-
.map(asRecord)
|
|
2594
|
-
.filter((item) => Boolean(item))
|
|
2595
|
-
.filter((item) => {
|
|
2596
|
-
const name = stringOrNull(item["name"]);
|
|
2597
|
-
const id = stringOrNull(item["id"]);
|
|
2598
|
-
return name === idOrName || id?.startsWith(idOrName);
|
|
2599
|
-
})
|
|
2600
|
-
.sort((a, b) => {
|
|
2601
|
-
const aTime = Date.parse(stringOrNull(a["created_at"]) ?? "") || 0;
|
|
2602
|
-
const bTime = Date.parse(stringOrNull(b["created_at"]) ?? "") || 0;
|
|
2603
|
-
return bTime - aTime;
|
|
2604
|
-
});
|
|
2605
|
-
return { sandbox: matches[0] ?? null, matchedBy: matches[0] ? "name" : null };
|
|
2606
|
-
}
|
|
2607
|
-
async function checkSandboxExec(c, sandboxId) {
|
|
2608
|
-
try {
|
|
2609
|
-
const result = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), {
|
|
2610
|
-
command: "pwd",
|
|
2611
|
-
}));
|
|
2612
|
-
const record = asRecord(result);
|
|
2613
|
-
return Number(record?.["exit_code"] ?? 0) === 0;
|
|
2614
|
-
}
|
|
2615
|
-
catch {
|
|
2616
|
-
return false;
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
|
-
async function safeDoctorSandbox(sandboxId, port, probePath) {
|
|
2620
|
-
try {
|
|
2621
|
-
return await doctorSandbox(sandboxId, port, probePath);
|
|
2622
|
-
}
|
|
2623
|
-
catch (err) {
|
|
2624
|
-
return {
|
|
2625
|
-
preview_ready: false,
|
|
2626
|
-
error: err instanceof Error ? err.message : String(err),
|
|
2627
|
-
};
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
function suggestedRecoveryPort(template) {
|
|
2631
|
-
if (template === "nextjs" || template === "next-js")
|
|
2632
|
-
return 3000;
|
|
2633
|
-
return null;
|
|
2634
|
-
}
|
|
2635
|
-
function buildRecoveryRecommendations(input) {
|
|
2636
|
-
const recs = [];
|
|
2637
|
-
const state = stringOrNull(input.sandbox["state"]);
|
|
2638
|
-
const ready = Boolean(input.sandbox["ready"]);
|
|
2639
|
-
if (state !== "running") {
|
|
2640
|
-
recs.push(`Sandbox state is ${state ?? "unknown"}; wait or recreate before uploading files.`);
|
|
2641
|
-
}
|
|
2642
|
-
else if (!ready) {
|
|
2643
|
-
recs.push("Sandbox exists but is not fully ready; try exec/write-file before retrying deploy.");
|
|
2644
|
-
}
|
|
2645
|
-
if (!input.execOk) {
|
|
2646
|
-
recs.push("Exec health failed; retry later or recreate the sandbox.");
|
|
2647
|
-
}
|
|
2648
|
-
else {
|
|
2649
|
-
recs.push("Exec works; if upload/deploy failed, use write-file/patch plus service up.");
|
|
2650
|
-
}
|
|
2651
|
-
if (input.template === "nextjs" && input.port !== 3000) {
|
|
2652
|
-
recs.push("Next.js templates default to port 3000; use 3000 unless you intentionally reconfigured readiness.");
|
|
2653
|
-
}
|
|
2654
|
-
if (input.port != null && input.previewReady === false) {
|
|
2655
|
-
recs.push("Preview is not ready; start the app process, then run sandbox wait.");
|
|
2656
|
-
}
|
|
2657
|
-
if (input.previewReady === true) {
|
|
2658
|
-
recs.push("Preview is ready; publish the sandbox to a durable deployment.");
|
|
2659
|
-
}
|
|
2660
|
-
return recs;
|
|
2661
|
-
}
|
|
2662
|
-
function recoveryCommands(sandboxId, name, port) {
|
|
2663
|
-
const commands = {
|
|
2664
|
-
health: `miosa sandbox exec ${sandboxId} --json -- pwd`,
|
|
2665
|
-
files: `miosa sandbox write-file ${sandboxId} /workspace/app/page.jsx ./page.jsx --json`,
|
|
2666
|
-
};
|
|
2667
|
-
if (port != null) {
|
|
2668
|
-
commands.start = `miosa sandbox service up ${sandboxId} next --cwd /workspace --port ${port} --cmd "npm run dev -- --hostname 0.0.0.0 --port ${port}" --json`;
|
|
2669
|
-
commands.wait = `miosa sandbox wait ${sandboxId} --port ${port} --timeout 180 --json`;
|
|
2670
|
-
commands.publish = `miosa sandbox publish ${sandboxId} --path /workspace --name ${shellArg(name ?? "Recovered app")} --build-command "npm run build" --run-command "npm run start" --port ${port} --wait --timeout 900s --json`;
|
|
2671
|
-
}
|
|
2672
|
-
return commands;
|
|
2673
|
-
}
|
|
2674
|
-
function renderRecoverReport(report) {
|
|
2675
|
-
if (!report.sandbox_id) {
|
|
2676
|
-
console.log(chalk.red("No matching sandbox found."));
|
|
2677
|
-
console.log(` ${report.commands["list"]}`);
|
|
2678
|
-
return;
|
|
2679
|
-
}
|
|
2680
|
-
console.log(chalk.bold("Sandbox recovery"));
|
|
2681
|
-
console.log();
|
|
2682
|
-
console.log(` ${chalk.bold("Sandbox")} ${report.sandbox_id}`);
|
|
2683
|
-
console.log(` ${chalk.bold("Matched")} ${report.matched_by}`);
|
|
2684
|
-
console.log(` ${chalk.bold("Exec")} ${report.exec_ok ? chalk.green("ok") : chalk.red("failed")}`);
|
|
2685
|
-
if (report.app_port != null) {
|
|
2686
|
-
console.log(` ${chalk.bold("Port")} ${report.app_port}`);
|
|
2687
|
-
}
|
|
2688
|
-
if (report.preview_url) {
|
|
2689
|
-
console.log(` ${chalk.bold("Preview")} ${chalk.cyan(report.preview_url)}`);
|
|
2690
|
-
}
|
|
2691
|
-
if (report.preview_ready != null) {
|
|
2692
|
-
console.log(` ${chalk.bold("Ready")} ${report.preview_ready ? chalk.green("yes") : chalk.yellow("no")}`);
|
|
2693
|
-
}
|
|
2694
|
-
console.log();
|
|
2695
|
-
for (const rec of report.recommendations) {
|
|
2696
|
-
console.log(` - ${rec}`);
|
|
2697
|
-
}
|
|
2698
|
-
console.log();
|
|
2699
|
-
console.log(chalk.bold("Next commands"));
|
|
2700
|
-
for (const [label, command] of Object.entries(report.commands)) {
|
|
2701
|
-
console.log(` ${chalk.dim(label.padEnd(7))} ${command}`);
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
async function createSandboxForDeploy(c, template, name, source) {
|
|
2705
|
-
const body = { template_id: template };
|
|
2706
|
-
if (name)
|
|
2707
|
-
body["name"] = name;
|
|
2708
|
-
if (source?.source)
|
|
2709
|
-
body["source"] = source.source;
|
|
2710
|
-
if (source?.revision)
|
|
2711
|
-
body["revision"] = source.revision;
|
|
2712
|
-
if (source?.depth != null)
|
|
2713
|
-
body["depth"] = source.depth;
|
|
2714
|
-
const created = unwrap(await c.apiPost(apiPath("/sandboxes"), body));
|
|
2715
|
-
const sandboxId = typeof created["id"] === "string" ? created["id"] : "";
|
|
2716
|
-
if (!sandboxId)
|
|
2717
|
-
throw new UserError("Sandbox create did not return an id.");
|
|
2718
|
-
return sandboxId;
|
|
2719
|
-
}
|
|
2720
|
-
async function readRemoteAppManifest(c, sandboxId, timeoutSec) {
|
|
2721
|
-
const deadline = Date.now() + Math.min(timeoutSec, 120) * 1000;
|
|
2722
|
-
while (Date.now() < deadline) {
|
|
2723
|
-
for (const filename of [
|
|
2724
|
-
"miosa.app.yml",
|
|
2725
|
-
"miosa.app.yaml",
|
|
2726
|
-
"miosa.app.json",
|
|
2727
|
-
]) {
|
|
2728
|
-
const result = await c
|
|
2729
|
-
.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), {
|
|
2730
|
-
command: `test -f /workspace/${filename} && cat /workspace/${filename}`,
|
|
2731
|
-
cwd: "/workspace",
|
|
2732
|
-
timeout: 10,
|
|
2733
|
-
})
|
|
2734
|
-
.then(unwrap)
|
|
2735
|
-
.catch(() => null);
|
|
2736
|
-
const row = asRecord(result);
|
|
2737
|
-
if (Number(row?.["exit_code"] ?? 1) === 0 &&
|
|
2738
|
-
typeof row?.["stdout"] === "string") {
|
|
2739
|
-
return parseAppManifest(filename, row["stdout"]);
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2742
|
-
await sleep(1500);
|
|
2743
|
-
}
|
|
2744
|
-
return null;
|
|
2745
|
-
}
|
|
2746
|
-
async function waitForSandboxRunning(c, sandboxId, timeoutSec,
|
|
2747
|
-
// Off by default so existing callers (e.g. `sandbox deploy`, which has its
|
|
2748
|
-
// own downstream recovery) keep their behavior. The explicit user-facing
|
|
2749
|
-
// waits (`create --wait`, `sandbox wait`) opt in.
|
|
2750
|
-
requireCommandReady = false) {
|
|
2751
|
-
const deadline = Date.now() + timeoutSec * 1000;
|
|
2752
|
-
while (Date.now() < deadline) {
|
|
2753
|
-
const sandbox = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
|
|
2754
|
-
const state = String(sandbox["state"] ?? sandbox["status"] ?? "").toLowerCase();
|
|
2755
|
-
if (state === "running" || state === "active") {
|
|
2756
|
-
// A running VM is not proof that exec will work: the command agent may
|
|
2757
|
-
// not be attached yet (409 SANDBOX_NOT_COMMAND_READY). Confirm
|
|
2758
|
-
// command-readiness so `--wait` only reports success once exec works.
|
|
2759
|
-
if (requireCommandReady) {
|
|
2760
|
-
await confirmCommandReady(c, sandboxId, deadline);
|
|
2761
|
-
}
|
|
2762
|
-
return sandbox;
|
|
2763
|
-
}
|
|
2764
|
-
if (state === "error" || state === "failed") {
|
|
2765
|
-
throw sandboxStateError(sandboxId, sandbox, state);
|
|
2766
|
-
}
|
|
2767
|
-
if (state === "destroyed") {
|
|
2768
|
-
throw sandboxStateError(sandboxId, sandbox, state);
|
|
2769
|
-
}
|
|
2770
|
-
await sleep(1500);
|
|
2771
|
-
}
|
|
2772
|
-
throw new UserError(`Sandbox ${sandboxId} did not become running within ${timeoutSec}s.`);
|
|
2773
|
-
}
|
|
2774
|
-
/**
|
|
2775
|
-
* After a sandbox reaches `running`, poll `GET /sandboxes/:id/readiness` until
|
|
2776
|
-
* it is command-ready (exec will succeed). Bounded by the running-wait
|
|
2777
|
-
* `deadline`, extended by a command-ready grace window. A missing readiness
|
|
2778
|
-
* endpoint (`NOT_FOUND`, older servers) is tolerated — return without blocking.
|
|
2779
|
-
* Throws when command-readiness is not reached before the deadline.
|
|
2780
|
-
*/
|
|
2781
|
-
async function confirmCommandReady(c, sandboxId, runningDeadline) {
|
|
2782
|
-
const deadline = Math.max(runningDeadline, Date.now() + COMMAND_READY_WAIT_TIMEOUT_SEC * 1000);
|
|
2783
|
-
for (;;) {
|
|
2784
|
-
try {
|
|
2785
|
-
const readiness = unwrap(await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}/readiness`)));
|
|
2786
|
-
if (isCommandReady(readiness))
|
|
2787
|
-
return;
|
|
2788
|
-
}
|
|
2789
|
-
catch (err) {
|
|
2790
|
-
// Older servers may not expose the readiness endpoint. Don't block the
|
|
2791
|
-
// create — treat a missing endpoint as best-effort success.
|
|
2792
|
-
if (err instanceof ApiResponseError && err.code === "NOT_FOUND")
|
|
2793
|
-
return;
|
|
2794
|
-
// Other transient errors: keep polling until the deadline.
|
|
2795
|
-
}
|
|
2796
|
-
if (Date.now() >= deadline) {
|
|
2797
|
-
throw new UserError(`Sandbox ${sandboxId} reached running but its command agent did not attach in time; exec may return SANDBOX_NOT_COMMAND_READY.`, "Retry the command, or poll `miosa sandbox get <id>` before exec.");
|
|
2798
|
-
}
|
|
2799
|
-
await sleep(COMMAND_READY_POLL_INTERVAL_MS);
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
async function resumeSandboxAndPrint(sandboxId, opts) {
|
|
2803
|
-
try {
|
|
2804
|
-
const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/resume`), {}, opts.idempotencyKey
|
|
2805
|
-
? { "Idempotency-Key": opts.idempotencyKey }
|
|
2806
|
-
: undefined));
|
|
2807
|
-
printValue(result, opts);
|
|
2808
|
-
}
|
|
2809
|
-
catch (err) {
|
|
2810
|
-
if (err instanceof ApiResponseError && err.code === "SANDBOX_NOT_PAUSED") {
|
|
2811
|
-
throw await enrichSandboxLifecycleError(sandboxId, err);
|
|
2812
|
-
}
|
|
2813
|
-
throw err;
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
async function preflightSandboxConnector(sandboxId, params) {
|
|
2817
|
-
await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/connectors/preflight`), {
|
|
2818
|
-
provider: params.provider,
|
|
2819
|
-
connector: params.connector,
|
|
2820
|
-
model: params.model,
|
|
2821
|
-
cwd: params.cwd,
|
|
2822
|
-
});
|
|
2823
|
-
}
|
|
2824
|
-
/**
|
|
2825
|
-
* Propagate a remote command's exit code to the CLI process.
|
|
2826
|
-
*
|
|
2827
|
-
* A `sandbox exec` whose remote command exits nonzero must make the CLI exit
|
|
2828
|
-
* nonzero too, or CI and shell `&&` chains treat a failed command as a success.
|
|
2829
|
-
* This applies even in --json mode: the JSON already carries `exit_code`, but a
|
|
2830
|
-
* script that pipes to `jq` still relies on `$?`. Exit codes are clamped to the
|
|
2831
|
-
* 1-255 range a process can actually report; any nonzero-but-unrepresentable
|
|
2832
|
-
* code collapses to 1.
|
|
2833
|
-
*/
|
|
2834
|
-
function applyRemoteExitCode(value) {
|
|
2835
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2836
|
-
return;
|
|
2837
|
-
const raw = value["exit_code"];
|
|
2838
|
-
if (raw == null)
|
|
2839
|
-
return;
|
|
2840
|
-
const code = Number(raw);
|
|
2841
|
-
if (!Number.isFinite(code) || code === 0)
|
|
2842
|
-
return;
|
|
2843
|
-
const clamped = Number.isInteger(code) && code >= 1 && code <= 255 ? code : 1;
|
|
2844
|
-
process.exitCode = clamped;
|
|
2845
|
-
}
|
|
2846
|
-
async function postSandboxExecAndPrint(sandboxId, opts, body) {
|
|
2847
|
-
try {
|
|
2848
|
-
const value = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
|
|
2849
|
-
printValue(value, opts);
|
|
2850
|
-
applyRemoteExitCode(value);
|
|
2851
|
-
}
|
|
2852
|
-
catch (err) {
|
|
2853
|
-
if (err instanceof ApiResponseError && err.code === "SANDBOX_NOT_RUNNING") {
|
|
2854
|
-
throw await enrichSandboxLifecycleError(sandboxId, err);
|
|
2855
|
-
}
|
|
2856
|
-
throw err;
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
|
-
async function enrichSandboxLifecycleError(sandboxId, err) {
|
|
2860
|
-
let sandbox = null;
|
|
2861
|
-
try {
|
|
2862
|
-
sandbox = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(sandboxId)}`)));
|
|
2863
|
-
}
|
|
2864
|
-
catch {
|
|
2865
|
-
// Preserve the original API error when the follow-up lookup is unavailable.
|
|
2866
|
-
}
|
|
2867
|
-
if (!sandbox)
|
|
2868
|
-
return err;
|
|
2869
|
-
const state = String(sandbox["state"] ?? sandbox["status"] ?? "unknown");
|
|
2870
|
-
const details = sandboxLifecycleDetails(sandbox);
|
|
2871
|
-
const message = state === "destroyed"
|
|
2872
|
-
? `Sandbox ${sandboxId} is destroyed, not paused. It cannot be resumed.`
|
|
2873
|
-
: `${err.message}. Current sandbox state: ${state}.`;
|
|
2874
|
-
return new ApiResponseError(err.code, message, EXIT_USER_ERROR, false, sandboxRecoveryHint(sandboxId, sandbox), details, err.requestId);
|
|
2875
|
-
}
|
|
2876
|
-
function sandboxStateError(sandboxId, sandbox, state) {
|
|
2877
|
-
const reason = sandboxLastErrorReason(sandbox);
|
|
2878
|
-
const message = state === "destroyed"
|
|
2879
|
-
? `Sandbox ${sandboxId} is destroyed, not paused. It cannot be resumed.`
|
|
2880
|
-
: `Sandbox ${sandboxId} entered ${state} state${reason ? `: ${reason}` : ""}.`;
|
|
2881
|
-
return new UserError(message, sandboxRecoveryHint(sandboxId, sandbox));
|
|
2882
|
-
}
|
|
2883
|
-
function sandboxRecoveryHint(sandboxId, sandbox) {
|
|
2884
|
-
const state = String(sandbox["state"] ?? sandbox["status"] ?? "unknown");
|
|
2885
|
-
const name = String(sandbox["name"] ?? "new-sandbox");
|
|
2886
|
-
const template = String(sandbox["template_id"] ?? "nextjs");
|
|
2887
|
-
const timeoutRemainingSec = timeoutRemainingSeconds(sandbox);
|
|
2888
|
-
if (state === "running" &&
|
|
2889
|
-
timeoutRemainingSec != null &&
|
|
2890
|
-
timeoutRemainingSec <= EXPIRING_SANDBOX_THRESHOLD_SEC) {
|
|
2891
|
-
return `Sandbox expires soon. Extend it with: miosa sandbox extend ${sandboxId} --timeout 1h`;
|
|
2892
|
-
}
|
|
2893
|
-
if (state === "paused") {
|
|
2894
|
-
return `Resume it with: miosa sandbox resume ${sandboxId}`;
|
|
2895
|
-
}
|
|
2896
|
-
if (state === "destroyed") {
|
|
2897
|
-
return `Create a replacement with: miosa sandbox create --template ${template} --name ${shellQuote(name)} --timeout 1h --wait --json`;
|
|
2898
|
-
}
|
|
2899
|
-
if (state === "error" || state === "failed") {
|
|
2900
|
-
return `Inspect recovery with: miosa sandbox recover ${sandboxId}`;
|
|
2901
|
-
}
|
|
2902
|
-
return `Inspect it with: miosa sandbox show ${sandboxId} --json`;
|
|
2903
|
-
}
|
|
2904
|
-
function sandboxLifecycleDetails(sandbox) {
|
|
2905
|
-
return {
|
|
2906
|
-
sandbox_id: sandbox["id"],
|
|
2907
|
-
name: sandbox["name"],
|
|
2908
|
-
state: sandbox["state"] ?? sandbox["status"],
|
|
2909
|
-
timeout_sec: sandbox["timeout_sec"],
|
|
2910
|
-
timeout_remaining_ms: sandbox["timeout_remaining_ms"],
|
|
2911
|
-
destroyed_at: sandbox["destroyed_at"],
|
|
2912
|
-
last_error: sandboxLastError(sandbox),
|
|
2913
|
-
};
|
|
2914
|
-
}
|
|
2915
|
-
function sandboxLastErrorReason(sandbox) {
|
|
2916
|
-
const lastError = sandboxLastError(sandbox);
|
|
2917
|
-
if (!lastError)
|
|
2918
|
-
return null;
|
|
2919
|
-
const reason = lastError["reason"];
|
|
2920
|
-
return typeof reason === "string" && reason.length > 0 ? reason : null;
|
|
2921
|
-
}
|
|
2922
|
-
function sandboxLastError(sandbox) {
|
|
2923
|
-
const metadata = sandbox["metadata"];
|
|
2924
|
-
if (!metadata || typeof metadata !== "object")
|
|
2925
|
-
return null;
|
|
2926
|
-
const lastError = metadata["last_error"];
|
|
2927
|
-
return lastError && typeof lastError === "object"
|
|
2928
|
-
? lastError
|
|
2929
|
-
: null;
|
|
2930
|
-
}
|
|
2931
|
-
function timeoutRemainingSeconds(sandbox) {
|
|
2932
|
-
const ms = sandbox["timeout_remaining_ms"];
|
|
2933
|
-
if (typeof ms === "number")
|
|
2934
|
-
return Math.ceil(ms / 1000);
|
|
2935
|
-
const sec = sandbox["timeout_remaining_sec"];
|
|
2936
|
-
return typeof sec === "number" ? sec : null;
|
|
2937
|
-
}
|
|
2938
|
-
/**
|
|
2939
|
-
* Prints a yellow expiry warning when a running sandbox has <5 minutes left.
|
|
2940
|
-
* Call this in any non-JSON render path that shows sandbox state.
|
|
2941
|
-
*/
|
|
2942
|
-
function warnIfExpiringSoon(sandbox, sandboxId) {
|
|
2943
|
-
const state = String(sandbox["state"] ?? sandbox["status"] ?? "").toLowerCase();
|
|
2944
|
-
if (state !== "running" && state !== "active")
|
|
2945
|
-
return;
|
|
2946
|
-
const remainingSec = timeoutRemainingSeconds(sandbox);
|
|
2947
|
-
if (remainingSec == null || remainingSec > EXPIRING_SANDBOX_THRESHOLD_SEC)
|
|
2948
|
-
return;
|
|
2949
|
-
const mins = Math.floor(remainingSec / 60);
|
|
2950
|
-
const secs = remainingSec % 60;
|
|
2951
|
-
const humanTime = mins > 0 ? `${mins}m${secs > 0 ? `${secs}s` : ""}` : `${secs}s`;
|
|
2952
|
-
console.log(chalk.yellow(` Warning: Sandbox expires in ${humanTime}. Extend: miosa sandbox extend ${sandboxId} --timeout 1h`));
|
|
2953
|
-
console.log();
|
|
2954
|
-
}
|
|
2955
|
-
function createDeployArchive(sourceDir) {
|
|
2956
|
-
const archivePath = path.join(os.tmpdir(), `miosa-deploy-${process.pid}-${Date.now()}.tgz`);
|
|
2957
|
-
const result = spawnSync("tar", [
|
|
2958
|
-
"--exclude",
|
|
2959
|
-
".git",
|
|
2960
|
-
"--exclude",
|
|
2961
|
-
"node_modules",
|
|
2962
|
-
"--exclude",
|
|
2963
|
-
".next",
|
|
2964
|
-
"--exclude",
|
|
2965
|
-
"dist",
|
|
2966
|
-
"--exclude",
|
|
2967
|
-
".DS_Store",
|
|
2968
|
-
"--exclude",
|
|
2969
|
-
"._*",
|
|
2970
|
-
"--exclude",
|
|
2971
|
-
"__MACOSX",
|
|
2972
|
-
"-czf",
|
|
2973
|
-
archivePath,
|
|
2974
|
-
"-C",
|
|
2975
|
-
sourceDir,
|
|
2976
|
-
".",
|
|
2977
|
-
], { stdio: "pipe", env: { ...process.env, COPYFILE_DISABLE: "1" } });
|
|
2978
|
-
if (result.status !== 0) {
|
|
2979
|
-
throw new UserError(`Could not archive ${sourceDir}: ${result.stderr.toString().trim() || "tar failed"}`);
|
|
2980
|
-
}
|
|
2981
|
-
return archivePath;
|
|
2982
|
-
}
|
|
2983
|
-
async function uploadFileToSandbox(c, sandboxId, localPath, remotePath) {
|
|
2984
|
-
return writeBytesToSandbox(c, sandboxId, remotePath, fs.readFileSync(localPath));
|
|
2985
|
-
}
|
|
2986
|
-
async function writeBytesToSandbox(c, sandboxId, remotePath, bytes) {
|
|
2987
|
-
try {
|
|
2988
|
-
return await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/files`), {
|
|
2989
|
-
path: remotePath,
|
|
2990
|
-
content: bytes.toString("base64"),
|
|
2991
|
-
});
|
|
2992
|
-
}
|
|
2993
|
-
catch (err) {
|
|
2994
|
-
if (!shouldFallbackSandboxUpload(err))
|
|
2995
|
-
throw err;
|
|
2996
|
-
await writeBytesToSandboxViaExec(c, sandboxId, remotePath, bytes);
|
|
2997
|
-
return {
|
|
2998
|
-
data: {
|
|
2999
|
-
sandbox_id: sandboxId,
|
|
3000
|
-
path: remotePath,
|
|
3001
|
-
size: bytes.length,
|
|
3002
|
-
transport: "exec_chunked_fallback",
|
|
3003
|
-
},
|
|
3004
|
-
};
|
|
3005
|
-
}
|
|
3006
|
-
}
|
|
3007
|
-
async function writeBytesToSandboxViaExec(c, sandboxId, remotePath, bytes) {
|
|
3008
|
-
const base64 = bytes.toString("base64");
|
|
3009
|
-
const base64Path = `${remotePath}.b64`;
|
|
3010
|
-
const chunkSize = 48_000;
|
|
3011
|
-
await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(path.posix.dirname(remotePath))} && rm -f ${shellQuote(remotePath)} ${shellQuote(base64Path)}`, "/");
|
|
3012
|
-
for (let offset = 0; offset < base64.length; offset += chunkSize) {
|
|
3013
|
-
const chunk = base64.slice(offset, offset + chunkSize);
|
|
3014
|
-
await execSandbox(c, sandboxId, `printf '%s' ${shellQuote(chunk)} >> ${shellQuote(base64Path)}`, "/");
|
|
3015
|
-
}
|
|
3016
|
-
await execSandbox(c, sandboxId, `base64 -d ${shellQuote(base64Path)} > ${shellQuote(remotePath)} && rm -f ${shellQuote(base64Path)}`, "/");
|
|
3017
|
-
}
|
|
3018
|
-
function shouldFallbackSandboxUpload(err) {
|
|
3019
|
-
if (err instanceof NetworkError || err instanceof ServerError)
|
|
3020
|
-
return true;
|
|
3021
|
-
if (err instanceof ApiResponseError) {
|
|
3022
|
-
return (err.retryable ||
|
|
3023
|
-
/AGENT_UNAVAILABLE|SANDBOX_FILE_AGENT_UNAVAILABLE/i.test(err.code));
|
|
3024
|
-
}
|
|
3025
|
-
if (err instanceof Error) {
|
|
3026
|
-
return /fetch failed|ECONNRESET|HTTP 502|AGENT_UNAVAILABLE|other side closed|socket hang up/i.test(err.message);
|
|
3027
|
-
}
|
|
3028
|
-
return false;
|
|
3029
|
-
}
|
|
3030
|
-
function recoveryCommandForSandboxDeploy(sandboxId, opts, localDir) {
|
|
3031
|
-
const parts = [
|
|
3032
|
-
"miosa",
|
|
3033
|
-
"sandbox",
|
|
3034
|
-
"deploy",
|
|
3035
|
-
shellQuote(localDir),
|
|
3036
|
-
"--sandbox",
|
|
3037
|
-
shellQuote(sandboxId),
|
|
3038
|
-
];
|
|
3039
|
-
if (opts.port != null)
|
|
3040
|
-
parts.push("--port", String(opts.port));
|
|
3041
|
-
if (opts.publishPort != null)
|
|
3042
|
-
parts.push("--publish-port", String(opts.publishPort));
|
|
3043
|
-
if (opts.installCommand)
|
|
3044
|
-
parts.push("--install-command", shellQuote(opts.installCommand));
|
|
3045
|
-
if (opts.install === false)
|
|
3046
|
-
parts.push("--no-install");
|
|
3047
|
-
if (opts.start)
|
|
3048
|
-
parts.push("--start", shellQuote(opts.start));
|
|
3049
|
-
if (opts.wait)
|
|
3050
|
-
parts.push("--wait");
|
|
3051
|
-
if (opts.timeout != null)
|
|
3052
|
-
parts.push("--timeout", `${opts.timeout}s`);
|
|
3053
|
-
if (opts.probePath)
|
|
3054
|
-
parts.push("--probe-path", shellQuote(opts.probePath));
|
|
3055
|
-
return parts.join(" ");
|
|
3056
|
-
}
|
|
3057
|
-
function handleSandboxDeployError(err, opts) {
|
|
3058
|
-
if (err instanceof SandboxDeployPartialError) {
|
|
3059
|
-
const cause = err.causeError;
|
|
3060
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3061
|
-
const retryable = cause instanceof ApiResponseError
|
|
3062
|
-
? cause.retryable
|
|
3063
|
-
: shouldFallbackSandboxUpload(cause);
|
|
3064
|
-
if (isJsonMode(opts)) {
|
|
3065
|
-
console.log(JSON.stringify({
|
|
3066
|
-
ok: false,
|
|
3067
|
-
error: {
|
|
3068
|
-
code: errorCodeForDeployCause(cause),
|
|
3069
|
-
message,
|
|
3070
|
-
retryable,
|
|
3071
|
-
...(cause instanceof MiosaError && cause.requestId
|
|
3072
|
-
? { request_id: cause.requestId }
|
|
3073
|
-
: {}),
|
|
3074
|
-
},
|
|
3075
|
-
partial_resource: {
|
|
3076
|
-
type: "sandbox",
|
|
3077
|
-
id: err.sandboxId,
|
|
3078
|
-
recovery_command: err.recoveryCommand,
|
|
3079
|
-
},
|
|
3080
|
-
}, null, 2));
|
|
3081
|
-
return process.exit(1);
|
|
3082
|
-
}
|
|
3083
|
-
console.error(chalk.red(`Error: ${message}`));
|
|
3084
|
-
console.error(chalk.yellow(`Sandbox ${err.sandboxId} exists. Retry into it with:\n ${err.recoveryCommand}`));
|
|
3085
|
-
return process.exit(1);
|
|
3086
|
-
}
|
|
3087
|
-
handleError(err);
|
|
3088
|
-
}
|
|
3089
|
-
function errorCodeForDeployCause(err) {
|
|
3090
|
-
if (err instanceof ApiResponseError)
|
|
3091
|
-
return err.code;
|
|
3092
|
-
if (err instanceof NetworkError)
|
|
3093
|
-
return "NETWORK";
|
|
3094
|
-
if (err instanceof ServerError)
|
|
3095
|
-
return "SERVER";
|
|
3096
|
-
if (err instanceof MiosaError)
|
|
3097
|
-
return err.constructor.name.toUpperCase();
|
|
3098
|
-
if (err instanceof Error && /fetch failed|ECONNRESET/i.test(err.message))
|
|
3099
|
-
return "NETWORK";
|
|
3100
|
-
return "UNEXPECTED_ERROR";
|
|
3101
|
-
}
|
|
3102
|
-
async function uploadDirToSandbox(sandboxId, localDir, remoteDir, opts) {
|
|
3103
|
-
const sourceDir = path.resolve(localDir);
|
|
3104
|
-
if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) {
|
|
3105
|
-
throw new UserError(`Local directory not found: ${sourceDir}`);
|
|
3106
|
-
}
|
|
3107
|
-
const deleteTarget = opts.delete
|
|
3108
|
-
? await confirmRemoteDeleteDir(sandboxId, remoteDir, !!opts.force)
|
|
3109
|
-
: null;
|
|
3110
|
-
const c = client();
|
|
3111
|
-
const archivePath = createDeployArchive(sourceDir);
|
|
3112
|
-
const remoteArchive = `/tmp/miosa-upload-${Date.now()}.tgz`;
|
|
3113
|
-
try {
|
|
3114
|
-
await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
|
|
3115
|
-
const clean = deleteTarget != null
|
|
3116
|
-
? `rm -rf ${shellQuote(deleteTarget)} && mkdir -p ${shellQuote(deleteTarget)}`
|
|
3117
|
-
: `mkdir -p ${shellQuote(remoteDir)}`;
|
|
3118
|
-
await execSandbox(c, sandboxId, `${clean} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteDir)} && rm -f ${shellQuote(remoteArchive)}`, "/");
|
|
3119
|
-
}
|
|
3120
|
-
finally {
|
|
3121
|
-
fs.rmSync(archivePath, { force: true });
|
|
3122
|
-
}
|
|
3123
|
-
return {
|
|
3124
|
-
sandbox_id: sandboxId,
|
|
3125
|
-
local_dir: sourceDir,
|
|
3126
|
-
remote_dir: remoteDir,
|
|
3127
|
-
files_label: path.basename(sourceDir) || sourceDir,
|
|
3128
|
-
};
|
|
3129
|
-
}
|
|
3130
|
-
// --delete runs `rm -rf` inside the sandbox. Refuse protected roots, then
|
|
3131
|
-
// require an explicit confirmation (interactive prompt, or --force/--yes)
|
|
3132
|
-
// before anything is wiped. Returns the normalized remote dir to delete.
|
|
3133
|
-
async function confirmRemoteDeleteDir(sandboxId, remoteDir, force) {
|
|
3134
|
-
const target = assertDeletableRemoteDir(remoteDir);
|
|
3135
|
-
if (force)
|
|
3136
|
-
return target;
|
|
3137
|
-
if (!process.stdin.isTTY) {
|
|
3138
|
-
throw new UserError(`--delete will permanently wipe ${target} on sandbox ${sandboxId}.`, "Non-interactive session: re-run with --force (or --yes) to confirm the wipe.");
|
|
3139
|
-
}
|
|
3140
|
-
console.log(chalk.yellow(`--delete will permanently wipe sandbox ${sandboxId}:${target} before uploading.`));
|
|
3141
|
-
const { default: inquirer } = await import("inquirer");
|
|
3142
|
-
const { confirmed } = await inquirer.prompt([
|
|
3143
|
-
{
|
|
3144
|
-
type: "confirm",
|
|
3145
|
-
name: "confirmed",
|
|
3146
|
-
message: `Wipe ${sandboxId}:${target}?`,
|
|
3147
|
-
default: false,
|
|
3148
|
-
},
|
|
3149
|
-
]);
|
|
3150
|
-
if (!confirmed) {
|
|
3151
|
-
throw new UserError("Cancelled - nothing was deleted or uploaded.");
|
|
3152
|
-
}
|
|
3153
|
-
return target;
|
|
3154
|
-
}
|
|
3155
|
-
async function execSandbox(c, sandboxId, command, cwd, timeout) {
|
|
3156
|
-
const body = { command: commandInCwd(command, cwd) };
|
|
3157
|
-
if (cwd) {
|
|
3158
|
-
body["cwd"] = cwd;
|
|
3159
|
-
body["dir"] = cwd;
|
|
3160
|
-
}
|
|
3161
|
-
if (timeout != null)
|
|
3162
|
-
body["timeout"] = timeout;
|
|
3163
|
-
const result = unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
|
|
3164
|
-
const exitCode = Number(result["exit_code"] ?? 0);
|
|
3165
|
-
if (exitCode !== 0) {
|
|
3166
|
-
throw new UserError(`Sandbox command failed with exit code ${exitCode}: ${command}`, String(result["stderr"] ?? result["stdout"] ?? ""));
|
|
3167
|
-
}
|
|
3168
|
-
return result;
|
|
3169
|
-
}
|
|
3170
|
-
// Like execSandbox but does NOT throw on a non-zero exit code.
|
|
3171
|
-
async function execSandboxRaw(c, sandboxId, command, cwd, timeout) {
|
|
3172
|
-
const body = { command: commandInCwd(command, cwd) };
|
|
3173
|
-
if (cwd) {
|
|
3174
|
-
body["cwd"] = cwd;
|
|
3175
|
-
body["dir"] = cwd;
|
|
3176
|
-
}
|
|
3177
|
-
if (timeout != null)
|
|
3178
|
-
body["timeout"] = timeout;
|
|
3179
|
-
return unwrap(await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), body));
|
|
3180
|
-
}
|
|
3181
|
-
function sandboxPortsFromResponse(raw) {
|
|
3182
|
-
const root = unwrap(raw);
|
|
3183
|
-
if (Array.isArray(root))
|
|
3184
|
-
return root;
|
|
3185
|
-
if (root && typeof root === "object") {
|
|
3186
|
-
const row = root;
|
|
3187
|
-
if (Array.isArray(row["ports"]))
|
|
3188
|
-
return row["ports"];
|
|
3189
|
-
if (Array.isArray(row["data"]))
|
|
3190
|
-
return row["data"];
|
|
3191
|
-
}
|
|
3192
|
-
return [];
|
|
3193
|
-
}
|
|
3194
|
-
function renderResourceMetrics(title, raw) {
|
|
3195
|
-
const root = unwrap(raw);
|
|
3196
|
-
if (!root || typeof root !== "object") {
|
|
3197
|
-
printValue(root, {});
|
|
3198
|
-
return;
|
|
3199
|
-
}
|
|
3200
|
-
const row = root;
|
|
3201
|
-
const current = row["current"] && typeof row["current"] === "object"
|
|
3202
|
-
? row["current"]
|
|
3203
|
-
: {};
|
|
3204
|
-
printBanner({ subtitle: title });
|
|
3205
|
-
console.log(kvPanel([
|
|
3206
|
-
{
|
|
3207
|
-
label: "resource_id",
|
|
3208
|
-
value: String(row["resource_id"] ?? row["sandbox_id"] ?? "-"),
|
|
3209
|
-
},
|
|
3210
|
-
{ label: "window", value: String(row["window"] ?? "1h") },
|
|
3211
|
-
{ label: "state", value: formatState(current["state"]) },
|
|
3212
|
-
{ label: "ready", value: formatBool(current["ready"]) },
|
|
3213
|
-
{ label: "cpu", value: formatMaybe(current["cpu_count"]) },
|
|
3214
|
-
{ label: "memory", value: formatMb(current["memory_mb"]) },
|
|
3215
|
-
{ label: "disk", value: formatMb(current["disk_size_mb"]) },
|
|
3216
|
-
{ label: "uptime", value: formatSeconds(current["uptime_sec"]) },
|
|
3217
|
-
{
|
|
3218
|
-
label: "timeout_remaining",
|
|
3219
|
-
value: formatSecondsOrAlwaysOn(current["timeout_remaining_sec"]),
|
|
3220
|
-
},
|
|
3221
|
-
{ label: "node", value: formatMaybe(current["node_id"]) },
|
|
3222
|
-
{ label: "ip", value: formatMaybe(current["ip_address"]) },
|
|
3223
|
-
{ label: "boot", value: formatMs(current["boot_ms"]) },
|
|
3224
|
-
{ label: "envd_ready", value: formatMs(current["envd_ready_ms"]) },
|
|
3225
|
-
]));
|
|
3226
|
-
}
|
|
3227
|
-
function formatState(value) {
|
|
3228
|
-
const state = String(value ?? "unknown");
|
|
3229
|
-
if (["running", "active", "healthy", "ready"].includes(state)) {
|
|
3230
|
-
return chalk.green(state);
|
|
3231
|
-
}
|
|
3232
|
-
if (["provisioning", "starting", "building", "pending"].includes(state)) {
|
|
3233
|
-
return chalk.yellow(state);
|
|
3234
|
-
}
|
|
3235
|
-
if (["failed", "error", "unhealthy"].includes(state)) {
|
|
3236
|
-
return chalk.red(state);
|
|
3237
|
-
}
|
|
3238
|
-
return state;
|
|
3239
|
-
}
|
|
3240
|
-
function formatBool(value) {
|
|
3241
|
-
if (value === true)
|
|
3242
|
-
return chalk.green("true");
|
|
3243
|
-
if (value === false)
|
|
3244
|
-
return chalk.red("false");
|
|
3245
|
-
return chalk.dim("-");
|
|
3246
|
-
}
|
|
3247
|
-
function formatMaybe(value) {
|
|
3248
|
-
if (value === null || value === undefined || value === "")
|
|
3249
|
-
return chalk.dim("-");
|
|
3250
|
-
return String(value);
|
|
3251
|
-
}
|
|
3252
|
-
function formatMb(value) {
|
|
3253
|
-
if (typeof value !== "number")
|
|
3254
|
-
return formatMaybe(value);
|
|
3255
|
-
return formatBytes(value * 1024 * 1024);
|
|
3256
|
-
}
|
|
3257
|
-
function formatMs(value) {
|
|
3258
|
-
if (typeof value !== "number")
|
|
3259
|
-
return formatMaybe(value);
|
|
3260
|
-
return `${value}ms`;
|
|
3261
|
-
}
|
|
3262
|
-
function formatSeconds(value) {
|
|
3263
|
-
if (typeof value !== "number")
|
|
3264
|
-
return formatMaybe(value);
|
|
3265
|
-
return formatDuration(value * 1000);
|
|
3266
|
-
}
|
|
3267
|
-
function formatSecondsOrAlwaysOn(value) {
|
|
3268
|
-
if (value === null || value === undefined)
|
|
3269
|
-
return chalk.dim("always-on/none");
|
|
3270
|
-
return formatSeconds(value);
|
|
3271
|
-
}
|
|
3272
|
-
// Stream exec output: run in background to a log file, poll-read new bytes
|
|
3273
|
-
// until the process exits, then print the final exit code.
|
|
3274
|
-
async function runFollowExec(sandboxId, cmd, cwd, env, timeoutSec, opts) {
|
|
3275
|
-
if (!cmd) {
|
|
3276
|
-
throw new UserError("No command given to follow.");
|
|
3277
|
-
}
|
|
3278
|
-
const c = client();
|
|
3279
|
-
const logPath = `/tmp/miosa-follow-${Date.now()}.log`;
|
|
3280
|
-
const envPrefix = Object.entries(env)
|
|
3281
|
-
.map(([k, v]) => `${k}=${shellQuote(v)} `)
|
|
3282
|
-
.join("");
|
|
3283
|
-
// Run command, capture exit code into a sentinel file, in the background.
|
|
3284
|
-
const exitPath = `${logPath}.exit`;
|
|
3285
|
-
const inner = `${envPrefix}${cmd}`;
|
|
3286
|
-
const launch = `nohup sh -lc ${shellQuote(`( ${inner} ) > ${shellQuote(logPath)} 2>&1; echo $? > ${shellQuote(exitPath)}`)} >/dev/null 2>&1 & echo $!`;
|
|
3287
|
-
const started = await execSandboxRaw(c, sandboxId, launch, cwd, timeoutSec);
|
|
3288
|
-
const pid = String(started["stdout"] ?? "").trim();
|
|
3289
|
-
if (!pid) {
|
|
3290
|
-
throw new UserError("Could not start background command for --follow.");
|
|
3291
|
-
}
|
|
3292
|
-
const deadline = timeoutSec ? Date.now() + timeoutSec * 1000 : Infinity;
|
|
3293
|
-
let offset = 0;
|
|
3294
|
-
for (;;) {
|
|
3295
|
-
const read = await execSandboxRaw(c, sandboxId, `tail -c +${offset + 1} ${shellQuote(logPath)} 2>/dev/null`);
|
|
3296
|
-
const chunk = String(read["stdout"] ?? "");
|
|
3297
|
-
if (chunk.length > 0) {
|
|
3298
|
-
process.stdout.write(chunk);
|
|
3299
|
-
offset += Buffer.byteLength(chunk);
|
|
3300
|
-
}
|
|
3301
|
-
const alive = await execSandboxRaw(c, sandboxId, `kill -0 ${shellQuote(pid)} 2>/dev/null && echo alive || echo done`);
|
|
3302
|
-
if (String(alive["stdout"] ?? "").trim() === "done")
|
|
3303
|
-
break;
|
|
3304
|
-
if (Date.now() > deadline) {
|
|
3305
|
-
console.error(chalk.yellow(`\nTimed out after ${timeoutSec}s — command may still be running (pid ${pid}).`));
|
|
3306
|
-
return;
|
|
3307
|
-
}
|
|
3308
|
-
await sleep(1000);
|
|
3309
|
-
}
|
|
3310
|
-
// Drain any trailing output.
|
|
3311
|
-
const tail = await execSandboxRaw(c, sandboxId, `tail -c +${offset + 1} ${shellQuote(logPath)} 2>/dev/null`);
|
|
3312
|
-
const tailChunk = String(tail["stdout"] ?? "");
|
|
3313
|
-
if (tailChunk.length > 0)
|
|
3314
|
-
process.stdout.write(tailChunk);
|
|
3315
|
-
const exitRead = await execSandboxRaw(c, sandboxId, `cat ${shellQuote(exitPath)} 2>/dev/null`);
|
|
3316
|
-
const exitCode = Number(String(exitRead["stdout"] ?? "0").trim() || "0");
|
|
3317
|
-
if (!isJsonMode(opts) && exitCode !== 0) {
|
|
3318
|
-
console.error(chalk.red(`\nexit code: ${exitCode}`));
|
|
3319
|
-
}
|
|
3320
|
-
// Streamed commands must fail the CLI process on a nonzero remote exit too,
|
|
3321
|
-
// so `--follow` in CI behaves the same as a plain exec.
|
|
3322
|
-
applyRemoteExitCode({ exit_code: exitCode });
|
|
3323
|
-
}
|
|
3324
|
-
async function waitForInternalHttp(c, sandboxId, port, probePath, timeoutSec) {
|
|
3325
|
-
const deadline = Date.now() + timeoutSec * 1000;
|
|
3326
|
-
let last = { ok: false, status: null };
|
|
3327
|
-
while (Date.now() < deadline) {
|
|
3328
|
-
last = await probeInternalHttp(c, sandboxId, port, probePath);
|
|
3329
|
-
if (last.ok)
|
|
3330
|
-
return last;
|
|
3331
|
-
await sleep(1500);
|
|
3332
|
-
}
|
|
3333
|
-
throw new UserError(`App did not answer inside the sandbox on port ${port} within ${timeoutSec}s.`, last.error);
|
|
3334
|
-
}
|
|
3335
|
-
async function probeInternalHttp(c, sandboxId, port, probePath) {
|
|
3336
|
-
try {
|
|
3337
|
-
const result = await execSandbox(c, sandboxId, `python3 - <<'PY'\nimport urllib.request, sys\nurl = 'http://127.0.0.1:${port}${probePath.startsWith("/") ? probePath : `/${probePath}`}'\ntry:\n r = urllib.request.urlopen(url, timeout=3)\n print(r.status)\n sys.exit(0 if 200 <= r.status < 400 else 1)\nexcept Exception as e:\n print(e)\n sys.exit(1)\nPY`, "/", 10);
|
|
3338
|
-
const status = Number(String(result["stdout"] ?? "")
|
|
3339
|
-
.trim()
|
|
3340
|
-
.split(/\s+/)[0]);
|
|
3341
|
-
return {
|
|
3342
|
-
ok: Number.isFinite(status) && status >= 200 && status < 400,
|
|
3343
|
-
status,
|
|
3344
|
-
};
|
|
3345
|
-
}
|
|
3346
|
-
catch (err) {
|
|
3347
|
-
return {
|
|
3348
|
-
ok: false,
|
|
3349
|
-
status: null,
|
|
3350
|
-
error: err instanceof Error ? err.message : String(err),
|
|
3351
|
-
};
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
async function waitForPublicPreview(previewUrl, probePath, timeoutSec) {
|
|
3355
|
-
const deadline = Date.now() + timeoutSec * 1000;
|
|
3356
|
-
let last = { ok: false, status: null };
|
|
3357
|
-
while (Date.now() < deadline) {
|
|
3358
|
-
last = await probePublicPreview(previewUrl, probePath);
|
|
3359
|
-
if (last.ok)
|
|
3360
|
-
return last;
|
|
3361
|
-
await sleep(2000);
|
|
3362
|
-
}
|
|
3363
|
-
throw new UserError(`Preview URL was created but did not become publicly ready within ${timeoutSec}s.`, last.error ??
|
|
3364
|
-
(last.status ? `Last HTTP status: ${last.status}` : undefined));
|
|
3365
|
-
}
|
|
3366
|
-
async function probePublicPreview(previewUrl, probePath) {
|
|
3367
|
-
try {
|
|
3368
|
-
const url = new URL(previewUrl);
|
|
3369
|
-
url.pathname = joinUrlPath(url.pathname, probePath);
|
|
3370
|
-
const t0 = Date.now();
|
|
3371
|
-
const res = await fetch(url, { method: "GET", redirect: "manual" });
|
|
3372
|
-
return {
|
|
3373
|
-
ok: (res.status >= 200 && res.status < 400) ||
|
|
3374
|
-
res.status === 401 ||
|
|
3375
|
-
res.status === 403,
|
|
3376
|
-
status: res.status,
|
|
3377
|
-
latency_ms: Date.now() - t0,
|
|
3378
|
-
};
|
|
3379
|
-
}
|
|
3380
|
-
catch (err) {
|
|
3381
|
-
return {
|
|
3382
|
-
ok: false,
|
|
3383
|
-
status: null,
|
|
3384
|
-
error: err instanceof Error ? err.message : String(err),
|
|
3385
|
-
};
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
async function waitForLocalHttp(localPort, probePath, timeoutSec) {
|
|
3389
|
-
const deadline = Date.now() + timeoutSec * 1000;
|
|
3390
|
-
let last = { ok: false, status: null };
|
|
3391
|
-
const url = `http://127.0.0.1:${localPort}${probePath.startsWith("/") ? probePath : `/${probePath}`}`;
|
|
3392
|
-
while (Date.now() < deadline) {
|
|
3393
|
-
last = await probePublicPreview(url, "/");
|
|
3394
|
-
if (last.ok)
|
|
3395
|
-
return last;
|
|
3396
|
-
await sleep(1000);
|
|
3397
|
-
}
|
|
3398
|
-
throw new UserError(`Local forwarded URL did not answer within ${timeoutSec}s.`, last.error ??
|
|
3399
|
-
(last.status ? `Last HTTP status: ${last.status}` : undefined));
|
|
3400
|
-
}
|
|
3401
|
-
function defaultInstallCommand(sourceDir) {
|
|
3402
|
-
if (fs.existsSync(path.join(sourceDir, "package.json")))
|
|
3403
|
-
return "npm install";
|
|
3404
|
-
if (fs.existsSync(path.join(sourceDir, "requirements.txt")))
|
|
3405
|
-
return "pip install -r requirements.txt";
|
|
3406
|
-
return null;
|
|
3407
|
-
}
|
|
3408
|
-
function defaultStartCommand(framework, port) {
|
|
3409
|
-
if (framework === "nextjs")
|
|
3410
|
-
return `npm run dev -- -H 0.0.0.0 -p ${port}`;
|
|
3411
|
-
if (framework === "vite-react")
|
|
3412
|
-
return `npm run dev -- --host 0.0.0.0 --port ${port}`;
|
|
3413
|
-
if (framework === "static")
|
|
3414
|
-
return `python3 -m http.server ${port} --bind 0.0.0.0`;
|
|
3415
|
-
return `npm run dev -- --host 0.0.0.0 --port ${port}`;
|
|
3416
|
-
}
|
|
3417
|
-
function normalizeRemoteWorkdir(value) {
|
|
3418
|
-
if (!value || value === ".")
|
|
3419
|
-
return "/workspace";
|
|
3420
|
-
if (!value.startsWith("/"))
|
|
3421
|
-
return `/workspace/${value.replace(/^\.\//, "")}`;
|
|
3422
|
-
return value;
|
|
3423
|
-
}
|
|
3424
|
-
function extractUrl(value) {
|
|
3425
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3426
|
-
return null;
|
|
3427
|
-
const row = value;
|
|
3428
|
-
for (const key of ["url", "preview_url", "public_url"]) {
|
|
3429
|
-
if (typeof row[key] === "string" && row[key])
|
|
3430
|
-
return row[key];
|
|
3431
|
-
}
|
|
3432
|
-
return null;
|
|
3433
|
-
}
|
|
3434
|
-
function asRecord(value) {
|
|
3435
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3436
|
-
return null;
|
|
3437
|
-
return value;
|
|
3438
|
-
}
|
|
3439
|
-
function stringField(row, key) {
|
|
3440
|
-
const value = row?.[key];
|
|
3441
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
3442
|
-
}
|
|
3443
|
-
function isSandboxTarget(value) {
|
|
3444
|
-
const idx = value.indexOf(":");
|
|
3445
|
-
if (idx <= 0)
|
|
3446
|
-
return false;
|
|
3447
|
-
return value.slice(idx + 1).startsWith("/");
|
|
3448
|
-
}
|
|
3449
|
-
function parseSandboxTarget(target) {
|
|
3450
|
-
const idx = target.indexOf(":");
|
|
3451
|
-
if (idx <= 0 || idx === target.length - 1) {
|
|
3452
|
-
throw new UserError(`Invalid sandbox target: ${target}`, "Use the form <sandbox-id>:/absolute/path");
|
|
3453
|
-
}
|
|
3454
|
-
const sandboxId = target.slice(0, idx);
|
|
3455
|
-
const remotePath = target.slice(idx + 1);
|
|
3456
|
-
if (!remotePath.startsWith("/")) {
|
|
3457
|
-
throw new UserError(`Invalid remote path: ${remotePath}`, "Sandbox copy targets must use an absolute path, e.g. sbx_123:/workspace");
|
|
3458
|
-
}
|
|
3459
|
-
return { sandboxId, remotePath };
|
|
3460
|
-
}
|
|
3461
|
-
async function downloadSandboxPath(sandboxId, remotePath, localTarget) {
|
|
3462
|
-
const c = client();
|
|
3463
|
-
const kind = await remotePathKind(c, sandboxId, remotePath);
|
|
3464
|
-
if (kind === "directory") {
|
|
3465
|
-
const remoteArchive = `/tmp/miosa-copy-${Date.now()}.tgz`;
|
|
3466
|
-
await execSandbox(c, sandboxId, `tar -czf ${shellQuote(remoteArchive)} -C ${shellQuote(remotePath)} .`, "/");
|
|
3467
|
-
const archiveBytes = await readSandboxFile(c, sandboxId, remoteArchive);
|
|
3468
|
-
const localDir = path.resolve(localTarget);
|
|
3469
|
-
fs.mkdirSync(localDir, { recursive: true });
|
|
3470
|
-
const localArchive = path.join(os.tmpdir(), `miosa-copy-${process.pid}-${Date.now()}.tgz`);
|
|
3471
|
-
fs.writeFileSync(localArchive, archiveBytes);
|
|
3472
|
-
try {
|
|
3473
|
-
const result = spawnSync("tar", ["-xzf", localArchive, "-C", localDir], {
|
|
3474
|
-
stdio: "pipe",
|
|
3475
|
-
});
|
|
3476
|
-
if (result.status !== 0) {
|
|
3477
|
-
throw new UserError(`Could not extract ${remotePath}: ${result.stderr.toString().trim() || "tar failed"}`);
|
|
3478
|
-
}
|
|
3479
|
-
}
|
|
3480
|
-
finally {
|
|
3481
|
-
fs.rmSync(localArchive, { force: true });
|
|
3482
|
-
await execSandbox(c, sandboxId, `rm -f ${shellQuote(remoteArchive)}`, "/").catch(() => ({}));
|
|
3483
|
-
}
|
|
3484
|
-
return {
|
|
3485
|
-
sandbox_id: sandboxId,
|
|
3486
|
-
remote_path: remotePath,
|
|
3487
|
-
local_path: localDir,
|
|
3488
|
-
type: "directory",
|
|
3489
|
-
};
|
|
3490
|
-
}
|
|
3491
|
-
const bytes = await readSandboxFile(c, sandboxId, remotePath);
|
|
3492
|
-
const localPath = fs.existsSync(localTarget) && fs.statSync(localTarget).isDirectory()
|
|
3493
|
-
? path.join(localTarget, path.basename(remotePath))
|
|
3494
|
-
: localTarget;
|
|
3495
|
-
fs.mkdirSync(path.dirname(path.resolve(localPath)), { recursive: true });
|
|
3496
|
-
fs.writeFileSync(localPath, bytes);
|
|
3497
|
-
return {
|
|
3498
|
-
sandbox_id: sandboxId,
|
|
3499
|
-
remote_path: remotePath,
|
|
3500
|
-
local_path: path.resolve(localPath),
|
|
3501
|
-
type: "file",
|
|
3502
|
-
};
|
|
3503
|
-
}
|
|
3504
|
-
async function remotePathKind(c, sandboxId, remotePath) {
|
|
3505
|
-
const result = await execSandbox(c, sandboxId, `if [ -d ${shellQuote(remotePath)} ]; then echo directory; elif [ -f ${shellQuote(remotePath)} ]; then echo file; else exit 2; fi`, "/");
|
|
3506
|
-
const kind = String(result["stdout"] ?? "").trim();
|
|
3507
|
-
if (kind === "directory")
|
|
3508
|
-
return "directory";
|
|
3509
|
-
return "file";
|
|
3510
|
-
}
|
|
3511
|
-
async function readSandboxFile(c, sandboxId, remotePath) {
|
|
3512
|
-
const encoded = enc(remotePath.replace(/^\//, ""));
|
|
3513
|
-
const result = await c.apiGet(apiPath(`/sandboxes/${enc(sandboxId)}/files/${encoded}`));
|
|
3514
|
-
const data = result !== null && typeof result === "object" && !Array.isArray(result)
|
|
3515
|
-
? (result["data"] ?? result)
|
|
3516
|
-
: result;
|
|
3517
|
-
if (data !== null &&
|
|
3518
|
-
typeof data === "object" &&
|
|
3519
|
-
!Array.isArray(data) &&
|
|
3520
|
-
typeof data["content"] === "string") {
|
|
3521
|
-
return Buffer.from(data["content"], "base64");
|
|
3522
|
-
}
|
|
3523
|
-
throw new UserError(`Could not read sandbox file: ${remotePath}`);
|
|
3524
|
-
}
|
|
3525
|
-
function commandInCwd(command, cwd) {
|
|
3526
|
-
if (!cwd)
|
|
3527
|
-
return command;
|
|
3528
|
-
return `cd ${shellQuote(cwd)} && ${command}`;
|
|
3529
|
-
}
|
|
3530
|
-
function supportedRunAgentRunners() {
|
|
3531
|
-
return [
|
|
3532
|
-
"claude",
|
|
3533
|
-
"claude-code",
|
|
3534
|
-
"codex",
|
|
3535
|
-
"pi",
|
|
3536
|
-
"hermes",
|
|
3537
|
-
"osa",
|
|
3538
|
-
"custom",
|
|
3539
|
-
];
|
|
3540
|
-
}
|
|
3541
|
-
function runtimeCommandForRunner(runner, runtimeCommand) {
|
|
3542
|
-
const normalized = runner.trim().toLowerCase();
|
|
3543
|
-
if (normalized === "custom") {
|
|
3544
|
-
if (!runtimeCommand?.trim()) {
|
|
3545
|
-
throw new Error("--runner custom requires --runtime-command, e.g. --runtime-command 'hermes-agent run'");
|
|
3546
|
-
}
|
|
3547
|
-
return runtimeCommand.trim();
|
|
3548
|
-
}
|
|
3549
|
-
const builtIns = {
|
|
3550
|
-
claude: "claude",
|
|
3551
|
-
"claude-code": "claude",
|
|
3552
|
-
codex: "codex",
|
|
3553
|
-
pi: "pi",
|
|
3554
|
-
hermes: "hermes",
|
|
3555
|
-
osa: "osa",
|
|
3556
|
-
};
|
|
3557
|
-
return builtIns[normalized] ?? null;
|
|
3558
|
-
}
|
|
3559
|
-
function isSupportedRunAgentRunner(runner) {
|
|
3560
|
-
return supportedRunAgentRunners().includes(runner.trim().toLowerCase());
|
|
3561
|
-
}
|
|
3562
|
-
function backgroundCommand(command) {
|
|
3563
|
-
if (!command.trim())
|
|
3564
|
-
return command;
|
|
3565
|
-
const logPath = `/tmp/miosa-bg-${Date.now()}.log`;
|
|
3566
|
-
return `nohup sh -lc ${shellQuote(command)} > ${shellQuote(logPath)} 2>&1 & echo $!`;
|
|
3567
|
-
}
|
|
3568
|
-
function resolveSandboxCommand(words, opts) {
|
|
3569
|
-
const positional = words.length === 1 ? (words[0] ?? "") : joinCommandWords(words);
|
|
3570
|
-
const cmd = opts.cmd ?? opts.command ?? positional;
|
|
3571
|
-
if (!opts.shellCmd)
|
|
3572
|
-
return cmd;
|
|
3573
|
-
if (!cmd.trim())
|
|
3574
|
-
return opts.shellCmd;
|
|
3575
|
-
return `${opts.shellCmd} ${shellQuote(cmd)}`;
|
|
3576
|
-
}
|
|
3577
|
-
function collectOption(value, previous) {
|
|
3578
|
-
return [...previous, value];
|
|
3579
|
-
}
|
|
3580
|
-
function parseEnvPairs(pairs) {
|
|
3581
|
-
const env = {};
|
|
3582
|
-
for (const pair of pairs) {
|
|
3583
|
-
const idx = pair.indexOf("=");
|
|
3584
|
-
if (idx <= 0) {
|
|
3585
|
-
throw new UserError(`Invalid --env value: ${pair}`, "Use KEY=VALUE, for example --env DATABASE_URL=postgresql://...");
|
|
3586
|
-
}
|
|
3587
|
-
env[pair.slice(0, idx)] = pair.slice(idx + 1);
|
|
3588
|
-
}
|
|
3589
|
-
return env;
|
|
3590
|
-
}
|
|
3591
|
-
function parseDurationSec(value) {
|
|
3592
|
-
const match = String(value)
|
|
3593
|
-
.trim()
|
|
3594
|
-
.match(/^(\d+)(ms|s|m|h|d)?$/i);
|
|
3595
|
-
if (!match)
|
|
3596
|
-
throw new UserError(`Invalid duration: ${value}`);
|
|
3597
|
-
const amount = Number(match[1]);
|
|
3598
|
-
const unit = (match[2] ?? "s").toLowerCase();
|
|
3599
|
-
if (unit === "ms")
|
|
3600
|
-
return Math.max(1, Math.ceil(amount / 1000));
|
|
3601
|
-
if (unit === "s")
|
|
3602
|
-
return amount;
|
|
3603
|
-
if (unit === "m")
|
|
3604
|
-
return amount * 60;
|
|
3605
|
-
if (unit === "h")
|
|
3606
|
-
return amount * 60 * 60;
|
|
3607
|
-
if (unit === "d")
|
|
3608
|
-
return amount * 24 * 60 * 60;
|
|
3609
|
-
return amount;
|
|
3610
|
-
}
|
|
3611
|
-
function parseIntegerOption(value) {
|
|
3612
|
-
const n = Number.parseInt(String(value), 10);
|
|
3613
|
-
if (!Number.isInteger(n))
|
|
3614
|
-
throw new UserError(`Invalid integer: ${value}`);
|
|
3615
|
-
return n;
|
|
3616
|
-
}
|
|
3617
|
-
function parseSandboxSize(value) {
|
|
3618
|
-
const size = String(value).trim().toLowerCase();
|
|
3619
|
-
if (size in SANDBOX_SIZE_CONTRACTS)
|
|
3620
|
-
return size;
|
|
3621
|
-
throw new UserError(`Invalid sandbox size: ${value}. Expected xs, small, medium, large, or xl.`);
|
|
3622
|
-
}
|
|
3623
|
-
/**
|
|
3624
|
-
* Resolve the resource fields for `sandbox create`.
|
|
3625
|
-
*
|
|
3626
|
-
* MIOSA bills per resource consumed, not per named tier: any cpu/memory/disk
|
|
3627
|
-
* triple is a valid custom shape. A raw --cpu/--memory pair is passed through
|
|
3628
|
-
* to the create request UNCHANGED, whether or not it happens to match a
|
|
3629
|
-
* published named size (xs/small/medium/large/xl) -- it is never rejected,
|
|
3630
|
-
* snapped to the nearest tier, or forced into a size. This mirrors the
|
|
3631
|
-
* `--data` path (and the SDK's cpuCount/memoryMb/diskSizeMb), which already
|
|
3632
|
-
* accept an off-tier pair; the flag path must behave identically.
|
|
3633
|
-
*
|
|
3634
|
-
* - No --cpu/--memory/--disk: forward only the named size (or small); the
|
|
3635
|
-
* server picks the concrete shape. Never pin a pair from the CLI.
|
|
3636
|
-
* - With --cpu and/or --memory: fill the missing member from --size (or
|
|
3637
|
-
* small), then forward the resulting pair as-is. If it happens to match a
|
|
3638
|
-
* published size, that size is also sent as a convenience label. Disk is a
|
|
3639
|
-
* floor the platform grows the rootfs to: `--disk` overrides the size's
|
|
3640
|
-
* floor on top of any pair (`--size small --disk 20gb` -> a 2 vCPU / 4 GB
|
|
3641
|
-
* box with a 20 GB disk).
|
|
3642
|
-
* - The one guard kept is a genuine caller contradiction: an explicit --size
|
|
3643
|
-
* that disagrees with the resolved shape of an explicit --cpu/--memory
|
|
3644
|
-
* pair still fails fast, since the caller named two different shapes.
|
|
3645
|
-
*/
|
|
3646
|
-
function resolveSandboxResources(opts) {
|
|
3647
|
-
// No explicit resource flags → just the named size (or small).
|
|
3648
|
-
const anyExplicit = opts.cpu != null || opts.memory != null || opts.disk != null;
|
|
3649
|
-
if (!anyExplicit)
|
|
3650
|
-
return { size: opts.size ?? "small" };
|
|
3651
|
-
const sizeBase = SANDBOX_SIZE_CONTRACTS[opts.size ?? "small"];
|
|
3652
|
-
const cpu = opts.cpu ?? sizeBase.cpu;
|
|
3653
|
-
const memory = opts.memory ?? sizeBase.memory;
|
|
3654
|
-
const shapeEntry = Object.entries(SANDBOX_SIZE_CONTRACTS).find(([, contract]) => contract.cpu === cpu && contract.memory === memory);
|
|
3655
|
-
const shape = shapeEntry ? shapeEntry[0] : undefined;
|
|
3656
|
-
if (opts.size && opts.size !== shape) {
|
|
3657
|
-
throw new UserError(`--cpu/--memory ${cpu}/${memory} match ${shape ?? "no published size"}, ` +
|
|
3658
|
-
`not requested size ${opts.size}.`);
|
|
3659
|
-
}
|
|
3660
|
-
const disk = opts.disk ?? (shape ? SANDBOX_SIZE_CONTRACTS[shape].disk : sizeBase.disk);
|
|
3661
|
-
return { size: shape, legacy: { cpu, memory, disk } };
|
|
3662
|
-
}
|
|
3663
|
-
function parseSizeMb(value) {
|
|
3664
|
-
const match = String(value)
|
|
3665
|
-
.trim()
|
|
3666
|
-
.match(/^(\d+)(mb|m|gb|g)?$/i);
|
|
3667
|
-
if (!match)
|
|
3668
|
-
throw new UserError(`Invalid size: ${value}`);
|
|
3669
|
-
const amount = Number(match[1]);
|
|
3670
|
-
const unit = (match[2] ?? "mb").toLowerCase();
|
|
3671
|
-
if (unit === "gb" || unit === "g")
|
|
3672
|
-
return amount * 1024;
|
|
3673
|
-
return amount;
|
|
3674
|
-
}
|
|
3675
|
-
function buildNetworkPolicy(opts) {
|
|
3676
|
-
const allowedDomains = opts.allowedDomain ?? [];
|
|
3677
|
-
const allowedCidrs = opts.allowedCidr ?? [];
|
|
3678
|
-
const deniedCidrs = opts.deniedCidr ?? [];
|
|
3679
|
-
if (!opts.networkPolicy &&
|
|
3680
|
-
allowedDomains.length === 0 &&
|
|
3681
|
-
allowedCidrs.length === 0 &&
|
|
3682
|
-
deniedCidrs.length === 0) {
|
|
3683
|
-
return null;
|
|
3684
|
-
}
|
|
3685
|
-
return {
|
|
3686
|
-
mode: opts.networkPolicy ?? "allow-all",
|
|
3687
|
-
allowed_domains: allowedDomains,
|
|
3688
|
-
allowed_cidrs: allowedCidrs,
|
|
3689
|
-
denied_cidrs: deniedCidrs,
|
|
3690
|
-
};
|
|
3691
|
-
}
|
|
3692
|
-
function shellQuote(value) {
|
|
3693
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3694
|
-
}
|
|
3695
|
-
// Join argv words back into a shell command, quoting only words that contain
|
|
3696
|
-
// whitespace or shell-significant characters. Simple tokens stay bare so
|
|
3697
|
-
// `exec <id> ls -la` reads naturally, while `bash -c "cd x && y"` survives.
|
|
3698
|
-
function joinCommandWords(words) {
|
|
3699
|
-
const SAFE = /^[A-Za-z0-9_./:=@%+-]+$/;
|
|
3700
|
-
return words
|
|
3701
|
-
.map((word) => word.length > 0 && SAFE.test(word) ? word : shellQuote(word))
|
|
3702
|
-
.join(" ");
|
|
3703
|
-
}
|
|
3704
|
-
function joinUrlPath(basePath, probePath) {
|
|
3705
|
-
const probe = probePath.startsWith("/") ? probePath : `/${probePath}`;
|
|
3706
|
-
if (!basePath || basePath === "/")
|
|
3707
|
-
return probe;
|
|
3708
|
-
return `${basePath.replace(/\/$/, "")}${probe}`;
|
|
3709
|
-
}
|
|
3710
|
-
function sleep(ms) {
|
|
3711
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3712
|
-
}
|
|
3713
|
-
// ── shared helpers ─────────────────────────────────────────────────────────
|
|
3714
|
-
function pickFreePort() {
|
|
3715
|
-
return new Promise((resolve, reject) => {
|
|
3716
|
-
const srv = createServer();
|
|
3717
|
-
srv.listen(0, "127.0.0.1", () => {
|
|
3718
|
-
const addr = srv.address();
|
|
3719
|
-
srv.close(() => {
|
|
3720
|
-
if (addr && typeof addr === "object")
|
|
3721
|
-
resolve(addr.port);
|
|
3722
|
-
else
|
|
3723
|
-
reject(new Error("Could not pick a free port"));
|
|
3724
|
-
});
|
|
3725
|
-
});
|
|
3726
|
-
});
|
|
3727
|
-
}
|
|
3728
|
-
function waitForSignal() {
|
|
3729
|
-
return new Promise((resolve) => {
|
|
3730
|
-
function done() {
|
|
3731
|
-
process.off("SIGINT", done);
|
|
3732
|
-
process.off("SIGTERM", done);
|
|
3733
|
-
resolve();
|
|
3734
|
-
}
|
|
3735
|
-
process.on("SIGINT", done);
|
|
3736
|
-
process.on("SIGTERM", done);
|
|
3737
|
-
});
|
|
3738
|
-
}
|
|
3739
|
-
function openUrl(url) {
|
|
3740
|
-
const command = process.platform === "darwin"
|
|
3741
|
-
? "open"
|
|
3742
|
-
: process.platform === "win32"
|
|
3743
|
-
? "cmd"
|
|
3744
|
-
: "xdg-open";
|
|
3745
|
-
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
3746
|
-
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
3747
|
-
child.unref();
|
|
3748
|
-
}
|
|
3749
|
-
// Sandbox render helpers
|
|
3750
|
-
/** Coerce an unknown API field to a display string. */
|
|
3751
|
-
function str(v) {
|
|
3752
|
-
if (v === null || v === undefined)
|
|
3753
|
-
return chalk.dim("-");
|
|
3754
|
-
return String(v);
|
|
3755
|
-
}
|
|
3756
|
-
function stringOrNull(v) {
|
|
3757
|
-
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
3758
|
-
}
|
|
3759
|
-
function shellArg(value) {
|
|
3760
|
-
return `'${value.replace(/'/g, "'\"'\"'")}'`;
|
|
3761
|
-
}
|
|
3762
|
-
/** Apply semantic color to a sandbox status string. */
|
|
3763
|
-
function statusColor(s) {
|
|
3764
|
-
const lower = s.toLowerCase().trim();
|
|
3765
|
-
if (lower === "running")
|
|
3766
|
-
return chalk.green(s);
|
|
3767
|
-
if (lower === "paused" || lower === "suspended")
|
|
3768
|
-
return chalk.yellow(s);
|
|
3769
|
-
if (lower === "stopped" ||
|
|
3770
|
-
lower === "error" ||
|
|
3771
|
-
lower === "failed" ||
|
|
3772
|
-
lower === "destroyed")
|
|
3773
|
-
return chalk.red(s);
|
|
3774
|
-
if (lower === "starting" || lower === "provisioning" || lower === "pending")
|
|
3775
|
-
return chalk.cyan(s);
|
|
3776
|
-
return chalk.dim(s);
|
|
3777
|
-
}
|
|
3778
|
-
/** Render the create-success panel. */
|
|
3779
|
-
function renderCreateSuccess(raw, elapsedMs) {
|
|
3780
|
-
const sb = (raw ?? {});
|
|
3781
|
-
const id = str(sb["id"]);
|
|
3782
|
-
printBanner({ subtitle: "Create sandbox" });
|
|
3783
|
-
console.log(kvPanel([
|
|
3784
|
-
{ icon: icon.ok, label: "ID", value: chalk.bold(id) },
|
|
3785
|
-
{ label: "Name", value: str(sb["name"]) },
|
|
3786
|
-
{ label: "Status", value: statusColor(str(sb["status"])) },
|
|
3787
|
-
]));
|
|
3788
|
-
console.log();
|
|
3789
|
-
warnIfExpiringSoon(sb, id);
|
|
3790
|
-
console.log(hintBlock("Next", [
|
|
3791
|
-
`miosa sandbox show ${id}`,
|
|
3792
|
-
`miosa sandbox exec ${id} --command 'python -c print(2+2)'`,
|
|
3793
|
-
]));
|
|
3794
|
-
printElapsed(formatDuration(elapsedMs));
|
|
3795
|
-
}
|
|
3796
|
-
//# sourceMappingURL=sandbox.js.map
|