Skip to content

Commit 74df194

Browse files
committed
feat: add devProxyPath setting and improve detection. Closes #334
1 parent c7bfc19 commit 74df194

6 files changed

Lines changed: 154 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added:
1313

14+
- Setting: Added `devProxyPath` setting to specify custom path to Dev Proxy executable
15+
- Detection: Added auto-detection fallback using login shell and common installation paths
1416
- Snippets: Added `devproxy-plugin-graph-connector-guidance` - GraphConnectorGuidancePlugin instance
1517
- Snippets: Added `devproxy-plugin-mock-stdio-response` - MockStdioResponsePlugin instance
1618
- Snippets: Added `devproxy-plugin-mock-stdio-response-config` - MockStdioResponsePlugin config section

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ Shows Dev Proxy status at a glance:
221221
| `dev-proxy-toolkit.showTerminal` | `boolean` | `true` | Show terminal when starting |
222222
| `dev-proxy-toolkit.closeTerminal` | `boolean` | `true` | Close terminal when stopping |
223223
| `dev-proxy-toolkit.apiPort` | `number` | `8897` | Port for Dev Proxy API communication |
224+
| `dev-proxy-toolkit.devProxyPath` | `string` | `""` | Custom path to Dev Proxy executable (uses auto-detection if empty) |
224225

225226
## Tasks
226227

src/detect.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DevProxyInstall } from './types';
22
import os from 'os';
33
import { VersionExeName, VersionPreference } from './enums';
4-
import { executeCommand } from './utils/shell';
4+
import { executeCommand, resolveDevProxyExecutable } from './utils/shell';
55
import * as vscode from 'vscode';
66

