diff --git a/src/assets/templates/runtime-docs/python/README.md b/src/assets/templates/runtime-docs/python/README.md
index b224f373cb..262bbc0bee 100644
--- a/src/assets/templates/runtime-docs/python/README.md
+++ b/src/assets/templates/runtime-docs/python/README.md
@@ -44,10 +44,14 @@ prints anything it changed:
- **Build type:** adding a file named exactly `Dockerfile` to this folder
switches the runtime to a container build, and the CLI says so. It never
- switches back on its own: to return to a direct code deploy, delete the
- Dockerfile, then in `agentcore.json` set `"build": "CodeZip"`, remove
- `"dockerfile"`, and add back a `"runtimeVersion"` (the CLI removed it when
- it switched; the schema requires it for CodeZip).
+ switches back on its own. To return to a direct code deploy, delete the
+ `Dockerfile`, then open `agentcore/agentcore.json`. In the `runtimes` array,
+ find the object whose `"name"` is `"{{name}}"`, set `"build": "CodeZip"`,
+ remove `"dockerfile"` (and `"buildContextPath"` or
+ `"customDockerBuildArgs"` if present), and add
+ `"runtimeVersion": "{{versions.latestPythonRuntime}}"` to that same object.
+ The CLI removed `runtimeVersion` when it switched to a container build, and
+ CodeZip requires it.
- **Python version** (CodeZip only): kept as long as `requires-python` in
`pyproject.toml` allows it (see below).
- **Entrypoint:** from `[tool.agentcore]` in `pyproject.toml`, if you want a
diff --git a/src/assets/templates/runtime-docs/typescript/README.md b/src/assets/templates/runtime-docs/typescript/README.md
index a040b6f15a..0809014cc9 100644
--- a/src/assets/templates/runtime-docs/typescript/README.md
+++ b/src/assets/templates/runtime-docs/typescript/README.md
@@ -45,10 +45,14 @@ the runtime in `agentcore/agentcore.json`, and prints anything it changed:
- **Build type:** adding a file named exactly `Dockerfile` to this folder
switches the runtime to a container build, and the CLI says so. It never
- switches back on its own: to return to a direct code deploy, delete the
- Dockerfile, then in `agentcore.json` set `"build": "CodeZip"`, remove
- `"dockerfile"`, and add back a `"runtimeVersion"` (the CLI removed it when
- it switched; the schema requires it for CodeZip).
+ switches back on its own. To return to a direct code deploy, delete the
+ `Dockerfile`, then open `agentcore/agentcore.json`. In the `runtimes` array,
+ find the object whose `"name"` is `"{{name}}"`, set `"build": "CodeZip"`,
+ remove `"dockerfile"` (and `"buildContextPath"` or
+ `"customDockerBuildArgs"` if present), and add
+ `"runtimeVersion": "{{versions.latestNodeRuntime}}"` to that same object. The
+ CLI removed `runtimeVersion` when it switched to a container build, and
+ CodeZip requires it.
- **Node.js version** (CodeZip only): kept as long as `engines.node` in
`package.json` allows it (see below).
- **Entrypoint** (CodeZip only): AgentCore currently bundles `main.ts` at the
diff --git a/src/components/RouterScreen.test.tsx b/src/components/RouterScreen.test.tsx
index a941820abb..4378cce87c 100644
--- a/src/components/RouterScreen.test.tsx
+++ b/src/components/RouterScreen.test.tsx
@@ -15,6 +15,8 @@ import {
import { glyphs } from "./ui/_core";
import { createProjectHandlers } from "../handlers/project";
import type { Project } from "../handlers/project/types";
+import { ProjectDetectedKey, V0ProjectDetectedKey } from "../handlers/project/context";
+import { V0_PROJECT_TUI_BANNER } from "../handlers/project/v0Notice";
afterEach(cleanupScreens);
@@ -52,6 +54,25 @@ function menuGroups(frame: string): { title: string | undefined; names: string[]
// rendered frames — behavior a user would see, not internal state.
describe("menu rendering", () => {
+ test("places the v0 compatibility warning below the command input and above dev", async () => {
+ const screen = renderScreen("/agentcore", {
+ withContext: (ctx) =>
+ ctx.withValue(ProjectDetectedKey, true).withValue(V0ProjectDetectedKey, true),
+ });
+ await waitForText(screen.lastFrame, V0_PROJECT_TUI_BANNER);
+
+ const lines = (screen.lastFrame() ?? "").split("\n");
+ const inputRow = lines.findIndex((line) => line.includes("type to choose a command"));
+ const warningRow = lines.findIndex((line) =>
+ line.includes(`${glyphs.warning} ${V0_PROJECT_TUI_BANNER}`),
+ );
+ const devRow = lines.findIndex((line) => line.includes("❯ dev"));
+
+ expect(inputRow).toBeGreaterThanOrEqual(0);
+ expect(warningRow).toBeGreaterThan(inputRow);
+ expect(devRow).toBeGreaterThan(warningRow);
+ });
+
test.each([
{
path: "/agentcore/gateway",
diff --git a/src/components/RouterScreen.tsx b/src/components/RouterScreen.tsx
index 9740093f69..4611e2aef4 100644
--- a/src/components/RouterScreen.tsx
+++ b/src/components/RouterScreen.tsx
@@ -16,7 +16,7 @@ import {
shouldHideBrandBanner,
} from "./BrandBanner";
import { Layout } from "./Layout";
-import { Alert } from "./ui/alert";
+import { Alert, type AlertProps } from "./ui/alert";
import { Divider } from "./ui/divider";
import { TextInput } from "./ui/text-input";
import { darkTheme, glyphs } from "./ui/_core.js";
@@ -84,6 +84,7 @@ export interface RouterScreenProps extends ScreenProps {
cliSectionCommands?: readonly string[];
// alert is optional guidance rendered between the filter and menu options.
alert?: string;
+ alertVariant?: AlertProps["variant"];
hiddenOptions?: readonly string[];
}
@@ -108,6 +109,7 @@ function CommandMenu({
optionNotices,
cliSectionCommands = [],
alert,
+ alertVariant,
hiddenOptions,
command,
}: RouterScreenProps & { command: Command }) {
@@ -226,6 +228,7 @@ function CommandMenu({
{({ columns, contentRows }) => (
- {alert && {alert}}
+ {alert && {alert}}
{filtered.length === 0 ? (
diff --git a/src/components/ui/alert/Alert.tsx b/src/components/ui/alert/Alert.tsx
index f5f918c440..654254644b 100644
--- a/src/components/ui/alert/Alert.tsx
+++ b/src/components/ui/alert/Alert.tsx
@@ -5,20 +5,19 @@ import { darkTheme, glyphs, type InkUITheme } from "../_core.js";
export interface AlertProps {
children: React.ReactNode;
theme?: InkUITheme;
+ variant?: "info" | "warning";
}
/**
- * Compact informational callout for inline TUI guidance.
+ * Compact callout for inline TUI guidance.
*/
-export function Alert({ children, theme = darkTheme }: AlertProps) {
+export function Alert({ children, theme = darkTheme, variant = "info" }: AlertProps) {
+ const color = variant === "warning" ? theme.colors.warning : theme.colors.secondary;
+ const glyph = variant === "warning" ? glyphs.warning : glyphs.info;
+
return (
-
- {glyphs.info}
+
+ {glyph}
{children}
);
diff --git a/src/core/project/templates/runtimeDocs.ts b/src/core/project/templates/runtimeDocs.ts
index 30e13e3ff0..09261c4d5c 100644
--- a/src/core/project/templates/runtimeDocs.ts
+++ b/src/core/project/templates/runtimeDocs.ts
@@ -65,6 +65,8 @@ export function runtimeDocsTree(
versions: {
latestPython: versionNumber(LATEST_PYTHON),
latestNode: versionNumber(LATEST_NODE),
+ latestPythonRuntime: LATEST_PYTHON,
+ latestNodeRuntime: LATEST_NODE,
python: listVersions(PythonRuntimeSchema.options.map(versionNumber)),
node: listVersions(NodeRuntimeSchema.options.map(versionNumber)),
},
diff --git a/src/handlers/index.tsx b/src/handlers/index.tsx
index e8d64afcae..b56a537bed 100644
--- a/src/handlers/index.tsx
+++ b/src/handlers/index.tsx
@@ -18,6 +18,7 @@ import {
withLogging,
withGlobalConfigAccessor,
withPlatform,
+ withV0ProjectNotice,
} from "../middleware";
import type { AppIO } from "../io";
import type { V0DeployRunner } from "../core/project/legacy/deploy";
@@ -86,6 +87,10 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router
// Pin the host platform so Windows-specific behavior is decided from the context.
root.use(withPlatform(config.platform ?? process.platform));
+ // Make v0 compatibility mode visible for every command and TUI session
+ // launched from a legacy project.
+ root.use(withV0ProjectNotice(io));
+
// Install sub handlers. Registration order is menu/help order; project is
// the primary workflow, so it goes first.
createProjectHandlers(core, io, { runV0Deploy: config.runV0Deploy }).forEach((handler) => {
diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts
index b5283eeed5..783a88f2ec 100644
--- a/src/handlers/project/add/runtime/index.test.ts
+++ b/src/handlers/project/add/runtime/index.test.ts
@@ -163,6 +163,10 @@ describe("project add runtime", () => {
: "https://github.com/aws/bedrock-agentcore-sdk-typescript",
);
expect(readme).toContain("## How `agentcore.json` follows your code");
+ expect(readme).toContain('find the object whose `"name"` is `"search"`');
+ expect(readme).toContain(`"runtimeVersion": "${expected.runtimeVersion as string}"`);
+ expect(readme).toContain('"buildContextPath"');
+ expect(readme).toContain('"customDockerBuildArgs"');
if (expected.entrypoint === "main.js") {
expect(readme).toContain("--format=esm");
expect(readme).toContain("--packages=external");
diff --git a/src/handlers/project/context.ts b/src/handlers/project/context.ts
index 8d6a5b6c05..b994af552c 100644
--- a/src/handlers/project/context.ts
+++ b/src/handlers/project/context.ts
@@ -3,3 +3,15 @@ import { contextKey } from "../../router";
// ProjectDetectedKey records whether the TUI was launched from inside a project
// without pinning a Project object that could become stale while the TUI is open.
export const ProjectDetectedKey = contextKey("project.detected");
+
+// V0ProjectDetectedKey lets every TUI screen show the compatibility notice
+// without loading or retaining a mutable Project object for the whole session.
+export const V0ProjectDetectedKey = contextKey("project.v0.detected");
+
+export interface V0ProjectNoticeState {
+ enteredTui: boolean;
+}
+
+// The root command middleware uses this mutable invocation-local marker to
+// avoid printing both a direct-command reminder and the TUI exit reminder.
+export const V0ProjectNoticeStateKey = contextKey("project.v0.noticeState");
diff --git a/src/handlers/project/deploy/index.test.ts b/src/handlers/project/deploy/index.test.ts
index c1936fb5d7..124783a662 100644
--- a/src/handlers/project/deploy/index.test.ts
+++ b/src/handlers/project/deploy/index.test.ts
@@ -21,6 +21,7 @@ import { writeMigrationState } from "../../../core/project/migrate/journal";
import { ProjectStateError } from "../../../errors";
import type { AwsDeploymentTarget } from "../../../projectSchemas/aws-targets";
import type { DeployResult, Project, ProjectEvent, TeardownConfirmationRequest } from "../types";
+import { V0_PROJECT_MIGRATION_MESSAGE } from "../v0Notice";
const DEFAULT_TARGET: AwsDeploymentTarget = {
name: "default",
@@ -277,10 +278,11 @@ describe("project deploy handler", () => {
"--region is ignored for v0 projects; v0 uses the target's region from " +
"aws-targets.json, or AWS_REGION / your AWS profile.";
expect(subject.io.stderr()).toBe(
- flag === "--json"
- ? regionWarning
- : `${regionWarning}\n` +
- "This is a v0 project; running the latest v0 AgentCore CLI deploy in compatibility mode.",
+ `${regionWarning}\n` +
+ (flag === "--json"
+ ? V0_PROJECT_MIGRATION_MESSAGE
+ : "This is a v0 project; running the latest v0 AgentCore CLI deploy in compatibility mode." +
+ `\n${V0_PROJECT_MIGRATION_MESSAGE}`),
);
expect(
await Promise.all([readFile(specPath, "utf8"), readFile(manifestPath, "utf8")]),
@@ -309,7 +311,8 @@ describe("project deploy handler", () => {
expect(legacyDeploys).toHaveLength(1);
expect(subject.io.stderr()).toBe(
- "This is a v0 project; running the latest v0 AgentCore CLI deploy in compatibility mode.",
+ "This is a v0 project; running the latest v0 AgentCore CLI deploy in compatibility mode." +
+ `\n${V0_PROJECT_MIGRATION_MESSAGE}`,
);
});
diff --git a/src/handlers/project/v0Notice.ts b/src/handlers/project/v0Notice.ts
new file mode 100644
index 0000000000..c5aaf96754
--- /dev/null
+++ b/src/handlers/project/v0Notice.ts
@@ -0,0 +1,6 @@
+export const V0_PROJECT_MIGRATION_MESSAGE =
+ "This is an AgentCore CLI v0 project running in v1 compatibility mode. " +
+ "Use 'agentcore migrate' to migrate the project fully.";
+
+export const V0_PROJECT_TUI_BANNER =
+ "v0 project · CLI v1 compatibility mode · use agentcore migrate to migrate fully";
diff --git a/src/handlers/screen.tsx b/src/handlers/screen.tsx
index 33848c5cd0..1d483ba1e5 100644
--- a/src/handlers/screen.tsx
+++ b/src/handlers/screen.tsx
@@ -7,6 +7,8 @@ import { useTuiUpdate } from "../components/TuiUpdateContext";
import { glyphs } from "../components/ui/_core.js";
import type { ScreenProps } from "./types";
import { LoadingFrame, useProjectDetected } from "./project/ProjectGate";
+import { V0ProjectDetectedKey } from "./project/context";
+import { V0_PROJECT_TUI_BANNER } from "./project/v0Notice";
const NO_PROJECT_HINTS = { create: `${glyphs.leftArrow} start here` };
const ROOT_CLI_SECTION_COMMANDS = ["update"];
@@ -36,6 +38,7 @@ export interface RootScreenProps extends ScreenProps {
export function RootScreen({ inProject, ...props }: RootScreenProps) {
const projectDetected = useProjectDetected(props.core, inProject);
const updateState = useTuiUpdate();
+ const v0Project = props.ctx.value(V0ProjectDetectedKey) === true;
if (projectDetected.data === undefined) {
const command = resolveCommand(props.ctx.require(CommandKey), ["agentcore"]);
return (
@@ -60,7 +63,10 @@ export function RootScreen({ inProject, ...props }: RootScreenProps) {
? { update: `· update to install ${updateState.latestVersion}` }
: undefined
}
- alert={projectDetected.data ? undefined : NO_PROJECT_ALERT}
+ alert={
+ v0Project ? V0_PROJECT_TUI_BANNER : projectDetected.data ? undefined : NO_PROJECT_ALERT
+ }
+ alertVariant={v0Project ? "warning" : "info"}
hiddenOptions={projectDetected.data ? PROJECT_HIDDEN_OPTIONS : NO_PROJECT_HIDDEN_OPTIONS}
/>
);
diff --git a/src/middleware/index.tsx b/src/middleware/index.tsx
index b6ea294aa3..fff26a14d6 100644
--- a/src/middleware/index.tsx
+++ b/src/middleware/index.tsx
@@ -6,3 +6,4 @@ export { withGlobalConfigAccessor } from "./withGlobalConfigAccessor";
export { withPlatform } from "./withPlatform";
export { withProject } from "./withProject";
export { withV0ProjectGuard } from "./withV0ProjectGuard";
+export { withV0ProjectNotice } from "./withV0ProjectNotice";
diff --git a/src/middleware/withV0ProjectGuard.ts b/src/middleware/withV0ProjectGuard.ts
index bf40035b9c..c91b7d1b1e 100644
--- a/src/middleware/withV0ProjectGuard.ts
+++ b/src/middleware/withV0ProjectGuard.ts
@@ -5,7 +5,7 @@ import type { Middleware } from "../router";
export const V0_PROJECT_ADD_REMOVE_MESSAGE =
"This project uses the v0 format (agentcore.json version 1). " +
"The v1 CLI cannot add or remove resources in it. " +
- "Migrate the project to v1 in order to add/remove its resources.";
+ "Use 'agentcore migrate' to migrate the project fully before adding or removing resources.";
/** Blocks project add/remove before withProject attempts to parse the v1 schema. */
export function withV0ProjectGuard(): Middleware {
diff --git a/src/middleware/withV0ProjectNotice.test.ts b/src/middleware/withV0ProjectNotice.test.ts
new file mode 100644
index 0000000000..d3b1b7f163
--- /dev/null
+++ b/src/middleware/withV0ProjectNotice.test.ts
@@ -0,0 +1,61 @@
+import { afterEach, describe, expect, test } from "bun:test";
+import { mkdir, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { createHandler, Router, type Context } from "../router";
+import { inTempDirectory, testIO } from "../testing";
+import { V0ProjectNoticeStateKey } from "../handlers/project/context";
+import { V0_PROJECT_MIGRATION_MESSAGE } from "../handlers/project/v0Notice";
+import { withV0ProjectNotice } from "./withV0ProjectNotice";
+
+const cleanups: Array<() => Promise> = [];
+afterEach(() => Promise.all(cleanups.splice(0).map((cleanup) => cleanup())));
+
+async function writeProject(version: number): Promise {
+ const { path, cleanup } = await inTempDirectory("agentcore-v0-notice-");
+ cleanups.push(cleanup);
+ await mkdir(join(path, "agentcore"));
+ await writeFile(
+ join(path, "agentcore", "agentcore.json"),
+ JSON.stringify({ name: "Project", version }),
+ );
+}
+
+function app(
+ io: ReturnType,
+ handle: (ctx: Context) => Promise = async () => {},
+) {
+ return new Router("app", "test")
+ .use(withV0ProjectNotice(io.io))
+ .handler(createHandler({ name: "check", description: "noop", handle }));
+}
+
+describe("v0 project notice", () => {
+ test("prints migration guidance after a direct command", async () => {
+ await writeProject(1);
+ const io = testIO();
+
+ await app(io).route(["node", "app", "check"]);
+
+ expect(io.stderr()).toBe(V0_PROJECT_MIGRATION_MESSAGE);
+ });
+
+ test("does not print for a v1 project", async () => {
+ await writeProject(2);
+ const io = testIO();
+
+ await app(io).route(["node", "app", "check"]);
+
+ expect(io.stderr()).toBe("");
+ });
+
+ test("lets the TUI own the reminder when one was entered", async () => {
+ await writeProject(1);
+ const io = testIO();
+
+ await app(io, async (ctx) => {
+ ctx.require(V0ProjectNoticeStateKey).enteredTui = true;
+ }).route(["node", "app", "check"]);
+
+ expect(io.stderr()).toBe("");
+ });
+});
diff --git a/src/middleware/withV0ProjectNotice.ts b/src/middleware/withV0ProjectNotice.ts
new file mode 100644
index 0000000000..a0dc3a87b3
--- /dev/null
+++ b/src/middleware/withV0ProjectNotice.ts
@@ -0,0 +1,40 @@
+import { enclosingProjectRoot, isV0Project } from "../core/project/fsUtils";
+import type { AppIO } from "../io";
+import type { Middleware } from "../router";
+import { V0ProjectDetectedKey, V0ProjectNoticeStateKey } from "../handlers/project/context";
+import { V0_PROJECT_MIGRATION_MESSAGE } from "../handlers/project/v0Notice";
+
+/**
+ * Marks commands launched from a v0 project and reminds direct-command users
+ * to migrate. TUI sessions render the reminder themselves after restoring the
+ * normal terminal buffer.
+ */
+export function withV0ProjectNotice(io: AppIO): Middleware {
+ return (h) => ({
+ name: () => h.name(),
+ description: () => h.description(),
+ flags: () => h.flags(),
+ arguments: () => h.arguments(),
+ doesSupportTui: () => h.doesSupportTui(),
+ children: () => h.children(),
+ handle: async (ctx, flags, args) => {
+ const rootPath = enclosingProjectRoot(process.cwd());
+ if (!rootPath || !(await isV0Project(rootPath))) {
+ await h.handle(ctx, flags, args);
+ return;
+ }
+
+ const state = { enteredTui: false };
+ const nextCtx = ctx
+ .withValue(V0ProjectDetectedKey, true)
+ .withValue(V0ProjectNoticeStateKey, state);
+ try {
+ await h.handle(nextCtx, flags, args);
+ } finally {
+ if (!state.enteredTui && h.name() !== "migrate") {
+ io.stderr.write(`${V0_PROJECT_MIGRATION_MESSAGE}\n`);
+ }
+ }
+ },
+ });
+}
diff --git a/src/tui/index.tsx b/src/tui/index.tsx
index 254a80c22b..95cd342a96 100644
--- a/src/tui/index.tsx
+++ b/src/tui/index.tsx
@@ -14,7 +14,13 @@ import {
import type { AppIO } from "../io";
import type { Core } from "../handlers/types";
import { JsonKey } from "../handlers/keys";
-import { ProjectDetectedKey } from "../handlers/project/context";
+import {
+ ProjectDetectedKey,
+ V0ProjectDetectedKey,
+ V0ProjectNoticeStateKey,
+} from "../handlers/project/context";
+import { V0_PROJECT_MIGRATION_MESSAGE } from "../handlers/project/v0Notice";
+import { enclosingProjectRoot, isV0Project } from "../core/project/fsUtils";
import { ExitCode, InvalidEnvironmentError } from "../errors";
import { TuiExitMessageKey } from "./exitMessage";
import { TuiExitErrorKey } from "./exitError";
@@ -69,8 +75,15 @@ export async function renderTuiAt(
let exitError: Error | undefined;
let handoff: string[] | undefined;
let tuiContext = ctx;
+ let v0Project = ctx.value(V0ProjectDetectedKey);
+ if (v0Project === undefined) {
+ const rootPath = enclosingProjectRoot(process.cwd());
+ v0Project = rootPath !== undefined && (await isV0Project(rootPath));
+ }
+ tuiContext = tuiContext.withValue(V0ProjectDetectedKey, v0Project);
if (isRootMenuPath(path)) {
const projectDetected =
+ v0Project ||
ctx.value(ProjectKey) !== undefined ||
(await core.projectManager.resolve({
filePath: process.cwd(),
@@ -92,6 +105,9 @@ export async function renderTuiAt(
// takes over the screen and the prior scrollback is restored on exit (like Vim).
const queryClient = new QueryClient();
+ const noticeState = ctx.value(V0ProjectNoticeStateKey);
+ if (noticeState) noticeState.enteredTui = true;
+
const resizeGate = createResizeGate(io.stdout);
const { waitUntilExit } = render(
,
@@ -108,6 +124,9 @@ export async function renderTuiAt(
await waitUntilExit();
} finally {
resizeGate.dispose();
+ if (v0Project) {
+ io.stderr.write(`${V0_PROJECT_MIGRATION_MESSAGE}\n`);
+ }
}
// Ink handles Ctrl+C before invoking screen input handlers.
if (exitError !== undefined) throw exitError;
diff --git a/src/tui/tui.test.tsx b/src/tui/tui.test.tsx
index 7cd2e9b4ad..2ee977fac6 100644
--- a/src/tui/tui.test.tsx
+++ b/src/tui/tui.test.tsx
@@ -1,4 +1,6 @@
import { test, expect, describe } from "bun:test";
+import { mkdir, writeFile } from "node:fs/promises";
+import { join } from "node:path";
import { Command } from "commander";
import { createRootHandler } from "../handlers";
import { ExitCode, InputValidationError, InvalidEnvironmentError } from "../errors";
@@ -14,7 +16,9 @@ import {
ttyTestIO,
tick,
waitFor,
+ inTempDirectory,
} from "../testing";
+import { V0_PROJECT_MIGRATION_MESSAGE, V0_PROJECT_TUI_BANNER } from "../handlers/project/v0Notice";
describe("renderJson", () => {
test("pretty-prints a value as indented JSON to the given writer", () => {
@@ -235,6 +239,31 @@ describe("TUI launch", () => {
const CREATE_ROW = "create a new project";
const BANNER = "No project detected - create a new project to get started";
+ test("a v0 project shows compatibility mode and repeats migration guidance after exit", async () => {
+ const { path, cleanup } = await inTempDirectory("agentcore-v0-tui-");
+ try {
+ await mkdir(join(path, "agentcore"));
+ await writeFile(join(path, "agentcore", "agentcore.json"), '{"name":"Legacy","version":1}\n');
+ const core = new TestCoreClient();
+ const { streams, stdin } = ttyTestIO();
+ const root = createRootHandler(core, {
+ io: streams.io,
+ logger: createSilentLogger(),
+ globalConfigAccessor: new TestGlobalConfigAccessor(),
+ });
+
+ const routePromise = root.route(["node", "agentcore"]);
+ await waitFor(() => streams.stdout().includes(V0_PROJECT_TUI_BANNER));
+ stdin.write(String.fromCharCode(3));
+ await routePromise;
+
+ expect(streams.stderr()).toContain(V0_PROJECT_MIGRATION_MESSAGE);
+ expect(streams.stderr().match(new RegExp(V0_PROJECT_MIGRATION_MESSAGE, "g"))).toHaveLength(1);
+ } finally {
+ await cleanup();
+ }
+ });
+
test("an unrelated TUI command does not resolve the current project", async () => {
const core = new TestCoreClient();
let resolveCalls = 0;