Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ The following command line options are available for the `start` command:
| Option | Description | Default | Alias |
| -------------- | ----------------------------------------------------------------------------- | ---------- | ----- |
| --port | Port to listen on | 4141 | -p |
| --listen-addr | Address to bind the server to (for example 127.0.0.1, 0.0.0.0, or ::1) | all interfaces | none |
| --verbose | Enable verbose logging | false | -v |
| --account-type | Account type to use (individual, business, enterprise) | individual | -a |
| --manual | Enable manual request approval | false | none |
Expand Down Expand Up @@ -220,6 +221,9 @@ npx copilot-api@latest start
# Run on custom port with verbose logging
npx copilot-api@latest start --port 8080 --verbose

# Bind to localhost only instead of all interfaces
npx copilot-api@latest start --port 8080 --listen-addr 127.0.0.1

# Use with a business plan GitHub account
npx copilot-api@latest start --account-type business

Expand Down
26 changes: 25 additions & 1 deletion src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { server } from "./server"

interface RunServerOptions {
port: number
listenAddr?: string
verbose: boolean
accountType: string
manual: boolean
Expand All @@ -27,6 +28,22 @@ interface RunServerOptions {
proxyEnv: boolean
}

function isAllInterfacesListenAddr(listenAddr: string): boolean {
return ["::", "[::]", "0.0.0.0"].includes(listenAddr)
}

function formatHostForUrl(host: string): string {
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host
}

export function getServerUrl(port: number, listenAddr?: string): string {
if (!listenAddr || isAllInterfacesListenAddr(listenAddr)) {
return `http://localhost:${port}`
}

return `http://${formatHostForUrl(listenAddr)}:${port}`
}

export async function runServer(options: RunServerOptions): Promise<void> {
if (options.proxyEnv) {
initProxyFromEnv()
Expand Down Expand Up @@ -64,7 +81,7 @@ export async function runServer(options: RunServerOptions): Promise<void> {
`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`,
)

const serverUrl = `http://localhost:${options.port}`
const serverUrl = getServerUrl(options.port, options.listenAddr)

if (options.claudeCode) {
invariant(state.models, "Models should be loaded by now")
Expand Down Expand Up @@ -117,6 +134,7 @@ export async function runServer(options: RunServerOptions): Promise<void> {
serve({
fetch: server.fetch as ServerHandler,
port: options.port,
hostname: options.listenAddr,
})
}

Expand All @@ -132,6 +150,11 @@ export const start = defineCommand({
default: "4141",
description: "Port to listen on",
},
"listen-addr": {
type: "string",
description:
"Address to bind the server to (for example 127.0.0.1, 0.0.0.0, or ::1)",
},
verbose: {
alias: "v",
type: "boolean",
Expand Down Expand Up @@ -193,6 +216,7 @@ export const start = defineCommand({

return runServer({
port: Number.parseInt(args.port, 10),
listenAddr: args["listen-addr"],
verbose: args.verbose,
accountType: args["account-type"],
manual: args.manual,
Expand Down
26 changes: 26 additions & 0 deletions tests/start.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, test } from "bun:test"

import { getServerUrl } from "../src/start"

describe("getServerUrl", () => {
test("uses localhost when listen address is omitted", () => {
expect(getServerUrl(4141)).toBe("http://localhost:4141")
})

test("uses localhost when listening on all IPv4 interfaces", () => {
expect(getServerUrl(4141, "0.0.0.0")).toBe("http://localhost:4141")
})

test("uses localhost when listening on all IPv6 interfaces", () => {
expect(getServerUrl(4141, "::")).toBe("http://localhost:4141")
expect(getServerUrl(4141, "[::]")).toBe("http://localhost:4141")
})

test("uses the configured address for loopback IPv4", () => {
expect(getServerUrl(4141, "127.0.0.1")).toBe("http://127.0.0.1:4141")
})

test("formats IPv6 addresses correctly", () => {
expect(getServerUrl(4141, "::1")).toBe("http://[::1]:4141")
})
})