pi-plans 0.1.2 → 0.3.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.
Files changed (76) hide show
  1. package/README.md +98 -19
  2. package/index.ts +147 -66
  3. package/package.json +7 -1
  4. package/references/pi-planning-workflow.md +21 -3
  5. package/references/state-and-config.md +35 -3
  6. package/scripts/validate.ts +4 -0
  7. package/src/autocomplete.ts +163 -0
  8. package/src/code-graph/commands.ts +437 -0
  9. package/src/code-graph/discovery.ts +118 -0
  10. package/src/code-graph/git.ts +108 -0
  11. package/src/code-graph/identity.ts +59 -0
  12. package/src/code-graph/indexer.ts +281 -0
  13. package/src/code-graph/materialize.ts +166 -0
  14. package/src/code-graph/mode.ts +28 -0
  15. package/src/code-graph/mutations.ts +160 -0
  16. package/src/code-graph/parser.ts +51 -0
  17. package/src/code-graph/parsers/javascript.ts +35 -0
  18. package/src/code-graph/parsers/python.ts +160 -0
  19. package/src/code-graph/parsers/tree-sitter.ts +316 -0
  20. package/src/code-graph/paths.ts +85 -0
  21. package/src/code-graph/prompts.ts +18 -0
  22. package/src/code-graph/resolver.ts +69 -0
  23. package/src/code-graph/runtime.ts +158 -0
  24. package/src/code-graph/schema.ts +135 -0
  25. package/src/code-graph/screening.ts +82 -0
  26. package/src/code-graph/store.ts +278 -0
  27. package/src/code-graph/summary.ts +435 -0
  28. package/src/code-graph/types.ts +163 -0
  29. package/src/compaction.ts +1256 -0
  30. package/src/config-command.ts +326 -0
  31. package/src/exec.ts +519 -625
  32. package/src/plan.ts +37 -0
  33. package/src/query-hook.ts +82 -0
  34. package/src/refine-prompts.ts +50 -0
  35. package/src/refine-ui-helpers.ts +142 -0
  36. package/src/refine-ui-state.ts +144 -0
  37. package/src/refine-ui.ts +430 -0
  38. package/src/state.ts +24 -29
  39. package/src/subagent.ts +299 -70
  40. package/tests/ask-choice.test.ts +263 -0
  41. package/tests/autocomplete.test.ts +147 -0
  42. package/tests/code-graph-apply.test.ts +185 -0
  43. package/tests/code-graph-commands.test.ts +211 -0
  44. package/tests/code-graph-db.test.ts +166 -0
  45. package/tests/code-graph-discovery.test.ts +38 -0
  46. package/tests/code-graph-git.test.ts +94 -0
  47. package/tests/code-graph-index.test.ts +175 -0
  48. package/tests/code-graph-loop.e2e.test.ts +159 -0
  49. package/tests/code-graph-mutations.test.ts +117 -0
  50. package/tests/code-graph-parser.test.ts +85 -0
  51. package/tests/code-graph-rollback.test.ts +100 -0
  52. package/tests/code-graph-summary-batching.test.ts +518 -0
  53. package/tests/code-graph-summary.test.ts +148 -0
  54. package/tests/compaction.test.ts +388 -0
  55. package/tests/config-command.test.ts +255 -0
  56. package/tests/exec.test.ts +751 -422
  57. package/tests/execute-plan.test.ts +65 -0
  58. package/tests/fixtures/code-graph/sample.js +36 -0
  59. package/tests/fixtures/code-graph/sample.py +20 -0
  60. package/tests/fixtures/code-graph/sample.ts +15 -0
  61. package/tests/graph-aware-file-tools.test.ts +411 -0
  62. package/tests/plan.test.ts +11 -1
  63. package/tests/plans.test.ts +6 -5
  64. package/tests/query-hook.test.ts +82 -0
  65. package/tests/refine-prompts.test.ts +67 -2
  66. package/tests/refine-ui.test.ts +392 -0
  67. package/tests/state.test.ts +12 -15
  68. package/tests/subagent.test.ts +120 -0
  69. package/tools/ask-choice.ts +180 -11
  70. package/tools/code-graph.ts +254 -0
  71. package/tools/execute-plan.ts +7 -39
  72. package/tools/graph-aware-file-tools.ts +392 -0
  73. package/tools/plans.ts +84 -18
  74. package/tools/refine.ts +180 -80
  75. package/src/execution-panel.ts +0 -633
  76. package/tests/execution-panel.test.ts +0 -234
