@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "14.5.11",
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.11"
41
+ "@oh-my-pi/pi-natives": "14.5.13"
42
42
  },
43
43
  "engines": {
44
44
  "bun": ">=1.3.7"
@@ -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
- return obj.map(normalizeKeys) as T;
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 normalizedKey = kebabToCamel(key);
25
- result[normalizedKey] = normalizeKeys(value);
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
- /** Sequential wall-clock markers (next marker closes the previous segment). */
102
- let gTimings: [op: string, ts: number][] = [];
103
-
104
- /** Await-accurate durations (safe for parallel work; sums can overlap). */
105
- let gAsyncSpans: [op: string, durationMs: number][] = [];
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
- /** Whether to record timings. */
112
+ const spanStorage = new AsyncLocalStorage<Span>();
113
+ let gRootSpan: Span | undefined;
108
114
  let gRecordTimings = false;
109
115
 
110
116
  /**
111
- * Print collected timings to stderr.
112
- * Wall segments are gaps between consecutive {@link time} markers only; they are wrong when
113
- * concurrent code also calls {@link time} (e.g. parallel capability loads). Use {@link timeAsync}
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 || gTimings.length === 0) {
122
+ if (!gRecordTimings || !gRootSpan) {
118
123
  console.error("\n--- Startup Timings ---\n(no markers)\n");
119
124
  return;
120
125
  }
121
126
 
122
- const endTs = performance.now();
123
- gTimings.push(["(end)", endTs]);
124
-
125
- console.error("\n--- Startup timings (wall segments between time() markers) ---");
126
- const firstTs = gTimings[0][1];
127
- for (let i = 0; i < gTimings.length - 1; i++) {
128
- const [op, ts] = gTimings[i];
129
- const [, nextTs] = gTimings[i + 1];
130
- const dur = nextTs - ts;
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
- console.error(` span (first marker end): ${endTs - firstTs}ms`);
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
- console.error("------------------------\n");
147
-
148
- gTimings.pop();
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. Seeds the timeline so the first segment is meaningful.
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
- gTimings = [["(startup)", performance.now()]];
156
- gAsyncSpans = [];
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
- gTimings = [];
165
- gAsyncSpans = [];
197
+ gRootSpan = undefined;
166
198
  gRecordTimings = false;
167
199
  }
168
200
 
169
- function recordAsyncSpan(op: string, start: number): void {
170
- const dur = performance.now() - start;
171
- if (dur > LOGGED_TIMING_THRESHOLD_MS) {
172
- gAsyncSpans.push([op, dur]);
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
- * Wall-clock segment boundary: duration for this label runs until the next {@link time} call.
178
- * Do not use across `await` when other tasks may call {@link time}; use {@link timeAsync} for the awaited work.
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
- if (gRecordTimings) {
185
- gTimings.push([op, performance.now()]);
186
- }
337
+ span.end = span.start;
338
+ span.point = true;
187
339
  return undefined as T;
188
- } else if (gRecordTimings) {
189
- const start = performance.now();
190
- try {
191
- const result = fn(...args);
192
- if (result instanceof Promise) {
193
- return result.finally(recordAsyncSpan.bind(null, op, start)) as T;
194
- }
195
- recordAsyncSpan(op, start);
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
- } else {
202
- return fn(...args);
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 * as timers from "node:timers";
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
- try {
215
- if (typeof pid === "number") {
216
- process.kill(pid, 0);
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
- function joinSignals(...sigs: (AbortSignal | null | undefined)[]): AbortSignal | undefined {
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
- if (!isPidRunning(proc)) {
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
- return handlebars.compile(disambiguateClosingBraces(template), { noEscape: true, strict: false });
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) void terminate({ target: this.proc });
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 ───────────────────────────────────────────────────