@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 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
  [![npm version](https://img.shields.io/npm/v/@chappibunny/repolens)](https://www.npmjs.com/package/@chappibunny/repolens)
12
- [![VS Code Extension](https://img.shields.io/visual-studio-marketplace/v/repolensdox.repolensdox?label=VS%20Code)](https://marketplace.visualstudio.com/items?itemName=repolensdox.repolensdox)
12
+ [![VS Code Extension](https://img.shields.io/badge/VS_Code-Marketplace-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=repolensdox.repolensdox)
13
13
  [![Tests](https://img.shields.io/github/actions/workflow/status/CHAPIBUNNY/repolens/test.yml?label=tests)](https://github.com/CHAPIBUNNY/repolens/actions/workflows/test.yml)
14
14
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
15
15
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.CHAPIBUNNY%2Frepolens-6E56CF)](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. Run `npx @chappibunny/repolens demo` on any repo for an instant local preview.
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` | Minimal setup, skip wizard |
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 demo` | Quick local preview — no API keys needed |
180
- | `npx @chappibunny/repolens doctor` | Validate your setup |
181
- | `npx @chappibunny/repolens watch` | Auto-regenerate docs on file changes |
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.14.3`). 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`.
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.14.3
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.14.1` has to exist before `uses: CHAPIBUNNY/repolens@v1.14.3` resolves, and `@chappibunny/repolens@1.14.3` 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.
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
 
@@ -13,7 +13,7 @@ if (wantsVersion && !wantsHelp) {
13
13
 
14
14
  if (wantsHelp) {
15
15
  const { printHelp } = await import("../src/help.js");
16
- printHelp();
16
+ printHelp("mcp");
17
17
  process.exit(0);
18
18
  }
19
19
 
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 wantsHelp = argv.includes("--help") || argv.includes("-h") || argv[0] === "help";
6
- const wantsVersion = argv.includes("--version") || argv.includes("-v") || argv[0] === "version";
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 and --help must not load the scanner, publishers, or telemetry.
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
- printHelp();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.14.3",
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,
@@ -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) : 100;
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
- info(`Code health: avg ${avgScore}/100 across ${totalFiles} files (${hotspots.length} hotspots)`);
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
@@ -4,6 +4,7 @@ const BOOLEAN_FLAGS = new Set([
4
4
  "--non-interactive",
5
5
  "--dry-run",
6
6
  "--force",
7
+ "--strict",
7
8
  "--verbose",
8
9
  "--help",
9
10
  "-h",
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
- printHelp();
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" || !command) {
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
- info("No .repolens.yml found — using default scan patterns");
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: ["**/*.{js,ts,jsx,tsx,mjs,cjs,py,go,rs,java,rb,php,cs,swift,kt}"],
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: ["src", "lib", "app", "packages"],
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
- warn("git diff failed, returning empty diff.");
76
+ const stderr = typeof error.stderr === "string" ? error.stderr : "";
77
+ warn(gitDiffFailureMessage(stderr, error.message));
64
78
  return emptyDiff();
65
79
  }
66
80