@go-to-k/cdkd 0.196.0 → 0.198.0
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/dist/{aws-clients-B15NAPbL.js → aws-clients-DWUnLza1.js} +18 -2
- package/dist/{aws-clients-B15NAPbL.js.map → aws-clients-DWUnLza1.js.map} +1 -1
- package/dist/cli.js +248 -1185
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-C6v_fcDw.js → deploy-engine-B3Xc-bxB.js} +998 -9
- package/dist/deploy-engine-B3Xc-bxB.js.map +1 -0
- package/dist/index.js +2 -3
- package/package.json +1 -1
- package/dist/deploy-engine-C6v_fcDw.js.map +0 -1
- package/dist/docker-cmd-iDMcWcre.js +0 -1004
- package/dist/docker-cmd-iDMcWcre.js.map +0 -1
- package/dist/rolldown-runtime-CjeV3_4I.js +0 -18
|
@@ -1,1004 +0,0 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-CjeV3_4I.js";
|
|
2
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
|
|
6
|
-
//#region src/provisioning/resource-name.ts
|
|
7
|
-
/**
|
|
8
|
-
* Per-async-context stack name. Resource-name generation reads this so that
|
|
9
|
-
* concurrent deploys (`cdkd deploy --all` runs stacks in parallel up to
|
|
10
|
-
* `--stack-concurrency`) don't fight over a single shared variable.
|
|
11
|
-
*
|
|
12
|
-
* History: this was `let currentStackName: string | undefined` until
|
|
13
|
-
* 2026-05-01. Two parallel `deploy()` calls would each call
|
|
14
|
-
* `setCurrentStackName(...)` and the second would overwrite the first;
|
|
15
|
-
* any IAM Role / SQS Queue / etc. created by the first stack while the
|
|
16
|
-
* second was active would get the second stack's prefix in its physical
|
|
17
|
-
* name, then the second stack's own create attempt for the same logical
|
|
18
|
-
* id would collide ("Role with name X already exists"). Switching to
|
|
19
|
-
* `AsyncLocalStorage` scopes the value to each deploy's async chain.
|
|
20
|
-
*/
|
|
21
|
-
const stackNameStore = new AsyncLocalStorage();
|
|
22
|
-
function withStackName(stackName, fn) {
|
|
23
|
-
return stackNameStore.run(stackName, fn);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Read the current async context's stack name, if any.
|
|
27
|
-
*
|
|
28
|
-
* Returns `undefined` outside any `withStackName` / `setCurrentStackName`
|
|
29
|
-
* scope. Used by the live renderer to scope per-stack in-flight task
|
|
30
|
-
* entries so concurrent deploys don't clobber each other's tasks (same
|
|
31
|
-
* `logicalId` in two stacks would collide on the singleton renderer's
|
|
32
|
-
* task Map without this).
|
|
33
|
-
*/
|
|
34
|
-
function getCurrentStackName() {
|
|
35
|
-
return stackNameStore.getStore();
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Per-async-context "skip the stack-name prefix on user-supplied physical
|
|
39
|
-
* names" flag. Read by `generateResourceName` when its caller passes
|
|
40
|
-
* `userSupplied: true`; auto-generated-name paths
|
|
41
|
-
* (`generateResourceName(logicalId, ...)`) ignore this flag.
|
|
42
|
-
*
|
|
43
|
-
* Scoped via AsyncLocalStorage so that `--stack-concurrency > 1` runs
|
|
44
|
-
* cannot cross-contaminate — each deploy's body is wrapped in its own
|
|
45
|
-
* `withSkipPrefix(...)` scope (the deploy CLI plumbs the resolved
|
|
46
|
-
* `--no-prefix-user-supplied-names` value through here). Default
|
|
47
|
-
* `false` preserves pre-PR behavior when the flag is not set.
|
|
48
|
-
*/
|
|
49
|
-
const skipPrefixStore = new AsyncLocalStorage();
|
|
50
|
-
function withSkipPrefix(skip, fn) {
|
|
51
|
-
return skipPrefixStore.run(skip, fn);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Read the current async context's skip-prefix flag. Defaults to
|
|
55
|
-
* `false` when no `withSkipPrefix` scope is active.
|
|
56
|
-
*
|
|
57
|
-
* Public for unit tests; `generateResourceName` consumes this
|
|
58
|
-
* internally.
|
|
59
|
-
*/
|
|
60
|
-
function getCurrentSkipPrefix() {
|
|
61
|
-
return skipPrefixStore.getStore() ?? false;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Resource types whose pre-PR #297 code path ran user-supplied
|
|
65
|
-
* physical names through `generateResourceName` (= got the stack-name
|
|
66
|
-
* prefix). These are the only types affected by
|
|
67
|
-
* `--no-prefix-user-supplied-names`; flipping the flag against an
|
|
68
|
-
* existing stack proposes REPLACEMENT on every state resource of
|
|
69
|
-
* one of these types whose `physicalId` is still prefixed.
|
|
70
|
-
*
|
|
71
|
-
* Pattern A providers (Lambda, S3, SNS, SQS, DynamoDB, Logs LogGroup,
|
|
72
|
-
* Events Rule, etc.) historically short-circuited user-supplied names
|
|
73
|
-
* **out** of `generateResourceName` entirely — those types never got
|
|
74
|
-
* the prefix regardless of the flag, so they are NOT included here.
|
|
75
|
-
*
|
|
76
|
-
* Used by the deploy-time pre-flight migration check in
|
|
77
|
-
* `src/cli/commands/prefix-migration-check.ts`. Keep in sync with the
|
|
78
|
-
* Pattern B provider call sites that consume
|
|
79
|
-
* `generateResourceNameWithFallback(...)`.
|
|
80
|
-
*/
|
|
81
|
-
const PATTERN_B_RESOURCE_TYPES = [
|
|
82
|
-
"AWS::IAM::Role",
|
|
83
|
-
"AWS::IAM::User",
|
|
84
|
-
"AWS::IAM::Group",
|
|
85
|
-
"AWS::IAM::InstanceProfile",
|
|
86
|
-
"AWS::IAM::ManagedPolicy",
|
|
87
|
-
"AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
88
|
-
"AWS::ElasticLoadBalancingV2::TargetGroup"
|
|
89
|
-
];
|
|
90
|
-
/**
|
|
91
|
-
* For each Pattern B resource type, the CFn template `Properties` field
|
|
92
|
-
* the user sets to supply a physical name (`new iam.Role(this, 'X',
|
|
93
|
-
* { roleName: 'my-role' })` → `Properties.RoleName: 'my-role'`).
|
|
94
|
-
*
|
|
95
|
-
* Used by the prefix-migration check to distinguish user-supplied
|
|
96
|
-
* physical names (which the v0.94.0 default flip would actually
|
|
97
|
-
* REPLACE on next deploy) from auto-generated logical-id-fallback
|
|
98
|
-
* names (which keep the prefix in BOTH old and new default — no
|
|
99
|
-
* REPLACE pending). Without this discriminator, the migration
|
|
100
|
-
* check naively flags every prefix-style physicalId regardless of
|
|
101
|
-
* its origin, surfacing a false-positive WARNING on auto-generated
|
|
102
|
-
* names that won't actually be touched.
|
|
103
|
-
*
|
|
104
|
-
* Keep in sync with `PATTERN_B_RESOURCE_TYPES` and with each
|
|
105
|
-
* provider's `Properties[<NameField>]` lookup in
|
|
106
|
-
* `src/provisioning/providers/`.
|
|
107
|
-
*/
|
|
108
|
-
const PATTERN_B_NAME_PROPERTIES = {
|
|
109
|
-
"AWS::IAM::Role": "RoleName",
|
|
110
|
-
"AWS::IAM::User": "UserName",
|
|
111
|
-
"AWS::IAM::Group": "GroupName",
|
|
112
|
-
"AWS::IAM::InstanceProfile": "InstanceProfileName",
|
|
113
|
-
"AWS::IAM::ManagedPolicy": "ManagedPolicyName",
|
|
114
|
-
"AWS::ElasticLoadBalancingV2::LoadBalancer": "Name",
|
|
115
|
-
"AWS::ElasticLoadBalancingV2::TargetGroup": "Name"
|
|
116
|
-
};
|
|
117
|
-
/**
|
|
118
|
-
* Generate a unique resource name from the logical ID.
|
|
119
|
-
*
|
|
120
|
-
* Generates names in CloudFormation-compatible format:
|
|
121
|
-
* `{StackName}-{LogicalId}-{Hash}` (truncated to maxLength).
|
|
122
|
-
*
|
|
123
|
-
* @param name The raw name (from properties or logicalId fallback)
|
|
124
|
-
* @param options Length and character constraints
|
|
125
|
-
* @returns A sanitized, truncated name that fits the constraints
|
|
126
|
-
*/
|
|
127
|
-
function generateResourceName(name, options) {
|
|
128
|
-
const { maxLength, lowercase = false, allowedPattern = /[^a-zA-Z0-9-]/g, userSupplied = false } = options;
|
|
129
|
-
const currentStackName = stackNameStore.getStore();
|
|
130
|
-
const fullName = currentStackName && !(userSupplied && getCurrentSkipPrefix()) ? `${currentStackName}-${name}` : name;
|
|
131
|
-
let sanitized = lowercase ? fullName.toLowerCase() : fullName;
|
|
132
|
-
sanitized = sanitized.replace(allowedPattern, "-");
|
|
133
|
-
sanitized = sanitized.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
134
|
-
if (sanitized.length <= maxLength) return sanitized;
|
|
135
|
-
const hash = createHash("sha256").update(fullName).digest("hex").substring(0, 8);
|
|
136
|
-
const maxPrefixLength = maxLength - hash.length - 1;
|
|
137
|
-
return `${sanitized.substring(0, maxPrefixLength).replace(/-+$/, "")}-${hash}`;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Generate a resource name from a user-declared physical name OR
|
|
141
|
-
* fall back to the logical id.
|
|
142
|
-
*
|
|
143
|
-
* Wraps {@link generateResourceName} to express the Pattern B call-site
|
|
144
|
-
* shape (`generateResourceName((properties['Name'] as string | undefined)
|
|
145
|
-
* || logicalId, opts)`) as a single typed helper. The user-supplied
|
|
146
|
-
* branch passes `userSupplied: true`, which makes the per-deploy
|
|
147
|
-
* `withSkipPrefix(true)` flag drop the stack-name prefix on that name.
|
|
148
|
-
* The fallback (logical-id) branch is `userSupplied: false` and keeps
|
|
149
|
-
* the prefix regardless of the flag — auto-generated names rely on
|
|
150
|
-
* the prefix for cross-stack uniqueness.
|
|
151
|
-
*
|
|
152
|
-
* Use at every Pattern B provider call site (currently IAM Role, IAM
|
|
153
|
-
* User, IAM Group, IAM InstanceProfile, ELBv2 LoadBalancer, ELBv2
|
|
154
|
-
* TargetGroup) so the `--no-prefix-user-supplied-names` flag controls
|
|
155
|
-
* those types consistently. Pattern A providers (Lambda, S3, SNS,
|
|
156
|
-
* SQS, DynamoDB, etc.) do NOT need this helper — they already
|
|
157
|
-
* short-circuit the user-supplied name out of the
|
|
158
|
-
* `generateResourceName` call entirely, so the prefix is never
|
|
159
|
-
* applied to user-supplied names regardless of the flag.
|
|
160
|
-
*/
|
|
161
|
-
function generateResourceNameWithFallback(userSuppliedName, logicalId, options) {
|
|
162
|
-
if (userSuppliedName !== void 0 && userSuppliedName !== "") return generateResourceName(userSuppliedName, {
|
|
163
|
-
...options,
|
|
164
|
-
userSupplied: true
|
|
165
|
-
});
|
|
166
|
-
return generateResourceName(logicalId, {
|
|
167
|
-
...options,
|
|
168
|
-
userSupplied: false
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Default name generation rules for CC API fallback.
|
|
173
|
-
*
|
|
174
|
-
* When an SDK provider falls back to CC API, the resource may need a
|
|
175
|
-
* default name that the SDK provider would have generated. This map
|
|
176
|
-
* defines the name property and generation options for each resource type.
|
|
177
|
-
*
|
|
178
|
-
* Format: resourceType → { nameProperty, options, postProcess? }
|
|
179
|
-
*/
|
|
180
|
-
const FALLBACK_NAME_RULES = {
|
|
181
|
-
"AWS::S3::Bucket": {
|
|
182
|
-
nameProperty: "BucketName",
|
|
183
|
-
options: {
|
|
184
|
-
maxLength: 63,
|
|
185
|
-
lowercase: true
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
"AWS::SQS::Queue": {
|
|
189
|
-
nameProperty: "QueueName",
|
|
190
|
-
options: { maxLength: 80 }
|
|
191
|
-
},
|
|
192
|
-
"AWS::SNS::Topic": {
|
|
193
|
-
nameProperty: "TopicName",
|
|
194
|
-
options: { maxLength: 256 }
|
|
195
|
-
},
|
|
196
|
-
"AWS::Lambda::Function": {
|
|
197
|
-
nameProperty: "FunctionName",
|
|
198
|
-
options: { maxLength: 64 }
|
|
199
|
-
},
|
|
200
|
-
"AWS::Lambda::LayerVersion": {
|
|
201
|
-
nameProperty: "LayerName",
|
|
202
|
-
options: { maxLength: 64 }
|
|
203
|
-
},
|
|
204
|
-
"AWS::IAM::Role": {
|
|
205
|
-
nameProperty: "RoleName",
|
|
206
|
-
options: { maxLength: 64 }
|
|
207
|
-
},
|
|
208
|
-
"AWS::IAM::Policy": {
|
|
209
|
-
nameProperty: "PolicyName",
|
|
210
|
-
options: { maxLength: 64 }
|
|
211
|
-
},
|
|
212
|
-
"AWS::IAM::ManagedPolicy": {
|
|
213
|
-
nameProperty: "ManagedPolicyName",
|
|
214
|
-
options: { maxLength: 128 }
|
|
215
|
-
},
|
|
216
|
-
"AWS::IAM::User": {
|
|
217
|
-
nameProperty: "UserName",
|
|
218
|
-
options: { maxLength: 64 }
|
|
219
|
-
},
|
|
220
|
-
"AWS::IAM::Group": {
|
|
221
|
-
nameProperty: "GroupName",
|
|
222
|
-
options: { maxLength: 128 }
|
|
223
|
-
},
|
|
224
|
-
"AWS::IAM::InstanceProfile": {
|
|
225
|
-
nameProperty: "InstanceProfileName",
|
|
226
|
-
options: { maxLength: 128 }
|
|
227
|
-
},
|
|
228
|
-
"AWS::DynamoDB::Table": {
|
|
229
|
-
nameProperty: "TableName",
|
|
230
|
-
options: { maxLength: 255 }
|
|
231
|
-
},
|
|
232
|
-
"AWS::ECR::Repository": {
|
|
233
|
-
nameProperty: "RepositoryName",
|
|
234
|
-
options: {
|
|
235
|
-
maxLength: 256,
|
|
236
|
-
lowercase: true
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
"AWS::ECS::Cluster": {
|
|
240
|
-
nameProperty: "ClusterName",
|
|
241
|
-
options: { maxLength: 255 }
|
|
242
|
-
},
|
|
243
|
-
"AWS::ECS::Service": {
|
|
244
|
-
nameProperty: "ServiceName",
|
|
245
|
-
options: { maxLength: 255 }
|
|
246
|
-
},
|
|
247
|
-
"AWS::Logs::LogGroup": {
|
|
248
|
-
nameProperty: "LogGroupName",
|
|
249
|
-
options: { maxLength: 512 }
|
|
250
|
-
},
|
|
251
|
-
"AWS::CloudWatch::Alarm": {
|
|
252
|
-
nameProperty: "AlarmName",
|
|
253
|
-
options: { maxLength: 256 }
|
|
254
|
-
},
|
|
255
|
-
"AWS::Events::Rule": {
|
|
256
|
-
nameProperty: "Name",
|
|
257
|
-
options: { maxLength: 64 }
|
|
258
|
-
},
|
|
259
|
-
"AWS::Events::EventBus": {
|
|
260
|
-
nameProperty: "Name",
|
|
261
|
-
options: { maxLength: 256 }
|
|
262
|
-
},
|
|
263
|
-
"AWS::Kinesis::Stream": {
|
|
264
|
-
nameProperty: "Name",
|
|
265
|
-
options: { maxLength: 128 }
|
|
266
|
-
},
|
|
267
|
-
"AWS::StepFunctions::StateMachine": {
|
|
268
|
-
nameProperty: "StateMachineName",
|
|
269
|
-
options: { maxLength: 80 }
|
|
270
|
-
},
|
|
271
|
-
"AWS::SecretsManager::Secret": {
|
|
272
|
-
nameProperty: "Name",
|
|
273
|
-
options: {
|
|
274
|
-
maxLength: 512,
|
|
275
|
-
allowedPattern: /[^a-zA-Z0-9-/_]/g
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
"AWS::SSM::Parameter": {
|
|
279
|
-
nameProperty: "Name",
|
|
280
|
-
options: { maxLength: 2048 }
|
|
281
|
-
},
|
|
282
|
-
"AWS::Cognito::UserPool": {
|
|
283
|
-
nameProperty: "UserPoolName",
|
|
284
|
-
options: { maxLength: 128 }
|
|
285
|
-
},
|
|
286
|
-
"AWS::ElastiCache::SubnetGroup": {
|
|
287
|
-
nameProperty: "CacheSubnetGroupName",
|
|
288
|
-
options: {
|
|
289
|
-
maxLength: 255,
|
|
290
|
-
lowercase: true
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
|
-
"AWS::ElastiCache::CacheCluster": {
|
|
294
|
-
nameProperty: "ClusterName",
|
|
295
|
-
options: {
|
|
296
|
-
maxLength: 40,
|
|
297
|
-
lowercase: true
|
|
298
|
-
}
|
|
299
|
-
},
|
|
300
|
-
"AWS::RDS::DBSubnetGroup": {
|
|
301
|
-
nameProperty: "DBSubnetGroupName",
|
|
302
|
-
options: {
|
|
303
|
-
maxLength: 255,
|
|
304
|
-
lowercase: true
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
"AWS::RDS::DBCluster": {
|
|
308
|
-
nameProperty: "DBClusterIdentifier",
|
|
309
|
-
options: {
|
|
310
|
-
maxLength: 63,
|
|
311
|
-
lowercase: true
|
|
312
|
-
}
|
|
313
|
-
},
|
|
314
|
-
"AWS::RDS::DBInstance": {
|
|
315
|
-
nameProperty: "DBInstanceIdentifier",
|
|
316
|
-
options: {
|
|
317
|
-
maxLength: 63,
|
|
318
|
-
lowercase: true
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
"AWS::DocDB::DBSubnetGroup": {
|
|
322
|
-
nameProperty: "DBSubnetGroupName",
|
|
323
|
-
options: {
|
|
324
|
-
maxLength: 255,
|
|
325
|
-
lowercase: true
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
"AWS::DocDB::DBCluster": {
|
|
329
|
-
nameProperty: "DBClusterIdentifier",
|
|
330
|
-
options: {
|
|
331
|
-
maxLength: 63,
|
|
332
|
-
lowercase: true
|
|
333
|
-
}
|
|
334
|
-
},
|
|
335
|
-
"AWS::DocDB::DBInstance": {
|
|
336
|
-
nameProperty: "DBInstanceIdentifier",
|
|
337
|
-
options: {
|
|
338
|
-
maxLength: 63,
|
|
339
|
-
lowercase: true
|
|
340
|
-
}
|
|
341
|
-
},
|
|
342
|
-
"AWS::Neptune::DBSubnetGroup": {
|
|
343
|
-
nameProperty: "DBSubnetGroupName",
|
|
344
|
-
options: {
|
|
345
|
-
maxLength: 255,
|
|
346
|
-
lowercase: true
|
|
347
|
-
}
|
|
348
|
-
},
|
|
349
|
-
"AWS::Neptune::DBCluster": {
|
|
350
|
-
nameProperty: "DBClusterIdentifier",
|
|
351
|
-
options: {
|
|
352
|
-
maxLength: 63,
|
|
353
|
-
lowercase: true
|
|
354
|
-
}
|
|
355
|
-
},
|
|
356
|
-
"AWS::Neptune::DBInstance": {
|
|
357
|
-
nameProperty: "DBInstanceIdentifier",
|
|
358
|
-
options: {
|
|
359
|
-
maxLength: 63,
|
|
360
|
-
lowercase: true
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
|
-
"AWS::ElasticLoadBalancingV2::LoadBalancer": {
|
|
364
|
-
nameProperty: "Name",
|
|
365
|
-
options: { maxLength: 32 }
|
|
366
|
-
},
|
|
367
|
-
"AWS::ElasticLoadBalancingV2::TargetGroup": {
|
|
368
|
-
nameProperty: "Name",
|
|
369
|
-
options: { maxLength: 32 }
|
|
370
|
-
},
|
|
371
|
-
"AWS::WAFv2::WebACL": {
|
|
372
|
-
nameProperty: "Name",
|
|
373
|
-
options: { maxLength: 128 }
|
|
374
|
-
},
|
|
375
|
-
"AWS::CodeBuild::Project": {
|
|
376
|
-
nameProperty: "Name",
|
|
377
|
-
options: { maxLength: 255 }
|
|
378
|
-
},
|
|
379
|
-
"AWS::S3Express::DirectoryBucket": {
|
|
380
|
-
nameProperty: "BucketName",
|
|
381
|
-
options: {
|
|
382
|
-
maxLength: 63,
|
|
383
|
-
lowercase: true
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
/**
|
|
388
|
-
* Apply default name generation for CC API fallback.
|
|
389
|
-
*
|
|
390
|
-
* When a resource doesn't have an explicit name property set,
|
|
391
|
-
* generates the same default name that the SDK provider would have created.
|
|
392
|
-
* This ensures consistent naming regardless of whether SDK or CC API handles the resource.
|
|
393
|
-
*
|
|
394
|
-
* @param logicalId Logical ID from the template
|
|
395
|
-
* @param resourceType CloudFormation resource type
|
|
396
|
-
* @param properties Resource properties (will not be mutated)
|
|
397
|
-
* @returns Properties with default name applied if needed, or original properties if no rule exists
|
|
398
|
-
*/
|
|
399
|
-
function applyDefaultNameForFallback(logicalId, resourceType, properties) {
|
|
400
|
-
const rule = FALLBACK_NAME_RULES[resourceType];
|
|
401
|
-
if (!rule) return properties;
|
|
402
|
-
if (properties[rule.nameProperty]) return properties;
|
|
403
|
-
const generatedName = generateResourceName(logicalId, rule.options);
|
|
404
|
-
return {
|
|
405
|
-
...properties,
|
|
406
|
-
[rule.nameProperty]: generatedName
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
//#endregion
|
|
411
|
-
//#region src/utils/live-renderer.ts
|
|
412
|
-
/**
|
|
413
|
-
* Live multi-line progress renderer for the bottom of the terminal.
|
|
414
|
-
*
|
|
415
|
-
* Maintains a "live area" listing in-flight tasks (Creating MyBucket...),
|
|
416
|
-
* redrawn on a spinner timer. Other log output is routed through
|
|
417
|
-
* {@link LiveRenderer.printAbove} so it appears above the live area without
|
|
418
|
-
* disturbing the currently-displayed in-flight tasks.
|
|
419
|
-
*
|
|
420
|
-
* Design notes:
|
|
421
|
-
* - Multiple resources can be in flight concurrently (cdkd uses parallel DAG
|
|
422
|
-
* dispatch), so a single in-place line overwrite is not enough — each
|
|
423
|
-
* in-flight resource is its own line in the live area.
|
|
424
|
-
* - On non-TTY (CI/log-collection), the renderer stays inactive and
|
|
425
|
-
* {@link LiveRenderer.printAbove} falls through to a direct write, so output
|
|
426
|
-
* matches the previous append-only behavior.
|
|
427
|
-
* - In verbose mode (debug level) the caller should not start the renderer:
|
|
428
|
-
* debug logs would interleave too aggressively with the live area.
|
|
429
|
-
*/
|
|
430
|
-
const SPINNER_FRAMES = [
|
|
431
|
-
"⠋",
|
|
432
|
-
"⠙",
|
|
433
|
-
"⠹",
|
|
434
|
-
"⠸",
|
|
435
|
-
"⠼",
|
|
436
|
-
"⠴",
|
|
437
|
-
"⠦",
|
|
438
|
-
"⠧",
|
|
439
|
-
"⠇",
|
|
440
|
-
"⠏"
|
|
441
|
-
];
|
|
442
|
-
const FRAME_INTERVAL_MS = 80;
|
|
443
|
-
const ESC = "\x1B[";
|
|
444
|
-
/**
|
|
445
|
-
* Scope a task `id` to its calling stack so two stacks running in
|
|
446
|
-
* parallel — `cdkd deploy --all` with `--stack-concurrency > 1` — don't
|
|
447
|
-
* collide on the same `logicalId` in the renderer's task Map. Without
|
|
448
|
-
* this, stack B's `addTask('MyQueue', ...)` would overwrite stack A's
|
|
449
|
-
* entry, and stack A's later `removeTask('MyQueue')` would erase
|
|
450
|
-
* stack B's.
|
|
451
|
-
*/
|
|
452
|
-
function scopedKey(id, stackName) {
|
|
453
|
-
return stackName ? `${stackName}:${id}` : id;
|
|
454
|
-
}
|
|
455
|
-
var LiveRenderer = class {
|
|
456
|
-
tasks = /* @__PURE__ */ new Map();
|
|
457
|
-
active = false;
|
|
458
|
-
spinnerIndex = 0;
|
|
459
|
-
interval = null;
|
|
460
|
-
linesDrawn = 0;
|
|
461
|
-
cursorHidden = false;
|
|
462
|
-
exitListener = null;
|
|
463
|
-
stream;
|
|
464
|
-
constructor(stream = process.stdout) {
|
|
465
|
-
this.stream = stream;
|
|
466
|
-
}
|
|
467
|
-
isActive() {
|
|
468
|
-
return this.active;
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Enable the live renderer. No-op if stdout is not a TTY or if
|
|
472
|
-
* `CDKD_NO_LIVE=1`. Returns true if successfully enabled.
|
|
473
|
-
*/
|
|
474
|
-
start() {
|
|
475
|
-
if (this.active) return true;
|
|
476
|
-
if (!this.stream.isTTY) return false;
|
|
477
|
-
if (process.env["CDKD_NO_LIVE"] === "1") return false;
|
|
478
|
-
this.active = true;
|
|
479
|
-
this.hideCursor();
|
|
480
|
-
if (!this.exitListener) {
|
|
481
|
-
this.exitListener = () => this.showCursor();
|
|
482
|
-
process.on("exit", this.exitListener);
|
|
483
|
-
}
|
|
484
|
-
this.interval = setInterval(() => this.draw(), FRAME_INTERVAL_MS);
|
|
485
|
-
if (typeof this.interval.unref === "function") this.interval.unref();
|
|
486
|
-
return true;
|
|
487
|
-
}
|
|
488
|
-
stop() {
|
|
489
|
-
if (!this.active) return;
|
|
490
|
-
if (this.interval) {
|
|
491
|
-
clearInterval(this.interval);
|
|
492
|
-
this.interval = null;
|
|
493
|
-
}
|
|
494
|
-
this.clear();
|
|
495
|
-
this.showCursor();
|
|
496
|
-
if (this.exitListener) {
|
|
497
|
-
process.removeListener("exit", this.exitListener);
|
|
498
|
-
this.exitListener = null;
|
|
499
|
-
}
|
|
500
|
-
this.tasks.clear();
|
|
501
|
-
this.active = false;
|
|
502
|
-
}
|
|
503
|
-
addTask(id, label) {
|
|
504
|
-
const stackName = getCurrentStackName();
|
|
505
|
-
this.tasks.set(scopedKey(id, stackName), {
|
|
506
|
-
label,
|
|
507
|
-
startedAt: Date.now(),
|
|
508
|
-
stackName
|
|
509
|
-
});
|
|
510
|
-
if (this.active) this.draw();
|
|
511
|
-
}
|
|
512
|
-
removeTask(id) {
|
|
513
|
-
const stackName = getCurrentStackName();
|
|
514
|
-
if (!this.tasks.delete(scopedKey(id, stackName))) return;
|
|
515
|
-
if (this.active) this.draw();
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Replace the label of a previously-added task in place. No-op if the
|
|
519
|
-
* task is not currently tracked (e.g. it already finished). Used by the
|
|
520
|
-
* per-resource deadline wrapper to surface a "[taking longer than
|
|
521
|
-
* expected, Nm+]" suffix without disturbing the elapsed-time counter
|
|
522
|
-
* the renderer tracks via `startedAt`.
|
|
523
|
-
*/
|
|
524
|
-
updateTaskLabel(id, label) {
|
|
525
|
-
const stackName = getCurrentStackName();
|
|
526
|
-
const task = this.tasks.get(scopedKey(id, stackName));
|
|
527
|
-
if (!task) return;
|
|
528
|
-
task.label = label;
|
|
529
|
-
if (this.active) this.draw();
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Print content above the live area. Clears the live area, runs the writer,
|
|
533
|
-
* then redraws the live area. When the renderer is inactive, the writer
|
|
534
|
-
* runs directly so callers can use this unconditionally.
|
|
535
|
-
*/
|
|
536
|
-
printAbove(write) {
|
|
537
|
-
if (!this.active) {
|
|
538
|
-
write();
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
this.clear();
|
|
542
|
-
write();
|
|
543
|
-
this.draw();
|
|
544
|
-
}
|
|
545
|
-
clear() {
|
|
546
|
-
if (this.linesDrawn === 0) return;
|
|
547
|
-
this.stream.write("\r");
|
|
548
|
-
for (let i = 0; i < this.linesDrawn; i++) this.stream.write(`${ESC}1A${ESC}2K`);
|
|
549
|
-
this.linesDrawn = 0;
|
|
550
|
-
}
|
|
551
|
-
draw() {
|
|
552
|
-
if (!this.active) return;
|
|
553
|
-
this.clear();
|
|
554
|
-
if (this.tasks.size === 0) return;
|
|
555
|
-
const frame = SPINNER_FRAMES[this.spinnerIndex % SPINNER_FRAMES.length];
|
|
556
|
-
this.spinnerIndex++;
|
|
557
|
-
const distinctStacks = /* @__PURE__ */ new Set();
|
|
558
|
-
for (const task of this.tasks.values()) distinctStacks.add(task.stackName);
|
|
559
|
-
const showStackPrefix = distinctStacks.size > 1;
|
|
560
|
-
const cols = this.stream.columns ?? 80;
|
|
561
|
-
const lines = [];
|
|
562
|
-
for (const task of this.tasks.values()) {
|
|
563
|
-
const elapsed = ((Date.now() - task.startedAt) / 1e3).toFixed(1);
|
|
564
|
-
const raw = ` ${frame} ${showStackPrefix && task.stackName ? `[${task.stackName}] ` : ""}${task.label} (${elapsed}s)`;
|
|
565
|
-
lines.push(this.truncate(raw, cols));
|
|
566
|
-
}
|
|
567
|
-
this.stream.write(lines.join("\n") + "\n");
|
|
568
|
-
this.linesDrawn = lines.length;
|
|
569
|
-
}
|
|
570
|
-
truncate(s, maxLen) {
|
|
571
|
-
if (s.length <= maxLen) return s;
|
|
572
|
-
if (maxLen <= 1) return "…";
|
|
573
|
-
return s.substring(0, maxLen - 1) + "…";
|
|
574
|
-
}
|
|
575
|
-
hideCursor() {
|
|
576
|
-
if (this.cursorHidden) return;
|
|
577
|
-
this.stream.write(`${ESC}?25l`);
|
|
578
|
-
this.cursorHidden = true;
|
|
579
|
-
}
|
|
580
|
-
showCursor() {
|
|
581
|
-
if (!this.cursorHidden) return;
|
|
582
|
-
this.stream.write(`${ESC}?25h`);
|
|
583
|
-
this.cursorHidden = false;
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
let globalRenderer = null;
|
|
587
|
-
function getLiveRenderer() {
|
|
588
|
-
if (!globalRenderer) globalRenderer = new LiveRenderer();
|
|
589
|
-
return globalRenderer;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
//#endregion
|
|
593
|
-
//#region src/utils/stack-context.ts
|
|
594
|
-
const outputBufferStore = new AsyncLocalStorage();
|
|
595
|
-
/**
|
|
596
|
-
* Run `fn` with a fresh log buffer scoped to its async chain. Any
|
|
597
|
-
* `logger.info / debug / warn / error` calls inside `fn` (and any
|
|
598
|
-
* `await`s) push into the buffer instead of writing to stdout/stderr.
|
|
599
|
-
* Returns the buffered lines (and either `result` or `error`) so the
|
|
600
|
-
* caller can flush them in one block.
|
|
601
|
-
*/
|
|
602
|
-
async function runStackBuffered(fn) {
|
|
603
|
-
const buffer = { lines: [] };
|
|
604
|
-
return outputBufferStore.run(buffer, async () => {
|
|
605
|
-
try {
|
|
606
|
-
return {
|
|
607
|
-
ok: true,
|
|
608
|
-
result: await fn(),
|
|
609
|
-
lines: buffer.lines
|
|
610
|
-
};
|
|
611
|
-
} catch (error) {
|
|
612
|
-
return {
|
|
613
|
-
ok: false,
|
|
614
|
-
error,
|
|
615
|
-
lines: buffer.lines
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Get the current async context's stack output buffer, or `undefined`
|
|
622
|
-
* if no `runStackBuffered` is active. The logger consults this on every
|
|
623
|
-
* call: present → push to buffer; absent → fall through to live
|
|
624
|
-
* renderer / console.
|
|
625
|
-
*/
|
|
626
|
-
function getCurrentStackOutputBuffer() {
|
|
627
|
-
return outputBufferStore.getStore();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
//#endregion
|
|
631
|
-
//#region src/utils/logger.ts
|
|
632
|
-
/**
|
|
633
|
-
* ANSI color codes
|
|
634
|
-
*
|
|
635
|
-
* Kept internal — `ConsoleLogger.formatMessage` references these for the
|
|
636
|
-
* verbose/compact mode level prefixes. For inline color wrapping in
|
|
637
|
-
* production code, import from `./colors.js` instead (which lives in a
|
|
638
|
-
* separate module so unit tests that mock `logger.ts` don't strip color
|
|
639
|
-
* helpers as a side effect).
|
|
640
|
-
*/
|
|
641
|
-
const colors = {
|
|
642
|
-
reset: "\x1B[0m",
|
|
643
|
-
bright: "\x1B[1m",
|
|
644
|
-
dim: "\x1B[2m",
|
|
645
|
-
red: "\x1B[31m",
|
|
646
|
-
green: "\x1B[32m",
|
|
647
|
-
yellow: "\x1B[33m",
|
|
648
|
-
blue: "\x1B[34m",
|
|
649
|
-
cyan: "\x1B[36m",
|
|
650
|
-
gray: "\x1B[90m"
|
|
651
|
-
};
|
|
652
|
-
/**
|
|
653
|
-
* Format timestamp
|
|
654
|
-
*/
|
|
655
|
-
function formatTimestamp() {
|
|
656
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* Console logger implementation
|
|
660
|
-
*
|
|
661
|
-
* Supports two output modes:
|
|
662
|
-
* - verbose (debug level): timestamps, module prefixes, all details
|
|
663
|
-
* - compact (info level): clean output without timestamps or prefixes
|
|
664
|
-
*/
|
|
665
|
-
var ConsoleLogger = class {
|
|
666
|
-
level;
|
|
667
|
-
useColors;
|
|
668
|
-
constructor(level = "info", useColors = true) {
|
|
669
|
-
this.level = level;
|
|
670
|
-
this.useColors = useColors;
|
|
671
|
-
}
|
|
672
|
-
shouldLog(level) {
|
|
673
|
-
const levels = [
|
|
674
|
-
"debug",
|
|
675
|
-
"info",
|
|
676
|
-
"warn",
|
|
677
|
-
"error"
|
|
678
|
-
];
|
|
679
|
-
const currentLevelIndex = levels.indexOf(this.level);
|
|
680
|
-
return levels.indexOf(level) >= currentLevelIndex;
|
|
681
|
-
}
|
|
682
|
-
formatMessage(level, message, ...args) {
|
|
683
|
-
const formattedArgs = args.length > 0 ? " " + args.map((a) => JSON.stringify(a)).join(" ") : "";
|
|
684
|
-
if (this.level === "debug") {
|
|
685
|
-
const timestamp = formatTimestamp();
|
|
686
|
-
const levelStr = level.toUpperCase().padEnd(5);
|
|
687
|
-
if (this.useColors) {
|
|
688
|
-
const levelColor = {
|
|
689
|
-
debug: colors.gray,
|
|
690
|
-
info: colors.blue,
|
|
691
|
-
warn: colors.yellow,
|
|
692
|
-
error: colors.red
|
|
693
|
-
}[level];
|
|
694
|
-
return `${colors.dim}${timestamp}${colors.reset} ${levelColor}${levelStr}${colors.reset} ${message}${formattedArgs}`;
|
|
695
|
-
}
|
|
696
|
-
return `${timestamp} ${levelStr} ${message}${formattedArgs}`;
|
|
697
|
-
}
|
|
698
|
-
if (this.useColors) {
|
|
699
|
-
if (level === "error") return `${colors.red}${message}${formattedArgs}${colors.reset}`;
|
|
700
|
-
if (level === "warn") return `${colors.yellow}${message}${formattedArgs}${colors.reset}`;
|
|
701
|
-
return `${message}${formattedArgs}`;
|
|
702
|
-
}
|
|
703
|
-
return `${message}${formattedArgs}`;
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Route a formatted log line. When a per-stack output buffer is active in
|
|
707
|
-
* the current async context (parallel multi-stack deploy), capture the
|
|
708
|
-
* line into the buffer so it can be flushed as one atomic block when the
|
|
709
|
-
* stack finishes. Otherwise fall through to the live renderer / console
|
|
710
|
-
* as before.
|
|
711
|
-
*/
|
|
712
|
-
emit(level, formatted) {
|
|
713
|
-
const buffer = getCurrentStackOutputBuffer();
|
|
714
|
-
if (buffer) {
|
|
715
|
-
buffer.lines.push(formatted);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
getLiveRenderer().printAbove(() => {
|
|
719
|
-
if (level === "error") console.error(formatted);
|
|
720
|
-
else if (level === "warn") console.warn(formatted);
|
|
721
|
-
else if (level === "info") console.info(formatted);
|
|
722
|
-
else console.debug(formatted);
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
debug(message, ...args) {
|
|
726
|
-
if (this.shouldLog("debug")) this.emit("debug", this.formatMessage("debug", message, ...args));
|
|
727
|
-
}
|
|
728
|
-
info(message, ...args) {
|
|
729
|
-
if (this.shouldLog("info")) this.emit("info", this.formatMessage("info", message, ...args));
|
|
730
|
-
}
|
|
731
|
-
warn(message, ...args) {
|
|
732
|
-
if (this.shouldLog("warn")) this.emit("warn", this.formatMessage("warn", message, ...args));
|
|
733
|
-
}
|
|
734
|
-
error(message, ...args) {
|
|
735
|
-
if (this.shouldLog("error")) this.emit("error", this.formatMessage("error", message, ...args));
|
|
736
|
-
}
|
|
737
|
-
/**
|
|
738
|
-
* Set log level
|
|
739
|
-
*/
|
|
740
|
-
setLevel(level) {
|
|
741
|
-
this.level = level;
|
|
742
|
-
}
|
|
743
|
-
getLevel() {
|
|
744
|
-
return this.level;
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Create a child logger with a prefix
|
|
748
|
-
*
|
|
749
|
-
* In verbose mode, prefix is shown as [Prefix]. In compact mode, prefix is hidden.
|
|
750
|
-
*/
|
|
751
|
-
child(prefix) {
|
|
752
|
-
return new ChildLogger(prefix, this.useColors);
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
/**
|
|
756
|
-
* Child logger that always syncs level from global logger
|
|
757
|
-
*/
|
|
758
|
-
var ChildLogger = class extends ConsoleLogger {
|
|
759
|
-
prefix;
|
|
760
|
-
constructor(prefix, useColors) {
|
|
761
|
-
super("info", useColors);
|
|
762
|
-
this.prefix = prefix;
|
|
763
|
-
}
|
|
764
|
-
syncLevel() {
|
|
765
|
-
if (globalLogger) this.setLevel(globalLogger.getLevel());
|
|
766
|
-
}
|
|
767
|
-
debug(message, ...args) {
|
|
768
|
-
this.syncLevel();
|
|
769
|
-
super.debug(`[${this.prefix}] ${message}`, ...args);
|
|
770
|
-
}
|
|
771
|
-
info(message, ...args) {
|
|
772
|
-
this.syncLevel();
|
|
773
|
-
const msg = this.getLevel() === "debug" ? `[${this.prefix}] ${message}` : message;
|
|
774
|
-
super.info(msg, ...args);
|
|
775
|
-
}
|
|
776
|
-
warn(message, ...args) {
|
|
777
|
-
this.syncLevel();
|
|
778
|
-
const msg = this.getLevel() === "debug" ? `[${this.prefix}] ${message}` : message;
|
|
779
|
-
super.warn(msg, ...args);
|
|
780
|
-
}
|
|
781
|
-
error(message, ...args) {
|
|
782
|
-
this.syncLevel();
|
|
783
|
-
const msg = this.getLevel() === "debug" ? `[${this.prefix}] ${message}` : message;
|
|
784
|
-
super.error(msg, ...args);
|
|
785
|
-
}
|
|
786
|
-
};
|
|
787
|
-
/**
|
|
788
|
-
* Global logger instance
|
|
789
|
-
*/
|
|
790
|
-
let globalLogger = null;
|
|
791
|
-
/**
|
|
792
|
-
* Get or create global logger
|
|
793
|
-
*/
|
|
794
|
-
function getLogger() {
|
|
795
|
-
if (!globalLogger) globalLogger = new ConsoleLogger();
|
|
796
|
-
return globalLogger;
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* Set global logger instance
|
|
800
|
-
*/
|
|
801
|
-
function setLogger(logger) {
|
|
802
|
-
globalLogger = logger;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
//#endregion
|
|
806
|
-
//#region src/utils/docker-cmd.ts
|
|
807
|
-
var docker_cmd_exports = /* @__PURE__ */ __exportAll({
|
|
808
|
-
formatDockerLoginError: () => formatDockerLoginError,
|
|
809
|
-
getDockerCmd: () => getDockerCmd,
|
|
810
|
-
runDockerForeground: () => runDockerForeground,
|
|
811
|
-
runDockerStreaming: () => runDockerStreaming,
|
|
812
|
-
spawnForeground: () => spawnForeground,
|
|
813
|
-
spawnStreaming: () => spawnStreaming
|
|
814
|
-
});
|
|
815
|
-
/**
|
|
816
|
-
* Shared helpers for invoking the docker-compatible CLI binary across cdkd.
|
|
817
|
-
*
|
|
818
|
-
* Two parity decisions with `aws-cdk-cli`'s `cdk-assets-lib`:
|
|
819
|
-
* 1. `CDK_DOCKER` env var swaps the binary so podman / finch users can
|
|
820
|
-
* run cdkd without code changes (`CDK_DOCKER=podman cdkd deploy`).
|
|
821
|
-
* 2. `runDockerStreaming` uses streaming spawn rather than `execFile`'s
|
|
822
|
-
* buffered `maxBuffer` ceiling. BuildKit's progress output can run to
|
|
823
|
-
* tens of MB on multi-stage builds with `# syntax=docker/dockerfile:1`
|
|
824
|
-
* frontend downloads + heredoc / `RUN --mount=...` features; the 50 MB
|
|
825
|
-
* `execFile` ceiling cdkd used to set silently killed those builds
|
|
826
|
-
* with `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`.
|
|
827
|
-
*
|
|
828
|
-
* Output handling: stdout/stderr are collected in memory unconditionally so
|
|
829
|
-
* `runDockerStreaming` can return them to the caller for error wrapping.
|
|
830
|
-
* When the logger is at debug level (i.e. the user passed `--verbose`),
|
|
831
|
-
* the chunks are ALSO mirrored to `process.stdout` / `process.stderr` so
|
|
832
|
-
* the user sees live build progress.
|
|
833
|
-
*/
|
|
834
|
-
/**
|
|
835
|
-
* Return the docker-compatible CLI binary to invoke. Matches CDK CLI:
|
|
836
|
-
* `CDK_DOCKER` env var overrides the default `docker` so users on
|
|
837
|
-
* podman / finch / nerdctl can swap without changing cdkd code.
|
|
838
|
-
*/
|
|
839
|
-
function getDockerCmd() {
|
|
840
|
-
const override = process.env["CDK_DOCKER"];
|
|
841
|
-
return override && override.length > 0 ? override : "docker";
|
|
842
|
-
}
|
|
843
|
-
/**
|
|
844
|
-
* Spawn a docker-compatible CLI binary (resolved via `getDockerCmd`) with
|
|
845
|
-
* streaming I/O. Collects stdout/stderr in memory and resolves with both
|
|
846
|
-
* on exit code 0; rejects with a `SpawnError` carrying both streams on any
|
|
847
|
-
* non-zero exit so the caller can wrap with its own error class without
|
|
848
|
-
* losing the upstream output.
|
|
849
|
-
*
|
|
850
|
-
* No `maxBuffer` ceiling: BuildKit progress output frequently exceeds the
|
|
851
|
-
* `child_process.execFile` default of 1 MB (cdkd previously bumped to 50 MB
|
|
852
|
-
* but BuildKit + frontend pulls can still exceed that on first-time builds).
|
|
853
|
-
*/
|
|
854
|
-
async function runDockerStreaming(args, options = {}) {
|
|
855
|
-
return spawnStreaming(getDockerCmd(), args, options);
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Generic streaming spawn — used by `runDockerStreaming` AND by the
|
|
859
|
-
* `executable` source mode in `docker-build.ts` (which runs an arbitrary
|
|
860
|
-
* user-supplied build command, not docker).
|
|
861
|
-
*/
|
|
862
|
-
async function spawnStreaming(cmd, args, options = {}) {
|
|
863
|
-
const streamLive = options.streamLive ?? getLogger().getLevel() === "debug";
|
|
864
|
-
const env = options.env ? mergeEnv(options.env) : void 0;
|
|
865
|
-
return new Promise((resolve, reject) => {
|
|
866
|
-
const child = spawn(cmd, args, {
|
|
867
|
-
cwd: options.cwd,
|
|
868
|
-
env,
|
|
869
|
-
stdio: [
|
|
870
|
-
options.input ? "pipe" : "ignore",
|
|
871
|
-
"pipe",
|
|
872
|
-
"pipe"
|
|
873
|
-
]
|
|
874
|
-
});
|
|
875
|
-
const stdoutChunks = [];
|
|
876
|
-
const stderrChunks = [];
|
|
877
|
-
child.stdout.on("data", (chunk) => {
|
|
878
|
-
stdoutChunks.push(chunk);
|
|
879
|
-
if (streamLive) process.stdout.write(chunk);
|
|
880
|
-
});
|
|
881
|
-
child.stderr.on("data", (chunk) => {
|
|
882
|
-
stderrChunks.push(chunk);
|
|
883
|
-
if (streamLive) process.stderr.write(chunk);
|
|
884
|
-
});
|
|
885
|
-
child.once("error", (err) => {
|
|
886
|
-
if (err.code === "ENOENT") {
|
|
887
|
-
const usingOverride = process.env["CDK_DOCKER"] === cmd && cmd !== "docker";
|
|
888
|
-
reject(/* @__PURE__ */ new Error(usingOverride ? `Failed to find and execute '${cmd}' (resolved via CDK_DOCKER). Install '${cmd}' or unset CDK_DOCKER to fall back to 'docker'.` : `Failed to find and execute '${cmd}'. Install Docker (or set the 'CDK_DOCKER' environment variable to a compatible binary such as podman / finch).`));
|
|
889
|
-
} else reject(err);
|
|
890
|
-
});
|
|
891
|
-
child.once("close", (code) => {
|
|
892
|
-
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
893
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
894
|
-
if (code === 0) resolve({
|
|
895
|
-
stdout,
|
|
896
|
-
stderr
|
|
897
|
-
});
|
|
898
|
-
else {
|
|
899
|
-
const message = stderr.trim() || stdout.trim() || `${cmd} ${args[0] ?? ""} exited with code ${code}`;
|
|
900
|
-
const err = new Error(message);
|
|
901
|
-
err.stderr = stderr;
|
|
902
|
-
err.stdout = stdout;
|
|
903
|
-
err.exitCode = code;
|
|
904
|
-
reject(err);
|
|
905
|
-
}
|
|
906
|
-
});
|
|
907
|
-
if (options.input !== void 0) {
|
|
908
|
-
child.stdin.on("error", () => {});
|
|
909
|
-
child.stdin.write(options.input);
|
|
910
|
-
child.stdin.end();
|
|
911
|
-
}
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
/**
|
|
915
|
-
* Spawn a docker-compatible CLI binary (resolved via `getDockerCmd`) attached
|
|
916
|
-
* to the parent process's stdio so the user sees live output (`docker pull`
|
|
917
|
-
* layer progress, `docker login` interactive prompts that should never fire
|
|
918
|
-
* with `--password-stdin` but still safe to inherit, etc.). Resolves on exit
|
|
919
|
-
* code 0; rejects with a plain `Error` carrying the exit code on any non-zero
|
|
920
|
-
* exit, so the caller can wrap with its own error class.
|
|
921
|
-
*
|
|
922
|
-
* Differs from {@link runDockerStreaming} in two ways:
|
|
923
|
-
* 1. `stdio: 'inherit'` — output is NOT captured, so terminal control codes
|
|
924
|
-
* (color, progress bar overwrites) flow through unchanged. This is the
|
|
925
|
-
* load-bearing reason for the split: `docker pull`'s progress bars only
|
|
926
|
-
* animate properly when stdout is a real TTY connected to the parent.
|
|
927
|
-
* 2. No `input` / `streamLive` options — inherit-mode has nothing to
|
|
928
|
-
* capture and nothing to mirror.
|
|
929
|
-
*
|
|
930
|
-
* Used by the `--verbose`-mode `docker pull` plumbing in `docker-runner.ts`
|
|
931
|
-
* and `ecr-puller.ts` (visible layer progress). Non-verbose pulls go through
|
|
932
|
-
* {@link runDockerStreaming} so stderr can be folded into the error message.
|
|
933
|
-
*/
|
|
934
|
-
async function runDockerForeground(args, options = {}) {
|
|
935
|
-
return spawnForeground(getDockerCmd(), args, options);
|
|
936
|
-
}
|
|
937
|
-
/**
|
|
938
|
-
* Foreground (stdio-inherit) spawn — the inherit-mode counterpart to
|
|
939
|
-
* {@link spawnStreaming}. Used by {@link runDockerForeground} for docker-CLI
|
|
940
|
-
* subprocesses.
|
|
941
|
-
*
|
|
942
|
-
* The ENOENT branch crafts a docker-specific install hint ("Install Docker
|
|
943
|
-
* (or set CDK_DOCKER ...)"), so non-docker callers reusing this helper
|
|
944
|
-
* would see a misleading error on missing-binary failures. Keep the binary
|
|
945
|
-
* docker-shaped, or update the ENOENT message before adding a non-docker
|
|
946
|
-
* call site.
|
|
947
|
-
*/
|
|
948
|
-
async function spawnForeground(cmd, args, options = {}) {
|
|
949
|
-
const env = options.env ? mergeEnv(options.env) : void 0;
|
|
950
|
-
return new Promise((resolve, reject) => {
|
|
951
|
-
const child = spawn(cmd, args, {
|
|
952
|
-
cwd: options.cwd,
|
|
953
|
-
env,
|
|
954
|
-
stdio: "inherit"
|
|
955
|
-
});
|
|
956
|
-
child.once("error", (err) => {
|
|
957
|
-
if (err.code === "ENOENT") {
|
|
958
|
-
const usingOverride = process.env["CDK_DOCKER"] === cmd && cmd !== "docker";
|
|
959
|
-
reject(/* @__PURE__ */ new Error(usingOverride ? `Failed to find and execute '${cmd}' (resolved via CDK_DOCKER). Install '${cmd}' or unset CDK_DOCKER to fall back to 'docker'.` : `Failed to find and execute '${cmd}'. Install Docker (or set the 'CDK_DOCKER' environment variable to a compatible binary such as podman / finch).`));
|
|
960
|
-
} else reject(/* @__PURE__ */ new Error(`${cmd} failed: ${err.message}`));
|
|
961
|
-
});
|
|
962
|
-
child.once("close", (code) => {
|
|
963
|
-
if (code === 0) resolve();
|
|
964
|
-
else reject(/* @__PURE__ */ new Error(`${cmd} exited with code ${code}`));
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Format the stderr from a failed `docker login` so the surfaced cdkd
|
|
970
|
-
* error gives the user an actionable workaround when the underlying
|
|
971
|
-
* failure is a credential-helper persistence bug (which has nothing to
|
|
972
|
-
* do with cdkd, AWS, or IAM perms — the docker CLI itself fails to
|
|
973
|
-
* save the auth token to the platform's credential store). The most
|
|
974
|
-
* common shape is `osxkeychain` on macOS rejecting an overwrite for
|
|
975
|
-
* an existing entry, but `wincred` (Windows), `pass` (Linux), and
|
|
976
|
-
* `secretservice` (Linux) hit the same class of `Error saving
|
|
977
|
-
* credentials` failure, so the rewritten message stays platform-
|
|
978
|
-
* agnostic — `docker logout <endpoint>` is the correct recovery on
|
|
979
|
-
* every backend.
|
|
980
|
-
*
|
|
981
|
-
* Detected docker / docker-credential-* output patterns:
|
|
982
|
-
* - `error storing credentials - err: exit status 1, out: \`The
|
|
983
|
-
* specified item already exists in the keychain.\`` (osxkeychain)
|
|
984
|
-
* - `Error saving credentials: ...` (any backend)
|
|
985
|
-
*
|
|
986
|
-
* Non-matching failures (genuine IAM / network / endpoint problems)
|
|
987
|
-
* pass through with just the stderr trimmed — the original message
|
|
988
|
-
* stays load-bearing for diagnosis.
|
|
989
|
-
*/
|
|
990
|
-
function formatDockerLoginError(stderr, endpoint) {
|
|
991
|
-
const trimmed = stderr.trim();
|
|
992
|
-
if (trimmed.includes("already exists in the keychain") || trimmed.includes("Error saving credentials")) return `docker's credential helper (osxkeychain on macOS / wincred on Windows / pass / secretservice on Linux) failed to persist the ECR auth token. The "already exists in the keychain" / "Error saving credentials" output is a known docker-credential-helpers issue — unrelated to cdkd, AWS credentials, or IAM perms. Quick fix: run \`docker logout ${endpoint}\` to clear the stale entry, then retry the cdkd command. Permanent fix: edit ~/.docker/config.json and remove (or empty) the platform-specific "credsStore" entry (e.g. "osxkeychain" → "" or "desktop" on macOS Docker Desktop). Original docker stderr: ${trimmed}`;
|
|
993
|
-
return trimmed;
|
|
994
|
-
}
|
|
995
|
-
function mergeEnv(overrides) {
|
|
996
|
-
const merged = { ...process.env };
|
|
997
|
-
for (const [k, v] of Object.entries(overrides)) if (v === void 0) delete merged[k];
|
|
998
|
-
else merged[k] = v;
|
|
999
|
-
return merged;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
//#endregion
|
|
1003
|
-
export { withSkipPrefix as _, runDockerStreaming as a, getLogger as c, getLiveRenderer as d, PATTERN_B_NAME_PROPERTIES as f, generateResourceNameWithFallback as g, generateResourceName as h, runDockerForeground as i, setLogger as l, applyDefaultNameForFallback as m, formatDockerLoginError as n, spawnStreaming as o, PATTERN_B_RESOURCE_TYPES as p, getDockerCmd as r, ConsoleLogger as s, docker_cmd_exports as t, runStackBuffered as u, withStackName as v };
|
|
1004
|
-
//# sourceMappingURL=docker-cmd-iDMcWcre.js.map
|