@clickraft/cli 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.
package/dist/cli.js ADDED
@@ -0,0 +1,3170 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync } from "fs";
5
+ import { dirname as dirname2, join as join2 } from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // src/api/client.ts
9
+ import { request as undiciRequest } from "undici";
10
+ import { z as z2 } from "zod";
11
+
12
+ // src/auth/redact.ts
13
+ var TOKEN_FIELD_PATTERN = /"(access_token|refresh_token|device_code|user_code|code_verifier|client_secret)"\s*:\s*"[^"]*"/gi;
14
+ var BARE_TOKEN_PATTERN = /\bck_(?:live|dev|test)_[A-Za-z0-9_-]+/gi;
15
+ function redactTokenLikeFields(input) {
16
+ return input.replace(TOKEN_FIELD_PATTERN, '"$1":"[REDACTED]"').replace(BARE_TOKEN_PATTERN, "[REDACTED]");
17
+ }
18
+ function redactTokenLikeValue(token) {
19
+ if (!token) return "<empty>";
20
+ if (token.length <= 12) return "[REDACTED]";
21
+ return `${token.slice(0, 8)}...[REDACTED]`;
22
+ }
23
+
24
+ // src/errors/codes.ts
25
+ var LOGIN_HINT = "Run `clickraft login` to authenticate.";
26
+ var RELOGIN_HINT = "Run `clickraft login` to refresh.";
27
+ var EXIT_CODE_MAP = {
28
+ // --- 37 Phase 8 Q7 codes ----------------------------------------------------
29
+ AUTH_TOKEN_MISSING: {
30
+ exitCode: 3,
31
+ category: "auth",
32
+ retryable: false,
33
+ hint: "Set `CLICKRAFT_TOKEN` or run `clickraft login`."
34
+ },
35
+ AUTH_TOKEN_INVALID: {
36
+ exitCode: 3,
37
+ category: "auth",
38
+ retryable: false,
39
+ hint: "The token is malformed or unknown. Run `clickraft login`."
40
+ },
41
+ AUTH_TOKEN_EXPIRED: {
42
+ exitCode: 3,
43
+ category: "auth",
44
+ retryable: false,
45
+ hint: RELOGIN_HINT
46
+ },
47
+ AUTH_TOKEN_REVOKED: {
48
+ exitCode: 3,
49
+ category: "auth",
50
+ retryable: false,
51
+ hint: "Token was revoked. Run `clickraft login` to mint a new one."
52
+ },
53
+ AUTH_TOKEN_SCOPE_INSUFFICIENT: {
54
+ exitCode: 4,
55
+ category: "permission",
56
+ retryable: false,
57
+ hint: "This token does not have the required scope. Re-run `clickraft login` selecting the needed scopes."
58
+ },
59
+ AUTH_ORG_MISMATCH: {
60
+ exitCode: 4,
61
+ category: "permission",
62
+ retryable: false,
63
+ hint: "This token's org no longer exists or you have been removed."
64
+ },
65
+ AUTH_DEVICE_CODE_PENDING: {
66
+ exitCode: 0,
67
+ category: "success",
68
+ retryable: false,
69
+ hint: "Continue polling."
70
+ },
71
+ AUTH_DEVICE_CODE_EXPIRED: {
72
+ exitCode: 3,
73
+ category: "auth",
74
+ retryable: false,
75
+ hint: "Device code expired. Re-run `clickraft login`."
76
+ },
77
+ AUTH_DEVICE_CODE_SLOW_DOWN: {
78
+ exitCode: 0,
79
+ category: "success",
80
+ retryable: false,
81
+ hint: "Increase polling interval (Retry-After)."
82
+ },
83
+ IDEMPOTENCY_KEY_MISSING: {
84
+ exitCode: 2,
85
+ category: "usage",
86
+ retryable: false,
87
+ hint: "Add `X-Idempotency-Key` header (or `--idempotency-key`)."
88
+ },
89
+ IDEMPOTENCY_KEY_CONFLICT: {
90
+ exitCode: 7,
91
+ category: "conflict",
92
+ retryable: false,
93
+ hint: "Same key was used with a different request body. Use a new key."
94
+ },
95
+ PLAN_FEATURE_LOCKED: {
96
+ exitCode: 4,
97
+ category: "permission",
98
+ retryable: false,
99
+ hint: "API access is not enabled on your plan. Visit billing settings."
100
+ },
101
+ PLAN_QUOTA_EXCEEDED: {
102
+ exitCode: 5,
103
+ category: "quota",
104
+ retryable: false,
105
+ hint: "Daily API quota exhausted. Wait, or upgrade your plan."
106
+ },
107
+ INSUFFICIENT_CREDITS: {
108
+ exitCode: 5,
109
+ category: "quota",
110
+ retryable: false,
111
+ hint: "Top up credits to retry."
112
+ },
113
+ CREDIT_DEDUCTION_FAILED: {
114
+ exitCode: 1,
115
+ category: "runtime",
116
+ retryable: true,
117
+ hint: "Could not charge credits. Retry; no charge was made."
118
+ },
119
+ RATE_LIMITED: {
120
+ exitCode: 5,
121
+ category: "quota",
122
+ retryable: true,
123
+ hint: "Slow down. Honor Retry-After."
124
+ },
125
+ INPUT_INVALID_FORMAT: {
126
+ exitCode: 2,
127
+ category: "usage",
128
+ retryable: false,
129
+ hint: "Check input format and try again."
130
+ },
131
+ INPUT_TOO_LONG: {
132
+ exitCode: 2,
133
+ category: "usage",
134
+ retryable: false,
135
+ hint: "Field exceeded max length."
136
+ },
137
+ WORKFLOW_VALIDATION_FAILED: {
138
+ exitCode: 2,
139
+ category: "usage",
140
+ retryable: false,
141
+ hint: "Run `workflow validate` for the full list of errors."
142
+ },
143
+ WORKFLOW_NODE_TYPE_UNKNOWN: {
144
+ exitCode: 2,
145
+ category: "usage",
146
+ retryable: false,
147
+ hint: "Run `node-type list` for the current set of node types."
148
+ },
149
+ WORKFLOW_VERSION_REQUIRED: {
150
+ exitCode: 2,
151
+ category: "usage",
152
+ retryable: false,
153
+ hint: 'Send `If-Match: "<version>"` from the last `workflow get` response.'
154
+ },
155
+ WORKFLOW_VERSION_CONFLICT: {
156
+ exitCode: 7,
157
+ category: "conflict",
158
+ retryable: false,
159
+ hint: "The workflow was updated elsewhere. Re-fetch and retry."
160
+ },
161
+ WORKFLOW_NOT_FOUND: {
162
+ exitCode: 2,
163
+ category: "usage",
164
+ retryable: false,
165
+ hint: "No workflow with that ID in your org."
166
+ },
167
+ NOT_FOUND: {
168
+ exitCode: 2,
169
+ category: "usage",
170
+ retryable: false,
171
+ hint: "Resource not found."
172
+ },
173
+ FORBIDDEN: {
174
+ exitCode: 4,
175
+ category: "permission",
176
+ retryable: false,
177
+ hint: "Your role does not permit this action."
178
+ },
179
+ MODEL_NOT_FOUND: {
180
+ exitCode: 2,
181
+ category: "usage",
182
+ retryable: false,
183
+ hint: "Unknown `modelSlug`. Run `brand-model list` for the current set."
184
+ },
185
+ PROVIDER_UNAVAILABLE: {
186
+ exitCode: 6,
187
+ category: "network",
188
+ retryable: true,
189
+ hint: "Provider is temporarily unavailable. Retry shortly."
190
+ },
191
+ STORAGE_UPLOAD_FAILED: {
192
+ exitCode: 1,
193
+ category: "runtime",
194
+ retryable: true,
195
+ hint: "Upload failed mid-stream. Retry."
196
+ },
197
+ STORAGE_QUOTA_EXCEEDED: {
198
+ exitCode: 5,
199
+ category: "quota",
200
+ retryable: false,
201
+ hint: "Org storage quota exceeded. Free space or upgrade."
202
+ },
203
+ GEN_CONTENT_REFUSAL: {
204
+ exitCode: 1,
205
+ category: "runtime",
206
+ retryable: false,
207
+ hint: "The model declined the request via safety policy."
208
+ },
209
+ GEN_OUTPUT_SCHEMA_MISMATCH: {
210
+ exitCode: 1,
211
+ category: "runtime",
212
+ retryable: true,
213
+ hint: "Provider returned malformed output. Full refund issued. Retry."
214
+ },
215
+ GEN_EMPTY_OUTPUT: {
216
+ exitCode: 1,
217
+ category: "runtime",
218
+ retryable: true,
219
+ hint: "Provider returned no usable output. Full refund issued. Retry."
220
+ },
221
+ GEN_ITEM_COUNT_MISMATCH: {
222
+ exitCode: 0,
223
+ category: "success",
224
+ retryable: false,
225
+ hint: "Partial output kept; no refund."
226
+ },
227
+ AGENT_FINGERPRINT_REJECTED: {
228
+ exitCode: 4,
229
+ category: "permission",
230
+ retryable: false,
231
+ hint: "This client identification is not permitted. Contact support."
232
+ },
233
+ INTERNAL_ERROR: {
234
+ exitCode: 1,
235
+ category: "runtime",
236
+ retryable: true,
237
+ hint: "Server error. Retry; if it persists, contact support with the request ID."
238
+ },
239
+ CREDENTIALS_LOCKED: {
240
+ exitCode: 1,
241
+ category: "runtime",
242
+ retryable: true,
243
+ hint: "Another CLI invocation holds the credentials lock. Retry shortly."
244
+ },
245
+ CREDENTIALS_PERMISSIONS_LOOSE: {
246
+ exitCode: 0,
247
+ category: "success",
248
+ retryable: false,
249
+ hint: "Credentials file mode is wider than 0600; continuing."
250
+ },
251
+ // --- 5 additional codes introduced by 1A.1 ---------------------------------
252
+ AUTH_ACCESS_DENIED: {
253
+ exitCode: 3,
254
+ category: "auth",
255
+ retryable: false,
256
+ hint: "You denied the OAuth grant. Re-run `clickraft login` to retry."
257
+ },
258
+ AUTH_FLOW_CANCELLED: {
259
+ exitCode: 3,
260
+ category: "auth",
261
+ retryable: false,
262
+ hint: "Authentication was cancelled. Re-run `clickraft login`."
263
+ },
264
+ CREDENTIALS_CORRUPT: {
265
+ exitCode: 1,
266
+ category: "runtime",
267
+ retryable: false,
268
+ hint: "Credentials file is unreadable. Remove it and run `clickraft login`."
269
+ },
270
+ CREDENTIALS_NOT_FOUND: {
271
+ exitCode: 3,
272
+ category: "auth",
273
+ retryable: false,
274
+ hint: LOGIN_HINT
275
+ },
276
+ CREDENTIALS_PROFILE_NOT_FOUND: {
277
+ exitCode: 2,
278
+ category: "usage",
279
+ retryable: false,
280
+ hint: "No such profile. Use `clickraft auth profiles` to list available profiles."
281
+ },
282
+ // --- 4 CLI-synthetic codes --------------------------------------------------
283
+ NETWORK_ERROR: {
284
+ exitCode: 6,
285
+ category: "network",
286
+ retryable: true,
287
+ hint: "Network connection failed. Check connectivity and retry."
288
+ },
289
+ TIMEOUT: {
290
+ exitCode: 6,
291
+ category: "network",
292
+ retryable: true,
293
+ hint: "Request timed out. Retry; consider increasing the timeout."
294
+ },
295
+ PARSE_ERROR: {
296
+ exitCode: 1,
297
+ category: "runtime",
298
+ retryable: false,
299
+ hint: "Server returned an unexpected response. Contact support with the request ID."
300
+ },
301
+ SCHEMA_MISMATCH: {
302
+ exitCode: 1,
303
+ category: "runtime",
304
+ retryable: false,
305
+ hint: "Server response did not match the expected schema. Update the CLI."
306
+ }
307
+ };
308
+ var CLI_ONLY_CODES = /* @__PURE__ */ new Set([
309
+ "NETWORK_ERROR",
310
+ "TIMEOUT",
311
+ "PARSE_ERROR",
312
+ "SCHEMA_MISMATCH"
313
+ ]);
314
+ var KNOWN_SERVER_CODES = new Set(
315
+ Object.keys(EXIT_CODE_MAP).filter((k) => !CLI_ONLY_CODES.has(k))
316
+ );
317
+ function getExitCode(code) {
318
+ return EXIT_CODE_MAP[code]?.exitCode ?? 1;
319
+ }
320
+ function toEnvelopeCode(code) {
321
+ return code.startsWith("E_") ? code : `E_${code}`;
322
+ }
323
+ var SIGINT_EXIT_CODE = 130;
324
+
325
+ // src/api/idempotency.ts
326
+ import { createHash, randomUUID } from "crypto";
327
+ function generateIdempotencyKey() {
328
+ return randomUUID();
329
+ }
330
+ function generateUploadIdempotencyKey(content, scope) {
331
+ const hash = createHash("sha256").update(content).digest("hex");
332
+ return `upload-${scope}-${hash}`;
333
+ }
334
+
335
+ // src/api/retry.ts
336
+ var DEFAULT_RETRY_OPTIONS = {
337
+ maxAttempts: 3,
338
+ baseDelayMs: 200,
339
+ maxDelayMs: 1e4,
340
+ jitterFraction: 0.25
341
+ };
342
+ var ABSOLUTE_MAX_RETRY_AFTER_MS = 5 * 60 * 1e3;
343
+ function computeBackoffMs(attempt, baseDelayMs, maxDelayMs, jitterFraction) {
344
+ const exp = Math.min(baseDelayMs * 2 ** Math.max(0, attempt - 1), maxDelayMs);
345
+ const jitter = exp * jitterFraction * (Math.random() * 2 - 1);
346
+ return Math.max(0, Math.round(exp + jitter));
347
+ }
348
+ function decideRetry(params) {
349
+ const { attempt, options, serverRetryAfterMs, isNetworkError, isRetryable } = params;
350
+ if (options.signal?.aborted) {
351
+ return { shouldRetry: false, delayMs: 0, reason: "aborted" };
352
+ }
353
+ if (attempt >= options.maxAttempts) {
354
+ return { shouldRetry: false, delayMs: 0, reason: "max_attempts" };
355
+ }
356
+ if (!isNetworkError && !isRetryable) {
357
+ return { shouldRetry: false, delayMs: 0, reason: "not_retryable" };
358
+ }
359
+ if (typeof serverRetryAfterMs === "number" && serverRetryAfterMs >= 0) {
360
+ let delayMs2 = serverRetryAfterMs;
361
+ if (delayMs2 > ABSOLUTE_MAX_RETRY_AFTER_MS) {
362
+ console.warn(`Server Retry-After exceeded ${ABSOLUTE_MAX_RETRY_AFTER_MS}ms; clipping.`);
363
+ delayMs2 = ABSOLUTE_MAX_RETRY_AFTER_MS;
364
+ }
365
+ return { shouldRetry: true, delayMs: delayMs2, reason: "rate_limited" };
366
+ }
367
+ const delayMs = computeBackoffMs(
368
+ attempt,
369
+ options.baseDelayMs,
370
+ options.maxDelayMs,
371
+ options.jitterFraction
372
+ );
373
+ return {
374
+ shouldRetry: true,
375
+ delayMs,
376
+ reason: isNetworkError ? "network_failure" : "retryable_error"
377
+ };
378
+ }
379
+ async function sleep(delayMs, signal) {
380
+ if (signal?.aborted) {
381
+ throw signalToAbortError(signal);
382
+ }
383
+ await new Promise((resolve, reject) => {
384
+ const timer = setTimeout(() => {
385
+ signal?.removeEventListener("abort", onAbort);
386
+ resolve();
387
+ }, delayMs);
388
+ const onAbort = () => {
389
+ clearTimeout(timer);
390
+ reject(signalToAbortError(signal));
391
+ };
392
+ signal?.addEventListener("abort", onAbort, { once: true });
393
+ });
394
+ }
395
+ function signalToAbortError(signal) {
396
+ const reason = signal?.reason;
397
+ if (reason instanceof Error) return reason;
398
+ const err = new Error("The operation was aborted.");
399
+ err.name = "AbortError";
400
+ return err;
401
+ }
402
+
403
+ // src/api/types.ts
404
+ import { z } from "zod";
405
+ var ServerSuccessSchema = (payload) => z.object({ data: payload }).strict();
406
+ var ServerErrorBodySchema = z.object({
407
+ error: z.object({
408
+ code: z.string().min(1),
409
+ message: z.string(),
410
+ correlationId: z.string().optional(),
411
+ retryAfter: z.number().int().nonnegative().optional(),
412
+ details: z.unknown().optional()
413
+ }).strict()
414
+ }).strict();
415
+
416
+ // src/api/client.ts
417
+ var ApiError = class extends Error {
418
+ code;
419
+ meta;
420
+ cause;
421
+ constructor(code, message, meta, cause) {
422
+ super(message);
423
+ this.name = "ApiError";
424
+ this.code = code;
425
+ this.meta = meta;
426
+ this.cause = cause;
427
+ }
428
+ };
429
+ var REDACT_BODY_LIMIT = 1024;
430
+ var REDACT_INPUT_CAP = 100 * 1024;
431
+ var DEFAULT_USER_AGENT = "clickraft-cli";
432
+ var ApiClient = class {
433
+ options;
434
+ retryDefaults;
435
+ constructor(options) {
436
+ this.options = options;
437
+ this.retryDefaults = {
438
+ ...DEFAULT_RETRY_OPTIONS,
439
+ ...options.retryDefaults
440
+ };
441
+ }
442
+ // Issues a request with built-in retry, Zod validation, and token redaction.
443
+ //
444
+ // Non-GET requests get an auto-generated UUIDv4 `X-Idempotency-Key` per
445
+ // Phase 8 Q8 of the wave-1 investigation. The key is computed once and
446
+ // reused across every retry attempt so the server can deduplicate. Callers
447
+ // may pass `opts.idempotencyKey` to override the default — the upload
448
+ // command uses this to derive the key from the content's SHA-256.
449
+ async request(opts) {
450
+ const retry = {
451
+ ...this.retryDefaults,
452
+ ...opts.retry
453
+ };
454
+ const signal = composeSignals([this.options.signal, opts.signal, retry.signal]);
455
+ const url = buildUrl(this.options.apiBaseUrl, opts.path, opts.query);
456
+ const effectiveIdempotencyKey = opts.method === "GET" ? opts.idempotencyKey : opts.idempotencyKey ?? generateIdempotencyKey();
457
+ const headers = this.buildHeaders({ ...opts, idempotencyKey: effectiveIdempotencyKey });
458
+ const bodyText = serializeBody(opts);
459
+ let attempt = 0;
460
+ let lastError;
461
+ while (attempt < retry.maxAttempts) {
462
+ attempt += 1;
463
+ if (signal.aborted) {
464
+ throw toAbortApiError(signal);
465
+ }
466
+ try {
467
+ return await this.singleAttempt({
468
+ url,
469
+ method: opts.method,
470
+ headers,
471
+ bodyText,
472
+ signal,
473
+ responseSchema: opts.responseSchema
474
+ });
475
+ } catch (err) {
476
+ if (!(err instanceof ApiError)) throw err;
477
+ lastError = err;
478
+ const isNetworkError = err.code === "NETWORK_ERROR";
479
+ const isRetryable = err.meta.retryable;
480
+ const decision = decideRetry({
481
+ attempt,
482
+ options: { ...retry, signal },
483
+ serverRetryAfterMs: err.meta.retryAfterMs,
484
+ isNetworkError,
485
+ isRetryable
486
+ });
487
+ if (!decision.shouldRetry) throw err;
488
+ try {
489
+ await sleep(decision.delayMs, signal);
490
+ } catch (sleepErr) {
491
+ throw toAbortApiError(signal, sleepErr);
492
+ }
493
+ }
494
+ }
495
+ throw lastError ?? new ApiError("INTERNAL_ERROR", "Retry loop terminated unexpectedly.", {
496
+ retryable: false
497
+ });
498
+ }
499
+ buildHeaders(opts) {
500
+ const h = {
501
+ accept: "application/json",
502
+ "user-agent": this.options.userAgent ?? DEFAULT_USER_AGENT
503
+ };
504
+ if (this.options.accessToken) {
505
+ h.authorization = `Bearer ${this.options.accessToken}`;
506
+ }
507
+ if (opts.body !== void 0 && opts.method !== "GET" && opts.method !== "DELETE") {
508
+ h["content-type"] = "application/json";
509
+ }
510
+ if (opts.idempotencyKey) {
511
+ h["x-idempotency-key"] = opts.idempotencyKey;
512
+ }
513
+ if (opts.headers) {
514
+ for (const [k, v] of Object.entries(opts.headers)) {
515
+ h[k.toLowerCase()] = v;
516
+ }
517
+ }
518
+ return h;
519
+ }
520
+ async singleAttempt(args) {
521
+ let response;
522
+ try {
523
+ response = await undiciRequest(args.url, {
524
+ method: args.method,
525
+ headers: args.headers,
526
+ body: args.bodyText,
527
+ signal: args.signal,
528
+ dispatcher: this.options.dispatcher
529
+ });
530
+ } catch (err) {
531
+ if (isAbortError(err)) {
532
+ throw toAbortApiError(args.signal, err);
533
+ }
534
+ throw new ApiError(
535
+ "NETWORK_ERROR",
536
+ `Network request failed: ${errorMessage(err)}`,
537
+ { retryable: true },
538
+ err
539
+ );
540
+ }
541
+ const { statusCode, headers: responseHeaders, body } = response;
542
+ let bodyText;
543
+ try {
544
+ bodyText = await body.text();
545
+ } catch (err) {
546
+ if (isAbortError(err)) {
547
+ throw toAbortApiError(args.signal, err);
548
+ }
549
+ throw new ApiError(
550
+ "NETWORK_ERROR",
551
+ `Failed to read response body: ${errorMessage(err)}`,
552
+ { status: statusCode, retryable: true },
553
+ err
554
+ );
555
+ }
556
+ const requestId = pickHeader(responseHeaders, "x-correlation-id");
557
+ if (statusCode >= 200 && statusCode < 300) {
558
+ return parseSuccessBody({
559
+ bodyText,
560
+ statusCode,
561
+ requestId,
562
+ responseSchema: args.responseSchema
563
+ });
564
+ }
565
+ throw parseErrorBody({
566
+ bodyText,
567
+ statusCode,
568
+ requestId,
569
+ retryAfterHeader: pickHeader(responseHeaders, "retry-after")
570
+ });
571
+ }
572
+ };
573
+ function parseSuccessBody(args) {
574
+ let parsed;
575
+ try {
576
+ parsed = JSON.parse(args.bodyText);
577
+ } catch (err) {
578
+ throw new ApiError(
579
+ "PARSE_ERROR",
580
+ "Server returned a non-JSON response.",
581
+ {
582
+ status: args.statusCode,
583
+ requestId: args.requestId,
584
+ retryable: false,
585
+ redactedBody: redactAndTruncate(args.bodyText)
586
+ },
587
+ err
588
+ );
589
+ }
590
+ const envelope = ServerSuccessSchema(args.responseSchema ?? z2.unknown()).safeParse(parsed);
591
+ if (!envelope.success) {
592
+ throw new ApiError("SCHEMA_MISMATCH", "Server response did not match the expected schema.", {
593
+ status: args.statusCode,
594
+ requestId: args.requestId,
595
+ retryable: false,
596
+ redactedBody: redactAndTruncate(args.bodyText)
597
+ });
598
+ }
599
+ return envelope.data.data;
600
+ }
601
+ function parseErrorBody(args) {
602
+ const redactedBody = redactAndTruncate(args.bodyText);
603
+ let parsed;
604
+ try {
605
+ parsed = JSON.parse(args.bodyText);
606
+ } catch {
607
+ return new ApiError(
608
+ "PARSE_ERROR",
609
+ `Server returned a ${args.statusCode} response with a non-JSON body.`,
610
+ {
611
+ status: args.statusCode,
612
+ requestId: args.requestId,
613
+ retryable: args.statusCode >= 500,
614
+ redactedBody
615
+ }
616
+ );
617
+ }
618
+ const envelope = ServerErrorBodySchema.safeParse(parsed);
619
+ if (!envelope.success) {
620
+ return new ApiError(
621
+ "PARSE_ERROR",
622
+ `Server returned ${args.statusCode} but body did not match the error envelope.`,
623
+ {
624
+ status: args.statusCode,
625
+ requestId: args.requestId,
626
+ retryable: args.statusCode >= 500,
627
+ redactedBody
628
+ }
629
+ );
630
+ }
631
+ return apiErrorFromServerBody({
632
+ body: envelope.data,
633
+ statusCode: args.statusCode,
634
+ requestId: args.requestId,
635
+ retryAfterHeader: args.retryAfterHeader,
636
+ redactedBody
637
+ });
638
+ }
639
+ function apiErrorFromServerBody(args) {
640
+ const { error } = args.body;
641
+ let code;
642
+ let safeMessage;
643
+ if (KNOWN_SERVER_CODES.has(error.code)) {
644
+ code = error.code;
645
+ safeMessage = redactTokenLikeFields(error.message);
646
+ } else {
647
+ const redactedRawCode = redactTokenLikeFields(error.code);
648
+ console.warn(`Unknown server error code: ${redactedRawCode}. Treating as INTERNAL_ERROR.`);
649
+ code = "INTERNAL_ERROR";
650
+ safeMessage = `Unknown server error code: ${redactedRawCode}.`;
651
+ }
652
+ const mapping = EXIT_CODE_MAP[code];
653
+ const retryable = mapping?.retryable ?? (args.statusCode >= 500 || args.statusCode === 429);
654
+ const retryAfterMs = typeof error.retryAfter === "number" ? error.retryAfter * 1e3 : parseRetryAfterHeader(args.retryAfterHeader);
655
+ return new ApiError(code, safeMessage, {
656
+ status: args.statusCode,
657
+ requestId: error.correlationId ?? args.requestId,
658
+ retryable,
659
+ retryAfterMs,
660
+ redactedBody: args.redactedBody
661
+ });
662
+ }
663
+ function parseRetryAfterHeader(headerValue) {
664
+ if (!headerValue) return void 0;
665
+ const trimmed = headerValue.trim();
666
+ if (/^\d+$/.test(trimmed)) {
667
+ return Number.parseInt(trimmed, 10) * 1e3;
668
+ }
669
+ if (/[A-Za-z]/.test(trimmed)) {
670
+ const parsed = Date.parse(trimmed);
671
+ if (!Number.isNaN(parsed)) {
672
+ return Math.max(0, parsed - Date.now());
673
+ }
674
+ }
675
+ return void 0;
676
+ }
677
+ function buildUrl(baseUrl, path, query) {
678
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
679
+ const relative = path.startsWith("/") ? path.slice(1) : path;
680
+ const url = new URL(relative, base);
681
+ if (query) {
682
+ for (const [key, value] of Object.entries(query)) {
683
+ if (value === void 0) continue;
684
+ url.searchParams.append(key, String(value));
685
+ }
686
+ }
687
+ return url.toString();
688
+ }
689
+ function serializeBody(opts) {
690
+ if (opts.body === void 0) return void 0;
691
+ if (opts.method === "GET") return void 0;
692
+ return JSON.stringify(opts.body);
693
+ }
694
+ function composeSignals(signals) {
695
+ const present = signals.filter((s) => s !== void 0);
696
+ if (present.length === 0) return new AbortController().signal;
697
+ if (present.length === 1) return present[0];
698
+ const any = AbortSignal.any;
699
+ if (typeof any === "function") return any(present);
700
+ const ctrl = new AbortController();
701
+ for (const s of present) {
702
+ if (s.aborted) {
703
+ ctrl.abort(s.reason);
704
+ break;
705
+ }
706
+ s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
707
+ }
708
+ return ctrl.signal;
709
+ }
710
+ function pickHeader(headers, name) {
711
+ const value = headers[name] ?? headers[name.toLowerCase()];
712
+ if (Array.isArray(value)) return value[0];
713
+ return value;
714
+ }
715
+ function redactAndTruncate(body) {
716
+ const input = body.length > REDACT_INPUT_CAP ? body.slice(0, REDACT_INPUT_CAP) : body;
717
+ const redacted = redactTokenLikeFields(input);
718
+ return redacted.length > REDACT_BODY_LIMIT ? `${redacted.slice(0, REDACT_BODY_LIMIT)}\u2026` : redacted;
719
+ }
720
+ function isAbortError(err) {
721
+ if (!(err instanceof Error)) return false;
722
+ return err.name === "AbortError" || /aborted/i.test(err.message);
723
+ }
724
+ function toAbortApiError(signal, cause) {
725
+ const reasonMessage = signal.reason instanceof Error ? signal.reason.message : "Request was aborted.";
726
+ return new ApiError("TIMEOUT", reasonMessage, { retryable: true }, cause);
727
+ }
728
+ function errorMessage(err) {
729
+ if (err instanceof Error) return err.message;
730
+ return String(err);
731
+ }
732
+
733
+ // src/auth/errors.ts
734
+ var AuthError = class extends Error {
735
+ code;
736
+ cause;
737
+ constructor(code, message, cause) {
738
+ super(message);
739
+ this.name = "AuthError";
740
+ this.code = code;
741
+ this.cause = cause;
742
+ }
743
+ };
744
+ var CredentialsError = class extends Error {
745
+ code;
746
+ cause;
747
+ constructor(code, message, cause) {
748
+ super(message);
749
+ this.name = "CredentialsError";
750
+ this.code = code;
751
+ this.cause = cause;
752
+ }
753
+ };
754
+
755
+ // src/api/envelope.ts
756
+ function wrapSuccess(data, ctx) {
757
+ const meta = {
758
+ command: ctx.command,
759
+ version: ctx.version,
760
+ duration_ms: Math.max(0, Date.now() - ctx.startMs)
761
+ };
762
+ if (ctx.requestId) meta.request_id = ctx.requestId;
763
+ if (ctx.nextCursor) meta.next_cursor = ctx.nextCursor;
764
+ return { ok: true, data, error: null, meta };
765
+ }
766
+ function wrapError(err, ctx) {
767
+ const payload = errorToEnvelopePayload(err);
768
+ const meta = {
769
+ command: ctx.command,
770
+ version: ctx.version,
771
+ duration_ms: Math.max(0, Date.now() - ctx.startMs)
772
+ };
773
+ if (payload.request_id ?? ctx.requestId) {
774
+ meta.request_id = payload.request_id ?? ctx.requestId;
775
+ }
776
+ return { ok: false, data: null, error: payload, meta };
777
+ }
778
+ function errorToEnvelopePayload(err) {
779
+ if (err instanceof ApiError) {
780
+ return buildPayload({
781
+ code: err.code,
782
+ message: err.message,
783
+ retryable: err.meta.retryable,
784
+ retryAfterMs: err.meta.retryAfterMs,
785
+ requestId: err.meta.requestId
786
+ });
787
+ }
788
+ if (err instanceof AuthError || err instanceof CredentialsError) {
789
+ const code = err.code;
790
+ const mapping = EXIT_CODE_MAP[code];
791
+ return buildPayload({
792
+ code,
793
+ message: err.message,
794
+ retryable: mapping?.retryable ?? false
795
+ });
796
+ }
797
+ const message = err instanceof Error ? err.message : String(err);
798
+ return buildPayload({
799
+ code: "INTERNAL_ERROR",
800
+ message,
801
+ retryable: false
802
+ });
803
+ }
804
+ function buildPayload(args) {
805
+ const mapping = EXIT_CODE_MAP[args.code];
806
+ const payload = {
807
+ code: toEnvelopeCode(args.code),
808
+ message: args.message,
809
+ retryable: args.retryable
810
+ };
811
+ if (typeof args.retryAfterMs === "number") payload.retry_after_ms = args.retryAfterMs;
812
+ if (args.requestId) payload.request_id = args.requestId;
813
+ if (mapping?.hint) payload.hint = mapping.hint;
814
+ return payload;
815
+ }
816
+
817
+ // src/schemas/jobs.ts
818
+ import { z as z3 } from "zod";
819
+ var GenerateCreateRequestSchema = z3.object({
820
+ modelSlug: z3.string().min(1).max(100),
821
+ input: z3.object({
822
+ prompt: z3.string().max(1e4).optional(),
823
+ referenceImageUrl: z3.string().url().optional(),
824
+ referenceImages: z3.array(z3.string().url()).max(8).optional(),
825
+ aspectRatio: z3.string().regex(/^\d+:\d+$/).optional(),
826
+ resolution: z3.string().regex(/^\d+x\d+$/).optional(),
827
+ durationSeconds: z3.number().int().min(1).max(60).optional(),
828
+ providerParams: z3.record(z3.string(), z3.unknown()).optional()
829
+ }).strict(),
830
+ options: z3.object({
831
+ workflowId: z3.string().uuid().optional(),
832
+ nodeId: z3.string().optional()
833
+ }).strict().optional()
834
+ }).strict();
835
+ var GenerateCreateResponseSchema = z3.object({
836
+ jobId: z3.string().uuid(),
837
+ status: z3.enum(["queued", "processing", "completed", "failed"]),
838
+ estimatedSeconds: z3.number().int().nonnegative(),
839
+ creditsCharged: z3.number().int().nonnegative()
840
+ }).strict();
841
+ var GenerationStatusSchema = z3.enum([
842
+ "queued",
843
+ "processing",
844
+ "uploading",
845
+ "completed",
846
+ "failed",
847
+ "cancelled"
848
+ ]);
849
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
850
+ "completed",
851
+ "failed",
852
+ "cancelled"
853
+ ]);
854
+ function isTerminalStatus(status) {
855
+ return TERMINAL_STATUSES.has(status);
856
+ }
857
+ var GenerationResultSchema = z3.object({
858
+ jobId: z3.string().uuid(),
859
+ status: GenerationStatusSchema,
860
+ modelSlug: z3.string(),
861
+ resultUrl: z3.string().url().nullable(),
862
+ thumbnailUrl: z3.string().url().nullable(),
863
+ errorCode: z3.string().nullable(),
864
+ errorMessage: z3.string().nullable(),
865
+ creditsCharged: z3.number().int().nonnegative(),
866
+ creditsRefunded: z3.boolean(),
867
+ startedAt: z3.string().datetime().nullable(),
868
+ completedAt: z3.string().datetime().nullable()
869
+ }).strict();
870
+
871
+ // src/commands/upload.ts
872
+ import { createHash as createHash2 } from "crypto";
873
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
874
+ import { basename } from "path";
875
+ import { request as undiciRequest2 } from "undici";
876
+
877
+ // src/auth/credentials.ts
878
+ import {
879
+ access,
880
+ chmod,
881
+ mkdir,
882
+ readFile,
883
+ rename,
884
+ rm,
885
+ stat,
886
+ unlink,
887
+ writeFile
888
+ } from "fs/promises";
889
+ import { dirname } from "path";
890
+ import * as lockfile from "proper-lockfile";
891
+
892
+ // src/config/paths.ts
893
+ import { homedir } from "os";
894
+ import { join } from "path";
895
+ var CREDENTIALS_FILE = "credentials.json";
896
+ var APP_DIR = "clickraft";
897
+ function getConfigDir(ctx = {}) {
898
+ const env = ctx.env ?? process.env;
899
+ const platform = ctx.platform ?? process.platform;
900
+ const home = ctx.home ?? homedir();
901
+ const xdg = env.XDG_CONFIG_HOME;
902
+ if (xdg && xdg.length > 0) {
903
+ return join(xdg, APP_DIR);
904
+ }
905
+ if (platform === "win32") {
906
+ const appData = env.APPDATA;
907
+ if (appData && appData.length > 0) {
908
+ return join(appData, APP_DIR);
909
+ }
910
+ return join(home, "AppData", "Roaming", APP_DIR);
911
+ }
912
+ return join(home, ".config", APP_DIR);
913
+ }
914
+ function getCredentialsPath(ctx = {}) {
915
+ return join(getConfigDir(ctx), CREDENTIALS_FILE);
916
+ }
917
+
918
+ // src/auth/types.ts
919
+ import { z as z4 } from "zod";
920
+ var DeviceAuthorizationRequestSchema = z4.object({
921
+ client_id: z4.string(),
922
+ scope: z4.string()
923
+ });
924
+ var DeviceAuthorizationResponseSchema = z4.object({
925
+ device_code: z4.string(),
926
+ user_code: z4.string().regex(/^[A-Z0-9]{4}-[A-Z0-9]{4}$/),
927
+ verification_uri: z4.string().url(),
928
+ verification_uri_complete: z4.string().url().optional(),
929
+ expires_in: z4.number().int().positive(),
930
+ interval: z4.number().int().positive().default(5)
931
+ });
932
+ var DeviceTokenPollRequestSchema = z4.object({
933
+ client_id: z4.string(),
934
+ device_code: z4.string(),
935
+ grant_type: z4.literal("urn:ietf:params:oauth:grant-type:device_code")
936
+ });
937
+ var TokenResponseSchema = z4.object({
938
+ access_token: z4.string(),
939
+ token_type: z4.literal("Bearer"),
940
+ expires_in: z4.number().int().positive(),
941
+ scope: z4.string()
942
+ });
943
+ var DeviceTokenPollErrorCodeSchema = z4.enum([
944
+ "authorization_pending",
945
+ "slow_down",
946
+ "access_denied",
947
+ "expired_token",
948
+ "invalid_client",
949
+ "invalid_grant",
950
+ "unauthorized_client",
951
+ "unsupported_grant_type",
952
+ "invalid_scope"
953
+ ]);
954
+ var DeviceTokenPollErrorSchema = z4.object({
955
+ error: DeviceTokenPollErrorCodeSchema,
956
+ error_description: z4.string().optional()
957
+ });
958
+ var CredentialsProfileSchema = z4.object({
959
+ apiBaseUrl: z4.string().url(),
960
+ accessToken: z4.string(),
961
+ tokenType: z4.literal("Bearer"),
962
+ tokenPrefix: z4.string(),
963
+ organizationId: z4.string().uuid(),
964
+ organizationSlug: z4.string(),
965
+ userId: z4.string().uuid(),
966
+ scopes: z4.array(z4.string()),
967
+ expiresAt: z4.string().datetime(),
968
+ issuedAt: z4.string().datetime(),
969
+ clientId: z4.string()
970
+ }).strict();
971
+ var CredentialsFileSchema = z4.object({
972
+ version: z4.literal(1),
973
+ defaultProfile: z4.string(),
974
+ profiles: z4.record(z4.string(), CredentialsProfileSchema)
975
+ }).strict();
976
+
977
+ // src/auth/credentials.ts
978
+ var LOCK_OPTS = {
979
+ retries: { retries: 10, factor: 1.5, minTimeout: 100, maxTimeout: 500 },
980
+ realpath: false,
981
+ stale: 5e3,
982
+ update: 2e3
983
+ };
984
+ var IS_WINDOWS = process.platform === "win32";
985
+ async function readCredentials(path) {
986
+ const target = path ?? getCredentialsPath();
987
+ try {
988
+ await access(target);
989
+ } catch {
990
+ return null;
991
+ }
992
+ if (!IS_WINDOWS) {
993
+ try {
994
+ const s = await stat(target);
995
+ if ((s.mode & 63) !== 0) {
996
+ const modeStr = (s.mode & 511).toString(8).padStart(3, "0");
997
+ console.warn(
998
+ `[clickraft] credentials file at ${target} has loose permissions (mode ${modeStr}). Run \`chmod 600 ${target}\` to restrict it.`
999
+ );
1000
+ }
1001
+ } catch {
1002
+ }
1003
+ }
1004
+ return parseCredentialsFile(await readFile(target, "utf8"), target);
1005
+ }
1006
+ async function setProfile(name, profile, path) {
1007
+ const target = path ?? getCredentialsPath();
1008
+ const validatedProfile = validateProfile(profile);
1009
+ await withLock(target, async () => {
1010
+ const current = await readUnderLock(target) ?? {
1011
+ version: 1,
1012
+ defaultProfile: name,
1013
+ profiles: {}
1014
+ };
1015
+ current.profiles[name] = validatedProfile;
1016
+ await writeAtomic(target, serialize(current));
1017
+ });
1018
+ }
1019
+ async function removeProfile(name, path) {
1020
+ const target = path ?? getCredentialsPath();
1021
+ await withLock(target, async () => {
1022
+ const current = await readUnderLock(target);
1023
+ if (!current) {
1024
+ throw new CredentialsError(
1025
+ "CREDENTIALS_NOT_FOUND",
1026
+ `No credentials file at ${target}; nothing to remove.`
1027
+ );
1028
+ }
1029
+ if (!(name in current.profiles)) {
1030
+ throw new CredentialsError(
1031
+ "CREDENTIALS_PROFILE_NOT_FOUND",
1032
+ `Profile "${name}" not found in credentials file.`
1033
+ );
1034
+ }
1035
+ delete current.profiles[name];
1036
+ if (current.defaultProfile === name) {
1037
+ const remaining = Object.keys(current.profiles);
1038
+ current.defaultProfile = remaining[0] ?? "";
1039
+ }
1040
+ await writeAtomic(target, serialize(current));
1041
+ });
1042
+ }
1043
+ async function deleteCredentials(path) {
1044
+ const target = path ?? getCredentialsPath();
1045
+ await withLock(target, async () => {
1046
+ try {
1047
+ await rm(target);
1048
+ } catch (err) {
1049
+ if (err.code !== "ENOENT") throw err;
1050
+ }
1051
+ });
1052
+ }
1053
+ async function withLock(target, fn) {
1054
+ await ensureDir(dirname(target));
1055
+ let release;
1056
+ try {
1057
+ release = await lockfile.lock(target, LOCK_OPTS);
1058
+ } catch (err) {
1059
+ throw new CredentialsError(
1060
+ "CREDENTIALS_LOCKED",
1061
+ `Failed to acquire exclusive lock on ${target}. Another clickraft process may be holding it; retry shortly.`,
1062
+ err
1063
+ );
1064
+ }
1065
+ try {
1066
+ return await fn();
1067
+ } finally {
1068
+ try {
1069
+ await release();
1070
+ } catch {
1071
+ }
1072
+ }
1073
+ }
1074
+ async function readUnderLock(target) {
1075
+ try {
1076
+ await access(target);
1077
+ } catch {
1078
+ return null;
1079
+ }
1080
+ return parseCredentialsFile(await readFile(target, "utf8"), target);
1081
+ }
1082
+ function validateProfile(profile) {
1083
+ const result = CredentialsProfileSchema.safeParse(profile);
1084
+ if (!result.success) {
1085
+ throw new CredentialsError(
1086
+ "CREDENTIALS_CORRUPT",
1087
+ "Refusing to write credentials profile: input failed schema validation.",
1088
+ result.error
1089
+ );
1090
+ }
1091
+ return result.data;
1092
+ }
1093
+ function parseCredentialsFile(raw, target) {
1094
+ let parsed;
1095
+ try {
1096
+ parsed = JSON.parse(raw);
1097
+ } catch (err) {
1098
+ throw new CredentialsError(
1099
+ "CREDENTIALS_CORRUPT",
1100
+ `credentials file at ${target} is not valid JSON. Run \`clickraft login\` to re-authenticate.`,
1101
+ err
1102
+ );
1103
+ }
1104
+ const result = CredentialsFileSchema.safeParse(parsed);
1105
+ if (!result.success) {
1106
+ throw new CredentialsError(
1107
+ "CREDENTIALS_CORRUPT",
1108
+ `credentials file at ${target} has an unexpected shape. Run \`clickraft login\` to re-authenticate.`,
1109
+ result.error
1110
+ );
1111
+ }
1112
+ return result.data;
1113
+ }
1114
+ async function ensureDir(dir) {
1115
+ const opts = IS_WINDOWS ? { recursive: true } : { recursive: true, mode: 448 };
1116
+ await mkdir(dir, opts);
1117
+ }
1118
+ async function writeAtomic(target, payload) {
1119
+ const tmp = `${target}.tmp`;
1120
+ const writeOpts = IS_WINDOWS ? { encoding: "utf8" } : { encoding: "utf8", mode: 384 };
1121
+ await writeFile(tmp, payload, writeOpts);
1122
+ try {
1123
+ if (!IS_WINDOWS) {
1124
+ await chmod(tmp, 384);
1125
+ }
1126
+ await rename(tmp, target);
1127
+ } catch (err) {
1128
+ try {
1129
+ await unlink(tmp);
1130
+ } catch {
1131
+ }
1132
+ throw err;
1133
+ }
1134
+ }
1135
+ function serialize(file) {
1136
+ return JSON.stringify(file, null, 2) + "\n";
1137
+ }
1138
+
1139
+ // src/config/loader.ts
1140
+ var DEFAULT_API_BASE_URL = "https://clickraft.ai/api/agents/v1";
1141
+ var DEFAULT_PROFILE_NAME = "default";
1142
+ var TOKEN_ENV_VAR = "CLICKRAFT_TOKEN";
1143
+ var PROFILE_ENV_VAR = "CLICKRAFT_PROFILE";
1144
+ var API_BASE_URL_ENV_VAR = "CLICKRAFT_API_BASE_URL";
1145
+ var TELEMETRY_DISABLE_ENV_VAR = "CLICKRAFT_DISABLE_TELEMETRY";
1146
+ async function loadConfig(options = {}) {
1147
+ const env = options.env ?? process.env;
1148
+ const flagToken = trim(options.flagToken);
1149
+ const envToken = trim(env[TOKEN_ENV_VAR]);
1150
+ const flagProfile = trim(options.flagProfile);
1151
+ const envProfile = trim(env[PROFILE_ENV_VAR]);
1152
+ const flagApiBaseUrl = trim(options.flagApiBaseUrl);
1153
+ const envApiBaseUrl = trim(env[API_BASE_URL_ENV_VAR]);
1154
+ const profileName = flagProfile ?? envProfile;
1155
+ const tokenFromFlagOrEnv = flagToken ?? envToken;
1156
+ const apiBaseFromFlagOrEnv = flagApiBaseUrl ?? envApiBaseUrl;
1157
+ const needsFile = !tokenFromFlagOrEnv || !apiBaseFromFlagOrEnv || !profileName;
1158
+ let fileProfileName;
1159
+ let fileAccessToken;
1160
+ let fileApiBaseUrl;
1161
+ if (needsFile) {
1162
+ const path = options.credentialsPath ?? getCredentialsPath();
1163
+ const file = await readCredentials(path);
1164
+ if (file) {
1165
+ const resolvedName = profileName ?? file.defaultProfile ?? DEFAULT_PROFILE_NAME;
1166
+ fileProfileName = file.defaultProfile;
1167
+ const profile = file.profiles[resolvedName];
1168
+ if (profile) {
1169
+ fileAccessToken = profile.accessToken;
1170
+ fileApiBaseUrl = profile.apiBaseUrl;
1171
+ }
1172
+ }
1173
+ }
1174
+ return {
1175
+ accessToken: tokenFromFlagOrEnv ?? fileAccessToken ?? null,
1176
+ profileName: profileName ?? fileProfileName ?? DEFAULT_PROFILE_NAME,
1177
+ apiBaseUrl: apiBaseFromFlagOrEnv ?? fileApiBaseUrl ?? DEFAULT_API_BASE_URL
1178
+ };
1179
+ }
1180
+ function trim(value) {
1181
+ if (value === void 0) return void 0;
1182
+ const t = value.trim();
1183
+ return t.length === 0 ? void 0 : t;
1184
+ }
1185
+
1186
+ // src/schemas/assets.ts
1187
+ import { z as z5 } from "zod";
1188
+ var MAX_UPLOAD_SIZE_BYTES = 15 * 1024 * 1024;
1189
+ var UploadRequestSchema = z5.object({
1190
+ filename: z5.string().min(1).max(255),
1191
+ contentType: z5.string().regex(/^[\w-]+\/[\w+.-]+$/),
1192
+ sizeBytes: z5.number().int().positive().max(MAX_UPLOAD_SIZE_BYTES),
1193
+ contentHash: z5.string().regex(/^sha256:[0-9a-f]{64}$/)
1194
+ }).strict();
1195
+ var UploadResponseSchema = z5.object({
1196
+ assetId: z5.string().uuid(),
1197
+ uploadUrl: z5.string().url(),
1198
+ publicUrl: z5.string().url(),
1199
+ deduplicated: z5.boolean()
1200
+ }).strict();
1201
+
1202
+ // src/commands/upload.ts
1203
+ var DEFAULT_UPLOAD_SCOPE = "user-uploads";
1204
+ async function uploadFile(opts) {
1205
+ const scope = opts.scope ?? DEFAULT_UPLOAD_SCOPE;
1206
+ const size = await safeStatSize(opts.filePath);
1207
+ validateSize(size);
1208
+ const content = await safeReadFile(opts.filePath);
1209
+ const contentType = detectContentType(opts.filePath, content);
1210
+ const sha = sha256Hex(content);
1211
+ const contentHash = `sha256:${sha}`;
1212
+ const filename = basename(opts.filePath);
1213
+ const idempotencyKey = generateUploadIdempotencyKey(content, scope);
1214
+ const client = opts.client ?? await buildClient(opts);
1215
+ const response = await postAssetMetadata({
1216
+ client,
1217
+ filename,
1218
+ contentType,
1219
+ sizeBytes: content.length,
1220
+ contentHash,
1221
+ idempotencyKey,
1222
+ signal: opts.signal
1223
+ });
1224
+ if (!response.deduplicated) {
1225
+ const first = await tryPut({
1226
+ uploadUrl: response.uploadUrl,
1227
+ content,
1228
+ contentType,
1229
+ signal: opts.signal,
1230
+ dispatcher: opts.putDispatcher
1231
+ });
1232
+ if (!first.ok) {
1233
+ if (first.kind === "fatal") {
1234
+ throw new ApiError(
1235
+ "STORAGE_UPLOAD_FAILED",
1236
+ `Upload to signed URL failed for ${filename} (HTTP ${first.status ?? "unknown"}).`,
1237
+ { retryable: false }
1238
+ );
1239
+ }
1240
+ const retry = await tryPut({
1241
+ uploadUrl: response.uploadUrl,
1242
+ content,
1243
+ contentType,
1244
+ signal: opts.signal,
1245
+ dispatcher: opts.putDispatcher
1246
+ });
1247
+ if (!retry.ok) {
1248
+ throw new ApiError(
1249
+ "STORAGE_UPLOAD_FAILED",
1250
+ `Upload to signed URL failed for ${filename} after one retry.`,
1251
+ { retryable: true }
1252
+ );
1253
+ }
1254
+ }
1255
+ }
1256
+ return {
1257
+ assetId: response.assetId,
1258
+ publicUrl: response.publicUrl,
1259
+ deduplicated: response.deduplicated,
1260
+ bytes: content.length,
1261
+ contentHash
1262
+ };
1263
+ }
1264
+ async function buildClient(opts) {
1265
+ const cfg = await loadConfig({
1266
+ flagProfile: opts.profile,
1267
+ flagApiBaseUrl: opts.apiBaseUrl,
1268
+ flagToken: opts.token
1269
+ });
1270
+ return new ApiClient({
1271
+ apiBaseUrl: cfg.apiBaseUrl,
1272
+ accessToken: cfg.accessToken,
1273
+ signal: opts.signal
1274
+ });
1275
+ }
1276
+ async function postAssetMetadata(args) {
1277
+ return args.client.request({
1278
+ method: "POST",
1279
+ path: "/api/agents/v1/assets",
1280
+ body: {
1281
+ filename: args.filename,
1282
+ contentType: args.contentType,
1283
+ sizeBytes: args.sizeBytes,
1284
+ contentHash: args.contentHash
1285
+ },
1286
+ idempotencyKey: args.idempotencyKey,
1287
+ responseSchema: UploadResponseSchema,
1288
+ signal: args.signal
1289
+ });
1290
+ }
1291
+ async function tryPut(args) {
1292
+ try {
1293
+ const response = await undiciRequest2(args.uploadUrl, {
1294
+ method: "PUT",
1295
+ body: args.content,
1296
+ headers: {
1297
+ "content-type": args.contentType,
1298
+ "content-length": String(args.content.length)
1299
+ },
1300
+ signal: args.signal,
1301
+ dispatcher: args.dispatcher
1302
+ });
1303
+ await response.body.text();
1304
+ const status = response.statusCode;
1305
+ if (status >= 200 && status < 300) return { ok: true };
1306
+ if (status >= 400 && status < 500) return { ok: false, kind: "fatal", status };
1307
+ return { ok: false, kind: "retryable", status };
1308
+ } catch {
1309
+ return { ok: false, kind: "retryable" };
1310
+ }
1311
+ }
1312
+ async function safeStatSize(filePath) {
1313
+ let stats;
1314
+ try {
1315
+ stats = await stat2(filePath);
1316
+ } catch (err) {
1317
+ throw toFsApiError(err, filePath);
1318
+ }
1319
+ if (stats.isDirectory()) {
1320
+ throw new ApiError("INPUT_INVALID_FORMAT", `Expected a file, got a directory: ${filePath}`, {
1321
+ retryable: false
1322
+ });
1323
+ }
1324
+ if (!stats.isFile()) {
1325
+ throw new ApiError("INPUT_INVALID_FORMAT", `Expected a regular file: ${filePath}`, {
1326
+ retryable: false
1327
+ });
1328
+ }
1329
+ return stats.size;
1330
+ }
1331
+ async function safeReadFile(filePath) {
1332
+ try {
1333
+ return await readFile2(filePath);
1334
+ } catch (err) {
1335
+ throw toFsApiError(err, filePath);
1336
+ }
1337
+ }
1338
+ function toFsApiError(err, filePath) {
1339
+ if (err instanceof Error && "code" in err) {
1340
+ const code = err.code;
1341
+ if (code === "ENOENT") {
1342
+ return new ApiError("INPUT_INVALID_FORMAT", `File not found: ${filePath}`, {
1343
+ retryable: false
1344
+ });
1345
+ }
1346
+ if (code === "EISDIR") {
1347
+ return new ApiError("INPUT_INVALID_FORMAT", `Expected a file, got a directory: ${filePath}`, {
1348
+ retryable: false
1349
+ });
1350
+ }
1351
+ if (code === "EACCES" || code === "EPERM") {
1352
+ return new ApiError("INPUT_INVALID_FORMAT", `Permission denied: ${filePath}`, {
1353
+ retryable: false
1354
+ });
1355
+ }
1356
+ }
1357
+ return err;
1358
+ }
1359
+ function validateSize(bytes) {
1360
+ if (bytes === 0) {
1361
+ throw new ApiError("INPUT_INVALID_FORMAT", "Refusing to upload an empty file.", {
1362
+ retryable: false
1363
+ });
1364
+ }
1365
+ if (bytes > MAX_UPLOAD_SIZE_BYTES) {
1366
+ throw new ApiError(
1367
+ "INPUT_INVALID_FORMAT",
1368
+ `File is ${bytes} bytes; the upload endpoint caps at ${MAX_UPLOAD_SIZE_BYTES} bytes.`,
1369
+ { retryable: false }
1370
+ );
1371
+ }
1372
+ }
1373
+ function sha256Hex(content) {
1374
+ return createHash2("sha256").update(content).digest("hex");
1375
+ }
1376
+ var MAGIC_NUMBERS = [
1377
+ { contentType: "image/png", prefix: [137, 80, 78, 71, 13, 10, 26, 10] },
1378
+ { contentType: "image/jpeg", prefix: [255, 216, 255] },
1379
+ { contentType: "image/webp", prefix: [82, 73, 70, 70] }
1380
+ // also matches WAV — disambiguated below
1381
+ ];
1382
+ var EXTENSION_FALLBACK = {
1383
+ png: "image/png",
1384
+ jpg: "image/jpeg",
1385
+ jpeg: "image/jpeg",
1386
+ gif: "image/gif",
1387
+ webp: "image/webp",
1388
+ mp4: "video/mp4",
1389
+ mov: "video/quicktime",
1390
+ mp3: "audio/mpeg",
1391
+ wav: "audio/wav",
1392
+ pdf: "application/pdf",
1393
+ txt: "text/plain",
1394
+ json: "application/json"
1395
+ };
1396
+ function detectContentType(filePath, content) {
1397
+ if (content.length >= 6) {
1398
+ const head = content.subarray(0, 6).toString("ascii");
1399
+ if (head === "GIF87a" || head === "GIF89a") return "image/gif";
1400
+ }
1401
+ if (content.length >= 12 && content.subarray(4, 8).toString("ascii") === "ftyp") {
1402
+ return "video/mp4";
1403
+ }
1404
+ for (const { contentType, prefix } of MAGIC_NUMBERS) {
1405
+ if (matchesPrefix(content, prefix)) {
1406
+ if (contentType === "image/webp") {
1407
+ const formatId = content.subarray(8, 12).toString("ascii");
1408
+ if (formatId === "WEBP") return "image/webp";
1409
+ if (formatId === "WAVE") return "audio/wav";
1410
+ break;
1411
+ }
1412
+ return contentType;
1413
+ }
1414
+ }
1415
+ const ext = filePath.toLowerCase().split(".").pop();
1416
+ if (ext && EXTENSION_FALLBACK[ext]) {
1417
+ return EXTENSION_FALLBACK[ext];
1418
+ }
1419
+ return "application/octet-stream";
1420
+ }
1421
+ function matchesPrefix(content, prefix) {
1422
+ if (content.length < prefix.length) return false;
1423
+ for (let i = 0; i < prefix.length; i++) {
1424
+ if (content[i] !== prefix[i]) return false;
1425
+ }
1426
+ return true;
1427
+ }
1428
+
1429
+ // src/commands/generate/internal.ts
1430
+ async function buildGenerateClient(opts) {
1431
+ const cfg = await loadConfig({
1432
+ flagProfile: opts.profile,
1433
+ flagApiBaseUrl: opts.apiBaseUrl,
1434
+ flagToken: opts.token
1435
+ });
1436
+ return new ApiClient({
1437
+ apiBaseUrl: cfg.apiBaseUrl,
1438
+ accessToken: cfg.accessToken,
1439
+ signal: opts.signal
1440
+ });
1441
+ }
1442
+ function isUrlLike(value) {
1443
+ return value.startsWith("http://") || value.startsWith("https://");
1444
+ }
1445
+
1446
+ // src/cli/long-poll.ts
1447
+ async function longPoll(opts) {
1448
+ const now = opts.now ?? Date.now;
1449
+ const started = now();
1450
+ for (; ; ) {
1451
+ if (opts.signal?.aborted) {
1452
+ throw new ApiError("TIMEOUT", "Wait was aborted.", { retryable: true });
1453
+ }
1454
+ const elapsedBefore = now() - started;
1455
+ if (elapsedBefore >= opts.timeoutMs) {
1456
+ throw new ApiError(
1457
+ "TIMEOUT",
1458
+ `Wait exceeded ${opts.timeoutMs}ms without reaching a terminal state.`,
1459
+ { retryable: true }
1460
+ );
1461
+ }
1462
+ const remainingMs = opts.timeoutMs - elapsedBefore;
1463
+ const iterationSignal = composeIterationSignal(opts.signal, remainingMs);
1464
+ let result;
1465
+ try {
1466
+ result = await opts.fetcher({ signal: iterationSignal });
1467
+ } catch (err) {
1468
+ if (opts.signal?.aborted || now() - started >= opts.timeoutMs) {
1469
+ throw new ApiError(
1470
+ "TIMEOUT",
1471
+ `Wait exceeded ${opts.timeoutMs}ms without reaching a terminal state.`,
1472
+ { retryable: true }
1473
+ );
1474
+ }
1475
+ throw err;
1476
+ }
1477
+ if (opts.signal?.aborted) {
1478
+ throw new ApiError("TIMEOUT", "Wait was aborted.", { retryable: true });
1479
+ }
1480
+ const elapsedAfter = now() - started;
1481
+ if (elapsedAfter >= opts.timeoutMs) {
1482
+ throw new ApiError(
1483
+ "TIMEOUT",
1484
+ `Wait exceeded ${opts.timeoutMs}ms without reaching a terminal state.`,
1485
+ { retryable: true }
1486
+ );
1487
+ }
1488
+ if (opts.isTerminal(result)) {
1489
+ return result;
1490
+ }
1491
+ }
1492
+ }
1493
+ function composeIterationSignal(userSignal, remainingMs) {
1494
+ const deadlineSignal = AbortSignal.timeout(Math.max(remainingMs, 0));
1495
+ return userSignal ? AbortSignal.any([userSignal, deadlineSignal]) : deadlineSignal;
1496
+ }
1497
+
1498
+ // src/commands/generate/get.ts
1499
+ async function generateGet(opts) {
1500
+ const client = opts.client ?? await buildGenerateClient(opts);
1501
+ return client.request({
1502
+ method: "GET",
1503
+ path: `/api/agents/v1/jobs/${encodeURIComponent(opts.jobId)}`,
1504
+ query: opts.waitSeconds && opts.waitSeconds > 0 ? { wait: opts.waitSeconds } : void 0,
1505
+ responseSchema: GenerationResultSchema,
1506
+ signal: opts.signal
1507
+ });
1508
+ }
1509
+
1510
+ // src/commands/generate/wait.ts
1511
+ var DEFAULT_WAIT_TIMEOUT_SECONDS = 120;
1512
+ var SERVER_LONG_POLL_SECONDS = 30;
1513
+ async function generateWait(opts) {
1514
+ const client = opts.client ?? await buildGenerateClient(opts);
1515
+ const timeoutSeconds = opts.timeoutSeconds ?? DEFAULT_WAIT_TIMEOUT_SECONDS;
1516
+ return longPoll({
1517
+ // Forward the iteration signal so the in-flight server hold is aborted
1518
+ // as soon as the overall deadline expires — otherwise `--timeout 1` would
1519
+ // still block for the full 30s server long-poll round.
1520
+ fetcher: (args) => generateGet({
1521
+ jobId: opts.jobId,
1522
+ waitSeconds: SERVER_LONG_POLL_SECONDS,
1523
+ client,
1524
+ signal: args?.signal
1525
+ }),
1526
+ isTerminal: (r) => isTerminalStatus(r.status),
1527
+ timeoutMs: timeoutSeconds * 1e3,
1528
+ signal: opts.signal
1529
+ });
1530
+ }
1531
+
1532
+ // src/commands/generate/create.ts
1533
+ var PREFLIGHT_PLACEHOLDER_URL = "https://placeholder.invalid/pending-upload";
1534
+ async function generateCreate(opts) {
1535
+ const client = opts.client ?? await buildGenerateClient(opts);
1536
+ const preflightRefs = (opts.referenceImages ?? []).map(
1537
+ (v) => isUrlLike(v) ? v : PREFLIGHT_PLACEHOLDER_URL
1538
+ );
1539
+ buildRequestBody({ ...opts, referenceImages: preflightRefs });
1540
+ const referenceImages = await resolveReferenceImages({
1541
+ items: opts.referenceImages ?? [],
1542
+ client,
1543
+ signal: opts.signal
1544
+ });
1545
+ const body = buildRequestBody({ ...opts, referenceImages });
1546
+ const created = await client.request({
1547
+ method: "POST",
1548
+ path: "/api/agents/v1/jobs",
1549
+ body,
1550
+ responseSchema: GenerateCreateResponseSchema,
1551
+ signal: opts.signal
1552
+ });
1553
+ const wait = opts.wait ?? true;
1554
+ if (!wait) {
1555
+ return projectCreateToResult(created, opts.modelSlug);
1556
+ }
1557
+ return generateWait({
1558
+ jobId: created.jobId,
1559
+ timeoutSeconds: opts.timeoutSeconds ?? DEFAULT_WAIT_TIMEOUT_SECONDS,
1560
+ client,
1561
+ signal: opts.signal
1562
+ });
1563
+ }
1564
+ async function resolveReferenceImages(args) {
1565
+ const out = [];
1566
+ for (const item of args.items) {
1567
+ if (isUrlLike(item)) {
1568
+ out.push(item);
1569
+ continue;
1570
+ }
1571
+ const uploaded = await uploadFile({
1572
+ filePath: item,
1573
+ scope: DEFAULT_UPLOAD_SCOPE,
1574
+ client: args.client,
1575
+ signal: args.signal
1576
+ });
1577
+ out.push(uploaded.publicUrl);
1578
+ }
1579
+ return out;
1580
+ }
1581
+ function buildRequestBody(args) {
1582
+ const input = {};
1583
+ if (args.prompt !== void 0) input.prompt = args.prompt;
1584
+ if (args.referenceImages.length === 1) {
1585
+ input.referenceImageUrl = args.referenceImages[0];
1586
+ } else if (args.referenceImages.length > 1) {
1587
+ input.referenceImages = args.referenceImages;
1588
+ }
1589
+ if (args.aspectRatio !== void 0) input.aspectRatio = args.aspectRatio;
1590
+ if (args.resolution !== void 0) input.resolution = args.resolution;
1591
+ if (args.durationSeconds !== void 0) input.durationSeconds = args.durationSeconds;
1592
+ if (args.brandModel !== void 0) input.providerParams = { brandModelId: args.brandModel };
1593
+ const body = {
1594
+ modelSlug: args.modelSlug,
1595
+ input
1596
+ };
1597
+ if (args.workflowId !== void 0 || args.nodeId !== void 0) {
1598
+ const options = {};
1599
+ if (args.workflowId !== void 0) options.workflowId = args.workflowId;
1600
+ if (args.nodeId !== void 0) options.nodeId = args.nodeId;
1601
+ body.options = options;
1602
+ }
1603
+ const parsed = GenerateCreateRequestSchema.safeParse(body);
1604
+ if (!parsed.success) {
1605
+ throw new ApiError(
1606
+ "INPUT_INVALID_FORMAT",
1607
+ `Invalid generate create request: ${parsed.error.message}`,
1608
+ { retryable: false }
1609
+ );
1610
+ }
1611
+ return parsed.data;
1612
+ }
1613
+ function projectCreateToResult(response, modelSlug) {
1614
+ return {
1615
+ jobId: response.jobId,
1616
+ status: response.status,
1617
+ modelSlug,
1618
+ resultUrl: null,
1619
+ thumbnailUrl: null,
1620
+ errorCode: null,
1621
+ errorMessage: null,
1622
+ creditsCharged: response.creditsCharged,
1623
+ creditsRefunded: false,
1624
+ startedAt: null,
1625
+ completedAt: null
1626
+ };
1627
+ }
1628
+
1629
+ // src/auth/device-flow.ts
1630
+ import { setTimeout as delay } from "timers/promises";
1631
+ import { request } from "undici";
1632
+ var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
1633
+ var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
1634
+ async function runDeviceFlow(opts) {
1635
+ throwIfCancelled(opts.signal);
1636
+ const authResp = await requestDeviceAuthorization(opts);
1637
+ const deadlineMs = Date.now() + authResp.expires_in * 1e3;
1638
+ opts.onUserCode?.({
1639
+ userCode: authResp.user_code,
1640
+ verificationUri: authResp.verification_uri,
1641
+ verificationUriComplete: authResp.verification_uri_complete ?? authResp.verification_uri,
1642
+ expiresAt: new Date(deadlineMs)
1643
+ });
1644
+ let intervalSeconds = authResp.interval;
1645
+ let iteration = 0;
1646
+ let consecutiveNetworkFailures = 0;
1647
+ const MAX_CONSECUTIVE_NETWORK_FAILURES = 5;
1648
+ while (true) {
1649
+ const sleepMs = Math.min(intervalSeconds * 1e3, deadlineMs - Date.now());
1650
+ if (sleepMs <= 0) {
1651
+ throw new AuthError(
1652
+ "AUTH_DEVICE_CODE_EXPIRED",
1653
+ `Device authorization expired after ${authResp.expires_in}s. Run \`clickraft login\` again.`
1654
+ );
1655
+ }
1656
+ await abortableSleep(sleepMs, opts.signal);
1657
+ throwIfCancelled(opts.signal);
1658
+ if (Date.now() >= deadlineMs) {
1659
+ throw new AuthError(
1660
+ "AUTH_DEVICE_CODE_EXPIRED",
1661
+ `Device authorization expired after ${authResp.expires_in}s. Run \`clickraft login\` again.`
1662
+ );
1663
+ }
1664
+ iteration += 1;
1665
+ const result = await pollToken({
1666
+ apiBaseUrl: opts.apiBaseUrl,
1667
+ clientId: opts.clientId,
1668
+ deviceCode: authResp.device_code,
1669
+ signal: opts.signal
1670
+ });
1671
+ if (result.kind === "success") {
1672
+ return result.token;
1673
+ }
1674
+ if (result.kind === "pending") {
1675
+ consecutiveNetworkFailures = 0;
1676
+ opts.onPoll?.({ iteration, intervalSeconds, status: "pending" });
1677
+ continue;
1678
+ }
1679
+ if (result.kind === "slow_down") {
1680
+ consecutiveNetworkFailures = 0;
1681
+ intervalSeconds *= 2;
1682
+ opts.onPoll?.({ iteration, intervalSeconds, status: "slow_down" });
1683
+ continue;
1684
+ }
1685
+ if (result.kind === "network_error") {
1686
+ consecutiveNetworkFailures += 1;
1687
+ if (consecutiveNetworkFailures >= MAX_CONSECUTIVE_NETWORK_FAILURES) {
1688
+ throw new ApiError(
1689
+ "NETWORK_ERROR",
1690
+ `Network repeatedly failed during polling (${MAX_CONSECUTIVE_NETWORK_FAILURES} consecutive errors).`,
1691
+ { retryable: true },
1692
+ result.cause
1693
+ );
1694
+ }
1695
+ intervalSeconds *= 2;
1696
+ opts.onPoll?.({ iteration, intervalSeconds, status: "slow_down" });
1697
+ continue;
1698
+ }
1699
+ throw result.error;
1700
+ }
1701
+ }
1702
+ async function requestDeviceAuthorization(opts) {
1703
+ const body = new URLSearchParams({ client_id: opts.clientId, scope: opts.scope }).toString();
1704
+ let resp;
1705
+ try {
1706
+ resp = await request(`${opts.apiBaseUrl}/oauth/device-authorization`, {
1707
+ method: "POST",
1708
+ headers: { "content-type": FORM_CONTENT_TYPE },
1709
+ body,
1710
+ signal: opts.signal
1711
+ });
1712
+ } catch (err) {
1713
+ if (isAbortError2(err) || opts.signal?.aborted) {
1714
+ throw new AuthError(
1715
+ "AUTH_FLOW_CANCELLED",
1716
+ "Device flow cancelled during authorization request.",
1717
+ err
1718
+ );
1719
+ }
1720
+ throw new ApiError(
1721
+ "NETWORK_ERROR",
1722
+ `Network error contacting authorization endpoint: ${err.message}`,
1723
+ { retryable: true },
1724
+ err
1725
+ );
1726
+ }
1727
+ const text = await resp.body.text();
1728
+ if (resp.statusCode !== 200) {
1729
+ const redacted = redactTokenLikeFields(truncate(text));
1730
+ throw new AuthError(
1731
+ "AUTH_TOKEN_INVALID",
1732
+ `Authorization endpoint returned HTTP ${resp.statusCode}: ${redacted}`,
1733
+ {
1734
+ status: resp.statusCode,
1735
+ redactedBody: redactTokenLikeFields(text),
1736
+ headers: safeHeaders(resp.headers)
1737
+ }
1738
+ );
1739
+ }
1740
+ const parsed = parseJson(text, "authorization");
1741
+ const validated = DeviceAuthorizationResponseSchema.safeParse(parsed);
1742
+ if (!validated.success) {
1743
+ throw new AuthError(
1744
+ "AUTH_TOKEN_INVALID",
1745
+ "Authorization endpoint response failed schema validation.",
1746
+ validated.error
1747
+ );
1748
+ }
1749
+ return validated.data;
1750
+ }
1751
+ var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
1752
+ "ECONNRESET",
1753
+ "ECONNREFUSED",
1754
+ "ETIMEDOUT",
1755
+ "EAI_AGAIN",
1756
+ "UND_ERR_SOCKET",
1757
+ "UND_ERR_CONNECT_TIMEOUT",
1758
+ "UND_ERR_HEADERS_TIMEOUT"
1759
+ ]);
1760
+ function isTransientNetworkError(err) {
1761
+ const code = err.code;
1762
+ return typeof code === "string" && TRANSIENT_NETWORK_CODES.has(code);
1763
+ }
1764
+ async function pollToken(args) {
1765
+ const body = new URLSearchParams({
1766
+ client_id: args.clientId,
1767
+ device_code: args.deviceCode,
1768
+ grant_type: GRANT_TYPE
1769
+ }).toString();
1770
+ let resp;
1771
+ try {
1772
+ resp = await request(`${args.apiBaseUrl}/oauth/token`, {
1773
+ method: "POST",
1774
+ headers: { "content-type": FORM_CONTENT_TYPE },
1775
+ body,
1776
+ signal: args.signal
1777
+ });
1778
+ } catch (err) {
1779
+ if (isAbortError2(err) || args.signal?.aborted) {
1780
+ return {
1781
+ kind: "error",
1782
+ error: new AuthError("AUTH_FLOW_CANCELLED", "Device flow cancelled mid-poll.", err)
1783
+ };
1784
+ }
1785
+ if (isTransientNetworkError(err)) {
1786
+ return { kind: "network_error", cause: err };
1787
+ }
1788
+ return {
1789
+ kind: "error",
1790
+ error: new ApiError(
1791
+ "NETWORK_ERROR",
1792
+ `Network error contacting token endpoint: ${err.message}`,
1793
+ { retryable: true },
1794
+ err
1795
+ )
1796
+ };
1797
+ }
1798
+ const text = await resp.body.text();
1799
+ if (resp.statusCode === 200) {
1800
+ return parseTokenSuccess(text);
1801
+ }
1802
+ if (resp.statusCode === 400) {
1803
+ return parseTokenError(text);
1804
+ }
1805
+ return {
1806
+ kind: "error",
1807
+ error: new AuthError(
1808
+ "AUTH_TOKEN_INVALID",
1809
+ `Token endpoint returned unexpected HTTP ${resp.statusCode}: ${redactTokenLikeFields(truncate(text))}`,
1810
+ {
1811
+ status: resp.statusCode,
1812
+ redactedBody: redactTokenLikeFields(text),
1813
+ headers: safeHeaders(resp.headers)
1814
+ }
1815
+ )
1816
+ };
1817
+ }
1818
+ function parseTokenSuccess(text) {
1819
+ let parsed;
1820
+ try {
1821
+ parsed = JSON.parse(text);
1822
+ } catch (err) {
1823
+ return {
1824
+ kind: "error",
1825
+ error: new AuthError("AUTH_TOKEN_INVALID", "Token endpoint returned invalid JSON.", err)
1826
+ };
1827
+ }
1828
+ const validated = TokenResponseSchema.safeParse(parsed);
1829
+ if (!validated.success) {
1830
+ return {
1831
+ kind: "error",
1832
+ error: new AuthError(
1833
+ "AUTH_TOKEN_INVALID",
1834
+ "Token endpoint success response failed schema validation.",
1835
+ validated.error
1836
+ )
1837
+ };
1838
+ }
1839
+ return { kind: "success", token: validated.data };
1840
+ }
1841
+ function parseTokenError(text) {
1842
+ let parsed;
1843
+ try {
1844
+ parsed = JSON.parse(text);
1845
+ } catch (err) {
1846
+ return {
1847
+ kind: "error",
1848
+ error: new AuthError("AUTH_TOKEN_INVALID", "Token error response was not valid JSON.", err)
1849
+ };
1850
+ }
1851
+ const validated = DeviceTokenPollErrorSchema.safeParse(parsed);
1852
+ if (!validated.success) {
1853
+ return {
1854
+ kind: "error",
1855
+ error: new AuthError(
1856
+ "AUTH_TOKEN_INVALID",
1857
+ "Token error response failed schema validation.",
1858
+ validated.error
1859
+ )
1860
+ };
1861
+ }
1862
+ const { error, error_description } = validated.data;
1863
+ switch (error) {
1864
+ case "authorization_pending":
1865
+ return { kind: "pending" };
1866
+ case "slow_down":
1867
+ return { kind: "slow_down" };
1868
+ case "access_denied":
1869
+ return {
1870
+ kind: "error",
1871
+ error: new AuthError(
1872
+ "AUTH_ACCESS_DENIED",
1873
+ error_description ?? "User denied the device authorization request."
1874
+ )
1875
+ };
1876
+ case "expired_token":
1877
+ return {
1878
+ kind: "error",
1879
+ error: new AuthError(
1880
+ "AUTH_DEVICE_CODE_EXPIRED",
1881
+ error_description ?? "Device code has expired. Run `clickraft login` again."
1882
+ )
1883
+ };
1884
+ default:
1885
+ return {
1886
+ kind: "error",
1887
+ error: new AuthError(
1888
+ "AUTH_TOKEN_INVALID",
1889
+ `Token endpoint returned error: ${error}${error_description ? ` (${error_description})` : ""}`
1890
+ )
1891
+ };
1892
+ }
1893
+ }
1894
+ async function abortableSleep(ms, signal) {
1895
+ try {
1896
+ await delay(ms, void 0, { signal });
1897
+ } catch (err) {
1898
+ if (isAbortError2(err)) {
1899
+ throw new AuthError("AUTH_FLOW_CANCELLED", "Device flow cancelled while waiting.", err);
1900
+ }
1901
+ throw err;
1902
+ }
1903
+ }
1904
+ function throwIfCancelled(signal) {
1905
+ if (signal?.aborted) {
1906
+ throw new AuthError("AUTH_FLOW_CANCELLED", "Device flow cancelled.");
1907
+ }
1908
+ }
1909
+ function isAbortError2(err) {
1910
+ if (err instanceof Error) {
1911
+ if (err.name === "AbortError") return true;
1912
+ if (err.code === "ABORT_ERR") return true;
1913
+ }
1914
+ return false;
1915
+ }
1916
+ function parseJson(text, label) {
1917
+ try {
1918
+ return JSON.parse(text);
1919
+ } catch (err) {
1920
+ throw new AuthError(
1921
+ "AUTH_TOKEN_INVALID",
1922
+ `${label} endpoint returned invalid JSON: ${redactTokenLikeFields(truncate(text))}`,
1923
+ err
1924
+ );
1925
+ }
1926
+ }
1927
+ function truncate(text, max = 200) {
1928
+ return text.length > max ? `${text.slice(0, max)}...` : text;
1929
+ }
1930
+ function safeHeaders(headers) {
1931
+ const out = {};
1932
+ for (const [k, v] of Object.entries(headers)) {
1933
+ if (typeof v === "string") out[k] = v;
1934
+ }
1935
+ return out;
1936
+ }
1937
+
1938
+ // src/commands/login.ts
1939
+ var PLACEHOLDER_UUID = "00000000-0000-0000-0000-000000000000";
1940
+ var PLACEHOLDER_ORG_SLUG = "unknown";
1941
+ var CLIENT_ID = "clickraft-cli";
1942
+ var DEFAULT_SCOPES = [
1943
+ "templates:read",
1944
+ "generations:write",
1945
+ "generations:read",
1946
+ "workflows:write",
1947
+ "workflows:read",
1948
+ "brand-models:read",
1949
+ "assets:write",
1950
+ "balance:read"
1951
+ ];
1952
+ async function login(options) {
1953
+ const config = await loadConfig({
1954
+ flagProfile: options.profile,
1955
+ flagApiBaseUrl: options.apiBaseUrl,
1956
+ credentialsPath: options.credentialsPath
1957
+ });
1958
+ const profileName = config.profileName || DEFAULT_PROFILE_NAME;
1959
+ const apiBaseUrl = options.apiBaseUrl ?? config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
1960
+ if (!options.forceReauth) {
1961
+ const existing = await readCredentials(options.credentialsPath);
1962
+ const profile2 = existing?.profiles[profileName];
1963
+ if (profile2 && !isExpired(profile2.expiresAt)) {
1964
+ throw new AuthError(
1965
+ "AUTH_TOKEN_INVALID",
1966
+ `Profile "${profileName}" is already logged in (token valid until ${profile2.expiresAt}). Pass --force-reauth to mint a new token.`
1967
+ );
1968
+ }
1969
+ }
1970
+ const scope = options.scope?.trim() || DEFAULT_SCOPES.join(" ");
1971
+ const flow = options.runDeviceFlow ?? runDeviceFlow;
1972
+ const token = await flow({
1973
+ apiBaseUrl,
1974
+ clientId: CLIENT_ID,
1975
+ scope,
1976
+ onUserCode: options.onUserCode,
1977
+ onPoll: options.onPoll,
1978
+ signal: options.signal
1979
+ });
1980
+ const profile = buildCredentialsProfile({ token, apiBaseUrl });
1981
+ await setProfile(profileName, profile, options.credentialsPath);
1982
+ return {
1983
+ profileName,
1984
+ tokenPrefix: profile.tokenPrefix,
1985
+ expiresAt: profile.expiresAt,
1986
+ scopes: profile.scopes,
1987
+ identityKnown: false,
1988
+ organizationId: profile.organizationId,
1989
+ organizationSlug: profile.organizationSlug,
1990
+ userId: profile.userId
1991
+ };
1992
+ }
1993
+ function buildCredentialsProfile(args) {
1994
+ const now = Date.now();
1995
+ const issuedAt = new Date(now).toISOString();
1996
+ const expiresAt = new Date(now + args.token.expires_in * 1e3).toISOString();
1997
+ const tokenPrefix = args.token.access_token.slice(0, 12);
1998
+ const scopes = args.token.scope.split(/\s+/).filter(Boolean);
1999
+ return {
2000
+ apiBaseUrl: args.apiBaseUrl,
2001
+ accessToken: args.token.access_token,
2002
+ tokenType: args.token.token_type,
2003
+ tokenPrefix,
2004
+ organizationId: PLACEHOLDER_UUID,
2005
+ organizationSlug: PLACEHOLDER_ORG_SLUG,
2006
+ userId: PLACEHOLDER_UUID,
2007
+ scopes,
2008
+ expiresAt,
2009
+ issuedAt,
2010
+ clientId: CLIENT_ID
2011
+ };
2012
+ }
2013
+ function isExpired(expiresAt) {
2014
+ const t = Date.parse(expiresAt);
2015
+ if (Number.isNaN(t)) return true;
2016
+ return t <= Date.now();
2017
+ }
2018
+
2019
+ // src/commands/logout.ts
2020
+ async function logout(options) {
2021
+ if (options.all) {
2022
+ await deleteCredentials(options.credentialsPath);
2023
+ return { profilesRemoved: ["*"], serverRevoked: false };
2024
+ }
2025
+ const config = await loadConfig({
2026
+ flagProfile: options.profile,
2027
+ credentialsPath: options.credentialsPath
2028
+ });
2029
+ const profileName = config.profileName || DEFAULT_PROFILE_NAME;
2030
+ const file = await readCredentials(options.credentialsPath);
2031
+ const profile = file?.profiles[profileName];
2032
+ if (!profile) {
2033
+ throw new CredentialsError(
2034
+ "CREDENTIALS_PROFILE_NOT_FOUND",
2035
+ `Profile "${profileName}" not found. Use \`clickraft logout --all\` to clear all credentials.`
2036
+ );
2037
+ }
2038
+ let serverRevoked = false;
2039
+ let revokeFailureMessage;
2040
+ if (options.revoke) {
2041
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({ apiBaseUrl, accessToken, signal: options.signal }));
2042
+ const client = factory(profile.apiBaseUrl, profile.accessToken);
2043
+ try {
2044
+ await client.request({
2045
+ method: "POST",
2046
+ path: "/oauth/revoke",
2047
+ body: { token: profile.accessToken }
2048
+ });
2049
+ serverRevoked = true;
2050
+ } catch (err) {
2051
+ revokeFailureMessage = formatRevokeFailure(err, profile.accessToken);
2052
+ const sink = options.warn ?? ((line) => process.stderr.write(`${line}
2053
+ `));
2054
+ sink(revokeFailureMessage);
2055
+ }
2056
+ }
2057
+ await removeProfile(profileName, options.credentialsPath);
2058
+ return {
2059
+ profilesRemoved: [profileName],
2060
+ serverRevoked,
2061
+ ...revokeFailureMessage ? { revokeFailureMessage } : {}
2062
+ };
2063
+ }
2064
+ function formatRevokeFailure(err, token) {
2065
+ const reason = redactTokenLikeFields(err instanceof Error ? err.message : String(err));
2066
+ return `warning: server-side revoke failed (${reason}). Local profile removed; token ${redactTokenLikeValue(token)} remains server-valid until expiry.`;
2067
+ }
2068
+
2069
+ // src/telemetry/detect.ts
2070
+ var AGENT_SIGNALS = [
2071
+ { envVar: "CLAUDE_CODE_SESSION_ID", agent: "claude-code" },
2072
+ { envVar: "CLAUDECODE", agent: "claude-code" },
2073
+ { envVar: "CURSOR_SESSION", agent: "cursor" },
2074
+ { envVar: "CURSOR_TRACE_ID", agent: "cursor" },
2075
+ { envVar: "OPENAI_CODEX_ENV", agent: "codex" },
2076
+ { envVar: "CODEX_CLI", agent: "codex" },
2077
+ { envVar: "CI", agent: "ci" }
2078
+ ];
2079
+ function detectAgents(env = process.env) {
2080
+ const detected = /* @__PURE__ */ new Set();
2081
+ const signals = {};
2082
+ for (const { envVar, agent } of AGENT_SIGNALS) {
2083
+ const value = env[envVar];
2084
+ if (value !== void 0 && value !== "" && value !== "0") {
2085
+ detected.add(agent);
2086
+ signals[envVar] = agent;
2087
+ }
2088
+ }
2089
+ return { detected: [...detected].sort(), signals };
2090
+ }
2091
+
2092
+ // src/commands/telemetry/inspect.ts
2093
+ function inspectTelemetry(options = {}) {
2094
+ const env = options.env ?? process.env;
2095
+ const optedOut = env[TELEMETRY_DISABLE_ENV_VAR] === "1" || env.DO_NOT_TRACK === "1";
2096
+ const agents = detectAgents(env);
2097
+ const crashSdkShipped = false;
2098
+ const productSdkShipped = false;
2099
+ return {
2100
+ crashReports: {
2101
+ enabled: false,
2102
+ // SDK not shipped → not enabled regardless of opt-out flags
2103
+ reason: optedOut ? "env-disabled" : "default",
2104
+ sdkShipped: crashSdkShipped
2105
+ },
2106
+ productAnalytics: {
2107
+ enabled: false,
2108
+ reason: "opt-in-pending",
2109
+ sdkShipped: productSdkShipped
2110
+ },
2111
+ agentDetection: {
2112
+ enabled: agents.detected.length > 0,
2113
+ signals: agents.signals,
2114
+ detected: agents.detected
2115
+ },
2116
+ envOverrides: {
2117
+ CLICKRAFT_DISABLE_TELEMETRY: env[TELEMETRY_DISABLE_ENV_VAR] === "1",
2118
+ DO_NOT_TRACK: env.DO_NOT_TRACK === "1"
2119
+ },
2120
+ payloadShape: {
2121
+ crashReports: {
2122
+ event: "unhandled_exception",
2123
+ error_type: "string",
2124
+ error_message: "string (token-redacted)",
2125
+ stack_redacted: "string",
2126
+ cli_version: "string",
2127
+ node_version: "string",
2128
+ platform: "string",
2129
+ agents_detected: "string[]"
2130
+ },
2131
+ productAnalytics: {
2132
+ event: "command_completed",
2133
+ command: 'string (e.g. "tokens.list")',
2134
+ exit_code: "number",
2135
+ duration_ms: "number",
2136
+ cli_version: "string",
2137
+ agents_detected: "string[]"
2138
+ }
2139
+ }
2140
+ };
2141
+ }
2142
+
2143
+ // src/commands/tokens/list.ts
2144
+ import { z as z6 } from "zod";
2145
+ var TokenRowSchema = z6.object({
2146
+ id: z6.string(),
2147
+ prefix: z6.string(),
2148
+ name: z6.string().nullable(),
2149
+ scopes: z6.array(z6.string()),
2150
+ last_used_at: z6.string().nullable(),
2151
+ expires_at: z6.string(),
2152
+ created_at: z6.string()
2153
+ }).strict();
2154
+ var TokensListResponseSchema = z6.object({
2155
+ items: z6.array(TokenRowSchema)
2156
+ }).strict();
2157
+ async function listTokens(options) {
2158
+ const config = await loadConfig({
2159
+ flagProfile: options.profile,
2160
+ credentialsPath: options.credentialsPath
2161
+ });
2162
+ if (!config.accessToken) {
2163
+ throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
2164
+ }
2165
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({ apiBaseUrl, accessToken, signal: options.signal }));
2166
+ const client = factory(config.apiBaseUrl, config.accessToken);
2167
+ const data = await client.request({
2168
+ method: "GET",
2169
+ path: "/tokens",
2170
+ responseSchema: TokensListResponseSchema,
2171
+ signal: options.signal
2172
+ });
2173
+ return data.items;
2174
+ }
2175
+
2176
+ // src/errors/format.ts
2177
+ var FALLBACK_HINT = "See `clickraft --help` or contact support.";
2178
+ function formatHumanError(err) {
2179
+ const view = errorToView(err);
2180
+ return [
2181
+ `Error: ${view.message}`,
2182
+ `Code: ${view.code}`,
2183
+ `Try: ${view.hint}`,
2184
+ `Request ID: ${view.requestId ?? "n/a"}`
2185
+ ].join("\n");
2186
+ }
2187
+ function formatJsonError(err, ctx) {
2188
+ return JSON.stringify(wrapError(err, ctx));
2189
+ }
2190
+ function errorToView(err) {
2191
+ if (err instanceof ApiError) {
2192
+ return {
2193
+ code: toEnvelopeCode(err.code),
2194
+ message: err.message,
2195
+ hint: EXIT_CODE_MAP[err.code]?.hint ?? FALLBACK_HINT,
2196
+ requestId: err.meta.requestId
2197
+ };
2198
+ }
2199
+ if (err instanceof AuthError || err instanceof CredentialsError) {
2200
+ const code = err.code;
2201
+ return {
2202
+ code: toEnvelopeCode(code),
2203
+ message: err.message,
2204
+ hint: EXIT_CODE_MAP[code]?.hint ?? FALLBACK_HINT
2205
+ };
2206
+ }
2207
+ return {
2208
+ code: toEnvelopeCode("INTERNAL_ERROR"),
2209
+ message: err instanceof Error ? err.message : String(err),
2210
+ hint: FALLBACK_HINT
2211
+ };
2212
+ }
2213
+
2214
+ // src/cli/args.ts
2215
+ var ArgError = class extends Error {
2216
+ constructor(message) {
2217
+ super(message);
2218
+ this.name = "ArgError";
2219
+ }
2220
+ };
2221
+ function parseArgs(argv, spec) {
2222
+ const result = {
2223
+ positional: [],
2224
+ string: {},
2225
+ boolean: {},
2226
+ array: {}
2227
+ };
2228
+ const stringFlags = new Set(spec.string ?? []);
2229
+ const booleanFlags = new Set(spec.boolean ?? []);
2230
+ const arrayFlags = new Set(spec.array ?? []);
2231
+ const aliases = spec.aliases ?? {};
2232
+ let i = 0;
2233
+ let stopFlags = false;
2234
+ while (i < argv.length) {
2235
+ const raw = argv[i];
2236
+ if (raw === void 0) {
2237
+ i += 1;
2238
+ continue;
2239
+ }
2240
+ if (stopFlags) {
2241
+ result.positional.push(raw);
2242
+ i += 1;
2243
+ continue;
2244
+ }
2245
+ if (raw === "--") {
2246
+ stopFlags = true;
2247
+ i += 1;
2248
+ continue;
2249
+ }
2250
+ if (!raw.startsWith("--")) {
2251
+ result.positional.push(raw);
2252
+ i += 1;
2253
+ continue;
2254
+ }
2255
+ const eqIdx = raw.indexOf("=");
2256
+ const flagToken = eqIdx >= 0 ? raw.slice(2, eqIdx) : raw.slice(2);
2257
+ const inlineValue = eqIdx >= 0 ? raw.slice(eqIdx + 1) : void 0;
2258
+ const canonical = resolveCanonical(flagToken, { booleanFlags, aliases });
2259
+ if (booleanFlags.has(canonical)) {
2260
+ if (inlineValue !== void 0) {
2261
+ result.boolean[canonical] = parseBool(inlineValue);
2262
+ } else if (flagToken.startsWith("no-") && booleanFlags.has(flagToken.slice(3))) {
2263
+ result.boolean[flagToken.slice(3)] = false;
2264
+ } else {
2265
+ result.boolean[canonical] = true;
2266
+ }
2267
+ i += 1;
2268
+ continue;
2269
+ }
2270
+ if (stringFlags.has(canonical) || arrayFlags.has(canonical)) {
2271
+ let value;
2272
+ if (inlineValue !== void 0) {
2273
+ value = inlineValue;
2274
+ } else {
2275
+ const next = argv[i + 1];
2276
+ if (next === void 0 || next.startsWith("--")) {
2277
+ throw new ArgError(`Flag --${canonical} requires a value.`);
2278
+ }
2279
+ value = next;
2280
+ i += 1;
2281
+ }
2282
+ if (arrayFlags.has(canonical)) {
2283
+ const existing = result.array[canonical] ?? [];
2284
+ existing.push(value);
2285
+ result.array[canonical] = existing;
2286
+ } else {
2287
+ result.string[canonical] = value;
2288
+ }
2289
+ i += 1;
2290
+ continue;
2291
+ }
2292
+ throw new ArgError(`Unknown flag --${flagToken}`);
2293
+ }
2294
+ return result;
2295
+ }
2296
+ function resolveCanonical(flagToken, ctx) {
2297
+ if (ctx.aliases[flagToken]) return ctx.aliases[flagToken];
2298
+ if (flagToken.startsWith("no-") && ctx.booleanFlags.has(flagToken.slice(3))) {
2299
+ return flagToken.slice(3);
2300
+ }
2301
+ return flagToken;
2302
+ }
2303
+ function parseBool(value) {
2304
+ if (value === "true" || value === "1") return true;
2305
+ if (value === "false" || value === "0") return false;
2306
+ throw new ArgError(`Expected boolean value (true/false), got "${value}".`);
2307
+ }
2308
+
2309
+ // src/cli/parse-args.ts
2310
+ var SHORT_ALIASES = {
2311
+ h: "help",
2312
+ v: "version"
2313
+ };
2314
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["profile", "api-base-url", "scope", "token"]);
2315
+ function parseArgs2(argv) {
2316
+ const flags = {};
2317
+ const positional = [];
2318
+ const command = [];
2319
+ let commandLocked = false;
2320
+ let i = 0;
2321
+ while (i < argv.length) {
2322
+ const raw = argv[i];
2323
+ if (raw === "--") {
2324
+ for (let j = i + 1; j < argv.length; j += 1) positional.push(argv[j]);
2325
+ break;
2326
+ }
2327
+ if (raw.startsWith("--")) {
2328
+ const eq = raw.indexOf("=");
2329
+ const name = eq === -1 ? raw.slice(2) : raw.slice(2, eq);
2330
+ const inline = eq === -1 ? void 0 : raw.slice(eq + 1);
2331
+ if (name.startsWith("no-")) {
2332
+ flags[name.slice(3)] = false;
2333
+ i += 1;
2334
+ commandLocked = true;
2335
+ continue;
2336
+ }
2337
+ if (VALUE_FLAGS.has(name)) {
2338
+ if (inline !== void 0) {
2339
+ flags[name] = inline;
2340
+ } else {
2341
+ const next = argv[i + 1];
2342
+ if (next === void 0 || next.startsWith("-")) {
2343
+ flags[name] = true;
2344
+ } else {
2345
+ flags[name] = next;
2346
+ i += 1;
2347
+ }
2348
+ }
2349
+ } else {
2350
+ flags[name] = inline ?? true;
2351
+ }
2352
+ i += 1;
2353
+ commandLocked = true;
2354
+ continue;
2355
+ }
2356
+ if (raw.startsWith("-") && raw.length > 1) {
2357
+ const short = raw.slice(1);
2358
+ const aliased = SHORT_ALIASES[short];
2359
+ if (aliased) {
2360
+ flags[aliased] = true;
2361
+ i += 1;
2362
+ commandLocked = true;
2363
+ continue;
2364
+ }
2365
+ positional.push(raw);
2366
+ i += 1;
2367
+ commandLocked = true;
2368
+ continue;
2369
+ }
2370
+ if (!commandLocked) {
2371
+ command.push(raw);
2372
+ } else {
2373
+ positional.push(raw);
2374
+ }
2375
+ i += 1;
2376
+ }
2377
+ return { command, flags, positional };
2378
+ }
2379
+
2380
+ // src/cli/progress.ts
2381
+ import { spawn } from "child_process";
2382
+ function renderUserCodePrompt(info, stream = process.stderr) {
2383
+ const lines = [
2384
+ "",
2385
+ "To complete login, visit:",
2386
+ ` ${info.verificationUriComplete}`,
2387
+ "",
2388
+ `And enter the code: ${info.userCode}`,
2389
+ `Code expires at: ${info.expiresAt.toISOString()}`,
2390
+ ""
2391
+ ];
2392
+ stream.write(lines.join("\n"));
2393
+ }
2394
+ function openBrowser(url) {
2395
+ if (!process.stderr.isTTY) return false;
2396
+ const command = pickBrowserCommand();
2397
+ if (!command) return false;
2398
+ try {
2399
+ const child = spawn(command.bin, [...command.args, url], {
2400
+ stdio: "ignore",
2401
+ detached: true
2402
+ });
2403
+ child.on("error", () => {
2404
+ });
2405
+ child.unref();
2406
+ return true;
2407
+ } catch {
2408
+ return false;
2409
+ }
2410
+ }
2411
+ function pickBrowserCommand() {
2412
+ if (process.platform === "darwin") return { bin: "open", args: [] };
2413
+ if (process.platform === "win32") return { bin: "cmd", args: ["/c", "start", ""] };
2414
+ if (process.platform === "linux") return { bin: "xdg-open", args: [] };
2415
+ return void 0;
2416
+ }
2417
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2418
+ var SPINNER_INTERVAL_MS = 80;
2419
+ function startPollingSpinner(label = "Waiting for authorization\u2026", stream = process.stderr) {
2420
+ if (!stream.isTTY) {
2421
+ return { stop() {
2422
+ } };
2423
+ }
2424
+ let frame = 0;
2425
+ const handle = setInterval(() => {
2426
+ stream.write(`\r${SPINNER_FRAMES[frame]} ${label}`);
2427
+ frame = (frame + 1) % SPINNER_FRAMES.length;
2428
+ }, SPINNER_INTERVAL_MS);
2429
+ return {
2430
+ stop() {
2431
+ clearInterval(handle);
2432
+ stream.write(`\r${" ".repeat(label.length + 4)}\r`);
2433
+ }
2434
+ };
2435
+ }
2436
+
2437
+ // src/cli/render.ts
2438
+ function renderLoginResult(result, out) {
2439
+ const expiresHuman = formatExpiresAt(result.expiresAt);
2440
+ const lines = [
2441
+ `\u2713 Logged in (profile: ${result.profileName}).`,
2442
+ ` Token prefix: ${result.tokenPrefix}`,
2443
+ ` Expires: ${expiresHuman}`,
2444
+ ` Scopes: ${result.scopes.join(" ")}`
2445
+ ];
2446
+ if (!result.identityKnown) {
2447
+ lines.push(
2448
+ ` Note: Identity not populated (Wave 1 server-response gap, see PR description).`
2449
+ );
2450
+ }
2451
+ out.write(lines.join("\n") + "\n");
2452
+ }
2453
+ function renderLogoutResult(result, out) {
2454
+ if (result.profilesRemoved[0] === "*") {
2455
+ out.write("\u2713 Removed all profiles.\n");
2456
+ return;
2457
+ }
2458
+ const name = result.profilesRemoved[0] ?? "(unknown)";
2459
+ if (result.serverRevoked) {
2460
+ out.write(`\u2713 Logged out of profile "${name}" (server-side token revoked).
2461
+ `);
2462
+ } else if (result.revokeFailureMessage) {
2463
+ out.write(`\u2713 Logged out of profile "${name}" locally; server-side revoke failed.
2464
+ `);
2465
+ } else {
2466
+ out.write(`\u2713 Logged out of profile "${name}" (local only).
2467
+ `);
2468
+ }
2469
+ }
2470
+ function renderTokensList(rows, out) {
2471
+ if (rows.length === 0) {
2472
+ out.write("No tokens.\n");
2473
+ return;
2474
+ }
2475
+ const headers = ["PREFIX", "NAME", "SCOPES", "LAST USED", "EXPIRES"];
2476
+ const data = rows.map((row) => [
2477
+ row.prefix,
2478
+ row.name ?? "\u2014",
2479
+ row.scopes.join(",") || "\u2014",
2480
+ row.last_used_at ?? "never",
2481
+ row.expires_at
2482
+ ]);
2483
+ const widths = headers.map(
2484
+ (header, i) => Math.max(header.length, ...data.map((cells) => cells[i].length))
2485
+ );
2486
+ const padRow = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
2487
+ out.write(padRow(headers) + "\n");
2488
+ for (const cells of data) out.write(padRow(cells) + "\n");
2489
+ }
2490
+ function renderTelemetryState(state, out) {
2491
+ const crash = state.crashReports;
2492
+ const product = state.productAnalytics;
2493
+ const agents = state.agentDetection;
2494
+ const lines = [
2495
+ "Telemetry state:",
2496
+ "",
2497
+ ` Crash reports: ${crash.enabled ? "ON" : "OFF"} (${crash.reason}${crash.sdkShipped ? "" : ", SDK not shipped"})`,
2498
+ ` Product analytics: ${product.enabled ? "ON" : "OFF"} (${product.reason}${product.sdkShipped ? "" : ", SDK not shipped"})`,
2499
+ ` Agent detection: ${agents.enabled ? "ON" : "OFF"}`,
2500
+ ` Detected agents: ${agents.detected.length === 0 ? "(none)" : agents.detected.join(", ")}`,
2501
+ "",
2502
+ "Env overrides:",
2503
+ ` CLICKRAFT_DISABLE_TELEMETRY=${state.envOverrides.CLICKRAFT_DISABLE_TELEMETRY ? "1" : "0"}`,
2504
+ ` DO_NOT_TRACK=${state.envOverrides.DO_NOT_TRACK ? "1" : "0"}`,
2505
+ "",
2506
+ "Payload shape (illustrative \u2014 no data is currently sent):",
2507
+ ` ${JSON.stringify(state.payloadShape, null, 2).split("\n").join("\n ")}`
2508
+ ];
2509
+ out.write(lines.join("\n") + "\n");
2510
+ }
2511
+ function formatExpiresAt(iso) {
2512
+ const ms = Date.parse(iso);
2513
+ if (Number.isNaN(ms)) return iso;
2514
+ const date = new Date(ms);
2515
+ const days = Math.round((ms - Date.now()) / (24 * 60 * 60 * 1e3));
2516
+ return `${date.toISOString()} (~${days}d)`;
2517
+ }
2518
+
2519
+ // src/cli/dispatch.ts
2520
+ async function dispatch(options) {
2521
+ const stdout = options.stdout ?? process.stdout;
2522
+ const stderr = options.stderr ?? process.stderr;
2523
+ const newCmd = matchNewCommand(options.argv);
2524
+ if (newCmd !== null) {
2525
+ return runNewCommand(newCmd, options, stdout, stderr);
2526
+ }
2527
+ const parsed = parseArgs2(options.argv);
2528
+ const jsonMode = parsed.flags.json === true;
2529
+ if (parsed.flags.version === true) {
2530
+ stdout.write(`${options.version}
2531
+ `);
2532
+ return 0;
2533
+ }
2534
+ if (parsed.command.length === 0 || parsed.flags.help === true) {
2535
+ if (parsed.command.length === 0) {
2536
+ stdout.write(renderRootHelp(options.version));
2537
+ } else {
2538
+ stdout.write(renderCommandHelp(parsed.command));
2539
+ }
2540
+ return 0;
2541
+ }
2542
+ const commandName = parsed.command.join(".");
2543
+ const ctx = {
2544
+ startMs: Date.now(),
2545
+ command: commandName,
2546
+ version: options.version
2547
+ };
2548
+ try {
2549
+ validateFlags(parsed);
2550
+ return await runCommand(parsed, ctx, {
2551
+ stdout,
2552
+ stderr,
2553
+ jsonMode,
2554
+ version: options.version,
2555
+ signal: options.signal,
2556
+ uiHooks: options.uiHooks
2557
+ });
2558
+ } catch (err) {
2559
+ if (isAbortedError(err, options.signal)) {
2560
+ if (jsonMode) {
2561
+ stdout.write(
2562
+ formatJsonError(new AuthError("AUTH_FLOW_CANCELLED", "Cancelled."), ctx) + "\n"
2563
+ );
2564
+ } else {
2565
+ stderr.write("Cancelled.\n");
2566
+ }
2567
+ return SIGINT_EXIT_CODE;
2568
+ }
2569
+ const code = extractCode(err);
2570
+ if (jsonMode) {
2571
+ stdout.write(formatJsonError(err, ctx) + "\n");
2572
+ } else {
2573
+ stderr.write(formatHumanError(err) + "\n");
2574
+ }
2575
+ return getExitCode(code);
2576
+ }
2577
+ }
2578
+ async function runCommand(parsed, ctx, rc) {
2579
+ const path = parsed.command.join(".");
2580
+ switch (path) {
2581
+ case "login": {
2582
+ const result = await loginCommand(parsed, rc);
2583
+ emit(result, renderLoginResult, ctx, rc);
2584
+ return 0;
2585
+ }
2586
+ case "logout": {
2587
+ const result = await logoutCommand(parsed, rc);
2588
+ emit(result, renderLogoutResult, ctx, rc);
2589
+ return 0;
2590
+ }
2591
+ case "tokens.list": {
2592
+ const rows = await listTokens({
2593
+ profile: stringFlag(parsed.flags.profile),
2594
+ signal: rc.signal
2595
+ });
2596
+ emit(rows, renderTokensList, ctx, rc);
2597
+ return 0;
2598
+ }
2599
+ case "telemetry.inspect": {
2600
+ const state = inspectTelemetry();
2601
+ emit(state, renderTelemetryState, ctx, rc);
2602
+ return 0;
2603
+ }
2604
+ default:
2605
+ throw new ApiError(
2606
+ "INPUT_INVALID_FORMAT",
2607
+ `Unknown command: ${parsed.command.join(" ")}. Run \`clickraft --help\` for available commands.`,
2608
+ { retryable: false }
2609
+ );
2610
+ }
2611
+ }
2612
+ async function loginCommand(parsed, rc) {
2613
+ const noOpen = parsed.flags["open-browser"] === false;
2614
+ let spinner;
2615
+ try {
2616
+ return await login({
2617
+ profile: stringFlag(parsed.flags.profile),
2618
+ apiBaseUrl: stringFlag(parsed.flags["api-base-url"]),
2619
+ scope: stringFlag(parsed.flags.scope),
2620
+ forceReauth: parsed.flags["force-reauth"] === true,
2621
+ openBrowser: !noOpen,
2622
+ signal: rc.signal,
2623
+ onUserCode: (info) => {
2624
+ if (rc.uiHooks?.onUserCode) {
2625
+ rc.uiHooks.onUserCode(info);
2626
+ return;
2627
+ }
2628
+ if (!rc.jsonMode) {
2629
+ renderUserCodePrompt(info, rc.stderr);
2630
+ if (!noOpen) openBrowser(info.verificationUriComplete);
2631
+ spinner = startPollingSpinner(void 0, rc.stderr);
2632
+ }
2633
+ },
2634
+ onPoll: () => {
2635
+ rc.uiHooks?.onPoll?.();
2636
+ }
2637
+ });
2638
+ } finally {
2639
+ spinner?.stop();
2640
+ }
2641
+ }
2642
+ async function logoutCommand(parsed, rc) {
2643
+ return logout({
2644
+ profile: stringFlag(parsed.flags.profile),
2645
+ all: parsed.flags.all === true,
2646
+ revoke: parsed.flags.revoke !== false,
2647
+ signal: rc.signal,
2648
+ warn: rc.jsonMode ? () => {
2649
+ } : void 0
2650
+ });
2651
+ }
2652
+ function emit(result, human, ctx, rc) {
2653
+ if (rc.signal?.aborted) {
2654
+ throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
2655
+ }
2656
+ if (rc.jsonMode) {
2657
+ rc.stdout.write(JSON.stringify(wrapSuccess(result, ctx)) + "\n");
2658
+ return;
2659
+ }
2660
+ human(result, rc.stdout);
2661
+ }
2662
+ function stringFlag(value) {
2663
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2664
+ }
2665
+ var COMMAND_FLAGS = {
2666
+ login: {
2667
+ profile: "string",
2668
+ "api-base-url": "string",
2669
+ scope: "string",
2670
+ "force-reauth": "boolean",
2671
+ "open-browser": "boolean",
2672
+ json: "boolean"
2673
+ },
2674
+ logout: { profile: "string", all: "boolean", revoke: "boolean", json: "boolean" },
2675
+ "tokens.list": { profile: "string", json: "boolean" },
2676
+ "telemetry.inspect": { json: "boolean" }
2677
+ };
2678
+ function validateFlags(parsed) {
2679
+ const allowed = COMMAND_FLAGS[parsed.command.join(".")];
2680
+ if (!allowed) return;
2681
+ for (const [name, value] of Object.entries(parsed.flags)) {
2682
+ const kind = allowed[name];
2683
+ if (!kind) {
2684
+ throw new ApiError("INPUT_INVALID_FORMAT", `Unknown flag: --${name}.`, { retryable: false });
2685
+ }
2686
+ if (kind === "string" && typeof value !== "string") {
2687
+ throw new ApiError("INPUT_INVALID_FORMAT", `Flag --${name} requires a value.`, {
2688
+ retryable: false
2689
+ });
2690
+ }
2691
+ }
2692
+ }
2693
+ function isAbortedError(err, signal) {
2694
+ if (signal?.aborted) return true;
2695
+ if (err instanceof AuthError && err.code === "AUTH_FLOW_CANCELLED") return true;
2696
+ return false;
2697
+ }
2698
+ function extractCode(err) {
2699
+ if (err && typeof err === "object" && "code" in err) {
2700
+ const code = err.code;
2701
+ if (typeof code === "string" && code in EXIT_CODE_MAP) {
2702
+ return code;
2703
+ }
2704
+ }
2705
+ return "INTERNAL_ERROR";
2706
+ }
2707
+ function renderRootHelp(version) {
2708
+ return [
2709
+ `clickraft v${version} \u2014 the Clickraft CLI`,
2710
+ "",
2711
+ "Usage: clickraft <command> [options]",
2712
+ "",
2713
+ "Commands:",
2714
+ " login Authenticate via OAuth device flow",
2715
+ " logout Remove credentials (optional server-side revoke)",
2716
+ " tokens list List the caller's API tokens",
2717
+ " telemetry inspect Show current telemetry state and payload shape",
2718
+ " generate create Submit a generation job and wait for completion",
2719
+ " generate wait Wait for an existing job to reach a terminal state",
2720
+ " generate get Fetch the current state of a job",
2721
+ " jobs wait Alias of `generate wait`",
2722
+ " upload <file> Upload a local file and return its public URL",
2723
+ "",
2724
+ "Options:",
2725
+ " -v, --version Show version",
2726
+ " -h, --help Show help",
2727
+ " --json Emit JSON envelope instead of human output",
2728
+ ""
2729
+ ].join("\n");
2730
+ }
2731
+ function renderCommandHelp(command) {
2732
+ const path = command.join(" ");
2733
+ switch (path) {
2734
+ case "login":
2735
+ return [
2736
+ "Usage: clickraft login [options]",
2737
+ "",
2738
+ "Options:",
2739
+ ' --profile <name> Write to a specific profile (default: "default")',
2740
+ " --api-base-url <url> Override the API base URL",
2741
+ " --scope <s> Space-separated scope list (default: all 8)",
2742
+ " --force-reauth Re-authenticate even if a valid token exists",
2743
+ " --no-open-browser Do not auto-open the verification URL",
2744
+ " --json Emit JSON envelope instead of human output",
2745
+ ""
2746
+ ].join("\n");
2747
+ case "logout":
2748
+ return [
2749
+ "Usage: clickraft logout [options]",
2750
+ "",
2751
+ "Options:",
2752
+ ' --profile <name> Remove a specific profile (default: "default")',
2753
+ " --all Remove every profile (delete the credentials file)",
2754
+ " --no-revoke Skip the server-side revoke call",
2755
+ " --json Emit JSON envelope instead of human output",
2756
+ ""
2757
+ ].join("\n");
2758
+ case "tokens list":
2759
+ return [
2760
+ "Usage: clickraft tokens list [options]",
2761
+ "",
2762
+ "Options:",
2763
+ " --profile <name> Read credentials from a specific profile",
2764
+ " --json Emit JSON envelope instead of human output",
2765
+ ""
2766
+ ].join("\n");
2767
+ case "telemetry inspect":
2768
+ return [
2769
+ "Usage: clickraft telemetry inspect [options]",
2770
+ "",
2771
+ "Options:",
2772
+ " --json Emit JSON envelope instead of human output",
2773
+ ""
2774
+ ].join("\n");
2775
+ case "generate":
2776
+ return [
2777
+ "Usage: clickraft generate <subcommand> [options]",
2778
+ "",
2779
+ "Subcommands:",
2780
+ " create Submit a generation job (waits for completion by default)",
2781
+ " wait Long-poll an existing job until terminal",
2782
+ " get Fetch the current state of a job (no polling)",
2783
+ ""
2784
+ ].join("\n");
2785
+ case "generate create":
2786
+ return [
2787
+ "Usage: clickraft generate create [prompt] --model-slug <slug> [options]",
2788
+ "",
2789
+ "Submits a generation job. By default the command waits up to 120s for the",
2790
+ "job to reach a terminal state via the server long-poll (?wait=30) rounds.",
2791
+ "",
2792
+ "Options:",
2793
+ " --prompt <text> Text prompt (or pass as the positional argument)",
2794
+ " --model-slug <slug> Required model identifier",
2795
+ " --brand-model <id> Optional Brand Model reference",
2796
+ " --reference-image <v> URL or local path (repeatable, max 8)",
2797
+ " --aspect-ratio <a:b> e.g. 16:9",
2798
+ " --resolution <WxH> e.g. 1024x768",
2799
+ " --duration-seconds <n> For video/audio models (1-60)",
2800
+ " --workflow-id <uuid> Attach to an existing workflow",
2801
+ " --node-id <id> Attach to a specific node",
2802
+ " --no-wait, --async Return immediately with the jobId",
2803
+ " --timeout <seconds> Override the 120s default wait",
2804
+ " --profile <name> Credentials profile name",
2805
+ " --api-base-url <url> Override the API base URL",
2806
+ " --token <token> Override the access token",
2807
+ " --json Emit a JSON envelope instead of human output",
2808
+ " -h, --help Show help",
2809
+ ""
2810
+ ].join("\n");
2811
+ case "generate wait":
2812
+ return [
2813
+ "Usage: clickraft generate wait <jobId> [options]",
2814
+ "",
2815
+ "Long-polls an existing job until it reaches a terminal state (completed,",
2816
+ "failed, or cancelled) or the timeout elapses.",
2817
+ "",
2818
+ "Options:",
2819
+ " --timeout <seconds> Overall wait deadline (default: 120)",
2820
+ " --profile <name>",
2821
+ " --api-base-url <url>",
2822
+ " --token <token>",
2823
+ " --json",
2824
+ ""
2825
+ ].join("\n");
2826
+ case "generate get":
2827
+ return [
2828
+ "Usage: clickraft generate get <jobId> [options]",
2829
+ "",
2830
+ "Fetches the current state of a generation job without polling.",
2831
+ "",
2832
+ "Options:",
2833
+ " --profile <name>",
2834
+ " --api-base-url <url>",
2835
+ " --token <token>",
2836
+ " --json",
2837
+ ""
2838
+ ].join("\n");
2839
+ case "jobs":
2840
+ return [
2841
+ "Usage: clickraft jobs <subcommand> [options]",
2842
+ "",
2843
+ "Subcommands:",
2844
+ " wait Long-poll a job until terminal (alias of `generate wait`)",
2845
+ ""
2846
+ ].join("\n");
2847
+ case "jobs wait":
2848
+ return [
2849
+ "Usage: clickraft jobs wait <jobId> [options]",
2850
+ "",
2851
+ "Alias of `clickraft generate wait`. See `clickraft generate wait --help`.",
2852
+ ""
2853
+ ].join("\n");
2854
+ case "upload":
2855
+ return [
2856
+ "Usage: clickraft upload <file-path> [options]",
2857
+ "",
2858
+ "Uploads a local file (up to 15MB) to the Agents API. The CLI computes a",
2859
+ "sha256 of the contents and uses it as the idempotency key, so uploading",
2860
+ "the same file twice within the same scope reuses the original asset.",
2861
+ "",
2862
+ "Options:",
2863
+ " --scope <name> Namespace for idempotency dedup (default: user-uploads)",
2864
+ " --profile <name>",
2865
+ " --api-base-url <url>",
2866
+ " --token <token>",
2867
+ " --json",
2868
+ ""
2869
+ ].join("\n");
2870
+ default:
2871
+ return `Unknown command: ${path}
2872
+ Run \`clickraft --help\` for available commands.
2873
+ `;
2874
+ }
2875
+ }
2876
+ function matchNewCommand(argv) {
2877
+ if (argv.length === 0) return null;
2878
+ const verb = argv[0];
2879
+ if (verb === "upload") {
2880
+ return { kind: "cmd", path: "upload", verbTokenCount: 1 };
2881
+ }
2882
+ if (verb === "generate") {
2883
+ const sub = argv[1];
2884
+ if (sub === "create") return { kind: "cmd", path: "generate.create", verbTokenCount: 2 };
2885
+ if (sub === "wait") return { kind: "cmd", path: "generate.wait", verbTokenCount: 2 };
2886
+ if (sub === "get") return { kind: "cmd", path: "generate.get", verbTokenCount: 2 };
2887
+ if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "generate" };
2888
+ return { kind: "unknown-sub", verb: "generate", sub };
2889
+ }
2890
+ if (verb === "jobs") {
2891
+ const sub = argv[1];
2892
+ if (sub === "wait") return { kind: "cmd", path: "jobs.wait", verbTokenCount: 2 };
2893
+ if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "jobs" };
2894
+ return { kind: "unknown-sub", verb: "jobs", sub };
2895
+ }
2896
+ return null;
2897
+ }
2898
+ async function runNewCommand(newCmd, options, stdout, stderr) {
2899
+ if (newCmd.kind === "sub-help") {
2900
+ stdout.write(renderCommandHelp([newCmd.verb]));
2901
+ return 0;
2902
+ }
2903
+ if (newCmd.kind === "unknown-sub") {
2904
+ const err = new ApiError(
2905
+ "INPUT_INVALID_FORMAT",
2906
+ `Unknown ${newCmd.verb} subcommand: ${newCmd.sub}. Run \`clickraft ${newCmd.verb} --help\` for available subcommands.`,
2907
+ { retryable: false }
2908
+ );
2909
+ const ctx2 = {
2910
+ startMs: Date.now(),
2911
+ command: newCmd.verb,
2912
+ version: options.version
2913
+ };
2914
+ if (options.argv.includes("--json")) {
2915
+ stdout.write(formatJsonError(err, ctx2) + "\n");
2916
+ } else {
2917
+ stderr.write(formatHumanError(err) + "\n");
2918
+ }
2919
+ return 2;
2920
+ }
2921
+ const path = newCmd.path;
2922
+ const argvAfterVerb = options.argv.slice(newCmd.verbTokenCount);
2923
+ const jsonMode = argvAfterVerb.includes("--json");
2924
+ if (argvAfterVerb.includes("--help") || argvAfterVerb.includes("-h")) {
2925
+ stdout.write(renderCommandHelp(path.split(".")));
2926
+ return 0;
2927
+ }
2928
+ const ctx = {
2929
+ startMs: Date.now(),
2930
+ command: path,
2931
+ version: options.version
2932
+ };
2933
+ const rc = { stdout, stderr, jsonMode, signal: options.signal };
2934
+ try {
2935
+ switch (path) {
2936
+ case "generate.create":
2937
+ return await runGenerateCreate(argvAfterVerb, ctx, rc);
2938
+ case "generate.wait":
2939
+ return await runGenerateWait(argvAfterVerb, ctx, rc);
2940
+ case "generate.get":
2941
+ return await runGenerateGet(argvAfterVerb, ctx, rc);
2942
+ case "jobs.wait":
2943
+ return await runJobsWait(argvAfterVerb, ctx, rc);
2944
+ case "upload":
2945
+ return await runUploadCmd(argvAfterVerb, ctx, rc);
2946
+ }
2947
+ } catch (err) {
2948
+ if (isAbortedError(err, options.signal)) {
2949
+ if (jsonMode) {
2950
+ stdout.write(
2951
+ formatJsonError(new AuthError("AUTH_FLOW_CANCELLED", "Cancelled."), ctx) + "\n"
2952
+ );
2953
+ } else {
2954
+ stderr.write("Cancelled.\n");
2955
+ }
2956
+ return SIGINT_EXIT_CODE;
2957
+ }
2958
+ const mapped = err instanceof ArgError ? new ApiError("INPUT_INVALID_FORMAT", err.message, { retryable: false }) : err;
2959
+ const code = extractCode(mapped);
2960
+ if (jsonMode) {
2961
+ stdout.write(formatJsonError(mapped, ctx) + "\n");
2962
+ } else {
2963
+ stderr.write(formatHumanError(mapped) + "\n");
2964
+ }
2965
+ return getExitCode(code);
2966
+ }
2967
+ }
2968
+ var COMMON_RICH_STRING_FLAGS = ["profile", "api-base-url", "token"];
2969
+ var COMMON_RICH_BOOL_FLAGS = ["json", "help"];
2970
+ async function runGenerateCreate(argv, ctx, rc) {
2971
+ const parsed = parseArgs(argv, {
2972
+ string: [
2973
+ ...COMMON_RICH_STRING_FLAGS,
2974
+ "prompt",
2975
+ "model-slug",
2976
+ "brand-model",
2977
+ "aspect-ratio",
2978
+ "resolution",
2979
+ "duration-seconds",
2980
+ "workflow-id",
2981
+ "node-id",
2982
+ "timeout"
2983
+ ],
2984
+ boolean: [...COMMON_RICH_BOOL_FLAGS, "wait", "async"],
2985
+ array: ["reference-image"]
2986
+ });
2987
+ const modelSlug = parsed.string["model-slug"];
2988
+ if (!modelSlug) throw new ArgError("--model-slug is required.");
2989
+ const result = await generateCreate({
2990
+ prompt: parsed.string.prompt ?? parsed.positional[0],
2991
+ modelSlug,
2992
+ referenceImages: parsed.array["reference-image"],
2993
+ aspectRatio: parsed.string["aspect-ratio"],
2994
+ resolution: parsed.string.resolution,
2995
+ durationSeconds: optionalInt(parsed.string["duration-seconds"], "duration-seconds"),
2996
+ brandModel: parsed.string["brand-model"],
2997
+ workflowId: parsed.string["workflow-id"],
2998
+ nodeId: parsed.string["node-id"],
2999
+ wait: computeWaitFlag(parsed),
3000
+ timeoutSeconds: optionalInt(parsed.string.timeout, "timeout"),
3001
+ profile: parsed.string.profile,
3002
+ apiBaseUrl: parsed.string["api-base-url"],
3003
+ token: parsed.string.token,
3004
+ signal: rc.signal
3005
+ });
3006
+ emitJob(result, ctx, rc);
3007
+ return exitCodeForResult(result);
3008
+ }
3009
+ async function runGenerateWait(argv, ctx, rc) {
3010
+ const parsed = parseArgs(argv, {
3011
+ string: [...COMMON_RICH_STRING_FLAGS, "timeout"],
3012
+ boolean: [...COMMON_RICH_BOOL_FLAGS]
3013
+ });
3014
+ const jobId = parsed.positional[0];
3015
+ if (!jobId) throw new ArgError("Missing required <jobId> argument.");
3016
+ const result = await generateWait({
3017
+ jobId,
3018
+ timeoutSeconds: optionalInt(parsed.string.timeout, "timeout"),
3019
+ profile: parsed.string.profile,
3020
+ apiBaseUrl: parsed.string["api-base-url"],
3021
+ token: parsed.string.token,
3022
+ signal: rc.signal
3023
+ });
3024
+ emitJob(result, ctx, rc);
3025
+ return exitCodeForResult(result);
3026
+ }
3027
+ async function runGenerateGet(argv, ctx, rc) {
3028
+ const parsed = parseArgs(argv, {
3029
+ string: [...COMMON_RICH_STRING_FLAGS],
3030
+ boolean: [...COMMON_RICH_BOOL_FLAGS]
3031
+ });
3032
+ const jobId = parsed.positional[0];
3033
+ if (!jobId) throw new ArgError("Missing required <jobId> argument.");
3034
+ const result = await generateGet({
3035
+ jobId,
3036
+ profile: parsed.string.profile,
3037
+ apiBaseUrl: parsed.string["api-base-url"],
3038
+ token: parsed.string.token,
3039
+ signal: rc.signal
3040
+ });
3041
+ emitJob(result, ctx, rc);
3042
+ return exitCodeForResult(result);
3043
+ }
3044
+ async function runJobsWait(argv, ctx, rc) {
3045
+ const parsed = parseArgs(argv, {
3046
+ string: [...COMMON_RICH_STRING_FLAGS, "timeout"],
3047
+ boolean: [...COMMON_RICH_BOOL_FLAGS]
3048
+ });
3049
+ const jobId = parsed.positional[0];
3050
+ if (!jobId) throw new ArgError("Missing required <jobId> argument.");
3051
+ const result = await generateWait({
3052
+ jobId,
3053
+ timeoutSeconds: optionalInt(parsed.string.timeout, "timeout"),
3054
+ profile: parsed.string.profile,
3055
+ apiBaseUrl: parsed.string["api-base-url"],
3056
+ token: parsed.string.token,
3057
+ signal: rc.signal
3058
+ });
3059
+ emitJob(result, ctx, rc);
3060
+ return exitCodeForResult(result);
3061
+ }
3062
+ async function runUploadCmd(argv, ctx, rc) {
3063
+ const parsed = parseArgs(argv, {
3064
+ string: [...COMMON_RICH_STRING_FLAGS, "scope"],
3065
+ boolean: [...COMMON_RICH_BOOL_FLAGS]
3066
+ });
3067
+ const filePath = parsed.positional[0];
3068
+ if (!filePath) throw new ArgError("Missing required <file-path> argument.");
3069
+ const result = await uploadFile({
3070
+ filePath,
3071
+ scope: parsed.string.scope,
3072
+ profile: parsed.string.profile,
3073
+ apiBaseUrl: parsed.string["api-base-url"],
3074
+ token: parsed.string.token,
3075
+ signal: rc.signal
3076
+ });
3077
+ emitUpload(result, ctx, rc);
3078
+ return 0;
3079
+ }
3080
+ function emitJob(result, ctx, rc) {
3081
+ if (rc.signal?.aborted) {
3082
+ throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
3083
+ }
3084
+ if (rc.jsonMode) {
3085
+ rc.stdout.write(JSON.stringify(wrapSuccess(result, ctx)) + "\n");
3086
+ return;
3087
+ }
3088
+ rc.stdout.write(renderGenerationResult(result) + "\n");
3089
+ }
3090
+ function emitUpload(result, ctx, rc) {
3091
+ if (rc.signal?.aborted) {
3092
+ throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
3093
+ }
3094
+ if (rc.jsonMode) {
3095
+ rc.stdout.write(JSON.stringify(wrapSuccess(result, ctx)) + "\n");
3096
+ return;
3097
+ }
3098
+ rc.stdout.write(renderUploadResult(result) + "\n");
3099
+ }
3100
+ function renderGenerationResult(r) {
3101
+ const lines = [`Job ID: ${r.jobId}`, `Status: ${r.status}`, `Model: ${r.modelSlug}`];
3102
+ if (r.resultUrl) lines.push(`Result URL: ${r.resultUrl}`);
3103
+ if (r.thumbnailUrl) lines.push(`Thumbnail: ${r.thumbnailUrl}`);
3104
+ lines.push(`Credits charged: ${r.creditsCharged}`);
3105
+ if (r.creditsRefunded) lines.push("Credits refunded: yes");
3106
+ if (r.errorCode || r.errorMessage) {
3107
+ lines.push(`Error: ${r.errorCode ?? "unknown"}${r.errorMessage ? ` \u2014 ${r.errorMessage}` : ""}`);
3108
+ }
3109
+ return lines.join("\n");
3110
+ }
3111
+ function renderUploadResult(r) {
3112
+ return [
3113
+ `Uploaded: ${r.publicUrl}`,
3114
+ `Asset ID: ${r.assetId}`,
3115
+ `Bytes: ${r.bytes}`,
3116
+ `Deduplicated: ${r.deduplicated}`
3117
+ ].join("\n");
3118
+ }
3119
+ function computeWaitFlag(parsed) {
3120
+ if (typeof parsed.boolean.wait === "boolean") return parsed.boolean.wait;
3121
+ if (parsed.boolean.async === true) return false;
3122
+ return true;
3123
+ }
3124
+ function optionalInt(value, label) {
3125
+ if (value === void 0) return void 0;
3126
+ const n = Number.parseInt(value, 10);
3127
+ if (!Number.isFinite(n) || n < 0 || String(n) !== value) {
3128
+ throw new ArgError(`--${label} must be a non-negative integer.`);
3129
+ }
3130
+ return n;
3131
+ }
3132
+ function exitCodeForResult(result) {
3133
+ if (result.status === "completed") return 0;
3134
+ if (result.status === "failed" || result.status === "cancelled") {
3135
+ if (result.errorCode) return getExitCode(result.errorCode);
3136
+ return 1;
3137
+ }
3138
+ return 0;
3139
+ }
3140
+
3141
+ // src/index.ts
3142
+ var __filename2 = fileURLToPath(import.meta.url);
3143
+ var __dirname2 = dirname2(__filename2);
3144
+ var pkg = JSON.parse(readFileSync(join2(__dirname2, "..", "package.json"), "utf8"));
3145
+ async function run(args, signal) {
3146
+ return dispatch({
3147
+ argv: args,
3148
+ version: pkg.version,
3149
+ signal
3150
+ });
3151
+ }
3152
+
3153
+ // src/cli.ts
3154
+ var controller = new AbortController();
3155
+ var sigintCount = 0;
3156
+ var onSigint = () => {
3157
+ sigintCount += 1;
3158
+ controller.abort();
3159
+ if (sigintCount >= 2) process.exit(130);
3160
+ };
3161
+ process.on("SIGINT", onSigint);
3162
+ process.on("SIGTERM", onSigint);
3163
+ run(process.argv.slice(2), controller.signal).then((exitCode) => {
3164
+ process.exit(exitCode);
3165
+ }).catch((error) => {
3166
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
3167
+ `);
3168
+ process.exit(1);
3169
+ });
3170
+ //# sourceMappingURL=cli.js.map