summaryrefslogtreecommitdiffhomepage
path: root/packages/opencode/src/tool/ls.ts
blob: 46efb307a16b0a55a25f4d6a455942031a3084e8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { z } from "zod";
import { Tool } from "./tool";
import { App } from "../app/app";
import * as path from "path";

const IGNORE_PATTERNS = [
  "node_modules/",
  "__pycache__/",
  ".git/",
  "dist/",
  "build/",
  "target/",
  "vendor/",
  "bin/",
  "obj/",
  ".idea/",
  ".vscode/",
];

export const ls = Tool.define({
  name: "opencode.ls",
  description: "List directory contents",
  parameters: z.object({
    path: z.string().optional(),
    ignore: z.array(z.string()).optional(),
  }),
  async execute(params) {
    const app = await App.use();
    const searchPath = path.resolve(app.root, params.path || ".");

    const glob = new Bun.Glob("**/*");
    const files = [];

    for await (const file of glob.scan({ cwd: searchPath })) {
      if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
        continue;
      if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
        continue;
      files.push(file);
      if (files.length >= 1000) break;
    }

    // Build directory structure
    const dirs = new Set<string>();
    const filesByDir = new Map<string, string[]>();

    for (const file of files) {
      const dir = path.dirname(file);
      const parts = dir === "." ? [] : dir.split("/");

      // Add all parent directories
      for (let i = 0; i <= parts.length; i++) {
        const dirPath = i === 0 ? "." : parts.slice(0, i).join("/");
        dirs.add(dirPath);
      }

      // Add file to its directory
      if (!filesByDir.has(dir)) filesByDir.set(dir, []);
      filesByDir.get(dir)!.push(path.basename(file));
    }

    function renderDir(dirPath: string, depth: number): string {
      const indent = "  ".repeat(depth);
      let output = "";

      if (depth > 0) {
        output += `${indent}${path.basename(dirPath)}/\n`;
      }

      const childIndent = "  ".repeat(depth + 1);
      const children = Array.from(dirs)
        .filter((d) => path.dirname(d) === dirPath && d !== dirPath)
        .sort();

      // Render subdirectories first
      for (const child of children) {
        output += renderDir(child, depth + 1);
      }

      // Render files
      const files = filesByDir.get(dirPath) || [];
      for (const file of files.sort()) {
        output += `${childIndent}${file}\n`;
      }

      return output;
    }

    const output = `${searchPath}/\n` + renderDir(".", 0);

    return {
      metadata: { count: files.length, truncated: files.length >= 1000 },
      output,
    };
  },
});