package/src/subagent.ts CHANGED
@@ -9,6 +9,25 @@ import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
 
12
+ export type SubagentTranscriptEntryType = "assistant-text" | "thinking" | "tool-call" | "tool-result";
13
+
14
+ export type SubagentProgressEvent =
15
+ | { type: "process"; phase: "started" | "exited"; code?: number }
16
+ | { type: "turn"; phase: "start" | "end"; turnIndex?: number }
17
+ | {
18
+ type: "transcript";
19
+ phase: "start" | "update" | "end";
20
+ entryType: SubagentTranscriptEntryType;
21
+ key: string;
22
+ text: string;
23
+ update: "append" | "replace";
24
+ streaming: boolean;
25
+ toolCallId?: string;
26
+ toolName?: string;
27
+ isError?: boolean;
28
+ }
29
+ | { type: "stderr"; text: string };
30
+
12
31
  export interface SubagentOptions {
13
32
  systemPrompt: string;
14
33
  task: string;
@@ -19,6 +38,8 @@ export interface SubagentOptions {
19
38
  tools?: string[];
20
39
  signal?: AbortSignal;
21
40
  timeoutMs?: number;
41
+ /** Optional normalized progress sink. Exceptions from the sink are ignored. */
42
+ onProgress?: (event: SubagentProgressEvent) => void;
22
43
  }
23
44
 
24
45
  export interface SubagentResult {
@@ -28,6 +49,8 @@ export interface SubagentResult {
28
49
  errorMessage?: string;
29
50
  stderr: string;
30
51
  turns: number;
52
+ cancelled?: boolean;
53
+ timedOut?: boolean;
31
54
  }
32
55
 
33
56
  /** Strip YAML frontmatter from an agent definition file. */
@@ -54,43 +77,229 @@ export function getPiInvocation(args: string[]): { command: string; args: string
54
77
 
55
78
  interface MessageLike {
56
79
  role: string;
57
- content: Array<{ type: string; text?: string }>;
80
+ content?: unknown;
58
81
  model?: string;
59
82
  }
60
83
 
84
+ interface RawSubagentEvent {
85
+ type?: unknown;
86
+ turnIndex?: unknown;
87
+ message?: MessageLike;
88
+ toolCallId?: unknown;
89
+ toolName?: unknown;
90
+ args?: unknown;
91
+ partialResult?: unknown;
92
+ result?: unknown;
93
+ isError?: unknown;
94
+ assistantMessageEvent?: unknown;
95
+ }
96
+
97
+ function formatValue(value: unknown): string {
98
+ if (typeof value === "string") return value;
99
+ if (value === undefined) return "";
100
+ if (value && typeof value === "object") {
101
+ const content = (value as { content?: unknown }).content;
102
+ if (Array.isArray(content)) {
103
+ const text = content
104
+ .map((part) => {
105
+ if (part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string") {
106
+ return (part as { text: string }).text;
107
+ }
108
+ return formatValue(part);
109
+ })
110
+ .filter(Boolean)
111
+ .join("\n");
112
+ if (text) return text;
113
+ }
114
+ }
115
+ try {
116
+ return JSON.stringify(value, null, 2) ?? String(value);
117
+ } catch {
118
+ return String(value);
119
+ }
120
+ }
121
+
122
+ function messageText(message: MessageLike | undefined, kind: "text" | "thinking" = "text"): string {
123
+ if (!Array.isArray(message?.content)) return typeof message?.content === "string" && kind === "text" ? message.content : "";
124
+ return message.content
125
+ .filter((part) => part && typeof part === "object" && (part as { type?: unknown }).type === kind)
126
+ .map((part) => {
127
+ const value = part as { text?: unknown; thinking?: unknown };
128
+ return typeof value.text === "string" ? value.text : typeof value.thinking === "string" ? value.thinking : "";
129
+ })
130
+ .join("\n");
131
+ }
132
+
61
133
  function finalOutput(messages: MessageLike[]): string {
62
134
  for (let i = messages.length - 1; i >= 0; i--) {
63
135
  const message = messages[i];
64
136
  if (message.role === "assistant") {
65
- const text = message.content
66
- .filter((part) => part.type === "text")
67
- .map((part) => part.text ?? "")
68
- .join("\n")
69
- .trim();
137
+ const text = messageText(message, "text").trim();
70
138
  if (text) return text;
71
139
  }
72
140
  }
73
141
  return "";
74
142
  }
75
143
 
76
- const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
144
+ interface TranscriptEventOptions {
145
+ toolCallId?: string;
146
+ toolName?: string;
147
+ isError?: boolean;
148
+ }
77
149
 
78
- export async function runPiSubagent(options: SubagentOptions): Promise<SubagentResult> {
79
- const tools = options.tools ?? ["read", "grep", "find", "ls"];
80
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-subagent-"));
81
- const promptFile = path.join(tmpDir, "system-prompt.md");
82
- fs.writeFileSync(promptFile, options.systemPrompt, { encoding: "utf8", mode: 0o600 });
150
+ function transcriptEvent(
151
+ phase: "start" | "update" | "end",
152
+ entryType: SubagentTranscriptEntryType,
153
+ key: string,
154
+ text: string,
155
+ update: "append" | "replace",
156
+ streaming: boolean,
157
+ options: TranscriptEventOptions = {},
158
+ ): SubagentProgressEvent {
159
+ return { type: "transcript", phase, entryType, key, text, update, streaming, ...options };
160
+ }
161
+
162
+ function messageContentEvents(message: MessageLike | undefined, phase: "start" | "end"): SubagentProgressEvent[] {
163
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) return [];
164
+ return message.content.flatMap((part, index) => {
165
+ if (!part || typeof part !== "object") return [];
166
+ const value = part as { type?: unknown; text?: unknown; thinking?: unknown; id?: unknown; name?: unknown; arguments?: unknown };
167
+ if (value.type === "text" && typeof value.text === "string") {
168
+ return [transcriptEvent(phase, "assistant-text", `content:${index}`, value.text, "replace", phase !== "end")];
169
+ }
170
+ if (value.type === "thinking") {
171
+ const text = typeof value.thinking === "string" ? value.thinking : typeof value.text === "string" ? value.text : "";
172
+ return [transcriptEvent(phase, "thinking", `content:${index}`, text, "replace", phase !== "end")];
173
+ }
174
+ if (value.type === "toolCall") {
175
+ return [transcriptEvent(phase, "tool-call", `content:${index}`, formatValue(value.arguments), "replace", phase !== "end", {
176
+ toolCallId: typeof value.id === "string" ? value.id : undefined,
177
+ toolName: typeof value.name === "string" ? value.name : undefined,
178
+ })];
179
+ }
180
+ return [];
181
+ });
182
+ }
183
+
184
+ function assistantUpdateEvents(event: RawSubagentEvent): SubagentProgressEvent[] {
185
+ const update = event.assistantMessageEvent;
186
+ if (!update || typeof update !== "object") return messageContentEvents(event.message, "end");
187
+ const value = update as {
188
+ type?: unknown;
189
+ contentIndex?: unknown;
190
+ delta?: unknown;
191
+ content?: unknown;
192
+ id?: unknown;
193
+ toolName?: unknown;
194
+ toolCall?: { id?: unknown; name?: unknown; arguments?: unknown };
195
+ };
196
+ const type = typeof value.type === "string" ? value.type : "";
197
+ const index = typeof value.contentIndex === "number" ? value.contentIndex : 0;
198
+ const key = `content:${index}`;
199
+ switch (type) {
200
+ case "text_start":
201
+ return [transcriptEvent("start", "assistant-text", key, "", "replace", true)];
202
+ case "text_delta":
203
+ return [transcriptEvent("update", "assistant-text", key, typeof value.delta === "string" ? value.delta : "", "append", true)];
204
+ case "text_end":
205
+ return [transcriptEvent("end", "assistant-text", key, typeof value.content === "string" ? value.content : "", "replace", false)];
206
+ case "thinking_start":
207
+ return [transcriptEvent("start", "thinking", key, "", "replace", true)];
208
+ case "thinking_delta":
209
+ return [transcriptEvent("update", "thinking", key, typeof value.delta === "string" ? value.delta : "", "append", true)];
210
+ case "thinking_end":
211
+ return [transcriptEvent("end", "thinking", key, typeof value.content === "string" ? value.content : "", "replace", false)];
212
+ case "toolcall_start":
213
+ return [transcriptEvent("start", "tool-call", key, "", "replace", true, {
214
+ toolCallId: typeof value.id === "string" ? value.id : undefined,
215
+ toolName: typeof value.toolName === "string" ? value.toolName : undefined,
216
+ })];
217
+ case "toolcall_delta":
218
+ return [transcriptEvent("update", "tool-call", key, typeof value.delta === "string" ? value.delta : "", "append", true, {
219
+ toolCallId: typeof value.id === "string" ? value.id : undefined,
220
+ toolName: typeof value.toolName === "string" ? value.toolName : undefined,
221
+ })];
222
+ case "toolcall_end":
223
+ return [transcriptEvent("end", "tool-call", key, formatValue(value.toolCall?.arguments), "replace", false, {
224
+ toolCallId: typeof value.toolCall?.id === "string" ? value.toolCall.id : typeof value.id === "string" ? value.id : undefined,
225
+ toolName: typeof value.toolCall?.name === "string" ? value.toolCall.name : typeof value.toolName === "string" ? value.toolName : undefined,
226
+ })];
227
+ default:
228
+ return [];
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Normalize the JSONL events emitted by `pi --mode json` into the small
234
+ * protocol consumed by the refinement overlay. Unknown events are ignored so
235
+ * adding progress support cannot make result parsing version-fragile.
236
+ */
237
+ export function normalizeSubagentEvent(value: unknown): SubagentProgressEvent[] {
238
+ if (!value || typeof value !== "object") return [];
239
+ const event = value as RawSubagentEvent;
240
+ if (typeof event.type !== "string") return [];
241
+
242
+ if (event.type === "turn_start") {
243
+ return [{ type: "turn", phase: "start", ...(typeof event.turnIndex === "number" ? { turnIndex: event.turnIndex } : {}) }];
244
+ }
245
+ if (event.type === "turn_end") {
246
+ return [{ type: "turn", phase: "end", ...(typeof event.turnIndex === "number" ? { turnIndex: event.turnIndex } : {}) }];
247
+ }
248
+
249
+ if (event.type === "message_start") return messageContentEvents(event.message, "start");
250
+ if (event.type === "message_update") return assistantUpdateEvents(event);
251
+ if (event.type === "message_end") return messageContentEvents(event.message, "end");
252
+
253
+ if (event.type === "tool_execution_start") {
254
+ return [transcriptEvent("start", "tool-call", `tool:${String(event.toolCallId ?? "unknown")}`, formatValue(event.args), "replace", true, {
255
+ toolCallId: typeof event.toolCallId === "string" ? event.toolCallId : undefined,
256
+ toolName: typeof event.toolName === "string" ? event.toolName : undefined,
257
+ })];
258
+ }
259
+ if (event.type === "tool_execution_update") {
260
+ return [transcriptEvent("update", "tool-result", `tool:${String(event.toolCallId ?? "unknown")}`, formatValue(event.partialResult), "replace", true, {
261
+ toolCallId: typeof event.toolCallId === "string" ? event.toolCallId : undefined,
262
+ toolName: typeof event.toolName === "string" ? event.toolName : undefined,
263
+ })];
264
+ }
265
+ if (event.type === "tool_execution_end") {
266
+ return [transcriptEvent("end", "tool-result", `tool:${String(event.toolCallId ?? "unknown")}`, formatValue(event.result), "replace", false, {
267
+ toolCallId: typeof event.toolCallId === "string" ? event.toolCallId : undefined,
268
+ toolName: typeof event.toolName === "string" ? event.toolName : undefined,
269
+ isError: typeof event.isError === "boolean" ? event.isError : undefined,
270
+ })];
271
+ }
83
272
 
84
- const args: string[] = ["--mode", "json", "-p", "--no-session", "--tools", tools.join(",")];
85
- if (options.model) args.push("--model", options.model);
86
- args.push("--append-system-prompt", promptFile);
87
- args.push(`Task: ${options.task}`);
273
+ return [];
274
+ }
88
275
 
276
+ function emitProgress(options: SubagentOptions, event: SubagentProgressEvent): void {
277
+ try {
278
+ options.onProgress?.(event);
279
+ } catch {
280
+ // A display sink must not be able to fail the child runner.
281
+ }
282
+ }
283
+
284
+ const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
285
+
286
+ export async function runPiSubagent(options: SubagentOptions): Promise<SubagentResult> {
287
+ const tools = options.tools ?? ["read", "grep", "find", "ls"];
288
+ let tmpDir = "";
89
289
  const messages: MessageLike[] = [];
90
290
  let stderr = "";
91
- let wasAborted = false;
291
+ let termination: "abort" | "timeout" | null = null;
92
292
 
93
293
  try {
294
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-subagent-"));
295
+ const promptFile = path.join(tmpDir, "system-prompt.md");
296
+ fs.writeFileSync(promptFile, options.systemPrompt, { encoding: "utf8", mode: 0o600 });
297
+
298
+ const args: string[] = ["--mode", "json", "-p", "--no-session", "--tools", tools.join(",")];
299
+ if (options.model) args.push("--model", options.model);
300
+ args.push("--append-system-prompt", promptFile);
301
+ args.push(`Task: ${options.task}`);
302
+
94
303
  const exitCode = await new Promise<number>((resolve) => {
95
304
  const invocation = getPiInvocation(args);
96
305
  const proc = spawn(invocation.command, invocation.args, {
@@ -99,20 +308,61 @@ export async function runPiSubagent(options: SubagentOptions): Promise<SubagentR
99
308
  stdio: ["ignore", "pipe", "pipe"],
100
309
  });
101
310
  let buffer = "";
311
+ let closed = false;
312
+ let settled = false;
313
+ let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
314
+ let forceKillTimer: ReturnType<typeof setTimeout> | undefined;
315
+
316
+ const onAbort = () => {
317
+ if (termination === null) termination = "abort";
318
+ killProc();
319
+ };
320
+
321
+ const cleanup = () => {
322
+ if (timeoutTimer) clearTimeout(timeoutTimer);
323
+ if (forceKillTimer) clearTimeout(forceKillTimer);
324
+ options.signal?.removeEventListener("abort", onAbort);
325
+ };
326
+
327
+ const finish = (code: number) => {
328
+ if (settled) return;
329
+ settled = true;
330
+ closed = true;
331
+ cleanup();
332
+ emitProgress(options, { type: "process", phase: "exited", code });
333
+ resolve(code);
334
+ };
102
335
 
103
336
  const processLine = (line: string) => {
104
337
  if (!line.trim()) return;
105
- let event: { type?: string; message?: MessageLike };
338
+ let event: RawSubagentEvent;
106
339
  try {
107
- event = JSON.parse(line);
340
+ event = JSON.parse(line) as RawSubagentEvent;
108
341
  } catch {
109
342
  return;
110
343
  }
344
+ const progress = normalizeSubagentEvent(event);
345
+ for (const progressEvent of progress) emitProgress(options, progressEvent);
111
346
  if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
112
347
  messages.push(event.message);
113
348
  }
114
349
  };
115
350
 
351
+ const killProc = () => {
352
+ if (settled || termination === null) return;
353
+ if (!proc.killed) proc.kill("SIGTERM");
354
+ if (!forceKillTimer) {
355
+ forceKillTimer = setTimeout(() => {
356
+ try {
357
+ if (!closed) proc.kill("SIGKILL");
358
+ } catch {
359
+ // The process already exited.
360
+ }
361
+ }, 5000);
362
+ }
363
+ };
364
+
365
+ emitProgress(options, { type: "process", phase: "started" });
116
366
  proc.stdout.on("data", (data) => {
117
367
  buffer += data.toString();
118
368
  const lines = buffer.split("\n");
@@ -120,78 +370,57 @@ export async function runPiSubagent(options: SubagentOptions): Promise<SubagentR
120
370
  for (const line of lines) processLine(line);
121
371
  });
122
372
  proc.stderr.on("data", (data) => {
123
- stderr += data.toString();
373
+ const text = data.toString();
374
+ stderr += text;
375
+ emitProgress(options, { type: "stderr", text });
124
376
  });
125
377
  proc.on("close", (code) => {
126
378
  if (buffer.trim()) processLine(buffer);
127
- resolve(code ?? 0);
379
+ finish(code ?? 0);
380
+ });
381
+ proc.on("error", (error) => {
382
+ stderr += error instanceof Error ? error.message : String(error);
383
+ finish(1);
128
384
  });
129
- proc.on("error", () => resolve(1));
130
-
131
- const killProc = () => {
132
- wasAborted = true;
133
- proc.kill("SIGTERM");
134
- setTimeout(() => {
135
- try {
136
- if (!proc.killed) proc.kill("SIGKILL");
137
- } catch {
138
- /* already gone */
139
- }
140
- }, 5000);
141
- };
142
385
 
143
- const timer = setTimeout(killProc, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
144
- const onAbort = () => killProc();
386
+ timeoutTimer = setTimeout(() => {
387
+ if (termination === null) termination = "timeout";
388
+ killProc();
389
+ }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
145
390
  if (options.signal) {
146
- if (options.signal.aborted) killProc();
391
+ if (options.signal.aborted) onAbort();
147
392
  else options.signal.addEventListener("abort", onAbort, { once: true });
148
393
  }
149
- proc.on("close", () => {
150
- clearTimeout(timer);
151
- options.signal?.removeEventListener("abort", onAbort);
152
- });
153
394
  });
154
395
 
155
- if (wasAborted) {
156
- return {
157
- ok: false,
158
- output: finalOutput(messages),
159
- stderr,
160
- turns: messages.filter((m) => m.role === "assistant").length,
161
- errorMessage: "Subagent was aborted",
162
- };
396
+ const turns = messages.filter((message) => message.role === "assistant").length;
397
+ const output = finalOutput(messages);
398
+ if (termination === "abort") {
399
+ return { ok: false, output, stderr, turns, cancelled: true, errorMessage: "Subagent was aborted" };
400
+ }
401
+ if (termination === "timeout") {
402
+ return { ok: false, output, stderr, turns, timedOut: true, errorMessage: "Subagent timed out" };
163
403
  }
164
404
  if (exitCode !== 0) {
165
- return {
166
- ok: false,
167
- output: finalOutput(messages),
168
- stderr,
169
- turns: messages.filter((m) => m.role === "assistant").length,
170
- errorMessage: `pi exited with code ${exitCode}`,
171
- };
405
+ return { ok: false, output, stderr, turns, errorMessage: `pi exited with code ${exitCode}` };
172
406
  }
173
- const output = finalOutput(messages);
174
407
  if (!output) {
175
- return {
176
- ok: false,
177
- output: "",
178
- stderr,
179
- turns: messages.filter((m) => m.role === "assistant").length,
180
- errorMessage: "subagent produced no final output",
181
- };
408
+ return { ok: false, output: "", stderr, turns, errorMessage: "subagent produced no final output" };
182
409
  }
183
410
  return {
184
411
  ok: true,
185
412
  output,
186
- model: [...messages].reverse().find((m) => m.role === "assistant" && m.model)?.model,
413
+ model: [...messages].reverse().find((message) => message.role === "assistant" && message.model)?.model,
187
414
  stderr,
188
- turns: messages.filter((m) => m.role === "assistant").length,
415
+ turns,
189
416
  };
190
417
  } finally {
191
- try {
192
- fs.rmSync(tmpDir, { recursive: true, force: true });
193
- } catch {
194
- /* best effort */
418
+ if (tmpDir) {
419
+ try {
420
+ fs.rmSync(tmpDir, { recursive: true, force: true });
421
+ } catch {
422
+ /* best effort */
423
+ }
195
424
  }
196
425
  }
197
426
  }