@go-to-k/cdkd 0.157.0 → 0.158.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/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { AddPermissionCommand, CreateEventSourceMappingCommand, CreateFunctionCo
12
12
  import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
13
13
  import { AssociateRouteTableCommand, AttachInternetGatewayCommand, AuthorizeSecurityGroupEgressCommand, AuthorizeSecurityGroupIngressCommand, CreateInternetGatewayCommand, CreateNatGatewayCommand, CreateNetworkAclCommand, CreateNetworkAclEntryCommand, CreateRouteCommand, CreateRouteTableCommand, CreateSecurityGroupCommand, CreateSubnetCommand, CreateTagsCommand, CreateVpcCommand, DeleteInternetGatewayCommand, DeleteNatGatewayCommand, DeleteNetworkAclCommand, DeleteNetworkAclEntryCommand, DeleteNetworkInterfaceCommand, DeleteRouteCommand, DeleteRouteTableCommand, DeleteSecurityGroupCommand, DeleteSubnetCommand, DeleteTagsCommand, DeleteVpcCommand, DescribeAvailabilityZonesCommand, DescribeInstanceAttributeCommand, DescribeInstancesCommand, DescribeInternetGatewaysCommand, DescribeNatGatewaysCommand, DescribeNetworkAclsCommand, DescribeNetworkInterfacesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVolumesCommand, DescribeVpcAttributeCommand, DescribeVpcsCommand, DetachInternetGatewayCommand, DisassociateRouteTableCommand, EC2Client, ModifyInstanceAttributeCommand, ModifySubnetAttributeCommand, ModifyVpcAttributeCommand, ReplaceNetworkAclAssociationCommand, RevokeSecurityGroupEgressCommand, RevokeSecurityGroupIngressCommand, RunInstancesCommand, TerminateInstancesCommand, waitUntilInstanceRunning, waitUntilInstanceTerminated, waitUntilNatGatewayAvailable, waitUntilNatGatewayDeleted } from "@aws-sdk/client-ec2";
14
14
  import { CreateTableCommand, DeleteTableCommand, DescribeContinuousBackupsCommand, DescribeContributorInsightsCommand, DescribeKinesisStreamingDestinationCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDBClient, ListTablesCommand, ListTagsOfResourceCommand, ResourceNotFoundException as ResourceNotFoundException$1, TagResourceCommand as TagResourceCommand$2, UntagResourceCommand as UntagResourceCommand$2, UpdateTableCommand, UpdateTimeToLiveCommand } from "@aws-sdk/client-dynamodb";
15
- import { CloudFormationClient, CreateChangeSetCommand, DeleteChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, DescribeStackEventsCommand, DescribeStackResourcesCommand, DescribeStacksCommand, DescribeTypeCommand, ExecuteChangeSetCommand, GetTemplateCommand, UpdateStackCommand, waitUntilChangeSetCreateComplete, waitUntilStackDeleteComplete, waitUntilStackImportComplete, waitUntilStackUpdateComplete } from "@aws-sdk/client-cloudformation";
15
+ import { CloudFormationClient, CreateChangeSetCommand, DeleteChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, DescribeStackEventsCommand, DescribeStackResourcesCommand, DescribeStacksCommand, DescribeTypeCommand, ExecuteChangeSetCommand, GetTemplateCommand, ListExportsCommand, UpdateStackCommand, waitUntilChangeSetCreateComplete, waitUntilStackDeleteComplete, waitUntilStackImportComplete, waitUntilStackUpdateComplete } from "@aws-sdk/client-cloudformation";
16
16
  import { APIGatewayClient, CreateAuthorizerCommand, CreateDeploymentCommand, CreateResourceCommand, CreateStageCommand, DeleteAuthorizerCommand, DeleteDeploymentCommand, DeleteMethodCommand, DeleteResourceCommand, DeleteStageCommand, GetAccountCommand, GetAuthorizerCommand, GetDeploymentCommand, GetMethodCommand, GetResourceCommand, GetStageCommand, NotFoundException as NotFoundException$1, PutIntegrationCommand, PutIntegrationResponseCommand, PutMethodCommand, PutMethodResponseCommand, TagResourceCommand as TagResourceCommand$3, UntagResourceCommand as UntagResourceCommand$3, UpdateAccountCommand, UpdateAuthorizerCommand, UpdateMethodCommand, UpdateStageCommand } from "@aws-sdk/client-api-gateway";
17
17
  import { CreateEventBusCommand, DeleteEventBusCommand, DeleteRuleCommand, DescribeEventBusCommand, DescribeRuleCommand, EventBridgeClient, ListEventBusesCommand, ListRulesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$1, ListTargetsByRuleCommand, PutRuleCommand, PutTargetsCommand, RemoveTargetsCommand, ResourceNotFoundException as ResourceNotFoundException$2, TagResourceCommand as TagResourceCommand$4, UntagResourceCommand as UntagResourceCommand$4, UpdateEventBusCommand } from "@aws-sdk/client-eventbridge";
18
18
  import { CreateSecretCommand, DeleteSecretCommand, DescribeSecretCommand, GetSecretValueCommand, ListSecretsCommand, RemoveRegionsFromReplicationCommand, ReplicateSecretToRegionsCommand, ResourceNotFoundException as ResourceNotFoundException$3, SecretsManagerClient, TagResourceCommand as TagResourceCommand$5, UntagResourceCommand as UntagResourceCommand$5, UpdateSecretCommand } from "@aws-sdk/client-secrets-manager";
@@ -41485,6 +41485,418 @@ async function buildCrossStackResolver(consumerRegion, opts) {
41485
41485
  };
41486
41486
  }
41487
41487
 
