From 3253ab0dd9e3775977828014de583ffc11eae725 Mon Sep 17 00:00:00 2001 From: notgitika Date: Thu, 13 Aug 2026 14:34:43 -0400 Subject: [PATCH 01/18] feat(project): implement project deploy Deploys a project by synthesizing it, bootstrapping each target environment, then deploying every stack. Synthesis reuses build's code path so what deploys is what was just synthesized, and build's dependency check runs before anything touches AWS. Stack environments come from agentcore/aws-targets.json, which create() scaffolds empty. An empty list is an error naming the file to fill in rather than an account resolved from the active credentials, which would let deploy guess where the user's infrastructure belongs. Bootstrap is idempotent and no-ops quickly on a current environment, so it runs every deploy instead of probing CloudFormation first; --skip-bootstrap opts out. Targets sharing an environment are bootstrapped once. Bootstrap and deploy drive @aws-cdk/toolkit-lib in-process rather than shelling out to npx cdk, so progress arrives as structured messages and failures as typed errors instead of scraped stdout. src/io/cdk.ts adapts the toolkit's push-based IIoHost to the generator the manager pulls from, and is injectable so tests exercise deploy without reaching AWS. Deploy reads the cdk.out assembly synthesis just wrote instead of re-synthesizing, which both avoids a second synth and makes "deploy exactly what was synthesized" structural. A deploy runs for minutes, so those messages stream to stderr as they arrive: ProjectEvent gains an output variant carrying the toolkit's own wording, with debug and trace levels left in the debug log rather than on screen. --- bun.lock | 1225 ++++++++++++++++++++++++-- package.json | 1 + src/core/project/manager.test.ts | 300 ++++++- src/core/project/manager.tsx | 128 ++- src/handlers/project/build/index.ts | 4 +- src/handlers/project/create/index.ts | 4 +- src/handlers/project/deploy/index.ts | 42 +- src/handlers/project/index.ts | 6 +- src/handlers/project/project.test.ts | 98 ++- src/handlers/project/types.ts | 19 +- src/io/cdk.ts | 109 +++ src/io/index.ts | 7 + src/projectSchemas/aws-targets.ts | 12 + src/testing/TestCoreClient.tsx | 28 + 14 files changed, 1892 insertions(+), 91 deletions(-) create mode 100644 src/io/cdk.ts create mode 100644 src/projectSchemas/aws-targets.ts diff --git a/bun.lock b/bun.lock index 779b6bc4c..1a9f6297d 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "agentcore", "dependencies": { + "@aws-cdk/toolkit-lib": "^1.38.2", "@aws-sdk/client-bedrock-agentcore": "^3.1092.0", "@aws-sdk/client-bedrock-agentcore-control": "^3.1102.0", "@aws-sdk/client-cloudwatch-logs": "^3.1092.0", @@ -46,37 +47,105 @@ "packages": { "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="], + "@aws-cdk/aws-service-spec": ["@aws-cdk/aws-service-spec@0.1.202", "", { "dependencies": { "@aws-cdk/service-spec-types": "^0.0.268", "@cdklabs/tskb": "^0.0.4" } }, "sha512-eICyDelvVagJ3LQ0nNW+y1bQoBlDj47fknnGh8ZJYZ64LGqrGwPRPTNeLDaJi16JBU1y1HmQYsK6pgC5mNZgcQ=="], + + "@aws-cdk/cdk-assets-lib": ["@aws-cdk/cdk-assets-lib@1.4.15", "", { "dependencies": { "@aws-cdk/cloud-assembly-api": "2.3.0", "@aws-cdk/cloud-assembly-schema": ">=54.13.0", "@aws-sdk/client-ecr": "^3", "@aws-sdk/client-s3": "^3", "@aws-sdk/client-secrets-manager": "^3", "@aws-sdk/client-sts": "^3", "@aws-sdk/credential-providers": "^3", "@aws-sdk/lib-storage": "^3", "@smithy/config-resolver": "^4", "@smithy/node-config-provider": "^4", "fast-glob": "^3.3.3", "mime": "^2", "picomatch": "^4.0.4", "yazl": "^3.3.1" } }, "sha512-2zhDcauYPkZKL+kgwNGLq7HBMvaxJwjoM3zf0Ai9B2nBoBOcbyxMw8wr1gwiIys4ZsbGOLMyRJD10lOBSO3BFg=="], + + "@aws-cdk/cli-plugin-contract": ["@aws-cdk/cli-plugin-contract@2.182.2", "", {}, "sha512-M1G52uA2JZPErOIKAkr0cjty7Y+/g99KHH7p9ZQv5jpZG7PNhtIraufbgBAo3iCEAH38SDtiPJL3+Ac8EttrIQ=="], + + "@aws-cdk/cloud-assembly-api": ["@aws-cdk/cloud-assembly-api@2.3.0", "", { "dependencies": { "json-source-map": "^0.6.1", "jsonschema": "^1.5.0", "semver": "^7.8.5" }, "peerDependencies": { "@aws-cdk/cloud-assembly-schema": ">=54.12.0" } }, "sha512-Z+TYWH9YJGDQSwjORTWvfRFHGK5Gzdp73ElVO8mmZrgndlZgNV2LJOcX1HYRlU/OuR7I+Qo5GLpfc/PM1s7JlQ=="], + + "@aws-cdk/cloud-assembly-schema": ["@aws-cdk/cloud-assembly-schema@54.18.0", "", { "dependencies": { "jsonschema": "^1.5.0", "semver": "^7.8.5" } }, "sha512-KceoGMrKbXp3qigQ8y67yq3Cd1k43GOw+E4KMf/MbDbRGyZEkfdLihhCD7obXvN5f1cKemZECFj+klLEoo1biA=="], + + "@aws-cdk/cloudformation-diff": ["@aws-cdk/cloudformation-diff@2.187.2", "", { "dependencies": { "@aws-cdk/aws-service-spec": "^0.1.185", "@aws-cdk/service-spec-types": "^0.0.251", "chalk": "^4", "diff": "^9.0.0", "fast-deep-equal": "^3.1.3", "string-width": "^4", "table": "^6" }, "peerDependencies": { "@aws-sdk/client-cloudformation": "^3" } }, "sha512-+B6OmBtMxtl0wkPsNNtVZjuoy8b2A1tou66yrR+lXgFq2czA6S0t9sUjsTP/UVuUnQRiFWJVP3UuMiZuVDNZ2A=="], + + "@aws-cdk/cx-api": ["@aws-cdk/cx-api@2.265.0", "", { "dependencies": { "@aws-cdk/cloud-assembly-api": "^2.2.6", "semver": "^7.8.5" }, "peerDependencies": { "@aws-cdk/cloud-assembly-schema": ">=53.25.0" } }, "sha512-OD3AfWX7YxGF5uy+KTjKB24E93Qt6/g/3dzsSOZDxt/MQfY3N+KkdIpEDy9VF70bpyiF+OUjtNXjoMNa6o1wLA=="], + + "@aws-cdk/service-spec-types": ["@aws-cdk/service-spec-types@0.0.251", "", { "dependencies": { "@cdklabs/tskb": "^0.0.4" } }, "sha512-193kY3ki/wNZJVMY2plwmQzOiTOtePeEfudAj8bw7/UAFtHUAS9qTLY2YLr+4JIHqkoV12GkshPgsLRFySclFg=="], + + "@aws-cdk/toolkit-lib": ["@aws-cdk/toolkit-lib@1.38.2", "", { "dependencies": { "@aws-cdk/cdk-assets-lib": "^1", "@aws-cdk/cloud-assembly-api": "2.3.0", "@aws-cdk/cloud-assembly-schema": ">=54.18.0", "@aws-cdk/cloudformation-diff": "^2", "@aws-cdk/cx-api": "^2", "@aws-sdk/client-appsync": "^3", "@aws-sdk/client-bedrock-agentcore-control": "^3", "@aws-sdk/client-cloudcontrol": "^3", "@aws-sdk/client-cloudformation": "^3", "@aws-sdk/client-cloudtrail": "^3", "@aws-sdk/client-cloudwatch-logs": "^3", "@aws-sdk/client-codebuild": "^3", "@aws-sdk/client-ec2": "^3", "@aws-sdk/client-ecr": "^3", "@aws-sdk/client-ecs": "^3", "@aws-sdk/client-elastic-load-balancing-v2": "^3", "@aws-sdk/client-iam": "^3", "@aws-sdk/client-kms": "^3", "@aws-sdk/client-lambda": "^3", "@aws-sdk/client-route-53": "^3", "@aws-sdk/client-s3": "^3", "@aws-sdk/client-secrets-manager": "^3", "@aws-sdk/client-sfn": "^3", "@aws-sdk/client-ssm": "^3", "@aws-sdk/client-sts": "^3", "@aws-sdk/credential-providers": "^3", "@aws-sdk/ec2-metadata-service": "^3", "@aws-sdk/lib-storage": "^3", "@smithy/middleware-endpoint": "^4", "@smithy/property-provider": "^4", "@smithy/shared-ini-file-loader": "^4", "@smithy/util-retry": "^4", "@smithy/util-waiter": "^4", "cdk-from-cfn": "^0.321.0", "chalk": "^4", "chokidar": "^4", "fast-deep-equal": "^3.1.3", "fast-glob": "^3.3.3", "fs-extra": "^11", "p-limit": "^3", "picomatch": "^4", "semver": "^7.8.5", "split2": "^4.2.0", "wrap-ansi": "^7", "yaml": "^1", "yazl": "^3.3.1" }, "peerDependencies": { "@aws-cdk/cli-plugin-contract": "^2" } }, "sha512-WsyPjZnLr4zk16sp1tuPGXUKZ9elHJLqcpogxfQ+92cKolw9srbRJe57Z5whMZ0HNXarOYf0pOm5hWKqr8UAEw=="], + + "@aws-sdk/checksums": ["@aws-sdk/checksums@3.1000.28", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-VCpnmyHQ1IH49ni3LXnQj7DPr7rmcJmzYeiCkYdCcfgNtkvOj38cdcL9lapBWoItZWFACJPFJlymqC7/gem3Gw=="], + + "@aws-sdk/client-appsync": ["@aws-sdk/client-appsync@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-ZwXRex+FRcItheCLBGHpVnYQMdEo8Ko0WShXDHGZvp1CYQ1Bxx0HryciKbSfx6f0ytKpqlgAeuzymHn0+no5mw=="], + "@aws-sdk/client-bedrock-agentcore": ["@aws-sdk/client-bedrock-agentcore@3.1094.0", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/credential-provider-node": "^3.972.71", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-K8+YaYFyaVJvidwVSnmXlHjpOel/qz4ii2TZY2shXbkMoQxftjHuiXlplKeb5pWhzdS092q6YqPR5CZrMH90Ug=="], "@aws-sdk/client-bedrock-agentcore-control": ["@aws-sdk/client-bedrock-agentcore-control@3.1102.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/credential-provider-node": "^3.972.77", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6HjZkg14iiJr8VinzAKy5XRJe5JLrDg9USBia2UZZAg8leGff2/+cyH0y4ZYQqRe1f1TaMZfecNSQLCy3XInjQ=="], + "@aws-sdk/client-cloudcontrol": ["@aws-sdk/client-cloudcontrol@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-XzKYzb3roiSMNqfyWjTk45ZEIx497aIbJjHF3F+myeM9gJEX/eELgo2nX9JoQLaK1X/WEWV15nCDM+X4vjUi+Q=="], + + "@aws-sdk/client-cloudformation": ["@aws-sdk/client-cloudformation@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-tZHW6n0vbA1fiJZtxCWVFWUssP47es/EAu7kH2FId6nY/svNoQVHYTqA7ALhLqB+W6ASzNTpnJeCq8evoXlKBQ=="], + + "@aws-sdk/client-cloudtrail": ["@aws-sdk/client-cloudtrail@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-5YRE0ERJ+S7Cj+vC7ZhNFHRHGYZYgjY7EOxqmYBYemu9NMXbFTOQDwvjB30AfxUe4M5DcFG2PvbW1uDUF/4QYg=="], + "@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.1104.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-node": "^3.972.78", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-cetLaPXxPgAVjVyC+dvLFaoasgt1/SX2RhjhEBTepTX8tX+WMD1pWoyRn9Uk2XYzz3RODdavekmue66r52S7nw=="], + "@aws-sdk/client-codebuild": ["@aws-sdk/client-codebuild@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-2vLeFS/cbF4WKC+zTcVR6rT1diNAshB2Zn5H5boLg31JE+bzYuPYEd5tKotR/keXaIirpMx5ibzMaJh67QM6pA=="], + + "@aws-sdk/client-ec2": ["@aws-sdk/client-ec2@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/middleware-sdk-ec2": "^3.972.57", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-t76xpiFV1dG+kpr+qEd2AMdOBpk7m3594taY2K94A/7SUB0wb83+jVyCT/XK3dN+VI7hgQFA0mjk2rRLclSJag=="], + + "@aws-sdk/client-ecr": ["@aws-sdk/client-ecr@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-H0oAs3nDo3gSXwcTRg2pGfT7xfs8kkHWA3iQToRGAWk73tYXbFIp3TsEYqJ+jw9Rx/Rd7+ybgBU6jEgntAB3lQ=="], + + "@aws-sdk/client-ecs": ["@aws-sdk/client-ecs@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-ii4ixDi2f2ULAE/jA32Htp+wjbJ6DIUwsh4Iq4kKkbzS8mQL+ByPyWWGnAqXDyQeI/90FGmq+sKciXoAUb3i0Q=="], + + "@aws-sdk/client-elastic-load-balancing-v2": ["@aws-sdk/client-elastic-load-balancing-v2@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CBjXXDDVermcUqg988+UIuPA7K1+3yrE9SVKv7zD5ysv1RHyzZ2HitAu/E37eZnujUxTh3ShTwiG2muy4HQ0Vg=="], + "@aws-sdk/client-iam": ["@aws-sdk/client-iam@3.1087.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/credential-provider-node": "^3.972.68", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-S2UBBQnPMTREF52WPz7yabvags3rPYLOPwq8LOPNJIekyAIqUwtGN46z2tqGNr+NyGz9cpEI+3brJgkWKQGNIQ=="], + "@aws-sdk/client-kms": ["@aws-sdk/client-kms@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-vs7v+BWPp7CBvcZ6Dp4LwseZ4M0ERCICsheNAyabvnAuL+qPo4FNIm2TILZ18amN737+V2XqKuDelsePthd91A=="], + + "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-jG2rB6LY2rx3XT55axgH3C0aIv1egvI0kqPYC6jfghVp82kWssU7R/1dJxXr0PnPLv4wq+WXRyLEmjyKURUjFQ=="], + + "@aws-sdk/client-route-53": ["@aws-sdk/client-route-53@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/middleware-sdk-route53": "^3.972.25", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pfFpCuwsnALNDTnWG2nf46e0DOGTclRbx2rLlb4rVuIwsfFTQ89L0Y5CeKMScePisYO8+kVrayuvnxc5C6+/OA=="], + + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1111.0", "", { "dependencies": { "@aws-sdk/checksums": "^3.1000.28", "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/middleware-sdk-s3": "^3.972.74", "@aws-sdk/signature-v4-multi-region": "^3.996.45", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-VnLT6aSTN8tWl/NsXUysXNZor7wQBp9CRwufo7kt8cwGXvHLZ0S/cV1K9WFcREGboVYSo3NGQ3ZvU7LRidh2aQ=="], + + "@aws-sdk/client-secrets-manager": ["@aws-sdk/client-secrets-manager@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-+OQYrSTkRJV826I4+PuXY+h6m1lqo5LTGtbPsJ5JWlpuYr+GY9vTyWO4qOoyUu+YnRqD5NTXSCQqmZXACJsYJg=="], + + "@aws-sdk/client-sfn": ["@aws-sdk/client-sfn@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-2Plp0wZNf1hKlkN62YuGA0mYIV04aSOGrGQ6DrTE/9fwfRiI0asxhLCSFiX3KW4McRK7xpp1EjyxXHZNJaqz0g=="], + + "@aws-sdk/client-ssm": ["@aws-sdk/client-ssm@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-NhVyZL203unBVPi/oFQ48m0f5z/0M8nKM6nVEoOnX9/hfWXfa81rh5J1Tj4YoDuDjb5aTKyQZAtsbD48Dwiujw=="], + + "@aws-sdk/client-sts": ["@aws-sdk/client-sts@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/signature-v4-multi-region": "^3.996.45", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-b41gi3Z0tMt/e7RpiBwm2H+Fz+NvlczQ6vxy2pTsDp7WcwXLFCXeluvvl1Y/RBL4g7+4/qaRODkMUSSPBjUWcA=="], + "@aws-sdk/core": ["@aws-sdk/core@3.976.0", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.36", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.4", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-0cjRaEdlVoOrsNb9pP5q1Syyc8pXw5xSj2Np2ryReRTr9FppIIRVSdZK4lbnfmc2Hvgux/xBOUU6baB7z8//uA=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-BAkxdoe7tpDDqCghGpuOeHQRbm/2znVvOQm0AvpQbA2tbfMN46doN4zx65fv85ImP3KADwc2zQPmbrlI9MPfMg=="], + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.68", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Fo2RC5hZmEVhnVLMzNjYL6Iu78F4wDuaNtkgzRVU4DlUaFhcXxsnxWBOTH2drxZ59OHSipQ1LndfkCWPxGBJHA=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-AreCFzcB4kH2HF9031Ot0jSJr3KXvRg6e8uDeub20JEVdZU3Bv0sTq1plc7VsT3KiqutlzH7l0j50UcCWHUioA=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.62", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g/0fGqKTb9xpKdd9AtpmV5Eo3DFKbnkpA2+w0peISSlu7NfAoWOuYBFxsu+yWBtxU89ka55ezoZBCbFaS8pjYQ=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.71", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-A8ObcqVmDMnk4F9NozZ7JwmUu9Q4xyBJkmyq1C5U+wNM9ht9J7+EuuyabsLWXZnOoTqFaJuYBYTKf5CTipkEjA=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.5", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/credential-provider-env": "^3.972.60", "@aws-sdk/credential-provider-http": "^3.972.62", "@aws-sdk/credential-provider-login": "^3.972.67", "@aws-sdk/credential-provider-process": "^3.972.60", "@aws-sdk/credential-provider-sso": "^3.973.4", "@aws-sdk/credential-provider-web-identity": "^3.972.66", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/credential-provider-imds": "^4.4.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-ylubazcRfq2TVus/qXucSXeC42Qdjp5HQxTu68K/BsdMiZlcSLD1zkpoCgApXZX1Y6YJhtGGs7ZHhO/GuIgBlw=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.14", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-login": "^3.972.76", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7c+Wti2LsERNWMfm7ySz3/6RPopFW3Nmn7s63Xpcq6R/tRuY5hpvkHA2xVgi5ukJbvok9l0IDtVEvqTtg+X7dw=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CCygIKJ9YbI3n84OClSaSppkgKKHVj2TGT33c6FRORZrYNZQ1POmD+ip0FLYokiJAK7sSdc3YVkOsBm90oxWMQ=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-LVixwOnEJfrrfKHeZjBA8pIMTZjNDq8ak8VpcoWUuCJDrSnBNU8POJksULMgvN089P0MXtQYH2Zs627/MK1K0g=="], "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.71", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.60", "@aws-sdk/credential-provider-http": "^3.972.62", "@aws-sdk/credential-provider-ini": "^3.973.5", "@aws-sdk/credential-provider-process": "^3.972.60", "@aws-sdk/credential-provider-sso": "^3.973.4", "@aws-sdk/credential-provider-web-identity": "^3.972.66", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/credential-provider-imds": "^4.4.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-HIg7Q2osBzajQwL+1Vkyh2E7Gim3eTNb9RHIsOxDGjW0eZg4oEKtRs5sioCnc73ilhaOm4gX2lHVF8J7+nt2rg=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YIo3f99hM43QdYG8hDzwGemnR/pU95b0kramqSJUTleCqaB7+HwKf7YZFHqvOgTqZTPx/mRmNIqoDRr3U0Z3Tw=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-9kpTNdZTrcqXTfhxM7fgl9Z68ek3Fu5oe3Yf+A/pJGibEqpgZxz2tSY7SinmyCIU2PJ+ygY4FPoBBnLpocMtrQ=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.13", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/token-providers": "3.1111.0", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Oc81qauMPzUoTnAS2YKpNwY6sY/LUyQTEeaf6yP197WMxkEBQfcKLR1MFpD7+pNTubXnfkH6gwpji+Gc7iyD2Q=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.75", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YPN6uoGDgjjjeVFZrcOeCJqmB6zpXoeeNgIjqe+DexJaWqdjVfCCe+VAZwli9Z2h8KhFW8oxkO39emQ1tyz/Mw=="], + + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-cognito-identity": "^3.972.68", "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-login": "^3.972.76", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-kon/lEuONhTw4F4nYo2mOlzi+2yWJyLWzyuTFK8bH0j5TG5fA/6B4y754ma6lvM9N2QNjhF9TG01kHO2eFYAcQ=="], + + "@aws-sdk/ec2-metadata-service": ["@aws-sdk/ec2-metadata-service@3.1111.0", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-jvPRAWnKSaxmP1rrd8FXVOU/+IOPtneTkmPZokUxo1jgXwe83VRtoa4/NQf6QhDgBkB4hNEnT63e/gwjjMhRuw=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.4", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/token-providers": "3.1092.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-BPdmL8sSBOCv4ngZ+3LHxyc3CNqDCEK37CHioCk7zGrTMY5sUtkH8q+o6qA80nn6w3/fyBPGNE7OIRlmoOxRQA=="], + "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.1111.0", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.1111.0" } }, "sha512-UjLJE0Zxv/m8Xgd3DLBXssZYISgoEPZpJiPWY+MadEdP36oOcjHVsR8ToUq6MMmKdzuBFYB6qGHWS/yKAG4fow=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-kSAziJboOmZmsR9/MTbiNjowl2BPes1bQuJpne4qAZ62ubi8fjfr/aupJSQje6udBoYxXTQbsL0e0kby2la3ng=="], + "@aws-sdk/middleware-sdk-ec2": ["@aws-sdk/middleware-sdk-ec2@3.972.57", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-2e7mOkzSJn5oIqNZy+2JquiG8wRcLw7dYX99Y1My234NKkm48YbKLZFskCQxGp5L16W3Jtmp+ssf6YPC1gv/bw=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.34", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/signature-v4-multi-region": "^3.996.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA=="], + "@aws-sdk/middleware-sdk-route53": ["@aws-sdk/middleware-sdk-route53@3.972.25", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YrpXfJUEDsjS5vZiSDA4MBggBowNZNfNwzSD9pRnitE8lJwqukj2S2EAkJTYJ0RSSaB83EZoy00IvGiuc8KYWA=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.41", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng=="], + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.74", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/signature-v4-multi-region": "^3.996.45", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-2lzoV2z2QO5KJZYGOCnIZ1WVQgzMECvwuzr1xb034a++8QW4U4eGrmC2u4yg1xvNv4TLL/Uv5DLyuAiw0b9z7Q=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1092.0", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hBYUAr6iBLNFcsiWTgtBb0stdSw39VOUq4Sp4A5caCNf66BAZplWN4FleKrVpJx5li2YgdnK2DqoFSMWC642FQ=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.43", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/signature-v4-multi-region": "^3.996.45", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bit+VpqWNyi3wHxFoTsTliNXimCSL2r2OeDTm7ZrG+YsTZ2D7ofDJ6r/t9PVBn80i6/v0X2h9Tgw6QP2MAKfPw=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.45", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bBuyztukzXq6plzFGHAWiQt0QXo+HL8b8lX5cFTzkez/74PtS1c0qPFCIVuHkyoT+miH2qOjAcm1/yoro2ESPA=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-JfljgoVtl+s3Qy21n9a7Z48uCQaOXcN74KJ3TEQfPoB293GrXFSt6HSQJF1sTZ8c/5QedEvd3NjJQMO4u9qa5A=="], "@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], @@ -84,10 +153,18 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@cdklabs/tskb": ["@cdklabs/tskb@0.0.4", "", {}, "sha512-NFx1X0l7p5DyHtLLEyNeh1hPN4UN9hTkZkzFs/Mp+kFk7dpdINGmGVpCfRDjJ2DrcNSNENUmT+w8g73TvmBbTw=="], + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], "@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.221.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ=="], @@ -148,18 +225,32 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.74.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.7.0", "", { "dependencies": { "@smithy/core": "^3.32.0", "tslib": "^2.6.2" } }, "sha512-2/dD9ftRHTKH7pVdTSHoTGAYW/ruYmqzFeEn2cRRkNLs6GSRzlCoIAz/wSIk3g47t+k5+1JOMoXcI0+PFPV3MA=="], + "@smithy/core": ["@smithy/core@3.29.8", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-rpCbCV+TimOBi3VLNBMmtTvgfOWcFIEAru3+TFlG87SL2F+te4jOnnNR+cf3uR4eJ5Qf4LnT80fqnBKgPRS6zA=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.13", "", { "dependencies": { "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-X+2HNZhWi5i3rJsCas0LPf6fTQUaKyJ40zd8aTO/bwpRfpU3biYaqLr7C1WMibL7PVKJalpi1PyybjGPNoHC8Q=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w=="], "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.10", "", { "dependencies": { "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-5/Yj9mS2JjTsB3B8ZX7euh77mrY9aXW23ag1yAmFykSRmA6vldqBrgqmSeQ50EjY+5SB8+aE4w14B6LKbBVEhQ=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.7.0", "", { "dependencies": { "@smithy/core": "^3.32.0", "tslib": "^2.6.2" } }, "sha512-GBsFfpfmaxJCkoT4JoSQSvmtlnEF4yCL7tIfnTIic5NSgk6UdK9g1ooDerLVsTqtsL+SPe0FKrURCpWdJwj6uA=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.6.0", "", { "dependencies": { "@smithy/core": "^3.32.0", "tslib": "^2.6.2" } }, "sha512-DXGmwJDsQzx033lbe5+mMnK88n9PK14akJb67GQFmxD7PXPa1+AbawssOZA8q9Y4Wp+6M8a9RKfFYYoRCdfcuw=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.10", "", { "dependencies": { "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-ETQz9v/Z+nTQc6fRWTXxUpxJqwpmzB3Tn3WKAdHwWkeT+m+HE5czs6GNG8vW+4vyxXSls65RVcvOZwk7Q/PS/Q=="], + "@smithy/property-provider": ["@smithy/property-provider@4.5.0", "", { "dependencies": { "@smithy/core": "^3.32.0", "tslib": "^2.6.2" } }, "sha512-SstEB8BO8Y65PI66tX5KXk0LpRxBcSoQui1mgZlDRjgzLekVHeIy1+XS6kE/SGtGcIOw6+KkwFQKj5dUHjwLaw=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.7.0", "", { "dependencies": { "@smithy/core": "^3.32.0", "tslib": "^2.6.2" } }, "sha512-9t3SeuWNuNfCw4uHUiJsc6EDqTf82ZC28jgfwD5nVj6zSoU/TBX2GNDKFMadMlMoOwFEhqXo5v1o2BKZAaavmQ=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.6.9", "", { "dependencies": { "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g5rnEii/mkT0mjVJmlsaOfyNBtHNTecD9Lo4NP8D5HzMUEnZNpz7/FbvBCjNcV4vteHFAxOGiLUYNxPkDZZAPw=="], "@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + "@smithy/util-retry": ["@smithy/util-retry@4.6.0", "", { "dependencies": { "@smithy/core": "^3.32.0", "tslib": "^2.6.2" } }, "sha512-J6ws8wYMJjPI0HBBxqCuHkHAfr1Xd/H/MKMrUVHiEhRUHWXZ2R6i6E8XTBAp55msMB8Q436D56Cl2tjgwOHfog=="], + + "@smithy/util-waiter": ["@smithy/util-waiter@4.6.0", "", { "dependencies": { "@smithy/core": "^3.32.0", "tslib": "^2.6.2" } }, "sha512-6vcYgT4Tr2norfflY2FPxu536GGPC+P0KFfy4DoLWgeuVMbloksaVvKyV/OE94AtI9KTTHBXworzKXxctfUTAg=="], + "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], "@tanstack/query-core": ["@tanstack/query-core@5.101.2", "", {}, "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw=="], @@ -174,21 +265,37 @@ "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], + + "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "cdk-from-cfn": ["cdk-from-cfn@0.321.0", "", {}, "sha512-3h+7G25I1Put372+S+ayps97i+CiRRZYGc+8fYf5tUXxS0uuIAU1e6y95/4fpqLVrIa+9BZpzZCeilyZBu+kmQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "cli-boxes": ["cli-boxes@4.0.1", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="], @@ -200,9 +307,9 @@ "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], - "color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "color-name": ["color-name@2.1.1", "", {}, "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="], @@ -214,7 +321,9 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], @@ -226,18 +335,40 @@ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], "file-stream-rotator": ["file-stream-rotator@0.6.1", "", { "dependencies": { "moment": "^2.29.1" } }, "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "handlebars": ["handlebars@4.7.9", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -248,12 +379,26 @@ "ink-testing-library": ["ink-testing-library@4.0.0", "", { "peerDependencies": { "@types/react": ">=18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-source-map": ["json-source-map@0.6.1", "", {}, "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg=="], + + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "jsonschema": ["jsonschema@1.5.0", "", {}, "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw=="], + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], "lint-staged": ["lint-staged@17.0.8", "", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA=="], @@ -262,10 +407,18 @@ "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], @@ -286,12 +439,16 @@ "oxlint": ["oxlint@1.74.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.74.0", "@oxlint/binding-android-arm64": "1.74.0", "@oxlint/binding-darwin-arm64": "1.74.0", "@oxlint/binding-darwin-x64": "1.74.0", "@oxlint/binding-freebsd-x64": "1.74.0", "@oxlint/binding-linux-arm-gnueabihf": "1.74.0", "@oxlint/binding-linux-arm-musleabihf": "1.74.0", "@oxlint/binding-linux-arm64-gnu": "1.74.0", "@oxlint/binding-linux-arm64-musl": "1.74.0", "@oxlint/binding-linux-ppc64-gnu": "1.74.0", "@oxlint/binding-linux-riscv64-gnu": "1.74.0", "@oxlint/binding-linux-riscv64-musl": "1.74.0", "@oxlint/binding-linux-s390x-gnu": "1.74.0", "@oxlint/binding-linux-x64-gnu": "1.74.0", "@oxlint/binding-linux-x64-musl": "1.74.0", "@oxlint/binding-openharmony-arm64": "1.74.0", "@oxlint/binding-win32-arm64-msvc": "1.74.0", "@oxlint/binding-win32-ia32-msvc": "1.74.0", "@oxlint/binding-win32-x64-msvc": "1.74.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.24.0", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "prettier": ["prettier@3.9.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], "react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], @@ -302,16 +459,26 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="], "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -320,10 +487,14 @@ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], "string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="], @@ -332,6 +503,10 @@ "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], @@ -340,6 +515,8 @@ "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -352,6 +529,8 @@ "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="], @@ -364,16 +543,52 @@ "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yazl": ["yazl@3.3.1", "", { "dependencies": { "buffer-crc32": "^1.0.0" } }, "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@aws-cdk/aws-service-spec/@aws-cdk/service-spec-types": ["@aws-cdk/service-spec-types@0.0.268", "", { "dependencies": { "@cdklabs/tskb": "^0.0.4" } }, "sha512-q2JE2nSmY2GrAby3XxsSn5koezqDyWlCh8Jnqd1kh/uXEJQ9Iy3DN6UjHcQcl8zRM7BwiX91w9JTvbMS1L/b/w=="], + + "@aws-cdk/cloud-assembly-schema/jsonschema": ["jsonschema@1.5.0", "", { "bundled": true }, "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw=="], + + "@aws-cdk/cloud-assembly-schema/semver": ["semver@7.8.5", "", { "bundled": true, "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "@aws-cdk/cloudformation-diff/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@aws-cdk/toolkit-lib/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], + + "@aws-sdk/checksums/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/checksums/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/checksums/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/checksums/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-appsync/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/client-appsync/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/client-appsync/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/client-appsync/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/client-appsync/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-appsync/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/client-appsync/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core": ["@aws-sdk/core@3.977.5", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-O5otOc1c6UZh5HsHAaPdYBcUUR9HL6mtnKqvc8nxN/CKDGUBUpsdh0q8K04Uz/dd1i0TaGyIQuQNqoO7+ad2TQ=="], "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.77", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.66", "@aws-sdk/credential-provider-http": "^3.972.68", "@aws-sdk/credential-provider-ini": "^3.973.11", "@aws-sdk/credential-provider-process": "^3.972.66", "@aws-sdk/credential-provider-sso": "^3.973.10", "@aws-sdk/credential-provider-web-identity": "^3.972.72", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-l4nitYCN/Ls57vtUfdextCjTjW41JD7lQiAnuR0RTbdByFc/6OmEAzwGd+lrp6CUtiXGQL1FCaYiamfHASrwBw=="], @@ -384,6 +599,48 @@ "@aws-sdk/client-bedrock-agentcore-control/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + "@aws-sdk/client-cloudcontrol/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/client-cloudcontrol/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/client-cloudcontrol/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/client-cloudcontrol/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/client-cloudcontrol/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-cloudcontrol/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/client-cloudcontrol/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-cloudformation/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/client-cloudformation/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/client-cloudformation/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/client-cloudformation/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-cloudformation/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/client-cloudformation/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-cloudtrail/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/client-cloudtrail/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/client-cloudtrail/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/client-cloudtrail/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/client-cloudtrail/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-cloudtrail/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/client-cloudtrail/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.78", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-ini": "^3.973.12", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-OgPAnfvbGAMWac6yvxJ1ihslrvDpPVwR68D2csospdNCCyPvHk9JLzYKwz48SNiS1T2znDwHauywRKRFfpyYng=="], @@ -394,6 +651,76 @@ "@aws-sdk/client-cloudwatch-logs/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + "@aws-sdk/client-codebuild/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/client-codebuild/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/client-codebuild/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/client-codebuild/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/client-codebuild/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-codebuild/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/client-codebuild/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-ec2/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/client-ec2/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/client-ec2/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/client-ec2/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/client-ec2/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-ec2/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/client-ec2/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-ecr/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/client-ecr/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/client-ecr/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/client-ecr/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/client-ecr/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-ecr/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/client-ecr/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-ecs/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/client-ecs/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/client-ecs/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/client-ecs/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/client-ecs/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-ecs/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/client-ecs/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + "@aws-sdk/client-iam/@aws-sdk/core": ["@aws-sdk/core@3.975.2", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@aws-sdk/xml-builder": "^3.972.35", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.3", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-iyeXwziyjJpixq5OmhsIyrSWx8vwcI7gDo4yRUC3EP7NQtOo9iAJiIEc3G+/HkhtNXqOhofiCK7Lc34Sq+fJWg=="], "@aws-sdk/client-iam/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.68", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.58", "@aws-sdk/credential-provider-http": "^3.972.60", "@aws-sdk/credential-provider-ini": "^3.973.2", "@aws-sdk/credential-provider-process": "^3.972.58", "@aws-sdk/credential-provider-sso": "^3.973.2", "@aws-sdk/credential-provider-web-identity": "^3.972.64", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4akjzW9CjorByYfqXBXmYUh/h7Io3U4DtVgGGh9TQraZ7ZlyJqNyHwDRGiUFnHD+BTOeTbCesCa4sJaK7BGZ7A=="], @@ -406,109 +733,871 @@ "@aws-sdk/client-iam/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw=="], - "listr2/cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + "@aws-sdk/client-kms/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], - "log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + "@aws-sdk/client-kms/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], - "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + "@aws-sdk/client-kms/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], - "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "@aws-sdk/client-kms/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], - "react-devtools-core/ws": ["ws@7.5.12", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg=="], + "@aws-sdk/client-kms/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], + "@aws-sdk/client-kms/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + "@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bOzP2+zdJ0XrghywB4FaJXtGZCx9yS0AGps+VJ5yEgg30wVyHNmVDBwVDXcRypzQY5iLGCS3NSn0nsuISqjFCQ=="], + "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.68", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lkunS8X+H6V76WE+t/uGQm/U8v0JXK5mLfNFTUAMlE1kqaCjwlmqKJrgCVtqjK/vqnlrSWsLK4Lr4NANBWlfTQ=="], + "@aws-sdk/client-lambda/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.11", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/credential-provider-env": "^3.972.66", "@aws-sdk/credential-provider-http": "^3.972.68", "@aws-sdk/credential-provider-login": "^3.972.73", "@aws-sdk/credential-provider-process": "^3.972.66", "@aws-sdk/credential-provider-sso": "^3.973.10", "@aws-sdk/credential-provider-web-identity": "^3.972.72", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-KoDEolYtLHG/8C+IiZpXbJWyBOMkrHV+j66Kb9PBXmLv5euGb7aELvuCmLenoGAV6gBW2wM7TsG/1e5iulH4kA=="], + "@aws-sdk/client-lambda/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YOnX6bIhdjx0QfaENu2PB0eFm5MEc9ft8XNGQ+NxMfeLSq9aE+XjWCwDupEnV4UWv5ZFpBLJbTREIx7KNOoqpQ=="], + "@aws-sdk/client-lambda/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.10", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/token-providers": "3.1102.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-IsXnQ35j5VE+3ZK6aIhT5ypB+Jim3zRwVz0nYuVwyBKZyu/SYx+O2/LQpng8c2EiuwyqceabsDlYrICHDlJPsA=="], + "@aws-sdk/client-lambda/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-nj9Zlsy7ya+fy+jhWTJwgfr7YdtDM4xHyZvgKuftuny0UgROVx9lxwvsWJSLvpKk4lig0m0tHng3k1fEnt0LeA=="], + "@aws-sdk/client-lambda/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w=="], + "@aws-sdk/client-route-53/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], + "@aws-sdk/client-route-53/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + "@aws-sdk/client-route-53/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-rcIpk5kxUqDaaNa6Xk23pQ6ViY7jlqzmfFWCahQcBT97ddXaXYYwzCen9Tz1Jvo6aJft6wDl5bN44/Jw5B4oLA=="], + "@aws-sdk/client-route-53/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-nggwJtZ4eeNsUw5IeWBMXsi1ryct5idi0K+/SCRF3kybLubOMaNTb3XCihXpWMiVpyzyPeIrl0zTkzhBH9porA=="], + "@aws-sdk/client-route-53/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.12", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-login": "^3.972.74", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg=="], + "@aws-sdk/client-route-53/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-IlUEejorGTWKb4/Dm7K5Yw4QxUmXLThLhrvBmzVBqZFTbW72cv9LTcITmo1dsnYriALE4h68mOq4LB99x6sQ7Q=="], + "@aws-sdk/client-route-53/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.11", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/token-providers": "3.1103.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-gAQBkBZxUB84d71+pPcI9L+jh2ujhuAVxc/4FgGiWFDjkPBlMKxzd5XDtkSXTFX8Ro7ansnT88+XadasxMeCRw=="], + "@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.73", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-SnlEmQa6SjOgs6iOPLUQl1Eyq4AKiAdPQlkOhFhqNfDtDCwibMGvL6QlkSmf3o6vAUSImzdPCxowT5dfQUZP1A=="], + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w=="], + "@aws-sdk/client-s3/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], - "@aws-sdk/client-iam/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.35", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pXzaWe3evZhjxDXAlMnqISe/XefTCGwBJG4nFTXaWSgAnMkqPEhxEPqJNhhpGesEvKFhvNpnozJJ4GTL11bRYw=="], + "@aws-sdk/client-s3/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], - "@aws-sdk/client-iam/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], + "@aws-sdk/client-s3/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-vyGtvK1rY940eq7JT0yIGKuZ+2kpPSJcHibSvGlit5oiMFDamzC7cxBGLl4FLnd6suihMXDI2FSF2dL6TmBqPA=="], + "@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g9b9YzDrD5pcKiPBJfCSXRfFMrA39eR0guUhZ5SRm+7vMAVc43+effxbcamxBjSd5bUhrdKo5te/yQuWurLXLA=="], + "@aws-sdk/client-secrets-manager/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.2", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/credential-provider-env": "^3.972.58", "@aws-sdk/credential-provider-http": "^3.972.60", "@aws-sdk/credential-provider-login": "^3.972.64", "@aws-sdk/credential-provider-process": "^3.972.58", "@aws-sdk/credential-provider-sso": "^3.973.2", "@aws-sdk/credential-provider-web-identity": "^3.972.64", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Yr7yxNyQ8aHt9Ww0RPFUZx+xiem+vl7vuwhP0tniTijoesJNV5jou9HCgVpI0GEPAF+89TkOvilE5uRrZJnjaw=="], + "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-1nYitRCaDmXWUrpBJt6WlcGjLx1JVsMY8rlYuHHsTYTSaYikbixYdQSyINN2VYq1F798uTO9qHAzytL25M8g3A=="], + "@aws-sdk/client-secrets-manager/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.2", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/token-providers": "3.1087.0", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pjMLaLU/JZi5lVfmR14V1OZqRBTuMHf6AwGNZA0K9hK+JKtO3jcLBarfD8iq5oc8cSowvc/9R32sqMVXZPo6xQ=="], + "@aws-sdk/client-secrets-manager/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7Buc7p0OvDHW7iBsu4b+YdS0WnaFBDGKDfbVQqaac9dkWiSiUtIoarBDsA1RmOVXZijaZJDoHJFIQiicQvWRlQ=="], + "@aws-sdk/client-secrets-manager/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA=="], + "@aws-sdk/client-secrets-manager/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - "listr2/cli-truncate/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "@aws-sdk/client-secrets-manager/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], - "log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "@aws-sdk/client-sfn/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], - "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "@aws-sdk/client-sfn/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.73", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-tjsxMkTAFkmiV9ycmymapb9nLECWVOwFs0bZMQ9gB9bnbY8/HwfukHZlWbXZZp7qkPU6EXAfOcMm3DioFFEywA=="], + "@aws-sdk/client-sfn/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], + "@aws-sdk/client-sfn/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], + "@aws-sdk/client-sfn/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1102.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Ua700vVvM1q105yABSUQWkCK6FeTrNfU6ORGetJe5BzkZWY7QhkF7SVTOlmDGWRDNd6jbyY0Dv5e+E4bMBEmLg=="], + "@aws-sdk/client-sfn/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], + "@aws-sdk/client-sfn/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.74", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-0AQfDcf99TNmqVKv0owHrw/TQs6i4ZE5t9qmz6NvO53bE/sA/tpXhXL9AAcEP1qHc6Zzjd1UMb69+/9zdhvY3g=="], + "@aws-sdk/client-ssm/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], + "@aws-sdk/client-ssm/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], + "@aws-sdk/client-ssm/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1103.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-N4wy26MNn31ItGVHYHPrEuCIFY4MBBjC+C5v1lJKqIUSA7OZBdhleCY53zCCrXn27hsk7YNOaTuhQu807S4AfQ=="], + "@aws-sdk/client-ssm/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], + "@aws-sdk/client-ssm/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YQoSI4d6kXvoenoG/0Jv/PqaAuukHzGmGXGyHBQYeEUNsYovlNAn/Sw1wp/WQbhcQ3HsEMGgjEahvD3igz6ecQ=="], + "@aws-sdk/client-ssm/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], + "@aws-sdk/client-ssm/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], + "@aws-sdk/client-sts/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1087.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-umM+qNq16f2fH+VLM5MqXW4ORNQAjk+TOSto73xbUHcKaU41L48j786r3UWQYlejeJk37NlvRYgxBT+MBkfaYQ=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], + "@aws-sdk/client-sts/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], - "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "@aws-sdk/client-sts/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], - "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "@aws-sdk/client-sts/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/client-sts/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/credential-provider-cognito-identity/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/credential-provider-cognito-identity/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/credential-provider-env/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/credential-provider-env/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/credential-provider-http/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/credential-provider-http/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/credential-provider-http/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/credential-provider-ini/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/credential-provider-ini/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/credential-provider-login/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/credential-provider-login/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-BAkxdoe7tpDDqCghGpuOeHQRbm/2znVvOQm0AvpQbA2tbfMN46doN4zx65fv85ImP3KADwc2zQPmbrlI9MPfMg=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.62", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g/0fGqKTb9xpKdd9AtpmV5Eo3DFKbnkpA2+w0peISSlu7NfAoWOuYBFxsu+yWBtxU89ka55ezoZBCbFaS8pjYQ=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.5", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/credential-provider-env": "^3.972.60", "@aws-sdk/credential-provider-http": "^3.972.62", "@aws-sdk/credential-provider-login": "^3.972.67", "@aws-sdk/credential-provider-process": "^3.972.60", "@aws-sdk/credential-provider-sso": "^3.973.4", "@aws-sdk/credential-provider-web-identity": "^3.972.66", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/credential-provider-imds": "^4.4.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-ylubazcRfq2TVus/qXucSXeC42Qdjp5HQxTu68K/BsdMiZlcSLD1zkpoCgApXZX1Y6YJhtGGs7ZHhO/GuIgBlw=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YIo3f99hM43QdYG8hDzwGemnR/pU95b0kramqSJUTleCqaB7+HwKf7YZFHqvOgTqZTPx/mRmNIqoDRr3U0Z3Tw=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.4", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/token-providers": "3.1092.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-BPdmL8sSBOCv4ngZ+3LHxyc3CNqDCEK37CHioCk7zGrTMY5sUtkH8q+o6qA80nn6w3/fyBPGNE7OIRlmoOxRQA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-kSAziJboOmZmsR9/MTbiNjowl2BPes1bQuJpne4qAZ62ubi8fjfr/aupJSQje6udBoYxXTQbsL0e0kby2la3ng=="], + + "@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.13", "", { "dependencies": { "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-X+2HNZhWi5i3rJsCas0LPf6fTQUaKyJ40zd8aTO/bwpRfpU3biYaqLr7C1WMibL7PVKJalpi1PyybjGPNoHC8Q=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/credential-provider-process/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/credential-provider-process/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/credential-provider-sso/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/credential-provider-sso/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/credential-provider-web-identity/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/credential-provider-web-identity/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-providers/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/credential-providers/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/ec2-metadata-service/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/ec2-metadata-service/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/ec2-metadata-service/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/ec2-metadata-service/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/lib-storage/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/middleware-sdk-ec2/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/middleware-sdk-ec2/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/middleware-sdk-ec2/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/middleware-sdk-ec2/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/middleware-sdk-ec2/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/middleware-sdk-route53/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/middleware-sdk-route53/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/middleware-sdk-s3/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/nested-clients/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], + + "@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], + + "@aws-sdk/nested-clients/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/signature-v4-multi-region/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/token-providers/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@aws-sdk/token-providers/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@smithy/config-resolver/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@smithy/credential-provider-imds/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@smithy/middleware-endpoint/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@smithy/node-config-provider/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@smithy/property-provider/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@smithy/shared-ini-file-loader/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@smithy/util-retry/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "@smithy/util-waiter/@smithy/core": ["@smithy/core@3.33.0", "", { "dependencies": { "@smithy/types": "^4.17.0", "tslib": "^2.6.2" } }, "sha512-uKbkxgqLyepQDZoq8aRSdUqD1ID//rOqG96ixBhp++O7vBtmwYM6fwldGhr9HJP0iYrdc7GP/AlgzPWEZIrNRg=="], + + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "color/color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], + + "color-string/color-name": ["color-name@2.1.1", "", {}, "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg=="], + + "ink/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "ink/wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], + + "listr2/cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + + "listr2/wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], + + "log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "react-devtools-core/ws": ["ws@7.5.12", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg=="], + + "table/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "table/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "table/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@aws-cdk/cloudformation-diff/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "@aws-cdk/cloudformation-diff/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@aws-sdk/checksums/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/checksums/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-appsync/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-appsync/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-appsync/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-appsync/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-appsync/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-appsync/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bOzP2+zdJ0XrghywB4FaJXtGZCx9yS0AGps+VJ5yEgg30wVyHNmVDBwVDXcRypzQY5iLGCS3NSn0nsuISqjFCQ=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.68", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lkunS8X+H6V76WE+t/uGQm/U8v0JXK5mLfNFTUAMlE1kqaCjwlmqKJrgCVtqjK/vqnlrSWsLK4Lr4NANBWlfTQ=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.11", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/credential-provider-env": "^3.972.66", "@aws-sdk/credential-provider-http": "^3.972.68", "@aws-sdk/credential-provider-login": "^3.972.73", "@aws-sdk/credential-provider-process": "^3.972.66", "@aws-sdk/credential-provider-sso": "^3.973.10", "@aws-sdk/credential-provider-web-identity": "^3.972.72", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-KoDEolYtLHG/8C+IiZpXbJWyBOMkrHV+j66Kb9PBXmLv5euGb7aELvuCmLenoGAV6gBW2wM7TsG/1e5iulH4kA=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YOnX6bIhdjx0QfaENu2PB0eFm5MEc9ft8XNGQ+NxMfeLSq9aE+XjWCwDupEnV4UWv5ZFpBLJbTREIx7KNOoqpQ=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.10", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/token-providers": "3.1102.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-IsXnQ35j5VE+3ZK6aIhT5ypB+Jim3zRwVz0nYuVwyBKZyu/SYx+O2/LQpng8c2EiuwyqceabsDlYrICHDlJPsA=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-nj9Zlsy7ya+fy+jhWTJwgfr7YdtDM4xHyZvgKuftuny0UgROVx9lxwvsWJSLvpKk4lig0m0tHng3k1fEnt0LeA=="], + + "@aws-sdk/client-cloudcontrol/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-cloudcontrol/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-cloudcontrol/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-cloudcontrol/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-cloudcontrol/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-cloudcontrol/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-cloudformation/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-cloudformation/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-cloudformation/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-cloudformation/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-cloudformation/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-cloudformation/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-cloudtrail/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-cloudtrail/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-cloudtrail/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-cloudtrail/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-cloudtrail/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-cloudtrail/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-rcIpk5kxUqDaaNa6Xk23pQ6ViY7jlqzmfFWCahQcBT97ddXaXYYwzCen9Tz1Jvo6aJft6wDl5bN44/Jw5B4oLA=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-nggwJtZ4eeNsUw5IeWBMXsi1ryct5idi0K+/SCRF3kybLubOMaNTb3XCihXpWMiVpyzyPeIrl0zTkzhBH9porA=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.12", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-login": "^3.972.74", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-IlUEejorGTWKb4/Dm7K5Yw4QxUmXLThLhrvBmzVBqZFTbW72cv9LTcITmo1dsnYriALE4h68mOq4LB99x6sQ7Q=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.11", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/token-providers": "3.1103.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-gAQBkBZxUB84d71+pPcI9L+jh2ujhuAVxc/4FgGiWFDjkPBlMKxzd5XDtkSXTFX8Ro7ansnT88+XadasxMeCRw=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.73", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-SnlEmQa6SjOgs6iOPLUQl1Eyq4AKiAdPQlkOhFhqNfDtDCwibMGvL6QlkSmf3o6vAUSImzdPCxowT5dfQUZP1A=="], + + "@aws-sdk/client-codebuild/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-codebuild/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-codebuild/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-codebuild/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-codebuild/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-codebuild/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ec2/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-ec2/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-ec2/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ec2/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ec2/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ec2/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ecr/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-ecr/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-ecr/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ecr/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ecr/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ecr/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ecs/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-ecs/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-ecs/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ecs/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ecs/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ecs/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-iam/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.35", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pXzaWe3evZhjxDXAlMnqISe/XefTCGwBJG4nFTXaWSgAnMkqPEhxEPqJNhhpGesEvKFhvNpnozJJ4GTL11bRYw=="], + + "@aws-sdk/client-iam/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-vyGtvK1rY940eq7JT0yIGKuZ+2kpPSJcHibSvGlit5oiMFDamzC7cxBGLl4FLnd6suihMXDI2FSF2dL6TmBqPA=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g9b9YzDrD5pcKiPBJfCSXRfFMrA39eR0guUhZ5SRm+7vMAVc43+effxbcamxBjSd5bUhrdKo5te/yQuWurLXLA=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.2", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/credential-provider-env": "^3.972.58", "@aws-sdk/credential-provider-http": "^3.972.60", "@aws-sdk/credential-provider-login": "^3.972.64", "@aws-sdk/credential-provider-process": "^3.972.58", "@aws-sdk/credential-provider-sso": "^3.973.2", "@aws-sdk/credential-provider-web-identity": "^3.972.64", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Yr7yxNyQ8aHt9Ww0RPFUZx+xiem+vl7vuwhP0tniTijoesJNV5jou9HCgVpI0GEPAF+89TkOvilE5uRrZJnjaw=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-1nYitRCaDmXWUrpBJt6WlcGjLx1JVsMY8rlYuHHsTYTSaYikbixYdQSyINN2VYq1F798uTO9qHAzytL25M8g3A=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.2", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/token-providers": "3.1087.0", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pjMLaLU/JZi5lVfmR14V1OZqRBTuMHf6AwGNZA0K9hK+JKtO3jcLBarfD8iq5oc8cSowvc/9R32sqMVXZPo6xQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7Buc7p0OvDHW7iBsu4b+YdS0WnaFBDGKDfbVQqaac9dkWiSiUtIoarBDsA1RmOVXZijaZJDoHJFIQiicQvWRlQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA=="], + + "@aws-sdk/client-kms/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-kms/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-kms/@aws-sdk/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-kms/@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-kms/@aws-sdk/types/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-kms/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-kms/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-kms/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-lambda/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-lambda/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-lambda/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-lambda/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-lambda/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-route-53/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-route-53/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-route-53/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-route-53/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-route-53/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-route-53/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-s3/@aws-sdk/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-s3/@aws-sdk/types/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-s3/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-s3/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-secrets-manager/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-secrets-manager/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-secrets-manager/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-secrets-manager/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-secrets-manager/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-secrets-manager/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-sfn/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-sfn/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-sfn/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-sfn/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-sfn/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-sfn/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ssm/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-ssm/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-ssm/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ssm/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ssm/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ssm/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-sts/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/client-sts/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/client-sts/@aws-sdk/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-sts/@aws-sdk/types/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-sts/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/client-sts/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-sts/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-provider-http/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-http/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CCygIKJ9YbI3n84OClSaSppkgKKHVj2TGT33c6FRORZrYNZQ1POmD+ip0FLYokiJAK7sSdc3YVkOsBm90oxWMQ=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.34", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/signature-v4-multi-region": "^3.996.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.34", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/signature-v4-multi-region": "^3.996.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1092.0", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hBYUAr6iBLNFcsiWTgtBb0stdSw39VOUq4Sp4A5caCNf66BAZplWN4FleKrVpJx5li2YgdnK2DqoFSMWC642FQ=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.34", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/signature-v4-multi-region": "^3.996.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-providers/@aws-sdk/types/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/credential-providers/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/ec2-metadata-service/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/ec2-metadata-service/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/lib-storage/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@aws-sdk/middleware-sdk-ec2/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/middleware-sdk-ec2/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/middleware-sdk-ec2/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + + "@smithy/config-resolver/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@smithy/middleware-endpoint/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@smithy/node-config-provider/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@smithy/property-provider/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@smithy/shared-ini-file-loader/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@smithy/util-retry/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "@smithy/util-waiter/@smithy/core/@smithy/types": ["@smithy/types@4.17.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg=="], + + "color/color-convert/color-name": ["color-name@2.1.1", "", {}, "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg=="], + + "listr2/cli-truncate/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + + "log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "table/slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "table/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "table/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@aws-cdk/cloudformation-diff/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@aws-sdk/checksums/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/checksums/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-appsync/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-appsync/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.73", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-tjsxMkTAFkmiV9ycmymapb9nLECWVOwFs0bZMQ9gB9bnbY8/HwfukHZlWbXZZp7qkPU6EXAfOcMm3DioFFEywA=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1102.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Ua700vVvM1q105yABSUQWkCK6FeTrNfU6ORGetJe5BzkZWY7QhkF7SVTOlmDGWRDNd6jbyY0Dv5e+E4bMBEmLg=="], + + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], + + "@aws-sdk/client-cloudcontrol/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-cloudcontrol/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-cloudformation/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-cloudformation/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-cloudtrail/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-cloudtrail/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.74", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-0AQfDcf99TNmqVKv0owHrw/TQs6i4ZE5t9qmz6NvO53bE/sA/tpXhXL9AAcEP1qHc6Zzjd1UMb69+/9zdhvY3g=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1103.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-N4wy26MNn31ItGVHYHPrEuCIFY4MBBjC+C5v1lJKqIUSA7OZBdhleCY53zCCrXn27hsk7YNOaTuhQu807S4AfQ=="], + + "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], + + "@aws-sdk/client-codebuild/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-codebuild/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ec2/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ec2/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ecr/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ecr/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ecs/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ecs/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YQoSI4d6kXvoenoG/0Jv/PqaAuukHzGmGXGyHBQYeEUNsYovlNAn/Sw1wp/WQbhcQ3HsEMGgjEahvD3igz6ecQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1087.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-umM+qNq16f2fH+VLM5MqXW4ORNQAjk+TOSto73xbUHcKaU41L48j786r3UWQYlejeJk37NlvRYgxBT+MBkfaYQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], + + "@aws-sdk/client-kms/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-kms/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-lambda/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-lambda/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-route-53/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-route-53/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-secrets-manager/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-secrets-manager/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-sfn/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-sfn/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-ssm/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-ssm/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/client-sts/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/client-sts/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.41", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.41", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.41", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], diff --git a/package.json b/package.json index 279d871a6..136d2f8ae 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "typescript": "^5" }, "dependencies": { + "@aws-cdk/toolkit-lib": "^1.38.2", "@aws-sdk/client-bedrock-agentcore": "^3.1092.0", "@aws-sdk/client-bedrock-agentcore-control": "^3.1102.0", "@aws-sdk/client-cloudwatch-logs": "^3.1092.0", diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index a355dc4b4..488f86817 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -11,6 +11,7 @@ import { type ProjectEvent, } from "../../handlers/project/types"; import { createSilentLogger } from "../../testing"; +import type { CdkEvent, CdkOperation, CdkRunOptions } from "../../io"; const originalCwd = process.cwd(); const tempDirectories: string[] = []; @@ -254,10 +255,22 @@ describe("FsProjectManager.build", () => { const events = await drain(subject.build(project)); + const cdkDir = join(directory, "example", "agentcore", "cdk"); expect(commands).toEqual([ { - command: ["npm", "run", "cdk", "--", "synth", "--quiet"], - cwd: join(directory, "example", "agentcore", "cdk"), + // --output pins the assembly where deploy looks for it, so a project that + // sets cdk.json's `output` cannot send synth somewhere deploy never reads. + command: [ + "npm", + "run", + "cdk", + "--", + "synth", + "--quiet", + "--output", + join(cdkDir, "cdk.out"), + ], + cwd: cdkDir, }, ]); expect(events).toEqual([{ message: "Synthesizing CloudFormation templates" }]); @@ -301,6 +314,289 @@ describe("FsProjectManager.build", () => { }); }); +describe("FsProjectManager.deploy", () => { + // Synth is pinned to the directory deploy reads, so both name it the same way. + function synthCommand(directory: string): string[] { + return [ + "npm", + "run", + "cdk", + "--", + "synth", + "--quiet", + "--output", + assemblyDirectory(directory), + ]; + } + const REGION = "us-west-2"; + + type RecordedCommand = { command: string[]; cwd: string }; + type RecordedCdkRun = { operation: CdkOperation; options: CdkRunOptions }; + + // Like manager(), but also records the CDK operations the manager drives, and lets + // a test supply the events one of them emits (and the failure it ends with). + function deployManager( + onCdk?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, + ): { manager: FsProjectManager; commands: RecordedCommand[]; runs: RecordedCdkRun[] } { + const commands: RecordedCommand[] = []; + const runs: RecordedCdkRun[] = []; + return { + manager: new FsProjectManager({ + logger: createSilentLogger(), + runner: async (command, { cwd }) => { + commands.push({ command, cwd }); + }, + cdk: async function* (operation, options) { + runs.push({ operation, options }); + const events: CdkEvent[] = []; + let failure: unknown; + try { + onCdk?.(operation, (event) => events.push(event)); + } catch (error) { + failure = error; + } + // Emit before throwing, as the real runner does: the output explaining a + // failure is only useful if the consumer sees it. + yield* events; + if (failure) throw failure; + }, + checkTool: async () => {}, + }), + commands, + runs, + }; + } + + // The assembly directory every operation reads, which build synthesized into. + function assemblyDirectory(directory: string): string { + return join(directory, "example", "agentcore", "cdk", "cdk.out"); + } + + // deploy() builds first, so the CDK app's node_modules must exist; create() with + // skipInstall never produces them. Targets overwrite the empty list create() + // scaffolds; null leaves that empty list in place, and a string is written verbatim + // so a test can supply invalid JSON. + async function scaffolded( + subject: FsProjectManager, + directory: string, + targets: unknown = [{ name: "default", account: "111122223333", region: "us-east-1" }], + ): Promise { + const { project } = await runCreate(subject, { + name: "example", + template: PROJECT_TEMPLATES.HELLO_WORLD_PYTHON, + skipInstall: true, + skipGit: true, + }); + await mkdir(join(directory, "example", "agentcore", "cdk", "node_modules"), { + recursive: true, + }); + if (targets !== null) { + await writeFile( + join(directory, "example", "agentcore", "aws-targets.json"), + typeof targets === "string" ? targets : JSON.stringify(targets), + ); + } + return project; + } + + async function drain(generator: AsyncGenerator): Promise { + const events: ProjectEvent[] = []; + for await (const event of generator) events.push(event); + return events; + } + + test("synthesizes, bootstraps the target environment, then deploys every stack", async () => { + const directory = await inTempDirectory(); + const { manager: subject, commands, runs } = deployManager(); + const project = await scaffolded(subject, directory); + commands.length = 0; // discard create()'s commands + const cdkDir = join(directory, "example", "agentcore", "cdk"); + const options = { assemblyDirectory: assemblyDirectory(directory), region: REGION }; + + const events = await drain(subject.deploy(project, { region: REGION, skipBootstrap: false })); + + // Only synthesis shells out; everything that reaches AWS goes through the toolkit. + expect(commands).toEqual([{ command: synthCommand(directory), cwd: cdkDir }]); + expect(runs).toEqual([ + { operation: { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, options }, + { operation: { kind: "deploy" }, options }, + ]); + expect(events).toEqual([ + { message: "Synthesizing CloudFormation templates" }, + { message: "Bootstrapping aws://111122223333/us-east-1" }, + { message: "Deploying stacks" }, + ]); + }); + + test("synthesizes into the same directory the toolkit deploys from", async () => { + const directory = await inTempDirectory(); + const { manager: subject, commands, runs } = deployManager(); + const project = await scaffolded(subject, directory); + commands.length = 0; + + await drain(subject.deploy(project, { region: REGION, skipBootstrap: true })); + + // The invariant behind passing --output at all: whatever synth was told to write + // is exactly what the toolkit is handed. Left to cdk.json's `output`, synth could + // write elsewhere and deploy would ship a stale assembly while reporting success. + const assembly = assemblyDirectory(directory); + expect(commands[0]?.command.at(-1)).toBe(assembly); + expect(runs.map(({ options }) => options.assemblyDirectory)).toEqual([assembly]); + }); + + test("bootstraps each distinct environment once, however many targets share it", async () => { + const directory = await inTempDirectory(); + const { manager: subject, runs } = deployManager(); + const project = await scaffolded(subject, directory, [ + { name: "alpha", account: "111122223333", region: "us-east-1" }, + { name: "beta", account: "111122223333", region: "us-east-1" }, // same environment + { name: "gamma", account: "444455556666", region: "eu-west-1" }, + ]); + + await drain(subject.deploy(project, { region: REGION, skipBootstrap: false })); + + expect( + runs.flatMap(({ operation }) => + operation.kind === "bootstrap" ? operation.environments : [], + ), + ).toEqual(["aws://111122223333/us-east-1", "aws://444455556666/eu-west-1"]); + }); + + test("skips bootstrapping when asked, and still deploys", async () => { + const directory = await inTempDirectory(); + const { manager: subject, commands, runs } = deployManager(); + const project = await scaffolded(subject, directory); + commands.length = 0; + const cdkDir = join(directory, "example", "agentcore", "cdk"); + + const events = await drain(subject.deploy(project, { region: REGION, skipBootstrap: true })); + + expect(commands).toEqual([{ command: synthCommand(directory), cwd: cdkDir }]); + expect(runs.map(({ operation }) => operation)).toEqual([{ kind: "deploy" }]); + expect(events).not.toContainEqual({ message: "Bootstrapping aws://111122223333/us-east-1" }); + }); + + test("names the file to fix when no deployment targets are configured", async () => { + const directory = await inTempDirectory(); + const { manager: subject, commands, runs } = deployManager(); + // create() scaffolds an empty list, which is what a fresh project has. + const project = await scaffolded(subject, directory, null); + commands.length = 0; + + await expect( + drain(subject.deploy(project, { region: REGION, skipBootstrap: false })), + ).rejects.toThrow(/aws-targets\.json/); + // Nothing ran at all: a deploy with nowhere to go does not even synthesize. + expect(commands).toEqual([]); + expect(runs).toEqual([]); + }); + + test("reports a malformed aws-targets.json as an actionable error", async () => { + const directory = await inTempDirectory(); + const { manager: subject } = deployManager(); + const project = await scaffolded(subject, directory, "{ not a target list"); + + await expect( + drain(subject.deploy(project, { region: REGION, skipBootstrap: false })), + ).rejects.toThrow(/is not a valid list of deployment targets/); + }); + + test("rejects targets that omit a required field", async () => { + const directory = await inTempDirectory(); + const { manager: subject } = deployManager(); + const project = await scaffolded(subject, directory, [ + { name: "default", region: "us-east-1" }, + ]); + + await expect( + drain(subject.deploy(project, { region: REGION, skipBootstrap: false })), + ).rejects.toThrow(/is not a valid list of deployment targets/); + }); + + test("streams the CDK toolkit's messages as they arrive", async () => { + const directory = await inTempDirectory(); + const { manager: subject } = deployManager((operation, emit) => { + if (operation.kind !== "deploy") return; + emit({ level: "info", message: "example-stack: creating CloudFormation changeset..." }); + emit({ level: "result", message: "example-stack: deployed" }); + }); + const project = await scaffolded(subject, directory); + + const events = await drain(subject.deploy(project, { region: REGION, skipBootstrap: true })); + + expect(events.map((event) => event.output).filter(Boolean)).toEqual([ + "example-stack: creating CloudFormation changeset...", + "example-stack: deployed", + ]); + }); + + test("keeps the toolkit's debug and trace messages off screen", async () => { + const directory = await inTempDirectory(); + const { manager: subject } = deployManager((operation, emit) => { + if (operation.kind !== "deploy") return; + emit({ level: "debug", message: "resolved 3 environments" }); + emit({ level: "trace", message: "sdk call: DescribeStacks" }); + emit({ level: "warn", message: "example-stack: no changes" }); + }); + const project = await scaffolded(subject, directory); + + const events = await drain(subject.deploy(project, { region: REGION, skipBootstrap: true })); + + // The suppressed ones are still in the debug log; only the warning is surfaced. + expect(events.map((event) => event.output).filter(Boolean)).toEqual([ + "example-stack: no changes", + ]); + }); + + test("yields the output that explains a failure before propagating it", async () => { + const directory = await inTempDirectory(); + const { manager: subject } = deployManager((operation, emit) => { + if (operation.kind !== "deploy") return; + emit({ level: "error", message: "example-stack: CREATE_FAILED" }); + throw new Error("cdk deploy exploded"); + }); + const project = await scaffolded(subject, directory); + + const events: ProjectEvent[] = []; + const generator = subject.deploy(project, { region: REGION, skipBootstrap: true }); + await expect( + (async () => { + for await (const event of generator) events.push(event); + })(), + ).rejects.toThrow("cdk deploy exploded"); + expect(events).toContainEqual({ output: "example-stack: CREATE_FAILED" }); + }); + + test("refuses a project managed by a backend it cannot deploy", async () => { + const directory = await inTempDirectory(); + const { manager: subject, commands, runs } = deployManager(); + const project = await scaffolded(subject, directory); + commands.length = 0; + + // CDK is the only backend today; the cast stands in for a future one. + const foreign = { ...project, managedBy: "Terraform" as Project["managedBy"] }; + await expect( + drain(subject.deploy(foreign, { region: REGION, skipBootstrap: false })), + ).rejects.toThrow(/unsupported backend: Terraform/); + expect(commands).toEqual([]); + expect(runs).toEqual([]); + }); + + test("fails before touching AWS when the CDK dependencies are missing", async () => { + const directory = await inTempDirectory(); + const { manager: subject, commands, runs } = deployManager(); + const project = await scaffolded(subject, directory); + await rm(join(directory, "example", "agentcore", "cdk", "node_modules"), { recursive: true }); + commands.length = 0; + + await expect( + drain(subject.deploy(project, { region: REGION, skipBootstrap: false })), + ).rejects.toThrow(/npm install/); + expect(commands).toEqual([]); + expect(runs).toEqual([]); + }); +}); + describe("FsProjectManager.resolve", () => { test("round-trips a project it just created", async () => { const root = await inTempDirectory(); diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index dbbce6106..b18e8ccf5 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -2,6 +2,7 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import type { CreateProjectInput, + DeployProjectOptions, ResolveProjectInput, Project, ProjectManager, @@ -13,13 +14,18 @@ import type { Logger } from "../../logging"; import { FsReadWriteJson, requireTool, + runCdk, runProcess, + type CdkOperation, + type CdkRunner, + type CdkRunOptions, type ProcessRunner, type ReadWriteJson, } from "../../io"; import { defaultSource, type AssetSource } from "./source"; import { createProjectTreeFromTemplate, TEMPLATES } from "./templates"; import { ProjectSpecSchema } from "../../projectSchemas/project"; +import { AwsTargetsSchema, type AwsTarget } from "../../projectSchemas/aws-targets"; import { enclosingProjectRoot } from "./fsUtils"; import { DeserializationError, @@ -28,10 +34,14 @@ import { ProjectStateError, } from "../../errors/errors"; +// Shown when aws-targets.json names no usable target, so the fix is on screen. +const TARGETS_EXAMPLE = `[{ "name": "default", "account": "111122223333", "region": "us-east-1" }]`; + type ProjectManagerConfig = { logger: Logger; source?: AssetSource; // Bun executable or dist/assets depending on runtime runner?: ProcessRunner; // injectable so tests never spawn real processes + cdk?: CdkRunner; // injectable so tests never reach AWS checkTool?: typeof requireTool; // injectable so tests don't depend on the host's PATH json?: ReadWriteJson; // injectable so tests read fixtures instead of disk }; @@ -43,6 +53,7 @@ export class FsProjectManager implements ProjectManager { private readonly logger: Logger; private readonly source: AssetSource; private readonly runner: ProcessRunner; + private readonly cdk: CdkRunner; private readonly checkTool: typeof requireTool; private readonly json: ReadWriteJson; @@ -50,6 +61,7 @@ export class FsProjectManager implements ProjectManager { this.logger = config.logger; this.source = config.source ?? defaultSource(); this.runner = config.runner ?? runProcess; + this.cdk = config.cdk ?? runCdk; this.checkTool = config.checkTool ?? requireTool; this.json = config.json ?? new FsReadWriteJson({ logger: config.logger }); } @@ -174,8 +186,122 @@ export class FsProjectManager implements ProjectManager { // The generated package.json defines `cdk` as "npm run build && cdk", so this // single command compiles the app and then synthesizes it. Synthesis needs no // AWS credentials: each stack's environment comes from aws-targets.json. + // + // --output is passed explicitly rather than left to cdk.json, because deploy + // reads this directory back. A project that sets cdk.json's `output` would + // otherwise send synth somewhere deploy never looks, and deploy would ship + // whatever stale assembly it found there while reporting success. yield { message: "Synthesizing CloudFormation templates" }; - await this.run(["npm", "run", "cdk", "--", "synth", "--quiet"], cdkDir); + await this.run( + ["npm", "run", "cdk", "--", "synth", "--quiet", "--output", this.assemblyPath(project)], + cdkDir, + ); + } + + // Where build writes the synthesized assembly and deploy reads it from. Both go + // through here so the two can never disagree about the location. + private assemblyPath(project: Project): string { + return join(project.rootPath, "agentcore", "cdk", "cdk.out"); + } + + public async *deploy( + project: Project, + options: DeployProjectOptions, + ): AsyncGenerator { + // Dispatched the same way build is: the backend that owns the artifacts owns + // how they reach AWS. + switch (project.managedBy) { + case "CDK": + yield* this.deployWithCdk(project, options); + break; + default: { + // Exhaustiveness: a new ManagedBy member fails to compile until it is handled. + const unsupported: never = project.managedBy; + throw new ProjectStateError( + `project '${project.name}' declares an unsupported backend: ${String(unsupported)}`, + ); + } + } + } + + // Synthesizes, bootstraps each target environment, then deploys every stack. + private async *deployWithCdk( + project: Project, + options: DeployProjectOptions, + ): AsyncGenerator { + // Every stack's environment comes from aws-targets.json, so an empty list means + // there is nowhere to deploy. Resolving an account from the active credentials + // would let deploy guess where the user's infrastructure belongs; name the file + // to fix instead, which also keeps deploy from depending on a working STS call. + // + // Read before synthesizing: the generated CDK app refuses to synth an empty + // list too, so checking first replaces its message with one naming the file to + // edit, and skips a synth that could not have produced anything deployable. + const targets = await this.readDeploymentTargets(project.rootPath); + if (targets.length === 0) { + throw new ProjectStateError( + `No deployment targets are configured for project '${project.name}'. ` + + `Add at least one to ${this.targetsPath(project.rootPath)}, e.g.:\n\n${TARGETS_EXAMPLE}`, + ); + } + + // Deploying exactly what was just synthesized keeps the two from drifting, and + // build's dependency check runs before anything touches AWS. + yield* this.buildWithCdk(project); + + // build synthesized into cdk.out; deploy reads that assembly rather than + // re-synthesizing, so what reaches AWS is exactly what was synthesized. + const run = { + assemblyDirectory: this.assemblyPath(project), + region: options.region, + }; + + if (!options.skipBootstrap) { + // Bootstrap is idempotent and no-ops quickly on an already-current + // environment, so it runs every deploy rather than probing CloudFormation + // first. Targets often share an environment, so deduplicate first. + const environments = [ + ...new Set(targets.map((target) => `aws://${target.account}/${target.region}`)), + ]; + yield { message: `Bootstrapping ${environments.join(", ")}` }; + yield* this.streamCdk({ kind: "bootstrap", environments }, run); + } + + yield { message: "Deploying stacks" }; + yield* this.streamCdk({ kind: "deploy" }, run); + } + + // Drives a CDK operation, surfacing the toolkit's messages as project events and + // logging them. Anything below info stays in the debug log rather than on screen. + private async *streamCdk( + operation: CdkOperation, + options: CdkRunOptions, + ): AsyncGenerator { + for await (const event of this.cdk(operation, options)) { + this.logger.debug(event.message); + if (event.level === "debug" || event.level === "trace") continue; + yield { output: event.message }; + } + } + + // Reads aws-targets.json, treating a missing file as empty and surfacing a parse + // failure as an actionable error that names the file rather than a Zod dump. + private async readDeploymentTargets(projectRoot: string): Promise { + const path = this.targetsPath(projectRoot); + if (!existsSync(path)) return []; + try { + return await this.json.read(path, AwsTargetsSchema); + } catch (cause) { + if (!(cause instanceof DeserializationError)) throw cause; + throw new ProjectStateError( + `${path} is not a valid list of deployment targets. Fix it, e.g.:\n\n${TARGETS_EXAMPLE}`, + { cause }, + ); + } + } + + private targetsPath(projectRoot: string): string { + return join(projectRoot, "agentcore", "aws-targets.json"); } // Runs a command with its output streamed to the file logger. diff --git a/src/handlers/project/build/index.ts b/src/handlers/project/build/index.ts index 8c41c21ec..bc184b652 100644 --- a/src/handlers/project/build/index.ts +++ b/src/handlers/project/build/index.ts @@ -18,7 +18,9 @@ export const createBuildProjectHandler = (config: BuildProjectHandlerConfig) => // Progress goes to stderr, keeping stdout for machine output. Subprocess // output goes to the debug log; on failure ProcessFailedError carries it. for await (const event of config.projectManager.build(project)) { - config.io.stderr.write(`${event.message}\n`); + if (event.message) { + config.io.stderr.write(`${event.message}\n`); + } } config.io.stderr.write(`Built project '${project.name}'\n`); diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 6ee6dd7e9..940b2307e 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -35,7 +35,9 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = skipInstall: flags["skip-install"], skipGit: flags["skip-git"], })) { - config.io.stderr.write(`${event.message}\n`); + if (event.message) { + config.io.stderr.write(`${event.message}\n`); + } } config.io.stderr.write(`Created project '${flags["name"]}' in ./${flags["name"]}\n`); diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index f73a75565..5e3e7fbaa 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -1,11 +1,43 @@ -import { createHandler } from "../../../router"; -import { NotImplementedError } from "../../../errors"; +import z from "zod"; +import { createHandler, flag, ProjectKey } from "../../../router"; +import type { AppIO } from "../../../io"; +import { RegionKey } from "../../keys"; +import type { ProjectManager } from "../types"; -export const createDeployProjectHandler = () => +type DeployProjectHandlerConfig = { + projectManager: ProjectManager; + io: AppIO; +}; + +export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) => createHandler({ name: "deploy", description: "deploy the project to AWS", - handle: async () => { - throw new NotImplementedError("agentcore project deploy is not implemented yet"); + flags: [ + flag( + "skip-bootstrap", + "skip bootstrapping the target environments before deploying", + z.boolean().default(false), + ), + ], + handle: async (ctx, flags) => { + // withProject has already resolved the enclosing project. + const project = ctx.require(ProjectKey); + + // Progress and the CDK toolkit's own output both go to stderr, keeping stdout + // for machine output. + for await (const event of config.projectManager.deploy(project, { + region: ctx.require(RegionKey), + skipBootstrap: flags["skip-bootstrap"], + })) { + if (event.message) { + config.io.stderr.write(`${event.message}\n`); + } + if (event.output) { + config.io.stderr.write(`${event.output}\n`); + } + } + + config.io.stderr.write(`Deployed project '${project.name}'\n`); }, }); diff --git a/src/handlers/project/index.ts b/src/handlers/project/index.ts index 91d0350ad..ae1994ede 100644 --- a/src/handlers/project/index.ts +++ b/src/handlers/project/index.ts @@ -24,7 +24,6 @@ export function createProjectHandler(config: ProjectHandlerConfig): Router { project.handler(createAddProjectResourceHandler(config)); project.handler(createRemoveProjectHandler()); project.handler(createDevProjectHandler()); - project.handler(createDeployProjectHandler()); project.handler(createStatusProjectHandler()); // withProject wraps only the commands that require an existing project, so // `create` (which refuses to nest inside one) stays unaffected. @@ -33,6 +32,11 @@ export function createProjectHandler(config: ProjectHandlerConfig): Router { createBuildProjectHandler({ projectManager: config.projectManager, io: config.io }), ), ); + project.handler( + withProject({ projectManager: config.projectManager })( + createDeployProjectHandler({ projectManager: config.projectManager, io: config.io }), + ), + ); return project; } diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index d9dce3bba..4622a6ba6 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -1,5 +1,5 @@ import { afterEach, test, expect, describe } from "bun:test"; -import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { createRootHandler } from "../index"; @@ -10,10 +10,20 @@ import { testIO, } from "../../testing"; import { InputValidationError } from "../../errors"; +import type { CdkEvent, CdkOperation } from "../../io"; -async function run(args: string[]) { +// The synth command build and deploy both issue. --output pins the assembly to the +// directory deploy reads, so cdk.json's own `output` cannot send the two apart. +function SYNTH(cdkDir: string): string[] { + return ["npm", "run", "cdk", "--", "synth", "--quiet", "--output", join(cdkDir, "cdk.out")]; +} + +async function run( + args: string[], + onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, +) { const io = testIO(); - const core = new TestCoreClient(); + const core = new TestCoreClient({ onCdkOperation }); const root = createRootHandler(core, { io: io.io, globalConfigAccessor: new TestGlobalConfigAccessor(), @@ -23,7 +33,7 @@ async function run(args: string[]) { return { io, core }; } -describe.each(["remove", "dev", "deploy", "status"])("project %s", (command) => { +describe.each(["remove", "dev", "status"])("project %s", (command) => { test("throws because it is not implemented yet", async () => { await expect(run([command])).rejects.toThrow(/not implemented/); }); @@ -365,12 +375,8 @@ describe("project build", () => { const projectRoot = await inBuildableProject(); const { io, core } = await run(["build"]); - expect(core.projectCommands).toEqual([ - { - command: ["npm", "run", "cdk", "--", "synth", "--quiet"], - cwd: join(projectRoot, "agentcore", "cdk"), - }, - ]); + const cdkDir = join(projectRoot, "agentcore", "cdk"); + expect(core.projectCommands).toEqual([{ command: SYNTH(cdkDir), cwd: cdkDir }]); expect(io.stderr()).toContain("Synthesizing CloudFormation templates"); expect(io.stderr()).toContain("Built project 'MyAgent'"); }); @@ -398,3 +404,75 @@ describe("project build", () => { await expect(run(["build"])).rejects.toThrow(/npm install/); }); }); + +describe("project deploy", () => { + // Scaffolds a project with one deployment target, then runs from inside it so + // withProject resolves it. create() scaffolds an empty target list, which deploy + // refuses, so the file is filled in here. + async function inProject(): Promise { + const directory = await inTempDirectory(); + await run(["create", "--name", "MyAgent", "--skip-install", "--skip-git"]); + + const projectRoot = join(directory, "MyAgent"); + // create --skip-install leaves no node_modules, which the build step requires. + await mkdir(join(projectRoot, "agentcore", "cdk", "node_modules"), { recursive: true }); + await writeFile( + join(projectRoot, "agentcore", "aws-targets.json"), + JSON.stringify([{ name: "default", account: "111122223333", region: "us-east-1" }]), + ); + process.chdir(projectRoot); + return projectRoot; + } + + test("synthesizes, bootstraps, and deploys the enclosing project", async () => { + const projectRoot = await inProject(); + const { io, core } = await run(["deploy"]); + + const cdkDir = join(projectRoot, "agentcore", "cdk"); + // Synthesis shells out; bootstrap and deploy go through the CDK toolkit, which + // reads the assembly synthesis just wrote. + expect(core.projectCommands).toEqual([{ command: SYNTH(cdkDir), cwd: cdkDir }]); + const options = { assemblyDirectory: join(cdkDir, "cdk.out"), region: "us-east-1" }; + expect(core.cdkRuns).toEqual([ + { operation: { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, options }, + { operation: { kind: "deploy" }, options }, + ]); + expect(io.stderr()).toContain("Bootstrapping aws://111122223333/us-east-1"); + expect(io.stderr()).toContain("Deploying stacks"); + expect(io.stderr()).toContain("Deployed project 'MyAgent'"); + }); + + test("--skip-bootstrap deploys without bootstrapping", async () => { + const projectRoot = await inProject(); + const { io, core } = await run(["deploy", "--skip-bootstrap"]); + + const cdkDir = join(projectRoot, "agentcore", "cdk"); + expect(core.projectCommands).toEqual([{ command: SYNTH(cdkDir), cwd: cdkDir }]); + expect(core.cdkRuns.map(({ operation }) => operation)).toEqual([{ kind: "deploy" }]); + expect(io.stderr()).not.toContain("Bootstrapping"); + expect(io.stderr()).toContain("Deployed project 'MyAgent'"); + }); + + test("writes the CDK toolkit's own messages to stderr", async () => { + await inProject(); + const { io } = await run(["deploy", "--skip-bootstrap"], (operation, emit) => { + if (operation.kind === "deploy") { + emit({ level: "info", message: "example-stack | 1/2 | CREATE_IN_PROGRESS" }); + } + }); + + expect(io.stderr()).toContain("example-stack | 1/2 | CREATE_IN_PROGRESS\n"); + }); + + test("fails with actionable guidance when no targets are configured", async () => { + const projectRoot = await inProject(); + await writeFile(join(projectRoot, "agentcore", "aws-targets.json"), "[]"); + + await expect(run(["deploy"])).rejects.toThrow(/aws-targets\.json/); + }); + + test("fails with actionable guidance outside a project", async () => { + await inTempDirectory(); + await expect(run(["deploy"])).rejects.toThrow(/No AgentCore project found/); + }); +}); diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 7e8c25cb5..292f7eda0 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -32,9 +32,21 @@ export type CreateProjectInput = { skipGit?: boolean; }; -/** A progress step reported while a long-running project operation runs. */ +/** + * A progress step reported while a long-running project operation runs, or a line + * of output from the tool it drives. One of the two is set per event: a step is + * this CLI's own wording, while output is forwarded as the tool phrased it. + */ export type ProjectEvent = { - message: string; + message?: string; + output?: string; +}; + +export type DeployProjectOptions = { + /** The resolved AWS region, forwarded to the CDK subprocesses. */ + region: string; + /** Skip bootstrapping the target environments before deploying. */ + skipBootstrap: boolean; }; export type ResolveProjectInput = { @@ -62,6 +74,9 @@ export interface ProjectManager { /** Compile the project's CDK app and synthesize its CloudFormation templates. */ build(project: Project): AsyncGenerator; + /** Build the project, then deploy the synthesized stacks to its deployment targets. */ + deploy(project: Project, options: DeployProjectOptions): AsyncGenerator; + /** Locate an existing AgentCore project. Returns undefined if no project can be found. */ resolve(input: ResolveProjectInput): Promise; diff --git a/src/io/cdk.ts b/src/io/cdk.ts new file mode 100644 index 000000000..ba6b74179 --- /dev/null +++ b/src/io/cdk.ts @@ -0,0 +1,109 @@ +// Programmatic CDK operations via @aws-cdk/toolkit-lib. Deploy drives the toolkit +// in-process rather than shelling out to `npx cdk`, so its progress arrives as +// structured messages and its failures as typed errors instead of scraped stdout. +import { + BaseCredentials, + BootstrapEnvironments, + BootstrapStackParameters, + Toolkit, + type IIoHost, + type IoMessageLevel, +} from "@aws-cdk/toolkit-lib"; + +/** A message emitted by the toolkit while an operation runs. */ +export type CdkEvent = { + level: IoMessageLevel; + message: string; +}; + +export type CdkOperation = + /** Prepare each environment (`aws://account/region`) to receive stacks. */ + | { kind: "bootstrap"; environments: string[] } + /** Deploy every stack in the synthesized assembly. */ + | { kind: "deploy" }; + +export type CdkRunOptions = { + /** + * Directory holding the synthesized cloud assembly. Deploy reads the assembly + * `build` produced rather than re-synthesizing, so what reaches AWS is exactly + * what was synthesized. Unused by bootstrap, which needs no assembly. + */ + assemblyDirectory: string; + /** + * Region for the toolkit's own SDK calls. Each stack's account and region come + * from the assembly, which the CDK app derives from aws-targets.json. + */ + region: string; +}; + +/** + * Runs a CDK operation, yielding the toolkit's messages as they arrive. + * Injectable so tests exercise deploy without reaching AWS. + */ +export type CdkRunner = ( + operation: CdkOperation, + options: CdkRunOptions, +) => AsyncGenerator; + +/** + * The real runner. The toolkit pushes messages at an IIoHost while the caller + * pulls them from a generator, so messages land in a queue this drains; a failure + * propagates only once the queue is empty, so the error arrives after the output + * that explains it. + */ +export const runCdk: CdkRunner = async function* (operation, options) { + const queue: CdkEvent[] = []; + let wake = () => {}; + let settled = false; + let failure: unknown; + + const ioHost: IIoHost = { + notify: async (message) => { + queue.push({ level: message.level, message: message.message }); + wake(); + }, + // Never prompt: there is no guaranteed TTY, so an approval prompt would hang + // rather than fail. The suggested default is the documented non-interactive + // response, and matches the `--require-approval never` this replaces. + requestResponse: async (request) => request.defaultResponse, + }; + + const toolkit = new Toolkit({ + ioHost, + // Mirrors the AWS SDK's own precedence, with the resolved region as the default. + sdkConfig: { + baseCredentials: BaseCredentials.awsCliCompatible({ defaultRegion: options.region }), + }, + }); + + void (async () => { + if (operation.kind === "bootstrap") { + await toolkit.bootstrap(BootstrapEnvironments.fromList(operation.environments), { + // Provisions a customer-managed KMS key for the staging bucket, matching + // the parameters the original CLI bootstraps with. + parameters: BootstrapStackParameters.withExisting({ createCustomerMasterKey: true }), + }); + return; + } + const source = await toolkit.fromAssemblyDirectory(options.assemblyDirectory); + await toolkit.deploy(source); + })() + .catch((error: unknown) => { + failure = error; + }) + .finally(() => { + settled = true; + wake(); + }); + + while (!settled || queue.length > 0) { + if (queue.length === 0) { + await new Promise((resolve) => { + wake = resolve; + }); + continue; + } + yield queue.shift()!; + } + if (failure) throw failure; +}; diff --git a/src/io/index.ts b/src/io/index.ts index e93169768..33f739fb3 100644 --- a/src/io/index.ts +++ b/src/io/index.ts @@ -4,6 +4,13 @@ export { type AtomicWriteStreamOptions, type AtomicWriteStreamSource, } from "./atomicWrite"; +export { + runCdk, + type CdkEvent, + type CdkOperation, + type CdkRunner, + type CdkRunOptions, +} from "./cdk"; export { MissingToolError, ProcessFailedError, diff --git a/src/projectSchemas/aws-targets.ts b/src/projectSchemas/aws-targets.ts new file mode 100644 index 000000000..72024706f --- /dev/null +++ b/src/projectSchemas/aws-targets.ts @@ -0,0 +1,12 @@ +import z from "zod"; + +/** + * Mirrors AwsDeploymentTarget in @aws/agentcore-cdk: each entry names the account and + * region the CDK app synthesizes a stack for. `agentcore project create` scaffolds an + * empty list, so a project has no targets until the user fills them in. + */ +export const AwsTargetsSchema = z.array( + z.object({ name: z.string(), account: z.string(), region: z.string() }), +); + +export type AwsTarget = z.infer[number]; diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 15ad2b854..fc9d7e092 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -144,6 +144,7 @@ import type { import { isTerminalStatus } from "../core/batchEvaluationResults"; import { abortable } from "../core/abortable"; import type { CoreOptions } from "../core/types"; +import type { CdkEvent, CdkOperation, CdkRunOptions } from "../io"; import type { ProjectManager } from "../handlers/project/types"; import type { Logger } from "../logging"; import { createSilentLogger } from "./logging"; @@ -1177,6 +1178,12 @@ export class TestGatewayClient implements CoreGatewayClient { type TestCoreClientOptions = { logger?: Logger; + /** + * Called in place of driving the CDK toolkit, so a test can stand in for it: + * `emit` yields a toolkit message to the operation's consumer, and throwing + * fails the operation. + */ + onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void; }; export class TestIdentityClient implements CoreIdentityClient { @@ -1945,12 +1952,33 @@ export class TestCoreClient implements Core { // recorded instead of spawned so tests stay fast and hermetic. readonly projectCommands: { command: string[]; cwd: string }[] = []; + // CDK operations the project manager would have driven, recorded instead of + // run so tests never reach AWS. + readonly cdkRuns: { operation: CdkOperation; options: CdkRunOptions }[] = []; + constructor(options?: TestCoreClientOptions) { + // A generator cannot be an arrow function, so close over the array instead of + // reaching for `this` inside it. + const cdkRuns = this.cdkRuns; this.projectManager = new FsProjectManager({ logger: options?.logger ?? createSilentLogger(), runner: async (command, { cwd }) => { this.projectCommands.push({ command, cwd }); }, + cdk: async function* (operation, runOptions) { + cdkRuns.push({ operation, options: runOptions }); + const emitted: CdkEvent[] = []; + let failure: unknown; + try { + options?.onCdkOperation?.(operation, (event) => emitted.push(event)); + } catch (error) { + failure = error; + } + // Emit before throwing, as the real runner does: the output explaining a + // failure is only useful if the consumer sees it. + yield* emitted; + if (failure) throw failure; + }, checkTool: async () => {}, // CI hosts don't have uv installed }); } From 5ae34c83d006b2f7fe7da1b18e5dfd6a155c5bcd Mon Sep 17 00:00:00 2001 From: notgitika Date: Fri, 14 Aug 2026 11:51:19 -0400 Subject: [PATCH 02/18] feat(project): deploy one target per invocation deploy shipped every stack in the assembly, so a project with a staging and a prod target reached both at once. It now takes --target, defaulting to "default", and bootstraps and deploys only that target. The target is resolved from aws-targets.json before synthesizing, so a misspelled --target costs no build and the error lists the configured names. Which stack belongs to the target comes from the synthesized manifest, matched on the agentcore:target-name tag the generated CDK app writes, rather than from the CLI reproducing that app's naming convention. The lookup runs before bootstrap so a mismatch fails in seconds, and the toolkit selects with PATTERN_MUST_MATCH so a name the assembly does not contain fails loudly instead of deploying nothing. --- src/core/project/assembly.ts | 62 +++++++++++ src/core/project/manager.test.ts | 158 ++++++++++++++++++++++----- src/core/project/manager.tsx | 34 ++++-- src/handlers/project/deploy/index.ts | 8 +- src/handlers/project/project.test.ts | 60 +++++++++- src/handlers/project/types.ts | 4 +- src/io/cdk.ts | 15 ++- 7 files changed, 294 insertions(+), 47 deletions(-) create mode 100644 src/core/project/assembly.ts diff --git a/src/core/project/assembly.ts b/src/core/project/assembly.ts new file mode 100644 index 000000000..d8d9815c3 --- /dev/null +++ b/src/core/project/assembly.ts @@ -0,0 +1,62 @@ +// Reads the cloud assembly `build` synthesized, so deploy can find the stack that +// belongs to a deployment target. +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import z from "zod"; +import { ProjectStateError } from "../../errors/errors"; +import type { ReadWriteJson } from "../../io"; + +// The tag the generated CDK app puts on every stack, naming the deployment target +// the stack was synthesized for. Selecting on it means the CLI never has to +// reproduce the app's stack-naming convention, so a project that renames its +// stacks still deploys. +const TARGET_TAG = "agentcore:target-name"; + +const STACK_ARTIFACT = "aws:cloudformation:stack"; + +// Only the parts of the manifest deploy reads. Artifacts are keyed by their +// hierarchical id, which is what the CDK toolkit matches stack patterns against. +const AssemblyManifestSchema = z.object({ + artifacts: z + .record( + z.string(), + z.object({ + type: z.string(), + properties: z.object({ tags: z.record(z.string(), z.string()).optional() }).optional(), + }), + ) + .default({}), +}); + +/** + * The name of the stack in the synthesized assembly that belongs to `target`. + * + * The generated CDK app synthesizes one stack per deployment target and tags each + * with the target's name, so deploy asks the assembly which stack to ship rather + * than deriving the name itself and hoping the two agree. + */ +export async function stackForTarget( + json: ReadWriteJson, + assemblyDirectory: string, + target: string, +): Promise { + const path = join(assemblyDirectory, "manifest.json"); + // deploy synthesizes immediately before this, so a missing manifest means synth + // wrote somewhere else entirely rather than that the user skipped a step. + if (!existsSync(path)) { + throw new ProjectStateError(`No synthesized cloud assembly was found at ${path}.`); + } + + const manifest = await json.read(path, AssemblyManifestSchema); + const stacks = Object.entries(manifest.artifacts).filter( + ([, artifact]) => artifact.type === STACK_ARTIFACT, + ); + const match = stacks.find(([, artifact]) => artifact.properties?.tags?.[TARGET_TAG] === target); + if (!match) { + throw new ProjectStateError( + `The synthesized cloud assembly has no stack for deployment target '${target}'. ` + + `${path} defines ${stacks.length} stack(s), none tagged ${TARGET_TAG}='${target}'.`, + ); + } + return match[0]; +} diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index 488f86817..a9cbe2fc3 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -372,10 +372,43 @@ describe("FsProjectManager.deploy", () => { return join(directory, "example", "agentcore", "cdk", "cdk.out"); } + // How the generated CDK app names the stack it synthesizes for a target. + function stackName(target: string): string { + return `AgentCore-example-${target}`; + } + + // Stands in for what synth leaves behind: a manifest with one stack per target, + // each tagged with the target it belongs to. deploy reads it to find the stack to + // ship, and the stubbed runner never writes one. + async function synthesized(directory: string, targetNames: string[]): Promise { + const assembly = assemblyDirectory(directory); + await mkdir(assembly, { recursive: true }); + await writeFile( + join(assembly, "manifest.json"), + JSON.stringify({ + version: "36.0.0", + artifacts: { + // A non-stack artifact, as a real assembly has: only stacks are candidates. + Tree: { type: "cdk:tree" }, + ...Object.fromEntries( + targetNames.map((target) => [ + stackName(target), + { + type: "aws:cloudformation:stack", + properties: { tags: { "agentcore:target-name": target } }, + }, + ]), + ), + }, + }), + ); + } + // deploy() builds first, so the CDK app's node_modules must exist; create() with // skipInstall never produces them. Targets overwrite the empty list create() // scaffolds; null leaves that empty list in place, and a string is written verbatim - // so a test can supply invalid JSON. + // so a test can supply invalid JSON. A well-formed list also gets the assembly + // synth would have produced for it. async function scaffolded( subject: FsProjectManager, directory: string, @@ -396,6 +429,12 @@ describe("FsProjectManager.deploy", () => { typeof targets === "string" ? targets : JSON.stringify(targets), ); } + if (Array.isArray(targets)) { + await synthesized( + directory, + (targets as { name?: string }[]).flatMap(({ name }) => (name ? [name] : [])), + ); + } return project; } @@ -405,7 +444,7 @@ describe("FsProjectManager.deploy", () => { return events; } - test("synthesizes, bootstraps the target environment, then deploys every stack", async () => { + test("synthesizes, bootstraps the target environment, then deploys its stack", async () => { const directory = await inTempDirectory(); const { manager: subject, commands, runs } = deployManager(); const project = await scaffolded(subject, directory); @@ -413,18 +452,20 @@ describe("FsProjectManager.deploy", () => { const cdkDir = join(directory, "example", "agentcore", "cdk"); const options = { assemblyDirectory: assemblyDirectory(directory), region: REGION }; - const events = await drain(subject.deploy(project, { region: REGION, skipBootstrap: false })); + const events = await drain( + subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" }), + ); // Only synthesis shells out; everything that reaches AWS goes through the toolkit. expect(commands).toEqual([{ command: synthCommand(directory), cwd: cdkDir }]); expect(runs).toEqual([ { operation: { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, options }, - { operation: { kind: "deploy" }, options }, + { operation: { kind: "deploy", stackName: stackName("default") }, options }, ]); expect(events).toEqual([ { message: "Synthesizing CloudFormation templates" }, { message: "Bootstrapping aws://111122223333/us-east-1" }, - { message: "Deploying stacks" }, + { message: `Deploying ${stackName("default")}` }, ]); }); @@ -434,7 +475,9 @@ describe("FsProjectManager.deploy", () => { const project = await scaffolded(subject, directory); commands.length = 0; - await drain(subject.deploy(project, { region: REGION, skipBootstrap: true })); + await drain( + subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), + ); // The invariant behind passing --output at all: whatever synth was told to write // is exactly what the toolkit is handed. Left to cdk.json's `output`, synth could @@ -444,22 +487,71 @@ describe("FsProjectManager.deploy", () => { expect(runs.map(({ options }) => options.assemblyDirectory)).toEqual([assembly]); }); - test("bootstraps each distinct environment once, however many targets share it", async () => { + // The three-target project the target-selection tests share. + const TARGETS = [ + { name: "staging", account: "111122223333", region: "us-east-1" }, + { name: "prod", account: "444455556666", region: "eu-west-1" }, + { name: "default", account: "777788889999", region: "us-west-2" }, + ]; + + test("deploys only the requested target's stack, into only its environment", async () => { const directory = await inTempDirectory(); const { manager: subject, runs } = deployManager(); - const project = await scaffolded(subject, directory, [ - { name: "alpha", account: "111122223333", region: "us-east-1" }, - { name: "beta", account: "111122223333", region: "us-east-1" }, // same environment - { name: "gamma", account: "444455556666", region: "eu-west-1" }, + const project = await scaffolded(subject, directory, TARGETS); + + await drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "prod" })); + + // A project with a staging and a prod target cannot reach the others by + // accident: one deploy bootstraps one environment and ships one stack. + expect(runs.map(({ operation }) => operation)).toEqual([ + { kind: "bootstrap", environments: ["aws://444455556666/eu-west-1"] }, + { kind: "deploy", stackName: stackName("prod") }, + ]); + }); + + test("deploys the target named 'default' when none is requested", async () => { + const directory = await inTempDirectory(); + const { manager: subject, runs } = deployManager(); + const project = await scaffolded(subject, directory, TARGETS); + + // What the handler passes when --target is omitted, and the name the example in + // the empty-targets error uses. + await drain( + subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), + ); + + expect(runs.map(({ operation }) => operation)).toEqual([ + { kind: "deploy", stackName: stackName("default") }, ]); + }); + + test("names the configured targets when the requested one is not among them", async () => { + const directory = await inTempDirectory(); + const { manager: subject, commands, runs } = deployManager(); + const project = await scaffolded(subject, directory, TARGETS); + commands.length = 0; + + await expect( + drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "prd" })), + ).rejects.toThrow(/no deployment target named 'prd'.*staging, prod, default/s); + // Resolved before synthesizing, so a misspelled --target costs no build. + expect(commands).toEqual([]); + expect(runs).toEqual([]); + }); - await drain(subject.deploy(project, { region: REGION, skipBootstrap: false })); + test("fails when the synthesized assembly has no stack for the target", async () => { + const directory = await inTempDirectory(); + const { manager: subject, runs } = deployManager(); + const project = await scaffolded(subject, directory); + // An assembly synthesized from a different target list than the one on disk — + // what a hand-edited CDK app that stops tagging its stacks would leave behind. + await synthesized(directory, ["other"]); - expect( - runs.flatMap(({ operation }) => - operation.kind === "bootstrap" ? operation.environments : [], - ), - ).toEqual(["aws://111122223333/us-east-1", "aws://444455556666/eu-west-1"]); + await expect( + drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), + ).rejects.toThrow(/no stack for deployment target 'default'/); + // Resolved before bootstrapping, so nothing reached AWS. + expect(runs).toEqual([]); }); test("skips bootstrapping when asked, and still deploys", async () => { @@ -469,10 +561,14 @@ describe("FsProjectManager.deploy", () => { commands.length = 0; const cdkDir = join(directory, "example", "agentcore", "cdk"); - const events = await drain(subject.deploy(project, { region: REGION, skipBootstrap: true })); + const events = await drain( + subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), + ); expect(commands).toEqual([{ command: synthCommand(directory), cwd: cdkDir }]); - expect(runs.map(({ operation }) => operation)).toEqual([{ kind: "deploy" }]); + expect(runs.map(({ operation }) => operation)).toEqual([ + { kind: "deploy", stackName: stackName("default") }, + ]); expect(events).not.toContainEqual({ message: "Bootstrapping aws://111122223333/us-east-1" }); }); @@ -484,7 +580,7 @@ describe("FsProjectManager.deploy", () => { commands.length = 0; await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false })), + drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), ).rejects.toThrow(/aws-targets\.json/); // Nothing ran at all: a deploy with nowhere to go does not even synthesize. expect(commands).toEqual([]); @@ -497,7 +593,7 @@ describe("FsProjectManager.deploy", () => { const project = await scaffolded(subject, directory, "{ not a target list"); await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false })), + drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), ).rejects.toThrow(/is not a valid list of deployment targets/); }); @@ -509,7 +605,7 @@ describe("FsProjectManager.deploy", () => { ]); await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false })), + drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), ).rejects.toThrow(/is not a valid list of deployment targets/); }); @@ -522,7 +618,9 @@ describe("FsProjectManager.deploy", () => { }); const project = await scaffolded(subject, directory); - const events = await drain(subject.deploy(project, { region: REGION, skipBootstrap: true })); + const events = await drain( + subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), + ); expect(events.map((event) => event.output).filter(Boolean)).toEqual([ "example-stack: creating CloudFormation changeset...", @@ -540,7 +638,9 @@ describe("FsProjectManager.deploy", () => { }); const project = await scaffolded(subject, directory); - const events = await drain(subject.deploy(project, { region: REGION, skipBootstrap: true })); + const events = await drain( + subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), + ); // The suppressed ones are still in the debug log; only the warning is surfaced. expect(events.map((event) => event.output).filter(Boolean)).toEqual([ @@ -558,7 +658,11 @@ describe("FsProjectManager.deploy", () => { const project = await scaffolded(subject, directory); const events: ProjectEvent[] = []; - const generator = subject.deploy(project, { region: REGION, skipBootstrap: true }); + const generator = subject.deploy(project, { + region: REGION, + skipBootstrap: true, + target: "default", + }); await expect( (async () => { for await (const event of generator) events.push(event); @@ -576,7 +680,7 @@ describe("FsProjectManager.deploy", () => { // CDK is the only backend today; the cast stands in for a future one. const foreign = { ...project, managedBy: "Terraform" as Project["managedBy"] }; await expect( - drain(subject.deploy(foreign, { region: REGION, skipBootstrap: false })), + drain(subject.deploy(foreign, { region: REGION, skipBootstrap: false, target: "default" })), ).rejects.toThrow(/unsupported backend: Terraform/); expect(commands).toEqual([]); expect(runs).toEqual([]); @@ -590,7 +694,7 @@ describe("FsProjectManager.deploy", () => { commands.length = 0; await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false })), + drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), ).rejects.toThrow(/npm install/); expect(commands).toEqual([]); expect(runs).toEqual([]); diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index b18e8ccf5..8a180d973 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -22,6 +22,7 @@ import { type ProcessRunner, type ReadWriteJson, } from "../../io"; +import { stackForTarget } from "./assembly"; import { defaultSource, type AssetSource } from "./source"; import { createProjectTreeFromTemplate, TEMPLATES } from "./templates"; import { ProjectSpecSchema } from "../../projectSchemas/project"; @@ -224,7 +225,7 @@ export class FsProjectManager implements ProjectManager { } } - // Synthesizes, bootstraps each target environment, then deploys every stack. + // Synthesizes, bootstraps the target's environment, then deploys its stack. private async *deployWithCdk( project: Project, options: DeployProjectOptions, @@ -245,6 +246,19 @@ export class FsProjectManager implements ProjectManager { ); } + // One deploy ships one target, so a project with a staging and a prod target + // cannot reach prod by accident. Resolved before synthesizing so a misspelled + // --target costs nothing, and because bootstrap needs the target's environment. + const target = targets.find((candidate) => candidate.name === options.target); + if (!target) { + throw new ProjectStateError( + `Project '${project.name}' has no deployment target named '${options.target}'. ` + + `${this.targetsPath(project.rootPath)} defines: ${targets + .map((candidate) => candidate.name) + .join(", ")}.`, + ); + } + // Deploying exactly what was just synthesized keeps the two from drifting, and // build's dependency check runs before anything touches AWS. yield* this.buildWithCdk(project); @@ -256,19 +270,21 @@ export class FsProjectManager implements ProjectManager { region: options.region, }; + // Resolved before bootstrapping so an assembly without a stack for this target + // fails immediately rather than after minutes of bootstrapping. + const stackName = await stackForTarget(this.json, run.assemblyDirectory, target.name); + if (!options.skipBootstrap) { // Bootstrap is idempotent and no-ops quickly on an already-current // environment, so it runs every deploy rather than probing CloudFormation - // first. Targets often share an environment, so deduplicate first. - const environments = [ - ...new Set(targets.map((target) => `aws://${target.account}/${target.region}`)), - ]; - yield { message: `Bootstrapping ${environments.join(", ")}` }; - yield* this.streamCdk({ kind: "bootstrap", environments }, run); + // first. + const environment = `aws://${target.account}/${target.region}`; + yield { message: `Bootstrapping ${environment}` }; + yield* this.streamCdk({ kind: "bootstrap", environments: [environment] }, run); } - yield { message: "Deploying stacks" }; - yield* this.streamCdk({ kind: "deploy" }, run); + yield { message: `Deploying ${stackName}` }; + yield* this.streamCdk({ kind: "deploy", stackName }, run); } // Drives a CDK operation, surfacing the toolkit's messages as project events and diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index 5e3e7fbaa..66b76c1ef 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -14,9 +14,14 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = name: "deploy", description: "deploy the project to AWS", flags: [ + flag( + "target", + "name of the aws-targets.json entry to deploy to", + z.string().default("default"), + ), flag( "skip-bootstrap", - "skip bootstrapping the target environments before deploying", + "skip bootstrapping the target environment before deploying", z.boolean().default(false), ), ], @@ -29,6 +34,7 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = for await (const event of config.projectManager.deploy(project, { region: ctx.require(RegionKey), skipBootstrap: flags["skip-bootstrap"], + target: flags.target, })) { if (event.message) { config.io.stderr.write(`${event.message}\n`); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 4622a6ba6..99ef25b7d 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -18,6 +18,11 @@ function SYNTH(cdkDir: string): string[] { return ["npm", "run", "cdk", "--", "synth", "--quiet", "--output", join(cdkDir, "cdk.out")]; } +// How the generated CDK app names the stack it synthesizes for a target. +function STACK(target: string): string { + return `AgentCore-MyAgent-${target}`; +} + async function run( args: string[], onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, @@ -409,16 +414,33 @@ describe("project deploy", () => { // Scaffolds a project with one deployment target, then runs from inside it so // withProject resolves it. create() scaffolds an empty target list, which deploy // refuses, so the file is filled in here. - async function inProject(): Promise { + async function inProject( + targets = [{ name: "default", account: "111122223333", region: "us-east-1" }], + ): Promise { const directory = await inTempDirectory(); await run(["create", "--name", "MyAgent", "--skip-install", "--skip-git"]); const projectRoot = join(directory, "MyAgent"); // create --skip-install leaves no node_modules, which the build step requires. await mkdir(join(projectRoot, "agentcore", "cdk", "node_modules"), { recursive: true }); + await writeFile(join(projectRoot, "agentcore", "aws-targets.json"), JSON.stringify(targets)); + // Synthesis is stubbed here, so stand in for the assembly it would have written: + // deploy reads the manifest to find the stack belonging to the target. + const assembly = join(projectRoot, "agentcore", "cdk", "cdk.out"); + await mkdir(assembly, { recursive: true }); await writeFile( - join(projectRoot, "agentcore", "aws-targets.json"), - JSON.stringify([{ name: "default", account: "111122223333", region: "us-east-1" }]), + join(assembly, "manifest.json"), + JSON.stringify({ + artifacts: Object.fromEntries( + targets.map(({ name }) => [ + STACK(name), + { + type: "aws:cloudformation:stack", + properties: { tags: { "agentcore:target-name": name } }, + }, + ]), + ), + }), ); process.chdir(projectRoot); return projectRoot; @@ -435,20 +457,46 @@ describe("project deploy", () => { const options = { assemblyDirectory: join(cdkDir, "cdk.out"), region: "us-east-1" }; expect(core.cdkRuns).toEqual([ { operation: { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, options }, - { operation: { kind: "deploy" }, options }, + { operation: { kind: "deploy", stackName: STACK("default") }, options }, ]); expect(io.stderr()).toContain("Bootstrapping aws://111122223333/us-east-1"); - expect(io.stderr()).toContain("Deploying stacks"); + expect(io.stderr()).toContain(`Deploying ${STACK("default")}`); + expect(io.stderr()).toContain("Deployed project 'MyAgent'"); + }); + + test("--target selects which configured target to deploy to", async () => { + const projectRoot = await inProject([ + { name: "default", account: "111122223333", region: "us-east-1" }, + { name: "prod", account: "444455556666", region: "eu-west-1" }, + ]); + const { io, core } = await run(["deploy", "--target", "prod"]); + + const cdkDir = join(projectRoot, "agentcore", "cdk"); + expect(core.projectCommands).toEqual([{ command: SYNTH(cdkDir), cwd: cdkDir }]); + expect(core.cdkRuns.map(({ operation }) => operation)).toEqual([ + { kind: "bootstrap", environments: ["aws://444455556666/eu-west-1"] }, + { kind: "deploy", stackName: STACK("prod") }, + ]); expect(io.stderr()).toContain("Deployed project 'MyAgent'"); }); + test("fails with actionable guidance when --target names no configured target", async () => { + await inProject(); + + await expect(run(["deploy", "--target", "prod"])).rejects.toThrow( + /no deployment target named 'prod'/, + ); + }); + test("--skip-bootstrap deploys without bootstrapping", async () => { const projectRoot = await inProject(); const { io, core } = await run(["deploy", "--skip-bootstrap"]); const cdkDir = join(projectRoot, "agentcore", "cdk"); expect(core.projectCommands).toEqual([{ command: SYNTH(cdkDir), cwd: cdkDir }]); - expect(core.cdkRuns.map(({ operation }) => operation)).toEqual([{ kind: "deploy" }]); + expect(core.cdkRuns.map(({ operation }) => operation)).toEqual([ + { kind: "deploy", stackName: STACK("default") }, + ]); expect(io.stderr()).not.toContain("Bootstrapping"); expect(io.stderr()).toContain("Deployed project 'MyAgent'"); }); diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 292f7eda0..8c06f4cc0 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -45,8 +45,10 @@ export type ProjectEvent = { export type DeployProjectOptions = { /** The resolved AWS region, forwarded to the CDK subprocesses. */ region: string; - /** Skip bootstrapping the target environments before deploying. */ + /** Skip bootstrapping the target environment before deploying. */ skipBootstrap: boolean; + /** Name of the aws-targets.json entry to deploy. */ + target: string; }; export type ResolveProjectInput = { diff --git a/src/io/cdk.ts b/src/io/cdk.ts index ba6b74179..ad8ad8785 100644 --- a/src/io/cdk.ts +++ b/src/io/cdk.ts @@ -5,6 +5,7 @@ import { BaseCredentials, BootstrapEnvironments, BootstrapStackParameters, + StackSelectionStrategy, Toolkit, type IIoHost, type IoMessageLevel, @@ -19,8 +20,8 @@ export type CdkEvent = { export type CdkOperation = /** Prepare each environment (`aws://account/region`) to receive stacks. */ | { kind: "bootstrap"; environments: string[] } - /** Deploy every stack in the synthesized assembly. */ - | { kind: "deploy" }; + /** Deploy one stack of the synthesized assembly, named as the assembly names it. */ + | { kind: "deploy"; stackName: string }; export type CdkRunOptions = { /** @@ -86,7 +87,15 @@ export const runCdk: CdkRunner = async function* (operation, options) { return; } const source = await toolkit.fromAssemblyDirectory(options.assemblyDirectory); - await toolkit.deploy(source); + // The assembly holds one stack per deployment target, and a deploy ships one + // target. MUST_MATCH so a name the assembly does not contain fails loudly + // instead of quietly deploying nothing. + await toolkit.deploy(source, { + stacks: { + strategy: StackSelectionStrategy.PATTERN_MUST_MATCH, + patterns: [operation.stackName], + }, + }); })() .catch((error: unknown) => { failure = error; From 78348f6de409e6f0d61c3c846a0e9a1ca81e2872 Mon Sep 17 00:00:00 2001 From: notgitika Date: Fri, 14 Aug 2026 19:16:23 -0400 Subject: [PATCH 03/18] refactor(project): load the CDK toolkit lazily and test the adapter The toolkit is the heaviest dependency in the CLI and src/io/index.ts re-exports runCdk, so a static import made every command load it: agentcore --help ran in 2.7s from source, 0.7s once the import moved inside the function that builds the toolkit. The compiled binary is unchanged either way, since --compile embeds the module regardless. src/io/cdk.ts splits into the three things a run does -- load the toolkit, perform one operation with it, bridge its reporting to a generator -- so each is reachable from a test. src/io/cdk.test.ts covers them against the real toolkit package: constructing a Toolkit and its BootstrapEnvironments, BootstrapStackParameters, and StackSelectionStrategy helpers resolves no credentials and calls no API, so the arguments a deploy passes are asserted against the values the toolkit itself defines rather than stand-ins. What the tests assert includes the ones a caller cannot see and a fake cannot check: that messages are yielded while the operation is still running, that a failure surfaces only after the output explaining it, that a request is answered with its suggested default rather than prompting, and that createCustomerMasterKey and PATTERN_MUST_MATCH reach the toolkit. The fake in TestCoreClient still buffers rather than streams; it now says so, and names the test that covers the real behaviour. ProjectEvent becomes a discriminated union. It documented that exactly one of step and output is set while typing both optional, which allowed {} and both-at-once and spread `if (event.output)` checks through three handlers. Both variants carry `message`, so a consumer that only writes text needs no switch, and those checks are gone. --- src/core/project/manager.test.ts | 40 +++-- src/core/project/manager.tsx | 16 +- src/handlers/project/build/index.ts | 4 +- src/handlers/project/create/index.ts | 4 +- src/handlers/project/deploy/index.ts | 7 +- src/handlers/project/types.ts | 17 ++- src/io/cdk.test.ts | 215 +++++++++++++++++++++++++++ src/io/cdk.ts | 123 +++++++++------ src/testing/TestCoreClient.tsx | 4 + 9 files changed, 350 insertions(+), 80 deletions(-) create mode 100644 src/io/cdk.test.ts diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index a9cbe2fc3..bb6723e4a 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -273,7 +273,7 @@ describe("FsProjectManager.build", () => { cwd: cdkDir, }, ]); - expect(events).toEqual([{ message: "Synthesizing CloudFormation templates" }]); + expect(events).toEqual([{ kind: "step", message: "Synthesizing CloudFormation templates" }]); }); test("fails actionably when the CDK dependencies are missing", async () => { @@ -444,6 +444,11 @@ describe("FsProjectManager.deploy", () => { return events; } + // The toolkit's own messages, as opposed to the steps this CLI words itself. + function forwarded(events: ProjectEvent[]): string[] { + return events.filter((event) => event.kind === "output").map((event) => event.message); + } + test("synthesizes, bootstraps the target environment, then deploys its stack", async () => { const directory = await inTempDirectory(); const { manager: subject, commands, runs } = deployManager(); @@ -463,9 +468,9 @@ describe("FsProjectManager.deploy", () => { { operation: { kind: "deploy", stackName: stackName("default") }, options }, ]); expect(events).toEqual([ - { message: "Synthesizing CloudFormation templates" }, - { message: "Bootstrapping aws://111122223333/us-east-1" }, - { message: `Deploying ${stackName("default")}` }, + { kind: "step", message: "Synthesizing CloudFormation templates" }, + { kind: "step", message: "Bootstrapping aws://111122223333/us-east-1" }, + { kind: "step", message: `Deploying ${stackName("default")}` }, ]); }); @@ -554,6 +559,20 @@ describe("FsProjectManager.deploy", () => { expect(runs).toEqual([]); }); + test("names the path it looked in when synthesis wrote no assembly", async () => { + const directory = await inTempDirectory(); + const { manager: subject, runs } = deployManager(); + const project = await scaffolded(subject, directory); + // Synthesis is stubbed in these tests, so removing the stand-in manifest is what + // a real synth writing somewhere else entirely would leave behind. + await rm(join(assemblyDirectory(directory), "manifest.json")); + + await expect( + drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), + ).rejects.toThrow(/No synthesized cloud assembly was found at .*manifest\.json/); + expect(runs).toEqual([]); + }); + test("skips bootstrapping when asked, and still deploys", async () => { const directory = await inTempDirectory(); const { manager: subject, commands, runs } = deployManager(); @@ -569,7 +588,10 @@ describe("FsProjectManager.deploy", () => { expect(runs.map(({ operation }) => operation)).toEqual([ { kind: "deploy", stackName: stackName("default") }, ]); - expect(events).not.toContainEqual({ message: "Bootstrapping aws://111122223333/us-east-1" }); + expect(events).not.toContainEqual({ + kind: "step", + message: "Bootstrapping aws://111122223333/us-east-1", + }); }); test("names the file to fix when no deployment targets are configured", async () => { @@ -622,7 +644,7 @@ describe("FsProjectManager.deploy", () => { subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), ); - expect(events.map((event) => event.output).filter(Boolean)).toEqual([ + expect(forwarded(events)).toEqual([ "example-stack: creating CloudFormation changeset...", "example-stack: deployed", ]); @@ -643,9 +665,7 @@ describe("FsProjectManager.deploy", () => { ); // The suppressed ones are still in the debug log; only the warning is surfaced. - expect(events.map((event) => event.output).filter(Boolean)).toEqual([ - "example-stack: no changes", - ]); + expect(forwarded(events)).toEqual(["example-stack: no changes"]); }); test("yields the output that explains a failure before propagating it", async () => { @@ -668,7 +688,7 @@ describe("FsProjectManager.deploy", () => { for await (const event of generator) events.push(event); })(), ).rejects.toThrow("cdk deploy exploded"); - expect(events).toContainEqual({ output: "example-stack: CREATE_FAILED" }); + expect(events).toContainEqual({ kind: "output", message: "example-stack: CREATE_FAILED" }); }); test("refuses a project managed by a backend it cannot deploy", async () => { diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 8a180d973..5c3fd8629 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -103,7 +103,7 @@ export class FsProjectManager implements ProjectManager { const destination = join(process.cwd(), input.name); this.logger.debug(`scaffolding project "${input.name}" from template "${input.template}"`); - yield { message: "Creating project tree" }; + yield { kind: "step", message: "Creating project tree" }; const tree = await createProjectTreeFromTemplate(input.name, input.template, this.source); await tree.write(destination); @@ -111,7 +111,7 @@ export class FsProjectManager implements ProjectManager { // user how to rerun the step by hand. if (!input.skipInstall) { await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); - yield { message: "Installing CDK dependencies with npm" }; + yield { kind: "step", message: "Installing CDK dependencies with npm" }; await this.run(["npm", "install"], join(destination, "agentcore", "cdk")); const appDir = join(destination, "app", TEMPLATES[input.template].appDir); @@ -120,14 +120,14 @@ export class FsProjectManager implements ProjectManager { "uv", "Install uv: https://docs.astral.sh/uv/getting-started/installation/", ); - yield { message: "Syncing Python dependencies with uv" }; + yield { kind: "step", message: "Syncing Python dependencies with uv" }; await this.run(["uv", "sync"], appDir); } } if (!input.skipGit) { await this.checkTool("git", "Install git: https://git-scm.com/downloads"); - yield { message: "Initializing git repository" }; + yield { kind: "step", message: "Initializing git repository" }; await this.run(["git", "init"], destination); } @@ -192,7 +192,7 @@ export class FsProjectManager implements ProjectManager { // reads this directory back. A project that sets cdk.json's `output` would // otherwise send synth somewhere deploy never looks, and deploy would ship // whatever stale assembly it found there while reporting success. - yield { message: "Synthesizing CloudFormation templates" }; + yield { kind: "step", message: "Synthesizing CloudFormation templates" }; await this.run( ["npm", "run", "cdk", "--", "synth", "--quiet", "--output", this.assemblyPath(project)], cdkDir, @@ -279,11 +279,11 @@ export class FsProjectManager implements ProjectManager { // environment, so it runs every deploy rather than probing CloudFormation // first. const environment = `aws://${target.account}/${target.region}`; - yield { message: `Bootstrapping ${environment}` }; + yield { kind: "step", message: `Bootstrapping ${environment}` }; yield* this.streamCdk({ kind: "bootstrap", environments: [environment] }, run); } - yield { message: `Deploying ${stackName}` }; + yield { kind: "step", message: `Deploying ${stackName}` }; yield* this.streamCdk({ kind: "deploy", stackName }, run); } @@ -296,7 +296,7 @@ export class FsProjectManager implements ProjectManager { for await (const event of this.cdk(operation, options)) { this.logger.debug(event.message); if (event.level === "debug" || event.level === "trace") continue; - yield { output: event.message }; + yield { kind: "output", message: event.message }; } } diff --git a/src/handlers/project/build/index.ts b/src/handlers/project/build/index.ts index bc184b652..8c41c21ec 100644 --- a/src/handlers/project/build/index.ts +++ b/src/handlers/project/build/index.ts @@ -18,9 +18,7 @@ export const createBuildProjectHandler = (config: BuildProjectHandlerConfig) => // Progress goes to stderr, keeping stdout for machine output. Subprocess // output goes to the debug log; on failure ProcessFailedError carries it. for await (const event of config.projectManager.build(project)) { - if (event.message) { - config.io.stderr.write(`${event.message}\n`); - } + config.io.stderr.write(`${event.message}\n`); } config.io.stderr.write(`Built project '${project.name}'\n`); diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 940b2307e..6ee6dd7e9 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -35,9 +35,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = skipInstall: flags["skip-install"], skipGit: flags["skip-git"], })) { - if (event.message) { - config.io.stderr.write(`${event.message}\n`); - } + config.io.stderr.write(`${event.message}\n`); } config.io.stderr.write(`Created project '${flags["name"]}' in ./${flags["name"]}\n`); diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index 66b76c1ef..f91df4a1b 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -36,12 +36,7 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = skipBootstrap: flags["skip-bootstrap"], target: flags.target, })) { - if (event.message) { - config.io.stderr.write(`${event.message}\n`); - } - if (event.output) { - config.io.stderr.write(`${event.output}\n`); - } + config.io.stderr.write(`${event.message}\n`); } config.io.stderr.write(`Deployed project '${project.name}'\n`); diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 8c06f4cc0..ea76e1e80 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -33,14 +33,17 @@ export type CreateProjectInput = { }; /** - * A progress step reported while a long-running project operation runs, or a line - * of output from the tool it drives. One of the two is set per event: a step is - * this CLI's own wording, while output is forwarded as the tool phrased it. + * Something worth showing the user while a long-running project operation runs. + * + * A union rather than two optional fields, so an event is always exactly one of the + * two and a consumer that only writes text can read `message` without checking + * which it got. */ -export type ProjectEvent = { - message?: string; - output?: string; -}; +export type ProjectEvent = + /** A progress step, in this CLI's own wording. */ + | { kind: "step"; message: string } + /** A line of output, forwarded as the tool that produced it phrased it. */ + | { kind: "output"; message: string }; export type DeployProjectOptions = { /** The resolved AWS region, forwarded to the CDK subprocesses. */ diff --git a/src/io/cdk.test.ts b/src/io/cdk.test.ts new file mode 100644 index 000000000..d39f1778e --- /dev/null +++ b/src/io/cdk.test.ts @@ -0,0 +1,215 @@ +import { test, expect, describe } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import type { IIoHost, IoMessage, IoRequest } from "@aws-cdk/toolkit-lib"; +// The real toolkit package, so the arguments these tests assert on are the values +// the toolkit itself defines rather than ones the tests made up. Imported statically +// here, unlike in cdk.ts, because a test file pays no startup cost. +import * as toolkit from "@aws-cdk/toolkit-lib"; +import { + loadCdkToolkit, + performCdkOperation, + runCdk, + streamCdkOperation, + type CdkEvent, + type CdkToolkit, + type CdkToolkitLib, +} from "./cdk"; + +// The toolkit reports far more than these two fields; a run only reads these. +function message(level: string, text: string): IoMessage { + return { level, message: text } as unknown as IoMessage; +} + +const lib: CdkToolkitLib = toolkit; + +async function collect(generator: AsyncGenerator): Promise { + const events: CdkEvent[] = []; + for await (const event of generator) events.push(event); + return events; +} + +describe("streamCdkOperation", () => { + test("yields the messages the operation reports, in order", async () => { + const events = await collect( + streamCdkOperation(async (ioHost) => { + await ioHost.notify(message("info", "first")); + await ioHost.notify(message("warn", "second")); + }), + ); + + expect(events).toEqual([ + { level: "info", message: "first" }, + { level: "warn", message: "second" }, + ]); + }); + + test("yields each message while the operation is still running", async () => { + // The point of the generator: a deploy runs for minutes, so the caller must see + // a message before the operation that produced it finishes. Reaching the second + // notify at all proves the first was consumed while the operation was awaiting. + let released = false; + const generator = streamCdkOperation(async (ioHost) => { + await ioHost.notify(message("info", "creating")); + await new Promise((resolve) => setTimeout(resolve, 1)); + released = true; + await ioHost.notify(message("info", "created")); + }); + + const first = await generator.next(); + expect(first.value).toEqual({ level: "info", message: "creating" }); + // Delivered before the operation reached its second half, not buffered until the end. + expect(released).toBe(false); + + expect(await collect(generator)).toEqual([{ level: "info", message: "created" }]); + }); + + test("throws the operation's failure only after draining the messages that explain it", async () => { + const generator = streamCdkOperation(async (ioHost) => { + await ioHost.notify(message("error", "example-stack | CREATE_FAILED")); + throw new Error("stack rollback complete"); + }); + + // The explanatory output arrives first... + expect(await generator.next()).toEqual({ + done: false, + value: { level: "error", message: "example-stack | CREATE_FAILED" }, + }); + // ...and the failure surfaces only once there is nothing left to read. + await expect(generator.next()).rejects.toThrow(/stack rollback complete/); + }); + + test("answers requests with the toolkit's own suggested default", async () => { + // Approval must never block on a prompt: there is no guaranteed TTY, so a + // request has to be answered without one. + let answer: unknown; + await collect( + streamCdkOperation(async (ioHost) => { + answer = await ioHost.requestResponse({ + defaultResponse: true, + } as unknown as IoRequest); + }), + ); + + expect(answer).toBe(true); + }); + + test("reports nothing for an operation that reports nothing", async () => { + expect(await collect(streamCdkOperation(async () => {}))).toEqual([]); + }); +}); + +describe("performCdkOperation", () => { + // Records what a run asks the toolkit to do, without doing it. + function stubToolkit() { + const calls: { method: string; args: unknown[] }[] = []; + const toolkit = { + bootstrap: async (...args: unknown[]) => { + calls.push({ method: "bootstrap", args }); + }, + fromAssemblyDirectory: async (...args: unknown[]) => { + calls.push({ method: "fromAssemblyDirectory", args }); + return { produce: undefined }; + }, + deploy: async (...args: unknown[]) => { + calls.push({ method: "deploy", args }); + }, + } as unknown as CdkToolkit; + return { toolkit, calls }; + } + + test("bootstraps the environments with a customer-managed key", async () => { + const { toolkit, calls } = stubToolkit(); + + await performCdkOperation( + { lib, toolkit }, + { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, + { assemblyDirectory: "/unused", region: "us-east-1" }, + ); + + expect(calls.map(({ method }) => method)).toEqual(["bootstrap"]); + const [environments, options] = calls[0]!.args as [ + { getEnvironments: () => Promise<{ name: string; account: string; region: string }[]> }, + { parameters: { parameters: Record } }, + ]; + expect(await environments.getEnvironments()).toEqual([ + { name: "aws://111122223333/us-east-1", account: "111122223333", region: "us-east-1" }, + ]); + // The parameter the original CLI bootstraps with. Dropping it would silently + // change what a default deploy provisions, so it is asserted rather than assumed. + expect(options.parameters.parameters).toEqual({ createCustomerMasterKey: true }); + }); + + test("deploys only the named stack, from the assembly build synthesized", async () => { + const { toolkit, calls } = stubToolkit(); + + await performCdkOperation( + { lib, toolkit }, + { kind: "deploy", stackName: "AgentCore-MyAgent-default" }, + { assemblyDirectory: "/tmp/example/cdk.out", region: "us-east-1" }, + ); + + expect(calls.map(({ method }) => method)).toEqual(["fromAssemblyDirectory", "deploy"]); + // Reads the directory build wrote rather than re-synthesizing. + expect(calls[0]!.args[0]).toBe("/tmp/example/cdk.out"); + const [, deployOptions] = calls[1]!.args as [unknown, { stacks: unknown }]; + // MUST_MATCH, so a stack the assembly does not contain fails loudly instead of + // deploying nothing and reporting success. + expect(deployOptions.stacks).toEqual({ + strategy: lib.StackSelectionStrategy.PATTERN_MUST_MATCH, + patterns: ["AgentCore-MyAgent-default"], + }); + }); + + test("does not bootstrap while deploying", async () => { + const { toolkit, calls } = stubToolkit(); + + await performCdkOperation( + { lib, toolkit }, + { kind: "deploy", stackName: "AgentCore-MyAgent-default" }, + { assemblyDirectory: "/tmp/example/cdk.out", region: "us-east-1" }, + ); + + expect(calls.map(({ method }) => method)).not.toContain("bootstrap"); + }); +}); + +describe("loadCdkToolkit", () => { + test("builds a toolkit without needing credentials", async () => { + // Constructing the toolkit resolves no credentials and calls no API, which is + // what lets the rest of these tests run offline. + const ioHost: IIoHost = { + notify: async () => {}, + requestResponse: async (request) => request.defaultResponse, + }; + + const { lib, toolkit } = await loadCdkToolkit(ioHost, "us-west-2"); + + expect(typeof toolkit.deploy).toBe("function"); + expect(typeof toolkit.bootstrap).toBe("function"); + // The lazily loaded module has to carry the helpers an operation calls, since + // a missing one would only surface at deploy time rather than at import time. + expect(typeof lib.BootstrapEnvironments.fromList).toBe("function"); + expect(typeof lib.BootstrapStackParameters.withExisting).toBe("function"); + expect(lib.StackSelectionStrategy.PATTERN_MUST_MATCH).toBeTruthy(); + }); +}); + +describe("runCdk", () => { + test("reports the toolkit's failure for an assembly directory that holds no assembly", async () => { + // The one path through the real toolkit that fails before it needs AWS, so the + // glue between loading, performing, and streaming is exercised end to end. + const directory = await mkdtemp(join(tmpdir(), "agentcore-cdk-")); + try { + const generator = runCdk( + { kind: "deploy", stackName: "AgentCore-MyAgent-default" }, + { assemblyDirectory: join(directory, "cdk.out"), region: "us-west-2" }, + ); + + await expect(collect(generator)).rejects.toThrow(); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); +}); diff --git a/src/io/cdk.ts b/src/io/cdk.ts index ad8ad8785..3f3664a64 100644 --- a/src/io/cdk.ts +++ b/src/io/cdk.ts @@ -1,15 +1,7 @@ // Programmatic CDK operations via @aws-cdk/toolkit-lib. Deploy drives the toolkit // in-process rather than shelling out to `npx cdk`, so its progress arrives as // structured messages and its failures as typed errors instead of scraped stdout. -import { - BaseCredentials, - BootstrapEnvironments, - BootstrapStackParameters, - StackSelectionStrategy, - Toolkit, - type IIoHost, - type IoMessageLevel, -} from "@aws-cdk/toolkit-lib"; +import type { IIoHost, IoMessageLevel, Toolkit } from "@aws-cdk/toolkit-lib"; /** A message emitted by the toolkit while an operation runs. */ export type CdkEvent = { @@ -47,12 +39,77 @@ export type CdkRunner = ( ) => AsyncGenerator; /** - * The real runner. The toolkit pushes messages at an IIoHost while the caller - * pulls them from a generator, so messages land in a queue this drains; a failure - * propagates only once the queue is empty, so the error arrives after the output - * that explains it. + * The toolkit methods an operation drives, narrowed to those actually called so a + * test can stand in for the toolkit without reaching AWS. */ -export const runCdk: CdkRunner = async function* (operation, options) { +export type CdkToolkit = Pick; + +/** The parts of the toolkit package an operation needs, loaded on demand. */ +export type CdkToolkitLib = Pick< + typeof import("@aws-cdk/toolkit-lib"), + "BootstrapEnvironments" | "BootstrapStackParameters" | "StackSelectionStrategy" +>; + +/** + * Loads the toolkit package and builds a toolkit that reports to `ioHost`. + * + * Loaded here rather than imported at module scope: the toolkit is the heaviest + * dependency in the CLI and `src/io` is reachable from every command, so a static + * import would make even `agentcore --help` pay to load a deploy it is not doing. + */ +export async function loadCdkToolkit( + ioHost: IIoHost, + region: string, +): Promise<{ lib: CdkToolkitLib; toolkit: CdkToolkit }> { + const lib = await import("@aws-cdk/toolkit-lib"); + const toolkit = new lib.Toolkit({ + ioHost, + // Mirrors the AWS SDK's own precedence, with the resolved region as the default. + sdkConfig: { + baseCredentials: lib.BaseCredentials.awsCliCompatible({ defaultRegion: region }), + }, + }); + return { lib, toolkit }; +} + +/** Performs one operation, awaiting the toolkit call it maps to. */ +export async function performCdkOperation( + { lib, toolkit }: { lib: CdkToolkitLib; toolkit: CdkToolkit }, + operation: CdkOperation, + options: CdkRunOptions, +): Promise { + if (operation.kind === "bootstrap") { + await toolkit.bootstrap(lib.BootstrapEnvironments.fromList(operation.environments), { + // Provisions a customer-managed KMS key for the staging bucket, matching + // the parameters the original CLI bootstraps with. + parameters: lib.BootstrapStackParameters.withExisting({ createCustomerMasterKey: true }), + }); + return; + } + + const source = await toolkit.fromAssemblyDirectory(options.assemblyDirectory); + // The assembly holds one stack per deployment target, and a deploy ships one + // target. MUST_MATCH so a name the assembly does not contain fails loudly + // instead of quietly deploying nothing. + await toolkit.deploy(source, { + stacks: { + strategy: lib.StackSelectionStrategy.PATTERN_MUST_MATCH, + patterns: [operation.stackName], + }, + }); +} + +/** + * Bridges the toolkit's push-based reporting to a pull-based generator. + * + * `drive` receives the `IIoHost` to report to and runs to completion while the + * caller pulls: messages land in a queue this drains, so they are yielded as they + * arrive rather than after the operation ends. A failure propagates only once the + * queue is empty, so the error arrives after the output that explains it. + */ +export async function* streamCdkOperation( + drive: (ioHost: IIoHost) => Promise, +): AsyncGenerator { const queue: CdkEvent[] = []; let wake = () => {}; let settled = false; @@ -69,34 +126,7 @@ export const runCdk: CdkRunner = async function* (operation, options) { requestResponse: async (request) => request.defaultResponse, }; - const toolkit = new Toolkit({ - ioHost, - // Mirrors the AWS SDK's own precedence, with the resolved region as the default. - sdkConfig: { - baseCredentials: BaseCredentials.awsCliCompatible({ defaultRegion: options.region }), - }, - }); - - void (async () => { - if (operation.kind === "bootstrap") { - await toolkit.bootstrap(BootstrapEnvironments.fromList(operation.environments), { - // Provisions a customer-managed KMS key for the staging bucket, matching - // the parameters the original CLI bootstraps with. - parameters: BootstrapStackParameters.withExisting({ createCustomerMasterKey: true }), - }); - return; - } - const source = await toolkit.fromAssemblyDirectory(options.assemblyDirectory); - // The assembly holds one stack per deployment target, and a deploy ships one - // target. MUST_MATCH so a name the assembly does not contain fails loudly - // instead of quietly deploying nothing. - await toolkit.deploy(source, { - stacks: { - strategy: StackSelectionStrategy.PATTERN_MUST_MATCH, - patterns: [operation.stackName], - }, - }); - })() + void drive(ioHost) .catch((error: unknown) => { failure = error; }) @@ -115,4 +145,11 @@ export const runCdk: CdkRunner = async function* (operation, options) { yield queue.shift()!; } if (failure) throw failure; -}; +} + +/** The real runner: loads the toolkit, then streams the operation it performs. */ +export const runCdk: CdkRunner = (operation, options) => + streamCdkOperation(async (ioHost) => { + const loaded = await loadCdkToolkit(ioHost, options.region); + await performCdkOperation(loaded, operation, options); + }); diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index fc9d7e092..93f130459 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -1182,6 +1182,10 @@ type TestCoreClientOptions = { * Called in place of driving the CDK toolkit, so a test can stand in for it: * `emit` yields a toolkit message to the operation's consumer, and throwing * fails the operation. + * + * Emitted messages are collected and then yielded, rather than delivered while + * the operation is still running as the real runner does. That the real one + * delivers them as they arrive is covered directly, in `src/io/cdk.test.ts`. */ onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void; }; From 5e56870977f89f2e7f098f6376900bcc498890ac Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 14:05:29 -0400 Subject: [PATCH 04/18] fix(project): ship the toolkit's bootstrap template with the artifacts Bootstrap uploads a CloudFormation template that @aws-cdk/toolkit-lib ships as a file in its own package directory and finds at runtime relative to where that directory sits on disk. Bundling the package rewrote that lookup to the build machine's absolute path, so a released build reached a node_modules that only exists on the machine that built it: the npm bundle failed with ENOENT on the template, and the compiled executables, which have no node_modules at all, failed the same way. Nothing read that template until someone bootstrapped an account, so every build check passed. The bundle keeps the package external, as the pre-refactor CLI's esbuild config did, so node resolves a real @aws-cdk/toolkit-lib -- it is a declared runtime dependency, so installing the CLI installs it. That also returns the bundle to its previous size (32.8 MB back to 6.4 MB) and warm `agentcore --help` to ~0.5s, since the lazy import no longer has a second CDK toolchain inlined behind it to parse. A compiled executable cannot have externals, so the build embeds the template as an asset and bootstrap is pointed at a copy written out for it, in the one mode where the toolkit cannot find its own. The template is read from the installed package rather than vendored here, so it always matches the toolkit being compiled in, and compile fails if the package stops shipping it or if the executable does not carry its bytes -- the check the missing template needed and did not have. Verified on the artifacts: with the toolkit's own template moved aside, the executable bootstraps to the first AWS call, while the bundle under node fails on exactly the ENOENT above. --- scripts/build.ts | 68 +++++++++++++++++++++++++++++++++++---- src/io/cdk.test.ts | 79 +++++++++++++++++++++++++++++++++++++++++++++- src/io/cdk.ts | 66 ++++++++++++++++++++++++++++++++++---- 3 files changed, 199 insertions(+), 14 deletions(-) diff --git a/scripts/build.ts b/scripts/build.ts index ea2b316ec..ca5ef5608 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,6 +1,7 @@ #!/usr/bin/env bun import { $ } from "bun"; +import { existsSync } from "node:fs"; import { join, resolve } from "node:path"; import { runWithExitCode } from "../src/runnable"; @@ -11,6 +12,16 @@ const DIST = join(REPO_ROOT, "dist"); const ASSET_NAMING = "agentcore-assets/[dir]/[name].[ext]"; +// Kept out of the bundle so the toolkit stays a real directory in node_modules at +// runtime: it reads its bootstrap template from its own package directory, which a +// bundle would rewrite to this machine's absolute path. The npm package declares it +// as a dependency, so installing the CLI installs it. +const EXTERNAL = ["@aws-cdk/toolkit-lib"]; + +// A compiled executable has no node_modules, so the template the toolkit would read +// from its package directory is embedded instead. See loadBootstrapTemplate. +const BOOTSTRAP_TEMPLATE = ["lib", "api", "bootstrap", "bootstrap-template.yaml"]; + // Shrink whitespace/syntax but keep identifiers: minified names make stack // traces unreadable and erase error names telemetry keys on. const MINIFY = { whitespace: true, syntax: true, identifiers: false } as const; @@ -26,14 +37,54 @@ function assetLoaderPlugin(): Bun.BunPlugin { return { name: "asset-file-loader", setup(build) { - build.onLoad({ filter: /src[/\\]assets[/\\]/ }, async ({ path }) => ({ - contents: await Bun.file(path).bytes(), - loader: "file", - })); + build.onLoad( + { filter: /src[/\\]assets[/\\]|bootstrap-template\.yaml$/ }, + async ({ path }) => ({ + contents: await Bun.file(path).bytes(), + loader: "file", + }), + ); }, }; } +/** + * Absolute path of the toolkit's own bootstrap template. + * + * Resolved from the installed package rather than a copy in this repo, so the + * embedded template is always the one the toolkit being compiled in expects. + */ +function bootstrapTemplate(): string { + const manifest = Bun.resolveSync("@aws-cdk/toolkit-lib/package.json", REPO_ROOT); + const template = join(resolve(manifest, ".."), ...BOOTSTRAP_TEMPLATE); + if (!existsSync(template)) { + throw new Error( + `@aws-cdk/toolkit-lib no longer ships ${BOOTSTRAP_TEMPLATE.join("/")}; ` + + `bootstrap in a compiled executable reads the embedded copy, so this must be found. Looked in ${template}`, + ); + } + return template; +} + +/** + * Fail loudly unless the compiled executable carries the bootstrap template's bytes. + * + * Nothing reads that template until someone bootstraps an AWS account, so an + * executable that lost it looks healthy in every build check and fails in a user's + * first deploy instead. + */ +async function assertTemplateIsEmbedded(outfile: string, template: string): Promise { + const [executable, bytes] = await Promise.all([ + Bun.file(outfile).bytes(), + Bun.file(template).bytes(), + ]); + if (!Buffer.from(executable).includes(bytes)) { + throw new Error( + `${outfile} does not carry ${BOOTSTRAP_TEMPLATE.join("/")}, so bootstrap would fail wherever it runs`, + ); + } +} + /** Fail loudly on a non-UTF-8 asset — the source reads every asset as text. */ async function assertAssetsAreText(assets: string[]): Promise { const decoder = new TextDecoder("utf-8", { fatal: true }); @@ -54,6 +105,7 @@ async function bundle(): Promise { outdir: DIST, target: "node", minify: MINIFY, + external: EXTERNAL, }); // Mirror assets beside the emitted module for resolveAssetsRoot(). @@ -70,15 +122,19 @@ async function compile(target: string): Promise { const outfile = join(DIST, "bin", `agentcore-${target.replace(/^bun-/, "")}`); await $`mkdir -p ${join(DIST, "bin")}`; + const template = bootstrapTemplate(); await Bun.build({ - entrypoints: [ENTRYPOINT, ...assets], + entrypoints: [ENTRYPOINT, ...assets, template], compile: { target: target as Bun.Build.CompileTarget, outfile }, minify: MINIFY, root: REPO_ROOT, naming: { asset: ASSET_NAMING }, plugins: [assetLoaderPlugin()], }); - console.log(`Compiled ${target} → ${outfile} (${assets.length} assets embedded)`); + await assertTemplateIsEmbedded(outfile, template); + console.log( + `Compiled ${target} → ${outfile} (${assets.length} assets embedded, plus the bootstrap template)`, + ); } process.exit( diff --git a/src/io/cdk.test.ts b/src/io/cdk.test.ts index d39f1778e..18b5bf77d 100644 --- a/src/io/cdk.test.ts +++ b/src/io/cdk.test.ts @@ -1,6 +1,6 @@ import { test, expect, describe } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { tmpdir } from "node:os"; import type { IIoHost, IoMessage, IoRequest } from "@aws-cdk/toolkit-lib"; // The real toolkit package, so the arguments these tests assert on are the values @@ -8,6 +8,7 @@ import type { IIoHost, IoMessage, IoRequest } from "@aws-cdk/toolkit-lib"; // here, unlike in cdk.ts, because a test file pays no startup cost. import * as toolkit from "@aws-cdk/toolkit-lib"; import { + loadBootstrapTemplate, loadCdkToolkit, performCdkOperation, runCdk, @@ -24,6 +25,15 @@ function message(level: string, text: string): IoMessage { const lib: CdkToolkitLib = toolkit; +// What a compiled executable holds: the toolkit's template, under the name the +// build's asset naming gives it, beside the assets it also embeds. +function embedded(text: string): (Blob & { name: string })[] { + return [ + new File(["unrelated"], "agentcore-assets/src/assets/cdk/cdk.json"), + new File([text], "agentcore-assets/lib/api/bootstrap/bootstrap-template.yaml"), + ]; +} + async function collect(generator: AsyncGenerator): Promise { const events: CdkEvent[] = []; for await (const event of generator) events.push(event); @@ -128,6 +138,10 @@ describe("performCdkOperation", () => { { assemblyDirectory: "/unused", region: "us-east-1" }, ); + // Nothing is embedded when running from source, so the toolkit reads its own + // template rather than one written out for it. + expect(calls[0]!.args[1]).not.toHaveProperty("source"); + expect(calls.map(({ method }) => method)).toEqual(["bootstrap"]); const [environments, options] = calls[0]!.args as [ { getEnvironments: () => Promise<{ name: string; account: string; region: string }[]> }, @@ -141,6 +155,52 @@ describe("performCdkOperation", () => { expect(options.parameters.parameters).toEqual({ createCustomerMasterKey: true }); }); + test("bootstraps a compiled executable from the template it has embedded", async () => { + // The released-binary path: no node_modules, so the toolkit cannot find its own + // template and is handed the embedded copy instead. + let templateFile: string | undefined; + let uploaded: string | undefined; + const toolkit = { + bootstrap: async (_environments: unknown, options: { source?: { templateFile: string } }) => { + templateFile = options.source?.templateFile; + // Read here rather than after: the file lives only for the operation. + uploaded = await Bun.file(templateFile!).text(); + }, + } as unknown as CdkToolkit; + + await performCdkOperation( + { lib, toolkit }, + { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, + { assemblyDirectory: "/unused", region: "us-east-1" }, + embedded("Resources: {}"), + ); + + expect(uploaded).toBe("Resources: {}"); + // Cleaned up, so a long-lived process bootstrapping repeatedly leaves nothing behind. + expect(await Bun.file(templateFile!).exists()).toBe(false); + }); + + test("removes the template it wrote out even when bootstrap fails", async () => { + let templateFile: string | undefined; + const toolkit = { + bootstrap: async (_environments: unknown, options: { source?: { templateFile: string } }) => { + templateFile = options.source?.templateFile; + throw new Error("bootstrap stack rollback complete"); + }, + } as unknown as CdkToolkit; + + await expect( + performCdkOperation( + { lib, toolkit }, + { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, + { assemblyDirectory: "/unused", region: "us-east-1" }, + embedded("Resources: {}"), + ), + ).rejects.toThrow(/rollback complete/); + + expect(await Bun.file(templateFile!).exists()).toBe(false); + }); + test("deploys only the named stack, from the assembly build synthesized", async () => { const { toolkit, calls } = stubToolkit(); @@ -175,6 +235,23 @@ describe("performCdkOperation", () => { }); }); +describe("loadBootstrapTemplate", () => { + test("writes the embedded template where the toolkit can read it", async () => { + const path = await loadBootstrapTemplate(embedded("Resources: {}")); + + // A path, not bytes: the toolkit takes a template file to upload. + expect(path).toBeString(); + expect(await Bun.file(path!).text()).toBe("Resources: {}"); + await rm(dirname(path!), { recursive: true, force: true }); + }); + + test("reports no template when nothing is embedded", async () => { + // Running from source or the bundle, where the toolkit package is a real + // directory on disk and finds its own template. + expect(await loadBootstrapTemplate([])).toBeUndefined(); + }); +}); + describe("loadCdkToolkit", () => { test("builds a toolkit without needing credentials", async () => { // Constructing the toolkit resolves no credentials and calls no API, which is diff --git a/src/io/cdk.ts b/src/io/cdk.ts index 3f3664a64..a863b8580 100644 --- a/src/io/cdk.ts +++ b/src/io/cdk.ts @@ -1,6 +1,9 @@ // Programmatic CDK operations via @aws-cdk/toolkit-lib. Deploy drives the toolkit // in-process rather than shelling out to `npx cdk`, so its progress arrives as // structured messages and its failures as typed errors instead of scraped stdout. +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { tmpdir } from "node:os"; import type { IIoHost, IoMessageLevel, Toolkit } from "@aws-cdk/toolkit-lib"; /** A message emitted by the toolkit while an operation runs. */ @@ -47,15 +50,52 @@ export type CdkToolkit = Pick; +/** An embedded file, whose name Bun's types widen away on Bun.embeddedFiles. */ +type NamedBlob = Blob & { readonly name: string }; + +const BOOTSTRAP_TEMPLATE = "bootstrap-template.yaml"; + +/** The files compiled into this executable, or none when running from source or a bundle. */ +function embeddedFiles(): readonly NamedBlob[] { + return typeof Bun === "undefined" ? [] : (Bun.embeddedFiles as readonly NamedBlob[]); +} + +/** + * Writes the embedded bootstrap template to a file the toolkit can read, or returns + * undefined when it should read its own. + * + * Bootstrap uploads a CloudFormation template that the toolkit ships as a file in its + * package directory, found at runtime relative to where that package sits on disk. A + * compiled executable has no node_modules, and the path it was compiled with belongs + * to the build machine, so the build embeds the template and bootstrap is pointed at + * a copy of it. Everywhere else the package is a real directory and finds its own. + */ +export async function loadBootstrapTemplate( + files: readonly NamedBlob[] = embeddedFiles(), +): Promise { + const template = files.find((file) => file.name.endsWith(BOOTSTRAP_TEMPLATE)); + if (!template) return undefined; + + const directory = await mkdtemp(join(tmpdir(), "agentcore-bootstrap-")); + const path = join(directory, BOOTSTRAP_TEMPLATE); + await writeFile(path, await template.text()); + return path; +} + /** * Loads the toolkit package and builds a toolkit that reports to `ioHost`. * * Loaded here rather than imported at module scope: the toolkit is the heaviest * dependency in the CLI and `src/io` is reachable from every command, so a static * import would make even `agentcore --help` pay to load a deploy it is not doing. + * The bundle keeps the package external, so a command that never deploys never + * reads it; a compiled executable has it inlined and pays only to parse it. */ export async function loadCdkToolkit( ioHost: IIoHost, @@ -72,18 +112,30 @@ export async function loadCdkToolkit( return { lib, toolkit }; } -/** Performs one operation, awaiting the toolkit call it maps to. */ +/** + * Performs one operation, awaiting the toolkit call it maps to. + * + * `files` is what this executable has embedded, taken as an argument so a test can + * exercise the compiled executable's bootstrap path without being one. + */ export async function performCdkOperation( { lib, toolkit }: { lib: CdkToolkitLib; toolkit: CdkToolkit }, operation: CdkOperation, options: CdkRunOptions, + files: readonly NamedBlob[] = embeddedFiles(), ): Promise { if (operation.kind === "bootstrap") { - await toolkit.bootstrap(lib.BootstrapEnvironments.fromList(operation.environments), { - // Provisions a customer-managed KMS key for the staging bucket, matching - // the parameters the original CLI bootstraps with. - parameters: lib.BootstrapStackParameters.withExisting({ createCustomerMasterKey: true }), - }); + const template = await loadBootstrapTemplate(files); + try { + await toolkit.bootstrap(lib.BootstrapEnvironments.fromList(operation.environments), { + // Provisions a customer-managed KMS key for the staging bucket, matching + // the parameters the original CLI bootstraps with. + parameters: lib.BootstrapStackParameters.withExisting({ createCustomerMasterKey: true }), + ...(template && { source: lib.BootstrapSource.customTemplate(template) }), + }); + } finally { + if (template) await rm(dirname(template), { recursive: true, force: true }); + } return; } From 5bcd05f25cfe7b7fdba3ab3e1b8963f3aefecbe4 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 15:20:52 -0400 Subject: [PATCH 05/18] feat(project): log deploy's detailed output to the shared log The CDK toolkit narrates a deploy in hundreds of lines, which buried the steps deploy words itself. Those messages now go only to the shared file logger at the standard location, each keeping the toolkit's own severity, so a failure reads as an error in the log rather than as one debug line among thousands. What a deploy shows is its own steps, and it names where the detail is: Synthesizing CloudFormation templates Bootstrapping aws://111122223333/us-east-1 Deploying AgentCore-MyAgent-default Deployed project 'MyAgent' Detailed logs: ~/.agentcore/logs/output-2026-08-17.log The location is printed however the deploy ended, since a failed one is when the log matters most. It is shortened to `~` rather than made relative to the working directory: the log lives under the user's home, so a relative path from a project is a run of `../` that stops being correct as soon as they cd. src/logging/location.tsx owns the path, so the logger the entrypoint builds and the location deploy prints cannot disagree, and its test pins the predicted file name against the one a real rotating logger writes. With nothing forwarding the toolkit's output, ProjectEvent loses its `output` variant. --- src/core/project/manager.test.ts | 79 ++++++++++++++++++---------- src/core/project/manager.tsx | 35 ++++++++---- src/handlers/project/deploy/index.ts | 28 ++++++---- src/handlers/project/project.test.ts | 42 ++++++++++++--- src/handlers/project/types.ts | 15 +++--- src/index.ts | 5 +- src/logging/index.tsx | 1 + src/logging/location.test.ts | 60 +++++++++++++++++++++ src/logging/location.tsx | 40 ++++++++++++++ src/testing/index.tsx | 8 ++- src/testing/logging.tsx | 36 ++++++++++++- 11 files changed, 282 insertions(+), 67 deletions(-) create mode 100644 src/logging/location.test.ts create mode 100644 src/logging/location.tsx diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index bb6723e4a..0ed58a12f 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -10,7 +10,7 @@ import { type Project, type ProjectEvent, } from "../../handlers/project/types"; -import { createSilentLogger } from "../../testing"; +import { createRecordingLogger, createSilentLogger, type RecordedLog } from "../../testing"; import type { CdkEvent, CdkOperation, CdkRunOptions } from "../../io"; const originalCwd = process.cwd(); @@ -337,12 +337,18 @@ describe("FsProjectManager.deploy", () => { // a test supply the events one of them emits (and the failure it ends with). function deployManager( onCdk?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, - ): { manager: FsProjectManager; commands: RecordedCommand[]; runs: RecordedCdkRun[] } { + ): { + manager: FsProjectManager; + commands: RecordedCommand[]; + runs: RecordedCdkRun[]; + logs: RecordedLog[]; + } { const commands: RecordedCommand[] = []; const runs: RecordedCdkRun[] = []; + const { logger, logs } = createRecordingLogger(); return { manager: new FsProjectManager({ - logger: createSilentLogger(), + logger, runner: async (command, { cwd }) => { commands.push({ command, cwd }); }, @@ -364,6 +370,7 @@ describe("FsProjectManager.deploy", () => { }), commands, runs, + logs, }; } @@ -444,9 +451,12 @@ describe("FsProjectManager.deploy", () => { return events; } - // The toolkit's own messages, as opposed to the steps this CLI words itself. - function forwarded(events: ProjectEvent[]): string[] { - return events.filter((event) => event.kind === "output").map((event) => event.message); + // The toolkit's own messages, as they landed in the log: tagged with the operation + // that reported them, unlike the manager's own lines. + function logged(logs: RecordedLog[]): { level: string; message: string }[] { + return logs + .filter((line) => line.bindings.cdk !== undefined) + .map(({ level, message }) => ({ level, message })); } test("synthesizes, bootstraps the target environment, then deploys its stack", async () => { @@ -631,9 +641,9 @@ describe("FsProjectManager.deploy", () => { ).rejects.toThrow(/is not a valid list of deployment targets/); }); - test("streams the CDK toolkit's messages as they arrive", async () => { + test("logs the toolkit's messages instead of reporting them as events", async () => { const directory = await inTempDirectory(); - const { manager: subject } = deployManager((operation, emit) => { + const { manager: subject, logs } = deployManager((operation, emit) => { if (operation.kind !== "deploy") return; emit({ level: "info", message: "example-stack: creating CloudFormation changeset..." }); emit({ level: "result", message: "example-stack: deployed" }); @@ -644,51 +654,64 @@ describe("FsProjectManager.deploy", () => { subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), ); - expect(forwarded(events)).toEqual([ - "example-stack: creating CloudFormation changeset...", - "example-stack: deployed", + // A deploy reports its own steps and nothing the toolkit said, so what reaches + // the screen stays the same length however talkative a deploy turns out to be. + expect(events).toEqual([ + { kind: "step", message: "Synthesizing CloudFormation templates" }, + { kind: "step", message: `Deploying ${stackName("default")}` }, + ]); + // `result` reports an outcome rather than a severity, so it lands as info. + expect(logged(logs)).toEqual([ + { level: "info", message: "example-stack: creating CloudFormation changeset..." }, + { level: "info", message: "example-stack: deployed" }, ]); }); - test("keeps the toolkit's debug and trace messages off screen", async () => { + test("logs each toolkit message at the severity the toolkit gave it", async () => { const directory = await inTempDirectory(); - const { manager: subject } = deployManager((operation, emit) => { + const { manager: subject, logs } = deployManager((operation, emit) => { if (operation.kind !== "deploy") return; emit({ level: "debug", message: "resolved 3 environments" }); emit({ level: "trace", message: "sdk call: DescribeStacks" }); emit({ level: "warn", message: "example-stack: no changes" }); + emit({ level: "error", message: "example-stack: UPDATE_ROLLBACK_COMPLETE" }); }); const project = await scaffolded(subject, directory); - const events = await drain( + await drain( subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), ); - // The suppressed ones are still in the debug log; only the warning is surfaced. - expect(forwarded(events)).toEqual(["example-stack: no changes"]); + // Severity is what makes the log readable once a deploy has written thousands of + // lines to it: a failure is greppable rather than buried among the trace, which + // is finer than any level this CLI logs at and so joins debug. + expect(logged(logs)).toEqual([ + { level: "debug", message: "resolved 3 environments" }, + { level: "debug", message: "sdk call: DescribeStacks" }, + { level: "warn", message: "example-stack: no changes" }, + { level: "error", message: "example-stack: UPDATE_ROLLBACK_COMPLETE" }, + ]); }); - test("yields the output that explains a failure before propagating it", async () => { + test("logs the output that explains a failure before propagating it", async () => { const directory = await inTempDirectory(); - const { manager: subject } = deployManager((operation, emit) => { + const { manager: subject, logs } = deployManager((operation, emit) => { if (operation.kind !== "deploy") return; emit({ level: "error", message: "example-stack: CREATE_FAILED" }); throw new Error("cdk deploy exploded"); }); const project = await scaffolded(subject, directory); - const events: ProjectEvent[] = []; - const generator = subject.deploy(project, { - region: REGION, - skipBootstrap: true, - target: "default", - }); await expect( - (async () => { - for await (const event of generator) events.push(event); - })(), + drain(subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" })), ).rejects.toThrow("cdk deploy exploded"); - expect(events).toContainEqual({ kind: "output", message: "example-stack: CREATE_FAILED" }); + + // The failure reaches the caller, but only the log says what went wrong, which is + // why deploy prints where the log is. + expect(logged(logs)).toContainEqual({ + level: "error", + message: "example-stack: CREATE_FAILED", + }); }); test("refuses a project managed by a backend it cannot deploy", async () => { diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 5c3fd8629..8b97bc37b 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -16,6 +16,7 @@ import { requireTool, runCdk, runProcess, + type CdkEvent, type CdkOperation, type CdkRunner, type CdkRunOptions, @@ -38,6 +39,18 @@ import { // Shown when aws-targets.json names no usable target, so the fix is on screen. const TARGETS_EXAMPLE = `[{ "name": "default", "account": "111122223333", "region": "us-east-1" }]`; +// How the CDK toolkit's message levels land in the log. Its `result` reports an +// operation's outcome rather than a severity, and nothing this CLI logs is finer +// than debug, so `trace` joins it there. +const CDK_LOG_LEVELS: Record = { + error: "error", + warn: "warn", + info: "info", + result: "info", + debug: "debug", + trace: "debug", +}; + type ProjectManagerConfig = { logger: Logger; source?: AssetSource; // Bun executable or dist/assets depending on runtime @@ -280,23 +293,23 @@ export class FsProjectManager implements ProjectManager { // first. const environment = `aws://${target.account}/${target.region}`; yield { kind: "step", message: `Bootstrapping ${environment}` }; - yield* this.streamCdk({ kind: "bootstrap", environments: [environment] }, run); + await this.runCdkOperation({ kind: "bootstrap", environments: [environment] }, run); } yield { kind: "step", message: `Deploying ${stackName}` }; - yield* this.streamCdk({ kind: "deploy", stackName }, run); + await this.runCdkOperation({ kind: "deploy", stackName }, run); } - // Drives a CDK operation, surfacing the toolkit's messages as project events and - // logging them. Anything below info stays in the debug log rather than on screen. - private async *streamCdk( - operation: CdkOperation, - options: CdkRunOptions, - ): AsyncGenerator { + // Drives a CDK operation, recording everything it reports in the log file. + // + // The toolkit narrates a deploy in hundreds of lines, so none of it reaches the + // screen: the steps above are what a deploy shows, and the command points at the + // log for the detail. Each message keeps the toolkit's own severity, so a failure + // reads as an error in the log rather than as one debug line among thousands. + private async runCdkOperation(operation: CdkOperation, options: CdkRunOptions): Promise { + const logger = this.logger.child({ cdk: operation.kind }); for await (const event of this.cdk(operation, options)) { - this.logger.debug(event.message); - if (event.level === "debug" || event.level === "trace") continue; - yield { kind: "output", message: event.message }; + logger[CDK_LOG_LEVELS[event.level]](event.message); } } diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index f91df4a1b..0cb6c4239 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -1,6 +1,7 @@ import z from "zod"; import { createHandler, flag, ProjectKey } from "../../../router"; import type { AppIO } from "../../../io"; +import { detailedLogLocation } from "../../../logging"; import { RegionKey } from "../../keys"; import type { ProjectManager } from "../types"; @@ -29,16 +30,23 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = // withProject has already resolved the enclosing project. const project = ctx.require(ProjectKey); - // Progress and the CDK toolkit's own output both go to stderr, keeping stdout - // for machine output. - for await (const event of config.projectManager.deploy(project, { - region: ctx.require(RegionKey), - skipBootstrap: flags["skip-bootstrap"], - target: flags.target, - })) { - config.io.stderr.write(`${event.message}\n`); - } + // Progress goes to stderr, keeping stdout for machine output. The CDK toolkit's + // own narration goes to the log instead, so a deploy shows its steps and + // nothing else. + try { + for await (const event of config.projectManager.deploy(project, { + region: ctx.require(RegionKey), + skipBootstrap: flags["skip-bootstrap"], + target: flags.target, + })) { + config.io.stderr.write(`${event.message}\n`); + } - config.io.stderr.write(`Deployed project '${project.name}'\n`); + config.io.stderr.write(`Deployed project '${project.name}'\n`); + } finally { + // Printed however the deploy ended: a failed one is when the detail the log + // holds matters most, and the error itself surfaces after this line. + config.io.stderr.write(`Detailed logs: ${detailedLogLocation()}\n`); + } }, }); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 99ef25b7d..31b9b2794 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -11,6 +11,7 @@ import { } from "../../testing"; import { InputValidationError } from "../../errors"; import type { CdkEvent, CdkOperation } from "../../io"; +import { detailedLogLocation } from "../../logging"; // The synth command build and deploy both issue. --output pins the assembly to the // directory deploy reads, so cdk.json's own `output` cannot send the two apart. @@ -23,10 +24,9 @@ function STACK(target: string): string { return `AgentCore-MyAgent-${target}`; } -async function run( - args: string[], - onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, -) { +// A CLI wired to test doubles, whose route() a test drives directly when it needs +// what was written after a command failed. +function cli(onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void) { const io = testIO(); const core = new TestCoreClient({ onCdkOperation }); const root = createRootHandler(core, { @@ -34,7 +34,19 @@ async function run( globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + return { + io, + core, + route: (args: string[]) => root.route(["node", "agentcore", "project", ...args]), + }; +} + +async function run( + args: string[], + onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, +) { + const { io, core, route } = cli(onCdkOperation); + await route(args); return { io, core }; } @@ -501,7 +513,7 @@ describe("project deploy", () => { expect(io.stderr()).toContain("Deployed project 'MyAgent'"); }); - test("writes the CDK toolkit's own messages to stderr", async () => { + test("keeps the CDK toolkit's own messages off screen, naming the log instead", async () => { await inProject(); const { io } = await run(["deploy", "--skip-bootstrap"], (operation, emit) => { if (operation.kind === "deploy") { @@ -509,7 +521,23 @@ describe("project deploy", () => { } }); - expect(io.stderr()).toContain("example-stack | 1/2 | CREATE_IN_PROGRESS\n"); + // A deploy shows its steps; the toolkit's narration is in the log, so the + // location is the last thing printed. + expect(io.stderr()).not.toContain("CREATE_IN_PROGRESS"); + expect(io.stderr()).toEndWith(`Detailed logs: ${detailedLogLocation()}`); + }); + + test("names the log even when the deploy fails", async () => { + await inProject(); + const { io, route } = cli((operation) => { + if (operation.kind === "deploy") throw new Error("cdk deploy exploded"); + }); + + await expect(route(["deploy", "--skip-bootstrap"])).rejects.toThrow("cdk deploy exploded"); + + // The failure is when the log matters most: it holds the toolkit's account of + // what went wrong, which the error itself does not carry. + expect(io.stderr()).toContain(`Detailed logs: ${detailedLogLocation()}`); }); test("fails with actionable guidance when no targets are configured", async () => { diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index ea76e1e80..c56f1e3ac 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -35,15 +35,16 @@ export type CreateProjectInput = { /** * Something worth showing the user while a long-running project operation runs. * - * A union rather than two optional fields, so an event is always exactly one of the - * two and a consumer that only writes text can read `message` without checking - * which it got. + * Discriminated on `kind`, of which there is one today: the tools an operation + * drives report to the log rather than to the screen, so what a command shows is + * its own steps. A later variant can carry fields a step has no use for without + * changing what a consumer of `step` reads. */ -export type ProjectEvent = +export type ProjectEvent = { /** A progress step, in this CLI's own wording. */ - | { kind: "step"; message: string } - /** A line of output, forwarded as the tool that produced it phrased it. */ - | { kind: "output"; message: string }; + kind: "step"; + message: string; +}; export type DeployProjectOptions = { /** The resolved AWS region, forwarded to the CDK subprocesses. */ diff --git a/src/index.ts b/src/index.ts index aa2834b1b..2db95ffbe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { } from "./core/factories"; import { createRootHandler } from "./handlers"; import { FsReadWriteJson } from "./io"; -import { createFileLogger, LOG_LEVEL } from "./logging"; +import { createFileLogger, logFilePrefix, LOG_LEVEL } from "./logging"; import { runWithExitCode } from "./runnable"; import { DefaultGlobalConfigAccessor } from "./globalConfig"; import { DefaultTelemetryClient } from "./telemetry"; @@ -31,7 +31,8 @@ process.exit( const cliSessionId = crypto.randomUUID(); const rootLogger = createFileLogger({ - filePath: join(homedir(), ".agentcore", "logs", "output"), + // The standard location, shared with the commands that print where it is. + filePath: logFilePrefix(), logLevel: LOG_LEVEL.DEBUG, bindings: { cliSessionId, version: PACKAGE_VERSION }, }); diff --git a/src/logging/index.tsx b/src/logging/index.tsx index e9321f4fa..18b2f1bb9 100644 --- a/src/logging/index.tsx +++ b/src/logging/index.tsx @@ -1,2 +1,3 @@ export { type Logger, type LoggerBindings, LOG_LEVEL } from "./types"; export { createFileLogger } from "./fileLogger"; +export { detailedLogLocation, logFilePrefix } from "./location"; diff --git a/src/logging/location.test.ts b/src/logging/location.test.ts new file mode 100644 index 000000000..645cbf40f --- /dev/null +++ b/src/logging/location.test.ts @@ -0,0 +1,60 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, readdir, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { createFileLogger } from "./fileLogger"; +import { LOG_LEVEL } from "./types"; +import { detailedLogLocation, logFilePrefix, rotatedLogFile } from "./location"; + +const tempDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +describe("rotatedLogFile", () => { + test("names the file the rotating logger writes", async () => { + // The point of the function: it predicts a name winston chooses, so it is + // checked against the file a real logger leaves behind rather than against the + // pattern it was written from. + const directory = await mkdtemp(join(tmpdir(), "agentcore-logs-")); + tempDirectories.push(directory); + const prefix = join(directory, "output"); + + const logger = createFileLogger({ filePath: prefix, logLevel: LOG_LEVEL.DEBUG }); + logger.info("example"); + await logger.end(); + + // The transport also writes a hidden audit file beside the log. + const written = (await readdir(directory)).filter((name) => name.endsWith(".log")); + expect(written).toEqual([rotatedLogFile("output", new Date())]); + }); + + test("pads a single-digit month and day, as the date pattern does", () => { + expect(rotatedLogFile("output", new Date(2026, 0, 5))).toBe("output-2026-01-05.log"); + }); +}); + +describe("detailedLogLocation", () => { + test("points at the file the logger writes, with the home directory shortened", () => { + const home = join(tmpdir(), "example-home"); + const at = new Date(2026, 7, 17, 13, 45); + + expect(detailedLogLocation(at, home)).toBe( + join("~", ".agentcore", "logs", "output-2026-08-17.log"), + ); + // The same file spelled in full, which is what the `~` stands in for. + expect(rotatedLogFile(logFilePrefix(home), at)).toBe( + join(home, ".agentcore", "logs", "output-2026-08-17.log"), + ); + }); + + test("stays absolute for a log that does not sit under the home directory", () => { + // Nothing shortens to a bare `~`, so a home of `/` is spelled out instead. + expect(detailedLogLocation(new Date(2026, 7, 17), "/")).toBe( + join("/", ".agentcore", "logs", "output-2026-08-17.log"), + ); + }); +}); diff --git a/src/logging/location.tsx b/src/logging/location.tsx new file mode 100644 index 000000000..bb456a791 --- /dev/null +++ b/src/logging/location.tsx @@ -0,0 +1,40 @@ +import { homedir } from "node:os"; +import { join, sep } from "node:path"; + +// The CLI keeps its log beside the rest of its state, under the user's home. +const LOG_DIRECTORY = [".agentcore", "logs"]; + +/** + * The file path prefix the CLI's log rotates on. + * + * Everything that needs the log's location reads it from here — the entrypoint that + * creates the logger, and the commands that tell the user where to look — so the + * location printed is always the one being written to. + */ +export function logFilePrefix(home: string = homedir()): string { + return join(home, ...LOG_DIRECTORY, "output"); +} + +/** + * The file a logger rotating on `prefix` writes `at`'s messages to. + * + * Mirrors what {@link createFileLogger} configures its transport with: + * `${prefix}-%DATE%` plus a `.log` extension, with `YYYY-MM-DD` in local time. + */ +export function rotatedLogFile(prefix: string, at: Date): string { + const month = String(at.getMonth() + 1).padStart(2, "0"); + const day = String(at.getDate()).padStart(2, "0"); + return `${prefix}-${at.getFullYear()}-${month}-${day}.log`; +} + +/** + * Where to tell the user their detailed logs are, written as they would type it. + * + * Shortened to `~` rather than made relative to the working directory: the log lives + * under the user's home, so a path relative to a project directory is a run of `../` + * that stops being correct as soon as they cd. + */ +export function detailedLogLocation(at: Date = new Date(), home: string = homedir()): string { + const path = rotatedLogFile(logFilePrefix(home), at); + return path.startsWith(`${home}${sep}`) ? `~${path.slice(home.length)}` : path; +} diff --git a/src/testing/index.tsx b/src/testing/index.tsx index fc313b4f5..8ec113795 100644 --- a/src/testing/index.tsx +++ b/src/testing/index.tsx @@ -20,5 +20,11 @@ export { type RenderScreenOptions, type RenderScreenResult, } from "./renderScreen"; -export { createSilentLogger, assertLogsMatch, type LogQuery } from "./logging"; +export { + createSilentLogger, + createRecordingLogger, + assertLogsMatch, + type LogQuery, + type RecordedLog, +} from "./logging"; export { TestGlobalConfigAccessor } from "./globalConfig"; diff --git a/src/testing/logging.tsx b/src/testing/logging.tsx index cc3321fa9..7c71ded1f 100644 --- a/src/testing/logging.tsx +++ b/src/testing/logging.tsx @@ -1,4 +1,4 @@ -import { type Logger } from "../logging"; +import { type Logger, type LoggerBindings } from "../logging"; import { readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; import { waitFor } from "./timing"; @@ -38,6 +38,40 @@ export function createSilentLogger(): Logger { return silent; } +/** A line as a recording logger captured it. */ +export interface RecordedLog { + level: "debug" | "info" | "warn" | "error"; + message: string; + /** The bindings in effect on the child it was logged through. */ + bindings: LoggerBindings; +} + +/** + * Create a logger that records what was logged instead of writing it anywhere, so a + * test can assert on the level and bindings a line carries rather than only its text. + */ +export function createRecordingLogger(): { logger: Logger; logs: RecordedLog[] } { + const logs: RecordedLog[] = []; + + const at = (bindings: LoggerBindings): Logger => { + // Joined with a space, as the file logger joins the arguments it is given. + const record = + (level: RecordedLog["level"]) => + (...messages: string[]) => + logs.push({ level, message: messages.join(" "), bindings }); + + return { + debug: record("debug"), + info: record("info"), + warn: record("warn"), + error: record("error"), + child: (childBindings) => at({ ...bindings, ...childBindings }), + }; + }; + + return { logger: at({}), logs }; +} + /** A predicate over parsed log lines with an optional exact match count. */ export interface LogQuery { filter: (log: object) => boolean; From 7e328ea66df2355987390a6a294ff45d26b82da6 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 17:07:49 -0400 Subject: [PATCH 06/18] docs(project): shorten the comments deploy's plumbing carries Review found the comments in the assembly reader and the CDK adapter longer than what they explain. Each keeps the fact a reader cannot get from the code -- why stack selection goes through the target tag, why a failure surfaces only after the output explaining it -- and drops the restatement of the code around it. --- src/core/project/assembly.ts | 22 +++++--------- src/io/cdk.ts | 57 +++++++++++------------------------- 2 files changed, 24 insertions(+), 55 deletions(-) diff --git a/src/core/project/assembly.ts b/src/core/project/assembly.ts index d8d9815c3..10d58bee1 100644 --- a/src/core/project/assembly.ts +++ b/src/core/project/assembly.ts @@ -1,21 +1,16 @@ -// Reads the cloud assembly `build` synthesized, so deploy can find the stack that -// belongs to a deployment target. import { existsSync } from "node:fs"; import { join } from "node:path"; import z from "zod"; import { ProjectStateError } from "../../errors/errors"; import type { ReadWriteJson } from "../../io"; -// The tag the generated CDK app puts on every stack, naming the deployment target -// the stack was synthesized for. Selecting on it means the CLI never has to -// reproduce the app's stack-naming convention, so a project that renames its -// stacks still deploys. +// The generated app tags every stack with the target it was synthesized for. Selecting +// on the tag keeps the CLI from reproducing the app's stack-naming convention. const TARGET_TAG = "agentcore:target-name"; const STACK_ARTIFACT = "aws:cloudformation:stack"; -// Only the parts of the manifest deploy reads. Artifacts are keyed by their -// hierarchical id, which is what the CDK toolkit matches stack patterns against. +// Artifacts are keyed by the hierarchical id the toolkit matches stack patterns against. const AssemblyManifestSchema = z.object({ artifacts: z .record( @@ -29,11 +24,8 @@ const AssemblyManifestSchema = z.object({ }); /** - * The name of the stack in the synthesized assembly that belongs to `target`. - * - * The generated CDK app synthesizes one stack per deployment target and tags each - * with the target's name, so deploy asks the assembly which stack to ship rather - * than deriving the name itself and hoping the two agree. + * Asks the synthesized manifest which stack belongs to `target`, rather than deriving + * the name and hoping it matches what synth chose. */ export async function stackForTarget( json: ReadWriteJson, @@ -41,8 +33,8 @@ export async function stackForTarget( target: string, ): Promise { const path = join(assemblyDirectory, "manifest.json"); - // deploy synthesizes immediately before this, so a missing manifest means synth - // wrote somewhere else entirely rather than that the user skipped a step. + // deploy synthesizes immediately before this, so a missing manifest means synth wrote + // somewhere else rather than that the user skipped a step. if (!existsSync(path)) { throw new ProjectStateError(`No synthesized cloud assembly was found at ${path}.`); } diff --git a/src/io/cdk.ts b/src/io/cdk.ts index a863b8580..64b9da3a0 100644 --- a/src/io/cdk.ts +++ b/src/io/cdk.ts @@ -1,6 +1,5 @@ -// Programmatic CDK operations via @aws-cdk/toolkit-lib. Deploy drives the toolkit -// in-process rather than shelling out to `npx cdk`, so its progress arrives as -// structured messages and its failures as typed errors instead of scraped stdout. +// CDK operations driven in-process via @aws-cdk/toolkit-lib rather than by shelling out +// to `npx cdk`, so progress arrives as structured messages instead of scraped stdout. import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { tmpdir } from "node:os"; @@ -19,32 +18,19 @@ export type CdkOperation = | { kind: "deploy"; stackName: string }; export type CdkRunOptions = { - /** - * Directory holding the synthesized cloud assembly. Deploy reads the assembly - * `build` produced rather than re-synthesizing, so what reaches AWS is exactly - * what was synthesized. Unused by bootstrap, which needs no assembly. - */ + /** The synthesized cloud assembly to deploy from. Unused by bootstrap. */ assemblyDirectory: string; - /** - * Region for the toolkit's own SDK calls. Each stack's account and region come - * from the assembly, which the CDK app derives from aws-targets.json. - */ + /** Region for the toolkit's own SDK calls; each stack's environment comes from the assembly. */ region: string; }; -/** - * Runs a CDK operation, yielding the toolkit's messages as they arrive. - * Injectable so tests exercise deploy without reaching AWS. - */ +/** Yields the toolkit's messages as they arrive. Injectable so tests never reach AWS. */ export type CdkRunner = ( operation: CdkOperation, options: CdkRunOptions, ) => AsyncGenerator; -/** - * The toolkit methods an operation drives, narrowed to those actually called so a - * test can stand in for the toolkit without reaching AWS. - */ +/** Narrowed to the methods actually called, so a test can stand in for the toolkit. */ export type CdkToolkit = Pick; /** The parts of the toolkit package an operation needs, loaded on demand. */ @@ -70,11 +56,9 @@ function embeddedFiles(): readonly NamedBlob[] { * Writes the embedded bootstrap template to a file the toolkit can read, or returns * undefined when it should read its own. * - * Bootstrap uploads a CloudFormation template that the toolkit ships as a file in its - * package directory, found at runtime relative to where that package sits on disk. A - * compiled executable has no node_modules, and the path it was compiled with belongs - * to the build machine, so the build embeds the template and bootstrap is pointed at - * a copy of it. Everywhere else the package is a real directory and finds its own. + * The toolkit finds its bootstrap template relative to its own package directory. A + * compiled executable has no node_modules, so the build embeds the template and points + * bootstrap at a copy; everywhere else the package finds its own. */ export async function loadBootstrapTemplate( files: readonly NamedBlob[] = embeddedFiles(), @@ -91,11 +75,9 @@ export async function loadBootstrapTemplate( /** * Loads the toolkit package and builds a toolkit that reports to `ioHost`. * - * Loaded here rather than imported at module scope: the toolkit is the heaviest - * dependency in the CLI and `src/io` is reachable from every command, so a static - * import would make even `agentcore --help` pay to load a deploy it is not doing. - * The bundle keeps the package external, so a command that never deploys never - * reads it; a compiled executable has it inlined and pays only to parse it. + * Imported here rather than at module scope: it is the CLI's heaviest dependency and + * `src/io` is reachable from every command, so a static import would make even + * `agentcore --help` pay to load a deploy it is not doing. */ export async function loadCdkToolkit( ioHost: IIoHost, @@ -113,10 +95,8 @@ export async function loadCdkToolkit( } /** - * Performs one operation, awaiting the toolkit call it maps to. - * - * `files` is what this executable has embedded, taken as an argument so a test can - * exercise the compiled executable's bootstrap path without being one. + * Performs one operation, awaiting the toolkit call it maps to. `files` is taken as an + * argument so a test can exercise the compiled executable's bootstrap path. */ export async function performCdkOperation( { lib, toolkit }: { lib: CdkToolkitLib; toolkit: CdkToolkit }, @@ -152,12 +132,9 @@ export async function performCdkOperation( } /** - * Bridges the toolkit's push-based reporting to a pull-based generator. - * - * `drive` receives the `IIoHost` to report to and runs to completion while the - * caller pulls: messages land in a queue this drains, so they are yielded as they - * arrive rather than after the operation ends. A failure propagates only once the - * queue is empty, so the error arrives after the output that explains it. + * Bridges the toolkit's push-based reporting to a pull-based generator: `drive` runs to + * completion while the caller drains the queue its messages land in. A failure + * propagates only once that queue is empty, so the output explaining it comes first. */ export async function* streamCdkOperation( drive: (ioHost: IIoHost) => Promise, From e822485c213241cdb9fabf703c7c2560f331f92d Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 17:08:01 -0400 Subject: [PATCH 07/18] refactor(project): move CDK behind a project backend Terraform and no-IaC projects are coming, so the manager can no longer be the place that knows how a project is built. src/core/project/backends holds the seam: ProjectBackend is build and deploy as performed by the tool that owns a project's artifacts, CdkBackend is today's only implementation, and agentcore.json's `managedBy` selects one. Nothing above the seam names a tool -- FsProjectManager reads aws-targets.json, resolves the requested target, and hands it to the backend -- so a second backend is a new file and an entry in the dispatch table rather than a change to the manager or to any handler. A project declaring a backend this build has none for is reported as a configuration error naming the value, since `managedBy` is data a user can edit. The CDK tests move with the code. backends/cdk.test.ts covers synth, bootstrap and deploy ordering, the invariant that synth writes the assembly the toolkit is handed, and how the toolkit's messages reach the log; its project fixture is the directory tree the backend reads rather than a scaffolded project, so nothing chdirs. manager.test.ts keeps what the manager itself does -- target selection, and the errors an unusable aws-targets.json produces -- against a backend that records what it is handed instead of building anything. --skip-bootstrap goes with it. Bootstrap is idempotent and takes seconds on an environment that is already current, so a deploy runs it every time rather than asking the user to decide; the flag existed only in this branch, with nothing depending on it. --- src/core/project/backends/cdk.test.ts | 372 ++++++++++++++++ src/core/project/backends/cdk.tsx | 136 ++++++ src/core/project/backends/types.ts | 19 + src/core/project/index.tsx | 2 + src/core/project/manager.test.ts | 598 ++++++-------------------- src/core/project/manager.tsx | 186 ++------ src/handlers/project/deploy/index.ts | 15 +- src/handlers/project/project.test.ts | 17 +- src/handlers/project/types.ts | 23 +- src/testing/TestCoreClient.tsx | 50 ++- 10 files changed, 737 insertions(+), 681 deletions(-) create mode 100644 src/core/project/backends/cdk.test.ts create mode 100644 src/core/project/backends/cdk.tsx create mode 100644 src/core/project/backends/types.ts diff --git a/src/core/project/backends/cdk.test.ts b/src/core/project/backends/cdk.test.ts new file mode 100644 index 000000000..fe7de0f22 --- /dev/null +++ b/src/core/project/backends/cdk.test.ts @@ -0,0 +1,372 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { CdkBackend } from "./cdk"; +import type { Project, ProjectEvent } from "../../../handlers/project/types"; +import { createRecordingLogger, type RecordedLog } from "../../../testing"; +import type { CdkEvent, CdkOperation, CdkRunOptions } from "../../../io"; + +const tempDirectories: string[] = []; + +async function inTempDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), "agentcore-cdk-backend-")); + tempDirectories.push(directory); + return directory; +} + +afterEach(async () => { + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +// Spelled as an escape so no control character lands in this file. +const ESC = "\u001b"; + +// The region the toolkit makes its own calls in, which is not a target's region. +const REGION = "us-west-2"; +const TARGET = { name: "default", account: "111122223333", region: "us-east-1" }; + +function cdkDirectory(root: string): string { + return join(root, "agentcore", "cdk"); +} + +// The assembly directory every operation reads, which build synthesized into. +function assemblyDirectory(root: string): string { + return join(cdkDirectory(root), "cdk.out"); +} + +// Synth is pinned to the directory deploy reads, so both name it the same way. +function synthCommand(root: string): string[] { + return ["npm", "run", "cdk", "--", "synth", "--quiet", "--output", assemblyDirectory(root)]; +} + +// How the generated CDK app names the stack it synthesizes for a target. +function stackName(target: string): string { + return `AgentCore-example-${target}`; +} + +// Only the parts of a scaffolded project the backend reads: the CDK app's directory, +// and the node_modules it refuses to build without. +async function project(root: string, withDependencies = true): Promise { + const directory = cdkDirectory(root); + await mkdir(withDependencies ? join(directory, "node_modules") : directory, { recursive: true }); + return { name: "example", rootPath: root, managedBy: "CDK", runtimes: [] }; +} + +// Stands in for what synth leaves behind: a manifest with one stack per target, each +// tagged with the target it belongs to. deploy reads it to find the stack to ship, and +// the stubbed runner never writes one. +async function synthesized(root: string, targetNames: string[]): Promise { + const assembly = assemblyDirectory(root); + await mkdir(assembly, { recursive: true }); + await writeFile( + join(assembly, "manifest.json"), + JSON.stringify({ + version: "36.0.0", + artifacts: { + // A non-stack artifact, as a real assembly has: only stacks are candidates. + Tree: { type: "cdk:tree" }, + ...Object.fromEntries( + targetNames.map((target) => [ + stackName(target), + { + type: "aws:cloudformation:stack", + properties: { tags: { "agentcore:target-name": target } }, + }, + ]), + ), + }, + }), + ); +} + +type RecordedCommand = { command: string[]; cwd: string }; +type RecordedCdkRun = { operation: CdkOperation; options: CdkRunOptions }; + +// A backend whose runner records commands instead of spawning them and whose toolkit +// records operations instead of reaching AWS. `onCdk` supplies the events one operation +// emits, and the failure it ends with. +function backend( + onCdk?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, + runner?: () => Promise, +): { + backend: CdkBackend; + commands: RecordedCommand[]; + runs: RecordedCdkRun[]; + logs: RecordedLog[]; +} { + const commands: RecordedCommand[] = []; + const runs: RecordedCdkRun[] = []; + const { logger, logs } = createRecordingLogger(); + return { + backend: new CdkBackend({ + logger, + runner: async (command, { cwd }) => { + commands.push({ command, cwd }); + await runner?.(); + }, + cdk: async function* (operation, options) { + runs.push({ operation, options }); + const events: CdkEvent[] = []; + let failure: unknown; + try { + onCdk?.(operation, (event) => events.push(event)); + } catch (error) { + failure = error; + } + // Emit before throwing, as the real runner does: the output explaining a + // failure is only useful if the consumer sees it. + yield* events; + if (failure) throw failure; + }, + checkTool: async () => {}, + }), + commands, + runs, + logs, + }; +} + +async function drain(generator: AsyncGenerator): Promise { + const events: ProjectEvent[] = []; + for await (const event of generator) events.push(event); + return events; +} + +// The toolkit's own messages, as they landed in the log: tagged with the operation that +// reported them, unlike the backend's own lines. +function logged(logs: RecordedLog[]): { level: string; message: string }[] { + return logs + .filter((line) => line.bindings.cdk !== undefined) + .map(({ level, message }) => ({ level, message })); +} + +describe("CdkBackend.build", () => { + test("compiles and synthesizes via the generated cdk script", async () => { + const root = await inTempDirectory(); + const { backend: subject, commands } = backend(); + + const events = await drain(subject.build(await project(root))); + + expect(commands).toEqual([ + { + // --output pins the assembly where deploy looks for it, so a project that + // sets cdk.json's `output` cannot send synth somewhere deploy never reads. + command: synthCommand(root), + cwd: cdkDirectory(root), + }, + ]); + expect(events).toEqual([{ kind: "step", message: "Synthesizing CloudFormation templates" }]); + }); + + test("fails actionably when the CDK dependencies are missing", async () => { + const root = await inTempDirectory(); + const { backend: subject, commands } = backend(); + + await expect(drain(subject.build(await project(root, false)))).rejects.toThrow(/npm install/); + expect(commands).toEqual([]); + }); + + test("propagates a synthesis failure", async () => { + const root = await inTempDirectory(); + const { backend: subject } = backend(undefined, () => { + throw new Error("cdk synth exploded"); + }); + + await expect(drain(subject.build(await project(root)))).rejects.toThrow("cdk synth exploded"); + }); +}); + +describe("CdkBackend.deploy", () => { + test("synthesizes, bootstraps the target environment, then deploys its stack", async () => { + const root = await inTempDirectory(); + const { backend: subject, commands, runs } = backend(); + const built = await project(root); + await synthesized(root, [TARGET.name]); + const options = { assemblyDirectory: assemblyDirectory(root), region: REGION }; + + const events = await drain(subject.deploy(built, { target: TARGET, region: REGION })); + + // Only synthesis shells out; everything that reaches AWS goes through the toolkit. + expect(commands).toEqual([{ command: synthCommand(root), cwd: cdkDirectory(root) }]); + expect(runs).toEqual([ + { operation: { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, options }, + { operation: { kind: "deploy", stackName: stackName("default") }, options }, + ]); + expect(events).toEqual([ + { kind: "step", message: "Synthesizing CloudFormation templates" }, + { kind: "step", message: "Bootstrapping aws://111122223333/us-east-1" }, + { kind: "step", message: `Deploying ${stackName("default")}` }, + ]); + }); + + test("bootstraps only the target's environment, and ships only its stack", async () => { + const root = await inTempDirectory(); + const { backend: subject, runs } = backend(); + const built = await project(root); + await synthesized(root, ["staging", "prod", "default"]); + + await drain( + subject.deploy(built, { + target: { name: "prod", account: "444455556666", region: "eu-west-1" }, + region: REGION, + }), + ); + + // A project with a staging and a prod target cannot reach the others by accident: + // one deploy bootstraps one environment and ships one stack. + expect(runs.map(({ operation }) => operation)).toEqual([ + { kind: "bootstrap", environments: ["aws://444455556666/eu-west-1"] }, + { kind: "deploy", stackName: stackName("prod") }, + ]); + }); + + test("synthesizes into the same directory the toolkit deploys from", async () => { + const root = await inTempDirectory(); + const { backend: subject, commands, runs } = backend(); + const built = await project(root); + await synthesized(root, [TARGET.name]); + + await drain(subject.deploy(built, { target: TARGET, region: REGION })); + + // The invariant behind passing --output at all: whatever synth was told to write + // is exactly what the toolkit is handed. Left to cdk.json's `output`, synth could + // write elsewhere and deploy would ship a stale assembly while reporting success. + const assembly = assemblyDirectory(root); + expect(commands[0]?.command.at(-1)).toBe(assembly); + expect(new Set(runs.map(({ options }) => options.assemblyDirectory))).toEqual( + new Set([assembly]), + ); + }); + + test("fails when the synthesized assembly has no stack for the target", async () => { + const root = await inTempDirectory(); + const { backend: subject, runs } = backend(); + const built = await project(root); + // An assembly synthesized from a different target list than the one on disk — what + // a hand-edited CDK app that stops tagging its stacks would leave behind. + await synthesized(root, ["other"]); + + await expect(drain(subject.deploy(built, { target: TARGET, region: REGION }))).rejects.toThrow( + /no stack for deployment target 'default'/, + ); + // Resolved before bootstrapping, so nothing reached AWS. + expect(runs).toEqual([]); + }); + + test("names the path it looked in when synthesis wrote no assembly", async () => { + const root = await inTempDirectory(); + const { backend: subject, runs } = backend(); + const built = await project(root); + // Synthesis is stubbed in these tests, so writing no manifest at all is what a real + // synth writing somewhere else entirely would leave behind. + + await expect(drain(subject.deploy(built, { target: TARGET, region: REGION }))).rejects.toThrow( + /No synthesized cloud assembly was found at .*manifest\.json/, + ); + expect(runs).toEqual([]); + }); + + test("fails before touching AWS when the CDK dependencies are missing", async () => { + const root = await inTempDirectory(); + const { backend: subject, commands, runs } = backend(); + const built = await project(root, false); + await synthesized(root, [TARGET.name]); + + await expect(drain(subject.deploy(built, { target: TARGET, region: REGION }))).rejects.toThrow( + /npm install/, + ); + expect(commands).toEqual([]); + expect(runs).toEqual([]); + }); + + test("logs the toolkit's messages instead of reporting them as events", async () => { + const root = await inTempDirectory(); + const { backend: subject, logs } = backend((operation, emit) => { + if (operation.kind !== "deploy") return; + emit({ level: "info", message: "example-stack: creating CloudFormation changeset..." }); + emit({ level: "result", message: "example-stack: deployed" }); + }); + const built = await project(root); + await synthesized(root, [TARGET.name]); + + const events = await drain(subject.deploy(built, { target: TARGET, region: REGION })); + + // A deploy reports its own steps and nothing the toolkit said, so what reaches the + // screen stays the same length however talkative a deploy turns out to be. + expect(events).toEqual([ + { kind: "step", message: "Synthesizing CloudFormation templates" }, + { kind: "step", message: "Bootstrapping aws://111122223333/us-east-1" }, + { kind: "step", message: `Deploying ${stackName("default")}` }, + ]); + // `result` reports an outcome rather than a severity, so it lands as info. + expect(logged(logs)).toEqual([ + { level: "info", message: "example-stack: creating CloudFormation changeset..." }, + { level: "info", message: "example-stack: deployed" }, + ]); + }); + + test("logs each toolkit message at the severity the toolkit gave it", async () => { + const root = await inTempDirectory(); + const { backend: subject, logs } = backend((operation, emit) => { + if (operation.kind !== "deploy") return; + emit({ level: "debug", message: "resolved 3 environments" }); + emit({ level: "trace", message: "sdk call: DescribeStacks" }); + emit({ level: "warn", message: "example-stack: no changes" }); + emit({ level: "error", message: "example-stack: UPDATE_ROLLBACK_COMPLETE" }); + }); + const built = await project(root); + await synthesized(root, [TARGET.name]); + + await drain(subject.deploy(built, { target: TARGET, region: REGION })); + + // Severity is what makes the log readable once a deploy has written thousands of + // lines to it: a failure is greppable rather than buried among the trace, which is + // finer than any level this CLI logs at and so joins debug. + expect(logged(logs)).toEqual([ + { level: "debug", message: "resolved 3 environments" }, + { level: "debug", message: "sdk call: DescribeStacks" }, + { level: "warn", message: "example-stack: no changes" }, + { level: "error", message: "example-stack: UPDATE_ROLLBACK_COMPLETE" }, + ]); + }); + + test("strips the colours the toolkit writes for the terminal it assumes", async () => { + const root = await inTempDirectory(); + const { backend: subject, logs } = backend((operation, emit) => { + if (operation.kind !== "deploy") return; + emit({ level: "info", message: `${ESC}[32m✅ example-stack${ESC}[39m` }); + }); + const built = await project(root); + await synthesized(root, [TARGET.name]); + + await drain(subject.deploy(built, { target: TARGET, region: REGION })); + + // The log is a file, where escape codes are noise a grep has to match around. + expect(logged(logs)).toEqual([{ level: "info", message: "✅ example-stack" }]); + }); + + test("logs the output that explains a failure before propagating it", async () => { + const root = await inTempDirectory(); + const { backend: subject, logs } = backend((operation, emit) => { + if (operation.kind !== "deploy") return; + emit({ level: "error", message: "example-stack: CREATE_FAILED" }); + throw new Error("cdk deploy exploded"); + }); + const built = await project(root); + await synthesized(root, [TARGET.name]); + + await expect(drain(subject.deploy(built, { target: TARGET, region: REGION }))).rejects.toThrow( + "cdk deploy exploded", + ); + + // The failure reaches the caller, but only the log says what went wrong, which is + // why deploy prints where the log is. + expect(logged(logs)).toContainEqual({ + level: "error", + message: "example-stack: CREATE_FAILED", + }); + }); +}); diff --git a/src/core/project/backends/cdk.tsx b/src/core/project/backends/cdk.tsx new file mode 100644 index 000000000..9ed50a265 --- /dev/null +++ b/src/core/project/backends/cdk.tsx @@ -0,0 +1,136 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import type { Project, ProjectEvent } from "../../../handlers/project/types"; +import type { Logger } from "../../../logging"; +import { + FsReadWriteJson, + requireTool, + runCdk, + runProcess, + type CdkEvent, + type CdkOperation, + type CdkRunner, + type CdkRunOptions, + type ProcessRunner, + type ReadWriteJson, +} from "../../../io"; +import { ProjectStateError } from "../../../errors/errors"; +import { stackForTarget } from "../assembly"; +import type { BackendDeployInput, ProjectBackend } from "./types"; + +// `result` is an outcome rather than a severity, and the log has no level below debug. +const CDK_LOG_LEVELS: Record = { + error: "error", + warn: "warn", + info: "info", + result: "info", + debug: "debug", + trace: "debug", +}; + +// The toolkit colours its output for the terminal it assumes it is writing to. +// eslint-disable-next-line no-control-regex +const ANSI_ESCAPES = /\x1b\[[0-9;]*[a-zA-Z]/g; + +export type CdkBackendConfig = { + logger: Logger; + runner?: ProcessRunner; // injectable so tests never spawn real processes + cdk?: CdkRunner; // injectable so tests never reach AWS + checkTool?: typeof requireTool; // injectable so tests don't depend on the host's PATH + json?: ReadWriteJson; // injectable so tests read fixtures instead of disk +}; + +/** Builds and deploys a project through the CDK app that `agentcore create` generates. */ +export class CdkBackend implements ProjectBackend { + private readonly logger: Logger; + private readonly runner: ProcessRunner; + private readonly cdk: CdkRunner; + private readonly checkTool: typeof requireTool; + private readonly json: ReadWriteJson; + + constructor(config: CdkBackendConfig) { + this.logger = config.logger; + this.runner = config.runner ?? runProcess; + this.cdk = config.cdk ?? runCdk; + this.checkTool = config.checkTool ?? requireTool; + this.json = config.json ?? new FsReadWriteJson({ logger: config.logger }); + } + + public async *build(project: Project): AsyncGenerator { + const cdkDir = this.cdkPath(project); + + // The generated CDK app is built from its own node_modules; without them the + // failure would otherwise surface as an opaque "cdk: not found". + if (!existsSync(join(cdkDir, "node_modules"))) { + throw new ProjectStateError( + `CDK dependencies are missing for project '${project.name}'. ` + + `Run 'cd ${cdkDir} && npm install'.`, + ); + } + await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); + + // The generated package.json defines `cdk` as "npm run build && cdk", so this one + // command compiles and then synthesizes. Synthesis needs no AWS credentials. + // + // Build deliberately does not require a deployment target. A freshly created + // project has none, and building is how the user first typechecks their agent, so + // the generated app synthesizes one environment-agnostic stack when the list is + // empty. Only deploy needs a real target, and it checks for one itself. + // + // --output is explicit because deploy reads this directory back: a project that set + // cdk.json's `output` would otherwise have deploy ship whatever it found here. + yield { kind: "step", message: "Synthesizing CloudFormation templates" }; + await this.run( + ["npm", "run", "cdk", "--", "synth", "--quiet", "--output", this.assemblyPath(project)], + cdkDir, + ); + } + + public async *deploy( + project: Project, + input: BackendDeployInput, + ): AsyncGenerator { + // Deploy ships the assembly build just synthesized, so the two cannot drift. + yield* this.build(project); + const run = { + assemblyDirectory: this.assemblyPath(project), + region: input.region, + }; + + // Resolved before bootstrapping so an assembly with no stack for this target fails + // immediately rather than after minutes of bootstrapping. + const stackName = await stackForTarget(this.json, run.assemblyDirectory, input.target.name); + + // Bootstrap is idempotent and quick on an already-current environment, so it runs + // every deploy rather than probing CloudFormation first. + const environment = `aws://${input.target.account}/${input.target.region}`; + yield { kind: "step", message: `Bootstrapping ${environment}` }; + await this.runCdkOperation({ kind: "bootstrap", environments: [environment] }, run); + + yield { kind: "step", message: `Deploying ${stackName}` }; + await this.runCdkOperation({ kind: "deploy", stackName }, run); + } + + private cdkPath(project: Project): string { + return join(project.rootPath, "agentcore", "cdk"); + } + + // Shared by build and deploy so the two cannot disagree about where the assembly is. + private assemblyPath(project: Project): string { + return join(this.cdkPath(project), "cdk.out"); + } + + // The toolkit narrates a deploy in hundreds of lines, so all of it goes to the log at + // the severity the toolkit gave it, and the steps above are what a deploy shows. + private async runCdkOperation(operation: CdkOperation, options: CdkRunOptions): Promise { + const logger = this.logger.child({ cdk: operation.kind }); + for await (const event of this.cdk(operation, options)) { + logger[CDK_LOG_LEVELS[event.level]](event.message.replace(ANSI_ESCAPES, "")); + } + } + + // Runs a command with its output streamed to the file logger. + private run(command: string[], cwd: string): Promise { + return this.runner(command, { cwd, onOutput: (chunk) => this.logger.debug(chunk) }); + } +} diff --git a/src/core/project/backends/types.ts b/src/core/project/backends/types.ts new file mode 100644 index 000000000..ffc82002e --- /dev/null +++ b/src/core/project/backends/types.ts @@ -0,0 +1,19 @@ +import type { Project, ProjectEvent } from "../../../handlers/project/types"; +import type { AwsTarget } from "../../../projectSchemas/aws-targets"; + +export type BackendDeployInput = { + /** The entry from aws-targets.json this deploy ships to. */ + target: AwsTarget; + /** The resolved AWS region the backend's own tooling makes its calls in. */ + region: string; +}; + +/** + * The part of build and deploy that belongs to whichever tool owns a project's + * deployable artifacts, selected by the project's `managedBy`. A terraform or no-IaC + * project implements this; nothing above it names a tool. + */ +export interface ProjectBackend { + build(project: Project): AsyncGenerator; + deploy(project: Project, input: BackendDeployInput): AsyncGenerator; +} diff --git a/src/core/project/index.tsx b/src/core/project/index.tsx index dfc769f53..0d94b0f15 100644 --- a/src/core/project/index.tsx +++ b/src/core/project/index.tsx @@ -1 +1,3 @@ export { FsProjectManager } from "./manager"; +export { CdkBackend } from "./backends/cdk"; +export type { BackendDeployInput, ProjectBackend } from "./backends/types"; diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index 0ed58a12f..278f4b9fe 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -10,8 +10,8 @@ import { type Project, type ProjectEvent, } from "../../handlers/project/types"; -import { createRecordingLogger, createSilentLogger, type RecordedLog } from "../../testing"; -import type { CdkEvent, CdkOperation, CdkRunOptions } from "../../io"; +import { createSilentLogger } from "../../testing"; +import type { BackendDeployInput, ProjectBackend } from "./backends/types"; const originalCwd = process.cwd(); const tempDirectories: string[] = []; @@ -47,6 +47,60 @@ function manager(): { manager: FsProjectManager; commands: { command: string[]; }; } +type RecordedDeploy = { project: Project; input: BackendDeployInput }; + +// A manager whose backend records what it is handed instead of building anything, so +// these tests cover only the part of build and deploy that is not a tool's business. +function delegating(): { + manager: FsProjectManager; + builds: Project[]; + deploys: RecordedDeploy[]; +} { + const builds: Project[] = []; + const deploys: RecordedDeploy[] = []; + const backend: ProjectBackend = { + // eslint-disable-next-line require-yield + build: async function* (project) { + builds.push(project); + }, + // eslint-disable-next-line require-yield + deploy: async function* (project, input) { + deploys.push({ project, input }); + }, + }; + return { + manager: new FsProjectManager({ + logger: createSilentLogger(), + runner: async () => {}, + checkTool: async () => {}, + backends: { CDK: backend }, + }), + builds, + deploys, + }; +} + +const TARGET = { name: "default", account: "111122223333", region: "us-east-1" }; + +// Only what the manager itself reads: a project, and the target list beside it. A string +// is written verbatim so a test can supply invalid JSON, and null writes no file at all. +async function withTargets(root: string, targets: unknown = [TARGET]): Promise { + await mkdir(join(root, "agentcore"), { recursive: true }); + if (targets !== null) { + await writeFile( + join(root, "agentcore", "aws-targets.json"), + typeof targets === "string" ? targets : JSON.stringify(targets), + ); + } + return { name: "example", rootPath: root, managedBy: "CDK", runtimes: [] }; +} + +async function drain(generator: AsyncGenerator): Promise { + const events: ProjectEvent[] = []; + for await (const event of generator) events.push(event); + return events; +} + async function runCreate( subject: FsProjectManager, input: CreateProjectInput, @@ -220,527 +274,135 @@ describe("FsProjectManager.create", () => { }); describe("FsProjectManager.build", () => { - // build() requires the CDK app's node_modules; create() with skipInstall - // never produces them, so tests stub the directory in. - async function scaffolded( - subject: FsProjectManager, - directory: string, - withDependencies = true, - ): Promise { - const { project } = await runCreate(subject, { - name: "example", - template: PROJECT_TEMPLATES.HELLO_WORLD_PYTHON, - skipInstall: true, - skipGit: true, - }); - if (withDependencies) { - await mkdir(join(directory, "example", "agentcore", "cdk", "node_modules"), { - recursive: true, - }); - } - return project; - } - - async function drain(generator: AsyncGenerator): Promise { - const events: ProjectEvent[] = []; - for await (const event of generator) events.push(event); - return events; - } - - test("compiles and synthesizes via the generated cdk script", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands } = manager(); - const project = await scaffolded(subject, directory); - commands.length = 0; // discard create()'s commands - - const events = await drain(subject.build(project)); - - const cdkDir = join(directory, "example", "agentcore", "cdk"); - expect(commands).toEqual([ - { - // --output pins the assembly where deploy looks for it, so a project that - // sets cdk.json's `output` cannot send synth somewhere deploy never reads. - command: [ - "npm", - "run", - "cdk", - "--", - "synth", - "--quiet", - "--output", - join(cdkDir, "cdk.out"), - ], - cwd: cdkDir, - }, - ]); - expect(events).toEqual([{ kind: "step", message: "Synthesizing CloudFormation templates" }]); - }); + test("hands the project to the backend that owns its artifacts", async () => { + const root = await inTempDirectory(); + const { manager: subject, builds } = delegating(); + const project = await withTargets(root); - test("fails actionably when the CDK dependencies are missing", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands } = manager(); - const project = await scaffolded(subject, directory, false); - commands.length = 0; + await drain(subject.build(project)); - await expect(drain(subject.build(project))).rejects.toThrow(/npm install/); - expect(commands).toEqual([]); + expect(builds).toEqual([project]); }); - test("refuses a project managed by a backend it cannot build", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands } = manager(); - const project = await scaffolded(subject, directory); - commands.length = 0; + test("refuses a project managed by a backend it has none for", async () => { + const root = await inTempDirectory(); + const { manager: subject, builds } = delegating(); + const project = await withTargets(root); // CDK is the only backend today; the cast stands in for a future one. const foreign = { ...project, managedBy: "Terraform" as Project["managedBy"] }; await expect(drain(subject.build(foreign))).rejects.toThrow(/unsupported backend: Terraform/); - expect(commands).toEqual([]); - }); - - test("propagates a synthesis failure", async () => { - const directory = await inTempDirectory(); - const { manager: subject } = manager(); - const project = await scaffolded(subject, directory); - const failing = new FsProjectManager({ - logger: createSilentLogger(), - runner: async () => { - throw new Error("cdk synth exploded"); - }, - checkTool: async () => {}, - }); - - await expect(drain(failing.build(project))).rejects.toThrow("cdk synth exploded"); + expect(builds).toEqual([]); }); }); describe("FsProjectManager.deploy", () => { - // Synth is pinned to the directory deploy reads, so both name it the same way. - function synthCommand(directory: string): string[] { - return [ - "npm", - "run", - "cdk", - "--", - "synth", - "--quiet", - "--output", - assemblyDirectory(directory), - ]; - } + // The region the backend's own tooling makes its calls in, which is not a target's. const REGION = "us-west-2"; - type RecordedCommand = { command: string[]; cwd: string }; - type RecordedCdkRun = { operation: CdkOperation; options: CdkRunOptions }; - - // Like manager(), but also records the CDK operations the manager drives, and lets - // a test supply the events one of them emits (and the failure it ends with). - function deployManager( - onCdk?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, - ): { - manager: FsProjectManager; - commands: RecordedCommand[]; - runs: RecordedCdkRun[]; - logs: RecordedLog[]; - } { - const commands: RecordedCommand[] = []; - const runs: RecordedCdkRun[] = []; - const { logger, logs } = createRecordingLogger(); - return { - manager: new FsProjectManager({ - logger, - runner: async (command, { cwd }) => { - commands.push({ command, cwd }); - }, - cdk: async function* (operation, options) { - runs.push({ operation, options }); - const events: CdkEvent[] = []; - let failure: unknown; - try { - onCdk?.(operation, (event) => events.push(event)); - } catch (error) { - failure = error; - } - // Emit before throwing, as the real runner does: the output explaining a - // failure is only useful if the consumer sees it. - yield* events; - if (failure) throw failure; - }, - checkTool: async () => {}, - }), - commands, - runs, - logs, - }; - } - - // The assembly directory every operation reads, which build synthesized into. - function assemblyDirectory(directory: string): string { - return join(directory, "example", "agentcore", "cdk", "cdk.out"); - } - - // How the generated CDK app names the stack it synthesizes for a target. - function stackName(target: string): string { - return `AgentCore-example-${target}`; - } - - // Stands in for what synth leaves behind: a manifest with one stack per target, - // each tagged with the target it belongs to. deploy reads it to find the stack to - // ship, and the stubbed runner never writes one. - async function synthesized(directory: string, targetNames: string[]): Promise { - const assembly = assemblyDirectory(directory); - await mkdir(assembly, { recursive: true }); - await writeFile( - join(assembly, "manifest.json"), - JSON.stringify({ - version: "36.0.0", - artifacts: { - // A non-stack artifact, as a real assembly has: only stacks are candidates. - Tree: { type: "cdk:tree" }, - ...Object.fromEntries( - targetNames.map((target) => [ - stackName(target), - { - type: "aws:cloudformation:stack", - properties: { tags: { "agentcore:target-name": target } }, - }, - ]), - ), - }, - }), - ); - } - - // deploy() builds first, so the CDK app's node_modules must exist; create() with - // skipInstall never produces them. Targets overwrite the empty list create() - // scaffolds; null leaves that empty list in place, and a string is written verbatim - // so a test can supply invalid JSON. A well-formed list also gets the assembly - // synth would have produced for it. - async function scaffolded( - subject: FsProjectManager, - directory: string, - targets: unknown = [{ name: "default", account: "111122223333", region: "us-east-1" }], - ): Promise { - const { project } = await runCreate(subject, { - name: "example", - template: PROJECT_TEMPLATES.HELLO_WORLD_PYTHON, - skipInstall: true, - skipGit: true, - }); - await mkdir(join(directory, "example", "agentcore", "cdk", "node_modules"), { - recursive: true, - }); - if (targets !== null) { - await writeFile( - join(directory, "example", "agentcore", "aws-targets.json"), - typeof targets === "string" ? targets : JSON.stringify(targets), - ); - } - if (Array.isArray(targets)) { - await synthesized( - directory, - (targets as { name?: string }[]).flatMap(({ name }) => (name ? [name] : [])), - ); - } - return project; - } - - async function drain(generator: AsyncGenerator): Promise { - const events: ProjectEvent[] = []; - for await (const event of generator) events.push(event); - return events; - } - - // The toolkit's own messages, as they landed in the log: tagged with the operation - // that reported them, unlike the manager's own lines. - function logged(logs: RecordedLog[]): { level: string; message: string }[] { - return logs - .filter((line) => line.bindings.cdk !== undefined) - .map(({ level, message }) => ({ level, message })); - } - - test("synthesizes, bootstraps the target environment, then deploys its stack", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands, runs } = deployManager(); - const project = await scaffolded(subject, directory); - commands.length = 0; // discard create()'s commands - const cdkDir = join(directory, "example", "agentcore", "cdk"); - const options = { assemblyDirectory: assemblyDirectory(directory), region: REGION }; - - const events = await drain( - subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" }), - ); - - // Only synthesis shells out; everything that reaches AWS goes through the toolkit. - expect(commands).toEqual([{ command: synthCommand(directory), cwd: cdkDir }]); - expect(runs).toEqual([ - { operation: { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, options }, - { operation: { kind: "deploy", stackName: stackName("default") }, options }, - ]); - expect(events).toEqual([ - { kind: "step", message: "Synthesizing CloudFormation templates" }, - { kind: "step", message: "Bootstrapping aws://111122223333/us-east-1" }, - { kind: "step", message: `Deploying ${stackName("default")}` }, - ]); - }); + // The three-target project the target-selection tests share. 'default' is last, so + // selecting it cannot be an artifact of the order the list happens to be in. + const PROD = { name: "prod", account: "444455556666", region: "eu-west-1" }; + const DEFAULT = { name: "default", account: "777788889999", region: "us-west-2" }; + const TARGETS = [ + { name: "staging", account: "111122223333", region: "us-east-1" }, + PROD, + DEFAULT, + ]; - test("synthesizes into the same directory the toolkit deploys from", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands, runs } = deployManager(); - const project = await scaffolded(subject, directory); - commands.length = 0; + test("hands the backend the requested target, and the region to call in", async () => { + const root = await inTempDirectory(); + const { manager: subject, deploys } = delegating(); + const project = await withTargets(root); - await drain( - subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), - ); + await drain(subject.deploy(project, { region: REGION, target: "default" })); - // The invariant behind passing --output at all: whatever synth was told to write - // is exactly what the toolkit is handed. Left to cdk.json's `output`, synth could - // write elsewhere and deploy would ship a stale assembly while reporting success. - const assembly = assemblyDirectory(directory); - expect(commands[0]?.command.at(-1)).toBe(assembly); - expect(runs.map(({ options }) => options.assemblyDirectory)).toEqual([assembly]); + expect(deploys).toEqual([{ project, input: { target: TARGET, region: REGION } }]); }); - // The three-target project the target-selection tests share. - const TARGETS = [ - { name: "staging", account: "111122223333", region: "us-east-1" }, - { name: "prod", account: "444455556666", region: "eu-west-1" }, - { name: "default", account: "777788889999", region: "us-west-2" }, - ]; - - test("deploys only the requested target's stack, into only its environment", async () => { - const directory = await inTempDirectory(); - const { manager: subject, runs } = deployManager(); - const project = await scaffolded(subject, directory, TARGETS); + test("deploys only the requested target", async () => { + const root = await inTempDirectory(); + const { manager: subject, deploys } = delegating(); + const project = await withTargets(root, TARGETS); - await drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "prod" })); + await drain(subject.deploy(project, { region: REGION, target: "prod" })); - // A project with a staging and a prod target cannot reach the others by - // accident: one deploy bootstraps one environment and ships one stack. - expect(runs.map(({ operation }) => operation)).toEqual([ - { kind: "bootstrap", environments: ["aws://444455556666/eu-west-1"] }, - { kind: "deploy", stackName: stackName("prod") }, - ]); + // A project with a staging and a prod target cannot reach the others by accident: + // one deploy ships one target. + expect(deploys.map(({ input }) => input.target)).toEqual([PROD]); }); test("deploys the target named 'default' when none is requested", async () => { - const directory = await inTempDirectory(); - const { manager: subject, runs } = deployManager(); - const project = await scaffolded(subject, directory, TARGETS); + const root = await inTempDirectory(); + const { manager: subject, deploys } = delegating(); + const project = await withTargets(root, TARGETS); // What the handler passes when --target is omitted, and the name the example in // the empty-targets error uses. - await drain( - subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), - ); + await drain(subject.deploy(project, { region: REGION, target: "default" })); - expect(runs.map(({ operation }) => operation)).toEqual([ - { kind: "deploy", stackName: stackName("default") }, - ]); + expect(deploys.map(({ input }) => input.target)).toEqual([DEFAULT]); }); test("names the configured targets when the requested one is not among them", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands, runs } = deployManager(); - const project = await scaffolded(subject, directory, TARGETS); - commands.length = 0; - - await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "prd" })), - ).rejects.toThrow(/no deployment target named 'prd'.*staging, prod, default/s); - // Resolved before synthesizing, so a misspelled --target costs no build. - expect(commands).toEqual([]); - expect(runs).toEqual([]); - }); - - test("fails when the synthesized assembly has no stack for the target", async () => { - const directory = await inTempDirectory(); - const { manager: subject, runs } = deployManager(); - const project = await scaffolded(subject, directory); - // An assembly synthesized from a different target list than the one on disk — - // what a hand-edited CDK app that stops tagging its stacks would leave behind. - await synthesized(directory, ["other"]); - - await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), - ).rejects.toThrow(/no stack for deployment target 'default'/); - // Resolved before bootstrapping, so nothing reached AWS. - expect(runs).toEqual([]); - }); - - test("names the path it looked in when synthesis wrote no assembly", async () => { - const directory = await inTempDirectory(); - const { manager: subject, runs } = deployManager(); - const project = await scaffolded(subject, directory); - // Synthesis is stubbed in these tests, so removing the stand-in manifest is what - // a real synth writing somewhere else entirely would leave behind. - await rm(join(assemblyDirectory(directory), "manifest.json")); - - await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), - ).rejects.toThrow(/No synthesized cloud assembly was found at .*manifest\.json/); - expect(runs).toEqual([]); - }); - - test("skips bootstrapping when asked, and still deploys", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands, runs } = deployManager(); - const project = await scaffolded(subject, directory); - commands.length = 0; - const cdkDir = join(directory, "example", "agentcore", "cdk"); + const root = await inTempDirectory(); + const { manager: subject, deploys } = delegating(); + const project = await withTargets(root, TARGETS); - const events = await drain( - subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), + await expect(drain(subject.deploy(project, { region: REGION, target: "prd" }))).rejects.toThrow( + /no deployment target named 'prd'.*staging, prod, default/s, ); - - expect(commands).toEqual([{ command: synthCommand(directory), cwd: cdkDir }]); - expect(runs.map(({ operation }) => operation)).toEqual([ - { kind: "deploy", stackName: stackName("default") }, - ]); - expect(events).not.toContainEqual({ - kind: "step", - message: "Bootstrapping aws://111122223333/us-east-1", - }); + // Resolved before the backend runs, so a misspelled --target costs no build. + expect(deploys).toEqual([]); }); test("names the file to fix when no deployment targets are configured", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands, runs } = deployManager(); - // create() scaffolds an empty list, which is what a fresh project has. - const project = await scaffolded(subject, directory, null); - commands.length = 0; - - await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), - ).rejects.toThrow(/aws-targets\.json/); - // Nothing ran at all: a deploy with nowhere to go does not even synthesize. - expect(commands).toEqual([]); - expect(runs).toEqual([]); + const root = await inTempDirectory(); + const { manager: subject, deploys } = delegating(); + + // The empty list create() scaffolds, and a project with no list at all: both are a + // deploy with nowhere to go, and neither reaches the backend. + for (const targets of [[], null]) { + const project = await withTargets(root, targets); + await expect( + drain(subject.deploy(project, { region: REGION, target: "default" })), + ).rejects.toThrow(/aws-targets\.json/); + await rm(join(root, "agentcore", "aws-targets.json"), { force: true }); + } + expect(deploys).toEqual([]); }); test("reports a malformed aws-targets.json as an actionable error", async () => { - const directory = await inTempDirectory(); - const { manager: subject } = deployManager(); - const project = await scaffolded(subject, directory, "{ not a target list"); + const root = await inTempDirectory(); + const { manager: subject } = delegating(); + const project = await withTargets(root, "{ not a target list"); await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), + drain(subject.deploy(project, { region: REGION, target: "default" })), ).rejects.toThrow(/is not a valid list of deployment targets/); }); test("rejects targets that omit a required field", async () => { - const directory = await inTempDirectory(); - const { manager: subject } = deployManager(); - const project = await scaffolded(subject, directory, [ - { name: "default", region: "us-east-1" }, - ]); + const root = await inTempDirectory(); + const { manager: subject } = delegating(); + const project = await withTargets(root, [{ name: "default", region: "us-east-1" }]); await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), + drain(subject.deploy(project, { region: REGION, target: "default" })), ).rejects.toThrow(/is not a valid list of deployment targets/); }); - test("logs the toolkit's messages instead of reporting them as events", async () => { - const directory = await inTempDirectory(); - const { manager: subject, logs } = deployManager((operation, emit) => { - if (operation.kind !== "deploy") return; - emit({ level: "info", message: "example-stack: creating CloudFormation changeset..." }); - emit({ level: "result", message: "example-stack: deployed" }); - }); - const project = await scaffolded(subject, directory); - - const events = await drain( - subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), - ); - - // A deploy reports its own steps and nothing the toolkit said, so what reaches - // the screen stays the same length however talkative a deploy turns out to be. - expect(events).toEqual([ - { kind: "step", message: "Synthesizing CloudFormation templates" }, - { kind: "step", message: `Deploying ${stackName("default")}` }, - ]); - // `result` reports an outcome rather than a severity, so it lands as info. - expect(logged(logs)).toEqual([ - { level: "info", message: "example-stack: creating CloudFormation changeset..." }, - { level: "info", message: "example-stack: deployed" }, - ]); - }); - - test("logs each toolkit message at the severity the toolkit gave it", async () => { - const directory = await inTempDirectory(); - const { manager: subject, logs } = deployManager((operation, emit) => { - if (operation.kind !== "deploy") return; - emit({ level: "debug", message: "resolved 3 environments" }); - emit({ level: "trace", message: "sdk call: DescribeStacks" }); - emit({ level: "warn", message: "example-stack: no changes" }); - emit({ level: "error", message: "example-stack: UPDATE_ROLLBACK_COMPLETE" }); - }); - const project = await scaffolded(subject, directory); - - await drain( - subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" }), - ); - - // Severity is what makes the log readable once a deploy has written thousands of - // lines to it: a failure is greppable rather than buried among the trace, which - // is finer than any level this CLI logs at and so joins debug. - expect(logged(logs)).toEqual([ - { level: "debug", message: "resolved 3 environments" }, - { level: "debug", message: "sdk call: DescribeStacks" }, - { level: "warn", message: "example-stack: no changes" }, - { level: "error", message: "example-stack: UPDATE_ROLLBACK_COMPLETE" }, - ]); - }); - - test("logs the output that explains a failure before propagating it", async () => { - const directory = await inTempDirectory(); - const { manager: subject, logs } = deployManager((operation, emit) => { - if (operation.kind !== "deploy") return; - emit({ level: "error", message: "example-stack: CREATE_FAILED" }); - throw new Error("cdk deploy exploded"); - }); - const project = await scaffolded(subject, directory); - - await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: true, target: "default" })), - ).rejects.toThrow("cdk deploy exploded"); - - // The failure reaches the caller, but only the log says what went wrong, which is - // why deploy prints where the log is. - expect(logged(logs)).toContainEqual({ - level: "error", - message: "example-stack: CREATE_FAILED", - }); - }); - - test("refuses a project managed by a backend it cannot deploy", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands, runs } = deployManager(); - const project = await scaffolded(subject, directory); - commands.length = 0; + test("refuses a project managed by a backend it has none for", async () => { + const root = await inTempDirectory(); + const { manager: subject, deploys } = delegating(); + const project = await withTargets(root); // CDK is the only backend today; the cast stands in for a future one. const foreign = { ...project, managedBy: "Terraform" as Project["managedBy"] }; await expect( - drain(subject.deploy(foreign, { region: REGION, skipBootstrap: false, target: "default" })), + drain(subject.deploy(foreign, { region: REGION, target: "default" })), ).rejects.toThrow(/unsupported backend: Terraform/); - expect(commands).toEqual([]); - expect(runs).toEqual([]); - }); - - test("fails before touching AWS when the CDK dependencies are missing", async () => { - const directory = await inTempDirectory(); - const { manager: subject, commands, runs } = deployManager(); - const project = await scaffolded(subject, directory); - await rm(join(directory, "example", "agentcore", "cdk", "node_modules"), { recursive: true }); - commands.length = 0; - - await expect( - drain(subject.deploy(project, { region: REGION, skipBootstrap: false, target: "default" })), - ).rejects.toThrow(/npm install/); - expect(commands).toEqual([]); - expect(runs).toEqual([]); + expect(deploys).toEqual([]); }); }); diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 8b97bc37b..171fc9612 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import type { CreateProjectInput, - DeployProjectOptions, + DeployProjectInput, ResolveProjectInput, Project, ProjectManager, @@ -14,19 +14,15 @@ import type { Logger } from "../../logging"; import { FsReadWriteJson, requireTool, - runCdk, runProcess, - type CdkEvent, - type CdkOperation, - type CdkRunner, - type CdkRunOptions, type ProcessRunner, type ReadWriteJson, } from "../../io"; -import { stackForTarget } from "./assembly"; +import { CdkBackend } from "./backends/cdk"; +import type { ProjectBackend } from "./backends/types"; import { defaultSource, type AssetSource } from "./source"; import { createProjectTreeFromTemplate, TEMPLATES } from "./templates"; -import { ProjectSpecSchema } from "../../projectSchemas/project"; +import { ProjectSpecSchema, type ManagedBy } from "../../projectSchemas/project"; import { AwsTargetsSchema, type AwsTarget } from "../../projectSchemas/aws-targets"; import { enclosingProjectRoot } from "./fsUtils"; import { @@ -39,25 +35,13 @@ import { // Shown when aws-targets.json names no usable target, so the fix is on screen. const TARGETS_EXAMPLE = `[{ "name": "default", "account": "111122223333", "region": "us-east-1" }]`; -// How the CDK toolkit's message levels land in the log. Its `result` reports an -// operation's outcome rather than a severity, and nothing this CLI logs is finer -// than debug, so `trace` joins it there. -const CDK_LOG_LEVELS: Record = { - error: "error", - warn: "warn", - info: "info", - result: "info", - debug: "debug", - trace: "debug", -}; - type ProjectManagerConfig = { logger: Logger; source?: AssetSource; // Bun executable or dist/assets depending on runtime runner?: ProcessRunner; // injectable so tests never spawn real processes - cdk?: CdkRunner; // injectable so tests never reach AWS checkTool?: typeof requireTool; // injectable so tests don't depend on the host's PATH json?: ReadWriteJson; // injectable so tests read fixtures instead of disk + backends?: Partial>; // injectable so tests never reach AWS }; /** @@ -67,17 +51,24 @@ export class FsProjectManager implements ProjectManager { private readonly logger: Logger; private readonly source: AssetSource; private readonly runner: ProcessRunner; - private readonly cdk: CdkRunner; private readonly checkTool: typeof requireTool; private readonly json: ReadWriteJson; + private readonly backends: Partial>; constructor(config: ProjectManagerConfig) { this.logger = config.logger; this.source = config.source ?? defaultSource(); this.runner = config.runner ?? runProcess; - this.cdk = config.cdk ?? runCdk; this.checkTool = config.checkTool ?? requireTool; this.json = config.json ?? new FsReadWriteJson({ logger: config.logger }); + this.backends = config.backends ?? { + CDK: new CdkBackend({ + logger: config.logger, + runner: config.runner, + checkTool: config.checkTool, + json: config.json, + }), + }; } public async resolve(input: ResolveProjectInput): Promise { @@ -105,7 +96,6 @@ export class FsProjectManager implements ProjectManager { } public async *create(input: CreateProjectInput): AsyncGenerator { - // Scaffold into a fresh directory, refusing to nest inside an existing project. const enclosing = enclosingProjectRoot(process.cwd()); if (enclosing) { throw new ProjectStateError( @@ -144,9 +134,8 @@ export class FsProjectManager implements ProjectManager { await this.run(["git", "init"], destination); } - // A created project is a resolvable one, so read it back rather than - // duplicating the template's shape: the returned runtimes are then - // schema-validated instead of the template's loosely-typed spec sections. + // Read back rather than restating the template's shape, so the returned runtimes + // are schema-validated. const project = await this.resolve({ filePath: destination }); if (!project) { throw new ProjectStateError( @@ -166,91 +155,16 @@ export class FsProjectManager implements ProjectManager { } public async *build(project: Project): AsyncGenerator { - // agentcore.json records which backend owns the project's artifacts. CDK is the - // only one today; a terraform or no-IaC backend adds an arm here rather than - // editing the CDK path. - switch (project.managedBy) { - case "CDK": - yield* this.buildWithCdk(project); - break; - default: { - // Exhaustiveness: a new ManagedBy member fails to compile until it is handled. - const unsupported: never = project.managedBy; - throw new ProjectStateError( - `project '${project.name}' declares an unsupported backend: ${String(unsupported)}`, - ); - } - } - } - - // Compiles the generated CDK app and synthesizes its CloudFormation templates. - private async *buildWithCdk(project: Project): AsyncGenerator { - const cdkDir = join(project.rootPath, "agentcore", "cdk"); - - // The generated CDK app is built from its own node_modules; without them the - // failure would otherwise surface as an opaque "cdk: not found". - if (!existsSync(join(cdkDir, "node_modules"))) { - throw new ProjectStateError( - `CDK dependencies are missing for project '${project.name}'. ` + - `Run 'cd ${cdkDir} && npm install'.`, - ); - } - await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); - - // The generated package.json defines `cdk` as "npm run build && cdk", so this - // single command compiles the app and then synthesizes it. Synthesis needs no - // AWS credentials: each stack's environment comes from aws-targets.json. - // - // --output is passed explicitly rather than left to cdk.json, because deploy - // reads this directory back. A project that sets cdk.json's `output` would - // otherwise send synth somewhere deploy never looks, and deploy would ship - // whatever stale assembly it found there while reporting success. - yield { kind: "step", message: "Synthesizing CloudFormation templates" }; - await this.run( - ["npm", "run", "cdk", "--", "synth", "--quiet", "--output", this.assemblyPath(project)], - cdkDir, - ); - } - - // Where build writes the synthesized assembly and deploy reads it from. Both go - // through here so the two can never disagree about the location. - private assemblyPath(project: Project): string { - return join(project.rootPath, "agentcore", "cdk", "cdk.out"); + yield* this.backendFor(project).build(project); } public async *deploy( project: Project, - options: DeployProjectOptions, + input: DeployProjectInput, ): AsyncGenerator { - // Dispatched the same way build is: the backend that owns the artifacts owns - // how they reach AWS. - switch (project.managedBy) { - case "CDK": - yield* this.deployWithCdk(project, options); - break; - default: { - // Exhaustiveness: a new ManagedBy member fails to compile until it is handled. - const unsupported: never = project.managedBy; - throw new ProjectStateError( - `project '${project.name}' declares an unsupported backend: ${String(unsupported)}`, - ); - } - } - } - - // Synthesizes, bootstraps the target's environment, then deploys its stack. - private async *deployWithCdk( - project: Project, - options: DeployProjectOptions, - ): AsyncGenerator { - // Every stack's environment comes from aws-targets.json, so an empty list means - // there is nowhere to deploy. Resolving an account from the active credentials - // would let deploy guess where the user's infrastructure belongs; name the file - // to fix instead, which also keeps deploy from depending on a working STS call. - // - // Read before synthesizing: the generated CDK app refuses to synth an empty - // list too, so checking first replaces its message with one naming the file to - // edit, and skips a synth that could not have produced anything deployable. + // aws-targets.json is the only source of a stack's account and region, so deploy + // never has to resolve one from the active credentials. Read before building so an + // unusable list is reported as the file to edit rather than as a build failure. const targets = await this.readDeploymentTargets(project.rootPath); if (targets.length === 0) { throw new ProjectStateError( @@ -259,62 +173,34 @@ export class FsProjectManager implements ProjectManager { ); } - // One deploy ships one target, so a project with a staging and a prod target - // cannot reach prod by accident. Resolved before synthesizing so a misspelled - // --target costs nothing, and because bootstrap needs the target's environment. - const target = targets.find((candidate) => candidate.name === options.target); + // One deploy ships one target, so a project with staging and prod targets cannot + // reach prod by accident. Resolved first so a misspelled --target costs nothing. + const target = targets.find((candidate) => candidate.name === input.target); if (!target) { throw new ProjectStateError( - `Project '${project.name}' has no deployment target named '${options.target}'. ` + + `Project '${project.name}' has no deployment target named '${input.target}'. ` + `${this.targetsPath(project.rootPath)} defines: ${targets .map((candidate) => candidate.name) .join(", ")}.`, ); } - // Deploying exactly what was just synthesized keeps the two from drifting, and - // build's dependency check runs before anything touches AWS. - yield* this.buildWithCdk(project); - - // build synthesized into cdk.out; deploy reads that assembly rather than - // re-synthesizing, so what reaches AWS is exactly what was synthesized. - const run = { - assemblyDirectory: this.assemblyPath(project), - region: options.region, - }; - - // Resolved before bootstrapping so an assembly without a stack for this target - // fails immediately rather than after minutes of bootstrapping. - const stackName = await stackForTarget(this.json, run.assemblyDirectory, target.name); - - if (!options.skipBootstrap) { - // Bootstrap is idempotent and no-ops quickly on an already-current - // environment, so it runs every deploy rather than probing CloudFormation - // first. - const environment = `aws://${target.account}/${target.region}`; - yield { kind: "step", message: `Bootstrapping ${environment}` }; - await this.runCdkOperation({ kind: "bootstrap", environments: [environment] }, run); - } - - yield { kind: "step", message: `Deploying ${stackName}` }; - await this.runCdkOperation({ kind: "deploy", stackName }, run); + yield* this.backendFor(project).deploy(project, { target, region: input.region }); } - // Drives a CDK operation, recording everything it reports in the log file. - // - // The toolkit narrates a deploy in hundreds of lines, so none of it reaches the - // screen: the steps above are what a deploy shows, and the command points at the - // log for the detail. Each message keeps the toolkit's own severity, so a failure - // reads as an error in the log rather than as one debug line among thousands. - private async runCdkOperation(operation: CdkOperation, options: CdkRunOptions): Promise { - const logger = this.logger.child({ cdk: operation.kind }); - for await (const event of this.cdk(operation, options)) { - logger[CDK_LOG_LEVELS[event.level]](event.message); + // agentcore.json records which tool owns the project's artifacts; a project declaring + // one this build has no backend for is a configuration error, not a crash. + private backendFor(project: Project): ProjectBackend { + const backend = this.backends[project.managedBy]; + if (!backend) { + throw new ProjectStateError( + `project '${project.name}' declares an unsupported backend: ${project.managedBy}`, + ); } + return backend; } - // Reads aws-targets.json, treating a missing file as empty and surfacing a parse - // failure as an actionable error that names the file rather than a Zod dump. + // A missing file reads as empty; a malformed one names itself rather than dumping Zod. private async readDeploymentTargets(projectRoot: string): Promise { const path = this.targetsPath(projectRoot); if (!existsSync(path)) return []; diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index 0cb6c4239..b5c6fa252 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -20,23 +20,15 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = "name of the aws-targets.json entry to deploy to", z.string().default("default"), ), - flag( - "skip-bootstrap", - "skip bootstrapping the target environment before deploying", - z.boolean().default(false), - ), ], handle: async (ctx, flags) => { - // withProject has already resolved the enclosing project. const project = ctx.require(ProjectKey); - // Progress goes to stderr, keeping stdout for machine output. The CDK toolkit's - // own narration goes to the log instead, so a deploy shows its steps and - // nothing else. + // Progress goes to stderr, keeping stdout for machine output. The deployment + // tooling's own narration goes to the log, so a deploy shows only its steps. try { for await (const event of config.projectManager.deploy(project, { region: ctx.require(RegionKey), - skipBootstrap: flags["skip-bootstrap"], target: flags.target, })) { config.io.stderr.write(`${event.message}\n`); @@ -44,8 +36,7 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = config.io.stderr.write(`Deployed project '${project.name}'\n`); } finally { - // Printed however the deploy ended: a failed one is when the detail the log - // holds matters most, and the error itself surfaces after this line. + // Printed however the deploy ended: a failed one is when the detail matters most. config.io.stderr.write(`Detailed logs: ${detailedLogLocation()}\n`); } }, diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 31b9b2794..0cb96a817 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -500,22 +500,9 @@ describe("project deploy", () => { ); }); - test("--skip-bootstrap deploys without bootstrapping", async () => { - const projectRoot = await inProject(); - const { io, core } = await run(["deploy", "--skip-bootstrap"]); - - const cdkDir = join(projectRoot, "agentcore", "cdk"); - expect(core.projectCommands).toEqual([{ command: SYNTH(cdkDir), cwd: cdkDir }]); - expect(core.cdkRuns.map(({ operation }) => operation)).toEqual([ - { kind: "deploy", stackName: STACK("default") }, - ]); - expect(io.stderr()).not.toContain("Bootstrapping"); - expect(io.stderr()).toContain("Deployed project 'MyAgent'"); - }); - test("keeps the CDK toolkit's own messages off screen, naming the log instead", async () => { await inProject(); - const { io } = await run(["deploy", "--skip-bootstrap"], (operation, emit) => { + const { io } = await run(["deploy"], (operation, emit) => { if (operation.kind === "deploy") { emit({ level: "info", message: "example-stack | 1/2 | CREATE_IN_PROGRESS" }); } @@ -533,7 +520,7 @@ describe("project deploy", () => { if (operation.kind === "deploy") throw new Error("cdk deploy exploded"); }); - await expect(route(["deploy", "--skip-bootstrap"])).rejects.toThrow("cdk deploy exploded"); + await expect(route(["deploy"])).rejects.toThrow("cdk deploy exploded"); // The failure is when the log matters most: it holds the toolkit's account of // what went wrong, which the error itself does not carry. diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index c56f1e3ac..f76fe5ff3 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -32,25 +32,16 @@ export type CreateProjectInput = { skipGit?: boolean; }; -/** - * Something worth showing the user while a long-running project operation runs. - * - * Discriminated on `kind`, of which there is one today: the tools an operation - * drives report to the log rather than to the screen, so what a command shows is - * its own steps. A later variant can carry fields a step has no use for without - * changing what a consumer of `step` reads. - */ +/** Progress worth showing while a long-running project operation runs. */ export type ProjectEvent = { - /** A progress step, in this CLI's own wording. */ + /** Discriminated so a later variant can carry fields a step has no use for. */ kind: "step"; message: string; }; -export type DeployProjectOptions = { - /** The resolved AWS region, forwarded to the CDK subprocesses. */ +export type DeployProjectInput = { + /** The resolved AWS region the deployment tooling makes its own calls in. */ region: string; - /** Skip bootstrapping the target environment before deploying. */ - skipBootstrap: boolean; /** Name of the aws-targets.json entry to deploy. */ target: string; }; @@ -77,11 +68,11 @@ export interface ProjectManager { /** Scaffold a new AgentCore project from the given template. */ create(input: CreateProjectInput): AsyncGenerator; - /** Compile the project's CDK app and synthesize its CloudFormation templates. */ + /** Build the project's deployable artifacts with whatever backend owns them. */ build(project: Project): AsyncGenerator; - /** Build the project, then deploy the synthesized stacks to its deployment targets. */ - deploy(project: Project, options: DeployProjectOptions): AsyncGenerator; + /** Build the project, then deploy it to one of its deployment targets. */ + deploy(project: Project, input: DeployProjectInput): AsyncGenerator; /** Locate an existing AgentCore project. Returns undefined if no project can be found. */ resolve(input: ResolveProjectInput): Promise; diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 93f130459..72cdc3c8e 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -144,11 +144,11 @@ import type { import { isTerminalStatus } from "../core/batchEvaluationResults"; import { abortable } from "../core/abortable"; import type { CoreOptions } from "../core/types"; -import type { CdkEvent, CdkOperation, CdkRunOptions } from "../io"; +import type { CdkEvent, CdkOperation, CdkRunOptions, ProcessRunner } from "../io"; import type { ProjectManager } from "../handlers/project/types"; import type { Logger } from "../logging"; import { createSilentLogger } from "./logging"; -import { FsProjectManager } from "../core/project"; +import { CdkBackend, FsProjectManager } from "../core/project"; // TestCoreClient is a hand-controllable `Core` for tests. It implements the same // interface the real CoreClient satisfies, so it drops straight into @@ -1964,26 +1964,36 @@ export class TestCoreClient implements Core { // A generator cannot be an arrow function, so close over the array instead of // reaching for `this` inside it. const cdkRuns = this.cdkRuns; + const logger = options?.logger ?? createSilentLogger(); + const runner: ProcessRunner = async (command, { cwd }) => { + this.projectCommands.push({ command, cwd }); + }; + const checkTool = async () => {}; // CI hosts don't have uv installed this.projectManager = new FsProjectManager({ - logger: options?.logger ?? createSilentLogger(), - runner: async (command, { cwd }) => { - this.projectCommands.push({ command, cwd }); - }, - cdk: async function* (operation, runOptions) { - cdkRuns.push({ operation, options: runOptions }); - const emitted: CdkEvent[] = []; - let failure: unknown; - try { - options?.onCdkOperation?.(operation, (event) => emitted.push(event)); - } catch (error) { - failure = error; - } - // Emit before throwing, as the real runner does: the output explaining a - // failure is only useful if the consumer sees it. - yield* emitted; - if (failure) throw failure; + logger, + runner, + checkTool, + backends: { + CDK: new CdkBackend({ + logger, + runner, + checkTool, + cdk: async function* (operation, runOptions) { + cdkRuns.push({ operation, options: runOptions }); + const emitted: CdkEvent[] = []; + let failure: unknown; + try { + options?.onCdkOperation?.(operation, (event) => emitted.push(event)); + } catch (error) { + failure = error; + } + // Emit before throwing, as the real runner does: the output explaining a + // failure is only useful if the consumer sees it. + yield* emitted; + if (failure) throw failure; + }, + }), }, - checkTool: async () => {}, // CI hosts don't have uv installed }); } } From 5008322a2319e514be1ac7d1740b53ac951364f5 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 17:08:11 -0400 Subject: [PATCH 08/18] feat(logging): file each run's log under the command that wrote it A project's log held every command's output in one file per day, so reading a failed deploy meant reading around every create, list and invoke that shared the day with it. Each command now writes into a directory of its own: agentcore/.cli/logs/deploy/deploy-2026-08-17.log Rotation is unchanged, so a command's directory holds one file per day, capped at ten. The name comes from argv, because the logger has to exist before there is a router to ask what is running: it is the last segment of the leading run of non-flag tokens, so `--region us-east-1 project deploy` and `project deploy --target prod` both name deploy, and a run with no command of its own -- the TUI -- is filed under the CLI. Only that last segment is used, which is why finding it needs no flag definitions. logFilePrefix stays the single place the path is built, so the file the entrypoint opens and the one deploy prints cannot disagree. --- src/index.ts | 6 +- src/logging/location.test.ts | 128 ++++++++++++++++++++++++++++++----- src/logging/location.tsx | 74 ++++++++++++++------ 3 files changed, 170 insertions(+), 38 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2db95ffbe..fc0b5a60f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,8 +31,10 @@ process.exit( const cliSessionId = crypto.randomUUID(); const rootLogger = createFileLogger({ - // The standard location, shared with the commands that print where it is. - filePath: logFilePrefix(), + // The standard location, shared with the commands that print where it is. The log + // is filed under the command being run, which is read from the argv the router is + // about to parse: the logger has to exist before there is a router to ask. + filePath: logFilePrefix({ argv }), logLevel: LOG_LEVEL.DEBUG, bindings: { cliSessionId, version: PACKAGE_VERSION }, }); diff --git a/src/logging/location.test.ts b/src/logging/location.test.ts index 645cbf40f..52a8d4d25 100644 --- a/src/logging/location.test.ts +++ b/src/logging/location.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { mkdtemp, readdir, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { createFileLogger } from "./fileLogger"; @@ -14,6 +14,20 @@ afterEach(async () => { ); }); +// A directory the location functions read as a project: the marker they walk up to. +async function project(): Promise { + const root = await mkdtemp(join(tmpdir(), "agentcore-project-")); + tempDirectories.push(root); + await mkdir(join(root, "agentcore"), { recursive: true }); + await writeFile(join(root, "agentcore", "agentcore.json"), "{}"); + return root; +} + +// argv as the CLI is handed it: the executable and the script, then the user's tokens. +function argv(...tokens: string[]): string[] { + return ["node", "agentcore", ...tokens]; +} + describe("rotatedLogFile", () => { test("names the file the rotating logger writes", async () => { // The point of the function: it predicts a name winston chooses, so it is @@ -21,40 +35,122 @@ describe("rotatedLogFile", () => { // pattern it was written from. const directory = await mkdtemp(join(tmpdir(), "agentcore-logs-")); tempDirectories.push(directory); - const prefix = join(directory, "output"); + // Nested under a command as the real prefix is, which also covers the transport + // creating that directory rather than failing on a path that does not exist yet. + const commandDirectory = join(directory, "deploy"); - const logger = createFileLogger({ filePath: prefix, logLevel: LOG_LEVEL.DEBUG }); + const logger = createFileLogger({ + filePath: join(commandDirectory, "deploy"), + logLevel: LOG_LEVEL.DEBUG, + }); logger.info("example"); await logger.end(); // The transport also writes a hidden audit file beside the log. - const written = (await readdir(directory)).filter((name) => name.endsWith(".log")); - expect(written).toEqual([rotatedLogFile("output", new Date())]); + const written = (await readdir(commandDirectory)).filter((name) => name.endsWith(".log")); + expect(written).toEqual([rotatedLogFile("deploy", new Date())]); }); test("pads a single-digit month and day, as the date pattern does", () => { - expect(rotatedLogFile("output", new Date(2026, 0, 5))).toBe("output-2026-01-05.log"); + expect(rotatedLogFile("deploy", new Date(2026, 0, 5))).toBe("deploy-2026-01-05.log"); + }); +}); + +describe("logFilePrefix", () => { + test("writes into the project the command was run from, filed under the command", async () => { + const root = await project(); + + expect(logFilePrefix({ cwd: root, argv: argv("project", "deploy") })).toBe( + join(root, "agentcore", ".cli", "logs", "deploy", "deploy"), + ); + }); + + test("finds the project from a directory inside it", async () => { + const root = await project(); + const inside = join(root, "app", "src"); + await mkdir(inside, { recursive: true }); + + expect(logFilePrefix({ cwd: inside, argv: argv("project", "deploy") })).toBe( + join(root, "agentcore", ".cli", "logs", "deploy", "deploy"), + ); + }); + + test("falls back to the home directory outside a project", async () => { + const elsewhere = await mkdtemp(join(tmpdir(), "agentcore-elsewhere-")); + tempDirectories.push(elsewhere); + const home = join(tmpdir(), "example-home"); + + expect(logFilePrefix({ cwd: elsewhere, home, argv: argv("project", "deploy") })).toBe( + join(home, ".agentcore", "logs", "deploy", "deploy"), + ); + }); + + test("names the command whatever surrounds it on the command line", async () => { + const root = await project(); + const deploy = join(root, "agentcore", ".cli", "logs", "deploy", "deploy"); + + // A global flag's value is not a command, and neither is a flag after one. + expect(logFilePrefix({ cwd: root, argv: argv("project", "deploy", "--target", "prod") })).toBe( + deploy, + ); + expect( + logFilePrefix({ cwd: root, argv: argv("--region", "us-east-1", "project", "deploy") }), + ).toBe(deploy); + expect(logFilePrefix({ cwd: root, argv: argv("--verbose", "project", "deploy") })).toBe(deploy); + }); + + test("files a run with no command of its own under the CLI itself", async () => { + const root = await project(); + const bare = join(root, "agentcore", ".cli", "logs", "agentcore", "agentcore"); + + // The TUI, and the TUI with a global flag that could be mistaken for a command. + expect(logFilePrefix({ cwd: root, argv: argv() })).toBe(bare); + expect(logFilePrefix({ cwd: root, argv: argv("--region", "us-east-1") })).toBe(bare); }); }); describe("detailedLogLocation", () => { - test("points at the file the logger writes, with the home directory shortened", () => { + const DEPLOY = argv("project", "deploy"); + + test("names a project's log relative to where the command ran", async () => { + const root = await project(); + const at = new Date(2026, 7, 17, 13, 45); + + expect(detailedLogLocation({ at, cwd: root, argv: DEPLOY })).toBe( + join("agentcore", ".cli", "logs", "deploy", "deploy-2026-08-17.log"), + ); + // Run from a subdirectory, the same file is named relative to that. + const inside = join(root, "agentcore"); + expect(detailedLogLocation({ at, cwd: inside, argv: DEPLOY })).toBe( + join(".cli", "logs", "deploy", "deploy-2026-08-17.log"), + ); + // Both spell the file the logger actually writes. + expect(rotatedLogFile(logFilePrefix({ cwd: inside, argv: DEPLOY }), at)).toBe( + join(root, "agentcore", ".cli", "logs", "deploy", "deploy-2026-08-17.log"), + ); + }); + + test("shortens the fallback log to `~` rather than making it relative", async () => { + const elsewhere = await mkdtemp(join(tmpdir(), "agentcore-elsewhere-")); + tempDirectories.push(elsewhere); const home = join(tmpdir(), "example-home"); const at = new Date(2026, 7, 17, 13, 45); - expect(detailedLogLocation(at, home)).toBe( - join("~", ".agentcore", "logs", "output-2026-08-17.log"), + expect(detailedLogLocation({ at, cwd: elsewhere, home, argv: DEPLOY })).toBe( + join("~", ".agentcore", "logs", "deploy", "deploy-2026-08-17.log"), ); - // The same file spelled in full, which is what the `~` stands in for. - expect(rotatedLogFile(logFilePrefix(home), at)).toBe( - join(home, ".agentcore", "logs", "output-2026-08-17.log"), + expect(rotatedLogFile(logFilePrefix({ cwd: elsewhere, home, argv: DEPLOY }), at)).toBe( + join(home, ".agentcore", "logs", "deploy", "deploy-2026-08-17.log"), ); }); - test("stays absolute for a log that does not sit under the home directory", () => { + test("stays absolute for a log that does not sit under the home directory", async () => { + const elsewhere = await mkdtemp(join(tmpdir(), "agentcore-elsewhere-")); + tempDirectories.push(elsewhere); + // Nothing shortens to a bare `~`, so a home of `/` is spelled out instead. - expect(detailedLogLocation(new Date(2026, 7, 17), "/")).toBe( - join("/", ".agentcore", "logs", "output-2026-08-17.log"), - ); + expect( + detailedLogLocation({ at: new Date(2026, 7, 17), cwd: elsewhere, home: "/", argv: DEPLOY }), + ).toBe(join("/", ".agentcore", "logs", "deploy", "deploy-2026-08-17.log")); }); }); diff --git a/src/logging/location.tsx b/src/logging/location.tsx index bb456a791..6dd8c6263 100644 --- a/src/logging/location.tsx +++ b/src/logging/location.tsx @@ -1,26 +1,56 @@ import { homedir } from "node:os"; -import { join, sep } from "node:path"; +import { join, relative, sep } from "node:path"; +import { enclosingProjectRoot } from "../core/project/fsUtils"; -// The CLI keeps its log beside the rest of its state, under the user's home. -const LOG_DIRECTORY = [".agentcore", "logs"]; +// A project's log joins the rest of its CLI state under agentcore/.cli, which the +// scaffolded .gitignore excludes. Outside a project there is nothing to write into, so +// the log falls back beside the CLI's global state. +const PROJECT_LOG_DIRECTORY = ["agentcore", ".cli", "logs"]; +const HOME_LOG_DIRECTORY = [".agentcore", "logs"]; + +// What a run with no command to name it after — a bare `agentcore` — is filed under. +const CLI_NAME = "agentcore"; + +type Location = { + /** Where the command was run from; decides whether a project owns the log. */ + cwd?: string; + home?: string; + /** `process.argv`, as the router is handed it; names the command the log belongs to. */ + argv?: string[]; +}; /** - * The file path prefix the CLI's log rotates on. + * The command a log belongs to: `deploy`, for `agentcore project deploy --target prod`. * - * Everything that needs the log's location reads it from here — the entrypoint that - * creates the logger, and the commands that tell the user where to look — so the - * location printed is always the one being written to. + * Only the last segment of the command path is wanted, so this needs no knowledge of any + * flag: the path is the first unbroken run of non-flag tokens, less its first token when + * a flag precedes the run, since that token may be the flag's value. */ -export function logFilePrefix(home: string = homedir()): string { - return join(home, ...LOG_DIRECTORY, "output"); +function commandName(argv: string[]): string { + const tokens = argv.slice(2); + const first = tokens.findIndex((token) => !token.startsWith("-")); + if (first === -1) return CLI_NAME; + const flagged = tokens.findIndex((token, index) => index > first && token.startsWith("-")); + const path = tokens.slice(first === 0 ? 0 : first + 1, flagged === -1 ? undefined : flagged); + return path.at(-1) ?? CLI_NAME; } /** - * The file a logger rotating on `prefix` writes `at`'s messages to. - * - * Mirrors what {@link createFileLogger} configures its transport with: - * `${prefix}-%DATE%` plus a `.log` extension, with `YYYY-MM-DD` in local time. + * The prefix the rotating log file is named from: a directory per command holding that + * command's runs, so reading a deploy's log means reading only deploys. */ +export function logFilePrefix({ + cwd = process.cwd(), + home = homedir(), + argv = process.argv, +}: Location = {}): string { + const command = commandName(argv); + const projectRoot = enclosingProjectRoot(cwd); + return projectRoot + ? join(projectRoot, ...PROJECT_LOG_DIRECTORY, command, command) + : join(home, ...HOME_LOG_DIRECTORY, command, command); +} + export function rotatedLogFile(prefix: string, at: Date): string { const month = String(at.getMonth() + 1).padStart(2, "0"); const day = String(at.getDate()).padStart(2, "0"); @@ -28,13 +58,17 @@ export function rotatedLogFile(prefix: string, at: Date): string { } /** - * Where to tell the user their detailed logs are, written as they would type it. - * - * Shortened to `~` rather than made relative to the working directory: the log lives - * under the user's home, so a path relative to a project directory is a run of `../` - * that stops being correct as soon as they cd. + * The log file, spelled the shortest way that stays correct: relative for a project's + * log, and `~`-shortened for the fallback, where relative would be a run of `../` that + * breaks on the next cd. */ -export function detailedLogLocation(at: Date = new Date(), home: string = homedir()): string { - const path = rotatedLogFile(logFilePrefix(home), at); +export function detailedLogLocation({ + at = new Date(), + cwd = process.cwd(), + home = homedir(), + argv = process.argv, +}: Location & { at?: Date } = {}): string { + const path = rotatedLogFile(logFilePrefix({ cwd, home, argv }), at); + if (enclosingProjectRoot(cwd)) return relative(cwd, path); return path.startsWith(`${home}${sep}`) ? `~${path.slice(home.length)}` : path; } From d682e8750d315ae510bb3cea22147eb94c61b5cb Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 18:13:51 -0400 Subject: [PATCH 09/18] fix(logging): write one file per run, named for the run and nothing typed The log rotated daily, so every run of a command on the same day appended to one file and reading a deploy's log meant finding where that deploy started. main writes one timestamped file per run; this does the same, dropping winston-daily-rotate-file with the rotation it configured. The file also no longer takes its directory from the command line. The command was guessed by parsing argv, which a positional argument answers as readily as a subcommand: `config telemetry.enabled false` filed its log under `false`, and `config endpoint ../../../../../../tmp/pwn` under whatever that resolved to. A run's log is named for the CLI and the time it started, so nothing a user types decides where it is written. --- bun.lock | 9 -- package.json | 1 - src/index.ts | 9 +- src/logging/fileLogger.tsx | 35 +++---- src/logging/index.tsx | 2 +- src/logging/location.test.ts | 161 +++++++++++++++-------------- src/logging/location.tsx | 64 +++++------- src/middleware/withLogging.test.ts | 2 +- src/telemetry/client.test.tsx | 2 +- 9 files changed, 130 insertions(+), 155 deletions(-) diff --git a/bun.lock b/bun.lock index 1a9f6297d..165714d1b 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,6 @@ "react-router": "^8.3.0", "string-width": "^8.2.2", "winston": "^3.19.0", - "winston-daily-rotate-file": "^5.0.0", "zod": "^4.4.3", }, "devDependencies": { @@ -347,8 +346,6 @@ "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], - "file-stream-rotator": ["file-stream-rotator@0.6.1", "", { "dependencies": { "moment": "^2.29.1" } }, "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], @@ -425,14 +422,10 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], @@ -537,8 +530,6 @@ "winston": ["winston@3.19.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="], - "winston-daily-rotate-file": ["winston-daily-rotate-file@5.0.0", "", { "dependencies": { "file-stream-rotator": "^0.6.1", "object-hash": "^3.0.0", "triple-beam": "^1.4.1", "winston-transport": "^4.7.0" }, "peerDependencies": { "winston": "^3" } }, "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw=="], - "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], diff --git a/package.json b/package.json index a94afba28..3aab2d1a9 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "react-router": "^8.3.0", "string-width": "^8.2.2", "winston": "^3.19.0", - "winston-daily-rotate-file": "^5.0.0", "zod": "^4.4.3" } } diff --git a/src/index.ts b/src/index.ts index fc0b5a60f..8ed6bdbf7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { } from "./core/factories"; import { createRootHandler } from "./handlers"; import { FsReadWriteJson } from "./io"; -import { createFileLogger, logFilePrefix, LOG_LEVEL } from "./logging"; +import { createFileLogger, logFilePath, LOG_LEVEL } from "./logging"; import { runWithExitCode } from "./runnable"; import { DefaultGlobalConfigAccessor } from "./globalConfig"; import { DefaultTelemetryClient } from "./telemetry"; @@ -31,10 +31,9 @@ process.exit( const cliSessionId = crypto.randomUUID(); const rootLogger = createFileLogger({ - // The standard location, shared with the commands that print where it is. The log - // is filed under the command being run, which is read from the argv the router is - // about to parse: the logger has to exist before there is a router to ask. - filePath: logFilePrefix({ argv }), + // The standard location, shared with the commands that print where it is: one file + // for this run, in the project it was run in or beside the CLI's global state. + filePath: logFilePath(), logLevel: LOG_LEVEL.DEBUG, bindings: { cliSessionId, version: PACKAGE_VERSION }, }); diff --git a/src/logging/fileLogger.tsx b/src/logging/fileLogger.tsx index 1b37dbf6b..1689f2948 100644 --- a/src/logging/fileLogger.tsx +++ b/src/logging/fileLogger.tsx @@ -1,18 +1,16 @@ import winston from "winston"; -import DailyRotateFile from "winston-daily-rotate-file"; import { type AsyncLogger, type LoggerBindings, type LogLevel } from "./types"; export interface FileLoggerConfig { + /** The exact file to write. Its directory is created if it does not exist yet. */ filePath: string; - maxSizeInMB?: number; - maxFileCount?: number; bindings?: LoggerBindings; logLevel: LogLevel; } function wrapWinstonLogger( winstonLogger: winston.Logger, - transport: DailyRotateFile, + transport: winston.transports.FileTransportInstance, bindings: LoggerBindings, ): AsyncLogger { const log = @@ -29,32 +27,29 @@ function wrapWinstonLogger( wrapWinstonLogger(winstonLogger, transport, { ...bindings, ...childBindings }), end: () => new Promise((resolve) => { - transport.on("finish", resolve); - // note: we prefer close over end since close calls end on the stream internally: https://github.com/winstonjs/winston-daily-rotate-file/blob/a1a4668cfea77476cd6a4a11f038c2aac9d10741/daily-rotate-file.js#L201-L207 - if (transport.close) transport.close(); + // Ending the transport flushes what it has buffered and closes the file stream; + // `finish` is what it emits once that has happened. + transport.once("finish", resolve); + transport.end(); }), }; } /** - * Creates a logger that writes structured JSON to a rotating file. + * Creates a logger that writes structured JSON to one file per run. * - * @param config - Logger configuration (file path, rotation limits, level). - * @returns A {@link AsyncLogger} that writes to a rotating file via winston. + * A run's log is a whole run and only that run, so nothing rotates and nothing is + * appended to by a later run; {@link logFilePath} names the file for the run. + * + * @param config - Logger configuration (file path, level, bindings). + * @returns A {@link AsyncLogger} that writes to `config.filePath` via winston. */ export function createFileLogger(config: FileLoggerConfig): AsyncLogger { - const maxSizeInMB = config.maxSizeInMB ?? 5; - const maxFileCount = config.maxFileCount ?? 10; const bindings = config.bindings ?? {}; - const transport = new DailyRotateFile({ - filename: `${config.filePath}-%DATE%`, - extension: ".log", - datePattern: "YYYY-MM-DD", - maxSize: `${maxSizeInMB}m`, - maxFiles: maxFileCount, - createSymlink: false, - }); + // The transport creates the file's directory itself, so a command's first ever run + // needs no separate mkdir. + const transport = new winston.transports.File({ filename: config.filePath }); const jsonFormat = winston.format.printf((info) => { const { level, message, ...rest } = info; diff --git a/src/logging/index.tsx b/src/logging/index.tsx index 18b2f1bb9..c1ebec3e8 100644 --- a/src/logging/index.tsx +++ b/src/logging/index.tsx @@ -1,3 +1,3 @@ export { type Logger, type LoggerBindings, LOG_LEVEL } from "./types"; export { createFileLogger } from "./fileLogger"; -export { detailedLogLocation, logFilePrefix } from "./location"; +export { detailedLogLocation, logFilePath } from "./location"; diff --git a/src/logging/location.test.ts b/src/logging/location.test.ts index 52a8d4d25..2e89d807c 100644 --- a/src/logging/location.test.ts +++ b/src/logging/location.test.ts @@ -1,10 +1,10 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { createFileLogger } from "./fileLogger"; import { LOG_LEVEL } from "./types"; -import { detailedLogLocation, logFilePrefix, rotatedLogFile } from "./location"; +import { detailedLogLocation, logFilePath } from "./location"; const tempDirectories: string[] = []; @@ -23,134 +23,135 @@ async function project(): Promise { return root; } -// argv as the CLI is handed it: the executable and the script, then the user's tokens. -function argv(...tokens: string[]): string[] { - return ["node", "agentcore", ...tokens]; +async function outsideAnyProject(): Promise { + const directory = await mkdtemp(join(tmpdir(), "agentcore-elsewhere-")); + tempDirectories.push(directory); + return directory; } -describe("rotatedLogFile", () => { - test("names the file the rotating logger writes", async () => { - // The point of the function: it predicts a name winston chooses, so it is - // checked against the file a real logger leaves behind rather than against the - // pattern it was written from. - const directory = await mkdtemp(join(tmpdir(), "agentcore-logs-")); - tempDirectories.push(directory); - // Nested under a command as the real prefix is, which also covers the transport - // creating that directory rather than failing on a path that does not exist yet. - const commandDirectory = join(directory, "deploy"); - - const logger = createFileLogger({ - filePath: join(commandDirectory, "deploy"), - logLevel: LOG_LEVEL.DEBUG, - }); - logger.info("example"); - await logger.end(); - - // The transport also writes a hidden audit file beside the log. - const written = (await readdir(commandDirectory)).filter((name) => name.endsWith(".log")); - expect(written).toEqual([rotatedLogFile("deploy", new Date())]); - }); +const AT = new Date(2026, 7, 17, 13, 45, 6); +const RUN_LOG = "agentcore-20260817-134506.log"; + +describe("logFilePath", () => { + test("names one file per run, in the project the command was run in", async () => { + const root = await project(); - test("pads a single-digit month and day, as the date pattern does", () => { - expect(rotatedLogFile("deploy", new Date(2026, 0, 5))).toBe("deploy-2026-01-05.log"); + expect(logFilePath({ cwd: root, at: AT })).toBe( + join(root, "agentcore", ".cli", "logs", RUN_LOG), + ); }); -}); -describe("logFilePrefix", () => { - test("writes into the project the command was run from, filed under the command", async () => { + test("pads every single-digit part of the timestamp", async () => { const root = await project(); - expect(logFilePrefix({ cwd: root, argv: argv("project", "deploy") })).toBe( - join(root, "agentcore", ".cli", "logs", "deploy", "deploy"), + expect(logFilePath({ cwd: root, at: new Date(2026, 0, 5, 9, 8, 7) })).toBe( + join(root, "agentcore", ".cli", "logs", "agentcore-20260105-090807.log"), ); }); + test("names the same file however long the run takes", async () => { + const root = await project(); + + // The run's log is decided once: a command that prints where its log is has to name + // the file the logger opened, not one named for the moment it printed. + expect(logFilePath({ cwd: root })).toBe(logFilePath({ cwd: root })); + }); + test("finds the project from a directory inside it", async () => { const root = await project(); const inside = join(root, "app", "src"); await mkdir(inside, { recursive: true }); - expect(logFilePrefix({ cwd: inside, argv: argv("project", "deploy") })).toBe( - join(root, "agentcore", ".cli", "logs", "deploy", "deploy"), + expect(logFilePath({ cwd: inside, at: AT })).toBe( + join(root, "agentcore", ".cli", "logs", RUN_LOG), ); }); test("falls back to the home directory outside a project", async () => { - const elsewhere = await mkdtemp(join(tmpdir(), "agentcore-elsewhere-")); - tempDirectories.push(elsewhere); const home = join(tmpdir(), "example-home"); - expect(logFilePrefix({ cwd: elsewhere, home, argv: argv("project", "deploy") })).toBe( - join(home, ".agentcore", "logs", "deploy", "deploy"), + expect(logFilePath({ cwd: await outsideAnyProject(), home, at: AT })).toBe( + join(home, ".agentcore", "logs", RUN_LOG), ); }); - test("names the command whatever surrounds it on the command line", async () => { + test("is decided by where the command ran, not by what was typed", async () => { const root = await project(); - const deploy = join(root, "agentcore", ".cli", "logs", "deploy", "deploy"); - - // A global flag's value is not a command, and neither is a flag after one. - expect(logFilePrefix({ cwd: root, argv: argv("project", "deploy", "--target", "prod") })).toBe( - deploy, - ); - expect( - logFilePrefix({ cwd: root, argv: argv("--region", "us-east-1", "project", "deploy") }), - ).toBe(deploy); - expect(logFilePrefix({ cwd: root, argv: argv("--verbose", "project", "deploy") })).toBe(deploy); + const expected = join(root, "agentcore", ".cli", "logs", RUN_LOG); + const argv = process.argv; + + // Naming the log after the command means parsing argv, and a token taken for a + // command is as likely to be a positional argument: `config endpoint ../../../tmp` + // once put the log wherever that resolved to. Nothing here reads argv at all. + try { + process.argv = ["node", "agentcore", "config", "endpoint", "../../../../../../tmp/pwn"]; + expect(logFilePath({ cwd: root, at: AT })).toBe(expected); + process.argv = ["node", "agentcore", "config", "telemetry.enabled", "false"]; + expect(logFilePath({ cwd: root, at: AT })).toBe(expected); + } finally { + process.argv = argv; + } }); - test("files a run with no command of its own under the CLI itself", async () => { + test("is the file a real logger writes, and one file per run", async () => { + // The point of the function: it names the file winston is handed, so it is checked + // against what a real logger leaves behind — including the logs directory, which no + // run has created yet. const root = await project(); - const bare = join(root, "agentcore", ".cli", "logs", "agentcore", "agentcore"); - - // The TUI, and the TUI with a global flag that could be mistaken for a command. - expect(logFilePrefix({ cwd: root, argv: argv() })).toBe(bare); - expect(logFilePrefix({ cwd: root, argv: argv("--region", "us-east-1") })).toBe(bare); + const first = logFilePath({ cwd: root, at: AT }); + const second = logFilePath({ cwd: root, at: new Date(2026, 7, 17, 13, 45, 7) }); + + for (const [filePath, message] of [ + [first, "first run"], + [second, "second run"], + ] as const) { + const logger = createFileLogger({ filePath, logLevel: LOG_LEVEL.DEBUG }); + logger.info(message); + await logger.end(); + } + + expect((await readdir(join(root, "agentcore", ".cli", "logs"))).sort()).toEqual([ + "agentcore-20260817-134506.log", + "agentcore-20260817-134507.log", + ]); + // A later run writes its own file rather than appending to the run before it. + expect(await readFile(first, "utf8")).toContain("first run"); + expect(await readFile(first, "utf8")).not.toContain("second run"); }); }); describe("detailedLogLocation", () => { - const DEPLOY = argv("project", "deploy"); - test("names a project's log relative to where the command ran", async () => { const root = await project(); - const at = new Date(2026, 7, 17, 13, 45); - expect(detailedLogLocation({ at, cwd: root, argv: DEPLOY })).toBe( - join("agentcore", ".cli", "logs", "deploy", "deploy-2026-08-17.log"), + expect(detailedLogLocation({ at: AT, cwd: root })).toBe( + join("agentcore", ".cli", "logs", RUN_LOG), ); // Run from a subdirectory, the same file is named relative to that. const inside = join(root, "agentcore"); - expect(detailedLogLocation({ at, cwd: inside, argv: DEPLOY })).toBe( - join(".cli", "logs", "deploy", "deploy-2026-08-17.log"), - ); + expect(detailedLogLocation({ at: AT, cwd: inside })).toBe(join(".cli", "logs", RUN_LOG)); // Both spell the file the logger actually writes. - expect(rotatedLogFile(logFilePrefix({ cwd: inside, argv: DEPLOY }), at)).toBe( - join(root, "agentcore", ".cli", "logs", "deploy", "deploy-2026-08-17.log"), + expect(logFilePath({ cwd: inside, at: AT })).toBe( + join(root, "agentcore", ".cli", "logs", RUN_LOG), ); }); test("shortens the fallback log to `~` rather than making it relative", async () => { - const elsewhere = await mkdtemp(join(tmpdir(), "agentcore-elsewhere-")); - tempDirectories.push(elsewhere); + const elsewhere = await outsideAnyProject(); const home = join(tmpdir(), "example-home"); - const at = new Date(2026, 7, 17, 13, 45); - expect(detailedLogLocation({ at, cwd: elsewhere, home, argv: DEPLOY })).toBe( - join("~", ".agentcore", "logs", "deploy", "deploy-2026-08-17.log"), + expect(detailedLogLocation({ at: AT, cwd: elsewhere, home })).toBe( + join("~", ".agentcore", "logs", RUN_LOG), ); - expect(rotatedLogFile(logFilePrefix({ cwd: elsewhere, home, argv: DEPLOY }), at)).toBe( - join(home, ".agentcore", "logs", "deploy", "deploy-2026-08-17.log"), + expect(logFilePath({ cwd: elsewhere, home, at: AT })).toBe( + join(home, ".agentcore", "logs", RUN_LOG), ); }); test("stays absolute for a log that does not sit under the home directory", async () => { - const elsewhere = await mkdtemp(join(tmpdir(), "agentcore-elsewhere-")); - tempDirectories.push(elsewhere); - // Nothing shortens to a bare `~`, so a home of `/` is spelled out instead. - expect( - detailedLogLocation({ at: new Date(2026, 7, 17), cwd: elsewhere, home: "/", argv: DEPLOY }), - ).toBe(join("/", ".agentcore", "logs", "deploy", "deploy-2026-08-17.log")); + expect(detailedLogLocation({ at: AT, cwd: await outsideAnyProject(), home: "/" })).toBe( + join("/", ".agentcore", "logs", RUN_LOG), + ); }); }); diff --git a/src/logging/location.tsx b/src/logging/location.tsx index 6dd8c6263..be974da91 100644 --- a/src/logging/location.tsx +++ b/src/logging/location.tsx @@ -8,53 +8,47 @@ import { enclosingProjectRoot } from "../core/project/fsUtils"; const PROJECT_LOG_DIRECTORY = ["agentcore", ".cli", "logs"]; const HOME_LOG_DIRECTORY = [".agentcore", "logs"]; -// What a run with no command to name it after — a bare `agentcore` — is filed under. +// Every log is named for the CLI rather than for the command that wrote it: the command +// is only knowable by parsing argv, and a guess there puts a file wherever a positional +// argument happens to point. The name still identifies the file once it has been copied +// out of the directory to be attached to a bug report. const CLI_NAME = "agentcore"; +// A run's log is named for when the run started, so one run is one file. Read once, +// rather than per call, so the logger that opens the file and the commands that print +// where it is name the same file however long the run takes. +const RUN_STARTED_AT = new Date(); + type Location = { /** Where the command was run from; decides whether a project owns the log. */ cwd?: string; home?: string; - /** `process.argv`, as the router is handed it; names the command the log belongs to. */ - argv?: string[]; + /** When this run started; names its log file. */ + at?: Date; }; -/** - * The command a log belongs to: `deploy`, for `agentcore project deploy --target prod`. - * - * Only the last segment of the command path is wanted, so this needs no knowledge of any - * flag: the path is the first unbroken run of non-flag tokens, less its first token when - * a flag precedes the run, since that token may be the flag's value. - */ -function commandName(argv: string[]): string { - const tokens = argv.slice(2); - const first = tokens.findIndex((token) => !token.startsWith("-")); - if (first === -1) return CLI_NAME; - const flagged = tokens.findIndex((token, index) => index > first && token.startsWith("-")); - const path = tokens.slice(first === 0 ? 0 : first + 1, flagged === -1 ? undefined : flagged); - return path.at(-1) ?? CLI_NAME; +// YYYYMMDD-HHMMSS, in local time: a run's logs sort chronologically by name, and a +// timestamp a user recognizes is what they match against when reporting a problem. +function runStamp(at: Date): string { + const pad = (value: number) => String(value).padStart(2, "0"); + const date = `${at.getFullYear()}${pad(at.getMonth() + 1)}${pad(at.getDate())}`; + return `${date}-${pad(at.getHours())}${pad(at.getMinutes())}${pad(at.getSeconds())}`; } /** - * The prefix the rotating log file is named from: a directory per command holding that - * command's runs, so reading a deploy's log means reading only deploys. + * The file this run writes its log to: one file per run, so reading a deploy's log means + * reading that deploy and nothing else. */ -export function logFilePrefix({ +export function logFilePath({ cwd = process.cwd(), home = homedir(), - argv = process.argv, + at = RUN_STARTED_AT, }: Location = {}): string { - const command = commandName(argv); + const file = `${CLI_NAME}-${runStamp(at)}.log`; const projectRoot = enclosingProjectRoot(cwd); return projectRoot - ? join(projectRoot, ...PROJECT_LOG_DIRECTORY, command, command) - : join(home, ...HOME_LOG_DIRECTORY, command, command); -} - -export function rotatedLogFile(prefix: string, at: Date): string { - const month = String(at.getMonth() + 1).padStart(2, "0"); - const day = String(at.getDate()).padStart(2, "0"); - return `${prefix}-${at.getFullYear()}-${month}-${day}.log`; + ? join(projectRoot, ...PROJECT_LOG_DIRECTORY, file) + : join(home, ...HOME_LOG_DIRECTORY, file); } /** @@ -62,13 +56,9 @@ export function rotatedLogFile(prefix: string, at: Date): string { * log, and `~`-shortened for the fallback, where relative would be a run of `../` that * breaks on the next cd. */ -export function detailedLogLocation({ - at = new Date(), - cwd = process.cwd(), - home = homedir(), - argv = process.argv, -}: Location & { at?: Date } = {}): string { - const path = rotatedLogFile(logFilePrefix({ cwd, home, argv }), at); +export function detailedLogLocation(location: Location = {}): string { + const { cwd = process.cwd(), home = homedir() } = location; + const path = logFilePath({ ...location, cwd, home }); if (enclosingProjectRoot(cwd)) return relative(cwd, path); return path.startsWith(`${home}${sep}`) ? `~${path.slice(home.length)}` : path; } diff --git a/src/middleware/withLogging.test.ts b/src/middleware/withLogging.test.ts index 94359f865..2832394bb 100644 --- a/src/middleware/withLogging.test.ts +++ b/src/middleware/withLogging.test.ts @@ -16,7 +16,7 @@ describe("withLogging", () => { beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "logging-test-")); logger = createFileLogger({ - filePath: join(tempDir, "output"), + filePath: join(tempDir, "output.log"), logLevel: LOG_LEVEL.DEBUG, }); }); diff --git a/src/telemetry/client.test.tsx b/src/telemetry/client.test.tsx index bad75db7b..42e87be72 100644 --- a/src/telemetry/client.test.tsx +++ b/src/telemetry/client.test.tsx @@ -18,7 +18,7 @@ describe("DefaultTelemetryClient", () => { beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "telemetry-client-test-")); logger = createFileLogger({ - filePath: join(tempDir, "output"), + filePath: join(tempDir, "output.log"), logLevel: LOG_LEVEL.DEBUG, }); }); From 8910f5dd0c9301897456d5eb30a457c1af10cc0f Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 18:16:28 -0400 Subject: [PATCH 10/18] fix(build): name the Windows executable what Bun writes Bun appends .exe when compiling for a Windows target whatever outfile it is given, so the build recorded a path that was never written: reading the executable back to report its size failed with ENOENT on the extensionless name, and the Windows build never got as far as uploading anything. The name now carries the extension, matching the file the build workflow smoke tests. --- scripts/build.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/build.ts b/scripts/build.ts index ca5ef5608..74c662adb 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -119,7 +119,15 @@ async function compile(target: string): Promise { const assets = discoverAssets(); await assertAssetsAreText(assets); - const outfile = join(DIST, "bin", `agentcore-${target.replace(/^bun-/, "")}`); + // Bun appends .exe to a Windows executable whatever it is asked for, so the extension + // is asked for: the name below is also the one this script then reads back and the one + // the build workflow smoke tests. + const platform = target.replace(/^bun-/, ""); + const outfile = join( + DIST, + "bin", + `agentcore-${platform}${platform.startsWith("windows") ? ".exe" : ""}`, + ); await $`mkdir -p ${join(DIST, "bin")}`; const template = bootstrapTemplate(); From d1c1fb217a356a371d8a38f443c292fbf9c2c781 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 18:16:41 -0400 Subject: [PATCH 11/18] feat(project): report the outputs of the stack a deploy created MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The toolkit returns a typed result naming every output of every stack it deployed — the runtime ARN, the gateway URL, the ids a caller needs — and it was thrown away: the toolkit's own narration went to the log file, so a deploy told the user it had deployed and nothing about what it had deployed. The outputs now travel back as the return value of the generator that streams the operation's messages, so progress stays progress and the result stays the result, and reach deploy as an `outputs` event alongside the steps it already reports. deploy prints them under the result, sorted, and prints nothing extra for a stack that declares none. --- src/core/project/backends/cdk.test.ts | 41 +++++++++++++++++++-- src/core/project/backends/cdk.tsx | 27 ++++++++++---- src/core/project/backends/types.ts | 5 +-- src/core/project/manager.test.ts | 5 +-- src/core/project/manager.tsx | 7 ++-- src/handlers/project/deploy/index.ts | 14 ++++++++ src/handlers/project/project.test.ts | 37 ++++++++++++++++++- src/handlers/project/types.ts | 18 +++++++--- src/io/cdk.test.ts | 51 +++++++++++++++++++++++++-- src/io/cdk.ts | 46 +++++++++++++++++------- src/io/index.ts | 1 + src/testing/TestCoreClient.tsx | 6 +++- 12 files changed, 221 insertions(+), 37 deletions(-) diff --git a/src/core/project/backends/cdk.test.ts b/src/core/project/backends/cdk.test.ts index ad7a3a16d..5c6c6f684 100644 --- a/src/core/project/backends/cdk.test.ts +++ b/src/core/project/backends/cdk.test.ts @@ -6,7 +6,7 @@ import { CdkBackend } from "./cdk"; import type { Project, ProjectEvent } from "../../../handlers/project/types"; import { createRecordingLogger, type RecordedLog } from "../../../testing"; import { ProjectSpecSchema } from "../../../projectSchemas/project"; -import type { CdkEvent, CdkOperation, CdkRunOptions } from "../../../io"; +import type { CdkEvent, CdkOperation, CdkOutputs, CdkRunOptions } from "../../../io"; const tempDirectories: string[] = []; @@ -92,10 +92,11 @@ type RecordedCdkRun = { operation: CdkOperation; options: CdkRunOptions }; // A backend whose runner records commands instead of spawning them and whose toolkit // records operations instead of reaching AWS. `onCdk` supplies the events one operation -// emits, and the failure it ends with. +// emits, and the failure it ends with; `outputs` what its deploy produced. function backend( onCdk?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, runner?: () => Promise, + outputs: CdkOutputs = {}, ): { backend: CdkBackend; commands: RecordedCommand[]; @@ -125,6 +126,8 @@ function backend( // failure is only useful if the consumer sees it. yield* events; if (failure) throw failure; + // Bootstrap produces no outputs of the project's own, as with the real runner. + return operation.kind === "deploy" ? outputs : {}; }, checkTool: async () => {}, }), @@ -287,6 +290,40 @@ describe("CdkBackend.deploy", () => { expect(runs).toEqual([]); }); + test("reports the deployed stack's outputs", async () => { + const root = await inTempDirectory(); + const { backend: subject } = backend(undefined, undefined, { + RuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/example", + StackNameOutput: stackName("default"), + }); + const built = await project(root); + await synthesized(root, [TARGET.name]); + + const events = await drain(subject.deploy(built, { target: TARGET, region: REGION })); + + // The endpoints and ARNs the deploy just created: the toolkit is the only thing that + // knows them, so a deploy that keeps them to itself leaves the user nothing to call. + expect(events.at(-1)).toEqual({ + kind: "outputs", + outputs: { + RuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/example", + StackNameOutput: stackName("default"), + }, + }); + }); + + test("reports no outputs for a stack that declares none", async () => { + const root = await inTempDirectory(); + const { backend: subject } = backend(); + const built = await project(root); + await synthesized(root, [TARGET.name]); + + const events = await drain(subject.deploy(built, { target: TARGET, region: REGION })); + + // Nothing above has to render an empty set of outputs, so nothing is told about one. + expect(events.map((event) => event.kind)).toEqual(["step", "step", "step"]); + }); + test("logs the toolkit's messages instead of reporting them as events", async () => { const root = await inTempDirectory(); const { backend: subject, logs } = backend((operation, emit) => { diff --git a/src/core/project/backends/cdk.tsx b/src/core/project/backends/cdk.tsx index 9ed50a265..da8764848 100644 --- a/src/core/project/backends/cdk.tsx +++ b/src/core/project/backends/cdk.tsx @@ -1,6 +1,6 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; -import type { Project, ProjectEvent } from "../../../handlers/project/types"; +import type { Project, ProjectEvent, ProjectStep } from "../../../handlers/project/types"; import type { Logger } from "../../../logging"; import { FsReadWriteJson, @@ -9,6 +9,7 @@ import { runProcess, type CdkEvent, type CdkOperation, + type CdkOutputs, type CdkRunner, type CdkRunOptions, type ProcessRunner, @@ -56,7 +57,7 @@ export class CdkBackend implements ProjectBackend { this.json = config.json ?? new FsReadWriteJson({ logger: config.logger }); } - public async *build(project: Project): AsyncGenerator { + public async *build(project: Project): AsyncGenerator { const cdkDir = this.cdkPath(project); // The generated CDK app is built from its own node_modules; without them the @@ -108,7 +109,12 @@ export class CdkBackend implements ProjectBackend { await this.runCdkOperation({ kind: "bootstrap", environments: [environment] }, run); yield { kind: "step", message: `Deploying ${stackName}` }; - await this.runCdkOperation({ kind: "deploy", stackName }, run); + const outputs = await this.runCdkOperation({ kind: "deploy", stackName }, run); + + // The stack's outputs are the endpoints and ARNs the deploy just created, so they are + // reported rather than left in the log. Withheld when the stack declares none, so + // nothing above has to decide what an empty set of outputs looks like. + if (Object.keys(outputs).length > 0) yield { kind: "outputs", outputs }; } private cdkPath(project: Project): string { @@ -121,11 +127,18 @@ export class CdkBackend implements ProjectBackend { } // The toolkit narrates a deploy in hundreds of lines, so all of it goes to the log at - // the severity the toolkit gave it, and the steps above are what a deploy shows. - private async runCdkOperation(operation: CdkOperation, options: CdkRunOptions): Promise { + // the severity the toolkit gave it, and the steps above are what a deploy shows. What + // the operation produced is its return value, which `for await` would discard, so the + // messages are drained by hand. + private async runCdkOperation( + operation: CdkOperation, + options: CdkRunOptions, + ): Promise { const logger = this.logger.child({ cdk: operation.kind }); - for await (const event of this.cdk(operation, options)) { - logger[CDK_LOG_LEVELS[event.level]](event.message.replace(ANSI_ESCAPES, "")); + const events = this.cdk(operation, options); + for (let next = await events.next(); ; next = await events.next()) { + if (next.done) return next.value; + logger[CDK_LOG_LEVELS[next.value.level]](next.value.message.replace(ANSI_ESCAPES, "")); } } diff --git a/src/core/project/backends/types.ts b/src/core/project/backends/types.ts index ffc82002e..aa8a8a123 100644 --- a/src/core/project/backends/types.ts +++ b/src/core/project/backends/types.ts @@ -1,4 +1,4 @@ -import type { Project, ProjectEvent } from "../../../handlers/project/types"; +import type { Project, ProjectEvent, ProjectStep } from "../../../handlers/project/types"; import type { AwsTarget } from "../../../projectSchemas/aws-targets"; export type BackendDeployInput = { @@ -14,6 +14,7 @@ export type BackendDeployInput = { * project implements this; nothing above it names a tool. */ export interface ProjectBackend { - build(project: Project): AsyncGenerator; + build(project: Project): AsyncGenerator; + /** Reports the deployed stack's outputs, which only the tool that deployed it knows. */ deploy(project: Project, input: BackendDeployInput): AsyncGenerator; } diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index 8dedf952b..019a3cb85 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -9,6 +9,7 @@ import { type CreateProjectInput, type Project, type ProjectEvent, + type ProjectStep, } from "../../handlers/project/types"; import { createSilentLogger } from "../../testing"; import { ProjectSpecSchema } from "../../projectSchemas/project"; @@ -109,9 +110,9 @@ async function drain(generator: AsyncGenerator): Promise { +): Promise<{ events: ProjectStep[]; project: Project }> { const iterator = subject.create(input); - const events: ProjectEvent[] = []; + const events: ProjectStep[] = []; while (true) { const next = await iterator.next(); diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 6db39503a..62b510dcb 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -9,6 +9,7 @@ import type { Project, ProjectManager, ProjectEvent, + ProjectStep, ProjectResource, } from "../../handlers/project/types"; import type { Logger } from "../../logging"; @@ -98,7 +99,7 @@ export class FsProjectManager implements ProjectManager { } } - public async *create(input: CreateProjectInput): AsyncGenerator { + public async *create(input: CreateProjectInput): AsyncGenerator { const enclosing = enclosingProjectRoot(process.cwd()); if (enclosing) { throw new ProjectStateError( @@ -151,7 +152,7 @@ export class FsProjectManager implements ProjectManager { public async *addResource( project: Project, input: AddResourceInput, - ): AsyncGenerator { + ): AsyncGenerator { const { resourceType, resourceConfig } = input; const agentCoreSpecPath = join(project.rootPath, "agentcore", "agentcore.json"); const projectSpecKey = toProjectSpecKey(resourceType); @@ -242,7 +243,7 @@ export class FsProjectManager implements ProjectManager { return outputPath; } - public async *build(project: Project): AsyncGenerator { + public async *build(project: Project): AsyncGenerator { yield* this.backendFor(project).build(project); } diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index b5c6fa252..626feaafc 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -27,14 +27,28 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = // Progress goes to stderr, keeping stdout for machine output. The deployment // tooling's own narration goes to the log, so a deploy shows only its steps. try { + let outputs: Record = {}; for await (const event of config.projectManager.deploy(project, { region: ctx.require(RegionKey), target: flags.target, })) { + if (event.kind === "outputs") { + // Held back until the deploy has succeeded: they belong with the result, not + // in the middle of the steps still running. + outputs = event.outputs; + continue; + } config.io.stderr.write(`${event.message}\n`); } config.io.stderr.write(`Deployed project '${project.name}'\n`); + + // The stack's outputs are what the deploy created — endpoints, ARNs, ids — and + // are the reason to read the end of a deploy. Sorted so a stack's outputs are + // listed the same way every run. + for (const [key, value] of Object.entries(outputs).sort(([a], [b]) => a.localeCompare(b))) { + config.io.stderr.write(` ${key}: ${value}\n`); + } } finally { // Printed however the deploy ended: a failed one is when the detail matters most. config.io.stderr.write(`Detailed logs: ${detailedLogLocation()}\n`); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index e255d2567..f31859eb5 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -30,13 +30,20 @@ type CliOptions = { onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void; /** An already-wired client, for a test that needs one of its doubles to fail. */ core?: TestCoreClient; + /** The outputs the deployed stack reports; see {@link TestCoreClient}. */ + cdkOutputs?: Record; }; // A CLI wired to test doubles, whose route() a test drives directly when it needs // what was written after a command failed. function cli(options?: CliOptions) { const io = testIO(); - const core = options?.core ?? new TestCoreClient({ onCdkOperation: options?.onCdkOperation }); + const core = + options?.core ?? + new TestCoreClient({ + onCdkOperation: options?.onCdkOperation, + cdkOutputs: options?.cdkOutputs, + }); const root = createRootHandler(core, { io: io.io, globalConfigAccessor: new TestGlobalConfigAccessor(), @@ -825,6 +832,34 @@ describe("project deploy", () => { ); }); + test("prints the deployed stack's outputs with the result", async () => { + await inProject(); + const { io } = await run(["deploy"], { + cdkOutputs: { + RuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/MyAgent", + GatewayUrl: "https://example.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp", + }, + }); + + // What the deploy created is the reason to read the end of one: the ARNs and URLs + // the user's next command needs, listed after the line saying it worked. + expect(io.stderr()).toContain( + "Deployed project 'MyAgent'\n" + + " GatewayUrl: https://example.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp\n" + + " RuntimeArn: arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/MyAgent\n", + ); + }); + + test("prints no outputs for a stack that declares none", async () => { + await inProject(); + const { io } = await run(["deploy"]); + + // Nothing between the result and the log location, rather than an empty heading. + expect(io.stderr()).toContain( + `Deployed project 'MyAgent'\nDetailed logs: ${detailedLogLocation()}`, + ); + }); + test("keeps the CDK toolkit's own messages off screen, naming the log instead", async () => { await inProject(); const { io } = await run(["deploy"], { diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 8668c1d0f..d458fd720 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -23,12 +23,20 @@ export type CreateProjectInput = { }; /** Progress worth showing while a long-running project operation runs. */ -export type ProjectEvent = { - /** Discriminated so a later variant can carry fields a step has no use for. */ +export type ProjectStep = { kind: "step"; message: string; }; +/** What a deploy left behind: the deployed stack's outputs, keyed as the stack names them. */ +export type ProjectOutputs = { + kind: "outputs"; + outputs: Record; +}; + +/** Anything a project operation reports as it runs. Only deploy has outputs to report. */ +export type ProjectEvent = ProjectStep | ProjectOutputs; + export type DeployProjectInput = { /** The resolved AWS region the deployment tooling makes its own calls in. */ region: string; @@ -67,10 +75,10 @@ export type ProjectResource = AddResourceInput["resourceType"]; */ export interface ProjectManager { /** Scaffold a new AgentCore project from the given template. */ - create(input: CreateProjectInput): AsyncGenerator; + create(input: CreateProjectInput): AsyncGenerator; /** Build the project's deployable artifacts with whatever backend owns them. */ - build(project: Project): AsyncGenerator; + build(project: Project): AsyncGenerator; /** Build the project, then deploy it to one of its deployment targets. */ deploy(project: Project, input: DeployProjectInput): AsyncGenerator; @@ -79,5 +87,5 @@ export interface ProjectManager { resolve(input: ResolveProjectInput): Promise; /** Add a resource to an existing AgentCore project. */ - addResource(project: Project, input: AddResourceInput): AsyncGenerator; + addResource(project: Project, input: AddResourceInput): AsyncGenerator; } diff --git a/src/io/cdk.test.ts b/src/io/cdk.test.ts index 18b5bf77d..d4af69d62 100644 --- a/src/io/cdk.test.ts +++ b/src/io/cdk.test.ts @@ -14,6 +14,7 @@ import { runCdk, streamCdkOperation, type CdkEvent, + type CdkOutputs, type CdkToolkit, type CdkToolkitLib, } from "./cdk"; @@ -34,7 +35,7 @@ function embedded(text: string): (Blob & { name: string })[] { ]; } -async function collect(generator: AsyncGenerator): Promise { +async function collect(generator: AsyncGenerator): Promise { const events: CdkEvent[] = []; for await (const event of generator) events.push(event); return events; @@ -46,6 +47,7 @@ describe("streamCdkOperation", () => { streamCdkOperation(async (ioHost) => { await ioHost.notify(message("info", "first")); await ioHost.notify(message("warn", "second")); + return {}; }), ); @@ -65,6 +67,7 @@ describe("streamCdkOperation", () => { await new Promise((resolve) => setTimeout(resolve, 1)); released = true; await ioHost.notify(message("info", "created")); + return {}; }); const first = await generator.next(); @@ -99,6 +102,7 @@ describe("streamCdkOperation", () => { answer = await ioHost.requestResponse({ defaultResponse: true, } as unknown as IoRequest); + return {}; }), ); @@ -106,7 +110,22 @@ describe("streamCdkOperation", () => { }); test("reports nothing for an operation that reports nothing", async () => { - expect(await collect(streamCdkOperation(async () => {}))).toEqual([]); + expect(await collect(streamCdkOperation(async () => ({})))).toEqual([]); + }); + + test("returns what the operation produced once its messages are drained", async () => { + const generator = streamCdkOperation(async (ioHost) => { + await ioHost.notify(message("info", "creating")); + return { RuntimeArn: "arn:example" }; + }); + + // The messages are progress; the outputs are the result, so they reach the caller as + // the generator's return value rather than as one more message to be filtered out. + expect(await generator.next()).toEqual({ + done: false, + value: { level: "info", message: "creating" }, + }); + expect(await generator.next()).toEqual({ done: true, value: { RuntimeArn: "arn:example" } }); }); }); @@ -124,6 +143,7 @@ describe("performCdkOperation", () => { }, deploy: async (...args: unknown[]) => { calls.push({ method: "deploy", args }); + return { stacks: [{ outputs: { RuntimeArn: "arn:example" } }] }; }, } as unknown as CdkToolkit; return { toolkit, calls }; @@ -222,6 +242,33 @@ describe("performCdkOperation", () => { }); }); + test("returns the outputs of the stack it deployed", async () => { + const { toolkit } = stubToolkit(); + + const outputs = await performCdkOperation( + { lib, toolkit }, + { kind: "deploy", stackName: "AgentCore-MyAgent-default" }, + { assemblyDirectory: "/tmp/example/cdk.out", region: "us-east-1" }, + ); + + // The toolkit is the only thing that knows them, and a caller that never sees them + // can only tell the user that something was deployed. + expect(outputs).toEqual({ RuntimeArn: "arn:example" }); + }); + + test("returns no outputs for a bootstrap", async () => { + const { toolkit } = stubToolkit(); + + // Bootstrapping deploys the toolkit's own stack, whose outputs are not the user's. + expect( + await performCdkOperation( + { lib, toolkit }, + { kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, + { assemblyDirectory: "/unused", region: "us-east-1" }, + ), + ).toEqual({}); + }); + test("does not bootstrap while deploying", async () => { const { toolkit, calls } = stubToolkit(); diff --git a/src/io/cdk.ts b/src/io/cdk.ts index 64b9da3a0..b1bbd0580 100644 --- a/src/io/cdk.ts +++ b/src/io/cdk.ts @@ -24,11 +24,20 @@ export type CdkRunOptions = { region: string; }; -/** Yields the toolkit's messages as they arrive. Injectable so tests never reach AWS. */ +/** + * A deployed stack's CloudFormation outputs, keyed as the stack names them. Empty for + * every operation that deploys nothing. + */ +export type CdkOutputs = Record; + +/** + * Yields the toolkit's messages as they arrive and returns what the operation produced. + * Injectable so tests never reach AWS. + */ export type CdkRunner = ( operation: CdkOperation, options: CdkRunOptions, -) => AsyncGenerator; +) => AsyncGenerator; /** Narrowed to the methods actually called, so a test can stand in for the toolkit. */ export type CdkToolkit = Pick; @@ -95,15 +104,16 @@ export async function loadCdkToolkit( } /** - * Performs one operation, awaiting the toolkit call it maps to. `files` is taken as an - * argument so a test can exercise the compiled executable's bootstrap path. + * Performs one operation, awaiting the toolkit call it maps to, and returns the outputs + * of whatever it deployed. `files` is taken as an argument so a test can exercise the + * compiled executable's bootstrap path. */ export async function performCdkOperation( { lib, toolkit }: { lib: CdkToolkitLib; toolkit: CdkToolkit }, operation: CdkOperation, options: CdkRunOptions, files: readonly NamedBlob[] = embeddedFiles(), -): Promise { +): Promise { if (operation.kind === "bootstrap") { const template = await loadBootstrapTemplate(files); try { @@ -116,33 +126,41 @@ export async function performCdkOperation( } finally { if (template) await rm(dirname(template), { recursive: true, force: true }); } - return; + // Bootstrapping deploys none of the project's own stacks, so it has no outputs to + // report: what its own stack holds is the toolkit's business, not the user's. + return {}; } const source = await toolkit.fromAssemblyDirectory(options.assemblyDirectory); // The assembly holds one stack per deployment target, and a deploy ships one // target. MUST_MATCH so a name the assembly does not contain fails loudly // instead of quietly deploying nothing. - await toolkit.deploy(source, { + const result = await toolkit.deploy(source, { stacks: { strategy: lib.StackSelectionStrategy.PATTERN_MUST_MATCH, patterns: [operation.stackName], }, }); + + // One stack, by the selection above — merged rather than indexed so a future deploy of + // several stacks reports all of their outputs instead of silently the first one's. + return Object.assign({}, ...result.stacks.map((stack) => stack.outputs)) as CdkOutputs; } /** * Bridges the toolkit's push-based reporting to a pull-based generator: `drive` runs to - * completion while the caller drains the queue its messages land in. A failure - * propagates only once that queue is empty, so the output explaining it comes first. + * completion while the caller drains the queue its messages land in, and what it produced + * is the generator's return value. A failure propagates only once that queue is empty, so + * the output explaining it comes first. */ export async function* streamCdkOperation( - drive: (ioHost: IIoHost) => Promise, -): AsyncGenerator { + drive: (ioHost: IIoHost) => Promise, +): AsyncGenerator { const queue: CdkEvent[] = []; let wake = () => {}; let settled = false; let failure: unknown; + let outputs: CdkOutputs = {}; const ioHost: IIoHost = { notify: async (message) => { @@ -156,6 +174,9 @@ export async function* streamCdkOperation( }; void drive(ioHost) + .then((result) => { + outputs = result; + }) .catch((error: unknown) => { failure = error; }) @@ -174,11 +195,12 @@ export async function* streamCdkOperation( yield queue.shift()!; } if (failure) throw failure; + return outputs; } /** The real runner: loads the toolkit, then streams the operation it performs. */ export const runCdk: CdkRunner = (operation, options) => streamCdkOperation(async (ioHost) => { const loaded = await loadCdkToolkit(ioHost, options.region); - await performCdkOperation(loaded, operation, options); + return performCdkOperation(loaded, operation, options); }); diff --git a/src/io/index.ts b/src/io/index.ts index 1f5c34a11..422a32a1b 100644 --- a/src/io/index.ts +++ b/src/io/index.ts @@ -8,6 +8,7 @@ export { runCdk, type CdkEvent, type CdkOperation, + type CdkOutputs, type CdkRunner, type CdkRunOptions, } from "./cdk"; diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 01393bc7a..ac4a06cb5 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -144,7 +144,7 @@ import type { import { isTerminalStatus } from "../core/batchEvaluationResults"; import { abortable } from "../core/abortable"; import type { CoreOptions } from "../core/types"; -import type { CdkEvent, CdkOperation, CdkRunOptions, ProcessRunner } from "../io"; +import type { CdkEvent, CdkOperation, CdkOutputs, CdkRunOptions, ProcessRunner } from "../io"; import type { ProjectManager } from "../handlers/project/types"; import type { Logger } from "../logging"; import type { ReadWriteJson } from "../io"; @@ -1189,6 +1189,8 @@ type TestCoreClientOptions = { * delivers them as they arrive is covered directly, in `src/io/cdk.test.ts`. */ onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void; + /** The outputs a deploy produces, as the real toolkit reports a deployed stack's. */ + cdkOutputs?: CdkOutputs; json?: ReadWriteJson; }; @@ -1994,6 +1996,8 @@ export class TestCoreClient implements Core { // failure is only useful if the consumer sees it. yield* emitted; if (failure) throw failure; + // Only a deploy produces outputs, as with the real runner. + return operation.kind === "deploy" ? (options?.cdkOutputs ?? {}) : {}; }, }), }, From 85ebc15f0e590cca1c7f8e57272fb32d5171e3c6 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 18:23:16 -0400 Subject: [PATCH 12/18] fix(logging): keep the log where the CLI's own state is The log moved into whichever project the command was run in, so a user chasing a failure had to know which project directory to look in, and the same CLI wrote its logs to as many places as it had projects. It writes to ~/.agentcore/logs again, as it did before, and still prints the path it actually wrote. One file per run is kept: the file is named for the run, not the project. --- src/index.ts | 2 +- src/logging/location.test.ts | 117 ++++++++++++----------------------- src/logging/location.tsx | 36 ++++------- 3 files changed, 50 insertions(+), 105 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8ed6bdbf7..3c2b0cb28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,7 @@ process.exit( const rootLogger = createFileLogger({ // The standard location, shared with the commands that print where it is: one file - // for this run, in the project it was run in or beside the CLI's global state. + // for this run, beside the rest of the CLI's own state. filePath: logFilePath(), logLevel: LOG_LEVEL.DEBUG, bindings: { cliSessionId, version: PACKAGE_VERSION }, diff --git a/src/logging/location.test.ts b/src/logging/location.test.ts index 2e89d807c..cbf11c297 100644 --- a/src/logging/location.test.ts +++ b/src/logging/location.test.ts @@ -14,81 +14,59 @@ afterEach(async () => { ); }); -// A directory the location functions read as a project: the marker they walk up to. +async function inTempDirectory(prefix: string): Promise { + const directory = await mkdtemp(join(tmpdir(), prefix)); + tempDirectories.push(directory); + return directory; +} + +// A directory the CLI reads as a project: the marker it walks up to. The log is written +// outside it regardless, which is what the test below is for. async function project(): Promise { - const root = await mkdtemp(join(tmpdir(), "agentcore-project-")); - tempDirectories.push(root); + const root = await inTempDirectory("agentcore-project-"); await mkdir(join(root, "agentcore"), { recursive: true }); await writeFile(join(root, "agentcore", "agentcore.json"), "{}"); return root; } -async function outsideAnyProject(): Promise { - const directory = await mkdtemp(join(tmpdir(), "agentcore-elsewhere-")); - tempDirectories.push(directory); - return directory; -} - const AT = new Date(2026, 7, 17, 13, 45, 6); const RUN_LOG = "agentcore-20260817-134506.log"; +const HOME = join(tmpdir(), "example-home"); describe("logFilePath", () => { - test("names one file per run, in the project the command was run in", async () => { - const root = await project(); - - expect(logFilePath({ cwd: root, at: AT })).toBe( - join(root, "agentcore", ".cli", "logs", RUN_LOG), - ); + test("names one file per run, under the CLI's own state", () => { + expect(logFilePath({ home: HOME, at: AT })).toBe(join(HOME, ".agentcore", "logs", RUN_LOG)); }); - test("pads every single-digit part of the timestamp", async () => { - const root = await project(); - - expect(logFilePath({ cwd: root, at: new Date(2026, 0, 5, 9, 8, 7) })).toBe( - join(root, "agentcore", ".cli", "logs", "agentcore-20260105-090807.log"), + test("pads every single-digit part of the timestamp", () => { + expect(logFilePath({ home: HOME, at: new Date(2026, 0, 5, 9, 8, 7) })).toBe( + join(HOME, ".agentcore", "logs", "agentcore-20260105-090807.log"), ); }); - test("names the same file however long the run takes", async () => { - const root = await project(); - + test("names the same file however long the run takes", () => { // The run's log is decided once: a command that prints where its log is has to name // the file the logger opened, not one named for the moment it printed. - expect(logFilePath({ cwd: root })).toBe(logFilePath({ cwd: root })); + expect(logFilePath({ home: HOME })).toBe(logFilePath({ home: HOME })); }); - test("finds the project from a directory inside it", async () => { - const root = await project(); - const inside = join(root, "app", "src"); - await mkdir(inside, { recursive: true }); - - expect(logFilePath({ cwd: inside, at: AT })).toBe( - join(root, "agentcore", ".cli", "logs", RUN_LOG), - ); - }); - - test("falls back to the home directory outside a project", async () => { - const home = join(tmpdir(), "example-home"); - - expect(logFilePath({ cwd: await outsideAnyProject(), home, at: AT })).toBe( - join(home, ".agentcore", "logs", RUN_LOG), - ); - }); - - test("is decided by where the command ran, not by what was typed", async () => { - const root = await project(); - const expected = join(root, "agentcore", ".cli", "logs", RUN_LOG); + test("is the same file wherever the command ran and whatever was typed", async () => { + const expected = join(HOME, ".agentcore", "logs", RUN_LOG); + const cwd = process.cwd(); const argv = process.argv; - // Naming the log after the command means parsing argv, and a token taken for a - // command is as likely to be a positional argument: `config endpoint ../../../tmp` - // once put the log wherever that resolved to. Nothing here reads argv at all. + // Neither input a user controls reaches the path. Being inside a project does not move + // the log into it, and nothing is read from argv: a token taken for a command name is + // as likely to be a positional argument, and `config endpoint ../../../tmp/pwn` once + // put the log wherever that resolved to. try { + process.chdir(await project()); process.argv = ["node", "agentcore", "config", "endpoint", "../../../../../../tmp/pwn"]; - expect(logFilePath({ cwd: root, at: AT })).toBe(expected); + expect(logFilePath({ home: HOME, at: AT })).toBe(expected); process.argv = ["node", "agentcore", "config", "telemetry.enabled", "false"]; - expect(logFilePath({ cwd: root, at: AT })).toBe(expected); + expect(logFilePath({ home: HOME, at: AT })).toBe(expected); } finally { + process.chdir(cwd); process.argv = argv; } }); @@ -97,9 +75,9 @@ describe("logFilePath", () => { // The point of the function: it names the file winston is handed, so it is checked // against what a real logger leaves behind — including the logs directory, which no // run has created yet. - const root = await project(); - const first = logFilePath({ cwd: root, at: AT }); - const second = logFilePath({ cwd: root, at: new Date(2026, 7, 17, 13, 45, 7) }); + const home = await inTempDirectory("agentcore-home-"); + const first = logFilePath({ home, at: AT }); + const second = logFilePath({ home, at: new Date(2026, 7, 17, 13, 45, 7) }); for (const [filePath, message] of [ [first, "first run"], @@ -110,7 +88,7 @@ describe("logFilePath", () => { await logger.end(); } - expect((await readdir(join(root, "agentcore", ".cli", "logs"))).sort()).toEqual([ + expect((await readdir(join(home, ".agentcore", "logs"))).sort()).toEqual([ "agentcore-20260817-134506.log", "agentcore-20260817-134507.log", ]); @@ -121,36 +99,17 @@ describe("logFilePath", () => { }); describe("detailedLogLocation", () => { - test("names a project's log relative to where the command ran", async () => { - const root = await project(); - - expect(detailedLogLocation({ at: AT, cwd: root })).toBe( - join("agentcore", ".cli", "logs", RUN_LOG), - ); - // Run from a subdirectory, the same file is named relative to that. - const inside = join(root, "agentcore"); - expect(detailedLogLocation({ at: AT, cwd: inside })).toBe(join(".cli", "logs", RUN_LOG)); - // Both spell the file the logger actually writes. - expect(logFilePath({ cwd: inside, at: AT })).toBe( - join(root, "agentcore", ".cli", "logs", RUN_LOG), - ); - }); - - test("shortens the fallback log to `~` rather than making it relative", async () => { - const elsewhere = await outsideAnyProject(); - const home = join(tmpdir(), "example-home"); - - expect(detailedLogLocation({ at: AT, cwd: elsewhere, home })).toBe( + test("shortens the home directory it sits under to `~`", () => { + expect(detailedLogLocation({ home: HOME, at: AT })).toBe( join("~", ".agentcore", "logs", RUN_LOG), ); - expect(logFilePath({ cwd: elsewhere, home, at: AT })).toBe( - join(home, ".agentcore", "logs", RUN_LOG), - ); + // Spells the file the logger actually writes, just more briefly. + expect(logFilePath({ home: HOME, at: AT })).toBe(join(HOME, ".agentcore", "logs", RUN_LOG)); }); - test("stays absolute for a log that does not sit under the home directory", async () => { + test("stays absolute for a log that does not sit under the home directory", () => { // Nothing shortens to a bare `~`, so a home of `/` is spelled out instead. - expect(detailedLogLocation({ at: AT, cwd: await outsideAnyProject(), home: "/" })).toBe( + expect(detailedLogLocation({ home: "/", at: AT })).toBe( join("/", ".agentcore", "logs", RUN_LOG), ); }); diff --git a/src/logging/location.tsx b/src/logging/location.tsx index be974da91..1ba1029ff 100644 --- a/src/logging/location.tsx +++ b/src/logging/location.tsx @@ -1,12 +1,10 @@ import { homedir } from "node:os"; -import { join, relative, sep } from "node:path"; -import { enclosingProjectRoot } from "../core/project/fsUtils"; +import { join, sep } from "node:path"; -// A project's log joins the rest of its CLI state under agentcore/.cli, which the -// scaffolded .gitignore excludes. Outside a project there is nothing to write into, so -// the log falls back beside the CLI's global state. -const PROJECT_LOG_DIRECTORY = ["agentcore", ".cli", "logs"]; -const HOME_LOG_DIRECTORY = [".agentcore", "logs"]; +// The CLI's own state rather than a project's: every run writes here whichever project — +// or none — it was run in, so there is one directory to look in and one to attach a log +// from, and nothing about where a command ran can move the file somewhere else. +const LOG_DIRECTORY = [".agentcore", "logs"]; // Every log is named for the CLI rather than for the command that wrote it: the command // is only knowable by parsing argv, and a guess there puts a file wherever a positional @@ -20,8 +18,6 @@ const CLI_NAME = "agentcore"; const RUN_STARTED_AT = new Date(); type Location = { - /** Where the command was run from; decides whether a project owns the log. */ - cwd?: string; home?: string; /** When this run started; names its log file. */ at?: Date; @@ -39,26 +35,16 @@ function runStamp(at: Date): string { * The file this run writes its log to: one file per run, so reading a deploy's log means * reading that deploy and nothing else. */ -export function logFilePath({ - cwd = process.cwd(), - home = homedir(), - at = RUN_STARTED_AT, -}: Location = {}): string { - const file = `${CLI_NAME}-${runStamp(at)}.log`; - const projectRoot = enclosingProjectRoot(cwd); - return projectRoot - ? join(projectRoot, ...PROJECT_LOG_DIRECTORY, file) - : join(home, ...HOME_LOG_DIRECTORY, file); +export function logFilePath({ home = homedir(), at = RUN_STARTED_AT }: Location = {}): string { + return join(home, ...LOG_DIRECTORY, `${CLI_NAME}-${runStamp(at)}.log`); } /** - * The log file, spelled the shortest way that stays correct: relative for a project's - * log, and `~`-shortened for the fallback, where relative would be a run of `../` that - * breaks on the next cd. + * The log file as a command prints it: `~`-shortened, where a relative path would be a + * run of `../` that stops being correct after the next cd. */ export function detailedLogLocation(location: Location = {}): string { - const { cwd = process.cwd(), home = homedir() } = location; - const path = logFilePath({ ...location, cwd, home }); - if (enclosingProjectRoot(cwd)) return relative(cwd, path); + const { home = homedir() } = location; + const path = logFilePath({ ...location, home }); return path.startsWith(`${home}${sep}`) ? `~${path.slice(home.length)}` : path; } From 541299086c4c527c38cfd26e23d0af9d66e1d7d9 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 23:18:38 -0400 Subject: [PATCH 13/18] fix(logging): bound the log directory now that runs no longer share a file One file per run bounds a run's log and nothing else: the rotating file it replaced kept ten files of five megabytes, and dropping it left a directory that only ever grew, a file per invocation, forever. Each run now deletes the logs it has made redundant before writing its own, keeping the fifty most recent runs and at most fifty megabytes of them -- the ceiling the rotating file enforced. Only the two names the CLI writes are considered, so the daily files an earlier version left behind are cleared too and anything else in the directory is left where it was put. A run never deletes the file it is still writing, and housekeeping that fails is housekeeping that did nothing rather than a command that failed. --- src/index.ts | 13 ++-- src/logging/fileLogger.tsx | 4 +- src/logging/index.tsx | 1 + src/logging/retention.test.ts | 128 ++++++++++++++++++++++++++++++++++ src/logging/retention.tsx | 74 ++++++++++++++++++++ 5 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 src/logging/retention.test.ts create mode 100644 src/logging/retention.tsx diff --git a/src/index.ts b/src/index.ts index 3c2b0cb28..7837d5f92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { } from "./core/factories"; import { createRootHandler } from "./handlers"; import { FsReadWriteJson } from "./io"; -import { createFileLogger, logFilePath, LOG_LEVEL } from "./logging"; +import { createFileLogger, logFilePath, LOG_LEVEL, pruneOldLogs } from "./logging"; import { runWithExitCode } from "./runnable"; import { DefaultGlobalConfigAccessor } from "./globalConfig"; import { DefaultTelemetryClient } from "./telemetry"; @@ -30,13 +30,17 @@ process.exit( // generate a unique identifier corresponding to this process of this CLI. (ex. one command invoke, one TUI session) const cliSessionId = crypto.randomUUID(); + // The standard location, shared with the commands that print where it is: one file + // for this run, beside the rest of the CLI's own state. + const logFile = logFilePath(); const rootLogger = createFileLogger({ - // The standard location, shared with the commands that print where it is: one file - // for this run, beside the rest of the CLI's own state. - filePath: logFilePath(), + filePath: logFile, logLevel: LOG_LEVEL.DEBUG, bindings: { cliSessionId, version: PACKAGE_VERSION }, }); + // One file per run only bounds a run's log, not the directory of them, so each run + // clears the ones it has made redundant before writing its own. + const prunedLogs = await pruneOldLogs(logFile); const io = { stdin: process.stdin, @@ -64,6 +68,7 @@ process.exit( try { rootLogger.info(`running CLI`); + if (prunedLogs > 0) rootLogger.debug(`pruned ${prunedLogs} log(s) of earlier runs`); // factories (rather than instances) lets CoreClient build one client per // region on demand. diff --git a/src/logging/fileLogger.tsx b/src/logging/fileLogger.tsx index 1689f2948..e645bda73 100644 --- a/src/logging/fileLogger.tsx +++ b/src/logging/fileLogger.tsx @@ -39,7 +39,9 @@ function wrapWinstonLogger( * Creates a logger that writes structured JSON to one file per run. * * A run's log is a whole run and only that run, so nothing rotates and nothing is - * appended to by a later run; {@link logFilePath} names the file for the run. + * appended to by a later run; {@link logFilePath} names the file for the run. Bounding + * the directory of them is {@link pruneOldLogs}, since it is the run that knows which + * file is its own and must be kept. * * @param config - Logger configuration (file path, level, bindings). * @returns A {@link AsyncLogger} that writes to `config.filePath` via winston. diff --git a/src/logging/index.tsx b/src/logging/index.tsx index c1ebec3e8..9e9cbc860 100644 --- a/src/logging/index.tsx +++ b/src/logging/index.tsx @@ -1,3 +1,4 @@ export { type Logger, type LoggerBindings, LOG_LEVEL } from "./types"; export { createFileLogger } from "./fileLogger"; export { detailedLogLocation, logFilePath } from "./location"; +export { pruneOldLogs, RETENTION } from "./retention"; diff --git a/src/logging/retention.test.ts b/src/logging/retention.test.ts new file mode 100644 index 000000000..c9d819bbe --- /dev/null +++ b/src/logging/retention.test.ts @@ -0,0 +1,128 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { pruneOldLogs, RETENTION } from "./retention"; + +const tempDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function logDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), "agentcore-logs-")); + tempDirectories.push(directory); + return directory; +} + +// A log of `bytes` bytes, so a test can put the directory over the byte ceiling. +async function log(directory: string, name: string, bytes = 1): Promise { + const path = join(directory, name); + await writeFile(path, "x".repeat(bytes)); + return path; +} + +function runLog(second: number): string { + return `agentcore-20260817-1345${String(second).padStart(2, "0")}.log`; +} + +async function remaining(directory: string): Promise { + return (await readdir(directory)).sort(); +} + +const NO_BYTE_LIMIT = { runs: RETENTION.runs, bytes: Number.MAX_SAFE_INTEGER }; + +describe("pruneOldLogs", () => { + test("keeps the newest runs and deletes the runs before them", async () => { + const directory = await logDirectory(); + for (let second = 1; second <= 6; second += 1) await log(directory, runLog(second)); + const current = join(directory, runLog(6)); + + expect(await pruneOldLogs(current, { runs: 3, bytes: Number.MAX_SAFE_INTEGER })).toBe(3); + + // The three newest, which for the run doing the pruning includes its own. + expect(await remaining(directory)).toEqual([runLog(4), runLog(5), runLog(6)]); + }); + + test("never deletes the log this run is writing", async () => { + const directory = await logDirectory(); + // The oldest file is this run's, which happens when the clock moved backwards: even + // then a run cannot delete the file it still has open. + const current = await log(directory, runLog(1)); + for (const second of [2, 3, 4]) await log(directory, runLog(second)); + + await pruneOldLogs(current, { runs: 1, bytes: Number.MAX_SAFE_INTEGER }); + + expect(await remaining(directory)).toEqual([runLog(1), runLog(4)]); + }); + + test("deletes the oldest runs until what is left fits the byte ceiling", async () => { + const directory = await logDirectory(); + // A deploy's log is the large one, and a few of them reach the ceiling long before + // the run count does. + for (const second of [1, 2, 3]) await log(directory, runLog(second), 400); + const current = await log(directory, runLog(4), 400); + + expect(await pruneOldLogs(current, { runs: 50, bytes: 1000 })).toBe(2); + + expect(await remaining(directory)).toEqual([runLog(3), runLog(4)]); + }); + + test("keeps this run's log even when it alone exceeds the ceiling", async () => { + const directory = await logDirectory(); + await log(directory, runLog(1), 10); + const current = await log(directory, runLog(2), 5000); + + await pruneOldLogs(current, { runs: 50, bytes: 1000 }); + + expect(await remaining(directory)).toEqual([runLog(2)]); + }); + + test("deletes the daily logs an earlier version left behind", async () => { + const directory = await logDirectory(); + // What the rotating file wrote before one file per run replaced it. Nothing else would + // ever delete these, so an upgrade does not leave a directory that only grows. + await log(directory, "output-2026-08-10.log"); + await log(directory, "output-2026-08-17.log"); + const current = await log(directory, runLog(1)); + + expect(await pruneOldLogs(current, { runs: 2, bytes: Number.MAX_SAFE_INTEGER })).toBe(1); + + // The older daily file goes first: a date sorts before any run of that date. + expect(await remaining(directory)).toEqual([runLog(1), "output-2026-08-17.log"]); + }); + + test("leaves files it did not write", async () => { + const directory = await logDirectory(); + const current = await log(directory, runLog(2)); + await log(directory, runLog(1)); + await log(directory, "notes.txt"); + await log(directory, "deploy-20260817-134506.log"); + + expect(await pruneOldLogs(current, { runs: 1, bytes: Number.MAX_SAFE_INTEGER })).toBe(1); + + // Only the CLI's own logs are pruned, so a file a user copied in stays where they put it. + expect(await remaining(directory)).toEqual([ + runLog(2), + "deploy-20260817-134506.log", + "notes.txt", + ]); + }); + + test("prunes nothing when this run has yet to write anything", async () => { + // The first run on a machine: the directory the transport is about to create. + const directory = join(await logDirectory(), "not-created-yet"); + + expect(await pruneOldLogs(join(directory, runLog(1)), NO_BYTE_LIMIT)).toBe(0); + }); + + test("bounds the directory by default rather than letting it grow", async () => { + // The ceiling the rotating file enforced before one file per run replaced it, so the + // default is a bound and not merely a large number. + expect(RETENTION.bytes).toBe(50 * 1024 * 1024); + expect(RETENTION.runs).toBeGreaterThan(0); + }); +}); diff --git a/src/logging/retention.tsx b/src/logging/retention.tsx new file mode 100644 index 000000000..5bea3d5df --- /dev/null +++ b/src/logging/retention.tsx @@ -0,0 +1,74 @@ +import { readdir, rm, stat } from "node:fs/promises"; +import { dirname, join } from "node:path"; + +// The names the CLI writes its logs under: one file per run, and the daily file earlier +// versions rotated. Only these are pruned, so anything else a user keeps in the log +// directory is left where they put it. +const RUN_LOG = /^agentcore-\d{8}-\d{6}\.log$/; +const ROTATED_LOG = /^output-\d{4}-\d{2}-\d{2}\.log$/; + +/** + * How much history is kept. The byte ceiling is what the rotating file enforced before + * one file per run replaced it (5 MB × 10); the count is generous against it because a + * run's log is usually a few kilobytes, and one deploy's worth of history is worth more + * than ten runs. + */ +export const RETENTION = { runs: 50, bytes: 50 * 1024 * 1024 }; + +/** + * Deletes the logs of the oldest runs until what is left fits {@link RETENTION}, and + * returns how many were deleted. + * + * The log of the run doing the pruning is always kept, however far over the ceiling it + * takes the directory: a run cannot delete the file it is still writing to. That also + * makes the byte ceiling approximate, since that file keeps growing after it is measured. + * + * Housekeeping, so nothing here is allowed to fail a command: a directory that cannot be + * read prunes nothing, and a file that another run deleted first is not an error. + */ +export async function pruneOldLogs(currentLogFile: string, limits = RETENTION): Promise { + const directory = dirname(currentLogFile); + + let names: string[]; + try { + names = (await readdir(directory)).filter( + (name) => RUN_LOG.test(name) || ROTATED_LOG.test(name), + ); + } catch { + // No log has been written yet, so there is nothing to prune. + return 0; + } + + // Both names carry a zero-padded timestamp, so their digits alone sort by age — by name + // would rank every `output-` file above every `agentcore-` one. A daily file's digits + // stop at its date, which makes it older than any run of that date, as it is. + const age = (name: string) => name.replace(/\D/g, ""); + const newestFirst = names.sort((a, b) => age(b).localeCompare(age(a))); + + const stale: string[] = []; + let kept = 0; + let bytes = 0; + for (const name of newestFirst) { + const path = join(directory, name); + const size = await stat(path) + .then((stats) => stats.size) + .catch(() => 0); + + if (path !== currentLogFile && (kept >= limits.runs || bytes + size > limits.bytes)) { + stale.push(path); + continue; + } + kept += 1; + bytes += size; + } + + const deleted = await Promise.all( + stale.map((path) => + rm(path, { force: true }).then( + () => true, + () => false, + ), + ), + ); + return deleted.filter(Boolean).length; +} From 9bdfeb6cb5f396d9f3e6ba05acac1671c5b373cf Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 23:35:39 -0400 Subject: [PATCH 14/18] revert(logging): keep the rotating log every command has written to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One file per run, pruned at startup, replaced a transport that rotated daily and kept 5 MB × 10. Two mechanisms for bounding the same directory is one more than it needs, and the rotating one enforces a per-file size limit at write time rather than a directory-wide one a run later. So the transport, its dependency and `~/.agentcore/logs/output-.log` are back as they were, and the run-scoped naming and the pruning that existed only to bound it are gone. What stays is `location.tsx` naming the file, since a command that prints where its log is has to name the file the transport opened: the prefix the transport is handed is not a path a user can open, and spelling the file in two places is how the printed path and the real one drift. --- bun.lock | 9 +++ package.json | 1 + src/index.ts | 13 ++-- src/logging/fileLogger.tsx | 37 +++++----- src/logging/index.tsx | 3 +- src/logging/location.test.ts | 69 +++++++++--------- src/logging/location.tsx | 44 ++++++------ src/logging/retention.test.ts | 128 ---------------------------------- src/logging/retention.tsx | 74 -------------------- 9 files changed, 92 insertions(+), 286 deletions(-) delete mode 100644 src/logging/retention.test.ts delete mode 100644 src/logging/retention.tsx diff --git a/bun.lock b/bun.lock index 165714d1b..1a9f6297d 100644 --- a/bun.lock +++ b/bun.lock @@ -27,6 +27,7 @@ "react-router": "^8.3.0", "string-width": "^8.2.2", "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0", "zod": "^4.4.3", }, "devDependencies": { @@ -346,6 +347,8 @@ "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + "file-stream-rotator": ["file-stream-rotator@0.6.1", "", { "dependencies": { "moment": "^2.29.1" } }, "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], @@ -422,10 +425,14 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], @@ -530,6 +537,8 @@ "winston": ["winston@3.19.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="], + "winston-daily-rotate-file": ["winston-daily-rotate-file@5.0.0", "", { "dependencies": { "file-stream-rotator": "^0.6.1", "object-hash": "^3.0.0", "triple-beam": "^1.4.1", "winston-transport": "^4.7.0" }, "peerDependencies": { "winston": "^3" } }, "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw=="], + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], diff --git a/package.json b/package.json index 3aab2d1a9..a94afba28 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "react-router": "^8.3.0", "string-width": "^8.2.2", "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0", "zod": "^4.4.3" } } diff --git a/src/index.ts b/src/index.ts index 7837d5f92..0c33f4f56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { } from "./core/factories"; import { createRootHandler } from "./handlers"; import { FsReadWriteJson } from "./io"; -import { createFileLogger, logFilePath, LOG_LEVEL, pruneOldLogs } from "./logging"; +import { createFileLogger, logFilePrefix, LOG_LEVEL } from "./logging"; import { runWithExitCode } from "./runnable"; import { DefaultGlobalConfigAccessor } from "./globalConfig"; import { DefaultTelemetryClient } from "./telemetry"; @@ -30,17 +30,13 @@ process.exit( // generate a unique identifier corresponding to this process of this CLI. (ex. one command invoke, one TUI session) const cliSessionId = crypto.randomUUID(); - // The standard location, shared with the commands that print where it is: one file - // for this run, beside the rest of the CLI's own state. - const logFile = logFilePath(); + // The rotating log every command writes to, shared with the commands that print where + // it is so the path shown is the file the transport opened. const rootLogger = createFileLogger({ - filePath: logFile, + filePath: logFilePrefix(), logLevel: LOG_LEVEL.DEBUG, bindings: { cliSessionId, version: PACKAGE_VERSION }, }); - // One file per run only bounds a run's log, not the directory of them, so each run - // clears the ones it has made redundant before writing its own. - const prunedLogs = await pruneOldLogs(logFile); const io = { stdin: process.stdin, @@ -68,7 +64,6 @@ process.exit( try { rootLogger.info(`running CLI`); - if (prunedLogs > 0) rootLogger.debug(`pruned ${prunedLogs} log(s) of earlier runs`); // factories (rather than instances) lets CoreClient build one client per // region on demand. diff --git a/src/logging/fileLogger.tsx b/src/logging/fileLogger.tsx index e645bda73..1b37dbf6b 100644 --- a/src/logging/fileLogger.tsx +++ b/src/logging/fileLogger.tsx @@ -1,16 +1,18 @@ import winston from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; import { type AsyncLogger, type LoggerBindings, type LogLevel } from "./types"; export interface FileLoggerConfig { - /** The exact file to write. Its directory is created if it does not exist yet. */ filePath: string; + maxSizeInMB?: number; + maxFileCount?: number; bindings?: LoggerBindings; logLevel: LogLevel; } function wrapWinstonLogger( winstonLogger: winston.Logger, - transport: winston.transports.FileTransportInstance, + transport: DailyRotateFile, bindings: LoggerBindings, ): AsyncLogger { const log = @@ -27,31 +29,32 @@ function wrapWinstonLogger( wrapWinstonLogger(winstonLogger, transport, { ...bindings, ...childBindings }), end: () => new Promise((resolve) => { - // Ending the transport flushes what it has buffered and closes the file stream; - // `finish` is what it emits once that has happened. - transport.once("finish", resolve); - transport.end(); + transport.on("finish", resolve); + // note: we prefer close over end since close calls end on the stream internally: https://github.com/winstonjs/winston-daily-rotate-file/blob/a1a4668cfea77476cd6a4a11f038c2aac9d10741/daily-rotate-file.js#L201-L207 + if (transport.close) transport.close(); }), }; } /** - * Creates a logger that writes structured JSON to one file per run. + * Creates a logger that writes structured JSON to a rotating file. * - * A run's log is a whole run and only that run, so nothing rotates and nothing is - * appended to by a later run; {@link logFilePath} names the file for the run. Bounding - * the directory of them is {@link pruneOldLogs}, since it is the run that knows which - * file is its own and must be kept. - * - * @param config - Logger configuration (file path, level, bindings). - * @returns A {@link AsyncLogger} that writes to `config.filePath` via winston. + * @param config - Logger configuration (file path, rotation limits, level). + * @returns A {@link AsyncLogger} that writes to a rotating file via winston. */ export function createFileLogger(config: FileLoggerConfig): AsyncLogger { + const maxSizeInMB = config.maxSizeInMB ?? 5; + const maxFileCount = config.maxFileCount ?? 10; const bindings = config.bindings ?? {}; - // The transport creates the file's directory itself, so a command's first ever run - // needs no separate mkdir. - const transport = new winston.transports.File({ filename: config.filePath }); + const transport = new DailyRotateFile({ + filename: `${config.filePath}-%DATE%`, + extension: ".log", + datePattern: "YYYY-MM-DD", + maxSize: `${maxSizeInMB}m`, + maxFiles: maxFileCount, + createSymlink: false, + }); const jsonFormat = winston.format.printf((info) => { const { level, message, ...rest } = info; diff --git a/src/logging/index.tsx b/src/logging/index.tsx index 9e9cbc860..18b2f1bb9 100644 --- a/src/logging/index.tsx +++ b/src/logging/index.tsx @@ -1,4 +1,3 @@ export { type Logger, type LoggerBindings, LOG_LEVEL } from "./types"; export { createFileLogger } from "./fileLogger"; -export { detailedLogLocation, logFilePath } from "./location"; -export { pruneOldLogs, RETENTION } from "./retention"; +export { detailedLogLocation, logFilePrefix } from "./location"; diff --git a/src/logging/location.test.ts b/src/logging/location.test.ts index cbf11c297..6ae98f2e5 100644 --- a/src/logging/location.test.ts +++ b/src/logging/location.test.ts @@ -1,10 +1,10 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { createFileLogger } from "./fileLogger"; import { LOG_LEVEL } from "./types"; -import { detailedLogLocation, logFilePath } from "./location"; +import { detailedLogLocation, logFilePath, logFilePrefix } from "./location"; const tempDirectories: string[] = []; @@ -30,28 +30,28 @@ async function project(): Promise { } const AT = new Date(2026, 7, 17, 13, 45, 6); -const RUN_LOG = "agentcore-20260817-134506.log"; +const DAY_LOG = "output-2026-08-17.log"; const HOME = join(tmpdir(), "example-home"); describe("logFilePath", () => { - test("names one file per run, under the CLI's own state", () => { - expect(logFilePath({ home: HOME, at: AT })).toBe(join(HOME, ".agentcore", "logs", RUN_LOG)); + test("names the day's file, under the CLI's own state", () => { + expect(logFilePath({ home: HOME, at: AT })).toBe(join(HOME, ".agentcore", "logs", DAY_LOG)); }); - test("pads every single-digit part of the timestamp", () => { + test("pads a single-digit month and day", () => { expect(logFilePath({ home: HOME, at: new Date(2026, 0, 5, 9, 8, 7) })).toBe( - join(HOME, ".agentcore", "logs", "agentcore-20260105-090807.log"), + join(HOME, ".agentcore", "logs", "output-2026-01-05.log"), ); }); test("names the same file however long the run takes", () => { - // The run's log is decided once: a command that prints where its log is has to name + // The run's day is decided once: a command that prints where its log is has to name // the file the logger opened, not one named for the moment it printed. expect(logFilePath({ home: HOME })).toBe(logFilePath({ home: HOME })); }); test("is the same file wherever the command ran and whatever was typed", async () => { - const expected = join(HOME, ".agentcore", "logs", RUN_LOG); + const expected = join(HOME, ".agentcore", "logs", DAY_LOG); const cwd = process.cwd(); const argv = process.argv; @@ -71,46 +71,47 @@ describe("logFilePath", () => { } }); - test("is the file a real logger writes, and one file per run", async () => { - // The point of the function: it names the file winston is handed, so it is checked - // against what a real logger leaves behind — including the logs directory, which no - // run has created yet. + test("is the file a real logger writes", async () => { + // The point of the function: it spells the file the rotating transport produces from + // the prefix it is handed, so it is checked against what a real logger leaves behind — + // including the logs directory, which no run has created yet. const home = await inTempDirectory("agentcore-home-"); - const first = logFilePath({ home, at: AT }); - const second = logFilePath({ home, at: new Date(2026, 7, 17, 13, 45, 7) }); - - for (const [filePath, message] of [ - [first, "first run"], - [second, "second run"], - ] as const) { - const logger = createFileLogger({ filePath, logLevel: LOG_LEVEL.DEBUG }); - logger.info(message); - await logger.end(); - } - expect((await readdir(join(home, ".agentcore", "logs"))).sort()).toEqual([ - "agentcore-20260817-134506.log", - "agentcore-20260817-134507.log", - ]); - // A later run writes its own file rather than appending to the run before it. - expect(await readFile(first, "utf8")).toContain("first run"); - expect(await readFile(first, "utf8")).not.toContain("second run"); + const logger = createFileLogger({ + filePath: logFilePrefix({ home }), + logLevel: LOG_LEVEL.DEBUG, + }); + logger.info("a run"); + await logger.end(); + + expect(await readFile(logFilePath({ home }), "utf8")).toContain("a run"); + }); +}); + +describe("logFilePrefix", () => { + test("is what the rotating transport is handed rather than a file", () => { + // The transport appends the date and the extension, so the prefix is the same for + // every day and the file is not. + expect(logFilePrefix({ home: HOME })).toBe(join(HOME, ".agentcore", "logs", "output")); + expect(logFilePath({ home: HOME, at: AT }).startsWith(logFilePrefix({ home: HOME }))).toBe( + true, + ); }); }); describe("detailedLogLocation", () => { test("shortens the home directory it sits under to `~`", () => { expect(detailedLogLocation({ home: HOME, at: AT })).toBe( - join("~", ".agentcore", "logs", RUN_LOG), + join("~", ".agentcore", "logs", DAY_LOG), ); // Spells the file the logger actually writes, just more briefly. - expect(logFilePath({ home: HOME, at: AT })).toBe(join(HOME, ".agentcore", "logs", RUN_LOG)); + expect(logFilePath({ home: HOME, at: AT })).toBe(join(HOME, ".agentcore", "logs", DAY_LOG)); }); test("stays absolute for a log that does not sit under the home directory", () => { // Nothing shortens to a bare `~`, so a home of `/` is spelled out instead. expect(detailedLogLocation({ home: "/", at: AT })).toBe( - join("/", ".agentcore", "logs", RUN_LOG), + join("/", ".agentcore", "logs", DAY_LOG), ); }); }); diff --git a/src/logging/location.tsx b/src/logging/location.tsx index 1ba1029ff..3079ff819 100644 --- a/src/logging/location.tsx +++ b/src/logging/location.tsx @@ -2,46 +2,46 @@ import { homedir } from "node:os"; import { join, sep } from "node:path"; // The CLI's own state rather than a project's: every run writes here whichever project — -// or none — it was run in, so there is one directory to look in and one to attach a log -// from, and nothing about where a command ran can move the file somewhere else. +// or none — it was run in, so there is one directory to look in and one log to attach to a +// bug report, and nothing about where a command ran can move the file somewhere else. const LOG_DIRECTORY = [".agentcore", "logs"]; -// Every log is named for the CLI rather than for the command that wrote it: the command -// is only knowable by parsing argv, and a guess there puts a file wherever a positional -// argument happens to point. The name still identifies the file once it has been copied -// out of the directory to be attached to a bug report. -const CLI_NAME = "agentcore"; +// What the rotating transport is handed. It appends the date and the extension itself, so +// the prefix is not a file: `logFilePath` spells the file that prefix produces. +const LOG_PREFIX = "output"; -// A run's log is named for when the run started, so one run is one file. Read once, -// rather than per call, so the logger that opens the file and the commands that print -// where it is name the same file however long the run takes. +// The day the rotating transport is writing under, read once at startup so a command that +// prints where its log is names the file the logger opened, even across midnight. const RUN_STARTED_AT = new Date(); type Location = { home?: string; - /** When this run started; names its log file. */ + /** The day whose file the rotating transport is writing. */ at?: Date; }; -// YYYYMMDD-HHMMSS, in local time: a run's logs sort chronologically by name, and a -// timestamp a user recognizes is what they match against when reporting a problem. -function runStamp(at: Date): string { +// `datePattern: "YYYY-MM-DD"` in local time, which is what the transport defaults to. +function day(at: Date): string { const pad = (value: number) => String(value).padStart(2, "0"); - const date = `${at.getFullYear()}${pad(at.getMonth() + 1)}${pad(at.getDate())}`; - return `${date}-${pad(at.getHours())}${pad(at.getMinutes())}${pad(at.getSeconds())}`; + return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`; } /** - * The file this run writes its log to: one file per run, so reading a deploy's log means - * reading that deploy and nothing else. + * The prefix the rotating file transport names its files after: it writes + * `-.log`, and a further `.` when a day's log passes the size limit. */ -export function logFilePath({ home = homedir(), at = RUN_STARTED_AT }: Location = {}): string { - return join(home, ...LOG_DIRECTORY, `${CLI_NAME}-${runStamp(at)}.log`); +export function logFilePrefix({ home = homedir() }: Location = {}): string { + return join(home, ...LOG_DIRECTORY, LOG_PREFIX); +} + +/** The file the rotating transport is writing for this run's day. */ +export function logFilePath({ home, at = RUN_STARTED_AT }: Location = {}): string { + return `${logFilePrefix({ home })}-${day(at)}.log`; } /** - * The log file as a command prints it: `~`-shortened, where a relative path would be a - * run of `../` that stops being correct after the next cd. + * Where to look for the detail a command left out of its own output, for printing: the + * log file itself, with the home directory it sits under shortened to `~`. */ export function detailedLogLocation(location: Location = {}): string { const { home = homedir() } = location; diff --git a/src/logging/retention.test.ts b/src/logging/retention.test.ts deleted file mode 100644 index c9d819bbe..000000000 --- a/src/logging/retention.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test"; -import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { tmpdir } from "node:os"; -import { pruneOldLogs, RETENTION } from "./retention"; - -const tempDirectories: string[] = []; - -afterEach(async () => { - await Promise.all( - tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), - ); -}); - -async function logDirectory(): Promise { - const directory = await mkdtemp(join(tmpdir(), "agentcore-logs-")); - tempDirectories.push(directory); - return directory; -} - -// A log of `bytes` bytes, so a test can put the directory over the byte ceiling. -async function log(directory: string, name: string, bytes = 1): Promise { - const path = join(directory, name); - await writeFile(path, "x".repeat(bytes)); - return path; -} - -function runLog(second: number): string { - return `agentcore-20260817-1345${String(second).padStart(2, "0")}.log`; -} - -async function remaining(directory: string): Promise { - return (await readdir(directory)).sort(); -} - -const NO_BYTE_LIMIT = { runs: RETENTION.runs, bytes: Number.MAX_SAFE_INTEGER }; - -describe("pruneOldLogs", () => { - test("keeps the newest runs and deletes the runs before them", async () => { - const directory = await logDirectory(); - for (let second = 1; second <= 6; second += 1) await log(directory, runLog(second)); - const current = join(directory, runLog(6)); - - expect(await pruneOldLogs(current, { runs: 3, bytes: Number.MAX_SAFE_INTEGER })).toBe(3); - - // The three newest, which for the run doing the pruning includes its own. - expect(await remaining(directory)).toEqual([runLog(4), runLog(5), runLog(6)]); - }); - - test("never deletes the log this run is writing", async () => { - const directory = await logDirectory(); - // The oldest file is this run's, which happens when the clock moved backwards: even - // then a run cannot delete the file it still has open. - const current = await log(directory, runLog(1)); - for (const second of [2, 3, 4]) await log(directory, runLog(second)); - - await pruneOldLogs(current, { runs: 1, bytes: Number.MAX_SAFE_INTEGER }); - - expect(await remaining(directory)).toEqual([runLog(1), runLog(4)]); - }); - - test("deletes the oldest runs until what is left fits the byte ceiling", async () => { - const directory = await logDirectory(); - // A deploy's log is the large one, and a few of them reach the ceiling long before - // the run count does. - for (const second of [1, 2, 3]) await log(directory, runLog(second), 400); - const current = await log(directory, runLog(4), 400); - - expect(await pruneOldLogs(current, { runs: 50, bytes: 1000 })).toBe(2); - - expect(await remaining(directory)).toEqual([runLog(3), runLog(4)]); - }); - - test("keeps this run's log even when it alone exceeds the ceiling", async () => { - const directory = await logDirectory(); - await log(directory, runLog(1), 10); - const current = await log(directory, runLog(2), 5000); - - await pruneOldLogs(current, { runs: 50, bytes: 1000 }); - - expect(await remaining(directory)).toEqual([runLog(2)]); - }); - - test("deletes the daily logs an earlier version left behind", async () => { - const directory = await logDirectory(); - // What the rotating file wrote before one file per run replaced it. Nothing else would - // ever delete these, so an upgrade does not leave a directory that only grows. - await log(directory, "output-2026-08-10.log"); - await log(directory, "output-2026-08-17.log"); - const current = await log(directory, runLog(1)); - - expect(await pruneOldLogs(current, { runs: 2, bytes: Number.MAX_SAFE_INTEGER })).toBe(1); - - // The older daily file goes first: a date sorts before any run of that date. - expect(await remaining(directory)).toEqual([runLog(1), "output-2026-08-17.log"]); - }); - - test("leaves files it did not write", async () => { - const directory = await logDirectory(); - const current = await log(directory, runLog(2)); - await log(directory, runLog(1)); - await log(directory, "notes.txt"); - await log(directory, "deploy-20260817-134506.log"); - - expect(await pruneOldLogs(current, { runs: 1, bytes: Number.MAX_SAFE_INTEGER })).toBe(1); - - // Only the CLI's own logs are pruned, so a file a user copied in stays where they put it. - expect(await remaining(directory)).toEqual([ - runLog(2), - "deploy-20260817-134506.log", - "notes.txt", - ]); - }); - - test("prunes nothing when this run has yet to write anything", async () => { - // The first run on a machine: the directory the transport is about to create. - const directory = join(await logDirectory(), "not-created-yet"); - - expect(await pruneOldLogs(join(directory, runLog(1)), NO_BYTE_LIMIT)).toBe(0); - }); - - test("bounds the directory by default rather than letting it grow", async () => { - // The ceiling the rotating file enforced before one file per run replaced it, so the - // default is a bound and not merely a large number. - expect(RETENTION.bytes).toBe(50 * 1024 * 1024); - expect(RETENTION.runs).toBeGreaterThan(0); - }); -}); diff --git a/src/logging/retention.tsx b/src/logging/retention.tsx deleted file mode 100644 index 5bea3d5df..000000000 --- a/src/logging/retention.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { readdir, rm, stat } from "node:fs/promises"; -import { dirname, join } from "node:path"; - -// The names the CLI writes its logs under: one file per run, and the daily file earlier -// versions rotated. Only these are pruned, so anything else a user keeps in the log -// directory is left where they put it. -const RUN_LOG = /^agentcore-\d{8}-\d{6}\.log$/; -const ROTATED_LOG = /^output-\d{4}-\d{2}-\d{2}\.log$/; - -/** - * How much history is kept. The byte ceiling is what the rotating file enforced before - * one file per run replaced it (5 MB × 10); the count is generous against it because a - * run's log is usually a few kilobytes, and one deploy's worth of history is worth more - * than ten runs. - */ -export const RETENTION = { runs: 50, bytes: 50 * 1024 * 1024 }; - -/** - * Deletes the logs of the oldest runs until what is left fits {@link RETENTION}, and - * returns how many were deleted. - * - * The log of the run doing the pruning is always kept, however far over the ceiling it - * takes the directory: a run cannot delete the file it is still writing to. That also - * makes the byte ceiling approximate, since that file keeps growing after it is measured. - * - * Housekeeping, so nothing here is allowed to fail a command: a directory that cannot be - * read prunes nothing, and a file that another run deleted first is not an error. - */ -export async function pruneOldLogs(currentLogFile: string, limits = RETENTION): Promise { - const directory = dirname(currentLogFile); - - let names: string[]; - try { - names = (await readdir(directory)).filter( - (name) => RUN_LOG.test(name) || ROTATED_LOG.test(name), - ); - } catch { - // No log has been written yet, so there is nothing to prune. - return 0; - } - - // Both names carry a zero-padded timestamp, so their digits alone sort by age — by name - // would rank every `output-` file above every `agentcore-` one. A daily file's digits - // stop at its date, which makes it older than any run of that date, as it is. - const age = (name: string) => name.replace(/\D/g, ""); - const newestFirst = names.sort((a, b) => age(b).localeCompare(age(a))); - - const stale: string[] = []; - let kept = 0; - let bytes = 0; - for (const name of newestFirst) { - const path = join(directory, name); - const size = await stat(path) - .then((stats) => stats.size) - .catch(() => 0); - - if (path !== currentLogFile && (kept >= limits.runs || bytes + size > limits.bytes)) { - stale.push(path); - continue; - } - kept += 1; - bytes += size; - } - - const deleted = await Promise.all( - stale.map((path) => - rm(path, { force: true }).then( - () => true, - () => false, - ), - ), - ); - return deleted.filter(Boolean).length; -} From e9365ba6cafaf94f257ddcb804f337da2c7d33a3 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 23:35:46 -0400 Subject: [PATCH 15/18] feat(project): write what a deploy shows to the log it points at The log held the toolkit's account of a deploy and nothing of the deploy itself, so the hundreds of lines under a step gave no sign of which step they belonged to, and the outputs a deploy ended with were only ever on screen. Deploy now writes its steps and its result through the same logger the toolkit's narration goes through, with the outputs as bindings so they are data rather than the lines printed for them. --- src/handlers/project/deploy/index.ts | 11 ++++++- src/handlers/project/project.test.ts | 43 +++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index 626feaafc..6769b2a84 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -1,5 +1,5 @@ import z from "zod"; -import { createHandler, flag, ProjectKey } from "../../../router"; +import { createHandler, flag, LoggerKey, ProjectKey } from "../../../router"; import type { AppIO } from "../../../io"; import { detailedLogLocation } from "../../../logging"; import { RegionKey } from "../../keys"; @@ -24,6 +24,11 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = handle: async (ctx, flags) => { const project = ctx.require(ProjectKey); + // The log is the record of a deploy, so what the deploy shows is written there too, + // interleaved with the deployment tooling's own narration: reading the log alone is + // then enough to see which step the hundreds of lines under it belong to. + const logger = ctx.require(LoggerKey).child({ project: project.name, target: flags.target }); + // Progress goes to stderr, keeping stdout for machine output. The deployment // tooling's own narration goes to the log, so a deploy shows only its steps. try { @@ -38,9 +43,13 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = outputs = event.outputs; continue; } + logger.info(event.message); config.io.stderr.write(`${event.message}\n`); } + // The outputs are logged as they are reported rather than as the lines below print + // them, so a later run can be compared against this one. + logger.child({ outputs }).info("deployed project"); config.io.stderr.write(`Deployed project '${project.name}'\n`); // The stack's outputs are what the deploy created — endpoints, ARNs, ids — and diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index f31859eb5..9e0b84eaf 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -5,11 +5,13 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { createRootHandler } from "../index"; import { + createRecordingLogger, createSilentLogger, TestCoreClient, TestGlobalConfigAccessor, testIO, } from "../../testing"; +import type { Logger } from "../../logging"; import { InputValidationError } from "../../errors"; import { FsReadWriteJson, type CdkEvent, type CdkOperation, type ReadWriteJson } from "../../io"; import { detailedLogLocation } from "../../logging"; @@ -32,6 +34,8 @@ type CliOptions = { core?: TestCoreClient; /** The outputs the deployed stack reports; see {@link TestCoreClient}. */ cdkOutputs?: Record; + /** The log every command writes to, for a test that asserts on what was logged. */ + logger?: Logger; }; // A CLI wired to test doubles, whose route() a test drives directly when it needs @@ -47,7 +51,7 @@ function cli(options?: CliOptions) { const root = createRootHandler(core, { io: io.io, globalConfigAccessor: new TestGlobalConfigAccessor(), - logger: createSilentLogger(), + logger: options?.logger ?? createSilentLogger(), }); return { io, @@ -876,6 +880,43 @@ describe("project deploy", () => { expect(io.stderr()).toEndWith(`Detailed logs: ${detailedLogLocation()}`); }); + test("writes what it shows to the log, beside the toolkit's narration", async () => { + await inProject(); + const { logger, logs } = createRecordingLogger(); + // The one logger the commands and the deployment tooling both write to, as the CLI + // wires it, so a deploy's steps and the toolkit's narration land in the same file. + const { route } = cli({ + logger, + core: new TestCoreClient({ + logger, + cdkOutputs: { + RuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/MyAgent", + }, + onCdkOperation: (operation, emit) => { + if (operation.kind === "deploy") { + emit({ level: "info", message: "example-stack | 1/2 | CREATE_IN_PROGRESS" }); + } + }, + }), + }); + + await route(["deploy"]); + + // The steps a deploy printed are in the log as well, so the hundreds of toolkit lines + // between two of them can be read against the step they belong to. + const messages = logs.map(({ message }) => message); + expect(messages).toContain(`Deploying ${STACK("default")}`); + expect(messages).toContain("example-stack | 1/2 | CREATE_IN_PROGRESS"); + + // The result carries what the deploy created as data rather than as the lines printed + // for it, along with which project and target it was. + expect(logs.find(({ message }) => message === "deployed project")?.bindings).toMatchObject({ + project: "MyAgent", + target: "default", + outputs: { RuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/MyAgent" }, + }); + }); + test("names the log even when the deploy fails", async () => { await inProject(); const { io, route } = cli({ From 4cca59c84eec3b3b269a2b89a5c5208c346a5c09 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 17 Aug 2026 23:52:10 -0400 Subject: [PATCH 16/18] fix(project): bootstrap only an environment that needs it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A deploy bootstrapped every time, and the parameters it bootstraps with override the stack's existing ones, so a deploy into an environment somebody had already bootstrapped without a customer-managed key rewrote the CDKToolkit stack the whole account shares. It also required CloudFormation write access on every deploy, which a deploy-only role does not have. So the bootstrap version is read first and bootstrap runs only when the stack is missing, unreadable or older than we deploy against — the same check the published CLI makes. The step is no longer printed when it does not happen. --- bun.lock | 1 + package.json | 1 + src/core/project/backends/cdk.test.ts | 24 +++++++++++++- src/core/project/backends/cdk.tsx | 16 +++++++--- src/handlers/project/project.test.ts | 15 +++++++++ src/io/cdk.test.ts | 23 ++++++++++++++ src/io/cdk.ts | 46 +++++++++++++++++++++++++-- src/io/index.ts | 2 ++ src/testing/TestCoreClient.tsx | 3 ++ 9 files changed, 124 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index 1a9f6297d..99cb59ffa 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@aws-cdk/toolkit-lib": "^1.38.2", "@aws-sdk/client-bedrock-agentcore": "^3.1092.0", "@aws-sdk/client-bedrock-agentcore-control": "^3.1102.0", + "@aws-sdk/client-cloudformation": "^3.1092.0", "@aws-sdk/client-cloudwatch-logs": "^3.1092.0", "@aws-sdk/client-iam": "^3.1080.0", "@opentelemetry/api": "^1.9.1", diff --git a/package.json b/package.json index a94afba28..fbbd9f210 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@aws-cdk/toolkit-lib": "^1.38.2", "@aws-sdk/client-bedrock-agentcore": "^3.1092.0", "@aws-sdk/client-bedrock-agentcore-control": "^3.1102.0", + "@aws-sdk/client-cloudformation": "^3.1092.0", "@aws-sdk/client-cloudwatch-logs": "^3.1092.0", "@aws-sdk/client-iam": "^3.1080.0", "@opentelemetry/api": "^1.9.1", diff --git a/src/core/project/backends/cdk.test.ts b/src/core/project/backends/cdk.test.ts index 5c6c6f684..c6cae6b17 100644 --- a/src/core/project/backends/cdk.test.ts +++ b/src/core/project/backends/cdk.test.ts @@ -92,11 +92,13 @@ type RecordedCdkRun = { operation: CdkOperation; options: CdkRunOptions }; // A backend whose runner records commands instead of spawning them and whose toolkit // records operations instead of reaching AWS. `onCdk` supplies the events one operation -// emits, and the failure it ends with; `outputs` what its deploy produced. +// emits, and the failure it ends with; `outputs` what its deploy produced; +// `bootstrapped` whether the environment already has a bootstrap stack. function backend( onCdk?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void, runner?: () => Promise, outputs: CdkOutputs = {}, + bootstrapped = false, ): { backend: CdkBackend; commands: RecordedCommand[]; @@ -129,6 +131,7 @@ function backend( // Bootstrap produces no outputs of the project's own, as with the real runner. return operation.kind === "deploy" ? outputs : {}; }, + bootstrapped: async () => bootstrapped, checkTool: async () => {}, }), commands, @@ -210,6 +213,25 @@ describe("CdkBackend.deploy", () => { ]); }); + test("leaves an environment that is already bootstrapped alone", async () => { + const root = await inTempDirectory(); + const { backend: subject, runs } = backend(undefined, undefined, {}, true); + const built = await project(root); + await synthesized(root, [TARGET.name]); + + const events = await drain(subject.deploy(built, { target: TARGET, region: REGION })); + + // Bootstrapping again would rewrite a stack the whole account shares, so a deploy + // neither does it nor claims to. + expect(runs.map(({ operation }) => operation)).toEqual([ + { kind: "deploy", stackName: stackName("default") }, + ]); + expect(events).toEqual([ + { kind: "step", message: "Synthesizing CloudFormation templates" }, + { kind: "step", message: `Deploying ${stackName("default")}` }, + ]); + }); + test("bootstraps only the target's environment, and ships only its stack", async () => { const root = await inTempDirectory(); const { backend: subject, runs } = backend(); diff --git a/src/core/project/backends/cdk.tsx b/src/core/project/backends/cdk.tsx index da8764848..d1e85513d 100644 --- a/src/core/project/backends/cdk.tsx +++ b/src/core/project/backends/cdk.tsx @@ -4,9 +4,11 @@ import type { Project, ProjectEvent, ProjectStep } from "../../../handlers/proje import type { Logger } from "../../../logging"; import { FsReadWriteJson, + isBootstrapCurrent, requireTool, runCdk, runProcess, + type BootstrapProbe, type CdkEvent, type CdkOperation, type CdkOutputs, @@ -37,6 +39,7 @@ export type CdkBackendConfig = { logger: Logger; runner?: ProcessRunner; // injectable so tests never spawn real processes cdk?: CdkRunner; // injectable so tests never reach AWS + bootstrapped?: BootstrapProbe; // injectable so tests never reach AWS checkTool?: typeof requireTool; // injectable so tests don't depend on the host's PATH json?: ReadWriteJson; // injectable so tests read fixtures instead of disk }; @@ -46,6 +49,7 @@ export class CdkBackend implements ProjectBackend { private readonly logger: Logger; private readonly runner: ProcessRunner; private readonly cdk: CdkRunner; + private readonly bootstrapped: BootstrapProbe; private readonly checkTool: typeof requireTool; private readonly json: ReadWriteJson; @@ -53,6 +57,7 @@ export class CdkBackend implements ProjectBackend { this.logger = config.logger; this.runner = config.runner ?? runProcess; this.cdk = config.cdk ?? runCdk; + this.bootstrapped = config.bootstrapped ?? isBootstrapCurrent; this.checkTool = config.checkTool ?? requireTool; this.json = config.json ?? new FsReadWriteJson({ logger: config.logger }); } @@ -102,11 +107,14 @@ export class CdkBackend implements ProjectBackend { // immediately rather than after minutes of bootstrapping. const stackName = await stackForTarget(this.json, run.assemblyDirectory, input.target.name); - // Bootstrap is idempotent and quick on an already-current environment, so it runs - // every deploy rather than probing CloudFormation first. + // Bootstrapping updates a CloudFormation stack every deploy in the account shares — + // and the parameters we bootstrap with would put a customer-managed key on a staging + // bucket that had none — so an environment somebody has already prepared is left alone. const environment = `aws://${input.target.account}/${input.target.region}`; - yield { kind: "step", message: `Bootstrapping ${environment}` }; - await this.runCdkOperation({ kind: "bootstrap", environments: [environment] }, run); + if (!(await this.bootstrapped(input.target.region))) { + yield { kind: "step", message: `Bootstrapping ${environment}` }; + await this.runCdkOperation({ kind: "bootstrap", environments: [environment] }, run); + } yield { kind: "step", message: `Deploying ${stackName}` }; const outputs = await this.runCdkOperation({ kind: "deploy", stackName }, run); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 9e0b84eaf..dbcea8036 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -812,6 +812,21 @@ describe("project deploy", () => { expect(io.stderr()).toContain("Deployed project 'MyAgent'"); }); + test("deploys straight away into an environment that is already bootstrapped", async () => { + await inProject(); + const { io, core } = await run(["deploy"], { + core: new TestCoreClient({ alreadyBootstrapped: true }), + }); + + // Nothing touches the CDKToolkit stack the account shares, and the deploy does not + // report a step it skipped. + expect(core.cdkRuns.map(({ operation }) => operation)).toEqual([ + { kind: "deploy", stackName: STACK("default") }, + ]); + expect(io.stderr()).not.toContain("Bootstrapping"); + expect(io.stderr()).toContain("Deployed project 'MyAgent'"); + }); + test("--target selects which configured target to deploy to", async () => { const projectRoot = await inProject([ { name: "default", account: "111122223333", region: "us-east-1" }, diff --git a/src/io/cdk.test.ts b/src/io/cdk.test.ts index d4af69d62..89cfa65a4 100644 --- a/src/io/cdk.test.ts +++ b/src/io/cdk.test.ts @@ -8,6 +8,7 @@ import type { IIoHost, IoMessage, IoRequest } from "@aws-cdk/toolkit-lib"; // here, unlike in cdk.ts, because a test file pays no startup cost. import * as toolkit from "@aws-cdk/toolkit-lib"; import { + isBootstrapCurrent, loadBootstrapTemplate, loadCdkToolkit, performCdkOperation, @@ -337,3 +338,25 @@ describe("runCdk", () => { } }); }); + +describe("isBootstrapCurrent", () => { + test("is current for a bootstrap new enough to deploy against", async () => { + expect(await isBootstrapCurrent("us-east-1", async () => 30)).toBe(true); + expect(await isBootstrapCurrent("us-east-1", async () => 99)).toBe(true); + }); + + test("is not current for a bootstrap older than we deploy against", async () => { + // An old bootstrap is bootstrapped again, which is what upgrades it. + expect(await isBootstrapCurrent("us-east-1", async () => 29)).toBe(false); + expect(await isBootstrapCurrent("us-east-1", async () => 0)).toBe(false); + }); + + test("is not current when the stack cannot be read", async () => { + // No such stack, or no permission to look: bootstrap runs and reports the real + // problem, rather than the probe guessing at it. + const unreadable = async () => { + throw new Error("AccessDenied"); + }; + expect(await isBootstrapCurrent("us-east-1", unreadable)).toBe(false); + }); +}); diff --git a/src/io/cdk.ts b/src/io/cdk.ts index b1bbd0580..ed1b449ff 100644 --- a/src/io/cdk.ts +++ b/src/io/cdk.ts @@ -118,8 +118,10 @@ export async function performCdkOperation( const template = await loadBootstrapTemplate(files); try { await toolkit.bootstrap(lib.BootstrapEnvironments.fromList(operation.environments), { - // Provisions a customer-managed KMS key for the staging bucket, matching - // the parameters the original CLI bootstraps with. + // Provisions a customer-managed KMS key for the staging bucket, matching the + // parameters the original CLI bootstraps with. It overrides whatever the stack + // was bootstrapped with, so callers only bootstrap environments that need it + // (see {@link isBootstrapCurrent}). parameters: lib.BootstrapStackParameters.withExisting({ createCustomerMasterKey: true }), ...(template && { source: lib.BootstrapSource.customTemplate(template) }), }); @@ -204,3 +206,43 @@ export const runCdk: CdkRunner = (operation, options) => const loaded = await loadCdkToolkit(ioHost, options.region); return performCdkOperation(loaded, operation, options); }); + +/** The stack `cdk bootstrap` creates, and the output it records its version in. */ +const BOOTSTRAP_STACK = "CDKToolkit"; +const BOOTSTRAP_VERSION_OUTPUT = "BootstrapVersion"; + +/** The oldest bootstrap our deploys work against, as the original CLI requires. */ +const MINIMUM_BOOTSTRAP_VERSION = 30; + +/** Whether an environment is bootstrapped. Injectable so tests never reach AWS. */ +export type BootstrapProbe = (region: string) => Promise; + +/** The version of a region's bootstrap stack, or 0 where it has none. */ +type ReadBootstrapVersion = (region: string) => Promise; + +const readBootstrapVersion: ReadBootstrapVersion = async (region) => { + // Imported on demand for the same reason the toolkit is: src/io is reachable from every + // command, and only a deploy needs an AWS client. + const { CloudFormationClient, DescribeStacksCommand } = + await import("@aws-sdk/client-cloudformation"); + const client = new CloudFormationClient({ region }); + const { Stacks } = await client.send(new DescribeStacksCommand({ StackName: BOOTSTRAP_STACK })); + const output = Stacks?.[0]?.Outputs?.find((o) => o.OutputKey === BOOTSTRAP_VERSION_OUTPUT); + return Number(output?.OutputValue ?? 0); +}; + +/** + * Whether `region` already has a bootstrap stack new enough to deploy against. + * + * Worth one read: bootstrapping updates a CloudFormation stack every deploy in the account + * shares, so an environment somebody has already prepared is left exactly as they left it. + */ +export const isBootstrapCurrent = async ( + region: string, + read: ReadBootstrapVersion = readBootstrapVersion, +): Promise => { + // A version that cannot be read counts as absent: bootstrap then reports the real + // problem — no such stack, no permission — better than a probe could. + const version = await read(region).catch(() => 0); + return version >= MINIMUM_BOOTSTRAP_VERSION; +}; diff --git a/src/io/index.ts b/src/io/index.ts index 422a32a1b..2002397e2 100644 --- a/src/io/index.ts +++ b/src/io/index.ts @@ -5,7 +5,9 @@ export { type AtomicWriteStreamSource, } from "./atomicWrite"; export { + isBootstrapCurrent, runCdk, + type BootstrapProbe, type CdkEvent, type CdkOperation, type CdkOutputs, diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index ac4a06cb5..ae4e22d5f 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -1191,6 +1191,8 @@ type TestCoreClientOptions = { onCdkOperation?: (operation: CdkOperation, emit: (event: CdkEvent) => void) => void; /** The outputs a deploy produces, as the real toolkit reports a deployed stack's. */ cdkOutputs?: CdkOutputs; + /** Whether the target environment already has a bootstrap stack a deploy can use. */ + alreadyBootstrapped?: boolean; json?: ReadWriteJson; }; @@ -1983,6 +1985,7 @@ export class TestCoreClient implements Core { logger, runner, checkTool, + bootstrapped: async () => options?.alreadyBootstrapped ?? false, cdk: async function* (operation, runOptions) { cdkRuns.push({ operation, options: runOptions }); const emitted: CdkEvent[] = []; From 569c5625ce9dadb7c51caf21d84b9a11e95fc294 Mon Sep 17 00:00:00 2001 From: notgitika Date: Tue, 18 Aug 2026 00:07:50 -0400 Subject: [PATCH 17/18] fix(project): survive an unmapped toolkit log level, and catch a bad account early Three small ones from review: - The toolkit is EXTERNAL to the bundle, so the version a user resolves can report a log level this build has no mapping for. That indexed into undefined and took the whole deploy down over a log line; it now lands at info. - A deployment target's account becomes `aws:///`, so a typo used to surface minutes later from inside the toolkit. The schema requires 12 digits and names the field instead. - TestCoreClient handed its json adapter to the project manager but not to the backend it builds, so a backend's reads went to real disk while everything else read the test's fixtures. --- src/core/project/backends/cdk.test.ts | 21 +++++++++++++++++++++ src/core/project/backends/cdk.tsx | 6 +++++- src/handlers/project/project.test.ts | 19 +++++++++++++++++++ src/projectSchemas/aws-targets.test.ts | 24 ++++++++++++++++++++++++ src/projectSchemas/aws-targets.ts | 8 +++++++- src/testing/TestCoreClient.tsx | 1 + 6 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/projectSchemas/aws-targets.test.ts diff --git a/src/core/project/backends/cdk.test.ts b/src/core/project/backends/cdk.test.ts index c6cae6b17..a824ac1f4 100644 --- a/src/core/project/backends/cdk.test.ts +++ b/src/core/project/backends/cdk.test.ts @@ -397,6 +397,27 @@ describe("CdkBackend.deploy", () => { ]); }); + test("logs a message whose level this build has no mapping for", async () => { + const root = await inTempDirectory(); + const { backend: subject, logs } = backend((operation, emit) => { + if (operation.kind !== "deploy") return; + // The toolkit is resolved at the user's install rather than bundled, so it can + // report a level that did not exist when this was compiled. + emit({ + level: "success" as unknown as CdkEvent["level"], + message: "example-stack: deployed", + }); + }); + const built = await project(root); + await synthesized(root, [TARGET.name]); + + await drain(subject.deploy(built, { target: TARGET, region: REGION })); + + // Written at info, since losing the deploy over an unrecognized log line would be a + // far worse outcome than logging one at the wrong severity. + expect(logged(logs)).toEqual([{ level: "info", message: "example-stack: deployed" }]); + }); + test("strips the colours the toolkit writes for the terminal it assumes", async () => { const root = await inTempDirectory(); const { backend: subject, logs } = backend((operation, emit) => { diff --git a/src/core/project/backends/cdk.tsx b/src/core/project/backends/cdk.tsx index d1e85513d..a6741b680 100644 --- a/src/core/project/backends/cdk.tsx +++ b/src/core/project/backends/cdk.tsx @@ -146,7 +146,11 @@ export class CdkBackend implements ProjectBackend { const events = this.cdk(operation, options); for (let next = await events.next(); ; next = await events.next()) { if (next.done) return next.value; - logger[CDK_LOG_LEVELS[next.value.level]](next.value.message.replace(ANSI_ESCAPES, "")); + // A level this build has no mapping for still gets logged: the toolkit is resolved + // at the user's install rather than bundled, so a newer one can report a level that + // did not exist when this was compiled, and a deploy must not die over a log line. + const level = CDK_LOG_LEVELS[next.value.level] ?? "info"; + logger[level](next.value.message.replace(ANSI_ESCAPES, "")); } } diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index dbcea8036..b375c1fd6 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -827,6 +827,25 @@ describe("project deploy", () => { expect(io.stderr()).toContain("Deployed project 'MyAgent'"); }); + test("reads the assembly through the json adapter it was given", async () => { + const projectRoot = await inProject(); + const real = new FsReadWriteJson({ logger: createSilentLogger() }); + const read: string[] = []; + const recordingJson: ReadWriteJson = { + read: (path, schema) => { + read.push(path); + return real.read(path, schema); + }, + write: (path, data) => real.write(path, data), + }; + + await run(["deploy"], { core: new TestCoreClient({ json: recordingJson }) }); + + // Every file a deploy reads goes through the injected adapter, the backend's own reads + // included, so a test can stand a project's files up however it likes. + expect(read).toContain(join(projectRoot, "agentcore", "cdk", "cdk.out", "manifest.json")); + }); + test("--target selects which configured target to deploy to", async () => { const projectRoot = await inProject([ { name: "default", account: "111122223333", region: "us-east-1" }, diff --git a/src/projectSchemas/aws-targets.test.ts b/src/projectSchemas/aws-targets.test.ts new file mode 100644 index 000000000..5aad32ade --- /dev/null +++ b/src/projectSchemas/aws-targets.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test"; +import { AwsTargetsSchema } from "./aws-targets"; + +describe("AwsTargetsSchema", () => { + test("accepts a target naming an account and a region", () => { + const targets = [{ name: "default", account: "111122223333", region: "us-east-1" }]; + + expect(AwsTargetsSchema.parse(targets)).toEqual(targets); + }); + + test.each([ + ["a digit short", "11112222333"], + ["a digit long", "1111222233334"], + ["punctuated", "1111-2222-3333"], + ["an account alias", "my-account"], + ["empty", ""], + ])("rejects an account that is %s", (_, account) => { + // Deploy turns this straight into `aws:///`, so catching a typo here + // names the field that is wrong instead of failing inside the CDK toolkit minutes in. + expect(() => + AwsTargetsSchema.parse([{ name: "default", account, region: "us-east-1" }]), + ).toThrow(/12-digit AWS account ID/); + }); +}); diff --git a/src/projectSchemas/aws-targets.ts b/src/projectSchemas/aws-targets.ts index 72024706f..2541ea4df 100644 --- a/src/projectSchemas/aws-targets.ts +++ b/src/projectSchemas/aws-targets.ts @@ -6,7 +6,13 @@ import z from "zod"; * empty list, so a project has no targets until the user fills them in. */ export const AwsTargetsSchema = z.array( - z.object({ name: z.string(), account: z.string(), region: z.string() }), + z.object({ + name: z.string(), + // Checked here because a deploy turns this straight into `aws:///`: + // a typo would otherwise surface minutes later, from inside the CDK toolkit. + account: z.string().regex(/^\d{12}$/, "must be a 12-digit AWS account ID"), + region: z.string(), + }), ); export type AwsTarget = z.infer[number]; diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index ae4e22d5f..07a3975cc 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -1983,6 +1983,7 @@ export class TestCoreClient implements Core { backends: { CDK: new CdkBackend({ logger, + json: options?.json, runner, checkTool, bootstrapped: async () => options?.alreadyBootstrapped ?? false, From bb0ba2edaec48f21d26688df33c216122bcf26b7 Mon Sep 17 00:00:00 2001 From: notgitika Date: Tue, 18 Aug 2026 00:30:14 -0400 Subject: [PATCH 18/18] fix(project): only treat a usable bootstrap stack as bootstrapped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The probe read the version output and nothing else, so a CDKToolkit that was rolled back or mid-delete reported version 32 and counted as ready: the deploy skipped the bootstrap that would have repaired it and failed later against roles and a bucket that were not there. It now requires the stack to be in one of the states the published CLI's own check accepts, and the parsing of the DescribeStacks response is a pure function with tests of its own rather than the one AWS-touching seam nothing covered. Also: the deploy logs what the probe decided, since it decides whether the step below runs at all; the version floor now says where the published CLI sets it, so the number is checkable; and two comments that overclaimed are corrected — a run crossing midnight rotates into the next day's file, and a rejected account fails the file rather than naming the field. --- src/core/project/backends/cdk.tsx | 6 ++- src/io/cdk.test.ts | 69 ++++++++++++++++++++++++-- src/io/cdk.ts | 56 +++++++++++++++++---- src/logging/location.tsx | 6 ++- src/projectSchemas/aws-targets.test.ts | 2 +- src/projectSchemas/aws-targets.ts | 5 +- 6 files changed, 123 insertions(+), 21 deletions(-) diff --git a/src/core/project/backends/cdk.tsx b/src/core/project/backends/cdk.tsx index a6741b680..35075eac2 100644 --- a/src/core/project/backends/cdk.tsx +++ b/src/core/project/backends/cdk.tsx @@ -111,7 +111,11 @@ export class CdkBackend implements ProjectBackend { // and the parameters we bootstrap with would put a customer-managed key on a staging // bucket that had none — so an environment somebody has already prepared is left alone. const environment = `aws://${input.target.account}/${input.target.region}`; - if (!(await this.bootstrapped(input.target.region))) { + const bootstrapped = await this.bootstrapped(input.target.region); + // Logged because it decides whether the step below runs at all: a deploy that then + // fails on the bootstrap it skipped is otherwise unexplainable from the log alone. + this.logger.child({ environment, bootstrapped }).debug("checked bootstrap"); + if (!bootstrapped) { yield { kind: "step", message: `Bootstrapping ${environment}` }; await this.runCdkOperation({ kind: "bootstrap", environments: [environment] }, run); } diff --git a/src/io/cdk.test.ts b/src/io/cdk.test.ts index 89cfa65a4..370b56011 100644 --- a/src/io/cdk.test.ts +++ b/src/io/cdk.test.ts @@ -7,8 +7,10 @@ import type { IIoHost, IoMessage, IoRequest } from "@aws-cdk/toolkit-lib"; // the toolkit itself defines rather than ones the tests made up. Imported statically // here, unlike in cdk.ts, because a test file pays no startup cost. import * as toolkit from "@aws-cdk/toolkit-lib"; +import type { Stack } from "@aws-sdk/client-cloudformation"; import { isBootstrapCurrent, + readBootstrapStack, loadBootstrapTemplate, loadCdkToolkit, performCdkOperation, @@ -339,16 +341,75 @@ describe("runCdk", () => { }); }); +describe("readBootstrapStack", () => { + // What DescribeStacks returns for a bootstrap stack in the given state. + function described(status: Stack["StackStatus"], version?: string): Stack[] { + return [ + { + StackName: "CDKToolkit", + CreationTime: new Date(0), + StackStatus: status, + Outputs: [ + { OutputKey: "BucketName", OutputValue: "cdk-hnb659fds-assets-111122223333-us-east-1" }, + ...(version ? [{ OutputKey: "BootstrapVersion", OutputValue: version }] : []), + ], + }, + ]; + } + + test("reads the version a healthy bootstrap stack reports", () => { + expect(readBootstrapStack(described("CREATE_COMPLETE", "32"))).toEqual({ + version: 32, + usable: true, + }); + // Bootstrapping over a stack that had rolled back leaves it here, and it works. + expect(readBootstrapStack(described("UPDATE_ROLLBACK_COMPLETE", "30"))).toEqual({ + version: 30, + usable: true, + }); + }); + + test("reads a stack that is not in a state to deploy against as unusable", () => { + // The version output says what the stack was meant to be; these say whether the roles + // and bucket a deploy needs are actually there. + expect(readBootstrapStack(described("ROLLBACK_COMPLETE", "32")).usable).toBe(false); + expect(readBootstrapStack(described("DELETE_IN_PROGRESS", "32")).usable).toBe(false); + expect(readBootstrapStack(described("UPDATE_ROLLBACK_FAILED", "32")).usable).toBe(false); + }); + + test("reads a missing or unparseable version as none at all", () => { + // Rather than NaN, which would compare false against every threshold silently. + expect(readBootstrapStack(described("CREATE_COMPLETE"))).toEqual({ version: 0, usable: true }); + expect(readBootstrapStack(described("CREATE_COMPLETE", "v32")).version).toBe(0); + }); + + test("reads an empty response as no bootstrap stack", () => { + expect(readBootstrapStack([])).toEqual({ version: 0, usable: false }); + expect(readBootstrapStack()).toEqual({ version: 0, usable: false }); + }); +}); + describe("isBootstrapCurrent", () => { + // A probe answer, as DescribeStacks would have produced it. + const stack = + (version: number, usable = true) => + async () => ({ version, usable }); + test("is current for a bootstrap new enough to deploy against", async () => { - expect(await isBootstrapCurrent("us-east-1", async () => 30)).toBe(true); - expect(await isBootstrapCurrent("us-east-1", async () => 99)).toBe(true); + expect(await isBootstrapCurrent("us-east-1", stack(30))).toBe(true); + expect(await isBootstrapCurrent("us-east-1", stack(99))).toBe(true); }); test("is not current for a bootstrap older than we deploy against", async () => { // An old bootstrap is bootstrapped again, which is what upgrades it. - expect(await isBootstrapCurrent("us-east-1", async () => 29)).toBe(false); - expect(await isBootstrapCurrent("us-east-1", async () => 0)).toBe(false); + expect(await isBootstrapCurrent("us-east-1", stack(29))).toBe(false); + expect(await isBootstrapCurrent("us-east-1", stack(0))).toBe(false); + }); + + test("is not current for a new enough bootstrap that is not in a usable state", async () => { + // Deploying against a rolled-back or half-deleted CDKToolkit fails confusingly; it is + // bootstrapped again instead, which is what repairs it. + expect(await isBootstrapCurrent("us-east-1", stack(32, false))).toBe(false); }); test("is not current when the stack cannot be read", async () => { diff --git a/src/io/cdk.ts b/src/io/cdk.ts index ed1b449ff..4b9bbc20c 100644 --- a/src/io/cdk.ts +++ b/src/io/cdk.ts @@ -4,6 +4,8 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { tmpdir } from "node:os"; import type { IIoHost, IoMessageLevel, Toolkit } from "@aws-cdk/toolkit-lib"; +// Type-only, so it is erased rather than loading the client every command pays for. +import type { Stack } from "@aws-sdk/client-cloudformation"; /** A message emitted by the toolkit while an operation runs. */ export type CdkEvent = { @@ -211,24 +213,56 @@ export const runCdk: CdkRunner = (operation, options) => const BOOTSTRAP_STACK = "CDKToolkit"; const BOOTSTRAP_VERSION_OUTPUT = "BootstrapVersion"; -/** The oldest bootstrap our deploys work against, as the original CLI requires. */ +/** + * The oldest bootstrap our deploys work against: the floor the published CLI applies in + * `operations/deploy/preflight.ts` as `MINIMUM_CDK_BOOTSTRAP_VERSION`, kept the same here + * so both CLIs re-bootstrap the same environments. It is only a floor — each stack in the + * assembly declares the version it needs and the toolkit enforces that when it deploys, so + * a stack wanting more than its region has fails rather than deploying against too little. + */ const MINIMUM_BOOTSTRAP_VERSION = 30; +/** + * The statuses a bootstrap stack can be deployed against, as the published CLI's own check + * uses. One that is rolled back, mid-delete, or mid-update is bootstrapped again instead: + * its version output says what the stack was meant to be, not whether the roles and bucket + * a deploy needs are there. + */ +const USABLE_BOOTSTRAP_STATUSES = new Set([ + "CREATE_COMPLETE", + "UPDATE_COMPLETE", + "UPDATE_ROLLBACK_COMPLETE", +]); + +/** A region's bootstrap stack. An absent or unreadable one reads as version 0, unusable. */ +export type BootstrapStack = { version: number; usable: boolean }; + /** Whether an environment is bootstrapped. Injectable so tests never reach AWS. */ export type BootstrapProbe = (region: string) => Promise; -/** The version of a region's bootstrap stack, or 0 where it has none. */ -type ReadBootstrapVersion = (region: string) => Promise; +/** What DescribeStacks said about the bootstrap stack. Pure, so it is testable offline. */ +export function readBootstrapStack(stacks?: Stack[]): BootstrapStack { + const stack = stacks?.[0]; + const version = stack?.Outputs?.find( + (output) => output.OutputKey === BOOTSTRAP_VERSION_OUTPUT, + )?.OutputValue; + return { + // A version that is missing or unparseable reads as 0, so the stack is bootstrapped again. + version: Number.parseInt(version ?? "", 10) || 0, + usable: USABLE_BOOTSTRAP_STATUSES.has(stack?.StackStatus ?? ""), + }; +} -const readBootstrapVersion: ReadBootstrapVersion = async (region) => { +const describeBootstrapStack = async (region: string): Promise => { // Imported on demand for the same reason the toolkit is: src/io is reachable from every - // command, and only a deploy needs an AWS client. + // command, and only a deploy needs an AWS client. The default credential chain is the one + // the toolkit resolves through as well, so the probe reads the bootstrap stack of the + // account a bootstrap would have written to. const { CloudFormationClient, DescribeStacksCommand } = await import("@aws-sdk/client-cloudformation"); const client = new CloudFormationClient({ region }); const { Stacks } = await client.send(new DescribeStacksCommand({ StackName: BOOTSTRAP_STACK })); - const output = Stacks?.[0]?.Outputs?.find((o) => o.OutputKey === BOOTSTRAP_VERSION_OUTPUT); - return Number(output?.OutputValue ?? 0); + return readBootstrapStack(Stacks); }; /** @@ -239,10 +273,10 @@ const readBootstrapVersion: ReadBootstrapVersion = async (region) => { */ export const isBootstrapCurrent = async ( region: string, - read: ReadBootstrapVersion = readBootstrapVersion, + read: (region: string) => Promise = describeBootstrapStack, ): Promise => { - // A version that cannot be read counts as absent: bootstrap then reports the real + // A stack that cannot be read counts as absent: bootstrap then reports the real // problem — no such stack, no permission — better than a probe could. - const version = await read(region).catch(() => 0); - return version >= MINIMUM_BOOTSTRAP_VERSION; + const { version, usable } = await read(region).catch(() => ({ version: 0, usable: false })); + return usable && version >= MINIMUM_BOOTSTRAP_VERSION; }; diff --git a/src/logging/location.tsx b/src/logging/location.tsx index 3079ff819..abe5d4347 100644 --- a/src/logging/location.tsx +++ b/src/logging/location.tsx @@ -10,8 +10,10 @@ const LOG_DIRECTORY = [".agentcore", "logs"]; // the prefix is not a file: `logFilePath` spells the file that prefix produces. const LOG_PREFIX = "output"; -// The day the rotating transport is writing under, read once at startup so a command that -// prints where its log is names the file the logger opened, even across midnight. +// The day the rotating transport opened a file for, read once at startup so every mention +// of the log within one run names the same file. A run that crosses midnight rotates into +// the next day's file, so this names where the run started writing rather than where it +// finished — the directory is right either way, and the run's first lines are here. const RUN_STARTED_AT = new Date(); type Location = { diff --git a/src/projectSchemas/aws-targets.test.ts b/src/projectSchemas/aws-targets.test.ts index 5aad32ade..0b00d7aec 100644 --- a/src/projectSchemas/aws-targets.test.ts +++ b/src/projectSchemas/aws-targets.test.ts @@ -16,7 +16,7 @@ describe("AwsTargetsSchema", () => { ["empty", ""], ])("rejects an account that is %s", (_, account) => { // Deploy turns this straight into `aws:///`, so catching a typo here - // names the field that is wrong instead of failing inside the CDK toolkit minutes in. + // fails the file that holds it instead of failing inside the CDK toolkit minutes in. expect(() => AwsTargetsSchema.parse([{ name: "default", account, region: "us-east-1" }]), ).toThrow(/12-digit AWS account ID/); diff --git a/src/projectSchemas/aws-targets.ts b/src/projectSchemas/aws-targets.ts index 2541ea4df..3cafd2d43 100644 --- a/src/projectSchemas/aws-targets.ts +++ b/src/projectSchemas/aws-targets.ts @@ -8,8 +8,9 @@ import z from "zod"; export const AwsTargetsSchema = z.array( z.object({ name: z.string(), - // Checked here because a deploy turns this straight into `aws:///`: - // a typo would otherwise surface minutes later, from inside the CDK toolkit. + // Checked here because a deploy turns this straight into `aws:///`: a + // typo fails while reading the file, alongside the example of a valid one the reader + // prints, rather than minutes later from inside the CDK toolkit. account: z.string().regex(/^\d{12}$/, "must be a 12-digit AWS account ID"), region: z.string(), }),