@wraps.dev/cli 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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.3.0",
149
+ version: "1.4.1",
150
150
  description: "CLI for deploying Wraps email infrastructure to your AWS account",
151
151
  type: "module",
152
152
  main: "./dist/cli.js",
@@ -546,6 +546,12 @@ To remove: wraps destroy --stack ${stackName}`,
546
546
  "PULUMI_NOT_INSTALLED",
547
547
  "Install Pulumi:\n macOS: brew install pulumi/tap/pulumi\n Linux: curl -fsSL https://get.pulumi.com | sh\n Windows: choco install pulumi\n\nOr download from: https://www.pulumi.com/docs/install/",
548
548
  "https://www.pulumi.com/docs/install/"
549
+ ),
550
+ stackLocked: () => new WrapsError(
551
+ "The Pulumi stack is locked from a previous run",
552
+ "STACK_LOCKED",
553
+ "This happens when a previous deployment was interrupted.\n\nTo unlock, run:\n rm -rf ~/.wraps/pulumi/.pulumi/locks\n\nThen try your command again.",
554
+ "https://wraps.dev/docs/troubleshooting"
549
555
  )
550
556
  };
551
557
  }
@@ -2796,7 +2802,9 @@ function createConnectionMetadata(accountId, region, provider, emailConfig, pres
2796
2802
  function applyConfigUpdates(existingConfig, updates) {
2797
2803
  const result = { ...existingConfig };
2798
2804
  for (const [key, value] of Object.entries(updates)) {
2799
- if (value === void 0) continue;
2805
+ if (value === void 0) {
2806
+ continue;
2807
+ }
2800
2808
  if (key === "tracking" && typeof value === "object") {
2801
2809
  const trackingUpdate = value;
2802
2810
  result.tracking = {
@@ -3269,6 +3277,7 @@ ${pc3.bold("Updated Permissions:")}`);
3269
3277
  console.log(
3270
3278
  ` ${pc3.green("\u2713")} SES metrics and identity verification (always enabled)`
3271
3279
  );
3280
+ console.log(` ${pc3.green("\u2713")} SES template management (always enabled)`);
3272
3281
  if (sendingEnabled) {
3273
3282
  console.log(` ${pc3.green("\u2713")} Email sending via SES`);
3274
3283
  }
@@ -3302,6 +3311,18 @@ function buildConsolePolicyDocument(emailConfig) {
3302
3311
  ],
3303
3312
  Resource: "*"
3304
3313
  });
3314
+ statements.push({
3315
+ Effect: "Allow",
3316
+ Action: [
3317
+ "ses:GetTemplate",
3318
+ "ses:ListTemplates",
3319
+ "ses:CreateTemplate",
3320
+ "ses:UpdateTemplate",
3321
+ "ses:DeleteTemplate",
3322
+ "ses:TestRenderTemplate"
3323
+ ],
3324
+ Resource: "*"
3325
+ });
3305
3326
  const sendingEnabled = !emailConfig || emailConfig.sendingEnabled !== false;
