Skip to content

Commit 6a4ec50

Browse files
committed
fix: ensure npm installation for Claude Code and handle already-installed plugins
1 parent 157d808 commit 6a4ec50

File tree

2 files changed

+44
-6
lines changed

2 files changed

+44
-6
lines changed

installer/steps/dependencies.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,22 @@ def install_python_tools() -> bool:
9999
return False
100100

101101

102+
def _is_claude_installed_via_npm() -> bool:
103+
"""Check if Claude Code is installed via npm (required for LSP fix)."""
104+
try:
105+
result = subprocess.run(
106+
["npm", "list", "-g", "@anthropic-ai/claude-code", "--depth=0"],
107+
capture_output=True,
108+
text=True,
109+
)
110+
return "@anthropic-ai/claude-code" in result.stdout
111+
except Exception:
112+
return False
113+
114+
102115
def install_claude_code() -> bool:
103-
"""Install Claude Code CLI via npm."""
104-
if command_exists("claude"):
116+
"""Install Claude Code CLI via npm (required for LSP fix to work)."""
117+
if _is_claude_installed_via_npm():
105118
return True
106119

107120
return _run_bash_with_retry("npm install -g @anthropic-ai/claude-code")
@@ -202,7 +215,15 @@ def install_pyright_lsp() -> bool:
202215

203216
def install_claude_mem() -> bool:
204217
"""Install claude-mem plugin via claude plugin marketplace."""
205-
if not _run_bash_with_retry("claude plugin marketplace add thedotmack/claude-mem"):
218+
try:
219+
result = subprocess.run(
220+
["bash", "-c", "claude plugin marketplace add thedotmack/claude-mem"],
221+
capture_output=True,
222+
text=True,
223+
)
224+
if result.returncode != 0 and "already installed" not in result.stderr.lower():
225+
return False
226+
except Exception:
206227
return False
207228

208229
return _run_bash_with_retry("claude plugin install claude-mem")

tests/unit/installer/steps/test_dependencies.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,15 +342,32 @@ def test_install_claude_mem_uses_plugin_system(self, mock_run):
342342
"""install_claude_mem uses claude plugin marketplace and install."""
343343
from installer.steps.dependencies import install_claude_mem
344344

345-
mock_run.return_value = MagicMock(returncode=0)
345+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
346346

347347
result = install_claude_mem()
348348

349349
assert mock_run.call_count >= 2
350-
# Check marketplace add call
350+
# First call adds marketplace
351351
first_call = mock_run.call_args_list[0][0][0]
352352
assert "claude plugin marketplace add" in first_call[2]
353353
assert "thedotmack/claude-mem" in first_call[2]
354-
# Check plugin install call
354+
# Second call installs plugin
355355
second_call = mock_run.call_args_list[1][0][0]
356356
assert "claude plugin install claude-mem" in second_call[2]
357+
358+
@patch("subprocess.run")
359+
def test_install_claude_mem_succeeds_if_marketplace_already_added(self, mock_run):
360+
"""install_claude_mem succeeds when marketplace already exists."""
361+
from installer.steps.dependencies import install_claude_mem
362+
363+
def side_effect(*args, **kwargs):
364+
cmd = args[0] if args else kwargs.get("args", [])
365+
if isinstance(cmd, list) and "marketplace add" in cmd[2]:
366+
return MagicMock(returncode=1, stderr="already installed", stdout="")
367+
return MagicMock(returncode=0, stdout="", stderr="")
368+
369+
mock_run.side_effect = side_effect
370+
371+
result = install_claude_mem()
372+
373+
assert result is True

0 commit comments

Comments
 (0)