@wraps.dev/cli 2.12.0 → 2.12.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
@@ -69,6 +69,7 @@ var init_ci_detection = __esm({
69
69
  // src/utils/shared/s3-state.ts
70
70
  var s3_state_exports = {};
71
71
  __export(s3_state_exports, {
72
+ deleteMetadata: () => deleteMetadata,
72
73
  downloadMetadata: () => downloadMetadata,
73
74
  ensureStateBucket: () => ensureStateBucket,
74
75
  getS3BackendUrl: () => getS3BackendUrl,
@@ -188,6 +189,17 @@ async function uploadMetadata(bucketName, metadata) {
188
189
  })
189
190
  );
190
191
  }
192
+ async function deleteMetadata(bucketName, accountId, region) {
193
+ const { S3Client: S3Client2, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
194
+ const client = new S3Client2({ region });
195
+ const key = `metadata/${accountId}-${region}.json`;
196
+ await client.send(
197
+ new DeleteObjectCommand({
198
+ Bucket: bucketName,
199
+ Key: key
200
+ })
201
+ );
202
+ }
191
203
  async function downloadMetadata(bucketName, accountId, region) {
192
204
  const { S3Client: S3Client2, GetObjectCommand: GetObjectCommand2 } = await import("@aws-sdk/client-s3");
193
205
  const client = new S3Client2({ region });
@@ -4156,6 +4168,17 @@ async function deleteConnectionMetadata(accountId, region) {
4156
4168
  const { unlink } = await import("fs/promises");
4157
4169
  await unlink(metadataPath);
4158
4170
  }
4171
+ if (process.env.WRAPS_LOCAL_ONLY !== "1") {
4172
+ try {
4173
+ const { stateBucketExists: stateBucketExists2, deleteMetadata: deleteMetadata2, getStateBucketName: getStateBucketName2 } = await Promise.resolve().then(() => (init_s3_state(), s3_state_exports));
4174
+ const bucketExists = await stateBucketExists2(accountId, region);
4175
+ if (bucketExists) {
4176
+ const bucketName = getStateBucketName2(accountId, region);
4177
+ await deleteMetadata2(bucketName, accountId, region);
4178
+ }
4179
+ } catch {
4180
+ }
4181
+ }
4159
4182
  }
4160
4183
  async function listConnections() {
4161
4184
  const connectionsDir = getConnectionsDir();
@@ -7631,7 +7654,11 @@ var test_exports = {};
7631
7654
  __export(test_exports, {
7632
7655
  emailTest: () => emailTest
7633
7656
  });
7634
- import { SESv2Client as SESv2Client5, SendEmailCommand } from "@aws-sdk/client-sesv2";
7657
+ import {
7658
+ GetEmailIdentityCommand as GetEmailIdentityCommand3,
7659
+ SESv2Client as SESv2Client5,
7660
+ SendEmailCommand
7661
+ } from "@aws-sdk/client-sesv2";
7635
7662
  import * as clack21 from "@clack/prompts";
7636
7663
  import pc22 from "picocolors";
7637
7664
  async function emailTest(options) {
@@ -7736,11 +7763,35 @@ Run ${pc22.cyan("wraps email init")} to set up a domain.
7736
7763
  }
7737
7764
  }
7738
7765
  const fromEmail = `test@${domain}`;
7766
+ const sesClient = new SESv2Client5({ region });
7767
+ try {
7768
+ const identityResponse = await sesClient.send(
7769
+ new GetEmailIdentityCommand3({ EmailIdentity: domain })
7770
+ );
7771
+ if (!identityResponse.VerifiedForSendingStatus) {
7772
+ progress.stop();
7773
+ const dkimStatus = identityResponse.DkimAttributes?.Status || "PENDING";
7774
+ clack21.log.error(
7775
+ `Sending domain ${pc22.cyan(domain)} is not yet verified (DKIM: ${dkimStatus})`
7776
+ );
7777
+ console.log(
7778
+ "\nDKIM DNS records need to propagate before you can send emails."
7779
+ );
7780
+ console.log("This typically takes a few minutes after deployment.\n");
7781
+ console.log(
7782
+ `Run ${pc22.cyan(`wraps email domains verify --domain ${domain}`)} to check DNS status.
7783
+ `
7784
+ );
7785
+ process.exit(1);
7786
+ return;
7787
+ }
7788
+ } catch (identityError) {
7789
+ }
7739
7790
  try {
7740
7791
  const messageId = await progress.execute(
7741
7792
  `Sending test email to ${toEmail}`,
7742
7793
  async () => {
7743
- const client = new SESv2Client5({ region });
7794
+ const client = sesClient;
7744
7795
  const response = await client.send(
7745
7796
  new SendEmailCommand({
7746
7797
  FromEmailAddress: fromEmail,
@@ -7806,12 +7857,33 @@ Run ${pc22.cyan("wraps email init")} to set up a domain.
7806
7857
  });
7807
7858
  if (errorName === "MessageRejected" || errorMessage.includes("MessageRejected")) {
7808
7859
  if (errorMessage.includes("not verified")) {
7809
- clack21.log.error("Email address is not verified");
7810
- console.log("\nIn sandbox mode, recipient addresses must be verified.");
7811
- console.log(
7812
- `Simulator addresses always work: ${pc22.cyan("success@simulator.amazonses.com")}
7813
- `
7860
+ const failedIdentityMatch = errorMessage.match(
7861
+ /identities failed the check.*?:\s*(.+)/i
7814
7862
  );
7863
+ const failedIdentity = failedIdentityMatch?.[1]?.trim();
7864
+ const isFromDomainFailure = failedIdentity && (failedIdentity === fromEmail || failedIdentity.endsWith(`@${domain}`) || failedIdentity === domain);
7865
+ if (isFromDomainFailure) {
7866
+ clack21.log.error(
7867
+ `Sending domain ${pc22.cyan(domain)} is not yet verified`
7868
+ );
7869
+ console.log(
7870
+ "\nDKIM DNS records need to propagate before you can send emails."
7871
+ );
7872
+ console.log("This typically takes a few minutes after deployment.\n");
7873
+ console.log(
7874
+ `Run ${pc22.cyan(`wraps email domains verify --domain ${domain}`)} to check DNS status.
7875
+ `
7876
+ );
7877
+ } else {
7878
+ clack21.log.error("Email address is not verified");
7879
+ console.log(
7880
+ "\nIn sandbox mode, recipient addresses must be verified."
7881
+ );
7882
+ console.log(
7883
+ `Simulator addresses always work: ${pc22.cyan("success@simulator.amazonses.com")}
7884
+ `
7885
+ );
7886
+ }
7815
7887
  } else {
7816
7888
  clack21.log.error(`Email rejected: ${errorMessage}`);
7817
7889
  }
@@ -8243,7 +8315,7 @@ var init_dynamodb_metrics = __esm({
8243
8315
  // src/cli.ts
8244
8316
  init_esm_shims();
8245
8317
  import { readFileSync as readFileSync3 } from "fs";
8246
- import { dirname as dirname3, join as join15 } from "path";
8318
+ import { dirname as dirname3, join as join16 } from "path";
8247
8319
  import { fileURLToPath as fileURLToPath5 } from "url";
8248
8320
  import * as clack47 from "@clack/prompts";
8249
8321
  import args from "args";
@@ -15765,10 +15837,10 @@ async function eventDestinationExists(configSetName, eventDestName, region) {
15765
15837
  }
15766
15838
  async function emailIdentityExists(emailIdentity, region) {
15767
15839
  try {
15768
- const { SESv2Client: SESv2Client8, GetEmailIdentityCommand: GetEmailIdentityCommand5 } = await import("@aws-sdk/client-sesv2");
15840
+ const { SESv2Client: SESv2Client8, GetEmailIdentityCommand: GetEmailIdentityCommand6 } = await import("@aws-sdk/client-sesv2");
15769
15841
  const ses = new SESv2Client8({ region });
15770
15842
  await ses.send(
15771
- new GetEmailIdentityCommand5({ EmailIdentity: emailIdentity })
15843
+ new GetEmailIdentityCommand6({ EmailIdentity: emailIdentity })
15772
15844
  );
15773
15845
  return true;
15774
15846
  } catch (error) {
@@ -15808,9 +15880,8 @@ async function createSESResources(config2) {
15808
15880
  }
15809
15881
  const configSetName = "wraps-email-tracking";
15810
15882
  const exists = await configurationSetExists(configSetName, config2.region);
15811
- const configSet = exists ? new aws8.sesv2.ConfigurationSet(configSetName, configSetOptions, {
15883
+ const configSet = exists && !config2.skipResourceImports ? new aws8.sesv2.ConfigurationSet(configSetName, configSetOptions, {
15812
15884
  import: configSetName
15813
- // Import existing configuration set
15814
15885
  }) : new aws8.sesv2.ConfigurationSet(configSetName, configSetOptions);
15815
15886
  const defaultEventBus = aws8.cloudwatch.getEventBusOutput({
15816
15887
  name: "default"
@@ -15843,9 +15914,9 @@ async function createSESResources(config2) {
15843
15914
  }
15844
15915
  },
15845
15916
  {
15846
- // Import existing resource if it already exists in AWS
15847
- // This prevents AlreadyExistsException when the resource exists but isn't in Pulumi state
15848
- import: config2.importExistingEventDestination ? `wraps-email-tracking|${eventDestName}` : void 0
15917
+ // Import existing resource if it already exists in AWS but not in Pulumi state.
15918
+ // Skip when skipResourceImports is true (resource already tracked in state).
15919
+ import: config2.importExistingEventDestination && !config2.skipResourceImports ? `wraps-email-tracking|${eventDestName}` : void 0
15849
15920
  }
15850
15921
  );
15851
15922
  }
@@ -15857,12 +15928,11 @@ async function createSESResources(config2) {
15857
15928
  config2.domain,
15858
15929
  config2.region
15859
15930
  );
15860
- domainIdentity = identityExists ? new aws8.sesv2.EmailIdentity(
15931
+ domainIdentity = identityExists && !config2.skipResourceImports ? new aws8.sesv2.EmailIdentity(
15861
15932
  "wraps-email-domain",
15862
15933
  {
15863
15934
  emailIdentity: config2.domain,
15864
15935
  configurationSetName: configSet.configurationSetName,
15865
- // Link configuration set to domain
15866
15936
  dkimSigningAttributes: {
15867
15937
  nextSigningKeyLength: "RSA_2048_BIT"
15868
15938
  },
@@ -15872,7 +15942,6 @@ async function createSESResources(config2) {
15872
15942
  },
15873
15943
  {
15874
15944
  import: config2.domain
15875
- // Import existing identity
15876
15945
  }
15877
15946
  ) : new aws8.sesv2.EmailIdentity("wraps-email-domain", {
15878
15947
  emailIdentity: config2.domain,
@@ -16106,7 +16175,7 @@ async function deployEmailStack(config2) {
16106
16175
  }
16107
16176
  let sesResources;
16108
16177
  if (emailConfig.tracking?.enabled || emailConfig.eventTracking?.enabled) {
16109
- const shouldImportEventDest = emailConfig.eventTracking?.enabled && await eventDestinationExists(
16178
+ const shouldImportEventDest = !config2.skipResourceImports && emailConfig.eventTracking?.enabled && await eventDestinationExists(
16110
16179
  "wraps-email-tracking",
16111
16180
  "wraps-email-eventbridge",
16112
16181
  config2.region
@@ -16133,8 +16202,10 @@ async function deployEmailStack(config2) {
16133
16202
  // Pass flag to create EventBridge destination
16134
16203
  tlsRequired: emailConfig.tlsRequired,
16135
16204
  // Require TLS encryption for all emails
16136
- importExistingEventDestination: shouldImportEventDest
16205
+ importExistingEventDestination: shouldImportEventDest,
16137
16206
  // Import if exists to avoid AlreadyExistsException
16207
+ skipResourceImports: config2.skipResourceImports
16208
+ // Skip import flags when resources already in Pulumi state
16138
16209
  });
16139
16210
  }
16140
16211
  let dynamoTables;
@@ -16484,6 +16555,7 @@ ${pc17.bold("Current Configuration:")}
16484
16555
  await stack.setConfig("aws:region", { value: region });
16485
16556
  await stack.refresh({ onOutput: () => {
16486
16557
  } });
16558
+ stackConfig.skipResourceImports = true;
16487
16559
  const upResult = await stack.up({ onOutput: () => {
16488
16560
  } });
16489
16561
  const pulumiOutputs = upResult.outputs;
@@ -17178,10 +17250,10 @@ async function withTimeout(promise, timeoutMs, operation) {
17178
17250
  // src/commands/email/destroy.ts
17179
17251
  async function getEmailIdentityInfo(domain, region) {
17180
17252
  try {
17181
- const { SESv2Client: SESv2Client8, GetEmailIdentityCommand: GetEmailIdentityCommand5 } = await import("@aws-sdk/client-sesv2");
17253
+ const { SESv2Client: SESv2Client8, GetEmailIdentityCommand: GetEmailIdentityCommand6 } = await import("@aws-sdk/client-sesv2");
17182
17254
  const ses = new SESv2Client8({ region });
17183
17255
  const response = await ses.send(
17184
- new GetEmailIdentityCommand5({ EmailIdentity: domain })
17256
+ new GetEmailIdentityCommand6({ EmailIdentity: domain })
17185
17257
  );
17186
17258
  return {
17187
17259
  dkimTokens: response.DkimAttributes?.Tokens || [],
@@ -17347,6 +17419,7 @@ async function emailDestroy(options) {
17347
17419
  clack18.log.info("You may need to delete them manually from Route53");
17348
17420
  }
17349
17421
  }
17422
+ let destroyFailed = false;
17350
17423
  try {
17351
17424
  await progress.execute(
17352
17425
  "Destroying email infrastructure (this may take 2-3 minutes)",
@@ -17362,10 +17435,11 @@ async function emailDestroy(options) {
17362
17435
  } catch {
17363
17436
  throw new Error("No email infrastructure found to destroy");
17364
17437
  }
17438
+ await stack.refresh({ onOutput: () => {
17439
+ } });
17365
17440
  await withTimeout(
17366
17441
  stack.destroy({ onOutput: () => {
17367
- } }),
17368
- // Suppress Pulumi output
17442
+ }, continueOnError: true }),
17369
17443
  DEFAULT_PULUMI_TIMEOUT_MS,
17370
17444
  "Pulumi destroy"
17371
17445
  );
@@ -17386,36 +17460,51 @@ async function emailDestroy(options) {
17386
17460
  }
17387
17461
  trackError("DESTROY_FAILED", "email destroy", { step: "destroy" });
17388
17462
  clack18.log.error("Email infrastructure destruction failed");
17389
- throw error;
17463
+ destroyFailed = true;
17464
+ clack18.log.warn(
17465
+ "Some resources may not have been fully removed. You can re-run this command or clean up manually in the AWS console."
17466
+ );
17390
17467
  }
17391
17468
  await deleteConnectionMetadata(identity.accountId, region);
17392
17469
  progress.stop();
17393
- const deletedItems = ["AWS infrastructure"];
17394
- if (shouldCleanDNS && hostedZone) {
17395
- deletedItems.push("Route53 DNS records");
17396
- }
17397
- clack18.outro(pc19.green("Email infrastructure has been removed"));
17398
- if (domain) {
17399
- console.log(`
17470
+ if (destroyFailed) {
17471
+ clack18.outro(
17472
+ pc19.yellow("Email infrastructure partially removed. Metadata cleaned up.")
17473
+ );
17474
+ console.log(
17475
+ `
17476
+ Run ${pc19.cyan("wraps email init")} to redeploy, or clean up remaining resources in the AWS console.
17477
+ `
17478
+ );
17479
+ } else {
17480
+ const deletedItems = ["AWS infrastructure"];
17481
+ if (shouldCleanDNS && hostedZone) {
17482
+ deletedItems.push("Route53 DNS records");
17483
+ }
17484
+ clack18.outro(pc19.green("Email infrastructure has been removed"));
17485
+ if (domain) {
17486
+ console.log(`
17400
17487
  ${pc19.bold("Cleaned up:")}`);
17401
- for (const item of deletedItems) {
17402
- console.log(` ${pc19.green("\u2713")} ${item}`);
17488
+ for (const item of deletedItems) {
17489
+ console.log(` ${pc19.green("\u2713")} ${item}`);
17490
+ }
17491
+ console.log(
17492
+ `
17493
+ ${pc19.dim("Note: SPF record was not deleted. Remove 'include:amazonses.com' manually if needed.")}`
17494
+ );
17403
17495
  }
17404
17496
  console.log(
17405
17497
  `
17406
- ${pc19.dim("Note: SPF record was not deleted. Remove 'include:amazonses.com' manually if needed.")}`
17407
- );
17408
- }
17409
- console.log(
17410
- `
17411
17498
  Run ${pc19.cyan("wraps email init")} to deploy infrastructure again.
17412
17499
  `
17413
- );
17500
+ );
17501
+ }
17414
17502
  trackServiceRemoved("email", {
17415
17503
  reason: "user_initiated",
17416
17504
  region,
17417
17505
  duration_ms: Date.now() - startTime,
17418
- dns_cleaned: shouldCleanDNS
17506
+ dns_cleaned: shouldCleanDNS,
17507
+ partial_failure: destroyFailed
17419
17508
  });
17420
17509
  }
17421
17510
 
@@ -19972,7 +20061,7 @@ Run ${pc25.cyan("wraps email init")} to deploy email infrastructure.
19972
20061
  throw error;
19973
20062
  }
19974
20063
  const domains = await listSESDomains(region);
19975
- const { SESv2Client: SESv2Client8, GetEmailIdentityCommand: GetEmailIdentityCommand5 } = await import("@aws-sdk/client-sesv2");
20064
+ const { SESv2Client: SESv2Client8, GetEmailIdentityCommand: GetEmailIdentityCommand6 } = await import("@aws-sdk/client-sesv2");
19976
20065
  const sesv2Client = new SESv2Client8({ region });
19977
20066
  const metadata = await loadConnectionMetadata(identity.accountId, region);
19978
20067
  const trackedDomains = metadata ? getAllTrackedDomains(metadata) : [];
@@ -19982,7 +20071,7 @@ Run ${pc25.cyan("wraps email init")} to deploy email infrastructure.
19982
20071
  const tracked = trackedMap.get(d.domain);
19983
20072
  try {
19984
20073
  const sesIdentity = await sesv2Client.send(
19985
- new GetEmailIdentityCommand5({ EmailIdentity: d.domain })
20074
+ new GetEmailIdentityCommand6({ EmailIdentity: d.domain })
19986
20075
  );
19987
20076
  return {
19988
20077
  domain: d.domain,
@@ -20853,20 +20942,56 @@ function renderErrorPage(err) {
20853
20942
  init_esm_shims();
20854
20943
  init_events();
20855
20944
  import { createHash } from "crypto";
20856
- import { existsSync as existsSync10 } from "fs";
20857
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile6 } from "fs/promises";
20858
- import { join as join11 } from "path";
20945
+ import { existsSync as existsSync11 } from "fs";
20946
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile7 } from "fs/promises";
20947
+ import { join as join12 } from "path";
20859
20948
  import * as clack27 from "@clack/prompts";
20860
20949
  import pc28 from "picocolors";
20861
20950
  init_config();
20862
20951
  init_errors();
20952
+
20953
+ // src/utils/shared/lockfile.ts
20954
+ init_esm_shims();
20955
+ import { existsSync as existsSync10 } from "fs";
20956
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile6 } from "fs/promises";
20957
+ import { join as join11 } from "path";
20958
+ function getLockfilePath(wrapsDir) {
20959
+ return join11(wrapsDir, ".wraps", "lockfile.json");
20960
+ }
20961
+ async function loadLockfile(wrapsDir) {
20962
+ const path3 = getLockfilePath(wrapsDir);
20963
+ if (!existsSync10(path3)) {
20964
+ return { version: "1.0.0", lastSync: "", templates: {}, workflows: {} };
20965
+ }
20966
+ try {
20967
+ const content = await readFile4(path3, "utf-8");
20968
+ const parsed = JSON.parse(content);
20969
+ if (!parsed.templates) {
20970
+ parsed.templates = {};
20971
+ }
20972
+ if (!parsed.workflows) {
20973
+ parsed.workflows = {};
20974
+ }
20975
+ return parsed;
20976
+ } catch {
20977
+ return { version: "1.0.0", lastSync: "", templates: {}, workflows: {} };
20978
+ }
20979
+ }
20980
+ async function saveLockfile(wrapsDir, lockfile) {
20981
+ const path3 = getLockfilePath(wrapsDir);
20982
+ const dir = join11(path3, "..");
20983
+ await mkdir4(dir, { recursive: true });
20984
+ await writeFile6(path3, JSON.stringify(lockfile, null, 2), "utf-8");
20985
+ }
20986
+
20987
+ // src/commands/email/templates/push.ts
20863
20988
  init_output();
20864
20989
  async function templatesPush(options) {
20865
20990
  const startTime = Date.now();
20866
20991
  const cwd = process.cwd();
20867
- const wrapsDir = join11(cwd, "wraps");
20868
- const configPath = join11(wrapsDir, "wraps.config.ts");
20869
- if (!existsSync10(configPath)) {
20992
+ const wrapsDir = join12(cwd, "wraps");
20993
+ const configPath = join12(wrapsDir, "wraps.config.ts");
20994
+ if (!existsSync11(configPath)) {
20870
20995
  throw errors.wrapsConfigNotFound();
20871
20996
  }
20872
20997
  if (!options.json) {
@@ -20876,8 +21001,8 @@ async function templatesPush(options) {
20876
21001
  progress.start("Loading configuration");
20877
21002
  const config2 = await loadWrapsConfig(wrapsDir);
20878
21003
  progress.succeed("Configuration loaded");
20879
- const templatesDir = join11(wrapsDir, config2.templatesDir || "./templates");
20880
- if (!existsSync10(templatesDir)) {
21004
+ const templatesDir = join12(wrapsDir, config2.templatesDir || "./templates");
21005
+ if (!existsSync11(templatesDir)) {
20881
21006
  throw errors.wrapsConfigNotFound();
20882
21007
  }
20883
21008
  const templateFiles = await discoverTemplates(templatesDir, options.template);
@@ -20887,8 +21012,7 @@ async function templatesPush(options) {
20887
21012
  }
20888
21013
  return;
20889
21014
  }
20890
- const lockfilePath = join11(wrapsDir, ".wraps", "lockfile.json");
20891
- const lockfile = await loadLockfile(lockfilePath);
21015
+ const lockfile = await loadLockfile(wrapsDir);
20892
21016
  const token = await resolveTokenAsync({ token: options.token });
20893
21017
  const remoteTemplateSlugs = await fetchRemoteTemplateSlugs(token, progress);
20894
21018
  const compiled = [];
@@ -20896,8 +21020,8 @@ async function templatesPush(options) {
20896
21020
  const compileErrors = [];
20897
21021
  for (const file of templateFiles) {
20898
21022
  const slug = file.replace(/\.tsx?$/, "");
20899
- const filePath = join11(templatesDir, file);
20900
- const source = await readFile4(filePath, "utf-8");
21023
+ const filePath = join12(templatesDir, file);
21024
+ const source = await readFile5(filePath, "utf-8");
20901
21025
  const sourceHash = sha256(source);
20902
21026
  const localHashMatches = lockfile.templates[slug]?.localHash === sourceHash;
20903
21027
  const existsRemotely = remoteTemplateSlugs === null || remoteTemplateSlugs.has(slug);
@@ -20976,27 +21100,25 @@ async function templatesPush(options) {
20976
21100
  }
20977
21101
  return;
20978
21102
  }
20979
- await pushToSES(compiled, progress);
20980
- const apiResults = await pushToAPI(
20981
- compiled,
20982
- token,
20983
- config2.org,
20984
- progress,
20985
- options.force
20986
- );
21103
+ const sesResults = await pushToSES(compiled, progress);
21104
+ const apiResults = await pushToAPI(compiled, token, progress, options.force);
20987
21105
  for (const t of compiled) {
21106
+ const sesOk = sesResults.find((r) => r.slug === t.slug)?.success;
20988
21107
  const apiResult = apiResults.find((r) => r.slug === t.slug);
20989
- lockfile.templates[t.slug] = {
20990
- id: apiResult?.id,
20991
- localHash: t.sourceHash,
20992
- remoteHash: t.sourceHash,
20993
- sesTemplateName: t.sesTemplateName,
20994
- lastPushed: (/* @__PURE__ */ new Date()).toISOString()
20995
- };
21108
+ const apiOk = apiResult?.success;
21109
+ if (sesOk || apiOk) {
21110
+ lockfile.templates[t.slug] = {
21111
+ id: apiResult?.id,
21112
+ localHash: t.sourceHash,
21113
+ remoteHash: t.sourceHash,
21114
+ sesTemplateName: t.sesTemplateName,
21115
+ lastPushed: (/* @__PURE__ */ new Date()).toISOString()
21116
+ };
21117
+ }
20996
21118
  }
20997
21119
  lockfile.lastSync = (/* @__PURE__ */ new Date()).toISOString();
20998
21120
  lockfile.org = config2.org;
20999
- await saveLockfile(lockfilePath, lockfile);
21121
+ await saveLockfile(wrapsDir, lockfile);
21000
21122
  if (options.json) {
21001
21123
  console.log(
21002
21124
  JSON.stringify({
@@ -21054,11 +21176,11 @@ async function compileTemplate(filePath, slug, source, sourceHash, wrapsDir) {
21054
21176
  }
21055
21177
  });
21056
21178
  const bundledCode = result.outputFiles[0].text;
21057
- const projectRoot = join11(wrapsDir, "..");
21058
- const tmpDir = join11(projectRoot, "node_modules", ".wraps-compiled");
21059
- await mkdir4(tmpDir, { recursive: true });
21060
- const tmpPath = join11(tmpDir, `${slug}.mjs`);
21061
- await writeFile6(tmpPath, bundledCode, "utf-8");
21179
+ const projectRoot = join12(wrapsDir, "..");
21180
+ const tmpDir = join12(projectRoot, "node_modules", ".wraps-compiled");
21181
+ await mkdir5(tmpDir, { recursive: true });
21182
+ const tmpPath = join12(tmpDir, `${slug}.mjs`);
21183
+ await writeFile7(tmpPath, bundledCode, "utf-8");
21062
21184
  const mod = await import(tmpPath);
21063
21185
  const Component = mod.default;
21064
21186
  const subject = mod.subject || slug;
@@ -21156,9 +21278,9 @@ async function pushToSES(templates, progress) {
21156
21278
  UpdateTemplateCommand
21157
21279
  } = await import("@aws-sdk/client-ses");
21158
21280
  const ses = new SESClient5({ region });
21159
- for (const t of templates) {
21160
- progress.start(`Pushing ${pc28.cyan(t.slug)} to SES`);
21161
- try {
21281
+ const settled = await Promise.allSettled(
21282
+ templates.map(async (t) => {
21283
+ progress.start(`Pushing ${pc28.cyan(t.slug)} to SES`);
21162
21284
  const templateData = {
21163
21285
  TemplateName: t.sesTemplateName,
21164
21286
  SubjectPart: t.subject,
@@ -21182,12 +21304,18 @@ async function pushToSES(templates, progress) {
21182
21304
  } else {
21183
21305
  await ses.send(new CreateTemplateCommand({ Template: templateData }));
21184
21306
  }
21185
- results.push({ slug: t.slug, success: true });
21186
21307
  progress.succeed(`Pushed ${pc28.cyan(t.slug)} to SES`);
21187
- } catch (err) {
21188
- const msg = err instanceof Error ? err.message : String(err);
21189
- results.push({ slug: t.slug, success: false });
21190
- progress.fail(`SES push failed for ${t.slug}: ${msg}`);
21308
+ return { slug: t.slug };
21309
+ })
21310
+ );
21311
+ for (let i = 0; i < templates.length; i++) {
21312
+ const result = settled[i];
21313
+ if (result.status === "fulfilled") {
21314
+ results.push({ slug: result.value.slug, success: true });
21315
+ } else {
21316
+ const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
21317
+ results.push({ slug: templates[i].slug, success: false });
21318
+ progress.fail(`SES push failed for ${templates[i].slug}: ${msg}`);
21191
21319
  }
21192
21320
  }
21193
21321
  return results;
@@ -21219,7 +21347,7 @@ async function fetchRemoteTemplateSlugs(token, progress) {
21219
21347
  return null;
21220
21348
  }
21221
21349
  }
21222
- async function pushToAPI(templates, token, _org, progress, force) {
21350
+ async function pushToAPI(templates, token, progress, force) {
21223
21351
  if (!token) {
21224
21352
  progress.info(
21225
21353
  "No API token \u2014 skipping dashboard sync. Run: wraps auth login"
@@ -21341,22 +21469,6 @@ async function pushToAPI(templates, token, _org, progress, force) {
21341
21469
  }
21342
21470
  return results;
21343
21471
  }
21344
- async function loadLockfile(path3) {
21345
- if (!existsSync10(path3)) {
21346
- return { version: "1.0.0", lastSync: "", templates: {} };
21347
- }
21348
- try {
21349
- const content = await readFile4(path3, "utf-8");
21350
- return JSON.parse(content);
21351
- } catch {
21352
- return { version: "1.0.0", lastSync: "", templates: {} };
21353
- }
21354
- }
21355
- async function saveLockfile(path3, lockfile) {
21356
- const dir = join11(path3, "..");
21357
- await mkdir4(dir, { recursive: true });
21358
- await writeFile6(path3, JSON.stringify(lockfile, null, 2), "utf-8");
21359
- }
21360
21472
  function sha256(content) {
21361
21473
  return createHash("sha256").update(content).digest("hex");
21362
21474
  }
@@ -22696,6 +22808,7 @@ ${pc29.bold("Cost Impact:")}`);
22696
22808
  await stack.setConfig("aws:region", { value: region });
22697
22809
  await stack.refresh({ onOutput: () => {
22698
22810
  } });
22811
+ stackConfig.skipResourceImports = true;
22699
22812
  const upResult = await stack.up({ onOutput: () => {
22700
22813
  } });
22701
22814
  const pulumiOutputs = upResult.outputs;
@@ -23085,9 +23198,8 @@ ${pc29.green("\u2713")} ${pc29.bold("Upgrade complete!")}
23085
23198
  // src/commands/email/workflows/push.ts
23086
23199
  init_esm_shims();
23087
23200
  init_events();
23088
- import { existsSync as existsSync12 } from "fs";
23089
- import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile8 } from "fs/promises";
23090
- import { join as join13 } from "path";
23201
+ import { existsSync as existsSync13 } from "fs";
23202
+ import { join as join14 } from "path";
23091
23203
  import * as clack29 from "@clack/prompts";
23092
23204
  import pc30 from "picocolors";
23093
23205
 
@@ -23276,11 +23388,11 @@ function assignPositions(steps, transitions) {
23276
23388
  // src/utils/email/workflow-ts.ts
23277
23389
  init_esm_shims();
23278
23390
  import { createHash as createHash2 } from "crypto";
23279
- import { existsSync as existsSync11 } from "fs";
23280
- import { mkdir as mkdir5, readdir as readdir3, readFile as readFile5, writeFile as writeFile7 } from "fs/promises";
23281
- import { basename, join as join12 } from "path";
23391
+ import { existsSync as existsSync12 } from "fs";
23392
+ import { mkdir as mkdir6, readdir as readdir3, readFile as readFile6, writeFile as writeFile8 } from "fs/promises";
23393
+ import { basename, join as join13 } from "path";
23282
23394
  async function discoverWorkflows(dir, filter) {
23283
- if (!existsSync11(dir)) {
23395
+ if (!existsSync12(dir)) {
23284
23396
  return [];
23285
23397
  }
23286
23398
  const entries = await readdir3(dir);
@@ -23300,11 +23412,11 @@ async function discoverWorkflows(dir, filter) {
23300
23412
  }
23301
23413
  async function parseWorkflowTs(filePath, wrapsDir) {
23302
23414
  const { build: build2 } = await import("esbuild");
23303
- const source = await readFile5(filePath, "utf-8");
23415
+ const source = await readFile6(filePath, "utf-8");
23304
23416
  const sourceHash = createHash2("sha256").update(source).digest("hex");
23305
23417
  const slug = basename(filePath, ".ts");
23306
- const shimDir = join12(wrapsDir, ".wraps", "_shims");
23307
- await mkdir5(shimDir, { recursive: true });
23418
+ const shimDir = join13(wrapsDir, ".wraps", "_shims");
23419
+ await mkdir6(shimDir, { recursive: true });
23308
23420
  const clientShimContent = `
23309
23421
  // Identity functions for workflow definitions
23310
23422
  export const defineWorkflow = (def) => def;
@@ -23446,8 +23558,8 @@ function durationToSeconds(duration) {
23446
23558
  return seconds > 0 ? seconds : undefined;
23447
23559
  }
23448
23560
  `;
23449
- await writeFile7(
23450
- join12(shimDir, "wraps-client-shim.mjs"),
23561
+ await writeFile8(
23562
+ join13(shimDir, "wraps-client-shim.mjs"),
23451
23563
  clientShimContent,
23452
23564
  "utf-8"
23453
23565
  );
@@ -23459,14 +23571,14 @@ function durationToSeconds(duration) {
23459
23571
  platform: "node",
23460
23572
  target: "node20",
23461
23573
  alias: {
23462
- "@wraps.dev/client": join12(shimDir, "wraps-client-shim.mjs")
23574
+ "@wraps.dev/client": join13(shimDir, "wraps-client-shim.mjs")
23463
23575
  }
23464
23576
  });
23465
23577
  const bundledCode = result.outputFiles[0].text;
23466
- const tmpDir = join12(wrapsDir, ".wraps", "_workflows");
23467
- await mkdir5(tmpDir, { recursive: true });
23468
- const tmpPath = join12(tmpDir, `${slug}.mjs`);
23469
- await writeFile7(tmpPath, bundledCode, "utf-8");
23578
+ const tmpDir = join13(wrapsDir, ".wraps", "_workflows");
23579
+ await mkdir6(tmpDir, { recursive: true });
23580
+ const tmpPath = join13(tmpDir, `${slug}.mjs`);
23581
+ await writeFile8(tmpPath, bundledCode, "utf-8");
23470
23582
  const mod = await import(`${tmpPath}?t=${Date.now()}`);
23471
23583
  const definition = mod.default;
23472
23584
  if (!definition || typeof definition !== "object") {
@@ -23817,9 +23929,9 @@ init_output();
23817
23929
  async function workflowsPush(options) {
23818
23930
  const startTime = Date.now();
23819
23931
  const cwd = process.cwd();
23820
- const wrapsDir = join13(cwd, "wraps");
23821
- const configPath = join13(wrapsDir, "wraps.config.ts");
23822
- if (!existsSync12(configPath)) {
23932
+ const wrapsDir = join14(cwd, "wraps");
23933
+ const configPath = join14(wrapsDir, "wraps.config.ts");
23934
+ if (!existsSync13(configPath)) {
23823
23935
  throw errors.wrapsConfigNotFound();
23824
23936
  }
23825
23937
  if (!options.json) {
@@ -23829,8 +23941,8 @@ async function workflowsPush(options) {
23829
23941
  progress.start("Loading configuration");
23830
23942
  const config2 = await loadWrapsConfig(wrapsDir);
23831
23943
  progress.succeed("Configuration loaded");
23832
- const workflowsDir = join13(wrapsDir, config2.workflowsDir || "./workflows");
23833
- if (!existsSync12(workflowsDir)) {
23944
+ const workflowsDir = join14(wrapsDir, config2.workflowsDir || "./workflows");
23945
+ if (!existsSync13(workflowsDir)) {
23834
23946
  if (options.json) {
23835
23947
  console.log(
23836
23948
  JSON.stringify({
@@ -23859,11 +23971,10 @@ async function workflowsPush(options) {
23859
23971
  }
23860
23972
  return;
23861
23973
  }
23862
- const lockfilePath = join13(wrapsDir, ".wraps", "lockfile.json");
23863
- const lockfile = await loadLockfile2(lockfilePath);
23864
- const templatesDir = join13(wrapsDir, config2.templatesDir || "./templates");
23974
+ const lockfile = await loadLockfile(wrapsDir);
23975
+ const templatesDir = join14(wrapsDir, config2.templatesDir || "./templates");
23865
23976
  let localTemplateSlugs;
23866
- if (existsSync12(templatesDir)) {
23977
+ if (existsSync13(templatesDir)) {
23867
23978
  const templateFiles = await discoverTemplates(templatesDir);
23868
23979
  localTemplateSlugs = new Set(
23869
23980
  templateFiles.map((f) => f.replace(/\.tsx?$/, ""))
@@ -23875,7 +23986,7 @@ async function workflowsPush(options) {
23875
23986
  const validationErrors = [];
23876
23987
  for (const file of workflowFiles) {
23877
23988
  const slug = file.replace(/\.ts$/, "");
23878
- const filePath = join13(workflowsDir, file);
23989
+ const filePath = join14(workflowsDir, file);
23879
23990
  progress.start(`Processing ${pc30.cyan(slug)}`);
23880
23991
  try {
23881
23992
  const parsed = await parseWorkflowTs(filePath, wrapsDir);
@@ -23998,28 +24109,21 @@ async function workflowsPush(options) {
23998
24109
  return;
23999
24110
  }
24000
24111
  const token = await resolveTokenAsync({ token: options.token });
24001
- const apiResults = await pushToAPI2(
24002
- toProcess,
24003
- token,
24004
- config2.org,
24005
- progress,
24006
- options.force
24007
- );
24008
- if (!lockfile.workflows) {
24009
- lockfile.workflows = {};
24010
- }
24112
+ const apiResults = await pushToAPI2(toProcess, token, progress, options.force);
24011
24113
  for (const w of toProcess) {
24012
24114
  const apiResult = apiResults.find((r) => r.slug === w.slug);
24013
- lockfile.workflows[w.slug] = {
24014
- id: apiResult?.id,
24015
- localHash: w.parsed.sourceHash,
24016
- remoteHash: w.parsed.sourceHash,
24017
- lastPushed: (/* @__PURE__ */ new Date()).toISOString()
24018
- };
24115
+ if (apiResult?.success) {
24116
+ lockfile.workflows[w.slug] = {
24117
+ id: apiResult?.id,
24118
+ localHash: w.parsed.sourceHash,
24119
+ remoteHash: w.parsed.sourceHash,
24120
+ lastPushed: (/* @__PURE__ */ new Date()).toISOString()
24121
+ };
24122
+ }
24019
24123
  }
24020
24124
  lockfile.lastSync = (/* @__PURE__ */ new Date()).toISOString();
24021
24125
  lockfile.org = config2.org;
24022
- await saveLockfile2(lockfilePath, lockfile);
24126
+ await saveLockfile(wrapsDir, lockfile);
24023
24127
  const pushed = apiResults.filter((r) => r.success);
24024
24128
  const conflicts = apiResults.filter((r) => r.conflict);
24025
24129
  if (options.json) {
@@ -24068,7 +24172,7 @@ async function workflowsPush(options) {
24068
24172
  conflict_count: conflicts.length
24069
24173
  });
24070
24174
  }
24071
- async function pushToAPI2(workflows, token, _org, progress, force) {
24175
+ async function pushToAPI2(workflows, token, progress, force) {
24072
24176
  if (!token) {
24073
24177
  progress.info(
24074
24178
  "No API token \u2014 skipping dashboard sync. Run: wraps auth login"
@@ -24192,32 +24296,12 @@ async function pushToAPI2(workflows, token, _org, progress, force) {
24192
24296
  }
24193
24297
  return results;
24194
24298
  }
24195
- async function loadLockfile2(path3) {
24196
- if (!existsSync12(path3)) {
24197
- return { version: "1.0.0", lastSync: "", templates: {}, workflows: {} };
24198
- }
24199
- try {
24200
- const content = await readFile6(path3, "utf-8");
24201
- const parsed = JSON.parse(content);
24202
- if (!parsed.workflows) {
24203
- parsed.workflows = {};
24204
- }
24205
- return parsed;
24206
- } catch {
24207
- return { version: "1.0.0", lastSync: "", templates: {}, workflows: {} };
24208
- }
24209
- }
24210
- async function saveLockfile2(path3, lockfile) {
24211
- const dir = join13(path3, "..");
24212
- await mkdir6(dir, { recursive: true });
24213
- await writeFile8(path3, JSON.stringify(lockfile, null, 2), "utf-8");
24214
- }
24215
24299
 
24216
24300
  // src/commands/email/workflows/validate.ts
24217
24301
  init_esm_shims();
24218
24302
  init_events();
24219
- import { existsSync as existsSync13 } from "fs";
24220
- import { join as join14 } from "path";
24303
+ import { existsSync as existsSync14 } from "fs";
24304
+ import { join as join15 } from "path";
24221
24305
  import * as clack30 from "@clack/prompts";
24222
24306
  import pc31 from "picocolors";
24223
24307
  init_errors();
@@ -24225,9 +24309,9 @@ init_output();
24225
24309
  async function workflowsValidate(options) {
24226
24310
  const startTime = Date.now();
24227
24311
  const cwd = process.cwd();
24228
- const wrapsDir = join14(cwd, "wraps");
24229
- const configPath = join14(wrapsDir, "wraps.config.ts");
24230
- if (!existsSync13(configPath)) {
24312
+ const wrapsDir = join15(cwd, "wraps");
24313
+ const configPath = join15(wrapsDir, "wraps.config.ts");
24314
+ if (!existsSync14(configPath)) {
24231
24315
  throw errors.wrapsConfigNotFound();
24232
24316
  }
24233
24317
  if (!options.json) {
@@ -24237,8 +24321,8 @@ async function workflowsValidate(options) {
24237
24321
  progress.start("Loading configuration");
24238
24322
  const config2 = await loadWrapsConfig(wrapsDir);
24239
24323
  progress.succeed("Configuration loaded");
24240
- const workflowsDir = join14(wrapsDir, config2.workflowsDir || "./workflows");
24241
- if (!existsSync13(workflowsDir)) {
24324
+ const workflowsDir = join15(wrapsDir, config2.workflowsDir || "./workflows");
24325
+ if (!existsSync14(workflowsDir)) {
24242
24326
  if (options.json) {
24243
24327
  console.log(
24244
24328
  JSON.stringify({
@@ -24267,9 +24351,9 @@ async function workflowsValidate(options) {
24267
24351
  }
24268
24352
  return;
24269
24353
  }
24270
- const templatesDir = join14(wrapsDir, config2.templatesDir || "./templates");
24354
+ const templatesDir = join15(wrapsDir, config2.templatesDir || "./templates");
24271
24355
  let localTemplateSlugs;
24272
- if (existsSync13(templatesDir)) {
24356
+ if (existsSync14(templatesDir)) {
24273
24357
  const templateFiles = await discoverTemplates(templatesDir);
24274
24358
  localTemplateSlugs = new Set(
24275
24359
  templateFiles.map((f) => f.replace(/\.tsx?$/, ""))
@@ -24279,7 +24363,7 @@ async function workflowsValidate(options) {
24279
24363
  const parseErrors = [];
24280
24364
  for (const file of workflowFiles) {
24281
24365
  const slug = file.replace(/\.ts$/, "");
24282
- const filePath = join14(workflowsDir, file);
24366
+ const filePath = join15(workflowsDir, file);
24283
24367
  progress.start(`Validating ${pc31.cyan(slug)}`);
24284
24368
  try {
24285
24369
  const parsed = await parseWorkflowTs(filePath, wrapsDir);
@@ -25155,6 +25239,11 @@ async function deployEventBridge(metadata, region, identity, webhookSecret, prog
25155
25239
  await stack.setConfig("aws:region", { value: region });
25156
25240
  await stack.refresh({ onOutput: () => {
25157
25241
  } });
25242
+ const stackState = await stack.exportStack();
25243
+ const resourceCount = stackState.deployment?.resources?.length ?? 0;
25244
+ if (resourceCount > 1) {
25245
+ stackConfig.skipResourceImports = true;
25246
+ }
25158
25247
  await stack.up({ onOutput: () => {
25159
25248
  } });
25160
25249
  });
@@ -25626,6 +25715,11 @@ Run ${pc34.cyan("wraps email init")} or ${pc34.cyan("wraps sms init")} first.
25626
25715
  await stack.setConfig("aws:region", { value: region });
25627
25716
  await stack.refresh({ onOutput: () => {
25628
25717
  } });
25718
+ const stackState = await stack.exportStack();
25719
+ const resourceCount = stackState.deployment?.resources?.length ?? 0;
25720
+ if (resourceCount > 1) {
25721
+ stackConfig.skipResourceImports = true;
25722
+ }
25629
25723
  await stack.up({ onOutput: () => {
25630
25724
  } });
25631
25725
  });
@@ -26629,7 +26723,7 @@ import { Router as createRouter2 } from "express";
26629
26723
  init_esm_shims();
26630
26724
  init_assume_role();
26631
26725
  import { GetSendQuotaCommand, SESClient as SESClient4 } from "@aws-sdk/client-ses";
26632
- import { GetEmailIdentityCommand as GetEmailIdentityCommand3, SESv2Client as SESv2Client6 } from "@aws-sdk/client-sesv2";
26726
+ import { GetEmailIdentityCommand as GetEmailIdentityCommand4, SESv2Client as SESv2Client6 } from "@aws-sdk/client-sesv2";
26633
26727
  async function fetchSendQuota(roleArn, region) {
26634
26728
  const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
26635
26729
  const ses = new SESClient4({ region, credentials });
@@ -26644,7 +26738,7 @@ async function fetchDomainInfo(roleArn, region, domain) {
26644
26738
  const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
26645
26739
  const sesv22 = new SESv2Client6({ region, credentials });
26646
26740
  const response = await sesv22.send(
26647
- new GetEmailIdentityCommand3({
26741
+ new GetEmailIdentityCommand4({
26648
26742
  EmailIdentity: domain
26649
26743
  })
26650
26744
  );
@@ -27470,7 +27564,7 @@ init_esm_shims();
27470
27564
  init_assume_role();
27471
27565
  import {
27472
27566
  GetConfigurationSetCommand,
27473
- GetEmailIdentityCommand as GetEmailIdentityCommand4,
27567
+ GetEmailIdentityCommand as GetEmailIdentityCommand5,
27474
27568
  SESv2Client as SESv2Client7
27475
27569
  } from "@aws-sdk/client-sesv2";
27476
27570
  async function fetchConfigurationSet(roleArn, region, configSetName) {
@@ -27507,7 +27601,7 @@ async function fetchEmailIdentity(roleArn, region, identityName) {
27507
27601
  const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
27508
27602
  const sesv22 = new SESv2Client7({ region, credentials });
27509
27603
  const response = await sesv22.send(
27510
- new GetEmailIdentityCommand4({
27604
+ new GetEmailIdentityCommand5({
27511
27605
  EmailIdentity: identityName
27512
27606
  })
27513
27607
  );
@@ -32950,7 +33044,7 @@ if (nodeMajorVersion < 20) {
32950
33044
  var __filename2 = fileURLToPath5(import.meta.url);
32951
33045
  var __dirname3 = dirname3(__filename2);
32952
33046
  var packageJson = JSON.parse(
32953
- readFileSync3(join15(__dirname3, "../package.json"), "utf-8")
33047
+ readFileSync3(join16(__dirname3, "../package.json"), "utf-8")
32954
33048
  );
32955
33049
  var VERSION = packageJson.version;
32956
33050
  setupTabCompletion();