@trim21/personal-pi-extensions 0.0.134 → 0.0.137

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/gh-readonly.ts +44 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.134",
3
+ "version": "0.0.137",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -43,11 +43,13 @@ interface GhResult {
43
43
  code: number;
44
44
  killed: boolean;
45
45
  combined: string;
46
+ /** Why the process was killed, when `killed` is true. */
47
+ reason?: "timeout" | "abort";
46
48
  }
47
49
 
48
50
  // ── helpers ──────────────────────────────────────────────────────────────────
49
51
 
50
- function runGh(
52
+ export function runGh(
51
53
  args: string[],
52
54
  ctx: { cwd?: string; signal?: AbortSignal; timeout?: number },
53
55
  ): Promise<GhResult> {
@@ -63,11 +65,14 @@ function runGh(
63
65
  let stderr = "";
64
66
  const combined: string[] = [];
65
67
  let killed = false;
68
+ let killReason: "timeout" | "abort" | undefined;
66
69
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
70
+ let onAbort: (() => void) | undefined;
67
71
 
68
- const killProcess = () => {
72
+ const killProcess = (reason: "timeout" | "abort") => {
69
73
  if (!killed) {
70
74
  killed = true;
75
+ killReason = reason;
71
76
  proc.kill("SIGTERM");
72
77
  setTimeout(() => {
73
78
  if (!proc.killed) proc.kill("SIGKILL");
@@ -76,16 +81,21 @@ function runGh(
76
81
  };
77
82
 
78
83
  if (ctx.signal) {
84
+ onAbort = () => killProcess("abort");
79
85
  if (ctx.signal.aborted) {
80
- killProcess();
86
+ killProcess("abort");
81
87
  } else {
82
- ctx.signal.addEventListener("abort", killProcess, { once: true });
88
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
83
89
  }
84
90
  }
85
91
 
86
- const timeout = ctx.timeout ?? 30_000;
92
+ // Default timeout: 10 minutes. Long operations like downloading a CI job's
93
+ // full log routinely take well over 30s, so a short default would kill them
94
+ // mid-transfer; combined with `code ?? 0` that would silently cache a
95
+ // truncated log as success. A killed process must never look successful.
96
+ const timeout = ctx.timeout ?? 600_000;
87
97
  if (timeout > 0) {
88
- timeoutId = setTimeout(killProcess, timeout);
98
+ timeoutId = setTimeout(() => killProcess("timeout"), timeout);
89
99
  }
90
100
 
91
101
  proc.stdout?.on("data", (data: Buffer) => {
@@ -101,18 +111,29 @@ function runGh(
101
111
 
102
112
  proc.on("close", (code) => {
103
113
  if (timeoutId) clearTimeout(timeoutId);
104
- if (ctx.signal) {
105
- ctx.signal.removeEventListener("abort", killProcess);
114
+ if (ctx.signal && onAbort) {
115
+ ctx.signal.removeEventListener("abort", onAbort);
106
116
  }
107
- resolve({ stdout, stderr, code: code ?? 0, killed, combined: combined.join("") });
117
+ resolve({
118
+ stdout,
119
+ stderr,
120
+ // When killed by a signal the close event's code is null; report the
121
+ // process as failed instead of pretending it succeeded. -1 is a
122
+ // sentinel for "did not exit normally" — distinct from a real gh
123
+ // failure exit code (1), which is always in 0-255.
124
+ code: code ?? (killed ? -1 : 0),
125
+ killed,
126
+ combined: combined.join(""),
127
+ reason: killReason,
128
+ });
108
129
  });
109
130
 
110
131
  proc.on("error", () => {
111
132
  if (timeoutId) clearTimeout(timeoutId);
112
- if (ctx.signal) {
113
- ctx.signal.removeEventListener("abort", killProcess);
133
+ if (ctx.signal && onAbort) {
134
+ ctx.signal.removeEventListener("abort", onAbort);
114
135
  }
115
- resolve({ stdout, stderr, code: 1, killed, combined: combined.join("") });
136
+ resolve({ stdout, stderr, code: 1, killed, combined: combined.join(""), reason: killReason });
116
137
  });
117
138
  });
118
139
  }
@@ -131,7 +152,16 @@ export class GhError extends Error {
131
152
 
132
153
  constructor(args: string[], result: GhResult, input?: unknown) {
133
154
  const inputText = input === undefined ? "" : `<input>${JSON.stringify(input)}<input>\n`;
134
- super(`${inputText}<output>${result.combined.trim() || `exit code ${result.code}`}<output>`);
155
+ const killedText = result.killed
156
+ ? result.reason === "timeout"
157
+ ? " (command timed out)"
158
+ : result.reason === "abort"
159
+ ? " (command aborted)"
160
+ : ""
161
+ : "";
162
+ super(
163
+ `${inputText}<output>${result.combined.trim() || `exit code ${result.code}`}${killedText}<output>`,
164
+ );
135
165
  this.name = "GhError";
136
166
  this.args = args;
137
167
  this.code = result.code;
@@ -196,7 +226,7 @@ export async function pollChecksResult<R extends { code: number; stdout: string
196
226
  }
197
227
 
198
228
  /** Run `gh` and return stdout. On non-zero exit, throws a `GhError` carrying the toolcall input and raw command. */
199
- async function ghExec(
229
+ export async function ghExec(
200
230
  args: string[],
201
231
  ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
202
232
  ): Promise<string> {