@chappibunny/repolens 1.12.0 → 1.12.4
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 +82 -0
- package/README.md +4 -4
- package/bin/repolens.js +21 -1
- package/package.json +22 -12
- package/src/ai/provider.js +1 -2
- package/src/analyzers/monorepo-detector.js +8 -20
- package/src/cli-args.js +60 -0
- package/src/cli.js +80 -100
- package/src/core/config.js +5 -2
- package/src/core/diff.js +43 -20
- package/src/core/scan.js +24 -4
- package/src/delivery/comment.js +5 -2
- package/src/docs/write-doc-set.js +9 -7
- package/src/doctor.js +2 -2
- package/src/help.js +54 -0
- package/src/index.js +13 -0
- package/src/init.js +38 -41
- package/src/integrations/discord.js +16 -2
- package/src/migrate.js +2 -2
- package/src/publishers/confluence.js +17 -3
- package/src/publishers/github-wiki.js +61 -32
- package/src/publishers/index.js +4 -9
- package/src/publishers/markdown.js +3 -2
- package/src/publishers/notion.js +10 -4
- package/src/utils/branch.js +23 -44
- package/src/utils/github-repo.js +8 -0
- package/src/utils/interactive.js +20 -0
- package/src/utils/logger.js +5 -1
- package/src/utils/retry.js +30 -6
- package/src/utils/safe-path.js +50 -0
- package/src/utils/secrets.js +3 -1
- package/src/utils/telemetry.js +34 -6
- package/src/utils/update-check.js +23 -6
- package/src/utils/validate.js +7 -0
- package/src/utils/yaml.js +27 -0
- package/src/watch.js +1 -1
- package/docs/AI.md +0 -329
- package/docs/ARCHITECTURE.md +0 -145
- package/docs/CONFIGURATION.md +0 -121
- package/docs/DEVELOPMENT.md +0 -79
- package/docs/ENVIRONMENT.md +0 -79
- package/docs/INSTALLATION.md +0 -45
- package/docs/KNOWN_ISSUES.md +0 -309
- package/docs/MIGRATION.md +0 -311
- package/docs/ONBOARDING.md +0 -382
- package/docs/PRODUCTION_CHECKLIST.md +0 -470
- package/docs/RELEASE.md +0 -76
- package/docs/ROADMAP.md +0 -151
- package/docs/STABILITY.md +0 -265
- package/docs/TELEMETRY.md +0 -166
- package/docs/TROUBLESHOOTING.md +0 -377
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RepoLens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
### Security
|
|
8
|
+
|
|
9
|
+
- `repolens init` workflows grant `contents: read` and `pull-requests: write`. Architecture Diff comments use the Issues comments API on the pull request, which accepts `pull-requests: write`. `contents: write` is not granted. GitHub Wiki pushes still need `contents: write` on that job only
|
|
10
|
+
|
|
11
|
+
### Docs
|
|
12
|
+
|
|
13
|
+
- The README no longer hard-codes the release version. It points at `package.json`, which is the source of the version
|
|
14
|
+
- Test totals are no longer hard-coded in the README, development guide, or security guide. The tests badge follows the `test.yml` workflow status
|
|
15
|
+
|
|
16
|
+
## 1.12.4
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
- GitHub Wiki git commands use an argument array. The token is passed with `http.extraheader` and is no longer written into the clone URL or `origin` remote
|
|
21
|
+
- Wiki page names and generated document names cannot leave their output directory
|
|
22
|
+
- `documentation.output_dir` must stay inside the repository. Absolute paths and `..` are rejected
|
|
23
|
+
- Scan results that resolve outside the repository root are dropped, and absolute `scan.include` patterns are rejected
|
|
24
|
+
- Monorepo workspace globs that point outside the repository are ignored
|
|
25
|
+
- YAML configs are size-capped and alias-capped before `js-yaml` parses them. The js-yaml 4 schema is unchanged
|
|
26
|
+
- Notion, Confluence, GitHub, and Discord error bodies are truncated and redacted before they are logged
|
|
27
|
+
- Confluence URLs with embedded credentials, or a scheme other than http/https, are rejected
|
|
28
|
+
- Discord webhooks must be `https` and time out after 15 seconds
|
|
29
|
+
- AI startup logs no longer print a prefix of the API key
|
|
30
|
+
- Secret detection covers fine-grained GitHub tokens (`github_pat_`), `gho_` / `ghu_` / `ghr_` tokens, and npm tokens
|
|
31
|
+
- `Retry-After` values that are not seconds or an HTTP date no longer become `NaN` delays
|
|
32
|
+
- Browser signup links are opened without a shell
|
|
33
|
+
- GitHub Actions: test and security jobs are `contents: read`. `id-token: write` is only on the npm publish job. The token-present check no longer prints the token length
|
|
34
|
+
- Test workflow runs on Node.js 18, 20, and 22. Node 18 runs `scripts/node18-runtime.js` because Vitest 4 cannot start there (`util.styleText` needs Node 20.12). Node 20 and 22 run the full Vitest suite
|
|
35
|
+
|
|
36
|
+
### Fixes
|
|
37
|
+
|
|
38
|
+
- Update checks query `@chappibunny/repolens` on the npm registry. The cache file uses the real home directory on Windows and Unix
|
|
39
|
+
- `repolens init` in CI, tests, and non-TTY sessions scaffolds files instead of exiting before writing anything
|
|
40
|
+
- `repolens demo --target` looks for `.repolens.yml` from the target directory, so a demo of another folder does not scan the shell's current repository
|
|
41
|
+
- `repolens feedback` exits 2 when there is no terminal, and exits 1 when sending fails
|
|
42
|
+
- Unknown flags and missing `--config` / `--target` values exit 2. `--help` and `--version` work before or after the command
|
|
43
|
+
- `repolens --version` and `repolens --help` from the installed binary do not load the scanner or telemetry
|
|
44
|
+
- Empty repositories finish the scan with a warning instead of looking like a hung run. Binary files and files over 1 MB are not parsed as source
|
|
45
|
+
- Git renames in `git diff --name-status` record the new path. Unsafe diff refs are ignored instead of being interpolated into a shell command. A missing repo or ref no longer prints git's usage text, and the diff runs in the scanned repository rather than whatever directory the shell happened to be in
|
|
46
|
+
- Branch filters escape regex metacharacters, so a pattern like `release/*)` does not throw
|
|
47
|
+
- Confluence page-cache filenames cannot contain path separators from the branch name
|
|
48
|
+
- Notion block pagination stops if a cursor repeats
|
|
49
|
+
|
|
50
|
+
### Dependencies
|
|
51
|
+
|
|
52
|
+
- `dotenv` 18.0.8 (`import "dotenv/config"` is unchanged; Node `-r dotenv/config` preload was removed upstream)
|
|
53
|
+
- `tinyexec` 1.3.1 (dev dependency, Node 18+)
|
|
54
|
+
- js-yaml stays on 4.x, Vitest stays on 4.x, and `@sentry/node` stays on 10.x so `engines` can remain `>=18`
|
|
55
|
+
|
|
56
|
+
## 1.12.3
|
|
57
|
+
|
|
58
|
+
### Security
|
|
59
|
+
|
|
60
|
+
- Bump runtime `js-yaml` to 4.3.2 (merge-key CPU limit and `!!omap` duplicate-key complexity fixes)
|
|
61
|
+
- Bump transitive `source-map-js` to 1.2.2 (CVE-2026-93749)
|
|
62
|
+
- Bump transitive `postcss` to 8.5.31 (Dependabot #8 requested 8.5.25; 8.5.31 includes those fixes)
|
|
63
|
+
- Bump `vitest` and `@vitest/mocker` to 4.1.12 (Dependabot #10 requested 4.1.11; 4.1.12 is the current 4.1 release and includes that fix)
|
|
64
|
+
- No patched `braces` release exists for GHSA-vfj7-8cjw-p6xm (latest is 3.0.3, which the advisory covers). The release audit now checks production dependencies only and waives that one advisory. Any other high or critical runtime finding still fails the job. See `scripts/audit-waivers.json` and `SECURITY.md`.
|
|
65
|
+
|
|
66
|
+
### Packaging
|
|
67
|
+
|
|
68
|
+
- Require Node.js `>=18` (`engines`), matching built-in `fetch` and `node:readline/promises`
|
|
69
|
+
- Add an `exports` map. `main` now points at `src/index.js`, so importing the package does not run the CLI
|
|
70
|
+
- Keep the `repolens` binary on `bin/repolens.js`
|
|
71
|
+
- Replace `node-fetch` with the built-in `fetch` (`AbortSignal.timeout` for the update check)
|
|
72
|
+
- Move `@sentry/node` to `optionalDependencies`. Telemetry and feedback no-op when it is not installed
|
|
73
|
+
- Stop publishing the `docs/` handbook. Runtime document generation stays in `src/docs/`
|
|
74
|
+
|
|
75
|
+
### CI
|
|
76
|
+
|
|
77
|
+
- Publish Documentation runs on pushes to `main` and manual dispatch, and skips `dependabot[bot]`, because publisher secrets are not available on Dependabot branches
|
|
78
|
+
- Add a Test workflow that runs `npm test` on pull requests and branch pushes
|
|
79
|
+
- CI installs with `npm ci` so the committed lockfile is what runs. A fresh `npm install` on npm 10 crashes while resolving Vitest 4.1 optional peers
|
|
80
|
+
- Release workflow publishes with `npm publish --provenance` (requires an `NPM_TOKEN` Actions secret)
|
|
81
|
+
|
|
82
|
+
### Fixes
|
|
83
|
+
|
|
84
|
+
- `docs:publish` discovers `.repolens.yml` from the current directory instead of `../../.repolens.yml`
|
|
85
|
+
- `release:check` runs `npm pack --dry-run`
|
|
86
|
+
|
|
5
87
|
## 1.12.0
|
|
6
88
|
|
|
7
89
|
### 🛡️ Security Pattern Detection
|
package/README.md
CHANGED
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
[](https://www.npmjs.com/package/@chappibunny/repolens)
|
|
12
12
|
[](https://marketplace.visualstudio.com/items?itemName=CHAPIBUNNY.repolens-architecture)
|
|
13
|
-
[](https://github.com/CHAPIBUNNY/repolens/actions/workflows/test.yml)
|
|
14
14
|
[](LICENSE)
|
|
15
15
|
|
|
16
16
|
**Your architecture docs are already outdated.** RepoLens fixes that.
|
|
17
17
|
|
|
18
18
|
RepoLens scans your repository, generates living architecture documentation, and publishes it to Notion, Confluence, GitHub Wiki, or Markdown — automatically on every push. Engineers get technical docs. Stakeholders get readable system overviews. Nobody writes a word.
|
|
19
19
|
|
|
20
|
-
> Stable as of v1.0 — [API guarantees](docs/STABILITY.md) · [Security hardened](SECURITY.md) ·
|
|
20
|
+
> Stable as of v1.0 — [API guarantees](docs/STABILITY.md) · [Security hardened](SECURITY.md) · version from [`package.json`](package.json)
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -50,7 +50,7 @@ Plus framework detection: **Django** · **FastAPI** · **Flask** · **Gin** · *
|
|
|
50
50
|
|
|
51
51
|
## 🚀 Quick Start (60 seconds)
|
|
52
52
|
|
|
53
|
-
**Step 1: Install**
|
|
53
|
+
**Step 1: Install** (Node.js 18+)
|
|
54
54
|
```bash
|
|
55
55
|
npm install @chappibunny/repolens
|
|
56
56
|
```
|
|
@@ -259,7 +259,7 @@ When you open a pull request, RepoLens posts:
|
|
|
259
259
|
| [Configuration](docs/CONFIGURATION.md) | Complete `.repolens.yml` schema and examples |
|
|
260
260
|
| [Environment Variables](docs/ENVIRONMENT.md) | All env vars by publisher and feature |
|
|
261
261
|
| [Architecture](docs/ARCHITECTURE.md) | Pipeline diagram, project structure |
|
|
262
|
-
| [Development](docs/DEVELOPMENT.md) | Setup, tests
|
|
262
|
+
| [Development](docs/DEVELOPMENT.md) | Setup, tests, and the release process |
|
|
263
263
|
| [Security](SECURITY.md) | Threat model, secret detection, validation layers |
|
|
264
264
|
| [Telemetry](docs/TELEMETRY.md) | Opt-in privacy-first usage analytics |
|
|
265
265
|
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues and fixes |
|
package/bin/repolens.js
CHANGED
|
@@ -1,2 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
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";
|
|
7
|
+
|
|
8
|
+
// --version and --help must not load the scanner, publishers, or telemetry.
|
|
9
|
+
if (wantsVersion && !wantsHelp) {
|
|
10
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
|
+
console.log(pkg.version || "0.0.0");
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (wantsHelp) {
|
|
16
|
+
const { printHelp } = await import("../src/help.js");
|
|
17
|
+
printHelp();
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { runCli } = await import("../src/cli.js");
|
|
22
|
+
runCli();
|
package/package.json
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chappibunny/repolens",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.4",
|
|
4
4
|
"description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
8
15
|
"bin": {
|
|
9
16
|
"repolens": "bin/repolens.js"
|
|
10
17
|
},
|
|
11
18
|
"files": [
|
|
12
19
|
"bin",
|
|
13
20
|
"src",
|
|
14
|
-
"docs",
|
|
15
21
|
"package.json",
|
|
16
22
|
"README.md",
|
|
17
23
|
"CHANGELOG.md"
|
|
@@ -47,28 +53,32 @@
|
|
|
47
53
|
},
|
|
48
54
|
"scripts": {
|
|
49
55
|
"test:notion": "node notion-test.js",
|
|
50
|
-
"docs:publish": "node
|
|
56
|
+
"docs:publish": "node bin/repolens.js publish",
|
|
51
57
|
"init:test": "node src/cli.js init --target /tmp/repolens-init-test",
|
|
52
58
|
"test": "NODE_ENV=test vitest run --no-watch --reporter=verbose",
|
|
53
59
|
"pack:check": "npm pack",
|
|
54
|
-
"
|
|
60
|
+
"audit:runtime": "node scripts/audit-runtime.js",
|
|
61
|
+
"release:check": "npm test && npm run audit:runtime && npm pack --dry-run",
|
|
55
62
|
"test:install": "./test-install.sh",
|
|
56
63
|
"release:patch": "npm version patch",
|
|
57
64
|
"release:minor": "npm version minor",
|
|
58
65
|
"release:major": "npm version major"
|
|
59
66
|
},
|
|
60
67
|
"dependencies": {
|
|
61
|
-
"
|
|
62
|
-
"dotenv": "^17.3.1",
|
|
68
|
+
"dotenv": "^18.0.8",
|
|
63
69
|
"fast-glob": "^3.3.3",
|
|
64
|
-
"js-yaml": "^4.
|
|
65
|
-
|
|
70
|
+
"js-yaml": "^4.3.2"
|
|
71
|
+
},
|
|
72
|
+
"optionalDependencies": {
|
|
73
|
+
"@sentry/node": "^10.42.0"
|
|
66
74
|
},
|
|
67
75
|
"devDependencies": {
|
|
68
|
-
"tinyexec": "1.
|
|
69
|
-
"vitest": "^4.
|
|
76
|
+
"tinyexec": "^1.3.1",
|
|
77
|
+
"vitest": "^4.1.11"
|
|
70
78
|
},
|
|
71
79
|
"overrides": {
|
|
72
|
-
"yauzl": ">=3.2.1"
|
|
80
|
+
"yauzl": ">=3.2.1",
|
|
81
|
+
"brace-expansion": ">=5.0.6",
|
|
82
|
+
"@opentelemetry/core": ">=2.8.0"
|
|
73
83
|
}
|
|
74
84
|
}
|
package/src/ai/provider.js
CHANGED
|
@@ -83,8 +83,7 @@ export async function generateText({ system, user, temperature, maxTokens, confi
|
|
|
83
83
|
|
|
84
84
|
// Log configuration once per run
|
|
85
85
|
if (!hasLoggedConfig) {
|
|
86
|
-
|
|
87
|
-
info(`AI Config: provider=${provider}, model=${model}, key=${keyPreview}`);
|
|
86
|
+
info(`AI Config: provider=${provider}, model=${model}, key=set`);
|
|
88
87
|
hasLoggedConfig = true;
|
|
89
88
|
}
|
|
90
89
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { info } from "../utils/logger.js";
|
|
7
|
+
import { isInsideRoot } from "../utils/safe-path.js";
|
|
8
|
+
import { loadYaml } from "../utils/yaml.js";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Detect monorepo workspaces in a repository.
|
|
@@ -68,24 +70,8 @@ async function detectPnpmWorkspaces(repoRoot) {
|
|
|
68
70
|
try {
|
|
69
71
|
const yamlPath = path.join(repoRoot, "pnpm-workspace.yaml");
|
|
70
72
|
const raw = await fs.readFile(yamlPath, "utf8");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const patterns = [];
|
|
74
|
-
let inPackages = false;
|
|
75
|
-
for (const line of raw.split("\n")) {
|
|
76
|
-
const trimmed = line.trim();
|
|
77
|
-
if (trimmed === "packages:") {
|
|
78
|
-
inPackages = true;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (inPackages) {
|
|
82
|
-
if (trimmed.startsWith("- ")) {
|
|
83
|
-
patterns.push(trimmed.slice(2).replace(/['"]/g, "").trim());
|
|
84
|
-
} else if (trimmed && !trimmed.startsWith("#")) {
|
|
85
|
-
break; // End of packages list
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
73
|
+
const doc = loadYaml(raw);
|
|
74
|
+
const patterns = Array.isArray(doc?.packages) ? doc.packages.filter((item) => typeof item === "string") : [];
|
|
89
75
|
|
|
90
76
|
return await resolveWorkspacePatterns(repoRoot, patterns);
|
|
91
77
|
} catch {
|
|
@@ -116,8 +102,10 @@ async function resolveWorkspacePatterns(repoRoot, patterns) {
|
|
|
116
102
|
for (const pattern of patterns) {
|
|
117
103
|
// Convert glob pattern to directory search
|
|
118
104
|
// e.g. "packages/*" → list dirs in packages/
|
|
119
|
-
|
|
120
|
-
const
|
|
105
|
+
if (typeof pattern !== "string" || pattern.includes("\0")) continue;
|
|
106
|
+
const basePath = pattern.replace(/\/?\*.*$/, "") || ".";
|
|
107
|
+
const searchDir = path.resolve(repoRoot, basePath);
|
|
108
|
+
if (!isInsideRoot(repoRoot, searchDir)) continue;
|
|
121
109
|
|
|
122
110
|
try {
|
|
123
111
|
const entries = await fs.readdir(searchDir, { withFileTypes: true });
|
package/src/cli-args.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const FLAGS_WITH_VALUE = new Set(["--config", "--target"]);
|
|
2
|
+
const BOOLEAN_FLAGS = new Set([
|
|
3
|
+
"--quick",
|
|
4
|
+
"--non-interactive",
|
|
5
|
+
"--dry-run",
|
|
6
|
+
"--force",
|
|
7
|
+
"--verbose",
|
|
8
|
+
"--help",
|
|
9
|
+
"-h",
|
|
10
|
+
"--version",
|
|
11
|
+
"-v",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse RepoLens argv (already sliced past node and the script path).
|
|
16
|
+
* Global options may appear before or after the command.
|
|
17
|
+
*/
|
|
18
|
+
export function parseCliArgs(argv) {
|
|
19
|
+
const flags = {};
|
|
20
|
+
const positionals = [];
|
|
21
|
+
const errors = [];
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
24
|
+
const arg = argv[i];
|
|
25
|
+
if (arg === "--") {
|
|
26
|
+
positionals.push(...argv.slice(i + 1));
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (arg.startsWith("-")) {
|
|
30
|
+
if (FLAGS_WITH_VALUE.has(arg)) {
|
|
31
|
+
const value = argv[i + 1];
|
|
32
|
+
if (value == null || value.startsWith("-")) {
|
|
33
|
+
errors.push(`${arg} requires a value`);
|
|
34
|
+
} else {
|
|
35
|
+
flags[arg] = value;
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
} else if (BOOLEAN_FLAGS.has(arg)) {
|
|
39
|
+
flags[arg] = true;
|
|
40
|
+
} else {
|
|
41
|
+
errors.push(`Unknown option: ${arg}`);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
positionals.push(arg);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (positionals.length > 1) {
|
|
49
|
+
errors.push(`Unexpected argument: ${positionals.slice(1).join(" ")}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const command = positionals[0];
|
|
53
|
+
return {
|
|
54
|
+
command,
|
|
55
|
+
flags,
|
|
56
|
+
errors,
|
|
57
|
+
help: Boolean(flags["--help"] || flags["-h"] || command === "help"),
|
|
58
|
+
version: Boolean(flags["--version"] || flags["-v"] || command === "version"),
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { loadConfig } from "./core/config.js";
|
|
7
7
|
import { scanRepo } from "./core/scan.js";
|
|
@@ -29,6 +29,9 @@ import { generateDocumentSet } from "./docs/generate-doc-set.js";
|
|
|
29
29
|
import { writeDocumentSet } from "./docs/write-doc-set.js";
|
|
30
30
|
import { loadPlugins } from "./plugins/loader.js";
|
|
31
31
|
import { PluginManager } from "./plugins/manager.js";
|
|
32
|
+
import { printHelp } from "./help.js";
|
|
33
|
+
import { parseCliArgs } from "./cli-args.js";
|
|
34
|
+
import { isNonInteractiveEnvironment } from "./utils/interactive.js";
|
|
32
35
|
import {
|
|
33
36
|
initTelemetry,
|
|
34
37
|
captureError,
|
|
@@ -219,56 +222,33 @@ async function findConfig(startDir = process.cwd()) {
|
|
|
219
222
|
}
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
function printHelp() {
|
|
223
|
-
console.log(`
|
|
224
|
-
RepoLens — Repository Intelligence CLI
|
|
225
|
-
|
|
226
|
-
Usage:
|
|
227
|
-
repolens <command> [options]
|
|
228
|
-
|
|
229
|
-
Commands:
|
|
230
|
-
init Scaffold RepoLens files in your repository
|
|
231
|
-
doctor Validate your RepoLens setup
|
|
232
|
-
migrate Upgrade workflow files to current format
|
|
233
|
-
publish Scan, render, and publish documentation
|
|
234
|
-
demo Generate local docs without API keys (quick preview)
|
|
235
|
-
watch Watch for file changes and regenerate docs
|
|
236
|
-
uninstall Remove all RepoLens-generated files from your repository
|
|
237
|
-
feedback Send feedback to the RepoLens team
|
|
238
|
-
version Print the current RepoLens version
|
|
239
|
-
|
|
240
|
-
Options:
|
|
241
|
-
--config Path to .repolens.yml (auto-discovered if not provided)
|
|
242
|
-
--target Target repository path for init/doctor/migrate
|
|
243
|
-
--quick Run init with minimal prompts (skip wizard)
|
|
244
|
-
--dry-run Preview migration changes without applying them
|
|
245
|
-
--force Skip interactive confirmation for migration
|
|
246
|
-
--verbose Enable verbose logging
|
|
247
|
-
--version Print version
|
|
248
|
-
--help Show this help message
|
|
249
|
-
|
|
250
|
-
Examples:
|
|
251
|
-
repolens init # Full interactive wizard (recommended)
|
|
252
|
-
repolens init --quick # Minimal setup, skip wizard
|
|
253
|
-
repolens init --target /tmp/my-repo
|
|
254
|
-
repolens doctor --target /tmp/my-repo
|
|
255
|
-
repolens migrate # Migrate workflows in current directory
|
|
256
|
-
repolens migrate --dry-run # Preview changes without applying
|
|
257
|
-
repolens publish # Auto-discovers .repolens.yml
|
|
258
|
-
repolens publish --config /path/.repolens.yml # Explicit config path
|
|
259
|
-
repolens demo # Quick local preview (no API keys)
|
|
260
|
-
repolens watch # Watch mode (Markdown only)
|
|
261
|
-
repolens uninstall # Remove all RepoLens files
|
|
262
|
-
repolens uninstall --target /tmp/my-repo # Uninstall from specific repo
|
|
263
|
-
repolens --version
|
|
264
|
-
`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
225
|
async function main() {
|
|
268
|
-
const
|
|
226
|
+
const parsed = parseCliArgs(process.argv.slice(2));
|
|
227
|
+
|
|
228
|
+
// Help and version return before telemetry and the update check.
|
|
229
|
+
// bin/repolens.js answers --version/--help without loading this module.
|
|
230
|
+
if (parsed.help) {
|
|
231
|
+
printHelp();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
269
234
|
|
|
270
|
-
|
|
271
|
-
|
|
235
|
+
if (parsed.version) {
|
|
236
|
+
const version = await getPackageVersion();
|
|
237
|
+
console.log(version);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (parsed.errors.length > 0) {
|
|
242
|
+
for (const message of parsed.errors) error(message);
|
|
243
|
+
error("Run 'repolens help' for usage information.");
|
|
244
|
+
process.exit(EXIT_VALIDATION);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const command = parsed.command;
|
|
248
|
+
|
|
249
|
+
// Initialize telemetry (opt-in via REPOLENS_TELEMETRY_ENABLED=true).
|
|
250
|
+
// @sentry/node is optional; init is a no-op when it is not installed.
|
|
251
|
+
await initTelemetry();
|
|
272
252
|
setContext("cli", {
|
|
273
253
|
command: command || "publish",
|
|
274
254
|
args: process.argv.slice(2),
|
|
@@ -279,17 +259,6 @@ async function main() {
|
|
|
279
259
|
// Check for updates (non-blocking, runs in background)
|
|
280
260
|
checkForUpdates().catch(() => {/* Silently fail */});
|
|
281
261
|
|
|
282
|
-
if (command === "--help" || command === "-h" || command === "help") {
|
|
283
|
-
printHelp();
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (command === "--version" || command === "-v" || command === "version") {
|
|
288
|
-
const version = await getPackageVersion();
|
|
289
|
-
console.log(version);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
262
|
if (command === "init") {
|
|
294
263
|
await printBanner();
|
|
295
264
|
const targetDir = getArg("--target") || process.cwd();
|
|
@@ -392,13 +361,6 @@ async function main() {
|
|
|
392
361
|
return;
|
|
393
362
|
}
|
|
394
363
|
|
|
395
|
-
// Reject unknown flags/commands before falling through to publish
|
|
396
|
-
if (command && command.startsWith("--") && !["--help", "-h", "--version", "-v"].includes(command)) {
|
|
397
|
-
error(`Unknown option: ${command}`);
|
|
398
|
-
error("Run 'repolens help' for usage information.");
|
|
399
|
-
process.exit(EXIT_ERROR);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
364
|
if (command === "publish" || !command) {
|
|
403
365
|
await printBanner();
|
|
404
366
|
|
|
@@ -461,7 +423,7 @@ async function main() {
|
|
|
461
423
|
process.exit(EXIT_ERROR);
|
|
462
424
|
}
|
|
463
425
|
|
|
464
|
-
const rawDiff = getGitDiff("origin/main");
|
|
426
|
+
const rawDiff = getGitDiff("origin/main", cfg.__repoRoot || process.cwd());
|
|
465
427
|
const diffData = buildArchitectureDiffData(rawDiff);
|
|
466
428
|
|
|
467
429
|
try {
|
|
@@ -534,10 +496,12 @@ async function main() {
|
|
|
534
496
|
const commandTimer = startTimer("demo");
|
|
535
497
|
const targetDir = getArg("--target") || process.cwd();
|
|
536
498
|
|
|
537
|
-
// Try to load existing config, otherwise use sensible defaults
|
|
499
|
+
// Try to load existing config, otherwise use sensible defaults.
|
|
500
|
+
// Start at --target so a demo of another directory does not pick up
|
|
501
|
+
// the config in the shell's current directory.
|
|
538
502
|
let cfg;
|
|
539
503
|
try {
|
|
540
|
-
const configPath = getArg("--config") || await findConfig();
|
|
504
|
+
const configPath = getArg("--config") || await findConfig(targetDir);
|
|
541
505
|
info(`Using config: ${configPath}`);
|
|
542
506
|
cfg = await loadConfig(configPath);
|
|
543
507
|
} catch {
|
|
@@ -580,7 +544,7 @@ async function main() {
|
|
|
580
544
|
stopTimer(scanTimer);
|
|
581
545
|
info(`Detected ${scan.modules?.length || 0} modules, ${scan.filesCount || 0} files`);
|
|
582
546
|
|
|
583
|
-
const rawDiff = getGitDiff("origin/main");
|
|
547
|
+
const rawDiff = getGitDiff("origin/main", cfg.__repoRoot || targetDir);
|
|
584
548
|
|
|
585
549
|
info("Generating documentation set...");
|
|
586
550
|
const renderTimer = startTimer("render");
|
|
@@ -675,7 +639,7 @@ async function main() {
|
|
|
675
639
|
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST;
|
|
676
640
|
|
|
677
641
|
if (isCI || isTest) {
|
|
678
|
-
info("\
|
|
642
|
+
info("\nNon-interactive environment: removing RepoLens files without a prompt. Pass --force to hide this notice.");
|
|
679
643
|
} else {
|
|
680
644
|
const confirmed = await promptYesNo(`\nRemove ${found.length} item${found.length === 1 ? "" : "s"}? This cannot be undone.`, false);
|
|
681
645
|
|
|
@@ -717,6 +681,11 @@ async function main() {
|
|
|
717
681
|
|
|
718
682
|
if (command === "feedback") {
|
|
719
683
|
await printBanner();
|
|
684
|
+
if (isNonInteractiveEnvironment()) {
|
|
685
|
+
error("feedback needs an interactive terminal.");
|
|
686
|
+
await closeTelemetry();
|
|
687
|
+
process.exit(EXIT_VALIDATION);
|
|
688
|
+
}
|
|
720
689
|
info("Send feedback to the RepoLens team");
|
|
721
690
|
info("Your feedback is anonymous and helps us improve RepoLens.\n");
|
|
722
691
|
|
|
@@ -754,6 +723,8 @@ async function main() {
|
|
|
754
723
|
captureError(err, { command: "feedback" });
|
|
755
724
|
error("Failed to send feedback:");
|
|
756
725
|
error(err.message);
|
|
726
|
+
await closeTelemetry();
|
|
727
|
+
process.exit(EXIT_ERROR);
|
|
757
728
|
}
|
|
758
729
|
|
|
759
730
|
await closeTelemetry();
|
|
@@ -765,32 +736,41 @@ async function main() {
|
|
|
765
736
|
process.exit(EXIT_ERROR);
|
|
766
737
|
}
|
|
767
738
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
739
|
+
export function runCli() {
|
|
740
|
+
return main().catch(async (err) => {
|
|
741
|
+
// Capture unexpected errors
|
|
742
|
+
captureError(err, {
|
|
743
|
+
type: "uncaught",
|
|
744
|
+
command: process.argv[2] || "unknown"
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
console.error("\n❌ RepoLens encountered an unexpected error:\n");
|
|
748
|
+
|
|
749
|
+
if (err.code === "ENOENT") {
|
|
750
|
+
error(formatError("FILE_NOT_FOUND", err.path));
|
|
751
|
+
} else if (err.code === "EACCES") {
|
|
752
|
+
error(formatError("FILE_PERMISSION_DENIED", err.path));
|
|
753
|
+
} else if (err.message) {
|
|
754
|
+
error(err.message);
|
|
755
|
+
} else {
|
|
756
|
+
error(err);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (process.env.VERBOSE || process.argv.includes("--verbose")) {
|
|
760
|
+
console.error("\nStack trace:");
|
|
761
|
+
console.error(err.stack);
|
|
762
|
+
} else {
|
|
763
|
+
console.error("\nRun with --verbose for full error details.");
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
await closeTelemetry();
|
|
767
|
+
process.exit(EXIT_ERROR);
|
|
773
768
|
});
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
error(err.message);
|
|
783
|
-
} else {
|
|
784
|
-
error(err);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (process.env.VERBOSE || process.argv.includes("--verbose")) {
|
|
788
|
-
console.error("\nStack trace:");
|
|
789
|
-
console.error(err.stack);
|
|
790
|
-
} else {
|
|
791
|
-
console.error("\nRun with --verbose for full error details.");
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
await closeTelemetry();
|
|
795
|
-
process.exit(EXIT_ERROR);
|
|
796
|
-
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Run only when this file is the process entry (node src/cli.js).
|
|
772
|
+
// bin/repolens.js imports runCli() explicitly so a package import does not start the CLI.
|
|
773
|
+
const entryArg = process.argv[1];
|
|
774
|
+
if (entryArg && pathToFileURL(path.resolve(entryArg)).href === import.meta.url) {
|
|
775
|
+
runCli();
|
|
776
|
+
}
|
package/src/core/config.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import yaml from "js-yaml";
|
|
4
3
|
import { validateConfig as validateSchema } from "./config-schema.js";
|
|
5
4
|
import { validateConfig } from "../utils/validate.js";
|
|
6
5
|
import { warn, error } from "../utils/logger.js";
|
|
6
|
+
import { loadYaml } from "../utils/yaml.js";
|
|
7
7
|
|
|
8
8
|
export async function loadConfig(configPath) {
|
|
9
9
|
const absoluteConfigPath = path.resolve(configPath);
|
|
10
10
|
const raw = await fs.readFile(absoluteConfigPath, "utf8");
|
|
11
|
-
const cfg =
|
|
11
|
+
const cfg = loadYaml(raw);
|
|
12
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) {
|
|
13
|
+
throw new Error("Configuration must be a YAML mapping");
|
|
14
|
+
}
|
|
12
15
|
|
|
13
16
|
// Validate config against schema (structure)
|
|
14
17
|
validateSchema(cfg);
|