@oh-my-pi/pi-coding-agent 1.337.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.
- package/CHANGELOG.md +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { SettingsManager } from "../core/settings-manager.js";
|
|
3
|
+
|
|
4
|
+
let cachedShellConfig: { shell: string; args: string[] } | null = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find bash executable on PATH (Windows)
|
|
8
|
+
*/
|
|
9
|
+
function findBashOnPath(): string | null {
|
|
10
|
+
try {
|
|
11
|
+
const result = Bun.spawnSync(["where", "bash.exe"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
12
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
13
|
+
const firstMatch = result.stdout.toString().trim().split(/\r?\n/)[0];
|
|
14
|
+
if (firstMatch && existsSync(firstMatch)) {
|
|
15
|
+
return firstMatch;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// Ignore errors
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get shell configuration based on platform.
|
|
26
|
+
* Resolution order:
|
|
27
|
+
* 1. User-specified shellPath in settings.json
|
|
28
|
+
* 2. On Windows: Git Bash in known locations, then bash on PATH
|
|
29
|
+
* 3. On Unix: /bin/bash
|
|
30
|
+
* 4. Fallback: sh
|
|
31
|
+
*/
|
|
32
|
+
export function getShellConfig(): { shell: string; args: string[] } {
|
|
33
|
+
if (cachedShellConfig) {
|
|
34
|
+
return cachedShellConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const settings = SettingsManager.create();
|
|
38
|
+
const customShellPath = settings.getShellPath();
|
|
39
|
+
|
|
40
|
+
// 1. Check user-specified shell path
|
|
41
|
+
if (customShellPath) {
|
|
42
|
+
if (existsSync(customShellPath)) {
|
|
43
|
+
cachedShellConfig = { shell: customShellPath, args: ["-c"] };
|
|
44
|
+
return cachedShellConfig;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Custom shell path not found: ${customShellPath}\nPlease update shellPath in ~/.pi/agent/settings.json`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (process.platform === "win32") {
|
|
52
|
+
// 2. Try Git Bash in known locations
|
|
53
|
+
const paths: string[] = [];
|
|
54
|
+
const programFiles = process.env.ProgramFiles;
|
|
55
|
+
if (programFiles) {
|
|
56
|
+
paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
|
|
57
|
+
}
|
|
58
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"];
|
|
59
|
+
if (programFilesX86) {
|
|
60
|
+
paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const path of paths) {
|
|
64
|
+
if (existsSync(path)) {
|
|
65
|
+
cachedShellConfig = { shell: path, args: ["-c"] };
|
|
66
|
+
return cachedShellConfig;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
|
|
71
|
+
const bashOnPath = findBashOnPath();
|
|
72
|
+
if (bashOnPath) {
|
|
73
|
+
cachedShellConfig = { shell: bashOnPath, args: ["-c"] };
|
|
74
|
+
return cachedShellConfig;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(
|
|
78
|
+
`No bash shell found. Options:\n` +
|
|
79
|
+
` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
|
|
80
|
+
` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
|
|
81
|
+
` 3. Set shellPath in ~/.pi/agent/settings.json\n\n` +
|
|
82
|
+
`Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Unix: prefer bash over sh
|
|
87
|
+
if (existsSync("/bin/bash")) {
|
|
88
|
+
cachedShellConfig = { shell: "/bin/bash", args: ["-c"] };
|
|
89
|
+
return cachedShellConfig;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
cachedShellConfig = { shell: "sh", args: ["-c"] };
|
|
93
|
+
return cachedShellConfig;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sanitize binary output for display/storage.
|
|
98
|
+
* Removes characters that crash string-width or cause display issues:
|
|
99
|
+
* - Control characters (except tab, newline, carriage return)
|
|
100
|
+
* - Lone surrogates
|
|
101
|
+
* - Unicode Format characters (crash string-width due to a bug)
|
|
102
|
+
* - Characters with undefined code points
|
|
103
|
+
*/
|
|
104
|
+
export function sanitizeBinaryOutput(str: string): string {
|
|
105
|
+
// Use Array.from to properly iterate over code points (not code units)
|
|
106
|
+
// This handles surrogate pairs correctly and catches edge cases where
|
|
107
|
+
// codePointAt() might return undefined
|
|
108
|
+
return Array.from(str)
|
|
109
|
+
.filter((char) => {
|
|
110
|
+
// Filter out characters that cause string-width to crash
|
|
111
|
+
// This includes:
|
|
112
|
+
// - Unicode format characters
|
|
113
|
+
// - Lone surrogates (already filtered by Array.from)
|
|
114
|
+
// - Control chars except \t \n \r
|
|
115
|
+
// - Characters with undefined code points
|
|
116
|
+
|
|
117
|
+
const code = char.codePointAt(0);
|
|
118
|
+
|
|
119
|
+
// Skip if code point is undefined (edge case with invalid strings)
|
|
120
|
+
if (code === undefined) return false;
|
|
121
|
+
|
|
122
|
+
// Allow tab, newline, carriage return
|
|
123
|
+
if (code === 0x09 || code === 0x0a || code === 0x0d) return true;
|
|
124
|
+
|
|
125
|
+
// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)
|
|
126
|
+
if (code <= 0x1f) return false;
|
|
127
|
+
|
|
128
|
+
// Filter out Unicode format characters
|
|
129
|
+
if (code >= 0xfff9 && code <= 0xfffb) return false;
|
|
130
|
+
|
|
131
|
+
return true;
|
|
132
|
+
})
|
|
133
|
+
.join("");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let pgrepAvailable: boolean | null = null;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if pgrep is available on this system (cached).
|
|
140
|
+
*/
|
|
141
|
+
function hasPgrep(): boolean {
|
|
142
|
+
if (pgrepAvailable === null) {
|
|
143
|
+
try {
|
|
144
|
+
const result = Bun.spawnSync(["pgrep", "--version"], {
|
|
145
|
+
stdin: "ignore",
|
|
146
|
+
stdout: "ignore",
|
|
147
|
+
stderr: "ignore",
|
|
148
|
+
});
|
|
149
|
+
// pgrep exists if it ran (exit 0 or 1 are both valid)
|
|
150
|
+
pgrepAvailable = result.exitCode !== null;
|
|
151
|
+
} catch {
|
|
152
|
+
pgrepAvailable = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return pgrepAvailable;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get direct children of a PID using pgrep.
|
|
160
|
+
*/
|
|
161
|
+
function getChildrenViaPgrep(pid: number): number[] {
|
|
162
|
+
const result = Bun.spawnSync(["pgrep", "-P", String(pid)], {
|
|
163
|
+
stdin: "ignore",
|
|
164
|
+
stdout: "pipe",
|
|
165
|
+
stderr: "ignore",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (result.exitCode !== 0 || !result.stdout) return [];
|
|
169
|
+
|
|
170
|
+
const children: number[] = [];
|
|
171
|
+
for (const line of result.stdout.toString().trim().split("\n")) {
|
|
172
|
+
const childPid = parseInt(line, 10);
|
|
173
|
+
if (!Number.isNaN(childPid)) children.push(childPid);
|
|
174
|
+
}
|
|
175
|
+
return children;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get direct children of a PID using /proc (Linux only).
|
|
180
|
+
*/
|
|
181
|
+
function getChildrenViaProc(pid: number): number[] {
|
|
182
|
+
try {
|
|
183
|
+
const result = Bun.spawnSync(
|
|
184
|
+
[
|
|
185
|
+
"sh",
|
|
186
|
+
"-c",
|
|
187
|
+
`for p in /proc/[0-9]*/stat; do cat "$p" 2>/dev/null; done | awk -v ppid=${pid} '$4 == ppid { print $1 }'`,
|
|
188
|
+
],
|
|
189
|
+
{ stdin: "ignore", stdout: "pipe", stderr: "ignore" },
|
|
190
|
+
);
|
|
191
|
+
if (result.exitCode !== 0 || !result.stdout) return [];
|
|
192
|
+
|
|
193
|
+
const children: number[] = [];
|
|
194
|
+
for (const line of result.stdout.toString().trim().split("\n")) {
|
|
195
|
+
const childPid = parseInt(line, 10);
|
|
196
|
+
if (!Number.isNaN(childPid)) children.push(childPid);
|
|
197
|
+
}
|
|
198
|
+
return children;
|
|
199
|
+
} catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Collect all descendant PIDs breadth-first.
|
|
206
|
+
* Returns deepest descendants first (reverse BFS order) for proper kill ordering.
|
|
207
|
+
*/
|
|
208
|
+
function getDescendantPids(pid: number): number[] {
|
|
209
|
+
const getChildren = hasPgrep() ? getChildrenViaPgrep : getChildrenViaProc;
|
|
210
|
+
const descendants: number[] = [];
|
|
211
|
+
const queue = [pid];
|
|
212
|
+
|
|
213
|
+
while (queue.length > 0) {
|
|
214
|
+
const current = queue.shift()!;
|
|
215
|
+
const children = getChildren(current);
|
|
216
|
+
for (const child of children) {
|
|
217
|
+
descendants.push(child);
|
|
218
|
+
queue.push(child);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Reverse so deepest children are killed first
|
|
223
|
+
return descendants.reverse();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function tryKill(pid: number, signal: NodeJS.Signals): boolean {
|
|
227
|
+
try {
|
|
228
|
+
process.kill(pid, signal);
|
|
229
|
+
return true;
|
|
230
|
+
} catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Kill a process and all its descendants.
|
|
237
|
+
* @param gracePeriodMs - Time to wait after SIGTERM before SIGKILL (0 = immediate SIGKILL)
|
|
238
|
+
*/
|
|
239
|
+
export function killProcessTree(pid: number, gracePeriodMs = 0): void {
|
|
240
|
+
if (process.platform === "win32") {
|
|
241
|
+
Bun.spawnSync(["taskkill", "/F", "/T", "/PID", String(pid)], {
|
|
242
|
+
stdin: "ignore",
|
|
243
|
+
stdout: "ignore",
|
|
244
|
+
stderr: "ignore",
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const signal = gracePeriodMs > 0 ? "SIGTERM" : "SIGKILL";
|
|
250
|
+
|
|
251
|
+
// Fast path: process group kill (works if pid is group leader)
|
|
252
|
+
try {
|
|
253
|
+
process.kill(-pid, signal);
|
|
254
|
+
if (gracePeriodMs > 0) {
|
|
255
|
+
Bun.sleepSync(gracePeriodMs);
|
|
256
|
+
try {
|
|
257
|
+
process.kill(-pid, "SIGKILL");
|
|
258
|
+
} catch {
|
|
259
|
+
// Already dead
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
} catch {
|
|
264
|
+
// Not a process group leader, fall through
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Collect descendants BEFORE killing to minimize race window
|
|
268
|
+
const allPids = [...getDescendantPids(pid), pid];
|
|
269
|
+
|
|
270
|
+
if (gracePeriodMs > 0) {
|
|
271
|
+
for (const p of allPids) tryKill(p, "SIGTERM");
|
|
272
|
+
Bun.sleepSync(gracePeriodMs);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const p of allPids) tryKill(p, "SIGKILL");
|
|
276
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
2
|
+
import { arch, platform } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { APP_NAME, getToolsDir } from "../config.js";
|
|
6
|
+
|
|
7
|
+
const TOOLS_DIR = getToolsDir();
|
|
8
|
+
|
|
9
|
+
interface ToolConfig {
|
|
10
|
+
name: string;
|
|
11
|
+
repo: string; // GitHub repo (e.g., "sharkdp/fd")
|
|
12
|
+
binaryName: string; // Name of the binary inside the archive
|
|
13
|
+
tagPrefix: string; // Prefix for tags (e.g., "v" for v1.0.0, "" for 1.0.0)
|
|
14
|
+
getAssetName: (version: string, plat: string, architecture: string) => string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TOOLS: Record<string, ToolConfig> = {
|
|
18
|
+
fd: {
|
|
19
|
+
name: "fd",
|
|
20
|
+
repo: "sharkdp/fd",
|
|
21
|
+
binaryName: "fd",
|
|
22
|
+
tagPrefix: "v",
|
|
23
|
+
getAssetName: (version, plat, architecture) => {
|
|
24
|
+
if (plat === "darwin") {
|
|
25
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
26
|
+
return `fd-v${version}-${archStr}-apple-darwin.tar.gz`;
|
|
27
|
+
} else if (plat === "linux") {
|
|
28
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
29
|
+
return `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;
|
|
30
|
+
} else if (plat === "win32") {
|
|
31
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
32
|
+
return `fd-v${version}-${archStr}-pc-windows-msvc.zip`;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
rg: {
|
|
38
|
+
name: "ripgrep",
|
|
39
|
+
repo: "BurntSushi/ripgrep",
|
|
40
|
+
binaryName: "rg",
|
|
41
|
+
tagPrefix: "",
|
|
42
|
+
getAssetName: (version, plat, architecture) => {
|
|
43
|
+
if (plat === "darwin") {
|
|
44
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
45
|
+
return `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;
|
|
46
|
+
} else if (plat === "linux") {
|
|
47
|
+
if (architecture === "arm64") {
|
|
48
|
+
return `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;
|
|
49
|
+
}
|
|
50
|
+
return `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;
|
|
51
|
+
} else if (plat === "win32") {
|
|
52
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
53
|
+
return `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
sd: {
|
|
59
|
+
name: "sd",
|
|
60
|
+
repo: "chmln/sd",
|
|
61
|
+
binaryName: "sd",
|
|
62
|
+
tagPrefix: "v",
|
|
63
|
+
getAssetName: (version, plat, architecture) => {
|
|
64
|
+
if (plat === "darwin") {
|
|
65
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
66
|
+
return `sd-v${version}-${archStr}-apple-darwin.tar.gz`;
|
|
67
|
+
} else if (plat === "linux") {
|
|
68
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
69
|
+
return `sd-v${version}-${archStr}-unknown-linux-musl.tar.gz`;
|
|
70
|
+
} else if (plat === "win32") {
|
|
71
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
72
|
+
return `sd-v${version}-${archStr}-pc-windows-msvc.zip`;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
sg: {
|
|
78
|
+
name: "ast-grep",
|
|
79
|
+
repo: "ast-grep/ast-grep",
|
|
80
|
+
binaryName: "sg",
|
|
81
|
+
tagPrefix: "",
|
|
82
|
+
getAssetName: (_version, plat, architecture) => {
|
|
83
|
+
if (plat === "darwin") {
|
|
84
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
85
|
+
return `ast-grep-${archStr}-apple-darwin.zip`;
|
|
86
|
+
} else if (plat === "linux") {
|
|
87
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
88
|
+
return `ast-grep-${archStr}-unknown-linux-gnu.zip`;
|
|
89
|
+
} else if (plat === "win32") {
|
|
90
|
+
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
91
|
+
return `ast-grep-${archStr}-pc-windows-msvc.zip`;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Check if a command exists in PATH by trying to run it
|
|
99
|
+
function commandExists(cmd: string): boolean {
|
|
100
|
+
try {
|
|
101
|
+
const proc = Bun.spawnSync([cmd, "--version"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
102
|
+
return proc.exitCode !== null;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Get the path to a tool (system-wide or in our tools dir)
|
|
109
|
+
export function getToolPath(tool: "fd" | "rg" | "sd" | "sg"): string | null {
|
|
110
|
+
const config = TOOLS[tool];
|
|
111
|
+
if (!config) return null;
|
|
112
|
+
|
|
113
|
+
// Check our tools directory first
|
|
114
|
+
const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : ""));
|
|
115
|
+
if (existsSync(localPath)) {
|
|
116
|
+
return localPath;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check system PATH - if found, just return the command name (it's in PATH)
|
|
120
|
+
if (commandExists(config.binaryName)) {
|
|
121
|
+
return config.binaryName;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fetch latest release version from GitHub
|
|
128
|
+
async function getLatestVersion(repo: string): Promise<string> {
|
|
129
|
+
const response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
|
|
130
|
+
headers: { "User-Agent": `${APP_NAME}-coding-agent` },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`GitHub API error: ${response.status}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const data = (await response.json()) as { tag_name: string };
|
|
138
|
+
return data.tag_name.replace(/^v/, "");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Download a file from URL
|
|
142
|
+
async function downloadFile(url: string, dest: string): Promise<void> {
|
|
143
|
+
const response = await fetch(url);
|
|
144
|
+
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(`Failed to download: ${response.status}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!response.body) {
|
|
150
|
+
throw new Error("No response body");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fileStream = createWriteStream(dest);
|
|
154
|
+
const reader = response.body.getReader();
|
|
155
|
+
while (true) {
|
|
156
|
+
const { done, value } = await reader.read();
|
|
157
|
+
if (done) break;
|
|
158
|
+
fileStream.write(Buffer.from(value));
|
|
159
|
+
}
|
|
160
|
+
fileStream.end();
|
|
161
|
+
await new Promise<void>((resolve, reject) => {
|
|
162
|
+
fileStream.on("finish", resolve);
|
|
163
|
+
fileStream.on("error", reject);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Download and install a tool
|
|
168
|
+
async function downloadTool(tool: "fd" | "rg" | "sd" | "sg"): Promise<string> {
|
|
169
|
+
const config = TOOLS[tool];
|
|
170
|
+
if (!config) throw new Error(`Unknown tool: ${tool}`);
|
|
171
|
+
|
|
172
|
+
const plat = platform();
|
|
173
|
+
const architecture = arch();
|
|
174
|
+
|
|
175
|
+
// Get latest version
|
|
176
|
+
const version = await getLatestVersion(config.repo);
|
|
177
|
+
|
|
178
|
+
// Get asset name for this platform
|
|
179
|
+
const assetName = config.getAssetName(version, plat, architecture);
|
|
180
|
+
if (!assetName) {
|
|
181
|
+
throw new Error(`Unsupported platform: ${plat}/${architecture}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create tools directory
|
|
185
|
+
mkdirSync(TOOLS_DIR, { recursive: true });
|
|
186
|
+
|
|
187
|
+
const downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;
|
|
188
|
+
const archivePath = join(TOOLS_DIR, assetName);
|
|
189
|
+
const binaryExt = plat === "win32" ? ".exe" : "";
|
|
190
|
+
const binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);
|
|
191
|
+
|
|
192
|
+
// Download
|
|
193
|
+
await downloadFile(downloadUrl, archivePath);
|
|
194
|
+
|
|
195
|
+
// Extract
|
|
196
|
+
const extractDir = join(TOOLS_DIR, "extract_tmp");
|
|
197
|
+
mkdirSync(extractDir, { recursive: true });
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
if (assetName.endsWith(".tar.gz")) {
|
|
201
|
+
Bun.spawnSync(["tar", "xzf", archivePath, "-C", extractDir], {
|
|
202
|
+
stdin: "ignore",
|
|
203
|
+
stdout: "pipe",
|
|
204
|
+
stderr: "pipe",
|
|
205
|
+
});
|
|
206
|
+
} else if (assetName.endsWith(".zip")) {
|
|
207
|
+
Bun.spawnSync(["unzip", "-o", archivePath, "-d", extractDir], {
|
|
208
|
+
stdin: "ignore",
|
|
209
|
+
stdout: "pipe",
|
|
210
|
+
stderr: "pipe",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Find the binary in extracted files
|
|
215
|
+
// ast-grep releases the binary directly in the zip, not in a subdirectory
|
|
216
|
+
let extractedBinary: string;
|
|
217
|
+
if (tool === "sg") {
|
|
218
|
+
extractedBinary = join(extractDir, config.binaryName + binaryExt);
|
|
219
|
+
} else {
|
|
220
|
+
const extractedDir = join(extractDir, assetName.replace(/\.(tar\.gz|zip)$/, ""));
|
|
221
|
+
extractedBinary = join(extractedDir, config.binaryName + binaryExt);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (existsSync(extractedBinary)) {
|
|
225
|
+
renameSync(extractedBinary, binaryPath);
|
|
226
|
+
} else {
|
|
227
|
+
throw new Error(`Binary not found in archive: ${extractedBinary}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Make executable (Unix only)
|
|
231
|
+
if (plat !== "win32") {
|
|
232
|
+
chmodSync(binaryPath, 0o755);
|
|
233
|
+
}
|
|
234
|
+
} finally {
|
|
235
|
+
// Cleanup
|
|
236
|
+
rmSync(archivePath, { force: true });
|
|
237
|
+
rmSync(extractDir, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return binaryPath;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Ensure a tool is available, downloading if necessary
|
|
244
|
+
// Returns the path to the tool, or null if unavailable
|
|
245
|
+
export async function ensureTool(
|
|
246
|
+
tool: "fd" | "rg" | "sd" | "sg",
|
|
247
|
+
silent: boolean = false,
|
|
248
|
+
): Promise<string | undefined> {
|
|
249
|
+
const existingPath = getToolPath(tool);
|
|
250
|
+
if (existingPath) {
|
|
251
|
+
return existingPath;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const config = TOOLS[tool];
|
|
255
|
+
if (!config) return undefined;
|
|
256
|
+
|
|
257
|
+
// Tool not found - download it
|
|
258
|
+
if (!silent) {
|
|
259
|
+
console.log(chalk.dim(`${config.name} not found. Downloading...`));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const path = await downloadTool(tool);
|
|
264
|
+
if (!silent) {
|
|
265
|
+
console.log(chalk.dim(`${config.name} installed to ${path}`));
|
|
266
|
+
}
|
|
267
|
+
return path;
|
|
268
|
+
} catch (e) {
|
|
269
|
+
if (!silent) {
|
|
270
|
+
console.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
}
|