@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,428 +0,0 @@
|
|
|
1
|
-
import { ChildProcess, spawn, spawnSync } from "child_process"
|
|
2
|
-
import { existsSync, statSync } from "fs"
|
|
3
|
-
import path from "path"
|
|
4
|
-
import { EventBus } from "../events/bus"
|
|
5
|
-
import { LogLevel, WorkspaceLogEntry } from "../api-types"
|
|
6
|
-
import { Logger } from "../logger"
|
|
7
|
-
|
|
8
|
-
export const WINDOWS_CMD_EXTENSIONS = new Set([".cmd", ".bat"])
|
|
9
|
-
export const WINDOWS_POWERSHELL_EXTENSIONS = new Set([".ps1"])
|
|
10
|
-
|
|
11
|
-
export function buildSpawnSpec(binaryPath: string, args: string[]) {
|
|
12
|
-
if (process.platform !== "win32") {
|
|
13
|
-
return { command: binaryPath, args, options: {} as const }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const extension = path.extname(binaryPath).toLowerCase()
|
|
17
|
-
|
|
18
|
-
if (WINDOWS_CMD_EXTENSIONS.has(extension)) {
|
|
19
|
-
const comspec = process.env.ComSpec || "cmd.exe"
|
|
20
|
-
// cmd.exe requires the full command as a single string.
|
|
21
|
-
// Using the ""<script> <args>"" pattern ensures paths with spaces are handled.
|
|
22
|
-
const commandLine = `""${binaryPath}" ${args.join(" ")}"`
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
command: comspec,
|
|
26
|
-
args: ["/d", "/s", "/c", commandLine],
|
|
27
|
-
options: { windowsVerbatimArguments: true } as const,
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (WINDOWS_POWERSHELL_EXTENSIONS.has(extension)) {
|
|
32
|
-
// powershell.exe ships with Windows. (pwsh may not.)
|
|
33
|
-
return {
|
|
34
|
-
command: "powershell.exe",
|
|
35
|
-
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", binaryPath, ...args],
|
|
36
|
-
options: {} as const,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { command: binaryPath, args, options: {} as const }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const SENSITIVE_ENV_KEY = /(PASSWORD|TOKEN|SECRET)/i
|
|
44
|
-
|
|
45
|
-
function redactEnvironment(env: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
46
|
-
const redacted: Record<string, string | undefined> = {}
|
|
47
|
-
for (const [key, value] of Object.entries(env)) {
|
|
48
|
-
if (value === undefined) {
|
|
49
|
-
redacted[key] = value
|
|
50
|
-
continue
|
|
51
|
-
}
|
|
52
|
-
redacted[key] = SENSITIVE_ENV_KEY.test(key) ? "[REDACTED]" : value
|
|
53
|
-
}
|
|
54
|
-
return redacted
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface LaunchOptions {
|
|
58
|
-
workspaceId: string
|
|
59
|
-
folder: string
|
|
60
|
-
binaryPath: string
|
|
61
|
-
environment?: Record<string, string>
|
|
62
|
-
onExit?: (info: ProcessExitInfo) => void
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface ProcessExitInfo {
|
|
66
|
-
workspaceId: string
|
|
67
|
-
code: number | null
|
|
68
|
-
signal: NodeJS.Signals | null
|
|
69
|
-
requested: boolean
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface ManagedProcess {
|
|
73
|
-
child: ChildProcess
|
|
74
|
-
requestedStop: boolean
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export class WorkspaceRuntime {
|
|
78
|
-
private processes = new Map<string, ManagedProcess>()
|
|
79
|
-
|
|
80
|
-
constructor(private readonly eventBus: EventBus, private readonly logger: Logger) {}
|
|
81
|
-
|
|
82
|
-
async launch(options: LaunchOptions): Promise<{ pid: number; port: number; exitPromise: Promise<ProcessExitInfo>; getLastOutput: () => string }> {
|
|
83
|
-
this.validateFolder(options.folder)
|
|
84
|
-
|
|
85
|
-
const args = ["serve", "--port", "0", "--print-logs", "--log-level", "DEBUG"]
|
|
86
|
-
const env = { ...process.env, ...(options.environment ?? {}) }
|
|
87
|
-
|
|
88
|
-
let exitResolve: ((info: ProcessExitInfo) => void) | null = null
|
|
89
|
-
const exitPromise = new Promise<ProcessExitInfo>((resolveExit) => {
|
|
90
|
-
exitResolve = resolveExit
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
// Store recent output for debugging - keep last 50 lines from each stream
|
|
94
|
-
const MAX_OUTPUT_LINES = 50
|
|
95
|
-
const recentStdout: string[] = []
|
|
96
|
-
const recentStderr: string[] = []
|
|
97
|
-
const getLastOutput = () => {
|
|
98
|
-
const combined: string[] = []
|
|
99
|
-
if (recentStderr.length > 0) {
|
|
100
|
-
combined.push("Error Stream")
|
|
101
|
-
combined.push(...recentStderr.slice(-10))
|
|
102
|
-
}
|
|
103
|
-
if (recentStdout.length > 0) {
|
|
104
|
-
combined.push("Output Stream")
|
|
105
|
-
combined.push(...recentStdout.slice(-10))
|
|
106
|
-
}
|
|
107
|
-
return combined.join("\n")
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return new Promise((resolve, reject) => {
|
|
111
|
-
const spec = buildSpawnSpec(options.binaryPath, args)
|
|
112
|
-
const commandLine = [spec.command, ...spec.args].join(" ")
|
|
113
|
-
this.logger.info(
|
|
114
|
-
{
|
|
115
|
-
workspaceId: options.workspaceId,
|
|
116
|
-
folder: options.folder,
|
|
117
|
-
binary: options.binaryPath,
|
|
118
|
-
spawnCommand: spec.command,
|
|
119
|
-
commandLine,
|
|
120
|
-
},
|
|
121
|
-
"Launching OpenCode process",
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
this.logger.debug(
|
|
125
|
-
{
|
|
126
|
-
workspaceId: options.workspaceId,
|
|
127
|
-
spawnArgs: spec.args,
|
|
128
|
-
},
|
|
129
|
-
"OpenCode spawn args",
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
this.logger.trace(
|
|
133
|
-
{
|
|
134
|
-
workspaceId: options.workspaceId,
|
|
135
|
-
env: redactEnvironment(env),
|
|
136
|
-
},
|
|
137
|
-
"OpenCode spawn environment",
|
|
138
|
-
)
|
|
139
|
-
const detached = process.platform !== "win32"
|
|
140
|
-
const child = spawn(spec.command, spec.args, {
|
|
141
|
-
cwd: options.folder,
|
|
142
|
-
env,
|
|
143
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
144
|
-
detached,
|
|
145
|
-
...spec.options,
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
const managed: ManagedProcess = { child, requestedStop: false }
|
|
149
|
-
this.processes.set(options.workspaceId, managed)
|
|
150
|
-
|
|
151
|
-
let stdoutBuffer = ""
|
|
152
|
-
let stderrBuffer = ""
|
|
153
|
-
let portFound = false
|
|
154
|
-
|
|
155
|
-
let warningTimer: NodeJS.Timeout | null = null
|
|
156
|
-
|
|
157
|
-
const startWarningTimer = () => {
|
|
158
|
-
warningTimer = setInterval(() => {
|
|
159
|
-
this.logger.warn({ workspaceId: options.workspaceId }, "Workspace runtime has not reported a port yet")
|
|
160
|
-
}, 10000)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const stopWarningTimer = () => {
|
|
164
|
-
if (warningTimer) {
|
|
165
|
-
clearInterval(warningTimer)
|
|
166
|
-
warningTimer = null
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
startWarningTimer()
|
|
171
|
-
|
|
172
|
-
const cleanupStreams = () => {
|
|
173
|
-
stopWarningTimer()
|
|
174
|
-
child.stdout?.removeAllListeners()
|
|
175
|
-
child.stderr?.removeAllListeners()
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const handleExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
179
|
-
this.logger.info({ workspaceId: options.workspaceId, code, signal }, "OpenCode process exited")
|
|
180
|
-
this.processes.delete(options.workspaceId)
|
|
181
|
-
cleanupStreams()
|
|
182
|
-
child.removeListener("error", handleError)
|
|
183
|
-
child.removeListener("exit", handleExit)
|
|
184
|
-
const exitInfo: ProcessExitInfo = {
|
|
185
|
-
workspaceId: options.workspaceId,
|
|
186
|
-
code,
|
|
187
|
-
signal,
|
|
188
|
-
requested: managed.requestedStop,
|
|
189
|
-
}
|
|
190
|
-
if (exitResolve) {
|
|
191
|
-
exitResolve(exitInfo)
|
|
192
|
-
exitResolve = null
|
|
193
|
-
}
|
|
194
|
-
if (!portFound) {
|
|
195
|
-
const recentOutput = getLastOutput().trim()
|
|
196
|
-
const reason = recentOutput || stderrBuffer || `Process exited with code ${code}`
|
|
197
|
-
reject(new Error(reason))
|
|
198
|
-
} else {
|
|
199
|
-
options.onExit?.(exitInfo)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const handleError = (error: Error) => {
|
|
204
|
-
cleanupStreams()
|
|
205
|
-
child.removeListener("exit", handleExit)
|
|
206
|
-
this.processes.delete(options.workspaceId)
|
|
207
|
-
this.logger.error({ workspaceId: options.workspaceId, err: error }, "Workspace runtime error")
|
|
208
|
-
if (exitResolve) {
|
|
209
|
-
exitResolve({ workspaceId: options.workspaceId, code: null, signal: null, requested: managed.requestedStop })
|
|
210
|
-
exitResolve = null
|
|
211
|
-
}
|
|
212
|
-
reject(error)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
child.on("error", handleError)
|
|
216
|
-
child.on("exit", handleExit)
|
|
217
|
-
|
|
218
|
-
child.stdout?.on("data", (data: Buffer) => {
|
|
219
|
-
const text = data.toString()
|
|
220
|
-
stdoutBuffer += text
|
|
221
|
-
const lines = stdoutBuffer.split("\n")
|
|
222
|
-
stdoutBuffer = lines.pop() ?? ""
|
|
223
|
-
|
|
224
|
-
for (const line of lines) {
|
|
225
|
-
const trimmed = line.trim()
|
|
226
|
-
if (!trimmed) continue
|
|
227
|
-
|
|
228
|
-
recentStdout.push(trimmed)
|
|
229
|
-
if (recentStdout.length > MAX_OUTPUT_LINES) {
|
|
230
|
-
recentStdout.shift()
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
this.emitLog(options.workspaceId, "info", line)
|
|
234
|
-
|
|
235
|
-
if (!portFound) {
|
|
236
|
-
const portMatch = line.match(/opencode server listening on http:\/\/.+:(\d+)/i)
|
|
237
|
-
if (portMatch) {
|
|
238
|
-
portFound = true
|
|
239
|
-
stopWarningTimer()
|
|
240
|
-
child.removeListener("error", handleError)
|
|
241
|
-
const port = parseInt(portMatch[1], 10)
|
|
242
|
-
this.logger.info({ workspaceId: options.workspaceId, port }, "Workspace runtime allocated port")
|
|
243
|
-
resolve({ pid: child.pid!, port, exitPromise, getLastOutput })
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
child.stderr?.on("data", (data: Buffer) => {
|
|
250
|
-
const text = data.toString()
|
|
251
|
-
stderrBuffer += text
|
|
252
|
-
const lines = stderrBuffer.split("\n")
|
|
253
|
-
stderrBuffer = lines.pop() ?? ""
|
|
254
|
-
|
|
255
|
-
for (const line of lines) {
|
|
256
|
-
const trimmed = line.trim()
|
|
257
|
-
if (!trimmed) continue
|
|
258
|
-
|
|
259
|
-
recentStderr.push(trimmed)
|
|
260
|
-
if (recentStderr.length > MAX_OUTPUT_LINES) {
|
|
261
|
-
recentStderr.shift()
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
this.emitLog(options.workspaceId, "error", line)
|
|
265
|
-
}
|
|
266
|
-
})
|
|
267
|
-
})
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async stop(workspaceId: string): Promise<void> {
|
|
271
|
-
const managed = this.processes.get(workspaceId)
|
|
272
|
-
if (!managed) return
|
|
273
|
-
|
|
274
|
-
managed.requestedStop = true
|
|
275
|
-
const child = managed.child
|
|
276
|
-
this.logger.info({ workspaceId }, "Stopping OpenCode process")
|
|
277
|
-
|
|
278
|
-
const pid = child.pid
|
|
279
|
-
if (!pid) {
|
|
280
|
-
this.logger.warn({ workspaceId }, "Workspace process missing PID; cannot stop")
|
|
281
|
-
return
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const isAlreadyExited = () => child.exitCode !== null || child.signalCode !== null
|
|
285
|
-
|
|
286
|
-
const tryKillPosixGroup = (signal: NodeJS.Signals) => {
|
|
287
|
-
try {
|
|
288
|
-
// Negative PID targets the process group (POSIX).
|
|
289
|
-
process.kill(-pid, signal)
|
|
290
|
-
return true
|
|
291
|
-
} catch (error) {
|
|
292
|
-
const err = error as NodeJS.ErrnoException
|
|
293
|
-
if (err?.code === "ESRCH") {
|
|
294
|
-
return true
|
|
295
|
-
}
|
|
296
|
-
this.logger.debug({ workspaceId, pid, err }, "Failed to signal POSIX process group")
|
|
297
|
-
return false
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const tryKillSinglePid = (signal: NodeJS.Signals) => {
|
|
302
|
-
try {
|
|
303
|
-
process.kill(pid, signal)
|
|
304
|
-
return true
|
|
305
|
-
} catch (error) {
|
|
306
|
-
const err = error as NodeJS.ErrnoException
|
|
307
|
-
if (err?.code === "ESRCH") {
|
|
308
|
-
return true
|
|
309
|
-
}
|
|
310
|
-
this.logger.debug({ workspaceId, pid, err }, "Failed to signal workspace PID")
|
|
311
|
-
return false
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const tryTaskkill = (force: boolean) => {
|
|
316
|
-
const args = ["/PID", String(pid), "/T"]
|
|
317
|
-
if (force) {
|
|
318
|
-
args.push("/F")
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
const result = spawnSync("taskkill", args, { encoding: "utf8" })
|
|
323
|
-
const exitCode = result.status
|
|
324
|
-
if (exitCode === 0) {
|
|
325
|
-
return true
|
|
326
|
-
}
|
|
327
|
-
// If the PID is already gone, treat it as success.
|
|
328
|
-
const stderr = (result.stderr ?? "").toString().toLowerCase()
|
|
329
|
-
const stdout = (result.stdout ?? "").toString().toLowerCase()
|
|
330
|
-
const combined = `${stdout}\n${stderr}`
|
|
331
|
-
if (combined.includes("not found") || combined.includes("no running instance") || combined.includes("process") && combined.includes("not")) {
|
|
332
|
-
return true
|
|
333
|
-
}
|
|
334
|
-
this.logger.debug({ workspaceId, pid, exitCode, stderr: result.stderr, stdout: result.stdout }, "taskkill failed")
|
|
335
|
-
return false
|
|
336
|
-
} catch (error) {
|
|
337
|
-
this.logger.debug({ workspaceId, pid, err: error }, "taskkill failed to execute")
|
|
338
|
-
return false
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const sendStopSignal = (signal: NodeJS.Signals) => {
|
|
343
|
-
if (process.platform === "win32") {
|
|
344
|
-
// Best-effort: terminate the whole process tree rooted at pid.
|
|
345
|
-
// Use /F only for escalation.
|
|
346
|
-
tryTaskkill(signal === "SIGKILL")
|
|
347
|
-
return
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Prefer process-group signaling so wrapper launchers (bun/node) don't orphan the real server.
|
|
351
|
-
const groupOk = tryKillPosixGroup(signal)
|
|
352
|
-
if (!groupOk) {
|
|
353
|
-
// Fallback to direct PID kill.
|
|
354
|
-
tryKillSinglePid(signal)
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
await new Promise<void>((resolve, reject) => {
|
|
359
|
-
let escalationTimer: NodeJS.Timeout | null = null
|
|
360
|
-
|
|
361
|
-
const cleanup = () => {
|
|
362
|
-
child.removeListener("exit", onExit)
|
|
363
|
-
child.removeListener("error", onError)
|
|
364
|
-
if (escalationTimer) {
|
|
365
|
-
clearTimeout(escalationTimer)
|
|
366
|
-
escalationTimer = null
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const onExit = () => {
|
|
371
|
-
cleanup()
|
|
372
|
-
resolve()
|
|
373
|
-
}
|
|
374
|
-
const onError = (error: Error) => {
|
|
375
|
-
cleanup()
|
|
376
|
-
reject(error)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (isAlreadyExited()) {
|
|
380
|
-
this.logger.debug({ workspaceId, exitCode: child.exitCode, signal: child.signalCode }, "Process already exited")
|
|
381
|
-
cleanup()
|
|
382
|
-
resolve()
|
|
383
|
-
return
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
child.once("exit", onExit)
|
|
387
|
-
child.once("error", onError)
|
|
388
|
-
|
|
389
|
-
this.logger.debug(
|
|
390
|
-
{ workspaceId, pid, detached: process.platform !== "win32" },
|
|
391
|
-
"Sending SIGTERM to workspace process (tree/group)",
|
|
392
|
-
)
|
|
393
|
-
sendStopSignal("SIGTERM")
|
|
394
|
-
|
|
395
|
-
escalationTimer = setTimeout(() => {
|
|
396
|
-
escalationTimer = null
|
|
397
|
-
if (isAlreadyExited()) {
|
|
398
|
-
this.logger.debug({ workspaceId, pid }, "Workspace exited before SIGKILL escalation")
|
|
399
|
-
return
|
|
400
|
-
}
|
|
401
|
-
this.logger.warn({ workspaceId, pid }, "Process did not stop after SIGTERM, escalating")
|
|
402
|
-
sendStopSignal("SIGKILL")
|
|
403
|
-
}, 2000)
|
|
404
|
-
})
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
private emitLog(workspaceId: string, level: LogLevel, message: string) {
|
|
408
|
-
const entry: WorkspaceLogEntry = {
|
|
409
|
-
workspaceId,
|
|
410
|
-
timestamp: new Date().toISOString(),
|
|
411
|
-
level,
|
|
412
|
-
message: message.trim(),
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
this.eventBus.publish({ type: "workspace.log", entry })
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
private validateFolder(folder: string) {
|
|
419
|
-
const resolved = path.resolve(folder)
|
|
420
|
-
if (!existsSync(resolved)) {
|
|
421
|
-
throw new Error(`Folder does not exist: ${resolved}`)
|
|
422
|
-
}
|
|
423
|
-
const stats = statSync(resolved)
|
|
424
|
-
if (!stats.isDirectory()) {
|
|
425
|
-
throw new Error(`Path is not a directory: ${resolved}`)
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import fs from "fs"
|
|
2
|
-
import { promises as fsp } from "fs"
|
|
3
|
-
import path from "path"
|
|
4
|
-
import type { WorktreeMap } from "../api-types"
|
|
5
|
-
import { resolveRepoRoot } from "./git-worktrees"
|
|
6
|
-
import type { LogLike } from "./git-worktrees"
|
|
7
|
-
|
|
8
|
-
const DEFAULT_MAP: WorktreeMap = {
|
|
9
|
-
version: 1,
|
|
10
|
-
defaultWorktreeSlug: "root",
|
|
11
|
-
parentSessionWorktreeSlug: {},
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function getMapPath(repoRoot: string): string {
|
|
15
|
-
return path.join(repoRoot, ".codenomad", "worktreeMap.json")
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getGitExcludePath(repoRoot: string): string {
|
|
19
|
-
return path.join(repoRoot, ".git", "info", "exclude")
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function ensureGitExclude(repoRoot: string, logger?: LogLike): Promise<void> {
|
|
23
|
-
const excludePath = getGitExcludePath(repoRoot)
|
|
24
|
-
try {
|
|
25
|
-
await fsp.mkdir(path.dirname(excludePath), { recursive: true })
|
|
26
|
-
} catch {
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const entries = [
|
|
31
|
-
".codenomad/worktrees/",
|
|
32
|
-
".codenomad/worktreeMap.json",
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
let existing = ""
|
|
36
|
-
try {
|
|
37
|
-
existing = await fsp.readFile(excludePath, "utf-8")
|
|
38
|
-
} catch (error) {
|
|
39
|
-
const code = (error as NodeJS.ErrnoException).code
|
|
40
|
-
if (code !== "ENOENT") {
|
|
41
|
-
logger?.debug?.({ err: error, excludePath }, "Failed to read .git/info/exclude")
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
existing = ""
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const lines = new Set(existing.split(/\r?\n/).map((l) => l.trim()).filter(Boolean))
|
|
48
|
-
const missing = entries.filter((e) => !lines.has(e))
|
|
49
|
-
if (missing.length === 0) {
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const header = existing.includes("# codenomad") ? "" : (existing.trim() ? "\n" : "") + "# codenomad\n"
|
|
54
|
-
const suffix = missing.map((e) => `${e}\n`).join("")
|
|
55
|
-
await fsp.writeFile(excludePath, `${existing}${header}${suffix}`, "utf-8")
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function ensureCodenomadGitExclude(workspaceFolder: string, logger?: LogLike): Promise<void> {
|
|
59
|
-
const { repoRoot, isGitRepo } = await resolveRepoRoot(workspaceFolder, logger)
|
|
60
|
-
if (!isGitRepo) {
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
await ensureGitExclude(repoRoot, logger)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function readWorktreeMap(workspaceFolder: string, logger?: LogLike): Promise<WorktreeMap> {
|
|
67
|
-
const { repoRoot, isGitRepo } = await resolveRepoRoot(workspaceFolder, logger)
|
|
68
|
-
const filePath = getMapPath(repoRoot)
|
|
69
|
-
try {
|
|
70
|
-
const raw = await fsp.readFile(filePath, "utf-8")
|
|
71
|
-
const parsed = JSON.parse(raw)
|
|
72
|
-
if (!parsed || typeof parsed !== "object") {
|
|
73
|
-
return DEFAULT_MAP
|
|
74
|
-
}
|
|
75
|
-
const version = (parsed as any).version
|
|
76
|
-
if (version !== 1) {
|
|
77
|
-
return DEFAULT_MAP
|
|
78
|
-
}
|
|
79
|
-
const defaultWorktreeSlug = typeof (parsed as any).defaultWorktreeSlug === "string" ? (parsed as any).defaultWorktreeSlug : "root"
|
|
80
|
-
const parentSessionWorktreeSlug = (parsed as any).parentSessionWorktreeSlug
|
|
81
|
-
const mapping = parentSessionWorktreeSlug && typeof parentSessionWorktreeSlug === "object" ? parentSessionWorktreeSlug : {}
|
|
82
|
-
return {
|
|
83
|
-
version: 1,
|
|
84
|
-
defaultWorktreeSlug,
|
|
85
|
-
parentSessionWorktreeSlug: { ...mapping },
|
|
86
|
-
}
|
|
87
|
-
} catch (error) {
|
|
88
|
-
const code = (error as NodeJS.ErrnoException).code
|
|
89
|
-
if (code === "ENOENT") {
|
|
90
|
-
if (isGitRepo) {
|
|
91
|
-
// Best-effort ignore setup on first use.
|
|
92
|
-
await ensureGitExclude(repoRoot, logger).catch(() => undefined)
|
|
93
|
-
}
|
|
94
|
-
return DEFAULT_MAP
|
|
95
|
-
}
|
|
96
|
-
logger?.warn?.({ err: error, filePath }, "Failed to read worktree map")
|
|
97
|
-
return DEFAULT_MAP
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function writeWorktreeMap(workspaceFolder: string, next: WorktreeMap, logger?: LogLike): Promise<void> {
|
|
102
|
-
const { repoRoot, isGitRepo } = await resolveRepoRoot(workspaceFolder, logger)
|
|
103
|
-
const filePath = getMapPath(repoRoot)
|
|
104
|
-
await fsp.mkdir(path.dirname(filePath), { recursive: true })
|
|
105
|
-
|
|
106
|
-
// Ensure ignore rules are present (local-only).
|
|
107
|
-
if (isGitRepo) {
|
|
108
|
-
await ensureGitExclude(repoRoot, logger).catch(() => undefined)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const payload: WorktreeMap = {
|
|
112
|
-
version: 1,
|
|
113
|
-
defaultWorktreeSlug: next.defaultWorktreeSlug || "root",
|
|
114
|
-
parentSessionWorktreeSlug: next.parentSessionWorktreeSlug ?? {},
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Write atomically.
|
|
118
|
-
const tmpPath = `${filePath}.${process.pid}.tmp`
|
|
119
|
-
await fsp.writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf-8")
|
|
120
|
-
await fsp.rename(tmpPath, filePath)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function worktreeMapExists(repoRoot: string): boolean {
|
|
124
|
-
try {
|
|
125
|
-
return fs.existsSync(getMapPath(repoRoot))
|
|
126
|
-
} catch {
|
|
127
|
-
return false
|
|
128
|
-
}
|
|
129
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Node",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"resolveJsonModule": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"forceConsistentCasingInFileNames": true,
|
|
11
|
-
"outDir": "dist",
|
|
12
|
-
"rootDir": "src",
|
|
13
|
-
"types": ["node"]
|
|
14
|
-
},
|
|
15
|
-
"include": ["src/**/*.ts"],
|
|
16
|
-
"exclude": ["dist", "node_modules"]
|
|
17
|
-
}
|