41488
+ //#endregion
41489
+ //#region src/local/s3-local-state-provider.ts
41490
+ /**
41491
+ * `S3LocalStateProvider` — implementation of {@link LocalStateProvider}
41492
+ * backed by cdkd's S3 state. Wraps the existing
41493
+ * `loadStateForStack` + `buildCrossStackResolver` helpers in
41494
+ * `src/cli/commands/local-state-loader.ts` so the four `cdkd local *`
41495
+ * commands can route both `--from-state` and `--from-cfn-stack`
41496
+ * through the same provider-shaped interface (issue #606).
41497
+ *
41498
+ * Behavior is identical to the pre-issue-#606 code path — this file
41499
+ * exists ONLY to give the CLI layer a single interface against which
41500
+ * to wire both flags, so adding a third state source (a future
41501
+ * `--from-tf-state`? out of scope for now) doesn't require touching
41502
+ * the four `local-*.ts` command files again.
41503
+ */
41504
+ var S3LocalStateProvider = class {
41505
+ label = "--from-state";
41506
+ opts;
41507
+ disposers = [];
41508
+ constructor(opts) {
41509
+ this.opts = opts;
41510
+ }
41511
+ async load(stackName, synthRegion) {
41512
+ const loaded = await loadStateForStack(stackName, synthRegion, {
41513
+ statePrefix: this.opts.statePrefix,
41514
+ ...this.opts.stackRegion !== void 0 && { stackRegion: this.opts.stackRegion },
41515
+ ...this.opts.stateBucket !== void 0 && { stateBucket: this.opts.stateBucket },
41516
+ ...this.opts.region !== void 0 && { region: this.opts.region },
41517
+ ...this.opts.profile !== void 0 && { profile: this.opts.profile }
41518
+ });
41519
+ if (!loaded) return void 0;
41520
+ const outputs = {};
41521
+ for (const [k, v] of Object.entries(loaded.state.outputs ?? {})) if (typeof v === "string") outputs[k] = v;
41522
+ else if (typeof v === "number" || typeof v === "boolean") outputs[k] = String(v);
41523
+ else outputs[k] = JSON.stringify(v);
41524
+ return {
41525
+ resources: loaded.state.resources,
41526
+ outputs,
41527
+ region: loaded.region
41528
+ };
41529
+ }
41530
+ async buildCrossStackResolver(consumerRegion) {
41531
+ const built = await buildCrossStackResolver(consumerRegion, {
41532
+ statePrefix: this.opts.statePrefix,
41533
+ ...this.opts.stateBucket !== void 0 && { stateBucket: this.opts.stateBucket },
41534
+ ...this.opts.region !== void 0 && { region: this.opts.region },
41535
+ ...this.opts.profile !== void 0 && { profile: this.opts.profile }
41536
+ });
41537
+ if (!built) return void 0;
41538
+ this.disposers.push(built.dispose);
41539
+ return built.resolver;
41540
+ }
41541
+ dispose() {
41542
+ while (this.disposers.length > 0) {
41543
+ const fn = this.disposers.pop();
41544
+ if (fn) try {
41545
+ fn();
41546
+ } catch {}
41547
+ }
41548
+ }
41549
+ };
41550
+
41551
+ //#endregion
41552
+ //#region src/local/cfn-local-state-provider.ts
41553
+ /**
41554
+ * `CfnLocalStateProvider` — implementation of {@link LocalStateProvider}
41555
+ * backed by a deployed CloudFormation stack. Powers `cdkd local *
41556
+ * --from-cfn-stack` (issue #606).
41557
+ *
41558
+ * The shape mirrors the SAM CLI's `sam local invoke --stack-name X`
41559
+ * behavior: reach into a deployed CFn stack via `DescribeStackResources`
41560
+ * to look up physical IDs of every same-stack resource, then make those
41561
+ * IDs available to the existing `state-resolver.ts` substitution engine.
41562
+ * This lets `cdkd local *` substitute env vars / secrets / images that
41563
+ * reference deployed resources in a CDK app deployed via the upstream
41564
+ * CDK CLI (`cdk deploy` → CloudFormation) WITHOUT first migrating the
41565
+ * stack to cdkd.
41566
+ *
41567
+ * Wire-format mapping:
41568
+ *
41569
+ * - `Ref: <LogicalId>` → resolved via the synthetic `ResourceState`
41570
+ * map built from `DescribeStackResources.StackResources[]` (one
41571
+ * entry per `(LogicalResourceId, PhysicalResourceId, ResourceType)`
41572
+ * tuple).
41573
+ * - `Fn::GetAtt: [<LogicalId>, <Attr>]` → **warn-and-drop**. CFn's
41574
+ * `DescribeStackResources` does NOT return per-attribute values
41575
+ * and the v1 policy (issue #606 recommendation (a)) is to surface
41576
+ * a per-key warn instead of pulling in the full provisioning layer
41577
+ * to call provider-specific describe APIs (e.g. `GetQueueAttributes`
41578
+ * for SQS, `GetFunction` for Lambda). Users override the affected
41579
+ * env var via `--env-vars` if the value is critical.
41580
+ * - `Fn::ImportValue: <exportName>` → resolved via `ListExports`
41581
+ * (paginated). Same-region only — CFn exports are region-scoped.
41582
+ * - `Fn::GetStackOutput` → rejected with a clear pointer that the
41583
+ * intrinsic is cdkd-specific (CFn has no equivalent — exports +
41584
+ * outputs are the only cross-stack vocabulary CFn understands).
41585
+ * - Stack outputs (consumed by both `Fn::GetStackOutput` and the
41586
+ * cross-stack-resolver's index-miss fallback) → sourced from
41587
+ * `DescribeStacks.Outputs[]`.
41588
+ *
41589
+ * Region handling: the provider takes a single region at construction
41590
+ * time (the `cdkd local *` commands resolve this from
41591
+ * `--stack-region` > `--region` > `AWS_REGION` > the synth-derived
41592
+ * region per the existing `--from-state` precedence). Cross-region
41593
+ * `Fn::ImportValue` is out of scope for v1 (CFn's `ListExports` is
41594
+ * region-scoped; a future PR can add a multi-region scan if real
41595
+ * usage justifies it).
41596
+ *
41597
+ * AWS API contract notes:
41598
+ *
41599
+ * - `DescribeStackResources` is unpaginated up to 500 resources (CFn's
41600
+ * hard stack cap). One call suffices for the entire stack.
41601
+ * - `DescribeStacks` is unpaginated when called with `StackName`.
41602
+ * - `ListExports` is paginated; the provider walks `NextToken` until
41603
+ * the page set is exhausted.
41604
+ */
41605
+ var CfnLocalStateProvider = class {
41606
+ label = "--from-cfn-stack";
41607
+ cfnStackName;
41608
+ region;
41609
+ client;
41610
+ clientOptions;
41611
+ constructor(opts) {
41612
+ this.cfnStackName = opts.cfnStackName;
41613
+ this.region = opts.region;
41614
+ this.clientOptions = { region: opts.region };
41615
+ if (opts.profile !== void 0) this.clientOptions.profile = opts.profile;
41616
+ }
41617
+ getClient() {
41618
+ if (!this.client) this.client = new CloudFormationClient({ region: this.region });
41619
+ return this.client;
41620
+ }
41621
+ /**
41622
+ * Load the deployed CFn stack's resources + outputs and return them
41623
+ * as a synthetic `LocalStateRecord` (matching the shape the existing
41624
+ * S3-state-driven path produces). `synthRegion` is accepted for
41625
+ * interface parity with the S3 provider but ignored here — the
41626
+ * provider is region-bound at construction time.
41627
+ *
41628
+ * Best-effort: on any CFn API failure (stack not found, access
41629
+ * denied, throttling) the provider logs a warn and returns
41630
+ * `undefined`. The caller then falls back to the PR 1 warn-and-drop
41631
+ * behavior on every intrinsic-valued env var.
41632
+ */
41633
+ async load(_stackName, _synthRegion) {
41634
+ const logger = getLogger();
41635
+ const client = this.getClient();
41636
+ let resourceMap;
41637
+ try {
41638
+ resourceMap = buildResourceStateMap((await client.send(new DescribeStackResourcesCommand({ StackName: this.cfnStackName }))).StackResources ?? []);
41639
+ } catch (err) {
41640
+ logger.warn(`${this.label}: DescribeStackResources(${this.cfnStackName}) failed: ${err instanceof Error ? err.message : String(err)}. Was the stack deployed in region '${this.region}'? Falling back.`);
41641
+ return;
41642
+ }
41643
+ let outputs;
41644
+ try {
41645
+ const stack = (await client.send(new DescribeStacksCommand({ StackName: this.cfnStackName }))).Stacks?.[0];
41646
+ if (!stack) {
41647
+ logger.warn(`${this.label}: DescribeStacks(${this.cfnStackName}) returned no stack; outputs will be empty.`);
41648
+ outputs = {};
41649
+ } else outputs = buildOutputsMap(stack.Outputs ?? []);
41650
+ } catch (err) {
41651
+ logger.warn(`${this.label}: DescribeStacks(${this.cfnStackName}) failed: ${err instanceof Error ? err.message : String(err)}. Outputs will be empty (Fn::GetStackOutput cannot resolve).`);
41652
+ outputs = {};
41653
+ }
41654
+ return {
41655
+ resources: resourceMap,
41656
+ outputs,
41657
+ region: this.region
41658
+ };
41659
+ }
41660
+ /**
41661
+ * Build a `CrossStackResolver` that resolves `Fn::ImportValue` via
41662
+ * `cloudformation:ListExports`. `Fn::GetStackOutput` is rejected here
41663
+ * — it's a cdkd-specific intrinsic with no CFn-side equivalent, and
41664
+ * the user-visible error message names the right intrinsic
41665
+ * (`Fn::ImportValue`) for that use case.
41666
+ *
41667
+ * `consumerRegion` is accepted for interface parity with the S3
41668
+ * provider but the `CfnLocalStateProvider` only resolves exports in
41669
+ * the region the stack lives in (which is the same region the
41670
+ * consumer Lambda runs in for the common single-region use case).
41671
+ * A future PR can extend this to multi-region by walking the SDK's
41672
+ * partition-aware region list.
41673
+ */
41674
+ async buildCrossStackResolver(_consumerRegion) {
41675
+ const logger = getLogger();
41676
+ const client = this.getClient();
41677
+ const label = this.label;
41678
+ const region = this.region;
41679
+ let cachedExports;
41680
+ const ensureExports = async () => {
41681
+ if (cachedExports) return cachedExports;
41682
+ const result = await fetchAllExports(client).catch((err) => {
41683
+ logger.warn(`${label}: ListExports (${region}) failed: ${err instanceof Error ? err.message : String(err)}. Fn::ImportValue intrinsics will warn-and-drop.`);
41684
+ });
41685
+ if (result) cachedExports = result;
41686
+ return result;
41687
+ };
41688
+ return {
41689
+ async resolveImport(exportName) {
41690
+ const map = await ensureExports();
41691
+ if (!map) return void 0;
41692
+ return map.get(exportName);
41693
+ },
41694
+ async resolveGetStackOutput(producerStack, producerRegion, outputName) {
41695
+ logger.warn(`${label}: Fn::GetStackOutput '${producerStack}.${outputName}' (${producerRegion}) is a cdkd-specific intrinsic with no CloudFormation equivalent. Use Fn::ImportValue against an exported output instead, or deploy the producer stack via cdkd deploy and use --from-state.`);
41696
+ }
41697
+ };
41698
+ }
41699
+ dispose() {
41700
+ if (this.client) {
41701
+ this.client.destroy();
41702
+ this.client = void 0;
41703
+ }
41704
+ }
41705
+ };
41706
+ /**
41707
+ * Build the synthetic per-logical-id resource map from
41708
+ * `DescribeStackResources` output. Each `ResourceState` carries the
41709
+ * physical id (covers `Ref`) and the resource type; `attributes` is
41710
+ * left empty per issue #606's (a) recommendation — the warn-and-drop
41711
+ * policy on unresolvable `Fn::GetAtt` is the v1 contract. The other
41712
+ * `ResourceState` fields (`properties`, `dependencies`, etc.) are
41713
+ * also left empty since the substituter doesn't read them.
41714
+ *
41715
+ * Exported for unit testing.
41716
+ */
41717
+ function buildResourceStateMap(stackResources) {
41718
+ const out = {};
41719
+ for (const r of stackResources) {
41720
+ if (!r.LogicalResourceId || !r.PhysicalResourceId || !r.ResourceType) continue;
41721
+ out[r.LogicalResourceId] = {
41722
+ physicalId: r.PhysicalResourceId,
41723
+ resourceType: r.ResourceType,
41724
+ properties: {},
41725
+ attributes: {},
41726
+ dependencies: []
41727
+ };
41728
+ }
41729
+ return out;
41730
+ }
41731
+ /**
41732
+ * Build the outputs map from `DescribeStacks.Outputs[]`. CFn outputs
41733
+ * are stringly typed at the wire level (key + value, with the value
41734
+ * always a string), so the cast is safe.
41735
+ *
41736
+ * Exported for unit testing.
41737
+ */
41738
+ function buildOutputsMap(outputs) {
41739
+ const out = {};
41740
+ for (const o of outputs) {
41741
+ if (o.OutputKey === void 0 || o.OutputValue === void 0) continue;
41742
+ out[o.OutputKey] = o.OutputValue;
41743
+ }
41744
+ return out;
41745
+ }
41746
+ /**
41747
+ * Walk `ListExports` until every page is consumed and return the
41748
+ * `Name -> Value` map. Same-region only (CFn exports are
41749
+ * region-scoped); the caller picks the region at provider
41750
+ * construction time.
41751
+ *
41752
+ * Exported for unit testing.
41753
+ */
41754
+ async function fetchAllExports(client) {
41755
+ const out = /* @__PURE__ */ new Map();
41756
+ let nextToken;
41757
+ let pages = 0;
41758
+ do {
41759
+ const resp = await client.send(new ListExportsCommand({ ...nextToken !== void 0 && { NextToken: nextToken } }));
41760
+ for (const exp of resp.Exports ?? []) {
41761
+ if (exp.Name === void 0 || exp.Value === void 0) continue;
41762
+ out.set(exp.Name, exp.Value);
41763
+ }
41764
+ nextToken = resp.NextToken;
41765
+ pages += 1;
41766
+ if (pages > 50) throw new Error("ListExports pagination exceeded 50 pages — likely a malformed NextToken loop.");
41767
+ } while (nextToken !== void 0);
41768
+ return out;
41769
+ }
41770
+
41771
+ //#endregion
41772
+ //#region src/cli/commands/local-state-source.ts
41773
+ /**
41774
+ * Single-source-of-truth helper that picks a {@link LocalStateProvider}
41775
+ * for the `cdkd local *` family from CLI flags (issue #606).
41776
+ *
41777
+ * The four `cdkd local *` commands all support two mutually-exclusive
41778
+ * state-source flags:
41779
+ *
41780
+ * - `--from-state` (S3-backed; reads cdkd's state for a stack
41781
+ * deployed via `cdkd deploy`).
41782
+ * - `--from-cfn-stack [<cfn-stack-name>]` (CFn-backed; reads a
41783
+ * deployed CloudFormation stack via `DescribeStackResources`).
41784
+ *
41785
+ * This module centralizes:
41786
+ *
41787
+ * - The mutual-exclusion check (rejected at the CLI layer before any
41788
+ * synth / AWS call fires).
41789
+ * - The bare-vs-explicit `--from-cfn-stack` resolution: bare flag uses
41790
+ * the cdkd stack name; explicit value overrides. Matches the
41791
+ * `cdkd import --migrate-from-cloudformation` precedent.
41792
+ * - Region resolution for the CFn client: reuses the existing
41793
+ * `--stack-region` flag (no separate `--cfn-stack-region`) per
41794
+ * issue #606 recommendation.
41795
+ *
41796
+ * Returns `undefined` when neither flag is set — the caller skips the
41797
+ * substitution pass entirely (which is the pre-issue-#606 behavior
41798
+ * when `--from-state` was absent).
41799
+ */
41800
+ /**
41801
+ * Default cdkd stack name → CFn stack name. Matches the
41802
+ * `cdkd import --migrate-from-cloudformation` bare-form precedent:
41803
+ * bare `--from-cfn-stack` uses the cdkd stack name verbatim as the CFn
41804
+ * stack name (typical for CDK apps where the names are the same).
41805
+ * Override by passing `--from-cfn-stack <explicit-name>`.
41806
+ *
41807
+ * Exported for unit testing.
41808
+ */
41809
+ function resolveCfnStackName(fromCfnStack, cdkdStackName) {
41810
+ if (typeof fromCfnStack === "string") return fromCfnStack;
41811
+ return cdkdStackName;
41812
+ }
41813
+ /**
41814
+ * Resolve the region used for the CFn client. The CFn provider is
41815
+ * region-bound at construction time; we apply the precedence chain
41816
+ * `--stack-region` > `--region` > `AWS_REGION` > `AWS_DEFAULT_REGION`
41817
+ * > the synth-derived stack region. Throws `LocalStateSourceError`
41818
+ * when none of these signals is set — the CFn API call needs a
41819
+ * concrete region and silently picking `us-east-1` would query the
41820
+ * wrong stack environment (worst case: succeed against the wrong
41821
+ * stack and return wrong physical IDs). Distinct from
41822
+ * `loadStateForStack`'s behavior: the S3 state bucket name is
41823
+ * account-scoped (not region-scoped) and the bucket's region is
41824
+ * auto-discovered via `GetBucketLocation`, so the S3 provider can
41825
+ * tolerate a missing region. The CFn provider cannot.
41826
+ *
41827
+ * Exported for unit testing.
41828
+ */
41829
+ function resolveCfnRegion(options, synthRegion) {
41830
+ const region = options.stackRegion ?? options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion;
41831
+ if (region === void 0) throw new LocalStateSourceError("--from-cfn-stack requires a region to query CloudFormation. Set one of: --stack-region <region>, --region <region>, AWS_REGION env var, AWS_DEFAULT_REGION env var, or an env.region on the target CDK stack.");
41832
+ return region;
41833
+ }
41834
+ /**
41835
+ * Common error class for the mutual-exclusion check so the CLI layer
41836
+ * can surface a consistent error message from all four commands.
41837
+ */
41838
+ var LocalStateSourceError = class extends Error {
41839
+ constructor(message) {
41840
+ super(message);
41841
+ this.name = "LocalStateSourceError";
41842
+ }
41843
+ };
41844
+ /**
41845
+ * Pre-flight check for `--from-cfn-stack <explicit-name>` when the
41846
+ * caller will construct one provider per routed stack (`local
41847
+ * start-api` / `local start-service`). An explicit value applies to
41848
+ * the SINGLE CFn stack named — when multiple cdkd stacks are routed,
41849
+ * every one of them would query the same CFn stack, yielding silent
41850
+ * wrong-physical-id substitutions for any logical id that happens to
41851
+ * collide between the user's stacks. Reject at the CLI layer instead.
41852
+ *
41853
+ * Bare `--from-cfn-stack` (the cdkdStackName-default) is fine for
41854
+ * multi-stack: each routed stack reads its own CFn counterpart.
41855
+ * `--from-state` is also fine: cdkd's state is per-(stack, region).
41856
+ *
41857
+ * Call this from `start-api` / `start-service` BEFORE the per-stack
41858
+ * `createLocalStateProvider` loop when `routedStackCount > 1`.
41859
+ */
41860
+ function rejectExplicitCfnStackWithMultipleStacks(options, routedStackCount) {
41861
+ if (routedStackCount <= 1) return;
41862
+ if (typeof options.fromCfnStack !== "string") return;
41863
+ throw new LocalStateSourceError(`--from-cfn-stack <name> cannot be used with multiple routed stacks (got ${routedStackCount}). An explicit CFn stack name applies to one stack only and would silently mismap logical IDs across siblings. Use bare --from-cfn-stack (each cdkd stack uses its own name as the CFn stack name) or run one cdkd local invocation per stack.`);
41864
+ }
41865
+ /**
41866
+ * Pick and construct the right `LocalStateProvider` for the supplied
41867
+ * flag set. Returns `undefined` when neither flag is set (caller skips
41868
+ * the substitution pass). Throws `LocalStateSourceError` when both
41869
+ * flags are set (mutually exclusive — different state sources, asking
41870
+ * for both is ambiguous about precedence).
41871
+ *
41872
+ * `cdkdStackName` is the cdkd-side stack name the local command
41873
+ * resolved to its target — needed to apply the bare-`--from-cfn-stack`
41874
+ * default. `synthRegion` is the synth-derived stack region (`env.region`
41875
+ * on the CDK stack) — fallback for the CFn client when no explicit
41876
+ * region override is set.
41877
+ *
41878
+ * For multi-stack callers (`local start-api` / `local start-service`)
41879
+ * also invoke `rejectExplicitCfnStackWithMultipleStacks` BEFORE the
41880
+ * per-stack loop — see that helper's docstring for the rationale.
41881
+ */
41882
+ function createLocalStateProvider(options, cdkdStackName, synthRegion) {
41883
+ const cfnStackOpt = options.fromCfnStack;
41884
+ const cfnFlagPresent = cfnStackOpt !== void 0 && cfnStackOpt !== false;
41885
+ if (options.fromState && cfnFlagPresent) throw new LocalStateSourceError("--from-state and --from-cfn-stack are mutually exclusive. Use --from-state for stacks deployed via `cdkd deploy`; use --from-cfn-stack for stacks deployed via `cdk deploy` (CloudFormation).");
41886
+ if (options.fromState) return new S3LocalStateProvider({
41887
+ statePrefix: options.statePrefix,
41888
+ ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
41889
+ ...options.region !== void 0 && { region: options.region },
41890
+ ...options.profile !== void 0 && { profile: options.profile },
41891
+ ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion }
41892
+ });
41893
+ if (cfnFlagPresent) return new CfnLocalStateProvider({
41894
+ cfnStackName: resolveCfnStackName(cfnStackOpt, cdkdStackName),
41895
+ region: resolveCfnRegion(options, synthRegion),
41896
+ ...options.profile !== void 0 && { profile: options.profile }
41897
+ });
41898
+ }
41899
+
41488
41900
  //#endregion
