@devramps/cli 0.1.2 → 0.1.3

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 +171 -107
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -75,77 +75,110 @@ var CloudFormationError = class extends DevRampsError {
75
75
  import chalk from "chalk";
76
76
  var verboseMode = false;
77
77
  var ProgressBar = class {
78
- current = 0;
78
+ completed = 0;
79
79
  total = 0;
80
80
  label;
81
81
  barWidth = 30;
82
82
  lastLineCount = 0;
83
- eventLines = [];
84
- maxVisibleEvents = 5;
83
+ inProgressResources = /* @__PURE__ */ new Map();
84
+ isTTY;
85
85
  constructor(label, total) {
86
86
  this.label = label;
87
87
  this.total = total;
88
+ this.isTTY = process.stdout.isTTY ?? false;
88
89
  this.render();
89
90
  }
90
91
  /**
91
- * Update progress and re-render
92
+ * Update progress with resource status
92
93
  */
93
- update(current, eventMessage) {
94
- this.current = current;
95
- if (eventMessage) {
96
- this.eventLines.push(eventMessage);
97
- if (this.eventLines.length > this.maxVisibleEvents) {
98
- this.eventLines.shift();
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);
99
101
  }
100
102
  }
101
103
  this.render();
102
104
  }
103
105
  /**
104
- * Add an event message without changing progress
106
+ * Set a resource as in-progress
105
107
  */
106
- addEvent(message) {
107
- this.eventLines.push(message);
108
- if (this.eventLines.length > this.maxVisibleEvents) {
109
- this.eventLines.shift();
110
- }
108
+ setInProgress(logicalId, resourceType) {
109
+ this.inProgressResources.set(logicalId, {
110
+ logicalId,
111
+ resourceType,
112
+ status: "in_progress"
113
+ });
114
+ this.render();
115
+ }
116
+ /**
117
+ * Mark a resource as complete (removes from in-progress)
118
+ */
119
+ setComplete(logicalId) {
120
+ this.inProgressResources.delete(logicalId);
111
121
  this.render();
112
122
  }
113
123
  /**
114
- * Clear the progress bar from the terminal
124
+ * Clear the progress bar from the terminal (only for TTY)
115
125
  */
116
126
  clear() {
127
+ if (!this.isTTY) return;
117
128
  for (let i = 0; i < this.lastLineCount; i++) {
118
129
  process.stdout.write("\x1B[A\x1B[2K");
119
130
  }
120
131
  this.lastLineCount = 0;
121
132
  }
122
133
  /**
123
- * Finish and clear the progress bar
134
+ * Finish the progress bar (don't clear - leave final state visible)
124
135
  */
125
136
  finish() {
126
137
  this.clear();
138
+ this.inProgressResources.clear();
139
+ const percentage = this.total > 0 ? this.completed / this.total : 0;
140
+ const filled = Math.round(this.barWidth * percentage);
141
+ 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`);
127
146
  }
128
147
  /**
129
- * Render the progress bar
148
+ * Render the progress bar with in-progress resources
130
149
  */
131
150
  render() {
132
151
  this.clear();
133
152
  const lines = [];
134
- for (const event of this.eventLines) {
135
- lines.push(event);
136
- }
137
- const percentage = this.total > 0 ? this.current / this.total : 0;
153
+ const percentage = this.total > 0 ? this.completed / this.total : 0;
138
154
  const filled = Math.round(this.barWidth * percentage);
139
155
  const empty = this.barWidth - filled;
140
156
  const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
141
- const count = chalk.cyan(`${this.current}/${this.total}`);
157
+ const count = chalk.cyan(`${this.completed}/${this.total}`);
142
158
  const labelText = chalk.bold(this.label);
143
159
  lines.push(`${labelText} ${bar} ${count} resources`);
144
- lines.push("");
145
- for (const line of lines) {
146
- process.stdout.write(line + "\n");
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
+ }
171
+ }
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
+ }
147
181
  }
148
- this.lastLineCount = lines.length;
149
182
  }
150
183
  };
151
184
  function setVerbose(enabled) {
@@ -208,6 +241,18 @@ function table(rows) {
208
241
  function newline() {
209
242
  console.log();
210
243
  }
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
+ }
211
256
 
212
257
  // src/aws/credentials.ts
213
258
  var DEFAULT_REGION = "us-east-1";
@@ -353,7 +398,7 @@ async function previewStackChanges(options) {
353
398
  const stackStatus = await getStackStatus(stackName, credentials, region);
354
399
  const changeSetName = `devramps-preview-${Date.now()}`;
355
400
  if (!stackStatus.exists) {
356
- info(` Stack ${stackName} will be created (new stack)`);
401
+ info(` Stack ${stackName} will be created (new stack) in account ${options.accountId} (${region || "default region"})`);
357
402
  return;
358
403
  }
359
404
  try {
@@ -376,7 +421,7 @@ async function previewStackChanges(options) {
376
421
  ChangeSetName: changeSetName
377
422
  })
378
423
  );
379
- logStackChanges(stackName, changeSetResponse.Changes || [], stackStatus.exists);
424
+ logStackChanges(stackName, changeSetResponse.Changes || [], stackStatus.exists, options.accountId, region);
380
425
  await client.send(
381
426
  new DeleteChangeSetCommand({
382
427
  StackName: stackName,
@@ -401,13 +446,13 @@ async function previewStackChanges(options) {
401
446
  verbose(` Could not preview changes for ${stackName}: ${errorMessage}`);
402
447
  }
403
448
  }
404
- function logStackChanges(stackName, changes, isUpdate) {
449
+ function logStackChanges(stackName, changes, isUpdate, accountId, region) {
405
450
  if (changes.length === 0) {
406
451
  verbose(` Stack ${stackName}: No changes`);
407
452
  return;
408
453
  }
409
454
  const action = isUpdate ? "update" : "create";
410
- info(` Stack ${stackName} will ${action} ${changes.length} resource(s):`);
455
+ info(` Stack ${stackName} will ${action} ${changes.length} resource(s) in account ${accountId} (${region || "default region"}):`);
411
456
  for (const change of changes) {
412
457
  const resourceChange = change.ResourceChange;
413
458
  if (!resourceChange) continue;
@@ -450,21 +495,6 @@ var SUCCESS_STATES = /* @__PURE__ */ new Set([
450
495
  "CREATE_COMPLETE",
451
496
  "UPDATE_COMPLETE"
452
497
  ]);
453
- function getStatusSymbol(status) {
454
- if (!status) return "?";
455
- if (status.includes("COMPLETE") && !status.includes("ROLLBACK")) return "\u2714";
456
- if (status.includes("FAILED") || status.includes("ROLLBACK")) return "\u2716";
457
- if (status.includes("IN_PROGRESS")) return "\u22EF";
458
- return "?";
459
- }
460
- function formatStackEvent(event) {
461
- const symbol = getStatusSymbol(event.ResourceStatus);
462
- const resourceType = event.ResourceType || "Unknown";
463
- const logicalId = event.LogicalResourceId || "Unknown";
464
- const status = event.ResourceStatus || "Unknown";
465
- const reason = event.ResourceStatusReason ? ` - ${event.ResourceStatusReason}` : "";
466
- return ` ${symbol} ${resourceType} (${logicalId}): ${status}${reason}`;
467
- }
468
498
  function isResourceComplete(status) {
469
499
  if (!status) return false;
470
500
  return status.includes("_COMPLETE") && !status.includes("ROLLBACK");
@@ -472,6 +502,7 @@ function isResourceComplete(status) {
472
502
  async function waitForStackWithProgress(client, stackName, operationStartTime, totalResources, maxWaitTime = 600, showProgress = true) {
473
503
  const seenEventIds = /* @__PURE__ */ new Set();
474
504
  const completedResources = /* @__PURE__ */ new Set();
505
+ const inProgressResources = /* @__PURE__ */ new Map();
475
506
  const startTime = Date.now();
476
507
  const pollInterval = 3e3;
477
508
  const progressBar = showProgress ? new ProgressBar(stackName, totalResources) : null;
@@ -500,11 +531,48 @@ async function waitForStackWithProgress(client, stackName, operationStartTime, t
500
531
  seenEventIds.add(event.EventId);
501
532
  }
502
533
  const logicalId = event.LogicalResourceId;
503
- if (logicalId && logicalId !== stackName && isResourceComplete(event.ResourceStatus)) {
504
- completedResources.add(logicalId);
505
- }
506
- if (progressBar) {
507
- progressBar.update(completedResources.size, formatStackEvent(event));
534
+ const resourceType = event.ResourceType || "Unknown";
535
+ const status = event.ResourceStatus || "";
536
+ 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)) {
550
+ 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
+ }
508
576
  }
509
577
  }
510
578
  const currentStatus = stack.StackStatus || "";
@@ -2548,30 +2616,39 @@ async function showDryRunPlan(plan) {
2548
2616
  info(`CI/CD Account: ${plan.cicdAccountId}`);
2549
2617
  info(`CI/CD Region: ${plan.cicdRegion}`);
2550
2618
  newline();
2551
- info("Phase 1: Org Stack");
2619
+ info("Org Stack:");
2552
2620
  info(` ${plan.orgStack.action}: ${plan.orgStack.stackName}`);
2553
- info(` Account: ${plan.orgStack.accountId}`);
2621
+ info(` Account: ${plan.orgStack.accountId}, Region: ${plan.orgStack.region}`);
2554
2622
  info(` Target accounts with bucket access: ${plan.orgStack.targetAccountIds.length}`);
2555
- newline();
2556
- info("Phase 2: Pipeline & Account Stacks (parallel)");
2557
- for (const stack of plan.pipelineStacks) {
2558
- info(` ${stack.action}: ${stack.stackName}`);
2559
- info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2623
+ if (plan.pipelineStacks.length > 0) {
2624
+ newline();
2625
+ info("Pipeline Stacks:");
2626
+ for (const stack of plan.pipelineStacks) {
2627
+ info(` ${stack.action}: ${stack.stackName}`);
2628
+ info(` Account: ${stack.accountId}, Region: ${stack.region}`);
2629
+ info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2630
+ }
2560
2631
  }
2561
- for (const stack of plan.accountStacks) {
2562
- info(` ${stack.action}: ${stack.stackName}`);
2563
- info(` Account: ${stack.accountId} (OIDC provider)`);
2632
+ if (plan.accountStacks.length > 0) {
2633
+ newline();
2634
+ info("Account Stacks:");
2635
+ for (const stack of plan.accountStacks) {
2636
+ info(` ${stack.action}: ${stack.stackName}`);
2637
+ info(` Account: ${stack.accountId}, Region: ${stack.region} (OIDC provider)`);
2638
+ }
2564
2639
  }
2565
- newline();
2566
- info("Phase 3: Stage Stacks (parallel)");
2567
- for (const stack of plan.stageStacks) {
2568
- info(` ${stack.action}: ${stack.stackName}`);
2569
- info(` Account: ${stack.accountId}, Region: ${stack.region}`);
2570
- info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2640
+ if (plan.stageStacks.length > 0) {
2641
+ newline();
2642
+ info("Stage Stacks:");
2643
+ for (const stack of plan.stageStacks) {
2644
+ info(` ${stack.action}: ${stack.stackName}`);
2645
+ info(` Account: ${stack.accountId}, Region: ${stack.region}`);
2646
+ info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2647
+ }
2571
2648
  }
2572
2649
  const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2573
2650
  newline();
2574
- info(`Total stacks to deploy: ${totalStacks}`);
2651
+ info(`Total stacks to deploy (in parallel): ${totalStacks}`);
2575
2652
  }
2576
2653
  async function confirmDeploymentPlan(plan) {
2577
2654
  const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
@@ -2593,22 +2670,23 @@ async function confirmDeploymentPlan(plan) {
2593
2670
  }
2594
2671
  async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, currentAccountId, options) {
2595
2672
  const results = { success: 0, failed: 0 };
2673
+ const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2596
2674
  newline();
2597
- header("Phase 1: Org Stack");
2598
- try {
2599
- await deployOrgStack(plan, pipelines, authData, currentAccountId, options);
2600
- results.success++;
2601
- success("Org stack deployed successfully");
2602
- } catch (error2) {
2603
- results.failed++;
2604
- error(`Org stack failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
2605
- throw error2;
2606
- }
2607
- newline();
2608
- header("Phase 2: Pipeline & Account Stacks (parallel)");
2609
- const totalPhase2Stacks = plan.pipelineStacks.length + plan.accountStacks.length;
2610
- info(`Deploying ${totalPhase2Stacks} stack(s) in parallel...`);
2675
+ header("Deploying All Stacks");
2676
+ info(`Deploying ${totalStacks} stack(s) in parallel...`);
2611
2677
  newline();
2678
+ const orgPromise = (async () => {
2679
+ try {
2680
+ await deployOrgStack(plan, pipelines, authData, currentAccountId, options);
2681
+ return { stack: plan.orgStack.stackName, success: true };
2682
+ } catch (error2) {
2683
+ return {
2684
+ stack: plan.orgStack.stackName,
2685
+ success: false,
2686
+ error: error2 instanceof Error ? error2.message : String(error2)
2687
+ };
2688
+ }
2689
+ })();
2612
2690
  const pipelinePromises = plan.pipelineStacks.map(async (stack) => {
2613
2691
  try {
2614
2692
  await deployPipelineStack(stack, authData, currentAccountId, options);
@@ -2633,27 +2711,6 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2633
2711
  };
2634
2712
  }
2635
2713
  });
2636
- const pipelineResults = await Promise.all(pipelinePromises);
2637
- const accountResults = await Promise.all(accountPromises);
2638
- newline();
2639
- let accountStacksFailed = false;
2640
- for (const result of [...pipelineResults, ...accountResults]) {
2641
- if (result.success) {
2642
- success(`${result.stack} deployed`);
2643
- results.success++;
2644
- } else {
2645
- error(`${result.stack} failed: ${result.error}`);
2646
- results.failed++;
2647
- }
2648
- }
2649
- accountStacksFailed = accountResults.some((r) => !r.success);
2650
- if (accountStacksFailed) {
2651
- warn("Some Account stacks failed. Stage stacks may fail if their account OIDC provider did not deploy.");
2652
- }
2653
- newline();
2654
- header("Phase 3: Stage Stacks (parallel)");
2655
- info(`Deploying ${plan.stageStacks.length} stack(s) in parallel...`);
2656
- newline();
2657
2714
  const stagePromises = plan.stageStacks.map(async (stack) => {
2658
2715
  try {
2659
2716
  await deployStageStack(stack, authData, currentAccountId, options);
@@ -2666,9 +2723,14 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2666
2723
  };
2667
2724
  }
2668
2725
  });
2669
- const phase3Results = await Promise.all(stagePromises);
2726
+ const allResults = await Promise.all([
2727
+ orgPromise,
2728
+ ...pipelinePromises,
2729
+ ...accountPromises,
2730
+ ...stagePromises
2731
+ ]);
2670
2732
  newline();
2671
- for (const result of phase3Results) {
2733
+ for (const result of allResults) {
2672
2734
  if (result.success) {
2673
2735
  success(`${result.stack} deployed`);
2674
2736
  results.success++;
@@ -2722,7 +2784,9 @@ async function deployOrgStack(plan, pipelines, authData, currentAccountId, optio
2722
2784
  template,
2723
2785
  accountId: cicdAccountId,
2724
2786
  region: cicdRegion,
2725
- credentials
2787
+ credentials,
2788
+ showProgress: false
2789
+ // Disable progress bar for parallel deployment
2726
2790
  };
2727
2791
  await previewStackChanges(deployOptions);
2728
2792
  await deployStack(deployOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devramps/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines",
5
5
  "main": "dist/index.js",
6
6
  "bin": {