@go-to-k/cdkd 0.5.1 → 0.7.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
@@ -428,6 +428,25 @@ cdkd destroy --all --force
428
428
 
429
429
  # Force-unlock a stale lock from interrupted deploy
430
430
  cdkd force-unlock MyStack
431
+
432
+ # List stacks registered in the cdkd state bucket
433
+ cdkd state list
434
+ cdkd state ls --long # include resource count, last-modified, lock status
435
+ cdkd state list --json # JSON output (alone, or combined with --long)
436
+
437
+ # List resources of a single stack from state
438
+ cdkd state resources MyStack # aligned columns: LogicalID, Type, PhysicalID
439
+ cdkd state resources MyStack --long # per-resource block with dependencies and attributes
440
+ cdkd state resources MyStack --json # full JSON array
441
+
442
+ # Show full state record for a stack (metadata, outputs, all resources incl. properties)
443
+ cdkd state show MyStack
444
+ cdkd state show MyStack --json # raw {state, lock} JSON
445
+
446
+ # Remove cdkd's state record for a stack (does NOT delete AWS resources)
447
+ cdkd state rm MyStack # confirmation prompt (y/N)
448
+ cdkd state rm MyStack --yes # skip confirmation
449
+ cdkd state rm StackA StackB --force # also bypass the locked-stack refusal
431
450
  ```
432
451
 
433
452
  ### Concurrency Options
package/dist/cli.js CHANGED
@@ -447,7 +447,7 @@ var init_aws_clients = __esm({
447
447
  });
448
448
 
449
449
  // src/cli/index.ts
450
- import { Command as Command9 } from "commander";
450
+ import { Command as Command10 } from "commander";
451
451
 
452
452
  // src/cli/commands/bootstrap.ts
453
453
  import { Command } from "commander";
@@ -2401,7 +2401,9 @@ function toYaml(obj, indent = 0) {
2401
2401
  let result = "\n";
2402
2402
  for (const [key, value] of entries) {
2403
2403
  const safeKey = key.includes(" ") ? `"${key}"` : key;
2404
- if (typeof value === "object" && value !== null) {
2404
+ const isContainer = typeof value === "object" && value !== null;
2405
+ const isEmptyContainer = isContainer && (Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0);
2406
+ if (isContainer && !isEmptyContainer) {
2405
2407
  result += `${prefix}${safeKey}:${toYaml(value, indent + 1)}`;
2406
2408
  } else {
2407
2409
  result += `${prefix}${safeKey}: ${toYaml(value, indent + 1).trimStart()}`;
@@ -2584,17 +2586,20 @@ async function listCommand(patterns, options) {
2584
2586
  }
2585
2587
  if (options.showDependencies) {
2586
2588
  const records = sorted.map((s) => ({
2587
- id: s.displayName,
2589
+ id: formatDisplayId(s),
2588
2590
  dependencies: [...s.dependencyNames]
2589
2591
  }));
2590
2592
  emitStructured(records, options.json);
2591
2593
  return;
2592
2594
  }
2593
2595
  for (const stack of sorted) {
2594
- process.stdout.write(`${stack.displayName}
2596
+ process.stdout.write(`${formatDisplayId(stack)}
2595
2597
  `);
2596
2598
  }
2597
2599
  }
