Skip to content

Commit 08c83d4

Browse files
feat: add cloudflared custom path config to dashboard settings UI #47
1 parent 9b560fa commit 08c83d4

3 files changed

Lines changed: 81 additions & 15 deletions

File tree

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@
299299
"type": "boolean",
300300
"default": true,
301301
"description": "Enable or disable MCP support globally."
302+
},
303+
"githubCopilotApi.tunnel.cloudflaredPath": {
304+
"type": "string",
305+
"default": "",
306+
"description": "Optional custom path to the cloudflared executable. If empty, the extension will attempt to use cloudflared from your system PATH, or download it automatically."
302307
}
303308
}
304309
}

src/CopilotApiGateway.ts

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface ApiServerConfig {
7373
maxPayloadSizeMb: number
7474
maxConnectionsPerIp: number
7575
mcpEnabled: boolean
76+
cloudflaredPath: string
7677
}
7778

7879
// Request history entry
@@ -951,6 +952,10 @@ export class CopilotApiGateway implements vscode.Disposable {
951952
await this.updateServerConfig({ maxConnectionsPerIp: normalized });
952953
}
953954

955+
public async setCloudflaredPath(path: string): Promise<void> {
956+
await this.updateServerConfig({ cloudflaredPath: path });
957+
}
958+
954959
public async setMaxConcurrency(limit: number): Promise<void> {
955960
const normalized = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 4;
956961
await this.updateServerConfig({ maxConcurrentRequests: normalized });
@@ -1005,25 +1010,52 @@ export class CopilotApiGateway implements vscode.Disposable {
10051010
fs.mkdirSync(globalStoragePath, { recursive: true });
10061011
}
10071012

1008-
const cloudflaredBin = path.join(
1009-
globalStoragePath,
1010-
process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared'
1011-
);
1013+
let cloudflaredBin = '';
1014+
const customPath = this.config.cloudflaredPath;
10121015

1013-
// Download the binary if it doesn't exist
1014-
if (!fs.existsSync(cloudflaredBin)) {
1015-
this.logInfo('Cloudflared binary not found, downloading...');
1016+
// 1. Check custom configured path
1017+
if (customPath && fs.existsSync(customPath)) {
1018+
this.logInfo(`Using custom cloudflared path from configuration: ${customPath}`);
1019+
cloudflaredBin = customPath;
1020+
}
1021+
// 2. Check system PATH
1022+
else {
1023+
const { execSync } = await import('child_process');
10161024
try {
1017-
await install(cloudflaredBin);
1018-
this.logInfo('Cloudflared binary downloaded successfully');
1019-
} catch (downloadError) {
1020-
const errMsg = downloadError instanceof Error ? downloadError.message : String(downloadError);
1021-
this.logError('Failed to download cloudflared binary', downloadError);
1022-
return { success: false, error: `Failed to download cloudflared: ${errMsg}. Check your internet connection.` };
1025+
const sysPath = execSync(process.platform === 'win32' ? 'where cloudflared' : 'which cloudflared').toString().split('\n')[0].trim();
1026+
if (sysPath && fs.existsSync(sysPath)) {
1027+
this.logInfo(`Found cloudflared in system PATH: ${sysPath}`);
1028+
cloudflaredBin = sysPath;
1029+
}
1030+
} catch (e) {
1031+
// cloudflared not in PATH
1032+
}
1033+
}
1034+
1035+
// 3. Fallback to downloading or using cached binary
1036+
if (!cloudflaredBin) {
1037+
cloudflaredBin = path.join(
1038+
globalStoragePath,
1039+
process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared'
1040+
);
1041+
1042+
if (!fs.existsSync(cloudflaredBin)) {
1043+
this.logInfo('Cloudflared binary not found locally or in PATH, downloading...');
1044+
try {
1045+
await install(cloudflaredBin);
1046+
this.logInfo('Cloudflared binary downloaded successfully');
1047+
} catch (downloadError) {
1048+
const errMsg = downloadError instanceof Error ? downloadError.message : String(downloadError);
1049+
this.logError('Failed to download cloudflared binary', downloadError);
1050+
return { success: false, error: `Failed to download cloudflared: ${errMsg}. Check your internet connection.` };
1051+
}
1052+
} else {
1053+
this.logInfo(`Using cached cloudflared binary: ${cloudflaredBin}`);
10231054
}
1055+
} else {
1056+
this.logInfo(`Using chosen cloudflared binary: ${cloudflaredBin}`);
10241057
}
10251058

1026-
this.logInfo(`Using cloudflared binary: ${cloudflaredBin}`);
10271059
use(cloudflaredBin);
10281060

10291061
const localUrl = `http://${this.config.host === '0.0.0.0' ? '127.0.0.1' : this.config.host}:${this.config.port}`;
@@ -5404,6 +5436,9 @@ export class CopilotApiGateway implements vscode.Disposable {
54045436
if (patch.maxConnectionsPerIp !== undefined) {
54055437
updates.push(Promise.resolve(config.update('server.maxConnectionsPerIp', patch.maxConnectionsPerIp, vscode.ConfigurationTarget.Global)));
54065438
}
5439+
if (patch.cloudflaredPath !== undefined) {
5440+
updates.push(Promise.resolve(config.update('tunnel.cloudflaredPath', patch.cloudflaredPath, vscode.ConfigurationTarget.Global)));
5441+
}
54075442
if (patch.redactionPatterns !== undefined) {
54085443
updates.push(Promise.resolve(config.update('server.redactionPatterns', patch.redactionPatterns, vscode.ConfigurationTarget.Global)));
54095444
}
@@ -5633,12 +5668,13 @@ function getServerConfig(): ApiServerConfig {
56335668
const maxPayloadSizeMb = configuration.get<number>('server.maxPayloadSizeMb', 1);
56345669
const maxConnectionsPerIp = configuration.get<number>('server.maxConnectionsPerIp', 10);
56355670
const mcpEnabled = vscode.workspace.getConfiguration('githubCopilotApi.mcp').get<boolean>('enabled', true);
5671+
const cloudflaredPath = vscode.workspace.getConfiguration('githubCopilotApi.tunnel').get<string>('cloudflaredPath', '').trim();
56365672

56375673
return {
56385674
enabled, enableHttp, enableWebSocket, enableHttps, tlsCertPath, tlsKeyPath, host, port, maxConcurrentRequests,
56395675
defaultModel, apiKey, enableLogging, rateLimitPerMinute, defaultSystemPrompt,
56405676
redactionPatterns, ipAllowlist, requestTimeoutSeconds, maxPayloadSizeMb, maxConnectionsPerIp,
5641-
mcpEnabled
5677+
mcpEnabled, cloudflaredPath
56425678
};
56435679
}
56445680

src/CopilotPanel.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,15 @@ for await (const chunk of stream) {
718718
});
719719
}
720720
break;
721+
case 'setCloudflaredPath':
722+
if (typeof data.value === 'string') {
723+
void gateway.setCloudflaredPath(data.value).then(async () => {
724+
if (CopilotPanel.currentPanel) {
725+
CopilotPanel.currentPanel.webview.html = await CopilotPanel.getPanelHtml(CopilotPanel.currentPanel.webview, gateway);
726+
}
727+
});
728+
}
729+
break;
721730
case 'setMaxConcurrency':
722731
if (typeof data.value === 'number') {
723732
void gateway.setMaxConcurrency(data.value).then(async () => {
@@ -2103,6 +2112,14 @@ print(response.choices[0].message.content)\`;
21032112
</ul>
21042113
</div>
21052114
2115+
<div style="margin-bottom: 16px; border-top: 1px solid var(--vscode-widget-border); padding-top: 16px;">
2116+
<div style="font-size: 11px; font-weight: 600; margin-bottom: 8px; opacity: 0.8;">CLOUDFLARED BINARY PATH (OPTIONAL)</div>
2117+
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
2118+
<input type="text" id="cloudflared-path-input" value="${config.cloudflaredPath || ''}" placeholder="Leave empty for auto-download or PATH" style="flex: 1; min-width: 200px;">
2119+
<button class="secondary" id="btn-set-cloudflared-path" style="width: auto; padding: 4px 12px; font-size: 11px;">Save Path</button>
2120+
</div>
2121+
</div>
2122+
21062123
<div id="tunnel-status-area" style="margin-bottom: 16px;">
21072124
${status.tunnel?.running ? (status.tunnel?.url ? `
21082125
<div style="background: color-mix(in srgb, var(--vscode-testing-iconPassed) 15%, transparent); border: 1px solid var(--vscode-testing-iconPassed); border-radius: 8px; padding: 16px;">
@@ -2755,6 +2772,14 @@ print(response.choices[0].message.content)\`;
27552772
vscode.postMessage({ type: 'setMaxConnectionsPerIp', value: Number(val) });
27562773
};
27572774
}
2775+
2776+
const btnSetCloudflaredPath = document.getElementById('btn-set-cloudflared-path');
2777+
if (btnSetCloudflaredPath) {
2778+
btnSetCloudflaredPath.onclick = function() {
2779+
var val = document.getElementById('cloudflared-path-input').value;
2780+
vscode.postMessage({ type: 'setCloudflaredPath', value: String(val).trim() });
2781+
};
2782+
}
27582783
document.getElementById('btn-set-concurrency').onclick = function() {
27592784
var val = document.getElementById('concurrency-input').value;
27602785
vscode.postMessage({ type: 'setMaxConcurrency', value: Number(val) });

0 commit comments

Comments
 (0)