@devramps/cli 0.1.4 → 0.1.5

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.
Files changed (2) hide show
  1. package/dist/index.js +88 -48
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -75,6 +75,9 @@ var CloudFormationError = class extends DevRampsError {
75
75
  import chalk from "chalk";
76
76
  var verboseMode = false;
77
77
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
78
+ function getStackKey(stackName, accountId, region) {
79
+ return `${stackName}:${accountId}:${region}`;
80
+ }
78
81
  var MultiStackProgress = class {
79
82
  stacks = /* @__PURE__ */ new Map();
80
83
  stackOrder = [];
@@ -96,7 +99,11 @@ var MultiStackProgress = class {
96
99
  * Register a stack to track
97
100
  */
98
101
  addStack(stackName, accountId, region, totalResources) {
99
- this.stacks.set(stackName, {
102
+ const key = getStackKey(stackName, accountId, region);
103
+ if (this.stacks.has(key)) {
104
+ return;
105
+ }
106
+ this.stacks.set(key, {
100
107
  stackName,
101
108
  accountId,
102
109
  region,
@@ -104,14 +111,15 @@ var MultiStackProgress = class {
104
111
  total: totalResources,
105
112
  status: "pending"
106
113
  });
107
- this.stackOrder.push(stackName);
114
+ this.stackOrder.push(key);
108
115
  this.render();
109
116
  }
110
117
  /**
111
118
  * Update a stack's progress
112
119
  */
113
- updateStack(stackName, completed, status, latestEvent, latestResourceId) {
114
- const stack = this.stacks.get(stackName);
120
+ updateStack(stackName, accountId, region, completed, status, latestEvent, latestResourceId) {
121
+ const key = getStackKey(stackName, accountId, region);
122
+ const stack = this.stacks.get(key);
115
123
  if (stack) {
116
124
  stack.completed = completed;
117
125
  stack.status = status;
@@ -123,8 +131,9 @@ var MultiStackProgress = class {
123
131
  /**
124
132
  * Mark a stack as started
125
133
  */
126
- startStack(stackName) {
127
- const stack = this.stacks.get(stackName);
134
+ startStack(stackName, accountId, region) {
135
+ const key = getStackKey(stackName, accountId, region);
136
+ const stack = this.stacks.get(key);
128
137
  if (stack) {
129
138
  stack.status = "in_progress";
130
139
  this.render();
@@ -133,8 +142,9 @@ var MultiStackProgress = class {
133
142
  /**
134
143
  * Mark a stack as complete
135
144
  */
136
- completeStack(stackName, success2) {
137
- const stack = this.stacks.get(stackName);
145
+ completeStack(stackName, accountId, region, success2) {
146
+ const key = getStackKey(stackName, accountId, region);
147
+ const stack = this.stacks.get(key);
138
148
  if (stack) {
139
149
  stack.status = success2 ? "complete" : "failed";
140
150
  stack.completed = success2 ? stack.total : stack.completed;
@@ -166,8 +176,8 @@ var MultiStackProgress = class {
166
176
  * Render final state (no clearing, just print)
167
177
  */
168
178
  renderFinal() {
169
- for (const stackName of this.stackOrder) {
170
- const stack = this.stacks.get(stackName);
179
+ for (const key of this.stackOrder) {
180
+ const stack = this.stacks.get(key);
171
181
  if (!stack) continue;
172
182
  console.log(this.formatStackLine(stack, false));
173
183
  }
@@ -201,37 +211,45 @@ var MultiStackProgress = class {
201
211
  const filled = Math.round(this.barWidth * percentage);
202
212
  const empty = this.barWidth - filled;
203
213
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
204
- const accountLabel = `[${accountId} - ${region}]`;
214
+ const shortAccount = accountId.slice(-4);
215
+ const accountLabel = `[...${shortAccount}/${region}]`;
205
216
  const countLabel = `(${completed}/${total})`;
206
217
  let eventLabel = "";
207
- if (latestEvent && latestResourceId) {
208
- const maxEventLen = 30;
209
- const resourceIdTrunc = latestResourceId.length > 20 ? latestResourceId.slice(0, 17) + "..." : latestResourceId;
210
- eventLabel = ` ${latestEvent} ${resourceIdTrunc}`;
211
- if (eventLabel.length > maxEventLen) {
212
- eventLabel = eventLabel.slice(0, maxEventLen - 3) + "...";
213
- }
214
- }
215
- const line = `${statusIndicator} ${accountLabel} ${stackName} [${bar}] ${countLabel}${eventLabel}`;
218
+ if (status === "in_progress" && latestEvent && latestResourceId) {
219
+ const shortStatus = latestEvent.replace("CREATE_", "").replace("UPDATE_", "").replace("DELETE_", "");
220
+ const resourceIdTrunc = latestResourceId.length > 15 ? latestResourceId.slice(0, 12) + "..." : latestResourceId;
221
+ eventLabel = ` ${shortStatus} ${resourceIdTrunc}`;
222
+ }
223
+ const maxStackNameLen = 40;
224
+ const displayName = stackName.length > maxStackNameLen ? "..." + stackName.slice(-(maxStackNameLen - 3)) : stackName;
225
+ const line = `${statusIndicator} ${accountLabel} ${displayName} [${bar}] ${countLabel}${eventLabel}`;
216
226
  return colorFn(line);
217
227
  }
218
228
  /**
219
229
  * Render all stack progress bars
220
230
  */
221
231
  render() {
222
- if (!this.isTTY) return;
223
- this.clear();
224
- const lines = [];
225
- for (const stackName of this.stackOrder) {
226
- const stack = this.stacks.get(stackName);
227
- if (!stack) continue;
228
- lines.push(this.formatStackLine(stack, true));
229
- }
230
- for (const line of lines) {
231
- process.stdout.write(line + "\n");
232
+ if (this.isRendering) return;
233
+ this.isRendering = true;
234
+ try {
235
+ if (this.isTTY) {
236
+ this.clear();
237
+ const lines = [];
238
+ for (const key of this.stackOrder) {
239
+ const stack = this.stacks.get(key);
240
+ if (!stack) continue;
241
+ lines.push(this.formatStackLine(stack, true));
242
+ }
243
+ for (const line of lines) {
244
+ process.stdout.write(line + "\n");
245
+ }
246
+ this.lastLineCount = lines.length;
247
+ }
248
+ } finally {
249
+ this.isRendering = false;
232
250
  }
233
- this.lastLineCount = lines.length;
234
251
  }
252
+ isRendering = false;
235
253
  };
236
254
  var globalProgress = null;
237
255
  function getMultiStackProgress() {
@@ -409,10 +427,12 @@ import {
409
427
  DescribeStackEventsCommand,
410
428
  CreateStackCommand,
411
429
  UpdateStackCommand,
430
+ DeleteStackCommand,
412
431
  CreateChangeSetCommand,
413
432
  DescribeChangeSetCommand,
414
433
  DeleteChangeSetCommand,
415
434
  waitUntilChangeSetCreateComplete,
435
+ waitUntilStackDeleteComplete,
416
436
  ChangeSetType
417
437
  } from "@aws-sdk/client-cloudformation";
418
438
  async function getStackStatus(stackName, credentials, region) {
@@ -552,14 +572,15 @@ function isResourceComplete(status) {
552
572
  if (!status) return false;
553
573
  return status.includes("_COMPLETE") && !status.includes("ROLLBACK");
554
574
  }
555
- async function waitForStackWithProgress(client, stackName, operationStartTime, totalResources, maxWaitTime = 600) {
575
+ async function waitForStackWithProgress(client, stackName, accountId, region, operationStartTime, totalResources, maxWaitTime = 600) {
556
576
  const seenEventIds = /* @__PURE__ */ new Set();
557
577
  const completedResources = /* @__PURE__ */ new Set();
558
578
  const startTime = Date.now();
559
- const pollInterval = 3e3;
579
+ const pollInterval = 2e3;
560
580
  const progress = getMultiStackProgress();
561
581
  let latestEvent = "";
562
582
  let latestResourceId = "";
583
+ verbose(`[${stackName}] Starting to wait for stack operation...`);
563
584
  try {
564
585
  while (true) {
565
586
  if (Date.now() - startTime > maxWaitTime * 1e3) {
@@ -572,6 +593,8 @@ async function waitForStackWithProgress(client, stackName, operationStartTime, t
572
593
  if (!stack) {
573
594
  throw new Error(`Stack ${stackName} not found`);
574
595
  }
596
+ const currentStatus = stack.StackStatus || "";
597
+ verbose(`[${stackName}] Current status: ${currentStatus}`);
575
598
  const eventsResponse = await client.send(
576
599
  new DescribeStackEventsCommand({ StackName: stackName })
577
600
  );
@@ -589,22 +612,23 @@ async function waitForStackWithProgress(client, stackName, operationStartTime, t
589
612
  if (logicalId && logicalId !== stackName) {
590
613
  latestEvent = status;
591
614
  latestResourceId = logicalId;
615
+ verbose(`[${stackName}] Resource ${logicalId}: ${status}`);
592
616
  if (isResourceComplete(status)) {
593
617
  completedResources.add(logicalId);
594
618
  }
595
619
  }
596
620
  }
597
- const currentStatus = stack.StackStatus || "";
598
621
  let displayStatus = "in_progress";
599
622
  if (currentStatus.includes("ROLLBACK")) {
600
623
  displayStatus = "rollback";
601
624
  } else if (currentStatus.includes("FAILED")) {
602
625
  displayStatus = "failed";
603
626
  }
604
- progress.updateStack(stackName, completedResources.size, displayStatus, latestEvent, latestResourceId);
627
+ progress.updateStack(stackName, accountId, region, completedResources.size, displayStatus, latestEvent, latestResourceId);
605
628
  if (TERMINAL_STATES.has(currentStatus)) {
606
629
  const success2 = SUCCESS_STATES.has(currentStatus);
607
- progress.completeStack(stackName, success2);
630
+ progress.completeStack(stackName, accountId, region, success2);
631
+ verbose(`[${stackName}] Reached terminal state: ${currentStatus} (success: ${success2})`);
608
632
  if (success2) {
609
633
  return;
610
634
  }
@@ -613,12 +637,12 @@ async function waitForStackWithProgress(client, stackName, operationStartTime, t
613
637
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
614
638
  }
615
639
  } catch (error2) {
616
- progress.completeStack(stackName, false);
640
+ progress.completeStack(stackName, accountId, region, false);
617
641
  throw error2;
618
642
  }
619
643
  }
620
644
  async function deployStack(options) {
621
- const { stackName, template, accountId, region, credentials } = options;
645
+ const { stackName, template, accountId, region = "us-east-1", credentials } = options;
622
646
  const client = new CloudFormationClient({
623
647
  credentials,
624
648
  region
@@ -626,28 +650,44 @@ async function deployStack(options) {
626
650
  const templateBody = JSON.stringify(template);
627
651
  const resourceCount = Object.keys(template.Resources || {}).length;
628
652
  const progress = getMultiStackProgress();
629
- progress.startStack(stackName);
653
+ progress.startStack(stackName, accountId, region);
630
654
  try {
631
655
  const stackStatus = await getStackStatus(stackName, credentials, region);
632
- if (stackStatus.exists) {
656
+ if (stackStatus.exists && stackStatus.status === "ROLLBACK_COMPLETE") {
657
+ verbose(`Stack ${stackName} is in ROLLBACK_COMPLETE state, deleting before recreating...`);
658
+ await deleteStack(client, stackName);
659
+ verbose(`Stack ${stackName} deleted, now creating...`);
660
+ await createStack(client, stackName, accountId, region, templateBody, resourceCount);
661
+ } else if (stackStatus.exists) {
633
662
  verbose(`Stack ${stackName} exists, updating...`);
634
- await updateStack(client, stackName, templateBody, accountId, resourceCount);
663
+ await updateStack(client, stackName, accountId, region, templateBody, resourceCount);
635
664
  } else {
636
665
  verbose(`Stack ${stackName} does not exist, creating...`);
637
- await createStack(client, stackName, templateBody, accountId, resourceCount);
666
+ await createStack(client, stackName, accountId, region, templateBody, resourceCount);
638
667
  }
639
668
  } catch (error2) {
640
669
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
641
670
  if (errorMessage.includes("No updates are to be performed")) {
642
671
  verbose(`Stack ${stackName} is already up to date`);
643
- progress.completeStack(stackName, true);
672
+ progress.completeStack(stackName, accountId, region, true);
644
673
  return;
645
674
  }
646
- progress.completeStack(stackName, false);
675
+ progress.completeStack(stackName, accountId, region, false);
647
676
  throw new CloudFormationError(stackName, accountId, errorMessage);
648
677
  }
649
678
  }
650
- async function createStack(client, stackName, templateBody, _accountId, resourceCount) {
679
+ async function deleteStack(client, stackName) {
680
+ await client.send(
681
+ new DeleteStackCommand({
682
+ StackName: stackName
683
+ })
684
+ );
685
+ await waitUntilStackDeleteComplete(
686
+ { client, maxWaitTime: 300 },
687
+ { StackName: stackName }
688
+ );
689
+ }
690
+ async function createStack(client, stackName, accountId, region, templateBody, resourceCount) {
651
691
  const operationStartTime = /* @__PURE__ */ new Date();
652
692
  await client.send(
653
693
  new CreateStackCommand({
@@ -660,9 +700,9 @@ async function createStack(client, stackName, templateBody, _accountId, resource
660
700
  ]
661
701
  })
662
702
  );
663
- await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount);
703
+ await waitForStackWithProgress(client, stackName, accountId, region, operationStartTime, resourceCount);
664
704
  }
665
- async function updateStack(client, stackName, templateBody, _accountId, resourceCount) {
705
+ async function updateStack(client, stackName, accountId, region, templateBody, resourceCount) {
666
706
  const operationStartTime = /* @__PURE__ */ new Date();
667
707
  await client.send(
668
708
  new UpdateStackCommand({
@@ -671,7 +711,7 @@ async function updateStack(client, stackName, templateBody, _accountId, resource
671
711
  Capabilities: ["CAPABILITY_NAMED_IAM"]
672
712
  })
673
713
  );
674
- await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount);
714
+ await waitForStackWithProgress(client, stackName, accountId, region, operationStartTime, resourceCount);
675
715
  }
676
716
  async function readExistingStack(stackName, accountId, region, credentials) {
677
717
  const client = new CloudFormationClient({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devramps/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines",
5
5
  "main": "dist/index.js",
6
6
  "bin": {