@wraps.dev/cli 3.19.0 → 3.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js 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 {
@@ -61951,6 +61967,320 @@ async function suppressionsRemove(options) {
61951
61967
  );
61952
61968
  }
61953
61969
  }
61970
+ function addFailure(status2, text15) {
61971
+ if (status2 === 400) {
61972
+ return {
61973
+ code: "VALIDATION_FAILED",
61974
+ message: text15,
61975
+ suggestion: `Check the arguments, then re-run: ${ADD_USAGE}`
61976
+ };
61977
+ }
61978
+ if (status2 === 404) {
61979
+ return {
61980
+ code: "NOT_FOUND",
61981
+ message: text15,
61982
+ suggestion: "Check that --account is the awsAccountId of a connected account. `wraps email health` lists each account's SES status."
61983
+ };
61984
+ }
61985
+ return httpFailure4("add", status2, text15);
61986
+ }
61987
+ function undoHint(accountId, reason) {
61988
+ const ack = reason === "COMPLAINT" ? " --acknowledge-complaint" : "";
61989
+ return `wraps email suppressions remove <email> --account ${accountId} --yes${ack}`;
61990
+ }
61991
+ function uniqueAddresses2(raw) {
61992
+ const seen = /* @__PURE__ */ new Map();
61993
+ for (const item of raw) {
61994
+ const email = item.trim();
61995
+ if (email !== "" && !seen.has(email.toLowerCase())) {
61996
+ seen.set(email.toLowerCase(), email);
61997
+ }
61998
+ }
61999
+ return [...seen.values()];
62000
+ }
62001
+ async function readAddressFile(path3) {
62002
+ try {
62003
+ const text15 = await readFile6(path3, "utf8");
62004
+ const lines = text15.split(LINE_BREAK).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#"));
62005
+ return { ok: true, value: lines };
62006
+ } catch (err) {
62007
+ return {
62008
+ ok: false,
62009
+ failure: {
62010
+ code: "INVALID_FLAG_VALUE",
62011
+ message: `Could not read --file ${path3} (${errorText10(err)})`,
62012
+ suggestion: "Check the path, then pass --file <path> to a text file with one address per line."
62013
+ }
62014
+ };
62015
+ }
62016
+ }
62017
+ function bulkResult(email, item) {
62018
+ if (item !== null && typeof item === "object") {
62019
+ const row = item;
62020
+ if (row.success === true) {
62021
+ return {
62022
+ email,
62023
+ success: true,
62024
+ alreadyPresent: row.alreadyPresent === true
62025
+ };
62026
+ }
62027
+ if (row.success === false) {
62028
+ return {
62029
+ email,
62030
+ success: false,
62031
+ error: typeof row.error === "string" ? row.error : "Failed to add suppression"
62032
+ };
62033
+ }
62034
+ }
62035
+ return {
62036
+ email,
62037
+ success: false,
62038
+ error: "The API returned no result for this address"
62039
+ };
62040
+ }
62041
+ async function collectAddresses(options) {
62042
+ const file = optionalText(
62043
+ "--file",
62044
+ options.file,
62045
+ `Pass --file <path> to a text file with one address per line, or give the addresses as arguments: ${ADD_USAGE}`
62046
+ );
62047
+ if (!file.ok) {
62048
+ return file;
62049
+ }
62050
+ let fromFile = [];
62051
+ if (file.value !== void 0) {
62052
+ const read = await readAddressFile(file.value);
62053
+ if (!read.ok) {
62054
+ return read;
62055
+ }
62056
+ fromFile = read.value;
62057
+ }
62058
+ const fromArgs = Array.isArray(options.emails) ? options.emails.filter((item) => typeof item === "string") : [];
62059
+ const emails = uniqueAddresses2([...fromArgs, ...fromFile]);
62060
+ if (emails.length === 0) {
62061
+ return {
62062
+ ok: false,
62063
+ failure: {
62064
+ code: "MISSING_REQUIRED_FLAG",
62065
+ message: "No email addresses given",
62066
+ suggestion: `Usage: ${ADD_USAGE}, or pass --file <path>`
62067
+ }
62068
+ };
62069
+ }
62070
+ if (emails.length > MAX_BATCH) {
62071
+ return {
62072
+ ok: false,
62073
+ failure: {
62074
+ code: "VALIDATION_FAILED",
62075
+ message: `${emails.length} addresses given; one command adds at most ${MAX_BATCH}`,
62076
+ suggestion: `Split the list into files of ${MAX_BATCH} or fewer, then run the command once per file with --file <path>.`
62077
+ }
62078
+ };
62079
+ }
62080
+ return { ok: true, value: emails };
62081
+ }
62082
+ function parseAccount2(raw) {
62083
+ const account = optionalText(
62084
+ "--account",
62085
+ raw,
62086
+ `Find the awsAccountId with \`${LIST_USAGE3}\`, then re-run: ${ADD_USAGE}`
62087
+ );
62088
+ if (!account.ok) {
62089
+ return account;
62090
+ }
62091
+ if (account.value === void 0) {
62092
+ return {
62093
+ ok: false,
62094
+ failure: {
62095
+ code: "VALIDATION_FAILED",
62096
+ message: "Missing --account <awsAccountId>",
62097
+ suggestion: `Find the awsAccountId with \`${LIST_USAGE3}\`, then re-run: ${ADD_USAGE}`
62098
+ }
62099
+ };
62100
+ }
62101
+ if (account.value.length > MAX_ACCOUNT_ID_LENGTH2) {
62102
+ return {
62103
+ ok: false,
62104
+ failure: {
62105
+ code: "INVALID_FLAG_VALUE",
62106
+ message: "--account is longer than 128 characters",
62107
+ suggestion: `Copy the awsAccountId exactly from \`${LIST_USAGE3}\`.`
62108
+ }
62109
+ };
62110
+ }
62111
+ return { ok: true, value: account.value };
62112
+ }
62113
+ function parseAddReason(raw) {
62114
+ const text15 = typeof raw === "string" ? raw.trim().toLowerCase() : "";
62115
+ if (text15 === "bounce") {
62116
+ return { ok: true, value: "BOUNCE" };
62117
+ }
62118
+ if (text15 === "complaint") {
62119
+ return { ok: true, value: "COMPLAINT" };
62120
+ }
62121
+ return {
62122
+ ok: false,
62123
+ failure: {
62124
+ code: raw === void 0 ? "MISSING_REQUIRED_FLAG" : "INVALID_FLAG_VALUE",
62125
+ message: "--reason must be bounce or complaint",
62126
+ suggestion: `Re-run with --reason bounce or --reason complaint: ${ADD_USAGE}`
62127
+ }
62128
+ };
62129
+ }
62130
+ async function sendAdds(target, accountId, reason, emails) {
62131
+ const only = emails.length === 1 ? emails[0] : void 0;
62132
+ if (only !== void 0) {
62133
+ const outcome2 = await callApi8({
62134
+ target,
62135
+ method: "POST",
62136
+ path: "/v1/suppressions",
62137
+ query: new URLSearchParams(),
62138
+ body: { awsAccountId: accountId, email: only, reason },
62139
+ onFailure: addFailure
62140
+ });
62141
+ if (!outcome2.ok) {
62142
+ return outcome2;
62143
+ }
62144
+ return {
62145
+ ok: true,
62146
+ value: [
62147
+ {
62148
+ email: only,
62149
+ success: true,
62150
+ alreadyPresent: outcome2.body.alreadyPresent === true
62151
+ }
62152
+ ]
62153
+ };
62154
+ }
62155
+ const outcome = await callApi8({
62156
+ target,
62157
+ method: "POST",
62158
+ path: "/v1/suppressions/bulk",
62159
+ query: new URLSearchParams(),
62160
+ body: {
62161
+ entries: emails.map((email) => ({
62162
+ awsAccountId: accountId,
62163
+ email,
62164
+ reason
62165
+ }))
62166
+ },
62167
+ onFailure: addFailure
62168
+ });
62169
+ if (!outcome.ok) {
62170
+ return outcome;
62171
+ }
62172
+ const rows = Array.isArray(outcome.body.results) ? outcome.body.results : [];
62173
+ return {
62174
+ ok: true,
62175
+ value: emails.map((email, index) => bulkResult(email, rows[index]))
62176
+ };
62177
+ }
62178
+ function printAddHuman(accountId, reason, results, counts) {
62179
+ for (const r6 of results) {
62180
+ if (!r6.success) {
62181
+ log49.error(`${r6.email}: ${r6.error ?? "Failed to add suppression"}`);
62182
+ } else if (r6.alreadyPresent === true) {
62183
+ log49.info(
62184
+ `${r6.email} was already suppressed on account ${accountId}; nothing changed.`
62185
+ );
62186
+ } else {
62187
+ log49.success(
62188
+ `Suppressed ${r6.email} on account ${accountId} as ${reason}.`
62189
+ );
62190
+ }
62191
+ }
62192
+ log49.info(
62193
+ `${counts.added} added, ${counts.alreadyPresent} already suppressed, ${counts.failed} failed.`
62194
+ );
62195
+ if (counts.added > 0) {
62196
+ log49.info(`Undo with: ${undoHint(accountId, reason)}`);
62197
+ }
62198
+ }
62199
+ async function suppressionsAdd(options) {
62200
+ const command6 = ADD_COMMAND;
62201
+ const addresses = await collectAddresses(options);
62202
+ if (!addresses.ok) {
62203
+ emitError5(command6, addresses.failure);
62204
+ return;
62205
+ }
62206
+ const account = parseAccount2(options.account);
62207
+ if (!account.ok) {
62208
+ emitError5(command6, account.failure);
62209
+ return;
62210
+ }
62211
+ const parsedReason = parseAddReason(options.reason);
62212
+ if (!parsedReason.ok) {
62213
+ emitError5(command6, parsedReason.failure);
62214
+ return;
62215
+ }
62216
+ const emails = addresses.value;
62217
+ const accountId = account.value;
62218
+ const reason = parsedReason.value;
62219
+ if (options.dryRun) {
62220
+ const data = {
62221
+ awsAccountId: accountId,
62222
+ reason,
62223
+ dryRun: true,
62224
+ emails,
62225
+ results: [],
62226
+ added: 0,
62227
+ alreadyPresent: 0,
62228
+ failed: 0
62229
+ };
62230
+ if (isJsonMode()) {
62231
+ jsonSuccess(command6, data);
62232
+ return;
62233
+ }
62234
+ note21(
62235
+ emails.join("\n"),
62236
+ `Dry run: ${emails.length} address(es) on account ${accountId} as ${reason}`
62237
+ );
62238
+ log49.info("No request was sent.");
62239
+ return;
62240
+ }
62241
+ if (!options.yes) {
62242
+ const shown = emails.length === 1 ? emails[0] : "<email...>";
62243
+ emitError5(command6, {
62244
+ code: "CONFIRMATION_REQUIRED",
62245
+ message: "Suppressing an address stops Wraps from sending to it. Nothing was added because --yes was not passed.",
62246
+ suggestion: `Re-run with --yes to confirm: wraps email suppressions add ${shown} --account ${accountId} --reason ${reason.toLowerCase()} --yes. Undo later with: ${undoHint(accountId, reason)}`
62247
+ });
62248
+ return;
62249
+ }
62250
+ const target = await connect5(command6, options.token);
62251
+ if (!target) {
62252
+ return;
62253
+ }
62254
+ const sent = await sendAdds(target, accountId, reason, emails);
62255
+ if (!sent.ok) {
62256
+ emitError5(command6, sent.failure);
62257
+ return;
62258
+ }
62259
+ const results = sent.value;
62260
+ const added = results.filter(
62261
+ (r6) => r6.success && r6.alreadyPresent !== true
62262
+ ).length;
62263
+ const alreadyPresent = results.filter(
62264
+ (r6) => r6.success && r6.alreadyPresent === true
62265
+ ).length;
62266
+ const failed = results.filter((r6) => !r6.success).length;
62267
+ if (failed > 0) {
62268
+ process.exitCode = 1;
62269
+ }
62270
+ if (isJsonMode()) {
62271
+ jsonSuccess(command6, {
62272
+ awsAccountId: accountId,
62273
+ reason,
62274
+ dryRun: false,
62275
+ results,
62276
+ added,
62277
+ alreadyPresent,
62278
+ failed
62279
+ });
62280
+ return;
62281
+ }
62282
+ printAddHuman(accountId, reason, results, { added, alreadyPresent, failed });
62283
+ }
61954
62284
 
