cdk-local 0.30.0 → 0.32.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-DvoehoBl.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--8TRSn9z.js";
2
2
  import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import * as path from "node:path";
@@ -91,17 +91,6 @@ function appOptions() {
91
91
  * Context options.
92
92
  */
93
93
  const contextOptions = [new Option("-c, --context <key=value...>", "Set context values (can be specified multiple times)")];
94
- /**
95
- * `-i, --interactive` — present an arrow-key picker to choose the
96
- * target(s) for this command instead of typing a CDK path / logical ID.
97
- * Added to the four run commands (NOT `list`, which lists everything).
98
- * Requires a TTY; in a non-interactive shell the command errors with a
99
- * clear message. The required-target commands (`invoke` / `run-task` /
100
- * `start-service`) also auto-launch the picker when the target is
101
- * omitted in a TTY; `start-api` shows it only with the explicit flag
102
- * (a bare `start-api` keeps serving every discovered API).
103
- */
104
- const interactiveOption = new Option("-i, --interactive", "Pick the target(s) from an interactive list instead of passing them as arguments (requires a TTY)").default(false);
105
94
  const IAM_ROLE_ARN_REGEX = /^arn:[^:]+:iam::\d+:role\//;
106
95
  function parseAssumeRoleToken(raw, previous) {
107
96
  const acc = previous ?? { perLambda: {} };
@@ -2000,7 +1989,7 @@ function readApiCdkPath(logicalId, template) {
2000
1989
 
2001
1990
  //#endregion
2002
1991
  //#region src/local/target-lister.ts
2003
- function makeEntry(stackName, logicalId, cdkPath) {
1992
+ function makeEntry(stackName, logicalId, cdkPath, kind) {
2004
1993
  const entry = {
2005
1994
  logicalId,
2006
1995
  stackName,
@@ -2008,8 +1997,17 @@ function makeEntry(stackName, logicalId, cdkPath) {
2008
1997
  };
2009
1998
  const display = cdkPath ? cdkPath.replace(/\/Resource$/, "") : void 0;
2010
1999
  if (display) entry.displayPath = display;
2000
+ if (kind) entry.kind = kind;
2011
2001
  return entry;
2012
2002
  }
2003
+ /** Map a discovered route's `source` to the human-readable surface kind. */
2004
+ function apiKindLabel(source) {
2005
+ switch (source) {
2006
+ case "http-api": return "HTTP API v2";
2007
+ case "rest-v1": return "REST API v1";
2008
+ case "function-url": return "Function URL";
2009
+ }
2010
+ }
2013
2011
  function scanByType(stacks, type) {
2014
2012
  const entries = [];
2015
2013
  for (const stack of stacks) {
@@ -2038,22 +2036,22 @@ function scanByType(stacks, type) {
2038
2036
  */
2039
2037
  function listApiSurfaces(stacks) {
2040
2038
  const byKey = /* @__PURE__ */ new Map();
2041
- const add = (stackName, logicalId, cdkPath) => {
2039
+ const add = (stackName, logicalId, cdkPath, kind) => {
2042
2040
  const key = `${stackName}:${logicalId}`;
2043
- if (!byKey.has(key)) byKey.set(key, makeEntry(stackName, logicalId, cdkPath));
2041
+ if (!byKey.has(key)) byKey.set(key, makeEntry(stackName, logicalId, cdkPath, kind));
2044
2042
  };
2045
2043
  try {
2046
2044
  for (const route of discoverRoutes(stacks)) {
2047
2045
  if (!route.apiStackName) continue;
2048
2046
  const logicalId = route.source === "function-url" ? route.lambdaLogicalId : route.apiLogicalId;
2049
2047
  if (!logicalId) continue;
2050
- add(route.apiStackName, logicalId, route.apiCdkPath);
2048
+ add(route.apiStackName, logicalId, route.apiCdkPath, apiKindLabel(route.source));
2051
2049
  }
2052
2050
  } catch (err) {
2053
2051
  getLogger().warn(`Could not enumerate REST / HTTP / Function URL targets: ${err instanceof Error ? err.message : String(err)}`);
2054
2052
  }
2055
2053
  const { apis, errors } = discoverWebSocketApis(stacks);
2056
- for (const api of apis) add(api.apiStackName, api.apiLogicalId, api.apiCdkPath);
2054
+ for (const api of apis) add(api.apiStackName, api.apiLogicalId, api.apiCdkPath, "WebSocket");
2057
2055
  for (const e of errors) getLogger().warn(`Could not enumerate a WebSocket API target: ${e}`);
2058
2056
  return [...byKey.values()];
2059
2057
  }
@@ -2099,7 +2097,7 @@ function toOption(entry) {
2099
2097
  value,
2100
2098
  label: value
2101
2099
  };
2102
- if (entry.displayPath) option.hint = entry.qualifiedId;
2100
+ if (entry.kind) option.hint = entry.kind;
2103
2101
  return option;
2104
2102
  }
2105
2103
  /**
@@ -2123,19 +2121,25 @@ async function pickOneTarget(message, entries) {
2123
2121
  * The key hint is baked into the message because multi-select's
2124
2122
  * space-to-toggle is not discoverable — users expect enter to pick the
2125
2123
  * highlighted row and miss that nothing is selected yet.
2124
+ *
2125
+ * When `preselectAll` is true, every row starts selected (via
2126
+ * `@clack/prompts` `initialValues`) so a bare Enter confirms the whole
2127
+ * set — used by `start-api`, whose long-standing default is "serve every
2128
+ * discovered API". The user deselects rows to serve a subset.
2126
2129
  */
2127
- async function pickManyTargets(message, entries) {
2130
+ async function pickManyTargets(message, entries, options = {}) {
2131
+ const opts = entries.map(toOption);
2128
2132
  const chosen = await multiselect({
2129
2133
  message: `${message} (space to select, enter to confirm)`,
2130
- options: entries.map(toOption),
2131
- required: true
2134
+ options: opts,
2135
+ required: true,
2136
+ ...options.preselectAll && { initialValues: opts.map((o) => o.value) }
2132
2137
  });
2133
2138
  if (isCancel(chosen)) throw new TargetSelectionCancelledError();
2134
2139
  return chosen;
2135
2140
  }
2136
- function ensureCanPrompt(interactive, onMissing) {
2141
+ function ensureCanPrompt(onMissing) {
2137
2142
  if (isInteractive()) return;
2138
- if (interactive) throw new InteractiveTtyRequiredError("`-i/--interactive` requires an interactive terminal, but stdin/stdout is not a TTY.");
2139
2143
  throw onMissing();
2140
2144
  }
2141
2145
  function ensureHasCandidates(count, noun) {
@@ -2144,17 +2148,15 @@ function ensureHasCandidates(count, noun) {
2144
2148
  }
2145
2149
  /**
2146
2150
  * Resolve a single positional target, prompting interactively when the
2147
- * user passed `-i/--interactive` or omitted the target in a TTY.
2151
+ * target is omitted in a TTY.
2148
2152
  *
2149
- * - `provided` set and no `-i` → returned as-is (no prompt).
2150
- * - `-i` set → always prompt (any `provided` value is ignored).
2153
+ * - `provided` set → returned as-is (no prompt).
2151
2154
  * - omitted, TTY → prompt.
2152
2155
  * - omitted, no TTY → `onMissing()` (the command's required-arg error).
2153
2156
  */
2154
2157
  async function resolveSingleTarget(provided, params) {
2155
- if (provided && !params.interactive) return provided;
2156
- if (provided && params.interactive) getLogger().warn(`-i/--interactive ignores the provided target '${provided}' — pick one from the list instead.`);
2157
- ensureCanPrompt(params.interactive, params.onMissing);
2158
+ if (provided) return provided;
2159
+ ensureCanPrompt(params.onMissing);
2158
2160
  ensureHasCandidates(params.entries.length, params.noun);
2159
2161
  return pickOneTarget(params.message, params.entries);
2160
2162
  }
@@ -2164,9 +2166,8 @@ async function resolveSingleTarget(provided, params) {
2164
2166
  * rules as {@link resolveSingleTarget}.
2165
2167
  */
2166
2168
  async function resolveMultiTarget(provided, params) {
2167
- if (provided.length > 0 && !params.interactive) return provided;
2168
- if (provided.length > 0 && params.interactive) getLogger().warn(`-i/--interactive ignores the provided target(s) [${provided.join(", ")}] — pick from the list instead.`);
2169
- ensureCanPrompt(params.interactive, params.onMissing);
2169
+ if (provided.length > 0) return provided;
2170
+ ensureCanPrompt(params.onMissing);
2170
2171
  ensureHasCandidates(params.entries.length, params.noun);
2171
2172
  return pickManyTargets(params.message, params.entries);
2172
2173
  }
@@ -7303,11 +7304,10 @@ async function localInvokeCommand(target, options, extraStateProviders) {
7303
7304
  };
7304
7305
  const { stacks } = await synthesizer.synthesize(synthOpts);
7305
7306
  const lambda = resolveLambdaTarget(await resolveSingleTarget(target, {
7306
- interactive: options.interactive,
7307
7307
  entries: listTargets(stacks).lambdas,
7308
7308
  message: "Select a Lambda function to invoke",
7309
7309
  noun: "Lambda functions",
7310
- onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} invoke requires a <target> (a Lambda display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or pass -i to pick interactively.`, "LOCAL_INVOKE_TARGET_REQUIRED")
7310
+ onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} invoke requires a <target> (a Lambda display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or run it in a TTY to pick interactively.`, "LOCAL_INVOKE_TARGET_REQUIRED")
7311
7311
  }), stacks);
7312
7312
  const targetLabel = lambda.kind === "zip" ? lambda.runtime : "container image";
7313
7313
  logger.info(`Target: ${lambda.stack.stackName}/${lambda.logicalId} (${targetLabel})`);
@@ -7781,7 +7781,7 @@ function pickReferencedLogicalId(intrinsic) {
7781
7781
  }
7782
7782
  function createLocalInvokeCommand(opts = {}) {
7783
7783
  setEmbedConfig(opts.embedConfig);
7784
- const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix. Omit <target> in an interactive terminal (or pass -i) to pick the Lambda from a list.").argument("[target]", "CDK display path or stack-qualified logical ID of the Lambda to invoke (omit to pick interactively in a TTY)").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions. 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.")).action(withErrorHandling(async (target, options) => {
7784
+ const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix. Omit <target> in an interactive terminal to pick the Lambda from a list.").argument("[target]", "CDK display path or stack-qualified logical ID of the Lambda to invoke (omit to pick interactively in a TTY)").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions. 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.")).action(withErrorHandling(async (target, options) => {
7785
7785
  await localInvokeCommand(target, options, opts.extraStateProviders);
7786
7786
  }));
7787
7787
  [
@@ -7789,7 +7789,6 @@ function createLocalInvokeCommand(opts = {}) {
7789
7789
  ...appOptions(),
7790
7790
  ...contextOptions
7791
7791
  ].forEach((option) => invoke.addOption(option));
7792
- invoke.addOption(interactiveOption);
7793
7792
  invoke.addOption(deprecatedRegionOption);
7794
7793
  return invoke;
7795
7794
  }
@@ -13031,14 +13030,15 @@ async function verifySigV4(req, loadCredentials, opts = {}) {
13031
13030
  local = await loadCredentials();
13032
13031
  } catch (err) {
13033
13032
  const reason = err instanceof Error ? err.message : String(err);
13033
+ const { sigV4StrictByDefault, sigV4OptFlag: optFlag } = getEmbedConfig();
13034
13034
  if (opts.strict && !opts.oacFronted) {
13035
- logger.warn(`AWS_IAM authorizer: could not resolve local AWS credentials (${reason}), so the request's SigV4 signature cannot be verified. --strict-sigv4 is set, so cdk-local denies unverifiable IAM requests; remove --strict-sigv4 to warn-and-pass (the default), or configure AWS credentials cdk-local can read.`);
13035
+ logger.warn(sigV4StrictByDefault ? `AWS_IAM authorizer: could not resolve local AWS credentials (${reason}), so the request's SigV4 signature cannot be verified. cdk-local denies unverifiable IAM requests by default; pass ${optFlag} to warn-and-pass, or configure AWS credentials cdk-local can read.` : `AWS_IAM authorizer: could not resolve local AWS credentials (${reason}), so the request's SigV4 signature cannot be verified. ${optFlag} is set, so cdk-local denies unverifiable IAM requests; remove ${optFlag} to warn-and-pass (the default), or configure AWS credentials cdk-local can read.`);
13036
13036
  return {
13037
13037
  allow: false,
13038
13038
  identityHash: void 0
13039
13039
  };
13040
13040
  }
13041
- logger.warn(opts.oacFronted ? `AWS_IAM authorizer: Function URL is fronted by CloudFront OAC (CloudFront re-signs origin requests in production), and local AWS credentials could not be resolved (${reason}). Passing through with unverified principalId 'unverified-no-creds'. Do NOT trust event.requestContext.identity.accessKey in handler code.` : `AWS_IAM authorizer: could not resolve local AWS credentials (${reason}), so the request's SigV4 signature cannot be verified locally (SigV4 is an HMAC shared-secret signature; the deployed API Gateway verifies it against AWS's copy of the secret). Passing through with unverified principalId 'unverified-no-creds' — cdk-local's default for unverifiable IAM requests; pass --strict-sigv4 to deny instead. Do NOT trust event.requestContext.identity.accessKey in handler code.`);
13041
+ logger.warn(opts.oacFronted ? `AWS_IAM authorizer: Function URL is fronted by CloudFront OAC (CloudFront re-signs origin requests in production), and local AWS credentials could not be resolved (${reason}). Passing through with unverified principalId 'unverified-no-creds'. Do NOT trust event.requestContext.identity.accessKey in handler code.` : sigV4StrictByDefault ? `AWS_IAM authorizer: could not resolve local AWS credentials (${reason}), so the request's SigV4 signature cannot be verified locally (SigV4 is an HMAC shared-secret signature; the deployed API Gateway verifies it against AWS's copy of the secret). ${optFlag} is set; passing through with unverified principalId 'unverified-no-creds'. Do NOT trust event.requestContext.identity.accessKey in handler code.` : `AWS_IAM authorizer: could not resolve local AWS credentials (${reason}), so the request's SigV4 signature cannot be verified locally (SigV4 is an HMAC shared-secret signature; the deployed API Gateway verifies it against AWS's copy of the secret). Passing through with unverified principalId 'unverified-no-creds' — cdk-local's default for unverifiable IAM requests; pass ${optFlag} to deny instead. Do NOT trust event.requestContext.identity.accessKey in handler code.`);
13042
13042
  return {
13043
13043
  allow: true,
13044
13044
  principalId: "unverified-no-creds",
@@ -13048,9 +13048,10 @@ async function verifySigV4(req, loadCredentials, opts = {}) {
13048
13048
  if (local.accessKeyId.toLowerCase() !== parsed.credentialAccessKeyId.toLowerCase()) {
13049
13049
  const warned = opts.warnedForeignIds;
13050
13050
  const dedupKey = parsed.credentialAccessKeyId.toLowerCase();
13051
+ const { sigV4StrictByDefault, sigV4OptFlag: optFlag } = getEmbedConfig();
13051
13052
  if (opts.strict && !opts.oacFronted) {
13052
13053
  if (!warned || !warned.has(dedupKey)) {
13053
- logger.warn(`AWS_IAM authorizer: request signed with access-key-id '${parsed.credentialAccessKeyId}', which differs from the AWS credentials cdk-local resolved locally — SigV4 (HMAC / shared-secret) can only be verified with the signer's own credentials, never a federated / Cognito Identity Pool / cross-account signer's. --strict-sigv4 is set, so cdk-local denies it; remove --strict-sigv4 to warn-and-pass (the default), or sign the request with the same credentials cdk-local resolves locally.`);
13054
+ logger.warn(sigV4StrictByDefault ? `AWS_IAM authorizer: request signed with access-key-id '${parsed.credentialAccessKeyId}', which differs from the AWS credentials cdk-local resolved locally — SigV4 (HMAC / shared-secret) can only be verified with the signer's own credentials, never a federated / Cognito Identity Pool / cross-account signer's. cdk-local denies it by default; pass ${optFlag} to warn-and-pass, or sign the request with the same credentials cdk-local resolves locally.` : `AWS_IAM authorizer: request signed with access-key-id '${parsed.credentialAccessKeyId}', which differs from the AWS credentials cdk-local resolved locally — SigV4 (HMAC / shared-secret) can only be verified with the signer's own credentials, never a federated / Cognito Identity Pool / cross-account signer's. ${optFlag} is set, so cdk-local denies it; remove ${optFlag} to warn-and-pass (the default), or sign the request with the same credentials cdk-local resolves locally.`);
13054
13055
  warned?.add(dedupKey);
13055
13056
  }
13056
13057
  return {
@@ -13059,7 +13060,7 @@ async function verifySigV4(req, loadCredentials, opts = {}) {
13059
13060
  };
13060
13061
  }
13061
13062
  if (!warned || !warned.has(dedupKey)) {
13062
- logger.warn(opts.oacFronted ? `AWS_IAM authorizer: Function URL is fronted by CloudFront OAC — in production CloudFront re-signs the origin request, so the local client's signature (access-key-id '${parsed.credentialAccessKeyId}') cannot be verified. Passing through with unverified principalId 'unverified-foreign-identity'. Do NOT trust event.requestContext.authorizer.principalId in handler code.` : `AWS_IAM authorizer: request signed with access-key-id '${parsed.credentialAccessKeyId}', a federated / Cognito Identity Pool / cross-account signer cdk-local cannot verify locally (SigV4 is an HMAC shared-secret signature; the deployed API Gateway verifies it because AWS holds the secret). Passing through with unverified principalId 'unverified-foreign-identity' — cdk-local's default for unverifiable IAM requests; pass --strict-sigv4 to deny instead. Do NOT trust event.requestContext.authorizer.principalId in handler code.`);
13063
+ logger.warn(opts.oacFronted ? `AWS_IAM authorizer: Function URL is fronted by CloudFront OAC — in production CloudFront re-signs the origin request, so the local client's signature (access-key-id '${parsed.credentialAccessKeyId}') cannot be verified. Passing through with unverified principalId 'unverified-foreign-identity'. Do NOT trust event.requestContext.authorizer.principalId in handler code.` : sigV4StrictByDefault ? `AWS_IAM authorizer: request signed with access-key-id '${parsed.credentialAccessKeyId}', a federated / Cognito Identity Pool / cross-account signer cdk-local cannot verify locally (SigV4 is an HMAC shared-secret signature; the deployed API Gateway verifies it because AWS holds the secret). ${optFlag} is set; passing through with unverified principalId 'unverified-foreign-identity'. Do NOT trust event.requestContext.authorizer.principalId in handler code.` : `AWS_IAM authorizer: request signed with access-key-id '${parsed.credentialAccessKeyId}', a federated / Cognito Identity Pool / cross-account signer cdk-local cannot verify locally (SigV4 is an HMAC shared-secret signature; the deployed API Gateway verifies it because AWS holds the secret). Passing through with unverified principalId 'unverified-foreign-identity' — cdk-local's default for unverifiable IAM requests; pass ${optFlag} to deny instead. Do NOT trust event.requestContext.authorizer.principalId in handler code.`);
13063
13064
  warned?.add(dedupKey);
13064
13065
  }
13065
13066
  return {
@@ -14659,6 +14660,27 @@ function filterRoutesByApiIdentifier(routes, identifier) {
14659
14660
  return routes.filter((rwa) => routeMatchesIdentifier(rwa.route, identifier));
14660
14661
  }
14661
14662
  /**
14663
+ * Filter the route list to the UNION of several user-supplied
14664
+ * identifiers — the variadic `cdkl start-api <target...>` shape, where
14665
+ * passing two or more API identifiers serves exactly that subset (each
14666
+ * on its own port, via {@link groupRoutesByServer}).
14667
+ *
14668
+ * A route is kept when it matches ANY of the identifiers (same matching
14669
+ * rules as {@link filterRoutesByApiIdentifier}). Output order is the
14670
+ * input route order, so {@link groupRoutesByServer}'s stable grouping is
14671
+ * preserved. An empty `identifiers` list returns every route unchanged
14672
+ * (the "serve all" default path never calls this with an empty set, but
14673
+ * the no-op behavior keeps the helper total).
14674
+ *
14675
+ * Returns an empty array when no route matches any identifier — the
14676
+ * caller surfaces a "no API matched" error with the available
14677
+ * identifiers (see {@link availableApiIdentifiers}).
14678
+ */
14679
+ function filterRoutesByApiIdentifiers(routes, identifiers) {
14680
+ if (identifiers.length === 0) return [...routes];
14681
+ return routes.filter((rwa) => identifiers.some((id) => routeMatchesIdentifier(rwa.route, id)));
14682
+ }
14683
+ /**
14662
14684
  * Predicate behind {@link filterRoutesByApiIdentifier} and
14663
14685
  * {@link availableApiIdentifiers}'s primary-form selection. Exported
14664
14686
  * for test coverage only — the production code path goes through
@@ -14948,20 +14970,20 @@ function createAuthorizerCache(opts = {}) {
14948
14970
 
14949
14971
  //#endregion
14950
14972
  //#region src/cli/commands/local-start-api.ts
14951
- async function localStartApiCommand(target, options, extraStateProviders) {
14973
+ async function localStartApiCommand(targets, options, extraStateProviders) {
14952
14974
  const logger = getLogger();
14953
14975
  if (options.verbose) logger.setLevel("debug");
14954
- let apiFilter = target;
14976
+ let apiFilters = [...targets];
14955
14977
  if (options.api !== void 0) {
14956
- if (target !== void 0) throw new Error(`Cannot specify both positional target ('${target}') and --api flag ('${options.api}'). Use one or the other. The positional form is preferred — '--api' is a deprecated alias.`);
14978
+ if (targets.length > 0) throw new Error(`Cannot specify both positional target(s) ([${targets.join(", ")}]) and --api flag ('${options.api}'). Use one or the other. The positional form is preferred — '--api' is a deprecated alias.`);
14957
14979
  logger.warn(`[deprecated] --api <id> will be removed in a future major release. Use the positional argument instead: '${getEmbedConfig().cliName} start-api <id>'.`);
14958
- apiFilter = options.api;
14980
+ apiFilters = [options.api];
14959
14981
  }
14960
- assertStartApiInteractiveAllowed(options.interactive, apiFilter, options.allStacks);
14961
- let effectiveTarget = target;
14982
+ const shouldPromptBare = shouldPromptBareMultiSelect(apiFilters, options.allStacks, isInteractive());
14983
+ let effectiveTargets = [...apiFilters];
14962
14984
  const interactivePicked = { value: false };
14963
- const allStacksConflictList = allStacksConflicts(options, target, apiFilter);
14964
- if (allStacksConflictList.length > 0) throw new Error(`--all-stacks serves every stack's API and cannot be combined with a single-target selector (${allStacksConflictList.join(", ")}). Drop --all-stacks to target one stack, or drop the selector to serve them all. The bare --from-cfn-stack flag (no value) IS compatible with --all-stacks.`);
14985
+ const allStacksConflictList = allStacksConflicts(options, targets, apiFilters);
14986
+ 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.`);
14965
14987
  warnIfDeprecatedRegion(options);
14966
14988
  await applyRoleArnIfSet({
14967
14989
  roleArn: options.roleArn,
@@ -14982,6 +15004,7 @@ async function localStartApiCommand(target, options, extraStateProviders) {
14982
15004
  let sigV4CredentialsLoader;
14983
15005
  const sigV4WarnedForeignIds = /* @__PURE__ */ new Set();
14984
15006
  const fromCfnTipEmitted = { value: false };
15007
+ const unmatchedTargetWarned = /* @__PURE__ */ new Set();
14985
15008
  /**
14986
15009
  * One synth + discover + build pass. Returns the next-state
14987
15010
  * material. Reused on initial boot AND every hot-reload firing.
@@ -15005,17 +15028,17 @@ async function localStartApiCommand(target, options, extraStateProviders) {
15005
15028
  ...Object.keys(context).length > 0 && { context }
15006
15029
  };
15007
15030
  const { stacks } = await synthesizer.synthesize(synthOpts);
15008
- if (options.interactive && !interactivePicked.value) {
15031
+ if (shouldPromptBare && !interactivePicked.value) {
15009
15032
  const apis = listTargets(stacks).apis;
15010
15033
  if (apis.length === 0) throw new Error(`No APIs found in this CDK app to choose from. Run \`${getEmbedConfig().cliName} list\` to see what is available.`);
15011
- const picked = await pickOneTarget("Select an API to serve", apis);
15012
- apiFilter = picked;
15013
- effectiveTarget = picked;
15034
+ const picked = await pickManyTargets("Select APIs to serve", apis, { preselectAll: true });
15035
+ apiFilters = picked;
15036
+ effectiveTargets = picked;
15014
15037
  interactivePicked.value = true;
15015
15038
  }
15016
15039
  const cfnStackFallback = typeof options.fromCfnStack === "string" ? options.fromCfnStack : void 0;
15017
- const targetStackPrefix = effectiveTarget?.includes("/") === true ? effectiveTarget.slice(0, effectiveTarget.indexOf("/")) : void 0;
15018
- const targetStacks = pickTargetStacks(stacks, options.stack, cfnStackFallback, targetStackPrefix, options.allStacks);
15040
+ const targetStackPrefix = deriveSynthStackPrefix(effectiveTargets);
15041
+ const targetStacks = pickTargetStacks(stacks, options.stack, cfnStackFallback, targetStackPrefix, options.allStacks || shouldSynthAllStacks(effectiveTargets, options.stack, cfnStackFallback));
15019
15042
  if (targetStacks.length === 0) throw new Error("No stacks matched. Pass --stack <name> (or --from-cfn-stack <name>) or run from a single-stack app.");
15020
15043
  const routedStackNames = targetStacks.map((s) => s.stackName);
15021
15044
  tryEmitFromCfnRedundancyTipOnce(options.fromCfnStack, routedStackNames, fromCfnTipEmitted, (routedStackName) => {
@@ -15041,12 +15064,15 @@ async function localStartApiCommand(target, options, extraStateProviders) {
15041
15064
  }
15042
15065
  attachStageContext(routes, stageMap);
15043
15066
  let routesWithAuth = attachAuthorizers(targetStacks, routes);
15044
- if (apiFilter !== void 0) {
15045
- if (!apiFilter.includes(":") && !apiFilter.includes("/") && targetStacks.length > 1) throw new Error(`Multiple stacks in app, target '${apiFilter}' is missing a stack prefix. Use 'StackName:${apiFilter}' or 'StackName/${apiFilter}' (Construct path form). Available stacks: ${targetStacks.map((s) => s.stackName).join(", ")}.`);
15046
- const filtered = filterRoutesByApiIdentifier(routesWithAuth, apiFilter);
15047
- if (filtered.length === 0) {
15067
+ if (apiFilters.length > 0) {
15068
+ const { filtered, unmatched } = resolveApiTargetSubset(routesWithAuth, apiFilters, targetStacks.map((s) => s.stackName));
15069
+ if (unmatched.length > 0) {
15048
15070
  const available = availableApiIdentifiers(routesWithAuth).join(", ") || "(none)";
15049
- throw new Error(`Target '${apiFilter}' did not match any discovered API. Available identifiers: ${available}.`);
15071
+ for (const id of unmatched) {
15072
+ if (unmatchedTargetWarned.has(id)) continue;
15073
+ unmatchedTargetWarned.add(id);
15074
+ logger.warn(`Target '${id}' did not match any discovered API; it is ignored. Available identifiers: ${available}.`);
15075
+ }
15050
15076
  }
15051
15077
  routesWithAuth = filtered;
15052
15078
  }
@@ -15437,7 +15463,7 @@ function pickTargetStacks(stacks, pattern, cfnStackFallback, targetFallback, all
15437
15463
  }
15438
15464
  /**
15439
15465
  * Issue #55: `--all-stacks` serves every stack's API as a union, so it
15440
- * cannot be combined with a selector that names exactly ONE target.
15466
+ * cannot be combined with a selector that names a target subset.
15441
15467
  * Returns the human-readable list of conflicting selectors (empty when
15442
15468
  * `--all-stacks` is off or there is no conflict).
15443
15469
  *
@@ -15451,31 +15477,121 @@ function pickTargetStacks(stacks, pattern, cfnStackFallback, targetFallback, all
15451
15477
  *
15452
15478
  * @internal exported for unit tests.
15453
15479
  */
15454
- function allStacksConflicts(options, target, apiFilter) {
15480
+ function allStacksConflicts(options, targets, apiFilters) {
15455
15481
  if (!options.allStacks) return [];
15456
15482
  const conflicts = [];
15457
- if (apiFilter !== void 0) conflicts.push(target !== void 0 ? `target '${target}'` : `--api '${options.api}'`);
15483
+ if (apiFilters.length > 0) conflicts.push(targets.length > 0 ? `target(s) [${targets.join(", ")}]` : `--api '${options.api}'`);
15458
15484
  if (options.stack !== void 0) conflicts.push(`--stack '${options.stack}'`);
15459
15485
  if (typeof options.fromCfnStack === "string") conflicts.push(`--from-cfn-stack '${options.fromCfnStack}'`);
15460
15486
  return conflicts;
15461
15487
  }
15462
15488
  /**
15463
- * Validate `-i/--interactive` for `start-api`. The picker is opt-in here
15464
- * (a bare `start-api` serves every API), so `-i` is mutually exclusive
15465
- * with any single-target selector (`apiFilter` = positional target OR
15466
- * `--api`) and with `--all-stacks`, and it requires a TTY. Throws when
15467
- * the combination is invalid; otherwise returns and the prompt runs
15468
- * later inside `synthesizeAndBuild`.
15489
+ * Resolve the variadic `cdkl start-api <target...>` subset against the
15490
+ * discovered route surface the pure core of the subset-serving path.
15491
+ *
15492
+ * Behavior:
15493
+ * - Rejects a BARE logical id (no `:`, no `/`) when the app has more than
15494
+ * one stack: a bare id is ambiguous because two stacks can carry the
15495
+ * same logical id (mirrors `cdkl invoke` / `cdkl run-task`'s resolver).
15496
+ * THROWS with the disambiguation hint.
15497
+ * - Filters routes to the UNION of the identifiers via
15498
+ * {@link filterRoutesByApiIdentifiers}. THROWS when the union is empty
15499
+ * (no identifier matched anything), listing the available identifiers.
15500
+ * - Returns the surviving union in `filtered` and the identifiers that
15501
+ * matched nothing in `unmatched`. A single typo among valid ids keeps
15502
+ * the siblings in `filtered` and surfaces the typo via `unmatched`;
15503
+ * the caller is responsible for warning (so the warning can be gated
15504
+ * one-shot across `--watch` reloads — see the call site).
15505
+ *
15506
+ * `availableApiIdentifiers(routes)` is computed ONCE here (not per-id) so
15507
+ * the resolution is O(N·M) rather than O(N²·M).
15508
+ *
15509
+ * @internal exported for unit tests + library consumers (re-exported from
15510
+ * the package entry).
15511
+ */
15512
+ function resolveApiTargetSubset(routes, identifiers, stackNames) {
15513
+ if (stackNames.length > 1) {
15514
+ for (const id of identifiers) if (!id.includes(":") && !id.includes("/")) throw new Error(`Multiple stacks in app, target '${id}' is missing a stack prefix. Use 'StackName:${id}' or 'StackName/${id}' (Construct path form). Available stacks: ${stackNames.join(", ")}.`);
15515
+ }
15516
+ const filtered = filterRoutesByApiIdentifiers(routes, identifiers);
15517
+ if (filtered.length === 0) {
15518
+ const available = availableApiIdentifiers(routes).join(", ") || "(none)";
15519
+ throw new Error(`Target(s) [${identifiers.join(", ")}] did not match any discovered API. Available identifiers: ${available}.`);
15520
+ }
15521
+ return {
15522
+ filtered,
15523
+ unmatched: identifiers.filter((id) => filterRoutesByApiIdentifiers(routes, [id]).length === 0)
15524
+ };
15525
+ }
15526
+ /**
15527
+ * Derive the single synth stack prefix shared by every supplied target,
15528
+ * for the synth-scope optimization (item 5 of the start-api subset UX).
15529
+ *
15530
+ * Each target's stack prefix is the segment before its first `/` (the
15531
+ * `<StackName>/<construct>` Construct-path form). Returns:
15532
+ * - `undefined` when `targets` is empty (serve-all path — no prefix).
15533
+ * - `undefined` when ANY target lacks a `/` (bare logical id — the synth
15534
+ * target can't be inferred from it, so fall back to the existing
15535
+ * single-stack auto-pick / multi-stack rejection path).
15536
+ * - `undefined` when targets span MORE THAN ONE distinct prefix (the
15537
+ * subset crosses stacks, so synth must cover all of them — see
15538
+ * {@link shouldSynthAllStacks}).
15539
+ * - the shared prefix when every target carries the SAME `<StackName>/`
15540
+ * prefix — keeping the single-stack synth optimization.
15541
+ *
15542
+ * @internal exported for unit tests.
15543
+ */
15544
+ function deriveSynthStackPrefix(targets) {
15545
+ if (targets.length === 0) return void 0;
15546
+ const prefixes = /* @__PURE__ */ new Set();
15547
+ for (const t of targets) {
15548
+ const slash = t.indexOf("/");
15549
+ if (slash === -1) return void 0;
15550
+ prefixes.add(t.slice(0, slash));
15551
+ }
15552
+ return prefixes.size === 1 ? [...prefixes][0] : void 0;
15553
+ }
15554
+ /**
15555
+ * Decide whether synth should cover ALL stacks for the given target
15556
+ * subset (item 5). Synth is scoped to one stack only when a single stack
15557
+ * can be pinned; otherwise we synth every stack and the union is filtered
15558
+ * down by `apiFilters` afterwards.
15559
+ *
15560
+ * Returns `true` (synth all) when there ARE explicit targets but no
15561
+ * single stack can be pinned from them — i.e. `--stack` is unset, an
15562
+ * explicit `--from-cfn-stack <name>` is unset, AND
15563
+ * {@link deriveSynthStackPrefix} can't resolve one shared prefix (targets
15564
+ * span stacks, or any target is a bare logical id). Returns `false` when
15565
+ * there are no targets (serve-all already synths everything via the
15566
+ * regular path) or a stack is otherwise pinned.
15567
+ *
15568
+ * @internal exported for unit tests.
15569
+ */
15570
+ function shouldSynthAllStacks(targets, stackPattern, cfnStackFallback) {
15571
+ if (targets.length === 0) return false;
15572
+ if (stackPattern !== void 0 || cfnStackFallback !== void 0) return false;
15573
+ return deriveSynthStackPrefix(targets) === void 0;
15574
+ }
15575
+ /**
15576
+ * Decide whether bare `start-api` should open the pre-selected-all
15577
+ * multi-select picker. The picker runs ONLY when:
15578
+ * - no explicit API subset was named (`apiFilters` empty — neither
15579
+ * positional `<targets...>` nor the deprecated `--api`),
15580
+ * - `--all-stacks` was not passed (that path already serves every API),
15581
+ * - AND stdin/stdout is a TTY.
15582
+ *
15583
+ * In a non-TTY (CI / pipe) this returns `false`, and the caller serves
15584
+ * EVERY discovered API without prompting — start-api's one intentional
15585
+ * asymmetry vs invoke / run-task (which error when bare in a non-TTY),
15586
+ * because start-api legitimately has a "serve all" default.
15469
15587
  *
15470
- * Extracted as a pure-ish function (TTY is read via {@link isInteractive})
15471
- * so the error contract is unit-testable without booting the server.
15588
+ * Extracted as a pure function (TTY is passed in) so the decision is
15589
+ * unit-testable without booting the server or juggling global TTY state.
15472
15590
  *
15473
15591
  * @internal exported for unit tests.
15474
15592
  */
15475
- function assertStartApiInteractiveAllowed(interactive, apiFilter, allStacks) {
15476
- if (!interactive) return;
15477
- if (apiFilter !== void 0 || allStacks) throw new Error("`-i/--interactive` cannot be combined with a positional target, --api, or --all-stacks; it interactively picks one API to serve. Drop the selector, or drop -i.");
15478
- if (!isInteractive()) throw new InteractiveTtyRequiredError("`-i/--interactive` requires an interactive terminal, but stdin/stdout is not a TTY.");
15593
+ function shouldPromptBareMultiSelect(apiFilters, allStacks, isTty) {
15594
+ return apiFilters.length === 0 && !allStacks && isTty;
15479
15595
  }
15480
15596
  /**
15481
15597
  * Decide whether the `--from-cfn-stack <name>` redundancy tip should
@@ -16563,15 +16679,14 @@ function resolveMtlsConfig(options) {
16563
16679
  */
16564
16680
  function createLocalStartApiCommand(opts = {}) {
16565
16681
  setEmbedConfig(opts.embedConfig);
16566
- const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and AWS_IAM auth (REST v1 `AuthorizationType: AWS_IAM` and Function URL `AuthType: AWS_IAM` — SigV4 signature verification only; IAM policy evaluation is NOT emulated). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", `Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server (pass -i to pick one interactively instead). Mirrors \`${getEmbedConfig().cliName} invoke\` / \`${getEmbedConfig().cliName} run-task\` target syntax.`).addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--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, --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 <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--from-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)).action(withErrorHandling(async (target, options) => {
16567
- await localStartApiCommand(target, options, opts.extraStateProviders);
16682
+ const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and AWS_IAM auth (REST v1 `AuthorizationType: AWS_IAM` and Function URL `AuthType: AWS_IAM` — SigV4 signature verification only; IAM policy evaluation is NOT emulated). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[targets...]", `Optional API subset filter. Pass one or more identifiers to serve exactly that subset (the union; each on its own port). Each accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted in a TTY, a multi-select picker opens with every API pre-selected (Enter serves all, deselect to pick a subset); when omitted in a non-TTY (CI / pipe) every discovered API is served. Mirrors \`${getEmbedConfig().cliName} invoke\` / \`${getEmbedConfig().cliName} run-task\` target syntax.`).addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--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)).action(withErrorHandling(async (targets, options) => {
16683
+ await localStartApiCommand(targets, options, opts.extraStateProviders);
16568
16684
  }));
16569
16685
  [
16570
16686
  ...commonOptions(),
16571
16687
  ...appOptions(),
16572
16688
  ...contextOptions
16573
16689
  ].forEach((opt) => startApi.addOption(opt));
16574
- startApi.addOption(interactiveOption);
16575
16690
  startApi.addOption(deprecatedRegionOption);
16576
16691
  return startApi;
16577
16692
  }
@@ -17627,11 +17742,10 @@ async function localRunTaskCommand(target, options, extraStateProviders) {
17627
17742
  };
17628
17743
  const { stacks } = await synthesizer.synthesize(synthOpts);
17629
17744
  const resolvedTarget = await resolveSingleTarget(target, {
17630
- interactive: options.interactive,
17631
17745
  entries: listTargets(stacks).ecsTaskDefinitions,
17632
17746
  message: "Select an ECS task definition to run",
17633
17747
  noun: "ECS task definitions",
17634
- 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 pass -i to pick interactively.`, "LOCAL_RUN_TASK_TARGET_REQUIRED")
17748
+ 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")
17635
17749
  });
17636
17750
  const candidate = pickCandidateStack$1(parseEcsTarget(resolvedTarget).stackPattern, stacks);
17637
17751
  stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
@@ -17858,7 +17972,7 @@ async function resolveSidecarCredentials(options, assumedCredentials) {
17858
17972
  }
17859
17973
  function createLocalRunTaskCommand(opts = {}) {
17860
17974
  setEmbedConfig(opts.embedConfig);
17861
- 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 (or pass -i) 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)").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 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.")).action(withErrorHandling(async (target, options) => {
17975
+ 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)").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 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.")).action(withErrorHandling(async (target, options) => {
17862
17976
  await localRunTaskCommand(target, options, opts.extraStateProviders);
17863
17977
  }));
17864
17978
  [
@@ -17866,7 +17980,6 @@ function createLocalRunTaskCommand(opts = {}) {
17866
17980
  ...appOptions(),
17867
17981
  ...contextOptions
17868
17982
  ].forEach((opt) => cmd.addOption(opt));
17869
- cmd.addOption(interactiveOption);
17870
17983
  cmd.addOption(deprecatedRegionOption);
17871
17984
  return cmd;
17872
17985
  }
@@ -18604,7 +18717,7 @@ function pickEssentialContainerId(instance, service) {
18604
18717
  const defaultWaitForExitImpl = async (containerId) => {
18605
18718
  const { execFile } = await import("node:child_process");
18606
18719
  const { promisify } = await import("node:util");
18607
- const { getDockerCmd } = await import("./docker-cmd-DvoehoBl.js").then((n) => n.t);
18720
+ const { getDockerCmd } = await import("./docker-cmd--8TRSn9z.js").then((n) => n.t);
18608
18721
  const { stdout } = await promisify(execFile)(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
18609
18722
  const code = parseInt(stdout.trim(), 10);
18610
18723
  return Number.isFinite(code) ? code : -1;
@@ -18624,7 +18737,7 @@ const EXIT_LOG_TAIL_LINES = 50;
18624
18737
  const defaultReadContainerLogsImpl = async (containerId) => {
18625
18738
  const { execFile } = await import("node:child_process");
18626
18739
  const { promisify } = await import("node:util");
18627
- const { getDockerCmd } = await import("./docker-cmd-DvoehoBl.js").then((n) => n.t);
18740
+ const { getDockerCmd } = await import("./docker-cmd--8TRSn9z.js").then((n) => n.t);
18628
18741
  const { stdout, stderr } = await promisify(execFile)(getDockerCmd(), [
18629
18742
  "logs",
18630
18743
  "--tail",
@@ -19040,11 +19153,10 @@ async function localStartServiceCommand(targets, options, extraStateProviders) {
19040
19153
  };
19041
19154
  const { stacks } = await synthesizer.synthesize(synthOpts);
19042
19155
  const resolvedTargets = await resolveMultiTarget(targets, {
19043
- interactive: options.interactive,
19044
19156
  entries: listTargets(stacks).ecsServices,
19045
19157
  message: "Select one or more ECS services to run",
19046
19158
  noun: "ECS services",
19047
- onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend', or run it in a TTY (or with -i) to pick interactively.`)
19159
+ onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend', or run it in a TTY to pick interactively.`)
19048
19160
  });
19049
19161
  rejectExplicitCfnStackWithMultipleStacks(options, resolvedTargets.length);
19050
19162
  perTarget = resolvedTargets.map((t) => ({
@@ -19336,7 +19448,7 @@ async function resolveSharedSidecarCredentials(options) {
19336
19448
  }
19337
19449
  function createLocalStartServiceCommand(opts = {}) {
19338
19450
  setEmbedConfig(opts.embedConfig);
19339
- const cmd = new Command("start-service").description(`Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as \`${getEmbedConfig().cliName} run-task\`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay. Omit <targets> in an interactive terminal (or pass -i) to multi-select the services from a list.`).argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run (omit to multi-select interactively in a TTY)").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. (Single-replica services only — multi-replica services do not publish host ports.)")).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 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.")).action(withErrorHandling(async (targets, options) => {
19451
+ const cmd = new Command("start-service").description(`Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as \`${getEmbedConfig().cliName} run-task\`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay. Omit <targets> in an interactive terminal to multi-select the services from a list.`).argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run (omit to multi-select interactively in a TTY)").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. (Single-replica services only — multi-replica services do not publish host ports.)")).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 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.")).action(withErrorHandling(async (targets, options) => {
19340
19452
  await localStartServiceCommand(targets, options, opts.extraStateProviders);
19341
19453
  }));
19342
19454
  [
@@ -19344,7 +19456,6 @@ function createLocalStartServiceCommand(opts = {}) {
19344
19456
  ...appOptions(),
19345
19457
  ...contextOptions
19346
19458
  ].forEach((opt) => cmd.addOption(opt));
19347
- cmd.addOption(interactiveOption);
19348
19459
  cmd.addOption(deprecatedRegionOption);
19349
19460
  return cmd;
19350
19461
  }
@@ -19388,7 +19499,7 @@ function formatTargetListing(listing, cliName, options = {}) {
19388
19499
  const long = options.long ?? false;
19389
19500
  return "\n" + [
19390
19501
  formatSection("Lambda Functions", `${cliName} invoke <target>`, listing.lambdas, long),
19391
- formatSection("APIs", `${cliName} start-api [target]`, listing.apis, long),
19502
+ formatSection("APIs", `${cliName} start-api [target...]`, listing.apis, long),
19392
19503
  formatSection("ECS Services", `${cliName} start-service <target...>`, listing.ecsServices, long),
19393
19504
  formatSection("ECS Task Definitions", `${cliName} run-task <target>`, listing.ecsTaskDefinitions, long)
19394
19505
  ].filter((lines) => lines.length > 0).map((lines) => lines.join("\n")).join("\n\n");
@@ -19398,14 +19509,14 @@ function formatSection(title, command, entries, long) {
19398
19509
  const lines = [`${title} -> ${command}`];
19399
19510
  for (const entry of entries) {
19400
19511
  const primary = entry.displayPath ?? entry.qualifiedId;
19401
- lines.push(` ${primary}`);
19512
+ lines.push(entry.kind ? ` ${primary} (${entry.kind})` : ` ${primary}`);
19402
19513
  if (long && entry.displayPath) lines.push(` ${entry.qualifiedId}`);
19403
19514
  }
19404
19515
  return lines;
19405
19516
  }
19406
19517
  function createLocalListCommand(opts = {}) {
19407
19518
  setEmbedConfig(opts.embedConfig);
19408
- const cmd = new Command("list").alias("ls").description("List the runnable targets in the synthesized CDK app, grouped by the command that runs them: Lambda functions (invoke), API Gateway REST v1 / HTTP v2 / Function URL / WebSocket surfaces (start-api), ECS services (start-service), and ECS task definitions (run-task). Each target is shown by its CDK display path; pass -l to also print the stack-qualified logical ID. Tip: you usually do not need to copy these — just run the command (e.g. `invoke`) and pick from the list, or pass -i.").addOption(new Option("-l, --long", "Also print each target's stack-qualified logical ID (<Stack>:<LogicalId>) beneath it").default(false)).action(withErrorHandling(async (options) => {
19519
+ const cmd = new Command("list").alias("ls").description("List the runnable targets in the synthesized CDK app, grouped by the command that runs them: Lambda functions (invoke), API Gateway REST v1 / HTTP v2 / Function URL / WebSocket surfaces (start-api), ECS services (start-service), and ECS task definitions (run-task). Each target is shown by its CDK display path; pass -l to also print the stack-qualified logical ID. Tip: you usually do not need to copy these — just run the command (e.g. `invoke`) with no target in a terminal and pick from the list.").addOption(new Option("-l, --long", "Also print each target's stack-qualified logical ID (<Stack>:<LogicalId>) beneath it").default(false)).action(withErrorHandling(async (options) => {
19409
19520
  await localListCommand(options);
19410
19521
  }));
19411
19522
  [
@@ -19418,5 +19529,5 @@ function createLocalListCommand(opts = {}) {
19418
19529
  }
19419
19530
 
19420
19531
  //#endregion
19421
- export { resolveRuntimeFileExtension as $, matchPreflight as A, VtlEvaluationError as B, computeRequestIdentityHash as C, discoverWebSocketApis as Ct, applyCorsResponseHeaders as D, pickRefLogicalId as Dt, invokeTokenAuthorizer as E, discoverRoutes as Et, buildRestV1Event as F, buildMgmtEndpointEnvUrl as G, probeHostGatewaySupport as H, evaluateResponseParameters as I, buildConnectEvent as J, handleConnectionsRequest as K, pickResponseTemplate as L, translateLambdaResponse as M, applyAuthorizerOverlay as N, buildCorsConfigByApiId as O, resolveLambdaArnIntrinsic as Ot, buildHttpApiV2Event as P, resolveRuntimeCodeMountPath as Q, selectIntegrationResponse as R, buildMethodArn as S, listTargets as St, invokeRequestAuthorizer as T, parseSelectionExpressionPath as Tt, bufferToBody as U, HOST_GATEWAY_MIN_VERSION as V, ConnectionRegistry as W, buildMessageEvent as X, buildDisconnectEvent as Y, createLocalInvokeCommand as Z, buildCognitoJwksUrl as _, resolveCfnStackName as _t, CloudMapRegistry as a, substituteEnvVarsFromStateAsync as at, verifyCognitoJwt as b, resolveSsmParameters as bt, createLocalStartApiCommand as c, derivePseudoParametersFromRegion as ct, buildStageMap as d, LocalStateSourceError as dt, resolveRuntimeImage as et, availableApiIdentifiers as f, createLocalStateProvider as ft, resolveServiceIntegrationParameters as g, resolveCfnRegion as gt, resolveSelectionExpression as h, resolveCfnFallbackRegion as ht, buildCloudMapIndex as i, substituteEnvVarsFromState as it, matchRoute as j, buildCorsConfigFromCloudFrontChain as k, createAuthorizerCache as l, substituteImagePlaceholders as lt, groupRoutesByServer as m, rejectExplicitCfnStackWithMultipleStacks as mt, formatTargetListing as n, substituteAgainstState as nt, getContainerNetworkIp as o, resolveEnvVars as ot, filterRoutesByApiIdentifier as p, isCfnFlagPresent as pt, parseConnectionsPath as q, createLocalStartServiceCommand as r, substituteAgainstStateAsync as rt, createLocalRunTaskCommand as s, materializeLayerFromArn as st, createLocalListCommand as t, EcsTaskResolutionError as tt, attachStageContext as u, tryResolveImageFnJoin as ut, buildJwksUrlFromIssuer as v, CfnLocalStateProvider as vt, evaluateCachedLambdaPolicy as w, discoverWebSocketApisOrThrow as wt, verifyJwtAuthorizer as x, countTargets as xt, createJwksCache as y, collectSsmParameterRefs as yt, tryParseStatus as z };
19422
- //# sourceMappingURL=local-list-Qa6V8bzl.js.map
19532
+ export { handleConnectionsRequest as $, invokeRequestAuthorizer as A, discoverWebSocketApis as At, applyAuthorizerOverlay as B, buildJwksUrlFromIssuer as C, resolveCfnRegion as Ct, buildMethodArn as D, resolveSsmParameters as Dt, verifyJwtAuthorizer as E, collectSsmParameterRefs as Et, buildCorsConfigFromCloudFrontChain as F, resolveLambdaArnIntrinsic as Ft, selectIntegrationResponse as G, buildRestV1Event as H, isFunctionUrlOacFronted as I, HOST_GATEWAY_MIN_VERSION as J, tryParseStatus as K, matchPreflight as L, attachAuthorizers as M, parseSelectionExpressionPath as Mt, applyCorsResponseHeaders as N, discoverRoutes as Nt, computeRequestIdentityHash as O, countTargets as Ot, buildCorsConfigByApiId as P, pickRefLogicalId as Pt, buildMgmtEndpointEnvUrl as Q, matchRoute as R, buildCognitoJwksUrl as S, resolveCfnFallbackRegion as St, verifyCognitoJwt as T, CfnLocalStateProvider as Tt, evaluateResponseParameters as U, buildHttpApiV2Event as V, pickResponseTemplate as W, bufferToBody as X, probeHostGatewaySupport as Y, ConnectionRegistry as Z, readMtlsMaterialsFromDisk as _, tryResolveImageFnJoin as _t, CloudMapRegistry as a, resolveRuntimeCodeMountPath as at, resolveServiceIntegrationParameters as b, isCfnFlagPresent as bt, createLocalStartApiCommand as c, EcsTaskResolutionError as ct, attachStageContext as d, substituteEnvVarsFromState as dt, parseConnectionsPath as et, buildStageMap as f, substituteEnvVarsFromStateAsync as ft, groupRoutesByServer as g, substituteImagePlaceholders as gt, filterRoutesByApiIdentifiers as h, derivePseudoParametersFromRegion as ht, buildCloudMapIndex as i, createLocalInvokeCommand as it, invokeTokenAuthorizer as j, discoverWebSocketApisOrThrow as jt, evaluateCachedLambdaPolicy as k, listTargets as kt, resolveApiTargetSubset as l, substituteAgainstState as lt, filterRoutesByApiIdentifier as m, materializeLayerFromArn as mt, formatTargetListing as n, buildDisconnectEvent as nt, getContainerNetworkIp as o, resolveRuntimeFileExtension as ot, availableApiIdentifiers as p, resolveEnvVars as pt, VtlEvaluationError as q, createLocalStartServiceCommand as r, buildMessageEvent as rt, createLocalRunTaskCommand as s, resolveRuntimeImage as st, createLocalListCommand as t, buildConnectEvent as tt, createAuthorizerCache as u, substituteAgainstStateAsync as ut, startApiServer as v, LocalStateSourceError as vt, createJwksCache as w, resolveCfnStackName as wt, defaultCredentialsLoader as x, rejectExplicitCfnStackWithMultipleStacks as xt, resolveSelectionExpression as y, createLocalStateProvider as yt, translateLambdaResponse as z };
19533
+ //# sourceMappingURL=local-list-DdmjsVLF.js.map