From 5250b424eea58c85164cf99dedd409b66ea74ddf Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:22:59 -0400 Subject: [PATCH 1/2] refactor(project): lift project commands to the top-level. (#2397) * refactor(project): lift project command to the top level * fix(project): preserve lifted command and tui wiring * fix(project): preserve lifted tui behavior * test(project): update unit coverage for top-level commands * test(e2e): update project commands to top level * docs(project): document top-level project commands * test(tui): update root menu navigation expectations * fix(project): update payment manager guidance * fix(project): address top-level command review feedback * test(project): remove cwd regression coverage * refactor(project): wire leaf handlers through middleware config * revert(project): drop noisy add cwd fix --- CONTRIBUTING.md | 2 +- README.md | 50 ++-- docs/harness-project-configuration.md | 12 +- e2eTest/project/templates.test.ts | 44 +-- scripts/generate-command-reference.mjs | 18 +- src/assets/cdk/README.md | 10 +- src/assets/evaluators/python-lambda/README.md | 2 +- .../templates/a2a-python-strands/README.md | 4 +- .../agent-python-langchain/README.md | 10 +- .../templates/agent-python-minimal/README.md | 8 +- .../templates/agent-python-strands/README.md | 6 +- .../agent-typescript-strands/README.md | 6 +- .../agent-typescript-vercel/README.md | 4 +- .../templates/agui-python-strands/README.md | 6 +- .../templates/export-harness-python/README.md | 6 +- .../templates/mcp-python-fastmcp/README.md | 6 +- .../templates/shared/env.local.template | 2 +- src/components/CliOnlyScreen.test.tsx | 18 +- src/components/CliOnlyScreen.tsx | 2 +- .../ProjectResourceCreateScreen.test.tsx | 10 +- .../ProjectResourceCreateScreen.tsx | 10 +- src/components/Root.tsx | 44 +-- src/components/RouterScreen.test.tsx | 28 +- src/core/dev/localInvoke.ts | 2 +- src/core/project/backends/cdk.test.ts | 8 +- src/core/project/backends/cdk.ts | 10 +- src/core/project/backends/cdk/credentials.ts | 2 +- src/core/project/backends/cdk/stackReader.ts | 2 +- src/core/project/manager.tsx | 10 +- src/core/project/templates/export.ts | 4 +- .../gateway/gateway.mutations.screen.test.tsx | 12 +- .../gateway/gateway.mutations.test.tsx | 3 +- src/handlers/index.tsx | 22 +- src/handlers/project/add/add.screen.test.tsx | 25 +- .../add/evaluator/code-based/index.test.ts | 2 +- .../evaluator/llm-as-a-judge/index.test.ts | 2 +- .../project/add/gateway-target/index.ts | 2 +- .../project/add/gateway-test-support.ts | 2 +- .../project/add/harness/index.test.ts | 2 +- src/handlers/project/add/harness/index.ts | 2 +- src/handlers/project/add/memory/index.test.ts | 2 +- .../project/add/memory/memory.screen.test.tsx | 25 +- src/handlers/project/add/memory/screen.tsx | 4 +- .../project/add/online-eval/index.test.ts | 2 +- .../project/add/online-insight/index.test.ts | 2 +- .../project/add/payment-manager/index.test.ts | 2 +- .../project/add/payment-manager/index.ts | 2 +- .../project/add/payment-test-support.ts | 2 +- .../add/runtime-endpoint/index.test.ts | 2 +- .../project/add/runtime/index.test.ts | 2 +- .../add/runtime/runtime.screen.test.tsx | 27 +- src/handlers/project/add/runtime/screen.tsx | 4 +- src/handlers/project/build/index.test.ts | 2 +- src/handlers/project/build/index.ts | 4 +- src/handlers/project/build/screen.tsx | 6 +- .../project/buildDeploy.screen.test.tsx | 50 ++-- .../project/create/create.screen.test.tsx | 59 ++--- src/handlers/project/create/index.ts | 4 +- src/handlers/project/create/screen.tsx | 10 +- src/handlers/project/deploy/index.test.ts | 2 +- src/handlers/project/deploy/index.ts | 10 +- src/handlers/project/deploy/screen.tsx | 4 +- src/handlers/project/dev/index.ts | 4 +- src/handlers/project/export/harness.test.ts | 4 +- src/handlers/project/export/harness.ts | 2 +- src/handlers/project/export/types.ts | 2 +- src/handlers/project/index.ts | 137 ++++------ src/handlers/project/invoke/index.test.tsx | 6 +- src/handlers/project/invoke/index.tsx | 2 +- .../project/invoke/invoke.screen.test.tsx | 30 +-- src/handlers/project/invoke/screen.tsx | 4 +- src/handlers/project/log/harness.test.tsx | 11 +- src/handlers/project/log/runtime.test.tsx | 11 +- src/handlers/project/project.screen.test.tsx | 250 ------------------ src/handlers/project/project.test.ts | 4 +- src/handlers/project/remove/index.test.ts | 2 +- .../project/remove/remove.screen.test.tsx | 56 ++-- src/handlers/project/remove/screen.tsx | 16 +- src/handlers/project/screen.tsx | 7 - src/handlers/project/status/index.test.ts | 4 +- src/handlers/project/status/screen.tsx | 6 +- .../project/status/status.screen.test.tsx | 20 +- src/handlers/project/traces/harness.test.tsx | 11 +- src/handlers/project/traces/runtime.test.tsx | 11 +- src/handlers/root.test.tsx | 12 +- src/io/openBrowser.ts | 2 +- src/middleware/withProject.tsx | 2 +- src/projectSchemas/aws-targets.ts | 2 +- src/projectSchemas/harness.test.ts | 2 +- src/testing/projects.ts | 7 +- 90 files changed, 475 insertions(+), 799 deletions(-) delete mode 100644 src/handlers/project/project.screen.test.tsx delete mode 100644 src/handlers/project/screen.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c316f5086..3283799629 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ npm i -g "./$TARBALL" - **The CLI looks frozen in a PowerShell window**: legacy conhost pauses all output while text is selected (the title bar shows `Select`). Press `Esc`. Windows Terminal does not do this. -- **`project create` refuses a long path**: Windows caps paths at 260 characters +- **`agentcore create` refuses a long path**: Windows caps paths at 260 characters unless `LongPathsEnabled` is set, and the CDK app's `node_modules` puts its deepest file 155 characters below the project root (aws-cdk-lib's own shipped fixtures), so the project root must be at most 104 characters. Create the diff --git a/README.md b/README.md index 50f13231e0..6cf51764f8 100644 --- a/README.md +++ b/README.md @@ -29,51 +29,51 @@ those details so you can create, deploy, and invoke agents from your terminal. Create a managed Harness project, deploy it, and send a prompt: ```bash -agentcore project create --name MyAssistant +agentcore create --name MyAssistant cd MyAssistant -agentcore project deploy -agentcore project invoke harness --prompt "Hey, what can you do for me?" +agentcore deploy +agentcore invoke harness --prompt "Hey, what can you do for me?" ``` To start with code you own instead, create a Runtime project from a template. Run this alternative from outside an existing project: ```bash -agentcore project create --name MyAgent --template agent-python-strands +agentcore create --name MyAgent --template agent-python-strands ``` ## Command Surface -`project` commands manage local project specifications and their deployments. -Resource commands operate on deployed resources without requiring a local project. - -| Command | Purpose | -| ---------- | ----------------------------------------------------------------------------- | -| `project` | Create, develop, build, deploy, invoke, and inspect a project | -| `harness` | Manage Harnesses, versions, and endpoints; invoke and inspect them | -| `identity` | Manage credential providers | -| `runtime` | Inspect, invoke, and open a shell in deployed Runtimes | -| `memory` | Inspect Memories, actors, sessions, events, and records | -| `gateway` | Inspect and invoke Gateways, inspect targets and rules, and generate policies | -| `payment` | Inspect payment managers, connectors, sessions, instruments, and balances | -| `eval` | Evaluate agents, manage datasets and configurations, and run experiments | -| `feedback` | Submit feedback | -| `config` | Read and write global CLI settings | -| `update` | Check for and install CLI updates | +Project commands manage local project specifications and their deployments. Resource commands +operate on deployed resources without requiring a local project. + +| Command | Purpose | +| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `create`, `add`, `export`, `remove`, `dev`, `deploy`, `invoke`, `log`, `traces`, `status`, `build` | Create, develop, build, deploy, invoke, and inspect a project | +| `harness` | Manage Harnesses, versions, and endpoints; invoke and inspect them | +| `identity` | Manage credential providers | +| `runtime` | Inspect, invoke, and open a shell in deployed Runtimes | +| `memory` | Inspect Memories, actors, sessions, events, and records | +| `gateway` | Inspect and invoke Gateways, inspect targets and rules, and generate policies | +| `payment` | Inspect payment managers, connectors, sessions, instruments, and balances | +| `eval` | Evaluate agents, manage datasets and configurations, and run experiments | +| `feedback` | Submit feedback | +| `config` | Read and write global CLI settings | +| `update` | Check for and install CLI updates | Use `--help` for subcommands and flags, or browse the [command reference](command.md): ```bash agentcore --help -agentcore project --help +agentcore add --help agentcore runtime invoke --help ``` Supported bare commands open their interactive flows in a terminal. Operation flags select headless behavior for most commands, but invoke commands can use selectors such as `--id` and `--session-id` to seed an interactive console. -Run `agentcore project create` for guided setup. To create a default project -without the wizard, run `agentcore project create --name MyAssistant`. +Run `agentcore create` for guided setup. To create a default project without +the wizard, run `agentcore create --name MyAssistant`. Global flags (declared at the root, available on every command): @@ -129,14 +129,14 @@ export class AgentCoreStack extends Stack { For a Harness, use `this.application.harness("")` instead. -Run `agentcore project deploy` to apply your changes. The names passed to +Run `agentcore deploy` to apply your changes. The names passed to `runtime()` or `harness()` must match the names in your project. If you use an existing execution role through `executionRoleArn`, CDK cannot change its permissions. You'll need to add the required permissions to that role yourself. -Note that `agentcore project status` reports only the resources `agentcore.json` +Note that `agentcore status` reports only the resources `agentcore.json` declares, not the ones you add in the stack. ## Documentation diff --git a/docs/harness-project-configuration.md b/docs/harness-project-configuration.md index 8ff83b86a9..a5e415ced5 100644 --- a/docs/harness-project-configuration.md +++ b/docs/harness-project-configuration.md @@ -17,7 +17,7 @@ ## Harness Project Files -`project create` (without `--template`) and `project add harness` share the same +`agentcore create` (without `--template`) and `agentcore add harness` share the same scaffolding flow. Each harness has `app//harness.yaml` and `app//system-prompt.md`: @@ -50,7 +50,7 @@ memory: mode: managed ``` -Edit the file, then run `agentcore project deploy` from the project directory to +Edit the file, then run `agentcore deploy` from the project directory to apply changes. A local edit does not update an already deployed harness. The examples below are separate alternatives or sections to add to your file. Replace a section when switching modes rather than keeping fields from both. @@ -151,7 +151,7 @@ systemPrompt: | Path-shaped inline values ending in `.md` or `.txt` are rejected by the current schema. Use the conventional `system-prompt.md` file instead. -`project add harness --system-prompt "Your instructions"` writes the supplied +`agentcore add harness --system-prompt "Your instructions"` writes the supplied text to `system-prompt.md`, leaving `systemPrompt` out of the generated YAML. ## Memory @@ -379,7 +379,7 @@ skills: For a private repository, store its access token in an AgentCore Identity API-key credential provider. The project deployment path resolves `auth.credentialName` -from the [project credentials](../command.md#agentcore-project-add-credentials) declared in +from the [project credentials](../command.md#agentcore-add-credentials) declared in `agentcore.json`: ```yaml @@ -471,7 +471,7 @@ rather than being silently skipped. Exporting a Harness to a code-owned Runtime has additional limits: filesystem skills are rejected, and bundled AWS skills are omitted with an explanation in `EXPORT_NOTES.md`. S3 and Git sources are supported by the exporter. See -[Export a Harness](../command.md#agentcore-project-export-harness). +[Export a Harness](../command.md#agentcore-export-harness). See [Harness skills](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-skills.html) and the [Agent Skills format](https://agentskills.io/specification) for source @@ -551,7 +551,7 @@ dockerfile: Dockerfile ``` The path is relative to the directory containing `harness.yaml`. If supplied -through `project add harness --dockerfile`, the CLI copies the file into the +through `agentcore add harness --dockerfile`, the CLI copies the file into the harness directory as `Dockerfile`. Alternatively, reference a pre-built ECR image: diff --git a/e2eTest/project/templates.test.ts b/e2eTest/project/templates.test.ts index afa47b9a7f..66d2e98aa2 100644 --- a/e2eTest/project/templates.test.ts +++ b/e2eTest/project/templates.test.ts @@ -161,16 +161,7 @@ describe( const created = parseResult( ProjectCreatedSchema, await cli.run( - [ - "project", - "create", - "--name", - projectName, - "--template", - "empty", - "--skip-git", - "--json", - ], + ["create", "--name", projectName, "--template", "empty", "--skip-git", "--json"], projectRoot, ), ); @@ -184,16 +175,7 @@ describe( const added = parseResult( OperationSchema, await cli.run( - [ - "project", - "add", - "runtime", - "--name", - runtime.name, - "--template", - runtime.template, - "--json", - ], + ["add", "runtime", "--name", runtime.name, "--template", runtime.template, "--json"], projectDir, ), ); @@ -220,7 +202,7 @@ describe( }; beforeAll(() => { - dev = cli.start(["project", "dev", "--mode", "headless"], projectDir); + dev = cli.start(["dev", "--mode", "headless"], projectDir); dev.stdout?.on("data", captureDevOutput); dev.stderr?.on("data", captureDevOutput); dev.stdout?.resume(); @@ -242,11 +224,11 @@ describe( // the server may take a bit to get ready, so we retry on a timeout. const response = await retry(async () => { if (!dev) { - throw new Error(`project dev did not start. \nstdout/stdout = ${pendingOutput}`); + throw new Error(`agentcore dev did not start. \nstdout/stdout = ${pendingOutput}`); } if (dev.exitCode !== null) { throw new Error( - `project dev exited with code ${dev.exitCode ?? "unknown"}. \nstdout/stdout = ${pendingOutput}`, + `agentcore dev exited with code ${dev.exitCode ?? "unknown"}. \nstdout/stdout = ${pendingOutput}`, ); } @@ -261,7 +243,6 @@ describe( LocalRuntimeInvokeResponseSchema, await cli.run( [ - "project", "invoke", "runtime", "--local", @@ -292,7 +273,7 @@ describe( test("deploys all runtimes", { timeout: TIMEOUT_MS.PROJECT_DEPLOY }, async () => { const deployment = parseResult( DeployResponseSchema, - await cli.run(["project", "deploy", "--yes", "--json"], projectDir), + await cli.run(["deploy", "--yes", "--json"], projectDir), ); expect(deployment.message).toContain("Deployed project"); }); @@ -306,7 +287,6 @@ describe( RuntimeInvokeResponseSchema, await cli.run( [ - "project", "invoke", "runtime", "--name", @@ -335,10 +315,7 @@ describe( async (runtime) => { const removed = parseResult( OperationSchema, - await cli.run( - ["project", "remove", "runtime", "--name", runtime.name, "--json"], - projectDir, - ), + await cli.run(["remove", "runtime", "--name", runtime.name, "--json"], projectDir), ); expect(removed.operation).toBe("remove"); }, @@ -350,13 +327,10 @@ describe( async () => { parseResult( JsonObjectSchema, - await cli.run(["project", "remove", "all", "--yes", "--json"], projectDir), + await cli.run(["remove", "all", "--yes", "--json"], projectDir), ); - parseResult( - JsonObjectSchema, - await cli.run(["project", "deploy", "--yes", "--json"], projectDir), - ); + parseResult(JsonObjectSchema, await cli.run(["deploy", "--yes", "--json"], projectDir)); }, ); }, diff --git a/scripts/generate-command-reference.mjs b/scripts/generate-command-reference.mjs index 106f16f89a..f60b440a0c 100644 --- a/scripts/generate-command-reference.mjs +++ b/scripts/generate-command-reference.mjs @@ -9,7 +9,23 @@ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); const REPOSITORY_ROOT = resolve(SCRIPT_DIR, ".."); const DEFAULT_GROUPS = [ { id: "global-options", title: "Global options", commands: [] }, - { id: "project", title: "Project commands", commands: ["project"] }, + { + id: "project", + title: "Project commands", + commands: [ + "create", + "add", + "export", + "remove", + "dev", + "deploy", + "invoke", + "log", + "traces", + "status", + "build", + ], + }, { id: "harness", title: "Harness commands", commands: ["harness"] }, { id: "identity", title: "Identity commands", commands: ["identity"] }, { id: "runtime", title: "Runtime commands", commands: ["runtime"] }, diff --git a/src/assets/cdk/README.md b/src/assets/cdk/README.md index 68fcdfa410..7bec9847f5 100644 --- a/src/assets/cdk/README.md +++ b/src/assets/cdk/README.md @@ -17,9 +17,9 @@ the conventional `system-prompt.md` file in the harness directory. You normally do not run this app directly: ```bash -agentcore project build # synthesizes the CloudFormation templates into agentcore/cdk/cdk.out -agentcore project deploy # synthesizes, then deploys the stack for the selected target -agentcore project status # reports the resources agentcore.json declares +agentcore build # synthesizes the CloudFormation templates into agentcore/cdk/cdk.out +agentcore deploy # synthesizes, then deploys the stack for the selected target +agentcore status # reports the resources agentcore.json declares ``` `npm run build` compiles the app, and `npx cdk synth` / `npx cdk diff` work from this directory too. @@ -43,10 +43,10 @@ checkout.addEnvironmentVariable('ORDERS_TABLE', orders.tableName); orders.grantReadData(this.application.harness('support')); // any AWS L2 grant works too ``` -Then run `agentcore project deploy` again. An unknown name fails at synth and lists the names that exist. +Then run `agentcore deploy` again. An unknown name fails at synth and lists the names that exist. If a runtime or harness is configured with an `executionRoleArn`, CDK cannot modify that imported role: every grant emits a synth-time warning listing the permissions that were not attached, and the role must already carry them. -`agentcore project status` reports only the resources `agentcore.json` declares; resources you add here are visible +`agentcore status` reports only the resources `agentcore.json` declares; resources you add here are visible through CloudFormation (`aws cloudformation describe-stack-resources`). diff --git a/src/assets/evaluators/python-lambda/README.md b/src/assets/evaluators/python-lambda/README.md index 7c4732b98a..8e6cb06b20 100644 --- a/src/assets/evaluators/python-lambda/README.md +++ b/src/assets/evaluators/python-lambda/README.md @@ -28,5 +28,5 @@ def handler(input: EvaluatorInput, context) -> EvaluatorOutput: return EvaluatorOutput(value=1.0, label="Pass", explanation="…") ``` -Then `agentcore project deploy` packages this directory into the evaluator +Then `agentcore deploy` packages this directory into the evaluator Lambda and registers the evaluator. diff --git a/src/assets/templates/a2a-python-strands/README.md b/src/assets/templates/a2a-python-strands/README.md index e6509f8e5f..7da24eb69a 100644 --- a/src/assets/templates/a2a-python-strands/README.md +++ b/src/assets/templates/a2a-python-strands/README.md @@ -21,11 +21,11 @@ def my_tool(param: str) -> str: ## Developing locally -`agentcore project dev` starts the agent locally on `0.0.0.0:9000`. Fetch its agent card at +`agentcore dev` starts the agent locally on `0.0.0.0:9000`. Fetch its agent card at `http://127.0.0.1:9000/.well-known/agent-card.json` and send it messages by posting A2A JSON-RPC to `http://127.0.0.1:9000/`. ## Deployment -`agentcore project deploy` deploys the agent into Amazon Bedrock AgentCore. Invoke it with the +`agentcore deploy` deploys the agent into Amazon Bedrock AgentCore. Invoke it with the AWS CLI (`bedrock-agentcore invoke-agent-runtime`) using an A2A JSON-RPC payload. diff --git a/src/assets/templates/agent-python-langchain/README.md b/src/assets/templates/agent-python-langchain/README.md index 306d9ff0b2..fe41ce72f7 100644 --- a/src/assets/templates/agent-python-langchain/README.md +++ b/src/assets/templates/agent-python-langchain/README.md @@ -13,7 +13,7 @@ session, and streams its response. between turns. - `model/load.py`: creates the Bedrock chat model with `init_chat_model`. - `pyproject.toml`: Python dependencies, managed with - [uv](https://docs.astral.sh/uv/). `agentcore project create` has already run + [uv](https://docs.astral.sh/uv/). `agentcore create` has already run `uv sync` for you (unless you passed `--skip-install`), so `.venv/` is ready. ## Develop @@ -21,8 +21,8 @@ session, and streams its response. Run the agent locally from the project root: ```bash -agentcore project dev -agentcore project invoke runtime --local --name {{name}} --payload '{"prompt":"What is 2 plus 3?"}' +agentcore dev +agentcore invoke runtime --local --name {{name}} --payload '{"prompt":"What is 2 plus 3?"}' ``` Environment variables for local development go in `agentcore/.env.local` @@ -31,8 +31,8 @@ Environment variables for local development go in `agentcore/.env.local` ## Deploy ```bash -agentcore project deploy -agentcore project invoke runtime --payload '{"prompt":"Hello!"}' +agentcore deploy +agentcore invoke runtime --payload '{"prompt":"Hello!"}' ``` Traces are collected automatically: AgentCore Runtime starts the agent under diff --git a/src/assets/templates/agent-python-minimal/README.md b/src/assets/templates/agent-python-minimal/README.md index 2093a041fc..b8bc638f7b 100644 --- a/src/assets/templates/agent-python-minimal/README.md +++ b/src/assets/templates/agent-python-minimal/README.md @@ -9,7 +9,7 @@ invocation — a starting point you own and grow into a real agent. - `main.py` — the agent. A `BedrockAgentCoreApp` wraps the entrypoint that receives each invocation payload and returns the response. - `pyproject.toml` — Python dependencies, managed with - [uv](https://docs.astral.sh/uv/). `agentcore project create` has already run + [uv](https://docs.astral.sh/uv/). `agentcore create` has already run `uv sync` for you (unless you passed `--skip-install`), so `.venv/` is ready. ## Develop @@ -17,7 +17,7 @@ invocation — a starting point you own and grow into a real agent. Run the agent locally from the project root: ```bash -agentcore project dev +agentcore dev ``` Environment variables for local development go in `agentcore/.env.local` @@ -26,6 +26,6 @@ Environment variables for local development go in `agentcore/.env.local` ## Deploy ```bash -agentcore project deploy -agentcore project invoke runtime --payload '{"prompt":"Hello!"}' +agentcore deploy +agentcore invoke runtime --payload '{"prompt":"Hello!"}' ``` diff --git a/src/assets/templates/agent-python-strands/README.md b/src/assets/templates/agent-python-strands/README.md index 0414cf1be7..98120c90b3 100644 --- a/src/assets/templates/agent-python-strands/README.md +++ b/src/assets/templates/agent-python-strands/README.md @@ -47,14 +47,14 @@ If installation was successful, a virtual environment is already created with de Activate the environment with `source .venv/bin/activate` on macOS/Linux, `.venv\Scripts\activate.bat` in Windows Command Prompt, or `.\.venv\Scripts\activate.ps1` in Windows PowerShell. -`agentcore project dev` will start a local server on 0.0.0.0:8080. +`agentcore dev` will start a local server on 0.0.0.0:8080. # Deployment -After providing credentials, `agentcore project deploy` will deploy your project into Amazon Bedrock AgentCore. +After providing credentials, `agentcore deploy` will deploy your project into Amazon Bedrock AgentCore. Invoke the deployed Runtime with its native payload: ```bash -agentcore project invoke runtime --payload '{"prompt":"Hello!"}' +agentcore invoke runtime --payload '{"prompt":"Hello!"}' ``` diff --git a/src/assets/templates/agent-typescript-strands/README.md b/src/assets/templates/agent-typescript-strands/README.md index 6dbf92a69d..07d4845fb8 100644 --- a/src/assets/templates/agent-typescript-strands/README.md +++ b/src/assets/templates/agent-typescript-strands/README.md @@ -37,14 +37,14 @@ The `sessionId` is not in the body. It comes from the If installation was successful, `node_modules/` is already populated with dependencies. -`agentcore project dev` will start a local server using `tsx watch main.ts` for hot reload on 0.0.0.0:8080. +`agentcore dev` will start a local server using `tsx watch main.ts` for hot reload on 0.0.0.0:8080. # Deployment -After providing credentials, `agentcore project deploy` will deploy your project into Amazon Bedrock AgentCore. +After providing credentials, `agentcore deploy` will deploy your project into Amazon Bedrock AgentCore. Invoke the deployed Runtime with its native payload: ```bash -agentcore project invoke runtime --payload '{"prompt":"Hello!","actorId":"user-123"}' +agentcore invoke runtime --payload '{"prompt":"Hello!","actorId":"user-123"}' ``` diff --git a/src/assets/templates/agent-typescript-vercel/README.md b/src/assets/templates/agent-typescript-vercel/README.md index b5592fe267..94815bd8f7 100644 --- a/src/assets/templates/agent-typescript-vercel/README.md +++ b/src/assets/templates/agent-typescript-vercel/README.md @@ -15,8 +15,8 @@ defines an HTTP server that streams tokens from Amazon Bedrock via the Vercel AI If installation was successful, `node_modules/` is already populated with dependencies. -`agentcore project dev` will start a local server using `tsx watch main.ts` for hot reload on 0.0.0.0:8080. +`agentcore dev` will start a local server using `tsx watch main.ts` for hot reload on 0.0.0.0:8080. # Deployment -After providing credentials, `agentcore project deploy` will deploy your project into Amazon Bedrock AgentCore. +After providing credentials, `agentcore deploy` will deploy your project into Amazon Bedrock AgentCore. diff --git a/src/assets/templates/agui-python-strands/README.md b/src/assets/templates/agui-python-strands/README.md index 9d9e880e55..7b84681720 100644 --- a/src/assets/templates/agui-python-strands/README.md +++ b/src/assets/templates/agui-python-strands/README.md @@ -22,17 +22,17 @@ def my_tool(param: str) -> str: ## Developing locally -`agentcore project dev` starts the agent locally on `0.0.0.0:8080`. Post an AG-UI +`agentcore dev` starts the agent locally on `0.0.0.0:8080`. Post an AG-UI `RunAgentInput` body to `http://127.0.0.1:8080/invocations` to invoke it, and check its health at `http://127.0.0.1:8080/ping`. ## Deployment -`agentcore project deploy` deploys the agent into Amazon Bedrock AgentCore. Invoke the deployed +`agentcore deploy` deploys the agent into Amazon Bedrock AgentCore. Invoke the deployed runtime with an AG-UI `RunAgentInput` payload: ```bash -agentcore project invoke runtime --name {{ name }} \ +agentcore invoke runtime --name {{ name }} \ --payload '{"threadId":"t1","runId":"r1","state":{},"messages":[{"id":"m1","role":"user","content":"Hello!"}],"tools":[],"context":[],"forwardedProps":{}}' ``` diff --git a/src/assets/templates/export-harness-python/README.md b/src/assets/templates/export-harness-python/README.md index 5714aafbf2..391f294ea2 100644 --- a/src/assets/templates/export-harness-python/README.md +++ b/src/assets/templates/export-harness-python/README.md @@ -33,14 +33,14 @@ If installation was successful, a virtual environment is already created with de Activate the environment with `source .venv/bin/activate` on macOS/Linux, `.venv\Scripts\activate.bat` in Windows Command Prompt, or `.\.venv\Scripts\activate.ps1` in Windows PowerShell. -`agentcore project dev` will start a local server on 0.0.0.0:8080. +`agentcore dev` will start a local server on 0.0.0.0:8080. # Deployment -After providing credentials, `agentcore project deploy` will deploy your project into Amazon Bedrock AgentCore. +After providing credentials, `agentcore deploy` will deploy your project into Amazon Bedrock AgentCore. Invoke the deployed Runtime with its native payload: ```bash -agentcore project invoke runtime --payload '{"prompt":"Hello!"}' +agentcore invoke runtime --payload '{"prompt":"Hello!"}' ``` diff --git a/src/assets/templates/mcp-python-fastmcp/README.md b/src/assets/templates/mcp-python-fastmcp/README.md index af0cf4c46d..1497916786 100644 --- a/src/assets/templates/mcp-python-fastmcp/README.md +++ b/src/assets/templates/mcp-python-fastmcp/README.md @@ -23,10 +23,10 @@ def my_tool(param: str) -> str: If installation was successful, a virtual environment is already created with dependencies installed. -`agentcore project dev` starts the server locally on `0.0.0.0:8000`. List and call tools by +`agentcore dev` starts the server locally on `0.0.0.0:8000`. List and call tools by sending JSON-RPC to `http://127.0.0.1:8000/mcp`. ## Deployment -`agentcore project deploy` deploys the server into Amazon Bedrock AgentCore. Invoke it with -`agentcore project invoke runtime`, supplying an MCP JSON-RPC payload (e.g. `tools/list`, `tools/call`). +`agentcore deploy` deploys the server into Amazon Bedrock AgentCore. Invoke it with +`agentcore invoke runtime`, supplying an MCP JSON-RPC payload (e.g. `tools/list`, `tools/call`). diff --git a/src/assets/templates/shared/env.local.template b/src/assets/templates/shared/env.local.template index fb931a12b3..97af2bb1b2 100644 --- a/src/assets/templates/shared/env.local.template +++ b/src/assets/templates/shared/env.local.template @@ -1,5 +1,5 @@ # Environment variables for local development. -# `agentcore project dev` loads this file into your agent's process. Values here +# `agentcore dev` loads this file into your agent's process. Values here # override injected values except PORT, FASTMCP_PORT, and LOCAL_DEV, which the # CLI owns. While trace collection is on (the default), the CLI also owns the # OTEL_* and AGENT_OBSERVABILITY_ENABLED variables so traces reach its local diff --git a/src/components/CliOnlyScreen.test.tsx b/src/components/CliOnlyScreen.test.tsx index 16e483b1da..ca90406a3d 100644 --- a/src/components/CliOnlyScreen.test.tsx +++ b/src/components/CliOnlyScreen.test.tsx @@ -39,8 +39,22 @@ describe("menus list command-line-only subcommands below a divider", () => { await waitForText(r.lastFrame, "command line only"); expect(menuEntries(r.lastFrame()!)).toEqual({ - screens: ["project", "harness", "identity", "runtime", "memory", "gateway", "eval"], - cliOnly: ["payment", "feedback", "config", "update"], + screens: [ + "create", + "add", + "remove", + "deploy", + "invoke", + "status", + "build", + "harness", + "identity", + "runtime", + "memory", + "gateway", + "eval", + ], + cliOnly: ["export", "dev", "log", "traces", "payment", "feedback", "config", "update"], }); r.unmount(); }); diff --git a/src/components/CliOnlyScreen.tsx b/src/components/CliOnlyScreen.tsx index 350c955d55..05da1255bc 100644 --- a/src/components/CliOnlyScreen.tsx +++ b/src/components/CliOnlyScreen.tsx @@ -13,7 +13,7 @@ import { darkTheme } from "./ui/_core.js"; const theme = darkTheme; export interface CliOnlyScreenProps extends ScreenProps { - // path is the command's path, e.g. ["agentcore", "project", "dev"]. + // path is the command's path, e.g. ["agentcore", "dev"]. path: string[]; } diff --git a/src/components/ProjectResourceCreateScreen.test.tsx b/src/components/ProjectResourceCreateScreen.test.tsx index 92af129cf2..d735f9e3a8 100644 --- a/src/components/ProjectResourceCreateScreen.test.tsx +++ b/src/components/ProjectResourceCreateScreen.test.tsx @@ -18,19 +18,19 @@ const RESOURCES = [ resource: "runtime", label: "Runtime", parentDescription: "inspect AgentCore Runtimes", - addCommand: "agentcore project add runtime", + addCommand: "agentcore add runtime", }, { resource: "memory", label: "Memory", parentDescription: "inspect AgentCore Memories", - addCommand: "agentcore project add memory", + addCommand: "agentcore add memory", }, { resource: "gateway", label: "Gateway", parentDescription: "manage AgentCore Gateways", - addCommand: "agentcore project add gateway --name MyGateway", + addCommand: "agentcore add gateway --name MyGateway", }, ] as const satisfies { resource: ProjectCreateResource; @@ -52,10 +52,10 @@ describe("project resource creation guidance", () => { await waitForText(r.lastFrame, `Create an AgentCore ${label}`); const frame = r.lastFrame()!; - expect(frame).toContain("agentcore project create"); + expect(frame).toContain("agentcore create"); expect(frame).toContain("cd "); expect(frame).toContain(addCommand); - expect(frame).toContain("agentcore project deploy"); + expect(frame).toContain("agentcore deploy"); expect(frame).not.toContain("┌"); await r.press("escape"); diff --git a/src/components/ProjectResourceCreateScreen.tsx b/src/components/ProjectResourceCreateScreen.tsx index f141fc61c4..87963f72a4 100644 --- a/src/components/ProjectResourceCreateScreen.tsx +++ b/src/components/ProjectResourceCreateScreen.tsx @@ -13,19 +13,19 @@ const RESOURCES: Record< label: "Runtime", pluralLabel: "Runtimes", description: "create an AgentCore Runtime in a project", - addCommand: "agentcore project add runtime", + addCommand: "agentcore add runtime", }, memory: { label: "Memory", pluralLabel: "Memories", description: "create an AgentCore Memory in a project", - addCommand: "agentcore project add memory", + addCommand: "agentcore add memory", }, gateway: { label: "Gateway", pluralLabel: "Gateways", description: "create an AgentCore Gateway in a project", - addCommand: "agentcore project add gateway --name MyGateway", + addCommand: "agentcore add gateway --name MyGateway", }, }; @@ -57,10 +57,10 @@ export function ProjectResourceCreateScreen({ resource }: ProjectResourceCreateS Run these commands from the command line: - {" agentcore project create"} + {" agentcore create"} {" cd "} {` ${config.addCommand}`} - {" agentcore project deploy"} + {" agentcore deploy"} ); } diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 101ba04364..f86b1b93d6 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -120,7 +120,7 @@ import { GatewayRuleListScreen } from "../handlers/gateway/rule/list/screen.tsx" import { GatewayRuleGetScreen } from "../handlers/gateway/rule/get/screen.tsx"; import { GatewayInvokeScreen } from "../handlers/gateway/invoke/screen.tsx"; import { GatewayPolicyGenerateScreen } from "../handlers/gateway/policy/screen.tsx"; -import { ProjectScreen } from "../handlers/project/screen.tsx"; +import { RouterScreen } from "./RouterScreen.tsx"; import { CommandFallbackScreen } from "./CliOnlyScreen.tsx"; import { ProjectResourceCreateScreen } from "./ProjectResourceCreateScreen.tsx"; import { BuildProjectScreen } from "../handlers/project/build/screen.tsx"; @@ -242,7 +242,7 @@ function RouteTable({ ctx, core }: ScreenProps) { } /> } /> } /> @@ -868,41 +868,23 @@ function RouteTable({ ctx, core }: ScreenProps) { path="agentcore/identity/oauth2-credential-provider/get/:name/json" element={} /> - } /> } + path="agentcore/add" + element={} /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } /> } /> {/* Every known command without a screen of its own: a group opens its diff --git a/src/components/RouterScreen.test.tsx b/src/components/RouterScreen.test.tsx index 1ccd88beb7..f9095c029e 100644 --- a/src/components/RouterScreen.test.tsx +++ b/src/components/RouterScreen.test.tsx @@ -46,8 +46,8 @@ describe("menu rendering", () => { test("highlights the first option by default", async () => { const r = renderScreen("/agentcore"); await waitForText(r.lastFrame, "harness"); - // The focus caret marks the highlighted row; the first option is project. - expect(r.lastFrame()).toContain("❯ project"); + // The focus caret marks the highlighted row; the first option is create. + expect(r.lastFrame()).toContain("❯ create"); r.unmount(); }); }); @@ -89,37 +89,33 @@ describe("filtering", () => { describe("navigation", () => { test("down arrow moves the highlight to the next option", async () => { const r = renderScreen("/agentcore"); - await waitForText(r.lastFrame, "❯ project"); + await waitForText(r.lastFrame, "❯ create"); await r.press("down"); - await waitForText(r.lastFrame, "❯ harness"); + await waitForText(r.lastFrame, "❯ add"); await r.press("down"); - await waitForText(r.lastFrame, "❯ identity"); + await waitForText(r.lastFrame, "❯ remove"); r.unmount(); }); test("up arrow does not move past the first option", async () => { const r = renderScreen("/agentcore"); - await waitForText(r.lastFrame, "❯ project"); + await waitForText(r.lastFrame, "❯ create"); await r.press("up"); await tick(20); // Still on the first option. - expect(r.lastFrame()).toContain("❯ project"); + expect(r.lastFrame()).toContain("❯ create"); r.unmount(); }); test("enter navigates into the highlighted subcommand's screen", async () => { const r = renderScreen("/agentcore"); - await waitForText(r.lastFrame, "❯ project"); + await waitForText(r.lastFrame, "❯ create"); - await r.press("down"); - await waitForText(r.lastFrame, "❯ harness"); await r.press("return"); - // The harness screen is itself a RouterScreen showing harness subcommands. - await waitForText(r.lastFrame, "agentcore → harness"); - expect(r.lastFrame()).toContain("list"); + await waitForText(r.lastFrame, "name your project"); r.unmount(); }); @@ -130,17 +126,17 @@ describe("navigation", () => { await r.press("escape"); // Back at the root menu (breadcrumb no longer includes harness). await waitForText(r.lastFrame, "the platform for production AI agents"); - expect(r.lastFrame()).toContain("❯ project"); + expect(r.lastFrame()).toContain("❯ create"); r.unmount(); }); test("esc at the root menu is a no-op (no parent to go to)", async () => { const r = renderScreen("/agentcore"); - await waitForText(r.lastFrame, "❯ project"); + await waitForText(r.lastFrame, "❯ create"); await r.press("escape"); await tick(20); - expect(r.lastFrame()).toContain("❯ project"); + expect(r.lastFrame()).toContain("❯ create"); r.unmount(); }); }); diff --git a/src/core/dev/localInvoke.ts b/src/core/dev/localInvoke.ts index 3364932bc5..92db71231b 100644 --- a/src/core/dev/localInvoke.ts +++ b/src/core/dev/localInvoke.ts @@ -79,7 +79,7 @@ export async function invokeLocalRuntime( const detail = error instanceof Error ? error.message : String(error); throw new InvalidEnvironmentError( `Could not reach local dev server on port ${request.port} (${detail}). Start it with: ` + - `agentcore project dev --mode headless --agent --port ${request.port}`, + `agentcore dev --mode headless --agent --port ${request.port}`, { cause: error }, ); } diff --git a/src/core/project/backends/cdk.test.ts b/src/core/project/backends/cdk.test.ts index 1a358890ef..59ddd6cc6e 100644 --- a/src/core/project/backends/cdk.test.ts +++ b/src/core/project/backends/cdk.test.ts @@ -397,7 +397,7 @@ describe("CdkBackend.build", () => { await expect(collect(subject.backend.build(input))).rejects.toThrow( `Project build cannot resolve credential "${name}" before its first deployment. ` + - `Run 'agentcore project deploy' to provision the credential and build the project.`, + `Run 'agentcore deploy' to provision the credential and build the project.`, ); }); }); @@ -760,7 +760,7 @@ describe("CdkBackend.deploy", () => { }); test("hands teardown the target and the providers recorded before the deploy overwrote them", async () => { - // The `project remove all` shape: the spec declares nothing, so provisioning + // The `agentcore remove all` shape: the spec declares nothing, so provisioning // returns nothing and rewrites the credentials map to empty before teardown runs. // The recorded providers are the only remaining record of what to delete, and // the target name is what scopes their provider names. @@ -1099,7 +1099,7 @@ describe("CdkBackend.resolveDeployedResources", () => { await expect( subject.backend.resolveDeployedResources(input, { target: TARGET }), - ).rejects.toThrow(/not deployed.*project deploy --target default/s); + ).rejects.toThrow(/not deployed.*agentcore deploy --target default/s); expect(subject.stackReads).toEqual([]); expect(subject.accountCredentials).toEqual([]); }); @@ -1111,7 +1111,7 @@ describe("CdkBackend.resolveDeployedResources", () => { await expect( subject.backend.resolveDeployedResources(input, { target: TARGET }), - ).rejects.toThrow(/not deployed.*project deploy --target default/s); + ).rejects.toThrow(/not deployed.*agentcore deploy --target default/s); expect(subject.stackReads[0]?.stackName).toBe(STACK_ARN); }); diff --git a/src/core/project/backends/cdk.ts b/src/core/project/backends/cdk.ts index 3d4cf9d2f4..2ffeae274a 100644 --- a/src/core/project/backends/cdk.ts +++ b/src/core/project/backends/cdk.ts @@ -246,7 +246,7 @@ export class CdkBackend implements ProjectBackend { if (credentialName) { throw new ProjectStateError( `Project build cannot resolve credential "${credentialName}" before its first deployment. ` + - `Run 'agentcore project deploy' to provision the credential and build the project.`, + `Run 'agentcore deploy' to provision the credential and build the project.`, { cause: error, meta: { credentialName } }, ); } @@ -414,7 +414,7 @@ export class CdkBackend implements ProjectBackend { throw new ProjectStateError( `Project '${project.name}' declares no resources to deploy, and no stack ` + `'${artifact.stackName}' exists in ${target.account}/${target.region} to remove. ` + - `Add a resource — for example 'agentcore project add runtime' — before deploying.`, + `Add a resource — for example 'agentcore add runtime' — before deploying.`, ); } @@ -436,7 +436,7 @@ export class CdkBackend implements ProjectBackend { yield { type: "step", message: `Removing stack ${artifact.stackName}` }; yield* this.runCdk({ kind: "destroy", stackArtifactId: artifact.id }, options); // After the stack, since a resource in it may still be using the provider. The - // declared credentials are included because `project remove all` empties the spec + // declared credentials are included because `agentcore remove all` empties the spec // before the deploy that gets here, so what it recorded is all that names them. yield* this.removeCredentials(project, { credentials: options.credentials, @@ -490,7 +490,7 @@ export class CdkBackend implements ProjectBackend { if (!stackReference) { throw new ProjectStateError( `Project '${project.name}' is not deployed to target '${target.name}'. ` + - `Run 'agentcore project deploy --target ${target.name}' first.`, + `Run 'agentcore deploy --target ${target.name}' first.`, ); } @@ -499,7 +499,7 @@ export class CdkBackend implements ProjectBackend { if (!stack) { throw new ProjectStateError( `Project '${project.name}' is not deployed to target '${target.name}'. ` + - `Run 'agentcore project deploy --target ${target.name}' first.`, + `Run 'agentcore deploy --target ${target.name}' first.`, ); } diff --git a/src/core/project/backends/cdk/credentials.ts b/src/core/project/backends/cdk/credentials.ts index b2820ddbdf..c76c7b42ab 100644 --- a/src/core/project/backends/cdk/credentials.ts +++ b/src/core/project/backends/cdk/credentials.ts @@ -29,7 +29,7 @@ export type DeployedCredential = { clientSecretArn?: string; /** * Which kind of provider the ARN belongs to. Recorded so a teardown knows which - * providers it owns without the spec that declared them — `project remove all` + * providers it owns without the spec that declared them — `agentcore remove all` * empties the spec before the deploy that tears the target down. */ authorizerType?: CredentialType; diff --git a/src/core/project/backends/cdk/stackReader.ts b/src/core/project/backends/cdk/stackReader.ts index a0a4bf88fb..c4fceaf2b9 100644 --- a/src/core/project/backends/cdk/stackReader.ts +++ b/src/core/project/backends/cdk/stackReader.ts @@ -34,7 +34,7 @@ function cloudFormationDescriber( * * This is only the read: interpreting the stack's status and outputs (deployed * vs. in-progress vs. failed, which outputs to surface) is left to the caller — - * e.g. `project status` — which owns that shape. + * e.g. `agentcore status` — which owns that shape. */ export async function describeStack( region: string, diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 66acb64769..22f6b8f03a 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -227,7 +227,7 @@ export class FsProjectManager implements ProjectManager { } // A harness project scaffolds through the same addResource flow that - // `project add harness` uses, so a create-time harness and an added one can + // `agentcore add harness` uses, so a create-time harness and an added one can // never drift apart. if (input.scaffoldHarnessInput) { const scaffolded = await this.resolve({ filePath: destination }); @@ -1052,7 +1052,7 @@ export class FsProjectManager implements ProjectManager { const label = input.resourceType === "runtime" ? "Runtime" : "Harness"; throw new ProjectStateError( `${label} '${input.name}' is not deployed to target '${input.target}'. ` + - `Run 'agentcore project deploy --target ${input.target}' first.`, + `Run 'agentcore deploy --target ${input.target}' first.`, ); } @@ -1085,7 +1085,7 @@ export class FsProjectManager implements ProjectManager { if (targets.length === 0) { throw new ProjectStateError( `No deployment targets are configured for project '${project.name}'. ` + - `Please deploy your project using 'agentcore project deploy'.`, + `Please deploy your project using 'agentcore deploy'.`, ); } @@ -1129,7 +1129,7 @@ export class FsProjectManager implements ProjectManager { `Cannot create the default deployment target for project '${project.name}' because ` + `the AWS account could not be resolved: ${cause.message}\n` + `Check that valid AWS credentials are configured (for example via 'aws configure', ` + - `AWS_PROFILE, or environment variables) and re-run 'agentcore project deploy'.`, + `AWS_PROFILE, or environment variables) and re-run 'agentcore deploy'.`, { cause: error }, ); } @@ -1227,7 +1227,7 @@ export class FsProjectManager implements ProjectManager { type: "step", message: `Warning: could not generate ${lockfile} in ${appDir}. ` + - `Run \`${command.join(" ")}\` there before \`agentcore project dev\` or \`deploy\` — ` + + `Run \`${command.join(" ")}\` there before \`agentcore dev\` or \`deploy\` — ` + "container builds install from it.", }; } diff --git a/src/core/project/templates/export.ts b/src/core/project/templates/export.ts index 969d87006e..596305af47 100644 --- a/src/core/project/templates/export.ts +++ b/src/core/project/templates/export.ts @@ -413,7 +413,7 @@ function attachIdentityProvider( `"${credentialName}" (${apiKeyArn}). A credential entry named "${credentialName}" was ` + `added to agentcore.json. Deploy creates a provider for it scoped to the project and ` + `target, so add ${envVarName}= to agentcore/.env.local before the first ` + - `deploy. \`agentcore project dev\` reads the same variable.`, + `deploy. \`agentcore dev\` reads the same variable.`, }); } @@ -440,7 +440,7 @@ function resolveMemory( category: MEMORY_MANAGED_NOTE_CATEGORY, message: "The harness used managed memory, which the service provisions and owns. The exported " + - "agent has no memory wired. Add a project memory (`agentcore project add memory`) and " + + "agent has no memory wired. Add a project memory (`agentcore add memory`) and " + "re-run the export, or wire memory/session.py to an existing AgentCore Memory by hand.", }); return {}; diff --git a/src/handlers/gateway/gateway.mutations.screen.test.tsx b/src/handlers/gateway/gateway.mutations.screen.test.tsx index 6b0b04941b..88f4501cb8 100644 --- a/src/handlers/gateway/gateway.mutations.screen.test.tsx +++ b/src/handlers/gateway/gateway.mutations.screen.test.tsx @@ -36,10 +36,10 @@ describe("Gateway mutation menus", () => { await screen.press("return"); await waitForText(screen.lastFrame, "Create an AgentCore Gateway"); const frame = screen.lastFrame()!; - expect(frame).toContain("agentcore project create"); + expect(frame).toContain("agentcore create"); expect(frame).toContain("cd "); - expect(frame).toContain("agentcore project add gateway --name MyGateway"); - expect(frame).toContain("agentcore project deploy"); + expect(frame).toContain("agentcore add gateway --name MyGateway"); + expect(frame).toContain("agentcore deploy"); expect(frame).not.toContain("agentcore gateway create"); expect(frame).not.toContain("this command runs from the command line"); expect(screen.core.gateway.calls).toEqual([]); @@ -56,7 +56,7 @@ describe("Gateway mutation menus", () => { screen.lastFrame, enabled ? "this command runs from the command line" : "Create an AgentCore Gateway", ); - expect(screen.lastFrame()?.includes("agentcore project add gateway")).toBe(!enabled); + expect(screen.lastFrame()?.includes("agentcore add gateway")).toBe(!enabled); expect(screen.lastFrame()?.includes("--authorizer-type")).toBe(enabled); await screen.press("escape"); await waitForText(screen.lastFrame, "manage AgentCore Gateways"); @@ -83,10 +83,10 @@ describe("Gateway mutation menus", () => { await waitForText(screen.lastFrame, "Create an AgentCore Gateway"); await screen.resize(50, 12); await screen.write("\u001b[6~"); - await waitForText(screen.lastFrame, "agentcore project deploy"); + await waitForText(screen.lastFrame, "agentcore deploy"); await screen.resize(100, 40); await waitForText(screen.lastFrame, "Create an AgentCore Gateway"); - expect(screen.lastFrame()).toContain("agentcore project deploy"); + expect(screen.lastFrame()).toContain("agentcore deploy"); await screen.press("escape"); await waitForText(screen.lastFrame, "manage AgentCore Gateways"); }); diff --git a/src/handlers/gateway/gateway.mutations.test.tsx b/src/handlers/gateway/gateway.mutations.test.tsx index 25510619e5..3e3f51c516 100644 --- a/src/handlers/gateway/gateway.mutations.test.tsx +++ b/src/handlers/gateway/gateway.mutations.test.tsx @@ -84,8 +84,7 @@ describe("Gateway imperative mutation availability", () => { expect(gateway.commands.find((child) => child.name() === "policy")?.commands[0]?.name()).toBe( "generate", ); - const project = command.commands.find((child) => child.name() === "project")!; - const add = project.commands.find((child) => child.name() === "add")!; + const add = command.commands.find((child) => child.name() === "add")!; expect(add.commands.map((child) => child.name())).toContain("gateway"); const harness = command.commands.find((child) => child.name() === "harness")!; expect(harness.commands.map((child) => child.name())).toContain("create"); diff --git a/src/handlers/index.tsx b/src/handlers/index.tsx index c6687c9d12..6bd9e54f86 100644 --- a/src/handlers/index.tsx +++ b/src/handlers/index.tsx @@ -9,7 +9,7 @@ import { createPaymentHandler } from "./payment/index.tsx"; import { createRuntimeHandler } from "./runtime/index.tsx"; import { DebugKey, JsonKey, RegionKey } from "./keys.tsx"; import { createConfigHandler } from "./config/"; -import { createProjectHandler } from "./project/index.ts"; +import { createProjectHandlers } from "./project/index.ts"; import { createUpdateHandler } from "./update/index.tsx"; import { renderTui } from "../tui"; import { @@ -46,7 +46,21 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router const root = new Router( "agentcore", "the platform for production AI agents", - ).supportedTuiCommands("project", "harness", "identity", "runtime", "memory", "gateway", "eval"); + ).supportedTuiCommands( + "create", + "invoke", + "build", + "deploy", + "status", + "add", + "remove", + "harness", + "identity", + "runtime", + "memory", + "gateway", + "eval", + ); // `agentcore --version` prints the build-time package version. root.version(PACKAGE_VERSION); @@ -76,7 +90,9 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router // Install sub handlers. Registration order is menu/help order; project is // the primary workflow, so it goes first. - root.handler(createProjectHandler({ core, io })); + createProjectHandlers(core, io).forEach((handler) => { + root.handler(handler); + }); root.handler(createHarnessHandler(core, io)); root.handler(createIdentityHandler(core, io)); root.handler(createRuntimeHandler(core, io)); diff --git a/src/handlers/project/add/add.screen.test.tsx b/src/handlers/project/add/add.screen.test.tsx index 2d1f74bf0c..c2f47fc99a 100644 --- a/src/handlers/project/add/add.screen.test.tsx +++ b/src/handlers/project/add/add.screen.test.tsx @@ -10,12 +10,11 @@ import { afterEach(cleanupScreens); // addSubcommands reads the resources off the compiled Commander tree, so a -// `project add` resource added later is covered without editing this file. +// An `add` resource added later is covered without editing this file. // `help` is Commander's own, not one of ours. function addSubcommands(): string[] { const root = compiledRootCommand(); - const project = root.commands.find((command) => command.name() === "project")!; - const add = project.commands.find((command) => command.name() === "add")!; + const add = root.commands.find((command) => command.name() === "add")!; return add.commands.map((command) => command.name()).filter((name) => name !== "help"); } @@ -25,7 +24,7 @@ const WITH_SCREENS = ["runtime", "memory"]; describe("project add menu", () => { test("lists every add resource", async () => { - const r = renderScreen("/agentcore/project/add"); + const r = renderScreen("/agentcore/add"); await waitForText(r.lastFrame, "add project resources"); const frame = r.lastFrame()!; @@ -36,7 +35,7 @@ describe("project add menu", () => { }); test("the resources with a wizard are listed above the divider", async () => { - const r = renderScreen("/agentcore/project/add"); + const r = renderScreen("/agentcore/add"); await waitForText(r.lastFrame, "command line only"); const { screens, cliOnly } = menuEntries(r.lastFrame()!); @@ -49,25 +48,25 @@ describe("project add menu", () => { r.unmount(); }); - test("is reachable from the project menu", async () => { - const r = renderScreen("/agentcore/project"); + test("is reachable from the root menu", async () => { + const r = renderScreen("/agentcore"); - await waitForText(r.lastFrame, "agentcore → project"); + await waitForText(r.lastFrame, "the platform for production AI agents"); await r.write("add"); await waitForText(r.lastFrame, "❯ add"); await r.press("return"); - await waitForText(r.lastFrame, "agentcore → project → add"); + await waitForText(r.lastFrame, "agentcore → add"); r.unmount(); }); - test("esc returns to the project menu", async () => { - const r = renderScreen("/agentcore/project/add"); + test("esc returns to the root menu", async () => { + const r = renderScreen("/agentcore/add"); - await waitForText(r.lastFrame, "agentcore → project → add"); + await waitForText(r.lastFrame, "agentcore → add"); await r.press("escape"); - await waitForText(r.lastFrame, "manage an AgentCore project"); + await waitForText(r.lastFrame, "the platform for production AI agents"); r.unmount(); }); }); diff --git a/src/handlers/project/add/evaluator/code-based/index.test.ts b/src/handlers/project/add/evaluator/code-based/index.test.ts index 691bc73908..2b1052557e 100644 --- a/src/handlers/project/add/evaluator/code-based/index.test.ts +++ b/src/handlers/project/add/evaluator/code-based/index.test.ts @@ -22,7 +22,7 @@ async function run(args: string[]) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io }; } diff --git a/src/handlers/project/add/evaluator/llm-as-a-judge/index.test.ts b/src/handlers/project/add/evaluator/llm-as-a-judge/index.test.ts index b4a0ca3b92..ad0c3f5323 100644 --- a/src/handlers/project/add/evaluator/llm-as-a-judge/index.test.ts +++ b/src/handlers/project/add/evaluator/llm-as-a-judge/index.test.ts @@ -23,7 +23,7 @@ async function run(args: string[], opts?: { core?: TestCoreClient }) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io, core }; } diff --git a/src/handlers/project/add/gateway-target/index.ts b/src/handlers/project/add/gateway-target/index.ts index 205a19e1fe..fa2f81785d 100644 --- a/src/handlers/project/add/gateway-target/index.ts +++ b/src/handlers/project/add/gateway-target/index.ts @@ -33,7 +33,7 @@ Use --endpoint for an external MCP server or --runtime for a project Runtime. For every complete project Target shape, pass targetType and its configuration here. Supported targetType values: mcpServer, httpRuntime, apiGateway, openApiSchema, smithyModel, lambdaFunctionArn, connector, and passthrough. -Use project add gateway-connector for curated Connector shortcuts.`, +Use agentcore add gateway-connector for curated Connector shortcuts.`, }, ), flag( diff --git a/src/handlers/project/add/gateway-test-support.ts b/src/handlers/project/add/gateway-test-support.ts index 5e1ce042b4..3950930e39 100644 --- a/src/handlers/project/add/gateway-test-support.ts +++ b/src/handlers/project/add/gateway-test-support.ts @@ -30,7 +30,7 @@ export function createGatewayProjectTestHarness(directoryPrefix: string) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return io; } diff --git a/src/handlers/project/add/harness/index.test.ts b/src/handlers/project/add/harness/index.test.ts index 0c2d082f80..087fa2acdd 100644 --- a/src/handlers/project/add/harness/index.test.ts +++ b/src/handlers/project/add/harness/index.test.ts @@ -24,7 +24,7 @@ async function run(args: string[], opts?: { core?: TestCoreClient }) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io, core }; } diff --git a/src/handlers/project/add/harness/index.ts b/src/handlers/project/add/harness/index.ts index 675deb5837..050e7b2e1d 100644 --- a/src/handlers/project/add/harness/index.ts +++ b/src/handlers/project/add/harness/index.ts @@ -6,7 +6,7 @@ import { parseJsonFlag, parseTags } from "../../../utils"; import { InputValidationError } from "../../../../errors"; import { HarnessSpecSchema } from "../../../../projectSchemas/harness"; -/** The model a harness runs on when none is configured; `project create`'s +/** The model a harness runs on when none is configured; `agentcore create`'s * harness path shares it so the two entry points cannot drift. */ export const DEFAULT_HARNESS_MODEL = { provider: "bedrock", diff --git a/src/handlers/project/add/memory/index.test.ts b/src/handlers/project/add/memory/index.test.ts index 91b56989eb..b9336e3ab7 100644 --- a/src/handlers/project/add/memory/index.test.ts +++ b/src/handlers/project/add/memory/index.test.ts @@ -33,7 +33,7 @@ async function run(args: string[], opts?: { core?: TestCoreClient; isTTY?: boole globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io, core }; } diff --git a/src/handlers/project/add/memory/memory.screen.test.tsx b/src/handlers/project/add/memory/memory.screen.test.tsx index aa3f51ae05..f3c751b524 100644 --- a/src/handlers/project/add/memory/memory.screen.test.tsx +++ b/src/handlers/project/add/memory/memory.screen.test.tsx @@ -54,7 +54,7 @@ describe("project add memory wizard", () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity, staleTime: Infinity } }, }); - const r = renderScreen("/agentcore/project/add/memory", { queryClient }); + const r = renderScreen("/agentcore/add/memory", { queryClient }); await waitForText(r.lastFrame, "what should this Memory be called?"); await r.write("orders_memory"); @@ -104,7 +104,7 @@ describe("project add memory wizard", () => { test("selecting no strategy adds a memory that only keeps raw events", async () => { const projectRoot = await inProject(); - const r = renderScreen("/agentcore/project/add/memory"); + const r = renderScreen("/agentcore/add/memory"); await waitForText(r.lastFrame, "what should this Memory be called?"); await r.write("events_only"); @@ -129,7 +129,7 @@ describe("project add memory wizard", () => { test("the strategies chosen are ordered as the list draws them", async () => { const projectRoot = await inProject(); - const r = renderScreen("/agentcore/project/add/memory"); + const r = renderScreen("/agentcore/add/memory"); await waitForText(r.lastFrame, "what should this Memory be called?"); await r.write("ordered_memory"); @@ -158,7 +158,7 @@ describe("project add memory wizard", () => { test("a blank name is refused", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/memory"); + const r = renderScreen("/agentcore/add/memory"); await waitForText(r.lastFrame, "what should this Memory be called?"); await r.press("return"); @@ -170,7 +170,7 @@ describe("project add memory wizard", () => { test("a name that breaks the schema's pattern is rejected as it is typed", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/memory"); + const r = renderScreen("/agentcore/add/memory"); await waitForText(r.lastFrame, "what should this Memory be called?"); await r.write("1memory"); @@ -181,7 +181,7 @@ describe("project add memory wizard", () => { test("retention outside the service's range keeps the step", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/memory"); + const r = renderScreen("/agentcore/add/memory"); await waitForText(r.lastFrame, "what should this Memory be called?"); await r.write("short_memory"); @@ -201,7 +201,7 @@ describe("project add memory wizard", () => { test("retention that is not a number keeps the step", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/memory"); + const r = renderScreen("/agentcore/add/memory"); await waitForText(r.lastFrame, "what should this Memory be called?"); await r.write("odd_memory"); @@ -222,7 +222,7 @@ describe("project add memory wizard", () => { // The name is taken, so addResource refuses it — the realistic failure, and // one the user can fix without starting over. await run(["add", "memory", "--name", "orders_memory"]); - const r = renderScreen("/agentcore/project/add/memory"); + const r = renderScreen("/agentcore/add/memory"); await waitForText(r.lastFrame, "what should this Memory be called?"); await r.write("orders_memory"); @@ -245,7 +245,7 @@ describe("project add memory wizard", () => { test("esc on the first step returns to the add menu", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/memory"); + const r = renderScreen("/agentcore/add/memory"); await waitForText(r.lastFrame, "what should this Memory be called?"); await r.press("escape"); @@ -256,7 +256,7 @@ describe("project add memory wizard", () => { }); // These drive the real CLI entrypoint rather than mounting the screen, because -// what they cover is the routing in front of it: a bare `project add memory` has +// what they cover is the routing in front of it: a bare `agentcore add memory` has // to reach the wizard, and everything else has to stay headless. describe("project add memory dispatch", () => { function buildRoot(io: AppIO) { @@ -271,7 +271,7 @@ describe("project add memory dispatch", () => { async function routeError(io: AppIO, args: string[]): Promise { return buildRoot(io) - .route(["node", "agentcore", "project", "add", "memory", ...args]) + .route(["node", "agentcore", "add", "memory", ...args]) .then( () => undefined, (caught: unknown) => caught, @@ -283,7 +283,7 @@ describe("project add memory dispatch", () => { const { streams, stdin } = ttyTestIO(); const outcome = buildRoot(streams.io) - .route(["node", "agentcore", "project", "add", "memory"]) + .route(["node", "agentcore", "add", "memory"]) .then( () => ({ ok: true as const }), (error: unknown) => ({ ok: false as const, error }), @@ -343,7 +343,6 @@ describe("project add memory dispatch", () => { await buildRoot(streams.io).route([ "node", "agentcore", - "project", "add", "memory", "--name", diff --git a/src/handlers/project/add/memory/screen.tsx b/src/handlers/project/add/memory/screen.tsx index c83063de1f..4e9e2acd84 100644 --- a/src/handlers/project/add/memory/screen.tsx +++ b/src/handlers/project/add/memory/screen.tsx @@ -26,9 +26,9 @@ import { type MemoryInput, } from "./index"; -const BREADCRUMB = ["agentcore", "project", "add", "memory"]; +const BREADCRUMB = ["agentcore", "add", "memory"]; const DESCRIPTION = "add a Memory to the current project"; -const ADD_MENU = "/agentcore/project/add"; +const ADD_MENU = "/agentcore/add"; const STRATEGY_DESCRIPTIONS: Record = { SEMANTIC: "durable facts about the actor", diff --git a/src/handlers/project/add/online-eval/index.test.ts b/src/handlers/project/add/online-eval/index.test.ts index f48b67583b..69f0597de2 100644 --- a/src/handlers/project/add/online-eval/index.test.ts +++ b/src/handlers/project/add/online-eval/index.test.ts @@ -22,7 +22,7 @@ async function run(args: string[], opts?: { core?: TestCoreClient }) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io, core }; } diff --git a/src/handlers/project/add/online-insight/index.test.ts b/src/handlers/project/add/online-insight/index.test.ts index 6fea31485f..7a54e87ae3 100644 --- a/src/handlers/project/add/online-insight/index.test.ts +++ b/src/handlers/project/add/online-insight/index.test.ts @@ -22,7 +22,7 @@ async function run(args: string[], opts?: { core?: TestCoreClient }) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io, core }; } diff --git a/src/handlers/project/add/payment-manager/index.test.ts b/src/handlers/project/add/payment-manager/index.test.ts index 3f20473206..25acde9e2b 100644 --- a/src/handlers/project/add/payment-manager/index.test.ts +++ b/src/handlers/project/add/payment-manager/index.test.ts @@ -94,7 +94,7 @@ describe("project add payment-manager", () => { expect(result.notes).toEqual([ "Warning: auto-payment is ENABLED for manager 'payments'. Agents can automatically settle " + "402 responses without human approval. Use --no-auto-payment to require manual approval.", - "Warning: project add payment-manager does not modify runtime source code. " + + "Warning: agentcore add payment-manager does not modify runtime source code. " + "Configure the Payments SDK or plugin in supported runtimes before invoking payment-enabled agents.", ]); expect(io.stderr()).not.toContain("auto-payment is ENABLED"); diff --git a/src/handlers/project/add/payment-manager/index.ts b/src/handlers/project/add/payment-manager/index.ts index ceccbe8967..b3968d0efa 100644 --- a/src/handlers/project/add/payment-manager/index.ts +++ b/src/handlers/project/add/payment-manager/index.ts @@ -74,7 +74,7 @@ export const createAddPaymentManagerHandler = (config: AddProjectResourceConfig) } if (project.spec.runtimes.length > 0) { notes.push( - "Warning: project add payment-manager does not modify runtime source code. " + + "Warning: agentcore add payment-manager does not modify runtime source code. " + "Configure the Payments SDK or plugin in supported runtimes before invoking payment-enabled agents.", ); } diff --git a/src/handlers/project/add/payment-test-support.ts b/src/handlers/project/add/payment-test-support.ts index c3f2ba85f4..534014710d 100644 --- a/src/handlers/project/add/payment-test-support.ts +++ b/src/handlers/project/add/payment-test-support.ts @@ -18,7 +18,7 @@ export function createPaymentProjectTestHarness(directoryPrefix: string) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return io; } diff --git a/src/handlers/project/add/runtime-endpoint/index.test.ts b/src/handlers/project/add/runtime-endpoint/index.test.ts index 25fdd9fd50..451354be1b 100644 --- a/src/handlers/project/add/runtime-endpoint/index.test.ts +++ b/src/handlers/project/add/runtime-endpoint/index.test.ts @@ -22,7 +22,7 @@ async function run(args: string[], opts?: { isTTY?: boolean }) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io }; } diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index 072ae5fca6..3e7652a306 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -24,7 +24,7 @@ async function run(args: string[], opts?: { core?: TestCoreClient }) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io, core }; } diff --git a/src/handlers/project/add/runtime/runtime.screen.test.tsx b/src/handlers/project/add/runtime/runtime.screen.test.tsx index 86863a4a54..2a795d96ee 100644 --- a/src/handlers/project/add/runtime/runtime.screen.test.tsx +++ b/src/handlers/project/add/runtime/runtime.screen.test.tsx @@ -63,7 +63,7 @@ describe("project add runtime wizard", () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity, staleTime: Infinity } }, }); - const r = renderScreen("/agentcore/project/add/runtime", { queryClient }); + const r = renderScreen("/agentcore/add/runtime", { queryClient }); await waitForText(r.lastFrame, "what should this runtime be called?"); await r.write("orders_agent"); @@ -107,7 +107,7 @@ describe("project add runtime wizard", () => { test("scaffolds the template the user picks", async () => { const projectRoot = await inProject(); - const r = renderScreen("/agentcore/project/add/runtime"); + const r = renderScreen("/agentcore/add/runtime"); await waitForText(r.lastFrame, "what should this runtime be called?"); await r.write("packing_agent"); @@ -134,7 +134,7 @@ describe("project add runtime wizard", () => { test("a blank name is refused", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/runtime"); + const r = renderScreen("/agentcore/add/runtime"); await waitForText(r.lastFrame, "what should this runtime be called?"); await r.press("return"); @@ -146,7 +146,7 @@ describe("project add runtime wizard", () => { test("a name that breaks the schema's pattern is rejected as it is typed", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/runtime"); + const r = renderScreen("/agentcore/add/runtime"); await waitForText(r.lastFrame, "what should this runtime be called?"); // No enter: the name is checked while it is being typed, so the rule is @@ -159,7 +159,7 @@ describe("project add runtime wizard", () => { test("accepts a 48-character name", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/runtime"); + const r = renderScreen("/agentcore/add/runtime"); await waitForText(r.lastFrame, "what should this runtime be called?"); await r.write("a".repeat(48)); @@ -171,7 +171,7 @@ describe("project add runtime wizard", () => { test("rejects a name longer than 48 characters", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/runtime"); + const r = renderScreen("/agentcore/add/runtime"); await waitForText(r.lastFrame, "what should this runtime be called?"); await r.write("a".repeat(49)); @@ -182,7 +182,7 @@ describe("project add runtime wizard", () => { test("a name that is only valid once trimmed is refused, not silently trimmed", async () => { const projectRoot = await inProject(); - const r = renderScreen("/agentcore/project/add/runtime"); + const r = renderScreen("/agentcore/add/runtime"); await waitForText(r.lastFrame, "what should this runtime be called?"); await r.write(" orders_agent "); @@ -201,7 +201,7 @@ describe("project add runtime wizard", () => { // The name is taken, so addResource refuses it — the realistic failure, and // one the user can fix without starting over. await run(["add", "runtime", "--name", "orders_agent"]); - const r = renderScreen("/agentcore/project/add/runtime"); + const r = renderScreen("/agentcore/add/runtime"); await waitForText(r.lastFrame, "what should this runtime be called?"); await r.write("orders_agent"); @@ -225,7 +225,7 @@ describe("project add runtime wizard", () => { test("esc on the first step returns to the add menu", async () => { await inProject(); - const r = renderScreen("/agentcore/project/add/runtime"); + const r = renderScreen("/agentcore/add/runtime"); await waitForText(r.lastFrame, "what should this runtime be called?"); await r.press("escape"); @@ -236,7 +236,7 @@ describe("project add runtime wizard", () => { }); // These drive the real CLI entrypoint rather than mounting the screen, because -// what they cover is the routing in front of it: a bare `project add runtime` +// what they cover is the routing in front of it: a bare `agentcore add runtime` // has to reach the wizard, and everything else has to stay headless. describe("project add runtime dispatch", () => { function buildRoot(io: AppIO) { @@ -251,7 +251,7 @@ describe("project add runtime dispatch", () => { async function routeError(io: AppIO, args: string[]): Promise { return buildRoot(io) - .route(["node", "agentcore", "project", "add", "runtime", ...args]) + .route(["node", "agentcore", "add", "runtime", ...args]) .then( () => undefined, (caught: unknown) => caught, @@ -265,7 +265,7 @@ describe("project add runtime dispatch", () => { // outcome never rejects, so a mid-pump failure cannot trip bun's // unhandled-rejection detection before the final assertion. const outcome = buildRoot(streams.io) - .route(["node", "agentcore", "project", "add", "runtime"]) + .route(["node", "agentcore", "add", "runtime"]) .then( () => ({ ok: true as const }), (error: unknown) => ({ ok: false as const, error }), @@ -296,7 +296,7 @@ describe("project add runtime dispatch", () => { const io = testIO(); const error = await buildRoot(io.io) - .route(["node", "agentcore", "project", "add"]) + .route(["node", "agentcore", "add"]) .then( () => undefined, (caught: unknown) => caught, @@ -340,7 +340,6 @@ describe("project add runtime dispatch", () => { await buildRoot(streams.io).route([ "node", "agentcore", - "project", "add", "runtime", "--name", diff --git a/src/handlers/project/add/runtime/screen.tsx b/src/handlers/project/add/runtime/screen.tsx index c7a609063d..62bfeca0cc 100644 --- a/src/handlers/project/add/runtime/screen.tsx +++ b/src/handlers/project/add/runtime/screen.tsx @@ -22,9 +22,9 @@ import { } from "../../shortcuts"; import { toAddRuntimeInput, type RuntimeInput } from "./index"; -const BREADCRUMB = ["agentcore", "project", "add", "runtime"]; +const BREADCRUMB = ["agentcore", "add", "runtime"]; const DESCRIPTION = "add a Runtime to the current project"; -const ADD_MENU = "/agentcore/project/add"; +const ADD_MENU = "/agentcore/add"; const DEFAULT_TEMPLATE: RuntimeTemplateShortcutName = "agent-python-minimal"; diff --git a/src/handlers/project/build/index.test.ts b/src/handlers/project/build/index.test.ts index 0e74aabce6..80e62eab13 100644 --- a/src/handlers/project/build/index.test.ts +++ b/src/handlers/project/build/index.test.ts @@ -43,7 +43,7 @@ function testBuildCommand(options: TestBuildOptions = {}) { return { io, - run: (args: string[] = []) => root.route(["node", "agentcore", "project", "build", ...args]), + run: (args: string[] = []) => root.route(["node", "agentcore", "build", ...args]), }; } diff --git a/src/handlers/project/build/index.ts b/src/handlers/project/build/index.ts index c6d5a0444e..cbe0f42ee3 100644 --- a/src/handlers/project/build/index.ts +++ b/src/handlers/project/build/index.ts @@ -1,4 +1,4 @@ -import { createHandler, ProjectKey } from "../../../router"; +import { createHandler, ProjectKey, type Middleware } from "../../../router"; import type { AppIO } from "../../../io"; import { runWithProgress } from "../../../tui/progress"; import { JsonKey } from "../../keys"; @@ -8,6 +8,7 @@ import type { Project, ProjectManager } from "../types"; type BuildProjectHandlerConfig = { projectManager: ProjectManager; io: AppIO; + middlewares?: Middleware[]; }; /** The line both entry points print once a build finishes. */ @@ -19,6 +20,7 @@ export const createBuildProjectHandler = (config: BuildProjectHandlerConfig) => createHandler({ name: "build", description: "build the project's deployable artifacts", + middlewares: config.middlewares, handle: async (ctx) => { // withProject has already resolved the enclosing project. const project = ctx.require(ProjectKey); diff --git a/src/handlers/project/build/screen.tsx b/src/handlers/project/build/screen.tsx index 2257f08056..7c7298cf40 100644 --- a/src/handlers/project/build/screen.tsx +++ b/src/handlers/project/build/screen.tsx @@ -6,9 +6,9 @@ import { ProjectGate } from "../ProjectGate"; import type { Project } from "../types"; import { builtMessage } from "./index"; -const BREADCRUMB = ["agentcore", "project", "build"]; +const BREADCRUMB = ["agentcore", "build"]; const DESCRIPTION = "build the project's deployable artifacts"; -const PROJECT_MENU = "/agentcore/project"; +const PROJECT_MENU = "/agentcore"; // BuildProjectScreen runs the same projectManager.build generator the command // runs; ConfirmAction renders its steps through the same TaskList. @@ -44,7 +44,7 @@ function BuildConfirm({ project, core }: { project: Project; core: ScreenProps[" }} successTitle={builtMessage(project)} runningLabel="building…" - nextSteps={["agentcore project deploy"]} + nextSteps={["agentcore deploy"]} onDone={() => navigate(PROJECT_MENU)} doneLabel="go back" onCancel={() => navigate(PROJECT_MENU)} diff --git a/src/handlers/project/buildDeploy.screen.test.tsx b/src/handlers/project/buildDeploy.screen.test.tsx index ffe4dee297..5931542ea0 100644 --- a/src/handlers/project/buildDeploy.screen.test.tsx +++ b/src/handlers/project/buildDeploy.screen.test.tsx @@ -100,13 +100,13 @@ describe("project build screen", () => { const { backend } = fakeBackend(); const core = new TestCoreClient({ backends: { CDK: backend } }); await inProject(core); - const r = renderScreen("/agentcore/project/build", { core }); + const r = renderScreen("/agentcore/build", { core }); // Both steps settle to ✓, as the inline TaskList leaves them on the // command line; the finished steps' output tails collapse. await waitForText(r.lastFrame, "✔ Built project 'orders'"); const frame = r.lastFrame()!; - expect(frame).toContain("agentcore → project → build"); + expect(frame).toContain("agentcore → build"); // No frame — not even the first — advertised a question. expect(r.frames.some((painted) => painted.includes("(y/N)") || painted.includes("y/n"))).toBe( false, @@ -114,11 +114,11 @@ describe("project build screen", () => { expect(frame).toContain("✓ Synthesizing CloudFormation templates"); expect(frame).toContain("✓ Deploying stack"); expect(frame).not.toContain("cdk synth"); - expect(frame).toContain("agentcore project deploy"); + expect(frame).toContain("agentcore deploy"); - // Enter stays in the TUI: back to the project menu. + // Enter stays in the TUI: back to the root menu. await r.press("return"); - await waitForText(r.lastFrame, "manage an AgentCore project"); + await waitForText(r.lastFrame, "the platform for production AI agents"); r.unmount(); }); @@ -126,29 +126,29 @@ describe("project build screen", () => { const { backend } = fakeBackend({ failure: new Error("synth exploded") }); const core = new TestCoreClient({ backends: { CDK: backend } }); await inProject(core); - const r = renderScreen("/agentcore/project/build", { core }); + const r = renderScreen("/agentcore/build", { core }); await waitForText(r.lastFrame, "✗ synth exploded"); const frame = r.lastFrame()!; expect(frame).toContain("✓ Synthesizing CloudFormation templates"); expect(frame).toContain("✕ Deploying stack"); expect(frame).toContain("CREATE_IN_PROGRESS | AWS::IAM::Role"); - // With no confirmation to return to, esc leaves for the project menu + // With no confirmation to return to, esc leaves for the root menu // rather than running the build again. await r.press("escape"); - await waitForText(r.lastFrame, "manage an AgentCore project"); + await waitForText(r.lastFrame, "the platform for production AI agents"); r.unmount(); }); test("reports the CLI's own guidance outside a project", async () => { cleanups.push((await inTempDirectory()).cleanup); - const r = renderScreen("/agentcore/project/build"); + const r = renderScreen("/agentcore/build"); await waitForFlatText(r.lastFrame, "No AgentCore project found"); - expect(flatFrame(r.lastFrame)).toContain("agentcore project create"); + expect(flatFrame(r.lastFrame)).toContain("agentcore create"); // esc is a way off the error, not just ctrl+c. await r.press("escape"); - await waitForText(r.lastFrame, "manage an AgentCore project"); + await waitForText(r.lastFrame, "the platform for production AI agents"); r.unmount(); }); }); @@ -158,7 +158,7 @@ describe("project deploy screen", () => { const { backend, deploys } = fakeBackend(); const core = new TestCoreClient({ backends: { CDK: backend } }); await inProject(core); - const r = renderScreen("/agentcore/project/deploy", { core }); + const r = renderScreen("/agentcore/deploy", { core }); // A project with resources is not asked anything, as on the command line. await waitForText(r.lastFrame, "✔ Deployed project 'orders' to target 'default'"); @@ -190,7 +190,7 @@ describe("project deploy screen", () => { if (attempts === 1) throw new Error("aws-targets.json is unreadable"); return listTargets(project); }; - const r = renderScreen("/agentcore/project/deploy", { core }); + const r = renderScreen("/agentcore/deploy", { core }); await waitForText(r.lastFrame, "✗ aws-targets.json is unreadable"); expect(r.lastFrame()).toContain("[r] retry"); @@ -203,17 +203,17 @@ describe("project deploy screen", () => { r.unmount(); }); - test("esc leaves a target-loading failure for the project menu", async () => { + test("esc leaves a target-loading failure for the root menu", async () => { const core = new TestCoreClient({ backends: { CDK: fakeBackend().backend } }); await inProject(core); core.projectManager.listTargets = async () => { throw new Error("aws-targets.json is unreadable"); }; - const r = renderScreen("/agentcore/project/deploy", { core }); + const r = renderScreen("/agentcore/deploy", { core }); await waitForText(r.lastFrame, "✗ aws-targets.json is unreadable"); await r.press("escape"); - await waitForText(r.lastFrame, "manage an AgentCore project"); + await waitForText(r.lastFrame, "the platform for production AI agents"); r.unmount(); }); @@ -221,7 +221,7 @@ describe("project deploy screen", () => { const { backend, deploys } = fakeBackend(); const core = new TestCoreClient({ backends: { CDK: backend } }); await inProject(core, { staging: true }); - const r = renderScreen("/agentcore/project/deploy", { core }); + const r = renderScreen("/agentcore/deploy", { core }); await waitForText(r.lastFrame, "choose a deployment target"); const picker = flatFrame(r.lastFrame); @@ -245,12 +245,12 @@ describe("project deploy screen", () => { queries: { retry: false, gcTime: Infinity, staleTime: 0 }, }, }); - const r = renderScreen("/agentcore/project/deploy", { core, queryClient }); + const r = renderScreen("/agentcore/deploy", { core, queryClient }); await waitForText(r.lastFrame, "✔ Deployed project 'orders' to target 'default'"); expect(deploys).toHaveLength(1); await r.press("return"); - await waitForText(r.lastFrame, "manage an AgentCore project"); + await waitForText(r.lastFrame, "the platform for production AI agents"); await writeFile( join(projectRoot, "agentcore", "aws-targets.json"), @@ -273,12 +273,12 @@ describe("project deploy screen", () => { queries: { retry: false, gcTime: Infinity, staleTime: 0 }, }, }); - const r = renderScreen("/agentcore/project/deploy", { core, queryClient }); + const r = renderScreen("/agentcore/deploy", { core, queryClient }); await waitForText(r.lastFrame, "✔ Deployed project 'orders' to target 'default'"); expect(deploys).toHaveLength(1); await r.press("return"); - await waitForText(r.lastFrame, "manage an AgentCore project"); + await waitForText(r.lastFrame, "the platform for production AI agents"); await writeFile( join(projectRoot, "agentcore", "agentcore.json"), @@ -299,7 +299,7 @@ describe("project deploy screen", () => { resolveAccount: async () => "887863153624", }); await inProject(core, { targets: false }); - const r = renderScreen("/agentcore/project/deploy", { core }); + const r = renderScreen("/agentcore/deploy", { core }); await waitForText(r.lastFrame, "✔ Deployed project 'orders' to target 'default'"); const frame = flatFrame(r.lastFrame); @@ -313,7 +313,7 @@ describe("project deploy screen", () => { const { backend, deploys } = fakeBackend({ result: { outputs: {}, tornDown: true } }); const core = new TestCoreClient({ backends: { CDK: backend } }); await inProject(core, { empty: true }); - const r = renderScreen("/agentcore/project/deploy", { core }); + const r = renderScreen("/agentcore/deploy", { core }); await waitForFlatText(r.lastFrame, "declares no resources to deploy"); // Confirm lays its (y/N) inline, so the question wraps around it. @@ -334,7 +334,7 @@ describe("project deploy screen", () => { const { backend } = fakeBackend({ result: { outputs: {}, tornDown: true } }); const core = new TestCoreClient({ backends: { CDK: backend } }); await inProject(core); - const r = renderScreen("/agentcore/project/deploy", { core }); + const r = renderScreen("/agentcore/deploy", { core }); await waitForText(r.lastFrame, "✔ Removed project 'orders' from target 'default'"); expect(r.lastFrame()).not.toContain("Deployed project"); @@ -345,7 +345,7 @@ describe("project deploy screen", () => { const { backend } = fakeBackend({ failure: new Error("stack rolled back") }); const core = new TestCoreClient({ backends: { CDK: backend } }); await inProject(core); - const r = renderScreen("/agentcore/project/deploy", { core }); + const r = renderScreen("/agentcore/deploy", { core }); await waitForText(r.lastFrame, "✗ stack rolled back"); expect(r.lastFrame()).toContain("✕ Deploying stack"); diff --git a/src/handlers/project/create/create.screen.test.tsx b/src/handlers/project/create/create.screen.test.tsx index 40315adbed..fb111a9759 100644 --- a/src/handlers/project/create/create.screen.test.tsx +++ b/src/handlers/project/create/create.screen.test.tsx @@ -52,7 +52,7 @@ describe("project create wizard", () => { cleanups.push(cleanup); const core = new TestCoreClient(); const inputs = spyOnCreate(core); - const r = renderScreen("/agentcore/project/create", { core }); + const r = renderScreen("/agentcore/create", { core }); await waitForText(r.lastFrame, "name your project"); await r.write("DemoApp"); @@ -92,10 +92,10 @@ describe("project create wizard", () => { // Success: next steps point at the new directory and deploy. await waitForText(r.lastFrame, "✔ project created in ./DemoApp", 5000); expect(r.lastFrame()).toContain("cd DemoApp"); - expect(r.lastFrame()).toContain("agentcore project deploy"); + expect(r.lastFrame()).toContain("agentcore deploy"); // The manager received exactly the input the flag-driven handler builds - // for `project create --name DemoApp`. + // for `agentcore create --name DemoApp`. expect(inputs).toEqual([ { name: "DemoApp", @@ -119,7 +119,7 @@ describe("project create wizard", () => { cleanups.push((await inTempDirectory()).cleanup); const core = new TestCoreClient(); const inputs = spyOnCreate(core); - const r = renderScreen("/agentcore/project/create", { core }); + const r = renderScreen("/agentcore/create", { core }); await waitForText(r.lastFrame, "name your project"); await r.write("TunedApp"); @@ -155,7 +155,7 @@ describe("project create wizard", () => { cleanups.push(cleanup); const core = new TestCoreClient(); const inputs = spyOnCreate(core); - const r = renderScreen("/agentcore/project/create", { core }); + const r = renderScreen("/agentcore/create", { core }); const apiKeyArn = "arn:aws:bedrock-agentcore:us-east-1:123456789012:token-vault/default/apikeycredentialprovider/OpenAIKey"; @@ -217,7 +217,7 @@ describe("project create wizard", () => { }, 10000); test("switching providers preserves each provider's model input", async () => { - const r = renderScreen("/agentcore/project/create"); + const r = renderScreen("/agentcore/create"); await waitForText(r.lastFrame, "name your project"); await r.write("ProviderApp"); @@ -239,7 +239,7 @@ describe("project create wizard", () => { }); test("reveals model fields only after enter and hides them again on escape", async () => { - const r = renderScreen("/agentcore/project/create"); + const r = renderScreen("/agentcore/create"); await waitForText(r.lastFrame, "name your project"); await r.write("ModelApp"); @@ -265,7 +265,7 @@ describe("project create wizard", () => { }); test("the model picker remains readable in an 80x24 terminal", async () => { - const r = renderScreen("/agentcore/project/create"); + const r = renderScreen("/agentcore/create"); await r.resize(80, 24); await waitForText(r.lastFrame, "name your project"); @@ -277,7 +277,7 @@ describe("project create wizard", () => { const frame = r.lastFrame()!; const lines = frame.split("\n"); - expect(lines[0]).toContain("agentcore → project → create"); + expect(lines[0]).toContain("agentcore → create"); expect(lines[1]).toBe("─".repeat(80)); expect(lines[2]).toContain("✓ name"); expect(frame).toContain("● bedrock"); @@ -297,7 +297,7 @@ describe("project create wizard", () => { cleanups.push(cleanup); const core = new TestCoreClient(); const inputs = spyOnCreate(core); - const r = renderScreen("/agentcore/project/create", { core }); + const r = renderScreen("/agentcore/create", { core }); await waitForText(r.lastFrame, "name your project"); await r.write("StrandsApp"); @@ -348,7 +348,7 @@ describe("project create wizard", () => { cleanups.push(cleanup); const core = new TestCoreClient(); const inputs = spyOnCreate(core); - const r = renderScreen("/agentcore/project/create", { core }); + const r = renderScreen("/agentcore/create", { core }); await waitForText(r.lastFrame, "name your project"); await r.write("HelloApp"); @@ -386,7 +386,7 @@ describe("project create wizard", () => { cleanups.push((await inTempDirectory()).cleanup); const core = new TestCoreClient(); const inputs = spyOnCreate(core); - const r = renderScreen("/agentcore/project/create", { core }); + const r = renderScreen("/agentcore/create", { core }); await waitForText(r.lastFrame, "name your project"); await r.write("LangChainApp"); @@ -424,7 +424,7 @@ describe("project create wizard", () => { cleanups.push(cleanup); const core = new TestCoreClient(); const inputs = spyOnCreate(core); - const r = renderScreen("/agentcore/project/create", { core }); + const r = renderScreen("/agentcore/create", { core }); await waitForText(r.lastFrame, "name your project"); await r.write("EmptyApp"); @@ -450,7 +450,7 @@ describe("project create wizard", () => { }, 10000); test("the name step shows the schema's messages and blocks continuing", async () => { - const r = renderScreen("/agentcore/project/create"); + const r = renderScreen("/agentcore/create"); await waitForText(r.lastFrame, "name your project"); // Submitting an empty name surfaces the schema's required message. @@ -467,7 +467,7 @@ describe("project create wizard", () => { }); test("a reserved name is rejected with the schema's message", async () => { - const r = renderScreen("/agentcore/project/create"); + const r = renderScreen("/agentcore/create"); await waitForText(r.lastFrame, "name your project"); await r.write("bedrock"); @@ -478,7 +478,7 @@ describe("project create wizard", () => { }); test("a pasted chunk with a trailing return keeps the name clean", async () => { - const r = renderScreen("/agentcore/project/create"); + const r = renderScreen("/agentcore/create"); await waitForText(r.lastFrame, "name your project"); // A terminal paste (or keystrokes coalesced under load) arrives as one @@ -497,7 +497,7 @@ describe("project create wizard", () => { }); test("esc steps back through the flow and leaves from the first step", async () => { - const r = renderScreen("/agentcore/project/create"); + const r = renderScreen("/agentcore/create"); await waitForText(r.lastFrame, "name your project"); await r.write("DemoApp"); @@ -505,14 +505,14 @@ describe("project create wizard", () => { await waitForText(r.lastFrame, "what kind of agent to start with?"); await r.press("escape"); await waitForText(r.lastFrame, "name your project"); - // Esc on the first step lands on the project menu. + // Esc on the first step lands on the root menu. await r.press("escape"); - await waitForText(r.lastFrame, "manage an AgentCore project"); + await waitForText(r.lastFrame, "the platform for production AI agents"); r.unmount(); }); - test("selecting create from the project menu opens the wizard", async () => { - const r = renderScreen("/agentcore/project"); + test("selecting create from the root menu opens the wizard", async () => { + const r = renderScreen("/agentcore"); // `create` is the first menu item, so it is already selected. await waitForText(r.lastFrame, "❯ create"); @@ -539,7 +539,7 @@ describe("project create wizard", () => { throw new Error("stopped"); })(); }; - const r = renderScreen("/agentcore/project/create", { core }); + const r = renderScreen("/agentcore/create", { core }); await waitForText(r.lastFrame, "name your project"); await r.write("DemoApp"); @@ -582,7 +582,7 @@ describe("project create wizard", () => { throw new Error("'git' was not found on your PATH."); })(); }; - const r = renderScreen("/agentcore/project/create", { core }); + const r = renderScreen("/agentcore/create", { core }); await waitForText(r.lastFrame, "name your project"); await r.write("DemoApp"); @@ -624,7 +624,7 @@ describe("project create wizard", () => { const deep = join(path, "n".repeat(120)); await mkdir(deep); process.chdir(deep); - const r = renderScreen("/agentcore/project/create", { platform: "win32" }); + const r = renderScreen("/agentcore/create", { platform: "win32" }); await waitForText(r.lastFrame, "name your project"); await r.write("DemoApp"); @@ -663,7 +663,7 @@ describe("project create dispatch", () => { // outcome never rejects, so a mid-pump failure cannot trip bun's // unhandled-rejection detection before the final assertion. - const outcome = root.route(["node", "agentcore", "project", "create"]).then( + const outcome = root.route(["node", "agentcore", "create"]).then( () => ({ ok: true as const }), (error: unknown) => ({ ok: false as const, error }), ); @@ -702,7 +702,7 @@ describe("project create dispatch", () => { })(); const root = buildRoot(streams.io, core); - const outcome = root.route(["node", "agentcore", "project", "create"]); + const outcome = root.route(["node", "agentcore", "create"]); await waitFor(() => streams.stdout().includes("name your project")); stdin.write("DemoApp"); @@ -728,7 +728,7 @@ describe("project create dispatch", () => { const root = buildRoot(io.io); const error: unknown = await root - .route(["node", "agentcore", "project", "create"]) + .route(["node", "agentcore", "create"]) .then(() => undefined) .catch((caught: unknown) => caught); @@ -741,7 +741,7 @@ describe("project create dispatch", () => { const root = buildRoot(streams.io); const error: unknown = await root - .route(["node", "agentcore", "project", "create", "--skip-git"]) + .route(["node", "agentcore", "create", "--skip-git"]) .then(() => undefined) .catch((caught: unknown) => caught); @@ -754,7 +754,7 @@ describe("project create dispatch", () => { const root = buildRoot(streams.io); const error: unknown = await root - .route(["node", "agentcore", "project", "create", "--json"]) + .route(["node", "agentcore", "create", "--json"]) .then(() => undefined) .catch((caught: unknown) => caught); @@ -771,7 +771,6 @@ describe("project create dispatch", () => { await root.route([ "node", "agentcore", - "project", "create", "--name", "FlagApp", diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index b2e2041fcd..3b87e3edc2 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -146,7 +146,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = }, () => { config.io.stderr.write(`Created project '${name}' in ./${name}\n`); - config.io.stderr.write(`Next steps:\n cd ${name}\n agentcore project deploy\n`); + config.io.stderr.write(`Next steps:\n cd ${name}\n agentcore deploy\n`); }, ); }, @@ -160,7 +160,7 @@ type HarnessPathFlagValues = { "api-base"?: string; }; -// The harness input validates against the same schema `project add harness` +// The harness input validates against the same schema `agentcore add harness` // uses, before any file is written; the manager then scaffolds it through the // same addResource path. Exported so the TUI create wizard builds its harness // input through the exact same translation as the flag-driven path. diff --git a/src/handlers/project/create/screen.tsx b/src/handlers/project/create/screen.tsx index a056c85418..20bced6f30 100644 --- a/src/handlers/project/create/screen.tsx +++ b/src/handlers/project/create/screen.tsx @@ -201,7 +201,7 @@ function providerLabel(provider: HarnessModelProvider): string { // ─── wizard ─────────────────────────────────────────────────────────────────── -// ProjectCreateScreen is the interactive flow behind a bare `agentcore project +// ProjectCreateScreen is the interactive flow behind a bare `agentcore // create`: name → type → (model | template) → review, then the // creation itself, streaming the ProjectManager's progress events. It drives // core.projectManager.create with the same input the flag-driven handler @@ -217,11 +217,11 @@ export function ProjectCreateScreen({ ctx, core }: ScreenProps) { return ( navigate("/agentcore/project")} + onCancel={() => navigate("/agentcore")} onSubmit={() => { // Both of these throw before anything is written, so the wizard reports // them the way it reports a failed create — with the retry still on @@ -231,7 +231,7 @@ export function ProjectCreateScreen({ ctx, core }: ScreenProps) { }} runningLabel={`creating ${values.name}…`} successLabel={`project created in ./${values.name}`} - successNextSteps={[`cd ${values.name}`, "agentcore project deploy"]} + successNextSteps={[`cd ${values.name}`, "agentcore deploy"]} successHint="enter exits" onDone={() => { ctx.value(TuiExitMessageKey)?.(`Next step:\n cd ${values.name}/ && agentcore`); diff --git a/src/handlers/project/deploy/index.test.ts b/src/handlers/project/deploy/index.test.ts index 896eb6a954..fb3fccf416 100644 --- a/src/handlers/project/deploy/index.test.ts +++ b/src/handlers/project/deploy/index.test.ts @@ -116,7 +116,7 @@ function testDeployCommand( return { ...fake, io, - run: (args: string[] = []) => root.route(["node", "agentcore", "project", "deploy", ...args]), + run: (args: string[] = []) => root.route(["node", "agentcore", "deploy", ...args]), }; } diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index 6eae2d6964..d4eb9cb739 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -3,7 +3,13 @@ import z from "zod"; import { UserCancellationError } from "../../../errors/errors"; import type { AppIO } from "../../../io"; import { DEFAULT_TARGET_NAME } from "../../../projectSchemas/aws-targets"; -import { createHandler, flag, GlobalConfigAccessorKey, ProjectKey } from "../../../router"; +import { + createHandler, + flag, + GlobalConfigAccessorKey, + ProjectKey, + type Middleware, +} from "../../../router"; import { JsonRendererKey } from "../../../tui"; import { runWithProgress } from "../../../tui/progress"; import { JsonKey, RegionKey } from "../../keys"; @@ -13,6 +19,7 @@ import type { DeployResult, Project, ProjectManager, TeardownConfirmationHandler type DeployProjectHandlerConfig = { projectManager: ProjectManager; io: AppIO; + middlewares?: Middleware[]; }; /** The line both entry points print once a deploy finishes. */ @@ -45,6 +52,7 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = createHandler({ name: "deploy", description: "deploy the project to AWS", + middlewares: config.middlewares, flags: [ flag( "target", diff --git a/src/handlers/project/deploy/screen.tsx b/src/handlers/project/deploy/screen.tsx index 32b7b2a695..c066a4de5c 100644 --- a/src/handlers/project/deploy/screen.tsx +++ b/src/handlers/project/deploy/screen.tsx @@ -9,9 +9,9 @@ import { ProjectGate } from "../ProjectGate"; import type { Project } from "../types"; import { declaresNothingDeployable, deployedMessage, teardownQuestion } from "./index"; -const BREADCRUMB = ["agentcore", "project", "deploy"]; +const BREADCRUMB = ["agentcore", "deploy"]; const DESCRIPTION = "deploy the project to AWS"; -const PROJECT_MENU = "/agentcore/project"; +const PROJECT_MENU = "/agentcore"; // DeployProjectScreen runs the same projectManager.deploy generator the command // runs; ConfirmAction renders its steps through the same TaskList. With several diff --git a/src/handlers/project/dev/index.ts b/src/handlers/project/dev/index.ts index 1b5ce4f70c..7a389e34e8 100644 --- a/src/handlers/project/dev/index.ts +++ b/src/handlers/project/dev/index.ts @@ -15,7 +15,7 @@ import { UserCancellationError, } from "../../../errors"; import type { AppIO, BrowserOpener, FileWatcher, PortChecker, startHttpServer } from "../../../io"; -import { createHandler, flag, ProjectKey } from "../../../router"; +import { createHandler, flag, ProjectKey, type Middleware } from "../../../router"; import { JsonRendererKey, type JsonRenderer } from "../../../tui"; import { JsonKey, RegionKey } from "../../keys"; import type { Project, ProjectManager } from "../types"; @@ -27,6 +27,7 @@ const UI_DEFAULT_PORT = 8081; export type DevProjectHandlerConfig = { io: AppIO; + middlewares?: Middleware[]; runners: { CodeZip: DevRunner; Container: DevRunner }; loadDevEnvironment: DevEnvironmentLoader; checkPort: PortChecker; @@ -94,6 +95,7 @@ export const createDevProjectHandler = (config: DevProjectHandlerConfig) => createHandler({ name: "dev", description: "run the project locally for development", + middlewares: config.middlewares, flags: [ flag("agent", "Runtime to run", z.string().optional()), flag( diff --git a/src/handlers/project/export/harness.test.ts b/src/handlers/project/export/harness.test.ts index e14b1f89b8..5a91a3286d 100644 --- a/src/handlers/project/export/harness.test.ts +++ b/src/handlers/project/export/harness.test.ts @@ -34,8 +34,8 @@ function testExportCommand() { /** IO captured for the most recent invocation. */ io: undefined as unknown as ReturnType, core, - project: (args: string[]) => route(["project", ...args]), - run: (args: string[] = []) => route(["project", "export", "harness", ...args]), + project: (args: string[]) => route(args), + run: (args: string[] = []) => route(["export", "harness", ...args]), }; return subject; } diff --git a/src/handlers/project/export/harness.ts b/src/handlers/project/export/harness.ts index 4bf495baae..09a6097b1a 100644 --- a/src/handlers/project/export/harness.ts +++ b/src/handlers/project/export/harness.ts @@ -76,7 +76,7 @@ export const createExportHarnessHandler = (config: ExportProjectResourceConfig) config.io.stderr.write(`${line.text}\n`); } config.io.stderr.write( - "Next steps: review the generated code, then `agentcore project build` and `agentcore project deploy`\n", + "Next steps: review the generated code, then `agentcore build` and `agentcore deploy`\n", ); if (jsonOutput) { diff --git a/src/handlers/project/export/types.ts b/src/handlers/project/export/types.ts index b4d8b7cd12..07760d9930 100644 --- a/src/handlers/project/export/types.ts +++ b/src/handlers/project/export/types.ts @@ -2,7 +2,7 @@ import type { AppIO } from "../../../io"; import type { Core } from "../../types"; import type { ProjectManager } from "../types"; -/** Dependencies for `project export` handlers. */ +/** Dependencies for `agentcore export` handlers. */ export type ExportProjectResourceConfig = { projectManager: ProjectManager; /** Service clients, for exporting a harness fetched by ARN. */ diff --git a/src/handlers/project/index.ts b/src/handlers/project/index.ts index 60c25832b8..f552223274 100644 --- a/src/handlers/project/index.ts +++ b/src/handlers/project/index.ts @@ -1,11 +1,10 @@ -import { Router } from "../../router"; +import { type Handler } from "../../router"; import { checkPort, openBrowser, startHttpServer, watchFile, type AppIO } from "../../io"; import { CodeZipDevRunner } from "../../core/dev/codezip"; import { ContainerDevRunner } from "../../core/dev/container"; import { InspectorAssets } from "../../core/dev/inspectorAssets"; import { startOtelCollector } from "../../core/dev/otel/collector"; import { withProject, withTuiWhenInteractive } from "../../middleware"; -import { renderTui } from "../../tui"; import type { Core } from "../types"; import { createCreateProjectHandler } from "./create"; import { createRemoveProjectHandler } from "./remove"; @@ -21,102 +20,60 @@ import { createProjectInvokeHandler } from "./invoke"; import { createProjectLogHandler } from "./log"; import { createProjectTracesHandler } from "./traces"; -type ProjectHandlerConfig = { - core: Core; - io: AppIO; -}; - -export function createProjectHandler({ core, io }: ProjectHandlerConfig): Router { +export function createProjectHandlers(core: Core, io: AppIO): Handler[] { const projectManager: ProjectManager = core.projectManager; - const config = { projectManager, io, bedrockAgentImporter: core.bedrockAgentImporter }; - // The subcommands with a screen of their own. Every other subcommand is - // listed in the menu as command line only and opens its help instead (see - // CliOnlyScreen). `add` is a group: it has the resource menu, and which of - // its resources have a wizard is declared on that router. - const project = new Router("project", "manage an AgentCore project").supportedTuiCommands( - "create", - "invoke", - "build", - "deploy", - "status", - "add", - "remove", - ); - // Without a default, a bare `agentcore project` falls back to Commander's help - // and a usage exit code instead of the menu every sibling router opens. - project.default(renderTui(core, io)); + const createHandler = createCreateProjectHandler({ + projectManager, + io, + middlewares: [withTuiWhenInteractive(core, io)], + }); - // A bare `agentcore project create` in an interactive session opens the TUI - // create wizard; any user-supplied flag, --json, or a non-TTY invocation keeps - // the headless handler. The TUI is exposed as middleware so the router runs it - // before flag validation, letting the wizard supply a required flag. - project.handler( - createCreateProjectHandler({ + const withProjectMiddleware = withProject({ projectManager }); + const config = { projectManager, io, bedrockAgentImporter: core.bedrockAgentImporter }; + const projectBoundHandlers = [ + createAddProjectResourceHandler(config, core), + createExportProjectResourceHandler({ projectManager, core, io }), + createRemoveProjectHandler({ projectManager, io, - middlewares: [withTuiWhenInteractive(core, io)], + middlewares: [withProjectMiddleware, withTuiWhenInteractive(core, io)], }), - ); - project.handler(createAddProjectResourceHandler(config, core)); - project.handler(createExportProjectResourceHandler({ projectManager, core, io })); - project.handler( - createRemoveProjectHandler({ - projectManager: config.projectManager, - io: config.io, - middlewares: [ - withProject({ projectManager: config.projectManager }), - withTuiWhenInteractive(core, io), - ], + createDevProjectHandler({ + projectManager, + io, + middlewares: [withProjectMiddleware], + runners: { + CodeZip: new CodeZipDevRunner(), + Container: new ContainerDevRunner(), + }, + loadDevEnvironment, + checkPort, + startTraceCollector: startOtelCollector, + startServer: startHttpServer, + openBrowser, + inspectorAssets: new InspectorAssets(), + isInteractive: () => process.stdout.isTTY === true, + watchFile, }), - ); - project.handler( - withProject({ projectManager: config.projectManager })( - createDevProjectHandler({ - io: config.io, - runners: { - CodeZip: new CodeZipDevRunner(), - Container: new ContainerDevRunner(), - }, - loadDevEnvironment, - checkPort, - startTraceCollector: startOtelCollector, - startServer: startHttpServer, - openBrowser, - inspectorAssets: new InspectorAssets(), - isInteractive: () => process.stdout.isTTY === true, - watchFile, - projectManager: config.projectManager, - }), - ), - ); - project.handler( - withProject({ projectManager: config.projectManager })( - createDeployProjectHandler({ projectManager: config.projectManager, io: config.io }), - ), - ); - project.handler(createProjectInvokeHandler(core, io)); - project.handler(createProjectLogHandler(core, io)); - project.handler(createProjectTracesHandler(core, io)); - // A bare `agentcore project status` in an interactive session opens the TUI - // linked-resources screen; any user-supplied flag, --json, or a non-TTY - // invocation keeps the headless JSON report. withProject runs before the TUI - // so the not-found guidance is the CLI's own and the resolved project seeds - // the screen via ProjectKey. - const withStatusProject = withProject({ projectManager: config.projectManager }); - project.handler( + createDeployProjectHandler({ + projectManager, + io, + middlewares: [withProjectMiddleware], + }), + createProjectInvokeHandler(core, io), + createProjectLogHandler(core, io), + createProjectTracesHandler(core, io), createStatusProjectHandler({ - projectManager: config.projectManager, - middlewares: [withStatusProject, withTuiWhenInteractive(core, io)], + projectManager, + middlewares: [withProjectMiddleware, withTuiWhenInteractive(core, io)], + }), + createBuildProjectHandler({ + projectManager, + io, + middlewares: [withProjectMiddleware], }), - ); - // withProject wraps only the commands that require an existing project, so - // `create` (which refuses to nest inside one) stays unaffected. - project.handler( - withProject({ projectManager: config.projectManager })( - createBuildProjectHandler({ projectManager: config.projectManager, io: config.io }), - ), - ); + ]; - return project; + return [createHandler, ...projectBoundHandlers]; } diff --git a/src/handlers/project/invoke/index.test.tsx b/src/handlers/project/invoke/index.test.tsx index 42c33eb691..b8125865db 100644 --- a/src/handlers/project/invoke/index.test.tsx +++ b/src/handlers/project/invoke/index.test.tsx @@ -168,7 +168,7 @@ async function routedCommand( logger: createSilentLogger(), globalConfigAccessor: new TestGlobalConfigAccessor(), }); - const route = () => root.route(["node", "agentcore", "project", "invoke", ...args]); + const route = () => root.route(["node", "agentcore", "invoke", ...args]); return { core, io, resolved, route }; } @@ -401,7 +401,7 @@ describe("project invoke", () => { expect(code).toBe(ExitCode.FAILURE); expect(errors.join("\n")).toContain(`Could not reach local dev server on port ${port}`); expect(errors.join("\n")).toContain( - `agentcore project dev --mode headless --agent --port ${port}`, + `agentcore dev --mode headless --agent --port ${port}`, ); } finally { errorLog.mockRestore(); @@ -711,6 +711,6 @@ describe("project invoke", () => { await handler.defaultHandler()!.handle(context(project!), {}, {}); - expect(launches).toEqual(["/agentcore/project/invoke"]); + expect(launches).toEqual(["/agentcore/invoke"]); }); }); diff --git a/src/handlers/project/invoke/index.tsx b/src/handlers/project/invoke/index.tsx index 3c5c055b3b..3dbdfc82a3 100644 --- a/src/handlers/project/invoke/index.tsx +++ b/src/handlers/project/invoke/index.tsx @@ -23,6 +23,6 @@ export function createProjectInvokeHandler( "a Runtime or harness invoke subcommand is required with --json", ); } - return renderInvokeTui("/agentcore/project/invoke", ctx, core, io); + return renderInvokeTui("/agentcore/invoke", ctx, core, io); }); } diff --git a/src/handlers/project/invoke/invoke.screen.test.tsx b/src/handlers/project/invoke/invoke.screen.test.tsx index a7c52a7166..f93760d815 100644 --- a/src/handlers/project/invoke/invoke.screen.test.tsx +++ b/src/handlers/project/invoke/invoke.screen.test.tsx @@ -112,7 +112,7 @@ function core( describe("project invoke picker", () => { test("lists only resources present in the deployed target", async () => { - const screen = renderScreen("/agentcore/project/invoke", { + const screen = renderScreen("/agentcore/invoke", { core: core([ { resourceType: "harness", @@ -129,15 +129,15 @@ describe("project invoke picker", () => { expect(screen.lastFrame()).not.toContain("checkout"); }); - test("esc returns to the project command menu", async () => { - const screen = renderScreen("/agentcore/project/invoke", { + test("esc returns to the root command menu", async () => { + const screen = renderScreen("/agentcore/invoke", { core: core(), withContext: (ctx) => ctx.withValue(ProjectKey, project), }); await waitForText(screen.lastFrame, "checkout"); await screen.press("escape"); - await waitForText(screen.lastFrame, "manage an AgentCore project"); + await waitForText(screen.lastFrame, "the platform for production AI agents"); expect(screen.lastFrame()).toContain("invoke"); }); @@ -148,7 +148,7 @@ describe("project invoke picker", () => { requested = target; return { resources: DEPLOYED_RESOURCES, target: STAGING }; }; - const screen = renderScreen("/agentcore/project/invoke", { + const screen = renderScreen("/agentcore/invoke", { core: value, withContext: (ctx) => ctx.withValue(ProjectKey, project), }); @@ -174,7 +174,7 @@ describe("project invoke picker", () => { value.projectManager.resolveDeployedResources = async () => { throw new Error("No deployment targets are configured for project 'orders'."); }; - const screen = renderScreen("/agentcore/project/invoke", { + const screen = renderScreen("/agentcore/invoke", { core: value, withContext: (ctx) => ctx.withValue(ProjectKey, project), }); @@ -182,35 +182,35 @@ describe("project invoke picker", () => { await waitForText(screen.lastFrame, "No deployment targets are configured"); expect(screen.lastFrame()).not.toContain("checkout"); await screen.press("escape"); - await waitForText(screen.lastFrame, "manage an AgentCore project"); + await waitForText(screen.lastFrame, "the platform for production AI agents"); }); test("reports the CLI's own guidance outside a project", async () => { const { path: directory, cleanup } = await inTempDirectory(); cleanups.push(cleanup); - const screen = renderScreen("/agentcore/project/invoke", { core: core() }); + const screen = renderScreen("/agentcore/invoke", { core: core() }); await waitForFlatText(screen.lastFrame, "No AgentCore project found"); const frame = flatFrame(screen.lastFrame); expect(frame).toContain(directory); - expect(frame).toContain("agentcore project create"); + expect(frame).toContain("agentcore create"); expect(frame).not.toContain("Resolving project"); // esc is a way off the error, not just ctrl+c. await screen.press("escape"); - await waitForText(screen.lastFrame, "manage an AgentCore project"); + await waitForText(screen.lastFrame, "the platform for production AI agents"); }); - test("resolves the enclosing project when opened from the project menu", async () => { + test("resolves the enclosing project when opened from the root menu", async () => { const value = core(); value.projectManager.resolve = async () => project; - const screen = renderScreen("/agentcore/project/invoke", { core: value }); + const screen = renderScreen("/agentcore/invoke", { core: value }); await waitForText(screen.lastFrame, "checkout"); expect(screen.lastFrame()).toContain("support"); }); test("lists project Runtime and Harness resources", async () => { - const screen = renderScreen("/agentcore/project/invoke", { + const screen = renderScreen("/agentcore/invoke", { core: core(), withContext: (ctx) => ctx.withValue(ProjectKey, project), }); @@ -226,7 +226,7 @@ describe("project invoke picker", () => { test("opens the selected Harness chat in the same TUI", async () => { const value = core(); - const screen = renderScreen("/agentcore/project/invoke", { + const screen = renderScreen("/agentcore/invoke", { core: value, withContext: (ctx) => ctx.withValue(ProjectKey, project), }); @@ -245,7 +245,7 @@ describe("project invoke picker", () => { test("uses the existing Runtime endpoint picker before its JSON console", async () => { const value = core(); - const screen = renderScreen("/agentcore/project/invoke", { + const screen = renderScreen("/agentcore/invoke", { core: value, withContext: (ctx) => ctx.withValue(ProjectKey, project), }); diff --git a/src/handlers/project/invoke/screen.tsx b/src/handlers/project/invoke/screen.tsx index 4ff72e55e4..96ea15319b 100644 --- a/src/handlers/project/invoke/screen.tsx +++ b/src/handlers/project/invoke/screen.tsx @@ -35,9 +35,9 @@ type Destination = | { resourceType: "runtime"; id: string; ctx: Context; qualifier?: string } | { resourceType: "harness"; id: string; ctx: Context }; -const BREADCRUMB = ["agentcore", "project", "invoke"]; +const BREADCRUMB = ["agentcore", "invoke"]; const DESCRIPTION = "invoke a Runtime or harness from the current project"; -const PROJECT_MENU = "/agentcore/project"; +const PROJECT_MENU = "/agentcore"; // The project comes from the launch context when a project command opened the // TUI, and is resolved from the cwd otherwise — the gate reports the CLI's own diff --git a/src/handlers/project/log/harness.test.tsx b/src/handlers/project/log/harness.test.tsx index c33fb43761..72e04735e3 100644 --- a/src/handlers/project/log/harness.test.tsx +++ b/src/handlers/project/log/harness.test.tsx @@ -86,16 +86,7 @@ function command(projectBackend: ProjectBackend) { core, io, run: (args: string[] = []) => - root.route([ - "bun", - "agentcore", - "project", - "log", - "harness", - ...args, - "--region", - "us-east-1", - ]), + root.route(["bun", "agentcore", "log", "harness", ...args, "--region", "us-east-1"]), }; } diff --git a/src/handlers/project/log/runtime.test.tsx b/src/handlers/project/log/runtime.test.tsx index 9a47c47cac..163f3ced9f 100644 --- a/src/handlers/project/log/runtime.test.tsx +++ b/src/handlers/project/log/runtime.test.tsx @@ -104,16 +104,7 @@ function command(projectBackend: ProjectBackend) { core, io, run: (args: string[] = []) => - root.route([ - "bun", - "agentcore", - "project", - "log", - "runtime", - ...args, - "--region", - "us-east-1", - ]), + root.route(["bun", "agentcore", "log", "runtime", ...args, "--region", "us-east-1"]), }; } diff --git a/src/handlers/project/project.screen.test.tsx b/src/handlers/project/project.screen.test.tsx deleted file mode 100644 index 948b9735c0..0000000000 --- a/src/handlers/project/project.screen.test.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import { test, expect, describe, afterEach } from "bun:test"; -import { - renderScreen, - waitForFlatText, - waitForText, - cleanupScreens, - compiledRootCommand, - createSilentLogger, - menuEntries, - TestCoreClient, - TestGlobalConfigAccessor, - testIO, -} from "../../testing"; -import { ExitCode, InvalidEnvironmentError } from "../../errors"; -import { commandParameterDetails } from "../../router"; -import { createRootHandler } from "../index"; -import { - EMPTY_TEMPLATE_NAME, - RUNTIME_TEMPLATE_SHORTCUT_NAMES, - RUNTIME_TEMPLATE_SHORTCUTS, -} from "./shortcuts"; - -afterEach(cleanupScreens); - -// projectSubcommands reads the project group's children off the compiled -// Commander tree, so tests driven by it cover any subcommand added later. -function projectSubcommands(): string[] { - const root = compiledRootCommand(); - const project = root.commands.find((command) => command.name() === "project")!; - return project.commands.map((command) => command.name()); -} - -describe("project menu", () => { - test("lists every project subcommand", async () => { - const r = renderScreen("/agentcore/project"); - - await waitForText(r.lastFrame, "manage an AgentCore project"); - const frame = r.lastFrame()!; - for (const command of projectSubcommands()) { - expect(frame).toContain(command); - } - r.unmount(); - }); - - test("is reachable from the root menu", async () => { - const r = renderScreen("/agentcore"); - - await waitForText(r.lastFrame, "manage an AgentCore project"); - await r.write("project"); - await waitForText(r.lastFrame, "❯ project"); - await r.press("return"); - - await waitForText(r.lastFrame, "agentcore → project"); - expect(r.lastFrame()).toContain("create"); - r.unmount(); - }); - - test("esc returns to the root menu", async () => { - const r = renderScreen("/agentcore/project"); - - await waitForText(r.lastFrame, "agentcore → project"); - await r.press("escape"); - - await waitForText(r.lastFrame, "the platform for production AI agents"); - r.unmount(); - }); -}); - -// projectCommand resolves a compiled project subcommand by path, for reading -// the help the CLI-only screen must match. -function projectCommand(...path: string[]) { - const root = compiledRootCommand(); - let command = root.commands.find((c) => c.name() === "project")!; - for (const name of path) command = command.commands.find((c) => c.name() === name)!; - return command; -} - -describe("project menu: command-line-only subcommands", () => { - test("create and add runtime expose the shared registry-backed template help", () => { - const createDetails = commandParameterDetails(projectCommand("create"))!; - const addRuntimeDetails = commandParameterDetails(projectCommand("add", "runtime"))!; - - for (const name of RUNTIME_TEMPLATE_SHORTCUT_NAMES) { - const description = RUNTIME_TEMPLATE_SHORTCUTS[name].description; - expect(createDetails).toContain(name); - expect(createDetails).toContain(description); - expect(addRuntimeDetails).toContain(name); - expect(addRuntimeDetails).toContain(description); - } - expect(createDetails).toContain(EMPTY_TEMPLATE_NAME); - expect(addRuntimeDetails).not.toContain(EMPTY_TEMPLATE_NAME); - }); - - test("are listed below a divider, after the ones with a screen", async () => { - const r = renderScreen("/agentcore/project"); - - await waitForText(r.lastFrame, "command line only"); - const withScreens = ["create", "deploy", "invoke", "build", "status", "add", "remove"]; - const { screens, cliOnly } = menuEntries(r.lastFrame()!); - expect(screens.toSorted()).toEqual(withScreens.toSorted()); - expect(cliOnly.toSorted()).toEqual( - projectSubcommands() - .filter((c) => !withScreens.includes(c)) - .toSorted(), - ); - r.unmount(); - }); - - test("a group drills down to its leaves' help and back", async () => { - const r = renderScreen("/agentcore/project/add"); - - await waitForText(r.lastFrame, "agentcore → project → add"); - await r.write("gateway"); - await waitForText(r.lastFrame, "❯ gateway"); - await r.press("return"); - - await waitForText(r.lastFrame, "agentcore → project → add → gateway"); - const frame = r.lastFrame()!.replace(/\s+/g, " "); - expect(frame).toContain("this command runs from the command line"); - expect(frame).toContain("agentcore project add gateway [options]"); - expect(frame).toContain("--authorizer-type"); - - await r.press("escape"); - await waitForText(r.lastFrame, "agentcore → project → add"); - r.unmount(); - }); - - test("help longer than the terminal scrolls, and the parameter details are reachable", async () => { - // `add gateway-target` has ten options plus a long --target-configuration - // write-up, which `--help` appends as "Parameter details"; at 80×24 most of - // it is below the fold. - const r = renderScreen("/agentcore/project/add/gateway-target"); - await r.resize(80, 24); - await waitForText(r.lastFrame, "this command runs from the command line"); - expect(r.lastFrame()).not.toContain("curated Connector shortcuts"); - - // Scroll to the end: the write-up's last line is the last thing on the page. - for (let i = 0; i < 80; i++) await r.press("down"); - const bottom = r.lastFrame()!.replace(/\s+/g, " "); - expect(bottom).toContain("Use project add gateway-connector for curated Connector shortcuts."); - // …and the heading was on the way. - expect(r.frames.some((frame) => frame.includes("Parameter details:"))).toBe(true); - - for (let i = 0; i < 80; i++) await r.press("up"); - await waitForText(r.lastFrame, "this command runs from the command line"); - r.unmount(); - }); - - // These three exercise the help viewport, so they need a command-line-only - // resource whose help is longer than the terminal: `add payment-manager`. - test("growing the terminal after scrolling to the bottom pulls the content back into view", async () => { - const r = renderScreen("/agentcore/project/add/payment-manager"); - await r.resize(80, 24); - await waitForText(r.lastFrame, "this command runs from the command line"); - for (let i = 0; i < 80; i++) await r.press("down"); - expect(r.lastFrame()).not.toContain("this command runs from the command line"); - - // Tall enough for the whole help: the offset must fall back to the top - // rather than leave a mostly blank viewport. Height only — a width change - // reflows the content, which would mask a clamp that read a stale height. - await r.resize(80, 120); - await waitForText(r.lastFrame, "this command runs from the command line"); - expect(r.lastFrame()).toContain("--default-spend-limit"); - r.unmount(); - }); - - test("a key that fills its column still stands clear of its value", async () => { - const r = renderScreen("/agentcore/project/add/payment-manager"); - await r.resize(40, 60); - // Narrow enough that the intro wraps and the key column hits its cap. - await waitForFlatText(r.lastFrame, "this command runs from the command line"); - const lines = r.lastFrame()!.split("\n"); - // "--description " wraps within the capped column… - expect(lines.some((line) => /^\s+\s{2,}\S/.test(line))).toBe(true); - // …and no line runs a key straight into its value (checked case-insensitively; - // this is a terminal layout check, not an HTML filter). - expect(lines.some((line) => /<[a-z-]+>[a-z]/i.test(line))).toBe(false); - r.unmount(); - }); - - test("every option is reachable on a small terminal", async () => { - const r = renderScreen("/agentcore/project/add/payment-manager"); - await r.resize(80, 24); - await waitForText(r.lastFrame, "this command runs from the command line"); - - const seen = new Set(); - const collect = () => { - for (const match of r.lastFrame()!.matchAll(/--[a-z][a-z-]*/g)) seen.add(match[0]); - }; - collect(); - for (let i = 0; i < 60; i++) { - await r.press("down"); - collect(); - } - const compiled = projectCommand("add", "payment-manager"); - for (const option of compiled.options) { - if (option.long && option.long !== "--help") expect(seen).toContain(option.long); - } - r.unmount(); - }); - - test("an unknown project path retains the standard help fallback", async () => { - const r = renderScreen("/agentcore/project/no-such-command"); - await waitForText(() => r.frames.join("\n"), "Usage:"); - expect(r.frames.join("\n")).not.toContain("command line only"); - r.unmount(); - }); -}); - -describe("agentcore project (no subcommand)", () => { - // Exercises the real CLI entrypoint; the screen tests mount a path directly - // and so never caught the missing default handler. - // - // Asserts renderTui's TTY guard rather than a rendered frame: Ink only writes - // frames incrementally when interactive (`!isInCi && isTTY`), so asserting on - // frames here would pass locally and time out under CI. Reaching the guard at - // all proves the group routed to the TUI — Commander help neither throws nor - // touches stderr. - test("routes to the TUI rather than printing Commander help", async () => { - const io = testIO(); - const root = createRootHandler(new TestCoreClient(), { - io: io.io, - logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), - }); - - const caught: unknown = await root.route(["node", "agentcore", "project"]).then( - () => undefined, - (error: unknown) => error, - ); - - expect(caught).toBeInstanceOf(InvalidEnvironmentError); - expect((caught as InvalidEnvironmentError).exitCode).toBe(ExitCode.USAGE); - expect(io.stdout()).toBe(""); - }); - - test("prints help instead of the TUI under --json", async () => { - const io = testIO(); - const root = createRootHandler(new TestCoreClient(), { - io: io.io, - logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), - }); - - await root.route(["node", "agentcore", "project", "--json"]); - - expect(io.stdout()).toContain("Usage:"); - expect(io.stdout()).toContain("create"); - }); -}); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 66afc0712d..11558e9a16 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -28,7 +28,7 @@ async function run( logger: createSilentLogger(), platform: opts?.platform, }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io, core }; } @@ -223,7 +223,7 @@ describe("project create", () => { expect(io.stderr()).toContain("Syncing Python dependencies with uv"); expect(io.stderr()).toContain("Initializing git repository"); expect(io.stderr()).toContain("Created project 'MyAgent' in ./MyAgent"); - expect(io.stderr()).toContain("Next steps:\n cd MyAgent\n agentcore project deploy"); + expect(io.stderr()).toContain("Next steps:\n cd MyAgent\n agentcore deploy"); }); test("--skip-install and --skip-git run no commands", async () => { diff --git a/src/handlers/project/remove/index.test.ts b/src/handlers/project/remove/index.test.ts index 80090c955a..c09beedec1 100644 --- a/src/handlers/project/remove/index.test.ts +++ b/src/handlers/project/remove/index.test.ts @@ -31,7 +31,7 @@ async function run(args: string[], ioOptions?: TestIOOptions) { globalConfigAccessor: new TestGlobalConfigAccessor(), logger: createSilentLogger(), }); - await root.route(["node", "agentcore", "project", ...args]); + await root.route(["node", "agentcore", ...args]); return { io, core }; } diff --git a/src/handlers/project/remove/remove.screen.test.tsx b/src/handlers/project/remove/remove.screen.test.tsx index 6102accc56..fe314c0799 100644 --- a/src/handlers/project/remove/remove.screen.test.tsx +++ b/src/handlers/project/remove/remove.screen.test.tsx @@ -166,7 +166,7 @@ describe("project remove screen", () => { resourceConfig: { name: "prod", version: 1 }, }, ]); - const r = render("/agentcore/project/remove", core, project); + const r = render("/agentcore/remove", core, project); await waitForText(r.lastFrame, "choose a resource to remove from project orders"); const frame = r.lastFrame()!; @@ -196,7 +196,7 @@ describe("project remove screen", () => { test("lists the resource types the project holds plus an all option", async () => { const core = new TestCoreClient(); const { project } = await createProject(core, POLICY); - const r = render("/agentcore/project/remove", core, project); + const r = render("/agentcore/remove", core, project); await waitForText(r.lastFrame, "choose a resource to remove from project orders"); const frame = r.lastFrame()!; @@ -211,23 +211,23 @@ describe("project remove screen", () => { const core = new TestCoreClient(); const { project } = await createProject(core); process.chdir(project.rootPath); // cd into the project, as a user would; no ProjectKey injected - const r = renderScreen("/agentcore/project/remove", { core }); + const r = renderScreen("/agentcore/remove", { core }); await waitForText(r.lastFrame, "choose a resource to remove from project orders"); expect(r.lastFrame()).not.toContain("No AgentCore project"); r.unmount(); }); - test("esc from the no-project screen returns to the project menu", async () => { + test("esc from the no-project screen returns to the root menu", async () => { const core = new TestCoreClient(); const root = await mkdtemp(join(tmpdir(), "agentcore-no-project-")); temporaryDirectories.push(root); process.chdir(root); - const r = renderScreen("/agentcore/project/remove", { core }); + const r = renderScreen("/agentcore/remove", { core }); await waitForText(r.lastFrame, "No AgentCore project found"); await r.press("escape"); - await waitForText(r.lastFrame, "agentcore → project"); + await waitForText(r.lastFrame, "agentcore"); r.unmount(); }); @@ -238,7 +238,7 @@ describe("project remove screen", () => { resourceType: "runtime", name: project.spec.runtimes[0]!.name, }); - const r = render("/agentcore/project/remove", core, empty); + const r = render("/agentcore/remove", core, empty); await waitForText(r.lastFrame, "This project has no resources to remove."); expect(r.lastFrame()).not.toContain("all"); @@ -251,7 +251,7 @@ describe("project remove screen", () => { test("an empty resource-type list advertises only esc/ctrl+c", async () => { const core = new TestCoreClient(); const { project } = await createProject(core); // only a scaffolded runtime, no harnesses - const r = render("/agentcore/project/remove/harness", core, project); + const r = render("/agentcore/remove/harness", core, project); await waitForText(r.lastFrame, "This project has no harness resources."); expect(r.lastFrame()).toContain("esc"); @@ -266,7 +266,7 @@ describe("project remove screen", () => { resourceType: "runtime", name: project.spec.runtimes[0]!.name, }); - const r = render("/agentcore/project/remove/all", core, empty); + const r = render("/agentcore/remove/all", core, empty); await waitForText(r.lastFrame, "This project has no resources to remove."); // Static message screen — only esc/ctrl+c are advertised. @@ -287,12 +287,12 @@ describe("project remove screen", () => { spec: { ...empty.spec, knowledgeBases: [{ name: "kb" }] as ProjectSpec["knowledgeBases"] }, }; - const picker = render("/agentcore/project/remove", core, withKb); + const picker = render("/agentcore/remove", core, withKb); await waitForText(picker.lastFrame, "choose a resource to remove from project orders"); expect(picker.lastFrame()).toContain("all"); picker.unmount(); - const confirm = render("/agentcore/project/remove/all", core, withKb); + const confirm = render("/agentcore/remove/all", core, withKb); await waitForText(confirm.lastFrame, "Remove every resource from project orders?"); expect(confirm.lastFrame()).toContain("knowledge base"); confirm.unmount(); @@ -306,13 +306,13 @@ describe("project remove screen", () => { })); const { project } = await createProject(core, memories); - const many = render("/agentcore/project/remove/memory", core, project); + const many = render("/agentcore/remove/memory", core, project); await waitForText(many.lastFrame, "choose a memory to remove"); expect(many.lastFrame()).toContain("page"); // ←→/hl page hint on a paged list many.unmount(); // The runtime list has a single entry, so no paging hint. - const few = render("/agentcore/project/remove/runtime", core, project); + const few = render("/agentcore/remove/runtime", core, project); await waitForText(few.lastFrame, "choose a runtime to remove"); expect(few.lastFrame()).not.toContain("page"); few.unmount(); @@ -321,7 +321,7 @@ describe("project remove screen", () => { test("the all row counts the sum of every resource", async () => { const core = new TestCoreClient(); const { project } = await createProject(core, POLICY); - const r = render("/agentcore/project/remove", core, project); + const r = render("/agentcore/remove", core, project); await waitForText(r.lastFrame, "all"); // `all` = the sum of the listed rows: 1 runtime + 1 policy-engine + 1 policy. @@ -332,7 +332,7 @@ describe("project remove screen", () => { test("selecting a type lists that type's resources", async () => { const core = new TestCoreClient(); const { project } = await createProject(core); - const r = render("/agentcore/project/remove", core, project); + const r = render("/agentcore/remove", core, project); await waitForText(r.lastFrame, "runtime"); await r.press("return"); @@ -344,7 +344,7 @@ describe("project remove screen", () => { test("esc on the resource list returns to the resource-type list", async () => { const core = new TestCoreClient(); const { project } = await createProject(core); - const r = render("/agentcore/project/remove/runtime", core, project); + const r = render("/agentcore/remove/runtime", core, project); await waitForText(r.lastFrame, "choose a runtime to remove"); await r.press("escape"); @@ -352,21 +352,21 @@ describe("project remove screen", () => { r.unmount(); }); - test("esc on the resource-type list returns to the project menu", async () => { + test("esc on the resource-type list returns to the root menu", async () => { const core = new TestCoreClient(); const { project } = await createProject(core); - const r = render("/agentcore/project/remove", core, project); + const r = render("/agentcore/remove", core, project); await waitForText(r.lastFrame, "choose a resource to remove from project orders"); await r.press("escape"); - await waitForText(r.lastFrame, "agentcore → project"); + await waitForText(r.lastFrame, "agentcore"); r.unmount(); }); test("confirming a removal deletes the resource from the spec", async () => { const core = new TestCoreClient(); const { project, specPath } = await createProject(core); - const r = render("/agentcore/project/remove/runtime/0", core, project); + const r = render("/agentcore/remove/runtime/0", core, project); await waitForText(r.lastFrame, `Remove runtime '${RUNTIME}' from project orders?`); expect(r.lastFrame()).toContain("(y/N)"); @@ -386,7 +386,7 @@ describe("project remove screen", () => { const { project } = await createProject(core, POLICY); // ProjectKey is set in context (as the command wiring does): useProject uses // it as initialData and never refetches, so the removal must update the cache. - const r = render("/agentcore/project/remove/runtime/0", core, project); + const r = render("/agentcore/remove/runtime/0", core, project); await waitForText(r.lastFrame, `Remove runtime '${RUNTIME}' from project orders?`); await r.write("y"); @@ -408,7 +408,7 @@ describe("project remove screen", () => { const core = new TestCoreClient(); const { project } = await createProject(core, POLICY); process.chdir(project.rootPath); // cwd-resolve path, so the selector refreshes from disk - const r = renderScreen("/agentcore/project/remove/runtime/0", { core }); + const r = renderScreen("/agentcore/remove/runtime/0", { core }); await waitForText(r.lastFrame, `Remove runtime '${RUNTIME}' from project orders?`); await r.write("y"); @@ -431,7 +431,7 @@ describe("project remove screen", () => { test("declining leaves the resource in place", async () => { const core = new TestCoreClient(); const { project, specPath } = await createProject(core); - const r = render("/agentcore/project/remove/runtime/0", core, project); + const r = render("/agentcore/remove/runtime/0", core, project); await waitForText(r.lastFrame, `Remove runtime '${RUNTIME}' from project orders?`); await r.write("n"); @@ -444,7 +444,7 @@ describe("project remove screen", () => { test("lists a nested resource with its parent and removes it", async () => { const core = new TestCoreClient(); const { project, specPath } = await createProject(core, POLICY); - const list = render("/agentcore/project/remove/policy", core, project); + const list = render("/agentcore/remove/policy", core, project); await waitForText(list.lastFrame, "choose a policy to remove"); const frame = list.lastFrame()!; @@ -453,7 +453,7 @@ describe("project remove screen", () => { expect(frame).toContain("denyAll"); // policy name list.unmount(); - const confirm = render("/agentcore/project/remove/policy/0", core, project); + const confirm = render("/agentcore/remove/policy/0", core, project); await waitForText(confirm.lastFrame, "Remove policy 'denyAll' from project orders?"); expect(confirm.lastFrame()).toContain("guard"); // parent shown in the summary await confirm.write("y"); @@ -467,7 +467,7 @@ describe("project remove screen", () => { test("the remove-all summary itemizes the resource types being removed", async () => { const core = new TestCoreClient(); const { project } = await createProject(core, POLICY); - const r = render("/agentcore/project/remove/all", core, project); + const r = render("/agentcore/remove/all", core, project); await waitForText(r.lastFrame, "Remove every resource from project orders?"); const frame = r.lastFrame()!; @@ -480,7 +480,7 @@ describe("project remove screen", () => { test("enter after remove-all refreshes the list when the project is pinned in context", async () => { const core = new TestCoreClient(); const { project } = await createProject(core, POLICY); - const r = render("/agentcore/project/remove/all", core, project); // ProjectKey pinned in context + const r = render("/agentcore/remove/all", core, project); // ProjectKey pinned in context await waitForText(r.lastFrame, "Remove every resource from project orders?"); await r.write("y"); @@ -498,7 +498,7 @@ describe("project remove screen", () => { test("removing all empties every resource collection", async () => { const core = new TestCoreClient(); const { project, specPath } = await createProject(core, POLICY); - const r = render("/agentcore/project/remove/all", core, project); + const r = render("/agentcore/remove/all", core, project); await waitForText(r.lastFrame, "Remove every resource from project orders?"); await r.write("y"); diff --git a/src/handlers/project/remove/screen.tsx b/src/handlers/project/remove/screen.tsx index daff3bd1b1..600f8713d1 100644 --- a/src/handlers/project/remove/screen.tsx +++ b/src/handlers/project/remove/screen.tsx @@ -148,8 +148,8 @@ const RESOURCE_PICKER_CONFIGS: RemovableResourcePickerConfig[] = [ }, ]; -const PROJECT_MENU = "/agentcore/project"; -const REMOVE_ROOT = "/agentcore/project/remove"; +const PROJECT_MENU = "/agentcore"; +const REMOVE_ROOT = "/agentcore/remove"; // Nothing to navigate or select on an empty list or the nothing-to-remove message. const STATIC_KEY_HINTS = [ @@ -203,7 +203,7 @@ export function ProjectRemoveScreen({ ctx, core }: ScreenProps) { return ( navigate(PROJECT_MENU)} > @@ -252,7 +252,7 @@ function ResourceTypePicker({ project }: { project: Project }) { return ( 0 ? KEY_HINTS : STATIC_KEY_HINTS} > @@ -303,7 +303,7 @@ function ResourcePicker({ return ( @@ -345,7 +345,7 @@ function RemoveConfirm({ return ( + This project has no resources to remove. ); @@ -411,7 +411,7 @@ function RemoveAllConfirm({ project, core }: { project: Project; core: ScreenPro return ( ; -} diff --git a/src/handlers/project/status/index.test.ts b/src/handlers/project/status/index.test.ts index c8414663fc..b7381322cf 100644 --- a/src/handlers/project/status/index.test.ts +++ b/src/handlers/project/status/index.test.ts @@ -58,7 +58,7 @@ function statusCommand(backend: ProjectBackend, io = testIO()) { return { io, json: () => JSON.parse(io.stdout()), - run: (args: string[] = []) => root.route(["node", "agentcore", "project", "status", ...args]), + run: (args: string[] = []) => root.route(["node", "agentcore", "status", ...args]), }; } @@ -281,7 +281,7 @@ describe("project status handler", () => { await inProject({ memories: [memory("shortTerm")] }, []); await expect(subject.run()).rejects.toThrow( - /No deployment targets are configured for project 'orders'\. Please deploy your project using 'agentcore project deploy'\./, + /No deployment targets are configured for project 'orders'\. Please deploy your project using 'agentcore deploy'\./, ); expect(subject.targets).toEqual([]); }); diff --git a/src/handlers/project/status/screen.tsx b/src/handlers/project/status/screen.tsx index d849fea9f8..6d6ad4a8fe 100644 --- a/src/handlers/project/status/screen.tsx +++ b/src/handlers/project/status/screen.tsx @@ -19,9 +19,9 @@ import { LoadingFrame, ProjectGate } from "../ProjectGate"; const theme = darkTheme; -const BREADCRUMB = ["agentcore", "project", "status"]; +const BREADCRUMB = ["agentcore", "status"]; const DESCRIPTION = "the project's linked resources"; -const PROJECT_MENU = "/agentcore/project"; +const PROJECT_MENU = "/agentcore"; // The detail routes a deployed resource can forward to. Types without a detail // screen are listed but not navigable. @@ -255,7 +255,7 @@ function ProjectStatusView({ {nodes.length === 0 ? ( - No resources are declared in this project. Run `agentcore project add` to declare one. + No resources are declared in this project. Run `agentcore add` to declare one. ) : ( diff --git a/src/handlers/project/status/status.screen.test.tsx b/src/handlers/project/status/status.screen.test.tsx index 0c4d63d89a..27eee3596b 100644 --- a/src/handlers/project/status/status.screen.test.tsx +++ b/src/handlers/project/status/status.screen.test.tsx @@ -103,7 +103,7 @@ function core( } function renderStatus(value: TestCoreClient, seed: Project = RUNTIME_PROJECT) { - return renderScreen("/agentcore/project/status", { + return renderScreen("/agentcore/status", { core: value, withContext: (ctx) => ctx.withValue(ProjectKey, seed), }); @@ -241,7 +241,7 @@ describe("project status screen", () => { await screen.press("return"); await waitForText(screen.lastFrame, "credential svc-key has no detail view."); - expect(screen.lastFrame()).toContain("agentcore → project → status"); + expect(screen.lastFrame()).toContain("agentcore → status"); }); test("left and right arrows collapse and expand an agent group", async () => { @@ -255,12 +255,12 @@ describe("project status screen", () => { await waitForText(screen.lastFrame, "runtime"); }); - test("esc returns to the project command menu", async () => { + test("esc returns to the root command menu", async () => { const screen = renderStatus(core()); await waitForGroup(screen); await screen.press("escape"); - await waitForText(screen.lastFrame, "manage an AgentCore project"); + await waitForText(screen.lastFrame, "the platform for production AI agents"); }); test("shows resolution errors with the standard treatment", async () => { @@ -273,7 +273,7 @@ describe("project status screen", () => { await waitForText(screen.lastFrame, "No deployment targets are configured"); expect(screen.lastFrame()).toContain("✗"); await screen.press("escape"); - await waitForText(screen.lastFrame, "manage an AgentCore project"); + await waitForText(screen.lastFrame, "the platform for production AI agents"); }); test("an empty project reports that nothing is declared", async () => { @@ -338,11 +338,9 @@ describe("project status screen", () => { await screen.press("escape"); await waitForText(screen.lastFrame, "choose a deployment target"); await screen.press("escape"); - await waitForText(screen.lastFrame, "manage an AgentCore project"); + await waitForText(screen.lastFrame, "the platform for production AI agents"); // Past the screen that pinned, the menus and what they open fetch in the // launch region again. - await screen.press("escape"); - await waitForText(screen.lastFrame, "❯ project"); await screen.write("harness"); await screen.press("return"); await waitForText(screen.lastFrame, "agentcore → harness"); @@ -355,11 +353,11 @@ describe("project status screen", () => { test("reports the CLI's own guidance outside a project", async () => { cleanups.push((await inTempDirectory()).cleanup); - const screen = renderScreen("/agentcore/project/status", { core: core() }); + const screen = renderScreen("/agentcore/status", { core: core() }); await waitForFlatText(screen.lastFrame, "No AgentCore project found"); - expect(flatFrame(screen.lastFrame)).toContain("agentcore project create"); + expect(flatFrame(screen.lastFrame)).toContain("agentcore create"); await screen.press("escape"); - await waitForText(screen.lastFrame, "manage an AgentCore project"); + await waitForText(screen.lastFrame, "the platform for production AI agents"); }); }); diff --git a/src/handlers/project/traces/harness.test.tsx b/src/handlers/project/traces/harness.test.tsx index 6fc9ace173..d6fb1b9a16 100644 --- a/src/handlers/project/traces/harness.test.tsx +++ b/src/handlers/project/traces/harness.test.tsx @@ -91,16 +91,7 @@ function command(projectBackend: ProjectBackend) { core, io, run: (args: string[]) => - root.route([ - "bun", - "agentcore", - "project", - "traces", - "harness", - ...args, - "--region", - "us-east-1", - ]), + root.route(["bun", "agentcore", "traces", "harness", ...args, "--region", "us-east-1"]), }; } diff --git a/src/handlers/project/traces/runtime.test.tsx b/src/handlers/project/traces/runtime.test.tsx index 582b8fa3b3..990009609e 100644 --- a/src/handlers/project/traces/runtime.test.tsx +++ b/src/handlers/project/traces/runtime.test.tsx @@ -103,16 +103,7 @@ function command(projectBackend: ProjectBackend) { core, io, run: (args: string[]) => - root.route([ - "bun", - "agentcore", - "project", - "traces", - "runtime", - ...args, - "--region", - "us-east-1", - ]), + root.route(["bun", "agentcore", "traces", "runtime", ...args, "--region", "us-east-1"]), }; } diff --git a/src/handlers/root.test.tsx b/src/handlers/root.test.tsx index 2a128f467d..57db288fd3 100644 --- a/src/handlers/root.test.tsx +++ b/src/handlers/root.test.tsx @@ -11,7 +11,17 @@ describe("createRootHandler", () => { }); expect(root.name()).toBe("agentcore"); expect(root.children().map((c) => c.name())).toEqual([ - "project", + "create", + "add", + "export", + "remove", + "dev", + "deploy", + "invoke", + "log", + "traces", + "status", + "build", "harness", "identity", "runtime", diff --git a/src/io/openBrowser.ts b/src/io/openBrowser.ts index 1d6bfbe683..9e4990c405 100644 --- a/src/io/openBrowser.ts +++ b/src/io/openBrowser.ts @@ -5,7 +5,7 @@ export type BrowserOpener = (url: string) => Promise; /** * Open a URL in the user's default browser, best-effort: failures resolve * quietly because the URL is always printed as well, so a machine without a - * browser association must not fail `project dev`. + * browser association must not fail `agentcore dev`. */ export const openBrowser: BrowserOpener = (url) => { const [command, args] = diff --git a/src/middleware/withProject.tsx b/src/middleware/withProject.tsx index f2166151f3..d49f8ccc6d 100644 --- a/src/middleware/withProject.tsx +++ b/src/middleware/withProject.tsx @@ -20,7 +20,7 @@ export function projectNotFoundMessage(from: string): string { return ( `No AgentCore project found at ${from} or any parent directory ` + `(looked for agentcore/agentcore.json). ` + - `Run 'agentcore project create' to scaffold one.` + `Run 'agentcore create' to scaffold one.` ); } diff --git a/src/projectSchemas/aws-targets.ts b/src/projectSchemas/aws-targets.ts index d3dde8a719..b3dc0c5ec3 100644 --- a/src/projectSchemas/aws-targets.ts +++ b/src/projectSchemas/aws-targets.ts @@ -29,7 +29,7 @@ export const AgentCoreRegionSchema = z.enum([ ]); /** - * The target `project deploy` uses when --target is omitted. Only this target + * The target `agentcore deploy` uses when --target is omitted. Only this target * is ever synthesized from the environment when aws-targets.json lacks it; * named targets must be defined explicitly so a typo cannot invent one. */ diff --git a/src/projectSchemas/harness.test.ts b/src/projectSchemas/harness.test.ts index e6f84bb2b9..4f4b26dd20 100644 --- a/src/projectSchemas/harness.test.ts +++ b/src/projectSchemas/harness.test.ts @@ -42,7 +42,7 @@ describe("harness custom validation", () => { }); // The pinned @aws/agentcore-cdk rejects additionalParams on every provider but lite_llm, and // re-parses harness.yaml at synth — so accepting it here would defer the failure to - // `project build` instead of surfacing it at authoring time. + // `agentcore build` instead of surfacing it at authoring time. it("accepts additional parameters only for the lite_llm provider", () => { expect( HarnessModelSchema.safeParse({ diff --git a/src/testing/projects.ts b/src/testing/projects.ts index f20eee8451..8857cd4f74 100644 --- a/src/testing/projects.ts +++ b/src/testing/projects.ts @@ -8,9 +8,9 @@ import { TestCoreClient } from "./TestCoreClient"; import { TestGlobalConfigAccessor } from "./globalConfig"; export type InitProjectOptions = { - /** Project name passed to `project create`. */ + /** Project name passed to `agentcore create`. */ name?: string; - /** Extra flags appended to the `project create` command, e.g. `["--template", "empty"]`. */ + /** Extra flags appended to the `agentcore create` command, e.g. `["--template", "empty"]`. */ flags?: string[]; /** Temp directory prefix, for recognizable paths while debugging. */ prefix?: string; @@ -25,7 +25,7 @@ export type InitializedProject = { cleanup: () => Promise; }; -/** Scaffolds a project with `project create` and cds into it so withProject resolves it. */ +/** Scaffolds a project with `agentcore create` and cds into it so withProject resolves it. */ export async function initProject(options: InitProjectOptions = {}): Promise { const { name = "TestProject", @@ -45,7 +45,6 @@ export async function initProject(options: InitProjectOptions = {}): Promise Date: Wed, 23 Sep 2026 23:36:44 +0000 Subject: [PATCH 2/2] fix(e2e): retain local dev output on failure --- e2eTest/project/templates.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/e2eTest/project/templates.test.ts b/e2eTest/project/templates.test.ts index 66d2e98aa2..133c9d6852 100644 --- a/e2eTest/project/templates.test.ts +++ b/e2eTest/project/templates.test.ts @@ -187,10 +187,13 @@ describe( const runtimePorts = new Map(); let dev: ReturnType | undefined; let pendingOutput = ""; + let devOutput = ""; /** Given dev-process output, records the ports announced by running runtimes. */ const captureDevOutput = (chunk: Buffer) => { - pendingOutput += chunk.toString(); + const text = chunk.toString(); + devOutput += text; + pendingOutput += text; const lines = pendingOutput.split(/\r?\n/); pendingOutput = lines.pop() ?? ""; @@ -221,21 +224,21 @@ describe( async (runtime) => { const sessionId = getSessionId(`${runtime.name}`); - // the server may take a bit to get ready, so we retry on a timeout. + // The server may take a bit to get ready, so we retry on a timeout. const response = await retry(async () => { if (!dev) { - throw new Error(`agentcore dev did not start. \nstdout/stdout = ${pendingOutput}`); + throw new Error(`agentcore dev did not start. \nstdout/stderr = ${devOutput}`); } if (dev.exitCode !== null) { throw new Error( - `agentcore dev exited with code ${dev.exitCode ?? "unknown"}. \nstdout/stdout = ${pendingOutput}`, + `agentcore dev exited with code ${dev.exitCode ?? "unknown"}. \nstdout/stderr = ${devOutput}`, ); } const port = runtimePorts.get(runtime.name); if (!port) { throw new Error( - `Runtime '${runtime.name}' is not ready. \nstdout/stdout = ${pendingOutput}`, + `Runtime '${runtime.name}' is not ready. \nstdout/stderr = ${devOutput}`, ); }