@f5xc-salesdemos/pi-utils 14.0.2
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 +60 -0
- package/src/abortable.ts +85 -0
- package/src/async.ts +50 -0
- package/src/cli.ts +432 -0
- package/src/color.ts +204 -0
- package/src/dirs.ts +425 -0
- package/src/env.ts +84 -0
- package/src/format.ts +106 -0
- package/src/frontmatter.ts +118 -0
- package/src/fs-error.ts +56 -0
- package/src/glob.ts +189 -0
- package/src/hook-fetch.ts +30 -0
- package/src/index.ts +47 -0
- package/src/json.ts +10 -0
- package/src/logger.ts +204 -0
- package/src/mermaid-ascii.ts +31 -0
- package/src/mime.ts +159 -0
- package/src/peek-file.ts +114 -0
- package/src/postmortem.ts +197 -0
- package/src/procmgr.ts +326 -0
- package/src/prompt.ts +401 -0
- package/src/ptree.ts +386 -0
- package/src/ring.ts +169 -0
- package/src/snowflake.ts +136 -0
- package/src/stream.ts +316 -0
- package/src/temp.ts +77 -0
- package/src/type-guards.ts +11 -0
- package/src/which.ts +230 -0
package/src/procmgr.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import * as timers from "node:timers";
|
|
4
|
+
import type { Subprocess } from "bun";
|
|
5
|
+
import { $env } from "./env";
|
|
6
|
+
import { $which } from "./which";
|
|
7
|
+
|
|
8
|
+
export interface ShellConfig {
|
|
9
|
+
shell: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
prefix: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let cachedShellConfig: ShellConfig | null = null;
|
|
16
|
+
|
|
17
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
18
|
+
const TERM_SIGNAL = IS_WINDOWS ? undefined : "SIGTERM";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a shell binary is executable.
|
|
22
|
+
*/
|
|
23
|
+
function isExecutable(path: string): boolean {
|
|
24
|
+
try {
|
|
25
|
+
fs.accessSync(path, fs.constants.X_OK);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build the spawn environment (cached).
|
|
34
|
+
*/
|
|
35
|
+
function buildSpawnEnv(shell: string): Record<string, string> {
|
|
36
|
+
const noCI = $env.PI_BASH_NO_CI || $env.CLAUDE_BASH_NO_CI;
|
|
37
|
+
return {
|
|
38
|
+
...Bun.env,
|
|
39
|
+
SHELL: shell,
|
|
40
|
+
GIT_EDITOR: "true",
|
|
41
|
+
GPG_TTY: "not a tty",
|
|
42
|
+
OMPCODE: "1",
|
|
43
|
+
CLAUDECODE: "1",
|
|
44
|
+
...(noCI ? {} : { CI: "true" }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get shell args, optionally including login shell flag.
|
|
50
|
+
* Supports PI_BASH_NO_LOGIN and CLAUDE_BASH_NO_LOGIN to skip -l.
|
|
51
|
+
*/
|
|
52
|
+
function getShellArgs(): string[] {
|
|
53
|
+
const noLogin = $env.PI_BASH_NO_LOGIN || $env.CLAUDE_BASH_NO_LOGIN;
|
|
54
|
+
return noLogin ? ["-c"] : ["-l", "-c"];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get shell prefix for wrapping commands (profilers, strace, etc.).
|
|
59
|
+
*/
|
|
60
|
+
function getShellPrefix(): string | undefined {
|
|
61
|
+
return $env.PI_SHELL_PREFIX || $env.CLAUDE_CODE_SHELL_PREFIX;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build full shell config from a shell path.
|
|
66
|
+
*/
|
|
67
|
+
function buildConfig(shell: string): ShellConfig {
|
|
68
|
+
return {
|
|
69
|
+
shell,
|
|
70
|
+
args: getShellArgs(),
|
|
71
|
+
env: buildSpawnEnv(shell),
|
|
72
|
+
prefix: getShellPrefix(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a basic shell (bash or sh) as fallback.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveBasicShell(): string | undefined {
|
|
80
|
+
for (const name of ["bash", "bash.exe", "sh", "sh.exe"]) {
|
|
81
|
+
const resolved = $which(name);
|
|
82
|
+
if (resolved) return resolved;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (process.platform !== "win32") {
|
|
86
|
+
const searchPaths = ["/bin", "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"];
|
|
87
|
+
const candidates = ["bash", "sh"];
|
|
88
|
+
|
|
89
|
+
for (const name of candidates) {
|
|
90
|
+
for (const dir of searchPaths) {
|
|
91
|
+
const fullPath = path.join(dir, name);
|
|
92
|
+
if (fs.existsSync(fullPath)) return fullPath;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get shell configuration based on platform.
|
|
102
|
+
* Resolution order:
|
|
103
|
+
* 1. User-specified shellPath in settings.json
|
|
104
|
+
* 2. On Windows: Git Bash in known locations, then bash on PATH
|
|
105
|
+
* 3. On Unix: $SHELL if bash/zsh, then fallback paths
|
|
106
|
+
* 4. Fallback: sh
|
|
107
|
+
*/
|
|
108
|
+
export function getShellConfig(customShellPath?: string): ShellConfig {
|
|
109
|
+
if (cachedShellConfig) {
|
|
110
|
+
return cachedShellConfig;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 1. Check user-specified shell path
|
|
114
|
+
if (customShellPath) {
|
|
115
|
+
if (fs.existsSync(customShellPath)) {
|
|
116
|
+
cachedShellConfig = buildConfig(customShellPath);
|
|
117
|
+
return cachedShellConfig;
|
|
118
|
+
}
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Custom shell path not found: ${customShellPath}\nPlease update shellPath in ~/.xcsh/agent/settings.json`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (process.platform === "win32") {
|
|
125
|
+
// 2. Try Git Bash in known locations
|
|
126
|
+
const paths: string[] = [];
|
|
127
|
+
const programFiles = Bun.env.ProgramFiles;
|
|
128
|
+
if (programFiles) {
|
|
129
|
+
paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
|
|
130
|
+
}
|
|
131
|
+
const programFilesX86 = Bun.env["ProgramFiles(x86)"];
|
|
132
|
+
if (programFilesX86) {
|
|
133
|
+
paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const path of paths) {
|
|
137
|
+
if (fs.existsSync(path)) {
|
|
138
|
+
cachedShellConfig = buildConfig(path);
|
|
139
|
+
return cachedShellConfig;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
|
|
144
|
+
const bashOnPath = $which("bash.exe");
|
|
145
|
+
if (bashOnPath) {
|
|
146
|
+
cachedShellConfig = buildConfig(bashOnPath);
|
|
147
|
+
return cachedShellConfig;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw new Error(
|
|
151
|
+
`No bash shell found. Options:\n` +
|
|
152
|
+
` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
|
|
153
|
+
` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
|
|
154
|
+
` 3. Set shellPath in ~/.xcsh/agent/settings.json\n\n` +
|
|
155
|
+
`Searched Git Bash in:\n${paths.map(p => ` ${p}`).join("\n")}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Unix: prefer user's shell from $SHELL if it's bash/zsh and executable
|
|
160
|
+
const userShell = Bun.env.SHELL;
|
|
161
|
+
const isValidShell = userShell && (userShell.includes("bash") || userShell.includes("zsh"));
|
|
162
|
+
if (isValidShell && isExecutable(userShell)) {
|
|
163
|
+
cachedShellConfig = buildConfig(userShell);
|
|
164
|
+
return cachedShellConfig;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 4. Fallback: use basic shell
|
|
168
|
+
const basicShell = resolveBasicShell();
|
|
169
|
+
if (basicShell) {
|
|
170
|
+
cachedShellConfig = buildConfig(basicShell);
|
|
171
|
+
return cachedShellConfig;
|
|
172
|
+
}
|
|
173
|
+
cachedShellConfig = buildConfig("sh");
|
|
174
|
+
return cachedShellConfig;
|
|
175
|
+
}
|
|
176
|
+
|
|
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
|
+
/**
|
|
211
|
+
* Check if a process is running.
|
|
212
|
+
*/
|
|
213
|
+
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
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
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);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function onProcessExit(proc: Subprocess | number, abortSignal?: AbortSignal): Promise<boolean> {
|
|
235
|
+
if (typeof proc !== "number") {
|
|
236
|
+
return proc.exited.then(
|
|
237
|
+
() => true,
|
|
238
|
+
() => true,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
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
|
+
}
|
|
326
|
+
}
|