@go-to-k/cdkd 0.67.0 → 0.69.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
@@ -581,8 +581,9 @@ Lambda Runtime Interface Emulator (RIE). Modeled on `sam local invoke`
581
581
  but reusing cdkd's synthesis / asset / construct-path plumbing — no
582
582
  `template.yaml` to maintain, no `cdk synth | sam ...` round-trip.
583
583
 
584
- Requires Docker. v1 supports Node.js runtimes only (`nodejs18.x` /
585
- `nodejs20.x` / `nodejs22.x`); other runtimes follow in subsequent PRs.
584
+ Requires Docker. v1 supports Node.js and Python runtimes (`nodejs18.x` /
585
+ `nodejs20.x` / `nodejs22.x` / `python3.11` / `python3.12` / `python3.13`);
586
+ other runtimes follow in subsequent PRs.
586
587
 
587
588
  ```bash
588
589
  # Invoke by CDK display path (single-stack apps may omit the prefix)
@@ -606,6 +607,14 @@ cdkd local invoke MyStack/Handler --assume-role arn:aws:iam::123456789012:role/M
606
607
 
607
608
  # Attach a Node debugger
608
609
  cdkd local invoke MyStack/Handler --debug-port 9229
610
+
611
+ # After `cdkd deploy`, recover intrinsic-valued env vars (Ref / Fn::GetAtt
612
+ # / Fn::Sub) from cdkd's S3 state instead of dropping them. Off by default
613
+ # — keeps the local-only / unscoped flow safe; opt in when you want the
614
+ # handler to see the deployed physical IDs (S3 bucket names, DDB table
615
+ # names, IAM role ARNs, ...). Disambiguate with `--stack-region <region>`
616
+ # when the same stack name has state in multiple regions.
617
+ cdkd local invoke MyStack/Handler --from-state
609
618
  ```
610
619
 
