@brew.new/cli 0.9.0 → 0.10.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/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from 'readline/promises';
3
+ import { BrewApiError, BrewConnectionError, BrewTimeoutError, createBrewClient, GENERATE_IMAGE_DEFAULT_TIMEOUT_MS, GIF_DEFAULT_TIMEOUT_MS, AUDIT_EMAIL_DEFAULT_TIMEOUT_MS, EDIT_EMAIL_DEFAULT_TIMEOUT_MS, GENERATE_EMAIL_DEFAULT_TIMEOUT_MS, IMPORT_EMAIL_DEFAULT_TIMEOUT_MS, IMPORT_FIGMA_DEFAULT_TIMEOUT_MS, PREVIEW_EMAIL_CLIENTS_DEFAULT_TIMEOUT_MS } from '@brew.new/sdk';
3
4
  import { CommanderError, Command } from 'commander';
4
- import { BrewApiError, createBrewClient, AUDIT_EMAIL_DEFAULT_TIMEOUT_MS } from '@brew.new/sdk';
5
5
  import { readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
6
6
  import { homedir } from 'os';
7
7
  import { join } from 'path';
@@ -12,6 +12,35 @@ var EXIT_RUNTIME = 1;
12
12
  var EXIT_USAGE = 2;
13
13
  var EXIT_AUTH = 3;
14
14
  var EXIT_CONFIRM = 4;
15
+ var EXIT_INTERRUPTED = 130;
16
+ var EXIT_TERMINATED = 143;
17
+ function exitCodeForSignal(signal) {
18
+ return signal === "SIGINT" ? EXIT_INTERRUPTED : EXIT_TERMINATED;
19
+ }
20
+ var CliInterruptError = class extends Error {
21
+ signal;
22
+ constructor(signal) {
23
+ super(signal === "SIGINT" ? "Interrupted." : "Terminated.");
24
+ this.name = "CliInterruptError";
25
+ this.signal = signal;
26
+ }
27
+ };
28
+ var CliTimeoutError = class extends Error {
29
+ timeoutMs;
30
+ constructor(timeoutMs) {
31
+ super(`No complete response within ${timeoutMs}ms.`);
32
+ this.name = "TimeoutError";
33
+ this.timeoutMs = timeoutMs;
34
+ }
35
+ };
36
+ var CliConnectionError = class extends Error {
37
+ constructor(cause) {
38
+ super("The connection failed before a complete response arrived.", {
39
+ cause
40
+ });
41
+ this.name = "CliConnectionError";
42
+ }
43
+ };
15
44
  var CliUsageError = class extends Error {
16
45
  };
17
46
  var CliAuthError = class extends Error {
@@ -88,6 +117,9 @@ var ConfirmationRequiredError = class extends Error {
88
117
  }
89
118
  };
90
119
  function toExitCode(error) {
120
+ if (error instanceof CliInterruptError) {
121
+ return exitCodeForSignal(error.signal);
122
+ }
91
123
  if (error instanceof ConfirmationRequiredError) {
92
124
  return EXIT_CONFIRM;
93
125
  }
@@ -103,7 +135,40 @@ function toExitCode(error) {
103
135
  }
104
136
  return EXIT_RUNTIME;
105
137
  }
106
- function toErrorEnvelope(error) {
138
+ function toErrorEnvelope(error, context = {}) {
139
+ const envelope = baseEnvelope(error, context);
140
+ const drain = context.drain;
141
+ return withReplayAdvice(
142
+ drain === void 0 ? envelope : { ...envelope, progress: drain },
143
+ error,
144
+ context
145
+ );
146
+ }
147
+ function baseEnvelope(error, context) {
148
+ if (error instanceof CliInterruptError) {
149
+ return {
150
+ code: "CLI_INTERRUPTED",
151
+ type: "cancelled",
152
+ message: `${error.signal === "SIGINT" ? "Interrupted" : "Terminated"} (${error.signal}) ${context.request === void 0 ? "before any request was sent." : `while waiting for ${context.request.method} ${pathOnly(context.request.path)}.`}`
153
+ };
154
+ }
155
+ if (isTimeoutError(error)) {
156
+ return {
157
+ code: "CLI_TIMEOUT",
158
+ type: "service_unavailable",
159
+ message: timeoutMessage(error, context),
160
+ suggestion: "Retry the request. Reuse the same Idempotency-Key for a POST request."
161
+ };
162
+ }
163
+ if (error instanceof BrewConnectionError || error instanceof CliConnectionError) {
164
+ const cause = error.cause instanceof Error && error.cause.message !== "" ? `: ${error.cause.message}` : "";
165
+ return {
166
+ code: "CLI_CONNECTION",
167
+ type: "service_unavailable",
168
+ message: error instanceof BrewConnectionError ? error.message : `${error.message.replace(/\.$/, "")}${context.request === void 0 ? "" : ` (${context.request.method} ${pathOnly(context.request.path)})`}${cause}.`,
169
+ suggestion: "Check the connection and retry the request."
170
+ };
171
+ }
107
172
  if (error instanceof BrewApiError || error instanceof CliApiError) {
108
173
  const details = readErrorDetails(error);
109
174
  return {
@@ -133,14 +198,6 @@ function toErrorEnvelope(error) {
133
198
  message: error.message
134
199
  };
135
200
  }
136
- if (isTimeoutError(error)) {
137
- return {
138
- code: "CLI_TIMEOUT",
139
- type: "service_unavailable",
140
- message: "The request timed out before the API responded.",
141
- suggestion: "Retry the request. Reuse the same Idempotency-Key for a POST request."
142
- };
143
- }
144
201
  if (error instanceof CommandAbortedError) {
145
202
  return {
146
203
  code: "CLI_ABORTED",
@@ -155,7 +212,60 @@ function toErrorEnvelope(error) {
155
212
  }
156
213
  return { code: "CLI_UNEXPECTED", type: "internal_error", message };
157
214
  }
158
- function printError(ctx, error) {
215
+ var IN_PROGRESS_CODE = "IDEMPOTENCY_IN_PROGRESS";
216
+ function withReplayAdvice(envelope, error, context) {
217
+ if (!isUnknownOutcome(envelope)) {
218
+ return envelope;
219
+ }
220
+ const isInProgress = envelope.code === IN_PROGRESS_CODE || error instanceof BrewTimeoutError && error.inProgress || error instanceof BrewConnectionError && error.inProgress;
221
+ const holdNote = isInProgress ? " The first attempt is still running on the server, which holds its key for up to 15 minutes: until then a replay answers 409 IDEMPOTENCY_IN_PROGRESS, so wait and re-run." : "";
222
+ if (context.request === void 0) {
223
+ return envelope.code === "CLI_INTERRUPTED" ? { ...envelope, suggestion: "Nothing was sent." } : envelope;
224
+ }
225
+ if (context.isRead === true || context.request.method === "GET") {
226
+ return envelope.code === "CLI_INTERRUPTED" ? { ...envelope, suggestion: "It only reads; nothing was changed." } : envelope;
227
+ }
228
+ const key = errorIdempotencyKey(error) ?? context.request.idempotencyKey;
229
+ if (context.replays !== true || key === void 0) {
230
+ return {
231
+ ...envelope,
232
+ suggestion: `${pathOnly(context.request.path)} may already have been changed, and this route does not replay a request: check its current state before running it again.`
233
+ };
234
+ }
235
+ const retryCommand = context.retryCommand?.(key);
236
+ return {
237
+ ...envelope,
238
+ suggestion: `The API keeps working after the CLI disconnects, so this may still complete. Re-run with --idempotency-key ${key} to get its result instead of running it twice (the key replays for 24 hours${context.readsStdin === true ? "; pipe the same input to it again" : ""}).${holdNote}`,
239
+ idempotencyKey: key,
240
+ ...retryCommand === void 0 ? {} : { retryCommand }
241
+ };
242
+ }
243
+ function isUnknownOutcome(envelope) {
244
+ if (envelope.code === "CLI_INTERRUPTED" || envelope.code === "CLI_TIMEOUT" || envelope.code === "CLI_CONNECTION" || envelope.code === IN_PROGRESS_CODE) {
245
+ return true;
246
+ }
247
+ return envelope.status !== void 0 && envelope.status >= 500;
248
+ }
249
+ function errorIdempotencyKey(error) {
250
+ if (error instanceof BrewApiError || error instanceof BrewTimeoutError || error instanceof BrewConnectionError) {
251
+ return error.idempotencyKey;
252
+ }
253
+ return void 0;
254
+ }
255
+ function timeoutMessage(error, context) {
256
+ if (error instanceof BrewTimeoutError) {
257
+ return error.message;
258
+ }
259
+ if (error instanceof CliTimeoutError) {
260
+ const request = context.request;
261
+ return request === void 0 ? error.message : `${error.message.replace(/\.$/, "")} (${request.method} ${pathOnly(request.path)}).`;
262
+ }
263
+ return "The request timed out before the API responded.";
264
+ }
265
+ function pathOnly(path) {
266
+ return path.split("?")[0] ?? path;
267
+ }
268
+ function printError(ctx, error, context = {}) {
159
269
  if (error instanceof ConfirmationRequiredError) {
160
270
  ctx.io.stdout.write(`${JSON.stringify(error.envelope)}
161
271
  `);
@@ -168,7 +278,7 @@ function printError(ctx, error) {
168
278
  }
169
279
  return;
170
280
  }
171
- const envelope = toErrorEnvelope(error);
281
+ const envelope = toErrorEnvelope(error, context);
172
282
  if (ctx.mode === "json") {
173
283
  ctx.io.stderr.write(`${JSON.stringify({ error: envelope })}
174
284
  `);
@@ -181,6 +291,15 @@ function printError(ctx, error) {
181
291
  if (envelope.suggestion) {
182
292
  lines.push(envelope.suggestion);
183
293
  }
294
+ if (envelope.retryCommand) {
295
+ lines.push(`Re-run: ${envelope.retryCommand}`);
296
+ }
297
+ if (envelope.progress) {
298
+ const { rowsFetched, pagesFetched, resumeCursor } = envelope.progress;
299
+ lines.push(
300
+ `Fetched ${rowsFetched} rows in ${pagesFetched} pages before stopping${resumeCursor === void 0 ? "." : `; resume with --cursor ${resumeCursor} --all.`}`
301
+ );
302
+ }
184
303
  if (envelope.docs) {
185
304
  lines.push(`Docs: ${envelope.docs}`);
186
305
  }
@@ -219,7 +338,7 @@ function apiErrorStatus(error) {
219
338
  }
220
339
  }
221
340
  function isTimeoutError(error) {
222
- return typeof error === "object" && error !== null && "name" in error && error.name === "TimeoutError";
341
+ return error instanceof BrewTimeoutError || typeof error === "object" && error !== null && "name" in error && error.name === "TimeoutError";
223
342
  }
224
343
 
225
344
  // src/lib/confirm.ts
@@ -255,22 +374,58 @@ Proceed? [y/N] `);
255
374
  });
256
375
  }
257
376
  function buildConfirmCommand(rawArgv) {
377
+ return ["brew-cli", ...rerunTokens(rawArgv), "--yes"].join(" ");
378
+ }
379
+ function buildRetryCommand(rawArgv, idempotencyKey) {
380
+ return [
381
+ "brew-cli",
382
+ ...rerunTokens(rawArgv, { dropIdempotencyKey: true }),
383
+ "--idempotency-key",
384
+ shellQuote(idempotencyKey)
385
+ ].join(" ");
386
+ }
387
+ function rerunTokens(rawArgv, options = {}) {
258
388
  const tokens = [];
259
389
  for (let index = 0; index < rawArgv.length; index += 1) {
260
390
  const token = rawArgv[index];
261
391
  if (token === void 0) {
262
392
  continue;
263
393
  }
264
- if (token === "--api-key") {
394
+ if (token === "--api-key" || options.dropIdempotencyKey === true && token === "--idempotency-key") {
265
395
  index += 1;
266
396
  continue;
267
397
  }
268
- if (token.startsWith("--api-key=")) {
398
+ if (token.startsWith("--api-key=") || options.dropIdempotencyKey === true && token.startsWith("--idempotency-key=")) {
399
+ continue;
400
+ }
401
+ if (token.startsWith("--header=")) {
402
+ if (!isCredentialHeader(token.slice("--header=".length))) {
403
+ tokens.push(shellQuote(token));
404
+ }
405
+ continue;
406
+ }
407
+ if (token === "--header") {
408
+ const values = [];
409
+ while (index + 1 < rawArgv.length && !(rawArgv[index + 1] ?? "").startsWith("-")) {
410
+ index += 1;
411
+ const value = rawArgv[index] ?? "";
412
+ if (!isCredentialHeader(value)) {
413
+ values.push(shellQuote(value));
414
+ }
415
+ }
416
+ if (values.length > 0) {
417
+ tokens.push("--header", ...values);
418
+ }
269
419
  continue;
270
420
  }
271
421
  tokens.push(shellQuote(token));
272
422
  }
273
- return ["brew-cli", ...tokens, "--yes"].join(" ");
423
+ return tokens;
424
+ }
425
+ var CREDENTIAL_HEADER_NAME = /authorization|cookie|api-?key|auth|token|secret|passw(or)?d|session|signature|credential/i;
426
+ function isCredentialHeader(header) {
427
+ const name = /^\s*([^:\s]+)\s*:/.exec(header)?.[1];
428
+ return name !== void 0 && CREDENTIAL_HEADER_NAME.test(name);
274
429
  }
275
430
  function shellQuote(token) {
276
431
  if (/^[A-Za-z0-9@%+=:,./_-]+$/.test(token)) {
@@ -281,7 +436,7 @@ function shellQuote(token) {
281
436
 
282
437
  // src/version.ts
283
438
  var CLI_NAME = "brew-cli";
284
- var CLI_VERSION = "0.9.0" ;
439
+ var CLI_VERSION = "0.10.0" ;
285
440
  var CONFIG_KEYS = ["apiKey", "brandId", "apiUrl"];
286
441
  function isConfigKey(value) {
287
442
  return CONFIG_KEYS.includes(value);
@@ -384,12 +539,18 @@ function resolveAuth(input) {
384
539
  const apiKeySource = keyFromFlag ? "flag" : keyFromEnv ? "env" : "config";
385
540
  return { apiKey, apiKeySource, brandId, apiUrl };
386
541
  }
387
- function buildSdkClient(auth) {
542
+ function buildSdkClient(auth, transport) {
388
543
  return createBrewClient({
389
544
  apiKey: auth.apiKey,
390
545
  baseUrl: auth.apiUrl,
391
546
  userAgent: `${CLI_NAME}/${CLI_VERSION}`,
392
- ...auth.brandId === void 0 ? {} : { brandId: auth.brandId }
547
+ ...auth.brandId === void 0 ? {} : { brandId: auth.brandId },
548
+ // Client-wide, so every call the command makes observes the interrupt
549
+ // and the deadline without each command file threading a signal.
550
+ signal: transport.signal,
551
+ fetch: transport.fetch,
552
+ ...transport.timeoutMs === void 0 ? {} : { timeoutMs: transport.timeoutMs },
553
+ ...transport.maxRetries === void 0 ? {} : { maxRetries: transport.maxRetries }
393
554
  });
394
555
  }
395
556
  function isOrgLevelPath(url) {
@@ -466,6 +627,114 @@ function formatCell(value) {
466
627
  return String(value);
467
628
  }
468
629
 
630
+ // src/lib/transport.ts
631
+ function apiPathOf(url) {
632
+ const index = url.pathname.indexOf("/v1/");
633
+ const path = index === -1 ? url.pathname : url.pathname.slice(index);
634
+ return `${path}${url.search}`;
635
+ }
636
+ function createTransportState() {
637
+ let lastRequest;
638
+ let attempts = 0;
639
+ let drain;
640
+ return {
641
+ lastRequest: () => lastRequest,
642
+ attempts: () => attempts,
643
+ record: (request) => {
644
+ lastRequest = request;
645
+ attempts += 1;
646
+ },
647
+ drain: () => drain,
648
+ setDrain: (progress2) => {
649
+ drain = progress2;
650
+ }
651
+ };
652
+ }
653
+ function recordingFetch(state) {
654
+ return (input, init) => {
655
+ if (init?.signal?.aborted === true) {
656
+ return globalThis.fetch(input, init);
657
+ }
658
+ const url = new URL(
659
+ typeof input === "string" || input instanceof URL ? input : input.url
660
+ );
661
+ state.record({
662
+ method: (init?.method ?? "GET").toUpperCase(),
663
+ path: apiPathOf(url),
664
+ idempotencyKey: new Headers(init?.headers).get("idempotency-key") ?? void 0
665
+ });
666
+ return globalThis.fetch(input, init);
667
+ };
668
+ }
669
+ function anySignal(signals) {
670
+ const present = signals.filter(
671
+ (signal) => signal !== void 0
672
+ );
673
+ const [only, ...rest] = present;
674
+ if (only !== void 0 && rest.length === 0) {
675
+ return { signal: only, release: () => void 0 };
676
+ }
677
+ if (typeof AbortSignal.any === "function") {
678
+ return { signal: AbortSignal.any(present), release: () => void 0 };
679
+ }
680
+ const controller = new AbortController();
681
+ const alreadyAborted = present.find((signal) => signal.aborted);
682
+ if (alreadyAborted !== void 0) {
683
+ controller.abort(alreadyAborted.reason);
684
+ return { signal: controller.signal, release: () => void 0 };
685
+ }
686
+ const listeners = present.map((source) => {
687
+ const onAbort = () => {
688
+ controller.abort(source.reason);
689
+ };
690
+ source.addEventListener("abort", onAbort, { once: true });
691
+ return { source, onAbort };
692
+ });
693
+ return {
694
+ signal: controller.signal,
695
+ release: () => {
696
+ for (const { source, onAbort } of listeners) {
697
+ source.removeEventListener("abort", onAbort);
698
+ }
699
+ }
700
+ };
701
+ }
702
+ var DURATION = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/;
703
+ var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
704
+ var MAX_TIMEOUT_MS = 24 * 36e5;
705
+ function parseTimeoutFlag(value) {
706
+ if (value === void 0) {
707
+ return;
708
+ }
709
+ const match = typeof value === "string" ? DURATION.exec(value.trim()) : null;
710
+ const unit = match?.[2] ?? "s";
711
+ const ms = match === null ? Number.NaN : Math.round(Number(match[1]) * UNIT_MS[unit]);
712
+ if (!(ms >= 1 && ms <= MAX_TIMEOUT_MS)) {
713
+ throw new CliUsageError(
714
+ `--timeout takes a positive duration up to 24h: 90, 90s, 1500ms or 5m (a bare number is seconds); got "${String(value)}".`
715
+ );
716
+ }
717
+ return ms;
718
+ }
719
+ function parseMaxRetriesFlag(value) {
720
+ if (value === void 0) {
721
+ return;
722
+ }
723
+ const parsed = typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
724
+ if (!(Number.isInteger(parsed) && parsed >= 0 && parsed <= 10)) {
725
+ throw new CliUsageError(
726
+ `--max-retries takes a whole number from 0 to 10; got "${String(value)}".`
727
+ );
728
+ }
729
+ return parsed;
730
+ }
731
+ function resolveBudget(input) {
732
+ if (input.timeoutMs !== void 0) {
733
+ return { deadlineMs: input.timeoutMs, attemptMs: input.timeoutMs };
734
+ }
735
+ return { deadlineMs: input.defaultTimeoutMs, attemptMs: void 0 };
736
+ }
737
+
469
738
  // src/lib/context.ts
470
739
  function makeContext(input) {
471
740
  const globals = readGlobalFlags(input.flags);
@@ -473,12 +742,38 @@ function makeContext(input) {
473
742
  isJsonFlag: globals.json,
474
743
  isTtyOut: input.io.isTtyOut
475
744
  });
745
+ const budget = resolveBudget({
746
+ timeoutMs: globals.timeoutMs,
747
+ defaultTimeoutMs: input.traits?.defaultTimeoutMs
748
+ });
749
+ const deadline = new AbortController();
750
+ let deadlineTimer;
751
+ const { signal } = anySignal([input.io.signal, deadline.signal]);
752
+ const transport = createTransportState();
476
753
  const cache = /* @__PURE__ */ new Map();
477
- return {
754
+ const ctx = {
478
755
  io: input.io,
479
756
  mode,
480
757
  globals,
481
758
  rawArgv: input.rawArgv,
759
+ signal,
760
+ budget,
761
+ transport,
762
+ traits: input.traits,
763
+ startDeadline: () => {
764
+ const ms = budget.deadlineMs;
765
+ if (ms === void 0 || deadlineTimer !== void 0) {
766
+ return;
767
+ }
768
+ deadlineTimer = setTimeout(() => {
769
+ deadline.abort(new CliTimeoutError(ms));
770
+ }, ms);
771
+ },
772
+ stopDeadline: () => {
773
+ if (deadlineTimer !== void 0) {
774
+ clearTimeout(deadlineTimer);
775
+ }
776
+ },
482
777
  client: (options) => {
483
778
  const key = options?.allowAnonymous === true ? "anonymous" : "authed";
484
779
  const existing = cache.get(key);
@@ -490,11 +785,20 @@ function makeContext(input) {
490
785
  env: input.io.env,
491
786
  allowAnonymous: options?.allowAnonymous === true
492
787
  });
493
- const client = buildSdkClient(auth);
788
+ const client = buildSdkClient(auth, sdkTransportFor(ctx));
494
789
  cache.set(key, client);
495
790
  return client;
496
791
  }
497
792
  };
793
+ return ctx;
794
+ }
795
+ function sdkTransportFor(ctx) {
796
+ return {
797
+ signal: ctx.signal,
798
+ fetch: recordingFetch(ctx.transport),
799
+ timeoutMs: ctx.budget.attemptMs,
800
+ maxRetries: ctx.globals.maxRetries
801
+ };
498
802
  }
499
803
  function makeFallbackContext(io, rawArgv) {
500
804
  const flags = {
@@ -510,7 +814,9 @@ function readGlobalFlags(flags) {
510
814
  yes: flags.yes === true,
511
815
  apiKey: readStringFlag(flags.apiKey),
512
816
  brand: readStringFlag(flags.brand),
513
- apiUrl: readStringFlag(flags.apiUrl)
817
+ apiUrl: readStringFlag(flags.apiUrl),
818
+ timeoutMs: parseTimeoutFlag(flags.timeout),
819
+ maxRetries: parseMaxRetriesFlag(flags.maxRetries)
514
820
  };
515
821
  }
516
822
  function readStringFlag(value) {
@@ -542,6 +848,14 @@ var GLOBAL_FLAGS = [
542
848
  {
543
849
  flag: "--api-url <url>",
544
850
  summary: "API base URL (else BREW_API_URL, else https://brew.new/api)"
851
+ },
852
+ {
853
+ flag: "--timeout <duration>",
854
+ summary: "Give up after this long, retries and the response body included (90s, 1500ms, 5m)"
855
+ },
856
+ {
857
+ flag: "--max-retries <n>",
858
+ summary: "Retries after a transient failure, 0-10 (default 2; raw requests make one attempt)"
545
859
  }
546
860
  ];
547
861
  function asSdkInput(value) {
@@ -661,6 +975,61 @@ var INPUT_FLAG = {
661
975
  flag: "--input <json>",
662
976
  summary: "Full JSON request body, or - to read stdin (flags override it)"
663
977
  };
978
+ var LONG_RUNNING_ROUTES = [
979
+ {
980
+ method: "POST",
981
+ path: "/v1/emails",
982
+ timeoutMs: GENERATE_EMAIL_DEFAULT_TIMEOUT_MS
983
+ },
984
+ {
985
+ method: "PATCH",
986
+ path: "/v1/emails/{emailId}",
987
+ timeoutMs: EDIT_EMAIL_DEFAULT_TIMEOUT_MS
988
+ },
989
+ {
990
+ method: "POST",
991
+ path: "/v1/emails/audit",
992
+ timeoutMs: AUDIT_EMAIL_DEFAULT_TIMEOUT_MS
993
+ },
994
+ {
995
+ method: "POST",
996
+ path: "/v1/emails/{emailId}/client-previews",
997
+ timeoutMs: PREVIEW_EMAIL_CLIENTS_DEFAULT_TIMEOUT_MS
998
+ },
999
+ {
1000
+ method: "POST",
1001
+ path: "/v1/emails/import",
1002
+ timeoutMs: IMPORT_EMAIL_DEFAULT_TIMEOUT_MS
1003
+ },
1004
+ {
1005
+ method: "POST",
1006
+ path: "/v1/emails/figma",
1007
+ timeoutMs: IMPORT_FIGMA_DEFAULT_TIMEOUT_MS
1008
+ },
1009
+ {
1010
+ method: "POST",
1011
+ path: "/v1/content/gif",
1012
+ timeoutMs: GIF_DEFAULT_TIMEOUT_MS
1013
+ },
1014
+ {
1015
+ method: "POST",
1016
+ path: "/v1/content/generate-image",
1017
+ timeoutMs: GENERATE_IMAGE_DEFAULT_TIMEOUT_MS
1018
+ }
1019
+ ];
1020
+ function longRunningTimeoutMs(input) {
1021
+ const bare = input.path.split("?")[0] ?? input.path;
1022
+ return LONG_RUNNING_ROUTES.find(
1023
+ (route) => route.method === input.method && matchesTemplate(route.path, bare)
1024
+ )?.timeoutMs;
1025
+ }
1026
+ function matchesTemplate(template, path) {
1027
+ const want = template.split("/");
1028
+ const have = path.split("/");
1029
+ return want.length === have.length && want.every(
1030
+ (segment, index) => segment.startsWith("{") && segment.endsWith("}") ? (have[index] ?? "") !== "" : segment === have[index]
1031
+ );
1032
+ }
664
1033
 
665
1034
  // src/lib/paginate.ts
666
1035
  async function collectAll(ctx, fetchPage) {
@@ -668,7 +1037,20 @@ async function collectAll(ctx, fetchPage) {
668
1037
  let cursor;
669
1038
  let pageCount = 0;
670
1039
  for (; ; ) {
671
- const page = await fetchPage(cursor);
1040
+ let page;
1041
+ try {
1042
+ ctx.signal.throwIfAborted();
1043
+ page = await fetchPage(cursor);
1044
+ } catch (error) {
1045
+ if (pageCount > 0) {
1046
+ ctx.transport.setDrain({
1047
+ rowsFetched: rows.length,
1048
+ pagesFetched: pageCount,
1049
+ ...cursor === void 0 ? {} : { resumeCursor: cursor }
1050
+ });
1051
+ }
1052
+ throw error;
1053
+ }
672
1054
  rows.push(...page.data);
673
1055
  pageCount += 1;
674
1056
  const next = page.pagination?.cursor ?? null;
@@ -861,116 +1243,15 @@ var analyticsCampaignsCommand = defineCommand({
861
1243
  }
862
1244
  });
863
1245
 
864
- // src/lib/raw-request.ts
865
- async function rawRequest(ctx, request) {
866
- const auth = resolveAuth({
867
- globals: ctx.globals,
868
- env: ctx.io.env,
869
- ...request.allowAnonymous === true ? { allowAnonymous: true } : {}
870
- });
871
- const hasBody = request.body !== void 0;
872
- const idempotencyKey = resolveRawIdempotencyKey(
873
- request.method,
874
- request.idempotencyKey
875
- );
876
- const headers = buildRawHeaders({
877
- auth,
878
- path: request.path,
879
- hasJsonBody: hasBody,
880
- idempotencyKey
881
- });
882
- const url = new URL(`${auth.apiUrl}${request.path}`);
883
- for (const [key, value] of Object.entries(request.query ?? {})) {
884
- if (value !== void 0) {
885
- url.searchParams.set(key, value);
886
- }
887
- }
888
- const response = await fetch(url, {
889
- method: request.method,
890
- headers,
891
- ...hasBody ? { body: JSON.stringify(request.body) } : {},
892
- ...request.signal === void 0 ? {} : { signal: request.signal }
893
- });
894
- const text = await response.text();
895
- const parsed = tryParseJson(text);
896
- if (!response.ok) {
897
- throw responseToApiError(response, parsed);
898
- }
899
- if (parsed === void 0) {
900
- throw new CliApiError({
901
- status: response.status,
902
- code: "UNEXPECTED_RESPONSE",
903
- type: "internal_error",
904
- message: "The API returned a non-JSON success response."
905
- });
906
- }
907
- return parsed;
908
- }
909
- function resolveRawIdempotencyKey(method, provided) {
910
- if (provided !== void 0 && provided !== "") {
911
- return provided;
912
- }
913
- return method === "POST" ? crypto.randomUUID() : void 0;
914
- }
915
- function buildRawHeaders(input) {
916
- const headers = new Headers({
917
- authorization: `Bearer ${input.auth.apiKey}`,
918
- "user-agent": `${CLI_NAME}/${CLI_VERSION}`,
919
- accept: "application/json, text/plain;q=0.9"
920
- });
921
- if (input.hasJsonBody) {
922
- headers.set("content-type", "application/json");
923
- }
924
- if (input.auth.brandId !== void 0 && !isOrgLevelPath(input.path)) {
925
- headers.set("x-brand-id", input.auth.brandId);
926
- }
927
- if (input.idempotencyKey !== void 0 && input.idempotencyKey !== "") {
928
- headers.set("idempotency-key", input.idempotencyKey);
929
- }
930
- return headers;
931
- }
932
- function responseToApiError(response, parsed) {
933
- const requestId = response.headers.get("x-request-id") ?? void 0;
934
- const record = isRecord2(parsed) ? parsed : void 0;
935
- const standard = record !== void 0 && isRecord2(record.error) ? record.error : void 0;
936
- const envelope = standard ?? record;
937
- const readField = (key) => {
938
- const value = envelope?.[key];
939
- return typeof value === "string" ? value : void 0;
940
- };
941
- const param = readField("param");
942
- const suggestion = readField("suggestion") ?? (envelope === void 0 ? void 0 : suggestionForStatus(response.status));
943
- const docs = readField("docs");
944
- const details = isRecord2(envelope?.details) ? envelope.details : void 0;
945
- return new CliApiError({
946
- status: response.status,
947
- code: readField("code") ?? "HTTP_ERROR",
948
- type: readField("type") ?? errorTypeForStatus(response.status),
949
- message: readField("message") ?? `Request failed with status ${response.status}`,
950
- ...param === void 0 ? {} : { param },
951
- ...suggestion === void 0 ? {} : { suggestion },
952
- ...docs === void 0 ? {} : { docs },
953
- ...requestId === void 0 ? {} : { requestId },
954
- ...details === void 0 ? {} : { details },
955
- ...parsed === void 0 ? {} : { body: parsed }
956
- });
957
- }
958
- function isRecord2(value) {
959
- return typeof value === "object" && value !== null && !Array.isArray(value);
960
- }
961
- function tryParseJson(text) {
962
- try {
963
- return JSON.parse(text);
964
- } catch {
965
- }
966
- }
967
-
968
1246
  // src/commands/analytics/event-counts.ts
1247
+ var ROW_ONLY_KEYS = {
1248
+ cursor: "A grouped count is not paged; drop `cursor`.",
1249
+ automationRunId: "`groupBy` and `bucket` count email events and do not combine with `automationRunId`."
1250
+ };
969
1251
  var analyticsEventCountsCommand = defineCommand({
970
1252
  path: ["analytics", "event-counts"],
971
1253
  summary: "Count email events per field and/or period (clicks per link, events per day, unsubscribe reasons)",
972
- sdkMethod: null,
973
- isRawTransport: true,
1254
+ sdkMethod: "analytics.eventCounts",
974
1255
  route: { method: "GET", path: "/v1/analytics/events" },
975
1256
  commandClass: "read",
976
1257
  flags: [
@@ -1018,21 +1299,27 @@ var analyticsEventCountsCommand = defineCommand({
1018
1299
  "Pass --group-by, --bucket, or both (`analytics events` lists the rows)."
1019
1300
  );
1020
1301
  }
1021
- const query = {};
1022
- for (const [key, value] of Object.entries(input)) {
1023
- if (key === "cursor" || key === "limit") {
1024
- continue;
1302
+ for (const [key, reason] of Object.entries(ROW_ONLY_KEYS)) {
1303
+ if (input[key] !== void 0) {
1304
+ throw new CliUsageError(reason);
1025
1305
  }
1026
- query[key] = value === void 0 || value === null ? void 0 : Array.isArray(value) ? value.map(String).join(",") : String(value);
1027
1306
  }
1028
- const body = await rawRequest(ctx, {
1029
- method: "GET",
1030
- path: "/v1/analytics/events",
1031
- query
1032
- });
1307
+ const { limit: _pageSize, ...counted } = input;
1308
+ const body = await ctx.client().analytics.eventCounts(
1309
+ asSdkInput({
1310
+ ...counted,
1311
+ ...counted.groupBy === void 0 ? {} : { groupBy: toFieldList(counted.groupBy) }
1312
+ })
1313
+ );
1033
1314
  return { data: body, human: renderCounts(body) };
1034
1315
  }
1035
1316
  });
1317
+ function toFieldList(value) {
1318
+ if (typeof value !== "string") {
1319
+ return value;
1320
+ }
1321
+ return value.split(",").map((field) => field.trim()).filter((field) => field !== "");
1322
+ }
1036
1323
  function renderCounts(body) {
1037
1324
  if (body.groups.length === 0) {
1038
1325
  return `No events counted (total ${body.count}).`;
@@ -1043,7 +1330,7 @@ function renderCounts(body) {
1043
1330
  count: group.count
1044
1331
  }));
1045
1332
  const table = renderTable(rows, [
1046
- { key: "group", header: "GROUP" },
1333
+ ...rows.some((row) => row.group !== "") ? [{ key: "group", header: "GROUP" }] : [],
1047
1334
  ...rows.some((row) => "bucket" in row) ? [{ key: "bucket", header: "PERIOD" }] : [],
1048
1335
  { key: "count", header: "COUNT" }
1049
1336
  ]);
@@ -1402,7 +1689,171 @@ var analyticsTriggerInstancesListCommand = defineCommand({
1402
1689
  });
1403
1690
  return await listTriggerInstances(ctx, input, flags.all === true);
1404
1691
  }
1405
- });
1692
+ });
1693
+
1694
+ // src/lib/raw-request.ts
1695
+ var DEFAULT_ATTEMPT_TIMEOUT_MS = 3e4;
1696
+ async function rawFetch(ctx, request) {
1697
+ const attempt = new AbortController();
1698
+ const timer = setTimeout(() => {
1699
+ attempt.abort(new CliTimeoutError(request.timeoutMs));
1700
+ }, request.timeoutMs);
1701
+ const combined = anySignal([ctx.signal, attempt.signal]);
1702
+ const { signal } = combined;
1703
+ if (signal.aborted) {
1704
+ clearTimeout(timer);
1705
+ combined.release();
1706
+ throw signal.reason;
1707
+ }
1708
+ ctx.transport.record({
1709
+ method: request.method,
1710
+ path: apiPathOf(new URL(request.url)),
1711
+ idempotencyKey: request.headers.get("idempotency-key") ?? void 0
1712
+ });
1713
+ try {
1714
+ const response = await fetch(request.url, {
1715
+ method: request.method,
1716
+ headers: request.headers,
1717
+ signal,
1718
+ ...request.body === void 0 ? {} : { body: request.body }
1719
+ });
1720
+ return { response, text: await readBodyText(response, signal) };
1721
+ } catch (error) {
1722
+ if (signal.aborted) {
1723
+ throw signal.reason;
1724
+ }
1725
+ throw new CliConnectionError(error);
1726
+ } finally {
1727
+ clearTimeout(timer);
1728
+ combined.release();
1729
+ }
1730
+ }
1731
+ async function readBodyText(response, signal) {
1732
+ if (response.body === null) {
1733
+ return "";
1734
+ }
1735
+ const reader = response.body.getReader();
1736
+ const onAbort = () => {
1737
+ reader.cancel(signal.reason).catch(() => void 0);
1738
+ };
1739
+ signal.addEventListener("abort", onAbort, { once: true });
1740
+ const decoder = new TextDecoder();
1741
+ let text = "";
1742
+ try {
1743
+ for (; ; ) {
1744
+ const { done, value } = await reader.read();
1745
+ signal.throwIfAborted();
1746
+ if (done) {
1747
+ return text + decoder.decode();
1748
+ }
1749
+ text += decoder.decode(value, { stream: true });
1750
+ }
1751
+ } finally {
1752
+ signal.removeEventListener("abort", onAbort);
1753
+ }
1754
+ }
1755
+ async function rawRequest(ctx, request) {
1756
+ const auth = resolveAuth({
1757
+ globals: ctx.globals,
1758
+ env: ctx.io.env,
1759
+ ...request.allowAnonymous === true ? { allowAnonymous: true } : {}
1760
+ });
1761
+ const hasBody = request.body !== void 0;
1762
+ const idempotencyKey = resolveRawIdempotencyKey(
1763
+ request.method,
1764
+ request.idempotencyKey
1765
+ );
1766
+ const headers = buildRawHeaders({
1767
+ auth,
1768
+ path: request.path,
1769
+ hasJsonBody: hasBody,
1770
+ idempotencyKey
1771
+ });
1772
+ const url = new URL(`${auth.apiUrl}${request.path}`);
1773
+ for (const [key, value] of Object.entries(request.query ?? {})) {
1774
+ if (value !== void 0) {
1775
+ url.searchParams.set(key, value);
1776
+ }
1777
+ }
1778
+ const { response, text } = await rawFetch(ctx, {
1779
+ method: request.method,
1780
+ url: url.toString(),
1781
+ headers,
1782
+ ...hasBody ? { body: JSON.stringify(request.body) } : {},
1783
+ timeoutMs: ctx.budget.attemptMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS
1784
+ });
1785
+ const parsed = tryParseJson(text);
1786
+ if (!response.ok) {
1787
+ throw responseToApiError(response, parsed);
1788
+ }
1789
+ if (parsed === void 0) {
1790
+ throw new CliApiError({
1791
+ status: response.status,
1792
+ code: "UNEXPECTED_RESPONSE",
1793
+ type: "internal_error",
1794
+ message: "The API returned a non-JSON success response."
1795
+ });
1796
+ }
1797
+ return parsed;
1798
+ }
1799
+ function resolveRawIdempotencyKey(method, provided) {
1800
+ if (provided !== void 0 && provided !== "") {
1801
+ return provided;
1802
+ }
1803
+ return method === "POST" ? crypto.randomUUID() : void 0;
1804
+ }
1805
+ function buildRawHeaders(input) {
1806
+ const headers = new Headers({
1807
+ authorization: `Bearer ${input.auth.apiKey}`,
1808
+ "user-agent": `${CLI_NAME}/${CLI_VERSION}`,
1809
+ accept: "application/json, text/plain;q=0.9"
1810
+ });
1811
+ if (input.hasJsonBody) {
1812
+ headers.set("content-type", "application/json");
1813
+ }
1814
+ if (input.auth.brandId !== void 0 && !isOrgLevelPath(input.path)) {
1815
+ headers.set("x-brand-id", input.auth.brandId);
1816
+ }
1817
+ if (input.idempotencyKey !== void 0 && input.idempotencyKey !== "") {
1818
+ headers.set("idempotency-key", input.idempotencyKey);
1819
+ }
1820
+ return headers;
1821
+ }
1822
+ function responseToApiError(response, parsed) {
1823
+ const requestId = response.headers.get("x-request-id") ?? void 0;
1824
+ const record = isRecord2(parsed) ? parsed : void 0;
1825
+ const standard = record !== void 0 && isRecord2(record.error) ? record.error : void 0;
1826
+ const envelope = standard ?? record;
1827
+ const readField = (key) => {
1828
+ const value = envelope?.[key];
1829
+ return typeof value === "string" ? value : void 0;
1830
+ };
1831
+ const param = readField("param");
1832
+ const suggestion = readField("suggestion") ?? (envelope === void 0 ? void 0 : suggestionForStatus(response.status));
1833
+ const docs = readField("docs");
1834
+ const details = isRecord2(envelope?.details) ? envelope.details : void 0;
1835
+ return new CliApiError({
1836
+ status: response.status,
1837
+ code: readField("code") ?? "HTTP_ERROR",
1838
+ type: readField("type") ?? errorTypeForStatus(response.status),
1839
+ message: readField("message") ?? `Request failed with status ${response.status}`,
1840
+ ...param === void 0 ? {} : { param },
1841
+ ...suggestion === void 0 ? {} : { suggestion },
1842
+ ...docs === void 0 ? {} : { docs },
1843
+ ...requestId === void 0 ? {} : { requestId },
1844
+ ...details === void 0 ? {} : { details },
1845
+ ...parsed === void 0 ? {} : { body: parsed }
1846
+ });
1847
+ }
1848
+ function isRecord2(value) {
1849
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1850
+ }
1851
+ function tryParseJson(text) {
1852
+ try {
1853
+ return JSON.parse(text);
1854
+ } catch {
1855
+ }
1856
+ }
1406
1857
 
1407
1858
  // src/commands/api.ts
1408
1859
  var METHODS = ["GET", "POST", "PATCH", "DELETE"];
@@ -1432,7 +1883,7 @@ var apiCommand = defineCommand({
1432
1883
  },
1433
1884
  {
1434
1885
  flag: "--idempotency-key <key>",
1435
- summary: "Idempotency-Key header for safe POST retries"
1886
+ summary: "Idempotency-Key header for safe POST retries (a POST gets one generated otherwise)"
1436
1887
  }
1437
1888
  ],
1438
1889
  examples: [
@@ -1464,7 +1915,7 @@ var apiCommand = defineCommand({
1464
1915
  auth,
1465
1916
  path,
1466
1917
  hasJsonBody: body !== void 0,
1467
- idempotencyKey: typeof flags.idempotencyKey === "string" ? flags.idempotencyKey : void 0
1918
+ idempotencyKey: typeof flags.idempotencyKey === "string" && flags.idempotencyKey !== "" ? flags.idempotencyKey : method === "POST" ? crypto.randomUUID() : void 0
1468
1919
  });
1469
1920
  for (const header of readHeaderFlags(flags.header)) {
1470
1921
  const separator = header.indexOf(":");
@@ -1478,12 +1929,16 @@ var apiCommand = defineCommand({
1478
1929
  header.slice(separator + 1).trim()
1479
1930
  );
1480
1931
  }
1481
- const response = await fetch(`${auth.apiUrl}${path}`, {
1932
+ const { response, text } = await rawFetch(ctx, {
1482
1933
  method,
1934
+ url: `${auth.apiUrl}${path}`,
1483
1935
  headers,
1484
- ...body === void 0 ? {} : { body }
1936
+ ...body === void 0 ? {} : { body },
1937
+ // --timeout, else the SDK's own default for this route when it is a
1938
+ // long-running one (`api POST /v1/emails` waits as long as `emails
1939
+ // generate`), else the SDK's 30 s.
1940
+ timeoutMs: ctx.budget.attemptMs ?? longRunningTimeoutMs({ method, path: barePath }) ?? DEFAULT_ATTEMPT_TIMEOUT_MS
1485
1941
  });
1486
- const text = await response.text();
1487
1942
  const parsed = tryParseJson(text);
1488
1943
  if (!response.ok) {
1489
1944
  throw responseToApiError(response, parsed);
@@ -1813,6 +2268,10 @@ var audiencesListCommand = defineCommand({
1813
2268
  flag: "--include <tokens>",
1814
2269
  summary: "0.6 flag: includes ride the detail read now (`audiences get --include count,build`)"
1815
2270
  },
2271
+ {
2272
+ flag: "--search <text>",
2273
+ summary: "Only audiences whose name contains this text"
2274
+ },
1816
2275
  LIMIT_FLAG,
1817
2276
  CURSOR_FLAG,
1818
2277
  ALL_FLAG,
@@ -1829,6 +2288,7 @@ var audiencesListCommand = defineCommand({
1829
2288
  throw includeRidesDetailRead("audiences get");
1830
2289
  }
1831
2290
  const input = mergeInput(base, {
2291
+ search: flagString(flags.search),
1832
2292
  limit: flagInt(flags.limit, "--limit"),
1833
2293
  cursor: flagString(flags.cursor)
1834
2294
  });
@@ -1867,23 +2327,41 @@ function renderAudiences(rows) {
1867
2327
  // src/commands/audiences/update.ts
1868
2328
  var audiencesUpdateCommand = defineCommand({
1869
2329
  path: ["audiences", "update"],
1870
- summary: "Update an audience segment (name and/or filters)",
2330
+ summary: "Update an audience segment: its name, its filters, or its members by email",
1871
2331
  sdkMethod: "audiences.update",
1872
2332
  route: { method: "PATCH", path: "/v1/audiences/{audienceId}" },
1873
2333
  commandClass: "write",
1874
2334
  args: [
1875
2335
  { name: "audienceId", summary: "Audience id to update", isRequired: true }
1876
2336
  ],
1877
- flags: [{ flag: "--name <name>", summary: "New audience name" }, INPUT_FLAG],
2337
+ flags: [
2338
+ { flag: "--name <name>", summary: "New audience name" },
2339
+ {
2340
+ flag: "--add-email <addresses...>",
2341
+ summary: "Add these contacts by email, repeatable (refused when the filters cannot express it exactly)"
2342
+ },
2343
+ {
2344
+ flag: "--remove-email <addresses...>",
2345
+ summary: "Remove these contacts by email, repeatable (same rule)"
2346
+ },
2347
+ INPUT_FLAG
2348
+ ],
1878
2349
  examples: [
1879
2350
  'brew-cli audiences update aud_3k9sQ --name "VIP customers"',
2351
+ "brew-cli audiences update aud_3k9sQ --add-email ada@example.com --remove-email bo@example.com",
1880
2352
  `brew-cli audiences update aud_3k9sQ --input '{"filters":{"filters":[{"field":"plan","operator":"equals","value":"vip"}],"logicalOperator":"and"}}'`
1881
2353
  ],
1882
2354
  run: async ({ ctx, args, flags }) => {
1883
2355
  const base = await readJsonFlag(ctx, flags.input, "--input");
1884
- const input = mergeInput(base, { name: flagString(flags.name) });
2356
+ const input = mergeInput(base, {
2357
+ name: flagString(flags.name),
2358
+ addEmails: toStringArray(flags.addEmail),
2359
+ removeEmails: toStringArray(flags.removeEmail)
2360
+ });
1885
2361
  if (Object.keys(input).length === 0) {
1886
- throw new CliUsageError("Nothing to update \u2014 pass --name or --input.");
2362
+ throw new CliUsageError(
2363
+ "Nothing to update \u2014 pass --name, --add-email, --remove-email or --input."
2364
+ );
1887
2365
  }
1888
2366
  const result = await ctx.client().audiences.update(
1889
2367
  asSdkInput({
@@ -2236,7 +2714,16 @@ var automationsListCommand = defineCommand({
2236
2714
  sdkMethod: "automations.list",
2237
2715
  route: { method: "GET", path: "/v1/automations" },
2238
2716
  commandClass: "read",
2239
- flags: [LIMIT_FLAG, CURSOR_FLAG, ALL_FLAG, INPUT_FLAG],
2717
+ flags: [
2718
+ {
2719
+ flag: "--search <text>",
2720
+ summary: "Only automations whose name matches these words"
2721
+ },
2722
+ LIMIT_FLAG,
2723
+ CURSOR_FLAG,
2724
+ ALL_FLAG,
2725
+ INPUT_FLAG
2726
+ ],
2240
2727
  examples: [
2241
2728
  "brew-cli automations list",
2242
2729
  "brew-cli automations list --all --json"
@@ -2244,6 +2731,7 @@ var automationsListCommand = defineCommand({
2244
2731
  run: async ({ ctx, flags }) => {
2245
2732
  const base = await readJsonFlag(ctx, flags.input, "--input");
2246
2733
  const input = mergeInput(base, {
2734
+ search: flagString(flags.search),
2247
2735
  limit: flagInt(flags.limit, "--limit"),
2248
2736
  cursor: flagString(flags.cursor)
2249
2737
  });
@@ -2632,17 +3120,14 @@ var FORMAT_FLAG = {
2632
3120
  var FORMATS = /* @__PURE__ */ new Set(["json", "ts", "zod", "jsonschema", "skill"]);
2633
3121
  var ENFORCEMENT_MODES = /* @__PURE__ */ new Set(["off", "prune", "strict"]);
2634
3122
  var VALIDATE_ENFORCEMENT_MODES = /* @__PURE__ */ new Set(["prune", "strict"]);
2635
- function formatQuery(value) {
3123
+ function formatOption(value) {
2636
3124
  const format = flagString(value);
2637
- if (format === void 0) {
2638
- return "";
2639
- }
2640
- if (!FORMATS.has(format)) {
3125
+ if (format !== void 0 && !FORMATS.has(format)) {
2641
3126
  throw new CliUsageError(
2642
3127
  `Unknown --format '${format}' (expected json | ts | zod | jsonschema | skill).`
2643
3128
  );
2644
3129
  }
2645
- return `?format=${format}`;
3130
+ return format;
2646
3131
  }
2647
3132
  function contentHuman(body) {
2648
3133
  return typeof body.content === "string" ? { human: body.content } : {};
@@ -2650,8 +3135,7 @@ function contentHuman(body) {
2650
3135
  var automationsTriggersContractGetCommand = defineCommand({
2651
3136
  path: ["automations", "triggers", "contract", "get"],
2652
3137
  summary: "Read a trigger payload contract: stored when declared, derived otherwise; --format renders ts/zod/jsonschema/skill",
2653
- sdkMethod: null,
2654
- isRawTransport: true,
3138
+ sdkMethod: "automations.triggers.getContract",
2655
3139
  route: {
2656
3140
  method: "GET",
2657
3141
  path: "/v1/automations/triggers/{triggerEventId}/contract"
@@ -2671,18 +3155,20 @@ var automationsTriggersContractGetCommand = defineCommand({
2671
3155
  "brew-cli automations triggers contract get tri_signup --format skill --json | jq -r .content"
2672
3156
  ],
2673
3157
  run: async ({ ctx, args, flags }) => {
2674
- const body = await rawRequest(ctx, {
2675
- method: "GET",
2676
- path: `/v1/automations/triggers/${encodeURIComponent(args.triggerEventId ?? "")}/contract${formatQuery(flags.format)}`
2677
- });
3158
+ const format = formatOption(flags.format);
3159
+ const body = await ctx.client().automations.triggers.getContract(
3160
+ asSdkInput({
3161
+ triggerEventId: args.triggerEventId ?? "",
3162
+ ...format === void 0 ? {} : { format }
3163
+ })
3164
+ );
2678
3165
  return { data: body, ...contentHuman(body) };
2679
3166
  }
2680
3167
  });
2681
3168
  var automationsTriggersContractPutCommand = defineCommand({
2682
3169
  path: ["automations", "triggers", "contract", "put"],
2683
3170
  summary: "Declare (or replace) the stored payload contract for a trigger \u2014 tree-validated before any write; omitting --enforcement leaves the stored setting unchanged",
2684
- sdkMethod: null,
2685
- isRawTransport: true,
3171
+ sdkMethod: "automations.triggers.putContract",
2686
3172
  route: {
2687
3173
  method: "PUT",
2688
3174
  path: "/v1/automations/triggers/{triggerEventId}/contract"
@@ -2734,19 +3220,19 @@ var automationsTriggersContractPutCommand = defineCommand({
2734
3220
  "Empty contract body \u2014 pass --input (fields and/or name) or --enforcement <mode>."
2735
3221
  );
2736
3222
  }
2737
- const body = await rawRequest(ctx, {
2738
- method: "PUT",
2739
- path: `/v1/automations/triggers/${encodeURIComponent(args.triggerEventId ?? "")}/contract`,
2740
- body: requestBody
2741
- });
3223
+ const body = await ctx.client().automations.triggers.putContract(
3224
+ asSdkInput({
3225
+ ...requestBody,
3226
+ triggerEventId: args.triggerEventId ?? ""
3227
+ })
3228
+ );
2742
3229
  return { data: body };
2743
3230
  }
2744
3231
  });
2745
3232
  var automationsTriggersContractValidateCommand = defineCommand({
2746
3233
  path: ["automations", "triggers", "contract", "validate"],
2747
3234
  summary: "Dry-run a payload against a trigger's contract (the fire path's validator) \u2014 never fires; invalid payloads still exit 0",
2748
- sdkMethod: null,
2749
- isRawTransport: true,
3235
+ sdkMethod: "automations.triggers.validatePayload",
2750
3236
  route: {
2751
3237
  method: "POST",
2752
3238
  path: "/v1/automations/triggers/{triggerEventId}/contract/validate"
@@ -2781,14 +3267,13 @@ var automationsTriggersContractValidateCommand = defineCommand({
2781
3267
  `Unknown --enforcement '${enforcement}' (expected prune | strict).`
2782
3268
  );
2783
3269
  }
2784
- const body = await rawRequest(ctx, {
2785
- method: "POST",
2786
- path: `/v1/automations/triggers/${encodeURIComponent(args.triggerEventId ?? "")}/contract/validate`,
2787
- body: {
3270
+ const body = await ctx.client().automations.triggers.validatePayload(
3271
+ asSdkInput({
3272
+ triggerEventId: args.triggerEventId ?? "",
2788
3273
  payload,
2789
3274
  ...enforcement !== void 0 ? { enforcement } : {}
2790
- }
2791
- });
3275
+ })
3276
+ );
2792
3277
  return { data: body };
2793
3278
  }
2794
3279
  });
@@ -3164,8 +3649,7 @@ var RETIRED_FILTERS = {
3164
3649
  var brandGetImagesCommand = defineCommand({
3165
3650
  path: ["brand", "get-images"],
3166
3651
  summary: "Browse or semantically search the brand's assets (logos, brand images, images made with Brew)",
3167
- sdkMethod: null,
3168
- isRawTransport: true,
3652
+ sdkMethod: "brand.getImages",
3169
3653
  route: { method: "GET", path: "/v1/brand/images" },
3170
3654
  commandClass: "read",
3171
3655
  flags: [
@@ -3215,11 +3699,11 @@ var brandGetImagesCommand = defineCommand({
3215
3699
  throw new CliUsageError(message);
3216
3700
  }
3217
3701
  }
3218
- const page = (cursor) => rawRequest(ctx, {
3219
- method: "GET",
3220
- path: "/v1/brand/images",
3221
- query: toQuery(cursor === void 0 ? input : { ...input, cursor })
3222
- });
3702
+ const page = (cursor) => ctx.client().brand.getImages(
3703
+ asSdkInput(
3704
+ cursor === void 0 ? input : { ...input, cursor }
3705
+ )
3706
+ );
3223
3707
  if (flags.all === true) {
3224
3708
  const rows = await collectAll(ctx, page);
3225
3709
  return {
@@ -3231,13 +3715,6 @@ var brandGetImagesCommand = defineCommand({
3231
3715
  return { data: result, human: renderAssets(result.data) };
3232
3716
  }
3233
3717
  });
3234
- function toQuery(input) {
3235
- const query = {};
3236
- for (const [key, value] of Object.entries(input)) {
3237
- query[key] = value === void 0 || value === null ? void 0 : String(value);
3238
- }
3239
- return query;
3240
- }
3241
3718
  function renderAssets(rows) {
3242
3719
  if (rows.length === 0) {
3243
3720
  return "No brand assets found.";
@@ -3538,6 +4015,90 @@ var contactsCountCommand = defineCommand({
3538
4015
  }
3539
4016
  });
3540
4017
 
4018
+ // src/commands/contacts/count-by.ts
4019
+ var contactsCountByCommand = defineCommand({
4020
+ path: ["contacts", "count-by"],
4021
+ summary: "Count contacts per field value, email domain, or signup period (largest group first)",
4022
+ sdkMethod: "contacts.countBy",
4023
+ route: { method: "POST", path: "/v1/contacts/search" },
4024
+ commandClass: "read",
4025
+ flags: [
4026
+ {
4027
+ flag: "--group-by <fields>",
4028
+ summary: "One or two comma-separated fields: a contact field, a custom field, or emailDomain"
4029
+ },
4030
+ {
4031
+ flag: "--bucket <period>",
4032
+ summary: "Count per UTC day, week or month of createdAt"
4033
+ },
4034
+ { flag: "--search <text>", summary: "Free-text search" },
4035
+ {
4036
+ flag: "--filter <filters...>",
4037
+ summary: "Structured filter field:operator[:value], repeatable"
4038
+ },
4039
+ { flag: "--audience <audienceId>", summary: "Scope to one audience" },
4040
+ { flag: "--logic <logic>", summary: "Filter combinator: and | or" },
4041
+ INPUT_FLAG
4042
+ ],
4043
+ examples: [
4044
+ "brew-cli contacts count-by --group-by emailDomain",
4045
+ "brew-cli contacts count-by --bucket month --filter subscribed:equals:true",
4046
+ "brew-cli contacts count-by --group-by country,plan --json"
4047
+ ],
4048
+ run: async ({ ctx, flags }) => {
4049
+ const base = await readJsonFlag(ctx, flags.input, "--input");
4050
+ const input = mergeInput(base, {
4051
+ groupBy: flagString(flags.groupBy),
4052
+ bucket: flagString(flags.bucket),
4053
+ search: flagString(flags.search),
4054
+ filters: parseFilterFlags(flags.filter),
4055
+ audienceId: flagString(flags.audience),
4056
+ logic: flagString(flags.logic)
4057
+ });
4058
+ if (input.groupBy === void 0 && input.bucket === void 0) {
4059
+ throw new CliUsageError(
4060
+ "Pass --group-by, --bucket, or both (`contacts count` gives the total alone)."
4061
+ );
4062
+ }
4063
+ const body = await ctx.client().contacts.countBy(
4064
+ asSdkInput({
4065
+ ...input,
4066
+ ...input.groupBy === void 0 ? {} : { groupBy: toFieldList2(input.groupBy) }
4067
+ })
4068
+ );
4069
+ return { data: body, human: renderCounts2(body) };
4070
+ }
4071
+ });
4072
+ function toFieldList2(value) {
4073
+ if (typeof value !== "string") {
4074
+ return value;
4075
+ }
4076
+ return value.split(",").map((field) => field.trim()).filter((field) => field !== "");
4077
+ }
4078
+ function renderCounts2(body) {
4079
+ const groups = body.groups ?? [];
4080
+ if (groups.length === 0) {
4081
+ return `No contacts counted (total ${body.count}).`;
4082
+ }
4083
+ const rows = groups.map((group) => ({
4084
+ group: Object.entries(group.key).map(([field, value]) => `${field}=${String(value)}`).join(" "),
4085
+ ...group.bucket === void 0 || group.bucket === null ? {} : { bucket: group.bucket },
4086
+ count: group.count
4087
+ }));
4088
+ const table = renderTable(rows, [
4089
+ ...rows.some((row) => row.group !== "") ? [{ key: "group", header: "GROUP" }] : [],
4090
+ ...rows.some((row) => "bucket" in row) ? [{ key: "bucket", header: "PERIOD" }] : [],
4091
+ { key: "count", header: "COUNT" }
4092
+ ]);
4093
+ const otherCount = body.otherCount ?? 0;
4094
+ const footer = [
4095
+ `total ${body.count}`,
4096
+ otherCount > 0 ? `${otherCount} in unlisted groups` : ""
4097
+ ].filter(Boolean).join(" \xB7 ");
4098
+ return `${table}
4099
+ ${footer}`;
4100
+ }
4101
+
3541
4102
  // src/commands/contacts/delete.ts
3542
4103
  var contactsDeleteCommand = defineCommand({
3543
4104
  path: ["contacts", "delete"],
@@ -4044,14 +4605,13 @@ var contentAddImageCommand = defineCommand({
4044
4605
  return { data: result };
4045
4606
  }
4046
4607
  });
4047
-
4048
- // src/commands/content/generate-image.ts
4049
4608
  var contentGenerateImageCommand = defineCommand({
4050
4609
  path: ["content", "generate-image"],
4051
4610
  summary: "Generate or edit an image from a prompt",
4052
4611
  sdkMethod: "content.generateImage",
4053
4612
  route: { method: "POST", path: "/v1/content/generate-image" },
4054
4613
  commandClass: "write",
4614
+ defaultTimeoutMs: GENERATE_IMAGE_DEFAULT_TIMEOUT_MS,
4055
4615
  isCredited: true,
4056
4616
  flags: [
4057
4617
  { flag: "--prompt <text>", summary: "What to generate" },
@@ -4092,14 +4652,13 @@ var contentGenerateImageCommand = defineCommand({
4092
4652
  return { data: result };
4093
4653
  }
4094
4654
  });
4095
-
4096
- // src/commands/content/gif.ts
4097
4655
  var contentGifCommand = defineCommand({
4098
4656
  path: ["content", "gif"],
4099
4657
  summary: "Create an animated GIF from a prompt, image, or video",
4100
4658
  sdkMethod: "content.gif",
4101
4659
  route: { method: "POST", path: "/v1/content/gif" },
4102
4660
  commandClass: "write",
4661
+ defaultTimeoutMs: GIF_DEFAULT_TIMEOUT_MS,
4103
4662
  isCredited: true,
4104
4663
  flags: [
4105
4664
  { flag: "--from <source>", summary: "prompt | image | video" },
@@ -4281,8 +4840,7 @@ var contentTransformCommand = defineCommand({
4281
4840
  var contractsInferCommand = defineCommand({
4282
4841
  path: ["contracts", "infer"],
4283
4842
  summary: "Draft a payload contract from a real example payload \u2014 nothing is saved; PUT the draft on a trigger",
4284
- sdkMethod: null,
4285
- isRawTransport: true,
4843
+ sdkMethod: "payloadContracts.infer",
4286
4844
  route: { method: "POST", path: "/v1/payload-contracts/infer" },
4287
4845
  commandClass: "read",
4288
4846
  flags: [INPUT_FLAG],
@@ -4305,14 +4863,12 @@ var contractsInferCommand = defineCommand({
4305
4863
  "--input with a non-empty example object is required (the JSON your system sends)."
4306
4864
  );
4307
4865
  }
4308
- const body = await rawRequest(ctx, {
4309
- method: "POST",
4310
- path: "/v1/payload-contracts/infer",
4311
- body: {
4866
+ const body = await ctx.client().payloadContracts.infer(
4867
+ asSdkInput({
4312
4868
  example,
4313
4869
  ...isEnvelope && input.subjectKind !== void 0 ? { subjectKind: input.subjectKind } : {}
4314
- }
4315
- });
4870
+ })
4871
+ );
4316
4872
  return { data: body };
4317
4873
  }
4318
4874
  });
@@ -4351,26 +4907,6 @@ var dataRunCommand = defineCommand({
4351
4907
 
4352
4908
  // src/skip-list.ts
4353
4909
  var SDK_SKIP_LIST = [
4354
- {
4355
- sdkPath: "automations.triggers.getContract",
4356
- reason: "covered by `automations triggers contract get` (raw route, bound pre-SDK-v9; SDK-method migration tracked separately)"
4357
- },
4358
- {
4359
- sdkPath: "automations.triggers.putContract",
4360
- reason: "covered by `automations triggers contract put` (raw route, bound pre-SDK-v9)"
4361
- },
4362
- {
4363
- sdkPath: "automations.triggers.validatePayload",
4364
- reason: "covered by `automations triggers contract validate` (raw route, bound pre-SDK-v9)"
4365
- },
4366
- {
4367
- sdkPath: "brand.getImages",
4368
- reason: "covered by `brand get-images` (raw route: SDK 10 cannot send `kind` or `sort`, brew-v2#1713; bind SDK 11.2)"
4369
- },
4370
- {
4371
- sdkPath: "payloadContracts.infer",
4372
- reason: "covered by `contracts infer` (raw route, bound pre-SDK-v9)"
4373
- },
4374
4910
  {
4375
4911
  sdkPath: "contacts.searchAll",
4376
4912
  reason: "auto-pager covered by `contacts search --all`"
@@ -4415,7 +4951,9 @@ function buildManifest() {
4415
4951
  "1": "API or runtime error",
4416
4952
  "2": "usage error",
4417
4953
  "3": "authentication error",
4418
- "4": "confirmation required (re-run with --yes)"
4954
+ "4": "confirmation required (re-run with --yes)",
4955
+ "130": "interrupted by SIGINT (Ctrl-C); the CLI then re-raises the signal",
4956
+ "143": "terminated by SIGTERM; the CLI then re-raises the signal"
4419
4957
  },
4420
4958
  confirmationProtocol: {
4421
4959
  exitCode: 4,
@@ -4523,6 +5061,7 @@ var doctorCommand = defineCommand({
4523
5061
  isReachable = health.status === "ok";
4524
5062
  apiVersion = health.version ?? null;
4525
5063
  } catch (error) {
5064
+ rethrowIfStopped(ctx);
4526
5065
  reachabilityError = error instanceof Error ? error.message : String(error);
4527
5066
  }
4528
5067
  let isAuthValid = null;
@@ -4533,6 +5072,7 @@ var doctorCommand = defineCommand({
4533
5072
  isAuthValid = true;
4534
5073
  plan = usage.plan?.key ?? null;
4535
5074
  } catch (error) {
5075
+ rethrowIfStopped(ctx);
4536
5076
  if ((error instanceof BrewApiError || error instanceof CliApiError) && (error.status === 401 || error.status === 403)) {
4537
5077
  isAuthValid = false;
4538
5078
  }
@@ -4558,6 +5098,7 @@ var doctorCommand = defineCommand({
4558
5098
  extraLocally = [...localOps].filter((op) => !liveOps.has(op));
4559
5099
  isSurfaceChecked = true;
4560
5100
  } catch {
5101
+ rethrowIfStopped(ctx);
4561
5102
  }
4562
5103
  }
4563
5104
  const isHealthy = isReachable && isAuthValid !== false && isSurfaceChecked && missingLocally.length === 0;
@@ -4595,6 +5136,9 @@ ${missingLocally.map((op) => ` ${op}`).join("\n")}` : "-- surface not chec
4595
5136
  };
4596
5137
  }
4597
5138
  });
5139
+ function rethrowIfStopped(ctx) {
5140
+ ctx.signal.throwIfAborted();
5141
+ }
4598
5142
 
4599
5143
  // src/commands/domains/add.ts
4600
5144
  var domainsAddCommand = defineCommand({
@@ -4745,8 +5289,7 @@ function renderDomains(rows) {
4745
5289
  var domainsUnsubscribesAddCommand = defineCommand({
4746
5290
  path: ["domains", "unsubscribes", "add"],
4747
5291
  summary: "Add addresses to a marketing domain's unsubscribe list (that domain only)",
4748
- sdkMethod: null,
4749
- isRawTransport: true,
5292
+ sdkMethod: "domains.unsubscribes.add",
4750
5293
  route: { method: "POST", path: "/v1/domains/{domainId}/unsubscribes" },
4751
5294
  commandClass: "write",
4752
5295
  args: [
@@ -4767,12 +5310,10 @@ var domainsUnsubscribesAddCommand = defineCommand({
4767
5310
  if (emails === void 0) {
4768
5311
  throw new CliUsageError("Pass at least one --email.");
4769
5312
  }
4770
- const body = await rawRequest(ctx, {
4771
- method: "POST",
4772
- path: `/v1/domains/${encodeURIComponent(args.domainId ?? "")}/unsubscribes`,
4773
- body: { emails },
4774
- idempotencyKey: flagString(flags.idempotencyKey)
4775
- });
5313
+ const body = await ctx.client().domains.unsubscribes.add(
5314
+ { domainId: args.domainId ?? "", emails: [...emails] },
5315
+ requestOptions(flags)
5316
+ );
4776
5317
  return { data: body };
4777
5318
  }
4778
5319
  });
@@ -4781,8 +5322,7 @@ var domainsUnsubscribesAddCommand = defineCommand({
4781
5322
  var domainsUnsubscribesExportCommand = defineCommand({
4782
5323
  path: ["domains", "unsubscribes", "export"],
4783
5324
  summary: "A domain's unsubscribe list as CSV (truncated when capped)",
4784
- sdkMethod: null,
4785
- isRawTransport: true,
5325
+ sdkMethod: "domains.unsubscribes.export",
4786
5326
  route: {
4787
5327
  method: "GET",
4788
5328
  path: "/v1/domains/{domainId}/unsubscribes/export"
@@ -4802,11 +5342,13 @@ var domainsUnsubscribesExportCommand = defineCommand({
4802
5342
  "brew-cli domains unsubscribes export dom_123 --scope all --json"
4803
5343
  ],
4804
5344
  run: async ({ ctx, args, flags }) => {
4805
- const body = await rawRequest(ctx, {
4806
- method: "GET",
4807
- path: `/v1/domains/${encodeURIComponent(args.domainId ?? "")}/unsubscribes/export`,
4808
- query: { scope: flagString(flags.scope) }
4809
- });
5345
+ const scope = flagString(flags.scope);
5346
+ const body = await ctx.client().domains.unsubscribes.export(
5347
+ asSdkInput({
5348
+ domainId: args.domainId ?? "",
5349
+ ...scope === void 0 ? {} : { scope }
5350
+ })
5351
+ );
4810
5352
  return { data: body, human: body.csv };
4811
5353
  }
4812
5354
  });
@@ -4815,8 +5357,7 @@ var domainsUnsubscribesExportCommand = defineCommand({
4815
5357
  var domainsUnsubscribesImportCommand = defineCommand({
4816
5358
  path: ["domains", "unsubscribes", "import"],
4817
5359
  summary: "Add every address in a CSV to a domain's unsubscribe list",
4818
- sdkMethod: null,
4819
- isRawTransport: true,
5360
+ sdkMethod: "domains.unsubscribes.import",
4820
5361
  route: {
4821
5362
  method: "POST",
4822
5363
  path: "/v1/domains/{domainId}/unsubscribes/import"
@@ -4844,12 +5385,14 @@ var domainsUnsubscribesImportCommand = defineCommand({
4844
5385
  );
4845
5386
  }
4846
5387
  const column = flagString(flags.column);
4847
- const body = await rawRequest(ctx, {
4848
- method: "POST",
4849
- path: `/v1/domains/${encodeURIComponent(args.domainId ?? "")}/unsubscribes/import`,
4850
- body: { csv, ...column === void 0 ? {} : { column } },
4851
- idempotencyKey: flagString(flags.idempotencyKey)
4852
- });
5388
+ const body = await ctx.client().domains.unsubscribes.import(
5389
+ {
5390
+ domainId: args.domainId ?? "",
5391
+ csv,
5392
+ ...column === void 0 ? {} : { column }
5393
+ },
5394
+ requestOptions(flags)
5395
+ );
4853
5396
  return { data: body };
4854
5397
  }
4855
5398
  });
@@ -4858,8 +5401,7 @@ var domainsUnsubscribesImportCommand = defineCommand({
4858
5401
  var domainsUnsubscribesListCommand = defineCommand({
4859
5402
  path: ["domains", "unsubscribes", "list"],
4860
5403
  summary: "One page of a marketing domain's unsubscribe list, newest first",
4861
- sdkMethod: null,
4862
- isRawTransport: true,
5404
+ sdkMethod: "domains.unsubscribes.list",
4863
5405
  route: { method: "GET", path: "/v1/domains/{domainId}/unsubscribes" },
4864
5406
  commandClass: "read",
4865
5407
  args: [
@@ -4880,16 +5422,18 @@ var domainsUnsubscribesListCommand = defineCommand({
4880
5422
  ],
4881
5423
  run: async ({ ctx, args, flags }) => {
4882
5424
  const limit = flagInt(flags.limit, "--limit");
4883
- const body = await rawRequest(ctx, {
4884
- method: "GET",
4885
- path: `/v1/domains/${encodeURIComponent(args.domainId ?? "")}/unsubscribes`,
4886
- query: {
4887
- q: flagString(flags.q),
4888
- scope: flagString(flags.scope),
4889
- limit: limit === void 0 ? void 0 : String(limit),
4890
- cursor: flagString(flags.cursor)
4891
- }
4892
- });
5425
+ const q = flagString(flags.q);
5426
+ const scope = flagString(flags.scope);
5427
+ const cursor = flagString(flags.cursor);
5428
+ const body = await ctx.client().domains.unsubscribes.list(
5429
+ asSdkInput({
5430
+ domainId: args.domainId ?? "",
5431
+ ...q === void 0 ? {} : { q },
5432
+ ...scope === void 0 ? {} : { scope },
5433
+ ...limit === void 0 ? {} : { limit },
5434
+ ...cursor === void 0 ? {} : { cursor }
5435
+ })
5436
+ );
4893
5437
  return { data: body, human: renderRows(body.data) };
4894
5438
  }
4895
5439
  });
@@ -4908,8 +5452,7 @@ function renderRows(rows) {
4908
5452
  var domainsUnsubscribesRemoveCommand = defineCommand({
4909
5453
  path: ["domains", "unsubscribes", "remove"],
4910
5454
  summary: "Take one address off a domain's unsubscribe list (never re-subscribes a brand-wide opt-out)",
4911
- sdkMethod: null,
4912
- isRawTransport: true,
5455
+ sdkMethod: "domains.unsubscribes.remove",
4913
5456
  route: {
4914
5457
  method: "DELETE",
4915
5458
  path: "/v1/domains/{domainId}/unsubscribes/{email}"
@@ -4922,9 +5465,9 @@ var domainsUnsubscribesRemoveCommand = defineCommand({
4922
5465
  flags: [],
4923
5466
  examples: ["brew-cli domains unsubscribes remove dom_123 ada@example.com"],
4924
5467
  run: async ({ ctx, args }) => ({
4925
- data: await rawRequest(ctx, {
4926
- method: "DELETE",
4927
- path: `/v1/domains/${encodeURIComponent(args.domainId ?? "")}/unsubscribes/${encodeURIComponent(args.email ?? "")}`
5468
+ data: await ctx.client().domains.unsubscribes.remove({
5469
+ domainId: args.domainId ?? "",
5470
+ email: args.email ?? ""
4928
5471
  })
4929
5472
  })
4930
5473
  });
@@ -5002,6 +5545,7 @@ var emailsAuditCommand = defineCommand({
5002
5545
  sdkMethod: "emails.auditEmail",
5003
5546
  route: { method: "POST", path: "/v1/emails/audit" },
5004
5547
  commandClass: "write",
5548
+ defaultTimeoutMs: AUDIT_EMAIL_DEFAULT_TIMEOUT_MS,
5005
5549
  isCredited: true,
5006
5550
  flags: [
5007
5551
  {
@@ -5045,19 +5589,11 @@ var emailsAuditCommand = defineCommand({
5045
5589
  "--file is required (a path, or - for stdin), or emailHtml via --input."
5046
5590
  );
5047
5591
  }
5048
- const signal = AbortSignal.timeout(AUDIT_EMAIL_DEFAULT_TIMEOUT_MS);
5049
- try {
5050
- const result = await ctx.client().emails.auditEmail(asSdkInput(input), {
5051
- signal,
5052
- ...requestOptions(flags) ?? {}
5053
- });
5054
- return { data: result };
5055
- } catch (error) {
5056
- if (signal.aborted) {
5057
- throw signal.reason;
5058
- }
5059
- throw error;
5060
- }
5592
+ const result = await ctx.client().emails.auditEmail(
5593
+ asSdkInput(input),
5594
+ requestOptions(flags)
5595
+ );
5596
+ return { data: result };
5061
5597
  }
5062
5598
  });
5063
5599
 
@@ -5195,8 +5731,6 @@ var emailsDeleteCommand = defineCommand({
5195
5731
  data: await ctx.client().emails.delete({ emailId: args.emailId ?? "" })
5196
5732
  })
5197
5733
  });
5198
-
5199
- // src/commands/emails/edit.ts
5200
5734
  var ENVELOPE_FIELDS = ["title", "subjectLine", "groupId"];
5201
5735
  function isText(value) {
5202
5736
  return typeof value === "string" && value !== "";
@@ -5207,6 +5741,7 @@ var emailsEditCommand = defineCommand({
5207
5741
  sdkMethod: "emails.edit",
5208
5742
  route: { method: "PATCH", path: "/v1/emails/{emailId}" },
5209
5743
  commandClass: "write",
5744
+ defaultTimeoutMs: EDIT_EMAIL_DEFAULT_TIMEOUT_MS,
5210
5745
  isCredited: true,
5211
5746
  args: [{ name: "emailId", summary: "Design id to edit", isRequired: true }],
5212
5747
  flags: [
@@ -5273,8 +5808,7 @@ var emailsEditCommand = defineCommand({
5273
5808
  isPromptEdit ? "Editing email\u2026 (typically 30-90s)" : envelope.length === 1 && envelope[0] === "subjectLine" ? "Setting the subject line\u2026" : "Updating the design (no AI run)\u2026"
5274
5809
  );
5275
5810
  const result = await ctx.client().emails.edit(
5276
- asSdkInput({ ...input, emailId: args.emailId ?? "" }),
5277
- { timeoutMs: 24e4 }
5811
+ asSdkInput({ ...input, emailId: args.emailId ?? "" })
5278
5812
  );
5279
5813
  return { data: result };
5280
5814
  }
@@ -5335,14 +5869,13 @@ var emailsExportCommand = defineCommand({
5335
5869
  };
5336
5870
  }
5337
5871
  });
5338
-
5339
- // src/commands/emails/generate.ts
5340
5872
  var emailsGenerateCommand = defineCommand({
5341
5873
  path: ["emails", "generate"],
5342
5874
  summary: "Generate a new on-brand email design from a prompt",
5343
5875
  sdkMethod: "emails.generate",
5344
5876
  route: { method: "POST", path: "/v1/emails" },
5345
5877
  commandClass: "write",
5878
+ defaultTimeoutMs: GENERATE_EMAIL_DEFAULT_TIMEOUT_MS,
5346
5879
  isCredited: true,
5347
5880
  flags: [
5348
5881
  { flag: "--prompt <text>", summary: "What the email should be" },
@@ -5386,10 +5919,10 @@ var emailsGenerateCommand = defineCommand({
5386
5919
  );
5387
5920
  }
5388
5921
  progress(ctx, "Generating email\u2026 (typically 30-90s)");
5389
- const result = await ctx.client().emails.generate(asSdkInput(input), {
5390
- timeoutMs: 24e4,
5391
- ...requestOptions(flags) ?? {}
5392
- });
5922
+ const result = await ctx.client().emails.generate(
5923
+ asSdkInput(input),
5924
+ requestOptions(flags)
5925
+ );
5393
5926
  return { data: result };
5394
5927
  }
5395
5928
  });
@@ -5438,16 +5971,11 @@ var emailsGetCommand = defineCommand({
5438
5971
  "Pass --email-version-id or --run-id, not both: each selects the version to read."
5439
5972
  );
5440
5973
  }
5441
- if (emailVersionId === void 0 && runId === void 0) {
5442
- return {
5443
- data: await ctx.client().emails.get(emailId, include === void 0 ? void 0 : { include })
5444
- };
5445
- }
5446
5974
  return {
5447
- data: await rawRequest(ctx, {
5448
- method: "GET",
5449
- path: `/v1/emails/${encodeURIComponent(emailId)}`,
5450
- query: { include, emailVersionId, runId }
5975
+ data: await ctx.client().emails.get(emailId, {
5976
+ ...include === void 0 ? {} : { include },
5977
+ ...emailVersionId === void 0 ? {} : { emailVersionId },
5978
+ ...runId === void 0 ? {} : { runId }
5451
5979
  })
5452
5980
  };
5453
5981
  }
@@ -5457,8 +5985,7 @@ var emailsGetCommand = defineCommand({
5457
5985
  var emailsGetAuditCommand = defineCommand({
5458
5986
  path: ["emails", "get-audit"],
5459
5987
  summary: "Read a saved email audit: one page of its findings (free; never reruns the audit)",
5460
- sdkMethod: null,
5461
- isRawTransport: true,
5988
+ sdkMethod: "emails.getAudit",
5462
5989
  route: { method: "GET", path: "/v1/emails/audits/{auditId}" },
5463
5990
  commandClass: "read",
5464
5991
  args: [
@@ -5482,13 +6009,10 @@ var emailsGetAuditCommand = defineCommand({
5482
6009
  // says whether one exists.
5483
6010
  run: async ({ ctx, args, flags }) => {
5484
6011
  const limit = flagInt(flags.limit, "--limit");
5485
- const body = await rawRequest(ctx, {
5486
- method: "GET",
5487
- path: `/v1/emails/audits/${encodeURIComponent(args.auditId ?? "")}`,
5488
- query: {
5489
- cursor: flagString(flags.cursor),
5490
- limit: limit === void 0 ? void 0 : String(limit)
5491
- }
6012
+ const cursor = flagString(flags.cursor);
6013
+ const body = await ctx.client().emails.getAudit(args.auditId ?? "", {
6014
+ ...cursor === void 0 ? {} : { cursor },
6015
+ ...limit === void 0 ? {} : { limit }
5492
6016
  });
5493
6017
  return { data: body };
5494
6018
  }
@@ -5498,8 +6022,7 @@ var emailsGetAuditCommand = defineCommand({
5498
6022
  var emailsGetClientPreviewCommand = defineCommand({
5499
6023
  path: ["emails", "get-client-preview"],
5500
6024
  summary: "Poll a client-preview rendering job: per-client screenshot links once it settles (free; never re-renders)",
5501
- sdkMethod: null,
5502
- isRawTransport: true,
6025
+ sdkMethod: "emails.getClientPreview",
5503
6026
  route: { method: "GET", path: "/v1/emails/client-previews/{previewId}" },
5504
6027
  commandClass: "read",
5505
6028
  args: [
@@ -5518,10 +6041,7 @@ var emailsGetClientPreviewCommand = defineCommand({
5518
6041
  // `failed`; each client then carries its `imageUrl`, or a `reason` and
5519
6042
  // whether it is `retryable`.
5520
6043
  run: async ({ ctx, args }) => {
5521
- const body = await rawRequest(ctx, {
5522
- method: "GET",
5523
- path: `/v1/emails/client-previews/${encodeURIComponent(args.previewId ?? "")}`
5524
- });
6044
+ const body = await ctx.client().emails.getClientPreview(args.previewId ?? "");
5525
6045
  return { data: body };
5526
6046
  }
5527
6047
  });
@@ -5735,14 +6255,13 @@ var emailsGroupsUpdateCommand = defineCommand({
5735
6255
  };
5736
6256
  }
5737
6257
  });
5738
-
5739
- // src/commands/emails/import.ts
5740
6258
  var emailsImportCommand = defineCommand({
5741
6259
  path: ["emails", "import"],
5742
6260
  summary: "Import existing HTML, MJML, or JSX as a new editable design",
5743
6261
  sdkMethod: "emails.import",
5744
6262
  route: { method: "POST", path: "/v1/emails/import" },
5745
6263
  commandClass: "write",
6264
+ defaultTimeoutMs: IMPORT_EMAIL_DEFAULT_TIMEOUT_MS,
5746
6265
  flags: [
5747
6266
  {
5748
6267
  flag: "--file <path>",
@@ -5788,14 +6307,13 @@ var emailsImportCommand = defineCommand({
5788
6307
  return { data: result };
5789
6308
  }
5790
6309
  });
5791
-
5792
- // src/commands/emails/import-figma.ts
5793
6310
  var emailsImportFigmaCommand = defineCommand({
5794
6311
  path: ["emails", "import-figma"],
5795
6312
  summary: "Convert one Figma frame into an editable design (deterministic, free)",
5796
6313
  sdkMethod: "emails.importFigma",
5797
6314
  route: { method: "POST", path: "/v1/emails/figma" },
5798
6315
  commandClass: "write",
6316
+ defaultTimeoutMs: IMPORT_FIGMA_DEFAULT_TIMEOUT_MS,
5799
6317
  flags: [
5800
6318
  {
5801
6319
  flag: "--url <figmaUrl>",
@@ -5896,6 +6414,10 @@ var emailsListCommand = defineCommand({
5896
6414
  route: { method: "GET", path: "/v1/emails" },
5897
6415
  commandClass: "read",
5898
6416
  flags: [
6417
+ {
6418
+ flag: "--search <text>",
6419
+ summary: "Only designs whose title contains this text, or whose title, subject, preview or text matches its words"
6420
+ },
5899
6421
  {
5900
6422
  flag: "--status <status>",
5901
6423
  summary: "Filter by status: generating | ready | failed"
@@ -5955,6 +6477,7 @@ var emailsListCommand = defineCommand({
5955
6477
  );
5956
6478
  const base = await readJsonFlag(ctx, flags.input, "--input");
5957
6479
  const input = mergeInput(base, {
6480
+ search: flagString(flags.search),
5958
6481
  status: flagString(flags.status),
5959
6482
  groupId: flagString(flags.groupId),
5960
6483
  sortBy: flagString(flags.sortBy) ?? flagString(flags.sort) ?? legacy.sortBy,
@@ -6029,14 +6552,13 @@ function renderEmails(rows) {
6029
6552
  { key: "updatedAt", header: "UPDATED" }
6030
6553
  ]);
6031
6554
  }
6032
-
6033
- // src/commands/emails/preview-clients.ts
6034
6555
  var emailsPreviewClientsCommand = defineCommand({
6035
6556
  path: ["emails", "preview-clients"],
6036
6557
  summary: "Start a rendering job across real email clients (10 credits); poll it with `emails get-client-preview`",
6037
6558
  sdkMethod: "emails.previewClients",
6038
6559
  route: { method: "POST", path: "/v1/emails/{emailId}/client-previews" },
6039
6560
  commandClass: "write",
6561
+ defaultTimeoutMs: PREVIEW_EMAIL_CLIENTS_DEFAULT_TIMEOUT_MS,
6040
6562
  isCredited: true,
6041
6563
  args: [
6042
6564
  { name: "emailId", summary: "Design id to preview", isRequired: true }
@@ -6616,7 +7138,7 @@ var loginCommand = defineCommand({
6616
7138
  globals: { ...ctx.globals, apiKey: candidate },
6617
7139
  env: ctx.io.env
6618
7140
  });
6619
- await buildSdkClient(auth).usage.get();
7141
+ await buildSdkClient(auth, sdkTransportFor(ctx)).usage.get();
6620
7142
  const path = updateConfig(ctx.io.env, { apiKey: candidate });
6621
7143
  progress(ctx, `Logged in. Key stored in ${path} (0600).`);
6622
7144
  return {
@@ -6743,8 +7265,7 @@ var sendsResumeCommand = defineCommand({
6743
7265
  var templatesGetCommand = defineCommand({
6744
7266
  path: ["templates", "get"],
6745
7267
  summary: "Fetch one public template: its links and the referenceEmailId to remix; --include html adds its HTML",
6746
- sdkMethod: null,
6747
- isRawTransport: true,
7268
+ sdkMethod: "templates.get",
6748
7269
  route: { method: "GET", path: "/v1/templates/{templateId}" },
6749
7270
  commandClass: "read",
6750
7271
  args: [
@@ -6765,11 +7286,11 @@ var templatesGetCommand = defineCommand({
6765
7286
  "brew-cli templates get seed-vercel-newsletter --include html --json"
6766
7287
  ],
6767
7288
  run: async ({ ctx, args, flags }) => {
6768
- const body = await rawRequest(ctx, {
6769
- method: "GET",
6770
- path: `/v1/templates/${encodeURIComponent(args.templateId ?? "")}`,
6771
- query: { include: flagString(flags.include) }
6772
- });
7289
+ const include = flagString(flags.include);
7290
+ const body = await ctx.client().templates.get(
7291
+ args.templateId ?? "",
7292
+ include === void 0 ? void 0 : { include: asSdkInput(include) }
7293
+ );
6773
7294
  return { data: body };
6774
7295
  }
6775
7296
  });
@@ -7055,6 +7576,7 @@ var whoamiCommand = defineCommand({
7055
7576
  if (error instanceof BrewApiError && (error.status === 401 || error.status === 403)) {
7056
7577
  throw error;
7057
7578
  }
7579
+ ctx.signal.throwIfAborted();
7058
7580
  progress(
7059
7581
  ctx,
7060
7582
  "Warning: could not reach /v1/usage \u2014 showing local credential state only."
@@ -7085,6 +7607,7 @@ var ALL_COMMANDS = [
7085
7607
  contactsSearchCommand,
7086
7608
  contactsGetCommand,
7087
7609
  contactsCountCommand,
7610
+ contactsCountByCommand,
7088
7611
  contactsUpsertCommand,
7089
7612
  contactsUpsertManyCommand,
7090
7613
  contactsUpdateCommand,
@@ -7251,6 +7774,7 @@ Environment:
7251
7774
  Exit codes:
7252
7775
  0 success \xB7 1 API/runtime error \xB7 2 usage error \xB7 3 auth error
7253
7776
  4 confirmation required (re-run with --yes)
7777
+ 130 interrupted (SIGINT) \xB7 143 terminated (SIGTERM)
7254
7778
 
7255
7779
  Agents: run \`${CLI_NAME} docs --agent\` for the machine-readable manifest.
7256
7780
  `;
@@ -7268,8 +7792,8 @@ async function run(argv, io, commands = ALL_COMMANDS) {
7268
7792
  (text) => {
7269
7793
  commanderStderr.push(text);
7270
7794
  },
7271
- (code) => {
7272
- commandExitCode = code;
7795
+ (code2) => {
7796
+ commandExitCode = code2;
7273
7797
  }
7274
7798
  );
7275
7799
  const flushCommanderStderr = () => {
@@ -7289,9 +7813,51 @@ async function run(argv, io, commands = ALL_COMMANDS) {
7289
7813
  }
7290
7814
  flushCommanderStderr();
7291
7815
  const ctx = activeContext ?? makeFallbackContext(io, argv);
7292
- printError(ctx, error);
7293
- return toExitCode(error);
7816
+ const reported = reportedError(ctx, error);
7817
+ printError(ctx, reported, errorContextFor(ctx, commands));
7818
+ return toExitCode(reported);
7819
+ }
7820
+ }
7821
+ function reportedError(ctx, error) {
7822
+ if (!ctx.signal.aborted) {
7823
+ return error;
7824
+ }
7825
+ if (error instanceof BrewApiError || error instanceof CliApiError) {
7826
+ return error;
7294
7827
  }
7828
+ return ctx.signal.reason;
7829
+ }
7830
+ function errorContextFor(ctx, commands) {
7831
+ const request = ctx.transport.lastRequest();
7832
+ const drain = ctx.transport.drain();
7833
+ const bound = request === void 0 ? void 0 : commandFor(request, commands);
7834
+ const isRead = bound === void 0 ? ctx.traits?.isRead : bound.commandClass === "read";
7835
+ return {
7836
+ ...request === void 0 ? {} : { request },
7837
+ ...drain === void 0 ? {} : { drain },
7838
+ ...isRead === void 0 ? {} : { isRead },
7839
+ replays: bound === void 0 ? ctx.traits?.replays === true : (bound.flags ?? []).includes(IDEMPOTENCY_FLAG),
7840
+ readsStdin: readsStdin(ctx.rawArgv),
7841
+ retryCommand: (key) => buildRetryCommand(ctx.rawArgv, key)
7842
+ };
7843
+ }
7844
+ function commandFor(request, commands) {
7845
+ const apiPath = request.path.split("?")[0] ?? "";
7846
+ return commands.find(
7847
+ (spec) => spec.route !== void 0 && spec.route.method === request.method && matchesTemplate(spec.route.path, apiPath)
7848
+ );
7849
+ }
7850
+ function readsStdin(rawArgv) {
7851
+ return rawArgv.some(
7852
+ (token, index) => token === "-" && ["--input", "--file", "--data"].includes(rawArgv[index - 1] ?? "")
7853
+ );
7854
+ }
7855
+ function traitsOf(spec) {
7856
+ return {
7857
+ isRead: spec.commandClass === "read",
7858
+ replays: (spec.flags ?? []).includes(IDEMPOTENCY_FLAG),
7859
+ defaultTimeoutMs: spec.defaultTimeoutMs
7860
+ };
7295
7861
  }
7296
7862
  function handleCommanderError(error, ctx, flush) {
7297
7863
  const isHelpCode = error.code === "commander.helpDisplayed" || error.code === "commander.help";
@@ -7380,16 +7946,21 @@ function registerCommand(program, spec, io, rawArgv, onContext, onExitCode) {
7380
7946
  },
7381
7947
  {}
7382
7948
  );
7383
- const ctx = makeContext({ io, flags, rawArgv });
7949
+ const ctx = makeContext({ io, flags, rawArgv, traits: traitsOf(spec) });
7384
7950
  onContext?.(ctx);
7385
7951
  const invocation = { ctx, args: positional, flags };
7386
7952
  await enforceConfirmation(spec, invocation);
7387
- const outcome = await spec.run(invocation);
7388
- if (outcome !== void 0) {
7389
- printData(ctx, outcome.data, outcome.human);
7390
- if (outcome.exitCode !== void 0 && outcome.exitCode !== 0) {
7391
- onExitCode?.(outcome.exitCode);
7953
+ ctx.startDeadline();
7954
+ try {
7955
+ const outcome = await spec.run(invocation);
7956
+ if (outcome !== void 0) {
7957
+ printData(ctx, outcome.data, outcome.human);
7958
+ if (outcome.exitCode !== void 0 && outcome.exitCode !== 0) {
7959
+ onExitCode?.(outcome.exitCode);
7960
+ }
7392
7961
  }
7962
+ } finally {
7963
+ ctx.stopDeadline();
7393
7964
  }
7394
7965
  });
7395
7966
  }
@@ -7428,20 +7999,77 @@ function buildCommandHelpFooter(spec) {
7428
7999
  return lines.join("\n");
7429
8000
  }
7430
8001
 
8002
+ // src/lib/interrupt.ts
8003
+ function createInterrupter(input) {
8004
+ const controller = new AbortController();
8005
+ const graceMs = input.graceMs ?? 3e3;
8006
+ let first;
8007
+ let grace;
8008
+ const forceExit = (signal, why) => {
8009
+ input.stderr.write(`
8010
+ brew-cli: ${why}; exiting without waiting.
8011
+ `);
8012
+ input.exit(exitCodeForSignal(signal));
8013
+ };
8014
+ const trigger = (signal) => {
8015
+ if (first !== void 0) {
8016
+ forceExit(signal, `${signal} received twice`);
8017
+ return;
8018
+ }
8019
+ first = signal;
8020
+ controller.abort(new CliInterruptError(signal));
8021
+ grace = setTimeout(() => {
8022
+ forceExit(signal, `still stopping ${graceMs / 1e3}s after ${signal}`);
8023
+ }, graceMs);
8024
+ grace.unref?.();
8025
+ };
8026
+ input.source.on("SIGINT", () => {
8027
+ trigger("SIGINT");
8028
+ });
8029
+ input.source.on("SIGTERM", () => {
8030
+ trigger("SIGTERM");
8031
+ });
8032
+ return {
8033
+ signal: controller.signal,
8034
+ trigger,
8035
+ received: () => first,
8036
+ settle: () => {
8037
+ if (grace !== void 0) {
8038
+ clearTimeout(grace);
8039
+ }
8040
+ }
8041
+ };
8042
+ }
8043
+
7431
8044
  // src/bin.ts
7432
- function realIo() {
8045
+ function realIo(interrupter2) {
8046
+ const { signal } = interrupter2;
7433
8047
  return {
7434
8048
  stdout: process.stdout,
7435
8049
  stderr: process.stderr,
7436
8050
  isTtyOut: process.stdout.isTTY === true,
7437
8051
  isTtyIn: process.stdin.isTTY === true,
7438
8052
  env: process.env,
8053
+ signal,
7439
8054
  readStdin: async () => {
8055
+ const stop = () => {
8056
+ process.stdin.destroy();
8057
+ };
8058
+ signal.addEventListener("abort", stop, { once: true });
7440
8059
  process.stdin.setEncoding("utf8");
7441
8060
  let data = "";
7442
- for await (const chunk of process.stdin) {
7443
- data += chunk;
8061
+ try {
8062
+ for await (const chunk of process.stdin) {
8063
+ data += chunk;
8064
+ }
8065
+ } catch (error) {
8066
+ if (!signal.aborted) {
8067
+ throw error;
8068
+ }
8069
+ } finally {
8070
+ signal.removeEventListener("abort", stop);
7444
8071
  }
8072
+ signal.throwIfAborted();
7445
8073
  return data;
7446
8074
  },
7447
8075
  readLine: async (prompt) => {
@@ -7449,14 +8077,34 @@ function realIo() {
7449
8077
  input: process.stdin,
7450
8078
  output: process.stderr
7451
8079
  });
8080
+ rl.on("SIGINT", () => {
8081
+ interrupter2.trigger("SIGINT");
8082
+ });
7452
8083
  try {
7453
- return await rl.question(prompt);
8084
+ return await rl.question(prompt, { signal });
7454
8085
  } finally {
7455
8086
  rl.close();
7456
8087
  }
7457
8088
  }
7458
8089
  };
7459
8090
  }
7460
- process.exitCode = await run(process.argv.slice(2), realIo());
8091
+ var interrupter = createInterrupter({
8092
+ source: process,
8093
+ stderr: process.stderr,
8094
+ exit: (code2) => process.exit(code2)
8095
+ });
8096
+ var code = await run(process.argv.slice(2), realIo(interrupter));
8097
+ interrupter.settle();
8098
+ process.exitCode = code;
8099
+ var received = interrupter.received();
8100
+ if (received !== void 0 && code === exitCodeForSignal(received) && process.platform !== "win32") {
8101
+ await new Promise((resolve) => {
8102
+ process.stderr.write("", () => {
8103
+ resolve();
8104
+ });
8105
+ });
8106
+ process.removeAllListeners(received);
8107
+ process.kill(process.pid, received);
8108
+ }
7461
8109
  //# sourceMappingURL=bin.js.map
7462
8110
  //# sourceMappingURL=bin.js.map