mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-26 06:02:22 +00:00
fix(opencode): honor -e/--regexp as pattern and surface non-ENOENT plugin stat errors
This commit is contained in:
parent
45d6c48695
commit
cfdf31f72a
4 changed files with 62 additions and 7 deletions
|
|
@ -79,9 +79,16 @@ export function extractAugmentContext(stderr) {
|
|||
return output.slice(marker).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* rg/grep flags whose following argv token IS the search pattern
|
||||
* (`rg -e foo file.ts`, `grep --regexp foo .`). They must not be lumped in
|
||||
* with value-consuming flags — skipping the value would augment on the
|
||||
* filename instead of the search.
|
||||
*/
|
||||
const PATTERN_FLAGS = new Set(['-e', '--regexp', '--expression']);
|
||||
|
||||
/** Flags of rg/grep that consume the following argv token as a value. */
|
||||
const FLAGS_WITH_VALUES = new Set([
|
||||
'-e',
|
||||
'-f',
|
||||
'-m',
|
||||
'-A',
|
||||
|
|
@ -103,18 +110,25 @@ export function extractBashSearchPattern(command) {
|
|||
if (typeof command !== 'string' || !/\brg\b|\bgrep\b/.test(command)) return null;
|
||||
const tokens = command.split(/\s+/);
|
||||
let foundCmd = false;
|
||||
let skipNext = false;
|
||||
// 'skip' = drop the next token (flag value); 'pattern' = take it as the search.
|
||||
let pending = null;
|
||||
for (const token of tokens) {
|
||||
if (skipNext) {
|
||||
skipNext = false;
|
||||
if (pending === 'skip') {
|
||||
pending = null;
|
||||
continue;
|
||||
}
|
||||
if (pending === 'pattern') {
|
||||
pending = null;
|
||||
const cleaned = token.replace(/['"]/g, '');
|
||||
return cleaned.length >= 3 ? cleaned : null;
|
||||
}
|
||||
if (!foundCmd) {
|
||||
if (/\brg$|\bgrep$/.test(token)) foundCmd = true;
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith('-')) {
|
||||
if (FLAGS_WITH_VALUES.has(token)) skipNext = true;
|
||||
if (PATTERN_FLAGS.has(token)) pending = 'pattern';
|
||||
else if (FLAGS_WITH_VALUES.has(token)) pending = 'skip';
|
||||
continue;
|
||||
}
|
||||
const cleaned = token.replace(/['"]/g, '');
|
||||
|
|
|
|||
|
|
@ -205,8 +205,11 @@ async function removeFile(filePath: string, dryRun: boolean): Promise<boolean> {
|
|||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (!stat.isFile()) return false;
|
||||
} catch {
|
||||
return false;
|
||||
} catch (err) {
|
||||
// Only "not there" is a clean no-op; anything else (EACCES, EIO, ...)
|
||||
// must surface — the caller records it in result.errors.
|
||||
if (isEnoent(err)) return false;
|
||||
throw err;
|
||||
}
|
||||
if (!dryRun) await fs.rm(filePath, { force: true });
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,14 @@ describe('extractBashSearchPattern', () => {
|
|||
expect(extractBashSearchPattern('grep -r UserService .')).toBe('UserService');
|
||||
});
|
||||
|
||||
it('treats -e/--regexp/--expression as the pattern itself, not a value to skip', () => {
|
||||
expect(extractBashSearchPattern('rg -e UserService .')).toBe('UserService');
|
||||
expect(extractBashSearchPattern('grep -e UserService file.ts')).toBe('UserService');
|
||||
expect(extractBashSearchPattern('rg --regexp UserService .')).toBe('UserService');
|
||||
// The pattern flag must still win over a following positional filename.
|
||||
expect(extractBashSearchPattern('rg -n -e UserService setup.ts')).toBe('UserService');
|
||||
});
|
||||
|
||||
it('rejects short tokens and non-search commands', () => {
|
||||
expect(extractBashSearchPattern('rg -n "ab" .')).toBeNull();
|
||||
expect(extractBashSearchPattern('npm test')).toBeNull();
|
||||
|
|
|
|||
|
|
@ -444,6 +444,36 @@ describe('uninstallCommand', () => {
|
|||
expect(await fs.readFile(pluginFile, 'utf-8')).toBe('// gitnexus');
|
||||
});
|
||||
|
||||
it('reports a non-ENOENT stat error on the plugin file instead of silently skipping', async () => {
|
||||
const pluginFile = path.join(tempHome, '.config', 'opencode', 'plugins', 'gitnexus.js');
|
||||
await fs.mkdir(path.dirname(pluginFile), { recursive: true });
|
||||
await fs.writeFile(pluginFile, '// gitnexus', 'utf-8');
|
||||
|
||||
// Simulate EACCES on the plugin stat only; every other stat passes through.
|
||||
const realStat = fs.stat.bind(fs);
|
||||
const statSpy = vi.spyOn(fs, 'stat').mockImplementation((async (p: any, ...rest: any[]) => {
|
||||
if (String(p).endsWith(path.join('plugins', 'gitnexus.js'))) {
|
||||
const err: any = new Error(`EACCES: permission denied, stat '${p}'`);
|
||||
err.code = 'EACCES';
|
||||
throw err;
|
||||
}
|
||||
return (realStat as any)(p, ...rest);
|
||||
}) as any);
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log');
|
||||
const uninstallCommand = await importUninstall();
|
||||
await uninstallCommand({ force: true });
|
||||
|
||||
// Before the fix, removeFile swallowed every stat error → plugin left in
|
||||
// place with no error reported. Now EACCES surfaces in the Errors block.
|
||||
const output = logSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
||||
expect(output).toContain('hooks plugin');
|
||||
expect(statSpy).toHaveBeenCalled();
|
||||
// The plugin file was never reached (rm), so it still exists.
|
||||
expect(await fs.readFile(pluginFile, 'utf-8')).toBe('// gitnexus');
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
// ── Antigravity MCP + hooks (AfterTool / gitnexus-antigravity-hook) ──
|
||||
it('removes Antigravity MCP and AfterTool hooks plus the adapter script dir', async () => {
|
||||
const mcpPath = path.join(tempHome, '.gemini', 'antigravity', 'mcp_config.json');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue