@wraps.dev/cli 3.19.0 → 3.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -61495,17 +61495,22 @@ Run ${pc45.cyan("wraps email init")} to deploy email infrastructure.
61495
61495
 
61496
61496
  // src/commands/email/suppressions.ts
61497
61497
  init_esm_shims();
61498
+ import { readFile as readFile6 } from "fs/promises";
61498
61499
  import { log as log49, note as note21 } from "@clack/prompts";
61499
61500
  init_json_output();
61500
61501
  var LIST_COMMAND3 = "email.suppressions.list";
61501
61502
  var REMOVE_COMMAND = "email.suppressions.remove";
61502
61503
  var LIST_USAGE3 = "wraps email suppressions list --json";
61503
61504
  var REMOVE_USAGE = "wraps email suppressions remove <email> --account <awsAccountId> --yes";
61505
+ var ADD_COMMAND = "email.suppressions.add";
61506
+ var ADD_USAGE = "wraps email suppressions add <email...> --account <awsAccountId> --reason bounce|complaint --yes";
61507
+ var MAX_BATCH = 100;
61504
61508
  var MAX_LIMIT3 = 100;
61505
61509
  var MAX_CURSOR_LENGTH3 = 8192;
61506
61510
  var MAX_EMAIL_LENGTH = 320;
61507
61511
  var MAX_ACCOUNT_ID_LENGTH2 = 128;
61508
61512
  var DIGITS_ONLY4 = /^\d+$/;
61513
+ var LINE_BREAK = /\r?\n/;
61509
61514
  var PERMISSION_TEXT = /update-role|permission/i;
61510
61515
  var DENIED_SUGGESTION2 = "Wraps cannot read or change the suppression list on these accounts: their AWS role lacks SES permission. Run `wraps platform update-role` to refresh the permissions, then retry.";
61511
61516
  var UNREACHABLE_SUGGESTION2 = "Wraps could not reach these accounts (an AWS error, or the assume-role failed). Run `wraps email health` to see each account's SES status, then retry.";
@@ -61643,11 +61648,15 @@ function httpFailure4(action, status2, text15, remove) {
61643
61648
  };
61644
61649
  }
61645
61650
  if (status2 >= 500) {
61646
- return {
61647
- code: "API_ERROR",
61648
- message: text15,
61649
- suggestion: action === "list" ? "The Wraps API failed. Retry shortly; to find a failing account, run `wraps email health`." : `The Wraps API failed. Retry shortly; check the address is still listed with \`${LIST_USAGE3}\` first.`
61650
- };
61651
+ let suggestion;
61652
+ if (action === "list") {
61653
+ suggestion = "The Wraps API failed. Retry shortly; to find a failing account, run `wraps email health`.";
61654
+ } else if (action === "add") {
61655
+ suggestion = "The Wraps API failed. Retry shortly; adding an address twice is safe, so re-running the same command is fine.";
61656
+ } else {
61657
+ suggestion = `The Wraps API failed. Retry shortly; check the address is still listed with \`${LIST_USAGE3}\` first.`;
61658
+ }
61659
+ return { code: "API_ERROR", message: text15, suggestion };
61651
61660
  }
61652
61661
  return {
61653
61662
  code: "API_ERROR",
@@ -61656,14 +61665,21 @@ function httpFailure4(action, status2, text15, remove) {
61656
61665
  };
61657
61666
  }
