@go-to-k/cdkd 0.28.0 → 0.28.2

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
@@ -457,8 +457,10 @@ cdkd state show MyStack --json # raw {state, lock} JSON
457
457
 
458
458
  # Orphan one or more RESOURCES from cdkd's state (does NOT delete AWS resources).
459
459
  # Per-resource, mirrors aws-cdk-cli's `cdk orphan --unstable=orphan`.
460
- # Synth-driven — needs --app / cdk.json. Construct paths look like the CDK
461
- # `aws:cdk:path` tag (`<StackName>/<Path/To/Resource>`).
460
+ # Synth-driven — needs --app / cdk.json. Construct paths use CDK's L2-style form
461
+ # (`<StackName>/<Path/To/Construct>`); the synthesized `/Resource` suffix is
462
+ # matched implicitly. Passing an L2 wrapper that contains multiple CFn resources
463
+ # orphans every child under it (matches upstream's prefix-match semantics).
462
464
  cdkd orphan MyStack/MyTable # confirmation prompt (y/N)
463
465
  cdkd orphan MyStack/MyTable --yes
464
466
  cdkd orphan MyStack/MyTable MyStack/MyBucket # multiple resources, same stack
@@ -678,6 +680,20 @@ out of a larger stack — for example, you have one S3 bucket that was
678
680
  created manually that you want cdkd to manage, while the rest of the
679
681
  stack will be deployed fresh.
680
682
 
683
+ **Selective mode is non-destructive.** When state already exists for
684
+ the stack, listed resources are **merged** into it: unlisted entries
685
+ already in state are preserved (no `--force` needed). `--force` is
686
+ only required when a listed override would overwrite a resource
687
+ already in state — that's the one case where the merge is destructive.
688
+ This is the right command for "I have a deployed stack and want to
689
+ adopt one more resource into it":
690
+
691
+ ```bash
692
+ # Existing state has Queue + Topic; add Bucket without affecting them.
693
+ cdkd import MyStack --resource MyBucket=my-bucket-name
694
+ # Resulting state: Queue + Topic (preserved) + Bucket (newly imported).
695
+ ```
696
+
681
697
  ### Mode 3: hybrid (`--auto` with overrides)
682
698
 
683
699
  ```bash
@@ -698,7 +714,18 @@ the rest by tag automatically.
698
714
  | ----------- | ----------------------------------------------------------------------------- |
699
715
  | `--dry-run` | Preview what would be imported. State is NOT written. |
700
716
  | `--yes` | Skip the confirmation prompt before writing state. |
701
- | `--force` | Overwrite an existing state record. Without this, existing state aborts. |
717
+ | `--force` | Confirm a destructive write to existing state — see below. |
718
+
719
+ `--force` is only needed when the import would lose data:
720
+
721
+ - **Auto / whole-stack mode + existing state**: required. The resource
722
+ map is rebuilt from the template, so any state entry not re-imported
723
+ is dropped.
724
+ - **Selective mode + listed override already in state**: required.
725
+ The listed entry is overwritten with the new physical id.
726
+ - **Selective mode without a conflict (pure merge)**: not required.
727
+ Unlisted state entries are preserved automatically.
728
+ - **No existing state (first-time import)**: not required.
702
729
 
703
730
  ### After import
704
731
 
@@ -746,7 +773,7 @@ table to predict behavior when migrating from `cdk import`.
746
773
  | Bootstrap requirement | Bootstrap v12+ (deploy role needs to read the encrypted staging bucket). | cdkd's own state bucket; no CDK bootstrap version requirement. |
747
774
  | Resource-type coverage | Whatever [CloudFormation supports for import](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html). | The set of cdkd providers that implement `import()` (see [CLAUDE.md](CLAUDE.md) for the current list). For any other CC-API-supported type, use `--resource <id>=<physical>` to drive the Cloud Control API fallback. The two lists overlap heavily but are not identical. |
748
775
  | Confirmation prompt before writing state | n/a (CloudFormation operates atomically). | Yes — cdkd asks before writing the state file. Skip with `--yes`. |
749
- | `--force` | "Continue even if the diff includes updates or deletions" — about diff strictness. | "Overwrite an existing state record" — about state safety. **Same flag name, different meaning.** |
776
+ | `--force` | "Continue even if the diff includes updates or deletions" — about diff strictness. | "Confirm a destructive write to existing state" — required for auto/whole-stack rebuild and for overwriting a listed entry already in state; not required for a pure selective merge. **Same flag name, different meaning.** |
750
777
  | `--dry-run` | Implied by `--no-execute` (creates the changeset without executing). | Native: shows the import plan and exits without writing state. |
751
778
 
752
779
  #### Practical implications when migrating from `cdk import`
package/dist/cli.js CHANGED
@@ -33391,12 +33391,25 @@ function readCdkPath(resource) {
33391
33391
  function buildCdkPathIndex(template) {
33392
33392
  const index = /* @__PURE__ */ new Map();
33393
33393
  for (const [logicalId, resource] of Object.entries(template.Resources)) {
33394
+ if (resource.Type === "AWS::CDK::Metadata")
33395
+ continue;
33394
33396
  const path = readCdkPath(resource);
33395
33397
  if (path)
33396
33398
  index.set(path, logicalId);
33397
33399
  }
33398
33400
  return index;
33399
33401
  }
33402
+ function resolveCdkPathToLogicalIds(input, index) {
33403
+ const seen = /* @__PURE__ */ new Map();
33404
+ const prefix = `${input}/`;
33405
+ for (const [path, logicalId] of index) {
33406
+ if (path === input || path.startsWith(prefix)) {
33407
+ if (!seen.has(logicalId))
33408
+ seen.set(logicalId, path);
33409
+ }
33410
+ }
33411
+ return [...seen.entries()].map(([logicalId, cdkPath]) => ({ logicalId, cdkPath }));
33412
+ }
33400
33413
 
33401
33414
  // src/analyzer/orphan-rewriter.ts
33402
33415
  var AttributeFetcher = class {
@@ -33952,19 +33965,20 @@ function resolveConstructPaths(paths, stacks) {
33952
33965
  `All construct paths must reference the same stack. Got '${stack.stackName}' and '${candidate.stackName}'. Run 'cdkd orphan' once per stack.`
33953
33966
  );
33954
33967
  }
33955
- const cdkPath = p;
33956
33968
  const index = buildCdkPathIndex(candidate.template);
33957
- const logicalId = index.get(cdkPath);
33958
- if (!logicalId) {
33969
+ const matches = resolveCdkPathToLogicalIds(p, index);
33970
+ if (matches.length === 0) {
33959
33971
  const available = [...index.keys()].sort().join("\n ");
33960
33972
  throw new Error(
33961
- `Construct path '${cdkPath}' not found in template for stack '${candidate.stackName}'.
33973
+ `Construct path '${p}' not found in template for stack '${candidate.stackName}'.
33962
33974
  Available paths:
33963
33975
  ${available}`