2600
+ function formatDisplayId(stack) {
2601
+ return stack.displayName === stack.stackName ? stack.displayName : `${stack.displayName} (${stack.stackName})`;
2602
+ }
2598
2603
  function emitStructured(payload, asJson) {
2599
2604
  if (asJson) {
2600
2605
  process.stdout.write(`${JSON.stringify(payload, null, 2)}
@@ -3563,6 +3568,18 @@ var LockManager = class {
3563
3568
  );
3564
3569
  }
3565
3570
  }
3571
+ /**
3572
+ * Check whether a lock currently exists for a stack
3573
+ *
3574
+ * Returns true if a lock file is present in S3 (regardless of expiry).
3575
+ * This is intended for read-only inspection (e.g. `cdkd state list --long`),
3576
+ * not for acquisition decisions — use `acquireLock` for that, which has its
3577
+ * own expired-lock cleanup logic.
3578
+ */
3579
+ async isLocked(stackName) {
3580
+ const lockInfo = await this.getLockInfo(stackName);
3581
+ return lockInfo !== null;
3582
+ }
3566
3583
  /**
3567
3584
  * Release a lock for a stack
3568
3585
  */
@@ -28089,6 +28106,337 @@ function createForceUnlockCommand() {
28089
28106
  return cmd;
28090
28107
  }
28091
28108
 
28109
+ // src/cli/commands/state.ts
28110
+ import * as readline2 from "node:readline/promises";
28111
+ import { Command as Command9 } from "commander";
28112
+ init_aws_clients();
28113
+ async function setupStateBackend(options) {
28114
+ const awsClients = new AwsClients({
28115
+ ...options.region && { region: options.region },
28116
+ ...options.profile && { profile: options.profile }
28117
+ });
28118
+ setAwsClients(awsClients);
28119
+ const region = options.region || process.env["AWS_REGION"] || "us-east-1";
28120
+ const bucket = await resolveStateBucketWithDefault(options.stateBucket, region);
28121
+ const prefix = options.statePrefix;
28122
+ const stateConfig = { bucket, prefix };
28123
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig);
28124
+ const lockManager = new LockManager(awsClients.s3, stateConfig);
28125
+ await stateBackend.verifyBucketExists();
28126
+ return {
28127
+ stateBackend,
28128
+ lockManager,
28129
+ bucket,
28130
+ prefix,
28131
+ dispose: () => awsClients.destroy()
28132
+ };
28133
+ }
28134
+ async function stateListCommand(options) {
28135
+ const logger = getLogger();
28136
+ if (options.verbose)
28137
+ logger.setLevel("debug");
28138
+ const setup = await setupStateBackend(options);
28139
+ try {
28140
+ const stackNames = (await setup.stateBackend.listStacks()).slice().sort();
28141
+ if (!options.long && !options.json) {
28142
+ for (const name of stackNames) {
28143
+ process.stdout.write(`${name}
28144
+ `);
28145
+ }
28146
+ return;
28147
+ }
28148
+ if (options.json && !options.long) {
28149
+ process.stdout.write(`${JSON.stringify(stackNames, null, 2)}
28150
+ `);
28151
+ return;
28152
+ }
28153
+ const details = await Promise.all(
28154
+ stackNames.map(async (stackName) => {
28155
+ const [stateResult, locked] = await Promise.all([
28156
+ setup.stateBackend.getState(stackName),
28157
+ setup.lockManager.isLocked(stackName)
28158
+ ]);
28159
+ const state = stateResult?.state;
28160
+ return {
28161
+ stackName,
28162
+ resourceCount: state ? Object.keys(state.resources).length : 0,
28163
+ lastModified: state && typeof state.lastModified === "number" ? new Date(state.lastModified).toISOString() : null,
28164
+ locked
28165
+ };
28166
+ })
28167
+ );
28168
+ if (options.json) {
28169
+ process.stdout.write(`${JSON.stringify(details, null, 2)}
28170
+ `);
28171
+ return;
28172
+ }
28173
+ const lines = [];
28174
+ for (const detail of details) {
28175
+ lines.push(detail.stackName);
28176
+ lines.push(` Resources: ${detail.resourceCount}`);
28177
+ lines.push(` Last Modified: ${detail.lastModified ?? "unknown"}`);
28178
+ lines.push(` Lock: ${detail.locked ? "locked" : "unlocked"}`);
28179
+ lines.push("");
28180
+ }
28181
+ if (lines.length > 0) {
28182
+ if (lines[lines.length - 1] === "") {
28183
+ lines.pop();
28184
+ }
28185
+ process.stdout.write(`${lines.join("\n")}
28186
+ `);
28187
+ }
28188
+ } finally {
28189
+ setup.dispose();
28190
+ }
28191
+ }
28192
+ function createStateListCommand() {
28193
+ const cmd = new Command9("list").alias("ls").description("List stacks registered in the cdkd state bucket").option("-l, --long", "Show resource count, last-modified time, and lock status", false).option("--json", "Output as JSON", false).action(withErrorHandling(stateListCommand));
28194
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28195
+ return cmd;
28196
+ }
28197
+ async function stateResourcesCommand(stackName, options) {
28198
+ const logger = getLogger();
28199
+ if (options.verbose)
28200
+ logger.setLevel("debug");
28201
+ const setup = await setupStateBackend(options);
28202
+ try {
28203
+ const stateResult = await setup.stateBackend.getState(stackName);
28204
+ if (!stateResult) {
28205
+ throw new Error(
28206
+ `No state found for stack '${stackName}' in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`
28207
+ );
28208
+ }
28209
+ const resources = stateResult.state.resources ?? {};
28210
+ const details = Object.entries(resources).map(([logicalId, resource]) => ({
28211
+ logicalId,
28212
+ resourceType: resource.resourceType,
28213
+ physicalId: resource.physicalId,
28214
+ dependencies: resource.dependencies ?? [],
28215
+ attributes: resource.attributes ?? {}
28216
+ })).sort((a, b) => a.logicalId.localeCompare(b.logicalId));
28217
+ if (options.json) {
28218
+ process.stdout.write(`${JSON.stringify(details, null, 2)}
28219
+ `);
28220
+ return;
28221
+ }
28222
+ if (details.length === 0) {
28223
+ return;
28224
+ }
28225
+ if (options.long) {
28226
+ const lines = [];
28227
+ for (const detail of details) {
28228
+ lines.push(detail.logicalId);
28229
+ lines.push(` Type: ${detail.resourceType}`);
28230
+ lines.push(` PhysicalID: ${detail.physicalId}`);
28231
+ lines.push(
28232
+ ` Dependencies: ${detail.dependencies.length > 0 ? detail.dependencies.join(", ") : "(none)"}`
28233
+ );
28234
+ const attrEntries = Object.entries(detail.attributes);
28235
+ if (attrEntries.length === 0) {
28236
+ lines.push(" Attributes: (none)");
28237
+ } else {
28238
+ lines.push(" Attributes:");
28239
+ for (const [k, v] of attrEntries) {
28240
+ lines.push(` ${k}: ${formatAttributeValue(v)}`);
28241
+ }
28242
+ }
28243
+ lines.push("");
28244
+ }
28245
+ if (lines[lines.length - 1] === "") {
28246
+ lines.pop();
28247
+ }
28248
+ process.stdout.write(`${lines.join("\n")}
28249
+ `);
28250
+ return;
28251
+ }
28252
+ const idWidth = Math.max(...details.map((d) => d.logicalId.length));
28253
+ const typeWidth = Math.max(...details.map((d) => d.resourceType.length));
28254
+ for (const detail of details) {
28255
+ process.stdout.write(
28256
+ `${detail.logicalId.padEnd(idWidth)} ${detail.resourceType.padEnd(typeWidth)} ${detail.physicalId}
28257
+ `
28258
+ );
28259
+ }
28260
+ } finally {
28261
+ setup.dispose();
28262
+ }
28263
+ }
28264
+ function formatAttributeValue(value) {
28265
+ if (value === null)
28266
+ return "null";
28267
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
28268
+ return String(value);
28269
+ }
28270
+ return JSON.stringify(value);
28271
+ }
28272
+ function formatDuration(ms) {
28273
+ const seconds = Math.floor(ms / 1e3);
28274
+ if (seconds < 60)
28275
+ return `${seconds}s`;
28276
+ const minutes = Math.floor(seconds / 60);
28277
+ const remainingSeconds = seconds % 60;
28278
+ return `${minutes}m${remainingSeconds}s`;
28279
+ }
28280
+ function formatLockSummary(lockInfo) {
28281
+ if (!lockInfo)
28282
+ return "unlocked";
28283
+ const opStr = lockInfo.operation ? ` (operation: ${lockInfo.operation})` : "";
28284
+ const expiresInMs = lockInfo.expiresAt - Date.now();
28285
+ const expiresStr = expiresInMs > 0 ? `expires in ${formatDuration(expiresInMs)}` : `expired ${formatDuration(-expiresInMs)} ago`;
28286
+ return `locked by ${lockInfo.owner}${opStr}, ${expiresStr}`;
28287
+ }
28288
+ function createStateResourcesCommand() {
28289
+ const cmd = new Command9("resources").description("List resources recorded in a stack's state").argument("<stack>", "Stack name (physical CloudFormation name)").option("-l, --long", "Include dependencies and attributes per resource", false).option("--json", "Output as JSON", false).action(withErrorHandling(stateResourcesCommand));
28290
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28291
+ return cmd;
28292
+ }
28293
+ async function stateShowCommand(stackName, options) {
28294
+ const logger = getLogger();
28295
+ if (options.verbose)
28296
+ logger.setLevel("debug");
28297
+ const setup = await setupStateBackend(options);
28298
+ try {
28299
+ const [stateResult, lockInfo] = await Promise.all([
28300
+ setup.stateBackend.getState(stackName),
28301
+ setup.lockManager.getLockInfo(stackName)
28302
+ ]);
28303
+ if (!stateResult) {
28304
+ throw new Error(
28305
+ `No state found for stack '${stackName}' in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`
28306
+ );
28307
+ }
28308
+ if (options.json) {
28309
+ process.stdout.write(
28310
+ `${JSON.stringify({ state: stateResult.state, lock: lockInfo }, null, 2)}
28311
+ `
28312
+ );
28313
+ return;
28314
+ }
28315
+ const state = stateResult.state;
28316
+ const lines = [];
28317
+ lines.push(`Stack: ${state.stackName}`);
28318
+ if (state.region)
28319
+ lines.push(` Region: ${state.region}`);
28320
+ lines.push(` Version: ${state.version}`);
28321
+ lines.push(` Last Modified: ${new Date(state.lastModified).toISOString()}`);
28322
+ lines.push(` Lock: ${formatLockSummary(lockInfo)}`);
28323
+ const outputEntries = Object.entries(state.outputs ?? {});
28324
+ if (outputEntries.length > 0) {
28325
+ lines.push("");
28326
+ lines.push("Outputs:");
28327
+ for (const [k, v] of outputEntries) {
28328
+ lines.push(` ${k}: ${formatAttributeValue(v)}`);
28329
+ }
28330
+ }
28331
+ const resourceEntries = Object.entries(state.resources ?? {}).sort(
28332
+ ([a], [b]) => a.localeCompare(b)
28333
+ );
28334
+ lines.push("");
28335
+ lines.push(`Resources (${resourceEntries.length}):`);
28336
+ for (const [logicalId, resource] of resourceEntries) {
28337
+ lines.push("");
28338
+ lines.push(logicalId);
28339
+ lines.push(` Type: ${resource.resourceType}`);
28340
+ lines.push(` PhysicalID: ${resource.physicalId}`);
28341
+ const deps = resource.dependencies ?? [];
28342
+ lines.push(` Dependencies: ${deps.length > 0 ? deps.join(", ") : "(none)"}`);
28343
+ const attrEntries = Object.entries(resource.attributes ?? {});
28344
+ if (attrEntries.length === 0) {
28345
+ lines.push(" Attributes: (none)");
28346
+ } else {
28347
+ lines.push(" Attributes:");
28348
+ for (const [k, v] of attrEntries) {
28349
+ lines.push(` ${k}: ${formatAttributeValue(v)}`);
28350
+ }
28351
+ }
28352
+ const propEntries = Object.entries(resource.properties ?? {});
28353
+ if (propEntries.length === 0) {
28354
+ lines.push(" Properties: (none)");
28355
+ } else {
28356
+ lines.push(" Properties:");
28357
+ for (const [k, v] of propEntries) {
28358
+ lines.push(` ${k}: ${formatAttributeValue(v)}`);
28359
+ }
28360
+ }
28361
+ }
28362
+ process.stdout.write(`${lines.join("\n")}
28363
+ `);
28364
+ } finally {
28365
+ setup.dispose();
28366
+ }
28367
+ }
28368
+ function createStateShowCommand() {
28369
+ const cmd = new Command9("show").description("Show the full cdkd state record for a stack (metadata, outputs, resources)").argument("<stack>", "Stack name (physical CloudFormation name)").option("--json", "Output the raw state and lock as JSON", false).action(withErrorHandling(stateShowCommand));
28370
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28371
+ return cmd;
28372
+ }
28373
+ async function stateRmCommand(stackArgs, options) {
28374
+ const logger = getLogger();
28375
+ if (options.verbose)
28376
+ logger.setLevel("debug");
28377
+ if (stackArgs.length === 0) {
28378
+ throw new Error("Stack name is required. Usage: cdkd state rm <stack> [<stack>...]");
28379
+ }
28380
+ const setup = await setupStateBackend(options);
28381
+ try {
28382
+ for (const stackName of stackArgs) {
28383
+ const exists = await setup.stateBackend.stateExists(stackName);
28384
+ if (!exists) {
28385
+ logger.info(`No state found for stack: ${stackName}, skipping`);
28386
+ continue;
28387
+ }
28388
+ if (!options.force) {
28389
+ const locked = await setup.lockManager.isLocked(stackName);
28390
+ if (locked) {
28391
+ throw new Error(
28392
+ `Stack '${stackName}' is locked. Run 'cdkd force-unlock ${stackName}' first, or pass --force to remove anyway.`
28393
+ );
28394
+ }
28395
+ }
28396
+ if (!options.yes && !options.force) {
28397
+ process.stdout.write(
28398
+ `
28399
+ WARNING: This removes cdkd's state record for '${stackName}' only. AWS resources will NOT be deleted.
28400
+ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
28401
+
28402
+ `
28403
+ );
28404
+ const rl = readline2.createInterface({
28405
+ input: process.stdin,
28406
+ output: process.stdout
28407
+ });
28408
+ const answer = await rl.question(
28409
+ `Remove state for stack '${stackName}' from s3://${setup.bucket}/${setup.prefix}/? (y/N): `
28410
+ );
28411
+ rl.close();
28412
+ const trimmed = answer.trim().toLowerCase();
28413
+ if (trimmed !== "y" && trimmed !== "yes") {
28414
+ logger.info(`Cancelled removal of state for stack: ${stackName}`);
28415
+ continue;
28416
+ }
28417
+ }
28418
+ await setup.stateBackend.deleteState(stackName);
28419
+ await setup.lockManager.forceReleaseLock(stackName);
28420
+ logger.info(`\u2713 Removed state for stack: ${stackName}`);
28421
+ }
28422
+ } finally {
28423
+ setup.dispose();
28424
+ }
28425
+ }
28426
+ function createStateRmCommand() {
28427
+ const cmd = new Command9("rm").description("Remove cdkd state for one or more stacks (does NOT delete AWS resources)").argument("<stacks...>", "Stack name(s) to remove from state").option("-f, --force", "Skip confirmation and remove even if the stack is locked", false).action(withErrorHandling(stateRmCommand));
28428
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28429
+ return cmd;
28430
+ }
28431
+ function createStateCommand() {
28432
+ const cmd = new Command9("state").description("Manage cdkd state stored in S3");
28433
+ cmd.addCommand(createStateListCommand());
28434
+ cmd.addCommand(createStateResourcesCommand());
28435
+ cmd.addCommand(createStateShowCommand());
28436
+ cmd.addCommand(createStateRmCommand());
28437
+ return cmd;
28438
+ }
28439
+
28092
28440
  // src/cli/index.ts
