@neuralnomads/codenomad-dev 0.10.3-dev-20260213-ba418a85 → 0.10.3-dev-20260213-e9f281a6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/package.json +1 -1
  2. package/public/apple-touch-icon-180x180.png +0 -0
  3. package/public/assets/{main-CSlDZj4f.js → main-crtt5pqm.js} +82 -80
  4. package/public/index.html +1 -1
  5. package/public/sw.js +1 -1
  6. package/public/ui-version.json +1 -1
  7. package/dist/integrations/github/bot-signature.js +0 -11
  8. package/dist/integrations/github/git-ops.js +0 -133
  9. package/dist/integrations/github/github-types.js +0 -1
  10. package/dist/integrations/github/job-runner.js +0 -608
  11. package/dist/integrations/github/octokit.js +0 -58
  12. package/dist/integrations/github/sanitize-webhook.js +0 -42
  13. package/dist/integrations/github/webhook-verify.js +0 -21
  14. package/dist/integrations/github/workspace-context.js +0 -10
  15. package/dist/integrations/github/worktree-context.js +0 -15
  16. package/dist/opencode/request-context.js +0 -39
  17. package/dist/opencode/worktree-directory.js +0 -42
  18. package/dist/opencode-config-template/README.md +0 -32
  19. package/dist/opencode-config-template/opencode.jsonc +0 -3
  20. package/dist/opencode-config-template/plugin/codenomad.ts +0 -40
  21. package/dist/opencode-config-template/plugin/lib/background-process.ts +0 -160
  22. package/dist/opencode-config-template/plugin/lib/client.ts +0 -165
  23. package/dist/server/routes/github-plugin.js +0 -215
  24. package/dist/server/routes/github-webhook.js +0 -32
  25. package/scripts/copy-auth-pages.mjs +0 -22
  26. package/scripts/copy-opencode-config.mjs +0 -61
  27. package/scripts/copy-ui-dist.mjs +0 -21
  28. package/src/api-types.ts +0 -326
  29. package/src/auth/auth-store.ts +0 -175
  30. package/src/auth/http-auth.ts +0 -38
  31. package/src/auth/manager.ts +0 -163
  32. package/src/auth/password-hash.ts +0 -49
  33. package/src/auth/session-manager.ts +0 -23
  34. package/src/auth/token-manager.ts +0 -32
  35. package/src/background-processes/manager.ts +0 -519
  36. package/src/bin.ts +0 -29
  37. package/src/config/binaries.ts +0 -192
  38. package/src/config/location.ts +0 -78
  39. package/src/config/schema.ts +0 -104
  40. package/src/config/store.ts +0 -244
  41. package/src/events/bus.ts +0 -45
  42. package/src/filesystem/__tests__/search-cache.test.ts +0 -61
  43. package/src/filesystem/browser.ts +0 -353
  44. package/src/filesystem/search-cache.ts +0 -66
  45. package/src/filesystem/search.ts +0 -184
  46. package/src/index.ts +0 -540
  47. package/src/launcher.ts +0 -177
  48. package/src/loader.ts +0 -21
  49. package/src/logger.ts +0 -133
  50. package/src/opencode-config.ts +0 -31
  51. package/src/plugins/channel.ts +0 -55
  52. package/src/plugins/handlers.ts +0 -36
  53. package/src/releases/dev-release-monitor.ts +0 -118
  54. package/src/releases/release-monitor.ts +0 -149
  55. package/src/server/http-server.ts +0 -693
  56. package/src/server/network-addresses.ts +0 -75
  57. package/src/server/routes/auth-pages/login.html +0 -134
  58. package/src/server/routes/auth-pages/token.html +0 -93
  59. package/src/server/routes/auth.ts +0 -164
  60. package/src/server/routes/background-processes.ts +0 -85
  61. package/src/server/routes/config.ts +0 -76
  62. package/src/server/routes/events.ts +0 -61
  63. package/src/server/routes/filesystem.ts +0 -54
  64. package/src/server/routes/meta.ts +0 -58
  65. package/src/server/routes/plugin.ts +0 -75
  66. package/src/server/routes/storage.ts +0 -66
  67. package/src/server/routes/workspaces.ts +0 -113
  68. package/src/server/routes/worktrees.ts +0 -195
  69. package/src/server/tls.ts +0 -283
  70. package/src/storage/instance-store.ts +0 -64
  71. package/src/ui/__tests__/remote-ui.test.ts +0 -58
  72. package/src/ui/remote-ui.ts +0 -571
  73. package/src/workspaces/git-worktrees.ts +0 -241
  74. package/src/workspaces/instance-events.ts +0 -226
  75. package/src/workspaces/manager.ts +0 -493
  76. package/src/workspaces/opencode-auth.ts +0 -22
  77. package/src/workspaces/runtime.ts +0 -428
  78. package/src/workspaces/worktree-map.ts +0 -129
  79. package/tsconfig.json +0 -17
