cdk-local 0.68.0 → 0.69.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
  }
@@ -21404,6 +21408,200 @@ async function rollServiceReplica(args) {
21404
21408
  logger.info(`Rolling replica ${shadow.index} (gen ${shadow.generation}): swap complete; old retired.`);
21405
21409
  }
21406
21410
  /**
21411
+ * Phase 4 of issue #214 — bind-mount source fast path. `docker cp` the
21412
+ * post-synth asset source directory into each essential container of
21413
+ * the live replica, then `docker restart` it. Skips `docker build`,
21414
+ * skips a shadow boot, and keeps the container's network IP / Cloud
21415
+ * Map / front-door pool registrations intact (the registrations key
21416
+ * off the docker-assigned IP and the published host port; `docker
21417
+ * restart` preserves both via the container's stable network
21418
+ * namespace), so NO registry swap is needed.
21419
+ *
21420
+ * Sequence (per replica, sequenced by the rolling loop one at a time
21421
+ * so peer services + the ALB front-door always have at least N-1 live
21422
+ * endpoints across the reload — same zero-connection-refusal guarantee
21423
+ * the Phase 2/3 rebuild pathway makes):
21424
+ * 1. Locate the live replica by {@link oldReplicaIndex}; reject when
21425
+ * shutting down (the next save can roll a clean boot instead).
21426
+ * 2. Pre-restart DRAIN: drop the replica's Cloud Map handles + every
21427
+ * front-door pool entry under its owner key. Both registries are
21428
+ * synchronous Map mutations; once these complete, peer wget +
21429
+ * front-door `next()` calls route to the surviving replicas. The
21430
+ * handle / owner-key snapshots are kept on `instance.*` so the
21431
+ * symmetric re-register step can pick them up (Cloud Map handles
21432
+ * are rebuilt fresh via `publishReplicaToCloudMap`; the docker
21433
+ * network IP is preserved across `docker restart` so the new
21434
+ * handles point at the SAME endpoint).
21435
+ * 3. Set {@link ServiceReplicaInstance.softReloadInProgress} = true
21436
+ * so the watcher's `waitForExitImpl` post-exit branch defers to
21437
+ * the in-flight restart instead of re-bootstrapping the replica
21438
+ * from scratch.
21439
+ * 4. For each essential container in the replica's started set:
21440
+ * a. Resolve the container's image WORKDIR via `docker inspect`
21441
+ * (default `/` when unset — matches Docker's runtime default
21442
+ * for a Dockerfile with no `WORKDIR`).
21443
+ * b. `docker cp <sourceDirToCopy>/. <containerId>:<workdir>/`
21444
+ * — copy the synthesized asset directory's contents into the
21445
+ * container at the WORKDIR. Trailing `/.` is critical: it
21446
+ * copies the SOURCE DIRECTORY'S CONTENTS, not the directory
21447
+ * itself, mirroring `cp -r src/. dst/`.
21448
+ * c. `docker restart <containerId>` — cycle PID 1. Image,
21449
+ * network namespace, and host-port publish are preserved.
21450
+ * 5. {@link waitForReplicaTcpReady} confirms the essential
21451
+ * container's first port accepts a TCP connection.
21452
+ * 6. Post-TCP-ready RE-REGISTER: re-publish Cloud Map handles +
21453
+ * front-door pool entry under the SAME per-replica owner key
21454
+ * prefix used at initial boot, so the registrations remain
21455
+ * idempotent across multiple `--watch` reloads. After this
21456
+ * step, peers + the front-door route to the replica again.
21457
+ * 7. Clear `softReloadInProgress` in a `finally` so the watcher
21458
+ * always exits its defer-loop, even on a docker error.
21459
+ *
21460
+ * Failure modes:
21461
+ * - `docker inspect` / `docker cp` / `docker restart` errors:
21462
+ * surfaced to the caller via a throw. The replica may be in an
21463
+ * inconsistent state (drained from registries + partial cp + a
21464
+ * possibly-crashed PID 1). The caller (`reloadAllServices`) logs
21465
+ * the failure and continues with the remaining replicas; the
21466
+ * drained state is intentionally NOT re-registered on error so
21467
+ * peers + the front-door stop routing to a broken replica until
21468
+ * the next clean save (or `^C` and re-run).
21469
+ * - TCP probe timeout: best-effort warn (mirrors
21470
+ * {@link rollServiceReplica}); the registrations are re-published
21471
+ * anyway because the container IS up — just slow to bind. The
21472
+ * dying-old-handles-AND-fresh-app-not-yet-listening worst case
21473
+ * would otherwise leave the replica drained forever.
21474
+ *
21475
+ * Out of scope for the v1 primitive (deferred follow-ups):
21476
+ * - Per-container WORKDIR caching across multiple essential
21477
+ * containers in the same task. The `docker inspect` call is
21478
+ * ~10ms; not worth the cache invalidation surface for a path
21479
+ * fired ~once per save.
21480
+ * - SIGHUP / `docker exec`-driven in-process reload. Image-specific
21481
+ * (uvicorn / nodemon / etc.). `docker restart` is the universal
21482
+ * primitive; signal-based reload is a future opt-in if the
21483
+ * per-restart latency proves prohibitive.
21484
+ *
21485
+ * @internal — wired only by the emulator's reload pathway.
21486
+ */
21487
+ async function softReloadReplica(args) {
21488
+ const { controller, oldReplicaIndex, newService, sourceDirToCopy } = args;
21489
+ const logger = getLogger().child("ecs-service");
21490
+ const instance = controller.runState.replicas[oldReplicaIndex];
21491
+ if (!instance) throw new EcsServiceRunnerError(`softReloadReplica: no replica at index ${oldReplicaIndex} (replicas=${controller.runState.replicas.length}).`);
21492
+ if (instance.shuttingDown) {
21493
+ 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.`);
21494
+ return;
21495
+ }
21496
+ const essentialContainers = newService.task.containers.filter((c) => c.essential);
21497
+ const containersToCycle = essentialContainers.length > 0 ? essentialContainers : newService.task.containers.length > 0 ? [newService.task.containers[0]] : [];
21498
+ if (containersToCycle.length === 0) throw new EcsServiceRunnerError(`softReloadReplica: service '${newService.serviceLogicalId}' has no containers to cycle.`);
21499
+ const startedById = new Map(instance.state.startedContainers.map((c) => [c.name, c.id]));
21500
+ const targets = [];
21501
+ for (const container of containersToCycle) {
21502
+ const id = startedById.get(container.name);
21503
+ if (!id) throw new EcsServiceRunnerError(`softReloadReplica: replica r${instance.index} has no started container named '${container.name}' (started: ${[...startedById.keys()].join(", ") || "none"}).`);
21504
+ targets.push({
21505
+ name: container.name,
21506
+ id
21507
+ });
21508
+ }
21509
+ const controllerOptions = controller.options;
21510
+ if (controllerOptions.discovery) {
21511
+ for (const handle of instance.cloudMapHandles) try {
21512
+ controllerOptions.discovery.registry.unregister(handle);
21513
+ } catch {}
21514
+ instance.cloudMapHandles = [];
21515
+ }
21516
+ unregisterReplicaFromFrontDoor(instance, controllerOptions.frontDoor);
21517
+ instance.softReloadInProgress = true;
21518
+ try {
21519
+ logger.info(`Soft-reloading replica r${instance.index} (gen ${instance.generation}): docker cp ${sourceDirToCopy} -> ${targets.length} essential container(s); restart.`);
21520
+ for (const target of targets) {
21521
+ let workdir;
21522
+ try {
21523
+ workdir = await dockerInspectWorkdirImpl(target.id) || "/";
21524
+ } catch (err) {
21525
+ 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.`);
21526
+ }
21527
+ const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
21528
+ try {
21529
+ await dockerCpImpl(`${sourceDirToCopy}/.`, `${target.id}:${workdirDest}`);
21530
+ } catch (err) {
21531
+ 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.`);
21532
+ }
21533
+ try {
21534
+ await dockerRestartImpl(target.id);
21535
+ } catch (err) {
21536
+ 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.`);
21537
+ }
21538
+ }
21539
+ await waitForReplicaTcpReady(newService, instance, {
21540
+ timeoutMs: shadowReadyTimeoutMs,
21541
+ intervalMs: shadowReadyIntervalMs,
21542
+ label: `Soft-reloaded replica r${instance.index} (gen ${instance.generation})`
21543
+ });
21544
+ const ownerKeyGenSuffix = instance.generation > 0 ? `:g${instance.generation}` : "";
21545
+ const ownerKeyPrefix = `${newService.serviceLogicalId}:r${instance.index}${ownerKeyGenSuffix}`;
21546
+ if (controllerOptions.discovery) await publishReplicaToCloudMap(newService, instance, controllerOptions.discovery, ownerKeyPrefix);
21547
+ if (controllerOptions.frontDoor) await publishReplicaToFrontDoor(newService, instance, controllerOptions.frontDoor, controllerOptions.taskOptions.containerHost, ownerKeyPrefix);
21548
+ logger.info(`Soft-reloaded replica r${instance.index} (gen ${instance.generation}): restart + TCP-ready probe complete; Cloud Map + front-door re-published.`);
21549
+ } finally {
21550
+ instance.softReloadInProgress = false;
21551
+ }
21552
+ }
21553
+ /**
21554
+ * Production `docker inspect --format {{.Config.WorkingDir}} <id>`
21555
+ * impl. Returns the container's runtime WORKDIR; empty string when
21556
+ * the Dockerfile didn't set one (caller treats empty as `/`, matching
21557
+ * Docker's runtime default).
21558
+ *
21559
+ * Extracted as a test-overridable function so the soft-reload
21560
+ * primitive's unit tests can assert the WORKDIR resolution branch
21561
+ * without standing up a real container.
21562
+ */
21563
+ const defaultDockerInspectWorkdirImpl = async (containerId) => {
21564
+ const { execFile } = await import("node:child_process");
21565
+ const { promisify } = await import("node:util");
21566
+ const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21567
+ const { stdout } = await promisify(execFile)(getDockerCmd(), [
21568
+ "inspect",
21569
+ "--format",
21570
+ "{{.Config.WorkingDir}}",
21571
+ containerId
21572
+ ]);
21573
+ return stdout.trim();
21574
+ };
21575
+ let dockerInspectWorkdirImpl = defaultDockerInspectWorkdirImpl;
21576
+ /**
21577
+ * Production `docker cp <src> <containerId>:<dst>` impl. The source
21578
+ * path's trailing `/.` ensures CONTENTS of the directory are copied
21579
+ * (not the directory itself); the caller is responsible for that
21580
+ * convention.
21581
+ */
21582
+ const defaultDockerCpImpl = async (src, dst) => {
21583
+ const { execFile } = await import("node:child_process");
21584
+ const { promisify } = await import("node:util");
21585
+ const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21586
+ await promisify(execFile)(getDockerCmd(), [
21587
+ "cp",
21588
+ src,
21589
+ dst
21590
+ ], { maxBuffer: 64 * 1024 * 1024 });
21591
+ };
21592
+ let dockerCpImpl = defaultDockerCpImpl;
21593
+ /**
21594
+ * Production `docker restart <id>` impl. Synchronous in docker's
21595
+ * sense — blocks until the container is up again (or fails).
21596
+ */
21597
+ const defaultDockerRestartImpl = async (containerId) => {
21598
+ const { execFile } = await import("node:child_process");
21599
+ const { promisify } = await import("node:util");
21600
+ const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
21601
+ await promisify(execFile)(getDockerCmd(), ["restart", containerId]);
21602
+ };
21603
+ let dockerRestartImpl = defaultDockerRestartImpl;
21604
+ /**
21407
21605
  * Phase 2 of issue #214 — disconnect every container of the dying
21408
21606
  * replica from the shared service network BEFORE `cleanupEcsRun`'s
21409
21607
  * `docker stop → docker rm` sequence. Docker's embedded DNS strips an
@@ -21470,13 +21668,14 @@ let dockerNetworkDisconnectImpl = defaultDockerNetworkDisconnectImpl;
21470
21668
  * injectable via {@link __setTcpProbeImpl} so the rolling-primitive
21471
21669
  * unit test can avoid any real TCP socket.
21472
21670
  */
