Adam 5 месяцев назад
Родитель
Сommit
8749c0c707

+ 18 - 1
packages/opencode/src/cli/cmd/debug/file.ts

@@ -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({
   command: "file",
-  builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
+  builder: (yargs) =>
+    yargs.command(FileReadCommand).command(FileStatusCommand).command(FileListCommand).demandCommand(),
   async handler() {},
 })

+ 34 - 0
packages/opencode/src/file/index.ts

@@ -24,6 +24,17 @@ export namespace File {
 
   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 = {
     Edited: Bus.event(
       "file.edited",
@@ -120,4 +131,27 @@ export namespace File {
     }
     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)
+    })
+  }
 }

+ 28 - 4
packages/opencode/src/server/server.ts

@@ -937,6 +937,34 @@ export namespace Server {
       )
       .get(
         "/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({
           description: "Read a file",
           operationId: "file.read",
@@ -965,10 +993,6 @@ export namespace Server {
         async (c) => {
           const path = c.req.valid("query").path
           const content = await File.read(path)
-          log.info("read file", {
-            path,
-            content: content.content,
-          })
           return c.json(content)
         },
       )

+ 13 - 1
packages/sdk/js/src/gen/sdk.gen.ts

@@ -59,6 +59,8 @@ import type {
   FindFilesResponses,
   FindSymbolsData,
   FindSymbolsResponses,
+  FileListData,
+  FileListResponses,
   FileReadData,
   FileReadResponses,
   FileStatusData,
@@ -457,12 +459,22 @@ class Find 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
    */
   public read<ThrowOnError extends boolean = false>(options: Options<FileReadData, ThrowOnError>) {
     return (options.client ?? this._client).get<FileReadResponses, unknown, ThrowOnError>({
-      url: "/file",
+      url: "/file/content",
       ...options,
     })
   }

+ 25 - 1
packages/sdk/js/src/gen/types.gen.ts

@@ -1138,6 +1138,12 @@ export type Symbol = {
   }
 }
 
+export type FileNode = {
+  name: string
+  path: string
+  type: "file" | "directory"
+}
+
 export type File = {
   path: string
   added: number
@@ -1804,7 +1810,7 @@ export type FindSymbolsResponses = {
 
 export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses]
 
-export type FileReadData = {
+export type FileListData = {
   body?: never
   path?: never
   query: {
@@ -1813,6 +1819,24 @@ export type FileReadData = {
   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 = {
   /**
    * File content

+ 4 - 3
packages/sdk/stainless/stainless.yml

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