@chappibunny/repolens 1.15.0 → 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 CHANGED
@@ -2,6 +2,15 @@
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
+
5
14
  ## 1.15.0
6
15
 
7
16
  ### CLI usability
package/README.md CHANGED
@@ -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. `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.
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
 
@@ -181,7 +181,7 @@ Step-by-step setup for publishers, AI features, Notion, Confluence, GitHub Wiki,
181
181
  | `npx @chappibunny/repolens publish --strict` | Exit 2 when a configured publisher is missing credentials |
182
182
  | `npx @chappibunny/repolens demo` | Local preview — no API keys. Exits 2 when 0 files match |
183
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 nothing is watchable |
184
+ | `npx @chappibunny/repolens watch` | Auto-regenerate docs on file changes. Exits 2 if `module_roots` are missing or `scan.include` matches no files |
185
185
  | `npx @chappibunny/repolens migrate` | Upgrade from v0.3.0 workflows ([details](docs/MIGRATION.md)) |
186
186
  | `npx @chappibunny/repolens uninstall` | Remove all RepoLens files (config, docs, workflow) |
187
187
  | `npx @chappibunny/repolens uninstall --force` | Remove without confirmation prompt |
@@ -197,7 +197,7 @@ Step-by-step setup for publishers, AI features, Notion, Confluence, GitHub Wiki,
197
197
  |---|---|
198
198
  | `0` | Success, including a cancelled wizard or help with no other work |
199
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, `publish --strict` is missing publisher credentials, or `feedback` has no terminal |
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
201
 
202
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
203
 
@@ -237,7 +237,7 @@ If `npx` reports `Unknown command: mcp`, an `.npmrc` with `min-release-age` (or
237
237
 
238
238
  ## GitHub Action
239
239
 
240
- `action.yml` is a composite action named **RepoLens Architecture Docs**. It runs the published npm package (`npx --yes @chappibunny/repolens@1.15.0`). Inputs are `command` (`publish`, `demo`, or `doctor`), `config`, `node-version`, `pr-comment`, and `version`.
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`.
241
241
 
242
242
  ```yaml
243
243
  name: Architecture docs
@@ -253,7 +253,7 @@ jobs:
253
253
  runs-on: ubuntu-latest
254
254
  steps:
255
255
  - uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
256
- - uses: CHAPIBUNNY/repolens@v1.15.0
256
+ - uses: CHAPIBUNNY/repolens@v1.15.1
257
257
  with:
258
258
  command: publish
259
259
  config: .repolens.yml
@@ -261,7 +261,7 @@ jobs:
261
261
  pr-comment: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
262
262
  ```
263
263
 
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.0` has to exist before `uses: CHAPIBUNNY/repolens@v1.15.0` resolves, and `@chappibunny/repolens@1.15.0` has to be on npm before the action can install it.
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.
265
265
 
266
266
  ## Add the badge to your repo
267
267
 
@@ -281,7 +281,7 @@ Generated docs also link to the [gallery](https://chapibunny.github.io/repolens/
281
281
 
282
282
  Published system maps are Unicode diagrams drawn from import edges. RepoLens does not emit Mermaid.
283
283
 
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. 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).
285
285
 
286
286
  ### System Overview (Technical Profile)
287
287
 
package/llms-install.md CHANGED
@@ -12,3 +12,4 @@
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
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.15.0",
3
+ "version": "1.15.1",
4
4
  "mcpName": "io.github.CHAPIBUNNY/repolens",
5
5
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
6
6
  "license": "MIT",
@@ -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 JS_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"];
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 all JS/TS files in the project.
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 jsFiles = files
121
- .filter(f => JS_EXTENSIONS.some(ext => f.endsWith(ext)))
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: jsFiles.length,
261
+ filesAnalyzed: sourceFiles.length,
128
262
  };
129
263
 
130
- for (const file of jsFiles) {
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 fileComplexity = computeComplexity(content);
272
+ const language = languageForFile(file);
273
+ const fileComplexity = complexityForLanguage(language, content);
139
274
  const lineCount = content.split("\n").length;
140
- const funcs = extractFunctions(content);
275
+ const funcs = functionsForLanguage(language, content);
141
276
 
142
277
  result.files.push({
143
278
  file,
package/src/help.js CHANGED
@@ -3,7 +3,8 @@ const EXIT_CODES = `Exit codes:
3
3
  1 runtime error, or an unknown command
