diff --git a/packages/core/src/util/glob.ts b/packages/core/src/util/glob.ts index cd73b40ff..80878bb9a 100644 --- a/packages/core/src/util/glob.ts +++ b/packages/core/src/util/glob.ts @@ -1,4 +1,4 @@ -import { glob, globSync, type GlobOptions } from "glob" +import { glob, globSync, globIterate, type GlobOptions } from "glob" import { minimatch } from "minimatch" export namespace Glob { @@ -62,6 +62,16 @@ export namespace Glob { } } + // altimate_change start — upstream_fix: existence check that stops at the first match. + // `scan` resolves only once the whole walk is done, so a caller asking "does anything + // match?" pays for the entire tree even when the first directory answers it. `globIterate` + // yields lazily, so this abandons the walk as soon as one path matches. + export async function exists(pattern: string, options: Options = {}): Promise { + for await (const _ of globIterate(pattern, toGlobOptions(options))) return true + return false + } + // altimate_change end + export async function scan(pattern: string, options: Options = {}): Promise { return glob(pattern, toGlobOptions(options)) as Promise } diff --git a/packages/core/test/util/glob.test.ts b/packages/core/test/util/glob.test.ts index d305552b1..c2e8ac346 100644 --- a/packages/core/test/util/glob.test.ts +++ b/packages/core/test/util/glob.test.ts @@ -121,3 +121,43 @@ describe("Glob.DEFAULT_IGNORE", () => { }) }) // altimate_change end + +// altimate_change start — upstream_fix: `exists` must answer without walking the whole tree. +describe("Glob.exists", () => { + test("agrees with scan() on whether anything matched", async () => { + for (const pattern of ["**/*.ts", "**/mcp.json", "**/nothing-matches-this.xyz"]) { + const scanned = await Glob.scan(pattern, { cwd: root, absolute: true }) + const existed = await Glob.exists(pattern, { cwd: root, absolute: true }) + expect(existed, `pattern ${pattern}`).toBe(scanned.length > 0) + } + }) + + test("honours the same options as scan", async () => { + // `include: "file"` must not report a directory match, or a skill whose applyPaths names + // a directory would auto-load on every project that happens to have one. + const dirOnly = await Glob.exists("src", { cwd: root, include: "file" }) + const withDirs = await Glob.exists("src", { cwd: root, include: "all" }) + expect(dirOnly).toBe(false) + expect(withDirs).toBe(true) + }) + + test("prunes with ignore, like scan", async () => { + // Own fixture: the shared `root` has matching files outside the ignored trees too, which + // would make this pass for the wrong reason. + const own = await mkdtemp(path.join(tmpdir(), "glob-exists-")) + try { + await mkdir(path.join(own, "node_modules", "pkg"), { recursive: true }) + await writeFile(path.join(own, "node_modules", "pkg", "only-here.json"), "{}") + + expect(await Glob.exists("**/only-here.json", { cwd: own })).toBe(true) + expect(await Glob.exists("**/only-here.json", { cwd: own, ignore: ["**/node_modules/**"] })).toBe(false) + } finally { + await rm(own, { recursive: true, force: true }) + } + }) + + test("returns false for a directory that does not exist", async () => { + expect(await Glob.exists("**/*", { cwd: path.join(root, "no-such-dir") })).toBe(false) + }) +}) +// altimate_change end diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 6e035616c..da9692c02 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -246,16 +246,23 @@ export namespace SystemPrompt { // catches the file no matter how deep the user's cwd is. // Errors propagate to the caller's try/catch (collectAutoLoadedSkills) // so the warning log there actually fires. + // `Glob.exists` rather than `scan(...).length > 0`: this only needs to know whether any + // file matches, and `scan` walks the whole tree before the caller can look. That cost is + // paid once per `applyPaths` skill — two ship builtin — and the root is the worktree, which + // is `/` for a directory outside any git repo. Measured from such a directory, the two + // scans were ~45s of a ~51s startup, all of it before the first token. const root = Instance.worktree for (const g of globs) { - const matches = await Glob.scan(g, { - cwd: root, - absolute: true, - include: "file", - dot: false, - symlink: false, - }) - if (matches.length > 0) return true + if ( + await Glob.exists(g, { + cwd: root, + absolute: true, + include: "file", + dot: false, + symlink: false, + }) + ) + return true } return false }