33964
33976
  );
33965
33977
  }
33966
- if (!logicalIds.includes(logicalId)) {
33967
- logicalIds.push(logicalId);
33978
+ for (const { logicalId } of matches) {
33979
+ if (!logicalIds.includes(logicalId)) {
33980
+ logicalIds.push(logicalId);
33981
+ }
33968
33982
  }
33969
33983
  }
33970
33984
  if (!stack) {
@@ -35259,12 +35273,6 @@ async function importCommand(stackArg, options) {
35259
35273
  }
35260
35274
  const targetRegion = stackInfo.region || region;
35261
35275
  logger.info(`Target stack: ${stackInfo.stackName} (${targetRegion})`);
35262
- const existing = await stateBackend.stateExists(stackInfo.stackName, targetRegion);
35263
- if (existing && !options.force) {
35264
- throw new Error(
35265
- `State already exists for stack '${stackInfo.stackName}' (${targetRegion}). Pass --force to overwrite. (cdkd state import rebuilds the resource map from AWS, so the existing state \u2014 including any drift you've manually edited \u2014 will be lost.)`
35266
- );
35267
- }
35268
35276
  const overrides = parseResourceOverrides(
35269
35277
  options.resource,
35270
35278
  options.resourceMapping,
@@ -35279,6 +35287,34 @@ async function importCommand(stackArg, options) {
35279
35287
  `Selective mode: only importing the ${overrides.size} resource(s) you listed (${[...overrides.keys()].join(", ")}). Pass --auto to also tag-import the rest.`
35280
35288
  );
35281
35289
  }
35290
+ const existingResult = await stateBackend.getState(stackInfo.stackName, targetRegion);
35291
+ const existingState = existingResult?.state ?? null;
35292
+ const existingEtag = existingResult?.etag;
35293
+ const migrationPending = existingResult?.migrationPending ?? false;
35294
+ if (existingState) {
35295
+ if (!selectiveMode) {
35296
+ if (!options.force) {
35297
+ throw new Error(
35298
+ `State already exists for stack '${stackInfo.stackName}' (${targetRegion}). Auto / whole-stack import rebuilds the entire resource map from the template, which would drop any state entry not re-imported. Pass --force to confirm. To add specific resources without affecting unlisted ones, use --resource <id>=<physicalId> (selective merge \u2014 no --force needed).`
35299
+ );
35300
+ }
35301
+ } else {
35302
+ const conflicts = [...overrides.keys()].filter(
35303
+ (id) => Object.prototype.hasOwnProperty.call(existingState.resources, id)
35304
+ );
35305
+ if (conflicts.length > 0 && !options.force) {
35306
+ throw new Error(
35307
+ `Selective import would overwrite resource(s) already in state: ${conflicts.join(", ")}. Pass --force to confirm the overwrite, or remove these IDs from --resource / --resource-mapping.`
35308
+ );
35309
+ }
35310
+ const preservedCount = Object.keys(existingState.resources).filter(
35311
+ (id) => !overrides.has(id)
35312
+ ).length;
35313
+ logger.info(
35314
+ `Merging into existing state for ${stackInfo.stackName} (${targetRegion}): preserving ${preservedCount} unlisted resource(s)` + (conflicts.length > 0 ? `, overwriting ${conflicts.length} listed entry(ies)` : "")
35315
+ );
35316
+ }
35317
+ }
35282
35318
  const template = stackInfo.template;
35283
35319
  const templateParser = new TemplateParser();
35284
35320
  const resources = collectImportableResources(template);
@@ -35329,8 +35365,12 @@ async function importCommand(stackArg, options) {
35329
35365
  return;
35330
35366
  }
35331
35367
  if (!options.yes) {
35368
+ const importedCount = importedRows.length;
35369
+ const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length : 0;
35370
+ const totalAfter = importedCount + preservedCount;
35371
+ const breakdown = preservedCount > 0 ? ` (${importedCount} new/overwritten + ${preservedCount} preserved)` : "";
35332
35372
  const ok = await confirmPrompt3(
35333
- `Write state for ${stackInfo.stackName} (${targetRegion}) with ${importedRows.length} resource(s)?`
35373
+ `Write state for ${stackInfo.stackName} (${targetRegion}) with ${totalAfter} resource(s)${breakdown}?`
35334
35374
  );
35335
35375
  if (!ok) {
35336
35376
  logger.info("Import cancelled.");
@@ -35342,9 +35382,18 @@ async function importCommand(stackArg, options) {
35342
35382
  targetRegion,
35343
35383
  rows,
35344
35384
  templateParser,
35345
- template
35385
+ template,
35386
+ existingState,
35387
+ selectiveMode
35346
35388
  );
35347
- await stateBackend.saveState(stackInfo.stackName, targetRegion, stackState);
35389
+ const saveOptions = {};
35390
+ if (existingEtag) {
35391
+ saveOptions.expectedEtag = existingEtag;
35392
+ }
35393
+ if (migrationPending) {
35394
+ saveOptions.migrateLegacy = true;
35395
+ }
35396
+ await stateBackend.saveState(stackInfo.stackName, targetRegion, stackState, saveOptions);
35348
35397
  logger.info(`\u2713 State written: ${stackInfo.stackName} (${targetRegion})`);
35349
35398
  logger.info(
35350
35399
  ` ${importedRows.length} resource(s) imported. Run 'cdkd diff' to see how the imported state lines up with the template.`
@@ -35500,8 +35549,8 @@ function collectImportableResources(template) {
35500
35549
  }
35501
35550
  return out;
35502
35551
  }
35503
- function buildStackState(stackName, region, rows, templateParser, template) {
35504
- const resources = {};
35552
+ function buildStackState(stackName, region, rows, templateParser, template, existingState, selectiveMode) {
35553
+ const resources = selectiveMode && existingState ? { ...existingState.resources } : {};
35505
35554
  for (const row of rows) {
35506
35555
  if (row.outcome !== "imported" || !row.physicalId)
35507
35556
  continue;
@@ -35522,7 +35571,7 @@ function buildStackState(stackName, region, rows, templateParser, template) {
35522
35571
  stackName,
35523
35572
  region,
35524
35573
  resources,
35525
- outputs: {},
35574
+ outputs: existingState?.outputs ?? {},
35526
35575
  lastModified: Date.now()
35527
35576
  };
35528
35577
  }
@@ -35597,7 +35646,7 @@ function createImportCommand() {
35597
35646
  false
35598
35647
  ).option("--dry-run", "Show planned imports without writing state", false).option(
35599
35648
  "--force",
35600
- "Overwrite an existing state record. Without this, an existing state file aborts the import.",
35649
+ "Confirm a destructive write to existing state. Required for auto / whole-stack import when state already exists (rebuilds the entire resource map). Also required in selective mode if a listed override would overwrite a resource already in state. Not needed for a pure selective merge (adding new resources without touching unlisted entries).",
35601
35650
  false
35602
35651
  ).action(withErrorHandling(importCommand));
35603
35652
  [...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
@@ -35636,7 +35685,7 @@ function reorderArgs(argv) {
35636
35685
  }
35637
35686
  async function main() {
35638
35687
  const program = new Command13();
35639
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.28.0");
35688
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.28.2");
35640
35689
  program.addCommand(createBootstrapCommand());
35641
35690
  program.addCommand(createSynthCommand());
35642
35691
  program.addCommand(createListCommand());