@ukiyocompute/cli 0.0.0-stage → 0.3.1

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.
@@ -0,0 +1,1077 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // packages/machine-client/src/state.ts
18
+ var state_exports = {};
19
+ __export(state_exports, {
20
+ withMachineState: () => withMachineState
21
+ });
22
+ import { DatabaseSync } from "node:sqlite";
23
+ import { mkdir, open, readFile, rename, chmod, lstat } from "node:fs/promises";
24
+ import { join } from "node:path";
25
+ import { randomUUID } from "node:crypto";
26
+ async function withMachineState(directory2, fn) {
27
+ await mkdir(directory2, { recursive: true, mode: 448 });
28
+ const dir = await lstat(directory2);
29
+ if (!dir.isDirectory() || dir.isSymbolicLink() || dir.uid !== process.getuid?.()) throw Error("State directory must be an owned directory, not a symlink");
30
+ await chmod(directory2, 448);
31
+ for (const name of ["lock.sqlite", "state.json"]) {
32
+ try {
33
+ const info = await lstat(join(directory2, name));
34
+ if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.()) throw Error("Unsafe machine state file");
35
+ } catch (e) {
36
+ if (e.code !== "ENOENT") throw e;
37
+ }
38
+ }
39
+ const lock = new DatabaseSync(join(directory2, "lock.sqlite"), { timeout: 1e3 });
40
+ try {
41
+ lock.exec("CREATE TABLE IF NOT EXISTS client_lock (id INTEGER PRIMARY KEY); BEGIN IMMEDIATE;");
42
+ const file = join(directory2, "state.json");
43
+ let state;
44
+ try {
45
+ state = JSON.parse(await readFile(file, "utf8"));
46
+ } catch (e) {
47
+ if (e.code !== "ENOENT") throw e;
48
+ state = { reservedMinor: 0, purchases: {} };
49
+ }
50
+ if (!Number.isSafeInteger(state.reservedMinor) || state.reservedMinor < 0 || !state.purchases) throw Error("Invalid machine client state; refusing to reset spend history");
51
+ const save = async () => {
52
+ const temporary = join(directory2, `state-${randomUUID()}.tmp`), f = await open(temporary, "wx", 384);
53
+ try {
54
+ await f.writeFile(JSON.stringify(state));
55
+ await f.sync();
56
+ } finally {
57
+ await f.close();
58
+ }
59
+ await rename(temporary, file);
60
+ const d = await open(directory2, "r");
61
+ try {
62
+ await d.sync();
63
+ } finally {
64
+ await d.close();
65
+ }
66
+ };
67
+ return await fn(state, save);
68
+ } finally {
69
+ try {
70
+ lock.exec("ROLLBACK");
71
+ } catch {
72
+ }
73
+ lock.close();
74
+ }
75
+ }
76
+ var init_state = __esm({
77
+ "packages/machine-client/src/state.ts"() {
78
+ "use strict";
79
+ }
80
+ });
81
+
82
+ // apps/cli/src/main.ts
83
+ import { readFile as readFile3 } from "node:fs/promises";
84
+
85
+ // packages/machine-client/src/index.ts
86
+ import { readFile as readFile2, lstat as lstat2 } from "node:fs/promises";
87
+ import { resolve } from "node:path";
88
+ import { privateKeyToAccount } from "viem/accounts";
89
+ import { createKeyPairSignerFromBytes } from "@solana/kit";
90
+ import { x402Client } from "@x402/fetch";
91
+ import { registerExactEvmScheme } from "@x402/evm/exact/client";
92
+ import { Mppx, tempo } from "mppx/client";
93
+ import { Challenge } from "mppx";
94
+ import { solana } from "@solana/mpp/client";
95
+ import { generateKeyPair, exportJWK, importJWK, compactDecrypt } from "jose";
96
+
97
+ // packages/core/src/machine-client-policy.ts
98
+ import { createHash } from "node:crypto";
99
+
100
+ // packages/core/src/domain.ts
101
+ var DomainError = class extends Error {
102
+ constructor(code, message, details) {
103
+ super(message);
104
+ this.code = code;
105
+ this.details = details;
106
+ this.name = "DomainError";
107
+ }
108
+ code;
109
+ details;
110
+ };
111
+
112
+ // packages/core/src/machine-client-policy.ts
113
+ function canonical(v) {
114
+ if (v === null || typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
115
+ if (typeof v === "number" && Number.isSafeInteger(v)) return String(v);
116
+ if (Array.isArray(v)) return "[" + v.map(canonical).join(",") + "]";
117
+ if (v && typeof v === "object") return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + canonical(v[k])).join(",") + "}";
118
+ throw new DomainError("INVALID_QUOTE", "Invalid quote JSON");
119
+ }
120
+ function validateMachineQuote(envelope, request, policy) {
121
+ const e = envelope, q = e?.quote;
122
+ const fail = () => {
123
+ throw new DomainError("QUOTE_MISMATCH", "Quote differs from the requested purchase or local payment policy");
124
+ };
125
+ if (!q || typeof e.digest !== "string" || createHash("sha256").update(canonical(q)).digest("hex") !== e.digest) fail();
126
+ if (q.origin !== policy.origin || canonical(q.request) !== canonical(request) || !Number.isSafeInteger(q.amountMinor) || q.amountMinor <= 0 || !Number.isFinite(Date.parse(q.expiresAt)) || Date.parse(q.expiresAt) <= Date.now()) fail();
127
+ if (![policy.perPaymentMinor, policy.remainingMinor].every((x) => Number.isSafeInteger(x) && x >= 0) || q.amountMinor > policy.perPaymentMinor || q.amountMinor > policy.remainingMinor || q.amountMinor > q.request.maxPriceMinor) throw new DomainError("SPEND_CAP_EXCEEDED", "Payment exceeds a local cap");
128
+ const r = q.requirements;
129
+ if (q.request.method === "base") {
130
+ if (r?.scheme !== "exact" || r.network !== policy.network || String(r.asset).toLowerCase() !== policy.asset.toLowerCase() || String(r.payTo).toLowerCase() !== policy.recipient.toLowerCase() || r.amount !== (BigInt(q.amountMinor) * 10000n).toString()) fail();
131
+ } else {
132
+ if (!["tempo", "solana"].includes(q.request.method)) fail();
133
+ const details = r?.methodDetails;
134
+ const allowed = q.request.method === "tempo" ? ["chainId", "feePayer"] : ["network", "decimals", "tokenProgram", "recentBlockhash"];
135
+ if (!details || Object.keys(details).some((k) => !allowed.includes(k)) || details.feePayer === true || r.splits || r.decimals !== void 0 && r.decimals !== 6) fail();
136
+ if (q.request.method === "solana" && (details.decimals !== 6 || details.tokenProgram && details.tokenProgram !== "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) fail();
137
+ const chain = q.request.method === "tempo" ? `eip155:${r?.methodDetails?.chainId}` : r?.methodDetails?.network === "devnet" ? "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" : r?.methodDetails?.network === "mainnet" ? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" : "";
138
+ if (chain !== policy.network || r?.currency !== policy.asset || r?.recipient !== policy.recipient || r?.amount !== (BigInt(q.amountMinor) * 10000n).toString()) fail();
139
+ }
140
+ return { digest: e.digest, amountMinor: q.amountMinor, quote: q };
141
+ }
142
+ var machineQuoteDigest = (value) => createHash("sha256").update(canonical(value)).digest("hex");
143
+
144
+ // packages/machine-client/src/account-topups.ts
145
+ var FundingError = class extends Error {
146
+ constructor(code, message, status = 0, body = null) {
147
+ super(message);
148
+ this.code = code;
149
+ this.status = status;
150
+ this.body = body;
151
+ }
152
+ code;
153
+ status;
154
+ body;
155
+ };
156
+ async function fundingRequest(origin, token, path, init = {}, fetcher = fetch) {
157
+ const url = new URL(path, origin);
158
+ if (url.origin !== new URL(origin).origin || !url.pathname.startsWith("/api/v1/")) throw new FundingError("INVALID_URL", "Cross-origin funding request refused");
159
+ const response = await fetcher(url, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4), headers: { authorization: `Bearer ${token}`, ...init.body ? { "content-type": "application/json" } : {}, ...init.headers } });
160
+ const body = await response.json();
161
+ if (!response.ok) throw new FundingError(body?.error?.code ?? `HTTP_${response.status}`, body?.error?.message ?? JSON.stringify(body), response.status, body);
162
+ return body.settlementPending ? { ...body.data, settlementPending: true, message: "Settlement pending; do not pay again." } : body.data ?? body;
163
+ }
164
+ async function createAccountTopup(origin, token, amountMinor, idempotencyKey, fetcher = fetch) {
165
+ if (!idempotencyKey?.trim()) throw new FundingError("IDEMPOTENCY_KEY_REQUIRED", "Supply one stable top-up key and reuse it on retries");
166
+ const terms = await fundingRequest(origin, token, "/api/v1/terms", {}, fetcher);
167
+ if (!terms?.version || !/^[a-f0-9]{64}$/.test(terms.sha256 ?? "") || !/^https:\/\//.test(terms.url ?? "")) throw new FundingError("TERMS_UNAVAILABLE", "Read current approved Terms before authorizing this top-up");
168
+ return fundingRequest(origin, token, "/api/v1/billing/machine-topups", { method: "POST", headers: { "idempotency-key": idempotencyKey }, body: JSON.stringify({ amountMinor, termsAcceptance: { accepted: true, version: terms.version, sha256: terms.sha256 } }) }, fetcher);
169
+ }
170
+ async function topupChallenge(origin, token, topup, method, fetcher = fetch) {
171
+ const selected = topup.paymentMethods?.find((m) => m.method === method);
172
+ if (!selected) throw new FundingError("PAYMENT_METHOD_DISABLED", `Payment method ${method} disabled. Enabled rails: ${(topup.paymentMethods ?? []).map((m) => m.method).join(", ")}`);
173
+ const expected = `/api/v1/billing/machine-topups/${encodeURIComponent(topup.topUpId)}/pay${method === "tempo" ? "" : "/" + method}`;
174
+ const url = new URL(selected.url);
175
+ if (url.username || url.password || url.origin !== new URL(origin).origin || url.pathname !== expected || url.search || url.hash) throw new FundingError("INVALID_URL", "Unexpected top-up payment URL");
176
+ const response = await fetcher(url, { method: "POST", headers: { "x-ukiyo-api-key": token }, redirect: "error", signal: AbortSignal.timeout(3e4) });
177
+ const body = await response.json();
178
+ if (response.status === 202 || response.ok) return { topup: body.data ?? topup, settlementPending: response.status === 202 || body.settlementPending === true, message: response.status === 202 ? "Settlement pending; do not pay again." : void 0 };
179
+ if (response.status !== 402) throw new FundingError(body?.error?.code ?? `HTTP_${response.status}`, body?.error?.message ?? "Top-up challenge failed", response.status, body);
180
+ return { topup, paymentUrl: url.href, method, protocol: selected.protocol, challenge: body, paymentRequired: response.headers.get("payment-required"), wwwAuthenticate: response.headers.get("www-authenticate") };
181
+ }
182
+
183
+ // packages/machine-client/src/index.ts
184
+ async function withMachineState2(directory2, fn) {
185
+ if (Number(process.versions.node.split(".")[0]) < 24) throw Error("Wallet purchase commands require Node.js 24 or newer");
186
+ return (await Promise.resolve().then(() => (init_state(), state_exports))).withMachineState(directory2, fn);
187
+ }
188
+ async function loadMachineClient(path) {
189
+ const cfg = JSON.parse(await readFile2(path, "utf8"));
190
+ if (!["base", "solana", "tempo"].includes(cfg.method) || !Number.isSafeInteger(cfg.perPaymentMinor) || !Number.isSafeInteger(cfg.totalMinor) || cfg.perPaymentMinor <= 0 || cfg.totalMinor < cfg.perPaymentMinor) throw Error("Explicit positive payment and cumulative caps are required");
191
+ const url = new URL(cfg.origin);
192
+ if (url.origin !== cfg.origin || url.protocol !== "https:" && !(url.protocol === "http:" && url.hostname === "127.0.0.1")) throw Error("Configured origin must be HTTPS (loopback allowed for tests)");
193
+ for (const p of [cfg.walletFile, cfg.stateDirectory]) if (typeof p !== "string" || resolve(p) !== p) throw Error("Wallet and state paths must be absolute");
194
+ const info = await lstat2(cfg.walletFile);
195
+ if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.() || info.mode & 63) throw Error("Wallet file must be private (0600)");
196
+ const key = (await readFile2(cfg.walletFile, "utf8")).trim();
197
+ let signer;
198
+ try {
199
+ signer = cfg.method === "solana" ? await createKeyPairSignerFromBytes(Buffer.from(key, "base64")) : privateKeyToAccount(key);
200
+ } catch {
201
+ throw Error("Invalid private wallet file");
202
+ }
203
+ return new MachinePurchaseClient(cfg, signer);
204
+ }
205
+ var MachinePurchaseClient = class {
206
+ constructor(config, signer, fetcher = fetch) {
207
+ this.config = config;
208
+ this.signer = signer;
209
+ this.fetcher = fetcher;
210
+ }
211
+ config;
212
+ signer;
213
+ fetcher;
214
+ async post(path, body, headers = {}) {
215
+ const u = new URL(path, this.config.origin);
216
+ if (u.origin !== this.config.origin) throw Error("Cross-origin payment request refused");
217
+ const r = await this.fetcher(u, { method: "POST", redirect: "error", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body), signal: AbortSignal.timeout(3e4) });
218
+ const value = await r.json();
219
+ if (![200, 202, 402].includes(r.status)) throw Error(value?.error?.code ?? `HTTP ${r.status}`);
220
+ return { status: r.status, value, headers: r.headers };
221
+ }
222
+ async recover(p) {
223
+ const r = await this.fetcher(`${this.config.origin}/api/machine/quotes/${p.envelope.digest}/recovery`, { redirect: "error" });
224
+ if (!r.ok) throw Error("Purchase recovery unavailable");
225
+ const value = await r.json();
226
+ if (!value.encrypted) throw Error("Payment still pending; retry original payment");
227
+ const plain = await compactDecrypt(value.encrypted, await importJWK(p.privateKey, "ECDH-ES"));
228
+ const result = JSON.parse(Buffer.from(plain.plaintext).toString());
229
+ if (result.purchaseId !== p.envelope.digest) throw Error("Purchase mismatch");
230
+ return result;
231
+ }
232
+ async action(id, action) {
233
+ return withMachineState2(this.config.stateDirectory, async (state, save) => {
234
+ const p = state.purchases[id];
235
+ if (!p?.envelope) throw Error("Saved purchase is required for recovery");
236
+ if (p.binding !== machineQuoteDigest({ ...this.config, signer: this.signer.address })) throw Error("Wallet policy mismatch");
237
+ const current = p.result?.accessToken ? p.result : await this.recover(p);
238
+ const r = await this.post(`/api/machine/quotes/${p.envelope.digest}/actions/${action}`, {}, { authorization: `Bearer ${current.accessToken}` });
239
+ if (r.value.purchaseId !== p.envelope.digest) throw Error("Purchase mismatch");
240
+ p.result = r.value;
241
+ await save();
242
+ return r.value;
243
+ });
244
+ }
245
+ /** Account funding uses the same signer, policy and durable payment store as purchases. */
246
+ async fundTopup(topup, token, method = this.config.method) {
247
+ if (method !== this.config.method) throw new FundingError("WALLET_POLICY_MISMATCH", "Requested rail differs from configured wallet");
248
+ if (!/^[a-f0-9-]{36}$/i.test(topup.topUpId ?? "") || !Number.isSafeInteger(topup.amountMinor) || topup.amountMinor <= 0) throw new FundingError("INVALID_TOPUP", "Invalid account top-up");
249
+ return withMachineState2(this.config.stateDirectory, async (state, save) => {
250
+ const id = "topup:" + topup.topUpId, binding = machineQuoteDigest({ ...this.config, signer: this.signer.address });
251
+ let p = state.purchases[id];
252
+ if (p && (p.binding !== binding || p.amountMinor !== topup.amountMinor)) throw new FundingError("WALLET_POLICY_MISMATCH", "Saved top-up conflicts with wallet policy");
253
+ if (p?.pending || p?.complete) {
254
+ const result2 = await fundingRequest(this.config.origin, token, `/api/v1/billing/machine-topups/${topup.topUpId}`, {}, this.fetcher);
255
+ return { topup: result2, settlementPending: result2.status !== "SUCCEEDED", message: result2.status !== "SUCCEEDED" ? "Settlement pending; do not pay again." : void 0 };
256
+ }
257
+ if (!p) {
258
+ if (topup.amountMinor > this.config.perPaymentMinor || topup.amountMinor > this.config.totalMinor - state.reservedMinor) throw new FundingError("SPEND_CAP_EXCEEDED", "Top-up exceeds local wallet spending caps");
259
+ p = state.purchases[id] = { binding, amountMinor: topup.amountMinor };
260
+ state.reservedMinor += topup.amountMinor;
261
+ await save();
262
+ }
263
+ if (!p.payment) {
264
+ const q = await topupChallenge(this.config.origin, token, topup, method, this.fetcher);
265
+ if (!q.paymentUrl) {
266
+ p.pending = q.settlementPending;
267
+ p.complete = !q.settlementPending;
268
+ await save();
269
+ return q;
270
+ }
271
+ p.url = q.paymentUrl;
272
+ const required = method === "base" ? JSON.parse(Buffer.from(q.paymentRequired ?? "", "base64").toString()) : void 0;
273
+ const challenge = method === "base" ? void 0 : Challenge.fromResponse(new Response(null, { status: 402, headers: { "www-authenticate": q.wwwAuthenticate ?? "" } }));
274
+ const requirements = method === "base" ? required.accepts?.[0] : { ...challenge.request };
275
+ if (method === "base" && (required.resource?.url !== p.url || required.accepts?.length !== 1)) throw new FundingError("QUOTE_MISMATCH", "Unexpected funding resource");
276
+ if (challenge && (challenge.method !== method || challenge.intent !== "charge")) throw new FundingError("QUOTE_MISMATCH", "Unexpected funding challenge");
277
+ if (method !== "base") delete requirements.externalId;
278
+ const request = { method, maxPriceMinor: topup.amountMinor };
279
+ const quote = { origin: this.config.origin, request, amountMinor: topup.amountMinor, expiresAt: topup.expiresAt, requirements };
280
+ validateMachineQuote({ quote, digest: machineQuoteDigest(quote) }, request, { ...this.config, remainingMinor: topup.amountMinor });
281
+ if (method === "base") {
282
+ const client = registerExactEvmScheme(new x402Client().setSpendControls({ maxAmountPerPayment: `$${(this.config.perPaymentMinor / 100).toFixed(2)}` }), { signer: this.signer });
283
+ p.payment = await client.createPaymentPayload(required);
284
+ } else {
285
+ const methods = method === "solana" ? [solana.charge({ signer: this.signer, rpcUrl: this.config.network === "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com" })] : [...tempo({ account: this.signer })];
286
+ const client = Mppx.create({ methods, polyfill: false, fetch: async (i, init) => {
287
+ const credential = new Request(i, init).headers.get("authorization");
288
+ if (credential) {
289
+ p.payment = credential;
290
+ return Response.json({ captured: true });
291
+ }
292
+ return new Response(null, { status: 402, headers: { "www-authenticate": Challenge.serialize(challenge) } });
293
+ } });
294
+ await client.fetch(p.url, { method: "POST" });
295
+ if (!p.payment) throw new FundingError("SIGNING_FAILED", "Payment signer returned no credential");
296
+ }
297
+ await save();
298
+ }
299
+ const headers = { "x-ukiyo-api-key": token, ...method === "base" ? { "payment-signature": Buffer.from(JSON.stringify(p.payment)).toString("base64") } : { authorization: p.payment } };
300
+ const response = await this.fetcher(p.url, { method: "POST", headers, redirect: "error", signal: AbortSignal.timeout(3e4) });
301
+ const body = await response.json().catch(() => null);
302
+ if (response.status === 202 || body?.settlementPending) {
303
+ p.pending = true;
304
+ await save();
305
+ return { topup: body?.data ?? topup, settlementPending: true, message: "Settlement pending; do not pay again." };
306
+ }
307
+ if (!response.ok) throw new FundingError(body?.error?.code ?? `HTTP_${response.status}`, body?.error?.message ?? "Funding unresolved; retry only this same top-up", response.status, body);
308
+ const result = await fundingRequest(this.config.origin, token, `/api/v1/billing/machine-topups/${topup.topUpId}`, {}, this.fetcher);
309
+ p.complete = result.status === "SUCCEEDED";
310
+ p.pending = !p.complete;
311
+ await save();
312
+ return { topup: result, settlementPending: p.pending, ...p.pending ? { message: "Settlement pending; do not pay again." } : {} };
313
+ });
314
+ }
315
+ async purchase(id, input, serviceCredit = false) {
316
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(id)) throw Error("Supply a stable purchase id for retries");
317
+ return withMachineState2(this.config.stateDirectory, async (state, save) => {
318
+ const binding = machineQuoteDigest({ ...this.config, signer: this.signer.address });
319
+ let p = state.purchases[id];
320
+ if (p && (p.binding !== binding || p.serviceCredit !== serviceCredit || canonical(p.input) !== canonical(input))) throw Error("Purchase id conflicts with saved request or wallet policy");
321
+ if (!p) {
322
+ const R = await generateKeyPair("ECDH-ES", { extractable: true });
323
+ p = state.purchases[id] = { binding, input, serviceCredit, privateKey: await exportJWK(R.privateKey), publicKey: await exportJWK(R.publicKey) };
324
+ await save();
325
+ }
326
+ const request = { ...input, requestId: id, method: this.config.method, recoveryKey: p.publicKey };
327
+ if (!p.envelope) {
328
+ const q = await this.post(`/api/machine/${input.kind === "gpu-job" ? "gpu-jobs" : "gpu-blocks"}`, request);
329
+ if (q.status !== 402) throw Error("Expected immutable payment quote");
330
+ validateMachineQuote(q.value, request, { ...this.config, remainingMinor: this.config.totalMinor - state.reservedMinor });
331
+ p.envelope = q.value;
332
+ p.required = q.headers.get("payment-required");
333
+ state.reservedMinor += q.value.quote.amountMinor;
334
+ await save();
335
+ }
336
+ const e = p.envelope;
337
+ if (serviceCredit) {
338
+ const source = Object.values(state.purchases).find((v) => v !== p && v.binding === binding && v.result?.accessToken);
339
+ if (!source) throw Error("A saved source purchase token is required to reuse service credit");
340
+ const paid2 = await this.post(`/api/machine/quotes/${e.digest}/credits`, { sourcePurchaseId: source.result.purchaseId }, { authorization: `Bearer ${source.result.accessToken}` });
341
+ if (paid2.value.purchaseId !== e.digest) throw Error("Purchase mismatch");
342
+ p.result = paid2.value;
343
+ await save();
344
+ return { pending: paid2.status === 202, purchaseId: e.digest, result: p.result };
345
+ }
346
+ if (!p.payment) {
347
+ validateMachineQuote(e, request, { ...this.config, remainingMinor: e.quote.amountMinor });
348
+ if (this.config.method === "base") {
349
+ const client = registerExactEvmScheme(new x402Client().setSpendControls({ maxAmountPerPayment: `$${(this.config.perPaymentMinor / 100).toFixed(2)}` }), { signer: this.signer });
350
+ const required = JSON.parse(Buffer.from(p.required ?? "", "base64").toString());
351
+ if (required.resource?.url !== `${this.config.origin}/api/machine/${input.kind === "gpu-job" ? "gpu-jobs" : "gpu-blocks"}` || canonical(required.accepts) !== canonical([e.quote.requirements])) throw Error("x402 wire requirements mismatch");
352
+ p.payment = await client.createPaymentPayload(required);
353
+ } else {
354
+ const challenge = e.paymentChallenge, normalized = { ...challenge?.request };
355
+ delete normalized.externalId;
356
+ if (canonical(normalized) !== canonical(e.quote.requirements) || challenge.method !== this.config.method || this.config.method === "solana" && challenge.request.externalId !== e.digest) throw Error("MPP challenge does not match immutable quote");
357
+ const methods = this.config.method === "solana" ? [solana.charge({ signer: this.signer, rpcUrl: this.config.network === "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com" })] : [...tempo({ account: this.signer })];
358
+ const client = Mppx.create({ methods, polyfill: false, fetch: async (i, init) => {
359
+ const r = new Request(i, init);
360
+ const credential = r.headers.get("authorization");
361
+ if (credential) {
362
+ p.payment = credential;
363
+ return Response.json({ captured: true });
364
+ }
365
+ return new Response(null, { status: 402, headers: { "www-authenticate": Challenge.serialize(challenge) } });
366
+ } });
367
+ await client.fetch(`${this.config.origin}/api/machine/${input.kind === "gpu-job" ? "gpu-jobs" : "gpu-blocks"}`, { method: "POST", body: JSON.stringify(request) });
368
+ if (!p.payment) throw Error("Payment signer returned no credential");
369
+ }
370
+ await save();
371
+ }
372
+ const headers = this.config.method === "base" ? { "payment-signature": Buffer.from(JSON.stringify(p.payment)).toString("base64") } : { authorization: p.payment };
373
+ const paid = await this.post(`/api/machine/${input.kind === "gpu-job" ? "gpu-jobs" : "gpu-blocks"}`, request, headers);
374
+ if (paid.value.purchaseId !== e.digest) throw Error("Purchase mismatch");
375
+ p.result = paid.value;
376
+ await save();
377
+ return { pending: paid.status === 202, purchaseId: e.digest, result: p.result };
378
+ });
379
+ }
380
+ };
381
+
382
+ // apps/cli/src/main.ts
383
+ import { Command } from "commander";
384
+ import { randomUUID as randomUUID3 } from "node:crypto";
385
+
386
+ // apps/cli/src/client.ts
387
+ var ApiError = class extends Error {
388
+ constructor(status, code, message, body) {
389
+ super(message);
390
+ this.status = status;
391
+ this.code = code;
392
+ this.body = body;
393
+ this.name = "ApiError";
394
+ this.requestId = body?.error?.requestId ?? body?.requestId ?? body?.meta?.requestId;
395
+ }
396
+ status;
397
+ code;
398
+ body;
399
+ context = {};
400
+ requestId;
401
+ };
402
+ var UkiyoClient = class {
403
+ baseUrl;
404
+ token;
405
+ fetcher;
406
+ constructor(options = {}) {
407
+ this.baseUrl = (options.baseUrl ?? "https://pay.u-kiyo.ai").replace(/\/+$/, "");
408
+ this.token = options.token;
409
+ this.fetcher = options.fetcher ?? fetch;
410
+ }
411
+ async request(path, init = {}) {
412
+ const { authenticated = true, headers, ...rest } = init;
413
+ if (authenticated && !this.token) {
414
+ throw new ApiError(0, "UNAUTHENTICATED", "No API token. New account: ukiyo signup --json. Existing human-owned account: ukiyo connect. Never give an agent an owner token.", null);
415
+ }
416
+ const signal = rest.signal ?? AbortSignal.timeout(15e3);
417
+ let response;
418
+ try {
419
+ response = await this.fetcher(`${this.baseUrl}/api/v1${path}`, {
420
+ ...rest,
421
+ signal,
422
+ redirect: "error",
423
+ headers: {
424
+ // Only declare a JSON body when there is one. A bodyless POST that
425
+ // still claims application/json is rejected before it reaches a route.
426
+ ...rest.body === void 0 ? {} : { "content-type": "application/json" },
427
+ ...authenticated && this.token ? { authorization: `Bearer ${this.token}` } : {},
428
+ ...headers
429
+ }
430
+ });
431
+ } catch {
432
+ throw new ApiError(0, "NETWORK_ERROR", "API request failed or timed out. For a write, retry only with the same Idempotency-Key and original parameters.", null);
433
+ }
434
+ const body = await response.json().catch(() => null);
435
+ if (signal.aborted) throw new ApiError(0, "NETWORK_ERROR", "API response timed out. Retry writes only with the same Idempotency-Key and original parameters.", null);
436
+ if (!response.ok) {
437
+ const code = body?.error?.code ?? body?.code ?? `HTTP_${response.status}`;
438
+ const message = body?.error?.message ?? body?.message ?? describe(response.status);
439
+ throw new ApiError(response.status, String(code), String(message), body);
440
+ }
441
+ if (body === null) throw new ApiError(response.status, "INVALID_RESPONSE", "API did not return a JSON response.", null);
442
+ return body?.data ?? body;
443
+ }
444
+ offers() {
445
+ return this.request("/offers", { authenticated: false });
446
+ }
447
+ me(signal) {
448
+ return this.request("/me", { signal: signal ?? null });
449
+ }
450
+ balance() {
451
+ return this.request("/billing/balance");
452
+ }
453
+ topup(amountMinor, idempotencyKey) {
454
+ return this.request("/billing/topups", { method: "POST", headers: { "idempotency-key": idempotencyKey }, body: JSON.stringify({ amountMinor }) });
455
+ }
456
+ async topupStatus(id) {
457
+ if (!this.token) throw new ApiError(401, "UNAUTHENTICATED", "An account token is required", null);
458
+ try {
459
+ return await fundingRequest(this.baseUrl, this.token, `/api/v1/billing/machine-topups/${encodeURIComponent(id)}`, {}, this.fetcher);
460
+ } catch (error) {
461
+ if (error instanceof FundingError) throw new ApiError(error.status, error.code, error.message, error.body);
462
+ throw error;
463
+ }
464
+ }
465
+ async topupRails(amountMinor, key, method, plain = false) {
466
+ if (!this.token) throw new ApiError(401, "UNAUTHENTICATED", "An account token is required", null);
467
+ if (!plain && !process.env.UKIYO_MACHINE_CONFIG) throw new ApiError(0, "WALLET_CONFIG_REQUIRED", "Set UKIYO_MACHINE_CONFIG or use --plain for an unpaid challenge", null);
468
+ try {
469
+ const wallet = plain ? void 0 : await loadMachineClient(process.env.UKIYO_MACHINE_CONFIG);
470
+ if (wallet && wallet.config.origin !== this.baseUrl) throw new ApiError(0, "INVALID_URL", "Wallet and API origins must match", null);
471
+ const topup = await createAccountTopup(this.baseUrl, this.token, amountMinor, key, this.fetcher);
472
+ const rail = method ?? wallet?.config.method ?? topup.paymentMethods?.[0]?.method;
473
+ if (!rail) throw new ApiError(0, "PAYMENT_METHOD_DISABLED", "No funding rails are enabled", null);
474
+ return plain ? topupChallenge(this.baseUrl, this.token, topup, rail, this.fetcher) : wallet.fundTopup(topup, this.token, rail);
475
+ } catch (error) {
476
+ if (error instanceof FundingError) throw new ApiError(error.status, error.code, error.message, error.body);
477
+ throw error;
478
+ }
479
+ }
480
+ rent(offerId, budgetMinor, idempotencyKey, signal) {
481
+ return this.request("/rentals", { signal: signal ?? null, method: "POST", headers: { "idempotency-key": idempotencyKey }, body: JSON.stringify({ offerId, ...budgetMinor === void 0 ? {} : { budgetMinor } }) });
482
+ }
483
+ rentGpu(input, idempotencyKey, signal) {
484
+ return this.request("/rentals", { signal: signal ?? null, method: "POST", headers: { "idempotency-key": idempotencyKey }, body: JSON.stringify(input) });
485
+ }
486
+ rental(id, signal) {
487
+ return this.request(`/rentals/${encodeURIComponent(id)}`, signal ? { signal } : {});
488
+ }
489
+ deployments() {
490
+ return this.request("/deployments");
491
+ }
492
+ deployment(id, signal) {
493
+ return this.request(`/deployments/${encodeURIComponent(id)}`, signal ? { signal } : {});
494
+ }
495
+ credentials(id) {
496
+ return this.request(`/deployments/${encodeURIComponent(id)}/credentials/reveal`, { method: "POST" });
497
+ }
498
+ terminate(id, signal) {
499
+ return this.request(`/deployments/${encodeURIComponent(id)}/terminate`, { signal: signal ?? null, method: "POST", body: JSON.stringify({ reason: "USER_REQUESTED" }) });
500
+ }
501
+ checkout(offerId, budgetMinor, idempotencyKey) {
502
+ return this.request("/orders/checkout", { method: "POST", headers: { "idempotency-key": idempotencyKey }, body: JSON.stringify({ offerId, budgetMinor }) });
503
+ }
504
+ };
505
+ function describe(status) {
506
+ if (status === 401) return "Invalid or expired API token. Check the saved credential and API origin. Existing-account agents can reconnect with owner approval; do not create a new account to recover a funded balance.";
507
+ if (status === 404) return "Not found.";
508
+ if (status === 429) return "Rate limited. Try again shortly.";
509
+ return `Request failed with status ${status}.`;
510
+ }
511
+ function dollarsToMinor(value) {
512
+ if (!/^\d+(\.\d{1,2})?$/.test(value.trim())) throw new ApiError(0, "INVALID_BUDGET", `Budget must be a dollar amount such as 20 or 19.99, received "${value}".`, null);
513
+ const [whole, fraction = ""] = value.trim().split(".");
514
+ const minor = Number(whole) * 100 + Number(fraction.padEnd(2, "0"));
515
+ if (!Number.isSafeInteger(minor)) throw new ApiError(0, "INVALID_BUDGET", "Amount exceeds safe integer precision.", null);
516
+ return minor;
517
+ }
518
+
519
+ // apps/cli/src/credentials.ts
520
+ import { constants as constants2, closeSync as closeSync2, fsyncSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
521
+ import { join as join3 } from "node:path";
522
+ import { randomUUID as randomUUID2 } from "node:crypto";
523
+
524
+ // apps/cli/src/storage.ts
525
+ import { execFileSync } from "node:child_process";
526
+ import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync } from "node:fs";
527
+ import { homedir } from "node:os";
528
+ import { join as join2 } from "node:path";
529
+ var isWindows = () => process.platform === "win32";
530
+ var credentialDirectory = () => join2(homedir(), ".ukiyo");
531
+ var WINDOWS_ACL = `
532
+ $ErrorActionPreference = 'Stop'
533
+ $env:PSModulePath = Join-Path $PSHOME 'Modules'
534
+ $p = $env:UKIYO_STORAGE_PATH
535
+ $item = Get-Item -LiteralPath $p -Force
536
+ if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Reparse point' }
537
+ $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User
538
+ $acl = Get-Acl -LiteralPath $p
539
+ if ($env:UKIYO_STORAGE_ACTION -eq 'protect') {
540
+ if ($item.PSIsContainer) {
541
+ $acl = New-Object Security.AccessControl.DirectorySecurity
542
+ $inherit = [Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
543
+ } else {
544
+ $acl = New-Object Security.AccessControl.FileSecurity
545
+ $inherit = [Security.AccessControl.InheritanceFlags]::None
546
+ }
547
+ $acl.SetOwner($sid)
548
+ $acl.SetAccessRuleProtection($true, $false)
549
+ $rule = New-Object Security.AccessControl.FileSystemAccessRule($sid, [Security.AccessControl.FileSystemRights]::FullControl, $inherit, [Security.AccessControl.PropagationFlags]::None, [Security.AccessControl.AccessControlType]::Allow)
550
+ $acl.AddAccessRule($rule)
551
+ Set-Acl -LiteralPath $p -AclObject $acl
552
+ $acl = Get-Acl -LiteralPath $p
553
+ }
554
+ if ($acl.GetOwner([Security.Principal.SecurityIdentifier]).Value -ne $sid.Value -or -not $acl.AreAccessRulesProtected) { throw 'Owner or inheritance' }
555
+ $own = $false
556
+ foreach ($r in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
557
+ if ($r.AccessControlType -eq [Security.AccessControl.AccessControlType]::Allow) {
558
+ if ($r.IdentityReference.Value -ne $sid.Value) { throw 'Other principal has access' }
559
+ if (($r.FileSystemRights -band [Security.AccessControl.FileSystemRights]::FullControl) -eq [Security.AccessControl.FileSystemRights]::FullControl) { $own = $true }
560
+ }
561
+ }
562
+ if (-not $own) { throw 'Missing owner access' }
563
+ exit 0
564
+ `;
565
+ function windowsAcl(path, action) {
566
+ try {
567
+ const env = { UKIYO_STORAGE_PATH: path, UKIYO_STORAGE_ACTION: action };
568
+ for (const name of ["SystemRoot", "WINDIR", "USERPROFILE", "LOCALAPPDATA", "APPDATA", "PATH", "TEMP", "TMP", "PSModulePath", "PSModuleAnalysisCachePath", "ProgramFiles", "ProgramFiles(x86)", "ProgramW6432", "ProgramData", "ALLUSERSPROFILE", "USERDOMAIN", "USERNAME", "COMPUTERNAME", "ComSpec", "OS", "PROCESSOR_ARCHITECTURE", "PROCESSOR_IDENTIFIER", "NUMBER_OF_PROCESSORS"]) if (process.env[name]) env[name] = process.env[name];
569
+ env.PSModulePath = join2(process.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "Modules");
570
+ execFileSync(
571
+ join2(process.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
572
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(WINDOWS_ACL, "utf16le").toString("base64")],
573
+ { env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4, windowsHide: true }
574
+ );
575
+ } catch (error) {
576
+ throw new Error(`Ukiyo storage requires user-only Windows access controls (ACLs), an owned regular path, and Windows PowerShell (${error.code === "ETIMEDOUT" ? "access-control check timed out" : "access-control check failed"}). No credential was exposed.`);
577
+ }
578
+ }
579
+ function validateStorageStat(stat, dir) {
580
+ if ((dir ? !stat.isDirectory() : !stat.isFile()) || stat.isSymbolicLink()) throw new Error("Ukiyo credential storage must be a regular owned path, not a symlink.");
581
+ if (!isWindows() && ((stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid()))
582
+ throw new Error("Ukiyo credential storage must be owned by you, private (0700 directory / 0600 file), and not a symlink.");
583
+ }
584
+ function validateStoragePath(path, dir) {
585
+ validateStorageStat(lstatSync(path), dir);
586
+ if (isWindows()) windowsAcl(path, "validate");
587
+ }
588
+ function ensureCredentialDirectory() {
589
+ const path = credentialDirectory();
590
+ let created = false;
591
+ try {
592
+ mkdirSync(path, isWindows() ? {} : { mode: 448 });
593
+ created = true;
594
+ } catch (error) {
595
+ if (error.code !== "EEXIST") throw error;
596
+ }
597
+ validateStorageStat(lstatSync(path), true);
598
+ if (isWindows()) windowsAcl(path, created ? "protect" : "validate");
599
+ return path;
600
+ }
601
+ function openPrivateFile(path, flags, create = false) {
602
+ const noFollow = isWindows() ? 0 : constants.O_NOFOLLOW;
603
+ let fd;
604
+ if (create) fd = openSync(path, flags | constants.O_CREAT | constants.O_EXCL | noFollow, isWindows() ? void 0 : 384);
605
+ else {
606
+ validateStoragePath(path, false);
607
+ fd = openSync(path, flags | noFollow);
608
+ }
609
+ try {
610
+ validateStorageStat(fstatSync(fd), false);
611
+ if (isWindows()) windowsAcl(path, create ? "protect" : "validate");
612
+ return fd;
613
+ } catch (error) {
614
+ closeSync(fd);
615
+ throw error;
616
+ }
617
+ }
618
+
619
+ // apps/cli/src/credentials.ts
620
+ var DEFAULT_API_URL = "https://pay.u-kiyo.ai";
621
+ function apiOrigin(value = DEFAULT_API_URL) {
622
+ const url = new URL(value);
623
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash || !(url.protocol === "https:" || url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))) {
624
+ throw new Error("Use an HTTPS API origin (HTTP is allowed only on loopback for local testing).");
625
+ }
626
+ return url.origin;
627
+ }
628
+ var directory = credentialDirectory;
629
+ var filename = () => join3(directory(), "credentials.json");
630
+ function savedCredential(baseUrl) {
631
+ try {
632
+ validateStoragePath(directory(), true);
633
+ } catch (error) {
634
+ if (error.code === "ENOENT") return void 0;
635
+ throw error;
636
+ }
637
+ let fd;
638
+ try {
639
+ fd = openPrivateFile(filename(), constants2.O_RDONLY);
640
+ } catch (error) {
641
+ if (error.code === "ENOENT") return void 0;
642
+ throw error;
643
+ }
644
+ try {
645
+ const data = JSON.parse(readFileSync(fd, "utf8"));
646
+ if (data.apiUrl !== apiOrigin(baseUrl)) throw new Error("Saved credential belongs to a different API origin.");
647
+ if (typeof data.token !== "string" || !/^uk_[a-f0-9]{12}\.[A-Za-z0-9_-]{32}$/.test(data.token)) throw new Error("Invalid saved Ukiyo credential.");
648
+ return data.token;
649
+ } finally {
650
+ closeSync2(fd);
651
+ }
652
+ }
653
+ async function connectAgent(setupCode, baseUrl = DEFAULT_API_URL, fetcher = fetch) {
654
+ const apiUrl = apiOrigin(baseUrl);
655
+ if (!/^uks_[A-Za-z0-9_-]{43}$/.test(setupCode)) throw new Error("Invalid setup code. Ask the owner for a fresh Connect an agent code.");
656
+ if (process.env.UKIYO_API_TOKEN) throw new Error("Unset UKIYO_API_TOKEN before connecting. Only the scoped agent credential should be used.");
657
+ ensureCredentialDirectory();
658
+ const fd = openPrivateFile(filename(), constants2.O_WRONLY, true);
659
+ let stored = false;
660
+ try {
661
+ const response = await fetcher(`${apiUrl}/api/v1/agent-setups/exchange`, {
662
+ method: "POST",
663
+ redirect: "error",
664
+ signal: AbortSignal.timeout(15e3),
665
+ headers: { "content-type": "application/json" },
666
+ body: JSON.stringify({ setupCode })
667
+ });
668
+ const body = await response.json();
669
+ if (!response.ok) throw new ApiError(response.status, body?.error?.code ?? "CONNECT_FAILED", body?.error?.message ?? "Connection failed.", body);
670
+ if (body?.data?.key?.kind !== "AGENT" || !/^uk_[a-f0-9]{12}\.[A-Za-z0-9_-]{32}$/.test(body?.data?.token ?? "") || JSON.stringify([...body?.data?.key?.scopes ?? []].sort()) !== JSON.stringify(["manage", "read", "rent"])) throw new Error("Server did not return the expected scoped agent credential.");
671
+ writeFileSync(fd, JSON.stringify({ apiUrl, token: body.data.token, keyId: body.data.key.id }) + "\n", { flush: true });
672
+ fsyncSync(fd);
673
+ stored = true;
674
+ return { key: body.data.key, path: filename() };
675
+ } catch (error) {
676
+ if (error instanceof ApiError) throw error;
677
+ throw new Error(`${error instanceof Error ? error.message : "Connection failed."} If the code was consumed, ask the owner to revoke the agent token and create a new setup code.`);
678
+ } finally {
679
+ closeSync2(fd);
680
+ if (!stored) unlinkSync(filename());
681
+ }
682
+ }
683
+ async function connectExistingAccount(printUrl, baseUrl = DEFAULT_API_URL, fetcher = fetch) {
684
+ const apiUrl = apiOrigin(baseUrl);
685
+ if (process.env.UKIYO_API_TOKEN || savedCredential(apiUrl)) throw new Error("Already authenticated. Use a fresh HOME for an additional agent; do not replace an existing identity.");
686
+ const response = await fetcher(`${apiUrl}/api/v1/agent-authorizations`, { method: "POST", redirect: "error", signal: AbortSignal.timeout(15e3) });
687
+ const body = await response.json();
688
+ if (!response.ok) throw new ApiError(response.status, body?.error?.code ?? "CONNECT_FAILED", body?.error?.message ?? "Could not start authorization.", body);
689
+ const session = body.data;
690
+ const approval = new URL(session.approvalUrl);
691
+ if (approval.origin !== apiUrl || !/^\/connect\/[a-f0-9]{48}$/.test(approval.pathname)) throw new Error("Unexpected approval URL.");
692
+ printUrl(approval.href);
693
+ const deadline = Math.min(Date.now() + 6e5, Date.parse(session.expiresAt));
694
+ while (Date.now() < deadline) {
695
+ try {
696
+ const result = await connectAgent(session.setupCode, apiUrl, fetcher);
697
+ const token = savedCredential(apiUrl);
698
+ const me = await fetcher(`${apiUrl}/api/v1/me`, { headers: { authorization: `Bearer ${token}` }, redirect: "error", signal: AbortSignal.timeout(15e3) });
699
+ if (!me.ok || (await me.json())?.data?.key?.id !== result.key.id) throw new Error("Credential saved; verification failed. Retry ukiyo me.");
700
+ return result;
701
+ } catch (error) {
702
+ if (!(error instanceof Error) || !error.message.startsWith("Waiting for account-owner approval.")) throw error;
703
+ await new Promise((resolve3) => setTimeout(resolve3, 2e3));
704
+ }
705
+ }
706
+ throw new Error("Authorization expired. Run ukiyo connect again.");
707
+ }
708
+ async function signupAgent(limits, baseUrl = DEFAULT_API_URL, fetcher = fetch, acceptedTermsVersion) {
709
+ const apiUrl = apiOrigin(baseUrl);
710
+ if (process.env.UKIYO_API_TOKEN) throw new Error("Unset UKIYO_API_TOKEN before signup; signup creates a new account, never attaches to an existing one.");
711
+ ensureCredentialDirectory();
712
+ const fd = openPrivateFile(filename(), constants2.O_WRONLY, true);
713
+ let stored = false;
714
+ try {
715
+ let agreedTerms;
716
+ if (acceptedTermsVersion !== void 0) {
717
+ const response2 = await fetcher(`${apiUrl}/api/v1/terms`, { redirect: "error", signal: AbortSignal.timeout(15e3) });
718
+ const body2 = await response2.json();
719
+ if (!response2.ok) throw new ApiError(response2.status, body2?.error?.code ?? "TERMS_UNAVAILABLE", body2?.error?.message ?? "Approved Terms are unavailable.", body2);
720
+ const document = body2?.data;
721
+ if (!document || typeof document.version !== "string" || !/^https:\/\//.test(document.url ?? "") || !/^[a-f0-9]{64}$/.test(document.sha256 ?? "")) throw new ApiError(0, "TERMS_UNAVAILABLE", "Approved Terms are not configured. Signup was not attempted.", null);
722
+ if (document.version !== acceptedTermsVersion) throw new ApiError(0, "TERMS_VERSION_MISMATCH", "Review GET /api/v1/terms and explicitly supply its current version with --accept-terms. Signup was not attempted.", null);
723
+ agreedTerms = { version: document.version, sha256: document.sha256 };
724
+ }
725
+ const response = await fetcher(`${apiUrl}/api/v1/signup`, {
726
+ method: "POST",
727
+ redirect: "error",
728
+ signal: AbortSignal.timeout(15e3),
729
+ headers: { "content-type": "application/json", "idempotency-key": randomUUID2() },
730
+ body: JSON.stringify({ ...limits, ...agreedTerms ? { termsAcceptance: { ...agreedTerms, accepted: true } } : {} })
731
+ });
732
+ const body = await response.json();
733
+ if (!response.ok) throw new ApiError(response.status, body?.error?.code ?? "SIGNUP_FAILED", body?.error?.message ?? "Signup failed.", body);
734
+ const data = body.data;
735
+ if (data?.key?.kind !== "AGENT" || !/^uk_[a-f0-9]{12}\.[A-Za-z0-9_-]{32}$/.test(data?.token ?? "") || JSON.stringify([...data?.key?.scopes ?? []].sort()) !== JSON.stringify(["billing", "manage", "read", "rent"])) throw new Error("Unexpected signup credential.");
736
+ writeFileSync(fd, JSON.stringify({ apiUrl, token: data.token, keyId: data.key.id }) + "\n");
737
+ fsyncSync(fd);
738
+ stored = true;
739
+ const me = await fetcher(`${apiUrl}/api/v1/me`, { redirect: "error", signal: AbortSignal.timeout(15e3), headers: { authorization: `Bearer ${data.token}` } });
740
+ if (!me.ok) throw new Error("Credential saved, but verification failed. Retry ukiyo me --json; do not sign up again.");
741
+ const identity = (await me.json()).data;
742
+ if (identity?.user?.id !== data.account.id || identity?.key?.id !== data.key.id || identity?.key?.kind !== "AGENT") throw new Error("Saved credential identity did not match signup.");
743
+ return { account: data.account, key: data.key, credentialFile: filename(), ...agreedTerms ? { termsAcceptance: agreedTerms } : {} };
744
+ } finally {
745
+ closeSync2(fd);
746
+ if (!stored) unlinkSync(filename());
747
+ }
748
+ }
749
+
750
+ // apps/cli/src/ssh.ts
751
+ import { execFile } from "node:child_process";
752
+ import { constants as constants3, closeSync as closeSync3, mkdtempSync, unlinkSync as unlinkSync2, rmdirSync, writeFileSync as writeFileSync2 } from "node:fs";
753
+ import { resolve as resolve2, join as join4 } from "node:path";
754
+
755
+ // apps/cli/src/output.ts
756
+ function redact(text, secrets = []) {
757
+ let result = text;
758
+ for (const secret of secrets) if (secret) result = result.split(secret).join("[REDACTED]");
759
+ return result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g, "[REDACTED PRIVATE KEY]").replace(/uk_[a-f0-9]{12}\.[A-Za-z0-9_-]{32,}/g, "[REDACTED TOKEN]");
760
+ }
761
+ function safeOutput(value) {
762
+ if (typeof value === "string") return redact(value, [process.env.UKIYO_API_TOKEN ?? ""]);
763
+ if (Array.isArray(value)) return value.map(safeOutput);
764
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, /^(?:privateKey|private_key|token|secret|password|authorization)$/i.test(key) ? "[REDACTED]" : safeOutput(item)]));
765
+ return value;
766
+ }
767
+
768
+ // apps/cli/src/ssh.ts
769
+ async function executeSsh(id, access, command, timeout = 60, runner = execFile) {
770
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9.:-]*$/.test(access.host) || !/^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/.test(access.username) || !Number.isInteger(access.port) || access.port < 1 || access.port > 65535 || typeof access.privateKey !== "string" || !access.privateKey.includes("PRIVATE KEY") || !/^[a-zA-Z0-9-]+$/.test(id) || !command.length || command.some((arg) => arg.includes("\0")) || !Number.isSafeInteger(timeout) || timeout < 1 || timeout > 3600) throw new Error("Invalid SSH connection, command, or timeout.");
771
+ const directory2 = credentialDirectory();
772
+ validateStoragePath(directory2, true);
773
+ const knownHosts = join4(directory2, "known_hosts");
774
+ let fd;
775
+ try {
776
+ fd = openPrivateFile(knownHosts, constants3.O_RDWR, true);
777
+ } catch (error) {
778
+ if (error.code !== "EEXIST") throw error;
779
+ fd = openPrivateFile(knownHosts, constants3.O_RDWR);
780
+ }
781
+ closeSync3(fd);
782
+ const temp = mkdtempSync(join4(directory2, "ssh-"));
783
+ const keyFile = join4(temp, "identity");
784
+ try {
785
+ if (isWindows()) windowsAcl(temp, "protect");
786
+ validateStoragePath(temp, true);
787
+ const keyFd = openPrivateFile(keyFile, constants3.O_WRONLY, true);
788
+ try {
789
+ writeFileSync2(keyFd, access.privateKey);
790
+ } finally {
791
+ closeSync3(keyFd);
792
+ }
793
+ const remote = command.map((arg) => `'${arg.replaceAll("'", "'\\''")}'`).join(" ");
794
+ const args = [
795
+ "-F",
796
+ isWindows() ? "NUL" : "/dev/null",
797
+ "-T",
798
+ "-i",
799
+ keyFile,
800
+ "-p",
801
+ String(access.port),
802
+ "-o",
803
+ "BatchMode=yes",
804
+ "-o",
805
+ "IdentitiesOnly=yes",
806
+ "-o",
807
+ "ConnectTimeout=15",
808
+ "-o",
809
+ "StrictHostKeyChecking=accept-new",
810
+ "-o",
811
+ `HostKeyAlias=ukiyo-${id}`,
812
+ "-o",
813
+ `UserKnownHostsFile="${(isWindows() ? knownHosts.replaceAll("\\", "/") : knownHosts.replaceAll("\\", "\\\\")).replaceAll('"', '\\"')}"`,
814
+ `${access.username}@${access.host}`,
815
+ remote
816
+ ];
817
+ const env = { PATH: process.env.PATH };
818
+ if (isWindows()) {
819
+ for (const name of ["SystemRoot", "WINDIR", "USERPROFILE", "LOCALAPPDATA", "TEMP", "TMP"]) if (process.env[name]) env[name] = process.env[name];
820
+ }
821
+ return await new Promise((resolve3, reject) => runner("ssh", args, { timeout: timeout * 1e3, maxBuffer: 4 * 1024 * 1024, env, encoding: "utf8", windowsHide: true }, (error, stdout, stderr) => {
822
+ if (error && typeof error.code !== "number") {
823
+ reject(new Error(error.killed ? "SSH command timed out." : "SSH transport failed. Check SSH availability and the deployment state."));
824
+ return;
825
+ }
826
+ resolve3({ exitCode: error?.code ?? 0, stdout: redact(String(stdout), [access.privateKey, process.env.UKIYO_API_TOKEN ?? ""]), stderr: redact(String(stderr), [access.privateKey, process.env.UKIYO_API_TOKEN ?? ""]) });
827
+ }));
828
+ } finally {
829
+ try {
830
+ try {
831
+ unlinkSync2(keyFile);
832
+ } catch (error) {
833
+ if (error.code !== "ENOENT") throw error;
834
+ }
835
+ } finally {
836
+ rmdirSync(temp);
837
+ }
838
+ }
839
+ }
840
+ function saveSshKey(path, access) {
841
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9.:-]*$/.test(access.host) || !/^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/.test(access.username) || !Number.isInteger(access.port) || access.port < 1 || access.port > 65535 || !access.privateKey?.includes("PRIVATE KEY")) throw new Error("Invalid SSH connection.");
842
+ const keyPath = resolve2(path);
843
+ const fd = openPrivateFile(keyPath, constants3.O_WRONLY, true);
844
+ try {
845
+ writeFileSync2(fd, access.privateKey);
846
+ } catch (error) {
847
+ unlinkSync2(keyPath);
848
+ throw error;
849
+ } finally {
850
+ closeSync3(fd);
851
+ }
852
+ const quoted = isWindows() ? "'" + keyPath.replaceAll("'", "''") + "'" : "'" + keyPath.replaceAll("'", "'\\''") + "'";
853
+ return {
854
+ deploymentId: access.deploymentId,
855
+ rentalId: access.rentalId,
856
+ host: access.host,
857
+ port: access.port,
858
+ username: access.username,
859
+ keyPath,
860
+ sshCommand: `ssh -i ${quoted} -p ${access.port} -o IdentitiesOnly=yes ${access.username}@${access.host}`
861
+ };
862
+ }
863
+
864
+ // apps/cli/src/wait.ts
865
+ function timeoutSeconds(value) {
866
+ const seconds = Number(value);
867
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 3600) throw new ApiError(0, "INVALID_USAGE", "Timeout must be 1\u20133600 seconds.", null);
868
+ return seconds;
869
+ }
870
+ async function beforeDeadline(read, deadline, context) {
871
+ const expired = () => {
872
+ const error = new ApiError(0, "WAIT_TIMEOUT", "Wait ended; inspect the known resource ID. For a write, retry only with the same Idempotency-Key and original parameters; never create a replacement rental.", null);
873
+ error.context = { ...context };
874
+ return error;
875
+ };
876
+ const remaining = deadline - Date.now();
877
+ if (remaining <= 0) throw expired();
878
+ const controller = new AbortController();
879
+ let timer;
880
+ try {
881
+ const value = await Promise.race([read(controller.signal), new Promise((_, reject) => {
882
+ timer = setTimeout(() => {
883
+ controller.abort();
884
+ reject(expired());
885
+ }, remaining);
886
+ })]);
887
+ if (Date.now() >= deadline) throw expired();
888
+ return value;
889
+ } catch (error) {
890
+ if (Date.now() >= deadline) throw expired();
891
+ throw error;
892
+ } finally {
893
+ if (timer) clearTimeout(timer);
894
+ }
895
+ }
896
+ async function waitFor(read, done, seconds, context, deadline = Date.now() + seconds * 1e3) {
897
+ let latest;
898
+ for (; ; ) {
899
+ try {
900
+ latest = await beforeDeadline(read, deadline, { ...context, ...latest ? { resource: latest } : {} });
901
+ } catch (error) {
902
+ if (error instanceof ApiError) error.context = { ...context, ...error.context };
903
+ throw error;
904
+ }
905
+ if (latest?.rentalId) context.rentalId = latest.rentalId;
906
+ if (latest?.deploymentId) context.deploymentId = latest.deploymentId;
907
+ if (done(latest)) return latest;
908
+ await new Promise((resolve3) => setTimeout(resolve3, Math.max(0, Math.min(1e3, deadline - Date.now()))));
909
+ }
910
+ }
911
+
912
+ // apps/cli/src/main.ts
913
+ function filteredOffers(value, gpu, count) {
914
+ const normalize = (name) => name.toUpperCase().replace(/NVIDIA|AMD/g, "").replace(/[^A-Z0-9]/g, "");
915
+ if (count !== void 0 && (!Number.isSafeInteger(Number(count)) || Number(count) < 1)) throw new ApiError(0, "INVALID_USAGE", "GPU count must be a positive integer.", null);
916
+ if (!Array.isArray(value)) throw new ApiError(0, "INVALID_RESPONSE", "Expected an offer list from the API.", null);
917
+ return value.filter((offer) => (!gpu || normalize(offer.name) === normalize(gpu)) && (count === void 0 || offer.gpuCount === Number(count)));
918
+ }
919
+ function contextError(error, context) {
920
+ const failure = error instanceof ApiError ? error : new ApiError(0, "CLI_ERROR", error.message, null);
921
+ failure.context = { ...context, ...failure.context };
922
+ throw failure;
923
+ }
924
+ function buildProgram(options = {}) {
925
+ const out = options.out ?? console.log;
926
+ const client = () => {
927
+ if (options.client) return options.client;
928
+ const baseUrl = apiOrigin(process.env.UKIYO_API_URL);
929
+ const token = process.env.UKIYO_API_TOKEN ?? savedCredential(baseUrl);
930
+ return new UkiyoClient({ baseUrl, ...token ? { token } : {} });
931
+ };
932
+ const print = (value) => out(JSON.stringify(safeOutput(value), null, 2));
933
+ const program = new Command().name("ukiyo").description("Manage Ukiyo compute through the public API").version("0.3.1").configureOutput({ writeErr: () => {
934
+ } }).exitOverride();
935
+ const machine = program.command("machine").description("Wallet-paid compute with no prior Ukiyo account; requires local signer config and explicit spend caps");
936
+ const machineClient = () => {
937
+ if (!process.env.UKIYO_MACHINE_CONFIG) throw new Error("Set UKIYO_MACHINE_CONFIG to a private local wallet/cap configuration file");
938
+ return loadMachineClient(process.env.UKIYO_MACHINE_CONFIG);
939
+ };
940
+ machine.command("buy").requiredOption("--request <path>", "JSON compute request including current Terms and Privacy acceptance").requiredOption("--purchase-id <id>", "stable local id; reuse for retries").option("--service-credit", "spend this chain-qualified wallet\u2019s existing service credit").action(async (opts) => print(await (await machineClient()).purchase(opts.purchaseId, JSON.parse(await readFile3(opts.request, "utf8")), !!opts.serviceCredit)));
941
+ for (const action of ["status", "terminate"]) machine.command(action).requiredOption("--purchase-id <id>").action(async (opts) => print(await (await machineClient()).action(opts.purchaseId, action)));
942
+ program.command("auth").description("Explain API token authentication").action(() => out([
943
+ "New agent: ukiyo signup --json (no account, email, browser or Telegram required).",
944
+ "Existing human-owned account: ukiyo connect, then ask its owner to approve the displayed URL.",
945
+ "Fund once: ukiyo balance --json; ukiyo topup link --amount 5 --json.",
946
+ "Ukiyo authenticates with a scoped API token.",
947
+ "",
948
+ "No owner credential is needed by the CLI/agent. Do not copy an owner token into its environment.",
949
+ "",
950
+ "Optional: export UKIYO_API_URL to target a different environment.",
951
+ "signup/connect store only the scoped agent token in ~/.ukiyo/credentials.json (0600). Never give an agent an owner token."
952
+ ].join("\n")));
953
+ program.command("connect [setup-code]").option("--json").description("Attach to an existing owner account using a short-lived approval session").action(async (setupCode) => {
954
+ const result = setupCode ? await connectAgent(setupCode, process.env.UKIYO_API_URL) : await connectExistingAccount(console.error, process.env.UKIYO_API_URL);
955
+ print({ connected: true, key: result.key, credentialFile: result.path });
956
+ });
957
+ program.command("signup").description("Create a new headless account and scoped agent credential; no free credits").option("--json").option("--max-per-rental <usd>", "per-rental spending cap", "5").option("--budget <usd>", "total net spending cap", "500").option("--max-concurrent <count>", "concurrent rental cap", "1").option("--accept-terms <version>", "explicitly agree to the reviewed current version from GET /api/v1/terms; never inferred").addHelpText("after", "\nReview https://u-kiyo.ai/terms/ and https://u-kiyo.ai/privacy/. An authorized agent may agree once at signup; no per-rental approval is required.").action(async (opts) => print(await signupAgent({ maxPerRentalMinor: dollarsToMinor(opts.maxPerRental), budgetLimitMinor: dollarsToMinor(opts.budget), maxConcurrentDeployments: Number(opts.maxConcurrent) }, process.env.UKIYO_API_URL, void 0, opts.acceptTerms)));
958
+ program.command("me").option("--json").action(async () => print(await client().me()));
959
+ program.command("balance").option("--json").action(async () => {
960
+ const account = await client().me();
961
+ print({ ...account.balance, key: account.key });
962
+ });
963
+ const topup = program.command("topup").description("Fund your own compute-credit balance");
964
+ topup.command("link").requiredOption("--amount <usd>").option("--json").option("--idempotency-key <key>", "reuse this key after a timeout").action(async (opts) => {
965
+ const key = opts.idempotencyKey ?? randomUUID3();
966
+ try {
967
+ print({ ...await client().topup(dollarsToMinor(opts.amount), key), idempotencyKey: key });
968
+ } catch (error) {
969
+ contextError(error, { idempotencyKey: key });
970
+ }
971
+ });
972
+ topup.command("rails").requiredOption("--amount <usd>").option("--method <rail>", "base, solana or tempo").option("--idempotency-key <key>", "stable top-up intent; reuse the returned key and amount on every retry").option("--plain", "print the unpaid payment challenge and stop; do not sign").option("--json").description("Authorize current Terms and fund with configured wallet; read https://u-kiyo.ai/terms/ first").action(async (opts) => {
973
+ if (opts.method && !["base", "solana", "tempo"].includes(opts.method)) throw new ApiError(0, "INVALID_USAGE", "Choose base, solana or tempo", null);
974
+ const key = opts.idempotencyKey ?? randomUUID3();
975
+ try {
976
+ print({ ...await client().topupRails(dollarsToMinor(opts.amount), key, opts.method, !!opts.plain), idempotencyKey: key });
977
+ } catch (error) {
978
+ contextError(error, { idempotencyKey: key });
979
+ }
980
+ });
981
+ topup.command("status").requiredOption("--topup-id <id>").option("--json").description("Inspect funding; pending means do not pay again").action(async (opts) => print(await client().topupStatus(opts.topupId)));
982
+ program.command("rent").option("--offer <id>").option("--gpu <name>").option("--count <count>").option("--budget <usd>", "required for fixed-prepaid accounts; optional for account-balance billing").option("--max-hourly-price <usd>", "GPU/count accepted hourly ceiling; reuse the same ceiling on retries").option("--idempotency-key <key>", "unique per intended rental; reuse with original parameters on retries").option("--json").option("--wait").option("--timeout <seconds>", "maximum status wait", "600").action(async (opts) => {
983
+ const seconds = timeoutSeconds(opts.timeout);
984
+ const deadline = Date.now() + seconds * 1e3;
985
+ const budget = opts.budget === void 0 ? void 0 : dollarsToMinor(opts.budget);
986
+ if (Boolean(opts.offer) === Boolean(opts.gpu) || opts.offer && (opts.count || opts.maxHourlyPrice)) throw new ApiError(0, "INVALID_USAGE", "Choose --offer OR --gpu with optional --count and hourly ceiling.", null);
987
+ const api = client();
988
+ if (budget === void 0) {
989
+ const account = await (opts.wait ? beforeDeadline((signal) => api.me(signal), deadline, {}) : api.me());
990
+ if (account.billing?.model !== "ACCOUNT_BALANCE" || account.billing.budgetRequired !== false) {
991
+ throw new ApiError(0, "INVALID_USAGE", "--budget is required for this account. Budget-free rentals require account-balance billing.", null);
992
+ }
993
+ }
994
+ const budgetFields = budget === void 0 ? {} : { budgetMinor: budget };
995
+ let intent;
996
+ if (opts.gpu) {
997
+ const count = opts.count ?? "1";
998
+ if (![1, 2, 4, 8].includes(Number(count))) throw new ApiError(0, "INVALID_USAGE", "Supported GPU counts are 1, 2, 4 or 8.", null);
999
+ intent = {
1000
+ gpuModel: opts.gpu,
1001
+ gpuCount: Number(count),
1002
+ ...budgetFields,
1003
+ ...opts.maxHourlyPrice === void 0 ? {} : { maxHourlyPriceMinor: dollarsToMinor(opts.maxHourlyPrice) }
1004
+ };
1005
+ }
1006
+ const idempotencyKey = opts.idempotencyKey ?? randomUUID3();
1007
+ const context = { ...intent ?? { offerId: opts.offer, ...budgetFields }, idempotencyKey };
1008
+ try {
1009
+ const create = (signal) => intent ? api.rentGpu(intent, idempotencyKey, signal) : api.rent(opts.offer, budget, idempotencyKey, signal);
1010
+ const rental = await (opts.wait ? beforeDeadline(create, deadline, context) : create());
1011
+ Object.assign(context, { rentalId: rental.rentalId, deploymentId: rental.deploymentId });
1012
+ const result = opts.wait && !["ACTIVE", "FAILED", "TERMINATED"].includes(rental.status) ? await waitFor((signal) => api.rental(rental.rentalId, signal), (value) => ["ACTIVE", "FAILED", "TERMINATED"].includes(value.status), seconds, context, deadline) : rental;
1013
+ print({ ...result, ...context });
1014
+ if (opts.wait && result.status !== "ACTIVE") process.exitCode = 1;
1015
+ } catch (error) {
1016
+ contextError(error, context);
1017
+ }
1018
+ });
1019
+ program.command("rental <id>").option("--json").option("--wait", "wait for ACTIVE or a final failure").option("--timeout <seconds>", "maximum wait", "600").action(async (id, opts) => {
1020
+ const seconds = timeoutSeconds(opts.timeout);
1021
+ const api = client();
1022
+ const value = opts.wait ? await waitFor((signal) => api.rental(id, signal), (value2) => ["ACTIVE", "FAILED", "TERMINATED"].includes(value2.status), seconds, { rentalId: id }) : await api.rental(id);
1023
+ print(value);
1024
+ if (opts.wait && value.status !== "ACTIVE") process.exitCode = 1;
1025
+ });
1026
+ program.command("exec <id> <command...>").option("--json").option("--timeout <seconds>", "command timeout", "60").description("Execute a command over authenticated SSH (first host key is pinned locally)").action(async (id, command, opts) => {
1027
+ const seconds = timeoutSeconds(opts.timeout);
1028
+ const access = await client().credentials(id);
1029
+ const result = await executeSsh(access.deploymentId ?? id, access, command, seconds);
1030
+ print({ deploymentId: access.deploymentId ?? id, rentalId: access.rentalId, ...result });
1031
+ if (result.exitCode) process.exitCode = 1;
1032
+ });
1033
+ program.command("offers").option("--json").option("--gpu <name>").option("--count <count>").description("List available compute").action(async (opts) => print(filteredOffers(await client().offers(), opts.gpu, opts.count)));
1034
+ program.command("list").option("--json").description("List your deployments").action(async () => print(await client().deployments()));
1035
+ program.command("status <id>").option("--json").description("Show one deployment").action(async (id) => print(await client().deployment(id)));
1036
+ program.command("credentials <id>").option("--json").option("--save-key <path>", "save a private SSH key securely without printing it").option("--reveal", "explicitly print the private key; audited and unsafe for shared transcripts").description("Audited SSH connection metadata; private keys are redacted by default. Prefer exec or --save-key.").action(async (id, opts) => {
1037
+ if (opts.saveKey && opts.reveal) throw new ApiError(0, "INVALID_USAGE", "Choose --save-key or --reveal, not both.", null);
1038
+ const access = await client().credentials(id);
1039
+ if (opts.saveKey) print(saveSshKey(opts.saveKey, access));
1040
+ else if (opts.reveal) {
1041
+ console.error("WARNING: credential reveal is audited. This output contains a private key; do not share or log it.");
1042
+ out(JSON.stringify(access, null, 2));
1043
+ } else print(access);
1044
+ });
1045
+ program.command("deploy").description("Rent an offer").requiredOption("--offer <id>").requiredOption("--budget <usd>", "budget in dollars, e.g. 20 or 19.99").action(async (opts) => print(await client().checkout(opts.offer, dollarsToMinor(opts.budget), randomUUID3())));
1046
+ program.command("terminate <id>").option("--json").option("--wait").option("--timeout <seconds>", "maximum termination wait", "600").description("Terminate a deployment. This is irreversible.").option("--yes", "skip the confirmation prompt").action(async (id, opts) => {
1047
+ const seconds = timeoutSeconds(opts.timeout);
1048
+ if (!opts.yes) throw new ApiError(0, "CONFIRMATION_REQUIRED", `Terminating ${id} is irreversible and destroys anything on the instance. Re-run with --yes to confirm.`, null);
1049
+ const api = client();
1050
+ try {
1051
+ const deadline = Date.now() + seconds * 1e3;
1052
+ const accepted = await (opts.wait ? beforeDeadline((signal) => api.terminate(id, signal), deadline, { deploymentId: id }) : api.terminate(id));
1053
+ const value = opts.wait ? await waitFor((signal) => api.deployment(id, signal), (value2) => value2.status === "TERMINATED", seconds, { deploymentId: id, ...accepted }, deadline) : accepted;
1054
+ print({ deploymentId: id, ...value });
1055
+ } catch (error) {
1056
+ contextError(error, { deploymentId: id });
1057
+ }
1058
+ });
1059
+ return program;
1060
+ }
1061
+ async function run(argv = process.argv) {
1062
+ try {
1063
+ await buildProgram().parseAsync(argv);
1064
+ } catch (error) {
1065
+ const commander = error?.code?.startsWith?.("commander.");
1066
+ if (commander && error.exitCode === 0) return;
1067
+ const api = error instanceof ApiError ? error : void 0;
1068
+ process.exitCode = api?.code === "WAIT_TIMEOUT" ? 3 : commander || ["INVALID_USAGE", "INVALID_BUDGET", "CONFIRMATION_REQUIRED"].includes(api?.code ?? "") ? 2 : 1;
1069
+ const result = safeOutput({ error: { code: api?.code ?? (commander ? "INVALID_USAGE" : "CLI_ERROR"), status: api?.status ?? 0, message: error.message, requestId: api?.requestId ?? null }, ...api?.context });
1070
+ if (argv.slice(0, argv.indexOf("--") < 0 ? void 0 : argv.indexOf("--")).includes("--json")) console.log(JSON.stringify(result));
1071
+ else console.error(JSON.stringify(result));
1072
+ }
1073
+ }
1074
+ export {
1075
+ buildProgram,
1076
+ run
1077
+ };