@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,519 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync, type ChildProcess } from "child_process"
|
|
2
|
-
import { createWriteStream, existsSync, promises as fs } from "fs"
|
|
3
|
-
import path from "path"
|
|
4
|
-
import { randomBytes } from "crypto"
|
|
5
|
-
import type { EventBus } from "../events/bus"
|
|
6
|
-
import type { WorkspaceManager } from "../workspaces/manager"
|
|
7
|
-
import type { Logger } from "../logger"
|
|
8
|
-
import type { BackgroundProcess, BackgroundProcessStatus } from "../api-types"
|
|
9
|
-
|
|
10
|
-
const ROOT_DIR = ".codenomad/background_processes"
|
|
11
|
-
const INDEX_FILE = "index.json"
|
|
12
|
-
const OUTPUT_FILE = "output.txt"
|
|
13
|
-
const STOP_TIMEOUT_MS = 2000
|
|
14
|
-
const EXIT_WAIT_TIMEOUT_MS = 5000
|
|
15
|
-
const MAX_OUTPUT_BYTES = 20 * 1024
|
|
16
|
-
const OUTPUT_PUBLISH_INTERVAL_MS = 1000
|
|
17
|
-
|
|
18
|
-
interface ManagerDeps {
|
|
19
|
-
workspaceManager: WorkspaceManager
|
|
20
|
-
eventBus: EventBus
|
|
21
|
-
logger: Logger
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface RunningProcess {
|
|
25
|
-
id: string
|
|
26
|
-
child: ChildProcess
|
|
27
|
-
outputPath: string
|
|
28
|
-
exitPromise: Promise<void>
|
|
29
|
-
workspaceId: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class BackgroundProcessManager {
|
|
33
|
-
private readonly running = new Map<string, RunningProcess>()
|
|
34
|
-
|
|
35
|
-
constructor(private readonly deps: ManagerDeps) {
|
|
36
|
-
this.deps.eventBus.on("workspace.stopped", (event) => this.cleanupWorkspace(event.workspaceId))
|
|
37
|
-
this.deps.eventBus.on("workspace.error", (event) => this.cleanupWorkspace(event.workspace.id))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async list(workspaceId: string): Promise<BackgroundProcess[]> {
|
|
41
|
-
const records = await this.readIndex(workspaceId)
|
|
42
|
-
const enriched = await Promise.all(
|
|
43
|
-
records.map(async (record) => ({
|
|
44
|
-
...record,
|
|
45
|
-
outputSizeBytes: await this.getOutputSize(workspaceId, record.id),
|
|
46
|
-
})),
|
|
47
|
-
)
|
|
48
|
-
return enriched
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async start(workspaceId: string, title: string, command: string): Promise<BackgroundProcess> {
|
|
52
|
-
const workspace = this.deps.workspaceManager.get(workspaceId)
|
|
53
|
-
if (!workspace) {
|
|
54
|
-
throw new Error("Workspace not found")
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const id = this.generateId()
|
|
58
|
-
const processDir = await this.ensureProcessDir(workspaceId, id)
|
|
59
|
-
const outputPath = path.join(processDir, OUTPUT_FILE)
|
|
60
|
-
|
|
61
|
-
const outputStream = createWriteStream(outputPath, { flags: "a" })
|
|
62
|
-
|
|
63
|
-
const { shellCommand, shellArgs, spawnOptions } = this.buildShellSpawn(command)
|
|
64
|
-
|
|
65
|
-
const child = spawn(shellCommand, shellArgs, {
|
|
66
|
-
cwd: workspace.path,
|
|
67
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
68
|
-
detached: process.platform !== "win32",
|
|
69
|
-
...spawnOptions,
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
child.on("exit", () => {
|
|
73
|
-
this.killProcessTree(child, "SIGTERM")
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const record: BackgroundProcess = {
|
|
77
|
-
|
|
78
|
-
id,
|
|
79
|
-
workspaceId,
|
|
80
|
-
title,
|
|
81
|
-
command,
|
|
82
|
-
cwd: workspace.path,
|
|
83
|
-
status: "running",
|
|
84
|
-
pid: child.pid,
|
|
85
|
-
startedAt: new Date().toISOString(),
|
|
86
|
-
outputSizeBytes: 0,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const exitPromise = new Promise<void>((resolve) => {
|
|
90
|
-
child.on("close", async (code) => {
|
|
91
|
-
await new Promise<void>((resolve) => outputStream.end(resolve))
|
|
92
|
-
this.running.delete(id)
|
|
93
|
-
|
|
94
|
-
record.status = this.statusFromExit(code)
|
|
95
|
-
record.exitCode = code === null ? undefined : code
|
|
96
|
-
record.stoppedAt = new Date().toISOString()
|
|
97
|
-
|
|
98
|
-
await this.upsertIndex(workspaceId, record)
|
|
99
|
-
record.outputSizeBytes = await this.getOutputSize(workspaceId, record.id)
|
|
100
|
-
this.publishUpdate(workspaceId, record)
|
|
101
|
-
resolve()
|
|
102
|
-
})
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
this.running.set(id, { id, child, outputPath, exitPromise, workspaceId })
|
|
106
|
-
|
|
107
|
-
let lastPublishAt = 0
|
|
108
|
-
const maybePublishSize = () => {
|
|
109
|
-
const now = Date.now()
|
|
110
|
-
if (now - lastPublishAt < OUTPUT_PUBLISH_INTERVAL_MS) {
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
lastPublishAt = now
|
|
114
|
-
this.publishUpdate(workspaceId, record)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
child.stdout?.on("data", (data) => {
|
|
118
|
-
outputStream.write(data)
|
|
119
|
-
record.outputSizeBytes = (record.outputSizeBytes ?? 0) + data.length
|
|
120
|
-
maybePublishSize()
|
|
121
|
-
})
|
|
122
|
-
child.stderr?.on("data", (data) => {
|
|
123
|
-
outputStream.write(data)
|
|
124
|
-
record.outputSizeBytes = (record.outputSizeBytes ?? 0) + data.length
|
|
125
|
-
maybePublishSize()
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
await this.upsertIndex(workspaceId, record)
|
|
129
|
-
record.outputSizeBytes = await this.getOutputSize(workspaceId, record.id)
|
|
130
|
-
this.publishUpdate(workspaceId, record)
|
|
131
|
-
return record
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async stop(workspaceId: string, processId: string): Promise<BackgroundProcess | null> {
|
|
135
|
-
const record = await this.findProcess(workspaceId, processId)
|
|
136
|
-
if (!record) {
|
|
137
|
-
return null
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const running = this.running.get(processId)
|
|
141
|
-
if (running?.child && !running.child.killed) {
|
|
142
|
-
this.killProcessTree(running.child, "SIGTERM")
|
|
143
|
-
await this.waitForExit(running)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (record.status === "running") {
|
|
147
|
-
record.status = "stopped"
|
|
148
|
-
record.stoppedAt = new Date().toISOString()
|
|
149
|
-
await this.upsertIndex(workspaceId, record)
|
|
150
|
-
record.outputSizeBytes = await this.getOutputSize(workspaceId, record.id)
|
|
151
|
-
this.publishUpdate(workspaceId, record)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return record
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async terminate(workspaceId: string, processId: string): Promise<void> {
|
|
158
|
-
const record = await this.findProcess(workspaceId, processId)
|
|
159
|
-
if (!record) return
|
|
160
|
-
|
|
161
|
-
const running = this.running.get(processId)
|
|
162
|
-
if (running?.child && !running.child.killed) {
|
|
163
|
-
this.killProcessTree(running.child, "SIGTERM")
|
|
164
|
-
await this.waitForExit(running)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
await this.removeFromIndex(workspaceId, processId)
|
|
168
|
-
await this.removeProcessDir(workspaceId, processId)
|
|
169
|
-
|
|
170
|
-
this.deps.eventBus.publish({
|
|
171
|
-
type: "instance.event",
|
|
172
|
-
instanceId: workspaceId,
|
|
173
|
-
event: { type: "background.process.removed", properties: { processId } },
|
|
174
|
-
})
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async readOutput(
|
|
178
|
-
workspaceId: string,
|
|
179
|
-
processId: string,
|
|
180
|
-
options: { method?: "full" | "tail" | "head" | "grep"; pattern?: string; lines?: number; maxBytes?: number },
|
|
181
|
-
) {
|
|
182
|
-
const outputPath = this.getOutputPath(workspaceId, processId)
|
|
183
|
-
if (!existsSync(outputPath)) {
|
|
184
|
-
return { id: processId, content: "", truncated: false, sizeBytes: 0 }
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const stats = await fs.stat(outputPath)
|
|
188
|
-
const sizeBytes = stats.size
|
|
189
|
-
const method = options.method ?? "full"
|
|
190
|
-
const lineCount = options.lines ?? 10
|
|
191
|
-
|
|
192
|
-
const raw = await this.readOutputBytes(outputPath, sizeBytes, options.maxBytes)
|
|
193
|
-
let content = raw
|
|
194
|
-
|
|
195
|
-
switch (method) {
|
|
196
|
-
case "head":
|
|
197
|
-
content = this.headLines(raw, lineCount)
|
|
198
|
-
break
|
|
199
|
-
case "tail":
|
|
200
|
-
content = this.tailLines(raw, lineCount)
|
|
201
|
-
break
|
|
202
|
-
case "grep":
|
|
203
|
-
if (!options.pattern) {
|
|
204
|
-
throw new Error("Pattern is required for grep output")
|
|
205
|
-
}
|
|
206
|
-
content = this.grepLines(raw, options.pattern)
|
|
207
|
-
break
|
|
208
|
-
default:
|
|
209
|
-
content = raw
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const effectiveMaxBytes = options.maxBytes
|
|
213
|
-
return {
|
|
214
|
-
id: processId,
|
|
215
|
-
content,
|
|
216
|
-
truncated: effectiveMaxBytes !== undefined && sizeBytes > effectiveMaxBytes,
|
|
217
|
-
sizeBytes,
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async streamOutput(workspaceId: string, processId: string, reply: any) {
|
|
222
|
-
const outputPath = this.getOutputPath(workspaceId, processId)
|
|
223
|
-
if (!existsSync(outputPath)) {
|
|
224
|
-
reply.code(404).send({ error: "Output not found" })
|
|
225
|
-
return
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
reply.raw.setHeader("Content-Type", "text/event-stream")
|
|
229
|
-
reply.raw.setHeader("Cache-Control", "no-cache")
|
|
230
|
-
reply.raw.setHeader("Connection", "keep-alive")
|
|
231
|
-
reply.raw.flushHeaders?.()
|
|
232
|
-
reply.hijack()
|
|
233
|
-
|
|
234
|
-
const file = await fs.open(outputPath, "r")
|
|
235
|
-
let position = (await file.stat()).size
|
|
236
|
-
|
|
237
|
-
const tick = async () => {
|
|
238
|
-
const stats = await file.stat()
|
|
239
|
-
if (stats.size <= position) return
|
|
240
|
-
|
|
241
|
-
const length = stats.size - position
|
|
242
|
-
const buffer = Buffer.alloc(length)
|
|
243
|
-
await file.read(buffer, 0, length, position)
|
|
244
|
-
position = stats.size
|
|
245
|
-
|
|
246
|
-
const content = buffer.toString("utf-8")
|
|
247
|
-
reply.raw.write(`data: ${JSON.stringify({ type: "chunk", content })}\n\n`)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const interval = setInterval(() => {
|
|
251
|
-
tick().catch((error) => {
|
|
252
|
-
this.deps.logger.warn({ err: error }, "Failed to stream background process output")
|
|
253
|
-
})
|
|
254
|
-
}, 1000)
|
|
255
|
-
|
|
256
|
-
const close = () => {
|
|
257
|
-
clearInterval(interval)
|
|
258
|
-
file.close().catch(() => undefined)
|
|
259
|
-
reply.raw.end?.()
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
reply.raw.on("close", close)
|
|
263
|
-
reply.raw.on("error", close)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private async cleanupWorkspace(workspaceId: string) {
|
|
267
|
-
for (const [, running] of this.running.entries()) {
|
|
268
|
-
if (running.workspaceId !== workspaceId) continue
|
|
269
|
-
this.killProcessTree(running.child, "SIGTERM")
|
|
270
|
-
await this.waitForExit(running)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
await this.removeWorkspaceDir(workspaceId)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private killProcessTree(child: ChildProcess, signal: NodeJS.Signals) {
|
|
277
|
-
const pid = child.pid
|
|
278
|
-
if (!pid) return
|
|
279
|
-
|
|
280
|
-
if (process.platform === "win32") {
|
|
281
|
-
const args = this.buildWindowsTaskkillArgs(pid, signal)
|
|
282
|
-
try {
|
|
283
|
-
spawnSync("taskkill", args, { stdio: "ignore" })
|
|
284
|
-
return
|
|
285
|
-
} catch {
|
|
286
|
-
// Fall back to killing the direct child.
|
|
287
|
-
}
|
|
288
|
-
} else {
|
|
289
|
-
try {
|
|
290
|
-
process.kill(-pid, signal)
|
|
291
|
-
return
|
|
292
|
-
} catch {
|
|
293
|
-
// Fall back to killing the direct child.
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
child.kill(signal)
|
|
299
|
-
} catch {
|
|
300
|
-
// ignore
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private async waitForExit(running: RunningProcess) {
|
|
305
|
-
let exited = false
|
|
306
|
-
const exitPromise = running.exitPromise.finally(() => {
|
|
307
|
-
exited = true
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
const killTimeout = setTimeout(() => {
|
|
311
|
-
if (!exited) {
|
|
312
|
-
this.killProcessTree(running.child, "SIGKILL")
|
|
313
|
-
}
|
|
314
|
-
}, STOP_TIMEOUT_MS)
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
await Promise.race([
|
|
318
|
-
exitPromise,
|
|
319
|
-
new Promise<void>((resolve) => {
|
|
320
|
-
setTimeout(resolve, EXIT_WAIT_TIMEOUT_MS)
|
|
321
|
-
}),
|
|
322
|
-
])
|
|
323
|
-
|
|
324
|
-
if (!exited) {
|
|
325
|
-
this.killProcessTree(running.child, "SIGKILL")
|
|
326
|
-
this.running.delete(running.id)
|
|
327
|
-
this.deps.logger.warn({ pid: running.child.pid }, "Timed out waiting for background process to exit")
|
|
328
|
-
}
|
|
329
|
-
} finally {
|
|
330
|
-
clearTimeout(killTimeout)
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
private buildShellSpawn(command: string): { shellCommand: string; shellArgs: string[]; spawnOptions?: Record<string, unknown> } {
|
|
336
|
-
if (process.platform === "win32") {
|
|
337
|
-
const comspec = process.env.ComSpec || "cmd.exe"
|
|
338
|
-
return {
|
|
339
|
-
shellCommand: comspec,
|
|
340
|
-
shellArgs: ["/d", "/s", "/c", command],
|
|
341
|
-
spawnOptions: { windowsVerbatimArguments: true },
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Keep bash for macOS/Linux.
|
|
346
|
-
return { shellCommand: "bash", shellArgs: ["-c", command] }
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private buildWindowsTaskkillArgs(pid: number, signal: NodeJS.Signals): string[] {
|
|
350
|
-
// Default to graceful termination (no /F), then force kill when we escalate.
|
|
351
|
-
const force = signal === "SIGKILL"
|
|
352
|
-
const args = ["/PID", String(pid), "/T"]
|
|
353
|
-
if (force) {
|
|
354
|
-
args.push("/F")
|
|
355
|
-
}
|
|
356
|
-
return args
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
private statusFromExit(code: number | null): BackgroundProcessStatus {
|
|
360
|
-
if (code === null) return "stopped"
|
|
361
|
-
if (code === 0) return "stopped"
|
|
362
|
-
return "error"
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
private async readOutputBytes(outputPath: string, sizeBytes: number, maxBytes?: number): Promise<string> {
|
|
366
|
-
if (maxBytes === undefined || sizeBytes <= maxBytes) {
|
|
367
|
-
return await fs.readFile(outputPath, "utf-8")
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const start = Math.max(0, sizeBytes - maxBytes)
|
|
371
|
-
const file = await fs.open(outputPath, "r")
|
|
372
|
-
const buffer = Buffer.alloc(sizeBytes - start)
|
|
373
|
-
await file.read(buffer, 0, buffer.length, start)
|
|
374
|
-
await file.close()
|
|
375
|
-
return buffer.toString("utf-8")
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
private headLines(input: string, lines: number): string {
|
|
379
|
-
const parts = input.split(/\r?\n/)
|
|
380
|
-
return parts.slice(0, Math.max(0, lines)).join("\n")
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
private tailLines(input: string, lines: number): string {
|
|
384
|
-
const parts = input.split(/\r?\n/)
|
|
385
|
-
return parts.slice(Math.max(0, parts.length - lines)).join("\n")
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
private grepLines(input: string, pattern: string): string {
|
|
389
|
-
let matcher: RegExp
|
|
390
|
-
try {
|
|
391
|
-
matcher = new RegExp(pattern)
|
|
392
|
-
} catch {
|
|
393
|
-
throw new Error("Invalid grep pattern")
|
|
394
|
-
}
|
|
395
|
-
return input
|
|
396
|
-
.split(/\r?\n/)
|
|
397
|
-
.filter((line) => matcher.test(line))
|
|
398
|
-
.join("\n")
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
private async ensureProcessDir(workspaceId: string, processId: string) {
|
|
402
|
-
const root = await this.ensureWorkspaceDir(workspaceId)
|
|
403
|
-
const processDir = path.join(root, processId)
|
|
404
|
-
await fs.mkdir(processDir, { recursive: true })
|
|
405
|
-
return processDir
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
private async ensureWorkspaceDir(workspaceId: string) {
|
|
409
|
-
const workspace = this.deps.workspaceManager.get(workspaceId)
|
|
410
|
-
if (!workspace) {
|
|
411
|
-
throw new Error("Workspace not found")
|
|
412
|
-
}
|
|
413
|
-
const root = path.join(workspace.path, ROOT_DIR, workspaceId)
|
|
414
|
-
await fs.mkdir(root, { recursive: true })
|
|
415
|
-
return root
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
private getOutputPath(workspaceId: string, processId: string) {
|
|
419
|
-
const workspace = this.deps.workspaceManager.get(workspaceId)
|
|
420
|
-
if (!workspace) {
|
|
421
|
-
throw new Error("Workspace not found")
|
|
422
|
-
}
|
|
423
|
-
return path.join(workspace.path, ROOT_DIR, workspaceId, processId, OUTPUT_FILE)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
private async findProcess(workspaceId: string, processId: string): Promise<BackgroundProcess | null> {
|
|
427
|
-
const records = await this.readIndex(workspaceId)
|
|
428
|
-
return records.find((entry) => entry.id === processId) ?? null
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
private async readIndex(workspaceId: string): Promise<BackgroundProcess[]> {
|
|
432
|
-
const indexPath = await this.getIndexPath(workspaceId)
|
|
433
|
-
if (!existsSync(indexPath)) return []
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
const raw = await fs.readFile(indexPath, "utf-8")
|
|
437
|
-
const parsed = JSON.parse(raw)
|
|
438
|
-
return Array.isArray(parsed) ? (parsed as BackgroundProcess[]) : []
|
|
439
|
-
} catch {
|
|
440
|
-
return []
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
private async upsertIndex(workspaceId: string, record: BackgroundProcess) {
|
|
445
|
-
const records = await this.readIndex(workspaceId)
|
|
446
|
-
const index = records.findIndex((entry) => entry.id === record.id)
|
|
447
|
-
if (index >= 0) {
|
|
448
|
-
records[index] = record
|
|
449
|
-
} else {
|
|
450
|
-
records.push(record)
|
|
451
|
-
}
|
|
452
|
-
await this.writeIndex(workspaceId, records)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private async removeFromIndex(workspaceId: string, processId: string) {
|
|
456
|
-
const records = await this.readIndex(workspaceId)
|
|
457
|
-
const next = records.filter((entry) => entry.id !== processId)
|
|
458
|
-
await this.writeIndex(workspaceId, next)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
private async writeIndex(workspaceId: string, records: BackgroundProcess[]) {
|
|
462
|
-
const indexPath = await this.getIndexPath(workspaceId)
|
|
463
|
-
await fs.mkdir(path.dirname(indexPath), { recursive: true })
|
|
464
|
-
await fs.writeFile(indexPath, JSON.stringify(records, null, 2))
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
private async getIndexPath(workspaceId: string) {
|
|
468
|
-
const workspace = this.deps.workspaceManager.get(workspaceId)
|
|
469
|
-
if (!workspace) {
|
|
470
|
-
throw new Error("Workspace not found")
|
|
471
|
-
}
|
|
472
|
-
return path.join(workspace.path, ROOT_DIR, workspaceId, INDEX_FILE)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
private async removeProcessDir(workspaceId: string, processId: string) {
|
|
476
|
-
const workspace = this.deps.workspaceManager.get(workspaceId)
|
|
477
|
-
if (!workspace) {
|
|
478
|
-
return
|
|
479
|
-
}
|
|
480
|
-
const processDir = path.join(workspace.path, ROOT_DIR, workspaceId, processId)
|
|
481
|
-
await fs.rm(processDir, { recursive: true, force: true })
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
private async removeWorkspaceDir(workspaceId: string) {
|
|
485
|
-
const workspace = this.deps.workspaceManager.get(workspaceId)
|
|
486
|
-
if (!workspace) {
|
|
487
|
-
return
|
|
488
|
-
}
|
|
489
|
-
const workspaceDir = path.join(workspace.path, ROOT_DIR, workspaceId)
|
|
490
|
-
await fs.rm(workspaceDir, { recursive: true, force: true })
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
private async getOutputSize(workspaceId: string, processId: string): Promise<number> {
|
|
494
|
-
const outputPath = this.getOutputPath(workspaceId, processId)
|
|
495
|
-
if (!existsSync(outputPath)) {
|
|
496
|
-
return 0
|
|
497
|
-
}
|
|
498
|
-
try {
|
|
499
|
-
const stats = await fs.stat(outputPath)
|
|
500
|
-
return stats.size
|
|
501
|
-
} catch {
|
|
502
|
-
return 0
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private publishUpdate(workspaceId: string, record: BackgroundProcess) {
|
|
507
|
-
this.deps.eventBus.publish({
|
|
508
|
-
type: "instance.event",
|
|
509
|
-
instanceId: workspaceId,
|
|
510
|
-
event: { type: "background.process.updated", properties: { process: record } },
|
|
511
|
-
})
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
private generateId(): string {
|
|
515
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15)
|
|
516
|
-
const random = randomBytes(3).toString("hex")
|
|
517
|
-
return `proc_${timestamp}_${random}`
|
|
518
|
-
}
|
|
519
|
-
}
|
package/src/bin.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawn } from "child_process"
|
|
4
|
-
import path from "path"
|
|
5
|
-
import { fileURLToPath, pathToFileURL } from "url"
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
-
const __dirname = path.dirname(__filename)
|
|
9
|
-
const cliEntry = path.join(__dirname, "index.js")
|
|
10
|
-
const loaderFileUrl = pathToFileURL(path.join(__dirname, "loader.js")).href
|
|
11
|
-
const registerScript = `import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("${encodeURI(loaderFileUrl)}", pathToFileURL("./"));`
|
|
12
|
-
const loaderArg = `data:text/javascript,${registerScript}`
|
|
13
|
-
|
|
14
|
-
const child = spawn(process.execPath, ["--import", loaderArg, cliEntry, ...process.argv.slice(2)], {
|
|
15
|
-
stdio: "inherit",
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
child.on("exit", (code, signal) => {
|
|
19
|
-
if (signal) {
|
|
20
|
-
process.kill(process.pid, signal)
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
process.exit(code ?? 0)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
child.on("error", (error) => {
|
|
27
|
-
console.error("Failed to launch CLI runtime", error)
|
|
28
|
-
process.exit(1)
|
|
29
|
-
})
|