From d0d2ea8748fa4c93c0324bba5c12fef4b1431713 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Mon, 20 Jul 2026 15:24:58 +0000 Subject: [PATCH 1/6] feat(project): scaffold project manager interface --- src/core/core.test.ts | 85 +++++++++++++++------------ src/core/index.tsx | 32 ++++++++-- src/core/project/index.tsx | 1 + src/core/project/manager.tsx | 20 +++++++ src/handlers/harness/harness.test.tsx | 7 ++- src/handlers/index.tsx | 2 +- src/handlers/project/create/index.ts | 13 ++-- src/handlers/project/index.ts | 9 ++- src/handlers/project/types.ts | 28 +++++++++ src/handlers/types.tsx | 2 + src/index.ts | 7 ++- src/middleware/withProject.test.ts | 23 ++++++++ src/middleware/withProject.tsx | 30 ++++++++++ src/router/index.tsx | 1 + src/router/router.tsx | 3 + src/testing/TestCoreClient.tsx | 16 ++++- 16 files changed, 227 insertions(+), 52 deletions(-) create mode 100644 src/core/project/index.tsx create mode 100644 src/core/project/manager.tsx create mode 100644 src/handlers/project/types.ts create mode 100644 src/middleware/withProject.test.ts create mode 100644 src/middleware/withProject.tsx diff --git a/src/core/core.test.ts b/src/core/core.test.ts index 8a3a2bba8..2c773710a 100644 --- a/src/core/core.test.ts +++ b/src/core/core.test.ts @@ -9,6 +9,7 @@ import { import { CoreClient } from "./index"; import type { ClientConfig } from "./types"; import { toClientConfig } from "./utils"; +import { createSilentLogger } from "../testing"; // A minimal stand-in for the SDK clients; CoreClient only stores and returns // them, so an opaque tagged object is enough to assert identity/caching. @@ -24,14 +25,15 @@ function fakeIam(config: ClientConfig): IAMClient { test("control() constructs a client once per config and caches it", () => { let built = 0; - const core = new CoreClient( - (config) => { + const core = new CoreClient({ + createControlClient: (config) => { built++; return fakeControl(config); }, - fakeData, - fakeIam, - ); + createDataClient: fakeData, + createIamClient: fakeIam, + logger: createSilentLogger(), + }); const a = core.control({ region: "us-east-1" }); const b = core.control({ region: "us-east-1" }); @@ -42,14 +44,15 @@ test("control() constructs a client once per config and caches it", () => { test("control() builds a distinct client per distinct config", () => { let built = 0; - const core = new CoreClient( - (config) => { + const core = new CoreClient({ + createControlClient: (config) => { built++; return fakeControl(config); }, - fakeData, - fakeIam, - ); + createDataClient: fakeData, + createIamClient: fakeIam, + logger: createSilentLogger(), + }); core.control({ region: "us-east-1" }); core.control({ region: "us-west-2" }); @@ -61,17 +64,18 @@ test("control() builds a distinct client per distinct config", () => { test("data() caches independently of control()", () => { let controlBuilt = 0; let dataBuilt = 0; - const core = new CoreClient( - (config) => { + const core = new CoreClient({ + createControlClient: (config) => { controlBuilt++; return fakeControl(config); }, - (config) => { + createDataClient: (config) => { dataBuilt++; return fakeData(config); }, - fakeIam, - ); + createIamClient: fakeIam, + logger: createSilentLogger(), + }); core.control({ region: "us-east-1" }); const d1 = core.data({ region: "us-east-1" }); @@ -83,7 +87,12 @@ test("data() caches independently of control()", () => { }); test("exposes a harness sub-client", () => { - const core = new CoreClient(fakeControl, fakeData, fakeIam); + const core = new CoreClient({ + createControlClient: fakeControl, + createDataClient: fakeData, + createIamClient: fakeIam, + logger: createSilentLogger(), + }); expect(core.harness).toBeDefined(); }); @@ -93,9 +102,9 @@ test("invokeHarness sends an InvokeHarnessCommand on the data client with the ab const sent: { command: unknown; options: unknown }[] = []; const configs: ClientConfig[] = []; const response = { stream: undefined }; - const core = new CoreClient( - fakeControl, - (config) => { + const core = new CoreClient({ + createControlClient: fakeControl, + createDataClient: (config) => { configs.push(config); return { config, @@ -106,8 +115,9 @@ test("invokeHarness sends an InvokeHarnessCommand on the data client with the ab }, } as unknown as BedrockAgentCoreClient; }, - fakeIam, - ); + createIamClient: fakeIam, + logger: createSilentLogger(), + }); const request = { harnessArn: "arn:aws:bedrock-agentcore:us-east-1:123:harness/h-1", @@ -138,16 +148,17 @@ test("invokeHarness stream iteration rejects promptly when aborted mid-stream", await new Promise(() => {}); }, }; - const core = new CoreClient( - fakeControl, - (config) => + const core = new CoreClient({ + createControlClient: fakeControl, + createDataClient: (config) => ({ config, kind: "data", send: async () => ({ stream: hangingStream }), }) as unknown as BedrockAgentCoreClient, - fakeIam, - ); + createIamClient: fakeIam, + logger: createSilentLogger(), + }); const controller = new AbortController(); const response = await core.harness.invokeHarness( @@ -166,9 +177,9 @@ test("invokeHarness stream iteration rejects promptly when aborted mid-stream", test("invokeAgentRuntimeCommand sends the command on the data client with the abort signal", async () => { const sent: { command: unknown; options: unknown }[] = []; - const core = new CoreClient( - fakeControl, - (config) => + const core = new CoreClient({ + createControlClient: fakeControl, + createDataClient: (config) => ({ config, kind: "data", @@ -177,8 +188,9 @@ test("invokeAgentRuntimeCommand sends the command on the data client with the ab return { statusCode: 200, stream: undefined }; }, }) as unknown as BedrockAgentCoreClient, - fakeIam, - ); + createIamClient: fakeIam, + logger: createSilentLogger(), + }); const request = { agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123:harness/h-1", @@ -195,16 +207,17 @@ test("invokeAgentRuntimeCommand sends the command on the data client with the ab test("invokeHarness returns the stream untouched when no abort signal is given", async () => { const stream = (async function* () {})(); - const core = new CoreClient( - fakeControl, - (config) => + const core = new CoreClient({ + createControlClient: fakeControl, + createDataClient: (config) => ({ config, kind: "data", send: async () => ({ stream }), }) as unknown as BedrockAgentCoreClient, - fakeIam, - ); + createIamClient: fakeIam, + logger: createSilentLogger(), + }); const response = await core.harness.invokeHarness( { harnessArn: "arn", runtimeSessionId: "s".repeat(40), messages: [] }, diff --git a/src/core/index.tsx b/src/core/index.tsx index b28fa5323..d1f4b3683 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -10,6 +10,9 @@ import type { CreateDataClient, CreateIamClient, } from "./types"; +import type { Logger } from "../logging"; +import type { ProjectManager } from "../handlers/project/types"; +import { createProjectManager } from "./project"; export type { AwsClients, @@ -19,6 +22,13 @@ export type { CreateIamClient, } from "./types"; +interface CoreClientConfig { + createControlClient: CreateControlClient; + createDataClient: CreateDataClient; + createIamClient: CreateIamClient; + logger: Logger; +} + // CoreClient is the single entry point to the Bedrock AgentCore APIs. It owns the // underlying SDK clients (one per config, created on demand from the injected // factories) and exposes feature-scoped sub-clients such as `harness`, keeping the @@ -28,15 +38,27 @@ export class CoreClient implements AwsClients { private dataClients = new Map(); private iamClients = new Map(); + private readonly createControlClient: CreateControlClient; + private readonly createDataClient: CreateDataClient; + private readonly createIamClient: CreateIamClient; + private logger: Logger; + // Feature-scoped sub-clients. Access as e.g. `coreClient.harness.getHarness(...)`. readonly harness: HarnessClient = new HarnessClient(this); readonly runtime: RuntimeClient = new RuntimeClient(this); - constructor( - private readonly createControlClient: CreateControlClient, - private readonly createDataClient: CreateDataClient, - private readonly createIamClient: CreateIamClient, - ) {} + readonly projectManager: ProjectManager; + + constructor(config: CoreClientConfig) { + this.createControlClient = config.createControlClient; + this.createDataClient = config.createDataClient; + this.createIamClient = config.createIamClient; + this.logger = config.logger; + + this.projectManager = createProjectManager({ + logger: this.logger.child({ module: "projectManager" }), + }); + } // control returns the control-plane client for `config`, creating and caching it // on first use. diff --git a/src/core/project/index.tsx b/src/core/project/index.tsx new file mode 100644 index 000000000..b141b90d2 --- /dev/null +++ b/src/core/project/index.tsx @@ -0,0 +1 @@ +export { createProjectManager } from "./manager"; diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx new file mode 100644 index 000000000..3c4ec8e03 --- /dev/null +++ b/src/core/project/manager.tsx @@ -0,0 +1,20 @@ +import type { ProjectManager } from "../../handlers/project/types"; +import type { Logger } from "../../logging"; + +interface CreateProjectManagerConfig { + logger: Logger; +} + +/** + * Creates a {@link ProjectManager} that relies on the local file system to manage access to projects. + */ +export function createProjectManager(_config: CreateProjectManagerConfig): ProjectManager { + return { + find: (_input) => { + throw new Error(`ProjectManager.find is not implemented yet`); + }, + create: (_input) => { + throw new Error(`ProjectManager.create is not implemented yet`); + }, + }; +} diff --git a/src/handlers/harness/harness.test.tsx b/src/handlers/harness/harness.test.tsx index 59a8a11a0..7f705d84e 100644 --- a/src/handlers/harness/harness.test.tsx +++ b/src/handlers/harness/harness.test.tsx @@ -30,7 +30,12 @@ const REGION = "us-west-2"; // and returns whatever the command wrote to stdout. async function run(args: string[]): Promise { const { createControlClient, createDataClient, createIamClient } = fixtureFactories(FIXTURES); - const core = new CoreClient(createControlClient, createDataClient, createIamClient); + const core = new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + logger: createSilentLogger(), + }); const io = testIO(); const root = createRootHandler(core, { io: io.io, logger: createSilentLogger() }); await root.route(["node", "agentcore", ...args, "--region", REGION]); diff --git a/src/handlers/index.tsx b/src/handlers/index.tsx index 3707fc373..a5d5dce94 100644 --- a/src/handlers/index.tsx +++ b/src/handlers/index.tsx @@ -36,7 +36,7 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router root.handler(createHarnessHandler(core, io)); root.handler(createRuntimeHandler(core, io)); root.handler(createConfigHandler(io)); - root.handler(createProjectHandler()); + root.handler(createProjectHandler({ projectManager: core.projectManager })); // Invoking with no subcommand launches the interactive TUI. root.default(renderTui(core, io)); diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 63e8c375c..4cdefe824 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -1,9 +1,12 @@ import z from "zod"; import { createHandler, flag } from "../../../router"; +import { PROJECT_TEMPLATES, type ProjectManager } from "../types"; -export const PROJECT_TEMPLATES = ["placeholder"] as const; +interface CreateProjectHandlerConfig { + projectManager: ProjectManager; +} -export const createCreateProjectHandler = () => +export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) => createHandler({ name: "create", description: "create a new AgentCore project", @@ -14,7 +17,9 @@ export const createCreateProjectHandler = () => z.enum(PROJECT_TEMPLATES).default("placeholder"), ), ], - handle: async () => { - throw new Error("`agentcore project create` is not implemented yet"); + handle: async (_ctx, flags) => { + await config.projectManager.create({ + template: flags.template, + }); }, }); diff --git a/src/handlers/project/index.ts b/src/handlers/project/index.ts index fdeb5b343..928ad1698 100644 --- a/src/handlers/project/index.ts +++ b/src/handlers/project/index.ts @@ -1,10 +1,15 @@ import { Router } from "../../router"; import { createCreateProjectHandler } from "./create"; +import type { ProjectManager } from "./types"; -export function createProjectHandler(): Router { +interface ProjectHandlerConfig { + projectManager: ProjectManager; +} + +export function createProjectHandler(config: ProjectHandlerConfig): Router { const project = new Router("project", "manage an AgentCore project"); - project.handler(createCreateProjectHandler()); + project.handler(createCreateProjectHandler({ projectManager: config.projectManager })); return project; } diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts new file mode 100644 index 000000000..09b0df7c4 --- /dev/null +++ b/src/handlers/project/types.ts @@ -0,0 +1,28 @@ +/** Available project templates for scaffolding new AgentCore projects. */ +export const PROJECT_TEMPLATES = ["placeholder"] as const; + +export interface CreateProjectInput { + /** The project template to scaffold from. */ + template: (typeof PROJECT_TEMPLATES)[number]; +} + +export interface FindProjectInput { + /** A path to search from when locating the project root. */ + filePath: string; +} + +/** + * Exposes the ability to configure, develop, and deploy a resolved AgentCore project. + */ +export interface Project {} + +/** + * Manages project lifecycle: creation (scaffolding) and discovery + */ +export interface ProjectManager { + /** Scaffold a new AgentCore project from the given template. */ + create(input: CreateProjectInput): Promise; + + /** Locate an existing AgentCore project. Returns undefined if no project can be found. */ + find(input: FindProjectInput): Promise; +} diff --git a/src/handlers/types.tsx b/src/handlers/types.tsx index 3e95a2b14..58eecb093 100644 --- a/src/handlers/types.tsx +++ b/src/handlers/types.tsx @@ -1,10 +1,12 @@ import type { CoreHarnessClient } from "./harness/types.tsx"; import type { CoreRuntimeClient } from "./runtime/types.tsx"; import type { Context } from "../router"; +import type { ProjectManager } from "./project/types.ts"; export interface Core { harness: CoreHarnessClient; runtime: CoreRuntimeClient; + projectManager: ProjectManager; } // AppIO is the set of standard streams the app reads from and writes to. It is diff --git a/src/index.ts b/src/index.ts index bb41c6042..a74c0161a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,12 @@ process.exit( // Wrap the SDK clients in the CoreClient the handlers consume. Passing // factories (rather than instances) lets CoreClient build one client per // region on demand. - const coreClient = new CoreClient(createControlClient, createDataClient, createIamClient); + const coreClient = new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + logger: rootLogger.child({ module: "core" }), + }); // Pass it to the root handler, along with the process's standard streams as // the app's io. CoreClient exposes feature sub-clients (e.g. `.harness`), so diff --git a/src/middleware/withProject.test.ts b/src/middleware/withProject.test.ts new file mode 100644 index 000000000..d01a9eb02 --- /dev/null +++ b/src/middleware/withProject.test.ts @@ -0,0 +1,23 @@ +import { test, expect, describe } from "bun:test"; +import { Router, createHandler } from "../router"; +import { createProjectManager } from "../core/project"; +import { createSilentLogger } from "../testing"; +import { withProject } from "./withProject"; + +describe("withProject", () => { + test("throws not implemented", async () => { + const projectManager = createProjectManager({ logger: createSilentLogger() }); + + const app = new Router("app", "test"); + app.use(withProject({ projectManager, cwd: "/some/path" })); + app.handler( + createHandler({ + name: "check", + description: "noop", + handle: async () => {}, + }), + ); + + await expect(app.route(["node", "app", "check"])).rejects.toThrow(/not implemented/); + }); +}); diff --git a/src/middleware/withProject.tsx b/src/middleware/withProject.tsx new file mode 100644 index 000000000..7bceccc8d --- /dev/null +++ b/src/middleware/withProject.tsx @@ -0,0 +1,30 @@ +import type { Project, ProjectManager } from "../handlers/project/types"; +import { ProjectKey, type Middleware } from "../router"; + +interface WithProjectConfig { + projectManager: ProjectManager; + cwd: string; +} + +/** + * Middleware that locates the AgentCore project from the configured working + * directory and pins it on the context under {@link ProjectKey}. + * Throws if no project can be found. + * + * @param config - Contains the {@link ProjectManager} and the `cwd` to search from. + */ +export function withProject(config: WithProjectConfig): Middleware { + return (h) => ({ + name: () => h.name(), + description: () => h.description(), + flags: () => h.flags(), + arguments: () => h.arguments(), + children: () => h.children(), + handle: async (ctx, flags, args) => { + const project = await config.projectManager.find({ filePath: config.cwd }); + // TODO: swap this for a typed error. + if (!project) throw new Error(`Unable to find project at path ${config.cwd}`); + await h.handle(ctx.withValue(ProjectKey, project), flags, args); + }, + }); +} diff --git a/src/router/index.tsx b/src/router/index.tsx index 98f581fa8..fb8c31a88 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -4,6 +4,7 @@ export { CommandKey, PathKey, LoggerKey, + ProjectKey, type DefaultHandle, type DefaultHandlerProvider, isDefaultHandlerProvider, diff --git a/src/router/router.tsx b/src/router/router.tsx index 5aec62670..eab6778b2 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -6,6 +6,7 @@ import { parseArguments, toCommanderArgument } from "./args"; import { Command } from "commander"; import type { Logger } from "../logging"; +import type { Project } from "../handlers/project/types"; // CommandKey exposes the Commander Command for the executing leaf via context. export const CommandKey: ContextKey = contextKey("commander.command"); @@ -14,6 +15,8 @@ export const PathKey: ContextKey = contextKey("path"); export const LoggerKey = contextKey("logger"); +export const ProjectKey = contextKey("project"); + // DefaultHandle runs when a group is selected without a subcommand (e.g. // `agentcore` or `agentcore harness`). It reads group-level/global flags from the // context; own flags/arguments are not supported, so it receives empty objects. diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index c12f17aad..8d9dda484 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -33,6 +33,10 @@ import type { Core } from "../handlers/types"; import type { CoreHarnessClient, CreateHarnessInput } from "../handlers/harness/types"; import type { CoreRuntimeClient } from "../handlers/runtime/types"; import type { CoreOptions } from "../core/types"; +import type { ProjectManager } from "../handlers/project/types"; +import type { Logger } from "../logging"; +import { createSilentLogger } from "./logging"; +import { createProjectManager } from "../core/project"; // TestCoreClient is a hand-controllable `Core` for tests. It implements the same // interface the real CoreClient satisfies, so it drops straight into @@ -101,10 +105,10 @@ async function* abortable(source: AsyncIterable, signal?: AbortSignal): As if (signal.aborted) reject(abortError()); else signal.addEventListener("abort", () => reject(abortError()), { once: true }); }); - aborted.catch(() => {}); + aborted.catch(() => { }); const iterator = source[Symbol.asyncIterator](); - for (;;) { + for (; ;) { const result = await Promise.race([iterator.next(), aborted]); if (result.done) return; yield result.value; @@ -466,9 +470,17 @@ class TestRuntimeClient implements CoreRuntimeClient { return DEFAULT_LIST_RUNTIME_ENDPOINTS_RESPONSE; } } +type TestCoreClientOptions = { + logger?: Logger; +} // TestCoreClient implements the Core contract with fully controllable sub-clients. export class TestCoreClient implements Core { readonly harness = new TestHarnessClient(); readonly runtime = new TestRuntimeClient(); + readonly projectManager: ProjectManager; + + constructor(options?: TestCoreClientOptions) { + this.projectManager = createProjectManager({ logger: options?.logger ?? createSilentLogger() }); + } } From e6ff78e095dce7318de2d78946b1e9506e809403 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 21 Jul 2026 17:07:02 +0000 Subject: [PATCH 2/6] refactor(project): swap to using types instead of interfaces --- src/core/index.tsx | 4 ++-- src/core/project/manager.tsx | 4 ++-- src/handlers/project/create/index.ts | 4 ++-- src/handlers/project/index.ts | 4 ++-- src/handlers/project/types.ts | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/index.tsx b/src/core/index.tsx index d1f4b3683..0bf14b0c6 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -22,12 +22,12 @@ export type { CreateIamClient, } from "./types"; -interface CoreClientConfig { +type CoreClientConfig = { createControlClient: CreateControlClient; createDataClient: CreateDataClient; createIamClient: CreateIamClient; logger: Logger; -} +}; // CoreClient is the single entry point to the Bedrock AgentCore APIs. It owns the // underlying SDK clients (one per config, created on demand from the injected diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 3c4ec8e03..3356bdf33 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -1,9 +1,9 @@ import type { ProjectManager } from "../../handlers/project/types"; import type { Logger } from "../../logging"; -interface CreateProjectManagerConfig { +type CreateProjectManagerConfig = { logger: Logger; -} +}; /** * Creates a {@link ProjectManager} that relies on the local file system to manage access to projects. diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 4cdefe824..b62174815 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -2,9 +2,9 @@ import z from "zod"; import { createHandler, flag } from "../../../router"; import { PROJECT_TEMPLATES, type ProjectManager } from "../types"; -interface CreateProjectHandlerConfig { +type CreateProjectHandlerConfig = { projectManager: ProjectManager; -} +}; export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) => createHandler({ diff --git a/src/handlers/project/index.ts b/src/handlers/project/index.ts index 928ad1698..ef1d03b1d 100644 --- a/src/handlers/project/index.ts +++ b/src/handlers/project/index.ts @@ -2,9 +2,9 @@ import { Router } from "../../router"; import { createCreateProjectHandler } from "./create"; import type { ProjectManager } from "./types"; -interface ProjectHandlerConfig { +type ProjectHandlerConfig = { projectManager: ProjectManager; -} +}; export function createProjectHandler(config: ProjectHandlerConfig): Router { const project = new Router("project", "manage an AgentCore project"); diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 09b0df7c4..0a7b1aa78 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -1,15 +1,15 @@ /** Available project templates for scaffolding new AgentCore projects. */ export const PROJECT_TEMPLATES = ["placeholder"] as const; -export interface CreateProjectInput { +export type CreateProjectInput = { /** The project template to scaffold from. */ template: (typeof PROJECT_TEMPLATES)[number]; -} +}; -export interface FindProjectInput { +export type FindProjectInput = { /** A path to search from when locating the project root. */ filePath: string; -} +}; /** * Exposes the ability to configure, develop, and deploy a resolved AgentCore project. From b69d8176d13bc45c7e0652429255ef9230e37895 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 21 Jul 2026 17:10:58 +0000 Subject: [PATCH 3/6] refactor(project): switch to enum object --- src/handlers/project/create/index.ts | 2 +- src/handlers/project/project.test.ts | 2 +- src/handlers/project/types.ts | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index b62174815..aef7c9788 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -14,7 +14,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = flag( "template", "project template to scaffold from", - z.enum(PROJECT_TEMPLATES).default("placeholder"), + z.enum(PROJECT_TEMPLATES).default(PROJECT_TEMPLATES.BAREBONES), ), ], handle: async (_ctx, flags) => { diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 6c2922920..301853b56 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -17,7 +17,7 @@ describe("project create", () => { }); test("accepts a known --template value", async () => { - await expect(run(["create", "--template", "placeholder"])).rejects.toThrow(/not implemented/); + await expect(run(["create", "--template", "barebones"])).rejects.toThrow(/not implemented/); }); test("rejects an unknown --template value", async () => { diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 0a7b1aa78..6bb8015ba 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -1,9 +1,13 @@ /** Available project templates for scaffolding new AgentCore projects. */ -export const PROJECT_TEMPLATES = ["placeholder"] as const; +export const PROJECT_TEMPLATES = { + BAREBONES: "barebones", +} as const; + +export type ProjectTemplate = (typeof PROJECT_TEMPLATES)[keyof typeof PROJECT_TEMPLATES]; export type CreateProjectInput = { /** The project template to scaffold from. */ - template: (typeof PROJECT_TEMPLATES)[number]; + template: ProjectTemplate; }; export type FindProjectInput = { From c68e6ca83c54fbd505f2b0d53b3f22840e07b089 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 21 Jul 2026 17:40:31 +0000 Subject: [PATCH 4/6] refactor(project): swap to class based implementation --- src/core/index.tsx | 4 ++-- src/core/project/index.tsx | 2 +- src/core/project/manager.tsx | 30 ++++++++++++++++----------- src/handlers/runtime/runtime.test.tsx | 2 +- src/middleware/withProject.test.ts | 4 ++-- src/testing/TestCoreClient.tsx | 4 ++-- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/core/index.tsx b/src/core/index.tsx index 0bf14b0c6..afddd3e3b 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -12,7 +12,7 @@ import type { } from "./types"; import type { Logger } from "../logging"; import type { ProjectManager } from "../handlers/project/types"; -import { createProjectManager } from "./project"; +import { FsProjectManager } from "./project"; export type { AwsClients, @@ -55,7 +55,7 @@ export class CoreClient implements AwsClients { this.createIamClient = config.createIamClient; this.logger = config.logger; - this.projectManager = createProjectManager({ + this.projectManager = new FsProjectManager({ logger: this.logger.child({ module: "projectManager" }), }); } diff --git a/src/core/project/index.tsx b/src/core/project/index.tsx index b141b90d2..dfc769f53 100644 --- a/src/core/project/index.tsx +++ b/src/core/project/index.tsx @@ -1 +1 @@ -export { createProjectManager } from "./manager"; +export { FsProjectManager } from "./manager"; diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 3356bdf33..c8c6f4d89 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -1,20 +1,26 @@ -import type { ProjectManager } from "../../handlers/project/types"; +import type { + CreateProjectInput, + FindProjectInput, + Project, + ProjectManager, +} from "../../handlers/project/types"; import type { Logger } from "../../logging"; -type CreateProjectManagerConfig = { +type ProjectManagerConfig = { logger: Logger; }; /** - * Creates a {@link ProjectManager} that relies on the local file system to manage access to projects. + * An implementation of {@link ProjectManager} that relies on the local file system to manage access to projects. */ -export function createProjectManager(_config: CreateProjectManagerConfig): ProjectManager { - return { - find: (_input) => { - throw new Error(`ProjectManager.find is not implemented yet`); - }, - create: (_input) => { - throw new Error(`ProjectManager.create is not implemented yet`); - }, - }; +export class FsProjectManager implements ProjectManager { + constructor(_config: ProjectManagerConfig) {} + + public find(_input: FindProjectInput): Promise { + throw new Error(`ProjectManager.find is not implemented yet`); + } + + public create(_input: CreateProjectInput): Promise { + throw new Error(`ProjectManager.create is not implemented yet`); + } } diff --git a/src/handlers/runtime/runtime.test.tsx b/src/handlers/runtime/runtime.test.tsx index 5685d8619..5f531886f 100644 --- a/src/handlers/runtime/runtime.test.tsx +++ b/src/handlers/runtime/runtime.test.tsx @@ -16,7 +16,7 @@ const MISSING_RUNTIME_ID = "missing_runtime-0000000000"; function createFixtureCore(): CoreClient { const { createControlClient, createDataClient, createIamClient } = fixtureFactories(FIXTURES); - return new CoreClient(createControlClient, createDataClient, createIamClient); + return new CoreClient({ createControlClient, createDataClient, createIamClient, logger: createSilentLogger() }); } async function run(args: string[]): Promise { diff --git a/src/middleware/withProject.test.ts b/src/middleware/withProject.test.ts index d01a9eb02..852e75bf3 100644 --- a/src/middleware/withProject.test.ts +++ b/src/middleware/withProject.test.ts @@ -1,12 +1,12 @@ import { test, expect, describe } from "bun:test"; import { Router, createHandler } from "../router"; -import { createProjectManager } from "../core/project"; import { createSilentLogger } from "../testing"; import { withProject } from "./withProject"; +import { FsProjectManager } from "../core/project"; describe("withProject", () => { test("throws not implemented", async () => { - const projectManager = createProjectManager({ logger: createSilentLogger() }); + const projectManager = new FsProjectManager({ logger: createSilentLogger() }); const app = new Router("app", "test"); app.use(withProject({ projectManager, cwd: "/some/path" })); diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 8d9dda484..261307cf7 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -36,7 +36,7 @@ import type { CoreOptions } from "../core/types"; import type { ProjectManager } from "../handlers/project/types"; import type { Logger } from "../logging"; import { createSilentLogger } from "./logging"; -import { createProjectManager } from "../core/project"; +import { FsProjectManager } from "../core/project"; // TestCoreClient is a hand-controllable `Core` for tests. It implements the same // interface the real CoreClient satisfies, so it drops straight into @@ -481,6 +481,6 @@ export class TestCoreClient implements Core { readonly projectManager: ProjectManager; constructor(options?: TestCoreClientOptions) { - this.projectManager = createProjectManager({ logger: options?.logger ?? createSilentLogger() }); + this.projectManager = new FsProjectManager({ logger: options?.logger ?? createSilentLogger() }); } } From 69485f123aefb82ac73e59ef2d1c275d89c5663b Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 21 Jul 2026 20:15:02 +0000 Subject: [PATCH 5/6] fix: address formatting issues from rebase --- src/handlers/runtime/runtime.test.tsx | 7 ++++++- src/testing/TestCoreClient.tsx | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/handlers/runtime/runtime.test.tsx b/src/handlers/runtime/runtime.test.tsx index 5f531886f..501aed620 100644 --- a/src/handlers/runtime/runtime.test.tsx +++ b/src/handlers/runtime/runtime.test.tsx @@ -16,7 +16,12 @@ const MISSING_RUNTIME_ID = "missing_runtime-0000000000"; function createFixtureCore(): CoreClient { const { createControlClient, createDataClient, createIamClient } = fixtureFactories(FIXTURES); - return new CoreClient({ createControlClient, createDataClient, createIamClient, logger: createSilentLogger() }); + return new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + logger: createSilentLogger(), + }); } async function run(args: string[]): Promise { diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 261307cf7..f1559bda7 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -105,10 +105,10 @@ async function* abortable(source: AsyncIterable, signal?: AbortSignal): As if (signal.aborted) reject(abortError()); else signal.addEventListener("abort", () => reject(abortError()), { once: true }); }); - aborted.catch(() => { }); + aborted.catch(() => {}); const iterator = source[Symbol.asyncIterator](); - for (; ;) { + for (;;) { const result = await Promise.race([iterator.next(), aborted]); if (result.done) return; yield result.value; @@ -472,7 +472,7 @@ class TestRuntimeClient implements CoreRuntimeClient { } type TestCoreClientOptions = { logger?: Logger; -} +}; // TestCoreClient implements the Core contract with fully controllable sub-clients. export class TestCoreClient implements Core { From bfaf476b0d38e4ef9a27d14e0cee8ef5256add27 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 21 Jul 2026 21:11:04 +0000 Subject: [PATCH 6/6] refactor(project): make project concrete --- src/core/project/manager.tsx | 8 ++++---- src/handlers/project/types.ts | 13 ++++++------- src/middleware/withProject.tsx | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index c8c6f4d89..5e91ff693 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -1,6 +1,6 @@ import type { CreateProjectInput, - FindProjectInput, + ResolveProjectInput, Project, ProjectManager, } from "../../handlers/project/types"; @@ -11,13 +11,13 @@ type ProjectManagerConfig = { }; /** - * An implementation of {@link ProjectManager} that relies on the local file system to manage access to projects. + * An implementation of {@link ProjectManager} that relies on the local file system to manage projects. */ export class FsProjectManager implements ProjectManager { constructor(_config: ProjectManagerConfig) {} - public find(_input: FindProjectInput): Promise { - throw new Error(`ProjectManager.find is not implemented yet`); + public resolve(_input: ResolveProjectInput): Promise { + throw new Error(`ProjectManager.resolve is not implemented yet`); } public create(_input: CreateProjectInput): Promise { diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 6bb8015ba..1215cfd51 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -10,23 +10,22 @@ export type CreateProjectInput = { template: ProjectTemplate; }; -export type FindProjectInput = { +export type ResolveProjectInput = { /** A path to search from when locating the project root. */ filePath: string; }; -/** - * Exposes the ability to configure, develop, and deploy a resolved AgentCore project. - */ -export interface Project {} +export type Project = { + name: string; +}; /** - * Manages project lifecycle: creation (scaffolding) and discovery + * The primary interface for interacting with projects */ export interface ProjectManager { /** Scaffold a new AgentCore project from the given template. */ create(input: CreateProjectInput): Promise; /** Locate an existing AgentCore project. Returns undefined if no project can be found. */ - find(input: FindProjectInput): Promise; + resolve(input: ResolveProjectInput): Promise; } diff --git a/src/middleware/withProject.tsx b/src/middleware/withProject.tsx index 7bceccc8d..288d227f7 100644 --- a/src/middleware/withProject.tsx +++ b/src/middleware/withProject.tsx @@ -21,7 +21,7 @@ export function withProject(config: WithProjectConfig): Middleware { arguments: () => h.arguments(), children: () => h.children(), handle: async (ctx, flags, args) => { - const project = await config.projectManager.find({ filePath: config.cwd }); + const project = await config.projectManager.resolve({ filePath: config.cwd }); // TODO: swap this for a typed error. if (!project) throw new Error(`Unable to find project at path ${config.cwd}`); await h.handle(ctx.withValue(ProjectKey, project), flags, args);