xapi-to 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -1
- package/dist/chunk-UEQCIJ7T.js +922 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1802 -726
- package/dist/openai-sandbox-client.d.ts +85 -0
- package/dist/openai-sandbox-client.js +285 -0
- package/examples/openai-agents-sandbox-local.ts +131 -0
- package/examples/sandbox-api-cli-openai.mjs +450 -0
- package/package.json +25 -3
- package/scripts/openai-sandbox-agent-e2e.ts +219 -0
- package/scripts/sandbox-playground-e2e.mjs +463 -0
- package/skills/xapi/SKILL.md +19 -7
- package/skills/xapi/guides/linkedin.md +55 -0
- package/skills/xapi/guides/provider.md +198 -0
- package/skills/xapi/guides/sandbox.md +520 -0
- package/skills/xapi/guides/serper.md +124 -0
- package/src/client.ts +715 -0
- package/src/config.ts +160 -0
- package/src/openai-sandbox-client.ts +349 -0
- package/src/sandbox-client.ts +309 -0
package/dist/index.js
CHANGED
|
@@ -1,661 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return { [primitiveKey]: row };
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
function singularKey(key) {
|
|
57
|
-
if (key === "categories") return "category";
|
|
58
|
-
if (key.endsWith("ies")) return `${key.slice(0, -3)}y`;
|
|
59
|
-
if (key.endsWith("s")) return key.slice(0, -1);
|
|
60
|
-
return "value";
|
|
61
|
-
}
|
|
62
|
-
function formatCell(value) {
|
|
63
|
-
if (value === null || value === void 0) return "";
|
|
64
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
65
|
-
return String(value);
|
|
66
|
-
}
|
|
67
|
-
function printTable(rows) {
|
|
68
|
-
if (rows.length === 0) {
|
|
69
|
-
console.log("(empty)");
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const keys = Object.keys(rows[0]);
|
|
73
|
-
const widths = keys.map(
|
|
74
|
-
(k) => Math.min(40, Math.max(k.length, ...rows.map((r) => formatCell(r[k]).length)))
|
|
75
|
-
);
|
|
76
|
-
const sep = widths.map((w) => "-".repeat(w)).join(" ");
|
|
77
|
-
const header = keys.map((k, i) => k.padEnd(widths[i])).join(" ");
|
|
78
|
-
console.log(header);
|
|
79
|
-
console.log(sep);
|
|
80
|
-
for (const row of rows) {
|
|
81
|
-
const line = keys.map((k, i) => formatCell(row[k]).slice(0, widths[i]).padEnd(widths[i])).join(" ");
|
|
82
|
-
console.log(line);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function err(msg, detail) {
|
|
86
|
-
if (process.stderr.isTTY) {
|
|
87
|
-
console.error(`Error: ${msg}`);
|
|
88
|
-
if (detail !== void 0) console.error(` ${detail}`);
|
|
89
|
-
} else {
|
|
90
|
-
const out = { error: msg };
|
|
91
|
-
if (detail !== void 0) out.detail = detail;
|
|
92
|
-
console.error(JSON.stringify(out));
|
|
93
|
-
}
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// src/config.ts
|
|
98
|
-
import { homedir } from "os";
|
|
99
|
-
import { join } from "path";
|
|
100
|
-
var XAPI_ACTION_HOST = process.env.XAPI_ACTION_HOST || "action.xapi.to";
|
|
101
|
-
var XAPI_API_HOST = process.env.XAPI_API_HOST || "api.xapi.to";
|
|
102
|
-
function scheme(host) {
|
|
103
|
-
return isLoopbackHost(host) ? "http" : "https";
|
|
104
|
-
}
|
|
105
|
-
var ALLOWED_HOST_EXACT = ["xapi.to", "xapi.xyz"];
|
|
106
|
-
var ALLOWED_HOST_SUFFIXES = [".xapi.to", ".xapi.xyz"];
|
|
107
|
-
function hostnameOf(hostOrUrl) {
|
|
108
|
-
const raw = hostOrUrl.includes("://") ? hostOrUrl : `http://${hostOrUrl}`;
|
|
109
|
-
try {
|
|
110
|
-
return new URL(raw).hostname.toLowerCase();
|
|
111
|
-
} catch {
|
|
112
|
-
return "";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function isLoopbackIPv4(h) {
|
|
116
|
-
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
117
|
-
if (!m) return false;
|
|
118
|
-
const octets = m.slice(1).map(Number);
|
|
119
|
-
return octets.every((o) => o <= 255) && octets[0] === 127;
|
|
120
|
-
}
|
|
121
|
-
function isLoopbackHostname(h) {
|
|
122
|
-
return h === "localhost" || h.endsWith(".localhost") || h === "::1" || h === "[::1]" || isLoopbackIPv4(h);
|
|
123
|
-
}
|
|
124
|
-
function isLoopbackHost(hostOrUrl) {
|
|
125
|
-
return isLoopbackHostname(hostnameOf(hostOrUrl));
|
|
126
|
-
}
|
|
127
|
-
function isAllowedHost(hostOrUrl) {
|
|
128
|
-
const h = hostnameOf(hostOrUrl);
|
|
129
|
-
if (!h) return false;
|
|
130
|
-
if (isLoopbackHostname(h)) return true;
|
|
131
|
-
if (ALLOWED_HOST_EXACT.includes(h)) return true;
|
|
132
|
-
return ALLOWED_HOST_SUFFIXES.some((suffix) => h.endsWith(suffix));
|
|
133
|
-
}
|
|
134
|
-
function assertAllowedHost(hostOrUrl) {
|
|
135
|
-
if (!isAllowedHost(hostOrUrl)) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`refusing to contact untrusted host "${hostnameOf(hostOrUrl) || hostOrUrl}": the xapi API key may only be sent to *.xapi.to, *.xapi.xyz, or localhost`
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
var CONFIG_DIR = join(homedir(), ".xapi");
|
|
142
|
-
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
143
|
-
function loadFileConfig() {
|
|
144
|
-
if (!existsSync(CONFIG_FILE)) return {};
|
|
145
|
-
try {
|
|
146
|
-
const parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
147
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
148
|
-
return typeof parsed.apiKey === "string" && parsed.apiKey.trim() ? { apiKey: parsed.apiKey } : {};
|
|
149
|
-
} catch {
|
|
150
|
-
return {};
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
function getApiKeySource() {
|
|
154
|
-
if (process.env.XAPI_KEY) return "XAPI_KEY";
|
|
155
|
-
if (process.env.XAPI_API_KEY) return "XAPI_API_KEY";
|
|
156
|
-
return loadFileConfig().apiKey ? "file" : "none";
|
|
157
|
-
}
|
|
158
|
-
function getConfig() {
|
|
159
|
-
const file = loadFileConfig();
|
|
160
|
-
return {
|
|
161
|
-
actionHost: XAPI_ACTION_HOST,
|
|
162
|
-
apiKey: process.env.XAPI_KEY || process.env.XAPI_API_KEY || file.apiKey
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
function requireApiKey(cfg) {
|
|
166
|
-
if (!cfg.apiKey) {
|
|
167
|
-
err("API key not configured", 'Run "npx xapi-to register" to create an account, or "npx xapi-to config set apiKey=<key>" to set an existing key.');
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function saveConfig(updates) {
|
|
171
|
-
const current = loadFileConfig();
|
|
172
|
-
const merged = { ...current, ...updates };
|
|
173
|
-
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
174
|
-
if (process.platform !== "win32") chmodSync(CONFIG_DIR, 448);
|
|
175
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
176
|
-
if (process.platform !== "win32") chmodSync(CONFIG_FILE, 384);
|
|
177
|
-
}
|
|
178
|
-
function showConfig() {
|
|
179
|
-
const cfg = getConfig();
|
|
180
|
-
return {
|
|
181
|
-
actionHost: cfg.actionHost,
|
|
182
|
-
apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : void 0,
|
|
183
|
-
source: {
|
|
184
|
-
apiKey: getApiKeySource()
|
|
185
|
-
},
|
|
186
|
-
configFile: CONFIG_FILE
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// src/client.ts
|
|
191
|
-
import { open, rm } from "fs/promises";
|
|
192
|
-
import { once } from "events";
|
|
193
|
-
import { resolve } from "path";
|
|
194
|
-
import { Readable, Transform } from "stream";
|
|
195
|
-
import { pipeline } from "stream/promises";
|
|
196
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
197
|
-
var EXECUTE_TIMEOUT_MS = 6e4;
|
|
198
|
-
var TRANSFER_IDLE_TIMEOUT_MS = 6e4;
|
|
199
|
-
var IDEMPOTENT_RETRIES = 2;
|
|
200
|
-
var RETRY_BASE_DELAY_MS = 500;
|
|
201
|
-
var RETRY_MAX_DELAY_MS = 8e3;
|
|
202
|
-
var HttpError = class extends Error {
|
|
203
|
-
constructor(status, detail, retryAfterMs) {
|
|
204
|
-
super(`HTTP ${status}: ${detail}`);
|
|
205
|
-
this.status = status;
|
|
206
|
-
this.retryAfterMs = retryAfterMs;
|
|
207
|
-
this.name = "HttpError";
|
|
208
|
-
}
|
|
209
|
-
status;
|
|
210
|
-
retryAfterMs;
|
|
211
|
-
};
|
|
212
|
-
var RequestTimeoutError = class extends Error {
|
|
213
|
-
constructor(timeoutMs) {
|
|
214
|
-
super(`request timed out after ${timeoutMs}ms`);
|
|
215
|
-
this.timeoutMs = timeoutMs;
|
|
216
|
-
this.name = "RequestTimeoutError";
|
|
217
|
-
}
|
|
218
|
-
timeoutMs;
|
|
219
|
-
};
|
|
220
|
-
function isRetryableStatus(status) {
|
|
221
|
-
return status === 408 || status === 429 || status === 502 || status === 503 || status === 504;
|
|
222
|
-
}
|
|
223
|
-
function isRetryableNetworkError(e) {
|
|
224
|
-
if (!(e instanceof Error)) return false;
|
|
225
|
-
if (e instanceof HttpError || e instanceof RequestTimeoutError) return false;
|
|
226
|
-
if (e.name === "AbortError") return false;
|
|
227
|
-
return e instanceof TypeError || /network|fetch failed|econn|etimedout|eai_again|socket|dns/i.test(e.message);
|
|
228
|
-
}
|
|
229
|
-
function isRetryableRequestError(e) {
|
|
230
|
-
if (e instanceof HttpError) return isRetryableStatus(e.status);
|
|
231
|
-
if (e instanceof RequestTimeoutError) return true;
|
|
232
|
-
return isRetryableNetworkError(e);
|
|
233
|
-
}
|
|
234
|
-
function retryBaseDelayMs() {
|
|
235
|
-
const override = Number(process.env.XAPI_RETRY_BASE_MS);
|
|
236
|
-
return Number.isFinite(override) && override > 0 ? override : RETRY_BASE_DELAY_MS;
|
|
237
|
-
}
|
|
238
|
-
function transferIdleTimeoutMs() {
|
|
239
|
-
const override = Number(process.env.XAPI_TRANSFER_IDLE_TIMEOUT_MS);
|
|
240
|
-
return Number.isFinite(override) && override > 0 ? override : TRANSFER_IDLE_TIMEOUT_MS;
|
|
241
|
-
}
|
|
242
|
-
function backoffDelayMs(attempt, retryAfterMs) {
|
|
243
|
-
if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
|
|
244
|
-
return Math.min(retryAfterMs, RETRY_MAX_DELAY_MS);
|
|
245
|
-
}
|
|
246
|
-
const capped = Math.min(retryBaseDelayMs() * 2 ** attempt, RETRY_MAX_DELAY_MS);
|
|
247
|
-
return capped / 2 + Math.random() * (capped / 2);
|
|
248
|
-
}
|
|
249
|
-
function parseRetryAfterMs(res) {
|
|
250
|
-
const header = res.headers.get("retry-after");
|
|
251
|
-
if (!header) return void 0;
|
|
252
|
-
const seconds = Number(header);
|
|
253
|
-
if (Number.isFinite(seconds)) return seconds * 1e3;
|
|
254
|
-
const at = Date.parse(header);
|
|
255
|
-
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
|
|
256
|
-
}
|
|
257
|
-
function sleep(ms) {
|
|
258
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
259
|
-
}
|
|
260
|
-
async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
|
|
261
|
-
assertAllowedHost(url);
|
|
262
|
-
let attempt = 0;
|
|
263
|
-
while (true) {
|
|
264
|
-
const controller = new AbortController();
|
|
265
|
-
let timedOut = false;
|
|
266
|
-
const timer = setTimeout(() => {
|
|
267
|
-
timedOut = true;
|
|
268
|
-
controller.abort();
|
|
269
|
-
}, timeoutMs);
|
|
270
|
-
try {
|
|
271
|
-
const res = await fetch(url, { ...options, redirect: "manual", signal: controller.signal });
|
|
272
|
-
if (res.status >= 300 && res.status < 400) {
|
|
273
|
-
throw new Error(
|
|
274
|
-
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (!res.ok) {
|
|
278
|
-
const retryAfterMs = isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0;
|
|
279
|
-
if (isRetryableStatus(res.status) && attempt < retries) {
|
|
280
|
-
await res.text().catch(() => "");
|
|
281
|
-
clearTimeout(timer);
|
|
282
|
-
await sleep(backoffDelayMs(attempt, retryAfterMs));
|
|
283
|
-
attempt++;
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
const text2 = await res.text();
|
|
287
|
-
throw new HttpError(res.status, text2.slice(0, 300), retryAfterMs);
|
|
288
|
-
}
|
|
289
|
-
if (res.status === 204) {
|
|
290
|
-
return void 0;
|
|
291
|
-
}
|
|
292
|
-
const text = await res.text();
|
|
293
|
-
if (!text.trim()) {
|
|
294
|
-
return void 0;
|
|
295
|
-
}
|
|
296
|
-
const body = JSON.parse(text);
|
|
297
|
-
if (body && typeof body === "object" && "success" in body && body.success === false) {
|
|
298
|
-
const data = body.data;
|
|
299
|
-
if (data?.statusCode === 401 || data?.error === "Unauthorized") {
|
|
300
|
-
throw new Error(
|
|
301
|
-
"Authentication failed: " + (data.message || "Invalid or missing API key") + '. Run "npx xapi-to config set apiKey=<key>" to update your key.'
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
if (data?.error === "OAuth Required" || data?.statusCode === 403 && data?.message?.includes("OAuth")) {
|
|
305
|
-
throw new Error(
|
|
306
|
-
(data.message || "OAuth authorization required") + '. Run "xapi-to oauth bind" to connect your account.'
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return body;
|
|
311
|
-
} catch (e) {
|
|
312
|
-
if (timedOut) {
|
|
313
|
-
const timeoutError = new RequestTimeoutError(timeoutMs);
|
|
314
|
-
if (attempt < retries) {
|
|
315
|
-
await sleep(backoffDelayMs(attempt));
|
|
316
|
-
attempt++;
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
throw timeoutError;
|
|
320
|
-
}
|
|
321
|
-
if (isRetryableNetworkError(e) && attempt < retries) {
|
|
322
|
-
clearTimeout(timer);
|
|
323
|
-
await sleep(backoffDelayMs(attempt));
|
|
324
|
-
attempt++;
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
throw e;
|
|
328
|
-
} finally {
|
|
329
|
-
clearTimeout(timer);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function headers(apiKey) {
|
|
334
|
-
const h = { "Content-Type": "application/json" };
|
|
335
|
-
if (apiKey) h["XAPI-Key"] = apiKey;
|
|
336
|
-
return h;
|
|
337
|
-
}
|
|
338
|
-
function baseUrl(opts) {
|
|
339
|
-
return `${scheme(opts.actionHost)}://${opts.actionHost}`;
|
|
340
|
-
}
|
|
341
|
-
async function actionList(opts, params = {}) {
|
|
342
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions`);
|
|
343
|
-
if (params.page) url.searchParams.set("page", String(params.page));
|
|
344
|
-
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
345
|
-
if (params.category) url.searchParams.set("category", params.category);
|
|
346
|
-
if (params.source) url.searchParams.set("source", params.source);
|
|
347
|
-
if (params.service_id) url.searchParams.set("service_id", params.service_id);
|
|
348
|
-
return request(
|
|
349
|
-
url.toString(),
|
|
350
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
351
|
-
DEFAULT_TIMEOUT_MS,
|
|
352
|
-
IDEMPOTENT_RETRIES
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
async function actionSearch(query, opts, params = {}) {
|
|
356
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions/search`);
|
|
357
|
-
url.searchParams.set("q", query);
|
|
358
|
-
if (params.category) url.searchParams.set("category", params.category);
|
|
359
|
-
if (params.source) url.searchParams.set("source", params.source);
|
|
360
|
-
if (params.page) url.searchParams.set("page", String(params.page));
|
|
361
|
-
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
362
|
-
if (params.include_all_versions) url.searchParams.set("include_all_versions", "true");
|
|
363
|
-
if (params.sort) url.searchParams.set("sort", params.sort);
|
|
364
|
-
return request(
|
|
365
|
-
url.toString(),
|
|
366
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
367
|
-
DEFAULT_TIMEOUT_MS,
|
|
368
|
-
IDEMPOTENT_RETRIES
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
async function actionCategories(opts, params = {}) {
|
|
372
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions/categories`);
|
|
373
|
-
if (params.source) url.searchParams.set("source", params.source);
|
|
374
|
-
return request(
|
|
375
|
-
url.toString(),
|
|
376
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
377
|
-
DEFAULT_TIMEOUT_MS,
|
|
378
|
-
IDEMPOTENT_RETRIES
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
async function actionGet(id, opts) {
|
|
382
|
-
return request(
|
|
383
|
-
`${baseUrl(opts)}/v1/actions/${encodeURIComponent(id)}`,
|
|
384
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
385
|
-
DEFAULT_TIMEOUT_MS,
|
|
386
|
-
IDEMPOTENT_RETRIES
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
async function actionBatch(ids, opts) {
|
|
390
|
-
return request(
|
|
391
|
-
`${baseUrl(opts)}/v1/actions/batch`,
|
|
392
|
-
{
|
|
393
|
-
method: "POST",
|
|
394
|
-
headers: headers(opts.apiKey),
|
|
395
|
-
body: JSON.stringify({ ids })
|
|
396
|
-
},
|
|
397
|
-
DEFAULT_TIMEOUT_MS,
|
|
398
|
-
IDEMPOTENT_RETRIES
|
|
399
|
-
// read-only metadata fetch — safe to retry
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeoutMs = EXECUTE_TIMEOUT_MS) {
|
|
403
|
-
return request(
|
|
404
|
-
`${baseUrl(opts)}/v1/actions/execute`,
|
|
405
|
-
{
|
|
406
|
-
method: "POST",
|
|
407
|
-
headers: headers(opts.apiKey),
|
|
408
|
-
body: JSON.stringify({ action_id: actionId, ...httpMethod ? { method: httpMethod } : {}, input })
|
|
409
|
-
},
|
|
410
|
-
Math.min(timeoutMs, EXECUTE_TIMEOUT_MS),
|
|
411
|
-
retries
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
async function actionStream(actionId, input, opts, httpMethod) {
|
|
415
|
-
const controller = new AbortController();
|
|
416
|
-
let timedOut = false;
|
|
417
|
-
let activeTimeoutMs = EXECUTE_TIMEOUT_MS;
|
|
418
|
-
let timer;
|
|
419
|
-
const resetTimeout = (timeoutMs) => {
|
|
420
|
-
if (timer) clearTimeout(timer);
|
|
421
|
-
activeTimeoutMs = timeoutMs;
|
|
422
|
-
timer = setTimeout(() => {
|
|
423
|
-
timedOut = true;
|
|
424
|
-
controller.abort();
|
|
425
|
-
}, timeoutMs);
|
|
426
|
-
};
|
|
427
|
-
resetTimeout(EXECUTE_TIMEOUT_MS);
|
|
428
|
-
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
429
|
-
assertAllowedHost(url);
|
|
430
|
-
try {
|
|
431
|
-
const res = await fetch(url, {
|
|
432
|
-
method: "POST",
|
|
433
|
-
headers: {
|
|
434
|
-
...headers(opts.apiKey),
|
|
435
|
-
Accept: "text/event-stream"
|
|
436
|
-
},
|
|
437
|
-
body: JSON.stringify({
|
|
438
|
-
action_id: actionId,
|
|
439
|
-
...httpMethod ? { method: httpMethod } : {},
|
|
440
|
-
input,
|
|
441
|
-
stream: true
|
|
442
|
-
}),
|
|
443
|
-
redirect: "manual",
|
|
444
|
-
signal: controller.signal
|
|
445
|
-
});
|
|
446
|
-
if (res.status >= 300 && res.status < 400) {
|
|
447
|
-
throw new Error(
|
|
448
|
-
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
if (!res.ok) {
|
|
452
|
-
const text = await res.text();
|
|
453
|
-
throw new HttpError(
|
|
454
|
-
res.status,
|
|
455
|
-
text.slice(0, 300),
|
|
456
|
-
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
const contentType = res.headers.get("content-type") || "";
|
|
460
|
-
if (!contentType.toLowerCase().includes("text/event-stream")) {
|
|
461
|
-
const text = await res.text();
|
|
462
|
-
throw new Error(
|
|
463
|
-
`expected an SSE response but received "${contentType || "unknown"}": ${text.slice(0, 300)}`
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
if (!res.body) return;
|
|
467
|
-
const idleTimeoutMs = transferIdleTimeoutMs();
|
|
468
|
-
resetTimeout(idleTimeoutMs);
|
|
469
|
-
const source = Readable.fromWeb(res.body);
|
|
470
|
-
for await (const chunk of source) {
|
|
471
|
-
resetTimeout(idleTimeoutMs);
|
|
472
|
-
if (!process.stdout.write(chunk)) await once(process.stdout, "drain");
|
|
473
|
-
}
|
|
474
|
-
} catch (error) {
|
|
475
|
-
if (timedOut) throw new RequestTimeoutError(activeTimeoutMs);
|
|
476
|
-
throw error;
|
|
477
|
-
} finally {
|
|
478
|
-
if (timer) clearTimeout(timer);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
async function actionDownload(actionId, input, opts, outputPath, httpMethod) {
|
|
482
|
-
const controller = new AbortController();
|
|
483
|
-
let timedOut = false;
|
|
484
|
-
let activeTimeoutMs = EXECUTE_TIMEOUT_MS;
|
|
485
|
-
let timer;
|
|
486
|
-
const resetTimeout = (timeoutMs) => {
|
|
487
|
-
if (timer) clearTimeout(timer);
|
|
488
|
-
activeTimeoutMs = timeoutMs;
|
|
489
|
-
timer = setTimeout(() => {
|
|
490
|
-
timedOut = true;
|
|
491
|
-
controller.abort();
|
|
492
|
-
}, timeoutMs);
|
|
493
|
-
};
|
|
494
|
-
resetTimeout(EXECUTE_TIMEOUT_MS);
|
|
495
|
-
const target = resolve(outputPath);
|
|
496
|
-
let file;
|
|
497
|
-
let complete = false;
|
|
498
|
-
try {
|
|
499
|
-
try {
|
|
500
|
-
file = await open(target, "wx");
|
|
501
|
-
} catch (error) {
|
|
502
|
-
if (error?.code === "EEXIST") {
|
|
503
|
-
throw new Error(`Output file already exists: ${target}`);
|
|
504
|
-
}
|
|
505
|
-
throw error;
|
|
506
|
-
}
|
|
507
|
-
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
508
|
-
assertAllowedHost(url);
|
|
509
|
-
const res = await fetch(url, {
|
|
510
|
-
method: "POST",
|
|
511
|
-
headers: headers(opts.apiKey),
|
|
512
|
-
body: JSON.stringify({
|
|
513
|
-
action_id: actionId,
|
|
514
|
-
...httpMethod ? { method: httpMethod } : {},
|
|
515
|
-
input,
|
|
516
|
-
response_mode: "raw"
|
|
517
|
-
}),
|
|
518
|
-
redirect: "manual",
|
|
519
|
-
signal: controller.signal
|
|
520
|
-
});
|
|
521
|
-
if (res.status >= 300 && res.status < 400) {
|
|
522
|
-
throw new Error(
|
|
523
|
-
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
if (!res.ok) {
|
|
527
|
-
const text = await res.text();
|
|
528
|
-
throw new HttpError(
|
|
529
|
-
res.status,
|
|
530
|
-
text.slice(0, 300),
|
|
531
|
-
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
let bytes = 0;
|
|
535
|
-
if (res.body) {
|
|
536
|
-
const idleTimeoutMs = transferIdleTimeoutMs();
|
|
537
|
-
resetTimeout(idleTimeoutMs);
|
|
538
|
-
const source = Readable.fromWeb(res.body);
|
|
539
|
-
const counter = new Transform({
|
|
540
|
-
transform(chunk, _encoding, callback) {
|
|
541
|
-
resetTimeout(idleTimeoutMs);
|
|
542
|
-
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
543
|
-
callback(null, chunk);
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
await pipeline(source, counter, file.createWriteStream());
|
|
547
|
-
} else {
|
|
548
|
-
await file.close();
|
|
549
|
-
}
|
|
550
|
-
complete = true;
|
|
551
|
-
return {
|
|
552
|
-
output: target,
|
|
553
|
-
bytes,
|
|
554
|
-
contentType: res.headers.get("content-type") || void 0,
|
|
555
|
-
contentDisposition: res.headers.get("content-disposition") || void 0,
|
|
556
|
-
status: res.status
|
|
557
|
-
};
|
|
558
|
-
} catch (error) {
|
|
559
|
-
if (timedOut) throw new RequestTimeoutError(activeTimeoutMs);
|
|
560
|
-
throw error;
|
|
561
|
-
} finally {
|
|
562
|
-
if (timer) clearTimeout(timer);
|
|
563
|
-
if (!complete && file) {
|
|
564
|
-
await file.close().catch(() => void 0);
|
|
565
|
-
await rm(target, { force: true }).catch(() => void 0);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
async function actionServices(opts, params = {}) {
|
|
570
|
-
const url = new URL(`${baseUrl(opts)}/v1/actions/services`);
|
|
571
|
-
if (params.page) url.searchParams.set("page", String(params.page));
|
|
572
|
-
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
573
|
-
if (params.category) url.searchParams.set("category", params.category);
|
|
574
|
-
return request(
|
|
575
|
-
url.toString(),
|
|
576
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
577
|
-
DEFAULT_TIMEOUT_MS,
|
|
578
|
-
IDEMPOTENT_RETRIES
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
async function healthCheck(opts) {
|
|
582
|
-
return request(
|
|
583
|
-
`${baseUrl(opts)}/health`,
|
|
584
|
-
{ method: "GET", headers: headers(opts.apiKey) },
|
|
585
|
-
5e3,
|
|
586
|
-
0
|
|
587
|
-
// health is a quick connectivity probe — fail fast, don't retry
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
async function loginWithApiKey(apiKey, apiHost) {
|
|
591
|
-
return request(
|
|
592
|
-
`${scheme(apiHost)}://${apiHost}/api/auth/login/apikey`,
|
|
593
|
-
{
|
|
594
|
-
method: "POST",
|
|
595
|
-
headers: { "Content-Type": "application/json" },
|
|
596
|
-
body: JSON.stringify({ apiKey })
|
|
597
|
-
},
|
|
598
|
-
DEFAULT_TIMEOUT_MS,
|
|
599
|
-
IDEMPOTENT_RETRIES
|
|
600
|
-
// auth exchange has no side effect — safe to retry
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
function jwtHeaders(jwtToken) {
|
|
604
|
-
return { "Content-Type": "application/json", Authorization: `Bearer ${jwtToken}` };
|
|
605
|
-
}
|
|
606
|
-
async function listKeys(jwtToken, apiHost) {
|
|
607
|
-
return request(
|
|
608
|
-
`${scheme(apiHost)}://${apiHost}/api/keys`,
|
|
609
|
-
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
610
|
-
DEFAULT_TIMEOUT_MS,
|
|
611
|
-
IDEMPOTENT_RETRIES
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
async function enableOAuthForKey(keyId, plaintextKey, jwtToken, apiHost) {
|
|
615
|
-
return request(
|
|
616
|
-
`${scheme(apiHost)}://${apiHost}/api/keys/${keyId}/enable-oauth`,
|
|
617
|
-
{
|
|
618
|
-
method: "POST",
|
|
619
|
-
headers: jwtHeaders(jwtToken),
|
|
620
|
-
body: JSON.stringify({ plaintextKey })
|
|
621
|
-
}
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
async function listOAuthProviders(apiHost) {
|
|
625
|
-
return request(
|
|
626
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/providers`,
|
|
627
|
-
{ method: "GET", headers: { "Content-Type": "application/json" } },
|
|
628
|
-
DEFAULT_TIMEOUT_MS,
|
|
629
|
-
IDEMPOTENT_RETRIES
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost, scopes) {
|
|
633
|
-
const body = { apiKeyId, providerId };
|
|
634
|
-
if (scopes) body.scopes = scopes;
|
|
635
|
-
return request(
|
|
636
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/authorize`,
|
|
637
|
-
{
|
|
638
|
-
method: "POST",
|
|
639
|
-
headers: jwtHeaders(jwtToken),
|
|
640
|
-
body: JSON.stringify(body)
|
|
641
|
-
}
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
async function listOAuthBindings(jwtToken, apiHost) {
|
|
645
|
-
return request(
|
|
646
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
|
|
647
|
-
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
648
|
-
DEFAULT_TIMEOUT_MS,
|
|
649
|
-
IDEMPOTENT_RETRIES
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
async function deleteOAuthBinding(bindingId, jwtToken, apiHost) {
|
|
653
|
-
const result = await request(
|
|
654
|
-
`${scheme(apiHost)}://${apiHost}/api/oauth/bindings/${bindingId}`,
|
|
655
|
-
{ method: "DELETE", headers: jwtHeaders(jwtToken) }
|
|
656
|
-
);
|
|
657
|
-
return result ?? { success: true };
|
|
658
|
-
}
|
|
2
|
+
import {
|
|
3
|
+
HttpError,
|
|
4
|
+
XAPI_API_HOST,
|
|
5
|
+
XAPI_SANDBOX_HOST,
|
|
6
|
+
__export,
|
|
7
|
+
actionBatch,
|
|
8
|
+
actionCall,
|
|
9
|
+
actionCategories,
|
|
10
|
+
actionDownload,
|
|
11
|
+
actionGet,
|
|
12
|
+
actionList,
|
|
13
|
+
actionSearch,
|
|
14
|
+
actionServices,
|
|
15
|
+
actionStream,
|
|
16
|
+
apiKeyApiRequest,
|
|
17
|
+
assertAllowedHost,
|
|
18
|
+
deleteOAuthBinding,
|
|
19
|
+
enableOAuthForKey,
|
|
20
|
+
err,
|
|
21
|
+
getApiKeySource,
|
|
22
|
+
getConfig,
|
|
23
|
+
getFormat,
|
|
24
|
+
healthCheck,
|
|
25
|
+
initiateOAuth,
|
|
26
|
+
isRetryableRequestError,
|
|
27
|
+
listKeys,
|
|
28
|
+
listOAuthBindings,
|
|
29
|
+
listOAuthProviders,
|
|
30
|
+
loginWithApiKey,
|
|
31
|
+
output,
|
|
32
|
+
request,
|
|
33
|
+
requireApiKey,
|
|
34
|
+
sandboxAudit,
|
|
35
|
+
sandboxCreate,
|
|
36
|
+
sandboxExec,
|
|
37
|
+
sandboxExtension,
|
|
38
|
+
sandboxFileList,
|
|
39
|
+
sandboxFileRead,
|
|
40
|
+
sandboxFileWrite,
|
|
41
|
+
sandboxGet,
|
|
42
|
+
sandboxHistory,
|
|
43
|
+
sandboxList,
|
|
44
|
+
sandboxOfferings,
|
|
45
|
+
sandboxPort,
|
|
46
|
+
sandboxQuote,
|
|
47
|
+
sandboxStateAction,
|
|
48
|
+
sandboxWait,
|
|
49
|
+
saveConfig,
|
|
50
|
+
scheme,
|
|
51
|
+
showConfig
|
|
52
|
+
} from "./chunk-UEQCIJ7T.js";
|
|
659
53
|
|
|
660
54
|
// src/codegen.ts
|
|
661
55
|
var TARGET_MAP = {
|
|
@@ -730,7 +124,7 @@ function validateHost(host) {
|
|
|
730
124
|
throw new Error(`invalid actionHost: "${host}" \u2014 must be a valid hostname with optional port`);
|
|
731
125
|
}
|
|
732
126
|
}
|
|
733
|
-
function
|
|
127
|
+
function baseUrl(actionHost) {
|
|
734
128
|
validateHost(actionHost);
|
|
735
129
|
return `${scheme(actionHost)}://${actionHost}/v1/actions/execute`;
|
|
736
130
|
}
|
|
@@ -746,7 +140,7 @@ function shellEscape(s) {
|
|
|
746
140
|
return s.replace(/'/g, "'\\''");
|
|
747
141
|
}
|
|
748
142
|
function genCurl(params) {
|
|
749
|
-
const url =
|
|
143
|
+
const url = baseUrl(params.actionHost);
|
|
750
144
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
751
145
|
return [
|
|
752
146
|
"# Set XAPI_KEY env var or replace with your key",
|
|
@@ -757,7 +151,7 @@ function genCurl(params) {
|
|
|
757
151
|
].join("\n");
|
|
758
152
|
}
|
|
759
153
|
function genPython(lib, params) {
|
|
760
|
-
const url =
|
|
154
|
+
const url = baseUrl(params.actionHost);
|
|
761
155
|
const payload = { action_id: params.actionId, ...params.method ? { method: params.method } : {}, input: params.input };
|
|
762
156
|
return [
|
|
763
157
|
`# pip install ${lib}`,
|
|
@@ -777,7 +171,7 @@ function genPython(lib, params) {
|
|
|
777
171
|
].join("\n");
|
|
778
172
|
}
|
|
779
173
|
function genJavaScriptFetch(params) {
|
|
780
|
-
const url =
|
|
174
|
+
const url = baseUrl(params.actionHost);
|
|
781
175
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
782
176
|
return [
|
|
783
177
|
"// Set XAPI_KEY env var or replace with your key",
|
|
@@ -793,7 +187,7 @@ function genJavaScriptFetch(params) {
|
|
|
793
187
|
].join("\n");
|
|
794
188
|
}
|
|
795
189
|
function genJavaScriptAxios(params) {
|
|
796
|
-
const url =
|
|
190
|
+
const url = baseUrl(params.actionHost);
|
|
797
191
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
798
192
|
return [
|
|
799
193
|
"// npm install axios",
|
|
@@ -814,7 +208,7 @@ function genJavaScriptAxios(params) {
|
|
|
814
208
|
].join("\n");
|
|
815
209
|
}
|
|
816
210
|
function genTypescriptFetch(params) {
|
|
817
|
-
const url =
|
|
211
|
+
const url = baseUrl(params.actionHost);
|
|
818
212
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
819
213
|
return [
|
|
820
214
|
"// Set XAPI_KEY env var or replace with your key",
|
|
@@ -831,7 +225,7 @@ function genTypescriptFetch(params) {
|
|
|
831
225
|
].join("\n");
|
|
832
226
|
}
|
|
833
227
|
function genGo(params) {
|
|
834
|
-
const url =
|
|
228
|
+
const url = baseUrl(params.actionHost);
|
|
835
229
|
const body = jsonBody(params.actionId, params.input, params.method);
|
|
836
230
|
const escaped = body.replace(/`/g, '` + "`" + `');
|
|
837
231
|
return [
|
|
@@ -1152,7 +546,8 @@ async function actionSearch2(args, flags) {
|
|
|
1152
546
|
category: flags.category,
|
|
1153
547
|
page: positiveIntegerFlag(flags.page, "--page"),
|
|
1154
548
|
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
1155
|
-
|
|
549
|
+
// Backward-compatible aliases from the original feature branch.
|
|
550
|
+
include_all_versions: ["include-all-versions", "all-versions", "include-history"].some((name) => flags[name] === "true"),
|
|
1156
551
|
sort: requestedSort
|
|
1157
552
|
});
|
|
1158
553
|
if (requestedSort && res.sort !== requestedSort) {
|
|
@@ -1339,7 +734,7 @@ __export(config_exports, {
|
|
|
1339
734
|
configSet: () => configSet,
|
|
1340
735
|
configShow: () => configShow
|
|
1341
736
|
});
|
|
1342
|
-
import { readFileSync
|
|
737
|
+
import { readFileSync } from "fs";
|
|
1343
738
|
var CONFIG_HELP = `xapi-to config - Manage CLI configuration
|
|
1344
739
|
|
|
1345
740
|
USAGE
|
|
@@ -1382,7 +777,7 @@ async function configSet(args, flags) {
|
|
|
1382
777
|
if (key !== "apiKey") err(`unknown config key: ${key} (only apiKey is configurable)`);
|
|
1383
778
|
let value = arg.slice(eq + 1);
|
|
1384
779
|
if (value === "-") {
|
|
1385
|
-
value =
|
|
780
|
+
value = readFileSync(0, "utf-8").trim();
|
|
1386
781
|
}
|
|
1387
782
|
if (!value) err("apiKey is empty");
|
|
1388
783
|
updates.apiKey = value;
|
|
@@ -1576,43 +971,240 @@ async function balance(args, flags) {
|
|
|
1576
971
|
}
|
|
1577
972
|
}
|
|
1578
973
|
|
|
1579
|
-
// src/commands/
|
|
1580
|
-
var
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
974
|
+
// src/commands/usage.ts
|
|
975
|
+
var READ_RETRIES = 2;
|
|
976
|
+
var USAGE_HELP = `xapi-to usage - Read a finalized request cost receipt
|
|
977
|
+
|
|
978
|
+
USAGE
|
|
979
|
+
xapi-to usage <request-id> [--format json|pretty|table]
|
|
980
|
+
xapi-to usage wait <request-id> [--interval 1s] [--timeout 30s]
|
|
981
|
+
|
|
982
|
+
Use the request ID returned in X-XAPI-Request-Id or the final xapi.usage SSE event.
|
|
983
|
+
The receipt is visible only to the API key that made the request.
|
|
984
|
+
|
|
985
|
+
"usage wait" polls through the normal finalization window. A 404 means the
|
|
986
|
+
receipt is not finalized yet; invalid credentials and other permanent errors
|
|
987
|
+
still fail immediately.
|
|
988
|
+
`;
|
|
989
|
+
function parsePositiveDurationMs(raw, flagName) {
|
|
990
|
+
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h)?$/);
|
|
991
|
+
if (!match) {
|
|
992
|
+
err(`${flagName} must be a duration such as 500ms, 2s, 5m, or 1h`);
|
|
993
|
+
}
|
|
994
|
+
const value = Number(match[1]);
|
|
995
|
+
const unit = match[2] || "ms";
|
|
996
|
+
const multiplier = unit === "h" ? 36e5 : unit === "m" ? 6e4 : unit === "s" ? 1e3 : 1;
|
|
997
|
+
const result = value * multiplier;
|
|
998
|
+
if (!Number.isSafeInteger(result) || result <= 0) {
|
|
999
|
+
err(`${flagName} must be greater than 0`);
|
|
1596
1000
|
}
|
|
1001
|
+
return result;
|
|
1597
1002
|
}
|
|
1598
|
-
function
|
|
1599
|
-
|
|
1600
|
-
if (!Number.isFinite(changedAt)) return !existingBindingIds.has(binding.id);
|
|
1601
|
-
return changedAt >= startedAtMs;
|
|
1003
|
+
function sleep(ms) {
|
|
1004
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1602
1005
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1006
|
+
function receiptUrl(requestId) {
|
|
1007
|
+
return `${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/usage/requests/${encodeURIComponent(requestId)}`;
|
|
1008
|
+
}
|
|
1009
|
+
async function fetchReceipt(requestId, apiKey, timeoutMs, retries) {
|
|
1010
|
+
return request(
|
|
1011
|
+
receiptUrl(requestId),
|
|
1012
|
+
{
|
|
1013
|
+
method: "GET",
|
|
1014
|
+
headers: { "XAPI-KEY": apiKey }
|
|
1015
|
+
},
|
|
1016
|
+
timeoutMs,
|
|
1017
|
+
retries
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
async function waitForReceipt(requestId, apiKey, flags) {
|
|
1021
|
+
const intervalMs = parsePositiveDurationMs(flags.interval || "1s", "--interval");
|
|
1022
|
+
const timeoutMs = parsePositiveDurationMs(flags.timeout || "30s", "--timeout");
|
|
1023
|
+
const startedAt = Date.now();
|
|
1024
|
+
const deadline = startedAt + timeoutMs;
|
|
1025
|
+
while (true) {
|
|
1026
|
+
const remainingMs = deadline - Date.now();
|
|
1027
|
+
if (remainingMs <= 0) {
|
|
1028
|
+
err(
|
|
1029
|
+
"usage receipt wait timeout",
|
|
1030
|
+
`request_id=${requestId}, elapsed_ms=${Date.now() - startedAt}, timeout_ms=${timeoutMs}`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1609
1033
|
try {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
)
|
|
1614
|
-
|
|
1615
|
-
|
|
1034
|
+
return await fetchReceipt(requestId, apiKey, remainingMs, 0);
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
const pending = e instanceof HttpError && e.status === 404;
|
|
1037
|
+
if (!pending && !isRetryableRequestError(e)) throw e;
|
|
1038
|
+
}
|
|
1039
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
async function usage(args, flags) {
|
|
1043
|
+
if (flags.help) {
|
|
1044
|
+
console.log(USAGE_HELP);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const shouldWait = args[0] === "wait";
|
|
1048
|
+
const requestId = args[shouldWait ? 1 : 0]?.trim();
|
|
1049
|
+
if (!requestId) {
|
|
1050
|
+
err(
|
|
1051
|
+
"request ID required",
|
|
1052
|
+
shouldWait ? "Run: xapi-to usage wait <request-id>" : "Run: xapi-to usage <request-id>"
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
const cfg = getConfig();
|
|
1056
|
+
requireApiKey(cfg);
|
|
1057
|
+
try {
|
|
1058
|
+
const result = shouldWait ? await waitForReceipt(requestId, cfg.apiKey, flags) : await fetchReceipt(requestId, cfg.apiKey, 3e4, READ_RETRIES);
|
|
1059
|
+
output(result, flags.format);
|
|
1060
|
+
} catch (e) {
|
|
1061
|
+
err(
|
|
1062
|
+
shouldWait ? "usage receipt wait failed" : "usage receipt fetch failed",
|
|
1063
|
+
e.message
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/commands/earnings.ts
|
|
1069
|
+
var READ_RETRIES2 = 2;
|
|
1070
|
+
var EARNINGS_HELP = `xapi-to earnings - Inspect and reinvest provider earnings
|
|
1071
|
+
|
|
1072
|
+
USAGE
|
|
1073
|
+
xapi-to earnings [summary] [--format json|pretty|table]
|
|
1074
|
+
xapi-to earnings list [--status PENDING|SETTLED] [--limit 20] [--cursor <id>]
|
|
1075
|
+
xapi-to earnings transfer <amount> --idempotency-key <key>
|
|
1076
|
+
|
|
1077
|
+
SCOPES
|
|
1078
|
+
summary/list earnings:read
|
|
1079
|
+
transfer earnings:transfer
|
|
1080
|
+
|
|
1081
|
+
The transfer is one-way: settled provider earnings become spendable xapi balance.
|
|
1082
|
+
Reuse the same idempotency key only when retrying the same amount.
|
|
1083
|
+
`;
|
|
1084
|
+
function baseUrl2() {
|
|
1085
|
+
return `${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/agent`;
|
|
1086
|
+
}
|
|
1087
|
+
function keyHeaders(apiKey) {
|
|
1088
|
+
return { "Content-Type": "application/json", "XAPI-KEY": apiKey };
|
|
1089
|
+
}
|
|
1090
|
+
async function earnings(args, flags) {
|
|
1091
|
+
if (flags.help) {
|
|
1092
|
+
console.log(EARNINGS_HELP);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const cfg = getConfig();
|
|
1096
|
+
requireApiKey(cfg);
|
|
1097
|
+
const apiKey = cfg.apiKey;
|
|
1098
|
+
const subcommand = args[0] ?? "summary";
|
|
1099
|
+
try {
|
|
1100
|
+
if (subcommand === "summary") {
|
|
1101
|
+
const result = await request(
|
|
1102
|
+
`${baseUrl2()}/economy`,
|
|
1103
|
+
{ method: "GET", headers: keyHeaders(apiKey) },
|
|
1104
|
+
3e4,
|
|
1105
|
+
READ_RETRIES2
|
|
1106
|
+
);
|
|
1107
|
+
output(result, flags.format);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (subcommand === "list") {
|
|
1111
|
+
const url = new URL(`${baseUrl2()}/earnings`);
|
|
1112
|
+
if (flags.status) {
|
|
1113
|
+
const status = flags.status.toUpperCase();
|
|
1114
|
+
if (!["PENDING", "SETTLED"].includes(status)) {
|
|
1115
|
+
err("invalid earnings status", "Expected PENDING or SETTLED.");
|
|
1116
|
+
}
|
|
1117
|
+
url.searchParams.set("status", status);
|
|
1118
|
+
}
|
|
1119
|
+
if (flags.limit) {
|
|
1120
|
+
const limit = Number(flags.limit);
|
|
1121
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
1122
|
+
err("invalid earnings limit", "Expected an integer from 1 to 100.");
|
|
1123
|
+
}
|
|
1124
|
+
url.searchParams.set("limit", String(limit));
|
|
1125
|
+
}
|
|
1126
|
+
if (flags.cursor) url.searchParams.set("cursor", flags.cursor);
|
|
1127
|
+
const result = await request(
|
|
1128
|
+
url.toString(),
|
|
1129
|
+
{ method: "GET", headers: keyHeaders(apiKey) },
|
|
1130
|
+
3e4,
|
|
1131
|
+
READ_RETRIES2
|
|
1132
|
+
);
|
|
1133
|
+
output(result, flags.format);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (subcommand === "transfer") {
|
|
1137
|
+
const amount = Number(args[1]);
|
|
1138
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
1139
|
+
err("invalid transfer amount", "Pass a positive USD amount.");
|
|
1140
|
+
}
|
|
1141
|
+
const idempotencyKey = flags["idempotency-key"] || flags.idempotencyKey;
|
|
1142
|
+
if (!idempotencyKey) {
|
|
1143
|
+
err(
|
|
1144
|
+
"idempotency key required",
|
|
1145
|
+
"Pass --idempotency-key <stable-key> and reuse it only when retrying this same transfer."
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
const result = await request(
|
|
1149
|
+
`${baseUrl2()}/earnings/transfer`,
|
|
1150
|
+
{
|
|
1151
|
+
method: "POST",
|
|
1152
|
+
headers: keyHeaders(apiKey),
|
|
1153
|
+
body: JSON.stringify({ amount, idempotencyKey })
|
|
1154
|
+
},
|
|
1155
|
+
3e4,
|
|
1156
|
+
// The server binds the idempotency key to the amount, so transport retries are safe.
|
|
1157
|
+
READ_RETRIES2
|
|
1158
|
+
);
|
|
1159
|
+
output(result, flags.format);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
err(
|
|
1163
|
+
`unknown earnings command: ${subcommand}`,
|
|
1164
|
+
"Valid commands: summary, list, transfer."
|
|
1165
|
+
);
|
|
1166
|
+
} catch (e) {
|
|
1167
|
+
err("earnings request failed", e.message);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/commands/oauth.ts
|
|
1172
|
+
var oauth_exports = {};
|
|
1173
|
+
__export(oauth_exports, {
|
|
1174
|
+
OAUTH_HELP: () => OAUTH_HELP,
|
|
1175
|
+
oauthBind: () => oauthBind,
|
|
1176
|
+
oauthProviders: () => oauthProviders,
|
|
1177
|
+
oauthStatus: () => oauthStatus,
|
|
1178
|
+
oauthUnbind: () => oauthUnbind,
|
|
1179
|
+
pollForBinding: () => pollForBinding
|
|
1180
|
+
});
|
|
1181
|
+
import { spawnSync } from "child_process";
|
|
1182
|
+
function openBrowser(url) {
|
|
1183
|
+
const cmd = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
1184
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
1185
|
+
try {
|
|
1186
|
+
spawnSync(cmd, args, { stdio: "ignore" });
|
|
1187
|
+
} catch {
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function bindingChangedAfter(binding, startedAtMs, existingBindingIds) {
|
|
1191
|
+
const changedAt = Date.parse(binding.updatedAt || binding.createdAt || "");
|
|
1192
|
+
if (!Number.isFinite(changedAt)) return !existingBindingIds.has(binding.id);
|
|
1193
|
+
return changedAt >= startedAtMs;
|
|
1194
|
+
}
|
|
1195
|
+
async function pollForBinding(apiKeyId, providerId, jwtToken, startedAt, existingBindingIds = /* @__PURE__ */ new Set(), timeoutMs = 5 * 60 * 1e3, intervalMs = 3e3) {
|
|
1196
|
+
const deadline = Date.now() + timeoutMs;
|
|
1197
|
+
const isTTY = process.stdout.isTTY;
|
|
1198
|
+
const startedAtMs = startedAt.getTime() - 5e3;
|
|
1199
|
+
while (Date.now() < deadline) {
|
|
1200
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1201
|
+
try {
|
|
1202
|
+
const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
|
|
1203
|
+
const match = Array.isArray(bindings) ? bindings.find(
|
|
1204
|
+
(b) => b.apiKeyId === apiKeyId && b.providerId === providerId && bindingChangedAfter(b, startedAtMs, existingBindingIds)
|
|
1205
|
+
) : null;
|
|
1206
|
+
if (match) return match;
|
|
1207
|
+
} catch {
|
|
1616
1208
|
}
|
|
1617
1209
|
if (isTTY) {
|
|
1618
1210
|
const remaining = Math.ceil((deadline - Date.now()) / 1e3);
|
|
@@ -1644,11 +1236,11 @@ async function findCurrentKeyRecord(plaintextKey, jwtToken) {
|
|
|
1644
1236
|
`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.`
|
|
1645
1237
|
);
|
|
1646
1238
|
}
|
|
1647
|
-
function resolveScopeDefs(
|
|
1648
|
-
if (Array.isArray(
|
|
1649
|
-
return
|
|
1239
|
+
function resolveScopeDefs(provider2) {
|
|
1240
|
+
if (Array.isArray(provider2.scopeDefinitions) && provider2.scopeDefinitions.length > 0) {
|
|
1241
|
+
return provider2.scopeDefinitions;
|
|
1650
1242
|
}
|
|
1651
|
-
const raw = (
|
|
1243
|
+
const raw = (provider2.defaultScopes || "").split(/[\s,]+/).filter(Boolean);
|
|
1652
1244
|
return raw.map((s) => ({
|
|
1653
1245
|
scope: s,
|
|
1654
1246
|
label: s,
|
|
@@ -1657,21 +1249,21 @@ function resolveScopeDefs(provider) {
|
|
|
1657
1249
|
category: ""
|
|
1658
1250
|
}));
|
|
1659
1251
|
}
|
|
1660
|
-
async function selectScopesInteractive(
|
|
1661
|
-
const defs = resolveScopeDefs(
|
|
1252
|
+
async function selectScopesInteractive(provider2) {
|
|
1253
|
+
const defs = resolveScopeDefs(provider2);
|
|
1662
1254
|
if (defs.length === 0) return "";
|
|
1663
|
-
const
|
|
1255
|
+
const required3 = defs.filter((d) => d.required);
|
|
1664
1256
|
const optional = defs.filter((d) => !d.required);
|
|
1665
1257
|
const selected = new Set(defs.map((d) => d.scope));
|
|
1666
1258
|
if (optional.length === 0) {
|
|
1667
|
-
return
|
|
1259
|
+
return required3.map((d) => d.scope).join(" ");
|
|
1668
1260
|
}
|
|
1669
1261
|
const out = process.stderr;
|
|
1670
1262
|
let cursor = 0;
|
|
1671
1263
|
const hint = " \u2191\u2193 navigate \xB7 space toggle \xB7 a all \xB7 n none \xB7 enter confirm";
|
|
1672
1264
|
const buildFrame = () => {
|
|
1673
1265
|
const lines = [];
|
|
1674
|
-
for (const d of
|
|
1266
|
+
for (const d of required3) {
|
|
1675
1267
|
const desc = d.description ? ` \u2014 ${d.description}` : "";
|
|
1676
1268
|
lines.push(` \x1B[2m[*] ${d.label}${desc} (required)\x1B[0m`);
|
|
1677
1269
|
}
|
|
@@ -1694,7 +1286,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1694
1286
|
out.write("\x1B[J");
|
|
1695
1287
|
out.write(buildFrame());
|
|
1696
1288
|
};
|
|
1697
|
-
return new Promise((
|
|
1289
|
+
return new Promise((resolve4) => {
|
|
1698
1290
|
const { stdin } = process;
|
|
1699
1291
|
const wasRaw = stdin.isRaw;
|
|
1700
1292
|
stdin.setRawMode(true);
|
|
@@ -1705,7 +1297,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1705
1297
|
stdin.pause();
|
|
1706
1298
|
out.write("\x1B[?25h");
|
|
1707
1299
|
out.write("\n");
|
|
1708
|
-
|
|
1300
|
+
resolve4(result);
|
|
1709
1301
|
};
|
|
1710
1302
|
const onData = (buf) => {
|
|
1711
1303
|
const key = buf.toString();
|
|
@@ -1784,10 +1376,10 @@ async function oauthBind(args, flags) {
|
|
|
1784
1376
|
if (!Array.isArray(providers) || providers.length === 0) {
|
|
1785
1377
|
throw new Error("No OAuth providers available");
|
|
1786
1378
|
}
|
|
1787
|
-
const
|
|
1379
|
+
const provider2 = providers.find(
|
|
1788
1380
|
(p) => p.type.toLowerCase() === providerName || p.name.toLowerCase().includes(providerName)
|
|
1789
1381
|
);
|
|
1790
|
-
if (!
|
|
1382
|
+
if (!provider2) {
|
|
1791
1383
|
const available = providers.map((p) => p.type).join(", ");
|
|
1792
1384
|
throw new Error(
|
|
1793
1385
|
`Provider "${providerName}" not found. Available: ${available}`
|
|
@@ -1801,13 +1393,13 @@ async function oauthBind(args, flags) {
|
|
|
1801
1393
|
if (flags.scopes) {
|
|
1802
1394
|
scopes = flags.scopes;
|
|
1803
1395
|
} else if (isTTY) {
|
|
1804
|
-
const defs = resolveScopeDefs(
|
|
1396
|
+
const defs = resolveScopeDefs(provider2);
|
|
1805
1397
|
if (defs.length > 0) {
|
|
1806
1398
|
console.error(`
|
|
1807
|
-
Provider : ${
|
|
1399
|
+
Provider : ${provider2.name}`);
|
|
1808
1400
|
console.error(` API Key : ${keyRecord.keyPreview}`);
|
|
1809
1401
|
headerPrinted = true;
|
|
1810
|
-
scopes = await selectScopesInteractive(
|
|
1402
|
+
scopes = await selectScopesInteractive(provider2) || void 0;
|
|
1811
1403
|
}
|
|
1812
1404
|
}
|
|
1813
1405
|
const existingBindingIds = /* @__PURE__ */ new Set();
|
|
@@ -1816,7 +1408,7 @@ async function oauthBind(args, flags) {
|
|
|
1816
1408
|
const existingBindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
|
|
1817
1409
|
if (Array.isArray(existingBindings)) {
|
|
1818
1410
|
for (const binding of existingBindings) {
|
|
1819
|
-
if (binding.apiKeyId === keyRecord.id && binding.providerId ===
|
|
1411
|
+
if (binding.apiKeyId === keyRecord.id && binding.providerId === provider2.id) {
|
|
1820
1412
|
existingBindingIds.add(binding.id);
|
|
1821
1413
|
}
|
|
1822
1414
|
}
|
|
@@ -1825,7 +1417,7 @@ async function oauthBind(args, flags) {
|
|
|
1825
1417
|
}
|
|
1826
1418
|
}
|
|
1827
1419
|
const authorizationStartedAt = /* @__PURE__ */ new Date();
|
|
1828
|
-
const result = await initiateOAuth(keyRecord.id,
|
|
1420
|
+
const result = await initiateOAuth(keyRecord.id, provider2.id, jwtToken, XAPI_API_HOST, scopes);
|
|
1829
1421
|
const { authorizationUrl } = result;
|
|
1830
1422
|
let authorizationTarget;
|
|
1831
1423
|
try {
|
|
@@ -1840,7 +1432,7 @@ async function oauthBind(args, flags) {
|
|
|
1840
1432
|
if (isTTY) {
|
|
1841
1433
|
if (!headerPrinted) {
|
|
1842
1434
|
console.error(`
|
|
1843
|
-
Provider : ${
|
|
1435
|
+
Provider : ${provider2.name}`);
|
|
1844
1436
|
console.error(` API Key : ${keyRecord.keyPreview}`);
|
|
1845
1437
|
}
|
|
1846
1438
|
if (scopes) {
|
|
@@ -1855,7 +1447,7 @@ async function oauthBind(args, flags) {
|
|
|
1855
1447
|
console.error(" Waiting for you to complete authorization in the browser...\n");
|
|
1856
1448
|
const binding = await pollForBinding(
|
|
1857
1449
|
keyRecord.id,
|
|
1858
|
-
|
|
1450
|
+
provider2.id,
|
|
1859
1451
|
jwtToken,
|
|
1860
1452
|
authorizationStartedAt,
|
|
1861
1453
|
existingBindingIds
|
|
@@ -1866,14 +1458,14 @@ async function oauthBind(args, flags) {
|
|
|
1866
1458
|
console.error(`
|
|
1867
1459
|
Authorization complete! Bound to @${account}
|
|
1868
1460
|
`);
|
|
1869
|
-
output({ status: "success", provider:
|
|
1461
|
+
output({ status: "success", provider: provider2.name, account, scopes }, flags.format);
|
|
1870
1462
|
} else {
|
|
1871
1463
|
err("oauth bind timed out", 'Authorization was not completed within 5 minutes. Run "xapi-to oauth bind" again.');
|
|
1872
1464
|
}
|
|
1873
1465
|
} else {
|
|
1874
1466
|
output({
|
|
1875
1467
|
status: "pending",
|
|
1876
|
-
provider:
|
|
1468
|
+
provider: provider2.name,
|
|
1877
1469
|
apiKey: keyRecord.keyPreview,
|
|
1878
1470
|
authorizationUrl,
|
|
1879
1471
|
scopes
|
|
@@ -2009,7 +1601,7 @@ function parseDurationMs(raw) {
|
|
|
2009
1601
|
return value;
|
|
2010
1602
|
}
|
|
2011
1603
|
}
|
|
2012
|
-
function
|
|
1604
|
+
function parsePositiveDurationMs2(raw, flagName) {
|
|
2013
1605
|
const ms = parseDurationMs(raw);
|
|
2014
1606
|
if (ms <= 0) {
|
|
2015
1607
|
err(`${flagName} must be greater than 0`);
|
|
@@ -2025,7 +1617,7 @@ function parsePositiveInt(raw, flagName) {
|
|
|
2025
1617
|
}
|
|
2026
1618
|
function sleep2(ms) {
|
|
2027
1619
|
if (ms <= 0) return Promise.resolve();
|
|
2028
|
-
return new Promise((
|
|
1620
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
2029
1621
|
}
|
|
2030
1622
|
function extractTaskPayload(res) {
|
|
2031
1623
|
if (res && typeof res === "object") {
|
|
@@ -2063,7 +1655,7 @@ async function taskWait(args, flags) {
|
|
|
2063
1655
|
showHelpIfRequested2(flags, WAIT_HELP);
|
|
2064
1656
|
const taskId = args[0];
|
|
2065
1657
|
if (!taskId) err("usage: xapi-to task wait <task_id>");
|
|
2066
|
-
const intervalMs =
|
|
1658
|
+
const intervalMs = parsePositiveDurationMs2(flags.interval || "2s", "--interval");
|
|
2067
1659
|
const timeoutMs = flags.timeout ? parseDurationMs(flags.timeout) : void 0;
|
|
2068
1660
|
const maxAttempts = flags["max-attempts"] ? parsePositiveInt(flags["max-attempts"], "--max-attempts") : void 0;
|
|
2069
1661
|
const cfg = getConfig();
|
|
@@ -2127,6 +1719,1400 @@ function taskHelp() {
|
|
|
2127
1719
|
return TASK_HELP;
|
|
2128
1720
|
}
|
|
2129
1721
|
|
|
1722
|
+
// src/commands/sandbox.ts
|
|
1723
|
+
import { randomUUID } from "crypto";
|
|
1724
|
+
import { readFile, open, rm } from "fs/promises";
|
|
1725
|
+
import { resolve } from "path";
|
|
1726
|
+
var SANDBOX_HELP = `xapi-to sandbox - Managed, auditable cloud sandboxes
|
|
1727
|
+
|
|
1728
|
+
USAGE
|
|
1729
|
+
xapi-to sandbox <command> [args] [flags]
|
|
1730
|
+
|
|
1731
|
+
QUICK START
|
|
1732
|
+
sandbox run --command <shell> Quote, create, wait, execute, and terminate
|
|
1733
|
+
sandbox run -- <command...> Positional shorthand after a bare --
|
|
1734
|
+
|
|
1735
|
+
LIFECYCLE
|
|
1736
|
+
offerings List available provider offerings
|
|
1737
|
+
quote Quote requirements without creating
|
|
1738
|
+
list List sandbox instances
|
|
1739
|
+
history Search paginated instance history
|
|
1740
|
+
get <id> Get instance state, usage, and cost
|
|
1741
|
+
create Create from a quote, offering, or requirements
|
|
1742
|
+
wait <id> Wait for a state (default: RUNNING)
|
|
1743
|
+
exec <id> --command <shell> Execute a shell command
|
|
1744
|
+
suspend|resume|terminate <id> Change state and wait for completion
|
|
1745
|
+
|
|
1746
|
+
FILES, PORTS, AUDIT
|
|
1747
|
+
file write <id> <remote> --file <local>
|
|
1748
|
+
file write <id> <remote> --content <text>
|
|
1749
|
+
file read <id> <remote> [--output <local>]
|
|
1750
|
+
file list <id> [--path <remote>] [--depth N]
|
|
1751
|
+
port <id> <port> Get a public URL for a listening port
|
|
1752
|
+
extension <id> <extension-id> Invoke an offering-declared extension
|
|
1753
|
+
audit <id> [--kind operations|events|usageSegments|billingPeriods]
|
|
1754
|
+
|
|
1755
|
+
SELECTION FLAGS
|
|
1756
|
+
--provider auto|daytona|cf-edge|e2b|runpod|runloop|modal|vc-sandbox|fly|blaxel|cubesandbox
|
|
1757
|
+
Pin a provider gateway (default: auto)
|
|
1758
|
+
--capabilities exec,files,ports Required capabilities
|
|
1759
|
+
--cpu N --memory N --volume N Minimum resources
|
|
1760
|
+
--gpu-count N --gpu-model NAME GPU requirements
|
|
1761
|
+
--regions a,b Allowed regions
|
|
1762
|
+
--min-runtime 24h Minimum documented continuous runtime
|
|
1763
|
+
--requirements <json> Complete requirements object
|
|
1764
|
+
--max-hourly-usd N Price ceiling (sandbox run default: 0.20)
|
|
1765
|
+
|
|
1766
|
+
COMMON FLAGS
|
|
1767
|
+
--host <sandbox.xapi.to> Override gateway; must be *.xapi.to/localhost
|
|
1768
|
+
--wait-timeout 5m State wait timeout (default: 5m)
|
|
1769
|
+
--interval 2s State polling interval (default: 2s)
|
|
1770
|
+
--format json|pretty|table Output format
|
|
1771
|
+
|
|
1772
|
+
HISTORY FLAGS
|
|
1773
|
+
--state ALL|ACTIVE|HISTORY|RUNNING|SUSPENDED|TERMINATED|FAILED
|
|
1774
|
+
--search <text> --from <ISO time> --to <ISO time>
|
|
1775
|
+
--page N --page-size N Pagination (page size: 1-100)
|
|
1776
|
+
|
|
1777
|
+
SAFETY
|
|
1778
|
+
sandbox run terminates in finally, including command failure. Use --keep only
|
|
1779
|
+
when you intentionally want billing to continue after the CLI exits.
|
|
1780
|
+
|
|
1781
|
+
EXAMPLES
|
|
1782
|
+
xapi-to sandbox offerings --format table
|
|
1783
|
+
xapi-to sandbox quote --capabilities exec,files --max-hourly-usd 0.20
|
|
1784
|
+
xapi-to sandbox run --command 'python3 -c "print(6*7)"'
|
|
1785
|
+
xapi-to sandbox run --provider cf-edge --capabilities exec,files,ports --command 'pwd'
|
|
1786
|
+
xapi-to sandbox exec <id> -- npm test
|
|
1787
|
+
xapi-to sandbox extension <id> runpod.connection_info --input '{}'
|
|
1788
|
+
xapi-to sandbox terminate <id>
|
|
1789
|
+
`;
|
|
1790
|
+
var SANDBOX_COMMAND_HELP = {
|
|
1791
|
+
offerings: `USAGE
|
|
1792
|
+
xapi-to sandbox offerings [--provider NAME] [--format json|pretty|table]
|
|
1793
|
+
|
|
1794
|
+
Lists current resources, capabilities, lifecycle support, and hourly prices.
|
|
1795
|
+
This command does not create or bill an instance.`,
|
|
1796
|
+
quote: `USAGE
|
|
1797
|
+
xapi-to sandbox quote [selection flags] [--max-hourly-usd N]
|
|
1798
|
+
|
|
1799
|
+
SELECTION
|
|
1800
|
+
--capabilities exec,files,ports Required capabilities
|
|
1801
|
+
--cpu N --memory N --volume N Minimum resources
|
|
1802
|
+
--gpu-count N --gpu-model NAME GPU requirements
|
|
1803
|
+
--regions a,b Allowed regions
|
|
1804
|
+
--min-runtime 24h Minimum documented continuous runtime
|
|
1805
|
+
--requirements <json> Complete requirements object
|
|
1806
|
+
--max-hourly-usd N Hard hourly price ceiling
|
|
1807
|
+
|
|
1808
|
+
Returns a short-lived quote without creating or billing an instance.`,
|
|
1809
|
+
list: `USAGE
|
|
1810
|
+
xapi-to sandbox list [--provider NAME] [--format json|pretty|table]
|
|
1811
|
+
|
|
1812
|
+
Lists current Sandbox instances visible to the configured xAPI key.`,
|
|
1813
|
+
history: `USAGE
|
|
1814
|
+
xapi-to sandbox history [--state STATE] [--search TEXT] [--from ISO] [--to ISO]
|
|
1815
|
+
[--page N] [--page-size 1-100] [--format json|pretty|table]
|
|
1816
|
+
|
|
1817
|
+
Searches current and historical Sandbox instances with server-side pagination.`,
|
|
1818
|
+
get: `USAGE
|
|
1819
|
+
xapi-to sandbox get <id> [--format json|pretty|table]
|
|
1820
|
+
|
|
1821
|
+
Returns current state, operations, usage, billing, and service-calculated cost.`,
|
|
1822
|
+
create: `USAGE
|
|
1823
|
+
xapi-to sandbox create [selection flags] [--max-hourly-usd N] [--wait]
|
|
1824
|
+
|
|
1825
|
+
SELECTION MODES
|
|
1826
|
+
--quote-id ID Create from an existing quote
|
|
1827
|
+
--offering-id ID Create an exact offering (cannot use a price ceiling)
|
|
1828
|
+
--requirements <json> Create from requirements
|
|
1829
|
+
--capabilities/--cpu/--memory/... Requirements shortcuts
|
|
1830
|
+
|
|
1831
|
+
CONTROL
|
|
1832
|
+
--idempotency-key KEY Stable retry key; generated and returned if omitted
|
|
1833
|
+
--metadata <json> Instance metadata
|
|
1834
|
+
--resume-on-access Request automatic resume on supported providers
|
|
1835
|
+
--wait Wait until RUNNING
|
|
1836
|
+
--wait-timeout 5m --interval 2s Polling controls
|
|
1837
|
+
|
|
1838
|
+
On a wait failure, the error includes the instance ID and recovery instructions.`,
|
|
1839
|
+
wait: `USAGE
|
|
1840
|
+
xapi-to sandbox wait <id> [--state RUNNING[,STATE]]
|
|
1841
|
+
[--wait-timeout 5m] [--interval 2s]
|
|
1842
|
+
|
|
1843
|
+
State names are case-insensitive and validated before polling.`,
|
|
1844
|
+
exec: `USAGE
|
|
1845
|
+
xapi-to sandbox exec <id> --command <shell> [--cwd PATH] [--timeout SECONDS]
|
|
1846
|
+
[--background]
|
|
1847
|
+
xapi-to sandbox exec <id> -- <command...>
|
|
1848
|
+
|
|
1849
|
+
Executes a command and maps a remote non-zero exit code to the local process.
|
|
1850
|
+
--background requires offering.capabilities.backgroundExec=true and returns a
|
|
1851
|
+
provider-managed session immediately; use it for long-running Web servers.`,
|
|
1852
|
+
file: `USAGE
|
|
1853
|
+
xapi-to sandbox file write <id> <remote> (--file <local>|--content <text>)
|
|
1854
|
+
xapi-to sandbox file read <id> <remote> [--output <local>]
|
|
1855
|
+
xapi-to sandbox file list <id> [--path <remote>] [--depth N]
|
|
1856
|
+
|
|
1857
|
+
Binary local files are transferred as base64. --output never overwrites a file.`,
|
|
1858
|
+
port: `USAGE
|
|
1859
|
+
xapi-to sandbox port <id> <1-65535>
|
|
1860
|
+
|
|
1861
|
+
Returns the provider's temporary public URL for a listening instance port.`,
|
|
1862
|
+
extension: `USAGE
|
|
1863
|
+
xapi-to sandbox extension <id> <extension-id> [--input <json>]
|
|
1864
|
+
[--idempotency-key KEY]
|
|
1865
|
+
|
|
1866
|
+
Invoke only extension IDs declared by the selected offering.`,
|
|
1867
|
+
audit: `USAGE
|
|
1868
|
+
xapi-to sandbox audit <id> [--kind operations|events|usageSegments|billingPeriods]
|
|
1869
|
+
[--page N] [--page-size 1-100] [--format json|pretty|table]`,
|
|
1870
|
+
suspend: `USAGE
|
|
1871
|
+
xapi-to sandbox suspend <id> [--no-wait] [--idempotency-key KEY]
|
|
1872
|
+
[--wait-timeout 5m] [--interval 2s]
|
|
1873
|
+
|
|
1874
|
+
Check offering lifecycle support before suspending; storage may continue billing.`,
|
|
1875
|
+
resume: `USAGE
|
|
1876
|
+
xapi-to sandbox resume <id> [--no-wait] [--idempotency-key KEY]
|
|
1877
|
+
[--wait-timeout 5m] [--interval 2s]`,
|
|
1878
|
+
terminate: `USAGE
|
|
1879
|
+
xapi-to sandbox terminate <id> [--no-wait] [--idempotency-key KEY]
|
|
1880
|
+
[--wait-timeout 5m] [--interval 2s]
|
|
1881
|
+
|
|
1882
|
+
Waits for TERMINATED or FAILED by default.`,
|
|
1883
|
+
run: `USAGE
|
|
1884
|
+
xapi-to sandbox run --command <shell> [selection flags] [run flags]
|
|
1885
|
+
xapi-to sandbox run [selection flags] -- <command...>
|
|
1886
|
+
|
|
1887
|
+
RUN FLAGS
|
|
1888
|
+
--max-hourly-usd N Hard ceiling (default: 0.20)
|
|
1889
|
+
--timeout SECONDS Remote command timeout (default: 60)
|
|
1890
|
+
--cwd PATH Remote working directory
|
|
1891
|
+
--metadata <json> Instance metadata for audit correlation
|
|
1892
|
+
--idempotency-key KEY Stable create retry key
|
|
1893
|
+
--wait-timeout 5m --interval 2s Lifecycle polling controls
|
|
1894
|
+
--keep Keep the instance running and billing
|
|
1895
|
+
|
|
1896
|
+
Runs quote -> create -> wait -> exec -> terminate. Cleanup also runs after
|
|
1897
|
+
command failure, SIGINT, or SIGTERM.`
|
|
1898
|
+
};
|
|
1899
|
+
var COMMON_FLAGS = ["help", "host", "provider", "format"];
|
|
1900
|
+
var SELECTION_FLAGS = [
|
|
1901
|
+
"capabilities",
|
|
1902
|
+
"cpu",
|
|
1903
|
+
"memory",
|
|
1904
|
+
"volume",
|
|
1905
|
+
"gpu-count",
|
|
1906
|
+
"gpu-model",
|
|
1907
|
+
"regions",
|
|
1908
|
+
"min-runtime",
|
|
1909
|
+
"requirements",
|
|
1910
|
+
"max-hourly-usd"
|
|
1911
|
+
];
|
|
1912
|
+
var POLL_FLAGS = ["wait-timeout", "interval"];
|
|
1913
|
+
function help(flags, command) {
|
|
1914
|
+
if (flags.help) {
|
|
1915
|
+
console.log(`xapi-to sandbox ${command}
|
|
1916
|
+
|
|
1917
|
+
${SANDBOX_COMMAND_HELP[command]}
|
|
1918
|
+
|
|
1919
|
+
COMMON
|
|
1920
|
+
--host HOST --provider NAME --format json|pretty|table --help`);
|
|
1921
|
+
process.exit(0);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
function validateFlags(flags, command, allowed = []) {
|
|
1925
|
+
const valid = /* @__PURE__ */ new Set([...COMMON_FLAGS, ...allowed]);
|
|
1926
|
+
const unknown = Object.keys(flags).filter((flag) => !valid.has(flag));
|
|
1927
|
+
if (unknown.length) {
|
|
1928
|
+
err(`unknown flag${unknown.length > 1 ? "s" : ""} for sandbox ${command}: ${unknown.map((flag) => `--${flag}`).join(", ")}`, {
|
|
1929
|
+
hint: `run xapi-to sandbox ${command} --help`,
|
|
1930
|
+
validFlags: [...valid].sort().map((flag) => `--${flag}`)
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
if (flags.format && !["json", "pretty", "table"].includes(flags.format)) {
|
|
1934
|
+
err("--format must be one of: json, pretty, table");
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
function collection(data) {
|
|
1938
|
+
if (Array.isArray(data)) return data;
|
|
1939
|
+
if (Array.isArray(data?.items)) return data.items;
|
|
1940
|
+
if (Array.isArray(data?.data)) return data.data;
|
|
1941
|
+
return data ? [data] : [];
|
|
1942
|
+
}
|
|
1943
|
+
function capability(value, name) {
|
|
1944
|
+
const enabled = value?.capabilities?.[name];
|
|
1945
|
+
return enabled === true ? "yes" : enabled === false ? "no" : "";
|
|
1946
|
+
}
|
|
1947
|
+
function sandboxTableRows(view, data) {
|
|
1948
|
+
if (view === "offerings") {
|
|
1949
|
+
return collection(data).map((item) => ({
|
|
1950
|
+
id: item.id,
|
|
1951
|
+
name: item.name,
|
|
1952
|
+
cpu: item.resources?.cpu,
|
|
1953
|
+
memoryGiB: item.resources?.memoryGiB,
|
|
1954
|
+
volumeGiB: item.resources?.volumeGiB,
|
|
1955
|
+
gpu: Array.isArray(item.resources?.gpu) ? item.resources.gpu.map((gpu) => `${gpu.count || 1}x ${gpu.model || "GPU"}`).join(", ") : "",
|
|
1956
|
+
exec: capability(item, "exec"),
|
|
1957
|
+
background: capability(item, "backgroundExec"),
|
|
1958
|
+
files: capability(item, "files"),
|
|
1959
|
+
ports: capability(item, "ports"),
|
|
1960
|
+
suspend: item.lifecycle?.suspension?.supported === true ? "yes" : "no",
|
|
1961
|
+
hourlyUsd: item.billing?.estimatedHourlyUsdByState?.RUNNING,
|
|
1962
|
+
extensions: Array.isArray(item.capabilities?.extensionIds) ? item.capabilities.extensionIds.join(",") : ""
|
|
1963
|
+
}));
|
|
1964
|
+
}
|
|
1965
|
+
if (view === "quote") {
|
|
1966
|
+
return collection(data).map((item) => ({
|
|
1967
|
+
quoteId: item.quoteId || item.id,
|
|
1968
|
+
offeringId: item.offeringId || item.offering?.id,
|
|
1969
|
+
offering: item.offering?.name || item.offeringName,
|
|
1970
|
+
provider: item.provider?.name || item.providerName || item.provider,
|
|
1971
|
+
hourlyUsd: item.estimatedHourlyUsd || item.hourlyUsd || item.offering?.billing?.estimatedHourlyUsdByState?.RUNNING,
|
|
1972
|
+
expiresAt: item.expiresAt
|
|
1973
|
+
}));
|
|
1974
|
+
}
|
|
1975
|
+
if (view === "run") {
|
|
1976
|
+
return collection(data).map((item) => ({
|
|
1977
|
+
instanceId: item.instanceId,
|
|
1978
|
+
provider: item.provider,
|
|
1979
|
+
offering: item.offering?.name || item.offering,
|
|
1980
|
+
exitCode: item.result?.exitCode,
|
|
1981
|
+
finalState: item.finalState,
|
|
1982
|
+
cleanup: item.cleanup?.state || (item.cleanup?.kept ? "KEPT" : ""),
|
|
1983
|
+
totalCost: item.totalCost
|
|
1984
|
+
}));
|
|
1985
|
+
}
|
|
1986
|
+
return collection(data).map((item) => ({
|
|
1987
|
+
id: item.id || item.instanceId,
|
|
1988
|
+
state: item.observedState || item.state || item.finalState,
|
|
1989
|
+
desiredState: item.desiredState,
|
|
1990
|
+
offering: item.offering?.name || item.offeringName || item.offeringId,
|
|
1991
|
+
provider: item.provider?.name || item.providerName || item.provider,
|
|
1992
|
+
createdAt: item.createdAt,
|
|
1993
|
+
updatedAt: item.updatedAt,
|
|
1994
|
+
totalCost: item.totalCost
|
|
1995
|
+
}));
|
|
1996
|
+
}
|
|
1997
|
+
function sandboxOutput(view, data, flags) {
|
|
1998
|
+
const format = flags.format || getFormat();
|
|
1999
|
+
if (format === "table") {
|
|
2000
|
+
output(sandboxTableRows(view, data), "table");
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
output(data, flags.format);
|
|
2004
|
+
}
|
|
2005
|
+
function flagValue(flags, name) {
|
|
2006
|
+
const value = flags[name];
|
|
2007
|
+
if (value === "true") err(`--${name} requires a value`);
|
|
2008
|
+
return value;
|
|
2009
|
+
}
|
|
2010
|
+
function booleanFlag(flags, name) {
|
|
2011
|
+
const raw = flags[name];
|
|
2012
|
+
if (raw === void 0 || raw === "false") return false;
|
|
2013
|
+
if (raw === "true") return true;
|
|
2014
|
+
err(`--${name} must be a boolean flag or --${name}=true|false`);
|
|
2015
|
+
}
|
|
2016
|
+
function positiveNumber(raw, name) {
|
|
2017
|
+
if (raw === void 0) return void 0;
|
|
2018
|
+
const value = Number(raw);
|
|
2019
|
+
if (!Number.isFinite(value) || value <= 0) err(`--${name} must be a positive number`);
|
|
2020
|
+
return value;
|
|
2021
|
+
}
|
|
2022
|
+
function positiveInteger(raw, name) {
|
|
2023
|
+
const value = positiveNumber(raw, name);
|
|
2024
|
+
if (value !== void 0 && !Number.isInteger(value)) err(`--${name} must be a positive integer`);
|
|
2025
|
+
return value;
|
|
2026
|
+
}
|
|
2027
|
+
function durationMs(raw, fallback, name) {
|
|
2028
|
+
if (raw === void 0) return fallback;
|
|
2029
|
+
if (raw === "true") err(`--${name} requires a value`);
|
|
2030
|
+
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h|d)?$/);
|
|
2031
|
+
if (!match || Number(match[1]) <= 0) err(`--${name} must be a duration like 500ms, 2s, 5m, 1h, or 1d`);
|
|
2032
|
+
const value = Number(match[1]);
|
|
2033
|
+
return value * { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 }[match[2] || "ms"];
|
|
2034
|
+
}
|
|
2035
|
+
function jsonObject(raw, name) {
|
|
2036
|
+
try {
|
|
2037
|
+
const value = JSON.parse(raw);
|
|
2038
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected an object");
|
|
2039
|
+
return value;
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
err(`--${name} must be a valid JSON object`, error.message);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function csv(raw) {
|
|
2045
|
+
if (!raw) return void 0;
|
|
2046
|
+
return raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
2047
|
+
}
|
|
2048
|
+
function sandboxOptions(flags) {
|
|
2049
|
+
const cfg = getConfig();
|
|
2050
|
+
requireApiKey(cfg);
|
|
2051
|
+
const host = flagValue(flags, "host") || cfg.sandboxHost || XAPI_SANDBOX_HOST;
|
|
2052
|
+
const provider2 = flagValue(flags, "provider");
|
|
2053
|
+
return { sandboxHost: host, apiKey: cfg.apiKey, ...provider2 ? { provider: provider2 } : {} };
|
|
2054
|
+
}
|
|
2055
|
+
function requirementsFromFlags(flags, defaultCapabilities) {
|
|
2056
|
+
const requirements = flagValue(flags, "requirements") ? jsonObject(flagValue(flags, "requirements"), "requirements") : {};
|
|
2057
|
+
const capabilities = csv(flagValue(flags, "capabilities")) || (Array.isArray(requirements.capabilities) ? void 0 : defaultCapabilities);
|
|
2058
|
+
const regions = csv(flagValue(flags, "regions"));
|
|
2059
|
+
const cpu = positiveNumber(flagValue(flags, "cpu"), "cpu");
|
|
2060
|
+
const memory = positiveNumber(flagValue(flags, "memory"), "memory");
|
|
2061
|
+
const volume = positiveNumber(flagValue(flags, "volume"), "volume");
|
|
2062
|
+
const gpuCount = positiveInteger(flagValue(flags, "gpu-count"), "gpu-count");
|
|
2063
|
+
const gpuModel = flagValue(flags, "gpu-model");
|
|
2064
|
+
const minRuntime = flagValue(flags, "min-runtime");
|
|
2065
|
+
if (gpuModel && gpuCount === void 0 && !(Number(requirements.gpu?.count) > 0)) {
|
|
2066
|
+
err("--gpu-model requires --gpu-count (or requirements.gpu.count)");
|
|
2067
|
+
}
|
|
2068
|
+
if (capabilities?.length) requirements.capabilities = capabilities;
|
|
2069
|
+
if (regions?.length) requirements.regions = regions;
|
|
2070
|
+
if (cpu !== void 0) requirements.cpu = { ...requirements.cpu || {}, min: cpu };
|
|
2071
|
+
if (memory !== void 0) requirements.memoryGiB = { ...requirements.memoryGiB || {}, min: memory };
|
|
2072
|
+
if (volume !== void 0) requirements.volumeGiB = { ...requirements.volumeGiB || {}, min: volume };
|
|
2073
|
+
if (gpuCount !== void 0 || gpuModel) {
|
|
2074
|
+
requirements.gpu = {
|
|
2075
|
+
...requirements.gpu || {},
|
|
2076
|
+
...gpuCount !== void 0 ? { count: gpuCount } : {},
|
|
2077
|
+
...gpuModel ? { model: gpuModel } : {}
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
if (minRuntime !== void 0) {
|
|
2081
|
+
requirements.minContinuousRuntimeSeconds = Math.ceil(
|
|
2082
|
+
durationMs(minRuntime, 0, "min-runtime") / 1e3
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
return requirements;
|
|
2086
|
+
}
|
|
2087
|
+
function quoteBody(flags, defaultCapabilities) {
|
|
2088
|
+
const max = positiveNumber(flagValue(flags, "max-hourly-usd"), "max-hourly-usd");
|
|
2089
|
+
return {
|
|
2090
|
+
requirements: requirementsFromFlags(flags, defaultCapabilities),
|
|
2091
|
+
...max !== void 0 ? { maxEstimatedHourlyUsd: max.toFixed(8) } : {}
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
function createDefaultCapabilities(flags) {
|
|
2095
|
+
if (flagValue(flags, "requirements") || flagValue(flags, "provider") === "runpod" || flagValue(flags, "gpu-count") || flagValue(flags, "gpu-model")) return void 0;
|
|
2096
|
+
return ["exec"];
|
|
2097
|
+
}
|
|
2098
|
+
function waitSettings(flags) {
|
|
2099
|
+
return {
|
|
2100
|
+
timeoutMs: durationMs(flags["wait-timeout"], 3e5, "wait-timeout"),
|
|
2101
|
+
intervalMs: durationMs(flags.interval, 2e3, "interval")
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
function commandFrom(args, flags, usage2) {
|
|
2105
|
+
const fromFlag = flagValue(flags, "command");
|
|
2106
|
+
const command = fromFlag ?? args.join(" ");
|
|
2107
|
+
if (!command.trim()) err(usage2);
|
|
2108
|
+
return command;
|
|
2109
|
+
}
|
|
2110
|
+
function instanceId(args, usage2) {
|
|
2111
|
+
if (!args[0]) err(usage2);
|
|
2112
|
+
return args[0];
|
|
2113
|
+
}
|
|
2114
|
+
async function terminateAndWait(opts, id, flags) {
|
|
2115
|
+
const { timeoutMs, intervalMs } = waitSettings(flags);
|
|
2116
|
+
const deadline = Date.now() + timeoutMs;
|
|
2117
|
+
const clientIdempotencyKey = flagValue(flags, "idempotency-key") || `cli:terminate:${randomUUID()}`;
|
|
2118
|
+
let operation;
|
|
2119
|
+
while (Date.now() < deadline) {
|
|
2120
|
+
const detail2 = await sandboxGet(opts, id);
|
|
2121
|
+
if (["TERMINATED", "FAILED"].includes(String(detail2.observedState))) {
|
|
2122
|
+
return { operation, sandbox: detail2, clientIdempotencyKey };
|
|
2123
|
+
}
|
|
2124
|
+
try {
|
|
2125
|
+
operation = await sandboxStateAction(opts, id, "terminate", {
|
|
2126
|
+
idempotencyKey: clientIdempotencyKey
|
|
2127
|
+
});
|
|
2128
|
+
break;
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
if (!(error instanceof HttpError) || error.status !== 409) throw error;
|
|
2131
|
+
await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
const remaining = Math.max(1, deadline - Date.now());
|
|
2135
|
+
let detail;
|
|
2136
|
+
try {
|
|
2137
|
+
detail = await sandboxWait(opts, id, ["TERMINATED", "FAILED"], remaining, intervalMs);
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
if (!(error instanceof HttpError) || error.status !== 404 || !opts.provider) throw error;
|
|
2140
|
+
detail = await sandboxWait(
|
|
2141
|
+
{ ...opts, provider: void 0 },
|
|
2142
|
+
id,
|
|
2143
|
+
["TERMINATED", "FAILED"],
|
|
2144
|
+
Math.max(1, deadline - Date.now()),
|
|
2145
|
+
intervalMs
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
return { operation, sandbox: detail, clientIdempotencyKey };
|
|
2149
|
+
}
|
|
2150
|
+
function cleanupSummary(cleanup) {
|
|
2151
|
+
if (!cleanup) return void 0;
|
|
2152
|
+
if (cleanup.error) return { error: cleanup.error };
|
|
2153
|
+
return {
|
|
2154
|
+
operationId: cleanup.operation?.id,
|
|
2155
|
+
operationStatus: cleanup.sandbox?.operations?.find?.((item) => item.type === "TERMINATE")?.status || cleanup.operation?.status,
|
|
2156
|
+
state: cleanup.sandbox?.observedState,
|
|
2157
|
+
totalCost: cleanup.sandbox?.totalCost
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function sandboxResultExitCode(result) {
|
|
2161
|
+
const value = result?.exitCode;
|
|
2162
|
+
if (typeof value !== "number" || value === 0) return void 0;
|
|
2163
|
+
return Math.min(255, Math.max(1, Math.trunc(value)));
|
|
2164
|
+
}
|
|
2165
|
+
async function sandboxOfferings2(args, flags) {
|
|
2166
|
+
help(flags, "offerings");
|
|
2167
|
+
validateFlags(flags, "offerings");
|
|
2168
|
+
try {
|
|
2169
|
+
sandboxOutput("offerings", await sandboxOfferings(sandboxOptions(flags)), flags);
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
err("sandbox offerings failed", error.message);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
async function sandboxQuote2(args, flags) {
|
|
2175
|
+
help(flags, "quote");
|
|
2176
|
+
validateFlags(flags, "quote", SELECTION_FLAGS);
|
|
2177
|
+
try {
|
|
2178
|
+
sandboxOutput("quote", await sandboxQuote(sandboxOptions(flags), quoteBody(flags)), flags);
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
err("sandbox quote failed", error.message);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
async function sandboxList2(args, flags) {
|
|
2184
|
+
help(flags, "list");
|
|
2185
|
+
validateFlags(flags, "list");
|
|
2186
|
+
try {
|
|
2187
|
+
sandboxOutput("instances", await sandboxList(sandboxOptions(flags)), flags);
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
err("sandbox list failed", error.message);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
async function sandboxHistory2(args, flags) {
|
|
2193
|
+
help(flags, "history");
|
|
2194
|
+
validateFlags(flags, "history", ["state", "search", "from", "to", "page", "page-size"]);
|
|
2195
|
+
const state = flagValue(flags, "state");
|
|
2196
|
+
const allowedStates = ["ALL", "ACTIVE", "HISTORY", "PROVISIONING", "RUNNING", "SUSPENDED", "TERMINATED", "FAILED", "UNKNOWN"];
|
|
2197
|
+
if (state && !allowedStates.includes(state.toUpperCase())) {
|
|
2198
|
+
err(`--state must be one of: ${allowedStates.join(", ")}`);
|
|
2199
|
+
}
|
|
2200
|
+
const page = positiveInteger(flagValue(flags, "page"), "page") || 1;
|
|
2201
|
+
const pageSize = positiveInteger(flagValue(flags, "page-size"), "page-size") || 100;
|
|
2202
|
+
if (pageSize > 100) err("--page-size must be at most 100");
|
|
2203
|
+
try {
|
|
2204
|
+
sandboxOutput("instances", await sandboxHistory(sandboxOptions(flags), {
|
|
2205
|
+
...state ? { state: state.toUpperCase() } : {},
|
|
2206
|
+
...flagValue(flags, "search") ? { search: flagValue(flags, "search") } : {},
|
|
2207
|
+
...flagValue(flags, "from") ? { from: flagValue(flags, "from") } : {},
|
|
2208
|
+
...flagValue(flags, "to") ? { to: flagValue(flags, "to") } : {},
|
|
2209
|
+
page,
|
|
2210
|
+
pageSize
|
|
2211
|
+
}), flags);
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
err("sandbox history failed", error.message);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
async function sandboxGet2(args, flags) {
|
|
2217
|
+
help(flags, "get");
|
|
2218
|
+
validateFlags(flags, "get");
|
|
2219
|
+
const id = instanceId(args, "usage: xapi-to sandbox get <id>");
|
|
2220
|
+
try {
|
|
2221
|
+
sandboxOutput("detail", await sandboxGet(sandboxOptions(flags), id), flags);
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
err("sandbox get failed", error.message);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
async function sandboxCreate2(args, flags) {
|
|
2227
|
+
help(flags, "create");
|
|
2228
|
+
validateFlags(flags, "create", [
|
|
2229
|
+
...SELECTION_FLAGS,
|
|
2230
|
+
...POLL_FLAGS,
|
|
2231
|
+
"quote-id",
|
|
2232
|
+
"offering-id",
|
|
2233
|
+
"metadata",
|
|
2234
|
+
"idempotency-key",
|
|
2235
|
+
"resume-on-access",
|
|
2236
|
+
"wait"
|
|
2237
|
+
]);
|
|
2238
|
+
const wait = booleanFlag(flags, "wait");
|
|
2239
|
+
const resumeOnAccess = booleanFlag(flags, "resume-on-access");
|
|
2240
|
+
const opts = sandboxOptions(flags);
|
|
2241
|
+
const quoteId = flagValue(flags, "quote-id");
|
|
2242
|
+
const offeringId = flagValue(flags, "offering-id");
|
|
2243
|
+
const maxHourly = flagValue(flags, "max-hourly-usd");
|
|
2244
|
+
const requirementFlags = [
|
|
2245
|
+
"requirements",
|
|
2246
|
+
"capabilities",
|
|
2247
|
+
"cpu",
|
|
2248
|
+
"memory",
|
|
2249
|
+
"volume",
|
|
2250
|
+
"gpu-count",
|
|
2251
|
+
"gpu-model",
|
|
2252
|
+
"regions",
|
|
2253
|
+
"min-runtime"
|
|
2254
|
+
].filter((name) => flagValue(flags, name) !== void 0);
|
|
2255
|
+
if (quoteId && offeringId) err("--quote-id and --offering-id are mutually exclusive");
|
|
2256
|
+
if ((quoteId || offeringId) && requirementFlags.length) {
|
|
2257
|
+
err(`${quoteId ? "--quote-id" : "--offering-id"} cannot be combined with requirement flags`, {
|
|
2258
|
+
conflictingFlags: requirementFlags.map((name) => `--${name}`)
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
if (quoteId && maxHourly) {
|
|
2262
|
+
err("--max-hourly-usd cannot be combined with --quote-id; the quote already fixes the price");
|
|
2263
|
+
}
|
|
2264
|
+
if (offeringId && maxHourly) {
|
|
2265
|
+
err("--max-hourly-usd cannot be combined with --offering-id; create from requirements to enforce a price ceiling");
|
|
2266
|
+
}
|
|
2267
|
+
const idempotencyKey = flagValue(flags, "idempotency-key") || `cli:create:${randomUUID()}`;
|
|
2268
|
+
let created;
|
|
2269
|
+
try {
|
|
2270
|
+
let selection;
|
|
2271
|
+
if (quoteId) selection = { quoteId };
|
|
2272
|
+
else if (offeringId) selection = { offeringId };
|
|
2273
|
+
else if (maxHourly) {
|
|
2274
|
+
const quoted = await sandboxQuote(opts, quoteBody(flags, createDefaultCapabilities(flags)));
|
|
2275
|
+
if (!quoted?.quoteId) throw new Error("quote response did not include quoteId");
|
|
2276
|
+
selection = { quoteId: quoted.quoteId };
|
|
2277
|
+
} else selection = { requirements: requirementsFromFlags(flags, createDefaultCapabilities(flags)) };
|
|
2278
|
+
const metadata = flagValue(flags, "metadata") ? jsonObject(flagValue(flags, "metadata"), "metadata") : { client: "xapi-cli" };
|
|
2279
|
+
created = await sandboxCreate(opts, {
|
|
2280
|
+
selection,
|
|
2281
|
+
metadata,
|
|
2282
|
+
idempotencyKey,
|
|
2283
|
+
policy: { resumeOnAccess }
|
|
2284
|
+
});
|
|
2285
|
+
let result = created;
|
|
2286
|
+
if (wait && created.id) {
|
|
2287
|
+
const settings = waitSettings(flags);
|
|
2288
|
+
result = await sandboxWait(opts, created.id, ["RUNNING"], settings.timeoutMs, settings.intervalMs);
|
|
2289
|
+
}
|
|
2290
|
+
sandboxOutput("detail", { ...result, clientIdempotencyKey: idempotencyKey }, flags);
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
let latest = created;
|
|
2293
|
+
if (created?.id) {
|
|
2294
|
+
try {
|
|
2295
|
+
latest = await sandboxGet(opts, created.id);
|
|
2296
|
+
} catch {
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
err("sandbox create failed", {
|
|
2300
|
+
message: error.message,
|
|
2301
|
+
instanceId: created?.id,
|
|
2302
|
+
observedState: latest?.observedState,
|
|
2303
|
+
clientIdempotencyKey: idempotencyKey,
|
|
2304
|
+
recovery: created?.id ? {
|
|
2305
|
+
inspect: `xapi-to sandbox get ${created.id}`,
|
|
2306
|
+
terminate: `xapi-to sandbox terminate ${created.id}`
|
|
2307
|
+
} : {
|
|
2308
|
+
retry: `repeat the create command with --idempotency-key ${idempotencyKey}`,
|
|
2309
|
+
reconcile: "xapi-to sandbox history --state ACTIVE --page-size 100"
|
|
2310
|
+
}
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
async function sandboxWait2(args, flags) {
|
|
2315
|
+
help(flags, "wait");
|
|
2316
|
+
validateFlags(flags, "wait", ["state", ...POLL_FLAGS]);
|
|
2317
|
+
const id = instanceId(args, "usage: xapi-to sandbox wait <id> [--state RUNNING]");
|
|
2318
|
+
const allowedStates = ["PROVISIONING", "RUNNING", "SUSPENDING", "SUSPENDED", "RESUMING", "TERMINATING", "TERMINATED", "FAILED", "UNKNOWN"];
|
|
2319
|
+
const wanted = (csv(flagValue(flags, "state")) || ["RUNNING"]).map((state) => state.toUpperCase());
|
|
2320
|
+
const invalid = wanted.filter((state) => !allowedStates.includes(state));
|
|
2321
|
+
if (invalid.length) err(`--state must contain only: ${allowedStates.join(", ")}`);
|
|
2322
|
+
const settings = waitSettings(flags);
|
|
2323
|
+
try {
|
|
2324
|
+
output(await sandboxWait(sandboxOptions(flags), id, wanted, settings.timeoutMs, settings.intervalMs), flags.format);
|
|
2325
|
+
} catch (error) {
|
|
2326
|
+
err("sandbox wait failed", error.message);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
async function sandboxExec2(args, flags) {
|
|
2330
|
+
help(flags, "exec");
|
|
2331
|
+
validateFlags(flags, "exec", ["command", "timeout", "cwd", "background"]);
|
|
2332
|
+
const id = instanceId(args, "usage: xapi-to sandbox exec <id> --command <shell>");
|
|
2333
|
+
const command = commandFrom(args.slice(1), flags, "usage: xapi-to sandbox exec <id> --command <shell>");
|
|
2334
|
+
const timeoutSeconds = positiveInteger(flagValue(flags, "timeout"), "timeout") || 60;
|
|
2335
|
+
const background = booleanFlag(flags, "background");
|
|
2336
|
+
try {
|
|
2337
|
+
const result = await sandboxExec(sandboxOptions(flags), id, {
|
|
2338
|
+
command,
|
|
2339
|
+
timeoutSeconds,
|
|
2340
|
+
...flagValue(flags, "cwd") ? { cwd: flagValue(flags, "cwd") } : {},
|
|
2341
|
+
...background ? { background: true } : {}
|
|
2342
|
+
});
|
|
2343
|
+
output(result, flags.format);
|
|
2344
|
+
const exitCode = sandboxResultExitCode(result);
|
|
2345
|
+
if (exitCode !== void 0) process.exitCode = exitCode;
|
|
2346
|
+
} catch (error) {
|
|
2347
|
+
err("sandbox exec failed", error.message);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
async function sandboxFile(args, flags) {
|
|
2351
|
+
help(flags, "file");
|
|
2352
|
+
validateFlags(flags, "file", ["file", "content", "output", "path", "depth"]);
|
|
2353
|
+
const [action, id, remote] = args;
|
|
2354
|
+
if (!action || !id) err("usage: xapi-to sandbox file <write|read|list> <id> [remote-path]");
|
|
2355
|
+
const opts = sandboxOptions(flags);
|
|
2356
|
+
try {
|
|
2357
|
+
if (action === "write") {
|
|
2358
|
+
if (!remote) err("usage: xapi-to sandbox file write <id> <remote-path> (--file <local>|--content <text>)");
|
|
2359
|
+
const local = flagValue(flags, "file");
|
|
2360
|
+
const inline = flagValue(flags, "content");
|
|
2361
|
+
if (Boolean(local) === Boolean(inline)) err("provide exactly one of --file or --content");
|
|
2362
|
+
const body = local ? { path: remote, content: (await readFile(local)).toString("base64"), encoding: "base64" } : { path: remote, content: inline, encoding: "utf8" };
|
|
2363
|
+
output(await sandboxFileWrite(opts, id, body), flags.format);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
if (action === "read") {
|
|
2367
|
+
if (!remote) err("usage: xapi-to sandbox file read <id> <remote-path> [--output <local>]");
|
|
2368
|
+
const outputPath = flagValue(flags, "output");
|
|
2369
|
+
const result = await sandboxFileRead(opts, id, remote, outputPath ? "base64" : "utf8");
|
|
2370
|
+
if (!outputPath) {
|
|
2371
|
+
output(result, flags.format);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
const target = resolve(outputPath);
|
|
2375
|
+
const file = await open(target, "wx");
|
|
2376
|
+
let complete = false;
|
|
2377
|
+
try {
|
|
2378
|
+
const data = result?.encoding === "base64" ? Buffer.from(String(result.content || ""), "base64") : Buffer.from(String(result?.content || ""), "utf8");
|
|
2379
|
+
await file.writeFile(data);
|
|
2380
|
+
complete = true;
|
|
2381
|
+
output({ output: target, bytes: data.length, path: remote }, flags.format);
|
|
2382
|
+
} finally {
|
|
2383
|
+
await file.close();
|
|
2384
|
+
if (!complete) await rm(target, { force: true });
|
|
2385
|
+
}
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
if (action === "list") {
|
|
2389
|
+
const depth = positiveInteger(flagValue(flags, "depth"), "depth") || 2;
|
|
2390
|
+
output(await sandboxFileList(opts, id, flagValue(flags, "path") || remote || ".", depth), flags.format);
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
err(`unknown sandbox file command: ${action}`);
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
err(`sandbox file ${action} failed`, error.message);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
async function sandboxPort2(args, flags) {
|
|
2399
|
+
help(flags, "port");
|
|
2400
|
+
validateFlags(flags, "port");
|
|
2401
|
+
const id = instanceId(args, "usage: xapi-to sandbox port <id> <port>");
|
|
2402
|
+
const port = positiveInteger(args[1], "port");
|
|
2403
|
+
if (!port || port > 65535) err("port must be between 1 and 65535");
|
|
2404
|
+
try {
|
|
2405
|
+
output(await sandboxPort(sandboxOptions(flags), id, port), flags.format);
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
err("sandbox port failed", error.message);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
async function sandboxExtension2(args, flags) {
|
|
2411
|
+
help(flags, "extension");
|
|
2412
|
+
validateFlags(flags, "extension", ["input", "idempotency-key"]);
|
|
2413
|
+
const id = instanceId(args, "usage: xapi-to sandbox extension <id> <extension-id> --input <json>");
|
|
2414
|
+
const extensionId = args[1];
|
|
2415
|
+
if (!extensionId) err("usage: xapi-to sandbox extension <id> <extension-id> --input <json>");
|
|
2416
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,119}$/i.test(extensionId)) err("invalid Sandbox extension id");
|
|
2417
|
+
const input = flagValue(flags, "input") ? jsonObject(flagValue(flags, "input"), "input") : {};
|
|
2418
|
+
const clientIdempotencyKey = flagValue(flags, "idempotency-key") || `cli:extension:${extensionId}:${randomUUID()}`;
|
|
2419
|
+
try {
|
|
2420
|
+
const result = await sandboxExtension(sandboxOptions(flags), id, extensionId, {
|
|
2421
|
+
input,
|
|
2422
|
+
idempotencyKey: clientIdempotencyKey
|
|
2423
|
+
});
|
|
2424
|
+
output({ ...result, clientIdempotencyKey }, flags.format);
|
|
2425
|
+
} catch (error) {
|
|
2426
|
+
err("sandbox extension failed", {
|
|
2427
|
+
message: error.message,
|
|
2428
|
+
clientIdempotencyKey,
|
|
2429
|
+
retry: `repeat with --idempotency-key ${clientIdempotencyKey}`
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
async function sandboxAudit2(args, flags) {
|
|
2434
|
+
help(flags, "audit");
|
|
2435
|
+
validateFlags(flags, "audit", ["kind", "page", "page-size"]);
|
|
2436
|
+
const id = instanceId(args, "usage: xapi-to sandbox audit <id> [--kind operations]");
|
|
2437
|
+
const kind = flagValue(flags, "kind") || "operations";
|
|
2438
|
+
const allowed = ["operations", "events", "usageSegments", "billingPeriods"];
|
|
2439
|
+
if (!allowed.includes(kind)) err(`--kind must be one of: ${allowed.join(", ")}`);
|
|
2440
|
+
const page = positiveInteger(flagValue(flags, "page"), "page") || 1;
|
|
2441
|
+
const pageSize = positiveInteger(flagValue(flags, "page-size"), "page-size") || 100;
|
|
2442
|
+
if (pageSize > 100) err("--page-size must be at most 100");
|
|
2443
|
+
try {
|
|
2444
|
+
output(await sandboxAudit(sandboxOptions(flags), id, kind, page, pageSize), flags.format);
|
|
2445
|
+
} catch (error) {
|
|
2446
|
+
err("sandbox audit failed", error.message);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
async function sandboxState(action, args, flags) {
|
|
2450
|
+
help(flags, action);
|
|
2451
|
+
validateFlags(flags, action, [...POLL_FLAGS, "no-wait", "idempotency-key"]);
|
|
2452
|
+
const id = instanceId(args, `usage: xapi-to sandbox ${action} <id>`);
|
|
2453
|
+
const opts = sandboxOptions(flags);
|
|
2454
|
+
const noWait = booleanFlag(flags, "no-wait");
|
|
2455
|
+
const clientIdempotencyKey = flagValue(flags, "idempotency-key") || `cli:${action}:${randomUUID()}`;
|
|
2456
|
+
try {
|
|
2457
|
+
if (action === "terminate" && !noWait) {
|
|
2458
|
+
output(await terminateAndWait(opts, id, flags), flags.format);
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
const operation = await sandboxStateAction(opts, id, action, {
|
|
2462
|
+
idempotencyKey: clientIdempotencyKey
|
|
2463
|
+
});
|
|
2464
|
+
if (noWait) {
|
|
2465
|
+
output({ ...operation, clientIdempotencyKey }, flags.format);
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
const wanted = action === "suspend" ? ["SUSPENDED"] : action === "resume" ? ["RUNNING"] : ["TERMINATED", "FAILED"];
|
|
2469
|
+
const settings = waitSettings(flags);
|
|
2470
|
+
const detail = await sandboxWait(opts, id, wanted, settings.timeoutMs, settings.intervalMs);
|
|
2471
|
+
output({ operation, sandbox: detail, clientIdempotencyKey }, flags.format);
|
|
2472
|
+
} catch (error) {
|
|
2473
|
+
err(`sandbox ${action} failed`, {
|
|
2474
|
+
message: error.message,
|
|
2475
|
+
instanceId: id,
|
|
2476
|
+
clientIdempotencyKey,
|
|
2477
|
+
recovery: {
|
|
2478
|
+
inspect: `xapi-to sandbox get ${id}`,
|
|
2479
|
+
retry: `repeat with --idempotency-key ${clientIdempotencyKey}`
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
async function sandboxRun(args, flags) {
|
|
2485
|
+
help(flags, "run");
|
|
2486
|
+
validateFlags(flags, "run", [
|
|
2487
|
+
...SELECTION_FLAGS,
|
|
2488
|
+
...POLL_FLAGS,
|
|
2489
|
+
"command",
|
|
2490
|
+
"timeout",
|
|
2491
|
+
"cwd",
|
|
2492
|
+
"metadata",
|
|
2493
|
+
"keep",
|
|
2494
|
+
"idempotency-key"
|
|
2495
|
+
]);
|
|
2496
|
+
const command = commandFrom(args, flags, "usage: xapi-to sandbox run --command <shell>");
|
|
2497
|
+
const opts = sandboxOptions(flags);
|
|
2498
|
+
const maxHourly = flagValue(flags, "max-hourly-usd") || "0.20";
|
|
2499
|
+
positiveNumber(maxHourly, "max-hourly-usd");
|
|
2500
|
+
const timeoutSeconds = positiveInteger(flagValue(flags, "timeout"), "timeout") || 60;
|
|
2501
|
+
const settings = waitSettings(flags);
|
|
2502
|
+
const idempotencyKey = flagValue(flags, "idempotency-key") || `cli:run:${randomUUID()}`;
|
|
2503
|
+
const metadata = flagValue(flags, "metadata") ? jsonObject(flagValue(flags, "metadata"), "metadata") : {};
|
|
2504
|
+
const keep = booleanFlag(flags, "keep");
|
|
2505
|
+
let id;
|
|
2506
|
+
let failure;
|
|
2507
|
+
let quote;
|
|
2508
|
+
let created;
|
|
2509
|
+
let ready;
|
|
2510
|
+
let result;
|
|
2511
|
+
let cleanup;
|
|
2512
|
+
let interruptedBy;
|
|
2513
|
+
const waitAbort = new AbortController();
|
|
2514
|
+
const interrupt = (signal) => {
|
|
2515
|
+
interruptedBy = signal;
|
|
2516
|
+
waitAbort.abort();
|
|
2517
|
+
};
|
|
2518
|
+
const interruptSigint = () => interrupt("SIGINT");
|
|
2519
|
+
const interruptSigterm = () => interrupt("SIGTERM");
|
|
2520
|
+
const throwIfInterrupted = () => {
|
|
2521
|
+
if (interruptedBy) throw new Error(`interrupted by ${interruptedBy}`);
|
|
2522
|
+
};
|
|
2523
|
+
process.once("SIGINT", interruptSigint);
|
|
2524
|
+
process.once("SIGTERM", interruptSigterm);
|
|
2525
|
+
try {
|
|
2526
|
+
quote = await sandboxQuote(
|
|
2527
|
+
opts,
|
|
2528
|
+
quoteBody({ ...flags, "max-hourly-usd": maxHourly }, ["exec"]),
|
|
2529
|
+
waitAbort.signal
|
|
2530
|
+
);
|
|
2531
|
+
if (!quote?.quoteId) throw new Error("quote response did not include quoteId");
|
|
2532
|
+
throwIfInterrupted();
|
|
2533
|
+
created = await sandboxCreate(opts, {
|
|
2534
|
+
selection: { quoteId: quote.quoteId },
|
|
2535
|
+
metadata: { ...metadata, client: "xapi-cli", command: "sandbox run" },
|
|
2536
|
+
policy: { resumeOnAccess: false },
|
|
2537
|
+
idempotencyKey
|
|
2538
|
+
});
|
|
2539
|
+
id = created?.id;
|
|
2540
|
+
if (!id) throw new Error("create response did not include sandbox id");
|
|
2541
|
+
throwIfInterrupted();
|
|
2542
|
+
ready = await sandboxWait(
|
|
2543
|
+
opts,
|
|
2544
|
+
id,
|
|
2545
|
+
["RUNNING"],
|
|
2546
|
+
settings.timeoutMs,
|
|
2547
|
+
settings.intervalMs,
|
|
2548
|
+
waitAbort.signal
|
|
2549
|
+
);
|
|
2550
|
+
throwIfInterrupted();
|
|
2551
|
+
result = await sandboxExec(opts, id, {
|
|
2552
|
+
command,
|
|
2553
|
+
timeoutSeconds,
|
|
2554
|
+
...flagValue(flags, "cwd") ? { cwd: flagValue(flags, "cwd") } : {}
|
|
2555
|
+
}, waitAbort.signal);
|
|
2556
|
+
throwIfInterrupted();
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
failure = error;
|
|
2559
|
+
} finally {
|
|
2560
|
+
if (id && !keep) {
|
|
2561
|
+
try {
|
|
2562
|
+
cleanup = await terminateAndWait(opts, id, flags);
|
|
2563
|
+
} catch (cleanupError) {
|
|
2564
|
+
cleanup = { error: cleanupError.message };
|
|
2565
|
+
if (!failure) failure = new Error(`command completed but cleanup failed: ${cleanupError.message}`);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
process.removeListener("SIGINT", interruptSigint);
|
|
2569
|
+
process.removeListener("SIGTERM", interruptSigterm);
|
|
2570
|
+
}
|
|
2571
|
+
if (failure) {
|
|
2572
|
+
err("sandbox run failed", {
|
|
2573
|
+
message: failure?.message || String(failure),
|
|
2574
|
+
instanceId: id,
|
|
2575
|
+
clientIdempotencyKey: idempotencyKey,
|
|
2576
|
+
cleanup: keep ? { kept: true, warning: "billing continues until terminated" } : cleanupSummary(cleanup),
|
|
2577
|
+
recovery: id ? { inspect: `xapi-to sandbox get ${id}`, terminate: `xapi-to sandbox terminate ${id}` } : {
|
|
2578
|
+
reconcile: "xapi-to sandbox history --state ACTIVE --page-size 100",
|
|
2579
|
+
retryCreateWithSameKey: idempotencyKey
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
let finalDetail;
|
|
2584
|
+
let finalReadError;
|
|
2585
|
+
if (id) {
|
|
2586
|
+
try {
|
|
2587
|
+
finalDetail = await sandboxGet(opts, id);
|
|
2588
|
+
} catch (error) {
|
|
2589
|
+
finalReadError = error.message;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
const summary = {
|
|
2593
|
+
instanceId: id,
|
|
2594
|
+
clientIdempotencyKey: idempotencyKey,
|
|
2595
|
+
provider: opts.provider || "auto",
|
|
2596
|
+
offering: quote?.offering,
|
|
2597
|
+
createdState: created?.observedState,
|
|
2598
|
+
readyState: ready?.observedState,
|
|
2599
|
+
result,
|
|
2600
|
+
cleanup: keep ? { kept: true, warning: "billing continues until terminated" } : cleanupSummary(cleanup),
|
|
2601
|
+
finalState: finalDetail?.observedState || cleanup?.sandbox?.observedState,
|
|
2602
|
+
totalCost: finalDetail?.totalCost || cleanup?.sandbox?.totalCost,
|
|
2603
|
+
...finalReadError ? { finalReadError } : {}
|
|
2604
|
+
};
|
|
2605
|
+
sandboxOutput("run", summary, flags);
|
|
2606
|
+
const remoteExitCode = sandboxResultExitCode(result);
|
|
2607
|
+
if (remoteExitCode !== void 0) process.exitCode = remoteExitCode;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// src/commands/provider.ts
|
|
2611
|
+
import { mkdir, open as open2, readFile as readFile2 } from "fs/promises";
|
|
2612
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
2613
|
+
var READ_RETRIES3 = 2;
|
|
2614
|
+
var BASE = "/api/api-services/agent";
|
|
2615
|
+
var PROVIDER_HELP = `xapi-to provider - Manage provider services and their content
|
|
2616
|
+
|
|
2617
|
+
USAGE
|
|
2618
|
+
xapi-to provider list
|
|
2619
|
+
xapi-to provider get <service-id> [--version <version>]
|
|
2620
|
+
xapi-to provider create --file <service.json>
|
|
2621
|
+
xapi-to provider update <service-id> [metadata flags]
|
|
2622
|
+
xapi-to provider versions <service-id>
|
|
2623
|
+
xapi-to provider version update <service-id> <version-id> --file <contract.json> [--replace]
|
|
2624
|
+
xapi-to provider major create <service-id>
|
|
2625
|
+
xapi-to provider revision start <service-id> <major>
|
|
2626
|
+
xapi-to provider publish <service-id> <revision-id> [--changelog <text>|--changelog-file <path>]
|
|
2627
|
+
xapi-to provider rollback <service-id> <major> --revision <revision-id> [--reason <text>|--reason-file <path>]
|
|
2628
|
+
xapi-to provider default-major <service-id> <major>
|
|
2629
|
+
xapi-to provider deprecate|restore <service-id> <major>
|
|
2630
|
+
xapi-to provider review <service-id> <revision-id>
|
|
2631
|
+
xapi-to provider diff <service-id> <major>
|
|
2632
|
+
xapi-to provider metrics [service-id] [--days 30]
|
|
2633
|
+
xapi-to provider events [--after <cursor>] [--limit 50]
|
|
2634
|
+
xapi-to provider skill context <service-id>
|
|
2635
|
+
xapi-to provider skill scaffold <service-id> --output <SKILL.md> [--force]
|
|
2636
|
+
xapi-to provider skill link <service-id> <skill-id>
|
|
2637
|
+
xapi-to provider skill unlink <service-id>
|
|
2638
|
+
xapi-to provider skill fingerprint <service-id> [--skill-version-id <id>]
|
|
2639
|
+
xapi-to provider delete <service-id> --confirm <service-name-or-id>
|
|
2640
|
+
|
|
2641
|
+
METADATA FLAGS
|
|
2642
|
+
--file <metadata.json> Read metadata from JSON
|
|
2643
|
+
--name <name> Service display name
|
|
2644
|
+
--description <text> Marketplace card description
|
|
2645
|
+
--description-file <path|-> Read description from a file or stdin
|
|
2646
|
+
--about <markdown> Long About content
|
|
2647
|
+
--about-file <path|-> Read About Markdown from a file or stdin
|
|
2648
|
+
--clear-about Clear About content
|
|
2649
|
+
--website <url> Public service website
|
|
2650
|
+
--clear-website Clear website
|
|
2651
|
+
--logo-url <url> Service logo URL
|
|
2652
|
+
--category <category> Marketplace category
|
|
2653
|
+
|
|
2654
|
+
SCOPES
|
|
2655
|
+
list/get/versions/review/diff/skill context: service:read
|
|
2656
|
+
create: service:create
|
|
2657
|
+
update/version update/skill link/fingerprint: service:update
|
|
2658
|
+
major/revision start: version:create
|
|
2659
|
+
publish: service:publish
|
|
2660
|
+
rollback/default-major/deprecate/restore: service:rollback
|
|
2661
|
+
metrics/events: observability:read
|
|
2662
|
+
delete: service:delete
|
|
2663
|
+
`;
|
|
2664
|
+
function servicePath(serviceId, suffix = "") {
|
|
2665
|
+
return `${BASE}/services/${encodeURIComponent(serviceId)}${suffix}`;
|
|
2666
|
+
}
|
|
2667
|
+
function required(value, usage2) {
|
|
2668
|
+
if (!value?.trim()) err(`usage: ${usage2}`);
|
|
2669
|
+
return value.trim();
|
|
2670
|
+
}
|
|
2671
|
+
function requiredFlag(value, usage2) {
|
|
2672
|
+
if (!value?.trim() || value === "true") err(`usage: ${usage2}`);
|
|
2673
|
+
return value.trim();
|
|
2674
|
+
}
|
|
2675
|
+
function optionalFlag(value, flagName) {
|
|
2676
|
+
if (value === void 0) return void 0;
|
|
2677
|
+
if (value === "true") err(`${flagName} requires a value`);
|
|
2678
|
+
return value;
|
|
2679
|
+
}
|
|
2680
|
+
function positiveInt(raw, name, max) {
|
|
2681
|
+
if (raw === void 0) return void 0;
|
|
2682
|
+
const value = Number(raw);
|
|
2683
|
+
if (!Number.isInteger(value) || value < 1 || max !== void 0 && value > max) {
|
|
2684
|
+
err(`invalid ${name}`, `Expected an integer from 1${max ? ` to ${max}` : ""}.`);
|
|
2685
|
+
}
|
|
2686
|
+
return value;
|
|
2687
|
+
}
|
|
2688
|
+
function boolFlag(flags, name) {
|
|
2689
|
+
return ["true", "1", "yes"].includes((flags[name] || "").toLowerCase());
|
|
2690
|
+
}
|
|
2691
|
+
async function readText(path, flagName) {
|
|
2692
|
+
if (path === "true") err(`${flagName} requires a path or - for stdin`);
|
|
2693
|
+
if (path === "-") {
|
|
2694
|
+
const chunks = [];
|
|
2695
|
+
for await (const chunk of process.stdin) {
|
|
2696
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2697
|
+
}
|
|
2698
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2699
|
+
}
|
|
2700
|
+
return readFile2(resolve2(path), "utf8");
|
|
2701
|
+
}
|
|
2702
|
+
async function textOption(flags, directName, fileName) {
|
|
2703
|
+
const direct = flags[directName];
|
|
2704
|
+
const file = flags[fileName];
|
|
2705
|
+
if (direct !== void 0 && file !== void 0) {
|
|
2706
|
+
err(`--${directName} and --${fileName} are mutually exclusive`);
|
|
2707
|
+
}
|
|
2708
|
+
if (direct === "true") err(`--${directName} requires a value`);
|
|
2709
|
+
if (file !== void 0) return readText(file, `--${fileName}`);
|
|
2710
|
+
return direct;
|
|
2711
|
+
}
|
|
2712
|
+
async function readJsonObject(path, flagName = "--file") {
|
|
2713
|
+
if (!path || path === "true") err(`${flagName} requires a JSON file path or - for stdin`);
|
|
2714
|
+
let parsed;
|
|
2715
|
+
try {
|
|
2716
|
+
parsed = JSON.parse(await readText(path, flagName));
|
|
2717
|
+
} catch (error) {
|
|
2718
|
+
err(`invalid JSON from ${flagName}`, error.message);
|
|
2719
|
+
}
|
|
2720
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2721
|
+
err(`${flagName} must contain a JSON object`);
|
|
2722
|
+
}
|
|
2723
|
+
return parsed;
|
|
2724
|
+
}
|
|
2725
|
+
async function metadataBody(flags) {
|
|
2726
|
+
const body = flags.file ? await readJsonObject(flags.file) : {};
|
|
2727
|
+
const description = await textOption(flags, "description", "description-file");
|
|
2728
|
+
const about = await textOption(flags, "about", "about-file");
|
|
2729
|
+
if (boolFlag(flags, "clear-about") && about !== void 0) {
|
|
2730
|
+
err("--clear-about cannot be combined with --about or --about-file");
|
|
2731
|
+
}
|
|
2732
|
+
if (boolFlag(flags, "clear-website") && flags.website !== void 0) {
|
|
2733
|
+
err("--clear-website cannot be combined with --website");
|
|
2734
|
+
}
|
|
2735
|
+
if (description !== void 0) body.description = description;
|
|
2736
|
+
if (about !== void 0) body.aboutMarkdown = about;
|
|
2737
|
+
for (const [flagName, fieldName] of [
|
|
2738
|
+
["name", "name"],
|
|
2739
|
+
["website", "website"],
|
|
2740
|
+
["logo-url", "logoUrl"],
|
|
2741
|
+
["category", "category"]
|
|
2742
|
+
]) {
|
|
2743
|
+
if (flags[flagName] === "true") err(`--${flagName} requires a value`);
|
|
2744
|
+
if (flags[flagName] !== void 0) body[fieldName] = flags[flagName];
|
|
2745
|
+
}
|
|
2746
|
+
if (boolFlag(flags, "clear-about")) body.aboutMarkdown = null;
|
|
2747
|
+
if (boolFlag(flags, "clear-website")) body.website = null;
|
|
2748
|
+
if (Object.keys(body).length === 0) {
|
|
2749
|
+
err("no provider metadata supplied", "Pass --file or at least one metadata flag.");
|
|
2750
|
+
}
|
|
2751
|
+
return body;
|
|
2752
|
+
}
|
|
2753
|
+
async function writeExclusive(path, content, force) {
|
|
2754
|
+
const target = resolve2(path);
|
|
2755
|
+
await mkdir(dirname(target), { recursive: true });
|
|
2756
|
+
const handle = await open2(target, force ? "w" : "wx");
|
|
2757
|
+
try {
|
|
2758
|
+
await handle.writeFile(content, "utf8");
|
|
2759
|
+
} finally {
|
|
2760
|
+
await handle.close();
|
|
2761
|
+
}
|
|
2762
|
+
return target;
|
|
2763
|
+
}
|
|
2764
|
+
async function provider(args, flags) {
|
|
2765
|
+
if (flags.help || args.length === 0) {
|
|
2766
|
+
console.log(PROVIDER_HELP);
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
const cfg = getConfig();
|
|
2770
|
+
requireApiKey(cfg);
|
|
2771
|
+
const apiKey = cfg.apiKey;
|
|
2772
|
+
const [command, ...rest] = args;
|
|
2773
|
+
try {
|
|
2774
|
+
let result;
|
|
2775
|
+
switch (command) {
|
|
2776
|
+
case "list":
|
|
2777
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE}/services`, { retries: READ_RETRIES3 });
|
|
2778
|
+
break;
|
|
2779
|
+
case "get": {
|
|
2780
|
+
const id = required(rest[0], "xapi-to provider get <service-id>");
|
|
2781
|
+
const path = new URL(`https://placeholder${servicePath(id)}`);
|
|
2782
|
+
const version = optionalFlag(flags.version, "--version");
|
|
2783
|
+
if (version !== void 0) path.searchParams.set("version", version);
|
|
2784
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
2785
|
+
break;
|
|
2786
|
+
}
|
|
2787
|
+
case "create":
|
|
2788
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE}/services`, {
|
|
2789
|
+
method: "POST",
|
|
2790
|
+
body: await readJsonObject(required(flags.file, "xapi-to provider create --file <service.json>"))
|
|
2791
|
+
});
|
|
2792
|
+
break;
|
|
2793
|
+
case "update": {
|
|
2794
|
+
const id = required(rest[0], "xapi-to provider update <service-id> [metadata flags]");
|
|
2795
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), {
|
|
2796
|
+
method: "PATCH",
|
|
2797
|
+
body: await metadataBody(flags)
|
|
2798
|
+
});
|
|
2799
|
+
break;
|
|
2800
|
+
}
|
|
2801
|
+
case "versions": {
|
|
2802
|
+
const id = required(rest[0], "xapi-to provider versions <service-id>");
|
|
2803
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/version-overview"), { retries: READ_RETRIES3 });
|
|
2804
|
+
break;
|
|
2805
|
+
}
|
|
2806
|
+
case "version": {
|
|
2807
|
+
if (rest[0] !== "update") err("usage: xapi-to provider version update <service-id> <version-id> --file <contract.json> [--replace]");
|
|
2808
|
+
const id = required(rest[1], "xapi-to provider version update <service-id> <version-id> --file <contract.json>");
|
|
2809
|
+
const versionId = required(rest[2], "xapi-to provider version update <service-id> <version-id> --file <contract.json>");
|
|
2810
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/versions/${encodeURIComponent(versionId)}`), {
|
|
2811
|
+
method: boolFlag(flags, "replace") ? "PUT" : "PATCH",
|
|
2812
|
+
body: await readJsonObject(required(flags.file, "xapi-to provider version update <service-id> <version-id> --file <contract.json>"))
|
|
2813
|
+
});
|
|
2814
|
+
break;
|
|
2815
|
+
}
|
|
2816
|
+
case "major": {
|
|
2817
|
+
if (rest[0] !== "create") err("usage: xapi-to provider major create <service-id>");
|
|
2818
|
+
const id = required(rest[1], "xapi-to provider major create <service-id>");
|
|
2819
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/majors"), { method: "POST" });
|
|
2820
|
+
break;
|
|
2821
|
+
}
|
|
2822
|
+
case "revision": {
|
|
2823
|
+
if (rest[0] !== "start") err("usage: xapi-to provider revision start <service-id> <major>");
|
|
2824
|
+
const id = required(rest[1], "xapi-to provider revision start <service-id> <major>");
|
|
2825
|
+
const major = positiveInt(required(rest[2], "xapi-to provider revision start <service-id> <major>"), "major");
|
|
2826
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/working-revision`), { method: "POST" });
|
|
2827
|
+
break;
|
|
2828
|
+
}
|
|
2829
|
+
case "publish": {
|
|
2830
|
+
const id = required(rest[0], "xapi-to provider publish <service-id> <revision-id>");
|
|
2831
|
+
const revisionId = required(rest[1], "xapi-to provider publish <service-id> <revision-id>");
|
|
2832
|
+
const changelog = await textOption(flags, "changelog", "changelog-file");
|
|
2833
|
+
if (changelog !== void 0 && changelog.length > 2e3) err("changelog is too long", "Maximum length is 2000 characters.");
|
|
2834
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/revisions/${encodeURIComponent(revisionId)}/submit`), {
|
|
2835
|
+
method: "POST",
|
|
2836
|
+
body: changelog === void 0 ? {} : { changelog }
|
|
2837
|
+
});
|
|
2838
|
+
break;
|
|
2839
|
+
}
|
|
2840
|
+
case "rollback": {
|
|
2841
|
+
const id = required(rest[0], "xapi-to provider rollback <service-id> <major> --revision <revision-id>");
|
|
2842
|
+
const major = positiveInt(required(rest[1], "xapi-to provider rollback <service-id> <major> --revision <revision-id>"), "major");
|
|
2843
|
+
const revisionId = requiredFlag(flags.revision, "xapi-to provider rollback <service-id> <major> --revision <revision-id>");
|
|
2844
|
+
const reason = await textOption(flags, "reason", "reason-file");
|
|
2845
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/rollback`), {
|
|
2846
|
+
method: "POST",
|
|
2847
|
+
body: { revisionId, ...reason !== void 0 ? { reason } : {} }
|
|
2848
|
+
});
|
|
2849
|
+
break;
|
|
2850
|
+
}
|
|
2851
|
+
case "default-major": {
|
|
2852
|
+
const id = required(rest[0], "xapi-to provider default-major <service-id> <major>");
|
|
2853
|
+
const major = positiveInt(required(rest[1], "xapi-to provider default-major <service-id> <major>"), "major");
|
|
2854
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/default-major"), { method: "PUT", body: { major } });
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
case "deprecate":
|
|
2858
|
+
case "restore": {
|
|
2859
|
+
const id = required(rest[0], `xapi-to provider ${command} <service-id> <major>`);
|
|
2860
|
+
const major = positiveInt(required(rest[1], `xapi-to provider ${command} <service-id> <major>`), "major");
|
|
2861
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/deprecated`), {
|
|
2862
|
+
method: "PUT",
|
|
2863
|
+
body: { deprecated: command === "deprecate" }
|
|
2864
|
+
});
|
|
2865
|
+
break;
|
|
2866
|
+
}
|
|
2867
|
+
case "review": {
|
|
2868
|
+
const id = required(rest[0], "xapi-to provider review <service-id> <revision-id>");
|
|
2869
|
+
const revisionId = required(rest[1], "xapi-to provider review <service-id> <revision-id>");
|
|
2870
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/revisions/${encodeURIComponent(revisionId)}/review`), { retries: READ_RETRIES3 });
|
|
2871
|
+
break;
|
|
2872
|
+
}
|
|
2873
|
+
case "diff": {
|
|
2874
|
+
const id = required(rest[0], "xapi-to provider diff <service-id> <major>");
|
|
2875
|
+
const major = positiveInt(required(rest[1], "xapi-to provider diff <service-id> <major>"), "major");
|
|
2876
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/diff-preview`), { retries: READ_RETRIES3 });
|
|
2877
|
+
break;
|
|
2878
|
+
}
|
|
2879
|
+
case "metrics": {
|
|
2880
|
+
const days = positiveInt(flags.days, "days", 365);
|
|
2881
|
+
const path = new URL(`https://placeholder${rest[0] ? servicePath(rest[0], "/metrics") : `${BASE}/metrics`}`);
|
|
2882
|
+
if (days) path.searchParams.set("days", String(days));
|
|
2883
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
2884
|
+
break;
|
|
2885
|
+
}
|
|
2886
|
+
case "events": {
|
|
2887
|
+
const limit = positiveInt(flags.limit, "limit", 100);
|
|
2888
|
+
const path = new URL("https://placeholder/api/agent/events");
|
|
2889
|
+
const after = optionalFlag(flags.after, "--after");
|
|
2890
|
+
if (after !== void 0) path.searchParams.set("after", after);
|
|
2891
|
+
if (limit) path.searchParams.set("limit", String(limit));
|
|
2892
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
case "skill": {
|
|
2896
|
+
const subcommand = required(rest[0], "xapi-to provider skill context|scaffold|link|unlink|fingerprint ...");
|
|
2897
|
+
const id = required(rest[1], `xapi-to provider skill ${subcommand} <service-id>`);
|
|
2898
|
+
if (subcommand === "context" || subcommand === "scaffold") {
|
|
2899
|
+
const destination = subcommand === "scaffold" ? requiredFlag(flags.output, "xapi-to provider skill scaffold <service-id> --output <SKILL.md>") : void 0;
|
|
2900
|
+
const context = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/skill-context"), { retries: READ_RETRIES3 });
|
|
2901
|
+
if (subcommand === "scaffold") {
|
|
2902
|
+
if (!context || typeof context.scaffoldMarkdown !== "string") throw new Error("skill context response is missing scaffoldMarkdown");
|
|
2903
|
+
const savedTo = await writeExclusive(destination, context.scaffoldMarkdown, boolFlag(flags, "force"));
|
|
2904
|
+
result = { serviceId: id, savedTo, currentFingerprint: context.currentFingerprint };
|
|
2905
|
+
} else {
|
|
2906
|
+
result = context;
|
|
2907
|
+
}
|
|
2908
|
+
} else if (subcommand === "link") {
|
|
2909
|
+
const skillId = required(rest[2], "xapi-to provider skill link <service-id> <skill-id>");
|
|
2910
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), { method: "PATCH", body: { linkedSkillId: skillId } });
|
|
2911
|
+
} else if (subcommand === "unlink") {
|
|
2912
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), { method: "PATCH", body: { linkedSkillId: null } });
|
|
2913
|
+
} else if (subcommand === "fingerprint") {
|
|
2914
|
+
const skillVersionId = optionalFlag(flags["skill-version-id"], "--skill-version-id");
|
|
2915
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/skill-fingerprint"), {
|
|
2916
|
+
method: "PUT",
|
|
2917
|
+
body: skillVersionId !== void 0 ? { skillVersionId } : {}
|
|
2918
|
+
});
|
|
2919
|
+
} else {
|
|
2920
|
+
err(`unknown provider skill command: ${subcommand}`, "Valid commands: context, scaffold, link, unlink, fingerprint.");
|
|
2921
|
+
}
|
|
2922
|
+
break;
|
|
2923
|
+
}
|
|
2924
|
+
case "delete": {
|
|
2925
|
+
const id = required(rest[0], "xapi-to provider delete <service-id> --confirm <service-name-or-id>");
|
|
2926
|
+
const confirm = requiredFlag(flags.confirm, "xapi-to provider delete <service-id> --confirm <service-name-or-id>");
|
|
2927
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), { method: "DELETE", body: { confirm } });
|
|
2928
|
+
break;
|
|
2929
|
+
}
|
|
2930
|
+
default:
|
|
2931
|
+
err(`unknown provider command: ${command}`, 'Run "xapi-to provider --help".');
|
|
2932
|
+
}
|
|
2933
|
+
output(result, flags.format);
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
err("provider request failed", error.message);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// src/commands/skill.ts
|
|
2940
|
+
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
2941
|
+
import { relative, resolve as resolve3, sep } from "path";
|
|
2942
|
+
var READ_RETRIES4 = 2;
|
|
2943
|
+
var MAX_FILES = 100;
|
|
2944
|
+
var MAX_PACKAGE_BYTES = 2 * 1024 * 1024;
|
|
2945
|
+
var MAX_FILE_BYTES = 512 * 1024;
|
|
2946
|
+
var BASE2 = "/api/skills/agent";
|
|
2947
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2948
|
+
var SKILL_HELP = `xapi-to skill - Upload and publish service usage skills
|
|
2949
|
+
|
|
2950
|
+
USAGE
|
|
2951
|
+
xapi-to skill spec
|
|
2952
|
+
xapi-to skill submit --dir <skill-directory>
|
|
2953
|
+
xapi-to skill submit --github <public-github-url> [metadata flags]
|
|
2954
|
+
xapi-to skill status <submission-id>
|
|
2955
|
+
xapi-to skill wait <submission-id> [--interval 2s] [--timeout 10m]
|
|
2956
|
+
|
|
2957
|
+
GITHUB METADATA FLAGS
|
|
2958
|
+
--version <semver>
|
|
2959
|
+
--name <display-name>
|
|
2960
|
+
--description <text>
|
|
2961
|
+
--category <value> Repeat is not supported; use comma-separated values
|
|
2962
|
+
--tag <value> Repeat is not supported; use comma-separated values
|
|
2963
|
+
|
|
2964
|
+
Local submissions recursively upload regular files. Symlinks, .git, and
|
|
2965
|
+
node_modules are excluded. The package must contain SKILL.md, have at most
|
|
2966
|
+
100 files, keep each file at or below 512 KiB, and keep the encoded package
|
|
2967
|
+
at or below 2 MiB.
|
|
2968
|
+
|
|
2969
|
+
SCOPES
|
|
2970
|
+
spec/status/wait: skill:read
|
|
2971
|
+
submit: skill:submit
|
|
2972
|
+
`;
|
|
2973
|
+
function required2(value, usage2) {
|
|
2974
|
+
if (!value?.trim() || value === "true") err(`usage: ${usage2}`);
|
|
2975
|
+
return value.trim();
|
|
2976
|
+
}
|
|
2977
|
+
function parseDuration(raw, flagName) {
|
|
2978
|
+
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h)?$/);
|
|
2979
|
+
if (!match) err(`${flagName} must be a duration such as 500ms, 2s, 5m, or 1h`);
|
|
2980
|
+
const value = Number(match[1]);
|
|
2981
|
+
const multiplier = match[2] === "h" ? 36e5 : match[2] === "m" ? 6e4 : match[2] === "s" ? 1e3 : 1;
|
|
2982
|
+
const result = value * multiplier;
|
|
2983
|
+
if (!Number.isSafeInteger(result) || result <= 0) err(`${flagName} must be greater than 0`);
|
|
2984
|
+
return result;
|
|
2985
|
+
}
|
|
2986
|
+
function listFlag(value) {
|
|
2987
|
+
if (!value || value === "true") return void 0;
|
|
2988
|
+
const items = [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
|
|
2989
|
+
return items.length ? items : void 0;
|
|
2990
|
+
}
|
|
2991
|
+
async function collectInlineFiles(directory) {
|
|
2992
|
+
const root = resolve3(directory);
|
|
2993
|
+
const files = [];
|
|
2994
|
+
async function walk(current) {
|
|
2995
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
2996
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2997
|
+
if (entry.isSymbolicLink()) continue;
|
|
2998
|
+
const absolute = resolve3(current, entry.name);
|
|
2999
|
+
if (entry.isDirectory()) {
|
|
3000
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) await walk(absolute);
|
|
3001
|
+
continue;
|
|
3002
|
+
}
|
|
3003
|
+
if (!entry.isFile()) continue;
|
|
3004
|
+
if (files.length >= MAX_FILES) {
|
|
3005
|
+
throw new Error(`skill package exceeds ${MAX_FILES} files`);
|
|
3006
|
+
}
|
|
3007
|
+
const content = await readFile3(absolute);
|
|
3008
|
+
if (content.byteLength > MAX_FILE_BYTES) {
|
|
3009
|
+
throw new Error(`skill file ${entry.name} exceeds ${MAX_FILE_BYTES} bytes`);
|
|
3010
|
+
}
|
|
3011
|
+
const path = relative(root, absolute).split(sep).join("/");
|
|
3012
|
+
if (!path || path.startsWith("../")) throw new Error("skill file escaped the selected directory");
|
|
3013
|
+
files.push({ path, contentBase64: content.toString("base64") });
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
await walk(root);
|
|
3017
|
+
if (!files.some((file) => file.path === "SKILL.md")) {
|
|
3018
|
+
throw new Error("skill package root must contain SKILL.md");
|
|
3019
|
+
}
|
|
3020
|
+
const encodedBytes = Buffer.byteLength(JSON.stringify({ sourceType: "inline", files }), "utf8");
|
|
3021
|
+
if (encodedBytes > MAX_PACKAGE_BYTES) {
|
|
3022
|
+
throw new Error(`encoded skill package exceeds ${MAX_PACKAGE_BYTES} bytes`);
|
|
3023
|
+
}
|
|
3024
|
+
return files;
|
|
3025
|
+
}
|
|
3026
|
+
function statusOf(value) {
|
|
3027
|
+
return value?.status || value?.submission?.status || value?.skill?.status;
|
|
3028
|
+
}
|
|
3029
|
+
function sleep3(ms) {
|
|
3030
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
3031
|
+
}
|
|
3032
|
+
async function getSubmission(apiKey, id, timeoutMs = 3e4, retries = READ_RETRIES4) {
|
|
3033
|
+
return apiKeyApiRequest(
|
|
3034
|
+
XAPI_API_HOST,
|
|
3035
|
+
apiKey,
|
|
3036
|
+
`${BASE2}/submissions/${encodeURIComponent(id)}`,
|
|
3037
|
+
{ timeoutMs, retries }
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
async function waitForSubmission(apiKey, id, flags) {
|
|
3041
|
+
const intervalMs = parseDuration(flags.interval || "2s", "--interval");
|
|
3042
|
+
const timeoutMs = parseDuration(flags.timeout || "10m", "--timeout");
|
|
3043
|
+
const startedAt = Date.now();
|
|
3044
|
+
const deadline = startedAt + timeoutMs;
|
|
3045
|
+
while (true) {
|
|
3046
|
+
const remaining = deadline - Date.now();
|
|
3047
|
+
if (remaining <= 0) {
|
|
3048
|
+
err("skill wait timeout", `submission_id=${id}, elapsed_ms=${Date.now() - startedAt}, timeout_ms=${timeoutMs}`);
|
|
3049
|
+
}
|
|
3050
|
+
try {
|
|
3051
|
+
const result = await getSubmission(apiKey, id, remaining, 0);
|
|
3052
|
+
const status = statusOf(result);
|
|
3053
|
+
if (status === "PUBLISHED") return result;
|
|
3054
|
+
if (["NEEDS_CHANGES", "REJECTED", "SUSPENDED", "ARCHIVED"].includes(String(status))) {
|
|
3055
|
+
output(result, flags.format);
|
|
3056
|
+
process.exit(1);
|
|
3057
|
+
}
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
const pending = error instanceof HttpError && error.status === 404;
|
|
3060
|
+
if (!pending && !isRetryableRequestError(error)) throw error;
|
|
3061
|
+
}
|
|
3062
|
+
await sleep3(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
async function skill(args, flags) {
|
|
3066
|
+
if (flags.help || args.length === 0) {
|
|
3067
|
+
console.log(SKILL_HELP);
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
const cfg = getConfig();
|
|
3071
|
+
requireApiKey(cfg);
|
|
3072
|
+
const apiKey = cfg.apiKey;
|
|
3073
|
+
const [command, ...rest] = args;
|
|
3074
|
+
try {
|
|
3075
|
+
let result;
|
|
3076
|
+
if (command === "spec") {
|
|
3077
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE2}/spec`, { retries: READ_RETRIES4 });
|
|
3078
|
+
} else if (command === "submit") {
|
|
3079
|
+
const directory = flags.dir;
|
|
3080
|
+
const github = flags.github;
|
|
3081
|
+
if (directory && github || !directory && !github) {
|
|
3082
|
+
err("choose exactly one skill source", "Pass either --dir <path> or --github <public-url>.");
|
|
3083
|
+
}
|
|
3084
|
+
if (directory) {
|
|
3085
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE2}/submissions`, {
|
|
3086
|
+
method: "POST",
|
|
3087
|
+
body: { files: await collectInlineFiles(required2(directory, "xapi-to skill submit --dir <skill-directory>")) }
|
|
3088
|
+
});
|
|
3089
|
+
} else {
|
|
3090
|
+
const body = {
|
|
3091
|
+
url: required2(github, "xapi-to skill submit --github <public-github-url>")
|
|
3092
|
+
};
|
|
3093
|
+
for (const name of ["version", "name", "description"]) {
|
|
3094
|
+
if (flags[name] && flags[name] !== "true") body[name] = flags[name];
|
|
3095
|
+
}
|
|
3096
|
+
const categories = listFlag(flags.category);
|
|
3097
|
+
const tags = listFlag(flags.tag);
|
|
3098
|
+
if (categories) body.categories = categories;
|
|
3099
|
+
if (tags) body.tags = tags;
|
|
3100
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE2}/submissions/github`, { method: "POST", body });
|
|
3101
|
+
}
|
|
3102
|
+
} else if (command === "status") {
|
|
3103
|
+
result = await getSubmission(apiKey, required2(rest[0], "xapi-to skill status <submission-id>"));
|
|
3104
|
+
} else if (command === "wait") {
|
|
3105
|
+
result = await waitForSubmission(apiKey, required2(rest[0], "xapi-to skill wait <submission-id>"), flags);
|
|
3106
|
+
} else {
|
|
3107
|
+
err(`unknown skill command: ${command}`, "Valid commands: spec, submit, status, wait.");
|
|
3108
|
+
}
|
|
3109
|
+
output(result, flags.format);
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
if (error instanceof Error && error.message === "process.exit") throw error;
|
|
3112
|
+
err("skill request failed", error.message);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
|
|
2130
3116
|
// src/args.ts
|
|
2131
3117
|
function parseArgs(argv) {
|
|
2132
3118
|
const positional = [];
|
|
@@ -2178,7 +3164,7 @@ COMMANDS
|
|
|
2178
3164
|
--page N --page-size N Pagination
|
|
2179
3165
|
--category <name> Filter by category
|
|
2180
3166
|
--service-id <id> Filter by service
|
|
2181
|
-
search <query> Search actions by keyword
|
|
3167
|
+
search <query> Search actions by keyword (--all-versions: \u542B\u975E\u9ED8\u8BA4\u4F46\u4ECD\u5728\u8DD1\u7684\u5927\u7248\u672C)
|
|
2182
3168
|
--source capability|api Filter by source type
|
|
2183
3169
|
--category <name> Filter by category
|
|
2184
3170
|
--page N --page-size N Pagination
|
|
@@ -2205,6 +3191,22 @@ COMMANDS
|
|
|
2205
3191
|
--timeout <duration> Max wait duration, e.g. 10m
|
|
2206
3192
|
--max-attempts <number> Max poll attempts
|
|
2207
3193
|
|
|
3194
|
+
sandbox <command> Managed cloud sandbox lifecycle
|
|
3195
|
+
run --command <shell> Quote, create, execute, and auto-terminate
|
|
3196
|
+
offerings|quote|list|history|get|create|wait|exec
|
|
3197
|
+
file|port|extension|audit|suspend|resume|terminate
|
|
3198
|
+
Run "xapi-to sandbox --help" for selection and safety flags
|
|
3199
|
+
|
|
3200
|
+
provider <command> Manage provider services, releases, metrics, events, and linked skills
|
|
3201
|
+
list|get|create|update|versions|version|major|revision
|
|
3202
|
+
publish|rollback|default-major|deprecate|restore|review|diff
|
|
3203
|
+
metrics|events|skill|delete
|
|
3204
|
+
Run "xapi-to provider --help" for metadata and lifecycle flags
|
|
3205
|
+
|
|
3206
|
+
skill <command> Upload and submit service usage skills
|
|
3207
|
+
spec|submit|status|wait
|
|
3208
|
+
Run "xapi-to skill --help" for local directory and GitHub workflows
|
|
3209
|
+
|
|
2208
3210
|
oauth bind [--provider twitter] Bind Twitter OAuth to your API key
|
|
2209
3211
|
oauth status List current OAuth bindings
|
|
2210
3212
|
oauth unbind <binding-id> Remove an OAuth binding
|
|
@@ -2214,6 +3216,14 @@ COMMANDS
|
|
|
2214
3216
|
--referral-code <code> Register with an inviter's referral code (also: --referralCode, or as positional arg)
|
|
2215
3217
|
--force Replace an existing file-based apiKey
|
|
2216
3218
|
balance Show current account balance
|
|
3219
|
+
usage <request-id> Show a finalized per-request cost receipt
|
|
3220
|
+
usage wait <request-id> Wait until a request receipt is finalized
|
|
3221
|
+
--interval <duration> Poll interval (default: 1s)
|
|
3222
|
+
--timeout <duration> Max wait duration (default: 30s)
|
|
3223
|
+
earnings [summary] Show spendable balance and provider earnings
|
|
3224
|
+
earnings list List provider earning records
|
|
3225
|
+
earnings transfer <usd> --idempotency-key <key>
|
|
3226
|
+
Reinvest settled earnings into xapi balance
|
|
2217
3227
|
topup [--amount <usd>] [--method stripe|x402] Generate payment URL
|
|
2218
3228
|
|
|
2219
3229
|
health Check backend connectivity
|
|
@@ -2231,6 +3241,7 @@ ENV VARS
|
|
|
2231
3241
|
XAPI_API_KEY Compatible API key alias
|
|
2232
3242
|
XAPI_ACTION_HOST Action service host (default: action.xapi.to)
|
|
2233
3243
|
XAPI_API_HOST Auth/account service host (default: api.xapi.to)
|
|
3244
|
+
XAPI_SANDBOX_HOST Sandbox gateway host (default: sandbox.xapi.to)
|
|
2234
3245
|
XAPI_OUTPUT Default output format
|
|
2235
3246
|
XAPI_TRANSFER_IDLE_TIMEOUT_MS SSE/download idle timeout (default: 60000)
|
|
2236
3247
|
|
|
@@ -2250,9 +3261,18 @@ EXAMPLES
|
|
|
2250
3261
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
|
|
2251
3262
|
xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
|
|
2252
3263
|
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 2s --timeout 10m
|
|
3264
|
+
xapi-to sandbox run --command 'python3 -c "print(6*7)"'
|
|
2253
3265
|
xapi-to categories
|
|
2254
3266
|
xapi-to services --format table
|
|
2255
3267
|
xapi-to config set apiKey=xapi_abc123
|
|
3268
|
+
xapi-to earnings
|
|
3269
|
+
xapi-to usage c7fe24d5-e1d4-4bc1-a9bb-e16df8ab93b0
|
|
3270
|
+
xapi-to usage wait c7fe24d5-e1d4-4bc1-a9bb-e16df8ab93b0 --timeout 1m
|
|
3271
|
+
xapi-to earnings transfer 1 --idempotency-key reinvest-001
|
|
3272
|
+
xapi-to provider update svc_123 --about-file ./ABOUT.md --website https://example.com
|
|
3273
|
+
xapi-to provider publish svc_123 rev_456 --changelog-file ./CHANGELOG.md
|
|
3274
|
+
xapi-to skill submit --dir ./skills/my-service
|
|
3275
|
+
xapi-to provider skill link svc_123 11111111-1111-4111-8111-111111111111
|
|
2256
3276
|
xapi-to health
|
|
2257
3277
|
`;
|
|
2258
3278
|
async function main() {
|
|
@@ -2288,6 +3308,10 @@ async function main() {
|
|
|
2288
3308
|
return actionBatchGet(rest, flags);
|
|
2289
3309
|
case "call":
|
|
2290
3310
|
return actionCall2(rest, flags);
|
|
3311
|
+
case "provider":
|
|
3312
|
+
return provider(rest, flags);
|
|
3313
|
+
case "skill":
|
|
3314
|
+
return skill(rest, flags);
|
|
2291
3315
|
case "task": {
|
|
2292
3316
|
if (rest.length === 0) {
|
|
2293
3317
|
console.log(taskHelp());
|
|
@@ -2305,6 +3329,54 @@ async function main() {
|
|
|
2305
3329
|
}
|
|
2306
3330
|
break;
|
|
2307
3331
|
}
|
|
3332
|
+
case "sandbox": {
|
|
3333
|
+
if (rest.length === 0) {
|
|
3334
|
+
console.log(SANDBOX_HELP);
|
|
3335
|
+
process.exit(0);
|
|
3336
|
+
}
|
|
3337
|
+
const [subCmd, ...subRest] = rest;
|
|
3338
|
+
switch (subCmd) {
|
|
3339
|
+
case "offerings":
|
|
3340
|
+
return sandboxOfferings2(subRest, flags);
|
|
3341
|
+
case "quote":
|
|
3342
|
+
return sandboxQuote2(subRest, flags);
|
|
3343
|
+
case "list":
|
|
3344
|
+
return sandboxList2(subRest, flags);
|
|
3345
|
+
case "history":
|
|
3346
|
+
return sandboxHistory2(subRest, flags);
|
|
3347
|
+
case "get":
|
|
3348
|
+
return sandboxGet2(subRest, flags);
|
|
3349
|
+
case "create":
|
|
3350
|
+
return sandboxCreate2(subRest, flags);
|
|
3351
|
+
case "wait":
|
|
3352
|
+
return sandboxWait2(subRest, flags);
|
|
3353
|
+
case "exec":
|
|
3354
|
+
return sandboxExec2(subRest, flags);
|
|
3355
|
+
case "file":
|
|
3356
|
+
return sandboxFile(subRest, flags);
|
|
3357
|
+
case "port":
|
|
3358
|
+
return sandboxPort2(subRest, flags);
|
|
3359
|
+
case "extension":
|
|
3360
|
+
return sandboxExtension2(subRest, flags);
|
|
3361
|
+
case "audit":
|
|
3362
|
+
return sandboxAudit2(subRest, flags);
|
|
3363
|
+
case "suspend":
|
|
3364
|
+
return sandboxState("suspend", subRest, flags);
|
|
3365
|
+
case "resume":
|
|
3366
|
+
return sandboxState("resume", subRest, flags);
|
|
3367
|
+
case "terminate":
|
|
3368
|
+
return sandboxState("terminate", subRest, flags);
|
|
3369
|
+
case "run":
|
|
3370
|
+
return sandboxRun(subRest, flags);
|
|
3371
|
+
default:
|
|
3372
|
+
console.error(JSON.stringify({
|
|
3373
|
+
error: `unknown sandbox command: ${subCmd}`,
|
|
3374
|
+
hint: "run xapi-to sandbox --help"
|
|
3375
|
+
}));
|
|
3376
|
+
process.exit(1);
|
|
3377
|
+
}
|
|
3378
|
+
break;
|
|
3379
|
+
}
|
|
2308
3380
|
// ── OAuth commands ──
|
|
2309
3381
|
case "oauth": {
|
|
2310
3382
|
if (flags.help || rest.length === 0) {
|
|
@@ -2332,6 +3404,10 @@ async function main() {
|
|
|
2332
3404
|
return register(rest, flags);
|
|
2333
3405
|
case "balance":
|
|
2334
3406
|
return balance(rest, flags);
|
|
3407
|
+
case "usage":
|
|
3408
|
+
return usage(rest, flags);
|
|
3409
|
+
case "earnings":
|
|
3410
|
+
return earnings(rest, flags);
|
|
2335
3411
|
case "topup":
|
|
2336
3412
|
return topup(rest, flags);
|
|
2337
3413
|
case "health":
|