novacode 0.5.2 → 0.5.5
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/README.md +15 -4
- package/dist/app-QfQR2FN9.mjs +21 -0
- package/dist/app-QfQR2FN9.mjs.map +1 -0
- package/dist/main.mjs +110 -0
- package/dist/main.mjs.map +1 -0
- package/package.json +25 -18
- package/src/agent/loop.ts +27 -8
- package/src/commands/index.ts +4 -3
- package/src/commands/models.ts +8 -9
- package/src/commands/providers.ts +63 -72
- package/src/config/providers.ts +12 -4
- package/src/config/store.ts +6 -7
- package/src/main.ts +7 -6
- package/src/onboarding/wizard.ts +26 -30
- package/src/provider/gemini.ts +12 -5
- package/src/provider/openai.ts +6 -9
- package/src/provider/stream.ts +64 -3
- package/src/session/compact.ts +1 -1
- package/src/session/store.ts +59 -36
- package/src/tools/fs.ts +10 -16
- package/src/tools/git.ts +26 -9
- package/src/tools/search.ts +33 -11
- package/src/tools/shell.ts +26 -25
- package/src/tui/app.tsx +141 -324
- package/src/tui/components/liveArea.tsx +73 -0
- package/src/tui/components/message.tsx +113 -0
- package/src/tui/components/statusBar.tsx +58 -0
- package/src/tui/prompts.tsx +205 -0
- package/src/types.ts +15 -0
- package/src/update.ts +44 -23
- package/src/util.ts +1 -28
- package/src/provider/registry.ts +0 -62
package/src/session/store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unlinkSync } from "node:fs"
|
|
2
2
|
import { join } from "node:path"
|
|
3
|
+
import BetterSqlite3 from "better-sqlite3"
|
|
3
4
|
import type { Msg, Session } from "../types.ts"
|
|
4
5
|
|
|
5
6
|
const SCHEMA = `
|
|
@@ -40,13 +41,35 @@ function generateId(): string {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export class SessionStore {
|
|
43
|
-
#db: Database
|
|
44
|
+
#db: BetterSqlite3.Database
|
|
44
45
|
|
|
45
46
|
constructor(dbPath: string) {
|
|
46
|
-
this.#db =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
this.#db = SessionStore.#open(dbPath)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Opens and fully initialises the DB. If anything throws (e.g. corrupt file),
|
|
51
|
+
// the bad file is deleted and a fresh DB is created and returned.
|
|
52
|
+
static #open(dbPath: string): BetterSqlite3.Database {
|
|
53
|
+
const init = (db: BetterSqlite3.Database) => {
|
|
54
|
+
db.pragma("journal_mode = WAL")
|
|
55
|
+
db.pragma("foreign_keys = ON")
|
|
56
|
+
db.exec(SCHEMA)
|
|
57
|
+
return db
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return init(new BetterSqlite3(dbPath))
|
|
61
|
+
} catch {
|
|
62
|
+
// Delete the main DB and WAL sidecar files — all three must go or
|
|
63
|
+
// SQLite will fail again trying to replay a corrupt WAL on reopen.
|
|
64
|
+
for (const f of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
65
|
+
try {
|
|
66
|
+
unlinkSync(f)
|
|
67
|
+
} catch {
|
|
68
|
+
// file may already be absent — ignore
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return init(new BetterSqlite3(dbPath))
|
|
72
|
+
}
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
create(cwd: string, model: string, provider: string): Session {
|
|
@@ -57,13 +80,13 @@ export class SessionStore {
|
|
|
57
80
|
"INSERT INTO sessions (id, cwd, model, provider, title, created, updated) VALUES ($id, $cwd, $model, $provider, $title, $created, $updated)",
|
|
58
81
|
)
|
|
59
82
|
.run({
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
id: id,
|
|
84
|
+
cwd: cwd,
|
|
85
|
+
model: model,
|
|
86
|
+
provider: provider,
|
|
87
|
+
title: null,
|
|
88
|
+
created: now,
|
|
89
|
+
updated: now,
|
|
67
90
|
})
|
|
68
91
|
return { id, cwd, model, provider, title: null, created: now, updated: now }
|
|
69
92
|
}
|
|
@@ -74,7 +97,7 @@ export class SessionStore {
|
|
|
74
97
|
.prepare(
|
|
75
98
|
"SELECT id, cwd, model, provider, title, created, updated FROM sessions WHERE id = $id",
|
|
76
99
|
)
|
|
77
|
-
.get({
|
|
100
|
+
.get({ id: id }) as Session | null) ?? null
|
|
78
101
|
)
|
|
79
102
|
}
|
|
80
103
|
|
|
@@ -83,11 +106,11 @@ export class SessionStore {
|
|
|
83
106
|
.prepare(
|
|
84
107
|
"SELECT id, cwd, model, provider, title, created, updated FROM sessions ORDER BY updated DESC LIMIT $limit",
|
|
85
108
|
)
|
|
86
|
-
.all({
|
|
109
|
+
.all({ limit: limit }) as Session[]
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
delete(id: string): boolean {
|
|
90
|
-
const result = this.#db.prepare("DELETE FROM sessions WHERE id = $id").run({
|
|
113
|
+
const result = this.#db.prepare("DELETE FROM sessions WHERE id = $id").run({ id: id })
|
|
91
114
|
return result.changes > 0
|
|
92
115
|
}
|
|
93
116
|
|
|
@@ -98,15 +121,15 @@ export class SessionStore {
|
|
|
98
121
|
"INSERT INTO messages (session_id, seq, role, content, ts) VALUES ($sid, $seq, $role, $content, $ts)",
|
|
99
122
|
)
|
|
100
123
|
.run({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
sid: sessionId,
|
|
125
|
+
seq: seq,
|
|
126
|
+
role: msg.role,
|
|
127
|
+
content: JSON.stringify(msg),
|
|
128
|
+
ts: msg.ts,
|
|
106
129
|
})
|
|
107
130
|
this.#db
|
|
108
131
|
.prepare("UPDATE sessions SET updated = $now WHERE id = $id")
|
|
109
|
-
.run({
|
|
132
|
+
.run({ now: Date.now(), id: sessionId })
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
appendMany(sessionId: string, msgs: Msg[]): void {
|
|
@@ -121,7 +144,7 @@ export class SessionStore {
|
|
|
121
144
|
messages(sessionId: string): Msg[] {
|
|
122
145
|
const rows = this.#db
|
|
123
146
|
.prepare("SELECT content FROM messages WHERE session_id = $sid ORDER BY seq ASC")
|
|
124
|
-
.all({
|
|
147
|
+
.all({ sid: sessionId }) as { content: string }[]
|
|
125
148
|
return rows.map((r) => JSON.parse(r.content) as Msg)
|
|
126
149
|
}
|
|
127
150
|
|
|
@@ -130,20 +153,20 @@ export class SessionStore {
|
|
|
130
153
|
.prepare(
|
|
131
154
|
"SELECT content FROM messages WHERE session_id = $sid AND seq > $seq ORDER BY seq ASC",
|
|
132
155
|
)
|
|
133
|
-
.all({
|
|
156
|
+
.all({ sid: sessionId, seq: afterSeq }) as { content: string }[]
|
|
134
157
|
return rows.map((r) => JSON.parse(r.content) as Msg)
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
setTitle(sessionId: string, title: string): void {
|
|
138
161
|
this.#db
|
|
139
162
|
.prepare("UPDATE sessions SET title = $title WHERE id = $id")
|
|
140
|
-
.run({
|
|
163
|
+
.run({ title: title, id: sessionId })
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
messageCount(sessionId: string): number {
|
|
144
167
|
const row = this.#db
|
|
145
168
|
.prepare("SELECT COUNT(*) as count FROM messages WHERE session_id = $sid")
|
|
146
|
-
.get({
|
|
169
|
+
.get({ sid: sessionId }) as { count: number }
|
|
147
170
|
return row.count
|
|
148
171
|
}
|
|
149
172
|
|
|
@@ -159,12 +182,12 @@ export class SessionStore {
|
|
|
159
182
|
"INSERT INTO compactions (session_id, summary, files_read, files_wrote, seq_before, ts) VALUES ($sid, $summary, $read, $wrote, $seq, $ts)",
|
|
160
183
|
)
|
|
161
184
|
.run({
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
sid: sessionId,
|
|
186
|
+
summary: summary,
|
|
187
|
+
read: JSON.stringify(filesRead),
|
|
188
|
+
wrote: JSON.stringify(filesWrote),
|
|
189
|
+
seq: seqBefore,
|
|
190
|
+
ts: Date.now(),
|
|
168
191
|
})
|
|
169
192
|
}
|
|
170
193
|
|
|
@@ -174,24 +197,24 @@ export class SessionStore {
|
|
|
174
197
|
.prepare(
|
|
175
198
|
"SELECT summary, seq_before FROM compactions WHERE session_id = $sid ORDER BY ts DESC LIMIT 1",
|
|
176
199
|
)
|
|
177
|
-
.get({
|
|
200
|
+
.get({ sid: sessionId }) as { summary: string; seqBefore: number } | null) ?? null
|
|
178
201
|
)
|
|
179
202
|
}
|
|
180
203
|
|
|
181
204
|
truncateBeforeSeq(sessionId: string, seq: number): void {
|
|
182
205
|
this.#db
|
|
183
206
|
.prepare("DELETE FROM messages WHERE session_id = $sid AND seq < $seq")
|
|
184
|
-
.run({
|
|
207
|
+
.run({ sid: sessionId, seq: seq })
|
|
185
208
|
}
|
|
186
209
|
|
|
187
210
|
close(): void {
|
|
188
|
-
this.#db.close(
|
|
211
|
+
this.#db.close()
|
|
189
212
|
}
|
|
190
213
|
|
|
191
214
|
#nextSeq(sessionId: string): number {
|
|
192
215
|
const row = this.#db
|
|
193
216
|
.prepare("SELECT MAX(seq) as maxSeq FROM messages WHERE session_id = $sid")
|
|
194
|
-
.get({
|
|
217
|
+
.get({ sid: sessionId }) as { maxSeq: number | null }
|
|
195
218
|
return (row.maxSeq ?? 0) + 1
|
|
196
219
|
}
|
|
197
220
|
}
|
package/src/tools/fs.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Filesystem tools for reading, writing, and editing files.
|
|
3
3
|
* Includes safety checks to prevent path traversal.
|
|
4
4
|
*/
|
|
5
|
-
import { mkdir } from "node:fs/promises"
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
6
6
|
import { dirname, extname, resolve } from "node:path"
|
|
7
7
|
import type { Tool, ToolResult } from "../types.ts"
|
|
8
8
|
import { getRelativeIfInside, textPart } from "../util.ts"
|
|
@@ -37,22 +37,16 @@ export function readTool(cwd: string): Tool {
|
|
|
37
37
|
async execute(args): Promise<ToolResult> {
|
|
38
38
|
try {
|
|
39
39
|
const filePath = safePath(cwd, args.path as string)
|
|
40
|
-
const relPath = getRelativeIfInside(cwd, filePath)
|
|
41
|
-
const file = Bun.file(filePath)
|
|
42
|
-
if (!(await file.exists())) {
|
|
43
|
-
return { content: [textPart(`File not found: ${relPath}`)], isError: true }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
40
|
// Return images as base64 so the LLM can process them visually
|
|
47
41
|
const ext = extname(filePath).toLowerCase()
|
|
48
42
|
if (IMAGES.has(ext)) {
|
|
49
|
-
const buf = await
|
|
50
|
-
const b64 =
|
|
43
|
+
const buf = await readFile(filePath)
|
|
44
|
+
const b64 = buf.toString("base64")
|
|
51
45
|
const mime = ext === ".jpg" ? "image/jpeg" : `image/${ext.slice(1)}`
|
|
52
46
|
return { content: [{ type: "image", data: b64, mime }], isError: false }
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
const content = await
|
|
49
|
+
const content = await readFile(filePath, "utf-8")
|
|
56
50
|
const lines = content.split("\n")
|
|
57
51
|
const offset = Math.max(0, (Number(args.offset ?? 1) || 1) - 1)
|
|
58
52
|
const limit = Number(args.limit ?? 2000) || 2000
|
|
@@ -92,7 +86,7 @@ export function writeTool(cwd: string): Tool {
|
|
|
92
86
|
const filePath = safePath(cwd, args.path as string)
|
|
93
87
|
const content = args.content as string
|
|
94
88
|
await mkdir(dirname(filePath), { recursive: true })
|
|
95
|
-
await
|
|
89
|
+
await writeFile(filePath, content)
|
|
96
90
|
const relPath = getRelativeIfInside(cwd, filePath)
|
|
97
91
|
return {
|
|
98
92
|
content: [textPart(`Wrote ${content.length} bytes → ${relPath}`)],
|
|
@@ -139,12 +133,12 @@ export function editTool(cwd: string): Tool {
|
|
|
139
133
|
async execute(args): Promise<ToolResult> {
|
|
140
134
|
try {
|
|
141
135
|
const filePath = safePath(cwd, args.path as string)
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
let content: string
|
|
137
|
+
try {
|
|
138
|
+
content = await readFile(filePath, "utf-8")
|
|
139
|
+
} catch {
|
|
144
140
|
return { content: [textPart(`File not found: ${args.path}`)], isError: true }
|
|
145
141
|
}
|
|
146
|
-
|
|
147
|
-
let content = await file.text()
|
|
148
142
|
const edits = args.edits as Array<{ oldText: string; newText: string }>
|
|
149
143
|
|
|
150
144
|
// Validate all edits before applying any — avoids partial writes on bad input
|
|
@@ -174,7 +168,7 @@ export function editTool(cwd: string): Tool {
|
|
|
174
168
|
content = content.replace(edit.oldText, edit.newText)
|
|
175
169
|
}
|
|
176
170
|
|
|
177
|
-
await
|
|
171
|
+
await writeFile(filePath, content)
|
|
178
172
|
const relPath = getRelativeIfInside(cwd, filePath)
|
|
179
173
|
return {
|
|
180
174
|
content: [
|
package/src/tools/git.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Git tools for executing safe repository operations programmatically.
|
|
3
3
|
*/
|
|
4
|
+
import { spawn } from "node:child_process"
|
|
4
5
|
import type { Tool, ToolResult } from "../types.ts"
|
|
5
6
|
import { textPart } from "../util.ts"
|
|
6
7
|
|
|
@@ -41,21 +42,37 @@ export function gitTool(cwd: string): Tool {
|
|
|
41
42
|
|
|
42
43
|
try {
|
|
43
44
|
const cmd = ["git", action, ...extraArgs]
|
|
44
|
-
const proc =
|
|
45
|
+
const proc = spawn(cmd[0]!, cmd.slice(1), {
|
|
45
46
|
cwd,
|
|
46
|
-
|
|
47
|
-
stderr: "pipe",
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
48
|
env: { ...process.env, PAGER: "cat" },
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
let stdout = ""
|
|
52
|
+
let stderr = ""
|
|
53
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
54
|
+
stdout += chunk.toString()
|
|
55
|
+
})
|
|
56
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
57
|
+
stderr += chunk.toString()
|
|
58
|
+
})
|
|
53
59
|
|
|
54
|
-
const
|
|
55
|
-
|
|
60
|
+
const onAbort = () => {
|
|
61
|
+
proc.kill("SIGKILL")
|
|
62
|
+
proc.stdout.destroy()
|
|
63
|
+
proc.stderr.destroy()
|
|
64
|
+
}
|
|
65
|
+
signal?.addEventListener("abort", onAbort, { once: true })
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
let exitCode: number
|
|
68
|
+
try {
|
|
69
|
+
exitCode = await new Promise<number>((resolve, reject) => {
|
|
70
|
+
proc.on("error", reject)
|
|
71
|
+
proc.on("close", (code) => resolve(code ?? -1))
|
|
72
|
+
})
|
|
73
|
+
} finally {
|
|
74
|
+
signal?.removeEventListener("abort", onAbort)
|
|
75
|
+
}
|
|
59
76
|
|
|
60
77
|
// Prevent context window blowout by truncating very large outputs
|
|
61
78
|
const MAX = 50_000
|
package/src/tools/search.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Search tools for finding files and content.
|
|
3
3
|
* Uses 'rg' (ripgrep) if available, falling back to a pure JS implementation.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
import { spawn } from "node:child_process"
|
|
7
|
+
import { readdir, readFile } from "node:fs/promises"
|
|
6
8
|
import { relative, resolve } from "node:path"
|
|
7
9
|
import { glob } from "glob"
|
|
8
10
|
import type { Tool, ToolResult } from "../types.ts"
|
|
@@ -91,18 +93,35 @@ export function grepTool(cwd: string): Tool {
|
|
|
91
93
|
if (globFilter) cmd.push(`--glob=${globFilter}`)
|
|
92
94
|
cmd.push("--", pattern, relSearchPath)
|
|
93
95
|
|
|
94
|
-
const proc =
|
|
96
|
+
const proc = spawn(cmd[0]!, cmd.slice(1), {
|
|
95
97
|
cwd,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const onAbort = () => {
|
|
102
|
+
proc.kill()
|
|
103
|
+
proc.stdout.destroy()
|
|
104
|
+
proc.stderr.destroy()
|
|
105
|
+
}
|
|
106
|
+
signal?.addEventListener("abort", onAbort, { once: true })
|
|
107
|
+
|
|
108
|
+
let stdout = ""
|
|
109
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
110
|
+
stdout += chunk.toString()
|
|
98
111
|
})
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
|
|
113
|
+
let exitCode: number
|
|
114
|
+
try {
|
|
115
|
+
exitCode = await new Promise<number>((resolve, reject) => {
|
|
116
|
+
proc.on("error", reject)
|
|
117
|
+
proc.on("close", (code) => resolve(code ?? -1))
|
|
118
|
+
})
|
|
119
|
+
} finally {
|
|
120
|
+
signal?.removeEventListener("abort", onAbort)
|
|
121
|
+
}
|
|
102
122
|
|
|
103
123
|
if (exitCode === 0) {
|
|
104
|
-
const
|
|
105
|
-
const lines = out.split("\n").slice(0, 200).join("\n")
|
|
124
|
+
const lines = stdout.split("\n").slice(0, 200).join("\n")
|
|
106
125
|
return { content: [textPart(lines || "No matches")], isError: false }
|
|
107
126
|
}
|
|
108
127
|
} catch {
|
|
@@ -110,14 +129,17 @@ export function grepTool(cwd: string): Tool {
|
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
// Pure JS fallback when rg is not available
|
|
113
|
-
const files = await glob(globFilter || "**/*", {
|
|
132
|
+
const files = await glob(globFilter || "**/*", {
|
|
133
|
+
cwd: dir,
|
|
134
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
135
|
+
})
|
|
114
136
|
const prefix = relSearchPath === "." ? "" : `${relSearchPath}/`
|
|
115
137
|
const re = new RegExp(pattern, "i")
|
|
116
138
|
const matches: string[] = []
|
|
117
139
|
for (const file of files.slice(0, 500)) {
|
|
118
140
|
if (signal?.aborted) break
|
|
119
141
|
try {
|
|
120
|
-
const content = await
|
|
142
|
+
const content = await readFile(resolve(dir, file), "utf-8")
|
|
121
143
|
const lines = content.split("\n")
|
|
122
144
|
for (let i = 0; i < lines.length && matches.length < 200; i++) {
|
|
123
145
|
const line = lines[i]
|
package/src/tools/shell.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Tool for executing shell commands within the project root.
|
|
3
3
|
* Supports timeouts and output truncation to protect the context window.
|
|
4
4
|
*/
|
|
5
|
+
import { spawn } from "node:child_process"
|
|
5
6
|
import type { Tool, ToolResult } from "../types.ts"
|
|
6
7
|
|
|
7
8
|
import { textPart } from "../util.ts"
|
|
@@ -26,43 +27,43 @@ export function bashTool(cwd: string): Tool {
|
|
|
26
27
|
const timeoutMs = (Number(args.timeout) || 120) * 1000
|
|
27
28
|
|
|
28
29
|
try {
|
|
29
|
-
const proc =
|
|
30
|
-
cwd,
|
|
31
|
-
stdout: "pipe",
|
|
32
|
-
stderr: "pipe",
|
|
33
|
-
})
|
|
30
|
+
const proc = spawn("sh", ["-c", command], { cwd, stdio: ["ignore", "pipe", "pipe"] })
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
let stdout = ""
|
|
33
|
+
let stderr = ""
|
|
34
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
35
|
+
stdout += chunk.toString()
|
|
36
|
+
})
|
|
37
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
38
|
+
stderr += chunk.toString()
|
|
39
|
+
})
|
|
38
40
|
|
|
39
|
-
// Track whether we killed it vs normal exit, so the output reflects the cause
|
|
40
41
|
let killed = false
|
|
41
42
|
const timer = setTimeout(() => {
|
|
42
43
|
killed = true
|
|
43
|
-
proc.kill(
|
|
44
|
+
proc.kill("SIGKILL")
|
|
45
|
+
proc.stdout.destroy()
|
|
46
|
+
proc.stderr.destroy()
|
|
44
47
|
}, timeoutMs)
|
|
45
48
|
|
|
46
49
|
const onAbort = () => {
|
|
47
50
|
killed = true
|
|
48
|
-
proc.kill(
|
|
51
|
+
proc.kill("SIGKILL")
|
|
52
|
+
proc.stdout.destroy()
|
|
53
|
+
proc.stderr.destroy()
|
|
49
54
|
}
|
|
50
55
|
signal?.addEventListener("abort", onAbort, { once: true })
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const stderr = await Promise.race([
|
|
63
|
-
stderrPromise,
|
|
64
|
-
new Promise<string>((r) => setTimeout(() => r(""), 500)),
|
|
65
|
-
])
|
|
57
|
+
let exitCode: number
|
|
58
|
+
try {
|
|
59
|
+
exitCode = await new Promise<number>((resolve, reject) => {
|
|
60
|
+
proc.on("error", reject)
|
|
61
|
+
proc.on("close", (code) => resolve(code ?? -1))
|
|
62
|
+
})
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer)
|
|
65
|
+
signal?.removeEventListener("abort", onAbort)
|
|
66
|
+
}
|
|
66
67
|
|
|
67
68
|
// Prevent context-window blowout from noisy commands
|
|
68
69
|
const MAX = 50_000
|