Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/core/src/util/glob.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<boolean> {
for await (const _ of globIterate(pattern, toGlobOptions(options))) return true
return false
}
// altimate_change end

export async function scan(pattern: string, options: Options = {}): Promise<string[]> {
return glob(pattern, toGlobOptions(options)) as Promise<string[]>
}
Expand Down
40 changes: 40 additions & 0 deletions packages/core/test/util/glob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 15 additions & 8 deletions packages/opencode/src/session/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading