@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.
Binary file
package/dist/index.js CHANGED
@@ -432,6 +432,141 @@ var init_aws_clients = __esm({
432
432
  }
433
433
  });
434
434
 
435
+ // src/utils/live-renderer.ts
436
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
437
+ var FRAME_INTERVAL_MS = 80;
438
+ var ESC = "\x1B[";
439
+ var LiveRenderer = class {
440
+ constructor(stream = process.stdout) {
441
+ this.stream = stream;
442
+ }
443
+ tasks = /* @__PURE__ */ new Map();
444
+ active = false;
445
+ spinnerIndex = 0;
446
+ interval = null;
447
+ linesDrawn = 0;
448
+ cursorHidden = false;
449
+ exitListener = null;
450
+ isActive() {
451
+ return this.active;
452
+ }
453
+ /**
454
+ * Enable the live renderer. No-op if stdout is not a TTY or if
455
+ * `CDKD_NO_LIVE=1`. Returns true if successfully enabled.
456
+ */
457
+ start() {
458
+ if (this.active)
459
+ return true;
460
+ if (!this.stream.isTTY)
461
+ return false;
462
+ if (process.env["CDKD_NO_LIVE"] === "1")
463
+ return false;
464
+ this.active = true;
465
+ this.hideCursor();
466
+ if (!this.exitListener) {
467
+ this.exitListener = () => this.showCursor();
468
+ process.on("exit", this.exitListener);
469
+ }
470
+ this.interval = setInterval(() => this.draw(), FRAME_INTERVAL_MS);
471
+ if (typeof this.interval.unref === "function")
472
+ this.interval.unref();
473
+ return true;
474
+ }
475
+ stop() {
476
+ if (!this.active)
477
+ return;
478
+ if (this.interval) {
479
+ clearInterval(this.interval);
480
+ this.interval = null;
481
+ }
482
+ this.clear();
483
+ this.showCursor();
484
+ if (this.exitListener) {
485
+ process.removeListener("exit", this.exitListener);
486
+ this.exitListener = null;
487
+ }
488
+ this.tasks.clear();
489
+ this.active = false;
490
+ }
491
+ addTask(id, label) {
492
+ this.tasks.set(id, { label, startedAt: Date.now() });
493
+ if (this.active)
494
+ this.draw();
495
+ }
496
+ removeTask(id) {
497
+ if (!this.tasks.delete(id))
498
+ return;
499
+ if (this.active)
500
+ this.draw();
501
+ }
502
+ /**
503
+ * Print content above the live area. Clears the live area, runs the writer,
504
+ * then redraws the live area. When the renderer is inactive, the writer
505
+ * runs directly so callers can use this unconditionally.
506
+ */
507
+ printAbove(write) {
508
+ if (!this.active) {
509
+ write();
510
+ return;
511
+ }
512
+ this.clear();
513
+ write();
514
+ this.draw();
515
+ }
516
+ clear() {
517
+ if (this.linesDrawn === 0)
518
+ return;
519
+ this.stream.write("\r");
520
+ for (let i = 0; i < this.linesDrawn; i++) {
521
+ this.stream.write(`${ESC}1A${ESC}2K`);
522
+ }
523
+ this.linesDrawn = 0;
524
+ }
525
+ draw() {
526
+ if (!this.active)
527
+ return;
528
+ this.clear();
529
+ if (this.tasks.size === 0)
530
+ return;
531
+ const frame = SPINNER_FRAMES[this.spinnerIndex % SPINNER_FRAMES.length];
532
+ this.spinnerIndex++;
533
+ const cols = this.stream.columns ?? 80;
534
+ const lines = [];
535
+ for (const task of this.tasks.values()) {
536
+ const elapsed = ((Date.now() - task.startedAt) / 1e3).toFixed(1);
537
+ const raw = ` ${frame} ${task.label} (${elapsed}s)`;
538
+ lines.push(this.truncate(raw, cols));
539
+ }
540
+ this.stream.write(lines.join("\n") + "\n");
541
+ this.linesDrawn = lines.length;
542
+ }
543
+ truncate(s, maxLen) {
544
+ if (s.length <= maxLen)
545
+ return s;
546
+ if (maxLen <= 1)
547
+ return "\u2026";
548
+ return s.substring(0, maxLen - 1) + "\u2026";
549
+ }
550
+ hideCursor() {
551
+ if (this.cursorHidden)
552
+ return;
553
+ this.stream.write(`${ESC}?25l`);
554
+ this.cursorHidden = true;
555
+ }
556
+ showCursor() {
557
+ if (!this.cursorHidden)
558
+ return;
559
+ this.stream.write(`${ESC}?25h`);
560
+ this.cursorHidden = false;
561
+ }
562
+ };
563
+ var globalRenderer = null;
564
+ function getLiveRenderer() {
565
+ if (!globalRenderer)
566
+ globalRenderer = new LiveRenderer();
567
+ return globalRenderer;
568
+ }
569
+
435
570
  // src/utils/logger.ts
