@devramps/cli 0.1.6 → 0.1.8

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 +128 -62
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -81,30 +81,54 @@ function getStackKey(stackName, accountId, region) {
81
81
  var MultiStackProgress = class {
82
82
  stacks = /* @__PURE__ */ new Map();
83
83
  stackOrder = [];
84
- lastLineCount = 0;
85
84
  isTTY;
86
85
  spinnerFrame = 0;
87
86
  spinnerInterval = null;
88
87
  barWidth = 20;
88
+ renderScheduled = false;
89
+ lastRenderTime = 0;
90
+ hasRenderedOnce = false;
91
+ useAltScreen = true;
92
+ // Use alternate screen buffer for clean display
93
+ maxStackNameLen = 40;
94
+ // Will be calculated dynamically
89
95
  constructor() {
90
96
  this.isTTY = process.stdout.isTTY ?? false;
97
+ }
98
+ /**
99
+ * Start the progress display (call after all stacks are registered)
100
+ */
101
+ start() {
102
+ this.maxStackNameLen = Math.max(
103
+ ...Array.from(this.stacks.values()).map((s) => s.stackName.length),
104
+ 20
105
+ // minimum width
106
+ );
91
107
  if (this.isTTY) {
108
+ if (this.useAltScreen) {
109
+ process.stdout.write("\x1B[?1049h");
110
+ }
111
+ process.stdout.write("\x1B[?25l");
112
+ process.stdout.write("\x1B[H");
113
+ process.stdout.write("\x1B[2J");
114
+ this.doRender();
92
115
  this.spinnerInterval = setInterval(() => {
93
116
  this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
94
- this.render();
95
- }, 80);
117
+ this.scheduleRender();
118
+ }, 100);
96
119
  }
97
120
  }
98
121
  /**
99
122
  * Register a stack to track
100
123
  */