61955
62285
  // src/commands/email/templates/init.ts
61956
62286
  init_esm_shims();
@@ -61960,7 +62290,7 @@ init_errors();
61960
62290
  init_json_output();
61961
62291
  init_output();
61962
62292
  import { existsSync as existsSync10 } from "fs";
61963
- import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
62293
+ import { mkdir as mkdir3, readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
61964
62294
  import { join as join15 } from "path";
61965
62295
  import * as clack43 from "@clack/prompts";
61966
62296
  import pc46 from "picocolors";
@@ -61968,7 +62298,7 @@ import pc46 from "picocolors";
61968
62298
  // src/utils/shared/scaffold-claude.ts
61969
62299
  init_esm_shims();
61970
62300
  import { existsSync as existsSync9 } from "fs";
61971
- import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
62301
+ import { mkdir as mkdir2, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
61972
62302
  import { join as join14 } from "path";
61973
62303
  var CLAUDE_MD_HEADER = `# Wraps
61974
62304
 
@@ -61997,7 +62327,7 @@ ${wrappedContent}
61997
62327
  );
61998
62328
  return;
61999
62329
  }
62000
- let existing = await readFile6(claudeMdPath, "utf-8");
62330
+ let existing = await readFile7(claudeMdPath, "utf-8");
62001
62331
  const markerRegex = new RegExp(
62002
62332
  `${escapeRegex(startMarker)}[\\s\\S]*?${escapeRegex(endMarker)}`,
62003
62333
  "g"
@@ -62449,7 +62779,7 @@ async function templatesInit(options) {
62449
62779
  try {
62450
62780
  const pkgPath = join15(cwd, "package.json");
62451
62781
  if (existsSync10(pkgPath)) {
62452
- const pkg = JSON.parse(await readFile7(pkgPath, "utf-8"));
62782
+ const pkg = JSON.parse(await readFile8(pkgPath, "utf-8"));
62453
62783
  if (pkg.homepage) {
62454
62784
  try {
62455
62785
  const url = new URL(pkg.homepage);
@@ -62506,7 +62836,7 @@ async function templatesInit(options) {
62506
62836
  await writeFile8(gitignorePath, ".wraps/\n", "utf-8");
62507
62837
  const rootGitignorePath = join15(cwd, ".gitignore");
62508
62838
  if (existsSync10(rootGitignorePath)) {
62509
- const gitignoreContent = await readFile7(rootGitignorePath, "utf-8");
62839
+ const gitignoreContent = await readFile8(rootGitignorePath, "utf-8");
62510
62840
  if (!gitignoreContent.includes("wraps/.wraps")) {
62511
62841
  await writeFile8(
62512
62842
  rootGitignorePath,
@@ -63281,7 +63611,7 @@ init_esm_shims();
63281
63611
  init_events();
63282
63612
  import { createHash as createHash5 } from "crypto";
63283
63613
  import { existsSync as existsSync14 } from "fs";
63284
- import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile11 } from "fs/promises";
63614
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile11 } from "fs/promises";
63285
63615
  import { join as join19 } from "path";
63286
63616
  import * as clack46 from "@clack/prompts";
63287
63617
  import pc49 from "picocolors";
@@ -63382,7 +63712,7 @@ init_json_output();
63382
63712
  // src/utils/shared/lockfile.ts
63383
63713
  init_esm_shims();
63384
63714
  import { existsSync as existsSync13 } from "fs";
63385
- import { mkdir as mkdir5, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
63715
+ import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile10 } from "fs/promises";
63386
63716
  import { join as join18 } from "path";
63387
63717
  function getLockfilePath(wrapsDir) {
63388
63718
  return join18(wrapsDir, ".wraps", "lockfile.json");
@@ -63393,7 +63723,7 @@ async function loadLockfile(wrapsDir) {
63393
63723
  return { version: "1.0.0", lastSync: "", templates: {}, workflows: {} };
63394
63724
  }
63395
63725
  try {
63396
- const content = await readFile8(path3, "utf-8");
63726
+ const content = await readFile9(path3, "utf-8");
63397
63727
  const parsed = JSON.parse(content);
63398
63728
  if (!parsed.templates) {
63399
63729
  parsed.templates = {};
@@ -63543,7 +63873,7 @@ async function templatesPush(options) {
63543
63873
  for (const file of templateFiles) {
63544
63874
  const slug = file.replace(/\.tsx?$/, "");
63545
63875
  const filePath = join19(templatesDir, file);
63546
- const source = await readFile9(filePath, "utf-8");
63876
+ const source = await readFile10(filePath, "utf-8");
63547
63877
  const sourceHash = sha256(source);
63548
63878
  const existsRemotely = remoteTemplateSlugs === null || remoteTemplateSlugs.has(slug);
63549
63879
  if (isTemplateUnchanged({
@@ -64094,10 +64424,12 @@ var CMD3 = {
64094
64424
  get: "email.templates.get",
64095
64425
  pull: "email.templates.pull",
64096
64426
  publish: "email.templates.publish",
64097
- duplicate: "email.templates.duplicate"
64427
+ duplicate: "email.templates.duplicate",
64428
+ delete: "email.templates.delete"
64098
64429
  };
64099
64430
  var TEMPLATE_STATUSES = ["DRAFT", "PUBLISHED", "ARCHIVED"];
64100
64431
  var TEMPLATE_CHANNELS = ["email", "sms"];
64432
+ var DELETE_BULK_MAX = 100;
64101
64433
  var PULL_PAGE_SIZE = 200;
64102
64434
  var PULL_MAX_PAGES = 1e3;
64103
64435
  var SAFE_SLUG = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
@@ -64547,6 +64879,154 @@ async function templatesDuplicate(options) {
64547
64879
  }
64548
64880
  log54.success(`Created draft copy ${copy.id} (${copy.name}).`);
64549
64881
  }
64882
+ async function deleteFailure(resp, singleId) {
64883
+ if (resp.status === 409) {
64884
+ const body = await resp.json().catch(() => null);
64885
+ const blockingIds = Array.isArray(body?.blockingIds) ? body.blockingIds.filter((id) => typeof id === "string") : [];
64886
+ return {
64887
+ code: "CONFLICT",
64888
+ message: `${firstText(body?.error) ?? "A broadcast or workflow still uses one of these templates."} Blocking template ids: ${blockingIds.join(", ") || "none reported"}.`,
64889
+ suggestion: "Finish or cancel each scheduled, queued or processing broadcast that uses a blocking template (`wraps broadcasts cancel <id> --yes`), or disable each enabled workflow that sends it (`wraps workflows disable <workflow-id>`). Find the workflows with `wraps workflows list` and the broadcasts in the dashboard. Then re-run the delete. Nothing was deleted."
64890
+ };
64891
+ }
64892
+ if (resp.status === 404 && singleId !== void 0) {
64893
+ return {
64894
+ code: "NOT_FOUND",
64895
+ message: `Template ${singleId} was not found in this organization. Nothing was deleted.`,
64896
+ suggestion: "Check the id with `wraps email templates list`, then re-run the delete."
64897
+ };
64898
+ }
64899
+ return apiFailure2(resp);
64900
+ }
64901
+ function deleteIds(options) {
64902
+ return [
64903
+ ...new Set(
64904
+ (options.ids ?? []).map((id) => id.trim()).filter((id) => id !== "")
64905
+ )
64906
+ ];
64907
+ }
64908
+ function printDeleteDryRun(command6, ids, route) {
64909
+ if (isJsonMode()) {
64910
+ jsonSuccess(command6, {
64911
+ dryRun: true,
64912
+ route,
64913
+ wouldDelete: ids,
64914
+ deleted: [],
64915
+ notFound: [],
64916
+ count: 0
64917
+ });
64918
+ return;
64919
+ }
64920
+ log54.info(
64921
+ `Dry run: would delete ${ids.length} template(s) via the ${route} route: ${ids.join(", ")}. Nothing was deleted.`
64922
+ );
64923
+ }
64924
+ function sendDelete(command6, target, ids, singleId) {
64925
+ const confirm35 = new URLSearchParams({ confirm: "true" });
64926
+ if (singleId === void 0) {
64927
+ return call(command6, target, withQuery("/v1/templates", confirm35), {
64928
+ method: "DELETE",
64929
+ body: { ids }
64930
+ });
64931
+ }
64932
+ return call(
64933
+ command6,
64934
+ target,
64935
+ withQuery(`/v1/templates/${encodeURIComponent(singleId)}`, confirm35),
64936
+ { method: "DELETE" }
64937
+ );
64938
+ }
64939
+ async function readDeleteResult(command6, resp, ids, singleId) {
64940
+ if (singleId !== void 0) {
64941
+ const body2 = await readBody(resp);
64942
+ if (!body2 || body2.success !== true) {
64943
+ failUnexpected(command6);
64944
+ return null;
64945
+ }
64946
+ return { deleted: [singleId], notFound: [] };
64947
+ }
64948
+ const body = await readBody(resp);
64949
+ if (!(body && Array.isArray(body.notFound))) {
64950
+ failUnexpected(command6);
64951
+ return null;
64952
+ }
64953
+ const notFound = body.notFound.filter(
64954
+ (id) => typeof id === "string"
64955
+ );
64956
+ return {
64957
+ deleted: ids.filter((id) => !notFound.includes(id)),
64958
+ notFound
64959
+ };
64960
+ }
64961
+ function printDeleteResult(route, result) {
64962
+ const { deleted, notFound } = result;
64963
+ log54.success(
64964
+ route === "single" ? `Deleted template ${deleted[0]}.` : `Deleted ${deleted.length} template(s).`
64965
+ );
64966
+ if (notFound.length > 0) {
64967
+ log54.warn(
64968
+ `Not found in this organization, so skipped: ${notFound.join(", ")}.`
64969
+ );
64970
+ }
64971
+ }
64972
+ async function templatesDelete(options) {
64973
+ const command6 = CMD3.delete;
64974
+ const ids = deleteIds(options);
64975
+ if (ids.length === 0) {
64976
+ failMissingId(command6, "wraps email templates delete <id...> --yes");
64977
+ return;
64978
+ }
64979
+ if (ids.length > DELETE_BULK_MAX) {
64980
+ failValidation(
64981
+ command6,
64982
+ `${ids.length} template ids were given, but one delete accepts at most ${DELETE_BULK_MAX}. Nothing was deleted.`,
64983
+ `Split the ids into batches of ${DELETE_BULK_MAX} or fewer, and run one delete per batch with --yes.`
64984
+ );
64985
+ return;
64986
+ }
64987
+ const [first] = ids;
64988
+ const singleId = ids.length === 1 ? first : void 0;
64989
+ const route = singleId === void 0 ? "bulk" : "single";
64990
+ if (options.dryRun) {
64991
+ printDeleteDryRun(command6, ids, route);
64992
+ return;
64993
+ }
64994
+ if (!options.yes) {
64995
+ fail9(command6, {
64996
+ code: "CONFIRMATION_REQUIRED",
64997
+ 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 scheduled, queued or processing broadcast, or an enabled workflow, the whole delete is refused. Nothing was deleted.`,
64998
+ suggestion: `Preview with \`wraps email templates delete ${ids.join(" ")} --dry-run\`, then re-run with --yes.`
64999
+ });
65000
+ return;
65001
+ }
65002
+ const target = await connect6(command6, options.token);
65003
+ if (!target) {
65004
+ return;
65005
+ }
65006
+ const resp = await sendDelete(command6, target, ids, singleId);
65007
+ if (!resp) {
65008
+ return;
65009
+ }
65010
+ if (!resp.ok) {
65011
+ fail9(command6, await deleteFailure(resp, singleId));
65012
+ return;
65013
+ }
65014
+ const result = await readDeleteResult(command6, resp, ids, singleId);
65015
+ if (!result) {
65016
+ return;
65017
+ }
65018
+ if (isJsonMode()) {
65019
+ jsonSuccess(command6, {
65020
+ dryRun: false,
65021
+ route,
65022
+ deleted: result.deleted,
65023
+ notFound: result.notFound,
65024
+ count: result.deleted.length
65025
+ });
65026
+ return;
65027
+ }
65028
+ printDeleteResult(route, result);
65029
+ }
64550
65030
  async function fetchAllPullRows(command6, target) {
64551
65031
  const rows = [];
64552
65032
  const seenCursors = /* @__PURE__ */ new Set();
@@ -67110,7 +67590,7 @@ init_events();
67110
67590
  init_json_output();
67111
67591
  init_output();
67112
67592
  import { existsSync as existsSync16 } from "fs";
67113
- import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile13 } from "fs/promises";
67593
+ import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile13 } from "fs/promises";
67114
67594
  import { join as join21 } from "path";
