@aws/nx-plugin 1.0.0-rc.28 → 1.0.0-rc.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/mcp-server/tools/general-guidance.js +1 -0
- package/src/mcp-server/tools/general-guidance.js.map +1 -1
- package/src/py/agent/a2a-connection/__snapshots__/generator.spec.ts.snap +7 -18
- package/src/py/agent/gateway-connection/__snapshots__/generator.spec.ts.snap +8 -19
- package/src/py/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +9 -24
- package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +3 -10
- package/src/utils/format.d.ts +1 -1
- package/src/utils/format.js +71 -4
- package/src/utils/format.js.map +1 -1
package/package.json
CHANGED
|
@@ -49,6 +49,7 @@ ${PACKAGE_MANAGERS.map((pm)=>buildNxCommand('<options>', pm)).join(' - \n')}
|
|
|
49
49
|
- (Omit -D for production dependencies)
|
|
50
50
|
- When specifying project names as arguments to generators, prefer the _fully qualified_ project name, for example \`@workspace-name/project-name\`. Check the \`project.json\` file for the specific package to find its fully qualified name
|
|
51
51
|
- When no generator exists for a specific framework required, use the base \`ts#project\` and \`py#project\` generators and build on top.
|
|
52
|
+
- Leave the \`--infra\` option at its default value unless the user has explicitly instructed otherwise. Generators choose a sensible default type of infrastructure to deploy the project with, so only override \`--infra\` when the user has specified a particular requirement.
|
|
52
53
|
|
|
53
54
|
## Useful Commands
|
|
54
55
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/mcp-server/tools/general-guidance.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { NxGeneratorInfo } from '../../utils/generators';\nimport { IAC_PROVIDERS } from '../../utils/iac-providers';\nimport { buildNxCommand, fetchGuidePages } from '../generator-info';\nimport { PACKAGE_MANAGERS } from '../schema';\n\nexport const TOOL_SELECTION_GUIDE = `## Tool Selection Guide\n\n- Use the \\`general-guidance\\` tool for guidance and best practices for working with Nx and the Nx Plugin for AWS.\n- Use the \\`create-workspace-command\\` tool to discover how to create a workspace to start a new project.\n- Use the \\`list-generators\\` tool to discover the available generators and how to run them.\n- Use the \\`generator-guide\\` tool to retrieve detailed information about a specific generator.`;\n\n/**\n * Add a tool which provides general guidance for using Nx and the Nx Plugin for AWS\n */\nexport const addGeneralGuidanceTool = (\n server: McpServer,\n generators: NxGeneratorInfo[],\n) => {\n server.registerTool(\n 'general-guidance',\n {\n title: 'General Guidance',\n description:\n 'Tool for guidance and best practices for working with Nx and the Nx Plugin for AWS',\n },\n async () => ({\n content: [\n {\n type: 'text' as const,\n text: `# Nx Plugin for AWS Guidance\n\n${TOOL_SELECTION_GUIDE}\n\n## Getting Started\n\n- Choose a package manager first. You can choose between ${PACKAGE_MANAGERS.join(', ')}. It's recommended to use \"pnpm\" if the user has no preference\n- Choose an infrastructure as code (IaC) provider next. You can choose between ${IAC_PROVIDERS.join(', ')}. It's recommended to use CDK if the user has no preference\n- Next, you must create an Nx workspace. Use the \\`create-workspace-command\\` tool for more details, and provide it with your chosen package manager\n- After this, you can start scaffolding the main components of your application using generators. Use the \\`list-generators\\` tool to discover available generators, and the \\`generator-guide\\` tool for more detailed information about a specific generator\n\n## Nx Primer\n\n- Prefix nx commands with the appropriate prefix for your package manager, for example:\n${PACKAGE_MANAGERS.map((pm) => buildNxCommand('<options>', pm)).join(' - \\n')}\n- Each project in your workspace has a file named \\`project.json\\` which contains important project information such as its name, and defines the \"targets\" which can be run for that project, for example building or testing the project\n- Use the command \\`nx reset\\` to reset the Nx daemon when unexpected issues arise\n- After adding dependencies between TypeScript projects, use \\`nx sync\\` to ensure project references are set up correctly\n\n## General Instructions\n\n- Workspaces contain a single \\`package.json\\` file at the root which defines the dependencies for all projects. Therefore when installing dependencies, you must add these to the root workspace using the appropriate command for your package manager:\n - pnpm add -w -D <package>\n - yarn add -D <package>\n - npm install --legacy-peer-deps -D <package>\n - bun install -D <package>\n - (Omit -D for production dependencies)\n- When specifying project names as arguments to generators, prefer the _fully qualified_ project name, for example \\`@workspace-name/project-name\\`. Check the \\`project.json\\` file for the specific package to find its fully qualified name\n- When no generator exists for a specific framework required, use the base \\`ts#project\\` and \\`py#project\\` generators and build on top.\n\n## Useful Commands\n\n- Fix lint issues with \\`nx run-many --target lint --configuration=fix --all --output-style=stream\\`\n- Build all projects with \\`nx run-many --target build --all --output-style=stream\\`\n- Prefer importing the CDK constructs vended by generators in \\`packages/common/constructs\\` over writing your own\n\n## Best Practices\n\n- After running a generator, use the \\`nx show projects\\` command to check which projects have been added (if any)\n- Carefully examine the files that have been generated and always refer back to the generator guide when working in a generated project\n- Generate all projects into the \\`packages/\\` directory\n- After making changes to your projects, fix linting issues, then run a full build\n- When it's time to start testing a project, suggest to the user that infrastructure is deployed to AWS. For websites, if a runtime-config.json is needed, use the load:runtime-config target after a deployment to point a local website at a sandbox stack.\n\n## Batching Generators\n\nWhen scaffolding several projects in one go, chain generators to avoid a slow dependency install after every generator:\n\n- **Chain generators** with \\`&&\\` in a single command. Pass \\`--prefer-install-dependencies=false\\` on each generator except the last so dependencies install once at the end, for example:\n\n${PACKAGE_MANAGERS.map(\n (pm) => ` \\`\\`\\`bash\n ${buildNxCommand('g @aws/nx-plugin:ts#trpc-api --no-interactive --name=my-app-api --auth=IAM --prefer-install-dependencies=false', pm)} && \\\\\n ${buildNxCommand('g @aws/nx-plugin:ts#react-website --no-interactive --name=my-app-website --prefer-install-dependencies=false', pm)} && \\\\\n ${buildNxCommand('g @aws/nx-plugin:connection --no-interactive --sourceProject=@my-app/my-app-website --targetProject=@my-app/my-app-api --prefer-install-dependencies=false', pm)} && \\\\\n ${buildNxCommand('g @aws/nx-plugin:ts#infra --no-interactive --name=infra', pm)} && \\\\\n ${buildNxCommand('sync', pm)}\n \\`\\`\\``,\n).join('\\n')}\n\n- **\\`--prefer-install-dependencies=false\\`** asks a generator to defer its dependency install so the batch installs once at the end (the final generator above omits the flag and installs everything).\n- **\\`nx sync\\`** is required before building — generators modify TypeScript project references.\n\n## Detailed Guides\n\nPlease refer to the below documentation for important details regarding workspaces and working with TypeScript or Python projects.\n\n${await fetchGuidePages(['workspace', 'typescript-project', 'python-project'], generators)}\n\n `,\n },\n ],\n }),\n );\n};\n"],"names":["IAC_PROVIDERS","buildNxCommand","fetchGuidePages","PACKAGE_MANAGERS","TOOL_SELECTION_GUIDE","addGeneralGuidanceTool","server","generators","registerTool","title","description","content","type","text","join","map","pm"],"mappings":"AAAA;;;CAGC,GAGD,SAASA,aAAa,QAAQ,+BAA4B;AAC1D,SAASC,cAAc,EAAEC,eAAe,QAAQ,uBAAoB;AACpE,SAASC,gBAAgB,QAAQ,eAAY;AAE7C,OAAO,MAAMC,uBAAuB,CAAC;;;;;+FAK0D,CAAC,CAAC;AAEjG;;CAEC,GACD,OAAO,MAAMC,yBAAyB,CACpCC,QACAC;IAEAD,OAAOE,YAAY,CACjB,oBACA;QACEC,OAAO;QACPC,aACE;IACJ,GACA,UAAa,CAAA;YACXC,SAAS;gBACP;oBACEC,MAAM;oBACNC,MAAM,CAAC;;AAEjB,EAAET,qBAAqB;;;;yDAIkC,EAAED,iBAAiBW,IAAI,CAAC,MAAM;+EACR,EAAEd,cAAcc,IAAI,CAAC,MAAM;;;;;;;AAO1G,EAAEX,iBAAiBY,GAAG,CAAC,CAACC,KAAOf,eAAe,aAAae,KAAKF,IAAI,CAAC,SAAS
|
|
1
|
+
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/mcp-server/tools/general-guidance.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { NxGeneratorInfo } from '../../utils/generators';\nimport { IAC_PROVIDERS } from '../../utils/iac-providers';\nimport { buildNxCommand, fetchGuidePages } from '../generator-info';\nimport { PACKAGE_MANAGERS } from '../schema';\n\nexport const TOOL_SELECTION_GUIDE = `## Tool Selection Guide\n\n- Use the \\`general-guidance\\` tool for guidance and best practices for working with Nx and the Nx Plugin for AWS.\n- Use the \\`create-workspace-command\\` tool to discover how to create a workspace to start a new project.\n- Use the \\`list-generators\\` tool to discover the available generators and how to run them.\n- Use the \\`generator-guide\\` tool to retrieve detailed information about a specific generator.`;\n\n/**\n * Add a tool which provides general guidance for using Nx and the Nx Plugin for AWS\n */\nexport const addGeneralGuidanceTool = (\n server: McpServer,\n generators: NxGeneratorInfo[],\n) => {\n server.registerTool(\n 'general-guidance',\n {\n title: 'General Guidance',\n description:\n 'Tool for guidance and best practices for working with Nx and the Nx Plugin for AWS',\n },\n async () => ({\n content: [\n {\n type: 'text' as const,\n text: `# Nx Plugin for AWS Guidance\n\n${TOOL_SELECTION_GUIDE}\n\n## Getting Started\n\n- Choose a package manager first. You can choose between ${PACKAGE_MANAGERS.join(', ')}. It's recommended to use \"pnpm\" if the user has no preference\n- Choose an infrastructure as code (IaC) provider next. You can choose between ${IAC_PROVIDERS.join(', ')}. It's recommended to use CDK if the user has no preference\n- Next, you must create an Nx workspace. Use the \\`create-workspace-command\\` tool for more details, and provide it with your chosen package manager\n- After this, you can start scaffolding the main components of your application using generators. Use the \\`list-generators\\` tool to discover available generators, and the \\`generator-guide\\` tool for more detailed information about a specific generator\n\n## Nx Primer\n\n- Prefix nx commands with the appropriate prefix for your package manager, for example:\n${PACKAGE_MANAGERS.map((pm) => buildNxCommand('<options>', pm)).join(' - \\n')}\n- Each project in your workspace has a file named \\`project.json\\` which contains important project information such as its name, and defines the \"targets\" which can be run for that project, for example building or testing the project\n- Use the command \\`nx reset\\` to reset the Nx daemon when unexpected issues arise\n- After adding dependencies between TypeScript projects, use \\`nx sync\\` to ensure project references are set up correctly\n\n## General Instructions\n\n- Workspaces contain a single \\`package.json\\` file at the root which defines the dependencies for all projects. Therefore when installing dependencies, you must add these to the root workspace using the appropriate command for your package manager:\n - pnpm add -w -D <package>\n - yarn add -D <package>\n - npm install --legacy-peer-deps -D <package>\n - bun install -D <package>\n - (Omit -D for production dependencies)\n- When specifying project names as arguments to generators, prefer the _fully qualified_ project name, for example \\`@workspace-name/project-name\\`. Check the \\`project.json\\` file for the specific package to find its fully qualified name\n- When no generator exists for a specific framework required, use the base \\`ts#project\\` and \\`py#project\\` generators and build on top.\n- Leave the \\`--infra\\` option at its default value unless the user has explicitly instructed otherwise. Generators choose a sensible default type of infrastructure to deploy the project with, so only override \\`--infra\\` when the user has specified a particular requirement.\n\n## Useful Commands\n\n- Fix lint issues with \\`nx run-many --target lint --configuration=fix --all --output-style=stream\\`\n- Build all projects with \\`nx run-many --target build --all --output-style=stream\\`\n- Prefer importing the CDK constructs vended by generators in \\`packages/common/constructs\\` over writing your own\n\n## Best Practices\n\n- After running a generator, use the \\`nx show projects\\` command to check which projects have been added (if any)\n- Carefully examine the files that have been generated and always refer back to the generator guide when working in a generated project\n- Generate all projects into the \\`packages/\\` directory\n- After making changes to your projects, fix linting issues, then run a full build\n- When it's time to start testing a project, suggest to the user that infrastructure is deployed to AWS. For websites, if a runtime-config.json is needed, use the load:runtime-config target after a deployment to point a local website at a sandbox stack.\n\n## Batching Generators\n\nWhen scaffolding several projects in one go, chain generators to avoid a slow dependency install after every generator:\n\n- **Chain generators** with \\`&&\\` in a single command. Pass \\`--prefer-install-dependencies=false\\` on each generator except the last so dependencies install once at the end, for example:\n\n${PACKAGE_MANAGERS.map(\n (pm) => ` \\`\\`\\`bash\n ${buildNxCommand('g @aws/nx-plugin:ts#trpc-api --no-interactive --name=my-app-api --auth=IAM --prefer-install-dependencies=false', pm)} && \\\\\n ${buildNxCommand('g @aws/nx-plugin:ts#react-website --no-interactive --name=my-app-website --prefer-install-dependencies=false', pm)} && \\\\\n ${buildNxCommand('g @aws/nx-plugin:connection --no-interactive --sourceProject=@my-app/my-app-website --targetProject=@my-app/my-app-api --prefer-install-dependencies=false', pm)} && \\\\\n ${buildNxCommand('g @aws/nx-plugin:ts#infra --no-interactive --name=infra', pm)} && \\\\\n ${buildNxCommand('sync', pm)}\n \\`\\`\\``,\n).join('\\n')}\n\n- **\\`--prefer-install-dependencies=false\\`** asks a generator to defer its dependency install so the batch installs once at the end (the final generator above omits the flag and installs everything).\n- **\\`nx sync\\`** is required before building — generators modify TypeScript project references.\n\n## Detailed Guides\n\nPlease refer to the below documentation for important details regarding workspaces and working with TypeScript or Python projects.\n\n${await fetchGuidePages(['workspace', 'typescript-project', 'python-project'], generators)}\n\n `,\n },\n ],\n }),\n );\n};\n"],"names":["IAC_PROVIDERS","buildNxCommand","fetchGuidePages","PACKAGE_MANAGERS","TOOL_SELECTION_GUIDE","addGeneralGuidanceTool","server","generators","registerTool","title","description","content","type","text","join","map","pm"],"mappings":"AAAA;;;CAGC,GAGD,SAASA,aAAa,QAAQ,+BAA4B;AAC1D,SAASC,cAAc,EAAEC,eAAe,QAAQ,uBAAoB;AACpE,SAASC,gBAAgB,QAAQ,eAAY;AAE7C,OAAO,MAAMC,uBAAuB,CAAC;;;;;+FAK0D,CAAC,CAAC;AAEjG;;CAEC,GACD,OAAO,MAAMC,yBAAyB,CACpCC,QACAC;IAEAD,OAAOE,YAAY,CACjB,oBACA;QACEC,OAAO;QACPC,aACE;IACJ,GACA,UAAa,CAAA;YACXC,SAAS;gBACP;oBACEC,MAAM;oBACNC,MAAM,CAAC;;AAEjB,EAAET,qBAAqB;;;;yDAIkC,EAAED,iBAAiBW,IAAI,CAAC,MAAM;+EACR,EAAEd,cAAcc,IAAI,CAAC,MAAM;;;;;;;AAO1G,EAAEX,iBAAiBY,GAAG,CAAC,CAACC,KAAOf,eAAe,aAAae,KAAKF,IAAI,CAAC,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqC9E,EAAEX,iBAAiBY,GAAG,CACpB,CAACC,KAAO,CAAC;EACT,EAAEf,eAAe,kHAAkHe,IAAI;IACrI,EAAEf,eAAe,gHAAgHe,IAAI;IACrI,EAAEf,eAAe,8JAA8Je,IAAI;IACnL,EAAEf,eAAe,2DAA2De,IAAI;IAChF,EAAEf,eAAe,QAAQe,IAAI;QACzB,CAAC,EACPF,IAAI,CAAC,MAAM;;;;;;;;;AASb,EAAE,MAAMZ,gBAAgB;wBAAC;wBAAa;wBAAsB;qBAAiB,EAAEK,YAAY;;IAEvF,CAAC;gBACG;aACD;QACH,CAAA;AAEJ,EAAE"}
|
|
@@ -27,18 +27,12 @@ class AgentCoreA2aClientConfig:
|
|
|
27
27
|
"""SigV4-authenticated A2A client config for a Bedrock AgentCore runtime."""
|
|
28
28
|
region = region_from_arn(agent_runtime_arn)
|
|
29
29
|
credentials = boto3.Session(region_name=region).get_credentials()
|
|
30
|
-
return _config(
|
|
31
|
-
a2a_url_from_arn(agent_runtime_arn), sigv4_auth(credentials, region)
|
|
32
|
-
)
|
|
30
|
+
return _config(a2a_url_from_arn(agent_runtime_arn), sigv4_auth(credentials, region))
|
|
33
31
|
|
|
34
32
|
@staticmethod
|
|
35
|
-
def with_jwt_auth(
|
|
36
|
-
agent_runtime_arn: str, access_token_provider: Callable[[], str]
|
|
37
|
-
) -> tuple[str, ClientConfig]:
|
|
33
|
+
def with_jwt_auth(agent_runtime_arn: str, access_token_provider: Callable[[], str]) -> tuple[str, ClientConfig]:
|
|
38
34
|
"""Bearer-authenticated A2A client config for a Bedrock AgentCore runtime."""
|
|
39
|
-
return _config(
|
|
40
|
-
a2a_url_from_arn(agent_runtime_arn), jwt_auth(access_token_provider)
|
|
41
|
-
)
|
|
35
|
+
return _config(a2a_url_from_arn(agent_runtime_arn), jwt_auth(access_token_provider))
|
|
42
36
|
|
|
43
37
|
@staticmethod
|
|
44
38
|
def without_auth(url: str) -> tuple[str, ClientConfig]:
|
|
@@ -92,9 +86,7 @@ class AgentCoreA2aClientStrands:
|
|
|
92
86
|
) -> A2AAgent:
|
|
93
87
|
"""Bearer-authenticated client for a Bedrock AgentCore runtime."""
|
|
94
88
|
return _build(
|
|
95
|
-
AgentCoreA2aClientConfig.with_jwt_auth(
|
|
96
|
-
agent_runtime_arn, access_token_provider
|
|
97
|
-
),
|
|
89
|
+
AgentCoreA2aClientConfig.with_jwt_auth(agent_runtime_arn, access_token_provider),
|
|
98
90
|
name=name,
|
|
99
91
|
description=description,
|
|
100
92
|
)
|
|
@@ -119,6 +111,7 @@ exports[`py#agent#a2a-connection generator > should match snapshot for agent-con
|
|
|
119
111
|
"import os
|
|
120
112
|
|
|
121
113
|
from strands.agent.a2a_agent import A2AAgent
|
|
114
|
+
|
|
122
115
|
from test_agent_connection.core.agentcore_a2a_client_strands import (
|
|
123
116
|
AgentCoreA2aClientStrands,
|
|
124
117
|
)
|
|
@@ -137,9 +130,7 @@ class RemoteClientStrands:
|
|
|
137
130
|
config = get_agentcore_runtime_config()
|
|
138
131
|
agent_runtime_arn = config.get("agentRuntimes", {}).get("Remote")
|
|
139
132
|
if not agent_runtime_arn:
|
|
140
|
-
raise RuntimeError(
|
|
141
|
-
"No connected agent runtime named 'Remote' found in runtime configuration."
|
|
142
|
-
)
|
|
133
|
+
raise RuntimeError("No connected agent runtime named 'Remote' found in runtime configuration.")
|
|
143
134
|
return AgentCoreA2aClientStrands.with_iam_auth(agent_runtime_arn)
|
|
144
135
|
"
|
|
145
136
|
`;
|
|
@@ -175,9 +166,7 @@ class SessionHeaderAuth(httpx.Auth):
|
|
|
175
166
|
yield from self._inner.auth_flow(request)
|
|
176
167
|
|
|
177
168
|
|
|
178
|
-
def sigv4_auth(
|
|
179
|
-
credentials, region: str, service: str = "bedrock-agentcore"
|
|
180
|
-
) -> httpx.Auth:
|
|
169
|
+
def sigv4_auth(credentials, region: str, service: str = "bedrock-agentcore") -> httpx.Auth:
|
|
181
170
|
"""Session-forwarding SigV4 auth (per-request, body-aware)."""
|
|
182
171
|
return SessionHeaderAuth(SigV4HTTPXAuth(credentials, service, region))
|
|
183
172
|
|
|
@@ -15,9 +15,7 @@ class AgentCoreGatewayMCPClientStrands:
|
|
|
15
15
|
region: str | None = None,
|
|
16
16
|
) -> MCPClient:
|
|
17
17
|
"""Create a gateway MCP client authenticated with IAM SigV4."""
|
|
18
|
-
return MCPClient(
|
|
19
|
-
AgentCoreGatewayMCPTransport.with_iam_auth(gateway_url, region)
|
|
20
|
-
)
|
|
18
|
+
return MCPClient(AgentCoreGatewayMCPTransport.with_iam_auth(gateway_url, region))
|
|
21
19
|
|
|
22
20
|
@staticmethod
|
|
23
21
|
def without_auth(gateway_url: str) -> MCPClient:
|
|
@@ -47,9 +45,7 @@ class AgentCoreGatewayMCPTransport:
|
|
|
47
45
|
region: str | None = None,
|
|
48
46
|
) -> TransportFactory:
|
|
49
47
|
"""Create a gateway MCP transport authenticated with IAM SigV4."""
|
|
50
|
-
return sigv4_transport(
|
|
51
|
-
gateway_url, region or region_from_gateway_url(gateway_url)
|
|
52
|
-
)
|
|
48
|
+
return sigv4_transport(gateway_url, region or region_from_gateway_url(gateway_url))
|
|
53
49
|
|
|
54
50
|
@staticmethod
|
|
55
51
|
def without_auth(gateway_url: str) -> TransportFactory:
|
|
@@ -88,9 +84,7 @@ def sigv4_transport(url: str, region: str) -> TransportFactory:
|
|
|
88
84
|
return _factory(url, sigv4_auth(credentials, region))
|
|
89
85
|
|
|
90
86
|
|
|
91
|
-
def jwt_transport(
|
|
92
|
-
url: str, access_token_provider: Callable[[], str]
|
|
93
|
-
) -> TransportFactory:
|
|
87
|
+
def jwt_transport(url: str, access_token_provider: Callable[[], str]) -> TransportFactory:
|
|
94
88
|
"""Bearer-token transport factory for a resolved AgentCore endpoint."""
|
|
95
89
|
return _factory(url, jwt_auth(access_token_provider))
|
|
96
90
|
|
|
@@ -104,13 +98,14 @@ def no_auth_transport(url: str) -> TransportFactory:
|
|
|
104
98
|
exports[`py#agent#gateway-connection generator > should match snapshot for agent-connection core files > my_gateway_client_strands.py 1`] = `
|
|
105
99
|
"import os
|
|
106
100
|
|
|
101
|
+
from strands.tools.mcp.mcp_client import MCPClient
|
|
102
|
+
|
|
107
103
|
from proj_agent_connection.core.agentcore_gateway_mcp_client_strands import (
|
|
108
104
|
AgentCoreGatewayMCPClientStrands,
|
|
109
105
|
)
|
|
110
106
|
from proj_agent_connection.core.runtime_config import (
|
|
111
107
|
get_agentcore_runtime_config,
|
|
112
108
|
)
|
|
113
|
-
from strands.tools.mcp.mcp_client import MCPClient
|
|
114
109
|
|
|
115
110
|
|
|
116
111
|
class MyGatewayClientStrands:
|
|
@@ -126,15 +121,11 @@ class MyGatewayClientStrands:
|
|
|
126
121
|
@staticmethod
|
|
127
122
|
def create() -> MCPClient:
|
|
128
123
|
if os.environ.get("LOCAL_DEV") == "true":
|
|
129
|
-
return AgentCoreGatewayMCPClientStrands.without_auth(
|
|
130
|
-
gateway_url="http://localhost:8100/mcp"
|
|
131
|
-
)
|
|
124
|
+
return AgentCoreGatewayMCPClientStrands.without_auth(gateway_url="http://localhost:8100/mcp")
|
|
132
125
|
config = get_agentcore_runtime_config()
|
|
133
126
|
gateway_url = config.get("gateways", {}).get("MyGateway")
|
|
134
127
|
if not gateway_url:
|
|
135
|
-
raise RuntimeError(
|
|
136
|
-
"No connected gateway named 'MyGateway' found in runtime configuration."
|
|
137
|
-
)
|
|
128
|
+
raise RuntimeError("No connected gateway named 'MyGateway' found in runtime configuration.")
|
|
138
129
|
return AgentCoreGatewayMCPClientStrands.with_iam_auth(gateway_url=gateway_url)
|
|
139
130
|
"
|
|
140
131
|
`;
|
|
@@ -170,9 +161,7 @@ class SessionHeaderAuth(httpx.Auth):
|
|
|
170
161
|
yield from self._inner.auth_flow(request)
|
|
171
162
|
|
|
172
163
|
|
|
173
|
-
def sigv4_auth(
|
|
174
|
-
credentials, region: str, service: str = "bedrock-agentcore"
|
|
175
|
-
) -> httpx.Auth:
|
|
164
|
+
def sigv4_auth(credentials, region: str, service: str = "bedrock-agentcore") -> httpx.Auth:
|
|
176
165
|
"""Session-forwarding SigV4 auth (per-request, body-aware)."""
|
|
177
166
|
return SessionHeaderAuth(SigV4HTTPXAuth(credentials, service, region))
|
|
178
167
|
|
|
@@ -35,15 +35,9 @@ class AgentCoreMCPClientStrands:
|
|
|
35
35
|
return MCPClient(AgentCoreMCPTransport.with_iam_auth(agent_runtime_arn))
|
|
36
36
|
|
|
37
37
|
@staticmethod
|
|
38
|
-
def with_jwt_auth(
|
|
39
|
-
agent_runtime_arn: str, access_token_provider: Callable[[], str]
|
|
40
|
-
) -> MCPClient:
|
|
38
|
+
def with_jwt_auth(agent_runtime_arn: str, access_token_provider: Callable[[], str]) -> MCPClient:
|
|
41
39
|
"""Bearer-authenticated client for a Bedrock AgentCore runtime."""
|
|
42
|
-
return MCPClient(
|
|
43
|
-
AgentCoreMCPTransport.with_jwt_auth(
|
|
44
|
-
agent_runtime_arn, access_token_provider
|
|
45
|
-
)
|
|
46
|
-
)
|
|
40
|
+
return MCPClient(AgentCoreMCPTransport.with_jwt_auth(agent_runtime_arn, access_token_provider))
|
|
47
41
|
|
|
48
42
|
@staticmethod
|
|
49
43
|
def without_auth(url: str) -> MCPClient:
|
|
@@ -70,14 +64,10 @@ class AgentCoreMCPTransport:
|
|
|
70
64
|
@staticmethod
|
|
71
65
|
def with_iam_auth(agent_runtime_arn: str) -> TransportFactory:
|
|
72
66
|
"""SigV4-authenticated transport for a Bedrock AgentCore runtime."""
|
|
73
|
-
return sigv4_transport(
|
|
74
|
-
mcp_url_from_arn(agent_runtime_arn), region_from_arn(agent_runtime_arn)
|
|
75
|
-
)
|
|
67
|
+
return sigv4_transport(mcp_url_from_arn(agent_runtime_arn), region_from_arn(agent_runtime_arn))
|
|
76
68
|
|
|
77
69
|
@staticmethod
|
|
78
|
-
def with_jwt_auth(
|
|
79
|
-
agent_runtime_arn: str, access_token_provider: Callable[[], str]
|
|
80
|
-
) -> TransportFactory:
|
|
70
|
+
def with_jwt_auth(agent_runtime_arn: str, access_token_provider: Callable[[], str]) -> TransportFactory:
|
|
81
71
|
"""Bearer-authenticated transport for a Bedrock AgentCore runtime."""
|
|
82
72
|
return jwt_transport(mcp_url_from_arn(agent_runtime_arn), access_token_provider)
|
|
83
73
|
|
|
@@ -118,9 +108,7 @@ def sigv4_transport(url: str, region: str) -> TransportFactory:
|
|
|
118
108
|
return _factory(url, sigv4_auth(credentials, region))
|
|
119
109
|
|
|
120
110
|
|
|
121
|
-
def jwt_transport(
|
|
122
|
-
url: str, access_token_provider: Callable[[], str]
|
|
123
|
-
) -> TransportFactory:
|
|
111
|
+
def jwt_transport(url: str, access_token_provider: Callable[[], str]) -> TransportFactory:
|
|
124
112
|
"""Bearer-token transport factory for a resolved AgentCore endpoint."""
|
|
125
113
|
return _factory(url, jwt_auth(access_token_provider))
|
|
126
114
|
|
|
@@ -134,13 +122,14 @@ def no_auth_transport(url: str) -> TransportFactory:
|
|
|
134
122
|
exports[`py#agent#mcp-connection generator > should match snapshot for generated files > inventory_mcp_client_strands.py 1`] = `
|
|
135
123
|
"import os
|
|
136
124
|
|
|
125
|
+
from strands.tools.mcp.mcp_client import MCPClient
|
|
126
|
+
|
|
137
127
|
from proj_agent_connection.core.agentcore_mcp_client_strands import (
|
|
138
128
|
AgentCoreMCPClientStrands,
|
|
139
129
|
)
|
|
140
130
|
from proj_agent_connection.core.runtime_config import (
|
|
141
131
|
get_agentcore_runtime_config,
|
|
142
132
|
)
|
|
143
|
-
from strands.tools.mcp.mcp_client import MCPClient
|
|
144
133
|
|
|
145
134
|
|
|
146
135
|
class InventoryMcpClientStrands:
|
|
@@ -153,9 +142,7 @@ class InventoryMcpClientStrands:
|
|
|
153
142
|
config = get_agentcore_runtime_config()
|
|
154
143
|
agent_runtime_arn = config.get("agentRuntimes", {}).get("InventoryMcp")
|
|
155
144
|
if not agent_runtime_arn:
|
|
156
|
-
raise RuntimeError(
|
|
157
|
-
"No connected MCP server runtime named 'InventoryMcp' found in runtime configuration."
|
|
158
|
-
)
|
|
145
|
+
raise RuntimeError("No connected MCP server runtime named 'InventoryMcp' found in runtime configuration.")
|
|
159
146
|
return AgentCoreMCPClientStrands.with_iam_auth(agent_runtime_arn)
|
|
160
147
|
"
|
|
161
148
|
`;
|
|
@@ -191,9 +178,7 @@ class SessionHeaderAuth(httpx.Auth):
|
|
|
191
178
|
yield from self._inner.auth_flow(request)
|
|
192
179
|
|
|
193
180
|
|
|
194
|
-
def sigv4_auth(
|
|
195
|
-
credentials, region: str, service: str = "bedrock-agentcore"
|
|
196
|
-
) -> httpx.Auth:
|
|
181
|
+
def sigv4_auth(credentials, region: str, service: str = "bedrock-agentcore") -> httpx.Auth:
|
|
197
182
|
"""Session-forwarding SigV4 auth (per-request, body-aware)."""
|
|
198
183
|
return SessionHeaderAuth(SigV4HTTPXAuth(credentials, service, region))
|
|
199
184
|
|
|
@@ -70,9 +70,7 @@ class JsonStreamingResponse(StreamingResponse):
|
|
|
70
70
|
"description": description,
|
|
71
71
|
"content": {
|
|
72
72
|
"application/jsonl": {
|
|
73
|
-
"itemSchema": {
|
|
74
|
-
"$ref": f"#/components/schemas/{item_model.__name__}"
|
|
75
|
-
},
|
|
73
|
+
"itemSchema": {"$ref": f"#/components/schemas/{item_model.__name__}"},
|
|
76
74
|
}
|
|
77
75
|
},
|
|
78
76
|
# Include the model so FastAPI registers the schema in components/schemas
|
|
@@ -93,11 +91,7 @@ async def cors_middleware(request: Request, call_next):
|
|
|
93
91
|
response = await call_next(request)
|
|
94
92
|
|
|
95
93
|
origin = request.headers.get("origin")
|
|
96
|
-
allowed_origins = (
|
|
97
|
-
os.environ.get("ALLOWED_ORIGINS", "").split(",")
|
|
98
|
-
if os.environ.get("ALLOWED_ORIGINS")
|
|
99
|
-
else []
|
|
100
|
-
)
|
|
94
|
+
allowed_origins = os.environ.get("ALLOWED_ORIGINS", "").split(",") if os.environ.get("ALLOWED_ORIGINS") else []
|
|
101
95
|
|
|
102
96
|
is_localhost = origin and urlparse(origin).hostname in ["localhost", "127.0.0.1"]
|
|
103
97
|
is_allowed_origin = origin and origin in allowed_origins
|
|
@@ -124,8 +118,7 @@ async def unhandled_exception_handler(request, err):
|
|
|
124
118
|
metrics.add_metric(name="Failure", unit=MetricUnit.Count, value=1)
|
|
125
119
|
|
|
126
120
|
return JSONResponse(
|
|
127
|
-
status_code=500,
|
|
128
|
-
content=InternalServerErrorDetails(detail="Internal Server Error").model_dump(),
|
|
121
|
+
status_code=500, content=InternalServerErrorDetails(detail="Internal Server Error").model_dump()
|
|
129
122
|
)
|
|
130
123
|
|
|
131
124
|
|
package/src/utils/format.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
-
import type
|
|
5
|
+
import { type Tree } from '@nx/devkit';
|
|
6
6
|
export declare const DEFAULT_BIOME_CONFIG: {
|
|
7
7
|
$schema: string;
|
|
8
8
|
root: boolean;
|
package/src/utils/format.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/ import { Biome } from "@biomejs/js-api/nodejs";
|
|
5
|
+
import { getProjects } from "@nx/devkit";
|
|
5
6
|
import { execFileSync, execSync } from "child_process";
|
|
6
7
|
import { existsSync, readFileSync } from "fs";
|
|
7
8
|
import { createRequire } from "module";
|
|
8
9
|
import path from "path";
|
|
10
|
+
import { readToml } from "./toml.js";
|
|
9
11
|
const require = createRequire(import.meta.url);
|
|
10
12
|
export const DEFAULT_BIOME_CONFIG = {
|
|
11
13
|
$schema: 'https://biomejs.dev/schemas/2.4.16/schema.json',
|
|
@@ -79,10 +81,13 @@ const BIOME_FORMATTABLE_EXTENSIONS = new Set([
|
|
|
79
81
|
const changedFiles = tree.listChanges().filter((file)=>file.type !== 'DELETE').filter((file)=>dir ? file.path.startsWith(dir) : true);
|
|
80
82
|
const pyFiles = changedFiles.filter((file)=>file.path.endsWith('.py'));
|
|
81
83
|
const otherFiles = changedFiles.filter((file)=>BIOME_FORMATTABLE_EXTENSIONS.has(path.extname(file.path)));
|
|
84
|
+
// Resolve each project's ruff settings (module names, line-length) so files
|
|
85
|
+
// are formatted to match the on-disk build (see getPythonProjectRuffConfigs).
|
|
86
|
+
const pythonProjectConfigs = pyFiles.length ? getPythonProjectRuffConfigs(tree) : [];
|
|
82
87
|
// Format Python files with ruff (lint fixes + formatting)
|
|
83
88
|
for (const file of pyFiles){
|
|
84
89
|
try {
|
|
85
|
-
const content = ruffFixAndFormat(file.content.toString('utf-8'), file.path, hasRuffConfigOnDisk(tree, file.path));
|
|
90
|
+
const content = ruffFixAndFormat(file.content.toString('utf-8'), file.path, hasRuffConfigOnDisk(tree, file.path), getOwningProjectRuffConfig(file.path, pythonProjectConfigs));
|
|
86
91
|
tree.write(file.path, content);
|
|
87
92
|
} catch {
|
|
88
93
|
// Silently skip ruff formatting failures
|
|
@@ -263,6 +268,52 @@ function getRuffCommand() {
|
|
|
263
268
|
dir = parent;
|
|
264
269
|
}
|
|
265
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Map each Nx project with a `pyproject.toml` to the ruff settings the on-disk
|
|
273
|
+
* build enforces for it: its top-level module names (from
|
|
274
|
+
* `[tool.hatch.build.targets.wheel].packages`) and its `[tool.ruff].line-length`.
|
|
275
|
+
*/ function getPythonProjectRuffConfigs(tree) {
|
|
276
|
+
const configs = [];
|
|
277
|
+
for (const project of getProjects(tree).values()){
|
|
278
|
+
const pyprojectPath = path.join(project.root, 'pyproject.toml');
|
|
279
|
+
if (tree.exists(pyprojectPath)) {
|
|
280
|
+
try {
|
|
281
|
+
const pyproject = readToml(tree, pyprojectPath);
|
|
282
|
+
const wheelPackages = pyproject?.tool?.hatch?.build?.targets?.wheel?.packages;
|
|
283
|
+
// Record the top-level module segment (`pkg/sub` -> `pkg`), which is
|
|
284
|
+
// all `known-first-party` keys off.
|
|
285
|
+
const modules = Array.isArray(wheelPackages) ? wheelPackages.filter((pkg)=>typeof pkg === 'string' && !!pkg).map((pkg)=>pkg.split('/')[0]) : [];
|
|
286
|
+
const lineLength = pyproject?.tool?.ruff?.['line-length'];
|
|
287
|
+
if (modules.length || typeof lineLength === 'number') {
|
|
288
|
+
configs.push({
|
|
289
|
+
root: project.root.split(path.sep).join('/'),
|
|
290
|
+
modules,
|
|
291
|
+
lineLength: typeof lineLength === 'number' ? lineLength : undefined
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
// Skip projects whose pyproject.toml cannot be parsed
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return configs;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Resolve the ruff config for the project that owns a file (the project with
|
|
303
|
+
* the longest root that is a prefix of the file path). Ruff runs per-project on
|
|
304
|
+
* disk, so a file's settings come from its own project — only its own module is
|
|
305
|
+
* first-party (sibling workspace packages are third-party) and its own
|
|
306
|
+
* line-length applies — and scoping this way keeps in-tree formatting
|
|
307
|
+
* consistent with the on-disk build.
|
|
308
|
+
*/ function getOwningProjectRuffConfig(filePath, configs) {
|
|
309
|
+
let owner;
|
|
310
|
+
for (const config of configs){
|
|
311
|
+
if ((filePath === config.root || filePath.startsWith(`${config.root}/`)) && (!owner || config.root.length > owner.root.length)) {
|
|
312
|
+
owner = config;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return owner;
|
|
316
|
+
}
|
|
266
317
|
/**
|
|
267
318
|
* Run ruff check --fix and ruff format on Python file content via stdin.
|
|
268
319
|
* Applies all configured lint fixes (including import sorting) and formatting.
|
|
@@ -272,13 +323,29 @@ function getRuffCommand() {
|
|
|
272
323
|
* build fails on unsorted imports (I001). In that case we add `--extend-select
|
|
273
324
|
* I` so import sorting matches what the build enforces. When a config does
|
|
274
325
|
* exist we defer to it entirely, honouring the user's rule selection.
|
|
275
|
-
|
|
326
|
+
*
|
|
327
|
+
* `projectConfig` carries the owning project's ruff settings, which ruff cannot
|
|
328
|
+
* detect from the filesystem during generation because the project lives only
|
|
329
|
+
* in the tree. We pass them via `--config` so in-tree formatting matches the
|
|
330
|
+
* on-disk build: `known-first-party` (the project's own modules) keeps its
|
|
331
|
+
* imports in their own group, and `line-length` keeps wrapping consistent (the
|
|
332
|
+
* generated config raises it above ruff's default of 88). These are additive to
|
|
333
|
+
* any on-disk config, so they are safe to pass regardless of `hasConfig`.
|
|
334
|
+
*/ function ruffFixAndFormat(content, filePath, hasConfig, projectConfig) {
|
|
276
335
|
const ruff = getRuffCommand();
|
|
277
336
|
if (!ruff) return content;
|
|
278
337
|
const extendSelect = hasConfig ? '' : ' --extend-select I';
|
|
338
|
+
const configArgs = [];
|
|
339
|
+
if (projectConfig?.modules.length) {
|
|
340
|
+
configArgs.push(`lint.isort.known-first-party = ${JSON.stringify(projectConfig.modules)}`);
|
|
341
|
+
}
|
|
342
|
+
if (typeof projectConfig?.lineLength === 'number') {
|
|
343
|
+
configArgs.push(`line-length = ${projectConfig.lineLength}`);
|
|
344
|
+
}
|
|
345
|
+
const config = configArgs.map((arg)=>` --config ${JSON.stringify(arg)}`).join('');
|
|
279
346
|
// First apply lint fixes (import sorting, unused imports, etc.)
|
|
280
347
|
try {
|
|
281
|
-
const result = execSync(`${ruff} check --fix${extendSelect} --stdin-filename ${filePath} -`, {
|
|
348
|
+
const result = execSync(`${ruff} check --fix${extendSelect}${config} --stdin-filename ${filePath} -`, {
|
|
282
349
|
input: content,
|
|
283
350
|
encoding: 'utf-8',
|
|
284
351
|
stdio: [
|
|
@@ -297,7 +364,7 @@ function getRuffCommand() {
|
|
|
297
364
|
}
|
|
298
365
|
// Then apply formatting
|
|
299
366
|
try {
|
|
300
|
-
content = execSync(`${ruff} format --stdin-filename ${filePath} -`, {
|
|
367
|
+
content = execSync(`${ruff} format${config} --stdin-filename ${filePath} -`, {
|
|
301
368
|
input: content,
|
|
302
369
|
encoding: 'utf-8',
|
|
303
370
|
stdio: [
|
package/src/utils/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../packages/nx-plugin/src/utils/format.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { Biome } from '@biomejs/js-api/nodejs';\nimport type { Tree } from '@nx/devkit';\nimport { execFileSync, execSync } from 'child_process';\nimport { existsSync, readFileSync } from 'fs';\nimport { createRequire } from 'module';\nimport path from 'path';\n\nconst require = createRequire(import.meta.url);\n\nexport const DEFAULT_BIOME_CONFIG = {\n $schema: 'https://biomejs.dev/schemas/2.4.16/schema.json',\n root: true,\n formatter: {\n enabled: true,\n indentStyle: 'space',\n indentWidth: 2,\n lineWidth: 80,\n },\n javascript: {\n formatter: {\n quoteStyle: 'single',\n trailingCommas: 'all',\n },\n },\n css: {\n formatter: {\n quoteStyle: 'single',\n },\n linter: {\n enabled: false,\n },\n },\n linter: {\n enabled: true,\n rules: {\n recommended: false,\n correctness: {\n noUndeclaredDependencies: 'warn',\n },\n },\n },\n assist: {\n actions: {\n source: {\n organizeImports: 'on',\n },\n },\n },\n files: {\n includes: [\n '**',\n '!**/dist',\n '!**/out-tsc',\n '!**/node_modules',\n '!**/.nx',\n '!**/.venv',\n '!**/*.css',\n ],\n },\n};\n\nconst BIOME_FORMATTABLE_EXTENSIONS = new Set([\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n '.mts',\n '.cts',\n '.json',\n '.jsonc',\n '.css',\n]);\n\n/**\n * Format files in the given directory within the tree.\n * Handles both TypeScript/JavaScript/JSON (via biome) and Python (via ruff) files.\n * See https://github.com/nrwl/nx/blob/4cd640a9187954505d12de5b6d76a90d8ce4c2eb/packages/devkit/src/generators/format-files.ts#L11\n */\nexport async function formatFilesInSubtree(\n tree: Tree,\n dir?: string,\n): Promise<void> {\n const changedFiles = tree\n .listChanges()\n .filter((file) => file.type !== 'DELETE')\n .filter((file) => (dir ? file.path.startsWith(dir) : true));\n\n const pyFiles = changedFiles.filter((file) => file.path.endsWith('.py'));\n const otherFiles = changedFiles.filter((file) =>\n BIOME_FORMATTABLE_EXTENSIONS.has(path.extname(file.path)),\n );\n\n // Format Python files with ruff (lint fixes + formatting)\n for (const file of pyFiles) {\n try {\n const content = ruffFixAndFormat(\n file.content.toString('utf-8'),\n file.path,\n hasRuffConfigOnDisk(tree, file.path),\n );\n tree.write(file.path, content);\n } catch {\n // Silently skip ruff formatting failures\n }\n }\n\n if (otherFiles.length === 0) return;\n\n // Use the workspace's own Biome CLI (its version and config) when biome.json\n // exists on disk; otherwise format via the bundled library API with the\n // in-memory tree config. The CLI path does not see in-tree config changes.\n if (existsSync(path.join(tree.root, 'biome.json'))) {\n formatWithBiomeCli(tree, otherFiles);\n } else {\n formatWithBiomeApi(tree, otherFiles);\n }\n}\n\n/**\n * Format files via the workspace's Biome CLI, run from the workspace root so it\n * discovers the on-disk biome.json.\n */\nfunction formatWithBiomeCli(\n tree: Tree,\n files: { path: string; content: Buffer | null }[],\n): void {\n const biome = getBiomeCommand(tree.root);\n if (!biome) {\n // Fall back to the library API if the CLI cannot be resolved\n formatWithBiomeApi(tree, files);\n return;\n }\n\n for (const file of files) {\n try {\n const content = execFileSync(\n biome.command,\n [...biome.args, 'format', `--stdin-file-path=${file.path}`],\n {\n input: file.content?.toString('utf-8') ?? '',\n encoding: 'utf-8',\n cwd: tree.root,\n stdio: ['pipe', 'pipe', 'pipe'],\n },\n );\n tree.write(file.path, content);\n } catch {\n // Leave individual files that fail to format untouched\n }\n }\n}\n\n/**\n * Format files via the bundled Biome library API, applying the in-memory tree\n * config.\n */\nfunction formatWithBiomeApi(\n tree: Tree,\n files: { path: string; content: Buffer | null }[],\n): void {\n try {\n const biome = new Biome();\n const { projectKey } = biome.openProject();\n // Apply the workspace biome.json if it exists in the tree, otherwise the defaults.\n const treeConfig = tree.read('biome.json', 'utf-8');\n biome.applyConfiguration(\n projectKey,\n treeConfig ? JSON.parse(treeConfig) : DEFAULT_BIOME_CONFIG,\n );\n\n for (const file of files) {\n try {\n const { content } = biome.formatContent(\n projectKey,\n file.content?.toString('utf-8') ?? '',\n { filePath: file.path },\n );\n tree.write(file.path, content);\n } catch {\n // Leave individual files that fail to format untouched\n }\n }\n } catch {\n // Silently skip formatting failures\n }\n}\n\ninterface BiomeCommand {\n command: string;\n args: string[];\n}\n\n/**\n * Resolve the `@biomejs/biome` CLI from the user's workspace, falling back to a\n * `biome` binary on the PATH.\n */\nconst _biomeCommands = new Map<string, BiomeCommand | null>();\nfunction getBiomeCommand(root: string): BiomeCommand | undefined {\n if (_biomeCommands.has(root)) {\n return _biomeCommands.get(root) ?? undefined;\n }\n\n // Run via node for cross-platform execution of the bin shim.\n try {\n const pkgJsonPath = require.resolve('@biomejs/biome/package.json', {\n paths: [root, import.meta.dirname],\n });\n const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));\n const binRelative =\n typeof pkgJson.bin === 'string' ? pkgJson.bin : pkgJson.bin?.biome;\n if (binRelative) {\n const binPath = path.join(path.dirname(pkgJsonPath), binRelative);\n const command = { command: process.execPath, args: [binPath] };\n _biomeCommands.set(root, command);\n return command;\n }\n } catch {\n // Fall back to a biome binary on the PATH\n }\n\n try {\n execSync('biome --version', {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const command = { command: 'biome', args: [] };\n _biomeCommands.set(root, command);\n return command;\n } catch {\n _biomeCommands.set(root, null);\n return undefined;\n }\n}\n\n/**\n * Find the ruff command. Tries 'uv run ruff', then 'uvx ruff'.\n * Matches how @nxlv/python runs ruff via the UV provider.\n */\nlet _ruffCommand: string | undefined;\nfunction getRuffCommand(): string | undefined {\n if (_ruffCommand !== undefined) {\n return _ruffCommand || undefined;\n }\n for (const cmd of ['uv run ruff', 'uvx ruff']) {\n try {\n execSync(`${cmd} --version`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n _ruffCommand = cmd;\n return cmd;\n } catch {\n // Try next command\n }\n }\n _ruffCommand = '';\n return undefined;\n}\n\n/**\n * Whether ruff would discover a config on disk for a file, by walking from its\n * directory up to the workspace root looking for `.ruff.toml`, `ruff.toml`, or a\n * `pyproject.toml` with a `[tool.ruff]` section — the same files ruff itself\n * resolves. The walk stops at `tree.root` so a stray config in a parent of the\n * workspace (or the home directory) is never treated as the project's. Used to\n * decide whether to nudge ruff towards import sorting (see\n * {@link ruffFixAndFormat}).\n */\nfunction hasRuffConfigOnDisk(tree: Tree, filePath: string): boolean {\n const root = path.resolve(tree.root);\n let dir = path.resolve(root, path.dirname(filePath));\n while (true) {\n if (\n existsSync(path.join(dir, '.ruff.toml')) ||\n existsSync(path.join(dir, 'ruff.toml'))\n ) {\n return true;\n }\n const pyproject = path.join(dir, 'pyproject.toml');\n if (\n existsSync(pyproject) &&\n readFileSync(pyproject, 'utf-8').includes('[tool.ruff')\n ) {\n return true;\n }\n const parent = path.dirname(dir);\n // Stop once the workspace root has been checked (or we hit the FS root).\n if (dir === root || parent === dir) {\n return false;\n }\n dir = parent;\n }\n}\n\n/**\n * Run ruff check --fix and ruff format on Python file content via stdin.\n * Applies all configured lint fixes (including import sorting) and formatting.\n *\n * When no ruff config exists on disk (`hasConfig` false) ruff falls back to its\n * defaults, which omit isort — but generated projects enable rule `I` and their\n * build fails on unsorted imports (I001). In that case we add `--extend-select\n * I` so import sorting matches what the build enforces. When a config does\n * exist we defer to it entirely, honouring the user's rule selection.\n */\nfunction ruffFixAndFormat(\n content: string,\n filePath: string,\n hasConfig: boolean,\n): string {\n const ruff = getRuffCommand();\n if (!ruff) return content;\n\n const extendSelect = hasConfig ? '' : ' --extend-select I';\n\n // First apply lint fixes (import sorting, unused imports, etc.)\n try {\n const result = execSync(\n `${ruff} check --fix${extendSelect} --stdin-filename ${filePath} -`,\n { input: content, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },\n );\n content = result;\n } catch (e: any) {\n // ruff check exits non-zero when it finds unfixable issues,\n // but stdout still contains the fixed content\n if (e.stdout) {\n content = e.stdout;\n }\n }\n\n // Then apply formatting\n try {\n content = execSync(`${ruff} format --stdin-filename ${filePath} -`, {\n input: content,\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n } catch {\n // Fall through with whatever content we have\n }\n\n return content;\n}\n"],"names":["Biome","execFileSync","execSync","existsSync","readFileSync","createRequire","path","require","url","DEFAULT_BIOME_CONFIG","$schema","root","formatter","enabled","indentStyle","indentWidth","lineWidth","javascript","quoteStyle","trailingCommas","css","linter","rules","recommended","correctness","noUndeclaredDependencies","assist","actions","source","organizeImports","files","includes","BIOME_FORMATTABLE_EXTENSIONS","Set","formatFilesInSubtree","tree","dir","changedFiles","listChanges","filter","file","type","startsWith","pyFiles","endsWith","otherFiles","has","extname","content","ruffFixAndFormat","toString","hasRuffConfigOnDisk","write","length","join","formatWithBiomeCli","formatWithBiomeApi","biome","getBiomeCommand","command","args","input","encoding","cwd","stdio","projectKey","openProject","treeConfig","read","applyConfiguration","JSON","parse","formatContent","filePath","_biomeCommands","Map","get","undefined","pkgJsonPath","resolve","paths","dirname","pkgJson","binRelative","bin","binPath","process","execPath","set","_ruffCommand","getRuffCommand","cmd","pyproject","parent","hasConfig","ruff","extendSelect","result","e","stdout"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../packages/nx-plugin/src/utils/format.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { Biome } from '@biomejs/js-api/nodejs';\nimport { getProjects, type Tree } from '@nx/devkit';\nimport { execFileSync, execSync } from 'child_process';\nimport { existsSync, readFileSync } from 'fs';\nimport { createRequire } from 'module';\nimport path from 'path';\nimport { readToml } from './toml';\n\nconst require = createRequire(import.meta.url);\n\nexport const DEFAULT_BIOME_CONFIG = {\n $schema: 'https://biomejs.dev/schemas/2.4.16/schema.json',\n root: true,\n formatter: {\n enabled: true,\n indentStyle: 'space',\n indentWidth: 2,\n lineWidth: 80,\n },\n javascript: {\n formatter: {\n quoteStyle: 'single',\n trailingCommas: 'all',\n },\n },\n css: {\n formatter: {\n quoteStyle: 'single',\n },\n linter: {\n enabled: false,\n },\n },\n linter: {\n enabled: true,\n rules: {\n recommended: false,\n correctness: {\n noUndeclaredDependencies: 'warn',\n },\n },\n },\n assist: {\n actions: {\n source: {\n organizeImports: 'on',\n },\n },\n },\n files: {\n includes: [\n '**',\n '!**/dist',\n '!**/out-tsc',\n '!**/node_modules',\n '!**/.nx',\n '!**/.venv',\n '!**/*.css',\n ],\n },\n};\n\nconst BIOME_FORMATTABLE_EXTENSIONS = new Set([\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n '.mts',\n '.cts',\n '.json',\n '.jsonc',\n '.css',\n]);\n\n/**\n * Format files in the given directory within the tree.\n * Handles both TypeScript/JavaScript/JSON (via biome) and Python (via ruff) files.\n * See https://github.com/nrwl/nx/blob/4cd640a9187954505d12de5b6d76a90d8ce4c2eb/packages/devkit/src/generators/format-files.ts#L11\n */\nexport async function formatFilesInSubtree(\n tree: Tree,\n dir?: string,\n): Promise<void> {\n const changedFiles = tree\n .listChanges()\n .filter((file) => file.type !== 'DELETE')\n .filter((file) => (dir ? file.path.startsWith(dir) : true));\n\n const pyFiles = changedFiles.filter((file) => file.path.endsWith('.py'));\n const otherFiles = changedFiles.filter((file) =>\n BIOME_FORMATTABLE_EXTENSIONS.has(path.extname(file.path)),\n );\n\n // Resolve each project's ruff settings (module names, line-length) so files\n // are formatted to match the on-disk build (see getPythonProjectRuffConfigs).\n const pythonProjectConfigs = pyFiles.length\n ? getPythonProjectRuffConfigs(tree)\n : [];\n\n // Format Python files with ruff (lint fixes + formatting)\n for (const file of pyFiles) {\n try {\n const content = ruffFixAndFormat(\n file.content.toString('utf-8'),\n file.path,\n hasRuffConfigOnDisk(tree, file.path),\n getOwningProjectRuffConfig(file.path, pythonProjectConfigs),\n );\n tree.write(file.path, content);\n } catch {\n // Silently skip ruff formatting failures\n }\n }\n\n if (otherFiles.length === 0) return;\n\n // Use the workspace's own Biome CLI (its version and config) when biome.json\n // exists on disk; otherwise format via the bundled library API with the\n // in-memory tree config. The CLI path does not see in-tree config changes.\n if (existsSync(path.join(tree.root, 'biome.json'))) {\n formatWithBiomeCli(tree, otherFiles);\n } else {\n formatWithBiomeApi(tree, otherFiles);\n }\n}\n\n/**\n * Format files via the workspace's Biome CLI, run from the workspace root so it\n * discovers the on-disk biome.json.\n */\nfunction formatWithBiomeCli(\n tree: Tree,\n files: { path: string; content: Buffer | null }[],\n): void {\n const biome = getBiomeCommand(tree.root);\n if (!biome) {\n // Fall back to the library API if the CLI cannot be resolved\n formatWithBiomeApi(tree, files);\n return;\n }\n\n for (const file of files) {\n try {\n const content = execFileSync(\n biome.command,\n [...biome.args, 'format', `--stdin-file-path=${file.path}`],\n {\n input: file.content?.toString('utf-8') ?? '',\n encoding: 'utf-8',\n cwd: tree.root,\n stdio: ['pipe', 'pipe', 'pipe'],\n },\n );\n tree.write(file.path, content);\n } catch {\n // Leave individual files that fail to format untouched\n }\n }\n}\n\n/**\n * Format files via the bundled Biome library API, applying the in-memory tree\n * config.\n */\nfunction formatWithBiomeApi(\n tree: Tree,\n files: { path: string; content: Buffer | null }[],\n): void {\n try {\n const biome = new Biome();\n const { projectKey } = biome.openProject();\n // Apply the workspace biome.json if it exists in the tree, otherwise the defaults.\n const treeConfig = tree.read('biome.json', 'utf-8');\n biome.applyConfiguration(\n projectKey,\n treeConfig ? JSON.parse(treeConfig) : DEFAULT_BIOME_CONFIG,\n );\n\n for (const file of files) {\n try {\n const { content } = biome.formatContent(\n projectKey,\n file.content?.toString('utf-8') ?? '',\n { filePath: file.path },\n );\n tree.write(file.path, content);\n } catch {\n // Leave individual files that fail to format untouched\n }\n }\n } catch {\n // Silently skip formatting failures\n }\n}\n\ninterface BiomeCommand {\n command: string;\n args: string[];\n}\n\n/**\n * Resolve the `@biomejs/biome` CLI from the user's workspace, falling back to a\n * `biome` binary on the PATH.\n */\nconst _biomeCommands = new Map<string, BiomeCommand | null>();\nfunction getBiomeCommand(root: string): BiomeCommand | undefined {\n if (_biomeCommands.has(root)) {\n return _biomeCommands.get(root) ?? undefined;\n }\n\n // Run via node for cross-platform execution of the bin shim.\n try {\n const pkgJsonPath = require.resolve('@biomejs/biome/package.json', {\n paths: [root, import.meta.dirname],\n });\n const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));\n const binRelative =\n typeof pkgJson.bin === 'string' ? pkgJson.bin : pkgJson.bin?.biome;\n if (binRelative) {\n const binPath = path.join(path.dirname(pkgJsonPath), binRelative);\n const command = { command: process.execPath, args: [binPath] };\n _biomeCommands.set(root, command);\n return command;\n }\n } catch {\n // Fall back to a biome binary on the PATH\n }\n\n try {\n execSync('biome --version', {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const command = { command: 'biome', args: [] };\n _biomeCommands.set(root, command);\n return command;\n } catch {\n _biomeCommands.set(root, null);\n return undefined;\n }\n}\n\n/**\n * Find the ruff command. Tries 'uv run ruff', then 'uvx ruff'.\n * Matches how @nxlv/python runs ruff via the UV provider.\n */\nlet _ruffCommand: string | undefined;\nfunction getRuffCommand(): string | undefined {\n if (_ruffCommand !== undefined) {\n return _ruffCommand || undefined;\n }\n for (const cmd of ['uv run ruff', 'uvx ruff']) {\n try {\n execSync(`${cmd} --version`, {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n _ruffCommand = cmd;\n return cmd;\n } catch {\n // Try next command\n }\n }\n _ruffCommand = '';\n return undefined;\n}\n\n/**\n * Whether ruff would discover a config on disk for a file, by walking from its\n * directory up to the workspace root looking for `.ruff.toml`, `ruff.toml`, or a\n * `pyproject.toml` with a `[tool.ruff]` section — the same files ruff itself\n * resolves. The walk stops at `tree.root` so a stray config in a parent of the\n * workspace (or the home directory) is never treated as the project's. Used to\n * decide whether to nudge ruff towards import sorting (see\n * {@link ruffFixAndFormat}).\n */\nfunction hasRuffConfigOnDisk(tree: Tree, filePath: string): boolean {\n const root = path.resolve(tree.root);\n let dir = path.resolve(root, path.dirname(filePath));\n while (true) {\n if (\n existsSync(path.join(dir, '.ruff.toml')) ||\n existsSync(path.join(dir, 'ruff.toml'))\n ) {\n return true;\n }\n const pyproject = path.join(dir, 'pyproject.toml');\n if (\n existsSync(pyproject) &&\n readFileSync(pyproject, 'utf-8').includes('[tool.ruff')\n ) {\n return true;\n }\n const parent = path.dirname(dir);\n // Stop once the workspace root has been checked (or we hit the FS root).\n if (dir === root || parent === dir) {\n return false;\n }\n dir = parent;\n }\n}\n\ninterface PythonProjectRuffConfig {\n /** Project root, normalised to use forward slashes. */\n readonly root: string;\n /** Top-level importable module names declared by the project. */\n readonly modules: string[];\n /** The project's `[tool.ruff].line-length`, if set. */\n readonly lineLength?: number;\n}\n\n/**\n * Map each Nx project with a `pyproject.toml` to the ruff settings the on-disk\n * build enforces for it: its top-level module names (from\n * `[tool.hatch.build.targets.wheel].packages`) and its `[tool.ruff].line-length`.\n */\nfunction getPythonProjectRuffConfigs(tree: Tree): PythonProjectRuffConfig[] {\n const configs: PythonProjectRuffConfig[] = [];\n\n for (const project of getProjects(tree).values()) {\n const pyprojectPath = path.join(project.root, 'pyproject.toml');\n if (tree.exists(pyprojectPath)) {\n try {\n const pyproject = readToml(tree, pyprojectPath) as any;\n const wheelPackages: unknown =\n pyproject?.tool?.hatch?.build?.targets?.wheel?.packages;\n // Record the top-level module segment (`pkg/sub` -> `pkg`), which is\n // all `known-first-party` keys off.\n const modules = Array.isArray(wheelPackages)\n ? wheelPackages\n .filter((pkg): pkg is string => typeof pkg === 'string' && !!pkg)\n .map((pkg) => pkg.split('/')[0])\n : [];\n const lineLength: unknown = pyproject?.tool?.ruff?.['line-length'];\n if (modules.length || typeof lineLength === 'number') {\n configs.push({\n root: project.root.split(path.sep).join('/'),\n modules,\n lineLength: typeof lineLength === 'number' ? lineLength : undefined,\n });\n }\n } catch {\n // Skip projects whose pyproject.toml cannot be parsed\n }\n }\n }\n\n return configs;\n}\n\n/**\n * Resolve the ruff config for the project that owns a file (the project with\n * the longest root that is a prefix of the file path). Ruff runs per-project on\n * disk, so a file's settings come from its own project — only its own module is\n * first-party (sibling workspace packages are third-party) and its own\n * line-length applies — and scoping this way keeps in-tree formatting\n * consistent with the on-disk build.\n */\nfunction getOwningProjectRuffConfig(\n filePath: string,\n configs: PythonProjectRuffConfig[],\n): PythonProjectRuffConfig | undefined {\n let owner: PythonProjectRuffConfig | undefined;\n for (const config of configs) {\n if (\n (filePath === config.root || filePath.startsWith(`${config.root}/`)) &&\n (!owner || config.root.length > owner.root.length)\n ) {\n owner = config;\n }\n }\n return owner;\n}\n\n/**\n * Run ruff check --fix and ruff format on Python file content via stdin.\n * Applies all configured lint fixes (including import sorting) and formatting.\n *\n * When no ruff config exists on disk (`hasConfig` false) ruff falls back to its\n * defaults, which omit isort — but generated projects enable rule `I` and their\n * build fails on unsorted imports (I001). In that case we add `--extend-select\n * I` so import sorting matches what the build enforces. When a config does\n * exist we defer to it entirely, honouring the user's rule selection.\n *\n * `projectConfig` carries the owning project's ruff settings, which ruff cannot\n * detect from the filesystem during generation because the project lives only\n * in the tree. We pass them via `--config` so in-tree formatting matches the\n * on-disk build: `known-first-party` (the project's own modules) keeps its\n * imports in their own group, and `line-length` keeps wrapping consistent (the\n * generated config raises it above ruff's default of 88). These are additive to\n * any on-disk config, so they are safe to pass regardless of `hasConfig`.\n */\nfunction ruffFixAndFormat(\n content: string,\n filePath: string,\n hasConfig: boolean,\n projectConfig?: PythonProjectRuffConfig,\n): string {\n const ruff = getRuffCommand();\n if (!ruff) return content;\n\n const extendSelect = hasConfig ? '' : ' --extend-select I';\n const configArgs: string[] = [];\n if (projectConfig?.modules.length) {\n configArgs.push(\n `lint.isort.known-first-party = ${JSON.stringify(projectConfig.modules)}`,\n );\n }\n if (typeof projectConfig?.lineLength === 'number') {\n configArgs.push(`line-length = ${projectConfig.lineLength}`);\n }\n const config = configArgs\n .map((arg) => ` --config ${JSON.stringify(arg)}`)\n .join('');\n\n // First apply lint fixes (import sorting, unused imports, etc.)\n try {\n const result = execSync(\n `${ruff} check --fix${extendSelect}${config} --stdin-filename ${filePath} -`,\n { input: content, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },\n );\n content = result;\n } catch (e: any) {\n // ruff check exits non-zero when it finds unfixable issues,\n // but stdout still contains the fixed content\n if (e.stdout) {\n content = e.stdout;\n }\n }\n\n // Then apply formatting\n try {\n content = execSync(\n `${ruff} format${config} --stdin-filename ${filePath} -`,\n {\n input: content,\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n },\n );\n } catch {\n // Fall through with whatever content we have\n }\n\n return content;\n}\n"],"names":["Biome","getProjects","execFileSync","execSync","existsSync","readFileSync","createRequire","path","readToml","require","url","DEFAULT_BIOME_CONFIG","$schema","root","formatter","enabled","indentStyle","indentWidth","lineWidth","javascript","quoteStyle","trailingCommas","css","linter","rules","recommended","correctness","noUndeclaredDependencies","assist","actions","source","organizeImports","files","includes","BIOME_FORMATTABLE_EXTENSIONS","Set","formatFilesInSubtree","tree","dir","changedFiles","listChanges","filter","file","type","startsWith","pyFiles","endsWith","otherFiles","has","extname","pythonProjectConfigs","length","getPythonProjectRuffConfigs","content","ruffFixAndFormat","toString","hasRuffConfigOnDisk","getOwningProjectRuffConfig","write","join","formatWithBiomeCli","formatWithBiomeApi","biome","getBiomeCommand","command","args","input","encoding","cwd","stdio","projectKey","openProject","treeConfig","read","applyConfiguration","JSON","parse","formatContent","filePath","_biomeCommands","Map","get","undefined","pkgJsonPath","resolve","paths","dirname","pkgJson","binRelative","bin","binPath","process","execPath","set","_ruffCommand","getRuffCommand","cmd","pyproject","parent","configs","project","values","pyprojectPath","exists","wheelPackages","tool","hatch","build","targets","wheel","packages","modules","Array","isArray","pkg","map","split","lineLength","ruff","push","sep","owner","config","hasConfig","projectConfig","extendSelect","configArgs","stringify","arg","result","e","stdout"],"mappings":"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"}
|