@chappibunny/repolens 0.6.3 → 0.7.0
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 +16 -2
- package/README.md +23 -18
- package/package.json +1 -1
- package/src/cli.js +65 -20
- package/src/init.js +210 -6
- package/src/publishers/confluence.js +2 -5
- package/src/publishers/notion.js +2 -1
- package/src/utils/errors.js +132 -0
- package/src/utils/metrics.js +65 -12
- package/src/utils/telemetry.js +41 -20
- package/src/watch.js +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RepoLens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.7.0
|
|
6
|
+
|
|
7
|
+
### ✨ New Features
|
|
8
|
+
- **Interactive Init Wizard**: `repolens init --interactive` — step-by-step configuration wizard with scan presets (Next.js, Express, generic), publisher selection, AI provider setup, and branch filtering
|
|
9
|
+
- **Watch Mode**: `repolens watch` — watches source directories for changes and regenerates Markdown docs with 500ms debounce (no API calls)
|
|
10
|
+
- **Enhanced Error Messages**: Centralized error catalog with actionable guidance — every error now shows what went wrong, why, and how to fix it
|
|
11
|
+
- **Performance Monitoring**: Scan, render, and publish timing summary printed after every `publish` run
|
|
12
|
+
- **Coverage Scoring Improvements**: New section completeness metric (12 document types tracked), updated health score weights, and `metrics.json` snapshots saved to `.repolens/`
|
|
13
|
+
|
|
14
|
+
## 0.6.4
|
|
15
|
+
|
|
16
|
+
### 🔧 Maintenance
|
|
17
|
+
- Removed internal infrastructure branding from user-facing documentation
|
|
18
|
+
- Re-published to npm with corrected README and CHANGELOG
|
|
19
|
+
|
|
5
20
|
## 0.6.3
|
|
6
21
|
|
|
7
22
|
### ✨ New Features
|
|
8
|
-
- **User Feedback**: Added `repolens feedback` CLI command for sending feedback directly to the RepoLens team
|
|
23
|
+
- **User Feedback**: Added `repolens feedback` CLI command for sending feedback directly to the RepoLens team
|
|
9
24
|
- Interactive prompts for name, email, and message
|
|
10
25
|
- Works even when telemetry is disabled — feedback is always accepted
|
|
11
|
-
- Uses `Sentry.captureFeedback()` from `@sentry/node`
|
|
12
26
|
|
|
13
27
|
### 🔧 Maintenance
|
|
14
28
|
- Updated version references across all documentation
|
package/README.md
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="Avatar.png" alt="RepoLens" width="120" />
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
1
|
```
|
|
6
2
|
██████╗ ███████╗██████╗ ██████╗ ██╗ ███████╗███╗ ██╗███████╗
|
|
7
3
|
██╔══██╗██╔════╝██╔══██╗██╔═══██╗██║ ██╔════╝████╗ ██║██╔════╝
|
|
@@ -9,7 +5,7 @@
|
|
|
9
5
|
██╔══██╗██╔══╝ ██╔═══╝ ██║ ██║██║ ██╔══╝ ██║╚██╗██║╚════██║
|
|
10
6
|
██║ ██║███████╗██║ ╚██████╔╝███████╗███████╗██║ ╚████║███████║
|
|
11
7
|
╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═══╝╚══════╝
|
|
12
|
-
|
|
8
|
+
Repository Intelligence CLI
|
|
13
9
|
```
|
|
14
10
|
|
|
15
11
|
[](https://github.com/CHAPIBUNNY/repolens/actions)
|
|
@@ -20,7 +16,7 @@
|
|
|
20
16
|
|
|
21
17
|
AI-assisted documentation intelligence system that generates architecture docs for engineers AND readable system docs for stakeholders
|
|
22
18
|
|
|
23
|
-
**Current Status**: v0.
|
|
19
|
+
**Current Status**: v0.7.0 — Polish & Reliability
|
|
24
20
|
|
|
25
21
|
RepoLens automatically generates and maintains living architecture documentation by analyzing your repository structure, extracting meaningful insights from your package.json, and creating visual dependency graphs. Run it once, or let it auto-update on every push.
|
|
26
22
|
|
|
@@ -132,7 +128,11 @@ RepoLens automatically detects:
|
|
|
132
128
|
✅ **Branch-Aware** - Prevent doc conflicts across branches
|
|
133
129
|
✅ **GitHub Actions** - Autonomous operation on every push
|
|
134
130
|
✅ **Team Notifications** - Discord integration with rich embeds (NEW in v0.6.0)
|
|
135
|
-
✅ **Health Score Tracking** - Monitor documentation quality over time
|
|
131
|
+
✅ **Health Score Tracking** - Monitor documentation quality over time
|
|
132
|
+
✅ **Watch Mode** - Auto-regenerate docs on file changes (NEW in v0.7.0)
|
|
133
|
+
✅ **Interactive Setup** - Step-by-step configuration wizard (NEW in v0.7.0)
|
|
134
|
+
✅ **Performance Metrics** - Timing breakdown for scan/render/publish (NEW in v0.7.0)
|
|
135
|
+
✅ **Actionable Errors** - Enhanced error messages with fix guidance (NEW in v0.7.0)
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
@@ -224,7 +224,7 @@ npm link
|
|
|
224
224
|
Install from a specific version:
|
|
225
225
|
|
|
226
226
|
```bash
|
|
227
|
-
npm install https://github.com/CHAPIBUNNY/repolens/releases/download/v0.
|
|
227
|
+
npm install https://github.com/CHAPIBUNNY/repolens/releases/download/v0.7.0/chappibunny-repolens-0.7.0.tgz
|
|
228
228
|
```
|
|
229
229
|
</details>
|
|
230
230
|
|
|
@@ -1077,7 +1077,7 @@ Simulates the full user installation experience:
|
|
|
1077
1077
|
npm pack
|
|
1078
1078
|
|
|
1079
1079
|
# Install globally from tarball
|
|
1080
|
-
npm install -g chappibunny-repolens-0.
|
|
1080
|
+
npm install -g chappibunny-repolens-0.7.0.tgz
|
|
1081
1081
|
|
|
1082
1082
|
# Verify
|
|
1083
1083
|
repolens --version
|
|
@@ -1091,9 +1091,10 @@ repolens/
|
|
|
1091
1091
|
│ └── repolens.js # CLI executable wrapper
|
|
1092
1092
|
├── src/
|
|
1093
1093
|
│ ├── cli.js # Command orchestration + banner
|
|
1094
|
-
│ ├── init.js # Scaffolding command
|
|
1094
|
+
│ ├── init.js # Scaffolding command (+ interactive wizard)
|
|
1095
1095
|
│ ├── doctor.js # Validation command
|
|
1096
1096
|
│ ├── migrate.js # Workflow migration (legacy → current)
|
|
1097
|
+
│ ├── watch.js # Watch mode for local development
|
|
1097
1098
|
│ ├── core/
|
|
1098
1099
|
│ │ ├── config.js # Config loading + validation
|
|
1099
1100
|
│ │ ├── config-schema.js # Schema version tracking
|
|
@@ -1133,7 +1134,8 @@ repolens/
|
|
|
1133
1134
|
│ ├── metrics.js # Documentation coverage & health scoring
|
|
1134
1135
|
│ ├── rate-limit.js # Token bucket rate limiter for APIs
|
|
1135
1136
|
│ ├── secrets.js # Secret detection & sanitization
|
|
1136
|
-
│ ├── telemetry.js # Opt-in error tracking
|
|
1137
|
+
│ ├── telemetry.js # Opt-in error tracking + performance timers
|
|
1138
|
+
│ ├── errors.js # Enhanced error messages with guidance
|
|
1137
1139
|
│ └── update-check.js # Version update notifications
|
|
1138
1140
|
├── tests/ # Vitest test suite (90 tests across 11 files)
|
|
1139
1141
|
├── .repolens.yml # Dogfooding config
|
|
@@ -1152,13 +1154,13 @@ RepoLens uses automated GitHub Actions releases.
|
|
|
1152
1154
|
### Creating a Release
|
|
1153
1155
|
|
|
1154
1156
|
```bash
|
|
1155
|
-
# Patch version (0.
|
|
1157
|
+
# Patch version (0.7.0 → 0.7.1) - Bug fixes
|
|
1156
1158
|
npm run release:patch
|
|
1157
1159
|
|
|
1158
|
-
# Minor version (0.
|
|
1160
|
+
# Minor version (0.7.0 → 0.8.0) - New features
|
|
1159
1161
|
npm run release:minor
|
|
1160
1162
|
|
|
1161
|
-
# Major version (0.
|
|
1163
|
+
# Major version (0.7.0 → 1.0.0) - Breaking changes
|
|
1162
1164
|
npm run release:major
|
|
1163
1165
|
|
|
1164
1166
|
# Push the tag to trigger workflow
|
|
@@ -1190,11 +1192,11 @@ RepoLens is currently in early access. v1.0 will open for community contribution
|
|
|
1190
1192
|
|
|
1191
1193
|
## 🗺️ Roadmap to v1.0
|
|
1192
1194
|
|
|
1193
|
-
**Current Status:** v0.
|
|
1195
|
+
**Current Status:** v0.7.0 — Polish & Reliability
|
|
1194
1196
|
|
|
1195
1197
|
### Completed ✅
|
|
1196
1198
|
|
|
1197
|
-
- [x] CLI commands: `init`, `doctor`, `publish`, `migrate`, `feedback`, `version`, `help`
|
|
1199
|
+
- [x] CLI commands: `init`, `doctor`, `publish`, `migrate`, `watch`, `feedback`, `version`, `help`
|
|
1198
1200
|
- [x] Config schema v1 with validation
|
|
1199
1201
|
- [x] Auto-discovery of `.repolens.yml`
|
|
1200
1202
|
- [x] Publishers: Notion + Confluence + Markdown
|
|
@@ -1214,14 +1216,17 @@ RepoLens is currently in early access. v1.0 will open for community contribution
|
|
|
1214
1216
|
- [x] npm registry publication (`@chappibunny/repolens`)
|
|
1215
1217
|
- [x] Automated npm releases via GitHub Actions
|
|
1216
1218
|
- [x] Workflow migration command (`repolens migrate`)
|
|
1219
|
+
- [x] Interactive configuration wizard (`repolens init --interactive`)
|
|
1220
|
+
- [x] Watch mode for local development (`repolens watch`)
|
|
1221
|
+
- [x] Enhanced error messages with actionable guidance
|
|
1222
|
+
- [x] Performance monitoring (scan/render/publish timing)
|
|
1223
|
+
- [x] Improved documentation coverage scoring
|
|
1217
1224
|
|
|
1218
1225
|
### Planned for v1.0 🎯
|
|
1219
1226
|
|
|
1220
1227
|
- [ ] Plugin system for custom renderers
|
|
1221
1228
|
- [ ] GraphQL schema detection
|
|
1222
1229
|
- [ ] TypeScript type graph analysis
|
|
1223
|
-
- [ ] Interactive configuration wizard
|
|
1224
|
-
- [ ] Watch mode for local development
|
|
1225
1230
|
- [ ] Additional publishers (GitHub Wiki, Obsidian)
|
|
1226
1231
|
|
|
1227
1232
|
See [ROADMAP.md](./ROADMAP.md) for detailed planning.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -20,7 +20,9 @@ import { publishDocs } from "./publishers/index.js";
|
|
|
20
20
|
import { upsertPrComment } from "./delivery/comment.js";
|
|
21
21
|
import { runInit } from "./init.js";
|
|
22
22
|
import { runMigrate } from "./migrate.js";
|
|
23
|
+
import { runWatch } from "./watch.js";
|
|
23
24
|
import { info, error } from "./utils/logger.js";
|
|
25
|
+
import { formatError } from "./utils/errors.js";
|
|
24
26
|
import { checkForUpdates } from "./utils/update-check.js";
|
|
25
27
|
import { generateDocumentSet } from "./docs/generate-doc-set.js";
|
|
26
28
|
import { writeDocumentSet } from "./docs/write-doc-set.js";
|
|
@@ -32,10 +34,34 @@ import {
|
|
|
32
34
|
trackUsage,
|
|
33
35
|
startTimer,
|
|
34
36
|
stopTimer,
|
|
35
|
-
sendFeedback
|
|
37
|
+
sendFeedback,
|
|
38
|
+
getTimings
|
|
36
39
|
} from "./utils/telemetry.js";
|
|
37
40
|
import { createInterface } from "node:readline";
|
|
38
41
|
|
|
42
|
+
function formatDuration(ms) {
|
|
43
|
+
if (ms < 1000) return `${ms}ms`;
|
|
44
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printPerformanceSummary() {
|
|
48
|
+
const timings = getTimings();
|
|
49
|
+
if (timings.length === 0) return;
|
|
50
|
+
|
|
51
|
+
console.log("\n" + "─".repeat(40));
|
|
52
|
+
console.log(" Phase Duration");
|
|
53
|
+
console.log(" " + "─".repeat(30));
|
|
54
|
+
let total = 0;
|
|
55
|
+
for (const { operation, duration } of timings) {
|
|
56
|
+
const label = operation.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
|
|
57
|
+
console.log(` ${label.padEnd(16)} ${formatDuration(duration)}`);
|
|
58
|
+
total += duration;
|
|
59
|
+
}
|
|
60
|
+
console.log(" " + "─".repeat(30));
|
|
61
|
+
console.log(` ${"Total".padEnd(16)} ${formatDuration(total)}`);
|
|
62
|
+
console.log("─".repeat(40));
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
async function getPackageVersion() {
|
|
40
66
|
const __filename = fileURLToPath(import.meta.url);
|
|
41
67
|
const __dirname = path.dirname(__filename);
|
|
@@ -93,8 +119,7 @@ async function findConfig(startDir = process.cwd()) {
|
|
|
93
119
|
return rootConfigPath;
|
|
94
120
|
} catch {
|
|
95
121
|
throw new Error(
|
|
96
|
-
"
|
|
97
|
-
"Run 'repolens init' to create one, or use --config to specify a path."
|
|
122
|
+
formatError("CONFIG_NOT_FOUND")
|
|
98
123
|
);
|
|
99
124
|
}
|
|
100
125
|
}
|
|
@@ -111,25 +136,30 @@ Commands:
|
|
|
111
136
|
doctor Validate your RepoLens setup
|
|
112
137
|
migrate Upgrade workflow files to v0.4.0 format
|
|
113
138
|
publish Scan, render, and publish documentation
|
|
139
|
+
watch Watch for file changes and regenerate docs
|
|
114
140
|
feedback Send feedback to the RepoLens team
|
|
115
141
|
version Print the current RepoLens version
|
|
116
142
|
|
|
117
143
|
Options:
|
|
118
|
-
--config
|
|
119
|
-
--target
|
|
120
|
-
--
|
|
121
|
-
--
|
|
122
|
-
--
|
|
123
|
-
--
|
|
124
|
-
--
|
|
144
|
+
--config Path to .repolens.yml (auto-discovered if not provided)
|
|
145
|
+
--target Target repository path for init/doctor/migrate
|
|
146
|
+
--interactive Run init with step-by-step configuration wizard
|
|
147
|
+
--dry-run Preview migration changes without applying them
|
|
148
|
+
--force Skip interactive confirmation for migration
|
|
149
|
+
--verbose Enable verbose logging
|
|
150
|
+
--version Print version
|
|
151
|
+
--help Show this help message
|
|
125
152
|
|
|
126
153
|
Examples:
|
|
154
|
+
repolens init # Quick setup with auto-detection
|
|
155
|
+
repolens init --interactive # Step-by-step wizard
|
|
127
156
|
repolens init --target /tmp/my-repo
|
|
128
157
|
repolens doctor --target /tmp/my-repo
|
|
129
158
|
repolens migrate # Migrate workflows in current directory
|
|
130
159
|
repolens migrate --dry-run # Preview changes without applying
|
|
131
160
|
repolens publish # Auto-discovers .repolens.yml
|
|
132
161
|
repolens publish --config /path/.repolens.yml # Explicit config path
|
|
162
|
+
repolens watch # Watch mode (Markdown only)
|
|
133
163
|
repolens --version
|
|
134
164
|
`);
|
|
135
165
|
}
|
|
@@ -163,11 +193,12 @@ async function main() {
|
|
|
163
193
|
if (command === "init") {
|
|
164
194
|
await printBanner();
|
|
165
195
|
const targetDir = getArg("--target") || process.cwd();
|
|
196
|
+
const interactive = process.argv.includes("--interactive");
|
|
166
197
|
info(`Initializing RepoLens in: ${targetDir}`);
|
|
167
198
|
|
|
168
199
|
const timer = startTimer("init");
|
|
169
200
|
try {
|
|
170
|
-
await runInit(targetDir);
|
|
201
|
+
await runInit(targetDir, { interactive });
|
|
171
202
|
const duration = stopTimer(timer);
|
|
172
203
|
info("✓ RepoLens initialized successfully");
|
|
173
204
|
|
|
@@ -239,6 +270,20 @@ async function main() {
|
|
|
239
270
|
return;
|
|
240
271
|
}
|
|
241
272
|
|
|
273
|
+
if (command === "watch") {
|
|
274
|
+
await printBanner();
|
|
275
|
+
let configPath;
|
|
276
|
+
try {
|
|
277
|
+
configPath = getArg("--config") || await findConfig();
|
|
278
|
+
info(`Using config: ${configPath}`);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
error(err.message);
|
|
281
|
+
process.exit(2);
|
|
282
|
+
}
|
|
283
|
+
await runWatch(configPath);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
242
287
|
if (command === "publish" || !command || command.startsWith("--")) {
|
|
243
288
|
await printBanner();
|
|
244
289
|
|
|
@@ -263,8 +308,7 @@ async function main() {
|
|
|
263
308
|
stopTimer(commandTimer);
|
|
264
309
|
captureError(err, { command: "publish", step: "load-config", configPath });
|
|
265
310
|
trackUsage("publish", "failure", { step: "config-load" });
|
|
266
|
-
error("
|
|
267
|
-
error(err.message);
|
|
311
|
+
error(formatError("CONFIG_VALIDATION_FAILED", err));
|
|
268
312
|
await closeTelemetry();
|
|
269
313
|
process.exit(2);
|
|
270
314
|
}
|
|
@@ -279,8 +323,9 @@ async function main() {
|
|
|
279
323
|
stopTimer(commandTimer);
|
|
280
324
|
captureError(err, { command: "publish", step: "scan", patterns: cfg.scan?.patterns });
|
|
281
325
|
trackUsage("publish", "failure", { step: "scan" });
|
|
282
|
-
|
|
283
|
-
|
|
326
|
+
const code = err.message?.includes("No files") ? "SCAN_NO_FILES"
|
|
327
|
+
: err.message?.includes("limit") ? "SCAN_TOO_MANY_FILES" : null;
|
|
328
|
+
error(code ? formatError(code, err) : `Failed to scan repository:\n ${err.message}`);
|
|
284
329
|
await closeTelemetry();
|
|
285
330
|
process.exit(1);
|
|
286
331
|
}
|
|
@@ -316,6 +361,8 @@ async function main() {
|
|
|
316
361
|
|
|
317
362
|
const totalDuration = stopTimer(commandTimer);
|
|
318
363
|
|
|
364
|
+
printPerformanceSummary();
|
|
365
|
+
|
|
319
366
|
// Track successful publish with comprehensive metrics
|
|
320
367
|
const publishers = [];
|
|
321
368
|
if (cfg.publishers?.notion?.enabled !== false) publishers.push("notion");
|
|
@@ -388,7 +435,7 @@ async function main() {
|
|
|
388
435
|
}
|
|
389
436
|
|
|
390
437
|
error(`Unknown command: ${command}`);
|
|
391
|
-
error("Available commands: init, doctor, migrate, publish, feedback, version, help");
|
|
438
|
+
error("Available commands: init, doctor, migrate, publish, watch, feedback, version, help");
|
|
392
439
|
process.exit(1);
|
|
393
440
|
}
|
|
394
441
|
|
|
@@ -402,11 +449,9 @@ main().catch(async (err) => {
|
|
|
402
449
|
console.error("\n❌ RepoLens encountered an unexpected error:\n");
|
|
403
450
|
|
|
404
451
|
if (err.code === "ENOENT") {
|
|
405
|
-
error(
|
|
406
|
-
error("Check that all required files exist and paths are correct.");
|
|
452
|
+
error(formatError("FILE_NOT_FOUND", err.path));
|
|
407
453
|
} else if (err.code === "EACCES") {
|
|
408
|
-
error(
|
|
409
|
-
error("Check file permissions and try again.");
|
|
454
|
+
error(formatError("FILE_PERMISSION_DENIED", err.path));
|
|
410
455
|
} else if (err.message) {
|
|
411
456
|
error(err.message);
|
|
412
457
|
} else {
|
package/src/init.js
CHANGED
|
@@ -3,6 +3,39 @@ import path from "node:path";
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { info, warn } from "./utils/logger.js";
|
|
5
5
|
|
|
6
|
+
const PUBLISHER_CHOICES = ["markdown", "notion", "confluence"];
|
|
7
|
+
const AI_PROVIDERS = ["openai", "anthropic", "azure", "ollama"];
|
|
8
|
+
const SCAN_PRESETS = {
|
|
9
|
+
nextjs: {
|
|
10
|
+
include: [
|
|
11
|
+
"src/**/*.{ts,tsx,js,jsx}",
|
|
12
|
+
"app/**/*.{ts,tsx,js,jsx}",
|
|
13
|
+
"pages/**/*.{ts,tsx,js,jsx}",
|
|
14
|
+
"lib/**/*.{ts,tsx,js,jsx}",
|
|
15
|
+
"components/**/*.{ts,tsx,js,jsx}",
|
|
16
|
+
],
|
|
17
|
+
roots: ["src", "app", "pages", "lib", "components"],
|
|
18
|
+
},
|
|
19
|
+
express: {
|
|
20
|
+
include: [
|
|
21
|
+
"src/**/*.{ts,js}",
|
|
22
|
+
"routes/**/*.{ts,js}",
|
|
23
|
+
"controllers/**/*.{ts,js}",
|
|
24
|
+
"models/**/*.{ts,js}",
|
|
25
|
+
"middleware/**/*.{ts,js}",
|
|
26
|
+
],
|
|
27
|
+
roots: ["src", "routes", "controllers", "models"],
|
|
28
|
+
},
|
|
29
|
+
generic: {
|
|
30
|
+
include: [
|
|
31
|
+
"src/**/*.{ts,tsx,js,jsx,md}",
|
|
32
|
+
"app/**/*.{ts,tsx,js,jsx,md}",
|
|
33
|
+
"lib/**/*.{ts,tsx,js,jsx,md}",
|
|
34
|
+
],
|
|
35
|
+
roots: ["src", "app", "lib"],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
6
39
|
const DETECTABLE_ROOTS = [
|
|
7
40
|
"app",
|
|
8
41
|
"src/app",
|
|
@@ -465,14 +498,183 @@ async function ensureEnvInGitignore(repoRoot) {
|
|
|
465
498
|
}
|
|
466
499
|
}
|
|
467
500
|
|
|
468
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Run a fully interactive configuration wizard.
|
|
503
|
+
* Returns a structured config that replaces the auto-detected defaults.
|
|
504
|
+
*/
|
|
505
|
+
async function runInteractiveWizard(repoRoot) {
|
|
506
|
+
const isCI = process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI ||
|
|
507
|
+
process.env.CIRCLECI || process.env.JENKINS_HOME || process.env.CODEBUILD_BUILD_ID;
|
|
508
|
+
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST;
|
|
509
|
+
if (isCI || isTest) return null;
|
|
510
|
+
|
|
511
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
512
|
+
const ask = (q) => rl.question(q);
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
info("\n🧙 Interactive Configuration Wizard\n");
|
|
516
|
+
|
|
517
|
+
// 1. Project name
|
|
518
|
+
const defaultName = path.basename(repoRoot) || "my-project";
|
|
519
|
+
const projectName = (await ask(`Project name (${defaultName}): `)).trim() || defaultName;
|
|
520
|
+
|
|
521
|
+
// 2. Publishers
|
|
522
|
+
info("\nSelect publishers (comma-separated numbers):");
|
|
523
|
+
PUBLISHER_CHOICES.forEach((p, i) => info(` ${i + 1}. ${p}`));
|
|
524
|
+
const pubInput = (await ask(`Publishers [1,2,3] (default: 1): `)).trim() || "1";
|
|
525
|
+
const publishers = pubInput
|
|
526
|
+
.split(",")
|
|
527
|
+
.map((n) => parseInt(n.trim(), 10))
|
|
528
|
+
.filter((n) => n >= 1 && n <= PUBLISHER_CHOICES.length)
|
|
529
|
+
.map((n) => PUBLISHER_CHOICES[n - 1]);
|
|
530
|
+
if (publishers.length === 0) publishers.push("markdown");
|
|
531
|
+
|
|
532
|
+
// 3. AI
|
|
533
|
+
const enableAi = (await ask("\nEnable AI-enhanced documentation? (y/N): ")).trim().toLowerCase() === "y";
|
|
534
|
+
let aiProvider = null;
|
|
535
|
+
if (enableAi) {
|
|
536
|
+
info("Select AI provider:");
|
|
537
|
+
AI_PROVIDERS.forEach((p, i) => info(` ${i + 1}. ${p}`));
|
|
538
|
+
const aiInput = (await ask(`Provider [1] (default: 1 openai): `)).trim() || "1";
|
|
539
|
+
const idx = parseInt(aiInput, 10);
|
|
540
|
+
aiProvider = AI_PROVIDERS[(idx >= 1 && idx <= AI_PROVIDERS.length) ? idx - 1 : 0];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 4. Scan preset
|
|
544
|
+
info("\nScan preset:");
|
|
545
|
+
const presetKeys = Object.keys(SCAN_PRESETS);
|
|
546
|
+
presetKeys.forEach((p, i) => info(` ${i + 1}. ${p}`));
|
|
547
|
+
const presetInput = (await ask(`Preset [3] (default: 3 generic): `)).trim() || "3";
|
|
548
|
+
const presetIdx = parseInt(presetInput, 10);
|
|
549
|
+
const presetKey = presetKeys[(presetIdx >= 1 && presetIdx <= presetKeys.length) ? presetIdx - 1 : 2];
|
|
550
|
+
const preset = SCAN_PRESETS[presetKey];
|
|
551
|
+
|
|
552
|
+
// 5. Branch filtering
|
|
553
|
+
const branchInput = (await ask("\nBranches allowed to publish to Notion/Confluence (comma-separated, default: main): ")).trim() || "main";
|
|
554
|
+
const branches = branchInput.split(",").map((b) => b.trim()).filter(Boolean);
|
|
555
|
+
|
|
556
|
+
// 6. Discord
|
|
557
|
+
const enableDiscord = (await ask("\nEnable Discord notifications? (y/N): ")).trim().toLowerCase() === "y";
|
|
558
|
+
|
|
559
|
+
info("\n✓ Wizard complete. Generating config...\n");
|
|
560
|
+
return { projectName, publishers, enableAi, aiProvider, preset, branches, enableDiscord };
|
|
561
|
+
} finally {
|
|
562
|
+
rl.close();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Build a .repolens.yml from wizard answers.
|
|
568
|
+
*/
|
|
569
|
+
function buildWizardConfig(answers) {
|
|
570
|
+
const lines = [
|
|
571
|
+
`configVersion: 1`,
|
|
572
|
+
``,
|
|
573
|
+
`project:`,
|
|
574
|
+
` name: "${answers.projectName}"`,
|
|
575
|
+
` docs_title_prefix: "RepoLens"`,
|
|
576
|
+
``,
|
|
577
|
+
`publishers:`,
|
|
578
|
+
];
|
|
579
|
+
for (const p of answers.publishers) {
|
|
580
|
+
lines.push(` - ${p}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (answers.publishers.includes("notion") && answers.branches.length) {
|
|
584
|
+
lines.push(``);
|
|
585
|
+
lines.push(`notion:`);
|
|
586
|
+
lines.push(` branches:`);
|
|
587
|
+
for (const b of answers.branches) lines.push(` - ${b}`);
|
|
588
|
+
lines.push(` includeBranchInTitle: false`);
|
|
589
|
+
}
|
|
590
|
+
if (answers.publishers.includes("confluence") && answers.branches.length) {
|
|
591
|
+
lines.push(``);
|
|
592
|
+
lines.push(`confluence:`);
|
|
593
|
+
lines.push(` branches:`);
|
|
594
|
+
for (const b of answers.branches) lines.push(` - ${b}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (answers.enableDiscord) {
|
|
598
|
+
lines.push(``);
|
|
599
|
+
lines.push(`discord:`);
|
|
600
|
+
lines.push(` enabled: true`);
|
|
601
|
+
lines.push(` notifyOn: significant`);
|
|
602
|
+
lines.push(` significantThreshold: 10`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (answers.enableAi) {
|
|
606
|
+
lines.push(``);
|
|
607
|
+
lines.push(`ai:`);
|
|
608
|
+
lines.push(` enabled: true`);
|
|
609
|
+
lines.push(` mode: hybrid`);
|
|
610
|
+
lines.push(` temperature: 0.3`);
|
|
611
|
+
lines.push(``);
|
|
612
|
+
lines.push(`features:`);
|
|
613
|
+
lines.push(` executive_summary: true`);
|
|
614
|
+
lines.push(` business_domains: true`);
|
|
615
|
+
lines.push(` architecture_overview: true`);
|
|
616
|
+
lines.push(` data_flows: true`);
|
|
617
|
+
lines.push(` developer_onboarding: true`);
|
|
618
|
+
lines.push(` change_impact: true`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
lines.push(``);
|
|
622
|
+
lines.push(`scan:`);
|
|
623
|
+
lines.push(` include:`);
|
|
624
|
+
for (const p of answers.preset.include) lines.push(` - "${p}"`);
|
|
625
|
+
lines.push(``);
|
|
626
|
+
lines.push(` ignore:`);
|
|
627
|
+
for (const p of buildIgnorePatterns()) lines.push(` - "${p}"`);
|
|
628
|
+
|
|
629
|
+
lines.push(``);
|
|
630
|
+
lines.push(`module_roots:`);
|
|
631
|
+
for (const r of answers.preset.roots) lines.push(` - "${r}"`);
|
|
632
|
+
|
|
633
|
+
lines.push(``);
|
|
634
|
+
lines.push(`outputs:`);
|
|
635
|
+
lines.push(` pages:`);
|
|
636
|
+
lines.push(` - key: "system_overview"`);
|
|
637
|
+
lines.push(` title: "System Overview"`);
|
|
638
|
+
lines.push(` description: "High-level snapshot of the repo and what RepoLens detected."`);
|
|
639
|
+
lines.push(``);
|
|
640
|
+
lines.push(` - key: "module_catalog"`);
|
|
641
|
+
lines.push(` title: "Module Catalog"`);
|
|
642
|
+
lines.push(` description: "Auto-detected modules with file counts."`);
|
|
643
|
+
lines.push(``);
|
|
644
|
+
lines.push(` - key: "api_surface"`);
|
|
645
|
+
lines.push(` title: "API Surface"`);
|
|
646
|
+
lines.push(` description: "Auto-detected API routes/endpoints."`);
|
|
647
|
+
lines.push(``);
|
|
648
|
+
lines.push(` - key: "arch_diff"`);
|
|
649
|
+
lines.push(` title: "Architecture Diff"`);
|
|
650
|
+
lines.push(` description: "Reserved for PR/merge change summaries."`);
|
|
651
|
+
lines.push(``);
|
|
652
|
+
lines.push(` - key: "route_map"`);
|
|
653
|
+
lines.push(` title: "Route Map"`);
|
|
654
|
+
lines.push(` description: "Detected app routes and API routes."`);
|
|
655
|
+
lines.push(``);
|
|
656
|
+
lines.push(` - key: "system_map"`);
|
|
657
|
+
lines.push(` title: "System Map"`);
|
|
658
|
+
lines.push(` description: "Unicode architecture diagram of detected modules."`);
|
|
659
|
+
lines.push(``);
|
|
660
|
+
|
|
661
|
+
return lines.join("\n");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export async function runInit(targetDir = process.cwd(), options = {}) {
|
|
469
665
|
const repoRoot = path.resolve(targetDir);
|
|
470
666
|
|
|
471
667
|
// Ensure target directory exists
|
|
472
668
|
await fs.mkdir(repoRoot, { recursive: true });
|
|
473
669
|
|
|
474
|
-
//
|
|
475
|
-
|
|
670
|
+
// Interactive wizard if --interactive flag is set
|
|
671
|
+
let wizardAnswers = null;
|
|
672
|
+
if (options.interactive) {
|
|
673
|
+
wizardAnswers = await runInteractiveWizard(repoRoot);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Prompt for Notion credentials interactively (only in non-wizard mode)
|
|
677
|
+
const notionCredentials = wizardAnswers ? null : await promptNotionCredentials();
|
|
476
678
|
|
|
477
679
|
const repolensConfigPath = path.join(repoRoot, ".repolens.yml");
|
|
478
680
|
const workflowDir = path.join(repoRoot, ".github", "workflows");
|
|
@@ -489,9 +691,11 @@ export async function runInit(targetDir = process.cwd()) {
|
|
|
489
691
|
const envExists = await fileExists(envPath);
|
|
490
692
|
const readmeExists = await fileExists(readmePath);
|
|
491
693
|
|
|
492
|
-
const projectName = detectProjectName(repoRoot);
|
|
493
|
-
const detectedRoots = await detectRepoStructure(repoRoot);
|
|
494
|
-
const configContent =
|
|
694
|
+
const projectName = wizardAnswers?.projectName || detectProjectName(repoRoot);
|
|
695
|
+
const detectedRoots = wizardAnswers ? [] : await detectRepoStructure(repoRoot);
|
|
696
|
+
const configContent = wizardAnswers
|
|
697
|
+
? buildWizardConfig(wizardAnswers)
|
|
698
|
+
: buildRepoLensConfig(projectName, detectedRoots);
|
|
495
699
|
|
|
496
700
|
info(`Detected project name: ${projectName}`);
|
|
497
701
|
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { log, info, warn } from "../utils/logger.js";
|
|
5
5
|
import { fetchWithRetry } from "../utils/retry.js";
|
|
6
6
|
import { getCurrentBranch, getBranchQualifiedTitle } from "../utils/branch.js";
|
|
7
|
+
import { createRepoLensError } from "../utils/errors.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Confluence Publisher for RepoLens
|
|
@@ -25,11 +26,7 @@ function confluenceHeaders() {
|
|
|
25
26
|
const token = process.env.CONFLUENCE_API_TOKEN;
|
|
26
27
|
|
|
27
28
|
if (!email || !token) {
|
|
28
|
-
throw
|
|
29
|
-
"Missing CONFLUENCE_EMAIL or CONFLUENCE_API_TOKEN. " +
|
|
30
|
-
"Set these environment variables or GitHub Actions secrets. " +
|
|
31
|
-
"Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens"
|
|
32
|
-
);
|
|
29
|
+
throw createRepoLensError("CONFLUENCE_SECRETS_MISSING");
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
// Basic Auth for Atlassian Cloud: base64(email:api_token)
|
package/src/publishers/notion.js
CHANGED
|
@@ -4,13 +4,14 @@ import path from "node:path";
|
|
|
4
4
|
import { log } from "../utils/logger.js";
|
|
5
5
|
import { fetchWithRetry } from "../utils/retry.js";
|
|
6
6
|
import { executeNotionRequest } from "../utils/rate-limit.js";
|
|
7
|
+
import { createRepoLensError } from "../utils/errors.js";
|
|
7
8
|
|
|
8
9
|
function notionHeaders() {
|
|
9
10
|
const token = process.env.NOTION_TOKEN;
|
|
10
11
|
const version = process.env.NOTION_VERSION || "2022-06-28";
|
|
11
12
|
|
|
12
13
|
if (!token) {
|
|
13
|
-
throw
|
|
14
|
+
throw createRepoLensError("NOTION_TOKEN_MISSING");
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
return {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced error messages with actionable guidance.
|
|
3
|
+
* Each error includes a description, likely cause, and fix.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ERROR_CATALOG = {
|
|
7
|
+
CONFIG_NOT_FOUND: {
|
|
8
|
+
message: "RepoLens config not found (.repolens.yml)",
|
|
9
|
+
cause: "No .repolens.yml file was found in the current directory or any parent directory.",
|
|
10
|
+
fix: "Run 'repolens init' to create one, or use --config to specify a path.",
|
|
11
|
+
},
|
|
12
|
+
CONFIG_PARSE_FAILED: {
|
|
13
|
+
message: "Failed to parse .repolens.yml",
|
|
14
|
+
cause: "The configuration file contains invalid YAML syntax.",
|
|
15
|
+
fix: "Check .repolens.yml for YAML syntax errors (incorrect indentation, missing colons, etc.).",
|
|
16
|
+
},
|
|
17
|
+
CONFIG_VALIDATION_FAILED: {
|
|
18
|
+
message: "Configuration validation failed",
|
|
19
|
+
cause: "The configuration file is missing required fields or contains invalid values.",
|
|
20
|
+
fix: "Run 'repolens doctor' to identify specific issues, or compare with .repolens.example.yml.",
|
|
21
|
+
},
|
|
22
|
+
NOTION_TOKEN_MISSING: {
|
|
23
|
+
message: "NOTION_TOKEN not set",
|
|
24
|
+
cause: "The Notion integration token is not configured.",
|
|
25
|
+
fix: "Add NOTION_TOKEN to your .env file or GitHub Actions secrets.\n → Get a token at https://notion.so/my-integrations",
|
|
26
|
+
},
|
|
27
|
+
NOTION_PAGE_ID_MISSING: {
|
|
28
|
+
message: "NOTION_PARENT_PAGE_ID not set",
|
|
29
|
+
cause: "The Notion parent page ID is not configured.",
|
|
30
|
+
fix: "Add NOTION_PARENT_PAGE_ID to your .env file or GitHub Actions secrets.\n → Open your Notion page, copy the 32-char ID from the URL.",
|
|
31
|
+
},
|
|
32
|
+
NOTION_API_ERROR: {
|
|
33
|
+
message: "Notion API request failed",
|
|
34
|
+
cause: "The Notion API returned an error. Common causes: invalid token, page not shared with integration, rate limit hit.",
|
|
35
|
+
fix: "1. Verify NOTION_TOKEN is correct\n 2. Ensure the parent page is shared with your RepoLens integration\n 3. Check https://status.notion.so for API outages",
|
|
36
|
+
},
|
|
37
|
+
CONFLUENCE_SECRETS_MISSING: {
|
|
38
|
+
message: "Confluence credentials not configured",
|
|
39
|
+
cause: "One or more required Confluence environment variables are missing.",
|
|
40
|
+
fix: "Set these environment variables: CONFLUENCE_URL, CONFLUENCE_EMAIL, CONFLUENCE_API_TOKEN, CONFLUENCE_SPACE_KEY\n → Generate a token at https://id.atlassian.com/manage-profile/security/api-tokens",
|
|
41
|
+
},
|
|
42
|
+
CONFLUENCE_API_ERROR: {
|
|
43
|
+
message: "Confluence API request failed",
|
|
44
|
+
cause: "The Confluence API returned an error. Common causes: invalid credentials, wrong space key, permission denied.",
|
|
45
|
+
fix: "1. Verify CONFLUENCE_EMAIL and CONFLUENCE_API_TOKEN are correct\n 2. Confirm the space key exists\n 3. Check that your account has write access to the space",
|
|
46
|
+
},
|
|
47
|
+
SCAN_NO_FILES: {
|
|
48
|
+
message: "No files matched scan patterns",
|
|
49
|
+
cause: "The scan.include patterns in .repolens.yml didn't match any files.",
|
|
50
|
+
fix: "1. Check that scan.include patterns match your project structure\n 2. Ensure scan.ignore isn't excluding everything\n 3. Run 'repolens doctor' to validate your config",
|
|
51
|
+
},
|
|
52
|
+
SCAN_TOO_MANY_FILES: {
|
|
53
|
+
message: "Repository exceeds file limit (50,000 files)",
|
|
54
|
+
cause: "The scan patterns matched too many files, which would cause performance issues.",
|
|
55
|
+
fix: "Narrow your scan.include patterns or add more entries to scan.ignore.",
|
|
56
|
+
},
|
|
57
|
+
AI_API_KEY_MISSING: {
|
|
58
|
+
message: "AI API key not set",
|
|
59
|
+
cause: "REPOLENS_AI_ENABLED is true but no API key is configured.",
|
|
60
|
+
fix: "Add REPOLENS_AI_API_KEY to your .env file, or disable AI with REPOLENS_AI_ENABLED=false",
|
|
61
|
+
},
|
|
62
|
+
AI_API_ERROR: {
|
|
63
|
+
message: "AI provider returned an error",
|
|
64
|
+
cause: "The AI API request failed. Common causes: invalid key, quota exceeded, model unavailable.",
|
|
65
|
+
fix: "1. Verify your API key is valid and has credits\n 2. Check that the model name is correct\n 3. AI docs will fall back to deterministic mode automatically",
|
|
66
|
+
},
|
|
67
|
+
DISCORD_WEBHOOK_INVALID: {
|
|
68
|
+
message: "Discord webhook URL is invalid",
|
|
69
|
+
cause: "The DISCORD_WEBHOOK_URL environment variable doesn't contain a valid Discord webhook URL.",
|
|
70
|
+
fix: "Set DISCORD_WEBHOOK_URL to a valid webhook URL from Discord (Server Settings → Integrations → Webhooks).",
|
|
71
|
+
},
|
|
72
|
+
FILE_PERMISSION_DENIED: {
|
|
73
|
+
message: "Permission denied",
|
|
74
|
+
cause: "RepoLens doesn't have permission to read or write the specified file.",
|
|
75
|
+
fix: "Check file permissions and ensure the current user has read/write access.",
|
|
76
|
+
},
|
|
77
|
+
FILE_NOT_FOUND: {
|
|
78
|
+
message: "File not found",
|
|
79
|
+
cause: "A required file doesn't exist at the expected path.",
|
|
80
|
+
fix: "Check that all required files exist. Run 'repolens init' to recreate missing files.",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create an enhanced error with actionable guidance.
|
|
86
|
+
* @param {string} code - Error code from ERROR_CATALOG
|
|
87
|
+
* @param {string} [detail] - Additional detail/context
|
|
88
|
+
* @returns {Error}
|
|
89
|
+
*/
|
|
90
|
+
export function createRepoLensError(code, detail) {
|
|
91
|
+
const entry = ERROR_CATALOG[code];
|
|
92
|
+
if (!entry) {
|
|
93
|
+
const err = new Error(detail || code);
|
|
94
|
+
err.code = code;
|
|
95
|
+
return err;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parts = [entry.message];
|
|
99
|
+
if (detail) parts.push(` ${detail}`);
|
|
100
|
+
parts.push(` → Cause: ${entry.cause}`);
|
|
101
|
+
parts.push(` → Fix: ${entry.fix}`);
|
|
102
|
+
|
|
103
|
+
const err = new Error(parts.join("\n"));
|
|
104
|
+
err.code = code;
|
|
105
|
+
return err;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Format an error with guidance for display.
|
|
110
|
+
* Falls back to the raw message if the error code isn't recognized.
|
|
111
|
+
* @param {string} code - Error code from ERROR_CATALOG
|
|
112
|
+
* @param {Error|string} [originalError] - The original error or context
|
|
113
|
+
* @returns {string} Formatted error string
|
|
114
|
+
*/
|
|
115
|
+
export function formatError(code, originalError) {
|
|
116
|
+
const entry = ERROR_CATALOG[code];
|
|
117
|
+
if (!entry) {
|
|
118
|
+
return typeof originalError === "string" ? originalError : originalError?.message || code;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const detail = typeof originalError === "string"
|
|
122
|
+
? originalError
|
|
123
|
+
: originalError?.message;
|
|
124
|
+
|
|
125
|
+
const lines = [`Error: ${entry.message}`];
|
|
126
|
+
if (detail) lines.push(` ${detail}`);
|
|
127
|
+
lines.push(` → Cause: ${entry.cause}`);
|
|
128
|
+
lines.push(` → Fix: ${entry.fix}`);
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { ERROR_CATALOG };
|
package/src/utils/metrics.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Metrics Collection for RepoLens
|
|
3
|
-
* Calculates coverage, health scores, staleness, and quality issues
|
|
2
|
+
* Metrics Collection for RepoLens
|
|
3
|
+
* Calculates coverage, health scores, staleness, section completeness, and quality issues
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { info, warn } from "./logger.js";
|
|
9
9
|
|
|
10
|
+
/** All possible document keys that RepoLens can generate */
|
|
11
|
+
const ALL_DOCUMENT_KEYS = [
|
|
12
|
+
"system_overview",
|
|
13
|
+
"module_catalog",
|
|
14
|
+
"api_surface",
|
|
15
|
+
"route_map",
|
|
16
|
+
"system_map",
|
|
17
|
+
"arch_diff",
|
|
18
|
+
"executive_summary",
|
|
19
|
+
"business_domains",
|
|
20
|
+
"architecture_overview",
|
|
21
|
+
"data_flows",
|
|
22
|
+
"change_impact",
|
|
23
|
+
"developer_onboarding",
|
|
24
|
+
];
|
|
25
|
+
|
|
10
26
|
/**
|
|
11
27
|
* Calculate documentation coverage
|
|
12
28
|
* @param {object} scanResult - Repository scan result
|
|
@@ -59,22 +75,19 @@ export function calculateCoverage(scanResult, docs) {
|
|
|
59
75
|
|
|
60
76
|
/**
|
|
61
77
|
* Calculate health score (0-100)
|
|
78
|
+
* Weights: coverage 35%, freshness 25%, quality 25%, section completeness 15%
|
|
62
79
|
* @param {object} metrics - All metrics data
|
|
63
80
|
* @returns {number} - Health score
|
|
64
81
|
*/
|
|
65
82
|
export function calculateHealthScore(metrics) {
|
|
66
|
-
const { coverage, freshness, quality } = metrics;
|
|
67
|
-
|
|
68
|
-
// Coverage weight: 40%
|
|
69
|
-
const coverageScore = coverage.overall * 0.4;
|
|
83
|
+
const { coverage, freshness, quality, sectionCompleteness } = metrics;
|
|
70
84
|
|
|
71
|
-
|
|
72
|
-
const freshnessScore = freshness.score * 0.
|
|
85
|
+
const coverageScore = coverage.overall * 0.35;
|
|
86
|
+
const freshnessScore = freshness.score * 0.25;
|
|
87
|
+
const qualityScore = quality.score * 0.25;
|
|
88
|
+
const completenessScore = (sectionCompleteness?.percentage || 0) * 0.15;
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
const qualityScore = quality.score * 0.3;
|
|
76
|
-
|
|
77
|
-
const healthScore = coverageScore + freshnessScore + qualityScore;
|
|
90
|
+
const healthScore = coverageScore + freshnessScore + qualityScore + completenessScore;
|
|
78
91
|
|
|
79
92
|
return Math.round(Math.min(100, Math.max(0, healthScore)));
|
|
80
93
|
}
|
|
@@ -325,6 +338,27 @@ function calculateTrends(history) {
|
|
|
325
338
|
};
|
|
326
339
|
}
|
|
327
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Calculate section completeness — how many of the possible document types were generated.
|
|
343
|
+
* @param {object} docs - Rendered pages map (key → content)
|
|
344
|
+
* @returns {object} - Section completeness metrics
|
|
345
|
+
*/
|
|
346
|
+
export function calculateSectionCompleteness(docs) {
|
|
347
|
+
const generated = Object.keys(docs || {}).filter(
|
|
348
|
+
(k) => docs[k] && docs[k].trim().length > 0
|
|
349
|
+
);
|
|
350
|
+
const total = ALL_DOCUMENT_KEYS.length;
|
|
351
|
+
const present = generated.filter((k) => ALL_DOCUMENT_KEYS.includes(k)).length;
|
|
352
|
+
const missing = ALL_DOCUMENT_KEYS.filter((k) => !generated.includes(k));
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
generated: present,
|
|
356
|
+
total,
|
|
357
|
+
percentage: total > 0 ? Math.round((present / total) * 100) : 0,
|
|
358
|
+
missing,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
328
362
|
/**
|
|
329
363
|
* Collect all metrics
|
|
330
364
|
* @param {object} scanResult - Repository scan result
|
|
@@ -339,11 +373,13 @@ export async function collectMetrics(scanResult, docs, docsPath, historyPath) {
|
|
|
339
373
|
const coverage = calculateCoverage(scanResult, docs);
|
|
340
374
|
const freshness = await detectStaleness(docsPath);
|
|
341
375
|
const quality = analyzeQuality(scanResult, docs);
|
|
376
|
+
const sectionCompleteness = calculateSectionCompleteness(docs);
|
|
342
377
|
|
|
343
378
|
const metrics = {
|
|
344
379
|
coverage,
|
|
345
380
|
freshness,
|
|
346
381
|
quality,
|
|
382
|
+
sectionCompleteness,
|
|
347
383
|
timestamp: new Date().toISOString(),
|
|
348
384
|
};
|
|
349
385
|
|
|
@@ -354,8 +390,25 @@ export async function collectMetrics(scanResult, docs, docsPath, historyPath) {
|
|
|
354
390
|
metrics.history = history;
|
|
355
391
|
metrics.trends = trends;
|
|
356
392
|
|
|
393
|
+
// Save latest metrics snapshot for external tooling
|
|
394
|
+
try {
|
|
395
|
+
const snapshotPath = path.join(docsPath, "metrics.json");
|
|
396
|
+
await fs.mkdir(docsPath, { recursive: true });
|
|
397
|
+
await fs.writeFile(snapshotPath, JSON.stringify({
|
|
398
|
+
healthScore: metrics.healthScore,
|
|
399
|
+
coverage: metrics.coverage,
|
|
400
|
+
sectionCompleteness: metrics.sectionCompleteness,
|
|
401
|
+
quality: { score: metrics.quality.score, summary: metrics.quality.summary },
|
|
402
|
+
freshness: { score: metrics.freshness.score, isStale: metrics.freshness.isStale, daysSinceUpdate: metrics.freshness.daysSinceUpdate },
|
|
403
|
+
timestamp: metrics.timestamp,
|
|
404
|
+
}, null, 2));
|
|
405
|
+
} catch (err) {
|
|
406
|
+
warn(`Failed to save metrics snapshot: ${err.message}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
357
409
|
info(`✓ Health Score: ${metrics.healthScore}/100`);
|
|
358
410
|
info(`✓ Coverage: ${metrics.coverage.overall.toFixed(1)}%`);
|
|
411
|
+
info(`✓ Sections: ${sectionCompleteness.generated}/${sectionCompleteness.total} document types generated`);
|
|
359
412
|
|
|
360
413
|
return metrics;
|
|
361
414
|
}
|
package/src/utils/telemetry.js
CHANGED
|
@@ -175,15 +175,15 @@ export function isTelemetryEnabled() {
|
|
|
175
175
|
// ============================================================
|
|
176
176
|
|
|
177
177
|
const performanceTimers = new Map();
|
|
178
|
+
const completedTimings = [];
|
|
178
179
|
|
|
179
180
|
/**
|
|
180
|
-
* Start a performance timer for an operation
|
|
181
|
+
* Start a performance timer for an operation.
|
|
182
|
+
* Timers always work locally (even without telemetry) so we can print a summary.
|
|
181
183
|
* @param {string} operation - Operation name (e.g., "scan", "render", "publish")
|
|
182
184
|
* @param {object} metadata - Additional context about the operation
|
|
183
185
|
*/
|
|
184
186
|
export function startTimer(operation, metadata = {}) {
|
|
185
|
-
if (!enabled) return;
|
|
186
|
-
|
|
187
187
|
const key = `${operation}_${Date.now()}`;
|
|
188
188
|
performanceTimers.set(key, {
|
|
189
189
|
operation,
|
|
@@ -191,15 +191,16 @@ export function startTimer(operation, metadata = {}) {
|
|
|
191
191
|
metadata,
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
-
return key;
|
|
194
|
+
return key;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/**
|
|
198
198
|
* Stop a performance timer and record the metric
|
|
199
199
|
* @param {string} timerKey - Key returned from startTimer()
|
|
200
|
+
* @returns {number|undefined} Duration in milliseconds
|
|
200
201
|
*/
|
|
201
202
|
export function stopTimer(timerKey) {
|
|
202
|
-
if (!
|
|
203
|
+
if (!timerKey) return;
|
|
203
204
|
|
|
204
205
|
const timer = performanceTimers.get(timerKey);
|
|
205
206
|
if (!timer) return;
|
|
@@ -207,26 +208,46 @@ export function stopTimer(timerKey) {
|
|
|
207
208
|
const duration = Date.now() - timer.startTime;
|
|
208
209
|
performanceTimers.delete(timerKey);
|
|
209
210
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
211
|
+
// Store locally for summary
|
|
212
|
+
completedTimings.push({ operation: timer.operation, duration });
|
|
213
|
+
|
|
214
|
+
// Send to Sentry if enabled
|
|
215
|
+
if (enabled) {
|
|
216
|
+
try {
|
|
217
|
+
Sentry.metrics.distribution(
|
|
218
|
+
`operation.duration`,
|
|
219
|
+
duration,
|
|
220
|
+
{
|
|
221
|
+
unit: 'millisecond',
|
|
222
|
+
tags: {
|
|
223
|
+
operation: timer.operation,
|
|
224
|
+
...timer.metadata,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// Silently fail
|
|
230
|
+
}
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
return duration;
|
|
228
234
|
}
|
|
229
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Get all completed timings for summary display.
|
|
238
|
+
* @returns {Array<{operation: string, duration: number}>}
|
|
239
|
+
*/
|
|
240
|
+
export function getTimings() {
|
|
241
|
+
return [...completedTimings];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Clear stored timings (useful between runs).
|
|
246
|
+
*/
|
|
247
|
+
export function clearTimings() {
|
|
248
|
+
completedTimings.length = 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
230
251
|
/**
|
|
231
252
|
* Track a usage event (command execution)
|
|
232
253
|
* @param {string} command - Command name (init, doctor, migrate, publish)
|
package/src/watch.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch mode for RepoLens — regenerates Markdown docs when source files change.
|
|
3
|
+
* Only publishes to Markdown (no API calls on every save).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { loadConfig } from "./core/config.js";
|
|
9
|
+
import { scanRepo } from "./core/scan.js";
|
|
10
|
+
import { generateDocumentSet } from "./docs/generate-doc-set.js";
|
|
11
|
+
import { writeDocumentSet } from "./docs/write-doc-set.js";
|
|
12
|
+
import { getGitDiff } from "./core/diff.js";
|
|
13
|
+
import { info, warn, error } from "./utils/logger.js";
|
|
14
|
+
|
|
15
|
+
const DEBOUNCE_MS = 500;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run a single rebuild cycle (scan → render → write markdown).
|
|
19
|
+
*/
|
|
20
|
+
async function rebuild(configPath) {
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
try {
|
|
23
|
+
const cfg = await loadConfig(configPath);
|
|
24
|
+
const scan = await scanRepo(cfg);
|
|
25
|
+
const rawDiff = getGitDiff("origin/main");
|
|
26
|
+
const docSet = await generateDocumentSet(scan, cfg, rawDiff);
|
|
27
|
+
const result = await writeDocumentSet(docSet, process.cwd());
|
|
28
|
+
const elapsed = Date.now() - start;
|
|
29
|
+
info(`✓ Regenerated ${result.documentCount} docs in ${elapsed}ms`);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
error(`Rebuild failed: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Start watch mode.
|
|
37
|
+
* @param {string} configPath - Resolved path to .repolens.yml
|
|
38
|
+
*/
|
|
39
|
+
export async function runWatch(configPath) {
|
|
40
|
+
info("Starting watch mode (Markdown only)...");
|
|
41
|
+
info("Press Ctrl+C to stop.\n");
|
|
42
|
+
|
|
43
|
+
// Initial build
|
|
44
|
+
await rebuild(configPath);
|
|
45
|
+
|
|
46
|
+
// Determine directories to watch from config
|
|
47
|
+
const cfg = await loadConfig(configPath);
|
|
48
|
+
const repoRoot = cfg.__repoRoot || process.cwd();
|
|
49
|
+
const moduleRoots = cfg.module_roots || ["src", "app", "lib"];
|
|
50
|
+
|
|
51
|
+
let debounceTimer = null;
|
|
52
|
+
|
|
53
|
+
const onChange = () => {
|
|
54
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
55
|
+
debounceTimer = setTimeout(() => rebuild(configPath), DEBOUNCE_MS);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const watchers = [];
|
|
59
|
+
for (const root of moduleRoots) {
|
|
60
|
+
const dirPath = path.resolve(repoRoot, root);
|
|
61
|
+
try {
|
|
62
|
+
fs.accessSync(dirPath);
|
|
63
|
+
const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
|
|
64
|
+
if (filename && !filename.includes("node_modules")) {
|
|
65
|
+
info(`Change detected: ${root}/${filename}`);
|
|
66
|
+
onChange();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
watchers.push(watcher);
|
|
70
|
+
info(`Watching: ${root}/`);
|
|
71
|
+
} catch {
|
|
72
|
+
// Directory doesn't exist, skip
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (watchers.length === 0) {
|
|
77
|
+
warn("No directories to watch. Check module_roots in .repolens.yml");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
info("\nWaiting for changes...\n");
|
|
82
|
+
|
|
83
|
+
// Keep process alive and clean up on exit
|
|
84
|
+
process.on("SIGINT", () => {
|
|
85
|
+
info("\nStopping watch mode...");
|
|
86
|
+
for (const w of watchers) w.close();
|
|
87
|
+
process.exit(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Keep alive indefinitely
|
|
91
|
+
await new Promise(() => {});
|
|
92
|
+
}
|