@wraps.dev/cli 2.15.1 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -65,6 +65,7 @@ var init_ci_detection = __esm({
65
65
  // src/utils/shared/s3-state.ts
66
66
  var s3_state_exports = {};
67
67
  __export(s3_state_exports, {
68
+ clearS3StackLocks: () => clearS3StackLocks,
68
69
  deleteMetadata: () => deleteMetadata,
69
70
  downloadMetadata: () => downloadMetadata,
70
71
  ensureStateBucket: () => ensureStateBucket,
@@ -196,6 +197,27 @@ async function deleteMetadata(bucketName, accountId, region) {
196
197
  })
197
198
  );
198
199
  }
200
+ async function clearS3StackLocks(accountId, region) {
201
+ const { S3Client: S3Client2, ListObjectsV2Command: ListObjectsV2Command2, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
202
+ const client = new S3Client2({ region });
203
+ const bucketName = getStateBucketName(accountId, region);
204
+ const prefix = ".pulumi/locks/";
205
+ const response = await client.send(
206
+ new ListObjectsV2Command2({ Bucket: bucketName, Prefix: prefix })
207
+ );
208
+ const lockObjects = response.Contents ?? [];
209
+ if (lockObjects.length === 0) {
210
+ return 0;
211
+ }
212
+ for (const obj of lockObjects) {
213
+ if (obj.Key) {
214
+ await client.send(
215
+ new DeleteObjectCommand({ Bucket: bucketName, Key: obj.Key })
216
+ );
217
+ }
218
+ }
219
+ return lockObjects.length;
220
+ }
199
221
  async function downloadMetadata(bucketName, accountId, region) {
200
222
  const { S3Client: S3Client2, GetObjectCommand: GetObjectCommand2 } = await import("@aws-sdk/client-s3");
201
223
  const client = new S3Client2({ region });
@@ -302,8 +324,16 @@ var init_s3_state = __esm({
302
324
  });
303
325
 
304
326
  // src/utils/shared/fs.ts
327
+ var fs_exports = {};
328
+ __export(fs_exports, {
329
+ clearLocalStackLocks: () => clearLocalStackLocks,
330
+ ensurePulumiWorkDir: () => ensurePulumiWorkDir,
331
+ ensureWrapsDir: () => ensureWrapsDir,
332
+ getPulumiWorkDir: () => getPulumiWorkDir,
333
+ getWrapsDir: () => getWrapsDir
334
+ });
305
335
  import { existsSync as existsSync2 } from "fs";
306
- import { mkdir } from "fs/promises";
336
+ import { mkdir, readdir as readdir2, rm } from "fs/promises";
307
337
  import { homedir } from "os";
308
338
  import { join as join2 } from "path";
309
339
  function getWrapsDir() {
@@ -318,6 +348,27 @@ async function ensureWrapsDir() {
318
348
  await mkdir(wrapsDir, { recursive: true });
319
349
  }
320
350
  }
351
+ async function clearLocalStackLocks() {
352
+ const locksDir = join2(getPulumiWorkDir(), ".pulumi", "locks");
353
+ if (!existsSync2(locksDir)) {
354
+ return 0;
355
+ }
356
+ let count = 0;
357
+ async function walkAndDelete(dir) {
358
+ const entries = await readdir2(dir, { withFileTypes: true });
359
+ for (const entry of entries) {
360
+ const fullPath = join2(dir, entry.name);
361
+ if (entry.isDirectory()) {
362
+ await walkAndDelete(fullPath);
363
+ } else if (entry.name.endsWith(".json")) {
364
+ await rm(fullPath);
365
+ count++;
366
+ }
367
+ }
368
+ }
369
+ await walkAndDelete(locksDir);
370
+ return count;
371
+ }
321
372
  async function ensurePulumiWorkDir(options) {
322
373
  await ensureWrapsDir();
323
374
  const pulumiDir = getPulumiWorkDir();
@@ -4296,8 +4347,8 @@ async function listConnections() {
4296
4347
  return [];
4297
4348
  }
4298
4349
  try {
4299
- const { readdir: readdir4 } = await import("fs/promises");
4300
- const files = await readdir4(connectionsDir);
4350
+ const { readdir: readdir5 } = await import("fs/promises");
4351
+ const files = await readdir5(connectionsDir);
4301
4352
  const connections = [];
4302
4353
  for (const file of files) {
4303
4354
  if (file.endsWith(".json")) {
@@ -10113,6 +10164,48 @@ async function previewWithResourceChanges(stack, options) {
10113
10164
  resourceChanges
10114
10165
  };
10115
10166
  }
10167
+ async function clearStackLocks(accountId, region) {
10168
+ const backendUrl = process.env.PULUMI_BACKEND_URL || "";
10169
+ if (backendUrl.startsWith("s3://")) {
10170
+ const { clearS3StackLocks: clearS3StackLocks2 } = await Promise.resolve().then(() => (init_s3_state(), s3_state_exports));
10171
+ return clearS3StackLocks2(accountId, region);
10172
+ }
10173
+ const { clearLocalStackLocks: clearLocalStackLocks2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
10174
+ return clearLocalStackLocks2();
10175
+ }
10176
+ async function withLockRetry(fn, options) {
10177
+ try {
10178
+ return await fn();
10179
+ } catch (error) {
10180
+ if (!(error instanceof Error)) {
10181
+ throw error;
10182
+ }
10183
+ const parsed = parsePulumiError(error);
10184
+ if (parsed.code !== "STACK_LOCKED") {
10185
+ throw error;
10186
+ }
10187
+ const clack51 = await import("@clack/prompts");
10188
+ const pc54 = (await import("picocolors")).default;
10189
+ if (options.autoConfirm) {
10190
+ clack51.log.warn(
10191
+ "Stack is locked from a previous interrupted run. Auto-clearing..."
10192
+ );
10193
+ } else {
10194
+ const shouldClear = await clack51.confirm({
10195
+ message: `Stack is locked from a previous interrupted run. ${pc54.yellow("Clear the stale lock and retry?")}`,
10196
+ initialValue: true
10197
+ });
10198
+ if (clack51.isCancel(shouldClear) || !shouldClear) {
10199
+ throw errors.stackLocked();
10200
+ }
10201
+ }
10202
+ const cleared = await clearStackLocks(options.accountId, options.region);
10203
+ clack51.log.info(
10204
+ `Cleared ${cleared} lock file${cleared === 1 ? "" : "s"}. Retrying...`
10205
+ );
10206
+ return fn();
10207
+ }
10208
+ }
10116
10209
 
10117
10210
  // src/commands/cdn/destroy.ts
10118
10211
  async function cdnDestroy(options) {
@@ -10303,8 +10396,12 @@ async function cdnDestroy(options) {
10303
10396
  } catch (_error) {
10304
10397
  throw new Error("No CDN infrastructure found to destroy");
10305
10398
  }
10306
- await stack.destroy({ onOutput: () => {
10307
- } });
10399
+ await withLockRetry(() => stack.destroy({ onOutput: () => {
10400
+ } }), {
10401
+ accountId: identity.accountId,
10402
+ region,
10403
+ autoConfirm: options.force
10404
+ });
10308
10405
  await stack.workspace.removeStack(stackName);
10309
10406
  }
10310
10407
  );
@@ -10319,10 +10416,6 @@ async function cdnDestroy(options) {
10319
10416
  }
10320
10417
  process.exit(0);
10321
10418
  }
10322
- if (msg.includes("stack is currently locked")) {
10323
- trackError("STACK_LOCKED", "storage destroy", { step: "destroy" });
10324
- throw errors.stackLocked();
10325
- }
10326
10419
  trackError("DESTROY_FAILED", "storage destroy", { step: "destroy" });
10327
10420
  clack9.log.error("CDN infrastructure destruction failed");
10328
10421
  throw error;
@@ -11712,8 +11805,11 @@ ${pc11.yellow(pc11.bold("Configuration Notes:"))}`);
11712
11805
  `wraps-cdn-${identity.accountId}-${region}`
11713
11806
  );
11714
11807
  await stack.setConfig("aws:region", { value: region });
11715
- const upResult = await stack.up({ onOutput: () => {
11716
- } });
11808
+ const upResult = await withLockRetry(
11809
+ () => stack.up({ onOutput: () => {
11810
+ } }),
11811
+ { accountId: identity.accountId, region, autoConfirm: options.yes }
11812
+ );
11717
11813
  const pulumiOutputs = upResult.outputs;
11718
11814
  return {
11719
11815
  roleArn: pulumiOutputs.roleArn?.value,
@@ -11739,10 +11835,6 @@ ${pc11.yellow(pc11.bold("Configuration Notes:"))}`);
11739
11835
  region,
11740
11836
  duration_ms: Date.now() - startTime
11741
11837
  });
11742
- if (msg.includes("stack is currently locked")) {
11743
- trackError("STACK_LOCKED", "storage:init", { step: "deploy" });
11744
- throw errors.stackLocked();
11745
- }
11746
11838
  trackError("DEPLOYMENT_FAILED", "storage:init", { step: "deploy" });
11747
11839
  throw new Error(`Pulumi deployment failed: ${msg}`);
11748
11840
  }
@@ -18190,11 +18282,14 @@ async function emailDestroy(options) {
18190
18282
  }
18191
18283
  await stack.refresh({ onOutput: () => {
18192
18284
  } });
18193
- await withTimeout(
18194
- stack.destroy({ onOutput: () => {
18195
- }, continueOnError: true }),
18196
- DEFAULT_PULUMI_TIMEOUT_MS,
18197
- "Pulumi destroy"
18285
+ await withLockRetry(
18286
+ () => withTimeout(
18287
+ stack.destroy({ onOutput: () => {
18288
+ }, continueOnError: true }),
18289
+ DEFAULT_PULUMI_TIMEOUT_MS,
18290
+ "Pulumi destroy"
18291
+ ),
18292
+ { accountId: identity.accountId, region, autoConfirm: options.force }
18198
18293
  );
18199
18294
  await stack.workspace.removeStack(stackName);
18200
18295
  }
@@ -18207,10 +18302,6 @@ async function emailDestroy(options) {
18207
18302
  await deleteConnectionMetadata(identity.accountId, region);
18208
18303
  process.exit(0);
18209
18304
  }
18210
- if (msg.includes("stack is currently locked")) {
18211
- trackError("STACK_LOCKED", "email destroy", { step: "destroy" });
18212
- throw errors.stackLocked();
18213
- }
18214
18305
  trackError("DESTROY_FAILED", "email destroy", { step: "destroy" });
18215
18306
  clack18.log.error("Email infrastructure destruction failed");
18216
18307
  destroyFailed = true;
@@ -19460,8 +19551,8 @@ async function inboundInit(options) {
19460
19551
  if (!domain) {
19461
19552
  throw errors.inboundRequiresOutbound();
19462
19553
  }
19463
- const subdomain = options.subdomain || (options.yes ? "inbound" : await promptInboundSubdomain(domain));
19464
- const receivingDomain = `${subdomain}.${domain}`;
19554
+ const subdomain = options.root ? "" : options.subdomain ?? (options.yes ? "inbound" : await promptInboundSubdomain(domain));
19555
+ const receivingDomain = subdomain ? `${subdomain}.${domain}` : domain;
19465
19556
  clack20.log.info(`Receiving domain: ${pc21.cyan(receivingDomain)}`);
19466
19557
  const webhookUrl = options.webhookUrl || (options.yes ? void 0 : await promptWebhookUrl());
19467
19558
  const webhookSecret = randomBytes3(32).toString("hex");
@@ -19525,22 +19616,25 @@ async function inboundInit(options) {
19525
19616
  );
19526
19617
  await stack.setConfig("aws:region", { value: region });
19527
19618
  const pulumiOutput = [];
19528
- await withTimeout(
19529
- stack.up({
19530
- onOutput: (msg) => {
19531
- pulumiOutput.push(msg);
19619
+ await withLockRetry(
19620
+ () => withTimeout(
19621
+ stack.up({
19622
+ onOutput: (msg) => {
19623
+ pulumiOutput.push(msg);
19624
+ }
19625
+ }),
19626
+ DEFAULT_PULUMI_TIMEOUT_MS,
19627
+ "Pulumi deployment"
19628
+ ).catch((error) => {
19629
+ if (pulumiOutput.length > 0) {
19630
+ const fullOutput = pulumiOutput.join("");
19631
+ clack20.log.error("Pulumi deployment output:");
19632
+ console.error(fullOutput);
19532
19633
  }
19634
+ throw error;
19533
19635
  }),
19534
- DEFAULT_PULUMI_TIMEOUT_MS,
19535
- "Pulumi deployment"
19536
- ).catch((error) => {
19537
- if (pulumiOutput.length > 0) {
19538
- const fullOutput = pulumiOutput.join("");
19539
- clack20.log.error("Pulumi deployment output:");
19540
- console.error(fullOutput);
19541
- }
19542
- throw error;
19543
- });
19636
+ { accountId: identity.accountId, region, autoConfirm: options.yes }
19637
+ );
19544
19638
  });
19545
19639
  await progress.execute("Creating SES receipt rules", async () => {
19546
19640
  await createReceiptRuleSet(region);
@@ -19766,11 +19860,14 @@ Deploy first: ${pc21.cyan("wraps email inbound init")}
19766
19860
  }
19767
19861
  );
19768
19862
  await stack.setConfig("aws:region", { value: region });
19769
- await withTimeout(
19770
- stack.up({ onOutput: () => {
19771
- } }),
19772
- DEFAULT_PULUMI_TIMEOUT_MS,
19773
- "Pulumi deployment"
19863
+ await withLockRetry(
19864
+ () => withTimeout(
19865
+ stack.up({ onOutput: () => {
19866
+ } }),
19867
+ DEFAULT_PULUMI_TIMEOUT_MS,
19868
+ "Pulumi deployment"
19869
+ ),
19870
+ { accountId: identity.accountId, region, autoConfirm: options.force }
19774
19871
  );
19775
19872
  });
19776
19873
  await progress.execute("Saving configuration", async () => {
@@ -19820,7 +19917,7 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19820
19917
  const inboundDomains = emailConfig.inboundDomains ?? [];
19821
19918
  const activeRuleSet = await getActiveReceiptRuleSet(region);
19822
19919
  const domainList = inboundDomains.length > 0 ? inboundDomains.map((d) => d.receivingDomain) : [
19823
- inbound.receivingDomain || `${inbound.subdomain}.${emailConfig.domain}`
19920
+ inbound.receivingDomain || (inbound.subdomain ? `${inbound.subdomain}.${emailConfig.domain}` : emailConfig.domain || "")
19824
19921
  ];
19825
19922
  if (isJsonMode()) {
19826
19923
  jsonSuccess("email.inbound.status", {
@@ -19883,7 +19980,7 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19883
19980
  const inbound = emailConfig.inbound;
19884
19981
  const inboundDomains = emailConfig.inboundDomains ?? [];
19885
19982
  const domainList = inboundDomains.length > 0 ? inboundDomains.map((d) => d.receivingDomain) : [
19886
- inbound.receivingDomain || `${inbound.subdomain}.${emailConfig.domain}`
19983
+ inbound.receivingDomain || (inbound.subdomain ? `${inbound.subdomain}.${emailConfig.domain}` : emailConfig.domain || "")
19887
19984
  ];
19888
19985
  let allPassed = true;
19889
19986
  const domainChecks = {};
@@ -19998,7 +20095,7 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19998
20095
  }
19999
20096
  const emailConfig = metadata.services.email.config;
20000
20097
  const inbound = emailConfig.inbound;
20001
- const receivingDomain = inbound.receivingDomain || `${inbound.subdomain}.${emailConfig.domain}`;
20098
+ const receivingDomain = inbound.receivingDomain || (inbound.subdomain ? `${inbound.subdomain}.${emailConfig.domain}` : emailConfig.domain || "");
20002
20099
  const bucketName = inbound.bucketName || `wraps-inbound-${identity.accountId}-${region}`;
20003
20100
  const testRecipient = `test@${receivingDomain}`;
20004
20101
  const testSubject = `Wraps Inbound Test - ${(/* @__PURE__ */ new Date()).toISOString()}`;
@@ -20176,8 +20273,8 @@ Deploy first: ${pc21.cyan("wraps email inbound init")}
20176
20273
  parentDomain = selected;
20177
20274
  }
20178
20275
  }
20179
- const subdomain = options.subdomain || (options.yes ? "inbound" : await promptInboundSubdomain(parentDomain));
20180
- const receivingDomain = `${subdomain}.${parentDomain}`;
20276
+ const subdomain = options.root ? "" : options.subdomain ?? (options.yes ? "inbound" : await promptInboundSubdomain(parentDomain));
20277
+ const receivingDomain = subdomain ? `${subdomain}.${parentDomain}` : parentDomain;
20181
20278
  const existingDomains = emailConfig.inboundDomains ?? [];
20182
20279
  if (existingDomains.some((d) => d.receivingDomain === receivingDomain)) {
20183
20280
  clack20.log.warn(
@@ -20828,12 +20925,19 @@ ${pc24.yellow(pc24.bold("Configuration Warnings:"))}`);
20828
20925
  `wraps-${identity.accountId}-${region}`
20829
20926
  );
20830
20927
  await stack.setConfig("aws:region", { value: region });
20831
- const upResult = await withTimeout(
20832
- stack.up({ onOutput: () => {
20833
- } }),
20834
- // Suppress Pulumi output
20835
- DEFAULT_PULUMI_TIMEOUT_MS,
20836
- "Pulumi deployment"
20928
+ const upResult = await withLockRetry(
20929
+ () => withTimeout(
20930
+ stack.up({ onOutput: () => {
20931
+ } }),
20932
+ // Suppress Pulumi output
20933
+ DEFAULT_PULUMI_TIMEOUT_MS,
20934
+ "Pulumi deployment"
20935
+ ),
20936
+ {
20937
+ accountId: identity.accountId,
20938
+ region,
20939
+ autoConfirm: options.yes || options.quick
20940
+ }
20837
20941
  );
20838
20942
  const pulumiOutputs = upResult.outputs;
20839
20943
  return {
@@ -20860,10 +20964,6 @@ ${pc24.yellow(pc24.bold("Configuration Warnings:"))}`);
20860
20964
  region,
20861
20965
  duration_ms: Date.now() - startTime
20862
20966
  });
20863
- if (msg.includes("stack is currently locked")) {
20864
- trackError("STACK_LOCKED", "email:init", { step: "deploy" });
20865
- throw errors.stackLocked();
20866
- }
20867
20967
  if (isPulumiError(error)) {
20868
20968
  const { code, iamAction, service } = parsePulumiError(error);
20869
20969
  trackError(`PULUMI_${code}`, "email:init", {
@@ -22028,8 +22128,8 @@ async function templatesInit(options) {
22028
22128
  const { homedir: homedir3 } = await import("os");
22029
22129
  const connectionsDir = join9(homedir3(), ".wraps", "connections");
22030
22130
  if (existsSync8(connectionsDir)) {
22031
- const { readdir: readdir4 } = await import("fs/promises");
22032
- const files = await readdir4(connectionsDir);
22131
+ const { readdir: readdir5 } = await import("fs/promises");
22132
+ const files = await readdir5(connectionsDir);
22033
22133
  if (files.length > 0) {
22034
22134
  const firstFile = files[0];
22035
22135
  const match = firstFile.match(/\d+-(.+)\.json$/);
@@ -22348,7 +22448,7 @@ import pc28 from "picocolors";
22348
22448
  // src/utils/email/template-compiler.ts
22349
22449
  init_esm_shims();
22350
22450
  import { existsSync as existsSync9 } from "fs";
22351
- import { mkdir as mkdir4, readdir as readdir2, writeFile as writeFile6 } from "fs/promises";
22451
+ import { mkdir as mkdir4, readdir as readdir3, writeFile as writeFile6 } from "fs/promises";
22352
22452
  import { join as join10 } from "path";
22353
22453
  async function loadWrapsConfig(wrapsDir) {
22354
22454
  const configPath = join10(wrapsDir, "wraps.config.ts");
@@ -22383,7 +22483,7 @@ async function loadWrapsConfig(wrapsDir) {
22383
22483
  return config2;
22384
22484
  }
22385
22485
  async function discoverTemplates(dir, filter) {
22386
- const entries = await readdir2(dir);
22486
+ const entries = await readdir3(dir);
22387
22487
  const templates = entries.filter(
22388
22488
  (f) => (f.endsWith(".tsx") || f.endsWith(".ts")) && !f.startsWith("_") && !f.endsWith(".d.ts")
22389
22489
  );
@@ -25659,8 +25759,8 @@ async function workflowsInit(options) {
25659
25759
  }
25660
25760
  const progress = new DeploymentProgress();
25661
25761
  if (existsSync13(workflowsDir) && !options.force) {
25662
- const { readdir: readdir4 } = await import("fs/promises");
25663
- const files = await readdir4(workflowsDir);
25762
+ const { readdir: readdir5 } = await import("fs/promises");
25763
+ const files = await readdir5(workflowsDir);
25664
25764
  const tsFiles = files.filter(
25665
25765
  (f) => f.endsWith(".ts") && !f.startsWith("_")
25666
25766
  );
@@ -25994,13 +26094,13 @@ function assignPositions(steps, transitions) {
25994
26094
  init_esm_shims();
25995
26095
  import { createHash as createHash2 } from "crypto";
25996
26096
  import { existsSync as existsSync14 } from "fs";
25997
- import { mkdir as mkdir8, readdir as readdir3, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
26097
+ import { mkdir as mkdir8, readdir as readdir4, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
25998
26098
  import { basename, join as join15 } from "path";
25999
26099
  async function discoverWorkflows(dir, filter) {
26000
26100
  if (!existsSync14(dir)) {
26001
26101
  return [];
26002
26102
  }
26003
- const entries = await readdir3(dir);
26103
+ const entries = await readdir4(dir);
26004
26104
  const workflows = entries.filter(
26005
26105
  (f) => (
26006
26106
  // Include .ts files only (not .tsx for workflows)
@@ -32975,7 +33075,6 @@ import * as pulumi26 from "@pulumi/pulumi";
32975
33075
  import pc43 from "picocolors";
32976
33076
  init_events();
32977
33077
  init_aws();
32978
- init_errors();
32979
33078
  init_fs();
32980
33079
  init_json_output();
32981
33080
  init_metadata();
@@ -33653,7 +33752,10 @@ ${pc43.yellow(pc43.bold("Important Notes:"))}`);
33653
33752
  }
33654
33753
  );
33655
33754
  await stack.setConfig("aws:region", { value: region });
33656
- const upResult = await stack.up({ onOutput: console.log });
33755
+ const upResult = await withLockRetry(
33756
+ () => stack.up({ onOutput: console.log }),
33757
+ { accountId: identity.accountId, region, autoConfirm: options.yes }
33758
+ );
33657
33759
  const pulumiOutputs = upResult.outputs;
33658
33760
  return {
33659
33761
  roleArn: pulumiOutputs.roleArn?.value,
@@ -33736,10 +33838,6 @@ ${pc43.yellow(pc43.bold("Important Notes:"))}`);
33736
33838
  duration_ms: Date.now() - startTime
33737
33839
  });
33738
33840
  const errorMessage = error instanceof Error ? error.message : String(error);
33739
- if (errorMessage.includes("stack is currently locked")) {
33740
- trackError("STACK_LOCKED", "sms:init", { step: "deploy" });
33741
- throw errors.stackLocked();
33742
- }
33743
33841
  trackError("DEPLOYMENT_FAILED", "sms:init", { step: "deploy" });
33744
33842
  throw new Error(`SMS deployment failed: ${errorMessage}`);
33745
33843
  }