@go-to-k/cdkd 0.80.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -584,12 +584,16 @@ containers `cdkd local invoke` uses. Modeled on `sam local start-api`
584
584
  but reusing cdkd's synthesis / route-discovery plumbing.
585
585
 
586
586
  ```bash
587
- # Auto-allocate a port (printed at startup) and serve every discovered route
587
+ # Auto-allocate one port PER discovered API (printed at startup)
588
588
  cdkd local start-api
589
589
 
590
- # Pin to port 3000 (SAM-parity / curl muscle memory)
590
+ # Pin the FIRST server to port 3000; subsequent APIs get 3001, 3002, ...
591
591
  cdkd local start-api --port 3000
592
592
 
593
+ # Restrict to a single API by its CDK logical id (HTTP API / REST API logical
594
+ # id, or the backing Lambda's logical id for Function URLs)
595
+ cdkd local start-api --api MyAdminApi
596
+
593
597
  # Pre-warm one container per Lambda at server boot — eliminates first-request cold start
594
598
  cdkd local start-api --warm
595
599
 
@@ -606,6 +610,13 @@ cdkd local start-api --watch
606
610
  cdkd local start-api --stage prod
607
611
  ```
608
612
 
613
+ **One server per API** (since v0.81): every discovered API surface gets its
614
+ own HTTP server on its own port, so authorizers, CORS configs, and stage
615
+ variables stay scoped to the owning API and never bleed across APIs that
616
+ happen to share a path. `cdkd local start-api` prints one
617
+ `Server listening on http://<host>:<port> (<API> (<kind>))` line per
618
+ server at startup; pass `--api <id>` to launch only one of them.
619
+
609
620
  Scope: REST v1 + HTTP API + Function URL with AWS_PROXY integrations.
610
621
  Authorizers (Lambda TOKEN/REQUEST + Cognito User Pool + HTTP v2 JWT),
611
622
  VPC-config Lambda warnings, CORS preflight, hot reload, and stage