61658
61667
  async function callApi8(request) {
61659
- const { target, method, path: path3, query, onFailure } = request;
61668
+ const { target, method, path: path3, query, body: payload, onFailure } = request;
61660
61669
  const qs = query.toString();
61661
61670
  const url = `${target.apiBase}${path3}${qs ? `?${qs}` : ""}`;
61671
+ const headers = {
61672
+ Authorization: `Bearer ${target.token}`
61673
+ };
61674
+ if (payload !== void 0) {
61675
+ headers["Content-Type"] = "application/json";
61676
+ }
61662
61677
  let resp;
61663
61678
  try {
61664
61679
  resp = await fetch(url, {
61665
61680
  method,
61666
- headers: { Authorization: `Bearer ${target.token}` }
61681
+ headers,
61682
+ body: payload === void 0 ? void 0 : JSON.stringify(payload)
61667
61683
  });
61668
61684
  } catch (err) {
61669
61685
  return {
@@ -61946,10 +61962,322 @@ async function suppressionsRemove(options) {
61946
61962
  `Removed ${email} from the suppression list of account ${accountId}.`
61947
61963
  );
61948
61964
  } else {
61949
- log49.warn(
61950
- `${email} was no longer on the list of account ${accountId}; nothing changed.`
61965
+ log49.warn(`${email} was no longer on the list of account ${accountId}.`);
61966
+ }
61967
+ }
61968
+ function addFailure(status2, text15) {
61969
+ if (status2 === 400) {
61970
+ return {
61971
+ code: "VALIDATION_FAILED",
61972
+ message: text15,
61973
+ suggestion: `Check the arguments, then re-run: ${ADD_USAGE}`
61974
+ };
61975
+ }
61976
+ if (status2 === 404) {
61977
+ return {
61978
+ code: "NOT_FOUND",
61979
+ message: text15,
61980
+ suggestion: "Check that --account is the awsAccountId of a connected account. `wraps email health` lists each account's SES status."
61981
+ };
61982
+ }
61983
+ return httpFailure4("add", status2, text15);
61984
+ }
61985
+ function undoHint(accountId, reason) {
61986
+ const ack = reason === "COMPLAINT" ? " --acknowledge-complaint" : "";
61987
+ return `wraps email suppressions remove <email> --account ${accountId} --yes${ack}`;
61988
+ }
61989
+ function uniqueAddresses2(raw) {
61990
+ const seen = /* @__PURE__ */ new Map();
61991
+ for (const item of raw) {
61992
+ const email = item.trim();
61993
+ if (email !== "" && !seen.has(email.toLowerCase())) {
61994
+ seen.set(email.toLowerCase(), email);
61995
+ }
61996
+ }
61997
+ return [...seen.values()];
61998
+ }
61999
+ async function readAddressFile(path3) {
62000
+ try {
62001
+ const text15 = await readFile6(path3, "utf8");
62002
+ const lines = text15.split(LINE_BREAK).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#"));
62003
+ return { ok: true, value: lines };
62004
+ } catch (err) {
62005
+ return {
62006
+ ok: false,
62007
+ failure: {
62008
+ code: "INVALID_FLAG_VALUE",
62009
+ message: `Could not read --file ${path3} (${errorText10(err)})`,
62010
+ suggestion: "Check the path, then pass --file <path> to a text file with one address per line."
62011
+ }
62012
+ };
62013
+ }
62014
+ }
62015
+ function bulkResult(email, item) {
62016
+ if (item !== null && typeof item === "object") {
62017
+ const row = item;
62018
+ if (row.success === true) {
62019
+ return {
62020
+ email,
62021
+ success: true,
62022
+ alreadyPresent: row.alreadyPresent === true
62023
+ };
62024
+ }
62025
+ if (row.success === false) {
62026
+ return {
62027
+ email,
62028
+ success: false,
62029
+ error: typeof row.error === "string" ? row.error : "Failed to add suppression"
62030
+ };
62031
+ }
62032
+ }
62033
+ return {
62034
+ email,
62035
+ success: false,
62036
+ error: "The API returned no result for this address"
62037
+ };
62038
+ }
62039
+ async function collectAddresses(options) {
62040
+ const file = optionalText(
62041
+ "--file",
62042
+ options.file,
62043
+ `Pass --file <path> to a text file with one address per line, or give the addresses as arguments: ${ADD_USAGE}`
62044
+ );
62045
+ if (!file.ok) {
62046
+ return file;
62047
+ }
62048
+ let fromFile = [];
62049
+ if (file.value !== void 0) {
62050
+ const read = await readAddressFile(file.value);
62051
+ if (!read.ok) {
62052
+ return read;
62053
+ }
62054
+ fromFile = read.value;
62055
+ }
62056
+ const fromArgs = Array.isArray(options.emails) ? options.emails.filter((item) => typeof item === "string") : [];
62057
+ const emails = uniqueAddresses2([...fromArgs, ...fromFile]);
62058
+ if (emails.length === 0) {
62059
+ return {
62060
+ ok: false,
62061
+ failure: {
62062
+ code: "MISSING_REQUIRED_FLAG",
62063
+ message: "No email addresses given",
62064
+ suggestion: `Usage: ${ADD_USAGE}, or pass --file <path>`
62065
+ }
62066
+ };
62067
+ }
62068
+ if (emails.length > MAX_BATCH) {
62069
+ return {
62070
+ ok: false,
62071
+ failure: {
62072
+ code: "VALIDATION_FAILED",
62073
+ message: `${emails.length} addresses given; one command adds at most ${MAX_BATCH}`,
62074
+ suggestion: `Split the list into files of ${MAX_BATCH} or fewer, then run the command once per file with --file <path>.`
62075
+ }
62076
+ };
62077
+ }
62078
+ return { ok: true, value: emails };
62079
+ }
62080
+ function parseAccount2(raw) {
62081
+ const account = optionalText(
62082
+ "--account",
62083
+ raw,
62084
+ `Find the awsAccountId with \`${LIST_USAGE3}\`, then re-run: ${ADD_USAGE}`
62085
+ );
62086
+ if (!account.ok) {
62087
+ return account;
62088
+ }
62089
+ if (account.value === void 0) {
62090
+ return {
62091
+ ok: false,
62092
+ failure: {
62093
+ code: "VALIDATION_FAILED",
62094
+ message: "Missing --account <awsAccountId>",
62095
+ suggestion: `Find the awsAccountId with \`${LIST_USAGE3}\`, then re-run: ${ADD_USAGE}`
62096
+ }
62097
+ };
62098
+ }
62099
+ if (account.value.length > MAX_ACCOUNT_ID_LENGTH2) {
62100
+ return {
62101
+ ok: false,
62102
+ failure: {
62103
+ code: "INVALID_FLAG_VALUE",
62104
+ message: "--account is longer than 128 characters",
62105
+ suggestion: `Copy the awsAccountId exactly from \`${LIST_USAGE3}\`.`
62106
+ }
62107
+ };
62108
+ }
62109
+ return { ok: true, value: account.value };
62110
+ }
62111
+ function parseAddReason(raw) {
62112
+ const text15 = typeof raw === "string" ? raw.trim().toLowerCase() : "";
62113
+ if (text15 === "bounce") {
62114
+ return { ok: true, value: "BOUNCE" };
62115
+ }
62116
+ if (text15 === "complaint") {
62117
+ return { ok: true, value: "COMPLAINT" };
62118
+ }
62119
+ return {
62120
+ ok: false,
62121
+ failure: {
62122
+ code: raw === void 0 ? "MISSING_REQUIRED_FLAG" : "INVALID_FLAG_VALUE",
62123
+ message: "--reason must be bounce or complaint",
62124
+ suggestion: `Re-run with --reason bounce or --reason complaint: ${ADD_USAGE}`
62125
+ }
62126
+ };
62127
+ }
62128
+ async function sendAdds(target, accountId, reason, emails) {
62129
+ const only = emails.length === 1 ? emails[0] : void 0;
62130
+ if (only !== void 0) {
62131
+ const outcome2 = await callApi8({
62132
+ target,
62133
+ method: "POST",
62134
+ path: "/v1/suppressions",
62135
+ query: new URLSearchParams(),
62136
+ body: { awsAccountId: accountId, email: only, reason },
62137
+ onFailure: addFailure
62138
+ });
62139
+ if (!outcome2.ok) {
62140
+ return outcome2;
62141
+ }
62142
+ return {
62143
+ ok: true,
62144
+ value: [
62145
+ {
62146
+ email: only,
62147
+ success: true,
62148
+ alreadyPresent: outcome2.body.alreadyPresent === true
62149
+ }
62150
+ ]
62151
+ };
62152
+ }
62153
+ const outcome = await callApi8({
62154
+ target,
62155
+ method: "POST",
62156
+ path: "/v1/suppressions/bulk",
62157
+ query: new URLSearchParams(),
62158
+ body: {
62159
+ entries: emails.map((email) => ({
62160
+ awsAccountId: accountId,
62161
+ email,
62162
+ reason
62163
+ }))
62164
+ },
62165
+ onFailure: addFailure
62166
+ });
62167
+ if (!outcome.ok) {
62168
+ return outcome;
62169
+ }
62170
+ const rows = Array.isArray(outcome.body.results) ? outcome.body.results : [];
62171
+ return {
62172
+ ok: true,
62173
+ value: emails.map((email, index) => bulkResult(email, rows[index]))
62174
+ };
62175
+ }
62176
+ function printAddHuman(accountId, reason, results, counts) {
62177
+ for (const r6 of results) {
62178
+ if (!r6.success) {
62179
+ log49.error(`${r6.email}: ${r6.error ?? "Failed to add suppression"}`);
62180
+ } else if (r6.alreadyPresent === true) {
62181
+ log49.info(
62182
+ `${r6.email} was already suppressed on account ${accountId}; nothing changed.`
62183
+ );
62184
+ } else {
62185
+ log49.success(
62186
+ `Suppressed ${r6.email} on account ${accountId} as ${reason}.`
62187
+ );
62188
+ }
62189
+ }
62190
+ log49.info(
62191
+ `${counts.added} added, ${counts.alreadyPresent} already suppressed, ${counts.failed} failed.`
62192
+ );
62193
+ if (counts.added > 0) {
62194
+ log49.info(`Undo with: ${undoHint(accountId, reason)}`);
62195
+ }
62196
+ }
62197
+ async function suppressionsAdd(options) {
62198
+ const command6 = ADD_COMMAND;
62199
+ const addresses = await collectAddresses(options);
62200
+ if (!addresses.ok) {
62201
+ emitError5(command6, addresses.failure);
62202
+ return;
62203
+ }
62204
+ const account = parseAccount2(options.account);
62205
+ if (!account.ok) {
62206
+ emitError5(command6, account.failure);
62207
+ return;
62208
+ }
62209
+ const parsedReason = parseAddReason(options.reason);
62210
+ if (!parsedReason.ok) {
62211
+ emitError5(command6, parsedReason.failure);
62212
+ return;
62213
+ }
62214
+ const emails = addresses.value;
62215
+ const accountId = account.value;
62216
+ const reason = parsedReason.value;
62217
+ if (options.dryRun) {
62218
+ const data = {
62219
+ awsAccountId: accountId,
62220
+ reason,
62221
+ dryRun: true,
62222
+ emails,
62223
+ results: [],
62224
+ added: 0,
62225
+ alreadyPresent: 0,
62226
+ failed: 0
62227
+ };
62228
+ if (isJsonMode()) {
62229
+ jsonSuccess(command6, data);
62230
+ return;
62231
+ }
62232
+ note21(
62233
+ emails.join("\n"),
62234
+ `Dry run: ${emails.length} address(es) on account ${accountId} as ${reason}`
61951
62235
  );
62236
+ log49.info("No request was sent.");
62237
+ return;
62238
+ }
62239
+ if (!options.yes) {
62240
+ const shown = emails.length === 1 ? emails[0] : "<email...>";
62241
+ emitError5(command6, {
62242
+ code: "CONFIRMATION_REQUIRED",
62243
+ message: "Suppressing an address stops Wraps from sending to it. Nothing was added because --yes was not passed.",
62244
+ suggestion: `Re-run with --yes to confirm: wraps email suppressions add ${shown} --account ${accountId} --reason ${reason.toLowerCase()} --yes. Undo later with: ${undoHint(accountId, reason)}`
62245
+ });
62246
+ return;
62247
+ }
62248
+ const target = await connect5(command6, options.token);
62249
+ if (!target) {
62250
+ return;
62251
+ }
62252
+ const sent = await sendAdds(target, accountId, reason, emails);
62253
+ if (!sent.ok) {
62254
+ emitError5(command6, sent.failure);
62255
+ return;
62256
+ }
62257
+ const results = sent.value;
62258
+ const added = results.filter(
62259
+ (r6) => r6.success && r6.alreadyPresent !== true
62260
+ ).length;
62261
+ const alreadyPresent = results.filter(
62262
+ (r6) => r6.success && r6.alreadyPresent === true
62263
+ ).length;
62264
+ const failed = results.filter((r6) => !r6.success).length;
62265
+ if (failed > 0) {
62266
+ process.exitCode = 1;
62267
+ }
62268
+ if (isJsonMode()) {
62269
+ jsonSuccess(command6, {
62270
+ awsAccountId: accountId,
62271
+ reason,
62272
+ dryRun: false,
62273
+ results,
62274
+ added,
62275
+ alreadyPresent,
62276
+ failed
62277
+ });
62278
+ return;
61952
62279
  }
62280
+ printAddHuman(accountId, reason, results, { added, alreadyPresent, failed });
61953
62281
  }
61954
62282
 
61955
62283
  // src/commands/email/templates/init.ts
@@ -61960,7 +62288,7 @@ init_errors();
61960
62288
  init_json_output();
61961
62289
  init_output();
61962
62290
  import { existsSync as existsSync10 } from "fs";
61963
- import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
62291
+ import { mkdir as mkdir3, readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
61964
62292
  import { join as join15 } from "path";
61965
62293
  import * as clack43 from "@clack/prompts";
61966
62294
  import pc46 from "picocolors";
@@ -61968,7 +62296,7 @@ import pc46 from "picocolors";
61968
62296
  // src/utils/shared/scaffold-claude.ts
61969
62297
  init_esm_shims();
61970
62298
  import { existsSync as existsSync9 } from "fs";
61971
- import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
62299
+ import { mkdir as mkdir2, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
61972
62300
  import { join as join14 } from "path";
61973
62301
  var CLAUDE_MD_HEADER = `# Wraps
61974
62302
 
@@ -61997,7 +62325,7 @@ ${wrappedContent}
61997
62325
  );
61998
62326
  return;
61999
62327
  }
62000
- let existing = await readFile6(claudeMdPath, "utf-8");
62328
+ let existing = await readFile7(claudeMdPath, "utf-8");
62001
62329
  const markerRegex = new RegExp(
62002
62330
  `${escapeRegex(startMarker)}[\\s\\S]*?${escapeRegex(endMarker)}`,
62003
62331
  "g"
@@ -62449,7 +62777,7 @@ async function templatesInit(options) {
62449
62777
  try {
62450
62778
  const pkgPath = join15(cwd, "package.json");
62451
62779
  if (existsSync10(pkgPath)) {
62452
- const pkg = JSON.parse(await readFile7(pkgPath, "utf-8"));
62780
+ const pkg = JSON.parse(await readFile8(pkgPath, "utf-8"));
62453
62781
  if (pkg.homepage) {
62454
62782
  try {
62455
62783
  const url = new URL(pkg.homepage);
@@ -62506,7 +62834,7 @@ async function templatesInit(options) {
62506
62834
  await writeFile8(gitignorePath, ".wraps/\n", "utf-8");
62507
62835
  const rootGitignorePath = join15(cwd, ".gitignore");
62508
62836
  if (existsSync10(rootGitignorePath)) {
62509
- const gitignoreContent = await readFile7(rootGitignorePath, "utf-8");
62837
+ const gitignoreContent = await readFile8(rootGitignorePath, "utf-8");
62510
62838
  if (!gitignoreContent.includes("wraps/.wraps")) {
62511
62839
  await writeFile8(
62512
62840
  rootGitignorePath,
@@ -63281,7 +63609,7 @@ init_esm_shims();
63281
63609
  init_events();
63282
63610
  import { createHash as createHash5 } from "crypto";
63283
63611
  import { existsSync as existsSync14 } from "fs";
63284
- import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile11 } from "fs/promises";
63612
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile11 } from "fs/promises";
63285
63613
  import { join as join19 } from "path";
63286
63614
  import * as clack46 from "@clack/prompts";
63287
63615
  import pc49 from "picocolors";
@@ -63382,7 +63710,7 @@ init_json_output();
63382
63710
  // src/utils/shared/lockfile.ts
63383
63711
  init_esm_shims();
63384
63712
  import { existsSync as existsSync13 } from "fs";
63385
- import { mkdir as mkdir5, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
63713
+ import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile10 } from "fs/promises";
63386
63714
  import { join as join18 } from "path";
63387
63715
  function getLockfilePath(wrapsDir) {
63388
63716
  return join18(wrapsDir, ".wraps", "lockfile.json");
@@ -63393,7 +63721,7 @@ async function loadLockfile(wrapsDir) {
63393
63721
  return { version: "1.0.0", lastSync: "", templates: {}, workflows: {} };
63394
63722
  }
63395
63723
  try {
63396
- const content = await readFile8(path3, "utf-8");
63724
+ const content = await readFile9(path3, "utf-8");
63397
63725
  const parsed = JSON.parse(content);
63398
63726
  if (!parsed.templates) {
63399
63727
  parsed.templates = {};
@@ -63543,7 +63871,7 @@ async function templatesPush(options) {
63543
63871
  for (const file of templateFiles) {
63544
63872
  const slug = file.replace(/\.tsx?$/, "");
63545
63873
  const filePath = join19(templatesDir, file);
63546
- const source = await readFile9(filePath, "utf-8");
63874
+ const source = await readFile10(filePath, "utf-8");
63547
63875
  const sourceHash = sha256(source);
63548
63876
  const existsRemotely = remoteTemplateSlugs === null || remoteTemplateSlugs.has(slug);
63549
63877
  if (isTemplateUnchanged({
@@ -64094,10 +64422,12 @@ var CMD3 = {
64094
64422
  get: "email.templates.get",
64095
64423
  pull: "email.templates.pull",
64096
64424
  publish: "email.templates.publish",
64097
- duplicate: "email.templates.duplicate"
64425
+ duplicate: "email.templates.duplicate",
64426
+ delete: "email.templates.delete"
64098
64427
  };
64099
64428
  var TEMPLATE_STATUSES = ["DRAFT", "PUBLISHED", "ARCHIVED"];
64100
64429
  var TEMPLATE_CHANNELS = ["email", "sms"];
64430
+ var DELETE_BULK_MAX = 100;
64101
64431
  var PULL_PAGE_SIZE = 200;
64102
64432
  var PULL_MAX_PAGES = 1e3;
64103
64433
  var SAFE_SLUG = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
@@ -64547,6 +64877,154 @@ async function templatesDuplicate(options) {
64547
64877
  }
64548
64878
  log54.success(`Created draft copy ${copy.id} (${copy.name}).`);
64549
64879
  }
64880
+ async function deleteFailure(resp, singleId) {
64881
+ if (resp.status === 409) {
64882
+ const body = await resp.json().catch(() => null);
64883
+ const blockingIds = Array.isArray(body?.blockingIds) ? body.blockingIds.filter((id) => typeof id === "string") : [];
64884
+ return {
64885
+ code: "CONFLICT",
64886
+ message: `${firstText(body?.error) ?? "A broadcast or workflow still uses one of these templates."} Blocking template ids: ${blockingIds.join(", ") || "none reported"}.`,
64887
+ suggestion: "Point each draft broadcast that uses a blocking template at another one (`wraps broadcasts update <id> --template-id <other-id>`, or `--clear-template-id`). Finish or cancel each scheduled, queued or processing broadcast that uses one (`wraps broadcasts cancel <id> --yes`), and pause each enabled workflow in the dashboard, or, for a workflow defined in code, remove the template from its steps and run `wraps email workflows push` again (if the push reports a dashboard edit, ask the user before using `--force`, which overwrites those edits). Find the workflows with `wraps workflows list` and the broadcasts in the dashboard. Then re-run the delete. Nothing was deleted."
64888
+ };
64889
+ }
64890
+ if (resp.status === 404 && singleId !== void 0) {
64891
+ return {
64892
+ code: "NOT_FOUND",
64893
+ message: `Template ${singleId} was not found in this organization. Nothing was deleted.`,
64894
+ suggestion: "Check the id with `wraps email templates list`, then re-run the delete."
64895
+ };
64896
+ }
64897
+ return apiFailure2(resp);
64898
+ }
64899
+ function deleteIds(options) {
64900
+ return [
64901
+ ...new Set(
64902
+ (options.ids ?? []).map((id) => id.trim()).filter((id) => id !== "")
64903
+ )
64904
+ ];
64905
+ }
64906
+ function printDeleteDryRun(command6, ids, route) {
64907
+ if (isJsonMode()) {
64908
+ jsonSuccess(command6, {
64909
+ dryRun: true,
64910
+ route,
64911
+ wouldDelete: ids,
64912
+ deleted: [],
64913
+ notFound: [],
64914
+ count: 0
64915
+ });
64916
+ return;
64917
+ }
64918
+ log54.info(
64919
+ `Dry run: would delete ${ids.length} template(s) via the ${route} route: ${ids.join(", ")}. Nothing was deleted.`
64920
+ );
64921
+ }
64922
+ function sendDelete(command6, target, ids, singleId) {
64923
+ const confirm35 = new URLSearchParams({ confirm: "true" });
64924
+ if (singleId === void 0) {
64925
+ return call(command6, target, withQuery("/v1/templates", confirm35), {
64926
+ method: "DELETE",
64927
+ body: { ids }
64928
+ });
64929
+ }
64930
+ return call(
64931
+ command6,
64932
+ target,
64933
+ withQuery(`/v1/templates/${encodeURIComponent(singleId)}`, confirm35),
64934
+ { method: "DELETE" }
64935
+ );
64936
+ }
64937
+ async function readDeleteResult(command6, resp, ids, singleId) {
64938
+ if (singleId !== void 0) {
64939
+ const body2 = await readBody(resp);
64940
+ if (!body2 || body2.success !== true) {
64941
+ failUnexpected(command6);
64942
+ return null;
64943
+ }
64944
+ return { deleted: [singleId], notFound: [] };
64945
+ }
64946
+ const body = await readBody(resp);
64947
+ if (!(body && Array.isArray(body.notFound))) {
64948
+ failUnexpected(command6);
64949
+ return null;
64950
+ }
64951
+ const notFound = body.notFound.filter(
64952
+ (id) => typeof id === "string"
64953
+ );
64954
+ return {
64955
+ deleted: ids.filter((id) => !notFound.includes(id)),
64956
+ notFound
64957
+ };
64958
+ }
64959
+ function printDeleteResult(route, result) {
64960
+ const { deleted, notFound } = result;
64961
+ log54.success(
64962
+ route === "single" ? `Deleted template ${deleted[0]}.` : `Deleted ${deleted.length} template(s).`
64963
+ );
64964
+ if (notFound.length > 0) {
64965
+ log54.warn(
64966
+ `Not found in this organization, so skipped: ${notFound.join(", ")}.`
64967
+ );
64968
+ }
64969
+ }
64970
+ async function templatesDelete(options) {
64971
+ const command6 = CMD3.delete;
64972
+ const ids = deleteIds(options);
64973
+ if (ids.length === 0) {
64974
+ failMissingId(command6, "wraps email templates delete <id...> --yes");
64975
+ return;
64976
+ }
64977
+ if (ids.length > DELETE_BULK_MAX) {
64978
+ failValidation(
64979
+ command6,
64980
+ `${ids.length} template ids were given, but one delete accepts at most ${DELETE_BULK_MAX}. Nothing was deleted.`,
64981
+ `Split the ids into batches of ${DELETE_BULK_MAX} or fewer, and run one delete per batch with --yes.`
64982
+ );
64983
+ return;
64984
+ }
64985
+ const [first] = ids;
64986
+ const singleId = ids.length === 1 ? first : void 0;
64987
+ const route = singleId === void 0 ? "bulk" : "single";
64988
+ if (options.dryRun) {
64989
+ printDeleteDryRun(command6, ids, route);
64990
+ return;
64991
+ }
64992
+ if (!options.yes) {
64993
+ fail9(command6, {
64994
+ code: "CONFIRMATION_REQUIRED",
64995
+ message: `Deleting ${ids.length} template(s) removes them and their versions. A published SES copy is removed too, unless another template still uses that name. If any template is still used by a draft, scheduled, queued or processing broadcast, or an enabled workflow, the whole delete is refused. Nothing was deleted.`,
64996
+ suggestion: `Preview with \`wraps email templates delete ${ids.join(" ")} --dry-run\`, then re-run with --yes.`
64997
+ });
64998
+ return;
64999
+ }
65000
+ const target = await connect6(command6, options.token);
65001
+ if (!target) {
65002
+ return;
65003
+ }
65004
+ const resp = await sendDelete(command6, target, ids, singleId);
65005
+ if (!resp) {
65006
+ return;
65007
+ }
65008
+ if (!resp.ok) {
65009
+ fail9(command6, await deleteFailure(resp, singleId));
65010
+ return;
65011
+ }
65012
+ const result = await readDeleteResult(command6, resp, ids, singleId);
65013
+ if (!result) {
65014
+ return;
65015
+ }
65016
+ if (isJsonMode()) {
65017
+ jsonSuccess(command6, {
65018
+ dryRun: false,
65019
+ route,
65020
+ deleted: result.deleted,
65021
+ notFound: result.notFound,
65022
+ count: result.deleted.length
65023
+ });
65024
+ return;
65025
+ }
65026
+ printDeleteResult(route, result);
65027
+ }
64550
65028
  async function fetchAllPullRows(command6, target) {
64551
65029
  const rows = [];
64552
65030
  const seenCursors = /* @__PURE__ */ new Set();
@@ -67110,7 +67588,7 @@ init_events();
67110
67588
  init_json_output();
67111
67589
  init_output();
67112
67590
  import { existsSync as existsSync16 } from "fs";
67113
- import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile13 } from "fs/promises";
67591
+ import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile13 } from "fs/promises";
67114
67592
  import { join as join21 } from "path";
