@gavin-hu/my-pi-agent 0.14.0 → 0.15.0
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 +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
package/extensions/job/format.ts
CHANGED
|
@@ -7,51 +7,19 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Raw log output is untrusted: it can contain ANSI escapes, carriage-return
|
|
9
9
|
* progress rewrites, and control characters. Every boundary that surfaces log
|
|
10
|
-
* text goes through `
|
|
11
|
-
* background process cannot restyle or corrupt the
|
|
12
|
-
* is never modified.
|
|
10
|
+
* text goes through `sanitizeUntrustedLine`/`sanitizeUntrustedText` from
|
|
11
|
+
* `lib/format.ts`, so a background process cannot restyle or corrupt the
|
|
12
|
+
* terminal. The raw log file is never modified.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { formatDuration } from "../../lib/format.ts";
|
|
17
|
-
import { clip,
|
|
16
|
+
import { boundedTail, formatDuration, sanitizeUntrustedLine } from "../../lib/format.ts";
|
|
17
|
+
import { clip, clipLabel, LABEL_WIDTH } from "../../lib/tui/columns.ts";
|
|
18
18
|
import { GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
19
|
+
import { byRunningFirst } from "../../lib/tui/rows.ts";
|
|
19
20
|
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
20
21
|
import type { JobRecord, JobStatus } from "./types.ts";
|
|
21
22
|
|
|
22
|
-
/** Longest sanitized log line kept for display; longer lines are clipped. */
|
|
23
|
-
const MAX_LOG_LINE = 200;
|
|
24
|
-
|
|
25
|
-
/** ESC-introduced sequences: OSC, CSI, and single-character escapes. */
|
|
26
|
-
const OSC = /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
|
|
27
|
-
const CSI = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
28
|
-
const OTHER_ESC = /\u001b[@-Z\\-_]/g;
|
|
29
|
-
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
30
|
-
|
|
31
|
-
/** Remove ANSI/OSC escape sequences and control characters from one line. */
|
|
32
|
-
function stripEscapes(raw: string): string {
|
|
33
|
-
return raw.replace(OSC, "").replace(CSI, "").replace(OTHER_ESC, "");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Make one line of untrusted log output safe to render.
|
|
38
|
-
*
|
|
39
|
-
* Resolves carriage returns the way a terminal would (keep the text after the
|
|
40
|
-
* last `\r`, so progress rewrites collapse to their final state), strips
|
|
41
|
-
* escapes and control characters, collapses whitespace, and clips the result.
|
|
42
|
-
*/
|
|
43
|
-
export function sanitizeLogLine(raw: string): string {
|
|
44
|
-
const withoutAnsi = stripEscapes(raw);
|
|
45
|
-
const afterCr = withoutAnsi.slice(withoutAnsi.lastIndexOf("\r") + 1);
|
|
46
|
-
const cleaned = afterCr.replace(CONTROL, " ").replace(/\s+/g, " ").trim();
|
|
47
|
-
return cleaned.length > MAX_LOG_LINE ? `${cleaned.slice(0, MAX_LOG_LINE - 1)}…` : cleaned;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Sanitize a block of log text line by line. */
|
|
51
|
-
export function sanitizeLogText(raw: string): string[] {
|
|
52
|
-
return raw.split("\n").map(sanitizeLogLine);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
23
|
/** The last `count` entries of `lines`. */
|
|
56
24
|
export function tailLines(lines: string[], count: number): string[] {
|
|
57
25
|
if (count <= 0) return [];
|
|
@@ -95,22 +63,12 @@ export function statusGlyphChar(status: JobStatus): string {
|
|
|
95
63
|
}
|
|
96
64
|
}
|
|
97
65
|
|
|
98
|
-
/** Width-1 themed glyph for a status. */
|
|
99
|
-
export function statusGlyph(status: JobStatus, theme: Theme): string {
|
|
100
|
-
return paint(theme, statusRole(status), statusGlyphChar(status));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
66
|
/** Short status word for text output. */
|
|
104
67
|
function statusWord(job: JobRecord): string {
|
|
105
68
|
if (job.status === "failed" && job.exitCode !== null) return `failed (exit ${job.exitCode})`;
|
|
106
69
|
return job.status;
|
|
107
70
|
}
|
|
108
71
|
|
|
109
|
-
/** A label clipped to one safe display line. */
|
|
110
|
-
export function clipLabel(text: string): string {
|
|
111
|
-
return labelLine(text, sanitizeLogLine);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
72
|
/** A job's label (falling back to its command) clipped to one safe display line. */
|
|
115
73
|
export function shortLabel(job: JobRecord): string {
|
|
116
74
|
return clipLabel(job.label || job.command);
|
|
@@ -138,12 +96,12 @@ export function formatJobList(jobs: JobRecord[], now = Date.now(), ownSessionId?
|
|
|
138
96
|
export function formatJobStatus(job: JobRecord, now = Date.now(), ownSessionId?: string): string {
|
|
139
97
|
const lines = [
|
|
140
98
|
`${job.id}: ${statusWord(job)}`,
|
|
141
|
-
`command: ${
|
|
142
|
-
`cwd: ${
|
|
99
|
+
`command: ${sanitizeUntrustedLine(job.command)}`,
|
|
100
|
+
`cwd: ${sanitizeUntrustedLine(job.cwd)}`,
|
|
143
101
|
`elapsed: ${formatDuration(elapsedMs(job, now))}`,
|
|
144
102
|
];
|
|
145
103
|
if (job.pid) lines.push(`pid: ${job.pid}`);
|
|
146
|
-
if (job.logPath) lines.push(`log: ${
|
|
104
|
+
if (job.logPath) lines.push(`log: ${sanitizeUntrustedLine(job.logPath)}`);
|
|
147
105
|
if (ownSessionId !== undefined && job.sessionId !== ownSessionId) lines.push("session: another session");
|
|
148
106
|
if (job.lastLine) lines.push(`last: ${job.lastLine}`);
|
|
149
107
|
return lines.join("\n");
|
|
@@ -154,12 +112,7 @@ export function formatLogs(job: JobRecord, lines: string[], maxChars: number): {
|
|
|
154
112
|
if (lines.length === 0) {
|
|
155
113
|
return { text: `${job.id}: no output yet${job.logPath ? ` (log: ${job.logPath})` : ""}.`, truncated: false };
|
|
156
114
|
}
|
|
157
|
-
|
|
158
|
-
let truncated = false;
|
|
159
|
-
if (body.length > maxChars) {
|
|
160
|
-
body = body.slice(body.length - maxChars);
|
|
161
|
-
truncated = true;
|
|
162
|
-
}
|
|
115
|
+
const { text: body, truncated } = boundedTail(lines, maxChars);
|
|
163
116
|
const suffix = truncated ? `\n… truncated; full log: ${job.logPath}` : "";
|
|
164
117
|
return { text: `${job.id} (${statusWord(job)}) output:\n${body}${suffix}`, truncated };
|
|
165
118
|
}
|
|
@@ -172,10 +125,10 @@ export function formatCompletion(job: JobRecord, now = Date.now()): string {
|
|
|
172
125
|
|
|
173
126
|
/** Transcript call text for the `job` tool. */
|
|
174
127
|
export function formatCallText(action: string, args: Record<string, unknown>, argsComplete = true): string {
|
|
175
|
-
const target = typeof args.id === "string" && args.id ?
|
|
128
|
+
const target = typeof args.id === "string" && args.id ? sanitizeUntrustedLine(args.id) : undefined;
|
|
176
129
|
switch (action) {
|
|
177
130
|
case "start": {
|
|
178
|
-
const command = typeof args.command === "string" ?
|
|
131
|
+
const command = typeof args.command === "string" ? sanitizeUntrustedLine(args.command.trim()) : "";
|
|
179
132
|
if (!command) return argsComplete ? "start" : "start …";
|
|
180
133
|
const preview = clip(command, LABEL_WIDTH);
|
|
181
134
|
return `start → ${preview}`;
|
|
@@ -184,7 +137,7 @@ export function formatCallText(action: string, args: Record<string, unknown>, ar
|
|
|
184
137
|
return `kill ${target ?? ""}`.trim();
|
|
185
138
|
case "send": {
|
|
186
139
|
if (!target) return "send";
|
|
187
|
-
const input = typeof args.input === "string" ?
|
|
140
|
+
const input = typeof args.input === "string" ? sanitizeUntrustedLine(args.input) : "";
|
|
188
141
|
const preview = input ? clip(input, LABEL_WIDTH) : "";
|
|
189
142
|
const parts = [preview, args.eof === true ? "EOF" : ""].filter(Boolean).join(" + ");
|
|
190
143
|
return parts ? `send ${target} → ${parts}` : `send ${target}`;
|
|
@@ -201,10 +154,7 @@ export function formatCallText(action: string, args: Record<string, unknown>, ar
|
|
|
201
154
|
}
|
|
202
155
|
|
|
203
156
|
/** Order jobs for display: running first, then by most recent start. */
|
|
204
|
-
export
|
|
205
|
-
const rank = (job: JobRecord): number => (job.status === "running" ? 0 : 1);
|
|
206
|
-
return rank(a) - rank(b) || b.startedAt - a.startedAt;
|
|
207
|
-
}
|
|
157
|
+
export const compareJobs = byRunningFirst<JobRecord>((job) => job.status === "running");
|
|
208
158
|
|
|
209
159
|
export interface JobCounts {
|
|
210
160
|
total: number;
|
|
@@ -252,7 +202,7 @@ export function formatJobOutcomeLine(job: JobRecord, theme: Theme, now = Date.no
|
|
|
252
202
|
const outcome =
|
|
253
203
|
job.status === "failed" ? paint(theme, "bad", jobOutcome(job)) : paint(theme, "faint", jobOutcome(job));
|
|
254
204
|
return (
|
|
255
|
-
|
|
205
|
+
paint(theme, statusRole(job.status), statusGlyphChar(job.status)) +
|
|
256
206
|
" " +
|
|
257
207
|
paint(theme, "accent", job.id) +
|
|
258
208
|
" " +
|
|
@@ -267,11 +217,11 @@ export function formatJobOutcomeLine(job: JobRecord, theme: Theme, now = Date.no
|
|
|
267
217
|
/** Unstyled, sanitized detail lines for the `/jobs` focus pane. */
|
|
268
218
|
export function formatJobDetail(job: JobRecord, now = Date.now(), ownSessionId?: string): string[] {
|
|
269
219
|
const lines = [
|
|
270
|
-
|
|
271
|
-
|
|
220
|
+
sanitizeUntrustedLine(`command: ${job.command}`),
|
|
221
|
+
sanitizeUntrustedLine(`cwd: ${job.cwd} · ${jobOutcome(job)} · ${formatDuration(elapsedMs(job, now))}`),
|
|
272
222
|
];
|
|
273
223
|
if (ownSessionId !== undefined && job.sessionId !== ownSessionId) lines.push("owner: another session");
|
|
274
|
-
const last =
|
|
224
|
+
const last = sanitizeUntrustedLine(job.lastLine);
|
|
275
225
|
if (last) lines.push(`last: ${last}`);
|
|
276
226
|
return lines;
|
|
277
227
|
}
|
package/extensions/job/index.ts
CHANGED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
15
15
|
import { JOB_FINISHED_CHANNEL, type JobFinishedEvent } from "../../lib/events.ts";
|
|
16
|
-
import { noticeMessage, type NoticeMessage
|
|
17
|
-
import {
|
|
16
|
+
import { noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
|
|
17
|
+
import { createPendingReporter } from "../../lib/report.ts";
|
|
18
18
|
import { registerCommands } from "./commands.ts";
|
|
19
19
|
import { formatCompletion, jobOutcome, shortLabel } from "./format.ts";
|
|
20
20
|
import { createJobsRuntime, type JobsRuntime } from "./runtime.ts";
|
|
21
21
|
import { registerTools } from "./tools.ts";
|
|
22
|
-
import type {
|
|
22
|
+
import type { JobRecord } from "./types.ts";
|
|
23
23
|
|
|
24
24
|
/** Marker embedded in the injected completion context. */
|
|
25
25
|
const JOB_CONTEXT_MARKER = "[BACKGROUND JOBS]";
|
|
@@ -65,33 +65,27 @@ export interface JobsDeps {
|
|
|
65
65
|
export default function jobs(pi: ExtensionAPI, deps: JobsDeps = {}): void {
|
|
66
66
|
if (!isExtensionEnabled("job")) return;
|
|
67
67
|
const runtime = deps.runtime ?? createJobsRuntime();
|
|
68
|
-
|
|
68
|
+
|
|
69
|
+
// Opt-in wake: a `wake` job finished while the agent is idle. The reporter
|
|
70
|
+
// reports *all* pending completions, not just the waking one, so a job that
|
|
71
|
+
// finished earlier is not silently marked seen and dropped.
|
|
72
|
+
const reporter = createPendingReporter<JobRecord>({
|
|
73
|
+
wake: (notice) => pi.sendMessage(notice, { triggerTurn: true }),
|
|
74
|
+
pending: () => runtime.pendingCompletions(),
|
|
75
|
+
markReported: (records) => runtime.markReported(records.map((job) => job.id)),
|
|
76
|
+
buildNotice: jobNotice,
|
|
77
|
+
source: "job",
|
|
78
|
+
});
|
|
69
79
|
|
|
70
80
|
registerTools(pi, runtime);
|
|
71
81
|
registerCommands(pi, runtime);
|
|
72
82
|
|
|
73
|
-
// Opt-in wake: a `wake` job finished while the agent is idle. Report *all*
|
|
74
|
-
// pending completions, not just the waking one, so a non-wake job that
|
|
75
|
-
// finished earlier is not silently marked seen and dropped.
|
|
76
|
-
const handleFinish = (_job: Job): void => {
|
|
77
|
-
if (busy) return;
|
|
78
|
-
const pending = runtime.pendingCompletions();
|
|
79
|
-
if (pending.length === 0) return;
|
|
80
|
-
try {
|
|
81
|
-
pi.sendMessage(jobNotice(pending), { triggerTurn: true });
|
|
82
|
-
// Mark only once the send succeeded; a throw leaves them for the next turn.
|
|
83
|
-
runtime.markReported(pending.map((job) => job.id));
|
|
84
|
-
} catch {
|
|
85
|
-
// Waking is best-effort; the next turn still reports the completion.
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
83
|
pi.on("session_start", (_event, ctx) => {
|
|
90
|
-
|
|
84
|
+
reporter.sessionStart();
|
|
91
85
|
runtime.load(ctx);
|
|
92
|
-
// `shutdown` clears
|
|
93
|
-
// runtime, so re-arm
|
|
94
|
-
runtime.onFinish =
|
|
86
|
+
// `shutdown` clears these; a reload or session replacement reuses the
|
|
87
|
+
// runtime, so re-arm them on every session start.
|
|
88
|
+
runtime.onFinish = () => reporter.report();
|
|
95
89
|
// Notify-listening extensions follow completions on the shared bus; the
|
|
96
90
|
// runtime only knows the callback, never the channel.
|
|
97
91
|
runtime.onComplete = (job) => {
|
|
@@ -109,37 +103,28 @@ export default function jobs(pi: ExtensionAPI, deps: JobsDeps = {}): void {
|
|
|
109
103
|
});
|
|
110
104
|
|
|
111
105
|
pi.on("agent_start", () => {
|
|
112
|
-
|
|
106
|
+
reporter.agentStart();
|
|
113
107
|
});
|
|
114
108
|
pi.on("agent_settled", () => {
|
|
115
|
-
|
|
109
|
+
reporter.agentSettled();
|
|
116
110
|
});
|
|
117
111
|
|
|
118
112
|
// Report completions to the model at the start of the next turn.
|
|
119
113
|
pi.on("before_agent_start", (_event, ctx) => {
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
runtime.markReported(pending.map((job) => job.id));
|
|
114
|
+
const reported = reporter.beforeAgentStart();
|
|
115
|
+
if (!reported) return undefined;
|
|
123
116
|
runtime.setStatus(ctx);
|
|
124
|
-
return
|
|
117
|
+
return reported;
|
|
125
118
|
});
|
|
126
119
|
|
|
127
120
|
// Keep stale completion notices out of later turns. Only this extension's
|
|
128
121
|
// notices are pruned; another source's are left for its own handler.
|
|
129
|
-
pi.on("context", (event) =>
|
|
130
|
-
const pruned = pruneStaleNotices(event.messages, "job");
|
|
131
|
-
return pruned ? { messages: pruned } : undefined;
|
|
132
|
-
});
|
|
122
|
+
pi.on("context", (event) => reporter.filterContext(event.messages));
|
|
133
123
|
|
|
134
|
-
|
|
124
|
+
// `shutdown` detaches the UI controller, which clears both chips.
|
|
125
|
+
pi.on("session_shutdown", async () => {
|
|
135
126
|
runtime.onFinish = undefined;
|
|
136
127
|
runtime.onComplete = undefined;
|
|
137
128
|
await runtime.shutdown();
|
|
138
|
-
try {
|
|
139
|
-
ctx.ui.setStatus(STATUS_KEYS.jobs, undefined);
|
|
140
|
-
ctx.ui.setStatus(STATUS_KEYS.jobsFailure, undefined);
|
|
141
|
-
} catch {
|
|
142
|
-
// UI may already be gone.
|
|
143
|
-
}
|
|
144
129
|
});
|
|
145
130
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-job log sink: a write stream, the line splitter that feeds the job's
|
|
3
|
+
* `lastLine`, and rotation when the segment reaches its cap.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the job runtime so the process/table lifecycle and the log
|
|
6
|
+
* plumbing can be read separately. The writer owns no job state: every path and
|
|
7
|
+
* limit is a callback over the runtime's live config, and a buffered line is
|
|
8
|
+
* handed back through `onLine`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createWriteStream, existsSync, renameSync, unlinkSync, type WriteStream } from "node:fs";
|
|
12
|
+
import { LineSplitter } from "../../lib/lines.ts";
|
|
13
|
+
import type { Job, JobRecord } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
/** Bytes held while a rotation swaps the log stream before writes resume. */
|
|
16
|
+
const MAX_ROTATION_QUEUE_BYTES = 2 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
/** One job's log state: the live segment, the queue, and the counters. */
|
|
19
|
+
export interface LogHandle {
|
|
20
|
+
stream?: WriteStream;
|
|
21
|
+
lines: LineSplitter;
|
|
22
|
+
/** Chunks that arrived while the log stream was being swapped. */
|
|
23
|
+
queue: Buffer[];
|
|
24
|
+
/** Bytes currently in `queue`. */
|
|
25
|
+
queuedBytes: number;
|
|
26
|
+
/** True between ending the old stream and reopening the log. */
|
|
27
|
+
rotating: boolean;
|
|
28
|
+
/** Bytes dropped because a rotation could not keep up; reported by `logs`. */
|
|
29
|
+
droppedBytes: number;
|
|
30
|
+
/** Bytes written to the live segment, counted exactly as chunks arrive. */
|
|
31
|
+
logBytes: number;
|
|
32
|
+
/** True once the handle is disposed; a rotation in flight never reopens. */
|
|
33
|
+
disposed: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LogWriterOptions {
|
|
37
|
+
/** The clock the repaint window is measured on. */
|
|
38
|
+
now(): number;
|
|
39
|
+
/** The live segment for a job, or undefined when it cannot be used. */
|
|
40
|
+
logPath(job: JobRecord): string | undefined;
|
|
41
|
+
/** The rotated segment for a job, or undefined when it cannot be used. */
|
|
42
|
+
rotatedPath(job: JobRecord): string | undefined;
|
|
43
|
+
/** Bytes a segment may reach before it rotates; 0 disables rotation. */
|
|
44
|
+
maxBytes(): number;
|
|
45
|
+
/** The repaint window for output-driven painting. */
|
|
46
|
+
repaintMs(): number;
|
|
47
|
+
/** Record one output line; the runtime sanitizes it onto the job. */
|
|
48
|
+
onLine(job: Job, line: string): void;
|
|
49
|
+
/** Repaint, at most once per repaint window while output arrives. */
|
|
50
|
+
onPaint(): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LogWriter {
|
|
54
|
+
/** A handle for one job's log; `logPath` is undefined for a detached job. */
|
|
55
|
+
create(logPath: string | undefined): LogHandle;
|
|
56
|
+
/** Append child output, splitting lines and rotating at the cap. */
|
|
57
|
+
write(handle: LogHandle, job: Job, chunk: Buffer): void;
|
|
58
|
+
/** The buffered final lines, including a final line without a newline. */
|
|
59
|
+
flush(handle: LogHandle): string[];
|
|
60
|
+
/** Close the stream and mark the handle dead; idempotent. */
|
|
61
|
+
dispose(handle: LogHandle, options: { endStream: boolean }): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createLogWriter(options: LogWriterOptions): LogWriter {
|
|
65
|
+
/** Last output-driven repaint, so a busy child cannot repaint per chunk. */
|
|
66
|
+
let lastPaint = 0;
|
|
67
|
+
|
|
68
|
+
const writeLog = (handle: LogHandle, chunk: Buffer): void => {
|
|
69
|
+
try {
|
|
70
|
+
handle.stream?.write(chunk);
|
|
71
|
+
} catch {
|
|
72
|
+
// A closed stream must not break output handling.
|
|
73
|
+
}
|
|
74
|
+
// Count what we handed the stream: `WriteStream.bytesWritten` only updates on
|
|
75
|
+
// its completion callback, which can lag a tick behind the file on disk.
|
|
76
|
+
if (handle.stream) handle.logBytes += chunk.length;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Buffer a chunk while the log stream is being swapped, dropping the oldest past the cap. */
|
|
80
|
+
const enqueue = (handle: LogHandle, chunk: Buffer, front = false): void => {
|
|
81
|
+
if (front) handle.queue.unshift(chunk);
|
|
82
|
+
else handle.queue.push(chunk);
|
|
83
|
+
handle.queuedBytes += chunk.length;
|
|
84
|
+
while (handle.queuedBytes > MAX_ROTATION_QUEUE_BYTES && handle.queue.length > 1) {
|
|
85
|
+
const dropped = handle.queue.shift();
|
|
86
|
+
if (!dropped) break;
|
|
87
|
+
handle.queuedBytes -= dropped.length;
|
|
88
|
+
handle.droppedBytes += dropped.length;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Move the live segment aside, queueing `chunk` ahead of the backlog, and reopen.
|
|
94
|
+
*
|
|
95
|
+
* Returns false when no rotation can start (no stream, or a record whose log
|
|
96
|
+
* paths do not validate), so the caller writes the chunk instead of stranding it
|
|
97
|
+
* in a queue nothing will drain.
|
|
98
|
+
*
|
|
99
|
+
* The old stream is ended and its `close` awaited before the rename, because
|
|
100
|
+
* Windows refuses to rename an open file; chunks that arrive in that window
|
|
101
|
+
* are queued rather than written, so nothing lands in the renamed inode.
|
|
102
|
+
*/
|
|
103
|
+
const rotateLog = (handle: LogHandle, job: Job, chunk: Buffer): boolean => {
|
|
104
|
+
const stream = handle.stream;
|
|
105
|
+
const logPath = options.logPath(job);
|
|
106
|
+
const rotated = options.rotatedPath(job);
|
|
107
|
+
if (!stream || !logPath || !rotated || handle.rotating) return false;
|
|
108
|
+
// This chunk precedes whatever queued while the previous segment was closing.
|
|
109
|
+
enqueue(handle, chunk, true);
|
|
110
|
+
handle.rotating = true;
|
|
111
|
+
stream.once("close", () => {
|
|
112
|
+
// A killed or cleared job disposes its handle; never reopen for it.
|
|
113
|
+
if (handle.disposed) return;
|
|
114
|
+
try {
|
|
115
|
+
if (existsSync(rotated)) unlinkSync(rotated);
|
|
116
|
+
renameSync(logPath, rotated);
|
|
117
|
+
} catch {
|
|
118
|
+
// A failed rename leaves the oversized segment in place; keep appending.
|
|
119
|
+
}
|
|
120
|
+
const reopened = createWriteStream(logPath, { flags: "a" });
|
|
121
|
+
reopened.on("error", () => {});
|
|
122
|
+
handle.stream = reopened;
|
|
123
|
+
handle.rotating = false;
|
|
124
|
+
handle.logBytes = 0;
|
|
125
|
+
drain(handle, job);
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
stream.end();
|
|
129
|
+
} catch {
|
|
130
|
+
// Already ended.
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Write one output chunk, rotating the segment first when it reached the cap. */
|
|
136
|
+
const writeChunk = (handle: LogHandle, job: Job, chunk: Buffer): void => {
|
|
137
|
+
if (handle.rotating) {
|
|
138
|
+
// Later than everything already queued.
|
|
139
|
+
enqueue(handle, chunk);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const cap = options.maxBytes();
|
|
143
|
+
if (handle.stream && cap > 0 && handle.logBytes >= cap && rotateLog(handle, job, chunk)) return;
|
|
144
|
+
writeLog(handle, chunk);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Replay the queue through `writeChunk` after a rotation, so a backlog that is
|
|
149
|
+
* itself larger than the cap rotates again instead of filling one segment.
|
|
150
|
+
*/
|
|
151
|
+
const drain = (handle: LogHandle, job: Job): void => {
|
|
152
|
+
while (!handle.rotating && handle.queue.length > 0) {
|
|
153
|
+
const chunk = handle.queue.shift();
|
|
154
|
+
if (!chunk) break;
|
|
155
|
+
handle.queuedBytes -= chunk.length;
|
|
156
|
+
writeChunk(handle, job, chunk);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
create(logPath) {
|
|
162
|
+
const stream = logPath === undefined ? undefined : createWriteStream(logPath, { flags: "a" });
|
|
163
|
+
stream?.on("error", () => {});
|
|
164
|
+
return {
|
|
165
|
+
stream,
|
|
166
|
+
lines: new LineSplitter(),
|
|
167
|
+
queue: [],
|
|
168
|
+
queuedBytes: 0,
|
|
169
|
+
rotating: false,
|
|
170
|
+
droppedBytes: 0,
|
|
171
|
+
logBytes: 0,
|
|
172
|
+
disposed: false,
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
write(handle, job, chunk) {
|
|
177
|
+
if (handle.disposed) return;
|
|
178
|
+
writeChunk(handle, job, chunk);
|
|
179
|
+
for (const line of handle.lines.push(chunk)) options.onLine(job, line);
|
|
180
|
+
const time = options.now();
|
|
181
|
+
if (time - lastPaint >= options.repaintMs()) {
|
|
182
|
+
lastPaint = time;
|
|
183
|
+
options.onPaint();
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
flush(handle) {
|
|
188
|
+
return handle.lines.flush();
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
dispose(handle, { endStream }) {
|
|
192
|
+
if (handle.disposed) return;
|
|
193
|
+
handle.disposed = true;
|
|
194
|
+
// A rotation in flight checks `disposed` before reopening, so dropping the
|
|
195
|
+
// queue here cannot leak a stream for a dead job.
|
|
196
|
+
handle.queue = [];
|
|
197
|
+
handle.queuedBytes = 0;
|
|
198
|
+
if (endStream) {
|
|
199
|
+
try {
|
|
200
|
+
handle.stream?.end();
|
|
201
|
+
} catch {
|
|
202
|
+
// Already closed.
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
handle.stream = undefined;
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
package/extensions/job/logs.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs";
|
|
10
|
-
import {
|
|
10
|
+
import { sanitizeUntrustedText } from "../../lib/format.ts";
|
|
11
|
+
import { tailLines } from "./format.ts";
|
|
11
12
|
|
|
12
13
|
/** Bytes of log read for a `logs` call. */
|
|
13
14
|
const LOG_READ_BYTES = 64 * 1024;
|
|
@@ -50,7 +51,7 @@ export function readLogTail(path: string | undefined, lines: number): LogTail {
|
|
|
50
51
|
} catch {
|
|
51
52
|
// A missing/unreadable log yields an empty tail.
|
|
52
53
|
}
|
|
53
|
-
const all = dropTrailingBlank(
|
|
54
|
+
const all = dropTrailingBlank(sanitizeUntrustedText(text));
|
|
54
55
|
const more = start > 0 || all.length > lines;
|
|
55
56
|
return { lines: tailLines(all, lines), truncated, more };
|
|
56
57
|
}
|
|
@@ -1,64 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Process plumbing
|
|
2
|
+
* Process plumbing that only `job` needs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* On POSIX the child is started detached, making it a process-group leader;
|
|
10
|
-
* signals go to the whole group (`-pid`) so a shell and its descendants die
|
|
11
|
-
* together. On Windows the tree is killed with `taskkill /T`.
|
|
4
|
+
* The shared spawn, tree-kill, and liveness seams live in `lib/spawn.ts`, so
|
|
5
|
+
* what stays here is the job-specific remainder: the pid liveness and start-time
|
|
6
|
+
* seams the runtime injects, and the `ps` token that guards against pid reuse
|
|
7
|
+
* when a reattached job is settled.
|
|
12
8
|
*/
|
|
13
9
|
|
|
14
|
-
import { execFileSync
|
|
15
|
-
import type { Readable } from "node:stream";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The writable end of a child's stdin pipe.
|
|
19
|
-
*
|
|
20
|
-
* Only the operations `send` needs, so tests can hand the runtime a recording
|
|
21
|
-
* double instead of a real socket.
|
|
22
|
-
*/
|
|
23
|
-
export interface StdinSink {
|
|
24
|
-
write(chunk: string): boolean;
|
|
25
|
-
end(): void;
|
|
26
|
-
destroy?(): void;
|
|
27
|
-
on(event: "error" | "close", listener: (error?: Error) => void): unknown;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** The subset of a spawned child the runtime uses. */
|
|
31
|
-
export interface SpawnedProcess {
|
|
32
|
-
pid?: number;
|
|
33
|
-
stdout: Readable | null;
|
|
34
|
-
stderr: Readable | null;
|
|
35
|
-
/** Present only when spawned with `stdin: true`. */
|
|
36
|
-
stdin?: StdinSink | null;
|
|
37
|
-
on(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
38
|
-
on(event: "error", listener: (error: Error) => void): unknown;
|
|
39
|
-
kill(signal?: NodeJS.Signals): boolean;
|
|
40
|
-
/** Detach from the parent's event loop, so a background job cannot hold Pi open. */
|
|
41
|
-
unref?(): void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type SpawnFn = (
|
|
45
|
-
command: string,
|
|
46
|
-
options: {
|
|
47
|
-
cwd: string;
|
|
48
|
-
env?: NodeJS.ProcessEnv;
|
|
49
|
-
/** The command writes its own output to the log; keep no parent pipe. */
|
|
50
|
-
detachOutput?: boolean;
|
|
51
|
-
/** Give the child a stdin pipe instead of `/dev/null` (no immediate EOF). */
|
|
52
|
-
stdin?: boolean;
|
|
53
|
-
},
|
|
54
|
-
) => SpawnedProcess;
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
55
11
|
|
|
56
12
|
/** Whether a pid is still alive. */
|
|
57
13
|
export type LivenessFn = (pid: number) => boolean;
|
|
58
14
|
|
|
59
|
-
/** Send `signal` to a pid's process tree, escalating to SIGKILL when `force`. */
|
|
60
|
-
export type KillTreeFn = (pid: number, signal: NodeJS.Signals, force?: boolean) => void;
|
|
61
|
-
|
|
62
15
|
/**
|
|
63
16
|
* Best-effort OS start-time token for a pid, used to detect pid reuse.
|
|
64
17
|
* Returns undefined when the platform cannot provide one (for example Windows,
|
|
@@ -67,32 +20,6 @@ export type KillTreeFn = (pid: number, signal: NodeJS.Signals, force?: boolean)
|
|
|
67
20
|
*/
|
|
68
21
|
export type StartTokenFn = (pid: number) => string | undefined;
|
|
69
22
|
|
|
70
|
-
/** Default spawn: a shell command in its own process group. A detached job
|
|
71
|
-
* redirects its own output to the log, so it needs no pipe to this process.
|
|
72
|
-
* Stdin stays closed unless the job asked for it: a closed stdin gives the
|
|
73
|
-
* child EOF, so `read` exits instead of blocking forever. */
|
|
74
|
-
export const defaultSpawn: SpawnFn = (command, options) =>
|
|
75
|
-
nodeSpawn(command, {
|
|
76
|
-
cwd: options.cwd,
|
|
77
|
-
shell: true,
|
|
78
|
-
detached: process.platform !== "win32",
|
|
79
|
-
stdio: [
|
|
80
|
-
options.stdin ? "pipe" : "ignore",
|
|
81
|
-
...(options.detachOutput ? (["ignore", "ignore"] as const) : (["pipe", "pipe"] as const)),
|
|
82
|
-
],
|
|
83
|
-
env: options.env,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
/** Default liveness check. EPERM means the pid exists but is owned by someone else. */
|
|
87
|
-
export const defaultLiveness: LivenessFn = (pid) => {
|
|
88
|
-
try {
|
|
89
|
-
process.kill(pid, 0);
|
|
90
|
-
return true;
|
|
91
|
-
} catch (error) {
|
|
92
|
-
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
23
|
/** Default start-time token via `ps`. Undefined where `ps` is unavailable. */
|
|
97
24
|
export const defaultStartToken: StartTokenFn = (pid) => {
|
|
98
25
|
try {
|
|
@@ -106,23 +33,3 @@ export const defaultStartToken: StartTokenFn = (pid) => {
|
|
|
106
33
|
return undefined;
|
|
107
34
|
}
|
|
108
35
|
};
|
|
109
|
-
|
|
110
|
-
/** Default tree kill: process group on POSIX, `taskkill /T` on Windows. */
|
|
111
|
-
export const defaultKillTree: KillTreeFn = (pid, signal, force = false) => {
|
|
112
|
-
if (process.platform === "win32") {
|
|
113
|
-
spawnSync("taskkill", ["/pid", String(pid), "/T", ...(force ? ["/F"] : [])], { stdio: "ignore" });
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
process.kill(-pid, signal);
|
|
118
|
-
} catch (error) {
|
|
119
|
-
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
120
|
-
// Not a group leader (for example a reattached pid); signal it directly.
|
|
121
|
-
// Callers verify the start token first, so this cannot hit a reused pid.
|
|
122
|
-
try {
|
|
123
|
-
process.kill(pid, signal);
|
|
124
|
-
} catch {
|
|
125
|
-
// Already gone.
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
};
|