refactor: simplify hook execution - use Node directly instead of Bun (#290)
Removes bun-wrapper indirection. Hooks are compiled JavaScript that work perfectly with Node. Worker still uses Bun where performance matters. Fixes #264
This commit is contained in:
Generated
-3882
File diff suppressed because it is too large
Load Diff
@@ -7,12 +7,12 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\" && bun \"${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\" && node \"${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js\"",
|
||||
"timeout": 300
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js\"",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
@@ -23,7 +23,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js\"",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
@@ -35,7 +35,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js\"",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
@@ -46,7 +46,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js\"",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
@@ -57,7 +57,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js\"",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11
-11
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,9 +1,10 @@
|
||||
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { spawn, spawnSync } from 'child_process';
|
||||
import { spawn } from 'child_process';
|
||||
import { homedir } from 'os';
|
||||
import { DATA_DIR } from '../../shared/paths.js';
|
||||
import { getBunPath, isBunAvailable } from '../../utils/bun-path.js';
|
||||
|
||||
const PID_FILE = join(DATA_DIR, 'worker.pid');
|
||||
const LOG_DIR = join(DATA_DIR, 'logs');
|
||||
@@ -56,26 +57,22 @@ export class ProcessManager {
|
||||
}
|
||||
|
||||
private static isBunAvailable(): boolean {
|
||||
try {
|
||||
const result = spawnSync('bun', ['--version'], { stdio: 'pipe', timeout: 5000 });
|
||||
return result.status === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return isBunAvailable();
|
||||
}
|
||||
|
||||
private static async startWithBun(script: string, logFile: string, port: number): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||
if (!this.isBunAvailable()) {
|
||||
const bunPath = getBunPath();
|
||||
if (!bunPath) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Bun is required but not found in PATH. Install from https://bun.sh'
|
||||
error: 'Bun is required but not found in PATH or common installation paths. Install from https://bun.sh'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
const child = spawn('bun', [script], {
|
||||
const child = spawn(bunPath, [script], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env, CLAUDE_MEM_WORKER_PORT: String(port) },
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Bun Path Utility
|
||||
*
|
||||
* Resolves the Bun executable path for environments where Bun is not in PATH
|
||||
* (e.g., fish shell users where ~/.config/fish/config.fish isn't read by /bin/sh)
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
/**
|
||||
* Get the Bun executable path
|
||||
* Tries PATH first, then checks common installation locations
|
||||
* Returns absolute path if found, null otherwise
|
||||
*/
|
||||
export function getBunPath(): string | null {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Try PATH first
|
||||
try {
|
||||
const result = spawnSync('bun', ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: isWindows
|
||||
});
|
||||
if (result.status === 0) {
|
||||
return 'bun'; // Available in PATH
|
||||
}
|
||||
} catch {
|
||||
// Not in PATH, continue to check common locations
|
||||
}
|
||||
|
||||
// Check common installation paths
|
||||
const bunPaths = isWindows
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [
|
||||
join(homedir(), '.bun', 'bin', 'bun'),
|
||||
'/usr/local/bin/bun',
|
||||
'/opt/homebrew/bin/bun', // Apple Silicon Homebrew
|
||||
'/home/linuxbrew/.linuxbrew/bin/bun' // Linux Homebrew
|
||||
];
|
||||
|
||||
for (const bunPath of bunPaths) {
|
||||
if (existsSync(bunPath)) {
|
||||
return bunPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bun executable path or throw an error
|
||||
* Use this when Bun is required for operation
|
||||
*/
|
||||
export function getBunPathOrThrow(): string {
|
||||
const bunPath = getBunPath();
|
||||
if (!bunPath) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const installCmd = isWindows
|
||||
? 'powershell -c "irm bun.sh/install.ps1 | iex"'
|
||||
: 'curl -fsSL https://bun.sh/install | bash';
|
||||
throw new Error(
|
||||
`Bun is required but not found. Install it with:\n ${installCmd}\nThen restart your terminal.`
|
||||
);
|
||||
}
|
||||
return bunPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Bun is available (in PATH or common locations)
|
||||
*/
|
||||
export function isBunAvailable(): boolean {
|
||||
return getBunPath() !== null;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { existsSync } from 'fs';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('fs', () => ({
|
||||
existsSync: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('child_process', () => ({
|
||||
spawnSync: vi.fn()
|
||||
}));
|
||||
|
||||
// Import after mocking
|
||||
import { getBunPath, isBunAvailable, getBunPathOrThrow } from '../src/utils/bun-path';
|
||||
|
||||
describe('bun-path utility', () => {
|
||||
it('should return "bun" when available in PATH', () => {
|
||||
// Mock successful bun --version check
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
status: 0,
|
||||
stdout: Buffer.from('1.0.0'),
|
||||
stderr: Buffer.from(''),
|
||||
pid: 1234,
|
||||
output: [],
|
||||
signal: null
|
||||
} as any);
|
||||
|
||||
const result = getBunPath();
|
||||
expect(result).toBe('bun');
|
||||
expect(spawnSync).toHaveBeenCalledWith('bun', ['--version'], expect.any(Object));
|
||||
});
|
||||
|
||||
it('should check common installation paths when not in PATH', () => {
|
||||
// Mock failed PATH check
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
status: 1,
|
||||
stdout: Buffer.from(''),
|
||||
stderr: Buffer.from(''),
|
||||
pid: 1234,
|
||||
output: [],
|
||||
signal: null
|
||||
} as any);
|
||||
|
||||
// Mock existsSync to return true for ~/.bun/bin/bun
|
||||
vi.mocked(existsSync).mockImplementation((path: any) => {
|
||||
return path.includes('.bun/bin/bun');
|
||||
});
|
||||
|
||||
const result = getBunPath();
|
||||
expect(result).toContain('.bun/bin/bun');
|
||||
});
|
||||
|
||||
it('should return null when bun is not found anywhere', () => {
|
||||
// Mock failed PATH check
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
status: 1,
|
||||
stdout: Buffer.from(''),
|
||||
stderr: Buffer.from(''),
|
||||
pid: 1234,
|
||||
output: [],
|
||||
signal: null
|
||||
} as any);
|
||||
|
||||
// Mock existsSync to always return false
|
||||
vi.mocked(existsSync).mockReturnValue(false);
|
||||
|
||||
const result = getBunPath();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return true for isBunAvailable when bun is found', () => {
|
||||
// Mock successful bun check
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
status: 0,
|
||||
stdout: Buffer.from('1.0.0'),
|
||||
stderr: Buffer.from(''),
|
||||
pid: 1234,
|
||||
output: [],
|
||||
signal: null
|
||||
} as any);
|
||||
|
||||
const result = isBunAvailable();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw error in getBunPathOrThrow when bun not found', () => {
|
||||
// Mock failed bun check
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
status: 1,
|
||||
stdout: Buffer.from(''),
|
||||
stderr: Buffer.from(''),
|
||||
pid: 1234,
|
||||
output: [],
|
||||
signal: null
|
||||
} as any);
|
||||
vi.mocked(existsSync).mockReturnValue(false);
|
||||
|
||||
expect(() => getBunPathOrThrow()).toThrow('Bun is required');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user