67115
67595
  import * as clack50 from "@clack/prompts";
67116
67596
  import pc53 from "picocolors";
@@ -67523,7 +68003,7 @@ async function workflowsInit(options) {
67523
68003
  await mkdir8(workflowsDir, { recursive: true });
67524
68004
  const configPath = join21(cwd, "wraps", "wraps.config.ts");
67525
68005
  if (existsSync16(configPath)) {
67526
- const configContent = await readFile10(configPath, "utf-8");
68006
+ const configContent = await readFile11(configPath, "utf-8");
67527
68007
  if (!configContent.includes("workflowsDir")) {
67528
68008
  const updated = configContent.replace(
67529
68009
  /}\);(\s*)$/,
@@ -67843,7 +68323,7 @@ function assignPositions(steps, transitions) {
67843
68323
  init_esm_shims();
67844
68324
  import { createHash as createHash6 } from "crypto";
67845
68325
  import { existsSync as existsSync17 } from "fs";
67846
- import { mkdir as mkdir9, readdir as readdir4, readFile as readFile11, writeFile as writeFile14 } from "fs/promises";
68326
+ import { mkdir as mkdir9, readdir as readdir4, readFile as readFile12, writeFile as writeFile14 } from "fs/promises";
67847
68327
  import { basename as basename2, join as join23 } from "path";
67848
68328
  async function discoverWorkflows(dir, filter) {
67849
68329
  if (!existsSync17(dir)) {
@@ -67866,7 +68346,7 @@ async function discoverWorkflows(dir, filter) {
67866
68346
  }
67867
68347
  async function parseWorkflowTs(filePath, wrapsDir) {
67868
68348
  const { build: build2 } = await import("esbuild");
67869
- const source = await readFile11(filePath, "utf-8");
68349
+ const source = await readFile12(filePath, "utf-8");
67870
68350
  const sourceHash = createHash6("sha256").update(source).digest("hex");
67871
68351
  const slug = basename2(filePath, ".ts");
67872
68352
  const shimDir = join23(wrapsDir, ".wraps", "_shims");
@@ -82708,6 +83188,9 @@ function showHelp() {
82708
83188
  console.log(
82709
83189
  ` ${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
83190
  );
83191
+ console.log(
83192
+ ` ${pc87.cyan("email suppressions add <email...>")} Suppress addresses (needs --account, --reason bounce|complaint and --yes; --file for a list; --dry-run previews)`
83193
+ );
82711
83194
  console.log(
82712
83195
  ` ${pc87.cyan("email suppressions remove")} Remove one address (needs --account and --yes; --dry-run previews; --acknowledge-complaint for complaint suppressions)
82713
83196
  `
@@ -82757,6 +83240,9 @@ function showHelp() {
82757
83240
  console.log(
82758
83241
  ` ${pc87.cyan("email templates duplicate")} <id> Copy a template as a new draft`
82759
83242
  );
83243
+ console.log(
83244
+ ` ${pc87.cyan("email templates delete")} <id...> Delete templates (--yes; --dry-run previews; refuses in-use templates)`
83245
+ );
82760
83246
  console.log(
82761
83247
  ` ${pc87.cyan("email templates init")} Initialize templates-as-code`
82762
83248
  );
@@ -83133,6 +83619,10 @@ var parsedCli = parseCliArgs(process.argv);
83133
83619
  var flags = parsedCli.flags;
83134
83620
  var { sub, extraPositionals } = parsedCli;
83135
83621
  var [primaryCommand, subCommand] = sub;
83622
+ var VARIADIC_COMMANDS = /* @__PURE__ */ new Set([
83623
+ "email templates delete",
83624
+ "email suppressions add"
83625
+ ]);
83136
83626
  setJsonMode(flags.json === true);
83137
83627
  if (!primaryCommand) {
83138
83628
  async function interactiveMenu() {
@@ -83315,7 +83805,7 @@ async function run() {
83315
83805
  telemetry.markNotificationShown();
83316
83806
  }
83317
83807
  try {
83318
- if (extraPositionals.length > 0) {
83808
+ if (extraPositionals.length > 0 && !VARIADIC_COMMANDS.has(sub.slice(0, 3).join(" "))) {
83319
83809
  throw errors.invalidFlagUsage(
83320
83810
  `Unrecognized argument${extraPositionals.length > 1 ? "s" : ""}: ${extraPositionals.join(" ")}`,
83321
83811
  "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 +84327,16 @@ async function run() {
83837
84327
  region: flags.region
83838
84328
  });
83839
84329
  break;
84330
+ case "delete":
84331
+ await templatesDelete({
84332
+ ids: sub.slice(3),
84333
+ yes: flags.yes,
84334
+ dryRun: flags.dryRun,
84335
+ json: flags.json,
84336
+ token: flags.token,
84337
+ region: flags.region
84338
+ });
84339
+ break;
83840
84340
  case "preview":
83841
84341
  await templatesPreview({
83842
84342
  port: flags.port,
@@ -83848,7 +84348,7 @@ async function run() {
83848
84348
  throw errors.unknownCommand(
83849
84349
  "templates command",
83850
84350
  templatesSubCommand,
83851
- "Available commands: init, push, preview, list, get, pull, publish, duplicate"
84351
+ "Available commands: init, push, preview, list, get, pull, publish, duplicate, delete"
83852
84352
  );
83853
84353
  }
83854
84354
  break;
@@ -83935,6 +84435,19 @@ async function run() {
83935
84435
  region: flags.region
83936
84436
  });
83937
84437
  break;
84438
+ case "add":
84439
+ await suppressionsAdd({
84440
+ emails: sub.slice(3),
84441
+ file: flags.file,
84442
+ account: flags.account,
84443
+ reason: flags.reason,
84444
+ yes: flags.yes,
84445
+ dryRun: flags.dryRun,
84446
+ json: flags.json,
84447
+ token: flags.token,
84448
+ region: flags.region
84449
+ });
84450
+ break;
83938
84451
  case "remove":
83939
84452
  await suppressionsRemove({
83940
84453
  email: sub[3],
@@ -83951,7 +84464,7 @@ async function run() {
83951
84464
  throw errors.unknownCommand(
83952
84465
  "suppressions command",
83953
84466
  suppressionsSubCommand,
83954
- "Available commands: list, remove <email>"
84467
+ "Available commands: list, add <email...>, remove <email>"
83955
84468
  );
83956
84469
  }
83957
84470
  break;