3306
3327
  if (sendingEnabled) {
3307
3328
  statements.push({
@@ -3777,7 +3798,7 @@ Try running: pnpm build`
3777
3798
  entryPoints: [sourcePath],
3778
3799
  bundle: true,
3779
3800
  platform: "node",
3780
- target: "node20",
3801
+ target: "node24",
3781
3802
  format: "esm",
3782
3803
  outfile: join3(outdir, "index.mjs"),
3783
3804
  external: ["@aws-sdk/*"],
@@ -3849,7 +3870,7 @@ async function deployLambdaFunctions(config2) {
3849
3870
  functionName,
3850
3871
  {
3851
3872
  name: functionName,
3852
- runtime: aws4.lambda.Runtime.NodeJS20dX,
3873
+ runtime: "nodejs24.x",
3853
3874
  handler: "index.handler",
3854
3875
  role: lambdaRole.arn,
3855
3876
  code: new pulumi3.asset.FileArchive(eventProcessorCode),
@@ -3873,7 +3894,7 @@ async function deployLambdaFunctions(config2) {
3873
3894
  }
3874
3895
  ) : new aws4.lambda.Function(functionName, {
3875
3896
  name: functionName,
3876
- runtime: aws4.lambda.Runtime.NodeJS20dX,
3897
+ runtime: "nodejs24.x",
3877
3898
  handler: "index.handler",
3878
3899
  role: lambdaRole.arn,
3879
3900
  code: new pulumi3.asset.FileArchive(eventProcessorCode),
@@ -3942,6 +3963,28 @@ async function configurationSetExists(configSetName, region) {
3942
3963
  return false;
3943
3964
  }
3944
3965
  }
3966
+ async function eventDestinationExists(configSetName, eventDestName, region) {
3967
+ try {
3968
+ const {
3969
+ SESv2Client: SESv2Client5,
3970
+ GetConfigurationSetEventDestinationsCommand
3971
+ } = await import("@aws-sdk/client-sesv2");
3972
+ const ses = new SESv2Client5({ region });
3973
+ const response = await ses.send(
3974
+ new GetConfigurationSetEventDestinationsCommand({
3975
+ ConfigurationSetName: configSetName
3976
+ })
3977
+ );
3978
+ return response.EventDestinations?.some(
3979
+ (dest) => dest.Name === eventDestName
3980
+ ) ?? false;
3981
+ } catch (error) {
3982
+ if (error.name === "NotFoundException") {
3983
+ return false;
3984
+ }
3985
+ return false;
3986
+ }
3987
+ }
3945
3988
  async function emailIdentityExists(emailIdentity, region) {
3946
3989
  try {
3947
3990
  const { SESv2Client: SESv2Client5, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
@@ -3995,29 +4038,37 @@ async function createSESResources(config2) {
3995
4038
  });
3996
4039
  if (config2.eventTrackingEnabled) {
3997
4040
  const eventDestName = "wraps-email-eventbridge";
3998
- new aws5.sesv2.ConfigurationSetEventDestination("wraps-email-all-events", {
3999
- configurationSetName: configSet.configurationSetName,
4000
- eventDestinationName: eventDestName,
4001
- eventDestination: {
4002
- enabled: true,
4003
- matchingEventTypes: [
4004
- "SEND",
4005
- "DELIVERY",
4006
- "OPEN",
4007
- "CLICK",
4008
- "BOUNCE",
4009
- "COMPLAINT",
4010
- "REJECT",
4011
- "RENDERING_FAILURE",
4012
- "DELIVERY_DELAY",
4013
- "SUBSCRIPTION"
4014
- ],
4015
- eventBridgeDestination: {
4016
- // SES requires default bus - cannot use custom bus
4017
- eventBusArn: defaultEventBus.arn
4041
+ new aws5.sesv2.ConfigurationSetEventDestination(
4042
+ "wraps-email-all-events",
4043
+ {
4044
+ configurationSetName: configSet.configurationSetName,
4045
+ eventDestinationName: eventDestName,
4046
+ eventDestination: {
4047
+ enabled: true,
4048
+ matchingEventTypes: [
4049
+ "SEND",
4050
+ "DELIVERY",
4051
+ "OPEN",
4052
+ "CLICK",
4053
+ "BOUNCE",
4054
+ "COMPLAINT",
4055
+ "REJECT",
4056
+ "RENDERING_FAILURE",
4057
+ "DELIVERY_DELAY",
4058
+ "SUBSCRIPTION"
4059
+ ],
4060
+ eventBridgeDestination: {
4061
+ // SES requires default bus - cannot use custom bus
4062
+ eventBusArn: defaultEventBus.arn
4063
+ }
4018
4064
  }
4065
+ },
4066
+ {
4067
+ // Import existing resource if it already exists in AWS
4068
+ // This prevents AlreadyExistsException when the resource exists but isn't in Pulumi state
4069
+ import: config2.importExistingEventDestination ? `wraps-email-tracking|${eventDestName}` : void 0
4019
4070
  }
4020
- });
4071
+ );
4021
4072
  }
4022
4073
  let domainIdentity;
4023
4074
  let dkimTokens;
@@ -4229,6 +4280,11 @@ async function deployEmailStack(config2) {
4229
4280
  }
4230
4281
  let sesResources;
4231
4282
  if (emailConfig.tracking?.enabled || emailConfig.eventTracking?.enabled) {
4283
+ const shouldImportEventDest = emailConfig.eventTracking?.enabled && await eventDestinationExists(
4284
+ "wraps-email-tracking",
4285
+ "wraps-email-eventbridge",
4286
+ config2.region
4287
+ );
4232
4288
  sesResources = await createSESResources({
4233
4289
  domain: emailConfig.domain,
4234
4290
  mailFromDomain: emailConfig.mailFromDomain,
@@ -4237,8 +4293,10 @@ async function deployEmailStack(config2) {
4237
4293
  eventTypes: emailConfig.eventTracking?.events,
4238
4294
  eventTrackingEnabled: emailConfig.eventTracking?.enabled,
4239
4295
  // Pass flag to create EventBridge destination
4240
- tlsRequired: emailConfig.tlsRequired
4296
+ tlsRequired: emailConfig.tlsRequired,
4241
4297
  // Require TLS encryption for all emails
4298
+ importExistingEventDestination: shouldImportEventDest
4299
+ // Import if exists to avoid AlreadyExistsException
4242
4300
  });
4243
4301
  }
4244
4302
  let dynamoTables;
@@ -4303,6 +4361,7 @@ async function deployEmailStack(config2) {
4303
4361
 
4304
4362
  // src/commands/email/config.ts
4305
4363
  init_aws();
4364
+ init_errors();
4306
4365
 
4307
4366
  // src/utils/shared/pulumi.ts
4308
4367
  init_esm_shims();
@@ -4373,7 +4432,14 @@ ${pc4.bold("Current Configuration:")}
4373
4432
  } else {
4374
4433
  console.log(` Preset: ${pc4.cyan("custom")}`);
4375
4434
  }
4376
- const config2 = metadata.services.email.config;
4435
+ const config2 = metadata.services.email?.config;
4436
+ if (!config2) {
4437
+ clack3.log.error("No email configuration found in metadata");
4438
+ clack3.log.info(
4439
+ `Use ${pc4.cyan("wraps email init")} to create new infrastructure.`
4440
+ );
4441
+ process.exit(1);
4442
+ }
4377
4443
  if (config2.domain) {
4378
4444
  console.log(` Sending Domain: ${pc4.cyan(config2.domain)}`);
4379
4445
  }
@@ -4462,6 +4528,8 @@ ${pc4.bold("Current Configuration:")}
4462
4528
  metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`
4463
4529
  );
4464
4530
  await stack.setConfig("aws:region", { value: region });
4531
+ await stack.refresh({ onOutput: () => {
4532
+ } });
4465
4533
  const upResult = await stack.up({ onOutput: () => {
4466
4534
  } });
4467
4535
  const pulumiOutputs = upResult.outputs;
@@ -4478,12 +4546,8 @@ ${pc4.bold("Current Configuration:")}
4478
4546
  }
4479
4547
  );
