@wraps.dev/cli 1.5.0 → 1.5.2

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
@@ -146,7 +146,7 @@ var require_package = __commonJS({
146
146
  "package.json"(exports, module) {
147
147
  module.exports = {
148
148
  name: "@wraps.dev/cli",
149
- version: "1.5.0",
149
+ version: "1.5.2",
150
150
  description: "CLI for deploying Wraps email infrastructure to your AWS account",
151
151
  type: "module",
152
152
  main: "./dist/cli.js",
@@ -448,6 +448,21 @@ function trackCommand(command, metadata) {
448
448
  sanitized.email = void 0;
449
449
  client.track(`command:${command}`, sanitized);
450
450
  }
451
+ function trackServiceInit(service, success, metadata) {
452
+ const client = getTelemetryClient();
453
+ client.track("service:init", {
454
+ service,
455
+ success,
456
+ ...metadata
457
+ });
458
+ }
459
+ function trackServiceDeployed(service, metadata) {
460
+ const client = getTelemetryClient();
461
+ client.track("service:deployed", {
462
+ service,
463
+ ...metadata
464
+ });
465
+ }
451
466
  function trackError(errorCode, command, metadata) {
452
467
  const client = getTelemetryClient();
453
468
  client.track("error:occurred", {
@@ -456,6 +471,24 @@ function trackError(errorCode, command, metadata) {
456
471
  ...metadata
457
472
  });
458
473
  }
474
+ function trackFeature(feature, metadata) {
475
+ const client = getTelemetryClient();
476
+ client.track(`feature:${feature}`, metadata || {});
477
+ }
478
+ function trackServiceUpgrade(service, metadata) {
479
+ const client = getTelemetryClient();
480
+ client.track("service:upgraded", {
481
+ service,
482
+ ...metadata
483
+ });
484
+ }
485
+ function trackServiceRemoved(service, metadata) {
486
+ const client = getTelemetryClient();
487
+ client.track("service:removed", {
488
+ service,
489
+ ...metadata
490
+ });
491
+ }
459
492
  var init_events = __esm({
460
493
  "src/telemetry/events.ts"() {
461
494
  "use strict";
@@ -519,7 +552,7 @@ var init_errors = __esm({
519
552
  stackExists: (stackName) => new WrapsError(
520
553
  `Stack "${stackName}" already exists`,
521
554
  "STACK_EXISTS",
522
- `To update: wraps upgrade
555
+ `To update: wraps email upgrade
523
556
  To remove: wraps destroy --stack ${stackName}`,
524
557
  "https://wraps.dev/docs/cli/upgrade"
525
558
  ),
@@ -1559,7 +1592,7 @@ function getPresetInfo(preset) {
1559
1592
  volume: "10k-500k emails/month",
1560
1593
  features: [
1561
1594
  "Everything in Starter",
1562
- "Reputation metrics dashboard",
1595
+ "Reputation tracking",
1563
1596
  "Real-time event tracking (EventBridge)",
1564
1597
  "90-day email history storage",
1565
1598
  "Optional: Email archiving with rendered viewer",
@@ -1768,16 +1801,16 @@ async function promptProvider() {
1768
1801
  const provider = await clack4.select({
1769
1802
  message: "Where is your app hosted?",
1770
1803
  options: [
1771
- {
1772
- value: "vercel",
1773
- label: "Vercel",
1774
- hint: "Uses OIDC (no AWS credentials needed)"
1775
- },
1776
1804
  {
1777
1805
  value: "aws",
1778
1806
  label: "AWS (Lambda/ECS/EC2)",
1779
1807
  hint: "Uses IAM roles automatically"
1780
1808
  },
1809
+ {
1810
+ value: "vercel",
1811
+ label: "Vercel",
1812
+ hint: "Uses OIDC (no AWS credentials needed)"
1813
+ },
1781
1814
  {
1782
1815
  value: "railway",
1783
1816
  label: "Railway",
@@ -2171,6 +2204,14 @@ async function promptEmailArchiving() {
2171
2204
  async function promptCustomConfig(existingConfig) {
2172
2205
  clack4.log.info("Custom configuration builder");
2173
2206
  clack4.log.info("Configure each feature individually");
2207
+ const reputationMetrics = await clack4.confirm({
2208
+ message: "Enable reputation tracking (recommended)?",
2209
+ initialValue: existingConfig?.reputationMetrics ?? true
2210
+ });
2211
+ if (clack4.isCancel(reputationMetrics)) {
2212
+ clack4.cancel("Operation cancelled.");
2213
+ process.exit(0);
2214
+ }
2174
2215
  const trackingEnabled = await clack4.confirm({
2175
2216
  message: "Enable open & click tracking?",
2176
2217
  initialValue: existingConfig?.tracking?.enabled ?? true
@@ -2180,49 +2221,38 @@ async function promptCustomConfig(existingConfig) {
2180
2221
  process.exit(0);
2181
2222
  }
2182
2223
  const eventTrackingEnabled = await clack4.confirm({
2183
- message: "Enable real-time event tracking (EventBridge)?",
2224
+ message: "Store email events in DynamoDB?",
2184
2225
  initialValue: existingConfig?.eventTracking?.enabled ?? true
2185
2226
  });
2186
2227
  if (clack4.isCancel(eventTrackingEnabled)) {
2187
2228
  clack4.cancel("Operation cancelled.");
2188
2229
  process.exit(0);
2189
2230
  }
2190
- let dynamoDBHistory = false;
2191
2231
  let archiveRetention = "90days";
2192
2232
  if (eventTrackingEnabled) {
2193
- dynamoDBHistory = await clack4.confirm({
2194
- message: "Store email history in DynamoDB?",
2195
- initialValue: existingConfig?.eventTracking?.dynamoDBHistory ?? true
2233
+ archiveRetention = await clack4.select({
2234
+ message: "Event history retention period:",
2235
+ options: [
2236
+ { value: "7days", label: "7 days", hint: "Minimal storage cost" },
2237
+ { value: "30days", label: "30 days", hint: "Development/testing" },
2238
+ {
2239
+ value: "90days",
2240
+ label: "90 days (recommended)",
2241
+ hint: "Standard retention"
2242
+ },
2243
+ { value: "1year", label: "1 year", hint: "Compliance requirements" },
2244
+ {
2245
+ value: "indefinite",
2246
+ label: "Indefinite",
2247
+ hint: "Higher storage cost"
2248
+ }
2249
+ ],
2250
+ initialValue: existingConfig?.eventTracking?.archiveRetention || "90days"
2196
2251
  });
2197
- if (clack4.isCancel(dynamoDBHistory)) {
2252
+ if (clack4.isCancel(archiveRetention)) {
2198
2253
  clack4.cancel("Operation cancelled.");
2199
2254
  process.exit(0);
2200
2255
  }
2201
- if (dynamoDBHistory) {
2202
- archiveRetention = await clack4.select({
2203
- message: "Email history retention period:",
2204
- options: [
2205
- { value: "7days", label: "7 days", hint: "Minimal storage cost" },
2206
- { value: "30days", label: "30 days", hint: "Development/testing" },
2207
- {
2208
- value: "90days",
2209
- label: "90 days (recommended)",
2210
- hint: "Standard retention"
2211
- },
2212
- { value: "1year", label: "1 year", hint: "Compliance requirements" },
2213
- {
2214
- value: "indefinite",
2215
- label: "Indefinite",
2216
- hint: "Higher storage cost"
2217
- }
2218
- ],
2219
- initialValue: existingConfig?.eventTracking?.archiveRetention || "90days"
2220
- });
2221
- if (clack4.isCancel(archiveRetention)) {
2222
- clack4.cancel("Operation cancelled.");
2223
- process.exit(0);
2224
- }
2225
- }
2226
2256
  }
2227
2257
  const tlsRequired = await clack4.confirm({
2228
2258
  message: "Require TLS encryption for all emails?",
@@ -2232,14 +2262,40 @@ async function promptCustomConfig(existingConfig) {
2232
2262
  clack4.cancel("Operation cancelled.");
2233
2263
  process.exit(0);
2234
2264
  }
2235
- const reputationMetrics = await clack4.confirm({
2236
- message: "Enable reputation metrics dashboard?",
2237
- initialValue: existingConfig?.reputationMetrics ?? true
2265
+ const customMailFrom = await clack4.confirm({
2266
+ message: "Configure custom MAIL FROM domain? (improves DMARC alignment)",
2267
+ initialValue: existingConfig?.mailFromDomain !== void 0
2238
2268
  });
2239
- if (clack4.isCancel(reputationMetrics)) {
2269
+ if (clack4.isCancel(customMailFrom)) {
2240
2270
  clack4.cancel("Operation cancelled.");
2241
2271
  process.exit(0);
2242
2272
  }
2273
+ let mailFromSubdomain = "mail";
2274
+ if (customMailFrom) {
2275
+ mailFromSubdomain = await clack4.text({
2276
+ message: "MAIL FROM subdomain:",
2277
+ placeholder: "mail",
2278
+ initialValue: existingConfig?.mailFromDomain?.split(".")[0] || "mail",
2279
+ validate: (value) => {
2280
+ if (!value || value.trim() === "") {
2281
+ return "Subdomain is required";
2282
+ }
2283
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(value)) {
2284
+ return "Invalid subdomain format";
2285
+ }
2286
+ return void 0;
2287
+ }
2288
+ });
2289
+ if (clack4.isCancel(mailFromSubdomain)) {
2290
+ clack4.cancel("Operation cancelled.");
2291
+ process.exit(0);
2292
+ }
2293
+ clack4.log.info(
2294
+ pc5.dim(
2295
+ `MAIL FROM will be set to ${mailFromSubdomain}.yourdomain.com`
2296
+ )
2297
+ );
2298
+ }
2243
2299
  const dedicatedIp = await clack4.confirm({
2244
2300
  message: "Request dedicated IP address? (requires 100k+ emails/day)",
2245
2301
  initialValue: existingConfig?.dedicatedIp ?? false
@@ -2303,6 +2359,7 @@ async function promptCustomConfig(existingConfig) {
2303
2359
  } : { enabled: false },
2304
2360
  tlsRequired,
2305
2361
  reputationMetrics,
2362
+ mailFromSubdomain: customMailFrom ? typeof mailFromSubdomain === "string" ? mailFromSubdomain : "mail" : void 0,
2306
2363
  suppressionList: {
2307
2364
  enabled: true,
2308
2365
  reasons: ["BOUNCE", "COMPLAINT"]
@@ -2320,7 +2377,7 @@ async function promptCustomConfig(existingConfig) {
2320
2377
  "REJECT",
2321
2378
  "RENDERING_FAILURE"
2322
2379
  ],
2323
- dynamoDBHistory: Boolean(dynamoDBHistory),
2380
+ dynamoDBHistory: true,
2324
2381
  archiveRetention: typeof archiveRetention === "string" ? archiveRetention : "90days"
2325
2382
  } : { enabled: false },
2326
2383
  emailArchiving: emailArchivingEnabled ? {
@@ -2661,9 +2718,9 @@ init_esm_shims();
2661
2718
  import { readFileSync } from "fs";
2662
2719
  import { dirname as dirname2, join as join4 } from "path";
2663
2720
  import { fileURLToPath as fileURLToPath4 } from "url";
2664
- import * as clack14 from "@clack/prompts";
2721
+ import * as clack16 from "@clack/prompts";
2665
2722
  import args from "args";
2666
- import pc15 from "picocolors";
2723
+ import pc17 from "picocolors";
2667
2724
 
2668
2725
  // src/commands/dashboard/update-role.ts
2669
2726
  init_esm_shims();
@@ -2954,13 +3011,15 @@ Verification should complete within a few minutes.`,
2954
3011
  )
2955
3012
  ];
2956
3013
  if (domain) {
3014
+ const dmarcRuaDomain = outputs.mailFromDomain || domain;
2957
3015
  dnsLines.push(
2958
3016
  "",
2959
3017
  pc2.bold("SPF Record (TXT):"),
2960
3018
  ` ${pc2.cyan(domain)} ${pc2.dim("TXT")} "v=spf1 include:amazonses.com ~all"`,
3019
+ pc2.dim(" Note: If you have an existing SPF record, add 'include:amazonses.com' to it"),
2961
3020
  "",
2962
3021
  pc2.bold("DMARC Record (TXT):"),
2963
- ` ${pc2.cyan(`_dmarc.${domain}`)} ${pc2.dim("TXT")} "v=DMARC1; p=quarantine; rua=mailto:postmaster@${domain}"`
3022
+ ` ${pc2.cyan(`_dmarc.${domain}`)} ${pc2.dim("TXT")} "v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}"`
2964
3023
  );
2965
3024
  if (outputs.mailFromDomain) {
2966
3025
  dnsLines.push(
@@ -3018,7 +3077,7 @@ Verification should complete within a few minutes.`,
3018
3077
  if (outputs.customTrackingDomain) {
3019
3078
  console.log(
3020
3079
  `
3021
- ${pc2.dim("Run:")} ${pc2.yellow(`wraps verify --domain ${outputs.customTrackingDomain}`)} ${pc2.dim(
3080
+ ${pc2.dim("Run:")} ${pc2.yellow(`wraps email verify --domain ${outputs.customTrackingDomain}`)} ${pc2.dim(
3022
3081
  "(after DNS propagates)"
3023
3082
  )}
3024
3083
  `
@@ -3070,7 +3129,7 @@ ${domainStrings.join("\n")}`);
3070
3129
  );
3071
3130
  } else {
3072
3131
  featureLines.push(
3073
- ` ${pc2.dim("\u25CB")} Email Tracking ${pc2.dim("(run 'wraps upgrade' to enable)")}`
3132
+ ` ${pc2.dim("\u25CB")} Email Tracking ${pc2.dim("(run 'wraps email upgrade' to enable)")}`
3074
3133
  );
3075
3134
  }
3076
3135
  if (status2.resources.lambdaFunctions && status2.resources.lambdaFunctions > 0) {
@@ -3079,7 +3138,7 @@ ${domainStrings.join("\n")}`);
3079
3138
  );
3080
3139
  } else {
3081
3140
  featureLines.push(
3082
- ` ${pc2.dim("\u25CB")} Bounce/Complaint Handling ${pc2.dim("(run 'wraps upgrade' to enable)")}`
3141
+ ` ${pc2.dim("\u25CB")} Bounce/Complaint Handling ${pc2.dim("(run 'wraps email upgrade' to enable)")}`
3083
3142
  );
3084
3143
  }
3085
3144
  if (status2.resources.archivingEnabled) {
@@ -3096,7 +3155,7 @@ ${domainStrings.join("\n")}`);
3096
3155
  );
3097
3156
  } else {
3098
3157
  featureLines.push(
3099
- ` ${pc2.dim("\u25CB")} Email Archiving ${pc2.dim("(run 'wraps upgrade' to enable)")}`
3158
+ ` ${pc2.dim("\u25CB")} Email Archiving ${pc2.dim("(run 'wraps email upgrade' to enable)")}`
3100
3159
  );
3101
3160
  }
3102
3161
  if (status2.tracking?.customTrackingDomain) {
@@ -3109,7 +3168,7 @@ ${domainStrings.join("\n")}`);
3109
3168
  featureLines.push(` ${pc2.cyan(status2.tracking.customTrackingDomain)}`);
3110
3169
  } else {
3111
3170
  featureLines.push(
3112
- ` ${pc2.dim("\u25CB")} Custom Tracking Domain ${pc2.dim("(run 'wraps upgrade' to enable)")}`
3171
+ ` ${pc2.dim("\u25CB")} Custom Tracking Domain ${pc2.dim("(run 'wraps email upgrade' to enable)")}`
3113
3172
  );
3114
3173
  }
3115
3174
  featureLines.push(
@@ -3157,6 +3216,7 @@ ${domainStrings.join("\n")}`);
3157
3216
  for (const domain of domainsNeedingDNS) {
3158
3217
  const dnsLines = [];
3159
3218
  if (domain.status === "pending" && domain.dkimTokens && domain.dkimTokens.length > 0) {
3219
+ const dmarcRuaDomain = domain.mailFromDomain || domain.domain;
3160
3220
  dnsLines.push(
3161
3221
  pc2.bold("DKIM Records (CNAME):"),
3162
3222
  ...domain.dkimTokens.map(
@@ -3165,9 +3225,10 @@ ${domainStrings.join("\n")}`);
3165
3225
  "",
3166
3226
  pc2.bold("SPF Record (TXT):"),
3167
3227
  ` ${pc2.cyan(domain.domain)} ${pc2.dim("TXT")} "v=spf1 include:amazonses.com ~all"`,
3228
+ pc2.dim(" Note: If you have an existing SPF record, add 'include:amazonses.com' to it"),
3168
3229
  "",
3169
3230
  pc2.bold("DMARC Record (TXT):"),
3170
- ` ${pc2.cyan(`_dmarc.${domain.domain}`)} ${pc2.dim("TXT")} "v=DMARC1; p=quarantine; rua=mailto:postmaster@${domain.domain}"`
3231
+ ` ${pc2.cyan(`_dmarc.${domain.domain}`)} ${pc2.dim("TXT")} "v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}"`
3171
3232
  );
3172
3233
  }
3173
3234
  if (domain.mailFromDomain && domain.mailFromStatus !== "SUCCESS") {
@@ -3187,7 +3248,7 @@ ${domainStrings.join("\n")}`);
3187
3248
  const exampleDomain = domainsNeedingDNS[0].domain;
3188
3249
  console.log(
3189
3250
  `
3190
- ${pc2.dim("Run:")} ${pc2.yellow(`wraps verify --domain ${exampleDomain}`)} ${pc2.dim(
3251
+ ${pc2.dim("Run:")} ${pc2.yellow(`wraps email verify --domain ${exampleDomain}`)} ${pc2.dim(
3191
3252
  "(after DNS propagates)"
3192
3253
  )}
3193
3254
  `
@@ -4133,20 +4194,22 @@ async function createSESResources(config2) {
4133
4194
  dkimTokens = domainIdentity.dkimSigningAttributes.apply(
4134
4195
  (attrs) => attrs?.tokens || []
4135
4196
  );
4136
- mailFromDomain = config2.mailFromDomain || `mail.${config2.domain}`;
4137
- new aws5.sesv2.EmailIdentityMailFromAttributes(
4138
- "wraps-email-mail-from",
4139
- {
4140
- emailIdentity: config2.domain,
4141
- mailFromDomain,
4142
- behaviorOnMxFailure: "USE_DEFAULT_VALUE"
4143
- // Fallback to amazonses.com if MX record fails
4144
- },
4145
- {
4146
- dependsOn: [domainIdentity]
4147
- // Ensure domain identity exists first
4148
- }
4149
- );
4197
+ if (config2.mailFromDomain) {
4198
+ mailFromDomain = config2.mailFromDomain;
4199
+ new aws5.sesv2.EmailIdentityMailFromAttributes(
4200
+ "wraps-email-mail-from",
4201
+ {
4202
+ emailIdentity: config2.domain,
4203
+ mailFromDomain,
4204
+ behaviorOnMxFailure: "USE_DEFAULT_VALUE"
4205
+ // Fallback to amazonses.com if MX record fails
4206
+ },
4207
+ {
4208
+ dependsOn: [domainIdentity]
4209
+ // Ensure domain identity exists first
4210
+ }
4211
+ );
4212
+ }
4150
4213
  }
4151
4214
  return {
4152
4215
  configSet,
@@ -4282,8 +4345,8 @@ async function deployEmailStack(config2) {
4282
4345
  let cloudFrontResources;
4283
4346
  let acmResources;
4284
4347
  if (emailConfig.tracking?.enabled && emailConfig.tracking.customRedirectDomain && emailConfig.tracking.httpsEnabled) {
4285
- const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
4286
- const hostedZone = await findHostedZone2(
4348
+ const { findHostedZone: findHostedZone3 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
4349
+ const hostedZone = await findHostedZone3(
4287
4350
  emailConfig.tracking.customRedirectDomain,
4288
4351
  config2.region
4289
4352
  );
@@ -4309,9 +4372,13 @@ async function deployEmailStack(config2) {
4309
4372
  "wraps-email-eventbridge",
4310
4373
  config2.region
4311
4374
  );
4375
+ let mailFromDomain = emailConfig.mailFromDomain;
4376
+ if (!mailFromDomain && emailConfig.mailFromSubdomain && emailConfig.domain) {
4377
+ mailFromDomain = `${emailConfig.mailFromSubdomain}.${emailConfig.domain}`;
4378
+ }
4312
4379
  sesResources = await createSESResources({
4313
4380
  domain: emailConfig.domain,
4314
- mailFromDomain: emailConfig.mailFromDomain,
4381
+ mailFromDomain,
4315
4382
  region: config2.region,
4316
4383
  trackingConfig: emailConfig.tracking,
4317
4384
  eventTypes: emailConfig.eventTracking?.events,
@@ -4384,6 +4451,7 @@ async function deployEmailStack(config2) {
4384
4451
  }
4385
4452
 
4386
4453
  // src/commands/email/config.ts
4454
+ init_events();
4387
4455
  init_aws();
4388
4456
  init_errors();
4389
4457
 
@@ -4418,6 +4486,7 @@ async function ensurePulumiInstalled() {
4418
4486
 
4419
4487
  // src/commands/email/config.ts
4420
4488
  async function config(options) {
4489
+ const startTime = Date.now();
4421
4490
  clack3.intro(
4422
4491
  pc4.bold(
4423
4492
  options.preview ? "Wraps Config Preview" : "Wraps Config - Apply CLI Updates to Infrastructure"
@@ -4566,8 +4635,14 @@ ${pc4.bold("Current Configuration:")}
4566
4635
  clack3.outro(
4567
4636
  pc4.green("Preview complete. Run without --preview to update.")
4568
4637
  );
4638
+ trackCommand("email:config", {
4639
+ success: true,
4640
+ preview: true,
4641
+ duration_ms: Date.now() - startTime
4642
+ });
4569
4643
  return;
4570
4644
  } catch (error) {
4645
+ trackError("PREVIEW_FAILED", "email:config", { step: "preview" });
4571
4646
  if (error.message?.includes("stack is currently locked")) {
4572
4647
  throw errors.stackLocked();
4573
4648
  }
@@ -4629,9 +4704,15 @@ ${pc4.bold("Current Configuration:")}
4629
4704
  }
4630
4705
  );
4631
4706
  } catch (error) {
4707
+ trackCommand("email:config", {
4708
+ success: false,
4709
+ duration_ms: Date.now() - startTime
4710
+ });
4632
4711
  if (error.message?.includes("stack is currently locked")) {
4712
+ trackError("STACK_LOCKED", "email:config", { step: "update" });
4633
4713
  throw errors.stackLocked();
4634
4714
  }
4715
+ trackError("UPDATE_FAILED", "email:config", { step: "update" });
4635
4716
  throw new Error(`Pulumi update failed: ${error.message}`);
4636
4717
  }
4637
4718
  metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -4662,6 +4743,10 @@ ${pc4.green("\u2713")} ${pc4.bold("Update complete!")}
4662
4743
  ` ${pc4.cyan("3.")} View analytics at ${pc4.cyan("wraps console")}
4663
4744
  `
4664
4745
  );
4746
+ trackCommand("email:config", {
4747
+ success: true,
4748
+ duration_ms: Date.now() - startTime
4749
+ });
4665
4750
  }
4666
4751
 
4667
4752
  // src/commands/email/connect.ts
@@ -4669,6 +4754,7 @@ init_esm_shims();
4669
4754
  import * as clack5 from "@clack/prompts";
4670
4755
  import * as pulumi6 from "@pulumi/pulumi";
4671
4756
  import pc6 from "picocolors";
4757
+ init_events();
4672
4758
  init_presets();
4673
4759
  init_aws();
4674
4760
  init_errors();
@@ -4902,6 +4988,7 @@ async function scanAWSResources(region) {
4902
4988
 
4903
4989
  // src/commands/email/connect.ts
4904
4990
  async function connect(options) {
4991
+ const startTime = Date.now();
4905
4992
  clack5.intro(
4906
4993
  pc6.bold(
4907
4994
  options.preview ? "Wraps Connect Preview" : "Wraps Connect - Link Existing Infrastructure"
@@ -5042,8 +5129,16 @@ async function connect(options) {
5042
5129
  clack5.outro(
5043
5130
  pc6.green("Preview complete. Run without --preview to connect.")
5044
5131
  );
5132
+ trackServiceInit("email", true, {
5133
+ preset,
5134
+ provider,
5135
+ preview: true,
5136
+ duration_ms: Date.now() - startTime,
5137
+ existing_identities: selectedIdentities.length
5138
+ });
5045
5139
  return;
5046
5140
  } catch (error) {
5141
+ trackError("PREVIEW_FAILED", "email:connect", { step: "preview" });
5047
5142
  if (error.message?.includes("stack is currently locked")) {
5048
5143
  throw errors.stackLocked();
5049
5144
  }
@@ -5103,14 +5198,21 @@ async function connect(options) {
5103
5198
  }
5104
5199
  );
5105
5200
  } catch (error) {
5201
+ trackServiceInit("email", false, {
5202
+ preset,
5203
+ provider,
5204
+ duration_ms: Date.now() - startTime
5205
+ });
5106
5206
  if (error.message?.includes("stack is currently locked")) {
5207
+ trackError("STACK_LOCKED", "email:connect", { step: "deploy" });
5107
5208
  throw errors.stackLocked();
5108
5209
  }
5210
+ trackError("DEPLOYMENT_FAILED", "email:connect", { step: "deploy" });
5109
5211
  throw new Error(`Pulumi deployment failed: ${error.message}`);
5110
5212
  }
5111
5213
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
5112
- const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
5113
- const hostedZone = await findHostedZone2(outputs.domain, region);
5214
+ const { findHostedZone: findHostedZone3, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
5215
+ const hostedZone = await findHostedZone3(outputs.domain, region);
5114
5216
  if (hostedZone) {
5115
5217
  try {
5116
5218
  progress.start("Creating DNS records in Route53");
@@ -5172,105 +5274,407 @@ ${pc6.dim("Example:")}`);
5172
5274
  );
5173
5275
  console.log("");
5174
5276
  }
5277
+ const duration = Date.now() - startTime;
5278
+ const enabledFeatures = [];
5279
+ if (emailConfig.tracking?.enabled) enabledFeatures.push("tracking");
5280
+ if (emailConfig.suppressionList?.enabled)
5281
+ enabledFeatures.push("suppression_list");
5282
+ if (emailConfig.eventTracking?.enabled)
5283
+ enabledFeatures.push("event_tracking");
5284
+ if (emailConfig.eventTracking?.dynamoDBHistory)
5285
+ enabledFeatures.push("dynamodb_history");
5286
+ trackServiceInit("email", true, {
5287
+ preset,
5288
+ provider,
5289
+ features: enabledFeatures,
5290
+ duration_ms: duration,
5291
+ existing_identities: selectedIdentities.length
5292
+ });
5293
+ trackServiceDeployed("email", {
5294
+ duration_ms: duration,
5295
+ features: enabledFeatures,
5296
+ preset
5297
+ });
5175
5298
  }
5176
5299
 
5177
- // src/commands/email/domains.ts
5300
+ // src/commands/email/destroy.ts
5178
5301
  init_esm_shims();
5302
+ init_events();
5179
5303
  init_aws();
5180
- import { Resolver } from "dns/promises";
5181
- import { GetEmailIdentityCommand, SESv2Client as SESv2Client2 } from "@aws-sdk/client-sesv2";
5182
5304
  import * as clack6 from "@clack/prompts";
5305
+ import * as pulumi7 from "@pulumi/pulumi";
5183
5306
  import pc7 from "picocolors";
5184
- async function verifyDomain(options) {
5185
- clack6.intro(pc7.bold(`Verifying ${options.domain}`));
5186
- const progress = new DeploymentProgress();
5187
- const region = await getAWSRegion();
5188
- const sesClient = new SESv2Client2({ region });
5189
- let identity;
5190
- let dkimTokens = [];
5191
- let mailFromDomain;
5307
+
5308
+ // src/utils/route53.ts
5309
+ init_esm_shims();
5310
+ import {
5311
+ ChangeResourceRecordSetsCommand as ChangeResourceRecordSetsCommand2,
5312
+ ListHostedZonesByNameCommand as ListHostedZonesByNameCommand2,
5313
+ ListResourceRecordSetsCommand,
5314
+ Route53Client as Route53Client2
5315
+ } from "@aws-sdk/client-route-53";
5316
+ async function findHostedZone2(domain, region) {
5317
+ const client = new Route53Client2({ region });
5192
5318
  try {
5193
- identity = await progress.execute(
5194
- "Checking SES verification status",
5195
- async () => {
5196
- const response = await sesClient.send(
5197
- new GetEmailIdentityCommand({ EmailIdentity: options.domain })
5198
- );
5199
- return response;
5200
- }
5319
+ const response = await client.send(
5320
+ new ListHostedZonesByNameCommand2({
5321
+ DNSName: domain,
5322
+ MaxItems: 1
5323
+ })
5201
5324
  );
5202
- dkimTokens = identity.DkimAttributes?.Tokens || [];
5203
- mailFromDomain = identity.MailFromAttributes?.MailFromDomain;
5325
+ const zone = response.HostedZones?.[0];
5326
+ if (zone && zone.Name === `${domain}.` && zone.Id) {
5327
+ return {
5328
+ id: zone.Id.replace("/hostedzone/", ""),
5329
+ name: zone.Name
5330
+ };
5331
+ }
5332
+ return null;
5204
5333
  } catch (_error) {
5205
- progress.stop();
5206
- clack6.log.error(`Domain ${options.domain} not found in SES`);
5207
- console.log(
5208
- `
5209
- Run ${pc7.cyan(`wraps email init --domain ${options.domain}`)} to add this domain.
5210
- `
5211
- );
5212
- process.exit(1);
5213
- return;
5334
+ return null;
5214
5335
  }
5215
- const resolver = new Resolver();
5216
- resolver.setServers(["8.8.8.8", "1.1.1.1"]);
5217
- const dnsResults = [];
5218
- for (const token of dkimTokens) {
5219
- const dkimRecord = `${token}._domainkey.${options.domain}`;
5220
- try {
5221
- const records = await resolver.resolveCname(dkimRecord);
5222
- const expected = `${token}.dkim.amazonses.com`;
5223
- const found = records.some((r) => r === expected || r === `${expected}.`);
5224
- dnsResults.push({
5225
- name: dkimRecord,
5226
- type: "CNAME",
5227
- status: found ? "verified" : "incorrect",
5228
- records
5229
- });
5230
- } catch (_error) {
5231
- dnsResults.push({
5232
- name: dkimRecord,
5233
- type: "CNAME",
5234
- status: "missing"
5336
+ }
5337
+ async function deleteDNSRecords(hostedZoneId, domain, dkimTokens, region, customTrackingDomain, mailFromDomain) {
5338
+ const client = new Route53Client2({ region });
5339
+ const response = await client.send(
5340
+ new ListResourceRecordSetsCommand({
5341
+ HostedZoneId: hostedZoneId,
5342
+ MaxItems: 500
5343
+ })
5344
+ );
5345
+ const recordSets = response.ResourceRecordSets || [];
5346
+ const changes = [];
5347
+ const addDeletionIfExists = (name, type) => {
5348
+ const normalizedName = name.endsWith(".") ? name : `${name}.`;
5349
+ const record = recordSets.find(
5350
+ (rs) => rs.Name === normalizedName && rs.Type === type
5351
+ );
5352
+ if (record && record.ResourceRecords) {
5353
+ changes.push({
5354
+ Action: "DELETE",
5355
+ ResourceRecordSet: record
5235
5356
  });
5236
5357
  }
5358
+ };
5359
+ for (const token of dkimTokens) {
5360
+ addDeletionIfExists(`${token}._domainkey.${domain}`, "CNAME");
5237
5361
  }
5238
- try {
5239
- const records = await resolver.resolveTxt(options.domain);
5240
- const spfRecord = records.flat().find((r) => r.startsWith("v=spf1"));
5241
- const hasAmazonSES = spfRecord?.includes("include:amazonses.com");
5242
- dnsResults.push({
5243
- name: options.domain,
5244
- type: "TXT (SPF)",
5245
- status: hasAmazonSES ? "verified" : spfRecord ? "incorrect" : "missing",
5246
- records: spfRecord ? [spfRecord] : void 0
5247
- });
5248
- } catch (_error) {
5249
- dnsResults.push({
5250
- name: options.domain,
5251
- type: "TXT (SPF)",
5252
- status: "missing"
5253
- });
5362
+ addDeletionIfExists(`_dmarc.${domain}`, "TXT");
5363
+ if (customTrackingDomain) {
5364
+ addDeletionIfExists(customTrackingDomain, "CNAME");
5365
+ }
5366
+ if (mailFromDomain) {
5367
+ addDeletionIfExists(mailFromDomain, "MX");
5368
+ addDeletionIfExists(mailFromDomain, "TXT");
5369
+ }
5370
+ if (changes.length === 0) {
5371
+ return;
5254
5372
  }
5373
+ await client.send(
5374
+ new ChangeResourceRecordSetsCommand2({
5375
+ HostedZoneId: hostedZoneId,
5376
+ ChangeBatch: {
5377
+ Changes: changes
5378
+ }
5379
+ })
5380
+ );
5381
+ }
5382
+
5383
+ // src/commands/email/destroy.ts
5384
+ async function getDkimTokensFromSES(domain, region) {
5255
5385
  try {
5256
- const records = await resolver.resolveTxt(`_dmarc.${options.domain}`);
5257
- const dmarcRecord = records.flat().find((r) => r.startsWith("v=DMARC1"));
5258
- dnsResults.push({
5259
- name: `_dmarc.${options.domain}`,
5260
- type: "TXT (DMARC)",
5261
- status: dmarcRecord ? "verified" : "missing",
5262
- records: dmarcRecord ? [dmarcRecord] : void 0
5263
- });
5386
+ const { SESv2Client: SESv2Client5, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
5387
+ const ses = new SESv2Client5({ region });
5388
+ const response = await ses.send(
5389
+ new GetEmailIdentityCommand4({ EmailIdentity: domain })
5390
+ );
5391
+ return response.DkimAttributes?.Tokens || [];
5264
5392
  } catch (_error) {
5265
- dnsResults.push({
5266
- name: `_dmarc.${options.domain}`,
5267
- type: "TXT (DMARC)",
5268
- status: "missing"
5393
+ return [];
5394
+ }
5395
+ }
5396
+ async function emailDestroy(options) {
5397
+ const startTime = Date.now();
5398
+ clack6.intro(
5399
+ pc7.bold(
5400
+ options.preview ? "Email Infrastructure Destruction Preview" : "Email Infrastructure Teardown"
5401
+ )
5402
+ );
5403
+ const progress = new DeploymentProgress();
5404
+ const identity = await progress.execute(
5405
+ "Validating AWS credentials",
5406
+ async () => validateAWSCredentials()
5407
+ );
5408
+ const region = await getAWSRegion();
5409
+ const metadata = await loadConnectionMetadata(identity.accountId, region);
5410
+ const emailService = metadata?.services?.email;
5411
+ const emailConfig = emailService?.config;
5412
+ const domain = emailConfig?.domain;
5413
+ const storedStackName = emailService?.pulumiStackName;
5414
+ if (!(options.force || options.preview)) {
5415
+ const confirmed = await clack6.confirm({
5416
+ message: pc7.red(
5417
+ "Are you sure you want to destroy all email infrastructure?"
5418
+ ),
5419
+ initialValue: false
5269
5420
  });
5421
+ if (clack6.isCancel(confirmed) || !confirmed) {
5422
+ clack6.cancel("Destruction cancelled.");
5423
+ process.exit(0);
5424
+ }
5270
5425
  }
5271
- if (mailFromDomain) {
5426
+ let shouldCleanDNS = false;
5427
+ let hostedZone = null;
5428
+ let dkimTokens = [];
5429
+ if (domain && !options.preview) {
5430
+ hostedZone = await findHostedZone2(domain, region);
5431
+ if (hostedZone) {
5432
+ dkimTokens = await getDkimTokensFromSES(domain, region);
5433
+ if (!options.force) {
5434
+ const cleanDNS = await clack6.confirm({
5435
+ message: `Found Route53 hosted zone for ${pc7.cyan(domain)}. Delete DNS records (DKIM, DMARC, MAIL FROM)?`,
5436
+ initialValue: true
5437
+ });
5438
+ if (clack6.isCancel(cleanDNS)) {
5439
+ clack6.cancel("Destruction cancelled.");
5440
+ process.exit(0);
5441
+ }
5442
+ shouldCleanDNS = cleanDNS;
5443
+ } else {
5444
+ shouldCleanDNS = true;
5445
+ }
5446
+ }
5447
+ }
5448
+ if (options.preview) {
5272
5449
  try {
5273
- const mxRecords = await resolver.resolveMx(mailFromDomain);
5450
+ const previewResult = await progress.execute(
5451
+ "Generating destruction preview",
5452
+ async () => {
5453
+ await ensurePulumiWorkDir();
5454
+ const stackName = storedStackName || `wraps-email-${identity.accountId}-${region}`;
5455
+ let stack;
5456
+ try {
5457
+ stack = await pulumi7.automation.LocalWorkspace.selectStack({
5458
+ stackName,
5459
+ workDir: getPulumiWorkDir()
5460
+ });
5461
+ } catch (_error) {
5462
+ throw new Error("No email infrastructure found to preview");
5463
+ }
5464
+ const result = await stack.preview({ diff: true });
5465
+ return result;
5466
+ }
5467
+ );
5468
+ displayPreview({
5469
+ changeSummary: previewResult.changeSummary,
5470
+ costEstimate: "Monthly cost after destruction: $0.00",
5471
+ commandName: "wraps email destroy"
5472
+ });
5473
+ if (domain) {
5474
+ const previewHostedZone = await findHostedZone2(domain, region);
5475
+ if (previewHostedZone) {
5476
+ clack6.log.info(
5477
+ `DNS records in Route53 for ${pc7.cyan(domain)} will also be deleted`
5478
+ );
5479
+ }
5480
+ }
5481
+ clack6.outro(
5482
+ pc7.green("Preview complete. Run without --preview to destroy.")
5483
+ );
5484
+ trackServiceRemoved("email", {
5485
+ preview: true,
5486
+ duration_ms: Date.now() - startTime
5487
+ });
5488
+ return;
5489
+ } catch (error) {
5490
+ progress.stop();
5491
+ if (error.message.includes("No email infrastructure found")) {
5492
+ clack6.log.warn("No email infrastructure found to preview");
5493
+ process.exit(0);
5494
+ }
5495
+ trackError("PREVIEW_FAILED", "email destroy", { step: "preview" });
5496
+ throw new Error(`Preview failed: ${error.message}`);
5497
+ }
5498
+ }
5499
+ if (shouldCleanDNS && hostedZone && domain && dkimTokens.length > 0) {
5500
+ try {
5501
+ await progress.execute(
5502
+ `Deleting DNS records for ${domain}`,
5503
+ async () => {
5504
+ await deleteDNSRecords(
5505
+ hostedZone.id,
5506
+ domain,
5507
+ dkimTokens,
5508
+ region,
5509
+ emailConfig?.tracking?.customRedirectDomain,
5510
+ emailConfig?.mailFromDomain
5511
+ );
5512
+ }
5513
+ );
5514
+ } catch (error) {
5515
+ clack6.log.warn(`Could not delete DNS records: ${error.message}`);
5516
+ clack6.log.info("You may need to delete them manually from Route53");
5517
+ }
5518
+ }
5519
+ try {
5520
+ await progress.execute(
5521
+ "Destroying email infrastructure (this may take 2-3 minutes)",
5522
+ async () => {
5523
+ await ensurePulumiWorkDir();
5524
+ const stackName = storedStackName || `wraps-email-${identity.accountId}-${region}`;
5525
+ let stack;
5526
+ try {
5527
+ stack = await pulumi7.automation.LocalWorkspace.selectStack({
5528
+ stackName,
5529
+ workDir: getPulumiWorkDir()
5530
+ });
5531
+ } catch (_error) {
5532
+ throw new Error("No email infrastructure found to destroy");
5533
+ }
5534
+ await stack.destroy({ onOutput: () => {
5535
+ } });
5536
+ await stack.workspace.removeStack(stackName);
5537
+ }
5538
+ );
5539
+ } catch (error) {
5540
+ progress.stop();
5541
+ if (error.message.includes("No email infrastructure found")) {
5542
+ clack6.log.warn("No email infrastructure found");
5543
+ await deleteConnectionMetadata(identity.accountId, region);
5544
+ process.exit(0);
5545
+ }
5546
+ trackError("DESTROY_FAILED", "email destroy", { step: "destroy" });
5547
+ clack6.log.error("Email infrastructure destruction failed");
5548
+ throw error;
5549
+ }
5550
+ await deleteConnectionMetadata(identity.accountId, region);
5551
+ progress.stop();
5552
+ const deletedItems = ["AWS infrastructure"];
5553
+ if (shouldCleanDNS && hostedZone) {
5554
+ deletedItems.push("Route53 DNS records");
5555
+ }
5556
+ clack6.outro(pc7.green(`Email infrastructure has been removed`));
5557
+ if (domain) {
5558
+ console.log(`
5559
+ ${pc7.bold("Cleaned up:")}`);
5560
+ for (const item of deletedItems) {
5561
+ console.log(` ${pc7.green("\u2713")} ${item}`);
5562
+ }
5563
+ console.log(
5564
+ `
5565
+ ${pc7.dim("Note: SPF record was not deleted. Remove 'include:amazonses.com' manually if needed.")}`
5566
+ );
5567
+ }
5568
+ console.log(
5569
+ `
5570
+ Run ${pc7.cyan("wraps email init")} to deploy infrastructure again.
5571
+ `
5572
+ );
5573
+ trackServiceRemoved("email", {
5574
+ reason: "user_initiated",
5575
+ duration_ms: Date.now() - startTime,
5576
+ dns_cleaned: shouldCleanDNS
5577
+ });
5578
+ }
5579
+
5580
+ // src/commands/email/domains.ts
5581
+ init_esm_shims();
5582
+ init_events();
5583
+ init_aws();
5584
+ import { Resolver } from "dns/promises";
5585
+ import { GetEmailIdentityCommand, SESv2Client as SESv2Client2 } from "@aws-sdk/client-sesv2";
5586
+ import * as clack7 from "@clack/prompts";
5587
+ import pc8 from "picocolors";
5588
+ async function verifyDomain(options) {
5589
+ clack7.intro(pc8.bold(`Verifying ${options.domain}`));
5590
+ const progress = new DeploymentProgress();
5591
+ const region = await getAWSRegion();
5592
+ const sesClient = new SESv2Client2({ region });
5593
+ let identity;
5594
+ let dkimTokens = [];
5595
+ let mailFromDomain;
5596
+ try {
5597
+ identity = await progress.execute(
5598
+ "Checking SES verification status",
5599
+ async () => {
5600
+ const response = await sesClient.send(
5601
+ new GetEmailIdentityCommand({ EmailIdentity: options.domain })
5602
+ );
5603
+ return response;
5604
+ }
5605
+ );
5606
+ dkimTokens = identity.DkimAttributes?.Tokens || [];
5607
+ mailFromDomain = identity.MailFromAttributes?.MailFromDomain;
5608
+ } catch (_error) {
5609
+ progress.stop();
5610
+ clack7.log.error(`Domain ${options.domain} not found in SES`);
5611
+ console.log(
5612
+ `
5613
+ Run ${pc8.cyan(`wraps email init --domain ${options.domain}`)} to add this domain.
5614
+ `
5615
+ );
5616
+ process.exit(1);
5617
+ return;
5618
+ }
5619
+ const resolver = new Resolver();
5620
+ resolver.setServers(["8.8.8.8", "1.1.1.1"]);
5621
+ const dnsResults = [];
5622
+ for (const token of dkimTokens) {
5623
+ const dkimRecord = `${token}._domainkey.${options.domain}`;
5624
+ try {
5625
+ const records = await resolver.resolveCname(dkimRecord);
5626
+ const expected = `${token}.dkim.amazonses.com`;
5627
+ const found = records.some((r) => r === expected || r === `${expected}.`);
5628
+ dnsResults.push({
5629
+ name: dkimRecord,
5630
+ type: "CNAME",
5631
+ status: found ? "verified" : "incorrect",
5632
+ records
5633
+ });
5634
+ } catch (_error) {
5635
+ dnsResults.push({
5636
+ name: dkimRecord,
5637
+ type: "CNAME",
5638
+ status: "missing"
5639
+ });
5640
+ }
5641
+ }
5642
+ try {
5643
+ const records = await resolver.resolveTxt(options.domain);
5644
+ const spfRecord = records.flat().find((r) => r.startsWith("v=spf1"));
5645
+ const hasAmazonSES = spfRecord?.includes("include:amazonses.com");
5646
+ dnsResults.push({
5647
+ name: options.domain,
5648
+ type: "TXT (SPF)",
5649
+ status: hasAmazonSES ? "verified" : spfRecord ? "incorrect" : "missing",
5650
+ records: spfRecord ? [spfRecord] : void 0
5651
+ });
5652
+ } catch (_error) {
5653
+ dnsResults.push({
5654
+ name: options.domain,
5655
+ type: "TXT (SPF)",
5656
+ status: "missing"
5657
+ });
5658
+ }
5659
+ try {
5660
+ const records = await resolver.resolveTxt(`_dmarc.${options.domain}`);
5661
+ const dmarcRecord = records.flat().find((r) => r.startsWith("v=DMARC1"));
5662
+ dnsResults.push({
5663
+ name: `_dmarc.${options.domain}`,
5664
+ type: "TXT (DMARC)",
5665
+ status: dmarcRecord ? "verified" : "missing",
5666
+ records: dmarcRecord ? [dmarcRecord] : void 0
5667
+ });
5668
+ } catch (_error) {
5669
+ dnsResults.push({
5670
+ name: `_dmarc.${options.domain}`,
5671
+ type: "TXT (DMARC)",
5672
+ status: "missing"
5673
+ });
5674
+ }
5675
+ if (mailFromDomain) {
5676
+ try {
5677
+ const mxRecords = await resolver.resolveMx(mailFromDomain);
5274
5678
  const expectedMx = `feedback-smtp.${region}.amazonses.com`;
5275
5679
  const hasMx = mxRecords.some(
5276
5680
  (r) => r.exchange === expectedMx || r.exchange === `${expectedMx}.`
@@ -5311,63 +5715,69 @@ Run ${pc7.cyan(`wraps email init --domain ${options.domain}`)} to add this domai
5311
5715
  const dkimStatus = identity.DkimAttributes?.Status || "PENDING";
5312
5716
  const mailFromStatus = identity.MailFromAttributes?.MailFromDomainStatus || "NOT_CONFIGURED";
5313
5717
  const statusLines = [
5314
- `${pc7.bold("Domain:")} ${options.domain}`,
5315
- `${pc7.bold("Verification Status:")} ${verificationStatus === "verified" ? pc7.green("\u2713 Verified") : pc7.yellow("\u23F1 Pending")}`,
5316
- `${pc7.bold("DKIM Status:")} ${dkimStatus === "SUCCESS" ? pc7.green("\u2713 Success") : pc7.yellow(`\u23F1 ${dkimStatus}`)}`
5718
+ `${pc8.bold("Domain:")} ${options.domain}`,
5719
+ `${pc8.bold("Verification Status:")} ${verificationStatus === "verified" ? pc8.green("\u2713 Verified") : pc8.yellow("\u23F1 Pending")}`,
5720
+ `${pc8.bold("DKIM Status:")} ${dkimStatus === "SUCCESS" ? pc8.green("\u2713 Success") : pc8.yellow(`\u23F1 ${dkimStatus}`)}`
5317
5721
  ];
5318
5722
  if (mailFromDomain) {
5319
5723
  statusLines.push(
5320
- `${pc7.bold("MAIL FROM Domain:")} ${mailFromDomain}`,
5321
- `${pc7.bold("MAIL FROM Status:")} ${mailFromStatus === "SUCCESS" ? pc7.green("\u2713 Success") : mailFromStatus === "NOT_CONFIGURED" ? pc7.yellow("\u23F1 Not Configured") : pc7.yellow(`\u23F1 ${mailFromStatus}`)}`
5724
+ `${pc8.bold("MAIL FROM Domain:")} ${mailFromDomain}`,
5725
+ `${pc8.bold("MAIL FROM Status:")} ${mailFromStatus === "SUCCESS" ? pc8.green("\u2713 Success") : mailFromStatus === "NOT_CONFIGURED" ? pc8.yellow("\u23F1 Not Configured") : pc8.yellow(`\u23F1 ${mailFromStatus}`)}`
5322
5726
  );
5323
5727
  }
5324
- clack6.note(statusLines.join("\n"), "SES Status");
5728
+ clack7.note(statusLines.join("\n"), "SES Status");
5325
5729
  const dnsLines = dnsResults.map((record) => {
5326
5730
  let statusIcon;
5327
5731
  let statusColor;
5328
5732
  if (record.status === "verified") {
5329
5733
  statusIcon = "\u2713";
5330
- statusColor = pc7.green;
5734
+ statusColor = pc8.green;
5331
5735
  } else if (record.status === "incorrect") {
5332
5736
  statusIcon = "\u2717";
5333
- statusColor = pc7.red;
5737
+ statusColor = pc8.red;
5334
5738
  } else {
5335
5739
  statusIcon = "\u2717";
5336
- statusColor = pc7.red;
5740
+ statusColor = pc8.red;
5337
5741
  }
5338
5742
  const recordInfo = record.records ? ` \u2192 ${record.records.join(", ")}` : "";
5339
5743
  return ` ${statusColor(statusIcon)} ${record.name} (${record.type}) ${statusColor(
5340
5744
  record.status
5341
5745
  )}${recordInfo}`;
5342
5746
  });
5343
- clack6.note(dnsLines.join("\n"), "DNS Records");
5747
+ clack7.note(dnsLines.join("\n"), "DNS Records");
5344
5748
  const allVerified = dnsResults.every((r) => r.status === "verified");
5345
5749
  const someIncorrect = dnsResults.some((r) => r.status === "incorrect");
5346
5750
  if (verificationStatus === "verified" && allVerified) {
5347
- clack6.outro(
5348
- pc7.green("\u2713 Domain is fully verified and ready to send emails!")
5751
+ clack7.outro(
5752
+ pc8.green("\u2713 Domain is fully verified and ready to send emails!")
5349
5753
  );
5754
+ trackFeature("domain_verified", { dns_auto_detected: true });
5350
5755
  } else if (someIncorrect) {
5351
- clack6.outro(
5352
- pc7.red("\u2717 Some DNS records are incorrect. Please update them.")
5756
+ clack7.outro(
5757
+ pc8.red("\u2717 Some DNS records are incorrect. Please update them.")
5353
5758
  );
5354
5759
  console.log(
5355
5760
  `
5356
- Run ${pc7.cyan("wraps email status")} to see the correct DNS records.
5761
+ Run ${pc8.cyan("wraps email status")} to see the correct DNS records.
5357
5762
  `
5358
5763
  );
5359
5764
  } else {
5360
- clack6.outro(
5361
- pc7.yellow("\u23F1 Waiting for DNS propagation and SES verification")
5765
+ clack7.outro(
5766
+ pc8.yellow("\u23F1 Waiting for DNS propagation and SES verification")
5362
5767
  );
5363
5768
  console.log("\nDNS records can take up to 48 hours to propagate.");
5364
5769
  console.log(
5365
5770
  "SES verification usually completes within 72 hours after DNS propagation.\n"
5366
5771
  );
5367
5772
  }
5773
+ trackCommand("email:domains:verify", {
5774
+ success: true,
5775
+ verified: verificationStatus === "verified" && allVerified,
5776
+ dkim_status: dkimStatus
5777
+ });
5368
5778
  }
5369
5779
  async function addDomain(options) {
5370
- clack6.intro(pc7.bold(`Adding domain ${options.domain} to SES`));
5780
+ clack7.intro(pc8.bold(`Adding domain ${options.domain} to SES`));
5371
5781
  const progress = new DeploymentProgress();
5372
5782
  const region = await getAWSRegion();
5373
5783
  const sesClient = new SESv2Client2({ region });
@@ -5377,10 +5787,10 @@ async function addDomain(options) {
5377
5787
  new GetEmailIdentityCommand({ EmailIdentity: options.domain })
5378
5788
  );
5379
5789
  progress.stop();
5380
- clack6.log.warn(`Domain ${options.domain} already exists in SES`);
5790
+ clack7.log.warn(`Domain ${options.domain} already exists in SES`);
5381
5791
  console.log(
5382
5792
  `
5383
- Run ${pc7.cyan(`wraps email domains verify --domain ${options.domain}`)} to check verification status.
5793
+ Run ${pc8.cyan(`wraps email domains verify --domain ${options.domain}`)} to check verification status.
5384
5794
  `
5385
5795
  );
5386
5796
  return;
@@ -5405,30 +5815,37 @@ Run ${pc7.cyan(`wraps email domains verify --domain ${options.domain}`)} to chec
5405
5815
  );
5406
5816
  const dkimTokens = identity.DkimAttributes?.Tokens || [];
5407
5817
  progress.stop();
5408
- clack6.outro(pc7.green(`\u2713 Domain ${options.domain} added successfully!`));
5818
+ clack7.outro(pc8.green(`\u2713 Domain ${options.domain} added successfully!`));
5409
5819
  console.log(`
5410
- ${pc7.bold("Next steps:")}
5820
+ ${pc8.bold("Next steps:")}
5411
5821
  `);
5412
5822
  console.log("1. Add the following DKIM records to your DNS:\n");
5413
5823
  for (const token of dkimTokens) {
5414
- console.log(` ${pc7.cyan(`${token}._domainkey.${options.domain}`)}`);
5824
+ console.log(` ${pc8.cyan(`${token}._domainkey.${options.domain}`)}`);
5415
5825
  console.log(
5416
- ` ${pc7.dim("Type:")} CNAME ${pc7.dim("Value:")} ${token}.dkim.amazonses.com
5826
+ ` ${pc8.dim("Type:")} CNAME ${pc8.dim("Value:")} ${token}.dkim.amazonses.com
5417
5827
  `
5418
5828
  );
5419
5829
  }
5420
5830
  console.log(
5421
- `2. Verify DNS propagation: ${pc7.cyan(`wraps email domains verify --domain ${options.domain}`)}`
5831
+ `2. Verify DNS propagation: ${pc8.cyan(`wraps email domains verify --domain ${options.domain}`)}`
5422
5832
  );
5423
- console.log(`3. Check status: ${pc7.cyan("wraps email status")}
5833
+ console.log(`3. Check status: ${pc8.cyan("wraps email status")}
5424
5834
  `);
5835
+ trackCommand("email:domains:add", {
5836
+ success: true
5837
+ });
5838
+ trackFeature("domain_added", {});
5425
5839
  } catch (error) {
5426
5840
  progress.stop();
5841
+ trackCommand("email:domains:add", {
5842
+ success: false
5843
+ });
5427
5844
  throw error;
5428
5845
  }
5429
5846
  }
5430
5847
  async function listDomains() {
5431
- clack6.intro(pc7.bold("SES Email Domains"));
5848
+ clack7.intro(pc8.bold("SES Email Domains"));
5432
5849
  const progress = new DeploymentProgress();
5433
5850
  const region = await getAWSRegion();
5434
5851
  const sesClient = new SESv2Client2({ region });
@@ -5448,10 +5865,10 @@ async function listDomains() {
5448
5865
  );
5449
5866
  progress.stop();
5450
5867
  if (domains.length === 0) {
5451
- clack6.outro("No domains found in SES");
5868
+ clack7.outro("No domains found in SES");
5452
5869
  console.log(
5453
5870
  `
5454
- Run ${pc7.cyan("wraps email domains add <domain>")} to add a domain.
5871
+ Run ${pc8.cyan("wraps email domains add <domain>")} to add a domain.
5455
5872
  `
5456
5873
  );
5457
5874
  return;
@@ -5479,26 +5896,31 @@ Run ${pc7.cyan("wraps email domains add <domain>")} to add a domain.
5479
5896
  })
5480
5897
  );
5481
5898
  const domainLines = domainDetails.map((domain) => {
5482
- const statusIcon = domain.verified ? pc7.green("\u2713") : pc7.yellow("\u23F1");
5483
- const dkimIcon = domain.dkimStatus === "SUCCESS" ? pc7.green("\u2713") : pc7.yellow("\u23F1");
5484
- return ` ${statusIcon} ${pc7.bold(domain.name)} DKIM: ${dkimIcon} ${domain.dkimStatus}`;
5899
+ const statusIcon = domain.verified ? pc8.green("\u2713") : pc8.yellow("\u23F1");
5900
+ const dkimIcon = domain.dkimStatus === "SUCCESS" ? pc8.green("\u2713") : pc8.yellow("\u23F1");
5901
+ return ` ${statusIcon} ${pc8.bold(domain.name)} DKIM: ${dkimIcon} ${domain.dkimStatus}`;
5485
5902
  });
5486
- clack6.note(
5903
+ clack7.note(
5487
5904
  domainLines.join("\n"),
5488
5905
  `${domains.length} domain(s) in ${region}`
5489
5906
  );
5490
- clack6.outro(
5491
- pc7.dim(
5492
- `Run ${pc7.cyan("wraps email domains verify --domain <domain>")} for details`
5907
+ clack7.outro(
5908
+ pc8.dim(
5909
+ `Run ${pc8.cyan("wraps email domains verify --domain <domain>")} for details`
5493
5910
  )
5494
5911
  );
5912
+ trackCommand("email:domains:list", {
5913
+ success: true,
5914
+ domain_count: domains.length
5915
+ });
5495
5916
  } catch (error) {
5496
5917
  progress.stop();
5918
+ trackCommand("email:domains:list", { success: false });
5497
5919
  throw error;
5498
5920
  }
5499
5921
  }
5500
5922
  async function getDkim(options) {
5501
- clack6.intro(pc7.bold(`DKIM Tokens for ${options.domain}`));
5923
+ clack7.intro(pc8.bold(`DKIM Tokens for ${options.domain}`));
5502
5924
  const progress = new DeploymentProgress();
5503
5925
  const region = await getAWSRegion();
5504
5926
  const sesClient = new SESv2Client2({ region });
@@ -5516,33 +5938,38 @@ async function getDkim(options) {
5516
5938
  const dkimStatus = identity.DkimAttributes?.Status || "PENDING";
5517
5939
  progress.stop();
5518
5940
  if (dkimTokens.length === 0) {
5519
- clack6.outro(pc7.yellow("No DKIM tokens found for this domain"));
5941
+ clack7.outro(pc8.yellow("No DKIM tokens found for this domain"));
5520
5942
  return;
5521
5943
  }
5522
- const statusLine = `${pc7.bold("DKIM Status:")} ${dkimStatus === "SUCCESS" ? pc7.green("\u2713 Verified") : pc7.yellow(`\u23F1 ${dkimStatus}`)}`;
5523
- clack6.note(statusLine, "Status");
5944
+ const statusLine = `${pc8.bold("DKIM Status:")} ${dkimStatus === "SUCCESS" ? pc8.green("\u2713 Verified") : pc8.yellow(`\u23F1 ${dkimStatus}`)}`;
5945
+ clack7.note(statusLine, "Status");
5524
5946
  console.log(`
5525
- ${pc7.bold("DNS Records to add:")}
5947
+ ${pc8.bold("DNS Records to add:")}
5526
5948
  `);
5527
5949
  for (const token of dkimTokens) {
5528
- console.log(`${pc7.cyan(`${token}._domainkey.${options.domain}`)}`);
5529
- console.log(` ${pc7.dim("Type:")} CNAME`);
5530
- console.log(` ${pc7.dim("Value:")} ${token}.dkim.amazonses.com
5950
+ console.log(`${pc8.cyan(`${token}._domainkey.${options.domain}`)}`);
5951
+ console.log(` ${pc8.dim("Type:")} CNAME`);
5952
+ console.log(` ${pc8.dim("Value:")} ${token}.dkim.amazonses.com
5531
5953
  `);
5532
5954
  }
5533
5955
  if (dkimStatus !== "SUCCESS") {
5534
5956
  console.log(
5535
- `${pc7.dim("After adding these records, run:")} ${pc7.cyan(`wraps email domains verify --domain ${options.domain}`)}
5957
+ `${pc8.dim("After adding these records, run:")} ${pc8.cyan(`wraps email domains verify --domain ${options.domain}`)}
5536
5958
  `
5537
5959
  );
5538
5960
  }
5961
+ trackCommand("email:domains:get-dkim", {
5962
+ success: true,
5963
+ dkim_status: dkimStatus
5964
+ });
5539
5965
  } catch (error) {
5540
5966
  progress.stop();
5967
+ trackCommand("email:domains:get-dkim", { success: false });
5541
5968
  if (error.name === "NotFoundException") {
5542
- clack6.log.error(`Domain ${options.domain} not found in SES`);
5969
+ clack7.log.error(`Domain ${options.domain} not found in SES`);
5543
5970
  console.log(
5544
5971
  `
5545
- Run ${pc7.cyan(`wraps email domains add ${options.domain}`)} to add this domain.
5972
+ Run ${pc8.cyan(`wraps email domains add ${options.domain}`)} to add this domain.
5546
5973
  `
5547
5974
  );
5548
5975
  process.exit(1);
@@ -5552,7 +5979,7 @@ Run ${pc7.cyan(`wraps email domains add ${options.domain}`)} to add this domain.
5552
5979
  }
5553
5980
  }
5554
5981
  async function removeDomain(options) {
5555
- clack6.intro(pc7.bold(`Remove domain ${options.domain} from SES`));
5982
+ clack7.intro(pc8.bold(`Remove domain ${options.domain} from SES`));
5556
5983
  const progress = new DeploymentProgress();
5557
5984
  const region = await getAWSRegion();
5558
5985
  const sesClient = new SESv2Client2({ region });
@@ -5564,12 +5991,12 @@ async function removeDomain(options) {
5564
5991
  });
5565
5992
  progress.stop();
5566
5993
  if (!options.force) {
5567
- const shouldContinue = await clack6.confirm({
5568
- message: `Are you sure you want to remove ${pc7.red(options.domain)} from SES?`,
5994
+ const shouldContinue = await clack7.confirm({
5995
+ message: `Are you sure you want to remove ${pc8.red(options.domain)} from SES?`,
5569
5996
  initialValue: false
5570
5997
  });
5571
- if (clack6.isCancel(shouldContinue) || !shouldContinue) {
5572
- clack6.cancel("Operation cancelled");
5998
+ if (clack7.isCancel(shouldContinue) || !shouldContinue) {
5999
+ clack7.cancel("Operation cancelled");
5573
6000
  process.exit(0);
5574
6001
  }
5575
6002
  }
@@ -5582,11 +6009,16 @@ async function removeDomain(options) {
5582
6009
  );
5583
6010
  });
5584
6011
  progress.stop();
5585
- clack6.outro(pc7.green(`\u2713 Domain ${options.domain} removed successfully`));
6012
+ clack7.outro(pc8.green(`\u2713 Domain ${options.domain} removed successfully`));
6013
+ trackCommand("email:domains:remove", {
6014
+ success: true
6015
+ });
6016
+ trackFeature("domain_removed", {});
5586
6017
  } catch (error) {
5587
6018
  progress.stop();
6019
+ trackCommand("email:domains:remove", { success: false });
5588
6020
  if (error.name === "NotFoundException") {
5589
- clack6.log.error(`Domain ${options.domain} not found in SES`);
6021
+ clack7.log.error(`Domain ${options.domain} not found in SES`);
5590
6022
  process.exit(1);
5591
6023
  return;
5592
6024
  }
@@ -5596,17 +6028,19 @@ async function removeDomain(options) {
5596
6028
 
5597
6029
  // src/commands/email/init.ts
5598
6030
  init_esm_shims();
5599
- import * as clack7 from "@clack/prompts";
5600
- import * as pulumi7 from "@pulumi/pulumi";
5601
- import pc8 from "picocolors";
6031
+ import * as clack8 from "@clack/prompts";
6032
+ import * as pulumi8 from "@pulumi/pulumi";
6033
+ import pc9 from "picocolors";
6034
+ init_events();
5602
6035
  init_costs();
5603
6036
  init_presets();
5604
6037
  init_aws();
5605
6038
  init_errors();
5606
6039
  init_prompts();
5607
6040
  async function init(options) {
5608
- clack7.intro(
5609
- pc8.bold(
6041
+ const startTime = Date.now();
6042
+ clack8.intro(
6043
+ pc9.bold(
5610
6044
  options.preview ? "Wraps Email Infrastructure Preview" : "Wraps Email Infrastructure Setup"
5611
6045
  )
5612
6046
  );
@@ -5622,7 +6056,7 @@ async function init(options) {
5622
6056
  "Validating AWS credentials",
5623
6057
  async () => validateAWSCredentials()
5624
6058
  );
5625
- progress.info(`Connected to AWS account: ${pc8.cyan(identity.accountId)}`);
6059
+ progress.info(`Connected to AWS account: ${pc9.cyan(identity.accountId)}`);
5626
6060
  let provider = options.provider;
5627
6061
  if (!provider) {
5628
6062
  provider = await promptProvider();
@@ -5645,12 +6079,12 @@ async function init(options) {
5645
6079
  region
5646
6080
  );
5647
6081
  if (existingConnection) {
5648
- clack7.log.warn(
5649
- `Connection already exists for account ${pc8.cyan(identity.accountId)} in region ${pc8.cyan(region)}`
6082
+ clack8.log.warn(
6083
+ `Connection already exists for account ${pc9.cyan(identity.accountId)} in region ${pc9.cyan(region)}`
5650
6084
  );
5651
- clack7.log.info(`Created: ${existingConnection.timestamp}`);
5652
- clack7.log.info(`Use ${pc8.cyan("wraps status")} to view current setup`);
5653
- clack7.log.info(`Use ${pc8.cyan("wraps upgrade")} to add more features`);
6085
+ clack8.log.info(`Created: ${existingConnection.timestamp}`);
6086
+ clack8.log.info(`Use ${pc9.cyan("wraps status")} to view current setup`);
6087
+ clack8.log.info(`Use ${pc9.cyan("wraps upgrade")} to add more features`);
5654
6088
  process.exit(0);
5655
6089
  }
5656
6090
  let preset = options.preset;
@@ -5671,15 +6105,15 @@ async function init(options) {
5671
6105
  }
5672
6106
  const estimatedVolume = await promptEstimatedVolume();
5673
6107
  progress.info(`
5674
- ${pc8.bold("Cost Estimate:")}`);
6108
+ ${pc9.bold("Cost Estimate:")}`);
5675
6109
  const costSummary = getCostSummary(emailConfig, estimatedVolume);
5676
- clack7.log.info(costSummary);
6110
+ clack8.log.info(costSummary);
5677
6111
  const warnings = validateConfig(emailConfig);
5678
6112
  if (warnings.length > 0) {
5679
6113
  progress.info(`
5680
- ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
6114
+ ${pc9.yellow(pc9.bold("Configuration Warnings:"))}`);
5681
6115
  for (const warning of warnings) {
5682
- clack7.log.warn(warning);
6116
+ clack8.log.warn(warning);
5683
6117
  }
5684
6118
  }
5685
6119
  const metadata = createConnectionMetadata(
@@ -5695,7 +6129,7 @@ ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
5695
6129
  if (!(options.yes || options.preview)) {
5696
6130
  const confirmed = await confirmDeploy();
5697
6131
  if (!confirmed) {
5698
- clack7.cancel("Deployment cancelled.");
6132
+ clack8.cancel("Deployment cancelled.");
5699
6133
  process.exit(0);
5700
6134
  }
5701
6135
  }
@@ -5711,7 +6145,7 @@ ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
5711
6145
  "Generating infrastructure preview",
5712
6146
  async () => {
5713
6147
  await ensurePulumiWorkDir();
5714
- const stack = await pulumi7.automation.LocalWorkspace.createOrSelectStack(
6148
+ const stack = await pulumi8.automation.LocalWorkspace.createOrSelectStack(
5715
6149
  {
5716
6150
  stackName: `wraps-${identity.accountId}-${region}`,
5717
6151
  projectName: "wraps-email",
@@ -5752,11 +6186,18 @@ ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
5752
6186
  costEstimate: costSummary,
5753
6187
  commandName: "wraps email init"
5754
6188
  });
5755
- clack7.outro(
5756
- pc8.green("Preview complete. Run without --preview to deploy.")
6189
+ clack8.outro(
6190
+ pc9.green("Preview complete. Run without --preview to deploy.")
5757
6191
  );
6192
+ trackServiceInit("email", true, {
6193
+ preset,
6194
+ provider,
6195
+ preview: true,
6196
+ duration_ms: Date.now() - startTime
6197
+ });
5758
6198
  return;
5759
6199
  } catch (error) {
6200
+ trackError("PREVIEW_FAILED", "email:init", { step: "preview" });
5760
6201
  if (error.message?.includes("stack is currently locked")) {
5761
6202
  throw errors.stackLocked();
5762
6203
  }
@@ -5769,7 +6210,7 @@ ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
5769
6210
  "Deploying infrastructure (this may take 2-3 minutes)",
5770
6211
  async () => {
5771
6212
  await ensurePulumiWorkDir();
5772
- const stack = await pulumi7.automation.LocalWorkspace.createOrSelectStack(
6213
+ const stack = await pulumi8.automation.LocalWorkspace.createOrSelectStack(
5773
6214
  {
5774
6215
  stackName: `wraps-${identity.accountId}-${region}`,
5775
6216
  projectName: "wraps-email",
@@ -5826,9 +6267,16 @@ ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
5826
6267
  }
5827
6268
  );
5828
6269
  } catch (error) {
6270
+ trackServiceInit("email", false, {
6271
+ preset,
6272
+ provider,
6273
+ duration_ms: Date.now() - startTime
6274
+ });
5829
6275
  if (error.message?.includes("stack is currently locked")) {
6276
+ trackError("STACK_LOCKED", "email:init", { step: "deploy" });
5830
6277
  throw errors.stackLocked();
5831
6278
  }
6279
+ trackError("DEPLOYMENT_FAILED", "email:init", { step: "deploy" });
5832
6280
  throw new Error(`Pulumi deployment failed: ${error.message}`);
5833
6281
  }
5834
6282
  if (metadata.services.email) {
@@ -5838,8 +6286,8 @@ ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
5838
6286
  progress.info("Connection metadata saved for upgrade and restore capability");
5839
6287
  let dnsAutoCreated = false;
5840
6288
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
5841
- const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
5842
- const hostedZone = await findHostedZone2(outputs.domain, region);
6289
+ const { findHostedZone: findHostedZone3, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
6290
+ const hostedZone = await findHostedZone3(outputs.domain, region);
5843
6291
  if (hostedZone) {
5844
6292
  try {
5845
6293
  progress.start("Creating DNS records in Route53");
@@ -5855,7 +6303,7 @@ ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
5855
6303
  dnsAutoCreated = true;
5856
6304
  } catch (error) {
5857
6305
  progress.fail("Failed to create DNS records in Route53");
5858
- clack7.log.warn(`Could not auto-create DNS records: ${error.message}`);
6306
+ clack8.log.warn(`Could not auto-create DNS records: ${error.message}`);
5859
6307
  }
5860
6308
  }
5861
6309
  }
@@ -5879,24 +6327,49 @@ ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
5879
6327
  domain: outputs.domain,
5880
6328
  mailFromDomain: outputs.mailFromDomain
5881
6329
  });
6330
+ const duration = Date.now() - startTime;
6331
+ const enabledFeatures = [];
6332
+ if (emailConfig.tracking?.enabled) enabledFeatures.push("tracking");
6333
+ if (emailConfig.suppressionList?.enabled)
6334
+ enabledFeatures.push("suppression_list");
6335
+ if (emailConfig.eventTracking?.enabled)
6336
+ enabledFeatures.push("event_tracking");
6337
+ if (emailConfig.eventTracking?.dynamoDBHistory)
6338
+ enabledFeatures.push("dynamodb_history");
6339
+ if (emailConfig.dedicatedIp) enabledFeatures.push("dedicated_ip");
6340
+ if (emailConfig.emailArchiving?.enabled)
6341
+ enabledFeatures.push("email_archiving");
6342
+ trackServiceInit("email", true, {
6343
+ preset,
6344
+ provider,
6345
+ features: enabledFeatures,
6346
+ duration_ms: duration
6347
+ });
6348
+ trackServiceDeployed("email", {
6349
+ duration_ms: duration,
6350
+ features: enabledFeatures,
6351
+ preset
6352
+ });
5882
6353
  }
5883
6354
 
5884
6355
  // src/commands/email/restore.ts
5885
6356
  init_esm_shims();
6357
+ init_events();
5886
6358
  init_aws();
5887
- import * as clack8 from "@clack/prompts";
5888
- import * as pulumi8 from "@pulumi/pulumi";
5889
- import pc9 from "picocolors";
6359
+ import * as clack9 from "@clack/prompts";
6360
+ import * as pulumi9 from "@pulumi/pulumi";
6361
+ import pc10 from "picocolors";
5890
6362
  async function restore(options) {
5891
- clack8.intro(
5892
- pc9.bold(
6363
+ const startTime = Date.now();
6364
+ clack9.intro(
6365
+ pc10.bold(
5893
6366
  options.preview ? "Wraps Restore Preview" : "Wraps Restore - Remove Wraps Infrastructure"
5894
6367
  )
5895
6368
  );
5896
- clack8.log.info(
5897
- `${pc9.yellow("Note:")} This will remove all Wraps-managed infrastructure.`
6369
+ clack9.log.info(
6370
+ `${pc10.yellow("Note:")} This will remove all Wraps-managed infrastructure.`
5898
6371
  );
5899
- clack8.log.info(
6372
+ clack9.log.info(
5900
6373
  "Your original AWS resources remain untouched (Wraps never modifies them).\n"
5901
6374
  );
5902
6375
  const progress = new DeploymentProgress();
@@ -5904,7 +6377,7 @@ async function restore(options) {
5904
6377
  "Validating AWS credentials",
5905
6378
  async () => validateAWSCredentials()
5906
6379
  );
5907
- progress.info(`Connected to AWS account: ${pc9.cyan(identity.accountId)}`);
6380
+ progress.info(`Connected to AWS account: ${pc10.cyan(identity.accountId)}`);
5908
6381
  let region = options.region;
5909
6382
  if (!region) {
5910
6383
  const defaultRegion = await getAWSRegion();
@@ -5912,40 +6385,40 @@ async function restore(options) {
5912
6385
  }
5913
6386
  const metadata = await loadConnectionMetadata(identity.accountId, region);
5914
6387
  if (!metadata) {
5915
- clack8.log.error(
5916
- `No Wraps connection found for account ${pc9.cyan(identity.accountId)} in region ${pc9.cyan(region)}`
6388
+ clack9.log.error(
6389
+ `No Wraps connection found for account ${pc10.cyan(identity.accountId)} in region ${pc10.cyan(region)}`
5917
6390
  );
5918
- clack8.log.info(
5919
- `Use ${pc9.cyan("wraps email init")} or ${pc9.cyan("wraps email connect")} to create a connection first.`
6391
+ clack9.log.info(
6392
+ `Use ${pc10.cyan("wraps email init")} or ${pc10.cyan("wraps email connect")} to create a connection first.`
5920
6393
  );
5921
6394
  process.exit(1);
5922
6395
  }
5923
6396
  progress.info(`Found connection created: ${metadata.timestamp}`);
5924
6397
  console.log(
5925
6398
  `
5926
- ${pc9.bold("The following Wraps resources will be removed:")}
6399
+ ${pc10.bold("The following Wraps resources will be removed:")}
5927
6400
  `
5928
6401
  );
5929
6402
  if (metadata.services.email?.config.tracking?.enabled) {
5930
- console.log(` ${pc9.cyan("\u2713")} Configuration Set (wraps-email-tracking)`);
6403
+ console.log(` ${pc10.cyan("\u2713")} Configuration Set (wraps-email-tracking)`);
5931
6404
  }
5932
6405
  if (metadata.services.email?.config.eventTracking?.dynamoDBHistory) {
5933
- console.log(` ${pc9.cyan("\u2713")} DynamoDB Table (wraps-email-history)`);
6406
+ console.log(` ${pc10.cyan("\u2713")} DynamoDB Table (wraps-email-history)`);
5934
6407
  }
5935
6408
  if (metadata.services.email?.config.eventTracking?.enabled) {
5936
- console.log(` ${pc9.cyan("\u2713")} EventBridge Rules`);
5937
- console.log(` ${pc9.cyan("\u2713")} SQS Queues`);
5938
- console.log(` ${pc9.cyan("\u2713")} Lambda Functions`);
6409
+ console.log(` ${pc10.cyan("\u2713")} EventBridge Rules`);
6410
+ console.log(` ${pc10.cyan("\u2713")} SQS Queues`);
6411
+ console.log(` ${pc10.cyan("\u2713")} Lambda Functions`);
5939
6412
  }
5940
- console.log(` ${pc9.cyan("\u2713")} IAM Role (wraps-email-role)`);
6413
+ console.log(` ${pc10.cyan("\u2713")} IAM Role (wraps-email-role)`);
5941
6414
  console.log("");
5942
6415
  if (!(options.force || options.preview)) {
5943
- const confirmed = await clack8.confirm({
6416
+ const confirmed = await clack9.confirm({
5944
6417
  message: "Proceed with removal? This cannot be undone.",
5945
6418
  initialValue: false
5946
6419
  });
5947
- if (clack8.isCancel(confirmed) || !confirmed) {
5948
- clack8.cancel("Removal cancelled.");
6420
+ if (clack9.isCancel(confirmed) || !confirmed) {
6421
+ clack9.cancel("Removal cancelled.");
5949
6422
  process.exit(0);
5950
6423
  }
5951
6424
  }
@@ -5955,7 +6428,7 @@ ${pc9.bold("The following Wraps resources will be removed:")}
5955
6428
  const previewResult = await progress.execute(
5956
6429
  "Generating removal preview",
5957
6430
  async () => {
5958
- const stack = await pulumi8.automation.LocalWorkspace.selectStack(
6431
+ const stack = await pulumi9.automation.LocalWorkspace.selectStack(
5959
6432
  {
5960
6433
  stackName: metadata.services.email.pulumiStackName,
5961
6434
  projectName: "wraps-email",
@@ -5981,13 +6454,18 @@ ${pc9.bold("The following Wraps resources will be removed:")}
5981
6454
  costEstimate: "Monthly cost after removal: $0.00",
5982
6455
  commandName: "wraps email restore"
5983
6456
  });
5984
- clack8.outro(
5985
- pc9.green(
6457
+ clack9.outro(
6458
+ pc10.green(
5986
6459
  "Preview complete. Run without --preview to remove infrastructure."
5987
6460
  )
5988
6461
  );
6462
+ trackServiceRemoved("email", {
6463
+ preview: true,
6464
+ duration_ms: Date.now() - startTime
6465
+ });
5989
6466
  return;
5990
6467
  } catch (error) {
6468
+ trackError("PREVIEW_FAILED", "email:restore", { step: "preview" });
5991
6469
  throw new Error(`Preview failed: ${error.message}`);
5992
6470
  }
5993
6471
  }
@@ -5999,7 +6477,7 @@ ${pc9.bold("The following Wraps resources will be removed:")}
5999
6477
  if (!metadata.services.email?.pulumiStackName) {
6000
6478
  throw new Error("No Pulumi stack name found in metadata");
6001
6479
  }
6002
- const stack = await pulumi8.automation.LocalWorkspace.selectStack(
6480
+ const stack = await pulumi9.automation.LocalWorkspace.selectStack(
6003
6481
  {
6004
6482
  stackName: metadata.services.email.pulumiStackName,
6005
6483
  projectName: "wraps-email",
@@ -6022,6 +6500,7 @@ ${pc9.bold("The following Wraps resources will be removed:")}
6022
6500
  metadata.services.email.pulumiStackName
6023
6501
  );
6024
6502
  } catch (error) {
6503
+ trackError("DESTROY_FAILED", "email:restore", { step: "destroy" });
6025
6504
  throw new Error(`Failed to destroy Pulumi stack: ${error.message}`);
6026
6505
  }
6027
6506
  });
@@ -6030,29 +6509,127 @@ ${pc9.bold("The following Wraps resources will be removed:")}
6030
6509
  progress.info("Connection metadata deleted");
6031
6510
  console.log(
6032
6511
  `
6033
- ${pc9.green("\u2713")} ${pc9.bold("Infrastructure removed successfully!")}
6512
+ ${pc10.green("\u2713")} ${pc10.bold("Infrastructure removed successfully!")}
6034
6513
  `
6035
6514
  );
6036
6515
  console.log(
6037
- `${pc9.dim("All Wraps resources have been deleted from your AWS account.")}`
6516
+ `${pc10.dim("All Wraps resources have been deleted from your AWS account.")}`
6038
6517
  );
6039
- console.log(`${pc9.dim("Your original AWS resources remain unchanged.")}
6518
+ console.log(`${pc10.dim("Your original AWS resources remain unchanged.")}
6040
6519
  `);
6520
+ trackServiceRemoved("email", {
6521
+ reason: "user_initiated",
6522
+ duration_ms: Date.now() - startTime
6523
+ });
6524
+ }
6525
+
6526
+ // src/commands/email/status.ts
6527
+ init_esm_shims();
6528
+ init_events();
6529
+ init_aws();
6530
+ import * as clack10 from "@clack/prompts";
6531
+ import * as pulumi10 from "@pulumi/pulumi";
6532
+ import pc11 from "picocolors";
6533
+ async function emailStatus(_options) {
6534
+ const startTime = Date.now();
6535
+ const progress = new DeploymentProgress();
6536
+ clack10.intro(pc11.bold("Wraps Email Status"));
6537
+ const identity = await progress.execute(
6538
+ "Loading email infrastructure status",
6539
+ async () => validateAWSCredentials()
6540
+ );
6541
+ const region = await getAWSRegion();
6542
+ let stackOutputs = {};
6543
+ try {
6544
+ await ensurePulumiWorkDir();
6545
+ const stack = await pulumi10.automation.LocalWorkspace.selectStack({
6546
+ stackName: `wraps-${identity.accountId}-${region}`,
6547
+ workDir: getPulumiWorkDir()
6548
+ });
6549
+ stackOutputs = await stack.outputs();
6550
+ } catch (_error) {
6551
+ progress.stop();
6552
+ clack10.log.error("No email infrastructure found");
6553
+ console.log(
6554
+ `
6555
+ Run ${pc11.cyan("wraps email init")} to deploy email infrastructure.
6556
+ `
6557
+ );
6558
+ process.exit(1);
6559
+ }
6560
+ const domains = await listSESDomains(region);
6561
+ const { SESv2Client: SESv2Client5, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
6562
+ const sesv2Client = new SESv2Client5({ region });
6563
+ const domainsWithTokens = await Promise.all(
6564
+ domains.map(async (d) => {
6565
+ try {
6566
+ const identity2 = await sesv2Client.send(
6567
+ new GetEmailIdentityCommand4({ EmailIdentity: d.domain })
6568
+ );
6569
+ return {
6570
+ domain: d.domain,
6571
+ status: d.verified ? "verified" : "pending",
6572
+ dkimTokens: identity2.DkimAttributes?.Tokens || [],
6573
+ mailFromDomain: identity2.MailFromAttributes?.MailFromDomain,
6574
+ mailFromStatus: identity2.MailFromAttributes?.MailFromDomainStatus
6575
+ };
6576
+ } catch (_error) {
6577
+ return {
6578
+ domain: d.domain,
6579
+ status: d.verified ? "verified" : "pending",
6580
+ dkimTokens: void 0,
6581
+ mailFromDomain: void 0,
6582
+ mailFromStatus: void 0
6583
+ };
6584
+ }
6585
+ })
6586
+ );
6587
+ const integrationLevel = stackOutputs.configSetName ? "enhanced" : "dashboard-only";
6588
+ progress.stop();
6589
+ displayStatus({
6590
+ integrationLevel,
6591
+ region,
6592
+ domains: domainsWithTokens,
6593
+ resources: {
6594
+ roleArn: stackOutputs.roleArn?.value,
6595
+ configSetName: stackOutputs.configSetName?.value,
6596
+ tableName: stackOutputs.tableName?.value,
6597
+ lambdaFunctions: stackOutputs.lambdaFunctions?.value?.length || 0,
6598
+ snsTopics: integrationLevel === "enhanced" ? 1 : 0,
6599
+ archiveArn: stackOutputs.archiveArn?.value,
6600
+ archivingEnabled: stackOutputs.archivingEnabled?.value,
6601
+ archiveRetention: stackOutputs.archiveRetention?.value
6602
+ },
6603
+ tracking: stackOutputs.customTrackingDomain?.value ? {
6604
+ customTrackingDomain: stackOutputs.customTrackingDomain?.value,
6605
+ httpsEnabled: stackOutputs.httpsTrackingEnabled?.value,
6606
+ cloudFrontDomain: stackOutputs.cloudFrontDomain?.value
6607
+ } : void 0
6608
+ });
6609
+ trackCommand("email:status", {
6610
+ success: true,
6611
+ domain_count: domainsWithTokens.length,
6612
+ integration_level: integrationLevel,
6613
+ duration_ms: Date.now() - startTime
6614
+ });
6041
6615
  }
6042
6616
 
6043
6617
  // src/commands/email/upgrade.ts
6044
6618
  init_esm_shims();
6045
- import * as clack9 from "@clack/prompts";
6046
- import * as pulumi9 from "@pulumi/pulumi";
6047
- import pc10 from "picocolors";
6619
+ import * as clack11 from "@clack/prompts";
6620
+ import * as pulumi11 from "@pulumi/pulumi";
6621
+ import pc12 from "picocolors";
6622
+ init_events();
6048
6623
  init_costs();
6049
6624
  init_presets();
6050
6625
  init_aws();
6051
6626
  init_errors();
6052
6627
  init_prompts();
6053
6628
  async function upgrade(options) {
6054
- clack9.intro(
6055
- pc10.bold(
6629
+ const startTime = Date.now();
6630
+ let upgradeAction = "";
6631
+ clack11.intro(
6632
+ pc12.bold(
6056
6633
  options.preview ? "Wraps Upgrade Preview" : "Wraps Upgrade - Enhance Your Email Infrastructure"
6057
6634
  )
6058
6635
  );
@@ -6068,7 +6645,7 @@ async function upgrade(options) {
6068
6645
  "Validating AWS credentials",
6069
6646
  async () => validateAWSCredentials()
6070
6647
  );
6071
- progress.info(`Connected to AWS account: ${pc10.cyan(identity.accountId)}`);
6648
+ progress.info(`Connected to AWS account: ${pc12.cyan(identity.accountId)}`);
6072
6649
  let region = options.region;
6073
6650
  if (!region) {
6074
6651
  const defaultRegion = await getAWSRegion();
@@ -6076,55 +6653,55 @@ async function upgrade(options) {
6076
6653
  }
6077
6654
  const metadata = await loadConnectionMetadata(identity.accountId, region);
6078
6655
  if (!metadata) {
6079
- clack9.log.error(
6080
- `No Wraps connection found for account ${pc10.cyan(identity.accountId)} in region ${pc10.cyan(region)}`
6656
+ clack11.log.error(
6657
+ `No Wraps connection found for account ${pc12.cyan(identity.accountId)} in region ${pc12.cyan(region)}`
6081
6658
  );
6082
- clack9.log.info(
6083
- `Use ${pc10.cyan("wraps email init")} to create new infrastructure or ${pc10.cyan("wraps email connect")} to connect existing.`
6659
+ clack11.log.info(
6660
+ `Use ${pc12.cyan("wraps email init")} to create new infrastructure or ${pc12.cyan("wraps email connect")} to connect existing.`
6084
6661
  );
6085
6662
  process.exit(1);
6086
6663
  }
6087
6664
  progress.info(`Found existing connection created: ${metadata.timestamp}`);
6088
6665
  console.log(`
6089
- ${pc10.bold("Current Configuration:")}
6666
+ ${pc12.bold("Current Configuration:")}
6090
6667
  `);
6091
6668
  if (metadata.services.email?.preset) {
6092
- console.log(` Preset: ${pc10.cyan(metadata.services.email?.preset)}`);
6669
+ console.log(` Preset: ${pc12.cyan(metadata.services.email?.preset)}`);
6093
6670
  } else {
6094
- console.log(` Preset: ${pc10.cyan("custom")}`);
6671
+ console.log(` Preset: ${pc12.cyan("custom")}`);
6095
6672
  }
6096
6673
  const config2 = metadata.services.email?.config;
6097
6674
  if (!config2) {
6098
- clack9.log.error("No email configuration found in metadata");
6099
- clack9.log.info(
6100
- `Use ${pc10.cyan("wraps email init")} to create new infrastructure.`
6675
+ clack11.log.error("No email configuration found in metadata");
6676
+ clack11.log.info(
6677
+ `Use ${pc12.cyan("wraps email init")} to create new infrastructure.`
6101
6678
  );
6102
6679
  process.exit(1);
6103
6680
  }
6104
6681
  if (config2.domain) {
6105
- console.log(` Sending Domain: ${pc10.cyan(config2.domain)}`);
6682
+ console.log(` Sending Domain: ${pc12.cyan(config2.domain)}`);
6106
6683
  }
6107
6684
  if (config2.tracking?.enabled) {
6108
- console.log(` ${pc10.green("\u2713")} Open & Click Tracking`);
6685
+ console.log(` ${pc12.green("\u2713")} Open & Click Tracking`);
6109
6686
  if (config2.tracking.customRedirectDomain) {
6110
6687
  console.log(
6111
- ` ${pc10.dim("\u2514\u2500")} Custom domain: ${pc10.cyan(config2.tracking.customRedirectDomain)}`
6688
+ ` ${pc12.dim("\u2514\u2500")} Custom domain: ${pc12.cyan(config2.tracking.customRedirectDomain)}`
6112
6689
  );
6113
6690
  }
6114
6691
  }
6115
6692
  if (config2.suppressionList?.enabled) {
6116
- console.log(` ${pc10.green("\u2713")} Bounce/Complaint Suppression`);
6693
+ console.log(` ${pc12.green("\u2713")} Bounce/Complaint Suppression`);
6117
6694
  }
6118
6695
  if (config2.eventTracking?.enabled) {
6119
- console.log(` ${pc10.green("\u2713")} Event Tracking (EventBridge)`);
6696
+ console.log(` ${pc12.green("\u2713")} Event Tracking (EventBridge)`);
6120
6697
  if (config2.eventTracking.dynamoDBHistory) {
6121
6698
  console.log(
6122
- ` ${pc10.dim("\u2514\u2500")} Email History: ${pc10.cyan(config2.eventTracking.archiveRetention || "90days")}`
6699
+ ` ${pc12.dim("\u2514\u2500")} Email History: ${pc12.cyan(config2.eventTracking.archiveRetention || "90days")}`
6123
6700
  );
6124
6701
  }
6125
6702
  }
6126
6703
  if (config2.dedicatedIp) {
6127
- console.log(` ${pc10.green("\u2713")} Dedicated IP Address`);
6704
+ console.log(` ${pc12.green("\u2713")} Dedicated IP Address`);
6128
6705
  }
6129
6706
  if (config2.emailArchiving?.enabled) {
6130
6707
  const retentionLabel = {
@@ -6149,15 +6726,15 @@ ${pc10.bold("Current Configuration:")}
6149
6726
  indefinite: "indefinite",
6150
6727
  permanent: "permanent"
6151
6728
  }[config2.emailArchiving.retention] || "90 days";
6152
- console.log(` ${pc10.green("\u2713")} Email Archiving (${retentionLabel})`);
6729
+ console.log(` ${pc12.green("\u2713")} Email Archiving (${retentionLabel})`);
6153
6730
  }
6154
6731
  const currentCostData = calculateCosts(config2, 5e4);
6155
6732
  console.log(
6156
6733
  `
6157
- Estimated Cost: ${pc10.cyan(`~${formatCost(currentCostData.total.monthly)}/mo`)}`
6734
+ Estimated Cost: ${pc12.cyan(`~${formatCost(currentCostData.total.monthly)}/mo`)}`
6158
6735
  );
6159
6736
  console.log("");
6160
- const upgradeAction = await clack9.select({
6737
+ upgradeAction = await clack11.select({
6161
6738
  message: "What would you like to do?",
6162
6739
  options: [
6163
6740
  {
@@ -6197,8 +6774,8 @@ ${pc10.bold("Current Configuration:")}
6197
6774
  }
6198
6775
  ]
6199
6776
  });
6200
- if (clack9.isCancel(upgradeAction)) {
6201
- clack9.cancel("Upgrade cancelled.");
6777
+ if (clack11.isCancel(upgradeAction)) {
6778
+ clack11.cancel("Upgrade cancelled.");
6202
6779
  process.exit(0);
6203
6780
  }
6204
6781
  let updatedConfig = { ...config2 };
@@ -6216,15 +6793,15 @@ ${pc10.bold("Current Configuration:")}
6216
6793
  disabled: currentPresetIdx >= 0 && idx <= currentPresetIdx ? "Current or lower tier" : void 0
6217
6794
  })).filter((p) => !p.disabled);
6218
6795
  if (availablePresets.length === 0) {
6219
- clack9.log.warn("Already on highest preset (Enterprise)");
6796
+ clack11.log.warn("Already on highest preset (Enterprise)");
6220
6797
  process.exit(0);
6221
6798
  }
6222
- const selectedPreset = await clack9.select({
6799
+ const selectedPreset = await clack11.select({
6223
6800
  message: "Select new preset:",
6224
6801
  options: availablePresets
6225
6802
  });
6226
- if (clack9.isCancel(selectedPreset)) {
6227
- clack9.cancel("Upgrade cancelled.");
6803
+ if (clack11.isCancel(selectedPreset)) {
6804
+ clack11.cancel("Upgrade cancelled.");
6228
6805
  process.exit(0);
6229
6806
  }
6230
6807
  const presetConfig = getPreset(selectedPreset);
@@ -6234,7 +6811,7 @@ ${pc10.bold("Current Configuration:")}
6234
6811
  }
6235
6812
  case "archiving": {
6236
6813
  if (config2.emailArchiving?.enabled) {
6237
- const archivingAction = await clack9.select({
6814
+ const archivingAction = await clack11.select({
6238
6815
  message: "What would you like to do with email archiving?",
6239
6816
  options: [
6240
6817
  {
@@ -6249,17 +6826,17 @@ ${pc10.bold("Current Configuration:")}
6249
6826
  }
6250
6827
  ]
6251
6828
  });
6252
- if (clack9.isCancel(archivingAction)) {
6253
- clack9.cancel("Upgrade cancelled.");
6829
+ if (clack11.isCancel(archivingAction)) {
6830
+ clack11.cancel("Upgrade cancelled.");
6254
6831
  process.exit(0);
6255
6832
  }
6256
6833
  if (archivingAction === "disable") {
6257
- const confirmDisable = await clack9.confirm({
6834
+ const confirmDisable = await clack11.confirm({
6258
6835
  message: "Are you sure? Existing archived emails will remain, but new emails won't be archived.",
6259
6836
  initialValue: false
6260
6837
  });
6261
- if (clack9.isCancel(confirmDisable) || !confirmDisable) {
6262
- clack9.cancel("Archiving not disabled.");
6838
+ if (clack11.isCancel(confirmDisable) || !confirmDisable) {
6839
+ clack11.cancel("Archiving not disabled.");
6263
6840
  process.exit(0);
6264
6841
  }
6265
6842
  updatedConfig = {
@@ -6270,7 +6847,7 @@ ${pc10.bold("Current Configuration:")}
6270
6847
  }
6271
6848
  };
6272
6849
  } else {
6273
- const retention = await clack9.select({
6850
+ const retention = await clack11.select({
6274
6851
  message: "Email archive retention period:",
6275
6852
  options: [
6276
6853
  {
@@ -6306,8 +6883,8 @@ ${pc10.bold("Current Configuration:")}
6306
6883
  ],
6307
6884
  initialValue: config2.emailArchiving.retention
6308
6885
  });
6309
- if (clack9.isCancel(retention)) {
6310
- clack9.cancel("Upgrade cancelled.");
6886
+ if (clack11.isCancel(retention)) {
6887
+ clack11.cancel("Upgrade cancelled.");
6311
6888
  process.exit(0);
6312
6889
  }
6313
6890
  updatedConfig = {
@@ -6319,19 +6896,19 @@ ${pc10.bold("Current Configuration:")}
6319
6896
  };
6320
6897
  }
6321
6898
  } else {
6322
- const enableArchiving = await clack9.confirm({
6899
+ const enableArchiving = await clack11.confirm({
6323
6900
  message: "Enable email archiving? (Store full email content with HTML for viewing)",
6324
6901
  initialValue: true
6325
6902
  });
6326
- if (clack9.isCancel(enableArchiving)) {
6327
- clack9.cancel("Upgrade cancelled.");
6903
+ if (clack11.isCancel(enableArchiving)) {
6904
+ clack11.cancel("Upgrade cancelled.");
6328
6905
  process.exit(0);
6329
6906
  }
6330
6907
  if (!enableArchiving) {
6331
- clack9.log.info("Email archiving not enabled.");
6908
+ clack11.log.info("Email archiving not enabled.");
6332
6909
  process.exit(0);
6333
6910
  }
6334
- const retention = await clack9.select({
6911
+ const retention = await clack11.select({
6335
6912
  message: "Email archive retention period:",
6336
6913
  options: [
6337
6914
  {
@@ -6367,17 +6944,17 @@ ${pc10.bold("Current Configuration:")}
6367
6944
  ],
6368
6945
  initialValue: "90days"
6369
6946
  });
6370
- if (clack9.isCancel(retention)) {
6371
- clack9.cancel("Upgrade cancelled.");
6947
+ if (clack11.isCancel(retention)) {
6948
+ clack11.cancel("Upgrade cancelled.");
6372
6949
  process.exit(0);
6373
6950
  }
6374
- clack9.log.info(
6375
- pc10.dim(
6951
+ clack11.log.info(
6952
+ pc12.dim(
6376
6953
  "Archiving stores full RFC 822 emails with HTML, attachments, and headers"
6377
6954
  )
6378
6955
  );
6379
- clack9.log.info(
6380
- pc10.dim(
6956
+ clack11.log.info(
6957
+ pc12.dim(
6381
6958
  "Cost: $2/GB ingestion + $0.19/GB/month storage (~50KB per email)"
6382
6959
  )
6383
6960
  );
@@ -6394,11 +6971,11 @@ ${pc10.bold("Current Configuration:")}
6394
6971
  }
6395
6972
  case "tracking-domain": {
6396
6973
  if (!config2.domain) {
6397
- clack9.log.error(
6974
+ clack11.log.error(
6398
6975
  "No sending domain configured. You must configure a sending domain before adding a custom tracking domain."
6399
6976
  );
6400
- clack9.log.info(
6401
- `Use ${pc10.cyan("wraps email init")} to set up a sending domain first.`
6977
+ clack11.log.info(
6978
+ `Use ${pc12.cyan("wraps email init")} to set up a sending domain first.`
6402
6979
  );
6403
6980
  process.exit(1);
6404
6981
  }
@@ -6409,21 +6986,21 @@ ${pc10.bold("Current Configuration:")}
6409
6986
  );
6410
6987
  const sendingDomain = domains.find((d) => d.domain === config2.domain);
6411
6988
  if (!sendingDomain?.verified) {
6412
- clack9.log.error(
6413
- `Sending domain ${pc10.cyan(config2.domain)} is not verified.`
6989
+ clack11.log.error(
6990
+ `Sending domain ${pc12.cyan(config2.domain)} is not verified.`
6414
6991
  );
6415
- clack9.log.info(
6992
+ clack11.log.info(
6416
6993
  "You must verify your sending domain before adding a custom tracking domain."
6417
6994
  );
6418
- clack9.log.info(
6419
- `Use ${pc10.cyan("wraps verify")} to check DNS records and complete verification.`
6995
+ clack11.log.info(
6996
+ `Use ${pc12.cyan("wraps email verify")} to check DNS records and complete verification.`
6420
6997
  );
6421
6998
  process.exit(1);
6422
6999
  }
6423
7000
  progress.info(
6424
- `Sending domain ${pc10.cyan(config2.domain)} is verified ${pc10.green("\u2713")}`
7001
+ `Sending domain ${pc12.cyan(config2.domain)} is verified ${pc12.green("\u2713")}`
6425
7002
  );
6426
- const trackingDomain = await clack9.text({
7003
+ const trackingDomain = await clack11.text({
6427
7004
  message: "Custom tracking redirect domain:",
6428
7005
  placeholder: "track.yourdomain.com",
6429
7006
  initialValue: config2.tracking?.customRedirectDomain || "",
@@ -6433,62 +7010,62 @@ ${pc10.bold("Current Configuration:")}
6433
7010
  }
6434
7011
  }
6435
7012
  });
6436
- if (clack9.isCancel(trackingDomain)) {
6437
- clack9.cancel("Upgrade cancelled.");
7013
+ if (clack11.isCancel(trackingDomain)) {
7014
+ clack11.cancel("Upgrade cancelled.");
6438
7015
  process.exit(0);
6439
7016
  }
6440
- const enableHttps = await clack9.confirm({
7017
+ const enableHttps = await clack11.confirm({
6441
7018
  message: "Enable HTTPS tracking with CloudFront + SSL certificate?",
6442
7019
  initialValue: true
6443
7020
  });
6444
- if (clack9.isCancel(enableHttps)) {
6445
- clack9.cancel("Upgrade cancelled.");
7021
+ if (clack11.isCancel(enableHttps)) {
7022
+ clack11.cancel("Upgrade cancelled.");
6446
7023
  process.exit(0);
6447
7024
  }
6448
7025
  if (enableHttps) {
6449
- clack9.log.info(
6450
- pc10.dim(
7026
+ clack11.log.info(
7027
+ pc12.dim(
6451
7028
  "HTTPS tracking creates a CloudFront distribution with an SSL certificate."
6452
7029
  )
6453
7030
  );
6454
- clack9.log.info(
6455
- pc10.dim(
7031
+ clack11.log.info(
7032
+ pc12.dim(
6456
7033
  "This ensures all tracking links use secure HTTPS connections."
6457
7034
  )
6458
7035
  );
6459
- const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
7036
+ const { findHostedZone: findHostedZone3 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
6460
7037
  const hostedZone = await progress.execute(
6461
7038
  "Checking for Route53 hosted zone",
6462
- async () => await findHostedZone2(trackingDomain || config2.domain, region)
7039
+ async () => await findHostedZone3(trackingDomain || config2.domain, region)
6463
7040
  );
6464
7041
  if (hostedZone) {
6465
7042
  progress.info(
6466
- `Found Route53 hosted zone: ${pc10.cyan(hostedZone.name)} ${pc10.green("\u2713")}`
7043
+ `Found Route53 hosted zone: ${pc12.cyan(hostedZone.name)} ${pc12.green("\u2713")}`
6467
7044
  );
6468
- clack9.log.info(
6469
- pc10.dim(
7045
+ clack11.log.info(
7046
+ pc12.dim(
6470
7047
  "DNS records (SSL certificate validation + CloudFront) will be created automatically."
6471
7048
  )
6472
7049
  );
6473
7050
  } else {
6474
- clack9.log.warn(
6475
- `No Route53 hosted zone found for ${pc10.cyan(trackingDomain || config2.domain)}`
7051
+ clack11.log.warn(
7052
+ `No Route53 hosted zone found for ${pc12.cyan(trackingDomain || config2.domain)}`
6476
7053
  );
6477
- clack9.log.info(
6478
- pc10.dim(
7054
+ clack11.log.info(
7055
+ pc12.dim(
6479
7056
  "You'll need to manually create DNS records for SSL certificate validation and CloudFront."
6480
7057
  )
6481
7058
  );
6482
- clack9.log.info(
6483
- pc10.dim("DNS record details will be shown after deployment.")
7059
+ clack11.log.info(
7060
+ pc12.dim("DNS record details will be shown after deployment.")
6484
7061
  );
6485
7062
  }
6486
- const confirmHttps = await clack9.confirm({
7063
+ const confirmHttps = await clack11.confirm({
6487
7064
  message: hostedZone ? "Proceed with automatic HTTPS setup?" : "Proceed with manual HTTPS setup (requires DNS configuration)?",
6488
7065
  initialValue: true
6489
7066
  });
6490
- if (clack9.isCancel(confirmHttps) || !confirmHttps) {
6491
- clack9.log.info("HTTPS tracking not enabled. Using HTTP tracking.");
7067
+ if (clack11.isCancel(confirmHttps) || !confirmHttps) {
7068
+ clack11.log.info("HTTPS tracking not enabled. Using HTTP tracking.");
6492
7069
  updatedConfig = {
6493
7070
  ...config2,
6494
7071
  tracking: {
@@ -6510,8 +7087,8 @@ ${pc10.bold("Current Configuration:")}
6510
7087
  };
6511
7088
  }
6512
7089
  } else {
6513
- clack9.log.info(
6514
- pc10.dim(
7090
+ clack11.log.info(
7091
+ pc12.dim(
6515
7092
  "Using HTTP tracking (standard). Links will use http:// protocol."
6516
7093
  )
6517
7094
  );
@@ -6529,7 +7106,7 @@ ${pc10.bold("Current Configuration:")}
6529
7106
  break;
6530
7107
  }
6531
7108
  case "retention": {
6532
- const retention = await clack9.select({
7109
+ const retention = await clack11.select({
6533
7110
  message: "Email history retention period (event data in DynamoDB):",
6534
7111
  options: [
6535
7112
  { value: "7days", label: "7 days", hint: "Minimal storage cost" },
@@ -6553,17 +7130,17 @@ ${pc10.bold("Current Configuration:")}
6553
7130
  ],
6554
7131
  initialValue: config2.eventTracking?.archiveRetention || "90days"
6555
7132
  });
6556
- if (clack9.isCancel(retention)) {
6557
- clack9.cancel("Upgrade cancelled.");
7133
+ if (clack11.isCancel(retention)) {
7134
+ clack11.cancel("Upgrade cancelled.");
6558
7135
  process.exit(0);
6559
7136
  }
6560
- clack9.log.info(
6561
- pc10.dim(
7137
+ clack11.log.info(
7138
+ pc12.dim(
6562
7139
  "Note: This is for event data (sent, delivered, opened, etc.) stored in DynamoDB."
6563
7140
  )
6564
7141
  );
6565
- clack9.log.info(
6566
- pc10.dim(
7142
+ clack11.log.info(
7143
+ pc12.dim(
6567
7144
  "For full email content storage, use 'Enable email archiving' option."
6568
7145
  )
6569
7146
  );
@@ -6580,7 +7157,7 @@ ${pc10.bold("Current Configuration:")}
6580
7157
  break;
6581
7158
  }
6582
7159
  case "events": {
6583
- const selectedEvents = await clack9.multiselect({
7160
+ const selectedEvents = await clack11.multiselect({
6584
7161
  message: "Select SES event types to track:",
6585
7162
  options: [
6586
7163
  { value: "SEND", label: "Send", hint: "Email sent to SES" },
@@ -6624,8 +7201,8 @@ ${pc10.bold("Current Configuration:")}
6624
7201
  ],
6625
7202
  required: true
6626
7203
  });
6627
- if (clack9.isCancel(selectedEvents)) {
6628
- clack9.cancel("Upgrade cancelled.");
7204
+ if (clack11.isCancel(selectedEvents)) {
7205
+ clack11.cancel("Upgrade cancelled.");
6629
7206
  process.exit(0);
6630
7207
  }
6631
7208
  updatedConfig = {
@@ -6640,16 +7217,16 @@ ${pc10.bold("Current Configuration:")}
6640
7217
  break;
6641
7218
  }
6642
7219
  case "dedicated-ip": {
6643
- const confirmed = await clack9.confirm({
7220
+ const confirmed = await clack11.confirm({
6644
7221
  message: "Enable dedicated IP? (Requires 100k+ emails/day, adds ~$50-100/mo)",
6645
7222
  initialValue: false
6646
7223
  });
6647
- if (clack9.isCancel(confirmed)) {
6648
- clack9.cancel("Upgrade cancelled.");
7224
+ if (clack11.isCancel(confirmed)) {
7225
+ clack11.cancel("Upgrade cancelled.");
6649
7226
  process.exit(0);
6650
7227
  }
6651
7228
  if (!confirmed) {
6652
- clack9.log.info("Dedicated IP not enabled.");
7229
+ clack11.log.info("Dedicated IP not enabled.");
6653
7230
  process.exit(0);
6654
7231
  }
6655
7232
  updatedConfig = {
@@ -6670,28 +7247,28 @@ ${pc10.bold("Current Configuration:")}
6670
7247
  const newCostData = calculateCosts(updatedConfig, 5e4);
6671
7248
  const costDiff = newCostData.total.monthly - currentCostData.total.monthly;
6672
7249
  console.log(`
6673
- ${pc10.bold("Cost Impact:")}`);
7250
+ ${pc12.bold("Cost Impact:")}`);
6674
7251
  console.log(
6675
- ` Current: ${pc10.cyan(`${formatCost(currentCostData.total.monthly)}/mo`)}`
7252
+ ` Current: ${pc12.cyan(`${formatCost(currentCostData.total.monthly)}/mo`)}`
6676
7253
  );
6677
7254
  console.log(
6678
- ` New: ${pc10.cyan(`${formatCost(newCostData.total.monthly)}/mo`)}`
7255
+ ` New: ${pc12.cyan(`${formatCost(newCostData.total.monthly)}/mo`)}`
6679
7256
  );
6680
7257
  if (costDiff > 0) {
6681
- console.log(` Change: ${pc10.yellow(`+${formatCost(costDiff)}/mo`)}`);
7258
+ console.log(` Change: ${pc12.yellow(`+${formatCost(costDiff)}/mo`)}`);
6682
7259
  } else if (costDiff < 0) {
6683
7260
  console.log(
6684
- ` Change: ${pc10.green(`${formatCost(Math.abs(costDiff))}/mo`)}`
7261
+ ` Change: ${pc12.green(`${formatCost(Math.abs(costDiff))}/mo`)}`
6685
7262
  );
6686
7263
  }
6687
7264
  console.log("");
6688
7265
  if (!(options.yes || options.preview)) {
6689
- const confirmed = await clack9.confirm({
7266
+ const confirmed = await clack11.confirm({
6690
7267
  message: "Proceed with upgrade?",
6691
7268
  initialValue: true
6692
7269
  });
6693
- if (clack9.isCancel(confirmed) || !confirmed) {
6694
- clack9.cancel("Upgrade cancelled.");
7270
+ if (clack11.isCancel(confirmed) || !confirmed) {
7271
+ clack11.cancel("Upgrade cancelled.");
6695
7272
  process.exit(0);
6696
7273
  }
6697
7274
  }
@@ -6713,7 +7290,7 @@ ${pc10.bold("Cost Impact:")}`);
6713
7290
  "Generating upgrade preview",
6714
7291
  async () => {
6715
7292
  await ensurePulumiWorkDir();
6716
- const stack = await pulumi9.automation.LocalWorkspace.createOrSelectStack(
7293
+ const stack = await pulumi11.automation.LocalWorkspace.createOrSelectStack(
6717
7294
  {
6718
7295
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
6719
7296
  projectName: "wraps-email",
@@ -6763,11 +7340,19 @@ ${pc10.bold("Cost Impact:")}`);
6763
7340
  costEstimate: costComparison,
6764
7341
  commandName: "wraps email upgrade"
6765
7342
  });
6766
- clack9.outro(
6767
- pc10.green("Preview complete. Run without --preview to upgrade.")
7343
+ clack11.outro(
7344
+ pc12.green("Preview complete. Run without --preview to upgrade.")
6768
7345
  );
7346
+ trackServiceUpgrade("email", {
7347
+ from_preset: metadata.services.email?.preset,
7348
+ to_preset: newPreset,
7349
+ preview: true,
7350
+ action: typeof upgradeAction === "string" ? upgradeAction : void 0,
7351
+ duration_ms: Date.now() - startTime
7352
+ });
6769
7353
  return;
6770
7354
  } catch (error) {
7355
+ trackError("PREVIEW_FAILED", "email:upgrade", { step: "preview" });
6771
7356
  if (error.message?.includes("stack is currently locked")) {
6772
7357
  throw errors.stackLocked();
6773
7358
  }
@@ -6780,7 +7365,7 @@ ${pc10.bold("Cost Impact:")}`);
6780
7365
  "Updating Wraps infrastructure (this may take 2-3 minutes)",
6781
7366
  async () => {
6782
7367
  await ensurePulumiWorkDir();
6783
- const stack = await pulumi9.automation.LocalWorkspace.createOrSelectStack(
7368
+ const stack = await pulumi11.automation.LocalWorkspace.createOrSelectStack(
6784
7369
  {
6785
7370
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
6786
7371
  projectName: "wraps-email",
@@ -6841,14 +7426,22 @@ ${pc10.bold("Cost Impact:")}`);
6841
7426
  }
6842
7427
  );
6843
7428
  } catch (error) {
7429
+ trackServiceUpgrade("email", {
7430
+ from_preset: metadata.services.email?.preset,
7431
+ to_preset: newPreset,
7432
+ action: typeof upgradeAction === "string" ? upgradeAction : void 0,
7433
+ duration_ms: Date.now() - startTime
7434
+ });
6844
7435
  if (error.message?.includes("stack is currently locked")) {
7436
+ trackError("STACK_LOCKED", "email:upgrade", { step: "deploy" });
6845
7437
  throw errors.stackLocked();
6846
7438
  }
7439
+ trackError("UPGRADE_FAILED", "email:upgrade", { step: "deploy" });
6847
7440
  throw new Error(`Pulumi upgrade failed: ${error.message}`);
6848
7441
  }
6849
7442
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
6850
- const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
6851
- const hostedZone = await findHostedZone2(outputs.domain, region);
7443
+ const { findHostedZone: findHostedZone3, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
7444
+ const hostedZone = await findHostedZone3(outputs.domain, region);
6852
7445
  if (hostedZone) {
6853
7446
  try {
6854
7447
  progress.start("Creating DNS records in Route53");
@@ -6913,21 +7506,21 @@ ${pc10.bold("Cost Impact:")}`);
6913
7506
  httpsTrackingEnabled: outputs.httpsTrackingEnabled
6914
7507
  });
6915
7508
  console.log(`
6916
- ${pc10.green("\u2713")} ${pc10.bold("Upgrade complete!")}
7509
+ ${pc12.green("\u2713")} ${pc12.bold("Upgrade complete!")}
6917
7510
  `);
6918
7511
  if (upgradeAction === "preset" && newPreset) {
6919
7512
  console.log(
6920
- `Upgraded to ${pc10.cyan(newPreset)} preset (${pc10.green(`${formatCost(newCostData.total.monthly)}/mo`)})
7513
+ `Upgraded to ${pc12.cyan(newPreset)} preset (${pc12.green(`${formatCost(newCostData.total.monthly)}/mo`)})
6921
7514
  `
6922
7515
  );
6923
7516
  } else {
6924
7517
  console.log(
6925
- `Updated configuration (${pc10.green(`${formatCost(newCostData.total.monthly)}/mo`)})
7518
+ `Updated configuration (${pc12.green(`${formatCost(newCostData.total.monthly)}/mo`)})
6926
7519
  `
6927
7520
  );
6928
7521
  }
6929
7522
  if (needsCertificateValidation) {
6930
- console.log(pc10.bold("\u26A0\uFE0F HTTPS Tracking - Next Steps:\n"));
7523
+ console.log(pc12.bold("\u26A0\uFE0F HTTPS Tracking - Next Steps:\n"));
6931
7524
  console.log(
6932
7525
  " 1. Add the SSL certificate validation DNS record shown above to your DNS provider"
6933
7526
  );
@@ -6935,28 +7528,46 @@ ${pc10.green("\u2713")} ${pc10.bold("Upgrade complete!")}
6935
7528
  " 2. Wait for DNS propagation and certificate validation (5-30 minutes)"
6936
7529
  );
6937
7530
  console.log(
6938
- ` 3. Run ${pc10.cyan("wraps email upgrade")} again to complete CloudFront setup
7531
+ ` 3. Run ${pc12.cyan("wraps email upgrade")} again to complete CloudFront setup
6939
7532
  `
6940
7533
  );
6941
7534
  console.log(
6942
- pc10.dim(
7535
+ pc12.dim(
6943
7536
  " Note: CloudFront distribution will be created once the certificate is validated.\n"
6944
7537
  )
6945
7538
  );
6946
7539
  } else if (outputs.httpsTrackingEnabled && outputs.cloudFrontDomain) {
6947
7540
  console.log(
6948
- pc10.green("\u2713") + " " + pc10.bold("HTTPS tracking is fully configured and ready to use!\n")
7541
+ pc12.green("\u2713") + " " + pc12.bold("HTTPS tracking is fully configured and ready to use!\n")
6949
7542
  );
6950
7543
  }
7544
+ const enabledFeatures = [];
7545
+ if (updatedConfig.tracking?.enabled) enabledFeatures.push("tracking");
7546
+ if (updatedConfig.suppressionList?.enabled)
7547
+ enabledFeatures.push("suppression_list");
7548
+ if (updatedConfig.eventTracking?.enabled)
7549
+ enabledFeatures.push("event_tracking");
7550
+ if (updatedConfig.eventTracking?.dynamoDBHistory)
7551
+ enabledFeatures.push("dynamodb_history");
7552
+ if (updatedConfig.dedicatedIp) enabledFeatures.push("dedicated_ip");
7553
+ if (updatedConfig.emailArchiving?.enabled)
7554
+ enabledFeatures.push("email_archiving");
7555
+ trackServiceUpgrade("email", {
7556
+ from_preset: metadata.services.email?.preset,
7557
+ to_preset: newPreset,
7558
+ added_features: enabledFeatures,
7559
+ action: typeof upgradeAction === "string" ? upgradeAction : void 0,
7560
+ duration_ms: Date.now() - startTime
7561
+ });
6951
7562
  }
6952
7563
 
6953
7564
  // src/commands/shared/dashboard.ts
6954
7565
  init_esm_shims();
6955
- import * as clack10 from "@clack/prompts";
6956
- import * as pulumi10 from "@pulumi/pulumi";
7566
+ import * as clack12 from "@clack/prompts";
7567
+ import * as pulumi12 from "@pulumi/pulumi";
6957
7568
  import getPort from "get-port";
6958
7569
  import open from "open";
6959
- import pc11 from "picocolors";
7570
+ import pc13 from "picocolors";
6960
7571
 
6961
7572
  // src/console/server.ts
6962
7573
  init_esm_shims();
@@ -8178,9 +8789,10 @@ async function startConsoleServer(config2) {
8178
8789
  }
8179
8790
 
8180
8791
  // src/commands/shared/dashboard.ts
8792
+ init_events();
8181
8793
  init_aws();
8182
8794
  async function dashboard(options) {
8183
- clack10.intro(pc11.bold("Wraps Dashboard"));
8795
+ clack12.intro(pc13.bold("Wraps Dashboard"));
8184
8796
  const progress = new DeploymentProgress();
8185
8797
  const identity = await progress.execute(
8186
8798
  "Validating AWS credentials",
@@ -8190,16 +8802,16 @@ async function dashboard(options) {
8190
8802
  let stackOutputs = {};
8191
8803
  try {
8192
8804
  await ensurePulumiWorkDir();
8193
- const stack = await pulumi10.automation.LocalWorkspace.selectStack({
8805
+ const stack = await pulumi12.automation.LocalWorkspace.selectStack({
8194
8806
  stackName: `wraps-${identity.accountId}-${region}`,
8195
8807
  workDir: getPulumiWorkDir()
8196
8808
  });
8197
8809
  stackOutputs = await stack.outputs();
8198
8810
  } catch (_error) {
8199
8811
  progress.stop();
8200
- clack10.log.error("No Wraps infrastructure found");
8812
+ clack12.log.error("No Wraps infrastructure found");
8201
8813
  console.log(
8202
- `\\nRun ${pc11.cyan("wraps email init")} to deploy infrastructure first.\\n`
8814
+ `\\nRun ${pc13.cyan("wraps email init")} to deploy infrastructure first.\\n`
8203
8815
  );
8204
8816
  process.exit(1);
8205
8817
  }
@@ -8208,9 +8820,9 @@ async function dashboard(options) {
8208
8820
  const archivingEnabled = stackOutputs.archivingEnabled?.value ?? false;
8209
8821
  const port = options.port || await getPort({ port: [5555, 5556, 5557, 5558, 5559] });
8210
8822
  progress.stop();
8211
- clack10.log.success("Starting dashboard server...");
8823
+ clack12.log.success("Starting dashboard server...");
8212
8824
  console.log(
8213
- `${pc11.dim("Using current AWS credentials (no role assumption)")}\\n`
8825
+ `${pc13.dim("Using current AWS credentials (no role assumption)")}\\n`
8214
8826
  );
8215
8827
  const { url } = await startConsoleServer({
8216
8828
  port,
@@ -8223,11 +8835,16 @@ async function dashboard(options) {
8223
8835
  archiveArn,
8224
8836
  archivingEnabled
8225
8837
  });
8226
- console.log(`\\n${pc11.bold("Dashboard:")} ${pc11.cyan(url)}`);
8227
- console.log(`${pc11.dim("Press Ctrl+C to stop")}\\n`);
8838
+ console.log(`\\n${pc13.bold("Dashboard:")} ${pc13.cyan(url)}`);
8839
+ console.log(`${pc13.dim("Press Ctrl+C to stop")}\\n`);
8228
8840
  if (!options.noOpen) {
8229
8841
  await open(url);
8230
8842
  }
8843
+ trackCommand("console", {
8844
+ success: true,
8845
+ port,
8846
+ no_open: options.noOpen ?? false
8847
+ });
8231
8848
  await new Promise(() => {
8232
8849
  });
8233
8850
  }
@@ -8235,246 +8852,203 @@ async function dashboard(options) {
8235
8852
  // src/commands/shared/destroy.ts
8236
8853
  init_esm_shims();
8237
8854
  init_aws();
8238
- import * as clack11 from "@clack/prompts";
8239
- import * as pulumi11 from "@pulumi/pulumi";
8240
- import pc12 from "picocolors";
8855
+ import * as clack13 from "@clack/prompts";
8856
+ import pc14 from "picocolors";
8241
8857
  async function destroy(options) {
8242
- clack11.intro(
8243
- pc12.bold(
8244
- options.preview ? "Wraps Destruction Preview" : "Wraps Email Infrastructure Teardown"
8245
- )
8246
- );
8247
- const progress = new DeploymentProgress();
8248
- const identity = await progress.execute(
8249
- "Validating AWS credentials",
8250
- async () => validateAWSCredentials()
8251
- );
8858
+ clack13.intro(pc14.bold("Wraps Infrastructure Teardown"));
8859
+ const spinner3 = clack13.spinner();
8860
+ spinner3.start("Validating AWS credentials");
8861
+ let identity;
8862
+ try {
8863
+ identity = await validateAWSCredentials();
8864
+ spinner3.stop("AWS credentials validated");
8865
+ } catch (error) {
8866
+ spinner3.stop("AWS credentials validation failed");
8867
+ throw error;
8868
+ }
8252
8869
  const region = await getAWSRegion();
8253
- if (!(options.force || options.preview)) {
8254
- const confirmed = await clack11.confirm({
8255
- message: pc12.red(
8256
- "Are you sure you want to destroy all Wraps infrastructure?"
8257
- ),
8258
- initialValue: false
8259
- });
8260
- if (clack11.isCancel(confirmed) || !confirmed) {
8261
- clack11.cancel("Destruction cancelled.");
8262
- process.exit(0);
8263
- }
8870
+ const metadata = await loadConnectionMetadata(identity.accountId, region);
8871
+ const deployedServices = [];
8872
+ if (metadata?.services?.email) {
8873
+ deployedServices.push("email");
8264
8874
  }
8265
- if (options.preview) {
8266
- try {
8267
- const previewResult = await progress.execute(
8268
- "Generating destruction preview",
8269
- async () => {
8270
- await ensurePulumiWorkDir();
8271
- const stackName = `wraps-${identity.accountId}-${region}`;
8272
- let stack;
8273
- try {
8274
- stack = await pulumi11.automation.LocalWorkspace.selectStack({
8275
- stackName,
8276
- workDir: getPulumiWorkDir()
8277
- });
8278
- } catch (_error) {
8279
- throw new Error("No Wraps infrastructure found to preview");
8280
- }
8281
- const result = await stack.preview({ diff: true });
8282
- return result;
8283
- }
8284
- );
8285
- displayPreview({
8286
- changeSummary: previewResult.changeSummary,
8287
- costEstimate: "Monthly cost after destruction: $0.00",
8288
- commandName: "wraps destroy"
8289
- });
8290
- clack11.outro(
8291
- pc12.green("Preview complete. Run without --preview to destroy.")
8292
- );
8875
+ if (deployedServices.length === 0) {
8876
+ clack13.log.warn("No Wraps services found in this region");
8877
+ console.log(
8878
+ `
8879
+ Run ${pc14.cyan("wraps email init")} to deploy infrastructure.
8880
+ `
8881
+ );
8882
+ process.exit(0);
8883
+ }
8884
+ if (deployedServices.length === 1) {
8885
+ const service = deployedServices[0];
8886
+ clack13.log.info(`Found ${pc14.cyan(service)} service deployed`);
8887
+ if (service === "email") {
8888
+ await emailDestroy(options);
8293
8889
  return;
8294
- } catch (error) {
8295
- progress.stop();
8296
- if (error.message.includes("No Wraps infrastructure found")) {
8297
- clack11.log.warn("No Wraps infrastructure found to preview");
8298
- process.exit(0);
8299
- }
8300
- throw new Error(`Preview failed: ${error.message}`);
8301
8890
  }
8302
8891
  }
8303
- try {
8304
- await progress.execute(
8305
- "Destroying infrastructure (this may take 2-3 minutes)",
8306
- async () => {
8307
- await ensurePulumiWorkDir();
8308
- const stackName = `wraps-${identity.accountId}-${region}`;
8309
- let stack;
8310
- try {
8311
- stack = await pulumi11.automation.LocalWorkspace.selectStack({
8312
- stackName,
8313
- workDir: getPulumiWorkDir()
8314
- });
8315
- } catch (_error) {
8316
- throw new Error("No Wraps infrastructure found to destroy");
8317
- }
8318
- await stack.destroy({ onOutput: () => {
8319
- } });
8320
- await stack.workspace.removeStack(stackName);
8892
+ const serviceToDestroy = await clack13.select({
8893
+ message: "Which service would you like to destroy?",
8894
+ options: [
8895
+ ...deployedServices.map((s) => ({
8896
+ value: s,
8897
+ label: s.charAt(0).toUpperCase() + s.slice(1),
8898
+ hint: s === "email" ? "AWS SES email infrastructure" : void 0
8899
+ })),
8900
+ {
8901
+ value: "all",
8902
+ label: "All services",
8903
+ hint: "Destroy all Wraps infrastructure"
8321
8904
  }
8322
- );
8323
- } catch (error) {
8324
- progress.stop();
8325
- if (error.message.includes("No Wraps infrastructure found")) {
8326
- clack11.log.warn("No Wraps infrastructure found");
8327
- await deleteConnectionMetadata(identity.accountId, region);
8328
- process.exit(0);
8905
+ ]
8906
+ });
8907
+ if (clack13.isCancel(serviceToDestroy)) {
8908
+ clack13.cancel("Operation cancelled.");
8909
+ process.exit(0);
8910
+ }
8911
+ if (serviceToDestroy === "email" || serviceToDestroy === "all") {
8912
+ if (deployedServices.includes("email")) {
8913
+ await emailDestroy(options);
8329
8914
  }
8330
- clack11.log.error("Infrastructure destruction failed");
8331
- throw error;
8332
8915
  }
8333
- await deleteConnectionMetadata(identity.accountId, region);
8334
- progress.stop();
8335
- clack11.outro(pc12.green("All Wraps infrastructure has been removed"));
8336
- console.log(
8337
- `
8338
- Run ${pc12.cyan("wraps email init")} to deploy infrastructure again.
8339
- `
8340
- );
8916
+ if (serviceToDestroy === "all") {
8917
+ clack13.outro(pc14.green("All Wraps infrastructure has been removed"));
8918
+ }
8341
8919
  }
8342
8920
 
8343
8921
  // src/commands/shared/status.ts
8344
8922
  init_esm_shims();
8923
+ init_events();
8345
8924
  init_aws();
8346
- import * as clack12 from "@clack/prompts";
8347
- import * as pulumi12 from "@pulumi/pulumi";
8348
- import pc13 from "picocolors";
8925
+ import * as clack14 from "@clack/prompts";
8926
+ import * as pulumi13 from "@pulumi/pulumi";
8927
+ import pc15 from "picocolors";
8349
8928
  async function status(_options) {
8929
+ const startTime = Date.now();
8350
8930
  const progress = new DeploymentProgress();
8931
+ clack14.intro(pc15.bold("Wraps Infrastructure Status"));
8351
8932
  const identity = await progress.execute(
8352
8933
  "Loading infrastructure status",
8353
8934
  async () => validateAWSCredentials()
8354
8935
  );
8936
+ progress.info(`AWS Account: ${pc15.cyan(identity.accountId)}`);
8355
8937
  const region = await getAWSRegion();
8356
- let stackOutputs = {};
8938
+ progress.info(`Region: ${pc15.cyan(region)}`);
8939
+ const services = [];
8357
8940
  try {
8358
8941
  await ensurePulumiWorkDir();
8359
- const stack = await pulumi12.automation.LocalWorkspace.selectStack({
8942
+ const stack = await pulumi13.automation.LocalWorkspace.selectStack({
8360
8943
  stackName: `wraps-${identity.accountId}-${region}`,
8361
8944
  workDir: getPulumiWorkDir()
8362
8945
  });
8363
- stackOutputs = await stack.outputs();
8946
+ const outputs = await stack.outputs();
8947
+ if (outputs.roleArn?.value) {
8948
+ const domainCount = outputs.domains?.value?.length || 0;
8949
+ services.push({
8950
+ name: "Email",
8951
+ status: "deployed",
8952
+ details: domainCount > 0 ? `${domainCount} domain(s)` : void 0
8953
+ });
8954
+ } else {
8955
+ services.push({ name: "Email", status: "not_deployed" });
8956
+ }
8364
8957
  } catch (_error) {
8365
- progress.stop();
8366
- clack12.log.error("No Wraps infrastructure found");
8958
+ services.push({ name: "Email", status: "not_deployed" });
8959
+ }
8960
+ progress.stop();
8961
+ console.log();
8962
+ clack14.note(
8963
+ services.map((s) => {
8964
+ if (s.status === "deployed") {
8965
+ const details = s.details ? pc15.dim(` (${s.details})`) : "";
8966
+ return ` ${pc15.green("\u2713")} ${s.name}${details}`;
8967
+ }
8968
+ return ` ${pc15.dim("\u25CB")} ${s.name} ${pc15.dim("(not deployed)")}`;
8969
+ }).join("\n"),
8970
+ "Services"
8971
+ );
8972
+ const hasDeployedServices = services.some((s) => s.status === "deployed");
8973
+ if (hasDeployedServices) {
8974
+ console.log(`
8975
+ ${pc15.bold("Details:")}`);
8976
+ if (services.find((s) => s.name === "Email")?.status === "deployed") {
8977
+ console.log(
8978
+ ` ${pc15.dim("Email:")} ${pc15.cyan("wraps email status")}`
8979
+ );
8980
+ }
8981
+ } else {
8982
+ console.log(`
8983
+ ${pc15.bold("Get started:")}`);
8367
8984
  console.log(
8368
- `
8369
- Run ${pc13.cyan("wraps email init")} to deploy infrastructure.
8370
- `
8985
+ ` ${pc15.dim("Deploy email:")} ${pc15.cyan("wraps email init")}`
8371
8986
  );
8372
- process.exit(1);
8373
8987
  }
8374
- const domains = await listSESDomains(region);
8375
- const { SESv2Client: SESv2Client5, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
8376
- const sesv2Client = new SESv2Client5({ region });
8377
- const domainsWithTokens = await Promise.all(
8378
- domains.map(async (d) => {
8379
- try {
8380
- const identity2 = await sesv2Client.send(
8381
- new GetEmailIdentityCommand4({ EmailIdentity: d.domain })
8382
- );
8383
- return {
8384
- domain: d.domain,
8385
- status: d.verified ? "verified" : "pending",
8386
- dkimTokens: identity2.DkimAttributes?.Tokens || [],
8387
- mailFromDomain: identity2.MailFromAttributes?.MailFromDomain,
8388
- mailFromStatus: identity2.MailFromAttributes?.MailFromDomainStatus
8389
- };
8390
- } catch (_error) {
8391
- return {
8392
- domain: d.domain,
8393
- status: d.verified ? "verified" : "pending",
8394
- dkimTokens: void 0,
8395
- mailFromDomain: void 0,
8396
- mailFromStatus: void 0
8397
- };
8398
- }
8399
- })
8400
- );
8401
- const integrationLevel = stackOutputs.configSetName ? "enhanced" : "dashboard-only";
8402
- progress.stop();
8403
- displayStatus({
8404
- integrationLevel,
8405
- region,
8406
- domains: domainsWithTokens,
8407
- resources: {
8408
- roleArn: stackOutputs.roleArn?.value,
8409
- configSetName: stackOutputs.configSetName?.value,
8410
- tableName: stackOutputs.tableName?.value,
8411
- lambdaFunctions: stackOutputs.lambdaFunctions?.value?.length || 0,
8412
- snsTopics: integrationLevel === "enhanced" ? 1 : 0,
8413
- archiveArn: stackOutputs.archiveArn?.value,
8414
- archivingEnabled: stackOutputs.archivingEnabled?.value,
8415
- archiveRetention: stackOutputs.archiveRetention?.value
8416
- },
8417
- tracking: stackOutputs.customTrackingDomain?.value ? {
8418
- customTrackingDomain: stackOutputs.customTrackingDomain?.value,
8419
- httpsEnabled: stackOutputs.httpsTrackingEnabled?.value,
8420
- cloudFrontDomain: stackOutputs.cloudFrontDomain?.value
8421
- } : void 0
8988
+ console.log(`
8989
+ ${pc15.bold("Dashboard:")} ${pc15.blue("https://app.wraps.dev")}`);
8990
+ console.log(`${pc15.bold("Docs:")} ${pc15.blue("https://wraps.dev/docs")}
8991
+ `);
8992
+ trackCommand("status", {
8993
+ success: true,
8994
+ services_deployed: services.filter((s) => s.status === "deployed").length,
8995
+ duration_ms: Date.now() - startTime
8422
8996
  });
8423
8997
  }
8424
8998
 
8425
8999
  // src/commands/telemetry.ts
8426
9000
  init_esm_shims();
8427
9001
  init_client();
8428
- import * as clack13 from "@clack/prompts";
8429
- import pc14 from "picocolors";
9002
+ import * as clack15 from "@clack/prompts";
9003
+ import pc16 from "picocolors";
8430
9004
  async function telemetryEnable() {
8431
9005
  const client = getTelemetryClient();
8432
9006
  client.enable();
8433
- clack13.log.success(pc14.green("Telemetry enabled"));
8434
- console.log(` Config: ${pc14.dim(client.getConfigPath())}`);
9007
+ clack15.log.success(pc16.green("Telemetry enabled"));
9008
+ console.log(` Config: ${pc16.dim(client.getConfigPath())}`);
8435
9009
  console.log(`
8436
- ${pc14.dim("Thank you for helping improve Wraps!")}
9010
+ ${pc16.dim("Thank you for helping improve Wraps!")}
8437
9011
  `);
8438
9012
  }
8439
9013
  async function telemetryDisable() {
8440
9014
  const client = getTelemetryClient();
8441
9015
  client.disable();
8442
- clack13.log.success(pc14.green("Telemetry disabled"));
8443
- console.log(` Config: ${pc14.dim(client.getConfigPath())}`);
9016
+ clack15.log.success(pc16.green("Telemetry disabled"));
9017
+ console.log(` Config: ${pc16.dim(client.getConfigPath())}`);
8444
9018
  console.log(
8445
9019
  `
8446
- ${pc14.dim("You can re-enable with:")} wraps telemetry enable
9020
+ ${pc16.dim("You can re-enable with:")} wraps telemetry enable
8447
9021
  `
8448
9022
  );
8449
9023
  }
8450
9024
  async function telemetryStatus() {
8451
9025
  const client = getTelemetryClient();
8452
- clack13.intro(pc14.bold("Telemetry Status"));
8453
- const status2 = client.isEnabled() ? pc14.green("Enabled") : pc14.red("Disabled");
9026
+ clack15.intro(pc16.bold("Telemetry Status"));
9027
+ const status2 = client.isEnabled() ? pc16.green("Enabled") : pc16.red("Disabled");
8454
9028
  console.log();
8455
- console.log(` ${pc14.bold("Status:")} ${status2}`);
8456
- console.log(` ${pc14.bold("Config file:")} ${pc14.dim(client.getConfigPath())}`);
9029
+ console.log(` ${pc16.bold("Status:")} ${status2}`);
9030
+ console.log(` ${pc16.bold("Config file:")} ${pc16.dim(client.getConfigPath())}`);
8457
9031
  if (client.isEnabled()) {
8458
9032
  console.log();
8459
- console.log(pc14.bold(" How to opt-out:"));
8460
- console.log(` ${pc14.cyan("wraps telemetry disable")}`);
9033
+ console.log(pc16.bold(" How to opt-out:"));
9034
+ console.log(` ${pc16.cyan("wraps telemetry disable")}`);
8461
9035
  console.log(
8462
- ` ${pc14.dim("Or set:")} ${pc14.cyan("WRAPS_TELEMETRY_DISABLED=1")}`
9036
+ ` ${pc16.dim("Or set:")} ${pc16.cyan("WRAPS_TELEMETRY_DISABLED=1")}`
8463
9037
  );
8464
- console.log(` ${pc14.dim("Or set:")} ${pc14.cyan("DO_NOT_TRACK=1")}`);
9038
+ console.log(` ${pc16.dim("Or set:")} ${pc16.cyan("DO_NOT_TRACK=1")}`);
8465
9039
  } else {
8466
9040
  console.log();
8467
- console.log(pc14.bold(" How to opt-in:"));
8468
- console.log(` ${pc14.cyan("wraps telemetry enable")}`);
9041
+ console.log(pc16.bold(" How to opt-in:"));
9042
+ console.log(` ${pc16.cyan("wraps telemetry enable")}`);
8469
9043
  }
8470
9044
  console.log();
8471
- console.log(pc14.bold(" Debug mode:"));
9045
+ console.log(pc16.bold(" Debug mode:"));
8472
9046
  console.log(
8473
- ` ${pc14.dim("See what would be sent:")} ${pc14.cyan("WRAPS_TELEMETRY_DEBUG=1 wraps <command>")}`
9047
+ ` ${pc16.dim("See what would be sent:")} ${pc16.cyan("WRAPS_TELEMETRY_DEBUG=1 wraps <command>")}`
8474
9048
  );
8475
9049
  console.log();
8476
9050
  console.log(
8477
- ` ${pc14.dim("Learn more:")} ${pc14.cyan("https://wraps.dev/docs/telemetry")}`
9051
+ ` ${pc16.dim("Learn more:")} ${pc16.cyan("https://wraps.dev/docs/telemetry")}`
8478
9052
  );
8479
9053
  console.log();
8480
9054
  }
@@ -8492,19 +9066,41 @@ function printCompletionScript() {
8492
9066
  console.log("# ========================\n");
8493
9067
  console.log("# Tab completion will be available in a future release.\n");
8494
9068
  console.log("# For now, here are the available commands:\n");
8495
- console.log("# Commands:");
9069
+ console.log("# Email Commands:");
8496
9070
  console.log(
8497
9071
  "# wraps email init [--provider vercel|aws|railway|other] [--region <region>] [--domain <domain>]"
8498
9072
  );
8499
- console.log("# wraps status [--account <account-id>]");
8500
- console.log("# wraps completion\n");
9073
+ console.log("# wraps email connect [--region <region>]");
9074
+ console.log("# wraps email status [--account <account-id>]");
9075
+ console.log("# wraps email verify --domain <domain>");
9076
+ console.log("# wraps email sync");
9077
+ console.log("# wraps email upgrade");
9078
+ console.log("# wraps email restore [--region <region>] [--force]");
9079
+ console.log("# wraps email destroy [--force] [--preview]");
9080
+ console.log("# wraps email domains add --domain <domain>");
9081
+ console.log("# wraps email domains list");
9082
+ console.log("# wraps email domains verify --domain <domain>");
9083
+ console.log("# wraps email domains get-dkim --domain <domain>");
9084
+ console.log("# wraps email domains remove --domain <domain> [--force]\n");
9085
+ console.log("# Global Commands:");
9086
+ console.log("# wraps status");
9087
+ console.log("# wraps destroy [--force] [--preview]");
9088
+ console.log("# wraps console [--port <port>] [--no-open]");
9089
+ console.log("# wraps completion");
9090
+ console.log("# wraps telemetry [enable|disable|status]\n");
9091
+ console.log("# Dashboard Commands:");
9092
+ console.log("# wraps dashboard update-role [--region <region>] [--force]\n");
8501
9093
  console.log("# Flags:");
8502
- console.log("# --provider : vercel, aws, railway, other");
9094
+ console.log("# -p, --provider : vercel, aws, railway, other");
8503
9095
  console.log(
8504
- "# --region : us-east-1, us-east-2, us-west-1, us-west-2, eu-west-1, eu-west-2, etc."
9096
+ "# -r, --region : us-east-1, us-east-2, us-west-1, us-west-2, eu-west-1, eu-west-2, etc."
8505
9097
  );
8506
- console.log("# --domain : Your domain name (e.g., myapp.com)");
8507
- console.log("# --account : AWS account ID or alias\n");
9098
+ console.log("# -d, --domain : Your domain name (e.g., myapp.com)");
9099
+ console.log("# --account : AWS account ID or alias");
9100
+ console.log("# --preset : starter, production, enterprise, custom");
9101
+ console.log("# -y, --yes : Skip confirmation prompts");
9102
+ console.log("# -f, --force : Force destructive operations");
9103
+ console.log("# --preview : Preview changes without deploying\n");
8508
9104
  }
8509
9105
 
8510
9106
  // src/cli.ts
@@ -8521,62 +9117,66 @@ function showVersion() {
8521
9117
  process.exit(0);
8522
9118
  }
8523
9119
  function showHelp() {
8524
- clack14.intro(pc15.bold(`WRAPS CLI v${VERSION}`));
9120
+ clack16.intro(pc17.bold(`WRAPS CLI v${VERSION}`));
8525
9121
  console.log("Deploy AWS infrastructure to your account\n");
8526
9122
  console.log("Usage: wraps [service] <command> [options]\n");
8527
9123
  console.log("Services:");
8528
- console.log(` ${pc15.cyan("email")} Email infrastructure (AWS SES)`);
9124
+ console.log(` ${pc17.cyan("email")} Email infrastructure (AWS SES)
9125
+ `);
9126
+ console.log("Email Commands:");
8529
9127
  console.log(
8530
- ` ${pc15.cyan("sms")} SMS infrastructure (AWS End User Messaging) ${pc15.dim("[coming soon]")}
8531
- `
9128
+ ` ${pc17.cyan("email init")} Deploy new email infrastructure`
8532
9129
  );
8533
- console.log("Email Commands:");
8534
9130
  console.log(
8535
- ` ${pc15.cyan("email init")} Deploy new email infrastructure`
9131
+ ` ${pc17.cyan("email connect")} Connect to existing AWS SES`
8536
9132
  );
9133
+ console.log(` ${pc17.cyan("email status")} Show email infrastructure details`);
9134
+ console.log(` ${pc17.cyan("email verify")} Verify domain DNS records`);
8537
9135
  console.log(
8538
- ` ${pc15.cyan("email connect")} Connect to existing AWS SES`
9136
+ ` ${pc17.cyan("email sync")} Apply CLI updates to infrastructure`
8539
9137
  );
8540
- console.log(` ${pc15.cyan("email domains verify")} Verify domain DNS records`);
9138
+ console.log(` ${pc17.cyan("email upgrade")} Add features`);
8541
9139
  console.log(
8542
- ` ${pc15.cyan("email sync")} Apply CLI updates to infrastructure`
9140
+ ` ${pc17.cyan("email restore")} Restore original configuration`
8543
9141
  );
8544
- console.log(` ${pc15.cyan("email upgrade")} Add features`);
8545
9142
  console.log(
8546
- ` ${pc15.cyan("email restore")} Restore original configuration
8547
- `
9143
+ ` ${pc17.cyan("email destroy")} Remove email infrastructure`
8548
9144
  );
9145
+ console.log(` ${pc17.cyan("email domains add")} Add a domain to SES`);
9146
+ console.log(` ${pc17.cyan("email domains list")} List all domains`);
9147
+ console.log(` ${pc17.cyan("email domains remove")} Remove a domain
9148
+ `);
8549
9149
  console.log("Console & Dashboard:");
8550
- console.log(` ${pc15.cyan("console")} Start local web console`);
9150
+ console.log(` ${pc17.cyan("console")} Start local web console`);
8551
9151
  console.log(
8552
- ` ${pc15.cyan("dashboard update-role")} Update hosted dashboard IAM permissions
9152
+ ` ${pc17.cyan("dashboard update-role")} Update hosted dashboard IAM permissions
8553
9153
  `
8554
9154
  );
8555
9155
  console.log("Global Commands:");
8556
- console.log(` ${pc15.cyan("status")} Show all infrastructure status`);
8557
- console.log(` ${pc15.cyan("destroy")} Remove deployed infrastructure`);
8558
- console.log(` ${pc15.cyan("completion")} Generate shell completion script`);
9156
+ console.log(` ${pc17.cyan("status")} Show overview of all services`);
9157
+ console.log(` ${pc17.cyan("destroy")} Remove deployed infrastructure`);
9158
+ console.log(` ${pc17.cyan("completion")} Generate shell completion script`);
8559
9159
  console.log(
8560
- ` ${pc15.cyan("telemetry")} Manage anonymous telemetry settings
9160
+ ` ${pc17.cyan("telemetry")} Manage anonymous telemetry settings
8561
9161
  `
8562
9162
  );
8563
9163
  console.log("Options:");
8564
9164
  console.log(
8565
- ` ${pc15.dim("-p, --provider")} Hosting provider (vercel, aws, railway, other)`
9165
+ ` ${pc17.dim("-p, --provider")} Hosting provider (vercel, aws, railway, other)`
8566
9166
  );
8567
- console.log(` ${pc15.dim("-r, --region")} AWS region`);
8568
- console.log(` ${pc15.dim("-d, --domain")} Domain name`);
8569
- console.log(` ${pc15.dim("--account")} AWS account ID or alias`);
8570
- console.log(` ${pc15.dim("--preset")} Configuration preset`);
8571
- console.log(` ${pc15.dim("-y, --yes")} Skip confirmation prompts`);
8572
- console.log(` ${pc15.dim("-f, --force")} Force destructive operations`);
9167
+ console.log(` ${pc17.dim("-r, --region")} AWS region`);
9168
+ console.log(` ${pc17.dim("-d, --domain")} Domain name`);
9169
+ console.log(` ${pc17.dim("--account")} AWS account ID or alias`);
9170
+ console.log(` ${pc17.dim("--preset")} Configuration preset`);
9171
+ console.log(` ${pc17.dim("-y, --yes")} Skip confirmation prompts`);
9172
+ console.log(` ${pc17.dim("-f, --force")} Force destructive operations`);
8573
9173
  console.log(
8574
- ` ${pc15.dim("--preview")} Preview changes without deploying`
9174
+ ` ${pc17.dim("--preview")} Preview changes without deploying`
8575
9175
  );
8576
- console.log(` ${pc15.dim("-v, --version")} Show version number
9176
+ console.log(` ${pc17.dim("-v, --version")} Show version number
8577
9177
  `);
8578
9178
  console.log(
8579
- `Run ${pc15.cyan("wraps <service> <command> --help")} for more information.
9179
+ `Run ${pc17.cyan("wraps <service> <command> --help")} for more information.
8580
9180
  `
8581
9181
  );
8582
9182
  process.exit(0);
@@ -8643,37 +9243,9 @@ var flags = args.parse(process.argv);
8643
9243
  var [primaryCommand, subCommand] = args.sub;
8644
9244
  if (!primaryCommand) {
8645
9245
  async function selectService() {
8646
- clack14.intro(pc15.bold(`WRAPS CLI v${VERSION}`));
8647
- console.log("Welcome! Let's get started deploying your infrastructure.\n");
8648
- const service = await clack14.select({
8649
- message: "Which service would you like to set up?",
8650
- options: [
8651
- {
8652
- value: "email",
8653
- label: "Email",
8654
- hint: "AWS SES email infrastructure"
8655
- },
8656
- {
8657
- value: "sms",
8658
- label: "SMS",
8659
- hint: "Coming soon - AWS End User Messaging"
8660
- }
8661
- ]
8662
- });
8663
- if (clack14.isCancel(service)) {
8664
- clack14.cancel("Operation cancelled.");
8665
- process.exit(0);
8666
- }
8667
- if (service === "sms") {
8668
- clack14.log.warn("SMS infrastructure is coming soon!");
8669
- console.log(
8670
- `
8671
- Check back soon or follow our progress at ${pc15.cyan("https://github.com/wraps-team/wraps")}
8672
- `
8673
- );
8674
- process.exit(0);
8675
- }
8676
- const action = await clack14.select({
9246
+ clack16.intro(pc17.bold(`WRAPS CLI v${VERSION}`));
9247
+ console.log("Welcome! Let's get started deploying your email infrastructure.\n");
9248
+ const action = await clack16.select({
8677
9249
  message: "What would you like to do?",
8678
9250
  options: [
8679
9251
  {
@@ -8688,8 +9260,8 @@ Check back soon or follow our progress at ${pc15.cyan("https://github.com/wraps-
8688
9260
  }
8689
9261
  ]
8690
9262
  });
8691
- if (clack14.isCancel(action)) {
8692
- clack14.cancel("Operation cancelled.");
9263
+ if (clack16.isCancel(action)) {
9264
+ clack16.cancel("Operation cancelled.");
8693
9265
  process.exit(0);
8694
9266
  }
8695
9267
  if (action === "init") {
@@ -8718,20 +9290,20 @@ async function run() {
8718
9290
  const telemetry = getTelemetryClient();
8719
9291
  if (telemetry.shouldShowNotification()) {
8720
9292
  console.log();
8721
- clack14.log.info(pc15.bold("Anonymous Telemetry"));
9293
+ clack16.log.info(pc17.bold("Anonymous Telemetry"));
8722
9294
  console.log(
8723
- ` Wraps collects ${pc15.cyan("anonymous usage data")} to improve the CLI.`
9295
+ ` Wraps collects ${pc17.cyan("anonymous usage data")} to improve the CLI.`
8724
9296
  );
8725
9297
  console.log(
8726
- ` We ${pc15.bold("never")} collect: domains, AWS credentials, email content, or PII.`
9298
+ ` We ${pc17.bold("never")} collect: domains, AWS credentials, email content, or PII.`
8727
9299
  );
8728
9300
  console.log(
8729
- ` We ${pc15.bold("only")} collect: command names, success/failure, CLI version, OS.`
9301
+ ` We ${pc17.bold("only")} collect: command names, success/failure, CLI version, OS.`
8730
9302
  );
8731
9303
  console.log();
8732
- console.log(` Opt-out anytime: ${pc15.cyan("wraps telemetry disable")}`);
8733
- console.log(` Or set: ${pc15.cyan("WRAPS_TELEMETRY_DISABLED=1")}`);
8734
- console.log(` Learn more: ${pc15.cyan("https://wraps.dev/docs/telemetry")}`);
9304
+ console.log(` Opt-out anytime: ${pc17.cyan("wraps telemetry disable")}`);
9305
+ console.log(` Or set: ${pc17.cyan("WRAPS_TELEMETRY_DISABLED=1")}`);
9306
+ console.log(` Learn more: ${pc17.cyan("https://wraps.dev/docs/telemetry")}`);
8735
9307
  console.log();
8736
9308
  telemetry.markNotificationShown();
8737
9309
  }
@@ -8778,15 +9350,33 @@ async function run() {
8778
9350
  preview: flags.preview
8779
9351
  });
8780
9352
  break;
9353
+ case "status":
9354
+ await emailStatus({
9355
+ account: flags.account
9356
+ });
9357
+ break;
9358
+ case "verify": {
9359
+ if (!flags.domain) {
9360
+ clack16.log.error("--domain flag is required");
9361
+ console.log(
9362
+ `
9363
+ Usage: ${pc17.cyan("wraps email verify --domain yourapp.com")}
9364
+ `
9365
+ );
9366
+ process.exit(1);
9367
+ }
9368
+ await verifyDomain({ domain: flags.domain });
9369
+ break;
9370
+ }
8781
9371
  case "domains": {
8782
9372
  const domainsSubCommand = args.sub[2];
8783
9373
  switch (domainsSubCommand) {
8784
9374
  case "add": {
8785
9375
  if (!flags.domain) {
8786
- clack14.log.error("--domain flag is required");
9376
+ clack16.log.error("--domain flag is required");
8787
9377
  console.log(
8788
9378
  `
8789
- Usage: ${pc15.cyan("wraps email domains add --domain yourapp.com")}
9379
+ Usage: ${pc17.cyan("wraps email domains add --domain yourapp.com")}
8790
9380
  `
8791
9381
  );
8792
9382
  process.exit(1);
@@ -8799,10 +9389,10 @@ Usage: ${pc15.cyan("wraps email domains add --domain yourapp.com")}
8799
9389
  break;
8800
9390
  case "verify": {
8801
9391
  if (!flags.domain) {
8802
- clack14.log.error("--domain flag is required");
9392
+ clack16.log.error("--domain flag is required");
8803
9393
  console.log(
8804
9394
  `
8805
- Usage: ${pc15.cyan("wraps email domains verify --domain yourapp.com")}
9395
+ Usage: ${pc17.cyan("wraps email domains verify --domain yourapp.com")}
8806
9396
  `
8807
9397
  );
8808
9398
  process.exit(1);
@@ -8812,10 +9402,10 @@ Usage: ${pc15.cyan("wraps email domains verify --domain yourapp.com")}
8812
9402
  }
8813
9403
  case "get-dkim": {
8814
9404
  if (!flags.domain) {
8815
- clack14.log.error("--domain flag is required");
9405
+ clack16.log.error("--domain flag is required");
8816
9406
  console.log(
8817
9407
  `
8818
- Usage: ${pc15.cyan("wraps email domains get-dkim --domain yourapp.com")}
9408
+ Usage: ${pc17.cyan("wraps email domains get-dkim --domain yourapp.com")}
8819
9409
  `
8820
9410
  );
8821
9411
  process.exit(1);
@@ -8825,10 +9415,10 @@ Usage: ${pc15.cyan("wraps email domains get-dkim --domain yourapp.com")}
8825
9415
  }
8826
9416
  case "remove": {
8827
9417
  if (!flags.domain) {
8828
- clack14.log.error("--domain flag is required");
9418
+ clack16.log.error("--domain flag is required");
8829
9419
  console.log(
8830
9420
  `
8831
- Usage: ${pc15.cyan("wraps email domains remove --domain yourapp.com --force")}
9421
+ Usage: ${pc17.cyan("wraps email domains remove --domain yourapp.com --force")}
8832
9422
  `
8833
9423
  );
8834
9424
  process.exit(1);
@@ -8840,27 +9430,40 @@ Usage: ${pc15.cyan("wraps email domains remove --domain yourapp.com --force")}
8840
9430
  break;
8841
9431
  }
8842
9432
  default:
8843
- clack14.log.error(
9433
+ clack16.log.error(
8844
9434
  `Unknown domains command: ${domainsSubCommand || "(none)"}`
8845
9435
  );
8846
9436
  console.log(
8847
9437
  `
8848
- Available commands: ${pc15.cyan("add")}, ${pc15.cyan("list")}, ${pc15.cyan("verify")}, ${pc15.cyan("get-dkim")}, ${pc15.cyan("remove")}
9438
+ Available commands: ${pc17.cyan("add")}, ${pc17.cyan("list")}, ${pc17.cyan("verify")}, ${pc17.cyan("get-dkim")}, ${pc17.cyan("remove")}
8849
9439
  `
8850
9440
  );
8851
9441
  process.exit(1);
8852
9442
  }
8853
9443
  break;
8854
9444
  }
9445
+ case "destroy":
9446
+ await emailDestroy({
9447
+ force: flags.force,
9448
+ preview: flags.preview
9449
+ });
9450
+ break;
8855
9451
  default:
8856
- clack14.log.error(`Unknown email command: ${subCommand}`);
9452
+ clack16.log.error(`Unknown email command: ${subCommand}`);
8857
9453
  console.log(
8858
9454
  `
8859
- Run ${pc15.cyan("wraps --help")} for available commands.
9455
+ Run ${pc17.cyan("wraps --help")} for available commands.
8860
9456
  `
8861
9457
  );
8862
9458
  process.exit(1);
8863
9459
  }
9460
+ const emailDuration = Date.now() - startTime;
9461
+ const emailCommandName = `email:${subCommand}`;
9462
+ trackCommand(emailCommandName, {
9463
+ success: true,
9464
+ duration_ms: emailDuration,
9465
+ service: "email"
9466
+ });
8864
9467
  return;
8865
9468
  }
8866
9469
  if (primaryCommand === "dashboard" && subCommand) {
@@ -8872,25 +9475,22 @@ Run ${pc15.cyan("wraps --help")} for available commands.
8872
9475
  });
8873
9476
  break;
8874
9477
  default:
8875
- clack14.log.error(`Unknown dashboard command: ${subCommand}`);
9478
+ clack16.log.error(`Unknown dashboard command: ${subCommand}`);
8876
9479
  console.log(`
8877
- Available commands: ${pc15.cyan("update-role")}
9480
+ Available commands: ${pc17.cyan("update-role")}
8878
9481
  `);
8879
- console.log(`Run ${pc15.cyan("wraps --help")} for more information.
9482
+ console.log(`Run ${pc17.cyan("wraps --help")} for more information.
8880
9483
  `);
8881
9484
  process.exit(1);
8882
9485
  }
9486
+ const dashboardDuration = Date.now() - startTime;
9487
+ const dashboardCommandName = `dashboard:${subCommand}`;
9488
+ trackCommand(dashboardCommandName, {
9489
+ success: true,
9490
+ duration_ms: dashboardDuration
9491
+ });
8883
9492
  return;
8884
9493
  }
8885
- if (primaryCommand === "sms" && subCommand) {
8886
- clack14.log.warn("SMS infrastructure is coming soon!");
8887
- console.log(
8888
- `
8889
- Check back soon or follow our progress at ${pc15.cyan("https://github.com/wraps-team/wraps")}
8890
- `
8891
- );
8892
- process.exit(0);
8893
- }
8894
9494
  switch (primaryCommand) {
8895
9495
  // Global commands (work across all services)
8896
9496
  case "status":
@@ -8906,8 +9506,8 @@ Check back soon or follow our progress at ${pc15.cyan("https://github.com/wraps-
8906
9506
  break;
8907
9507
  case "dashboard":
8908
9508
  if (!subCommand) {
8909
- clack14.log.warn(
8910
- `'wraps dashboard' is deprecated. Use ${pc15.cyan("wraps console")} instead.`
9509
+ clack16.log.warn(
9510
+ `'wraps dashboard' is deprecated. Use ${pc17.cyan("wraps console")} instead.`
8911
9511
  );
8912
9512
  await dashboard({
8913
9513
  port: flags.port,
@@ -8937,10 +9537,10 @@ Check back soon or follow our progress at ${pc15.cyan("https://github.com/wraps-
8937
9537
  await telemetryStatus();
8938
9538
  break;
8939
9539
  default:
8940
- clack14.log.error(`Unknown telemetry command: ${subCommand}`);
9540
+ clack16.log.error(`Unknown telemetry command: ${subCommand}`);
8941
9541
  console.log(
8942
9542
  `
8943
- Available commands: ${pc15.cyan("enable")}, ${pc15.cyan("disable")}, ${pc15.cyan("status")}
9543
+ Available commands: ${pc17.cyan("enable")}, ${pc17.cyan("disable")}, ${pc17.cyan("status")}
8944
9544
  `
8945
9545
  );
8946
9546
  process.exit(1);
@@ -8949,7 +9549,6 @@ Available commands: ${pc15.cyan("enable")}, ${pc15.cyan("disable")}, ${pc15.cyan
8949
9549
  }
8950
9550
  // Show help for service without subcommand
8951
9551
  case "email":
8952
- case "sms":
8953
9552
  console.log(
8954
9553
  `
8955
9554
  Please specify a command for ${primaryCommand} service.
@@ -8958,10 +9557,10 @@ Please specify a command for ${primaryCommand} service.
8958
9557
  showHelp();
8959
9558
  break;
8960
9559
  default:
8961
- clack14.log.error(`Unknown command: ${primaryCommand}`);
9560
+ clack16.log.error(`Unknown command: ${primaryCommand}`);
8962
9561
  console.log(
8963
9562
  `
8964
- Run ${pc15.cyan("wraps --help")} for available commands.
9563
+ Run ${pc17.cyan("wraps --help")} for available commands.
8965
9564
  `
8966
9565
  );
8967
9566
  process.exit(1);