From bbcc72a81cbf25dd20f9afa302eed5ddada88f04 Mon Sep 17 00:00:00 2001 From: gitikavj Date: Fri, 4 Sep 2026 05:12:38 +0000 Subject: [PATCH 1/6] 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 | 32 ++++++++++- src/router/router.test.ts | 12 ++++ src/router/router.tsx | 5 +- 26 files changed, 464 insertions(+), 182 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 26959d59e..a25fdfa81 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 bce23d390..5a3781069 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -25,6 +25,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; @@ -129,13 +130,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(`Next steps:\n cd ${name}\n 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(`Next steps:\n cd ${name}\n 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 52437b63f..19db86ea1 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -67,6 +67,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"]); @@ -678,6 +698,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 4399bce4f..4416338c7 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.stderr()).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 b515b80b6..3dad3ac8d 100644 --- a/src/handlers/project/remove/index.ts +++ b/src/handlers/project/remove/index.ts @@ -5,8 +5,8 @@ import z from "zod"; import type { AppIO } from "../../../io"; import { ENV_LOCAL_RELATIVE_PATH } from "../../../core/project/envLocal"; import { JsonKey } from "../../keys"; -import { reportMessage } from "../../utils"; import type { ProjectManager } from "../types"; +import { projectReference, renderProjectMutationResult } from "../output"; type RemoveProjectResourceConfig = { projectManager: ProjectManager; @@ -83,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); - reportMessage(ctx, config.io, "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; } @@ -123,7 +132,24 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) } reportEnvCleanup(config.io, result.removedEnvKeys); - reportMessage(ctx, config.io, `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 c53b882f3..ecdc19726 100644 --- a/src/router/router.test.ts +++ b/src/router/router.test.ts @@ -747,6 +747,18 @@ test("command indexes show names without usage syntax", async () => { expect(out).not.toContain("nested [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 bd4e559fe..7fdbf1da5 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -177,7 +177,10 @@ export function compile( const compiledNode = withEffectiveTuiSupport(node, effectiveTuiSupport); const c = new RoutedCommand(compiledNode); c.addHelpCommand(false); - c.configureHelp({ subcommandTerm: (command) => command.name() }); + c.configureHelp({ + showGlobalOptions: true, + subcommandTerm: (command) => command.name(), + }); c.description(node.description()); const ownFlags = node.flags(); From 5304fe1596ddc253e5c525c58e7735b4fc5ff437 Mon Sep 17 00:00:00 2001 From: gitikavj Date: Fri, 4 Sep 2026 05:29:00 +0000 Subject: [PATCH 2/6] 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 { From 3a8607526438bcde9f072c05ceef69e8ed599880 Mon Sep 17 00:00:00 2001 From: gitikavj Date: Fri, 4 Sep 2026 19:12:50 +0000 Subject: [PATCH 3/6] refactor(project): tighten JSON mutation output contract --- src/handlers/project/add/shared.ts | 32 ++++------ src/handlers/project/output.ts | 76 ++++++++++++++++------- src/handlers/project/remove/index.test.ts | 68 +++++++++++++++++--- src/handlers/project/remove/index.ts | 37 +++++------ src/handlers/utils.tsx | 8 +++ 5 files changed, 147 insertions(+), 74 deletions(-) diff --git a/src/handlers/project/add/shared.ts b/src/handlers/project/add/shared.ts index f8d6f660f..90974ac9b 100644 --- a/src/handlers/project/add/shared.ts +++ b/src/handlers/project/add/shared.ts @@ -1,26 +1,18 @@ import { ProjectKey, type Context } from "../../../router"; import { runWithProgress } from "../../../tui/progress"; -import { projectReference, renderProjectMutationResult } from "../output"; +import { + projectMutationResource, + projectReference, + renderProjectMutationResult, + type ProjectMutationResourceType, +} from "../output"; import type { AddResourceInput, Project } from "../types"; import type { AddProjectResourceConfig } from "./types"; type AddProjectResourceResultOptions = { - resourceType?: string; + resourceType?: ProjectMutationResourceType; }; -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, @@ -41,11 +33,11 @@ export async function addProjectResource( { operation: "add", project: projectReference(updatedProject), - resource: { - type: options.resourceType ?? input.resourceType, - name: input.resourceConfig.name, - parent: parentFor(input), - }, + resource: projectMutationResource( + options.resourceType ?? input.resourceType, + input.resourceConfig.name, + input, + ), }, () => config.io.stderr.write(humanSuccessMessage), ); diff --git a/src/handlers/project/output.ts b/src/handlers/project/output.ts index b0c0945a0..ee6460021 100644 --- a/src/handlers/project/output.ts +++ b/src/handlers/project/output.ts @@ -1,42 +1,70 @@ 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; - }; +import { renderResult } from "../utils"; +import type { AddResourceInput, Project, RemoveResourceInput } from "./types"; + +export type ProjectMutationResourceType = AddResourceInput["resourceType"] | "gateway-connector"; + +type ProjectMutationParent = + | { type: "gateway"; name: string } + | { type: "policy-engine"; name: string } + | { type: "payment-manager"; name: string }; + +type ProjectMutationResource = { + type: ProjectMutationResourceType; + name: string; + parent?: ProjectMutationParent; }; -export type ProjectMutationResult = { - operation: "create" | "add" | "remove"; - project: { - name: string; - path: string; - }; - resource?: ProjectMutationResource; - removedEnvironmentKeys?: string[]; +type ProjectReference = { + name: string; + path: string; }; -export function projectReference(project: Project): ProjectMutationResult["project"] { +export type ProjectMutationResult = + | { operation: "create"; project: ProjectReference } + | { operation: "add"; project: ProjectReference; resource: ProjectMutationResource } + | { + operation: "remove"; + project: ProjectReference; + resource: ProjectMutationResource | { type: "all" }; + removedEnvironmentKeys: string[]; + }; + +export function projectReference(project: Project): ProjectReference { return { name: project.name, path: project.rootPath, }; } +export function projectMutationResource( + type: ProjectMutationResourceType, + name: string, + input: AddResourceInput | RemoveResourceInput, +): ProjectMutationResource { + const parent = parentFor(input); + return parent ? { type, name, parent } : { type, name }; +} + export function renderProjectMutationResult( ctx: Context, result: ProjectMutationResult, renderHuman: () => void, ): void { - if (ctx.require(JsonKey)) { - ctx.require(JsonRendererKey).renderJson(result); - return; + renderResult(ctx, result, renderHuman); +} + +function parentFor( + input: AddResourceInput | RemoveResourceInput, +): ProjectMutationParent | undefined { + switch (input.resourceType) { + case "gateway-target": + return { type: "gateway", name: input.gatewayName }; + case "policy": + return input.engineName ? { type: "policy-engine", name: input.engineName } : undefined; + case "payment-connector": + return { type: "payment-manager", name: input.managerName }; + default: + return undefined; } - renderHuman(); } diff --git a/src/handlers/project/remove/index.test.ts b/src/handlers/project/remove/index.test.ts index 4416338c7..8c0d40c28 100644 --- a/src/handlers/project/remove/index.test.ts +++ b/src/handlers/project/remove/index.test.ts @@ -306,12 +306,30 @@ describe("project remove", () => { ]); await run([...add]); - await run(["remove", resource, "--gateway", "tools", "--name", "remove"]); + const { io } = await run([ + "remove", + resource, + "--gateway", + "tools", + "--name", + "remove", + "--json", + ]); const agentcoreJson = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); expect( agentcoreJson.agentCoreGateways[0].targets.map((target: { name: string }) => target.name), ).toEqual(["keep"]); + expect(JSON.parse(io.stdout())).toEqual({ + operation: "remove", + project: { name: "TestProject", path: projectRoot }, + resource: { + type: resource, + name: "remove", + parent: { type: "gateway", name: "tools" }, + }, + removedEnvironmentKeys: [], + }); }); test("removes a payment manager with its connectors while preserving reusable credentials", async () => { @@ -370,7 +388,15 @@ describe("project remove", () => { "shared", ]); - await run(["remove", "payment-connector", "--manager", "payments", "--name", "remove"]); + const { io } = await run([ + "remove", + "payment-connector", + "--manager", + "payments", + "--name", + "remove", + "--json", + ]); const agentcoreJson = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); expect( @@ -383,6 +409,16 @@ describe("project remove", () => { provider: "CoinbaseCDP", }, ]); + expect(JSON.parse(io.stdout())).toEqual({ + operation: "remove", + project: { name: "TestProject", path: projectRoot }, + resource: { + type: "payment-connector", + name: "remove", + parent: { type: "payment-manager", name: "payments" }, + }, + removedEnvironmentKeys: [], + }); }); // Verifies that missing required inputs are rejected before calling the manager. @@ -425,16 +461,26 @@ describe("project remove", () => { } test.each([ - ["with --engine", ["--engine", "Guardrails"]], - ["resolving the engine from an unambiguous name", []], - ])("removes a policy from its engine %s", async (_label, engineArgs) => { + ["with --engine", ["--engine", "Guardrails"], { type: "policy-engine", name: "Guardrails" }], + ["resolving the engine from an unambiguous name", [], undefined], + ])("removes a policy from its engine %s", async (_label, engineArgs, parent) => { const projectRoot = await inProject(); await run(["add", "policy-engine", "--name", "Guardrails"]); await addPolicy("Guardrails", "DenyAll"); - await run(["remove", "policy", "--name", "DenyAll", ...engineArgs]); + const { io } = await run(["remove", "policy", "--name", "DenyAll", ...engineArgs, "--json"]); expect((await projectSpec(projectRoot)).policyEngines[0].policies).toEqual([]); + expect(JSON.parse(io.stdout())).toEqual({ + operation: "remove", + project: { name: "TestProject", path: projectRoot }, + resource: { + type: "policy", + name: "DenyAll", + ...(parent && { parent }), + }, + removedEnvironmentKeys: [], + }); }); test("rejects an ambiguous policy name without --engine", async () => { @@ -560,11 +606,17 @@ describe("project remove all", () => { }); test("reports the removal as JSON under --json", async () => { - await populatedProject(); + const projectRoot = await populatedProject(); + const envKey = credentialEnvVarName("svc-key"); const { io } = await run(["remove", "all", "--yes", "--json"]); - expect(JSON.parse(io.stdout())).toEqual({ message: "removed all resources from project" }); + expect(JSON.parse(io.stdout())).toEqual({ + operation: "remove", + project: { name: "TestProject", path: projectRoot }, + resource: { type: "all" }, + removedEnvironmentKeys: [envKey], + }); }); test("prompts on a TTY and proceeds on 'y'", async () => { diff --git a/src/handlers/project/remove/index.ts b/src/handlers/project/remove/index.ts index 3dad3ac8d..1bcedcb51 100644 --- a/src/handlers/project/remove/index.ts +++ b/src/handlers/project/remove/index.ts @@ -5,8 +5,8 @@ import z from "zod"; 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"; +import type { ProjectManager, RemoveResourceInput } from "../types"; +import { projectMutationResource, projectReference, renderProjectMutationResult } from "../output"; type RemoveProjectResourceConfig = { projectManager: ProjectManager; @@ -91,7 +91,7 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) resource: { type: "all" }, removedEnvironmentKeys: result.removedEnvKeys, }, - () => config.io.stdout.write(`removed all resources from project`), + () => config.io.stderr.write("removed all resources from project\n"), ); return; } @@ -99,56 +99,49 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) const name = flags["name"]; if (!name) throw new InputValidationError(`--name is required option`); - let result; + let input: RemoveResourceInput; if (resource === "gateway-target" || resource === "gateway-connector") { if (!flags.gateway) { throw new InputValidationError(`--gateway is required option`); } - result = await config.projectManager.removeResource(project, { + input = { resourceType: "gateway-target", gatewayName: flags.gateway, name, - }); + }; } else if (resource === "policy") { - result = await config.projectManager.removeResource(project, { + input = { resourceType: "policy", engineName: flags.engine, name, - }); + }; } else if (resource === "payment-connector") { if (!flags.manager) { throw new InputValidationError(`--manager is required option`); } - result = await config.projectManager.removeResource(project, { + input = { resourceType: "payment-connector", managerName: flags.manager, name, - }); + }; } else { - result = await config.projectManager.removeResource(project, { + input = { resourceType: resource, name, - }); + }; } + const result = await config.projectManager.removeResource(project, input); reportEnvCleanup(config.io, result.removedEnvKeys); - 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 }, + resource: projectMutationResource(resource, name, input), removedEnvironmentKeys: result.removedEnvKeys, }, - () => config.io.stdout.write(`removed ${resource} with name '${name}' from project`), + () => config.io.stderr.write(`removed ${resource} with name '${name}' from project\n`), ); }, }); diff --git a/src/handlers/utils.tsx b/src/handlers/utils.tsx index 55b79eb93..d32047b6c 100644 --- a/src/handlers/utils.tsx +++ b/src/handlers/utils.tsx @@ -151,6 +151,14 @@ export function renderJsonError(ctx: Context, error: unknown): void { ctx.require(JsonRendererKey).renderJson({ error: cliError.message }); } +export function renderResult(ctx: Context, result: unknown, renderHuman: () => void): void { + if (ctx.require(JsonKey)) { + ctx.require(JsonRendererKey).renderJson(result); + return; + } + renderHuman(); +} + // reportMessage is the success-side twin: the human line goes to stderr and, // under --json, the same message becomes the machine-readable result on stdout. export function reportMessage(ctx: Context, io: AppIO, message: string): void { From c5b203b285ca22d44e425b384cf7fdb3dea10e49 Mon Sep 17 00:00:00 2001 From: gitikavj Date: Fri, 4 Sep 2026 19:43:33 +0000 Subject: [PATCH 4/6] refactor(project): call shared result renderer directly --- src/handlers/project/add/shared.ts | 4 ++-- src/handlers/project/create/index.ts | 5 +++-- src/handlers/project/output.ts | 10 ---------- src/handlers/project/remove/index.ts | 7 ++++--- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/handlers/project/add/shared.ts b/src/handlers/project/add/shared.ts index 90974ac9b..db837100b 100644 --- a/src/handlers/project/add/shared.ts +++ b/src/handlers/project/add/shared.ts @@ -1,9 +1,9 @@ import { ProjectKey, type Context } from "../../../router"; import { runWithProgress } from "../../../tui/progress"; +import { renderResult } from "../../utils"; import { projectMutationResource, projectReference, - renderProjectMutationResult, type ProjectMutationResourceType, } from "../output"; import type { AddResourceInput, Project } from "../types"; @@ -28,7 +28,7 @@ export async function addProjectResource( interactive: false, }); - renderProjectMutationResult( + renderResult( ctx, { operation: "add", diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 5a3781069..37e1ed13f 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -25,7 +25,8 @@ import { import { InputValidationError } from "../../../errors"; import { DEFAULT_HARNESS_MODEL } from "../add/harness"; import { JsonKey } from "../../keys"; -import { projectReference, renderProjectMutationResult } from "../output"; +import { renderResult } from "../../utils"; +import { projectReference } from "../output"; type CreateProjectHandlerConfig = { projectManager: ProjectManager; @@ -135,7 +136,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = interactive: ctx.require(JsonKey) ? false : undefined, }); - renderProjectMutationResult( + renderResult( ctx, { operation: "create", diff --git a/src/handlers/project/output.ts b/src/handlers/project/output.ts index ee6460021..3df1caa80 100644 --- a/src/handlers/project/output.ts +++ b/src/handlers/project/output.ts @@ -1,5 +1,3 @@ -import type { Context } from "../../router"; -import { renderResult } from "../utils"; import type { AddResourceInput, Project, RemoveResourceInput } from "./types"; export type ProjectMutationResourceType = AddResourceInput["resourceType"] | "gateway-connector"; @@ -46,14 +44,6 @@ export function projectMutationResource( return parent ? { type, name, parent } : { type, name }; } -export function renderProjectMutationResult( - ctx: Context, - result: ProjectMutationResult, - renderHuman: () => void, -): void { - renderResult(ctx, result, renderHuman); -} - function parentFor( input: AddResourceInput | RemoveResourceInput, ): ProjectMutationParent | undefined { diff --git a/src/handlers/project/remove/index.ts b/src/handlers/project/remove/index.ts index 1bcedcb51..e12c21939 100644 --- a/src/handlers/project/remove/index.ts +++ b/src/handlers/project/remove/index.ts @@ -5,8 +5,9 @@ import z from "zod"; import type { AppIO } from "../../../io"; import { ENV_LOCAL_RELATIVE_PATH } from "../../../core/project/envLocal"; import { JsonKey } from "../../keys"; +import { renderResult } from "../../utils"; import type { ProjectManager, RemoveResourceInput } from "../types"; -import { projectMutationResource, projectReference, renderProjectMutationResult } from "../output"; +import { projectMutationResource, projectReference } from "../output"; type RemoveProjectResourceConfig = { projectManager: ProjectManager; @@ -83,7 +84,7 @@ 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); - renderProjectMutationResult( + renderResult( ctx, { operation: "remove", @@ -133,7 +134,7 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) const result = await config.projectManager.removeResource(project, input); reportEnvCleanup(config.io, result.removedEnvKeys); - renderProjectMutationResult( + renderResult( ctx, { operation: "remove", From 75b90a39ed525d3da53537f5965543441fc6d937 Mon Sep 17 00:00:00 2001 From: gitikavj Date: Fri, 4 Sep 2026 20:54:20 +0000 Subject: [PATCH 5/6] fix(project): address JSON output review feedback --- src/core/project/manager.tsx | 6 +++- .../project/add/config-bundle/index.ts | 3 +- .../project/add/credentials/shared.ts | 11 ++++--- .../add/evaluator/code-based/index.test.ts | 19 ++++++++++++ .../project/add/evaluator/code-based/index.ts | 17 +++++++---- .../add/evaluator/llm-as-a-judge/index.ts | 3 +- .../project/add/gateway-connector/index.ts | 3 +- .../project/add/gateway-target/index.ts | 3 +- src/handlers/project/add/gateway/index.ts | 3 +- src/handlers/project/add/harness/index.ts | 3 +- src/handlers/project/add/memory/index.ts | 3 +- src/handlers/project/add/online-eval/index.ts | 3 +- .../project/add/online-insight/index.ts | 3 +- .../project/add/payment-connector/index.ts | 3 +- .../project/add/payment-manager/index.test.ts | 16 ++++++++++ .../project/add/payment-manager/index.ts | 30 +++++++++++-------- .../project/add/policy-engine/index.test.ts | 18 +++++++++++ .../project/add/policy-engine/index.ts | 13 ++++---- src/handlers/project/add/policy/index.ts | 3 +- .../project/add/runtime/index.test.ts | 13 ++++++++ src/handlers/project/add/runtime/index.ts | 9 ++++-- src/handlers/project/add/shared.ts | 14 ++++++--- src/handlers/project/create/index.ts | 4 +-- src/handlers/project/output.ts | 7 ++++- src/handlers/project/project.test.ts | 12 +++++++- src/handlers/project/remove/index.test.ts | 8 ++--- src/handlers/project/remove/index.ts | 8 ++--- src/handlers/project/types.ts | 11 +++++-- 28 files changed, 186 insertions(+), 63 deletions(-) diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 6d0a83885..7495190d4 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -22,6 +22,7 @@ import type { ProjectResource, RemoveResourceInput, RemoveResourceResult, + RemoveResourcesResult, } from "../../handlers/project/types"; import type { Logger } from "../../logging"; import { @@ -531,6 +532,7 @@ export class FsProjectManager implements ProjectManager { let removed = false; let newSpec: unknown; + let removedResource = input; if (input.resourceType === "policy") { const candidates = existingProjectSpec.policyEngines.filter((engine) => engine.policies.some((policy) => policy.name === input.name), @@ -546,6 +548,7 @@ export class FsProjectManager implements ProjectManager { ? candidates.find((engine) => engine.name === input.engineName) : candidates[0]; removed = owner !== undefined; + if (owner) removedResource = { ...input, engineName: owner.name }; const engines = existingProjectSpec.policyEngines.map((engine) => engine === owner ? { ...engine, policies: engine.policies.filter((policy) => policy.name !== input.name) } @@ -624,10 +627,11 @@ export class FsProjectManager implements ProjectManager { return { project: { ...project, spec: newProjectSpec }, removedEnvKeys, + removedResource, }; } - public async removeAllResources(project: Project): Promise { + public async removeAllResources(project: Project): Promise { const agentCoreSpecPath = this.getProjectSpecPath(project); const existingProjectSpec = await this.json.read(agentCoreSpecPath, ProjectSpecSchema); diff --git a/src/handlers/project/add/config-bundle/index.ts b/src/handlers/project/add/config-bundle/index.ts index 98548474d..53a72363a 100644 --- a/src/handlers/project/add/config-bundle/index.ts +++ b/src/handlers/project/add/config-bundle/index.ts @@ -72,6 +72,7 @@ export const createAddConfigBundleHandler = (config: AddProjectResourceConfig) = await addProjectResource( ctx, config, + project, { resourceType: "config-bundle", resourceConfig: { @@ -83,7 +84,7 @@ export const createAddConfigBundleHandler = (config: AddProjectResourceConfig) = kmsKeyArn: flags["kms-key-arn"], }, }, - `added configuration bundle '${flags.name}' to '${project.name}'\n`, + `added configuration bundle '${flags.name}' to '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/credentials/shared.ts b/src/handlers/project/add/credentials/shared.ts index 4afc8e3b9..b894f7b04 100644 --- a/src/handlers/project/add/credentials/shared.ts +++ b/src/handlers/project/add/credentials/shared.ts @@ -72,13 +72,16 @@ export async function addCredentialToProject( await addProjectResource( ctx, config, + project, { resourceType: "credential", ...input, }, - `added credential '${input.resourceConfig.name}' to '${project.name}'\n`, + `added credential '${input.resourceConfig.name}' to '${project.name}'`, + { + notes: (input.envEntries ?? []) + .filter((entry) => entry.value === undefined) + .map((entry) => `Set ${entry.key} in agentcore/.env.local before you deploy.`), + }, ); - 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.test.ts b/src/handlers/project/add/evaluator/code-based/index.test.ts index 08faac121..99d8b4509 100644 --- a/src/handlers/project/add/evaluator/code-based/index.test.ts +++ b/src/handlers/project/add/evaluator/code-based/index.test.ts @@ -323,6 +323,25 @@ describe("project add evaluator code-based", () => { expect(io.stderr()).toContain("returns Pass for every session"); }); + test("--json reports the empty stub guidance as a structured note", async () => { + await inProject(); + const { io } = await run([ + "add", + "evaluator", + "code-based", + "--name", + "stub", + "--level", + "SESSION", + "--json", + ]); + + expect(JSON.parse(io.stdout()).notes).toEqual([ + "note: this evaluator returns Pass for every session until you implement app/stub/lambda_function.py", + ]); + expect(io.stderr()).not.toContain("returns Pass for every session"); + }); + test("external mode prints no stub note", async () => { await inProject(); const { io } = await run([ diff --git a/src/handlers/project/add/evaluator/code-based/index.ts b/src/handlers/project/add/evaluator/code-based/index.ts index bbb6355c7..85e2b2f87 100644 --- a/src/handlers/project/add/evaluator/code-based/index.ts +++ b/src/handlers/project/add/evaluator/code-based/index.ts @@ -87,11 +87,12 @@ export const createAddCodeBasedEvaluatorHandler = (config: AddProjectResourceCon await addProjectResource( ctx, config, + project, { resourceType: "evaluator", resourceConfig: parsed.data, }, - `added evaluator '${flags["name"]}' to '${project.name}'\n`, + `added evaluator '${flags["name"]}' to '${project.name}'`, ); return; } @@ -108,17 +109,21 @@ export const createAddCodeBasedEvaluatorHandler = (config: AddProjectResourceCon await addProjectResource( ctx, config, + project, { resourceType: "evaluator", resourceConfig: { name: scaffold.name }, scaffold, }, - `added evaluator '${flags["name"]}' to '${project.name}'\n`, + `added evaluator '${flags["name"]}' to '${project.name}'`, + { + notes: hasMetric + ? [] + : [ + `note: this evaluator returns Pass for every session until you implement app/${flags["name"]}/lambda_function.py`, + ], + }, ); - 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 df6b7764d..e8ba98de8 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 @@ -97,11 +97,12 @@ export const createAddLlmAsAJudgeEvaluatorHandler = (config: AddProjectResourceC await addProjectResource( ctx, config, + project, { resourceType: "evaluator", resourceConfig: parsed.data, }, - `added evaluator '${flags["name"]}' to '${project.name}'\n`, + `added evaluator '${flags["name"]}' to '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/gateway-connector/index.ts b/src/handlers/project/add/gateway-connector/index.ts index a25fdfa81..83a254a34 100644 --- a/src/handlers/project/add/gateway-connector/index.ts +++ b/src/handlers/project/add/gateway-connector/index.ts @@ -89,12 +89,13 @@ export const createAddGatewayConnectorHandler = (config: AddProjectResourceConfi await addProjectResource( ctx, config, + project, { resourceType: "gateway-target", gatewayName: flags.gateway, resourceConfig: target, }, - `added Connector Target '${target.name}' to Gateway '${flags.gateway}' in '${project.name}'\n`, + `added Connector Target '${target.name}' to Gateway '${flags.gateway}' in '${project.name}'`, { resourceType: "gateway-connector" }, ); }, diff --git a/src/handlers/project/add/gateway-target/index.ts b/src/handlers/project/add/gateway-target/index.ts index 92108efd0..43b3b02e7 100644 --- a/src/handlers/project/add/gateway-target/index.ts +++ b/src/handlers/project/add/gateway-target/index.ts @@ -124,12 +124,13 @@ Use project add gateway-connector for curated Connector shortcuts.`, await addProjectResource( ctx, config, + project, { resourceType: "gateway-target", gatewayName: flags.gateway, resourceConfig: target, }, - `added Target '${target.name}' to Gateway '${flags.gateway}' in '${project.name}'\n`, + `added Target '${target.name}' to Gateway '${flags.gateway}' in '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/gateway/index.ts b/src/handlers/project/add/gateway/index.ts index 7169901c3..507b7eb5c 100644 --- a/src/handlers/project/add/gateway/index.ts +++ b/src/handlers/project/add/gateway/index.ts @@ -129,11 +129,12 @@ export const createAddGatewayHandler = (config: AddProjectResourceConfig) => await addProjectResource( ctx, config, + project, { resourceType: "gateway", resourceConfig: gateway, }, - `added Gateway '${flags.name}' to '${project.name}'\n`, + `added Gateway '${flags.name}' to '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/harness/index.ts b/src/handlers/project/add/harness/index.ts index 39859607e..90b4b8b7b 100644 --- a/src/handlers/project/add/harness/index.ts +++ b/src/handlers/project/add/harness/index.ts @@ -113,11 +113,12 @@ export const createAddHarnessHandler = (config: AddProjectResourceConfig) => await addProjectResource( ctx, config, + project, { resourceType: "harness", resourceConfig: result.data, }, - `added harness '${flags["name"]}' to '${project.name}'\n`, + `added harness '${flags["name"]}' to '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/memory/index.ts b/src/handlers/project/add/memory/index.ts index a60c10d8f..61edd254f 100644 --- a/src/handlers/project/add/memory/index.ts +++ b/src/handlers/project/add/memory/index.ts @@ -176,11 +176,12 @@ export const createAddMemoryHandler = (config: AddProjectResourceConfig) => await addProjectResource( ctx, config, + project, { resourceType: "memory", resourceConfig: memoryConfig, }, - `added memory '${flags["name"]}' to '${project.name}'\n`, + `added memory '${flags["name"]}' to '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/online-eval/index.ts b/src/handlers/project/add/online-eval/index.ts index 67594ad6f..a1a18740c 100644 --- a/src/handlers/project/add/online-eval/index.ts +++ b/src/handlers/project/add/online-eval/index.ts @@ -85,11 +85,12 @@ export const createAddOnlineEvalHandler = (config: AddProjectResourceConfig) => await addProjectResource( ctx, config, + project, { resourceType: "online-eval", resourceConfig: parsed.data, }, - `added online-eval config '${flags["name"]}' to '${project.name}'\n`, + `added online-eval config '${flags["name"]}' to '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/online-insight/index.ts b/src/handlers/project/add/online-insight/index.ts index b066e77f9..e0bc183a6 100644 --- a/src/handlers/project/add/online-insight/index.ts +++ b/src/handlers/project/add/online-insight/index.ts @@ -102,11 +102,12 @@ export const createAddOnlineInsightHandler = (config: AddProjectResourceConfig) await addProjectResource( ctx, config, + project, { resourceType: "online-insight", resourceConfig: parsed.data, }, - `added online-insight config '${flags["name"]}' to '${project.name}'\n`, + `added online-insight config '${flags["name"]}' to '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/payment-connector/index.ts b/src/handlers/project/add/payment-connector/index.ts index 5c30965e8..a1cb76879 100644 --- a/src/handlers/project/add/payment-connector/index.ts +++ b/src/handlers/project/add/payment-connector/index.ts @@ -54,6 +54,7 @@ export const createAddPaymentConnectorHandler = (config: AddProjectResourceConfi await addProjectResource( ctx, config, + project, { resourceType: "payment-connector", managerName: flags.manager, @@ -69,7 +70,7 @@ export const createAddPaymentConnectorHandler = (config: AddProjectResourceConfi credentialName: credentialName!, }, }, - `added payment connector '${flags.name}' to manager '${flags.manager}' in '${project.name}'\n`, + `added payment connector '${flags.name}' to manager '${flags.manager}' in '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/payment-manager/index.test.ts b/src/handlers/project/add/payment-manager/index.test.ts index 8943d3771..f827d25de 100644 --- a/src/handlers/project/add/payment-manager/index.test.ts +++ b/src/handlers/project/add/payment-manager/index.test.ts @@ -83,6 +83,22 @@ describe("project add payment-manager", () => { expect(io.stderr()).toContain("does not modify runtime source code"); }); + test("--json reports payment warnings as structured notes", async () => { + await inProject(); + + const io = await run(["add", "payment-manager", "--name", "payments", "--json"]); + const result = JSON.parse(io.stdout()); + + 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. " + + "Configure the Payments SDK or plugin in supported runtimes before invoking payment-enabled agents.", + ]); + expect(io.stderr()).not.toContain("auto-payment is ENABLED"); + expect(io.stderr()).not.toContain("does not modify runtime source code"); + }); + test.each([ ["missing name", [], "required option '--name"], [ diff --git a/src/handlers/project/add/payment-manager/index.ts b/src/handlers/project/add/payment-manager/index.ts index 7f4dd0e29..c6816455c 100644 --- a/src/handlers/project/add/payment-manager/index.ts +++ b/src/handlers/project/add/payment-manager/index.ts @@ -69,9 +69,24 @@ export const createAddPaymentManagerHandler = (config: AddProjectResourceConfig) } const project = ctx.require(ProjectKey); + const notes: string[] = []; + if (flags["auto-payment"]) { + notes.push( + `Warning: auto-payment is ENABLED for manager '${flags.name}'. Agents can automatically settle ` + + "402 responses without human approval. Use --no-auto-payment to require manual approval.", + ); + } + if (project.spec.runtimes.length > 0) { + notes.push( + "Warning: project add payment-manager does not modify runtime source code. " + + "Configure the Payments SDK or plugin in supported runtimes before invoking payment-enabled agents.", + ); + } + await addProjectResource( ctx, config, + project, { resourceType: "payment-manager", resourceConfig: { @@ -96,19 +111,8 @@ export const createAddPaymentManagerHandler = (config: AddProjectResourceConfig) networkPreferences: flags["network-preferences"], }, }, - `added payment manager '${flags.name}' to '${project.name}'\n`, + `added payment manager '${flags.name}' to '${project.name}'`, + { notes }, ); - if (flags["auto-payment"]) { - config.io.stderr.write( - `Warning: auto-payment is ENABLED for manager '${flags.name}'. Agents can automatically settle ` + - "402 responses without human approval. Use --no-auto-payment to require manual approval.\n", - ); - } - if (project.spec.runtimes.length > 0) { - config.io.stderr.write( - "Warning: project add payment-manager does not modify runtime source code. " + - "Configure the Payments SDK or plugin in supported runtimes before invoking payment-enabled agents.\n", - ); - } }, }); diff --git a/src/handlers/project/add/policy-engine/index.test.ts b/src/handlers/project/add/policy-engine/index.test.ts index a095b9f9c..2738aa6e0 100644 --- a/src/handlers/project/add/policy-engine/index.test.ts +++ b/src/handlers/project/add/policy-engine/index.test.ts @@ -87,6 +87,24 @@ describe("project add policy-engine", () => { } }); + test("--json reports gateway attachments as structured notes", async () => { + await inProject(); + await addGateway("tools"); + + const io = await run([ + "add", + "policy-engine", + "--name", + "Guardrails", + "--attach-to-gateways", + "tools", + "--json", + ]); + + expect(JSON.parse(io.stdout()).notes).toEqual(["attached 'Guardrails' to 1 gateway(s)"]); + expect(io.stderr()).not.toContain("attached 'Guardrails'"); + }); + test("rejects unknown gateway names without writing the engine", async () => { const projectRoot = await inProject(); await expect( diff --git a/src/handlers/project/add/policy-engine/index.ts b/src/handlers/project/add/policy-engine/index.ts index 896084025..fc5c1a32f 100644 --- a/src/handlers/project/add/policy-engine/index.ts +++ b/src/handlers/project/add/policy-engine/index.ts @@ -58,6 +58,7 @@ export const createAddPolicyEngineHandler = (config: AddProjectResourceConfig) = await addProjectResource( ctx, config, + project, { resourceType: "policy-engine", resourceConfig: engine, @@ -68,12 +69,12 @@ export const createAddPolicyEngineHandler = (config: AddProjectResourceConfig) = } : undefined, }, - `added Policy Engine '${flags.name}' to '${project.name}'\n`, + `added Policy Engine '${flags.name}' to '${project.name}'`, + { + notes: flags["attach-to-gateways"] + ? [`attached '${flags.name}' to ${flags["attach-to-gateways"].length} gateway(s)`] + : [], + }, ); - 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 1cd0101ba..4d7022d7e 100644 --- a/src/handlers/project/add/policy/index.ts +++ b/src/handlers/project/add/policy/index.ts @@ -84,12 +84,13 @@ export const createAddPolicyHandler = (config: AddProjectResourceConfig) => await addProjectResource( ctx, config, + project, { resourceType: "policy", engineName: flags.engine, resourceConfig: policy, }, - `added Policy '${flags.name}' to Policy Engine '${flags.engine}' in '${project.name}'\n`, + `added Policy '${flags.name}' to Policy Engine '${flags.engine}' in '${project.name}'`, ); }, }); diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index 1e85c99fe..ef0ec71ef 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -507,6 +507,19 @@ describe("project add runtime --type import", () => { expect(pyproject).toContain('name = "support-proxy"'); }); + test("--json reports import follow-up as a structured note", async () => { + await inProject(); + const core = new TestCoreClient(); + core.bedrockAgentImportPlans["A1B2C3D4E5/TSTALIASID"] = translatedImportPlan(); + + const { io } = await run([...importArgs, "--json"], { core }); + + expect(JSON.parse(io.stdout()).notes).toEqual([ + "Import generated 1 manual follow-up item in app/support_proxy/IMPORT_NOTES.md.", + ]); + expect(io.stderr()).not.toContain("IMPORT_NOTES.md"); + }); + test("supports LangGraph translation", async () => { const projectRoot = await inProject(); const core = new TestCoreClient(); diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index 12a0b75f8..fec3ad924 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -156,6 +156,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => const apiKey = await source.resolveSecret("api-key", flags["api-key"]); const runtimeName = flags.name; + const notes: string[] = []; let importBedrockAgent: ImportBedrockAgentInput | undefined; if (isImport) { @@ -169,10 +170,10 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => memory: "longAndShortTerm", }); if (importBedrockAgent.notes.length > 0) { - config.io.stderr.write( + notes.push( `Import generated ${importBedrockAgent.notes.length} manual follow-up ` + `${importBedrockAgent.notes.length === 1 ? "item" : "items"} in ` + - `app/${runtimeName}/IMPORT_NOTES.md.\n`, + `app/${runtimeName}/IMPORT_NOTES.md.`, ); } } @@ -227,11 +228,13 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => await addProjectResource( ctx, config, + project, { resourceType: "runtime", resourceConfig: result.data, }, - `added runtime '${flags.name}' to '${project.name}'\n`, + `added runtime '${flags.name}' to '${project.name}'`, + { notes }, ); }, }); diff --git a/src/handlers/project/add/shared.ts b/src/handlers/project/add/shared.ts index db837100b..800e728a9 100644 --- a/src/handlers/project/add/shared.ts +++ b/src/handlers/project/add/shared.ts @@ -1,9 +1,10 @@ -import { ProjectKey, type Context } from "../../../router"; +import type { Context } from "../../../router"; import { runWithProgress } from "../../../tui/progress"; import { renderResult } from "../../utils"; import { projectMutationResource, projectReference, + type ProjectMutationResult, type ProjectMutationResourceType, } from "../output"; import type { AddResourceInput, Project } from "../types"; @@ -11,16 +12,17 @@ import type { AddProjectResourceConfig } from "./types"; type AddProjectResourceResultOptions = { resourceType?: ProjectMutationResourceType; + notes?: string[]; }; export async function addProjectResource( ctx: Context, config: AddProjectResourceConfig, + project: Project, 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 @@ -28,7 +30,7 @@ export async function addProjectResource( interactive: false, }); - renderResult( + renderResult( ctx, { operation: "add", @@ -38,8 +40,12 @@ export async function addProjectResource( input.resourceConfig.name, input, ), + ...(options.notes?.length ? { notes: options.notes } : {}), + }, + () => { + config.io.stderr.write(`${humanSuccessMessage}\n`); + for (const note of options.notes ?? []) config.io.stderr.write(`${note}\n`); }, - () => config.io.stderr.write(humanSuccessMessage), ); return updatedProject; diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 37e1ed13f..f0fd26c62 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -26,7 +26,7 @@ import { InputValidationError } from "../../../errors"; import { DEFAULT_HARNESS_MODEL } from "../add/harness"; import { JsonKey } from "../../keys"; import { renderResult } from "../../utils"; -import { projectReference } from "../output"; +import { projectReference, type ProjectMutationResult } from "../output"; type CreateProjectHandlerConfig = { projectManager: ProjectManager; @@ -136,7 +136,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = interactive: ctx.require(JsonKey) ? false : undefined, }); - renderResult( + renderResult( ctx, { operation: "create", diff --git a/src/handlers/project/output.ts b/src/handlers/project/output.ts index 3df1caa80..ba672b900 100644 --- a/src/handlers/project/output.ts +++ b/src/handlers/project/output.ts @@ -20,7 +20,12 @@ type ProjectReference = { export type ProjectMutationResult = | { operation: "create"; project: ProjectReference } - | { operation: "add"; project: ProjectReference; resource: ProjectMutationResource } + | { + operation: "add"; + project: ProjectReference; + resource: ProjectMutationResource; + notes?: string[]; + } | { operation: "remove"; project: ProjectReference; diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 19db86ea1..3dc5f2bd3 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -84,7 +84,7 @@ describe("project create", () => { project: { name: "JsonProject", path: projectRoot }, }); expect(io.stderr()).not.toContain("Created project"); - expect(io.stderr()).not.toContain("To deploy it"); + expect(io.stderr()).not.toContain("Next steps"); }); test("scaffolds a harness project by default, named for the project", async () => { @@ -761,6 +761,16 @@ describe("project add credentials", () => { ); }); + test("--json reports credential setup guidance as structured notes", async () => { + await inProject(); + const { io } = await run(["add", "credentials", "api-key", "--name", "svc-key", "--json"]); + + expect(JSON.parse(io.stdout()).notes).toEqual([ + "Set AGENTCORE_CREDENTIAL_SVC_KEY in agentcore/.env.local before you deploy.", + ]); + expect(io.stderr()).not.toContain("before you deploy"); + }); + test("api-key with an external secret reference records it in the spec and skips .env.local", async () => { const projectRoot = await inProject(); const secretRef = { diff --git a/src/handlers/project/remove/index.test.ts b/src/handlers/project/remove/index.test.ts index 8c0d40c28..e691ceb83 100644 --- a/src/handlers/project/remove/index.test.ts +++ b/src/handlers/project/remove/index.test.ts @@ -461,9 +461,9 @@ describe("project remove", () => { } test.each([ - ["with --engine", ["--engine", "Guardrails"], { type: "policy-engine", name: "Guardrails" }], - ["resolving the engine from an unambiguous name", [], undefined], - ])("removes a policy from its engine %s", async (_label, engineArgs, parent) => { + ["with --engine", ["--engine", "Guardrails"]], + ["resolving the engine from an unambiguous name", []], + ])("removes a policy from its engine %s", async (_label, engineArgs) => { const projectRoot = await inProject(); await run(["add", "policy-engine", "--name", "Guardrails"]); await addPolicy("Guardrails", "DenyAll"); @@ -477,7 +477,7 @@ describe("project remove", () => { resource: { type: "policy", name: "DenyAll", - ...(parent && { parent }), + parent: { type: "policy-engine", name: "Guardrails" }, }, removedEnvironmentKeys: [], }); diff --git a/src/handlers/project/remove/index.ts b/src/handlers/project/remove/index.ts index e12c21939..df5f00edf 100644 --- a/src/handlers/project/remove/index.ts +++ b/src/handlers/project/remove/index.ts @@ -7,7 +7,7 @@ import { ENV_LOCAL_RELATIVE_PATH } from "../../../core/project/envLocal"; import { JsonKey } from "../../keys"; import { renderResult } from "../../utils"; import type { ProjectManager, RemoveResourceInput } from "../types"; -import { projectMutationResource, projectReference } from "../output"; +import { projectMutationResource, projectReference, type ProjectMutationResult } from "../output"; type RemoveProjectResourceConfig = { projectManager: ProjectManager; @@ -84,7 +84,7 @@ 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); - renderResult( + renderResult( ctx, { operation: "remove", @@ -134,12 +134,12 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) const result = await config.projectManager.removeResource(project, input); reportEnvCleanup(config.io, result.removedEnvKeys); - renderResult( + renderResult( ctx, { operation: "remove", project: projectReference(result.project), - resource: projectMutationResource(resource, name, input), + resource: projectMutationResource(resource, name, result.removedResource), removedEnvironmentKeys: result.removedEnvKeys, }, () => config.io.stderr.write(`removed ${resource} with name '${name}' from project\n`), diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index b3d878eab..c42d21f34 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -420,13 +420,18 @@ export type RemoveResourceInput = name: string; }; -/** The outcome of a spec-level removal. */ -export type RemoveResourceResult = { +/** The shared outcome of a spec-level removal. */ +export type RemoveResourcesResult = { project: Project; /** .env.local keys deleted because the removed credential(s) reserved them. */ removedEnvKeys: string[]; }; +/** The outcome of removing one resource, including its resolved parent. */ +export type RemoveResourceResult = RemoveResourcesResult & { + removedResource: RemoveResourceInput; +}; + /** * The primary interface for interacting with projects */ @@ -495,7 +500,7 @@ export interface ProjectManager { * code directories under app/ and aws-targets.json survive, so a following * deploy can tear down the target's stack. */ - removeAllResources(project: Project): Promise; + removeAllResources(project: Project): Promise; /** * Convert a harness into an editable Strands runtime agent: render the agent From 3b9d6a56f87d96e46add1ccd8b6d00cb2365137f Mon Sep 17 00:00:00 2001 From: gitikavj Date: Fri, 4 Sep 2026 20:55:09 +0000 Subject: [PATCH 6/6] refactor(output): type shared result renderer --- src/handlers/utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/utils.tsx b/src/handlers/utils.tsx index d32047b6c..d7e0af858 100644 --- a/src/handlers/utils.tsx +++ b/src/handlers/utils.tsx @@ -151,7 +151,7 @@ export function renderJsonError(ctx: Context, error: unknown): void { ctx.require(JsonRendererKey).renderJson({ error: cliError.message }); } -export function renderResult(ctx: Context, result: unknown, renderHuman: () => void): void { +export function renderResult(ctx: Context, result: T, renderHuman: () => void): void { if (ctx.require(JsonKey)) { ctx.require(JsonRendererKey).renderJson(result); return;