@go-to-k/cdkd 0.84.0 → 0.86.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  - **Drop-in CDK compatible** — your existing CDK app code runs as-is.
6
6
  - **Up to 15x faster deploys than the AWS CDK CLI (CloudFormation)**
7
- - **Run AWS resources locally without deploying** — invoke Lambdas, serve API Gateway routes, and run ECS task definitions against Docker. SAM-compatible mental model, no `template.yaml` round-trip.
7
+ - **Run AWS resources locally without deploying** — invoke Lambdas, run ECS tasks, and serve API Gateway routes from Docker.
8
8
 
9
9
  ![cdkd demo](https://github.com/user-attachments/assets/0128730d-186d-4bd3-abea-aabc80ba4dd5)
10
10
 
package/dist/cli.js CHANGED
@@ -76798,6 +76798,13 @@ function inspectImageForSubstitutions(image, resources) {
76798
76798
  return { pseudo: false, state: true };
76799
76799
  }
76800
76800
  }
76801
+ const join11 = obj["Fn::Join"];
76802
+ if (Array.isArray(join11) && join11.length === 2 && Array.isArray(join11[1])) {
76803
+ const scan = { pseudo: false, state: false };
76804
+ inspectIntrinsicNeeds(join11[1], resources, scan);
76805
+ if (scan.pseudo || scan.state)
76806
+ return scan;
76807
+ }
76801
76808
  let sub;
76802
76809
  const subRaw = obj["Fn::Sub"];
76803
76810
  if (typeof subRaw === "string")
@@ -76823,6 +76830,42 @@ function inspectImageForSubstitutions(image, resources) {
76823
76830
  }
76824
76831
  return { pseudo, state };
76825
76832
  }
