@go-to-k/cdkd 0.219.2 → 0.219.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.
@@ -0,0 +1,1484 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { createHash } from "node:crypto";
3
+ import { ModifyInstanceAttributeCommand } from "@aws-sdk/client-ec2";
4
+
5
+ //#region src/provisioning/resource-name.ts
6
+ /**
7
+ * Per-async-context stack name. Resource-name generation reads this so that
8
+ * concurrent deploys (`cdkd deploy --all` runs stacks in parallel up to
9
+ * `--stack-concurrency`) don't fight over a single shared variable.
10
+ *
11
+ * History: this was `let currentStackName: string | undefined` until
12
+ * 2026-05-01. Two parallel `deploy()` calls would each call
13
+ * `setCurrentStackName(...)` and the second would overwrite the first;
14
+ * any IAM Role / SQS Queue / etc. created by the first stack while the
15
+ * second was active would get the second stack's prefix in its physical
16
+ * name, then the second stack's own create attempt for the same logical
17
+ * id would collide ("Role with name X already exists"). Switching to
18
+ * `AsyncLocalStorage` scopes the value to each deploy's async chain.
19
+ */
20
+ const stackNameStore = new AsyncLocalStorage();
21
+ function withStackName(stackName, fn) {
22
+ return stackNameStore.run(stackName, fn);
23
+ }
24
+ /**
25
+ * Read the current async context's stack name, if any.
26
+ *
27
+ * Returns `undefined` outside any `withStackName` / `setCurrentStackName`
28
+ * scope. Used by the live renderer to scope per-stack in-flight task
29
+ * entries so concurrent deploys don't clobber each other's tasks (same
30
+ * `logicalId` in two stacks would collide on the singleton renderer's
31
+ * task Map without this).
32
+ */
33
+ function getCurrentStackName() {
34
+ return stackNameStore.getStore();
35
+ }
36
+ /**
37
+ * Per-async-context "skip the stack-name prefix on user-supplied physical
38
+ * names" flag. Read by `generateResourceName` when its caller passes
39
+ * `userSupplied: true`; auto-generated-name paths
40
+ * (`generateResourceName(logicalId, ...)`) ignore this flag.
41
+ *
42
+ * Scoped via AsyncLocalStorage so that `--stack-concurrency > 1` runs
43
+ * cannot cross-contaminate — each deploy's body is wrapped in its own
44
+ * `withSkipPrefix(...)` scope (the deploy CLI plumbs the resolved
45
+ * `--no-prefix-user-supplied-names` value through here). Default
46
+ * `false` preserves pre-PR behavior when the flag is not set.
47
+ */
48
+ const skipPrefixStore = new AsyncLocalStorage();
49
+ function withSkipPrefix(skip, fn) {
50
+ return skipPrefixStore.run(skip, fn);
51
+ }
52
+ /**
53
+ * Read the current async context's skip-prefix flag. Defaults to
54
+ * `false` when no `withSkipPrefix` scope is active.
55
+ *
56
+ * Public for unit tests; `generateResourceName` consumes this
57
+ * internally.
58
+ */
59
+ function getCurrentSkipPrefix() {
60
+ return skipPrefixStore.getStore() ?? false;
61
+ }
62
+ /**
63
+ * Resource types whose pre-PR #297 code path ran user-supplied
64
+ * physical names through `generateResourceName` (= got the stack-name
65
+ * prefix). These are the only types affected by
66
+ * `--no-prefix-user-supplied-names`; flipping the flag against an
67
+ * existing stack proposes REPLACEMENT on every state resource of
68
+ * one of these types whose `physicalId` is still prefixed.
69
+ *
70
+ * Pattern A providers (Lambda, S3, SNS, SQS, DynamoDB, Logs LogGroup,
71
+ * Events Rule, etc.) historically short-circuited user-supplied names
72
+ * **out** of `generateResourceName` entirely — those types never got
73
+ * the prefix regardless of the flag, so they are NOT included here.
74
+ *
75
+ * Used by the deploy-time pre-flight migration check in
76
+ * `src/cli/commands/prefix-migration-check.ts`. Keep in sync with the
77
+ * Pattern B provider call sites that consume
78
+ * `generateResourceNameWithFallback(...)`.
79
+ */
80
+ const PATTERN_B_RESOURCE_TYPES = [
81
+ "AWS::IAM::Role",
82
+ "AWS::IAM::User",
83
+ "AWS::IAM::Group",
84
+ "AWS::IAM::InstanceProfile",
85
+ "AWS::IAM::ManagedPolicy",
86
+ "AWS::ElasticLoadBalancingV2::LoadBalancer",
87
+ "AWS::ElasticLoadBalancingV2::TargetGroup"
88
+ ];
89
+ /**
90
+ * For each Pattern B resource type, the CFn template `Properties` field
91
+ * the user sets to supply a physical name (`new iam.Role(this, 'X',
92
+ * { roleName: 'my-role' })` → `Properties.RoleName: 'my-role'`).
93
+ *
94
+ * Used by the prefix-migration check to distinguish user-supplied
95
+ * physical names (which the v0.94.0 default flip would actually
96
+ * REPLACE on next deploy) from auto-generated logical-id-fallback
97
+ * names (which keep the prefix in BOTH old and new default — no
98
+ * REPLACE pending). Without this discriminator, the migration
99
+ * check naively flags every prefix-style physicalId regardless of
100
+ * its origin, surfacing a false-positive WARNING on auto-generated
101
+ * names that won't actually be touched.
102
+ *
103
+ * Keep in sync with `PATTERN_B_RESOURCE_TYPES` and with each
104
+ * provider's `Properties[<NameField>]` lookup in
105
+ * `src/provisioning/providers/`.
106
+ */
107
+ const PATTERN_B_NAME_PROPERTIES = {
108
+ "AWS::IAM::Role": "RoleName",
109
+ "AWS::IAM::User": "UserName",
110
+ "AWS::IAM::Group": "GroupName",
111
+ "AWS::IAM::InstanceProfile": "InstanceProfileName",
112
+ "AWS::IAM::ManagedPolicy": "ManagedPolicyName",
113
+ "AWS::ElasticLoadBalancingV2::LoadBalancer": "Name",
114
+ "AWS::ElasticLoadBalancingV2::TargetGroup": "Name"
115
+ };
116
+ /**
117
+ * Generate a unique resource name from the logical ID.
118
+ *
119
+ * Generates names in CloudFormation-compatible format:
120
+ * `{StackName}-{LogicalId}-{Hash}` (truncated to maxLength).
121
+ *
122
+ * @param name The raw name (from properties or logicalId fallback)
123
+ * @param options Length and character constraints
124
+ * @returns A sanitized, truncated name that fits the constraints
125
+ */
126
+ function generateResourceName(name, options) {
127
+ const { maxLength, lowercase = false, allowedPattern = /[^a-zA-Z0-9-]/g, userSupplied = false } = options;
128
+ const currentStackName = stackNameStore.getStore();
129
+ const fullName = currentStackName && !(userSupplied && getCurrentSkipPrefix()) ? `${currentStackName}-${name}` : name;
130
+ let sanitized = lowercase ? fullName.toLowerCase() : fullName;
131
+ sanitized = sanitized.replace(allowedPattern, "-");
132
+ sanitized = sanitized.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
133
+ if (sanitized.length <= maxLength) return sanitized;
134
+ const hash = createHash("sha256").update(fullName).digest("hex").substring(0, 8);
135
+ const maxPrefixLength = maxLength - hash.length - 1;
136
+ return `${sanitized.substring(0, maxPrefixLength).replace(/-+$/, "")}-${hash}`;
137
+ }
138
+ /**
139
+ * Generate a resource name from a user-declared physical name OR
140
+ * fall back to the logical id.
141
+ *
142
+ * Wraps {@link generateResourceName} to express the Pattern B call-site
143
+ * shape (`generateResourceName((properties['Name'] as string | undefined)
144
+ * || logicalId, opts)`) as a single typed helper. The user-supplied
145
+ * branch passes `userSupplied: true`, which makes the per-deploy
146
+ * `withSkipPrefix(true)` flag drop the stack-name prefix on that name.
147
+ * The fallback (logical-id) branch is `userSupplied: false` and keeps
148
+ * the prefix regardless of the flag — auto-generated names rely on
149
+ * the prefix for cross-stack uniqueness.
150
+ *
151
+ * Use at every Pattern B provider call site (currently IAM Role, IAM
152
+ * User, IAM Group, IAM InstanceProfile, ELBv2 LoadBalancer, ELBv2
153
+ * TargetGroup) so the `--no-prefix-user-supplied-names` flag controls
154
+ * those types consistently. Pattern A providers (Lambda, S3, SNS,
155
+ * SQS, DynamoDB, etc.) do NOT need this helper — they already
156
+ * short-circuit the user-supplied name out of the
157
+ * `generateResourceName` call entirely, so the prefix is never
158
+ * applied to user-supplied names regardless of the flag.
159
+ */
160
+ function generateResourceNameWithFallback(userSuppliedName, logicalId, options) {
161
+ if (userSuppliedName !== void 0 && userSuppliedName !== "") return generateResourceName(userSuppliedName, {
162
+ ...options,
163
+ userSupplied: true
164
+ });
165
+ return generateResourceName(logicalId, {
166
+ ...options,
167
+ userSupplied: false
168
+ });
169
+ }
170
+ /**
171
+ * Default name generation rules for CC API fallback.
172
+ *
173
+ * When an SDK provider falls back to CC API, the resource may need a
174
+ * default name that the SDK provider would have generated. This map
175
+ * defines the name property and generation options for each resource type.
176
+ *
177
+ * Format: resourceType → { nameProperty, options, postProcess? }
178
+ */
179
+ const FALLBACK_NAME_RULES = {
180
+ "AWS::S3::Bucket": {
181
+ nameProperty: "BucketName",
182
+ options: {
183
+ maxLength: 63,
184
+ lowercase: true
185
+ }
186
+ },
187
+ "AWS::SQS::Queue": {
188
+ nameProperty: "QueueName",
189
+ options: { maxLength: 80 }
190
+ },
191
+ "AWS::SNS::Topic": {
192
+ nameProperty: "TopicName",
193
+ options: { maxLength: 256 }
194
+ },
195
+ "AWS::Lambda::Function": {
196
+ nameProperty: "FunctionName",
197
+ options: { maxLength: 64 }
198
+ },
199
+ "AWS::Lambda::LayerVersion": {
200
+ nameProperty: "LayerName",
201
+ options: { maxLength: 64 }
202
+ },
203
+ "AWS::IAM::Role": {
204
+ nameProperty: "RoleName",
205
+ options: { maxLength: 64 }
206
+ },
207
+ "AWS::IAM::Policy": {
208
+ nameProperty: "PolicyName",
209
+ options: { maxLength: 64 }
210
+ },
211
+ "AWS::IAM::ManagedPolicy": {
212
+ nameProperty: "ManagedPolicyName",
213
+ options: { maxLength: 128 }
214
+ },
215
+ "AWS::IAM::User": {
216
+ nameProperty: "UserName",
217
+ options: { maxLength: 64 }
218
+ },
219
+ "AWS::IAM::Group": {
220
+ nameProperty: "GroupName",
221
+ options: { maxLength: 128 }
222
+ },
223
+ "AWS::IAM::InstanceProfile": {
224
+ nameProperty: "InstanceProfileName",
225
+ options: { maxLength: 128 }
226
+ },
227
+ "AWS::DynamoDB::Table": {
228
+ nameProperty: "TableName",
229
+ options: { maxLength: 255 }
230
+ },
231
+ "AWS::ECR::Repository": {
232
+ nameProperty: "RepositoryName",
233
+ options: {
234
+ maxLength: 256,
235
+ lowercase: true
236
+ }
237
+ },
238
+ "AWS::ECS::Cluster": {
239
+ nameProperty: "ClusterName",
240
+ options: { maxLength: 255 }
241
+ },
242
+ "AWS::ECS::Service": {
243
+ nameProperty: "ServiceName",
244
+ options: { maxLength: 255 }
245
+ },
246
+ "AWS::Logs::LogGroup": {
247
+ nameProperty: "LogGroupName",
248
+ options: { maxLength: 512 }
249
+ },
250
+ "AWS::CloudWatch::Alarm": {
251
+ nameProperty: "AlarmName",
252
+ options: { maxLength: 256 }
253
+ },
254
+ "AWS::Events::Rule": {
255
+ nameProperty: "Name",
256
+ options: { maxLength: 64 }
257
+ },
258
+ "AWS::Events::EventBus": {
259
+ nameProperty: "Name",
260
+ options: { maxLength: 256 }
261
+ },
262
+ "AWS::Kinesis::Stream": {
263
+ nameProperty: "Name",
264
+ options: { maxLength: 128 }
265
+ },
266
+ "AWS::StepFunctions::StateMachine": {
267
+ nameProperty: "StateMachineName",
268
+ options: { maxLength: 80 }
269
+ },
270
+ "AWS::SecretsManager::Secret": {
271
+ nameProperty: "Name",
272
+ options: {
273
+ maxLength: 512,
274
+ allowedPattern: /[^a-zA-Z0-9-/_]/g
275
+ }
276
+ },
277
+ "AWS::SSM::Parameter": {
278
+ nameProperty: "Name",
279
+ options: { maxLength: 2048 }
280
+ },
281
+ "AWS::Cognito::UserPool": {
282
+ nameProperty: "UserPoolName",
283
+ options: { maxLength: 128 }
284
+ },
285
+ "AWS::ElastiCache::SubnetGroup": {
286
+ nameProperty: "CacheSubnetGroupName",
287
+ options: {
288
+ maxLength: 255,
289
+ lowercase: true
290
+ }
291
+ },
292
+ "AWS::ElastiCache::CacheCluster": {
293
+ nameProperty: "ClusterName",
294
+ options: {
295
+ maxLength: 40,
296
+ lowercase: true
297
+ }
298
+ },
299
+ "AWS::RDS::DBSubnetGroup": {
300
+ nameProperty: "DBSubnetGroupName",
301
+ options: {
302
+ maxLength: 255,
303
+ lowercase: true
304
+ }
305
+ },
306
+ "AWS::RDS::DBCluster": {
307
+ nameProperty: "DBClusterIdentifier",
308
+ options: {
309
+ maxLength: 63,
310
+ lowercase: true
311
+ }
312
+ },
313
+ "AWS::RDS::DBInstance": {
314
+ nameProperty: "DBInstanceIdentifier",
315
+ options: {
316
+ maxLength: 63,
317
+ lowercase: true
318
+ }
319
+ },
320
+ "AWS::DocDB::DBSubnetGroup": {
321
+ nameProperty: "DBSubnetGroupName",
322
+ options: {
323
+ maxLength: 255,
324
+ lowercase: true
325
+ }
326
+ },
327
+ "AWS::DocDB::DBCluster": {
328
+ nameProperty: "DBClusterIdentifier",
329
+ options: {
330
+ maxLength: 63,
331
+ lowercase: true
332
+ }
333
+ },
334
+ "AWS::DocDB::DBInstance": {
335
+ nameProperty: "DBInstanceIdentifier",
336
+ options: {
337
+ maxLength: 63,
338
+ lowercase: true
339
+ }
340
+ },
341
+ "AWS::Neptune::DBSubnetGroup": {
342
+ nameProperty: "DBSubnetGroupName",
343
+ options: {
344
+ maxLength: 255,
345
+ lowercase: true
346
+ }
347
+ },
348
+ "AWS::Neptune::DBCluster": {
349
+ nameProperty: "DBClusterIdentifier",
350
+ options: {
351
+ maxLength: 63,
352
+ lowercase: true
353
+ }
354
+ },
355
+ "AWS::Neptune::DBInstance": {
356
+ nameProperty: "DBInstanceIdentifier",
357
+ options: {
358
+ maxLength: 63,
359
+ lowercase: true
360
+ }
361
+ },
362
+ "AWS::ElasticLoadBalancingV2::LoadBalancer": {
363
+ nameProperty: "Name",
364
+ options: { maxLength: 32 }
365
+ },
366
+ "AWS::ElasticLoadBalancingV2::TargetGroup": {
367
+ nameProperty: "Name",
368
+ options: { maxLength: 32 }
369
+ },
370
+ "AWS::WAFv2::WebACL": {
371
+ nameProperty: "Name",
372
+ options: { maxLength: 128 }
373
+ },
374
+ "AWS::CodeBuild::Project": {
375
+ nameProperty: "Name",
376
+ options: { maxLength: 255 }
377
+ },
378
+ "AWS::S3Express::DirectoryBucket": {
379
+ nameProperty: "BucketName",
380
+ options: {
381
+ maxLength: 63,
382
+ lowercase: true
383
+ }
384
+ }
385
+ };
386
+ /**
387
+ * Apply default name generation for CC API fallback.
388
+ *
389
+ * When a resource doesn't have an explicit name property set,
390
+ * generates the same default name that the SDK provider would have created.
391
+ * This ensures consistent naming regardless of whether SDK or CC API handles the resource.
392
+ *
393
+ * @param logicalId Logical ID from the template
394
+ * @param resourceType CloudFormation resource type
395
+ * @param properties Resource properties (will not be mutated)
396
+ * @returns Properties with default name applied if needed, or original properties if no rule exists
397
+ */
398
+ function applyDefaultNameForFallback(logicalId, resourceType, properties) {
399
+ const rule = FALLBACK_NAME_RULES[resourceType];
400
+ if (!rule) return properties;
401
+ if (properties[rule.nameProperty]) return properties;
402
+ const generatedName = generateResourceName(logicalId, rule.options);
403
+ return {
404
+ ...properties,
405
+ [rule.nameProperty]: generatedName
406
+ };
407
+ }
408
+
409
+ //#endregion
410
+ //#region src/utils/live-renderer.ts
411
+ /**
412
+ * Live multi-line progress renderer for the bottom of the terminal.
413
+ *
414
+ * Maintains a "live area" listing in-flight tasks (Creating MyBucket...),
415
+ * redrawn on a spinner timer. Other log output is routed through
416
+ * {@link LiveRenderer.printAbove} so it appears above the live area without
417
+ * disturbing the currently-displayed in-flight tasks.
418
+ *
419
+ * Design notes:
420
+ * - Multiple resources can be in flight concurrently (cdkd uses parallel DAG
421
+ * dispatch), so a single in-place line overwrite is not enough — each
422
+ * in-flight resource is its own line in the live area.
423
+ * - On non-TTY (CI/log-collection), the renderer stays inactive and
424
+ * {@link LiveRenderer.printAbove} falls through to a direct write, so output
425
+ * matches the previous append-only behavior.
426
+ * - In verbose mode (debug level) the caller should not start the renderer:
427
+ * debug logs would interleave too aggressively with the live area.
428
+ */
429
+ const SPINNER_FRAMES = [
430
+ "⠋",
431
+ "⠙",
432
+ "⠹",
433
+ "⠸",
434
+ "⠼",
435
+ "⠴",
436
+ "⠦",
437
+ "⠧",
438
+ "⠇",
439
+ "⠏"
440
+ ];
441
+ const FRAME_INTERVAL_MS = 80;
442
+ const ESC = "\x1B[";
443
+ /**
444
+ * Scope a task `id` to its calling stack so two stacks running in
445
+ * parallel — `cdkd deploy --all` with `--stack-concurrency > 1` — don't
446
+ * collide on the same `logicalId` in the renderer's task Map. Without
447
+ * this, stack B's `addTask('MyQueue', ...)` would overwrite stack A's
448
+ * entry, and stack A's later `removeTask('MyQueue')` would erase
449
+ * stack B's.
450
+ */
451
+ function scopedKey(id, stackName) {
452
+ return stackName ? `${stackName}:${id}` : id;
453
+ }
454
+ var LiveRenderer = class {
455
+ tasks = /* @__PURE__ */ new Map();
456
+ active = false;
457
+ spinnerIndex = 0;
458
+ interval = null;
459
+ linesDrawn = 0;
460
+ cursorHidden = false;
461
+ exitListener = null;
462
+ stream;
463
+ constructor(stream = process.stdout) {
464
+ this.stream = stream;
465
+ }
466
+ isActive() {
467
+ return this.active;
468
+ }
469
+ /**
470
+ * Enable the live renderer. No-op if stdout is not a TTY or if
471
+ * `CDKD_NO_LIVE=1`. Returns true if successfully enabled.
472
+ */
473
+ start() {
474
+ if (this.active) return true;
475
+ if (!this.stream.isTTY) return false;
476
+ if (process.env["CDKD_NO_LIVE"] === "1") return false;
477
+ this.active = true;
478
+ this.hideCursor();
479
+ if (!this.exitListener) {
480
+ this.exitListener = () => this.showCursor();
481
+ process.on("exit", this.exitListener);
482
+ }
483
+ this.interval = setInterval(() => this.draw(), FRAME_INTERVAL_MS);
484
+ if (typeof this.interval.unref === "function") this.interval.unref();
485
+ return true;
486
+ }
487
+ stop() {
488
+ if (!this.active) return;
489
+ if (this.interval) {
490
+ clearInterval(this.interval);
491
+ this.interval = null;
492
+ }
493
+ this.clear();
494
+ this.showCursor();
495
+ if (this.exitListener) {
496
+ process.removeListener("exit", this.exitListener);
497
+ this.exitListener = null;
498
+ }
499
+ this.tasks.clear();
500
+ this.active = false;
501
+ }
502
+ addTask(id, label) {
503
+ const stackName = getCurrentStackName();
504
+ this.tasks.set(scopedKey(id, stackName), {
505
+ label,
506
+ startedAt: Date.now(),
507
+ stackName
508
+ });
509
+ if (this.active) this.draw();
510
+ }
511
+ removeTask(id) {
512
+ const stackName = getCurrentStackName();
513
+ if (!this.tasks.delete(scopedKey(id, stackName))) return;
514
+ if (this.active) this.draw();
515
+ }
516
+ /**
517
+ * Replace the label of a previously-added task in place. No-op if the
518
+ * task is not currently tracked (e.g. it already finished). Used by the
519
+ * per-resource deadline wrapper to surface a "[taking longer than
520
+ * expected, Nm+]" suffix without disturbing the elapsed-time counter
521
+ * the renderer tracks via `startedAt`.
522
+ */
523
+ updateTaskLabel(id, label) {
524
+ const stackName = getCurrentStackName();
525
+ const task = this.tasks.get(scopedKey(id, stackName));
526
+ if (!task) return;
527
+ task.label = label;
528
+ if (this.active) this.draw();
529
+ }
530
+ /**
531
+ * Print content above the live area. Clears the live area, runs the writer,
532
+ * then redraws the live area. When the renderer is inactive, the writer
533
+ * runs directly so callers can use this unconditionally.
534
+ */
535
+ printAbove(write) {
536
+ if (!this.active) {
537
+ write();
538
+ return;
539
+ }
540
+ this.clear();
541
+ write();
542
+ this.draw();
543
+ }
544
+ clear() {
545
+ if (this.linesDrawn === 0) return;
546
+ this.stream.write("\r");
547
+ for (let i = 0; i < this.linesDrawn; i++) this.stream.write(`${ESC}1A${ESC}2K`);
548
+ this.linesDrawn = 0;
549
+ }
550
+ draw() {
551
+ if (!this.active) return;
552
+ this.clear();
553
+ if (this.tasks.size === 0) return;
554
+ const frame = SPINNER_FRAMES[this.spinnerIndex % SPINNER_FRAMES.length];
555
+ this.spinnerIndex++;
556
+ const distinctStacks = /* @__PURE__ */ new Set();
557
+ for (const task of this.tasks.values()) distinctStacks.add(task.stackName);
558
+ const showStackPrefix = distinctStacks.size > 1;
559
+ const cols = this.stream.columns ?? 80;
560
+ const lines = [];
561
+ for (const task of this.tasks.values()) {
562
+ const elapsed = ((Date.now() - task.startedAt) / 1e3).toFixed(1);
563
+ const raw = ` ${frame} ${showStackPrefix && task.stackName ? `[${task.stackName}] ` : ""}${task.label} (${elapsed}s)`;
564
+ lines.push(this.truncate(raw, cols));
565
+ }
566
+ this.stream.write(lines.join("\n") + "\n");
567
+ this.linesDrawn = lines.length;
568
+ }
569
+ truncate(s, maxLen) {
570
+ if (s.length <= maxLen) return s;
571
+ if (maxLen <= 1) return "…";
572
+ return s.substring(0, maxLen - 1) + "…";
573
+ }
574
+ hideCursor() {
575
+ if (this.cursorHidden) return;
576
+ this.stream.write(`${ESC}?25l`);
577
+ this.cursorHidden = true;
578
+ }
579
+ showCursor() {
580
+ if (!this.cursorHidden) return;
581
+ this.stream.write(`${ESC}?25h`);
582
+ this.cursorHidden = false;
583
+ }
584
+ };
585
+ let globalRenderer = null;
586
+ function getLiveRenderer() {
587
+ if (!globalRenderer) globalRenderer = new LiveRenderer();
588
+ return globalRenderer;
589
+ }
590
+
591
+ //#endregion
592
+ //#region src/utils/stack-context.ts
593
+ const outputBufferStore = new AsyncLocalStorage();
594
+ /**
595
+ * Run `fn` with a fresh log buffer scoped to its async chain. Any
596
+ * `logger.info / debug / warn / error` calls inside `fn` (and any
597
+ * `await`s) push into the buffer instead of writing to stdout/stderr.
598
+ * Returns the buffered lines (and either `result` or `error`) so the
599
+ * caller can flush them in one block.
600
+ */
601
+ async function runStackBuffered(fn) {
602
+ const buffer = { lines: [] };
603
+ return outputBufferStore.run(buffer, async () => {
604
+ try {
605
+ return {
606
+ ok: true,
607
+ result: await fn(),
608
+ lines: buffer.lines
609
+ };
610
+ } catch (error) {
611
+ return {
612
+ ok: false,
613
+ error,
614
+ lines: buffer.lines
615
+ };
616
+ }
617
+ });
618
+ }
619
+ /**
620
+ * Get the current async context's stack output buffer, or `undefined`
621
+ * if no `runStackBuffered` is active. The logger consults this on every
622
+ * call: present → push to buffer; absent → fall through to live
623
+ * renderer / console.
624
+ */
625
+ function getCurrentStackOutputBuffer() {
626
+ return outputBufferStore.getStore();
627
+ }
628
+
629
+ //#endregion
630
+ //#region src/utils/logger.ts
631
+ /**
632
+ * ANSI color codes
633
+ *
634
+ * Kept internal — `ConsoleLogger.formatMessage` references these for the
635
+ * verbose/compact mode level prefixes. For inline color wrapping in
636
+ * production code, import from `./colors.js` instead (which lives in a
637
+ * separate module so unit tests that mock `logger.ts` don't strip color
638
+ * helpers as a side effect).
639
+ */
640
+ const colors = {
641
+ reset: "\x1B[0m",
642
+ bright: "\x1B[1m",
643
+ dim: "\x1B[2m",
644
+ red: "\x1B[31m",
645
+ green: "\x1B[32m",
646
+ yellow: "\x1B[33m",
647
+ blue: "\x1B[34m",
648
+ cyan: "\x1B[36m",
649
+ gray: "\x1B[90m"
650
+ };
651
+ /**
652
+ * Format timestamp
653
+ */
654
+ function formatTimestamp() {
655
+ return (/* @__PURE__ */ new Date()).toISOString();
656
+ }
657
+ /**
658
+ * Console logger implementation
659
+ *
660
+ * Supports two output modes:
661
+ * - verbose (debug level): timestamps, module prefixes, all details
662
+ * - compact (info level): clean output without timestamps or prefixes
663
+ */
664
+ var ConsoleLogger = class {
665
+ level;
666
+ useColors;
667
+ constructor(level = "info", useColors = true) {
668
+ this.level = level;
669
+ this.useColors = useColors;
670
+ }
671
+ shouldLog(level) {
672
+ const levels = [
673
+ "debug",
674
+ "info",
675
+ "warn",
676
+ "error"
677
+ ];
678
+ const currentLevelIndex = levels.indexOf(this.level);
679
+ return levels.indexOf(level) >= currentLevelIndex;
680
+ }
681
+ formatMessage(level, message, ...args) {
682
+ const formattedArgs = args.length > 0 ? " " + args.map((a) => JSON.stringify(a)).join(" ") : "";
683
+ if (this.level === "debug") {
684
+ const timestamp = formatTimestamp();
685
+ const levelStr = level.toUpperCase().padEnd(5);
686
+ if (this.useColors) {
687
+ const levelColor = {
688
+ debug: colors.gray,
689
+ info: colors.blue,
690
+ warn: colors.yellow,
691
+ error: colors.red
692
+ }[level];
693
+ return `${colors.dim}${timestamp}${colors.reset} ${levelColor}${levelStr}${colors.reset} ${message}${formattedArgs}`;
694
+ }
695
+ return `${timestamp} ${levelStr} ${message}${formattedArgs}`;
696
+ }
697
+ if (this.useColors) {
698
+ if (level === "error") return `${colors.red}${message}${formattedArgs}${colors.reset}`;
699
+ if (level === "warn") return `${colors.yellow}${message}${formattedArgs}${colors.reset}`;
700
+ return `${message}${formattedArgs}`;
701
+ }
702
+ return `${message}${formattedArgs}`;
703
+ }
704
+ /**
705
+ * Route a formatted log line. When a per-stack output buffer is active in
706
+ * the current async context (parallel multi-stack deploy), capture the
707
+ * line into the buffer so it can be flushed as one atomic block when the
708
+ * stack finishes. Otherwise fall through to the live renderer / console
709
+ * as before.
710
+ */
711
+ emit(level, formatted) {
712
+ const buffer = getCurrentStackOutputBuffer();
713
+ if (buffer) {
714
+ buffer.lines.push(formatted);
715
+ return;
716
+ }
717
+ getLiveRenderer().printAbove(() => {
718
+ if (level === "error") console.error(formatted);
719
+ else if (level === "warn") console.warn(formatted);
720
+ else if (level === "info") console.info(formatted);
721
+ else console.debug(formatted);
722
+ });
723
+ }
724
+ debug(message, ...args) {
725
+ if (this.shouldLog("debug")) this.emit("debug", this.formatMessage("debug", message, ...args));
726
+ }
727
+ info(message, ...args) {
728
+ if (this.shouldLog("info")) this.emit("info", this.formatMessage("info", message, ...args));
729
+ }
730
+ warn(message, ...args) {
731
+ if (this.shouldLog("warn")) this.emit("warn", this.formatMessage("warn", message, ...args));
732
+ }
733
+ error(message, ...args) {
734
+ if (this.shouldLog("error")) this.emit("error", this.formatMessage("error", message, ...args));
735
+ }
736
+ /**
737
+ * Set log level
738
+ */
739
+ setLevel(level) {
740
+ this.level = level;
741
+ }
742
+ getLevel() {
743
+ return this.level;
744
+ }
745
+ /**
746
+ * Create a child logger with a prefix
747
+ *
748
+ * In verbose mode, prefix is shown as [Prefix]. In compact mode, prefix is hidden.
749
+ */
750
+ child(prefix) {
751
+ return new ChildLogger(prefix, this.useColors);
752
+ }
753
+ };
754
+ /**
755
+ * Child logger that always syncs level from global logger
756
+ */
757
+ var ChildLogger = class extends ConsoleLogger {
758
+ prefix;
759
+ constructor(prefix, useColors) {
760
+ super("info", useColors);
761
+ this.prefix = prefix;
762
+ }
763
+ syncLevel() {
764
+ if (globalLogger) this.setLevel(globalLogger.getLevel());
765
+ }
766
+ debug(message, ...args) {
767
+ this.syncLevel();
768
+ super.debug(`[${this.prefix}] ${message}`, ...args);
769
+ }
770
+ info(message, ...args) {
771
+ this.syncLevel();
772
+ const msg = this.getLevel() === "debug" ? `[${this.prefix}] ${message}` : message;
773
+ super.info(msg, ...args);
774
+ }
775
+ warn(message, ...args) {
776
+ this.syncLevel();
777
+ const msg = this.getLevel() === "debug" ? `[${this.prefix}] ${message}` : message;
778
+ super.warn(msg, ...args);
779
+ }
780
+ error(message, ...args) {
781
+ this.syncLevel();
782
+ const msg = this.getLevel() === "debug" ? `[${this.prefix}] ${message}` : message;
783
+ super.error(msg, ...args);
784
+ }
785
+ };
786
+ /**
787
+ * Global logger instance
788
+ */
789
+ let globalLogger = null;
790
+ /**
791
+ * Get or create global logger
792
+ */
793
+ function getLogger() {
794
+ if (!globalLogger) globalLogger = new ConsoleLogger();
795
+ return globalLogger;
796
+ }
797
+ /**
798
+ * Set global logger instance
799
+ */
800
+ function setLogger(logger) {
801
+ globalLogger = logger;
802
+ }
803
+
804
+ //#endregion
805
+ //#region src/utils/error-handler.ts
806
+ /**
807
+ * Base error class for cdkd
808
+ */
809
+ var CdkdError = class CdkdError extends Error {
810
+ code;
811
+ cause;
812
+ constructor(message, code, cause) {
813
+ super(message);
814
+ this.code = code;
815
+ this.cause = cause;
816
+ this.name = "CdkdError";
817
+ Object.setPrototypeOf(this, CdkdError.prototype);
818
+ }
819
+ };
820
+ /**
821
+ * State management errors
822
+ */
823
+ var StateError = class StateError extends CdkdError {
824
+ constructor(message, cause) {
825
+ super(message, "STATE_ERROR", cause);
826
+ this.name = "StateError";
827
+ Object.setPrototypeOf(this, StateError.prototype);
828
+ }
829
+ };
830
+ /**
831
+ * Lock acquisition errors
832
+ */
833
+ var LockError = class LockError extends CdkdError {
834
+ constructor(message, cause) {
835
+ super(message, "LOCK_ERROR", cause);
836
+ this.name = "LockError";
837
+ Object.setPrototypeOf(this, LockError.prototype);
838
+ }
839
+ };
840
+ /**
841
+ * Synthesis errors
842
+ */
843
+ var SynthesisError = class SynthesisError extends CdkdError {
844
+ constructor(message, cause) {
845
+ super(message, "SYNTHESIS_ERROR", cause);
846
+ this.name = "SynthesisError";
847
+ Object.setPrototypeOf(this, SynthesisError.prototype);
848
+ }
849
+ };
850
+ /**
851
+ * Asset errors
852
+ */
853
+ var AssetError = class AssetError extends CdkdError {
854
+ constructor(message, cause) {
855
+ super(message, "ASSET_ERROR", cause);
856
+ this.name = "AssetError";
857
+ Object.setPrototypeOf(this, AssetError.prototype);
858
+ }
859
+ };
860
+ /**
861
+ * Local-invoke `docker build` failures.
862
+ *
863
+ * Surfaces the stderr captured from `docker build` so the user can
864
+ * re-run the same command directly to debug Dockerfile syntax errors
865
+ * or missing build context. Used by `src/local/docker-image-builder.ts`
866
+ * (PR 5) for container Lambdas; the parallel `AssetError` covers the
867
+ * `cdkd publish-assets` / `cdkd deploy` build path. Kept distinct from
868
+ * `AssetError` so `cdkd local invoke` failures don't show up under the
869
+ * "asset" error class.
870
+ */
871
+ var LocalInvokeBuildError = class LocalInvokeBuildError extends CdkdError {
872
+ constructor(message, cause) {
873
+ super(message, "LOCAL_INVOKE_BUILD_ERROR", cause);
874
+ this.name = "LocalInvokeBuildError";
875
+ Object.setPrototypeOf(this, LocalInvokeBuildError.prototype);
876
+ }
877
+ };
878
+ /**
879
+ * Resource provisioning errors
880
+ */
881
+ var ProvisioningError = class ProvisioningError extends CdkdError {
882
+ resourceType;
883
+ logicalId;
884
+ physicalId;
885
+ constructor(message, resourceType, logicalId, physicalId, cause) {
886
+ super(message, "PROVISIONING_ERROR", cause);
887
+ this.resourceType = resourceType;
888
+ this.logicalId = logicalId;
889
+ this.physicalId = physicalId;
890
+ this.name = "ProvisioningError";
891
+ Object.setPrototypeOf(this, ProvisioningError.prototype);
892
+ }
893
+ };
894
+ /**
895
+ * Resource provisioning timeout errors (per-resource wall-clock deadline).
896
+ *
897
+ * Thrown by `withResourceDeadline` when a single CREATE / UPDATE / DELETE
898
+ * operation exceeds the user-configured `--resource-timeout`. The deploy
899
+ * engine catches this, wraps it in {@link ProvisioningError}, and lets the
900
+ * existing failure path (interrupt siblings → pre-rollback save → rollback
901
+ * unless `--no-rollback`) take over.
902
+ *
903
+ * The message intentionally names the resource, type, region, elapsed time
904
+ * and operation, plus how to override the default. Long-running providers
905
+ * (e.g. Custom Resource: 1h polling cap) self-report their needed budget
906
+ * via `getMinResourceTimeoutMs()`, so the user only needs a per-type
907
+ * override (`--resource-timeout TYPE=DURATION`) when they want to bump a
908
+ * specific non-self-reporting type or shorten a self-reported one.
909
+ */
910
+ var ResourceTimeoutError = class ResourceTimeoutError extends CdkdError {
911
+ logicalId;
912
+ resourceType;
913
+ region;
914
+ elapsedMs;
915
+ operation;
916
+ timeoutMs;
917
+ constructor(logicalId, resourceType, region, elapsedMs, operation, timeoutMs) {
918
+ const elapsedLabel = formatDuration(elapsedMs);
919
+ const timeoutLabel = formatDuration(timeoutMs);
920
+ super(`Resource ${logicalId} (${resourceType}) in ${region} timed out after ${timeoutLabel} during ${operation} (elapsed ${elapsedLabel}).\nThis may indicate a stuck Cloud Control polling loop, hung Custom Resource, or
921
+ slow ENI provisioning. Re-run with --resource-timeout ${resourceType}=<DURATION>\nto bump the budget for this resource type only, or --verbose to see the
922
+ underlying provider activity.`, "RESOURCE_TIMEOUT");
923
+ this.logicalId = logicalId;
924
+ this.resourceType = resourceType;
925
+ this.region = region;
926
+ this.elapsedMs = elapsedMs;
927
+ this.operation = operation;
928
+ this.timeoutMs = timeoutMs;
929
+ this.name = "ResourceTimeoutError";
930
+ Object.setPrototypeOf(this, ResourceTimeoutError.prototype);
931
+ }
932
+ };
933
+ /**
934
+ * Format a duration in milliseconds as a short human-readable label
935
+ * (`30m`, `1h30m`, `45s`). Used by {@link ResourceTimeoutError} so the
936
+ * error message stays compact.
937
+ */
938
+ function formatDuration(ms) {
939
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
940
+ const totalMinutes = Math.round(ms / 6e4);
941
+ if (totalMinutes < 60) return `${totalMinutes}m`;
942
+ const hours = Math.floor(totalMinutes / 60);
943
+ const minutes = totalMinutes % 60;
944
+ return minutes === 0 ? `${hours}h` : `${hours}h${minutes}m`;
945
+ }
946
+ /**
947
+ * Dependency resolution errors
948
+ */
949
+ var DependencyError = class DependencyError extends CdkdError {
950
+ constructor(message, cause) {
951
+ super(message, "DEPENDENCY_ERROR", cause);
952
+ this.name = "DependencyError";
953
+ Object.setPrototypeOf(this, DependencyError.prototype);
954
+ }
955
+ };
956
+ /**
957
+ * Configuration errors
958
+ */
959
+ var ConfigError = class ConfigError extends CdkdError {
960
+ constructor(message, cause) {
961
+ super(message, "CONFIG_ERROR", cause);
962
+ this.name = "ConfigError";
963
+ Object.setPrototypeOf(this, ConfigError.prototype);
964
+ }
965
+ };
966
+ /**
967
+ * Signals a partial-failure outcome that should map to exit code 2 (not 1).
968
+ *
969
+ * Used by `cdkd destroy` and `cdkd state destroy` when one or more
970
+ * per-resource deletes failed but the overall command finished its work
971
+ * (state.json is preserved, the rest of the stack was deleted, and the
972
+ * user can re-run to clean up the remaining resources).
973
+ *
974
+ * Exit code conventions:
975
+ * - 0: command completed successfully, no resources left in error state.
976
+ * - 1: command-level failure (auth error, bad arguments, synth crash,
977
+ * unhandled exception). Default for any thrown error.
978
+ * - 2: partial failure — work completed but some resources are still in
979
+ * an error state. Re-running typically resolves it. Documented in
980
+ * README's "Exit codes" section.
981
+ *
982
+ * `handleError` recognizes this class via `instanceof` and uses its
983
+ * `exitCode` instead of the default 1.
984
+ */
985
+ var PartialFailureError = class PartialFailureError extends CdkdError {
986
+ exitCode = 2;
987
+ constructor(message, cause) {
988
+ super(message, "PARTIAL_FAILURE", cause);
989
+ this.name = "PartialFailureError";
990
+ Object.setPrototypeOf(this, PartialFailureError.prototype);
991
+ }
992
+ };
993
+ /**
994
+ * Signals that a provider cannot perform an in-place `update` for a
995
+ * resource type — most commonly because the AWS resource is structurally
996
+ * immutable (`AWS::Lambda::LayerVersion`, `AWS::S3Tables::TableBucket` once
997
+ * created, certain `AWS::EC2::*` sub-resources) or because the provider
998
+ * surfaces a sub-resource attachment whose only mutation pattern is
999
+ * delete + add (Lambda permission statements, IAM policy attachments).
1000
+ *
1001
+ * Surfaced through `cdkd drift --revert`, which calls
1002
+ * `provider.update(logicalId, physicalId, type, stateProps, awsProps)` to
1003
+ * push cdkd state values back into AWS for every drifted resource. When a
1004
+ * provider throws this error, the drift command collects it as a
1005
+ * per-resource outcome distinct from a generic AWS update failure: the
1006
+ * fix is to re-deploy with `--replace` (or recreate the resource), not to
1007
+ * retry the update.
1008
+ *
1009
+ * Carries the same `exitCode = 2` as {@link PartialFailureError} so a
1010
+ * drift run that hits one immutable resource is reported as partial-
1011
+ * success rather than fatal — the rest of the drifted resources still
1012
+ * had their `update` invoked, and the user has a clear next step printed
1013
+ * for the unsupported one.
1014
+ */
1015
+ var ResourceUpdateNotSupportedError = class ResourceUpdateNotSupportedError extends CdkdError {
1016
+ exitCode = 2;
1017
+ resourceType;
1018
+ logicalId;
1019
+ /**
1020
+ * Human-readable hint printed alongside the error. The default is
1021
+ * "use cdkd deploy with --replace, or change the resource definition
1022
+ * to create a new version" — providers are encouraged to override
1023
+ * with a more specific suggestion when one is available (e.g.
1024
+ * Lambda::Permission's "delete + add a new statement").
1025
+ */
1026
+ suggestion;
1027
+ constructor(resourceType, logicalId, suggestion, cause) {
1028
+ super(`${resourceType} (${logicalId}) cannot be updated in place: ${suggestion ? suggestion : "use cdkd deploy with --replace, or change the resource definition to create a new version"}.`, "RESOURCE_UPDATE_NOT_SUPPORTED", cause);
1029
+ this.resourceType = resourceType;
1030
+ this.logicalId = logicalId;
1031
+ this.suggestion = suggestion;
1032
+ this.name = "ResourceUpdateNotSupportedError";
1033
+ Object.setPrototypeOf(this, ResourceUpdateNotSupportedError.prototype);
1034
+ }
1035
+ };
1036
+ /**
1037
+ * Signals a refusal to destroy a stack whose CDK manifest has
1038
+ * `terminationProtection: true`.
1039
+ *
1040
+ * Surfaced from `cdkd destroy <stack>` / `cdkd destroy --all` BEFORE
1041
+ * any lock acquisition or per-resource delete. In multi-stack runs
1042
+ * (e.g. `--all`) this counts as a per-stack failure and the rest of
1043
+ * the targets continue — the aggregated count is wrapped in
1044
+ * {@link PartialFailureError} so the command exits with code 2.
1045
+ *
1046
+ * The bypass workflow is documented in the message: edit the CDK code
1047
+ * (`new Stack(app, '...', { terminationProtection: false })`),
1048
+ * redeploy, then retry the destroy. A future `--remove-protection`
1049
+ * flag (separate scope) will provide an explicit one-shot bypass.
1050
+ *
1051
+ * Note: `cdkd state destroy` (state-only, no synth) does NOT honor
1052
+ * `terminationProtection` — the flag is a CDK property not persisted
1053
+ * in cdkd's state.json. Use `cdkd destroy` when synth is available.
1054
+ */
1055
+ var StackTerminationProtectionError = class StackTerminationProtectionError extends CdkdError {
1056
+ stackName;
1057
+ constructor(stackName, cause) {
1058
+ super(`Stack '${stackName}' has terminationProtection: true and cannot be destroyed. Set terminationProtection: false in the CDK code, redeploy, then retry 'cdkd destroy ${stackName}'.`, "STACK_TERMINATION_PROTECTION", cause);
1059
+ this.stackName = stackName;
1060
+ this.name = "StackTerminationProtectionError";
1061
+ Object.setPrototypeOf(this, StackTerminationProtectionError.prototype);
1062
+ }
1063
+ };
1064
+ /**
1065
+ * `cdkd destroy <child>` refused because the named stack is a nested
1066
+ * child of another stack. Mirrors CloudFormation's `you can't directly
1067
+ * destroy a nested stack` semantic — destroying the child without
1068
+ * touching the parent would leave the parent's `AWS::CloudFormation::Stack`
1069
+ * record pointing at non-existent resources, and the parent's next
1070
+ * deploy would silently try to re-create them.
1071
+ *
1072
+ * Detected by reading the loaded state's v6 `parentStack` field — only
1073
+ * state files written by `NestedStackProvider.create` (or by the
1074
+ * recursive `cdkd import --migrate-from-cloudformation` walk) carry
1075
+ * this field; top-level stacks have `parentStack: undefined` and pass
1076
+ * the guard unchanged.
1077
+ *
1078
+ * Fires from `cdkd destroy` AFTER state load but BEFORE lock
1079
+ * acquisition or any per-resource delete, so the refusal is cheap.
1080
+ * Surfaced as a per-stack failure (wrapped in {@link PartialFailureError},
1081
+ * exit code 2) in multi-stack runs — siblings continue.
1082
+ *
1083
+ * Bypass paths (both intentional escape hatches):
1084
+ *
1085
+ * 1. `cdkd destroy <parent>` — the normal cascading-destroy path; the
1086
+ * parent's reverse-DAG walks into the child via
1087
+ * `NestedStackProvider.delete` and removes both layers atomically.
1088
+ * 2. `cdkd state destroy <child>` — state-only destroy with no parent
1089
+ * coupling check. The state-driven entry point intentionally
1090
+ * bypasses this guard for the same reason `cdkd state destroy`
1091
+ * bypasses `terminationProtection`: it's the "I know what I'm
1092
+ * doing" path for cleaning up state when synth is unavailable or
1093
+ * the user accepts leaving the parent's reference dangling.
1094
+ */
1095
+ var NestedStackChildDirectDestroyError = class NestedStackChildDirectDestroyError extends CdkdError {
1096
+ stackName;
1097
+ parentStack;
1098
+ parentLogicalId;
1099
+ constructor(stackName, parentStack, parentLogicalId, cause) {
1100
+ const logicalIdSuffix = parentLogicalId ? ` (parent's logical id: ${parentLogicalId})` : "";
1101
+ super(`Stack '${stackName}' is a nested child of '${parentStack}'${logicalIdSuffix}; directly destroying a nested stack is not supported. Either run 'cdkd destroy ${parentStack}' to cascade-delete this child along with its parent, or run 'cdkd state destroy ${stackName}' if you intentionally want to leave the parent's reference dangling (the state-only escape hatch).`, "NESTED_STACK_CHILD_DIRECT_DESTROY", cause);
1102
+ this.stackName = stackName;
1103
+ this.parentStack = parentStack;
1104
+ if (parentLogicalId !== void 0) this.parentLogicalId = parentLogicalId;
1105
+ this.name = "NestedStackChildDirectDestroyError";
1106
+ Object.setPrototypeOf(this, NestedStackChildDirectDestroyError.prototype);
1107
+ }
1108
+ };
1109
+ /**
1110
+ * `cdkd destroy <producer>` refused because at least one consumer stack
1111
+ * still records an `Fn::ImportValue` reference to one of the producer's
1112
+ * outputs. This matches CloudFormation's strong-reference semantics —
1113
+ * CFn rejects `DeleteStack` for an exporter while an importer exists.
1114
+ *
1115
+ * cdkd has no `--force` escape hatch for this (intentionally, mirroring
1116
+ * CFn). The error message lists every offending consumer and points the
1117
+ * user at the two valid resolution paths:
1118
+ *
1119
+ * 1. Destroy the consumer first: `cdkd destroy <consumer>`
1120
+ * 2. Remove the `Fn::ImportValue` from the consumer's template and
1121
+ * redeploy, then retry the producer destroy.
1122
+ *
1123
+ * Weak-reference consumers (`Fn::GetStackOutput`, cdkd-specific) never
1124
+ * trigger this error by design — the producer stays deletable
1125
+ * independently of consumers when the user intentionally chose a weak
1126
+ * reference at template-authoring time.
1127
+ *
1128
+ * Exit code 2 (same as `PartialFailureError`) so multi-stack `cdkd
1129
+ * destroy --all` runs that partially succeed still surface as
1130
+ * non-zero without being indistinguishable from a fatal cdkd error.
1131
+ */
1132
+ var StackHasActiveImportsError = class StackHasActiveImportsError extends CdkdError {
1133
+ exitCode = 2;
1134
+ producerStack;
1135
+ producerRegion;
1136
+ consumers;
1137
+ constructor(producerStack, producerRegion, consumers, cause) {
1138
+ const lines = consumers.map((c) => ` - ${c.consumerStack} (${c.consumerRegion}): imports export '${c.exportName}'`);
1139
+ super(`Cannot destroy stack '${producerStack}' (${producerRegion}): the following stacks still import its outputs via Fn::ImportValue:\n${lines.join("\n")}\n\nThis matches CloudFormation's strong-reference semantics — exports are\nprotected as long as a consumer references them.\n\nTo proceed:\n 1. Destroy the consumer first: cdkd destroy <consumer-stack>\n 2. Or remove the Fn::ImportValue from the consumer's template\n (e.g. inline the value, or refactor) and re-deploy the consumer,\n then retry this destroy.\n\nNote: cdkd's Fn::GetStackOutput intrinsic is a weak alternative that\ndoes NOT protect the producer — use it when you intentionally want\nthe producer to be deletable independently of consumers.`, "STACK_HAS_ACTIVE_IMPORTS", cause);
1140
+ this.producerStack = producerStack;
1141
+ this.producerRegion = producerRegion;
1142
+ this.consumers = consumers;
1143
+ this.name = "StackHasActiveImportsError";
1144
+ Object.setPrototypeOf(this, StackHasActiveImportsError.prototype);
1145
+ }
1146
+ };
1147
+ /**
1148
+ * Signals a `cdkd local start-service` orchestration failure (Phase 2
1149
+ * of #262 — `AWS::ECS::Service` emulator). Distinct from
1150
+ * `LocalRunTaskError` because the service runner has its own lifecycle
1151
+ * (long-running replica pool, restart-on-exit), so a failure inside it
1152
+ * carries different operator semantics than a one-shot task failure.
1153
+ */
1154
+ var LocalStartServiceError = class LocalStartServiceError extends CdkdError {
1155
+ constructor(message, cause) {
1156
+ super(message, "LOCAL_START_SERVICE_ERROR", cause);
1157
+ this.name = "LocalStartServiceError";
1158
+ Object.setPrototypeOf(this, LocalStartServiceError.prototype);
1159
+ }
1160
+ };
1161
+ /**
1162
+ * Signals that the upstream `cdk` CLI is not available on PATH (or at
1163
+ * the override path passed via `--cdk-bin`). Surfaced from `cdkd migrate`
1164
+ * (#465 PR A) before any other work runs.
1165
+ *
1166
+ * The message includes the install hint `npm install -g aws-cdk@latest`
1167
+ * so users on a fresh machine see exactly how to recover.
1168
+ */
1169
+ var MissingCdkCliError = class MissingCdkCliError extends CdkdError {
1170
+ constructor(detail, cause) {
1171
+ super(`${detail ?? "upstream 'cdk' CLI not found on PATH"}. 'cdkd migrate' shells out to the upstream aws-cdk CLI for L1 codegen — install it with 'npm install -g aws-cdk@latest' (or pass --cdk-bin <path>).`, "MISSING_CDK_CLI", cause);
1172
+ this.name = "MissingCdkCliError";
1173
+ Object.setPrototypeOf(this, MissingCdkCliError.prototype);
1174
+ }
1175
+ };
1176
+ /**
1177
+ * Generic local-migrate orchestration failure (#465 PR A). Used by
1178
+ * `cdkd migrate` for pre-flight rejections (Custom Resource / nested
1179
+ * stack / non-terminal CFn stack state), output-dir collisions, and
1180
+ * `cdk migrate` subprocess failures whose underlying stderr is folded
1181
+ * into the error message. Exit code 2 (partial-failure family) because
1182
+ * some pre-flight failures leave the user with a partially-populated
1183
+ * output directory that's still useful for debugging.
1184
+ */
1185
+ var LocalMigrateError = class LocalMigrateError extends CdkdError {
1186
+ exitCode = 2;
1187
+ constructor(message, cause) {
1188
+ super(message, "LOCAL_MIGRATE_ERROR", cause);
1189
+ this.name = "LocalMigrateError";
1190
+ Object.setPrototypeOf(this, LocalMigrateError.prototype);
1191
+ }
1192
+ };
1193
+ /**
1194
+ * CloudFormation macro / `Fn::Transform` expansion failure (#463).
1195
+ *
1196
+ * cdkd hands templates that declare `Transform: [...]` (or carry
1197
+ * `Fn::Transform: {...}` snippets) to CloudFormation server-side via a
1198
+ * transient `CreateChangeSet --change-set-type CREATE` against a
1199
+ * `cdkd-macro-expand-<id>` stack name. This error wraps every failure
1200
+ * mode of that round-trip:
1201
+ *
1202
+ * - `CreateChangeSet` rejection (bad template, missing macro IAM
1203
+ * permission, custom macro not found in the account).
1204
+ * - Changeset settles in `FAILED` (`StatusReason` from CFn is
1205
+ * surfaced verbatim — typically a custom macro Lambda error).
1206
+ * - Waiter timeout (the macro Lambda is stuck or oversized).
1207
+ * - `GetTemplate --template-stage Processed` returns no body (would
1208
+ * indicate a CFn-side regression — fail loud rather than silently
1209
+ * proceed with the un-expanded template).
1210
+ * - Multi-stage detection: the expanded template still contains
1211
+ * macros, which cdkd v1 does not support (the design intentionally
1212
+ * rejects this so a second round-trip is not silently triggered).
1213
+ *
1214
+ * The error surfaces at exit code 2 (partial-failure family) — the
1215
+ * cleanup `finally` in the expander always runs `DeleteChangeSet` +
1216
+ * `DeleteStack` regardless of this error firing, so a failed
1217
+ * expansion never leaves a transient CFn stack behind in a routine
1218
+ * case. The user can re-run `cdkd deploy` once the upstream cause is
1219
+ * fixed.
1220
+ */
1221
+ var MacroExpansionError = class MacroExpansionError extends CdkdError {
1222
+ exitCode = 2;
1223
+ constructor(message, cause) {
1224
+ super(message, "MACRO_EXPANSION_ERROR", cause);
1225
+ this.name = "MacroExpansionError";
1226
+ Object.setPrototypeOf(this, MacroExpansionError.prototype);
1227
+ }
1228
+ };
1229
+ /**
1230
+ * Check if error is a cdkd error
1231
+ */
1232
+ function isCdkdError(error) {
1233
+ return error instanceof CdkdError;
1234
+ }
1235
+ /**
1236
+ * Format error for display
1237
+ */
1238
+ function formatError(error) {
1239
+ if (isCdkdError(error)) {
1240
+ let message = `${error.name}: ${error.message}`;
1241
+ if (error.cause) message += `\nCaused by: ${error.cause.message}`;
1242
+ return message;
1243
+ }
1244
+ if (error instanceof Error) return `${error.name}: ${error.message}`;
1245
+ return String(error);
1246
+ }
1247
+ /**
1248
+ * Global error handler
1249
+ *
1250
+ * Default exit code is 1 (general error). `PartialFailureError`
1251
+ * overrides it to 2 so callers can distinguish "command crashed /
1252
+ * unauthorized / bad arguments" from "command completed but some
1253
+ * resources are still in an error state, re-run to clean up".
1254
+ *
1255
+ * A {@link CdkdError} subclass may set `silent = true` to suppress the
1256
+ * default `logger.error` line — used by `cdkd drift` where the command
1257
+ * has already printed a richer report and only needs the exit code.
1258
+ */
1259
+ function handleError(error) {
1260
+ const logger = getLogger();
1261
+ if (!(error instanceof CdkdError && error.silent)) logger.error(formatError(error));
1262
+ if (error instanceof Error && error.stack) logger.debug("Stack trace:", error.stack);
1263
+ const customExitCode = error instanceof CdkdError ? error.exitCode : void 0;
1264
+ const exitCode = typeof customExitCode === "number" ? customExitCode : 1;
1265
+ process.exit(exitCode);
1266
+ }
1267
+ /**
1268
+ * Wrap async function with error handling
1269
+ *
1270
+ * Note: Uses `any[]` for args to support Commander.js action handlers
1271
+ * which can have various parameter types
1272
+ */
1273
+ function withErrorHandling(fn) {
1274
+ return async (...args) => {
1275
+ try {
1276
+ await fn(...args);
1277
+ } catch (error) {
1278
+ handleError(error);
1279
+ }
1280
+ };
1281
+ }
1282
+ /**
1283
+ * Convert AWS SDK v3's synthetic `Unknown` / `UnknownError` exception into
1284
+ * an actionable `Error` keyed off `$metadata.httpStatusCode`.
1285
+ *
1286
+ * Background — why this helper exists:
1287
+ * AWS SDK v3 produces a synthetic `name: 'Unknown'`, `message:
1288
+ * 'UnknownError'` exception when the protocol parser hits a HEAD response
1289
+ * with an empty body. The most common trigger is `HeadBucket` against a
1290
+ * bucket in a different region than the client (S3 returns 301
1291
+ * PermanentRedirect with `x-amz-bucket-region` set, but the redirect
1292
+ * middleware doesn't recover from the empty body). Surfacing the literal
1293
+ * string `UnknownError` to users is uninformative.
1294
+ *
1295
+ * Behavior:
1296
+ * - Non-AWS-SDK errors (anything where `name` is not `Unknown` and
1297
+ * `message` is not `UnknownError`) pass through unchanged.
1298
+ * - AWS SDK Unknown errors are mapped by HTTP status:
1299
+ * - 301 → `Bucket '<name>' is in a different region…` (auto-resolved
1300
+ * elsewhere; if this surfaces, it's a bug worth reporting).
1301
+ * - 403 → `Access denied to bucket '<name>'.`
1302
+ * - 404 → `Bucket '<name>' does not exist.`
1303
+ * - other / unknown → `S3 error during <operation> on '<bucket>' (HTTP
1304
+ * <status>).`
1305
+ */
1306
+ function normalizeAwsError(err, context = {}) {
1307
+ if (!(err instanceof Error)) return new Error(String(err));
1308
+ if (!(err.name === "Unknown" || err.message === "UnknownError")) return err;
1309
+ const status = err.$metadata?.httpStatusCode;
1310
+ const bucket = context.bucket ?? "<unknown bucket>";
1311
+ const operation = context.operation ?? "operation";
1312
+ switch (status) {
1313
+ case 301: {
1314
+ const responseHeaders = err.$response?.headers;
1315
+ const region = responseHeaders?.["x-amz-bucket-region"] ?? responseHeaders?.["X-Amz-Bucket-Region"];
1316
+ const where = region ? ` (in ${region})` : "";
1317
+ return /* @__PURE__ */ new Error(`Bucket '${bucket}'${where} is in a different region than the client. cdkd resolves this automatically; if you see this message, please report it.`);
1318
+ }
1319
+ case 403: return /* @__PURE__ */ new Error(`Access denied to bucket '${bucket}'. Verify credentials and bucket policy.`);
1320
+ case 404: return /* @__PURE__ */ new Error(`Bucket '${bucket}' does not exist.`);
1321
+ default: {
1322
+ const statusStr = status !== void 0 ? `HTTP ${status}` : "unknown HTTP status";
1323
+ return /* @__PURE__ */ new Error(`S3 error during ${operation} on '${bucket}' (${statusStr}). See CloudTrail for details.`);
1324
+ }
1325
+ }
1326
+ }
1327
+
1328
+ //#endregion
1329
+ //#region src/provisioning/ec2-termination-protection.ts
1330
+ /**
1331
+ * Flip `DisableApiTermination` off on an instance. Idempotent — EC2 accepts the
1332
+ * call when the attribute is already false. Non-fatal: a NotFound (already
1333
+ * gone) or any other error is swallowed at debug so the actual delete still
1334
+ * proceeds (it will surface the real failure if the instance truly cannot be
1335
+ * deleted).
1336
+ */
1337
+ async function disableInstanceApiTermination(client, instanceId, logger) {
1338
+ try {
1339
+ await client.send(new ModifyInstanceAttributeCommand({
1340
+ InstanceId: instanceId,
1341
+ DisableApiTermination: { Value: false }
1342
+ }));
1343
+ logger.debug(`Disabled DisableApiTermination on EC2 Instance ${instanceId} before deletion`);
1344
+ } catch (flipError) {
1345
+ logger.debug(`Could not disable DisableApiTermination on ${instanceId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`);
1346
+ }
1347
+ }
1348
+ /**
1349
+ * Does this error message indicate the terminate / delete raced the
1350
+ * `DisableApiTermination` flip-off propagation (so re-flipping + retrying is
1351
+ * the right move)? Matches both the SDK `TerminateInstances` 400 and the Cloud
1352
+ * Control `DeleteResource` wrapper of the same underlying EC2 error.
1353
+ */
1354
+ function isTerminationProtectionPropagationError(message) {
1355
+ return /may not be terminated|disableApiTermination/i.test(message);
1356
+ }
1357
+
1358
+ //#endregion
1359
+ //#region src/provisioning/region-check.ts
1360
+ /**
1361
+ * Verify that the AWS client's region matches the region the resource is
1362
+ * expected to live in before treating a `NotFound` error as idempotent
1363
+ * delete success.
1364
+ *
1365
+ * Why: a destroy run with the wrong region would otherwise receive
1366
+ * `*NotFound` for every resource and silently strip them all from state,
1367
+ * leaving the actual AWS resources orphaned in the real region. The
1368
+ * silent-failure incident that motivated this check was a Lambda in
1369
+ * `us-west-2` removed from state by a destroy that ran with a `us-east-1`
1370
+ * client.
1371
+ *
1372
+ * Behavior:
1373
+ * - If `expectedRegion` is unset, this is a no-op (back-compat: existing
1374
+ * idempotent semantics preserved for callers that have not been
1375
+ * threaded with state region).
1376
+ * - If `clientRegion` matches `expectedRegion`, returns silently.
1377
+ * - Otherwise throws `ProvisioningError` so the caller surfaces the
1378
+ * mismatch instead of swallowing the NotFound.
1379
+ *
1380
+ * @param clientRegion Region resolved from the AWS SDK client config
1381
+ * (typically `await client.config.region()`).
1382
+ * @param expectedRegion Region recorded in stack state, or undefined if
1383
+ * the caller has no expected region.
1384
+ * @param resourceType CloudFormation resource type, used in the error
1385
+ * message and on the thrown ProvisioningError.
1386
+ * @param logicalId Logical ID of the resource, used in the error message
1387
+ * and on the thrown ProvisioningError.
1388
+ * @param physicalId Optional physical ID, used in the error message and
1389
+ * on the thrown ProvisioningError.
1390
+ */
1391
+ function assertRegionMatch(clientRegion, expectedRegion, resourceType, logicalId, physicalId) {
1392
+ if (!expectedRegion) return;
1393
+ if (!clientRegion) throw new ProvisioningError(`Refusing to treat NotFound as idempotent delete success for ${logicalId} (${resourceType}): AWS client region is unknown but stack state expects ${expectedRegion}. The resource may exist in ${expectedRegion} and would be silently removed from state if this NotFound were trusted.`, resourceType, logicalId, physicalId);
1394
+ if (clientRegion !== expectedRegion) throw new ProvisioningError(`Refusing to treat NotFound as idempotent delete success for ${logicalId} (${resourceType}): AWS client region ${clientRegion} does not match stack state region ${expectedRegion}. The resource likely still exists in ${expectedRegion}; rerun the destroy with the correct region (e.g. --region ${expectedRegion}).`, resourceType, logicalId, physicalId);
1395
+ }
1396
+
1397
+ //#endregion
1398
+ //#region src/provisioning/import-helpers.ts
1399
+ /**
1400
+ * Read an explicit name field from template properties. Returns `undefined`
1401
+ * when the property is missing or not a string — callers fall back to
1402
+ * tag-based lookup in that case.
1403
+ */
1404
+ function readNameProperty(input, propertyName) {
1405
+ const value = input.properties?.[propertyName];
1406
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1407
+ }
1408
+ /**
1409
+ * Resolve the physical id when the template provides an explicit name OR the
1410
+ * caller passed `--resource`/`--resource-mapping`. Returns `undefined` when
1411
+ * neither shortcut applies — caller must then fall back to tag-based lookup.
1412
+ *
1413
+ * Does NOT verify the resource exists: callers should follow up with a
1414
+ * service-specific `Head*`/`Get*`/`Describe*` to fail fast if the named
1415
+ * resource is missing.
1416
+ */
1417
+ function resolveExplicitPhysicalId(input, nameProperty) {
1418
+ if (input.knownPhysicalId) return input.knownPhysicalId;
1419
+ if (nameProperty) {
1420
+ const name = readNameProperty(input, nameProperty);
1421
+ if (name) return name;
1422
+ }
1423
+ }
1424
+ /**
1425
+ * The standard tag CDK puts on every deployed resource — its construct path
1426
+ * within the app, e.g. `MyStack/MyConstruct/MyBucket`. Used as the lookup key
1427
+ * when no explicit name is in the template.
1428
+ */
1429
+ const CDK_PATH_TAG = "aws:cdk:path";
1430
+ /**
1431
+ * Match an AWS resource's tag set against the CDK path the template carries.
1432
+ * Returns true if the resource was deployed by the same CDK construct.
1433
+ */
1434
+ function matchesCdkPath(tags, cdkPath) {
1435
+ if (!tags || !cdkPath) return false;
1436
+ for (const t of tags) if (t.Key === "aws:cdk:path" && t.Value === cdkPath) return true;
1437
+ return false;
1438
+ }
1439
+ /**
1440
+ * Re-shape an AWS tag list (any of the common shapes — array of `{Key, Value}`,
1441
+ * map keyed by tag name, or v2-style array of `{TagKey, TagValue}`) into the
1442
+ * canonical CFn shape (`Array<{Key, Value}>`) that cdkd state holds, with
1443
+ * `aws:`-prefixed entries filtered out.
1444
+ *
1445
+ * AWS reserves the `aws:` tag prefix; CDK injects `aws:cdk:path` (and
1446
+ * sometimes `aws:cdk:metadata`) on every resource it deploys. Those tags are
1447
+ * NOT in cdkd state's `Tags` (they come from CDK template `Metadata`, not
1448
+ * `Properties.Tags`), so leaving them in the AWS-current snapshot would fire
1449
+ * false-positive drift on every CDK-deployed resource.
1450
+ *
1451
+ * Returns an empty array `[]` when AWS reports no user tags. Callers decide
1452
+ * whether to surface `Tags: []` (most providers — matches the typical
1453
+ * CFn behavior of always emitting Tags in templates) or omit the key
1454
+ * entirely (when the corresponding `create()` only sets Tags when the user
1455
+ * explicitly passes them — see each provider's docstring).
1456
+ */
1457
+ function normalizeAwsTagsToCfn(tags) {
1458
+ if (!tags) return [];
1459
+ const out = [];
1460
+ if (Array.isArray(tags)) for (const t of tags) {
1461
+ const obj = t;
1462
+ const k = (typeof obj["Key"] === "string" ? obj["Key"] : void 0) ?? (typeof obj["TagKey"] === "string" ? obj["TagKey"] : void 0) ?? (typeof obj["key"] === "string" ? obj["key"] : void 0);
1463
+ const v = (typeof obj["Value"] === "string" ? obj["Value"] : void 0) ?? (typeof obj["TagValue"] === "string" ? obj["TagValue"] : void 0) ?? (typeof obj["value"] === "string" ? obj["value"] : void 0);
1464
+ if (typeof k !== "string" || k.length === 0) continue;
1465
+ if (k.startsWith("aws:")) continue;
1466
+ out.push({
1467
+ Key: k,
1468
+ Value: typeof v === "string" ? v : ""
1469
+ });
1470
+ }
1471
+ else for (const [k, v] of Object.entries(tags)) {
1472
+ if (!k || k.startsWith("aws:")) continue;
1473
+ out.push({
1474
+ Key: k,
1475
+ Value: typeof v === "string" ? v : ""
1476
+ });
1477
+ }
1478
+ out.sort((a, b) => a.Key < b.Key ? -1 : a.Key > b.Key ? 1 : 0);
1479
+ return out;
1480
+ }
1481
+
1482
+ //#endregion
1483
+ export { withErrorHandling as A, generateResourceNameWithFallback as B, StackHasActiveImportsError as C, formatError as D, SynthesisError as E, getLiveRenderer as F, withStackName as H, PATTERN_B_NAME_PROPERTIES as I, PATTERN_B_RESOURCE_TYPES as L, getLogger as M, setLogger as N, isCdkdError as O, runStackBuffered as P, applyDefaultNameForFallback as R, ResourceUpdateNotSupportedError as S, StateError as T, withSkipPrefix as V, MissingCdkCliError as _, assertRegionMatch as a, ProvisioningError as b, AssetError as c, DependencyError as d, LocalInvokeBuildError as f, MacroExpansionError as g, LockError as h, resolveExplicitPhysicalId as i, ConsoleLogger as j, normalizeAwsError as k, CdkdError as l, LocalStartServiceError as m, matchesCdkPath as n, disableInstanceApiTermination as o, LocalMigrateError as p, normalizeAwsTagsToCfn as r, isTerminationProtectionPropagationError as s, CDK_PATH_TAG as t, ConfigError as u, NestedStackChildDirectDestroyError as v, StackTerminationProtectionError as w, ResourceTimeoutError as x, PartialFailureError as y, generateResourceName as z };
1484
+ //# sourceMappingURL=import-helpers-wLipXr5g.js.map