cdk-local 0.73.6 → 0.75.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.
@@ -1,4 +1,4 @@
1
- import { a as runDockerStreaming, c as getEmbedConfig, i as runDockerForeground, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, s as getLogger, u as setEmbedConfig } from "./docker-cmd-voNPrcRh.js";
1
+ import { a as runDockerStreaming, c as getEmbedConfig, i as runDockerForeground, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, s as getLogger, u as setEmbedConfig } from "./docker-cmd-Dqx2YENO.js";
2
2
  import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
4
  import * as path$1 from "node:path";
@@ -113,6 +113,35 @@ function parseAssumeRoleToken(raw, previous) {
113
113
  return acc;
114
114
  }
115
115
  /**
116
+ * Compose `--assume-role` (value-form accumulator) and the separate
117
+ * `--assume-role-auto` boolean flag into the in-process
118
+ * representation `localStartApiCommand` consumes downstream.
119
+ *
120
+ * - both unset -> `undefined` (pass dev creds through)
121
+ * - only `--assume-role-auto` -> `{ perLambda: {}, bareAutoResolve: true }`
122
+ * (per-Lambda auto-resolve for every routed Lambda)
123
+ * - only `--assume-role` -> the value-form accumulator (global ARN
124
+ * and/or per-Lambda map) as-is
125
+ * - both -> accumulator with `bareAutoResolve = true` overlaid;
126
+ * the per-Lambda map still wins for named Lambdas, auto-resolve
127
+ * fills in for every Lambda the map does NOT name
128
+ *
129
+ * Enforces the mutual-exclusion guard: `--assume-role-auto` and a
130
+ * global ARN (`--assume-role <arn>`) both occupy the "default for
131
+ * every other Lambda" slot, so the combination is ambiguous and
132
+ * rejected at boot with a clear error. The per-Lambda map IS
133
+ * compatible with either side.
134
+ */
135
+ function normalizeStartApiAssumeRole(raw, autoResolve) {
136
+ if (raw === void 0) return autoResolve ? {
137
+ perLambda: {},
138
+ bareAutoResolve: true
139
+ } : void 0;
140
+ if (autoResolve && raw.globalArn) throw new Error(`--assume-role-auto auto-resolves EACH routed Lambda's own execution role, but --assume-role ${raw.globalArn} also names a single global default. These are mutually exclusive on the global slot. Either drop the global ARN to keep --assume-role-auto for every Lambda, or drop --assume-role-auto to keep the global default. Per-Lambda overrides (--assume-role <LogicalId>=<arn>) are compatible with either side.`);
141
+ if (autoResolve) raw.bareAutoResolve = true;
142
+ return raw;
143
+ }
144
+ /**
116
145
  * Resolve the effective IAM role ARN for a given Lambda. Per-Lambda
117
146
  * override wins; otherwise the global default; otherwise `undefined`
118
147
  * (no role to assume — pass developer creds through).
@@ -6498,7 +6527,10 @@ async function pullImage(image, skipPull) {
6498
6527
  }
6499
6528
  logger.debug(`Pulling ${image} (silent — pass --verbose to stream progress)`);
6500
6529
  try {
6501
- await runDockerStreaming(["pull", image], { streamLive: false });
6530
+ await runDockerStreaming(["pull", image], {
6531
+ streamLive: false,
6532
+ progressLabel: `Pulling ${image}`
6533
+ });
6502
6534
  } catch (err) {
6503
6535
  const e = err;
6504
6536
  if (e.exitCode === void 0 || e.exitCode === null) throw new DockerRunnerError(`docker pull ${image} failed: ${e.message ?? String(err)}`);
@@ -6773,7 +6805,8 @@ async function buildDockerImage(asset, cdkOutDir, options) {
6773
6805
  try {
6774
6806
  await runDockerStreaming(buildArgs, {
6775
6807
  cwd: contextDir,
6776
- env: { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" }
6808
+ env: { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" },
6809
+ ...options.progressLabel !== void 0 && { progressLabel: options.progressLabel }
6777
6810
  });
6778
6811
  } catch (err) {
6779
6812
  const e = err;
@@ -7080,7 +7113,8 @@ async function buildContainerImage(asset, cdkOutDir, options) {
7080
7113
  const actualTag = await buildDockerImage(asset, cdkOutDir, {
7081
7114
  tag,
7082
7115
  platform,
7083
- wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for container Lambda asset (${asset.source.directory ?? asset.source.executable?.join(" ")}): ${stderr}`)
7116
+ wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for container Lambda asset (${asset.source.directory ?? asset.source.executable?.join(" ")}): ${stderr}`),
7117
+ progressLabel: `Building container image (platform=${platform})`
7084
7118
  });
7085
7119
  if (actualTag !== tag) {
7086
7120
  logger.debug(`Re-tagging executable-built image '${actualTag}' → '${tag}'`);
@@ -15260,6 +15294,9 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
15260
15294
  const interactivePicked = { value: false };
15261
15295
  const allStacksConflictList = allStacksConflicts(options, targets, apiFilters);
15262
15296
  if (allStacksConflictList.length > 0) throw new Error(`--all-stacks serves every stack's API and cannot be combined with a target subset selector (${allStacksConflictList.join(", ")}). Drop --all-stacks to target specific APIs, or drop the selector to serve them all. The bare --from-cfn-stack flag (no value) IS compatible with --all-stacks.`);
15297
+ const normalizedAssumeRole = normalizeStartApiAssumeRole(options.assumeRole, options.assumeRoleAuto ?? false);
15298
+ if (normalizedAssumeRole === void 0) delete options.assumeRole;
15299
+ else options.assumeRole = normalizedAssumeRole;
15263
15300
  await applyRoleArnIfSet({
15264
15301
  roleArn: options.roleArn,
15265
15302
  region: options.region,
@@ -15269,7 +15306,8 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
15269
15306
  const appCmd = resolveApp(options.app);
15270
15307
  if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
15271
15308
  const overrides = readEnvOverridesFile$4(options.envVars);
15272
- const debugPortBase = options.debugPortBase ? parseDebugPort(options.debugPortBase) : void 0;
15309
+ const debugPortRaw = options.debugPortBase ?? options.debugPort;
15310
+ const debugPortBase = debugPortRaw ? parseDebugPort(debugPortRaw) : void 0;
15273
15311
  const perLambdaConcurrency = parsePerLambdaConcurrency(options.perLambdaConcurrency);
15274
15312
  const inlineTmpDirs = /* @__PURE__ */ new Set();
15275
15313
  const layerTmpDirs = /* @__PURE__ */ new Set();
@@ -15370,7 +15408,7 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
15370
15408
  logicalId,
15371
15409
  stacks: targetStacks,
15372
15410
  overrides,
15373
- assumeRole: options.assumeRole,
15411
+ assumeRole: normalizedAssumeRole,
15374
15412
  containerHost: options.containerHost,
15375
15413
  ...debugPortBase !== void 0 && { debugPort: debugPortBase + i },
15376
15414
  stsRegion: options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"],
@@ -15378,7 +15416,9 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
15378
15416
  layerTmpDirs,
15379
15417
  stateByStack,
15380
15418
  skipPull: options.pull === false,
15419
+ skipBuild: options.build === false,
15381
15420
  ...options.profile !== void 0 && { profile: options.profile },
15421
+ ...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
15382
15422
  ...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn },
15383
15423
  ...profileCredentials && { profileCredentials },
15384
15424
  ...profileCredsFile && { profileCredsFile },
@@ -16033,6 +16073,59 @@ function warnIamRoutes(routesWithAuth) {
16033
16073
  return true;
16034
16074
  }
16035
16075
  /**
16076
+ * Resolve the effective IAM role ARN to assume for a single routed
16077
+ * Lambda, honoring the four `--assume-role` forms of issue #256
16078
+ * Option 1 (non-breaking step):
16079
+ *
16080
+ * - per-Lambda override (`<LogicalId>=<arn>`) — wins over every other
16081
+ * form for the named Lambda.
16082
+ * - global default (bare ARN) — used when the per-Lambda map misses.
16083
+ * - bare-auto-resolve (`--assume-role` with no value) — when neither
16084
+ * the per-Lambda map nor a global default applies, resolves THIS
16085
+ * Lambda's own `Role` (its `AWS::Lambda::Function`'s `Role`
16086
+ * property -> role ARN). Resolution sources, in order:
16087
+ * 1. literal-ARN form of `Properties.Role` in the SYNTHESIZED
16088
+ * template (rare in CDK output but possible);
16089
+ * 2. deployed state (`stateBundle.state.resources`) when
16090
+ * `--from-cfn-stack` (or a host-provided extension) is
16091
+ * active, via the same shared helper
16092
+ * `resolveExecutionRoleArnFromState` `cdkl invoke`'s bare-
16093
+ * form uses.
16094
+ * - flag absent / `--no-assume-role-auto` — returns `undefined`
16095
+ * (per-Lambda map and global default already filtered out by the
16096
+ * null-check at the top; bare-auto-resolve = false short-circuits
16097
+ * before the state lookup).
16098
+ *
16099
+ * The shape-level mutual exclusion (bare-auto-resolve + global ARN
16100
+ * together) is caught at boot by
16101
+ * {@link normalizeStartApiAssumeRole}; this resolver only sees a
16102
+ * well-formed `AssumeRoleOption`.
16103
+ *
16104
+ * Bare-auto-resolve misses (no literal-ARN, no state, state did not
16105
+ * carry the role attribute) warn-and-fall-through to the
16106
+ * dev-creds-passed-through branch — same trade-off `cdkl invoke
16107
+ * --assume-role` makes when bare auto-resolve cannot find an ARN.
16108
+ *
16109
+ * Exported for unit testing.
16110
+ */
16111
+ function resolveStartApiAssumeRoleArn(args) {
16112
+ const { logicalId, assumeRole, lambdaResource, stateBundle } = args;
16113
+ if (!assumeRole) return void 0;
16114
+ const explicit = effectiveAssumeRoleArn(logicalId, assumeRole);
16115
+ if (explicit) return explicit;
16116
+ if (!assumeRole.bareAutoResolve) return void 0;
16117
+ const roleProp = (lambdaResource.Properties ?? {})["Role"];
16118
+ if (typeof roleProp === "string" && roleProp.startsWith("arn:")) return roleProp;
16119
+ if (stateBundle) {
16120
+ const fromState = resolveExecutionRoleArnFromState(stateBundle.state, logicalId);
16121
+ if (fromState) {
16122
+ getLogger().info(`--assume-role: auto-resolved execution role for '${logicalId}' from state: ${fromState}`);
16123
+ return fromState;
16124
+ }
16125
+ }
16126
+ getLogger().warn(`--assume-role: could not auto-resolve the execution role ARN for '${logicalId}'. Pair --assume-role with --from-cfn-stack (or a host-provided state-source extension) so the deployed Role's ARN can be looked up, OR pin the ARN explicitly with --assume-role ${logicalId}=<arn>. Falling back to the developer's shell credentials for this Lambda.`);
16127
+ }
16128
+ /**
16036
16129
  * Build the per-Lambda container spec — code dir, env vars (template +
16037
16130
  * --env-vars overlay), STS-issued creds when --assume-role names this
16038
16131
  * Lambda, optional --debug-port reservation. Errors out with a clear
@@ -16050,7 +16143,11 @@ async function buildContainerSpec(args) {
16050
16143
  codeDir = lambda.codePath ?? materializeInlineCode$2(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
16051
16144
  optDir = await materializeLambdaLayers$1(lambda.layers, layerTmpDirs, layerRoleArn);
16052
16145
  } else {
16053
- imageRef = (await resolveContainerImageForStartApi(lambda, skipPull, args.profile)).imageRef;
16146
+ imageRef = (await resolveContainerImageForStartApi(lambda, skipPull, args.profile, {
16147
+ ...args.skipBuild !== void 0 && { noBuild: args.skipBuild },
16148
+ ...args.ecrRoleArn !== void 0 && { ecrRoleArn: args.ecrRoleArn },
16149
+ ...args.stsRegion !== void 0 && { region: args.stsRegion }
16150
+ })).imageRef;
16054
16151
  platform = architectureToPlatform(lambda.architecture);
16055
16152
  }
16056
16153
  let templateEnv = getTemplateEnv$1(lambda.resource);
@@ -16099,7 +16196,12 @@ async function buildContainerSpec(args) {
16099
16196
  AWS_LAMBDA_LOG_STREAM_NAME: "local",
16100
16197
  ...envResult.resolved
16101
16198
  };
16102
- const roleArn = effectiveAssumeRoleArn(logicalId, assumeRole);
16199
+ const roleArn = resolveStartApiAssumeRoleArn({
16200
+ logicalId,
16201
+ assumeRole,
16202
+ lambdaResource: lambda.resource,
16203
+ stateBundle
16204
+ });
16103
16205
  if (roleArn) {
16104
16206
  const creds = await assumeLambdaExecutionRole$1(roleArn, stsRegion, profile);
16105
16207
  dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
@@ -16171,23 +16273,30 @@ async function buildContainerSpec(args) {
16171
16273
  * Resolve a container Lambda's local docker image — local build from
16172
16274
  * `cdk.out` asset manifest first, ECR-pull fallback when the asset
16173
16275
  * manifest has no matching entry. Mirrors `cdkl invoke`'s
16174
- * `resolveContainerImagePlan` shape; the start-api server doesn't
16175
- * need the no-build flag (deterministic-tag cache reuse is automatic
16176
- * across reloads because the per-Lambda tag is content-addressed).
16276
+ * `resolveContainerImagePlan` shape.
16177
16277
  *
16178
- * Same-account / same-region only on the ECR-pull path (matches the
16179
- * `cdkl invoke` PR 5 of #224 boundary). Cross-account /
16180
- * cross-region ECR pull is the W2-1 deferred follow-up.
16278
+ * Issue #249 / C7 + C8 `noBuild` and `ecrRoleArn` are accepted for
16279
+ * parity with `cdkl invoke` / `cdkl run-task`. `noBuild` short-circuits
16280
+ * the local-asset build path (the deterministic content-addressed tag
16281
+ * must already be in the local registry). `ecrRoleArn` lets the
16282
+ * ECR-pull fallback authenticate against cross-account / centralized
16283
+ * registries via STS AssumeRole — same-account / same-region pulls do
16284
+ * not need it.
16181
16285
  */
