@chappibunny/repolens 1.13.1 → 1.14.1
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/CHANGELOG.md +37 -0
- package/README.md +66 -1
- package/bin/repolens-mcp.js +23 -0
- package/package.json +10 -4
- package/src/cli.js +10 -1
- package/src/core/config-schema.js +13 -0
- package/src/core/diff.js +21 -3
- package/src/delivery/comment.js +6 -0
- package/src/docs/branding.js +29 -0
- package/src/docs/generate-doc-set.js +7 -3
- package/src/help.js +2 -0
- package/src/init.js +6 -0
- package/src/mcp/server.js +130 -0
- package/src/mcp/tools.js +202 -0
- package/src/mcp/workspace.js +52 -0
- package/src/utils/logger.js +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RepoLens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.14.1
|
|
6
|
+
|
|
7
|
+
### Registry name and publish skip
|
|
8
|
+
|
|
9
|
+
- MCP Registry server name is `io.github.CHAPIBUNNY/repolens`. `package.json` `mcpName` and `server.json` `name` use that spelling. The registry matches the GitHub username case
|
|
10
|
+
- `server.json` `description` is 100 characters or fewer
|
|
11
|
+
- The release workflow skips `npm publish` when that exact version is already on the registry
|
|
12
|
+
|
|
13
|
+
## 1.14.0
|
|
14
|
+
|
|
15
|
+
### MCP server
|
|
16
|
+
|
|
17
|
+
AI coding tools can call RepoLens over stdio.
|
|
18
|
+
|
|
19
|
+
- New command: `repolens mcp`. The package also installs `repolens-mcp`
|
|
20
|
+
- Official `@modelcontextprotocol/sdk` server. Tools: `get_system_overview`, `list_modules`, `get_api_surface` (includes routes), `get_dependency_graph`, `get_architecture_diff`, `generate_docs`
|
|
21
|
+
- Read-only except `generate_docs`, which writes Markdown inside the workspace
|
|
22
|
+
- Every path stays inside the workspace root (`REPOLENS_WORKSPACE` or the working directory), using the existing `isInsideRoot` and `resolveOutputDir` helpers
|
|
23
|
+
- Deterministic mode is the default and does not make network calls. Logs go to stderr so they do not corrupt the protocol
|
|
24
|
+
- `get_architecture_diff` uses the existing git diff. `baseRef` compares against the working tree. `headRef` diffs two refs. Unsafe refs are rejected
|
|
25
|
+
- `package.json` `mcpName` is `io.github.CHAPIBUNNY/repolens`. Root `server.json` matches it for the MCP Registry. `smithery.yaml` starts `npx -y @chappibunny/repolens mcp`
|
|
26
|
+
- Client setup, including Cursor and VS Code install badges, is in `docs/MCP.md`
|
|
27
|
+
|
|
28
|
+
### GitHub Action
|
|
29
|
+
|
|
30
|
+
- Root `action.yml`: composite action **RepoLens Architecture Docs** (icon `book-open`, color `blue`)
|
|
31
|
+
- Inputs: `command` (`publish`, `demo`, `doctor`), `config`, `node-version`, `pr-comment`, `version`
|
|
32
|
+
- Runs the published package with `npx --yes @chappibunny/repolens@<version>`. Inputs are passed as environment variables and checked before they are placed on the npx argument list
|
|
33
|
+
- `REPOLENS_PR_COMMENT=false` skips the architecture diff comment even when `GITHUB_EVENT_NAME` is `pull_request`
|
|
34
|
+
- Marketplace listing is a checkbox on a GitHub Release. The steps are in `docs/RELEASE.md`. This release does not publish the action, the npm package, or a tag
|
|
35
|
+
|
|
36
|
+
### Docs badge
|
|
37
|
+
|
|
38
|
+
- Generated Markdown ends with a shields.io badge linking to https://github.com/CHAPIBUNNY/repolens
|
|
39
|
+
- On by default. Set `branding: false` or `branding.badge: false` to turn it off. Schema v1 stays at `configVersion: 1`
|
|
40
|
+
- README section **Add the badge to your repo** has the snippet
|
|
41
|
+
|
|
5
42
|
## 1.13.1
|
|
6
43
|
|
|
7
44
|
### Docs
|
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
[](https://marketplace.visualstudio.com/items?itemName=repolensdox.repolensdox)
|
|
13
13
|
[](https://github.com/CHAPIBUNNY/repolens/actions/workflows/test.yml)
|
|
14
14
|
[](LICENSE)
|
|
15
|
+
[](https://cursor.com/install-mcp?name=repolens&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjaGFwcGlidW5ueS9yZXBvbGVucyIsIm1jcCJdfQ%3D%3D)
|
|
16
|
+
[](https://insiders.vscode.dev/redirect/mcp/install?name=repolens&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40chappibunny%2Frepolens%22%2C%22mcp%22%5D%7D)
|
|
15
17
|
|
|
16
18
|
**Your architecture docs are already outdated.** RepoLens fixes that.
|
|
17
19
|
|
|
@@ -175,19 +177,78 @@ Step-by-step setup for publishers, AI features, Notion, Confluence, GitHub Wiki,
|
|
|
175
177
|
| `npx @chappibunny/repolens uninstall` | Remove all RepoLens files (config, docs, workflow) |
|
|
176
178
|
| `npx @chappibunny/repolens uninstall --force` | Remove without confirmation prompt |
|
|
177
179
|
| `npx @chappibunny/repolens feedback` | Send feedback to the team |
|
|
180
|
+
| `npx @chappibunny/repolens mcp` | Start the stdio MCP server (`repolens-mcp` is the same server) |
|
|
178
181
|
|
|
179
182
|
> **Note:** If you installed globally with `npm install -g @chappibunny/repolens`, you can use the shorter `repolens <command>` form.
|
|
180
183
|
> **Warning:** Do not run bare `npx repolens` — there is an unrelated `repolens@0.0.1` placeholder package on npm. Always use the scoped name `@chappibunny/repolens`.
|
|
181
184
|
|
|
182
185
|
---
|
|
183
186
|
|
|
187
|
+
## MCP server
|
|
188
|
+
|
|
189
|
+
Cursor, Claude Desktop, Claude Code, and VS Code Copilot can call RepoLens over stdio. The server is read-only except for `generate_docs`, which writes Markdown inside the workspace. Deterministic mode does not use the network. Paths cannot leave the workspace root.
|
|
190
|
+
|
|
191
|
+
[](https://cursor.com/install-mcp?name=repolens&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjaGFwcGlidW5ueS9yZXBvbGVucyIsIm1jcCJdfQ%3D%3D)
|
|
192
|
+
[](https://insiders.vscode.dev/redirect/mcp/install?name=repolens&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40chappibunny%2Frepolens%22%2C%22mcp%22%5D%7D)
|
|
193
|
+
|
|
194
|
+
Copy-paste configs for Cursor, Claude Desktop, and VS Code are in [docs/MCP.md](docs/MCP.md).
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"mcpServers": {
|
|
199
|
+
"repolens": {
|
|
200
|
+
"command": "npx",
|
|
201
|
+
"args": ["-y", "@chappibunny/repolens", "mcp"]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## GitHub Action
|
|
208
|
+
|
|
209
|
+
`action.yml` is a composite action named **RepoLens Architecture Docs**. It runs the published npm package (`npx --yes @chappibunny/repolens@1.14.1`). Inputs are `command` (`publish`, `demo`, or `doctor`), `config`, `node-version`, `pr-comment`, and `version`.
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
name: Architecture docs
|
|
213
|
+
on:
|
|
214
|
+
pull_request:
|
|
215
|
+
push:
|
|
216
|
+
branches: [main]
|
|
217
|
+
permissions:
|
|
218
|
+
contents: read
|
|
219
|
+
pull-requests: write
|
|
220
|
+
jobs:
|
|
221
|
+
docs:
|
|
222
|
+
runs-on: ubuntu-latest
|
|
223
|
+
steps:
|
|
224
|
+
- uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
|
|
225
|
+
- uses: CHAPIBUNNY/repolens@v1.14.1
|
|
226
|
+
with:
|
|
227
|
+
command: publish
|
|
228
|
+
config: .repolens.yml
|
|
229
|
+
node-version: "20"
|
|
230
|
+
pr-comment: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The action is listed on the GitHub Marketplace only after a release is published with **Publish this Action to the GitHub Marketplace** checked. Those steps are in [docs/RELEASE.md](docs/RELEASE.md). The tag `v1.14.1` has to exist before `uses: CHAPIBUNNY/repolens@v1.14.1` resolves, and `@chappibunny/repolens@1.14.1` has to be on npm before the action can install it.
|
|
234
|
+
|
|
235
|
+
## Add the badge to your repo
|
|
236
|
+
|
|
237
|
+
Generated Markdown includes this line unless `.repolens.yml` sets `branding: false` (or `branding.badge: false`). Add the same badge to a repository README:
|
|
238
|
+
|
|
239
|
+
```markdown
|
|
240
|
+
[](https://github.com/CHAPIBUNNY/repolens)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
184
245
|
## 📸 Example Output
|
|
185
246
|
|
|
186
247
|
### System map
|
|
187
248
|
|
|
188
249
|
Published system maps are Unicode diagrams drawn from import edges. RepoLens does not emit Mermaid.
|
|
189
250
|
|
|
190
|
-
The publisher set
|
|
251
|
+
The publisher set is Notion, Confluence, GitHub Wiki, Obsidian, and Markdown (`src/publishers`, 7 files). 1.14.0 adds the MCP server, the Marketplace action, and the docs badge. 1.14.1 sets the registry name to `io.github.CHAPIBUNNY/repolens`. Layout and counts are in [ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
191
252
|
|
|
192
253
|
### System Overview (Technical Profile)
|
|
193
254
|
|
|
@@ -248,6 +309,7 @@ When you open a pull request, RepoLens posts:
|
|
|
248
309
|
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues and fixes |
|
|
249
310
|
| [Migration](docs/MIGRATION.md) | Upgrading from v0.3.0 or earlier |
|
|
250
311
|
| [Stability](docs/STABILITY.md) | API contract and semver guarantees |
|
|
312
|
+
| [MCP server](docs/MCP.md) | stdio server for Cursor, Claude, and VS Code |
|
|
251
313
|
|
|
252
314
|
---
|
|
253
315
|
|
|
@@ -269,6 +331,9 @@ v1.0+ features complete — CLI, config schema, and plugin interface are frozen.
|
|
|
269
331
|
**Completed:**
|
|
270
332
|
- [x] VS Code extension ([available on Marketplace](https://marketplace.visualstudio.com/items?itemName=repolensdox.repolensdox))
|
|
271
333
|
- [x] Obsidian vault publisher (local notes with wikilinks and frontmatter)
|
|
334
|
+
- [x] MCP server (`repolens mcp`) for Cursor, Claude, and VS Code
|
|
335
|
+
- [x] GitHub Action `RepoLens Architecture Docs` (`action.yml`)
|
|
336
|
+
- [x] Docs by RepoLens badge on generated Markdown
|
|
272
337
|
|
|
273
338
|
**Next:**
|
|
274
339
|
- [ ] GitHub App for automated setup and status checks
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const argv = process.argv.slice(2);
|
|
5
|
+
const wantsHelp = argv.includes("--help") || argv.includes("-h");
|
|
6
|
+
const wantsVersion = argv.includes("--version") || argv.includes("-v");
|
|
7
|
+
|
|
8
|
+
if (wantsVersion && !wantsHelp) {
|
|
9
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
10
|
+
console.log(pkg.version || "0.0.0");
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (wantsHelp) {
|
|
15
|
+
const { printHelp } = await import("../src/help.js");
|
|
16
|
+
printHelp();
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Stdio MCP owns stdout. Keep RepoLens logs on stderr.
|
|
21
|
+
process.env.REPOLENS_LOG_STDERR = "1";
|
|
22
|
+
const { startMcpServer } = await import("../src/mcp/server.js");
|
|
23
|
+
await startMcpServer();
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chappibunny/repolens",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
4
|
+
"mcpName": "io.github.CHAPIBUNNY/repolens",
|
|
4
5
|
"description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"type": "module",
|
|
@@ -13,7 +14,8 @@
|
|
|
13
14
|
"./package.json": "./package.json"
|
|
14
15
|
},
|
|
15
16
|
"bin": {
|
|
16
|
-
"repolens": "bin/repolens.js"
|
|
17
|
+
"repolens": "bin/repolens.js",
|
|
18
|
+
"repolens-mcp": "bin/repolens-mcp.js"
|
|
17
19
|
},
|
|
18
20
|
"files": [
|
|
19
21
|
"bin",
|
|
@@ -37,7 +39,9 @@
|
|
|
37
39
|
"team-collaboration",
|
|
38
40
|
"discord",
|
|
39
41
|
"obsidian",
|
|
40
|
-
"ai-documentation"
|
|
42
|
+
"ai-documentation",
|
|
43
|
+
"mcp",
|
|
44
|
+
"model-context-protocol"
|
|
41
45
|
],
|
|
42
46
|
"repository": {
|
|
43
47
|
"type": "git",
|
|
@@ -66,9 +70,11 @@
|
|
|
66
70
|
"release:major": "npm version major"
|
|
67
71
|
},
|
|
68
72
|
"dependencies": {
|
|
73
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
69
74
|
"dotenv": "^18.0.8",
|
|
70
75
|
"fast-glob": "^3.3.3",
|
|
71
|
-
"js-yaml": "^4.3.2"
|
|
76
|
+
"js-yaml": "^4.3.2",
|
|
77
|
+
"zod": "3.25.76"
|
|
72
78
|
},
|
|
73
79
|
"optionalDependencies": {
|
|
74
80
|
"@sentry/node": "^10.42.0"
|
package/src/cli.js
CHANGED
|
@@ -246,6 +246,15 @@ async function main() {
|
|
|
246
246
|
|
|
247
247
|
const command = parsed.command;
|
|
248
248
|
|
|
249
|
+
// MCP stdio must start before telemetry and the npm update check.
|
|
250
|
+
// Those both can open the network, and logs must stay off stdout.
|
|
251
|
+
if (command === "mcp") {
|
|
252
|
+
process.env.REPOLENS_LOG_STDERR = "1";
|
|
253
|
+
const { startMcpServer } = await import("./mcp/server.js");
|
|
254
|
+
await startMcpServer();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
249
258
|
// Initialize telemetry (opt-in via REPOLENS_TELEMETRY_ENABLED=true).
|
|
250
259
|
// @sentry/node is optional; init is a no-op when it is not installed.
|
|
251
260
|
await initTelemetry();
|
|
@@ -732,7 +741,7 @@ async function main() {
|
|
|
732
741
|
}
|
|
733
742
|
|
|
734
743
|
error(`Unknown command: ${command}`);
|
|
735
|
-
error("Available commands: init, doctor, migrate, publish, demo, watch, uninstall, feedback, version, help");
|
|
744
|
+
error("Available commands: init, doctor, migrate, publish, demo, watch, uninstall, feedback, mcp, version, help");
|
|
736
745
|
process.exit(EXIT_ERROR);
|
|
737
746
|
}
|
|
738
747
|
|
|
@@ -342,6 +342,18 @@ export function validateConfig(config) {
|
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
// Validate branding (optional). Omitted means the docs badge stays on.
|
|
346
|
+
if (config.branding !== undefined) {
|
|
347
|
+
const branding = config.branding;
|
|
348
|
+
const isBoolean = typeof branding === "boolean";
|
|
349
|
+
const isObject = branding !== null && typeof branding === "object" && !Array.isArray(branding);
|
|
350
|
+
if (!isBoolean && !isObject) {
|
|
351
|
+
errors.push("branding must be a boolean or an object");
|
|
352
|
+
} else if (isObject && branding.badge !== undefined && typeof branding.badge !== "boolean") {
|
|
353
|
+
errors.push("branding.badge must be a boolean");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
345
357
|
// Validate documentation configuration (optional)
|
|
346
358
|
if (config.documentation !== undefined) {
|
|
347
359
|
if (typeof config.documentation !== "object" || Array.isArray(config.documentation)) {
|
|
@@ -431,6 +443,7 @@ const VALID_CONFIG_KEYS = [
|
|
|
431
443
|
"documentation",
|
|
432
444
|
"domains",
|
|
433
445
|
"plugins",
|
|
446
|
+
"branding",
|
|
434
447
|
];
|
|
435
448
|
|
|
436
449
|
/**
|
package/src/core/diff.js
CHANGED
|
@@ -7,6 +7,14 @@ function emptyDiff() {
|
|
|
7
7
|
return { added: [], removed: [], modified: [] };
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* True when `ref` can be passed as one git argument.
|
|
12
|
+
* Rejects flag-like values and shell metacharacters.
|
|
13
|
+
*/
|
|
14
|
+
export function isSafeGitRef(ref) {
|
|
15
|
+
return typeof ref === "string" && ref.length > 0 && !ref.startsWith("-") && SAFE_REF.test(ref);
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
19
|
* Classify one `git diff --name-status` line.
|
|
12
20
|
* Renames and copies (`R100`, `C75`) record the destination path.
|
|
@@ -26,17 +34,27 @@ export function classifyDiffLine(line) {
|
|
|
26
34
|
return { bucket: "modified", file };
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
export function getGitDiff(baseRef = "origin/main", cwd = process.cwd()) {
|
|
30
|
-
if (
|
|
37
|
+
export function getGitDiff(baseRef = "origin/main", cwd = process.cwd(), headRef) {
|
|
38
|
+
if (!isSafeGitRef(baseRef)) {
|
|
31
39
|
warn("git diff skipped: base ref contains unsupported characters.");
|
|
32
40
|
return emptyDiff();
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
const args = ["diff", "--name-status", baseRef];
|
|
44
|
+
if (headRef != null && headRef !== "") {
|
|
45
|
+
if (!isSafeGitRef(headRef)) {
|
|
46
|
+
warn("git diff skipped: head ref contains unsupported characters.");
|
|
47
|
+
return emptyDiff();
|
|
48
|
+
}
|
|
49
|
+
args.push(headRef);
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
let output = "";
|
|
36
53
|
|
|
37
54
|
try {
|
|
38
55
|
// Pipe stderr so a missing repo or ref does not dump git's usage text.
|
|
39
|
-
|
|
56
|
+
// Arguments stay in an array. Refs are checked by isSafeGitRef first.
|
|
57
|
+
output = execFileSync("git", args, {
|
|
40
58
|
encoding: "utf8",
|
|
41
59
|
cwd,
|
|
42
60
|
stdio: ["ignore", "pipe", "pipe"],
|
package/src/delivery/comment.js
CHANGED
|
@@ -92,6 +92,12 @@ async function findExistingRepoLensComment(owner, repo, issueNumber) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export async function upsertPrComment(diff) {
|
|
95
|
+
const flag = process.env.REPOLENS_PR_COMMENT;
|
|
96
|
+
if (flag === "false" || flag === "0") {
|
|
97
|
+
log("REPOLENS_PR_COMMENT is disabled, skipping PR comment.");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
96
102
|
if (eventName !== "pull_request") {
|
|
97
103
|
log("GITHUB_EVENT_NAME is not 'pull_request', skipping PR comment.");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer badge on generated Markdown.
|
|
3
|
+
* On unless config sets `branding: false` or `branding.badge: false`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const DOCS_BADGE_MARKER = "<!-- repolens:docs-badge -->";
|
|
7
|
+
|
|
8
|
+
export const DOCS_BADGE_MARKDOWN =
|
|
9
|
+
"[](https://github.com/CHAPIBUNNY/repolens)";
|
|
10
|
+
|
|
11
|
+
export function brandingEnabled(config) {
|
|
12
|
+
const branding = config?.branding;
|
|
13
|
+
if (branding === false) return false;
|
|
14
|
+
if (branding && typeof branding === "object" && !Array.isArray(branding) && branding.badge === false) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Append the Docs by RepoLens line once. Non-strings are returned unchanged.
|
|
22
|
+
*/
|
|
23
|
+
export function applyBrandingFooter(content, config) {
|
|
24
|
+
if (typeof content !== "string") return content;
|
|
25
|
+
if (!brandingEnabled(config)) return content;
|
|
26
|
+
if (content.includes(DOCS_BADGE_MARKER)) return content;
|
|
27
|
+
const trimmed = content.trimEnd();
|
|
28
|
+
return `${trimmed}\n\n${DOCS_BADGE_MARKER}\n${DOCS_BADGE_MARKDOWN}\n`;
|
|
29
|
+
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "../renderers/renderAnalysis.js";
|
|
35
35
|
import { info, warn } from "../utils/logger.js";
|
|
36
36
|
import path from "node:path";
|
|
37
|
+
import { applyBrandingFooter } from "./branding.js";
|
|
37
38
|
|
|
38
39
|
export async function generateDocumentSet(scanResult, config, diffData = null, pluginManager = null) {
|
|
39
40
|
info("Building structured context for AI...");
|
|
@@ -152,7 +153,7 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
152
153
|
|
|
153
154
|
documents.push({
|
|
154
155
|
...docPlan,
|
|
155
|
-
content,
|
|
156
|
+
content: applyBrandingFooter(content, config),
|
|
156
157
|
generated: new Date().toISOString()
|
|
157
158
|
});
|
|
158
159
|
|
|
@@ -163,7 +164,10 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
163
164
|
// Add placeholder document so publishers know about the failure
|
|
164
165
|
documents.push({
|
|
165
166
|
...docPlan,
|
|
166
|
-
content:
|
|
167
|
+
content: applyBrandingFooter(
|
|
168
|
+
`# ${docPlan.title}\n\n> **Generation Error:** ${error.message}\n\nPlease check the RepoLens logs for details.\n`,
|
|
169
|
+
config
|
|
170
|
+
),
|
|
167
171
|
generated: new Date().toISOString(),
|
|
168
172
|
error: true
|
|
169
173
|
});
|
|
@@ -201,7 +205,7 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
201
205
|
filename: renderer.filename || `${key}.md`,
|
|
202
206
|
category: renderer.category || "custom",
|
|
203
207
|
audience: renderer.audience || ["technical"],
|
|
204
|
-
content,
|
|
208
|
+
content: applyBrandingFooter(content, config),
|
|
205
209
|
generated: new Date().toISOString(),
|
|
206
210
|
});
|
|
207
211
|
|
package/src/help.js
CHANGED
|
@@ -14,6 +14,7 @@ Commands:
|
|
|
14
14
|
watch Watch for file changes and regenerate docs
|
|
15
15
|
uninstall Remove RepoLens-generated files from a repository
|
|
16
16
|
feedback Send feedback to the RepoLens team
|
|
17
|
+
mcp Start the stdio MCP server (also available as repolens-mcp)
|
|
17
18
|
version Print the current RepoLens version
|
|
18
19
|
help Show this help message
|
|
19
20
|
|
|
@@ -49,6 +50,7 @@ Examples:
|
|
|
49
50
|
repolens demo
|
|
50
51
|
repolens watch
|
|
51
52
|
repolens uninstall --target /tmp/my-repo
|
|
53
|
+
repolens mcp
|
|
52
54
|
repolens --version
|
|
53
55
|
`);
|
|
54
56
|
}
|
package/src/init.js
CHANGED
|
@@ -738,6 +738,9 @@ function buildRepoLensConfig(projectName, detectedRoots, options = {}) {
|
|
|
738
738
|
` - notion # Auto-detected: publishes if NOTION_TOKEN is set`,
|
|
739
739
|
` - confluence # Auto-detected: publishes if CONFLUENCE_URL is set`,
|
|
740
740
|
``,
|
|
741
|
+
`# Docs by RepoLens badge on generated Markdown (default: on)`,
|
|
742
|
+
`# branding: false`,
|
|
743
|
+
``,
|
|
741
744
|
`# Optional: Configure Notion publishing behavior`,
|
|
742
745
|
`# notion:`,
|
|
743
746
|
`# branches:`,
|
|
@@ -1390,6 +1393,9 @@ function buildWizardConfig(answers) {
|
|
|
1390
1393
|
for (const p of answers.publishers) {
|
|
1391
1394
|
lines.push(` - ${p}`);
|
|
1392
1395
|
}
|
|
1396
|
+
lines.push(``);
|
|
1397
|
+
lines.push(`# Docs by RepoLens badge on generated Markdown (default: on)`);
|
|
1398
|
+
lines.push(`# branding: false`);
|
|
1393
1399
|
|
|
1394
1400
|
if (answers.publishers.includes("notion") && answers.branches.length) {
|
|
1395
1401
|
lines.push(``);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { callTool } from "./tools.js";
|
|
8
|
+
|
|
9
|
+
const rootField = z.string().optional().describe(
|
|
10
|
+
"Directory to read. Relative or absolute, and it must stay inside the workspace root. Defaults to the workspace root."
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const readOnly = {
|
|
14
|
+
readOnlyHint: true,
|
|
15
|
+
destructiveHint: false,
|
|
16
|
+
idempotentHint: true,
|
|
17
|
+
openWorldHint: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function packageVersion() {
|
|
21
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const pkg = JSON.parse(readFileSync(path.join(here, "../../package.json"), "utf8"));
|
|
23
|
+
return pkg.version || "0.0.0";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Stdio MCP server. Tool handlers live in tools.js so tests can call them
|
|
28
|
+
* without opening a transport. Logs must stay off stdout.
|
|
29
|
+
*/
|
|
30
|
+
export function createMcpServer() {
|
|
31
|
+
const server = new McpServer(
|
|
32
|
+
{ name: "repolens", version: packageVersion() },
|
|
33
|
+
{
|
|
34
|
+
instructions: [
|
|
35
|
+
"RepoLens reads the workspace and returns architecture documentation.",
|
|
36
|
+
"Every path stays inside the workspace root.",
|
|
37
|
+
"Tools are read-only except generate_docs, which writes Markdown inside that root.",
|
|
38
|
+
"Deterministic mode (the default) does not make network calls.",
|
|
39
|
+
].join(" "),
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
server.registerTool(
|
|
44
|
+
"get_system_overview",
|
|
45
|
+
{
|
|
46
|
+
title: "System overview",
|
|
47
|
+
description: "Scan the workspace and return the RepoLens system overview. Read-only. No network calls.",
|
|
48
|
+
inputSchema: { root: rootField },
|
|
49
|
+
annotations: readOnly,
|
|
50
|
+
},
|
|
51
|
+
async (args) => callTool("get_system_overview", args)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
server.registerTool(
|
|
55
|
+
"list_modules",
|
|
56
|
+
{
|
|
57
|
+
title: "List modules",
|
|
58
|
+
description: "Scan the workspace and list detected modules. Read-only. No network calls.",
|
|
59
|
+
inputSchema: { root: rootField },
|
|
60
|
+
annotations: readOnly,
|
|
61
|
+
},
|
|
62
|
+
async (args) => callTool("list_modules", args)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
server.registerTool(
|
|
66
|
+
"get_api_surface",
|
|
67
|
+
{
|
|
68
|
+
title: "API surface and routes",
|
|
69
|
+
description: "Scan the workspace and return the API surface and the route map. Read-only. No network calls.",
|
|
70
|
+
inputSchema: { root: rootField },
|
|
71
|
+
annotations: readOnly,
|
|
72
|
+
},
|
|
73
|
+
async (args) => callTool("get_api_surface", args)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
server.registerTool(
|
|
77
|
+
"get_dependency_graph",
|
|
78
|
+
{
|
|
79
|
+
title: "Dependency graph",
|
|
80
|
+
description: "Scan the workspace and return the import dependency graph. Read-only. No network calls.",
|
|
81
|
+
inputSchema: { root: rootField },
|
|
82
|
+
annotations: readOnly,
|
|
83
|
+
},
|
|
84
|
+
async (args) => callTool("get_dependency_graph", args)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
server.registerTool(
|
|
88
|
+
"get_architecture_diff",
|
|
89
|
+
{
|
|
90
|
+
title: "Architecture diff",
|
|
91
|
+
description: "Return the architecture diff. With only baseRef, compare that ref to the working tree. With baseRef and headRef, run git diff between those two refs. Read-only. No network calls.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
root: rootField,
|
|
94
|
+
baseRef: z.string().optional().describe("Base git ref. Defaults to HEAD. Compared with the working tree when headRef is omitted."),
|
|
95
|
+
headRef: z.string().optional().describe("Optional second git ref. When set, the diff is baseRef...headRef as git diff baseRef headRef."),
|
|
96
|
+
},
|
|
97
|
+
annotations: readOnly,
|
|
98
|
+
},
|
|
99
|
+
async (args) => callTool("get_architecture_diff", args)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
server.registerTool(
|
|
103
|
+
"generate_docs",
|
|
104
|
+
{
|
|
105
|
+
title: "Generate docs",
|
|
106
|
+
description: "Generate RepoLens Markdown into a directory inside the workspace. Deterministic by default, which disables AI and does not make network calls. This is the only tool that writes files.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
root: rootField,
|
|
109
|
+
outputDir: z.string().optional().describe("Output directory relative to the repository. Must stay inside the repository. Defaults to .repolens or documentation.output_dir."),
|
|
110
|
+
deterministic: z.boolean().optional().describe("Defaults to true. When true, AI stays off and this call does not use the network."),
|
|
111
|
+
},
|
|
112
|
+
annotations: {
|
|
113
|
+
readOnlyHint: false,
|
|
114
|
+
destructiveHint: false,
|
|
115
|
+
idempotentHint: true,
|
|
116
|
+
openWorldHint: false,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
async (args) => callTool("generate_docs", args)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return server;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function startMcpServer() {
|
|
126
|
+
process.env.REPOLENS_LOG_STDERR = "1";
|
|
127
|
+
const server = createMcpServer();
|
|
128
|
+
const transport = new StdioServerTransport();
|
|
129
|
+
await server.connect(transport);
|
|
130
|
+
}
|
package/src/mcp/tools.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadConfig } from "../core/config.js";
|
|
4
|
+
import { scanRepo } from "../core/scan.js";
|
|
5
|
+
import { getGitDiff, isSafeGitRef } from "../core/diff.js";
|
|
6
|
+
import { analyzeDependencyGraph } from "../analyzers/dependency-graph.js";
|
|
7
|
+
import {
|
|
8
|
+
renderSystemOverview,
|
|
9
|
+
renderModuleCatalog,
|
|
10
|
+
renderApiSurface,
|
|
11
|
+
renderRouteMap,
|
|
12
|
+
} from "../renderers/render.js";
|
|
13
|
+
import { renderDependencyGraph } from "../renderers/renderAnalysis.js";
|
|
14
|
+
import { renderArchitectureDiff } from "../renderers/renderDiff.js";
|
|
15
|
+
import { generateDocumentSet } from "../docs/generate-doc-set.js";
|
|
16
|
+
import { writeDocumentSet } from "../docs/write-doc-set.js";
|
|
17
|
+
import { resolveDocsOutputDir, resolveInsideWorkspace, resolveWorkspaceRoot } from "./workspace.js";
|
|
18
|
+
|
|
19
|
+
const MAX_TEXT = 80_000;
|
|
20
|
+
|
|
21
|
+
export function asToolText(text) {
|
|
22
|
+
const body = String(text ?? "");
|
|
23
|
+
if (body.length <= MAX_TEXT) return body;
|
|
24
|
+
return `${body.slice(0, MAX_TEXT)}\n\n[truncated ${body.length - MAX_TEXT} characters]`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ok(text) {
|
|
28
|
+
return { content: [{ type: "text", text: asToolText(text) }] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function fail(error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function projectRootFromArgs(args) {
|
|
37
|
+
const workspace = resolveWorkspaceRoot();
|
|
38
|
+
return resolveInsideWorkspace(workspace, args?.root, "root");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defaultConfig(root) {
|
|
42
|
+
return {
|
|
43
|
+
configVersion: 1,
|
|
44
|
+
project: { name: path.basename(root) || "repository" },
|
|
45
|
+
publishers: ["markdown"],
|
|
46
|
+
scan: {
|
|
47
|
+
include: ["**/*.{js,ts,jsx,tsx,mjs,cjs,py,go,rs,java,rb,php,cs,swift,kt}"],
|
|
48
|
+
ignore: [
|
|
49
|
+
"**/node_modules/**",
|
|
50
|
+
"**/dist/**",
|
|
51
|
+
"**/build/**",
|
|
52
|
+
"**/.git/**",
|
|
53
|
+
"**/vendor/**",
|
|
54
|
+
"**/target/**",
|
|
55
|
+
"**/__pycache__/**",
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
module_roots: ["src", "lib", "app", "packages"],
|
|
59
|
+
outputs: {
|
|
60
|
+
pages: [
|
|
61
|
+
{ key: "system_overview", title: "System Overview" },
|
|
62
|
+
{ key: "module_catalog", title: "Module Catalog" },
|
|
63
|
+
{ key: "api_surface", title: "API Surface" },
|
|
64
|
+
{ key: "route_map", title: "Route Map" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
documentation: { output_dir: ".repolens", include_artifacts: false },
|
|
68
|
+
ai: { enabled: false, mode: "off" },
|
|
69
|
+
branding: true,
|
|
70
|
+
__repoRoot: root,
|
|
71
|
+
__configPath: path.join(root, ".repolens.yml"),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function prepareConfig(root, { deterministic = true, outputDir } = {}) {
|
|
76
|
+
const configPath = path.join(root, ".repolens.yml");
|
|
77
|
+
let cfg;
|
|
78
|
+
try {
|
|
79
|
+
await fs.access(configPath);
|
|
80
|
+
cfg = await loadConfig(configPath);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error && error.code === "ENOENT") {
|
|
83
|
+
cfg = defaultConfig(root);
|
|
84
|
+
} else {
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
cfg.__repoRoot = root;
|
|
90
|
+
if (deterministic) {
|
|
91
|
+
cfg.ai = { ...(cfg.ai || {}), enabled: false, mode: "off" };
|
|
92
|
+
}
|
|
93
|
+
if (outputDir != null && outputDir !== "") {
|
|
94
|
+
cfg.documentation = {
|
|
95
|
+
...(cfg.documentation || {}),
|
|
96
|
+
output_dir: resolveDocsOutputDir(root, outputDir),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return cfg;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Drop REPOLENS_AI_ENABLED for one call so deterministic mode cannot
|
|
104
|
+
* reach an AI provider. The previous value is restored afterwards.
|
|
105
|
+
*/
|
|
106
|
+
export async function runDeterministic(fn) {
|
|
107
|
+
const previous = process.env.REPOLENS_AI_ENABLED;
|
|
108
|
+
delete process.env.REPOLENS_AI_ENABLED;
|
|
109
|
+
try {
|
|
110
|
+
return await fn();
|
|
111
|
+
} finally {
|
|
112
|
+
if (previous === undefined) delete process.env.REPOLENS_AI_ENABLED;
|
|
113
|
+
else process.env.REPOLENS_AI_ENABLED = previous;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function scanned(args) {
|
|
118
|
+
const root = projectRootFromArgs(args);
|
|
119
|
+
const cfg = await prepareConfig(root, { deterministic: true });
|
|
120
|
+
const scan = await runDeterministic(() => scanRepo(cfg));
|
|
121
|
+
return { root, cfg, scan };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function getSystemOverview(args = {}) {
|
|
125
|
+
const { cfg, scan } = await scanned(args);
|
|
126
|
+
return renderSystemOverview(cfg, scan);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function listModules(args = {}) {
|
|
130
|
+
const { cfg, scan } = await scanned(args);
|
|
131
|
+
return renderModuleCatalog(cfg, scan);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function getApiSurface(args = {}) {
|
|
135
|
+
const { cfg, scan } = await scanned(args);
|
|
136
|
+
return `${renderApiSurface(cfg, scan)}\n\n${renderRouteMap(cfg, scan)}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function getDependencyGraph(args = {}) {
|
|
140
|
+
const { root, scan } = await scanned(args);
|
|
141
|
+
const graph = await runDeterministic(() => analyzeDependencyGraph(scan._files || [], root));
|
|
142
|
+
return renderDependencyGraph(graph);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function getArchitectureDiff(args = {}) {
|
|
146
|
+
const root = projectRootFromArgs(args);
|
|
147
|
+
const baseRef = args.baseRef == null || args.baseRef === "" ? "HEAD" : args.baseRef;
|
|
148
|
+
const headRef = args.headRef == null || args.headRef === "" ? undefined : args.headRef;
|
|
149
|
+
if (!isSafeGitRef(baseRef)) {
|
|
150
|
+
throw new Error("baseRef contains unsupported characters");
|
|
151
|
+
}
|
|
152
|
+
if (headRef !== undefined && !isSafeGitRef(headRef)) {
|
|
153
|
+
throw new Error("headRef contains unsupported characters");
|
|
154
|
+
}
|
|
155
|
+
const raw = getGitDiff(baseRef, root, headRef);
|
|
156
|
+
return renderArchitectureDiff(raw);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function generateDocs(args = {}) {
|
|
160
|
+
const root = projectRootFromArgs(args);
|
|
161
|
+
const deterministic = args.deterministic !== false;
|
|
162
|
+
const outputDir = args.outputDir;
|
|
163
|
+
|
|
164
|
+
const run = async () => {
|
|
165
|
+
const cfg = await prepareConfig(root, { deterministic, outputDir });
|
|
166
|
+
const scan = await scanRepo(cfg);
|
|
167
|
+
const raw = getGitDiff("HEAD", root);
|
|
168
|
+
const docSet = await generateDocumentSet(scan, cfg, raw);
|
|
169
|
+
const written = await writeDocumentSet(docSet, root);
|
|
170
|
+
const rel = path.relative(root, written.outputDir) || ".";
|
|
171
|
+
const lines = [
|
|
172
|
+
`Wrote ${written.documentCount} documents to ${rel}`,
|
|
173
|
+
...written.files.map((file) => `- ${file}`),
|
|
174
|
+
];
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (deterministic) return runDeterministic(run);
|
|
179
|
+
return run();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const HANDLERS = {
|
|
183
|
+
get_system_overview: getSystemOverview,
|
|
184
|
+
list_modules: listModules,
|
|
185
|
+
get_api_surface: getApiSurface,
|
|
186
|
+
get_dependency_graph: getDependencyGraph,
|
|
187
|
+
get_architecture_diff: getArchitectureDiff,
|
|
188
|
+
generate_docs: generateDocs,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const TOOL_NAMES = Object.keys(HANDLERS);
|
|
192
|
+
|
|
193
|
+
export async function callTool(name, args) {
|
|
194
|
+
const handler = HANDLERS[name];
|
|
195
|
+
if (!handler) return fail(new Error(`Unknown tool: ${name}`));
|
|
196
|
+
try {
|
|
197
|
+
const text = await handler(args || {});
|
|
198
|
+
return ok(text);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return fail(error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { isInsideRoot, resolveOutputDir } from "../utils/safe-path.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Workspace root for the MCP server.
|
|
6
|
+
* REPOLENS_WORKSPACE wins over the process working directory.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveWorkspaceRoot(explicit) {
|
|
9
|
+
const candidate = explicit || process.env.REPOLENS_WORKSPACE || process.cwd();
|
|
10
|
+
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
11
|
+
throw new Error("Workspace root must be a path string");
|
|
12
|
+
}
|
|
13
|
+
if (candidate.includes("\0")) {
|
|
14
|
+
throw new Error("Workspace root contains a null byte");
|
|
15
|
+
}
|
|
16
|
+
return path.resolve(candidate);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve `requested` and require it to stay inside `workspaceRoot`.
|
|
21
|
+
* An empty value is the workspace root itself.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveInsideWorkspace(workspaceRoot, requested, label = "path") {
|
|
24
|
+
const root = path.resolve(workspaceRoot);
|
|
25
|
+
if (!isInsideRoot(root, root)) {
|
|
26
|
+
throw new Error("Workspace root is not a directory path");
|
|
27
|
+
}
|
|
28
|
+
if (requested == null || requested === "" || requested === ".") {
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
if (typeof requested !== "string") {
|
|
32
|
+
throw new Error(`${label} must be a string`);
|
|
33
|
+
}
|
|
34
|
+
if (requested.includes("\0")) {
|
|
35
|
+
throw new Error(`${label} contains a null byte`);
|
|
36
|
+
}
|
|
37
|
+
const resolved = path.resolve(root, requested);
|
|
38
|
+
if (!isInsideRoot(root, resolved)) {
|
|
39
|
+
throw new Error(`${label} escapes the workspace: ${requested}`);
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate a documentation output directory and return the relative
|
|
46
|
+
* path `resolveOutputDir` will accept later.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveDocsOutputDir(projectRoot, outputDir) {
|
|
49
|
+
const relative = outputDir == null || outputDir === "" ? ".repolens" : outputDir;
|
|
50
|
+
resolveOutputDir(projectRoot, relative);
|
|
51
|
+
return relative;
|
|
52
|
+
}
|
package/src/utils/logger.js
CHANGED
|
@@ -69,9 +69,14 @@ function sanitizeArgs(args) {
|
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function logSink() {
|
|
73
|
+
// The MCP stdio transport owns stdout. Logs go to stderr in that mode.
|
|
74
|
+
return process.env.REPOLENS_LOG_STDERR === "1" ? console.error : console.log;
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
export function log(...args) {
|
|
73
78
|
if (!isTest && isVerbose) {
|
|
74
|
-
|
|
79
|
+
logSink()("[RepoLens]", ...sanitizeArgs(args));
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
@@ -81,13 +86,13 @@ export function log(...args) {
|
|
|
81
86
|
*/
|
|
82
87
|
export function verbose(...args) {
|
|
83
88
|
if (!isTest && isVerbose) {
|
|
84
|
-
|
|
89
|
+
logSink()("[RepoLens]", ...sanitizeArgs(args));
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
export function info(...args) {
|
|
89
94
|
if (!isTest) {
|
|
90
|
-
|
|
95
|
+
logSink()("[RepoLens]", ...sanitizeArgs(args));
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
|