@chappibunny/repolens 1.14.3 → 1.15.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 +43 -0
- package/README.md +24 -11
- package/bin/repolens-mcp.js +1 -1
- package/bin/repolens.js +8 -5
- package/llms-install.md +2 -0
- package/package.json +1 -1
- package/src/analyzers/complexity-analyzer.js +149 -10
- package/src/cli-args.js +1 -0
- package/src/cli.js +67 -9
- package/src/core/diff.js +15 -1
- package/src/core/languages.js +268 -0
- package/src/core/scan.js +11 -6
- package/src/doctor.js +24 -3
- package/src/help.js +257 -10
- package/src/init.js +49 -82
- package/src/publishers/index.js +33 -0
- package/src/renderers/renderAnalysis.js +2 -2
- package/src/utils/banner.js +9 -0
- package/src/utils/invocation.js +54 -0
- package/src/watch.js +43 -26
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RepoLens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.15.1
|
|
6
|
+
|
|
7
|
+
### Watch and code health
|
|
8
|
+
|
|
9
|
+
- `watch` exits 2 when `scan.include` matches no files, including after `init --quick` in an empty directory. It prints a hint to fix `scan.include` and `module_roots` and does not wait for changes
|
|
10
|
+
- Code health analyzes Python (`.py`, `.pyw`) and Go (`.go`) as well as JavaScript and TypeScript. A Python or Go repository no longer logs `Code health: no source files analyzed` when source files were scanned. Test modules (`test_*.py`, `*_test.py`, `*_test.go`, and `tests/`) are skipped
|
|
11
|
+
|
|
12
|
+
Exit codes stay 0 success, 1 runtime error or unknown command, and 2 validation failure (bad flags, missing config, missing target, doctor failure, 0 files scanned, nothing to watch, `--strict` credentials, feedback without a terminal).
|
|
13
|
+
|
|
14
|
+
## 1.15.0
|
|
15
|
+
|
|
16
|
+
### CLI usability
|
|
17
|
+
|
|
18
|
+
- `init --quick` detects the repository language (Python, Go, Rust, Java/Kotlin, JavaScript/TypeScript, Ruby, PHP, C#, Swift, C/C++, and mixes of those) and writes matching `scan.include` globs and `module_roots`. It no longer defaults to JavaScript-only patterns such as `src/**/*.{ts,tsx,js,jsx,md}`
|
|
19
|
+
- `init --quick` enables the Markdown publisher only. The interactive wizard can still add Notion, Confluence, GitHub Wiki, and Obsidian
|
|
20
|
+
- `doctor` fails when `scan.include` matches 0 files
|
|
21
|
+
- `demo` and `publish` print a warning and exit 2 when 0 files are scanned. They do not write empty documentation
|
|
22
|
+
- `repolens <command> --help` prints that command's usage and flags for init, doctor, migrate, publish, demo, watch, uninstall, feedback, and mcp
|
|
23
|
+
- `--target` is checked before any scan or publish. A missing path exits 2 with `Target directory not found: <path>`
|
|
24
|
+
- A directory that is not a git repository is reported as `not a git repository`. Code health does not claim an average of 100/100 across 0 files
|
|
25
|
+
- `watch` exits 2 when none of the `module_roots` directories exist
|
|
26
|
+
- `publish --strict` exits 2 when a configured publisher is missing credentials
|
|
27
|
+
- `repolens` with no arguments prints help and does not publish
|
|
28
|
+
- The ASCII banner is omitted when stdout is not a terminal or `CI` is set
|
|
29
|
+
- Init's next-steps text uses `repolens publish` when the CLI is installed globally, and `npx @chappibunny/repolens publish` otherwise
|
|
30
|
+
|
|
31
|
+
Exit codes stay 0 success, 1 runtime error or unknown command, and 2 validation failure (bad flags, missing config, missing target, doctor failure, 0 files scanned, nothing to watch, `--strict` credentials, feedback without a terminal).
|
|
32
|
+
|
|
33
|
+
## Unreleased
|
|
34
|
+
|
|
35
|
+
### Release workflow
|
|
36
|
+
|
|
37
|
+
- Before publishing, the release workflow queries `npm view <pkg>@<ver> version --prefer-online` up to three times, waiting 10 seconds between tries, and skips `npm publish` when that version is already on npm
|
|
38
|
+
- If `npm publish` fails, the same check runs again. The step passes when the version is live
|
|
39
|
+
- The GitHub Release uploads `chappibunny-repolens-*.tgz`, which is the tarball name `npm pack` writes for `@chappibunny/repolens`
|
|
40
|
+
|
|
41
|
+
## Unreleased
|
|
42
|
+
|
|
43
|
+
### Docs
|
|
44
|
+
|
|
45
|
+
- README VS Code badge switched to a static shields.io badge (the visual-studio-marketplace badges were retired)
|
|
46
|
+
- New `assets/social-preview.png` (1280x640, rendered from HTML with headless Chrome)
|
|
47
|
+
|
|
5
48
|
## 1.14.3
|
|
6
49
|
|
|
7
50
|
### Docs
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
[](https://www.npmjs.com/package/@chappibunny/repolens)
|
|
12
|
-
[](https://marketplace.visualstudio.com/items?itemName=repolensdox.repolensdox)
|
|
13
13
|
[](https://github.com/CHAPIBUNNY/repolens/actions/workflows/test.yml)
|
|
14
14
|
[](LICENSE)
|
|
15
15
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.CHAPIBUNNY/repolens)
|
|
@@ -29,7 +29,7 @@ RepoLens scans your repository, generates living architecture documentation, and
|
|
|
29
29
|
|
|
30
30
|
## 🎬 Demo
|
|
31
31
|
|
|
32
|
-
> **Try it now** — no installation required.
|
|
32
|
+
> **Try it now** — no installation required. `npx @chappibunny/repolens demo` previews the repository you point it at. It detects common languages (Python, Go, Rust, Java, JavaScript/TypeScript, and others). If the scan matches no source files, demo exits 2 and does not write empty docs. Code health scores JavaScript/TypeScript, Python, and Go.
|
|
33
33
|
|
|
34
34
|
**[See it on real repos](https://chapibunny.github.io/repolens/)** — architecture pages RepoLens generated for Express, FastAPI, Django, Gin, mdBook, and other public projects. Deterministic mode, no API keys.
|
|
35
35
|
|
|
@@ -173,21 +173,34 @@ Step-by-step setup for publishers, AI features, Notion, Confluence, GitHub Wiki,
|
|
|
173
173
|
|
|
174
174
|
| Command | Description |
|
|
175
175
|
|---|---|
|
|
176
|
+
| `npx @chappibunny/repolens` | Show help. A bare invocation does not publish |
|
|
176
177
|
| `npx @chappibunny/repolens init` | **Interactive wizard** — configure publishers, AI, credentials |
|
|
177
|
-
| `npx @chappibunny/repolens init --quick` |
|
|
178
|
+
| `npx @chappibunny/repolens init --quick` | Markdown-only setup. Globs follow the detected language |
|
|
179
|
+
| `npx @chappibunny/repolens init --help` | Flags for that command. Same pattern for every command below |
|
|
178
180
|
| `npx @chappibunny/repolens publish` | Scan, generate, and publish documentation |
|
|
179
|
-
| `npx @chappibunny/repolens
|
|
180
|
-
| `npx @chappibunny/repolens
|
|
181
|
-
| `npx @chappibunny/repolens
|
|
181
|
+
| `npx @chappibunny/repolens publish --strict` | Exit 2 when a configured publisher is missing credentials |
|
|
182
|
+
| `npx @chappibunny/repolens demo` | Local preview — no API keys. Exits 2 when 0 files match |
|
|
183
|
+
| `npx @chappibunny/repolens doctor` | Validate your setup, including that scan globs match files |
|
|
184
|
+
| `npx @chappibunny/repolens watch` | Auto-regenerate docs on file changes. Exits 2 if `module_roots` are missing or `scan.include` matches no files |
|
|
182
185
|
| `npx @chappibunny/repolens migrate` | Upgrade from v0.3.0 workflows ([details](docs/MIGRATION.md)) |
|
|
183
186
|
| `npx @chappibunny/repolens uninstall` | Remove all RepoLens files (config, docs, workflow) |
|
|
184
187
|
| `npx @chappibunny/repolens uninstall --force` | Remove without confirmation prompt |
|
|
185
188
|
| `npx @chappibunny/repolens feedback` | Send feedback to the team |
|
|
186
189
|
| `npx @chappibunny/repolens mcp` | Start the stdio MCP server (`repolens-mcp` is the same server) |
|
|
187
190
|
|
|
188
|
-
> **Note:** If you installed globally with `npm install -g @chappibunny/repolens`, you can use the shorter `repolens <command>` form.
|
|
191
|
+
> **Note:** If you installed globally with `npm install -g @chappibunny/repolens`, you can use the shorter `repolens <command>` form. Init's next-steps text uses that shorter form when the command on your PATH is the global install.
|
|
189
192
|
> **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`.
|
|
190
193
|
|
|
194
|
+
### Exit codes
|
|
195
|
+
|
|
196
|
+
| Code | Meaning |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `0` | Success, including a cancelled wizard or help with no other work |
|
|
199
|
+
| `1` | Runtime error, or an unknown command |
|
|
200
|
+
| `2` | Invalid flags, missing config, target directory not found, a failed `doctor` check (including 0 files matched), `demo` or `publish` scanned 0 files, `watch` has nothing to watch (missing `module_roots`, or `scan.include` matches no files), `publish --strict` is missing publisher credentials, or `feedback` has no terminal |
|
|
201
|
+
|
|
202
|
+
`--target` must already exist. A missing path exits 2 with `Target directory not found: <path>` before any scan. `repolens <command> --help` prints that command's flags. The ASCII banner is omitted when stdout is not a terminal or `CI` is set.
|
|
203
|
+
|
|
191
204
|
---
|
|
192
205
|
|
|
193
206
|
## MCP server
|
|
@@ -224,7 +237,7 @@ If `npx` reports `Unknown command: mcp`, an `.npmrc` with `min-release-age` (or
|
|
|
224
237
|
|
|
225
238
|
## GitHub Action
|
|
226
239
|
|
|
227
|
-
`action.yml` is a composite action named **RepoLens Architecture Docs**. It runs the published npm package (`npx --yes @chappibunny/repolens@1.
|
|
240
|
+
`action.yml` is a composite action named **RepoLens Architecture Docs**. It runs the published npm package (`npx --yes @chappibunny/repolens@1.15.1`). Inputs are `command` (`publish`, `demo`, or `doctor`), `config`, `node-version`, `pr-comment`, and `version`.
|
|
228
241
|
|
|
229
242
|
```yaml
|
|
230
243
|
name: Architecture docs
|
|
@@ -240,7 +253,7 @@ jobs:
|
|
|
240
253
|
runs-on: ubuntu-latest
|
|
241
254
|
steps:
|
|
242
255
|
- uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
|
|
243
|
-
- uses: CHAPIBUNNY/repolens@v1.
|
|
256
|
+
- uses: CHAPIBUNNY/repolens@v1.15.1
|
|
244
257
|
with:
|
|
245
258
|
command: publish
|
|
246
259
|
config: .repolens.yml
|
|
@@ -248,7 +261,7 @@ jobs:
|
|
|
248
261
|
pr-comment: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
|
|
249
262
|
```
|
|
250
263
|
|
|
251
|
-
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.
|
|
264
|
+
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.15.1` has to exist before `uses: CHAPIBUNNY/repolens@v1.15.1` resolves, and `@chappibunny/repolens@1.15.1` has to be on npm before the action can install it.
|
|
252
265
|
|
|
253
266
|
## Add the badge to your repo
|
|
254
267
|
|
|
@@ -268,7 +281,7 @@ Generated docs also link to the [gallery](https://chapibunny.github.io/repolens/
|
|
|
268
281
|
|
|
269
282
|
Published system maps are Unicode diagrams drawn from import edges. RepoLens does not emit Mermaid.
|
|
270
283
|
|
|
271
|
-
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`. 1.14.2 adds the gallery link to the badge line, `glama.json`, and `CITATION.cff`. 1.14.3 adds Cline setup docs and `llms-install.md`. Layout and counts are in [ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
284
|
+
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`. 1.14.2 adds the gallery link to the badge line, `glama.json`, and `CITATION.cff`. 1.14.3 adds Cline setup docs and `llms-install.md`. 1.15.0 detects repository languages in `init --quick`, refuses empty scans, and adds per-command help. 1.15.1 makes `watch` exit 2 when those globs match nothing, and scores code health for Python and Go as well as JavaScript and TypeScript. Layout and counts are in [ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
272
285
|
|
|
273
286
|
### System Overview (Technical Profile)
|
|
274
287
|
|
package/bin/repolens-mcp.js
CHANGED
package/bin/repolens.js
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
|
|
4
4
|
const argv = process.argv.slice(2);
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const command = argv.find((arg) => !arg.startsWith("-"));
|
|
6
|
+
const wantsHelp = argv.includes("--help") || argv.includes("-h") || command === "help";
|
|
7
|
+
const wantsVersion = argv.includes("--version") || argv.includes("-v") || command === "version";
|
|
7
8
|
|
|
8
|
-
// --version
|
|
9
|
+
// --version, --help, and a bare invocation must not load the scanner,
|
|
10
|
+
// publishers, or telemetry. `repolens <command> --help` prints that command.
|
|
9
11
|
if (wantsVersion && !wantsHelp) {
|
|
10
12
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
13
|
console.log(pkg.version || "0.0.0");
|
|
12
14
|
process.exit(0);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
if (wantsHelp) {
|
|
17
|
+
if (argv.length === 0 || wantsHelp) {
|
|
16
18
|
const { printHelp } = await import("../src/help.js");
|
|
17
|
-
|
|
19
|
+
const topic = wantsHelp && command && command !== "help" ? command : undefined;
|
|
20
|
+
printHelp(topic);
|
|
18
21
|
process.exit(0);
|
|
19
22
|
}
|
|
20
23
|
|
package/llms-install.md
CHANGED
|
@@ -11,3 +11,5 @@
|
|
|
11
11
|
4. No API keys are needed. Tools run deterministically and offline unless `generate_docs` is called with `deterministic: false`.
|
|
12
12
|
5. Verify: the server should list six tools — `get_system_overview`, `list_modules`, `get_api_surface`, `get_dependency_graph`, `get_architecture_diff`, `generate_docs`. Call `get_system_overview` with no arguments.
|
|
13
13
|
6. If you see `Unknown command: mcp`, an older release was resolved (often an `.npmrc` with `min-release-age`). Use `@chappibunny/repolens@latest`.
|
|
14
|
+
7. CLI (1.15.0): `repolens` with no arguments prints help. `repolens <command> --help` prints that command's flags. `init --quick` writes language-specific globs and Markdown only. `demo` and `publish` exit 2 when zero source files match, instead of writing empty docs. `publish --strict` exits 2 when publisher credentials are missing.
|
|
15
|
+
8. CLI (1.15.1): `watch` exits 2 when `scan.include` matches no files (it does not print `Waiting for changes...` and hang). Code health scores Python and Go as well as JavaScript and TypeScript.
|
package/package.json
CHANGED
|
@@ -6,7 +6,17 @@ import fs from "node:fs/promises";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { info } from "../utils/logger.js";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const LANGUAGE_BY_EXTENSION = {
|
|
10
|
+
".js": "javascript",
|
|
11
|
+
".mjs": "javascript",
|
|
12
|
+
".cjs": "javascript",
|
|
13
|
+
".ts": "javascript",
|
|
14
|
+
".tsx": "javascript",
|
|
15
|
+
".jsx": "javascript",
|
|
16
|
+
".py": "python",
|
|
17
|
+
".pyw": "python",
|
|
18
|
+
".go": "go",
|
|
19
|
+
};
|
|
10
20
|
|
|
11
21
|
// Skip test files, build outputs, etc.
|
|
12
22
|
const SKIP_PATTERNS = [
|
|
@@ -19,6 +29,10 @@ const SKIP_PATTERNS = [
|
|
|
19
29
|
/\.min\.[jt]s$/,
|
|
20
30
|
/\.d\.ts$/,
|
|
21
31
|
/\.repolens\//,
|
|
32
|
+
/__pycache__\//,
|
|
33
|
+
/(?:^|\/)test_[^/]*\.py$/,
|
|
34
|
+
/_test\.py$/,
|
|
35
|
+
/_test\.go$/,
|
|
22
36
|
];
|
|
23
37
|
|
|
24
38
|
function shouldSkip(filePath) {
|
|
@@ -66,6 +80,100 @@ export function computeComplexity(source) {
|
|
|
66
80
|
return complexity;
|
|
67
81
|
}
|
|
68
82
|
|
|
83
|
+
function blankExceptNewlines(match) {
|
|
84
|
+
return match.replace(/[^\n]/g, " ");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stripPythonNoise(source) {
|
|
88
|
+
let stripped = source.replace(/"""[\s\S]*?"""/g, blankExceptNewlines);
|
|
89
|
+
stripped = stripped.replace(/'''[\s\S]*?'''/g, blankExceptNewlines);
|
|
90
|
+
stripped = stripped.replace(/"(?:[^"\\\n]|\\.)*"/g, '""');
|
|
91
|
+
stripped = stripped.replace(/'(?:[^'\\\n]|\\.)*'/g, "''");
|
|
92
|
+
stripped = stripped.replace(/#.*$/gm, "");
|
|
93
|
+
return stripped;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function stripGoNoise(source) {
|
|
97
|
+
let stripped = source.replace(/`[^`]*`/g, blankExceptNewlines);
|
|
98
|
+
stripped = stripped.replace(/"(?:[^"\\\n]|\\.)*"/g, '""');
|
|
99
|
+
stripped = stripped.replace(/'(?:[^'\\\n]|\\.)*'/g, "''");
|
|
100
|
+
stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, blankExceptNewlines);
|
|
101
|
+
stripped = stripped.replace(/\/\/.*$/gm, "");
|
|
102
|
+
return stripped;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Cyclomatic complexity for Python. Regex approximation: if, elif, for,
|
|
107
|
+
* while, except, and, or. Comments and strings are ignored.
|
|
108
|
+
*/
|
|
109
|
+
export function computePythonComplexity(source) {
|
|
110
|
+
const stripped = stripPythonNoise(source);
|
|
111
|
+
let complexity = 1;
|
|
112
|
+
const patterns = [
|
|
113
|
+
/\bif\b/g,
|
|
114
|
+
/\belif\b/g,
|
|
115
|
+
/\bfor\b/g,
|
|
116
|
+
/\bwhile\b/g,
|
|
117
|
+
/\bexcept\b/g,
|
|
118
|
+
/\band\b/g,
|
|
119
|
+
/\bor\b/g,
|
|
120
|
+
];
|
|
121
|
+
for (const pattern of patterns) {
|
|
122
|
+
const matches = stripped.match(pattern);
|
|
123
|
+
if (matches) complexity += matches.length;
|
|
124
|
+
}
|
|
125
|
+
return complexity;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Cyclomatic complexity for Go. Regex approximation: if, for, case
|
|
130
|
+
* (switch and select branches), &&, and ||. Comments and strings are ignored.
|
|
131
|
+
*/
|
|
132
|
+
export function computeGoComplexity(source) {
|
|
133
|
+
const stripped = stripGoNoise(source);
|
|
134
|
+
let complexity = 1;
|
|
135
|
+
const patterns = [
|
|
136
|
+
/\bif\b/g,
|
|
137
|
+
/\bfor\b/g,
|
|
138
|
+
/\bcase\b/g,
|
|
139
|
+
/&&/g,
|
|
140
|
+
/\|\|/g,
|
|
141
|
+
];
|
|
142
|
+
for (const pattern of patterns) {
|
|
143
|
+
const matches = stripped.match(pattern);
|
|
144
|
+
if (matches) complexity += matches.length;
|
|
145
|
+
}
|
|
146
|
+
return complexity;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function languageForFile(file) {
|
|
150
|
+
return LANGUAGE_BY_EXTENSION[path.extname(file).toLowerCase()] || null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function complexityForLanguage(language, source) {
|
|
154
|
+
if (language === "python") return computePythonComplexity(source);
|
|
155
|
+
if (language === "go") return computeGoComplexity(source);
|
|
156
|
+
return computeComplexity(source);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function collectFunctions(source, pattern, nameGroup, complexityFn) {
|
|
160
|
+
const functions = [];
|
|
161
|
+
let match;
|
|
162
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
163
|
+
const startLine = source.slice(0, match.index).split("\n").length;
|
|
164
|
+
functions.push({ name: match[nameGroup], line: startLine, index: match.index });
|
|
165
|
+
}
|
|
166
|
+
functions.sort((a, b) => a.index - b.index);
|
|
167
|
+
return functions.map((fn, i) => {
|
|
168
|
+
const end = i + 1 < functions.length ? functions[i + 1].index : source.length;
|
|
169
|
+
return {
|
|
170
|
+
name: fn.name,
|
|
171
|
+
line: fn.line,
|
|
172
|
+
complexity: complexityFn(source.slice(fn.index, end)),
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
69
177
|
/**
|
|
70
178
|
* Extract function-level complexity from source code.
|
|
71
179
|
* Returns per-function complexity for the top-level functions.
|
|
@@ -110,24 +218,50 @@ export function extractFunctions(source) {
|
|
|
110
218
|
return results;
|
|
111
219
|
}
|
|
112
220
|
|
|
221
|
+
/** Python `def` / `async def` names and their approximate complexity. */
|
|
222
|
+
export function extractPythonFunctions(source) {
|
|
223
|
+
return collectFunctions(
|
|
224
|
+
source,
|
|
225
|
+
/^[ \t]*(?:async[ \t]+)?def[ \t]+(\w+)[ \t]*\(/gm,
|
|
226
|
+
1,
|
|
227
|
+
computePythonComplexity,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Go `func Name` and `func (recv) Name` declarations. */
|
|
232
|
+
export function extractGoFunctions(source) {
|
|
233
|
+
return collectFunctions(
|
|
234
|
+
source,
|
|
235
|
+
/^func[ \t]+(?:\([^)\n]*\)[ \t]+)?(\w+)[ \t]*\(/gm,
|
|
236
|
+
1,
|
|
237
|
+
computeGoComplexity,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function functionsForLanguage(language, source) {
|
|
242
|
+
if (language === "python") return extractPythonFunctions(source);
|
|
243
|
+
if (language === "go") return extractGoFunctions(source);
|
|
244
|
+
return extractFunctions(source);
|
|
245
|
+
}
|
|
246
|
+
|
|
113
247
|
/**
|
|
114
|
-
* Analyze complexity for
|
|
248
|
+
* Analyze complexity for JavaScript/TypeScript, Python, and Go files.
|
|
115
249
|
* @param {string[]} files - File paths relative to repoRoot
|
|
116
250
|
* @param {string} repoRoot - Absolute path to repo root
|
|
117
251
|
* @returns {Promise<Object>} Complexity analysis results
|
|
118
252
|
*/
|
|
119
253
|
export async function analyzeComplexity(files, repoRoot) {
|
|
120
|
-
const
|
|
121
|
-
.filter(f =>
|
|
254
|
+
const sourceFiles = files
|
|
255
|
+
.filter(f => languageForFile(f))
|
|
122
256
|
.filter(f => !shouldSkip(f));
|
|
123
257
|
|
|
124
258
|
const result = {
|
|
125
259
|
files: [],
|
|
126
260
|
functions: [],
|
|
127
|
-
filesAnalyzed:
|
|
261
|
+
filesAnalyzed: sourceFiles.length,
|
|
128
262
|
};
|
|
129
263
|
|
|
130
|
-
for (const file of
|
|
264
|
+
for (const file of sourceFiles) {
|
|
131
265
|
let content;
|
|
132
266
|
try {
|
|
133
267
|
content = await fs.readFile(path.join(repoRoot, file), "utf8");
|
|
@@ -135,9 +269,10 @@ export async function analyzeComplexity(files, repoRoot) {
|
|
|
135
269
|
continue;
|
|
136
270
|
}
|
|
137
271
|
|
|
138
|
-
const
|
|
272
|
+
const language = languageForFile(file);
|
|
273
|
+
const fileComplexity = complexityForLanguage(language, content);
|
|
139
274
|
const lineCount = content.split("\n").length;
|
|
140
|
-
const funcs =
|
|
275
|
+
const funcs = functionsForLanguage(language, content);
|
|
141
276
|
|
|
142
277
|
result.files.push({
|
|
143
278
|
file,
|
|
@@ -264,7 +399,7 @@ export function computeCodeHealth(complexityResult, depGraph, jsdocResult, secur
|
|
|
264
399
|
|
|
265
400
|
// Aggregate stats
|
|
266
401
|
const totalFiles = modules.length;
|
|
267
|
-
const avgScore = totalFiles > 0 ? Math.round(modules.reduce((s, m) => s + m.score, 0) / totalFiles) :
|
|
402
|
+
const avgScore = totalFiles > 0 ? Math.round(modules.reduce((s, m) => s + m.score, 0) / totalFiles) : null;
|
|
268
403
|
const avgComplexity = totalFiles > 0 ? Math.round(modules.reduce((s, m) => s + m.complexity, 0) / totalFiles) : 0;
|
|
269
404
|
|
|
270
405
|
const gradeDistribution = { A: 0, B: 0, C: 0, D: 0, F: 0 };
|
|
@@ -280,7 +415,11 @@ export function computeCodeHealth(complexityResult, depGraph, jsdocResult, secur
|
|
|
280
415
|
? `${totalFiles} files analyzed · Average health: ${avgScore}/100 · ${gradeDistribution.A} A, ${gradeDistribution.B} B, ${gradeDistribution.C} C, ${gradeDistribution.D} D, ${gradeDistribution.F} F`
|
|
281
416
|
: "No source files analyzed.";
|
|
282
417
|
|
|
283
|
-
|
|
418
|
+
if (totalFiles === 0) {
|
|
419
|
+
info("Code health: no source files analyzed.");
|
|
420
|
+
} else {
|
|
421
|
+
info(`Code health: avg ${avgScore}/100 across ${totalFiles} files (${hotspots.length} hotspots)`);
|
|
422
|
+
}
|
|
284
423
|
|
|
285
424
|
return {
|
|
286
425
|
modules,
|
package/src/cli-args.js
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
import { renderSystemMap } from "./renderers/renderMap.js";
|
|
18
18
|
import { renderArchitectureDiff, buildArchitectureDiffData } from "./renderers/renderDiff.js";
|
|
19
19
|
|
|
20
|
-
import { publishDocs } from "./publishers/index.js";
|
|
21
20
|
import { upsertPrComment } from "./delivery/comment.js";
|
|
22
21
|
import { runInit } from "./init.js";
|
|
23
22
|
import { runMigrate } from "./migrate.js";
|
|
@@ -32,6 +31,9 @@ import { PluginManager } from "./plugins/manager.js";
|
|
|
32
31
|
import { printHelp } from "./help.js";
|
|
33
32
|
import { parseCliArgs } from "./cli-args.js";
|
|
34
33
|
import { isNonInteractiveEnvironment } from "./utils/interactive.js";
|
|
34
|
+
import { shouldPrintBanner } from "./utils/banner.js";
|
|
35
|
+
import { detectRepoLanguages } from "./core/languages.js";
|
|
36
|
+
import { publishDocs, missingPublisherCredentials } from "./publishers/index.js";
|
|
35
37
|
import {
|
|
36
38
|
initTelemetry,
|
|
37
39
|
captureError,
|
|
@@ -49,6 +51,18 @@ const EXIT_SUCCESS = 0;
|
|
|
49
51
|
const EXIT_ERROR = 1;
|
|
50
52
|
const EXIT_VALIDATION = 2;
|
|
51
53
|
|
|
54
|
+
function exitIfNoFilesScanned(scan, cfg) {
|
|
55
|
+
if ((scan?.filesCount || 0) > 0) return;
|
|
56
|
+
const patterns = Array.isArray(cfg?.scan?.include) && cfg.scan.include.length
|
|
57
|
+
? cfg.scan.include.join(", ")
|
|
58
|
+
: "(none)";
|
|
59
|
+
error("WARNING: 0 files scanned.");
|
|
60
|
+
error(`scan.include matched no files (${patterns}).`);
|
|
61
|
+
error("Empty documentation was not written.");
|
|
62
|
+
error("Update scan.include, or run `repolens init --quick` to detect this repository's language.");
|
|
63
|
+
process.exit(EXIT_VALIDATION);
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
function formatDuration(ms) {
|
|
53
67
|
if (ms < 1000) return `${ms}ms`;
|
|
54
68
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
@@ -172,6 +186,7 @@ async function getPackageVersion() {
|
|
|
172
186
|
}
|
|
173
187
|
|
|
174
188
|
async function printBanner() {
|
|
189
|
+
if (!shouldPrintBanner()) return;
|
|
175
190
|
const version = await getPackageVersion();
|
|
176
191
|
console.log(`
|
|
177
192
|
██████╗ ███████╗██████╗ ██████╗ ██╗ ███████╗███╗ ██╗███████╗
|
|
@@ -228,7 +243,8 @@ async function main() {
|
|
|
228
243
|
// Help and version return before telemetry and the update check.
|
|
229
244
|
// bin/repolens.js answers --version/--help without loading this module.
|
|
230
245
|
if (parsed.help) {
|
|
231
|
-
|
|
246
|
+
const topic = parsed.command && parsed.command !== "help" ? parsed.command : undefined;
|
|
247
|
+
printHelp(topic);
|
|
232
248
|
return;
|
|
233
249
|
}
|
|
234
250
|
|
|
@@ -244,8 +260,28 @@ async function main() {
|
|
|
244
260
|
process.exit(EXIT_VALIDATION);
|
|
245
261
|
}
|
|
246
262
|
|
|
263
|
+
if (parsed.flags["--target"]) {
|
|
264
|
+
const targetPath = parsed.flags["--target"];
|
|
265
|
+
let targetIsDirectory = false;
|
|
266
|
+
try {
|
|
267
|
+
const stat = await fs.stat(targetPath);
|
|
268
|
+
targetIsDirectory = stat.isDirectory();
|
|
269
|
+
} catch {
|
|
270
|
+
targetIsDirectory = false;
|
|
271
|
+
}
|
|
272
|
+
if (!targetIsDirectory) {
|
|
273
|
+
error(`Target directory not found: ${targetPath}`);
|
|
274
|
+
process.exit(EXIT_VALIDATION);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
247
278
|
const command = parsed.command;
|
|
248
279
|
|
|
280
|
+
if (!command) {
|
|
281
|
+
printHelp();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
249
285
|
// MCP stdio must start before telemetry and the npm update check.
|
|
250
286
|
// Those both can open the network, and logs must stay off stdout.
|
|
251
287
|
if (command === "mcp") {
|
|
@@ -366,11 +402,15 @@ async function main() {
|
|
|
366
402
|
error(err.message);
|
|
367
403
|
process.exit(EXIT_VALIDATION);
|
|
368
404
|
}
|
|
369
|
-
await runWatch(configPath);
|
|
405
|
+
const watchResult = await runWatch(configPath);
|
|
406
|
+
if (watchResult && watchResult.ok === false) {
|
|
407
|
+
await closeTelemetry();
|
|
408
|
+
process.exit(EXIT_VALIDATION);
|
|
409
|
+
}
|
|
370
410
|
return;
|
|
371
411
|
}
|
|
372
412
|
|
|
373
|
-
if (command === "publish"
|
|
413
|
+
if (command === "publish") {
|
|
374
414
|
await printBanner();
|
|
375
415
|
|
|
376
416
|
const commandTimer = startTimer("publish");
|
|
@@ -399,6 +439,18 @@ async function main() {
|
|
|
399
439
|
process.exit(EXIT_VALIDATION);
|
|
400
440
|
}
|
|
401
441
|
|
|
442
|
+
if (parsed.flags["--strict"]) {
|
|
443
|
+
const missing = missingPublisherCredentials(cfg);
|
|
444
|
+
if (missing.length > 0) {
|
|
445
|
+
stopTimer(commandTimer);
|
|
446
|
+
error("Missing publisher credentials (--strict):");
|
|
447
|
+
for (const item of missing) error(` ${item}`);
|
|
448
|
+
trackUsage("publish", "failure", { step: "strict-credentials" });
|
|
449
|
+
await closeTelemetry();
|
|
450
|
+
process.exit(EXIT_VALIDATION);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
402
454
|
// Prompt for AI enhancement if GITHUB_TOKEN is available
|
|
403
455
|
const aiResult = await promptForAI(cfg);
|
|
404
456
|
|
|
@@ -421,6 +473,7 @@ async function main() {
|
|
|
421
473
|
scan = await scanRepo(cfg);
|
|
422
474
|
stopTimer(scanTimer);
|
|
423
475
|
info(`Detected ${scan.modules?.length || 0} modules`);
|
|
476
|
+
exitIfNoFilesScanned(scan, cfg);
|
|
424
477
|
} catch (err) {
|
|
425
478
|
stopTimer(commandTimer);
|
|
426
479
|
captureError(err, { command: "publish", step: "scan", patterns: cfg.scan?.patterns });
|
|
@@ -514,16 +567,20 @@ async function main() {
|
|
|
514
567
|
info(`Using config: ${configPath}`);
|
|
515
568
|
cfg = await loadConfig(configPath);
|
|
516
569
|
} catch {
|
|
517
|
-
|
|
570
|
+
const detected = await detectRepoLanguages(targetDir);
|
|
571
|
+
const languageNames = detected.languages.map((lang) => lang.label).join(", ");
|
|
572
|
+
info(languageNames
|
|
573
|
+
? `No .repolens.yml found — scanning ${languageNames}`
|
|
574
|
+
: "No .repolens.yml found — using multi-language scan patterns");
|
|
518
575
|
cfg = {
|
|
519
576
|
configVersion: 1,
|
|
520
577
|
project: { name: path.basename(targetDir) },
|
|
521
578
|
publishers: ["markdown"],
|
|
522
579
|
scan: {
|
|
523
|
-
include:
|
|
524
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**", "**/vendor/**", "**/target/**", "**/__pycache__/**"],
|
|
580
|
+
include: detected.include,
|
|
581
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**", "**/vendor/**", "**/target/**", "**/__pycache__/**", "**/.venv/**"],
|
|
525
582
|
},
|
|
526
|
-
module_roots:
|
|
583
|
+
module_roots: detected.moduleRoots,
|
|
527
584
|
outputs: {
|
|
528
585
|
pages: [
|
|
529
586
|
{ key: "system_overview", title: "System Overview" },
|
|
@@ -552,7 +609,8 @@ async function main() {
|
|
|
552
609
|
const scan = await scanRepo(cfg);
|
|
553
610
|
stopTimer(scanTimer);
|
|
554
611
|
info(`Detected ${scan.modules?.length || 0} modules, ${scan.filesCount || 0} files`);
|
|
555
|
-
|
|
612
|
+
exitIfNoFilesScanned(scan, cfg);
|
|
613
|
+
|
|
556
614
|
const rawDiff = getGitDiff("origin/main", cfg.__repoRoot || targetDir);
|
|
557
615
|
|
|
558
616
|
info("Generating documentation set...");
|
package/src/core/diff.js
CHANGED
|
@@ -34,6 +34,19 @@ export function classifyDiffLine(line) {
|
|
|
34
34
|
return { bucket: "modified", file };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Plain message for a failed git diff.
|
|
39
|
+
* A directory that is not a repository says so. The old internal
|
|
40
|
+
* "git diff failed, returning empty diff" line is not used.
|
|
41
|
+
*/
|
|
42
|
+
export function gitDiffFailureMessage(stderr = "", message = "") {
|
|
43
|
+
const text = `${stderr}\n${message}`;
|
|
44
|
+
if (/not a git repository/i.test(text)) {
|
|
45
|
+
return "not a git repository";
|
|
46
|
+
}
|
|
47
|
+
return "git diff could not be read; continuing with an empty diff.";
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
export function getGitDiff(baseRef = "origin/main", cwd = process.cwd(), headRef) {
|
|
38
51
|
if (!isSafeGitRef(baseRef)) {
|
|
39
52
|
warn("git diff skipped: base ref contains unsupported characters.");
|
|
@@ -60,7 +73,8 @@ export function getGitDiff(baseRef = "origin/main", cwd = process.cwd(), headRef
|
|
|
60
73
|
stdio: ["ignore", "pipe", "pipe"],
|
|
61
74
|
});
|
|
62
75
|
} catch (error) {
|
|
63
|
-
|
|
76
|
+
const stderr = typeof error.stderr === "string" ? error.stderr : "";
|
|
77
|
+
warn(gitDiffFailureMessage(stderr, error.message));
|
|
64
78
|
return emptyDiff();
|
|
65
79
|
}
|
|
66
80
|
|