feat: Add semble install#176
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
|
@greptileai review |
Confidence Score: 4/5Safe to merge with one follow-up: the uninstall code path needs an end-to-end test. The installer logic itself is sound and individual helpers are well-tested. The gap is that run("uninstall") is never called end-to-end — the full uninstall flow (remove operations, "not-found" returns through _apply, different UI text, different footer) can silently regress without the test suite catching it. The reviewer specifically asked for e2e coverage of all codepaths, so this missing test is the main concern. tests/test_installer.py — needs a run("uninstall") end-to-end test to match the run("install") test that already exists. Reviews (3): Last reviewed commit: "Fix toml edgecase" | Re-trigger Greptile |
Confidence Score: 3/5The install path works correctly for fresh configs; the uninstall path can corrupt pre-existing configs where semble was not the first entry. The src/semble/installer.py — specifically the Reviews (2): Last reviewed commit: "Group types/vars" | Re-trigger Greptile |
|
@greptileai review this branch again main. Focus on two things:
|
stephantul
left a comment
There was a problem hiding this comment.
Small docs comments, but looks good!
| ```bash | ||
| uv tool install semble | ||
| semble install # configure your agents | ||
| semble uninstall # undo |
There was a problem hiding this comment.
This is a bit weird, because the instruction seems to imply that you need to uninstall it immediately. Maybe make it a separate block?
i.e., if I copy paste this block, I'll just install and uninstall.
| """Return the project-relative path where the semble sub-agent file should be written.""" | ||
| base_dir = ".github" if agent is Agent.COPILOT else f".{agent.value}" | ||
| return Path(base_dir) / "agents" / "semble-search.md" | ||
| # Copilot requires the .agent.md extension; the others use a plain .md file. |
There was a problem hiding this comment.
do the others not accept .agent.md? As in: copilot only reads .agent.md files, but maybe the others do as well. If so, could be useful to just keep it like this. But maybe a bit brittle
| "tree-sitter-language-pack>=1.0,<1.8.0,!=1.6.3", | ||
| "orjson" | ||
| "orjson", | ||
| "questionary>=2.0", |
There was a problem hiding this comment.
maybe an upper bound on the pin as well? (i.e., <3.0)
| from tree_sitter import Node, Parser | ||
| from tree_sitter_language_pack import SupportedLanguage, download, get_parser | ||
|
|
||
| _HOME = Path.home() |
There was a problem hiding this comment.
Just out of caution: if this function does not work for some reason, this script will fail on import. I'm not sure if it ever will, just something to keep in mind.
To avoid this, you can create a function and potentially cache the result.
| "json5" ships in tree-sitter-language-pack but isn't in its typed language list, hence the cast. | ||
| """ | ||
| try: | ||
| download(["json5"]) |
There was a problem hiding this comment.
we can potentially ship this grammar with the package. It's a useful exception. I don't know how large it is, but I suspect it's pretty small.
| ] | ||
|
|
||
|
|
||
| def _json5_parser() -> Parser | None: |
There was a problem hiding this comment.
I think this could be cached: if the download fails or times out, it will fail or time out every time.
| return None | ||
|
|
||
|
|
||
| def _json5_object(text: str) -> JsonObjectResult: |
There was a problem hiding this comment.
I'm not a big fan of these kinds of returns: they put a lot of work in the caller. Instead, you could consider returning a tuple containing
(result, success)
if success is True, you can safely read result, if it is False, the result contains the reason for the error.
| class McpConfig: | ||
| """MCP integration config for one agent.""" | ||
|
|
||
| path: Path | PathResolver |
There was a problem hiding this comment.
As far as I can tell, there is no need for this disjunction. The paths can resolve when creating the McpConfig itself, e.g.
instead of doing:
McpConfig(_vscode_mcp_path)you can do:
McpConfig(_vscode_mcp_path())| SEMBLE_START = "<!-- SEMBLE_START -->" | ||
| SEMBLE_END = "<!-- SEMBLE_END -->" | ||
|
|
||
| _STDIO_ENTRY: dict[str, object] = { |
There was a problem hiding this comment.
I think the use of _ENTRY is slightly ambiguous here. I first read it as "an entry" (i.e., a record), but I think you mean "entrypoint"? If so, maybe rename it to read entrypoint.
There was a problem hiding this comment.
One thing I think would really improve this code is moving the JSON5 and TOML parsing to a separate file or separate files. It would lead to a lot more readability. Likewise, the definitions can move to a separate const file? That way you separate the entry points for the TOML and JSON5 parsing from the rest of the code, and also separate the constants from operational code. I think just making a submodule called installer and adding those under this could be fine.
This PR adds a
semble installcommand. It detects installed coding agents and configures semble globally across three integration types (MCP, instructions, sub-agent). We use tree-sitter for json/jsonc parsing so comments in MCP/settings jsons are preserved.Here's what the flow looks like in the terminal on running
semble install(semble uninstallgives the same flow but the reverse (uninstalling whatever is selected)):