From e8e14a627005d38ebdfbf3b97bddb340ec27e2ca Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 22:41:49 +0000 Subject: [PATCH 1/7] feat(project): run create under the shared step-progress driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `project create` printed a flat line per step while `build` and `deploy` render the live step list #2163 introduced. It now uses the same `runWithProgress` driver, so a TTY gets the spinner and per-step ✓, and the non-TTY and --json paths keep the previous plain output byte for byte. Every event the create generator yields is already a `step`, so nothing else had to change. The bare interactive `project create` still opens the TUI wizard. --- src/handlers/project/create/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 4277f34a1..e158cbc9a 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -1,6 +1,7 @@ import z from "zod"; import { createHandler, flag } from "../../../router"; import { SourceResolver, type AppIO } from "../../../io"; +import { runWithProgress } from "../../../tui/progress"; import { LANGUAGE_VERSION_DEFAULTS, MEMORY_SHORTCUT_NAMES, @@ -28,7 +29,7 @@ import { DEFAULT_HARNESS_MODEL } from "../add/harness"; import type { CoreBedrockAgentImporter } from "../../../core/project/bedrockAgentImport"; import { importScaffoldRuntimeInput, resolveImportBedrockAgentInput } from "../importBedrockAgent"; import type { ImportBedrockAgentInput } from "../add/runtime/types"; -import { RegionKey } from "../../keys"; +import { JsonKey, RegionKey } from "../../keys"; type CreateProjectHandlerConfig = { projectManager: ProjectManager; @@ -300,9 +301,12 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = ); } - for await (const event of config.projectManager.create(createInput)) { - if (event.type === "step") config.io.stderr.write(`${event.message}\n`); - } + // 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), { + io: config.io, + interactive: ctx.require(JsonKey) ? false : undefined, + }); config.io.stderr.write(`Created project '${name}' in ./${name}\n`); config.io.stderr.write(`To deploy it: cd ${name} && agentcore project deploy\n`); From 38c12b1c218871b53cf1ca84c4cd8607390e6bed Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 22:56:59 +0000 Subject: [PATCH 2/7] feat(project): tail subprocess output under create's running step The step list create now runs under can show a live tail of the running step's output, but create's subprocesses sent their chunks only to the debug log, so the tail was always empty and create looked flatter than build and deploy. `npm install` (CDK app and scaffolded runtime), `uv sync`, and container lockfile generation now stream through withOutputEvents plus createLineSplitter, the same bridge the CDK backend uses for synth. Chunks still reach the debug log whole; the splitter reassembles them into lines for display and flushes an unterminated trailing chunk when the process exits. `git init` stays plain since it prints nothing worth tailing. Draining the generator on a real create yields 3 step events and 133 output events, including "added 320 packages, and audited 341 packages in 6s" and "Creating virtual environment at: .venv". A cached npm install finishes in milliseconds and flushes at the end, so the tail is only visible on a cold cache or a slow step -- and on failure, where the driver keeps it in scrollback. --- src/core/project/manager.test.ts | 28 ++++++++++++++++++++++++++++ src/core/project/manager.tsx | 31 +++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index 47b11e5fa..33cb422d7 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -95,6 +95,34 @@ async function projectManifest(projectRoot: string): Promise { } describe("FsProjectManager.create", () => { + // The progress driver renders a live tail under the running step from `output` events. Before + // these steps streamed, subprocess chunks went only to the debug log and the tail stayed empty. + test("streams subprocess output as output events so progress can tail it", async () => { + await inTempDirectory(); + const subject = new FsProjectManager({ + logger: createSilentLogger(), + identity: new TestIdentityClient(), + runner: async (_command, { onOutput }) => { + onOutput?.("added 320 packages, and audited 341 packages in 6s\n"); + onOutput?.("partial line with no trailing newline"); + }, + checkTool: async () => {}, + }); + + const { events } = await runCreate(subject, { + name: "example", + scaffoldRuntimeInput: HELLO_WORLD_PYTHON, + skipInstall: false, + skipGit: true, + }); + + const lines = events.flatMap((event) => (event.type === "output" ? [event.line] : [])); + expect(lines).toContain("added 320 packages, and audited 341 packages in 6s"); + // The splitter must flush the unterminated trailing chunk when the process exits. + expect(lines).toContain("partial line with no trailing newline"); + expect(events.some((event) => event.type === "step")).toBe(true); + }); + test("scaffolds the expected file tree into a fresh directory", async () => { const directory = await inTempDirectory(); await runCreate(manager().manager, { diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index c502cbdae..f88621363 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -24,11 +24,13 @@ import type { import type { Logger } from "../../logging"; import { FsReadWriteJson, + createLineSplitter, requireTool, runProcess, type ProcessRunner, type ReadWriteJson, } from "../../io"; +import { withOutputEvents } from "./events"; import { defaultSource, type AssetSource } from "./source"; import { ENV_LOCAL_RELATIVE_PATH, EnvLocalFile } from "./envLocal"; import { getHarnessTemplateResolver, validateHarnessTemplateSource } from "./templates/harness"; @@ -197,7 +199,7 @@ export class FsProjectManager implements ProjectManager { if (!input.skipInstall) { await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); yield { type: "step", message: "Installing CDK dependencies with npm" }; - await this.run(["npm", "install"], join(destination, "agentcore", "cdk")); + yield* this.runStreaming(["npm", "install"], join(destination, "agentcore", "cdk")); if (scaffoldRuntimeInput) { const appDir = join(destination, "app", scaffoldRuntimeInput.runtimeName); @@ -1044,11 +1046,11 @@ export class FsProjectManager implements ProjectManager { "Install uv: https://docs.astral.sh/uv/getting-started/installation/", ); yield { type: "step", message: "Syncing Python dependencies with uv" }; - await this.run(["uv", "sync"], appDir); + yield* this.runStreaming(["uv", "sync"], appDir); } else if (existsSync(join(appDir, "package.json"))) { await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); yield { type: "step", message: "Installing Node dependencies with npm" }; - await this.run(["npm", "install"], appDir); + yield* this.runStreaming(["npm", "install"], appDir); } } @@ -1068,7 +1070,7 @@ export class FsProjectManager implements ProjectManager { } yield { type: "step", message: `Generating ${lockfile} for container build` }; try { - await this.run(command, appDir); + yield* this.runStreaming(command, appDir); } catch { yield { type: "step", @@ -1086,6 +1088,27 @@ export class FsProjectManager implements ProjectManager { private run(command: string[], cwd: string): Promise { return this.runner(command, { cwd, onOutput: (chunk) => this.logger.debug(chunk) }); } + + /** + * Runs a command like {@link run}, additionally yielding its output as `output` events so a + * progress driver can show a live tail under the running step. Chunks still reach the debug log + * whole; the splitter reassembles them into lines for display. + */ + private async *runStreaming( + command: string[], + cwd: string, + ): AsyncGenerator { + yield* withOutputEvents((emit) => { + const lines = createLineSplitter(emit); + return this.runner(command, { + cwd, + onOutput: (chunk) => { + this.logger.debug(chunk); + lines.push(chunk); + }, + }).finally(() => lines.flush()); + }); + } } /** Map {@link ProjectResource} to keys in the project spec. From 97b155c865da8ed5682786477329dbb2a4854e20 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 23:18:31 +0000 Subject: [PATCH 3/7] test(project): use the renamed agent-python shortcut constant The rebase onto refactor picked up #2170, which renamed the hello-world-python template shortcut, so the new streaming test referenced a constant that no longer exists. --- src/core/project/manager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index 33cb422d7..32f103d98 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -111,7 +111,7 @@ describe("FsProjectManager.create", () => { const { events } = await runCreate(subject, { name: "example", - scaffoldRuntimeInput: HELLO_WORLD_PYTHON, + scaffoldRuntimeInput: AGENT_PYTHON, skipInstall: false, skipGit: true, }); From 06961a72c4345582baf9f109c6102380fd385eb1 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 23:40:13 +0000 Subject: [PATCH 4/7] feat(project): show package names while npm installs npm prints nothing at all while its stderr is piped, so the progress tail stayed empty for the first ~5.8s of a ~6.5s install. Neither --progress=true nor a real PTY helps: the flag is ignored when piped, and npm's TTY output is a textless spinner. Its HTTP log is the only per-package progress it will emit, so --loglevel=http is now parsed back into package names -- 'resolving aws-cdk-lib' rather than 'npm http fetch GET 200 https://registry.npmjs.org/aws-cdk-lib 34ms'. First readable line lands at 540ms instead of 5869ms, at no measurable cost. uv sync needs none of this; it already prints for people. --- src/core/project/manager.test.ts | 5 ++- src/core/project/manager.tsx | 19 ++++++++-- src/core/project/npmProgress.ts | 55 ++++++++++++++++++++++++++++ src/handlers/project/project.test.ts | 10 ++++- 4 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 src/core/project/npmProgress.ts diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index 32f103d98..70105dab0 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -298,7 +298,10 @@ describe("FsProjectManager.create", () => { const projectRoot = join(directory, "example"); expect(commands).toEqual([ - { command: ["npm", "install"], cwd: join(projectRoot, "agentcore", "cdk") }, + { + command: ["npm", "install", "--loglevel=http"], + cwd: join(projectRoot, "agentcore", "cdk"), + }, { command: ["uv", "sync"], cwd: join(projectRoot, "app", "agent_python") }, { command: ["git", "init"], cwd: projectRoot }, ]); diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index f88621363..5f6202e1f 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -31,6 +31,7 @@ import { type ReadWriteJson, } from "../../io"; import { withOutputEvents } from "./events"; +import { formatNpmProgressLine } from "./npmProgress"; import { defaultSource, type AssetSource } from "./source"; import { ENV_LOCAL_RELATIVE_PATH, EnvLocalFile } from "./envLocal"; import { getHarnessTemplateResolver, validateHarnessTemplateSource } from "./templates/harness"; @@ -85,6 +86,10 @@ import type { CoreIdentityClient } from "../../handlers/identity/types"; const TARGETS_EXAMPLE = '[{ "name": "default", "account": "111122223333", "region": "us-east-1" }]'; +// npm prints nothing until it exits when stderr is piped; its HTTP log is the only per-package +// progress it will emit, and formatNpmProgressLine renders it readably. +const NPM_INSTALL = ["npm", "install", "--loglevel=http"]; + type ProjectManagerConfig = { logger: Logger; createCloudFormationClient?: CreateCloudFormationClient; @@ -199,7 +204,11 @@ export class FsProjectManager implements ProjectManager { if (!input.skipInstall) { await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); yield { type: "step", message: "Installing CDK dependencies with npm" }; - yield* this.runStreaming(["npm", "install"], join(destination, "agentcore", "cdk")); + yield* this.runStreaming( + NPM_INSTALL, + join(destination, "agentcore", "cdk"), + formatNpmProgressLine, + ); if (scaffoldRuntimeInput) { const appDir = join(destination, "app", scaffoldRuntimeInput.runtimeName); @@ -1050,7 +1059,7 @@ export class FsProjectManager implements ProjectManager { } else if (existsSync(join(appDir, "package.json"))) { await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); yield { type: "step", message: "Installing Node dependencies with npm" }; - yield* this.runStreaming(["npm", "install"], appDir); + yield* this.runStreaming(NPM_INSTALL, appDir, formatNpmProgressLine); } } @@ -1097,9 +1106,13 @@ export class FsProjectManager implements ProjectManager { private async *runStreaming( command: string[], cwd: string, + formatLine: (line: string) => string | undefined = (line) => line, ): AsyncGenerator { yield* withOutputEvents((emit) => { - const lines = createLineSplitter(emit); + const lines = createLineSplitter((line) => { + const formatted = formatLine(line); + if (formatted !== undefined) emit(formatted); + }); return this.runner(command, { cwd, onOutput: (chunk) => { diff --git a/src/core/project/npmProgress.ts b/src/core/project/npmProgress.ts new file mode 100644 index 000000000..342c24a13 --- /dev/null +++ b/src/core/project/npmProgress.ts @@ -0,0 +1,55 @@ +const HTTP_FETCH = /^npm http fetch [A-Z]+ \d{3} (\S+)/; + +/** + * Rewrites `npm install --loglevel=http` output into lines worth showing in a progress tail. npm + * prints nothing at all while its stderr is piped, and its HTTP log is the only per-package progress + * it can be made to emit, so the registry URLs are turned back into package names here. Lines npm + * already writes for people -- deprecation warnings and the closing summary -- pass through. + */ +export function formatNpmProgressLine(line: string): string | undefined { + const url = HTTP_FETCH.exec(line)?.[1]; + if (url === undefined) return line; + + let path: string; + try { + path = new URL(url).pathname; + } catch { + return undefined; + } + // Registry API calls name no package. During an install the only one is the audit request. + if (path.startsWith("/-/")) return "auditing dependencies"; + + const [manifestPath = "", tarball] = path.split("/-/"); + const name = packageName(manifestPath); + if (!name) return undefined; + if (tarball === undefined) return `resolving ${name}`; + return `downloading ${name}@${tarballVersion(tarball, name)}`; +} + +/** + * The package name is the tail of the path, since a private registry may serve the same manifests + * under a prefix (`/artifactory/api/npm/registry/aws-cdk-lib`). A scope arrives either encoded into + * one segment (`@aws-sdk%2fcore`) or as its own (`@aws-sdk/core`). + */ +function packageName(manifestPath: string): string | undefined { + const segments = manifestPath.split("/").filter(Boolean).map(decodeSegment); + const last = segments[segments.length - 1]; + if (last === undefined) return undefined; + const scope = segments[segments.length - 2]; + return scope?.startsWith("@") ? `${scope}/${last}` : last; +} + +function decodeSegment(segment: string): string { + try { + return decodeURIComponent(segment); + } catch { + return segment; + } +} + +/** A tarball is named `-.tgz`. */ +function tarballVersion(tarball: string, name: string): string { + const base = tarball.replace(/\.tgz$/, ""); + const prefix = `${name.split("/").pop()}-`; + return base.startsWith(prefix) ? base.slice(prefix.length) : base; +} diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 200f55ce6..74dbc5f82 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -263,7 +263,10 @@ describe("project create", () => { const projectRoot = join(directory, "MyAgent"); expect(core.projectCommands).toEqual([ - { command: ["npm", "install"], cwd: join(projectRoot, "agentcore", "cdk") }, + { + command: ["npm", "install", "--loglevel=http"], + cwd: join(projectRoot, "agentcore", "cdk"), + }, { command: ["git", "init"], cwd: projectRoot }, ]); }); @@ -365,7 +368,10 @@ describe("project create", () => { const projectRoot = join(directory, "MyAgent"); expect(core.projectCommands).toEqual([ - { command: ["npm", "install"], cwd: join(projectRoot, "agentcore", "cdk") }, + { + command: ["npm", "install", "--loglevel=http"], + cwd: join(projectRoot, "agentcore", "cdk"), + }, { command: ["uv", "sync"], cwd: join(projectRoot, "app", "agent_python") }, { command: ["git", "init"], cwd: projectRoot }, ]); From 83c4dce82b523548b2630d9d7d13d2916eb2f939 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 23:44:54 +0000 Subject: [PATCH 5/7] refactor(project): inline npm progress rendering and drop its plumbing test The four functions in npmProgress.ts were each used once, and tarballVersion parsed a version out of a tarball name only to print it in a scrolling tail. Collapsed to a single function beside the flag that makes npm talk, dropping the file: the version, the URL parsing, and the decodeURIComponent guard all go away, since npm's only encoding is %2f for a scope slash. Also drops the create streaming test, which asserted that a mocked runner's chunks reach the generator -- plumbing the type system already pins. --- src/core/project/manager.test.ts | 28 ---------------------------- src/core/project/manager.tsx | 30 +++++++++++++++++++++--------- 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index 70105dab0..674b84be7 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -95,34 +95,6 @@ async function projectManifest(projectRoot: string): Promise { } describe("FsProjectManager.create", () => { - // The progress driver renders a live tail under the running step from `output` events. Before - // these steps streamed, subprocess chunks went only to the debug log and the tail stayed empty. - test("streams subprocess output as output events so progress can tail it", async () => { - await inTempDirectory(); - const subject = new FsProjectManager({ - logger: createSilentLogger(), - identity: new TestIdentityClient(), - runner: async (_command, { onOutput }) => { - onOutput?.("added 320 packages, and audited 341 packages in 6s\n"); - onOutput?.("partial line with no trailing newline"); - }, - checkTool: async () => {}, - }); - - const { events } = await runCreate(subject, { - name: "example", - scaffoldRuntimeInput: AGENT_PYTHON, - skipInstall: false, - skipGit: true, - }); - - const lines = events.flatMap((event) => (event.type === "output" ? [event.line] : [])); - expect(lines).toContain("added 320 packages, and audited 341 packages in 6s"); - // The splitter must flush the unterminated trailing chunk when the process exits. - expect(lines).toContain("partial line with no trailing newline"); - expect(events.some((event) => event.type === "step")).toBe(true); - }); - test("scaffolds the expected file tree into a fresh directory", async () => { const directory = await inTempDirectory(); await runCreate(manager().manager, { diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 5f6202e1f..ab177e26d 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -31,7 +31,6 @@ import { type ReadWriteJson, } from "../../io"; import { withOutputEvents } from "./events"; -import { formatNpmProgressLine } from "./npmProgress"; import { defaultSource, type AssetSource } from "./source"; import { ENV_LOCAL_RELATIVE_PATH, EnvLocalFile } from "./envLocal"; import { getHarnessTemplateResolver, validateHarnessTemplateSource } from "./templates/harness"; @@ -86,9 +85,26 @@ import type { CoreIdentityClient } from "../../handlers/identity/types"; const TARGETS_EXAMPLE = '[{ "name": "default", "account": "111122223333", "region": "us-east-1" }]'; -// npm prints nothing until it exits when stderr is piped; its HTTP log is the only per-package -// progress it will emit, and formatNpmProgressLine renders it readably. +// npm prints nothing until it exits when stderr is piped, and its HTTP log is the only per-package +// progress it will emit, so the log is asked for and then rewritten into package names. const NPM_INSTALL = ["npm", "install", "--loglevel=http"]; +const NPM_FETCH = /^npm http fetch [A-Z]+ \d{3} https?:\/\/[^/]+(\S*)/; + +function npmProgressLine(line: string): string | undefined { + const path = NPM_FETCH.exec(line)?.[1]; + // Deprecation warnings and the closing summary are already written for people. + if (path === undefined) return line; + // `/-/npm/v1/...` names no package; the only one an install makes is the audit request. + if (path.startsWith("/-/")) return "auditing dependencies"; + // A private registry may serve manifests under a prefix, so the name is the path's tail. A scope + // reaches us either as its own segment or encoded into one as %2f. + const [manifest = "", tarball] = path.split("/-/"); + const segments = manifest.replace(/%2f/gi, "/").split("/").filter(Boolean); + const scope = segments.at(-2); + const name = scope?.startsWith("@") ? `${scope}/${segments.at(-1)}` : segments.at(-1); + if (name === undefined) return undefined; + return `${tarball === undefined ? "resolving" : "downloading"} ${name}`; +} type ProjectManagerConfig = { logger: Logger; @@ -204,11 +220,7 @@ export class FsProjectManager implements ProjectManager { if (!input.skipInstall) { await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); yield { type: "step", message: "Installing CDK dependencies with npm" }; - yield* this.runStreaming( - NPM_INSTALL, - join(destination, "agentcore", "cdk"), - formatNpmProgressLine, - ); + yield* this.runStreaming(NPM_INSTALL, join(destination, "agentcore", "cdk"), npmProgressLine); if (scaffoldRuntimeInput) { const appDir = join(destination, "app", scaffoldRuntimeInput.runtimeName); @@ -1059,7 +1071,7 @@ export class FsProjectManager implements ProjectManager { } else if (existsSync(join(appDir, "package.json"))) { await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); yield { type: "step", message: "Installing Node dependencies with npm" }; - yield* this.runStreaming(NPM_INSTALL, appDir, formatNpmProgressLine); + yield* this.runStreaming(NPM_INSTALL, appDir, npmProgressLine); } } From 510c9b5ff827f179b31b898951fb68d9d71689e5 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 23:45:07 +0000 Subject: [PATCH 6/7] refactor(project): remove npmProgress.ts, now inlined in manager --- src/core/project/npmProgress.ts | 55 --------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 src/core/project/npmProgress.ts diff --git a/src/core/project/npmProgress.ts b/src/core/project/npmProgress.ts deleted file mode 100644 index 342c24a13..000000000 --- a/src/core/project/npmProgress.ts +++ /dev/null @@ -1,55 +0,0 @@ -const HTTP_FETCH = /^npm http fetch [A-Z]+ \d{3} (\S+)/; - -/** - * Rewrites `npm install --loglevel=http` output into lines worth showing in a progress tail. npm - * prints nothing at all while its stderr is piped, and its HTTP log is the only per-package progress - * it can be made to emit, so the registry URLs are turned back into package names here. Lines npm - * already writes for people -- deprecation warnings and the closing summary -- pass through. - */ -export function formatNpmProgressLine(line: string): string | undefined { - const url = HTTP_FETCH.exec(line)?.[1]; - if (url === undefined) return line; - - let path: string; - try { - path = new URL(url).pathname; - } catch { - return undefined; - } - // Registry API calls name no package. During an install the only one is the audit request. - if (path.startsWith("/-/")) return "auditing dependencies"; - - const [manifestPath = "", tarball] = path.split("/-/"); - const name = packageName(manifestPath); - if (!name) return undefined; - if (tarball === undefined) return `resolving ${name}`; - return `downloading ${name}@${tarballVersion(tarball, name)}`; -} - -/** - * The package name is the tail of the path, since a private registry may serve the same manifests - * under a prefix (`/artifactory/api/npm/registry/aws-cdk-lib`). A scope arrives either encoded into - * one segment (`@aws-sdk%2fcore`) or as its own (`@aws-sdk/core`). - */ -function packageName(manifestPath: string): string | undefined { - const segments = manifestPath.split("/").filter(Boolean).map(decodeSegment); - const last = segments[segments.length - 1]; - if (last === undefined) return undefined; - const scope = segments[segments.length - 2]; - return scope?.startsWith("@") ? `${scope}/${last}` : last; -} - -function decodeSegment(segment: string): string { - try { - return decodeURIComponent(segment); - } catch { - return segment; - } -} - -/** A tarball is named `-.tgz`. */ -function tarballVersion(tarball: string, name: string): string { - const base = tarball.replace(/\.tgz$/, ""); - const prefix = `${name.split("/").pop()}-`; - return base.startsWith(prefix) ? base.slice(prefix.length) : base; -} From 646fb74f46d10c884e4674a3912c052f7eb1cb61 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 23:50:32 +0000 Subject: [PATCH 7/7] refactor(project): make run stream instead of adding a second method runStreaming duplicated run rather than replacing it, leaving two methods doing one job and one caller -- git init -- still on the old path because it prints little worth tailing. That is not worth a duplicate method: run now streams, every caller yields it, and git init's one line shows like any other. --- src/core/project/manager.tsx | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index ab177e26d..82dbfe265 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -220,7 +220,7 @@ export class FsProjectManager implements ProjectManager { if (!input.skipInstall) { await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); yield { type: "step", message: "Installing CDK dependencies with npm" }; - yield* this.runStreaming(NPM_INSTALL, join(destination, "agentcore", "cdk"), npmProgressLine); + yield* this.run(NPM_INSTALL, join(destination, "agentcore", "cdk"), npmProgressLine); if (scaffoldRuntimeInput) { const appDir = join(destination, "app", scaffoldRuntimeInput.runtimeName); @@ -235,7 +235,7 @@ export class FsProjectManager implements ProjectManager { if (!input.skipGit) { await this.checkTool("git", "Install git: https://git-scm.com/downloads"); yield { type: "step", message: "Initializing git repository" }; - await this.run(["git", "init"], destination); + yield* this.run(["git", "init"], destination); } // A created project is a resolvable one, so read it back rather than @@ -1067,11 +1067,11 @@ export class FsProjectManager implements ProjectManager { "Install uv: https://docs.astral.sh/uv/getting-started/installation/", ); yield { type: "step", message: "Syncing Python dependencies with uv" }; - yield* this.runStreaming(["uv", "sync"], appDir); + yield* this.run(["uv", "sync"], appDir); } else if (existsSync(join(appDir, "package.json"))) { await this.checkTool("npm", "Install Node.js: https://nodejs.org/"); yield { type: "step", message: "Installing Node dependencies with npm" }; - yield* this.runStreaming(NPM_INSTALL, appDir, npmProgressLine); + yield* this.run(NPM_INSTALL, appDir, npmProgressLine); } } @@ -1091,7 +1091,7 @@ export class FsProjectManager implements ProjectManager { } yield { type: "step", message: `Generating ${lockfile} for container build` }; try { - yield* this.runStreaming(command, appDir); + yield* this.run(command, appDir); } catch { yield { type: "step", @@ -1105,17 +1105,12 @@ export class FsProjectManager implements ProjectManager { } } - // Runs a command with its output streamed to the file logger. - private run(command: string[], cwd: string): Promise { - return this.runner(command, { cwd, onOutput: (chunk) => this.logger.debug(chunk) }); - } - /** - * Runs a command like {@link run}, additionally yielding its output as `output` events so a - * progress driver can show a live tail under the running step. Chunks still reach the debug log - * whole; the splitter reassembles them into lines for display. + * Runs a command, yielding its output as `output` events so a progress driver can show a live tail + * under the running step. The debug log gets each chunk whole; the splitter reassembles them into + * lines for display, and `formatLine` may rewrite or drop a line before it is shown. */ - private async *runStreaming( + private async *run( command: string[], cwd: string, formatLine: (line: string) => string | undefined = (line) => line,