bitsnap 0.0.0-stage → 0.1.0

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.
Files changed (5) hide show
  1. package/AGENTS.md +110 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -2
  4. package/dist/cli.js +1685 -0
  5. package/package.json +35 -4
package/dist/cli.js ADDED
@@ -0,0 +1,1685 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { randomUUID } from "crypto";
5
+
6
+ // src/args.ts
7
+ import { parseArgs } from "util";
8
+
9
+ // src/errors.ts
10
+ var EXIT = { ok: 0, error: 1, auth: 2, network: 3 };
11
+ var CliError = class extends Error {
12
+ details;
13
+ constructor(details) {
14
+ super(details.message);
15
+ this.name = "CliError";
16
+ this.details = details;
17
+ }
18
+ get exitCode() {
19
+ switch (this.details.kind) {
20
+ case "auth":
21
+ return EXIT.auth;
22
+ case "network":
23
+ return EXIT.network;
24
+ default:
25
+ return EXIT.error;
26
+ }
27
+ }
28
+ /** The body printed to stderr with --json. */
29
+ toJSON() {
30
+ const { kind, message, detail, hint, reason, code: code2, status, requestId, metadata } = this.details;
31
+ const error = { kind, message };
32
+ if (detail !== void 0) error.detail = detail;
33
+ if (hint !== void 0) error.hint = hint;
34
+ if (reason !== void 0) error.reason = reason;
35
+ if (code2 !== void 0) error.code = code2;
36
+ if (status !== void 0) error.status = status;
37
+ if (requestId !== void 0) error.requestId = requestId;
38
+ if (metadata !== void 0 && Object.keys(metadata).length > 0) {
39
+ error.metadata = metadata;
40
+ }
41
+ return { error };
42
+ }
43
+ };
44
+ function usageError(message, hint) {
45
+ return new CliError({ kind: "usage", message, hint });
46
+ }
47
+ var LOGIN_HINT = "Run `bitsnap login --key <key>` or set BITSNAP_API_KEY. Keys live in Settings \u2192 Developer.";
48
+ var REASON_INFO = {
49
+ missing_credentials: {
50
+ message: "No API key was sent.",
51
+ hint: LOGIN_HINT,
52
+ kind: "auth"
53
+ },
54
+ invalid_credentials: {
55
+ message: "The API key is unknown, revoked, expired or malformed.",
56
+ hint: "Create a new key in Settings \u2192 Developer, then run `bitsnap login --key <key>`.",
57
+ kind: "auth"
58
+ },
59
+ missing_scope: {
60
+ message: (m) => m.scope ? `This key lacks the scope \`${m.scope}\`.` : "This key lacks a scope this command needs.",
61
+ hint: "Add the scope to the key in Settings \u2192 Developer, or use another key."
62
+ },
63
+ writes_disabled: {
64
+ message: "Writes are turned off for this key.",
65
+ hint: "Turn on writes for this key in Settings \u2192 Developer."
66
+ },
67
+ pii_scope_required: {
68
+ message: "Searching customer data needs the `customers:pii` scope.",
69
+ hint: "Add `customers:pii` to the key in Settings \u2192 Developer, or drop --search."
70
+ },
71
+ test_mode_only: {
72
+ message: "This command works only with a test key (bs_test_\u2026).",
73
+ hint: "Log in with a test key: `bitsnap login --key bs_test_\u2026`."
74
+ },
75
+ browser_origin_not_allowed: {
76
+ message: "The API refuses requests that carry an Origin header.",
77
+ hint: "Call the API from a server or the CLI, never from a browser."
78
+ },
79
+ invalid_request: {
80
+ message: "The API rejected the request.",
81
+ hint: "Check the value of the field named in the message."
82
+ },
83
+ invalid_cursor: {
84
+ message: "The cursor is not one this API returned.",
85
+ hint: "Pass the `nextCursor` of a previous page unchanged."
86
+ },
87
+ invalid_idempotency_key: {
88
+ message: "The idempotency key is invalid.",
89
+ hint: "Use 1-255 printable ASCII characters, or omit --idempotency-key."
90
+ },
91
+ discount_not_allowed: {
92
+ message: "That discount is not allowed for this key.",
93
+ hint: "Use a smaller percentage discount."
94
+ },
95
+ product_not_found: {
96
+ message: "No such product in this project.",
97
+ hint: "List ids with `bitsnap products list --drafts`."
98
+ },
99
+ order_not_found: {
100
+ message: "No such order in this project and environment.",
101
+ hint: "List ids with `bitsnap orders list`. Test and live orders are separate."
102
+ },
103
+ coupon_not_found: {
104
+ message: "No such coupon in this project.",
105
+ hint: "List coupons with `bitsnap coupons list`."
106
+ },
107
+ coupon_code_not_found: {
108
+ message: "No such coupon code in this project.",
109
+ hint: "List coupons with `bitsnap coupons list`."
110
+ },
111
+ webhook_not_found: {
112
+ message: "No such webhook endpoint in this environment.",
113
+ hint: "List endpoints with `bitsnap webhooks list`."
114
+ },
115
+ coupon_code_taken: {
116
+ message: "That coupon code is already used in the project.",
117
+ hint: "Pick another code."
118
+ },
119
+ revision_conflict: {
120
+ message: "The product changed since the revision you sent.",
121
+ hint: "Fetch it again and retry."
122
+ },
123
+ idempotency_key_reused: {
124
+ message: "That idempotency key was already used with a different request.",
125
+ hint: "Use a new --idempotency-key, or send the same request again."
126
+ },
127
+ idempotency_key_in_progress: {
128
+ message: "A request with that idempotency key is still running.",
129
+ hint: "Retry in a few seconds with the same --idempotency-key."
130
+ },
131
+ product_not_purchasable: {
132
+ message: "A product in the cart cannot be bought.",
133
+ hint: "Publish the product, check its stock and price, then retry."
134
+ },
135
+ checkout_unavailable: {
136
+ message: "The shop has no way to take this payment.",
137
+ hint: "Set up a payment gateway (the test gateway for test keys) in the dashboard."
138
+ },
139
+ order_not_settleable: {
140
+ message: "The test order is not waiting for a payment.",
141
+ hint: "Only an open test order of the Bitsnap test gateway can be settled. Create a new one with `bitsnap checkout create` and a test key."
142
+ },
143
+ product_moderation_hidden: {
144
+ message: "The product was hidden by moderation and cannot be published.",
145
+ hint: "Contact Bitsnap support."
146
+ },
147
+ rate_limited: {
148
+ message: "Too many requests.",
149
+ hint: "Slow down and retry in a few seconds."
150
+ },
151
+ daily_write_limit: {
152
+ message: "The key spent its daily write budget.",
153
+ hint: "The budget resets at 00:00 UTC."
154
+ },
155
+ method_not_found: {
156
+ message: "The API has no such method.",
157
+ hint: "Update the CLI: `npm i -g bitsnap@latest`."
158
+ },
159
+ service_unavailable: {
160
+ message: "A Bitsnap dependency is down.",
161
+ hint: "Retry in a minute."
162
+ },
163
+ internal: {
164
+ message: "Bitsnap failed on its side.",
165
+ hint: "Retry; if it persists report the request id."
166
+ }
167
+ };
168
+ function stripReasonPrefix(message) {
169
+ return message.replace(/^\[[a-z_]+\]\s*/, "");
170
+ }
171
+ function reasonOfMessage(message) {
172
+ return /^\[([a-z_]+)\]/.exec(message)?.[1];
173
+ }
174
+ function reasonFromDetails(details) {
175
+ if (!Array.isArray(details)) return {};
176
+ for (const entry of details) {
177
+ const debug = entry?.debug;
178
+ if (debug && typeof debug.reason === "string") {
179
+ const metadata = debug.metadata && typeof debug.metadata === "object" ? debug.metadata : void 0;
180
+ return { reason: debug.reason, metadata };
181
+ }
182
+ }
183
+ return {};
184
+ }
185
+ function apiErrorFromResponse(status, body, requestId) {
186
+ const parsed = body && typeof body === "object" ? body : {};
187
+ const rawMessage = typeof parsed.message === "string" ? parsed.message : void 0;
188
+ const code2 = typeof parsed.code === "string" ? parsed.code : void 0;
189
+ const fromDetails = reasonFromDetails(parsed.details);
190
+ const reason = fromDetails.reason ?? (rawMessage ? reasonOfMessage(rawMessage) : void 0);
191
+ const metadata = fromDetails.metadata ?? {};
192
+ const detail = rawMessage ? stripReasonPrefix(rawMessage) : void 0;
193
+ const info = reason ? REASON_INFO[reason] : void 0;
194
+ let kind = info?.kind ?? "api";
195
+ if (status === 401 || code2 === "unauthenticated") kind = "auth";
196
+ if (info) {
197
+ const message = typeof info.message === "function" ? info.message(metadata) : info.message;
198
+ return new CliError({
199
+ kind,
200
+ message,
201
+ detail: detail && detail !== message ? detail : void 0,
202
+ hint: info.hint,
203
+ reason,
204
+ code: code2,
205
+ status,
206
+ requestId,
207
+ metadata
208
+ });
209
+ }
210
+ const fallback = detail ?? (status === 401 ? "The API key was refused." : `The API answered with HTTP ${status}.`);
211
+ return new CliError({
212
+ kind,
213
+ message: fallback,
214
+ hint: kind === "auth" ? LOGIN_HINT : void 0,
215
+ reason,
216
+ code: code2,
217
+ status,
218
+ requestId,
219
+ metadata
220
+ });
221
+ }
222
+ function networkError(host, cause) {
223
+ const reason = cause instanceof Error ? cause.message : String(cause);
224
+ const nested = cause instanceof Error && cause.cause instanceof Error ? `: ${cause.cause.message}` : "";
225
+ return new CliError({
226
+ kind: "network",
227
+ message: `Could not reach ${host} (${reason}${nested}).`,
228
+ hint: "Check your connection and the --host / BITSNAP_HOST value, then retry."
229
+ });
230
+ }
231
+
232
+ // src/args.ts
233
+ var GLOBAL_OPTIONS = {
234
+ json: {
235
+ type: "boolean",
236
+ description: "Machine-readable output: JSON on stdout, errors as JSON on stderr."
237
+ },
238
+ host: {
239
+ type: "string",
240
+ value: "<url>",
241
+ description: "API host. Default: BITSNAP_HOST, the stored host, or https://bitsnap.pl."
242
+ },
243
+ "idempotency-key": {
244
+ type: "string",
245
+ value: "<key>",
246
+ description: "Idempotency-Key for write commands. Default: a random UUID."
247
+ },
248
+ help: { type: "boolean", short: "h", description: "Show help for the command." }
249
+ };
250
+ function parseOptions(args, specs, allowPositionals) {
251
+ const all = { ...GLOBAL_OPTIONS, ...specs };
252
+ const options = {};
253
+ for (const [name, spec] of Object.entries(all)) {
254
+ options[name] = {
255
+ type: spec.type,
256
+ ...spec.short ? { short: spec.short } : {},
257
+ ...spec.multiple ? { multiple: true } : {}
258
+ };
259
+ }
260
+ try {
261
+ const { values, positionals } = parseArgs({
262
+ args,
263
+ options,
264
+ allowPositionals,
265
+ strict: true
266
+ });
267
+ for (const [name, spec] of Object.entries(specs)) {
268
+ if (spec.required && values[name] === void 0 && values.help !== true) {
269
+ throw usageError(`Missing required option --${name}.`, `Run with --help for usage.`);
270
+ }
271
+ }
272
+ return { values, positionals };
273
+ } catch (error) {
274
+ if (error instanceof CliError) throw error;
275
+ const message = error instanceof Error ? error.message : String(error);
276
+ throw usageError(message.split("\n")[0] ?? message, "Run with --help for usage.");
277
+ }
278
+ }
279
+ function str(values, name) {
280
+ const value = values[name];
281
+ return typeof value === "string" ? value : void 0;
282
+ }
283
+ function flag(values, name) {
284
+ return values[name] === true;
285
+ }
286
+ function list(values, name) {
287
+ const value = values[name];
288
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
289
+ return typeof value === "string" ? [value] : [];
290
+ }
291
+ function int(values, name, range = {}) {
292
+ const raw = str(values, name);
293
+ if (raw === void 0) return void 0;
294
+ if (!/^-?\d+$/.test(raw)) throw usageError(`--${name} must be a whole number, got "${raw}".`);
295
+ const value = Number(raw);
296
+ if (range.min !== void 0 && value < range.min) {
297
+ throw usageError(`--${name} must be at least ${range.min}.`);
298
+ }
299
+ if (range.max !== void 0 && value > range.max) {
300
+ throw usageError(`--${name} must be at most ${range.max}.`);
301
+ }
302
+ return value;
303
+ }
304
+
305
+ // src/client.ts
306
+ var REQUEST_TIMEOUT_MS = 3e4;
307
+ var ApiClient = class {
308
+ constructor(rt, auth) {
309
+ this.rt = rt;
310
+ this.auth = auth;
311
+ }
312
+ rt;
313
+ auth;
314
+ requireKey() {
315
+ if (!this.auth.apiKey) {
316
+ throw new CliError({
317
+ kind: "auth",
318
+ message: "No API key configured.",
319
+ hint: "Run `bitsnap login --key <key>` or set BITSNAP_API_KEY. Keys live in Settings \u2192 Developer."
320
+ });
321
+ }
322
+ return this.auth.apiKey;
323
+ }
324
+ headers(extra = {}) {
325
+ return {
326
+ Authorization: `Bearer ${this.requireKey()}`,
327
+ "Content-Type": "application/json",
328
+ Accept: "application/json",
329
+ "X-Bitsnap-Client": `bitsnap-cli@${this.rt.version}`,
330
+ ...extra
331
+ };
332
+ }
333
+ /** `POST /api/v1/bitsnap.v1.<service>/<method>` with Connect JSON. */
334
+ async rpc(service, method, body = {}, options = {}) {
335
+ const extra = {};
336
+ if (options.write) {
337
+ extra["Idempotency-Key"] = options.idempotencyKey ?? this.rt.randomUUID();
338
+ }
339
+ return this.request(`/api/v1/bitsnap.v1.${service}/${method}`, {
340
+ method: "POST",
341
+ headers: this.headers(extra),
342
+ body: JSON.stringify(body)
343
+ });
344
+ }
345
+ /** A plain REST call of the dev endpoints (`/api/v1/webhooks/trigger`). */
346
+ async rest(path, init) {
347
+ const extra = {};
348
+ if (init.write) {
349
+ extra["Idempotency-Key"] = init.idempotencyKey ?? this.rt.randomUUID();
350
+ }
351
+ return this.request(path, {
352
+ method: init.method,
353
+ headers: this.headers(extra),
354
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
355
+ });
356
+ }
357
+ /** An unauthenticated GET that returns the body as text (OpenAPI document). */
358
+ async getText(path, accept) {
359
+ const response = await this.send(path, {
360
+ method: "GET",
361
+ headers: { Accept: accept, "X-Bitsnap-Client": `bitsnap-cli@${this.rt.version}` }
362
+ });
363
+ const text = await response.text();
364
+ if (!response.ok) {
365
+ throw apiErrorFromResponse(response.status, safeJson(text), response.headers.get("x-request-id") ?? void 0);
366
+ }
367
+ return text;
368
+ }
369
+ url(path) {
370
+ return this.auth.host + path;
371
+ }
372
+ /** fetch with the timeout and the network error mapping. */
373
+ async send(path, init) {
374
+ try {
375
+ return await this.rt.fetch(this.url(path), {
376
+ ...init,
377
+ signal: init.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS)
378
+ });
379
+ } catch (error) {
380
+ throw networkError(this.auth.host, error);
381
+ }
382
+ }
383
+ async request(path, init) {
384
+ const response = await this.send(path, init);
385
+ const requestId = response.headers.get("x-request-id") ?? void 0;
386
+ let text;
387
+ try {
388
+ text = await response.text();
389
+ } catch (error) {
390
+ throw networkError(this.auth.host, error);
391
+ }
392
+ const body = safeJson(text);
393
+ if (!response.ok) throw apiErrorFromResponse(response.status, body, requestId);
394
+ return {
395
+ body: body ?? {},
396
+ requestId,
397
+ replayed: response.headers.get("idempotent-replayed") === "true"
398
+ };
399
+ }
400
+ };
401
+ function safeJson(text) {
402
+ if (text === "") return void 0;
403
+ try {
404
+ return JSON.parse(text);
405
+ } catch {
406
+ return text;
407
+ }
408
+ }
409
+
410
+ // src/commands.ts
411
+ import { writeFileSync as writeFileSync2 } from "fs";
412
+ import { resolve } from "path";
413
+
414
+ // src/config.ts
415
+ import {
416
+ chmodSync,
417
+ existsSync,
418
+ mkdirSync,
419
+ readFileSync,
420
+ renameSync,
421
+ rmSync,
422
+ writeFileSync
423
+ } from "fs";
424
+ import { homedir } from "os";
425
+ import { dirname, join } from "path";
426
+ var DEFAULT_HOST = "https://bitsnap.pl";
427
+ function configPath(env) {
428
+ const xdg = env.XDG_CONFIG_HOME;
429
+ const base = xdg && xdg !== "" ? xdg : join(env.HOME || homedir(), ".config");
430
+ return join(base, "bitsnap", "config.json");
431
+ }
432
+ function readConfig(env) {
433
+ const path = configPath(env);
434
+ if (!existsSync(path)) return {};
435
+ try {
436
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
437
+ return parsed && typeof parsed === "object" ? parsed : {};
438
+ } catch {
439
+ throw new CliError({
440
+ kind: "usage",
441
+ message: `The config file ${path} is not valid JSON.`,
442
+ hint: "Run `bitsnap logout`, then `bitsnap login --key <key>` again."
443
+ });
444
+ }
445
+ }
446
+ function writeConfig(env, config) {
447
+ const path = configPath(env);
448
+ const dir = dirname(path);
449
+ mkdirSync(dir, { recursive: true, mode: 448 });
450
+ const temp = `${path}.${process.pid}.tmp`;
451
+ writeFileSync(temp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
452
+ chmodSync(temp, 384);
453
+ renameSync(temp, path);
454
+ return path;
455
+ }
456
+ function removeConfig(env) {
457
+ const path = configPath(env);
458
+ if (!existsSync(path)) return false;
459
+ rmSync(path);
460
+ return true;
461
+ }
462
+ function maskKey(key) {
463
+ const prefix = /^(bs_test_|bs_live_|bsat_|api_)/.exec(key)?.[1] ?? "";
464
+ const rest = key.slice(prefix.length);
465
+ if (rest.length <= 8) return `${prefix}\u2026`;
466
+ return `${prefix}\u2026${rest.slice(-4)}`;
467
+ }
468
+ function keyEnvironment(key) {
469
+ if (key.startsWith("bs_test_")) return "test";
470
+ if (key.startsWith("bs_live_") || key.startsWith("api_")) return "live";
471
+ return "unknown";
472
+ }
473
+ function normalizeHost(value) {
474
+ let url;
475
+ try {
476
+ url = new URL(value);
477
+ } catch {
478
+ throw usageError(`"${value}" is not a valid host URL.`, "Use e.g. --host https://bitsnap.pl");
479
+ }
480
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
481
+ throw usageError(`"${value}" must start with http:// or https://.`);
482
+ }
483
+ return url.origin;
484
+ }
485
+ function resolveAuth(rt, hostFlag) {
486
+ const stored = readConfig(rt.env);
487
+ const envKey = rt.env.BITSNAP_API_KEY?.trim();
488
+ const apiKey = envKey ? envKey : stored.apiKey;
489
+ const source = envKey ? "env" : stored.apiKey ? "config" : "none";
490
+ const host = normalizeHost(
491
+ hostFlag || rt.env.BITSNAP_HOST || stored.host || DEFAULT_HOST
492
+ );
493
+ return { apiKey, host, source };
494
+ }
495
+
496
+ // src/sse.ts
497
+ var SseParser = class {
498
+ buffer = "";
499
+ event = "";
500
+ data = [];
501
+ id;
502
+ /** The `id:` of the last event that had one; send it as `Last-Event-ID` when reconnecting. */
503
+ lastEventId;
504
+ skipLeadingLF = false;
505
+ push(chunk) {
506
+ if (this.skipLeadingLF && chunk.startsWith("\n")) chunk = chunk.slice(1);
507
+ if (chunk.length > 0) this.skipLeadingLF = false;
508
+ this.buffer += chunk;
509
+ const messages = [];
510
+ let start = 0;
511
+ for (let i = 0; i < this.buffer.length; i++) {
512
+ const char = this.buffer[i];
513
+ if (char !== "\n" && char !== "\r") continue;
514
+ const line = this.buffer.slice(start, i);
515
+ if (char === "\r") {
516
+ if (i + 1 < this.buffer.length) {
517
+ if (this.buffer[i + 1] === "\n") i++;
518
+ } else {
519
+ this.skipLeadingLF = true;
520
+ }
521
+ }
522
+ start = i + 1;
523
+ const message = this.line(line);
524
+ if (message) messages.push(message);
525
+ }
526
+ this.buffer = this.buffer.slice(start);
527
+ return messages;
528
+ }
529
+ line(line) {
530
+ if (line === "") {
531
+ const hasData = this.data.length > 0;
532
+ const message = hasData ? { id: this.id, event: this.event || "message", data: this.data.join("\n") } : void 0;
533
+ if (hasData && this.id !== void 0) this.lastEventId = this.id;
534
+ this.event = "";
535
+ this.data = [];
536
+ this.id = void 0;
537
+ return message;
538
+ }
539
+ if (line.startsWith(":")) return void 0;
540
+ const colon = line.indexOf(":");
541
+ const field = colon === -1 ? line : line.slice(0, colon);
542
+ let value = colon === -1 ? "" : line.slice(colon + 1);
543
+ if (value.startsWith(" ")) value = value.slice(1);
544
+ switch (field) {
545
+ case "event":
546
+ this.event = value;
547
+ break;
548
+ case "data":
549
+ this.data.push(value);
550
+ break;
551
+ case "id":
552
+ if (!value.includes("\0")) this.id = value;
553
+ break;
554
+ default:
555
+ break;
556
+ }
557
+ return void 0;
558
+ }
559
+ };
560
+
561
+ // src/listen.ts
562
+ var EVENTS_PATH = "/api/v1/webhooks/events";
563
+ var FORWARD_TIMEOUT_MS = 15e3;
564
+ var SSE_FAILURES_BEFORE_POLL = 3;
565
+ var FAILURES_BEFORE_GIVING_UP = 8;
566
+ var MAX_BACKOFF_MS = 15e3;
567
+ var SEEN_LIMIT = 1e3;
568
+ async function forwardEvent(rt, forward, event) {
569
+ const target = new URL(forward);
570
+ target.searchParams.set("sig", event.signature);
571
+ const started = Date.now();
572
+ try {
573
+ const response = await rt.fetch(target, {
574
+ method: "POST",
575
+ headers: {
576
+ "Content-Type": "application/json",
577
+ "X-Content-Signature": event.signature
578
+ },
579
+ body: event.payload,
580
+ signal: AbortSignal.timeout(FORWARD_TIMEOUT_MS)
581
+ });
582
+ const durationMs = Date.now() - started;
583
+ const result = {
584
+ url: forward,
585
+ status: response.status,
586
+ ok: response.ok,
587
+ durationMs
588
+ };
589
+ if (!response.ok) {
590
+ const text = (await response.text().catch(() => "")).slice(0, 500);
591
+ if (text !== "") result.body = text;
592
+ } else {
593
+ await response.body?.cancel().catch(() => void 0);
594
+ }
595
+ return result;
596
+ } catch (error) {
597
+ const cause = error instanceof Error ? error.message : String(error);
598
+ const nested = error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : "";
599
+ return { url: forward, error: `${cause}${nested}`, durationMs: Date.now() - started };
600
+ }
601
+ }
602
+ async function listenForWebhooks(rt, client, printer, options) {
603
+ client.requireKey();
604
+ const controller2 = new AbortController();
605
+ const stop = () => controller2.abort();
606
+ if (rt.signal?.aborted) controller2.abort();
607
+ rt.signal?.addEventListener("abort", stop, { once: true });
608
+ let timedOut = false;
609
+ const timer = options.timeoutSeconds === void 0 ? void 0 : setTimeout(() => {
610
+ timedOut = true;
611
+ controller2.abort();
612
+ }, options.timeoutSeconds * 1e3);
613
+ let lastEventId;
614
+ let announced = false;
615
+ let received = 0;
616
+ let mode = options.poll ? "poll" : "sse";
617
+ let sseFailures = 0;
618
+ let failures = 0;
619
+ const seen = /* @__PURE__ */ new Set();
620
+ const finished = () => controller2.signal.aborted || options.maxEvents !== void 0 && received >= options.maxEvents;
621
+ const announce = (ready) => {
622
+ if (announced) return;
623
+ announced = true;
624
+ if (printer.json) {
625
+ printer.ndjson({ type: "ready", ...ready, host: client.auth.host, mode });
626
+ return;
627
+ }
628
+ printer.line(`Listening for test webhook events on ${client.auth.host}` + (ready.projectId ? ` (project ${ready.projectId})` : "") + ".");
629
+ printer.line(`Signing secret: ${ready.secret}`);
630
+ printer.line(
631
+ "For local dev set BITSNAP_WEBHOOK_SECRET to this value in your app's env so it can verify the forwarded events."
632
+ );
633
+ if (options.forward) printer.line(`Forwarding events to ${options.forward}`);
634
+ printer.line("Press Ctrl+C to stop.");
635
+ };
636
+ const handle = async (event) => {
637
+ if (seen.has(event.id)) return;
638
+ seen.add(event.id);
639
+ if (seen.size > SEEN_LIMIT) seen.delete(seen.values().next().value);
640
+ if (event.id) lastEventId = event.id;
641
+ received++;
642
+ const forwarded = options.forward ? await forwardEvent(rt, options.forward, event) : void 0;
643
+ if (printer.json) {
644
+ const record = {
645
+ type: "event",
646
+ id: event.id,
647
+ event: event.event,
648
+ signature: event.signature,
649
+ payload: event.payload
650
+ };
651
+ if (forwarded) record.forward = forwarded;
652
+ printer.ndjson(record);
653
+ return;
654
+ }
655
+ let text = `${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} ${event.event} ${event.id}`;
656
+ if (forwarded) {
657
+ text += "error" in forwarded ? ` -> ${forwarded.url} failed: ${forwarded.error}` : ` -> ${forwarded.url} ${forwarded.status} (${forwarded.durationMs} ms)`;
658
+ }
659
+ printer.line(text);
660
+ if (forwarded && "body" in forwarded && forwarded.body) {
661
+ printer.line(` ${forwarded.body.replace(/\s+/g, " ").slice(0, 200)}`);
662
+ }
663
+ };
664
+ const connectHeaders = (accept) => client.headers({
665
+ Accept: accept,
666
+ ...lastEventId ? { "Last-Event-ID": lastEventId } : {}
667
+ });
668
+ const failResponse = async (response) => {
669
+ const text = await response.text().catch(() => "");
670
+ throw apiErrorFromResponse(
671
+ response.status,
672
+ safeJson(text),
673
+ response.headers.get("x-request-id") ?? void 0
674
+ );
675
+ };
676
+ const runSse = async () => {
677
+ const response = await client.send(EVENTS_PATH, {
678
+ method: "GET",
679
+ headers: connectHeaders("text/event-stream"),
680
+ signal: controller2.signal
681
+ });
682
+ if (!response.ok) await failResponse(response);
683
+ const type = response.headers.get("content-type") ?? "";
684
+ if (!type.includes("text/event-stream") || !response.body) {
685
+ await response.body?.cancel().catch(() => void 0);
686
+ throw new CliError({
687
+ kind: "network",
688
+ message: `The server did not answer with an event stream (content-type "${type}").`
689
+ });
690
+ }
691
+ const parser = new SseParser();
692
+ if (lastEventId) parser.lastEventId = lastEventId;
693
+ const decoder = new TextDecoder();
694
+ const reader = response.body.getReader();
695
+ let gotReady = false;
696
+ try {
697
+ for (; ; ) {
698
+ const { done, value } = await reader.read();
699
+ if (done) break;
700
+ for (const message of parser.push(decoder.decode(value, { stream: true }))) {
701
+ if (message.event === "ready") {
702
+ const ready = safeJson(message.data);
703
+ if (ready && typeof ready === "object" && typeof ready.secret === "string") {
704
+ gotReady = true;
705
+ failures = 0;
706
+ sseFailures = 0;
707
+ announce(ready);
708
+ }
709
+ } else {
710
+ const event = safeJson(message.data);
711
+ if (event && typeof event === "object" && typeof event.payload === "string") {
712
+ await handle({ ...event, id: event.id ?? message.id ?? "" });
713
+ }
714
+ }
715
+ if (finished()) return;
716
+ }
717
+ }
718
+ } finally {
719
+ await reader.cancel().catch(() => void 0);
720
+ }
721
+ if (!gotReady) {
722
+ throw new CliError({ kind: "network", message: "The event stream ended before it was ready." });
723
+ }
724
+ };
725
+ const runPoll = async () => {
726
+ const started = Date.now();
727
+ const response = await client.send(`${EVENTS_PATH}?mode=poll&wait=25`, {
728
+ method: "GET",
729
+ headers: connectHeaders("application/json"),
730
+ signal: controller2.signal
731
+ });
732
+ if (!response.ok) await failResponse(response);
733
+ const body = safeJson(await response.text());
734
+ if (!body || typeof body !== "object" || typeof body.secret !== "string") {
735
+ throw new CliError({ kind: "network", message: "Unexpected answer from the poll endpoint." });
736
+ }
737
+ failures = 0;
738
+ announce({ secret: body.secret });
739
+ for (const event of body.events ?? []) {
740
+ await handle(event);
741
+ if (finished()) return;
742
+ }
743
+ if ((body.events ?? []).length === 0 && Date.now() - started < 500) {
744
+ await rt.sleep(1e3, controller2.signal);
745
+ }
746
+ };
747
+ try {
748
+ while (!finished()) {
749
+ try {
750
+ if (mode === "sse") {
751
+ const connectedAt = Date.now();
752
+ await runSse();
753
+ if (!finished() && Date.now() - connectedAt < 2e3) {
754
+ await rt.sleep(1e3, controller2.signal);
755
+ }
756
+ } else {
757
+ await runPoll();
758
+ }
759
+ } catch (error) {
760
+ if (finished()) break;
761
+ if (!isTransient(error)) throw error;
762
+ failures++;
763
+ if (mode === "sse") sseFailures++;
764
+ if (failures >= FAILURES_BEFORE_GIVING_UP) throw error;
765
+ if (mode === "sse" && sseFailures >= SSE_FAILURES_BEFORE_POLL) {
766
+ mode = "poll";
767
+ if (printer.json) printer.ndjson({ type: "status", state: "polling" });
768
+ else printer.note("Event stream unavailable; switching to long polling.");
769
+ } else {
770
+ if (printer.json) printer.ndjson({ type: "status", state: "reconnecting" });
771
+ else printer.note("Connection lost; reconnecting\u2026");
772
+ }
773
+ await rt.sleep(Math.min(1e3 * 2 ** (failures - 1), MAX_BACKOFF_MS), controller2.signal);
774
+ }
775
+ }
776
+ } finally {
777
+ if (timer) clearTimeout(timer);
778
+ rt.signal?.removeEventListener("abort", stop);
779
+ }
780
+ if (timedOut && options.maxEvents !== void 0 && received < options.maxEvents) {
781
+ throw new CliError({
782
+ kind: "timeout",
783
+ message: `Timed out after ${options.timeoutSeconds} s with ${received} of ${options.maxEvents} events.`,
784
+ hint: "Trigger an event with `bitsnap webhooks trigger` or `bitsnap payments settle <orderId>`."
785
+ });
786
+ }
787
+ }
788
+ function isTransient(error) {
789
+ if (error instanceof CliError) {
790
+ const { kind, status } = error.details;
791
+ if (kind === "network") return true;
792
+ return kind === "api" && status !== void 0 && (status >= 500 || status === 429 || status === 408);
793
+ }
794
+ return !(error instanceof DOMException && error.name === "AbortError");
795
+ }
796
+
797
+ // src/output.ts
798
+ var Printer = class {
799
+ constructor(rt, json) {
800
+ this.rt = rt;
801
+ this.json = json;
802
+ }
803
+ rt;
804
+ json;
805
+ /** One result document: pretty JSON with --json, otherwise `human()` lines. */
806
+ result(value, human) {
807
+ if (this.json) {
808
+ this.rt.stdout.write(JSON.stringify(value, null, 2) + "\n");
809
+ return;
810
+ }
811
+ const text = human();
812
+ const lines = Array.isArray(text) ? text : [text];
813
+ this.rt.stdout.write(lines.join("\n") + "\n");
814
+ }
815
+ /** One compact JSON line (NDJSON) on stdout. */
816
+ ndjson(value) {
817
+ this.rt.stdout.write(JSON.stringify(value) + "\n");
818
+ }
819
+ line(text) {
820
+ this.rt.stdout.write(text + "\n");
821
+ }
822
+ /** Progress notes for humans; never printed with --json. */
823
+ note(text) {
824
+ if (!this.json) this.rt.stderr.write(text + "\n");
825
+ }
826
+ };
827
+ function table(headers, rows) {
828
+ const widths = headers.map(
829
+ (header, column) => Math.max(header.length, ...rows.map((row) => (row[column] ?? "").length))
830
+ );
831
+ const format = (cells) => cells.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
832
+ return [format(headers), ...rows.map(format)];
833
+ }
834
+ function formatMoney(money) {
835
+ if (!money) return "-";
836
+ return `${(money.amount / 100).toFixed(2)} ${money.currency}`;
837
+ }
838
+ function enumLabel(value, prefix) {
839
+ if (!value) return "-";
840
+ return value.replace(prefix, "").toLowerCase();
841
+ }
842
+
843
+ // src/commands.ts
844
+ var CURSOR_OPTION = {
845
+ type: "string",
846
+ value: "<cursor>",
847
+ description: "The nextCursor of the previous page."
848
+ };
849
+ function idempotencyKey(ctx) {
850
+ return str(ctx.values, "idempotency-key");
851
+ }
852
+ function onePositional(ctx, name) {
853
+ const [value, ...rest] = ctx.positionals;
854
+ if (value === void 0 || value === "") {
855
+ throw usageError(`Missing <${name}> argument.`, "Run with --help for usage.");
856
+ }
857
+ if (rest.length > 0) throw usageError(`Unexpected argument "${rest[0]}".`);
858
+ return value;
859
+ }
860
+ function noPositionals(ctx) {
861
+ if (ctx.positionals.length > 0) {
862
+ throw usageError(`Unexpected argument "${ctx.positionals[0]}".`, "Run with --help for usage.");
863
+ }
864
+ }
865
+ function envLabel(environment) {
866
+ return enumLabel(environment, "ENVIRONMENT_");
867
+ }
868
+ var PRODUCT_STATUSES = {
869
+ draft: "PRODUCT_STATUS_DRAFT",
870
+ unpublished: "PRODUCT_STATUS_UNPUBLISHED",
871
+ published: "PRODUCT_STATUS_PUBLISHED"
872
+ };
873
+ var ORDER_STATUSES = {
874
+ created: "ORDER_STATUS_CREATED",
875
+ paid: "ORDER_STATUS_PAID",
876
+ unpaid: "ORDER_STATUS_UNPAID",
877
+ canceled: "ORDER_STATUS_CANCELED",
878
+ sent: "ORDER_STATUS_SENT",
879
+ completed: "ORDER_STATUS_COMPLETED",
880
+ issue: "ORDER_STATUS_ISSUE",
881
+ returned: "ORDER_STATUS_RETURNED",
882
+ in_progress: "ORDER_STATUS_IN_PROGRESS"
883
+ };
884
+ function enumValue(option, raw, prefix, choices) {
885
+ const key = raw.toLowerCase().replace(/-/g, "_").replace(prefix.toLowerCase(), "");
886
+ const found = choices[key];
887
+ if (!found) {
888
+ throw usageError(
889
+ `--${option} must be one of: ${Object.keys(choices).join(", ")}. Got "${raw}".`
890
+ );
891
+ }
892
+ return found;
893
+ }
894
+ function parseTime(option, raw, end = false) {
895
+ if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
896
+ const day = /* @__PURE__ */ new Date(`${raw}T00:00:00Z`);
897
+ if (Number.isNaN(day.getTime())) throw usageError(`--${option} "${raw}" is not a valid date.`);
898
+ if (end) day.setUTCDate(day.getUTCDate() + 1);
899
+ return day.toISOString().replace(".000Z", "Z");
900
+ }
901
+ const parsed = new Date(raw);
902
+ if (Number.isNaN(parsed.getTime()) || !/^\d{4}-\d{2}-\d{2}T/.test(raw)) {
903
+ throw usageError(
904
+ `--${option} "${raw}" is not a date.`,
905
+ "Use YYYY-MM-DD or an RFC 3339 timestamp like 2026-10-30T12:00:00Z."
906
+ );
907
+ }
908
+ return parsed.toISOString().replace(".000Z", "Z");
909
+ }
910
+ function replayedNote(ctx, replayed) {
911
+ if (replayed) ctx.printer.note("(replayed: this idempotency key was already used)");
912
+ }
913
+ var COMMANDS = [
914
+ {
915
+ path: ["login"],
916
+ summary: "Store an API key and verify it with the API.",
917
+ options: {
918
+ key: {
919
+ type: "string",
920
+ value: "<key>",
921
+ description: "The API key (bs_test_\u2026 or bs_live_\u2026). Default: BITSNAP_API_KEY."
922
+ }
923
+ },
924
+ output: "GetAccount response + { key (masked), host, configPath }",
925
+ examples: [
926
+ "bitsnap login --key bs_test_01ABC\u2026",
927
+ "BITSNAP_API_KEY=bs_test_01ABC\u2026 bitsnap login --json"
928
+ ],
929
+ async run(ctx) {
930
+ noPositionals(ctx);
931
+ const key = (str(ctx.values, "key") ?? ctx.rt.env.BITSNAP_API_KEY ?? "").trim();
932
+ if (key === "") {
933
+ throw usageError(
934
+ "No API key given.",
935
+ "Pass --key <key> or set BITSNAP_API_KEY. Create a key in Settings \u2192 Developer. The CLI never prompts."
936
+ );
937
+ }
938
+ if (/\s/.test(key)) throw usageError("The API key must not contain whitespace.");
939
+ const auth = resolveAuth(ctx.rt, str(ctx.values, "host"));
940
+ const client = new ApiClient(ctx.rt, { ...auth, apiKey: key, source: "none" });
941
+ const { body } = await client.rpc("AccountService", "GetAccount");
942
+ const path = writeConfig(ctx.rt.env, {
943
+ apiKey: key,
944
+ host: auth.host,
945
+ projectId: body.projectId,
946
+ projectName: body.projectName,
947
+ environment: body.environment
948
+ });
949
+ const result = { ...body, key: maskKey(key), host: auth.host, configPath: path };
950
+ ctx.printer.result(result, () => [
951
+ `Logged in to ${body.projectName} (${body.projectId}) in ${envLabel(body.environment)} mode.`,
952
+ `Key: ${maskKey(key)} Host: ${auth.host}`,
953
+ `Saved to ${path}`,
954
+ ...ctx.rt.env.BITSNAP_API_KEY && ctx.rt.env.BITSNAP_API_KEY.trim() !== key ? ["Note: BITSNAP_API_KEY is set and wins over the stored key."] : []
955
+ ]);
956
+ }
957
+ },
958
+ {
959
+ path: ["logout"],
960
+ summary: "Delete the stored API key.",
961
+ options: {},
962
+ output: "{ ok, removed, configPath }",
963
+ examples: ["bitsnap logout"],
964
+ async run(ctx) {
965
+ noPositionals(ctx);
966
+ const removed = removeConfig(ctx.rt.env);
967
+ const path = configPath(ctx.rt.env);
968
+ ctx.printer.result(
969
+ { ok: true, removed, configPath: path },
970
+ () => removed ? [
971
+ `Removed ${path}.`,
972
+ ...ctx.rt.env.BITSNAP_API_KEY ? ["BITSNAP_API_KEY is still set in this environment."] : []
973
+ ] : "No stored credentials."
974
+ );
975
+ }
976
+ },
977
+ {
978
+ path: ["whoami"],
979
+ summary: "Show the project, environment and scopes of the current key.",
980
+ options: {},
981
+ output: "GetAccount response + { key (masked), keySource: env|config, host }",
982
+ examples: ["bitsnap whoami --json"],
983
+ async run(ctx) {
984
+ noPositionals(ctx);
985
+ const client = ctx.client();
986
+ const key = client.requireKey();
987
+ const { body } = await client.rpc("AccountService", "GetAccount");
988
+ const result = { ...body, key: maskKey(key), keySource: client.auth.source, host: client.auth.host };
989
+ ctx.printer.result(result, () => [
990
+ `Project: ${body.projectName} (${body.projectId})`,
991
+ `Environment: ${envLabel(body.environment)}`,
992
+ `Credential: ${enumLabel(body.credentialType, "CREDENTIAL_TYPE_")} ${maskKey(key)} (from ${client.auth.source})`,
993
+ `Writes: ${body.canWrite ? "enabled" : "disabled"}`,
994
+ `Scopes: ${body.scopes.join(", ") || "-"}`,
995
+ `Host: ${client.auth.host}`
996
+ ]);
997
+ }
998
+ },
999
+ {
1000
+ path: ["products", "list"],
1001
+ summary: "List products, newest first.",
1002
+ options: {
1003
+ search: { type: "string", value: "<text>", description: "Only products whose name contains this." },
1004
+ limit: { type: "string", value: "<1-100>", description: "Page size. Default 50." },
1005
+ drafts: { type: "boolean", description: "Include draft and unpublished products." },
1006
+ cursor: CURSOR_OPTION
1007
+ },
1008
+ output: "{ products: Product[], nextCursor }",
1009
+ examples: ["bitsnap products list --drafts --limit 20 --json"],
1010
+ async run(ctx) {
1011
+ noPositionals(ctx);
1012
+ const request = {};
1013
+ const search = str(ctx.values, "search");
1014
+ if (search) request.search = search;
1015
+ const limit = int(ctx.values, "limit", { min: 1, max: 100 });
1016
+ if (limit !== void 0) request.limit = limit;
1017
+ const cursor = str(ctx.values, "cursor");
1018
+ if (cursor) request.cursor = cursor;
1019
+ if (flag(ctx.values, "drafts")) request.includeDrafts = true;
1020
+ const { body } = await ctx.client().rpc("ProductsService", "ListProducts", request);
1021
+ ctx.printer.result(body, () => {
1022
+ if (body.products.length === 0) return "No products.";
1023
+ const rows = body.products.map((p) => [
1024
+ p.id,
1025
+ p.name,
1026
+ enumLabel(p.status, "PRODUCT_STATUS_"),
1027
+ formatMoney(p.price),
1028
+ p.availableQuantity === void 0 ? "-" : String(p.availableQuantity)
1029
+ ]);
1030
+ return [
1031
+ ...table(["ID", "NAME", "STATUS", "PRICE", "STOCK"], rows),
1032
+ ...body.nextCursor ? [`More: --cursor ${body.nextCursor}`] : []
1033
+ ];
1034
+ });
1035
+ }
1036
+ },
1037
+ {
1038
+ path: ["products", "get"],
1039
+ summary: "Show one product with its variants.",
1040
+ args: "<id>",
1041
+ options: {},
1042
+ output: "{ product: Product }",
1043
+ examples: ["bitsnap products get prodv2_abc --json"],
1044
+ async run(ctx) {
1045
+ const id = onePositional(ctx, "id");
1046
+ const { body } = await ctx.client().rpc("ProductsService", "GetProduct", { id });
1047
+ const p = body.product;
1048
+ ctx.printer.result(body, () => [
1049
+ `${p.name} (${p.id})`,
1050
+ `Status: ${enumLabel(p.status, "PRODUCT_STATUS_")}`,
1051
+ `Price: ${formatMoney(p.price)}`,
1052
+ `Stock: ${p.availableQuantity === void 0 ? "not tracked" : p.availableQuantity}`,
1053
+ `Revision: ${p.revision}`,
1054
+ `Images: ${p.images.length === 0 ? "-" : p.images.join(", ")}`,
1055
+ ...p.variants.length > 0 ? [
1056
+ "Variants:",
1057
+ ...table(
1058
+ [" ID", "NAME", "PRICE", "STOCK"],
1059
+ p.variants.map((v) => [
1060
+ ` ${v.id}`,
1061
+ v.name,
1062
+ formatMoney(v.price),
1063
+ v.availableQuantity === void 0 ? "-" : String(v.availableQuantity)
1064
+ ])
1065
+ )
1066
+ ] : []
1067
+ ]);
1068
+ }
1069
+ },
1070
+ {
1071
+ path: ["products", "create"],
1072
+ summary: "Create a product (a draft unless --status says otherwise).",
1073
+ options: {
1074
+ name: { type: "string", value: "<name>", description: "Product name.", required: true },
1075
+ price: {
1076
+ type: "string",
1077
+ value: "<minor units>",
1078
+ description: "Price in minor units: 4900 = 49.00 PLN.",
1079
+ required: true
1080
+ },
1081
+ currency: { type: "string", value: "<ISO 4217>", description: "Currency. Default PLN." },
1082
+ status: { type: "string", value: "<draft|published|unpublished>", description: "Initial status. Default draft." },
1083
+ stock: { type: "string", value: "<n>", description: "Units that can be sold. Default: not tracked." },
1084
+ image: { type: "string", multiple: true, value: "<url>", description: "Image URL; repeat for more." }
1085
+ },
1086
+ output: "{ product: Product }",
1087
+ examples: [
1088
+ "bitsnap products create --name 'Ebook' --price 4900 --status published --json",
1089
+ "bitsnap products create --name Shirt --price 8900 --image https://example.com/a.png --image https://example.com/b.png"
1090
+ ],
1091
+ async run(ctx) {
1092
+ noPositionals(ctx);
1093
+ const price = int(ctx.values, "price", { min: 0 });
1094
+ const request = {
1095
+ name: str(ctx.values, "name"),
1096
+ price: { amount: price, currency: (str(ctx.values, "currency") ?? "PLN").toUpperCase() }
1097
+ };
1098
+ const status = str(ctx.values, "status");
1099
+ if (status) request.status = enumValue("status", status, "PRODUCT_STATUS_", PRODUCT_STATUSES);
1100
+ const stock = int(ctx.values, "stock", { min: 0 });
1101
+ if (stock !== void 0) request.availableQuantity = stock;
1102
+ const images = list(ctx.values, "image");
1103
+ if (images.length > 0) request.images = images;
1104
+ const { body, replayed } = await ctx.client().rpc("ProductsService", "CreateProduct", request, {
1105
+ write: true,
1106
+ idempotencyKey: idempotencyKey(ctx)
1107
+ });
1108
+ replayedNote(ctx, replayed);
1109
+ ctx.printer.result(body, () => [
1110
+ `Created ${body.product.name} (${body.product.id}) as ${enumLabel(body.product.status, "PRODUCT_STATUS_")}.`
1111
+ ]);
1112
+ }
1113
+ },
1114
+ {
1115
+ path: ["orders", "list"],
1116
+ summary: "List orders of the key's environment, newest first.",
1117
+ options: {
1118
+ status: {
1119
+ type: "string",
1120
+ multiple: true,
1121
+ value: `<${Object.keys(ORDER_STATUSES).join("|")}>`,
1122
+ description: "Only orders in this status; repeat for several."
1123
+ },
1124
+ limit: { type: "string", value: "<n>", description: "Page size." },
1125
+ cursor: CURSOR_OPTION
1126
+ },
1127
+ output: "{ orders: Order[], nextCursor, totalCount }",
1128
+ examples: ["bitsnap orders list --status paid --limit 10 --json"],
1129
+ async run(ctx) {
1130
+ noPositionals(ctx);
1131
+ const request = {};
1132
+ const statuses = list(ctx.values, "status").map(
1133
+ (s) => enumValue("status", s, "ORDER_STATUS_", ORDER_STATUSES)
1134
+ );
1135
+ if (statuses.length > 0) request.statuses = statuses;
1136
+ const limit = int(ctx.values, "limit", { min: 1, max: 100 });
1137
+ if (limit !== void 0) request.limit = limit;
1138
+ const cursor = str(ctx.values, "cursor");
1139
+ if (cursor) request.cursor = cursor;
1140
+ const { body } = await ctx.client().rpc("OrdersService", "ListOrders", request);
1141
+ ctx.printer.result(body, () => {
1142
+ if (body.orders.length === 0) return "No orders.";
1143
+ const rows = body.orders.map((o) => [
1144
+ o.id,
1145
+ enumLabel(o.status, "ORDER_STATUS_"),
1146
+ envLabel(o.environment),
1147
+ formatMoney(o.total),
1148
+ o.customer?.email ?? "-",
1149
+ o.createdAt ?? "-"
1150
+ ]);
1151
+ return [
1152
+ ...table(["ID", "STATUS", "ENV", "TOTAL", "CUSTOMER", "CREATED"], rows),
1153
+ `${body.totalCount} total${body.nextCursor ? `; more: --cursor ${body.nextCursor}` : ""}`
1154
+ ];
1155
+ });
1156
+ }
1157
+ },
1158
+ {
1159
+ path: ["orders", "get"],
1160
+ summary: "Show one order.",
1161
+ args: "<id>",
1162
+ options: {},
1163
+ output: "{ order: Order }",
1164
+ examples: ["bitsnap orders get ord_123 --json"],
1165
+ async run(ctx) {
1166
+ const id = onePositional(ctx, "id");
1167
+ const { body } = await ctx.client().rpc("OrdersService", "GetOrder", { id });
1168
+ const o = body.order;
1169
+ ctx.printer.result(body, () => [
1170
+ `Order ${o.id}: ${enumLabel(o.status, "ORDER_STATUS_")} (${envLabel(o.environment)})`,
1171
+ `Total: ${formatMoney(o.total)}`,
1172
+ `Customer: ${o.customer?.email || "-"}`,
1173
+ `Gateway: ${o.paymentGateway || "-"}`,
1174
+ `Created: ${o.createdAt ?? "-"}`,
1175
+ ...o.items.map((i) => ` ${i.quantity} \xD7 ${i.name} (${i.productId}) ${formatMoney(i.unitPrice)}`)
1176
+ ]);
1177
+ }
1178
+ },
1179
+ {
1180
+ path: ["coupons", "list"],
1181
+ summary: "List coupons and their codes.",
1182
+ options: {},
1183
+ output: "{ coupons: Coupon[] }",
1184
+ examples: ["bitsnap coupons list --json"],
1185
+ async run(ctx) {
1186
+ noPositionals(ctx);
1187
+ const { body } = await ctx.client().rpc("CouponsService", "ListCoupons");
1188
+ ctx.printer.result(body, () => {
1189
+ if (body.coupons.length === 0) return "No coupons.";
1190
+ return table(
1191
+ ["ID", "NAME", "DISCOUNT", "CODES"],
1192
+ body.coupons.map((c) => [
1193
+ c.id,
1194
+ c.name,
1195
+ c.discount?.percentOff !== void 0 ? `${c.discount.percentOff}%` : formatMoney(c.discount?.amountOff),
1196
+ c.codes.map((code2) => code2.code).join(", ") || "-"
1197
+ ])
1198
+ );
1199
+ });
1200
+ }
1201
+ },
1202
+ {
1203
+ path: ["checkout", "create"],
1204
+ summary: "Create a checkout link for a product (test key: a test-mode order).",
1205
+ options: {
1206
+ product: { type: "string", value: "<id>", description: "Product id." },
1207
+ variant: { type: "string", value: "<id>", description: "Variant id; replaces --product as the item." },
1208
+ quantity: { type: "string", value: "<n>", description: "Units. Default 1." },
1209
+ "success-url": { type: "string", value: "<url>", description: "Where the buyer goes after paying (https, or http://localhost for test keys)." },
1210
+ "cancel-url": { type: "string", value: "<url>", description: "Where the buyer goes when they cancel." },
1211
+ email: { type: "string", value: "<email>", description: "Prefill the buyer's email." },
1212
+ reference: { type: "string", value: "<text>", description: "Your own reference, stored with the order (max 200)." }
1213
+ },
1214
+ output: "{ url, orderId, environment }",
1215
+ examples: [
1216
+ "bitsnap checkout create --product prodv2_abc --success-url http://localhost:3000/thanks --json"
1217
+ ],
1218
+ async run(ctx) {
1219
+ noPositionals(ctx);
1220
+ const product = str(ctx.values, "product");
1221
+ const variant = str(ctx.values, "variant");
1222
+ const productId = variant ?? product;
1223
+ if (!productId) {
1224
+ throw usageError("Give --product <id> (and optionally --variant <id>).");
1225
+ }
1226
+ const quantity = int(ctx.values, "quantity", { min: 1 }) ?? 1;
1227
+ const request = {
1228
+ items: [{ productId, quantity }]
1229
+ };
1230
+ const email = str(ctx.values, "email");
1231
+ if (email) request.customerEmail = email;
1232
+ const successUrl = str(ctx.values, "success-url");
1233
+ if (successUrl) request.successUrl = successUrl;
1234
+ const cancelUrl = str(ctx.values, "cancel-url");
1235
+ if (cancelUrl) request.cancelUrl = cancelUrl;
1236
+ const reference = str(ctx.values, "reference");
1237
+ if (reference) request.clientReferenceId = reference;
1238
+ const { body, replayed } = await ctx.client().rpc("CheckoutService", "CreateCheckoutLink", request, {
1239
+ write: true,
1240
+ idempotencyKey: idempotencyKey(ctx)
1241
+ });
1242
+ replayedNote(ctx, replayed);
1243
+ if (!ctx.printer.json) {
1244
+ ctx.printer.note(
1245
+ `Order ${body.orderId || "(created after payment)"} in ${envLabel(body.environment)} mode.`
1246
+ );
1247
+ }
1248
+ ctx.printer.result(body, () => body.url);
1249
+ }
1250
+ },
1251
+ {
1252
+ path: ["payments", "settle"],
1253
+ summary: "Settle a test-mode order as paid (or failed). Test keys only.",
1254
+ args: "<orderId>",
1255
+ options: {
1256
+ fail: { type: "boolean", description: "Settle the payment as failed instead of paid." }
1257
+ },
1258
+ output: "{ orderId, outcome }",
1259
+ examples: ["bitsnap payments settle ord_123 --json", "bitsnap payments settle ord_123 --fail"],
1260
+ async run(ctx) {
1261
+ const orderId = onePositional(ctx, "orderId");
1262
+ const client = ctx.client();
1263
+ const key = client.requireKey();
1264
+ if (keyEnvironment(key) === "live") {
1265
+ throw new CliError({
1266
+ kind: "usage",
1267
+ message: "payments settle works only with a test key; the current key is a live key.",
1268
+ hint: "Log in with a test key (bs_test_\u2026): `bitsnap login --key bs_test_\u2026`. Nothing was sent.",
1269
+ reason: "test_mode_only"
1270
+ });
1271
+ }
1272
+ const outcome = flag(ctx.values, "fail") ? "TEST_PAYMENT_OUTCOME_FAILED" : "TEST_PAYMENT_OUTCOME_PAID";
1273
+ const { body, replayed } = await client.rpc(
1274
+ "CheckoutService",
1275
+ "SettleTestPayment",
1276
+ { orderId, outcome },
1277
+ { write: true, idempotencyKey: idempotencyKey(ctx) }
1278
+ );
1279
+ replayedNote(ctx, replayed);
1280
+ ctx.printer.result(
1281
+ body,
1282
+ () => `Order ${body.orderId} settled as ${enumLabel(body.outcome, "TEST_PAYMENT_OUTCOME_")}.`
1283
+ );
1284
+ }
1285
+ },
1286
+ {
1287
+ path: ["webhooks", "list"],
1288
+ summary: "List webhook endpoints of the key's environment.",
1289
+ options: {},
1290
+ output: "{ webhooks: Webhook[] }",
1291
+ examples: ["bitsnap webhooks list --json"],
1292
+ async run(ctx) {
1293
+ noPositionals(ctx);
1294
+ const { body } = await ctx.client().rpc("WebhooksService", "ListWebhooks");
1295
+ ctx.printer.result(body, () => {
1296
+ if (body.webhooks.length === 0) return "No webhook endpoints.";
1297
+ return table(
1298
+ ["ID", "NAME", "URL", "EVENTS"],
1299
+ body.webhooks.map((w) => [w.id, w.name || "-", w.url, w.events.join(",") || "ALL"])
1300
+ );
1301
+ });
1302
+ }
1303
+ },
1304
+ {
1305
+ path: ["webhooks", "create"],
1306
+ summary: "Create a webhook endpoint. The signing secret is shown once.",
1307
+ options: {
1308
+ url: { type: "string", value: "<https url>", description: "Public https URL events are sent to.", required: true },
1309
+ name: { type: "string", value: "<label>", description: "Optional label." },
1310
+ event: { type: "string", multiple: true, value: "<EVENT>", description: "Event type, e.g. TRANSACTION_SUCCESS; repeat for more. Default: ALL." }
1311
+ },
1312
+ output: "{ webhook: Webhook, signingSecret }",
1313
+ examples: ["bitsnap webhooks create --url https://example.com/api/webhooks --event TRANSACTION_SUCCESS --json"],
1314
+ async run(ctx) {
1315
+ noPositionals(ctx);
1316
+ const request = {
1317
+ url: str(ctx.values, "url")
1318
+ };
1319
+ const name = str(ctx.values, "name");
1320
+ if (name) request.name = name;
1321
+ const events = list(ctx.values, "event");
1322
+ if (events.length > 0) request.events = events;
1323
+ const { body, replayed } = await ctx.client().rpc("WebhooksService", "CreateWebhook", request, {
1324
+ write: true,
1325
+ idempotencyKey: idempotencyKey(ctx)
1326
+ });
1327
+ replayedNote(ctx, replayed);
1328
+ ctx.printer.result(body, () => [
1329
+ `Created ${body.webhook.id} -> ${body.webhook.url}`,
1330
+ `Signing secret: ${body.signingSecret}`,
1331
+ "Store it as BITSNAP_WEBHOOK_SECRET; it is not shown again."
1332
+ ]);
1333
+ }
1334
+ },
1335
+ {
1336
+ path: ["webhooks", "delete"],
1337
+ summary: "Delete a webhook endpoint.",
1338
+ args: "<id>",
1339
+ options: {},
1340
+ output: "{} (the API's empty answer)",
1341
+ examples: ["bitsnap webhooks delete wh_k3j2h --json"],
1342
+ async run(ctx) {
1343
+ const id = onePositional(ctx, "id");
1344
+ const { body, replayed } = await ctx.client().rpc("WebhooksService", "DeleteWebhook", { id }, {
1345
+ write: true,
1346
+ idempotencyKey: idempotencyKey(ctx)
1347
+ });
1348
+ replayedNote(ctx, replayed);
1349
+ ctx.printer.result(body, () => `Deleted ${id}.`);
1350
+ }
1351
+ },
1352
+ {
1353
+ path: ["webhooks", "listen"],
1354
+ summary: "Stream test-mode webhook events to this terminal and optionally to a local server.",
1355
+ options: {
1356
+ forward: {
1357
+ type: "string",
1358
+ value: "<url>",
1359
+ description: "POST each event to this URL with X-Content-Signature and ?sig=, e.g. http://localhost:3000/api/webhooks."
1360
+ },
1361
+ poll: { type: "boolean", description: "Use long polling instead of the event stream." },
1362
+ "max-events": { type: "string", value: "<n>", description: "Exit 0 after n events." },
1363
+ timeout: {
1364
+ type: "string",
1365
+ value: "<seconds>",
1366
+ description: "Stop after this many seconds (exit 1 if --max-events was not reached)."
1367
+ }
1368
+ },
1369
+ output: "NDJSON, one object per line: { type: ready, secret, projectId?, host, mode } | { type: event, id, event, signature, payload (exact string), forward? } | { type: status, state }",
1370
+ examples: [
1371
+ "bitsnap webhooks listen --forward http://localhost:3000/api/webhooks",
1372
+ "bitsnap webhooks listen --json --max-events 1 --timeout 120"
1373
+ ],
1374
+ async run(ctx) {
1375
+ noPositionals(ctx);
1376
+ const forward = str(ctx.values, "forward");
1377
+ if (forward !== void 0) {
1378
+ let url;
1379
+ try {
1380
+ url = new URL(forward);
1381
+ } catch {
1382
+ }
1383
+ if (!url || url.protocol !== "http:" && url.protocol !== "https:") {
1384
+ throw usageError(`--forward "${forward}" must be an http(s) URL.`, "Example: http://localhost:3000/api/webhooks");
1385
+ }
1386
+ }
1387
+ await listenForWebhooks(ctx.rt, ctx.client(), ctx.printer, {
1388
+ forward,
1389
+ poll: flag(ctx.values, "poll"),
1390
+ maxEvents: int(ctx.values, "max-events", { min: 1 }),
1391
+ timeoutSeconds: int(ctx.values, "timeout", { min: 1 })
1392
+ });
1393
+ }
1394
+ },
1395
+ {
1396
+ path: ["webhooks", "trigger"],
1397
+ summary: "Send a sample event to your webhook endpoints and listeners (test keys only).",
1398
+ args: "[event]",
1399
+ options: {},
1400
+ output: "{ eventId, event, deliveredTo }",
1401
+ examples: ["bitsnap webhooks trigger TRANSACTION_SUCCESS --json"],
1402
+ async run(ctx) {
1403
+ if (ctx.positionals.length > 1) throw usageError(`Unexpected argument "${ctx.positionals[1]}".`);
1404
+ const event = (ctx.positionals[0] ?? "TRANSACTION_SUCCESS").toUpperCase();
1405
+ const { body, replayed } = await ctx.client().rest("/api/v1/webhooks/trigger", {
1406
+ method: "POST",
1407
+ body: { event },
1408
+ write: true,
1409
+ idempotencyKey: idempotencyKey(ctx)
1410
+ });
1411
+ replayedNote(ctx, replayed);
1412
+ ctx.printer.result(
1413
+ body,
1414
+ () => `Triggered ${body.event} (${body.eventId}); delivered to ${body.deliveredTo} receiver(s).`
1415
+ );
1416
+ }
1417
+ },
1418
+ {
1419
+ path: ["openapi"],
1420
+ summary: "Download the OpenAPI document of the public API (no key needed).",
1421
+ options: {
1422
+ format: { type: "string", value: "<yaml|json>", description: "Default yaml; json with --json." },
1423
+ out: { type: "string", value: "<file>", description: "Write to this file instead of stdout." }
1424
+ },
1425
+ output: "the JSON document; with --out: { ok, path, format, bytes }",
1426
+ examples: ["bitsnap openapi --out openapi.yaml", "bitsnap openapi --format json --out openapi.json --json"],
1427
+ async run(ctx) {
1428
+ noPositionals(ctx);
1429
+ const raw = str(ctx.values, "format") ?? (ctx.printer.json ? "json" : "yaml");
1430
+ if (raw !== "yaml" && raw !== "json") throw usageError(`--format must be yaml or json, got "${raw}".`);
1431
+ const auth = resolveAuth(ctx.rt, str(ctx.values, "host"));
1432
+ const client = new ApiClient(ctx.rt, auth);
1433
+ const text = await client.getText(
1434
+ `/api/v1/openapi.${raw}`,
1435
+ raw === "json" ? "application/json" : "application/yaml"
1436
+ );
1437
+ const out = str(ctx.values, "out");
1438
+ if (out) {
1439
+ const path = resolve(out);
1440
+ writeFileSync2(path, text.endsWith("\n") ? text : text + "\n");
1441
+ ctx.printer.result({ ok: true, path, format: raw, bytes: Buffer.byteLength(text) }, () => `Wrote ${path}`);
1442
+ return;
1443
+ }
1444
+ ctx.rt.stdout.write(text.endsWith("\n") ? text : text + "\n");
1445
+ }
1446
+ },
1447
+ {
1448
+ path: ["stats"],
1449
+ summary: "Sales summary for a time range (default: the last 30 days).",
1450
+ options: {
1451
+ from: { type: "string", value: "<date>", description: "Start, YYYY-MM-DD or RFC 3339 (inclusive)." },
1452
+ to: { type: "string", value: "<date>", description: "End; a plain date means the end of that day." }
1453
+ },
1454
+ output: "{ from, to, orders, unitsSold, revenue: Money[], daily: [{ date, units, revenue }] }",
1455
+ examples: ["bitsnap stats --from 2026-10-01 --to 2026-10-30 --json"],
1456
+ async run(ctx) {
1457
+ noPositionals(ctx);
1458
+ const request = {};
1459
+ const from = str(ctx.values, "from");
1460
+ if (from) request.from = parseTime("from", from);
1461
+ const to = str(ctx.values, "to");
1462
+ if (to) request.to = parseTime("to", to, true);
1463
+ const { body } = await ctx.client().rpc("StatsService", "GetSalesSummary", request);
1464
+ ctx.printer.result(body, () => [
1465
+ `${body.from ?? "?"} \u2192 ${body.to ?? "?"}`,
1466
+ `Orders: ${body.orders} Units: ${body.unitsSold} Revenue: ${body.revenue.map((m) => formatMoney(m)).join(", ") || "-"}`,
1467
+ ...body.daily.length > 0 ? table(
1468
+ ["DATE", "UNITS", "REVENUE"],
1469
+ body.daily.map((d) => [d.date, String(d.units), d.revenue.map((m) => formatMoney(m)).join(", ") || "-"])
1470
+ ) : []
1471
+ ]);
1472
+ }
1473
+ }
1474
+ ];
1475
+
1476
+ // src/main.ts
1477
+ var BOOLEAN_GLOBALS = /* @__PURE__ */ new Set(["--json", "--help", "-h", "--version", "-v"]);
1478
+ function findCommand(args) {
1479
+ const words = [];
1480
+ const rest = [];
1481
+ let collecting = true;
1482
+ for (const arg of args) {
1483
+ if (collecting && !arg.startsWith("-") && words.length < 2) {
1484
+ words.push(arg);
1485
+ continue;
1486
+ }
1487
+ if (arg.startsWith("-") && !BOOLEAN_GLOBALS.has(arg)) collecting = false;
1488
+ rest.push(arg);
1489
+ }
1490
+ for (let length = Math.min(words.length, 2); length >= 1; length--) {
1491
+ const path = words.slice(0, length);
1492
+ const command = COMMANDS.find((c) => c.path.length === length && c.path.every((w, i) => w === path[i]));
1493
+ if (command) return { command, rest: [...words.slice(length), ...rest], words };
1494
+ }
1495
+ return { rest: [...words, ...rest], words };
1496
+ }
1497
+ function optionHelp(specs) {
1498
+ const rows = Object.entries(specs).map(([name, spec]) => {
1499
+ const short = spec.short ? `-${spec.short}, ` : "";
1500
+ const value = spec.type === "string" ? ` ${spec.value ?? "<value>"}` : "";
1501
+ return [` ${short}--${name}${value}`, spec.description + (spec.required ? " (required)" : "")];
1502
+ });
1503
+ const width = Math.max(...rows.map(([left]) => (left ?? "").length));
1504
+ return rows.map(([left, right]) => `${(left ?? "").padEnd(width)} ${right}`);
1505
+ }
1506
+ function commandLine(command) {
1507
+ return ["bitsnap", ...command.path, ...command.args ? [command.args] : []].join(" ");
1508
+ }
1509
+ function commandHelp(command) {
1510
+ return [
1511
+ `${commandLine(command)}`,
1512
+ "",
1513
+ command.summary,
1514
+ "",
1515
+ "Options:",
1516
+ ...optionHelp({ ...command.options, ...GLOBAL_OPTIONS }),
1517
+ "",
1518
+ `--json output: ${command.output}`,
1519
+ "",
1520
+ "Examples:",
1521
+ ...command.examples.map((example) => ` ${example}`)
1522
+ ].join("\n");
1523
+ }
1524
+ function groupHelp(group) {
1525
+ const commands = COMMANDS.filter((c) => c.path[0] === group);
1526
+ return [
1527
+ `bitsnap ${group} <command>`,
1528
+ "",
1529
+ ...commands.map((c) => ` ${commandLine(c).replace("bitsnap ", "").padEnd(34)}${c.summary}`),
1530
+ "",
1531
+ `Run \`bitsnap ${group} <command> --help\` for options.`
1532
+ ].join("\n");
1533
+ }
1534
+ function rootHelp(version) {
1535
+ return [
1536
+ `bitsnap ${version} - command-line tool for the Bitsnap public API`,
1537
+ "",
1538
+ "Usage: bitsnap <command> [options]",
1539
+ "",
1540
+ "Commands:",
1541
+ ...COMMANDS.map((c) => ` ${commandLine(c).replace("bitsnap ", "").padEnd(34)}${c.summary}`),
1542
+ "",
1543
+ "Global options:",
1544
+ ...optionHelp(GLOBAL_OPTIONS),
1545
+ "",
1546
+ "Environment:",
1547
+ " BITSNAP_API_KEY API key; wins over the stored one.",
1548
+ " BITSNAP_HOST API host. Default https://bitsnap.pl.",
1549
+ " XDG_CONFIG_HOME Config dir; the file is <dir>/bitsnap/config.json (mode 0600).",
1550
+ "",
1551
+ "Exit codes: 0 ok, 1 API or usage error, 2 auth error, 3 network error.",
1552
+ 'With --json, results go to stdout as JSON and errors to stderr as {"error":{\u2026}}.',
1553
+ "The CLI never prompts. Docs: https://docs.bitsnap.pl"
1554
+ ].join("\n");
1555
+ }
1556
+ function helpJson(version) {
1557
+ return {
1558
+ version,
1559
+ globalOptions: Object.entries(GLOBAL_OPTIONS).map(([name, spec]) => ({ name, type: spec.type, description: spec.description })),
1560
+ exitCodes: { ok: 0, error: 1, auth: 2, network: 3 },
1561
+ commands: COMMANDS.map((c) => ({
1562
+ command: commandLine(c).replace("bitsnap ", ""),
1563
+ summary: c.summary,
1564
+ options: Object.entries(c.options).map(([name, spec]) => ({
1565
+ name,
1566
+ type: spec.type,
1567
+ multiple: spec.multiple === true,
1568
+ required: spec.required === true,
1569
+ description: spec.description
1570
+ })),
1571
+ output: c.output,
1572
+ examples: c.examples
1573
+ }))
1574
+ };
1575
+ }
1576
+ function renderError(error, json) {
1577
+ if (json) return JSON.stringify(error.toJSON()) + "\n";
1578
+ const { message, detail, hint, reason, requestId } = error.details;
1579
+ const lines = [`Error: ${message}${reason ? ` [${reason}]` : ""}`];
1580
+ if (detail) lines.push(` ${detail}`);
1581
+ if (hint) lines.push(`Hint: ${hint}`);
1582
+ if (requestId) lines.push(`Request id: ${requestId}`);
1583
+ return lines.join("\n") + "\n";
1584
+ }
1585
+ async function main(argv, rt) {
1586
+ const json = argv.includes("--json");
1587
+ const printer = new Printer(rt, json);
1588
+ try {
1589
+ const { command, rest, words } = findCommand(argv);
1590
+ if (!command) {
1591
+ const wantsVersion = rest.includes("--version") || rest.includes("-v");
1592
+ if (wantsVersion && words.length === 0) {
1593
+ printer.result({ version: rt.version }, () => rt.version);
1594
+ return EXIT.ok;
1595
+ }
1596
+ const wantsHelp = words.length === 0 || rest.includes("--help") || rest.includes("-h");
1597
+ const group = words[0];
1598
+ if (group && COMMANDS.some((c) => c.path[0] === group) && words.length === 1 && wantsHelp) {
1599
+ printer.result(
1600
+ { commands: helpJson(rt.version).commands.filter((c) => c.command.startsWith(group + " ")) },
1601
+ () => groupHelp(group)
1602
+ );
1603
+ return EXIT.ok;
1604
+ }
1605
+ if (words.length === 0 && wantsHelp) {
1606
+ printer.result(helpJson(rt.version), () => rootHelp(rt.version));
1607
+ return EXIT.ok;
1608
+ }
1609
+ if (group && COMMANDS.some((c) => c.path[0] === group)) {
1610
+ throw usageError(`Unknown command "${words.join(" ")}".`, `Run \`bitsnap ${group} --help\`.`);
1611
+ }
1612
+ throw usageError(`Unknown command "${words.join(" ")}".`, "Run `bitsnap --help` for the list.");
1613
+ }
1614
+ const { values, positionals } = parseOptions(rest, command.options, true);
1615
+ if (values.help === true) {
1616
+ printer.result(
1617
+ {
1618
+ command: commandLine(command).replace("bitsnap ", ""),
1619
+ summary: command.summary,
1620
+ options: helpJson(rt.version).commands.find((c) => c.command === commandLine(command).replace("bitsnap ", ""))?.options,
1621
+ output: command.output,
1622
+ examples: command.examples
1623
+ },
1624
+ () => commandHelp(command)
1625
+ );
1626
+ return EXIT.ok;
1627
+ }
1628
+ let client;
1629
+ const ctx = {
1630
+ rt,
1631
+ printer,
1632
+ values,
1633
+ positionals,
1634
+ client() {
1635
+ const host = typeof values.host === "string" ? values.host : void 0;
1636
+ client ??= new ApiClient(rt, resolveAuth(rt, host));
1637
+ return client;
1638
+ }
1639
+ };
1640
+ await command.run(ctx);
1641
+ return EXIT.ok;
1642
+ } catch (error) {
1643
+ const cliError = error instanceof CliError ? error : new CliError({
1644
+ kind: "api",
1645
+ message: `Unexpected error: ${error instanceof Error ? error.message : String(error)}`
1646
+ });
1647
+ rt.stderr.write(renderError(cliError, json));
1648
+ return cliError.exitCode;
1649
+ }
1650
+ }
1651
+
1652
+ // src/cli.ts
1653
+ for (const stream of [process.stdout, process.stderr]) {
1654
+ stream.on("error", (error) => {
1655
+ if (error.code === "EPIPE") process.exit(0);
1656
+ throw error;
1657
+ });
1658
+ }
1659
+ var controller = new AbortController();
1660
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1661
+ process.once(signal, () => controller.abort());
1662
+ }
1663
+ var runtime = {
1664
+ env: process.env,
1665
+ fetch: globalThis.fetch,
1666
+ stdout: process.stdout,
1667
+ stderr: process.stderr,
1668
+ isTTY: process.stdout.isTTY === true,
1669
+ randomUUID,
1670
+ sleep: (ms, signal) => new Promise((resolve2) => {
1671
+ const timer = setTimeout(resolve2, ms);
1672
+ signal?.addEventListener(
1673
+ "abort",
1674
+ () => {
1675
+ clearTimeout(timer);
1676
+ resolve2();
1677
+ },
1678
+ { once: true }
1679
+ );
1680
+ }),
1681
+ signal: controller.signal,
1682
+ version: true ? "0.1.0" : "0.0.0-dev"
1683
+ };
1684
+ var code = await main(process.argv.slice(2), runtime);
1685
+ process.exitCode = code;