cdk-local 0.68.0 → 0.70.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,8 +1,8 @@
1
1
  import { a as runDockerStreaming, c as getEmbedConfig, i as runDockerForeground, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, s as getLogger, u as setEmbedConfig } from "./docker-cmd-voNPrcRh.js";
2
2
  import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import * as path from "node:path";
5
- import { dirname, isAbsolute, join, normalize, resolve, sep } from "node:path";
4
+ import * as path$1 from "node:path";
5
+ import path, { dirname, isAbsolute, join, normalize, resolve, sep } from "node:path";
6
6
  import { Command, Option } from "commander";
7
7
  import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
8
8
  import { MultiSelectPrompt } from "@clack/core";
@@ -7834,8 +7834,8 @@ function getContainerAwsCredentialsPath() {
7834
7834
  async function writeProfileCredentialsFile(profileName, creds) {
7835
7835
  if (profileName === "") throw new Error("writeProfileCredentialsFile: profile name must not be empty.");
7836
7836
  if (/[\r\n[\]]/.test(profileName)) throw new Error(`writeProfileCredentialsFile: profile name '${profileName}' contains a forbidden character (any of CR, LF, '[', ']' would corrupt the INI file or the docker -e env var).`);
7837
- const dir = await mkdtemp(path.join(tmpdir(), `${getEmbedConfig().productName}-profile-creds-`));
7838
- const hostPath = path.join(dir, "credentials");
7837
+ const dir = await mkdtemp(path$1.join(tmpdir(), `${getEmbedConfig().productName}-profile-creds-`));
7838
+ const hostPath = path$1.join(dir, "credentials");
7839
7839
  const lines = [
7840
7840
  `[${profileName}]`,
7841
7841
  `aws_access_key_id = ${creds.accessKeyId}`,
@@ -8177,7 +8177,7 @@ function materializeLambdaLayers$1(layers) {
8177
8177
  containerPath: "/opt",
8178
8178
  readOnly: true
8179
8179
  } };
8180
- const tmpDir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-layers-`));
8180
+ const tmpDir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-layers-`));
8181
8181
  for (const layer of layers) cpSync(layer.assetPath, tmpDir, {
8182
8182
  recursive: true,
8183
8183
  force: true
@@ -8385,9 +8385,9 @@ function materializeInlineCode$2(handler, source, fileExtension) {
8385
8385
  const lastDot = handler.lastIndexOf(".");
8386
8386
  if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
8387
8387
  const modulePath = handler.substring(0, lastDot);
8388
- const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-`));
8389
- const filePath = path.join(dir, `${modulePath}${fileExtension}`);
8390
- mkdirSync(path.dirname(filePath), { recursive: true });
8388
+ const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-`));
8389
+ const filePath = path$1.join(dir, `${modulePath}${fileExtension}`);
8390
+ mkdirSync(path$1.dirname(filePath), { recursive: true });
8391
8391
  writeFileSync(filePath, source, "utf-8");
8392
8392
  return dir;
8393
8393
  }
@@ -16510,15 +16510,19 @@ function createFileWatcher(options) {
16510
16510
  ...options.ignored && { ignored: options.ignored }
16511
16511
  });
16512
16512
  let timer = null;
16513
+ let pending = /* @__PURE__ */ new Set();
16513
16514
  let closed = false;
