Skip to content

Commit d1a704f

Browse files
committed
refactor(project): align scaffolding with codebase conventions
Address review feedback: - AssetSource implementations are now classes (EmbeddedAssetSource, FsAssetSource) matching how interfaces are implemented elsewhere - TemplateSpec, Template, DirNode, and FileNode are types since they model concrete data, not extendable behavior - ProjectFileExistsError extends AgentCoreCLIError with a user error source so it participates in error classification and exit codes - atomicWrite moved from src/fs into the shared src/io module
1 parent 675cc66 commit d1a704f

8 files changed

Lines changed: 53 additions & 42 deletions

File tree

src/core/project/source.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { describe, expect, test } from "bun:test";
2-
import { embeddedSource } from "./source";
2+
import { EmbeddedAssetSource } from "./source";
33

4-
describe("embeddedSource", () => {
4+
describe("EmbeddedAssetSource", () => {
55
test("throws when the asset is not embedded", () => {
6-
expect(embeddedSource.read("cdk/package.json")).rejects.toThrow(/Embedded asset not found/);
6+
expect(new EmbeddedAssetSource().read("cdk/package.json")).rejects.toThrow(
7+
/Embedded asset not found/,
8+
);
79
});
810
});

src/core/project/source.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import { fileURLToPath } from "node:url";
88
* This is the one place that knows whether assets come from disk or from the compiled executable.
99
*/
1010
export interface AssetSource {
11-
/** Reads the text of the asset at `assetPath`. */
11+
/** Reads the text of the asset at assetPath. */
1212
read(assetPath: string): Promise<string>;
13-
/** Lists asset paths of every file under `assetDir`, sorted, recursively. */
13+
/** Lists asset paths of every file under assetDir, sorted, recursively. */
1414
list(assetDir: string): Promise<string[]>;
1515
}
1616

@@ -20,41 +20,47 @@ const EMBEDDED_PREFIX = "agentcore-assets/src/assets/";
2020
// Embedded files carry a name property that Bun's types widen to Blob.
2121
type NamedBlob = Blob & { readonly name: string };
2222

23-
const embeddedBlobs = () => Bun.embeddedFiles as readonly NamedBlob[];
24-
2523
/** Reads assets embedded in the compiled standalone executable. */
26-
export const embeddedSource: AssetSource = {
27-
async read(assetPath) {
24+
export class EmbeddedAssetSource implements AssetSource {
25+
private blobs(): readonly NamedBlob[] {
26+
return Bun.embeddedFiles as readonly NamedBlob[];
27+
}
28+
29+
public async read(assetPath: string): Promise<string> {
2830
const name = `${EMBEDDED_PREFIX}${assetPath}`;
29-
const blob = embeddedBlobs().find((f) => f.name === name);
31+
const blob = this.blobs().find((f) => f.name === name);
3032
if (!blob) {
3133
throw new Error(`Embedded asset not found: ${assetPath}`);
3234
}
3335
return blob.text();
34-
},
35-
async list(assetDir) {
36+
}
37+
38+
public async list(assetDir: string): Promise<string[]> {
3639
const prefix = `${EMBEDDED_PREFIX}${assetDir}/`;
37-
return embeddedBlobs()
40+
return this.blobs()
3841
.filter((f) => f.name.startsWith(prefix))
3942
.map((f) => f.name.slice(EMBEDDED_PREFIX.length))
4043
.sort();
41-
},
42-
};
44+
}
45+
}
4346

4447
/** Reads assets from the assets directory on disk. */
45-
export function fileSource(assetsRoot = resolveAssetsRoot()): AssetSource {
46-
return {
47-
read: (assetPath) => readFile(join(assetsRoot, assetPath), "utf8"),
48-
async list(assetDir) {
49-
const root = join(assetsRoot, assetDir);
50-
const entries = await readdir(root, { recursive: true, withFileTypes: true });
51-
return entries
52-
.filter((entry) => entry.isFile())
53-
.map((entry) => join(assetDir, relative(root, join(entry.parentPath, entry.name))))
54-
.map((p) => p.replaceAll("\\", "/"))
55-
.sort();
56-
},
57-
};
48+
export class FsAssetSource implements AssetSource {
49+
constructor(private readonly assetsRoot: string = resolveAssetsRoot()) {}
50+
51+
public read(assetPath: string): Promise<string> {
52+
return readFile(join(this.assetsRoot, assetPath), "utf8");
53+
}
54+
55+
public async list(assetDir: string): Promise<string[]> {
56+
const root = join(this.assetsRoot, assetDir);
57+
const entries = await readdir(root, { recursive: true, withFileTypes: true });
58+
return entries
59+
.filter((entry) => entry.isFile())
60+
.map((entry) => join(assetDir, relative(root, join(entry.parentPath, entry.name))))
61+
.map((p) => p.replaceAll("\\", "/"))
62+
.sort();
63+
}
5864
}
5965

6066
// Bundled builds place assets beside the emitted module and the source layout keeps them two levels up.
@@ -72,5 +78,5 @@ function resolveAssetsRoot(moduleDirectory = dirname(fileURLToPath(import.meta.u
7278
*/
7379
export function defaultSource(): AssetSource {
7480
const embedded = typeof Bun !== "undefined" && Bun.embeddedFiles.length > 0;
75-
return embedded ? embeddedSource : fileSource();
81+
return embedded ? new EmbeddedAssetSource() : new FsAssetSource();
7682
}

src/core/project/templates.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import { PROJECT_TEMPLATES, type ProjectTemplate } from "../../handlers/project/types";
22

3-
interface TemplateSpec {
3+
type TemplateSpec = {
44
runtimes?: unknown[];
55
memories?: unknown[];
66
harnesses?: unknown[];
7-
}
7+
};
88

99
/**
1010
* A project template pairs the agent code scaffolded under app/ with the resource
1111
* sections it registers in agentcore.json. Adding a template is one entry here plus its assets.
1212
*/
13-
interface Template {
13+
type Template = {
1414
/** Directory under app/ the template code is written to. */
1515
appDir: string;
1616
/** Asset directory relative to the asset root, expanded into the app directory. */
1717
assetDir: string;
1818
/** Resource sections this template contributes to agentcore.json. */
1919
spec: TemplateSpec;
20-
}
20+
};
2121

2222
export const TEMPLATES: Record<ProjectTemplate, Template> = {
2323
[PROJECT_TEMPLATES.HELLO_WORLD_PYTHON]: {

src/core/project/tree.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import { existsSync } from "node:fs";
22
import { mkdir } from "node:fs/promises";
33
import { join } from "node:path";
4-
import { atomicWrite } from "../../fs";
4+
import { AgentCoreCLIError, ERROR_SOURCE } from "../../errors";
5+
import { atomicWrite } from "../../io";
56

67
/**
78
* A node in a project's file tree where directories nest and files are leaves.
89
* File bytes come from a thunk so the tree never knows where the bytes originate.
910
*/
1011
export type ProjectNode = DirNode | FileNode;
1112

12-
export interface DirNode {
13+
export type DirNode = {
1314
kind: "dir";
1415
name: string;
1516
children: ProjectNode[];
16-
}
17+
};
1718

18-
export interface FileNode {
19+
export type FileNode = {
1920
kind: "file";
2021
name: string;
2122
bytes: () => Promise<string>;
22-
}
23+
};
2324

2425
export const dir = (name: string, children: ProjectNode[]): DirNode => ({
2526
kind: "dir",
@@ -34,10 +35,12 @@ export const file = (name: string, bytes: () => Promise<string>): FileNode => ({
3435
});
3536

3637
/** Thrown when scaffolding would overwrite a file that already exists. */
37-
export class ProjectFileExistsError extends Error {
38+
export class ProjectFileExistsError extends AgentCoreCLIError {
3839
constructor(public readonly path: string) {
39-
super(`Refusing to overwrite existing file: ${path}`);
40-
this.name = "ProjectFileExistsError";
40+
super(`Refusing to overwrite existing file: ${path}`, {
41+
source: ERROR_SOURCE.USER,
42+
meta: { path },
43+
});
4144
}
4245
}
4346

src/fs/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/io/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { atomicWrite } from "./atomicWrite";
12
export { FsReadWriteJson } from "./json";
23
export { SourceResolutionError, SourceResolver, type SourceResolverConfig } from "./source";
34
export type { AppIO, ReadWriteJson } from "./types";

0 commit comments

Comments
 (0)