76833
+ function inspectIntrinsicNeeds(node, resources, scan) {
76834
+ if (node === null || node === void 0)
76835
+ return;
76836
+ if (typeof node === "string" || typeof node === "number" || typeof node === "boolean")
76837
+ return;
76838
+ if (Array.isArray(node)) {
76839
+ for (const item of node)
76840
+ inspectIntrinsicNeeds(item, resources, scan);
76841
+ return;
76842
+ }
76843
+ if (typeof node !== "object")
76844
+ return;
76845
+ const obj = node;
76846
+ if (typeof obj["Ref"] === "string") {
76847
+ const target = obj["Ref"];
76848
+ if (target.startsWith("AWS::"))
76849
+ scan.pseudo = true;
76850
+ else if (resources[target]?.Type === "AWS::ECR::Repository")
76851
+ scan.state = true;
76852
+ return;
76853
+ }
76854
+ const getAtt = obj["Fn::GetAtt"];
76855
+ if (getAtt !== void 0) {
76856
+ let lid;
76857
+ if (Array.isArray(getAtt) && typeof getAtt[0] === "string")
76858
+ lid = getAtt[0];
76859
+ else if (typeof getAtt === "string")
76860
+ lid = getAtt.split(".")[0];
76861
+ if (lid && resources[lid]?.Type === "AWS::ECR::Repository")
76862
+ scan.state = true;
76863
+ return;
76864
+ }
76865
+ for (const value of Object.values(obj)) {
76866
+ inspectIntrinsicNeeds(value, resources, scan);
76867
+ }
76868
+ }
76826
76869
  function parseEcsTarget(target) {
76827
76870
  if (typeof target !== "string" || target.length === 0) {
76828
76871
  throw new EcsTaskResolutionError(
@@ -77147,6 +77190,20 @@ function parseContainerImage(raw, containerName, taskLogicalId, resources, _stac
77147
77190
  if (getAttImage) {
77148
77191
  return classifyResolvedImage(getAttImage);
77149
77192
  }
77193
+ const joinResolved = tryResolveImageFnJoin(raw, resources, context);
77194
+ if (joinResolved.kind === "resolved") {
77195
+ return classifyResolvedImage(joinResolved.uri);
77196
+ }
77197
+ if (joinResolved.kind === "needs-state") {
77198
+ throw new EcsTaskResolutionError(
77199
+ `Container '${containerName}' in task '${taskLogicalId}' references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join. cdkd local run-task cannot resolve the repository URI without state \u2014 pass --from-state (the stack must have been deployed via cdkd deploy), build via ContainerImage.fromAsset, or pin a public image.`
77200
+ );
77201
+ }
77202
+ if (joinResolved.kind === "unsupported-join") {
77203
+ throw new EcsTaskResolutionError(
77204
+ `Container '${containerName}' in task '${taskLogicalId}' has an unsupported Fn::Join Image shape: ${joinResolved.reason}. cdkd local run-task recognizes the canonical CDK 2.x ContainerImage.fromEcrRepository Fn::Join shape (delimiter "" with nested Fn::Select/Fn::Split over an ECR Repository Arn GetAtt + Ref to the repo).`
77205
+ );
77206
+ }
77150
77207
  const flat = extractImageString(raw);
77151
77208
  if (!flat) {
77152
77209
  throw new EcsTaskResolutionError(
@@ -77298,6 +77355,228 @@ function extractImageString(value) {
77298
77355
  }
77299
77356
  return void 0;
77300
77357
  }
77358
+ function tryResolveImageFnJoin(raw, resources, context) {
77359
+ if (!raw || typeof raw !== "object")
77360
+ return { kind: "not-applicable" };
77361
+ const obj = raw;
77362
+ const arg = obj["Fn::Join"];
77363
+ if (arg === void 0)
77364
+ return { kind: "not-applicable" };
77365
+ if (!Array.isArray(arg) || arg.length !== 2 || !Array.isArray(arg[1])) {
77366
+ return { kind: "unsupported-join", reason: "Fn::Join must be [delimiter, [elements]]" };
77367
+ }
77368
+ const [delimiter, elements] = arg;
77369
+ if (typeof delimiter !== "string") {
77370
+ return {
77371
+ kind: "unsupported-join",
77372
+ reason: `Fn::Join delimiter must be a string, got ${typeof delimiter}`
77373
+ };
77374
+ }
77375
+ const repoLogicalId = findEcrRepositoryRefInTree(elements, resources);
77376
+ const stateResources = context?.stateResources;
77377
+ if (repoLogicalId && !stateResources) {
77378
+ return { kind: "needs-state", repoLogicalId };
77379
+ }
77380
+ const parts = [];
77381
+ for (const element of elements) {
77382
+ const r = resolveImageIntrinsic(element, resources, context);
77383
+ if (r === void 0) {
77384
+ if (!repoLogicalId)
77385
+ return { kind: "not-applicable" };
77386
+ return {
77387
+ kind: "unsupported-join",
77388
+ reason: "one or more Fn::Join elements could not be resolved"
77389
+ };
77390
+ }
77391
+ parts.push(r);
77392
+ }
77393
+ return { kind: "resolved", uri: parts.join(delimiter) };
77394
+ }
77395
+ function findEcrRepositoryRefInTree(node, resources) {
77396
+ if (node === null || node === void 0)
77397
+ return void 0;
77398
+ if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
77399
+ return void 0;
77400
+ }
77401
+ if (Array.isArray(node)) {
77402
+ for (const item of node) {
77403
+ const hit = findEcrRepositoryRefInTree(item, resources);
77404
+ if (hit)
77405
+ return hit;
77406
+ }
77407
+ return void 0;
77408
+ }
77409
+ if (typeof node !== "object")
77410
+ return void 0;
77411
+ const obj = node;
77412
+ if (typeof obj["Ref"] === "string") {
77413
+ const target = obj["Ref"];
77414
+ if (resources[target]?.Type === "AWS::ECR::Repository")
77415
+ return target;
77416
+ return void 0;
77417
+ }
77418
+ const getAtt = obj["Fn::GetAtt"];
77419
+ if (getAtt !== void 0) {
77420
+ let lid;
77421
+ if (Array.isArray(getAtt) && typeof getAtt[0] === "string")
77422
+ lid = getAtt[0];
77423
+ else if (typeof getAtt === "string")
77424
+ lid = getAtt.split(".")[0];
77425
+ if (lid && resources[lid]?.Type === "AWS::ECR::Repository")
77426
+ return lid;
77427
+ return void 0;
77428
+ }
77429
+ for (const value of Object.values(obj)) {
77430
+ const hit = findEcrRepositoryRefInTree(value, resources);
77431
+ if (hit)
77432
+ return hit;
77433
+ }
77434
+ return void 0;
77435
+ }
77436
+ function resolveImageIntrinsic(node, resources, context) {
77437
+ const v = resolveImageIntrinsicAny(node, resources, context);
77438
+ if (typeof v === "string")
77439
+ return v;
77440
+ if (typeof v === "number" || typeof v === "boolean")
77441
+ return String(v);
77442
+ return void 0;
77443
+ }
77444
+ function resolveImageIntrinsicAny(node, resources, context) {
77445
+ if (node === null || node === void 0)
77446
+ return void 0;
77447
+ if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
77448
+ return node;
77449
+ }
77450
+ if (Array.isArray(node)) {
77451
+ return void 0;
77452
+ }
77453
+ if (typeof node !== "object")
77454
+ return void 0;
77455
+ const obj = node;
77456
+ const keys = Object.keys(obj);
77457
+ if (keys.length !== 1)
77458
+ return void 0;
77459
+ const intrinsic = keys[0];
77460
+ const arg = obj[intrinsic];
77461
+ if (intrinsic === "Ref") {
77462
+ if (typeof arg !== "string")
77463
+ return void 0;
77464
+ if (arg.startsWith("AWS::")) {
77465
+ const p = context?.pseudoParameters;
77466
+ if (!p)
77467
+ return void 0;
77468
+ if (arg === "AWS::URLSuffix")
77469
+ return p.urlSuffix;
77470
+ if (arg === "AWS::Partition")
77471
+ return p.partition;
77472
+ if (arg === "AWS::Region")
77473
+ return p.region;
77474
+ if (arg === "AWS::AccountId")
77475
+ return p.accountId;
77476
+ return void 0;
77477
+ }
77478
+ const refResource = resources[arg];
77479
+ if (refResource?.Type !== "AWS::ECR::Repository")
77480
+ return void 0;
77481
+ const stateEntry = context?.stateResources?.[arg];
77482
+ if (!stateEntry)
77483
+ return void 0;
77484
+ return stateEntry.physicalId;
77485
+ }
77486
+ if (intrinsic === "Fn::GetAtt") {
77487
+ let logicalId;
77488
+ let attr;
77489
+ if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "string") {
77490
+ logicalId = arg[0];
77491
+ attr = arg[1];
77492
+ } else if (typeof arg === "string") {
77493
+ const dot = arg.indexOf(".");
77494
+ if (dot > 0 && dot < arg.length - 1) {
77495
+ logicalId = arg.slice(0, dot);
77496
+ attr = arg.slice(dot + 1);
77497
+ }
77498
+ }
77499
+ if (!logicalId || !attr)
77500
+ return void 0;
77501
+ if (resources[logicalId]?.Type !== "AWS::ECR::Repository")
77502
+ return void 0;
77503
+ const cached = context?.stateResources?.[logicalId]?.attributes?.[attr];
77504
+ if (typeof cached === "string" && cached.length > 0)
77505
+ return cached;
77506
+ return void 0;
77507
+ }
77508
+ if (intrinsic === "Fn::Split") {
77509
+ if (!Array.isArray(arg) || arg.length !== 2)
77510
+ return void 0;
77511
+ const argArr = arg;
77512
+ const delim = argArr[0];
77513
+ if (typeof delim !== "string")
77514
+ return void 0;
77515
+ const src = resolveImageIntrinsicAny(argArr[1], resources, context);
77516
+ if (typeof src !== "string")
77517
+ return void 0;
77518
+ return src.split(delim);
77519
+ }
77520
+ if (intrinsic === "Fn::Select") {
77521
+ if (!Array.isArray(arg) || arg.length !== 2)
77522
+ return void 0;
77523
+ const argArr = arg;
77524
+ const rawIndex = argArr[0];
77525
+ let index;
77526
+ if (typeof rawIndex === "number") {
77527
+ index = rawIndex;
77528
+ } else if (typeof rawIndex === "string" && /^-?\d+$/.test(rawIndex)) {
77529
+ index = Number.parseInt(rawIndex, 10);
77530
+ }
77531
+ if (index === void 0 || !Number.isFinite(index))
77532
+ return void 0;
77533
+ const list = resolveImageIntrinsicAny(argArr[1], resources, context);
77534
+ if (Array.isArray(list)) {
77535
+ if (index < 0 || index >= list.length)
77536
+ return void 0;
77537
+ const picked = list[index];
77538
+ if (typeof picked === "string")
77539
+ return picked;
77540
+ return void 0;
77541
+ }
77542
+ if (Array.isArray(argArr[1])) {
77543
+ const listLiteral = argArr[1];
77544
+ if (index < 0 || index >= listLiteral.length)
77545
+ return void 0;
77546
+ return resolveImageIntrinsic(listLiteral[index], resources, context);
77547
+ }
77548
+ return void 0;
77549
+ }
77550
+ if (intrinsic === "Fn::Join") {
77551
+ if (!Array.isArray(arg) || arg.length !== 2)
77552
+ return void 0;
77553
+ const [delim, parts] = arg;
77554
+ if (typeof delim !== "string" || !Array.isArray(parts))
77555
+ return void 0;
77556
+ const resolved = [];
77557
+ for (const part of parts) {
77558
+ const r = resolveImageIntrinsic(part, resources, context);
77559
+ if (r === void 0)
77560
+ return void 0;
77561
+ resolved.push(r);
77562
+ }
77563
+ return resolved.join(delim);
77564
+ }
77565
+ if (intrinsic === "Fn::Sub") {
77566
+ let template;
77567
+ if (typeof arg === "string")
77568
+ template = arg;
77569
+ else if (Array.isArray(arg) && typeof arg[0] === "string")
77570
+ template = arg[0];
77571
+ if (template === void 0)
77572
+ return void 0;
77573
+ const out = substituteImagePlaceholders(template, resources, context);
77574
+ if (out.includes("${"))
77575
+ return void 0;
77576
+ return out;
77577
+ }
77578
+ return void 0;
77579
+ }
77301
77580
  function parseVolume(raw, idx, taskLogicalId) {
77302
77581
  if (!raw || typeof raw !== "object") {
77303
77582
  throw new EcsTaskResolutionError(`Task '${taskLogicalId}' Volumes[${idx}] is not an object.`);
@@ -79038,6 +79317,38 @@ var PRIMARY_IDENTIFIER_FALLBACK = {
79038
79317
  "AWS::Cognito::UserPool": "UserPoolId",
79039
79318
  "AWS::ECR::Repository": "RepositoryName"
79040
79319
  };
79320
+ var COMPOSITE_ID_SPLITTERS = {
79321
+ // cdkd stores `restApiId|resourceId|httpMethod` (apigateway-provider.ts);
79322
+ // CFn primary identifier is [RestApiId, ResourceId, HttpMethod] — same order.
79323
+ "AWS::ApiGateway::Method": (id) => {
79324
+ const parts = id.split("|");
79325
+ if (parts.length !== 3) {
79326
+ throw new Error(
79327
+ `expected 3 parts (restApiId|resourceId|httpMethod), got ${parts.length}: '${id}'`
79328
+ );
79329
+ }
79330
+ return { RestApiId: parts[0], ResourceId: parts[1], HttpMethod: parts[2] };
79331
+ },
79332
+ // cdkd stores `restApiId|resourceId` (apigateway-provider.ts);
79333
+ // CFn primary identifier is [RestApiId, ResourceId].
79334
+ "AWS::ApiGateway::Resource": (id) => {
79335
+ const parts = id.split("|");
79336
+ if (parts.length !== 2) {
79337
+ throw new Error(`expected 2 parts (restApiId|resourceId), got ${parts.length}: '${id}'`);
79338
+ }
79339
+ return { RestApiId: parts[0], ResourceId: parts[1] };
79340
+ },
79341
+ // cdkd stores `IGW|VpcId` (ec2-provider.ts);
79342
+ // CFn primary identifier is [VpcId, InternetGatewayId] — DIFFERENT order
79343
+ // from cdkd. Splitter reorders explicitly.
79344
+ "AWS::EC2::VPCGatewayAttachment": (id) => {
79345
+ const parts = id.split("|");
79346
+ if (parts.length !== 2) {
79347
+ throw new Error(`expected 2 parts (IGW|VpcId), got ${parts.length}: '${id}'`);
79348
+ }
79349
+ return { VpcId: parts[1], InternetGatewayId: parts[0] };
79350
+ }
79351
+ };
79041
79352
  async function exportCommand(stackArg, options) {
79042
79353
  const logger = getLogger();
79043
79354
  if (options.verbose) {
@@ -79308,14 +79619,19 @@ async function buildImportPlan(state, template, cfnClient) {
79308
79619
  });
79309
79620
  continue;
79310
79621
  }
79311
- let identifierKey;
79622
+ let resourceIdentifier;
79312
79623
  try {
79313
- identifierKey = await resolvePrimaryIdentifier(resourceType, cfnClient, identifierCache);
79624
+ resourceIdentifier = await resolveResourceIdentifier(
79625
+ resourceType,
79626
+ stateEntry.physicalId,
79627
+ cfnClient,
79628
+ identifierCache
79629
+ );
79314
79630
  } catch (err) {
79315
79631
  skipped.push({
79316
79632
  logicalId,
79317
79633
  resourceType,
79318
- reason: "could not resolve primary identifier: " + (err instanceof Error ? err.message : String(err))
79634
+ reason: "could not resolve resource identifier: " + (err instanceof Error ? err.message : String(err))
79319
79635
  });
79320
79636
  continue;
79321
79637
  }
@@ -79323,15 +79639,44 @@ async function buildImportPlan(state, template, cfnClient) {
79323
79639
  logicalId,
79324
79640
  resourceType,
79325
79641
  physicalId: stateEntry.physicalId,
79326
- identifierKey
79642
+ resourceIdentifier
79327
79643
  });
79328
79644
  }
79329
79645
  return { plan, skipped };
79330
79646
  }
79331
- async function resolvePrimaryIdentifier(resourceType, cfnClient, cache2) {
79332
- const cached = cache2.get(resourceType);
79333
- if (cached !== void 0)
79334
- return cached;
79647
+ async function resolveResourceIdentifier(resourceType, physicalId, cfnClient, cache2) {
79648
+ let entry = cache2.get(resourceType);
79649
+ if (entry === void 0) {
79650
+ entry = await fetchPrimaryIdentifier(resourceType, cfnClient);
79651
+ cache2.set(resourceType, entry);
79652
+ }
79653
+ if (entry.fields.length === 1) {
79654
+ return { [entry.fields[0]]: physicalId };
79655
+ }
79656
+ const splitter = COMPOSITE_ID_SPLITTERS[resourceType];
79657
+ if (!splitter) {
79658
+ throw new Error(
79659
+ `resource type uses a composite primary identifier (${entry.fields.length} fields: ${entry.fields.join(", ")}); add an entry to COMPOSITE_ID_SPLITTERS in src/cli/commands/export.ts that parses cdkd's physicalId for this type, or destroy the resource first and let CFn create it fresh`
79660
+ );
79661
+ }
79662
+ let split;
79663
+ try {
79664
+ split = splitter(physicalId);
79665
+ } catch (err) {
79666
+ throw new Error(
79667
+ `composite-id splitter for ${resourceType} failed: ` + (err instanceof Error ? err.message : String(err))
79668
+ );
79669
+ }
79670
+ for (const f of entry.fields) {
79671
+ if (!(f in split)) {
79672
+ throw new Error(
79673
+ `composite-id splitter for ${resourceType} did not produce field '${f}' (produced: ${Object.keys(split).join(", ")})`
79674
+ );
79675
+ }
79676
+ }
79677
+ return split;
79678
+ }
79679
+ async function fetchPrimaryIdentifier(resourceType, cfnClient) {
79335
79680
  try {
79336
79681
  const resp = await cfnClient.send(
79337
79682
  new DescribeTypeCommand({ Type: "RESOURCE", TypeName: resourceType })
@@ -79339,15 +79684,9 @@ async function resolvePrimaryIdentifier(resourceType, cfnClient, cache2) {
79339
79684
  if (resp.Schema) {
79340
79685
  const parsed = JSON.parse(resp.Schema);
79341
79686
  const primary = parsed.primaryIdentifier;
79342
- if (Array.isArray(primary) && primary.length === 1 && typeof primary[0] === "string") {
79343
- const propName = primary[0].replace(/^\/properties\//, "");
79344
- cache2.set(resourceType, propName);
79345
- return propName;
79346
- }
79347
- if (Array.isArray(primary) && primary.length > 1) {
79348
- throw new Error(
79349
- `resource type uses a composite primary identifier (${primary.length} fields); cdkd does not yet support composite identifiers for cdkd export`
79350
- );
79687
+ if (Array.isArray(primary) && primary.length > 0 && primary.every((p) => typeof p === "string")) {
79688
+ const fields = primary.map((p) => p.replace(/^\/properties\//, ""));
79689
+ return { fields };
79351
79690
  }
79352
79691
  }
79353
79692
  } catch (err) {
@@ -79356,8 +79695,7 @@ async function resolvePrimaryIdentifier(resourceType, cfnClient, cache2) {
79356
79695
  }
79357
79696
  const fallback = PRIMARY_IDENTIFIER_FALLBACK[resourceType];
79358
79697
  if (fallback) {
79359
- cache2.set(resourceType, fallback);
79360
- return fallback;
79698
+ return { fields: [fallback] };
79361
79699
  }
79362
79700
  throw new Error(
79363
79701
  `primary identifier unknown (DescribeType returned no usable schema and no fallback is registered). Add ${resourceType} to PRIMARY_IDENTIFIER_FALLBACK in export.ts, or open an issue.`
@@ -79417,9 +79755,8 @@ function printPlan(plan, cfnStackName) {
79417
79755
  logger.info("");
79418
79756
  logger.info(`Import plan for CloudFormation stack '${cfnStackName}':`);
79419
79757
  for (const entry of plan) {
79420
- logger.info(
79421
- ` ${entry.logicalId} (${entry.resourceType}) \u2190 ${entry.identifierKey}=${entry.physicalId}`
79422
- );
79758
+ const idStr = Object.entries(entry.resourceIdentifier).map(([k, v]) => `${k}=${v}`).join(", ");
79759
+ logger.info(` ${entry.logicalId} (${entry.resourceType}) \u2190 ${idStr}`);
79423
79760
  }
79424
79761
  logger.info("");
79425
79762
  }
@@ -79430,7 +79767,7 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan) {
79430
79767
  const resourcesToImport = plan.map((entry) => ({
79431
79768
  ResourceType: entry.resourceType,
79432
79769
  LogicalResourceId: entry.logicalId,
79433
- ResourceIdentifier: { [entry.identifierKey]: entry.physicalId }
79770
+ ResourceIdentifier: entry.resourceIdentifier
79434
79771
  }));
79435
79772
  logger.info(
79436
79773
  `Creating IMPORT changeset '${changeSetName}' for stack '${stackName}' (${plan.length} resource(s), ${templateBody.length} bytes)...`
@@ -79601,7 +79938,7 @@ function reorderArgs(argv) {
79601
79938
  }
79602
79939
  async function main() {
79603
79940
  const program = new Command18();
79604
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.84.0");
79941
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.86.0");
79605
79942
  program.addCommand(createBootstrapCommand());
79606
79943
  program.addCommand(createSynthCommand());
79607
79944
  program.addCommand(createListCommand());