diff --git a/LOCAL_DEV_SETUP.md b/LOCAL_DEV_SETUP.md new file mode 100644 index 000000000..66116d90c --- /dev/null +++ b/LOCAL_DEV_SETUP.md @@ -0,0 +1,122 @@ +# Local Development Setup + +This guide explains how to set up the agentcore-cli for local development when the L3 constructs package is not yet published to npm. + +## Prerequisites + +- Node.js >= 20 +- npm +- Both packages cloned as siblings: + ``` + workspace/ + ├── agentcore-cli/ + └── agentcore-l3-cdk-constructs/ + ``` + +## Setup Steps + +### 1. Install and Build the L3 Constructs Package + +```bash +cd agentcore-l3-cdk-constructs +npm install +npm run build +``` + +### 2. Create Global npm Link + +```bash +npm link +``` + +This creates a global symlink that makes `@aws/agentcore-l3-cdk-constructs` available to other local projects. + +### 3. Build the CLI + +```bash +cd ../agentcore-cli +npm install +npm run build +``` + +### 4. Create a Test Project + +```bash +npm run cli create +``` + +Follow the prompts to create a new project. + +### 5. Link the L3 Constructs in the Generated Project + +The generated CDK project includes a postinstall script that automatically attempts to link `@aws/agentcore-l3-cdk-constructs`. However, if npm install was run before you created the global link, you may need to manually link it: + +```bash +cd /agentcore/cdk +npm link @aws/agentcore-l3-cdk-constructs +``` + +Alternatively, you can re-run npm install to trigger the postinstall script: + +```bash +npm install +``` + +### 6. Build and Test + +```bash +npm run build +``` + +## How npm link Works + +1. `npm link` in the L3 package creates a global symlink +2. `npm link @aws/agentcore-l3-cdk-constructs` in the CDK project creates a local symlink to the global one +3. Changes to the L3 package are immediately reflected (after rebuilding) + +## Troubleshooting + +### "Cannot find module" errors + +Make sure you've built the L3 constructs package: +```bash +cd agentcore-l3-cdk-constructs +npm run build +``` + +### Link not working + +Re-create the links: +```bash +# In L3 constructs +npm unlink +npm link + +# In generated CDK project +npm unlink @aws/agentcore-l3-cdk-constructs +npm link @aws/agentcore-l3-cdk-constructs +``` + +### Changes not reflected + +Rebuild the L3 constructs package: +```bash +cd agentcore-l3-cdk-constructs +npm run build +``` + +## Alternative: Using LOCAL_L3_PATH + +If you prefer, you can set the `LOCAL_L3_PATH` environment variable before running create: + +```bash +# Windows PowerShell +$env:LOCAL_L3_PATH = "C:\path\to\agentcore-l3-cdk-constructs" +npm run cli create + +# Windows CMD +set LOCAL_L3_PATH=C:\path\to\agentcore-l3-cdk-constructs +npm run cli create +``` + +This will automatically use `file:` protocol in package.json instead of requiring npm link. diff --git a/package-lock.json b/package-lock.json index 895b2c479..48e023728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -209,6 +209,7 @@ "semver" ], "license": "Apache-2.0", + "peer": true, "dependencies": { "jsonschema": "~1.4.1", "semver": "^7.7.3" @@ -827,6 +828,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.975.0.tgz", "integrity": "sha512-xPFcBlpTDuTod9zAAnEsbezFOOqMfQfcd9RCl1LL4Q+qjmazuBSqlnzGE3Djr8Ax/PTV0TR3H2LuepO/ygXwsA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -1494,6 +1496,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.975.0.tgz", "integrity": "sha512-aF1M/iMD29BPcpxjqoym0YFa4WR9Xie1/IhVumwOGH6TB45DaqYO7vLwantDBcYNRn/cZH6DFHksO7RmwTFBhw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", @@ -2720,6 +2723,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -5397,6 +5401,7 @@ "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5414,6 +5419,7 @@ "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5453,6 +5459,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -6038,6 +6045,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7020,6 +7028,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7294,6 +7303,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=20" } @@ -7326,7 +7336,8 @@ "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.5.tgz", "integrity": "sha512-fOoP70YLevMZr5avJHx2DU3LNYmC6wM8OwdrNewMZou1kZnPGOeVzBrRjZNgFDHUlulYUjkpFRSpTE3D+n+ZSg==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -7873,6 +7884,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8059,6 +8071,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9244,6 +9257,7 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-6.6.0.tgz", "integrity": "sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", @@ -11007,6 +11021,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11092,6 +11107,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11178,6 +11194,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12619,6 +12636,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -12742,6 +12760,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12842,6 +12861,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -13065,6 +13085,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13134,24 +13155,6 @@ } } }, - "node_modules/vitest/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -13478,6 +13481,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index e23386abf..36e502436 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build": "npm run build:lib && npm run build:cli && npm run build:assets", "build:lib": "tsc -p tsconfig.build.json", "build:cli": "node esbuild.config.mjs", - "build:assets": "rsync -a --exclude='/AGENTS.md' src/assets/ dist/assets/", + "build:assets": "node scripts/copy-assets.mjs", "cli": "npx tsx src/cli/index.ts", "typecheck": "tsc --noEmit", "lint": "eslint src/", @@ -29,7 +29,7 @@ "format:check": "prettier --check .", "secrets:check": "secretlint '**/*'", "security:audit": "npm audit --audit-level=high", - "clean": "rm -rf dist", + "clean": "node -e \"require('fs').rmSync('dist', {recursive: true, force: true})\"", "prepare": "husky", "test": "vitest run", "test:watch": "vitest", diff --git a/scripts/copy-assets.mjs b/scripts/copy-assets.mjs new file mode 100644 index 000000000..a58b3ea48 --- /dev/null +++ b/scripts/copy-assets.mjs @@ -0,0 +1,50 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const srcDir = path.join(__dirname, '..', 'src', 'assets'); +const destDir = path.join(__dirname, '..', 'dist', 'assets'); + +/** + * Recursively copy directory contents, excluding specified files at root level only + * @param {string} src - Source directory + * @param {string} dest - Destination directory + * @param {string[]} excludeAtRoot - Files to exclude only at the root level (e.g., 'AGENTS.md') + * @param {boolean} isRoot - Whether this is the root level call + */ +function copyDir(src, dest, excludeAtRoot = [], isRoot = true) { + // Create destination directory if it doesn't exist + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + // Skip excluded files only at root level + if (isRoot && excludeAtRoot.includes(entry.name)) { + continue; + } + + if (entry.isDirectory()) { + copyDir(srcPath, destPath, excludeAtRoot, false); + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + +try { + console.log('Copying assets...'); + copyDir(srcDir, destDir, ['AGENTS.md']); + console.log('Assets copied successfully!'); +} catch (error) { + console.error('Error copying assets:', error); + process.exit(1); +} diff --git a/src/assets/cdk/package.json b/src/assets/cdk/package.json index 11702f6f1..47ee2b1a0 100644 --- a/src/assets/cdk/package.json +++ b/src/assets/cdk/package.json @@ -25,7 +25,6 @@ }, "dependencies": { "aws-cdk-lib": "2.234.1", - "constructs": "^10.0.0", - "@aws/agentcore-l3-cdk-constructs": "^0.1.0" + "constructs": "^10.0.0" } } diff --git a/src/assets/python/autogen/base/pyproject.toml b/src/assets/python/autogen/base/pyproject.toml index 58d8bdd63..08c795a0c 100644 --- a/src/assets/python/autogen/base/pyproject.toml +++ b/src/assets/python/autogen/base/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "opentelemetry-distro", "opentelemetry-exporter-otlp", "bedrock-agentcore >= 1.0.3", + "botocore[crt] >= 1.35.0", "python-dotenv >= 1.0.1", "tiktoken", {{#if (eq modelProvider "Bedrock")}} diff --git a/src/assets/python/crewai/base/pyproject.toml b/src/assets/python/crewai/base/pyproject.toml index 34b068093..8b7482956 100644 --- a/src/assets/python/crewai/base/pyproject.toml +++ b/src/assets/python/crewai/base/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "crewai-tools[mcp] >= 1.3.0", "mcp >= 1.20.0", "bedrock-agentcore >= 1.0.3", + "botocore[crt] >= 1.35.0", "python-dotenv >= 1.0.1", {{#if (eq modelProvider "Bedrock")}} "crewai[tools,bedrock] >= 1.3.0", diff --git a/src/assets/python/googleadk/base/pyproject.toml b/src/assets/python/googleadk/base/pyproject.toml index 973659c46..75c57194f 100644 --- a/src/assets/python/googleadk/base/pyproject.toml +++ b/src/assets/python/googleadk/base/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "opentelemetry-exporter-otlp", "google-adk >= 1.17.0", "bedrock-agentcore >= 1.0.3", + "botocore[crt] >= 1.35.0", "python-dotenv >= 1.0.1", ] diff --git a/src/assets/python/langchain_langgraph/base/pyproject.toml b/src/assets/python/langchain_langgraph/base/pyproject.toml index 3f751acbb..a793396cd 100644 --- a/src/assets/python/langchain_langgraph/base/pyproject.toml +++ b/src/assets/python/langchain_langgraph/base/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "langchain >= 1.0.3", "tiktoken == 0.11.0", "bedrock-agentcore >= 1.0.3", + "botocore[crt] >= 1.35.0", "python-dotenv >= 1.0.1", {{#if (eq modelProvider "Bedrock")}} "langchain-aws >= 1.0.0", diff --git a/src/assets/python/openaiagents/base/pyproject.toml b/src/assets/python/openaiagents/base/pyproject.toml index 2401f80af..dfa91ab1c 100644 --- a/src/assets/python/openaiagents/base/pyproject.toml +++ b/src/assets/python/openaiagents/base/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "opentelemetry-exporter-otlp", "openai-agents >= 0.4.2", "bedrock-agentcore >= 1.0.3", + "botocore[crt] >= 1.35.0", "python-dotenv >= 1.0.1", ] diff --git a/src/assets/python/strands/base/pyproject.toml b/src/assets/python/strands/base/pyproject.toml index 3c3e085a5..7ef2f47d7 100644 --- a/src/assets/python/strands/base/pyproject.toml +++ b/src/assets/python/strands/base/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "opentelemetry-distro", "opentelemetry-exporter-otlp", "bedrock-agentcore >= 1.0.3", + "botocore[crt] >= 1.35.0", "google-generativeai >= 0.5.0", "openai >= 1.0.0", "python-dotenv >= 1.0.1", diff --git a/src/assets/static/strands-bedrock/pyproject.toml b/src/assets/static/strands-bedrock/pyproject.toml index 5a0658508..e82e46328 100644 --- a/src/assets/static/strands-bedrock/pyproject.toml +++ b/src/assets/static/strands-bedrock/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "opentelemetry-distro", "opentelemetry-exporter-otlp", "bedrock-agentcore >= 1.0.3", + "botocore[crt] >= 1.35.0", "mcp >= 1.19.0", "pytest >= 7.0.0", "pytest-asyncio >= 0.21.0", diff --git a/src/cli/AGENTS.md b/src/cli/AGENTS.md index b1e288767..f606eea78 100644 --- a/src/cli/AGENTS.md +++ b/src/cli/AGENTS.md @@ -78,3 +78,46 @@ There should not be significant logic defined in the `commands/` directory. Bias towards initial values over placeholders. Unless a field is optional, the initial value allows the user to just accept the value and keep moving. For something like an AWS accountID, an initial value would be inappropriate. + +## Cross-Platform Development + +The CLI is designed to work seamlessly on both Windows and Unix-like systems (Linux, macOS). All code should be +cross-platform compatible. + +### Platform Abstraction + +Use utilities from `lib/utils/platform.ts` to handle platform differences: + +```typescript +import { getVenvExecutable, isWindows } from '../../lib/utils/platform'; + +// Get correct path to Python venv executables +const uvicorn = getVenvExecutable('.venv', 'uvicorn'); +// Unix: .venv/bin/uvicorn +// Windows: .venv\Scripts\uvicorn.exe +``` + +### Cross-Platform Guidelines + +1. **Never hardcode Unix-specific paths or commands** + - ❌ `.venv/bin/python`, `rm -rf`, `rsync` + - ✅ Use `getVenvExecutable()`, Node.js `fs` APIs, or cross-platform npm packages + +2. **Use platform utilities instead of direct checks** + - ❌ `process.platform === 'win32'` + - ✅ `import { isWindows } from '../../lib/utils/platform'` + +3. **Test on both platforms** + - Windows has different path separators, executable extensions, and shell commands + - Python venv structure differs (bin/ vs Scripts/) + - PTY/terminal features may not be available on Windows + +4. **Handle platform-specific features gracefully** + - Example: PTY via `script` command is Unix-only, fall back to one-shot execution on Windows + - Document platform limitations in code comments + +5. **Use Node.js built-ins for file operations** + - Prefer `fs`, `path`, `child_process` over shell commands + - These are cross-platform by design + +See `src/lib/AGENTS.md` for detailed documentation on platform utilities and examples. diff --git a/src/cli/cdk/local-cdk-project.ts b/src/cli/cdk/local-cdk-project.ts index e8616c59e..324fbad1e 100644 --- a/src/cli/cdk/local-cdk-project.ts +++ b/src/cli/cdk/local-cdk-project.ts @@ -32,6 +32,7 @@ export class LocalCdkProject { */ exists(): boolean { const packageJson = path.join(this.projectDir, 'package.json'); + // eslint-disable-next-line security/detect-non-literal-fs-filename return fs.existsSync(this.projectDir) && fs.existsSync(packageJson); } @@ -40,11 +41,13 @@ export class LocalCdkProject { * Throws an error if the project is missing or invalid. */ validate(): void { + // eslint-disable-next-line security/detect-non-literal-fs-filename if (!fs.existsSync(this.projectDir)) { throw new Error(`CDK project not found at ${this.projectDir}. Run 'agentcore create' first.`); } const packageJson = path.join(this.projectDir, 'package.json'); + // eslint-disable-next-line security/detect-non-literal-fs-filename if (!fs.existsSync(packageJson)) { throw new Error(`Invalid CDK project: missing package.json in ${this.projectDir}`); } @@ -71,4 +74,59 @@ export class LocalCdkProject { throw new Error(errorOutput); } } + + /** + * Ensure the L3 constructs package is available. + * Handles multiple scenarios: + * 1. Local development: tries npm link + * 2. Beta testing: package installed globally from tarball + * 3. Production: package available from npm registry + */ + async ensureL3Link(): Promise { + // First, check if the package is already available (installed or linked) + const checkResult = await runSubprocessCapture('npm', ['list', '@aws/agentcore-l3-cdk-constructs', '--depth=0'], { + cwd: this.projectDir, + }); + + // If package is already available, we're good + if (checkResult.code === 0 && !checkResult.stdout.includes('UNMET')) { + return; + } + + // Try to link the package (works for local development) + const linkResult = await runSubprocessCapture('npm', ['link', '@aws/agentcore-l3-cdk-constructs'], { + cwd: this.projectDir, + }); + + if (linkResult.code === 0) { + return; // Link succeeded + } + + // Link failed - check if package is globally installed (beta testing scenario) + const globalListResult = await runSubprocessCapture( + 'npm', + ['list', '-g', '@aws/agentcore-l3-cdk-constructs', '--depth=0'], + { + cwd: this.projectDir, + } + ); + + if (globalListResult.code === 0 && !globalListResult.stdout.includes('(empty)')) { + // Package is globally installed, try to install it locally from global + const installResult = await runSubprocessCapture('npm', ['install', '@aws/agentcore-l3-cdk-constructs'], { + cwd: this.projectDir, + }); + + if (installResult.code === 0) { + return; // Successfully installed from global/registry + } + } + + // If we get here, the package isn't available + throw new Error( + '@aws/agentcore-l3-cdk-constructs is not available. ' + + 'For local development, run: npm link @aws/agentcore-l3-cdk-constructs. ' + + 'For beta testing, ensure the package is installed globally first.' + ); + } } diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 09e357aa8..f55b37771 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -63,6 +63,11 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise void): boolean { const venvPath = join(cwd, '.venv'); - const uvicornPath = join(venvPath, 'bin', 'uvicorn'); + const uvicornPath = getVenvExecutable(venvPath, 'uvicorn'); // Check if venv and uvicorn already exist if (existsSync(uvicornPath)) { @@ -92,7 +93,7 @@ export function spawnDevServer(options: SpawnDevServerOptions): ChildProcess | n } // For Python, use the venv's uvicorn directly to avoid PATH issues - const cmd = isPython ? join(cwd, '.venv', 'bin', 'uvicorn') : 'npx'; + const cmd = isPython ? getVenvExecutable(join(cwd, '.venv'), 'uvicorn') : 'npx'; const args = isPython ? [convertEntrypointToModule(module), '--reload', '--host', '127.0.0.1', '--port', String(port)] : ['tsx', 'watch', (module.split(':')[0] ?? module).replace(/\./g, '/') + '.ts']; diff --git a/src/cli/templates/CDKRenderer.ts b/src/cli/templates/CDKRenderer.ts index 39ed57900..206ba5b8f 100644 --- a/src/cli/templates/CDKRenderer.ts +++ b/src/cli/templates/CDKRenderer.ts @@ -157,13 +157,9 @@ export class CDKRenderer { delete pkg.scripts.postinstall; } } else { - // Production: use npm link - const distroConfig = getDistroConfig(); - const packageName = distroConfig.packageName; - - if (pkg.scripts?.postinstall) { - pkg.scripts.postinstall = `npm link ${packageName} 2>/dev/null || echo 'Note: If CDK synth fails, run: npm link ${packageName}'`; - } + // Production: use npm link with the L3 constructs package + // Note: The template already has the correct postinstall script, so we don't need to modify it + // Just leave it as-is from the template } await fs.writeFile(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8'); diff --git a/src/cli/tui/hooks/useDevServer.ts b/src/cli/tui/hooks/useDevServer.ts index 3effbcb40..76375f5d6 100644 --- a/src/cli/tui/hooks/useDevServer.ts +++ b/src/cli/tui/hooks/useDevServer.ts @@ -45,6 +45,8 @@ export function useDevServer(options: { workingDir: string; port: number; agentN const loggerRef = useRef(null); // Track instance ID to ignore callbacks from stale server instances const instanceIdRef = useRef(0); + // Track if we're intentionally restarting to ignore exit callbacks + const isRestartingRef = useRef(false); const addLog = (level: LogEntry['level'], message: string) => { setLogs(prev => [...prev.slice(-MAX_LOG_ENTRIES), { level, message }]); @@ -125,6 +127,12 @@ export function useDevServer(options: { workingDir: string; port: number; agentN // Ignore exit events from stale server instances if (instanceIdRef.current !== currentInstanceId) return; + // Ignore exit events when intentionally restarting + if (isRestartingRef.current) { + isRestartingRef.current = false; + return; + } + setStatus(code === 0 ? 'stopped' : 'error'); addLog('system', `Server exited (code ${code})`); }, @@ -191,6 +199,7 @@ export function useDevServer(options: { workingDir: string; port: number; agentN const restart = () => { addLog('system', 'Restarting server...'); + isRestartingRef.current = true; killServer(serverRef.current); setStatus('starting'); setRestartTrigger(t => t + 1); diff --git a/src/lib/AGENTS.md b/src/lib/AGENTS.md index 276516476..38434304a 100644 --- a/src/lib/AGENTS.md +++ b/src/lib/AGENTS.md @@ -14,6 +14,44 @@ Both the CLI and the CDK require this functionality. **Util**: Subprocess command utilities, zod utilities, and OS utilities. +## Cross-Platform Support + +The CLI is designed to work on both Windows and Unix-like systems (Linux, macOS). Platform-specific differences are +abstracted through utilities in `lib/utils/platform.ts`. + +### Platform Utilities + +**Key utilities for cross-platform development:** + +- `isWindows`, `isMacOS`, `isLinux` - Platform detection flags +- `getVenvExecutable(venvPath, executable)` - Get correct path to Python venv executables + - Unix: `.venv/bin/python`, `.venv/bin/uvicorn` + - Windows: `.venv\Scripts\python.exe`, `.venv\Scripts\uvicorn.exe` +- `getShellCommand()` - Get platform-appropriate shell command +- `getShellArgs(command)` - Get platform-appropriate shell arguments +- `normalizeCommand(command)` - Add .exe extension on Windows when needed + +### Guidelines for Cross-Platform Code + +1. **Never hardcode Unix paths** - Use `getVenvExecutable()` for Python venv paths +2. **Use platform utilities** - Import from `lib/utils/platform` instead of checking `process.platform` directly +3. **Test on both platforms** - Ensure features work on Windows and Unix +4. **Avoid Unix-specific commands** - Use Node.js APIs or cross-platform alternatives (e.g., Node.js fs instead of `rm -rf`) +5. **Document platform differences** - Add comments explaining platform-specific behavior + +### Example + +```typescript +import { getVenvExecutable, isWindows } from '../lib/utils/platform'; + +// ❌ BAD: Hardcoded Unix path +const uvicorn = join(venvPath, 'bin', 'uvicorn'); + +// ✅ GOOD: Cross-platform +const uvicorn = getVenvExecutable(venvPath, 'uvicorn'); +// Returns: .venv/bin/uvicorn on Unix, .venv\Scripts\uvicorn.exe on Windows +``` + ## Future Direction Functionality such as imperative code implementations to write secret values into the AgentCore Identity primitive would diff --git a/src/lib/utils/platform.ts b/src/lib/utils/platform.ts index 9c9ec8ee6..3bf2f1d36 100644 --- a/src/lib/utils/platform.ts +++ b/src/lib/utils/platform.ts @@ -1 +1,86 @@ +import { join } from 'node:path'; + +/** + * Platform detection utilities and cross-platform path helpers. + * + * This module provides utilities to handle platform-specific differences + * between Windows and Unix-like systems (Linux, macOS). + * + * Key differences handled: + * - Python venv structure: bin/ (Unix) vs Scripts/ (Windows) + * - Executable extensions: none (Unix) vs .exe, .cmd, .bat (Windows) + * - Shell commands: sh/bash (Unix) vs cmd/powershell (Windows) + */ + export const isWindows = process.platform === 'win32'; +export const isMacOS = process.platform === 'darwin'; +export const isLinux = process.platform === 'linux'; + +/** + * Get the path to an executable in a Python virtual environment. + * + * Python virtual environments have different structures on different platforms: + * - Unix (Linux/macOS): .venv/bin/python, .venv/bin/uvicorn + * - Windows: .venv\Scripts\python.exe, .venv\Scripts\uvicorn.exe + * + * @param venvPath - Path to the virtual environment directory (e.g., '.venv') + * @param executable - Name of the executable without extension (e.g., 'python', 'uvicorn') + * @returns Full path to the executable with correct directory and extension + * + * @example + * ```ts + * // On Unix: /path/to/project/.venv/bin/uvicorn + * // On Windows: C:\path\to\project\.venv\Scripts\uvicorn.exe + * const uvicornPath = getVenvExecutable('.venv', 'uvicorn'); + * ``` + */ +export function getVenvExecutable(venvPath: string, executable: string): string { + const binDir = isWindows ? 'Scripts' : 'bin'; + const ext = isWindows ? '.exe' : ''; + return join(venvPath, binDir, executable + ext); +} + +/** + * Get the appropriate shell command for the current platform. + * + * @returns The default shell command ('cmd' on Windows, 'sh' on Unix) + */ +export function getShellCommand(): string { + return isWindows ? 'cmd' : (process.env.SHELL ?? '/bin/sh'); +} + +/** + * Get the appropriate shell arguments for executing a command. + * + * @param command - The command to execute + * @returns Array of arguments to pass to the shell + * + * @example + * ```ts + * // On Unix: ['-c', 'echo hello'] + * // On Windows: ['/c', 'echo hello'] + * const args = getShellArgs('echo hello'); + * spawn(getShellCommand(), args); + * ``` + */ +export function getShellArgs(command: string): string[] { + return isWindows ? ['/c', command] : ['-c', command]; +} + +/** + * Normalize a command for cross-platform execution. + * Adds .exe extension on Windows if needed. + * + * @param command - The command name + * @returns The command with appropriate extension + */ +export function normalizeCommand(command: string): string { + if (isWindows && !command.endsWith('.exe') && !command.endsWith('.cmd') && !command.endsWith('.bat')) { + // Check if it's a known command that needs .exe + const exeCommands = ['python', 'node', 'npm', 'git', 'uvicorn', 'pip']; + if (exeCommands.some(cmd => command.endsWith(cmd))) { + return command + '.exe'; + } + } + return command; +}