16182
- async function resolveContainerImageForStartApi(lambda, skipPull, profile) {
16286
+ async function resolveContainerImageForStartApi(lambda, skipPull, profile, options) {
16183
16287
  const logger = getLogger();
16184
16288
  const localBuild = await resolveLocalBuildPlan$1(lambda);
16185
- if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, { architecture: lambda.architecture }) };
16289
+ if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, {
16290
+ architecture: lambda.architecture,
16291
+ ...options?.noBuild !== void 0 && { noBuild: options.noBuild }
16292
+ }) };
16186
16293
  if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI ${getEmbedConfig().binaryName} can authenticate against. Re-synthesize the CDK app (so cdk.out includes the build context) or deploy the image to ECR first.`);
16187
- logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
16294
+ logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull...`);
16188
16295
  return { imageRef: await pullEcrImage(lambda.imageUri, {
16189
16296
  skipPull,
16190
- ...profile !== void 0 && { profile }
16297
+ ...profile !== void 0 && { profile },
16298
+ ...options?.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
16299
+ ...options?.region !== void 0 && { region: options.region }
16191
16300
  }) };
16192
16301
  }
16193
16302
  /**
@@ -16925,7 +17034,7 @@ function createLocalStartApiCommand(opts = {}) {
16925
17034
  * `--help` clusters. Chainable: returns `cmd`.
16926
17035
  */
16927
17036
  function addStartApiSpecificOptions(cmd) {
16928
- return cmd.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("--all-stacks", "Serve every stack's API in a multi-stack app (each API on its own port) instead of erroring out. Mutually exclusive with a positional target subset, --stack, and an explicit --from-cfn-stack <name>; the bare --from-cfn-stack flag stays compatible (binds each routed stack to its own CFn stack).").default(false)).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 the CDK app's source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). 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 <targets...> argument instead. Accepts a SINGLE identifier; for a subset pass multiple positional targets. 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-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources 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 resolved stack name per routed stack; pass an explicit value when a single CFn stack should serve every routed stack. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).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=${getEmbedConfig().resourceNamePrefix}-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("--strict-sigv4", "Opt-in: DENY AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id — e.g. a federated / Cognito Identity Pool / cross-account signer — OR no local AWS credentials configured) instead of the default warn-and-pass. DEFAULT off: cdk-local warn-and-passes unverifiable IAM requests with a placeholder principalId so local dev exercises app logic without reproducing an auth boundary it cannot fully emulate. OAC-fronted Function URLs always warn-and-pass regardless.").default(false));
17037
+ return cmd.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("--all-stacks", "Serve every stack's API in a multi-stack app (each API on its own port) instead of erroring out. Mutually exclusive with a positional target subset, --stack, and an explicit --from-cfn-stack <name>; the bare --from-cfn-stack flag stays compatible (binds each routed stack to its own CFn stack).").default(false)).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("--no-build", "Skip docker build on every container Lambda local-asset build (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("--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("--debug-port <port>", "Alias of --debug-port-base for parity with `cdkl invoke --debug-port`. Reserves a contiguous --debug-port range (one per Lambda) starting at <port>.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries on the container-Lambda ECR-pull fallback. 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("--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. Two forms: (1) `--assume-role <arn>` sets a single global default ARN used for every routed Lambda; (2) `--assume-role <LogicalId>=<arn>` (repeatable) is a per-Lambda override (wins over the global default and over --assume-role-auto for the named Lambda). Pair with --assume-role-auto to auto-resolve every other Lambda. Precedence: per-Lambda > (--assume-role-auto OR global default) > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--assume-role-auto", "Auto-resolve EACH routed Lambda's own execution role per-Lambda (literal ARN / Fn::GetAtt against template / --from-cfn-stack state). Slower boot — one STS call per Lambda — but the right shape when each Lambda's deployed role differs. Mutually exclusive with `--assume-role <arn>` (global default); errors at boot. Compatible with `--assume-role <LogicalId>=<arn>` per-Lambda overrides — the map wins for named Lambdas, auto-resolve handles the rest.").default(false)).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when the CDK app's source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). 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 <targets...> argument instead. Accepts a SINGLE identifier; for a subset pass multiple positional targets. 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-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources 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 resolved stack name per routed stack; pass an explicit value when a single CFn stack should serve every routed stack. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).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=${getEmbedConfig().resourceNamePrefix}-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("--strict-sigv4", "Opt-in: DENY AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id — e.g. a federated / Cognito Identity Pool / cross-account signer — OR no local AWS credentials configured) instead of the default warn-and-pass. DEFAULT off: cdk-local warn-and-passes unverifiable IAM requests with a placeholder principalId so local dev exercises app logic without reproducing an auth boundary it cannot fully emulate. OAC-fronted Function URLs always warn-and-pass regardless.").default(false));
16929
17038
  }
16930
17039
 
16931
17040
  //#endregion
@@ -17554,7 +17663,7 @@ function createLocalInvokeCommand(opts = {}) {
17554
17663
  * `--help` clusters. Chainable: returns `cmd`.
17555
17664
  */
17556
17665
  function addInvokeSpecificOptions(cmd) {
17557
- return cmd.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. Semantics differ by code path: ZIP Lambdas skip pulling the public Lambda base image; Container Lambdas on the local-build path are a no-op (docker build does not refresh the FROM cache by default); Container Lambdas on the ECR-pull fallback skip docker pull AND error if the image is not in the local cache (re-run without --no-pull or pre-pull manually).")).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. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from state (requires an active state source); (3) `--no-assume-role` explicitly opts out. 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. 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. 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-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources 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 resolved stack name; pass an explicit value when CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
17666
+ return cmd.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\"}, \"Parameters\": {...}})")).addOption(new Option("--no-pull", "Skip docker pull. Semantics differ by code path: ZIP Lambdas skip pulling the public Lambda base image; Container Lambdas on the local-build path are a no-op (docker build does not refresh the FROM cache by default); Container Lambdas on the ECR-pull fallback skip docker pull AND error if the image is not in the local cache (re-run without --no-pull or pre-pull manually).")).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 IP the host uses to bind the RIE port to. Must be a numeric IP (Docker rejects hostnames here). Defaults to 127.0.0.1.").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. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from state (requires an active state source); (3) `--no-assume-role` explicitly opts out. 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. 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. 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-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources 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 resolved stack name; pass an explicit value when CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
17558
17667
  }
17559
17668
 
17560
17669
  //#endregion
