@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
|
-
|
|
61647
|
-
|
|
61648
|
-
|
|
61649
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|