Skip to content

Commit 3aa33b3

Browse files
authored
Easy local backend env var (#3116)
* feat: add support for local backend URL override Add KILOCODE_BACKEND_BASE_URL environment variable to enable testing against a local Kilo Code backend. Refactor hardcoded API URLs across providers, webview, and CLI to use getKiloUrl() and getKiloUrlFromToken() functions for dynamic URL resolution. Include new launch configuration and documentation updates for local development workflow. * Clean and fixes * more cleanup * Flip the fn params * refactor(shared): replace getKiloUrl with getApiUrl and getAppUrl Replace the generic getKiloUrl function with two specific functions: - getApiUrl for API endpoints (e.g., /extension-config.json) - getAppUrl for app/web URLs (e.g., /profile, /support) This improves type safety and clarity by separating concerns between API and app URL generation. BREAKING CHANGE: getKiloUrl function removed, use getApiUrl or getAppUrl instead * refactor(shared): centralize kilocode URL utilities in @roo-code/types Move getApiUrl, getAppUrl, and getKiloUrlFromToken functions from src/shared/kilocode/ to packages/types/src/kilocode/kilocode.ts and update all imports across the codebase. This consolidates shared utilities into the types package, reducing duplication and improving maintainability. * refactor(types): add getExtensionConfigUrl and refactor URL utilities - Introduce getExtensionConfigUrl function for extension config endpoint - Refactor getApiUrl and getAppUrl to use shared buildUrl helper - Update tests to cover new function and maintain backwards compatibility - Use getExtensionConfigUrl in Gemini CLI provider instead of hardcoded URL * Address PR comments * Cleanup and revert post main-merge * Temporarily disable flakey autocomplete tests * ci: prevent API rate limit exceeded for actions/setup-java@v4 Add check-latest: false and token configuration to setup-java steps in code-qa.yml and marketplace-publish.yml. * 'chore: WIP'
1 parent fb96ff9 commit 3aa33b3

File tree

26 files changed

+427
-68
lines changed

26 files changed

+427
-68
lines changed

.github/workflows/code-qa.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ jobs:
162162
with:
163163
distribution: 'jetbrains'
164164
java-version: '21'
165+
check-latest: false
166+
token: ${{ secrets.GITHUB_TOKEN }}
165167
- name: Install system dependencies
166168
run: |
167169
sudo apt-get update
@@ -209,4 +211,4 @@ jobs:
209211
run: pnpm install
210212
- name: Run unit tests
211213
working-directory: cli
212-
run: pnpm test
214+
run: pnpm test

.github/workflows/marketplace-publish.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ jobs:
134134
with:
135135
distribution: "jetbrains"
136136
java-version: "21"
137+
check-latest: false
138+
token: ${{ secrets.GITHUB_TOKEN }}
137139
- name: Install system dependencies
138140
run: |
139141
sudo apt-get update

.vscode/launch.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@
4444
},
4545
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
4646
"presentation": { "hidden": false, "group": "tasks", "order": 1 }
47+
},
48+
{
49+
"name": "Run Extension [Local Backend]",
50+
"type": "extensionHost",
51+
"request": "launch",
52+
"runtimeExecutable": "${execPath}",
53+
"args": ["--extensionDevelopmentPath=${workspaceFolder}/src", "--disable-extensions"],
54+
"sourceMaps": true,
55+
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
56+
"preLaunchTask": "${defaultBuildTask}",
57+
"env": {
58+
"NODE_ENV": "development",
59+
"VSCODE_DEBUG_MODE": "true",
60+
"KILOCODE_BACKEND_BASE_URL": "${input:kilocodeBackendBaseUrl}"
61+
},
62+
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
63+
"presentation": { "hidden": false, "group": "tasks", "order": 2 }
4764
}
4865
],
4966
"inputs": [
@@ -52,6 +69,12 @@
5269
"description": "Directory the dev extension will open in",
5370
"default": "${workspaceFolder}/launch",
5471
"type": "promptString"
72+
},
73+
{
74+
"id": "kilocodeBackendBaseUrl",
75+
"description": "Override the kilocode backend base URL",
76+
"default": "http://localhost:3000",
77+
"type": "promptString"
5578
}
5679
]
5780
}

