@newly/cli 0.1.2 → 0.1.91
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 -19
- package/README.runcloud.md +253 -0
- package/dist/api.js +123 -43
- package/dist/commands/appStore.js +162 -0
- package/dist/commands/appStoreEvaluate.js +515 -0
- package/dist/commands/boxAccess.js +349 -0
- package/dist/commands/build.js +261 -13
- package/dist/commands/create.js +106 -30
- package/dist/commands/deploy.js +100 -129
- package/dist/commands/desktop.js +55 -0
- package/dist/commands/environment.js +290 -0
- package/dist/commands/image.js +145 -0
- package/dist/commands/login.js +265 -18
- package/dist/commands/run-cloud.js +318 -108
- package/dist/commands/sandbox.js +787 -0
- package/dist/commands/sandboxOutput.js +70 -0
- package/dist/commands/secrets.js +203 -0
- package/dist/config.js +56 -17
- package/dist/deviceLogin.js +48 -0
- package/dist/index.js +6 -0
- package/dist/prompt.js +196 -0
- package/dist/run-cloud.js +9 -2
- package/dist/runCloudAuth.js +79 -0
- package/dist/sandboxExec.js +91 -0
- package/dist/services-manifest.js +30 -1
- package/dist/shellSession.js +86 -0
- package/dist/terminal.js +158 -0
- package/dist/version.js +1 -0
- package/package.json +13 -7
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { Help, InvalidArgumentError } from 'commander';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { writeFileSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { ApiClient, ApiError, friendlyApiError } from '../api.js';
|
|
6
|
+
import { requireCredentials } from '../config.js';
|
|
7
|
+
import { runSandboxCommand, shellCommand } from '../sandboxExec.js';
|
|
8
|
+
import { runSandboxShell } from '../shellSession.js';
|
|
9
|
+
import { printJson, StatusLine, style } from '../terminal.js';
|
|
10
|
+
import { noteDeprecated, registerAccessCommands } from './boxAccess.js';
|
|
11
|
+
import { renderSandbox, renderSandboxList, } from './sandboxOutput.js';
|
|
12
|
+
const MAX_SANDBOX_DISK_GIB = 2048;
|
|
13
|
+
const DEFAULT_SANDBOX_TIMEOUT_SECONDS = 300;
|
|
14
|
+
const DEFAULT_BOX_CPU_CORES = 2;
|
|
15
|
+
const DEFAULT_BOX_MEMORY_MIB = 24 * 1024;
|
|
16
|
+
const SANDBOX_POLL_INTERVAL_MS = 1_000;
|
|
17
|
+
const SANDBOX_WAIT_TIMEOUT_MS = 20 * 60 * 1_000;
|
|
18
|
+
const READY_STATES = new Set(['running']);
|
|
19
|
+
const FAILED_STATES = new Set(['interrupted', 'stopped', 'destroyed']);
|
|
20
|
+
const METRIC_RANGES = new Set(['1h', '6h', '24h', '7d', '30d', 'all']);
|
|
21
|
+
const BYTE_UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
|
22
|
+
function runCloudApi() {
|
|
23
|
+
const credentials = requireCredentials();
|
|
24
|
+
return new ApiClient(credentials.apiUrl, credentials.token);
|
|
25
|
+
}
|
|
26
|
+
function print(value, opts) {
|
|
27
|
+
if (opts.json) {
|
|
28
|
+
printJson(value);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
for (const item of value)
|
|
33
|
+
console.log(formatRecord(item));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(formatRecord(value));
|
|
37
|
+
}
|
|
38
|
+
function sandboxStatusMessage(state) {
|
|
39
|
+
if (state === 'building_image')
|
|
40
|
+
return 'Building image';
|
|
41
|
+
if (state === 'pending')
|
|
42
|
+
return 'Waiting for capacity';
|
|
43
|
+
if (state === 'starting')
|
|
44
|
+
return 'Starting sandbox';
|
|
45
|
+
if (state === 'archived')
|
|
46
|
+
return 'Restoring sandbox';
|
|
47
|
+
return `Sandbox is ${state.replaceAll('_', ' ')}`;
|
|
48
|
+
}
|
|
49
|
+
export async function pollSandboxReady(api, id, opts = {}) {
|
|
50
|
+
const intervalMs = opts.intervalMs ?? SANDBOX_POLL_INTERVAL_MS;
|
|
51
|
+
const timeoutMs = opts.timeoutMs ?? SANDBOX_WAIT_TIMEOUT_MS;
|
|
52
|
+
const deadline = Date.now() + timeoutMs;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const record = (await api.get(`/run-cloud/sandboxes/${encodeURIComponent(id)}`));
|
|
55
|
+
const state = record.state ?? 'unknown';
|
|
56
|
+
opts.onState?.(state);
|
|
57
|
+
if (READY_STATES.has(state))
|
|
58
|
+
return record;
|
|
59
|
+
if (FAILED_STATES.has(state)) {
|
|
60
|
+
throw new Error(`sandbox ${id} entered ${state.replaceAll('_', ' ')} state`);
|
|
61
|
+
}
|
|
62
|
+
if (Date.now() > deadline) {
|
|
63
|
+
throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for sandbox ${id} to start`);
|
|
64
|
+
}
|
|
65
|
+
await new Promise((done) => setTimeout(done, intervalMs));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function prepareSandboxShell(api, id, status = new StatusLine()) {
|
|
69
|
+
let record = (await api.get(`/run-cloud/sandboxes/${encodeURIComponent(id)}`));
|
|
70
|
+
if (record.state === 'paused' || record.state === 'archived') {
|
|
71
|
+
status.update(`Resuming ${id}`);
|
|
72
|
+
record = (await api.post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/resume`));
|
|
73
|
+
if (record.state !== 'running') {
|
|
74
|
+
record = await pollSandboxReady(api, id, {
|
|
75
|
+
onState: (state) => status.update(sandboxStatusMessage(state)),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
status.succeed(`Resumed ${id}`);
|
|
79
|
+
}
|
|
80
|
+
if (record.state !== 'running') {
|
|
81
|
+
status.clear();
|
|
82
|
+
throw new Error(`sandbox ${id} is ${String(record.state).replaceAll('_', ' ')}; wait until it is running`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function formatRecord(value) {
|
|
86
|
+
if (!value || typeof value !== 'object')
|
|
87
|
+
return String(value);
|
|
88
|
+
return Object.entries(value)
|
|
89
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
90
|
+
.map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : String(v)}`)
|
|
91
|
+
.join('\n');
|
|
92
|
+
}
|
|
93
|
+
async function run(fn) {
|
|
94
|
+
try {
|
|
95
|
+
await fn();
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.error(friendlyApiError(err));
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function withOutput(cmd) {
|
|
103
|
+
return cmd.option('--json', 'output JSON', false);
|
|
104
|
+
}
|
|
105
|
+
function parseCpuCores(value) {
|
|
106
|
+
const n = Number(value);
|
|
107
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
108
|
+
throw new InvalidArgumentError('--cpu must be a positive number of cores, e.g. 0.5');
|
|
109
|
+
}
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
function parseMemoryMb(value) {
|
|
113
|
+
const n = Number(value);
|
|
114
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
115
|
+
throw new InvalidArgumentError('--memory must be a positive integer of MiB, e.g. 1024');
|
|
116
|
+
}
|
|
117
|
+
return n;
|
|
118
|
+
}
|
|
119
|
+
function parseDiskGb(value) {
|
|
120
|
+
const n = Number(value);
|
|
121
|
+
if (!Number.isSafeInteger(n) || n <= 0 || n > MAX_SANDBOX_DISK_GIB) {
|
|
122
|
+
throw new InvalidArgumentError(`--disk must be an integer from 1 to ${MAX_SANDBOX_DISK_GIB} GiB`);
|
|
123
|
+
}
|
|
124
|
+
return n;
|
|
125
|
+
}
|
|
126
|
+
function parseIdlePauseSeconds(value) {
|
|
127
|
+
const n = Number(value);
|
|
128
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
129
|
+
throw new InvalidArgumentError('--idle-pause must be a non-negative integer of seconds');
|
|
130
|
+
}
|
|
131
|
+
return n;
|
|
132
|
+
}
|
|
133
|
+
function parseTimeoutSeconds(value) {
|
|
134
|
+
const timeout = Number(value);
|
|
135
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0 || timeout > 86_400) {
|
|
136
|
+
throw new InvalidArgumentError('timeout must be an integer from 0 to 86400 seconds');
|
|
137
|
+
}
|
|
138
|
+
return timeout;
|
|
139
|
+
}
|
|
140
|
+
function parseExecTimeoutSeconds(value) {
|
|
141
|
+
const timeout = parseTimeoutSeconds(value);
|
|
142
|
+
if (timeout === 0) {
|
|
143
|
+
throw new InvalidArgumentError('command timeout must be between 1 and 86400 seconds');
|
|
144
|
+
}
|
|
145
|
+
return timeout;
|
|
146
|
+
}
|
|
147
|
+
function parsePort(value) {
|
|
148
|
+
const n = Number(value);
|
|
149
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
150
|
+
throw new InvalidArgumentError('--port must be an integer from 1 to 65535');
|
|
151
|
+
}
|
|
152
|
+
return n;
|
|
153
|
+
}
|
|
154
|
+
function parseMetricRange(value) {
|
|
155
|
+
if (!METRIC_RANGES.has(value)) {
|
|
156
|
+
throw new InvalidArgumentError('range must be one of 1h, 6h, 24h, 7d, 30d, all');
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function parseMetricInterval(value) {
|
|
161
|
+
const seconds = Number(value);
|
|
162
|
+
if (!Number.isFinite(seconds) || seconds < 1) {
|
|
163
|
+
throw new InvalidArgumentError('--interval must be at least 1 second');
|
|
164
|
+
}
|
|
165
|
+
return seconds;
|
|
166
|
+
}
|
|
167
|
+
function formatMetricBytes(value) {
|
|
168
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
169
|
+
return '0 B';
|
|
170
|
+
const unit = Math.max(0, Math.min(Math.floor(Math.log(value) / Math.log(1024)), BYTE_UNITS.length - 1));
|
|
171
|
+
const amount = value / 1024 ** unit;
|
|
172
|
+
return `${amount.toFixed(amount >= 10 || unit === 0 ? 0 : 1)} ${BYTE_UNITS[unit]}`;
|
|
173
|
+
}
|
|
174
|
+
function formatMetricCpu(millicores) {
|
|
175
|
+
if (!Number.isFinite(millicores) || millicores <= 0)
|
|
176
|
+
return '0m';
|
|
177
|
+
return millicores >= 1000
|
|
178
|
+
? `${(millicores / 1000).toFixed(2).replace(/\.?0+$/, '')} cores`
|
|
179
|
+
: `${Math.round(millicores)}m`;
|
|
180
|
+
}
|
|
181
|
+
function metricPercent(used, total) {
|
|
182
|
+
return total > 0 ? `${((used / total) * 100).toFixed(1)}%` : '—';
|
|
183
|
+
}
|
|
184
|
+
function metricSparkline(values) {
|
|
185
|
+
if (values.length === 0)
|
|
186
|
+
return '—';
|
|
187
|
+
const blocks = '▁▂▃▄▅▆▇█';
|
|
188
|
+
const max = Math.max(...values, 1);
|
|
189
|
+
const stride = Math.max(1, Math.ceil(values.length / 48));
|
|
190
|
+
return values
|
|
191
|
+
.filter((_, index) => index % stride === 0 || index === values.length - 1)
|
|
192
|
+
.map((value) => blocks[Math.min(blocks.length - 1, Math.floor((Math.max(0, value) / max) * (blocks.length - 1)))])
|
|
193
|
+
.join('');
|
|
194
|
+
}
|
|
195
|
+
export function renderSandboxMetrics(id, metrics) {
|
|
196
|
+
const point = metrics.current;
|
|
197
|
+
if (!point)
|
|
198
|
+
return `Sandbox metrics · ${id}\nNo samples recorded yet.`;
|
|
199
|
+
const history = metrics.history;
|
|
200
|
+
return [
|
|
201
|
+
`Sandbox metrics · ${id} · ${metrics.range}`,
|
|
202
|
+
`State ${point.state} · ${new Date(point.timestamp).toLocaleString()}`,
|
|
203
|
+
`CPU ${formatMetricCpu(point.cpu_millicores)}`,
|
|
204
|
+
`Memory ${formatMetricBytes(point.memory_bytes)}`,
|
|
205
|
+
`Network ↓ ${formatMetricBytes(point.network_rx_bytes_per_second)}/s · ↑ ${formatMetricBytes(point.network_tx_bytes_per_second)}/s`,
|
|
206
|
+
` totals ↓ ${formatMetricBytes(point.network_rx_bytes)} · ↑ ${formatMetricBytes(point.network_tx_bytes)}`,
|
|
207
|
+
`Disk ${formatMetricBytes(point.disk_used_bytes)} / ${formatMetricBytes(point.disk_total_bytes)} (${metricPercent(point.disk_used_bytes, point.disk_total_bytes)})`,
|
|
208
|
+
`History ${history.length} samples`,
|
|
209
|
+
`CPU ${metricSparkline(history.map((sample) => sample.cpu_millicores))}`,
|
|
210
|
+
`Memory ${metricSparkline(history.map((sample) => sample.memory_bytes))}`,
|
|
211
|
+
`Network ↓ ${metricSparkline(history.map((sample) => sample.network_rx_bytes_per_second))}`,
|
|
212
|
+
`Network ↑ ${metricSparkline(history.map((sample) => sample.network_tx_bytes_per_second))}`,
|
|
213
|
+
`Disk ${metricSparkline(history.map((sample) => sample.disk_used_bytes))}`,
|
|
214
|
+
].join('\n');
|
|
215
|
+
}
|
|
216
|
+
function parseCoordinate(value) {
|
|
217
|
+
const n = Number(value);
|
|
218
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
219
|
+
throw new InvalidArgumentError('coordinates must be non-negative integers');
|
|
220
|
+
}
|
|
221
|
+
return n;
|
|
222
|
+
}
|
|
223
|
+
export function openBrowserUrl(url, platform = process.platform) {
|
|
224
|
+
if (platform === 'darwin') {
|
|
225
|
+
execFileSync('open', [url], { stdio: 'ignore' });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (platform === 'win32') {
|
|
229
|
+
execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
execFileSync('xdg-open', [url], { stdio: 'ignore' });
|
|
233
|
+
}
|
|
234
|
+
async function findBoxForSandbox(api, sandboxId) {
|
|
235
|
+
const data = await api.get('/run-cloud/boxes');
|
|
236
|
+
return (data?.items ?? []).find((box) => box.sandboxId === sandboxId);
|
|
237
|
+
}
|
|
238
|
+
function attachHostname(api, sandboxId, name, port) {
|
|
239
|
+
return api.post('/run-cloud/boxes', { sandboxId, name, port });
|
|
240
|
+
}
|
|
241
|
+
function secretSelector(opts) {
|
|
242
|
+
const ordered = [...(opts.secretGroup ?? []), ...(opts.secret ?? [])];
|
|
243
|
+
const env = {};
|
|
244
|
+
for (const pair of opts.env ?? []) {
|
|
245
|
+
const eq = pair.indexOf('=');
|
|
246
|
+
if (eq <= 0)
|
|
247
|
+
throw new Error(`--env expects NAME=VALUE, got "${pair}"`);
|
|
248
|
+
env[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
249
|
+
}
|
|
250
|
+
if (opts.noSecrets) {
|
|
251
|
+
if (ordered.length > 0) {
|
|
252
|
+
throw new Error('--no-secrets cannot be combined with --secret-group or --secret');
|
|
253
|
+
}
|
|
254
|
+
return Object.keys(env).length > 0 ? { secrets: 'none', env } : { secrets: 'none' };
|
|
255
|
+
}
|
|
256
|
+
const out = {};
|
|
257
|
+
if (ordered.length > 0)
|
|
258
|
+
out.secrets = ordered;
|
|
259
|
+
if (Object.keys(env).length > 0)
|
|
260
|
+
out.env = env;
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
function collectRepeatable(value, previous) {
|
|
264
|
+
return [...previous, value];
|
|
265
|
+
}
|
|
266
|
+
export function registerSandbox(program) {
|
|
267
|
+
const sandbox = program.command('sandbox').description('Spawn and control microVM sandboxes');
|
|
268
|
+
const defaultHelp = new Help();
|
|
269
|
+
sandbox.configureHelp({
|
|
270
|
+
subcommandTerm: (command) => {
|
|
271
|
+
const term = defaultHelp.subcommandTerm(command);
|
|
272
|
+
const aliases = command.aliases();
|
|
273
|
+
if (aliases.length < 2)
|
|
274
|
+
return term;
|
|
275
|
+
return term.replace(`${command.name()}|${aliases[0]}`, `${command.name()}|${aliases.join('|')}`);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
withOutput(sandbox
|
|
279
|
+
.command('create')
|
|
280
|
+
.description('Spawn a microVM sandbox')
|
|
281
|
+
.option('--image <ref>', 'base image (default runcloud/agent-base, or runcloud/box with --expose)')
|
|
282
|
+
.option('--region <region>', 'placement region')
|
|
283
|
+
.option('--name <name>', 'human-readable name; with --expose, the hostname prefix <name>-box.run.cloud')
|
|
284
|
+
.option('--org <id>', 'billing and ownership organization')
|
|
285
|
+
.option('--cpu <cores>', 'fractional vCPU allocation, e.g. 0.5', parseCpuCores)
|
|
286
|
+
.option('--memory <mb>', 'memory allocation in MiB', parseMemoryMb)
|
|
287
|
+
.option('--disk <gb>', 'root disk capacity in GiB', parseDiskGb)
|
|
288
|
+
.option('--idle-pause <seconds>', 'pause after inactivity; 0 keeps it running', parseIdlePauseSeconds)
|
|
289
|
+
.option('--timeout <seconds>', 'maximum sandbox lifetime; defaults to 300, or 0 for persistent sandboxes', parseTimeoutSeconds)
|
|
290
|
+
.option('--persistent', 'never pause when idle (same as --idle-pause 0)', false)
|
|
291
|
+
.option('--expose <port>', 'publish a guest port at <name>-box.run.cloud; implies --persistent', parsePort)
|
|
292
|
+
.option('--secret-group <group>', 'attach a secret group; repeatable and ORDER MATTERS (a later group wins on a name collision)', collectRepeatable, [])
|
|
293
|
+
.option('--secret <group/name>', 'attach one secret out of a group; repeatable, same ordering', collectRepeatable, [])
|
|
294
|
+
.option('--env <NAME=VALUE>', 'literal value for this sandbox; applied last, so it wins; repeatable', collectRepeatable, [])
|
|
295
|
+
.option('--no-secrets', 'state explicitly that the sandbox holds no secrets')
|
|
296
|
+
.option('--no-wait', 'return before the sandbox finishes building and starting')).action((opts) => run(async () => {
|
|
297
|
+
const exposed = opts.expose !== undefined;
|
|
298
|
+
if (exposed && !opts.name) {
|
|
299
|
+
throw new Error('--expose needs --name <name>, the hostname prefix for <name>-box.run.cloud');
|
|
300
|
+
}
|
|
301
|
+
if ((exposed || opts.persistent) && opts.idlePause !== undefined && opts.idlePause !== 0) {
|
|
302
|
+
throw new Error('--persistent and --expose keep the sandbox running; drop the non-zero --idle-pause');
|
|
303
|
+
}
|
|
304
|
+
const body = {
|
|
305
|
+
image: opts.image ?? (exposed ? 'runcloud/box' : 'runcloud/agent-base'),
|
|
306
|
+
};
|
|
307
|
+
if (opts.name)
|
|
308
|
+
body.name = opts.name;
|
|
309
|
+
if (opts.region)
|
|
310
|
+
body.region = opts.region;
|
|
311
|
+
if (opts.org)
|
|
312
|
+
body.orgId = opts.org;
|
|
313
|
+
const cpu = opts.cpu ?? (exposed ? DEFAULT_BOX_CPU_CORES : undefined);
|
|
314
|
+
const memory = opts.memory ?? (exposed ? DEFAULT_BOX_MEMORY_MIB : undefined);
|
|
315
|
+
if (cpu !== undefined)
|
|
316
|
+
body.cpu = cpu;
|
|
317
|
+
if (memory !== undefined)
|
|
318
|
+
body.memory = memory;
|
|
319
|
+
if (opts.disk !== undefined)
|
|
320
|
+
body.disk = opts.disk;
|
|
321
|
+
const persistent = exposed || opts.persistent || opts.idlePause === 0;
|
|
322
|
+
body.timeoutSeconds = opts.timeout ?? (persistent ? 0 : DEFAULT_SANDBOX_TIMEOUT_SECONDS);
|
|
323
|
+
if (exposed || opts.persistent)
|
|
324
|
+
body.idlePauseSeconds = 0;
|
|
325
|
+
else if (opts.idlePause !== undefined)
|
|
326
|
+
body.idlePauseSeconds = opts.idlePause;
|
|
327
|
+
Object.assign(body, secretSelector({ ...opts, noSecrets: opts.secrets === false }));
|
|
328
|
+
const api = runCloudApi();
|
|
329
|
+
let created = (await api.post('/run-cloud/sandboxes', body));
|
|
330
|
+
if (typeof created.id !== 'string') {
|
|
331
|
+
throw new Error('sandbox create response did not include an id');
|
|
332
|
+
}
|
|
333
|
+
if (!opts.json && opts.wait && created.state && !READY_STATES.has(created.state)) {
|
|
334
|
+
const status = new StatusLine();
|
|
335
|
+
status.update(sandboxStatusMessage(created.state));
|
|
336
|
+
try {
|
|
337
|
+
created = await pollSandboxReady(api, created.id, {
|
|
338
|
+
onState: (state) => status.update(sandboxStatusMessage(state)),
|
|
339
|
+
});
|
|
340
|
+
status.clear();
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
status.fail('Sandbox failed to start');
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!exposed) {
|
|
348
|
+
if (opts.json)
|
|
349
|
+
printJson(created);
|
|
350
|
+
else
|
|
351
|
+
console.log(renderSandbox(created, 'created'));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const box = await attachHostname(api, created.id, opts.name, opts.expose);
|
|
355
|
+
const exposedRecord = { ...created, box };
|
|
356
|
+
if (opts.json)
|
|
357
|
+
printJson(exposedRecord);
|
|
358
|
+
else
|
|
359
|
+
console.log(renderSandbox(exposedRecord, 'created'));
|
|
360
|
+
}));
|
|
361
|
+
withOutput(sandbox.command('list').description('List sandboxes').option('--state <state>', 'filter by state')).action((opts) => run(async () => {
|
|
362
|
+
const qs = opts.state ? `?state=${encodeURIComponent(opts.state)}` : '';
|
|
363
|
+
const api = runCloudApi();
|
|
364
|
+
const [data, boxes] = await Promise.all([
|
|
365
|
+
api.get(`/run-cloud/sandboxes${qs}`),
|
|
366
|
+
api.get('/run-cloud/boxes').catch(() => ({ items: [] })),
|
|
367
|
+
]);
|
|
368
|
+
const bySandbox = new Map((boxes?.items ?? []).map((box) => [box.sandboxId, box]));
|
|
369
|
+
const items = (data?.items ?? []).map((item) => {
|
|
370
|
+
const box = bySandbox.get(item.id);
|
|
371
|
+
return box ? { ...item, hostname: box.hostname, exposedPort: box.port } : item;
|
|
372
|
+
});
|
|
373
|
+
if (opts.json)
|
|
374
|
+
printJson(items);
|
|
375
|
+
else
|
|
376
|
+
console.log(renderSandboxList(items));
|
|
377
|
+
}));
|
|
378
|
+
withOutput(sandbox
|
|
379
|
+
.command('get')
|
|
380
|
+
.description('Inspect a sandbox, including its public hostname when exposed')
|
|
381
|
+
.argument('<id>', 'sandbox id')).action((id, opts) => run(async () => {
|
|
382
|
+
const api = runCloudApi();
|
|
383
|
+
const [record, box] = await Promise.all([
|
|
384
|
+
api.get(`/run-cloud/sandboxes/${encodeURIComponent(id)}`),
|
|
385
|
+
findBoxForSandbox(api, id),
|
|
386
|
+
]);
|
|
387
|
+
const sandboxRecord = (box ? { ...record, box } : record);
|
|
388
|
+
if (opts.json)
|
|
389
|
+
printJson(sandboxRecord);
|
|
390
|
+
else
|
|
391
|
+
console.log(renderSandbox(sandboxRecord));
|
|
392
|
+
}));
|
|
393
|
+
withOutput(sandbox
|
|
394
|
+
.command('metrics')
|
|
395
|
+
.description('Show current and historical CPU, memory, network, and disk usage')
|
|
396
|
+
.argument('<id>', 'sandbox id')
|
|
397
|
+
.option('--range <range>', 'history range: 1h, 6h, 24h, 7d, 30d, all', parseMetricRange, '24h')
|
|
398
|
+
.option('--watch', 'refresh continuously', false)
|
|
399
|
+
.option('--interval <seconds>', 'watch refresh interval', parseMetricInterval, 5)).action((id, opts) => run(async () => {
|
|
400
|
+
const api = runCloudApi();
|
|
401
|
+
for (;;) {
|
|
402
|
+
const metrics = (await api.get(`/run-cloud/sandboxes/${encodeURIComponent(id)}/metrics?range=${encodeURIComponent(opts.range)}`));
|
|
403
|
+
if (opts.json) {
|
|
404
|
+
console.log(opts.watch ? JSON.stringify(metrics) : JSON.stringify(metrics, null, 2));
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
console.log(renderSandboxMetrics(id, metrics));
|
|
408
|
+
}
|
|
409
|
+
if (!opts.watch)
|
|
410
|
+
return;
|
|
411
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, opts.interval * 1_000));
|
|
412
|
+
}
|
|
413
|
+
}));
|
|
414
|
+
withOutput(sandbox
|
|
415
|
+
.command('secrets')
|
|
416
|
+
.description('Replace the secrets a running sandbox holds (a full replacement, not a merge)')
|
|
417
|
+
.argument('<id>', 'sandbox id')
|
|
418
|
+
.option('--secret-group <group>', 'attach a secret group; repeatable and ORDER MATTERS (a later group wins on a name collision)', collectRepeatable, [])
|
|
419
|
+
.option('--secret <group/name>', 'attach one secret out of a group; repeatable, same ordering', collectRepeatable, [])
|
|
420
|
+
.option('--env <NAME=VALUE>', 'literal value; applied last, so it wins; repeatable', collectRepeatable, [])).action((id, opts) => run(async () => {
|
|
421
|
+
const body = secretSelector(opts);
|
|
422
|
+
await runCloudApi().put(`/run-cloud/sandboxes/${encodeURIComponent(id)}/secrets`, body);
|
|
423
|
+
console.log(`${id}: secrets replaced`);
|
|
424
|
+
}));
|
|
425
|
+
withOutput(sandbox
|
|
426
|
+
.command('expose')
|
|
427
|
+
.description('Publish a sandbox guest port at a stable hostname, or change the published port')
|
|
428
|
+
.argument('<id>', 'sandbox id')
|
|
429
|
+
.option('--port <port>', 'guest port to publish (default 3000)', parsePort)
|
|
430
|
+
.option('--name <name>', 'hostname prefix for a first exposure: <name>-box.run.cloud (defaults to the sandbox name)')
|
|
431
|
+
.option('--reconcile', 'retry or reconcile hostname routing', false)).action((id, opts) => run(async () => {
|
|
432
|
+
const api = runCloudApi();
|
|
433
|
+
const existing = await findBoxForSandbox(api, id);
|
|
434
|
+
if (!existing) {
|
|
435
|
+
if (opts.reconcile)
|
|
436
|
+
throw new Error(`sandbox ${id} has no hostname to reconcile`);
|
|
437
|
+
let name = opts.name;
|
|
438
|
+
if (!name) {
|
|
439
|
+
const record = await api.get(`/run-cloud/sandboxes/${encodeURIComponent(id)}`);
|
|
440
|
+
name = typeof record?.name === 'string' && record.name ? record.name : undefined;
|
|
441
|
+
}
|
|
442
|
+
if (!name)
|
|
443
|
+
throw new Error('this sandbox has no name; pass --name <name> to mint <name>-box.run.cloud');
|
|
444
|
+
print(await attachHostname(api, id, name, opts.port ?? 3000), opts);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (opts.name && opts.name !== existing.name) {
|
|
448
|
+
throw new Error(`hostname is fixed at ${existing.hostname}; run 'sandbox rm ${id}' and expose again to rename`);
|
|
449
|
+
}
|
|
450
|
+
if (opts.reconcile) {
|
|
451
|
+
print(await api.post(`/run-cloud/boxes/${encodeURIComponent(existing.id)}/route`), opts);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (opts.port !== undefined) {
|
|
455
|
+
print(await api.patch(`/run-cloud/boxes/${encodeURIComponent(existing.id)}`, { port: opts.port }), opts);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
print(existing, opts);
|
|
459
|
+
}));
|
|
460
|
+
withOutput(sandbox
|
|
461
|
+
.command('exec')
|
|
462
|
+
.description('Run a command in a sandbox (via /bin/sh -c)')
|
|
463
|
+
.argument('<id>', 'sandbox id')
|
|
464
|
+
.argument('<cmd...>', 'command to run, e.g. npm run build')
|
|
465
|
+
.option('--timeout <seconds>', 'command timeout', parseExecTimeoutSeconds)
|
|
466
|
+
.allowUnknownOption(true)).action((id, cmdParts, opts) => run(async () => {
|
|
467
|
+
const credentials = requireCredentials();
|
|
468
|
+
const r = await runSandboxCommand({
|
|
469
|
+
apiUrl: credentials.apiUrl,
|
|
470
|
+
token: credentials.token,
|
|
471
|
+
sandboxId: id,
|
|
472
|
+
request: {
|
|
473
|
+
cmd: ['/bin/sh', '-c', shellCommand(cmdParts)],
|
|
474
|
+
...(opts.timeout === undefined ? {} : { timeout_seconds: opts.timeout }),
|
|
475
|
+
},
|
|
476
|
+
...(opts.json
|
|
477
|
+
? {}
|
|
478
|
+
: {
|
|
479
|
+
onStdout: (chunk) => process.stdout.write(chunk),
|
|
480
|
+
onStderr: (chunk) => process.stderr.write(chunk),
|
|
481
|
+
}),
|
|
482
|
+
});
|
|
483
|
+
if (opts.json) {
|
|
484
|
+
console.log(JSON.stringify(r, null, 2));
|
|
485
|
+
}
|
|
486
|
+
process.exitCode = r.exit_code ?? 0;
|
|
487
|
+
}));
|
|
488
|
+
sandbox
|
|
489
|
+
.command('shell')
|
|
490
|
+
.description('Open an interactive shell (resumes paused sandboxes)')
|
|
491
|
+
.argument('<id>', 'sandbox id')
|
|
492
|
+
.action((id) => run(async () => {
|
|
493
|
+
const credentials = requireCredentials();
|
|
494
|
+
const api = new ApiClient(credentials.apiUrl, credentials.token);
|
|
495
|
+
await prepareSandboxShell(api, id);
|
|
496
|
+
process.exitCode = await runSandboxShell({
|
|
497
|
+
apiUrl: credentials.apiUrl,
|
|
498
|
+
token: credentials.token,
|
|
499
|
+
sandboxId: id,
|
|
500
|
+
});
|
|
501
|
+
}));
|
|
502
|
+
for (const lifecycle of [
|
|
503
|
+
{
|
|
504
|
+
name: 'pause',
|
|
505
|
+
alias: 'stop',
|
|
506
|
+
description: 'Pause a sandbox so it can be resumed later',
|
|
507
|
+
pastTense: 'Paused',
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: 'resume',
|
|
511
|
+
alias: 'start',
|
|
512
|
+
description: 'Resume a paused sandbox',
|
|
513
|
+
pastTense: 'Resumed',
|
|
514
|
+
},
|
|
515
|
+
]) {
|
|
516
|
+
withOutput(sandbox
|
|
517
|
+
.command(lifecycle.name)
|
|
518
|
+
.alias(lifecycle.alias)
|
|
519
|
+
.description(lifecycle.description)
|
|
520
|
+
.argument('<id>', 'sandbox id')).action((id, opts) => run(async () => {
|
|
521
|
+
const record = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/${lifecycle.name}`);
|
|
522
|
+
if (opts.json)
|
|
523
|
+
printJson(record);
|
|
524
|
+
else
|
|
525
|
+
console.log(`${style.green('✓')} ${lifecycle.pastTense} ${id}`);
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
withOutput(sandbox
|
|
529
|
+
.command('logs')
|
|
530
|
+
.description('Show saved console logs (available after a sandbox is deleted)')
|
|
531
|
+
.argument('<id>', 'sandbox id')
|
|
532
|
+
.option('--lines <n>', 'number of lines to tail', '200')).action((id, opts) => run(async () => {
|
|
533
|
+
const n = Number(opts.lines) || 200;
|
|
534
|
+
const saved = await runCloudApi().getText(`/run-cloud/sandboxes/${encodeURIComponent(id)}/logs`);
|
|
535
|
+
const lines = saved.split('\n');
|
|
536
|
+
const logs = lines.slice(Math.max(0, lines.length - n - (saved.endsWith('\n') ? 1 : 0))).join('\n');
|
|
537
|
+
if (opts.json) {
|
|
538
|
+
printJson({ sandboxId: id, logs });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
process.stdout.write(logs);
|
|
542
|
+
}));
|
|
543
|
+
withOutput(sandbox
|
|
544
|
+
.command('desktop')
|
|
545
|
+
.description('Open a signed browser desktop for a runcloud/desktop sandbox')
|
|
546
|
+
.argument('<id>', 'sandbox id')
|
|
547
|
+
.option('--url', 'print the signed noVNC URL instead of opening it', false)
|
|
548
|
+
.option('--view-only', 'disable keyboard and pointer input', false)).action((id, opts) => run(async () => {
|
|
549
|
+
const desktop = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/desktop`, { viewOnly: opts.viewOnly });
|
|
550
|
+
if (typeof desktop?.url !== 'string' || !desktop.url.startsWith('https://')) {
|
|
551
|
+
throw new Error('sandbox desktop response did not include a secure URL');
|
|
552
|
+
}
|
|
553
|
+
if (opts.url) {
|
|
554
|
+
console.log(desktop.url);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (opts.json) {
|
|
558
|
+
print(desktop, opts);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
openBrowserUrl(desktop.url);
|
|
562
|
+
console.log(`Opened ${opts.viewOnly ? 'view-only ' : ''}desktop for ${id}`);
|
|
563
|
+
}));
|
|
564
|
+
withOutput(sandbox
|
|
565
|
+
.command('screenshot')
|
|
566
|
+
.description('Capture the sandbox desktop as a PNG')
|
|
567
|
+
.argument('<id>', 'sandbox id')
|
|
568
|
+
.option('--output <path>', 'PNG output path', 'screen.png')).action((id, opts) => run(async () => {
|
|
569
|
+
const result = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/computer`, { action: 'screenshot' });
|
|
570
|
+
if (typeof result?.image !== 'string' || !result.image) {
|
|
571
|
+
throw new Error('sandbox screenshot response did not include a PNG image');
|
|
572
|
+
}
|
|
573
|
+
const image = Buffer.from(result.image, 'base64');
|
|
574
|
+
const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
575
|
+
if (image.length < pngSignature.length ||
|
|
576
|
+
!image.subarray(0, pngSignature.length).equals(pngSignature)) {
|
|
577
|
+
throw new Error('sandbox screenshot response was not a valid PNG');
|
|
578
|
+
}
|
|
579
|
+
const output = resolve(opts.output);
|
|
580
|
+
writeFileSync(output, image, { mode: 0o600 });
|
|
581
|
+
print({ sandboxId: id, output, bytes: image.length }, opts);
|
|
582
|
+
}));
|
|
583
|
+
withOutput(sandbox
|
|
584
|
+
.command('click')
|
|
585
|
+
.description('Click a desktop pixel coordinate')
|
|
586
|
+
.argument('<id>', 'sandbox id')
|
|
587
|
+
.argument('<x>', 'horizontal pixel coordinate', parseCoordinate)
|
|
588
|
+
.argument('<y>', 'vertical pixel coordinate', parseCoordinate)).action((id, x, y, opts) => run(async () => {
|
|
589
|
+
const result = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/computer`, { action: 'click', x, y });
|
|
590
|
+
print(result ?? { ok: true }, opts);
|
|
591
|
+
}));
|
|
592
|
+
withOutput(sandbox
|
|
593
|
+
.command('type')
|
|
594
|
+
.description('Type text into the sandbox desktop')
|
|
595
|
+
.argument('<id>', 'sandbox id')
|
|
596
|
+
.argument('<text>', 'text to type')).action((id, text, opts) => run(async () => {
|
|
597
|
+
const result = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/computer`, { action: 'type', text });
|
|
598
|
+
print(result ?? { ok: true }, opts);
|
|
599
|
+
}));
|
|
600
|
+
withOutput(sandbox
|
|
601
|
+
.command('key')
|
|
602
|
+
.description('Send an xdotool key chord to the sandbox desktop')
|
|
603
|
+
.argument('<id>', 'sandbox id')
|
|
604
|
+
.argument('<keys>', 'key or chord, e.g. ctrl+l')).action((id, keys, opts) => run(async () => {
|
|
605
|
+
const result = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/computer`, { action: 'key', keys });
|
|
606
|
+
print(result ?? { ok: true }, opts);
|
|
607
|
+
}));
|
|
608
|
+
withOutput(sandbox
|
|
609
|
+
.command('rm')
|
|
610
|
+
.aliases(['kill', 'terminate'])
|
|
611
|
+
.description('Permanently destroy a sandbox and its public hostname when exposed')
|
|
612
|
+
.argument('<id>', 'sandbox id')).action((id, opts) => run(async () => {
|
|
613
|
+
const api = runCloudApi();
|
|
614
|
+
try {
|
|
615
|
+
await api.delete(`/run-cloud/sandboxes/${encodeURIComponent(id)}`);
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
if (!(err instanceof ApiError) || err.status !== 409)
|
|
619
|
+
throw err;
|
|
620
|
+
const box = await findBoxForSandbox(api, id);
|
|
621
|
+
if (!box)
|
|
622
|
+
throw err;
|
|
623
|
+
await api.delete(`/run-cloud/boxes/${encodeURIComponent(box.id)}`);
|
|
624
|
+
}
|
|
625
|
+
if (!opts.json)
|
|
626
|
+
console.log(`${style.green('✓')} Destroyed ${id}`);
|
|
627
|
+
}));
|
|
628
|
+
const snapshot = sandbox.command('snapshot').description('Capture and manage sandbox snapshots');
|
|
629
|
+
withOutput(snapshot
|
|
630
|
+
.command('create')
|
|
631
|
+
.description('Snapshot a sandbox for later restore')
|
|
632
|
+
.argument('<id>', 'sandbox id to snapshot')
|
|
633
|
+
.option('--label <label>', 'human-readable label')
|
|
634
|
+
.option('--timeout <seconds>', 'maximum time to wait for snapshot completion', parseExecTimeoutSeconds, 3600)).action((id, opts) => run(async () => {
|
|
635
|
+
const body = {};
|
|
636
|
+
if (opts.label)
|
|
637
|
+
body.label = opts.label;
|
|
638
|
+
const api = runCloudApi();
|
|
639
|
+
let result = await api.post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/snapshots`, body);
|
|
640
|
+
const deadline = Date.now() + opts.timeout * 1000;
|
|
641
|
+
while (result.state === 'pending') {
|
|
642
|
+
if (!result.id)
|
|
643
|
+
throw new Error('snapshot job did not return an id');
|
|
644
|
+
if (Date.now() >= deadline)
|
|
645
|
+
throw new Error(`snapshot ${result.id} did not finish before the timeout`);
|
|
646
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
647
|
+
result = await api.get(`/run-cloud/snapshots/${encodeURIComponent(result.id)}`);
|
|
648
|
+
}
|
|
649
|
+
if (result.state === 'failed')
|
|
650
|
+
throw new Error(`snapshot ${result.id ?? ''} failed`);
|
|
651
|
+
print(result, opts);
|
|
652
|
+
}));
|
|
653
|
+
withOutput(snapshot
|
|
654
|
+
.command('list')
|
|
655
|
+
.description('List snapshots')
|
|
656
|
+
.option('--sandbox <id>', 'only snapshots taken from this sandbox')).action((opts) => run(async () => {
|
|
657
|
+
const qs = opts.sandbox ? `?sandbox_id=${encodeURIComponent(opts.sandbox)}` : '';
|
|
658
|
+
const data = await runCloudApi().get(`/run-cloud/snapshots${qs}`);
|
|
659
|
+
print(data?.items ?? [], opts);
|
|
660
|
+
}));
|
|
661
|
+
withOutput(snapshot.command('rm').description('Delete a snapshot').argument('<id>', 'snapshot id')).action((id, opts) => run(async () => {
|
|
662
|
+
await runCloudApi().delete(`/run-cloud/snapshots/${encodeURIComponent(id)}`);
|
|
663
|
+
if (!opts.json)
|
|
664
|
+
console.log(`deleted ${id}`);
|
|
665
|
+
}));
|
|
666
|
+
withOutput(sandbox
|
|
667
|
+
.command('restore')
|
|
668
|
+
.description('Fork a new sandbox from a snapshot (warm start)')
|
|
669
|
+
.argument('<snapshot-id>', 'snapshot id to restore from')
|
|
670
|
+
.option('--name <name>', 'human-readable name for the new sandbox')
|
|
671
|
+
.option('--size <size>', 'size preset for the restored sandbox')
|
|
672
|
+
.option('--cpu <cores>', 'CPU cores, including fractional values', parseCpuCores)
|
|
673
|
+
.option('--memory <mb>', 'memory in MiB', parseMemoryMb)
|
|
674
|
+
.option('--disk <gb>', 'root disk capacity in GiB (may only grow)', parseDiskGb)
|
|
675
|
+
.option('--region <region>', 'placement region')
|
|
676
|
+
.option('--secret-group <group>', 'attach a secret group to the fork; a fork inherits NONE by default (secrets live in memory, so a snapshot cannot carry them)', collectRepeatable, [])
|
|
677
|
+
.option('--secret <group/name>', 'attach one secret out of a group; repeatable, order matters', collectRepeatable, [])
|
|
678
|
+
.option('--env <NAME=VALUE>', 'literal value for the fork; applied last; repeatable', collectRepeatable, [])).action((snapshotId, opts) => run(async () => {
|
|
679
|
+
const body = {};
|
|
680
|
+
if (opts.name)
|
|
681
|
+
body.name = opts.name;
|
|
682
|
+
if (opts.size)
|
|
683
|
+
body.size = opts.size;
|
|
684
|
+
if (opts.cpu !== undefined)
|
|
685
|
+
body.cpu = opts.cpu;
|
|
686
|
+
if (opts.memory !== undefined)
|
|
687
|
+
body.memory = opts.memory;
|
|
688
|
+
if (opts.disk !== undefined)
|
|
689
|
+
body.disk = opts.disk;
|
|
690
|
+
if (opts.region)
|
|
691
|
+
body.region = opts.region;
|
|
692
|
+
Object.assign(body, secretSelector(opts));
|
|
693
|
+
const restored = await runCloudApi().post(`/run-cloud/snapshots/${encodeURIComponent(snapshotId)}/restore`, body);
|
|
694
|
+
if (opts.json)
|
|
695
|
+
printJson(restored);
|
|
696
|
+
else
|
|
697
|
+
console.log(renderSandbox(restored, 'restored'));
|
|
698
|
+
}));
|
|
699
|
+
registerAccessCommands(sandbox);
|
|
700
|
+
const box = program
|
|
701
|
+
.command('box')
|
|
702
|
+
.description('Deprecated: a box is a sandbox with --persistent and --expose — use `runcloud sandbox`');
|
|
703
|
+
withOutput(box
|
|
704
|
+
.command('create')
|
|
705
|
+
.description('Deprecated: use `sandbox create --expose <port> --name <name>`')
|
|
706
|
+
.argument('<name>', 'hostname prefix: <name>-box.run.cloud')
|
|
707
|
+
.option('--sandbox <id>', 'attach an existing sandbox created with --idle-pause 0')
|
|
708
|
+
.option('--port <port>', 'guest port to publish', parsePort, 3000)
|
|
709
|
+
.option('--image <ref>', 'base image for a new sandbox (runcloud/box or runcloud/box-agent)', 'runcloud/box')
|
|
710
|
+
.option('--region <region>', 'placement region for a new sandbox')
|
|
711
|
+
.option('--org <id>', 'billing and ownership organization for a new sandbox')
|
|
712
|
+
.option('--cpu <cores>', 'vCPU allocation for a new box', parseCpuCores, DEFAULT_BOX_CPU_CORES)
|
|
713
|
+
.option('--memory <mb>', 'memory allocation in MiB for a new box', parseMemoryMb, DEFAULT_BOX_MEMORY_MIB)
|
|
714
|
+
.option('--disk <gb>', 'root disk capacity in GiB', parseDiskGb)).action((name, opts) => {
|
|
715
|
+
noteDeprecated('box create', opts.sandbox
|
|
716
|
+
? `sandbox expose ${opts.sandbox} --name ${name}`
|
|
717
|
+
: `sandbox create --expose ${opts.port} --name ${name}`);
|
|
718
|
+
return run(async () => {
|
|
719
|
+
const api = runCloudApi();
|
|
720
|
+
let sandboxId = opts.sandbox;
|
|
721
|
+
if (!sandboxId) {
|
|
722
|
+
const sandboxBody = {
|
|
723
|
+
image: opts.image,
|
|
724
|
+
name,
|
|
725
|
+
idlePauseSeconds: 0,
|
|
726
|
+
};
|
|
727
|
+
if (opts.region)
|
|
728
|
+
sandboxBody.region = opts.region;
|
|
729
|
+
if (opts.org)
|
|
730
|
+
sandboxBody.orgId = opts.org;
|
|
731
|
+
sandboxBody.cpu = opts.cpu;
|
|
732
|
+
sandboxBody.memory = opts.memory;
|
|
733
|
+
if (opts.disk !== undefined)
|
|
734
|
+
sandboxBody.disk = opts.disk;
|
|
735
|
+
const created = await api.post('/run-cloud/sandboxes', sandboxBody);
|
|
736
|
+
sandboxId = typeof created?.id === 'string' ? created.id : undefined;
|
|
737
|
+
if (!sandboxId)
|
|
738
|
+
throw new Error('sandbox create response did not include an id');
|
|
739
|
+
}
|
|
740
|
+
print(await attachHostname(api, sandboxId, name, opts.port), opts);
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
withOutput(box.command('list').description('Deprecated: use `sandbox list`')).action((opts) => {
|
|
744
|
+
noteDeprecated('box list', 'sandbox list');
|
|
745
|
+
return run(async () => {
|
|
746
|
+
const data = await runCloudApi().get('/run-cloud/boxes');
|
|
747
|
+
print(data?.items ?? [], opts);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
withOutput(box.command('get').description('Deprecated: use `sandbox get <sandbox-id>`').argument('<id>', 'box id')).action((id, opts) => {
|
|
751
|
+
noteDeprecated('box get', 'sandbox get <sandbox-id>');
|
|
752
|
+
return run(async () => {
|
|
753
|
+
print(await runCloudApi().get(`/run-cloud/boxes/${encodeURIComponent(id)}`), opts);
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
withOutput(box
|
|
757
|
+
.command('port')
|
|
758
|
+
.description('Deprecated: use `sandbox expose <sandbox-id> --port <port>`')
|
|
759
|
+
.argument('<id>', 'box id')
|
|
760
|
+
.argument('<port>', 'guest port', parsePort)).action((id, port, opts) => {
|
|
761
|
+
noteDeprecated('box port', `sandbox expose <sandbox-id> --port ${port}`);
|
|
762
|
+
return run(async () => {
|
|
763
|
+
print(await runCloudApi().patch(`/run-cloud/boxes/${encodeURIComponent(id)}`, { port }), opts);
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
withOutput(box
|
|
767
|
+
.command('route')
|
|
768
|
+
.description('Deprecated: use `sandbox expose <sandbox-id> --reconcile`')
|
|
769
|
+
.argument('<id>', 'box id')).action((id, opts) => {
|
|
770
|
+
noteDeprecated('box route', 'sandbox expose <sandbox-id> --reconcile');
|
|
771
|
+
return run(async () => {
|
|
772
|
+
print(await runCloudApi().post(`/run-cloud/boxes/${encodeURIComponent(id)}/route`), opts);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
withOutput(box
|
|
776
|
+
.command('rm')
|
|
777
|
+
.description('Deprecated: use `sandbox rm <sandbox-id>`')
|
|
778
|
+
.argument('<id>', 'box id')).action((id, opts) => {
|
|
779
|
+
noteDeprecated('box rm', 'sandbox rm <sandbox-id>');
|
|
780
|
+
return run(async () => {
|
|
781
|
+
await runCloudApi().delete(`/run-cloud/boxes/${encodeURIComponent(id)}`);
|
|
782
|
+
if (!opts.json)
|
|
783
|
+
console.log(`destroyed box ${id}`);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
registerAccessCommands(box, { deprecated: true });
|
|
787
|
+
}
|