@go-to-k/cdkd 0.5.1 → 0.6.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";
@@ -3563,6 +3563,18 @@ var LockManager = class {
3563
3563
  );
3564
3564
  }
3565
3565
  }
3566
+ /**
3567
+ * Check whether a lock currently exists for a stack
3568
+ *
3569
+ * Returns true if a lock file is present in S3 (regardless of expiry).
3570
+ * This is intended for read-only inspection (e.g. `cdkd state list --long`),
3571
+ * not for acquisition decisions — use `acquireLock` for that, which has its
3572
+ * own expired-lock cleanup logic.
3573
+ */
3574
+ async isLocked(stackName) {
3575
+ const lockInfo = await this.getLockInfo(stackName);
3576
+ return lockInfo !== null;
3577
+ }
3566
3578
  /**
3567
3579
  * Release a lock for a stack
3568
3580
  */
@@ -28089,6 +28101,337 @@ function createForceUnlockCommand() {
28089
28101
  return cmd;
28090
28102
  }
28091
28103
 
28104
+ // src/cli/commands/state.ts
28105
+ import * as readline2 from "node:readline/promises";
28106
+ import { Command as Command9 } from "commander";
28107
+ init_aws_clients();
28108
+ async function setupStateBackend(options) {
28109
+ const awsClients = new AwsClients({
28110
+ ...options.region && { region: options.region },
28111
+ ...options.profile && { profile: options.profile }
28112
+ });
28113
+ setAwsClients(awsClients);
28114
+ const region = options.region || process.env["AWS_REGION"] || "us-east-1";
28115
+ const bucket = await resolveStateBucketWithDefault(options.stateBucket, region);
28116
+ const prefix = options.statePrefix;
28117
+ const stateConfig = { bucket, prefix };
28118
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig);
28119
+ const lockManager = new LockManager(awsClients.s3, stateConfig);
28120
+ await stateBackend.verifyBucketExists();
28121
+ return {
28122
+ stateBackend,
28123
+ lockManager,
28124
+ bucket,
28125
+ prefix,
28126
+ dispose: () => awsClients.destroy()
28127
+ };
28128
+ }
28129
+ async function stateListCommand(options) {
28130
+ const logger = getLogger();
28131
+ if (options.verbose)
28132
+ logger.setLevel("debug");
28133
+ const setup = await setupStateBackend(options);
28134
+ try {
28135
+ const stackNames = (await setup.stateBackend.listStacks()).slice().sort();
28136
+ if (!options.long && !options.json) {
28137
+ for (const name of stackNames) {
28138
+ process.stdout.write(`${name}
28139
+ `);
28140
+ }
28141
+ return;
28142
+ }
28143
+ if (options.json && !options.long) {
28144
+ process.stdout.write(`${JSON.stringify(stackNames, null, 2)}
28145
+ `);
28146
+ return;
28147
+ }
28148
+ const details = await Promise.all(
28149
+ stackNames.map(async (stackName) => {
28150
+ const [stateResult, locked] = await Promise.all([
28151
+ setup.stateBackend.getState(stackName),
28152
+ setup.lockManager.isLocked(stackName)
28153
+ ]);
28154
+ const state = stateResult?.state;
28155
+ return {
28156
+ stackName,
28157
+ resourceCount: state ? Object.keys(state.resources).length : 0,
28158
+ lastModified: state && typeof state.lastModified === "number" ? new Date(state.lastModified).toISOString() : null,
28159
+ locked
28160
+ };
28161
+ })
28162
+ );
28163
+ if (options.json) {
28164
+ process.stdout.write(`${JSON.stringify(details, null, 2)}
28165
+ `);
28166
+ return;
28167
+ }
28168
+ const lines = [];
28169
+ for (const detail of details) {
28170
+ lines.push(detail.stackName);
28171
+ lines.push(` Resources: ${detail.resourceCount}`);
28172
+ lines.push(` Last Modified: ${detail.lastModified ?? "unknown"}`);
28173
+ lines.push(` Lock: ${detail.locked ? "locked" : "unlocked"}`);
28174
+ lines.push("");
28175
+ }
28176
+ if (lines.length > 0) {
28177
+ if (lines[lines.length - 1] === "") {
28178
+ lines.pop();
28179
+ }
28180
+ process.stdout.write(`${lines.join("\n")}
28181
+ `);
28182
+ }
28183
+ } finally {
28184
+ setup.dispose();
28185
+ }
28186
+ }
28187
+ function createStateListCommand() {
28188
+ 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));
28189
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28190
+ return cmd;
28191
+ }
28192
+ async function stateResourcesCommand(stackName, options) {
28193
+ const logger = getLogger();
28194
+ if (options.verbose)
28195
+ logger.setLevel("debug");
28196
+ const setup = await setupStateBackend(options);
28197
+ try {
28198
+ const stateResult = await setup.stateBackend.getState(stackName);
28199
+ if (!stateResult) {
28200
+ throw new Error(
28201
+ `No state found for stack '${stackName}' in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`
28202
+ );
28203
+ }
28204
+ const resources = stateResult.state.resources ?? {};
28205
+ const details = Object.entries(resources).map(([logicalId, resource]) => ({
28206
+ logicalId,
28207
+ resourceType: resource.resourceType,
28208
+ physicalId: resource.physicalId,
28209
+ dependencies: resource.dependencies ?? [],
28210
+ attributes: resource.attributes ?? {}
28211
+ })).sort((a, b) => a.logicalId.localeCompare(b.logicalId));
28212
+ if (options.json) {
28213
+ process.stdout.write(`${JSON.stringify(details, null, 2)}
28214
+ `);
28215
+ return;
28216
+ }
28217
+ if (details.length === 0) {
28218
+ return;
28219
+ }
28220
+ if (options.long) {
28221
+ const lines = [];
28222
+ for (const detail of details) {
28223
+ lines.push(detail.logicalId);
28224
+ lines.push(` Type: ${detail.resourceType}`);
28225
+ lines.push(` PhysicalID: ${detail.physicalId}`);
28226
+ lines.push(
28227
+ ` Dependencies: ${detail.dependencies.length > 0 ? detail.dependencies.join(", ") : "(none)"}`
28228
+ );
28229
+ const attrEntries = Object.entries(detail.attributes);
28230
+ if (attrEntries.length === 0) {
28231
+ lines.push(" Attributes: (none)");
28232
+ } else {
28233
+ lines.push(" Attributes:");
28234
+ for (const [k, v] of attrEntries) {
28235
+ lines.push(` ${k}: ${formatAttributeValue(v)}`);
28236
+ }
28237
+ }
28238
+ lines.push("");
28239
+ }
28240
+ if (lines[lines.length - 1] === "") {
28241
+ lines.pop();
28242
+ }
28243
+ process.stdout.write(`${lines.join("\n")}
28244
+ `);
28245
+ return;
28246
+ }
28247
+ const idWidth = Math.max(...details.map((d) => d.logicalId.length));
28248
+ const typeWidth = Math.max(...details.map((d) => d.resourceType.length));
28249
+ for (const detail of details) {
28250
+ process.stdout.write(
28251
+ `${detail.logicalId.padEnd(idWidth)} ${detail.resourceType.padEnd(typeWidth)} ${detail.physicalId}
28252
+ `
28253
+ );
28254
+ }
28255
+ } finally {
28256
+ setup.dispose();
28257
+ }
28258
+ }
28259
+ function formatAttributeValue(value) {
28260
+ if (value === null)
28261
+ return "null";
28262
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
28263
+ return String(value);
28264
+ }
28265
+ return JSON.stringify(value);
28266
+ }
28267
+ function formatDuration(ms) {
28268
+ const seconds = Math.floor(ms / 1e3);
28269
+ if (seconds < 60)
28270
+ return `${seconds}s`;
28271
+ const minutes = Math.floor(seconds / 60);
28272
+ const remainingSeconds = seconds % 60;
28273
+ return `${minutes}m${remainingSeconds}s`;
28274
+ }
28275
+ function formatLockSummary(lockInfo) {
28276
+ if (!lockInfo)
28277
+ return "unlocked";
28278
+ const opStr = lockInfo.operation ? ` (operation: ${lockInfo.operation})` : "";
28279
+ const expiresInMs = lockInfo.expiresAt - Date.now();
28280
+ const expiresStr = expiresInMs > 0 ? `expires in ${formatDuration(expiresInMs)}` : `expired ${formatDuration(-expiresInMs)} ago`;
28281
+ return `locked by ${lockInfo.owner}${opStr}, ${expiresStr}`;
28282
+ }
28283
+ function createStateResourcesCommand() {
28284
+ 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));
28285
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28286
+ return cmd;
28287
+ }
28288
+ async function stateShowCommand(stackName, options) {
28289
+ const logger = getLogger();
28290
+ if (options.verbose)
28291
+ logger.setLevel("debug");
28292
+ const setup = await setupStateBackend(options);
28293
+ try {
28294
+ const [stateResult, lockInfo] = await Promise.all([
28295
+ setup.stateBackend.getState(stackName),
28296
+ setup.lockManager.getLockInfo(stackName)
28297
+ ]);
28298
+ if (!stateResult) {
28299
+ throw new Error(
28300
+ `No state found for stack '${stackName}' in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`
28301
+ );
28302
+ }
28303
+ if (options.json) {
28304
+ process.stdout.write(
28305
+ `${JSON.stringify({ state: stateResult.state, lock: lockInfo }, null, 2)}
28306
+ `
28307
+ );
28308
+ return;
28309
+ }
28310
+ const state = stateResult.state;
28311
+ const lines = [];
28312
+ lines.push(`Stack: ${state.stackName}`);
28313
+ if (state.region)
28314
+ lines.push(` Region: ${state.region}`);
28315
+ lines.push(` Version: ${state.version}`);
28316
+ lines.push(` Last Modified: ${new Date(state.lastModified).toISOString()}`);
28317
+ lines.push(` Lock: ${formatLockSummary(lockInfo)}`);
28318
+ const outputEntries = Object.entries(state.outputs ?? {});
28319
+ if (outputEntries.length > 0) {
28320
+ lines.push("");
28321
+ lines.push("Outputs:");
28322
+ for (const [k, v] of outputEntries) {
28323
+ lines.push(` ${k}: ${formatAttributeValue(v)}`);
28324
+ }
28325
+ }
28326
+ const resourceEntries = Object.entries(state.resources ?? {}).sort(
28327
+ ([a], [b]) => a.localeCompare(b)
28328
+ );
28329
+ lines.push("");
28330
+ lines.push(`Resources (${resourceEntries.length}):`);
28331
+ for (const [logicalId, resource] of resourceEntries) {
28332
+ lines.push("");
28333
+ lines.push(logicalId);
28334
+ lines.push(` Type: ${resource.resourceType}`);
28335
+ lines.push(` PhysicalID: ${resource.physicalId}`);
28336
+ const deps = resource.dependencies ?? [];
28337
+ lines.push(` Dependencies: ${deps.length > 0 ? deps.join(", ") : "(none)"}`);
28338
+ const attrEntries = Object.entries(resource.attributes ?? {});
28339
+ if (attrEntries.length === 0) {
28340
+ lines.push(" Attributes: (none)");
28341
+ } else {
28342
+ lines.push(" Attributes:");
28343
+ for (const [k, v] of attrEntries) {
28344
+ lines.push(` ${k}: ${formatAttributeValue(v)}`);
28345
+ }
28346
+ }
28347
+ const propEntries = Object.entries(resource.properties ?? {});
28348
+ if (propEntries.length === 0) {
28349
+ lines.push(" Properties: (none)");
28350
+ } else {
28351
+ lines.push(" Properties:");
28352
+ for (const [k, v] of propEntries) {
28353
+ lines.push(` ${k}: ${formatAttributeValue(v)}`);
28354
+ }
28355
+ }
28356
+ }
28357
+ process.stdout.write(`${lines.join("\n")}
28358
+ `);
28359
+ } finally {
28360
+ setup.dispose();
28361
+ }
28362
+ }
28363
+ function createStateShowCommand() {
28364
+ 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));
28365
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28366
+ return cmd;
28367
+ }
28368
+ async function stateRmCommand(stackArgs, options) {
28369
+ const logger = getLogger();
28370
+ if (options.verbose)
28371
+ logger.setLevel("debug");
28372
+ if (stackArgs.length === 0) {
28373
+ throw new Error("Stack name is required. Usage: cdkd state rm <stack> [<stack>...]");
28374
+ }
28375
+ const setup = await setupStateBackend(options);
28376
+ try {
28377
+ for (const stackName of stackArgs) {
28378
+ const exists = await setup.stateBackend.stateExists(stackName);
28379
+ if (!exists) {
28380
+ logger.info(`No state found for stack: ${stackName}, skipping`);
28381
+ continue;
28382
+ }
28383
+ if (!options.force) {
28384
+ const locked = await setup.lockManager.isLocked(stackName);
28385
+ if (locked) {
28386
+ throw new Error(
28387
+ `Stack '${stackName}' is locked. Run 'cdkd force-unlock ${stackName}' first, or pass --force to remove anyway.`
28388
+ );
28389
+ }
28390
+ }
28391
+ if (!options.yes && !options.force) {
28392
+ process.stdout.write(
28393
+ `
28394
+ WARNING: This removes cdkd's state record for '${stackName}' only. AWS resources will NOT be deleted.
28395
+ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
28396
+
28397
+ `
28398
+ );
28399
+ const rl = readline2.createInterface({
28400
+ input: process.stdin,
28401
+ output: process.stdout
28402
+ });
28403
+ const answer = await rl.question(
28404
+ `Remove state for stack '${stackName}' from s3://${setup.bucket}/${setup.prefix}/? (y/N): `
28405
+ );
28406
+ rl.close();
28407
+ const trimmed = answer.trim().toLowerCase();
28408
+ if (trimmed !== "y" && trimmed !== "yes") {
28409
+ logger.info(`Cancelled removal of state for stack: ${stackName}`);
28410
+ continue;
28411
+ }
28412
+ }
28413
+ await setup.stateBackend.deleteState(stackName);
28414
+ await setup.lockManager.forceReleaseLock(stackName);
28415
+ logger.info(`\u2713 Removed state for stack: ${stackName}`);
28416
+ }
28417
+ } finally {
28418
+ setup.dispose();
28419
+ }
28420
+ }
28421
+ function createStateRmCommand() {
28422
+ 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));
28423
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28424
+ return cmd;
28425
+ }
28426
+ function createStateCommand() {
28427
+ const cmd = new Command9("state").description("Manage cdkd state stored in S3");
28428
+ cmd.addCommand(createStateListCommand());
28429
+ cmd.addCommand(createStateResourcesCommand());
28430
+ cmd.addCommand(createStateShowCommand());
28431
+ cmd.addCommand(createStateRmCommand());
28432
+ return cmd;
28433
+ }
28434
+
28092
28435
  // src/cli/index.ts