package/dist/cli.js CHANGED
@@ -44850,8 +44850,8 @@ var ApiGatewayV2Provider = class {
44850
44850
  async createRoute(logicalId, resourceType, properties) {
44851
44851
  this.logger.debug(`Creating API Gateway V2 Route ${logicalId}`);
44852
44852
  const apiId = properties["ApiId"];
44853
- const routeKey2 = properties["RouteKey"];
44854
- if (!apiId || !routeKey2) {
44853
+ const routeKey = properties["RouteKey"];
44854
+ if (!apiId || !routeKey) {
44855
44855
  throw new ProvisioningError(
44856
44856
  `ApiId and RouteKey are required for API Gateway V2 Route ${logicalId}`,
44857
44857
  resourceType,
@@ -44862,7 +44862,7 @@ var ApiGatewayV2Provider = class {
44862
44862
  const response = await this.getClient().send(
44863
44863
  new CreateRouteCommand2({
44864
44864
  ApiId: apiId,
44865
- RouteKey: routeKey2,
44865
+ RouteKey: routeKey,
44866
44866
  Target: properties["Target"],
44867
44867
  AuthorizationType: properties["AuthorizationType"],
44868
44868
  AuthorizerId: properties["AuthorizerId"]
@@ -70651,8 +70651,38 @@ async function pullImage(image, skipPull) {
70651
70651
  logger.debug(`Skipping docker pull for ${image} (--no-pull)`);
70652
70652
  return;
70653
70653
  }
70654
- logger.info(`Pulling ${image}...`);
70655
- await runForeground("docker", ["pull", image]);
70654
+ if (getLogger().getLevel() === "debug") {
70655
+ logger.info(`Pulling ${image}...`);
70656
+ await runForeground("docker", ["pull", image]);
70657
+ return;
70658
+ }
70659
+ logger.debug(`Pulling ${image} (silent \u2014 pass --verbose to stream progress)`);
70660
+ await runCaptured("docker", ["pull", image], image);
70661
+ }
70662
+ function runCaptured(cmd, args, image) {
70663
+ return new Promise((resolveProc, rejectProc) => {
70664
+ const proc = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
70665
+ let stdout = "";
70666
+ let stderr = "";
70667
+ proc.stdout?.on("data", (chunk) => {
70668
+ stdout += chunk.toString("utf-8");
70669
+ });
70670
+ proc.stderr?.on("data", (chunk) => {
70671
+ stderr += chunk.toString("utf-8");
70672
+ });
70673
+ proc.on(
70674
+ "error",
70675
+ (err) => rejectProc(new DockerRunnerError(`${cmd} pull ${image} failed: ${err.message}`))
70676
+ );
70677
+ proc.on("close", (code) => {
70678
+ if (code === 0) {
70679
+ resolveProc();
70680
+ return;
70681
+ }
70682
+ const detail = stderr.trim() || stdout.trim() || "(no output)";
70683
+ rejectProc(new DockerRunnerError(`docker pull ${image} exited with code ${code}: ${detail}`));
70684
+ });
70685
+ });
70656
70686
  }
70657
70687
  async function runDetached(opts) {
70658
70688
  const args = ["run", "-d", "--rm"];
@@ -71367,8 +71397,8 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
71367
71397
  );
71368
71398
  }
71369
71399
  }
71370
- const routeKey2 = props["RouteKey"];
71371
- if (typeof routeKey2 !== "string" || routeKey2.length === 0) {
71400
+ const routeKey = props["RouteKey"];
71401
+ if (typeof routeKey !== "string" || routeKey.length === 0) {
71372
71402
  throw new Error(
71373
71403
  `${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): RouteKey must be a string`
71374
71404
  );
@@ -71404,7 +71434,7 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
71404
71434
  integrationProps["IntegrationUri"],
71405
71435
  `${stackName}/${integrationLogicalId}.IntegrationUri`
71406
71436
  );
71407
- const { method, pathPattern } = parseRouteKey(routeKey2);
71437
+ const { method, pathPattern } = parseRouteKey(routeKey);
71408
71438
  return [
71409
71439
  {
71410
71440
  method,
@@ -71519,14 +71549,14 @@ function parseHttpApiTargetIntegration(target, location) {
71519
71549
  )}).`
71520
71550
  );
71521
71551
  }
71522
- function parseRouteKey(routeKey2) {
71523
- if (routeKey2 === "$default") {
71552
+ function parseRouteKey(routeKey) {
71553
+ if (routeKey === "$default") {
71524
71554
  return { method: "ANY", pathPattern: "$default" };
71525
71555
  }
71526
- const m = /^([A-Za-z]+)\s+(\S+)$/.exec(routeKey2);
71556
+ const m = /^([A-Za-z]+)\s+(\S+)$/.exec(routeKey);
71527
71557
  if (!m) {
71528
71558
  throw new Error(
71529
- `RouteKey '${routeKey2}' is malformed: expected '<METHOD> <path>' (e.g. 'GET /items/{id}') or '$default'.`
71559
+ `RouteKey '${routeKey}' is malformed: expected '<METHOD> <path>' (e.g. 'GET /items/{id}') or '$default'.`
71530
71560
  );
71531
71561
  }
71532
71562
  return { method: m[1].toUpperCase(), pathPattern: m[2] };
@@ -71867,10 +71897,10 @@ function buildHttpApiV2Event(req, ctx, opts = {}) {
71867
71897
  const contentType = headers["content-type"] ?? "";
71868
71898
  const { body, isBase64Encoded } = encodeBody(req.body, contentType);
71869
71899
  const now = opts.now ? opts.now() : /* @__PURE__ */ new Date();
71870
- const routeKey2 = ctx.route.pathPattern === "$default" ? "$default" : `${ctx.route.method} ${ctx.route.pathPattern}`;
71900
+ const routeKey = ctx.route.pathPattern === "$default" ? "$default" : `${ctx.route.method} ${ctx.route.pathPattern}`;
71871
71901
  const event = {
71872
71902
  version: "2.0",
71873
- routeKey: routeKey2,
71903
+ routeKey,
71874
71904
  rawPath,
71875
71905
  rawQueryString,
71876
71906
  cookies,
@@ -71894,7 +71924,7 @@ function buildHttpApiV2Event(req, ctx, opts = {}) {
71894
71924
  userAgent
71895
71925
  },
71896
71926
  requestId: randomUUID(),
71897
- routeKey: routeKey2,
71927
+ routeKey,
71898
71928
  stage: ctx.route.stage,
71899
71929
  time: formatRequestTime(now),
71900
71930
  timeEpoch: now.getTime(),
@@ -73798,6 +73828,74 @@ function writeError(res, statusCode, body = '{"message":"Internal server error"}
73798
73828
  res.end(body);
73799
73829
  }
73800
73830
 
73831
+ // src/local/api-server-grouping.ts
73832
+ function groupRoutesByServer(routes) {
73833
+ const order = [];
73834
+ const byKey = /* @__PURE__ */ new Map();
73835
+ for (const rwa of routes) {
73836
+ const r = rwa.route;
73837
+ let serverKey;
73838
+ let kind;
73839
+ let identifier;
73840
+ let displayName;
73841
+ if (r.source === "function-url") {
73842
+ identifier = r.lambdaLogicalId;
73843
+ serverKey = `function-url:${identifier}`;
73844
+ kind = "function-url";
73845
+ displayName = `${identifier} (Function URL)`;
73846
+ } else if (r.source === "http-api") {
73847
+ identifier = r.apiLogicalId ?? "<unknown>";
73848
+ serverKey = `http-api:${identifier}`;
73849
+ kind = "http-api";
73850
+ displayName = `${identifier} (HTTP API v2)`;
73851
+ } else {
73852
+ identifier = r.apiLogicalId ?? "<unknown>";
73853
+ serverKey = `rest-v1:${identifier}`;
73854
+ kind = "rest-v1";
73855
+ displayName = `${identifier} (REST API v1)`;
73856
+ }
73857
+ const existing = byKey.get(serverKey);
73858
+ if (existing) {
73859
+ existing.routes.push(rwa);
73860
+ } else {
73861
+ byKey.set(serverKey, { displayName, kind, identifier, routes: [rwa] });
73862
+ order.push(serverKey);
73863
+ }
73864
+ }
73865
+ return order.map((key) => {
73866
+ const entry = byKey.get(key);
73867
+ return {
73868
+ serverKey: key,
73869
+ displayName: entry.displayName,
73870
+ kind: entry.kind,
73871
+ identifier: entry.identifier,
73872
+ routes: entry.routes
73873
+ };
73874
+ });
73875
+ }
73876
+ function filterRoutesByApiIdentifier(routes, identifier) {
73877
+ return routes.filter((rwa) => {
73878
+ const r = rwa.route;
73879
+ if (r.source === "function-url") {
73880
+ return r.lambdaLogicalId === identifier;
73881
+ }
73882
+ return r.apiLogicalId === identifier;
73883
+ });
73884
+ }
73885
+ function availableApiIdentifiers(routes) {
73886
+ const seen = /* @__PURE__ */ new Set();
73887
+ const out = [];
73888
+ for (const rwa of routes) {
73889
+ const r = rwa.route;
73890
+ const id = r.source === "function-url" ? r.lambdaLogicalId : r.apiLogicalId ?? "<unknown>";
73891
+ if (!seen.has(id)) {
73892
+ seen.add(id);
73893
+ out.push(id);
73894
+ }
73895
+ }
73896
+ return out;
73897
+ }
73898
+
73801
73899
  // src/local/stage-resolver.ts
73802
73900
  function buildStageMap(template, stageOverride) {
73803
73901
  const out = /* @__PURE__ */ new Map();
@@ -75689,92 +75787,6 @@ function createFileWatcher(options) {
75689
75787
  };
75690
75788
  }
75691
75789
 
75692
- // src/local/reload-orchestrator.ts
75693
- function createReloadOrchestrator(deps) {
75694
- const logger = getLogger().child("start-api-reload");
75695
- let chain = Promise.resolve();
75696
- return {
75697
- reload() {
75698
- const next = chain.then(() => runOneReload(deps, logger));
75699
- chain = next.catch(() => void 0);
75700
- return next;
75701
- }
75702
- };
75703
- }
75704
- async function runOneReload(deps, logger) {
75705
- const previousState = deps.getServerState();
75706
- const start = Date.now();
75707
- let material;
75708
- try {
75709
- material = await deps.synthesizeAndBuild();
75710
- } catch (err) {
75711
- const reason = err instanceof Error ? err.message : String(err);
75712
- logger.warn(`cdk synth failed during reload; keeping previous version. (${reason})`);
75713
- return { ok: false, reason, added: [], removed: [], rebuiltLambdas: [] };
75714
- }
75715
- const oldRoutes = previousState.routes;
75716
- const newRoutes = material.routes;
75717
- const oldKeys = new Set(oldRoutes.map((r) => routeKey(r.route)));
75718
- const newKeys = new Set(newRoutes.map((r) => routeKey(r.route)));
75719
- const added = newRoutes.filter((r) => !oldKeys.has(routeKey(r.route)));
75720
- const removed = oldRoutes.filter((r) => !newKeys.has(routeKey(r.route)));
75721
- const previousSpecs = pickSpecsFromState(previousState);
75722
- const rebuiltLambdas = [];
75723
- for (const [logicalId, newSpec] of material.specs) {
75724
- const oldSpec = previousSpecs.get(logicalId);
75725
- if (!oldSpec)
75726
- continue;
75727
- if (specSignature(oldSpec) !== specSignature(newSpec)) {
75728
- rebuiltLambdas.push(logicalId);
75729
- }
75730
- }
75731
- const newPool = deps.buildPool(material.specs);
75732
- Object.defineProperty(newPool, "__cdkdSpecs", {
75733
- value: material.specs,
75734
- enumerable: false,
75735
- configurable: true
75736
- });
75737
- const newState = {
75738
- routes: material.routes,
75739
- pool: newPool,
75740
- corsConfigByApiId: material.corsConfigByApiId
75741
- };
75742
- deps.setServerState(newState);
75743
- void previousState.pool.dispose().catch(
75744
- (err) => logger.debug(
75745
- `Previous pool dispose() failed: ${err instanceof Error ? err.message : String(err)}`
75746
- )
75747
- );
75748
- const elapsed = Date.now() - start;
75749
- logger.info(
75750
- `Reloaded in ${elapsed}ms: +${added.length} route(s), -${removed.length} route(s), ${rebuiltLambdas.length} Lambda(s) rebuilt.`
75751
- );
75752
- return {
75753
- ok: true,
75754
- added,
75755
- removed,
75756
- rebuiltLambdas,
75757
- newState
75758
- };
75759
- }
75760
- function routeKey(route) {
75761
- return [route.method, route.pathPattern, route.lambdaLogicalId, route.source, route.apiVersion].map((s) => String(s)).join("|");
75762
- }
75763
- function specSignature(spec) {
75764
- return JSON.stringify({
75765
- codeDir: spec.codeDir,
75766
- env: spec.env,
75767
- handler: spec.lambda.handler,
75768
- runtime: spec.lambda.runtime,
75769
- containerHost: spec.containerHost,
75770
- debugPort: spec.debugPort ?? null
75771
- });
75772
- }
75773
- function pickSpecsFromState(state) {
75774
- const tagged = state.pool.__cdkdSpecs;
75775
- return tagged ?? /* @__PURE__ */ new Map();
75776
- }
75777
-
75778
75790
  // src/local/authorizer-cache.ts
75779
75791
  function createAuthorizerCache(opts = {}) {
75780
75792
  const now = opts.now ?? (() => Date.now());
@@ -75880,7 +75892,17 @@ async function localStartApiCommand(options) {
75880
75892
  }
75881
75893
  }
75882
75894
  attachStageContext(routes, stageMap);
75883
- const routesWithAuth = attachAuthorizers(targetStacks, routes);
75895
+ let routesWithAuth = attachAuthorizers(targetStacks, routes);
75896
+ if (options.api) {
75897
+ const filtered = filterRoutesByApiIdentifier(routesWithAuth, options.api);
75898
+ if (filtered.length === 0) {
75899
+ const available = availableApiIdentifiers(routesWithAuth).join(", ") || "(none)";
75900
+ throw new Error(
75901
+ `--api '${options.api}' did not match any discovered API. Available identifiers: ${available}.`
75902
+ );
75903
+ }
75904
+ routesWithAuth = filtered;
75905
+ }
75884
75906
  const corsConfigByApiId = /* @__PURE__ */ new Map();
75885
75907
  for (const stack of targetStacks) {
75886
75908
  const m = buildCorsConfigByApiId(stack.template);
@@ -75933,88 +75955,91 @@ async function localStartApiCommand(options) {
75933
75955
  return [...assetPaths];
75934
75956
  };
75935
75957
  const initialMaterial = await synthesizeAndBuild();
75936
- const initialPool = buildPool(initialMaterial.specs);
75937
75958
  lastAssetPaths.value = computeAssetPaths(initialMaterial.specs);
75938
75959
  await prewarmJwks(initialMaterial.routes, jwksCache);
75939
75960
  warnVpcConfigLambdas(initialMaterial.routes, initialMaterial.stacks ?? []);
75940
- if (options.warm) {
75941
- logger.info(`Pre-warming ${initialMaterial.specs.size} container(s)...`);
75942
- const handles = await Promise.allSettled(
75943
- [...initialMaterial.specs.keys()].map((id) => initialPool.acquire(id))
75944
- );
75945
- for (const result of handles) {
75946
- if (result.status === "fulfilled") {
75947
- initialPool.release(result.value);
75948
- } else {
75949
- logger.warn(
75950
- `Pre-warm failed for one Lambda (cold start cost will apply on first request): ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
75951
- );
75952
- }
75953
- }
75954
- }
75955
75961
  let maxTimeoutSec = 0;
75956
75962
  for (const spec of initialMaterial.specs.values()) {
75957
75963
  if (spec.lambda.timeoutSec > maxTimeoutSec)
75958
75964
  maxTimeoutSec = spec.lambda.timeoutSec;
75959
75965
  }
75960
75966
  const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
75961
- const port = parseInt(options.port, 10);
75962
- if (!Number.isFinite(port) || port < 0 || port > 65535) {
75967
+ const basePort = parseInt(options.port, 10);
75968
+ if (!Number.isFinite(basePort) || basePort < 0 || basePort > 65535) {
75963
75969
  throw new Error(`--port must be 0..65535 (got ${options.port}).`);
75964
75970
  }
75965
- const initialState = {
75966
- routes: initialMaterial.routes,
75967
- pool: initialPool,
75968
- corsConfigByApiId: initialMaterial.corsConfigByApiId
75969
- };
75970
- const server = await startApiServer({
75971
- state: initialState,
75972
- rieTimeoutMs,
75973
- host: options.host,
75974
- port,
75975
- authorizerCache,
75976
- jwksCache,
75977
- jwksWarnedUrls
75978
- });
75979
- printRouteTable(initialMaterial.routes);
75971
+ const initialGroups = groupRoutesByServer(initialMaterial.routes);
75972
+ const servers = [];
75973
+ let nextPort = basePort;
75974
+ for (const group of initialGroups) {
75975
+ const groupSpecs = filterSpecsForGroup(group, initialMaterial.specs);
75976
+ const groupPool = buildPool(groupSpecs);
75977
+ const groupState = {
75978
+ routes: group.routes,
75979
+ pool: groupPool,
75980
+ corsConfigByApiId: initialMaterial.corsConfigByApiId
75981
+ };
75982
+ if (options.warm) {
75983
+ logger.info(`Pre-warming ${groupSpecs.size} container(s) for ${group.displayName}...`);
75984
+ const handles = await Promise.allSettled(
75985
+ [...groupSpecs.keys()].map((id) => groupPool.acquire(id))
75986
+ );
75987
+ for (const result of handles) {
75988
+ if (result.status === "fulfilled") {
75989
+ groupPool.release(result.value);
75990
+ } else {
75991
+ logger.warn(
75992
+ `Pre-warm failed for one Lambda in ${group.displayName} (cold start cost will apply on first request): ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
75993
+ );
75994
+ }
75995
+ }
75996
+ }
75997
+ const started = await startApiServer({
75998
+ state: groupState,
75999
+ rieTimeoutMs,
76000
+ host: options.host,
76001
+ // Increment per server; basePort=0 leaves every server on auto-alloc.
76002
+ port: basePort === 0 ? 0 : nextPort,
76003
+ authorizerCache,
76004
+ jwksCache,
76005
+ jwksWarnedUrls
76006
+ });
76007
+ servers.push({ group, server: started });
76008
+ if (basePort !== 0)
76009
+ nextPort += 1;
76010
+ }
76011
+ printPerServerRouteTables(servers);
75980
76012
  logger.info(
75981
76013
  `Per-Lambda concurrency: ${perLambdaConcurrency} (override with --per-lambda-concurrency)`
75982
76014
  );
75983
- process.stdout.write(`Server listening on http://${server.host}:${server.port}
75984
- `);
76015
+ for (const { group, server } of servers) {
76016
+ process.stdout.write(
76017
+ `Server listening on http://${server.host}:${server.port} (${group.displayName})
76018
+ `
76019
+ );
76020
+ }
75985
76021
  process.stdout.write("^C to stop and clean up containers.\n");
75986
76022
  let watcher;
75987
- let orchestrator;
76023
+ let reloadChain = Promise.resolve();
75988
76024
  if (options.watch) {
75989
- orchestrator = createReloadOrchestrator({
75990
- synthesizeAndBuild,
75991
- buildPool,
75992
- setServerState: server.setServerState,
75993
- getServerState: server.getServerState
75994
- });
75995
76025
  const initialWatchPaths = [options.output, ...lastAssetPaths.value];
75996
76026
  watcher = createFileWatcher({
75997
76027
  paths: initialWatchPaths,
75998
76028
  onChange: () => {
75999
- if (!orchestrator)
76000
- return;
76001
76029
  logger.info("Detected file change; reloading...");
76002
- void orchestrator.reload().then((result) => {
76003
- if (result.ok && watcher && result.newState) {
76004
- const taggedSpecs = result.newState.pool.__cdkdSpecs;
76005
- if (taggedSpecs) {
76006
- lastAssetPaths.value = computeAssetPaths(taggedSpecs);
76007
- }
76008
- watcher.update([options.output, ...lastAssetPaths.value]);
76009
- if (result.added.length > 0 || result.removed.length > 0) {
76010
- printRouteTable(result.newState.routes);
76011
- }
76012
- }
76013
- }).catch((err) => {
76014
- logger.warn(
76015
- `Reload failed: ${err instanceof Error ? err.message : String(err)}. Keeping previous version.`
76016
- );
76017
- });
76030
+ const next = reloadChain.then(
76031
+ () => reloadAllServers({
76032
+ synthesizeAndBuild,
76033
+ servers,
76034
+ buildPool,
76035
+ computeAssetPaths,
76036
+ lastAssetPaths,
76037
+ watcher,
76038
+ output: options.output,
76039
+ logger
76040
+ })
76041
+ );
76042
+ reloadChain = next.catch(() => void 0);
76018
76043
  }
76019
76044
  });
76020
76045
  logger.info(`Watching ${options.output} (and ${lastAssetPaths.value.length} asset dir(s))`);
@@ -76041,16 +76066,28 @@ async function localStartApiCommand(options) {
76041
76066
  logger.warn(`watcher.close() failed: ${err instanceof Error ? err.message : String(err)}`);
76042
76067
  }
76043
76068
  }
76044
- try {
76045
- await server.close();
76046
- } catch (err) {
76047
- logger.warn(`server.close() failed: ${err instanceof Error ? err.message : String(err)}`);
76048
- }
76049
- try {
76050
- await server.getServerState().pool.dispose();
76051
- } catch (err) {
76052
- logger.warn(`pool.dispose() failed: ${err instanceof Error ? err.message : String(err)}`);
76053
- }
76069
+ await Promise.allSettled(
76070
+ servers.map(async ({ server, group }) => {
76071
+ try {
76072
+ await server.close();
76073
+ } catch (err) {
76074
+ logger.warn(
76075
+ `server.close() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`
76076
+ );
76077
+ }
76078
+ })
76079
+ );
76080
+ await Promise.allSettled(
76081
+ servers.map(async ({ server, group }) => {
76082
+ try {
76083
+ await server.getServerState().pool.dispose();
76084
+ } catch (err) {
76085
+ logger.warn(
76086
+ `pool.dispose() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`
76087
+ );
76088
+ }
76089
+ })
76090
+ );
76054
76091
  for (const dir of inlineTmpDirs) {
76055
76092
  try {
76056
76093
  rmSync2(dir, { recursive: true, force: true });
@@ -76422,6 +76459,91 @@ function parsePerLambdaConcurrency(raw) {
76422
76459
  }
76423
76460
  return parsed;
76424
76461
  }
76462
+ function filterSpecsForGroup(group, allSpecs) {
76463
+ const ids = /* @__PURE__ */ new Set();
76464
+ for (const rwa of group.routes) {
76465
+ ids.add(rwa.route.lambdaLogicalId);
76466
+ const auth = rwa.authorizer;
76467
+ if (auth && (auth.kind === "lambda-token" || auth.kind === "lambda-request")) {
76468
+ ids.add(auth.lambdaLogicalId);
76469
+ }
76470
+ }
76471
+ const out = /* @__PURE__ */ new Map();
76472
+ for (const id of ids) {
76473
+ const spec = allSpecs.get(id);
76474
+ if (spec)
76475
+ out.set(id, spec);
76476
+ }
76477
+ return out;
76478
+ }
76479
+ function printPerServerRouteTables(servers) {
76480
+ for (const { group, server } of servers) {
76481
+ process.stdout.write(`
76482
+ ${group.displayName} (http://${server.host}:${server.port})
76483
+ `);
76484
+ printRouteTable(group.routes);
76485
+ }
76486
+ }
76487
+ async function reloadAllServers(args) {
76488
+ const {
76489
+ synthesizeAndBuild,
76490
+ servers,
76491
+ buildPool,
76492
+ computeAssetPaths,
76493
+ lastAssetPaths,
76494
+ watcher,
76495
+ output,
76496
+ logger
76497
+ } = args;
76498
+ let material;
76499
+ try {
76500
+ material = await synthesizeAndBuild();
76501
+ } catch (err) {
76502
+ logger.warn(
76503
+ `cdk synth failed during reload; keeping previous version. (${err instanceof Error ? err.message : String(err)})`
76504
+ );
76505
+ return;
76506
+ }
76507
+ const newGroups = groupRoutesByServer(material.routes);
76508
+ const newByKey = new Map(newGroups.map((g) => [g.serverKey, g]));
76509
+ const oldKeys = new Set(servers.map((s) => s.group.serverKey));
76510
+ const newKeys = new Set(newByKey.keys());
76511
+ const added = [...newKeys].filter((k) => !oldKeys.has(k));
76512
+ const removed = [...oldKeys].filter((k) => !newKeys.has(k));
76513
+ if (added.length > 0) {
76514
+ logger.warn(
76515
+ `Reload detected new API surface(s): ${added.join(", ")}. Restart 'cdkd local start-api' to serve them.`
76516
+ );
76517
+ }
76518
+ if (removed.length > 0) {
76519
+ logger.warn(
76520
+ `Reload detected removed API surface(s): ${removed.join(", ")}. Their servers will keep serving stale routes until restart.`
76521
+ );
76522
+ }
76523
+ for (const booted of servers) {
76524
+ const group = newByKey.get(booted.group.serverKey);
76525
+ if (!group)
76526
+ continue;
76527
+ const groupSpecs = filterSpecsForGroup(group, material.specs);
76528
+ const newPool = buildPool(groupSpecs);
76529
+ const newState = {
76530
+ routes: group.routes,
76531
+ pool: newPool,
76532
+ corsConfigByApiId: material.corsConfigByApiId
76533
+ };
76534
+ const previousState = booted.server.setServerState(newState);
76535
+ void previousState.pool.dispose().catch((err) => {
76536
+ logger.debug(
76537
+ `Previous pool dispose() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`
76538
+ );
76539
+ });
76540
+ }
76541
+ lastAssetPaths.value = computeAssetPaths(material.specs);
76542
+ if (watcher) {
76543
+ watcher.update([output, ...lastAssetPaths.value]);
76544
+ }
76545
+ printPerServerRouteTables(servers);
76546
+ }
76425
76547
  function parseDebugPort(raw) {
76426
76548
  const parsed = parseInt(raw, 10);
76427
76549
  if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
@@ -76444,8 +76566,8 @@ function createLocalStartApiCommand() {
76444
76566
  ).addOption(new Option7("--no-pull", "Skip docker pull (cached image)")).addOption(
76445
76567
  new Option7(
76446
76568
  "--container-host <host>",
76447
- "Hostname/IP the container reaches the host on"
76448
- ).default("host.docker.internal")
76569
+ "IP the host uses to bind/probe the RIE port (must be a numeric IP \u2014 `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1."
76570
+ ).default("127.0.0.1")
76449
76571
  ).addOption(
76450
76572
  new Option7(
76451
76573
  "--debug-port-base <port>",
@@ -76471,6 +76593,11 @@ function createLocalStartApiCommand() {
76471
76593
  "--stage <name>",
76472
76594
  "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 \u2014 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."
76473
76595
  )
76596
+ ).addOption(
76597
+ new Option7(
76598
+ "--api <id>",
76599
+ "Restrict to a single API surface by its logical id (HTTP API / REST API logical id, or the backing Lambda's logical id for Function URLs). When unset, every discovered API gets its own server on its own port (basePort, basePort+1, ... when --port is set; auto-allocated otherwise)."
76600
+ )
76474
76601
  ).action(withErrorHandling(localStartApiCommand));
76475
76602
  [...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => startApi.addOption(opt));
76476
76603
  startApi.addOption(deprecatedRegionOption);
@@ -77056,7 +77183,7 @@ function reorderArgs(argv) {
77056
77183
  }
77057
77184
  async function main() {
77058
77185
  const program = new Command16();
77059
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.80.0");
77186
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.81.0");
77060
77187
  program.addCommand(createBootstrapCommand());
77061
77188
  program.addCommand(createSynthCommand());
77062
77189
  program.addCommand(createListCommand());