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)`);
}
// 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';
+1 -1
View File
@@ -116,7 +116,7 @@ export function detectInstalledIDEs(): IDEInfo[] {
id: 'cursor',
label: 'Cursor',
detected: existsSync(join(home, '.cursor')),
supported: true,
supported: false,
},
{
id: 'copilot-cli',
+46 -16
View File
@@ -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<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) {
switch (ideId) {
case 'claude-code':
@@ -133,6 +136,7 @@ async function setupIDEs(selectedIDEs: string[]): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
}
}
}
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<v
title: 'Setting up Bun and uv',
task: async (message) => {
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!');
}
}
}
+1 -1
View File
@@ -285,7 +285,7 @@ export async function installGooseMcpIntegration(): Promise<number> {
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)) {
@@ -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`);
}
}
@@ -278,6 +278,11 @@ export async function installWindsurfHooks(): Promise<number> {
// 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);
+1 -1
View File
@@ -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}`)");
});
});