4
4
  2 invalid flags, missing config, target directory not found,
5
5
  a failed doctor check (including 0 files matched),
6
- demo or publish scanned 0 files, watch has nothing to watch,
6
+ demo or publish scanned 0 files, watch has nothing to watch
7
+ (missing module_roots, or scan.include matches no files),
7
8
  publish --strict is missing publisher credentials,
8
9
  or feedback has no terminal`;
9
10
 
@@ -149,8 +150,10 @@ Options:
149
150
  --verbose Print debug logs
150
151
  --help, -h Show this help
151
152
 
152
- Watches directories listed in module_roots. If none of those directories
153
- exist, watch exits 2 and tells you to fix module_roots. Press Ctrl+C to stop
153
+ Watches directories listed in module_roots and regenerates Markdown when
154
+ files change. Exits 2 when none of those directories exist, and when
155
+ scan.include matches no files (including after init --quick in an empty
156
+ directory). It does not wait for changes in that case. Press Ctrl+C to stop
154
157
  when a watch is running.
155
158
 
156
159
  ${EXIT_CODES}
@@ -593,7 +593,7 @@ export function renderCodeHealth(healthResult) {
593
593
  lines.push("");
594
594
  lines.push("| Factor | Weight | How It's Measured |");
595
595
  lines.push("|--------|--------|-------------------|");
596
- lines.push("| Complexity | –1/pt above 10, –2/pt above 30 | Cyclomatic complexity (if/else/for/while/case/&&/\\|\\|/ternary) |");
596
+ lines.push("| Complexity | –1/pt above 10, –2/pt above 30 | Cyclomatic complexity for JavaScript/TypeScript, Python, and Go (regex, not AST) |");
597
597
  lines.push("| Function complexity | –2/pt above 8 per function | Highest complexity function in the file |");
598
598
  lines.push("| Coupling | –1/5 units above 20 | Fan-in × Fan-out (import connections) |");
599
599
  lines.push("| Documentation | –10 if <50%, –5 if <80% | JSDoc/TSDoc coverage of exports |");
@@ -602,7 +602,7 @@ export function renderCodeHealth(healthResult) {
602
602
 
603
603
  lines.push("---");
604
604
  lines.push("");
605
- lines.push("*Generated by RepoLens code health analysis. Complexity is regex-based and may differ from AST-based tools. Scores are relative indicators, not absolute quality measures.*");
605
+ lines.push("*Generated by RepoLens code health analysis. Complexity covers JavaScript/TypeScript, Python, and Go. It is regex-based and may differ from AST-based tools. Scores are relative indicators, not absolute quality measures.*");
606
606
  lines.push("");
607
607
 
608
608
  return lines.join("\n");
package/src/watch.js CHANGED
@@ -37,9 +37,6 @@ async function rebuild(configPath) {
37
37
  * @param {string} configPath - Resolved path to .repolens.yml
38
38
  */
39
39
  export async function runWatch(configPath) {
40
- info("Starting watch mode (Markdown only)...");
41
- info("Press Ctrl+C to stop.\n");
42
-
43
40
  const cfg = await loadConfig(configPath);
44
41
  const repoRoot = cfg.__repoRoot || process.cwd();
45
42
  const moduleRoots = cfg.module_roots || ["src", "app", "lib"];
@@ -62,6 +59,20 @@ export async function runWatch(configPath) {
62
59
  return { ok: false, message: hint };
63
60
  }
64
61
 
62
+ // A root can exist (including "." after init --quick in an empty directory)
63
+ // while scan.include still matches nothing. Do not wait forever in that case.
64
+ const scan = await scanRepo(cfg);
65
+ if ((scan?.filesCount || 0) === 0) {
66
+ const patterns = (cfg.scan?.include || []).join(", ") || "(none)";
67
+ const hint = `No files matched scan.include (${patterns}). Nothing to watch. Check scan.include and module_roots in .repolens.yml. Add source files or update the globs, then run watch again.`;
68
+ warn(hint);
69
+ error(hint);
70
+ return { ok: false, message: hint };
71
+ }
72
+
73
+ info("Starting watch mode (Markdown only)...");
74
+ info("Press Ctrl+C to stop.\n");
75
+
65
76
  // Initial build only when there is something to watch.
66
77
  await rebuild(configPath);
67
78