From d16014e38fb7fb38d2b5ec1c2aae05e9951937a6 Mon Sep 17 00:00:00 2001 From: gitikavj Date: Fri, 4 Sep 2026 05:12:38 +0000 Subject: [PATCH 1/2] feat(cli): add consistent JSON output to project mutations --- .../project/add/config-bundle/index.ts | 30 +++++----- .../project/add/credentials/shared.ts | 18 +++--- .../project/add/evaluator/code-based/index.ts | 36 ++++++------ .../add/evaluator/llm-as-a-judge/index.ts | 18 +++--- .../add/gateway-connector/index.test.ts | 28 ++++++++++ .../project/add/gateway-connector/index.ts | 18 +++--- .../project/add/gateway-target/index.ts | 17 +++--- src/handlers/project/add/gateway/index.ts | 17 +++--- src/handlers/project/add/harness/index.ts | 18 +++--- src/handlers/project/add/memory/index.test.ts | 12 ++++ src/handlers/project/add/memory/index.ts | 18 +++--- src/handlers/project/add/online-eval/index.ts | 18 +++--- .../project/add/online-insight/index.ts | 16 +++--- .../project/add/payment-connector/index.ts | 38 ++++++------- .../project/add/payment-manager/index.ts | 55 ++++++++++--------- .../project/add/policy-engine/index.ts | 29 +++++----- src/handlers/project/add/policy/index.ts | 17 +++--- src/handlers/project/add/runtime/index.ts | 18 +++--- src/handlers/project/add/shared.ts | 54 ++++++++++++++++++ src/handlers/project/create/index.ts | 16 +++++- src/handlers/project/output.ts | 42 ++++++++++++++ src/handlers/project/project.test.ts | 45 +++++++++++++++ src/handlers/project/remove/index.test.ts | 19 +++++++ src/handlers/project/remove/index.ts | 31 ++++++++++- src/router/router.test.ts | 12 ++++ src/router/router.tsx | 1 + 26 files changed, 461 insertions(+), 180 deletions(-) create mode 100644 src/handlers/project/add/shared.ts create mode 100644 src/handlers/project/output.ts diff --git a/src/handlers/project/add/config-bundle/index.ts b/src/handlers/project/add/config-bundle/index.ts index f78154024..98548474d 100644 --- a/src/handlers/project/add/config-bundle/index.ts +++ b/src/handlers/project/add/config-bundle/index.ts @@ -12,6 +12,7 @@ import { KmsKeyArnSchema } from "../../../../projectSchemas/evaluator"; import { createHandler, flag, ProjectKey } from "../../../../router"; import { parseJsonFlagWithSchema } from "../../../utils"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; const ComponentsSchema = z .record(z.string().min(1), ComponentConfigurationSchema.strict()) @@ -68,20 +69,21 @@ export const createAddConfigBundleHandler = (config: AddProjectResourceConfig) = } const project = ctx.require(ProjectKey); - for await (const event of config.projectManager.addResource(project, { - resourceType: "config-bundle", - resourceConfig: { - name: flags.name, - description: flags.description, - components, - branchName: flags["branch-name"], - commitMessage: flags["commit-message"], - kmsKeyArn: flags["kms-key-arn"], + await addProjectResource( + ctx, + config, + { + resourceType: "config-bundle", + resourceConfig: { + name: flags.name, + description: flags.description, + components, + branchName: flags["branch-name"], + commitMessage: flags["commit-message"], + kmsKeyArn: flags["kms-key-arn"], + }, }, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write(`added configuration bundle '${flags.name}' to '${project.name}'\n`); + `added configuration bundle '${flags.name}' to '${project.name}'\n`, + ); }, }); diff --git a/src/handlers/project/add/credentials/shared.ts b/src/handlers/project/add/credentials/shared.ts index 1f1c23a22..4afc8e3b9 100644 --- a/src/handlers/project/add/credentials/shared.ts +++ b/src/handlers/project/add/credentials/shared.ts @@ -8,6 +8,7 @@ import { credentialEnvVarName, credentialNameFieldSuffix, } from "../../../../projectSchemas/credential"; +import { addProjectResource } from "../shared"; export { credentialEnvVarName }; @@ -68,14 +69,15 @@ export async function addCredentialToProject( ); } - for await (const event of config.projectManager.addResource(project, { - resourceType: "credential", - ...input, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write(`added credential '${input.resourceConfig.name}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "credential", + ...input, + }, + `added credential '${input.resourceConfig.name}' to '${project.name}'\n`, + ); for (const entry of (input.envEntries ?? []).filter((e) => e.value === undefined)) { config.io.stderr.write(`Set ${entry.key} in agentcore/.env.local before you deploy.\n`); } diff --git a/src/handlers/project/add/evaluator/code-based/index.ts b/src/handlers/project/add/evaluator/code-based/index.ts index ad9db7986..bbb6355c7 100644 --- a/src/handlers/project/add/evaluator/code-based/index.ts +++ b/src/handlers/project/add/evaluator/code-based/index.ts @@ -14,6 +14,7 @@ import { } from "../../../types"; import { parseJsonFlagWithSchema } from "../../../../utils"; import type { AddProjectResourceConfig } from "../../types"; +import { addProjectResource } from "../../shared"; export const createAddCodeBasedEvaluatorHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -83,13 +84,15 @@ export const createAddCodeBasedEvaluatorHandler = (config: AddProjectResourceCon config: { codeBased: { external: { lambdaArn: flags["lambda-arn"] } } }, }); if (!parsed.success) throw new InputValidationError(z.prettifyError(parsed.error)); - for await (const event of config.projectManager.addResource(project, { - resourceType: "evaluator", - resourceConfig: parsed.data, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - config.io.stderr.write(`added evaluator '${flags["name"]}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "evaluator", + resourceConfig: parsed.data, + }, + `added evaluator '${flags["name"]}' to '${project.name}'\n`, + ); return; } @@ -102,15 +105,16 @@ export const createAddCodeBasedEvaluatorHandler = (config: AddProjectResourceCon ...(flags["timeout-seconds"] !== undefined && { timeoutSeconds: flags["timeout-seconds"] }), }; - for await (const event of config.projectManager.addResource(project, { - resourceType: "evaluator", - resourceConfig: { name: scaffold.name }, - scaffold, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write(`added evaluator '${flags["name"]}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "evaluator", + resourceConfig: { name: scaffold.name }, + scaffold, + }, + `added evaluator '${flags["name"]}' to '${project.name}'\n`, + ); if (!hasMetric) config.io.stderr.write( `note: this evaluator returns Pass for every session until you implement app/${flags["name"]}/lambda_function.py\n`, diff --git a/src/handlers/project/add/evaluator/llm-as-a-judge/index.ts b/src/handlers/project/add/evaluator/llm-as-a-judge/index.ts index 6da1f2ed7..df6b7764d 100644 --- a/src/handlers/project/add/evaluator/llm-as-a-judge/index.ts +++ b/src/handlers/project/add/evaluator/llm-as-a-judge/index.ts @@ -11,6 +11,7 @@ import { import { TagsSchema } from "../../../../../projectSchemas/tags"; import { parseJsonFlagWithSchema } from "../../../../utils"; import type { AddProjectResourceConfig } from "../../types"; +import { addProjectResource } from "../../shared"; import { isRatingScalePreset, RATING_SCALE_PRESETS, @@ -93,14 +94,15 @@ export const createAddLlmAsAJudgeEvaluatorHandler = (config: AddProjectResourceC if (!parsed.success) throw new InputValidationError(z.prettifyError(parsed.error)); const project = ctx.require(ProjectKey); - for await (const event of config.projectManager.addResource(project, { - resourceType: "evaluator", - resourceConfig: parsed.data, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write(`added evaluator '${flags["name"]}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "evaluator", + resourceConfig: parsed.data, + }, + `added evaluator '${flags["name"]}' to '${project.name}'\n`, + ); }, }); diff --git a/src/handlers/project/add/gateway-connector/index.test.ts b/src/handlers/project/add/gateway-connector/index.test.ts index dae8be5ff..9c7466dcb 100644 --- a/src/handlers/project/add/gateway-connector/index.test.ts +++ b/src/handlers/project/add/gateway-connector/index.test.ts @@ -15,6 +15,34 @@ const { addGateway, cleanup, inProject, projectSpec, run } = afterEach(cleanup); describe("project add gateway-connector", () => { + test("--json preserves the command resource type and parent Gateway", async () => { + const projectRoot = await inProject(); + await addGateway(); + + const io = await run([ + "add", + "gateway-connector", + "--gateway", + "tools", + "--name", + "web", + "--connector", + "web-search", + "--json", + ]); + + expect(JSON.parse(io.stdout())).toEqual({ + operation: "add", + project: { name: "TestProject", path: projectRoot }, + resource: { + type: "gateway-connector", + name: "web", + parent: { type: "gateway", name: "tools" }, + }, + }); + expect(io.stderr()).not.toContain("added Connector Target"); + }); + test("adds Web Search and external Knowledge Base connectors", async () => { const projectRoot = await inProject(); await addGateway(); diff --git a/src/handlers/project/add/gateway-connector/index.ts b/src/handlers/project/add/gateway-connector/index.ts index 6ac9942ca..b59a870c3 100644 --- a/src/handlers/project/add/gateway-connector/index.ts +++ b/src/handlers/project/add/gateway-connector/index.ts @@ -9,6 +9,7 @@ import { import { createHandler, flag, ProjectKey } from "../../../../router"; import { parseJsonFlagWithSchema } from "../../../utils"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; export const createAddGatewayConnectorHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -85,15 +86,16 @@ export const createAddGatewayConnectorHandler = (config: AddProjectResourceConfi ); } - for await (const event of config.projectManager.addResource(project, { - resourceType: "gateway-target", - gatewayName: flags.gateway, - resourceConfig: target, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - config.io.stderr.write( + await addProjectResource( + ctx, + config, + { + resourceType: "gateway-target", + gatewayName: flags.gateway, + resourceConfig: target, + }, `added Connector Target '${target.name}' to Gateway '${flags.gateway}' in '${project.name}'\n`, + { resourceType: "gateway-connector" }, ); }, }); diff --git a/src/handlers/project/add/gateway-target/index.ts b/src/handlers/project/add/gateway-target/index.ts index 694a35c60..92108efd0 100644 --- a/src/handlers/project/add/gateway-target/index.ts +++ b/src/handlers/project/add/gateway-target/index.ts @@ -11,6 +11,7 @@ import { createHandler, flag, ProjectKey } from "../../../../router"; import { parseJsonFlagWithSchema } from "../../../utils"; import type { Project } from "../../types"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; export const createAddGatewayTargetHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -120,14 +121,14 @@ Use project add gateway-connector for curated Connector shortcuts.`, }; } - for await (const event of config.projectManager.addResource(project, { - resourceType: "gateway-target", - gatewayName: flags.gateway, - resourceConfig: target, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - config.io.stderr.write( + await addProjectResource( + ctx, + config, + { + resourceType: "gateway-target", + gatewayName: flags.gateway, + resourceConfig: target, + }, `added Target '${target.name}' to Gateway '${flags.gateway}' in '${project.name}'\n`, ); }, diff --git a/src/handlers/project/add/gateway/index.ts b/src/handlers/project/add/gateway/index.ts index c0ce0e09e..7169901c3 100644 --- a/src/handlers/project/add/gateway/index.ts +++ b/src/handlers/project/add/gateway/index.ts @@ -6,6 +6,7 @@ import type { AgentCoreGateway } from "../../../../projectSchemas/gateway"; import { createHandler, flag, ProjectKey } from "../../../../router"; import { parseJsonFlagWithSchema, parseTags } from "../../../utils"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; const GatewayAuthorizerConfigurationInputSchema = GatewayAuthorizerConfigSchema.strict(); @@ -125,12 +126,14 @@ export const createAddGatewayHandler = (config: AddProjectResourceConfig) => tags: parseTags(flags.tags), }; - for await (const event of config.projectManager.addResource(project, { - resourceType: "gateway", - resourceConfig: gateway, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - config.io.stderr.write(`added Gateway '${flags.name}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "gateway", + resourceConfig: gateway, + }, + `added Gateway '${flags.name}' to '${project.name}'\n`, + ); }, }); diff --git a/src/handlers/project/add/harness/index.ts b/src/handlers/project/add/harness/index.ts index 0d94df984..39859607e 100644 --- a/src/handlers/project/add/harness/index.ts +++ b/src/handlers/project/add/harness/index.ts @@ -1,6 +1,7 @@ import z from "zod"; import { createHandler, flag, ProjectKey } from "../../../../router"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; import { parseJsonFlag, parseTags } from "../../../utils"; import { InputValidationError } from "../../../../errors"; import { HarnessSpecSchema } from "../../../../projectSchemas/harness"; @@ -109,13 +110,14 @@ export const createAddHarnessHandler = (config: AddProjectResourceConfig) => throw new InputValidationError(z.prettifyError(result.error), { cause: result.error }); const project = ctx.require(ProjectKey); - for await (const event of config.projectManager.addResource(project, { - resourceType: "harness", - resourceConfig: result.data, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write(`added harness '${flags["name"]}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "harness", + resourceConfig: result.data, + }, + `added harness '${flags["name"]}' to '${project.name}'\n`, + ); }, }); diff --git a/src/handlers/project/add/memory/index.test.ts b/src/handlers/project/add/memory/index.test.ts index 69ece2aec..509cd709b 100644 --- a/src/handlers/project/add/memory/index.test.ts +++ b/src/handlers/project/add/memory/index.test.ts @@ -54,6 +54,18 @@ async function inProject(name = "TestProject"): Promise { } describe("project add memory", () => { + test("--json returns a structured project mutation result", async () => { + const projectRoot = await inProject(); + const { io } = await run(["add", "memory", "--name", "customer_memory", "--json"]); + + expect(JSON.parse(io.stdout())).toEqual({ + operation: "add", + project: { name: "TestProject", path: projectRoot }, + resource: { type: "memory", name: "customer_memory" }, + }); + expect(io.stderr()).not.toContain("added memory"); + }); + /** Verify the flag -> agentcore.json memories[] entry for each flag. */ test.each<[string, string[], Record]>([ [ diff --git a/src/handlers/project/add/memory/index.ts b/src/handlers/project/add/memory/index.ts index 756814e49..a60c10d8f 100644 --- a/src/handlers/project/add/memory/index.ts +++ b/src/handlers/project/add/memory/index.ts @@ -13,6 +13,7 @@ import { type MemoryStrategy, } from "../../../../projectSchemas/memory"; import { TagsSchema } from "../../../../projectSchemas/tags"; +import { addProjectResource } from "../shared"; // The service default for raw event retention const DEFAULT_EVENT_EXPIRY_DURATION = 30; @@ -172,14 +173,15 @@ export const createAddMemoryHandler = (config: AddProjectResourceConfig) => }; const project = ctx.require(ProjectKey); - for await (const event of config.projectManager.addResource(project, { - resourceType: "memory", - resourceConfig: memoryConfig, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write(`added memory '${flags["name"]}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "memory", + resourceConfig: memoryConfig, + }, + `added memory '${flags["name"]}' to '${project.name}'\n`, + ); }, }); diff --git a/src/handlers/project/add/online-eval/index.ts b/src/handlers/project/add/online-eval/index.ts index 955a4c59d..67594ad6f 100644 --- a/src/handlers/project/add/online-eval/index.ts +++ b/src/handlers/project/add/online-eval/index.ts @@ -4,6 +4,7 @@ import { InputValidationError } from "../../../../errors"; import { OnlineEvalConfigSchema } from "../../../../projectSchemas/online-eval-config"; import { parseJsonFlag } from "../../../utils"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; export const createAddOnlineEvalHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -81,13 +82,14 @@ export const createAddOnlineEvalHandler = (config: AddProjectResourceConfig) => if (!parsed.success) throw new InputValidationError(z.prettifyError(parsed.error)); const project = ctx.require(ProjectKey); - for await (const event of config.projectManager.addResource(project, { - resourceType: "online-eval", - resourceConfig: parsed.data, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write(`added online-eval config '${flags["name"]}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "online-eval", + resourceConfig: parsed.data, + }, + `added online-eval config '${flags["name"]}' to '${project.name}'\n`, + ); }, }); diff --git a/src/handlers/project/add/online-insight/index.ts b/src/handlers/project/add/online-insight/index.ts index 575f663c2..b066e77f9 100644 --- a/src/handlers/project/add/online-insight/index.ts +++ b/src/handlers/project/add/online-insight/index.ts @@ -4,6 +4,7 @@ import { InputValidationError } from "../../../../errors"; import { OnlineEvalConfigSchema } from "../../../../projectSchemas/online-eval-config"; import { parseJsonFlag } from "../../../utils"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; const BUILTIN_INSIGHT_PREFIX = "Builtin.Insight."; const ARN_PREFIX = "arn:"; @@ -98,14 +99,13 @@ export const createAddOnlineInsightHandler = (config: AddProjectResourceConfig) if (!parsed.success) throw new InputValidationError(z.prettifyError(parsed.error)); const project = ctx.require(ProjectKey); - for await (const event of config.projectManager.addResource(project, { - resourceType: "online-insight", - resourceConfig: parsed.data, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write( + await addProjectResource( + ctx, + config, + { + resourceType: "online-insight", + resourceConfig: parsed.data, + }, `added online-insight config '${flags["name"]}' to '${project.name}'\n`, ); }, diff --git a/src/handlers/project/add/payment-connector/index.ts b/src/handlers/project/add/payment-connector/index.ts index ade2ace29..5c30965e8 100644 --- a/src/handlers/project/add/payment-connector/index.ts +++ b/src/handlers/project/add/payment-connector/index.ts @@ -2,6 +2,7 @@ import z from "zod"; import { InputValidationError } from "../../../../errors"; import { createHandler, flag, ProjectKey } from "../../../../router"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; export const createAddPaymentConnectorHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -50,25 +51,24 @@ export const createAddPaymentConnectorHandler = (config: AddProjectResourceConfi provider = credential.provider; } - for await (const event of config.projectManager.addResource(project, { - resourceType: "payment-connector", - managerName: flags.manager, - resourceConfig: flags["quick-create"] - ? { - name: flags.name, - provider: "CoinbaseCDP", - provisionMode: "QUICK_CREATE", - } - : { - name: flags.name, - provider, - credentialName: credentialName!, - }, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write( + await addProjectResource( + ctx, + config, + { + resourceType: "payment-connector", + managerName: flags.manager, + resourceConfig: flags["quick-create"] + ? { + name: flags.name, + provider: "CoinbaseCDP", + provisionMode: "QUICK_CREATE", + } + : { + name: flags.name, + provider, + credentialName: credentialName!, + }, + }, `added payment connector '${flags.name}' to manager '${flags.manager}' in '${project.name}'\n`, ); }, diff --git a/src/handlers/project/add/payment-manager/index.ts b/src/handlers/project/add/payment-manager/index.ts index 562f2c33b..7f4dd0e29 100644 --- a/src/handlers/project/add/payment-manager/index.ts +++ b/src/handlers/project/add/payment-manager/index.ts @@ -8,6 +8,7 @@ import { } from "../../../../projectSchemas/payment"; import { createHandler, flag, ProjectKey } from "../../../../router"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; export const createAddPaymentManagerHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -68,33 +69,35 @@ export const createAddPaymentManagerHandler = (config: AddProjectResourceConfig) } const project = ctx.require(ProjectKey); - for await (const event of config.projectManager.addResource(project, { - resourceType: "payment-manager", - resourceConfig: { - name: flags.name, - authorizerType: flags["authorizer-type"], - authorizerConfiguration: - flags["authorizer-type"] === "CUSTOM_JWT" - ? { - customJWTAuthorizer: { - discoveryUrl: flags["discovery-url"]!, - allowedClients: flags["allowed-clients"], - allowedAudience: flags["allowed-audience"], - allowedScopes: flags["allowed-scopes"], - }, - } - : undefined, - connectors: [], - description: flags.description, - autoPayment: flags["auto-payment"], - defaultSpendLimit: flags["default-spend-limit"], - paymentToolAllowlist: flags["tool-allowlist"], - networkPreferences: flags["network-preferences"], + await addProjectResource( + ctx, + config, + { + resourceType: "payment-manager", + resourceConfig: { + name: flags.name, + authorizerType: flags["authorizer-type"], + authorizerConfiguration: + flags["authorizer-type"] === "CUSTOM_JWT" + ? { + customJWTAuthorizer: { + discoveryUrl: flags["discovery-url"]!, + allowedClients: flags["allowed-clients"], + allowedAudience: flags["allowed-audience"], + allowedScopes: flags["allowed-scopes"], + }, + } + : undefined, + connectors: [], + description: flags.description, + autoPayment: flags["auto-payment"], + defaultSpendLimit: flags["default-spend-limit"], + paymentToolAllowlist: flags["tool-allowlist"], + networkPreferences: flags["network-preferences"], + }, }, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - config.io.stderr.write(`added payment manager '${flags.name}' to '${project.name}'\n`); + `added payment manager '${flags.name}' to '${project.name}'\n`, + ); if (flags["auto-payment"]) { config.io.stderr.write( `Warning: auto-payment is ENABLED for manager '${flags.name}'. Agents can automatically settle ` + diff --git a/src/handlers/project/add/policy-engine/index.ts b/src/handlers/project/add/policy-engine/index.ts index 8dc652da3..896084025 100644 --- a/src/handlers/project/add/policy-engine/index.ts +++ b/src/handlers/project/add/policy-engine/index.ts @@ -4,6 +4,7 @@ import type { PolicyEngineSchema } from "../../../../projectSchemas/policy"; import { createHandler, flag, ProjectKey } from "../../../../router"; import { parseTags } from "../../../utils"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; /** The deployed service name of a policy engine; mirrors the L3 AgentCorePolicyEngine construct's rule. @@ -54,19 +55,21 @@ export const createAddPolicyEngineHandler = (config: AddProjectResourceConfig) = tags: parseTags(flags.tags), }; - for await (const event of config.projectManager.addResource(project, { - resourceType: "policy-engine", - resourceConfig: engine, - attachGateways: flags["attach-to-gateways"] - ? { - names: flags["attach-to-gateways"], - mode: flags["attach-mode"] === "log-only" ? "LOG_ONLY" : "ENFORCE", - } - : undefined, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - config.io.stderr.write(`added Policy Engine '${flags.name}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "policy-engine", + resourceConfig: engine, + attachGateways: flags["attach-to-gateways"] + ? { + names: flags["attach-to-gateways"], + mode: flags["attach-mode"] === "log-only" ? "LOG_ONLY" : "ENFORCE", + } + : undefined, + }, + `added Policy Engine '${flags.name}' to '${project.name}'\n`, + ); if (flags["attach-to-gateways"]) { config.io.stderr.write( `attached '${flags.name}' to ${flags["attach-to-gateways"].length} gateway(s)\n`, diff --git a/src/handlers/project/add/policy/index.ts b/src/handlers/project/add/policy/index.ts index 954628704..1cd0101ba 100644 --- a/src/handlers/project/add/policy/index.ts +++ b/src/handlers/project/add/policy/index.ts @@ -4,6 +4,7 @@ import { SourceResolver } from "../../../../io"; import type { PolicySchema } from "../../../../projectSchemas/policy"; import { createHandler, flag, ProjectKey } from "../../../../router"; import type { AddProjectResourceConfig } from "../types"; +import { addProjectResource } from "../shared"; /** A substring heuristic, not a Cedar parser; --authorization-phase overrides it. @@ -80,14 +81,14 @@ export const createAddPolicyHandler = (config: AddProjectResourceConfig) => authorizationPhase, }; - for await (const event of config.projectManager.addResource(project, { - resourceType: "policy", - engineName: flags.engine, - resourceConfig: policy, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - config.io.stderr.write( + await addProjectResource( + ctx, + config, + { + resourceType: "policy", + engineName: flags.engine, + resourceConfig: policy, + }, `added Policy '${flags.name}' to Policy Engine '${flags.engine}' in '${project.name}'\n`, ); }, diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index d62142a7e..12a0b75f8 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -20,6 +20,7 @@ import { resolveImportBedrockAgentInput, } from "../../importBedrockAgent"; import { RegionKey } from "../../../keys"; +import { addProjectResource } from "../shared"; export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -223,14 +224,15 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => throw new InputValidationError(z.prettifyError(result.error), { cause: result.error }); const project = ctx.require(ProjectKey); - for await (const event of config.projectManager.addResource(project, { - resourceType: "runtime", - resourceConfig: result.data, - })) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } - - config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`); + await addProjectResource( + ctx, + config, + { + resourceType: "runtime", + resourceConfig: result.data, + }, + `added runtime '${flags.name}' to '${project.name}'\n`, + ); }, }); diff --git a/src/handlers/project/add/shared.ts b/src/handlers/project/add/shared.ts new file mode 100644 index 000000000..f8d6f660f --- /dev/null +++ b/src/handlers/project/add/shared.ts @@ -0,0 +1,54 @@ +import { ProjectKey, type Context } from "../../../router"; +import { runWithProgress } from "../../../tui/progress"; +import { projectReference, renderProjectMutationResult } from "../output"; +import type { AddResourceInput, Project } from "../types"; +import type { AddProjectResourceConfig } from "./types"; + +type AddProjectResourceResultOptions = { + resourceType?: string; +}; + +function parentFor(input: AddResourceInput) { + switch (input.resourceType) { + case "gateway-target": + return { type: "gateway", name: input.gatewayName }; + case "policy": + return { type: "policy-engine", name: input.engineName }; + case "payment-connector": + return { type: "payment-manager", name: input.managerName }; + default: + return undefined; + } +} + +export async function addProjectResource( + ctx: Context, + config: AddProjectResourceConfig, + input: AddResourceInput, + humanSuccessMessage: string, + options: AddProjectResourceResultOptions = {}, +): Promise { + const project = ctx.require(ProjectKey); + const updatedProject = await runWithProgress(config.projectManager.addResource(project, input), { + io: config.io, + // Project add commands historically print plain progress lines even on a + // TTY. Keep that behavior while still collecting the generator result. + interactive: false, + }); + + renderProjectMutationResult( + ctx, + { + operation: "add", + project: projectReference(updatedProject), + resource: { + type: options.resourceType ?? input.resourceType, + name: input.resourceConfig.name, + parent: parentFor(input), + }, + }, + () => config.io.stderr.write(humanSuccessMessage), + ); + + return updatedProject; +} diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 96b9e0862..dae4d2510 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -23,6 +23,7 @@ import { import { InputValidationError } from "../../../errors"; import { DEFAULT_HARNESS_MODEL } from "../add/harness"; import { JsonKey } from "../../keys"; +import { projectReference, renderProjectMutationResult } from "../output"; type CreateProjectHandlerConfig = { projectManager: ProjectManager; @@ -122,13 +123,22 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = // Same driver as build and deploy: a live step list in a TTY, and the previous plain // line-per-step output when stderr is not a TTY or --json wants no ANSI on it. - await runWithProgress(config.projectManager.create(createInput), { + const project = await runWithProgress(config.projectManager.create(createInput), { io: config.io, interactive: ctx.require(JsonKey) ? false : undefined, }); - config.io.stderr.write(`Created project '${name}' in ./${name}\n`); - config.io.stderr.write(`To deploy it: cd ${name} && agentcore project deploy\n`); + renderProjectMutationResult( + ctx, + { + operation: "create", + project: projectReference(project), + }, + () => { + config.io.stderr.write(`Created project '${name}' in ./${name}\n`); + config.io.stderr.write(`To deploy it: cd ${name} && agentcore project deploy\n`); + }, + ); }, }); diff --git a/src/handlers/project/output.ts b/src/handlers/project/output.ts new file mode 100644 index 000000000..b0c0945a0 --- /dev/null +++ b/src/handlers/project/output.ts @@ -0,0 +1,42 @@ +import type { Context } from "../../router"; +import { JsonRendererKey } from "../../tui"; +import { JsonKey } from "../keys"; +import type { Project } from "./types"; + +export type ProjectMutationResource = { + type: string; + name?: string; + parent?: { + type: string; + name: string; + }; +}; + +export type ProjectMutationResult = { + operation: "create" | "add" | "remove"; + project: { + name: string; + path: string; + }; + resource?: ProjectMutationResource; + removedEnvironmentKeys?: string[]; +}; + +export function projectReference(project: Project): ProjectMutationResult["project"] { + return { + name: project.name, + path: project.rootPath, + }; +} + +export function renderProjectMutationResult( + ctx: Context, + result: ProjectMutationResult, + renderHuman: () => void, +): void { + if (ctx.require(JsonKey)) { + ctx.require(JsonRendererKey).renderJson(result); + return; + } + renderHuman(); +} diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 3d071a0d2..b71e6f4ec 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -63,6 +63,26 @@ async function inProject(name = "TestProject"): Promise { } describe("project create", () => { + test("--json returns the created project without human success text", async () => { + const directory = await inTempDirectory(); + const { io } = await run([ + "create", + "--name", + "JsonProject", + "--skip-install", + "--skip-git", + "--json", + ]); + const projectRoot = join(directory, "JsonProject"); + + expect(JSON.parse(io.stdout())).toEqual({ + operation: "create", + project: { name: "JsonProject", path: projectRoot }, + }); + expect(io.stderr()).not.toContain("Created project"); + expect(io.stderr()).not.toContain("To deploy it"); + }); + test("scaffolds a harness project by default, named for the project", async () => { const directory = await inTempDirectory(); await run(["create", "--name", "MyAgent"]); @@ -638,6 +658,31 @@ describe("project add config-bundle", () => { }); describe("project add credentials", () => { + test("--json reports the credential without exposing its secret", async () => { + const projectRoot = await inProject(); + const keyPath = join(projectRoot, "key.txt"); + await Bun.write(keyPath, "sk-secret-value\n"); + + const { io } = await run([ + "add", + "credentials", + "api-key", + "--name", + "svc-key", + "--api-key", + `file://${keyPath}`, + "--json", + ]); + + expect(JSON.parse(io.stdout())).toEqual({ + operation: "add", + project: { name: "TestProject", path: projectRoot }, + resource: { type: "credential", name: "svc-key" }, + }); + expect(io.stdout()).not.toContain("sk-secret-value"); + expect(io.stderr()).not.toContain("added credential"); + }); + test("api-key with a file:// secret records the spec entry and stores the trailing-newline-stripped key in .env.local", async () => { const projectRoot = await inProject(); const keyPath = join(projectRoot, "key.txt"); diff --git a/src/handlers/project/remove/index.test.ts b/src/handlers/project/remove/index.test.ts index 937109743..932226b04 100644 --- a/src/handlers/project/remove/index.test.ts +++ b/src/handlers/project/remove/index.test.ts @@ -224,6 +224,25 @@ describe("project remove", () => { expect(io.stdout()).toContain("removed credential with name 'svc-key' from project"); }); + test("--json reports a removal and its cleaned environment keys", async () => { + const projectRoot = await inProject(); + await run(["add", "credentials", "api-key", "--name", "svc-key", "--api-key", "-"], { + stdin: "sekret", + }); + const envKey = credentialEnvVarName("svc-key"); + + const { io } = await run(["remove", "credential", "--name", "svc-key", "--json"]); + + expect(JSON.parse(io.stdout())).toEqual({ + operation: "remove", + project: { name: "TestProject", path: projectRoot }, + resource: { type: "credential", name: "svc-key" }, + removedEnvironmentKeys: [envKey], + }); + expect(io.stdout()).not.toContain("removed credential with name"); + expect(io.stderr()).toContain(`removed '${envKey}' from ${ENV_LOCAL_RELATIVE_PATH}`); + }); + test("removing a secret-reference credential leaves .env.local alone", async () => { const projectRoot = await inProject(); await run([ diff --git a/src/handlers/project/remove/index.ts b/src/handlers/project/remove/index.ts index 5c0a49f79..3dad3ac8d 100644 --- a/src/handlers/project/remove/index.ts +++ b/src/handlers/project/remove/index.ts @@ -6,6 +6,7 @@ import type { AppIO } from "../../../io"; import { ENV_LOCAL_RELATIVE_PATH } from "../../../core/project/envLocal"; import { JsonKey } from "../../keys"; import type { ProjectManager } from "../types"; +import { projectReference, renderProjectMutationResult } from "../output"; type RemoveProjectResourceConfig = { projectManager: ProjectManager; @@ -82,7 +83,16 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) await confirmRemoveAll(config.io, ctx.require(JsonKey), flags.yes, project.name); const result = await config.projectManager.removeAllResources(project); reportEnvCleanup(config.io, result.removedEnvKeys); - config.io.stdout.write(`removed all resources from project`); + renderProjectMutationResult( + ctx, + { + operation: "remove", + project: projectReference(result.project), + resource: { type: "all" }, + removedEnvironmentKeys: result.removedEnvKeys, + }, + () => config.io.stdout.write(`removed all resources from project`), + ); return; } @@ -122,7 +132,24 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) } reportEnvCleanup(config.io, result.removedEnvKeys); - config.io.stdout.write(`removed ${resource} with name '${name}' from project`); + const parent = + resource === "gateway-target" || resource === "gateway-connector" + ? { type: "gateway", name: flags.gateway! } + : resource === "policy" && flags.engine + ? { type: "policy-engine", name: flags.engine } + : resource === "payment-connector" + ? { type: "payment-manager", name: flags.manager! } + : undefined; + renderProjectMutationResult( + ctx, + { + operation: "remove", + project: projectReference(result.project), + resource: { type: resource, name, parent }, + removedEnvironmentKeys: result.removedEnvKeys, + }, + () => config.io.stdout.write(`removed ${resource} with name '${name}' from project`), + ); }, }); diff --git a/src/router/router.test.ts b/src/router/router.test.ts index ced99b7d3..26a1a5215 100644 --- a/src/router/router.test.ts +++ b/src/router/router.test.ts @@ -706,6 +706,18 @@ test("command groups omit Commander's generated help subcommand", async () => { expect(out).not.toContain("help [command]"); }); +test("nested help shows inherited global options", async () => { + const JsonKey = globalFlag("json", "JSON output", z.boolean().default(false)); + const nested = new Router("nested").handler(leaf("deep", () => {})); + const root = new Router("app").groupFlags(JsonKey).handler(nested); + + const out = await helpOutput(root, ["app", "nested", "deep", "--help"]); + + expect(out).toContain("Global Options:"); + expect(out).toContain("--json"); + expect(out).toContain("JSON output"); +}); + test("flags with long-form help render a Parameter details section", async () => { const create = createHandler({ name: "create", diff --git a/src/router/router.tsx b/src/router/router.tsx index 5b7f41e86..9aea8331c 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -166,6 +166,7 @@ export function compile( const compiledNode = withEffectiveTuiSupport(node, effectiveTuiSupport); const c = new RoutedCommand(compiledNode); c.addHelpCommand(false); + c.configureHelp({ showGlobalOptions: true }); c.description(node.description()); const ownFlags = node.flags(); From 775a74a91ebdeadc6e35b6f779b955dceec3dbca Mon Sep 17 00:00:00 2001 From: gitikavj Date: Fri, 4 Sep 2026 05:29:00 +0000 Subject: [PATCH 2/2] test(project): canonicalize gateway project paths --- src/handlers/project/add/gateway-test-support.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/project/add/gateway-test-support.ts b/src/handlers/project/add/gateway-test-support.ts index c0a6e0001..ab7518007 100644 --- a/src/handlers/project/add/gateway-test-support.ts +++ b/src/handlers/project/add/gateway-test-support.ts @@ -51,7 +51,7 @@ export function createGatewayProjectTestHarness(directoryPrefix: string) { ]); const projectRoot = join(directory, name); process.chdir(projectRoot); - return projectRoot; + return process.cwd(); } async function addGateway(name = "tools"): Promise {