101
- addStack(stackName, accountId, region, totalResources) {
124
+ addStack(stackName, stackType, accountId, region, totalResources) {
102
125
  const key = getStackKey(stackName, accountId, region);
103
126
  if (this.stacks.has(key)) {
104
127
  return;
105
128
  }
106
129
  this.stacks.set(key, {
107
130
  stackName,
131
+ stackType,
108
132
  accountId,
109
133
  region,
110
134
  completed: 0,
@@ -112,20 +136,19 @@ var MultiStackProgress = class {
112
136
  status: "pending"
113
137
  });
114
138
  this.stackOrder.push(key);
115
- this.render();
116
139
  }
117
140
  /**
118
141
  * Update a stack's progress
119
142
  */
120
- updateStack(stackName, accountId, region, completed, status, latestEvent, latestResourceId) {
143
+ updateStack(stackName, accountId, region, completed, status, cfnStatus, latestResourceId) {
121
144
  const key = getStackKey(stackName, accountId, region);
122
145
  const stack = this.stacks.get(key);
123
146
  if (stack) {
124
147
  stack.completed = completed;
125
148
  stack.status = status;
126
- if (latestEvent) stack.latestEvent = latestEvent;
149
+ if (cfnStatus) stack.cfnStatus = cfnStatus;
127
150
  if (latestResourceId) stack.latestResourceId = latestResourceId;
128
- this.render();
151
+ this.scheduleRender();
129
152
  }
130
153
  }
131
154
  /**
@@ -136,30 +159,40 @@ var MultiStackProgress = class {
136
159
  const stack = this.stacks.get(key);
137
160
  if (stack) {
138
161
  stack.status = "in_progress";
139
- this.render();
162
+ stack.cfnStatus = "STARTING";
163
+ this.scheduleRender();
140
164
  }
141
165
  }
142
166
  /**
143
167
  * Mark a stack as complete
144
168
  */
145
- completeStack(stackName, accountId, region, success2) {
169
+ completeStack(stackName, accountId, region, success2, failureReason) {
146
170
  const key = getStackKey(stackName, accountId, region);
147
171
  const stack = this.stacks.get(key);
148
172
  if (stack) {
149
173
  stack.status = success2 ? "complete" : "failed";
150
174
  stack.completed = success2 ? stack.total : stack.completed;
151
- this.render();
175
+ if (failureReason) stack.failureReason = failureReason;
176
+ this.scheduleRender();
152
177
  }
153
178
  }
154
179
  /**
155
- * Clear the display
180
+ * Schedule a render (debounced to prevent too many updates)
156
181
  */
157
- clear() {
158
- if (!this.isTTY) return;
159
- for (let i = 0; i < this.lastLineCount; i++) {
160
- process.stdout.write("\x1B[A\x1B[2K");
182
+ scheduleRender() {
183
+ if (this.renderScheduled) return;
184
+ const now = Date.now();
185
+ const timeSinceLastRender = now - this.lastRenderTime;
186
+ const minInterval = 50;
187
+ if (timeSinceLastRender >= minInterval) {
188
+ this.doRender();
189
+ } else {
190
+ this.renderScheduled = true;
191
+ setTimeout(() => {
192
+ this.renderScheduled = false;
193
+ this.doRender();
194
+ }, minInterval - timeSinceLastRender);
161
195
  }
162
- this.lastLineCount = 0;
163
196
  }
164
197
  /**
165
198
  * Finish and stop updates
@@ -169,7 +202,12 @@ var MultiStackProgress = class {
169
202
  clearInterval(this.spinnerInterval);
170
203
  this.spinnerInterval = null;
171
204
  }
172
- this.clear();
205
+ if (this.isTTY) {
206
+ process.stdout.write("\x1B[?25h");
207
+ if (this.useAltScreen) {
208
+ process.stdout.write("\x1B[?1049l");
209
+ }
210
+ }
173
211
  this.renderFinal();
174
212
  }
175
213
  /**
@@ -182,11 +220,26 @@ var MultiStackProgress = class {
182
220
  console.log(this.formatStackLine(stack, false));
183
221
  }
184
222
  }
223
+ /**
224
+ * Get a short label for stack type
225
+ */
226
+ getTypeLabel(stackType) {
227
+ switch (stackType) {
228
+ case "org":
229
+ return "ORG";
230
+ case "pipeline":
231
+ return "PIPE";
232
+ case "account":
233
+ return "ACCT";
234
+ case "stage":
235
+ return "STAGE";
236
+ }
237
+ }
185
238
  /**
186
239
  * Format a single stack line
187
240
  */
188
241
  formatStackLine(stack, withSpinner) {
189
- const { accountId, region, stackName, completed, total, status, latestEvent, latestResourceId } = stack;
242
+ const { accountId, region, stackName, stackType, completed, total, status, cfnStatus, latestResourceId, failureReason } = stack;
190
243
  let statusIndicator;
191
244
  let colorFn;
192
245
  switch (status) {
@@ -201,7 +254,7 @@ var MultiStackProgress = class {
201
254
  break;
202
255
  case "in_progress":
203
256
  statusIndicator = withSpinner ? SPINNER_FRAMES[this.spinnerFrame] : "\u22EF";
204
- colorFn = chalk.blue;
257
+ colorFn = chalk.cyanBright;
205
258
  break;
206
259
  default:
207
260
  statusIndicator = "\u25CB";
@@ -211,45 +264,53 @@ var MultiStackProgress = class {
211
264
  const filled = Math.round(this.barWidth * percentage);
212
265
  const empty = this.barWidth - filled;
213
266
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
214
- const shortAccount = accountId.slice(-4);
215
- const accountLabel = `[...${shortAccount}/${region}]`;
216
- const countLabel = `(${completed}/${total})`;
217
- let 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}`;
267
+ const typeLabel = this.getTypeLabel(stackType).padEnd(5);
268
+ const accountLabel = `${accountId} ${region.padEnd(12)}`;
269
+ const countLabel = `${completed}/${total}`;
270
+ const displayName = stackName.padEnd(this.maxStackNameLen);
271
+ let rightInfo = "";
272
+ if (status === "failed" || status === "rollback") {
273
+ const statusText = cfnStatus || "FAILED";
274
+ if (failureReason && failureReason !== cfnStatus) {
275
+ const maxLen = 50;
276
+ const fullReason = `${statusText}: ${failureReason}`;
277
+ rightInfo = fullReason.length > maxLen ? fullReason.slice(0, maxLen - 3) + "..." : fullReason;
278
+ } else {
279
+ rightInfo = statusText;
280
+ }
281
+ } else if (status === "in_progress") {
282
+ const cfnStatusDisplay = cfnStatus || "DEPLOYING";
283
+ const resourceDisplay = latestResourceId ? latestResourceId.length > 25 ? latestResourceId.slice(0, 22) + "..." : latestResourceId : "";
284
+ rightInfo = resourceDisplay ? `${cfnStatusDisplay} \u2192 ${resourceDisplay}` : cfnStatusDisplay;
285
+ } else if (status === "complete") {
286
+ rightInfo = cfnStatus || "COMPLETE";
287
+ }
288
+ const leftPart = `${statusIndicator} [${typeLabel}] ${accountLabel} ${displayName}`;
289
+ const middlePart = `[${bar}] ${countLabel}`;
290
+ const line = `${leftPart} ${middlePart} ${rightInfo}`;
226
291
  return colorFn(line);
227
292
  }
228
293
  /**
229
- * Render all stack progress bars
294
+ * Perform the actual render
230
295
  */
231
- render() {
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;
296
+ doRender() {
297
+ this.lastRenderTime = Date.now();
298
+ if (!this.isTTY) return;
299
+ process.stdout.write("\x1B[H");
300
+ process.stdout.write(chalk.bold.underline("Deploying Stacks") + "\x1B[K\n\n");
301
+ for (const key of this.stackOrder) {
302
+ const stack = this.stacks.get(key);
303
+ if (!stack) continue;
304
+ process.stdout.write(this.formatStackLine(stack, true) + "\x1B[K\n");
250
305
  }
306
+ const completed = Array.from(this.stacks.values()).filter((s) => s.status === "complete").length;
307
+ const failed = Array.from(this.stacks.values()).filter((s) => s.status === "failed" || s.status === "rollback").length;
308
+ const inProgress = Array.from(this.stacks.values()).filter((s) => s.status === "in_progress").length;
309
+ const pending = Array.from(this.stacks.values()).filter((s) => s.status === "pending").length;
310
+ process.stdout.write("\n");
311
+ process.stdout.write(chalk.gray(`Progress: ${completed} complete, ${inProgress} in progress, ${pending} pending, ${failed} failed`) + "\x1B[K\n");
312
+ this.hasRenderedOnce = true;
251
313
  }
252
- isRendering = false;
253
314
  };
254
315
  var globalProgress = null;
255
316
  function getMultiStackProgress() {
@@ -572,14 +633,14 @@ function isResourceComplete(status) {
572
633
  if (!status) return false;
573
634
  return status.includes("_COMPLETE") && !status.includes("ROLLBACK");
574
635
  }
575
- async function waitForStackWithProgress(client, stackName, accountId, region, operationStartTime, totalResources, maxWaitTime = 600) {
636
+ async function waitForStackWithProgress(client, stackName, accountId, region, operationStartTime, _totalResources, maxWaitTime = 600) {
576
637
  const seenEventIds = /* @__PURE__ */ new Set();
577
638
  const completedResources = /* @__PURE__ */ new Set();
578
639
  const startTime = Date.now();
579
640
  const pollInterval = 2e3;
580
641
  const progress = getMultiStackProgress();
581
- let latestEvent = "";
582
642
  let latestResourceId = "";
643
+ let latestFailureReason = "";
583
644
  verbose(`[${stackName}] Starting to wait for stack operation...`);
584
645
  try {
585
646
  while (true) {
@@ -610,12 +671,15 @@ async function waitForStackWithProgress(client, stackName, accountId, region, op
610
671
  const logicalId = event.LogicalResourceId;
611
672
  const status = event.ResourceStatus || "";
612
673
  if (logicalId && logicalId !== stackName) {
613
- latestEvent = status;
614
674
  latestResourceId = logicalId;
615
675
  verbose(`[${stackName}] Resource ${logicalId}: ${status}`);
616
676
  if (isResourceComplete(status)) {
617
677
  completedResources.add(logicalId);
618
678
  }
679
+ if (status.includes("FAILED") && event.ResourceStatusReason) {
680
+ latestFailureReason = `${logicalId}: ${event.ResourceStatusReason}`;
681
+ verbose(`[${stackName}] Failure reason: ${latestFailureReason}`);
682
+ }
619
683
  }
620
684
  }
621
685
  let displayStatus = "in_progress";
@@ -624,10 +688,11 @@ async function waitForStackWithProgress(client, stackName, accountId, region, op
624
688
  } else if (currentStatus.includes("FAILED")) {
625
689
  displayStatus = "failed";
626
690
  }
627
- progress.updateStack(stackName, accountId, region, completedResources.size, displayStatus, latestEvent, latestResourceId);
691
+ progress.updateStack(stackName, accountId, region, completedResources.size, displayStatus, currentStatus, latestResourceId);
628
692
  if (TERMINAL_STATES.has(currentStatus)) {
629
693
  const success2 = SUCCESS_STATES.has(currentStatus);
630
- progress.completeStack(stackName, accountId, region, success2);
694
+ const failureReason = success2 ? void 0 : latestFailureReason || currentStatus;
695
+ progress.completeStack(stackName, accountId, region, success2, failureReason);
631
696
  verbose(`[${stackName}] Reached terminal state: ${currentStatus} (success: ${success2})`);
632
697
  if (success2) {
633
698
  return;
@@ -2739,18 +2804,19 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2739
2804
  info(`Deploying ${totalStacks} stack(s) in parallel...`);
2740
2805
  newline();
2741
2806
  const progress = getMultiStackProgress();
2742
- progress.addStack(plan.orgStack.stackName, plan.orgStack.accountId, plan.orgStack.region, 5);
2807
+ progress.addStack(plan.orgStack.stackName, "org", plan.orgStack.accountId, plan.orgStack.region, 5);
2743
2808
  for (const stack of plan.pipelineStacks) {
2744
2809
  const resourceCount = stack.dockerArtifacts.length + stack.bundleArtifacts.length;
2745
- progress.addStack(stack.stackName, stack.accountId, stack.region, Math.max(resourceCount, 1));
2810
+ progress.addStack(stack.stackName, "pipeline", stack.accountId, stack.region, Math.max(resourceCount, 1));
2746
2811
  }
2747
2812
  for (const stack of plan.accountStacks) {
2748
- progress.addStack(stack.stackName, stack.accountId, stack.region, 1);
2813
+ progress.addStack(stack.stackName, "account", stack.accountId, stack.region, 1);
2749
2814
  }
2750
2815
  for (const stack of plan.stageStacks) {
2751
2816
  const resourceCount = stack.dockerArtifacts.length + stack.bundleArtifacts.length + 2;
2752
- progress.addStack(stack.stackName, stack.accountId, stack.region, resourceCount);
2817
+ progress.addStack(stack.stackName, "stage", stack.accountId, stack.region, resourceCount);
2753
2818
  }
2819
+ progress.start();
2754
2820
  const orgPromise = (async () => {
2755
2821
  try {
2756
2822
  await deployOrgStack(plan, pipelines, authData, currentAccountId, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devramps/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines",
5
5
  "main": "dist/index.js",
6
6
  "bin": {