diff --git a/src/core/project/templates/export.test.ts b/src/core/project/templates/export.test.ts index 14f8e841d..2b4ca67d9 100644 --- a/src/core/project/templates/export.test.ts +++ b/src/core/project/templates/export.test.ts @@ -21,7 +21,6 @@ import { MEMORY_NAME_NOT_FOUND_NOTE_CATEGORY, MODEL_API_KEY_NOTE_CATEGORY, buildExportNotesMarkdown, - formatExportNotes, mapHarnessToExportPlan, matchesAllowedTools, type HarnessExportInput, @@ -795,21 +794,4 @@ describe("export notes rendering", () => { const markdown = buildExportNotesMarkdown([], "assistant", "assistantAgent", "v"); expect(markdown).toContain("No manual steps required."); }); - - test("formatExportNotes renders a warning block or a quiet confirmation", () => { - expect(formatExportNotes([], "notes.md")).toEqual([ - { text: "No manual follow-up required. (Details: notes.md)", tone: "dim" }, - ]); - const lines = formatExportNotes( - [{ category: "Cat", message: "line one\nline two" }], - "notes.md", - ); - expect(lines.map((line) => line.text)).toEqual([ - "1 export note requiring manual follow-up:", - " - Cat", - " line one", - " line two", - "These notes are also saved to notes.md", - ]); - }); }); diff --git a/src/core/project/templates/export.ts b/src/core/project/templates/export.ts index 702fce355..8bdb1595b 100644 --- a/src/core/project/templates/export.ts +++ b/src/core/project/templates/export.ts @@ -32,12 +32,6 @@ export interface ExportNote { message: string; } -/** A single rendered output line + a tone the caller maps to its own styling. */ -export interface ExportNoteLine { - text: string; - tone: "warn" | "dim"; -} - /** Everything the mapper needs; all reads are done by the caller. */ export interface HarnessExportInput { harnessName: string; @@ -977,7 +971,7 @@ function fnmatch(pattern: string, str: string): boolean { } // ============================================================================ -// EXPORT_NOTES.md + display formatting +// EXPORT_NOTES.md // ============================================================================ /** Render the EXPORT_NOTES.md content written into the exported agent's directory. */ @@ -1010,26 +1004,3 @@ export function buildExportNotesMarkdown( lines.push(""); return lines.join("\n"); } - -/** - * Format export notes into styled lines for the export success path. Pure so the - * CLI (and a future TUI screen) render identical wording. - */ -export function formatExportNotes(notes: ExportNote[], notesFileHint: string): ExportNoteLine[] { - if (notes.length === 0) { - return [{ text: `No manual follow-up required. (Details: ${notesFileHint})`, tone: "dim" }]; - } - - const label = notes.length === 1 ? "note" : "notes"; - const lines: ExportNoteLine[] = [ - { text: `${notes.length} export ${label} requiring manual follow-up:`, tone: "warn" }, - ]; - for (const note of notes) { - lines.push({ text: ` - ${note.category}`, tone: "warn" }); - for (const messageLine of note.message.split("\n")) { - lines.push({ text: ` ${messageLine}`, tone: "dim" }); - } - } - lines.push({ text: `These notes are also saved to ${notesFileHint}`, tone: "dim" }); - return lines; -} diff --git a/src/handlers/project/export/harness.test.ts b/src/handlers/project/export/harness.test.ts index 5a91a3286..21fc42abe 100644 --- a/src/handlers/project/export/harness.test.ts +++ b/src/handlers/project/export/harness.test.ts @@ -163,7 +163,10 @@ describe("project export harness handler", () => { }); expect(subject.io.stderr()).toContain( - "Exported harness 'exportme' to runtime agent 'exportmeAgent'", + `Next steps:\n Review the generated code in ${join("app", "exportmeAgent")}\n agentcore build\n agentcore deploy`, + ); + expect(subject.io.stderr()).toContain( + `Exported harness 'exportme' to runtime agent 'exportmeAgent' (${join("app", "exportmeAgent")})\nNext steps:`, ); expect(subject.io.stdout()).toBe(""); }); @@ -226,6 +229,8 @@ describe("project export harness handler", () => { notesPath: join(projectRoot, "app", "exportmeAgent", "EXPORT_NOTES.md"), notes: [], }); + expect(subject.io.stderr()).not.toContain("Next steps:"); + expect(subject.io.stderr()).not.toContain("Exported harness"); }); test("exports a service harness by ARN, fetching from the ARN's region", async () => { @@ -262,6 +267,9 @@ describe("project export harness handler", () => { expect(spec.runtimes.map((runtime: { name: string }) => runtime.name)).toContain( "exported_arn", ); + expect(subject.io.stderr()).toContain( + `Exported harness 'remote_harness' to runtime agent 'exported_arn' (${join("app", "exported_arn")})`, + ); }); test("defaults the --arn target name from the fetched harness name", async () => { diff --git a/src/handlers/project/export/harness.ts b/src/handlers/project/export/harness.ts index 15deacf16..e08502d8f 100644 --- a/src/handlers/project/export/harness.ts +++ b/src/handlers/project/export/harness.ts @@ -1,10 +1,11 @@ import z from "zod"; +import { relative } from "node:path"; import { InputValidationError, ResourceNotFoundError } from "../../../errors"; import { createHandler, flag, ProjectKey } from "../../../router"; import { JsonRendererKey } from "../../../tui"; +import { runWithProgress } from "../../../tui/progress"; import { JsonKey } from "../../keys"; import { AgentNameSchema } from "../../../projectSchemas/runtime"; -import { formatExportNotes } from "../../../core/project/templates/export"; import { assertMutuallyExclusiveFlags, coreOptsFromCtx } from "../../utils"; import type { ExportHarnessInput } from "../types"; import type { ExportProjectResourceConfig } from "./types"; @@ -36,50 +37,41 @@ export const createExportHarnessHandler = (config: ExportProjectResourceConfig) const project = ctx.require(ProjectKey); const jsonOutput = ctx.require(JsonKey); - let input: ExportHarnessInput; - if (flags.arn) { - config.io.stderr.write(`Fetching harness from the service\n`); - const harnessId = harnessIdFromArn(flags.arn); - // The ARN names the region the harness lives in and takes precedence over - // the CLI's resolved region, so service fetches never drift to ambient config. - const coreOpts = coreOptsFromCtx(ctx); - const region = regionFromHarnessArn(flags.arn); - const response = await config.core.harness.getHarness(harnessId, { ...coreOpts, region }); - if (!response.harness) { - throw new ResourceNotFoundError(`no harness exists for '${flags.arn}'`); - } - const { spec, systemPrompt, notes, modelAdditionalParams } = mapServiceHarnessToSpec( - response.harness, - ); - input = { - prefetched: { spec, systemPrompt, notes, modelAdditionalParams }, - targetAgentName: resolveTargetAgentName(flags["target-agent-name"], spec.name), - }; - } else { - input = { - harnessName: flags.name!, - targetAgentName: resolveTargetAgentName(flags["target-agent-name"], flags.name!), - }; - } - - // Progress goes to stderr, keeping stdout for machine output. Driven by - // hand because the result is the generator's return value. - const exportRun = config.projectManager.exportHarness(project, input); - let next = await exportRun.next(); - while (!next.done) { - if (next.value.type === "step") config.io.stderr.write(`${next.value.message}\n`); - next = await exportRun.next(); - } - const result = next.value; - - config.io.stderr.write( - `Exported harness '${result.harnessName}' to runtime agent '${result.agentName}' (${result.agentPath})\n`, - ); - for (const line of formatExportNotes(result.notes, result.notesPath)) { - config.io.stderr.write(`${line.text}\n`); - } - config.io.stderr.write( - "Next steps: review the generated code, then `agentcore build` and `agentcore deploy`\n", + const result = await runWithProgress( + (async function* () { + let input: ExportHarnessInput; + if (flags.arn) { + const harnessId = harnessIdFromArn(flags.arn); + // The ARN's region takes precedence over the CLI's resolved region. + const coreOpts = coreOptsFromCtx(ctx); + const region = regionFromHarnessArn(flags.arn); + yield { type: "step" as const, message: "Fetching harness from the service" }; + const response = await config.core.harness.getHarness(harnessId, { + ...coreOpts, + region, + }); + if (!response.harness) { + throw new ResourceNotFoundError(`no harness exists for '${flags.arn}'`); + } + const { spec, systemPrompt, notes, modelAdditionalParams } = mapServiceHarnessToSpec( + response.harness, + ); + input = { + prefetched: { spec, systemPrompt, notes, modelAdditionalParams }, + targetAgentName: resolveTargetAgentName(flags["target-agent-name"], spec.name), + }; + } else { + input = { + harnessName: flags.name!, + targetAgentName: resolveTargetAgentName(flags["target-agent-name"], flags.name!), + }; + } + return yield* config.projectManager.exportHarness(project, input); + })(), + { + io: config.io, + interactive: jsonOutput ? false : undefined, + }, ); if (jsonOutput) { @@ -90,7 +82,20 @@ export const createExportHarnessHandler = (config: ExportProjectResourceConfig) notesPath: result.notesPath, notes: result.notes, }); + return; } + + const agentPath = relative(process.cwd(), result.agentPath); + config.io.stderr.write( + `Exported harness '${result.harnessName}' to runtime agent '${result.agentName}' (${agentPath})\n`, + ); + const reviewNotes = + result.notes.length > 0 + ? ` (${result.notes.length} manual follow-up${result.notes.length === 1 ? "" : "s"} in EXPORT_NOTES.md)` + : ""; + config.io.stderr.write( + `Next steps:\n Review the generated code in ${agentPath}${reviewNotes}\n agentcore build\n agentcore deploy\n`, + ); }, }); diff --git a/src/index.ts b/src/index.ts index 8abc433ca..cf9ea0e97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,9 @@ import { CommandRunMetricEventKey, ValueContext } from "./router"; import { NpmCliVersionManager, printUpdateNotice } from "./cliVersionManager"; import { CliVersionManagerKey } from "./handlers/keys"; +// Suppress only the AWS SDK's Node.js version support warning before constructing clients. +process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED = "true"; + process.exit( await runWithExitCode(async (argv: string[]) => { const startTime = Date.now();