From 190c74492f0448e5aae12e4a1140ba156f052ac8 Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Sat, 4 Apr 2026 14:17:18 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20address=20second=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20clean=20replace,=20IDE=20failure=20bubbling,=20bun?= =?UTF-8?q?=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cpSync now does rmSync before copy to avoid stale file merges - setupIDEs() returns failed IDE list; install reports partial success - runSmartInstall() returns boolean status instead of void - Worker port in next-steps URL reads CLAUDE_MEM_WORKER_PORT env var - Goose YAML regex stops at column-0 keys (prevents eating sibling sections) - AGENTS.md uninstall removes header-only stub files - findBunPath() validated before use in WindsurfHooksInstaller - Cursor marked unsupported in ide-detection until installer is wired Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/build-hooks.js | 2 +- src/npx-cli/commands/ide-detection.ts | 2 +- src/npx-cli/commands/install.ts | 62 ++++++++++++++----- src/services/integrations/McpIntegrations.ts | 2 +- .../integrations/OpenCodeInstaller.ts | 10 ++- .../integrations/WindsurfHooksInstaller.ts | 5 ++ tests/install-non-tty.test.ts | 2 +- 7 files changed, 62 insertions(+), 23 deletions(-) diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index 346457eb..cb175627 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -244,7 +244,7 @@ async function buildHooks() { console.log(`āœ“ openclaw plugin built (${(openclawStats.size / 1024).toFixed(2)} KB)`); } - // Build OpenCode plugin (self-contained, runs in Bun) + // Build OpenCode plugin (self-contained, Node.js ESM — Bun-compatible) if (fs.existsSync('src/integrations/opencode-plugin/index.ts')) { console.log(`\nšŸ”§ Building OpenCode plugin...`); const opencodeOutDir = 'dist/opencode-plugin'; diff --git a/src/npx-cli/commands/ide-detection.ts b/src/npx-cli/commands/ide-detection.ts index a97c21c1..7019be9a 100644 --- a/src/npx-cli/commands/ide-detection.ts +++ b/src/npx-cli/commands/ide-detection.ts @@ -116,7 +116,7 @@ export function detectInstalledIDEs(): IDEInfo[] { id: 'cursor', label: 'Cursor', detected: existsSync(join(home, '.cursor')), - supported: true, + supported: false, }, { id: 'copilot-cli', diff --git a/src/npx-cli/commands/install.ts b/src/npx-cli/commands/install.ts index 407bde08..640df71b 100644 --- a/src/npx-cli/commands/install.ts +++ b/src/npx-cli/commands/install.ts @@ -10,7 +10,7 @@ import * as p from '@clack/prompts'; import pc from 'picocolors'; import { execSync } from 'child_process'; -import { cpSync, existsSync, readFileSync } from 'fs'; +import { cpSync, existsSync, readFileSync, rmSync } from 'fs'; import { join } from 'path'; // Non-TTY detection: @clack/prompts crashes with ENOENT in non-TTY environments @@ -113,7 +113,10 @@ function enablePluginInClaudeSettings(): void { // IDE setup dispatcher // --------------------------------------------------------------------------- -async function setupIDEs(selectedIDEs: string[]): Promise { +/** Returns a list of IDE IDs that failed setup. */ +async function setupIDEs(selectedIDEs: string[]): Promise { + const failedIDEs: string[] = []; + for (const ideId of selectedIDEs) { switch (ideId) { case 'claude-code': @@ -133,6 +136,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise { log.success('Gemini CLI: hooks installed.'); } else { log.error('Gemini CLI: hook installation failed.'); + failedIDEs.push(ideId); } break; } @@ -144,6 +148,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise { log.success('OpenCode: plugin installed.'); } else { log.error('OpenCode: plugin installation failed.'); + failedIDEs.push(ideId); } break; } @@ -155,6 +160,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise { log.success('Windsurf: hooks installed.'); } else { log.error('Windsurf: hook installation failed.'); + failedIDEs.push(ideId); } break; } @@ -166,6 +172,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise { log.success('OpenClaw: plugin installed.'); } else { log.error('OpenClaw: plugin installation failed.'); + failedIDEs.push(ideId); } break; } @@ -177,6 +184,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise { log.success('Codex CLI: transcript watching configured.'); } else { log.error('Codex CLI: integration setup failed.'); + failedIDEs.push(ideId); } break; } @@ -198,6 +206,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise { log.success(`${ideLabel}: MCP integration installed.`); } else { log.error(`${ideLabel}: MCP integration failed.`); + failedIDEs.push(ideId); } } break; @@ -213,6 +222,8 @@ async function setupIDEs(selectedIDEs: string[]): Promise { } } } + + return failedIDEs; } // --------------------------------------------------------------------------- @@ -282,6 +293,10 @@ function copyPluginToMarketplace(): void { const destPath = join(marketplaceDir, entry); if (!existsSync(sourcePath)) continue; + // Clean replace: remove stale files from previous installs before copying + if (existsSync(destPath)) { + rmSync(destPath, { recursive: true, force: true }); + } cpSync(sourcePath, destPath, { recursive: true, force: true, @@ -293,6 +308,8 @@ function copyPluginToCache(version: string): void { const sourcePluginDirectory = npmPackagePluginDirectory(); const cachePath = pluginCacheDirectory(version); + // Clean replace: remove stale cache before copying + rmSync(cachePath, { recursive: true, force: true }); ensureDirectoryExists(cachePath); cpSync(sourcePluginDirectory, cachePath, { recursive: true, force: true }); } @@ -318,12 +335,12 @@ function runNpmInstallInMarketplace(): void { // Trigger smart-install for Bun / uv // --------------------------------------------------------------------------- -function runSmartInstall(): void { +function runSmartInstall(): boolean { const smartInstallPath = join(marketplaceDirectory(), 'plugin', 'scripts', 'smart-install.js'); if (!existsSync(smartInstallPath)) { log.warn('smart-install.js not found — skipping Bun/uv auto-install.'); - return; + return false; } try { @@ -331,8 +348,10 @@ function runSmartInstall(): void { stdio: 'inherit', ...(IS_WINDOWS ? { shell: true as const } : {}), }); + return true; } catch { log.warn('smart-install encountered an issue. You may need to install Bun/uv manually.'); + return false; } } @@ -461,46 +480,57 @@ export async function runInstallCommand(options: InstallOptions = {}): Promise { message('Running smart-install...'); - try { - runSmartInstall(); - return `Runtime dependencies ready ${pc.green('OK')}`; - } catch { - return `Runtime setup may need attention ${pc.yellow('!')}`; - } + return runSmartInstall() + ? `Runtime dependencies ready ${pc.green('OK')}` + : `Runtime setup may need attention ${pc.yellow('!')}`; }, }, ]); // IDE-specific setup - await setupIDEs(selectedIDEs); + const failedIDEs = await setupIDEs(selectedIDEs); // Summary + const installStatus = failedIDEs.length > 0 ? 'Installation Partial' : 'Installation Complete'; const summaryLines = [ `Version: ${pc.cyan(version)}`, `Plugin dir: ${pc.cyan(marketplaceDir)}`, `IDEs: ${pc.cyan(selectedIDEs.join(', '))}`, ]; + if (failedIDEs.length > 0) { + summaryLines.push(`Failed: ${pc.red(failedIDEs.join(', '))}`); + } if (isInteractive) { - p.note(summaryLines.join('\n'), 'Installation Complete'); + p.note(summaryLines.join('\n'), installStatus); } else { - console.log('\n Installation Complete'); + console.log(`\n ${installStatus}`); summaryLines.forEach(l => console.log(` ${l}`)); } + const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777'; const nextSteps = [ 'Open Claude Code and start a conversation -- memory is automatic!', - `View your memories: ${pc.underline('http://localhost:37777')}`, + `View your memories: ${pc.underline(`http://localhost:${workerPort}`)}`, `Search past work: use ${pc.bold('/mem-search')} in Claude Code`, `Start worker: ${pc.bold('npx claude-mem start')}`, ]; if (isInteractive) { p.note(nextSteps.join('\n'), 'Next Steps'); - p.outro(pc.green('claude-mem installed successfully!')); + if (failedIDEs.length > 0) { + p.outro(pc.yellow('claude-mem installed with some IDE setup failures.')); + } else { + p.outro(pc.green('claude-mem installed successfully!')); + } } else { console.log('\n Next Steps'); nextSteps.forEach(l => console.log(` ${l}`)); - console.log('\nclaude-mem installed successfully!'); + if (failedIDEs.length > 0) { + console.log('\nclaude-mem installed with some IDE setup failures.'); + process.exitCode = 1; + } else { + console.log('\nclaude-mem installed successfully!'); + } } } diff --git a/src/services/integrations/McpIntegrations.ts b/src/services/integrations/McpIntegrations.ts index 6dd55406..88aa2c27 100644 --- a/src/services/integrations/McpIntegrations.ts +++ b/src/services/integrations/McpIntegrations.ts @@ -285,7 +285,7 @@ export async function installGooseMcpIntegration(): Promise { if (gooseConfigHasClaudeMemEntry(yamlContent)) { // Already configured — replace the claude-mem block // Find the claude-mem entry and replace it - const claudeMemPattern = /( {2}claude-mem:\n(?:.*\n)*?(?= {2}\S|\n\n|$))/; + const claudeMemPattern = /( {2}claude-mem:\n(?:.*\n)*?(?= {2}\S|\n\n|^\S|$))/m; const newEntry = buildGooseClaudeMemEntryYaml(mcpServerPath) + '\n'; if (claudeMemPattern.test(yamlContent)) { diff --git a/src/services/integrations/OpenCodeInstaller.ts b/src/services/integrations/OpenCodeInstaller.ts index 319d3638..62d9410c 100644 --- a/src/services/integrations/OpenCodeInstaller.ts +++ b/src/services/integrations/OpenCodeInstaller.ts @@ -218,12 +218,16 @@ export function uninstallOpenCodePlugin(): number { '\n' + content.slice(tagEndIndex + CONTEXT_TAG_CLOSE.length).trimStart(); - // If the file is now essentially empty, don't bother keeping it - if (content.trim().length === 0) { + // If the file is now essentially empty or only has our header, remove it + const trimmedContent = content.trim(); + if ( + trimmedContent.length === 0 || + trimmedContent === '# Claude-Mem Memory Context' + ) { unlinkSync(agentsMdPath); console.log(` Removed empty AGENTS.md`); } else { - writeFileSync(agentsMdPath, content.trimEnd() + '\n', 'utf-8'); + writeFileSync(agentsMdPath, trimmedContent + '\n', 'utf-8'); console.log(` Cleaned context from AGENTS.md`); } } diff --git a/src/services/integrations/WindsurfHooksInstaller.ts b/src/services/integrations/WindsurfHooksInstaller.ts index a9514eb0..d40b8bfd 100644 --- a/src/services/integrations/WindsurfHooksInstaller.ts +++ b/src/services/integrations/WindsurfHooksInstaller.ts @@ -278,6 +278,11 @@ export async function installWindsurfHooks(): Promise { // Find bun executable — required because worker-service.cjs uses bun:sqlite const bunPath = findBunPath(); + if (!bunPath) { + console.error('Could not find Bun runtime'); + console.error(' Install Bun: curl -fsSL https://bun.sh/install | bash'); + return 1; + } // IMPORTANT: Tilde expansion is NOT supported in working_directory — use absolute paths const workingDirectory = path.dirname(workerServicePath); diff --git a/tests/install-non-tty.test.ts b/tests/install-non-tty.test.ts index 34ad1942..fdaca866 100644 --- a/tests/install-non-tty.test.ts +++ b/tests/install-non-tty.test.ts @@ -91,7 +91,7 @@ describe('Install Non-TTY Support', () => { }); it('uses console.log for note/summary in non-interactive mode', () => { - expect(installSource).toContain("console.log('\\n Installation Complete')"); + expect(installSource).toContain("console.log(`\\n ${installStatus}`)"); }); });