28093
28436
  var SUBCOMMANDS = /* @__PURE__ */ new Set([
28094
28437
  "bootstrap",
@@ -28099,7 +28442,8 @@ var SUBCOMMANDS = /* @__PURE__ */ new Set([
28099
28442
  "diff",
28100
28443
  "destroy",
28101
28444
  "publish-assets",
28102
- "force-unlock"
28445
+ "force-unlock",
28446
+ "state"
28103
28447
  ]);
28104
28448
  function reorderArgs(argv) {
28105
28449
  const prefix = argv.slice(0, 2);
@@ -28112,8 +28456,8 @@ function reorderArgs(argv) {
28112
28456
  return [...prefix, ...cmdAndAfter, ...beforeCmd];
28113
28457
  }
28114
28458
  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");
28459
+ const program = new Command10();
28460
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.6.0");
28117
28461
  program.addCommand(createBootstrapCommand());
28118
28462
  program.addCommand(createSynthCommand());
28119
28463
  program.addCommand(createListCommand());
@@ -28122,6 +28466,7 @@ async function main() {
28122
28466
  program.addCommand(createDestroyCommand());
28123
28467
  program.addCommand(createPublishAssetsCommand());
28124
28468
  program.addCommand(createForceUnlockCommand());
28469
+ program.addCommand(createStateCommand());
28125
28470
  const args = reorderArgs(process.argv);
28126
28471
  await program.parseAsync(args);
28127
28472
  }