@brew.new/cli 0.8.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.8.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) {
@@ -653,10 +967,69 @@ var IDEMPOTENCY_FLAG = {
653
967
  flag: "--idempotency-key <key>",
654
968
  summary: "Idempotency-Key for safe retries (auto-generated otherwise)"
655
969
  };
970
+ var NON_REPLAYING_IDEMPOTENCY_FLAG = {
971
+ flag: "--idempotency-key <key>",
972
+ summary: "Accepted for compatibility only: this route never replays, so a retry runs again"
973
+ };
656
974
  var INPUT_FLAG = {
657
975
  flag: "--input <json>",
658
976
  summary: "Full JSON request body, or - to read stdin (flags override it)"
659
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
+ }
660
1033
 
661
1034
  // src/lib/paginate.ts
662
1035
  async function collectAll(ctx, fetchPage) {
@@ -664,7 +1037,20 @@ async function collectAll(ctx, fetchPage) {
664
1037
  let cursor;
665
1038
  let pageCount = 0;
666
1039
  for (; ; ) {
667
- 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
+ }
668
1054
  rows.push(...page.data);
669
1055
  pageCount += 1;
670
1056
  const next = page.pagination?.cursor ?? null;
@@ -857,6 +1243,106 @@ var analyticsCampaignsCommand = defineCommand({
857
1243
  }
858
1244
  });
859
1245
 
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
+ };
1251
+ var analyticsEventCountsCommand = defineCommand({
1252
+ path: ["analytics", "event-counts"],
1253
+ summary: "Count email events per field and/or period (clicks per link, events per day, unsubscribe reasons)",
1254
+ sdkMethod: "analytics.eventCounts",
1255
+ route: { method: "GET", path: "/v1/analytics/events" },
1256
+ commandClass: "read",
1257
+ flags: [
1258
+ {
1259
+ flag: "--group-by <fields>",
1260
+ summary: "One or two comma-separated fields: eventType, emailId, automationId, sendId, source, link, recipientDomain, unsubscribeReason"
1261
+ },
1262
+ {
1263
+ flag: "--bucket <period>",
1264
+ summary: "Count per UTC day, week (Monday start) or month"
1265
+ },
1266
+ { flag: "--since <datetime>", summary: "Window start (ISO-8601)" },
1267
+ { flag: "--until <datetime>", summary: "Window end (ISO-8601)" },
1268
+ {
1269
+ flag: "--event-type <type>",
1270
+ summary: "Only one email event type (e.g. clicked)"
1271
+ },
1272
+ { flag: "--send-id <sendId>", summary: "Only one send" },
1273
+ { flag: "--email-id <emailId>", summary: "Only one design" },
1274
+ {
1275
+ flag: "--recipient <rules>",
1276
+ summary: "CSV of recipient rules (max 10): an address, @domain, or substring; prefix ! to exclude"
1277
+ },
1278
+ INPUT_FLAG
1279
+ ],
1280
+ examples: [
1281
+ "brew-cli analytics event-counts --send-id snd_123 --event-type clicked --group-by link",
1282
+ "brew-cli analytics event-counts --group-by eventType --bucket day",
1283
+ "brew-cli analytics event-counts --group-by unsubscribeReason --json"
1284
+ ],
1285
+ run: async ({ ctx, flags }) => {
1286
+ const base = await readJsonFlag(ctx, flags.input, "--input");
1287
+ const input = mergeInput(base, {
1288
+ groupBy: flagString(flags.groupBy),
1289
+ bucket: flagString(flags.bucket),
1290
+ from: flagString(flags.since),
1291
+ to: flagString(flags.until),
1292
+ eventType: flagString(flags.eventType),
1293
+ sendId: flagString(flags.sendId),
1294
+ emailId: flagString(flags.emailId),
1295
+ recipient: flagString(flags.recipient)
1296
+ });
1297
+ if (input.groupBy === void 0 && input.bucket === void 0) {
1298
+ throw new CliUsageError(
1299
+ "Pass --group-by, --bucket, or both (`analytics events` lists the rows)."
1300
+ );
1301
+ }
1302
+ for (const [key, reason] of Object.entries(ROW_ONLY_KEYS)) {
1303
+ if (input[key] !== void 0) {
1304
+ throw new CliUsageError(reason);
1305
+ }
1306
+ }
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
+ );
1314
+ return { data: body, human: renderCounts(body) };
1315
+ }
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
+ }
1323
+ function renderCounts(body) {
1324
+ if (body.groups.length === 0) {
1325
+ return `No events counted (total ${body.count}).`;
1326
+ }
1327
+ const rows = body.groups.map((group) => ({
1328
+ group: Object.entries(group.key ?? {}).map(([field, value]) => `${field}=${String(value)}`).join(" "),
1329
+ ...group.bucket === void 0 ? {} : { bucket: group.bucket },
1330
+ count: group.count
1331
+ }));
1332
+ const table = renderTable(rows, [
1333
+ ...rows.some((row) => row.group !== "") ? [{ key: "group", header: "GROUP" }] : [],
1334
+ ...rows.some((row) => "bucket" in row) ? [{ key: "bucket", header: "PERIOD" }] : [],
1335
+ { key: "count", header: "COUNT" }
1336
+ ]);
1337
+ const footer = [
1338
+ `total ${body.count}`,
1339
+ body.otherCount > 0 ? `${body.otherCount} in unlisted groups` : "",
1340
+ body.truncated ? `truncated (counted from ${body.coveredFrom ?? "?"})` : ""
1341
+ ].filter(Boolean).join(" \xB7 ");
1342
+ return `${table}
1343
+ ${footer}`;
1344
+ }
1345
+
860
1346
  // src/commands/analytics/events.ts
861
1347
  var analyticsEventsCommand = defineCommand({
862
1348
  path: ["analytics", "events"],
@@ -1206,6 +1692,66 @@ var analyticsTriggerInstancesListCommand = defineCommand({
1206
1692
  });
1207
1693
 
1208
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
+ }
1209
1755
  async function rawRequest(ctx, request) {
1210
1756
  const auth = resolveAuth({
1211
1757
  globals: ctx.globals,
@@ -1229,13 +1775,13 @@ async function rawRequest(ctx, request) {
1229
1775
  url.searchParams.set(key, value);
1230
1776
  }
1231
1777
  }
1232
- const response = await fetch(url, {
1778
+ const { response, text } = await rawFetch(ctx, {
1233
1779
  method: request.method,
1780
+ url: url.toString(),
1234
1781
  headers,
1235
1782
  ...hasBody ? { body: JSON.stringify(request.body) } : {},
1236
- ...request.signal === void 0 ? {} : { signal: request.signal }
1783
+ timeoutMs: ctx.budget.attemptMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS
1237
1784
  });
1238
- const text = await response.text();
1239
1785
  const parsed = tryParseJson(text);
1240
1786
  if (!response.ok) {
1241
1787
  throw responseToApiError(response, parsed);
@@ -1337,7 +1883,7 @@ var apiCommand = defineCommand({
1337
1883
  },
1338
1884
  {
1339
1885
  flag: "--idempotency-key <key>",
1340
- summary: "Idempotency-Key header for safe POST retries"
1886
+ summary: "Idempotency-Key header for safe POST retries (a POST gets one generated otherwise)"
1341
1887
  }
1342
1888
  ],
1343
1889
  examples: [
@@ -1369,7 +1915,7 @@ var apiCommand = defineCommand({
1369
1915
  auth,
1370
1916
  path,
1371
1917
  hasJsonBody: body !== void 0,
1372
- 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
1373
1919
  });
1374
1920
  for (const header of readHeaderFlags(flags.header)) {
1375
1921
  const separator = header.indexOf(":");
@@ -1383,12 +1929,16 @@ var apiCommand = defineCommand({
1383
1929
  header.slice(separator + 1).trim()
1384
1930
  );
1385
1931
  }
1386
- const response = await fetch(`${auth.apiUrl}${path}`, {
1932
+ const { response, text } = await rawFetch(ctx, {
1387
1933
  method,
1934
+ url: `${auth.apiUrl}${path}`,
1388
1935
  headers,
1389
- ...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
1390
1941
  });
1391
- const text = await response.text();
1392
1942
  const parsed = tryParseJson(text);
1393
1943
  if (!response.ok) {
1394
1944
  throw responseToApiError(response, parsed);
@@ -1451,7 +2001,9 @@ var apiKeysCreateCommand = defineCommand({
1451
2001
  summary: "Bind the NEW key to this brand id (omit for an organization-wide key); not the acting --brand"
1452
2002
  },
1453
2003
  INPUT_FLAG,
1454
- IDEMPOTENCY_FLAG
2004
+ // The route never replays (a replay would disclose the plaintext key
2005
+ // again): a retry mints a second key, whatever the key says.
2006
+ NON_REPLAYING_IDEMPOTENCY_FLAG
1455
2007
  ],
1456
2008
  examples: [
1457
2009
  "brew-cli api-keys create --name CI --permissions emails domains",
@@ -1495,9 +2047,31 @@ var apiKeysListCommand = defineCommand({
1495
2047
  sdkMethod: "apiKeys.list",
1496
2048
  route: { method: "GET", path: "/v1/api-keys" },
1497
2049
  commandClass: "read",
2050
+ flags: [LIMIT_FLAG, CURSOR_FLAG, ALL_FLAG],
1498
2051
  examples: ["brew-cli api-keys list", "brew-cli api-keys list --json"],
1499
- run: async ({ ctx }) => {
1500
- const result = await ctx.client().apiKeys.list();
2052
+ run: async ({ ctx, flags }) => {
2053
+ const limit = flagInt(flags.limit, "--limit");
2054
+ const cursor = flagString(flags.cursor);
2055
+ if (limit === void 0 && cursor === void 0 && flags.all !== true) {
2056
+ const result2 = await ctx.client().apiKeys.list();
2057
+ return { data: result2, human: renderApiKeys(result2.data) };
2058
+ }
2059
+ const page = (after) => rawRequest(ctx, {
2060
+ method: "GET",
2061
+ path: "/v1/api-keys",
2062
+ query: {
2063
+ limit: limit === void 0 ? void 0 : String(limit),
2064
+ cursor: after
2065
+ }
2066
+ });
2067
+ if (flags.all === true) {
2068
+ const rows = await collectAll(ctx, (next) => page(next ?? cursor));
2069
+ return {
2070
+ data: { data: rows, pagination: { cursor: null, hasMore: false } },
2071
+ human: renderApiKeys(rows)
2072
+ };
2073
+ }
2074
+ const result = await page(cursor);
1501
2075
  return { data: result, human: renderApiKeys(result.data) };
1502
2076
  }
1503
2077
  });
@@ -1694,6 +2268,10 @@ var audiencesListCommand = defineCommand({
1694
2268
  flag: "--include <tokens>",
1695
2269
  summary: "0.6 flag: includes ride the detail read now (`audiences get --include count,build`)"
1696
2270
  },
2271
+ {
2272
+ flag: "--search <text>",
2273
+ summary: "Only audiences whose name contains this text"
2274
+ },
1697
2275
  LIMIT_FLAG,
1698
2276
  CURSOR_FLAG,
1699
2277
  ALL_FLAG,
@@ -1710,6 +2288,7 @@ var audiencesListCommand = defineCommand({
1710
2288
  throw includeRidesDetailRead("audiences get");
1711
2289
  }
1712
2290
  const input = mergeInput(base, {
2291
+ search: flagString(flags.search),
1713
2292
  limit: flagInt(flags.limit, "--limit"),
1714
2293
  cursor: flagString(flags.cursor)
1715
2294
  });
@@ -1748,23 +2327,41 @@ function renderAudiences(rows) {
1748
2327
  // src/commands/audiences/update.ts
1749
2328
  var audiencesUpdateCommand = defineCommand({
1750
2329
  path: ["audiences", "update"],
1751
- summary: "Update an audience segment (name and/or filters)",
2330
+ summary: "Update an audience segment: its name, its filters, or its members by email",
1752
2331
  sdkMethod: "audiences.update",
1753
2332
  route: { method: "PATCH", path: "/v1/audiences/{audienceId}" },
1754
2333
  commandClass: "write",
1755
2334
  args: [
1756
2335
  { name: "audienceId", summary: "Audience id to update", isRequired: true }
1757
2336
  ],
1758
- 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
+ ],
1759
2349
  examples: [
1760
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",
1761
2352
  `brew-cli audiences update aud_3k9sQ --input '{"filters":{"filters":[{"field":"plan","operator":"equals","value":"vip"}],"logicalOperator":"and"}}'`
1762
2353
  ],
1763
2354
  run: async ({ ctx, args, flags }) => {
1764
2355
  const base = await readJsonFlag(ctx, flags.input, "--input");
1765
- 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
+ });
1766
2361
  if (Object.keys(input).length === 0) {
1767
- 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
+ );
1768
2365
  }
1769
2366
  const result = await ctx.client().audiences.update(
1770
2367
  asSdkInput({
@@ -2117,7 +2714,16 @@ var automationsListCommand = defineCommand({
2117
2714
  sdkMethod: "automations.list",
2118
2715
  route: { method: "GET", path: "/v1/automations" },
2119
2716
  commandClass: "read",
2120
- 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
+ ],
2121
2727
  examples: [
2122
2728
  "brew-cli automations list",
2123
2729
  "brew-cli automations list --all --json"
@@ -2125,6 +2731,7 @@ var automationsListCommand = defineCommand({
2125
2731
  run: async ({ ctx, flags }) => {
2126
2732
  const base = await readJsonFlag(ctx, flags.input, "--input");
2127
2733
  const input = mergeInput(base, {
2734
+ search: flagString(flags.search),
2128
2735
  limit: flagInt(flags.limit, "--limit"),
2129
2736
  cursor: flagString(flags.cursor)
2130
2737
  });
@@ -2513,17 +3120,14 @@ var FORMAT_FLAG = {
2513
3120
  var FORMATS = /* @__PURE__ */ new Set(["json", "ts", "zod", "jsonschema", "skill"]);
2514
3121
  var ENFORCEMENT_MODES = /* @__PURE__ */ new Set(["off", "prune", "strict"]);
2515
3122
  var VALIDATE_ENFORCEMENT_MODES = /* @__PURE__ */ new Set(["prune", "strict"]);
2516
- function formatQuery(value) {
3123
+ function formatOption(value) {
2517
3124
  const format = flagString(value);
2518
- if (format === void 0) {
2519
- return "";
2520
- }
2521
- if (!FORMATS.has(format)) {
3125
+ if (format !== void 0 && !FORMATS.has(format)) {
2522
3126
  throw new CliUsageError(
2523
3127
  `Unknown --format '${format}' (expected json | ts | zod | jsonschema | skill).`
2524
3128
  );
2525
3129
  }
2526
- return `?format=${format}`;
3130
+ return format;
2527
3131
  }
2528
3132
  function contentHuman(body) {
2529
3133
  return typeof body.content === "string" ? { human: body.content } : {};
@@ -2531,8 +3135,7 @@ function contentHuman(body) {
2531
3135
  var automationsTriggersContractGetCommand = defineCommand({
2532
3136
  path: ["automations", "triggers", "contract", "get"],
2533
3137
  summary: "Read a trigger payload contract: stored when declared, derived otherwise; --format renders ts/zod/jsonschema/skill",
2534
- sdkMethod: null,
2535
- isRawTransport: true,
3138
+ sdkMethod: "automations.triggers.getContract",
2536
3139
  route: {
2537
3140
  method: "GET",
2538
3141
  path: "/v1/automations/triggers/{triggerEventId}/contract"
@@ -2552,18 +3155,20 @@ var automationsTriggersContractGetCommand = defineCommand({
2552
3155
  "brew-cli automations triggers contract get tri_signup --format skill --json | jq -r .content"
2553
3156
  ],
2554
3157
  run: async ({ ctx, args, flags }) => {
2555
- const body = await rawRequest(ctx, {
2556
- method: "GET",
2557
- path: `/v1/automations/triggers/${encodeURIComponent(args.triggerEventId ?? "")}/contract${formatQuery(flags.format)}`
2558
- });
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
+ );
2559
3165
  return { data: body, ...contentHuman(body) };
2560
3166
  }
2561
3167
  });
2562
3168
  var automationsTriggersContractPutCommand = defineCommand({
2563
3169
  path: ["automations", "triggers", "contract", "put"],
2564
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",
2565
- sdkMethod: null,
2566
- isRawTransport: true,
3171
+ sdkMethod: "automations.triggers.putContract",
2567
3172
  route: {
2568
3173
  method: "PUT",
2569
3174
  path: "/v1/automations/triggers/{triggerEventId}/contract"
@@ -2615,19 +3220,19 @@ var automationsTriggersContractPutCommand = defineCommand({
2615
3220
  "Empty contract body \u2014 pass --input (fields and/or name) or --enforcement <mode>."
2616
3221
  );
2617
3222
  }
2618
- const body = await rawRequest(ctx, {
2619
- method: "PUT",
2620
- path: `/v1/automations/triggers/${encodeURIComponent(args.triggerEventId ?? "")}/contract`,
2621
- body: requestBody
2622
- });
3223
+ const body = await ctx.client().automations.triggers.putContract(
3224
+ asSdkInput({
3225
+ ...requestBody,
3226
+ triggerEventId: args.triggerEventId ?? ""
3227
+ })
3228
+ );
2623
3229
  return { data: body };
2624
3230
  }
2625
3231
  });
2626
3232
  var automationsTriggersContractValidateCommand = defineCommand({
2627
3233
  path: ["automations", "triggers", "contract", "validate"],
2628
3234
  summary: "Dry-run a payload against a trigger's contract (the fire path's validator) \u2014 never fires; invalid payloads still exit 0",
2629
- sdkMethod: null,
2630
- isRawTransport: true,
3235
+ sdkMethod: "automations.triggers.validatePayload",
2631
3236
  route: {
2632
3237
  method: "POST",
2633
3238
  path: "/v1/automations/triggers/{triggerEventId}/contract/validate"
@@ -2662,14 +3267,13 @@ var automationsTriggersContractValidateCommand = defineCommand({
2662
3267
  `Unknown --enforcement '${enforcement}' (expected prune | strict).`
2663
3268
  );
2664
3269
  }
2665
- const body = await rawRequest(ctx, {
2666
- method: "POST",
2667
- path: `/v1/automations/triggers/${encodeURIComponent(args.triggerEventId ?? "")}/contract/validate`,
2668
- body: {
3270
+ const body = await ctx.client().automations.triggers.validatePayload(
3271
+ asSdkInput({
3272
+ triggerEventId: args.triggerEventId ?? "",
2669
3273
  payload,
2670
3274
  ...enforcement !== void 0 ? { enforcement } : {}
2671
- }
2672
- });
3275
+ })
3276
+ );
2673
3277
  return { data: body };
2674
3278
  }
2675
3279
  });
@@ -3038,21 +3642,36 @@ var brandGetCommand = defineCommand({
3038
3642
  });
3039
3643
 
3040
3644
  // src/commands/brand/get-images.ts
3645
+ var RETIRED_FILTERS = {
3646
+ type: "--type (type) was retired by the API: use --kind logo|brand|generated.",
3647
+ aspectRatio: "--aspect-ratio (aspectRatio) was retired by the API: every image carries its width and height."
3648
+ };
3041
3649
  var brandGetImagesCommand = defineCommand({
3042
3650
  path: ["brand", "get-images"],
3043
- summary: "Browse or semantically search the brand's image library",
3651
+ summary: "Browse or semantically search the brand's assets (logos, brand images, images made with Brew)",
3044
3652
  sdkMethod: "brand.getImages",
3045
3653
  route: { method: "GET", path: "/v1/brand/images" },
3046
3654
  commandClass: "read",
3047
3655
  flags: [
3048
3656
  {
3049
3657
  flag: "--query <text>",
3050
- summary: "Semantic search over image descriptions"
3658
+ summary: "Semantic search over what the images show (1 credit per new search; logos are not searchable)"
3659
+ },
3660
+ {
3661
+ flag: "--kind <kind>",
3662
+ summary: "logo, brand (from the site or uploaded) or generated"
3663
+ },
3664
+ {
3665
+ flag: "--sort <order>",
3666
+ summary: "Browse order: newest (default) or oldest; ignored by --query"
3667
+ },
3668
+ {
3669
+ flag: "--type <type>",
3670
+ summary: "Retired by the API: use --kind"
3051
3671
  },
3052
- { flag: "--type <type>", summary: "Filter by image category" },
3053
3672
  {
3054
3673
  flag: "--aspect-ratio <ratio>",
3055
- summary: "Filter by aspect ratio (e.g. 16:9)"
3674
+ summary: "Retired by the API: rows carry width and height instead of a shape filter"
3056
3675
  },
3057
3676
  LIMIT_FLAG,
3058
3677
  CURSOR_FLAG,
@@ -3061,48 +3680,61 @@ var brandGetImagesCommand = defineCommand({
3061
3680
  ],
3062
3681
  examples: [
3063
3682
  "brew-cli brand get-images",
3064
- 'brew-cli brand get-images --query "team photo" --aspect-ratio 16:9'
3683
+ "brew-cli brand get-images --kind generated --sort oldest --all",
3684
+ 'brew-cli brand get-images --query "team photo" --kind brand'
3065
3685
  ],
3066
3686
  run: async ({ ctx, flags }) => {
3067
3687
  const base = await readJsonFlag(ctx, flags.input, "--input");
3068
3688
  const input = mergeInput(base, {
3069
3689
  q: flagString(flags.query),
3070
- type: flagString(flags.type),
3071
- aspectRatio: flagString(flags.aspectRatio),
3690
+ kind: flagString(flags.kind),
3691
+ sort: flagString(flags.sort),
3072
3692
  limit: flagInt(flags.limit, "--limit"),
3073
- cursor: flagString(flags.cursor)
3693
+ cursor: flagString(flags.cursor),
3694
+ type: flagString(flags.type),
3695
+ aspectRatio: flagString(flags.aspectRatio)
3074
3696
  });
3075
- const brand = ctx.client().brand;
3697
+ for (const [key, message] of Object.entries(RETIRED_FILTERS)) {
3698
+ if (input[key] !== void 0) {
3699
+ throw new CliUsageError(message);
3700
+ }
3701
+ }
3702
+ const page = (cursor) => ctx.client().brand.getImages(
3703
+ asSdkInput(
3704
+ cursor === void 0 ? input : { ...input, cursor }
3705
+ )
3706
+ );
3076
3707
  if (flags.all === true) {
3077
- const rows = await collectAll(
3078
- ctx,
3079
- (cursor) => brand.getImages(
3080
- asSdkInput({
3081
- ...input,
3082
- ...cursor === void 0 ? {} : { cursor }
3083
- })
3084
- )
3085
- );
3708
+ const rows = await collectAll(ctx, page);
3086
3709
  return {
3087
3710
  data: { data: rows, pagination: { cursor: null, hasMore: false } },
3088
- human: renderImages(rows)
3711
+ human: renderAssets(rows)
3089
3712
  };
3090
3713
  }
3091
- const result = await brand.getImages(
3092
- asSdkInput(input)
3093
- );
3094
- return { data: result, human: renderImages(result.data) };
3714
+ const result = await page(void 0);
3715
+ return { data: result, human: renderAssets(result.data) };
3095
3716
  }
3096
3717
  });
3097
- function renderImages(rows) {
3718
+ function renderAssets(rows) {
3098
3719
  if (rows.length === 0) {
3099
- return "No brand images found.";
3720
+ return "No brand assets found.";
3100
3721
  }
3101
- return renderTable(rows, [
3102
- { key: "url", header: "URL" },
3103
- { key: "category", header: "CATEGORY" },
3104
- { key: "aspectRatio", header: "ASPECT" }
3105
- ]);
3722
+ return renderTable(
3723
+ rows.map((row) => ({
3724
+ assetId: row.assetId,
3725
+ kind: row.kind,
3726
+ url: row.url,
3727
+ size: row.width !== void 0 && row.height !== void 0 ? `${row.width}x${row.height}` : "",
3728
+ added: row.addedAt ?? ""
3729
+ })),
3730
+ [
3731
+ { key: "assetId", header: "ASSET ID" },
3732
+ { key: "kind", header: "KIND" },
3733
+ { key: "url", header: "URL" },
3734
+ { key: "size", header: "SIZE" },
3735
+ { key: "added", header: "ADDED" }
3736
+ ]
3737
+ );
3106
3738
  }
3107
3739
 
3108
3740
  // src/commands/brand/update.ts
@@ -3204,10 +3836,43 @@ var brandsListCommand = defineCommand({
3204
3836
  sdkMethod: "brands.list",
3205
3837
  route: { method: "GET", path: "/v1/brands" },
3206
3838
  commandClass: "read",
3207
- examples: ["brew-cli brands list --json"],
3208
- run: async ({ ctx }) => ({
3209
- data: await ctx.client().brands.list()
3210
- })
3839
+ flags: [
3840
+ {
3841
+ flag: "--status <status>",
3842
+ summary: "Only brands in one lifecycle state: extracting | completed | failed | deleting"
3843
+ },
3844
+ LIMIT_FLAG,
3845
+ CURSOR_FLAG,
3846
+ ALL_FLAG
3847
+ ],
3848
+ examples: [
3849
+ "brew-cli brands list --json",
3850
+ "brew-cli brands list --status completed",
3851
+ "brew-cli brands list --all --json"
3852
+ ],
3853
+ run: async ({ ctx, flags }) => {
3854
+ const input = {
3855
+ status: flagString(flags.status),
3856
+ limit: flagInt(flags.limit, "--limit"),
3857
+ cursor: flagString(flags.cursor)
3858
+ };
3859
+ const brands = ctx.client().brands;
3860
+ if (flags.all === true) {
3861
+ const rows = await collectAll(
3862
+ ctx,
3863
+ (cursor) => brands.list(
3864
+ asSdkInput({
3865
+ ...input,
3866
+ ...cursor === void 0 ? {} : { cursor }
3867
+ })
3868
+ )
3869
+ );
3870
+ return {
3871
+ data: { data: rows, pagination: { cursor: null, hasMore: false } }
3872
+ };
3873
+ }
3874
+ return { data: await brands.list(asSdkInput(input)) };
3875
+ }
3211
3876
  });
3212
3877
 
3213
3878
  // src/commands/chats/get.ts
@@ -3350,6 +4015,90 @@ var contactsCountCommand = defineCommand({
3350
4015
  }
3351
4016
  });
3352
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
+
3353
4102
  // src/commands/contacts/delete.ts
3354
4103
  var contactsDeleteCommand = defineCommand({
3355
4104
  path: ["contacts", "delete"],
@@ -3439,35 +4188,65 @@ var contactsGetCommand = defineCommand({
3439
4188
  // src/commands/contacts/import-csv.ts
3440
4189
  var contactsImportCsvCommand = defineCommand({
3441
4190
  path: ["contacts", "import-csv"],
3442
- summary: "Bulk-import contacts from a CSV file or stdin",
4191
+ summary: "Bulk-import contacts from a CSV file or stdin (free; --validate costs 2 credits per address)",
3443
4192
  sdkMethod: "contacts.importCsv",
3444
4193
  route: { method: "POST", path: "/v1/contacts/import-csv" },
3445
4194
  commandClass: "write",
3446
4195
  flags: [
3447
- { flag: "--file <path>", summary: "CSV file to import, or - for stdin" },
4196
+ {
4197
+ flag: "--file <path>",
4198
+ summary: "CSV file to import, or - for stdin (or csv in --input)"
4199
+ },
3448
4200
  {
3449
4201
  flag: "--mapping <pairs...>",
3450
4202
  summary: "Column mapping csvColumn=fieldName, repeatable"
3451
4203
  },
4204
+ {
4205
+ flag: "--date-order <order>",
4206
+ summary: "month_first or day_first: how to read dates like 03/04/2026 (a day over 12 in the column wins; default month-first, with a DATE_ORDER_ASSUMED warning)"
4207
+ },
4208
+ {
4209
+ flag: "--validate",
4210
+ summary: "Deliverability-check every imported address (2 credits per address)"
4211
+ },
4212
+ {
4213
+ flag: "--consent-source <source>",
4214
+ summary: "Stamp a marketing consent record on every row: api, form or import (sets the source of a record given in --input)"
4215
+ },
4216
+ INPUT_FLAG,
3452
4217
  IDEMPOTENCY_FLAG
3453
4218
  ],
3454
4219
  examples: [
3455
4220
  "brew-cli contacts import-csv --file contacts.csv",
3456
- "cat contacts.csv | brew-cli contacts import-csv --file - --mapping Email=email"
4221
+ "cat contacts.csv | brew-cli contacts import-csv --file - --mapping Email=email",
4222
+ "brew-cli contacts import-csv --file eu-signups.csv --date-order day_first --consent-source import"
3457
4223
  ],
3458
4224
  run: async ({ ctx, flags }) => {
3459
- const csv = await readTextFlag(ctx, flags.file, "--file");
3460
- if (csv === void 0 || csv.trim() === "") {
4225
+ if (flags.input === "-" && flags.file === "-") {
4226
+ throw new CliUsageError("Only one of --input and --file can read stdin.");
4227
+ }
4228
+ const base = await readJsonFlag(ctx, flags.input, "--input");
4229
+ const consentSource = flagString(flags.consentSource);
4230
+ const input = mergeInput(base, {
4231
+ csv: await readTextFlag(ctx, flags.file, "--file"),
4232
+ mapping: parseKeyValues(flags.mapping, "--mapping"),
4233
+ dateOrder: flagString(flags.dateOrder),
4234
+ validate: flags.validate === true ? true : void 0
4235
+ });
4236
+ if (typeof input.csv !== "string" || input.csv.trim() === "") {
3461
4237
  throw new CliUsageError(
3462
- "--file is required (a CSV path, or - for stdin)."
4238
+ "Pass the CSV with --file (a path, or - for stdin) or as csv in --input."
3463
4239
  );
3464
4240
  }
3465
- const mapping = parseKeyValues(flags.mapping, "--mapping");
4241
+ if (consentSource !== void 0) {
4242
+ const record = input.consent;
4243
+ input.consent = {
4244
+ ...typeof record === "object" && record !== null ? record : {},
4245
+ source: consentSource
4246
+ };
4247
+ }
3466
4248
  const result = await ctx.client().contacts.importCsv(
3467
- asSdkInput({
3468
- csv,
3469
- ...mapping === void 0 ? {} : { mapping }
3470
- }),
4249
+ asSdkInput(input),
3471
4250
  requestOptions(flags)
3472
4251
  );
3473
4252
  return { data: result };
@@ -3826,14 +4605,13 @@ var contentAddImageCommand = defineCommand({
3826
4605
  return { data: result };
3827
4606
  }
3828
4607
  });
3829
-
3830
- // src/commands/content/generate-image.ts
3831
4608
  var contentGenerateImageCommand = defineCommand({
3832
4609
  path: ["content", "generate-image"],
3833
4610
  summary: "Generate or edit an image from a prompt",
3834
4611
  sdkMethod: "content.generateImage",
3835
4612
  route: { method: "POST", path: "/v1/content/generate-image" },
3836
4613
  commandClass: "write",
4614
+ defaultTimeoutMs: GENERATE_IMAGE_DEFAULT_TIMEOUT_MS,
3837
4615
  isCredited: true,
3838
4616
  flags: [
3839
4617
  { flag: "--prompt <text>", summary: "What to generate" },
@@ -3874,14 +4652,13 @@ var contentGenerateImageCommand = defineCommand({
3874
4652
  return { data: result };
3875
4653
  }
3876
4654
  });
3877
-
3878
- // src/commands/content/gif.ts
3879
4655
  var contentGifCommand = defineCommand({
3880
4656
  path: ["content", "gif"],
3881
4657
  summary: "Create an animated GIF from a prompt, image, or video",
3882
4658
  sdkMethod: "content.gif",
3883
4659
  route: { method: "POST", path: "/v1/content/gif" },
3884
4660
  commandClass: "write",
4661
+ defaultTimeoutMs: GIF_DEFAULT_TIMEOUT_MS,
3885
4662
  isCredited: true,
3886
4663
  flags: [
3887
4664
  { flag: "--from <source>", summary: "prompt | image | video" },
@@ -4063,32 +4840,35 @@ var contentTransformCommand = defineCommand({
4063
4840
  var contractsInferCommand = defineCommand({
4064
4841
  path: ["contracts", "infer"],
4065
4842
  summary: "Draft a payload contract from a real example payload \u2014 nothing is saved; PUT the draft on a trigger",
4066
- sdkMethod: null,
4067
- isRawTransport: true,
4843
+ sdkMethod: "payloadContracts.infer",
4068
4844
  route: { method: "POST", path: "/v1/payload-contracts/infer" },
4069
4845
  commandClass: "read",
4070
4846
  flags: [INPUT_FLAG],
4071
4847
  examples: [
4072
4848
  `brew-cli contracts infer --input '{"email":"jane@example.com","order":{"total":9.5}}'`
4073
4849
  ],
4074
- // Accepts either the bare example object or `{ example: {...} }`.
4075
- // Un-inferable spots (nulls, empty arrays) come back under `issues`.
4850
+ // Accepts either the bare example object or `{ example: {...} }`; only the
4851
+ // envelope carries `subjectKind` (a bare example may have its own field of
4852
+ // that name). Un-inferable spots (nulls, empty arrays) come back under
4853
+ // `issues`.
4076
4854
  run: async ({ ctx, flags }) => {
4077
4855
  const input = mergeInput(
4078
4856
  await readJsonFlag(ctx, flags.input, "--input"),
4079
4857
  {}
4080
4858
  );
4081
- const example = input.example ?? input;
4859
+ const isEnvelope = input.example !== void 0;
4860
+ const example = isEnvelope ? input.example : input;
4082
4861
  if (typeof example !== "object" || example === null || Array.isArray(example) || Object.keys(example).length === 0) {
4083
4862
  throw new CliUsageError(
4084
4863
  "--input with a non-empty example object is required (the JSON your system sends)."
4085
4864
  );
4086
4865
  }
4087
- const body = await rawRequest(ctx, {
4088
- method: "POST",
4089
- path: "/v1/payload-contracts/infer",
4090
- body: { example }
4091
- });
4866
+ const body = await ctx.client().payloadContracts.infer(
4867
+ asSdkInput({
4868
+ example,
4869
+ ...isEnvelope && input.subjectKind !== void 0 ? { subjectKind: input.subjectKind } : {}
4870
+ })
4871
+ );
4092
4872
  return { data: body };
4093
4873
  }
4094
4874
  });
@@ -4125,24 +4905,8 @@ var dataRunCommand = defineCommand({
4125
4905
  }
4126
4906
  });
4127
4907
 
4128
- // src/skip-list.ts
4129
- var SDK_SKIP_LIST = [
4130
- {
4131
- sdkPath: "automations.triggers.getContract",
4132
- reason: "covered by `automations triggers contract get` (raw route, bound pre-SDK-v9; SDK-method migration tracked separately)"
4133
- },
4134
- {
4135
- sdkPath: "automations.triggers.putContract",
4136
- reason: "covered by `automations triggers contract put` (raw route, bound pre-SDK-v9)"
4137
- },
4138
- {
4139
- sdkPath: "automations.triggers.validatePayload",
4140
- reason: "covered by `automations triggers contract validate` (raw route, bound pre-SDK-v9)"
4141
- },
4142
- {
4143
- sdkPath: "payloadContracts.infer",
4144
- reason: "covered by `contracts infer` (raw route, bound pre-SDK-v9)"
4145
- },
4908
+ // src/skip-list.ts
4909
+ var SDK_SKIP_LIST = [
4146
4910
  {
4147
4911
  sdkPath: "contacts.searchAll",
4148
4912
  reason: "auto-pager covered by `contacts search --all`"
@@ -4187,7 +4951,9 @@ function buildManifest() {
4187
4951
  "1": "API or runtime error",
4188
4952
  "2": "usage error",
4189
4953
  "3": "authentication error",
4190
- "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"
4191
4957
  },
4192
4958
  confirmationProtocol: {
4193
4959
  exitCode: 4,
@@ -4295,6 +5061,7 @@ var doctorCommand = defineCommand({
4295
5061
  isReachable = health.status === "ok";
4296
5062
  apiVersion = health.version ?? null;
4297
5063
  } catch (error) {
5064
+ rethrowIfStopped(ctx);
4298
5065
  reachabilityError = error instanceof Error ? error.message : String(error);
4299
5066
  }
4300
5067
  let isAuthValid = null;
@@ -4305,6 +5072,7 @@ var doctorCommand = defineCommand({
4305
5072
  isAuthValid = true;
4306
5073
  plan = usage.plan?.key ?? null;
4307
5074
  } catch (error) {
5075
+ rethrowIfStopped(ctx);
4308
5076
  if ((error instanceof BrewApiError || error instanceof CliApiError) && (error.status === 401 || error.status === 403)) {
4309
5077
  isAuthValid = false;
4310
5078
  }
@@ -4330,6 +5098,7 @@ var doctorCommand = defineCommand({
4330
5098
  extraLocally = [...localOps].filter((op) => !liveOps.has(op));
4331
5099
  isSurfaceChecked = true;
4332
5100
  } catch {
5101
+ rethrowIfStopped(ctx);
4333
5102
  }
4334
5103
  }
4335
5104
  const isHealthy = isReachable && isAuthValid !== false && isSurfaceChecked && missingLocally.length === 0;
@@ -4367,6 +5136,9 @@ ${missingLocally.map((op) => ` ${op}`).join("\n")}` : "-- surface not chec
4367
5136
  };
4368
5137
  }
4369
5138
  });
5139
+ function rethrowIfStopped(ctx) {
5140
+ ctx.signal.throwIfAborted();
5141
+ }
4370
5142
 
4371
5143
  // src/commands/domains/add.ts
4372
5144
  var domainsAddCommand = defineCommand({
@@ -4513,6 +5285,193 @@ function renderDomains(rows) {
4513
5285
  ]);
4514
5286
  }
4515
5287
 
5288
+ // src/commands/domains/unsubscribes/add.ts
5289
+ var domainsUnsubscribesAddCommand = defineCommand({
5290
+ path: ["domains", "unsubscribes", "add"],
5291
+ summary: "Add addresses to a marketing domain's unsubscribe list (that domain only)",
5292
+ sdkMethod: "domains.unsubscribes.add",
5293
+ route: { method: "POST", path: "/v1/domains/{domainId}/unsubscribes" },
5294
+ commandClass: "write",
5295
+ args: [
5296
+ { name: "domainId", summary: "Marketing domain id", isRequired: true }
5297
+ ],
5298
+ flags: [
5299
+ {
5300
+ flag: "--email <addresses...>",
5301
+ summary: "Address to suppress, repeatable (up to 1,000)"
5302
+ },
5303
+ IDEMPOTENCY_FLAG
5304
+ ],
5305
+ examples: [
5306
+ "brew-cli domains unsubscribes add dom_123 --email ada@example.com --email bo@example.com"
5307
+ ],
5308
+ run: async ({ ctx, args, flags }) => {
5309
+ const emails = toStringArray(flags.email);
5310
+ if (emails === void 0) {
5311
+ throw new CliUsageError("Pass at least one --email.");
5312
+ }
5313
+ const body = await ctx.client().domains.unsubscribes.add(
5314
+ { domainId: args.domainId ?? "", emails: [...emails] },
5315
+ requestOptions(flags)
5316
+ );
5317
+ return { data: body };
5318
+ }
5319
+ });
5320
+
5321
+ // src/commands/domains/unsubscribes/export.ts
5322
+ var domainsUnsubscribesExportCommand = defineCommand({
5323
+ path: ["domains", "unsubscribes", "export"],
5324
+ summary: "A domain's unsubscribe list as CSV (truncated when capped)",
5325
+ sdkMethod: "domains.unsubscribes.export",
5326
+ route: {
5327
+ method: "GET",
5328
+ path: "/v1/domains/{domainId}/unsubscribes/export"
5329
+ },
5330
+ commandClass: "read",
5331
+ args: [
5332
+ { name: "domainId", summary: "Marketing domain id", isRequired: true }
5333
+ ],
5334
+ flags: [
5335
+ {
5336
+ flag: "--scope <scope>",
5337
+ summary: "any (default), domain or all"
5338
+ }
5339
+ ],
5340
+ examples: [
5341
+ "brew-cli domains unsubscribes export dom_123 > optouts.csv",
5342
+ "brew-cli domains unsubscribes export dom_123 --scope all --json"
5343
+ ],
5344
+ run: async ({ ctx, args, flags }) => {
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
+ );
5352
+ return { data: body, human: body.csv };
5353
+ }
5354
+ });
5355
+
5356
+ // src/commands/domains/unsubscribes/import.ts
5357
+ var domainsUnsubscribesImportCommand = defineCommand({
5358
+ path: ["domains", "unsubscribes", "import"],
5359
+ summary: "Add every address in a CSV to a domain's unsubscribe list",
5360
+ sdkMethod: "domains.unsubscribes.import",
5361
+ route: {
5362
+ method: "POST",
5363
+ path: "/v1/domains/{domainId}/unsubscribes/import"
5364
+ },
5365
+ commandClass: "write",
5366
+ args: [
5367
+ { name: "domainId", summary: "Marketing domain id", isRequired: true }
5368
+ ],
5369
+ flags: [
5370
+ { flag: "--file <path>", summary: "CSV file, or - for stdin" },
5371
+ {
5372
+ flag: "--column <header>",
5373
+ summary: "The column holding the addresses (by header)"
5374
+ },
5375
+ IDEMPOTENCY_FLAG
5376
+ ],
5377
+ examples: [
5378
+ "brew-cli domains unsubscribes import dom_123 --file optouts.csv --column Email"
5379
+ ],
5380
+ run: async ({ ctx, args, flags }) => {
5381
+ const csv = await readTextFlag(ctx, flags.file, "--file");
5382
+ if (csv === void 0 || csv.trim() === "") {
5383
+ throw new CliUsageError(
5384
+ "--file is required (a CSV path, or - for stdin)."
5385
+ );
5386
+ }
5387
+ const column = flagString(flags.column);
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
+ );
5396
+ return { data: body };
5397
+ }
5398
+ });
5399
+
5400
+ // src/commands/domains/unsubscribes/list.ts
5401
+ var domainsUnsubscribesListCommand = defineCommand({
5402
+ path: ["domains", "unsubscribes", "list"],
5403
+ summary: "One page of a marketing domain's unsubscribe list, newest first",
5404
+ sdkMethod: "domains.unsubscribes.list",
5405
+ route: { method: "GET", path: "/v1/domains/{domainId}/unsubscribes" },
5406
+ commandClass: "read",
5407
+ args: [
5408
+ { name: "domainId", summary: "Marketing domain id", isRequired: true }
5409
+ ],
5410
+ flags: [
5411
+ { flag: "--q <text>", summary: "Search by address" },
5412
+ {
5413
+ flag: "--scope <scope>",
5414
+ summary: "any (default), domain (this list only) or all (brand-wide opt-outs)"
5415
+ },
5416
+ LIMIT_FLAG,
5417
+ CURSOR_FLAG
5418
+ ],
5419
+ examples: [
5420
+ "brew-cli domains unsubscribes list dom_123",
5421
+ "brew-cli domains unsubscribes list dom_123 --scope domain --q ada"
5422
+ ],
5423
+ run: async ({ ctx, args, flags }) => {
5424
+ const limit = flagInt(flags.limit, "--limit");
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
+ );
5437
+ return { data: body, human: renderRows(body.data) };
5438
+ }
5439
+ });
5440
+ function renderRows(rows) {
5441
+ if (rows.length === 0) {
5442
+ return "No unsubscribes on this list.";
5443
+ }
5444
+ return renderTable(rows, [
5445
+ { key: "email", header: "EMAIL" },
5446
+ { key: "scope", header: "SCOPE" },
5447
+ { key: "unsubscribedAt", header: "UNSUBSCRIBED" }
5448
+ ]);
5449
+ }
5450
+
5451
+ // src/commands/domains/unsubscribes/remove.ts
5452
+ var domainsUnsubscribesRemoveCommand = defineCommand({
5453
+ path: ["domains", "unsubscribes", "remove"],
5454
+ summary: "Take one address off a domain's unsubscribe list (never re-subscribes a brand-wide opt-out)",
5455
+ sdkMethod: "domains.unsubscribes.remove",
5456
+ route: {
5457
+ method: "DELETE",
5458
+ path: "/v1/domains/{domainId}/unsubscribes/{email}"
5459
+ },
5460
+ commandClass: "write",
5461
+ args: [
5462
+ { name: "domainId", summary: "Marketing domain id", isRequired: true },
5463
+ { name: "email", summary: "Address to remove", isRequired: true }
5464
+ ],
5465
+ flags: [],
5466
+ examples: ["brew-cli domains unsubscribes remove dom_123 ada@example.com"],
5467
+ run: async ({ ctx, args }) => ({
5468
+ data: await ctx.client().domains.unsubscribes.remove({
5469
+ domainId: args.domainId ?? "",
5470
+ email: args.email ?? ""
5471
+ })
5472
+ })
5473
+ });
5474
+
4516
5475
  // src/commands/domains/update.ts
4517
5476
  var domainsUpdateCommand = defineCommand({
4518
5477
  path: ["domains", "update"],
@@ -4586,6 +5545,7 @@ var emailsAuditCommand = defineCommand({
4586
5545
  sdkMethod: "emails.auditEmail",
4587
5546
  route: { method: "POST", path: "/v1/emails/audit" },
4588
5547
  commandClass: "write",
5548
+ defaultTimeoutMs: AUDIT_EMAIL_DEFAULT_TIMEOUT_MS,
4589
5549
  isCredited: true,
4590
5550
  flags: [
4591
5551
  {
@@ -4629,19 +5589,11 @@ var emailsAuditCommand = defineCommand({
4629
5589
  "--file is required (a path, or - for stdin), or emailHtml via --input."
4630
5590
  );
4631
5591
  }
4632
- const signal = AbortSignal.timeout(AUDIT_EMAIL_DEFAULT_TIMEOUT_MS);
4633
- try {
4634
- const result = await ctx.client().emails.auditEmail(asSdkInput(input), {
4635
- signal,
4636
- ...requestOptions(flags) ?? {}
4637
- });
4638
- return { data: result };
4639
- } catch (error) {
4640
- if (signal.aborted) {
4641
- throw signal.reason;
4642
- }
4643
- throw error;
4644
- }
5592
+ const result = await ctx.client().emails.auditEmail(
5593
+ asSdkInput(input),
5594
+ requestOptions(flags)
5595
+ );
5596
+ return { data: result };
4645
5597
  }
4646
5598
  });
4647
5599
 
@@ -4658,19 +5610,41 @@ var emailsCloneCommand = defineCommand({
4658
5610
  flag: "--email-version-id <id>",
4659
5611
  summary: "Exact source version to clone (default: latest)"
4660
5612
  },
5613
+ {
5614
+ flag: "--title <title>",
5615
+ summary: "Name for the clone (default: Copy of <source title>)"
5616
+ },
5617
+ {
5618
+ flag: "--group-id <groupId>",
5619
+ summary: "File the clone under an existing group (grp_\u2026), or ungrouped"
5620
+ },
5621
+ {
5622
+ flag: "--group-name <name>",
5623
+ summary: "File the clone under a group found (or created) by this name; not with --group-id"
5624
+ },
4661
5625
  IDEMPOTENCY_FLAG
4662
5626
  ],
4663
5627
  examples: [
4664
5628
  "brew-cli emails clone eml_2SmZOWV3ZQ7W5x6g3m4p",
4665
- "brew-cli emails clone eml_2SmZOWV3ZQ7W5x6g3m4p --email-version-id emv_9f2kX"
5629
+ "brew-cli emails clone eml_2SmZOWV3ZQ7W5x6g3m4p --email-version-id emv_9f2kX",
5630
+ 'brew-cli emails clone eml_2SmZOWV3ZQ7W5x6g3m4p --title "Fall sale (B)" --group-name "Fall campaign"'
4666
5631
  ],
4667
5632
  run: async ({ ctx, args, flags }) => {
4668
5633
  const emailVersionId = flagString(flags.emailVersionId);
5634
+ const title = flagString(flags.title);
5635
+ const groupId = flagString(flags.groupId);
5636
+ const groupName = flagString(flags.groupName);
5637
+ if (groupId !== void 0 && groupName !== void 0) {
5638
+ throw new CliUsageError("Pass --group-id or --group-name, not both.");
5639
+ }
4669
5640
  return {
4670
5641
  data: await ctx.client().emails.clone(
4671
5642
  asSdkInput({
4672
5643
  emailId: args.emailId ?? "",
4673
- ...emailVersionId === void 0 ? {} : { emailVersionId }
5644
+ ...emailVersionId === void 0 ? {} : { emailVersionId },
5645
+ ...title === void 0 ? {} : { title },
5646
+ ...groupId === void 0 ? {} : { groupId },
5647
+ ...groupName === void 0 ? {} : { groupName }
4674
5648
  }),
4675
5649
  requestOptions(flags)
4676
5650
  )
@@ -4757,8 +5731,6 @@ var emailsDeleteCommand = defineCommand({
4757
5731
  data: await ctx.client().emails.delete({ emailId: args.emailId ?? "" })
4758
5732
  })
4759
5733
  });
4760
-
4761
- // src/commands/emails/edit.ts
4762
5734
  var ENVELOPE_FIELDS = ["title", "subjectLine", "groupId"];
4763
5735
  function isText(value) {
4764
5736
  return typeof value === "string" && value !== "";
@@ -4769,6 +5741,7 @@ var emailsEditCommand = defineCommand({
4769
5741
  sdkMethod: "emails.edit",
4770
5742
  route: { method: "PATCH", path: "/v1/emails/{emailId}" },
4771
5743
  commandClass: "write",
5744
+ defaultTimeoutMs: EDIT_EMAIL_DEFAULT_TIMEOUT_MS,
4772
5745
  isCredited: true,
4773
5746
  args: [{ name: "emailId", summary: "Design id to edit", isRequired: true }],
4774
5747
  flags: [
@@ -4835,8 +5808,7 @@ var emailsEditCommand = defineCommand({
4835
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"
4836
5809
  );
4837
5810
  const result = await ctx.client().emails.edit(
4838
- asSdkInput({ ...input, emailId: args.emailId ?? "" }),
4839
- { timeoutMs: 24e4 }
5811
+ asSdkInput({ ...input, emailId: args.emailId ?? "" })
4840
5812
  );
4841
5813
  return { data: result };
4842
5814
  }
@@ -4853,12 +5825,16 @@ var emailsExportCommand = defineCommand({
4853
5825
  flags: [
4854
5826
  {
4855
5827
  flag: "--provider <provider>",
4856
- summary: "Connected ESP: braze, hubspot, klaviyo, mailchimp, iterable, postmark, onesignal, mailgun, sendgrid"
5828
+ summary: "Connected ESP: braze, brevo, hubspot, klaviyo, mailchimp, mailjet, iterable, postmark, onesignal, mailgun, sendgrid"
4857
5829
  },
4858
5830
  {
4859
5831
  flag: "--template-name <name>",
4860
5832
  summary: "Template name in the ESP (default: the email title)"
4861
5833
  },
5834
+ {
5835
+ flag: "--sender-email <email>",
5836
+ summary: "Brevo or Mailjet: the active sender to use (omit when the account has exactly one)"
5837
+ },
4862
5838
  {
4863
5839
  flag: "--dry-run",
4864
5840
  summary: "Validate design, ownership, and ESP connection without creating a template"
@@ -4873,16 +5849,18 @@ var emailsExportCommand = defineCommand({
4873
5849
  const provider = flagString(flags.provider);
4874
5850
  if (provider === void 0) {
4875
5851
  throw new CliUsageError(
4876
- "--provider is required: braze, hubspot, klaviyo, mailchimp, iterable, postmark, onesignal, mailgun, or sendgrid."
5852
+ "--provider is required: braze, brevo, hubspot, klaviyo, mailchimp, mailjet, iterable, postmark, onesignal, mailgun, or sendgrid."
4877
5853
  );
4878
5854
  }
4879
5855
  const templateName = flagString(flags.templateName);
5856
+ const senderEmail = flagString(flags.senderEmail);
4880
5857
  return {
4881
5858
  data: await ctx.client().emails.export(
4882
5859
  asSdkInput({
4883
5860
  emailId: args.emailId ?? "",
4884
5861
  provider,
4885
5862
  ...templateName === void 0 ? {} : { templateName },
5863
+ ...senderEmail === void 0 ? {} : { senderEmail },
4886
5864
  // The spec field is snake_case, unlike the rest of the API surface.
4887
5865
  ...flags.dryRun === true ? { dryRun: true } : {}
4888
5866
  }),
@@ -4891,14 +5869,13 @@ var emailsExportCommand = defineCommand({
4891
5869
  };
4892
5870
  }
4893
5871
  });
4894
-
4895
- // src/commands/emails/generate.ts
4896
5872
  var emailsGenerateCommand = defineCommand({
4897
5873
  path: ["emails", "generate"],
4898
5874
  summary: "Generate a new on-brand email design from a prompt",
4899
5875
  sdkMethod: "emails.generate",
4900
5876
  route: { method: "POST", path: "/v1/emails" },
4901
5877
  commandClass: "write",
5878
+ defaultTimeoutMs: GENERATE_EMAIL_DEFAULT_TIMEOUT_MS,
4902
5879
  isCredited: true,
4903
5880
  flags: [
4904
5881
  { flag: "--prompt <text>", summary: "What the email should be" },
@@ -4942,10 +5919,10 @@ var emailsGenerateCommand = defineCommand({
4942
5919
  );
4943
5920
  }
4944
5921
  progress(ctx, "Generating email\u2026 (typically 30-90s)");
4945
- const result = await ctx.client().emails.generate(asSdkInput(input), {
4946
- timeoutMs: 24e4,
4947
- ...requestOptions(flags) ?? {}
4948
- });
5922
+ const result = await ctx.client().emails.generate(
5923
+ asSdkInput(input),
5924
+ requestOptions(flags)
5925
+ );
4949
5926
  return { data: result };
4950
5927
  }
4951
5928
  });
@@ -4967,20 +5944,39 @@ var emailsGetCommand = defineCommand({
4967
5944
  flags: [
4968
5945
  {
4969
5946
  flag: "--include <tokens>",
4970
- summary: "Comma-separated expansions: html, versions"
5947
+ summary: "Comma-separated expansions: html, versions, text, links"
5948
+ },
5949
+ {
5950
+ flag: "--email-version-id <emailVersionId>",
5951
+ summary: "Read this saved version instead of the current head (ids from --include versions)"
5952
+ },
5953
+ {
5954
+ flag: "--run-id <runId>",
5955
+ summary: "Read the version a generate or edit run produced (the runId it returned)"
4971
5956
  }
4972
5957
  ],
4973
5958
  examples: [
4974
5959
  "brew-cli emails get eml_2SmZOWV3ZQ7W5x6g3m4p",
4975
- "brew-cli emails get eml_2SmZOWV3ZQ7W5x6g3m4p --include html,versions"
5960
+ "brew-cli emails get eml_2SmZOWV3ZQ7W5x6g3m4p --include html,versions",
5961
+ "brew-cli emails get eml_2SmZOWV3ZQ7W5x6g3m4p --include text,links",
5962
+ "brew-cli emails get eml_2SmZOWV3ZQ7W5x6g3m4p --email-version-id emv_2SmZOWV3ZQ7W5x6g3m4p --include html"
4976
5963
  ],
4977
5964
  run: async ({ ctx, args, flags }) => {
5965
+ const emailId = args.emailId ?? "";
4978
5966
  const include = flagString(flags.include);
5967
+ const emailVersionId = flagString(flags.emailVersionId);
5968
+ const runId = flagString(flags.runId);
5969
+ if (emailVersionId !== void 0 && runId !== void 0) {
5970
+ throw new CliUsageError(
5971
+ "Pass --email-version-id or --run-id, not both: each selects the version to read."
5972
+ );
5973
+ }
4979
5974
  return {
4980
- data: await ctx.client().emails.get(
4981
- args.emailId ?? "",
4982
- include === void 0 ? void 0 : { include }
4983
- )
5975
+ data: await ctx.client().emails.get(emailId, {
5976
+ ...include === void 0 ? {} : { include },
5977
+ ...emailVersionId === void 0 ? {} : { emailVersionId },
5978
+ ...runId === void 0 ? {} : { runId }
5979
+ })
4984
5980
  };
4985
5981
  }
4986
5982
  });
@@ -4989,8 +5985,7 @@ var emailsGetCommand = defineCommand({
4989
5985
  var emailsGetAuditCommand = defineCommand({
4990
5986
  path: ["emails", "get-audit"],
4991
5987
  summary: "Read a saved email audit: one page of its findings (free; never reruns the audit)",
4992
- sdkMethod: null,
4993
- isRawTransport: true,
5988
+ sdkMethod: "emails.getAudit",
4994
5989
  route: { method: "GET", path: "/v1/emails/audits/{auditId}" },
4995
5990
  commandClass: "read",
4996
5991
  args: [
@@ -5000,7 +5995,12 @@ var emailsGetAuditCommand = defineCommand({
5000
5995
  isRequired: true
5001
5996
  }
5002
5997
  ],
5003
- flags: [LIMIT_FLAG, CURSOR_FLAG],
5998
+ // Its own page size: a saved audit pages 1-50 findings (default 10), not
5999
+ // the shared list's 1-100.
6000
+ flags: [
6001
+ { flag: "--limit <n>", summary: "Findings per page, 1-50 (default 10)" },
6002
+ CURSOR_FLAG
6003
+ ],
5004
6004
  examples: [
5005
6005
  "brew-cli emails get-audit 6f1e2d3c-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
5006
6006
  "brew-cli emails get-audit 6f1e2d3c-4b5a-4c7d-8e9f-0a1b2c3d4e5f --limit 20"
@@ -5009,13 +6009,10 @@ var emailsGetAuditCommand = defineCommand({
5009
6009
  // says whether one exists.
5010
6010
  run: async ({ ctx, args, flags }) => {
5011
6011
  const limit = flagInt(flags.limit, "--limit");
5012
- const body = await rawRequest(ctx, {
5013
- method: "GET",
5014
- path: `/v1/emails/audits/${encodeURIComponent(args.auditId ?? "")}`,
5015
- query: {
5016
- cursor: flagString(flags.cursor),
5017
- limit: limit === void 0 ? void 0 : String(limit)
5018
- }
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 }
5019
6016
  });
5020
6017
  return { data: body };
5021
6018
  }
@@ -5025,8 +6022,7 @@ var emailsGetAuditCommand = defineCommand({
5025
6022
  var emailsGetClientPreviewCommand = defineCommand({
5026
6023
  path: ["emails", "get-client-preview"],
5027
6024
  summary: "Poll a client-preview rendering job: per-client screenshot links once it settles (free; never re-renders)",
5028
- sdkMethod: null,
5029
- isRawTransport: true,
6025
+ sdkMethod: "emails.getClientPreview",
5030
6026
  route: { method: "GET", path: "/v1/emails/client-previews/{previewId}" },
5031
6027
  commandClass: "read",
5032
6028
  args: [
@@ -5045,10 +6041,7 @@ var emailsGetClientPreviewCommand = defineCommand({
5045
6041
  // `failed`; each client then carries its `imageUrl`, or a `reason` and
5046
6042
  // whether it is `retryable`.
5047
6043
  run: async ({ ctx, args }) => {
5048
- const body = await rawRequest(ctx, {
5049
- method: "GET",
5050
- path: `/v1/emails/client-previews/${encodeURIComponent(args.previewId ?? "")}`
5051
- });
6044
+ const body = await ctx.client().emails.getClientPreview(args.previewId ?? "");
5052
6045
  return { data: body };
5053
6046
  }
5054
6047
  });
@@ -5262,14 +6255,13 @@ var emailsGroupsUpdateCommand = defineCommand({
5262
6255
  };
5263
6256
  }
5264
6257
  });
5265
-
5266
- // src/commands/emails/import.ts
5267
6258
  var emailsImportCommand = defineCommand({
5268
6259
  path: ["emails", "import"],
5269
6260
  summary: "Import existing HTML, MJML, or JSX as a new editable design",
5270
6261
  sdkMethod: "emails.import",
5271
6262
  route: { method: "POST", path: "/v1/emails/import" },
5272
6263
  commandClass: "write",
6264
+ defaultTimeoutMs: IMPORT_EMAIL_DEFAULT_TIMEOUT_MS,
5273
6265
  flags: [
5274
6266
  {
5275
6267
  flag: "--file <path>",
@@ -5315,14 +6307,13 @@ var emailsImportCommand = defineCommand({
5315
6307
  return { data: result };
5316
6308
  }
5317
6309
  });
5318
-
5319
- // src/commands/emails/import-figma.ts
5320
6310
  var emailsImportFigmaCommand = defineCommand({
5321
6311
  path: ["emails", "import-figma"],
5322
6312
  summary: "Convert one Figma frame into an editable design (deterministic, free)",
5323
6313
  sdkMethod: "emails.importFigma",
5324
6314
  route: { method: "POST", path: "/v1/emails/figma" },
5325
6315
  commandClass: "write",
6316
+ defaultTimeoutMs: IMPORT_FIGMA_DEFAULT_TIMEOUT_MS,
5326
6317
  flags: [
5327
6318
  {
5328
6319
  flag: "--url <figmaUrl>",
@@ -5340,6 +6331,14 @@ var emailsImportFigmaCommand = defineCommand({
5340
6331
  flag: "--subject-line <text>",
5341
6332
  summary: "The design's default inbox subject line"
5342
6333
  },
6334
+ {
6335
+ flag: "--group-id <groupId>",
6336
+ summary: "File the design under an existing group (grp_\u2026), or ungrouped"
6337
+ },
6338
+ {
6339
+ flag: "--group-name <name>",
6340
+ summary: "File the design under a group found (or created) by this name; not with --group-id"
6341
+ },
5343
6342
  IDEMPOTENCY_FLAG
5344
6343
  ],
5345
6344
  examples: [
@@ -5356,13 +6355,20 @@ var emailsImportFigmaCommand = defineCommand({
5356
6355
  const title = flagString(flags.title);
5357
6356
  const format = flagString(flags.format);
5358
6357
  const subjectLine = flagString(flags.subjectLine);
6358
+ const groupId = flagString(flags.groupId);
6359
+ const groupName = flagString(flags.groupName);
6360
+ if (groupId !== void 0 && groupName !== void 0) {
6361
+ throw new CliUsageError("Pass --group-id or --group-name, not both.");
6362
+ }
5359
6363
  return {
5360
6364
  data: await ctx.client().emails.importFigma(
5361
6365
  asSdkInput({
5362
6366
  figmaUrl,
5363
6367
  ...title === void 0 ? {} : { title },
5364
6368
  ...format === void 0 ? {} : { format },
5365
- ...subjectLine === void 0 ? {} : { subjectLine }
6369
+ ...subjectLine === void 0 ? {} : { subjectLine },
6370
+ ...groupId === void 0 ? {} : { groupId },
6371
+ ...groupName === void 0 ? {} : { groupName }
5366
6372
  }),
5367
6373
  requestOptions(flags)
5368
6374
  )
@@ -5408,6 +6414,10 @@ var emailsListCommand = defineCommand({
5408
6414
  route: { method: "GET", path: "/v1/emails" },
5409
6415
  commandClass: "read",
5410
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
+ },
5411
6421
  {
5412
6422
  flag: "--status <status>",
5413
6423
  summary: "Filter by status: generating | ready | failed"
@@ -5467,6 +6477,7 @@ var emailsListCommand = defineCommand({
5467
6477
  );
5468
6478
  const base = await readJsonFlag(ctx, flags.input, "--input");
5469
6479
  const input = mergeInput(base, {
6480
+ search: flagString(flags.search),
5470
6481
  status: flagString(flags.status),
5471
6482
  groupId: flagString(flags.groupId),
5472
6483
  sortBy: flagString(flags.sortBy) ?? flagString(flags.sort) ?? legacy.sortBy,
@@ -5541,14 +6552,13 @@ function renderEmails(rows) {
5541
6552
  { key: "updatedAt", header: "UPDATED" }
5542
6553
  ]);
5543
6554
  }
5544
-
5545
- // src/commands/emails/preview-clients.ts
5546
6555
  var emailsPreviewClientsCommand = defineCommand({
5547
6556
  path: ["emails", "preview-clients"],
5548
6557
  summary: "Start a rendering job across real email clients (10 credits); poll it with `emails get-client-preview`",
5549
6558
  sdkMethod: "emails.previewClients",
5550
6559
  route: { method: "POST", path: "/v1/emails/{emailId}/client-previews" },
5551
6560
  commandClass: "write",
6561
+ defaultTimeoutMs: PREVIEW_EMAIL_CLIENTS_DEFAULT_TIMEOUT_MS,
5552
6562
  isCredited: true,
5553
6563
  args: [
5554
6564
  { name: "emailId", summary: "Design id to preview", isRequired: true }
@@ -5828,8 +6838,48 @@ var fieldsListCommand = defineCommand({
5828
6838
  sdkMethod: "fields.list",
5829
6839
  route: { method: "GET", path: "/v1/fields" },
5830
6840
  commandClass: "read",
5831
- examples: ["brew-cli fields list"],
5832
- run: async ({ ctx }) => ({ data: await ctx.client().fields.list() })
6841
+ flags: [
6842
+ {
6843
+ flag: "--include <tokens>",
6844
+ summary: "Expansions: coverage (per-field fill stats: percent of contacts with a value, and top values)"
6845
+ },
6846
+ {
6847
+ flag: "--audience-id <audienceId>",
6848
+ summary: "Scope --include coverage to one saved audience (ignored without it)"
6849
+ },
6850
+ LIMIT_FLAG,
6851
+ CURSOR_FLAG,
6852
+ ALL_FLAG
6853
+ ],
6854
+ examples: [
6855
+ "brew-cli fields list",
6856
+ "brew-cli fields list --include coverage",
6857
+ "brew-cli fields list --include coverage --audience-id aud_2SmZOWV3ZQ7W5x6g3m4p --all --json"
6858
+ ],
6859
+ run: async ({ ctx, flags }) => {
6860
+ const input = {
6861
+ include: flagString(flags.include),
6862
+ audienceId: flagString(flags.audienceId),
6863
+ limit: flagInt(flags.limit, "--limit"),
6864
+ cursor: flagString(flags.cursor)
6865
+ };
6866
+ const fields = ctx.client().fields;
6867
+ if (flags.all === true) {
6868
+ const rows = await collectAll(
6869
+ ctx,
6870
+ (cursor) => fields.list(
6871
+ asSdkInput({
6872
+ ...input,
6873
+ ...cursor === void 0 ? {} : { cursor }
6874
+ })
6875
+ )
6876
+ );
6877
+ return {
6878
+ data: { data: rows, pagination: { cursor: null, hasMore: false } }
6879
+ };
6880
+ }
6881
+ return { data: await fields.list(asSdkInput(input)) };
6882
+ }
5833
6883
  });
5834
6884
 
5835
6885
  // src/commands/flows/get.ts
@@ -6024,9 +7074,31 @@ var integrationsListCommand = defineCommand({
6024
7074
  sdkMethod: "integrations.list",
6025
7075
  route: { method: "GET", path: "/v1/integrations" },
6026
7076
  commandClass: "read",
7077
+ flags: [LIMIT_FLAG, CURSOR_FLAG, ALL_FLAG],
6027
7078
  examples: ["brew-cli integrations list", "brew-cli integrations list --json"],
6028
- run: async ({ ctx }) => {
6029
- const result = await ctx.client().integrations.list();
7079
+ run: async ({ ctx, flags }) => {
7080
+ const limit = flagInt(flags.limit, "--limit");
7081
+ const cursor = flagString(flags.cursor);
7082
+ if (limit === void 0 && cursor === void 0 && flags.all !== true) {
7083
+ const result2 = await ctx.client().integrations.list();
7084
+ return { data: result2, human: renderIntegrations(result2.data) };
7085
+ }
7086
+ const page = (after) => rawRequest(ctx, {
7087
+ method: "GET",
7088
+ path: "/v1/integrations",
7089
+ query: {
7090
+ limit: limit === void 0 ? void 0 : String(limit),
7091
+ cursor: after
7092
+ }
7093
+ });
7094
+ if (flags.all === true) {
7095
+ const rows = await collectAll(ctx, (next) => page(next ?? cursor));
7096
+ return {
7097
+ data: { data: rows, pagination: { cursor: null, hasMore: false } },
7098
+ human: renderIntegrations(rows)
7099
+ };
7100
+ }
7101
+ const result = await page(cursor);
6030
7102
  return { data: result, human: renderIntegrations(result.data) };
6031
7103
  }
6032
7104
  });
@@ -6066,7 +7138,7 @@ var loginCommand = defineCommand({
6066
7138
  globals: { ...ctx.globals, apiKey: candidate },
6067
7139
  env: ctx.io.env
6068
7140
  });
6069
- await buildSdkClient(auth).usage.get();
7141
+ await buildSdkClient(auth, sdkTransportFor(ctx)).usage.get();
6070
7142
  const path = updateConfig(ctx.io.env, { apiKey: candidate });
6071
7143
  progress(ctx, `Logged in. Key stored in ${path} (0600).`);
6072
7144
  return {
@@ -6193,8 +7265,7 @@ var sendsResumeCommand = defineCommand({
6193
7265
  var templatesGetCommand = defineCommand({
6194
7266
  path: ["templates", "get"],
6195
7267
  summary: "Fetch one public template: its links and the referenceEmailId to remix; --include html adds its HTML",
6196
- sdkMethod: null,
6197
- isRawTransport: true,
7268
+ sdkMethod: "templates.get",
6198
7269
  route: { method: "GET", path: "/v1/templates/{templateId}" },
6199
7270
  commandClass: "read",
6200
7271
  args: [
@@ -6215,11 +7286,11 @@ var templatesGetCommand = defineCommand({
6215
7286
  "brew-cli templates get seed-vercel-newsletter --include html --json"
6216
7287
  ],
6217
7288
  run: async ({ ctx, args, flags }) => {
6218
- const body = await rawRequest(ctx, {
6219
- method: "GET",
6220
- path: `/v1/templates/${encodeURIComponent(args.templateId ?? "")}`,
6221
- query: { include: flagString(flags.include) }
6222
- });
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
+ );
6223
7294
  return { data: body };
6224
7295
  }
6225
7296
  });
@@ -6505,6 +7576,7 @@ var whoamiCommand = defineCommand({
6505
7576
  if (error instanceof BrewApiError && (error.status === 401 || error.status === 403)) {
6506
7577
  throw error;
6507
7578
  }
7579
+ ctx.signal.throwIfAborted();
6508
7580
  progress(
6509
7581
  ctx,
6510
7582
  "Warning: could not reach /v1/usage \u2014 showing local credential state only."
@@ -6535,6 +7607,7 @@ var ALL_COMMANDS = [
6535
7607
  contactsSearchCommand,
6536
7608
  contactsGetCommand,
6537
7609
  contactsCountCommand,
7610
+ contactsCountByCommand,
6538
7611
  contactsUpsertCommand,
6539
7612
  contactsUpsertManyCommand,
6540
7613
  contactsUpdateCommand,
@@ -6618,6 +7691,7 @@ var ALL_COMMANDS = [
6618
7691
  analyticsCampaignsCommand,
6619
7692
  analyticsAutomationsCommand,
6620
7693
  analyticsEventsCommand,
7694
+ analyticsEventCountsCommand,
6621
7695
  analyticsSendsListCommand,
6622
7696
  analyticsSendsGetCommand,
6623
7697
  analyticsTriggerInstancesListCommand,
@@ -6637,6 +7711,11 @@ var ALL_COMMANDS = [
6637
7711
  domainsHealthCommand,
6638
7712
  domainsUpdateCommand,
6639
7713
  domainsDeleteCommand,
7714
+ domainsUnsubscribesListCommand,
7715
+ domainsUnsubscribesAddCommand,
7716
+ domainsUnsubscribesRemoveCommand,
7717
+ domainsUnsubscribesImportCommand,
7718
+ domainsUnsubscribesExportCommand,
6640
7719
  contentGenerateImageCommand,
6641
7720
  contentGifCommand,
6642
7721
  contentTransformCommand,
@@ -6695,6 +7774,7 @@ Environment:
6695
7774
  Exit codes:
6696
7775
  0 success \xB7 1 API/runtime error \xB7 2 usage error \xB7 3 auth error
6697
7776
  4 confirmation required (re-run with --yes)
7777
+ 130 interrupted (SIGINT) \xB7 143 terminated (SIGTERM)
6698
7778
 
6699
7779
  Agents: run \`${CLI_NAME} docs --agent\` for the machine-readable manifest.
6700
7780
  `;
@@ -6712,8 +7792,8 @@ async function run(argv, io, commands = ALL_COMMANDS) {
6712
7792
  (text) => {
6713
7793
  commanderStderr.push(text);
6714
7794
  },
6715
- (code) => {
6716
- commandExitCode = code;
7795
+ (code2) => {
7796
+ commandExitCode = code2;
6717
7797
  }
6718
7798
  );
6719
7799
  const flushCommanderStderr = () => {
@@ -6733,9 +7813,51 @@ async function run(argv, io, commands = ALL_COMMANDS) {
6733
7813
  }
6734
7814
  flushCommanderStderr();
6735
7815
  const ctx = activeContext ?? makeFallbackContext(io, argv);
6736
- printError(ctx, error);
6737
- 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;
6738
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
+ };
6739
7861
  }
6740
7862
  function handleCommanderError(error, ctx, flush) {
6741
7863
  const isHelpCode = error.code === "commander.helpDisplayed" || error.code === "commander.help";
@@ -6824,16 +7946,21 @@ function registerCommand(program, spec, io, rawArgv, onContext, onExitCode) {
6824
7946
  },
6825
7947
  {}
6826
7948
  );
6827
- const ctx = makeContext({ io, flags, rawArgv });
7949
+ const ctx = makeContext({ io, flags, rawArgv, traits: traitsOf(spec) });
6828
7950
  onContext?.(ctx);
6829
7951
  const invocation = { ctx, args: positional, flags };
6830
7952
  await enforceConfirmation(spec, invocation);
6831
- const outcome = await spec.run(invocation);
6832
- if (outcome !== void 0) {
6833
- printData(ctx, outcome.data, outcome.human);
6834
- if (outcome.exitCode !== void 0 && outcome.exitCode !== 0) {
6835
- 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
+ }
6836
7961
  }
7962
+ } finally {
7963
+ ctx.stopDeadline();
6837
7964
  }
6838
7965
  });
6839
7966
  }
@@ -6872,20 +7999,77 @@ function buildCommandHelpFooter(spec) {
6872
7999
  return lines.join("\n");
6873
8000
  }
6874
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
+
6875
8044
  // src/bin.ts
6876
- function realIo() {
8045
+ function realIo(interrupter2) {
8046
+ const { signal } = interrupter2;
6877
8047
  return {
6878
8048
  stdout: process.stdout,
6879
8049
  stderr: process.stderr,
6880
8050
  isTtyOut: process.stdout.isTTY === true,
6881
8051
  isTtyIn: process.stdin.isTTY === true,
6882
8052
  env: process.env,
8053
+ signal,
6883
8054
  readStdin: async () => {
8055
+ const stop = () => {
8056
+ process.stdin.destroy();
8057
+ };
8058
+ signal.addEventListener("abort", stop, { once: true });
6884
8059
  process.stdin.setEncoding("utf8");
6885
8060
  let data = "";
6886
- for await (const chunk of process.stdin) {
6887
- 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);
6888
8071
  }
8072
+ signal.throwIfAborted();
6889
8073
  return data;
6890
8074
  },
6891
8075
  readLine: async (prompt) => {
@@ -6893,14 +8077,34 @@ function realIo() {
6893
8077
  input: process.stdin,
6894
8078
  output: process.stderr
6895
8079
  });
8080
+ rl.on("SIGINT", () => {
8081
+ interrupter2.trigger("SIGINT");
8082
+ });
6896
8083
  try {
6897
- return await rl.question(prompt);
8084
+ return await rl.question(prompt, { signal });
6898
8085
  } finally {
6899
8086
  rl.close();
6900
8087
  }
6901
8088
  }
6902
8089
  };
6903
8090
  }
6904
- 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
+ }
6905
8109
  //# sourceMappingURL=bin.js.map
6906
8110
  //# sourceMappingURL=bin.js.map