611
620
  See [docs/cli-reference.md](docs/cli-reference.md#local-invoke-run-lambda-functions-locally)
package/dist/cli.js CHANGED
@@ -70177,11 +70177,191 @@ function isLiteralEnvValue(value) {
70177
70177
  return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
70178
70178
  }
70179
70179
 
70180
+ // src/local-invoke/state-resolver.ts
70181
+ function substituteAgainstState(value, resources) {
70182
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
70183
+ return { kind: "literal", value };
70184
+ }
70185
+ if (value === null || typeof value !== "object") {
70186
+ return {
70187
+ kind: "unresolved",
70188
+ reason: `unsupported value type: ${value === null ? "null" : typeof value}`
70189
+ };
70190
+ }
70191
+ const obj = value;
70192
+ const keys = Object.keys(obj);
70193
+ if (keys.length !== 1) {
70194
+ return {
70195
+ kind: "unresolved",
70196
+ reason: `expected an intrinsic with one key, got ${keys.length} keys`
70197
+ };
70198
+ }
70199
+ const intrinsic = keys[0];
70200
+ const arg = obj[intrinsic];
70201
+ if (intrinsic === "Ref") {
70202
+ return resolveRef(arg, resources);
70203
+ }
70204
+ if (intrinsic === "Fn::GetAtt") {
70205
+ return resolveGetAtt(arg, resources);
70206
+ }
70207
+ if (intrinsic === "Fn::Sub") {
70208
+ return resolveSub(arg, resources);
70209
+ }
70210
+ return {
70211
+ kind: "unresolved",
70212
+ reason: `unsupported intrinsic '${intrinsic}' (only Ref, Fn::GetAtt, Fn::Sub are wired in --from-state v1)`
70213
+ };
70214
+ }
70215
+ function resolveRef(arg, resources) {
70216
+ if (typeof arg !== "string" || arg.length === 0) {
70217
+ return { kind: "unresolved", reason: `Ref expects a non-empty logical ID, got ${typeof arg}` };
70218
+ }
70219
+ const resource = resources[arg];
70220
+ if (!resource) {
70221
+ return {
70222
+ kind: "unresolved",
70223
+ reason: `Ref '${arg}': no record in cdkd state (was the resource deployed?)`
70224
+ };
70225
+ }
70226
+ return { kind: "literal", value: resource.physicalId };
70227
+ }
70228
+ function resolveGetAtt(arg, resources) {
70229
+ let logicalId;
70230
+ let attr;
70231
+ if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string") {
70232
+ logicalId = arg[0];
70233
+ if (typeof arg[1] !== "string") {
70234
+ return {
70235
+ kind: "unresolved",
70236
+ reason: `Fn::GetAtt's second arg must be a string attribute name, got ${typeof arg[1]} (nested intrinsics in attribute names are not supported in --from-state v1)`
70237
+ };
70238
+ }
70239
+ attr = arg[1];
70240
+ } else if (typeof arg === "string") {
70241
+ const dot = arg.indexOf(".");
70242
+ if (dot <= 0 || dot === arg.length - 1) {
70243
+ return {
70244
+ kind: "unresolved",
70245
+ reason: `Fn::GetAtt string form must be '<LogicalId>.<Attribute>', got '${arg}'`
70246
+ };
70247
+ }
70248
+ logicalId = arg.slice(0, dot);
70249
+ attr = arg.slice(dot + 1);
70250
+ } else {
70251
+ return {
70252
+ kind: "unresolved",
70253
+ reason: `Fn::GetAtt expects [LogicalId, Attribute] or 'LogicalId.Attribute', got ${Array.isArray(arg) ? `array of length ${arg.length}` : typeof arg}`
70254
+ };
70255
+ }
70256
+ const resource = resources[logicalId];
70257
+ if (!resource) {
70258
+ return {
70259
+ kind: "unresolved",
70260
+ reason: `Fn::GetAtt '${logicalId}.${attr}': no record in cdkd state`
70261
+ };
70262
+ }
70263
+ const cached = resource.attributes?.[attr];
70264
+ if (cached === void 0) {
70265
+ return {
70266
+ kind: "unresolved",
70267
+ reason: `Fn::GetAtt '${logicalId}.${attr}': attribute not captured in cdkd state at deploy time`
70268
+ };
70269
+ }
70270
+ if (typeof cached === "string" || typeof cached === "number" || typeof cached === "boolean") {
70271
+ return { kind: "literal", value: cached };
70272
+ }
70273
+ return { kind: "literal", value: JSON.stringify(cached) };
70274
+ }
70275
+ function resolveSub(arg, resources) {
70276
+ let template;
70277
+ let bindings = {};
70278
+ if (typeof arg === "string") {
70279
+ template = arg;
70280
+ } else if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && arg[1] !== null && typeof arg[1] === "object" && !Array.isArray(arg[1])) {
70281
+ template = arg[0];
70282
+ bindings = arg[1];
70283
+ } else {
70284
+ return {
70285
+ kind: "unresolved",
70286
+ reason: `Fn::Sub expects a string or [string, object], got ${Array.isArray(arg) ? "malformed array" : typeof arg}`
70287
+ };
70288
+ }
70289
+ const placeholderRegex = /\$\{([^}]+)\}/g;
70290
+ const placeholders = [];
70291
+ template.replace(placeholderRegex, (_, key) => {
70292
+ placeholders.push(key);
70293
+ return "";
70294
+ });
70295
+ const resolutions = /* @__PURE__ */ new Map();
70296
+ for (const placeholder of placeholders) {
70297
+ if (resolutions.has(placeholder))
70298
+ continue;
70299
+ if (placeholder in bindings) {
70300
+ const sub = substituteAgainstState(bindings[placeholder], resources);
70301
+ if (sub.kind !== "literal") {
70302
+ return {
70303
+ kind: "unresolved",
70304
+ reason: `Fn::Sub placeholder '\${${placeholder}}': ${sub.reason}`
70305
+ };
70306
+ }
70307
+ resolutions.set(placeholder, String(sub.value));
70308
+ continue;
70309
+ }
70310
+ const dot = placeholder.indexOf(".");
70311
+ if (dot === -1) {
70312
+ const sub = resolveRef(placeholder, resources);
70313
+ if (sub.kind !== "literal") {
70314
+ return {
70315
+ kind: "unresolved",
70316
+ reason: `Fn::Sub placeholder '\${${placeholder}}': ${sub.reason}`
70317
+ };
70318
+ }
70319
+ resolutions.set(placeholder, String(sub.value));
70320
+ } else {
70321
+ const sub = resolveGetAtt(placeholder, resources);
70322
+ if (sub.kind !== "literal") {
70323
+ return {
70324
+ kind: "unresolved",
70325
+ reason: `Fn::Sub placeholder '\${${placeholder}}': ${sub.reason}`
70326
+ };
70327
+ }
70328
+ resolutions.set(placeholder, String(sub.value));
70329
+ }
70330
+ }
70331
+ const out = template.replace(placeholderRegex, (_, key) => {
70332
+ return resolutions.get(key) ?? "";
70333
+ });
70334
+ return { kind: "literal", value: out };
70335
+ }
70336
+ function substituteEnvVarsFromState(templateEnv, resources) {
70337
+ const env = {};
70338
+ const audit = { resolvedKeys: [], unresolved: [] };
70339
+ if (!templateEnv)
70340
+ return { env, audit };
70341
+ for (const [key, value] of Object.entries(templateEnv)) {
70342
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
70343
+ env[key] = value;
70344
+ continue;
70345
+ }
70346
+ const result = substituteAgainstState(value, resources);
70347
+ if (result.kind === "literal") {
70348
+ env[key] = result.value;
70349
+ audit.resolvedKeys.push(key);
70350
+ } else {
70351
+ audit.unresolved.push({ key, reason: result.reason });
70352
+ }
70353
+ }
70354
+ return { env, audit };
70355
+ }
70356
+
70180
70357
  // src/local-invoke/runtime-image.ts
