diff --git a/lib/provider-detection.js b/lib/provider-detection.js index 6952649..6557386 100644 --- a/lib/provider-detection.js +++ b/lib/provider-detection.js @@ -7,8 +7,9 @@ function commandExists(command) { if (command.includes(path.sep)) { return fs.existsSync(command); } + const probe = process.platform === 'win32' ? `where ${command}` : `command -v ${command}`; try { - execSync(`command -v ${command}`, { stdio: 'pipe' }); + execSync(probe, { stdio: 'pipe' }); return true; } catch { return false; @@ -20,9 +21,11 @@ function getCommandPath(command) { if (command.includes(path.sep)) { return fs.existsSync(command) ? command : null; } + const probe = process.platform === 'win32' ? `where ${command}` : `command -v ${command}`; try { - const output = execSync(`command -v ${command}`, { encoding: 'utf8', stdio: 'pipe' }); - return output.trim() || null; + const output = execSync(probe, { encoding: 'utf8', stdio: 'pipe' }); + // `where` can return multiple matches (one per line); take the first. + return output.split(/\r?\n/)[0].trim() || null; } catch { return null; } diff --git a/tests/provider-detection.test.js b/tests/provider-detection.test.js new file mode 100644 index 0000000..396468d --- /dev/null +++ b/tests/provider-detection.test.js @@ -0,0 +1,86 @@ +/** + * Tests for provider-detection cross-platform CLI lookup. + * + * Regression: on Windows the lookup used the POSIX builtin `command -v`, + * which does not exist under cmd.exe, so every provider was reported as + * "not found". The lookup must use `where` on win32 and `command -v` elsewhere. + */ +const { expect } = require('chai'); +const sinon = require('sinon'); +const childProcess = require('child_process'); + +function withPlatform(value, fn) { + const original = Object.getOwnPropertyDescriptor(process, 'platform'); + Object.defineProperty(process, 'platform', { value, configurable: true }); + try { + fn(); + } finally { + Object.defineProperty(process, 'platform', original); + } +} + +describe('provider-detection', () => { + let execSyncStub; + let detection; + + beforeEach(() => { + // Stub before requiring the module so its destructured reference is the stub. + execSyncStub = sinon.stub(childProcess, 'execSync'); + delete require.cache[require.resolve('../lib/provider-detection.js')]; + detection = require('../lib/provider-detection.js'); + }); + + afterEach(() => { + sinon.restore(); + delete require.cache[require.resolve('../lib/provider-detection.js')]; + }); + + describe('commandExists', () => { + it('returns false for empty command without probing', () => { + expect(detection.commandExists('')).to.equal(false); + expect(execSyncStub.called).to.equal(false); + }); + + it('uses `where` on win32', () => { + execSyncStub.returns('C:\\bin\\claude.exe'); + withPlatform('win32', () => { + expect(detection.commandExists('claude')).to.equal(true); + }); + expect(execSyncStub.firstCall.args[0]).to.equal('where claude'); + }); + + it('uses `command -v` on non-win32', () => { + execSyncStub.returns('/usr/bin/claude'); + withPlatform('linux', () => { + expect(detection.commandExists('claude')).to.equal(true); + }); + expect(execSyncStub.firstCall.args[0]).to.equal('command -v claude'); + }); + + it('returns false when the probe throws (command missing)', () => { + execSyncStub.throws(new Error('not found')); + withPlatform('win32', () => { + expect(detection.commandExists('nope')).to.equal(false); + }); + }); + }); + + describe('getCommandPath', () => { + it('returns the first match line on win32 (`where` may return many)', () => { + execSyncStub.returns('C:\\a\\claude.exe\r\nC:\\b\\claude.cmd\r\n'); + let result; + withPlatform('win32', () => { + result = detection.getCommandPath('claude'); + }); + expect(result).to.equal('C:\\a\\claude.exe'); + expect(execSyncStub.firstCall.args[0]).to.equal('where claude'); + }); + + it('returns null when the probe throws', () => { + execSyncStub.throws(new Error('not found')); + withPlatform('linux', () => { + expect(detection.getCommandPath('nope')).to.equal(null); + }); + }); + }); +});