xapi-to 0.1.19 → 0.1.20
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 +208 -1
- package/dist/chunk-TYY6JR6O.js +870 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +998 -670
- package/dist/openai-sandbox-client.d.ts +85 -0
- package/dist/openai-sandbox-client.js +285 -0
- package/examples/openai-agents-sandbox-local.ts +131 -0
- package/examples/sandbox-api-cli-openai.mjs +450 -0
- package/package.json +25 -3
- package/scripts/openai-sandbox-agent-e2e.ts +219 -0
- package/scripts/sandbox-playground-e2e.mjs +463 -0
- package/skills/xapi/SKILL.md +18 -5
- package/skills/xapi/guides/linkedin.md +55 -0
- package/skills/xapi/guides/sandbox.md +466 -0
- package/skills/xapi/guides/serper.md +124 -0
- package/src/client.ts +664 -0
- package/src/config.ts +160 -0
- package/src/openai-sandbox-client.ts +349 -0
- package/src/sandbox-client.ts +289 -0
package/dist/index.js
CHANGED
|
@@ -1,661 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
return { [primitiveKey]: row };
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
function singularKey(key) {
|
|
57
|
-
if (key === "categories") return "category";
|
|
58
|
-
if (key.endsWith("ies")) return `${key.slice(0, -3)}y`;
|
|
59
|
-
if (key.endsWith("s")) return key.slice(0, -1);
|
|
60
|
-
return "value";
|
|
61
|
-
}
|
|
62
|
-
function formatCell(value) {
|
|
63
|
-
if (value === null || value === void 0) return "";
|
|
64
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
65
|
-
return String(value);
|
|
66
|
-
}
|
|
67
|
-
function printTable(rows) {
|
|
68
|
-
if (rows.length === 0) {
|
|
69
|
-
console.log("(empty)");
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const keys = Object.keys(rows[0]);
|
|
73
|
-
const widths = keys.map(
|
|
74
|
-
(k) => Math.min(40, Math.max(k.length, ...rows.map((r) => formatCell(r[k]).length)))
|
|
75
|
-
);
|
|
76
|
-
const sep = widths.map((w) => "-".repeat(w)).join(" ");
|
|
77
|
-
const header = keys.map((k, i) => k.padEnd(widths[i])).join(" ");
|
|
78
|
-
console.log(header);
|
|
79
|
-
console.log(sep);
|
|
80
|
-
for (const row of rows) {
|
|
81
|
-
const line = keys.map((k, i) => formatCell(row[k]).slice(0, widths[i]).padEnd(widths[i])).join(" ");
|
|
82
|
-
console.log(line);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function err(msg, detail) {
|
|
86
|
-
if (process.stderr.isTTY) {
|
|
87
|
-
console.error(`Error: ${msg}`);
|
|
88
|
-
if (detail !== void 0) console.error(` ${detail}`);
|
|
89
|
-
} else {
|
|
90
|
-
const out = { error: msg };
|
|
91
|
-
if (detail !== void 0) out.detail = detail;
|
|
92
|
-
console.error(JSON.stringify(out));
|
|
93
|
-
}
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// src/config.ts
|
|
98
|
-
import { homedir } from "os";
|
|
99
|
-
import { join } from "path";
|
|
100
|
-
var XAPI_ACTION_HOST = process.env.XAPI_ACTION_HOST || "action.xapi.to";
|
|
101
|
-
var XAPI_API_HOST = process.env.XAPI_API_HOST || "api.xapi.to";
|
|
102
|
-
function scheme(host) {
|
|
103
|
-
return isLoopbackHost(host) ? "http" : "https";
|
|
104
|
-
}
|
|
105
|
-
var ALLOWED_HOST_EXACT = ["xapi.to", "xapi.xyz"];
|
|
106
|
-
var ALLOWED_HOST_SUFFIXES = [".xapi.to", ".xapi.xyz"];
|
|
107
|
-
function hostnameOf(hostOrUrl) {
|
|
108
|
-
const raw = hostOrUrl.includes("://") ? hostOrUrl : `http://${hostOrUrl}`;
|
|
109
|
-
try {
|
|
110
|
-
return new URL(raw).hostname.toLowerCase();
|
|
111
|
-
} catch {
|
|
112
|
-
return "";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function isLoopbackIPv4(h) {
|
|
116
|
-
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
117
|
-
if (!m) return false;
|
|
118
|
-
const octets = m.slice(1).map(Number);
|
|
119
|
-
return octets.every((o) => o <= 255) && octets[0] === 127;
|
|
120
|
-
}
|
|
121
|
-
function isLoopbackHostname(h) {
|
|
122
|
-
return h === "localhost" || h.endsWith(".localhost") || h === "::1" || h === "[::1]" || isLoopbackIPv4(h);
|
|
123
|
-
}
|
|
124
|
-
function isLoopbackHost(hostOrUrl) {
|
|
125
|
-
return isLoopbackHostname(hostnameOf(hostOrUrl));
|
|
126
|
-
}
|
|
127
|
-
function isAllowedHost(hostOrUrl) {
|
|
128
|
-
const h = hostnameOf(hostOrUrl);
|
|
129
|
-
if (!h) return false;
|
|
130
|
-
if (isLoopbackHostname(h)) return true;
|
|
131
|
-
if (ALLOWED_HOST_EXACT.includes(h)) return true;
|
|
132
|
-
return ALLOWED_HOST_SUFFIXES.some((suffix) => h.endsWith(suffix));
|
|
133
|
-
}
|
|
134
|
-
function assertAllowedHost(hostOrUrl) {
|
|
135
|
-
if (!isAllowedHost(hostOrUrl)) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`refusing to contact untrusted host "${hostnameOf(hostOrUrl) || hostOrUrl}": the xapi API key may only be sent to *.xapi.to, *.xapi.xyz, or localhost`
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
var CONFIG_DIR = join(homedir(), ".xapi");
|
|
142
|
-
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
143
|
-
function loadFileConfig() {
|
|
144
|
-
if (!existsSync(CONFIG_FILE)) return {};
|
|
145
|
-
try {
|
|
146
|
-
const parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
147
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
148
|
-
return typeof parsed.apiKey === "string" && parsed.apiKey.trim() ? { apiKey: parsed.apiKey } : {};
|
|
149
|
-
} catch {
|
|
150
|
-
return {};
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
function getApiKeySource() {
|
|
154
|
-
if (process.env.XAPI_KEY) return "XAPI_KEY";
|
|
155
|
-
if (process.env.XAPI_API_KEY) return "XAPI_API_KEY";
|
|
156
|
-
return loadFileConfig().apiKey ? "file" : "none";
|
|
157
|
-
}
|
|
158
|
-
function getConfig() {
|
|
159
|
-
const file = loadFileConfig();
|
|
160
|
-
return {
|
|
161
|
-
actionHost: XAPI_ACTION_HOST,
|
|
162
|
-
apiKey: process.env.XAPI_KEY || process.env.XAPI_API_KEY || file.apiKey
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
function requireApiKey(cfg) {
|
|
166
|
-
if (!cfg.apiKey) {
|
|
167
|
-
err("API key not configured", 'Run "npx xapi-to register" to create an account, or "npx xapi-to config set apiKey=<key>" to set an existing key.');
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function saveConfig(updates) {
|
|
171
|
-
const current = loadFileConfig();
|
|
172
|
-
const merged = { ...current, ...updates };
|
|
173
|
-
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
174
|
-
if (process.platform !== "win32") chmodSync(CONFIG_DIR, 448);
|
|
175
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
176
|
-
if (process.platform !== "win32") chmodSync(CONFIG_FILE, 384);
|
|
177
|
-
}
|
|
178
|
-
function showConfig() {
|
|
179
|
-
const cfg = getConfig();
|
|
180
|
-
return {
|
|
181
|
-
actionHost: cfg.actionHost,
|
|
182
|
-
apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : void 0,
|
|
183
|
-
source: {
|
|
184
|
-
apiKey: getApiKeySource()
|
|
185
|
-
},
|
|
186
|
-
configFile: CONFIG_FILE
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// src/client.ts
|
|
191
|
-
import { open, rm } from "fs/promises";
|
|
192
|
-
import { once } from "events";
|
|
193
|
-
import { resolve } from "path";
|
|
194
|
-
import { Readable, Transform } from "stream";
|
|
195
|
-
import { pipeline } from "stream/promises";
|
|
196
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
197
|
-
var EXECUTE_TIMEOUT_MS = 6e4;
|
|
198
|
-
var TRANSFER_IDLE_TIMEOUT_MS = 6e4;
|
|
199
|
-
var IDEMPOTENT_RETRIES = 2;
|
|
200
|
-
var RETRY_BASE_DELAY_MS = 500;
|
|
201
|
-
var RETRY_MAX_DELAY_MS = 8e3;
|
|
202
|
-
var HttpError = class extends Error {
|
|
203
|
-
constructor(status, detail, retryAfterMs) {
|
|
204
|
-
super(`HTTP ${status}: ${detail}`);
|
|
205
|
-
this.status = status;
|
|
206
|
-
this.retryAfterMs = retryAfterMs;
|
|
207
|
-
this.name = "HttpError";
|
|
208
|
-
}
|
|
209
|
-
status;
|
|
210
|
-
retryAfterMs;
|
|
211
|
-
};
|
|
212
|
-
var RequestTimeoutError = class extends Error {
|
|
213
|
-
constructor(timeoutMs) {
|
|
214
|
-
super(`request timed out after ${timeoutMs}ms`);
|
|
215
|
-
this.timeoutMs = timeoutMs;
|
|
216
|
-
this.name = "RequestTimeoutError";
|
|
217
|
-
}
|
|
218
|
-
timeoutMs;
|
|
219
|
-
};
|
|
220
|
-
function isRetryableStatus(status) {
|
|
221
|
-
return status === 408 || status === 429 || status === 502 || status === 503 || status === 504;
|
|
222
|
-
}
|
|
223
|
-
function isRetryableNetworkError(e) {
|
|
224
|
-
if (!(e instanceof Error)) return false;
|
|
225
|
-
if (e instanceof HttpError || e instanceof RequestTimeoutError) return false;
|
|
226
|
-
if (e.name === "AbortError") return false;
|
|
227
|
-
return e instanceof TypeError || /network|fetch failed|econn|etimedout|eai_again|socket|dns/i.test(e.message);
|
|
228
|
-
}
|
|
229
|
-
function isRetryableRequestError(e) {
|
|
230
|
-
if (e instanceof HttpError) return isRetryableStatus(e.status);
|
|
231
|
-
if (e instanceof RequestTimeoutError) return true;
|
|
232
|
-
return isRetryableNetworkError(e);
|
|
233
|
-
}
|
|
234
|
-
function retryBaseDelayMs() {
|
|
235
|
-
const override = Number(process.env.XAPI_RETRY_BASE_MS);
|
|
236
|
-
return Number.isFinite(override) && override > 0 ? override : RETRY_BASE_DELAY_MS;
|
|
237
|
-
}
|
|
238
|
-
function transferIdleTimeoutMs() {
|
|
239
|
-
const override = Number(process.env.XAPI_TRANSFER_IDLE_TIMEOUT_MS);
|
|
240
|
-
return Number.isFinite(override) && override > 0 ? override : TRANSFER_IDLE_TIMEOUT_MS;
|
|
241
|
-
}
|
|
242
|
-
function backoffDelayMs(attempt, retryAfterMs) {
|
|
243
|
-
if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
|
|
244
|
-
return Math.min(retryAfterMs, RETRY_MAX_DELAY_MS);
|
|
245
|
-
}
|
|
246
|
-
const capped = Math.min(retryBaseDelayMs() * 2 ** attempt, RETRY_MAX_DELAY_MS);
|
|
247
|
-
return capped / 2 + Math.random() * (capped / 2);
|
|
248
|
-
}
|
|
249
|
-
function parseRetryAfterMs(res) {
|
|
250
|
-
const header = res.headers.get("retry-after");
|
|
251
|
-
if (!header) return void 0;
|
|
252
|
-
const seconds = Number(header);
|
|
253
|
-
if (Number.isFinite(seconds)) return seconds * 1e3;
|
|
254
|
-
const at = Date.parse(header);
|
|
255
|
-
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
|
|
256
|
-
}
|
|
257
|
-
function sleep(ms) {
|
|
258
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
259
|
-
}
|
|
260
|
-
async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
|
|
261
|
-
assertAllowedHost(url);
|
|
262
|
-
let attempt = 0;
|
|
263
|
-
while (true) {
|
|
264
|
-
const controller = new AbortController();
|
|
265
|
-
let timedOut = false;
|
|
266
|
-
const timer = setTimeout(() => {
|
|
267
|
-
timedOut = true;
|
|
268
|
-
controller.abort();
|
|
269
|
-
}, timeoutMs);
|
|
270
|
-
try {
|
|
271
|
-
const res = await fetch(url, { ...options, redirect: "manual", signal: controller.signal });
|
|
272
|
-
if (res.status >= 300 && res.status < 400) {
|
|
273
|
-
throw new Error(
|
|
274
|
-
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (!res.ok) {
|
|
278
|
-
const retryAfterMs = isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0;
|
|
279
|
-
if (isRetryableStatus(res.status) && attempt < retries) {
|
|
280
|
-
await res.text().catch(() => "");
|
|
281
|
-
clearTimeout(timer);
|
|
282
|
-
await sleep(backoffDelayMs(attempt, retryAfterMs));
|
|
283
|
-
attempt++;
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
const text2 = await res.text();
|
|
287
|
-
throw new HttpError(res.status, text2.slice(0, 300), retryAfterMs);
|
|
288
|
-
}
|
|
289
|
-
if (res.status === 204) {
|
|
290
|
-
return void 0;
|
|
291
|
-
}
|
|
292
|
-
const text = await res.text();
|
|
293
|
-
if (!text.trim()) {
|
|
294
|
-
return void 0;
|
|
295
|
-
}
|
|
296
|
-
const body = JSON.parse(text);
|
|
297
|
-
if (body && typeof body === "object" && "success" in body && body.success === false) {
|
|
298
|
-
const data = body.data;
|
|
299
|
-
if (data?.statusCode === 401 || data?.error === "Unauthorized") {
|
|
300
|
-
throw new Error(
|
|
301
|
-
"Authentication failed: " + (data.message || "Invalid or missing API key") + '. Run "npx xapi-to config set apiKey=<key>" to update your key.'
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
if (data?.error === "OAuth Required" || data?.statusCode === 403 && data?.message?.includes("OAuth")) {
|
|
305
|
-
throw new Error(
|
|
306
|
-
(data.message || "OAuth authorization required") + '. Run "xapi-to oauth bind" to connect your account.'
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return body;
|
|
311
|
-
} catch (e) {
|
|
312
|
-
if (timedOut) {
|
|
313
|
-
const timeoutError = new RequestTimeoutError(timeoutMs);
|
|
314
|
-
if (attempt < retries) {
|
|
315
|
-
await sleep(backoffDelayMs(attempt));
|
|
316
|
-
attempt++;
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
throw timeoutError;
|
|
320
|
-
}
|
|
321
|
-
if (isRetryableNetworkError(e) && attempt < retries) {
|
|
322
|
-
clearTimeout(timer);
|
|
323
|
-
await sleep(backoffDelayMs(attempt));
|
|
324
|
-
attempt++;
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
throw e;
|
|
328
|
-
} finally {
|
|
329
|
-
clearTimeout(timer);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function headers(apiKey) {
|
|
334
|
-
const h = { "Content-Type": "application/json" };
|
|
335
|
-
if (apiKey) h["XAPI-Key"] = apiKey;
|
|
336
|
-
return h;
|
|
337
|
-
}
|
|
338
|
-
function baseUrl(opts) {
|
|
339
|
-
return `${scheme(opts.actionHost)}://${opts.actionHost}`;
|
|
340
|
-
}
|
|
341
|
-
async function actionList(opts, params = {}) {
|
|
342
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions`);
|
|
343
|
-
if (params.page) url.searchParams.set("page", String(params.page));
|
|
344
|
-
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
345
|
-
if (params.category) url.searchParams.set("category", params.category);
|
|
346
|
-
if (params.source) url.searchParams.set("source", params.source);
|
|
347
|
-
if (params.service_id) url.searchParams.set("service_id", params.service_id);
|
|
348
|
-
return request(
|
|
349
|
-
url.toString(),
|
|
350
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
351
|
-
DEFAULT_TIMEOUT_MS,
|
|
352
|
-
IDEMPOTENT_RETRIES
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
async function actionSearch(query, opts, params = {}) {
|
|
356
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions/search`);
|
|
357
|
-
url.searchParams.set("q", query);
|
|
358
|
-
if (params.category) url.searchParams.set("category", params.category);
|
|
359
|
-
if (params.source) url.searchParams.set("source", params.source);
|
|
360
|
-
if (params.page) url.searchParams.set("page", String(params.page));
|
|
361
|
-
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
362
|
-
if (params.include_all_versions) url.searchParams.set("include_all_versions", "true");
|
|
363
|
-
if (params.sort) url.searchParams.set("sort", params.sort);
|
|
364
|
-
return request(
|
|
365
|
-
url.toString(),
|
|
366
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
367
|
-
DEFAULT_TIMEOUT_MS,
|
|
368
|
-
IDEMPOTENT_RETRIES
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
async function actionCategories(opts, params = {}) {
|
|
372
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions/categories`);
|
|
373
|
-
if (params.source) url.searchParams.set("source", params.source);
|
|
374
|
-
return request(
|
|
375
|
-
url.toString(),
|
|
376
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
377
|
-
DEFAULT_TIMEOUT_MS,
|
|
378
|
-
IDEMPOTENT_RETRIES
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
async function actionGet(id, opts) {
|
|
382
|
-
return request(
|
|
383
|
-
`${baseUrl(opts)}/v1/actions/${encodeURIComponent(id)}`,
|
|
384
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
385
|
-
DEFAULT_TIMEOUT_MS,
|
|
386
|
-
IDEMPOTENT_RETRIES
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
async function actionBatch(ids, opts) {
|
|
390
|
-
return request(
|
|
391
|
-
`${baseUrl(opts)}/v1/actions/batch`,
|
|
392
|
-
{
|
|
393
|
-
method: "POST",
|
|
394
|
-
headers: headers(opts.apiKey),
|
|
395
|
-
body: JSON.stringify({ ids })
|
|
396
|
-
},
|
|
397
|
-
DEFAULT_TIMEOUT_MS,
|
|
398
|
-
IDEMPOTENT_RETRIES
|
|
399
|
-
// read-only metadata fetch — safe to retry
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeoutMs = EXECUTE_TIMEOUT_MS) {
|
|
403
|
-
return request(
|
|
404
|
-
`${baseUrl(opts)}/v1/actions/execute`,
|
|
405
|
-
{
|
|
406
|
-
method: "POST",
|
|
407
|
-
headers: headers(opts.apiKey),
|
|
408
|
-
body: JSON.stringify({ action_id: actionId, ...httpMethod ? { method: httpMethod } : {}, input })
|
|
409
|
-
},
|
|
410
|
-
Math.min(timeoutMs, EXECUTE_TIMEOUT_MS),
|
|
411
|
-
retries
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
async function actionStream(actionId, input, opts, httpMethod) {
|
|
415
|
-
const controller = new AbortController();
|
|
416
|
-
let timedOut = false;
|
|
417
|
-
let activeTimeoutMs = EXECUTE_TIMEOUT_MS;
|
|
418
|
-
let timer;
|
|
419
|
-
const resetTimeout = (timeoutMs) => {
|
|
420
|
-
if (timer) clearTimeout(timer);
|
|
421
|
-
activeTimeoutMs = timeoutMs;
|
|
422
|
-
timer = setTimeout(() => {
|
|
423
|
-
timedOut = true;
|
|
424
|
-
controller.abort();
|
|
425
|
-
}, timeoutMs);
|
|
426
|
-
};
|
|
427
|
-
resetTimeout(EXECUTE_TIMEOUT_MS);
|
|
428
|
-
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
429
|
-
assertAllowedHost(url);
|
|
430
|
-
try {
|
|
431
|
-
const res = await fetch(url, {
|
|
432
|
-
method: "POST",
|
|
433
|
-
headers: {
|
|
434
|
-
...headers(opts.apiKey),
|
|
435
|
-
Accept: "text/event-stream"
|
|
436
|
-
},
|
|
437
|
-
body: JSON.stringify({
|
|
438
|
-
action_id: actionId,
|
|
439
|
-
...httpMethod ? { method: httpMethod } : {},
|
|
440
|
-
input,
|
|
441
|
-
stream: true
|
|
442
|
-
}),
|
|
443
|
-
redirect: "manual",
|
|
444
|
-
signal: controller.signal
|
|
445
|
-
});
|
|
446
|
-
if (res.status >= 300 && res.status < 400) {
|
|
447
|
-
throw new Error(
|
|
448
|
-
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
if (!res.ok) {
|
|
452
|
-
const text = await res.text();
|
|
453
|
-
throw new HttpError(
|
|
454
|
-
res.status,
|
|
455
|
-
text.slice(0, 300),
|
|
456
|
-
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
const contentType = res.headers.get("content-type") || "";
|
|
460
|
-
if (!contentType.toLowerCase().includes("text/event-stream")) {
|
|
461
|
-
const text = await res.text();
|
|
462
|
-
throw new Error(
|
|
463
|
-
`expected an SSE response but received "${contentType || "unknown"}": ${text.slice(0, 300)}`
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
if (!res.body) return;
|
|
467
|
-
const idleTimeoutMs = transferIdleTimeoutMs();
|
|
468
|
-
resetTimeout(idleTimeoutMs);
|
|
469
|
-
const source = Readable.fromWeb(res.body);
|
|
470
|
-
for await (const chunk of source) {
|
|
471
|
-
resetTimeout(idleTimeoutMs);
|
|
472
|
-
if (!process.stdout.write(chunk)) await once(process.stdout, "drain");
|
|
473
|
-
}
|
|
474
|
-
} catch (error) {
|
|
475
|
-
if (timedOut) throw new RequestTimeoutError(activeTimeoutMs);
|
|
476
|
-
throw error;
|
|
477
|
-
} finally {
|
|
478
|
-
if (timer) clearTimeout(timer);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
async function actionDownload(actionId, input, opts, outputPath, httpMethod) {
|
|
482
|
-
const controller = new AbortController();
|
|
483
|
-
let timedOut = false;
|
|
484
|
-
let activeTimeoutMs = EXECUTE_TIMEOUT_MS;
|
|
485
|
-
let timer;
|
|
486
|
-
const resetTimeout = (timeoutMs) => {
|
|
487
|
-
if (timer) clearTimeout(timer);
|
|
488
|
-
activeTimeoutMs = timeoutMs;
|
|
489
|
-
timer = setTimeout(() => {
|
|
490
|
-
timedOut = true;
|
|
491
|
-
controller.abort();
|
|
492
|
-
}, timeoutMs);
|
|
493
|
-
};
|
|
494
|
-
resetTimeout(EXECUTE_TIMEOUT_MS);
|
|
495
|
-
const target = resolve(outputPath);
|
|
496
|
-
let file;
|
|
497
|
-
let complete = false;
|
|
498
|
-
try {
|
|
499
|
-
try {
|
|
500
|
-
file = await open(target, "wx");
|
|
501
|
-
} catch (error) {
|
|
502
|
-
if (error?.code === "EEXIST") {
|
|
503
|
-
throw new Error(`Output file already exists: ${target}`);
|
|
504
|
-
}
|
|
505
|
-
throw error;
|
|
506
|
-
}
|
|
507
|
-
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
508
|
-
assertAllowedHost(url);
|
|
509
|
-
const res = await fetch(url, {
|
|
510
|
-
method: "POST",
|
|
511
|
-
headers: headers(opts.apiKey),
|
|
512
|
-
body: JSON.stringify({
|
|
513
|
-
action_id: actionId,
|
|
514
|
-
...httpMethod ? { method: httpMethod } : {},
|
|
515
|
-
input,
|
|
516
|
-
response_mode: "raw"
|
|
517
|
-
}),
|
|
518
|
-
redirect: "manual",
|
|
519
|
-
signal: controller.signal
|
|
520
|
-
});
|
|
521
|
-
if (res.status >= 300 && res.status < 400) {
|
|
522
|
-
throw new Error(
|
|
523
|
-
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
if (!res.ok) {
|
|
527
|
-
const text = await res.text();
|
|
528
|
-
throw new HttpError(
|
|
529
|
-
res.status,
|
|
530
|
-
text.slice(0, 300),
|
|
531
|
-
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
let bytes = 0;
|
|
535
|
-
if (res.body) {
|
|
536
|
-
const idleTimeoutMs = transferIdleTimeoutMs();
|
|
537
|
-
resetTimeout(idleTimeoutMs);
|
|
538
|
-
const source = Readable.fromWeb(res.body);
|
|
539
|
-
const counter = new Transform({
|
|
540
|
-
transform(chunk, _encoding, callback) {
|
|
541
|
-
resetTimeout(idleTimeoutMs);
|
|
542
|
-
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
543
|
-
callback(null, chunk);
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
await pipeline(source, counter, file.createWriteStream());
|
|
547
|
-
} else {
|
|
548
|
-
await file.close();
|
|
549
|
-
}
|
|
550
|
-
complete = true;
|
|
551
|
-
return {
|
|
552
|
-
output: target,
|
|
553
|
-
bytes,
|
|
554
|
-
contentType: res.headers.get("content-type") || void 0,
|
|
555
|
-
contentDisposition: res.headers.get("content-disposition") || void 0,
|
|
556
|
-
status: res.status
|
|
557
|
-
};
|
|
558
|
-
} catch (error) {
|
|
559
|
-
if (timedOut) throw new RequestTimeoutError(activeTimeoutMs);
|
|
560
|
-
throw error;
|
|
561
|
-
} finally {
|
|
562
|
-
if (timer) clearTimeout(timer);
|
|
563
|
-
if (!complete && file) {
|
|
564
|
-
await file.close().catch(() => void 0);
|
|
565
|
-
await rm(target, { force: true }).catch(() => void 0);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
async function actionServices(opts, params = {}) {
|
|
570
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions/services`);
|
|
571
|
-
if (params.page) url.searchParams.set("page", String(params.page));
|
|
572
|
-
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
573
|
-
if (params.category) url.searchParams.set("category", params.category);
|
|
574
|
-
return request(
|
|
575
|
-
url.toString(),
|
|
576
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
577
|
-
DEFAULT_TIMEOUT_MS,
|
|
578
|
-
IDEMPOTENT_RETRIES
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
async function healthCheck(opts) {
|
|
582
|
-
return request(
|
|
583
|
-
`${baseUrl(opts)}/health`,
|
|
584
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
585
|
-
5e3,
|
|
586
|
-
0
|
|
587
|
-
// health is a quick connectivity probe — fail fast, don't retry
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
async function loginWithApiKey(apiKey, apiHost) {
|
|
591
|
-
return request(
|
|
592
|
-
`${scheme(apiHost)}://${apiHost}/api/auth/login/apikey`,
|
|
593
|
-
{
|
|
594
|
-
method: "POST",
|
|
595
|
-
headers: { "Content-Type": "application/json" },
|
|
596
|
-
body: JSON.stringify({ apiKey })
|
|
597
|
-
},
|
|
598
|
-
DEFAULT_TIMEOUT_MS,
|
|
599
|
-
IDEMPOTENT_RETRIES
|
|
600
|
-
// auth exchange has no side effect — safe to retry
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
function jwtHeaders(jwtToken) {
|
|
604
|
-
return { "Content-Type": "application/json", Authorization: `Bearer ${jwtToken}` };
|
|
605
|
-
}
|
|
606
|
-
async function listKeys(jwtToken, apiHost) {
|
|
607
|
-
return request(
|
|
608
|
-
`${scheme(apiHost)}://${apiHost}/api/keys`,
|
|
609
|
-
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
610
|
-
DEFAULT_TIMEOUT_MS,
|
|
611
|
-
IDEMPOTENT_RETRIES
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
async function enableOAuthForKey(keyId, plaintextKey, jwtToken, apiHost) {
|
|
615
|
-
return request(
|
|
616
|
-
`${scheme(apiHost)}://${apiHost}/api/keys/${keyId}/enable-oauth`,
|
|
617
|
-
{
|
|
618
|
-
method: "POST",
|
|
619
|
-
headers: jwtHeaders(jwtToken),
|
|
620
|
-
body: JSON.stringify({ plaintextKey })
|
|
621
|
-
}
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
async function listOAuthProviders(apiHost) {
|
|
625
|
-
return request(
|
|
626
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/providers`,
|
|
627
|
-
{ method: "GET", headers: { "Content-Type": "application/json" } },
|
|
628
|
-
DEFAULT_TIMEOUT_MS,
|
|
629
|
-
IDEMPOTENT_RETRIES
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost, scopes) {
|
|
633
|
-
const body = { apiKeyId, providerId };
|
|
634
|
-
if (scopes) body.scopes = scopes;
|
|
635
|
-
return request(
|
|
636
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/authorize`,
|
|
637
|
-
{
|
|
638
|
-
method: "POST",
|
|
639
|
-
headers: jwtHeaders(jwtToken),
|
|
640
|
-
body: JSON.stringify(body)
|
|
641
|
-
}
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
async function listOAuthBindings(jwtToken, apiHost) {
|
|
645
|
-
return request(
|
|
646
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
|
|
647
|
-
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
648
|
-
DEFAULT_TIMEOUT_MS,
|
|
649
|
-
IDEMPOTENT_RETRIES
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
async function deleteOAuthBinding(bindingId, jwtToken, apiHost) {
|
|
653
|
-
const result = await request(
|
|
654
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/bindings/${bindingId}`,
|
|
655
|
-
{ method: "DELETE", headers: jwtHeaders(jwtToken) }
|
|
656
|
-
);
|
|
657
|
-
return result ?? { success: true };
|
|
658
|
-
}
|
|
2
|
+
import {
|
|
3
|
+
HttpError,
|
|
4
|
+
XAPI_API_HOST,
|
|
5
|
+
XAPI_SANDBOX_HOST,
|
|
6
|
+
__export,
|
|
7
|
+
actionBatch,
|
|
8
|
+
actionCall,
|
|
9
|
+
actionCategories,
|
|
10
|
+
actionDownload,
|
|
11
|
+
actionGet,
|
|
12
|
+
actionList,
|
|
13
|
+
actionSearch,
|
|
14
|
+
actionServices,
|
|
15
|
+
actionStream,
|
|
16
|
+
assertAllowedHost,
|
|
17
|
+
deleteOAuthBinding,
|
|
18
|
+
enableOAuthForKey,
|
|
19
|
+
err,
|
|
20
|
+
getApiKeySource,
|
|
21
|
+
getConfig,
|
|
22
|
+
getFormat,
|
|
23
|
+
healthCheck,
|
|
24
|
+
initiateOAuth,
|
|
25
|
+
isRetryableRequestError,
|
|
26
|
+
listKeys,
|
|
27
|
+
listOAuthBindings,
|
|
28
|
+
listOAuthProviders,
|
|
29
|
+
loginWithApiKey,
|
|
30
|
+
output,
|
|
31
|
+
request,
|
|
32
|
+
requireApiKey,
|
|
33
|
+
sandboxAudit,
|
|
34
|
+
sandboxCreate,
|
|
35
|
+
sandboxExec,
|
|
36
|
+
sandboxExtension,
|
|
37
|
+
sandboxFileList,
|
|
38
|
+
sandboxFileRead,
|
|
39
|
+
sandboxFileWrite,
|
|
40
|
+
sandboxGet,
|
|
41
|
+
sandboxHistory,
|
|
42
|
+
sandboxList,
|
|
43
|
+
sandboxOfferings,
|
|
44
|
+
sandboxPort,
|
|
45
|
+
sandboxQuote,
|
|
46
|
+
sandboxStateAction,
|
|
47
|
+
sandboxWait,
|
|
48
|
+
saveConfig,
|
|
49
|
+
scheme,
|
|
50
|
+
showConfig
|
|
51
|
+
} from "./chunk-TYY6JR6O.js";
|
|
659
52
|
|
|
660
53
|
// src/codegen.ts
|
|
661
54
|
var TARGET_MAP = {
|
|
@@ -730,7 +123,7 @@ function validateHost(host) {
|
|
|
730
123
|
throw new Error(`invalid actionHost: "${host}" \u2014 must be a valid hostname with optional port`);
|
|
731
124
|
}
|
|
732
125
|
}
|
|
733
|
-
function
|
|
126
|
+
function baseUrl(actionHost) {
|
|
734
127
|
validateHost(actionHost);
|
|
735
128
|
return `${scheme(actionHost)}://${actionHost}/v1/actions/execute`;
|
|
736
129
|
}
|
|
@@ -746,7 +139,7 @@ function shellEscape(s) {
|
|
|
746
139
|
return s.replace(/'/g, "'\\''");
|
|
747
140
|
}
|
|
748
141
|
function genCurl(params) {
|
|
749
|
-
const url =
|
|
142
|
+
const url = baseUrl(params.actionHost);
|
|
750
143
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
751
144
|
return [
|
|
752
145
|
"# Set XAPI_KEY env var or replace with your key",
|
|
@@ -757,7 +150,7 @@ function genCurl(params) {
|
|
|
757
150
|
].join("\n");
|
|
758
151
|
}
|
|
759
152
|
function genPython(lib, params) {
|
|
760
|
-
const url =
|
|
153
|
+
const url = baseUrl(params.actionHost);
|
|
761
154
|
const payload = { action_id: params.actionId, ...params.method ? { method: params.method } : {}, input: params.input };
|
|
762
155
|
return [
|
|
763
156
|
`# pip install ${lib}`,
|
|
@@ -777,7 +170,7 @@ function genPython(lib, params) {
|
|
|
777
170
|
].join("\n");
|
|
778
171
|
}
|
|
779
172
|
function genJavaScriptFetch(params) {
|
|
780
|
-
const url =
|
|
173
|
+
const url = baseUrl(params.actionHost);
|
|
781
174
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
782
175
|
return [
|
|
783
176
|
"// Set XAPI_KEY env var or replace with your key",
|
|
@@ -793,7 +186,7 @@ function genJavaScriptFetch(params) {
|
|
|
793
186
|
].join("\n");
|
|
794
187
|
}
|
|
795
188
|
function genJavaScriptAxios(params) {
|
|
796
|
-
const url =
|
|
189
|
+
const url = baseUrl(params.actionHost);
|
|
797
190
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
798
191
|
return [
|
|
799
192
|
"// npm install axios",
|
|
@@ -814,7 +207,7 @@ function genJavaScriptAxios(params) {
|
|
|
814
207
|
].join("\n");
|
|
815
208
|
}
|
|
816
209
|
function genTypescriptFetch(params) {
|
|
817
|
-
const url =
|
|
210
|
+
const url = baseUrl(params.actionHost);
|
|
818
211
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
819
212
|
return [
|
|
820
213
|
"// Set XAPI_KEY env var or replace with your key",
|
|
@@ -831,7 +224,7 @@ function genTypescriptFetch(params) {
|
|
|
831
224
|
].join("\n");
|
|
832
225
|
}
|
|
833
226
|
function genGo(params) {
|
|
834
|
-
const url =
|
|
227
|
+
const url = baseUrl(params.actionHost);
|
|
835
228
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
836
229
|
const escaped = body.replace(/`/g, '` + "`" + `');
|
|
837
230
|
return [
|
|
@@ -1152,7 +545,8 @@ async function actionSearch2(args, flags) {
|
|
|
1152
545
|
category: flags.category,
|
|
1153
546
|
page: positiveIntegerFlag(flags.page, "--page"),
|
|
1154
547
|
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
1155
|
-
|
|
548
|
+
// Backward-compatible aliases from the original feature branch.
|
|
549
|
+
include_all_versions: ["include-all-versions", "all-versions", "include-history"].some((name) => flags[name] === "true"),
|
|
1156
550
|
sort: requestedSort
|
|
1157
551
|
});
|
|
1158
552
|
if (requestedSort && res.sort !== requestedSort) {
|
|
@@ -1339,7 +733,7 @@ __export(config_exports, {
|
|
|
1339
733
|
configSet: () => configSet,
|
|
1340
734
|
configShow: () => configShow
|
|
1341
735
|
});
|
|
1342
|
-
import { readFileSync
|
|
736
|
+
import { readFileSync } from "fs";
|
|
1343
737
|
var CONFIG_HELP = `xapi-to config - Manage CLI configuration
|
|
1344
738
|
|
|
1345
739
|
USAGE
|
|
@@ -1382,7 +776,7 @@ async function configSet(args, flags) {
|
|
|
1382
776
|
if (key !== "apiKey") err(`unknown config key: ${key} (only apiKey is configurable)`);
|
|
1383
777
|
let value = arg.slice(eq + 1);
|
|
1384
778
|
if (value === "-") {
|
|
1385
|
-
value =
|
|
779
|
+
value = readFileSync(0, "utf-8").trim();
|
|
1386
780
|
}
|
|
1387
781
|
if (!value) err("apiKey is empty");
|
|
1388
782
|
updates.apiKey = value;
|
|
@@ -2023,7 +1417,7 @@ function parsePositiveInt(raw, flagName) {
|
|
|
2023
1417
|
}
|
|
2024
1418
|
return n;
|
|
2025
1419
|
}
|
|
2026
|
-
function
|
|
1420
|
+
function sleep(ms) {
|
|
2027
1421
|
if (ms <= 0) return Promise.resolve();
|
|
2028
1422
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2029
1423
|
}
|
|
@@ -2120,13 +1514,891 @@ async function taskWait(args, flags) {
|
|
|
2120
1514
|
}
|
|
2121
1515
|
const desiredWaitMs = retryDelayMs ?? intervalMs;
|
|
2122
1516
|
const waitMs = deadline !== void 0 ? Math.min(desiredWaitMs, Math.max(0, deadline - Date.now())) : desiredWaitMs;
|
|
2123
|
-
await
|
|
1517
|
+
await sleep(waitMs);
|
|
2124
1518
|
}
|
|
2125
1519
|
}
|
|
2126
1520
|
function taskHelp() {
|
|
2127
1521
|
return TASK_HELP;
|
|
2128
1522
|
}
|
|
2129
1523
|
|
|
1524
|
+
// src/commands/sandbox.ts
|
|
1525
|
+
import { randomUUID } from "crypto";
|
|
1526
|
+
import { readFile, open, rm } from "fs/promises";
|
|
1527
|
+
import { resolve } from "path";
|
|
1528
|
+
var SANDBOX_HELP = `xapi-to sandbox - Managed, auditable cloud sandboxes
|
|
1529
|
+
|
|
1530
|
+
USAGE
|
|
1531
|
+
xapi-to sandbox <command> [args] [flags]
|
|
1532
|
+
|
|
1533
|
+
QUICK START
|
|
1534
|
+
sandbox run --command <shell> Quote, create, wait, execute, and terminate
|
|
1535
|
+
sandbox run -- <command...> Positional shorthand after a bare --
|
|
1536
|
+
|
|
1537
|
+
LIFECYCLE
|
|
1538
|
+
offerings List available provider offerings
|
|
1539
|
+
quote Quote requirements without creating
|
|
1540
|
+
list List sandbox instances
|
|
1541
|
+
history Search paginated instance history
|
|
1542
|
+
get <id> Get instance state, usage, and cost
|
|
1543
|
+
create Create from a quote, offering, or requirements
|
|
1544
|
+
wait <id> Wait for a state (default: RUNNING)
|
|
1545
|
+
exec <id> --command <shell> Execute a shell command
|
|
1546
|
+
suspend|resume|terminate <id> Change state and wait for completion
|
|
1547
|
+
|
|
1548
|
+
FILES, PORTS, AUDIT
|
|
1549
|
+
file write <id> <remote> --file <local>
|
|
1550
|
+
file write <id> <remote> --content <text>
|
|
1551
|
+
file read <id> <remote> [--output <local>]
|
|
1552
|
+
file list <id> [--path <remote>] [--depth N]
|
|
1553
|
+
port <id> <port> Get a public URL for a listening port
|
|
1554
|
+
extension <id> <extension-id> Invoke an offering-declared extension
|
|
1555
|
+
audit <id> [--kind operations|events|usageSegments|billingPeriods]
|
|
1556
|
+
|
|
1557
|
+
SELECTION FLAGS
|
|
1558
|
+
--provider auto|daytona|cf-edge|e2b|runpod|runloop|modal|vc-sandbox|fly|blaxel|cubesandbox
|
|
1559
|
+
Pin a provider gateway (default: auto)
|
|
1560
|
+
--capabilities exec,files,ports Required capabilities
|
|
1561
|
+
--cpu N --memory N --volume N Minimum resources
|
|
1562
|
+
--gpu-count N --gpu-model NAME GPU requirements
|
|
1563
|
+
--regions a,b Allowed regions
|
|
1564
|
+
--requirements <json> Complete requirements object
|
|
1565
|
+
--max-hourly-usd N Price ceiling (sandbox run default: 0.20)
|
|
1566
|
+
|
|
1567
|
+
COMMON FLAGS
|
|
1568
|
+
--host <sandbox.xapi.to> Override gateway; must be *.xapi.to/localhost
|
|
1569
|
+
--wait-timeout 5m State wait timeout (default: 5m)
|
|
1570
|
+
--interval 2s State polling interval (default: 2s)
|
|
1571
|
+
--format json|pretty|table Output format
|
|
1572
|
+
|
|
1573
|
+
HISTORY FLAGS
|
|
1574
|
+
--state ALL|ACTIVE|HISTORY|RUNNING|SUSPENDED|TERMINATED|FAILED
|
|
1575
|
+
--search <text> --from <ISO time> --to <ISO time>
|
|
1576
|
+
--page N --page-size N Pagination (page size: 1-100)
|
|
1577
|
+
|
|
1578
|
+
SAFETY
|
|
1579
|
+
sandbox run terminates in finally, including command failure. Use --keep only
|
|
1580
|
+
when you intentionally want billing to continue after the CLI exits.
|
|
1581
|
+
|
|
1582
|
+
EXAMPLES
|
|
1583
|
+
xapi-to sandbox offerings --format table
|
|
1584
|
+
xapi-to sandbox quote --capabilities exec,files --max-hourly-usd 0.20
|
|
1585
|
+
xapi-to sandbox run --command 'python3 -c "print(6*7)"'
|
|
1586
|
+
xapi-to sandbox run --provider cf-edge --capabilities exec,files,ports --command 'pwd'
|
|
1587
|
+
xapi-to sandbox exec <id> -- npm test
|
|
1588
|
+
xapi-to sandbox extension <id> runpod.connection_info --input '{}'
|
|
1589
|
+
xapi-to sandbox terminate <id>
|
|
1590
|
+
`;
|
|
1591
|
+
var SANDBOX_COMMAND_HELP = {
|
|
1592
|
+
offerings: `USAGE
|
|
1593
|
+
xapi-to sandbox offerings [--provider NAME] [--format json|pretty|table]
|
|
1594
|
+
|
|
1595
|
+
Lists current resources, capabilities, lifecycle support, and hourly prices.
|
|
1596
|
+
This command does not create or bill an instance.`,
|
|
1597
|
+
quote: `USAGE
|
|
1598
|
+
xapi-to sandbox quote [selection flags] [--max-hourly-usd N]
|
|
1599
|
+
|
|
1600
|
+
SELECTION
|
|
1601
|
+
--capabilities exec,files,ports Required capabilities
|
|
1602
|
+
--cpu N --memory N --volume N Minimum resources
|
|
1603
|
+
--gpu-count N --gpu-model NAME GPU requirements
|
|
1604
|
+
--regions a,b Allowed regions
|
|
1605
|
+
--requirements <json> Complete requirements object
|
|
1606
|
+
--max-hourly-usd N Hard hourly price ceiling
|
|
1607
|
+
|
|
1608
|
+
Returns a short-lived quote without creating or billing an instance.`,
|
|
1609
|
+
list: `USAGE
|
|
1610
|
+
xapi-to sandbox list [--provider NAME] [--format json|pretty|table]
|
|
1611
|
+
|
|
1612
|
+
Lists current Sandbox instances visible to the configured xAPI key.`,
|
|
1613
|
+
history: `USAGE
|
|
1614
|
+
xapi-to sandbox history [--state STATE] [--search TEXT] [--from ISO] [--to ISO]
|
|
1615
|
+
[--page N] [--page-size 1-100] [--format json|pretty|table]
|
|
1616
|
+
|
|
1617
|
+
Searches current and historical Sandbox instances with server-side pagination.`,
|
|
1618
|
+
get: `USAGE
|
|
1619
|
+
xapi-to sandbox get <id> [--format json|pretty|table]
|
|
1620
|
+
|
|
1621
|
+
Returns current state, operations, usage, billing, and service-calculated cost.`,
|
|
1622
|
+
create: `USAGE
|
|
1623
|
+
xapi-to sandbox create [selection flags] [--max-hourly-usd N] [--wait]
|
|
1624
|
+
|
|
1625
|
+
SELECTION MODES
|
|
1626
|
+
--quote-id ID Create from an existing quote
|
|
1627
|
+
--offering-id ID Create an exact offering (cannot use a price ceiling)
|
|
1628
|
+
--requirements <json> Create from requirements
|
|
1629
|
+
--capabilities/--cpu/--memory/... Requirements shortcuts
|
|
1630
|
+
|
|
1631
|
+
CONTROL
|
|
1632
|
+
--idempotency-key KEY Stable retry key; generated and returned if omitted
|
|
1633
|
+
--metadata <json> Instance metadata
|
|
1634
|
+
--resume-on-access Request automatic resume on supported providers
|
|
1635
|
+
--wait Wait until RUNNING
|
|
1636
|
+
--wait-timeout 5m --interval 2s Polling controls
|
|
1637
|
+
|
|
1638
|
+
On a wait failure, the error includes the instance ID and recovery instructions.`,
|
|
1639
|
+
wait: `USAGE
|
|
1640
|
+
xapi-to sandbox wait <id> [--state RUNNING[,STATE]]
|
|
1641
|
+
[--wait-timeout 5m] [--interval 2s]
|
|
1642
|
+
|
|
1643
|
+
State names are case-insensitive and validated before polling.`,
|
|
1644
|
+
exec: `USAGE
|
|
1645
|
+
xapi-to sandbox exec <id> --command <shell> [--cwd PATH] [--timeout SECONDS]
|
|
1646
|
+
[--background]
|
|
1647
|
+
xapi-to sandbox exec <id> -- <command...>
|
|
1648
|
+
|
|
1649
|
+
Executes a command and maps a remote non-zero exit code to the local process.
|
|
1650
|
+
--background requires offering.capabilities.backgroundExec=true and returns a
|
|
1651
|
+
provider-managed session immediately; use it for long-running Web servers.`,
|
|
1652
|
+
file: `USAGE
|
|
1653
|
+
xapi-to sandbox file write <id> <remote> (--file <local>|--content <text>)
|
|
1654
|
+
xapi-to sandbox file read <id> <remote> [--output <local>]
|
|
1655
|
+
xapi-to sandbox file list <id> [--path <remote>] [--depth N]
|
|
1656
|
+
|
|
1657
|
+
Binary local files are transferred as base64. --output never overwrites a file.`,
|
|
1658
|
+
port: `USAGE
|
|
1659
|
+
xapi-to sandbox port <id> <1-65535>
|
|
1660
|
+
|
|
1661
|
+
Returns the provider's temporary public URL for a listening instance port.`,
|
|
1662
|
+
extension: `USAGE
|
|
1663
|
+
xapi-to sandbox extension <id> <extension-id> [--input <json>]
|
|
1664
|
+
[--idempotency-key KEY]
|
|
1665
|
+
|
|
1666
|
+
Invoke only extension IDs declared by the selected offering.`,
|
|
1667
|
+
audit: `USAGE
|
|
1668
|
+
xapi-to sandbox audit <id> [--kind operations|events|usageSegments|billingPeriods]
|
|
1669
|
+
[--page N] [--page-size 1-100] [--format json|pretty|table]`,
|
|
1670
|
+
suspend: `USAGE
|
|
1671
|
+
xapi-to sandbox suspend <id> [--no-wait] [--idempotency-key KEY]
|
|
1672
|
+
[--wait-timeout 5m] [--interval 2s]
|
|
1673
|
+
|
|
1674
|
+
Check offering lifecycle support before suspending; storage may continue billing.`,
|
|
1675
|
+
resume: `USAGE
|
|
1676
|
+
xapi-to sandbox resume <id> [--no-wait] [--idempotency-key KEY]
|
|
1677
|
+
[--wait-timeout 5m] [--interval 2s]`,
|
|
1678
|
+
terminate: `USAGE
|
|
1679
|
+
xapi-to sandbox terminate <id> [--no-wait] [--idempotency-key KEY]
|
|
1680
|
+
[--wait-timeout 5m] [--interval 2s]
|
|
1681
|
+
|
|
1682
|
+
Waits for TERMINATED or FAILED by default.`,
|
|
1683
|
+
run: `USAGE
|
|
1684
|
+
xapi-to sandbox run --command <shell> [selection flags] [run flags]
|
|
1685
|
+
xapi-to sandbox run [selection flags] -- <command...>
|
|
1686
|
+
|
|
1687
|
+
RUN FLAGS
|
|
1688
|
+
--max-hourly-usd N Hard ceiling (default: 0.20)
|
|
1689
|
+
--timeout SECONDS Remote command timeout (default: 60)
|
|
1690
|
+
--cwd PATH Remote working directory
|
|
1691
|
+
--metadata <json> Instance metadata for audit correlation
|
|
1692
|
+
--idempotency-key KEY Stable create retry key
|
|
1693
|
+
--wait-timeout 5m --interval 2s Lifecycle polling controls
|
|
1694
|
+
--keep Keep the instance running and billing
|
|
1695
|
+
|
|
1696
|
+
Runs quote -> create -> wait -> exec -> terminate. Cleanup also runs after
|
|
1697
|
+
command failure, SIGINT, or SIGTERM.`
|
|
1698
|
+
};
|
|
1699
|
+
var COMMON_FLAGS = ["help", "host", "provider", "format"];
|
|
1700
|
+
var SELECTION_FLAGS = [
|
|
1701
|
+
"capabilities",
|
|
1702
|
+
"cpu",
|
|
1703
|
+
"memory",
|
|
1704
|
+
"volume",
|
|
1705
|
+
"gpu-count",
|
|
1706
|
+
"gpu-model",
|
|
1707
|
+
"regions",
|
|
1708
|
+
"requirements",
|
|
1709
|
+
"max-hourly-usd"
|
|
1710
|
+
];
|
|
1711
|
+
var POLL_FLAGS = ["wait-timeout", "interval"];
|
|
1712
|
+
function help(flags, command) {
|
|
1713
|
+
if (flags.help) {
|
|
1714
|
+
console.log(`xapi-to sandbox ${command}
|
|
1715
|
+
|
|
1716
|
+
${SANDBOX_COMMAND_HELP[command]}
|
|
1717
|
+
|
|
1718
|
+
COMMON
|
|
1719
|
+
--host HOST --provider NAME --format json|pretty|table --help`);
|
|
1720
|
+
process.exit(0);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
function validateFlags(flags, command, allowed = []) {
|
|
1724
|
+
const valid = /* @__PURE__ */ new Set([...COMMON_FLAGS, ...allowed]);
|
|
1725
|
+
const unknown = Object.keys(flags).filter((flag) => !valid.has(flag));
|
|
1726
|
+
if (unknown.length) {
|
|
1727
|
+
err(`unknown flag${unknown.length > 1 ? "s" : ""} for sandbox ${command}: ${unknown.map((flag) => `--${flag}`).join(", ")}`, {
|
|
1728
|
+
hint: `run xapi-to sandbox ${command} --help`,
|
|
1729
|
+
validFlags: [...valid].sort().map((flag) => `--${flag}`)
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
if (flags.format && !["json", "pretty", "table"].includes(flags.format)) {
|
|
1733
|
+
err("--format must be one of: json, pretty, table");
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
function collection(data) {
|
|
1737
|
+
if (Array.isArray(data)) return data;
|
|
1738
|
+
if (Array.isArray(data?.items)) return data.items;
|
|
1739
|
+
if (Array.isArray(data?.data)) return data.data;
|
|
1740
|
+
return data ? [data] : [];
|
|
1741
|
+
}
|
|
1742
|
+
function capability(value, name) {
|
|
1743
|
+
const enabled = value?.capabilities?.[name];
|
|
1744
|
+
return enabled === true ? "yes" : enabled === false ? "no" : "";
|
|
1745
|
+
}
|
|
1746
|
+
function sandboxTableRows(view, data) {
|
|
1747
|
+
if (view === "offerings") {
|
|
1748
|
+
return collection(data).map((item) => ({
|
|
1749
|
+
id: item.id,
|
|
1750
|
+
name: item.name,
|
|
1751
|
+
cpu: item.resources?.cpu,
|
|
1752
|
+
memoryGiB: item.resources?.memoryGiB,
|
|
1753
|
+
volumeGiB: item.resources?.volumeGiB,
|
|
1754
|
+
gpu: Array.isArray(item.resources?.gpu) ? item.resources.gpu.map((gpu) => `${gpu.count || 1}x ${gpu.model || "GPU"}`).join(", ") : "",
|
|
1755
|
+
exec: capability(item, "exec"),
|
|
1756
|
+
background: capability(item, "backgroundExec"),
|
|
1757
|
+
files: capability(item, "files"),
|
|
1758
|
+
ports: capability(item, "ports"),
|
|
1759
|
+
suspend: item.lifecycle?.suspension?.supported === true ? "yes" : "no",
|
|
1760
|
+
hourlyUsd: item.billing?.estimatedHourlyUsdByState?.RUNNING,
|
|
1761
|
+
extensions: Array.isArray(item.capabilities?.extensionIds) ? item.capabilities.extensionIds.join(",") : ""
|
|
1762
|
+
}));
|
|
1763
|
+
}
|
|
1764
|
+
if (view === "quote") {
|
|
1765
|
+
return collection(data).map((item) => ({
|
|
1766
|
+
quoteId: item.quoteId || item.id,
|
|
1767
|
+
offeringId: item.offeringId || item.offering?.id,
|
|
1768
|
+
offering: item.offering?.name || item.offeringName,
|
|
1769
|
+
provider: item.provider?.name || item.providerName || item.provider,
|
|
1770
|
+
hourlyUsd: item.estimatedHourlyUsd || item.hourlyUsd || item.offering?.billing?.estimatedHourlyUsdByState?.RUNNING,
|
|
1771
|
+
expiresAt: item.expiresAt
|
|
1772
|
+
}));
|
|
1773
|
+
}
|
|
1774
|
+
if (view === "run") {
|
|
1775
|
+
return collection(data).map((item) => ({
|
|
1776
|
+
instanceId: item.instanceId,
|
|
1777
|
+
provider: item.provider,
|
|
1778
|
+
offering: item.offering?.name || item.offering,
|
|
1779
|
+
exitCode: item.result?.exitCode,
|
|
1780
|
+
finalState: item.finalState,
|
|
1781
|
+
cleanup: item.cleanup?.state || (item.cleanup?.kept ? "KEPT" : ""),
|
|
1782
|
+
totalCost: item.totalCost
|
|
1783
|
+
}));
|
|
1784
|
+
}
|
|
1785
|
+
return collection(data).map((item) => ({
|
|
1786
|
+
id: item.id || item.instanceId,
|
|
1787
|
+
state: item.observedState || item.state || item.finalState,
|
|
1788
|
+
desiredState: item.desiredState,
|
|
1789
|
+
offering: item.offering?.name || item.offeringName || item.offeringId,
|
|
1790
|
+
provider: item.provider?.name || item.providerName || item.provider,
|
|
1791
|
+
createdAt: item.createdAt,
|
|
1792
|
+
updatedAt: item.updatedAt,
|
|
1793
|
+
totalCost: item.totalCost
|
|
1794
|
+
}));
|
|
1795
|
+
}
|
|
1796
|
+
function sandboxOutput(view, data, flags) {
|
|
1797
|
+
const format = flags.format || getFormat();
|
|
1798
|
+
if (format === "table") {
|
|
1799
|
+
output(sandboxTableRows(view, data), "table");
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
output(data, flags.format);
|
|
1803
|
+
}
|
|
1804
|
+
function flagValue(flags, name) {
|
|
1805
|
+
const value = flags[name];
|
|
1806
|
+
if (value === "true") err(`--${name} requires a value`);
|
|
1807
|
+
return value;
|
|
1808
|
+
}
|
|
1809
|
+
function booleanFlag(flags, name) {
|
|
1810
|
+
const raw = flags[name];
|
|
1811
|
+
if (raw === void 0 || raw === "false") return false;
|
|
1812
|
+
if (raw === "true") return true;
|
|
1813
|
+
err(`--${name} must be a boolean flag or --${name}=true|false`);
|
|
1814
|
+
}
|
|
1815
|
+
function positiveNumber(raw, name) {
|
|
1816
|
+
if (raw === void 0) return void 0;
|
|
1817
|
+
const value = Number(raw);
|
|
1818
|
+
if (!Number.isFinite(value) || value <= 0) err(`--${name} must be a positive number`);
|
|
1819
|
+
return value;
|
|
1820
|
+
}
|
|
1821
|
+
function positiveInteger(raw, name) {
|
|
1822
|
+
const value = positiveNumber(raw, name);
|
|
1823
|
+
if (value !== void 0 && !Number.isInteger(value)) err(`--${name} must be a positive integer`);
|
|
1824
|
+
return value;
|
|
1825
|
+
}
|
|
1826
|
+
function durationMs(raw, fallback, name) {
|
|
1827
|
+
if (raw === void 0) return fallback;
|
|
1828
|
+
if (raw === "true") err(`--${name} requires a value`);
|
|
1829
|
+
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h)?$/);
|
|
1830
|
+
if (!match || Number(match[1]) <= 0) err(`--${name} must be a duration like 500ms, 2s, 5m, or 1h`);
|
|
1831
|
+
const value = Number(match[1]);
|
|
1832
|
+
return value * { ms: 1, s: 1e3, m: 6e4, h: 36e5 }[match[2] || "ms"];
|
|
1833
|
+
}
|
|
1834
|
+
function jsonObject(raw, name) {
|
|
1835
|
+
try {
|
|
1836
|
+
const value = JSON.parse(raw);
|
|
1837
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected an object");
|
|
1838
|
+
return value;
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
err(`--${name} must be a valid JSON object`, error.message);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
function csv(raw) {
|
|
1844
|
+
if (!raw) return void 0;
|
|
1845
|
+
return raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
1846
|
+
}
|
|
1847
|
+
function sandboxOptions(flags) {
|
|
1848
|
+
const cfg = getConfig();
|
|
1849
|
+
requireApiKey(cfg);
|
|
1850
|
+
const host = flagValue(flags, "host") || cfg.sandboxHost || XAPI_SANDBOX_HOST;
|
|
1851
|
+
const provider = flagValue(flags, "provider");
|
|
1852
|
+
return { sandboxHost: host, apiKey: cfg.apiKey, ...provider ? { provider } : {} };
|
|
1853
|
+
}
|
|
1854
|
+
function requirementsFromFlags(flags, defaultCapabilities) {
|
|
1855
|
+
const requirements = flagValue(flags, "requirements") ? jsonObject(flagValue(flags, "requirements"), "requirements") : {};
|
|
1856
|
+
const capabilities = csv(flagValue(flags, "capabilities")) || (Array.isArray(requirements.capabilities) ? void 0 : defaultCapabilities);
|
|
1857
|
+
const regions = csv(flagValue(flags, "regions"));
|
|
1858
|
+
const cpu = positiveNumber(flagValue(flags, "cpu"), "cpu");
|
|
1859
|
+
const memory = positiveNumber(flagValue(flags, "memory"), "memory");
|
|
1860
|
+
const volume = positiveNumber(flagValue(flags, "volume"), "volume");
|
|
1861
|
+
const gpuCount = positiveInteger(flagValue(flags, "gpu-count"), "gpu-count");
|
|
1862
|
+
const gpuModel = flagValue(flags, "gpu-model");
|
|
1863
|
+
if (gpuModel && gpuCount === void 0 && !(Number(requirements.gpu?.count) > 0)) {
|
|
1864
|
+
err("--gpu-model requires --gpu-count (or requirements.gpu.count)");
|
|
1865
|
+
}
|
|
1866
|
+
if (capabilities?.length) requirements.capabilities = capabilities;
|
|
1867
|
+
if (regions?.length) requirements.regions = regions;
|
|
1868
|
+
if (cpu !== void 0) requirements.cpu = { ...requirements.cpu || {}, min: cpu };
|
|
1869
|
+
if (memory !== void 0) requirements.memoryGiB = { ...requirements.memoryGiB || {}, min: memory };
|
|
1870
|
+
if (volume !== void 0) requirements.volumeGiB = { ...requirements.volumeGiB || {}, min: volume };
|
|
1871
|
+
if (gpuCount !== void 0 || gpuModel) {
|
|
1872
|
+
requirements.gpu = {
|
|
1873
|
+
...requirements.gpu || {},
|
|
1874
|
+
...gpuCount !== void 0 ? { count: gpuCount } : {},
|
|
1875
|
+
...gpuModel ? { model: gpuModel } : {}
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
return requirements;
|
|
1879
|
+
}
|
|
1880
|
+
function quoteBody(flags, defaultCapabilities) {
|
|
1881
|
+
const max = positiveNumber(flagValue(flags, "max-hourly-usd"), "max-hourly-usd");
|
|
1882
|
+
return {
|
|
1883
|
+
requirements: requirementsFromFlags(flags, defaultCapabilities),
|
|
1884
|
+
...max !== void 0 ? { maxEstimatedHourlyUsd: max.toFixed(8) } : {}
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
function createDefaultCapabilities(flags) {
|
|
1888
|
+
if (flagValue(flags, "requirements") || flagValue(flags, "provider") === "runpod" || flagValue(flags, "gpu-count") || flagValue(flags, "gpu-model")) return void 0;
|
|
1889
|
+
return ["exec"];
|
|
1890
|
+
}
|
|
1891
|
+
function waitSettings(flags) {
|
|
1892
|
+
return {
|
|
1893
|
+
timeoutMs: durationMs(flags["wait-timeout"], 3e5, "wait-timeout"),
|
|
1894
|
+
intervalMs: durationMs(flags.interval, 2e3, "interval")
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function commandFrom(args, flags, usage) {
|
|
1898
|
+
const fromFlag = flagValue(flags, "command");
|
|
1899
|
+
const command = fromFlag ?? args.join(" ");
|
|
1900
|
+
if (!command.trim()) err(usage);
|
|
1901
|
+
return command;
|
|
1902
|
+
}
|
|
1903
|
+
function instanceId(args, usage) {
|
|
1904
|
+
if (!args[0]) err(usage);
|
|
1905
|
+
return args[0];
|
|
1906
|
+
}
|
|
1907
|
+
async function terminateAndWait(opts, id, flags) {
|
|
1908
|
+
const { timeoutMs, intervalMs } = waitSettings(flags);
|
|
1909
|
+
const deadline = Date.now() + timeoutMs;
|
|
1910
|
+
const clientIdempotencyKey = flagValue(flags, "idempotency-key") || `cli:terminate:${randomUUID()}`;
|
|
1911
|
+
let operation;
|
|
1912
|
+
while (Date.now() < deadline) {
|
|
1913
|
+
const detail2 = await sandboxGet(opts, id);
|
|
1914
|
+
if (["TERMINATED", "FAILED"].includes(String(detail2.observedState))) {
|
|
1915
|
+
return { operation, sandbox: detail2, clientIdempotencyKey };
|
|
1916
|
+
}
|
|
1917
|
+
try {
|
|
1918
|
+
operation = await sandboxStateAction(opts, id, "terminate", {
|
|
1919
|
+
idempotencyKey: clientIdempotencyKey
|
|
1920
|
+
});
|
|
1921
|
+
break;
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
if (!(error instanceof HttpError) || error.status !== 409) throw error;
|
|
1924
|
+
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
const remaining = Math.max(1, deadline - Date.now());
|
|
1928
|
+
let detail;
|
|
1929
|
+
try {
|
|
1930
|
+
detail = await sandboxWait(opts, id, ["TERMINATED", "FAILED"], remaining, intervalMs);
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
if (!(error instanceof HttpError) || error.status !== 404 || !opts.provider) throw error;
|
|
1933
|
+
detail = await sandboxWait(
|
|
1934
|
+
{ ...opts, provider: void 0 },
|
|
1935
|
+
id,
|
|
1936
|
+
["TERMINATED", "FAILED"],
|
|
1937
|
+
Math.max(1, deadline - Date.now()),
|
|
1938
|
+
intervalMs
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
return { operation, sandbox: detail, clientIdempotencyKey };
|
|
1942
|
+
}
|
|
1943
|
+
function cleanupSummary(cleanup) {
|
|
1944
|
+
if (!cleanup) return void 0;
|
|
1945
|
+
if (cleanup.error) return { error: cleanup.error };
|
|
1946
|
+
return {
|
|
1947
|
+
operationId: cleanup.operation?.id,
|
|
1948
|
+
operationStatus: cleanup.sandbox?.operations?.find?.((item) => item.type === "TERMINATE")?.status || cleanup.operation?.status,
|
|
1949
|
+
state: cleanup.sandbox?.observedState,
|
|
1950
|
+
totalCost: cleanup.sandbox?.totalCost
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
function sandboxResultExitCode(result) {
|
|
1954
|
+
const value = result?.exitCode;
|
|
1955
|
+
if (typeof value !== "number" || value === 0) return void 0;
|
|
1956
|
+
return Math.min(255, Math.max(1, Math.trunc(value)));
|
|
1957
|
+
}
|
|
1958
|
+
async function sandboxOfferings2(args, flags) {
|
|
1959
|
+
help(flags, "offerings");
|
|
1960
|
+
validateFlags(flags, "offerings");
|
|
1961
|
+
try {
|
|
1962
|
+
sandboxOutput("offerings", await sandboxOfferings(sandboxOptions(flags)), flags);
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
err("sandbox offerings failed", error.message);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
async function sandboxQuote2(args, flags) {
|
|
1968
|
+
help(flags, "quote");
|
|
1969
|
+
validateFlags(flags, "quote", SELECTION_FLAGS);
|
|
1970
|
+
try {
|
|
1971
|
+
sandboxOutput("quote", await sandboxQuote(sandboxOptions(flags), quoteBody(flags)), flags);
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
err("sandbox quote failed", error.message);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
async function sandboxList2(args, flags) {
|
|
1977
|
+
help(flags, "list");
|
|
1978
|
+
validateFlags(flags, "list");
|
|
1979
|
+
try {
|
|
1980
|
+
sandboxOutput("instances", await sandboxList(sandboxOptions(flags)), flags);
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
err("sandbox list failed", error.message);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
async function sandboxHistory2(args, flags) {
|
|
1986
|
+
help(flags, "history");
|
|
1987
|
+
validateFlags(flags, "history", ["state", "search", "from", "to", "page", "page-size"]);
|
|
1988
|
+
const state = flagValue(flags, "state");
|
|
1989
|
+
const allowedStates = ["ALL", "ACTIVE", "HISTORY", "PROVISIONING", "RUNNING", "SUSPENDED", "TERMINATED", "FAILED", "UNKNOWN"];
|
|
1990
|
+
if (state && !allowedStates.includes(state.toUpperCase())) {
|
|
1991
|
+
err(`--state must be one of: ${allowedStates.join(", ")}`);
|
|
1992
|
+
}
|
|
1993
|
+
const page = positiveInteger(flagValue(flags, "page"), "page") || 1;
|
|
1994
|
+
const pageSize = positiveInteger(flagValue(flags, "page-size"), "page-size") || 100;
|
|
1995
|
+
if (pageSize > 100) err("--page-size must be at most 100");
|
|
1996
|
+
try {
|
|
1997
|
+
sandboxOutput("instances", await sandboxHistory(sandboxOptions(flags), {
|
|
1998
|
+
...state ? { state: state.toUpperCase() } : {},
|
|
1999
|
+
...flagValue(flags, "search") ? { search: flagValue(flags, "search") } : {},
|
|
2000
|
+
...flagValue(flags, "from") ? { from: flagValue(flags, "from") } : {},
|
|
2001
|
+
...flagValue(flags, "to") ? { to: flagValue(flags, "to") } : {},
|
|
2002
|
+
page,
|
|
2003
|
+
pageSize
|
|
2004
|
+
}), flags);
|
|
2005
|
+
} catch (error) {
|
|
2006
|
+
err("sandbox history failed", error.message);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
async function sandboxGet2(args, flags) {
|
|
2010
|
+
help(flags, "get");
|
|
2011
|
+
validateFlags(flags, "get");
|
|
2012
|
+
const id = instanceId(args, "usage: xapi-to sandbox get <id>");
|
|
2013
|
+
try {
|
|
2014
|
+
sandboxOutput("detail", await sandboxGet(sandboxOptions(flags), id), flags);
|
|
2015
|
+
} catch (error) {
|
|
2016
|
+
err("sandbox get failed", error.message);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
async function sandboxCreate2(args, flags) {
|
|
2020
|
+
help(flags, "create");
|
|
2021
|
+
validateFlags(flags, "create", [
|
|
2022
|
+
...SELECTION_FLAGS,
|
|
2023
|
+
...POLL_FLAGS,
|
|
2024
|
+
"quote-id",
|
|
2025
|
+
"offering-id",
|
|
2026
|
+
"metadata",
|
|
2027
|
+
"idempotency-key",
|
|
2028
|
+
"resume-on-access",
|
|
2029
|
+
"wait"
|
|
2030
|
+
]);
|
|
2031
|
+
const wait = booleanFlag(flags, "wait");
|
|
2032
|
+
const resumeOnAccess = booleanFlag(flags, "resume-on-access");
|
|
2033
|
+
const opts = sandboxOptions(flags);
|
|
2034
|
+
const quoteId = flagValue(flags, "quote-id");
|
|
2035
|
+
const offeringId = flagValue(flags, "offering-id");
|
|
2036
|
+
const maxHourly = flagValue(flags, "max-hourly-usd");
|
|
2037
|
+
const requirementFlags = [
|
|
2038
|
+
"requirements",
|
|
2039
|
+
"capabilities",
|
|
2040
|
+
"cpu",
|
|
2041
|
+
"memory",
|
|
2042
|
+
"volume",
|
|
2043
|
+
"gpu-count",
|
|
2044
|
+
"gpu-model",
|
|
2045
|
+
"regions"
|
|
2046
|
+
].filter((name) => flagValue(flags, name) !== void 0);
|
|
2047
|
+
if (quoteId && offeringId) err("--quote-id and --offering-id are mutually exclusive");
|
|
2048
|
+
if ((quoteId || offeringId) && requirementFlags.length) {
|
|
2049
|
+
err(`${quoteId ? "--quote-id" : "--offering-id"} cannot be combined with requirement flags`, {
|
|
2050
|
+
conflictingFlags: requirementFlags.map((name) => `--${name}`)
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
if (quoteId && maxHourly) {
|
|
2054
|
+
err("--max-hourly-usd cannot be combined with --quote-id; the quote already fixes the price");
|
|
2055
|
+
}
|
|
2056
|
+
if (offeringId && maxHourly) {
|
|
2057
|
+
err("--max-hourly-usd cannot be combined with --offering-id; create from requirements to enforce a price ceiling");
|
|
2058
|
+
}
|
|
2059
|
+
const idempotencyKey = flagValue(flags, "idempotency-key") || `cli:create:${randomUUID()}`;
|
|
2060
|
+
let created;
|
|
2061
|
+
try {
|
|
2062
|
+
let selection;
|
|
2063
|
+
if (quoteId) selection = { quoteId };
|
|
2064
|
+
else if (offeringId) selection = { offeringId };
|
|
2065
|
+
else if (maxHourly) {
|
|
2066
|
+
const quoted = await sandboxQuote(opts, quoteBody(flags, createDefaultCapabilities(flags)));
|
|
2067
|
+
if (!quoted?.quoteId) throw new Error("quote response did not include quoteId");
|
|
2068
|
+
selection = { quoteId: quoted.quoteId };
|
|
2069
|
+
} else selection = { requirements: requirementsFromFlags(flags, createDefaultCapabilities(flags)) };
|
|
2070
|
+
const metadata = flagValue(flags, "metadata") ? jsonObject(flagValue(flags, "metadata"), "metadata") : { client: "xapi-cli" };
|
|
2071
|
+
created = await sandboxCreate(opts, {
|
|
2072
|
+
selection,
|
|
2073
|
+
metadata,
|
|
2074
|
+
idempotencyKey,
|
|
2075
|
+
policy: { resumeOnAccess }
|
|
2076
|
+
});
|
|
2077
|
+
let result = created;
|
|
2078
|
+
if (wait && created.id) {
|
|
2079
|
+
const settings = waitSettings(flags);
|
|
2080
|
+
result = await sandboxWait(opts, created.id, ["RUNNING"], settings.timeoutMs, settings.intervalMs);
|
|
2081
|
+
}
|
|
2082
|
+
sandboxOutput("detail", { ...result, clientIdempotencyKey: idempotencyKey }, flags);
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
let latest = created;
|
|
2085
|
+
if (created?.id) {
|
|
2086
|
+
try {
|
|
2087
|
+
latest = await sandboxGet(opts, created.id);
|
|
2088
|
+
} catch {
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
err("sandbox create failed", {
|
|
2092
|
+
message: error.message,
|
|
2093
|
+
instanceId: created?.id,
|
|
2094
|
+
observedState: latest?.observedState,
|
|
2095
|
+
clientIdempotencyKey: idempotencyKey,
|
|
2096
|
+
recovery: created?.id ? {
|
|
2097
|
+
inspect: `xapi-to sandbox get ${created.id}`,
|
|
2098
|
+
terminate: `xapi-to sandbox terminate ${created.id}`
|
|
2099
|
+
} : {
|
|
2100
|
+
retry: `repeat the create command with --idempotency-key ${idempotencyKey}`,
|
|
2101
|
+
reconcile: "xapi-to sandbox history --state ACTIVE --page-size 100"
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
async function sandboxWait2(args, flags) {
|
|
2107
|
+
help(flags, "wait");
|
|
2108
|
+
validateFlags(flags, "wait", ["state", ...POLL_FLAGS]);
|
|
2109
|
+
const id = instanceId(args, "usage: xapi-to sandbox wait <id> [--state RUNNING]");
|
|
2110
|
+
const allowedStates = ["PROVISIONING", "RUNNING", "SUSPENDING", "SUSPENDED", "RESUMING", "TERMINATING", "TERMINATED", "FAILED", "UNKNOWN"];
|
|
2111
|
+
const wanted = (csv(flagValue(flags, "state")) || ["RUNNING"]).map((state) => state.toUpperCase());
|
|
2112
|
+
const invalid = wanted.filter((state) => !allowedStates.includes(state));
|
|
2113
|
+
if (invalid.length) err(`--state must contain only: ${allowedStates.join(", ")}`);
|
|
2114
|
+
const settings = waitSettings(flags);
|
|
2115
|
+
try {
|
|
2116
|
+
output(await sandboxWait(sandboxOptions(flags), id, wanted, settings.timeoutMs, settings.intervalMs), flags.format);
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
err("sandbox wait failed", error.message);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
async function sandboxExec2(args, flags) {
|
|
2122
|
+
help(flags, "exec");
|
|
2123
|
+
validateFlags(flags, "exec", ["command", "timeout", "cwd", "background"]);
|
|
2124
|
+
const id = instanceId(args, "usage: xapi-to sandbox exec <id> --command <shell>");
|
|
2125
|
+
const command = commandFrom(args.slice(1), flags, "usage: xapi-to sandbox exec <id> --command <shell>");
|
|
2126
|
+
const timeoutSeconds = positiveInteger(flagValue(flags, "timeout"), "timeout") || 60;
|
|
2127
|
+
const background = booleanFlag(flags, "background");
|
|
2128
|
+
try {
|
|
2129
|
+
const result = await sandboxExec(sandboxOptions(flags), id, {
|
|
2130
|
+
command,
|
|
2131
|
+
timeoutSeconds,
|
|
2132
|
+
...flagValue(flags, "cwd") ? { cwd: flagValue(flags, "cwd") } : {},
|
|
2133
|
+
...background ? { background: true } : {}
|
|
2134
|
+
});
|
|
2135
|
+
output(result, flags.format);
|
|
2136
|
+
const exitCode = sandboxResultExitCode(result);
|
|
2137
|
+
if (exitCode !== void 0) process.exitCode = exitCode;
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
err("sandbox exec failed", error.message);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
async function sandboxFile(args, flags) {
|
|
2143
|
+
help(flags, "file");
|
|
2144
|
+
validateFlags(flags, "file", ["file", "content", "output", "path", "depth"]);
|
|
2145
|
+
const [action, id, remote] = args;
|
|
2146
|
+
if (!action || !id) err("usage: xapi-to sandbox file <write|read|list> <id> [remote-path]");
|
|
2147
|
+
const opts = sandboxOptions(flags);
|
|
2148
|
+
try {
|
|
2149
|
+
if (action === "write") {
|
|
2150
|
+
if (!remote) err("usage: xapi-to sandbox file write <id> <remote-path> (--file <local>|--content <text>)");
|
|
2151
|
+
const local = flagValue(flags, "file");
|
|
2152
|
+
const inline = flagValue(flags, "content");
|
|
2153
|
+
if (Boolean(local) === Boolean(inline)) err("provide exactly one of --file or --content");
|
|
2154
|
+
const body = local ? { path: remote, content: (await readFile(local)).toString("base64"), encoding: "base64" } : { path: remote, content: inline, encoding: "utf8" };
|
|
2155
|
+
output(await sandboxFileWrite(opts, id, body), flags.format);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
if (action === "read") {
|
|
2159
|
+
if (!remote) err("usage: xapi-to sandbox file read <id> <remote-path> [--output <local>]");
|
|
2160
|
+
const outputPath = flagValue(flags, "output");
|
|
2161
|
+
const result = await sandboxFileRead(opts, id, remote, outputPath ? "base64" : "utf8");
|
|
2162
|
+
if (!outputPath) {
|
|
2163
|
+
output(result, flags.format);
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
const target = resolve(outputPath);
|
|
2167
|
+
const file = await open(target, "wx");
|
|
2168
|
+
let complete = false;
|
|
2169
|
+
try {
|
|
2170
|
+
const data = result?.encoding === "base64" ? Buffer.from(String(result.content || ""), "base64") : Buffer.from(String(result?.content || ""), "utf8");
|
|
2171
|
+
await file.writeFile(data);
|
|
2172
|
+
complete = true;
|
|
2173
|
+
output({ output: target, bytes: data.length, path: remote }, flags.format);
|
|
2174
|
+
} finally {
|
|
2175
|
+
await file.close();
|
|
2176
|
+
if (!complete) await rm(target, { force: true });
|
|
2177
|
+
}
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (action === "list") {
|
|
2181
|
+
const depth = positiveInteger(flagValue(flags, "depth"), "depth") || 2;
|
|
2182
|
+
output(await sandboxFileList(opts, id, flagValue(flags, "path") || remote || ".", depth), flags.format);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
err(`unknown sandbox file command: ${action}`);
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
err(`sandbox file ${action} failed`, error.message);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
async function sandboxPort2(args, flags) {
|
|
2191
|
+
help(flags, "port");
|
|
2192
|
+
validateFlags(flags, "port");
|
|
2193
|
+
const id = instanceId(args, "usage: xapi-to sandbox port <id> <port>");
|
|
2194
|
+
const port = positiveInteger(args[1], "port");
|
|
2195
|
+
if (!port || port > 65535) err("port must be between 1 and 65535");
|
|
2196
|
+
try {
|
|
2197
|
+
output(await sandboxPort(sandboxOptions(flags), id, port), flags.format);
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
err("sandbox port failed", error.message);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
async function sandboxExtension2(args, flags) {
|
|
2203
|
+
help(flags, "extension");
|
|
2204
|
+
validateFlags(flags, "extension", ["input", "idempotency-key"]);
|
|
2205
|
+
const id = instanceId(args, "usage: xapi-to sandbox extension <id> <extension-id> --input <json>");
|
|
2206
|
+
const extensionId = args[1];
|
|
2207
|
+
if (!extensionId) err("usage: xapi-to sandbox extension <id> <extension-id> --input <json>");
|
|
2208
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,119}$/i.test(extensionId)) err("invalid Sandbox extension id");
|
|
2209
|
+
const input = flagValue(flags, "input") ? jsonObject(flagValue(flags, "input"), "input") : {};
|
|
2210
|
+
const clientIdempotencyKey = flagValue(flags, "idempotency-key") || `cli:extension:${extensionId}:${randomUUID()}`;
|
|
2211
|
+
try {
|
|
2212
|
+
const result = await sandboxExtension(sandboxOptions(flags), id, extensionId, {
|
|
2213
|
+
input,
|
|
2214
|
+
idempotencyKey: clientIdempotencyKey
|
|
2215
|
+
});
|
|
2216
|
+
output({ ...result, clientIdempotencyKey }, flags.format);
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
err("sandbox extension failed", {
|
|
2219
|
+
message: error.message,
|
|
2220
|
+
clientIdempotencyKey,
|
|
2221
|
+
retry: `repeat with --idempotency-key ${clientIdempotencyKey}`
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
async function sandboxAudit2(args, flags) {
|
|
2226
|
+
help(flags, "audit");
|
|
2227
|
+
validateFlags(flags, "audit", ["kind", "page", "page-size"]);
|
|
2228
|
+
const id = instanceId(args, "usage: xapi-to sandbox audit <id> [--kind operations]");
|
|
2229
|
+
const kind = flagValue(flags, "kind") || "operations";
|
|
2230
|
+
const allowed = ["operations", "events", "usageSegments", "billingPeriods"];
|
|
2231
|
+
if (!allowed.includes(kind)) err(`--kind must be one of: ${allowed.join(", ")}`);
|
|
2232
|
+
const page = positiveInteger(flagValue(flags, "page"), "page") || 1;
|
|
2233
|
+
const pageSize = positiveInteger(flagValue(flags, "page-size"), "page-size") || 100;
|
|
2234
|
+
if (pageSize > 100) err("--page-size must be at most 100");
|
|
2235
|
+
try {
|
|
2236
|
+
output(await sandboxAudit(sandboxOptions(flags), id, kind, page, pageSize), flags.format);
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
err("sandbox audit failed", error.message);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
async function sandboxState(action, args, flags) {
|
|
2242
|
+
help(flags, action);
|
|
2243
|
+
validateFlags(flags, action, [...POLL_FLAGS, "no-wait", "idempotency-key"]);
|
|
2244
|
+
const id = instanceId(args, `usage: xapi-to sandbox ${action} <id>`);
|
|
2245
|
+
const opts = sandboxOptions(flags);
|
|
2246
|
+
const noWait = booleanFlag(flags, "no-wait");
|
|
2247
|
+
const clientIdempotencyKey = flagValue(flags, "idempotency-key") || `cli:${action}:${randomUUID()}`;
|
|
2248
|
+
try {
|
|
2249
|
+
if (action === "terminate" && !noWait) {
|
|
2250
|
+
output(await terminateAndWait(opts, id, flags), flags.format);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
const operation = await sandboxStateAction(opts, id, action, {
|
|
2254
|
+
idempotencyKey: clientIdempotencyKey
|
|
2255
|
+
});
|
|
2256
|
+
if (noWait) {
|
|
2257
|
+
output({ ...operation, clientIdempotencyKey }, flags.format);
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
const wanted = action === "suspend" ? ["SUSPENDED"] : action === "resume" ? ["RUNNING"] : ["TERMINATED", "FAILED"];
|
|
2261
|
+
const settings = waitSettings(flags);
|
|
2262
|
+
const detail = await sandboxWait(opts, id, wanted, settings.timeoutMs, settings.intervalMs);
|
|
2263
|
+
output({ operation, sandbox: detail, clientIdempotencyKey }, flags.format);
|
|
2264
|
+
} catch (error) {
|
|
2265
|
+
err(`sandbox ${action} failed`, {
|
|
2266
|
+
message: error.message,
|
|
2267
|
+
instanceId: id,
|
|
2268
|
+
clientIdempotencyKey,
|
|
2269
|
+
recovery: {
|
|
2270
|
+
inspect: `xapi-to sandbox get ${id}`,
|
|
2271
|
+
retry: `repeat with --idempotency-key ${clientIdempotencyKey}`
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async function sandboxRun(args, flags) {
|
|
2277
|
+
help(flags, "run");
|
|
2278
|
+
validateFlags(flags, "run", [
|
|
2279
|
+
...SELECTION_FLAGS,
|
|
2280
|
+
...POLL_FLAGS,
|
|
2281
|
+
"command",
|
|
2282
|
+
"timeout",
|
|
2283
|
+
"cwd",
|
|
2284
|
+
"metadata",
|
|
2285
|
+
"keep",
|
|
2286
|
+
"idempotency-key"
|
|
2287
|
+
]);
|
|
2288
|
+
const command = commandFrom(args, flags, "usage: xapi-to sandbox run --command <shell>");
|
|
2289
|
+
const opts = sandboxOptions(flags);
|
|
2290
|
+
const maxHourly = flagValue(flags, "max-hourly-usd") || "0.20";
|
|
2291
|
+
positiveNumber(maxHourly, "max-hourly-usd");
|
|
2292
|
+
const timeoutSeconds = positiveInteger(flagValue(flags, "timeout"), "timeout") || 60;
|
|
2293
|
+
const settings = waitSettings(flags);
|
|
2294
|
+
const idempotencyKey = flagValue(flags, "idempotency-key") || `cli:run:${randomUUID()}`;
|
|
2295
|
+
const metadata = flagValue(flags, "metadata") ? jsonObject(flagValue(flags, "metadata"), "metadata") : {};
|
|
2296
|
+
const keep = booleanFlag(flags, "keep");
|
|
2297
|
+
let id;
|
|
2298
|
+
let failure;
|
|
2299
|
+
let quote;
|
|
2300
|
+
let created;
|
|
2301
|
+
let ready;
|
|
2302
|
+
let result;
|
|
2303
|
+
let cleanup;
|
|
2304
|
+
let interruptedBy;
|
|
2305
|
+
const waitAbort = new AbortController();
|
|
2306
|
+
const interrupt = (signal) => {
|
|
2307
|
+
interruptedBy = signal;
|
|
2308
|
+
waitAbort.abort();
|
|
2309
|
+
};
|
|
2310
|
+
const interruptSigint = () => interrupt("SIGINT");
|
|
2311
|
+
const interruptSigterm = () => interrupt("SIGTERM");
|
|
2312
|
+
const throwIfInterrupted = () => {
|
|
2313
|
+
if (interruptedBy) throw new Error(`interrupted by ${interruptedBy}`);
|
|
2314
|
+
};
|
|
2315
|
+
process.once("SIGINT", interruptSigint);
|
|
2316
|
+
process.once("SIGTERM", interruptSigterm);
|
|
2317
|
+
try {
|
|
2318
|
+
quote = await sandboxQuote(
|
|
2319
|
+
opts,
|
|
2320
|
+
quoteBody({ ...flags, "max-hourly-usd": maxHourly }, ["exec"]),
|
|
2321
|
+
waitAbort.signal
|
|
2322
|
+
);
|
|
2323
|
+
if (!quote?.quoteId) throw new Error("quote response did not include quoteId");
|
|
2324
|
+
throwIfInterrupted();
|
|
2325
|
+
created = await sandboxCreate(opts, {
|
|
2326
|
+
selection: { quoteId: quote.quoteId },
|
|
2327
|
+
metadata: { ...metadata, client: "xapi-cli", command: "sandbox run" },
|
|
2328
|
+
policy: { resumeOnAccess: false },
|
|
2329
|
+
idempotencyKey
|
|
2330
|
+
});
|
|
2331
|
+
id = created?.id;
|
|
2332
|
+
if (!id) throw new Error("create response did not include sandbox id");
|
|
2333
|
+
throwIfInterrupted();
|
|
2334
|
+
ready = await sandboxWait(
|
|
2335
|
+
opts,
|
|
2336
|
+
id,
|
|
2337
|
+
["RUNNING"],
|
|
2338
|
+
settings.timeoutMs,
|
|
2339
|
+
settings.intervalMs,
|
|
2340
|
+
waitAbort.signal
|
|
2341
|
+
);
|
|
2342
|
+
throwIfInterrupted();
|
|
2343
|
+
result = await sandboxExec(opts, id, {
|
|
2344
|
+
command,
|
|
2345
|
+
timeoutSeconds,
|
|
2346
|
+
...flagValue(flags, "cwd") ? { cwd: flagValue(flags, "cwd") } : {}
|
|
2347
|
+
}, waitAbort.signal);
|
|
2348
|
+
throwIfInterrupted();
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
failure = error;
|
|
2351
|
+
} finally {
|
|
2352
|
+
if (id && !keep) {
|
|
2353
|
+
try {
|
|
2354
|
+
cleanup = await terminateAndWait(opts, id, flags);
|
|
2355
|
+
} catch (cleanupError) {
|
|
2356
|
+
cleanup = { error: cleanupError.message };
|
|
2357
|
+
if (!failure) failure = new Error(`command completed but cleanup failed: ${cleanupError.message}`);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
process.removeListener("SIGINT", interruptSigint);
|
|
2361
|
+
process.removeListener("SIGTERM", interruptSigterm);
|
|
2362
|
+
}
|
|
2363
|
+
if (failure) {
|
|
2364
|
+
err("sandbox run failed", {
|
|
2365
|
+
message: failure?.message || String(failure),
|
|
2366
|
+
instanceId: id,
|
|
2367
|
+
clientIdempotencyKey: idempotencyKey,
|
|
2368
|
+
cleanup: keep ? { kept: true, warning: "billing continues until terminated" } : cleanupSummary(cleanup),
|
|
2369
|
+
recovery: id ? { inspect: `xapi-to sandbox get ${id}`, terminate: `xapi-to sandbox terminate ${id}` } : {
|
|
2370
|
+
reconcile: "xapi-to sandbox history --state ACTIVE --page-size 100",
|
|
2371
|
+
retryCreateWithSameKey: idempotencyKey
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
let finalDetail;
|
|
2376
|
+
let finalReadError;
|
|
2377
|
+
if (id) {
|
|
2378
|
+
try {
|
|
2379
|
+
finalDetail = await sandboxGet(opts, id);
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
finalReadError = error.message;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
const summary = {
|
|
2385
|
+
instanceId: id,
|
|
2386
|
+
clientIdempotencyKey: idempotencyKey,
|
|
2387
|
+
provider: opts.provider || "auto",
|
|
2388
|
+
offering: quote?.offering,
|
|
2389
|
+
createdState: created?.observedState,
|
|
2390
|
+
readyState: ready?.observedState,
|
|
2391
|
+
result,
|
|
2392
|
+
cleanup: keep ? { kept: true, warning: "billing continues until terminated" } : cleanupSummary(cleanup),
|
|
2393
|
+
finalState: finalDetail?.observedState || cleanup?.sandbox?.observedState,
|
|
2394
|
+
totalCost: finalDetail?.totalCost || cleanup?.sandbox?.totalCost,
|
|
2395
|
+
...finalReadError ? { finalReadError } : {}
|
|
2396
|
+
};
|
|
2397
|
+
sandboxOutput("run", summary, flags);
|
|
2398
|
+
const remoteExitCode = sandboxResultExitCode(result);
|
|
2399
|
+
if (remoteExitCode !== void 0) process.exitCode = remoteExitCode;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2130
2402
|
// src/args.ts
|
|
2131
2403
|
function parseArgs(argv) {
|
|
2132
2404
|
const positional = [];
|
|
@@ -2178,7 +2450,7 @@ COMMANDS
|
|
|
2178
2450
|
--page N --page-size N Pagination
|
|
2179
2451
|
--category <name> Filter by category
|
|
2180
2452
|
--service-id <id> Filter by service
|
|
2181
|
-
search <query> Search actions by keyword
|
|
2453
|
+
search <query> Search actions by keyword (--all-versions: \u542B\u975E\u9ED8\u8BA4\u4F46\u4ECD\u5728\u8DD1\u7684\u5927\u7248\u672C)
|
|
2182
2454
|
--source capability|api Filter by source type
|
|
2183
2455
|
--category <name> Filter by category
|
|
2184
2456
|
--page N --page-size N Pagination
|
|
@@ -2205,6 +2477,12 @@ COMMANDS
|
|
|
2205
2477
|
--timeout <duration> Max wait duration, e.g. 10m
|
|
2206
2478
|
--max-attempts <number> Max poll attempts
|
|
2207
2479
|
|
|
2480
|
+
sandbox <command> Managed cloud sandbox lifecycle
|
|
2481
|
+
run --command <shell> Quote, create, execute, and auto-terminate
|
|
2482
|
+
offerings|quote|list|history|get|create|wait|exec
|
|
2483
|
+
file|port|extension|audit|suspend|resume|terminate
|
|
2484
|
+
Run "xapi-to sandbox --help" for selection and safety flags
|
|
2485
|
+
|
|
2208
2486
|
oauth bind [--provider twitter] Bind Twitter OAuth to your API key
|
|
2209
2487
|
oauth status List current OAuth bindings
|
|
2210
2488
|
oauth unbind <binding-id> Remove an OAuth binding
|
|
@@ -2231,6 +2509,7 @@ ENV VARS
|
|
|
2231
2509
|
XAPI_API_KEY Compatible API key alias
|
|
2232
2510
|
XAPI_ACTION_HOST Action service host (default: action.xapi.to)
|
|
2233
2511
|
XAPI_API_HOST Auth/account service host (default: api.xapi.to)
|
|
2512
|
+
XAPI_SANDBOX_HOST Sandbox gateway host (default: sandbox.xapi.to)
|
|
2234
2513
|
XAPI_OUTPUT Default output format
|
|
2235
2514
|
XAPI_TRANSFER_IDLE_TIMEOUT_MS SSE/download idle timeout (default: 60000)
|
|
2236
2515
|
|
|
@@ -2250,6 +2529,7 @@ EXAMPLES
|
|
|
2250
2529
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
|
|
2251
2530
|
xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
|
|
2252
2531
|
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 2s --timeout 10m
|
|
2532
|
+
xapi-to sandbox run --command 'python3 -c "print(6*7)"'
|
|
2253
2533
|
xapi-to categories
|
|
2254
2534
|
xapi-to services --format table
|
|
2255
2535
|
xapi-to config set apiKey=xapi_abc123
|
|
@@ -2305,6 +2585,54 @@ async function main() {
|
|
|
2305
2585
|
}
|
|
2306
2586
|
break;
|
|
2307
2587
|
}
|
|
2588
|
+
case "sandbox": {
|
|
2589
|
+
if (rest.length === 0) {
|
|
2590
|
+
console.log(SANDBOX_HELP);
|
|
2591
|
+
process.exit(0);
|
|
2592
|
+
}
|
|
2593
|
+
const [subCmd, ...subRest] = rest;
|
|
2594
|
+
switch (subCmd) {
|
|
2595
|
+
case "offerings":
|
|
2596
|
+
return sandboxOfferings2(subRest, flags);
|
|
2597
|
+
case "quote":
|
|
2598
|
+
return sandboxQuote2(subRest, flags);
|
|
2599
|
+
case "list":
|
|
2600
|
+
return sandboxList2(subRest, flags);
|
|
2601
|
+
case "history":
|
|
2602
|
+
return sandboxHistory2(subRest, flags);
|
|
2603
|
+
case "get":
|
|
2604
|
+
return sandboxGet2(subRest, flags);
|
|
2605
|
+
case "create":
|
|
2606
|
+
return sandboxCreate2(subRest, flags);
|
|
2607
|
+
case "wait":
|
|
2608
|
+
return sandboxWait2(subRest, flags);
|
|
2609
|
+
case "exec":
|
|
2610
|
+
return sandboxExec2(subRest, flags);
|
|
2611
|
+
case "file":
|
|
2612
|
+
return sandboxFile(subRest, flags);
|
|
2613
|
+
case "port":
|
|
2614
|
+
return sandboxPort2(subRest, flags);
|
|
2615
|
+
case "extension":
|
|
2616
|
+
return sandboxExtension2(subRest, flags);
|
|
2617
|
+
case "audit":
|
|
2618
|
+
return sandboxAudit2(subRest, flags);
|
|
2619
|
+
case "suspend":
|
|
2620
|
+
return sandboxState("suspend", subRest, flags);
|
|
2621
|
+
case "resume":
|
|
2622
|
+
return sandboxState("resume", subRest, flags);
|
|
2623
|
+
case "terminate":
|
|
2624
|
+
return sandboxState("terminate", subRest, flags);
|
|
2625
|
+
case "run":
|
|
2626
|
+
return sandboxRun(subRest, flags);
|
|
2627
|
+
default:
|
|
2628
|
+
console.error(JSON.stringify({
|
|
2629
|
+
error: `unknown sandbox command: ${subCmd}`,
|
|
2630
|
+
hint: "run xapi-to sandbox --help"
|
|
2631
|
+
}));
|
|
2632
|
+
process.exit(1);
|
|
2633
|
+
}
|
|
2634
|
+
break;
|
|
2635
|
+
}
|
|
2308
2636
|
// ── OAuth commands ──
|
|
2309
2637
|
case "oauth": {
|
|
2310
2638
|
if (flags.help || rest.length === 0) {
|