@go-to-k/cdkd 0.28.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -678,6 +678,20 @@ out of a larger stack — for example, you have one S3 bucket that was
678
678
  created manually that you want cdkd to manage, while the rest of the
679
679
  stack will be deployed fresh.
680
680
 
681
+ **Selective mode is non-destructive.** When state already exists for
682
+ the stack, listed resources are **merged** into it: unlisted entries
683
+ already in state are preserved (no `--force` needed). `--force` is
684
+ only required when a listed override would overwrite a resource
685
+ already in state — that's the one case where the merge is destructive.
686
+ This is the right command for "I have a deployed stack and want to
687
+ adopt one more resource into it":
688
+
689
+ ```bash
690
+ # Existing state has Queue + Topic; add Bucket without affecting them.
691
+ cdkd import MyStack --resource MyBucket=my-bucket-name
692
+ # Resulting state: Queue + Topic (preserved) + Bucket (newly imported).
693
+ ```
694
+
681
695
  ### Mode 3: hybrid (`--auto` with overrides)
682
696
 
683
697
  ```bash
@@ -698,7 +712,18 @@ the rest by tag automatically.
698
712
  | ----------- | ----------------------------------------------------------------------------- |
699
713
  | `--dry-run` | Preview what would be imported. State is NOT written. |
700
714
  | `--yes` | Skip the confirmation prompt before writing state. |
701
- | `--force` | Overwrite an existing state record. Without this, existing state aborts. |
715
+ | `--force` | Confirm a destructive write to existing state — see below. |
716
+
717
+ `--force` is only needed when the import would lose data:
718
+
719
+ - **Auto / whole-stack mode + existing state**: required. The resource
720
+ map is rebuilt from the template, so any state entry not re-imported
721
+ is dropped.
722
+ - **Selective mode + listed override already in state**: required.
723
+ The listed entry is overwritten with the new physical id.
724
+ - **Selective mode without a conflict (pure merge)**: not required.
725
+ Unlisted state entries are preserved automatically.
726
+ - **No existing state (first-time import)**: not required.
702
727
 
703
728
  ### After import
704
729
 
@@ -746,7 +771,7 @@ table to predict behavior when migrating from `cdk import`.
746
771
  | Bootstrap requirement | Bootstrap v12+ (deploy role needs to read the encrypted staging bucket). | cdkd's own state bucket; no CDK bootstrap version requirement. |
747
772
  | 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
773
  | 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.** |
774
+ | `--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
775
  | `--dry-run` | Implied by `--no-execute` (creates the changeset without executing). | Native: shows the import plan and exits without writing state. |
751
776
 
752
777
  #### Practical implications when migrating from `cdk import`
package/dist/cli.js CHANGED
@@ -35259,12 +35259,6 @@ async function importCommand(stackArg, options) {
35259
35259
  }
35260
35260
  const targetRegion = stackInfo.region || region;
35261
35261
  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
35262
  const overrides = parseResourceOverrides(
35269
35263
  options.resource,
35270
35264
  options.resourceMapping,
@@ -35279,6 +35273,34 @@ async function importCommand(stackArg, options) {
35279
35273
  `Selective mode: only importing the ${overrides.size} resource(s) you listed (${[...overrides.keys()].join(", ")}). Pass --auto to also tag-import the rest.`
35280
35274
  );
35281
35275
  }
35276
+ const existingResult = await stateBackend.getState(stackInfo.stackName, targetRegion);
35277
+ const existingState = existingResult?.state ?? null;
35278
+ const existingEtag = existingResult?.etag;
35279
+ const migrationPending = existingResult?.migrationPending ?? false;
35280
+ if (existingState) {
35281
+ if (!selectiveMode) {
35282
+ if (!options.force) {
35283
+ throw new Error(
35284
+ `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).`
35285
+ );
35286
+ }
35287
+ } else {
35288
+ const conflicts = [...overrides.keys()].filter(
35289
+ (id) => Object.prototype.hasOwnProperty.call(existingState.resources, id)
35290
+ );
35291
+ if (conflicts.length > 0 && !options.force) {
35292
+ throw new Error(
35293
+ `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.`
35294
+ );
35295
+ }
35296
+ const preservedCount = Object.keys(existingState.resources).filter(
35297
+ (id) => !overrides.has(id)
35298
+ ).length;
35299
+ logger.info(
35300
+ `Merging into existing state for ${stackInfo.stackName} (${targetRegion}): preserving ${preservedCount} unlisted resource(s)` + (conflicts.length > 0 ? `, overwriting ${conflicts.length} listed entry(ies)` : "")
35301
+ );
35302
+ }
35303
+ }
35282
35304
  const template = stackInfo.template;
