Skip to content

Commit b60fa52

Browse files
fix: support Windows paths and npm launchers
Adds Windows-safe TOML path serialization, resolved npm launcher execution, and regression coverage. Validated by the complete Forgejo matrix and the green external GitHub Windows gate.
1 parent bde3e0a commit b60fa52

4 files changed

Lines changed: 54 additions & 3 deletions

File tree

src/capacium/adapters/mcp_config_patcher.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@ def _write_toml_simple(f, data: dict, prefix: str = "") -> None:
236236
elif isinstance(value, (int, float)):
237237
f.write(f"{key} = {value}\n")
238238
else:
239-
f.write(f'{key} = "{value}"\n')
239+
# JSON string quoting is also valid TOML basic-string quoting
240+
# and correctly escapes Windows backslashes, quotes, and
241+
# control characters.
242+
f.write(f"{key} = {json.dumps(str(value), ensure_ascii=False)}\n")
240243

241244
@staticmethod
242245
def enrich_mcp_meta_for_git(

src/capacium/commands/install.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2139,10 +2139,10 @@ def _install_npm_dependencies(package_dir: Path, cap_name: str) -> bool:
21392139

21402140
has_lock = (runtime_dir / "package-lock.json").exists()
21412141
if has_lock:
2142-
cmd = ["npm", "ci", "--production"]
2142+
cmd = [npm_path, "ci", "--production"]
21432143
print(f" Running npm ci in {runtime_dir}...")
21442144
else:
2145-
cmd = ["npm", "install", "--production"]
2145+
cmd = [npm_path, "install", "--production"]
21462146
print(f" Running npm install in {runtime_dir}...")
21472147

21482148
try:

tests/test_install.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import subprocess
2+
from unittest.mock import patch
23

34

45
class TestResolveSource:
@@ -340,6 +341,28 @@ def test_install_accepts_cwd_with_capability(self, tmp_home, tmp_path, capsys, m
340341
assert "Use --source" in out
341342

342343

344+
class TestInstallNpmDependencies:
345+
def test_uses_resolved_npm_executable(self, tmp_path):
346+
from capacium.commands.install import _install_npm_dependencies
347+
348+
(tmp_path / "package.json").write_text('{"name": "test-server"}')
349+
npm_executable = r"C:\Program Files\nodejs\npm.cmd"
350+
completed = subprocess.CompletedProcess(
351+
[npm_executable, "install", "--production"],
352+
0,
353+
stdout="installed",
354+
stderr="",
355+
)
356+
357+
with patch("shutil.which", return_value=npm_executable), patch(
358+
"capacium.commands.install.subprocess.run",
359+
return_value=completed,
360+
) as run:
361+
assert _install_npm_dependencies(tmp_path, "test-server") is True
362+
363+
assert run.call_args.args[0][0] == npm_executable
364+
365+
343366
class TestFetchRemoteTags:
344367
def test_fetch_tags_from_local_bare(self, tmp_path):
345368
remote = tmp_path / "remote.git"

tests/test_mcp_adapters.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Ensures no data destruction on patch/unpatch operations.
55
"""
66
import json
7+
from io import StringIO
78
from unittest.mock import patch
89

910
import pytest
@@ -74,6 +75,30 @@ def test_json_roundtrip_preserves_data(self, tmp_path):
7475
assert roundtripped == original
7576

7677

78+
class TestMcpConfigPatcherToml:
79+
def test_simple_writer_escapes_windows_paths(self):
80+
from capacium.utils.toml_compat import tomllib
81+
82+
output = StringIO()
83+
windows_path = r"C:\Users\runneradmin\.capacium\packages\server.py"
84+
McpConfigPatcher._write_toml_simple(
85+
output,
86+
{
87+
"mcp_servers": {
88+
"test-server": {
89+
"command": windows_path,
90+
"args": [windows_path, '--label="Windows"'],
91+
}
92+
}
93+
},
94+
)
95+
96+
parsed = tomllib.loads(output.getvalue())
97+
entry = parsed["mcp_servers"]["test-server"]
98+
assert entry["command"] == windows_path
99+
assert entry["args"] == [windows_path, '--label="Windows"']
100+
101+
77102
class TestBuildMcpEntry:
78103
def test_stdio_with_explicit_command(self, tmp_path):
79104
entry = McpConfigPatcher.build_mcp_entry(

0 commit comments

Comments
 (0)