@go-to-k/cdkd 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -12448,7 +12448,7 @@ var require_graphql = __commonJS({
12448
12448
  var _validate2 = require_validate2();
12449
12449
  var _execute = require_execute();
12450
12450
  function graphql(args) {
12451
- return new Promise((resolve8) => resolve8(graphqlImpl(args)));
12451
+ return new Promise((resolve9) => resolve9(graphqlImpl(args)));
12452
12452
  }
12453
12453
  function graphqlSync(args) {
12454
12454
  const result = graphqlImpl(args);
@@ -17797,7 +17797,7 @@ var require_graphql2 = __commonJS({
17797
17797
  });
17798
17798
 
17799
17799
  // src/cli/index.ts
17800
- import { Command as Command16 } from "commander";
17800
+ import { Command as Command17 } from "commander";
17801
17801
 
17802
17802
  // src/cli/commands/bootstrap.ts
17803
17803
  import { Command, Option as Option2 } from "commander";
@@ -18869,7 +18869,7 @@ var IntrinsicFunctionResolver = class {
18869
18869
  this.logger.debug(
18870
18870
  `VPC ${physicalId} IPv6 CIDR still associating (attempt ${attempt}/${maxAttempts}), waiting...`
18871
18871
  );
18872
- await new Promise((resolve8) => setTimeout(resolve8, 2e3));
18872
+ await new Promise((resolve9) => setTimeout(resolve9, 2e3));
18873
18873
  }
18874
18874
  this.logger.warn(
18875
18875
  `VPC ${physicalId} IPv6 CIDR did not reach 'associated' state after ${maxAttempts} attempts`
@@ -19754,7 +19754,7 @@ var DagExecutor = class {
19754
19754
  async execute(concurrency, fn, cancelled = () => false) {
19755
19755
  let active = 0;
19756
19756
  const errors = [];
19757
- return new Promise((resolve8, reject) => {
19757
+ return new Promise((resolve9, reject) => {
19758
19758
  const dispatch = () => {
19759
19759
  let changed = true;
19760
19760
  while (changed) {
@@ -19816,7 +19816,7 @@ var DagExecutor = class {
19816
19816
  );
19817
19817
  return;
19818
19818
  }
19819
- resolve8();
19819
+ resolve9();
19820
19820
  }
19821
19821
  };
19822
19822
  dispatch();
@@ -20497,7 +20497,7 @@ Error: ${err.message || "Unknown error"}`,
20497
20497
  * Sleep for specified milliseconds
20498
20498
  */
20499
20499
  sleep(ms) {
20500
- return new Promise((resolve8) => setTimeout(resolve8, ms));
20500
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
20501
20501
  }
20502
20502
  /**
20503
20503
  * Check if a resource type is supported by Cloud Control API
@@ -21231,7 +21231,7 @@ var CustomResourceProvider = class _CustomResourceProvider {
21231
21231
  return result;
21232
21232
  }
21233
21233
  sleep(ms) {
21234
- return new Promise((resolve8) => setTimeout(resolve8, ms));
21234
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
21235
21235
  }
21236
21236
  /**
21237
21237
  * Adopt an existing custom resource into cdkd state.
@@ -21661,12 +21661,12 @@ function isRetryableTransientError(error, message) {
21661
21661
  }
21662
21662
 
21663
21663
  // src/deployment/retry.ts
21664
- var defaultSleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
21664
+ var defaultSleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
21665
21665
  async function withRetry(operation, logicalId, opts = {}) {
21666
21666
  const maxRetries = opts.maxRetries ?? 8;
21667
21667
  const initialDelayMs = opts.initialDelayMs ?? 1e3;
21668
21668
  const maxDelayMs = opts.maxDelayMs ?? 8e3;
21669
- const sleep = opts.sleep ?? defaultSleep;
21669
+ const sleep2 = opts.sleep ?? defaultSleep;
21670
21670
  let lastError;
21671
21671
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
21672
21672
  try {
@@ -21686,7 +21686,7 @@ async function withRetry(operation, logicalId, opts = {}) {
21686
21686
  if (opts.isInterrupted?.()) {
21687
21687
  throw opts.onInterrupted ? opts.onInterrupted() : new Error("Interrupted");
21688
21688
  }
21689
- await sleep(Math.min(1e3, delay2 - waited));
21689
+ await sleep2(Math.min(1e3, delay2 - waited));
21690
21690
  }
21691
21691
  }
21692
21692
  }
@@ -21716,7 +21716,7 @@ function validateOptions(opts) {
21716
21716
  async function withResourceDeadline(operation, opts) {
21717
21717
  validateOptions(opts);
21718
21718
  const startedAt = Date.now();
21719
- return new Promise((resolve8, reject) => {
21719
+ return new Promise((resolve9, reject) => {
21720
21720
  let settled = false;
21721
21721
  let warnTimer;
21722
21722
  let timeoutTimer;
@@ -21756,7 +21756,7 @@ async function withResourceDeadline(operation, opts) {
21756
21756
  return;
21757
21757
  settled = true;
21758
21758
  cleanup();
21759
- resolve8(value);
21759
+ resolve9(value);
21760
21760
  },
21761
21761
  (err) => {
21762
21762
  if (settled)
@@ -23791,7 +23791,7 @@ var AppExecutor = class {
23791
23791
  * Spawn subprocess and wait for completion
23792
23792
  */
23793
23793
  spawn(commandLine, env) {
23794
- return new Promise((resolve8, reject) => {
23794
+ return new Promise((resolve9, reject) => {
23795
23795
  const proc = spawn(commandLine, {
23796
23796
  stdio: ["ignore", "pipe", "pipe"],
23797
23797
  shell: true,
@@ -23817,7 +23817,7 @@ var AppExecutor = class {
23817
23817
  });
23818
23818
  proc.on("close", (code) => {
23819
23819
  if (code === 0) {
23820
- resolve8();
23820
+ resolve9();
23821
23821
  } else {
23822
23822
  const stderr = stderrChunks.join("\n");
23823
23823
  reject(
@@ -25339,11 +25339,11 @@ var FileAssetPublisher = class {
25339
25339
  */
25340
25340
  async uploadZip(client, dirPath, bucket, key) {
25341
25341
  const archiver = await import("archiver");
25342
- const body = await new Promise((resolve8, reject) => {
25342
+ const body = await new Promise((resolve9, reject) => {
25343
25343
  const chunks = [];
25344
25344
  const archive = archiver.default("zip", { zlib: { level: 9 } });
25345
25345
  archive.on("data", (chunk) => chunks.push(chunk));
25346
- archive.on("end", () => resolve8(Buffer.concat(chunks)));
25346
+ archive.on("end", () => resolve9(Buffer.concat(chunks)));
25347
25347
  archive.on("error", reject);
25348
25348
  const stat4 = statSync2(dirPath);
25349
25349
  if (stat4.isDirectory()) {
@@ -25528,7 +25528,7 @@ var DockerAssetPublisher = class {
25528
25528
  const token = Buffer.from(authData.authorizationToken, "base64").toString();
25529
25529
  const [username, password] = token.split(":");
25530
25530
  const endpoint = authData.proxyEndpoint || `https://${accountId}.dkr.ecr.${region}.amazonaws.com`;
25531
- await new Promise((resolve8, reject) => {
25531
+ await new Promise((resolve9, reject) => {
25532
25532
  const proc = spawn2(
25533
25533
  "docker",
25534
25534
  ["login", "--username", username, "--password-stdin", endpoint],
@@ -25542,7 +25542,7 @@ var DockerAssetPublisher = class {
25542
25542
  });
25543
25543
  proc.on("close", (code) => {
25544
25544
  if (code === 0) {
25545
- resolve8();
25545
+ resolve9();
25546
25546
  } else {
25547
25547
  reject(new AssetError(`ECR login failed: ${stderr.trim()}`));
25548
25548
  }
@@ -25595,7 +25595,7 @@ var WorkGraph = class {
25595
25595
  async execute(concurrency, fn) {
25596
25596
  const active = { "asset-build": 0, "asset-publish": 0, stack: 0 };
25597
25597
  const errors = [];
25598
- return new Promise((resolve8, reject) => {
25598
+ return new Promise((resolve9, reject) => {
25599
25599
  const dispatch = () => {
25600
25600
  const ready = [];
25601
25601
  for (const node of this.nodes.values()) {
@@ -25667,7 +25667,7 @@ ${msg}`
25667
25667
  );
25668
25668
  return;
25669
25669
  }
25670
- resolve8();
25670
+ resolve9();
25671
25671
  }
25672
25672
  };
25673
25673
  dispatch();
@@ -26658,7 +26658,7 @@ var LockManager = class {
26658
26658
  this.logger.info(
26659
26659
  `Stack '${stackName}' (${region}) is locked by ${lockInfo2.owner}${lockInfo2.operation ? ` (operation: ${lockInfo2.operation})` : ""}. Lock expires in ${this.formatDuration(remainingMs)}. Retrying in ${this.formatDuration(retryDelay)}... (attempt ${attempt + 1}/${maxRetries})`
26660
26660
  );
26661
- await new Promise((resolve8) => setTimeout(resolve8, retryDelay));
26661
+ await new Promise((resolve9) => setTimeout(resolve9, retryDelay));
26662
26662
  continue;
26663
26663
  }
26664
26664
  }
@@ -35152,7 +35152,7 @@ var LambdaFunctionProvider = class {
35152
35152
  return enis;
35153
35153
  }
35154
35154
  sleep(ms) {
35155
- return new Promise((resolve8) => setTimeout(resolve8, ms));
35155
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
35156
35156
  }
35157
35157
  /**
35158
35158
  * Build Lambda Code parameter from CDK properties
@@ -37029,7 +37029,7 @@ var DynamoDBTableProvider = class {
37029
37029
  if (status !== "CREATING") {
37030
37030
  throw new Error(`Unexpected table status: ${status}`);
37031
37031
  }
37032
- await new Promise((resolve8) => setTimeout(resolve8, 1e3));
37032
+ await new Promise((resolve9) => setTimeout(resolve9, 1e3));
37033
37033
  }
37034
37034
  throw new Error(`Table ${tableName} did not reach ACTIVE status within ${maxAttempts} seconds`);
37035
37035
  }
@@ -37049,7 +37049,7 @@ var DynamoDBTableProvider = class {
37049
37049
  if (status === "ACTIVE") {
37050
37050
  return;
37051
37051
  }
37052
- await new Promise((resolve8) => setTimeout(resolve8, 1e3));
37052
+ await new Promise((resolve9) => setTimeout(resolve9, 1e3));
37053
37053
  }
37054
37054
  throw new Error(
37055
37055
  `Table ${tableName} did not reach ACTIVE status within ${maxAttempts} seconds after UpdateTable`
@@ -40229,7 +40229,7 @@ var EC2Provider = class {
40229
40229
  this.logger.debug(
40230
40230
  `VPC ${physicalId} has dependencies (attempt ${attempt}/${maxAttempts}), retrying in ${attempt * 5}s...`
40231
40231
  );
40232
- await new Promise((resolve8) => setTimeout(resolve8, attempt * 5e3));
40232
+ await new Promise((resolve9) => setTimeout(resolve9, attempt * 5e3));
40233
40233
  continue;
40234
40234
  }
40235
40235
  const cause = error instanceof Error ? error : void 0;
@@ -40393,7 +40393,7 @@ var EC2Provider = class {
40393
40393
  this.logger.debug(
40394
40394
  `Subnet ${physicalId} has dependencies (attempt ${attempt}/${maxAttempts}), retrying in ${attempt * 5}s...`
40395
40395
  );
40396
- await new Promise((resolve8) => setTimeout(resolve8, attempt * 5e3));
40396
+ await new Promise((resolve9) => setTimeout(resolve9, attempt * 5e3));
40397
40397
  continue;
40398
40398
  }
40399
40399
  const cause = error instanceof Error ? error : void 0;
@@ -41151,7 +41151,7 @@ var EC2Provider = class {
41151
41151
  this.logger.debug(
41152
41152
  `SecurityGroup ${physicalId} has dependent objects (attempt ${attempt}/${maxAttempts}), retrying in ${attempt * 5}s...`
41153
41153
  );
41154
- await new Promise((resolve8) => setTimeout(resolve8, attempt * 5e3));
41154
+ await new Promise((resolve9) => setTimeout(resolve9, attempt * 5e3));
41155
41155
  continue;
41156
41156
  }
41157
41157
  const cause = error instanceof Error ? error : void 0;
@@ -42095,7 +42095,7 @@ var EC2Provider = class {
42095
42095
  * `setTimeout` globally.
42096
42096
  */
42097
42097
  sleep(ms) {
42098
- return new Promise((resolve8) => setTimeout(resolve8, ms));
42098
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
42099
42099
  }
42100
42100
  /**
42101
42101
  * Check if an error indicates the resource was not found
@@ -44195,7 +44195,7 @@ var ApiGatewayProvider = class _ApiGatewayProvider {
44195
44195
  * Sleep for specified milliseconds
44196
44196
  */
44197
44197
  sleep(ms) {
44198
- return new Promise((resolve8) => setTimeout(resolve8, ms));
44198
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
44199
44199
  }
44200
44200
  /**
44201
44201
  * Convert CloudFormation Tags (Array<{Key, Value}>) to SDK tags (Record<string, string>).
@@ -44850,8 +44850,8 @@ var ApiGatewayV2Provider = class {
44850
44850
  async createRoute(logicalId, resourceType, properties) {
44851
44851
  this.logger.debug(`Creating API Gateway V2 Route ${logicalId}`);
44852
44852
  const apiId = properties["ApiId"];
44853
- const routeKey2 = properties["RouteKey"];
44854
- if (!apiId || !routeKey2) {
44853
+ const routeKey = properties["RouteKey"];
44854
+ if (!apiId || !routeKey) {
44855
44855
  throw new ProvisioningError(
44856
44856
  `ApiId and RouteKey are required for API Gateway V2 Route ${logicalId}`,
44857
44857
  resourceType,
@@ -44862,7 +44862,7 @@ var ApiGatewayV2Provider = class {
44862
44862
  const response = await this.getClient().send(
44863
44863
  new CreateRouteCommand2({
44864
44864
  ApiId: apiId,
44865
- RouteKey: routeKey2,
44865
+ RouteKey: routeKey,
44866
44866
  Target: properties["Target"],
44867
44867
  AuthorizationType: properties["AuthorizationType"],
44868
44868
  AuthorizerId: properties["AuthorizerId"]
@@ -46087,7 +46087,7 @@ var CloudFrontDistributionProvider = class {
46087
46087
  );
46088
46088
  const sleepEnd = Date.now() + delay2;
46089
46089
  while (Date.now() < sleepEnd && !interrupted) {
46090
- await new Promise((resolve8) => setTimeout(resolve8, 1e3));
46090
+ await new Promise((resolve9) => setTimeout(resolve9, 1e3));
46091
46091
  }
46092
46092
  delay2 = Math.min(delay2 * 1.5, maxDelay);
46093
46093
  }
@@ -49899,7 +49899,7 @@ var RDSProvider = class {
49899
49899
  throw new Error(`Timed out waiting for DBInstance ${dbInstanceIdentifier} to be deleted`);
49900
49900
  }
49901
49901
  sleep(ms) {
49902
- return new Promise((resolve8) => setTimeout(resolve8, ms));
49902
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
49903
49903
  }
49904
49904
  /**
49905
49905
  * Adopt an existing RDS resource into cdkd state.
@@ -50884,7 +50884,7 @@ var DocDBProvider = class {
50884
50884
  throw new Error(`Timed out waiting for DocDB DBInstance ${dbInstanceIdentifier} to be deleted`);
50885
50885
  }
50886
50886
  sleep(ms) {
50887
- return new Promise((resolve8) => setTimeout(resolve8, ms));
50887
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
50888
50888
  }
50889
50889
  /**
50890
50890
  * Adopt an existing DocDB resource into cdkd state.
@@ -51874,7 +51874,7 @@ var NeptuneProvider = class {
51874
51874
  );
51875
51875
  }
51876
51876
  sleep(ms) {
51877
- return new Promise((resolve8) => setTimeout(resolve8, ms));
51877
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
51878
51878
  }
51879
51879
  /**
51880
51880
  * Adopt an existing Neptune resource into cdkd state.
@@ -54414,7 +54414,7 @@ var ElastiCacheProvider = class {
54414
54414
  throw new Error(`Timed out waiting for CacheCluster ${cacheClusterId} to be deleted`);
54415
54415
  }
54416
54416
  sleep(ms) {
54417
- return new Promise((resolve8) => setTimeout(resolve8, ms));
54417
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
54418
54418
  }
54419
54419
  /**
54420
54420
  * Read the AWS-current ElastiCache resource configuration in CFn-property shape.
@@ -55129,7 +55129,7 @@ var ServiceDiscoveryProvider = class {
55129
55129
  logicalId
55130
55130
  );
55131
55131
  }
55132
- await new Promise((resolve8) => setTimeout(resolve8, delay2));
55132
+ await new Promise((resolve9) => setTimeout(resolve9, delay2));
55133
55133
  delay2 = Math.min(delay2 * 2, 1e4);
55134
55134
  }
55135
55135
  throw new ProvisioningError(
@@ -59654,7 +59654,7 @@ var KinesisStreamProvider = class {
59654
59654
  if (status !== "CREATING" && status !== "UPDATING") {
59655
59655
  throw new Error(`Unexpected stream status: ${status}`);
59656
59656
  }
59657
- await new Promise((resolve8) => setTimeout(resolve8, 2e3));
59657
+ await new Promise((resolve9) => setTimeout(resolve9, 2e3));
59658
59658
  }
59659
59659
  throw new Error(
59660
59660
  `Stream ${streamName} did not reach ACTIVE status within ${maxAttempts * 2} seconds`
@@ -59987,7 +59987,7 @@ var KinesisStreamConsumerProvider = class {
59987
59987
  if (status !== "CREATING") {
59988
59988
  throw new Error(`Unexpected consumer status: ${status}`);
59989
59989
  }
59990
- await new Promise((resolve8) => setTimeout(resolve8, 1e3));
59990
+ await new Promise((resolve9) => setTimeout(resolve9, 1e3));
59991
59991
  }
59992
59992
  throw new Error(
59993
59993
  `Consumer ${consumerArn} did not reach ACTIVE status within ${maxAttempts} seconds`
@@ -60292,7 +60292,7 @@ var EFSProvider = class {
60292
60292
  this.logger.debug(
60293
60293
  `FileSystem ${fileSystemId} state: ${fs?.LifeCycleState ?? "unknown"}, waiting...`
60294
60294
  );
60295
- await new Promise((resolve8) => setTimeout(resolve8, pollIntervalMs));
60295
+ await new Promise((resolve9) => setTimeout(resolve9, pollIntervalMs));
60296
60296
  }
60297
60297
  throw new ProvisioningError(
60298
60298
  `Timed out waiting for EFS FileSystem ${fileSystemId} to become available (60s)`,
@@ -60370,7 +60370,7 @@ var EFSProvider = class {
60370
60370
  this.logger.debug(
60371
60371
  `MountTarget ${mountTargetId} state: ${mountTarget?.LifeCycleState ?? "unknown"}, waiting...`
60372
60372
  );
60373
- await new Promise((resolve8) => setTimeout(resolve8, pollIntervalMs));
60373
+ await new Promise((resolve9) => setTimeout(resolve9, pollIntervalMs));
60374
60374
  }
60375
60375
  throw new ProvisioningError(
60376
60376
  `Timed out waiting for EFS MountTarget ${mountTargetId} to become available (120s)`,
@@ -60436,7 +60436,7 @@ var EFSProvider = class {
60436
60436
  }
60437
60437
  throw error;
60438
60438
  }
60439
- await new Promise((resolve8) => setTimeout(resolve8, pollIntervalMs));
60439
+ await new Promise((resolve9) => setTimeout(resolve9, pollIntervalMs));
60440
60440
  }
60441
60441
  this.logger.warn(
60442
60442
  `Timed out waiting for EFS MountTarget ${mountTargetId} deletion for ${logicalId} (120s)`
@@ -61394,7 +61394,7 @@ var FirehoseProvider = class {
61394
61394
  this.logger.debug(
61395
61395
  `Firehose ${logicalId} status: ${status} (attempt ${attempt}/${maxAttempts})`
61396
61396
  );
61397
- await new Promise((resolve8) => setTimeout(resolve8, 2e3));
61397
+ await new Promise((resolve9) => setTimeout(resolve9, 2e3));
61398
61398
  }
61399
61399
  this.logger.warn(`Firehose ${logicalId} did not reach ACTIVE after ${maxAttempts} attempts`);
61400
61400
  }
@@ -64864,7 +64864,7 @@ var ASGProvider = class {
64864
64864
  );
64865
64865
  }
64866
64866
  sleep(ms) {
64867
- return new Promise((resolve8) => setTimeout(resolve8, ms));
64867
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
64868
64868
  }
64869
64869
  // ─── Sub-shape diff helpers ───────────────────────────────────────
64870
64870
  // Each helper is a no-op when before/after JSON is identical (the cheap
@@ -66804,7 +66804,7 @@ Acquiring lock for stack ${stackName}...`);
66804
66804
  logger.debug(
66805
66805
  ` \u23F3 Retrying delete ${logicalId} in ${delay2 / 1e3}s (attempt ${attempt + 1}/${maxAttempts})`
66806
66806
  );
66807
- await new Promise((resolve8) => setTimeout(resolve8, delay2));
66807
+ await new Promise((resolve9) => setTimeout(resolve9, delay2));
66808
66808
  }
66809
66809
  }
66810
66810
  if (lastDeleteError)
@@ -70034,11 +70034,11 @@ async function captureObservedForImportedResources(stackState, providerRegistry,
70034
70034
  }
70035
70035
 
70036
70036
  // src/cli/commands/local-invoke.ts
70037
- import { cpSync as cpSync2, mkdtempSync as mkdtempSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
70037
+ import { cpSync as cpSync2, mkdtempSync as mkdtempSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
70038
70038
  import { tmpdir as tmpdir3 } from "node:os";
70039
- import { dirname as dirname5 } from "node:path";
70039
+ import { dirname as dirname7 } from "node:path";
70040
70040
  import * as path2 from "node:path";
70041
- import { Command as Command15, Option as Option8 } from "commander";
70041
+ import { Command as Command16, Option as Option9 } from "commander";
70042
70042
 
70043
70043
  // src/local/lambda-resolver.ts
70044
70044
  import { existsSync as existsSync4, statSync as statSync3 } from "node:fs";
@@ -70651,14 +70651,47 @@ async function pullImage(image, skipPull) {
70651
70651
  logger.debug(`Skipping docker pull for ${image} (--no-pull)`);
70652
70652
  return;
70653
70653
  }
70654
- logger.info(`Pulling ${image}...`);
70655
- await runForeground("docker", ["pull", image]);
70654
+ if (getLogger().getLevel() === "debug") {
70655
+ logger.info(`Pulling ${image}...`);
70656
+ await runForeground("docker", ["pull", image]);
70657
+ return;
70658
+ }
70659
+ logger.debug(`Pulling ${image} (silent \u2014 pass --verbose to stream progress)`);
70660
+ await runCaptured("docker", ["pull", image], image);
70661
+ }
70662
+ function runCaptured(cmd, args, image) {
70663
+ return new Promise((resolveProc, rejectProc) => {
70664
+ const proc = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
70665
+ let stdout = "";
70666
+ let stderr = "";
70667
+ proc.stdout?.on("data", (chunk) => {
70668
+ stdout += chunk.toString("utf-8");
70669
+ });
70670
+ proc.stderr?.on("data", (chunk) => {
70671
+ stderr += chunk.toString("utf-8");
70672
+ });
70673
+ proc.on(
70674
+ "error",
70675
+ (err) => rejectProc(new DockerRunnerError(`${cmd} pull ${image} failed: ${err.message}`))
70676
+ );
70677
+ proc.on("close", (code) => {
70678
+ if (code === 0) {
70679
+ resolveProc();
70680
+ return;
70681
+ }
70682
+ const detail = stderr.trim() || stdout.trim() || "(no output)";
70683
+ rejectProc(new DockerRunnerError(`docker pull ${image} exited with code ${code}: ${detail}`));
70684
+ });
70685
+ });
70656
70686
  }
70657
70687
  async function runDetached(opts) {
70658
70688
  const args = ["run", "-d", "--rm"];
70659
70689
  if (opts.name) {
70660
70690
  args.push("--name", opts.name);
70661
70691
  }
70692
+ if (opts.network) {
70693
+ args.push("--network", opts.network);
70694
+ }
70662
70695
  if (opts.platform) {
70663
70696
  args.push("--platform", opts.platform);
70664
70697
  }
@@ -70879,7 +70912,7 @@ async function ecrLogin(client, accountId, region) {
70879
70912
  const token = Buffer.from(authData.authorizationToken, "base64").toString();
70880
70913
  const [username, password] = token.split(":");
70881
70914
  const endpoint = authData.proxyEndpoint || `https://${accountId}.dkr.ecr.${region}.amazonaws.com`;
70882
- await new Promise((resolve8, reject) => {
70915
+ await new Promise((resolve9, reject) => {
70883
70916
  const proc = spawn4("docker", ["login", "--username", username, "--password-stdin", endpoint], {
70884
70917
  stdio: ["pipe", "pipe", "pipe"]
70885
70918
  });
@@ -70889,7 +70922,7 @@ async function ecrLogin(client, accountId, region) {
70889
70922
  });
70890
70923
  proc.on("close", (code) => {
70891
70924
  if (code === 0)
70892
- resolve8();
70925
+ resolve9();
70893
70926
  else
70894
70927
  reject(new LocalInvokeBuildError(`ECR login failed: ${stderr.trim()}`));
70895
70928
  });
@@ -70918,12 +70951,12 @@ async function isImageInLocalCache(imageRef) {
70918
70951
  }
70919
70952
  }
70920
70953
  function runForeground2(cmd, args) {
70921
- return new Promise((resolve8, reject) => {
70954
+ return new Promise((resolve9, reject) => {
70922
70955
  const proc = spawn4(cmd, args, { stdio: "inherit" });
70923
70956
  proc.on("error", (err) => reject(new LocalInvokeBuildError(`${cmd} failed: ${err.message}`)));
70924
70957
  proc.on("close", (code) => {
70925
70958
  if (code === 0)
70926
- resolve8();
70959
+ resolve9();
70927
70960
  else
70928
70961
  reject(new LocalInvokeBuildError(`${cmd} exited with code ${code}`));
70929
70962
  });
@@ -71367,8 +71400,8 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
71367
71400
  );
71368
71401
  }
71369
71402
  }
71370
- const routeKey2 = props["RouteKey"];
71371
- if (typeof routeKey2 !== "string" || routeKey2.length === 0) {
71403
+ const routeKey = props["RouteKey"];
71404
+ if (typeof routeKey !== "string" || routeKey.length === 0) {
71372
71405
  throw new Error(
71373
71406
  `${stackName}/${logicalId} (AWS::ApiGatewayV2::Route): RouteKey must be a string`
71374
71407
  );
@@ -71404,7 +71437,7 @@ function discoverHttpApiRoute(logicalId, resource, template, stackName) {
71404
71437
  integrationProps["IntegrationUri"],
71405
71438
  `${stackName}/${integrationLogicalId}.IntegrationUri`
71406
71439
  );
71407
- const { method, pathPattern } = parseRouteKey(routeKey2);
71440
+ const { method, pathPattern } = parseRouteKey(routeKey);
71408
71441
  return [
71409
71442
  {
71410
71443
  method,
@@ -71519,14 +71552,14 @@ function parseHttpApiTargetIntegration(target, location) {
71519
71552
  )}).`
71520
71553
  );
71521
71554
  }
71522
- function parseRouteKey(routeKey2) {
71523
- if (routeKey2 === "$default") {
71555
+ function parseRouteKey(routeKey) {
71556
+ if (routeKey === "$default") {
71524
71557
  return { method: "ANY", pathPattern: "$default" };
71525
71558
  }
71526
- const m = /^([A-Za-z]+)\s+(\S+)$/.exec(routeKey2);
71559
+ const m = /^([A-Za-z]+)\s+(\S+)$/.exec(routeKey);
71527
71560
  if (!m) {
71528
71561
  throw new Error(
71529
- `RouteKey '${routeKey2}' is malformed: expected '<METHOD> <path>' (e.g. 'GET /items/{id}') or '$default'.`
71562
+ `RouteKey '${routeKey}' is malformed: expected '<METHOD> <path>' (e.g. 'GET /items/{id}') or '$default'.`
71530
71563
  );
71531
71564
  }
71532
71565
  return { method: m[1].toUpperCase(), pathPattern: m[2] };
@@ -71712,9 +71745,9 @@ function createContainerPool(specs, options) {
71712
71745
  if (disposed) {
71713
71746
  entry.warm.push(handle);
71714
71747
  if (entry.inUse.size === 0) {
71715
- for (const resolve8 of entry.drainResolvers.splice(0, entry.drainResolvers.length)) {
71748
+ for (const resolve9 of entry.drainResolvers.splice(0, entry.drainResolvers.length)) {
71716
71749
  try {
71717
- resolve8();
71750
+ resolve9();
71718
71751
  } catch {
71719
71752
  }
71720
71753
  }
@@ -71730,9 +71763,9 @@ function createContainerPool(specs, options) {
71730
71763
  entry.warm.push(handle);
71731
71764
  resetIdleTimer(entry);
71732
71765
  if (entry.inUse.size === 0 && entry.drainResolvers.length > 0) {
71733
- for (const resolve8 of entry.drainResolvers.splice(0, entry.drainResolvers.length)) {
71766
+ for (const resolve9 of entry.drainResolvers.splice(0, entry.drainResolvers.length)) {
71734
71767
  try {
71735
- resolve8();
71768
+ resolve9();
71736
71769
  } catch {
71737
71770
  }
71738
71771
  }
@@ -71867,10 +71900,10 @@ function buildHttpApiV2Event(req, ctx, opts = {}) {
71867
71900
  const contentType = headers["content-type"] ?? "";
71868
71901
  const { body, isBase64Encoded } = encodeBody(req.body, contentType);
71869
71902
  const now = opts.now ? opts.now() : /* @__PURE__ */ new Date();
71870
- const routeKey2 = ctx.route.pathPattern === "$default" ? "$default" : `${ctx.route.method} ${ctx.route.pathPattern}`;
71903
+ const routeKey = ctx.route.pathPattern === "$default" ? "$default" : `${ctx.route.method} ${ctx.route.pathPattern}`;
71871
71904
  const event = {
71872
71905
  version: "2.0",
71873
- routeKey: routeKey2,
71906
+ routeKey,
71874
71907
  rawPath,
71875
71908
  rawQueryString,
71876
71909
  cookies,
@@ -71894,7 +71927,7 @@ function buildHttpApiV2Event(req, ctx, opts = {}) {
71894
71927
  userAgent
71895
71928
  },
71896
71929
  requestId: randomUUID(),
71897
- routeKey: routeKey2,
71930
+ routeKey,
71898
71931
  stage: ctx.route.stage,
71899
71932
  time: formatRequestTime(now),
71900
71933
  timeEpoch: now.getTime(),
@@ -73798,6 +73831,74 @@ function writeError(res, statusCode, body = '{"message":"Internal server error"}
73798
73831
  res.end(body);
73799
73832
  }
73800
73833
 
73834
+ // src/local/api-server-grouping.ts
73835
+ function groupRoutesByServer(routes) {
73836
+ const order = [];
73837
+ const byKey = /* @__PURE__ */ new Map();
73838
+ for (const rwa of routes) {
73839
+ const r = rwa.route;
73840
+ let serverKey;
73841
+ let kind;
73842
+ let identifier;
73843
+ let displayName;
73844
+ if (r.source === "function-url") {
73845
+ identifier = r.lambdaLogicalId;
73846
+ serverKey = `function-url:${identifier}`;
73847
+ kind = "function-url";
73848
+ displayName = `${identifier} (Function URL)`;
73849
+ } else if (r.source === "http-api") {
73850
+ identifier = r.apiLogicalId ?? "<unknown>";
73851
+ serverKey = `http-api:${identifier}`;
73852
+ kind = "http-api";
73853
+ displayName = `${identifier} (HTTP API v2)`;
73854
+ } else {
73855
+ identifier = r.apiLogicalId ?? "<unknown>";
73856
+ serverKey = `rest-v1:${identifier}`;
73857
+ kind = "rest-v1";
73858
+ displayName = `${identifier} (REST API v1)`;
73859
+ }
73860
+ const existing = byKey.get(serverKey);
73861
+ if (existing) {
73862
+ existing.routes.push(rwa);
73863
+ } else {
73864
+ byKey.set(serverKey, { displayName, kind, identifier, routes: [rwa] });
73865
+ order.push(serverKey);
73866
+ }
73867
+ }
73868
+ return order.map((key) => {
73869
+ const entry = byKey.get(key);
73870
+ return {
73871
+ serverKey: key,
73872
+ displayName: entry.displayName,
73873
+ kind: entry.kind,
73874
+ identifier: entry.identifier,
73875
+ routes: entry.routes
73876
+ };
73877
+ });
73878
+ }
73879
+ function filterRoutesByApiIdentifier(routes, identifier) {
73880
+ return routes.filter((rwa) => {
73881
+ const r = rwa.route;
73882
+ if (r.source === "function-url") {
73883
+ return r.lambdaLogicalId === identifier;
73884
+ }
73885
+ return r.apiLogicalId === identifier;
73886
+ });
73887
+ }
73888
+ function availableApiIdentifiers(routes) {
73889
+ const seen = /* @__PURE__ */ new Set();
73890
+ const out = [];
73891
+ for (const rwa of routes) {
73892
+ const r = rwa.route;
73893
+ const id = r.source === "function-url" ? r.lambdaLogicalId : r.apiLogicalId ?? "<unknown>";
73894
+ if (!seen.has(id)) {
73895
+ seen.add(id);
73896
+ out.push(id);
73897
+ }
73898
+ }
73899
+ return out;
73900
+ }
73901
+
73801
73902
  // src/local/stage-resolver.ts
73802
73903
  function buildStageMap(template, stageOverride) {
73803
73904
  const out = /* @__PURE__ */ new Map();
@@ -74630,9 +74731,9 @@ var NodeFsHandler = class {
74630
74731
  if (this.fsw.closed) {
74631
74732
  return;
74632
74733
  }
74633
- const dirname7 = sp.dirname(file);
74734
+ const dirname9 = sp.dirname(file);
74634
74735
  const basename4 = sp.basename(file);
74635
- const parent = this.fsw._getWatchedDir(dirname7);
74736
+ const parent = this.fsw._getWatchedDir(dirname9);
74636
74737
  let prevStats = stats;
74637
74738
  if (parent.has(basename4))
74638
74739
  return;
@@ -74659,7 +74760,7 @@ var NodeFsHandler = class {
74659
74760
  prevStats = newStats2;
74660
74761
  }
74661
74762
  } catch (error) {
74662
- this.fsw._remove(dirname7, basename4);
74763
+ this.fsw._remove(dirname9, basename4);
74663
74764
  }
74664
74765
  } else if (parent.has(basename4)) {
74665
74766
  const at = newStats.atimeMs;
@@ -74756,7 +74857,7 @@ var NodeFsHandler = class {
74756
74857
  this._addToNodeFs(path3, initialAdd, wh, depth + 1);
74757
74858
  }
74758
74859
  }).on(EV.ERROR, this._boundHandleError);
74759
- return new Promise((resolve8, reject) => {
74860
+ return new Promise((resolve9, reject) => {
74760
74861
  if (!stream)
74761
74862
  return reject();
74762
74863
  stream.once(STR_END, () => {
@@ -74765,7 +74866,7 @@ var NodeFsHandler = class {
74765
74866
  return;
74766
74867
  }
74767
74868
  const wasThrottled = throttler ? throttler.clear() : false;
74768
- resolve8(void 0);
74869
+ resolve9(void 0);
74769
74870
  previous.getChildren().filter((item) => {
74770
74871
  return item !== directory && !current.has(item);
74771
74872
  }).forEach((item) => {
@@ -75689,92 +75790,6 @@ function createFileWatcher(options) {
75689
75790
  };
75690
75791
  }
75691
75792
 
75692
- // src/local/reload-orchestrator.ts
75693
- function createReloadOrchestrator(deps) {
75694
- const logger = getLogger().child("start-api-reload");
75695
- let chain = Promise.resolve();
75696
- return {
75697
- reload() {
75698
- const next = chain.then(() => runOneReload(deps, logger));
75699
- chain = next.catch(() => void 0);
75700
- return next;
75701
- }
75702
- };
75703
- }
75704
- async function runOneReload(deps, logger) {
75705
- const previousState = deps.getServerState();
75706
- const start = Date.now();
75707
- let material;
75708
- try {
75709
- material = await deps.synthesizeAndBuild();
75710
- } catch (err) {
75711
- const reason = err instanceof Error ? err.message : String(err);
75712
- logger.warn(`cdk synth failed during reload; keeping previous version. (${reason})`);
75713
- return { ok: false, reason, added: [], removed: [], rebuiltLambdas: [] };
75714
- }
75715
- const oldRoutes = previousState.routes;
75716
- const newRoutes = material.routes;
75717
- const oldKeys = new Set(oldRoutes.map((r) => routeKey(r.route)));
75718
- const newKeys = new Set(newRoutes.map((r) => routeKey(r.route)));
75719
- const added = newRoutes.filter((r) => !oldKeys.has(routeKey(r.route)));
75720
- const removed = oldRoutes.filter((r) => !newKeys.has(routeKey(r.route)));
75721
- const previousSpecs = pickSpecsFromState(previousState);
75722
- const rebuiltLambdas = [];
75723
- for (const [logicalId, newSpec] of material.specs) {
75724
- const oldSpec = previousSpecs.get(logicalId);
75725
- if (!oldSpec)
75726
- continue;
75727
- if (specSignature(oldSpec) !== specSignature(newSpec)) {
75728
- rebuiltLambdas.push(logicalId);
75729
- }
75730
- }
75731
- const newPool = deps.buildPool(material.specs);
75732
- Object.defineProperty(newPool, "__cdkdSpecs", {
75733
- value: material.specs,
75734
- enumerable: false,
75735
- configurable: true
75736
- });
75737
- const newState = {
75738
- routes: material.routes,
75739
- pool: newPool,
75740
- corsConfigByApiId: material.corsConfigByApiId
75741
- };
75742
- deps.setServerState(newState);
75743
- void previousState.pool.dispose().catch(
75744
- (err) => logger.debug(
75745
- `Previous pool dispose() failed: ${err instanceof Error ? err.message : String(err)}`
75746
- )
75747
- );
75748
- const elapsed = Date.now() - start;
75749
- logger.info(
75750
- `Reloaded in ${elapsed}ms: +${added.length} route(s), -${removed.length} route(s), ${rebuiltLambdas.length} Lambda(s) rebuilt.`
75751
- );
75752
- return {
75753
- ok: true,
75754
- added,
75755
- removed,
75756
- rebuiltLambdas,
75757
- newState
75758
- };
75759
- }
75760
- function routeKey(route) {
75761
- return [route.method, route.pathPattern, route.lambdaLogicalId, route.source, route.apiVersion].map((s) => String(s)).join("|");
75762
- }
75763
- function specSignature(spec) {
75764
- return JSON.stringify({
75765
- codeDir: spec.codeDir,
75766
- env: spec.env,
75767
- handler: spec.lambda.handler,
75768
- runtime: spec.lambda.runtime,
75769
- containerHost: spec.containerHost,
75770
- debugPort: spec.debugPort ?? null
75771
- });
75772
- }
75773
- function pickSpecsFromState(state) {
75774
- const tagged = state.pool.__cdkdSpecs;
75775
- return tagged ?? /* @__PURE__ */ new Map();
75776
- }
75777
-
75778
75793
  // src/local/authorizer-cache.ts
75779
75794
  function createAuthorizerCache(opts = {}) {
75780
75795
  const now = opts.now ?? (() => Date.now());
@@ -75880,7 +75895,17 @@ async function localStartApiCommand(options) {
75880
75895
  }
75881
75896
  }
75882
75897
  attachStageContext(routes, stageMap);
75883
- const routesWithAuth = attachAuthorizers(targetStacks, routes);
75898
+ let routesWithAuth = attachAuthorizers(targetStacks, routes);
75899
+ if (options.api) {
75900
+ const filtered = filterRoutesByApiIdentifier(routesWithAuth, options.api);
75901
+ if (filtered.length === 0) {
75902
+ const available = availableApiIdentifiers(routesWithAuth).join(", ") || "(none)";
75903
+ throw new Error(
75904
+ `--api '${options.api}' did not match any discovered API. Available identifiers: ${available}.`
75905
+ );
75906
+ }
75907
+ routesWithAuth = filtered;
75908
+ }
75884
75909
  const corsConfigByApiId = /* @__PURE__ */ new Map();
75885
75910
  for (const stack of targetStacks) {
75886
75911
  const m = buildCorsConfigByApiId(stack.template);
@@ -75933,88 +75958,91 @@ async function localStartApiCommand(options) {
75933
75958
  return [...assetPaths];
75934
75959
  };
75935
75960
  const initialMaterial = await synthesizeAndBuild();
75936
- const initialPool = buildPool(initialMaterial.specs);
75937
75961
  lastAssetPaths.value = computeAssetPaths(initialMaterial.specs);
75938
75962
  await prewarmJwks(initialMaterial.routes, jwksCache);
75939
75963
  warnVpcConfigLambdas(initialMaterial.routes, initialMaterial.stacks ?? []);
75940
- if (options.warm) {
75941
- logger.info(`Pre-warming ${initialMaterial.specs.size} container(s)...`);
75942
- const handles = await Promise.allSettled(
75943
- [...initialMaterial.specs.keys()].map((id) => initialPool.acquire(id))
75944
- );
75945
- for (const result of handles) {
75946
- if (result.status === "fulfilled") {
75947
- initialPool.release(result.value);
75948
- } else {
75949
- logger.warn(
75950
- `Pre-warm failed for one Lambda (cold start cost will apply on first request): ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
75951
- );
75952
- }
75953
- }
75954
- }
75955
75964
  let maxTimeoutSec = 0;
75956
75965
  for (const spec of initialMaterial.specs.values()) {
75957
75966
  if (spec.lambda.timeoutSec > maxTimeoutSec)
75958
75967
  maxTimeoutSec = spec.lambda.timeoutSec;
75959
75968
  }
75960
75969
  const rieTimeoutMs = Math.max(3e4, maxTimeoutSec * 2 * 1e3);
75961
- const port = parseInt(options.port, 10);
75962
- if (!Number.isFinite(port) || port < 0 || port > 65535) {
75970
+ const basePort = parseInt(options.port, 10);
75971
+ if (!Number.isFinite(basePort) || basePort < 0 || basePort > 65535) {
75963
75972
  throw new Error(`--port must be 0..65535 (got ${options.port}).`);
75964
75973
  }
75965
- const initialState = {
75966
- routes: initialMaterial.routes,
75967
- pool: initialPool,
75968
- corsConfigByApiId: initialMaterial.corsConfigByApiId
75969
- };
75970
- const server = await startApiServer({
75971
- state: initialState,
75972
- rieTimeoutMs,
75973
- host: options.host,
75974
- port,
75975
- authorizerCache,
75976
- jwksCache,
75977
- jwksWarnedUrls
75978
- });
75979
- printRouteTable(initialMaterial.routes);
75974
+ const initialGroups = groupRoutesByServer(initialMaterial.routes);
75975
+ const servers = [];
75976
+ let nextPort = basePort;
75977
+ for (const group of initialGroups) {
75978
+ const groupSpecs = filterSpecsForGroup(group, initialMaterial.specs);
75979
+ const groupPool = buildPool(groupSpecs);
75980
+ const groupState = {
75981
+ routes: group.routes,
75982
+ pool: groupPool,
75983
+ corsConfigByApiId: initialMaterial.corsConfigByApiId
75984
+ };
75985
+ if (options.warm) {
75986
+ logger.info(`Pre-warming ${groupSpecs.size} container(s) for ${group.displayName}...`);
75987
+ const handles = await Promise.allSettled(
75988
+ [...groupSpecs.keys()].map((id) => groupPool.acquire(id))
75989
+ );
75990
+ for (const result of handles) {
75991
+ if (result.status === "fulfilled") {
75992
+ groupPool.release(result.value);
75993
+ } else {
75994
+ logger.warn(
75995
+ `Pre-warm failed for one Lambda in ${group.displayName} (cold start cost will apply on first request): ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
75996
+ );
75997
+ }
75998
+ }
75999
+ }
76000
+ const started = await startApiServer({
76001
+ state: groupState,
76002
+ rieTimeoutMs,
76003
+ host: options.host,
76004
+ // Increment per server; basePort=0 leaves every server on auto-alloc.
76005
+ port: basePort === 0 ? 0 : nextPort,
76006
+ authorizerCache,
76007
+ jwksCache,
76008
+ jwksWarnedUrls
76009
+ });
76010
+ servers.push({ group, server: started });
76011
+ if (basePort !== 0)
76012
+ nextPort += 1;
76013
+ }
76014
+ printPerServerRouteTables(servers);
75980
76015
  logger.info(
75981
76016
  `Per-Lambda concurrency: ${perLambdaConcurrency} (override with --per-lambda-concurrency)`
75982
76017
  );
75983
- process.stdout.write(`Server listening on http://${server.host}:${server.port}
75984
- `);
76018
+ for (const { group, server } of servers) {
76019
+ process.stdout.write(
76020
+ `Server listening on http://${server.host}:${server.port} (${group.displayName})
76021
+ `
76022
+ );
76023
+ }
75985
76024
  process.stdout.write("^C to stop and clean up containers.\n");
75986
76025
  let watcher;
75987
- let orchestrator;
76026
+ let reloadChain = Promise.resolve();
75988
76027
  if (options.watch) {
75989
- orchestrator = createReloadOrchestrator({
75990
- synthesizeAndBuild,
75991
- buildPool,
75992
- setServerState: server.setServerState,
75993
- getServerState: server.getServerState
75994
- });
75995
76028
  const initialWatchPaths = [options.output, ...lastAssetPaths.value];
75996
76029
  watcher = createFileWatcher({
75997
76030
  paths: initialWatchPaths,
75998
76031
  onChange: () => {
75999
- if (!orchestrator)
76000
- return;
76001
76032
  logger.info("Detected file change; reloading...");
76002
- void orchestrator.reload().then((result) => {
76003
- if (result.ok && watcher && result.newState) {
76004
- const taggedSpecs = result.newState.pool.__cdkdSpecs;
76005
- if (taggedSpecs) {
76006
- lastAssetPaths.value = computeAssetPaths(taggedSpecs);
76007
- }
76008
- watcher.update([options.output, ...lastAssetPaths.value]);
76009
- if (result.added.length > 0 || result.removed.length > 0) {
76010
- printRouteTable(result.newState.routes);
76011
- }
76012
- }
76013
- }).catch((err) => {
76014
- logger.warn(
76015
- `Reload failed: ${err instanceof Error ? err.message : String(err)}. Keeping previous version.`
76016
- );
76017
- });
76033
+ const next = reloadChain.then(
76034
+ () => reloadAllServers({
76035
+ synthesizeAndBuild,
76036
+ servers,
76037
+ buildPool,
76038
+ computeAssetPaths,
76039
+ lastAssetPaths,
76040
+ watcher,
76041
+ output: options.output,
76042
+ logger
76043
+ })
76044
+ );
76045
+ reloadChain = next.catch(() => void 0);
76018
76046
  }
76019
76047
  });
76020
76048
  logger.info(`Watching ${options.output} (and ${lastAssetPaths.value.length} asset dir(s))`);
@@ -76041,16 +76069,28 @@ async function localStartApiCommand(options) {
76041
76069
  logger.warn(`watcher.close() failed: ${err instanceof Error ? err.message : String(err)}`);
76042
76070
  }
76043
76071
  }
76044
- try {
76045
- await server.close();
76046
- } catch (err) {
76047
- logger.warn(`server.close() failed: ${err instanceof Error ? err.message : String(err)}`);
76048
- }
76049
- try {
76050
- await server.getServerState().pool.dispose();
76051
- } catch (err) {
76052
- logger.warn(`pool.dispose() failed: ${err instanceof Error ? err.message : String(err)}`);
76053
- }
76072
+ await Promise.allSettled(
76073
+ servers.map(async ({ server, group }) => {
76074
+ try {
76075
+ await server.close();
76076
+ } catch (err) {
76077
+ logger.warn(
76078
+ `server.close() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`
76079
+ );
76080
+ }
76081
+ })
76082
+ );
76083
+ await Promise.allSettled(
76084
+ servers.map(async ({ server, group }) => {
76085
+ try {
76086
+ await server.getServerState().pool.dispose();
76087
+ } catch (err) {
76088
+ logger.warn(
76089
+ `pool.dispose() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`
76090
+ );
76091
+ }
76092
+ })
76093
+ );
76054
76094
  for (const dir of inlineTmpDirs) {
76055
76095
  try {
76056
76096
  rmSync2(dir, { recursive: true, force: true });
@@ -76422,6 +76462,91 @@ function parsePerLambdaConcurrency(raw) {
76422
76462
  }
76423
76463
  return parsed;
76424
76464
  }
76465
+ function filterSpecsForGroup(group, allSpecs) {
76466
+ const ids = /* @__PURE__ */ new Set();
76467
+ for (const rwa of group.routes) {
76468
+ ids.add(rwa.route.lambdaLogicalId);
76469
+ const auth = rwa.authorizer;
76470
+ if (auth && (auth.kind === "lambda-token" || auth.kind === "lambda-request")) {
76471
+ ids.add(auth.lambdaLogicalId);
76472
+ }
76473
+ }
76474
+ const out = /* @__PURE__ */ new Map();
76475
+ for (const id of ids) {
76476
+ const spec = allSpecs.get(id);
76477
+ if (spec)
76478
+ out.set(id, spec);
76479
+ }
76480
+ return out;
76481
+ }
76482
+ function printPerServerRouteTables(servers) {
76483
+ for (const { group, server } of servers) {
76484
+ process.stdout.write(`
76485
+ ${group.displayName} (http://${server.host}:${server.port})
76486
+ `);
76487
+ printRouteTable(group.routes);
76488
+ }
76489
+ }
76490
+ async function reloadAllServers(args) {
76491
+ const {
76492
+ synthesizeAndBuild,
76493
+ servers,
76494
+ buildPool,
76495
+ computeAssetPaths,
76496
+ lastAssetPaths,
76497
+ watcher,
76498
+ output,
76499
+ logger
76500
+ } = args;
76501
+ let material;
76502
+ try {
76503
+ material = await synthesizeAndBuild();
76504
+ } catch (err) {
76505
+ logger.warn(
76506
+ `cdk synth failed during reload; keeping previous version. (${err instanceof Error ? err.message : String(err)})`
76507
+ );
76508
+ return;
76509
+ }
76510
+ const newGroups = groupRoutesByServer(material.routes);
76511
+ const newByKey = new Map(newGroups.map((g) => [g.serverKey, g]));
76512
+ const oldKeys = new Set(servers.map((s) => s.group.serverKey));
76513
+ const newKeys = new Set(newByKey.keys());
76514
+ const added = [...newKeys].filter((k) => !oldKeys.has(k));
76515
+ const removed = [...oldKeys].filter((k) => !newKeys.has(k));
76516
+ if (added.length > 0) {
76517
+ logger.warn(
76518
+ `Reload detected new API surface(s): ${added.join(", ")}. Restart 'cdkd local start-api' to serve them.`
76519
+ );
76520
+ }
76521
+ if (removed.length > 0) {
76522
+ logger.warn(
76523
+ `Reload detected removed API surface(s): ${removed.join(", ")}. Their servers will keep serving stale routes until restart.`
76524
+ );
76525
+ }
76526
+ for (const booted of servers) {
76527
+ const group = newByKey.get(booted.group.serverKey);
76528
+ if (!group)
76529
+ continue;
76530
+ const groupSpecs = filterSpecsForGroup(group, material.specs);
76531
+ const newPool = buildPool(groupSpecs);
76532
+ const newState = {
76533
+ routes: group.routes,
76534
+ pool: newPool,
76535
+ corsConfigByApiId: material.corsConfigByApiId
76536
+ };
76537
+ const previousState = booted.server.setServerState(newState);
76538
+ void previousState.pool.dispose().catch((err) => {
76539
+ logger.debug(
76540
+ `Previous pool dispose() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`
76541
+ );
76542
+ });
76543
+ }
76544
+ lastAssetPaths.value = computeAssetPaths(material.specs);
76545
+ if (watcher) {
76546
+ watcher.update([output, ...lastAssetPaths.value]);
76547
+ }
76548
+ printPerServerRouteTables(servers);
76549
+ }
76425
76550
  function parseDebugPort(raw) {
76426
76551
  const parsed = parseInt(raw, 10);
76427
76552
  if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
@@ -76444,8 +76569,8 @@ function createLocalStartApiCommand() {
76444
76569
  ).addOption(new Option7("--no-pull", "Skip docker pull (cached image)")).addOption(
76445
76570
  new Option7(
76446
76571
  "--container-host <host>",
76447
- "Hostname/IP the container reaches the host on"
76448
- ).default("host.docker.internal")
76572
+ "IP the host uses to bind/probe the RIE port (must be a numeric IP \u2014 `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1."
76573
+ ).default("127.0.0.1")
76449
76574
  ).addOption(
76450
76575
  new Option7(
76451
76576
  "--debug-port-base <port>",
@@ -76471,12 +76596,1488 @@ function createLocalStartApiCommand() {
76471
76596
  "--stage <name>",
76472
76597
  "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation \u2014 HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage."
76473
76598
  )
76599
+ ).addOption(
76600
+ new Option7(
76601
+ "--api <id>",
76602
+ "Restrict to a single API surface by its logical id (HTTP API / REST API logical id, or the backing Lambda's logical id for Function URLs). When unset, every discovered API gets its own server on its own port (basePort, basePort+1, ... when --port is set; auto-allocated otherwise)."
76603
+ )
76474
76604
  ).action(withErrorHandling(localStartApiCommand));
76475
76605
  [...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => startApi.addOption(opt));
76476
76606
  startApi.addOption(deprecatedRegionOption);
76477
76607
  return startApi;
76478
76608
  }
76479
76609
 
76610
+ // src/cli/commands/local-run-task.ts
76611
+ import { readFileSync as readFileSync7 } from "node:fs";
76612
+ import { Command as Command15, Option as Option8 } from "commander";
76613
+
76614
+ // src/local/ecs-task-resolver.ts
76615
+ import { dirname as dirname5, isAbsolute as isAbsolute4, resolve as resolve8 } from "node:path";
76616
+ import { existsSync as existsSync5, statSync as statSync4 } from "node:fs";
76617
+ var EcsTaskResolutionError = class _EcsTaskResolutionError extends Error {
76618
+ constructor(message) {
76619
+ super(message);
76620
+ this.name = "EcsTaskResolutionError";
76621
+ Object.setPrototypeOf(this, _EcsTaskResolutionError.prototype);
76622
+ }
76623
+ };
76624
+ function parseEcsTarget(target) {
76625
+ if (typeof target !== "string" || target.length === 0) {
76626
+ throw new EcsTaskResolutionError(
76627
+ "Empty target. Pass a CDK display path (e.g. 'MyStack/MyService/TaskDef') or stack-qualified logical ID (e.g. 'MyStack:MyServiceTaskDefXYZ1234')."
76628
+ );
76629
+ }
76630
+ const colonIdx = target.indexOf(":");
76631
+ const slashIdx = target.indexOf("/");
76632
+ if (colonIdx > 0 && (slashIdx === -1 || colonIdx < slashIdx)) {
76633
+ const stackPattern = target.substring(0, colonIdx);
76634
+ const pathOrId = target.substring(colonIdx + 1);
76635
+ if (pathOrId.length === 0) {
76636
+ throw new EcsTaskResolutionError(`Target '${target}' has no logical ID after ':'.`);
76637
+ }
76638
+ return { stackPattern, pathOrId, isPath: pathOrId.includes("/") };
76639
+ }
76640
+ if (slashIdx > 0) {
76641
+ return { stackPattern: target.substring(0, slashIdx), pathOrId: target, isPath: true };
76642
+ }
76643
+ return { stackPattern: null, pathOrId: target, isPath: false };
76644
+ }
76645
+ function resolveEcsTaskTarget(target, stacks) {
76646
+ if (stacks.length === 0) {
76647
+ throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
76648
+ }
76649
+ const parsed = parseEcsTarget(target);
76650
+ const stack = pickStack2(parsed, stacks);
76651
+ const resources = stack.template.Resources ?? {};
76652
+ let logicalId;
76653
+ let resource;
76654
+ if (parsed.isPath) {
76655
+ const index = buildCdkPathIndex(stack.template);
76656
+ const resolved = resolveCdkPathToLogicalIds(parsed.pathOrId, index);
76657
+ const taskDefs = resolved.filter(
76658
+ ({ logicalId: l }) => resources[l]?.Type === "AWS::ECS::TaskDefinition"
76659
+ );
76660
+ if (taskDefs.length === 0) {
76661
+ throw notFoundError2(target, stack, resources);
76662
+ }
76663
+ if (taskDefs.length > 1) {
76664
+ throw new EcsTaskResolutionError(
76665
+ `Target '${target}' matches ${taskDefs.length} task definitions in ${stack.stackName}: ` + taskDefs.map((t) => t.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form."
76666
+ );
76667
+ }
76668
+ logicalId = taskDefs[0].logicalId;
76669
+ resource = resources[logicalId];
76670
+ } else {
76671
+ resource = resources[parsed.pathOrId];
76672
+ if (!resource)
76673
+ throw notFoundError2(target, stack, resources);
76674
+ logicalId = parsed.pathOrId;
76675
+ }
76676
+ if (!logicalId || !resource)
76677
+ throw notFoundError2(target, stack, resources);
76678
+ if (resource.Type === "AWS::Lambda::Function") {
76679
+ throw new EcsTaskResolutionError(
76680
+ `Resource '${logicalId}' in ${stack.stackName} is a Lambda function, not an ECS task definition. Use \`cdkd local invoke\` for Lambda; \`cdkd local run-task\` is ECS only.`
76681
+ );
76682
+ }
76683
+ if (resource.Type !== "AWS::ECS::TaskDefinition") {
76684
+ throw new EcsTaskResolutionError(
76685
+ `Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not an AWS::ECS::TaskDefinition.`
76686
+ );
76687
+ }
76688
+ return extractTaskDefinitionProperties(stack, logicalId, resource);
76689
+ }
76690
+ function pickStack2(parsed, stacks) {
76691
+ if (parsed.stackPattern === null) {
76692
+ if (stacks.length === 1)
76693
+ return stacks[0];
76694
+ throw new EcsTaskResolutionError(
76695
+ `Multiple stacks in app, target '${parsed.pathOrId}' is missing a stack prefix. Use 'StackName:${parsed.pathOrId}' or 'StackName/...' (path form). Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`
76696
+ );
76697
+ }
76698
+ const matched = matchStacks(stacks, [parsed.stackPattern]);
76699
+ if (matched.length === 0) {
76700
+ throw new EcsTaskResolutionError(
76701
+ `Stack '${parsed.stackPattern}' not found. Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`
76702
+ );
76703
+ }
76704
+ if (matched.length > 1) {
76705
+ throw new EcsTaskResolutionError(
76706
+ `Stack pattern '${parsed.stackPattern}' matched ${matched.length} stacks: ` + matched.map((s) => s.stackName).join(", ") + ". Use a more specific pattern."
76707
+ );
76708
+ }
76709
+ return matched[0];
76710
+ }
76711
+ function extractTaskDefinitionProperties(stack, logicalId, resource) {
76712
+ const props = resource.Properties ?? {};
76713
+ const warnings = [];
76714
+ const family = pickString(props["Family"]) ?? logicalId;
76715
+ const rawNetworkMode = pickString(props["NetworkMode"]) ?? "bridge";
76716
+ let networkMode;
76717
+ if (rawNetworkMode === "bridge" || rawNetworkMode === "awsvpc" || rawNetworkMode === "host" || rawNetworkMode === "none") {
76718
+ networkMode = rawNetworkMode;
76719
+ } else {
76720
+ throw new EcsTaskResolutionError(
76721
+ `Task definition '${logicalId}' has unsupported NetworkMode '${rawNetworkMode}'. Supported values: bridge / awsvpc / host / none.`
76722
+ );
76723
+ }
76724
+ if (networkMode === "awsvpc") {
76725
+ warnings.push(
76726
+ `NetworkMode 'awsvpc' on '${logicalId}' is mapped to docker bridge locally \u2014 docker cannot emulate ENI-per-task. AWS SDK calls still reach public endpoints via the developer network.`
76727
+ );
76728
+ }
76729
+ const resources = stack.template.Resources ?? {};
76730
+ const taskRoleArn = resolveRoleArn(props["TaskRoleArn"], resources);
76731
+ const executionRoleArn = resolveRoleArn(props["ExecutionRoleArn"], resources);
76732
+ const runtimePlatform = parseRuntimePlatform(props["RuntimePlatform"]);
76733
+ const rawContainers = props["ContainerDefinitions"];
76734
+ if (!Array.isArray(rawContainers) || rawContainers.length === 0) {
76735
+ throw new EcsTaskResolutionError(`Task definition '${logicalId}' has no ContainerDefinitions.`);
76736
+ }
76737
+ const containers = rawContainers.map(
76738
+ (c, idx) => parseContainerDefinition(c, idx, logicalId, resources, stack)
76739
+ );
76740
+ const rawVolumes = props["Volumes"];
76741
+ const volumes = Array.isArray(rawVolumes) ? rawVolumes.map((v, idx) => parseVolume(v, idx, logicalId)) : [];
76742
+ const containerNames = new Set(containers.map((c) => c.name));
76743
+ for (const c of containers) {
76744
+ for (const d of c.dependsOn) {
76745
+ if (!containerNames.has(d.containerName)) {
76746
+ throw new EcsTaskResolutionError(
76747
+ `Container '${c.name}' depends on '${d.containerName}', which is not defined in task '${logicalId}'.`
76748
+ );
76749
+ }
76750
+ }
76751
+ }
76752
+ const out = {
76753
+ stack,
76754
+ taskDefinitionLogicalId: logicalId,
76755
+ resource,
76756
+ family,
76757
+ networkMode,
76758
+ containers,
76759
+ volumes,
76760
+ warnings
76761
+ };
76762
+ if (taskRoleArn !== void 0)
76763
+ out.taskRoleArn = taskRoleArn;
76764
+ if (executionRoleArn !== void 0)
76765
+ out.executionRoleArn = executionRoleArn;
76766
+ if (runtimePlatform !== void 0)
76767
+ out.runtimePlatform = runtimePlatform;
76768
+ return out;
76769
+ }
76770
+ function parseRuntimePlatform(value) {
76771
+ if (!value || typeof value !== "object")
76772
+ return void 0;
76773
+ const obj = value;
76774
+ const cpu = obj["CpuArchitecture"];
76775
+ const os = obj["OperatingSystemFamily"];
76776
+ if (typeof cpu !== "string" || typeof os !== "string")
76777
+ return void 0;
76778
+ if (cpu !== "X86_64" && cpu !== "ARM64")
76779
+ return void 0;
76780
+ if (os !== "LINUX")
76781
+ return void 0;
76782
+ return { cpuArchitecture: cpu, operatingSystemFamily: os };
76783
+ }
76784
+ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack) {
76785
+ if (!raw || typeof raw !== "object") {
76786
+ throw new EcsTaskResolutionError(
76787
+ `Task '${taskLogicalId}' ContainerDefinitions[${idx}] is not an object.`
76788
+ );
76789
+ }
76790
+ const c = raw;
76791
+ const name = pickString(c["Name"]);
76792
+ if (!name) {
76793
+ throw new EcsTaskResolutionError(
76794
+ `Task '${taskLogicalId}' ContainerDefinitions[${idx}] has no Name.`
76795
+ );
76796
+ }
76797
+ const image = parseContainerImage(c["Image"], name, taskLogicalId, resources, stack);
76798
+ const command = pickStringArray2(c["Command"]);
76799
+ const entryPoint = pickStringArray2(c["EntryPoint"]);
76800
+ const workingDirectory = pickString(c["WorkingDirectory"]);
76801
+ const environment = {};
76802
+ if (Array.isArray(c["Environment"])) {
76803
+ for (const entry of c["Environment"]) {
76804
+ if (!entry || typeof entry !== "object")
76805
+ continue;
76806
+ const e = entry;
76807
+ const key = pickString(e["Name"]);
76808
+ const value = e["Value"];
76809
+ if (!key)
76810
+ continue;
76811
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
76812
+ environment[key] = String(value);
76813
+ }
76814
+ }
76815
+ }
76816
+ const secrets = [];
76817
+ if (Array.isArray(c["Secrets"])) {
76818
+ for (const entry of c["Secrets"]) {
76819
+ if (!entry || typeof entry !== "object")
76820
+ continue;
76821
+ const e = entry;
76822
+ const sName = pickString(e["Name"]);
76823
+ const valueFrom = pickString(e["ValueFrom"]);
76824
+ if (sName && valueFrom)
76825
+ secrets.push({ name: sName, valueFrom });
76826
+ }
76827
+ }
76828
+ const portMappings = [];
76829
+ if (Array.isArray(c["PortMappings"])) {
76830
+ for (const entry of c["PortMappings"]) {
76831
+ if (!entry || typeof entry !== "object")
76832
+ continue;
76833
+ const p = entry;
76834
+ const containerPort = typeof p["ContainerPort"] === "number" ? p["ContainerPort"] : void 0;
76835
+ if (containerPort === void 0)
76836
+ continue;
76837
+ const hostPort = typeof p["HostPort"] === "number" ? p["HostPort"] : void 0;
76838
+ const protocol = pickString(p["Protocol"]) === "udp" ? "udp" : "tcp";
76839
+ const pm = { containerPort, protocol };
76840
+ if (hostPort !== void 0)
76841
+ pm.hostPort = hostPort;
76842
+ portMappings.push(pm);
76843
+ }
76844
+ }
76845
+ const mountPoints = [];
76846
+ if (Array.isArray(c["MountPoints"])) {
76847
+ for (const entry of c["MountPoints"]) {
76848
+ if (!entry || typeof entry !== "object")
76849
+ continue;
76850
+ const m = entry;
76851
+ const sourceVolume = pickString(m["SourceVolume"]);
76852
+ const containerPath = pickString(m["ContainerPath"]);
76853
+ if (!sourceVolume || !containerPath)
76854
+ continue;
76855
+ mountPoints.push({
76856
+ sourceVolume,
76857
+ containerPath,
76858
+ readOnly: m["ReadOnly"] === true
76859
+ });
76860
+ }
76861
+ }
76862
+ const dependsOn = [];
76863
+ if (Array.isArray(c["DependsOn"])) {
76864
+ for (const entry of c["DependsOn"]) {
76865
+ if (!entry || typeof entry !== "object")
76866
+ continue;
76867
+ const d = entry;
76868
+ const containerName = pickString(d["ContainerName"]);
76869
+ const condition = pickString(d["Condition"]);
76870
+ if (!containerName || !condition)
76871
+ continue;
76872
+ if (condition !== "START" && condition !== "COMPLETE" && condition !== "SUCCESS" && condition !== "HEALTHY") {
76873
+ throw new EcsTaskResolutionError(
76874
+ `Container '${name}' has invalid DependsOn condition '${condition}'. Accepted values: START / COMPLETE / SUCCESS / HEALTHY.`
76875
+ );
76876
+ }
76877
+ dependsOn.push({ containerName, condition });
76878
+ }
76879
+ }
76880
+ const links = pickStringArray2(c["Links"]) ?? [];
76881
+ const essential = c["Essential"] === false ? false : true;
76882
+ let healthCheck;
76883
+ if (c["HealthCheck"] && typeof c["HealthCheck"] === "object") {
76884
+ const h = c["HealthCheck"];
76885
+ const command2 = pickStringArray2(h["Command"]);
76886
+ if (command2 && command2.length > 0) {
76887
+ healthCheck = { command: command2 };
76888
+ if (typeof h["Interval"] === "number")
76889
+ healthCheck.interval = h["Interval"];
76890
+ if (typeof h["Timeout"] === "number")
76891
+ healthCheck.timeout = h["Timeout"];
76892
+ if (typeof h["Retries"] === "number")
76893
+ healthCheck.retries = h["Retries"];
76894
+ if (typeof h["StartPeriod"] === "number")
76895
+ healthCheck.startPeriod = h["StartPeriod"];
76896
+ }
76897
+ }
76898
+ const user = pickString(c["User"]);
76899
+ const privileged = c["Privileged"] === true ? true : void 0;
76900
+ const readonlyRootFilesystem = c["ReadonlyRootFilesystem"] === true ? true : void 0;
76901
+ const ulimits = [];
76902
+ if (Array.isArray(c["Ulimits"])) {
76903
+ for (const entry of c["Ulimits"]) {
76904
+ if (!entry || typeof entry !== "object")
76905
+ continue;
76906
+ const u = entry;
76907
+ const uName = pickString(u["Name"]);
76908
+ const soft = typeof u["SoftLimit"] === "number" ? u["SoftLimit"] : void 0;
76909
+ const hard = typeof u["HardLimit"] === "number" ? u["HardLimit"] : void 0;
76910
+ if (!uName || soft === void 0 || hard === void 0)
76911
+ continue;
76912
+ ulimits.push({ name: uName, softLimit: soft, hardLimit: hard });
76913
+ }
76914
+ }
76915
+ const out = {
76916
+ name,
76917
+ image,
76918
+ environment,
76919
+ secrets,
76920
+ portMappings,
76921
+ mountPoints,
76922
+ dependsOn,
76923
+ links,
76924
+ essential,
76925
+ ulimits
76926
+ };
76927
+ if (command !== void 0)
76928
+ out.command = command;
76929
+ if (entryPoint !== void 0)
76930
+ out.entryPoint = entryPoint;
76931
+ if (workingDirectory !== void 0)
76932
+ out.workingDirectory = workingDirectory;
76933
+ if (healthCheck !== void 0)
76934
+ out.healthCheck = healthCheck;
76935
+ if (user !== void 0)
76936
+ out.user = user;
76937
+ if (privileged !== void 0)
76938
+ out.privileged = privileged;
76939
+ if (readonlyRootFilesystem !== void 0)
76940
+ out.readonlyRootFilesystem = readonlyRootFilesystem;
76941
+ return out;
76942
+ }
76943
+ function parseContainerImage(raw, containerName, taskLogicalId, resources, _stack) {
76944
+ const flat = extractImageString(raw);
76945
+ if (!flat) {
76946
+ throw new EcsTaskResolutionError(
76947
+ `Container '${containerName}' in task '${taskLogicalId}' has an unparseable Image property. cdkd local run-task v1 supports flat string images, single-key Fn::Sub bodies, and CDK-asset Image references.`
76948
+ );
76949
+ }
76950
+ if (flat.includes("cdk-hnb659fds-container-assets-")) {
76951
+ const hashMatch = /:([a-f0-9]{8,})$/.exec(flat);
76952
+ const out = { kind: "cdk-asset" };
76953
+ if (hashMatch)
76954
+ out.assetHash = hashMatch[1];
76955
+ return out;
76956
+ }
76957
+ const ecrMatch = /^(\d{12})\.dkr\.ecr\.([^.]+)\.amazonaws\.com(?:\.cn)?\//.exec(flat);
76958
+ if (ecrMatch) {
76959
+ return { kind: "ecr", uri: flat, account: ecrMatch[1], region: ecrMatch[2] };
76960
+ }
76961
+ if (flat.includes("${") && flat.includes("AWS::AccountId")) {
76962
+ throw new EcsTaskResolutionError(
76963
+ `Container '${containerName}' in task '${taskLogicalId}' has an Image that references AWS pseudo parameters (${flat}). cdkd local run-task v1 cannot resolve account-scoped ECR repos at synth time. Build the image locally (CDK ContainerImage.fromAsset) or pin to a public image to test locally.`
76964
+ );
76965
+ }
76966
+ for (const [refLogicalId, res] of Object.entries(resources)) {
76967
+ if (res.Type === "AWS::ECR::Repository" && flat.includes(refLogicalId)) {
76968
+ throw new EcsTaskResolutionError(
76969
+ `Container '${containerName}' in task '${taskLogicalId}' references same-stack ECR repository '${refLogicalId}'. cdkd local run-task v1 cannot resolve the repository URI without state \u2014 build via ContainerImage.fromAsset or pin a public image.`
76970
+ );
76971
+ }
76972
+ }
76973
+ return { kind: "public", uri: flat };
76974
+ }
76975
+ function extractImageString(value) {
76976
+ if (typeof value === "string" && value.length > 0)
76977
+ return value;
76978
+ if (value && typeof value === "object") {
76979
+ const obj = value;
76980
+ const sub = obj["Fn::Sub"];
76981
+ if (typeof sub === "string" && sub.length > 0)
76982
+ return sub;
76983
+ if (Array.isArray(sub) && typeof sub[0] === "string")
76984
+ return sub[0];
76985
+ const join11 = obj["Fn::Join"];
76986
+ if (Array.isArray(join11) && join11.length === 2 && Array.isArray(join11[1])) {
76987
+ const sep = typeof join11[0] === "string" ? join11[0] : "";
76988
+ const parts = join11[1].map((p) => typeof p === "string" ? p : extractImageString(p)).filter((p) => p !== void 0);
76989
+ if (parts.length === join11[1].length)
76990
+ return parts.join(sep);
76991
+ }
76992
+ }
76993
+ return void 0;
76994
+ }
76995
+ function parseVolume(raw, idx, taskLogicalId) {
76996
+ if (!raw || typeof raw !== "object") {
76997
+ throw new EcsTaskResolutionError(`Task '${taskLogicalId}' Volumes[${idx}] is not an object.`);
76998
+ }
76999
+ const v = raw;
77000
+ const name = pickString(v["Name"]);
77001
+ if (!name) {
77002
+ throw new EcsTaskResolutionError(`Task '${taskLogicalId}' Volumes[${idx}] has no Name.`);
77003
+ }
77004
+ if (v["EFSVolumeConfiguration"]) {
77005
+ throw new EcsTaskResolutionError(
77006
+ `Task '${taskLogicalId}' Volumes[${idx}] '${name}' uses EFSVolumeConfiguration, which cdkd local run-task cannot proxy locally. Workaround: bind-mount a local directory at the same containerPath via Host: { SourcePath: '<local-path>' }, or override at runtime via --env-vars semantics for a Phase 2 follow-up.`
77007
+ );
77008
+ }
77009
+ if (v["FSxWindowsFileServerVolumeConfiguration"]) {
77010
+ throw new EcsTaskResolutionError(
77011
+ `Task '${taskLogicalId}' Volumes[${idx}] '${name}' uses FSxWindowsFileServerVolumeConfiguration, which cdkd local run-task cannot proxy locally.`
77012
+ );
77013
+ }
77014
+ const dockerCfg = v["DockerVolumeConfiguration"];
77015
+ if (dockerCfg && typeof dockerCfg === "object") {
77016
+ const d = dockerCfg;
77017
+ const scope = pickString(d["Scope"]) === "shared" ? "shared" : "task";
77018
+ const cfg = { scope };
77019
+ if (typeof d["Autoprovision"] === "boolean")
77020
+ cfg.autoprovision = d["Autoprovision"];
77021
+ const driver = pickString(d["Driver"]);
77022
+ if (driver)
77023
+ cfg.driver = driver;
77024
+ if (d["DriverOpts"] && typeof d["DriverOpts"] === "object") {
77025
+ const opts = {};
77026
+ for (const [k, val] of Object.entries(d["DriverOpts"])) {
77027
+ if (typeof val === "string")
77028
+ opts[k] = val;
77029
+ }
77030
+ cfg.driverOpts = opts;
77031
+ }
77032
+ if (d["Labels"] && typeof d["Labels"] === "object") {
77033
+ const labels = {};
77034
+ for (const [k, val] of Object.entries(d["Labels"])) {
77035
+ if (typeof val === "string")
77036
+ labels[k] = val;
77037
+ }
77038
+ cfg.labels = labels;
77039
+ }
77040
+ return { name, kind: "docker", dockerVolumeConfig: cfg };
77041
+ }
77042
+ const host = v["Host"];
77043
+ if (host && typeof host === "object") {
77044
+ const sourcePath = pickString(host["SourcePath"]);
77045
+ if (sourcePath) {
77046
+ const abs = isAbsolute4(sourcePath) ? sourcePath : resolve8(process.cwd(), sourcePath);
77047
+ return { name, kind: "host", hostPath: abs };
77048
+ }
77049
+ }
77050
+ return { name, kind: "host" };
77051
+ }
77052
+ function resolveRoleArn(value, resources) {
77053
+ if (value === void 0 || value === null)
77054
+ return void 0;
77055
+ if (typeof value === "string")
77056
+ return value;
77057
+ if (typeof value !== "object")
77058
+ return void 0;
77059
+ const obj = value;
77060
+ if ("Ref" in obj && typeof obj["Ref"] === "string") {
77061
+ const refLogicalId = obj["Ref"];
77062
+ const role = resources[refLogicalId];
77063
+ if (role?.Type === "AWS::IAM::Role") {
77064
+ return void 0;
77065
+ }
77066
+ }
77067
+ if ("Fn::GetAtt" in obj) {
77068
+ const arg = obj["Fn::GetAtt"];
77069
+ if (Array.isArray(arg) && typeof arg[0] === "string") {
77070
+ const refLogicalId = arg[0];
77071
+ const role = resources[refLogicalId];
77072
+ if (role?.Type === "AWS::IAM::Role") {
77073
+ return void 0;
77074
+ }
77075
+ }
77076
+ }
77077
+ return void 0;
77078
+ }
77079
+ function pickString(value) {
77080
+ return typeof value === "string" && value.length > 0 ? value : void 0;
77081
+ }
77082
+ function pickStringArray2(value) {
77083
+ if (!Array.isArray(value))
77084
+ return void 0;
77085
+ const out = [];
77086
+ for (const v of value) {
77087
+ if (typeof v === "string")
77088
+ out.push(v);
77089
+ }
77090
+ return out;
77091
+ }
77092
+ function notFoundError2(target, stack, resources) {
77093
+ const tasks = [];
77094
+ for (const [logicalId, resource] of Object.entries(resources)) {
77095
+ if (resource.Type !== "AWS::ECS::TaskDefinition")
77096
+ continue;
77097
+ const meta = resource.Metadata;
77098
+ const cdkPath = typeof meta?.["aws:cdk:path"] === "string" ? meta["aws:cdk:path"] : "";
77099
+ tasks.push({ displayPath: cdkPath || logicalId, logicalId });
77100
+ }
77101
+ let msg = `target '${target}' did not match any ECS task definition in ${stack.stackName}.
77102
+
77103
+ `;
77104
+ if (tasks.length === 0) {
77105
+ msg += `Stack ${stack.stackName} has no AWS::ECS::TaskDefinition resources.`;
77106
+ } else {
77107
+ const width = Math.max(...tasks.map((t) => t.displayPath.length));
77108
+ msg += `Available task definitions in ${stack.stackName}:
77109
+ `;
77110
+ for (const t of tasks) {
77111
+ msg += ` ${t.displayPath.padEnd(width)} (${t.logicalId})
77112
+ `;
77113
+ }
77114
+ }
77115
+ return new EcsTaskResolutionError(msg.trimEnd());
77116
+ }
77117
+ function checkVolumeHostPath(hostPath) {
77118
+ try {
77119
+ return existsSync5(hostPath) && statSync4(hostPath).isDirectory();
77120
+ } catch {
77121
+ return false;
77122
+ }
77123
+ }
77124
+
77125
+ // src/local/ecs-task-runner.ts
77126
+ import { execFile as execFile6, spawn as spawn5 } from "node:child_process";
77127
+ import { randomBytes as randomBytes2 } from "node:crypto";
77128
+ import { dirname as dirname6 } from "node:path";
77129
+ import { promisify as promisify6 } from "node:util";
77130
+ import graphlib2 from "graphlib";
77131
+
77132
+ // src/local/ecs-network.ts
77133
+ import { execFile as execFile5 } from "node:child_process";
77134
+ import { randomBytes } from "node:crypto";
77135
+ import { promisify as promisify5 } from "node:util";
77136
+ var execFileAsync5 = promisify5(execFile5);
77137
+ var METADATA_ENDPOINT_IMAGE = "amazon/amazon-ecs-local-container-endpoints:latest-amd64";
77138
+ var METADATA_ENDPOINT_IP = "169.254.170.2";
77139
+ var METADATA_ENDPOINT_SUBNET = "169.254.170.0/24";
77140
+ async function createTaskNetwork(options = {}) {
77141
+ const logger = getLogger().child("ecs-network");
77142
+ const prefix = options.prefix ?? "cdkd-local";
77143
+ const suffix = randomBytes(4).toString("hex");
77144
+ const networkName = `${prefix}-task-${suffix}`;
77145
+ await pullImage(METADATA_ENDPOINT_IMAGE, options.skipPull ?? false);
77146
+ logger.info(`Creating docker network ${networkName} (subnet ${METADATA_ENDPOINT_SUBNET})...`);
77147
+ try {
77148
+ await execFileAsync5("docker", [
77149
+ "network",
77150
+ "create",
77151
+ "--driver",
77152
+ "bridge",
77153
+ "--subnet",
77154
+ METADATA_ENDPOINT_SUBNET,
77155
+ networkName
77156
+ ]);
77157
+ } catch (err) {
77158
+ const e = err;
77159
+ throw new DockerRunnerError(
77160
+ `docker network create failed: ${e.stderr?.trim() || e.message || String(err)}. Hint: another cdkd run-task on the same host may already own subnet ${METADATA_ENDPOINT_SUBNET}; wait for it to finish, or remove the leftover network with \`docker network ls\` + \`docker network rm\`.`
77161
+ );
77162
+ }
77163
+ const sidecarArgs = [
77164
+ "run",
77165
+ "-d",
77166
+ "--rm",
77167
+ "--name",
77168
+ `${networkName}-metadata`,
77169
+ "--network",
77170
+ networkName,
77171
+ "--ip",
77172
+ METADATA_ENDPOINT_IP
77173
+ ];
77174
+ const sidecarEnv = {};
77175
+ if (options.credentials) {
77176
+ sidecarEnv["AWS_ACCESS_KEY_ID"] = options.credentials.accessKeyId;
77177
+ sidecarEnv["AWS_SECRET_ACCESS_KEY"] = options.credentials.secretAccessKey;
77178
+ if (options.credentials.sessionToken) {
77179
+ sidecarEnv["AWS_SESSION_TOKEN"] = options.credentials.sessionToken;
77180
+ }
77181
+ }
77182
+ if (options.cluster)
77183
+ sidecarEnv["CLUSTER"] = options.cluster;
77184
+ for (const [k, v] of Object.entries(sidecarEnv)) {
77185
+ sidecarArgs.push("-e", `${k}=${v}`);
77186
+ }
77187
+ sidecarArgs.push(METADATA_ENDPOINT_IMAGE);
77188
+ logger.info("Starting ECS local-container-endpoints sidecar at 169.254.170.2...");
77189
+ let sidecarContainerId;
77190
+ try {
77191
+ const { stdout } = await execFileAsync5("docker", sidecarArgs, {
77192
+ maxBuffer: 10 * 1024 * 1024
77193
+ });
77194
+ sidecarContainerId = stdout.trim();
77195
+ } catch (err) {
77196
+ await destroyNetworkOnly(networkName);
77197
+ const e = err;
77198
+ throw new DockerRunnerError(
77199
+ `Failed to start metadata-endpoints sidecar: ${e.stderr?.trim() || e.message || String(err)}`
77200
+ );
77201
+ }
77202
+ return { networkName, sidecarContainerId };
77203
+ }
77204
+ function buildMetadataEnv(opts) {
77205
+ const env = {
77206
+ ECS_CONTAINER_METADATA_URI_V4: `http://${METADATA_ENDPOINT_IP}/v4/${opts.containerName}`,
77207
+ ECS_CONTAINER_METADATA_URI: `http://${METADATA_ENDPOINT_IP}/v3/${opts.containerName}`
77208
+ };
77209
+ if (opts.roleArn) {
77210
+ env["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"] = `/role/${encodeURIComponent(opts.roleArn)}`;
77211
+ }
77212
+ if (opts.region)
77213
+ env["AWS_REGION"] = opts.region;
77214
+ return env;
77215
+ }
77216
+ async function destroyTaskNetwork(net) {
77217
+ if (!net)
77218
+ return;
77219
+ await removeContainer(net.sidecarContainerId);
77220
+ await destroyNetworkOnly(net.networkName);
77221
+ }
77222
+ async function destroyNetworkOnly(networkName) {
77223
+ if (!networkName)
77224
+ return;
77225
+ const logger = getLogger().child("ecs-network");
77226
+ try {
77227
+ await execFileAsync5("docker", ["network", "rm", networkName]);
77228
+ logger.debug(`Removed docker network ${networkName}`);
77229
+ } catch (err) {
77230
+ const e = err;
77231
+ logger.debug(
77232
+ `docker network rm ${networkName} failed: ${e.stderr || e.message || String(err)}`
77233
+ );
77234
+ }
77235
+ }
77236
+
77237
+ // src/local/ecs-secrets-resolver.ts
77238
+ import { SecretsManagerClient as SecretsManagerClient3, GetSecretValueCommand as GetSecretValueCommand2 } from "@aws-sdk/client-secrets-manager";
77239
+ import { SSMClient as SSMClient4, GetParameterCommand as GetParameterCommand4 } from "@aws-sdk/client-ssm";
77240
+ var EcsSecretsResolutionError = class _EcsSecretsResolutionError extends Error {
77241
+ constructor(message) {
77242
+ super(message);
77243
+ this.name = "EcsSecretsResolutionError";
77244
+ Object.setPrototypeOf(this, _EcsSecretsResolutionError.prototype);
77245
+ }
77246
+ };
77247
+ async function resolveEcsSecrets(entries, options = {}) {
77248
+ if (entries.length === 0)
77249
+ return [];
77250
+ const logger = getLogger().child("ecs-secrets");
77251
+ const secretsClient = options.secretsManagerClient ?? new SecretsManagerClient3({ ...options.region && { region: options.region } });
77252
+ const ssmClient = options.ssmClient ?? new SSMClient4({ ...options.region && { region: options.region } });
77253
+ const ownsSecretsClient = options.secretsManagerClient === void 0;
77254
+ const ownsSsmClient = options.ssmClient === void 0;
77255
+ try {
77256
+ const results = await Promise.all(
77257
+ entries.map(async (entry) => {
77258
+ const value = await resolveOne(entry, secretsClient, ssmClient);
77259
+ logger.debug(`Resolved secret ${entry.containerName}.${entry.name} (${entry.valueFrom})`);
77260
+ return { ...entry, value };
77261
+ })
77262
+ );
77263
+ return results;
77264
+ } finally {
77265
+ if (ownsSecretsClient)
77266
+ secretsClient.destroy();
77267
+ if (ownsSsmClient)
77268
+ ssmClient.destroy();
77269
+ }
77270
+ }
77271
+ async function resolveOne(entry, secretsClient, ssmClient) {
77272
+ const arn = entry.valueFrom;
77273
+ const shape = classifySecretArn(arn);
77274
+ switch (shape.kind) {
77275
+ case "secrets-manager":
77276
+ return resolveSecretsManager(entry, shape, secretsClient);
77277
+ case "ssm":
77278
+ return resolveSsm(entry, shape, ssmClient);
77279
+ case "unknown":
77280
+ throw new EcsSecretsResolutionError(
77281
+ `Container '${entry.containerName}' secret '${entry.name}' references an unsupported ValueFrom shape '${arn}'. Expected Secrets Manager ARN (optionally with :<json-key>::) or SSM Parameter ARN.`
77282
+ );
77283
+ }
77284
+ }
77285
+ function classifySecretArn(arn) {
77286
+ if (!arn.startsWith("arn:"))
77287
+ return { kind: "unknown" };
77288
+ const smMatch = /^(arn:[^:]+:secretsmanager:[^:]+:\d+:secret:[^:]+)(?::([^:]+)::?)?$/.exec(arn);
77289
+ if (smMatch) {
77290
+ const out = { kind: "secrets-manager", baseArn: smMatch[1] };
77291
+ if (smMatch[2])
77292
+ out.jsonKey = smMatch[2];
77293
+ return out;
77294
+ }
77295
+ const ssmMatch = /^arn:[^:]+:ssm:[^:]+:\d+:parameter(\/.+)$/.exec(arn);
77296
+ if (ssmMatch) {
77297
+ return { kind: "ssm", name: ssmMatch[1] };
77298
+ }
77299
+ return { kind: "unknown" };
77300
+ }
77301
+ async function resolveSecretsManager(entry, shape, client) {
77302
+ let secretString;
77303
+ try {
77304
+ const resp = await client.send(new GetSecretValueCommand2({ SecretId: shape.baseArn }));
77305
+ secretString = resp.SecretString;
77306
+ } catch (err) {
77307
+ throw new EcsSecretsResolutionError(
77308
+ `Failed to resolve Secrets Manager secret for container '${entry.containerName}' / env '${entry.name}' (${shape.baseArn}): ${err instanceof Error ? err.message : String(err)}`
77309
+ );
77310
+ }
77311
+ if (secretString === void 0) {
77312
+ throw new EcsSecretsResolutionError(
77313
+ `Secrets Manager returned no SecretString for container '${entry.containerName}' / env '${entry.name}' (${shape.baseArn}). Binary secrets are not supported.`
77314
+ );
77315
+ }
77316
+ if (shape.jsonKey === void 0)
77317
+ return secretString;
77318
+ let parsed;
77319
+ try {
77320
+ parsed = JSON.parse(secretString);
77321
+ } catch (err) {
77322
+ throw new EcsSecretsResolutionError(
77323
+ `Container '${entry.containerName}' secret '${entry.name}' specified json-key '${shape.jsonKey}' but the secret value is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
77324
+ );
77325
+ }
77326
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
77327
+ throw new EcsSecretsResolutionError(
77328
+ `Container '${entry.containerName}' secret '${entry.name}' specified json-key '${shape.jsonKey}' but the secret root is not a JSON object.`
77329
+ );
77330
+ }
77331
+ const value = parsed[shape.jsonKey];
77332
+ if (value === void 0) {
77333
+ throw new EcsSecretsResolutionError(
77334
+ `Container '${entry.containerName}' secret '${entry.name}' specified json-key '${shape.jsonKey}' but no such key exists in the secret JSON.`
77335
+ );
77336
+ }
77337
+ return typeof value === "string" ? value : JSON.stringify(value);
77338
+ }
77339
+ async function resolveSsm(entry, shape, client) {
77340
+ try {
77341
+ const resp = await client.send(
77342
+ new GetParameterCommand4({ Name: shape.name, WithDecryption: true })
77343
+ );
77344
+ const value = resp.Parameter?.Value;
77345
+ if (value === void 0) {
77346
+ throw new EcsSecretsResolutionError(
77347
+ `SSM parameter '${shape.name}' returned no Value for container '${entry.containerName}' / env '${entry.name}'.`
77348
+ );
77349
+ }
77350
+ return value;
77351
+ } catch (err) {
77352
+ if (err instanceof EcsSecretsResolutionError)
77353
+ throw err;
77354
+ throw new EcsSecretsResolutionError(
77355
+ `Failed to resolve SSM parameter for container '${entry.containerName}' / env '${entry.name}' (${shape.name}): ${err instanceof Error ? err.message : String(err)}`
77356
+ );
77357
+ }
77358
+ }
77359
+
77360
+ // src/local/ecs-task-runner.ts
77361
+ var execFileAsync6 = promisify6(execFile6);
77362
+ var EcsTaskRunnerError = class _EcsTaskRunnerError extends Error {
77363
+ constructor(message) {
77364
+ super(message);
77365
+ this.name = "EcsTaskRunnerError";
77366
+ Object.setPrototypeOf(this, _EcsTaskRunnerError.prototype);
77367
+ }
77368
+ };
77369
+ function createEcsRunState() {
77370
+ return { network: void 0, dockerVolumeNames: [], startedContainers: [], logStoppers: [] };
77371
+ }
77372
+ async function cleanupEcsRun(state, options) {
77373
+ const logger = getLogger().child("ecs-runner");
77374
+ for (const stop of state.logStoppers) {
77375
+ try {
77376
+ stop();
77377
+ } catch (err) {
77378
+ logger.debug(`log stream stop failed: ${err instanceof Error ? err.message : String(err)}`);
77379
+ }
77380
+ }
77381
+ state.logStoppers = [];
77382
+ if (!options.keepRunning) {
77383
+ for (const c of state.startedContainers) {
77384
+ try {
77385
+ await stopContainer(c.id, 10);
77386
+ } catch (err) {
77387
+ logger.debug(
77388
+ `docker stop ${c.id} failed: ${err instanceof Error ? err.message : String(err)}`
77389
+ );
77390
+ }
77391
+ try {
77392
+ await removeContainer(c.id);
77393
+ } catch (err) {
77394
+ logger.debug(
77395
+ `docker rm -f ${c.id} failed: ${err instanceof Error ? err.message : String(err)}`
77396
+ );
77397
+ }
77398
+ }
77399
+ state.startedContainers = [];
77400
+ }
77401
+ await destroyTaskNetwork(state.network);
77402
+ state.network = void 0;
77403
+ for (const v of state.dockerVolumeNames) {
77404
+ try {
77405
+ await execFileAsync6("docker", ["volume", "rm", v]);
77406
+ logger.debug(`Removed docker volume ${v}`);
77407
+ } catch (err) {
77408
+ logger.debug(
77409
+ `docker volume rm ${v} failed: ${err instanceof Error ? err.message : String(err)}`
77410
+ );
77411
+ }
77412
+ }
77413
+ state.dockerVolumeNames = [];
77414
+ }
77415
+ async function runEcsTask(task, options, state) {
77416
+ const logger = getLogger();
77417
+ if (task.containers.length === 0) {
77418
+ throw new EcsTaskRunnerError(
77419
+ `Task '${task.taskDefinitionLogicalId}' has no containers \u2014 nothing to run.`
77420
+ );
77421
+ }
77422
+ for (const w of task.warnings)
77423
+ logger.warn(w);
77424
+ const dag = buildDependencyGraph(task.containers);
77425
+ const startOrder = topoSort(dag, task.containers);
77426
+ const imagePlan = options.imagePlanByContainer ?? /* @__PURE__ */ new Map();
77427
+ if (!options.imagePlanByContainer) {
77428
+ await prepareImages(task, imagePlan, options);
77429
+ }
77430
+ const allSecrets = [];
77431
+ for (const c of task.containers) {
77432
+ for (const s of c.secrets) {
77433
+ allSecrets.push({ containerName: c.name, name: s.name, valueFrom: s.valueFrom });
77434
+ }
77435
+ }
77436
+ const resolvedSecrets = await resolveEcsSecrets(allSecrets, {
77437
+ ...options.region !== void 0 && { region: options.region }
77438
+ });
77439
+ const secretsByContainer = groupSecretsByContainer(resolvedSecrets);
77440
+ const netCreateOpts = {
77441
+ prefix: options.cluster,
77442
+ skipPull: options.skipPull
77443
+ };
77444
+ if (options.taskCredentials)
77445
+ netCreateOpts.credentials = options.taskCredentials;
77446
+ if (options.cluster)
77447
+ netCreateOpts.cluster = options.cluster;
77448
+ state.network = await createTaskNetwork(netCreateOpts);
77449
+ const volumeByName = await realizeDockerVolumes(task.volumes, state);
77450
+ const dockerCmds = /* @__PURE__ */ new Map();
77451
+ for (const container of task.containers) {
77452
+ const image = imagePlan.get(container.name);
77453
+ if (!image) {
77454
+ throw new EcsTaskRunnerError(
77455
+ `Internal: no resolved image for container '${container.name}'.`
77456
+ );
77457
+ }
77458
+ dockerCmds.set(
77459
+ container.name,
77460
+ buildDockerRunArgs({
77461
+ task,
77462
+ container,
77463
+ image,
77464
+ network: state.network.networkName,
77465
+ volumeByName,
77466
+ secrets: secretsByContainer.get(container.name) ?? [],
77467
+ envOverrides: options.envOverrides,
77468
+ containerHost: options.containerHost,
77469
+ roleArn: options.taskRoleArn,
77470
+ platformOverride: options.platformOverride,
77471
+ region: options.region
77472
+ })
77473
+ );
77474
+ }
77475
+ const startedByName = /* @__PURE__ */ new Map();
77476
+ for (const containerName of startOrder) {
77477
+ const container = task.containers.find((c) => c.name === containerName);
77478
+ await awaitDependencies(container, startedByName);
77479
+ const args = dockerCmds.get(container.name);
77480
+ logger.info(`Starting container '${container.name}' (image=${imagePlan.get(container.name)})`);
77481
+ let id;
77482
+ try {
77483
+ const { stdout } = await execFileAsync6("docker", args, { maxBuffer: 10 * 1024 * 1024 });
77484
+ id = stdout.trim();
77485
+ } catch (err) {
77486
+ const e = err;
77487
+ throw new DockerRunnerError(
77488
+ `docker run failed for container '${container.name}': ${e.stderr?.trim() || e.message || String(err)}`
77489
+ );
77490
+ }
77491
+ state.startedContainers.push({ name: container.name, id });
77492
+ startedByName.set(container.name, { id, container });
77493
+ if (!options.detach) {
77494
+ state.logStoppers.push(streamContainerLogs(container.name, id));
77495
+ }
77496
+ }
77497
+ if (options.detach) {
77498
+ return { exitCode: 0, state };
77499
+ }
77500
+ const essential = task.containers.find((c) => c.essential) ?? task.containers[0];
77501
+ const essentialId = startedByName.get(essential.name)?.id;
77502
+ if (!essentialId) {
77503
+ throw new EcsTaskRunnerError(`Essential container '${essential.name}' did not start.`);
77504
+ }
77505
+ const exitCode = await waitForContainerExit(essentialId);
77506
+ return { exitCode, essentialContainerName: essential.name, state };
77507
+ }
77508
+ function buildDependencyGraph(containers) {
77509
+ const g = new graphlib2.Graph({ directed: true });
77510
+ for (const c of containers)
77511
+ g.setNode(c.name);
77512
+ for (const c of containers) {
77513
+ for (const d of c.dependsOn) {
77514
+ g.setEdge(c.name, d.containerName);
77515
+ }
77516
+ }
77517
+ const cycles = graphlib2.alg.findCycles(g);
77518
+ if (cycles.length > 0) {
77519
+ throw new EcsTaskRunnerError(
77520
+ `Cyclic DependsOn detected: ${cycles.map((c) => c.join(" -> ")).join("; ")}`
77521
+ );
77522
+ }
77523
+ return g;
77524
+ }
77525
+ function topoSort(g, containers) {
77526
+ const sorted = graphlib2.alg.topsort(g);
77527
+ const byPosition = /* @__PURE__ */ new Map();
77528
+ containers.forEach((c, idx) => byPosition.set(c.name, idx));
77529
+ return [...sorted].reverse().sort((a, b) => {
77530
+ return (byPosition.get(a) ?? 0) - (byPosition.get(b) ?? 0);
77531
+ });
77532
+ }
77533
+ async function awaitDependencies(container, started) {
77534
+ for (const dep of container.dependsOn) {
77535
+ const entry = started.get(dep.containerName);
77536
+ if (!entry) {
77537
+ throw new EcsTaskRunnerError(
77538
+ `Container '${container.name}' depends on '${dep.containerName}' but the latter never started.`
77539
+ );
77540
+ }
77541
+ switch (dep.condition) {
77542
+ case "START":
77543
+ break;
77544
+ case "COMPLETE":
77545
+ await waitForContainerExit(entry.id);
77546
+ break;
77547
+ case "SUCCESS": {
77548
+ const code = await waitForContainerExit(entry.id);
77549
+ if (code !== 0) {
77550
+ throw new EcsTaskRunnerError(
77551
+ `Container '${container.name}' requires dependency '${dep.containerName}' to exit 0, but it exited ${code}.`
77552
+ );
77553
+ }
77554
+ break;
77555
+ }
77556
+ case "HEALTHY":
77557
+ await waitForContainerHealthy(entry.id, dep.containerName);
77558
+ break;
77559
+ }
77560
+ }
77561
+ }
77562
+ async function waitForContainerHealthy(containerId, displayName) {
77563
+ const logger = getLogger().child("ecs-runner");
77564
+ const deadline = Date.now() + 5 * 60 * 1e3;
77565
+ let lastStatus = "";
77566
+ while (Date.now() < deadline) {
77567
+ try {
77568
+ const { stdout } = await execFileAsync6("docker", [
77569
+ "inspect",
77570
+ "--format",
77571
+ "{{.State.Health.Status}}",
77572
+ containerId
77573
+ ]);
77574
+ const status = stdout.trim();
77575
+ if (status !== lastStatus) {
77576
+ logger.debug(`Container '${displayName}' health status: ${status}`);
77577
+ lastStatus = status;
77578
+ }
77579
+ if (status === "healthy")
77580
+ return;
77581
+ if (status === "unhealthy") {
77582
+ throw new EcsTaskRunnerError(
77583
+ `Container '${displayName}' health status is 'unhealthy'; aborting before dependents start.`
77584
+ );
77585
+ }
77586
+ } catch (err) {
77587
+ if (err instanceof EcsTaskRunnerError)
77588
+ throw err;
77589
+ logger.debug(
77590
+ `docker inspect on '${displayName}' failed: ${err instanceof Error ? err.message : String(err)}`
77591
+ );
77592
+ }
77593
+ await sleep(1e3);
77594
+ }
77595
+ throw new EcsTaskRunnerError(
77596
+ `Container '${displayName}' did not become healthy within 5 minutes.`
77597
+ );
77598
+ }
77599
+ async function waitForContainerExit(containerId) {
77600
+ try {
77601
+ const { stdout } = await execFileAsync6("docker", ["wait", containerId], {
77602
+ maxBuffer: 1024 * 1024
77603
+ });
77604
+ const code = Number.parseInt(stdout.trim(), 10);
77605
+ return Number.isFinite(code) ? code : 1;
77606
+ } catch (err) {
77607
+ const e = err;
77608
+ throw new DockerRunnerError(
77609
+ `docker wait failed: ${e.stderr?.trim() || e.message || String(err)}`
77610
+ );
77611
+ }
77612
+ }
77613
+ async function stopContainer(containerId, graceSeconds) {
77614
+ try {
77615
+ await execFileAsync6("docker", ["stop", "-t", String(graceSeconds), containerId]);
77616
+ } catch {
77617
+ }
77618
+ }
77619
+ function sleep(ms) {
77620
+ return new Promise((res) => setTimeout(res, ms));
77621
+ }
77622
+ function streamContainerLogs(containerName, containerId) {
77623
+ const proc = spawn5("docker", ["logs", "-f", containerId], {
77624
+ stdio: ["ignore", "pipe", "pipe"]
77625
+ });
77626
+ const prefix = `[${containerName}] `;
77627
+ let stdoutBuf = "";
77628
+ let stderrBuf = "";
77629
+ proc.stdout?.on("data", (chunk) => {
77630
+ stdoutBuf = writePrefixed(prefix, stdoutBuf + chunk.toString("utf-8"), process.stdout);
77631
+ });
77632
+ proc.stderr?.on("data", (chunk) => {
77633
+ stderrBuf = writePrefixed(prefix, stderrBuf + chunk.toString("utf-8"), process.stderr);
77634
+ });
77635
+ proc.on("error", () => {
77636
+ });
77637
+ return () => {
77638
+ if (stdoutBuf)
77639
+ process.stdout.write(prefix + stdoutBuf + "\n");
77640
+ if (stderrBuf)
77641
+ process.stderr.write(prefix + stderrBuf + "\n");
77642
+ if (!proc.killed)
77643
+ proc.kill("SIGTERM");
77644
+ };
77645
+ }
77646
+ function writePrefixed(prefix, buffer, out) {
77647
+ const lines = buffer.split("\n");
77648
+ const remainder = lines.pop() ?? "";
77649
+ for (const line of lines) {
77650
+ out.write(prefix + line + "\n");
77651
+ }
77652
+ return remainder;
77653
+ }
77654
+ async function prepareImages(task, out, options) {
77655
+ const logger = getLogger().child("ecs-runner");
77656
+ for (const container of task.containers) {
77657
+ const image = await prepareOneImage(task, container, options);
77658
+ out.set(container.name, image);
77659
+ logger.debug(`Container '${container.name}' image=${image}`);
77660
+ }
77661
+ }
77662
+ async function prepareOneImage(task, container, options) {
77663
+ const image = container.image;
77664
+ switch (image.kind) {
77665
+ case "public": {
77666
+ await pullImage(image.uri, options.skipPull);
77667
+ return image.uri;
77668
+ }
77669
+ case "ecr": {
77670
+ return pullEcrImage(image.uri, {
77671
+ skipPull: options.skipPull,
77672
+ ...options.region !== void 0 && { region: options.region }
77673
+ });
77674
+ }
77675
+ case "cdk-asset": {
77676
+ const cdkOutDir = task.stack.assetManifestPath ? dirname6(task.stack.assetManifestPath) : void 0;
77677
+ if (!cdkOutDir) {
77678
+ throw new EcsTaskRunnerError(
77679
+ `Container '${container.name}' uses a CDK asset image but the stack has no asset manifest. Re-synthesize the app (without \`--output <stale-dir>\`) and retry.`
77680
+ );
77681
+ }
77682
+ const loader = new AssetManifestLoader();
77683
+ const manifest = await loader.loadManifest(cdkOutDir, task.stack.stackName);
77684
+ if (!manifest) {
77685
+ throw new EcsTaskRunnerError(
77686
+ `No asset manifest at ${cdkOutDir} for stack ${task.stack.stackName}.`
77687
+ );
77688
+ }
77689
+ const dockerImages = manifest.dockerImages ?? {};
77690
+ const entries = Object.entries(dockerImages);
77691
+ let asset;
77692
+ if (image.assetHash && dockerImages[image.assetHash]) {
77693
+ asset = dockerImages[image.assetHash];
77694
+ } else if (entries.length === 1) {
77695
+ asset = entries[0][1];
77696
+ }
77697
+ if (!asset) {
77698
+ throw new EcsTaskRunnerError(
77699
+ `Container '${container.name}' references a CDK asset image but no matching entry was found in cdk.out. Re-synthesize the CDK app and retry.`
77700
+ );
77701
+ }
77702
+ const tag = `cdkd-local-run-task-${(image.assetHash ?? "single").slice(0, 16)}`;
77703
+ await buildDockerImage(asset, cdkOutDir, tag, {
77704
+ ...options.platformOverride !== void 0 && { platform: options.platformOverride },
77705
+ wrapError: (stderr) => new LocalInvokeBuildError(
77706
+ `docker build failed for ECS container '${container.name}' (${asset.source.directory}): ${stderr}`
77707
+ )
77708
+ });
77709
+ return tag;
77710
+ }
77711
+ }
77712
+ }
77713
+ async function realizeDockerVolumes(volumes, state) {
77714
+ const logger = getLogger().child("ecs-runner");
77715
+ const out = /* @__PURE__ */ new Map();
77716
+ for (const v of volumes) {
77717
+ if (v.kind === "host") {
77718
+ if (v.hostPath && !checkVolumeHostPath(v.hostPath)) {
77719
+ logger.warn(
77720
+ `Volume '${v.name}': host path '${v.hostPath}' does not exist or is not a directory. Docker will create an anonymous bind mount; create the host path before run-task if you expected to bind-mount it.`
77721
+ );
77722
+ }
77723
+ out.set(v.name, v);
77724
+ continue;
77725
+ }
77726
+ const cfg = v.dockerVolumeConfig;
77727
+ const args = ["volume", "create"];
77728
+ if (cfg?.driver)
77729
+ args.push("--driver", cfg.driver);
77730
+ if (cfg?.driverOpts) {
77731
+ for (const [k, val] of Object.entries(cfg.driverOpts))
77732
+ args.push("--opt", `${k}=${val}`);
77733
+ }
77734
+ if (cfg?.labels) {
77735
+ for (const [k, val] of Object.entries(cfg.labels))
77736
+ args.push("--label", `${k}=${val}`);
77737
+ }
77738
+ const dockerVolumeName = `cdkd-local-${v.name}-${randHex(4)}`;
77739
+ args.push(dockerVolumeName);
77740
+ try {
77741
+ await execFileAsync6("docker", args);
77742
+ state.dockerVolumeNames.push(dockerVolumeName);
77743
+ logger.debug(`Created docker volume ${dockerVolumeName} for task volume '${v.name}'`);
77744
+ } catch (err) {
77745
+ const e = err;
77746
+ throw new DockerRunnerError(
77747
+ `docker volume create failed for '${v.name}': ${e.stderr?.trim() || e.message || String(err)}`
77748
+ );
77749
+ }
77750
+ out.set(v.name, { ...v, dockerVolumeName });
77751
+ }
77752
+ return out;
77753
+ }
77754
+ function randHex(bytes) {
77755
+ return randomBytes2(bytes).toString("hex");
77756
+ }
77757
+ function groupSecretsByContainer(resolved) {
77758
+ const out = /* @__PURE__ */ new Map();
77759
+ for (const r of resolved) {
77760
+ const arr = out.get(r.containerName) ?? [];
77761
+ arr.push({ name: r.name, value: r.value });
77762
+ out.set(r.containerName, arr);
77763
+ }
77764
+ return out;
77765
+ }
77766
+ function buildDockerRunArgs(opts) {
77767
+ const { task, container, image, network, volumeByName, secrets, containerHost, roleArn } = opts;
77768
+ const args = ["run", "-d"];
77769
+ args.push("--name", `cdkd-local-${task.family}-${container.name}-${randHex(3)}`);
77770
+ args.push("--network", network);
77771
+ args.push("--network-alias", container.name);
77772
+ if (opts.platformOverride) {
77773
+ args.push("--platform", opts.platformOverride);
77774
+ } else if (task.runtimePlatform) {
77775
+ args.push(
77776
+ "--platform",
77777
+ task.runtimePlatform.cpuArchitecture === "ARM64" ? "linux/arm64" : "linux/amd64"
77778
+ );
77779
+ }
77780
+ for (const pm of container.portMappings) {
77781
+ const hostPort = pm.hostPort ?? pm.containerPort;
77782
+ args.push("-p", `${containerHost}:${hostPort}:${pm.containerPort}/${pm.protocol}`);
77783
+ }
77784
+ for (const mp of container.mountPoints) {
77785
+ const v = volumeByName.get(mp.sourceVolume);
77786
+ if (!v)
77787
+ continue;
77788
+ if (v.kind === "host") {
77789
+ if (v.hostPath) {
77790
+ const ro = mp.readOnly ? ":ro" : "";
77791
+ args.push("-v", `${v.hostPath}:${mp.containerPath}${ro}`);
77792
+ } else {
77793
+ args.push("-v", mp.containerPath);
77794
+ }
77795
+ } else {
77796
+ const name = v.dockerVolumeName ?? v.name;
77797
+ const ro = mp.readOnly ? ":ro" : "";
77798
+ args.push("-v", `${name}:${mp.containerPath}${ro}`);
77799
+ }
77800
+ }
77801
+ const finalEnv = {};
77802
+ const metaEnv = buildMetadataEnv({
77803
+ containerName: container.name,
77804
+ ...roleArn !== void 0 && { roleArn },
77805
+ ...opts.region !== void 0 && { region: opts.region }
77806
+ });
77807
+ Object.assign(finalEnv, metaEnv);
77808
+ Object.assign(finalEnv, container.environment);
77809
+ for (const s of secrets)
77810
+ finalEnv[s.name] = s.value;
77811
+ const overrides = opts.envOverrides;
77812
+ if (overrides) {
77813
+ applyOverrideMap2(finalEnv, overrides["Parameters"]);
77814
+ applyOverrideMap2(finalEnv, overrides[container.name]);
77815
+ }
77816
+ for (const [k, v] of Object.entries(finalEnv)) {
77817
+ args.push("-e", `${k}=${v}`);
77818
+ }
77819
+ if (container.user)
77820
+ args.push("--user", container.user);
77821
+ if (container.privileged)
77822
+ args.push("--privileged");
77823
+ if (container.readonlyRootFilesystem)
77824
+ args.push("--read-only");
77825
+ if (container.workingDirectory)
77826
+ args.push("--workdir", container.workingDirectory);
77827
+ for (const u of container.ulimits) {
77828
+ args.push("--ulimit", `${u.name}=${u.softLimit}:${u.hardLimit}`);
77829
+ }
77830
+ for (const link of container.links)
77831
+ args.push("--link", link);
77832
+ if (container.healthCheck) {
77833
+ args.push("--health-cmd", shellJoin(container.healthCheck.command));
77834
+ if (container.healthCheck.interval !== void 0) {
77835
+ args.push("--health-interval", `${container.healthCheck.interval}s`);
77836
+ }
77837
+ if (container.healthCheck.timeout !== void 0) {
77838
+ args.push("--health-timeout", `${container.healthCheck.timeout}s`);
77839
+ }
77840
+ if (container.healthCheck.retries !== void 0) {
77841
+ args.push("--health-retries", String(container.healthCheck.retries));
77842
+ }
77843
+ if (container.healthCheck.startPeriod !== void 0) {
77844
+ args.push("--health-start-period", `${container.healthCheck.startPeriod}s`);
77845
+ }
77846
+ }
77847
+ let entryPointTail = [];
77848
+ if (container.entryPoint && container.entryPoint.length > 0) {
77849
+ args.push("--entrypoint", container.entryPoint[0]);
77850
+ entryPointTail = container.entryPoint.slice(1);
77851
+ }
77852
+ args.push(image, ...entryPointTail, ...container.command ?? []);
77853
+ return args;
77854
+ }
77855
+ function applyOverrideMap2(acc, map) {
77856
+ if (!map)
77857
+ return;
77858
+ for (const [k, v] of Object.entries(map)) {
77859
+ if (v === null)
77860
+ delete acc[k];
77861
+ else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
77862
+ acc[k] = String(v);
77863
+ }
77864
+ }
77865
+ }
77866
+ function shellJoin(parts) {
77867
+ return parts.map((p) => {
77868
+ if (/^[A-Za-z0-9_\-./=:]+$/.test(p))
77869
+ return p;
77870
+ return `'${p.replace(/'/g, "'\\''")}'`;
77871
+ }).join(" ");
77872
+ }
77873
+
77874
+ // src/cli/commands/local-run-task.ts
77875
+ async function localRunTaskCommand(target, options) {
77876
+ const logger = getLogger();
77877
+ if (options.verbose)
77878
+ logger.setLevel("debug");
77879
+ warnIfDeprecatedRegion(options);
77880
+ const state = createEcsRunState();
77881
+ let sigintHandler;
77882
+ let sigintCount = 0;
77883
+ let cleanupPromise;
77884
+ const cleanup = async () => {
77885
+ if (!cleanupPromise) {
77886
+ cleanupPromise = (async () => {
77887
+ try {
77888
+ await cleanupEcsRun(state, { keepRunning: options.keepRunning });
77889
+ } catch (err) {
77890
+ getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
77891
+ }
77892
+ })();
77893
+ }
77894
+ await cleanupPromise;
77895
+ };
77896
+ try {
77897
+ await applyRoleArnIfSet({ roleArn: options.roleArn, region: options.region });
77898
+ await ensureDockerAvailable();
77899
+ const appCmd = resolveApp(options.app);
77900
+ if (!appCmd) {
77901
+ throw new Error('No CDK app specified. Pass --app, set CDKD_APP, or add "app" to cdk.json.');
77902
+ }
77903
+ logger.info("Synthesizing CDK app...");
77904
+ const synthesizer = new Synthesizer();
77905
+ const context = parseContextOptions(options.context);
77906
+ const synthOpts = {
77907
+ app: appCmd,
77908
+ output: options.output,
77909
+ ...options.region && { region: options.region },
77910
+ ...options.profile && { profile: options.profile },
77911
+ ...Object.keys(context).length > 0 && { context }
77912
+ };
77913
+ const { stacks } = await synthesizer.synthesize(synthOpts);
77914
+ const task = resolveEcsTaskTarget(target, stacks);
77915
+ logger.info(
77916
+ `Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`
77917
+ );
77918
+ sigintHandler = () => {
77919
+ sigintCount += 1;
77920
+ if (sigintCount >= 2) {
77921
+ process.stderr.write("Force-exit on second ^C; container cleanup skipped.\n");
77922
+ process.exit(130);
77923
+ }
77924
+ logger.info("Stopping task...");
77925
+ void cleanup().then(() => process.exit(130));
77926
+ };
77927
+ process.on("SIGINT", sigintHandler);
77928
+ let assumedCredentials;
77929
+ let resolvedRoleArn;
77930
+ if (options.assumeTaskRole === true) {
77931
+ if (!task.taskRoleArn) {
77932
+ throw new Error(
77933
+ `--assume-task-role passed without an ARN but the task definition's TaskRoleArn could not be resolved statically. Pass the ARN explicitly: --assume-task-role <arn>`
77934
+ );
77935
+ }
77936
+ resolvedRoleArn = task.taskRoleArn;
77937
+ assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
77938
+ } else if (typeof options.assumeTaskRole === "string") {
77939
+ resolvedRoleArn = options.assumeTaskRole;
77940
+ assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
77941
+ }
77942
+ const envOverrides = readEnvOverridesFile2(options.envVars);
77943
+ const runOpts = {
77944
+ cluster: options.cluster,
77945
+ containerHost: options.containerHost,
77946
+ skipPull: options.pull === false,
77947
+ keepRunning: options.keepRunning,
77948
+ detach: options.detach
77949
+ };
77950
+ if (envOverrides)
77951
+ runOpts.envOverrides = envOverrides;
77952
+ if (assumedCredentials)
77953
+ runOpts.taskCredentials = assumedCredentials;
77954
+ if (resolvedRoleArn)
77955
+ runOpts.taskRoleArn = resolvedRoleArn;
77956
+ if (options.platform)
77957
+ runOpts.platformOverride = options.platform;
77958
+ if (options.region)
77959
+ runOpts.region = options.region;
77960
+ const result = await runEcsTask(task, runOpts, state);
77961
+ if (options.detach) {
77962
+ logger.info("Task containers started in detached mode; cdkd is exiting.");
77963
+ logger.info(
77964
+ `Use 'docker ps --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`
77965
+ );
77966
+ sigintCount = 99;
77967
+ return;
77968
+ }
77969
+ if (result.essentialContainerName) {
77970
+ logger.info(
77971
+ `Essential container '${result.essentialContainerName}' exited with code ${result.exitCode}.`
77972
+ );
77973
+ }
77974
+ if (result.exitCode !== 0) {
77975
+ process.exitCode = result.exitCode;
77976
+ }
77977
+ } finally {
77978
+ if (sigintHandler)
77979
+ process.off("SIGINT", sigintHandler);
77980
+ if (!options.detach)
77981
+ await cleanup();
77982
+ }
77983
+ }
77984
+ async function assumeTaskRole(roleArn, region) {
77985
+ const { STSClient: STSClient11, AssumeRoleCommand: AssumeRoleCommand2 } = await import("@aws-sdk/client-sts");
77986
+ const sts = new STSClient11({ ...region && { region } });
77987
+ try {
77988
+ const response = await sts.send(
77989
+ new AssumeRoleCommand2({
77990
+ RoleArn: roleArn,
77991
+ RoleSessionName: `cdkd-local-run-task-${Date.now()}`,
77992
+ DurationSeconds: 3600
77993
+ })
77994
+ );
77995
+ const creds = response.Credentials;
77996
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
77997
+ throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
77998
+ }
77999
+ return {
78000
+ accessKeyId: creds.AccessKeyId,
78001
+ secretAccessKey: creds.SecretAccessKey,
78002
+ sessionToken: creds.SessionToken
78003
+ };
78004
+ } finally {
78005
+ sts.destroy();
78006
+ }
78007
+ }
78008
+ function readEnvOverridesFile2(filePath) {
78009
+ if (!filePath)
78010
+ return void 0;
78011
+ let raw;
78012
+ try {
78013
+ raw = readFileSync7(filePath, "utf-8");
78014
+ } catch (err) {
78015
+ throw new Error(
78016
+ `Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`
78017
+ );
78018
+ }
78019
+ let parsed;
78020
+ try {
78021
+ parsed = JSON.parse(raw);
78022
+ } catch (err) {
78023
+ throw new Error(
78024
+ `Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`
78025
+ );
78026
+ }
78027
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
78028
+ throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
78029
+ }
78030
+ return parsed;
78031
+ }
78032
+ function createLocalRunTaskCommand() {
78033
+ const cmd = new Command15("run-task").description(
78034
+ "Run an AWS::ECS::TaskDefinition locally \u2014 pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix."
78035
+ ).argument(
78036
+ "<target>",
78037
+ "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run"
78038
+ ).addOption(
78039
+ new Option8(
78040
+ "--cluster <name>",
78041
+ "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix"
78042
+ ).default("cdkd-local")
78043
+ ).addOption(
78044
+ new Option8(
78045
+ "--env-vars <file>",
78046
+ 'JSON env-var overrides (SAM-compatible: {"ContainerName":{"KEY":"VALUE"}, "Parameters":{}})'
78047
+ )
78048
+ ).addOption(
78049
+ new Option8(
78050
+ "--container-host <ip>",
78051
+ "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)"
78052
+ ).default("127.0.0.1")
78053
+ ).addOption(
78054
+ new Option8(
78055
+ "--assume-task-role [arn]",
78056
+ "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override."
78057
+ )
78058
+ ).addOption(
78059
+ new Option8("--no-pull", "Skip docker pull for every container image and the metadata sidecar")
78060
+ ).addOption(
78061
+ new Option8(
78062
+ "--platform <platform>",
78063
+ "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture"
78064
+ )
78065
+ ).addOption(
78066
+ new Option8(
78067
+ "--keep-running",
78068
+ "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems."
78069
+ ).default(false)
78070
+ ).addOption(
78071
+ new Option8(
78072
+ "--detach",
78073
+ "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle."
78074
+ ).default(false)
78075
+ ).action(withErrorHandling(localRunTaskCommand));
78076
+ [...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
78077
+ cmd.addOption(deprecatedRegionOption);
78078
+ return cmd;
78079
+ }
78080
+
76480
78081
  // src/cli/commands/local-invoke.ts
76481
78082
  async function localInvokeCommand(target, options) {
76482
78083
  const logger = getLogger();
@@ -76574,7 +78175,7 @@ async function localInvokeCommand(target, options) {
76574
78175
  }
76575
78176
  }
76576
78177
  }
76577
- const overrides = readEnvOverridesFile2(options.envVars);
78178
+ const overrides = readEnvOverridesFile3(options.envVars);
76578
78179
  const envResult = resolveEnvVars(lambda.logicalId, templateEnv, overrides);
76579
78180
  for (const key of envResult.unresolved) {
76580
78181
  if (stateAudit && stateAudit.unresolved.some((u) => u.key === key))
@@ -76752,7 +78353,7 @@ async function resolveLocalBuildPlan(lambda) {
76752
78353
  const manifestPath = lambda.stack.assetManifestPath;
76753
78354
  if (!manifestPath)
76754
78355
  return void 0;
76755
- const cdkOutDir = dirname5(manifestPath);
78356
+ const cdkOutDir = dirname7(manifestPath);
76756
78357
  const loader = new AssetManifestLoader();
76757
78358
  const manifest = await loader.loadManifest(cdkOutDir, lambda.stack.stackName);
76758
78359
  if (!manifest)
@@ -76772,12 +78373,12 @@ function getTemplateEnv2(resource) {
76772
78373
  return void 0;
76773
78374
  return vars;
76774
78375
  }
76775
- function readEnvOverridesFile2(filePath) {
78376
+ function readEnvOverridesFile3(filePath) {
76776
78377
  if (!filePath)
76777
78378
  return void 0;
76778
78379
  let raw;
76779
78380
  try {
76780
- raw = readFileSync7(filePath, "utf-8");
78381
+ raw = readFileSync8(filePath, "utf-8");
76781
78382
  } catch (err) {
76782
78383
  throw new Error(
76783
78384
  `Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`
@@ -76805,7 +78406,7 @@ async function readEvent(options) {
76805
78406
  return parseEvent(raw, "<stdin>");
76806
78407
  }
76807
78408
  if (options.event) {
76808
- const raw = readFileSync7(options.event, "utf-8");
78409
+ const raw = readFileSync8(options.event, "utf-8");
76809
78410
  return parseEvent(raw, options.event);
76810
78411
  }
76811
78412
  return {};
@@ -76980,40 +78581,40 @@ function pickReferencedLogicalId(intrinsic) {
76980
78581
  return void 0;
76981
78582
  }
76982
78583
  function createLocalCommand() {
76983
- const local = new Command15("local").description(
76984
- "Local Lambda execution against the AWS Lambda Runtime Interface Emulator (Docker required)"
78584
+ const local = new Command16("local").description(
78585
+ "Local execution of Lambda functions (RIE) and ECS task definitions (Docker required)"
76985
78586
  );
76986
- const invoke = new Command15("invoke").description(
78587
+ const invoke = new Command16("invoke").description(
76987
78588
  "Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix."
76988
- ).argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option8("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option8("--event-stdin", "Read event JSON from stdin").default(false)).addOption(
76989
- new Option8(
78589
+ ).argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option9("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option9("--event-stdin", "Read event JSON from stdin").default(false)).addOption(
78590
+ new Option9(
76990
78591
  "--env-vars <file>",
76991
78592
  'JSON env-var overrides (SAM-compatible: {"LogicalId":{"KEY":"VALUE"}})'
76992
78593
  )
76993
78594
  ).addOption(
76994
- new Option8(
78595
+ new Option9(
76995
78596
  "--no-pull",
76996
78597
  "Skip docker pull (use cached image) \u2014 no-op for IMAGE local-build path; `docker build` does not pull base layers by default"
76997
78598
  )
76998
78599
  ).addOption(
76999
- new Option8(
78600
+ new Option9(
77000
78601
  "--no-build",
77001
78602
  "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull."
77002
78603
  )
77003
- ).addOption(new Option8("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(
77004
- new Option8("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")
78604
+ ).addOption(new Option9("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(
78605
+ new Option9("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")
77005
78606
  ).addOption(
77006
- new Option8(
78607
+ new Option9(
77007
78608
  "--assume-role <arn>",
77008
78609
  `Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions (closes the "developer admin / function narrow" skew). Off by default \u2014 when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default).`
77009
78610
  )
77010
78611
  ).addOption(
77011
- new Option8(
78612
+ new Option9(
77012
78613
  "--from-state",
77013
78614
  "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default \u2014 keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy."
77014
78615
  ).default(false)
77015
78616
  ).addOption(
77016
- new Option8(
78617
+ new Option9(
77017
78618
  "--stack-region <region>",
77018
78619
  "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions)."
77019
78620
  )
@@ -77024,6 +78625,7 @@ function createLocalCommand() {
77024
78625
  invoke.addOption(deprecatedRegionOption);
77025
78626
  local.addCommand(invoke);
77026
78627
  local.addCommand(createLocalStartApiCommand());
78628
+ local.addCommand(createLocalRunTaskCommand());
77027
78629
  return local;
77028
78630
  }
77029
78631
 
@@ -77055,8 +78657,8 @@ function reorderArgs(argv) {
77055
78657
  return [...prefix, ...cmdAndAfter, ...beforeCmd];
77056
78658
  }
77057
78659
  async function main() {
77058
- const program = new Command16();
77059
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.80.0");
78660
+ const program = new Command17();
78661
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.82.0");
77060
78662
  program.addCommand(createBootstrapCommand());
77061
78663
  program.addCommand(createSynthCommand());
77062
78664
  program.addCommand(createListCommand());