@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.
- package/package.json +1 -1
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/assets/{main-CSlDZj4f.js → main-crtt5pqm.js} +82 -80
- package/public/index.html +1 -1
- package/public/sw.js +1 -1
- package/public/ui-version.json +1 -1
- package/dist/integrations/github/bot-signature.js +0 -11
- package/dist/integrations/github/git-ops.js +0 -133
- package/dist/integrations/github/github-types.js +0 -1
- package/dist/integrations/github/job-runner.js +0 -608
- package/dist/integrations/github/octokit.js +0 -58
- package/dist/integrations/github/sanitize-webhook.js +0 -42
- package/dist/integrations/github/webhook-verify.js +0 -21
- package/dist/integrations/github/workspace-context.js +0 -10
- package/dist/integrations/github/worktree-context.js +0 -15
- package/dist/opencode/request-context.js +0 -39
- package/dist/opencode/worktree-directory.js +0 -42
- package/dist/opencode-config-template/README.md +0 -32
- package/dist/opencode-config-template/opencode.jsonc +0 -3
- package/dist/opencode-config-template/plugin/codenomad.ts +0 -40
- package/dist/opencode-config-template/plugin/lib/background-process.ts +0 -160
- package/dist/opencode-config-template/plugin/lib/client.ts +0 -165
- package/dist/server/routes/github-plugin.js +0 -215
- package/dist/server/routes/github-webhook.js +0 -32
- package/scripts/copy-auth-pages.mjs +0 -22
- package/scripts/copy-opencode-config.mjs +0 -61
- package/scripts/copy-ui-dist.mjs +0 -21
- package/src/api-types.ts +0 -326
- package/src/auth/auth-store.ts +0 -175
- package/src/auth/http-auth.ts +0 -38
- package/src/auth/manager.ts +0 -163
- package/src/auth/password-hash.ts +0 -49
- package/src/auth/session-manager.ts +0 -23
- package/src/auth/token-manager.ts +0 -32
- package/src/background-processes/manager.ts +0 -519
- package/src/bin.ts +0 -29
- package/src/config/binaries.ts +0 -192
- package/src/config/location.ts +0 -78
- package/src/config/schema.ts +0 -104
- package/src/config/store.ts +0 -244
- package/src/events/bus.ts +0 -45
- package/src/filesystem/__tests__/search-cache.test.ts +0 -61
- package/src/filesystem/browser.ts +0 -353
- package/src/filesystem/search-cache.ts +0 -66
- package/src/filesystem/search.ts +0 -184
- package/src/index.ts +0 -540
- package/src/launcher.ts +0 -177
- package/src/loader.ts +0 -21
- package/src/logger.ts +0 -133
- package/src/opencode-config.ts +0 -31
- package/src/plugins/channel.ts +0 -55
- package/src/plugins/handlers.ts +0 -36
- package/src/releases/dev-release-monitor.ts +0 -118
- package/src/releases/release-monitor.ts +0 -149
- package/src/server/http-server.ts +0 -693
- package/src/server/network-addresses.ts +0 -75
- package/src/server/routes/auth-pages/login.html +0 -134
- package/src/server/routes/auth-pages/token.html +0 -93
- package/src/server/routes/auth.ts +0 -164
- package/src/server/routes/background-processes.ts +0 -85
- package/src/server/routes/config.ts +0 -76
- package/src/server/routes/events.ts +0 -61
- package/src/server/routes/filesystem.ts +0 -54
- package/src/server/routes/meta.ts +0 -58
- package/src/server/routes/plugin.ts +0 -75
- package/src/server/routes/storage.ts +0 -66
- package/src/server/routes/workspaces.ts +0 -113
- package/src/server/routes/worktrees.ts +0 -195
- package/src/server/tls.ts +0 -283
- package/src/storage/instance-store.ts +0 -64
- package/src/ui/__tests__/remote-ui.test.ts +0 -58
- package/src/ui/remote-ui.ts +0 -571
- package/src/workspaces/git-worktrees.ts +0 -241
- package/src/workspaces/instance-events.ts +0 -226
- package/src/workspaces/manager.ts +0 -493
- package/src/workspaces/opencode-auth.ts +0 -22
- package/src/workspaces/runtime.ts +0 -428
- package/src/workspaces/worktree-map.ts +0 -129
- package/tsconfig.json +0 -17
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
import path from "path"
|
|
2
|
-
import { spawnSync } from "child_process"
|
|
3
|
-
import { connect } from "net"
|
|
4
|
-
import { EventBus } from "../events/bus"
|
|
5
|
-
import { ConfigStore } from "../config/store"
|
|
6
|
-
import { BinaryRegistry } from "../config/binaries"
|
|
7
|
-
import { FileSystemBrowser } from "../filesystem/browser"
|
|
8
|
-
import { searchWorkspaceFiles, WorkspaceFileSearchOptions } from "../filesystem/search"
|
|
9
|
-
import { clearWorkspaceSearchCache } from "../filesystem/search-cache"
|
|
10
|
-
import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types"
|
|
11
|
-
import { WorkspaceRuntime, ProcessExitInfo } from "./runtime"
|
|
12
|
-
import { Logger } from "../logger"
|
|
13
|
-
import { getOpencodeConfigDir } from "../opencode-config.js"
|
|
14
|
-
import {
|
|
15
|
-
buildOpencodeBasicAuthHeader,
|
|
16
|
-
DEFAULT_OPENCODE_USERNAME,
|
|
17
|
-
generateOpencodeServerPassword,
|
|
18
|
-
OPENCODE_SERVER_PASSWORD_ENV,
|
|
19
|
-
OPENCODE_SERVER_USERNAME_ENV,
|
|
20
|
-
} from "./opencode-auth"
|
|
21
|
-
|
|
22
|
-
const STARTUP_STABILITY_DELAY_MS = 1500
|
|
23
|
-
|
|
24
|
-
interface WorkspaceManagerOptions {
|
|
25
|
-
rootDir: string
|
|
26
|
-
configStore: ConfigStore
|
|
27
|
-
binaryRegistry: BinaryRegistry
|
|
28
|
-
eventBus: EventBus
|
|
29
|
-
logger: Logger
|
|
30
|
-
getServerBaseUrl: () => string
|
|
31
|
-
/** Optional CA bundle path to trust CodeNomad HTTPS certs. */
|
|
32
|
-
nodeExtraCaCertsPath?: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface WorkspaceRecord extends WorkspaceDescriptor {}
|
|
36
|
-
|
|
37
|
-
export class WorkspaceManager {
|
|
38
|
-
private readonly workspaces = new Map<string, WorkspaceRecord>()
|
|
39
|
-
private readonly runtime: WorkspaceRuntime
|
|
40
|
-
private readonly opencodeConfigDir: string
|
|
41
|
-
private readonly opencodeAuth = new Map<string, { username: string; password: string; authorization: string }>()
|
|
42
|
-
|
|
43
|
-
constructor(private readonly options: WorkspaceManagerOptions) {
|
|
44
|
-
this.runtime = new WorkspaceRuntime(this.options.eventBus, this.options.logger)
|
|
45
|
-
this.opencodeConfigDir = getOpencodeConfigDir()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
list(): WorkspaceDescriptor[] {
|
|
49
|
-
return Array.from(this.workspaces.values())
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
get(id: string): WorkspaceDescriptor | undefined {
|
|
53
|
-
return this.workspaces.get(id)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
getInstancePort(id: string): number | undefined {
|
|
57
|
-
return this.workspaces.get(id)?.port
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getInstanceAuthorizationHeader(id: string): string | undefined {
|
|
61
|
-
return this.opencodeAuth.get(id)?.authorization
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
listFiles(workspaceId: string, relativePath = "."): FileSystemEntry[] {
|
|
65
|
-
const workspace = this.requireWorkspace(workspaceId)
|
|
66
|
-
const browser = new FileSystemBrowser({ rootDir: workspace.path })
|
|
67
|
-
return browser.list(relativePath)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
searchFiles(workspaceId: string, query: string, options?: WorkspaceFileSearchOptions): FileSystemEntry[] {
|
|
71
|
-
const workspace = this.requireWorkspace(workspaceId)
|
|
72
|
-
return searchWorkspaceFiles(workspace.path, query, options)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
readFile(workspaceId: string, relativePath: string): WorkspaceFileResponse {
|
|
76
|
-
const workspace = this.requireWorkspace(workspaceId)
|
|
77
|
-
const browser = new FileSystemBrowser({ rootDir: workspace.path })
|
|
78
|
-
const contents = browser.readFile(relativePath)
|
|
79
|
-
return {
|
|
80
|
-
workspaceId,
|
|
81
|
-
relativePath,
|
|
82
|
-
contents,
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async create(folder: string, name?: string): Promise<WorkspaceDescriptor> {
|
|
87
|
-
|
|
88
|
-
const id = `${Date.now().toString(36)}`
|
|
89
|
-
const binary = this.options.binaryRegistry.resolveDefault()
|
|
90
|
-
const resolvedBinaryPath = this.resolveBinaryPath(binary.path)
|
|
91
|
-
const workspacePath = path.isAbsolute(folder) ? folder : path.resolve(this.options.rootDir, folder)
|
|
92
|
-
clearWorkspaceSearchCache(workspacePath)
|
|
93
|
-
|
|
94
|
-
this.options.logger.info({ workspaceId: id, folder: workspacePath, binary: resolvedBinaryPath }, "Creating workspace")
|
|
95
|
-
|
|
96
|
-
const proxyPath = `/workspaces/${id}/worktrees/root/instance`
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const descriptor: WorkspaceRecord = {
|
|
100
|
-
id,
|
|
101
|
-
path: workspacePath,
|
|
102
|
-
name,
|
|
103
|
-
status: "starting",
|
|
104
|
-
proxyPath,
|
|
105
|
-
binaryId: resolvedBinaryPath,
|
|
106
|
-
binaryLabel: binary.label,
|
|
107
|
-
binaryVersion: binary.version,
|
|
108
|
-
createdAt: new Date().toISOString(),
|
|
109
|
-
updatedAt: new Date().toISOString(),
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!descriptor.binaryVersion) {
|
|
113
|
-
descriptor.binaryVersion = this.detectBinaryVersion(resolvedBinaryPath)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
this.workspaces.set(id, descriptor)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.options.eventBus.publish({ type: "workspace.created", workspace: descriptor })
|
|
120
|
-
|
|
121
|
-
const preferences = this.options.configStore.get().preferences ?? {}
|
|
122
|
-
const userEnvironment = preferences.environmentVariables ?? {}
|
|
123
|
-
|
|
124
|
-
const opencodeUsername = DEFAULT_OPENCODE_USERNAME
|
|
125
|
-
const opencodePassword = generateOpencodeServerPassword()
|
|
126
|
-
const authorization = buildOpencodeBasicAuthHeader({ username: opencodeUsername, password: opencodePassword })
|
|
127
|
-
if (!authorization) {
|
|
128
|
-
throw new Error("Failed to build OpenCode auth header")
|
|
129
|
-
}
|
|
130
|
-
this.opencodeAuth.set(id, { username: opencodeUsername, password: opencodePassword, authorization })
|
|
131
|
-
|
|
132
|
-
const environment = {
|
|
133
|
-
...userEnvironment,
|
|
134
|
-
OPENCODE_CONFIG_DIR: this.opencodeConfigDir,
|
|
135
|
-
CODENOMAD_INSTANCE_ID: id,
|
|
136
|
-
CODENOMAD_BASE_URL: this.options.getServerBaseUrl(),
|
|
137
|
-
...(this.options.nodeExtraCaCertsPath ? { NODE_EXTRA_CA_CERTS: this.options.nodeExtraCaCertsPath } : {}),
|
|
138
|
-
[OPENCODE_SERVER_USERNAME_ENV]: opencodeUsername,
|
|
139
|
-
[OPENCODE_SERVER_PASSWORD_ENV]: opencodePassword,
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const { pid, port, exitPromise, getLastOutput } = await this.runtime.launch({
|
|
144
|
-
workspaceId: id,
|
|
145
|
-
folder: workspacePath,
|
|
146
|
-
binaryPath: resolvedBinaryPath,
|
|
147
|
-
environment,
|
|
148
|
-
onExit: (info) => this.handleProcessExit(info.workspaceId, info),
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
await this.waitForWorkspaceReadiness({ workspaceId: id, port, exitPromise, getLastOutput })
|
|
152
|
-
|
|
153
|
-
descriptor.pid = pid
|
|
154
|
-
descriptor.port = port
|
|
155
|
-
descriptor.status = "ready"
|
|
156
|
-
descriptor.updatedAt = new Date().toISOString()
|
|
157
|
-
this.options.eventBus.publish({ type: "workspace.started", workspace: descriptor })
|
|
158
|
-
this.options.logger.info({ workspaceId: id, port }, "Workspace ready")
|
|
159
|
-
return descriptor
|
|
160
|
-
} catch (error) {
|
|
161
|
-
descriptor.status = "error"
|
|
162
|
-
descriptor.error = error instanceof Error ? error.message : String(error)
|
|
163
|
-
descriptor.updatedAt = new Date().toISOString()
|
|
164
|
-
this.options.eventBus.publish({ type: "workspace.error", workspace: descriptor })
|
|
165
|
-
this.options.logger.error({ workspaceId: id, err: error }, "Workspace failed to start")
|
|
166
|
-
throw error
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async delete(id: string): Promise<WorkspaceDescriptor | undefined> {
|
|
171
|
-
const workspace = this.workspaces.get(id)
|
|
172
|
-
if (!workspace) return undefined
|
|
173
|
-
|
|
174
|
-
this.options.logger.info({ workspaceId: id }, "Stopping workspace")
|
|
175
|
-
const wasRunning = Boolean(workspace.pid)
|
|
176
|
-
if (wasRunning) {
|
|
177
|
-
await this.runtime.stop(id).catch((error) => {
|
|
178
|
-
this.options.logger.warn({ workspaceId: id, err: error }, "Failed to stop workspace process cleanly")
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
this.workspaces.delete(id)
|
|
183
|
-
this.opencodeAuth.delete(id)
|
|
184
|
-
clearWorkspaceSearchCache(workspace.path)
|
|
185
|
-
if (!wasRunning) {
|
|
186
|
-
this.options.eventBus.publish({ type: "workspace.stopped", workspaceId: id })
|
|
187
|
-
}
|
|
188
|
-
return workspace
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async shutdown() {
|
|
192
|
-
this.options.logger.info("Shutting down all workspaces")
|
|
193
|
-
|
|
194
|
-
const stopTasks: Array<Promise<void>> = []
|
|
195
|
-
|
|
196
|
-
for (const [id, workspace] of this.workspaces) {
|
|
197
|
-
if (!workspace.pid) {
|
|
198
|
-
this.options.logger.debug({ workspaceId: id }, "Workspace already stopped")
|
|
199
|
-
continue
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown")
|
|
203
|
-
stopTasks.push(
|
|
204
|
-
this.runtime.stop(id).catch((error) => {
|
|
205
|
-
this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown")
|
|
206
|
-
}),
|
|
207
|
-
)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (stopTasks.length > 0) {
|
|
211
|
-
await Promise.allSettled(stopTasks)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
this.workspaces.clear()
|
|
215
|
-
this.opencodeAuth.clear()
|
|
216
|
-
this.options.logger.info("All workspaces cleared")
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private requireWorkspace(id: string): WorkspaceRecord {
|
|
220
|
-
const workspace = this.workspaces.get(id)
|
|
221
|
-
if (!workspace) {
|
|
222
|
-
throw new Error("Workspace not found")
|
|
223
|
-
}
|
|
224
|
-
return workspace
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private resolveBinaryPath(identifier: string): string {
|
|
228
|
-
if (!identifier) {
|
|
229
|
-
return identifier
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const looksLikePath = identifier.includes("/") || identifier.includes("\\") || identifier.startsWith(".")
|
|
233
|
-
if (path.isAbsolute(identifier) || looksLikePath) {
|
|
234
|
-
return identifier
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const locator = process.platform === "win32" ? "where" : "which"
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
const result = spawnSync(locator, [identifier], { encoding: "utf8" })
|
|
241
|
-
if (result.status === 0 && result.stdout) {
|
|
242
|
-
const candidates = result.stdout
|
|
243
|
-
.split(/\r?\n/)
|
|
244
|
-
.map((line) => line.trim())
|
|
245
|
-
.filter((line) => line.length > 0)
|
|
246
|
-
.filter((line) => !/^INFO:/i.test(line))
|
|
247
|
-
|
|
248
|
-
if (candidates.length > 0) {
|
|
249
|
-
const resolved = this.pickBinaryCandidate(candidates)
|
|
250
|
-
this.options.logger.debug({ identifier, resolved, candidates }, "Resolved binary path from system PATH")
|
|
251
|
-
return resolved
|
|
252
|
-
}
|
|
253
|
-
} else if (result.error) {
|
|
254
|
-
this.options.logger.warn({ identifier, err: result.error }, "Failed to resolve binary path via locator command")
|
|
255
|
-
}
|
|
256
|
-
} catch (error) {
|
|
257
|
-
this.options.logger.warn({ identifier, err: error }, "Failed to resolve binary path from system PATH")
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return identifier
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
private pickBinaryCandidate(candidates: string[]): string {
|
|
264
|
-
if (process.platform !== "win32") {
|
|
265
|
-
return candidates[0] ?? ""
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const extensionPreference = [".exe", ".cmd", ".bat", ".ps1"]
|
|
269
|
-
|
|
270
|
-
for (const ext of extensionPreference) {
|
|
271
|
-
const match = candidates.find((candidate) => candidate.toLowerCase().endsWith(ext))
|
|
272
|
-
if (match) {
|
|
273
|
-
return match
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return candidates[0] ?? ""
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private detectBinaryVersion(resolvedPath: string): string | undefined {
|
|
281
|
-
if (!resolvedPath) {
|
|
282
|
-
return undefined
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const result = spawnSync(resolvedPath, ["--version"], { encoding: "utf8" })
|
|
287
|
-
if (result.status === 0 && result.stdout) {
|
|
288
|
-
const line = result.stdout.split(/\r?\n/).find((entry) => entry.trim().length > 0)
|
|
289
|
-
if (line) {
|
|
290
|
-
const normalized = line.trim()
|
|
291
|
-
const versionMatch = normalized.match(/([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/)
|
|
292
|
-
if (versionMatch) {
|
|
293
|
-
const version = versionMatch[1]
|
|
294
|
-
this.options.logger.debug({ binary: resolvedPath, version }, "Detected binary version")
|
|
295
|
-
return version
|
|
296
|
-
}
|
|
297
|
-
this.options.logger.debug({ binary: resolvedPath, reported: normalized }, "Binary reported version string")
|
|
298
|
-
return normalized
|
|
299
|
-
}
|
|
300
|
-
} else if (result.error) {
|
|
301
|
-
this.options.logger.warn({ binary: resolvedPath, err: result.error }, "Failed to read binary version")
|
|
302
|
-
}
|
|
303
|
-
} catch (error) {
|
|
304
|
-
this.options.logger.warn({ binary: resolvedPath, err: error }, "Failed to detect binary version")
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return undefined
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
private async waitForWorkspaceReadiness(params: {
|
|
311
|
-
workspaceId: string
|
|
312
|
-
port: number
|
|
313
|
-
exitPromise: Promise<ProcessExitInfo>
|
|
314
|
-
getLastOutput: () => string
|
|
315
|
-
}) {
|
|
316
|
-
|
|
317
|
-
await Promise.race([
|
|
318
|
-
this.waitForPortAvailability(params.port),
|
|
319
|
-
params.exitPromise.then((info) => {
|
|
320
|
-
throw this.buildStartupError(
|
|
321
|
-
params.workspaceId,
|
|
322
|
-
"exited before becoming ready",
|
|
323
|
-
info,
|
|
324
|
-
params.getLastOutput(),
|
|
325
|
-
)
|
|
326
|
-
}),
|
|
327
|
-
])
|
|
328
|
-
|
|
329
|
-
await this.waitForInstanceHealth(params)
|
|
330
|
-
|
|
331
|
-
await Promise.race([
|
|
332
|
-
this.delay(STARTUP_STABILITY_DELAY_MS),
|
|
333
|
-
params.exitPromise.then((info) => {
|
|
334
|
-
throw this.buildStartupError(
|
|
335
|
-
params.workspaceId,
|
|
336
|
-
"exited shortly after start",
|
|
337
|
-
info,
|
|
338
|
-
params.getLastOutput(),
|
|
339
|
-
)
|
|
340
|
-
}),
|
|
341
|
-
])
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
private async waitForInstanceHealth(params: {
|
|
345
|
-
workspaceId: string
|
|
346
|
-
port: number
|
|
347
|
-
exitPromise: Promise<ProcessExitInfo>
|
|
348
|
-
getLastOutput: () => string
|
|
349
|
-
}) {
|
|
350
|
-
const probeResult = await Promise.race([
|
|
351
|
-
this.probeInstance(params.workspaceId, params.port),
|
|
352
|
-
params.exitPromise.then((info) => {
|
|
353
|
-
throw this.buildStartupError(
|
|
354
|
-
params.workspaceId,
|
|
355
|
-
"exited during health checks",
|
|
356
|
-
info,
|
|
357
|
-
params.getLastOutput(),
|
|
358
|
-
)
|
|
359
|
-
}),
|
|
360
|
-
])
|
|
361
|
-
|
|
362
|
-
if (probeResult.ok) {
|
|
363
|
-
return
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const latestOutput = params.getLastOutput().trim()
|
|
367
|
-
if (latestOutput) {
|
|
368
|
-
throw new Error(latestOutput)
|
|
369
|
-
}
|
|
370
|
-
const reason = probeResult.reason ?? "Health check failed"
|
|
371
|
-
throw new Error(`Workspace ${params.workspaceId} failed health check: ${reason}.`)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
private async probeInstance(workspaceId: string, port: number): Promise<{ ok: boolean; reason?: string }> {
|
|
375
|
-
const url = `http://127.0.0.1:${port}/project/current`
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
const headers: Record<string, string> = {}
|
|
379
|
-
const authHeader = this.opencodeAuth.get(workspaceId)?.authorization
|
|
380
|
-
if (authHeader) {
|
|
381
|
-
headers["Authorization"] = authHeader
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const response = await fetch(url, { headers })
|
|
385
|
-
if (!response.ok) {
|
|
386
|
-
const reason = `health probe returned HTTP ${response.status}`
|
|
387
|
-
this.options.logger.debug({ workspaceId, status: response.status }, "Health probe returned server error")
|
|
388
|
-
return { ok: false, reason }
|
|
389
|
-
}
|
|
390
|
-
return { ok: true }
|
|
391
|
-
} catch (error) {
|
|
392
|
-
const reason = error instanceof Error ? error.message : String(error)
|
|
393
|
-
this.options.logger.debug({ workspaceId, err: error }, "Health probe failed")
|
|
394
|
-
return { ok: false, reason }
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
private buildStartupError(
|
|
399
|
-
workspaceId: string,
|
|
400
|
-
phase: string,
|
|
401
|
-
exitInfo: ProcessExitInfo,
|
|
402
|
-
lastOutput: string,
|
|
403
|
-
): Error {
|
|
404
|
-
const exitDetails = this.describeExit(exitInfo)
|
|
405
|
-
const trimmedOutput = lastOutput.trim()
|
|
406
|
-
const outputDetails = trimmedOutput ? ` Last output: ${trimmedOutput}` : ""
|
|
407
|
-
return new Error(`Workspace ${workspaceId} ${phase} (${exitDetails}).${outputDetails}`)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
private waitForPortAvailability(port: number, timeoutMs = 5000): Promise<void> {
|
|
411
|
-
return new Promise((resolve, reject) => {
|
|
412
|
-
const deadline = Date.now() + timeoutMs
|
|
413
|
-
let settled = false
|
|
414
|
-
let retryTimer: NodeJS.Timeout | null = null
|
|
415
|
-
|
|
416
|
-
const cleanup = () => {
|
|
417
|
-
settled = true
|
|
418
|
-
if (retryTimer) {
|
|
419
|
-
clearTimeout(retryTimer)
|
|
420
|
-
retryTimer = null
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const tryConnect = () => {
|
|
425
|
-
if (settled) {
|
|
426
|
-
return
|
|
427
|
-
}
|
|
428
|
-
const socket = connect({ port, host: "127.0.0.1" }, () => {
|
|
429
|
-
cleanup()
|
|
430
|
-
socket.end()
|
|
431
|
-
resolve()
|
|
432
|
-
})
|
|
433
|
-
socket.once("error", () => {
|
|
434
|
-
socket.destroy()
|
|
435
|
-
if (settled) {
|
|
436
|
-
return
|
|
437
|
-
}
|
|
438
|
-
if (Date.now() >= deadline) {
|
|
439
|
-
cleanup()
|
|
440
|
-
reject(new Error(`Workspace port ${port} did not become ready within ${timeoutMs}ms`))
|
|
441
|
-
} else {
|
|
442
|
-
retryTimer = setTimeout(() => {
|
|
443
|
-
retryTimer = null
|
|
444
|
-
tryConnect()
|
|
445
|
-
}, 100)
|
|
446
|
-
}
|
|
447
|
-
})
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
tryConnect()
|
|
451
|
-
})
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
private delay(durationMs: number): Promise<void> {
|
|
455
|
-
if (durationMs <= 0) {
|
|
456
|
-
return Promise.resolve()
|
|
457
|
-
}
|
|
458
|
-
return new Promise((resolve) => setTimeout(resolve, durationMs))
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
private describeExit(info: ProcessExitInfo): string {
|
|
462
|
-
if (info.signal) {
|
|
463
|
-
return `signal ${info.signal}`
|
|
464
|
-
}
|
|
465
|
-
if (info.code !== null) {
|
|
466
|
-
return `code ${info.code}`
|
|
467
|
-
}
|
|
468
|
-
return "unknown reason"
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
private handleProcessExit(workspaceId: string, info: { code: number | null; requested: boolean }) {
|
|
472
|
-
const workspace = this.workspaces.get(workspaceId)
|
|
473
|
-
if (!workspace) return
|
|
474
|
-
|
|
475
|
-
this.opencodeAuth.delete(workspaceId)
|
|
476
|
-
|
|
477
|
-
this.options.logger.info({ workspaceId, ...info }, "Workspace process exited")
|
|
478
|
-
|
|
479
|
-
workspace.pid = undefined
|
|
480
|
-
workspace.port = undefined
|
|
481
|
-
workspace.updatedAt = new Date().toISOString()
|
|
482
|
-
|
|
483
|
-
if (info.requested || info.code === 0) {
|
|
484
|
-
workspace.status = "stopped"
|
|
485
|
-
workspace.error = undefined
|
|
486
|
-
this.options.eventBus.publish({ type: "workspace.stopped", workspaceId })
|
|
487
|
-
} else {
|
|
488
|
-
workspace.status = "error"
|
|
489
|
-
workspace.error = `Process exited with code ${info.code}`
|
|
490
|
-
this.options.eventBus.publish({ type: "workspace.error", workspace })
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto"
|
|
2
|
-
|
|
3
|
-
export const OPENCODE_SERVER_USERNAME_ENV = "OPENCODE_SERVER_USERNAME" as const
|
|
4
|
-
export const OPENCODE_SERVER_PASSWORD_ENV = "OPENCODE_SERVER_PASSWORD" as const
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_OPENCODE_USERNAME = "codenomad" as const
|
|
7
|
-
|
|
8
|
-
export function generateOpencodeServerPassword(): string {
|
|
9
|
-
return crypto.randomBytes(32).toString("base64url")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function buildOpencodeBasicAuthHeader(params: { username?: string; password?: string }): string | undefined {
|
|
13
|
-
const username = params.username
|
|
14
|
-
const password = params.password
|
|
15
|
-
|
|
16
|
-
if (!username || !password) {
|
|
17
|
-
return undefined
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const token = Buffer.from(`${username}:${password}`, "utf8").toString("base64")
|
|
21
|
-
return `Basic ${token}`
|
|
22
|
-
}
|