dd7557ea62
- Auto-discovers Claude Code (~/.claude/settings.json) alongside Claude Desktop - Generated icons/app.icns (macOS) and icons/app.ico (Windows) from rounded PNGs - bcc.spec: PyInstaller spec for all platforms (.app on macOS, onefile on Windows/Linux) - .github/workflows/release.yml: builds and publishes GitHub Release on version tags - scripts/build_icons.py: regenerates icon files from source PNGs - requirements-dev.txt: adds pyinstaller and pillow for building - CLAUDE.md: initial repo guidance for Claude Code Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
4.4 KiB
Python
93 lines
4.4 KiB
Python
import json, tempfile, shutil
|
|
from pathlib import Path
|
|
import bcc_core as c
|
|
|
|
tmp = Path(tempfile.mkdtemp())
|
|
ok = []
|
|
|
|
def check(name, cond):
|
|
ok.append(cond)
|
|
print(("PASS" if cond else "FAIL"), "-", name)
|
|
|
|
# ---- 1. discovery (monkeypatch the base dir) ----
|
|
base = tmp / "AppSupport"
|
|
for d in ("Claude", "Claude-Work", "NotClaude"):
|
|
(base / d).mkdir(parents=True)
|
|
(base / "Claude" / c.CONFIG_FILENAME).write_text("{}")
|
|
# Claude-Work has no config yet; NotClaude shouldn't match
|
|
c.app_support_base = lambda: base
|
|
profs = c.discover_profiles()
|
|
labels = sorted(p.label for p in profs)
|
|
check("discovers Claude + Claude-Work, not NotClaude",
|
|
"Claude" in labels and "Claude-Work" in labels and "NotClaude" not in labels)
|
|
check("flags missing config file", any(p.label=="Claude-Work" and not p.config_exists for p in profs))
|
|
|
|
# ---- 2. write preserves OTHER keys + order, only touches mcpServers ----
|
|
cfgpath = tmp / "real.json"
|
|
original = {
|
|
"globalShortcut": "Cmd+Shift+Space",
|
|
"mcpServers": {"old": {"command": "node", "args": ["x.js"]}},
|
|
"someOtherTool": {"keep": True},
|
|
}
|
|
cfgpath.write_text(json.dumps(original, indent=2))
|
|
cfg = c.load_config(cfgpath)
|
|
servers = c.extract_servers(cfg)
|
|
# add a new one, disable the old one
|
|
servers.append(c.ServerEntry("brave", {"command": "npx", "args": ["-y", "@x/brave"]}, True))
|
|
servers[0].enabled = False
|
|
c.apply_servers(cfg, servers)
|
|
c.write_config(cfgpath, cfg)
|
|
written = json.loads(cfgpath.read_text())
|
|
keys = list(written.keys())
|
|
check("preserved globalShortcut", written.get("globalShortcut") == "Cmd+Shift+Space")
|
|
check("preserved someOtherTool", written.get("someOtherTool") == {"keep": True})
|
|
check("disabled server parked in _disabledMcpServers", "old" in written.get("_disabledMcpServers", {}))
|
|
check("enabled server in mcpServers", "brave" in written.get("mcpServers", {}))
|
|
check("old not in active mcpServers", "old" not in written.get("mcpServers", {}))
|
|
check("key order kept (globalShortcut before mcpServers)", keys.index("globalShortcut") < keys.index("mcpServers"))
|
|
check("backup created", (cfgpath.parent / c.BACKUP_DIRNAME).is_dir() and any((cfgpath.parent / c.BACKUP_DIRNAME).iterdir()))
|
|
|
|
# ---- 3. paste parser, all shapes ----
|
|
full = '{"mcpServers": {"a": {"command": "node", "args": ["a.js"]}}}'
|
|
check("parse full config shape", list(c.parse_pasted_json(full)) == ["a"])
|
|
inner = '{"b": {"command": "uvx", "args": ["mcp-server-git"]}}'
|
|
check("parse inner block shape", list(c.parse_pasted_json(inner)) == ["b"])
|
|
bare = '{"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]}'
|
|
parsed = c.parse_pasted_json(bare)
|
|
check("parse bare server + suggests name 'filesystem'", "filesystem" in parsed)
|
|
remote = '{"url": "https://mcp.asana.com/sse"}'
|
|
check("parse remote, suggests host name", "mcp" in c.parse_pasted_json(remote))
|
|
try:
|
|
c.parse_pasted_json('{"junk": 5}')
|
|
check("rejects junk", False)
|
|
except ValueError:
|
|
check("rejects junk", True)
|
|
|
|
# ---- 4. validation ----
|
|
bad = [c.ServerEntry("", {"command": "x"}, True),
|
|
c.ServerEntry("dup", {"command": "x"}, True),
|
|
c.ServerEntry("dup", {"command": "x"}, True),
|
|
c.ServerEntry("r", {"url": ""}, True),
|
|
c.ServerEntry("nocmd", {"command": ""}, True)]
|
|
probs = c.validate_servers(bad)
|
|
check("validation catches empty name", any("empty name" in p for p in probs))
|
|
check("validation catches duplicate", any("Duplicate" in p for p in probs))
|
|
check("validation catches missing url", any("no URL" in p for p in probs))
|
|
check("validation catches missing command", any("no command" in p for p in probs))
|
|
check("valid set passes clean", c.validate_servers([c.ServerEntry("good", {"command":"node"}, True)]) == [])
|
|
|
|
# ---- 5. dependency check (python3 exists; bogusxyz does not) ----
|
|
r_ok = c.check_dependency({"command": "python3"})
|
|
check("dep check finds python3", r_ok["status"] == "ok")
|
|
r_missing = c.check_dependency({"command": "definitely-not-real-binary-xyz"})
|
|
check("dep check flags missing", r_missing["status"] == "missing")
|
|
r_remote = c.check_dependency({"url": "https://example.com/mcp"})
|
|
check("dep check marks remote", r_remote["status"] == "remote")
|
|
# windows-style wrapper: cmd /c npx ... -> should look past cmd to npx
|
|
r_wrap = c.check_dependency({"command": "cmd", "args": ["/c", "definitely-not-real-xyz", "foo"]})
|
|
check("dep check sees through shell wrapper", "definitely-not-real-xyz" in r_wrap["label"])
|
|
|
|
print()
|
|
print(f"{sum(ok)}/{len(ok)} passed")
|
|
shutil.rmtree(tmp)
|