@@ -1,75 +0,0 @@
1
- import os from "os"
2
- import type { NetworkAddress } from "../api-types"
3
-
4
- export function resolveNetworkAddresses(args: {
5
- host: string
6
- protocol: "http" | "https"
7
- port: number
8
- }): NetworkAddress[] {
9
- const { host, protocol, port } = args
10
- const interfaces = os.networkInterfaces()
11
- const seen = new Set<string>()
12
- const results: NetworkAddress[] = []
13
-
14
- const addAddress = (ip: string, scope: NetworkAddress["scope"]) => {
15
- if (!ip || ip === "0.0.0.0") return
16
- const key = `ipv4-${ip}`
17
- if (seen.has(key)) return
18
- seen.add(key)
19
- results.push({ ip, family: "ipv4", scope, remoteUrl: `${protocol}://${ip}:${port}` })
20
- }
21
-
22
- const normalizeFamily = (value: string | number) => {
23
- if (typeof value === "string") {
24
- const lowered = value.toLowerCase()
25
- if (lowered === "ipv4") {
26
- return "ipv4" as const
27
- }
28
- }
29
- if (value === 4) return "ipv4" as const
30
- return null
31
- }
32
-
33
- if (host === "0.0.0.0") {
34
- // Enumerate system interfaces (IPv4 only)
35
- for (const entries of Object.values(interfaces)) {
36
- if (!entries) continue
37
- for (const entry of entries) {
38
- const family = normalizeFamily(entry.family)
39
- if (!family) continue
40
- if (!entry.address || entry.address === "0.0.0.0") continue
41
- const scope: NetworkAddress["scope"] = entry.internal ? "loopback" : "external"
42
- addAddress(entry.address, scope)
43
- }
44
- }
45
- }
46
-
47
- // Always include loopback address
48
- addAddress("127.0.0.1", "loopback")
49
-
50
- // Include explicitly configured host if it was IPv4
51
- if (isIPv4Address(host) && host !== "0.0.0.0") {
52
- const isLoopback = host.startsWith("127.")
53
- addAddress(host, isLoopback ? "loopback" : "external")
54
- }
55
-
56
- const scopeWeight: Record<NetworkAddress["scope"], number> = { external: 0, internal: 1, loopback: 2 }
57
-
58
- return results.sort((a, b) => {
59
- const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]
60
- if (scopeDelta !== 0) return scopeDelta
61
- return a.ip.localeCompare(b.ip)
62
- })
63
- }
64
-
65
- function isIPv4Address(value: string | undefined): value is string {
66
- if (!value) return false
67
- const parts = value.split(".")
68
- if (parts.length !== 4) return false
69
- return parts.every((part) => {
70
- if (part.length === 0 || part.length > 3) return false
71
- if (!/^[0-9]+$/.test(part)) return false
72
- const num = Number(part)
73
- return Number.isInteger(num) && num >= 0 && num <= 255
74
- })
75
- }
@@ -1,134 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>CodeNomad Login</title>
7
- <style>
8
- body {
9
- font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
10
- background: #0b0b0f;
11
- color: #fff;
12
- display: flex;
13
- align-items: center;
14
- justify-content: center;
15
- height: 100vh;
16
- margin: 0;
17
- }
18
- .card {
19
- width: 420px;
20
- max-width: calc(100vw - 32px);
21
- background: #14141c;
22
- border: 1px solid rgba(255, 255, 255, 0.08);
23
- border-radius: 14px;
24
- padding: 24px;
25
- }
26
- h1 {
27
- font-size: 18px;
28
- margin: 0 0 12px;
29
- }
30
- p {
31
- margin: 0 0 18px;
32
- color: rgba(255, 255, 255, 0.7);
33
- font-size: 13px;
34
- line-height: 1.4;
35
- }
36
- label {
37
- display: block;
38
- font-size: 12px;
39
- margin: 10px 0 6px;
40
- color: rgba(255, 255, 255, 0.75);
41
- }
42
- input {
43
- width: 100%;
44
- box-sizing: border-box;
45
- padding: 10px 12px;
46
- border-radius: 10px;
47
- border: 1px solid rgba(255, 255, 255, 0.12);
48
- background: #0f0f16;
49
- color: #fff;
50
- }
51
- button {
52
- width: 100%;
53
- margin-top: 14px;
54
- padding: 10px 12px;
55
- border-radius: 10px;
56
- border: 0;
57
- background: #4c6fff;
58
- color: #fff;
59
- font-weight: 600;
60
- cursor: pointer;
61
- }
62
- .error {
63
- margin-top: 12px;
64
- color: #ff6b6b;
65
- font-size: 13px;
66
- }
67
- </style>
68
- </head>
69
- <body>
70
- <div class="card">
71
- <h1>Sign in</h1>
72
- <p>This CodeNomad server is protected. Enter your credentials to continue.</p>
73
-
74
- <label for="username">Username</label>
75
- <input id="username" autocomplete="username" placeholder="{{DEFAULT_USERNAME}}" value="" />
76
-
77
- <label for="password">Password</label>
78
- <input id="password" type="password" autocomplete="current-password" value="" />
79
-
80
- <button id="submit" type="button">Continue</button>
81
- <div id="error" class="error" style="display: none"></div>
82
- </div>
83
-
84
- <script>
85
- const $ = (id) => document.getElementById(id)
86
- const errorEl = $("error")
87
- const showError = (msg) => {
88
- errorEl.textContent = msg
89
- errorEl.style.display = "block"
90
- }
91
- const hideError = () => {
92
- errorEl.textContent = ""
93
- errorEl.style.display = "none"
94
- }
95
-
96
- async function submit() {
97
- hideError()
98
- const username = $("username").value.trim()
99
- const password = $("password").value
100
- if (!username || !password) {
101
- showError("Username and password are required.")
102
- return
103
- }
104
- try {
105
- const res = await fetch("/api/auth/login", {
106
- method: "POST",
107
- headers: { "Content-Type": "application/json" },
108
- body: JSON.stringify({ username, password }),
109
- credentials: "include",
110
- })
111
- if (!res.ok) {
112
- let message = ""
113
- try {
114
- const json = await res.json()
115
- message = json && json.error ? String(json.error) : ""
116
- } catch {
117
- message = ""
118
- }
119
- showError(message || `Login failed (${res.status})`)
120
- return
121
- }
122
- window.location.href = "/"
123
- } catch (e) {
124
- showError(e && e.message ? e.message : String(e))
125
- }
126
- }
127
-
128
- $("submit").addEventListener("click", submit)
129
- $("password").addEventListener("keydown", (e) => {
130
- if (e.key === "Enter") submit()
131
- })
132
- </script>
133
- </body>
134
- </html>
@@ -1,93 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>CodeNomad</title>
7
- <style>
8
- body {
9
- font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
10
- background: #0b0b0f;
11
- color: #fff;
12
- display: flex;
13
- align-items: center;
14
- justify-content: center;
15
- height: 100vh;
16
- margin: 0;
17
- }
18
- .card {
19
- width: 420px;
20
- max-width: calc(100vw - 32px);
21
- background: #14141c;
22
- border: 1px solid rgba(255, 255, 255, 0.08);
23
- border-radius: 14px;
24
- padding: 24px;
25
- }
26
- h1 {
27
- font-size: 18px;
28
- margin: 0 0 12px;
29
- }
30
- p {
31
- margin: 0;
32
- color: rgba(255, 255, 255, 0.7);
33
- font-size: 13px;
34
- line-height: 1.4;
35
- }
36
- .error {
37
- margin-top: 12px;
38
- color: #ff6b6b;
39
- font-size: 13px;
40
- }
41
- </style>
42
- </head>
43
- <body>
44
- <div class="card">
45
- <h1>Connecting…</h1>
46
- <p>Finalizing local authentication.</p>
47
- <div id="error" class="error" style="display: none"></div>
48
- </div>
49
-
50
- <script>
51
- const token = (location.hash || "").replace(/^#/, "").trim()
52
- const errorEl = document.getElementById("error")
53
- const showError = (msg) => {
54
- errorEl.textContent = msg
55
- errorEl.style.display = "block"
56
- }
57
-
58
- async function run() {
59
- if (!token) {
60
- showError("Missing bootstrap token.")
61
- return
62
- }
63
-
64
- try {
65
- const res = await fetch("/api/auth/token", {
66
- method: "POST",
67
- headers: { "Content-Type": "application/json" },
68
- body: JSON.stringify({ token }),
69
- credentials: "include",
70
- })
71
-
72
- if (!res.ok) {
73
- let message = ""
74
- try {
75
- const json = await res.json()
76
- message = json && json.error ? String(json.error) : ""
77
- } catch {
78
- message = ""
79
- }
80
- showError(message || `Token exchange failed (${res.status})`)
81
- return
82
- }
83
-
84
- window.location.replace("/")
85
- } catch (e) {
86
- showError(e && e.message ? e.message : String(e))
87
- }
88
- }
89
-
90
- run()
91
- </script>
92
- </body>
93
- </html>
@@ -1,164 +0,0 @@
1
- import type { FastifyInstance } from "fastify"
2
- import fs from "fs"
3
- import { z } from "zod"
4
- import type { AuthManager } from "../../auth/manager"
5
- import { isLoopbackAddress } from "../../auth/http-auth"
6
-
7
- interface RouteDeps {
8
- authManager: AuthManager
9
- }
10
-
11
- const LoginSchema = z.object({
12
- username: z.string().min(1),
13
- password: z.string().min(1),
14
- })
15
-
16
- const TokenSchema = z.object({
17
- token: z.string().min(1),
18
- })
19
-
20
- const PasswordSchema = z.object({
21
- password: z.string().min(8),
22
- })
23
-
24
- const LOGIN_TEMPLATE_URL = new URL("./auth-pages/login.html", import.meta.url)
25
- const TOKEN_TEMPLATE_URL = new URL("./auth-pages/token.html", import.meta.url)
26
-
27
- let cachedLoginTemplate: string | null = null
28
- let cachedTokenTemplate: string | null = null
29
-
30
- function readTemplate(url: URL, cache: string | null): string {
31
- if (cache) return cache
32
- const content = fs.readFileSync(url, "utf-8")
33
- return content
34
- }
35
-
36
- function getLoginHtml(defaultUsername: string): string {
37
- if (!cachedLoginTemplate) {
38
- cachedLoginTemplate = readTemplate(LOGIN_TEMPLATE_URL, null)
39
- }
40
-
41
- const escapedUsername = escapeHtml(defaultUsername)
42
- return cachedLoginTemplate.replace(/\{\{DEFAULT_USERNAME\}\}/g, escapedUsername)
43
- }
44
-
45
- function getTokenHtml(): string {
46
- if (!cachedTokenTemplate) {
47
- cachedTokenTemplate = readTemplate(TOKEN_TEMPLATE_URL, null)
48
- }
49
-
50
- return cachedTokenTemplate
51
- }
52
-
53
- export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
54
- app.get("/login", async (_request, reply) => {
55
- const status = deps.authManager.getStatus()
56
- reply.type("text/html").send(getLoginHtml(status.username))
57
- })
58
-
59
- app.get("/auth/token", async (request, reply) => {
60
- if (!deps.authManager.isTokenBootstrapEnabled()) {
61
- reply.code(404).send({ error: "Not found" })
62
- return
63
- }
64
-
65
- if (!isLoopbackAddress(request.socket.remoteAddress)) {
66
- reply.code(404).send({ error: "Not found" })
67
- return
68
- }
69
-
70
- reply.type("text/html").send(getTokenHtml())
71
- })
72
-
73
- app.get("/api/auth/status", async (request, reply) => {
74
- const session = deps.authManager.getSessionFromRequest(request)
75
- if (!session) {
76
- reply.send({ authenticated: false })
77
- return
78
- }
79
- reply.send({ authenticated: true, ...deps.authManager.getStatus() })
80
- })
81
-
82
- app.post("/api/auth/login", async (request, reply) => {
83
- const body = LoginSchema.parse(request.body ?? {})
84
- const ok = deps.authManager.validateLogin(body.username, body.password)
85
- if (!ok) {
86
- reply.code(401).send({ error: "Invalid credentials" })
87
- return
88
- }
89
-
90
- const session = deps.authManager.createSession(body.username)
91
- deps.authManager.setSessionCookieWithOptions(reply, session.id, { secure: isSecureRequest(request) })
92
- reply.send({ ok: true })
93
- })
94
-
95
- app.post("/api/auth/token", async (request, reply) => {
96
- if (!deps.authManager.isTokenBootstrapEnabled()) {
97
- reply.code(404).send({ error: "Not found" })
98
- return
99
- }
100
-
101
- if (!isLoopbackAddress(request.socket.remoteAddress)) {
102
- reply.code(404).send({ error: "Not found" })
103
- return
104
- }
105
-
106
- const body = TokenSchema.parse(request.body ?? {})
107
- const ok = deps.authManager.consumeBootstrapToken(body.token)
108
- if (!ok) {
109
- reply.code(401).send({ error: "Invalid token" })
110
- return
111
- }
112
-
113
- const username = deps.authManager.getStatus().username
114
- const session = deps.authManager.createSession(username)
115
- deps.authManager.setSessionCookieWithOptions(reply, session.id, { secure: isSecureRequest(request) })
116
- reply.send({ ok: true })
117
- })
118
-
119
- app.post("/api/auth/logout", async (request, reply) => {
120
- deps.authManager.clearSessionCookieWithOptions(reply, { secure: isSecureRequest(request) })
121
- reply.send({ ok: true })
122
- })
123
-
124
- app.post("/api/auth/password", async (request, reply) => {
125
- const session = deps.authManager.getSessionFromRequest(request)
126
- if (!session) {
127
- reply.code(401).send({ error: "Unauthorized" })
128
- return
129
- }
130
-
131
- const body = PasswordSchema.parse(request.body ?? {})
132
- try {
133
- const status = deps.authManager.setPassword(body.password)
134
- reply.send({ ok: true, ...status })
135
- } catch (error) {
136
- const message = error instanceof Error ? error.message : String(error)
137
- reply.code(409).type("text/plain").send(message)
138
- }
139
- })
140
- }
141
-
142
- function isSecureRequest(request: any) {
143
- if (request.protocol === "https") {
144
- return true
145
- }
146
- return Boolean(request.raw?.socket && request.raw.socket.encrypted)
147
- }
148
-
149
- function escapeHtml(value: string) {
150
- return value.replace(/[&<>"]/g, (char) => {
151
- switch (char) {
152
- case "&":
153
- return "&amp;"
154
- case "<":
155
- return "&lt;"
156
- case ">":
157
- return "&gt;"
158
- case '"':
159
- return "&quot;"
160
- default:
161
- return char
162
- }
163
- })
164
- }
@@ -1,85 +0,0 @@
1
- import { FastifyInstance } from "fastify"
2
- import { z } from "zod"
3
- import type { BackgroundProcessManager } from "../../background-processes/manager"
4
-
5
- interface RouteDeps {
6
- backgroundProcessManager: BackgroundProcessManager
7
- }
8
-
9
- const StartSchema = z.object({
10
- title: z.string().trim().min(1),
11
- command: z.string().trim().min(1),
12
- })
13
-
14
- const OutputQuerySchema = z.object({
15
- method: z.enum(["full", "tail", "head", "grep"]).optional(),
16
- mode: z.enum(["full", "tail", "head", "grep"]).optional(),
17
- pattern: z.string().optional(),
18
- lines: z.coerce.number().int().positive().max(2000).optional(),
19
- maxBytes: z.coerce.number().int().positive().optional(),
20
- })
21
-
22
- export function registerBackgroundProcessRoutes(app: FastifyInstance, deps: RouteDeps) {
23
- app.get<{ Params: { id: string } }>("/workspaces/:id/plugin/background-processes", async (request) => {
24
- const processes = await deps.backgroundProcessManager.list(request.params.id)
25
- return { processes }
26
- })
27
-
28
- app.post<{ Params: { id: string } }>("/workspaces/:id/plugin/background-processes", async (request, reply) => {
29
- const payload = StartSchema.parse(request.body ?? {})
30
- const process = await deps.backgroundProcessManager.start(request.params.id, payload.title, payload.command)
31
- reply.code(201)
32
- return process
33
- })
34
-
35
- app.post<{ Params: { id: string; processId: string } }>(
36
- "/workspaces/:id/plugin/background-processes/:processId/stop",
37
- async (request, reply) => {
38
- const process = await deps.backgroundProcessManager.stop(request.params.id, request.params.processId)
39
- if (!process) {
40
- reply.code(404)
41
- return { error: "Process not found" }
42
- }
43
- return process
44
- },
45
- )
46
-
47
- app.post<{ Params: { id: string; processId: string } }>(
48
- "/workspaces/:id/plugin/background-processes/:processId/terminate",
49
- async (request, reply) => {
50
- await deps.backgroundProcessManager.terminate(request.params.id, request.params.processId)
51
- reply.code(204)
52
- return undefined
53
- },
54
- )
55
-
56
- app.get<{ Params: { id: string; processId: string } }>(
57
- "/workspaces/:id/plugin/background-processes/:processId/output",
58
- async (request, reply) => {
59
- const query = OutputQuerySchema.parse(request.query ?? {})
60
- const method = query.method ?? query.mode
61
- if (method === "grep" && !query.pattern) {
62
- reply.code(400)
63
- return { error: "Pattern is required for grep output" }
64
- }
65
- try {
66
- return await deps.backgroundProcessManager.readOutput(request.params.id, request.params.processId, {
67
- method,
68
- pattern: query.pattern,
69
- lines: query.lines,
70
- maxBytes: query.maxBytes,
71
- })
72
- } catch (error) {
73
- reply.code(400)
74
- return { error: error instanceof Error ? error.message : "Invalid output request" }
75
- }
76
- },
77
- )
78
-
79
- app.get<{ Params: { id: string; processId: string } }>(
80
- "/workspaces/:id/plugin/background-processes/:processId/stream",
81
- async (request, reply) => {
82
- await deps.backgroundProcessManager.streamOutput(request.params.id, request.params.processId, reply)
83
- },
84
- )
85
- }
@@ -1,76 +0,0 @@
1
- import { FastifyInstance } from "fastify"
2
- import { z } from "zod"
3
- import { ConfigStore } from "../../config/store"
4
- import { BinaryRegistry } from "../../config/binaries"
5
-
6
- interface RouteDeps {
7
- configStore: ConfigStore
8
- binaryRegistry: BinaryRegistry
9
- }
10
-
11
- const BinaryCreateSchema = z.object({
12
- path: z.string(),
13
- label: z.string().optional(),
14
- makeDefault: z.boolean().optional(),
15
- })
16
-
17
- const BinaryUpdateSchema = z.object({
18
- label: z.string().optional(),
19
- makeDefault: z.boolean().optional(),
20
- })
21
-
22
- const BinaryValidateSchema = z.object({
23
- path: z.string(),
24
- })
25
-
26
- export function registerConfigRoutes(app: FastifyInstance, deps: RouteDeps) {
27
- app.get("/api/config/app", async () => deps.configStore.get())
28
-
29
- app.put("/api/config/app", async (request, reply) => {
30
- // Backwards compatible: treat PUT as a merge-patch update.
31
- try {
32
- deps.configStore.mergePatch(request.body ?? {})
33
- return deps.configStore.get()
34
- } catch (error) {
35
- reply.code(400)
36
- return { error: error instanceof Error ? error.message : "Invalid config patch" }
37
- }
38
- })
39
-
40
- app.patch("/api/config/app", async (request, reply) => {
41
- try {
42
- deps.configStore.mergePatch(request.body ?? {})
43
- return deps.configStore.get()
44
- } catch (error) {
45
- reply.code(400)
46
- return { error: error instanceof Error ? error.message : "Invalid config patch" }
47
- }
48
- })
49
-
50
- app.get("/api/config/binaries", async () => {
51
- return { binaries: deps.binaryRegistry.list() }
52
- })
53
-
54
- app.post("/api/config/binaries", async (request, reply) => {
55
- const body = BinaryCreateSchema.parse(request.body ?? {})
56
- const binary = deps.binaryRegistry.create(body)
57
- reply.code(201)
58
- return { binary }
59
- })
60
-
61
- app.patch<{ Params: { id: string } }>("/api/config/binaries/:id", async (request) => {
62
- const body = BinaryUpdateSchema.parse(request.body ?? {})
63
- const binary = deps.binaryRegistry.update(request.params.id, body)
64
- return { binary }
65
- })
66
-
67
- app.delete<{ Params: { id: string } }>("/api/config/binaries/:id", async (request, reply) => {
68
- deps.binaryRegistry.remove(request.params.id)
69
- reply.code(204)
70
- })
71
-
72
- app.post("/api/config/binaries/validate", async (request) => {
73
- const body = BinaryValidateSchema.parse(request.body ?? {})
74
- return deps.binaryRegistry.validatePath(body.path)
75
- })
76
- }
@@ -1,61 +0,0 @@
1
- import { FastifyInstance } from "fastify"
2
- import { EventBus } from "../../events/bus"
3
- import { WorkspaceEventPayload } from "../../api-types"
4
- import { Logger } from "../../logger"
5
-
6
- interface RouteDeps {
7
- eventBus: EventBus
8
- registerClient: (cleanup: () => void) => () => void
9
- logger: Logger
10
- }
11
-
12
- let nextClientId = 0
13
-
14
- export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
15
- app.get("/api/events", (request, reply) => {
16
- const clientId = ++nextClientId
17
- deps.logger.debug({ clientId }, "SSE client connected")
18
-
19
- const origin = request.headers.origin ?? "*"
20
- reply.raw.setHeader("Access-Control-Allow-Origin", origin)
21
- reply.raw.setHeader("Access-Control-Allow-Credentials", "true")
22
- reply.raw.setHeader("Content-Type", "text/event-stream")
23
- reply.raw.setHeader("Cache-Control", "no-cache")
24
- reply.raw.setHeader("Connection", "keep-alive")
25
- reply.raw.flushHeaders?.()
26
- reply.hijack()
27
-
28
- const send = (event: WorkspaceEventPayload) => {
29
- deps.logger.debug({ clientId, type: event.type }, "SSE event dispatched")
30
- if (deps.logger.isLevelEnabled("trace")) {
31
- deps.logger.trace({ clientId, event }, "SSE event payload")
32
- }
33
- reply.raw.write(`data: ${JSON.stringify(event)}\n\n`)
34
- }
35
-
36
- const unsubscribe = deps.eventBus.onEvent(send)
37
- const heartbeat = setInterval(() => {
38
- reply.raw.write(`:hb ${Date.now()}\n\n`)
39
- }, 15000)
40
-
41
- let closed = false
42
- const close = () => {
43
- if (closed) return
44
- closed = true
45
- clearInterval(heartbeat)
46
- unsubscribe()
47
- reply.raw.end?.()
48
- deps.logger.debug({ clientId }, "SSE client disconnected")
49
- }
50
-
51
- const unregister = deps.registerClient(close)
52
-
53
- const handleClose = () => {
54
- close()
55
- unregister()
56
- }
57
-
58
- request.raw.on("close", handleClose)
59
- request.raw.on("error", handleClose)
60
- })
61
- }