67115
67593
  import * as clack50 from "@clack/prompts";
67116
67594
  import pc53 from "picocolors";
@@ -67181,7 +67659,7 @@ defineWorkflow({
67181
67659
  | Type | Required Config | Description |
67182
67660
  |------|----------------|-------------|
67183
67661
  | \`contact_created\` | \u2014 | Fires when a new contact is created |
67184
- | \`contact_updated\` | \u2014 | Fires when a contact is updated |
67662
+ | \`contact_updated\` | \u2014 | Fires on every contact update through the API or CLI (\`PATCH /v1/contacts/:id\`, \`wraps contacts update\`), when a contact is edited in the dashboard, and when its email status changes (suppressed, bounced, complained, unsubscribed, or active again after a suppression is removed). Imports (dashboard CSV, \`POST /v1/contacts/import\`, \`wraps contacts import\`) don't fire it. Event data carries \`updatedFields\` and \`changeSource\`: \`api\` (an API key), \`user\` (a signed-in user in the dashboard or CLI), \`bounce\`, \`complaint\`, \`ses_suppression\` or \`unsubscribe\` (the unsubscribe link, or the preference centre's "unsubscribe from all"). If the workflow sends email, set \`reentryDelaySeconds\` or branch on \`changeSource\`, so a bounce or unsubscribe from its own email does not start it again |
67185
67663
  | \`event\` | \`eventName: string\` | Fires on a custom event (e.g. \`'cart.abandoned'\`) |
67186
67664
  | \`segment_entry\` | \`segmentId: string\` | Fires when a contact enters a segment |
67187
67665
  | \`segment_exit\` | \`segmentId: string\` | Fires when a contact exits a segment |
@@ -67523,7 +68001,7 @@ async function workflowsInit(options) {
67523
68001
  await mkdir8(workflowsDir, { recursive: true });
67524
68002
  const configPath = join21(cwd, "wraps", "wraps.config.ts");
67525
68003
  if (existsSync16(configPath)) {
67526
- const configContent = await readFile10(configPath, "utf-8");
68004
+ const configContent = await readFile11(configPath, "utf-8");
67527
68005
  if (!configContent.includes("workflowsDir")) {
67528
68006
  const updated = configContent.replace(
67529
68007
  /}\);(\s*)$/,
@@ -67843,7 +68321,7 @@ function assignPositions(steps, transitions) {
67843
68321
  init_esm_shims();
67844
68322
  import { createHash as createHash6 } from "crypto";
67845
68323
  import { existsSync as existsSync17 } from "fs";
67846
- import { mkdir as mkdir9, readdir as readdir4, readFile as readFile11, writeFile as writeFile14 } from "fs/promises";
68324
+ import { mkdir as mkdir9, readdir as readdir4, readFile as readFile12, writeFile as writeFile14 } from "fs/promises";
67847
68325
  import { basename as basename2, join as join23 } from "path";
67848
68326
  async function discoverWorkflows(dir, filter) {
67849
68327
  if (!existsSync17(dir)) {
@@ -67866,7 +68344,7 @@ async function discoverWorkflows(dir, filter) {
67866
68344
  }
67867
68345
  async function parseWorkflowTs(filePath, wrapsDir) {
67868
68346
  const { build: build2 } = await import("esbuild");
67869
- const source = await readFile11(filePath, "utf-8");
68347
+ const source = await readFile12(filePath, "utf-8");
67870
68348
  const sourceHash = createHash6("sha256").update(source).digest("hex");
67871
68349
  const slug = basename2(filePath, ".ts");
67872
68350
  const shimDir = join23(wrapsDir, ".wraps", "_shims");
@@ -82708,6 +83186,9 @@ function showHelp() {
82708
83186
  console.log(
82709
83187
  ` ${pc87.cyan("email suppressions list")} List the SES suppression list (--account <awsAccountId>, --reason bounce|complaint, --limit, --cursor). A page can be empty while hasMore is true.`
82710
83188
  );
83189
+ console.log(
83190
+ ` ${pc87.cyan("email suppressions add <email...>")} Suppress addresses (needs --account, --reason bounce|complaint and --yes; --file for a list; --dry-run previews)`
83191
+ );
82711
83192
  console.log(
82712
83193
  ` ${pc87.cyan("email suppressions remove")} Remove one address (needs --account and --yes; --dry-run previews; --acknowledge-complaint for complaint suppressions)
82713
83194
  `
@@ -82757,6 +83238,9 @@ function showHelp() {
82757
83238
  console.log(
82758
83239
  ` ${pc87.cyan("email templates duplicate")} <id> Copy a template as a new draft`
82759
83240
  );
83241
+ console.log(
83242
+ ` ${pc87.cyan("email templates delete")} <id...> Delete templates (--yes; --dry-run previews; refuses in-use templates)`
83243
+ );
82760
83244
  console.log(
82761
83245
  ` ${pc87.cyan("email templates init")} Initialize templates-as-code`
82762
83246
  );
@@ -83133,6 +83617,10 @@ var parsedCli = parseCliArgs(process.argv);
83133
83617
  var flags = parsedCli.flags;
83134
83618
  var { sub, extraPositionals } = parsedCli;
83135
83619
  var [primaryCommand, subCommand] = sub;
83620
+ var VARIADIC_COMMANDS = /* @__PURE__ */ new Set([
83621
+ "email templates delete",
83622
+ "email suppressions add"
83623
+ ]);
83136
83624
  setJsonMode(flags.json === true);
83137
83625
  if (!primaryCommand) {
83138
83626
  async function interactiveMenu() {
@@ -83315,7 +83803,7 @@ async function run() {
83315
83803
  telemetry.markNotificationShown();
83316
83804
  }
83317
83805
  try {
83318
- if (extraPositionals.length > 0) {
83806
+ if (extraPositionals.length > 0 && !VARIADIC_COMMANDS.has(sub.slice(0, 3).join(" "))) {
83319
83807
  throw errors.invalidFlagUsage(
83320
83808
  `Unrecognized argument${extraPositionals.length > 1 ? "s" : ""}: ${extraPositionals.join(" ")}`,
83321
83809
  "A boolean flag (such as --root or --json) takes no value, so a value after it is discarded. Check the flags before these arguments, or run the command with --help."
@@ -83837,6 +84325,16 @@ async function run() {
83837
84325
  region: flags.region
83838
84326
  });
83839
84327
  break;
84328
+ case "delete":
84329
+ await templatesDelete({
84330
+ ids: sub.slice(3),
84331
+ yes: flags.yes,
84332
+ dryRun: flags.dryRun,
84333
+ json: flags.json,
84334
+ token: flags.token,
84335
+ region: flags.region
84336
+ });
84337
+ break;
83840
84338
  case "preview":
83841
84339
  await templatesPreview({
83842
84340
  port: flags.port,
@@ -83848,7 +84346,7 @@ async function run() {
83848
84346
  throw errors.unknownCommand(
83849
84347
  "templates command",
83850
84348
  templatesSubCommand,
83851
- "Available commands: init, push, preview, list, get, pull, publish, duplicate"
84349
+ "Available commands: init, push, preview, list, get, pull, publish, duplicate, delete"
83852
84350
  );
83853
84351
  }
83854
84352
  break;
@@ -83935,6 +84433,19 @@ async function run() {
83935
84433
  region: flags.region
83936
84434
  });
83937
84435
  break;
84436
+ case "add":
84437
+ await suppressionsAdd({
84438
+ emails: sub.slice(3),
84439
+ file: flags.file,
84440
+ account: flags.account,
84441
+ reason: flags.reason,
84442
+ yes: flags.yes,
84443
+ dryRun: flags.dryRun,
84444
+ json: flags.json,
84445
+ token: flags.token,
84446
+ region: flags.region
84447
+ });
84448
+ break;
83938
84449
  case "remove":
83939
84450
  await suppressionsRemove({
83940
84451
  email: sub[3],
@@ -83951,7 +84462,7 @@ async function run() {
83951
84462
  throw errors.unknownCommand(
83952
84463
  "suppressions command",
83953
84464
  suppressionsSubCommand,
83954
- "Available commands: list, remove <email>"
84465
+ "Available commands: list, add <email...>, remove <email>"
83955
84466
  );
83956
84467
  }
83957
84468
  break;