DEVELOPMENT.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,18 @@ These hooks help maintain code quality and consistency. If you encounter issues
248248
- Check the Output panel in VSCode (View > Output) and select "Kilo Code" from the dropdown
249249
- For webview issues, use the browser developer tools in the webview (right-click > "Inspect Element")
250250
251+
### Testing with Local Backend
252+
253+
To test the extension against a local Kilo Code backend:
254+
255+
1. **Set up your local backend** at `http://localhost:3000`
256+
2. **Use the "Run Extension [Local Backend]" launch configuration**:
257+
- Go to Run and Debug (Ctrl+Shift+D)
258+
- Select "Run Extension [Local Backend]" from the dropdown
259+
- Press F5 to start debugging
260+
261+
This automatically sets the `KILOCODE_BACKEND_BASE_URL` environment variable, making all sign-in/sign-up buttons point to your local backend instead of production.
262+
251263
## Contributing
252264
253265
We welcome contributions to Kilo Code! Here's how you can help:

cli/src/services/telemetry/identity.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as crypto from "crypto"
99
import * as os from "os"
1010
import { KiloCodePaths } from "../../utils/paths.js"
1111
import { logs } from "../logs.js"
12+
import { getApiUrl } from "@roo-code/types"
1213

1314
/**
1415
* User identity structure
@@ -107,7 +108,7 @@ export class IdentityManager {
107108

108109
try {
109110
// Fetch user profile from Kilocode API
110-
const response = await fetch("https://api.kilocode.ai/api/profile", {
111+
const response = await fetch(getApiUrl("/profile"), {
111112
headers: {
112113
Authorization: `Bearer ${kilocodeToken}`,
113114
"Content-Type": "application/json",

packages/types/src/__tests__/kilocode.test.ts

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
// npx vitest run src/__tests__/kilocode.test.ts
22

33
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
4-
import { ghostServiceSettingsSchema, checkKilocodeBalance } from "../kilocode/kilocode.js"
4+
import {
5+
ghostServiceSettingsSchema,
6+
checkKilocodeBalance,
7+
getApiUrl,
8+
getAppUrl,
9+
getKiloUrlFromToken,
10+
getExtensionConfigUrl,
11+
} from "../kilocode/kilocode.js"
512

613
describe("ghostServiceSettingsSchema", () => {
714
describe("autoTriggerDelay", () => {
@@ -205,3 +212,199 @@ describe("checkKilocodeBalance", () => {
205212
expect(result).toBe(false)
206213
})
207214
})
215+
216+
describe("URL functions", () => {
217+
const originalEnv = process.env.KILOCODE_BACKEND_BASE_URL
218+
219+
// Helper functions to create properly formatted test tokens
220+
const createDevToken = () => {
221+
const payload = { env: "development" }
222+
return `header.${btoa(JSON.stringify(payload))}.signature`
223+
}
224+
225+
const createProdToken = () => {
226+
const payload = {}
227+
return `header.${btoa(JSON.stringify(payload))}.signature`
228+
}
229+
230+
afterEach(() => {
231+
// Reset environment variable after each test
232+
if (originalEnv) {
233+
process.env.KILOCODE_BACKEND_BASE_URL = originalEnv
234+
} else {
235+
delete process.env.KILOCODE_BACKEND_BASE_URL
236+
}
237+
})
238+
239+
describe("getExtensionConfigUrl", () => {
240+
it("should use path structure for development", () => {
241+
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"
242+
expect(getExtensionConfigUrl()).toBe("http://localhost:3000/extension-config.json")
243+
})
244+
it("should use subdomain structure for production", () => {
245+
expect(getExtensionConfigUrl()).toBe("https://api.kilocode.ai/extension-config.json")
246+
})
247+
})
248+
249+
describe("getApiUrl", () => {
250+
it("should handle production URLs correctly", () => {
251+
// API URLs using /api path structure
252+
expect(getApiUrl("/extension-config.json")).toBe("https://kilocode.ai/api/extension-config.json")
253+
expect(getApiUrl("/marketplace/modes")).toBe("https://kilocode.ai/api/marketplace/modes")
254+
expect(getApiUrl("/marketplace/mcps")).toBe("https://kilocode.ai/api/marketplace/mcps")
255+
expect(getApiUrl("/profile/balance")).toBe("https://kilocode.ai/api/profile/balance")
256+
expect(getApiUrl()).toBe("https://kilocode.ai/api")
257+
})
258+
259+
it("should handle development environment", () => {
260+
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"
261+
262+
expect(getApiUrl("/extension-config.json")).toBe("http://localhost:3000/api/extension-config.json")
263+
expect(getApiUrl("/marketplace/modes")).toBe("http://localhost:3000/api/marketplace/modes")
264+
expect(getApiUrl("/marketplace/mcps")).toBe("http://localhost:3000/api/marketplace/mcps")
265+
expect(getApiUrl()).toBe("http://localhost:3000/api")
266+
})
267+
268+
it("should handle paths without leading slash", () => {
269+
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"
270+
expect(getApiUrl("extension-config.json")).toBe("http://localhost:3000/api/extension-config.json")
271+
})
272+
273+
it("should handle empty and root paths", () => {
274+
expect(getApiUrl("")).toBe("https://kilocode.ai/api")
275+
expect(getApiUrl("/")).toBe("https://kilocode.ai/api/")
276+
})
277+
})
278+
279+
describe("getAppUrl", () => {
280+
it("should handle production URLs correctly", () => {
281+
expect(getAppUrl()).toBe("https://kilocode.ai")
282+
expect(getAppUrl("/profile")).toBe("https://kilocode.ai/profile")
283+
expect(getAppUrl("/support")).toBe("https://kilocode.ai/support")
284+
expect(getAppUrl("/sign-in-to-editor")).toBe("https://kilocode.ai/sign-in-to-editor")
285+
})
286+
287+
it("should handle development environment", () => {
288+
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"
289+
290+
expect(getAppUrl()).toBe("http://localhost:3000")
291+
expect(getAppUrl("/profile")).toBe("http://localhost:3000/profile")
292+
expect(getAppUrl("/support")).toBe("http://localhost:3000/support")
293+
})
294+
295+
it("should handle paths without leading slash", () => {
296+
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"
297+
expect(getAppUrl("profile")).toBe("http://localhost:3000/profile")
298+
})
299+
300+
it("should handle empty and root paths", () => {
301+
expect(getAppUrl("")).toBe("https://kilocode.ai")
302+
expect(getAppUrl("/")).toBe("https://kilocode.ai")
303+
})
304+
})
305+
306+
describe("getKiloUrlFromToken", () => {
307+
it("should handle production token URLs correctly", () => {
308+
const prodToken = createProdToken()
309+
310+
// Token-based URLs using api.kilocode.ai subdomain
311+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile", prodToken)).toBe(
312+
"https://api.kilocode.ai/api/profile",
313+
)
314+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile/balance", prodToken)).toBe(
315+
"https://api.kilocode.ai/api/profile/balance",
316+
)
317+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/organizations/123/defaults", prodToken)).toBe(
318+
"https://api.kilocode.ai/api/organizations/123/defaults",
319+
)
320+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/openrouter/", prodToken)).toBe(
321+
"https://api.kilocode.ai/api/openrouter/",
322+
)
323+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/users/notifications", prodToken)).toBe(
324+
"https://api.kilocode.ai/api/users/notifications",
325+
)
326+
})
327+
328+
it("should map development tokens to localhost correctly", () => {
329+
const devToken = createDevToken()
330+
331+
// Development token should map to localhost:3000
332+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile", devToken)).toBe(
333+
"http://localhost:3000/api/profile",
334+
)
335+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile/balance", devToken)).toBe(
336+
"http://localhost:3000/api/profile/balance",
337+
)
338+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/organizations/456/defaults", devToken)).toBe(
339+
"http://localhost:3000/api/organizations/456/defaults",
340+
)
341+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/openrouter/", devToken)).toBe(
342+
"http://localhost:3000/api/openrouter/",
343+
)
344+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/users/notifications", devToken)).toBe(
345+
"http://localhost:3000/api/users/notifications",
346+
)
347+
})
348+
349+
it("should handle invalid tokens gracefully", () => {
350+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
351+
// Use a token that looks like JWT but has invalid JSON payload
352+
const result = getKiloUrlFromToken("https://api.kilocode.ai/api/test", "header.invalid-json.signature")
353+
expect(result).toBe("https://api.kilocode.ai/api/test")
354+
expect(consoleSpy).toHaveBeenCalledWith("Failed to get base URL from Kilo Code token")
355+
consoleSpy.mockRestore()
356+
})
357+
})
358+
359+
describe("Real-world URL patterns from application", () => {
360+
it("should correctly handle marketplace endpoints", () => {
361+
// These are the actual endpoints used in RemoteConfigLoader
362+
expect(getApiUrl("/marketplace/modes")).toBe("https://kilocode.ai/api/marketplace/modes")
363+
expect(getApiUrl("/marketplace/mcps")).toBe("https://kilocode.ai/api/marketplace/mcps")
364+
})
365+
366+
it("should correctly handle app navigation URLs", () => {
367+
// These are the actual URLs used in Task.ts and webviewMessageHandler.ts
368+
expect(getAppUrl("/profile")).toBe("https://kilocode.ai/profile")
369+
expect(getAppUrl("/support")).toBe("https://kilocode.ai/support")
370+
})
371+
372+
it("should correctly handle token-based API calls", () => {
373+
// These are the actual API endpoints used throughout the application
374+
const prodToken = createProdToken()
375+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile", prodToken)).toBe(
376+
"https://api.kilocode.ai/api/profile",
377+
)
378+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile/balance", prodToken)).toBe(
379+
"https://api.kilocode.ai/api/profile/balance",
380+
)
381+
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/users/notifications", prodToken)).toBe(
382+
"https://api.kilocode.ai/api/users/notifications",
383+
)
384+
})
385+
386+
it("should maintain backwards compatibility for legacy endpoints", () => {
387+
expect(getExtensionConfigUrl()).toBe("https://api.kilocode.ai/extension-config.json")
388+
expect(getApiUrl("/extension-config.json")).toBe("https://kilocode.ai/api/extension-config.json")
389+
expect(getApiUrl("/extension-config.json")).not.toBe(getExtensionConfigUrl())
390+
})
391+
})
392+
393+
describe("Edge cases and error handling", () => {
394+
it("should handle various localhost configurations", () => {
395+
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:8080"
396+
expect(getApiUrl("/test")).toBe("http://localhost:8080/api/test")
397+
398+
process.env.KILOCODE_BACKEND_BASE_URL = "http://127.0.0.1:3000"
399+
expect(getApiUrl("/test")).toBe("http://127.0.0.1:3000/api/test")
400+
})
401+
402+
it("should handle custom backend URLs", () => {
403+
process.env.KILOCODE_BACKEND_BASE_URL = "https://staging.example.com"
404+
405+
expect(getAppUrl()).toBe("https://staging.example.com")
406+
expect(getApiUrl("/test")).toBe("https://staging.example.com/api/test")
407+
expect(getAppUrl("/dashboard")).toBe("https://staging.example.com/dashboard")
408+
})
409+
})
410+
})

0 commit comments

Comments
 (0)