fix(opencode): skip plugin write on literal drift, glob token extraction, segment-anchored mutation check

This commit is contained in:
rgb-vgx 2026-10-18 11:14:44 +07:00
parent 111e045f50
commit 20cb030ec9
3 changed files with 35 additions and 12 deletions

View file

@ -184,8 +184,13 @@ export function extractOpenCodePattern(tool, args) {
}
if (tool === 'glob') {
const raw = typeof input.pattern === 'string' ? input.pattern : '';
const match = raw.match(/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/);
return match ? match[1] : null;
// Longest literal run (>=3 chars) in the glob, with glob metachars as
// delimiters: '**/UserService*' → UserService, 'src/**/*.ts' → src,
// '**/*.service.ts' → service. The older [*/]letter regex missed every
// recursive glob whose segments start with '*', '.', or another metachar.
const runs = raw.match(/[a-zA-Z0-9_-]{3,}/g);
if (!runs) return null;
return runs.reduce((best, run) => (run.length > best.length ? run : best));
}
if (tool === 'bash') {
return extractBashSearchPattern(input.command);
@ -193,12 +198,17 @@ export function extractOpenCodePattern(tool, args) {
return null;
}
/** True for the git commands that move HEAD (same set as the Claude adapter). */
/**
* True for the git commands that move HEAD (same set as the Claude adapter).
* Segments are checked after splitting on shell separators and each must
* START with `git`, so `echo 'git commit'` or `grep 'git commit' file` don't
* count as mutations while `cd x && git commit` still does.
*/
export function isGitMutationCommand(command) {
return (
typeof command === 'string' &&
/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(command)
);
if (typeof command !== 'string') return false;
return command
.split(/&&|\|\||;|\|/)
.some((seg) => /^\s*git\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(seg));
}
/**

View file

@ -1351,8 +1351,9 @@ async function installOpenCodePlugin(result: SetupResult): Promise<void> {
const jsonCli = JSON.stringify(normalizedCli);
if (!content.includes(OPENCODE_CLI_PATH_SOURCE_LITERAL)) {
result.errors.push(
'OpenCode hooks: gitnexus-plugin.js no longer contains the cliPath literal to patch — the installed plugin may fail to resolve the CLI. Update OPENCODE_CLI_PATH_SOURCE_LITERAL in setup.ts.',
'OpenCode hooks: gitnexus-plugin.js no longer contains the cliPath literal to patch — skipping plugin install (the unpatched relative cliPath would resolve under the OpenCode config dir). Update OPENCODE_CLI_PATH_SOURCE_LITERAL in setup.ts.',
);
return;
}
content = content.replace(OPENCODE_CLI_PATH_SOURCE_LITERAL, `let cliPath = ${jsonCli};`);
// Sanity: the installed copy must export the plugin initializer OpenCode calls.

View file

@ -48,7 +48,9 @@ describe('extractOpenCodePattern', () => {
it('derives a token from glob patterns', () => {
expect(extractOpenCodePattern('glob', { pattern: '**/UserService*' })).toBe('UserService');
expect(extractOpenCodePattern('glob', { pattern: 'src/**/*.ts' })).toBeNull();
expect(extractOpenCodePattern('glob', { pattern: 'src/**/*.ts' })).toBe('src');
expect(extractOpenCodePattern('glob', { pattern: '**/*.service.ts' })).toBe('service');
expect(extractOpenCodePattern('glob', { pattern: '**/*.{a,b}' })).toBeNull();
expect(extractOpenCodePattern('glob', {})).toBeNull();
});
@ -102,11 +104,21 @@ describe('isGitMutationCommand', () => {
'git rebase main',
'git cherry-pick abc',
'git pull',
'cd src && git commit -m x',
'npm test || git pull',
])('detects %s', (cmd) => expect(isGitMutationCommand(cmd)).toBe(true));
it.each(['git status', 'git diff', 'git stash push', 'npm test', ''])('ignores %s', (cmd) =>
expect(isGitMutationCommand(cmd)).toBe(false),
);
it.each([
'git status',
'git diff',
'git stash push',
'npm test',
'',
// Text that merely mentions a mutation must not count.
"echo 'git commit'",
"grep 'git commit' file.txt",
'cat notes.md | grep git-pull',
])('ignores %s', (cmd) => expect(isGitMutationCommand(cmd)).toBe(false));
it('rejects non-strings', () => {
expect(isGitMutationCommand(null)).toBe(false);