xapi-to 0.1.18 → 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/LICENSE +21 -0
- package/README.md +258 -10
- package/dist/chunk-TYY6JR6O.js +870 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1256 -590
- 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 +33 -4
- package/scripts/openai-sandbox-agent-e2e.ts +219 -0
- package/scripts/sandbox-playground-e2e.mjs +463 -0
- package/skills/xapi/SKILL.md +498 -0
- package/skills/xapi/guides/ai.md +200 -0
- package/skills/xapi/guides/ai_gateway.md +263 -0
- package/skills/xapi/guides/crypto.md +197 -0
- package/skills/xapi/guides/douyin.md +297 -0
- package/skills/xapi/guides/google_search.md +194 -0
- package/skills/xapi/guides/linkedin.md +253 -0
- package/skills/xapi/guides/reddit.md +312 -0
- package/skills/xapi/guides/sandbox.md +466 -0
- package/skills/xapi/guides/serper.md +124 -0
- package/skills/xapi/guides/sms.md +186 -0
- package/skills/xapi/guides/tiktok.md +322 -0
- package/skills/xapi/guides/twitter.md +276 -0
- package/skills/xapi/guides/weibo.md +301 -0
- package/skills/xapi/guides/ws_gateway.md +206 -0
- package/skills/xapi/guides/xiaohongshu.md +315 -0
- package/skills/xapi/scripts/download_tweet_videos.sh +125 -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,549 +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
|
-
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
147
|
-
} catch {
|
|
148
|
-
return {};
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
function getConfig() {
|
|
152
|
-
const file = loadFileConfig();
|
|
153
|
-
return {
|
|
154
|
-
actionHost: XAPI_ACTION_HOST,
|
|
155
|
-
apiKey: process.env.XAPI_KEY || process.env.XAPI_API_KEY || file.apiKey
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
function requireApiKey(cfg) {
|
|
159
|
-
if (!cfg.apiKey) {
|
|
160
|
-
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.');
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
function saveConfig(updates) {
|
|
164
|
-
const current = loadFileConfig();
|
|
165
|
-
const merged = { ...current, ...updates };
|
|
166
|
-
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
167
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
168
|
-
}
|
|
169
|
-
function showConfig() {
|
|
170
|
-
const cfg = getConfig();
|
|
171
|
-
const file = loadFileConfig();
|
|
172
|
-
console.log(JSON.stringify({
|
|
173
|
-
actionHost: cfg.actionHost,
|
|
174
|
-
apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : void 0,
|
|
175
|
-
source: {
|
|
176
|
-
apiKey: process.env.XAPI_KEY || process.env.XAPI_API_KEY ? "env" : file.apiKey ? "file" : "none"
|
|
177
|
-
},
|
|
178
|
-
configFile: CONFIG_FILE
|
|
179
|
-
}, null, 2));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// src/client.ts
|
|
183
|
-
import { open, rm } from "fs/promises";
|
|
184
|
-
import { resolve } from "path";
|
|
185
|
-
import { Readable, Transform } from "stream";
|
|
186
|
-
import { pipeline } from "stream/promises";
|
|
187
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
188
|
-
var EXECUTE_TIMEOUT_MS = 6e4;
|
|
189
|
-
var IDEMPOTENT_RETRIES = 2;
|
|
190
|
-
var RETRY_BASE_DELAY_MS = 500;
|
|
191
|
-
var RETRY_MAX_DELAY_MS = 8e3;
|
|
192
|
-
var HttpError = class extends Error {
|
|
193
|
-
constructor(status, detail, retryAfterMs) {
|
|
194
|
-
super(`HTTP ${status}: ${detail}`);
|
|
195
|
-
this.status = status;
|
|
196
|
-
this.retryAfterMs = retryAfterMs;
|
|
197
|
-
this.name = "HttpError";
|
|
198
|
-
}
|
|
199
|
-
status;
|
|
200
|
-
retryAfterMs;
|
|
201
|
-
};
|
|
202
|
-
var RequestTimeoutError = class extends Error {
|
|
203
|
-
constructor(timeoutMs) {
|
|
204
|
-
super(`request timed out after ${timeoutMs}ms`);
|
|
205
|
-
this.timeoutMs = timeoutMs;
|
|
206
|
-
this.name = "RequestTimeoutError";
|
|
207
|
-
}
|
|
208
|
-
timeoutMs;
|
|
209
|
-
};
|
|
210
|
-
function isRetryableStatus(status) {
|
|
211
|
-
return status === 408 || status === 429 || status === 502 || status === 503 || status === 504;
|
|
212
|
-
}
|
|
213
|
-
function isRetryableNetworkError(e) {
|
|
214
|
-
if (!(e instanceof Error)) return false;
|
|
215
|
-
if (e instanceof HttpError || e instanceof RequestTimeoutError) return false;
|
|
216
|
-
if (e.name === "AbortError") return false;
|
|
217
|
-
return e instanceof TypeError || /network|fetch failed|econn|etimedout|eai_again|socket|dns/i.test(e.message);
|
|
218
|
-
}
|
|
219
|
-
function isRetryableRequestError(e) {
|
|
220
|
-
if (e instanceof HttpError) return isRetryableStatus(e.status);
|
|
221
|
-
if (e instanceof RequestTimeoutError) return true;
|
|
222
|
-
return isRetryableNetworkError(e);
|
|
223
|
-
}
|
|
224
|
-
function retryBaseDelayMs() {
|
|
225
|
-
const override = Number(process.env.XAPI_RETRY_BASE_MS);
|
|
226
|
-
return Number.isFinite(override) && override > 0 ? override : RETRY_BASE_DELAY_MS;
|
|
227
|
-
}
|
|
228
|
-
function backoffDelayMs(attempt, retryAfterMs) {
|
|
229
|
-
if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
|
|
230
|
-
return Math.min(retryAfterMs, RETRY_MAX_DELAY_MS);
|
|
231
|
-
}
|
|
232
|
-
const capped = Math.min(retryBaseDelayMs() * 2 ** attempt, RETRY_MAX_DELAY_MS);
|
|
233
|
-
return capped / 2 + Math.random() * (capped / 2);
|
|
234
|
-
}
|
|
235
|
-
function parseRetryAfterMs(res) {
|
|
236
|
-
const header = res.headers.get("retry-after");
|
|
237
|
-
if (!header) return void 0;
|
|
238
|
-
const seconds = Number(header);
|
|
239
|
-
if (Number.isFinite(seconds)) return seconds * 1e3;
|
|
240
|
-
const at = Date.parse(header);
|
|
241
|
-
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
|
|
242
|
-
}
|
|
243
|
-
function sleep(ms) {
|
|
244
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
245
|
-
}
|
|
246
|
-
async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
|
|
247
|
-
assertAllowedHost(url);
|
|
248
|
-
let attempt = 0;
|
|
249
|
-
while (true) {
|
|
250
|
-
const controller = new AbortController();
|
|
251
|
-
let timedOut = false;
|
|
252
|
-
const timer = setTimeout(() => {
|
|
253
|
-
timedOut = true;
|
|
254
|
-
controller.abort();
|
|
255
|
-
}, timeoutMs);
|
|
256
|
-
try {
|
|
257
|
-
const res = await fetch(url, { ...options, redirect: "manual", signal: controller.signal });
|
|
258
|
-
if (res.status >= 300 && res.status < 400) {
|
|
259
|
-
throw new Error(
|
|
260
|
-
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
if (!res.ok) {
|
|
264
|
-
const retryAfterMs = isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0;
|
|
265
|
-
if (isRetryableStatus(res.status) && attempt < retries) {
|
|
266
|
-
await res.text().catch(() => "");
|
|
267
|
-
clearTimeout(timer);
|
|
268
|
-
await sleep(backoffDelayMs(attempt, retryAfterMs));
|
|
269
|
-
attempt++;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const text2 = await res.text();
|
|
273
|
-
throw new HttpError(res.status, text2.slice(0, 300), retryAfterMs);
|
|
274
|
-
}
|
|
275
|
-
if (res.status === 204) {
|
|
276
|
-
return void 0;
|
|
277
|
-
}
|
|
278
|
-
const text = await res.text();
|
|
279
|
-
if (!text.trim()) {
|
|
280
|
-
return void 0;
|
|
281
|
-
}
|
|
282
|
-
const body = JSON.parse(text);
|
|
283
|
-
if (body && typeof body === "object" && "success" in body && body.success === false) {
|
|
284
|
-
const data = body.data;
|
|
285
|
-
if (data?.statusCode === 401 || data?.error === "Unauthorized") {
|
|
286
|
-
throw new Error(
|
|
287
|
-
"Authentication failed: " + (data.message || "Invalid or missing API key") + '. Run "npx xapi-to config set apiKey=<key>" to update your key.'
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
if (data?.error === "OAuth Required" || data?.statusCode === 403 && data?.message?.includes("OAuth")) {
|
|
291
|
-
throw new Error(
|
|
292
|
-
(data.message || "OAuth authorization required") + '. Run "xapi-to oauth bind" to connect your account.'
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return body;
|
|
297
|
-
} catch (e) {
|
|
298
|
-
if (timedOut) {
|
|
299
|
-
throw new RequestTimeoutError(timeoutMs);
|
|
300
|
-
}
|
|
301
|
-
if (isRetryableNetworkError(e) && attempt < retries) {
|
|
302
|
-
clearTimeout(timer);
|
|
303
|
-
await sleep(backoffDelayMs(attempt));
|
|
304
|
-
attempt++;
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
throw e;
|
|
308
|
-
} finally {
|
|
309
|
-
clearTimeout(timer);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
function headers(apiKey) {
|
|
314
|
-
const h = { "Content-Type": "application/json" };
|
|
315
|
-
if (apiKey) h["XAPI-Key"] = apiKey;
|
|
316
|
-
return h;
|
|
317
|
-
}
|
|
318
|
-
function baseUrl(opts) {
|
|
319
|
-
return `${scheme(opts.actionHost)}://${opts.actionHost}`;
|
|
320
|
-
}
|
|
321
|
-
async function actionList(opts, params = {}) {
|
|
322
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions`);
|
|
323
|
-
if (params.page) url.searchParams.set("page", String(params.page));
|
|
324
|
-
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
325
|
-
if (params.category) url.searchParams.set("category", params.category);
|
|
326
|
-
if (params.source) url.searchParams.set("source", params.source);
|
|
327
|
-
if (params.service_id) url.searchParams.set("service_id", params.service_id);
|
|
328
|
-
return request(
|
|
329
|
-
url.toString(),
|
|
330
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
331
|
-
DEFAULT_TIMEOUT_MS,
|
|
332
|
-
IDEMPOTENT_RETRIES
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
async function actionSearch(query, opts, params = {}) {
|
|
336
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions/search`);
|
|
337
|
-
url.searchParams.set("q", query);
|
|
338
|
-
if (params.category) url.searchParams.set("category", params.category);
|
|
339
|
-
if (params.source) url.searchParams.set("source", params.source);
|
|
340
|
-
if (params.page) url.searchParams.set("page", String(params.page));
|
|
341
|
-
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
342
|
-
return request(
|
|
343
|
-
url.toString(),
|
|
344
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
345
|
-
DEFAULT_TIMEOUT_MS,
|
|
346
|
-
IDEMPOTENT_RETRIES
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
async function actionCategories(opts, params = {}) {
|
|
350
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions/categories`);
|
|
351
|
-
if (params.source) url.searchParams.set("source", params.source);
|
|
352
|
-
return request(
|
|
353
|
-
url.toString(),
|
|
354
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
355
|
-
DEFAULT_TIMEOUT_MS,
|
|
356
|
-
IDEMPOTENT_RETRIES
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
async function actionGet(id, opts) {
|
|
360
|
-
return request(
|
|
361
|
-
`${baseUrl(opts)}/v1/actions/${encodeURIComponent(id)}`,
|
|
362
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
363
|
-
DEFAULT_TIMEOUT_MS,
|
|
364
|
-
IDEMPOTENT_RETRIES
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeoutMs = EXECUTE_TIMEOUT_MS) {
|
|
368
|
-
return request(
|
|
369
|
-
`${baseUrl(opts)}/v1/actions/execute`,
|
|
370
|
-
{
|
|
371
|
-
method: "POST",
|
|
372
|
-
headers: headers(opts.apiKey),
|
|
373
|
-
body: JSON.stringify({ action_id: actionId, ...httpMethod ? { method: httpMethod } : {}, input })
|
|
374
|
-
},
|
|
375
|
-
Math.min(timeoutMs, EXECUTE_TIMEOUT_MS),
|
|
376
|
-
retries
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
async function actionDownload(actionId, input, opts, outputPath, httpMethod) {
|
|
380
|
-
const controller = new AbortController();
|
|
381
|
-
let timedOut = false;
|
|
382
|
-
const timer = setTimeout(() => {
|
|
383
|
-
timedOut = true;
|
|
384
|
-
controller.abort();
|
|
385
|
-
}, EXECUTE_TIMEOUT_MS);
|
|
386
|
-
const target = resolve(outputPath);
|
|
387
|
-
let file;
|
|
388
|
-
let complete = false;
|
|
389
|
-
try {
|
|
390
|
-
try {
|
|
391
|
-
file = await open(target, "wx");
|
|
392
|
-
} catch (error) {
|
|
393
|
-
if (error?.code === "EEXIST") {
|
|
394
|
-
throw new Error(`Output file already exists: ${target}`);
|
|
395
|
-
}
|
|
396
|
-
throw error;
|
|
397
|
-
}
|
|
398
|
-
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
399
|
-
assertAllowedHost(url);
|
|
400
|
-
const res = await fetch(url, {
|
|
401
|
-
method: "POST",
|
|
402
|
-
headers: headers(opts.apiKey),
|
|
403
|
-
body: JSON.stringify({
|
|
404
|
-
action_id: actionId,
|
|
405
|
-
...httpMethod ? { method: httpMethod } : {},
|
|
406
|
-
input,
|
|
407
|
-
response_mode: "raw"
|
|
408
|
-
}),
|
|
409
|
-
redirect: "manual",
|
|
410
|
-
signal: controller.signal
|
|
411
|
-
});
|
|
412
|
-
if (res.status >= 300 && res.status < 400) {
|
|
413
|
-
throw new Error(
|
|
414
|
-
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
if (!res.ok) {
|
|
418
|
-
const text = await res.text();
|
|
419
|
-
throw new HttpError(
|
|
420
|
-
res.status,
|
|
421
|
-
text.slice(0, 300),
|
|
422
|
-
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
let bytes = 0;
|
|
426
|
-
if (res.body) {
|
|
427
|
-
const source = Readable.fromWeb(res.body);
|
|
428
|
-
const counter = new Transform({
|
|
429
|
-
transform(chunk, _encoding, callback) {
|
|
430
|
-
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
431
|
-
callback(null, chunk);
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
await pipeline(source, counter, file.createWriteStream());
|
|
435
|
-
} else {
|
|
436
|
-
await file.close();
|
|
437
|
-
}
|
|
438
|
-
complete = true;
|
|
439
|
-
return {
|
|
440
|
-
output: target,
|
|
441
|
-
bytes,
|
|
442
|
-
contentType: res.headers.get("content-type") || void 0,
|
|
443
|
-
contentDisposition: res.headers.get("content-disposition") || void 0,
|
|
444
|
-
status: res.status
|
|
445
|
-
};
|
|
446
|
-
} catch (error) {
|
|
447
|
-
if (timedOut) throw new RequestTimeoutError(EXECUTE_TIMEOUT_MS);
|
|
448
|
-
throw error;
|
|
449
|
-
} finally {
|
|
450
|
-
clearTimeout(timer);
|
|
451
|
-
if (!complete && file) {
|
|
452
|
-
await file.close().catch(() => void 0);
|
|
453
|
-
await rm(target, { force: true }).catch(() => void 0);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
async function actionServices(opts, params = {}) {
|
|
458
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions/services`);
|
|
459
|
-
if (params.page) url.searchParams.set("page", String(params.page));
|
|
460
|
-
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
461
|
-
if (params.category) url.searchParams.set("category", params.category);
|
|
462
|
-
return request(
|
|
463
|
-
url.toString(),
|
|
464
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
465
|
-
DEFAULT_TIMEOUT_MS,
|
|
466
|
-
IDEMPOTENT_RETRIES
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
async function healthCheck(opts) {
|
|
470
|
-
return request(
|
|
471
|
-
`${baseUrl(opts)}/health`,
|
|
472
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
473
|
-
5e3,
|
|
474
|
-
0
|
|
475
|
-
// health is a quick connectivity probe — fail fast, don't retry
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
async function loginWithApiKey(apiKey, apiHost) {
|
|
479
|
-
return request(
|
|
480
|
-
`${scheme(apiHost)}://${apiHost}/api/auth/login/apikey`,
|
|
481
|
-
{
|
|
482
|
-
method: "POST",
|
|
483
|
-
headers: { "Content-Type": "application/json" },
|
|
484
|
-
body: JSON.stringify({ apiKey })
|
|
485
|
-
},
|
|
486
|
-
DEFAULT_TIMEOUT_MS,
|
|
487
|
-
IDEMPOTENT_RETRIES
|
|
488
|
-
// auth exchange has no side effect — safe to retry
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
function jwtHeaders(jwtToken) {
|
|
492
|
-
return { "Content-Type": "application/json", Authorization: `Bearer ${jwtToken}` };
|
|
493
|
-
}
|
|
494
|
-
async function listKeys(jwtToken, apiHost) {
|
|
495
|
-
return request(
|
|
496
|
-
`${scheme(apiHost)}://${apiHost}/api/keys`,
|
|
497
|
-
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
498
|
-
DEFAULT_TIMEOUT_MS,
|
|
499
|
-
IDEMPOTENT_RETRIES
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
async function enableOAuthForKey(keyId, plaintextKey, jwtToken, apiHost) {
|
|
503
|
-
return request(
|
|
504
|
-
`${scheme(apiHost)}://${apiHost}/api/keys/${keyId}/enable-oauth`,
|
|
505
|
-
{
|
|
506
|
-
method: "POST",
|
|
507
|
-
headers: jwtHeaders(jwtToken),
|
|
508
|
-
body: JSON.stringify({ plaintextKey })
|
|
509
|
-
}
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
async function listOAuthProviders(apiHost) {
|
|
513
|
-
return request(
|
|
514
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/providers`,
|
|
515
|
-
{ method: "GET", headers: { "Content-Type": "application/json" } },
|
|
516
|
-
DEFAULT_TIMEOUT_MS,
|
|
517
|
-
IDEMPOTENT_RETRIES
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost, scopes) {
|
|
521
|
-
const body = { apiKeyId, providerId };
|
|
522
|
-
if (scopes) body.scopes = scopes;
|
|
523
|
-
return request(
|
|
524
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/authorize`,
|
|
525
|
-
{
|
|
526
|
-
method: "POST",
|
|
527
|
-
headers: jwtHeaders(jwtToken),
|
|
528
|
-
body: JSON.stringify(body)
|
|
529
|
-
}
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
async function listOAuthBindings(jwtToken, apiHost) {
|
|
533
|
-
return request(
|
|
534
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
|
|
535
|
-
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
536
|
-
DEFAULT_TIMEOUT_MS,
|
|
537
|
-
IDEMPOTENT_RETRIES
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
async function deleteOAuthBinding(bindingId, jwtToken, apiHost) {
|
|
541
|
-
const result = await request(
|
|
542
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/bindings/${bindingId}`,
|
|
543
|
-
{ method: "DELETE", headers: jwtHeaders(jwtToken) }
|
|
544
|
-
);
|
|
545
|
-
return result ?? { success: true };
|
|
546
|
-
}
|
|
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";
|
|
547
52
|
|
|
548
53
|
// src/codegen.ts
|
|
549
54
|
var TARGET_MAP = {
|
|
@@ -618,7 +123,7 @@ function validateHost(host) {
|
|
|
618
123
|
throw new Error(`invalid actionHost: "${host}" \u2014 must be a valid hostname with optional port`);
|
|
619
124
|
}
|
|
620
125
|
}
|
|
621
|
-
function
|
|
126
|
+
function baseUrl(actionHost) {
|
|
622
127
|
validateHost(actionHost);
|
|
623
128
|
return `${scheme(actionHost)}://${actionHost}/v1/actions/execute`;
|
|
624
129
|
}
|
|
@@ -634,7 +139,7 @@ function shellEscape(s) {
|
|
|
634
139
|
return s.replace(/'/g, "'\\''");
|
|
635
140
|
}
|
|
636
141
|
function genCurl(params) {
|
|
637
|
-
const url =
|
|
142
|
+
const url = baseUrl(params.actionHost);
|
|
638
143
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
639
144
|
return [
|
|
640
145
|
"# Set XAPI_KEY env var or replace with your key",
|
|
@@ -645,7 +150,7 @@ function genCurl(params) {
|
|
|
645
150
|
].join("\n");
|
|
646
151
|
}
|
|
647
152
|
function genPython(lib, params) {
|
|
648
|
-
const url =
|
|
153
|
+
const url = baseUrl(params.actionHost);
|
|
649
154
|
const payload = { action_id: params.actionId, ...params.method ? { method: params.method } : {}, input: params.input };
|
|
650
155
|
return [
|
|
651
156
|
`# pip install ${lib}`,
|
|
@@ -665,7 +170,7 @@ function genPython(lib, params) {
|
|
|
665
170
|
].join("\n");
|
|
666
171
|
}
|
|
667
172
|
function genJavaScriptFetch(params) {
|
|
668
|
-
const url =
|
|
173
|
+
const url = baseUrl(params.actionHost);
|
|
669
174
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
670
175
|
return [
|
|
671
176
|
"// Set XAPI_KEY env var or replace with your key",
|
|
@@ -681,7 +186,7 @@ function genJavaScriptFetch(params) {
|
|
|
681
186
|
].join("\n");
|
|
682
187
|
}
|
|
683
188
|
function genJavaScriptAxios(params) {
|
|
684
|
-
const url =
|
|
189
|
+
const url = baseUrl(params.actionHost);
|
|
685
190
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
686
191
|
return [
|
|
687
192
|
"// npm install axios",
|
|
@@ -702,7 +207,7 @@ function genJavaScriptAxios(params) {
|
|
|
702
207
|
].join("\n");
|
|
703
208
|
}
|
|
704
209
|
function genTypescriptFetch(params) {
|
|
705
|
-
const url =
|
|
210
|
+
const url = baseUrl(params.actionHost);
|
|
706
211
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
707
212
|
return [
|
|
708
213
|
"// Set XAPI_KEY env var or replace with your key",
|
|
@@ -719,7 +224,7 @@ function genTypescriptFetch(params) {
|
|
|
719
224
|
].join("\n");
|
|
720
225
|
}
|
|
721
226
|
function genGo(params) {
|
|
722
|
-
const url =
|
|
227
|
+
const url = baseUrl(params.actionHost);
|
|
723
228
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
724
229
|
const escaped = body.replace(/`/g, '` + "`" + `');
|
|
725
230
|
return [
|
|
@@ -801,6 +306,7 @@ function generateCode(target, params) {
|
|
|
801
306
|
|
|
802
307
|
// src/commands/action.ts
|
|
803
308
|
var VALID_SOURCES = ["capability", "api"];
|
|
309
|
+
var VALID_SEARCH_SORTS = ["default", "relevance", "price"];
|
|
804
310
|
var LIST_HELP = `xapi-to list - List all actions
|
|
805
311
|
|
|
806
312
|
USAGE
|
|
@@ -829,12 +335,50 @@ FLAGS
|
|
|
829
335
|
--category <name> Filter by category
|
|
830
336
|
--page N Page number (default: 1)
|
|
831
337
|
--page-size N Results per page
|
|
338
|
+
--sort default|relevance|price
|
|
339
|
+
Recommended (default), strongest match, or lowest
|
|
340
|
+
comparable price after exact-id/local-match guards
|
|
341
|
+
--include-all-versions Include active non-default major versions
|
|
832
342
|
--format json|pretty|table Output format
|
|
833
343
|
|
|
834
344
|
EXAMPLES
|
|
835
345
|
xapi-to search twitter
|
|
836
346
|
xapi-to search "tweet detail" --source api
|
|
347
|
+
xapi-to search "tweet detail" --sort relevance
|
|
348
|
+
xapi-to search weather --sort price
|
|
837
349
|
xapi-to search weather --category utility --format table
|
|
350
|
+
xapi-to search twitter --include-all-versions
|
|
351
|
+
`;
|
|
352
|
+
var CATEGORIES_HELP = `xapi-to categories - List action categories
|
|
353
|
+
|
|
354
|
+
USAGE
|
|
355
|
+
xapi-to categories [flags]
|
|
356
|
+
|
|
357
|
+
FLAGS
|
|
358
|
+
--source capability|api Filter by source type
|
|
359
|
+
--format json|pretty|table Output format
|
|
360
|
+
`;
|
|
361
|
+
var SERVICES_HELP = `xapi-to services - List services
|
|
362
|
+
|
|
363
|
+
USAGE
|
|
364
|
+
xapi-to services [flags]
|
|
365
|
+
|
|
366
|
+
FLAGS
|
|
367
|
+
--category <name> Filter by category
|
|
368
|
+
--page N Page number
|
|
369
|
+
--page-size N Results per page
|
|
370
|
+
--format json|pretty|table Output format
|
|
371
|
+
`;
|
|
372
|
+
var GET_BATCH_HELP = `xapi-to get-batch - Get multiple action schemas
|
|
373
|
+
|
|
374
|
+
USAGE
|
|
375
|
+
xapi-to get-batch <id> [id ...] [flags]
|
|
376
|
+
|
|
377
|
+
FLAGS
|
|
378
|
+
--format json|pretty|table Output format
|
|
379
|
+
|
|
380
|
+
EXAMPLES
|
|
381
|
+
xapi-to get-batch twitter.tweet_detail crypto.token.price
|
|
838
382
|
`;
|
|
839
383
|
var GET_HELP = `xapi-to get - Get action schema
|
|
840
384
|
|
|
@@ -878,6 +422,7 @@ FLAGS
|
|
|
878
422
|
--input <json> Input payload as JSON (required for execution)
|
|
879
423
|
--method GET|POST|... Override HTTP method
|
|
880
424
|
--output <path> Save a raw binary response to a new file
|
|
425
|
+
--stream Forward the action's HTTP SSE response unchanged
|
|
881
426
|
--code <target> Generate code snippet instead of executing
|
|
882
427
|
--format json|pretty|table Output format
|
|
883
428
|
|
|
@@ -901,6 +446,7 @@ CODE TARGETS
|
|
|
901
446
|
EXAMPLES
|
|
902
447
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
903
448
|
xapi-to call openrouter.audio_speech --input '{"body":{"input":"Hello"}}' --output speech.mp3
|
|
449
|
+
xapi-to call ai.text.chat.fast --input '{"messages":[{"role":"user","content":"Hello"}]}' --stream
|
|
904
450
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
|
|
905
451
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
|
|
906
452
|
`;
|
|
@@ -931,6 +477,31 @@ function getSource(flags) {
|
|
|
931
477
|
}
|
|
932
478
|
return flags.source;
|
|
933
479
|
}
|
|
480
|
+
function getSearchSort(flags) {
|
|
481
|
+
const value = flags.sort;
|
|
482
|
+
if (value === void 0) return void 0;
|
|
483
|
+
if (value === "true") {
|
|
484
|
+
err("--sort requires a value: default, relevance, or price");
|
|
485
|
+
}
|
|
486
|
+
if (!VALID_SEARCH_SORTS.includes(value)) {
|
|
487
|
+
err(`invalid --sort value: "${value}". Must be default, relevance, or price.`);
|
|
488
|
+
}
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
function positiveIntegerFlag(value, name) {
|
|
492
|
+
if (value === void 0) return void 0;
|
|
493
|
+
if (!/^\d+$/.test(value) || Number(value) < 1) {
|
|
494
|
+
err(`${name} must be a positive integer`);
|
|
495
|
+
}
|
|
496
|
+
return Number(value);
|
|
497
|
+
}
|
|
498
|
+
function httpMethodFlag(value) {
|
|
499
|
+
if (value === void 0) return void 0;
|
|
500
|
+
if (value === "true" || !/^[A-Za-z]+$/.test(value)) {
|
|
501
|
+
err("--method requires an HTTP method, e.g. --method POST");
|
|
502
|
+
}
|
|
503
|
+
return value.toUpperCase();
|
|
504
|
+
}
|
|
934
505
|
async function actionList2(args, flags) {
|
|
935
506
|
showHelpIfRequested(flags, LIST_HELP);
|
|
936
507
|
const cfg = getConfig();
|
|
@@ -938,8 +509,8 @@ async function actionList2(args, flags) {
|
|
|
938
509
|
try {
|
|
939
510
|
const res = await actionList(cfg, {
|
|
940
511
|
source: getSource(flags),
|
|
941
|
-
page: flags.page
|
|
942
|
-
page_size: flags["page-size"]
|
|
512
|
+
page: positiveIntegerFlag(flags.page, "--page"),
|
|
513
|
+
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
943
514
|
category: flags.category,
|
|
944
515
|
service_id: flags["service-id"]
|
|
945
516
|
});
|
|
@@ -965,15 +536,24 @@ async function actionSearch2(args, flags) {
|
|
|
965
536
|
showHelpIfRequested(flags, SEARCH_HELP);
|
|
966
537
|
const query = args[0];
|
|
967
538
|
if (!query) err("usage: xapi-to search <query>");
|
|
539
|
+
const requestedSort = getSearchSort(flags);
|
|
968
540
|
const cfg = getConfig();
|
|
969
541
|
const fmt = flags.format || getFormat();
|
|
970
542
|
try {
|
|
971
543
|
const res = await actionSearch(query, cfg, {
|
|
972
544
|
source: getSource(flags),
|
|
973
545
|
category: flags.category,
|
|
974
|
-
page: flags.page
|
|
975
|
-
page_size: flags["page-size"]
|
|
546
|
+
page: positiveIntegerFlag(flags.page, "--page"),
|
|
547
|
+
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
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"),
|
|
550
|
+
sort: requestedSort
|
|
976
551
|
});
|
|
552
|
+
if (requestedSort && res.sort !== requestedSort) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
res.sort ? `backend applied sort "${res.sort}" instead of requested "${requestedSort}"` : "backend does not support search sorting yet; deploy the updated backend before using --sort"
|
|
555
|
+
);
|
|
556
|
+
}
|
|
977
557
|
const results = res.results || [];
|
|
978
558
|
if (fmt === "table") {
|
|
979
559
|
output(results.map((a) => ({
|
|
@@ -983,7 +563,8 @@ async function actionSearch2(args, flags) {
|
|
|
983
563
|
source: a.source ?? "",
|
|
984
564
|
category: a.meta?.category ?? "",
|
|
985
565
|
status: a.status ?? "",
|
|
986
|
-
|
|
566
|
+
price: a.meta?.pricing?.comparable ? a.meta.pricing.listed_price : "",
|
|
567
|
+
pricing: a.meta?.pricing?.billing_type ?? ""
|
|
987
568
|
})), "table");
|
|
988
569
|
} else {
|
|
989
570
|
output(res, flags.format);
|
|
@@ -993,6 +574,7 @@ async function actionSearch2(args, flags) {
|
|
|
993
574
|
}
|
|
994
575
|
}
|
|
995
576
|
async function actionCategories2(args, flags) {
|
|
577
|
+
showHelpIfRequested(flags, CATEGORIES_HELP);
|
|
996
578
|
const cfg = getConfig();
|
|
997
579
|
const fmt = flags.format || getFormat();
|
|
998
580
|
try {
|
|
@@ -1007,12 +589,13 @@ async function actionCategories2(args, flags) {
|
|
|
1007
589
|
}
|
|
1008
590
|
}
|
|
1009
591
|
async function actionServices2(args, flags) {
|
|
592
|
+
showHelpIfRequested(flags, SERVICES_HELP);
|
|
1010
593
|
const cfg = getConfig();
|
|
1011
594
|
const fmt = flags.format || getFormat();
|
|
1012
595
|
try {
|
|
1013
596
|
const res = await actionServices(cfg, {
|
|
1014
|
-
page: flags.page
|
|
1015
|
-
page_size: flags["page-size"]
|
|
597
|
+
page: positiveIntegerFlag(flags.page, "--page"),
|
|
598
|
+
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
1016
599
|
category: flags.category
|
|
1017
600
|
});
|
|
1018
601
|
const services = res.services || [];
|
|
@@ -1037,11 +620,11 @@ async function actionGet2(args, flags) {
|
|
|
1037
620
|
const id = args[0];
|
|
1038
621
|
if (!id) err("usage: xapi-to get <id> [--method GET|POST|DELETE|...]");
|
|
1039
622
|
if (flags.code) validateCodeFlag(flags);
|
|
623
|
+
const methodFilter = httpMethodFlag(flags.method);
|
|
1040
624
|
const cfg = getConfig();
|
|
1041
625
|
try {
|
|
1042
626
|
const res = await actionGet(id, cfg);
|
|
1043
627
|
const actions = Array.isArray(res) ? res : [res];
|
|
1044
|
-
const methodFilter = flags.method?.toUpperCase();
|
|
1045
628
|
const filtered = methodFilter ? actions.filter((a) => a.method?.toUpperCase() === methodFilter) : actions;
|
|
1046
629
|
if (filtered.length === 0) {
|
|
1047
630
|
err(`no endpoint found for method "${methodFilter}" in action "${id}"`);
|
|
@@ -1064,15 +647,30 @@ async function actionGet2(args, flags) {
|
|
|
1064
647
|
err("get failed", e.message);
|
|
1065
648
|
}
|
|
1066
649
|
}
|
|
650
|
+
async function actionBatchGet(args, flags) {
|
|
651
|
+
showHelpIfRequested(flags, GET_BATCH_HELP);
|
|
652
|
+
if (args.length === 0) err("usage: xapi-to get-batch <id> [id ...]");
|
|
653
|
+
if (args.length > 100) err("get-batch accepts at most 100 action IDs");
|
|
654
|
+
const cfg = getConfig();
|
|
655
|
+
try {
|
|
656
|
+
const res = await actionBatch(args, cfg);
|
|
657
|
+
output(res, flags.format);
|
|
658
|
+
} catch (e) {
|
|
659
|
+
err("get-batch failed", e.message);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
1067
662
|
async function actionCall2(args, flags) {
|
|
1068
663
|
showHelpIfRequested(flags, CALL_HELP);
|
|
1069
664
|
const id = args[0];
|
|
1070
665
|
if (!id) err(`usage: xapi-to call <id> --input '{"key":"val"}'`);
|
|
1071
666
|
if (flags.code) validateCodeFlag(flags);
|
|
1072
667
|
if (flags.output === "true") err("--output requires a file path");
|
|
668
|
+
const stream = flags.stream === "true" || flags.stream === "1" || flags.stream === "yes";
|
|
1073
669
|
if (flags.code && flags.output) {
|
|
1074
670
|
err("--output cannot be combined with --code");
|
|
1075
671
|
}
|
|
672
|
+
if (stream && flags.output) err("--stream cannot be combined with --output");
|
|
673
|
+
if (stream && flags.code) err("--stream cannot be combined with --code");
|
|
1076
674
|
const cfg = getConfig();
|
|
1077
675
|
let input = {};
|
|
1078
676
|
if (flags.input) {
|
|
@@ -1086,7 +684,7 @@ async function actionCall2(args, flags) {
|
|
|
1086
684
|
}
|
|
1087
685
|
}
|
|
1088
686
|
const { method: inputMethod, ...cleanInput } = input;
|
|
1089
|
-
const method = flags.method
|
|
687
|
+
const method = httpMethodFlag(flags.method) || (typeof inputMethod === "string" ? inputMethod.toUpperCase() : void 0);
|
|
1090
688
|
if (flags.code) {
|
|
1091
689
|
const result = generateCode(flags.code, { actionId: id, input: cleanInput, actionHost: cfg.actionHost, method });
|
|
1092
690
|
outputCode(result, flags);
|
|
@@ -1094,6 +692,10 @@ async function actionCall2(args, flags) {
|
|
|
1094
692
|
}
|
|
1095
693
|
requireApiKey(cfg);
|
|
1096
694
|
try {
|
|
695
|
+
if (stream) {
|
|
696
|
+
await actionStream(id, cleanInput, cfg, method);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
1097
699
|
if (flags.output) {
|
|
1098
700
|
const result = await actionDownload(
|
|
1099
701
|
id,
|
|
@@ -1126,11 +728,12 @@ async function actionCall2(args, flags) {
|
|
|
1126
728
|
var config_exports = {};
|
|
1127
729
|
__export(config_exports, {
|
|
1128
730
|
CONFIG_HELP: () => CONFIG_HELP,
|
|
731
|
+
HEALTH_HELP: () => HEALTH_HELP,
|
|
1129
732
|
configHealth: () => configHealth,
|
|
1130
733
|
configSet: () => configSet,
|
|
1131
734
|
configShow: () => configShow
|
|
1132
735
|
});
|
|
1133
|
-
import { readFileSync
|
|
736
|
+
import { readFileSync } from "fs";
|
|
1134
737
|
var CONFIG_HELP = `xapi-to config - Manage CLI configuration
|
|
1135
738
|
|
|
1136
739
|
USAGE
|
|
@@ -1144,14 +747,23 @@ COMMANDS
|
|
|
1144
747
|
FLAGS
|
|
1145
748
|
--format json|pretty|table Output format
|
|
1146
749
|
|
|
750
|
+
ENVIRONMENT OVERRIDES
|
|
751
|
+
XAPI_KEY takes precedence over XAPI_API_KEY, which takes precedence over the file.
|
|
752
|
+
Saving a file key does not replace an active environment-variable key.
|
|
753
|
+
|
|
1147
754
|
EXAMPLES
|
|
1148
755
|
xapi-to config show
|
|
1149
756
|
xapi-to config set apiKey=xapi_abc123
|
|
1150
757
|
echo "$XAPI_KEY" | xapi-to config set apiKey=- # keeps the key out of shell history
|
|
1151
758
|
xapi-to config health
|
|
1152
759
|
`;
|
|
760
|
+
var HEALTH_HELP = `xapi-to health - Check backend connectivity
|
|
761
|
+
|
|
762
|
+
USAGE
|
|
763
|
+
xapi-to health [--format json|pretty|table]
|
|
764
|
+
`;
|
|
1153
765
|
async function configShow(args, flags) {
|
|
1154
|
-
showConfig();
|
|
766
|
+
output(showConfig(), flags.format);
|
|
1155
767
|
}
|
|
1156
768
|
async function configSet(args, flags) {
|
|
1157
769
|
if (args.length === 0) err("usage: xapi-to config set apiKey=<key>");
|
|
@@ -1164,15 +776,27 @@ async function configSet(args, flags) {
|
|
|
1164
776
|
if (key !== "apiKey") err(`unknown config key: ${key} (only apiKey is configurable)`);
|
|
1165
777
|
let value = arg.slice(eq + 1);
|
|
1166
778
|
if (value === "-") {
|
|
1167
|
-
value =
|
|
779
|
+
value = readFileSync(0, "utf-8").trim();
|
|
1168
780
|
}
|
|
1169
781
|
if (!value) err("apiKey is empty");
|
|
1170
782
|
updates.apiKey = value;
|
|
1171
783
|
}
|
|
784
|
+
const sourceBeforeSave = getApiKeySource();
|
|
1172
785
|
saveConfig(updates);
|
|
1173
|
-
|
|
786
|
+
const source = sourceBeforeSave === "XAPI_KEY" || sourceBeforeSave === "XAPI_API_KEY" ? sourceBeforeSave : "file";
|
|
787
|
+
output({
|
|
788
|
+
ok: true,
|
|
789
|
+
updated: Object.keys(updates),
|
|
790
|
+
effective: source === "file",
|
|
791
|
+
source,
|
|
792
|
+
...source === "XAPI_KEY" || source === "XAPI_API_KEY" ? { warning: `${source} still overrides the saved file key` } : {}
|
|
793
|
+
}, flags.format);
|
|
1174
794
|
}
|
|
1175
795
|
async function configHealth(args, flags) {
|
|
796
|
+
if (flags.help) {
|
|
797
|
+
console.log(HEALTH_HELP);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
1176
800
|
const cfg = getConfig();
|
|
1177
801
|
const start = Date.now();
|
|
1178
802
|
try {
|
|
@@ -1185,6 +809,30 @@ async function configHealth(args, flags) {
|
|
|
1185
809
|
}
|
|
1186
810
|
|
|
1187
811
|
// src/commands/register.ts
|
|
812
|
+
var REGISTER_HELP = `xapi-to register - Create a new xAPI account
|
|
813
|
+
|
|
814
|
+
USAGE
|
|
815
|
+
xapi-to register [referral-code] [flags]
|
|
816
|
+
|
|
817
|
+
FLAGS
|
|
818
|
+
--referral-code <code> Submit an inviter's referral code
|
|
819
|
+
--referralCode <code> Alias for --referral-code
|
|
820
|
+
--force Replace an existing file-based API key
|
|
821
|
+
--format json|pretty|table Output format
|
|
822
|
+
|
|
823
|
+
The API key is saved to ~/.xapi/config.json. If XAPI_KEY or XAPI_API_KEY is set,
|
|
824
|
+
unset it before registering because environment variables override the saved file.
|
|
825
|
+
`;
|
|
826
|
+
function validateRegisterResponse(value) {
|
|
827
|
+
const res = value;
|
|
828
|
+
if (!res || typeof res.apiKey !== "string" || !res.apiKey.trim()) {
|
|
829
|
+
throw new Error("invalid register response: missing apiKey");
|
|
830
|
+
}
|
|
831
|
+
if (typeof res.referralCode !== "string" || !res.user || typeof res.user.id !== "string") {
|
|
832
|
+
throw new Error("invalid register response: missing account details");
|
|
833
|
+
}
|
|
834
|
+
return res;
|
|
835
|
+
}
|
|
1188
836
|
async function registerAccount(referralCode) {
|
|
1189
837
|
assertAllowedHost(XAPI_API_HOST);
|
|
1190
838
|
const controller = new AbortController();
|
|
@@ -1194,39 +842,57 @@ async function registerAccount(referralCode) {
|
|
|
1194
842
|
method: "POST",
|
|
1195
843
|
headers: { "Content-Type": "application/json" },
|
|
1196
844
|
body: JSON.stringify(referralCode ? { referralCode } : {}),
|
|
1197
|
-
signal: controller.signal
|
|
845
|
+
signal: controller.signal,
|
|
846
|
+
redirect: "manual"
|
|
1198
847
|
});
|
|
848
|
+
if (res.status >= 300 && res.status < 400) {
|
|
849
|
+
throw new Error(`refusing to follow redirect to "${res.headers.get("location") ?? "?"}"`);
|
|
850
|
+
}
|
|
1199
851
|
if (!res.ok) {
|
|
1200
852
|
const text = await res.text();
|
|
1201
853
|
throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
1202
854
|
}
|
|
1203
|
-
return res.json();
|
|
855
|
+
return validateRegisterResponse(await res.json());
|
|
1204
856
|
} finally {
|
|
1205
857
|
clearTimeout(timer);
|
|
1206
858
|
}
|
|
1207
859
|
}
|
|
1208
860
|
async function register(args, flags) {
|
|
861
|
+
if (flags.help) {
|
|
862
|
+
console.log(REGISTER_HELP);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
1209
865
|
try {
|
|
1210
866
|
const cfg = getConfig();
|
|
1211
867
|
const force = flags.force === "true" || flags.force === "1" || flags.force === "yes";
|
|
868
|
+
const source = getApiKeySource();
|
|
869
|
+
if (source === "XAPI_KEY" || source === "XAPI_API_KEY") {
|
|
870
|
+
err(
|
|
871
|
+
"register cannot replace an API key supplied by an environment variable",
|
|
872
|
+
`Unset ${source} first; it would continue to override the newly saved key.`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
1212
875
|
if (cfg.apiKey && !force) {
|
|
1213
876
|
err("register would overwrite existing apiKey", 'Run "xapi-to register --force" to create a new account and replace the saved key.');
|
|
1214
877
|
}
|
|
1215
878
|
const rawReferral = flags["referral-code"] ?? flags["referralCode"] ?? args[0];
|
|
1216
|
-
|
|
879
|
+
if (rawReferral === "true") {
|
|
880
|
+
err("--referral-code requires a code");
|
|
881
|
+
}
|
|
882
|
+
const referralCode = typeof rawReferral === "string" && rawReferral.length > 0 ? rawReferral : void 0;
|
|
1217
883
|
const res = await registerAccount(referralCode);
|
|
884
|
+
const bindUrl = res.bindUrl || res.claimUrl;
|
|
1218
885
|
saveConfig({ apiKey: res.apiKey });
|
|
1219
886
|
output({
|
|
1220
887
|
apiKey: res.apiKey,
|
|
1221
888
|
user: res.user,
|
|
1222
889
|
referralCode: res.referralCode,
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
...referralCode ? { referredBy: referralCode } : {},
|
|
890
|
+
bindUrl,
|
|
891
|
+
// Keep the backend's legacy field visible while clients migrate to bindUrl.
|
|
892
|
+
claimUrl: res.claimUrl || bindUrl,
|
|
893
|
+
// The backend may accept the registration while ignoring an invalid code,
|
|
894
|
+
// so only report that the code was submitted, not that a referral exists.
|
|
895
|
+
...referralCode ? { referralCodeProvided: referralCode } : {},
|
|
1230
896
|
note: force && cfg.apiKey ? "apiKey replaced in ~/.xapi/config.json" : "apiKey saved to ~/.xapi/config.json"
|
|
1231
897
|
}, flags.format);
|
|
1232
898
|
} catch (e) {
|
|
@@ -1236,23 +902,50 @@ async function register(args, flags) {
|
|
|
1236
902
|
|
|
1237
903
|
// src/commands/topup.ts
|
|
1238
904
|
var TOPUP_BASE_URL = "https://www.xapi.to/topup/payment";
|
|
905
|
+
var TOPUP_HELP = `xapi-to topup - Generate a private payment URL
|
|
906
|
+
|
|
907
|
+
USAGE
|
|
908
|
+
xapi-to topup [--amount <usd>] [--method stripe|x402]
|
|
909
|
+
|
|
910
|
+
The generated URL can contain your API key. Do not log or share it.
|
|
911
|
+
`;
|
|
1239
912
|
async function topup(args, flags) {
|
|
913
|
+
if (flags.help) {
|
|
914
|
+
console.log(TOPUP_HELP);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
1240
917
|
const cfg = getConfig();
|
|
1241
918
|
const url = new URL(TOPUP_BASE_URL);
|
|
1242
919
|
if (cfg.apiKey) url.searchParams.set("apikey", cfg.apiKey);
|
|
1243
|
-
if (flags.method)
|
|
920
|
+
if (flags.method) {
|
|
921
|
+
if (!["stripe", "x402"].includes(flags.method)) {
|
|
922
|
+
err("invalid --method value", "Expected stripe or x402.");
|
|
923
|
+
}
|
|
924
|
+
url.searchParams.set("method", flags.method);
|
|
925
|
+
}
|
|
1244
926
|
const amountStr = flags.amount || args[0];
|
|
1245
927
|
if (amountStr) {
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
928
|
+
const normalizedAmount = amountStr.trim();
|
|
929
|
+
const amountUsd = Number(normalizedAmount);
|
|
930
|
+
if (!/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(normalizedAmount) || !Number.isFinite(amountUsd) || amountUsd <= 0) {
|
|
931
|
+
err("invalid top-up amount", "Expected a positive USD number, e.g. --amount 10.");
|
|
1249
932
|
}
|
|
933
|
+
url.searchParams.set("amount", String(amountUsd));
|
|
1250
934
|
}
|
|
1251
935
|
output({ url: url.toString() }, flags.format);
|
|
1252
936
|
}
|
|
1253
937
|
|
|
1254
938
|
// src/commands/balance.ts
|
|
939
|
+
var BALANCE_HELP = `xapi-to balance - Show the current account balance
|
|
940
|
+
|
|
941
|
+
USAGE
|
|
942
|
+
xapi-to balance [--format json|pretty|table]
|
|
943
|
+
`;
|
|
1255
944
|
async function balance(args, flags) {
|
|
945
|
+
if (flags.help) {
|
|
946
|
+
console.log(BALANCE_HELP);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
1256
949
|
const cfg = getConfig();
|
|
1257
950
|
requireApiKey(cfg);
|
|
1258
951
|
let token;
|
|
@@ -1289,9 +982,10 @@ __export(oauth_exports, {
|
|
|
1289
982
|
});
|
|
1290
983
|
import { spawnSync } from "child_process";
|
|
1291
984
|
function openBrowser(url) {
|
|
1292
|
-
const cmd = process.platform === "win32" ? "
|
|
985
|
+
const cmd = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
986
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
1293
987
|
try {
|
|
1294
|
-
spawnSync(cmd,
|
|
988
|
+
spawnSync(cmd, args, { stdio: "ignore" });
|
|
1295
989
|
} catch {
|
|
1296
990
|
}
|
|
1297
991
|
}
|
|
@@ -1335,9 +1029,11 @@ async function findCurrentKeyRecord(plaintextKey, jwtToken) {
|
|
|
1335
1029
|
throw new Error("No API keys found for this account");
|
|
1336
1030
|
}
|
|
1337
1031
|
const prefix = plaintextKey.substring(0, 7);
|
|
1338
|
-
const
|
|
1032
|
+
const suffix = plaintextKey.slice(-4);
|
|
1033
|
+
const expectedPreview = `${prefix}****${suffix}`;
|
|
1034
|
+
const match = keys.find((k) => k.keyPreview === expectedPreview);
|
|
1339
1035
|
if (match) return match;
|
|
1340
|
-
if (keys.length === 1) return keys[0];
|
|
1036
|
+
if (keys.length === 1 && !keys[0].keyPreview.includes("****")) return keys[0];
|
|
1341
1037
|
throw new Error(
|
|
1342
1038
|
`Current API key (${prefix}...) was not found in your account keys. Run "xapi-to config set apiKey=<key>" with a valid key before binding OAuth.`
|
|
1343
1039
|
);
|
|
@@ -1454,13 +1150,20 @@ FLAGS
|
|
|
1454
1150
|
EXAMPLES
|
|
1455
1151
|
xapi-to oauth bind
|
|
1456
1152
|
xapi-to oauth bind --provider twitter
|
|
1457
|
-
xapi-to oauth
|
|
1153
|
+
xapi-to oauth providers # inspect current default scopes
|
|
1154
|
+
xapi-to oauth bind --scopes "<scope list>" # override only when needed
|
|
1458
1155
|
xapi-to oauth status
|
|
1459
1156
|
xapi-to oauth status --format pretty
|
|
1460
1157
|
xapi-to oauth unbind abc123
|
|
1461
1158
|
xapi-to oauth providers
|
|
1462
1159
|
`;
|
|
1463
1160
|
async function oauthBind(args, flags) {
|
|
1161
|
+
if (flags.provider === "true") {
|
|
1162
|
+
err("--provider requires a provider name, e.g. --provider twitter");
|
|
1163
|
+
}
|
|
1164
|
+
if (flags.scopes === "true") {
|
|
1165
|
+
err("--scopes requires a space-separated scope list");
|
|
1166
|
+
}
|
|
1464
1167
|
const cfg = getConfig();
|
|
1465
1168
|
requireApiKey(cfg);
|
|
1466
1169
|
const apiKey = cfg.apiKey;
|
|
@@ -1518,6 +1221,16 @@ async function oauthBind(args, flags) {
|
|
|
1518
1221
|
const authorizationStartedAt = /* @__PURE__ */ new Date();
|
|
1519
1222
|
const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST, scopes);
|
|
1520
1223
|
const { authorizationUrl } = result;
|
|
1224
|
+
let authorizationTarget;
|
|
1225
|
+
try {
|
|
1226
|
+
authorizationTarget = new URL(authorizationUrl);
|
|
1227
|
+
} catch {
|
|
1228
|
+
throw new Error("OAuth provider returned an invalid authorization URL");
|
|
1229
|
+
}
|
|
1230
|
+
const localHttp = authorizationTarget.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(authorizationTarget.hostname);
|
|
1231
|
+
if (authorizationTarget.protocol !== "https:" && !localHttp) {
|
|
1232
|
+
throw new Error(`OAuth provider returned an unsupported authorization URL protocol: ${authorizationTarget.protocol}`);
|
|
1233
|
+
}
|
|
1521
1234
|
if (isTTY) {
|
|
1522
1235
|
if (!headerPrinted) {
|
|
1523
1236
|
console.error(`
|
|
@@ -1704,7 +1417,7 @@ function parsePositiveInt(raw, flagName) {
|
|
|
1704
1417
|
}
|
|
1705
1418
|
return n;
|
|
1706
1419
|
}
|
|
1707
|
-
function
|
|
1420
|
+
function sleep(ms) {
|
|
1708
1421
|
if (ms <= 0) return Promise.resolve();
|
|
1709
1422
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1710
1423
|
}
|
|
@@ -1801,13 +1514,891 @@ async function taskWait(args, flags) {
|
|
|
1801
1514
|
}
|
|
1802
1515
|
const desiredWaitMs = retryDelayMs ?? intervalMs;
|
|
1803
1516
|
const waitMs = deadline !== void 0 ? Math.min(desiredWaitMs, Math.max(0, deadline - Date.now())) : desiredWaitMs;
|
|
1804
|
-
await
|
|
1517
|
+
await sleep(waitMs);
|
|
1805
1518
|
}
|
|
1806
1519
|
}
|
|
1807
1520
|
function taskHelp() {
|
|
1808
1521
|
return TASK_HELP;
|
|
1809
1522
|
}
|
|
1810
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
|
+
|
|
1811
2402
|
// src/args.ts
|
|
1812
2403
|
function parseArgs(argv) {
|
|
1813
2404
|
const positional = [];
|
|
@@ -1859,10 +2450,12 @@ COMMANDS
|
|
|
1859
2450
|
--page N --page-size N Pagination
|
|
1860
2451
|
--category <name> Filter by category
|
|
1861
2452
|
--service-id <id> Filter by service
|
|
1862
|
-
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)
|
|
1863
2454
|
--source capability|api Filter by source type
|
|
1864
2455
|
--category <name> Filter by category
|
|
1865
2456
|
--page N --page-size N Pagination
|
|
2457
|
+
--sort default|relevance|price Recommended, strongest match, or comparable price
|
|
2458
|
+
--include-all-versions Include active non-default major versions
|
|
1866
2459
|
categories List all action categories
|
|
1867
2460
|
--source capability|api Filter by source type
|
|
1868
2461
|
services List all services
|
|
@@ -1870,9 +2463,11 @@ COMMANDS
|
|
|
1870
2463
|
--category <name> Filter by category
|
|
1871
2464
|
get <id> [--method GET|POST|...] Get action schema (filter by HTTP method)
|
|
1872
2465
|
--code <target> Generate code snippet (curl, py, js, ts, go)
|
|
2466
|
+
get-batch <id> [id ...] Get up to 100 action schemas
|
|
1873
2467
|
call <id> --input '{"key":"val"}' Execute an action
|
|
1874
2468
|
--method GET|POST|... Override HTTP method
|
|
1875
2469
|
--output <path> Save a raw binary response to a new file
|
|
2470
|
+
--stream Forward HTTP SSE frames unchanged
|
|
1876
2471
|
--code <target> Generate code snippet instead of executing
|
|
1877
2472
|
Variants: python.requests, python.httpx, javascript.fetch, javascript.axios
|
|
1878
2473
|
|
|
@@ -1882,6 +2477,12 @@ COMMANDS
|
|
|
1882
2477
|
--timeout <duration> Max wait duration, e.g. 10m
|
|
1883
2478
|
--max-attempts <number> Max poll attempts
|
|
1884
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
|
+
|
|
1885
2486
|
oauth bind [--provider twitter] Bind Twitter OAuth to your API key
|
|
1886
2487
|
oauth status List current OAuth bindings
|
|
1887
2488
|
oauth unbind <binding-id> Remove an OAuth binding
|
|
@@ -1889,7 +2490,7 @@ COMMANDS
|
|
|
1889
2490
|
|
|
1890
2491
|
register [referral-code] Create a new user account (apiKey saved automatically)
|
|
1891
2492
|
--referral-code <code> Register with an inviter's referral code (also: --referralCode, or as positional arg)
|
|
1892
|
-
--force Replace an existing
|
|
2493
|
+
--force Replace an existing file-based apiKey
|
|
1893
2494
|
balance Show current account balance
|
|
1894
2495
|
topup [--amount <usd>] [--method stripe|x402] Generate payment URL
|
|
1895
2496
|
|
|
@@ -1904,9 +2505,13 @@ GLOBAL FLAGS
|
|
|
1904
2505
|
--help Show help (use with a command for details, e.g. xapi-to get --help)
|
|
1905
2506
|
|
|
1906
2507
|
ENV VARS
|
|
1907
|
-
XAPI_KEY
|
|
2508
|
+
XAPI_KEY API key (highest precedence; header: XAPI-Key)
|
|
2509
|
+
XAPI_API_KEY Compatible API key alias
|
|
1908
2510
|
XAPI_ACTION_HOST Action service host (default: action.xapi.to)
|
|
2511
|
+
XAPI_API_HOST Auth/account service host (default: api.xapi.to)
|
|
2512
|
+
XAPI_SANDBOX_HOST Sandbox gateway host (default: sandbox.xapi.to)
|
|
1909
2513
|
XAPI_OUTPUT Default output format
|
|
2514
|
+
XAPI_TRANSFER_IDLE_TIMEOUT_MS SSE/download idle timeout (default: 60000)
|
|
1910
2515
|
|
|
1911
2516
|
EXAMPLES
|
|
1912
2517
|
xapi-to register
|
|
@@ -1916,6 +2521,7 @@ EXAMPLES
|
|
|
1916
2521
|
xapi-to list --source capability
|
|
1917
2522
|
xapi-to search twitter --source api
|
|
1918
2523
|
xapi-to get twitter.tweet_detail
|
|
2524
|
+
xapi-to get-batch twitter.tweet_detail crypto.token.price
|
|
1919
2525
|
xapi-to get twitter.tweet_detail --code curl
|
|
1920
2526
|
xapi-to get twitter.tweet_detail --code py --format pretty
|
|
1921
2527
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
@@ -1923,6 +2529,7 @@ EXAMPLES
|
|
|
1923
2529
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
|
|
1924
2530
|
xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
|
|
1925
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)"'
|
|
1926
2533
|
xapi-to categories
|
|
1927
2534
|
xapi-to services --format table
|
|
1928
2535
|
xapi-to config set apiKey=xapi_abc123
|
|
@@ -1934,7 +2541,16 @@ async function main() {
|
|
|
1934
2541
|
console.log(HELP);
|
|
1935
2542
|
process.exit(0);
|
|
1936
2543
|
}
|
|
1937
|
-
if (flags.format)
|
|
2544
|
+
if (flags.format) {
|
|
2545
|
+
if (!["json", "pretty", "table"].includes(flags.format)) {
|
|
2546
|
+
console.error(JSON.stringify({
|
|
2547
|
+
error: `invalid --format value: ${flags.format}`,
|
|
2548
|
+
hint: "expected json, pretty, or table"
|
|
2549
|
+
}));
|
|
2550
|
+
process.exit(1);
|
|
2551
|
+
}
|
|
2552
|
+
process.env.XAPI_OUTPUT = flags.format;
|
|
2553
|
+
}
|
|
1938
2554
|
const [cmd, ...rest] = positional;
|
|
1939
2555
|
switch (cmd) {
|
|
1940
2556
|
// ── Action commands (top-level) ──
|
|
@@ -1948,6 +2564,8 @@ async function main() {
|
|
|
1948
2564
|
return actionServices2(rest, flags);
|
|
1949
2565
|
case "get":
|
|
1950
2566
|
return actionGet2(rest, flags);
|
|
2567
|
+
case "get-batch":
|
|
2568
|
+
return actionBatchGet(rest, flags);
|
|
1951
2569
|
case "call":
|
|
1952
2570
|
return actionCall2(rest, flags);
|
|
1953
2571
|
case "task": {
|
|
@@ -1967,6 +2585,54 @@ async function main() {
|
|
|
1967
2585
|
}
|
|
1968
2586
|
break;
|
|
1969
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
|
+
}
|
|
1970
2636
|
// ── OAuth commands ──
|
|
1971
2637
|
case "oauth": {
|
|
1972
2638
|
if (flags.help || rest.length === 0) {
|