@go-to-k/cdkd 0.3.6 → 0.4.1

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/README.md CHANGED
@@ -380,9 +380,15 @@ cdkd deploy Stack1 Stack2
380
380
  # Deploy all stacks
381
381
  cdkd deploy --all
382
382
 
383
- # Deploy with wildcard
383
+ # Deploy with wildcard (matched against the physical CloudFormation stack name)
384
384
  cdkd deploy 'My*'
385
385
 
386
+ # Deploy stacks under a CDK Stage using the hierarchical path (CDK CLI parity)
387
+ # Patterns containing '/' are routed to the CDK display path; both forms work:
388
+ cdkd deploy 'MyStage/*' # all stacks under MyStage
389
+ cdkd deploy MyStage/Api # specific stack by display path
390
+ cdkd deploy MyStage-Api # same stack by physical CloudFormation name
391
+
386
392
  # Deploy with context values
387
393
  cdkd deploy -c env=staging -c featureFlag=true
388
394
 
package/dist/cli.js CHANGED
@@ -527,6 +527,141 @@ var destroyOptions = [
527
527
  )
528
528
  ];
529
529
 
530
+ // src/utils/live-renderer.ts
531
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
532
+ var FRAME_INTERVAL_MS = 80;
533
+ var ESC = "\x1B[";
534
+ var LiveRenderer = class {
535
+ constructor(stream = process.stdout) {
536
+ this.stream = stream;
537
+ }
538
+ tasks = /* @__PURE__ */ new Map();
539
+ active = false;
540
+ spinnerIndex = 0;
541
+ interval = null;
542
+ linesDrawn = 0;
543
+ cursorHidden = false;
544
+ exitListener = null;
545
+ isActive() {
546
+ return this.active;
547
+ }
548
+ /**
549
+ * Enable the live renderer. No-op if stdout is not a TTY or if
550
+ * `CDKD_NO_LIVE=1`. Returns true if successfully enabled.
551
+ */
552
+ start() {
553
+ if (this.active)
554
+ return true;
555
+ if (!this.stream.isTTY)
556
+ return false;
557
+ if (process.env["CDKD_NO_LIVE"] === "1")
558
+ return false;
559
+ this.active = true;
560
+ this.hideCursor();
561
+ if (!this.exitListener) {
562
+ this.exitListener = () => this.showCursor();
563
+ process.on("exit", this.exitListener);
564
+ }
565
+ this.interval = setInterval(() => this.draw(), FRAME_INTERVAL_MS);
566
+ if (typeof this.interval.unref === "function")
567
+ this.interval.unref();
568
+ return true;
569
+ }
570
+ stop() {
571
+ if (!this.active)
572
+ return;
573
+ if (this.interval) {
574
+ clearInterval(this.interval);
575
+ this.interval = null;
576
+ }
577
+ this.clear();
578
+ this.showCursor();
579
+ if (this.exitListener) {
580
+ process.removeListener("exit", this.exitListener);
581
+ this.exitListener = null;
582
+ }
583
+ this.tasks.clear();
584
+ this.active = false;
585
+ }
586
+ addTask(id, label) {
587
+ this.tasks.set(id, { label, startedAt: Date.now() });
588
+ if (this.active)
589
+ this.draw();
590
+ }
591
+ removeTask(id) {
592
+ if (!this.tasks.delete(id))
593
+ return;
594
+ if (this.active)
595
+ this.draw();
596
+ }
597
+ /**
598
+ * Print content above the live area. Clears the live area, runs the writer,
599
+ * then redraws the live area. When the renderer is inactive, the writer
600
+ * runs directly so callers can use this unconditionally.
601
+ */
602
+ printAbove(write) {
603
+ if (!this.active) {
604
+ write();
605
+ return;
606
+ }
607
+ this.clear();
608
+ write();
609
+ this.draw();
610
+ }
611
+ clear() {
612
+ if (this.linesDrawn === 0)
613
+ return;
614
+ this.stream.write("\r");
615
+ for (let i = 0; i < this.linesDrawn; i++) {
616
+ this.stream.write(`${ESC}1A${ESC}2K`);
617
+ }
618
+ this.linesDrawn = 0;
619
+ }
620
+ draw() {
621
+ if (!this.active)
622
+ return;
623
+ this.clear();
624
+ if (this.tasks.size === 0)
625
+ return;
626
+ const frame = SPINNER_FRAMES[this.spinnerIndex % SPINNER_FRAMES.length];
627
+ this.spinnerIndex++;
628
+ const cols = this.stream.columns ?? 80;
629
+ const lines = [];
630
+ for (const task of this.tasks.values()) {
631
+ const elapsed = ((Date.now() - task.startedAt) / 1e3).toFixed(1);
632
+ const raw = ` ${frame} ${task.label} (${elapsed}s)`;
633
+ lines.push(this.truncate(raw, cols));
634
+ }
635
+ this.stream.write(lines.join("\n") + "\n");
636
+ this.linesDrawn = lines.length;
637
+ }
638
+ truncate(s, maxLen) {
639
+ if (s.length <= maxLen)
640
+ return s;
641
+ if (maxLen <= 1)
642
+ return "\u2026";
643
+ return s.substring(0, maxLen - 1) + "\u2026";
644
+ }
645
+ hideCursor() {
646
+ if (this.cursorHidden)
647
+ return;
648
+ this.stream.write(`${ESC}?25l`);
649
+ this.cursorHidden = true;
650
+ }
651
+ showCursor() {
652
+ if (!this.cursorHidden)
653
+ return;
654
+ this.stream.write(`${ESC}?25h`);
655
+ this.cursorHidden = false;
656
+ }
657
+ };
658
+ var globalRenderer = null;
659
+ function getLiveRenderer() {
660
+ if (!globalRenderer)
661
+ globalRenderer = new LiveRenderer();
662
+ return globalRenderer;
663
+ }
664
+
530
665
  // src/utils/logger.ts
