@oh-my-pi/pi-utils 14.5.11 → 14.5.13
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 +2 -2
- package/src/frontmatter.ts +17 -7
- package/src/index.ts +0 -1
- package/src/logger.ts +217 -65
- package/src/procmgr.ts +8 -138
- package/src/prompt.ts +10 -1
- package/src/ptree.ts +6 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "14.5.
|
|
4
|
+
"version": "14.5.13",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/bun": "^1.3",
|
|
41
|
-
"@oh-my-pi/pi-natives": "14.5.
|
|
41
|
+
"@oh-my-pi/pi-natives": "14.5.13"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
44
|
"bun": ">=1.3.7"
|
package/src/frontmatter.ts
CHANGED
|
@@ -8,23 +8,33 @@ function stripHtmlComments(content: string): string {
|
|
|
8
8
|
|
|
9
9
|
/** Convert kebab-case to camelCase (e.g. "thinking-level" -> "thinkingLevel") */
|
|
10
10
|
function kebabToCamel(key: string): string {
|
|
11
|
+
if (!key.includes("-")) return key;
|
|
11
12
|
return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/** Recursively normalize object keys from kebab-case to camelCase */
|
|
15
16
|
function normalizeKeys<T>(obj: T): T {
|
|
16
|
-
if (obj === null || typeof obj !== "object")
|
|
17
|
-
return obj;
|
|
18
|
-
}
|
|
17
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
19
18
|
if (Array.isArray(obj)) {
|
|
20
|
-
|
|
19
|
+
let changed = false;
|
|
20
|
+
const out: unknown[] = new Array(obj.length);
|
|
21
|
+
for (let i = 0; i < obj.length; i++) {
|
|
22
|
+
const v = obj[i];
|
|
23
|
+
const nv = normalizeKeys(v);
|
|
24
|
+
out[i] = nv;
|
|
25
|
+
if (nv !== v) changed = true;
|
|
26
|
+
}
|
|
27
|
+
return (changed ? (out as unknown) : obj) as T;
|
|
21
28
|
}
|
|
29
|
+
let changed = false;
|
|
22
30
|
const result: Record<string, unknown> = {};
|
|
23
31
|
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
24
|
-
const
|
|
25
|
-
|
|
32
|
+
const nk = key.includes("-") ? kebabToCamel(key) : key;
|
|
33
|
+
const nv = normalizeKeys(value);
|
|
34
|
+
result[nk] = nv;
|
|
35
|
+
if (nk !== key || nv !== value) changed = true;
|
|
26
36
|
}
|
|
27
|
-
return result as T;
|
|
37
|
+
return (changed ? result : obj) as T;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export class FrontmatterError extends Error {
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,6 @@ export * from "./mime";
|
|
|
15
15
|
export * from "./peek-file";
|
|
16
16
|
export * as postmortem from "./postmortem";
|
|
17
17
|
export * as procmgr from "./procmgr";
|
|
18
|
-
export { setNativeKillTree } from "./procmgr";
|
|
19
18
|
export * as prompt from "./prompt";
|
|
20
19
|
export * as ptree from "./ptree";
|
|
21
20
|
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
|
package/src/logger.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Logs to ~/.omp/logs/ with size-based rotation, supporting concurrent omp instances.
|
|
5
5
|
* Each log entry includes process.pid for traceability.
|
|
6
6
|
*/
|
|
7
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
8
|
import * as fs from "node:fs";
|
|
8
9
|
import winston from "winston";
|
|
9
10
|
import DailyRotateFile from "winston-daily-rotate-file";
|
|
@@ -96,109 +97,260 @@ export function debug(message: string, context?: Record<string, unknown>): void
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
const LOGGED_TIMING_THRESHOLD_MS = 5;
|
|
100
|
+
const LOGGED_TIMING_THRESHOLD_MS = 0.5;
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
interface Span {
|
|
103
|
+
op: string;
|
|
104
|
+
start: number;
|
|
105
|
+
end?: number;
|
|
106
|
+
parent?: Span;
|
|
107
|
+
children: Span[];
|
|
108
|
+
/** Marker / point event without a duration. */
|
|
109
|
+
point?: boolean;
|
|
110
|
+
}
|
|
106
111
|
|
|
107
|
-
|
|
112
|
+
const spanStorage = new AsyncLocalStorage<Span>();
|
|
113
|
+
let gRootSpan: Span | undefined;
|
|
108
114
|
let gRecordTimings = false;
|
|
109
115
|
|
|
110
116
|
/**
|
|
111
|
-
* Print collected timings
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* for those awaits instead.
|
|
117
|
+
* Print collected timings as an indented tree.
|
|
118
|
+
* Each span shows wall duration; parents with children also show "(self)" for unattributed time.
|
|
119
|
+
* Sibling spans are sorted by start time. Spans whose intervals overlap with siblings ran in parallel.
|
|
115
120
|
*/
|
|
116
121
|
export function printTimings(): void {
|
|
117
|
-
if (!gRecordTimings ||
|
|
122
|
+
if (!gRecordTimings || !gRootSpan) {
|
|
118
123
|
console.error("\n--- Startup Timings ---\n(no markers)\n");
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (dur > LOGGED_TIMING_THRESHOLD_MS) {
|
|
132
|
-
console.error(` ${op}: ${dur}ms`);
|
|
133
|
-
}
|
|
127
|
+
gRootSpan.end = performance.now();
|
|
128
|
+
const lines: string[] = [];
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push("--- Startup timings (hierarchical) ---");
|
|
131
|
+
const work: Span[] = [];
|
|
132
|
+
const loads: Span[] = [];
|
|
133
|
+
for (const child of gRootSpan.children) {
|
|
134
|
+
if (isModuleLoadSpan(child)) loads.push(child);
|
|
135
|
+
else work.push(child);
|
|
134
136
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (gAsyncSpans.length > 0) {
|
|
138
|
-
console.error("\n--- Async (await-accurate; parallel spans may overlap) ---");
|
|
139
|
-
for (const [op, dur] of gAsyncSpans) {
|
|
140
|
-
if (dur > LOGGED_TIMING_THRESHOLD_MS) {
|
|
141
|
-
console.error(` ${op}: ${dur}ms`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
137
|
+
for (const child of work.sort((a, b) => a.start - b.start)) {
|
|
138
|
+
printSpan(child, 0, lines);
|
|
144
139
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
if (loads.length > 0) {
|
|
141
|
+
printModuleLoadSummary(loads, 0, lines);
|
|
142
|
+
}
|
|
143
|
+
const totalMs = (gRootSpan.end - gRootSpan.start).toFixed(1);
|
|
144
|
+
lines.push(`Total: ${totalMs}ms`);
|
|
145
|
+
lines.push("--------------------------------------");
|
|
146
|
+
lines.push("");
|
|
147
|
+
console.error(lines.join("\n"));
|
|
148
|
+
gRootSpan.end = undefined;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
* Begin recording startup timings
|
|
152
|
+
* Begin recording startup timings under a new root span.
|
|
153
|
+
* Idempotent: a second call while already recording is a no-op so that side-effect
|
|
154
|
+
* starters (see module-timer.ts) and explicit starters (main.ts) can coexist.
|
|
153
155
|
*/
|
|
154
156
|
export function startTiming(): void {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
if (gRecordTimings) return;
|
|
158
|
+
gRootSpan = {
|
|
159
|
+
op: "(root)",
|
|
160
|
+
start: performance.now(),
|
|
161
|
+
parent: undefined,
|
|
162
|
+
children: [],
|
|
163
|
+
};
|
|
157
164
|
gRecordTimings = true;
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Record an externally-measured span as a leaf child of the active span (or root
|
|
169
|
+
* when no span is active). Used by the module-load timing plugin to splice load
|
|
170
|
+
* events into the tree retroactively.
|
|
171
|
+
*/
|
|
172
|
+
export function recordModuleLoadSpan(path: string, start: number, durationMs: number): void {
|
|
173
|
+
if (!gRecordTimings || !gRootSpan) return;
|
|
174
|
+
const parent = spanStorage.getStore() ?? gRootSpan;
|
|
175
|
+
const span: Span = {
|
|
176
|
+
op: `load:${shortenLoadPath(path)}`,
|
|
177
|
+
start,
|
|
178
|
+
end: start + durationMs,
|
|
179
|
+
parent,
|
|
180
|
+
children: [],
|
|
181
|
+
};
|
|
182
|
+
parent.children.push(span);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function shortenLoadPath(p: string): string {
|
|
186
|
+
const cwd = process.cwd();
|
|
187
|
+
if (p.startsWith(`${cwd}/`)) return p.slice(cwd.length + 1);
|
|
188
|
+
const home = process.env.HOME;
|
|
189
|
+
if (home && p.startsWith(`${home}/`)) return `~/${p.slice(home.length + 1)}`;
|
|
190
|
+
return p;
|
|
191
|
+
}
|
|
192
|
+
|
|
160
193
|
/**
|
|
161
194
|
* End timing window and clear buffers.
|
|
162
195
|
*/
|
|
163
196
|
export function endTiming(): void {
|
|
164
|
-
|
|
165
|
-
gAsyncSpans = [];
|
|
197
|
+
gRootSpan = undefined;
|
|
166
198
|
gRecordTimings = false;
|
|
167
199
|
}
|
|
168
200
|
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
201
|
+
function durationOf(span: Span): number {
|
|
202
|
+
if (span.point || span.end === undefined) return 0;
|
|
203
|
+
return span.end - span.start;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Self time = total - union of child intervals (handles parallel children correctly). */
|
|
207
|
+
function selfTimeOf(span: Span): number {
|
|
208
|
+
const dur = durationOf(span);
|
|
209
|
+
if (span.children.length === 0 || span.point) return dur;
|
|
210
|
+
const intervals = span.children
|
|
211
|
+
.filter(c => !c.point && c.end !== undefined)
|
|
212
|
+
.map(c => [c.start, c.end as number] as const)
|
|
213
|
+
.sort((a, b) => a[0] - b[0]);
|
|
214
|
+
if (intervals.length === 0) return dur;
|
|
215
|
+
let union = 0;
|
|
216
|
+
let curStart = intervals[0][0];
|
|
217
|
+
let curEnd = intervals[0][1];
|
|
218
|
+
for (let i = 1; i < intervals.length; i++) {
|
|
219
|
+
const [s, e] = intervals[i];
|
|
220
|
+
if (s > curEnd) {
|
|
221
|
+
union += curEnd - curStart;
|
|
222
|
+
curStart = s;
|
|
223
|
+
curEnd = e;
|
|
224
|
+
} else if (e > curEnd) {
|
|
225
|
+
curEnd = e;
|
|
226
|
+
}
|
|
173
227
|
}
|
|
228
|
+
union += curEnd - curStart;
|
|
229
|
+
return Math.max(0, dur - union);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function fmtMs(ms: number): string {
|
|
233
|
+
if (ms < 1) return `${ms.toFixed(2)}ms`;
|
|
234
|
+
if (ms < 100) return `${ms.toFixed(1)}ms`;
|
|
235
|
+
return `${ms.toFixed(0)}ms`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const MODULE_LOAD_PREFIX = "load:";
|
|
239
|
+
const MODULE_LOAD_VERBOSE_TOP = 10;
|
|
240
|
+
|
|
241
|
+
function isModuleLoadSpan(span: Span): boolean {
|
|
242
|
+
return span.op.startsWith(MODULE_LOAD_PREFIX);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function printSpan(span: Span, depth: number, lines: string[]): void {
|
|
246
|
+
const indent = " ".repeat(depth);
|
|
247
|
+
if (span.point) {
|
|
248
|
+
lines.push(`${indent}• ${span.op}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const dur = durationOf(span);
|
|
252
|
+
if (dur < LOGGED_TIMING_THRESHOLD_MS && span.children.length === 0) return;
|
|
253
|
+
const parallel = isParallel(span);
|
|
254
|
+
const tag = parallel ? " [parallel]" : "";
|
|
255
|
+
const self = selfTimeOf(span);
|
|
256
|
+
const selfStr = span.children.length > 0 && self > LOGGED_TIMING_THRESHOLD_MS ? ` (self ${fmtMs(self)})` : "";
|
|
257
|
+
lines.push(`${indent}${span.op}: ${fmtMs(dur)}${selfStr}${tag}`);
|
|
258
|
+
|
|
259
|
+
// Split children into work spans and module-load spans for summarization.
|
|
260
|
+
const work: Span[] = [];
|
|
261
|
+
const loads: Span[] = [];
|
|
262
|
+
for (const child of span.children) {
|
|
263
|
+
if (isModuleLoadSpan(child)) loads.push(child);
|
|
264
|
+
else work.push(child);
|
|
265
|
+
}
|
|
266
|
+
for (const child of work.sort((a, b) => a.start - b.start)) {
|
|
267
|
+
printSpan(child, depth + 1, lines);
|
|
268
|
+
}
|
|
269
|
+
if (loads.length > 0) {
|
|
270
|
+
printModuleLoadSummary(loads, depth + 1, lines);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Collapse the (typically hundreds of) module-load spans into one summary line. */
|
|
275
|
+
function printModuleLoadSummary(loads: Span[], depth: number, lines: string[]): void {
|
|
276
|
+
const childIndent = " ".repeat(depth);
|
|
277
|
+
const grandIndent = " ".repeat(depth + 1);
|
|
278
|
+
let unionStart = Number.POSITIVE_INFINITY;
|
|
279
|
+
let unionEnd = 0;
|
|
280
|
+
let totalSelf = 0;
|
|
281
|
+
for (const span of loads) {
|
|
282
|
+
if (span.end === undefined) continue;
|
|
283
|
+
if (span.start < unionStart) unionStart = span.start;
|
|
284
|
+
if (span.end > unionEnd) unionEnd = span.end;
|
|
285
|
+
totalSelf += span.end - span.start;
|
|
286
|
+
}
|
|
287
|
+
const wall = unionEnd > unionStart ? unionEnd - unionStart : 0;
|
|
288
|
+
lines.push(`${childIndent}(modules): ${loads.length} loaded, wall ${fmtMs(wall)}, sum ${fmtMs(totalSelf)}`);
|
|
289
|
+
const showAll = process.env.PI_TIMING === "full";
|
|
290
|
+
const sorted = [...loads].sort((a, b) => durationOf(b) - durationOf(a));
|
|
291
|
+
const visible = showAll ? sorted : sorted.slice(0, MODULE_LOAD_VERBOSE_TOP);
|
|
292
|
+
for (const span of visible) {
|
|
293
|
+
const dur = durationOf(span);
|
|
294
|
+
if (dur < LOGGED_TIMING_THRESHOLD_MS) break;
|
|
295
|
+
const tag = isParallel(span) ? " [parallel]" : "";
|
|
296
|
+
lines.push(`${grandIndent}${span.op}: ${fmtMs(dur)}${tag}`);
|
|
297
|
+
}
|
|
298
|
+
if (!showAll && sorted.length > MODULE_LOAD_VERBOSE_TOP) {
|
|
299
|
+
lines.push(`${grandIndent}… ${sorted.length - MODULE_LOAD_VERBOSE_TOP} more (PI_TIMING=full to show all)`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** A span is parallel if it overlaps a sibling that started before it. */
|
|
304
|
+
function isParallel(span: Span): boolean {
|
|
305
|
+
const parent = span.parent;
|
|
306
|
+
if (!parent || span.end === undefined) return false;
|
|
307
|
+
for (const sibling of parent.children) {
|
|
308
|
+
if (sibling === span || sibling.end === undefined || sibling.point) continue;
|
|
309
|
+
// Overlap test: A overlaps B iff A.start < B.end && B.start < A.end
|
|
310
|
+
if (sibling.start < span.end && span.start < sibling.end) return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
174
313
|
}
|
|
175
314
|
|
|
176
315
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
316
|
+
* Time a span. Three forms:
|
|
317
|
+
* time(op) — point event (zero-duration breadcrumb)
|
|
318
|
+
* time(op, fn, ...args) — wrap fn in a span; returns fn's return value (sync or Promise)
|
|
319
|
+
*
|
|
320
|
+
* Spans nest hierarchically via AsyncLocalStorage: a child started inside another span's fn
|
|
321
|
+
* (even across awaits) becomes that span's child. Parallel children are recorded as siblings
|
|
322
|
+
* with overlapping intervals.
|
|
179
323
|
*/
|
|
180
324
|
export function time(op: string): void;
|
|
181
325
|
export function time<T, A extends unknown[]>(op: string, fn: (...args: A) => T, ...args: A): T;
|
|
182
326
|
export function time<T, A extends unknown[]>(op: string, fn?: (...args: A) => T, ...args: A): T | undefined {
|
|
327
|
+
if (!gRecordTimings || !gRootSpan) {
|
|
328
|
+
if (fn === undefined) return undefined as T;
|
|
329
|
+
return fn(...args);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const parent = spanStorage.getStore() ?? gRootSpan;
|
|
333
|
+
const span: Span = { op, start: performance.now(), parent, children: [] };
|
|
334
|
+
parent.children.push(span);
|
|
335
|
+
|
|
183
336
|
if (fn === undefined) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
337
|
+
span.end = span.start;
|
|
338
|
+
span.point = true;
|
|
187
339
|
return undefined as T;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return result;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
recordAsyncSpan(op, start);
|
|
199
|
-
throw error;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const finish = (): void => {
|
|
343
|
+
span.end = performance.now();
|
|
344
|
+
};
|
|
345
|
+
try {
|
|
346
|
+
const result = spanStorage.run(span, () => fn(...args));
|
|
347
|
+
if (result instanceof Promise) {
|
|
348
|
+
return result.finally(finish) as T;
|
|
200
349
|
}
|
|
201
|
-
|
|
202
|
-
return
|
|
350
|
+
finish();
|
|
351
|
+
return result;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
finish();
|
|
354
|
+
throw error;
|
|
203
355
|
}
|
|
204
356
|
}
|
package/src/procmgr.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { Process, ProcessStatus } from "@oh-my-pi/pi-natives";
|
|
4
4
|
import type { Subprocess } from "bun";
|
|
5
5
|
import { $env } from "./env";
|
|
6
6
|
import { $which } from "./which";
|
|
@@ -14,9 +14,6 @@ export interface ShellConfig {
|
|
|
14
14
|
|
|
15
15
|
let cachedShellConfig: ShellConfig | null = null;
|
|
16
16
|
|
|
17
|
-
const IS_WINDOWS = process.platform === "win32";
|
|
18
|
-
const TERM_SIGNAL = IS_WINDOWS ? undefined : "SIGTERM";
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
18
|
* Check if a shell binary is executable.
|
|
22
19
|
*/
|
|
@@ -174,64 +171,20 @@ export function getShellConfig(customShellPath?: string): ShellConfig {
|
|
|
174
171
|
return cachedShellConfig;
|
|
175
172
|
}
|
|
176
173
|
|
|
177
|
-
/**
|
|
178
|
-
* Function signature for native process tree killing.
|
|
179
|
-
* Returns the number of processes killed.
|
|
180
|
-
*/
|
|
181
|
-
export type KillTreeFn = (pid: number, signal: number) => number;
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Global native kill tree function, injected by pi-natives when loaded.
|
|
185
|
-
* Falls back to platform-specific behavior if not set.
|
|
186
|
-
*/
|
|
187
|
-
export let nativeKillTree: KillTreeFn | undefined;
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Set the native kill tree function. Called by pi-natives on load.
|
|
191
|
-
*/
|
|
192
|
-
export function setNativeKillTree(fn: KillTreeFn): void {
|
|
193
|
-
nativeKillTree = fn;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Options for terminating a process and all its descendants.
|
|
198
|
-
*/
|
|
199
|
-
export interface TerminateOptions {
|
|
200
|
-
/** The process to terminate */
|
|
201
|
-
target: Subprocess | number;
|
|
202
|
-
/** Whether to terminate the process tree (all descendants) */
|
|
203
|
-
group?: boolean;
|
|
204
|
-
/** Timeout in milliseconds */
|
|
205
|
-
timeout?: number;
|
|
206
|
-
/** Abort signal */
|
|
207
|
-
signal?: AbortSignal;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
174
|
/**
|
|
211
175
|
* Check if a process is running.
|
|
212
176
|
*/
|
|
213
177
|
export function isPidRunning(pid: number | Subprocess): boolean {
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
} else {
|
|
218
|
-
if (pid.killed) return false;
|
|
219
|
-
if (pid.exitCode !== null) return false;
|
|
220
|
-
}
|
|
178
|
+
if (typeof pid !== "number") {
|
|
179
|
+
if (pid.killed) return false;
|
|
180
|
+
if (pid.exitCode !== null) return false;
|
|
221
181
|
return true;
|
|
222
|
-
} catch {
|
|
223
|
-
return false;
|
|
224
182
|
}
|
|
225
|
-
}
|
|
226
183
|
|
|
227
|
-
|
|
228
|
-
const nn = sigs.filter(Boolean) as AbortSignal[];
|
|
229
|
-
if (nn.length === 0) return undefined;
|
|
230
|
-
if (nn.length === 1) return nn[0];
|
|
231
|
-
return AbortSignal.any(nn);
|
|
184
|
+
return Process.fromPid(pid)?.status() === ProcessStatus.Running;
|
|
232
185
|
}
|
|
233
186
|
|
|
234
|
-
export function onProcessExit(proc: Subprocess | number, abortSignal?: AbortSignal): Promise<boolean> {
|
|
187
|
+
export async function onProcessExit(proc: Subprocess | number, abortSignal?: AbortSignal): Promise<boolean> {
|
|
235
188
|
if (typeof proc !== "number") {
|
|
236
189
|
return proc.exited.then(
|
|
237
190
|
() => true,
|
|
@@ -239,88 +192,5 @@ export function onProcessExit(proc: Subprocess | number, abortSignal?: AbortSign
|
|
|
239
192
|
);
|
|
240
193
|
}
|
|
241
194
|
|
|
242
|
-
|
|
243
|
-
return Promise.resolve(true);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const { promise, resolve, reject } = Promise.withResolvers<boolean>();
|
|
247
|
-
const localAbortController = new AbortController();
|
|
248
|
-
|
|
249
|
-
const timer = timers.promises.setInterval(300, null, {
|
|
250
|
-
signal: joinSignals(abortSignal, localAbortController.signal),
|
|
251
|
-
});
|
|
252
|
-
void (async () => {
|
|
253
|
-
try {
|
|
254
|
-
for await (const _ of timer) {
|
|
255
|
-
if (!isPidRunning(proc)) {
|
|
256
|
-
resolve(true);
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
} catch (error) {
|
|
261
|
-
return reject(error);
|
|
262
|
-
} finally {
|
|
263
|
-
localAbortController.abort();
|
|
264
|
-
}
|
|
265
|
-
resolve(false);
|
|
266
|
-
})();
|
|
267
|
-
|
|
268
|
-
return promise;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Terminate a process and all its descendants.
|
|
273
|
-
*/
|
|
274
|
-
export async function terminate(options: TerminateOptions): Promise<boolean> {
|
|
275
|
-
const { target, group = false, timeout = 5000, signal } = options;
|
|
276
|
-
|
|
277
|
-
const abortController = new AbortController();
|
|
278
|
-
try {
|
|
279
|
-
const abortSignal = joinSignals(signal, abortController.signal);
|
|
280
|
-
|
|
281
|
-
// Determine PID
|
|
282
|
-
let pid: number | undefined;
|
|
283
|
-
const exitPromise = onProcessExit(target, abortSignal);
|
|
284
|
-
if (typeof target === "number") {
|
|
285
|
-
pid = target;
|
|
286
|
-
} else {
|
|
287
|
-
pid = target.pid;
|
|
288
|
-
if (target.killed) return true;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Give it a moment to exit gracefully first.
|
|
292
|
-
try {
|
|
293
|
-
if (typeof target === "number") {
|
|
294
|
-
process.kill(target, TERM_SIGNAL);
|
|
295
|
-
} else {
|
|
296
|
-
target.kill(TERM_SIGNAL);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (exitPromise) {
|
|
300
|
-
const exited = await Promise.race([Bun.sleep(1000).then(() => false), exitPromise]);
|
|
301
|
-
if (exited) return true;
|
|
302
|
-
}
|
|
303
|
-
} catch {}
|
|
304
|
-
|
|
305
|
-
if (nativeKillTree) {
|
|
306
|
-
nativeKillTree(pid, 9);
|
|
307
|
-
} else {
|
|
308
|
-
if (group && !IS_WINDOWS) {
|
|
309
|
-
try {
|
|
310
|
-
process.kill(-pid, "SIGKILL");
|
|
311
|
-
} catch {}
|
|
312
|
-
}
|
|
313
|
-
try {
|
|
314
|
-
if (typeof target === "number") {
|
|
315
|
-
process.kill(target, "SIGKILL");
|
|
316
|
-
} else {
|
|
317
|
-
target.kill("SIGKILL");
|
|
318
|
-
}
|
|
319
|
-
} catch {}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return await Promise.race([Bun.sleep(timeout).then(() => false), exitPromise]);
|
|
323
|
-
} finally {
|
|
324
|
-
abortController.abort();
|
|
325
|
-
}
|
|
195
|
+
return (await Process.fromPid(proc)?.waitForExit({ signal: abortSignal })) ?? true;
|
|
326
196
|
}
|
package/src/prompt.ts
CHANGED
|
@@ -418,8 +418,17 @@ function disambiguateClosingBraces(template: string): string {
|
|
|
418
418
|
return template.replace(/\}\}(\}+)/g, "}}{{!---}}$1");
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
const compiledTemplateCache = new Map<string, (context: TemplateContext) => string>();
|
|
422
|
+
|
|
421
423
|
export function compile(template: string): (context: TemplateContext) => string {
|
|
422
|
-
|
|
424
|
+
const disambiguated = disambiguateClosingBraces(template);
|
|
425
|
+
const cached = compiledTemplateCache.get(disambiguated);
|
|
426
|
+
if (cached) return cached;
|
|
427
|
+
const compiled = handlebars.compile(disambiguated, { noEscape: true, strict: false }) as (
|
|
428
|
+
context: TemplateContext,
|
|
429
|
+
) => string;
|
|
430
|
+
compiledTemplateCache.set(disambiguated, compiled);
|
|
431
|
+
return compiled;
|
|
423
432
|
}
|
|
424
433
|
|
|
425
434
|
export function render(template: string, context: TemplateContext = {}): string {
|
package/src/ptree.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* - Cross-platform tree kill for process groups (Windows taskkill, Unix -pid).
|
|
7
7
|
* - Convenience helpers: captureText / execText, AbortSignal, timeouts.
|
|
8
8
|
*/
|
|
9
|
+
|
|
10
|
+
import { Process } from "@oh-my-pi/pi-natives";
|
|
9
11
|
import type { Spawn, Subprocess } from "bun";
|
|
10
|
-
import { terminate } from "./procmgr";
|
|
11
12
|
|
|
12
13
|
type InMask = "pipe" | "ignore" | Buffer | Uint8Array | null;
|
|
13
14
|
|
|
@@ -215,7 +216,10 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
215
216
|
|
|
216
217
|
kill(reason?: Exception) {
|
|
217
218
|
if (reason && !this.#exitReasonPending) this.#exitReasonPending = reason;
|
|
218
|
-
if (!this.proc.killed)
|
|
219
|
+
if (!this.proc.killed)
|
|
220
|
+
void Process.fromPid(this.proc.pid)
|
|
221
|
+
?.terminate()
|
|
222
|
+
?.catch(e => void e);
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
// ── Output helpers ───────────────────────────────────────────────────
|