@go-to-k/cdkd 0.5.0 → 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
  */
@@ -26161,7 +26173,12 @@ var RETRYABLE_ERROR_MESSAGE_PATTERNS = [
26161
26173
  // or SubscriptionFilter role propagation. CW Logs probes the destination
26162
26174
  // by delivering a test message; if the stream is freshly ACTIVE or the
26163
26175
  // assumed role hasn't propagated, the probe fails with "Invalid request".
26164
- "Could not deliver test message"
26176
+ "Could not deliver test message",
26177
+ // SQS: same-name queue can't be re-created until 60s after a delete.
26178
+ // Hits when a stack is destroyed and re-deployed in quick succession
26179
+ // (a common dev / iteration loop). Retry recovers within ~60s instead
26180
+ // of failing the whole deploy.
26181
+ "wait 60 seconds"
26165
26182
  ];
26166
26183
  var RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
26167
26184
  function isRetryableTransientError(error, message) {
@@ -26176,6 +26193,39 @@ function isRetryableTransientError(error, message) {
26176
26193
  return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
26177
26194
  }
26178
26195
 
26196
+ // src/deployment/retry.ts
26197
+ var defaultSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
26198
+ async function withRetry(operation, logicalId, opts = {}) {
26199
+ const maxRetries = opts.maxRetries ?? 8;
26200
+ const initialDelayMs = opts.initialDelayMs ?? 1e3;
26201
+ const maxDelayMs = opts.maxDelayMs ?? 8e3;
26202
+ const sleep = opts.sleep ?? defaultSleep;
26203
+ let lastError;
26204
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
26205
+ try {
26206
+ return await operation();
26207
+ } catch (error) {
26208
+ lastError = error;
26209
+ const message = error instanceof Error ? error.message : String(error);
26210
+ const retryable = isRetryableTransientError(error, message);
26211
+ if (!retryable || attempt >= maxRetries) {
26212
+ throw error;
26213
+ }
26214
+ const delay = Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs);
26215
+ opts.logger?.debug(
26216
+ ` \u23F3 Retrying ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxRetries}) - ${message}`
26217
+ );
26218
+ for (let waited = 0; waited < delay; waited += 1e3) {
26219
+ if (opts.isInterrupted?.()) {
26220
+ throw opts.onInterrupted ? opts.onInterrupted() : new Error("Interrupted");
26221
+ }
26222
+ await sleep(Math.min(1e3, delay - waited));
26223
+ }
26224
+ }
26225
+ }
26226
+ throw lastError;
26227
+ }
26228
+
26179
26229
  // src/deployment/deploy-engine.ts
26180
26230
  var InterruptedError = class extends Error {
26181
26231
  constructor() {
@@ -27162,32 +27212,20 @@ var DeployEngine = class {
27162
27212
  );
27163
27213
  }
27164
27214
  /**
27165
- * Execute an operation with retry for transient IAM propagation errors
27166
- */
27167
- async withRetry(operation, logicalId, maxRetries = 8, initialDelayMs = 1e4) {
27168
- let lastError;
27169
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
27170
- try {
27171
- return await operation();
27172
- } catch (error) {
27173
- lastError = error;
27174
- const message = error instanceof Error ? error.message : String(error);
27175
- const isRetryable = isRetryableTransientError(error, message);
27176
- if (!isRetryable || attempt >= maxRetries) {
27177
- throw error;
27178
- }
27179
- const delay = initialDelayMs * Math.pow(2, attempt);
27180
- this.logger.debug(
27181
- ` \u23F3 Retrying ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxRetries}) - ${message}`
27182
- );
27183
- for (let waited = 0; waited < delay; waited += 1e3) {
27184
- if (this.interrupted)
27185
- throw new InterruptedError();
27186
- await new Promise((resolve4) => setTimeout(resolve4, Math.min(1e3, delay - waited)));
27187
- }
27188
- }
27189
- }
27190
- throw lastError;
27215
+ * Execute an operation with retry for transient IAM propagation errors.
27216
+ *
27217
+ * Thin wrapper over `withRetry` from ./retry.js that injects this engine's
27218
+ * SIGINT-aware interrupt check and logger. The actual backoff schedule
27219
+ * lives there.
27220
+ */
27221
+ async withRetry(operation, logicalId, maxRetries, initialDelayMs) {
27222
+ return withRetry(operation, logicalId, {
27223
+ ...maxRetries !== void 0 && { maxRetries },
27224
+ ...initialDelayMs !== void 0 && { initialDelayMs },
27225
+ logger: this.logger,
27226
+ isInterrupted: () => this.interrupted,
27227
+ onInterrupted: () => new InterruptedError()
27228
+ });
27191
27229
  }
27192
27230
  /**
27193
27231
  * Resolve stack outputs from template and resource attributes
@@ -28063,6 +28101,337 @@ function createForceUnlockCommand() {
28063
28101
  return cmd;
28064
28102
  }
28065
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
+
28066
28435
  // src/cli/index.ts
28067
28436
  var SUBCOMMANDS = /* @__PURE__ */ new Set([
28068
28437
  "bootstrap",
@@ -28073,7 +28442,8 @@ var SUBCOMMANDS = /* @__PURE__ */ new Set([
28073
28442
  "diff",
28074
28443
  "destroy",
28075
28444
  "publish-assets",
28076
- "force-unlock"
28445
+ "force-unlock",
28446
+ "state"
28077
28447
  ]);
28078
28448
  function reorderArgs(argv) {
28079
28449
  const prefix = argv.slice(0, 2);
@@ -28086,8 +28456,8 @@ function reorderArgs(argv) {
28086
28456
  return [...prefix, ...cmdAndAfter, ...beforeCmd];
28087
28457
  }
28088
28458
  async function main() {
28089
- const program = new Command9();
28090
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.5.0");
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");
28091
28461
  program.addCommand(createBootstrapCommand());
28092
28462
  program.addCommand(createSynthCommand());
28093
28463
  program.addCommand(createListCommand());
@@ -28096,6 +28466,7 @@ async function main() {
28096
28466
  program.addCommand(createDestroyCommand());
28097
28467
  program.addCommand(createPublishAssetsCommand());
28098
28468
  program.addCommand(createForceUnlockCommand());
28469
+ program.addCommand(createStateCommand());
28099
28470
  const args = reorderArgs(process.argv);
28100
28471
  await program.parseAsync(args);
28101
28472
  }