70181
- var NODEJS_RUNTIMES = {
70182
- "nodejs18.x": "public.ecr.aws/lambda/nodejs:18",
70183
- "nodejs20.x": "public.ecr.aws/lambda/nodejs:20",
70184
- "nodejs22.x": "public.ecr.aws/lambda/nodejs:22"
70358
+ var SUPPORTED_RUNTIMES = {
70359
+ "nodejs18.x": { image: "public.ecr.aws/lambda/nodejs:18", fileExtension: ".js" },
70360
+ "nodejs20.x": { image: "public.ecr.aws/lambda/nodejs:20", fileExtension: ".js" },
70361
+ "nodejs22.x": { image: "public.ecr.aws/lambda/nodejs:22", fileExtension: ".js" },
70362
+ "python3.11": { image: "public.ecr.aws/lambda/python:3.11", fileExtension: ".py" },
70363
+ "python3.12": { image: "public.ecr.aws/lambda/python:3.12", fileExtension: ".py" },
70364
+ "python3.13": { image: "public.ecr.aws/lambda/python:3.13", fileExtension: ".py" }
70185
70365
  };
70186
70366
  var UnsupportedRuntimeError = class _UnsupportedRuntimeError extends Error {
70187
70367
  constructor(runtime, message) {
@@ -70192,24 +70372,30 @@ var UnsupportedRuntimeError = class _UnsupportedRuntimeError extends Error {
70192
70372
  }
70193
70373
  };
70194
70374
  function resolveRuntimeImage(runtime) {
70375
+ return resolveRuntimeSpec(runtime).image;
70376
+ }
70377
+ function resolveRuntimeFileExtension(runtime) {
70378
+ return resolveRuntimeSpec(runtime).fileExtension;
70379
+ }
70380
+ function resolveRuntimeSpec(runtime) {
70195
70381
  if (typeof runtime !== "string" || runtime.length === 0) {
70196
70382
  throw new UnsupportedRuntimeError(
70197
70383
  String(runtime),
70198
70384
  "Lambda function has no Runtime property. Container-image Lambdas (Code.ImageUri) are not supported in cdkd local invoke v1."
70199
70385
  );
70200
70386
  }
70201
- const image = NODEJS_RUNTIMES[runtime];
70202
- if (image)
70203
- return image;
70204
- if (runtime.startsWith("python") || runtime.startsWith("java") || runtime.startsWith("dotnet") || runtime.startsWith("ruby") || runtime.startsWith("go") || runtime.startsWith("provided")) {
70387
+ const spec = SUPPORTED_RUNTIMES[runtime];
70388
+ if (spec)
70389
+ return spec;
70390
+ if (runtime.startsWith("java") || runtime.startsWith("dotnet") || runtime.startsWith("ruby") || runtime.startsWith("go") || runtime.startsWith("provided")) {
70205
70391
  throw new UnsupportedRuntimeError(
70206
70392
  runtime,
70207
- `Runtime '${runtime}' is not supported in cdkd local invoke v1. Only Node.js runtimes (nodejs18.x / nodejs20.x / nodejs22.x) are supported. Python is planned for the next iteration; other runtimes follow.`
70393
+ `Runtime '${runtime}' is not supported in cdkd local invoke v1. Only Node.js (nodejs18.x / nodejs20.x / nodejs22.x) and Python (python3.11 / python3.12 / python3.13) runtimes are supported. Other runtimes follow in subsequent PRs.`
70208
70394
  );
70209
70395
  }
70210
70396
  throw new UnsupportedRuntimeError(
70211
70397
  runtime,
70212
- `Unknown runtime '${runtime}'. cdkd local invoke v1 supports nodejs18.x / nodejs20.x / nodejs22.x.`
70398
+ `Unknown runtime '${runtime}'. cdkd local invoke v1 supports nodejs18.x / nodejs20.x / nodejs22.x / python3.11 / python3.12 / python3.13.`
70213
70399
  );
70214
70400
  }
70215
70401
 
@@ -70343,9 +70529,11 @@ async function waitForRieReady(host, port, timeoutMs = 5e3) {
70343
70529
  let lastError;
70344
70530
  while (Date.now() < deadline) {
70345
70531
  try {
70346
- const ok = await tcpProbe(host, port, 500);
70347
- if (ok)
70532
+ const ok = await httpProbe(host, port, 500);
70533
+ if (ok) {
70534
+ await delay(250);
70348
70535
  return;
70536
+ }
70349
70537
  } catch (err) {
70350
70538
  lastError = err;
70351
70539
  }
@@ -70356,19 +70544,50 @@ async function waitForRieReady(host, port, timeoutMs = 5e3) {
70356
70544
  `RIE did not become ready on ${host}:${port} within ${timeoutMs}ms${tail}. The container may have exited early \u2014 check 'docker logs' output.`
70357
70545
  );
70358
70546
  }
70359
- async function invokeRie(host, port, event, timeoutMs) {
70360
- const url = `http://${host}:${port}${INVOKE_PATH}`;
70361
- const body = JSON.stringify(event ?? {});
70547
+ async function httpProbe(host, port, timeoutMs) {
70362
70548
  const controller = new AbortController();
70363
70549
  const timer = setTimeout(() => controller.abort(), timeoutMs);
70364
- let response;
70365
70550
  try {
70366
- response = await fetch(url, {
70551
+ const response = await fetch(`http://${host}:${port}/`, {
70367
70552
  method: "POST",
70368
70553
  headers: { "Content-Type": "application/json" },
70369
- body,
70554
+ body: "{}",
70370
70555
  signal: controller.signal
70371
70556
  });
70557
+ await response.text().catch(() => void 0);
70558
+ return true;
70559
+ } catch (err) {
70560
+ if (isTransientNetworkError(err))
70561
+ return false;
70562
+ throw err;
70563
+ } finally {
70564
+ clearTimeout(timer);
70565
+ }
70566
+ }
70567
+ function isTransientNetworkError(err) {
70568
+ if (!(err instanceof Error))
70569
+ return false;
70570
+ if (err.name === "AbortError")
70571
+ return true;
70572
+ if (err.name === "TypeError" && err.message === "fetch failed")
70573
+ return true;
70574
+ const cause = err.cause;
70575
+ if (cause?.code === "ECONNRESET")
70576
+ return true;
70577
+ if (cause?.code === "ECONNREFUSED")
70578
+ return true;
70579
+ if (cause?.code === "UND_ERR_SOCKET")
70580
+ return true;
70581
+ return false;
70582
+ }
70583
+ async function invokeRie(host, port, event, timeoutMs) {
70584
+ const url = `http://${host}:${port}${INVOKE_PATH}`;
70585
+ const body = JSON.stringify(event ?? {});
70586
+ const controller = new AbortController();
70587
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
70588
+ let response;
70589
+ try {
70590
+ response = await fetchWithStartupRetry(url, body, controller.signal);
70372
70591
  } catch (err) {
70373
70592
  if (err.name === "AbortError") {
70374
70593
  throw new Error(
@@ -70387,36 +70606,32 @@ async function invokeRie(host, port, event, timeoutMs) {
70387
70606
  }
70388
70607
  return { payload, raw };
70389
70608
  }
70390
- async function tcpProbe(host, port, timeoutMs) {
70391
- const { Socket } = await import("node:net");
70392
- return new Promise((resolveProbe, rejectProbe) => {
70393
- const socket = new Socket();
70394
- const cleanup = () => {
70395
- socket.removeAllListeners();
70396
- socket.destroy();
70397
- };
70398
- socket.setTimeout(timeoutMs);
70399
- socket.once("connect", () => {
70400
- cleanup();
70401
- resolveProbe(true);
70402
- });
70403
- socket.once("timeout", () => {
70404
- cleanup();
70405
- resolveProbe(false);
70406
- });
70407
- socket.once("error", (err) => {
70408
- cleanup();
70409
- if (err.code === "ECONNREFUSED" || err.code === "ECONNRESET") {
70410
- resolveProbe(false);
70411
- return;
70412
- }
70413
- rejectProbe(err);
70414
- });
70415
- socket.connect(port, host);
70416
- });
70609
+ async function fetchWithStartupRetry(url, body, signal) {
70610
+ const maxAttempts = 3;
70611
+ let lastError;
70612
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
70613
+ try {
70614
+ return await fetch(url, {
70615
+ method: "POST",
70616
+ headers: { "Content-Type": "application/json" },
70617
+ body,
70618
+ signal
70619
+ });
70620
+ } catch (err) {
70621
+ const name = err.name;
70622
+ if (name === "AbortError")
70623
+ throw err;
70624
+ lastError = err;
70625
+ if (attempt === maxAttempts)
70626
+ break;
70627
+ await delay(200);
70628
+ }
70629
+ }
70630
+ throw lastError;
70417
70631
  }
70418
70632
 
70419
70633
  // src/cli/commands/local-invoke.ts
70634
+ init_aws_clients();
70420
70635
  async function localInvokeCommand(target, options) {
70421
70636
  const logger = getLogger();
70422
70637
  if (options.verbose) {
@@ -70442,14 +70657,49 @@ async function localInvokeCommand(target, options) {
70442
70657
  const { stacks } = await synthesizer.synthesize(synthOpts);
70443
70658
  const lambda = resolveLambdaTarget(target, stacks);
70444
70659
  logger.info(`Target: ${lambda.stack.stackName}/${lambda.logicalId} (${lambda.runtime})`);
70445
- const codeDir = lambda.codePath ?? materializeInlineCode(lambda.handler, lambda.inlineCode ?? "");
70660
+ const codeDir = lambda.codePath ?? materializeInlineCode(
70661
+ lambda.handler,
70662
+ lambda.inlineCode ?? "",
70663
+ resolveRuntimeFileExtension(lambda.runtime)
70664
+ );
70665
+ let stateAudit;
70666
+ let templateEnv = getTemplateEnv(lambda.resource);
70667
+ let stateForRoleHint;
70668
+ if (options.fromState) {
70669
+ const loaded = await loadStateForStack(lambda.stack.stackName, lambda.stack.region, {
70670
+ ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
70671
+ ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
70672
+ statePrefix: options.statePrefix,
70673
+ ...options.region !== void 0 && { region: options.region },
70674
+ ...options.profile !== void 0 && { profile: options.profile }
70675
+ });
70676
+ if (loaded) {
70677
+ stateForRoleHint = loaded.state;
70678
+ const { env, audit } = substituteEnvVarsFromState(templateEnv, loaded.state.resources);
70679
+ templateEnv = env;
70680
+ stateAudit = audit;
70681
+ for (const key of audit.resolvedKeys) {
70682
+ logger.debug(`--from-state: substituted env var ${key} from cdkd state`);
70683
+ }
70684
+ for (const { key, reason } of audit.unresolved) {
70685
+ logger.warn(
70686
+ `--from-state: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`
70687
+ );
70688
+ }
70689
+ }
70690
+ }
70446
70691
  const overrides = readEnvOverridesFile(options.envVars);
70447
- const envResult = resolveEnvVars(lambda.logicalId, getTemplateEnv(lambda.resource), overrides);
70692
+ const envResult = resolveEnvVars(lambda.logicalId, templateEnv, overrides);
70448
70693
  for (const key of envResult.unresolved) {
70694
+ if (stateAudit && stateAudit.unresolved.some((u) => u.key === key))
70695
+ continue;
70449
70696
  logger.warn(
70450
- `Environment variable ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${lambda.logicalId}":{"${key}":"<literal>"}}) or wait for --from-state in PR 2.`
70697
+ `Environment variable ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${lambda.logicalId}":{"${key}":"<literal>"}}) or pass --from-state to recover deployed values.`
70451
70698
  );
70452
70699
  }
70700
+ if (options.fromState && !options.assumeRole && stateForRoleHint) {
70701
+ suggestAssumeRoleFromState(stateForRoleHint, lambda.logicalId);
70702
+ }
70453
70703
  const event = await readEvent(options);
70454
70704
  const image = resolveRuntimeImage(lambda.runtime);
70455
70705
  const dockerEnv = {
@@ -70616,18 +70866,121 @@ function forwardAwsEnv(env) {
70616
70866
  env[key] = value;
70617
70867
  }
70618
70868
  }
70619
- function materializeInlineCode(handler, source) {
70869
+ function materializeInlineCode(handler, source, fileExtension) {
70620
70870
  const lastDot = handler.lastIndexOf(".");
70621
70871
  if (lastDot <= 0) {
70622
70872
  throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
70623
70873
  }
70624
70874
  const modulePath = handler.substring(0, lastDot);
70625
70875
  const dir = mkdtempSync2(path.join(tmpdir2(), "cdkd-local-invoke-"));
70626
- const filePath = path.join(dir, `${modulePath}.js`);
70876
+ const filePath = path.join(dir, `${modulePath}${fileExtension}`);
70627
70877
  mkdirSync2(path.dirname(filePath), { recursive: true });
70628
70878
  writeFileSync5(filePath, source, "utf-8");
70629
70879
  return dir;
70630
70880
  }
70881
+ async function loadStateForStack(stackName, synthRegion, opts) {
70882
+ const logger = getLogger();
70883
+ const region = opts.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion ?? "us-east-1";
70884
+ let stateBucket;
70885
+ try {
70886
+ stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, region);
70887
+ } catch (err) {
70888
+ logger.warn(
70889
+ `--from-state: could not resolve state bucket: ${err instanceof Error ? err.message : String(err)}. Falling back to PR 1 warn-and-drop semantics.`
70890
+ );
70891
+ return void 0;
70892
+ }
70893
+ const awsClients = new AwsClients({
70894
+ ...opts.region !== void 0 && { region: opts.region },
70895
+ ...opts.profile !== void 0 && { profile: opts.profile }
70896
+ });
70897
+ setAwsClients(awsClients);
70898
+ try {
70899
+ const stateConfig = { bucket: stateBucket, prefix: opts.statePrefix };
70900
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
70901
+ ...opts.region !== void 0 && { region: opts.region },
70902
+ ...opts.profile !== void 0 && { profile: opts.profile }
70903
+ });
70904
+ await stateBackend.verifyBucketExists();
70905
+ const refs = (await stateBackend.listStacks()).filter((r) => r.stackName === stackName);
70906
+ if (refs.length === 0) {
70907
+ logger.warn(
70908
+ `--from-state: no cdkd state found for stack '${stackName}' in bucket '${stateBucket}'. Was it deployed via 'cdkd deploy'? Falling back to PR 1 warn-and-drop semantics.`
70909
+ );
70910
+ return void 0;
70911
+ }
70912
+ let targetRegion;
70913
+ if (opts.stackRegion) {
70914
+ const found = refs.find((r) => r.region === opts.stackRegion);
70915
+ if (!found) {
70916
+ const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
70917
+ logger.warn(
70918
+ `--from-state: stack '${stackName}' has no state in region '${opts.stackRegion}' (available: ${seen}). Falling back.`
70919
+ );
70920
+ return void 0;
70921
+ }
70922
+ targetRegion = opts.stackRegion;
70923
+ } else if (synthRegion && refs.some((r) => r.region === synthRegion)) {
70924
+ targetRegion = synthRegion;
70925
+ } else if (refs.length === 1) {
70926
+ targetRegion = refs[0].region ?? synthRegion ?? region;
70927
+ } else {
70928
+ const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
70929
+ logger.warn(
70930
+ `--from-state: stack '${stackName}' has state in multiple regions (${seen}). Re-run with --stack-region <region>. Falling back.`
70931
+ );
70932
+ return void 0;
70933
+ }
70934
+ const stateData = await stateBackend.getState(stackName, targetRegion);
70935
+ if (!stateData) {
70936
+ logger.warn(
70937
+ `--from-state: state record for '${stackName}' (${targetRegion}) returned empty. Falling back.`
70938
+ );
70939
+ return void 0;
70940
+ }
70941
+ logger.debug(`--from-state: loaded state for ${stackName} (${targetRegion})`);
70942
+ return { state: stateData.state, region: targetRegion };
70943
+ } finally {
70944
+ awsClients.destroy();
70945
+ }
70946
+ }
70947
+ function suggestAssumeRoleFromState(state, logicalId) {
70948
+ const logger = getLogger();
70949
+ const lambda = state.resources[logicalId];
70950
+ if (!lambda)
70951
+ return;
70952
+ const roleRef = lambda.properties?.["Role"] ?? lambda.observedProperties?.["Role"];
70953
+ let roleArn;
70954
+ if (typeof roleRef === "string" && roleRef.startsWith("arn:")) {
70955
+ roleArn = roleRef;
70956
+ } else if (typeof roleRef === "object" && roleRef !== null) {
70957
+ const refLogicalId = pickReferencedLogicalId(roleRef);
70958
+ if (refLogicalId) {
70959
+ const roleResource = state.resources[refLogicalId];
70960
+ const cached = roleResource?.attributes?.["Arn"];
70961
+ if (typeof cached === "string" && cached.startsWith("arn:")) {
70962
+ roleArn = cached;
70963
+ }
70964
+ }
70965
+ }
70966
+ if (roleArn) {
70967
+ logger.info(
70968
+ `Hint: the deployed function uses execution role ${roleArn}. Re-run with --assume-role <that-arn> to invoke under the deployed function's narrow permissions.`
70969
+ );
70970
+ }
70971
+ }
70972
+ function pickReferencedLogicalId(intrinsic) {
70973
+ if ("Ref" in intrinsic && typeof intrinsic["Ref"] === "string")
70974
+ return intrinsic["Ref"];
70975
+ if ("Fn::GetAtt" in intrinsic) {
70976
+ const arg = intrinsic["Fn::GetAtt"];
70977
+ if (Array.isArray(arg) && typeof arg[0] === "string")
70978
+ return arg[0];
70979
+ if (typeof arg === "string")
70980
+ return arg.split(".")[0];
70981
+ }
70982
+ return void 0;
70983
+ }
70631
70984
  function createLocalCommand() {
70632
70985
  const local = new Command14("local").description(
70633
70986
  "Local Lambda execution against the AWS Lambda Runtime Interface Emulator (Docker required)"
@@ -70646,8 +70999,20 @@ function createLocalCommand() {
70646
70999
  "--assume-role <arn>",
70647
71000
  `Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions (closes the "developer admin / function narrow" skew). Off by default \u2014 when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default).`
70648
71001
  )
71002
+ ).addOption(
71003
+ new Option7(
71004
+ "--from-state",
71005
+ "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default \u2014 keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy."
71006
+ ).default(false)
71007
+ ).addOption(
71008
+ new Option7(
71009
+ "--stack-region <region>",
71010
+ "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions)."
71011
+ )
70649
71012
  ).action(withErrorHandling(localInvokeCommand));
70650
- [...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => invoke.addOption(opt));
71013
+ [...commonOptions, ...appOptions, ...contextOptions, ...stateOptions].forEach(
71014
+ (opt) => invoke.addOption(opt)
71015
+ );
70651
71016
  invoke.addOption(deprecatedRegionOption);
70652
71017
  local.addCommand(invoke);
70653
71018
  return local;
@@ -70682,7 +71047,7 @@ function reorderArgs(argv) {
70682
71047
  }
70683
71048
  async function main() {
70684
71049
  const program = new Command15();
70685
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.67.0");
71050
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.69.0");
70686
71051
  program.addCommand(createBootstrapCommand());
70687
71052
  program.addCommand(createSynthCommand());
70688
71053
  program.addCommand(createListCommand());