@chappibunny/repolens 1.12.2 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +4 -4
  3. package/bin/repolens.js +21 -1
  4. package/package.json +19 -11
  5. package/src/ai/provider.js +1 -2
  6. package/src/analyzers/monorepo-detector.js +8 -20
  7. package/src/cli-args.js +60 -0
  8. package/src/cli.js +80 -100
  9. package/src/core/config.js +5 -2
  10. package/src/core/diff.js +43 -20
  11. package/src/core/scan.js +24 -4
  12. package/src/delivery/comment.js +5 -2
  13. package/src/docs/write-doc-set.js +9 -7
  14. package/src/doctor.js +2 -2
  15. package/src/help.js +54 -0
  16. package/src/index.js +13 -0
  17. package/src/init.js +38 -41
  18. package/src/integrations/discord.js +16 -2
  19. package/src/migrate.js +2 -2
  20. package/src/publishers/confluence.js +17 -3
  21. package/src/publishers/github-wiki.js +61 -32
  22. package/src/publishers/index.js +4 -9
  23. package/src/publishers/markdown.js +3 -2
  24. package/src/publishers/notion.js +10 -4
  25. package/src/utils/branch.js +23 -44
  26. package/src/utils/github-repo.js +8 -0
  27. package/src/utils/interactive.js +20 -0
  28. package/src/utils/logger.js +5 -1
  29. package/src/utils/retry.js +30 -6
  30. package/src/utils/safe-path.js +50 -0
  31. package/src/utils/secrets.js +3 -1
  32. package/src/utils/telemetry.js +34 -6
  33. package/src/utils/update-check.js +23 -6
  34. package/src/utils/validate.js +7 -0
  35. package/src/utils/yaml.js +27 -0
  36. package/src/watch.js +1 -1
  37. package/docs/AI.md +0 -329
  38. package/docs/ARCHITECTURE.md +0 -145
  39. package/docs/CONFIGURATION.md +0 -121
  40. package/docs/DEVELOPMENT.md +0 -79
  41. package/docs/ENVIRONMENT.md +0 -79
  42. package/docs/INSTALLATION.md +0 -45
  43. package/docs/KNOWN_ISSUES.md +0 -309
  44. package/docs/MIGRATION.md +0 -311
  45. package/docs/ONBOARDING.md +0 -382
  46. package/docs/PRODUCTION_CHECKLIST.md +0 -470
  47. package/docs/RELEASE.md +0 -76
  48. package/docs/ROADMAP.md +0 -151
  49. package/docs/STABILITY.md +0 -265
  50. package/docs/TELEMETRY.md +0 -166
  51. 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
  [![npm version](https://img.shields.io/npm/v/@chappibunny/repolens)](https://www.npmjs.com/package/@chappibunny/repolens)
12
12
  [![VS Code Extension](https://img.shields.io/visual-studio-marketplace/v/CHAPIBUNNY.repolens-architecture?label=VS%20Code)](https://marketplace.visualstudio.com/items?itemName=CHAPIBUNNY.repolens-architecture)
13
- [![Tests](https://img.shields.io/badge/tests-480%20passing-brightgreen)](https://github.com/CHAPIBUNNY/repolens/actions)
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
 
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) · v1.12.0
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 (380 across 22 files), release process |
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 "../src/cli.js";
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.2",
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
- "main": "src/cli.js",
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,26 +53,28 @@
47
53
  },
48
54
  "scripts": {
49
55
  "test:notion": "node notion-test.js",
50
- "docs:publish": "node src/cli.js publish --config ../../.repolens.yml",
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
- "release:check": "npm test && npm pack",
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
- "@sentry/node": "^10.42.0",
62
- "dotenv": "^17.3.1",
68
+ "dotenv": "^18.0.8",
63
69
  "fast-glob": "^3.3.3",
64
- "js-yaml": "^4.1.0",
65
- "node-fetch": "^3.3.2"
70
+ "js-yaml": "^4.3.2"
71
+ },
72
+ "optionalDependencies": {
73
+ "@sentry/node": "^10.42.0"
66
74
  },
67
75
  "devDependencies": {
68
- "tinyexec": "1.0.2",
69
- "vitest": "^4.1.0"
76
+ "tinyexec": "^1.3.1",
77
+ "vitest": "^4.1.11"
70
78
  },
71
79
  "overrides": {
72
80
  "yauzl": ">=3.2.1",
@@ -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
- const keyPreview = apiKey.substring(0, 8) + "...";
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
- // Simple YAML parsing for packages array (avoid adding js-yaml dependency for this)
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
- const basePath = pattern.replace(/\/?\*.*$/, "");
120
- const searchDir = path.join(repoRoot, basePath);
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 });
@@ -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 command = process.argv[2];
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
- // Initialize telemetry (opt-in via REPOLENS_TELEMETRY_ENABLED=true)
271
- initTelemetry();
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("\nCI/test environment detected — skipping confirmation (use --force to suppress this message).");
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
- main().catch(async (err) => {
769
- // Capture unexpected errors
770
- captureError(err, {
771
- type: "uncaught",
772
- command: process.argv[2] || "unknown"
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
- console.error("\n❌ RepoLens encountered an unexpected error:\n");
776
-
777
- if (err.code === "ENOENT") {
778
- error(formatError("FILE_NOT_FOUND", err.path));
779
- } else if (err.code === "EACCES") {
780
- error(formatError("FILE_PERMISSION_DENIED", err.path));
781
- } else if (err.message) {
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
+ }
@@ -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 = yaml.load(raw);
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);