diff --git a/src/bash-utils.ts b/src/bash-utils.ts index 2e5fe0db..06a4f6df 100644 --- a/src/bash-utils.ts +++ b/src/bash-utils.ts @@ -5,6 +5,14 @@ function stripQuotedStrings(command: string): string { return command.replace(/"[^"]*"|'[^']*'/g, match => ' '.repeat(match.length)) } +const COMMAND_PREFIXES = new Set([ + 'sudo', 'doas', + 'npx', 'bunx', + 'time', + 'nice', 'nohup', 'stdbuf', + 'rtk', +]) + export function extractBashCommands(rawCommand: string): string[] { if (!rawCommand || !rawCommand.trim()) return [] @@ -34,7 +42,17 @@ export function extractBashCommands(rawCommand: string): string[] { const tokens = segment.split(/\s+/) let i = 0 - while (i < tokens.length && /^\w+=/.test(tokens[i]!)) i++ + while (i < tokens.length) { + if (/^\w+=/.test(tokens[i]!)) { i++; continue } + const next = tokens[i + 1] + if ( + next !== undefined && + COMMAND_PREFIXES.has(basename(tokens[i]!)) && + !next.startsWith('-') && + !/["']/.test(next) + ) { i++; continue } + break + } const base = i < tokens.length ? basename(tokens[i]!) : '' if (base && base !== 'cd' && base !== 'true' && base !== 'false') { diff --git a/tests/bash-commands.test.ts b/tests/bash-commands.test.ts index 2c830e9d..3daf439a 100644 --- a/tests/bash-commands.test.ts +++ b/tests/bash-commands.test.ts @@ -73,6 +73,43 @@ describe('extractBashCommands', () => { it('handles env vars combined with chained commands', () => { expect(extractBashCommands('NODE_ENV=test npm test && git push')).toEqual(['npm', 'git']) }) + + it('skips command wrapper prefixes', () => { + expect(extractBashCommands('rtk git status')).toEqual(['git']) + expect(extractBashCommands('sudo npm install')).toEqual(['npm']) + expect(extractBashCommands('npx vitest --run')).toEqual(['vitest']) + }) + + it('skips prefix combined with env var assignment', () => { + expect(extractBashCommands('DEBUG=1 rtk git status')).toEqual(['git']) + }) + + it('skips nested wrapper prefixes', () => { + expect(extractBashCommands('sudo npx vitest --run')).toEqual(['vitest']) + }) + + it('skips prefix across chained commands', () => { + expect(extractBashCommands('rtk git add . && rtk git commit -m "msg"')).toEqual(['git', 'git']) + }) + + it('keeps a standalone prefix with no following command', () => { + expect(extractBashCommands('rtk')).toEqual(['rtk']) + expect(extractBashCommands('sudo')).toEqual(['sudo']) + }) + + it('keeps prefix when the next token is a flag', () => { + expect(extractBashCommands('nice -n 10 git push')).toEqual(['nice']) + }) + + it('skips env assignment that follows a wrapper prefix', () => { + expect(extractBashCommands('sudo NODE_ENV=production node server.js')).toEqual(['node']) + expect(extractBashCommands('time FOO=1 make build')).toEqual(['make']) + }) + + it('keeps prefix when the next token is quoted', () => { + expect(extractBashCommands('npx "@angular/cli" new app')).toEqual(['npx']) + expect(extractBashCommands("npx 'ts-node' script.ts")).toEqual(['npx']) + }) }) describe('BASH_TOOLS', () => {