From 74b3d68d76d6522ad684706c1242ceca7dfaa1d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:28:11 +0000 Subject: [PATCH 1/3] Initial plan From ab0cbda74655ad4e8ae002a3380491d18dbc560a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:34:53 +0000 Subject: [PATCH 2/3] fix: use stable marketplace path for claude-mem alias and update stale aliases Fixes two compound defects in installCLI(): Defect A: The alias was composed from ROOT which resolves to a version-pinned cache directory (e.g. .../cache/.../10.5.2/...). Now uses the stable marketplace path with $HOME for portability. Defect B: The includes() guard prevented repair of stale aliases. Now uses regex-based detection that can update stale/version-pinned aliases to the canonical form on every installer run. Also fixes the analogous Windows PowerShell branch and removes the now-unnecessary .cli-installed marker early-return. Agent-Logs-Url: https://github.com/thedotmack/claude-mem/sessions/e08b65f5-8940-41ad-b10c-fb568de0e873 Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> --- plugin/scripts/smart-install.js | 55 ++++++++++----- tests/smart-install.test.ts | 121 ++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 19 deletions(-) diff --git a/plugin/scripts/smart-install.js b/plugin/scripts/smart-install.js index 4d2f1a3767..1cd408f861 100644 --- a/plugin/scripts/smart-install.js +++ b/plugin/scripts/smart-install.js @@ -343,54 +343,71 @@ function installUv() { * Add shell alias for claude-mem command */ function installCLI() { - const WORKER_CLI = join(ROOT, 'scripts', 'worker-service.cjs'); const bunPath = getBunPath() || 'bun'; - const aliasLine = `alias claude-mem='${bunPath} "${WORKER_CLI}"'`; - const markerPath = join(ROOT, '.cli-installed'); - // Skip if already installed - if (existsSync(markerPath)) return; + // Use the stable marketplace path so the alias survives plugin upgrades. + // $HOME is expanded at shell invocation time, keeping the line portable. + const STABLE_WORKER_PATH = '$HOME/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs'; + const aliasLine = `alias claude-mem='${bunPath} "${STABLE_WORKER_PATH}"'`; + + // Regex anchored to line start (multiline) — ignores commented-out lines. + const ALIAS_RE = /^alias claude-mem=.*$/m; try { if (IS_WINDOWS) { - // Windows: Add to PATH via PowerShell profile + // Windows: Add/update PowerShell function in profile const profilePath = join(process.env.USERPROFILE || homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'); const profileDir = join(process.env.USERPROFILE || homedir(), 'Documents', 'PowerShell'); - const functionDef = `function claude-mem { & "${bunPath}" "${WORKER_CLI}" $args }\n`; + const STABLE_WORKER_PATH_WIN = '$HOME\\.claude\\plugins\\marketplaces\\thedotmack\\plugin\\scripts\\worker-service.cjs'; + const functionDef = `function claude-mem { & "${bunPath}" "${STABLE_WORKER_PATH_WIN}" $args }`; + const PS_FUNC_RE = /^function claude-mem \{[^}]*\}$/m; if (!existsSync(profileDir)) { execSync(`mkdir "${profileDir}"`, { stdio: 'ignore', shell: true }); } const existingContent = existsSync(profilePath) ? readFileSync(profilePath, 'utf-8') : ''; - if (!existingContent.includes('function claude-mem')) { - writeFileSync(profilePath, existingContent + '\n' + functionDef); + const psMatch = existingContent.match(PS_FUNC_RE); + + if (!psMatch) { + writeFileSync(profilePath, existingContent.trimEnd() + '\n' + functionDef + '\n'); console.error(`✅ PowerShell function added to profile`); console.error(' Restart your terminal to use: claude-mem '); + } else if (psMatch[0] !== functionDef) { + writeFileSync(profilePath, existingContent.replace(PS_FUNC_RE, functionDef)); + console.error(`✅ PowerShell function updated in profile`); } } else { - // Unix: Add alias to shell configs + // Unix: Add/update alias in shell configs const shellConfigs = [ join(homedir(), '.bashrc'), join(homedir(), '.zshrc') ]; for (const config of shellConfigs) { - if (existsSync(config)) { - const content = readFileSync(config, 'utf-8'); - if (!content.includes('alias claude-mem=')) { - writeFileSync(config, content + '\n' + aliasLine + '\n'); - console.error(`✅ Alias added to ${config}`); - } + if (!existsSync(config)) continue; + let content = readFileSync(config, 'utf-8'); + const match = content.match(ALIAS_RE); + + if (!match) { + // Not present — append it. + writeFileSync(config, content.trimEnd() + '\n' + aliasLine + '\n'); + console.error(`✅ Alias added to ${config}`); + continue; } + + if (match[0] === aliasLine) continue; // already canonical + + // Replace existing (potentially stale) alias line with the canonical one. + content = content.replace(ALIAS_RE, aliasLine); + writeFileSync(config, content); + console.error(`✅ Alias updated in ${config}`); } console.error(' Restart your terminal to use: claude-mem '); } - - writeFileSync(markerPath, new Date().toISOString()); } catch (error) { console.error(`⚠️ Could not add shell alias: ${error.message}`); - console.error(` Use directly: ${bunPath} "${WORKER_CLI}" `); + console.error(` Use directly: ${bunPath} "${STABLE_WORKER_PATH}" `); } } diff --git a/tests/smart-install.test.ts b/tests/smart-install.test.ts index d702e66cb8..47092c5811 100644 --- a/tests/smart-install.test.ts +++ b/tests/smart-install.test.ts @@ -239,6 +239,127 @@ describe('smart-install stdout JSON output (#1253)', () => { }); }); +/** + * Tests for installCLI() alias stability (version-pinning fix). + * + * installCLI() is not exported, so these tests verify the source-level + * patterns (stable path, regex guard, update logic) and the runtime + * behaviour via a helper that simulates the alias write/update cycle. + */ +describe('smart-install installCLI alias stability', () => { + const SCRIPT_PATH = join(__dirname, '..', 'plugin', 'scripts', 'smart-install.js'); + + it('should use the stable marketplace path instead of ROOT for the alias', () => { + const content = readFileSync(SCRIPT_PATH, 'utf-8'); + // The alias must point to the stable marketplace path, not a version-pinned ROOT + expect(content).toContain( + "const STABLE_WORKER_PATH = '$HOME/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs'" + ); + // The alias line should be composed from STABLE_WORKER_PATH, not WORKER_CLI/ROOT + expect(content).toContain("const aliasLine = `alias claude-mem='${bunPath} \"${STABLE_WORKER_PATH}\"'`"); + }); + + it('should not compose the alias from ROOT (which is version-pinned)', () => { + const content = readFileSync(SCRIPT_PATH, 'utf-8'); + // Extract just the installCLI function body + const funcStart = content.indexOf('function installCLI()'); + const funcEnd = content.indexOf('\n/**', funcStart + 1); + const funcBody = content.slice(funcStart, funcEnd); + // Should not use ROOT-derived WORKER_CLI for the alias + expect(funcBody).not.toContain("join(ROOT, 'scripts', 'worker-service.cjs')"); + }); + + it('should use a regex guard instead of simple includes() for alias detection', () => { + const content = readFileSync(SCRIPT_PATH, 'utf-8'); + const funcStart = content.indexOf('function installCLI()'); + const funcEnd = content.indexOf('\n/**', funcStart + 1); + const funcBody = content.slice(funcStart, funcEnd); + // Should use regex, not includes('alias claude-mem=') + expect(funcBody).not.toContain("content.includes('alias claude-mem=')"); + expect(funcBody).toContain('ALIAS_RE'); + expect(funcBody).toContain('content.match(ALIAS_RE)'); + }); + + it('should have logic to update stale aliases, not just skip them', () => { + const content = readFileSync(SCRIPT_PATH, 'utf-8'); + const funcStart = content.indexOf('function installCLI()'); + const funcEnd = content.indexOf('\n/**', funcStart + 1); + const funcBody = content.slice(funcStart, funcEnd); + // Should contain update logic (replace + log message) + expect(funcBody).toContain('content.replace(ALIAS_RE, aliasLine)'); + expect(funcBody).toContain('Alias updated in'); + }); + + it('should not use .cli-installed marker for early return', () => { + const content = readFileSync(SCRIPT_PATH, 'utf-8'); + const funcStart = content.indexOf('function installCLI()'); + const funcEnd = content.indexOf('\n/**', funcStart + 1); + const funcBody = content.slice(funcStart, funcEnd); + expect(funcBody).not.toContain('.cli-installed'); + }); + + // --- Runtime simulation tests --- + // These simulate the regex-based alias write/update logic + // without calling installCLI() directly. + + const ALIAS_RE = /^alias claude-mem=.*$/m; + + it('should add alias when none exists', () => { + const canonical = `alias claude-mem='bun "$HOME/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs"'`; + const shellContent = '# some existing config\nexport PATH="/usr/bin:$PATH"\n'; + + const match = shellContent.match(ALIAS_RE); + expect(match).toBeNull(); + + // Simulates the append path + const updated = shellContent.trimEnd() + '\n' + canonical + '\n'; + expect(updated).toContain(canonical); + }); + + it('should not modify config when alias is already canonical', () => { + const canonical = `alias claude-mem='bun "$HOME/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs"'`; + const shellContent = `# config\n${canonical}\nexport FOO=bar\n`; + + const match = shellContent.match(ALIAS_RE); + expect(match).not.toBeNull(); + expect(match![0]).toBe(canonical); + // No update needed + }); + + it('should replace a stale version-pinned alias with the canonical one', () => { + const canonical = `alias claude-mem='bun "$HOME/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs"'`; + const staleAlias = `alias claude-mem='bun "/Users/someone/.claude/plugins/cache/thedotmack/claude-mem/10.5.2/scripts/worker-service.cjs"'`; + const shellContent = `# config\n${staleAlias}\nexport FOO=bar\n`; + + const match = shellContent.match(ALIAS_RE); + expect(match).not.toBeNull(); + expect(match![0]).not.toBe(canonical); + + const updated = shellContent.replace(ALIAS_RE, canonical); + expect(updated).toContain(canonical); + expect(updated).not.toContain('10.5.2'); + expect(updated).toContain('export FOO=bar'); // other content preserved + }); + + it('should not match commented-out alias lines', () => { + const commentedAlias = `# alias claude-mem='bun "/old/path/worker-service.cjs"'`; + const shellContent = `# config\n${commentedAlias}\nexport FOO=bar\n`; + + const match = shellContent.match(ALIAS_RE); + expect(match).toBeNull(); + // Commented line starts with '#', not 'alias', so regex doesn't match + }); + + it('should use $HOME literal in the alias for portability', () => { + const content = readFileSync(SCRIPT_PATH, 'utf-8'); + const funcStart = content.indexOf('function installCLI()'); + const funcEnd = content.indexOf('\n/**', funcStart + 1); + const funcBody = content.slice(funcStart, funcEnd); + // Alias path should use $HOME, not homedir() or an absolute path + expect(funcBody).toContain("'$HOME/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs'"); + }); +}); + /** * Tests for checkBinaryPlatformCompatibility() (#1547). * From 205fa8d28814e986c191b28c87197e62c85183d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:37:10 +0000 Subject: [PATCH 3/3] refactor: extract getInstallCLIBody helper in tests to reduce duplication Agent-Logs-Url: https://github.com/thedotmack/claude-mem/sessions/e08b65f5-8940-41ad-b10c-fb568de0e873 Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> --- tests/smart-install.test.ts | 48 +++++++++++++------------------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/tests/smart-install.test.ts b/tests/smart-install.test.ts index 47092c5811..7126dba2bc 100644 --- a/tests/smart-install.test.ts +++ b/tests/smart-install.test.ts @@ -249,6 +249,14 @@ describe('smart-install stdout JSON output (#1253)', () => { describe('smart-install installCLI alias stability', () => { const SCRIPT_PATH = join(__dirname, '..', 'plugin', 'scripts', 'smart-install.js'); + /** Extract the installCLI() function body from the source file. */ + function getInstallCLIBody(): string { + const content = readFileSync(SCRIPT_PATH, 'utf-8'); + const funcStart = content.indexOf('function installCLI()'); + const funcEnd = content.indexOf('\n/**', funcStart + 1); + return content.slice(funcStart, funcEnd); + } + it('should use the stable marketplace path instead of ROOT for the alias', () => { const content = readFileSync(SCRIPT_PATH, 'utf-8'); // The alias must point to the stable marketplace path, not a version-pinned ROOT @@ -260,44 +268,33 @@ describe('smart-install installCLI alias stability', () => { }); it('should not compose the alias from ROOT (which is version-pinned)', () => { - const content = readFileSync(SCRIPT_PATH, 'utf-8'); - // Extract just the installCLI function body - const funcStart = content.indexOf('function installCLI()'); - const funcEnd = content.indexOf('\n/**', funcStart + 1); - const funcBody = content.slice(funcStart, funcEnd); - // Should not use ROOT-derived WORKER_CLI for the alias + const funcBody = getInstallCLIBody(); expect(funcBody).not.toContain("join(ROOT, 'scripts', 'worker-service.cjs')"); }); it('should use a regex guard instead of simple includes() for alias detection', () => { - const content = readFileSync(SCRIPT_PATH, 'utf-8'); - const funcStart = content.indexOf('function installCLI()'); - const funcEnd = content.indexOf('\n/**', funcStart + 1); - const funcBody = content.slice(funcStart, funcEnd); - // Should use regex, not includes('alias claude-mem=') + const funcBody = getInstallCLIBody(); expect(funcBody).not.toContain("content.includes('alias claude-mem=')"); expect(funcBody).toContain('ALIAS_RE'); expect(funcBody).toContain('content.match(ALIAS_RE)'); }); it('should have logic to update stale aliases, not just skip them', () => { - const content = readFileSync(SCRIPT_PATH, 'utf-8'); - const funcStart = content.indexOf('function installCLI()'); - const funcEnd = content.indexOf('\n/**', funcStart + 1); - const funcBody = content.slice(funcStart, funcEnd); - // Should contain update logic (replace + log message) + const funcBody = getInstallCLIBody(); expect(funcBody).toContain('content.replace(ALIAS_RE, aliasLine)'); expect(funcBody).toContain('Alias updated in'); }); it('should not use .cli-installed marker for early return', () => { - const content = readFileSync(SCRIPT_PATH, 'utf-8'); - const funcStart = content.indexOf('function installCLI()'); - const funcEnd = content.indexOf('\n/**', funcStart + 1); - const funcBody = content.slice(funcStart, funcEnd); + const funcBody = getInstallCLIBody(); expect(funcBody).not.toContain('.cli-installed'); }); + it('should use $HOME literal in the alias for portability', () => { + const funcBody = getInstallCLIBody(); + expect(funcBody).toContain("'$HOME/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs'"); + }); + // --- Runtime simulation tests --- // These simulate the regex-based alias write/update logic // without calling installCLI() directly. @@ -323,7 +320,6 @@ describe('smart-install installCLI alias stability', () => { const match = shellContent.match(ALIAS_RE); expect(match).not.toBeNull(); expect(match![0]).toBe(canonical); - // No update needed }); it('should replace a stale version-pinned alias with the canonical one', () => { @@ -347,16 +343,6 @@ describe('smart-install installCLI alias stability', () => { const match = shellContent.match(ALIAS_RE); expect(match).toBeNull(); - // Commented line starts with '#', not 'alias', so regex doesn't match - }); - - it('should use $HOME literal in the alias for portability', () => { - const content = readFileSync(SCRIPT_PATH, 'utf-8'); - const funcStart = content.indexOf('function installCLI()'); - const funcEnd = content.indexOf('\n/**', funcStart + 1); - const funcBody = content.slice(funcStart, funcEnd); - // Alias path should use $HOME, not homedir() or an absolute path - expect(funcBody).toContain("'$HOME/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs'"); }); });