41489
41901
  //#region src/local/intrinsic-image.ts
41490
41902
  /**
@@ -53415,7 +53827,7 @@ async function localStartApiCommand(target, options) {
53415
53827
  const m = buildCorsConfigByApiId(stack.template);
53416
53828
  for (const [k, v] of m) corsConfigByApiId.set(k, v);
53417
53829
  }
53418
- const stateByStack = options.fromState ? await loadStateForRoutedStacks(targetStacks, routes, routesWithAuth, options) : /* @__PURE__ */ new Map();
53830
+ const stateByStack = options.fromState || options.fromCfnStack !== void 0 && options.fromCfnStack !== false ? await loadStateForRoutedStacks(targetStacks, routes, routesWithAuth, options) : /* @__PURE__ */ new Map();
53419
53831
  const lambdaIds = uniqueLambdaIds(routes, routesWithAuth, webSocketApis);
53420
53832
  const specs = /* @__PURE__ */ new Map();
53421
53833
  for (let i = 0; i < lambdaIds.length; i++) {
@@ -54455,13 +54867,14 @@ function envHasIntrinsicValue$1(templateEnv) {
54455
54867
  return false;
54456
54868
  }
54457
54869
  /**
54458
- * Load cdkd's S3 state for every stack that owns a routed Lambda. Once
54870
+ * Load deployed state for every stack that owns a routed Lambda. Once
54459
54871
  * per `synthesizeAndBuild` pass (initial boot + every reload), so a
54460
54872
  * Lambda's per-spec build does not pay one round-trip per Lambda. Per-
54461
54873
  * stack failures (no state, ambiguous region, bucket resolution error)
54462
- * degrade to warn-and-fall-back via {@link loadStateForStack} the
54463
- * affected stack's reachable Lambdas behave as if `--from-state` were
54464
- * not set, while sibling stacks with loadable state still substitute.
54874
+ * degrade to warn-and-fall-back via the active `LocalStateProvider`
54875
+ * the affected stack's reachable Lambdas behave as if `--from-state` /
54876
+ * `--from-cfn-stack` were not set, while sibling stacks with loadable
54877
+ * state still substitute.
54465
54878
  *
54466
54879
  * Pseudo parameters are resolved per stack and only when at least one
54467
54880
  * reachable Lambda in that stack has an intrinsic-valued env entry
@@ -54490,24 +54903,31 @@ async function loadStateForRoutedStacks(stacks, routes, routesWithAuth, options)
54490
54903
  }
54491
54904
  return false;
54492
54905
  };
54906
+ rejectExplicitCfnStackWithMultipleStacks(options, reachableStackNames.size);
54493
54907
  for (const stackName of reachableStackNames) {
54494
54908
  const stack = stacks.find((s) => s.stackName === stackName);
54495
54909
  if (!stack) continue;
54496
- const loaded = await loadStateForStack(stack.stackName, stack.region, {
54497
- ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
54498
- ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
54499
- statePrefix: options.statePrefix,
54500
- ...options.region !== void 0 && { region: options.region },
54501
- ...options.profile !== void 0 && { profile: options.profile }
54502
- });
54503
- if (!loaded) continue;
54504
- const bundle = { state: loaded.state };
54505
- if (stackHasIntrinsicEnv(stackName)) {
54506
- const pseudo = await resolvePseudoParametersForStartApi(loaded.region, options);
54507
- if (pseudo) bundle.pseudoParameters = pseudo;
54910
+ const provider = createLocalStateProvider(options, stack.stackName, stack.region);
54911
+ if (!provider) continue;
54912
+ try {
54913
+ const loaded = await provider.load(stack.stackName, stack.region);
54914
+ if (!loaded) continue;
54915
+ const bundle = { state: {
54916
+ version: 1,
54917
+ stackName: stack.stackName,
54918
+ resources: loaded.resources,
54919
+ outputs: loaded.outputs,
54920
+ lastModified: 0
54921
+ } };
54922
+ if (stackHasIntrinsicEnv(stackName)) {
54923
+ const pseudo = await resolvePseudoParametersForStartApi(loaded.region, options);
54924
+ if (pseudo) bundle.pseudoParameters = pseudo;
54925
+ }
54926
+ out.set(stackName, bundle);
54927
+ logger.debug(`${provider.label}: loaded state for ${stackName} (${loaded.region})`);
54928
+ } finally {
54929
+ provider.dispose();
54508
54930
  }
54509
- out.set(stackName, bundle);
54510
- logger.debug(`--from-state: loaded state for ${stackName} (${loaded.region})`);
54511
54931
  }
54512
54932
  return out;
54513
54933
  }
@@ -54520,7 +54940,7 @@ async function loadStateForRoutedStacks(stacks, routes, routesWithAuth, options)
54520
54940
  * region takes priority).
54521
54941
  *
54522
54942
  * Region precedence: `--region` > `AWS_REGION` > `AWS_DEFAULT_REGION` >
54523
- * the state record's region (returned by `loadStateForStack`).
54943
+ * the state record's region (returned by the active `LocalStateProvider`).
54524
54944
  */
54525
54945
  async function resolvePseudoParametersForStartApi(stateRegion, options) {
54526
54946
  const logger = getLogger();
@@ -54584,7 +55004,7 @@ function resolveMtlsConfig(options) {
54584
55004
  * Builder for the `start-api` subcommand. Wired up by `local.ts`.
54585
55005
  */
54586
55006
  function createLocalStartApiCommand() {
54587
- const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and REST v1 AWS_IAM (SigV4 signature verification only — IAM policy evaluation is NOT emulated; see docs/local-emulation.md). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", "Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors `cdkd local invoke` / `cdkd local run-task` target syntax.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--from-state", "Read cdkd S3 state for every routed stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::Join (and AWS pseudo parameters) in Lambda env vars with the deployed physical IDs / attributes. Off by default — pre-PR warn-and-drop semantics are preserved. Turn on for stacks already deployed via cdkd deploy. Mirrors `cdkd local invoke --from-state` / `cdkd local run-task --from-state`. Re-runs against fresh state on every hot-reload firing (--watch).").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).addOption(new Option("--mtls-truststore <path>", "PEM-encoded CA bundle for client-certificate verification (mutual TLS). When set, the local server switches from HTTP to HTTPS and the TLS handshake rejects clients whose certificate doesn't chain to one of these CAs. Verified certs are surfaced on the Lambda event under requestContext.identity.clientCert (REST v1) / requestContext.authentication.clientCert (HTTP API v2). Must be set together with --mtls-cert + --mtls-key; partial flag sets are rejected. Generate a CA + server + client cert for local dev: openssl req -x509 -newkey rsa:2048 -nodes -keyout ca-key.pem -out ca.pem -subj \"/CN=cdkd-local-ca\" -days 365; openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-csr.pem -subj \"/CN=localhost\"; openssl x509 -req -in server-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365; openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-csr.pem -subj \"/CN=client\"; openssl x509 -req -in client-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365; curl --cacert ca.pem --cert client-cert.pem --key client-key.pem https://localhost:<port>/...")).addOption(new Option("--mtls-cert <path>", "PEM-encoded server certificate for mutual TLS. Self-signed is fine for local dev. Must be set together with --mtls-truststore + --mtls-key.")).addOption(new Option("--mtls-key <path>", "PEM-encoded server private key matching --mtls-cert. Must be set together with --mtls-truststore + --mtls-cert.")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(localStartApiCommand));
55007
+ const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and REST v1 AWS_IAM (SigV4 signature verification only — IAM policy evaluation is NOT emulated; see docs/local-emulation.md). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", "Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors `cdkd local invoke` / `cdkd local run-task` target syntax.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--from-state", "Read cdkd S3 state for every routed stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::Join (and AWS pseudo parameters) in Lambda env vars with the deployed physical IDs / attributes. Off by default — pre-PR warn-and-drop semantics are preserved. Turn on for stacks already deployed via cdkd deploy. Mirrors `cdkd local invoke --from-state` / `cdkd local run-task --from-state`. Re-runs against fresh state on every hot-reload firing (--watch).").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in Lambda env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name per routed stack; pass an explicit value when a single CFn stack should serve every routed stack. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).")).addOption(new Option("--mtls-truststore <path>", "PEM-encoded CA bundle for client-certificate verification (mutual TLS). When set, the local server switches from HTTP to HTTPS and the TLS handshake rejects clients whose certificate doesn't chain to one of these CAs. Verified certs are surfaced on the Lambda event under requestContext.identity.clientCert (REST v1) / requestContext.authentication.clientCert (HTTP API v2). Must be set together with --mtls-cert + --mtls-key; partial flag sets are rejected. Generate a CA + server + client cert for local dev: openssl req -x509 -newkey rsa:2048 -nodes -keyout ca-key.pem -out ca.pem -subj \"/CN=cdkd-local-ca\" -days 365; openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-csr.pem -subj \"/CN=localhost\"; openssl x509 -req -in server-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365; openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-csr.pem -subj \"/CN=client\"; openssl x509 -req -in client-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365; curl --cacert ca.pem --cert client-cert.pem --key client-key.pem https://localhost:<port>/...")).addOption(new Option("--mtls-cert <path>", "PEM-encoded server certificate for mutual TLS. Self-signed is fine for local dev. Must be set together with --mtls-truststore + --mtls-key.")).addOption(new Option("--mtls-key <path>", "PEM-encoded server private key matching --mtls-cert. Must be set together with --mtls-truststore + --mtls-cert.")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(localStartApiCommand));
54588
55008
  [
54589
55009
  ...commonOptions,
54590
55010
  ...appOptions,
@@ -55478,6 +55898,7 @@ async function localRunTaskCommand(target, options) {
55478
55898
  const state = createEcsRunState();
55479
55899
  let sigintHandler;
55480
55900
  let sigintCount = 0;
55901
+ let stateProvider;
55481
55902
  let cleanupPromise;
55482
55903
  const cleanup = async () => {
55483
55904
  if (!cleanupPromise) cleanupPromise = (async () => {
@@ -55510,34 +55931,22 @@ async function localRunTaskCommand(target, options) {
55510
55931
  ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
55511
55932
  };
55512
55933
  const { stacks } = await synthesizer.synthesize(synthOpts);
55513
- const imageContext = await buildEcsImageResolutionContext$1(target, stacks, options);
55934
+ const candidate = pickCandidateStack$1(parseEcsTarget(target).stackPattern, stacks);
55935
+ stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", candidate?.region);
55936
+ const imageContext = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
55514
55937
  const task = resolveEcsTaskTarget(target, stacks, imageContext);
55515
55938
  logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
55516
55939
  const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
55517
- let taskCrossStackDispose;
55518
- if (options.fromState && taskNeeds.needsCrossStackResolver) {
55940
+ if (stateProvider && taskNeeds.needsCrossStackResolver) {
55519
55941
  const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
55520
- const built = await buildCrossStackResolver(consumerRegion, {
55521
- ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
55522
- statePrefix: options.statePrefix,
55523
- ...options.region !== void 0 && { region: options.region },
55524
- ...options.profile !== void 0 && { profile: options.profile }
55942
+ const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
55943
+ if (resolver) await applyCrossStackResolverToTask(task, {
55944
+ resources: imageContext?.stateResources ?? {},
55945
+ ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
55946
+ consumerRegion,
55947
+ crossStackResolver: resolver
55525
55948
  });
55526
- if (built) {
55527
- taskCrossStackDispose = built.dispose;
55528
- try {
55529
- await applyCrossStackResolverToTask(task, {
55530
- resources: imageContext?.stateResources ?? {},
55531
- ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
55532
- consumerRegion,
55533
- crossStackResolver: built.resolver
55534
- });
55535
- } finally {
55536
- taskCrossStackDispose();
55537
- taskCrossStackDispose = void 0;
55538
- }
55539
- }
55540
- } else if (!options.fromState && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass --from-state to substitute them against deployed cdkd state.");
55949
+ } else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute them against deployed state.");
55541
55950
  sigintHandler = () => {
55542
55951
  sigintCount += 1;
55543
55952
  if (sigintCount >= 2) {
@@ -55583,6 +55992,7 @@ async function localRunTaskCommand(target, options) {
55583
55992
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
55584
55993
  } finally {
55585
55994
  if (sigintHandler) process.off("SIGINT", sigintHandler);
55995
+ if (stateProvider) stateProvider.dispose();
55586
55996
  if (!options.detach) await cleanup();
55587
55997
  }
55588
55998
  }
@@ -55635,19 +56045,18 @@ async function assumeTaskRole$1(roleArn, region) {
55635
56045
  *
55636
56046
  * Tier 1 (pseudo parameters) fires `sts:GetCallerIdentity` once for
55637
56047
  * `${AWS::AccountId}`; region / partition / URL suffix come from the CLI
55638
- * (`--region` → env vars → synth-derived stack region). Tier 2
55639
- * (`--from-state`) reuses the shared state-loader to pull cdkd's S3 state
55640
- * for the candidate stack same warn-and-fall-back error policy as
55641
- * `cdkd local invoke --from-state`.
56048
+ * (`--region` → env vars → synth-derived stack region). Tier 2 (state
56049
+ * load) routes through the active {@link LocalStateProvider} so both
56050
+ * `--from-state` and `--from-cfn-stack` produce the same downstream
56051
+ * context shape (issue #606).
55642
56052
  */
55643
- async function buildEcsImageResolutionContext$1(target, stacks, options) {
56053
+ async function buildEcsImageResolutionContext$1(candidate, stateProvider, options) {
55644
56054
  const logger = getLogger();
55645
- const candidate = pickCandidateStack$1(parseEcsTarget(target).stackPattern, stacks);
55646
56055
  if (!candidate) return void 0;
55647
56056
  const needs = detectEcsImageResolutionNeeds(candidate);
55648
56057
  if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
55649
56058
  const ctx = {};
55650
- const wantsPseudoForEnvOrSecret = options.fromState && needs.needsEnvOrSecretSubstitution;
56059
+ const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
55651
56060
  if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
55652
56061
  const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
55653
56062
  if (!region) logger.warn("Resolver references ${AWS::Region} but cdkd could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.");
@@ -55668,17 +56077,11 @@ async function buildEcsImageResolutionContext$1(target, stacks, options) {
55668
56077
  };
55669
56078
  }
55670
56079
  const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
55671
- if (options.fromState && wantsState) {
55672
- const loaded = await loadStateForStack(candidate.stackName, candidate.region, {
55673
- ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
55674
- ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
55675
- statePrefix: options.statePrefix,
55676
- ...options.region !== void 0 && { region: options.region },
55677
- ...options.profile !== void 0 && { profile: options.profile }
55678
- });
55679
- if (loaded) ctx.stateResources = loaded.state.resources;
55680
- } else if (!options.fromState && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass --from-state to substitute the deployed repository URI (requires the stack to have been deployed via cdkd deploy). Otherwise the resolver will surface its existing error.");
55681
- else if (!options.fromState && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass --from-state to substitute them against the deployed cdkd state. Without --from-state these entries are dropped (per-key warnings will follow).");
56080
+ if (stateProvider && wantsState) {
56081
+ const loaded = await stateProvider.load(candidate.stackName, candidate.region);
56082
+ if (loaded) ctx.stateResources = loaded.resources;
56083
+ } else if (!stateProvider && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute the deployed repository URI. Otherwise the resolver will surface its existing error.");
56084
+ else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute them against deployed state. Without a state source these entries are dropped (per-key warnings will follow).");
55682
56085
  return ctx;
55683
56086
  }
55684
56087
  function pickCandidateStack$1(stackPattern, stacks) {
@@ -55721,7 +56124,7 @@ function readEnvOverridesFile$2(filePath) {
55721
56124
  return parsed;
55722
56125
  }
55723
56126
  function createLocalRunTaskCommand() {
55724
- const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--keep-running", "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems.").default(false)).addOption(new Option("--detach", "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle.").default(false)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default — only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localRunTaskCommand));
56127
+ const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--keep-running", "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems.").default(false)).addOption(new Option("--detach", "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle.").default(false)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default — only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name; pass an explicit value when the CFn stack name differs. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).")).action(withErrorHandling(localRunTaskCommand));
55725
56128
  [
55726
56129
  ...commonOptions,
55727
56130
  ...appOptions,
@@ -56797,6 +57200,7 @@ async function localStartServiceCommand(targets, options) {
56797
57200
  warnIfDeprecatedRegion(options);
56798
57201
  const skipPull = options.pull === false;
56799
57202
  if (!targets || targets.length === 0) throw new LocalStartServiceError("cdkd local start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend'.");
57203
+ rejectExplicitCfnStackWithMultipleStacks(options, targets.length);
56800
57204
  const perTarget = targets.map((t) => ({
56801
57205
  target: t,
56802
57206
  runState: createServiceRunState()
@@ -56893,34 +57297,36 @@ async function localStartServiceCommand(targets, options) {
56893
57297
  * outer code to wait + tear down.
56894
57298
  */
56895
57299
  async function bootOneTarget(target, runState, stacks, options, discovery, skipPull) {
57300
+ const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
57301
+ const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", candidate?.region);
57302
+ try {
57303
+ return await runOneTarget(target, runState, stacks, options, discovery, skipPull, stateProvider);
57304
+ } finally {
57305
+ if (stateProvider) stateProvider.dispose();
57306
+ }
57307
+ }
57308
+ async function runOneTarget(target, runState, stacks, options, discovery, skipPull, stateProvider) {
56896
57309
  const logger = getLogger();
56897
- const imageContext = await buildEcsImageResolutionContext(target, stacks, options);
57310
+ const imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
56898
57311
  const service = resolveEcsServiceTarget(target, stacks, imageContext);
56899
57312
  logger.info(`Target: ${service.stack.stackName}/${service.serviceLogicalId} (service=${service.serviceName}, desiredCount=${service.desiredCount}, task=${service.task.taskDefinitionLogicalId})`);
56900
57313
  for (const w of service.warnings) logger.warn(w);
56901
57314
  if (service.serviceConnect) logger.info(`Service Connect: namespace='${service.serviceConnect.namespaceName}', ${service.serviceConnect.services.length} service(s) registered for peer discovery.`);
56902
57315
  if (service.serviceRegistries.length > 0) logger.info(`Cloud Map: ${service.serviceRegistries.length} ServiceRegistry binding(s).`);
56903
57316
  const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === service.stack.stackName) ?? service.stack);
56904
- if (options.fromState && taskNeeds.needsCrossStackResolver) {
57317
+ if (stateProvider && taskNeeds.needsCrossStackResolver) {
56905
57318
  const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? service.stack.region ?? "us-east-1";
56906
- const built = await buildCrossStackResolver(consumerRegion, {
56907
- ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
56908
- statePrefix: options.statePrefix,
56909
- ...options.region !== void 0 && { region: options.region },
56910
- ...options.profile !== void 0 && { profile: options.profile }
56911
- });
56912
- if (built) try {
57319
+ const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
57320
+ if (resolver) {
56913
57321
  const subContext = {
56914
57322
  resources: imageContext?.stateResources ?? {},
56915
57323
  ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
56916
57324
  consumerRegion,
56917
- crossStackResolver: built.resolver
57325
+ crossStackResolver: resolver
56918
57326
  };
56919
57327
  await applyCrossStackResolverToTask(service.task, subContext);
56920
- } finally {
56921
- built.dispose();
56922
57328
  }
56923
- } else if (!options.fromState && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass --from-state to substitute them against deployed cdkd state.");
57329
+ } else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute them against deployed state.");
56924
57330
  let assumedCredentials;
56925
57331
  let resolvedRoleArn;
56926
57332
  if (options.assumeTaskRole === true) {
@@ -56989,14 +57395,14 @@ async function assumeTaskRole(roleArn, region) {
56989
57395
  * the candidate stack picker differs because services and tasks share
56990
57396
  * the same stack-pattern grammar.
56991
57397
  */
56992
- async function buildEcsImageResolutionContext(target, stacks, options) {
57398
+ async function buildEcsImageResolutionContext(target, stacks, options, stateProvider) {
56993
57399
  const logger = getLogger();
56994
57400
  const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
56995
57401
  if (!candidate) return void 0;
56996
57402
  const needs = detectEcsImageResolutionNeeds(candidate);
56997
57403
  if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
56998
57404
  const ctx = {};
56999
- const wantsPseudoForEnvOrSecret = options.fromState && needs.needsEnvOrSecretSubstitution;
57405
+ const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
57000
57406
  if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
57001
57407
  const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
57002
57408
  if (!region) logger.warn("Resolver references ${AWS::Region} but cdkd could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.");
@@ -57017,17 +57423,11 @@ async function buildEcsImageResolutionContext(target, stacks, options) {
57017
57423
  };
57018
57424
  }
57019
57425
  const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
57020
- if (options.fromState && wantsState) {
57021
- const loaded = await loadStateForStack(candidate.stackName, candidate.region, {
57022
- ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
57023
- ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
57024
- statePrefix: options.statePrefix,
57025
- ...options.region !== void 0 && { region: options.region },
57026
- ...options.profile !== void 0 && { profile: options.profile }
57027
- });
57028
- if (loaded) ctx.stateResources = loaded.state.resources;
57029
- } else if (!options.fromState && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass --from-state to substitute the deployed repository URI.");
57030
- else if (!options.fromState && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics. Pass --from-state to substitute them against the deployed cdkd state.");
57426
+ if (stateProvider && wantsState) {
57427
+ const loaded = await stateProvider.load(candidate.stackName, candidate.region);
57428
+ if (loaded) ctx.stateResources = loaded.resources;
57429
+ } else if (!stateProvider && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute the deployed repository URI.");
57430
+ else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics. Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute them against the deployed cdkd state.");
57031
57431
  return ctx;
57032
57432
  }
57033
57433
  function pickCandidateStack(stackPattern, stacks) {
@@ -57098,7 +57498,7 @@ function parseRestartPolicy(raw) {
57098
57498
  throw new LocalStartServiceError(`--restart-policy must be one of 'on-failure', 'always', or 'none' (got '${raw}').`);
57099
57499
  }
57100
57500
  function createLocalStartServiceCommand() {
57101
- const cmd = new Command("start-service").description("Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as `cdkd local run-task`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay (Issue #460).").argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localStartServiceCommand));
57501
+ const cmd = new Command("start-service").description("Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as `cdkd local run-task`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay (Issue #460).").argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name; pass an explicit value when the CFn stack name differs. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).")).action(withErrorHandling(localStartServiceCommand));
57102
57502
  [
57103
57503
  ...commonOptions,
57104
57504
  ...appOptions,
@@ -57212,19 +57612,19 @@ async function localInvokeCommand(target, options) {
57212
57612
  let stateAudit;
57213
57613
  let templateEnv = getTemplateEnv(lambda.resource);
57214
57614
  let stateForRoleHint;
57215
- let crossStackDispose;
57216
- if (options.fromState) {
57217
- const loaded = await loadStateForStack(lambda.stack.stackName, lambda.stack.region, {
57218
- ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
57219
- ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
57220
- statePrefix: options.statePrefix,
57221
- ...options.region !== void 0 && { region: options.region },
57222
- ...options.profile !== void 0 && { profile: options.profile }
57223
- });
57615
+ const stateProvider = createLocalStateProvider(options, lambda.stack.stackName, lambda.stack.region);
57616
+ if (stateProvider) try {
57617
+ const loaded = await stateProvider.load(lambda.stack.stackName, lambda.stack.region);
57224
57618
  if (loaded) {
57225
- stateForRoleHint = loaded.state;
57619
+ stateForRoleHint = {
57620
+ version: 1,
57621
+ stackName: lambda.stack.stackName,
57622
+ resources: loaded.resources,
57623
+ outputs: loaded.outputs,
57624
+ lastModified: 0
57625
+ };
57226
57626
  const subContext = {
57227
- resources: loaded.state.resources,
57627
+ resources: loaded.resources,
57228
57628
  consumerRegion: loaded.region
57229
57629
  };
57230
57630
  if (envHasIntrinsicValue(templateEnv)) {
@@ -57232,36 +57632,24 @@ async function localInvokeCommand(target, options) {
57232
57632
  if (pseudo) subContext.pseudoParameters = pseudo;
57233
57633
  }
57234
57634
  if (envHasCrossStackIntrinsic(templateEnv)) {
57235
- const built = await buildCrossStackResolver(loaded.region, {
57236
- ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
57237
- statePrefix: options.statePrefix,
57238
- ...options.region !== void 0 && { region: options.region },
57239
- ...options.profile !== void 0 && { profile: options.profile }
57240
- });
57241
- if (built) {
57242
- subContext.crossStackResolver = built.resolver;
57243
- crossStackDispose = built.dispose;
57244
- }
57245
- }
57246
- try {
57247
- const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
57248
- templateEnv = env;
57249
- stateAudit = audit;
57250
- for (const key of audit.resolvedKeys) logger.debug(`--from-state: substituted env var ${key} from cdkd state`);
57251
- for (const { key, reason } of audit.unresolved) logger.warn(`--from-state: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
57252
- } finally {
57253
- if (crossStackDispose) {
57254
- crossStackDispose();
57255
- crossStackDispose = void 0;
57256
- }
57635
+ const resolver = await stateProvider.buildCrossStackResolver(loaded.region);
57636
+ if (resolver) subContext.crossStackResolver = resolver;
57257
57637
  }
57638
+ const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
57639
+ templateEnv = env;
57640
+ stateAudit = audit;
57641
+ const label = stateProvider.label;
57642
+ for (const key of audit.resolvedKeys) logger.debug(`${label}: substituted env var ${key}`);
57643
+ for (const { key, reason } of audit.unresolved) logger.warn(`${label}: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
57258
57644
  }
57645
+ } finally {
57646
+ stateProvider.dispose();
57259
57647
  }
57260
57648
  const overrides = readEnvOverridesFile(options.envVars);
57261
57649
  const envResult = resolveEnvVars(lambda.logicalId, templateEnv, overrides);
57262
57650
  for (const key of envResult.unresolved) {
57263
57651
  if (stateAudit && stateAudit.unresolved.some((u) => u.key === key)) continue;
57264
- logger.warn(`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.`);
57652
+ logger.warn(`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 (cdkd-deployed) / --from-cfn-stack (cdk-deployed) to recover deployed values.`);
57265
57653
  }
57266
57654
  let resolvedAssumeRoleArn;
57267
57655
  if (typeof options.assumeRole === "string") resolvedAssumeRoleArn = options.assumeRole;
@@ -57831,7 +58219,7 @@ function pickReferencedLogicalId(intrinsic) {
57831
58219
  */
57832
58220
  function createLocalCommand() {
57833
58221
  const local = new Command("local").description("Local execution of Lambda functions (RIE) and ECS task definitions (Docker required)");
57834
- const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "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). Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from cdkd state (requires --from-state); (3) `--no-assume-role` explicitly opts out (forces dev creds even with --from-state). Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-state", "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 — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localInvokeCommand));
58222
+ const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "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). Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from cdkd state (requires --from-state); (3) `--no-assume-role` explicitly opts out (forces dev creds even with --from-state). Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-state", "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 — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name; pass an explicit value when CFn stack name differs. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).")).action(withErrorHandling(localInvokeCommand));
57835
58223
  [
57836
58224
  ...commonOptions,
57837
58225
  ...appOptions,
@@ -58955,7 +59343,7 @@ function reorderArgs(argv) {
58955
59343
  */
58956
59344
  async function main() {
58957
59345
  const program = new Command();
58958
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.157.0");
59346
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.158.0");
58959
59347
  program.addCommand(createBootstrapCommand());
58960
59348
  program.addCommand(createSynthCommand());
58961
59349
  program.addCommand(createListCommand());