@rethunk/mcp-multi-root-git 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +53 -0
- package/HUMANS.md +115 -0
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/repo-paths.js +26 -0
- package/dist/server/git-inventory-tool.js +191 -0
- package/dist/server/git-parity-tool.js +149 -0
- package/dist/server/git-status-tool.js +84 -0
- package/dist/server/git.js +165 -0
- package/dist/server/inventory.js +133 -0
- package/dist/server/json.js +41 -0
- package/dist/server/list-presets-tool.js +105 -0
- package/dist/server/presets-resource.js +47 -0
- package/dist/server/presets.js +175 -0
- package/dist/server/roots.js +134 -0
- package/dist/server/schemas.js +22 -0
- package/dist/server/tools.js +12 -0
- package/dist/server.js +11 -0
- package/docs/install.md +199 -0
- package/docs/mcp-tools.md +51 -0
- package/git-mcp-presets.schema.json +79 -0
- package/package.json +67 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# AGENTS.md — LLM and developer onboarding
|
|
2
|
+
|
|
3
|
+
**Scope:** [`@rethunk/mcp-multi-root-git`](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git) is an MCP **stdio** server: entry [`src/server.ts`](src/server.ts) (FastMCP + `registerRethunkGitTools`), supporting modules under [`src/server/`](src/server/), build output [`dist/server.js`](dist/server.js) (see `package.json` `bin` / `exports`; publish ships the full `dist/` tree).
|
|
4
|
+
|
|
5
|
+
**Operators and integrators:** **[docs/install.md](docs/install.md)** is the **only** place for prerequisites, how to launch the server, and per-client MCP configuration — do not duplicate that in README, HUMANS, rules, or here. Preset file, dev workflow, CI, and publishing: **[HUMANS.md](HUMANS.md)**.
|
|
6
|
+
|
|
7
|
+
**Tool ids, client naming, `format` / JSON envelopes, resource URI, workspace root order:** **[docs/mcp-tools.md](docs/mcp-tools.md)** — canonical; do not duplicate those tables in this file or HUMANS.
|
|
8
|
+
|
|
9
|
+
## Implementation map
|
|
10
|
+
|
|
11
|
+
| File | Symbols / notes |
|
|
12
|
+
|------|-------------------|
|
|
13
|
+
| [`src/server.ts`](src/server.ts) | `FastMCP` + `roots: { enabled: true }`; `readMcpServerVersion()`; `registerRethunkGitTools` |
|
|
14
|
+
| [`src/server/json.ts`](src/server/json.ts) | `MCP_JSON_FORMAT_VERSION`, `jsonRespond()`, `spreadWhen`, `spreadDefined` — every tool JSON body ends with `rethunkGitMcp` |
|
|
15
|
+
| [`src/server/git.ts`](src/server/git.ts) | `gateGit()` — lazy `git --version`; `spawnGitAsync`, `asyncPool`, `GIT_SUBPROCESS_PARALLELISM`; `gitTopLevel`, `gitRevParseGitDir`, `gitRevParseHead`, `parseGitSubmodulePaths`, `hasGitMetadata`; `gitStatusSnapshotAsync`, `gitStatusShortBranchAsync`, `fetchAheadBehind`, `isSafeGitUpstreamToken` |
|
|
16
|
+
| [`src/server/roots.ts`](src/server/roots.ts) | `uriToPath`, `listFileRoots`, `pathMatchesWorkspaceRootHint`, `resolveWorkspaceRoots`, `resolveRootsForPreset`, `requireGitAndRoots` — session roots only (client wiring: [docs/install.md](docs/install.md)) |
|
|
17
|
+
| [`src/server/presets.ts`](src/server/presets.ts) | `PRESET_FILE_PATH`, `splitPresetFileRaw`, `loadPresetsFromGitTop`, `getPresetEntry`, `presetLoadErrorPayload`, `applyPresetNestedRoots`, `applyPresetParityPairs`; Zod `PresetEntrySchema` / `PresetFileSchema` must match [`git-mcp-presets.schema.json`](git-mcp-presets.schema.json) |
|
|
18
|
+
| [`src/server/schemas.ts`](src/server/schemas.ts) | `WorkspacePickSchema`, `MAX_INVENTORY_ROOTS_DEFAULT` |
|
|
19
|
+
| [`src/server/inventory.ts`](src/server/inventory.ts) | `validateRepoPath`, `makeSkipEntry`, `buildInventorySectionMarkdown`, `collectInventoryEntry` (uses repo-paths + git) |
|
|
20
|
+
| [`src/server/tools.ts`](src/server/tools.ts) | `registerRethunkGitTools` — calls per-surface `register*` below |
|
|
21
|
+
| [`src/server/git-status-tool.ts`](src/server/git-status-tool.ts) | `registerGitStatusTool` — `git_status` |
|
|
22
|
+
| [`src/server/git-inventory-tool.ts`](src/server/git-inventory-tool.ts) | `registerGitInventoryTool` — `git_inventory` |
|
|
23
|
+
| [`src/server/git-parity-tool.ts`](src/server/git-parity-tool.ts) | `registerGitParityTool` — `git_parity` |
|
|
24
|
+
| [`src/server/list-presets-tool.ts`](src/server/list-presets-tool.ts) | `registerListPresetsTool` — `list_presets` |
|
|
25
|
+
| [`src/server/presets-resource.ts`](src/server/presets-resource.ts) | `registerPresetsResource` — `rethunk-git://presets` resource |
|
|
26
|
+
| [`src/repo-paths.ts`](src/repo-paths.ts) | `resolvePathForRepo`, `assertRelativePathUnderTop`, `isStrictlyUnderGitTop`, `realPathOrSelf` |
|
|
27
|
+
|
|
28
|
+
## Changing contracts
|
|
29
|
+
|
|
30
|
+
- **Documentation layout:** do not add top-of-file **banner** paragraphs (bold blocks such as “Canonical doc for… / link here only”) to `docs/install.md` or other shipped docs. Use normal titles, TOC, and cross-links from README / this file / HUMANS.
|
|
31
|
+
- **`rethunkGitMcp.jsonFormatVersion`:** bump **`MCP_JSON_FORMAT_VERSION`** and document the migration in this file and in [docs/mcp-tools.md](docs/mcp-tools.md) when JSON field names or nesting change incompatibly.
|
|
32
|
+
- **Preset file:** keep **`splitPresetFileRaw`** + Zod parsing aligned with **`git-mcp-presets.schema.json`**; update the schema when adding keys or shapes.
|
|
33
|
+
- **Public tool surface:** if you add/rename tools, update [docs/mcp-tools.md](docs/mcp-tools.md) and [README.md](README.md) if the landing page still mentions tools; update [docs/install.md](docs/install.md) if install or client-specific wiring changes — **never** copy install steps into other docs; update [.cursor/rules/rethunk-git-mcp.mdc](.cursor/rules/rethunk-git-mcp.mdc) only if *when-to-use MCP vs shell* wording must change (that rule links `docs/install.md`, [HUMANS.md](HUMANS.md), and `docs/mcp-tools.md` without duplicating them).
|
|
34
|
+
|
|
35
|
+
## Validation and CI
|
|
36
|
+
|
|
37
|
+
Local: `bun run build` (`rimraf dist && tsc`), `bun run check` (Biome), `bun run test` (`bun test` for [`src/repo-paths.test.ts`](src/repo-paths.test.ts)). GitHub Actions runs the same after `bun install --frozen-lockfile` on PRs and `main` ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)), then uploads a **prerelease `npm pack` artifact**. Pushes of tag **`v*.*.*`** matching `package.json` `version` run [`.github/workflows/release.yml`](.github/workflows/release.yml) (**GitHub Packages** npm publish under **`@rethunk-ai/mcp-multi-root-git`** + **GitHub Release** with tarball); **npmjs** is manual only — see [HUMANS.md](HUMANS.md) Publishing.
|
|
38
|
+
|
|
39
|
+
Optional [`.githooks/`](.githooks): run **`bun run setup-hooks`** once per clone (`core.hooksPath` → `.githooks`). **pre-commit** = `check`; **pre-push** = frozen install + build + check. See [HUMANS.md](HUMANS.md) Development.
|
|
40
|
+
|
|
41
|
+
Path confinement helpers live in [`src/repo-paths.ts`](src/repo-paths.ts); extend tests when changing that logic.
|
|
42
|
+
|
|
43
|
+
## Repository MCP entry (contributors)
|
|
44
|
+
|
|
45
|
+
Dogfooding from a clone: **[docs/install.md](docs/install.md)** — *From source (this repository)* only.
|
|
46
|
+
|
|
47
|
+
This repo may ship **`.cursor/`** config (example MCP entry + **alwaysApply** rule [`.cursor/rules/rethunk-git-mcp.mdc`](.cursor/rules/rethunk-git-mcp.mdc)). The rule covers *when* to call these tools vs shell git and links **`docs/install.md`**, **[HUMANS.md](HUMANS.md)**, and **`docs/mcp-tools.md`** without duplicating their bodies; it does not re-link this file when it is already injected as project context.
|
|
48
|
+
|
|
49
|
+
User-level skills may still mention the GitHub **README** for discovery. Canonical references: **tools / JSON** — **[docs/mcp-tools.md](docs/mcp-tools.md)**; **install / client wiring** — **[docs/install.md](docs/install.md)** (link it; do not paste per-client JSON into skills); **preset file** — **[HUMANS.md](HUMANS.md)**.
|
|
50
|
+
|
|
51
|
+
## Commits
|
|
52
|
+
|
|
53
|
+
Use the team’s **conventional commits + batching** skill (or equivalent): small themed commits, why-focused messages, `git add` + `git commit` in one shell invocation per batch.
|
package/HUMANS.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @rethunk/mcp-multi-root-git — User guide
|
|
2
|
+
|
|
3
|
+
Read-only MCP git tools for any workspace. **How the server is installed and wired to clients:** **[docs/install.md](docs/install.md)** only (do not restate that material here).
|
|
4
|
+
|
|
5
|
+
**Implementation map (modules under `src/server/`, entry `src/server.ts`), symbols, and contract bumps** live in **`AGENTS.md`** at the repository root. This guide does not repeat those sections.
|
|
6
|
+
|
|
7
|
+
**Registered tool ids, client naming (`rethunk-git_*`), `format` / JSON envelopes, resource URI, workspace root resolution:** **[docs/mcp-tools.md](docs/mcp-tools.md)** — canonical; not duplicated here.
|
|
8
|
+
|
|
9
|
+
## Workspace preset file
|
|
10
|
+
|
|
11
|
+
Commit **`.rethunk/git-mcp-presets.json`** at the **git repository root** (next to `.git`).
|
|
12
|
+
|
|
13
|
+
Example (legacy-style map):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"push-prep": {
|
|
18
|
+
"nestedRoots": ["path/to/package-a", "path/to/package-b"],
|
|
19
|
+
"parityPairs": [
|
|
20
|
+
{ "left": "core/packages/shared", "right": "edge/packages/shared", "label": "shared" }
|
|
21
|
+
],
|
|
22
|
+
"workspaceRootHint": "my-repo"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Call tools with `"preset": "push-prep"` instead of passing paths inline. Use **`presetMerge`: true** on `git_inventory` or `git_parity` to merge preset paths/pairs with inline `nestedRoots` / `pairs`.
|
|
28
|
+
|
|
29
|
+
With **multiple MCP file roots**, the server picks a root whose git toplevel defines that preset. If the preset entry has **`workspaceRootHint`**, only MCP roots whose basename or path suffix match are considered.
|
|
30
|
+
|
|
31
|
+
**JSON Schema** ships with the package as **`git-mcp-presets.schema.json`** for editor validation. Example wrapped layout:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"$schema": "./node_modules/@rethunk/mcp-multi-root-git/git-mcp-presets.schema.json",
|
|
36
|
+
"schemaVersion": "1",
|
|
37
|
+
"presets": {
|
|
38
|
+
"push-prep": {
|
|
39
|
+
"nestedRoots": ["packages/a"],
|
|
40
|
+
"parityPairs": []
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If you installed from **GitHub Packages**, use **`./node_modules/@rethunk-ai/mcp-multi-root-git/git-mcp-presets.schema.json`** in **`$schema`** instead (see [docs/install.md](docs/install.md#github-packages)).
|
|
47
|
+
|
|
48
|
+
**Layouts:**
|
|
49
|
+
|
|
50
|
+
1. **Wrapped (recommended):** `{ "schemaVersion": "1", "presets": { "<name>": { ... } } }`.
|
|
51
|
+
2. **Legacy map:** `{ "<preset-name>": { ... } }` with optional top-level `"schemaVersion"` and `"$schema"`.
|
|
52
|
+
|
|
53
|
+
Invalid JSON or schema errors return **`preset_file_invalid`** (not a silent empty result).
|
|
54
|
+
|
|
55
|
+
### Preset entry (quick reference)
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"<preset-name>": {
|
|
60
|
+
"nestedRoots": ["<relative-path>", ...],
|
|
61
|
+
"parityPairs": [{ "left": "<rel>", "right": "<rel>", "label": "<display>" }],
|
|
62
|
+
"workspaceRootHint": "<basename-or-suffix>"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Relative preset paths must stay inside the git toplevel; escapes are rejected.
|
|
68
|
+
|
|
69
|
+
## `git_not_found`
|
|
70
|
+
|
|
71
|
+
If **`git`** is missing or not runnable, tools and the presets resource respond with **`git_not_found`**. Prerequisites and `PATH`: **[docs/install.md](docs/install.md)**.
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
**Package install and MCP clients:** **[docs/install.md](docs/install.md)**.
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
Requires **Bun ≥ 1.3.11** to build this repository (`packageManager` in `package.json`). **Published runtime** (Node/Bun/Git and how to launch the server): **[docs/install.md](docs/install.md)** — *Prerequisites*.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bun install
|
|
83
|
+
bun run build # rimraf dist + tsc → dist/server.js, dist/server/*.js, dist/repo-paths.js
|
|
84
|
+
bun run check # Biome
|
|
85
|
+
bun run check:fix # Biome --write
|
|
86
|
+
bun run setup-hooks # once per clone: use .githooks (pre-commit: check; pre-push: CI parity)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Git hooks:** after `setup-hooks`, **pre-commit** runs `bun run check`; **pre-push** runs `bun install --frozen-lockfile`, `bun run build`, `bun run check`, and `bun run test` (same order as CI). Set **`SKIP_GIT_HOOKS=1`** or use **`--no-verify`** to bypass.
|
|
90
|
+
|
|
91
|
+
**CI:** [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs on pull requests and pushes to `main`: **`actions/setup-node` with Node 24** (minimum 22 asserted), then `bun install --frozen-lockfile`, `bun run build`, `bun run check`, and `bun run test`. A follow-up job **`prerelease-pack`** builds the same tree and runs **`npm pack`**, then uploads a **prerelease `.tgz` artifact** (named with the commit SHA) you can download from the workflow run’s **Artifacts** section (retention 90 days). Match the check job locally before opening a PR.
|
|
92
|
+
|
|
93
|
+
## Publishing
|
|
94
|
+
|
|
95
|
+
### GitHub (automated) — version tags only
|
|
96
|
+
|
|
97
|
+
Tag pushes run [`.github/workflows/release.yml`](.github/workflows/release.yml): build, check, tests, then:
|
|
98
|
+
|
|
99
|
+
1. **`npm pack`** using the committed **`package.json`** name [`@rethunk/mcp-multi-root-git`](https://github.com/Rethunk-AI/mcp-multi-root-git) — tarball attached to a **GitHub Release** for that tag.
|
|
100
|
+
2. **GitHub Packages** (npm registry): the workflow temporarily rewrites the package **name** to **`@rethunk-ai/mcp-multi-root-git`** (required scope for org `Rethunk-AI` on GitHub) and runs **`npm publish`** to **`https://npm.pkg.github.com`** with **`GITHUB_TOKEN`** (`packages: write`). No npmjs token is used in CI.
|
|
101
|
+
|
|
102
|
+
Prerequisite: push a **semver git tag** `vX.Y.Z` that **exactly matches** `version` in `package.json` (e.g. `v1.2.3` and `"version": "1.2.3"`).
|
|
103
|
+
|
|
104
|
+
### npmjs (manual) — maintainers only
|
|
105
|
+
|
|
106
|
+
npmjs no longer fits an unattended CI publish flow for this org; **do not** rely on automation to [npmjs](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git). To publish the **same** package name consumers already use (**`@rethunk/mcp-multi-root-git`**):
|
|
107
|
+
|
|
108
|
+
1. On a clean checkout at the release commit (usually **`main`** after bumping version), run **`bun run prepublishOnly`** (or `bun run build && bun run check && bun run test`).
|
|
109
|
+
2. Log in to the public registry once per machine: **`npm login`** (or `npm adduser`) so **`npm whoami`** shows the account that owns **`@rethunk`** on npmjs.
|
|
110
|
+
3. Ensure **`package.json`** still has **`"name": "@rethunk/mcp-multi-root-git"`** and **`publishConfig.access`** is **`"public"`** (no **`publishConfig.registry`** pointing at GitHub — leave default registry for npmjs).
|
|
111
|
+
4. Publish: **`npm publish --access public`** (runs **`prepublishOnly`** again unless you pass **`--ignore-scripts`** after you already verified locally).
|
|
112
|
+
|
|
113
|
+
**`package.json` `files`** must keep the whole **`dist/`** directory so every emitted chunk the entry imports is packed; if you add new `src/server/*.ts` modules, `tsc` emits matching **`dist/server/*.js`** files — do not narrow **`files`** back to a single **`server.js`** or installs break.
|
|
114
|
+
|
|
115
|
+
**Preset `$schema`:** after **`npm install`**, the schema path is under **`node_modules/@rethunk/mcp-multi-root-git/`** for npmjs, or **`node_modules/@rethunk-ai/mcp-multi-root-git/`** when installing from GitHub Packages — adjust **`$schema`** accordingly (see [docs/install.md](docs/install.md)).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rethunk-AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @rethunk/mcp-multi-root-git
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Rethunk-AI/mcp-multi-root-git/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/Rethunk-AI/mcp-multi-root-git/actions/workflows/release.yml)
|
|
5
|
+
[](https://github.com/Rethunk-AI/mcp-multi-root-git/releases/latest)
|
|
6
|
+
[](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git)
|
|
7
|
+
[](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git)
|
|
8
|
+
[](https://github.com/Rethunk-AI/mcp-multi-root-git/pkgs/npm/mcp-multi-root-git)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://github.com/Rethunk-AI/mcp-multi-root-git/blob/main/package.json)
|
|
11
|
+
|
|
12
|
+
Read-only **git** tools over MCP (status, multi-root inventory, `HEAD` parity, presets). **Install and MCP client wiring:** **[docs/install.md](docs/install.md)** only — do not duplicate those steps elsewhere.
|
|
13
|
+
|
|
14
|
+
**Repository:** [github.com/Rethunk-AI/mcp-multi-root-git](https://github.com/Rethunk-AI/mcp-multi-root-git) · **npmjs (manual releases):** [`@rethunk/mcp-multi-root-git`](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git) · **GitHub Packages (CI on each tag):** [`@rethunk-ai/mcp-multi-root-git`](https://github.com/Rethunk-AI/mcp-multi-root-git/pkgs/npm/mcp-multi-root-git) — see [docs/install.md](docs/install.md) and [HUMANS.md](HUMANS.md) Publishing.
|
|
15
|
+
|
|
16
|
+
## Documentation
|
|
17
|
+
|
|
18
|
+
| Doc | Audience |
|
|
19
|
+
|-----|----------|
|
|
20
|
+
| **[docs/install.md](docs/install.md)** | Single source for prerequisites, running the package, and every supported MCP client (plus from-source and troubleshooting) |
|
|
21
|
+
| **[docs/mcp-tools.md](docs/mcp-tools.md)** | Tool ids, client naming, `format` / JSON, resource URI, workspace root resolution (canonical reference) |
|
|
22
|
+
| **[HUMANS.md](HUMANS.md)** | Preset file, dev commands, CI, publishing |
|
|
23
|
+
| **[AGENTS.md](AGENTS.md)** | Contributors: implementation map ([`src/server/`](src/server/) + entry [`src/server.ts`](src/server.ts)), contract bumps, CI (IDEs that inject this file as project context should not re-link it from rules) |
|
|
24
|
+
|
|
25
|
+
**Tools at a glance:** see the table in **[docs/mcp-tools.md](docs/mcp-tools.md)**.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
export function realPathOrSelf(p) {
|
|
4
|
+
try {
|
|
5
|
+
return realpathSync(p);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return p;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function isStrictlyUnderGitTop(absPath, gitTop) {
|
|
12
|
+
const absR = realPathOrSelf(resolve(absPath));
|
|
13
|
+
const topR = realPathOrSelf(resolve(gitTop));
|
|
14
|
+
const rel = relative(topR, absR);
|
|
15
|
+
if (rel === "")
|
|
16
|
+
return true;
|
|
17
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
18
|
+
}
|
|
19
|
+
export function resolvePathForRepo(p, gitTop) {
|
|
20
|
+
const t = p.trim();
|
|
21
|
+
return isAbsolute(t) ? resolve(t) : resolve(gitTop, t);
|
|
22
|
+
}
|
|
23
|
+
/** Resolved path must lie inside git toplevel (relative or absolute user input). */
|
|
24
|
+
export function assertRelativePathUnderTop(_relPath, absResolved, gitTop) {
|
|
25
|
+
return isStrictlyUnderGitTop(absResolved, gitTop);
|
|
26
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { asyncPool, GIT_SUBPROCESS_PARALLELISM, gitRevParseGitDir, gitTopLevel, isSafeGitUpstreamToken, } from "./git.js";
|
|
3
|
+
import { buildInventorySectionMarkdown, collectInventoryEntry, makeSkipEntry, validateRepoPath, } from "./inventory.js";
|
|
4
|
+
import { jsonRespond, spreadDefined, spreadWhen } from "./json.js";
|
|
5
|
+
import { applyPresetNestedRoots } from "./presets.js";
|
|
6
|
+
import { requireGitAndRoots } from "./roots.js";
|
|
7
|
+
import { MAX_INVENTORY_ROOTS_DEFAULT, WorkspacePickSchema } from "./schemas.js";
|
|
8
|
+
export function registerGitInventoryTool(server) {
|
|
9
|
+
server.addTool({
|
|
10
|
+
name: "git_inventory",
|
|
11
|
+
description: "Read-only push-prep inventory: status + ahead/behind per root. " +
|
|
12
|
+
"Uses each repo's configured upstream (`@{u}`) unless both `remote` and `branch` are set. " +
|
|
13
|
+
"Presets from `.rethunk/git-mcp-presets.json`; use `presetMerge` to combine with inline paths.",
|
|
14
|
+
parameters: WorkspacePickSchema.extend({
|
|
15
|
+
nestedRoots: z
|
|
16
|
+
.array(z.string())
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Paths relative to git toplevel. If empty/omitted, only the repo root is listed. With `presetMerge`, merged with preset paths."),
|
|
19
|
+
preset: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Named preset from .rethunk/git-mcp-presets.json (at git toplevel)."),
|
|
23
|
+
presetMerge: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.optional()
|
|
26
|
+
.default(false)
|
|
27
|
+
.describe("When true, merge `nestedRoots` with preset nestedRoots instead of replacing."),
|
|
28
|
+
remote: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Fixed upstream remote; must be set together with `branch` to override auto upstream."),
|
|
32
|
+
branch: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Fixed upstream branch name; must be set together with `remote`."),
|
|
36
|
+
maxRoots: z
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(256)
|
|
41
|
+
.optional()
|
|
42
|
+
.default(MAX_INVENTORY_ROOTS_DEFAULT)
|
|
43
|
+
.describe("Max nested roots to process (cap)."),
|
|
44
|
+
}),
|
|
45
|
+
execute: async (args) => {
|
|
46
|
+
const pre = requireGitAndRoots(server, args, args.preset);
|
|
47
|
+
if (!pre.ok) {
|
|
48
|
+
return jsonRespond(pre.error);
|
|
49
|
+
}
|
|
50
|
+
const fixedRemote = args.remote;
|
|
51
|
+
const fixedBranch = args.branch;
|
|
52
|
+
const hasRemote = fixedRemote !== undefined && fixedRemote.trim() !== "";
|
|
53
|
+
const hasBranch = fixedBranch !== undefined && fixedBranch.trim() !== "";
|
|
54
|
+
if (hasRemote !== hasBranch) {
|
|
55
|
+
return jsonRespond({
|
|
56
|
+
error: "remote_branch_mismatch",
|
|
57
|
+
message: "Set both `remote` and `branch` for fixed upstream, or omit both for auto `@{u}`.",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const useFixed = hasRemote && hasBranch;
|
|
61
|
+
if (useFixed) {
|
|
62
|
+
const r = String(fixedRemote).trim();
|
|
63
|
+
const b = String(fixedBranch).trim();
|
|
64
|
+
if (!isSafeGitUpstreamToken(r) || !isSafeGitUpstreamToken(b)) {
|
|
65
|
+
return jsonRespond({
|
|
66
|
+
error: "invalid_remote_or_branch",
|
|
67
|
+
message: "remote and branch must be plain tokens: no whitespace, control characters, `@`, `..`, leading `-`, or git rev metacharacters like `^ : ? * [ ] { } ~ \\`.",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const allJson = [];
|
|
72
|
+
const mdChunks = [];
|
|
73
|
+
for (const workspaceRoot of pre.roots) {
|
|
74
|
+
const top = gitTopLevel(workspaceRoot);
|
|
75
|
+
if (!top) {
|
|
76
|
+
const err = { error: "not_a_git_repository", path: workspaceRoot };
|
|
77
|
+
if (args.format === "json") {
|
|
78
|
+
allJson.push({
|
|
79
|
+
workspace_root: workspaceRoot,
|
|
80
|
+
upstream: {
|
|
81
|
+
mode: useFixed ? "fixed" : "auto",
|
|
82
|
+
remote: fixedRemote,
|
|
83
|
+
branch: fixedBranch,
|
|
84
|
+
},
|
|
85
|
+
entries: [
|
|
86
|
+
{
|
|
87
|
+
label: workspaceRoot,
|
|
88
|
+
path: workspaceRoot,
|
|
89
|
+
branchStatus: "",
|
|
90
|
+
shortStatus: "",
|
|
91
|
+
detached: false,
|
|
92
|
+
headAbbrev: "",
|
|
93
|
+
upstreamMode: useFixed ? "fixed" : "auto",
|
|
94
|
+
upstreamRef: null,
|
|
95
|
+
ahead: null,
|
|
96
|
+
behind: null,
|
|
97
|
+
upstreamNote: "",
|
|
98
|
+
skipReason: JSON.stringify(err),
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
mdChunks.push(`# Git inventory`, "", jsonRespond(err), "");
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
let nestedRoots = args.nestedRoots;
|
|
109
|
+
let presetSchemaVersion;
|
|
110
|
+
if (args.preset) {
|
|
111
|
+
const applied = applyPresetNestedRoots(top, args.preset, args.presetMerge, nestedRoots);
|
|
112
|
+
if (!applied.ok) {
|
|
113
|
+
return jsonRespond(applied.error);
|
|
114
|
+
}
|
|
115
|
+
nestedRoots = applied.nestedRoots;
|
|
116
|
+
presetSchemaVersion = applied.presetSchemaVersion;
|
|
117
|
+
}
|
|
118
|
+
const maxRoots = args.maxRoots ?? MAX_INVENTORY_ROOTS_DEFAULT;
|
|
119
|
+
let nestedRootsTruncated = false;
|
|
120
|
+
let nestedRootsOmittedCount = 0;
|
|
121
|
+
if (nestedRoots && nestedRoots.length > maxRoots) {
|
|
122
|
+
nestedRootsOmittedCount = nestedRoots.length - maxRoots;
|
|
123
|
+
nestedRoots = nestedRoots.slice(0, maxRoots);
|
|
124
|
+
nestedRootsTruncated = true;
|
|
125
|
+
}
|
|
126
|
+
const headerNote = useFixed
|
|
127
|
+
? `remote/branch (fixed): ${fixedRemote}/${fixedBranch}`
|
|
128
|
+
: "upstream: per-repo @{u} (configured upstream)";
|
|
129
|
+
const entries = [];
|
|
130
|
+
if (nestedRoots?.length) {
|
|
131
|
+
const jobs = [];
|
|
132
|
+
for (const rel of nestedRoots) {
|
|
133
|
+
const { abs, underTop } = validateRepoPath(rel, top);
|
|
134
|
+
if (!underTop) {
|
|
135
|
+
entries.push(makeSkipEntry(rel, abs, useFixed ? "fixed" : "auto", "(path escapes git toplevel — rejected)"));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!gitRevParseGitDir(abs)) {
|
|
139
|
+
entries.push(makeSkipEntry(rel, abs, useFixed ? "fixed" : "auto", "(not a git work tree — skip)"));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
jobs.push({ label: rel, abs });
|
|
143
|
+
}
|
|
144
|
+
const computed = await asyncPool(jobs, GIT_SUBPROCESS_PARALLELISM, (j) => collectInventoryEntry(j.label, j.abs, useFixed ? fixedRemote : undefined, useFixed ? fixedBranch : undefined));
|
|
145
|
+
entries.push(...computed);
|
|
146
|
+
}
|
|
147
|
+
else if (!gitRevParseGitDir(top)) {
|
|
148
|
+
entries.push(makeSkipEntry(".", top, useFixed ? "fixed" : "auto", "(not a git work tree — unexpected)"));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const one = await collectInventoryEntry(".", top, useFixed ? fixedRemote : undefined, useFixed ? fixedBranch : undefined);
|
|
152
|
+
entries.push(one);
|
|
153
|
+
}
|
|
154
|
+
if (args.format === "json") {
|
|
155
|
+
allJson.push({
|
|
156
|
+
workspace_root: top,
|
|
157
|
+
...spreadDefined("presetSchemaVersion", presetSchemaVersion),
|
|
158
|
+
...spreadWhen(nestedRootsTruncated, {
|
|
159
|
+
nestedRootsTruncated: true,
|
|
160
|
+
nestedRootsOmittedCount,
|
|
161
|
+
}),
|
|
162
|
+
upstream: useFixed
|
|
163
|
+
? { mode: "fixed", remote: fixedRemote, branch: fixedBranch }
|
|
164
|
+
: { mode: "auto" },
|
|
165
|
+
entries,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const sections = [
|
|
170
|
+
"# Git inventory",
|
|
171
|
+
"",
|
|
172
|
+
`workspace_root: ${top}`,
|
|
173
|
+
headerNote,
|
|
174
|
+
"",
|
|
175
|
+
];
|
|
176
|
+
if (nestedRootsTruncated) {
|
|
177
|
+
sections.push(`nested_roots_truncated: ${nestedRootsOmittedCount} path(s) not listed (maxRoots=${maxRoots})`, "");
|
|
178
|
+
}
|
|
179
|
+
for (const e of entries) {
|
|
180
|
+
sections.push(...buildInventorySectionMarkdown(e));
|
|
181
|
+
}
|
|
182
|
+
mdChunks.push(sections.join("\n"));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (args.format === "json") {
|
|
186
|
+
return jsonRespond({ inventories: allJson });
|
|
187
|
+
}
|
|
188
|
+
return mdChunks.join("\n\n---\n\n");
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { gitRevParseHead, gitTopLevel } from "./git.js";
|
|
3
|
+
import { validateRepoPath } from "./inventory.js";
|
|
4
|
+
import { jsonRespond, spreadDefined } from "./json.js";
|
|
5
|
+
import { applyPresetParityPairs } from "./presets.js";
|
|
6
|
+
import { requireGitAndRoots } from "./roots.js";
|
|
7
|
+
import { WorkspacePickSchema } from "./schemas.js";
|
|
8
|
+
export function registerGitParityTool(server) {
|
|
9
|
+
server.addTool({
|
|
10
|
+
name: "git_parity",
|
|
11
|
+
description: "Read-only: compare `git rev-parse HEAD` for path pairs. Presets or inline `pairs`; `presetMerge` merges with inline pairs.",
|
|
12
|
+
parameters: WorkspacePickSchema.extend({
|
|
13
|
+
pairs: z
|
|
14
|
+
.array(z.object({
|
|
15
|
+
left: z.string(),
|
|
16
|
+
right: z.string(),
|
|
17
|
+
label: z.string().optional(),
|
|
18
|
+
}))
|
|
19
|
+
.optional(),
|
|
20
|
+
preset: z.string().optional(),
|
|
21
|
+
presetMerge: z.boolean().optional().default(false),
|
|
22
|
+
}),
|
|
23
|
+
execute: async (args) => {
|
|
24
|
+
const pre = requireGitAndRoots(server, args, args.preset);
|
|
25
|
+
if (!pre.ok) {
|
|
26
|
+
return jsonRespond(pre.error);
|
|
27
|
+
}
|
|
28
|
+
const results = [];
|
|
29
|
+
const mdParts = [];
|
|
30
|
+
for (const workspaceRoot of pre.roots) {
|
|
31
|
+
const top = gitTopLevel(workspaceRoot);
|
|
32
|
+
if (!top) {
|
|
33
|
+
const errPayload = { error: "not_a_git_repository", path: workspaceRoot };
|
|
34
|
+
const err = jsonRespond(errPayload);
|
|
35
|
+
if (args.format === "json") {
|
|
36
|
+
results.push({
|
|
37
|
+
workspace_root: workspaceRoot,
|
|
38
|
+
status: "MISMATCH",
|
|
39
|
+
pairs: [{ label: "—", leftPath: "", rightPath: "", match: false, error: err }],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
mdParts.push(err);
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
let pairs = args.pairs;
|
|
48
|
+
let parityPresetSchemaVersion;
|
|
49
|
+
if (args.preset) {
|
|
50
|
+
const applied = applyPresetParityPairs(top, args.preset, args.presetMerge, pairs);
|
|
51
|
+
if (!applied.ok) {
|
|
52
|
+
return jsonRespond(applied.error);
|
|
53
|
+
}
|
|
54
|
+
pairs = applied.pairs;
|
|
55
|
+
parityPresetSchemaVersion = applied.presetSchemaVersion;
|
|
56
|
+
}
|
|
57
|
+
if (!pairs?.length) {
|
|
58
|
+
return jsonRespond({
|
|
59
|
+
error: "no_pairs",
|
|
60
|
+
message: "Pass `pairs` directly or a `preset` with parityPairs (or presetMerge).",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
let allOk = true;
|
|
64
|
+
const pairResults = [];
|
|
65
|
+
for (const pair of pairs) {
|
|
66
|
+
const pa = validateRepoPath(pair.left, top);
|
|
67
|
+
const pb = validateRepoPath(pair.right, top);
|
|
68
|
+
const label = pair.label ?? `${pair.left} / ${pair.right}`;
|
|
69
|
+
if (!pa.underTop || !pb.underTop) {
|
|
70
|
+
allOk = false;
|
|
71
|
+
pairResults.push({
|
|
72
|
+
label,
|
|
73
|
+
leftPath: pa.abs,
|
|
74
|
+
rightPath: pb.abs,
|
|
75
|
+
match: false,
|
|
76
|
+
error: "path escapes git toplevel — rejected",
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const ha = gitRevParseHead(pa.abs);
|
|
81
|
+
const hb = gitRevParseHead(pb.abs);
|
|
82
|
+
if (!ha.ok || !hb.ok) {
|
|
83
|
+
allOk = false;
|
|
84
|
+
pairResults.push({
|
|
85
|
+
label,
|
|
86
|
+
leftPath: pa.abs,
|
|
87
|
+
rightPath: pb.abs,
|
|
88
|
+
match: false,
|
|
89
|
+
error: [!ha.ok ? `left: ${ha.text}` : "", !hb.ok ? `right: ${hb.text}` : ""]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join("\n"),
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ha.sha !== hb.sha) {
|
|
96
|
+
allOk = false;
|
|
97
|
+
pairResults.push({
|
|
98
|
+
label,
|
|
99
|
+
leftPath: pa.abs,
|
|
100
|
+
rightPath: pb.abs,
|
|
101
|
+
match: false,
|
|
102
|
+
leftSha: ha.sha,
|
|
103
|
+
rightSha: hb.sha,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
pairResults.push({
|
|
108
|
+
label,
|
|
109
|
+
leftPath: pa.abs,
|
|
110
|
+
rightPath: pb.abs,
|
|
111
|
+
match: true,
|
|
112
|
+
sha: ha.sha,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
results.push({
|
|
117
|
+
workspace_root: top,
|
|
118
|
+
...spreadDefined("presetSchemaVersion", parityPresetSchemaVersion),
|
|
119
|
+
status: allOk ? "OK" : "MISMATCH",
|
|
120
|
+
pairs: pairResults,
|
|
121
|
+
});
|
|
122
|
+
if (args.format !== "json") {
|
|
123
|
+
const lines = [
|
|
124
|
+
"# Git HEAD parity",
|
|
125
|
+
"",
|
|
126
|
+
`status: ${allOk ? "OK" : "MISMATCH"}`,
|
|
127
|
+
"",
|
|
128
|
+
];
|
|
129
|
+
for (const pr of pairResults) {
|
|
130
|
+
if (pr.error) {
|
|
131
|
+
lines.push(`## ${pr.label} — error`, "```text", pr.error, "```", "");
|
|
132
|
+
}
|
|
133
|
+
else if (pr.match) {
|
|
134
|
+
lines.push(`## ${pr.label} — OK`, "```text", `SHA: ${pr.sha}`, "```", "");
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
lines.push(`## ${pr.label} — MISMATCH`, "```text", `left: ${pr.leftSha}`, `right: ${pr.rightSha}`, "```", "");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
mdParts.push(lines.join("\n"));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (args.format === "json") {
|
|
144
|
+
return jsonRespond({ parity: results });
|
|
145
|
+
}
|
|
146
|
+
return mdParts.join("\n\n---\n\n");
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|