@oh-my-pi/pi-coding-agent 0.1.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 +1629 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/config-usage.md +113 -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 +670 -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/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 +89 -0
- package/src/bun-imports.d.ts +16 -0
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +56 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +259 -0
- package/src/cli/file-processor.ts +121 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +661 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli/update-cli.ts +274 -0
- package/src/cli.ts +10 -0
- package/src/config.ts +391 -0
- package/src/core/agent-session.ts +2178 -0
- package/src/core/auth-storage.ts +258 -0
- package/src/core/bash-executor.ts +197 -0
- package/src/core/compaction/branch-summarization.ts +315 -0
- package/src/core/compaction/compaction.ts +664 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +153 -0
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +226 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +22 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +185 -0
- package/src/core/custom-tools/wrapper.ts +29 -0
- package/src/core/exec.ts +139 -0
- package/src/core/export-html/index.ts +159 -0
- package/src/core/export-html/template.css +774 -0
- package/src/core/export-html/template.generated.ts +2 -0
- package/src/core/export-html/template.html +45 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/template.macro.ts +24 -0
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +288 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +98 -0
- package/src/core/hooks/types.ts +770 -0
- package/src/core/index.ts +53 -0
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +185 -0
- package/src/core/mcp/config.ts +248 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +99 -0
- package/src/core/mcp/manager.ts +235 -0
- package/src/core/mcp/tool-bridge.ts +156 -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 +228 -0
- package/src/core/messages.ts +211 -0
- package/src/core/model-registry.ts +334 -0
- package/src/core/model-resolver.ts +494 -0
- package/src/core/plugins/doctor.ts +67 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +339 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +37 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +900 -0
- package/src/core/session-manager.ts +1837 -0
- package/src/core/settings-manager.ts +860 -0
- package/src/core/skills.ts +352 -0
- package/src/core/slash-commands.ts +132 -0
- package/src/core/system-prompt.ts +442 -0
- package/src/core/timings.ts +25 -0
- package/src/core/title-generator.ts +110 -0
- package/src/core/tools/ask.ts +193 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +91 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +487 -0
- package/src/core/tools/edit.ts +140 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +63 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +200 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +338 -0
- package/src/core/tools/exa/types.ts +167 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +244 -0
- package/src/core/tools/grep.ts +584 -0
- package/src/core/tools/index.ts +283 -0
- package/src/core/tools/ls.ts +142 -0
- package/src/core/tools/lsp/client.ts +767 -0
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +845 -0
- package/src/core/tools/lsp/edits.ts +110 -0
- package/src/core/tools/lsp/index.ts +1364 -0
- package/src/core/tools/lsp/render.ts +560 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +495 -0
- package/src/core/tools/lsp/utils.ts +526 -0
- package/src/core/tools/notebook.ts +182 -0
- package/src/core/tools/output.ts +198 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +507 -0
- package/src/core/tools/renderers.ts +820 -0
- package/src/core/tools/review.ts +275 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/core/tools/task/agents.ts +158 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/commands.ts +157 -0
- package/src/core/tools/task/discovery.ts +217 -0
- package/src/core/tools/task/executor.ts +531 -0
- package/src/core/tools/task/index.ts +548 -0
- package/src/core/tools/task/model-resolver.ts +176 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +502 -0
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +142 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2511 -0
- package/src/core/tools/web-search/auth.ts +199 -0
- package/src/core/tools/web-search/index.ts +583 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +196 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +372 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +63 -0
- package/src/core/ttsr.ts +211 -0
- package/src/core/utils.ts +187 -0
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +647 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +104 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +266 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +219 -0
- package/src/index.ts +192 -0
- package/src/main.ts +507 -0
- package/src/migrations.ts +156 -0
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +48 -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 +199 -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/extensions/extension-dashboard.ts +296 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -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 +560 -0
- package/src/modes/interactive/components/oauth-selector.ts +136 -0
- package/src/modes/interactive/components/plugin-settings.ts +481 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +220 -0
- package/src/modes/interactive/components/settings-defs.ts +597 -0
- package/src/modes/interactive/components/settings-selector.ts +545 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +384 -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 +946 -0
- package/src/modes/interactive/components/tree-selector.ts +877 -0
- package/src/modes/interactive/components/ttsr-notification.ts +82 -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 +228 -0
- package/src/modes/interactive/interactive-mode.ts +2669 -0
- package/src/modes/interactive/theme/dark.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +99 -0
- package/src/modes/interactive/theme/theme-schema.json +424 -0
- package/src/modes/interactive/theme/theme.ts +2211 -0
- package/src/modes/print-mode.ts +163 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +494 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/prompts/architect-plan.md +10 -0
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/browser.md +71 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/explore.md +82 -0
- package/src/prompts/implement-with-critic.md +11 -0
- package/src/prompts/implement.md +11 -0
- package/src/prompts/init.md +30 -0
- package/src/prompts/plan.md +54 -0
- package/src/prompts/reviewer.md +81 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/prompts/task.md +56 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -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-snapshot.ts +218 -0
- package/src/utils/shell.ts +364 -0
- package/src/utils/tools-manager.ts +265 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell environment snapshot for preserving user aliases, functions, and options.
|
|
3
|
+
*
|
|
4
|
+
* Creates a snapshot file that captures the user's shell environment from their
|
|
5
|
+
* .bashrc/.zshrc, which can be sourced before each command to provide a familiar
|
|
6
|
+
* shell experience.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
let cachedSnapshotPath: string | null = null;
|
|
14
|
+
let cleanupRegistered = false;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the user's shell config file path.
|
|
18
|
+
*/
|
|
19
|
+
function getShellConfigFile(shell: string): string {
|
|
20
|
+
const home = homedir();
|
|
21
|
+
if (shell.includes("zsh")) return join(home, ".zshrc");
|
|
22
|
+
if (shell.includes("bash")) return join(home, ".bashrc");
|
|
23
|
+
return join(home, ".profile");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate the snapshot creation script.
|
|
28
|
+
* This script sources the user's rc file and extracts functions, aliases, and options.
|
|
29
|
+
* Matches Claude Code's snapshot generation logic.
|
|
30
|
+
*/
|
|
31
|
+
function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: string): string {
|
|
32
|
+
const hasRcFile = existsSync(rcFile);
|
|
33
|
+
const isZsh = shell.includes("zsh");
|
|
34
|
+
|
|
35
|
+
// Escape the snapshot path for shell
|
|
36
|
+
const escapedPath = snapshotPath.replace(/'/g, "'\\''");
|
|
37
|
+
|
|
38
|
+
// Function extraction differs between bash and zsh
|
|
39
|
+
const functionScript = isZsh
|
|
40
|
+
? `
|
|
41
|
+
echo "# Functions" >> "$SNAPSHOT_FILE"
|
|
42
|
+
# Force autoload all functions first
|
|
43
|
+
typeset -f > /dev/null 2>&1
|
|
44
|
+
# Get user function names - filter system/private ones
|
|
45
|
+
typeset +f 2>/dev/null | grep -vE '^(_|__)' | while read func; do
|
|
46
|
+
typeset -f "$func" >> "$SNAPSHOT_FILE" 2>/dev/null
|
|
47
|
+
done
|
|
48
|
+
`
|
|
49
|
+
: `
|
|
50
|
+
echo "# Functions" >> "$SNAPSHOT_FILE"
|
|
51
|
+
# Force autoload all functions first
|
|
52
|
+
declare -f > /dev/null 2>&1
|
|
53
|
+
# Get user function names - filter system/private ones, use base64 for special chars
|
|
54
|
+
declare -F 2>/dev/null | cut -d' ' -f3 | grep -vE '^(_|__)' | while read func; do
|
|
55
|
+
encoded_func=$(declare -f "$func" | base64)
|
|
56
|
+
echo "eval \\"\\$(echo '$encoded_func' | base64 -d)\\" > /dev/null 2>&1" >> "$SNAPSHOT_FILE"
|
|
57
|
+
done
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
// Shell options extraction
|
|
61
|
+
const optionsScript = isZsh
|
|
62
|
+
? `
|
|
63
|
+
echo "# Shell Options" >> "$SNAPSHOT_FILE"
|
|
64
|
+
setopt 2>/dev/null | sed 's/^/setopt /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
65
|
+
`
|
|
66
|
+
: `
|
|
67
|
+
echo "# Shell Options" >> "$SNAPSHOT_FILE"
|
|
68
|
+
shopt -p 2>/dev/null | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
69
|
+
set -o 2>/dev/null | grep "on" | awk '{print "set -o " $1}' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
70
|
+
echo "shopt -s expand_aliases" >> "$SNAPSHOT_FILE"
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
return `
|
|
74
|
+
SNAPSHOT_FILE='${escapedPath}'
|
|
75
|
+
|
|
76
|
+
# Source user's rc file if it exists
|
|
77
|
+
${hasRcFile ? `source "${rcFile}" < /dev/null 2>/dev/null` : "# No user config file to source"}
|
|
78
|
+
|
|
79
|
+
# Create/clear the snapshot file
|
|
80
|
+
echo "# Shell snapshot - generated by omp agent" >| "$SNAPSHOT_FILE"
|
|
81
|
+
|
|
82
|
+
# Unalias everything first to avoid conflicts when sourced
|
|
83
|
+
echo "unalias -a 2>/dev/null || true" >> "$SNAPSHOT_FILE"
|
|
84
|
+
|
|
85
|
+
${functionScript}
|
|
86
|
+
|
|
87
|
+
${optionsScript}
|
|
88
|
+
|
|
89
|
+
# Export aliases (limit to 1000)
|
|
90
|
+
echo "# Aliases" >> "$SNAPSHOT_FILE"
|
|
91
|
+
# Filter out winpty aliases on Windows to avoid "stdin is not a tty" errors
|
|
92
|
+
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
|
93
|
+
alias 2>/dev/null | grep -v "='winpty " | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
94
|
+
else
|
|
95
|
+
alias 2>/dev/null | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Export PATH
|
|
99
|
+
echo "export PATH='$PATH'" >> "$SNAPSHOT_FILE"
|
|
100
|
+
|
|
101
|
+
# Verify snapshot was created
|
|
102
|
+
if [ ! -f "$SNAPSHOT_FILE" ]; then
|
|
103
|
+
echo "Error: Snapshot file was not created" >&2
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
`.trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create a shell snapshot, caching the result.
|
|
111
|
+
* Returns the path to the snapshot file, or null if creation failed.
|
|
112
|
+
*/
|
|
113
|
+
export async function getOrCreateSnapshot(
|
|
114
|
+
shell: string,
|
|
115
|
+
env: Record<string, string | undefined>,
|
|
116
|
+
): Promise<string | null> {
|
|
117
|
+
// Return cached snapshot if valid
|
|
118
|
+
if (cachedSnapshotPath && existsSync(cachedSnapshotPath)) {
|
|
119
|
+
return cachedSnapshotPath;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Skip on Windows (no .bashrc in standard location)
|
|
123
|
+
if (process.platform === "win32") {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rcFile = getShellConfigFile(shell);
|
|
128
|
+
|
|
129
|
+
// Create snapshot directory
|
|
130
|
+
const snapshotDir = join(tmpdir(), "omp-shell-snapshots");
|
|
131
|
+
try {
|
|
132
|
+
mkdirSync(snapshotDir, { recursive: true });
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Generate unique snapshot path
|
|
138
|
+
const timestamp = Date.now();
|
|
139
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
140
|
+
const shellName = shell.includes("zsh") ? "zsh" : shell.includes("bash") ? "bash" : "sh";
|
|
141
|
+
const snapshotPath = join(snapshotDir, `snapshot-${shellName}-${timestamp}-${random}.sh`);
|
|
142
|
+
|
|
143
|
+
// Generate and execute snapshot script
|
|
144
|
+
const script = generateSnapshotScript(shell, snapshotPath, rcFile);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const result = Bun.spawnSync([shell, "-l", "-c", script], {
|
|
148
|
+
stdin: "ignore",
|
|
149
|
+
stdout: "pipe",
|
|
150
|
+
stderr: "pipe",
|
|
151
|
+
env,
|
|
152
|
+
timeout: 10000, // 10 second timeout
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (result.exitCode === 0 && existsSync(snapshotPath)) {
|
|
156
|
+
cachedSnapshotPath = snapshotPath;
|
|
157
|
+
registerCleanup();
|
|
158
|
+
return snapshotPath;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Snapshot creation failed, proceed without it
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get the command prefix to source the snapshot.
|
|
169
|
+
* Returns empty string if no snapshot available.
|
|
170
|
+
*/
|
|
171
|
+
export function getSnapshotSourceCommand(snapshotPath: string | null): string {
|
|
172
|
+
if (!snapshotPath) return "";
|
|
173
|
+
// Escape for shell
|
|
174
|
+
const escaped = snapshotPath.replace(/'/g, "'\\''");
|
|
175
|
+
return `source '${escaped}' 2>/dev/null && `;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Register cleanup handler to delete snapshot on process exit.
|
|
180
|
+
*/
|
|
181
|
+
function registerCleanup(): void {
|
|
182
|
+
if (cleanupRegistered) return;
|
|
183
|
+
cleanupRegistered = true;
|
|
184
|
+
|
|
185
|
+
const cleanup = () => {
|
|
186
|
+
if (cachedSnapshotPath && existsSync(cachedSnapshotPath)) {
|
|
187
|
+
try {
|
|
188
|
+
unlinkSync(cachedSnapshotPath);
|
|
189
|
+
} catch {
|
|
190
|
+
// Ignore cleanup errors
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
process.on("exit", cleanup);
|
|
196
|
+
process.on("SIGINT", () => {
|
|
197
|
+
cleanup();
|
|
198
|
+
process.exit(130);
|
|
199
|
+
});
|
|
200
|
+
process.on("SIGTERM", () => {
|
|
201
|
+
cleanup();
|
|
202
|
+
process.exit(143);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Clear the cached snapshot (for testing or forced refresh).
|
|
208
|
+
*/
|
|
209
|
+
export function clearSnapshotCache(): void {
|
|
210
|
+
if (cachedSnapshotPath && existsSync(cachedSnapshotPath)) {
|
|
211
|
+
try {
|
|
212
|
+
unlinkSync(cachedSnapshotPath);
|
|
213
|
+
} catch {
|
|
214
|
+
// Ignore
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
cachedSnapshotPath = null;
|
|
218
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync } from "node:fs";
|
|
2
|
+
import { SettingsManager } from "../core/settings-manager";
|
|
3
|
+
|
|
4
|
+
export interface ShellConfig {
|
|
5
|
+
shell: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
env: Record<string, string | undefined>;
|
|
8
|
+
prefix: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let cachedShellConfig: ShellConfig | null = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a shell binary is executable.
|
|
15
|
+
*/
|
|
16
|
+
function isExecutable(path: string): boolean {
|
|
17
|
+
try {
|
|
18
|
+
accessSync(path, constants.X_OK);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build the spawn environment (cached).
|
|
27
|
+
*/
|
|
28
|
+
function buildSpawnEnv(shell: string): Record<string, string | undefined> {
|
|
29
|
+
const noCI = process.env.OMP_BASH_NO_CI || process.env.CLAUDE_BASH_NO_CI;
|
|
30
|
+
return {
|
|
31
|
+
...process.env,
|
|
32
|
+
SHELL: shell,
|
|
33
|
+
GIT_EDITOR: "true",
|
|
34
|
+
GPG_TTY: "not a tty",
|
|
35
|
+
OMPCODE: "1",
|
|
36
|
+
CLAUDECODE: "1",
|
|
37
|
+
...(noCI ? {} : { CI: "true" }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get shell args, optionally including login shell flag.
|
|
43
|
+
* Supports OMP_BASH_NO_LOGIN and CLAUDE_BASH_NO_LOGIN to skip -l.
|
|
44
|
+
*/
|
|
45
|
+
function getShellArgs(): string[] {
|
|
46
|
+
const noLogin = process.env.OMP_BASH_NO_LOGIN || process.env.CLAUDE_BASH_NO_LOGIN;
|
|
47
|
+
return noLogin ? ["-c"] : ["-l", "-c"];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get shell prefix for wrapping commands (profilers, strace, etc.).
|
|
52
|
+
*/
|
|
53
|
+
function getShellPrefix(): string | undefined {
|
|
54
|
+
return process.env.OMP_SHELL_PREFIX || process.env.CLAUDE_CODE_SHELL_PREFIX;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find bash executable on PATH (Windows)
|
|
59
|
+
*/
|
|
60
|
+
function findBashOnPath(): string | null {
|
|
61
|
+
try {
|
|
62
|
+
const result = Bun.spawnSync(["where", "bash.exe"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
63
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
64
|
+
const firstMatch = result.stdout.toString().trim().split(/\r?\n/)[0];
|
|
65
|
+
if (firstMatch && existsSync(firstMatch)) {
|
|
66
|
+
return firstMatch;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Ignore errors
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build full shell config from a shell path.
|
|
77
|
+
*/
|
|
78
|
+
function buildConfig(shell: string): ShellConfig {
|
|
79
|
+
return {
|
|
80
|
+
shell,
|
|
81
|
+
args: getShellArgs(),
|
|
82
|
+
env: buildSpawnEnv(shell),
|
|
83
|
+
prefix: getShellPrefix(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get shell configuration based on platform.
|
|
89
|
+
* Resolution order:
|
|
90
|
+
* 1. User-specified shellPath in settings.json
|
|
91
|
+
* 2. On Windows: Git Bash in known locations, then bash on PATH
|
|
92
|
+
* 3. On Unix: $SHELL if bash/zsh, then fallback paths
|
|
93
|
+
* 4. Fallback: sh
|
|
94
|
+
*/
|
|
95
|
+
export function getShellConfig(): ShellConfig {
|
|
96
|
+
if (cachedShellConfig) {
|
|
97
|
+
return cachedShellConfig;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const settings = SettingsManager.create();
|
|
101
|
+
const customShellPath = settings.getShellPath();
|
|
102
|
+
|
|
103
|
+
// 1. Check user-specified shell path
|
|
104
|
+
if (customShellPath) {
|
|
105
|
+
if (existsSync(customShellPath)) {
|
|
106
|
+
cachedShellConfig = buildConfig(customShellPath);
|
|
107
|
+
return cachedShellConfig;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Custom shell path not found: ${customShellPath}\nPlease update shellPath in ~/.omp/agent/settings.json`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (process.platform === "win32") {
|
|
115
|
+
// 2. Try Git Bash in known locations
|
|
116
|
+
const paths: string[] = [];
|
|
117
|
+
const programFiles = process.env.ProgramFiles;
|
|
118
|
+
if (programFiles) {
|
|
119
|
+
paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
|
|
120
|
+
}
|
|
121
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"];
|
|
122
|
+
if (programFilesX86) {
|
|
123
|
+
paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const path of paths) {
|
|
127
|
+
if (existsSync(path)) {
|
|
128
|
+
cachedShellConfig = buildConfig(path);
|
|
129
|
+
return cachedShellConfig;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
|
|
134
|
+
const bashOnPath = findBashOnPath();
|
|
135
|
+
if (bashOnPath) {
|
|
136
|
+
cachedShellConfig = buildConfig(bashOnPath);
|
|
137
|
+
return cachedShellConfig;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw new Error(
|
|
141
|
+
`No bash shell found. Options:\n` +
|
|
142
|
+
` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
|
|
143
|
+
` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
|
|
144
|
+
` 3. Set shellPath in ~/.omp/agent/settings.json\n\n` +
|
|
145
|
+
`Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Unix: prefer user's shell from $SHELL if it's bash/zsh and executable
|
|
150
|
+
const userShell = process.env.SHELL;
|
|
151
|
+
const isValidShell = userShell && (userShell.includes("bash") || userShell.includes("zsh"));
|
|
152
|
+
if (isValidShell && isExecutable(userShell)) {
|
|
153
|
+
cachedShellConfig = buildConfig(userShell);
|
|
154
|
+
return cachedShellConfig;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fallback paths (Claude's approach: check known locations)
|
|
158
|
+
const fallbackPaths = ["/bin", "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"];
|
|
159
|
+
const preferZsh = !userShell?.includes("bash");
|
|
160
|
+
const shellOrder = preferZsh ? ["zsh", "bash"] : ["bash", "zsh"];
|
|
161
|
+
|
|
162
|
+
for (const shellName of shellOrder) {
|
|
163
|
+
for (const dir of fallbackPaths) {
|
|
164
|
+
const shellPath = `${dir}/${shellName}`;
|
|
165
|
+
if (isExecutable(shellPath)) {
|
|
166
|
+
cachedShellConfig = buildConfig(shellPath);
|
|
167
|
+
return cachedShellConfig;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Last resort: use Bun.which
|
|
173
|
+
const bashPath = Bun.which("bash");
|
|
174
|
+
if (bashPath) {
|
|
175
|
+
cachedShellConfig = buildConfig(bashPath);
|
|
176
|
+
return cachedShellConfig;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const shPath = Bun.which("sh");
|
|
180
|
+
cachedShellConfig = buildConfig(shPath || "sh");
|
|
181
|
+
return cachedShellConfig;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Sanitize binary output for display/storage.
|
|
186
|
+
* Removes characters that crash string-width or cause display issues:
|
|
187
|
+
* - Control characters (except tab, newline, carriage return)
|
|
188
|
+
* - Lone surrogates
|
|
189
|
+
* - Unicode Format characters (crash string-width due to a bug)
|
|
190
|
+
* - Characters with undefined code points
|
|
191
|
+
*/
|
|
192
|
+
export function sanitizeBinaryOutput(str: string): string {
|
|
193
|
+
// Use Array.from to properly iterate over code points (not code units)
|
|
194
|
+
// This handles surrogate pairs correctly and catches edge cases where
|
|
195
|
+
// codePointAt() might return undefined
|
|
196
|
+
return Array.from(str)
|
|
197
|
+
.filter((char) => {
|
|
198
|
+
// Filter out characters that cause string-width to crash
|
|
199
|
+
// This includes:
|
|
200
|
+
// - Unicode format characters
|
|
201
|
+
// - Lone surrogates (already filtered by Array.from)
|
|
202
|
+
// - Control chars except \t \n \r
|
|
203
|
+
// - Characters with undefined code points
|
|
204
|
+
|
|
205
|
+
const code = char.codePointAt(0);
|
|
206
|
+
|
|
207
|
+
// Skip if code point is undefined (edge case with invalid strings)
|
|
208
|
+
if (code === undefined) return false;
|
|
209
|
+
|
|
210
|
+
// Allow tab, newline, carriage return
|
|
211
|
+
if (code === 0x09 || code === 0x0a || code === 0x0d) return true;
|
|
212
|
+
|
|
213
|
+
// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)
|
|
214
|
+
if (code <= 0x1f) return false;
|
|
215
|
+
|
|
216
|
+
// Filter out Unicode format characters
|
|
217
|
+
if (code >= 0xfff9 && code <= 0xfffb) return false;
|
|
218
|
+
|
|
219
|
+
return true;
|
|
220
|
+
})
|
|
221
|
+
.join("");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let pgrepAvailable: boolean | null = null;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if pgrep is available on this system (cached).
|
|
228
|
+
*/
|
|
229
|
+
function hasPgrep(): boolean {
|
|
230
|
+
if (pgrepAvailable === null) {
|
|
231
|
+
try {
|
|
232
|
+
const result = Bun.spawnSync(["pgrep", "--version"], {
|
|
233
|
+
stdin: "ignore",
|
|
234
|
+
stdout: "ignore",
|
|
235
|
+
stderr: "ignore",
|
|
236
|
+
});
|
|
237
|
+
// pgrep exists if it ran (exit 0 or 1 are both valid)
|
|
238
|
+
pgrepAvailable = result.exitCode !== null;
|
|
239
|
+
} catch {
|
|
240
|
+
pgrepAvailable = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return pgrepAvailable;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get direct children of a PID using pgrep.
|
|
248
|
+
*/
|
|
249
|
+
function getChildrenViaPgrep(pid: number): number[] {
|
|
250
|
+
const result = Bun.spawnSync(["pgrep", "-P", String(pid)], {
|
|
251
|
+
stdin: "ignore",
|
|
252
|
+
stdout: "pipe",
|
|
253
|
+
stderr: "ignore",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (result.exitCode !== 0 || !result.stdout) return [];
|
|
257
|
+
|
|
258
|
+
const children: number[] = [];
|
|
259
|
+
for (const line of result.stdout.toString().trim().split("\n")) {
|
|
260
|
+
const childPid = parseInt(line, 10);
|
|
261
|
+
if (!Number.isNaN(childPid)) children.push(childPid);
|
|
262
|
+
}
|
|
263
|
+
return children;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get direct children of a PID using /proc (Linux only).
|
|
268
|
+
*/
|
|
269
|
+
function getChildrenViaProc(pid: number): number[] {
|
|
270
|
+
try {
|
|
271
|
+
const result = Bun.spawnSync(
|
|
272
|
+
[
|
|
273
|
+
"sh",
|
|
274
|
+
"-c",
|
|
275
|
+
`for p in /proc/[0-9]*/stat; do cat "$p" 2>/dev/null; done | awk -v ppid=${pid} '$4 == ppid { print $1 }'`,
|
|
276
|
+
],
|
|
277
|
+
{ stdin: "ignore", stdout: "pipe", stderr: "ignore" },
|
|
278
|
+
);
|
|
279
|
+
if (result.exitCode !== 0 || !result.stdout) return [];
|
|
280
|
+
|
|
281
|
+
const children: number[] = [];
|
|
282
|
+
for (const line of result.stdout.toString().trim().split("\n")) {
|
|
283
|
+
const childPid = parseInt(line, 10);
|
|
284
|
+
if (!Number.isNaN(childPid)) children.push(childPid);
|
|
285
|
+
}
|
|
286
|
+
return children;
|
|
287
|
+
} catch {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Collect all descendant PIDs breadth-first.
|
|
294
|
+
* Returns deepest descendants first (reverse BFS order) for proper kill ordering.
|
|
295
|
+
*/
|
|
296
|
+
function getDescendantPids(pid: number): number[] {
|
|
297
|
+
const getChildren = hasPgrep() ? getChildrenViaPgrep : getChildrenViaProc;
|
|
298
|
+
const descendants: number[] = [];
|
|
299
|
+
const queue = [pid];
|
|
300
|
+
|
|
301
|
+
while (queue.length > 0) {
|
|
302
|
+
const current = queue.shift()!;
|
|
303
|
+
const children = getChildren(current);
|
|
304
|
+
for (const child of children) {
|
|
305
|
+
descendants.push(child);
|
|
306
|
+
queue.push(child);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Reverse so deepest children are killed first
|
|
311
|
+
return descendants.reverse();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function tryKill(pid: number, signal: NodeJS.Signals): boolean {
|
|
315
|
+
try {
|
|
316
|
+
process.kill(pid, signal);
|
|
317
|
+
return true;
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Kill a process and all its descendants.
|
|
325
|
+
* @param gracePeriodMs - Time to wait after SIGTERM before SIGKILL (0 = immediate SIGKILL)
|
|
326
|
+
*/
|
|
327
|
+
export function killProcessTree(pid: number, gracePeriodMs = 0): void {
|
|
328
|
+
if (process.platform === "win32") {
|
|
329
|
+
Bun.spawnSync(["taskkill", "/F", "/T", "/PID", String(pid)], {
|
|
330
|
+
stdin: "ignore",
|
|
331
|
+
stdout: "ignore",
|
|
332
|
+
stderr: "ignore",
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const signal = gracePeriodMs > 0 ? "SIGTERM" : "SIGKILL";
|
|
338
|
+
|
|
339
|
+
// Fast path: process group kill (works if pid is group leader)
|
|
340
|
+
try {
|
|
341
|
+
process.kill(-pid, signal);
|
|
342
|
+
if (gracePeriodMs > 0) {
|
|
343
|
+
Bun.sleepSync(gracePeriodMs);
|
|
344
|
+
try {
|
|
345
|
+
process.kill(-pid, "SIGKILL");
|
|
346
|
+
} catch {
|
|
347
|
+
// Already dead
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
} catch {
|
|
352
|
+
// Not a process group leader, fall through
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Collect descendants BEFORE killing to minimize race window
|
|
356
|
+
const allPids = [...getDescendantPids(pid), pid];
|
|
357
|
+
|
|
358
|
+
if (gracePeriodMs > 0) {
|
|
359
|
+
for (const p of allPids) tryKill(p, "SIGTERM");
|
|
360
|
+
Bun.sleepSync(gracePeriodMs);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const p of allPids) tryKill(p, "SIGKILL");
|
|
364
|
+
}
|