35283
35305
  const templateParser = new TemplateParser();
35284
35306
  const resources = collectImportableResources(template);
@@ -35329,8 +35351,12 @@ async function importCommand(stackArg, options) {
35329
35351
  return;
35330
35352
  }
35331
35353
  if (!options.yes) {
35354
+ const importedCount = importedRows.length;
35355
+ const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length : 0;
35356
+ const totalAfter = importedCount + preservedCount;
35357
+ const breakdown = preservedCount > 0 ? ` (${importedCount} new/overwritten + ${preservedCount} preserved)` : "";
35332
35358
  const ok = await confirmPrompt3(
35333
- `Write state for ${stackInfo.stackName} (${targetRegion}) with ${importedRows.length} resource(s)?`
35359
+ `Write state for ${stackInfo.stackName} (${targetRegion}) with ${totalAfter} resource(s)${breakdown}?`
35334
35360
  );
35335
35361
  if (!ok) {
35336
35362
  logger.info("Import cancelled.");
@@ -35342,9 +35368,18 @@ async function importCommand(stackArg, options) {
35342
35368
  targetRegion,
35343
35369
  rows,
35344
35370
  templateParser,
35345
- template
35371
+ template,
35372
+ existingState,
35373
+ selectiveMode
35346
35374
  );
35347
- await stateBackend.saveState(stackInfo.stackName, targetRegion, stackState);
35375
+ const saveOptions = {};
35376
+ if (existingEtag) {
35377
+ saveOptions.expectedEtag = existingEtag;
35378
+ }
35379
+ if (migrationPending) {
35380
+ saveOptions.migrateLegacy = true;
35381
+ }
35382
+ await stateBackend.saveState(stackInfo.stackName, targetRegion, stackState, saveOptions);
35348
35383
  logger.info(`\u2713 State written: ${stackInfo.stackName} (${targetRegion})`);
35349
35384
  logger.info(
35350
35385
  ` ${importedRows.length} resource(s) imported. Run 'cdkd diff' to see how the imported state lines up with the template.`
@@ -35500,8 +35535,8 @@ function collectImportableResources(template) {
35500
35535
  }
35501
35536
  return out;
35502
35537
  }
35503
- function buildStackState(stackName, region, rows, templateParser, template) {
35504
- const resources = {};
35538
+ function buildStackState(stackName, region, rows, templateParser, template, existingState, selectiveMode) {
35539
+ const resources = selectiveMode && existingState ? { ...existingState.resources } : {};
35505
35540
  for (const row of rows) {
35506
35541
  if (row.outcome !== "imported" || !row.physicalId)
35507
35542
  continue;
@@ -35522,7 +35557,7 @@ function buildStackState(stackName, region, rows, templateParser, template) {
35522
35557
  stackName,
35523
35558
  region,
35524
35559
  resources,
35525
- outputs: {},
35560
+ outputs: existingState?.outputs ?? {},
35526
35561
  lastModified: Date.now()
35527
35562
  };
35528
35563
  }
@@ -35597,7 +35632,7 @@ function createImportCommand() {
35597
35632
  false
35598
35633
  ).option("--dry-run", "Show planned imports without writing state", false).option(
35599
35634
  "--force",
35600
- "Overwrite an existing state record. Without this, an existing state file aborts the import.",
35635
+ "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
35636
  false
35602
35637
  ).action(withErrorHandling(importCommand));
35603
35638
  [...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
@@ -35636,7 +35671,7 @@ function reorderArgs(argv) {
35636
35671
  }
35637
35672
  async function main() {
35638
35673
  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");
35674
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.28.1");
35640
35675
  program.addCommand(createBootstrapCommand());
35641
35676
  program.addCommand(createSynthCommand());
35642
35677
  program.addCommand(createListCommand());