fix: address second PR review — clean replace, IDE failure bubbling, bun validation

- 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) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-04-04 14:17:18 -07:00
parent ae6915b88e
commit 190c74492f
7 changed files with 62 additions and 23 deletions
+1 -1
View File
@@ -244,7 +244,7 @@ async function buildHooks() {
console.log(`✓ openclaw plugin built (${(openclawStats.size / 1024).toFixed(2)} KB)`); 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')) { if (fs.existsSync('src/integrations/opencode-plugin/index.ts')) {
console.log(`\n🔧 Building OpenCode plugin...`); console.log(`\n🔧 Building OpenCode plugin...`);
const opencodeOutDir = 'dist/opencode-plugin'; const opencodeOutDir = 'dist/opencode-plugin';
+1 -1
View File
@@ -116,7 +116,7 @@ export function detectInstalledIDEs(): IDEInfo[] {
id: 'cursor', id: 'cursor',
label: 'Cursor', label: 'Cursor',
detected: existsSync(join(home, '.cursor')), detected: existsSync(join(home, '.cursor')),
supported: true, supported: false,
}, },
{ {
id: 'copilot-cli', id: 'copilot-cli',
+46 -16
View File
@@ -10,7 +10,7 @@
import * as p from '@clack/prompts'; import * as p from '@clack/prompts';
import pc from 'picocolors'; import pc from 'picocolors';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { cpSync, existsSync, readFileSync } from 'fs'; import { cpSync, existsSync, readFileSync, rmSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
// Non-TTY detection: @clack/prompts crashes with ENOENT in non-TTY environments // Non-TTY detection: @clack/prompts crashes with ENOENT in non-TTY environments
@@ -113,7 +113,10 @@ function enablePluginInClaudeSettings(): void {
// IDE setup dispatcher // IDE setup dispatcher
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function setupIDEs(selectedIDEs: string[]): Promise<void> { /** Returns a list of IDE IDs that failed setup. */
async function setupIDEs(selectedIDEs: string[]): Promise<string[]> {
const failedIDEs: string[] = [];
for (const ideId of selectedIDEs) { for (const ideId of selectedIDEs) {
switch (ideId) { switch (ideId) {
case 'claude-code': case 'claude-code':
@@ -133,6 +136,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise<void> {
log.success('Gemini CLI: hooks installed.'); log.success('Gemini CLI: hooks installed.');
} else { } else {
log.error('Gemini CLI: hook installation failed.'); log.error('Gemini CLI: hook installation failed.');
failedIDEs.push(ideId);
} }
break; break;
} }
@@ -144,6 +148,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise<void> {
log.success('OpenCode: plugin installed.'); log.success('OpenCode: plugin installed.');
} else { } else {
log.error('OpenCode: plugin installation failed.'); log.error('OpenCode: plugin installation failed.');
failedIDEs.push(ideId);
} }
break; break;
} }
@@ -155,6 +160,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise<void> {
log.success('Windsurf: hooks installed.'); log.success('Windsurf: hooks installed.');
} else { } else {
log.error('Windsurf: hook installation failed.'); log.error('Windsurf: hook installation failed.');
failedIDEs.push(ideId);
} }
break; break;
} }
@@ -166,6 +172,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise<void> {
log.success('OpenClaw: plugin installed.'); log.success('OpenClaw: plugin installed.');
} else { } else {
log.error('OpenClaw: plugin installation failed.'); log.error('OpenClaw: plugin installation failed.');
failedIDEs.push(ideId);
} }
break; break;
} }
@@ -177,6 +184,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise<void> {
log.success('Codex CLI: transcript watching configured.'); log.success('Codex CLI: transcript watching configured.');
} else { } else {
log.error('Codex CLI: integration setup failed.'); log.error('Codex CLI: integration setup failed.');
failedIDEs.push(ideId);
} }
break; break;
} }
@@ -198,6 +206,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise<void> {
log.success(`${ideLabel}: MCP integration installed.`); log.success(`${ideLabel}: MCP integration installed.`);
} else { } else {
log.error(`${ideLabel}: MCP integration failed.`); log.error(`${ideLabel}: MCP integration failed.`);
failedIDEs.push(ideId);
} }
} }
break; break;
@@ -213,6 +222,8 @@ async function setupIDEs(selectedIDEs: string[]): Promise<void> {
} }
} }
} }
return failedIDEs;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -282,6 +293,10 @@ function copyPluginToMarketplace(): void {
const destPath = join(marketplaceDir, entry); const destPath = join(marketplaceDir, entry);
if (!existsSync(sourcePath)) continue; 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, { cpSync(sourcePath, destPath, {
recursive: true, recursive: true,
force: true, force: true,
@@ -293,6 +308,8 @@ function copyPluginToCache(version: string): void {
const sourcePluginDirectory = npmPackagePluginDirectory(); const sourcePluginDirectory = npmPackagePluginDirectory();
const cachePath = pluginCacheDirectory(version); const cachePath = pluginCacheDirectory(version);
// Clean replace: remove stale cache before copying
rmSync(cachePath, { recursive: true, force: true });
ensureDirectoryExists(cachePath); ensureDirectoryExists(cachePath);
cpSync(sourcePluginDirectory, cachePath, { recursive: true, force: true }); cpSync(sourcePluginDirectory, cachePath, { recursive: true, force: true });
} }
@@ -318,12 +335,12 @@ function runNpmInstallInMarketplace(): void {
// Trigger smart-install for Bun / uv // Trigger smart-install for Bun / uv
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function runSmartInstall(): void { function runSmartInstall(): boolean {
const smartInstallPath = join(marketplaceDirectory(), 'plugin', 'scripts', 'smart-install.js'); const smartInstallPath = join(marketplaceDirectory(), 'plugin', 'scripts', 'smart-install.js');
if (!existsSync(smartInstallPath)) { if (!existsSync(smartInstallPath)) {
log.warn('smart-install.js not found — skipping Bun/uv auto-install.'); log.warn('smart-install.js not found — skipping Bun/uv auto-install.');
return; return false;
} }
try { try {
@@ -331,8 +348,10 @@ function runSmartInstall(): void {
stdio: 'inherit', stdio: 'inherit',
...(IS_WINDOWS ? { shell: true as const } : {}), ...(IS_WINDOWS ? { shell: true as const } : {}),
}); });
return true;
} catch { } catch {
log.warn('smart-install encountered an issue. You may need to install Bun/uv manually.'); 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<v
title: 'Setting up Bun and uv', title: 'Setting up Bun and uv',
task: async (message) => { task: async (message) => {
message('Running smart-install...'); message('Running smart-install...');
try { return runSmartInstall()
runSmartInstall(); ? `Runtime dependencies ready ${pc.green('OK')}`
return `Runtime dependencies ready ${pc.green('OK')}`; : `Runtime setup may need attention ${pc.yellow('!')}`;
} catch {
return `Runtime setup may need attention ${pc.yellow('!')}`;
}
}, },
}, },
]); ]);
// IDE-specific setup // IDE-specific setup
await setupIDEs(selectedIDEs); const failedIDEs = await setupIDEs(selectedIDEs);
// Summary // Summary
const installStatus = failedIDEs.length > 0 ? 'Installation Partial' : 'Installation Complete';
const summaryLines = [ const summaryLines = [
`Version: ${pc.cyan(version)}`, `Version: ${pc.cyan(version)}`,
`Plugin dir: ${pc.cyan(marketplaceDir)}`, `Plugin dir: ${pc.cyan(marketplaceDir)}`,
`IDEs: ${pc.cyan(selectedIDEs.join(', '))}`, `IDEs: ${pc.cyan(selectedIDEs.join(', '))}`,
]; ];
if (failedIDEs.length > 0) {
summaryLines.push(`Failed: ${pc.red(failedIDEs.join(', '))}`);
}
if (isInteractive) { if (isInteractive) {
p.note(summaryLines.join('\n'), 'Installation Complete'); p.note(summaryLines.join('\n'), installStatus);
} else { } else {
console.log('\n Installation Complete'); console.log(`\n ${installStatus}`);
summaryLines.forEach(l => console.log(` ${l}`)); summaryLines.forEach(l => console.log(` ${l}`));
} }
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777';
const nextSteps = [ const nextSteps = [
'Open Claude Code and start a conversation -- memory is automatic!', '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`, `Search past work: use ${pc.bold('/mem-search')} in Claude Code`,
`Start worker: ${pc.bold('npx claude-mem start')}`, `Start worker: ${pc.bold('npx claude-mem start')}`,
]; ];
if (isInteractive) { if (isInteractive) {
p.note(nextSteps.join('\n'), 'Next Steps'); 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 { } else {
console.log('\n Next Steps'); console.log('\n Next Steps');
nextSteps.forEach(l => console.log(` ${l}`)); 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!');
}
} }
} }
+1 -1
View File
@@ -285,7 +285,7 @@ export async function installGooseMcpIntegration(): Promise<number> {
if (gooseConfigHasClaudeMemEntry(yamlContent)) { if (gooseConfigHasClaudeMemEntry(yamlContent)) {
// Already configured — replace the claude-mem block // Already configured — replace the claude-mem block
// Find the claude-mem entry and replace it // 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'; const newEntry = buildGooseClaudeMemEntryYaml(mcpServerPath) + '\n';
if (claudeMemPattern.test(yamlContent)) { if (claudeMemPattern.test(yamlContent)) {
@@ -218,12 +218,16 @@ export function uninstallOpenCodePlugin(): number {
'\n' + '\n' +
content.slice(tagEndIndex + CONTEXT_TAG_CLOSE.length).trimStart(); content.slice(tagEndIndex + CONTEXT_TAG_CLOSE.length).trimStart();
// If the file is now essentially empty, don't bother keeping it // If the file is now essentially empty or only has our header, remove it
if (content.trim().length === 0) { const trimmedContent = content.trim();
if (
trimmedContent.length === 0 ||
trimmedContent === '# Claude-Mem Memory Context'
) {
unlinkSync(agentsMdPath); unlinkSync(agentsMdPath);
console.log(` Removed empty AGENTS.md`); console.log(` Removed empty AGENTS.md`);
} else { } else {
writeFileSync(agentsMdPath, content.trimEnd() + '\n', 'utf-8'); writeFileSync(agentsMdPath, trimmedContent + '\n', 'utf-8');
console.log(` Cleaned context from AGENTS.md`); console.log(` Cleaned context from AGENTS.md`);
} }
} }
@@ -278,6 +278,11 @@ export async function installWindsurfHooks(): Promise<number> {
// Find bun executable — required because worker-service.cjs uses bun:sqlite // Find bun executable — required because worker-service.cjs uses bun:sqlite
const bunPath = findBunPath(); 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 // IMPORTANT: Tilde expansion is NOT supported in working_directory — use absolute paths
const workingDirectory = path.dirname(workerServicePath); const workingDirectory = path.dirname(workerServicePath);
+1 -1
View File
@@ -91,7 +91,7 @@ describe('Install Non-TTY Support', () => {
}); });
it('uses console.log for note/summary in non-interactive mode', () => { 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}`)");
}); });
}); });