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:
@@ -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';
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user