fix(windows): Comprehensive fixes for Windows plugin installation

This PR addresses issue #193 affecting Windows installations of claude-mem.

## Bug 1: Missing ecosystem.config.cjs in packaged plugin

**Problem**: The ecosystem.config.cjs file was not included in the plugin
package, causing PM2 to fail when trying to start the worker from cache.

**Fix**: Added `plugin/ecosystem.config.cjs` with correct path for packaged
structure (`./scripts/worker-service.cjs` instead of `./plugin/scripts/`).

## Bug 2: Incorrect MCP Server Path (src/services/worker-service.ts)

**Problem**: Path `__dirname, '..', '..', 'plugin', 'scripts', 'mcp-server.cjs'`
only worked in dev structure, failed in packaged plugin.

**Error produced**:
```
Error: Cannot find module 'C:\Users\...\claude-mem\plugin\scripts\mcp-server.cjs'
[ERROR] [SYSTEM] Background initialization failed MCP error -32000: Connection closed
```

**Fix**: Changed to `path.join(__dirname, 'mcp-server.cjs')` since mcp-server.cjs
is in the same directory as worker-service.cjs after bundling.

## Bug 3: Missing smart-install.js in plugin package

**Problem**: smart-install.js was referenced in hooks.json but not included
in the plugin/ directory for cache deployment.

**Fix**: Added `plugin/scripts/smart-install.js` that uses `createRequire()`
to resolve modules from MARKETPLACE_ROOT.

## Bug 4: hooks.json incorrect path

**Problem**: Referenced `/../scripts/smart-install.js` but CLAUDE_PLUGIN_ROOT
points to the plugin/ directory.

**Fix**: Changed to `/scripts/smart-install.js`.

## Bug 5: Windows Worker Startup - Visible Console Windows

**Problem**: PM2 ignores windowsHide option on Windows, opening visible
console windows when starting the worker service.

**Fix**: Use PowerShell `Start-Process -WindowStyle Hidden` on Windows while
keeping PM2 for Unix systems (src/shared/worker-utils.ts).

## Additional Improvements

- Increased worker startup timeouts for Windows (500ms health check, 1000ms
  wait between retries, 15 retries = 15s total vs previous 5s)
- Added `windowsHide: true` to root ecosystem.config.cjs for PM2

## Note on Assertion Failure

The Windows libuv assertion failure `!(handle->flags & UV_HANDLE_CLOSING)`
at `src\win\async.c:76` is a known upstream issue in Claude Code (Issue #7579),
triggered by fetch() calls on Windows. This is NOT caused by worker spawning
and cannot be fixed in claude-mem.

Tested on Windows 11 with Node.js v24.

Fixes #193

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kat-bell
2025-12-09 05:00:44 -06:00
parent 679a077f9b
commit 1f2e5f1a9c
14 changed files with 543 additions and 107 deletions
+1 -1
View File
@@ -157,7 +157,7 @@ export class WorkerService {
logger.info('WORKER', 'SearchManager initialized and search routes registered');
// Connect to MCP server
const mcpServerPath = path.join(__dirname, '..', '..', 'plugin', 'scripts', 'mcp-server.cjs');
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
const transport = new StdioClientTransport({
command: 'node',
args: [mcpServerPath],
+47 -25
View File
@@ -9,9 +9,10 @@ import { SettingsDefaultsManager } from "../services/worker/settings/SettingsDef
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Named constants for health checks
const HEALTH_CHECK_TIMEOUT_MS = 100;
const WORKER_STARTUP_WAIT_MS = 500;
const WORKER_STARTUP_RETRIES = 10;
// Windows needs longer timeouts due to startup overhead
const HEALTH_CHECK_TIMEOUT_MS = 500;
const WORKER_STARTUP_WAIT_MS = 1000;
const WORKER_STARTUP_RETRIES = 15;
/**
* Get the worker port number
@@ -39,35 +40,56 @@ async function isWorkerHealthy(): Promise<boolean> {
}
/**
* Start the worker using PM2
* Start the worker service
* On Windows: Uses PowerShell Start-Process with hidden window to avoid console flash
* On Unix: Uses PM2 for process management
*/
async function startWorker(): Promise<boolean> {
try {
// CRITICAL: Always use marketplace directory for ecosystem.config.cjs
// This ensures PM2 starts from the correct location regardless of where hooks run from
const ecosystemPath = path.join(MARKETPLACE_ROOT, 'ecosystem.config.cjs');
const workerScript = path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs');
if (!existsSync(ecosystemPath)) {
throw new Error(`Ecosystem config not found at ${ecosystemPath}`);
if (!existsSync(workerScript)) {
throw new Error(`Worker script not found at ${workerScript}`);
}
// Try to use local PM2 from node_modules first, fall back to global PM2
// On Windows, PM2 executable is pm2.cmd, not pm2
const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2');
const localPm2Cmd = process.platform === 'win32' ? localPm2Base + '.cmd' : localPm2Base;
const pm2Command = existsSync(localPm2Cmd) ? localPm2Cmd : 'pm2';
if (process.platform === 'win32') {
// On Windows, use PowerShell Start-Process with -WindowStyle Hidden
// This avoids visible console windows that PM2 creates on Windows
const result = spawnSync('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-Command',
`Start-Process -FilePath 'node' -ArgumentList '${workerScript}' -WorkingDirectory '${MARKETPLACE_ROOT}' -WindowStyle Hidden`
], {
cwd: MARKETPLACE_ROOT,
stdio: 'pipe',
encoding: 'utf-8',
windowsHide: true
});
// Start using PM2 with the ecosystem config
// CRITICAL: Must set cwd to MARKETPLACE_ROOT so PM2 starts from marketplace directory
// Using spawnSync with array args to avoid command injection risks
const result = spawnSync(pm2Command, ['start', ecosystemPath], {
cwd: MARKETPLACE_ROOT,
stdio: 'pipe',
encoding: 'utf-8',
windowsHide: true
});
if (result.status !== 0) {
throw new Error(result.stderr || 'PM2 start failed');
if (result.status !== 0) {
throw new Error(result.stderr || 'PowerShell Start-Process failed');
}
} else {
// On Unix, use PM2 for process management
const ecosystemPath = path.join(MARKETPLACE_ROOT, 'ecosystem.config.cjs');
if (!existsSync(ecosystemPath)) {
throw new Error(`Ecosystem config not found at ${ecosystemPath}`);
}
const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2');
const pm2Command = existsSync(localPm2Base) ? localPm2Base : 'pm2';
const result = spawnSync(pm2Command, ['start', ecosystemPath], {
cwd: MARKETPLACE_ROOT,
stdio: 'pipe',
encoding: 'utf-8'
});
if (result.status !== 0) {
throw new Error(result.stderr || 'PM2 start failed');
}
}
// Wait for worker to become healthy