531
666
  var colors = {
532
667
  reset: "\x1B[0m",
@@ -583,22 +718,26 @@ var ConsoleLogger = class {
583
718
  }
584
719
  debug(message, ...args) {
585
720
  if (this.shouldLog("debug")) {
586
- console.debug(this.formatMessage("debug", message, ...args));
721
+ const formatted = this.formatMessage("debug", message, ...args);
722
+ getLiveRenderer().printAbove(() => console.debug(formatted));
587
723
  }
588
724
  }
589
725
  info(message, ...args) {
590
726
  if (this.shouldLog("info")) {
591
- console.info(this.formatMessage("info", message, ...args));
727
+ const formatted = this.formatMessage("info", message, ...args);
728
+ getLiveRenderer().printAbove(() => console.info(formatted));
592
729
  }
593
730
  }
594
731
  warn(message, ...args) {
595
732
  if (this.shouldLog("warn")) {
596
- console.warn(this.formatMessage("warn", message, ...args));
733
+ const formatted = this.formatMessage("warn", message, ...args);
734
+ getLiveRenderer().printAbove(() => console.warn(formatted));
597
735
  }
598
736
  }
599
737
  error(message, ...args) {
600
738
  if (this.shouldLog("error")) {
601
- console.error(this.formatMessage("error", message, ...args));
739
+ const formatted = this.formatMessage("error", message, ...args);
740
+ getLiveRenderer().printAbove(() => console.error(formatted));
602
741
  }
603
742
  }
604
743
  /**
@@ -1243,6 +1382,7 @@ var AssemblyReader = class {
1243
1382
  }
1244
1383
  return {
1245
1384
  stackName,
1385
+ displayName: artifact.displayName ?? stackName,
1246
1386
  artifactId,
1247
1387
  template,
1248
1388
  assetManifestPath,
@@ -25839,6 +25979,57 @@ var IMPLICIT_DELETE_DEPENDENCIES = {
25839
25979
  ]
25840
25980
  };
25841
25981
 
25982
+ // src/deployment/retryable-errors.ts
25983
+ var RETRYABLE_ERROR_MESSAGE_PATTERNS = [
25984
+ // IAM propagation
25985
+ "cannot be assumed",
25986
+ "role defined for the function",
25987
+ "not authorized to perform",
25988
+ "execution role",
25989
+ "trust policy",
25990
+ "Role validation failed",
25991
+ "does not have required permissions",
25992
+ "Trusted Entity",
25993
+ "currently in the following state: Pending",
25994
+ // DELETE dependency ordering (parallel deletion race conditions)
25995
+ "has dependencies and cannot be deleted",
25996
+ "can't be deleted since it has",
25997
+ "DependencyViolation",
25998
+ // AWS eventual consistency (dependency just created but not yet visible)
25999
+ // e.g., RDS DBCluster referencing a just-created DBSubnetGroup
26000
+ "does not exist",
26001
+ // AppSync schema is being created asynchronously
26002
+ "Schema is currently being altered",
26003
+ // IAM principal not yet propagated to S3 bucket policy
26004
+ "Invalid principal in policy",
26005
+ // S3 bucket creation/deletion still in progress
26006
+ "conflicting conditional operation",
26007
+ // Secrets Manager: ForceDeleteWithoutRecovery may take a moment to propagate
26008
+ "scheduled for deletion",
26009
+ // DynamoDB Streams / Kinesis: IAM role not yet propagated
26010
+ "Cannot access stream",
26011
+ "Please ensure the role can perform",
26012
+ // KMS: IAM role not yet propagated for CreateGrant
26013
+ "KMS key is invalid for CreateGrant",
26014
+ // CloudWatch Logs SubscriptionFilter: Kinesis stream eventual consistency
26015
+ // or SubscriptionFilter role propagation. CW Logs probes the destination
26016
+ // by delivering a test message; if the stream is freshly ACTIVE or the
26017
+ // assumed role hasn't propagated, the probe fails with "Invalid request".
26018
+ "Could not deliver test message"
26019
+ ];
26020
+ var RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
26021
+ function isRetryableTransientError(error, message) {
26022
+ const metadata = error.$metadata;
26023
+ const statusCode = metadata?.httpStatusCode;
26024
+ if (statusCode !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(statusCode))
26025
+ return true;
26026
+ const cause = error.cause;
26027
+ const causeStatus = cause?.$metadata?.httpStatusCode;
26028
+ if (causeStatus !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(causeStatus))
26029
+ return true;
26030
+ return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
26031
+ }
26032
+
25842
26033
  // src/deployment/deploy-engine.ts
25843
26034
  var InterruptedError = class extends Error {
25844
26035
  constructor() {
@@ -25874,11 +26065,15 @@ var DeployEngine = class {
25874
26065
  this.logger.debug(`Starting deployment for stack: ${stackName}`);
25875
26066
  setCurrentStackName(stackName);
25876
26067
  await this.lockManager.acquireLockWithRetry(stackName, void 0, "deploy");
26068
+ const renderer = getLiveRenderer();
26069
+ renderer.start();
25877
26070
  this.interrupted = false;
25878
26071
  const sigintHandler = () => {
25879
- process.stderr.write(
25880
- "\nInterrupted \u2014 saving partial state after current operations complete...\n"
25881
- );
26072
+ renderer.printAbove(() => {
26073
+ process.stderr.write(
26074
+ "\nInterrupted \u2014 saving partial state after current operations complete...\n"
26075
+ );
26076
+ });
25882
26077
  this.interrupted = true;
25883
26078
  };
25884
26079
  process.on("SIGINT", sigintHandler);
@@ -25993,6 +26188,7 @@ var DeployEngine = class {
25993
26188
  durationMs
25994
26189
  };
25995
26190
  } finally {
26191
+ renderer.stop();
25996
26192
  process.removeListener("SIGINT", sigintHandler);
25997
26193
  try {
25998
26194
  await this.lockManager.releaseLock(stackName);
@@ -26421,6 +26617,10 @@ var DeployEngine = class {
26421
26617
  async provisionResource(logicalId, change, stateResources, stackName, template, parameterValues, conditions, counts, progress) {
26422
26618
  const resourceType = change.resourceType;
26423
26619
  const provider = this.providerRegistry.getProvider(resourceType);
26620
+ const renderer = getLiveRenderer();
26621
+ const needsReplacement = change.changeType === "UPDATE" && (change.propertyChanges?.some((pc) => pc.requiresReplacement) ?? false);
26622
+ const verb = change.changeType === "CREATE" ? "Creating" : change.changeType === "DELETE" ? "Deleting" : needsReplacement ? "Replacing" : "Updating";
26623
+ renderer.addTask(logicalId, `${verb} ${logicalId} (${resourceType})`);
26424
26624
  try {
26425
26625
  switch (change.changeType) {
26426
26626
  case "CREATE": {
@@ -26452,6 +26652,7 @@ var DeployEngine = class {
26452
26652
  if (progress)
26453
26653
  progress.current++;
26454
26654
  const createPrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
26655
+ renderer.removeTask(logicalId);
26455
26656
  this.logger.info(`${createPrefix}\u2705 ${logicalId} (${resourceType}) created`);
26456
26657
  break;
26457
26658
  }
@@ -26479,9 +26680,9 @@ var DeployEngine = class {
26479
26680
  counts.skipped++;
26480
26681
  break;
26481
26682
  }
26482
- const needsReplacement = change.propertyChanges?.some((pc) => pc.requiresReplacement);
26683
+ const needsReplacement2 = change.propertyChanges?.some((pc) => pc.requiresReplacement);
26483
26684
  const dependencies = this.extractAllDependencies(template, logicalId);
26484
- if (needsReplacement) {
26685
+ if (needsReplacement2) {
26485
26686
  const replacedProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
26486
26687
  this.logger.info(
26487
26688
  `Replacing ${logicalId} (${resourceType}) - immutable properties changed: ${replacedProps}`
@@ -26525,6 +26726,7 @@ var DeployEngine = class {
26525
26726
  if (progress)
26526
26727
  progress.current++;
26527
26728
  const replacePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
26729
+ renderer.removeTask(logicalId);
26528
26730
  this.logger.info(`${replacePrefix}\u2705 ${logicalId} (${resourceType}) replaced`);
26529
26731
  } else {
26530
26732
  this.logger.debug(`Updating ${logicalId} (${resourceType})`);
@@ -26600,6 +26802,7 @@ var DeployEngine = class {
26600
26802
  if (progress)
26601
26803
  progress.current++;
26602
26804
  const updatePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
26805
+ renderer.removeTask(logicalId);
26603
26806
  this.logger.info(`${updatePrefix}\u2705 ${logicalId} (${resourceType}) updated`);
26604
26807
  }
26605
26808
  break;
@@ -26645,11 +26848,13 @@ var DeployEngine = class {
26645
26848
  if (progress)
26646
26849
  progress.current++;
26647
26850
  const deletePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
26851
+ renderer.removeTask(logicalId);
26648
26852
  this.logger.info(`${deletePrefix}\u2705 ${logicalId} (${resourceType}) deleted`);
26649
26853
  break;
26650
26854
  }
26651
26855
  }
26652
26856
  } catch (error) {
26857
+ renderer.removeTask(logicalId);
26653
26858
  const message = error instanceof Error ? error.message : String(error);
26654
26859
  this.logger.error(`Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`);
26655
26860
  throw new ProvisioningError(
@@ -26659,6 +26864,8 @@ var DeployEngine = class {
26659
26864
  stateResources[logicalId]?.physicalId,
26660
26865
  error instanceof Error ? error : void 0
26661
26866
  );
26867
+ } finally {
26868
+ renderer.removeTask(logicalId);
26662
26869
  }
26663
26870
  }
26664
26871
  /**
@@ -26819,7 +27026,7 @@ var DeployEngine = class {
26819
27026
  } catch (error) {
26820
27027
  lastError = error;
26821
27028
  const message = error instanceof Error ? error.message : String(error);
26822
- const isRetryable = this.isRetryableError(error, message);
27029
+ const isRetryable = isRetryableTransientError(error, message);
26823
27030
  if (!isRetryable || attempt >= maxRetries) {
26824
27031
  throw error;
26825
27032
  }
@@ -26836,53 +27043,6 @@ var DeployEngine = class {
26836
27043
  }
26837
27044
  throw lastError;
26838
27045
  }
26839
- /**
26840
- * Determine if an error is retryable (transient).
26841
- * Checks HTTP status codes (429 throttle, 503 unavailable)
26842
- * and IAM propagation delay message patterns.
26843
- */
26844
- isRetryableError(error, message) {
26845
- const metadata = error.$metadata;
26846
- const statusCode = metadata?.httpStatusCode;
26847
- if (statusCode === 429 || statusCode === 503)
26848
- return true;
26849
- const cause = error.cause;
26850
- const causeStatus = cause?.$metadata?.httpStatusCode;
26851
- if (causeStatus === 429 || causeStatus === 503)
26852
- return true;
26853
- const retryablePatterns = [
26854
- "cannot be assumed",
26855
- "role defined for the function",
26856
- "not authorized to perform",
26857
- "execution role",
26858
- "trust policy",
26859
- "Role validation failed",
26860
- "does not have required permissions",
26861
- "Trusted Entity",
26862
- "currently in the following state: Pending",
26863
- // DELETE dependency ordering (parallel deletion race conditions)
26864
- "has dependencies and cannot be deleted",
26865
- "can't be deleted since it has",
26866
- "DependencyViolation",
26867
- // AWS eventual consistency (dependency just created but not yet visible)
26868
- // e.g., RDS DBCluster referencing a just-created DBSubnetGroup
26869
- "does not exist",
26870
- // AppSync schema is being created asynchronously
26871
- "Schema is currently being altered",
26872
- // IAM principal not yet propagated to S3 bucket policy
26873
- "Invalid principal in policy",
26874
- // S3 bucket creation/deletion still in progress
26875
- "conflicting conditional operation",
26876
- // Secrets Manager: ForceDeleteWithoutRecovery may take a moment to propagate
26877
- "scheduled for deletion",
26878
- // DynamoDB Streams / Kinesis: IAM role not yet propagated
26879
- "Cannot access stream",
26880
- "Please ensure the role can perform",
26881
- // KMS: IAM role not yet propagated for CreateGrant
26882
- "KMS key is invalid for CreateGrant"
26883
- ];
26884
- return retryablePatterns.some((p) => message.includes(p));
26885
- }
26886
27046
  /**
26887
27047
  * Resolve stack outputs from template and resource attributes
26888
27048
  *
@@ -26922,10 +27082,43 @@ var DeployEngine = class {
26922
27082
 
26923
27083
  // src/cli/commands/deploy.ts
26924
27084
  init_aws_clients();
27085
+
27086
+ // src/cli/stack-matcher.ts
27087
+ function matchStacks(stacks, patterns) {
27088
+ if (patterns.length === 0)
27089
+ return [];
27090
+ const seen = /* @__PURE__ */ new Set();
27091
+ const result = [];
27092
+ for (const stack of stacks) {
27093
+ const matched = patterns.some((pattern) => stackMatchesPattern(stack, pattern));
27094
+ if (matched && !seen.has(stack.stackName)) {
27095
+ seen.add(stack.stackName);
27096
+ result.push(stack);
27097
+ }
27098
+ }
27099
+ return result;
27100
+ }
27101
+ function describeStack(stack) {
27102
+ if (stack.displayName && stack.displayName !== stack.stackName) {
27103
+ return `${stack.stackName} (${stack.displayName})`;
27104
+ }
27105
+ return stack.stackName;
27106
+ }
27107
+ function stackMatchesPattern(stack, pattern) {
27108
+ const target = pattern.includes("/") ? stack.displayName ?? stack.stackName : stack.stackName;
27109
+ if (pattern.includes("*")) {
27110
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
27111
+ return regex.test(target);
27112
+ }
27113
+ return target === pattern;
27114
+ }
27115
+
27116
+ // src/cli/commands/deploy.ts
26925
27117
  async function deployCommand(stacks, options) {
26926
27118
  const logger = getLogger();
26927
27119
  if (options.verbose) {
26928
27120
  logger.setLevel("debug");
27121
+ process.env["CDKD_NO_LIVE"] = "1";
26929
27122
  }
26930
27123
  if (!options.wait) {
26931
27124
  process.env["CDKD_NO_WAIT"] = "true";
@@ -26983,21 +27176,17 @@ async function deployCommand(stacks, options) {
26983
27176
  if (options.all) {
26984
27177
  targetStacks = allStacks;
26985
27178
  } else if (stackPatterns.length > 0) {
26986
- targetStacks = allStacks.filter(
26987
- (s) => stackPatterns.some(
26988
- (pattern) => pattern.includes("*") ? new RegExp("^" + pattern.replace(/\*/g, ".*") + "$").test(s.stackName) : s.stackName === pattern
26989
- )
26990
- );
27179
+ targetStacks = matchStacks(allStacks, stackPatterns);
26991
27180
  } else if (allStacks.length === 1) {
26992
27181
  targetStacks = allStacks;
26993
27182
  } else {
26994
27183
  throw new Error(
26995
- `Multiple stacks found: ${allStacks.map((s) => s.stackName).join(", ")}. Specify stack name(s) or use --all`
27184
+ `Multiple stacks found: ${allStacks.map(describeStack).join(", ")}. Specify stack name(s) or use --all`
26996
27185
  );
26997
27186
  }
26998
27187
  if (targetStacks.length === 0) {
26999
27188
  throw new Error(
27000
- stackPatterns.length > 0 ? `No stacks matching ${stackPatterns.join(", ")} found in assembly. Available: ${allStacks.map((s) => s.stackName).join(", ")}` : "No stacks found in assembly"
27189
+ stackPatterns.length > 0 ? `No stacks matching ${stackPatterns.join(", ")} found in assembly. Available: ${allStacks.map(describeStack).join(", ")}` : "No stacks found in assembly"
27001
27190
  );
27002
27191
  }
27003
27192
  if (!options.exclusively) {
@@ -27158,7 +27347,10 @@ Deploying stack: ${stackInfo.stackName}${stackRegion !== baseRegion ? ` (region:
27158
27347
  }
27159
27348
  }
27160
27349
  function createDeployCommand() {
27161
- const cmd = new Command3("deploy").description("Deploy CDK app using SDK/Cloud Control API").argument("[stacks...]", "Stack name(s) to deploy (supports wildcards)").option("--all", "Deploy all stacks", false).action(withErrorHandling(deployCommand));
27350
+ const cmd = new Command3("deploy").description("Deploy CDK app using SDK/Cloud Control API").argument(
27351
+ "[stacks...]",
27352
+ "Stack name(s) to deploy. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*')."
27353
+ ).option("--all", "Deploy all stacks", false).action(withErrorHandling(deployCommand));
27162
27354
  [
27163
27355
  ...commonOptions,
27164
27356
  ...appOptions,
@@ -27269,21 +27461,17 @@ async function diffCommand(stacks, options) {
27269
27461
  if (options.all) {
27270
27462
  targetStacks = allStacks;
27271
27463
  } else if (stackPatterns.length > 0) {
27272
- targetStacks = allStacks.filter(
27273
- (s) => stackPatterns.some(
27274
- (pattern) => pattern.includes("*") ? new RegExp("^" + pattern.replace(/\*/g, ".*") + "$").test(s.stackName) : s.stackName === pattern
27275
- )
27276
- );
27464
+ targetStacks = matchStacks(allStacks, stackPatterns);
27277
27465
  } else if (allStacks.length === 1) {
27278
27466
  targetStacks = allStacks;
27279
27467
  } else {
27280
27468
  throw new Error(
27281
- `Multiple stacks found: ${allStacks.map((s) => s.stackName).join(", ")}. Specify stack name(s) or use --all`
27469
+ `Multiple stacks found: ${allStacks.map(describeStack).join(", ")}. Specify stack name(s) or use --all`
27282
27470
  );
27283
27471
  }
27284
27472
  if (targetStacks.length === 0) {
27285
27473
  throw new Error(
27286
- stackPatterns.length > 0 ? `No stacks matching ${stackPatterns.join(", ")} found in assembly` : "No stacks found in assembly"
27474
+ stackPatterns.length > 0 ? `No stacks matching ${stackPatterns.join(", ")} found in assembly. Available: ${allStacks.map(describeStack).join(", ")}` : "No stacks found in assembly"
27287
27475
  );
27288
27476
  }
27289
27477
  const stateConfig = {
@@ -27373,7 +27561,10 @@ ${createCount} to create, ${updateCount} to update, ${deleteCount} to delete`);
27373
27561
  }
27374
27562
  }
27375
27563
  function createDiffCommand() {
27376
- const cmd = new Command4("diff").description("Show difference between current state and desired state").argument("[stacks...]", "Stack name(s) to diff (supports wildcards)").option("--all", "Diff all stacks", false).action(withErrorHandling(diffCommand));
27564
+ const cmd = new Command4("diff").description("Show difference between current state and desired state").argument(
27565
+ "[stacks...]",
27566
+ "Stack name(s) to diff. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*')."
27567
+ ).option("--all", "Diff all stacks", false).action(withErrorHandling(diffCommand));
27377
27568
  [...commonOptions, ...appOptions, ...stateOptions, ...stackOptions, ...contextOptions].forEach(
27378
27569
  (opt) => cmd.addOption(opt)
27379
27570
  );
@@ -27388,6 +27579,7 @@ async function destroyCommand(stackArgs, options) {
27388
27579
  const logger = getLogger();
27389
27580
  if (options.verbose) {
27390
27581
  logger.setLevel("debug");
27582
+ process.env["CDKD_NO_LIVE"] = "1";
27391
27583
  }
27392
27584
  const region = options.region || process.env["AWS_REGION"] || "us-east-1";
27393
27585
  const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
@@ -27415,7 +27607,7 @@ async function destroyCommand(stackArgs, options) {
27415
27607
  registerAllProviders(providerRegistry);
27416
27608
  providerRegistry.setCustomResourceResponseBucket(stateBucket);
27417
27609
  const appCmd = options.app || resolveApp();
27418
- let appStackNames = [];
27610
+ let appStacks = [];
27419
27611
  if (appCmd) {
27420
27612
  try {
27421
27613
  const synthesizer = new Synthesizer();
@@ -27425,17 +27617,21 @@ async function destroyCommand(stackArgs, options) {
27425
27617
  output: options.output || "cdk.out",
27426
27618
  ...Object.keys(context).length > 0 && { context }
27427
27619
  });
27428
- appStackNames = result.stacks.map((s) => s.stackName);
27620
+ appStacks = result.stacks.map((s) => ({
27621
+ stackName: s.stackName,
27622
+ displayName: s.displayName
27623
+ }));
27429
27624
  } catch {
27430
27625
  logger.debug("Could not synthesize app, falling back to state-based stack list");
27431
27626
  }
27432
27627
  }
27433
27628
  const allStateStacks = await stateBackend.listStacks();
27434
27629
  let candidateStacks;
27435
- if (appStackNames.length > 0) {
27436
- candidateStacks = appStackNames.filter((name) => allStateStacks.includes(name));
27630
+ if (appStacks.length > 0) {
27631
+ const stateSet = new Set(allStateStacks);
27632
+ candidateStacks = appStacks.filter((s) => stateSet.has(s.stackName));
27437
27633
  } else if (stackArgs.length > 0 || options.stack || options.all) {
27438
- candidateStacks = allStateStacks;
27634
+ candidateStacks = allStateStacks.map((name) => ({ stackName: name }));
27439
27635
  } else {
27440
27636
  throw new Error(
27441
27637
  "Could not determine which stacks belong to this app. Specify stack names explicitly, use --all, or ensure --app / cdk.json is configured."
@@ -27444,21 +27640,17 @@ async function destroyCommand(stackArgs, options) {
27444
27640
  const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
27445
27641
  let stackNames;
27446
27642
  if (options.all) {
27447
- stackNames = candidateStacks;
27643
+ stackNames = candidateStacks.map((s) => s.stackName);
27448
27644
  } else if (stackPatterns.length > 0) {
27449
- stackNames = candidateStacks.filter(
27450
- (name) => stackPatterns.some(
27451
- (pattern) => pattern.includes("*") ? new RegExp("^" + pattern.replace(/\*/g, ".*") + "$").test(name) : name === pattern
27452
- )
27453
- );
27645
+ stackNames = matchStacks(candidateStacks, stackPatterns).map((s) => s.stackName);
27454
27646
  } else if (candidateStacks.length === 1) {
27455
- stackNames = candidateStacks;
27647
+ stackNames = candidateStacks.map((s) => s.stackName);
27456
27648
  } else if (candidateStacks.length === 0) {
27457
27649
  logger.info("No stacks found in state");
27458
27650
  return;
27459
27651
  } else {
27460
27652
  throw new Error(
27461
- `Multiple stacks found: ${candidateStacks.join(", ")}. Specify stack name(s) or use --all`
27653
+ `Multiple stacks found: ${candidateStacks.map(describeStack).join(", ")}. Specify stack name(s) or use --all`
27462
27654
  );
27463
27655
  }
27464
27656
  if (stackNames.length === 0) {
@@ -27522,6 +27714,8 @@ Are you sure you want to destroy stack "${stackName}" and delete all ${resourceC
27522
27714
  logger.info(`
27523
27715
  Acquiring lock for stack ${stackName}...`);
27524
27716
  await lockManager.acquireLock(stackName, "destroy");
27717
+ const renderer = getLiveRenderer();
27718
+ renderer.start();
27525
27719
  try {
27526
27720
  logger.info("Building dependency graph...");
27527
27721
  const template = {
@@ -27585,6 +27779,7 @@ Acquiring lock for stack ${stackName}...`);
27585
27779
  logger.warn(`Resource ${logicalId} not found in state, skipping`);
27586
27780
  return;
27587
27781
  }
27782
+ renderer.addTask(logicalId, `Deleting ${logicalId} (${resource.resourceType})`);
27588
27783
  try {
27589
27784
  const provider = destroyProviderRegistry.getProvider(resource.resourceType);
27590
27785
  let lastDeleteError;
@@ -27613,9 +27808,11 @@ Acquiring lock for stack ${stackName}...`);
27613
27808
  }
27614
27809
  if (lastDeleteError)
27615
27810
  throw lastDeleteError;
27811
+ renderer.removeTask(logicalId);
27616
27812
  logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
27617
27813
  deletedCount++;
27618
27814
  } catch (error) {
27815
+ renderer.removeTask(logicalId);
27619
27816
  const msg = error instanceof Error ? error.message : String(error);
27620
27817
  if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
27621
27818
  logger.debug(` ${logicalId} already deleted, removing from state`);
@@ -27624,6 +27821,8 @@ Acquiring lock for stack ${stackName}...`);
27624
27821
  logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
27625
27822
  errorCount++;
27626
27823
  }
27824
+ } finally {
27825
+ renderer.removeTask(logicalId);
27627
27826
  }
27628
27827
  });
27629
27828
  await Promise.all(deletePromises);
@@ -27639,6 +27838,7 @@ Acquiring lock for stack ${stackName}...`);
27639
27838
  \u2713 Stack ${stackName} destroyed (${deletedCount} deleted, ${errorCount} errors)`
27640
27839
  );
27641
27840
  } finally {
27841
+ renderer.stop();
27642
27842
  logger.debug("Releasing lock...");
27643
27843
  await lockManager.releaseLock(stackName);
27644
27844
  if (destroyAwsClients) {
@@ -27654,7 +27854,10 @@ Acquiring lock for stack ${stackName}...`);
27654
27854
  }
27655
27855
  }
27656
27856
  function createDestroyCommand() {
27657
- const cmd = new Command5("destroy").description("Destroy all resources in the stack").argument("[stacks...]", "Stack name(s) to destroy (supports wildcards)").option("--all", "Destroy all stacks", false).action(withErrorHandling(destroyCommand));
27857
+ const cmd = new Command5("destroy").description("Destroy all resources in the stack").argument(
27858
+ "[stacks...]",
27859
+ "Stack name(s) to destroy. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*')."
27860
+ ).option("--all", "Destroy all stacks", false).action(withErrorHandling(destroyCommand));
27658
27861
  [
27659
27862
  ...commonOptions,
27660
27863
  ...appOptions,
@@ -27768,7 +27971,7 @@ function reorderArgs(argv) {
27768
27971
  }
27769
27972
  async function main() {
27770
27973
  const program = new Command8();
27771
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.3.6");
27974
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.4.1");
27772
27975
  program.addCommand(createBootstrapCommand());
27773
27976
  program.addCommand(createSynthCommand());
27774
27977
  program.addCommand(createDeployCommand());