Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e07b912

Browse files
furaoclaude
andcommitted
fix: escape glob-special chars in directory paths (#974)
Parentheses and square brackets in project directory paths broke fast-glob matching, causing glob-based artifact outputs to silently return empty results. Escape these characters in the directory portion before passing to fast-glob, preserving glob semantics in the generates pattern. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent f529b25 commit e07b912

2 files changed

Lines changed: 68 additions & 5 deletions

File tree

src/core/artifact-graph/outputs.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,34 @@ export function isGlobPattern(pattern: string): boolean {
1010
return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');
1111
}
1212

13+
/**
14+
* Escapes glob-special characters in a directory path so it is treated as a
15+
* literal string by fast-glob. Only parentheses and square brackets need
16+
* escaping — curly braces and other extglob prefixes do not break matching
17+
* when they appear in isolation inside a filesystem path.
18+
*/
19+
function escapeGlobPath(p: string): string {
20+
return p.replace(/[()[\]]/g, '\\$&');
21+
}
22+
1323
/**
1424
* Resolves an artifact's output path(s) to concrete files that currently exist.
1525
* Returns absolute file paths. Glob matches are sorted for deterministic output.
1626
*/
1727
export function resolveArtifactOutputs(changeDir: string, generates: string): string[] {
18-
const fullPattern = path.join(changeDir, generates);
19-
2028
if (!isGlobPattern(generates)) {
29+
const fullPath = path.join(changeDir, generates);
2130
try {
22-
return fs.statSync(fullPattern).isFile()
23-
? [FileSystemUtils.canonicalizeExistingPath(fullPattern)]
31+
return fs.statSync(fullPath).isFile()
32+
? [FileSystemUtils.canonicalizeExistingPath(fullPath)]
2433
: [];
2534
} catch {
2635
return [];
2736
}
2837
}
2938

30-
const normalizedPattern = FileSystemUtils.toPosixPath(fullPattern);
39+
const escapedDir = escapeGlobPath(FileSystemUtils.toPosixPath(changeDir));
40+
const normalizedPattern = escapedDir + '/' + FileSystemUtils.toPosixPath(generates);
3141
const matches = fg
3242
.sync(normalizedPattern, { onlyFiles: true })
3343
.map((match) => FileSystemUtils.canonicalizeExistingPath(path.normalize(match)));

test/core/artifact-graph/outputs.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,57 @@ describe('artifact-graph/outputs', () => {
106106
expect(resolveArtifactOutputs(tempDir, 'specs/*/spec.md')).toEqual([]);
107107
expect(artifactOutputExists(tempDir, 'specs/*/spec.md')).toBe(false);
108108
});
109+
110+
describe('glob-special characters in directory paths', () => {
111+
it('resolves glob patterns when directory contains parentheses', () => {
112+
const dirWithParens = path.join(tempDir, 'project (work)');
113+
const specDir = path.join(dirWithParens, 'specs', 'cap-a');
114+
const specFile = path.join(specDir, 'spec.md');
115+
fs.mkdirSync(specDir, { recursive: true });
116+
fs.writeFileSync(specFile, 'content');
117+
118+
expect(resolveArtifactOutputs(dirWithParens, 'specs/*/spec.md')).toEqual([
119+
canonical(specFile),
120+
]);
121+
expect(artifactOutputExists(dirWithParens, 'specs/*/spec.md')).toBe(true);
122+
});
123+
124+
it('resolves glob patterns when directory contains square brackets', () => {
125+
const dirWithBrackets = path.join(tempDir, '[projects]');
126+
const specDir = path.join(dirWithBrackets, 'specs', 'cap-a');
127+
const specFile = path.join(specDir, 'spec.md');
128+
fs.mkdirSync(specDir, { recursive: true });
129+
fs.writeFileSync(specFile, 'content');
130+
131+
expect(resolveArtifactOutputs(dirWithBrackets, 'specs/*/spec.md')).toEqual([
132+
canonical(specFile),
133+
]);
134+
expect(artifactOutputExists(dirWithBrackets, 'specs/*/spec.md')).toBe(true);
135+
});
136+
137+
it('resolves glob patterns when directory contains curly braces', () => {
138+
const dirWithBraces = path.join(tempDir, '{workspace}');
139+
const specDir = path.join(dirWithBraces, 'specs', 'cap-a');
140+
const specFile = path.join(specDir, 'spec.md');
141+
fs.mkdirSync(specDir, { recursive: true });
142+
fs.writeFileSync(specFile, 'content');
143+
144+
expect(resolveArtifactOutputs(dirWithBraces, 'specs/*/spec.md')).toEqual([
145+
canonical(specFile),
146+
]);
147+
expect(artifactOutputExists(dirWithBraces, 'specs/*/spec.md')).toBe(true);
148+
});
149+
150+
it('resolves non-glob generates when directory contains special characters', () => {
151+
const dirWithParens = path.join(tempDir, 'project (work)');
152+
const proposalFile = path.join(dirWithParens, 'proposal.md');
153+
fs.mkdirSync(dirWithParens, { recursive: true });
154+
fs.writeFileSync(proposalFile, 'content');
155+
156+
expect(resolveArtifactOutputs(dirWithParens, 'proposal.md')).toEqual([
157+
canonical(proposalFile),
158+
]);
159+
expect(artifactOutputExists(dirWithParens, 'proposal.md')).toBe(true);
160+
});
161+
});
109162
});

0 commit comments

Comments
 (0)