16514
- const fire = () => {
16515
+ const fire = (path) => {
16515
16516
  if (closed) return;
16517
+ pending.add(path);
16516
16518
  if (timer) clearTimeout(timer);
16517
16519
  timer = setTimeout(() => {
16518
16520
  timer = null;
16519
16521
  if (closed) return;
16522
+ const changed = Array.from(pending);
16523
+ pending = /* @__PURE__ */ new Set();
16520
16524
  try {
16521
- options.onChange();
16525
+ options.onChange(changed);
16522
16526
  } catch (err) {
16523
16527
  logger.warn(`onChange callback threw: ${err instanceof Error ? err.message : String(err)}`);
16524
16528
  }
@@ -16527,7 +16531,7 @@ function createFileWatcher(options) {
16527
16531
  };
16528
16532
  const onEvent = (path) => {
16529
16533
  if (options.shouldTrigger && !options.shouldTrigger(path)) return;
16530
- fire();
16534
+ fire(path);
16531
16535
  };
16532
16536
  watcher.on("add", onEvent);
16533
16537
  watcher.on("change", onEvent);
@@ -17040,8 +17044,8 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
17040
17044
  */
17041
17045
  function createWatchPredicates(args) {
17042
17046
  const { watchRoot, output, watchConfig } = args;
17043
- const toRel = (abs) => path.relative(watchRoot, abs).split(path.sep).join("/");
17044
- const outputRel = toRel(path.resolve(watchRoot, output));
17047
+ const toRel = (abs) => path$1.relative(watchRoot, abs).split(path$1.sep).join("/");
17048
+ const outputRel = toRel(path$1.resolve(watchRoot, output));
17045
17049
  const excludePatterns = [
17046
17050
  ...outputRel !== "" && !outputRel.startsWith("..") ? [outputRel] : [],
17047
17051
  "node_modules",
@@ -17546,7 +17550,7 @@ async function resolveContainerImageForStartApi(lambda, skipPull, profile) {
17546
17550
  async function resolveLocalBuildPlan(lambda) {
17547
17551
  const manifestPath = lambda.stack.assetManifestPath;
17548
17552
  if (!manifestPath) return void 0;
17549
- const cdkOutDir = path.dirname(manifestPath);
17553
+ const cdkOutDir = path$1.dirname(manifestPath);
17550
17554
  const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
17551
17555
  if (!manifest) return void 0;
17552
17556
  const entry = getDockerImageBySourceHash(manifest, lambda.imageUri);
@@ -17603,7 +17607,7 @@ async function materializeLambdaLayers(layers, layerTmpDirs, layerRoleArn) {
17603
17607
  });
17604
17608
  }
17605
17609
  if (flat.length === 1) return flat[0].assetPath;
17606
- const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-layers-`));
17610
+ const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-layers-`));
17607
17611
  for (const layer of flat) cpSync(layer.assetPath, dir, {
17608
17612
  recursive: true,
17609
17613
  force: true
@@ -17742,8 +17746,8 @@ function resolveImageLambda(args) {
17742
17746
  function resolveAssetCodePath(stack, logicalId, resource) {
17743
17747
  const assetPath = resource.Metadata?.["aws:asset:path"];
17744
17748
  if (typeof assetPath !== "string" || assetPath.length === 0) throw new Error(`Lambda '${logicalId}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} start-api needs this hint to find the local asset directory. Re-synthesize the app and retry.`);
17745
- const cdkOutDir = stack.assetManifestPath ? path.dirname(stack.assetManifestPath) : process.cwd();
17746
- return path.isAbsolute(assetPath) ? assetPath : path.resolve(cdkOutDir, assetPath);
17749
+ const cdkOutDir = stack.assetManifestPath ? path$1.dirname(stack.assetManifestPath) : process.cwd();
17750
+ return path$1.isAbsolute(assetPath) ? assetPath : path$1.resolve(cdkOutDir, assetPath);
17747
17751
  }
17748
17752
  /**
17749
17753
  * Print the discovered route table to stdout. Format mirrors the spec
@@ -17798,10 +17802,10 @@ function materializeInlineCode$1(handler, source, fileExtension, tmpDirsOut) {
17798
17802
  const lastDot = handler.lastIndexOf(".");
17799
17803
  if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
17800
17804
  const modulePath = handler.substring(0, lastDot);
17801
- const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-`));
17805
+ const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-`));
17802
17806
  tmpDirsOut.add(dir);
17803
- const filePath = path.join(dir, `${modulePath}${fileExtension}`);
17804
- mkdirSync(path.dirname(filePath), { recursive: true });
17807
+ const filePath = path$1.join(dir, `${modulePath}${fileExtension}`);
17808
+ mkdirSync(path$1.dirname(filePath), { recursive: true });
17805
17809
  writeFileSync(filePath, source, "utf-8");
17806
17810
  return dir;
17807
17811
  }
@@ -20883,6 +20887,22 @@ var EcsServiceRunnerError = class EcsServiceRunnerError extends Error {
20883
20887
  Object.setPrototypeOf(this, EcsServiceRunnerError.prototype);
20884
20888
  }
20885
20889
  };
20890
+ /**
20891
+ * Phase 4 (#214) — completion-log suffix the soft-reload primitive
20892
+ * emits AFTER `Soft-reloaded replica r<i> (gen <g>): ` to confirm
20893
+ * the docker restart + TCP-ready probe + Cloud Map / front-door
20894
+ * re-publish round trip is done.
20895
+ *
20896
+ * Exported so integ fixtures + unit tests can grep against the
20897
+ * canonical text instead of hand-copying the wording — a future
20898
+ * refactor that rewords this line stays detectable via the symbol
20899
+ * import instead of silently breaking every test's regex.
20900
+ *
20901
+ * Per-repo memory (#218 / test reviewer N4): log-line text is part
20902
+ * of the public contract for `--watch` integ scripts, so it earns a
20903
+ * constant.
20904
+ */
20905
+ const SOFT_RELOAD_COMPLETION_LOG_SUFFIX = "restart + TCP-ready probe complete; Cloud Map + front-door re-published.";
20886
20906
  function createServiceRunState() {
20887
20907
  return {
20888
20908
  replicas: [],
@@ -21137,6 +21157,27 @@ async function bootReplica(service, options, instance) {
21137
21157
  await runEcsTask(service.task, perReplicaTaskOptions, instance.state);
21138
21158
  if (options.discovery) await publishReplicaToCloudMap(service, instance, options.discovery, ownerKeyPrefix);
21139
21159
  if (options.frontDoor) await publishReplicaToFrontDoor(service, instance, options.frontDoor, options.taskOptions.containerHost, ownerKeyPrefix);
21160
+ instance.lastDeployedAssetHash = pickEssentialAssetHash(service);
21161
+ }
21162
+ /**
21163
+ * Phase 4 follow-up (#218) — extract the CDK asset hash from a
21164
+ * resolved service's first essential container (with the same
21165
+ * fallback the watcher uses: first essential, else first container).
21166
+ * Returns `undefined` when the image isn't a CDK asset OR carries
21167
+ * no hash. Pure helper so the boot + rolling + soft-reload paths
21168
+ * share one source of truth for "what's running right now".
21169
+ *
21170
+ * Exported for unit tests; not part of the semver-covered public
21171
+ * surface.
21172
+ *
21173
+ * @internal
21174
+ */
21175
+ function pickEssentialAssetHash(service) {
21176
+ const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
21177
+ if (!essential) return void 0;
21178
+ const image = essential.image;
21179
+ if (image?.kind !== "cdk-asset") return void 0;
21180
+ return image.assetHash;
21140
21181
  }
21141
21182
  /**
21142
21183
  * After the replica's main container is up, discover its docker
@@ -21404,6 +21445,202 @@ async function rollServiceReplica(args) {
21404
21445
  logger.info(`Rolling replica ${shadow.index} (gen ${shadow.generation}): swap complete; old retired.`);
21405
21446
  }
21406
21447
  /**
21448
+ * Phase 4 of issue #214 — bind-mount source fast path. `docker cp` the
21449
+ * post-synth asset source directory into each essential container of
21450
+ * the live replica, then `docker restart` it. Skips `docker build`,
21451
+ * skips a shadow boot, and keeps the container's network IP / Cloud
21452
+ * Map / front-door pool registrations intact (the registrations key
21453
+ * off the docker-assigned IP and the published host port; `docker
21454
+ * restart` preserves both via the container's stable network
21455
+ * namespace), so NO registry swap is needed.
21456
+ *
21457
+ * Sequence (per replica, sequenced by the rolling loop one at a time
21458
+ * so peer services + the ALB front-door always have at least N-1 live
21459
+ * endpoints across the reload — same zero-connection-refusal guarantee
21460
+ * the Phase 2/3 rebuild pathway makes):
21461
+ * 1. Locate the live replica by {@link oldReplicaIndex}; reject when
21462
+ * shutting down (the next save can roll a clean boot instead).
21463
+ * 2. Pre-restart DRAIN: drop the replica's Cloud Map handles + every
21464
+ * front-door pool entry under its owner key. Both registries are
21465
+ * synchronous Map mutations; once these complete, peer wget +
21466
+ * front-door `next()` calls route to the surviving replicas. The
21467
+ * handle / owner-key snapshots are kept on `instance.*` so the
21468
+ * symmetric re-register step can pick them up (Cloud Map handles
21469
+ * are rebuilt fresh via `publishReplicaToCloudMap`; the docker
21470
+ * network IP is preserved across `docker restart` so the new
21471
+ * handles point at the SAME endpoint).
21472
+ * 3. Set {@link ServiceReplicaInstance.softReloadInProgress} = true
21473
+ * so the watcher's `waitForExitImpl` post-exit branch defers to
21474
+ * the in-flight restart instead of re-bootstrapping the replica
21475
+ * from scratch.
21476
+ * 4. For each essential container in the replica's started set:
21477
+ * a. Resolve the container's image WORKDIR via `docker inspect`
21478
+ * (default `/` when unset — matches Docker's runtime default
21479
+ * for a Dockerfile with no `WORKDIR`).
21480
+ * b. `docker cp <sourceDirToCopy>/. <containerId>:<workdir>/`
21481
+ * — copy the synthesized asset directory's contents into the
21482
+ * container at the WORKDIR. Trailing `/.` is critical: it
21483
+ * copies the SOURCE DIRECTORY'S CONTENTS, not the directory
21484
+ * itself, mirroring `cp -r src/. dst/`.
21485
+ * c. `docker restart <containerId>` — cycle PID 1. Image,
21486
+ * network namespace, and host-port publish are preserved.
21487
+ * 5. {@link waitForReplicaTcpReady} confirms the essential
21488
+ * container's first port accepts a TCP connection.
21489
+ * 6. Post-TCP-ready RE-REGISTER: re-publish Cloud Map handles +
21490
+ * front-door pool entry under the SAME per-replica owner key
21491
+ * prefix used at initial boot, so the registrations remain
21492
+ * idempotent across multiple `--watch` reloads. After this
21493
+ * step, peers + the front-door route to the replica again.
21494
+ * 7. Clear `softReloadInProgress` in a `finally` so the watcher
21495
+ * always exits its defer-loop, even on a docker error.
21496
+ *
21497
+ * Failure modes:
21498
+ * - `docker inspect` / `docker cp` / `docker restart` errors:
21499
+ * surfaced to the caller via a throw. The replica may be in an
21500
+ * inconsistent state (drained from registries + partial cp + a
21501
+ * possibly-crashed PID 1). The caller (`reloadAllServices`) logs
21502
+ * the failure and continues with the remaining replicas; the
21503
+ * drained state is intentionally NOT re-registered on error so
21504
+ * peers + the front-door stop routing to a broken replica until
21505
+ * the next clean save (or `^C` and re-run).
21506
+ * - TCP probe timeout: best-effort warn (mirrors
21507
+ * {@link rollServiceReplica}); the registrations are re-published
21508
+ * anyway because the container IS up — just slow to bind. The
21509
+ * dying-old-handles-AND-fresh-app-not-yet-listening worst case
21510
+ * would otherwise leave the replica drained forever.
21511
+ *
21512
+ * Out of scope for the v1 primitive (deferred follow-ups):
21513
+ * - Per-container WORKDIR caching across multiple essential
21514
+ * containers in the same task. The `docker inspect` call is
21515
+ * ~10ms; not worth the cache invalidation surface for a path
21516
+ * fired ~once per save.
21517
+ * - SIGHUP / `docker exec`-driven in-process reload. Image-specific
21518
+ * (uvicorn / nodemon / etc.). `docker restart` is the universal
21519
+ * primitive; signal-based reload is a future opt-in if the
21520
+ * per-restart latency proves prohibitive.
21521
+ *
21522
+ * @internal — wired only by the emulator's reload pathway.
21523
+ */
21524
+ async function softReloadReplica(args) {
21525
+ const { controller, oldReplicaIndex, newService, sourceDirToCopy } = args;
21526
+ const logger = getLogger().child("ecs-service");
21527
+ const instance = controller.runState.replicas[oldReplicaIndex];
21528
+ if (!instance) throw new EcsServiceRunnerError(`softReloadReplica: no replica at index ${oldReplicaIndex} (replicas=${controller.runState.replicas.length}).`);
21529
+ if (instance.shuttingDown) {
21530
+ logger.warn(`Soft-reload of replica r${instance.index} (gen ${instance.generation}): retired by its own watcher mid-reload. Skipping; save again to re-boot it from scratch.`);
21531
+ return;
21532
+ }
21533
+ const essentialContainers = newService.task.containers.filter((c) => c.essential);
21534
+ const containersToCycle = essentialContainers.length > 0 ? essentialContainers : newService.task.containers.length > 0 ? [newService.task.containers[0]] : [];
21535
+ if (containersToCycle.length === 0) throw new EcsServiceRunnerError(`softReloadReplica: service '${newService.serviceLogicalId}' has no containers to cycle.`);
21536
+ const startedById = new Map(instance.state.startedContainers.map((c) => [c.name, c.id]));
21537
+ const targets = [];
21538
+ for (const container of containersToCycle) {
21539
+ const id = startedById.get(container.name);
21540
+ if (!id) throw new EcsServiceRunnerError(`softReloadReplica: replica r${instance.index} has no started container named '${container.name}' (started: ${[...startedById.keys()].join(", ") || "none"}).`);
21541
+ targets.push({
21542
+ name: container.name,
21543
+ id
21544
+ });
21545
+ }
21546
+ const controllerOptions = controller.options;
21547
+ if (controllerOptions.discovery) {
21548
+ for (const handle of instance.cloudMapHandles) try {
21549
+ controllerOptions.discovery.registry.unregister(handle);
21550
+ } catch {}
21551
+ instance.cloudMapHandles = [];
21552
+ }
21553
+ unregisterReplicaFromFrontDoor(instance, controllerOptions.frontDoor);
21554
+ instance.softReloadInProgress = true;
21555
+ instance.softReloadGeneration = (instance.softReloadGeneration ?? 0) + 1;
21556
+ try {
21557
+ logger.info(`Soft-reloading replica r${instance.index} (gen ${instance.generation}): docker cp ${sourceDirToCopy} -> ${targets.length} essential container(s); restart.`);
21558
+ for (const target of targets) {
21559
+ let workdir;
21560
+ try {
21561
+ workdir = await dockerInspectWorkdirImpl(target.id) || "/";
21562
+ } catch (err) {
21563
+ throw new EcsServiceRunnerError(`softReloadReplica: docker inspect of container '${target.name}' (${target.id}) failed: ${err instanceof Error ? err.message : String(err)}. This replica is unregistered from Cloud Map + the front-door pool until the next save triggers another reload.`);
21564
+ }
21565
+ const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
21566
+ try {
21567
+ await dockerCpImpl(`${sourceDirToCopy}/.`, `${target.id}:${workdirDest}`);
21568
+ } catch (err) {
21569
+ throw new EcsServiceRunnerError(`softReloadReplica: docker cp into '${target.name}' (${target.id}:${workdir}) failed: ${err instanceof Error ? err.message : String(err)}. This replica is unregistered from Cloud Map + the front-door pool until the next save triggers another reload.`);
21570
+ }
21571
+ try {
21572
+ await dockerRestartImpl(target.id);
21573
+ } catch (err) {
21574
+ throw new EcsServiceRunnerError(`softReloadReplica: docker restart of '${target.name}' (${target.id}) failed: ${err instanceof Error ? err.message : String(err)}. This replica is unregistered from Cloud Map + the front-door pool until the next save triggers another reload.`);
21575
+ }
21576
+ }
21577
+ await waitForReplicaTcpReady(newService, instance, {
21578
+ timeoutMs: shadowReadyTimeoutMs,
21579
+ intervalMs: shadowReadyIntervalMs,
21580
+ label: `Soft-reloaded replica r${instance.index} (gen ${instance.generation})`
21581
+ });
21582
+ const ownerKeyGenSuffix = instance.generation > 0 ? `:g${instance.generation}` : "";
21583
+ const ownerKeyPrefix = `${newService.serviceLogicalId}:r${instance.index}${ownerKeyGenSuffix}`;
21584
+ if (controllerOptions.discovery) await publishReplicaToCloudMap(newService, instance, controllerOptions.discovery, ownerKeyPrefix);
21585
+ if (controllerOptions.frontDoor) await publishReplicaToFrontDoor(newService, instance, controllerOptions.frontDoor, controllerOptions.taskOptions.containerHost, ownerKeyPrefix);
21586
+ instance.lastDeployedAssetHash = pickEssentialAssetHash(newService);
21587
+ logger.info(`Soft-reloaded replica r${instance.index} (gen ${instance.generation}): ${SOFT_RELOAD_COMPLETION_LOG_SUFFIX}`);
21588
+ } finally {
21589
+ instance.softReloadInProgress = false;
21590
+ }
21591
+ }
21592
+ /**
21593
+ * Production `docker inspect --format {{.Config.WorkingDir}} <id>`
21594
+ * impl. Returns the container's runtime WORKDIR; empty string when
21595
+ * the Dockerfile didn't set one (caller treats empty as `/`, matching
21596
+ * Docker's runtime default).
21597
+ *
21598
+ * Extracted as a test-overridable function so the soft-reload
21599
+ * primitive's unit tests can assert the WORKDIR resolution branch
21600
+ * without standing up a real container.
21601
+ */
21602
+ const defaultDockerInspectWorkdirImpl = async (containerId) => {
21603
+ const { execFile } = await import("node:child_process");
21604
+ const { promisify } = await import("node:util");
21605
+ const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21606
+ const { stdout } = await promisify(execFile)(getDockerCmd(), [
21607
+ "inspect",
21608
+ "--format",
21609
+ "{{.Config.WorkingDir}}",
21610
+ containerId
21611
+ ]);
21612
+ return stdout.trim();
21613
+ };
21614
+ let dockerInspectWorkdirImpl = defaultDockerInspectWorkdirImpl;
21615
+ /**
21616
+ * Production `docker cp <src> <containerId>:<dst>` impl. The source
21617
+ * path's trailing `/.` ensures CONTENTS of the directory are copied
21618
+ * (not the directory itself); the caller is responsible for that
21619
+ * convention.
21620
+ */
21621
+ const defaultDockerCpImpl = async (src, dst) => {
21622
+ const { execFile } = await import("node:child_process");
21623
+ const { promisify } = await import("node:util");
21624
+ const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21625
+ await promisify(execFile)(getDockerCmd(), [
21626
+ "cp",
21627
+ src,
21628
+ dst
21629
+ ], { maxBuffer: 64 * 1024 * 1024 });
21630
+ };
21631
+ let dockerCpImpl = defaultDockerCpImpl;
21632
+ /**
21633
+ * Production `docker restart <id>` impl. Synchronous in docker's
21634
+ * sense — blocks until the container is up again (or fails).
21635
+ */
21636
+ const defaultDockerRestartImpl = async (containerId) => {
21637
+ const { execFile } = await import("node:child_process");
21638
+ const { promisify } = await import("node:util");
21639
+ const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21640
+ await promisify(execFile)(getDockerCmd(), ["restart", containerId]);
21641
+ };
21642
+ let dockerRestartImpl = defaultDockerRestartImpl;
21643
+ /**
21407
21644
  * Phase 2 of issue #214 — disconnect every container of the dying
21408
21645
  * replica from the shared service network BEFORE `cleanupEcsRun`'s
21409
21646
  * `docker stop → docker rm` sequence. Docker's embedded DNS strips an
@@ -21470,13 +21707,14 @@ let dockerNetworkDisconnectImpl = defaultDockerNetworkDisconnectImpl;
21470
21707
  * injectable via {@link __setTcpProbeImpl} so the rolling-primitive
21471
21708
  * unit test can avoid any real TCP socket.
21472
21709
  */
21473
- async function waitForReplicaTcpReady(service, shadow, opts) {
21710
+ async function waitForReplicaTcpReady(service, replica, opts) {
21474
21711
  const logger = getLogger().child("ecs-service");
21475
- const networkName = shadow.state.network?.networkName;
21712
+ const label = opts.label ?? `Shadow replica r${replica.index} (gen ${replica.generation})`;
21713
+ const networkName = replica.state.network?.networkName;
21476
21714
  if (!networkName) return;
21477
21715
  const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
21478
21716
  if (!essential || essential.portMappings.length === 0) return;
21479
- const started = shadow.state.startedContainers.find((c) => c.name === essential.name);
21717
+ const started = replica.state.startedContainers.find((c) => c.name === essential.name);
21480
21718
  if (!started) return;
21481
21719
  let ip;
21482
21720
  try {
@@ -21484,7 +21722,7 @@ async function waitForReplicaTcpReady(service, shadow, opts) {
21484
21722
  if (!resolved) return;
21485
21723
  ip = resolved;
21486
21724
  } catch (err) {
21487
- logger.warn(`Shadow replica r${shadow.index} (gen ${shadow.generation}): TCP-ready probe could not resolve docker IP: ${err instanceof Error ? err.message : String(err)}. Proceeding with swap.`);
21725
+ logger.warn(`${label}: TCP-ready probe could not resolve docker IP: ${err instanceof Error ? err.message : String(err)}. Proceeding.`);
21488
21726
  return;
21489
21727
  }
21490
21728
  const port = essential.portMappings[0].containerPort;
@@ -21493,14 +21731,14 @@ async function waitForReplicaTcpReady(service, shadow, opts) {
21493
21731
  while (Date.now() < deadline) {
21494
21732
  try {
21495
21733
  await tcpProbeImpl(ip, port);
21496
- logger.debug(`Shadow replica r${shadow.index} (gen ${shadow.generation}): TCP probe ${ip}:${port} accepted; proceeding with swap.`);
21734
+ logger.debug(`${label}: TCP probe ${ip}:${port} accepted.`);
21497
21735
  return;
21498
21736
  } catch (err) {
21499
21737
  lastErr = err instanceof Error ? err.message : String(err);
21500
21738
  }
21501
21739
  await sleep(opts.intervalMs);
21502
21740
  }
21503
- logger.warn(`Shadow replica r${shadow.index} (gen ${shadow.generation}): TCP probe ${ip}:${port} did not accept within ${opts.timeoutMs}ms (last: ${lastErr ?? "n/a"}). Swapping anyway — the new image is the user intent. Initial requests after the swap may 502 until the app finishes binding.`);
21741
+ logger.warn(`${label}: TCP probe ${ip}:${port} did not accept within ${opts.timeoutMs}ms (last: ${lastErr ?? "n/a"}). Proceeding anyway — the new source is the user intent. Initial requests may 502 until the app finishes binding.`);
21504
21742
  }
21505
21743
  /**
21506
21744
  * Default TCP-connect probe used by {@link waitForReplicaTcpReady}.
@@ -21541,6 +21779,7 @@ async function watchReplica(service, options, instance, runState) {
21541
21779
  await sleep(500);
21542
21780
  continue;
21543
21781
  }
21782
+ const softReloadGenBeforeWait = instance.softReloadGeneration ?? 0;
21544
21783
  let exitCode;
21545
21784
  try {
21546
21785
  exitCode = await waitForExitImpl(essentialId);
@@ -21549,6 +21788,12 @@ async function watchReplica(service, options, instance, runState) {
21549
21788
  exitCode = -1;
21550
21789
  }
21551
21790
  if (instance.shuttingDown || runState.shuttingDown) return;
21791
+ const softReloadHappenedMidWait = (instance.softReloadGeneration ?? 0) !== softReloadGenBeforeWait;
21792
+ if (instance.softReloadInProgress || softReloadHappenedMidWait) {
21793
+ while (instance.softReloadInProgress && !instance.shuttingDown && !runState.shuttingDown) await sleep(100);
21794
+ if (instance.shuttingDown || runState.shuttingDown) return;
21795
+ continue;
21796
+ }
21552
21797
  logger.warn(`Replica ${instance.index} essential container exited with code ${exitCode} (restartCount=${instance.restartCount}).`);
21553
21798
  const willRestart = shouldRestart(exitCode, options.restartPolicy);
21554
21799
  if (!willRestart || instance.restartCount === 0) await printExitedContainerLogs(instance.index, essentialId, logger);
@@ -21664,6 +21909,152 @@ function sleep(ms) {
21664
21909
  return sleepImpl(ms);
21665
21910
  }
21666
21911
 
21912
+ //#endregion
21913
+ //#region src/local/source-change-classifier.ts
21914
+ /**
21915
+ * Dependency-manifest basenames recognized by the classifier. A change
21916
+ * to any of these forces a rebuild because the running container's
21917
+ * pre-built dependency layer is no longer in sync with the source.
21918
+ *
21919
+ * Coverage: the package managers commonly used inside a Lambda /
21920
+ * container image — Node (pnpm / npm / yarn), Python (pip / poetry /
21921
+ * pipenv), Ruby (bundler), Go (modules), Rust (cargo), Java / Kotlin
21922
+ * (Maven, Gradle). Adding a new ecosystem? Append its lockfile +
21923
+ * manifest here and add a classifier test row.
21924
+ */
21925
+ const REBUILD_TRIGGER_BASENAMES = new Set([
21926
+ "package.json",
21927
+ "package-lock.json",
21928
+ "pnpm-lock.yaml",
21929
+ "yarn.lock",
21930
+ "npm-shrinkwrap.json",
21931
+ "requirements.txt",
21932
+ "requirements-dev.txt",
21933
+ "pyproject.toml",
21934
+ "poetry.lock",
21935
+ "Pipfile",
21936
+ "Pipfile.lock",
21937
+ "uv.lock",
21938
+ "Gemfile",
21939
+ "Gemfile.lock",
21940
+ "go.mod",
21941
+ "go.sum",
21942
+ "Cargo.toml",
21943
+ "Cargo.lock",
21944
+ "pom.xml",
21945
+ "build.gradle",
21946
+ "build.gradle.kts",
21947
+ "settings.gradle",
21948
+ "settings.gradle.kts",
21949
+ "Makefile",
21950
+ "CMakeLists.txt"
21951
+ ]);
21952
+ /**
21953
+ * Compiled-language source extensions that require a build step
21954
+ * inside `docker build` (typically a `RUN go build` / `cargo build` /
21955
+ * `mvn package` etc.). A copy of the source alone would leave the
21956
+ * running binary stale, so the user's intent must be a rebuild.
21957
+ *
21958
+ * Interpreted-language runtimes (Node — `.js` / `.mjs` / `.cjs` /
21959
+ * `.ts` when transpiled at runtime, Python — `.py`, Ruby — `.rb`,
21960
+ * shell — `.sh`) read source at process start, so a `docker cp` +
21961
+ * `docker restart` cycle picks them up. Those extensions are
21962
+ * NOT in this set.
21963
+ */
21964
+ const COMPILED_LANGUAGE_EXTENSIONS = new Set([
21965
+ ".go",
21966
+ ".rs",
21967
+ ".java",
21968
+ ".kt",
21969
+ ".kts",
21970
+ ".scala",
21971
+ ".cs",
21972
+ ".swift",
21973
+ ".fs",
21974
+ ".fsx",
21975
+ ".c",
21976
+ ".cc",
21977
+ ".cpp",
21978
+ ".cxx",
21979
+ ".h",
21980
+ ".hpp",
21981
+ ".zig",
21982
+ ".ml",
21983
+ ".mli",
21984
+ ".elm",
21985
+ ".hs",
21986
+ ".dart"
21987
+ ]);
21988
+ /**
21989
+ * Classify a single watcher firing into rebuild vs soft-reload. Pure
21990
+ * + synchronous. The caller (emulator's reload pathway) invokes this
21991
+ * once per target per firing AFTER `cdk synth` has run and the new
21992
+ * asset manifest is on disk.
21993
+ *
21994
+ * Branching:
21995
+ * 1. No asset context (image isn't a CDK asset, or asset lookup
21996
+ * failed) → `rebuild`.
21997
+ * 2. The asset hash didn't change between old and new synths
21998
+ * (`oldAssetHash === newAssetHash`, or `oldAssetHash` missing)
21999
+ * → `rebuild`. Load-bearing guard for "user edited a CDK
22000
+ * construct file (e.g. `lib/stack.ts`) that flipped the task
22001
+ * spec but didn't touch the asset content". Soft-reload would
22002
+ * `docker cp` identical files and `docker restart` the
22003
+ * container with the OLD task spec (env / memory / mounts /
22004
+ * added sidecars are set at `docker create` time, not on
22005
+ * restart) — the user's intent would silently NOT apply.
22006
+ * Forcing rebuild keeps Phase 1-3 semantics exactly for this
22007
+ * case: the rolling primitive boots a shadow with the new task
22008
+ * spec, the user sees their construct edit take effect.
22009
+ * 3. No changed paths (the watcher fired on a debounce flush with
22010
+ * an empty pending set — shouldn't happen in practice, but
22011
+ * defensive) → `rebuild`.
22012
+ * 4. Any changed path's basename matches the Dockerfile or a
22013
+ * dependency manifest → `rebuild`.
22014
+ * 5. Any changed path's extension is a compiled-language source →
22015
+ * `rebuild`.
22016
+ * 6. Else → `soft-reload`.
22017
+ */
22018
+ function classifySourceChange(changedPaths, ctx) {
22019
+ if (!ctx) return {
22020
+ kind: "rebuild",
22021
+ reason: "target image is not a CDK docker-image asset"
22022
+ };
22023
+ if (!ctx.oldAssetHash || ctx.oldAssetHash === ctx.newAssetHash) return {
22024
+ kind: "rebuild",
22025
+ reason: "asset hash unchanged across the synth (CDK construct edit or unrelated file) — task-spec changes need a fresh `docker create`, which only the rebuild path runs"
22026
+ };
22027
+ if (changedPaths.length === 0) return {
22028
+ kind: "rebuild",
22029
+ reason: "no changed paths reported (defensive default)"
22030
+ };
22031
+ for (const p of changedPaths) {
22032
+ const basename = path.basename(p);
22033
+ if (basename === ctx.dockerFile) return {
22034
+ kind: "rebuild",
22035
+ reason: `Dockerfile edit (${basename})`
22036
+ };
22037
+ if (basename.startsWith("Dockerfile.")) return {
22038
+ kind: "rebuild",
22039
+ reason: `Dockerfile.* edit (${basename})`
22040
+ };
22041
+ if (REBUILD_TRIGGER_BASENAMES.has(basename)) return {
22042
+ kind: "rebuild",
22043
+ reason: `dependency manifest edit (${basename})`
22044
+ };
22045
+ const ext = path.extname(p).toLowerCase();
22046
+ if (COMPILED_LANGUAGE_EXTENSIONS.has(ext)) return {
22047
+ kind: "rebuild",
22048
+ reason: `compiled-language source edit (${basename}) — soft-reload would leave the built binary stale`
22049
+ };
22050
+ }
22051
+ return {
22052
+ kind: "soft-reload",
22053
+ reason: `${changedPaths.length} source-only path(s) — skipping rebuild`,
22054
+ newAssetSourceDir: ctx.newAssetSourceDir
22055
+ };
22056
+ }
22057
+
21667
22058
  //#endregion
21668
22059
  //#region src/local/cloud-map-registry.ts
21669
22060
  /**
@@ -22379,9 +22770,51 @@ function escapeRealmQuotes(realm) {
22379
22770
  }
22380
22771
  /** Reply 404 — an ALB listener with no matching rule and no default action. */
22381
22772
  function reply404(req, res, opts) {
22382
- writeError(res, 404, `No listener rule matched '${req.url ?? "/"}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
22773
+ writeError(res, 404, buildNoRuleMatched404Body(req, opts));
22383
22774
  return Promise.resolve();
22384
22775
  }
22776
+ /**
22777
+ * Build the no-rule-matched 404 body. When the call site supplied a
22778
+ * {@link StartFrontDoorServerOptions.rulesSummary}, the body lists every
22779
+ * ALB condition field that WAS evaluated (method, host, path) plus every
22780
+ * configured rule's priority + conditions + action target — so a user
22781
+ * whose request missed on, say, the Host header can spot the mismatch
22782
+ * without inspecting the synthesized template. Header conditions are
22783
+ * NOT spelled out in the evaluated section (too noisy) but ARE listed
22784
+ * in each rule's condition row when the rule constrains them. Without a
22785
+ * summary the body falls back to the original path-only shape (preserves
22786
+ * the behavior for direct callers that wire the proxy with just a
22787
+ * `selectPool` / `selectTarget`).
22788
+ */
22789
+ function buildNoRuleMatched404Body(req, opts) {
22790
+ const requestPath = req.url ?? "/";
22791
+ const summary = opts.rulesSummary;
22792
+ if (!summary) return `No listener rule matched '${requestPath}' on ${opts.label}, and the listener has no default action forwarding to a local target.`;
22793
+ const rawHost = req.headers.host;
22794
+ const hostValue = Array.isArray(rawHost) ? rawHost[0] : rawHost;
22795
+ const lines = [];
22796
+ lines.push(`No listener rule matched the request on ${opts.label}, and the listener has no default action forwarding to a local target.`);
22797
+ lines.push("");
22798
+ lines.push(" Evaluated:");
22799
+ lines.push(` Method: ${req.method ?? "(unknown)"}`);
22800
+ lines.push(` Host: ${hostValue ?? "(no Host header)"}`);
22801
+ lines.push(` Path: ${requestPath}`);
22802
+ lines.push("");
22803
+ if (summary.length === 0) lines.push(" Listener has 0 rule(s).");
22804
+ else {
22805
+ lines.push(` Listener has ${summary.length} rule(s):`);
22806
+ const ordered = [...summary].sort((a, b) => a.priority - b.priority);
22807
+ for (const rule of ordered) {
22808
+ const conditions = rule.conditions.length === 0 ? "(no condition)" : rule.conditions.map(formatRuleConditionSummary).join(" AND ");
22809
+ lines.push(` [priority=${rule.priority}] ${conditions} -> ${rule.action}`);
22810
+ }
22811
+ }
22812
+ return lines.join("\n");
22813
+ }
22814
+ /** Format one condition row of a {@link FrontDoorRuleSummary} for the 404 body. */
22815
+ function formatRuleConditionSummary(c) {
22816
+ return `${c.field} in [${c.values.join(", ")}]`;
22817
+ }
22385
22818
  function handlePoolRequest(req, res, pool, opts) {
22386
22819
  return new Promise((resolve) => {
22387
22820
  const endpoint = pool.next();
@@ -23319,9 +23752,9 @@ function materializeInlineCode(handler, source, fileExtension) {
23319
23752
  const lastDot = handler.lastIndexOf(".");
23320
23753
  if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
23321
23754
  const modulePath = handler.substring(0, lastDot);
23322
- const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-alb-lambda-`));
23323
- const filePath = path.join(dir, `${modulePath}${fileExtension}`);
23324
- mkdirSync(path.dirname(filePath), { recursive: true });
23755
+ const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-alb-lambda-`));
23756
+ const filePath = path$1.join(dir, `${modulePath}${fileExtension}`);
23757
+ mkdirSync(path$1.dirname(filePath), { recursive: true });
23325
23758
  writeFileSync(filePath, source, "utf-8");
23326
23759
  return dir;
23327
23760
  }
@@ -23353,7 +23786,7 @@ async function resolveContainerImagePlan(lambda, opts) {
23353
23786
  let imageRef;
23354
23787
  let localBuilt = false;
23355
23788
  if (manifestPath) {
23356
- const cdkOutDir = path.dirname(manifestPath);
23789
+ const cdkOutDir = path$1.dirname(manifestPath);
23357
23790
  const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
23358
23791
  const entry = manifest ? getDockerImageBySourceHash(manifest, lambda.imageUri) : void 0;
23359
23792
  if (entry) {
@@ -23719,8 +24152,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
23719
24152
  paths: [watchRoot],
23720
24153
  ignored,
23721
24154
  shouldTrigger,
23722
- onChange: () => {
23723
- logger.info("Detected source change; reloading service(s)...");
24155
+ onChange: (changedPaths) => {
24156
+ logger.info(`Detected source change (${changedPaths.length} path(s)); reloading service(s)...`);
23724
24157
  reloadChain = reloadChain.then(() => reloadAllServices({
23725
24158
  perTarget,
23726
24159
  synthesizer,
@@ -23734,6 +24167,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
23734
24167
  extraStateProviders,
23735
24168
  profileCredsFile,
23736
24169
  frontDoorByService,
24170
+ changedPaths,
23737
24171
  logger
23738
24172
  })).catch((err) => {
23739
24173
  logger.error(`reloadAllServices threw: ${err instanceof Error ? err.message : String(err)}`);
@@ -23752,11 +24186,22 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
23752
24186
  }
23753
24187
  }
23754
24188
  /**
23755
- * Phase 2 of issue #214 — multi-replica rolling reload cycle for
23756
- * `cdkl start-service --watch`. Mirrors start-api's `reloadAllServers`
23757
- * shape but per-ECS-service, replacing Phase 1's "tear single replica
23758
- * down, boot fresh" sequence with a per-replica rolling loop so the
23759
- * service stays available end-to-end:
24189
+ * Phase 2 + Phase 4 of issue #214 — multi-replica reload cycle for
24190
+ * `cdkl start-service --watch` (Phase 2) and `cdkl start-alb --watch`
24191
+ * (Phase 3 wires the same loop). Mirrors start-api's `reloadAllServers`
24192
+ * shape but per-ECS-service. Per-target verdict from
24193
+ * {@link classifySourceChange} (Phase 4) picks the per-replica action:
24194
+ *
24195
+ * - `'soft-reload'` → {@link softReloadReplica} runs `docker cp`
24196
+ * + `docker restart` against the live replica. No `docker build`,
24197
+ * no shadow boot, no Cloud Map / front-door pool swap — the
24198
+ * container's IP + host port are preserved across the restart, so
24199
+ * existing registrations stay valid. Fast path (~sub-second per
24200
+ * replica for typical interpreted-language handlers).
24201
+ * - `'rebuild'` → {@link rollServiceReplica}, the Phase 1-3 path,
24202
+ * replacing Phase 1's "tear single replica down, boot fresh"
24203
+ * sequence with a per-replica rolling loop so the service stays
24204
+ * available end-to-end:
23760
24205
  *
23761
24206
  * 1. Re-runs `synthesizer.synthesize(synthOpts)` once (failure → warn
23762
24207
  * + keep every replica serving).
@@ -23791,7 +24236,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
23791
24236
  * via the logger so the user can fix the source + save again.
23792
24237
  */
23793
24238
  async function reloadAllServices(args) {
23794
- const { perTarget, synthesizer, synthOpts, strategy, resolvedTargets, cloudMapIndexByStack, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService, logger } = args;
24239
+ const { perTarget, synthesizer, synthOpts, strategy, resolvedTargets, cloudMapIndexByStack, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService, changedPaths, logger } = args;
23795
24240
  let stacks;
23796
24241
  try {
23797
24242
  ({stacks} = await synthesizer.synthesize(synthOpts));
@@ -23808,6 +24253,8 @@ async function reloadAllServices(args) {
23808
24253
  cloudMapIndexByStack.set(stack.stackName, index);
23809
24254
  for (const w of index.warnings) logger.warn(w);
23810
24255
  }
24256
+ const cdkOutDir = options.output;
24257
+ const assetLoader = new AssetManifestLoader();
23811
24258
  for (const pt of perTarget) {
23812
24259
  const newBoot = newBootByTarget.get(pt.boot.target);
23813
24260
  if (!newBoot) {
@@ -23819,6 +24266,27 @@ async function reloadAllServices(args) {
23819
24266
  logger.warn(`Reload: target '${pt.boot.target}' has no live controller (previous boot likely failed); skipping roll. \`^C\` and re-run start-service to recover.`);
23820
24267
  continue;
23821
24268
  }
24269
+ let verdict = {
24270
+ kind: "rebuild",
24271
+ reason: "classifier not consulted"
24272
+ };
24273
+ try {
24274
+ verdict = classifySourceChange(changedPaths, await loadAssetContextForTarget({
24275
+ target: newBoot.target,
24276
+ controller,
24277
+ stacks,
24278
+ cdkOutDir,
24279
+ assetLoader,
24280
+ logger
24281
+ }));
24282
+ logger.info(`Reload of '${newBoot.target}': verdict=${verdict.kind} (${verdict.reason}).`);
24283
+ } catch (err) {
24284
+ logger.warn(`Reload of '${newBoot.target}': classifier context unavailable (${err instanceof Error ? err.message : String(err)}); falling back to rebuild.`);
24285
+ verdict = {
24286
+ kind: "rebuild",
24287
+ reason: "classifier context unavailable; falling back to rebuild"
24288
+ };
24289
+ }
23822
24290
  await rollOneTarget({
23823
24291
  controller,
23824
24292
  newBoot,
@@ -23830,12 +24298,73 @@ async function reloadAllServices(args) {
23830
24298
  profileCredsFile,
23831
24299
  frontDoorPools: frontDoorByService.get(newBoot.target),
23832
24300
  suppressLoadBalancerWarning: strategy.suppressLoadBalancerWarning === true,
24301
+ verdict,
23833
24302
  logger
23834
24303
  });
23835
24304
  }
23836
24305
  logger.info("Reload complete.");
23837
24306
  }
23838
24307
  /**
24308
+ * Phase 4 of issue #214 — load the per-target asset context the
24309
+ * source-change classifier consumes. Resolves the target's docker-image
24310
+ * asset hash via the freshly-synthed `<stackName>.assets.json` and
24311
+ * derives the staged source directory + Dockerfile basename.
24312
+ *
24313
+ * Returns `undefined` (and logs at debug) when:
24314
+ * - The target's image isn't a CDK docker-image asset (ECR / public
24315
+ * registry pin). The classifier treats `undefined` as `rebuild`
24316
+ * because there's no local source tree to copy.
24317
+ * - The asset manifest can't be loaded (stack not synthed yet, file
24318
+ * missing). Same treatment — defensive default to `rebuild`.
24319
+ * - The asset hash isn't in the manifest's `dockerImages`. Same.
24320
+ *
24321
+ * Throws on a malformed manifest (parse failure surfaced via
24322
+ * {@link AssetManifestLoader}) so the caller can fall back to rebuild
24323
+ * with a warn line that explains why the classifier couldn't run.
24324
+ */
24325
+ /**
24326
+ * @internal — exported for unit tests of the fall-through branches
24327
+ * (the 6 `return undefined` paths + the catch arm on
24328
+ * `resolveEcsServiceTarget` throw). Not part of the semver-covered
24329
+ * public surface; the only legitimate caller is `reloadAllServices`
24330
+ * inside this file.
24331
+ */
24332
+ async function loadAssetContextForTarget(args) {
24333
+ const { target, controller, stacks, cdkOutDir, assetLoader, logger } = args;
24334
+ const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
24335
+ if (!candidate) return void 0;
24336
+ let newService;
24337
+ try {
24338
+ newService = resolveEcsServiceTarget(target, stacks, void 0, { suppressLoadBalancerWarning: true });
24339
+ } catch (err) {
24340
+ logger.debug(`loadAssetContextForTarget: target '${target}' could not be re-resolved against the new stacks: ${err instanceof Error ? err.message : String(err)}. Classifier will see no asset context (rebuild).`);
24341
+ return;
24342
+ }
24343
+ const essential = newService.task.containers.find((c) => c.essential) ?? newService.task.containers[0];
24344
+ if (!essential) return void 0;
24345
+ if (essential.image.kind !== "cdk-asset" || !essential.image.assetHash) return;
24346
+ const newAssetHash = essential.image.assetHash;
24347
+ const manifest = await assetLoader.loadManifest(cdkOutDir, candidate.stackName);
24348
+ if (!manifest) return void 0;
24349
+ const newDockerImage = manifest.dockerImages?.[newAssetHash];
24350
+ if (!newDockerImage) return void 0;
24351
+ if (!newDockerImage.source.directory) return;
24352
+ const newAssetSourceDir = path.resolve(cdkOutDir, newDockerImage.source.directory);
24353
+ let oldAssetHash;
24354
+ const liveReplica = controller.runState.replicas.find((r) => !r.shuttingDown);
24355
+ if (liveReplica?.lastDeployedAssetHash !== void 0) oldAssetHash = liveReplica.lastDeployedAssetHash;
24356
+ else {
24357
+ const oldEssential = controller.service.task.containers.find((c) => c.essential) ?? controller.service.task.containers[0];
24358
+ if (oldEssential?.image.kind === "cdk-asset") oldAssetHash = oldEssential.image.assetHash;
24359
+ }
24360
+ return {
24361
+ ...oldAssetHash !== void 0 && { oldAssetHash },
24362
+ newAssetHash,
24363
+ newAssetSourceDir,
24364
+ dockerFile: path.basename(newDockerImage.source.dockerFile ?? "Dockerfile")
24365
+ };
24366
+ }
24367
+ /**
23839
24368
  * Phase 2 of issue #214 — roll every replica of one target through the
23840
24369
  * new task descriptor sequentially. Extracted from {@link reloadAllServices}
23841
24370
  * so the per-target try/catch logic (synth-failure / resolve-failure /
@@ -23846,7 +24375,7 @@ async function reloadAllServices(args) {
23846
24375
  * even when the resolve / roll throws.
23847
24376
  */
23848
24377
  async function rollOneTarget(args) {
23849
- const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, logger } = args;
24378
+ const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, verdict, logger } = args;
23850
24379
  const candidate = pickCandidateStack(parseEcsTarget(newBoot.target).stackPattern, stacks);
23851
24380
  const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
23852
24381
  try {
@@ -23865,7 +24394,8 @@ async function rollOneTarget(args) {
23865
24394
  return;
23866
24395
  }
23867
24396
  if (newService.desiredCount !== oldReplicas.length) logger.warn(`Reload of '${newBoot.target}': service DesiredCount=${newService.desiredCount} does not match the ${oldReplicas.length} live replica(s); rolling existing replicas only — scale changes during --watch are not yet supported. \`^C\` and re-run start-service to apply the new replica count.`);
23868
- logger.info(`Reload of '${newBoot.target}': rolling ${oldReplicas.length} replica(s) one at a time (start new shadowswap registrationsstop old).`);
24397
+ if (verdict.kind === "soft-reload") logger.info(`Reload of '${newBoot.target}': soft-reloading ${oldReplicas.length} replica(s) one at a time (docker cp sourcedocker restartTCP-ready probe; no rebuild).`);
24398
+ else logger.info(`Reload of '${newBoot.target}': rolling ${oldReplicas.length} replica(s) one at a time (start new shadow → swap registrations → stop old).`);
23869
24399
  for (const oldInstance of oldReplicas) {
23870
24400
  const idx = controller.runState.replicas.indexOf(oldInstance);
23871
24401
  if (idx === -1) {
@@ -23873,7 +24403,13 @@ async function rollOneTarget(args) {
23873
24403
  continue;
23874
24404
  }
23875
24405
  try {
23876
- await rollServiceReplica({
24406
+ if (verdict.kind === "soft-reload") await softReloadReplica({
24407
+ controller,
24408
+ oldReplicaIndex: idx,
24409
+ newService,
24410
+ sourceDirToCopy: verdict.newAssetSourceDir
24411
+ });
24412
+ else await rollServiceReplica({
23877
24413
  controller,
23878
24414
  oldReplicaIndex: idx,
23879
24415
  newService,
@@ -24116,6 +24652,7 @@ async function buildFrontDoor(plan, options, logger) {
24116
24652
  const tls = listener.protocol === "HTTPS" && wantTls ? tlsMaterials : void 0;
24117
24653
  const forwardedProto = listener.protocol === "HTTPS" ? "https" : "http";
24118
24654
  const degradedHttps = listener.protocol === "HTTPS" && !wantTls;
24655
+ const rulesSummary = listener.rules.map(buildRuleSummary);
24119
24656
  const server = await startFrontDoorServer({
24120
24657
  route,
24121
24658
  port: listener.hostPort,
@@ -24123,6 +24660,7 @@ async function buildFrontDoor(plan, options, logger) {
24123
24660
  listenerPort: listener.listenerPort,
24124
24661
  label: `listener port ${listener.listenerPort}`,
24125
24662
  forwardedProto,
24663
+ rulesSummary,
24126
24664
  ...tls ? { tls } : {}
24127
24665
  });
24128
24666
  servers.push(server);
@@ -24187,6 +24725,66 @@ function describeTarget(t) {
24187
24725
  function describeTargetShort(t) {
24188
24726
  return t.kind === "lambda" ? `Lambda ${t.lambda.logicalId}` : t.serviceTarget;
24189
24727
  }
24728
+ /**
24729
+ * Build the per-rule summary the front-door surfaces in its no-rule-matched
24730
+ * 404 body (issue #228). One condition row per constrained ALB field, plus a
24731
+ * pre-formatted action target. Distinct from {@link describeConditions} /
24732
+ * {@link describeAction} (which produce a single-line boot-banner string) —
24733
+ * the 404 body needs the rule decomposed into its constituent fields so the
24734
+ * formatter can render `field in [values]` rows.
24735
+ */
24736
+ function buildRuleSummary(rule) {
24737
+ const conditions = [];
24738
+ if (rule.pathPatterns.length > 0) conditions.push({
24739
+ field: "path-pattern",
24740
+ values: rule.pathPatterns
24741
+ });
24742
+ if (rule.hostPatterns.length > 0) conditions.push({
24743
+ field: "host-header",
24744
+ values: rule.hostPatterns
24745
+ });
24746
+ for (const h of rule.httpHeaderConditions) conditions.push({
24747
+ field: "http-header",
24748
+ values: [`${h.name}: ${h.values.join(", ")}`]
24749
+ });
24750
+ if (rule.httpRequestMethods.length > 0) conditions.push({
24751
+ field: "http-request-method",
24752
+ values: rule.httpRequestMethods
24753
+ });
24754
+ if (rule.queryStringConditions.length > 0) conditions.push({
24755
+ field: "query-string",
24756
+ values: rule.queryStringConditions.map(describeQueryStringCondition)
24757
+ });
24758
+ if (rule.sourceIpCidrs.length > 0) conditions.push({
24759
+ field: "source-ip",
24760
+ values: rule.sourceIpCidrs
24761
+ });
24762
+ return {
24763
+ priority: rule.priority,
24764
+ conditions,
24765
+ action: describeRuleActionForSummary(rule.action)
24766
+ };
24767
+ }
24768
+ /**
24769
+ * Describe a planned action for the no-rule-matched 404 body (issue #228).
24770
+ * Uses the `<ECS: ...>` / `<Lambda: ...>` shape the issue body proposes so a
24771
+ * user can read each rule's target at a glance.
24772
+ */
24773
+ function describeRuleActionForSummary(action) {
24774
+ if (action.kind === "redirect") return `redirect ${action.statusCode}`;
24775
+ if (action.kind === "fixed-response") return `fixed-response ${action.statusCode}`;
24776
+ if (action.targets.length === 1) return `forward to ${describeForwardTargetForSummary(action.targets[0])}`;
24777
+ return `forward weighted [${action.targets.map((t) => `${describeForwardTargetForSummary(t)}@${t.weight}`).join(", ")}]`;
24778
+ }
24779
+ /**
24780
+ * One forward target named the way the 404 body shows it: `<ECS: Service>` or
24781
+ * `<Lambda: LogicalId>` — distinct from the boot-banner format (which also
24782
+ * prints the container / port / round-robin hint) so the 404 body stays
24783
+ * scannable.
24784
+ */
24785
+ function describeForwardTargetForSummary(t) {
24786
+ return t.kind === "lambda" ? `<Lambda: ${t.lambda.logicalId}>` : `<ECS: ${t.serviceTarget}>`;
24787
+ }
24190
24788
  async function resolvePlaceholderAccount(arn, region) {
24191
24789
  if (!arn.includes("${AWS::AccountId}")) return arn;
24192
24790
  const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
@@ -24458,7 +25056,7 @@ function createLocalStartServiceCommand(opts = {}) {
24458
25056
  * for that contract to live.
24459
25057
  */
24460
25058
  function addStartServiceSpecificOptions(cmd) {
24461
- return cmd.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("--watch", "Hot-reload: re-synth + per-replica rolling deploy when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). Each replica is rolled one at a time — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically swap Service-Connect / Cloud Map registrations, then retire the old container so peer services see zero connection refusals across the reload even on multi-replica services. Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
25059
+ return cmd.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("--watch", "Hot-reload: re-synth + per-replica reload when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). A per-firing classifier picks the per-replica primitive: source-only edits on interpreted-language handlers (Node/Python/Ruby/shell) take a bind-mount FAST PATH (`docker cp` the new source into each replica + `docker restart`; no rebuild). Dockerfile / dependency manifest / compiled-language source / ambiguous edits fall through to the rebuild rolling primitive — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically swap Service-Connect / Cloud Map registrations, then retire the old container. Either path rolls one replica at a time, so peer services see zero connection refusals across the reload even on multi-replica services. Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
24462
25060
  }
24463
25061
 
24464
25062
  //#endregion
@@ -25311,7 +25909,7 @@ function createLocalStartAlbCommand(opts = {}) {
25311
25909
  * clusters. Chainable: returns `cmd`.
25312
25910
  */
25313
25911
  function addAlbSpecificOptions(cmd) {
25314
- return cmd.addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls", "Terminate TLS locally for cloud-HTTPS listeners. Default: a cloud-HTTPS listener is served over plain HTTP locally (X-Forwarded-Proto: https is preserved so the upstream app still sees the deployed listener protocol). Implied by --tls-cert / --tls-key. Use this when local-dev cookies need Secure / SameSite=None, when the upstream app inspects TLS metadata, or for mTLS / SNI testing — otherwise plain HTTP is friendlier (no self-signed cert warnings in curl / browser).")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Implies --tls. Must be set together with --tls-key. Pass --tls alone (without --tls-cert / --tls-key) to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Implies --tls. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT injected as Authorization: Bearer <jwt> when the inbound request has none. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Local-dev convenience; cookie pass-through (AWSELBAuthSessionCookie-*) also works.")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica rolling deploy of every ECS service behind the ALB when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). Each replica is rolled one at a time — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically register it in the front-door pool, then drop the old entry and retire the old container so a continuous external request stream against the listener port sees zero connection refusals across the reload. The host front-door (TLS, JWKS cache, Lambda-target containers, listener sockets) stays up across the reload. Lambda target groups behind the ALB are a no-op on reload (the warm RIE container keeps its boot-time image). Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
25912
+ return cmd.addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls", "Terminate TLS locally for cloud-HTTPS listeners. Default: a cloud-HTTPS listener is served over plain HTTP locally (X-Forwarded-Proto: https is preserved so the upstream app still sees the deployed listener protocol). Implied by --tls-cert / --tls-key. Use this when local-dev cookies need Secure / SameSite=None, when the upstream app inspects TLS metadata, or for mTLS / SNI testing — otherwise plain HTTP is friendlier (no self-signed cert warnings in curl / browser).")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Implies --tls. Must be set together with --tls-key. Pass --tls alone (without --tls-cert / --tls-key) to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Implies --tls. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT injected as Authorization: Bearer <jwt> when the inbound request has none. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Local-dev convenience; cookie pass-through (AWSELBAuthSessionCookie-*) also works.")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica reload of every ECS service behind the ALB when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). A per-firing classifier picks the per-replica primitive: source-only edits on interpreted-language handlers (Node/Python/Ruby/shell) take a bind-mount FAST PATH (`docker cp` the new source into each replica + `docker restart`; no rebuild, front-door pool entry unchanged since the IP/port are preserved). Dockerfile / dependency manifest / compiled-language source / ambiguous edits fall through to the rebuild rolling primitive — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically register it in the front-door pool, then drop the old entry and retire the old container. Either path rolls one replica at a time, so a continuous external request stream against the listener port sees zero connection refusals across the reload. The host front-door (TLS, JWKS cache, Lambda-target containers, listener sockets) stays up across the reload. Lambda target groups behind the ALB are a no-op on reload (the warm RIE container keeps its boot-time image). Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
25315
25913
  }
25316
25914
 
25317
25915
  //#endregion
@@ -25407,5 +26005,5 @@ function addListSpecificOptions(cmd) {
25407
26005
  }
25408
26006
 
25409
26007
  //#endregion
25410
- export { buildHttpApiV2Event as $, resolveRuntimeFileExtension as $t, createWatchPredicates as A, AGENTCORE_HTTP_PROTOCOL as An, A2A_PATH as At, readMtlsMaterialsFromDisk as B, LocalInvokeBuildError as Bn, invokeAgentCore as Bt, getContainerNetworkIp as C, discoverWebSocketApisOrThrow as Cn, applyCorsResponseHeaders as Ct, createLocalInvokeAgentCoreCommand as D, resolveLambdaArnIntrinsic as Dn, matchPreflight as Dt, addInvokeAgentCoreSpecificOptions as E, pickRefLogicalId as En, isFunctionUrlOacFronted as Et, buildStageMap as F, resolveAgentCoreTarget as Fn, mcpInvokeOnce as Ft, buildMethodArn as G, computeCodeImageTag as Gt, resolveSelectionExpression as H, downloadAndExtractS3Bundle as Ht, availableApiIdentifiers as I, derivePseudoParametersFromRegion as In, parseSseForJsonRpc as It, invokeRequestAuthorizer as J, addInvokeSpecificOptions as Jt, computeRequestIdentityHash as K, renderCodeDockerfile as Kt, filterRoutesByApiIdentifier as L, formatStateRemedy as Ln, AGENTCORE_SIGV4_SERVICE as Lt, createAuthorizerCache as M, AGENTCORE_RUNTIME_TYPE as Mn, MCP_CONTAINER_PORT as Mt, createFileWatcher as N, AgentCoreResolutionError as Nn, MCP_PATH as Nt, addStartApiSpecificOptions as O, AGENTCORE_A2A_PROTOCOL as On, invokeAgentCoreWs as Ot, attachStageContext as P, pickAgentCoreCandidateStack as Pn, MCP_PROTOCOL_VERSION as Pt, applyAuthorizerOverlay as Q, resolveRuntimeCodeMountPath as Qt, filterRoutesByApiIdentifiers as R, substituteImagePlaceholders as Rn, signAgentCoreInvocation as Rt, CloudMapRegistry as S, discoverWebSocketApis as Sn, attachAuthorizers as St, createLocalRunTaskCommand as T, discoverRoutes as Tn, buildCorsConfigFromCloudFrontChain as Tt, resolveServiceIntegrationParameters as U, SUPPORTED_CODE_RUNTIMES as Ut, startApiServer as V, waitForAgentCorePing as Vt, defaultCredentialsLoader as W, buildAgentCoreCodeImage as Wt, matchRoute as X, architectureToPlatform as Xt, invokeTokenAuthorizer as Y, createLocalInvokeCommand as Yt, translateLambdaResponse as Z, buildContainerImage as Zt, parseMaxTasks as _, resolveSsmParameters as _n, buildJwksUrlFromIssuer as _t, albStrategy as a, substituteEnvVarsFromStateAsync as an, VtlEvaluationError as at, runEcsServiceEmulator as b, countTargets as bn, verifyJwtAuthorizer as bt, resolveAlbTarget as c, LocalStateSourceError as cn, bufferToBody as ct, addStartServiceSpecificOptions as d, rejectExplicitCfnStackWithMultipleStacks as dn, handleConnectionsRequest as dt, resolveRuntimeImage as en, buildRestV1Event as et, createLocalStartServiceCommand as f, resolveCfnFallbackRegion as fn, parseConnectionsPath as ft, buildEcsImageResolutionContext as g, collectSsmParameterRefs as gn, buildCognitoJwksUrl as gt, addCommonEcsServiceOptions as h, CfnLocalStateProvider as hn, buildMessageEvent as ht, addAlbSpecificOptions as i, substituteEnvVarsFromState as in, tryParseStatus as it, resolveApiTargetSubset as j, AGENTCORE_MCP_PROTOCOL as jn, a2aInvokeOnce as jt, createLocalStartApiCommand as k, AGENTCORE_AGUI_PROTOCOL as kn, A2A_CONTAINER_PORT as kt, isApplicationLoadBalancer as l, createLocalStateProvider as ln, ConnectionRegistry as lt, MAX_TASKS_SUBNET_RANGE_CAP as m, resolveCfnStackName as mn, buildDisconnectEvent as mt, createLocalListCommand as n, substituteAgainstState as nn, pickResponseTemplate as nt, createLocalStartAlbCommand as o, resolveEnvVars as on, HOST_GATEWAY_MIN_VERSION as ot, serviceStrategy as p, resolveCfnRegion as pn, buildConnectEvent as pt, evaluateCachedLambdaPolicy as q, toCmdArgv as qt, formatTargetListing as r, substituteAgainstStateAsync as rn, selectIntegrationResponse as rt, parseLbPortOverrides as s, materializeLayerFromArn as sn, probeHostGatewaySupport as st, addListSpecificOptions as t, EcsTaskResolutionError as tn, evaluateResponseParameters as tt, resolveAlbFrontDoor as u, isCfnFlagPresent as un, buildMgmtEndpointEnvUrl as ut, parseRestartPolicy as v, resolveWatchConfig as vn, createJwksCache as vt, addRunTaskSpecificOptions as w, parseSelectionExpressionPath as wn, buildCorsConfigByApiId as wt, buildCloudMapIndex as x, listTargets as xn, verifyJwtViaDiscovery as xt, resolveSharedSidecarCredentials as y, resolveSingleTarget as yn, verifyCognitoJwt as yt, groupRoutesByServer as z, tryResolveImageFnJoin as zn, AGENTCORE_SESSION_ID_HEADER as zt };
25411
- //# sourceMappingURL=local-list-BFtuOXLq.js.map
26008
+ export { translateLambdaResponse as $, buildContainerImage as $t, addStartApiSpecificOptions as A, AGENTCORE_A2A_PROTOCOL as An, invokeAgentCoreWs as At, filterRoutesByApiIdentifiers as B, substituteImagePlaceholders as Bn, signAgentCoreInvocation as Bt, classifySourceChange as C, listTargets as Cn, verifyJwtViaDiscovery as Ct, createLocalRunTaskCommand as D, discoverRoutes as Dn, buildCorsConfigFromCloudFrontChain as Dt, addRunTaskSpecificOptions as E, parseSelectionExpressionPath as En, buildCorsConfigByApiId as Et, createFileWatcher as F, AgentCoreResolutionError as Fn, MCP_PATH as Ft, resolveServiceIntegrationParameters as G, SUPPORTED_CODE_RUNTIMES as Gt, readMtlsMaterialsFromDisk as H, LocalInvokeBuildError as Hn, invokeAgentCore as Ht, attachStageContext as I, pickAgentCoreCandidateStack as In, MCP_PROTOCOL_VERSION as It, computeRequestIdentityHash as J, renderCodeDockerfile as Jt, defaultCredentialsLoader as K, buildAgentCoreCodeImage as Kt, buildStageMap as L, resolveAgentCoreTarget as Ln, mcpInvokeOnce as Lt, createWatchPredicates as M, AGENTCORE_HTTP_PROTOCOL as Mn, A2A_PATH as Mt, resolveApiTargetSubset as N, AGENTCORE_MCP_PROTOCOL as Nn, a2aInvokeOnce as Nt, addInvokeAgentCoreSpecificOptions as O, pickRefLogicalId as On, isFunctionUrlOacFronted as Ot, createAuthorizerCache as P, AGENTCORE_RUNTIME_TYPE as Pn, MCP_CONTAINER_PORT as Pt, matchRoute as Q, architectureToPlatform as Qt, availableApiIdentifiers as R, derivePseudoParametersFromRegion as Rn, parseSseForJsonRpc as Rt, CloudMapRegistry as S, countTargets as Sn, verifyJwtAuthorizer as St, getContainerNetworkIp as T, discoverWebSocketApisOrThrow as Tn, applyCorsResponseHeaders as Tt, startApiServer as U, waitForAgentCorePing as Ut, groupRoutesByServer as V, tryResolveImageFnJoin as Vn, AGENTCORE_SESSION_ID_HEADER as Vt, resolveSelectionExpression as W, downloadAndExtractS3Bundle as Wt, invokeRequestAuthorizer as X, addInvokeSpecificOptions as Xt, evaluateCachedLambdaPolicy as Y, toCmdArgv as Yt, invokeTokenAuthorizer as Z, createLocalInvokeCommand as Zt, parseMaxTasks as _, CfnLocalStateProvider as _n, buildMessageEvent as _t, albStrategy as a, substituteAgainstStateAsync as an, selectIntegrationResponse as at, runEcsServiceEmulator as b, resolveWatchConfig as bn, createJwksCache as bt, resolveAlbTarget as c, resolveEnvVars as cn, HOST_GATEWAY_MIN_VERSION as ct, addStartServiceSpecificOptions as d, createLocalStateProvider as dn, ConnectionRegistry as dt, resolveRuntimeCodeMountPath as en, applyAuthorizerOverlay as et, createLocalStartServiceCommand as f, isCfnFlagPresent as fn, buildMgmtEndpointEnvUrl as ft, buildEcsImageResolutionContext as g, resolveCfnStackName as gn, buildDisconnectEvent as gt, addCommonEcsServiceOptions as h, resolveCfnRegion as hn, buildConnectEvent as ht, addAlbSpecificOptions as i, substituteAgainstState as in, pickResponseTemplate as it, createLocalStartApiCommand as j, AGENTCORE_AGUI_PROTOCOL as jn, A2A_CONTAINER_PORT as jt, createLocalInvokeAgentCoreCommand as k, resolveLambdaArnIntrinsic as kn, matchPreflight as kt, isApplicationLoadBalancer as l, materializeLayerFromArn as ln, probeHostGatewaySupport as lt, MAX_TASKS_SUBNET_RANGE_CAP as m, resolveCfnFallbackRegion as mn, parseConnectionsPath as mt, createLocalListCommand as n, resolveRuntimeImage as nn, buildRestV1Event as nt, createLocalStartAlbCommand as o, substituteEnvVarsFromState as on, tryParseStatus as ot, serviceStrategy as p, rejectExplicitCfnStackWithMultipleStacks as pn, handleConnectionsRequest as pt, buildMethodArn as q, computeCodeImageTag as qt, formatTargetListing as r, EcsTaskResolutionError as rn, evaluateResponseParameters as rt, parseLbPortOverrides as s, substituteEnvVarsFromStateAsync as sn, VtlEvaluationError as st, addListSpecificOptions as t, resolveRuntimeFileExtension as tn, buildHttpApiV2Event as tt, resolveAlbFrontDoor as u, LocalStateSourceError as un, bufferToBody as ut, parseRestartPolicy as v, collectSsmParameterRefs as vn, buildCognitoJwksUrl as vt, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as w, discoverWebSocketApis as wn, attachAuthorizers as wt, buildCloudMapIndex as x, resolveSingleTarget as xn, verifyCognitoJwt as xt, resolveSharedSidecarCredentials as y, resolveSsmParameters as yn, buildJwksUrlFromIssuer as yt, filterRoutesByApiIdentifier as z, formatStateRemedy as zn, AGENTCORE_SIGV4_SERVICE as zt };
26009
+ //# sourceMappingURL=local-list-C-wXKogn.js.map