28093
28441
  var SUBCOMMANDS = /* @__PURE__ */ new Set([
28094
28442
  "bootstrap",
@@ -28099,7 +28447,8 @@ var SUBCOMMANDS = /* @__PURE__ */ new Set([
28099
28447
  "diff",
28100
28448
  "destroy",
28101
28449
  "publish-assets",
28102
- "force-unlock"
28450
+ "force-unlock",
28451
+ "state"
28103
28452
  ]);
28104
28453
  function reorderArgs(argv) {
28105
28454
  const prefix = argv.slice(0, 2);
@@ -28112,8 +28461,8 @@ function reorderArgs(argv) {
28112
28461
  return [...prefix, ...cmdAndAfter, ...beforeCmd];
28113
28462
  }
28114
28463
  async function main() {
28115
- const program = new Command9();
28116
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.5.1");
28464
+ const program = new Command10();
28465
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.7.0");
28117
28466
  program.addCommand(createBootstrapCommand());
28118
28467
  program.addCommand(createSynthCommand());
28119
28468
  program.addCommand(createListCommand());
@@ -28122,6 +28471,7 @@ async function main() {
28122
28471
  program.addCommand(createDestroyCommand());
28123
28472
  program.addCommand(createPublishAssetsCommand());
28124
28473
  program.addCommand(createForceUnlockCommand());
28474
+ program.addCommand(createStateCommand());
28125
28475
  const args = reorderArgs(process.argv);
28126
28476
  await program.parseAsync(args);
28127
28477
  }