diff --git a/packages/opencode/src/util/wildcard.ts b/packages/opencode/src/util/wildcard.ts index 9b595a0a9..4a6eba96f 100644 --- a/packages/opencode/src/util/wildcard.ts +++ b/packages/opencode/src/util/wildcard.ts @@ -2,16 +2,18 @@ import { sortBy, pipe } from "remeda" export namespace Wildcard { export function match(str: string, pattern: string) { - const regex = new RegExp( - "^" + - pattern - .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars - .replace(/\*/g, ".*") // * becomes .* - .replace(/\?/g, ".") + // ? becomes . - "$", - "s", // s flag enables multiline matching - ) - return regex.test(str) + let escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars + .replace(/\*/g, ".*") // * becomes .* + .replace(/\?/g, ".") // ? becomes . + + // If pattern ends with " *" (space + wildcard), make the trailing part optional + // This allows "ls *" to match both "ls" and "ls -la" + if (escaped.endsWith(" .*")) { + escaped = escaped.slice(0, -3) + "( .*)?" + } + + return new RegExp("^" + escaped + "$", "s").test(str) } export function all(input: string, patterns: Record) { diff --git a/packages/opencode/test/util/wildcard.test.ts b/packages/opencode/test/util/wildcard.test.ts index f7f1e1545..9cd0e9b94 100644 --- a/packages/opencode/test/util/wildcard.test.ts +++ b/packages/opencode/test/util/wildcard.test.ts @@ -7,6 +7,26 @@ test("match handles glob tokens", () => { expect(Wildcard.match("foo+bar", "foo+bar")).toBe(true) }) +test("match with trailing space+wildcard matches command with or without args", () => { + // "ls *" should match "ls" (no args) and "ls -la" (with args) + expect(Wildcard.match("ls", "ls *")).toBe(true) + expect(Wildcard.match("ls -la", "ls *")).toBe(true) + expect(Wildcard.match("ls foo bar", "ls *")).toBe(true) + + // "ls*" (no space) should NOT match "ls" alone — wait, it should because .* matches empty + // but it WILL match "lstmeval" which is the dangerous case users should avoid + expect(Wildcard.match("ls", "ls*")).toBe(true) + expect(Wildcard.match("lstmeval", "ls*")).toBe(true) + + // "ls *" (with space) should NOT match "lstmeval" + expect(Wildcard.match("lstmeval", "ls *")).toBe(false) + + // multi-word commands + expect(Wildcard.match("git status", "git *")).toBe(true) + expect(Wildcard.match("git", "git *")).toBe(true) + expect(Wildcard.match("git commit -m foo", "git *")).toBe(true) +}) + test("all picks the most specific pattern", () => { const rules = { "*": "deny",