4480
4548
  } catch (error) {
4481
- clack3.log.error("Infrastructure update failed");
4482
4549
  if (error.message?.includes("stack is currently locked")) {
4483
- clack3.log.warn("\nThe Pulumi stack is locked from a previous run.");
4484
- clack3.log.info("To fix this, run:");
4485
- clack3.log.info(` ${pc4.cyan("rm -rf ~/.wraps/pulumi/.pulumi/locks")}`);
4486
- clack3.log.info("\nThen try running wraps update again.");
4550
+ throw errors.stackLocked();
4487
4551
  }
4488
4552
  throw new Error(`Pulumi update failed: ${error.message}`);
4489
4553
  }
@@ -4524,6 +4588,7 @@ import * as pulumi6 from "@pulumi/pulumi";
4524
4588
  import pc6 from "picocolors";
4525
4589
  init_presets();
4526
4590
  init_aws();
4591
+ init_errors();
4527
4592
  init_prompts();
4528
4593
 
4529
4594
  // src/utils/shared/scanner.ts
@@ -4898,12 +4963,8 @@ async function connect(options) {
4898
4963
  }
4899
4964
  );
4900
4965
  } catch (error) {
4901
- clack5.log.error("Infrastructure deployment failed");
4902
4966
  if (error.message?.includes("stack is currently locked")) {
4903
- clack5.log.warn("\nThe Pulumi stack is locked from a previous run.");
4904
- clack5.log.info("To fix this, run:");
4905
- clack5.log.info(` ${pc6.cyan("rm -rf ~/.wraps/pulumi/.pulumi/locks")}`);
4906
- clack5.log.info("\nThen try running wraps email connect again.");
4967
+ throw errors.stackLocked();
4907
4968
  }
4908
4969
  throw new Error(`Pulumi deployment failed: ${error.message}`);
4909
4970
  }
@@ -5401,6 +5462,7 @@ import pc8 from "picocolors";
5401
5462
  init_costs();
5402
5463
  init_presets();
5403
5464
  init_aws();
5465
+ init_errors();
5404
5466
  init_prompts();
5405
5467
  async function init(options) {
5406
5468
  clack7.intro(pc8.bold("Wraps Email Infrastructure Setup"));
@@ -5562,12 +5624,8 @@ ${pc8.yellow(pc8.bold("Configuration Warnings:"))}`);
5562
5624
  }
5563
5625
  );
5564
5626
  } catch (error) {
5565
- clack7.log.error("Infrastructure deployment failed");
5566
5627
  if (error.message?.includes("stack is currently locked")) {
5567
- clack7.log.warn("\nThe Pulumi stack is locked from a previous run.");
5568
- clack7.log.info("To fix this, run:");
5569
- clack7.log.info(` ${pc8.cyan("rm -rf ~/.wraps/pulumi/.pulumi/locks")}`);
5570
- clack7.log.info("\nThen try running wraps email init again.");
5628
+ throw errors.stackLocked();
5571
5629
  }
5572
5630
  throw new Error(`Pulumi deployment failed: ${error.message}`);
5573
5631
  }
@@ -5662,13 +5720,13 @@ async function restore(options) {
5662
5720
  ${pc9.bold("The following Wraps resources will be removed:")}
5663
5721
  `
5664
5722
  );
5665
- if (metadata.services.email.config.tracking?.enabled) {
5723
+ if (metadata.services.email?.config.tracking?.enabled) {
5666
5724
  console.log(` ${pc9.cyan("\u2713")} Configuration Set (wraps-email-tracking)`);
5667
5725
  }
5668
- if (metadata.services.email.config.eventTracking?.dynamoDBHistory) {
5726
+ if (metadata.services.email?.config.eventTracking?.dynamoDBHistory) {
5669
5727
  console.log(` ${pc9.cyan("\u2713")} DynamoDB Table (wraps-email-history)`);
5670
5728
  }
5671
- if (metadata.services.email.config.eventTracking?.enabled) {
5729
+ if (metadata.services.email?.config.eventTracking?.enabled) {
5672
5730
  console.log(` ${pc9.cyan("\u2713")} EventBridge Rules`);
5673
5731
  console.log(` ${pc9.cyan("\u2713")} SQS Queues`);
5674
5732
  console.log(` ${pc9.cyan("\u2713")} Lambda Functions`);
@@ -5740,6 +5798,7 @@ import pc10 from "picocolors";
5740
5798
  init_costs();
5741
5799
  init_presets();
5742
5800
  init_aws();
5801
+ init_errors();
5743
5802
  init_prompts();
5744
5803
  async function upgrade(options) {
5745
5804
  clack9.intro(pc10.bold("Wraps Upgrade - Enhance Your Email Infrastructure"));
@@ -5780,7 +5839,14 @@ ${pc10.bold("Current Configuration:")}
5780
5839
  } else {
5781
5840
  console.log(` Preset: ${pc10.cyan("custom")}`);
5782
5841
  }
5783
- const config2 = metadata.services.email.config;
5842
+ const config2 = metadata.services.email?.config;
5843
+ if (!config2) {
5844
+ clack9.log.error("No email configuration found in metadata");
5845
+ clack9.log.info(
5846
+ `Use ${pc10.cyan("wraps email init")} to create new infrastructure.`
5847
+ );
5848
+ process.exit(1);
5849
+ }
5784
5850
  if (config2.domain) {
5785
5851
  console.log(` Sending Domain: ${pc10.cyan(config2.domain)}`);
5786
5852
  }
@@ -6454,12 +6520,8 @@ ${pc10.bold("Cost Impact:")}`);
6454
6520
  }
6455
6521
  );
6456
6522
  } catch (error) {
6457
- clack9.log.error("Infrastructure upgrade failed");
6458
6523
  if (error.message?.includes("stack is currently locked")) {
6459
- clack9.log.warn("\nThe Pulumi stack is locked from a previous run.");
6460
- clack9.log.info("To fix this, run:");
6461
- clack9.log.info(` ${pc10.cyan("rm -rf ~/.wraps/pulumi/.pulumi/locks")}`);
6462
- clack9.log.info("\nThen try running wraps upgrade again.");
6524
+ throw errors.stackLocked();
6463
6525
  }
6464
6526
  throw new Error(`Pulumi upgrade failed: ${error.message}`);
6465
6527
  }
@@ -7438,7 +7500,7 @@ function createSettingsRouter(config2) {
7438
7500
  });
7439
7501
  }
7440
7502
  const configSetName = "wraps-email-tracking";
7441
- const domain = metadata.services.email.config.domain;
7503
+ const domain = metadata.services.email?.config.domain;
7442
7504
  const settings = await fetchEmailSettings(
7443
7505
  config2.roleArn,
7444
7506
  config2.region,
@@ -8113,7 +8175,9 @@ function showHelp() {
8113
8175
  ` ${pc15.cyan("email connect")} Connect to existing AWS SES`
8114
8176
  );
8115
8177
  console.log(` ${pc15.cyan("email domains verify")} Verify domain DNS records`);
8116
- console.log(` ${pc15.cyan("email config")} Update infrastructure`);
8178
+ console.log(
8179
+ ` ${pc15.cyan("email sync")} Apply CLI updates to infrastructure`
8180
+ );
8117
8181
  console.log(` ${pc15.cyan("email upgrade")} Add features`);
8118
8182
  console.log(
8119
8183
  ` ${pc15.cyan("email restore")} Restore original configuration
@@ -8318,6 +8382,7 @@ async function run() {
8318
8382
  });
8319
8383
  break;
8320
8384
  case "config":
8385
+ case "sync":
8321
8386
  await config({
8322
8387
  region: flags.region,
8323
8388
  yes: flags.yes