|
1 | | -import { stat } from 'node:fs/promises'; |
| 1 | +import type { Dirent } from 'node:fs'; |
| 2 | +import { readdir, stat } from 'node:fs/promises'; |
| 3 | +import { join, relative, sep } from 'node:path'; |
2 | 4 | import { exec } from '../../sandbox/exec.js'; |
3 | 5 | import type { Tool, ToolContext } from '../types.js'; |
4 | | -import { MAX_GLOB_RESULTS, escapeShellArg, quoted, stringArg } from './helpers.js'; |
| 6 | +import { MAX_GLOB_RESULTS, quoted, stringArg } from './helpers.js'; |
5 | 7 |
|
6 | 8 | export const globTool: Tool = { |
7 | 9 | name: 'glob', |
@@ -41,18 +43,62 @@ async function runGlob(ctx: ToolContext, pattern: string): Promise<string[]> { |
41 | 43 | ); |
42 | 44 | return stdout.split('\n').filter(Boolean); |
43 | 45 | } |
44 | | - const { stdout } = await exec( |
45 | | - '/bin/sh', |
46 | | - [ |
47 | | - '-lc', |
48 | | - `find . -path ${escapeShellArg(pattern)} -type f -not -path '*/.*' | head -n 1000`, |
49 | | - ], |
50 | | - { cwd: ctx.cwd, signal: ctx.signal }, |
51 | | - ); |
52 | | - return stdout |
53 | | - .split('\n') |
54 | | - .filter(Boolean) |
55 | | - .map((p) => (p.startsWith('./') ? p.slice(2) : p)); |
| 46 | + // Non-git worktree: walk the tree in Node (cross-platform; no reliance on a |
| 47 | + // POSIX `find`, which doesn't exist on Windows) and match the glob ourselves. |
| 48 | + const all = await walkFiles(ctx.cwd); |
| 49 | + const rx = globToRegExp(pattern); |
| 50 | + return all.filter((p) => rx.test(p)).slice(0, 1000); |
| 51 | +} |
| 52 | + |
| 53 | +/** Recursively list files relative to `root`, skipping dotfiles and node_modules. */ |
| 54 | +async function walkFiles(root: string, max = 5000): Promise<string[]> { |
| 55 | + const out: string[] = []; |
| 56 | + const rec = async (dir: string): Promise<void> => { |
| 57 | + if (out.length >= max) return; |
| 58 | + const entries: Dirent[] = await readdir(dir, { withFileTypes: true }).catch(() => [] as Dirent[]); |
| 59 | + for (const e of entries) { |
| 60 | + if (e.name.startsWith('.') || e.name === 'node_modules') continue; |
| 61 | + const full = join(dir, e.name); |
| 62 | + if (e.isDirectory()) { |
| 63 | + await rec(full); |
| 64 | + } else if (e.isFile()) { |
| 65 | + out.push(relative(root, full).split(sep).join('/')); |
| 66 | + if (out.length >= max) return; |
| 67 | + } |
| 68 | + } |
| 69 | + }; |
| 70 | + await rec(root); |
| 71 | + return out; |
| 72 | +} |
| 73 | + |
| 74 | +/** Convert a POSIX-style glob (`**`, `*`, `?`, `{a,b}`) to an anchored RegExp. */ |
| 75 | +function globToRegExp(glob: string): RegExp { |
| 76 | + let re = ''; |
| 77 | + for (let i = 0; i < glob.length; i++) { |
| 78 | + const c = glob.charAt(i); |
| 79 | + if (c === '*') { |
| 80 | + if (glob.charAt(i + 1) === '*') { |
| 81 | + re += '.*'; |
| 82 | + i++; |
| 83 | + if (glob.charAt(i + 1) === '/') i++; |
| 84 | + } else { |
| 85 | + re += '[^/]*'; |
| 86 | + } |
| 87 | + } else if (c === '?') { |
| 88 | + re += '[^/]'; |
| 89 | + } else if (c === '{') { |
| 90 | + re += '('; |
| 91 | + } else if (c === '}') { |
| 92 | + re += ')'; |
| 93 | + } else if (c === ',') { |
| 94 | + re += '|'; |
| 95 | + } else if ('.+^$()|[]\\'.includes(c)) { |
| 96 | + re += `\\${c}`; |
| 97 | + } else { |
| 98 | + re += c; |
| 99 | + } |
| 100 | + } |
| 101 | + return new RegExp(`^${re}$`); |
56 | 102 | } |
57 | 103 |
|
58 | 104 | async function isGitRepo(cwd: string): Promise<boolean> { |
|
0 commit comments