feat: file list api

This commit is contained in:
Adam
2025-08-27 15:27:49 -05:00
parent 3359417378
commit 8749c0c707
6 changed files with 122 additions and 10 deletions

View File

@@ -29,8 +29,25 @@ const FileStatusCommand = cmd({
}, },
}) })
const FileListCommand = cmd({
command: "list <path>",
builder: (yargs) =>
yargs.positional("path", {
type: "string",
demandOption: true,
description: "File path to list",
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
const files = await File.list(args.path)
console.log(JSON.stringify(files, null, 2))
})
},
})
export const FileCommand = cmd({ export const FileCommand = cmd({
command: "file", command: "file",
builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(), builder: (yargs) =>
yargs.command(FileReadCommand).command(FileStatusCommand).command(FileListCommand).demandCommand(),
async handler() {}, async handler() {},
}) })

View File

@@ -24,6 +24,17 @@ export namespace File {
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
export const Node = z
.object({
name: z.string(),
path: z.string(),
type: z.enum(["file", "directory"]),
})
.openapi({
ref: "FileNode",
})
export type Node = z.infer<typeof Node>
export const Event = { export const Event = {
Edited: Bus.event( Edited: Bus.event(
"file.edited", "file.edited",
@@ -120,4 +131,27 @@ export namespace File {
} }
return { type: "raw", content } return { type: "raw", content }
} }
export async function list(dir?: string) {
const ignore = [".git", ".DS_Store"]
const app = App.info()
const resolved = dir ? path.join(app.path.cwd, dir) : app.path.cwd
const nodes: Node[] = []
for (const entry of await fs.promises.readdir(resolved, { withFileTypes: true })) {
if (ignore.includes(entry.name)) continue
const fullPath = path.join(resolved, entry.name)
const relativePath = path.relative(app.path.cwd, fullPath)
nodes.push({
name: entry.name,
path: relativePath,
type: entry.isDirectory() ? "directory" : "file",
})
}
return nodes.sort((a, b) => {
if (a.type !== b.type) {
return a.type === "directory" ? -1 : 1
}
return a.name.localeCompare(b.name)
})
}
} }

View File

@@ -937,6 +937,34 @@ export namespace Server {
) )
.get( .get(
"/file", "/file",
describeRoute({
description: "List files and directories",
operationId: "file.list",
responses: {
200: {
description: "Files and directories",
content: {
"application/json": {
schema: resolver(File.Node.array()),
},
},
},
},
}),
zValidator(
"query",
z.object({
path: z.string(),
}),
),
async (c) => {
const path = c.req.valid("query").path
const content = await File.list(path)
return c.json(content)
},
)
.get(
"/file/content",
describeRoute({ describeRoute({
description: "Read a file", description: "Read a file",
operationId: "file.read", operationId: "file.read",
@@ -965,10 +993,6 @@ export namespace Server {
async (c) => { async (c) => {
const path = c.req.valid("query").path const path = c.req.valid("query").path
const content = await File.read(path) const content = await File.read(path)
log.info("read file", {
path,
content: content.content,
})
return c.json(content) return c.json(content)
}, },
) )

View File

@@ -59,6 +59,8 @@ import type {
FindFilesResponses, FindFilesResponses,
FindSymbolsData, FindSymbolsData,
FindSymbolsResponses, FindSymbolsResponses,
FileListData,
FileListResponses,
FileReadData, FileReadData,
FileReadResponses, FileReadResponses,
FileStatusData, FileStatusData,
@@ -457,12 +459,22 @@ class Find extends _HeyApiClient {
} }
class File extends _HeyApiClient { class File extends _HeyApiClient {
/**
* List files and directories
*/
public list<ThrowOnError extends boolean = false>(options: Options<FileListData, ThrowOnError>) {
return (options.client ?? this._client).get<FileListResponses, unknown, ThrowOnError>({
url: "/file",
...options,
})
}
/** /**
* Read a file * Read a file
*/ */
public read<ThrowOnError extends boolean = false>(options: Options<FileReadData, ThrowOnError>) { public read<ThrowOnError extends boolean = false>(options: Options<FileReadData, ThrowOnError>) {
return (options.client ?? this._client).get<FileReadResponses, unknown, ThrowOnError>({ return (options.client ?? this._client).get<FileReadResponses, unknown, ThrowOnError>({
url: "/file", url: "/file/content",
...options, ...options,
}) })
} }

View File

@@ -1138,6 +1138,12 @@ export type Symbol = {
} }
} }
export type FileNode = {
name: string
path: string
type: "file" | "directory"
}
export type File = { export type File = {
path: string path: string
added: number added: number
@@ -1804,7 +1810,7 @@ export type FindSymbolsResponses = {
export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses] export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses]
export type FileReadData = { export type FileListData = {
body?: never body?: never
path?: never path?: never
query: { query: {
@@ -1813,6 +1819,24 @@ export type FileReadData = {
url: "/file" url: "/file"
} }
export type FileListResponses = {
/**
* Files and directories
*/
200: Array<FileNode>
}
export type FileListResponse = FileListResponses[keyof FileListResponses]
export type FileReadData = {
body?: never
path?: never
query: {
path: string
}
url: "/file/content"
}
export type FileReadResponses = { export type FileReadResponses = {
/** /**
* File content * File content

View File

@@ -48,7 +48,6 @@ resources:
app: app:
models: models:
app: App app: App
logLevel: LogLevel
provider: Provider provider: Provider
model: Model model: Model
agent: Agent agent: Agent
@@ -61,7 +60,6 @@ resources:
find: find:
models: models:
match: Match
symbol: Symbol symbol: Symbol
methods: methods:
text: get /find text: get /find
@@ -71,8 +69,11 @@ resources:
file: file:
models: models:
file: File file: File
fileNode: FileNode
methods: methods:
read: get /file list: get /file
read: get /file/content
status: get /file/status status: get /file/status
config: config: