Skip to content

Commit ab3566a

Browse files
committed
feat(server): expose server info endpoint
1 parent b642c1d commit ab3566a

58 files changed

Lines changed: 508 additions & 209 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎V2_HTTP_API_AUDIT.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Review endpoints in document order. For each endpoint, select one disposition an
5454
### [x] `GET /api/health` and `GET /api/server`
5555

5656
- **Decision:** Merge and rename
57-
- **Replacement:** `GET /api/status` with operation ID `server.status`.
57+
- **Replacement:** `GET /api/info` with operation ID `server.info`.
5858
- **Notes:** Returns `version`, `pid`, and connection `urls`; readiness is conveyed by HTTP status.
5959

6060
### [x] `GET /api/project/current`
@@ -74,7 +74,7 @@ Review endpoints in document order. For each endpoint, select one disposition an
7474

7575
| Done | Method | Path | Operation ID | Decision | Notes |
7676
|---|---|---|---|---|---|
77-
| [x] 001–002 | `GET` | `/api/status` | `server.status` | Keep | Replaces the former health and server endpoints. |
77+
| [x] 001–002 | `GET` | `/api/info` | `server.info` | Keep | Replaces the former health and server endpoints. |
7878
| [x] 003 | `GET` | `/api/location` | `location.get` | Keep | Workspace selectors and response fields removed until workspace support ships. |
7979
| [x] 004 | `GET` | `/api/project` | `project.list` | Keep | Removed unused `time.initialized`; the database column remains for migration data. |
8080
| [x] 005 | `PATCH` | `/api/project/{projectID}` | `project.update` | Keep | Request and response accepted as-is. |

‎packages/app/e2e/regression/cross-server-tab-close.spec.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ async function mockServers(page: Page, requests: string[]) {
6262
const current = url.origin === serverA ? sessionA : sessionB
6363
const directory = url.searchParams.get("directory")
6464
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
65-
if (url.pathname === "/api/status") return json(route, { version: "test", pid: 1, urls: [url.origin] })
65+
if (url.pathname === "/api/info")
66+
return json(route, { version: "test", pid: 1, urls: [url.origin], paths: { tmp: "/tmp/opencode" } })
6667
if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
6768
if (url.pathname === "/api/session/active") return json(route, { data: {} })
6869
if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) })

‎packages/app/e2e/regression/server-dialog-focus.spec.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ test("server dialog keeps focus above fullscreen settings", async ({ page }) =>
1616
body: 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n',
1717
})
1818
}
19-
if (url.pathname === "/api/status") {
20-
return json(route, { version: "2.0.0", pid: 1, urls: [url.origin] })
19+
if (url.pathname === "/api/info") {
20+
return json(route, {
21+
version: "2.0.0",
22+
pid: 1,
23+
urls: [url.origin],
24+
paths: { tmp: "/tmp/opencode" },
25+
})
2126
}
2227
return json(route, {})
2328
})

‎packages/app/e2e/regression/session-summary-mcp.spec.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,14 @@ test("MCP authentication starts before a slow resource catalog finishes", async
138138
test("multiple desktop connections show the session's server name", async ({ page }) => {
139139
await mockStressTimeline(page)
140140
await page.route("http://secondary.test/**", (route) =>
141-
route.fulfill({ json: { version: "2.0.0", pid: 1, urls: ["http://secondary.test"] } }),
141+
route.fulfill({
142+
json: {
143+
version: "2.0.0",
144+
pid: 1,
145+
urls: ["http://secondary.test"],
146+
paths: { tmp: "/tmp/opencode" },
147+
},
148+
}),
142149
)
143150
await page.addInitScript(
144151
({ directory, server }) => {

‎packages/app/e2e/regression/session-timeline-transport.spec.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,11 @@ test("passes through non-event fetches", async ({ page }) => {
109109
const timeline = await setupTimeline(page)
110110

111111
const health = await page.evaluate(async () => {
112-
const response = await fetch("/api/status")
112+
const response = await fetch("/api/info")
113113
return response.json()
114114
})
115115

116-
expect(health).toEqual({ version: "2.0.0", pid: 1, urls: [] })
116+
expect(health).toEqual({ version: "2.0.0", pid: 1, urls: [], paths: { tmp: "/tmp/opencode" } })
117117
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
118118
})
119119

‎packages/app/e2e/service-worker/cache.spec.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ const fixture = test.extend<{ site: Site }, { builds: Record<string, Record<stri
8181
response.setHeader("cache-control", "no-store")
8282
if (path === "/observer.html")
8383
return void response.writeHead(200, { "content-type": "text/html" }).end("<title>Worker observer</title>")
84-
if (path === "/api/status")
84+
if (path === "/api/info")
8585
return void response
8686
.writeHead(200, { "content-type": "application/json" })
87-
.end(`{"version":"test","pid":1,"urls":["${url.origin}"]}`)
87+
.end(`{"version":"test","pid":1,"urls":["${url.origin}"],"paths":{"tmp":"/tmp/opencode"}}`)
8888
if (path === "/sw.js" && state.legacy && state.version === "old") {
8989
// Model the shipped worker's shared precache name and cache-first navigation behavior.
9090
const urls = Object.keys(builds.old).filter(
@@ -334,8 +334,13 @@ fixture("upgrades the legacy shared precache only after old tabs close", async (
334334

335335
fixture("does not substitute cached HTML for API or missing asset navigations", async ({ page, site }) => {
336336
await install(page, site.url)
337-
const api = await page.goto(`${site.url}/api/status`)
338-
expect(await api?.json()).toEqual({ version: "test", pid: 1, urls: ["http://localhost"] })
337+
const api = await page.goto(`${site.url}/api/info`)
338+
expect(await api?.json()).toEqual({
339+
version: "test",
340+
pid: 1,
341+
urls: ["http://localhost"],
342+
paths: { tmp: "/tmp/opencode" },
343+
})
339344
expect(api?.fromServiceWorker()).toBe(false)
340345
const asset = await page.goto(`${site.url}/_assets/missing.js`)
341346
expect(asset?.status()).toBe(404)

‎packages/app/e2e/utils/mock-api.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class MockBadRequest extends Schema.TaggedError<MockBadRequest>()("MockBa
3333
}) {}
3434

3535
const Group = HttpApiGroup.make("mock")
36-
.add(HttpApiEndpoint.get("status", "/api/status", { success: Json }))
36+
.add(HttpApiEndpoint.get("info", "/api/info", { success: Json }))
3737
.add(
3838
HttpApiEndpoint.get("event", "/api/event", {
3939
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),

‎packages/app/e2e/utils/mock-server.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,13 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map<string, st
219219
}),
220220
)
221221
.handleAll({
222-
status: () => Effect.succeed({ version: "2.0.0", pid: 1, urls: config.server ? [config.server] : [] }),
222+
info: () =>
223+
Effect.succeed({
224+
version: "2.0.0",
225+
pid: 1,
226+
urls: config.server ? [config.server] : [],
227+
paths: { tmp: "/tmp/opencode" },
228+
}),
223229
config: () => Effect.succeed(configEntries),
224230
reference: () =>
225231
Effect.succeed({

‎packages/app/src/runtime/server/client.test.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,31 +90,36 @@ test("rotates HTTP and PTY clients together", async () => {
9090
const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
9191
const request = input instanceof Request ? input : new Request(input, init)
9292
requests.push({ url: request.url, authorization: request.headers.get("authorization") })
93-
return Response.json({ version: "2.0.0-test", pid: 1, urls: [request.url] })
93+
return Response.json({
94+
version: "2.0.0-test",
95+
pid: 1,
96+
urls: [request.url],
97+
paths: { tmp: "/tmp/opencode" },
98+
})
9499
}) as typeof globalThis.fetch
95100
const transport = createServerTransport({
96101
http: { url: "http://127.0.0.1:4100", password: "first" },
97102
fetch,
98103
})
99104
const initialPty = transport.pty
100105

101-
await transport.api.server.status()
106+
await transport.api.server.info()
102107
const replacement = transport.update({
103108
url: "http://127.0.0.1:4200",
104109
password: "second",
105110
})
106-
await transport.api.server.status()
111+
await transport.api.server.info()
107112

108113
expect(replacement).toBe(transport.api)
109114
expect(transport.pty).not.toBe(initialPty)
110115
expect(transport.url).toBe("http://127.0.0.1:4200")
111116
expect(requests).toEqual([
112117
{
113-
url: "http://127.0.0.1:4100/api/status",
118+
url: "http://127.0.0.1:4100/api/info",
114119
authorization: `Basic ${btoa("opencode:first")}`,
115120
},
116121
{
117-
url: "http://127.0.0.1:4200/api/status",
122+
url: "http://127.0.0.1:4200/api/info",
118123
authorization: `Basic ${btoa("opencode:second")}`,
119124
},
120125
])

‎packages/app/src/runtime/server/health.test.ts‎

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ describe("checkServerHealth", () => {
1717
const headers: Array<string | null> = []
1818
const fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
1919
headers.push(new Headers(init?.headers).get("authorization"))
20-
return Response.json({ version: "2.0.0", pid: 1, urls: [server.url] })
20+
return Response.json({
21+
version: "2.0.0",
22+
pid: 1,
23+
urls: [server.url],
24+
paths: { tmp: "/tmp/opencode" },
25+
})
2126
}) as typeof globalThis.fetch
2227

2328
expect(await checkServerHealth({ ...server, password }, fetch)).toEqual({ healthy: true, version: "2.0.0" })
@@ -28,16 +33,19 @@ describe("checkServerHealth", () => {
2833
let request: URL | undefined
2934
const fetch = (async (input: RequestInfo | URL) => {
3035
request = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
31-
return new Response(JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url] }), {
32-
status: 200,
33-
headers: { "content-type": "application/json" },
34-
})
36+
return new Response(
37+
JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url], paths: { tmp: "/tmp/opencode" } }),
38+
{
39+
status: 200,
40+
headers: { "content-type": "application/json" },
41+
},
42+
)
3543
}) as unknown as typeof globalThis.fetch
3644

3745
const result = await checkServerHealth(server, fetch)
3846

3947
expect(result).toEqual({ healthy: true, version: "1.2.3" })
40-
expect(request?.pathname).toBe("/api/status")
48+
expect(request?.pathname).toBe("/api/info")
4149
})
4250

4351
test("allows slow servers thirty seconds by default", async () => {
@@ -52,7 +60,7 @@ describe("checkServerHealth", () => {
5260
})
5361

5462
const fetch = (async () =>
55-
new Response(JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url] }), {
63+
new Response(JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url], paths: { tmp: "/tmp/opencode" } }), {
5664
status: 200,
5765
headers: { "content-type": "application/json" },
5866
})) as unknown as typeof globalThis.fetch
@@ -111,10 +119,13 @@ describe("checkServerHealth", () => {
111119
let signal: AbortSignal | undefined
112120
const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
113121
signal = abortFromInput(input, init)
114-
return new Response(JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url] }), {
115-
status: 200,
116-
headers: { "content-type": "application/json" },
117-
})
122+
return new Response(
123+
JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url], paths: { tmp: "/tmp/opencode" } }),
124+
{
125+
status: 200,
126+
headers: { "content-type": "application/json" },
127+
},
128+
)
118129
}) as unknown as typeof globalThis.fetch
119130

120131
const abort = new AbortController()
@@ -130,10 +141,13 @@ describe("checkServerHealth", () => {
130141
const fetch = (async () => {
131142
count += 1
132143
if (count < 3) throw new TypeError("network")
133-
return new Response(JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url] }), {
134-
status: 200,
135-
headers: { "content-type": "application/json" },
136-
})
144+
return new Response(
145+
JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url], paths: { tmp: "/tmp/opencode" } }),
146+
{
147+
status: 200,
148+
headers: { "content-type": "application/json" },
149+
},
150+
)
137151
}) as unknown as typeof globalThis.fetch
138152

139153
const result = await checkServerHealth(server, fetch, {

0 commit comments

Comments
 (0)