21473
- async function waitForReplicaTcpReady(service, shadow, opts) {
21671
+ async function waitForReplicaTcpReady(service, replica, opts) {
21474
21672
  const logger = getLogger().child("ecs-service");
21475
- const networkName = shadow.state.network?.networkName;
21673
+ const label = opts.label ?? `Shadow replica r${replica.index} (gen ${replica.generation})`;
21674
+ const networkName = replica.state.network?.networkName;
21476
21675
  if (!networkName) return;
21477
21676
  const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
21478
21677
  if (!essential || essential.portMappings.length === 0) return;
21479
- const started = shadow.state.startedContainers.find((c) => c.name === essential.name);
21678
+ const started = replica.state.startedContainers.find((c) => c.name === essential.name);
21480
21679
  if (!started) return;
21481
21680
  let ip;
21482
21681
  try {
@@ -21484,7 +21683,7 @@ async function waitForReplicaTcpReady(service, shadow, opts) {
21484
21683
  if (!resolved) return;
21485
21684
  ip = resolved;
21486
21685
  } 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.`);
21686
+ logger.warn(`${label}: TCP-ready probe could not resolve docker IP: ${err instanceof Error ? err.message : String(err)}. Proceeding.`);
21488
21687
  return;
21489
21688
  }
21490
21689
  const port = essential.portMappings[0].containerPort;
@@ -21493,14 +21692,14 @@ async function waitForReplicaTcpReady(service, shadow, opts) {
21493
21692
  while (Date.now() < deadline) {
21494
21693
  try {
21495
21694
  await tcpProbeImpl(ip, port);
21496
- logger.debug(`Shadow replica r${shadow.index} (gen ${shadow.generation}): TCP probe ${ip}:${port} accepted; proceeding with swap.`);
21695
+ logger.debug(`${label}: TCP probe ${ip}:${port} accepted.`);
21497
21696
  return;
21498
21697
  } catch (err) {
21499
21698
  lastErr = err instanceof Error ? err.message : String(err);
21500
21699
  }
21501
21700
  await sleep(opts.intervalMs);
21502
21701
  }
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.`);
21702
+ 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
21703
  }
21505
21704
  /**
21506
21705
  * Default TCP-connect probe used by {@link waitForReplicaTcpReady}.
@@ -21549,6 +21748,11 @@ async function watchReplica(service, options, instance, runState) {
21549
21748
  exitCode = -1;
21550
21749
  }
21551
21750
  if (instance.shuttingDown || runState.shuttingDown) return;
21751
+ if (instance.softReloadInProgress) {
21752
+ while (instance.softReloadInProgress && !instance.shuttingDown && !runState.shuttingDown) await sleep(100);
21753
+ if (instance.shuttingDown || runState.shuttingDown) return;
21754
+ continue;
21755
+ }
21552
21756
  logger.warn(`Replica ${instance.index} essential container exited with code ${exitCode} (restartCount=${instance.restartCount}).`);
21553
21757
  const willRestart = shouldRestart(exitCode, options.restartPolicy);
21554
21758
  if (!willRestart || instance.restartCount === 0) await printExitedContainerLogs(instance.index, essentialId, logger);
@@ -21664,6 +21868,152 @@ function sleep(ms) {
21664
21868
  return sleepImpl(ms);
21665
21869
  }
21666
21870
 
21871
+ //#endregion
21872
+ //#region src/local/source-change-classifier.ts
21873
+ /**
21874
+ * Dependency-manifest basenames recognized by the classifier. A change
21875
+ * to any of these forces a rebuild because the running container's
21876
+ * pre-built dependency layer is no longer in sync with the source.
21877
+ *
21878
+ * Coverage: the package managers commonly used inside a Lambda /
21879
+ * container image — Node (pnpm / npm / yarn), Python (pip / poetry /
21880
+ * pipenv), Ruby (bundler), Go (modules), Rust (cargo), Java / Kotlin
21881
+ * (Maven, Gradle). Adding a new ecosystem? Append its lockfile +
21882
+ * manifest here and add a classifier test row.
21883
+ */
21884
+ const REBUILD_TRIGGER_BASENAMES = new Set([
21885
+ "package.json",
21886
+ "package-lock.json",
21887
+ "pnpm-lock.yaml",
21888
+ "yarn.lock",
21889
+ "npm-shrinkwrap.json",
21890
+ "requirements.txt",
21891
+ "requirements-dev.txt",
21892
+ "pyproject.toml",
21893
+ "poetry.lock",
21894
+ "Pipfile",
21895
+ "Pipfile.lock",
21896
+ "uv.lock",
21897
+ "Gemfile",
21898
+ "Gemfile.lock",
21899
+ "go.mod",
21900
+ "go.sum",
21901
+ "Cargo.toml",
21902
+ "Cargo.lock",
21903
+ "pom.xml",
21904
+ "build.gradle",
21905
+ "build.gradle.kts",
21906
+ "settings.gradle",
21907
+ "settings.gradle.kts",
21908
+ "Makefile",
21909
+ "CMakeLists.txt"
21910
+ ]);
21911
+ /**
21912
+ * Compiled-language source extensions that require a build step
21913
+ * inside `docker build` (typically a `RUN go build` / `cargo build` /
21914
+ * `mvn package` etc.). A copy of the source alone would leave the
21915
+ * running binary stale, so the user's intent must be a rebuild.
21916
+ *
21917
+ * Interpreted-language runtimes (Node — `.js` / `.mjs` / `.cjs` /
21918
+ * `.ts` when transpiled at runtime, Python — `.py`, Ruby — `.rb`,
21919
+ * shell — `.sh`) read source at process start, so a `docker cp` +
21920
+ * `docker restart` cycle picks them up. Those extensions are
21921
+ * NOT in this set.
21922
+ */
21923
+ const COMPILED_LANGUAGE_EXTENSIONS = new Set([
21924
+ ".go",
21925
+ ".rs",
21926
+ ".java",
21927
+ ".kt",
21928
+ ".kts",
21929
+ ".scala",
21930
+ ".cs",
21931
+ ".swift",
21932
+ ".fs",
21933
+ ".fsx",
21934
+ ".c",
21935
+ ".cc",
21936
+ ".cpp",
21937
+ ".cxx",
21938
+ ".h",
21939
+ ".hpp",
21940
+ ".zig",
21941
+ ".ml",
21942
+ ".mli",
21943
+ ".elm",
21944
+ ".hs",
21945
+ ".dart"
21946
+ ]);
21947
+ /**
21948
+ * Classify a single watcher firing into rebuild vs soft-reload. Pure
21949
+ * + synchronous. The caller (emulator's reload pathway) invokes this
21950
+ * once per target per firing AFTER `cdk synth` has run and the new
21951
+ * asset manifest is on disk.
21952
+ *
21953
+ * Branching:
21954
+ * 1. No asset context (image isn't a CDK asset, or asset lookup
21955
+ * failed) → `rebuild`.
21956
+ * 2. The asset hash didn't change between old and new synths
21957
+ * (`oldAssetHash === newAssetHash`, or `oldAssetHash` missing)
21958
+ * → `rebuild`. Load-bearing guard for "user edited a CDK
21959
+ * construct file (e.g. `lib/stack.ts`) that flipped the task
21960
+ * spec but didn't touch the asset content". Soft-reload would
21961
+ * `docker cp` identical files and `docker restart` the
21962
+ * container with the OLD task spec (env / memory / mounts /
21963
+ * added sidecars are set at `docker create` time, not on
21964
+ * restart) — the user's intent would silently NOT apply.
21965
+ * Forcing rebuild keeps Phase 1-3 semantics exactly for this
21966
+ * case: the rolling primitive boots a shadow with the new task
21967
+ * spec, the user sees their construct edit take effect.
21968
+ * 3. No changed paths (the watcher fired on a debounce flush with
21969
+ * an empty pending set — shouldn't happen in practice, but
21970
+ * defensive) → `rebuild`.
21971
+ * 4. Any changed path's basename matches the Dockerfile or a
21972
+ * dependency manifest → `rebuild`.
21973
+ * 5. Any changed path's extension is a compiled-language source →
21974
+ * `rebuild`.
21975
+ * 6. Else → `soft-reload`.
21976
+ */
21977
+ function classifySourceChange(changedPaths, ctx) {
21978
+ if (!ctx) return {
21979
+ kind: "rebuild",
21980
+ reason: "target image is not a CDK docker-image asset"
21981
+ };
21982
+ if (!ctx.oldAssetHash || ctx.oldAssetHash === ctx.newAssetHash) return {
21983
+ kind: "rebuild",
21984
+ 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"
21985
+ };
21986
+ if (changedPaths.length === 0) return {
21987
+ kind: "rebuild",
21988
+ reason: "no changed paths reported (defensive default)"
21989
+ };
21990
+ for (const p of changedPaths) {
21991
+ const basename = path.basename(p);
21992
+ if (basename === ctx.dockerFile) return {
21993
+ kind: "rebuild",
21994
+ reason: `Dockerfile edit (${basename})`
21995
+ };
21996
+ if (basename.startsWith("Dockerfile.")) return {
21997
+ kind: "rebuild",
21998
+ reason: `Dockerfile.* edit (${basename})`
21999
+ };
22000
+ if (REBUILD_TRIGGER_BASENAMES.has(basename)) return {
22001
+ kind: "rebuild",
22002
+ reason: `dependency manifest edit (${basename})`
22003
+ };
22004
+ const ext = path.extname(p).toLowerCase();
22005
+ if (COMPILED_LANGUAGE_EXTENSIONS.has(ext)) return {
22006
+ kind: "rebuild",
22007
+ reason: `compiled-language source edit (${basename}) — soft-reload would leave the built binary stale`
22008
+ };
22009
+ }
22010
+ return {
22011
+ kind: "soft-reload",
22012
+ reason: `${changedPaths.length} source-only path(s) — skipping rebuild`,
22013
+ newAssetSourceDir: ctx.newAssetSourceDir
22014
+ };
22015
+ }
22016
+
21667
22017
  //#endregion
21668
22018
  //#region src/local/cloud-map-registry.ts
21669
22019
  /**
@@ -23319,9 +23669,9 @@ function materializeInlineCode(handler, source, fileExtension) {
23319
23669
  const lastDot = handler.lastIndexOf(".");
23320
23670
  if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
23321
23671
  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 });
23672
+ const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-alb-lambda-`));
23673
+ const filePath = path$1.join(dir, `${modulePath}${fileExtension}`);
23674
+ mkdirSync(path$1.dirname(filePath), { recursive: true });
23325
23675
  writeFileSync(filePath, source, "utf-8");
23326
23676
  return dir;
23327
23677
  }
@@ -23353,7 +23703,7 @@ async function resolveContainerImagePlan(lambda, opts) {
23353
23703
  let imageRef;
23354
23704
  let localBuilt = false;
23355
23705
  if (manifestPath) {
23356
- const cdkOutDir = path.dirname(manifestPath);
23706
+ const cdkOutDir = path$1.dirname(manifestPath);
23357
23707
  const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
23358
23708
  const entry = manifest ? getDockerImageBySourceHash(manifest, lambda.imageUri) : void 0;
23359
23709
  if (entry) {
@@ -23719,8 +24069,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
23719
24069
  paths: [watchRoot],
23720
24070
  ignored,
23721
24071
  shouldTrigger,
23722
- onChange: () => {
23723
- logger.info("Detected source change; reloading service(s)...");
24072
+ onChange: (changedPaths) => {
24073
+ logger.info(`Detected source change (${changedPaths.length} path(s)); reloading service(s)...`);
23724
24074
  reloadChain = reloadChain.then(() => reloadAllServices({
23725
24075
  perTarget,
23726
24076
  synthesizer,
@@ -23734,6 +24084,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
23734
24084
  extraStateProviders,
23735
24085
  profileCredsFile,
23736
24086
  frontDoorByService,
24087
+ changedPaths,
23737
24088
  logger
23738
24089
  })).catch((err) => {
23739
24090
  logger.error(`reloadAllServices threw: ${err instanceof Error ? err.message : String(err)}`);
@@ -23752,11 +24103,22 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
23752
24103
  }
23753
24104
  }
23754
24105
  /**
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:
24106
+ * Phase 2 + Phase 4 of issue #214 — multi-replica reload cycle for
24107
+ * `cdkl start-service --watch` (Phase 2) and `cdkl start-alb --watch`
24108
+ * (Phase 3 wires the same loop). Mirrors start-api's `reloadAllServers`
24109
+ * shape but per-ECS-service. Per-target verdict from
24110
+ * {@link classifySourceChange} (Phase 4) picks the per-replica action:
24111
+ *
24112
+ * - `'soft-reload'` → {@link softReloadReplica} runs `docker cp`
24113
+ * + `docker restart` against the live replica. No `docker build`,
24114
+ * no shadow boot, no Cloud Map / front-door pool swap — the
24115
+ * container's IP + host port are preserved across the restart, so
24116
+ * existing registrations stay valid. Fast path (~sub-second per
24117
+ * replica for typical interpreted-language handlers).
24118
+ * - `'rebuild'` → {@link rollServiceReplica}, the Phase 1-3 path,
24119
+ * replacing Phase 1's "tear single replica down, boot fresh"
24120
+ * sequence with a per-replica rolling loop so the service stays
24121
+ * available end-to-end:
23760
24122
  *
23761
24123
  * 1. Re-runs `synthesizer.synthesize(synthOpts)` once (failure → warn
23762
24124
  * + keep every replica serving).
@@ -23791,7 +24153,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
23791
24153
  * via the logger so the user can fix the source + save again.
23792
24154
  */
23793
24155
  async function reloadAllServices(args) {
23794
- const { perTarget, synthesizer, synthOpts, strategy, resolvedTargets, cloudMapIndexByStack, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService, logger } = args;
24156
+ const { perTarget, synthesizer, synthOpts, strategy, resolvedTargets, cloudMapIndexByStack, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService, changedPaths, logger } = args;
23795
24157
  let stacks;
23796
24158
  try {
23797
24159
  ({stacks} = await synthesizer.synthesize(synthOpts));
@@ -23808,6 +24170,8 @@ async function reloadAllServices(args) {
23808
24170
  cloudMapIndexByStack.set(stack.stackName, index);
23809
24171
  for (const w of index.warnings) logger.warn(w);
23810
24172
  }
24173
+ const cdkOutDir = options.output;
24174
+ const assetLoader = new AssetManifestLoader();
23811
24175
  for (const pt of perTarget) {
23812
24176
  const newBoot = newBootByTarget.get(pt.boot.target);
23813
24177
  if (!newBoot) {
@@ -23819,6 +24183,27 @@ async function reloadAllServices(args) {
23819
24183
  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
24184
  continue;
23821
24185
  }
24186
+ let verdict = {
24187
+ kind: "rebuild",
24188
+ reason: "classifier not consulted"
24189
+ };
24190
+ try {
24191
+ verdict = classifySourceChange(changedPaths, await loadAssetContextForTarget({
24192
+ target: newBoot.target,
24193
+ controller,
24194
+ stacks,
24195
+ cdkOutDir,
24196
+ assetLoader,
24197
+ logger
24198
+ }));
24199
+ logger.info(`Reload of '${newBoot.target}': verdict=${verdict.kind} (${verdict.reason}).`);
24200
+ } catch (err) {
24201
+ logger.warn(`Reload of '${newBoot.target}': classifier context unavailable (${err instanceof Error ? err.message : String(err)}); falling back to rebuild.`);
24202
+ verdict = {
24203
+ kind: "rebuild",
24204
+ reason: "classifier context unavailable; falling back to rebuild"
24205
+ };
24206
+ }
23822
24207
  await rollOneTarget({
23823
24208
  controller,
23824
24209
  newBoot,
@@ -23830,12 +24215,62 @@ async function reloadAllServices(args) {
23830
24215
  profileCredsFile,
23831
24216
  frontDoorPools: frontDoorByService.get(newBoot.target),
23832
24217
  suppressLoadBalancerWarning: strategy.suppressLoadBalancerWarning === true,
24218
+ verdict,
23833
24219
  logger
23834
24220
  });
23835
24221
  }
23836
24222
  logger.info("Reload complete.");
23837
24223
  }
23838
24224
  /**
24225
+ * Phase 4 of issue #214 — load the per-target asset context the
24226
+ * source-change classifier consumes. Resolves the target's docker-image
24227
+ * asset hash via the freshly-synthed `<stackName>.assets.json` and
24228
+ * derives the staged source directory + Dockerfile basename.
24229
+ *
24230
+ * Returns `undefined` (and logs at debug) when:
24231
+ * - The target's image isn't a CDK docker-image asset (ECR / public
24232
+ * registry pin). The classifier treats `undefined` as `rebuild`
24233
+ * because there's no local source tree to copy.
24234
+ * - The asset manifest can't be loaded (stack not synthed yet, file
24235
+ * missing). Same treatment — defensive default to `rebuild`.
24236
+ * - The asset hash isn't in the manifest's `dockerImages`. Same.
24237
+ *
24238
+ * Throws on a malformed manifest (parse failure surfaced via
24239
+ * {@link AssetManifestLoader}) so the caller can fall back to rebuild
24240
+ * with a warn line that explains why the classifier couldn't run.
24241
+ */
24242
+ async function loadAssetContextForTarget(args) {
24243
+ const { target, controller, stacks, cdkOutDir, assetLoader, logger } = args;
24244
+ const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
24245
+ if (!candidate) return void 0;
24246
+ let newService;
24247
+ try {
24248
+ newService = resolveEcsServiceTarget(target, stacks, void 0, { suppressLoadBalancerWarning: true });
24249
+ } catch (err) {
24250
+ 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).`);
24251
+ return;
24252
+ }
24253
+ const essential = newService.task.containers.find((c) => c.essential) ?? newService.task.containers[0];
24254
+ if (!essential) return void 0;
24255
+ if (essential.image.kind !== "cdk-asset" || !essential.image.assetHash) return;
24256
+ const newAssetHash = essential.image.assetHash;
24257
+ const manifest = await assetLoader.loadManifest(cdkOutDir, candidate.stackName);
24258
+ if (!manifest) return void 0;
24259
+ const newDockerImage = manifest.dockerImages?.[newAssetHash];
24260
+ if (!newDockerImage) return void 0;
24261
+ if (!newDockerImage.source.directory) return;
24262
+ const newAssetSourceDir = path.resolve(cdkOutDir, newDockerImage.source.directory);
24263
+ let oldAssetHash;
24264
+ const oldEssential = controller.service.task.containers.find((c) => c.essential) ?? controller.service.task.containers[0];
24265
+ if (oldEssential?.image.kind === "cdk-asset") oldAssetHash = oldEssential.image.assetHash;
24266
+ return {
24267
+ ...oldAssetHash !== void 0 && { oldAssetHash },
24268
+ newAssetHash,
24269
+ newAssetSourceDir,
24270
+ dockerFile: path.basename(newDockerImage.source.dockerFile ?? "Dockerfile")
24271
+ };
24272
+ }
24273
+ /**
23839
24274
  * Phase 2 of issue #214 — roll every replica of one target through the
23840
24275
  * new task descriptor sequentially. Extracted from {@link reloadAllServices}
23841
24276
  * so the per-target try/catch logic (synth-failure / resolve-failure /
@@ -23846,7 +24281,7 @@ async function reloadAllServices(args) {
23846
24281
  * even when the resolve / roll throws.
23847
24282
  */
23848
24283
  async function rollOneTarget(args) {
23849
- const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, logger } = args;
24284
+ const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, verdict, logger } = args;
23850
24285
  const candidate = pickCandidateStack(parseEcsTarget(newBoot.target).stackPattern, stacks);
23851
24286
  const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
23852
24287
  try {
@@ -23865,7 +24300,8 @@ async function rollOneTarget(args) {
23865
24300
  return;
23866
24301
  }
23867
24302
  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).`);
24303
+ 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).`);
24304
+ else logger.info(`Reload of '${newBoot.target}': rolling ${oldReplicas.length} replica(s) one at a time (start new shadow → swap registrations → stop old).`);
23869
24305
  for (const oldInstance of oldReplicas) {
23870
24306
  const idx = controller.runState.replicas.indexOf(oldInstance);
23871
24307
  if (idx === -1) {
@@ -23873,7 +24309,13 @@ async function rollOneTarget(args) {
23873
24309
  continue;
23874
24310
  }
23875
24311
  try {
23876
- await rollServiceReplica({
24312
+ if (verdict.kind === "soft-reload") await softReloadReplica({
24313
+ controller,
24314
+ oldReplicaIndex: idx,
24315
+ newService,
24316
+ sourceDirToCopy: verdict.newAssetSourceDir
24317
+ });
24318
+ else await rollServiceReplica({
23877
24319
  controller,
23878
24320
  oldReplicaIndex: idx,
23879
24321
  newService,
@@ -24458,7 +24900,7 @@ function createLocalStartServiceCommand(opts = {}) {
24458
24900
  * for that contract to live.
24459
24901
  */
24460
24902
  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));
24903
+ 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
24904
  }
24463
24905
 
24464
24906
  //#endregion
@@ -25311,7 +25753,7 @@ function createLocalStartAlbCommand(opts = {}) {
25311
25753
  * clusters. Chainable: returns `cmd`.
25312
25754
  */
25313
25755
  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));
25756
+ 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
25757
  }
25316
25758
 
25317
25759
  //#endregion
@@ -25407,5 +25849,5 @@ function addListSpecificOptions(cmd) {
25407
25849
  }
25408
25850
 
25409
25851
  //#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
25852
+ export { applyAuthorizerOverlay as $, resolveRuntimeCodeMountPath as $t, createLocalStartApiCommand as A, AGENTCORE_AGUI_PROTOCOL as An, A2A_CONTAINER_PORT as At, groupRoutesByServer as B, tryResolveImageFnJoin as Bn, AGENTCORE_SESSION_ID_HEADER as Bt, classifySourceChange as C, discoverWebSocketApis as Cn, attachAuthorizers as Ct, addInvokeAgentCoreSpecificOptions as D, pickRefLogicalId as Dn, isFunctionUrlOacFronted as Dt, createLocalRunTaskCommand as E, discoverRoutes as En, buildCorsConfigFromCloudFrontChain as Et, attachStageContext as F, pickAgentCoreCandidateStack as Fn, MCP_PROTOCOL_VERSION as Ft, defaultCredentialsLoader as G, buildAgentCoreCodeImage as Gt, startApiServer as H, waitForAgentCorePing as Ht, buildStageMap as I, resolveAgentCoreTarget as In, mcpInvokeOnce as It, evaluateCachedLambdaPolicy as J, toCmdArgv as Jt, buildMethodArn as K, computeCodeImageTag as Kt, availableApiIdentifiers as L, derivePseudoParametersFromRegion as Ln, parseSseForJsonRpc as Lt, resolveApiTargetSubset as M, AGENTCORE_MCP_PROTOCOL as Mn, a2aInvokeOnce as Mt, createAuthorizerCache as N, AGENTCORE_RUNTIME_TYPE as Nn, MCP_CONTAINER_PORT as Nt, createLocalInvokeAgentCoreCommand as O, resolveLambdaArnIntrinsic as On, matchPreflight as Ot, createFileWatcher as P, AgentCoreResolutionError as Pn, MCP_PATH as Pt, translateLambdaResponse as Q, buildContainerImage as Qt, filterRoutesByApiIdentifier as R, formatStateRemedy as Rn, AGENTCORE_SIGV4_SERVICE as Rt, CloudMapRegistry as S, listTargets as Sn, verifyJwtViaDiscovery as St, addRunTaskSpecificOptions as T, parseSelectionExpressionPath as Tn, buildCorsConfigByApiId as Tt, resolveSelectionExpression as U, downloadAndExtractS3Bundle as Ut, readMtlsMaterialsFromDisk as V, LocalInvokeBuildError as Vn, invokeAgentCore as Vt, resolveServiceIntegrationParameters as W, SUPPORTED_CODE_RUNTIMES as Wt, invokeTokenAuthorizer as X, createLocalInvokeCommand as Xt, invokeRequestAuthorizer as Y, addInvokeSpecificOptions as Yt, matchRoute as Z, architectureToPlatform as Zt, parseMaxTasks as _, collectSsmParameterRefs as _n, buildCognitoJwksUrl as _t, albStrategy as a, substituteEnvVarsFromState as an, tryParseStatus as at, runEcsServiceEmulator as b, resolveSingleTarget as bn, verifyCognitoJwt as bt, resolveAlbTarget as c, materializeLayerFromArn as cn, probeHostGatewaySupport as ct, addStartServiceSpecificOptions as d, isCfnFlagPresent as dn, buildMgmtEndpointEnvUrl as dt, resolveRuntimeFileExtension as en, buildHttpApiV2Event as et, createLocalStartServiceCommand as f, rejectExplicitCfnStackWithMultipleStacks as fn, handleConnectionsRequest as ft, buildEcsImageResolutionContext as g, CfnLocalStateProvider as gn, buildMessageEvent as gt, addCommonEcsServiceOptions as h, resolveCfnStackName as hn, buildDisconnectEvent as ht, addAlbSpecificOptions as i, substituteAgainstStateAsync as in, selectIntegrationResponse as it, createWatchPredicates as j, AGENTCORE_HTTP_PROTOCOL as jn, A2A_PATH as jt, addStartApiSpecificOptions as k, AGENTCORE_A2A_PROTOCOL as kn, invokeAgentCoreWs as kt, isApplicationLoadBalancer as l, LocalStateSourceError as ln, bufferToBody as lt, MAX_TASKS_SUBNET_RANGE_CAP as m, resolveCfnRegion as mn, buildConnectEvent as mt, createLocalListCommand as n, EcsTaskResolutionError as nn, evaluateResponseParameters as nt, createLocalStartAlbCommand as o, substituteEnvVarsFromStateAsync as on, VtlEvaluationError as ot, serviceStrategy as p, resolveCfnFallbackRegion as pn, parseConnectionsPath as pt, computeRequestIdentityHash as q, renderCodeDockerfile as qt, formatTargetListing as r, substituteAgainstState as rn, pickResponseTemplate as rt, parseLbPortOverrides as s, resolveEnvVars as sn, HOST_GATEWAY_MIN_VERSION as st, addListSpecificOptions as t, resolveRuntimeImage as tn, buildRestV1Event as tt, resolveAlbFrontDoor as u, createLocalStateProvider as un, ConnectionRegistry as ut, parseRestartPolicy as v, resolveSsmParameters as vn, buildJwksUrlFromIssuer as vt, getContainerNetworkIp as w, discoverWebSocketApisOrThrow as wn, applyCorsResponseHeaders as wt, buildCloudMapIndex as x, countTargets as xn, verifyJwtAuthorizer as xt, resolveSharedSidecarCredentials as y, resolveWatchConfig as yn, createJwksCache as yt, filterRoutesByApiIdentifiers as z, substituteImagePlaceholders as zn, signAgentCoreInvocation as zt };
25853
+ //# sourceMappingURL=local-list-l2_7oGHF.js.map