@devramps/cli 0.1.3 → 0.1.4

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 +181 -152
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -74,54 +74,75 @@ var CloudFormationError = class extends DevRampsError {
74
74
  // src/utils/logger.ts
75
75
  import chalk from "chalk";
76
76
  var verboseMode = false;
77
- var ProgressBar = class {
78
- completed = 0;
79
- total = 0;
80
- label;
81
- barWidth = 30;
77
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
78
+ var MultiStackProgress = class {
79
+ stacks = /* @__PURE__ */ new Map();
80
+ stackOrder = [];
82
81
  lastLineCount = 0;
83
- inProgressResources = /* @__PURE__ */ new Map();
84
82
  isTTY;
85
- constructor(label, total) {
86
- this.label = label;
87
- this.total = total;
83
+ spinnerFrame = 0;
84
+ spinnerInterval = null;
85
+ barWidth = 20;
86
+ constructor() {
88
87
  this.isTTY = process.stdout.isTTY ?? false;
88
+ if (this.isTTY) {
89
+ this.spinnerInterval = setInterval(() => {
90
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
91
+ this.render();
92
+ }, 80);
93
+ }
94
+ }
95
+ /**
96
+ * Register a stack to track
97
+ */
98
+ addStack(stackName, accountId, region, totalResources) {
99
+ this.stacks.set(stackName, {
100
+ stackName,
101
+ accountId,
102
+ region,
103
+ completed: 0,
104
+ total: totalResources,
105
+ status: "pending"
106
+ });
107
+ this.stackOrder.push(stackName);
89
108
  this.render();
90
109
  }
91
110
  /**
92
- * Update progress with resource status
111
+ * Update a stack's progress
93
112
  */
94
- update(completed, resourceStatus) {
95
- this.completed = completed;
96
- if (resourceStatus) {
97
- if (resourceStatus.status === "in_progress") {
98
- this.inProgressResources.set(resourceStatus.logicalId, resourceStatus);
99
- } else {
100
- this.inProgressResources.delete(resourceStatus.logicalId);
101
- }
113
+ updateStack(stackName, completed, status, latestEvent, latestResourceId) {
114
+ const stack = this.stacks.get(stackName);
115
+ if (stack) {
116
+ stack.completed = completed;
117
+ stack.status = status;
118
+ if (latestEvent) stack.latestEvent = latestEvent;
119
+ if (latestResourceId) stack.latestResourceId = latestResourceId;
120
+ this.render();
102
121
  }
103
- this.render();
104
122
  }
105
123
  /**
106
- * Set a resource as in-progress
124
+ * Mark a stack as started
107
125
  */
108
- setInProgress(logicalId, resourceType) {
109
- this.inProgressResources.set(logicalId, {
110
- logicalId,
111
- resourceType,
112
- status: "in_progress"
113
- });
114
- this.render();
126
+ startStack(stackName) {
127
+ const stack = this.stacks.get(stackName);
128
+ if (stack) {
129
+ stack.status = "in_progress";
130
+ this.render();
131
+ }
115
132
  }
116
133
  /**
117
- * Mark a resource as complete (removes from in-progress)
134
+ * Mark a stack as complete
118
135
  */
119
- setComplete(logicalId) {
120
- this.inProgressResources.delete(logicalId);
121
- this.render();
136
+ completeStack(stackName, success2) {
137
+ const stack = this.stacks.get(stackName);
138
+ if (stack) {
139
+ stack.status = success2 ? "complete" : "failed";
140
+ stack.completed = success2 ? stack.total : stack.completed;
141
+ this.render();
142
+ }
122
143
  }
123
144
  /**
124
- * Clear the progress bar from the terminal (only for TTY)
145
+ * Clear the display
125
146
  */
126
147
  clear() {
127
148
  if (!this.isTTY) return;
@@ -131,56 +152,100 @@ var ProgressBar = class {
131
152
  this.lastLineCount = 0;
132
153
  }
133
154
  /**
134
- * Finish the progress bar (don't clear - leave final state visible)
155
+ * Finish and stop updates
135
156
  */
136
157
  finish() {
158
+ if (this.spinnerInterval) {
159
+ clearInterval(this.spinnerInterval);
160
+ this.spinnerInterval = null;
161
+ }
137
162
  this.clear();
138
- this.inProgressResources.clear();
139
- const percentage = this.total > 0 ? this.completed / this.total : 0;
163
+ this.renderFinal();
164
+ }
165
+ /**
166
+ * Render final state (no clearing, just print)
167
+ */
168
+ renderFinal() {
169
+ for (const stackName of this.stackOrder) {
170
+ const stack = this.stacks.get(stackName);
171
+ if (!stack) continue;
172
+ console.log(this.formatStackLine(stack, false));
173
+ }
174
+ }
175
+ /**
176
+ * Format a single stack line
177
+ */
178
+ formatStackLine(stack, withSpinner) {
179
+ const { accountId, region, stackName, completed, total, status, latestEvent, latestResourceId } = stack;
180
+ let statusIndicator;
181
+ let colorFn;
182
+ switch (status) {
183
+ case "complete":
184
+ statusIndicator = "\u2714";
185
+ colorFn = chalk.green;
186
+ break;
187
+ case "failed":
188
+ case "rollback":
189
+ statusIndicator = "\u2716";
190
+ colorFn = chalk.red;
191
+ break;
192
+ case "in_progress":
193
+ statusIndicator = withSpinner ? SPINNER_FRAMES[this.spinnerFrame] : "\u22EF";
194
+ colorFn = chalk.blue;
195
+ break;
196
+ default:
197
+ statusIndicator = "\u25CB";
198
+ colorFn = chalk.gray;
199
+ }
200
+ const percentage = total > 0 ? completed / total : 0;
140
201
  const filled = Math.round(this.barWidth * percentage);
141
202
  const empty = this.barWidth - filled;
142
- const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
143
- const count = chalk.cyan(`${this.completed}/${this.total}`);
144
- const labelText = chalk.bold(this.label);
145
- console.log(`${labelText} ${bar} ${count} resources`);
203
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
204
+ const accountLabel = `[${accountId} - ${region}]`;
205
+ const countLabel = `(${completed}/${total})`;
206
+ 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}`;
216
+ return colorFn(line);
146
217
  }
147
218
  /**
148
- * Render the progress bar with in-progress resources
219
+ * Render all stack progress bars
149
220
  */
150
221
  render() {
222
+ if (!this.isTTY) return;
151
223
  this.clear();
152
224
  const lines = [];
153
- const percentage = this.total > 0 ? this.completed / this.total : 0;
154
- const filled = Math.round(this.barWidth * percentage);
155
- const empty = this.barWidth - filled;
156
- const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
157
- const count = chalk.cyan(`${this.completed}/${this.total}`);
158
- const labelText = chalk.bold(this.label);
159
- lines.push(`${labelText} ${bar} ${count} resources`);
160
- const inProgress = Array.from(this.inProgressResources.values()).slice(0, 8);
161
- if (inProgress.length > 0) {
162
- for (const resource of inProgress) {
163
- const spinner = chalk.yellow("\u22EF");
164
- const type = chalk.gray(resource.resourceType);
165
- lines.push(` ${spinner} ${resource.logicalId} ${type}`);
166
- }
167
- const remaining = this.inProgressResources.size - inProgress.length;
168
- if (remaining > 0) {
169
- lines.push(chalk.gray(` ... and ${remaining} more in progress`));
170
- }
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));
171
229
  }
172
- if (this.isTTY) {
173
- for (const line of lines) {
174
- process.stdout.write(line + "\n");
175
- }
176
- this.lastLineCount = lines.length;
177
- } else {
178
- if (this.completed === 0 || this.completed === this.total) {
179
- console.log(lines[0]);
180
- }
230
+ for (const line of lines) {
231
+ process.stdout.write(line + "\n");
181
232
  }
233
+ this.lastLineCount = lines.length;
182
234
  }
183
235
  };
236
+ var globalProgress = null;
237
+ function getMultiStackProgress() {
238
+ if (!globalProgress) {
239
+ globalProgress = new MultiStackProgress();
240
+ }
241
+ return globalProgress;
242
+ }
243
+ function clearMultiStackProgress() {
244
+ if (globalProgress) {
245
+ globalProgress.finish();
246
+ globalProgress = null;
247
+ }
248
+ }
184
249
  function setVerbose(enabled) {
185
250
  verboseMode = enabled;
186
251
  }
@@ -241,18 +306,6 @@ function table(rows) {
241
306
  function newline() {
242
307
  console.log();
243
308
  }
244
- function resourceProgress(stackName, resourceId, resourceType, status, reason) {
245
- const stackLabel = chalk.cyan(`[${stackName}]`);
246
- const typeLabel = chalk.gray(resourceType);
247
- if (status === "in_progress") {
248
- console.log(`${stackLabel} ${chalk.yellow("\u22EF")} ${resourceId} ${typeLabel}`);
249
- } else if (status === "complete") {
250
- console.log(`${stackLabel} ${chalk.green("\u2714")} ${resourceId} ${typeLabel}`);
251
- } else if (status === "failed") {
252
- const reasonText = reason ? ` - ${reason}` : "";
253
- console.log(`${stackLabel} ${chalk.red("\u2716")} ${resourceId} ${typeLabel}${reasonText}`);
254
- }
255
- }
256
309
 
257
310
  // src/aws/credentials.ts
258
311
  var DEFAULT_REGION = "us-east-1";
@@ -499,13 +552,14 @@ function isResourceComplete(status) {
499
552
  if (!status) return false;
500
553
  return status.includes("_COMPLETE") && !status.includes("ROLLBACK");
501
554
  }
502
- async function waitForStackWithProgress(client, stackName, operationStartTime, totalResources, maxWaitTime = 600, showProgress = true) {
555
+ async function waitForStackWithProgress(client, stackName, operationStartTime, totalResources, maxWaitTime = 600) {
503
556
  const seenEventIds = /* @__PURE__ */ new Set();
504
557
  const completedResources = /* @__PURE__ */ new Set();
505
- const inProgressResources = /* @__PURE__ */ new Map();
506
558
  const startTime = Date.now();
507
559
  const pollInterval = 3e3;
508
- const progressBar = showProgress ? new ProgressBar(stackName, totalResources) : null;
560
+ const progress = getMultiStackProgress();
561
+ let latestEvent = "";
562
+ let latestResourceId = "";
509
563
  try {
510
564
  while (true) {
511
565
  if (Date.now() - startTime > maxWaitTime * 1e3) {
@@ -531,56 +585,27 @@ async function waitForStackWithProgress(client, stackName, operationStartTime, t
531
585
  seenEventIds.add(event.EventId);
532
586
  }
533
587
  const logicalId = event.LogicalResourceId;
534
- const resourceType = event.ResourceType || "Unknown";
535
588
  const status = event.ResourceStatus || "";
536
589
  if (logicalId && logicalId !== stackName) {
537
- if (status.includes("IN_PROGRESS")) {
538
- inProgressResources.set(logicalId, { resourceType });
539
- if (progressBar) {
540
- const resourceStatus = {
541
- logicalId,
542
- resourceType,
543
- status: "in_progress"
544
- };
545
- progressBar.update(completedResources.size, resourceStatus);
546
- } else {
547
- resourceProgress(stackName, logicalId, resourceType, "in_progress");
548
- }
549
- } else if (isResourceComplete(status)) {
590
+ latestEvent = status;
591
+ latestResourceId = logicalId;
592
+ if (isResourceComplete(status)) {
550
593
  completedResources.add(logicalId);
551
- inProgressResources.delete(logicalId);
552
- if (progressBar) {
553
- const resourceStatus = {
554
- logicalId,
555
- resourceType,
556
- status: "complete"
557
- };
558
- progressBar.update(completedResources.size, resourceStatus);
559
- } else {
560
- resourceProgress(stackName, logicalId, resourceType, "complete");
561
- }
562
- } else if (status.includes("FAILED") || status.includes("ROLLBACK")) {
563
- inProgressResources.delete(logicalId);
564
- if (progressBar) {
565
- const resourceStatus = {
566
- logicalId,
567
- resourceType,
568
- status: "failed",
569
- reason: event.ResourceStatusReason
570
- };
571
- progressBar.update(completedResources.size, resourceStatus);
572
- } else {
573
- resourceProgress(stackName, logicalId, resourceType, "failed", event.ResourceStatusReason);
574
- }
575
594
  }
576
595
  }
577
596
  }
578
597
  const currentStatus = stack.StackStatus || "";
598
+ let displayStatus = "in_progress";
599
+ if (currentStatus.includes("ROLLBACK")) {
600
+ displayStatus = "rollback";
601
+ } else if (currentStatus.includes("FAILED")) {
602
+ displayStatus = "failed";
603
+ }
604
+ progress.updateStack(stackName, completedResources.size, displayStatus, latestEvent, latestResourceId);
579
605
  if (TERMINAL_STATES.has(currentStatus)) {
580
- if (progressBar) {
581
- progressBar.finish();
582
- }
583
- if (SUCCESS_STATES.has(currentStatus)) {
606
+ const success2 = SUCCESS_STATES.has(currentStatus);
607
+ progress.completeStack(stackName, success2);
608
+ if (success2) {
584
609
  return;
585
610
  }
586
611
  throw new Error(`Stack operation failed with status: ${currentStatus}`);
@@ -588,39 +613,41 @@ async function waitForStackWithProgress(client, stackName, operationStartTime, t
588
613
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
589
614
  }
590
615
  } catch (error2) {
591
- if (progressBar) {
592
- progressBar.finish();
593
- }
616
+ progress.completeStack(stackName, false);
594
617
  throw error2;
595
618
  }
596
619
  }
597
620
  async function deployStack(options) {
598
- const { stackName, template, accountId, region, credentials, showProgress = true } = options;
621
+ const { stackName, template, accountId, region, credentials } = options;
599
622
  const client = new CloudFormationClient({
600
623
  credentials,
601
624
  region
602
625
  });
603
626
  const templateBody = JSON.stringify(template);
604
627
  const resourceCount = Object.keys(template.Resources || {}).length;
628
+ const progress = getMultiStackProgress();
629
+ progress.startStack(stackName);
605
630
  try {
606
631
  const stackStatus = await getStackStatus(stackName, credentials, region);
607
632
  if (stackStatus.exists) {
608
633
  verbose(`Stack ${stackName} exists, updating...`);
609
- await updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
634
+ await updateStack(client, stackName, templateBody, accountId, resourceCount);
610
635
  } else {
611
636
  verbose(`Stack ${stackName} does not exist, creating...`);
612
- await createStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
637
+ await createStack(client, stackName, templateBody, accountId, resourceCount);
613
638
  }
614
639
  } catch (error2) {
615
640
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
616
641
  if (errorMessage.includes("No updates are to be performed")) {
617
642
  verbose(`Stack ${stackName} is already up to date`);
643
+ progress.completeStack(stackName, true);
618
644
  return;
619
645
  }
646
+ progress.completeStack(stackName, false);
620
647
  throw new CloudFormationError(stackName, accountId, errorMessage);
621
648
  }
622
649
  }
623
- async function createStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
650
+ async function createStack(client, stackName, templateBody, _accountId, resourceCount) {
624
651
  const operationStartTime = /* @__PURE__ */ new Date();
625
652
  await client.send(
626
653
  new CreateStackCommand({
@@ -633,11 +660,9 @@ async function createStack(client, stackName, templateBody, accountId, resourceC
633
660
  ]
634
661
  })
635
662
  );
636
- info(`Creating stack ${stackName}...`);
637
- await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
638
- success(`Stack ${stackName} created successfully in account ${accountId}`);
663
+ await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount);
639
664
  }
640
- async function updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
665
+ async function updateStack(client, stackName, templateBody, _accountId, resourceCount) {
641
666
  const operationStartTime = /* @__PURE__ */ new Date();
642
667
  await client.send(
643
668
  new UpdateStackCommand({
@@ -646,9 +671,7 @@ async function updateStack(client, stackName, templateBody, accountId, resourceC
646
671
  Capabilities: ["CAPABILITY_NAMED_IAM"]
647
672
  })
648
673
  );
649
- info(`Updating stack ${stackName}...`);
650
- await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
651
- success(`Stack ${stackName} updated successfully in account ${accountId}`);
674
+ await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount);
652
675
  }
653
676
  async function readExistingStack(stackName, accountId, region, credentials) {
654
677
  const client = new CloudFormationClient({
@@ -2675,6 +2698,19 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2675
2698
  header("Deploying All Stacks");
2676
2699
  info(`Deploying ${totalStacks} stack(s) in parallel...`);
2677
2700
  newline();
2701
+ const progress = getMultiStackProgress();
2702
+ progress.addStack(plan.orgStack.stackName, plan.orgStack.accountId, plan.orgStack.region, 5);
2703
+ for (const stack of plan.pipelineStacks) {
2704
+ const resourceCount = stack.dockerArtifacts.length + stack.bundleArtifacts.length;
2705
+ progress.addStack(stack.stackName, stack.accountId, stack.region, Math.max(resourceCount, 1));
2706
+ }
2707
+ for (const stack of plan.accountStacks) {
2708
+ progress.addStack(stack.stackName, stack.accountId, stack.region, 1);
2709
+ }
2710
+ for (const stack of plan.stageStacks) {
2711
+ const resourceCount = stack.dockerArtifacts.length + stack.bundleArtifacts.length + 2;
2712
+ progress.addStack(stack.stackName, stack.accountId, stack.region, resourceCount);
2713
+ }
2678
2714
  const orgPromise = (async () => {
2679
2715
  try {
2680
2716
  await deployOrgStack(plan, pipelines, authData, currentAccountId, options);
@@ -2702,10 +2738,10 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2702
2738
  const accountPromises = plan.accountStacks.map(async (stack) => {
2703
2739
  try {
2704
2740
  await deployAccountStack(stack, currentAccountId, options);
2705
- return { stack: `${stack.stackName} (${stack.accountId})`, success: true };
2741
+ return { stack: stack.stackName, success: true };
2706
2742
  } catch (error2) {
2707
2743
  return {
2708
- stack: `${stack.stackName} (${stack.accountId})`,
2744
+ stack: stack.stackName,
2709
2745
  success: false,
2710
2746
  error: error2 instanceof Error ? error2.message : String(error2)
2711
2747
  };
@@ -2729,6 +2765,7 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2729
2765
  ...accountPromises,
2730
2766
  ...stagePromises
2731
2767
  ]);
2768
+ clearMultiStackProgress();
2732
2769
  newline();
2733
2770
  for (const result of allResults) {
2734
2771
  if (result.success) {
@@ -2784,9 +2821,7 @@ async function deployOrgStack(plan, pipelines, authData, currentAccountId, optio
2784
2821
  template,
2785
2822
  accountId: cicdAccountId,
2786
2823
  region: cicdRegion,
2787
- credentials,
2788
- showProgress: false
2789
- // Disable progress bar for parallel deployment
2824
+ credentials
2790
2825
  };
2791
2826
  await previewStackChanges(deployOptions);
2792
2827
  await deployStack(deployOptions);
@@ -2809,9 +2844,7 @@ async function deployPipelineStack(stack, authData, currentAccountId, options) {
2809
2844
  template,
2810
2845
  accountId: cicdAccountId,
2811
2846
  region: cicdRegion,
2812
- credentials,
2813
- showProgress: false
2814
- // Disable progress bar for parallel deployment
2847
+ credentials
2815
2848
  };
2816
2849
  await previewStackChanges(deployOptions);
2817
2850
  await deployStack(deployOptions);
@@ -2828,9 +2861,7 @@ async function deployAccountStack(stack, currentAccountId, options) {
2828
2861
  template,
2829
2862
  accountId: stack.accountId,
2830
2863
  region: stack.region,
2831
- credentials,
2832
- showProgress: false
2833
- // Disable progress bar for parallel deployment
2864
+ credentials
2834
2865
  };
2835
2866
  await previewStackChanges(deployOptions);
2836
2867
  await deployStack(deployOptions);
@@ -2856,9 +2887,7 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
2856
2887
  template,
2857
2888
  accountId: stack.accountId,
2858
2889
  region: stack.region,
2859
- credentials,
2860
- showProgress: false
2861
- // Disable progress bar for parallel deployment
2890
+ credentials
2862
2891
  };
2863
2892
  await previewStackChanges(deployOptions);
2864
2893
  await deployStack(deployOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devramps/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines",
5
5
  "main": "dist/index.js",
6
6
  "bin": {