@@ -17616,7 +17725,7 @@ async function buildAgentCoreCodeImage(options) {
17616
17725
  "--file",
17617
17726
  dockerfilePath,
17618
17727
  options.sourceDir
17619
- ]);
17728
+ ], { progressLabel: `Building agent image (runtime=${options.runtime})` });
17620
17729
  } catch (err) {
17621
17730
  const stderr = err.stderr?.trim();
17622
17731
  throw new LocalInvokeBuildError(`docker build failed for AgentCore code artifact (${options.sourceDir})${stderr ? `: ${stderr}` : ""}`);
@@ -19219,7 +19328,7 @@ function createLocalInvokeAgentCoreCommand(opts = {}) {
19219
19328
  * in three `--help` clusters. Chainable: returns `cmd`.
19220
19329
  */
19221
19330
  function addInvokeAgentCoreSpecificOptions(cmd) {
19222
- return cmd.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("--session-id <id>", "AgentCore runtime session id header value (default: a random UUID)")).addOption(new Option("--ws", "Stream over the HTTP-protocol agent's bidirectional /ws WebSocket endpoint (on 8080) instead of POST /invocations: send --event as the first frame and print every received frame to stdout until the agent closes. Ignored for an MCP runtime.").default(false)).addOption(new Option("--ws-interactive", "REPL mode for --ws: after the initial --event frame, read additional frames from stdin (one frame per line, trailing newline stripped) and send each as a text frame until stdin EOFs (Ctrl-D) or the agent closes. Only meaningful with --ws.").default(false)).addOption(new Option("--bearer-token <jwt>", "Bearer JWT to present when the runtime declares a customJwtAuthorizer. Verified against the runtime OIDC discovery URL (signature / issuer / expiry / audience) before the container starts, then forwarded to /invocations as Authorization: Bearer <jwt>.")).addOption(new Option("--no-verify-auth", "Skip inbound JWT verification even when the runtime declares a customJwtAuthorizer (local-dev escape hatch). A --bearer-token, if given, is still forwarded.")).addOption(new Option("--sigv4", "Sign the /invocations POST with AWS SigV4 (service bedrock-agentcore) using the resolved credentials, matching the cloud default when the runtime declares no customJwtAuthorizer. Opt-in: default unsigned. Mutually exclusive with --bearer-token; ignored on a JWT-protected runtime.").default(false)).addOption(new Option("--platform <platform>", "docker --platform for the agent container (linux/amd64 or linux/arm64)").choices(["linux/amd64", "linux/arm64"]).default("linux/arm64")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for the local-build path")).addOption(new Option("--no-build", "Skip docker build on the local-asset path (use the previously-built tag). No-op for the ECR / registry pull paths.")).addOption(new Option("--container-host <host>", "Host to bind the agent port to").default("127.0.0.1")).addOption(new Option("--timeout <ms>", "Per-request timeout in milliseconds. Applied to POST /invocations, POST /mcp, and the /ws open-to-close window. Raise this for long-running agent calls that exceed the default.").default(12e4).argParser(parseTimeoutMs)).addOption(new Option("--assume-role [arn]", "Assume the runtime's execution role and forward STS-issued temp credentials to the container so the agent runs with the deployed role. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) uses the runtime's RoleArn when it is a literal ARN; (3) `--no-assume-role` opts out. Off by default — the developer's shell credentials are forwarded unchanged.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Bare form uses the resolved stack name; pass an explicit value when the CFn stack name differs.")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
19331
+ return cmd.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\"}, \"Parameters\": {...}})")).addOption(new Option("--session-id <id>", "AgentCore runtime session id header value (default: a random UUID)")).addOption(new Option("--ws", "Stream over the HTTP-protocol agent's bidirectional /ws WebSocket endpoint (on 8080) instead of POST /invocations: send --event as the first frame and print every received frame to stdout until the agent closes. Ignored for an MCP runtime.").default(false)).addOption(new Option("--ws-interactive", "REPL mode for --ws: after the initial --event frame, read additional frames from stdin (one frame per line, trailing newline stripped) and send each as a text frame until stdin EOFs (Ctrl-D) or the agent closes. Only meaningful with --ws.").default(false)).addOption(new Option("--bearer-token <jwt>", "Bearer JWT this command SUPPLIES (the supplier role) when the runtime declares a customJwtAuthorizer — `cdkl invoke-agentcore` is the local-dev client making the outbound call, so it always presents this token to its own invocation. Verified against the runtime's OIDC discovery URL (signature / issuer / expiry / audience) before the container starts, then forwarded to /invocations as Authorization: Bearer <jwt>. Contrast with `cdkl start-alb --bearer-token`, where the role is reversed — the ALB front-door RECEIVES inbound requests and injects this token only as a default when an inbound request has none (default-when-missing).")).addOption(new Option("--no-verify-auth", "Skip inbound JWT verification even when the runtime declares a customJwtAuthorizer (local-dev escape hatch). A --bearer-token, if given, is still forwarded.")).addOption(new Option("--sigv4", "Sign the /invocations POST with AWS SigV4 (service bedrock-agentcore) using the resolved credentials, matching the cloud default when the runtime declares no customJwtAuthorizer. Opt-in: default unsigned. Mutually exclusive with --bearer-token; ignored on a JWT-protected runtime.").default(false)).addOption(new Option("--platform <platform>", "docker --platform for the agent container (linux/amd64 or linux/arm64). Defaults to linux/arm64 because the cloud AgentCore Runtime requires arm64. Override to linux/amd64 only when iterating against an amd64 dev container locally; note the image will not run on the cloud runtime as-is.").choices(["linux/amd64", "linux/arm64"]).default("linux/arm64")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for the local-build path")).addOption(new Option("--no-build", "Skip docker build on the local-asset path (use the previously-built tag). No-op for the ECR / registry pull paths.")).addOption(new Option("--container-host <host>", "Host IP the host uses to bind the agent port to. Must be a numeric IP (Docker rejects hostnames here). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--timeout <ms>", "Per-request timeout in milliseconds. Applied to POST /invocations, POST /mcp, and the /ws open-to-close window. Raise this for long-running agent calls that exceed the default.").default(12e4).argParser(parseTimeoutMs)).addOption(new Option("--assume-role [arn]", "Assume the runtime's execution role and forward STS-issued temp credentials to the container so the agent runs with the deployed role. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) uses the runtime's RoleArn when it is a literal ARN; (3) `--no-assume-role` opts out. Off by default — the developer's shell credentials are forwarded unchanged.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Bare form uses the resolved stack name; pass an explicit value when the CFn stack name differs.")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
19223
19332
  }
19224
19333
 
19225
19334
  //#endregion
@@ -20130,10 +20239,19 @@ async function prepareOneImage(task, container, options) {
20130
20239
  else if (entries.length === 1) asset = entries[0][1];
20131
20240
  if (!asset) throw new EcsTaskRunnerError(`Container '${container.name}' references a CDK asset image but no matching entry was found in cdk.out. Re-synthesize the CDK app and retry.`);
20132
20241
  const tag = `${getEmbedConfig().resourceNamePrefix}-run-task-${(image.assetHash ?? "single").slice(0, 16)}`;
20242
+ if (options.skipBuild === true) {
20243
+ const logger = getLogger().child("ecs-runner");
20244
+ if (image.assetHash === void 0) throw new LocalInvokeBuildError(`Cannot honor --no-build for ECS container '${container.name}': the asset has no stable assetHash (synthetic tag '${tag}' may collide with unrelated prior runs). Drop --no-build, or re-synthesize with an explicit assetHash on the ContainerImage.fromAsset call.`);
20245
+ logger.info(`Skipping docker build (--no-build) for container '${container.name}'; verifying ${tag} is in local registry...`);
20246
+ if (!await isImageInLocalCache(tag)) throw new LocalInvokeBuildError(`image '${tag}' not in local registry and --no-build is set (ECS container '${container.name}'); remove --no-build or run \`docker build\` manually first.`);
20247
+ logger.debug(`Local tag ${tag} is cached; skipping build for container '${container.name}'.`);
20248
+ return tag;
20249
+ }
20133
20250
  const actualTag = await buildDockerImage(asset, cdkOutDir, {
20134
20251
  tag,
20135
20252
  ...options.platformOverride !== void 0 && { platform: options.platformOverride },
20136
- wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for ECS container '${container.name}' (${asset.source.directory ?? asset.source.executable?.join(" ")}): ${stderr}`)
20253
+ wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for ECS container '${container.name}' (${asset.source.directory ?? asset.source.executable?.join(" ")}): ${stderr}`),
20254
+ progressLabel: `Building container image for '${container.name}'`
20137
20255
  });
20138
20256
  if (actualTag !== tag) try {
20139
20257
  await runDockerStreaming([
@@ -20356,419 +20474,85 @@ function shellJoin(parts) {
20356
20474
  }
20357
20475
 
20358
20476
  //#endregion
20359
- //#region src/cli/commands/local-run-task.ts
20360
- /**
20361
- * `cdkl run-task <target>` Phase 1 of the ECS local-execution
20362
- * trilogy. Synthesizes the CDK app, locates the target
20363
- * `AWS::ECS::TaskDefinition`, stands up a per-task docker network with
20364
- * the AWS-published `amazon-ecs-local-container-endpoints` sidecar, and
20365
- * starts every container in `dependsOn` order. The essential
20366
- * container's exit code drives the CLI's exit.
20367
- */
20368
- async function localRunTaskCommand(target, options, extraStateProviders) {
20369
- const logger = getLogger();
20370
- if (options.verbose) logger.setLevel("debug");
20371
- const state = createEcsRunState();
20372
- let sigintHandler;
20373
- let sigintCount = 0;
20374
- let stateProvider;
20375
- let profileCredsFile;
20376
- let cleanupPromise;
20377
- const cleanup = async () => {
20378
- if (!cleanupPromise) cleanupPromise = (async () => {
20379
- try {
20380
- await cleanupEcsRun(state, { keepRunning: options.keepRunning });
20381
- } catch (err) {
20382
- getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
20383
- }
20384
- if (profileCredsFile) try {
20385
- await profileCredsFile.dispose();
20386
- } catch (err) {
20387
- getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
20388
- }
20389
- })();
20390
- await cleanupPromise;
20391
- };
20392
- try {
20393
- await applyRoleArnIfSet({
20394
- roleArn: options.roleArn,
20395
- region: options.region,
20396
- profile: options.profile
20397
- });
20398
- await ensureDockerAvailable();
20399
- const appCmd = resolveApp(options.app);
20400
- if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
20401
- logger.info("Synthesizing CDK app...");
20402
- const synthesizer = new Synthesizer();
20403
- const context = parseContextOptions(options.context);
20404
- const synthOpts = {
20405
- app: appCmd,
20406
- output: options.output,
20407
- ...options.region && { region: options.region },
20408
- ...options.profile && { profile: options.profile },
20409
- ...Object.keys(context).length > 0 && { context }
20410
- };
20411
- const { stacks } = await synthesizer.synthesize(synthOpts);
20412
- const resolvedTarget = await resolveSingleTarget(target, {
20413
- entries: listTargets(stacks).ecsTaskDefinitions,
20414
- message: "Select an ECS task definition to run",
20415
- noun: "ECS task definitions",
20416
- onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} run-task requires a <target> (an ECS task definition display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or run it in a TTY to pick interactively.`, "LOCAL_RUN_TASK_TARGET_REQUIRED")
20417
- });
20418
- const candidate = pickCandidateStack$1(parseEcsTarget(resolvedTarget).stackPattern, stacks);
20419
- stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
20420
- const imageContext = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
20421
- const task = resolveEcsTaskTarget(resolvedTarget, stacks, imageContext);
20422
- logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
20423
- const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
20424
- if (stateProvider && taskNeeds.needsCrossStackResolver) {
20425
- const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
20426
- const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
20427
- if (resolver) await applyCrossStackResolverToTask(task, {
20428
- resources: imageContext?.stateResources ?? {},
20429
- ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
20430
- ...imageContext?.stateParameters && { parameters: imageContext.stateParameters },
20431
- ...imageContext?.stateSensitiveParameters?.length && { sensitiveParameters: new Set(imageContext.stateSensitiveParameters) },
20432
- consumerRegion,
20433
- crossStackResolver: resolver
20434
- });
20435
- } else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state.");
20436
- sigintHandler = () => {
20437
- sigintCount += 1;
20438
- if (sigintCount >= 2) {
20439
- process.stderr.write("Force-exit on second ^C; container cleanup skipped.\n");
20440
- process.exit(130);
20441
- }
20442
- logger.info("Stopping task...");
20443
- cleanup().then(() => process.exit(130));
20444
- };
20445
- process.on("SIGINT", sigintHandler);
20446
- let assumedCredentials;
20447
- let resolvedRoleArn;
20448
- if (options.assumeTaskRole === true) {
20449
- if (!task.taskRoleArn) throw new Error(`--assume-task-role passed without an ARN but the task definition has no resolvable TaskRoleArn. Either the task definition does not set TaskRoleArn, or it points at a resource ${getEmbedConfig().binaryName} cannot resolve to an IAM Role at synth time. Pass the ARN explicitly: --assume-task-role <arn>`);
20450
- resolvedRoleArn = await resolvePlaceholderAccount$1(task.taskRoleArn, options.region, options.profile);
20451
- assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region, options.profile);
20452
- } else if (typeof options.assumeTaskRole === "string") {
20453
- resolvedRoleArn = options.assumeTaskRole;
20454
- assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region, options.profile);
20455
- }
20456
- const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
20457
- if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
20458
- const envOverrides = readEnvOverridesFile$1(options.envVars);
20459
- const runOpts = {
20460
- cluster: options.cluster,
20461
- containerHost: options.containerHost,
20462
- skipPull: options.pull === false,
20463
- keepRunning: options.keepRunning,
20464
- detach: options.detach
20465
- };
20466
- if (envOverrides) runOpts.envOverrides = envOverrides;
20467
- if (sidecarCredentials) runOpts.taskCredentials = sidecarCredentials;
20468
- if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
20469
- if (options.platform) runOpts.platformOverride = options.platform;
20470
- if (options.region) runOpts.region = options.region;
20471
- if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
20472
- if (options.profile) runOpts.profile = options.profile;
20473
- const hostPortOverrides = parseHostPortOverrides(options.hostPort);
20474
- if (Object.keys(hostPortOverrides).length > 0) runOpts.hostPortOverrides = hostPortOverrides;
20475
- if (profileCredsFile) runOpts.profileCredentialsFile = {
20476
- hostPath: profileCredsFile.hostPath,
20477
- containerPath: profileCredsFile.containerPath,
20478
- profileName: profileCredsFile.profileName
20479
- };
20480
- const result = await runEcsTask(task, runOpts, state);
20481
- if (options.detach) {
20482
- logger.info(`Task containers started in detached mode; ${getEmbedConfig().binaryName} is exiting.`);
20483
- logger.info(`Use 'docker ps --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`);
20484
- sigintCount = 99;
20485
- return;
20486
- }
20487
- if (result.essentialContainerName) logger.info(`Essential container '${result.essentialContainerName}' exited with code ${result.exitCode}.`);
20488
- if (result.exitCode !== 0) process.exitCode = result.exitCode;
20489
- } finally {
20490
- if (sigintHandler) process.off("SIGINT", sigintHandler);
20491
- if (stateProvider) stateProvider.dispose();
20492
- if (!options.detach) await cleanup();
20477
+ //#region src/local/ecs-service-resolver.ts
20478
+ function resolveEcsServiceTarget(target, stacks, context, options) {
20479
+ if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
20480
+ const parsed = parseEcsTarget(target);
20481
+ const stack = pickStack$1(parsed, stacks);
20482
+ const resources = stack.template.Resources ?? {};
20483
+ let serviceLogicalId;
20484
+ let serviceResource;
20485
+ if (parsed.isPath) {
20486
+ const index = buildCdkPathIndex(stack.template);
20487
+ const services = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId: l }) => resources[l]?.Type === "AWS::ECS::Service");
20488
+ if (services.length === 0) throw notFoundError(target, stack, resources, parsed);
20489
+ if (services.length > 1) throw new EcsTaskResolutionError(`Target '${target}' matches ${services.length} ECS services in ${stack.stackName}: ` + services.map((s) => s.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form.");
20490
+ serviceLogicalId = services[0].logicalId;
20491
+ serviceResource = resources[serviceLogicalId];
20492
+ } else {
20493
+ serviceResource = resources[parsed.pathOrId];
20494
+ if (!serviceResource) throw notFoundError(target, stack, resources, parsed);
20495
+ serviceLogicalId = parsed.pathOrId;
20493
20496
  }
20497
+ if (!serviceLogicalId || !serviceResource) throw notFoundError(target, stack, resources, parsed);
20498
+ if (serviceResource.Type === "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is an ECS TaskDefinition, not a Service. Use \`${getEmbedConfig().cliName} run-task\` for one-shot tasks; \`${getEmbedConfig().cliName} start-service\` is Service-only.`);
20499
+ if (serviceResource.Type !== "AWS::ECS::Service") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is ${serviceResource.Type}, not an AWS::ECS::Service.`);
20500
+ return extractServiceProperties(stack, serviceLogicalId, serviceResource, stacks, context, options);
20494
20501
  }
20495
20502
  /**
20496
- * If `arn` contains the `${AWS::AccountId}` placeholder emitted by the
20497
- * resolver for inline same-stack IAM Roles, substitute the live caller
20498
- * account via STS `GetCallerIdentity`. Otherwise pass through unchanged.
20503
+ * Pure-functional extraction from the synth resource. Exposed for unit
20504
+ * testing the per-field resolution rules (DesiredCount default, missing
20505
+ * TaskDefinition, intrinsic shapes).
20499
20506
  */
20500
- async function resolvePlaceholderAccount$1(arn, region, profile) {
20501
- if (!arn.includes("${AWS::AccountId}")) return arn;
20502
- const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
20503
- const sts = new STSClient(buildStsClientConfig({
20504
- region,
20505
- profile
20506
- }));
20507
- try {
20508
- const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
20509
- if (!account) throw new Error(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'. Pass the ARN explicitly: --assume-task-role <arn>`);
20510
- return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
20511
- } finally {
20512
- sts.destroy();
20507
+ function extractServiceProperties(stack, serviceLogicalId, resource, stacks, context, options) {
20508
+ const props = resource.Properties ?? {};
20509
+ const warnings = [];
20510
+ const taskDefRef = props["TaskDefinition"];
20511
+ if (taskDefRef === void 0 || taskDefRef === null) throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' in ${stack.stackName} has no TaskDefinition property.`);
20512
+ const taskDefLogicalId = resolveTaskDefinitionReference(taskDefRef, stack, serviceLogicalId);
20513
+ const task = resolveEcsTaskTarget(`${stack.stackName}:${taskDefLogicalId}`, stacks, context);
20514
+ const desiredCount = parseDesiredCount(props["DesiredCount"], serviceLogicalId);
20515
+ const healthCheckGracePeriodSeconds = parseHealthCheckGrace(props["HealthCheckGracePeriodSeconds"], serviceLogicalId);
20516
+ const serviceName = parseServiceName(props["ServiceName"], serviceLogicalId);
20517
+ const serviceDisplayName = deriveServiceDisplayName(props["ServiceName"], serviceLogicalId, resource.Metadata);
20518
+ if (!options?.suppressLoadBalancerWarning && Array.isArray(props["LoadBalancers"]) && props["LoadBalancers"].length > 0) {
20519
+ const { cliName } = getEmbedConfig();
20520
+ warnings.push(`ECS Service '${serviceLogicalId}' declares LoadBalancers, but \`${cliName} start-service\` runs the replicas only; no local listener fronts them. Reach the containers via their published ports, or run \`${cliName} start-alb <Stack>/<Alb>\` to boot the same replicas behind a local front-door that round-robins the listener rules.`);
20513
20521
  }
20522
+ const serviceConnect = extractServiceConnect(props["ServiceConnectConfiguration"], task);
20523
+ const out = {
20524
+ stack,
20525
+ serviceLogicalId,
20526
+ resource,
20527
+ serviceName,
20528
+ serviceDisplayName,
20529
+ desiredCount,
20530
+ healthCheckGracePeriodSeconds,
20531
+ task,
20532
+ serviceRegistries: extractServiceRegistries(props["ServiceRegistries"], serviceLogicalId, warnings),
20533
+ warnings
20534
+ };
20535
+ if (serviceConnect) out.serviceConnect = serviceConnect;
20536
+ return out;
20514
20537
  }
20515
20538
  /**
20516
- * Assume `roleArn` and return temp credentials.
20517
- */
20518
- async function assumeTaskRole$1(roleArn, region, profile) {
20519
- const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
20520
- const sts = new STSClient(buildStsClientConfig({
20521
- region,
20522
- profile
20523
- }));
20524
- try {
20525
- const creds = (await sts.send(new AssumeRoleCommand({
20526
- RoleArn: roleArn,
20527
- RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-run-task-${Date.now()}`,
20528
- DurationSeconds: 3600
20529
- }))).Credentials;
20530
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
20531
- return {
20532
- accessKeyId: creds.AccessKeyId,
20533
- secretAccessKey: creds.SecretAccessKey,
20534
- sessionToken: creds.SessionToken
20535
- };
20536
- } finally {
20537
- sts.destroy();
20538
- }
20539
- }
20540
- /**
20541
- * Build the substitution context the ECS task resolver consumes.
20542
- * Returns `undefined` when no container's `Image` field needs
20543
- * substitution — the resolver behaves as before in that case.
20544
- */
20545
- async function buildEcsImageResolutionContext$1(candidate, stateProvider, options) {
20546
- const logger = getLogger();
20547
- if (!candidate) return void 0;
20548
- const needs = detectEcsImageResolutionNeeds(candidate);
20549
- if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
20550
- const ctx = {};
20551
- const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
20552
- if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
20553
- const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
20554
- if (!region) logger.warn(`Resolver references \${AWS::Region} but ${getEmbedConfig().binaryName} could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.`);
20555
- let accountId;
20556
- try {
20557
- accountId = await resolveCallerAccountId$1(region, options.profile);
20558
- } catch (err) {
20559
- logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`);
20560
- }
20561
- const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
20562
- ctx.pseudoParameters = {
20563
- ...accountId !== void 0 && { accountId },
20564
- ...region !== void 0 && { region },
20565
- ...partitionAndSuffix && {
20566
- partition: partitionAndSuffix.partition,
20567
- urlSuffix: partitionAndSuffix.urlSuffix
20568
- }
20569
- };
20570
- }
20571
- const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
20572
- if (stateProvider && wantsState) {
20573
- const loaded = await stateProvider.load(candidate.stackName, candidate.region);
20574
- if (loaded) ctx.stateResources = loaded.resources;
20575
- else {
20576
- const loadError = stateProvider.getLastLoadError?.();
20577
- if (loadError) ctx.stateLoadFailureMessage = loadError;
20578
- }
20579
- if (needs.needsEnvOrSecretSubstitution && stateProvider.resolveTemplateSsmParameters) {
20580
- const ssmParameters = await stateProvider.resolveTemplateSsmParameters(candidate.template);
20581
- if (Object.keys(ssmParameters.values).length > 0) ctx.stateParameters = ssmParameters.values;
20582
- if (ssmParameters.secureStringLogicalIds.length > 0) ctx.stateSensitiveParameters = ssmParameters.secureStringLogicalIds;
20583
- }
20584
- } else if (!stateProvider && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute the deployed repository URI. Otherwise the resolver will surface its existing error.");
20585
- else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state. Without a state source these entries are dropped (per-key warnings will follow).");
20586
- return ctx;
20587
- }
20588
- function pickCandidateStack$1(stackPattern, stacks) {
20589
- if (stackPattern === null) {
20590
- if (stacks.length === 1) return stacks[0];
20591
- return;
20592
- }
20593
- const matched = matchStacks(stacks, [stackPattern]);
20594
- if (matched.length === 1) return matched[0];
20595
- }
20596
- async function resolveCallerAccountId$1(region, profile) {
20597
- const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
20598
- const sts = new STSClient(buildStsClientConfig({
20599
- region,
20600
- profile
20601
- }));
20602
- try {
20603
- return (await sts.send(new GetCallerIdentityCommand({}))).Account;
20604
- } finally {
20605
- sts.destroy();
20606
- }
20607
- }
20608
- /**
20609
- * Read the `--env-vars` JSON file using the same SAM-style shape as
20610
- * `cdkl invoke --env-vars`: top-level keys are container names, with
20611
- * `Parameters` reserved for global entries.
20612
- */
20613
- function readEnvOverridesFile$1(filePath) {
20614
- if (!filePath) return void 0;
20615
- let raw;
20616
- try {
20617
- raw = readFileSync(filePath, "utf-8");
20618
- } catch (err) {
20619
- throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
20620
- }
20621
- let parsed;
20622
- try {
20623
- parsed = JSON.parse(raw);
20624
- } catch (err) {
20625
- throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
20626
- }
20627
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
20628
- return parsed;
20629
- }
20630
- /**
20631
- * Pick the credentials forwarded to the AWS-published
20632
- * `amazon-ecs-local-container-endpoints` sidecar. Precedence:
20633
- * 1. `--assume-task-role <arn>` (or bare `--assume-task-role` against
20634
- * a resolvable `TaskRoleArn`) → STS-assumed temp creds. Highest
20635
- * priority — when the user opted in to IAM emulation, those creds
20636
- * drive the sidecar regardless of `--profile`.
20637
- * 2. `--profile <p>` → resolved via {@link resolveProfileCredentials}
20638
- * (the SDK's default credential provider chain — SSO / IAM
20639
- * Identity Center / fromIni / role-assumption). NEW in this PR.
20640
- * 3. Neither set → `undefined`; the sidecar runs with its own
20641
- * default credential chain (typically empty inside a fresh
20642
- * container — user containers will get 4xx from the credentials
20643
- * endpoint, mimicking IAM-misconfigured prod).
20644
- *
20645
- * Extracted as an exported helper so a unit test can exercise every
20646
- * branch without having to mock the full Synth + Docker + AWS pipeline
20647
- * (the strategy used for the Lambda container path).
20648
- */
20649
- async function resolveSidecarCredentials(options, assumedCredentials) {
20650
- if (assumedCredentials) return assumedCredentials;
20651
- if (options.profile) return resolveProfileCredentials(options.profile);
20652
- }
20653
- function createLocalRunTaskCommand(opts = {}) {
20654
- setEmbedConfig(opts.embedConfig);
20655
- 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. Omit <target> in an interactive terminal to pick the task definition from a list.").argument("[target]", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run (omit to pick interactively in a TTY)").action(withErrorHandling(async (target, options) => {
20656
- await localRunTaskCommand(target, options, opts.extraStateProviders);
20657
- }));
20658
- addRunTaskSpecificOptions(cmd);
20659
- [
20660
- ...commonOptions(),
20661
- ...appOptions(),
20662
- ...contextOptions
20663
- ].forEach((opt) => cmd.addOption(opt));
20664
- cmd.addOption(regionOption);
20665
- return cmd;
20666
- }
20667
- /**
20668
- * Register the option block that `cdkl run-task` adds on top of the shared
20669
- * common / app / context option helpers. Shared between `cdkl run-task` and
20670
- * any host CLI (e.g. cdkd's `local run-task`) that wraps the single-task
20671
- * ECS local runner, so adding or renaming a `run-task`-only flag here
20672
- * propagates to every embedder without duplicate `.addOption(...)` blocks.
20673
- *
20674
- * Calling order only affects `--help` presentation (Commander parses
20675
- * insertion-order-independent). The host-CLI convention is host-specific
20676
- * options first, then this helper, then the shared common / app / context
20677
- * options — host flags / run-task flags / common flags grouped in three
20678
- * `--help` clusters. Chainable: returns `cmd`.
20679
- *
20680
- * NOTE: `run-task` does NOT compose with {@link addCommonEcsServiceOptions}
20681
- * even though many flags overlap. The two ECS surfaces (single-task vs
20682
- * multi-replica service) have intentionally divergent defaults
20683
- * (`run-task` has no `--max-tasks` / `--restart-policy`; `start-service`
20684
- * / `start-alb` have no `--host-port` / `--keep-running` / `--detach`),
20685
- * and folding `run-task` into the service common block would mutate the
20686
- * surface non-trivially. Each command keeps its own helper.
20687
- */
20688
- function addRunTaskSpecificOptions(cmd) {
20689
- return cmd.addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix)).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("--host-port <containerPort=hostPort...>", "Publish a container port on a specific host port (e.g. 80=8080); repeatable. Default: host port == container port. Use this on macOS to map a privileged container port (< 1024) to a non-privileged host port and avoid the Docker Desktop admin-password prompt.")).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. 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-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via ListStackResources 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 ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt in container Environment[].Value is warn-and-dropped: CFn ListStackResources does not return per-attribute values, and unlike Lambda (where \`cdkl invoke --from-cfn-stack\` recovers Fn::GetAtt from the deployed function via lambda:GetFunctionConfiguration), no ECS-side equivalent resolves attributes off a deployed task / service.`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
20690
- }
20691
-
20692
- //#endregion
20693
- //#region src/local/ecs-service-resolver.ts
20694
- function resolveEcsServiceTarget(target, stacks, context, options) {
20695
- if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
20696
- const parsed = parseEcsTarget(target);
20697
- const stack = pickStack$1(parsed, stacks);
20698
- const resources = stack.template.Resources ?? {};
20699
- let serviceLogicalId;
20700
- let serviceResource;
20701
- if (parsed.isPath) {
20702
- const index = buildCdkPathIndex(stack.template);
20703
- const services = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId: l }) => resources[l]?.Type === "AWS::ECS::Service");
20704
- if (services.length === 0) throw notFoundError(target, stack, resources, parsed);
20705
- if (services.length > 1) throw new EcsTaskResolutionError(`Target '${target}' matches ${services.length} ECS services in ${stack.stackName}: ` + services.map((s) => s.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form.");
20706
- serviceLogicalId = services[0].logicalId;
20707
- serviceResource = resources[serviceLogicalId];
20708
- } else {
20709
- serviceResource = resources[parsed.pathOrId];
20710
- if (!serviceResource) throw notFoundError(target, stack, resources, parsed);
20711
- serviceLogicalId = parsed.pathOrId;
20712
- }
20713
- if (!serviceLogicalId || !serviceResource) throw notFoundError(target, stack, resources, parsed);
20714
- if (serviceResource.Type === "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is an ECS TaskDefinition, not a Service. Use \`${getEmbedConfig().cliName} run-task\` for one-shot tasks; \`${getEmbedConfig().cliName} start-service\` is Service-only.`);
20715
- if (serviceResource.Type !== "AWS::ECS::Service") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is ${serviceResource.Type}, not an AWS::ECS::Service.`);
20716
- return extractServiceProperties(stack, serviceLogicalId, serviceResource, stacks, context, options);
20717
- }
20718
- /**
20719
- * Pure-functional extraction from the synth resource. Exposed for unit
20720
- * testing the per-field resolution rules (DesiredCount default, missing
20721
- * TaskDefinition, intrinsic shapes).
20722
- */
20723
- function extractServiceProperties(stack, serviceLogicalId, resource, stacks, context, options) {
20724
- const props = resource.Properties ?? {};
20725
- const warnings = [];
20726
- const taskDefRef = props["TaskDefinition"];
20727
- if (taskDefRef === void 0 || taskDefRef === null) throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' in ${stack.stackName} has no TaskDefinition property.`);
20728
- const taskDefLogicalId = resolveTaskDefinitionReference(taskDefRef, stack, serviceLogicalId);
20729
- const task = resolveEcsTaskTarget(`${stack.stackName}:${taskDefLogicalId}`, stacks, context);
20730
- const desiredCount = parseDesiredCount(props["DesiredCount"], serviceLogicalId);
20731
- const healthCheckGracePeriodSeconds = parseHealthCheckGrace(props["HealthCheckGracePeriodSeconds"], serviceLogicalId);
20732
- const serviceName = parseServiceName(props["ServiceName"], serviceLogicalId);
20733
- const serviceDisplayName = deriveServiceDisplayName(props["ServiceName"], serviceLogicalId, resource.Metadata);
20734
- if (!options?.suppressLoadBalancerWarning && Array.isArray(props["LoadBalancers"]) && props["LoadBalancers"].length > 0) {
20735
- const { cliName } = getEmbedConfig();
20736
- warnings.push(`ECS Service '${serviceLogicalId}' declares LoadBalancers, but \`${cliName} start-service\` runs the replicas only; no local listener fronts them. Reach the containers via their published ports, or run \`${cliName} start-alb <Stack>/<Alb>\` to boot the same replicas behind a local front-door that round-robins the listener rules.`);
20737
- }
20738
- const serviceConnect = extractServiceConnect(props["ServiceConnectConfiguration"], task);
20739
- const out = {
20740
- stack,
20741
- serviceLogicalId,
20742
- resource,
20743
- serviceName,
20744
- serviceDisplayName,
20745
- desiredCount,
20746
- healthCheckGracePeriodSeconds,
20747
- task,
20748
- serviceRegistries: extractServiceRegistries(props["ServiceRegistries"], serviceLogicalId, warnings),
20749
- warnings
20750
- };
20751
- if (serviceConnect) out.serviceConnect = serviceConnect;
20752
- return out;
20753
- }
20754
- /**
20755
- * Parse `ServiceConnectConfiguration` against the producer TaskDef.
20756
- * Returns `undefined` when the block is missing OR `Enabled: false`.
20757
- *
20758
- * Reject conditions (surface as resolver-time errors so the user sees
20759
- * them BEFORE the docker network is created):
20760
- * - `Namespace` is not a literal string. CDK 2.x always emits a
20761
- * literal string here (verified 2026-05-22); cross-stack /
20762
- * intrinsic shapes are out of scope.
20763
- * - `Services[].PortName` doesn't match any of the TaskDef's
20764
- * `ContainerDefinitions[].PortMappings[].Name` entries.
20765
- *
20766
- * Note on `clientAliases[]` shape: each ClientAlias can declare a
20767
- * `DnsName` (the bare short-name peers connect to, e.g. `orders`) AND
20768
- * a `Port` (the listening port the alias maps to inside the consumer).
20769
- * cdk-local surfaces both verbatim; the registry / `--add-host` overlay
20770
- * publishes each `DnsName` as a bare alias pointing at the same IP as
20771
- * the canonical fqdn.
20539
+ * Parse `ServiceConnectConfiguration` against the producer TaskDef.
20540
+ * Returns `undefined` when the block is missing OR `Enabled: false`.
20541
+ *
20542
+ * Reject conditions (surface as resolver-time errors so the user sees
20543
+ * them BEFORE the docker network is created):
20544
+ * - `Namespace` is not a literal string. CDK 2.x always emits a
20545
+ * literal string here (verified 2026-05-22); cross-stack /
20546
+ * intrinsic shapes are out of scope.
20547
+ * - `Services[].PortName` doesn't match any of the TaskDef's
20548
+ * `ContainerDefinitions[].PortMappings[].Name` entries.
20549
+ *
20550
+ * Note on `clientAliases[]` shape: each ClientAlias can declare a
20551
+ * `DnsName` (the bare short-name peers connect to, e.g. `orders`) AND
20552
+ * a `Port` (the listening port the alias maps to inside the consumer).
20553
+ * cdk-local surfaces both verbatim; the registry / `--add-host` overlay
20554
+ * publishes each `DnsName` as a bare alias pointing at the same IP as
20555
+ * the canonical fqdn.
20772
20556
  */
20773
20557
  function extractServiceConnect(raw, task) {
20774
20558
  if (!raw || typeof raw !== "object") return void 0;
@@ -21846,7 +21630,7 @@ async function softReloadReplica(args) {
21846
21630
  const defaultDockerInspectWorkdirImpl = async (containerId) => {
21847
21631
  const { execFile } = await import("node:child_process");
21848
21632
  const { promisify } = await import("node:util");
21849
- const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21633
+ const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
21850
21634
  const { stdout } = await promisify(execFile)(getDockerCmd(), [
21851
21635
  "inspect",
21852
21636
  "--format",
@@ -21865,7 +21649,7 @@ let dockerInspectWorkdirImpl = defaultDockerInspectWorkdirImpl;
21865
21649
  const defaultDockerCpImpl = async (src, dst) => {
21866
21650
  const { execFile } = await import("node:child_process");
21867
21651
  const { promisify } = await import("node:util");
21868
- const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21652
+ const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
21869
21653
  await promisify(execFile)(getDockerCmd(), [
21870
21654
  "cp",
21871
21655
  src,
@@ -21880,7 +21664,7 @@ let dockerCpImpl = defaultDockerCpImpl;
21880
21664
  const defaultDockerRestartImpl = async (containerId) => {
21881
21665
  const { execFile } = await import("node:child_process");
21882
21666
  const { promisify } = await import("node:util");
21883
- const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21667
+ const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
21884
21668
  await promisify(execFile)(getDockerCmd(), ["restart", containerId]);
21885
21669
  };
21886
21670
  let dockerRestartImpl = defaultDockerRestartImpl;
@@ -21920,7 +21704,7 @@ async function disconnectOldFromSharedNetwork(oldInstance) {
21920
21704
  const defaultDockerNetworkDisconnectImpl = async (networkName, containerId) => {
21921
21705
  const { execFile } = await import("node:child_process");
21922
21706
  const { promisify } = await import("node:util");
21923
- const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21707
+ const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
21924
21708
  await promisify(execFile)(getDockerCmd(), [
21925
21709
  "network",
21926
21710
  "disconnect",
@@ -22097,7 +21881,7 @@ function pickEssentialContainerId(instance, service) {
22097
21881
  const defaultWaitForExitImpl = async (containerId) => {
22098
21882
  const { execFile } = await import("node:child_process");
22099
21883
  const { promisify } = await import("node:util");
22100
- const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21884
+ const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
22101
21885
  const { stdout } = await promisify(execFile)(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
22102
21886
  const code = parseInt(stdout.trim(), 10);
22103
21887
  return Number.isFinite(code) ? code : -1;
@@ -22117,7 +21901,7 @@ const EXIT_LOG_TAIL_LINES = 50;
22117
21901
  const defaultReadContainerLogsImpl = async (containerId) => {
22118
21902
  const { execFile } = await import("node:child_process");
22119
21903
  const { promisify } = await import("node:util");
22120
- const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21904
+ const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
22121
21905
  const { stdout, stderr } = await promisify(execFile)(getDockerCmd(), [
22122
21906
  "logs",
22123
21907
  "--tail",
@@ -24965,7 +24749,8 @@ async function runImageOverrideBuilds(overrides) {
24965
24749
  try {
24966
24750
  await runDockerStreaming(args, {
24967
24751
  cwd: entry.contextDir,
24968
- env: { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" }
24752
+ env: { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" },
24753
+ progressLabel: `Building override image for '${target}'`
24969
24754
  });
24970
24755
  } catch (err) {
24971
24756
  const e = err;
@@ -25401,7 +25186,7 @@ async function reloadAllServices(args) {
25401
25186
  async function loadAssetContextForTarget(args) {
25402
25187
  const { target, controller, stacks, cdkOutDir, assetLoader, logger } = args;
25403
25188
  const parsed = parseEcsTarget(target);
25404
- const candidate = pickCandidateStack(parsed.stackPattern, stacks);
25189
+ const candidate = pickCandidateStack$1(parsed.stackPattern, stacks);
25405
25190
  if (!candidate) {
25406
25191
  logger.debug(`loadAssetContext: stack pattern '${parsed.stackPattern}' from target '${target}' not in the assembly. Classifier will see no asset context (rebuild).`);
25407
25192
  return;
@@ -25465,7 +25250,7 @@ async function loadAssetContextForTarget(args) {
25465
25250
  */
25466
25251
  async function rollOneTarget(args) {
25467
25252
  const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, verdict, imageOverrideTag, logger } = args;
25468
- const candidate = pickCandidateStack(parseEcsTarget(newBoot.target).stackPattern, stacks);
25253
+ const candidate = pickCandidateStack$1(parseEcsTarget(newBoot.target).stackPattern, stacks);
25469
25254
  const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
25470
25255
  try {
25471
25256
  let resolved;
@@ -25517,7 +25302,7 @@ async function rollOneTarget(args) {
25517
25302
  }
25518
25303
  }
25519
25304
  async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, imageOverrideTag) {
25520
- const candidate = pickCandidateStack(parseEcsTarget(boot.target).stackPattern, stacks);
25305
+ const candidate = pickCandidateStack$1(parseEcsTarget(boot.target).stackPattern, stacks);
25521
25306
  const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
25522
25307
  try {
25523
25308
  return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, imageOverrideTag);
@@ -25556,7 +25341,7 @@ async function resolveServiceAndRunnerOpts(boot, stacks, options, discovery, ski
25556
25341
  const logger = getLogger();
25557
25342
  const target = boot.target;
25558
25343
  const quiet = opts.quiet === true;
25559
- const imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
25344
+ const imageContext = await buildEcsImageResolutionContext$1(target, stacks, options, stateProvider);
25560
25345
  const service = resolveEcsServiceTarget(target, stacks, imageContext, { suppressLoadBalancerWarning });
25561
25346
  if (!quiet) logger.info(`Target: ${service.stack.stackName}/${service.serviceLogicalId} (service=${service.serviceName}, desiredCount=${service.desiredCount}, task=${service.task.taskDefinitionLogicalId})`);
25562
25347
  if (!quiet && service.serviceConnect) logger.info(`Service Connect: namespace='${service.serviceConnect.namespaceName}', ${service.serviceConnect.services.length} service(s) registered for peer discovery.`);
@@ -25577,21 +25362,23 @@ async function resolveServiceAndRunnerOpts(boot, stacks, options, discovery, ski
25577
25362
  await applyCrossStackResolverToTask(service.task, subContext);
25578
25363
  }
25579
25364
  } else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state.");
25365
+ const effectiveAssumeRole = resolveEcsAssumeRoleOption(options);
25580
25366
  let assumedCredentials;
25581
25367
  let resolvedRoleArn;
25582
- if (options.assumeTaskRole === true) {
25583
- if (!service.task.taskRoleArn) throw new LocalStartServiceError(`--assume-task-role passed without an ARN but service '${service.serviceLogicalId}' has no resolvable TaskRoleArn. Pass the ARN explicitly: --assume-task-role <arn>`);
25584
- resolvedRoleArn = await resolvePlaceholderAccount(service.task.taskRoleArn, options.region, options.profile);
25585
- assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region, options.profile);
25586
- } else if (typeof options.assumeTaskRole === "string") {
25587
- resolvedRoleArn = options.assumeTaskRole;
25588
- assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region, options.profile);
25589
- }
25590
- const envOverrides = readEnvOverridesFile(options.envVars);
25368
+ if (effectiveAssumeRole === true) {
25369
+ if (!service.task.taskRoleArn) throw new LocalStartServiceError(`--assume-role passed without an ARN but service '${service.serviceLogicalId}' has no resolvable TaskRoleArn. Pass the ARN explicitly: --assume-role <arn>`);
25370
+ resolvedRoleArn = await resolvePlaceholderAccount$1(service.task.taskRoleArn, options.region, options.profile);
25371
+ assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region, options.profile);
25372
+ } else if (typeof effectiveAssumeRole === "string") {
25373
+ resolvedRoleArn = effectiveAssumeRole;
25374
+ assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region, options.profile);
25375
+ }
25376
+ const envOverrides = readEnvOverridesFile$1(options.envVars);
25591
25377
  const taskOpts = {
25592
25378
  cluster: options.cluster,
25593
25379
  containerHost: options.containerHost,
25594
25380
  skipPull,
25381
+ skipBuild: options.build === false,
25595
25382
  keepRunning: false,
25596
25383
  detach: true
25597
25384
  };
@@ -25882,7 +25669,7 @@ function describeRuleActionForSummary(action) {
25882
25669
  function describeForwardTargetForSummary(t) {
25883
25670
  return t.kind === "lambda" ? `<Lambda: ${t.lambda.logicalId}>` : `<ECS: ${t.serviceTarget}>`;
25884
25671
  }
25885
- async function resolvePlaceholderAccount(arn, region, profile) {
25672
+ async function resolvePlaceholderAccount$1(arn, region, profile) {
25886
25673
  if (!arn.includes("${AWS::AccountId}")) return arn;
25887
25674
  const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
25888
25675
  const sts = new STSClient(buildStsClientConfig({
@@ -25897,7 +25684,7 @@ async function resolvePlaceholderAccount(arn, region, profile) {
25897
25684
  sts.destroy();
25898
25685
  }
25899
25686
  }
25900
- async function assumeTaskRole(roleArn, region, profile) {
25687
+ async function assumeTaskRole$1(roleArn, region, profile) {
25901
25688
  const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
25902
25689
  const sts = new STSClient(buildStsClientConfig({
25903
25690
  region,
@@ -25924,9 +25711,9 @@ async function assumeTaskRole(roleArn, region, profile) {
25924
25711
  * site-level binding test that locks the `--from-cfn-stack` SSM-parameter
25925
25712
  * resolution call (issue #94).
25926
25713
  */
25927
- async function buildEcsImageResolutionContext(target, stacks, options, stateProvider) {
25714
+ async function buildEcsImageResolutionContext$1(target, stacks, options, stateProvider) {
25928
25715
  const logger = getLogger();
25929
- const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
25716
+ const candidate = pickCandidateStack$1(parseEcsTarget(target).stackPattern, stacks);
25930
25717
  if (!candidate) return void 0;
25931
25718
  const needs = detectEcsImageResolutionNeeds(candidate);
25932
25719
  if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
@@ -25937,7 +25724,7 @@ async function buildEcsImageResolutionContext(target, stacks, options, stateProv
25937
25724
  if (!region) logger.warn(`Resolver references \${AWS::Region} but ${getEmbedConfig().binaryName} could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.`);
25938
25725
  let accountId;
25939
25726
  try {
25940
- accountId = await resolveCallerAccountId(region, options.profile);
25727
+ accountId = await resolveCallerAccountId$1(region, options.profile);
25941
25728
  } catch (err) {
25942
25729
  logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`);
25943
25730
  }
@@ -25968,7 +25755,7 @@ async function buildEcsImageResolutionContext(target, stacks, options, stateProv
25968
25755
  else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against the deployed state.");
25969
25756
  return ctx;
25970
25757
  }
25971
- function pickCandidateStack(stackPattern, stacks) {
25758
+ function pickCandidateStack$1(stackPattern, stacks) {
25972
25759
  if (stackPattern === null) {
25973
25760
  if (stacks.length === 1) return stacks[0];
25974
25761
  return;
@@ -25976,7 +25763,7 @@ function pickCandidateStack(stackPattern, stacks) {
25976
25763
  const matched = matchStacks(stacks, [stackPattern]);
25977
25764
  if (matched.length === 1) return matched[0];
25978
25765
  }
25979
- async function resolveCallerAccountId(region, profile) {
25766
+ async function resolveCallerAccountId$1(region, profile) {
25980
25767
  const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
25981
25768
  const sts = new STSClient(buildStsClientConfig({
25982
25769
  region,
@@ -25988,7 +25775,7 @@ async function resolveCallerAccountId(region, profile) {
25988
25775
  sts.destroy();
25989
25776
  }
25990
25777
  }
25991
- function readEnvOverridesFile(filePath) {
25778
+ function readEnvOverridesFile$1(filePath) {
25992
25779
  if (!filePath) return void 0;
25993
25780
  let raw;
25994
25781
  try {
@@ -26133,12 +25920,12 @@ async function resolveAndBuildImageOverrides(args) {
26133
25920
  const { perTarget, stacks, options, extraStateProviders, logger } = args;
26134
25921
  const resolvedForPeek = [];
26135
25922
  for (const target of perTarget) {
26136
- const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
25923
+ const candidate = pickCandidateStack$1(parseEcsTarget(target).stackPattern, stacks);
26137
25924
  const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
26138
25925
  let imageContext;
26139
25926
  let service;
26140
25927
  try {
26141
- imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
25928
+ imageContext = await buildEcsImageResolutionContext$1(target, stacks, options, stateProvider);
26142
25929
  service = resolveEcsServiceTarget(target, stacks, imageContext, { suppressLoadBalancerWarning: true });
26143
25930
  } catch (err) {
26144
25931
  logger.debug(`--image-override peek failed for '${target}': ${err instanceof Error ? err.message : String(err)}.`);
@@ -26197,20 +25984,81 @@ function enforceStrictOverrides(strict, uncoveredPinnedTargets) {
26197
25984
  throw new LocalStartServiceError(`--strict-overrides set, but ${uncoveredPinnedTargets.length} pinned target(s) remain uncovered: ${uncoveredPinnedTargets.join(", ")}. Pass --image-override <service>=<dockerfile> for each, drop --strict-overrides, or drop --from-cfn-stack to iterate on local CDK assets.`);
26198
25985
  }
26199
25986
  /**
26200
- * Add the CLI options shared by both ECS-service commands (`start-service` and
26201
- * `start-alb`) to a command. The command-specific argument / description and
26202
- * the one unique option (`--host-port` vs `--lb-port`) are added by each
26203
- * factory.
25987
+ * Build the canonical `--cluster <name>` Option for every ECS surface.
25988
+ *
25989
+ * Shared between `cdkl run-task` and the `start-service` / `start-alb`
25990
+ * pair so the default (the active embed config's `resourceNamePrefix`)
25991
+ * is defined exactly once — issue #249 / C12. Building it as a fresh
25992
+ * Option per call (not a module-level const) is load-bearing: the
25993
+ * embed config is host-installed at command-factory time, and a
25994
+ * module-level Option would freeze the host-default prefix at
25995
+ * import time.
26204
25996
  */
26205
- function addCommonEcsServiceOptions(cmd) {
26206
- cmd.addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix)).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-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via ListStackResources 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 ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt in container Environment[].Value is warn-and-dropped: CFn ListStackResources does not return per-attribute values, and unlike Lambda (where \`cdkl invoke --from-cfn-stack\` recovers Fn::GetAtt from the deployed function via lambda:GetFunctionConfiguration), no ECS-side equivalent resolves attributes off a deployed task / service.`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).addOption(new Option("--no-logs", "Disable foreground streaming of each replica container stdout/stderr. By default every booted replica streams its docker logs to the host terminal with a [svc=<service> r=<replica-index> c=<container>] prefix (parity with `run-task`). Pass --no-logs for multi-replica / multi-service runs whose interleaved log volume is unreadable; `docker logs -f <id>` in a separate terminal stays available.")).addOption(new Option("--shadow-ready-timeout <ms>", `Issue #265 — milliseconds the \`--watch\` rolling primitive waits for a shadow replica's first essential-container port to accept a TCP connection before swapping Cloud Map + front-door registrations off the old replica. Defaults to 60000 (60s) which covers realistic prod-shaped Node app cold-starts (TS->JS compile, full node_modules graph, framework boot, DB pool init). Bump higher when the reload log surfaces "TCP probe <ip>:<port> did not accept within <N>ms" — typical for Java / heavy ORM init / \`--inspect-brk\` attach pauses. Honored by --watch on \`${getEmbedConfig().binaryName} start-service\` and \`${getEmbedConfig().binaryName} start-alb\`. Env var: \`${getEmbedConfig().envPrefix}_SHADOW_READY_TIMEOUT_MS\` (flag wins over env).`).argParser(parseShadowReadyTimeout));
26207
- [
26208
- ...commonOptions(),
26209
- ...appOptions(),
26210
- ...contextOptions
26211
- ].forEach((opt) => cmd.addOption(opt));
26212
- cmd.addOption(regionOption);
26213
- return cmd;
25997
+ function ecsClusterOption() {
25998
+ return new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix);
25999
+ }
26000
+ /**
26001
+ * Add the `--assume-task-role` flag plus its `--assume-role` alias to
26002
+ * an ECS command. Issue #249 / C6 — every non-ECS command calls the
26003
+ * same concept `--assume-role`, so the ECS surface accepts the
26004
+ * cross-command name as a NON-BREAKING alias. Precedence when both
26005
+ * are passed: `--assume-role` (new) wins. {@link resolveEcsAssumeRoleOption}
26006
+ * computes the effective value and fires a one-time deprecation warn
26007
+ * when only the legacy `--assume-task-role` is set.
26008
+ *
26009
+ * Breaking unification of all three parsers (`invoke`, `start-api`,
26010
+ * ECS) is tracked separately under issue #256.
26011
+ */
26012
+ function addEcsAssumeRoleOptions(cmd) {
26013
+ return cmd.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. DEPRECATED alias of `--assume-role`; both forms work, but `--assume-role` is the cross-command name and will become the only form in a future release.")).addOption(new Option("--assume-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. Cross-command alias matching the same flag on `invoke` / `invoke-agentcore` / `start-api`; supersedes the older `--assume-task-role` (which still works)."));
26014
+ }
26015
+ /**
26016
+ * Module-level latch for the `--assume-task-role` deprecation warn.
26017
+ * Per-process single-fire, so a `--watch` reload that re-enters the
26018
+ * resolver on every source-change firing does not spam the user.
26019
+ *
26020
+ * Test-only `__resetAssumeTaskRoleDeprecationLatch` lets the unit suite
26021
+ * drive the latch through repeated invocations.
26022
+ */
26023
+ let assumeTaskRoleDeprecationWarned = false;
26024
+ /**
26025
+ * Collapse `--assume-task-role` (legacy) and `--assume-role` (new) into
26026
+ * a single effective value. When BOTH are set, `--assume-role` wins
26027
+ * (matches Commander's "later wins" default for repeatable flags, and
26028
+ * gives the cross-command name priority). When ONLY `--assume-task-role`
26029
+ * is set, fires a **per-process one-time** deprecation warn naming the
26030
+ * replacement — the latch above prevents `--watch` reloads from
26031
+ * re-emitting the warn on every source-change firing.
26032
+ *
26033
+ * Issue #249 / C6.
26034
+ */
26035
+ function resolveEcsAssumeRoleOption(options) {
26036
+ if (options.assumeRole !== void 0) return options.assumeRole;
26037
+ if (options.assumeTaskRole !== void 0) {
26038
+ if (!assumeTaskRoleDeprecationWarned) {
26039
+ assumeTaskRoleDeprecationWarned = true;
26040
+ getLogger().warn("--assume-task-role is deprecated; use --assume-role instead. Both forms continue to work, but --assume-role is the cross-command name.");
26041
+ }
26042
+ return options.assumeTaskRole;
26043
+ }
26044
+ }
26045
+ /**
26046
+ * Add the CLI options shared by both ECS-service commands (`start-service` and
26047
+ * `start-alb`) to a command. The command-specific argument / description and
26048
+ * the one unique option (`--host-port` vs `--lb-port`) are added by each
26049
+ * factory.
26050
+ */
26051
+ function addCommonEcsServiceOptions(cmd) {
26052
+ cmd.addOption(ecsClusterOption()).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"));
26053
+ addEcsAssumeRoleOptions(cmd);
26054
+ cmd.addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--no-build", "Skip docker build on the local CDK-asset path for every container (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 ECR-pull / public-registry images. Per-container --image-override mappings take precedence (the override tag is used as-is, --no-build does not apply). Compatible with --no-pull.")).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-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via ListStackResources 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 ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt in container Environment[].Value is warn-and-dropped: CFn ListStackResources does not return per-attribute values, and unlike Lambda (where \`cdkl invoke --from-cfn-stack\` recovers Fn::GetAtt from the deployed function via lambda:GetFunctionConfiguration), no ECS-side equivalent resolves attributes off a deployed task / service.`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).addOption(new Option("--no-logs", "Disable foreground streaming of each replica container stdout/stderr. By default every booted replica streams its docker logs to the host terminal with a [svc=<service> r=<replica-index> c=<container>] prefix (parity with `run-task`). Pass --no-logs for multi-replica / multi-service runs whose interleaved log volume is unreadable; `docker logs -f <id>` in a separate terminal stays available.")).addOption(new Option("--shadow-ready-timeout <ms>", `Issue #265 — milliseconds the \`--watch\` rolling primitive waits for a shadow replica's first essential-container port to accept a TCP connection before swapping Cloud Map + front-door registrations off the old replica. Defaults to 60000 (60s) which covers realistic prod-shaped Node app cold-starts (TS->JS compile, full node_modules graph, framework boot, DB pool init). Bump higher when the reload log surfaces "TCP probe <ip>:<port> did not accept within <N>ms" — typical for Java / heavy ORM init / \`--inspect-brk\` attach pauses. Honored by --watch on \`${getEmbedConfig().binaryName} start-service\` and \`${getEmbedConfig().binaryName} start-alb\`. Env var: \`${getEmbedConfig().envPrefix}_SHADOW_READY_TIMEOUT_MS\` (flag wins over env).`).argParser(parseShadowReadyTimeout));
26055
+ [
26056
+ ...commonOptions(),
26057
+ ...appOptions(),
26058
+ ...contextOptions
26059
+ ].forEach((opt) => cmd.addOption(opt));
26060
+ cmd.addOption(regionOption);
26061
+ return cmd;
26214
26062
  }
26215
26063
  /**
26216
26064
  * Issue #238 — register the `--image-override` flag family shared by
@@ -26232,6 +26080,345 @@ function addImageOverrideOptions(cmd) {
26232
26080
  return cmd.addOption(new Option("--image-override <service=dockerfile or dockerfile...>", "Replace a service target's deployed-registry image with a local `docker build` of the supplied Dockerfile (repeatable). Two forms: <service>=<dockerfile> binds the service explicitly; a bare <dockerfile> opens a multi-select picker against the still-uncovered pinned targets (one Dockerfile across N services). Mix freely. Lets `--from-cfn-stack` still reach real AWS state while you iterate locally on the application container.")).addOption(new Option("--image-build-arg <KEY=VAL...>", "Global `docker build --build-arg KEY=VAL` pair applied to every --image-override build (repeatable). Lets a CDK Dockerfile that branches on an ARG (env target, base image tag, package mirror) honor that ARG locally without editing the Dockerfile.")).addOption(new Option("--image-build-secret <id=src...>", "Global `docker build --secret id=<id>,src=<src>` entry applied to every --image-override build (repeatable). Enables `RUN --mount=type=secret,id=<id>` in the Dockerfile — the standard private-registry / npm-token recipe (e.g. `--image-build-secret npmrc=./.npmrc`).")).addOption(new Option("--image-target <stage or svc=stage...>", "Repeatable. Bare `<stage>` is a global --target for every --image-override build; `<service>=<stage>` (issue #240) scopes the --target to one overridden target so a monorepo with different multi-stage Dockerfiles per service can stop each at its own intermediate stage. Per-service form overrides the global on the named target.")).addOption(new Option("--no-interactive-overrides", "Suppress the interactive boot prompt that walks each pinned target asking for a Dockerfile path, and the multi-select prompt fired by `--image-override <dockerfile>` (picker form). Useful for CI / scripted invocations.")).addOption(new Option("--strict-overrides", "Fail fast at boot when any pinned target remains uncovered after `--image-override` + the boot prompt resolve. Off by default; the boot WARN per uncovered pinned target still fires regardless.").default(false));
26233
26081
  }
26234
26082
 
26083
+ //#endregion
26084
+ //#region src/cli/commands/local-run-task.ts
26085
+ /**
26086
+ * `cdkl run-task <target>` — Phase 1 of the ECS local-execution
26087
+ * trilogy. Synthesizes the CDK app, locates the target
26088
+ * `AWS::ECS::TaskDefinition`, stands up a per-task docker network with
26089
+ * the AWS-published `amazon-ecs-local-container-endpoints` sidecar, and
26090
+ * starts every container in `dependsOn` order. The essential
26091
+ * container's exit code drives the CLI's exit.
26092
+ */
26093
+ async function localRunTaskCommand(target, options, extraStateProviders) {
26094
+ const logger = getLogger();
26095
+ if (options.verbose) logger.setLevel("debug");
26096
+ const state = createEcsRunState();
26097
+ let sigintHandler;
26098
+ let sigintCount = 0;
26099
+ let stateProvider;
26100
+ let profileCredsFile;
26101
+ let cleanupPromise;
26102
+ const cleanup = async () => {
26103
+ if (!cleanupPromise) cleanupPromise = (async () => {
26104
+ try {
26105
+ await cleanupEcsRun(state, { keepRunning: options.keepRunning });
26106
+ } catch (err) {
26107
+ getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
26108
+ }
26109
+ if (profileCredsFile) try {
26110
+ await profileCredsFile.dispose();
26111
+ } catch (err) {
26112
+ getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
26113
+ }
26114
+ })();
26115
+ await cleanupPromise;
26116
+ };
26117
+ try {
26118
+ await applyRoleArnIfSet({
26119
+ roleArn: options.roleArn,
26120
+ region: options.region,
26121
+ profile: options.profile
26122
+ });
26123
+ await ensureDockerAvailable();
26124
+ const appCmd = resolveApp(options.app);
26125
+ if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
26126
+ logger.info("Synthesizing CDK app...");
26127
+ const synthesizer = new Synthesizer();
26128
+ const context = parseContextOptions(options.context);
26129
+ const synthOpts = {
26130
+ app: appCmd,
26131
+ output: options.output,
26132
+ ...options.region && { region: options.region },
26133
+ ...options.profile && { profile: options.profile },
26134
+ ...Object.keys(context).length > 0 && { context }
26135
+ };
26136
+ const { stacks } = await synthesizer.synthesize(synthOpts);
26137
+ const resolvedTarget = await resolveSingleTarget(target, {
26138
+ entries: listTargets(stacks).ecsTaskDefinitions,
26139
+ message: "Select an ECS task definition to run",
26140
+ noun: "ECS task definitions",
26141
+ onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} run-task requires a <target> (an ECS task definition display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or run it in a TTY to pick interactively.`, "LOCAL_RUN_TASK_TARGET_REQUIRED")
26142
+ });
26143
+ const candidate = pickCandidateStack(parseEcsTarget(resolvedTarget).stackPattern, stacks);
26144
+ stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
26145
+ const imageContext = await buildEcsImageResolutionContext(candidate, stateProvider, options);
26146
+ const task = resolveEcsTaskTarget(resolvedTarget, stacks, imageContext);
26147
+ logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
26148
+ const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
26149
+ if (stateProvider && taskNeeds.needsCrossStackResolver) {
26150
+ const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
26151
+ const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
26152
+ if (resolver) await applyCrossStackResolverToTask(task, {
26153
+ resources: imageContext?.stateResources ?? {},
26154
+ ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
26155
+ ...imageContext?.stateParameters && { parameters: imageContext.stateParameters },
26156
+ ...imageContext?.stateSensitiveParameters?.length && { sensitiveParameters: new Set(imageContext.stateSensitiveParameters) },
26157
+ consumerRegion,
26158
+ crossStackResolver: resolver
26159
+ });
26160
+ } else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state.");
26161
+ sigintHandler = () => {
26162
+ sigintCount += 1;
26163
+ if (sigintCount >= 2) {
26164
+ process.stderr.write("Force-exit on second ^C; container cleanup skipped.\n");
26165
+ process.exit(130);
26166
+ }
26167
+ logger.info("Stopping task...");
26168
+ cleanup().then(() => process.exit(130));
26169
+ };
26170
+ process.on("SIGINT", sigintHandler);
26171
+ const effectiveAssumeRole = resolveEcsAssumeRoleOption(options);
26172
+ let assumedCredentials;
26173
+ let resolvedRoleArn;
26174
+ if (effectiveAssumeRole === true) {
26175
+ if (!task.taskRoleArn) throw new Error(`--assume-role passed without an ARN but the task definition has no resolvable TaskRoleArn. Either the task definition does not set TaskRoleArn, or it points at a resource ${getEmbedConfig().binaryName} cannot resolve to an IAM Role at synth time. Pass the ARN explicitly: --assume-role <arn>`);
26176
+ resolvedRoleArn = await resolvePlaceholderAccount(task.taskRoleArn, options.region, options.profile);
26177
+ assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region, options.profile);
26178
+ } else if (typeof effectiveAssumeRole === "string") {
26179
+ resolvedRoleArn = effectiveAssumeRole;
26180
+ assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region, options.profile);
26181
+ }
26182
+ const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
26183
+ if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
26184
+ const envOverrides = readEnvOverridesFile(options.envVars);
26185
+ const runOpts = {
26186
+ cluster: options.cluster,
26187
+ containerHost: options.containerHost,
26188
+ skipPull: options.pull === false,
26189
+ skipBuild: options.build === false,
26190
+ keepRunning: options.keepRunning,
26191
+ detach: options.detach
26192
+ };
26193
+ if (envOverrides) runOpts.envOverrides = envOverrides;
26194
+ if (sidecarCredentials) runOpts.taskCredentials = sidecarCredentials;
26195
+ if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
26196
+ if (options.platform) runOpts.platformOverride = options.platform;
26197
+ if (options.region) runOpts.region = options.region;
26198
+ if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
26199
+ if (options.profile) runOpts.profile = options.profile;
26200
+ const hostPortOverrides = parseHostPortOverrides(options.hostPort);
26201
+ if (Object.keys(hostPortOverrides).length > 0) runOpts.hostPortOverrides = hostPortOverrides;
26202
+ if (profileCredsFile) runOpts.profileCredentialsFile = {
26203
+ hostPath: profileCredsFile.hostPath,
26204
+ containerPath: profileCredsFile.containerPath,
26205
+ profileName: profileCredsFile.profileName
26206
+ };
26207
+ const result = await runEcsTask(task, runOpts, state);
26208
+ if (options.detach) {
26209
+ logger.info(`Task containers started in detached mode; ${getEmbedConfig().binaryName} is exiting.`);
26210
+ logger.info(`Use 'docker ps --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`);
26211
+ sigintCount = 99;
26212
+ return;
26213
+ }
26214
+ if (result.essentialContainerName) logger.info(`Essential container '${result.essentialContainerName}' exited with code ${result.exitCode}.`);
26215
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
26216
+ } finally {
26217
+ if (sigintHandler) process.off("SIGINT", sigintHandler);
26218
+ if (stateProvider) stateProvider.dispose();
26219
+ if (!options.detach) await cleanup();
26220
+ }
26221
+ }
26222
+ /**
26223
+ * If `arn` contains the `${AWS::AccountId}` placeholder emitted by the
26224
+ * resolver for inline same-stack IAM Roles, substitute the live caller
26225
+ * account via STS `GetCallerIdentity`. Otherwise pass through unchanged.
26226
+ */
26227
+ async function resolvePlaceholderAccount(arn, region, profile) {
26228
+ if (!arn.includes("${AWS::AccountId}")) return arn;
26229
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
26230
+ const sts = new STSClient(buildStsClientConfig({
26231
+ region,
26232
+ profile
26233
+ }));
26234
+ try {
26235
+ const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
26236
+ if (!account) throw new Error(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'. Pass the ARN explicitly: --assume-task-role <arn>`);
26237
+ return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
26238
+ } finally {
26239
+ sts.destroy();
26240
+ }
26241
+ }
26242
+ /**
26243
+ * Assume `roleArn` and return temp credentials.
26244
+ */
26245
+ async function assumeTaskRole(roleArn, region, profile) {
26246
+ const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
26247
+ const sts = new STSClient(buildStsClientConfig({
26248
+ region,
26249
+ profile
26250
+ }));
26251
+ try {
26252
+ const creds = (await sts.send(new AssumeRoleCommand({
26253
+ RoleArn: roleArn,
26254
+ RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-run-task-${Date.now()}`,
26255
+ DurationSeconds: 3600
26256
+ }))).Credentials;
26257
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
26258
+ return {
26259
+ accessKeyId: creds.AccessKeyId,
26260
+ secretAccessKey: creds.SecretAccessKey,
26261
+ sessionToken: creds.SessionToken
26262
+ };
26263
+ } finally {
26264
+ sts.destroy();
26265
+ }
26266
+ }
26267
+ /**
26268
+ * Build the substitution context the ECS task resolver consumes.
26269
+ * Returns `undefined` when no container's `Image` field needs
26270
+ * substitution — the resolver behaves as before in that case.
26271
+ */
26272
+ async function buildEcsImageResolutionContext(candidate, stateProvider, options) {
26273
+ const logger = getLogger();
26274
+ if (!candidate) return void 0;
26275
+ const needs = detectEcsImageResolutionNeeds(candidate);
26276
+ if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
26277
+ const ctx = {};
26278
+ const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
26279
+ if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
26280
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
26281
+ if (!region) logger.warn(`Resolver references \${AWS::Region} but ${getEmbedConfig().binaryName} could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.`);
26282
+ let accountId;
26283
+ try {
26284
+ accountId = await resolveCallerAccountId(region, options.profile);
26285
+ } catch (err) {
26286
+ logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`);
26287
+ }
26288
+ const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
26289
+ ctx.pseudoParameters = {
26290
+ ...accountId !== void 0 && { accountId },
26291
+ ...region !== void 0 && { region },
26292
+ ...partitionAndSuffix && {
26293
+ partition: partitionAndSuffix.partition,
26294
+ urlSuffix: partitionAndSuffix.urlSuffix
26295
+ }
26296
+ };
26297
+ }
26298
+ const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
26299
+ if (stateProvider && wantsState) {
26300
+ const loaded = await stateProvider.load(candidate.stackName, candidate.region);
26301
+ if (loaded) ctx.stateResources = loaded.resources;
26302
+ else {
26303
+ const loadError = stateProvider.getLastLoadError?.();
26304
+ if (loadError) ctx.stateLoadFailureMessage = loadError;
26305
+ }
26306
+ if (needs.needsEnvOrSecretSubstitution && stateProvider.resolveTemplateSsmParameters) {
26307
+ const ssmParameters = await stateProvider.resolveTemplateSsmParameters(candidate.template);
26308
+ if (Object.keys(ssmParameters.values).length > 0) ctx.stateParameters = ssmParameters.values;
26309
+ if (ssmParameters.secureStringLogicalIds.length > 0) ctx.stateSensitiveParameters = ssmParameters.secureStringLogicalIds;
26310
+ }
26311
+ } else if (!stateProvider && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute the deployed repository URI. Otherwise the resolver will surface its existing error.");
26312
+ else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state. Without a state source these entries are dropped (per-key warnings will follow).");
26313
+ return ctx;
26314
+ }
26315
+ function pickCandidateStack(stackPattern, stacks) {
26316
+ if (stackPattern === null) {
26317
+ if (stacks.length === 1) return stacks[0];
26318
+ return;
26319
+ }
26320
+ const matched = matchStacks(stacks, [stackPattern]);
26321
+ if (matched.length === 1) return matched[0];
26322
+ }
26323
+ async function resolveCallerAccountId(region, profile) {
26324
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
26325
+ const sts = new STSClient(buildStsClientConfig({
26326
+ region,
26327
+ profile
26328
+ }));
26329
+ try {
26330
+ return (await sts.send(new GetCallerIdentityCommand({}))).Account;
26331
+ } finally {
26332
+ sts.destroy();
26333
+ }
26334
+ }
26335
+ /**
26336
+ * Read the `--env-vars` JSON file using the same SAM-style shape as
26337
+ * `cdkl invoke --env-vars`: top-level keys are container names, with
26338
+ * `Parameters` reserved for global entries.
26339
+ */
26340
+ function readEnvOverridesFile(filePath) {
26341
+ if (!filePath) return void 0;
26342
+ let raw;
26343
+ try {
26344
+ raw = readFileSync(filePath, "utf-8");
26345
+ } catch (err) {
26346
+ throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
26347
+ }
26348
+ let parsed;
26349
+ try {
26350
+ parsed = JSON.parse(raw);
26351
+ } catch (err) {
26352
+ throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
26353
+ }
26354
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
26355
+ return parsed;
26356
+ }
26357
+ /**
26358
+ * Pick the credentials forwarded to the AWS-published
26359
+ * `amazon-ecs-local-container-endpoints` sidecar. Precedence:
26360
+ * 1. `--assume-task-role <arn>` (or bare `--assume-task-role` against
26361
+ * a resolvable `TaskRoleArn`) → STS-assumed temp creds. Highest
26362
+ * priority — when the user opted in to IAM emulation, those creds
26363
+ * drive the sidecar regardless of `--profile`.
26364
+ * 2. `--profile <p>` → resolved via {@link resolveProfileCredentials}
26365
+ * (the SDK's default credential provider chain — SSO / IAM
26366
+ * Identity Center / fromIni / role-assumption). NEW in this PR.
26367
+ * 3. Neither set → `undefined`; the sidecar runs with its own
26368
+ * default credential chain (typically empty inside a fresh
26369
+ * container — user containers will get 4xx from the credentials
26370
+ * endpoint, mimicking IAM-misconfigured prod).
26371
+ *
26372
+ * Extracted as an exported helper so a unit test can exercise every
26373
+ * branch without having to mock the full Synth + Docker + AWS pipeline
26374
+ * (the strategy used for the Lambda container path).
26375
+ */
26376
+ async function resolveSidecarCredentials(options, assumedCredentials) {
26377
+ if (assumedCredentials) return assumedCredentials;
26378
+ if (options.profile) return resolveProfileCredentials(options.profile);
26379
+ }
26380
+ function createLocalRunTaskCommand(opts = {}) {
26381
+ setEmbedConfig(opts.embedConfig);
26382
+ 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. Omit <target> in an interactive terminal to pick the task definition from a list.").argument("[target]", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run (omit to pick interactively in a TTY)").action(withErrorHandling(async (target, options) => {
26383
+ await localRunTaskCommand(target, options, opts.extraStateProviders);
26384
+ }));
26385
+ addRunTaskSpecificOptions(cmd);
26386
+ [
26387
+ ...commonOptions(),
26388
+ ...appOptions(),
26389
+ ...contextOptions
26390
+ ].forEach((opt) => cmd.addOption(opt));
26391
+ cmd.addOption(regionOption);
26392
+ return cmd;
26393
+ }
26394
+ /**
26395
+ * Register the option block that `cdkl run-task` adds on top of the shared
26396
+ * common / app / context option helpers. Shared between `cdkl run-task` and
26397
+ * any host CLI (e.g. cdkd's `local run-task`) that wraps the single-task
26398
+ * ECS local runner, so adding or renaming a `run-task`-only flag here
26399
+ * propagates to every embedder without duplicate `.addOption(...)` blocks.
26400
+ *
26401
+ * Calling order only affects `--help` presentation (Commander parses
26402
+ * insertion-order-independent). The host-CLI convention is host-specific
26403
+ * options first, then this helper, then the shared common / app / context
26404
+ * options — host flags / run-task flags / common flags grouped in three
26405
+ * `--help` clusters. Chainable: returns `cmd`.
26406
+ *
26407
+ * NOTE: `run-task` does NOT compose with {@link addCommonEcsServiceOptions}
26408
+ * even though many flags overlap. The two ECS surfaces (single-task vs
26409
+ * multi-replica service) have intentionally divergent defaults
26410
+ * (`run-task` has no `--max-tasks` / `--restart-policy`; `start-service`
26411
+ * / `start-alb` have no `--host-port` / `--keep-running` / `--detach`),
26412
+ * and folding `run-task` into the service common block would mutate the
26413
+ * surface non-trivially. Each command keeps its own helper.
26414
+ */
26415
+ function addRunTaskSpecificOptions(cmd) {
26416
+ cmd.addOption(ecsClusterOption()).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("--host-port <containerPort=hostPort...>", "Publish a container port on a specific host port (e.g. 80=8080); repeatable. Default: host port == container port. Use this on macOS to map a privileged container port (< 1024) to a non-privileged host port and avoid the Docker Desktop admin-password prompt."));
26417
+ addEcsAssumeRoleOptions(cmd);
26418
+ cmd.addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--no-build", "Skip docker build on every CDK-asset container (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 ECR-pull / public-registry containers. Compatible with --no-pull.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. 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-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via ListStackResources 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 ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt in container Environment[].Value is warn-and-dropped: CFn ListStackResources does not return per-attribute values, and unlike Lambda (where \`cdkl invoke --from-cfn-stack\` recovers Fn::GetAtt from the deployed function via lambda:GetFunctionConfiguration), no ECS-side equivalent resolves attributes off a deployed task / service.`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
26419
+ return cmd;
26420
+ }
26421
+
26235
26422
  //#endregion
26236
26423
  //#region src/cli/commands/local-start-service.ts
26237
26424
  /**
@@ -27156,7 +27343,7 @@ function createLocalStartAlbCommand(opts = {}) {
27156
27343
  */
27157
27344
  function addAlbSpecificOptions(cmd) {
27158
27345
  addImageOverrideOptions(cmd);
27159
- return cmd.addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls", "Terminate TLS locally for cloud-HTTPS listeners. Default: a cloud-HTTPS listener is served over plain HTTP locally (X-Forwarded-Proto: https is preserved so the upstream app still sees the deployed listener protocol). Implied by --tls-cert / --tls-key. Use this when local-dev cookies need Secure / SameSite=None, when the upstream app inspects TLS metadata, or for mTLS / SNI testing — otherwise plain HTTP is friendlier (no self-signed cert warnings in curl / browser).")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Implies --tls. Must be set together with --tls-key. Pass --tls alone (without --tls-cert / --tls-key) to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Implies --tls. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT injected as Authorization: Bearer <jwt> when the inbound request has none. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Local-dev convenience; cookie pass-through (AWSELBAuthSessionCookie-*) also works.")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica reload of every ECS service behind the ALB when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). A per-firing classifier picks the per-replica primitive: source-only edits on interpreted-language handlers (Node/Python/Ruby/shell) take a bind-mount FAST PATH (`docker cp` the new source into each replica + `docker restart`; no rebuild, front-door pool entry unchanged since the IP/port are preserved). Dockerfile / dependency manifest / compiled-language source / ambiguous edits fall through to the rebuild rolling primitive — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically register it in the front-door pool, then drop the old entry and retire the old container. Either path rolls one replica at a time, so a continuous external request stream against the listener port sees zero connection refusals across the reload. The host front-door (TLS, JWKS cache, Lambda-target containers, listener sockets) stays up across the reload. Lambda target groups behind the ALB are a no-op on reload (the warm RIE container keeps its boot-time image). Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
27346
+ return cmd.addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls", "Terminate TLS locally for cloud-HTTPS listeners. Default: a cloud-HTTPS listener is served over plain HTTP locally (X-Forwarded-Proto: https is preserved so the upstream app still sees the deployed listener protocol). Implied by --tls-cert / --tls-key. Use this when local-dev cookies need Secure / SameSite=None, when the upstream app inspects TLS metadata, or for mTLS / SNI testing — otherwise plain HTTP is friendlier (no self-signed cert warnings in curl / browser).")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Implies --tls. Must be set together with --tls-key. Pass --tls alone (without --tls-cert / --tls-key) to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Implies --tls. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT this command INJECTS only when an inbound request has none (the default-when-missing role) — `cdkl start-alb` is the local ALB front-door RECEIVING outside-in requests, so this token is the fallback the front-door slots in as Authorization: Bearer <jwt> if the caller did not already supply one. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Cookie pass-through (AWSELBAuthSessionCookie-*) also bypasses the guard. Contrast with `cdkl invoke-agentcore --bearer-token`, where the role is reversed — that command is the outbound client and ALWAYS presents this token (the supplier).")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica reload of every ECS service behind the ALB when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). A per-firing classifier picks the per-replica primitive: source-only edits on interpreted-language handlers (Node/Python/Ruby/shell) take a bind-mount FAST PATH (`docker cp` the new source into each replica + `docker restart`; no rebuild, front-door pool entry unchanged since the IP/port are preserved). Dockerfile / dependency manifest / compiled-language source / ambiguous edits fall through to the rebuild rolling primitive — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically register it in the front-door pool, then drop the old entry and retire the old container. Either path rolls one replica at a time, so a continuous external request stream against the listener port sees zero connection refusals across the reload. The host front-door (TLS, JWKS cache, Lambda-target containers, listener sockets) stays up across the reload. Lambda target groups behind the ALB are a no-op on reload (the warm RIE container keeps its boot-time image). Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
27160
27347
  }
27161
27348
 
27162
27349
  //#endregion
@@ -27252,5 +27439,5 @@ function addListSpecificOptions(cmd) {
27252
27439
  }
27253
27440
 
27254
27441
  //#endregion
27255
- export { AGENTCORE_SIGV4_SERVICE as $, formatStateRemedy as $n, evaluateResponseParameters as $t, isLocalCdkAssetImage as A, resolveCfnStackName as An, defaultCredentialsLoader as At, createLocalRunTaskCommand as B, parseSelectionExpressionPath as Bn, invokeRequestAuthorizer as Bt, buildImageOverrideTag as C, substituteEnvVarsFromStateAsync as Cn, filterRoutesByApiIdentifier as Ct, resolveImageOverrides as D, rejectExplicitCfnStackWithMultipleStacks as Dn, startApiServer as Dt, parseImageOverrideFlags as E, isCfnFlagPresent as En, readMtlsMaterialsFromDisk as Et, DEFAULT_SHADOW_READY_TIMEOUT_MS as F, resolveSingleTarget as Fn, verifyJwtAuthorizer as Ft, A2A_CONTAINER_PORT as G, AGENTCORE_AGUI_PROTOCOL as Gn, buildCorsConfigFromCloudFrontChain as Gt, addInvokeAgentCoreSpecificOptions as H, pickRefLogicalId as Hn, attachAuthorizers as Ht, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as I, countTargets as In, verifyJwtViaDiscovery as It, MCP_CONTAINER_PORT as J, AGENTCORE_RUNTIME_TYPE as Jn, matchRoute as Jt, A2A_PATH as K, AGENTCORE_HTTP_PROTOCOL as Kn, isFunctionUrlOacFronted as Kt, setShadowReadyTimeoutMs as L, listTargets as Ln, buildMethodArn as Lt, buildCloudMapIndex as M, collectSsmParameterRefs as Mn, buildJwksUrlFromIssuer as Mt, CloudMapRegistry as N, resolveSsmParameters as Nn, createJwksCache as Nt, runImageOverrideBuilds as O, resolveCfnFallbackRegion as On, resolveSelectionExpression as Ot, classifySourceChange as P, resolveWatchConfig as Pn, verifyCognitoJwt as Pt, parseSseForJsonRpc as Q, derivePseudoParametersFromRegion as Qn, buildRestV1Event as Qt, getContainerNetworkIp as R, discoverWebSocketApis as Rn, computeRequestIdentityHash as Rt, ImageOverrideError as S, substituteEnvVarsFromState as Sn, availableApiIdentifiers as St, mergeForService as T, createLocalStateProvider as Tn, groupRoutesByServer as Tt, createLocalInvokeAgentCoreCommand as U, resolveLambdaArnIntrinsic as Un, applyCorsResponseHeaders as Ut, attachContainerLogStreamer as V, discoverRoutes as Vn, invokeTokenAuthorizer as Vt, invokeAgentCoreWs as W, AGENTCORE_A2A_PROTOCOL as Wn, buildCorsConfigByApiId as Wt, MCP_PROTOCOL_VERSION as X, pickAgentCoreCandidateStack as Xn, applyAuthorizerOverlay as Xt, MCP_PATH as Y, AgentCoreResolutionError as Yn, translateLambdaResponse as Yt, mcpInvokeOnce as Z, resolveAgentCoreTarget as Zn, buildHttpApiV2Event as Zt, buildEcsImageResolutionContext as _, resolveRuntimeFileExtension as _n, createFileWatcher as _t, albStrategy as a, probeHostGatewaySupport as an, SUPPORTED_CODE_RUNTIMES as at, resolveSharedSidecarCredentials as b, substituteAgainstState as bn, materializeLayerFromArn as bt, resolveAlbTarget as c, buildMgmtEndpointEnvUrl as cn, renderCodeDockerfile as ct, addStartServiceSpecificOptions as d, buildConnectEvent as dn, createLocalInvokeCommand as dt, pickResponseTemplate as en, substituteImagePlaceholders as er, signAgentCoreInvocation as et, createLocalStartServiceCommand as f, buildDisconnectEvent as fn, addStartApiSpecificOptions as ft, addImageOverrideOptions as g, resolveRuntimeCodeMountPath as gn, createAuthorizerCache as gt, addCommonEcsServiceOptions as h, buildContainerImage as hn, resolveApiTargetSubset as ht, addAlbSpecificOptions as i, HOST_GATEWAY_MIN_VERSION as in, resolveProfileCredentials as ir, downloadAndExtractS3Bundle as it, listPinnedTargets as j, CfnLocalStateProvider as jn, buildCognitoJwksUrl as jt, describePinnedImageUri as k, resolveCfnRegion as kn, resolveServiceIntegrationParameters as kt, isApplicationLoadBalancer as l, handleConnectionsRequest as ln, toCmdArgv as lt, MAX_TASKS_SUBNET_RANGE_CAP as m, architectureToPlatform as mn, createWatchPredicates as mt, createLocalListCommand as n, tryParseStatus as nn, LocalInvokeBuildError as nr, invokeAgentCore as nt, createLocalStartAlbCommand as o, bufferToBody as on, buildAgentCoreCodeImage as ot, serviceStrategy as p, buildMessageEvent as pn, createLocalStartApiCommand as pt, a2aInvokeOnce as q, AGENTCORE_MCP_PROTOCOL as qn, matchPreflight as qt, formatTargetListing as r, VtlEvaluationError as rn, buildStsClientConfig as rr, waitForAgentCorePing as rt, parseLbPortOverrides as s, ConnectionRegistry as sn, computeCodeImageTag as st, addListSpecificOptions as t, selectIntegrationResponse as tn, tryResolveImageFnJoin as tr, AGENTCORE_SESSION_ID_HEADER as tt, resolveAlbFrontDoor as u, parseConnectionsPath as un, addInvokeSpecificOptions as ut, parseMaxTasks as v, resolveRuntimeImage as vn, attachStageContext as vt, enforceImageOverrideOrphans as w, LocalStateSourceError as wn, filterRoutesByApiIdentifiers as wt, runEcsServiceEmulator as x, substituteAgainstStateAsync as xn, resolveEnvVars as xt, parseRestartPolicy as y, EcsTaskResolutionError as yn, buildStageMap as yt, addRunTaskSpecificOptions as z, discoverWebSocketApisOrThrow as zn, evaluateCachedLambdaPolicy as zt };
27256
- //# sourceMappingURL=local-list-DgwUotOK.js.map
27442
+ export { MCP_PROTOCOL_VERSION as $, pickAgentCoreCandidateStack as $n, applyAuthorizerOverlay as $t, mergeForService as A, rejectExplicitCfnStackWithMultipleStacks as An, startApiServer as At, DEFAULT_SHADOW_READY_TIMEOUT_MS as B, listTargets as Bn, buildMethodArn as Bt, parseRestartPolicy as C, substituteAgainstState as Cn, materializeLayerFromArn as Ct, ImageOverrideError as D, LocalStateSourceError as Dn, filterRoutesByApiIdentifiers as Dt, runEcsServiceEmulator as E, substituteEnvVarsFromStateAsync as En, filterRoutesByApiIdentifier as Et, isLocalCdkAssetImage as F, collectSsmParameterRefs as Fn, buildJwksUrlFromIssuer as Ft, addInvokeAgentCoreSpecificOptions as G, pickRefLogicalId as Gn, attachAuthorizers as Gt, setShadowReadyTimeoutMs as H, discoverWebSocketApisOrThrow as Hn, evaluateCachedLambdaPolicy as Ht, listPinnedTargets as I, resolveSsmParameters as In, createJwksCache as It, A2A_CONTAINER_PORT as J, AGENTCORE_AGUI_PROTOCOL as Jn, buildCorsConfigFromCloudFrontChain as Jt, createLocalInvokeAgentCoreCommand as K, resolveLambdaArnIntrinsic as Kn, applyCorsResponseHeaders as Kt, buildCloudMapIndex as L, resolveWatchConfig as Ln, verifyCognitoJwt as Lt, resolveImageOverrides as M, resolveCfnRegion as Mn, resolveServiceIntegrationParameters as Mt, runImageOverrideBuilds as N, resolveCfnStackName as Nn, defaultCredentialsLoader as Nt, buildImageOverrideTag as O, createLocalStateProvider as On, groupRoutesByServer as Ot, describePinnedImageUri as P, CfnLocalStateProvider as Pn, buildCognitoJwksUrl as Pt, MCP_PATH as Q, AgentCoreResolutionError as Qn, translateLambdaResponse as Qt, CloudMapRegistry as R, resolveSingleTarget as Rn, verifyJwtAuthorizer as Rt, parseMaxTasks as S, EcsTaskResolutionError as Sn, buildStageMap as St, resolveSharedSidecarCredentials as T, substituteEnvVarsFromState as Tn, availableApiIdentifiers as Tt, getContainerNetworkIp as U, parseSelectionExpressionPath as Un, invokeRequestAuthorizer as Ut, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as V, discoverWebSocketApis as Vn, computeRequestIdentityHash as Vt, attachContainerLogStreamer as W, discoverRoutes as Wn, invokeTokenAuthorizer as Wt, a2aInvokeOnce as X, AGENTCORE_MCP_PROTOCOL as Xn, matchPreflight as Xt, A2A_PATH as Y, AGENTCORE_HTTP_PROTOCOL as Yn, isFunctionUrlOacFronted as Yt, MCP_CONTAINER_PORT as Z, AGENTCORE_RUNTIME_TYPE as Zn, matchRoute as Zt, addCommonEcsServiceOptions as _, architectureToPlatform as _n, createWatchPredicates as _t, albStrategy as a, tryParseStatus as an, LocalInvokeBuildError as ar, invokeAgentCore as at, buildEcsImageResolutionContext$1 as b, resolveRuntimeFileExtension as bn, createFileWatcher as bt, resolveAlbTarget as c, probeHostGatewaySupport as cn, SUPPORTED_CODE_RUNTIMES as ct, addStartServiceSpecificOptions as d, buildMgmtEndpointEnvUrl as dn, renderCodeDockerfile as dt, buildHttpApiV2Event as en, resolveAgentCoreTarget as er, mcpInvokeOnce as et, createLocalStartServiceCommand as f, handleConnectionsRequest as fn, toCmdArgv as ft, MAX_TASKS_SUBNET_RANGE_CAP as g, buildMessageEvent as gn, createLocalStartApiCommand as gt, createLocalRunTaskCommand as h, buildDisconnectEvent as hn, addStartApiSpecificOptions as ht, addAlbSpecificOptions as i, selectIntegrationResponse as in, tryResolveImageFnJoin as ir, AGENTCORE_SESSION_ID_HEADER as it, parseImageOverrideFlags as j, resolveCfnFallbackRegion as jn, resolveSelectionExpression as jt, enforceImageOverrideOrphans as k, isCfnFlagPresent as kn, readMtlsMaterialsFromDisk as kt, isApplicationLoadBalancer as l, bufferToBody as ln, buildAgentCoreCodeImage as lt, addRunTaskSpecificOptions as m, buildConnectEvent as mn, createLocalInvokeCommand as mt, createLocalListCommand as n, evaluateResponseParameters as nn, formatStateRemedy as nr, AGENTCORE_SIGV4_SERVICE as nt, createLocalStartAlbCommand as o, VtlEvaluationError as on, buildStsClientConfig as or, waitForAgentCorePing as ot, serviceStrategy as p, parseConnectionsPath as pn, addInvokeSpecificOptions as pt, invokeAgentCoreWs as q, AGENTCORE_A2A_PROTOCOL as qn, buildCorsConfigByApiId as qt, formatTargetListing as r, pickResponseTemplate as rn, substituteImagePlaceholders as rr, signAgentCoreInvocation as rt, parseLbPortOverrides as s, HOST_GATEWAY_MIN_VERSION as sn, resolveProfileCredentials as sr, downloadAndExtractS3Bundle as st, addListSpecificOptions as t, buildRestV1Event as tn, derivePseudoParametersFromRegion as tr, parseSseForJsonRpc as tt, resolveAlbFrontDoor as u, ConnectionRegistry as un, computeCodeImageTag as ut, addEcsAssumeRoleOptions as v, buildContainerImage as vn, resolveApiTargetSubset as vt, resolveEcsAssumeRoleOption as w, substituteAgainstStateAsync as wn, resolveEnvVars as wt, ecsClusterOption as x, resolveRuntimeImage as xn, attachStageContext as xt, addImageOverrideOptions as y, resolveRuntimeCodeMountPath as yn, createAuthorizerCache as yt, classifySourceChange as z, countTargets as zn, verifyJwtViaDiscovery as zt };
27443
+ //# sourceMappingURL=local-list-CFINvNxo.js.map