77
export const getVersion = async (devProxyExe: string) => {
@@ -16,7 +16,10 @@ export const getVersion = async (devProxyExe: string) => {
1616
};
1717

1818
export const detectDevProxyInstall = async (versionPreference: VersionPreference): Promise<DevProxyInstall> => {
19-
const devProxyExe = getDevProxyExe(versionPreference);
19+
const configuration = vscode.workspace.getConfiguration('dev-proxy-toolkit');
20+
const customPath = configuration.get<string>('devProxyPath');
21+
const exeName = getDevProxyExe(versionPreference);
22+
const devProxyExe = await resolveDevProxyExecutable(exeName, customPath);
2023
const version = await getVersion(devProxyExe);
2124
const isInstalled = version !== '';
2225
const isBeta = version.includes('beta');

src/test/shell.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
* Tests for pure utility functions in shell.ts.
44
*/
55
import * as assert from 'assert';
6-
import { getPackageIdentifier } from '../utils/shell';
6+
import * as sinon from 'sinon';
7+
import * as fs from 'fs';
8+
import { getPackageIdentifier, resolveDevProxyExecutable } from '../utils/shell';
79
import {
810
PackageManager,
911
VersionPreference,
@@ -40,3 +42,47 @@ suite('getPackageIdentifier', () => {
4042
assert.strictEqual(result, undefined);
4143
});
4244
});
45+
46+
suite('resolveDevProxyExecutable', () => {
47+
let existsSyncStub: sinon.SinonStub;
48+
49+
setup(() => {
50+
existsSyncStub = sinon.stub(fs, 'existsSync');
51+
});
52+
53+
teardown(() => {
54+
sinon.restore();
55+
});
56+
57+
test('should return custom path when provided and non-empty', async () => {
58+
const customPath = '/custom/path/to/devproxy';
59+
const result = await resolveDevProxyExecutable('devproxy', customPath);
60+
assert.strictEqual(result, customPath);
61+
});
62+
63+
test('should trim whitespace from custom path', async () => {
64+
const customPath = ' /custom/path/to/devproxy ';
65+
const result = await resolveDevProxyExecutable('devproxy', customPath);
66+
assert.strictEqual(result, '/custom/path/to/devproxy');
67+
});
68+
69+
test('should ignore empty custom path and proceed with auto-detection', async () => {
70+
// With empty custom path and no auto-detection success, should return bare command
71+
existsSyncStub.returns(false);
72+
const result = await resolveDevProxyExecutable('devproxy', '');
73+
// Will fall through to bare command since nothing else succeeds
74+
assert.strictEqual(result, 'devproxy');
75+
});
76+
77+
test('should ignore whitespace-only custom path', async () => {
78+
existsSyncStub.returns(false);
79+
const result = await resolveDevProxyExecutable('devproxy', ' ');
80+
assert.strictEqual(result, 'devproxy');
81+
});
82+
83+
test('should handle undefined custom path', async () => {
84+
existsSyncStub.returns(false);
85+
const result = await resolveDevProxyExecutable('devproxy', undefined);
86+
assert.strictEqual(result, 'devproxy');
87+
});
88+
});

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export {
2525
getPackageIdentifier,
2626
upgradeDevProxyWithPackageManager,
2727
openUpgradeDocumentation,
28+
resolveDevProxyExecutable,
2829
} from './shell';
2930

3031
// Re-export from detect for convenience

src/utils/shell.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { exec, ExecOptions } from 'child_process';
22
import * as vscode from 'vscode';
3+
import * as os from 'os';
4+
import * as fs from 'fs';
35
import { Urls } from '../constants';
46
import {
57
HomebrewPackageIdentifier,
@@ -125,3 +127,99 @@ export async function upgradeDevProxyWithPackageManager(
125127
export function openUpgradeDocumentation(): void {
126128
vscode.env.openExternal(vscode.Uri.parse(Urls.upgradeDoc));
127129
}
130+
131+
/**
132+
* Common installation paths for Dev Proxy on different platforms.
133+
*/
134+
const COMMON_PATHS: Record<string, string[]> = {
135+
darwin: ['/opt/homebrew/bin', '/usr/local/bin'],
136+
linux: ['/usr/local/bin', '/home/linuxbrew/.linuxbrew/bin'],
137+
win32: [], // Windows typically has correct PATH from installer
138+
};
139+
140+
/**
141+
* Resolve the Dev Proxy executable path.
142+
*
143+
* Priority:
144+
* 1. Custom path from settings (if set and non-empty)
145+
* 2. Auto-detection:
146+
* a. Try bare command (devproxy/devproxy-beta)
147+
* b. Try via login shell (macOS/Linux only)
148+
* c. Try common installation paths
149+
*
150+
* @returns The resolved executable path, or the bare command name if not found
151+
*/
152+
export async function resolveDevProxyExecutable(
153+
exeName: string,
154+
customPath?: string
155+
): Promise<string> {
156+
// 1. Use custom path if provided
157+
if (customPath && customPath.trim() !== '') {
158+
return customPath.trim();
159+
}
160+
161+
// 2. Try bare command first
162+
if (await canExecute(exeName)) {
163+
return exeName;
164+
}
165+
166+
const platform = os.platform();
167+
168+
// 3. Try via login shell (macOS/Linux only)
169+
if (platform !== 'win32') {
170+
const loginShellPath = await tryLoginShell(exeName);
171+
if (loginShellPath) {
172+
return loginShellPath;
173+
}
174+
}
175+
176+
// 4. Try common installation paths
177+
const commonPaths = COMMON_PATHS[platform] || [];
178+
for (const dir of commonPaths) {
179+
const fullPath = `${dir}/${exeName}`;
180+
if (fs.existsSync(fullPath)) {
181+
return fullPath;
182+
}
183+
}
184+
185+
// Fallback to bare command (will fail gracefully in getVersion)
186+
return exeName;
187+
}
188+
189+
/**
190+
* Check if a command can be executed successfully.
191+
*/
192+
async function canExecute(cmd: string): Promise<boolean> {
193+
try {
194+
await executeCommand(`${cmd} --version`);
195+
return true;
196+
} catch {
197+
return false;
198+
}
199+
}
200+
201+
/**
202+
* Try to find the executable using a login shell.
203+
* This sources the user's shell profile which may include custom PATH entries.
204+
*/
205+
async function tryLoginShell(exeName: string): Promise<string | undefined> {
206+
const shells = ['/bin/zsh', '/bin/bash'];
207+
208+
for (const shell of shells) {
209+
if (!fs.existsSync(shell)) {
210+
continue;
211+
}
212+
213+
try {
214+
const result = await executeCommand(`${shell} -l -c "which ${exeName}"`);
215+
const path = result.trim();
216+
if (path && !path.includes('not found')) {
217+
return path;
218+
}
219+
} catch {
220+
// Shell failed, try next
221+
}
222+
}
223+
224+
return undefined;
225+
}

0 commit comments

Comments
 (0)