436
571
  var colors = {
437
572
  reset: "\x1B[0m",
@@ -488,22 +623,26 @@ var ConsoleLogger = class {
488
623
  }
489
624
  debug(message, ...args) {
490
625
  if (this.shouldLog("debug")) {
491
- console.debug(this.formatMessage("debug", message, ...args));
626
+ const formatted = this.formatMessage("debug", message, ...args);
627
+ getLiveRenderer().printAbove(() => console.debug(formatted));
492
628
  }
493
629
  }
494
630
  info(message, ...args) {
495
631
  if (this.shouldLog("info")) {
496
- console.info(this.formatMessage("info", message, ...args));
632
+ const formatted = this.formatMessage("info", message, ...args);
633
+ getLiveRenderer().printAbove(() => console.info(formatted));
497
634
  }
498
635
  }
499
636
  warn(message, ...args) {
500
637
  if (this.shouldLog("warn")) {
501
- console.warn(this.formatMessage("warn", message, ...args));
638
+ const formatted = this.formatMessage("warn", message, ...args);
639
+ getLiveRenderer().printAbove(() => console.warn(formatted));
502
640
  }
503
641
  }
504
642
  error(message, ...args) {
505
643
  if (this.shouldLog("error")) {
506
- console.error(this.formatMessage("error", message, ...args));
644
+ const formatted = this.formatMessage("error", message, ...args);
645
+ getLiveRenderer().printAbove(() => console.error(formatted));
507
646
  }
508
647
  }
509
648
  /**
@@ -934,6 +1073,7 @@ var AssemblyReader = class {
934
1073
  }
935
1074
  return {
936
1075
  stackName,
1076
+ displayName: artifact.displayName ?? stackName,
937
1077
  artifactId,
938
1078
  template,
939
1079
  assetManifestPath,
@@ -7212,6 +7352,57 @@ var IMPLICIT_DELETE_DEPENDENCIES = {
7212
7352
  ]
7213
7353
  };
7214
7354
 
7355
+ // src/deployment/retryable-errors.ts
7356
+ var RETRYABLE_ERROR_MESSAGE_PATTERNS = [
7357
+ // IAM propagation
7358
+ "cannot be assumed",
7359
+ "role defined for the function",
7360
+ "not authorized to perform",
7361
+ "execution role",
7362
+ "trust policy",
7363
+ "Role validation failed",
7364
+ "does not have required permissions",
7365
+ "Trusted Entity",
7366
+ "currently in the following state: Pending",
7367
+ // DELETE dependency ordering (parallel deletion race conditions)
7368
+ "has dependencies and cannot be deleted",
7369
+ "can't be deleted since it has",
7370
+ "DependencyViolation",
7371
+ // AWS eventual consistency (dependency just created but not yet visible)
7372
+ // e.g., RDS DBCluster referencing a just-created DBSubnetGroup
7373
+ "does not exist",
7374
+ // AppSync schema is being created asynchronously
7375
+ "Schema is currently being altered",
7376
+ // IAM principal not yet propagated to S3 bucket policy
7377
+ "Invalid principal in policy",
7378
+ // S3 bucket creation/deletion still in progress
7379
+ "conflicting conditional operation",
7380
+ // Secrets Manager: ForceDeleteWithoutRecovery may take a moment to propagate
7381
+ "scheduled for deletion",
7382
+ // DynamoDB Streams / Kinesis: IAM role not yet propagated
7383
+ "Cannot access stream",
7384
+ "Please ensure the role can perform",
7385
+ // KMS: IAM role not yet propagated for CreateGrant
7386
+ "KMS key is invalid for CreateGrant",
7387
+ // CloudWatch Logs SubscriptionFilter: Kinesis stream eventual consistency
7388
+ // or SubscriptionFilter role propagation. CW Logs probes the destination
7389
+ // by delivering a test message; if the stream is freshly ACTIVE or the
7390
+ // assumed role hasn't propagated, the probe fails with "Invalid request".
7391
+ "Could not deliver test message"
7392
+ ];
7393
+ var RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
7394
+ function isRetryableTransientError(error, message) {
7395
+ const metadata = error.$metadata;
7396
+ const statusCode = metadata?.httpStatusCode;
7397
+ if (statusCode !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(statusCode))
7398
+ return true;
7399
+ const cause = error.cause;
7400
+ const causeStatus = cause?.$metadata?.httpStatusCode;
7401
+ if (causeStatus !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(causeStatus))
7402
+ return true;
7403
+ return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
7404
+ }
7405
+
7215
7406
  // src/deployment/deploy-engine.ts
7216
7407
  var InterruptedError = class extends Error {
7217
7408
  constructor() {
@@ -7247,11 +7438,15 @@ var DeployEngine = class {
7247
7438
  this.logger.debug(`Starting deployment for stack: ${stackName}`);
7248
7439
  setCurrentStackName(stackName);
7249
7440
  await this.lockManager.acquireLockWithRetry(stackName, void 0, "deploy");
7441
+ const renderer = getLiveRenderer();
7442
+ renderer.start();
7250
7443
  this.interrupted = false;
7251
7444
  const sigintHandler = () => {
7252
- process.stderr.write(
7253
- "\nInterrupted \u2014 saving partial state after current operations complete...\n"
7254
- );
7445
+ renderer.printAbove(() => {
7446
+ process.stderr.write(
7447
+ "\nInterrupted \u2014 saving partial state after current operations complete...\n"
7448
+ );
7449
+ });
7255
7450
  this.interrupted = true;
7256
7451
  };
7257
7452
  process.on("SIGINT", sigintHandler);
@@ -7366,6 +7561,7 @@ var DeployEngine = class {
7366
7561
  durationMs
7367
7562
  };
7368
7563
  } finally {
7564
+ renderer.stop();
7369
7565
  process.removeListener("SIGINT", sigintHandler);
7370
7566
  try {
7371
7567
  await this.lockManager.releaseLock(stackName);
@@ -7794,6 +7990,10 @@ var DeployEngine = class {
7794
7990
  async provisionResource(logicalId, change, stateResources, stackName, template, parameterValues, conditions, counts, progress) {
7795
7991
  const resourceType = change.resourceType;
7796
7992
  const provider = this.providerRegistry.getProvider(resourceType);
7993
+ const renderer = getLiveRenderer();
7994
+ const needsReplacement = change.changeType === "UPDATE" && (change.propertyChanges?.some((pc) => pc.requiresReplacement) ?? false);
7995
+ const verb = change.changeType === "CREATE" ? "Creating" : change.changeType === "DELETE" ? "Deleting" : needsReplacement ? "Replacing" : "Updating";
7996
+ renderer.addTask(logicalId, `${verb} ${logicalId} (${resourceType})`);
7797
7997
  try {
7798
7998
  switch (change.changeType) {
7799
7999
  case "CREATE": {
@@ -7825,6 +8025,7 @@ var DeployEngine = class {
7825
8025
  if (progress)
7826
8026
  progress.current++;
7827
8027
  const createPrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
8028
+ renderer.removeTask(logicalId);
7828
8029
  this.logger.info(`${createPrefix}\u2705 ${logicalId} (${resourceType}) created`);
7829
8030
  break;
7830
8031
  }
@@ -7852,9 +8053,9 @@ var DeployEngine = class {
7852
8053
  counts.skipped++;
7853
8054
  break;
7854
8055
  }
7855
- const needsReplacement = change.propertyChanges?.some((pc) => pc.requiresReplacement);
8056
+ const needsReplacement2 = change.propertyChanges?.some((pc) => pc.requiresReplacement);
7856
8057
  const dependencies = this.extractAllDependencies(template, logicalId);
7857
- if (needsReplacement) {
8058
+ if (needsReplacement2) {
7858
8059
  const replacedProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
7859
8060
  this.logger.info(
7860
8061
  `Replacing ${logicalId} (${resourceType}) - immutable properties changed: ${replacedProps}`
@@ -7898,6 +8099,7 @@ var DeployEngine = class {
7898
8099
  if (progress)
7899
8100
  progress.current++;
7900
8101
  const replacePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
8102
+ renderer.removeTask(logicalId);
7901
8103
  this.logger.info(`${replacePrefix}\u2705 ${logicalId} (${resourceType}) replaced`);
7902
8104
  } else {
7903
8105
  this.logger.debug(`Updating ${logicalId} (${resourceType})`);
@@ -7973,6 +8175,7 @@ var DeployEngine = class {
7973
8175
  if (progress)
7974
8176
  progress.current++;
7975
8177
  const updatePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
8178
+ renderer.removeTask(logicalId);
7976
8179
  this.logger.info(`${updatePrefix}\u2705 ${logicalId} (${resourceType}) updated`);
7977
8180
  }
7978
8181
  break;
@@ -8018,11 +8221,13 @@ var DeployEngine = class {
8018
8221
  if (progress)
8019
8222
  progress.current++;
8020
8223
  const deletePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
8224
+ renderer.removeTask(logicalId);
8021
8225
  this.logger.info(`${deletePrefix}\u2705 ${logicalId} (${resourceType}) deleted`);
8022
8226
  break;
8023
8227
  }
8024
8228
  }
8025
8229
  } catch (error) {
8230
+ renderer.removeTask(logicalId);
8026
8231
  const message = error instanceof Error ? error.message : String(error);
8027
8232
  this.logger.error(`Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`);
8028
8233
  throw new ProvisioningError(
@@ -8032,6 +8237,8 @@ var DeployEngine = class {
8032
8237
  stateResources[logicalId]?.physicalId,
8033
8238
  error instanceof Error ? error : void 0
8034
8239
  );
8240
+ } finally {
8241
+ renderer.removeTask(logicalId);
8035
8242
  }
8036
8243
  }
8037
8244
  /**
@@ -8192,7 +8399,7 @@ var DeployEngine = class {
8192
8399
  } catch (error) {
8193
8400
  lastError = error;
8194
8401
  const message = error instanceof Error ? error.message : String(error);
8195
- const isRetryable = this.isRetryableError(error, message);
8402
+ const isRetryable = isRetryableTransientError(error, message);
8196
8403
  if (!isRetryable || attempt >= maxRetries) {
8197
8404
  throw error;
8198
8405
  }
@@ -8209,53 +8416,6 @@ var DeployEngine = class {
8209
8416
  }
8210
8417
  throw lastError;
8211
8418
  }
8212
- /**
8213
- * Determine if an error is retryable (transient).
8214
- * Checks HTTP status codes (429 throttle, 503 unavailable)
8215
- * and IAM propagation delay message patterns.
8216
- */
8217
- isRetryableError(error, message) {
8218
- const metadata = error.$metadata;
8219
- const statusCode = metadata?.httpStatusCode;
8220
- if (statusCode === 429 || statusCode === 503)
8221
- return true;
8222
- const cause = error.cause;
8223
- const causeStatus = cause?.$metadata?.httpStatusCode;
8224
- if (causeStatus === 429 || causeStatus === 503)
8225
- return true;
8226
- const retryablePatterns = [
8227
- "cannot be assumed",
8228
- "role defined for the function",
8229
- "not authorized to perform",
8230
- "execution role",
8231
- "trust policy",
8232
- "Role validation failed",
8233
- "does not have required permissions",
8234
- "Trusted Entity",
8235
- "currently in the following state: Pending",
8236
- // DELETE dependency ordering (parallel deletion race conditions)
8237
- "has dependencies and cannot be deleted",
8238
- "can't be deleted since it has",
8239
- "DependencyViolation",
8240
- // AWS eventual consistency (dependency just created but not yet visible)
8241
- // e.g., RDS DBCluster referencing a just-created DBSubnetGroup
8242
- "does not exist",
8243
- // AppSync schema is being created asynchronously
8244
- "Schema is currently being altered",
8245
- // IAM principal not yet propagated to S3 bucket policy
8246
- "Invalid principal in policy",
8247
- // S3 bucket creation/deletion still in progress
8248
- "conflicting conditional operation",
8249
- // Secrets Manager: ForceDeleteWithoutRecovery may take a moment to propagate
8250
- "scheduled for deletion",
8251
- // DynamoDB Streams / Kinesis: IAM role not yet propagated
8252
- "Cannot access stream",
8253
- "Please ensure the role can perform",
8254
- // KMS: IAM role not yet propagated for CreateGrant
8255
- "KMS key is invalid for CreateGrant"
8256
- ];
8257
- return retryablePatterns.some((p) => message.includes(p));
8258
- }
8259
8419
  /**
8260
8420
  * Resolve stack outputs from template and resource attributes
8261
8421
  *