@chappibunny/repolens 0.4.3 → 0.6.2

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/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "0.4.3",
3
+ "version": "0.6.2",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "src/cli.js",
8
8
  "bin": {
9
- "repolens": "./bin/repolens.js"
9
+ "repolens": "bin/repolens.js"
10
10
  },
11
11
  "files": [
12
12
  "bin",
@@ -18,12 +18,19 @@
18
18
  ],
19
19
  "keywords": [
20
20
  "cli",
21
- "docs",
21
+ "documentation",
22
22
  "architecture",
23
23
  "github",
24
24
  "notion",
25
+ "confluence",
25
26
  "markdown",
26
- "developer-tools"
27
+ "developer-tools",
28
+ "repolens",
29
+ "observability",
30
+ "metrics",
31
+ "team-collaboration",
32
+ "discord",
33
+ "ai-documentation"
27
34
  ],
28
35
  "repository": {
29
36
  "type": "git",
@@ -34,6 +41,10 @@
34
41
  "url": "https://github.com/CHAPIBUNNY/repolens/issues"
35
42
  },
36
43
  "author": "Charl van Zyl",
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "registry": "https://registry.npmjs.org/"
47
+ },
37
48
  "scripts": {
38
49
  "test:notion": "node notion-test.js",
39
50
  "docs:publish": "node src/cli.js publish --config ../../.repolens.yml",
@@ -47,6 +58,7 @@
47
58
  "release:major": "npm version major"
48
59
  },
49
60
  "dependencies": {
61
+ "@sentry/node": "^10.42.0",
50
62
  "dotenv": "^17.3.1",
51
63
  "fast-glob": "^3.3.3",
52
64
  "js-yaml": "^4.1.0",
@@ -1,6 +1,7 @@
1
1
  // Provider-agnostic AI text generation
2
2
 
3
3
  import { warn, info } from "../utils/logger.js";
4
+ import { executeAIRequest } from "../utils/rate-limit.js";
4
5
 
5
6
  const DEFAULT_TIMEOUT_MS = 60000;
6
7
  const DEFAULT_TEMPERATURE = 0.2;
@@ -68,54 +69,56 @@ export async function generateText({ system, user, temperature, maxTokens }) {
68
69
  }
69
70
 
70
71
  async function callOpenAICompatibleAPI({ baseUrl, apiKey, model, system, user, temperature, maxTokens, timeoutMs }) {
71
- const url = `${baseUrl}/chat/completions`;
72
-
73
- const controller = new AbortController();
74
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
75
-
76
- try {
77
- const response = await fetch(url, {
78
- method: "POST",
79
- headers: {
80
- "Content-Type": "application/json",
81
- "Authorization": `Bearer ${apiKey}`
82
- },
83
- body: JSON.stringify({
84
- model,
85
- messages: [
86
- { role: "system", content: system },
87
- { role: "user", content: user }
88
- ],
89
- temperature,
90
- max_tokens: maxTokens
91
- }),
92
- signal: controller.signal
93
- });
94
-
95
- clearTimeout(timeoutId);
96
-
97
- if (!response.ok) {
98
- const errorText = await response.text();
99
- throw new Error(`API error (${response.status}): ${errorText}`);
100
- }
72
+ return await executeAIRequest(async () => {
73
+ const url = `${baseUrl}/chat/completions`;
101
74
 
102
- const data = await response.json();
75
+ const controller = new AbortController();
76
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
103
77
 
104
- if (!data.choices || data.choices.length === 0) {
105
- throw new Error("No completion returned from API");
78
+ try {
79
+ const response = await fetch(url, {
80
+ method: "POST",
81
+ headers: {
82
+ "Content-Type": "application/json",
83
+ "Authorization": `Bearer ${apiKey}`
84
+ },
85
+ body: JSON.stringify({
86
+ model,
87
+ messages: [
88
+ { role: "system", content: system },
89
+ { role: "user", content: user }
90
+ ],
91
+ temperature,
92
+ max_tokens: maxTokens
93
+ }),
94
+ signal: controller.signal
95
+ });
96
+
97
+ clearTimeout(timeoutId);
98
+
99
+ if (!response.ok) {
100
+ const errorText = await response.text();
101
+ throw new Error(`API error (${response.status}): ${errorText}`);
102
+ }
103
+
104
+ const data = await response.json();
105
+
106
+ if (!data.choices || data.choices.length === 0) {
107
+ throw new Error("No completion returned from API");
108
+ }
109
+
110
+ return data.choices[0].message.content;
111
+
112
+ } catch (error) {
113
+ clearTimeout(timeoutId);
114
+
115
+ if (error.name === "AbortError") {
116
+ throw new Error(`Request timeout after ${timeoutMs}ms`);
117
+ }
118
+
119
+ throw error;
106
120
  }
107
-
108
- return data.choices[0].message.content;
109
-
110
- } catch (error) {
111
- clearTimeout(timeoutId);
112
-
113
- if (error.name === "AbortError") {
114
- throw new Error(`Request timeout after ${timeoutMs}ms`);
115
- }
116
-
117
- throw error;
118
- }
121
+ });
119
122
  }
120
123
 
121
124
  export function isAIEnabled() {
package/src/cli.js CHANGED
@@ -24,6 +24,15 @@ import { info, error } from "./utils/logger.js";
24
24
  import { checkForUpdates } from "./utils/update-check.js";
25
25
  import { generateDocumentSet } from "./docs/generate-doc-set.js";
26
26
  import { writeDocumentSet } from "./docs/write-doc-set.js";
27
+ import {
28
+ initTelemetry,
29
+ captureError,
30
+ setContext,
31
+ closeTelemetry,
32
+ trackUsage,
33
+ startTimer,
34
+ stopTimer
35
+ } from "./utils/telemetry.js";
27
36
 
28
37
  async function getPackageVersion() {
29
38
  const __filename = fileURLToPath(import.meta.url);
@@ -38,8 +47,17 @@ async function getPackageVersion() {
38
47
 
39
48
  async function printBanner() {
40
49
  const version = await getPackageVersion();
41
- console.log(`\nRepoLens v${version}`);
42
- console.log("─".repeat(40));
50
+ console.log(`
51
+ ██████╗ ███████╗██████╗ ██████╗ ██╗ ███████╗███╗ ██╗███████╗
52
+ ██╔══██╗██╔════╝██╔══██╗██╔═══██╗██║ ██╔════╝████╗ ██║██╔════╝
53
+ ██████╔╝█████╗ ██████╔╝██║ ██║██║ █████╗ ██╔██╗ ██║███████╗
54
+ ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██║██║ ██╔══╝ ██║╚██╗██║╚════██║
55
+ ██║ ██║███████╗██║ ╚██████╔╝███████╗███████╗██║ ╚████║███████║
56
+ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═══╝╚══════╝
57
+ 🔍 Repository Intelligence by RABITAI 🐰
58
+ v${version}
59
+ `);
60
+ console.log("─".repeat(70));
43
61
  }
44
62
 
45
63
  function getArg(name) {
@@ -81,16 +99,16 @@ async function findConfig(startDir = process.cwd()) {
81
99
 
82
100
  function printHelp() {
83
101
  console.log(`
84
- RepoLens — Repo intelligence CLI
102
+ RepoLens — Repository Intelligence CLI by RABITAI 🐰
85
103
 
86
104
  Usage:
87
105
  repolens <command> [options]
88
106
 
89
107
  Commands:
90
- init Scaffold RepoLens files in a target repository
91
- doctor Validate a repository's RepoLens setup
108
+ init Scaffold RepoLens files in your repository
109
+ doctor Validate your RepoLens setup
92
110
  migrate Upgrade workflow files to v0.4.0 format
93
- publish Scan, render, and publish RepoLens outputs
111
+ publish Scan, render, and publish documentation
94
112
  version Print the current RepoLens version
95
113
 
96
114
  Options:
@@ -116,6 +134,15 @@ Examples:
116
134
  async function main() {
117
135
  const command = process.argv[2];
118
136
 
137
+ // Initialize telemetry (opt-in via REPOLENS_TELEMETRY_ENABLED=true)
138
+ initTelemetry();
139
+ setContext("cli", {
140
+ command: command || "publish",
141
+ args: process.argv.slice(2),
142
+ nodeVersion: process.version,
143
+ platform: process.platform,
144
+ });
145
+
119
146
  // Check for updates (non-blocking, runs in background)
120
147
  checkForUpdates().catch(() => {/* Silently fail */});
121
148
 
@@ -134,12 +161,22 @@ async function main() {
134
161
  await printBanner();
135
162
  const targetDir = getArg("--target") || process.cwd();
136
163
  info(`Initializing RepoLens in: ${targetDir}`);
164
+
165
+ const timer = startTimer("init");
137
166
  try {
138
167
  await runInit(targetDir);
168
+ const duration = stopTimer(timer);
139
169
  info("✓ RepoLens initialized successfully");
170
+
171
+ trackUsage("init", "success", { duration });
172
+ await closeTelemetry();
140
173
  } catch (err) {
174
+ stopTimer(timer);
175
+ captureError(err, { command: "init", targetDir });
176
+ trackUsage("init", "failure");
141
177
  error("Failed to initialize RepoLens:");
142
178
  error(err.message);
179
+ await closeTelemetry();
143
180
  process.exit(1);
144
181
  }
145
182
  return;
@@ -149,12 +186,22 @@ async function main() {
149
186
  await printBanner();
150
187
  const targetDir = getArg("--target") || process.cwd();
151
188
  info(`Validating RepoLens setup in: ${targetDir}`);
189
+
190
+ const timer = startTimer("doctor");
152
191
  try {
153
192
  await runDoctor(targetDir);
193
+ const duration = stopTimer(timer);
154
194
  info("✓ RepoLens validation passed");
195
+
196
+ trackUsage("doctor", "success", { duration });
197
+ await closeTelemetry();
155
198
  } catch (err) {
199
+ stopTimer(timer);
200
+ captureError(err, { command: "doctor", targetDir });
201
+ trackUsage("doctor", "failure");
156
202
  error("Validation failed:");
157
203
  error(err.message);
204
+ await closeTelemetry();
158
205
  process.exit(2);
159
206
  }
160
207
  return;
@@ -165,11 +212,25 @@ async function main() {
165
212
  const dryRun = process.argv.includes("--dry-run");
166
213
  const force = process.argv.includes("--force");
167
214
 
215
+ const timer = startTimer("migrate");
168
216
  try {
169
- await runMigrate(targetDir, { dryRun, force });
217
+ const result = await runMigrate(targetDir, { dryRun, force });
218
+ const duration = stopTimer(timer);
219
+
220
+ trackUsage("migrate", "success", {
221
+ duration,
222
+ migratedCount: result?.migratedCount || 0,
223
+ skippedCount: result?.skippedCount || 0,
224
+ dryRun,
225
+ });
226
+ await closeTelemetry();
170
227
  } catch (err) {
228
+ stopTimer(timer);
229
+ captureError(err, { command: "migrate", targetDir, dryRun, force });
230
+ trackUsage("migrate", "failure", { dryRun });
171
231
  error("Migration failed:");
172
232
  error(err.message);
233
+ await closeTelemetry();
173
234
  process.exit(1);
174
235
  }
175
236
  return;
@@ -178,12 +239,15 @@ async function main() {
178
239
  if (command === "publish" || !command || command.startsWith("--")) {
179
240
  await printBanner();
180
241
 
242
+ const commandTimer = startTimer("publish");
243
+
181
244
  // Auto-discover config if not provided
182
245
  let configPath;
183
246
  try {
184
247
  configPath = getArg("--config") || await findConfig();
185
248
  info(`Using config: ${configPath}`);
186
249
  } catch (err) {
250
+ stopTimer(commandTimer);
187
251
  error(err.message);
188
252
  process.exit(2);
189
253
  }
@@ -193,18 +257,28 @@ async function main() {
193
257
  info("Loading configuration...");
194
258
  cfg = await loadConfig(configPath);
195
259
  } catch (err) {
260
+ stopTimer(commandTimer);
261
+ captureError(err, { command: "publish", step: "load-config", configPath });
262
+ trackUsage("publish", "failure", { step: "config-load" });
196
263
  error("Failed to load configuration:");
197
264
  error(err.message);
265
+ await closeTelemetry();
198
266
  process.exit(2);
199
267
  }
200
268
 
201
269
  try {
202
270
  info("Scanning repository...");
271
+ const scanTimer = startTimer("scan");
203
272
  scan = await scanRepo(cfg);
273
+ stopTimer(scanTimer);
204
274
  info(`Detected ${scan.modules?.length || 0} modules`);
205
275
  } catch (err) {
276
+ stopTimer(commandTimer);
277
+ captureError(err, { command: "publish", step: "scan", patterns: cfg.scan?.patterns });
278
+ trackUsage("publish", "failure", { step: "scan" });
206
279
  error("Failed to scan repository:");
207
280
  error(err.message);
281
+ await closeTelemetry();
208
282
  process.exit(1);
209
283
  }
210
284
 
@@ -213,7 +287,9 @@ async function main() {
213
287
 
214
288
  try {
215
289
  info("Generating documentation set...");
290
+ const renderTimer = startTimer("render");
216
291
  const docSet = await generateDocumentSet(scan, cfg, rawDiff);
292
+ stopTimer(renderTimer);
217
293
 
218
294
  info("Writing documentation to disk...");
219
295
  const writeResult = await writeDocumentSet(docSet, process.cwd());
@@ -228,15 +304,40 @@ async function main() {
228
304
  }
229
305
 
230
306
  info("Publishing documentation...");
231
- await publishDocs(cfg, renderedPages);
307
+ const publishTimer = startTimer("publish_docs");
308
+ await publishDocs(cfg, renderedPages, scan);
309
+ stopTimer(publishTimer);
310
+
232
311
  await upsertPrComment(diffData);
233
312
  info("✓ Documentation published successfully");
313
+
314
+ const totalDuration = stopTimer(commandTimer);
315
+
316
+ // Track successful publish with comprehensive metrics
317
+ const publishers = [];
318
+ if (cfg.publishers?.notion?.enabled !== false) publishers.push("notion");
319
+ if (cfg.publishers?.markdown?.enabled !== false) publishers.push("markdown");
320
+
321
+ trackUsage("publish", "success", {
322
+ duration: totalDuration,
323
+ fileCount: scan.filesCount || 0,
324
+ moduleCount: scan.modules?.length || 0,
325
+ aiEnabled: Boolean(process.env.REPOLENS_AI_API_KEY || process.env.OPENAI_API_KEY),
326
+ aiProvider: process.env.AI_PROVIDER || "openai",
327
+ publishers,
328
+ documentCount: writeResult.documentCount,
329
+ });
234
330
  } catch (err) {
331
+ stopTimer(commandTimer);
332
+ captureError(err, { command: "publish", step: "generate-or-publish", publishers: cfg.publishers });
333
+ trackUsage("publish", "failure", { step: "generate-or-publish" });
235
334
  error("Failed to publish documentation:");
236
335
  error(err.message);
336
+ await closeTelemetry();
237
337
  process.exit(1);
238
338
  }
239
339
 
340
+ await closeTelemetry();
240
341
  return;
241
342
  }
242
343
 
@@ -245,7 +346,13 @@ async function main() {
245
346
  process.exit(1);
246
347
  }
247
348
 
248
- main().catch((err) => {
349
+ main().catch(async (err) => {
350
+ // Capture unexpected errors
351
+ captureError(err, {
352
+ type: "uncaught",
353
+ command: process.argv[2] || "unknown"
354
+ });
355
+
249
356
  console.error("\n❌ RepoLens encountered an unexpected error:\n");
250
357
 
251
358
  if (err.code === "ENOENT") {
@@ -267,5 +374,6 @@ main().catch((err) => {
267
374
  console.error("\nRun with --verbose for full error details.");
268
375
  }
269
376
 
377
+ await closeTelemetry();
270
378
  process.exit(1);
271
379
  });
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  const CURRENT_SCHEMA_VERSION = 1;
11
- const SUPPORTED_PUBLISHERS = ["notion", "markdown"];
11
+ const SUPPORTED_PUBLISHERS = ["notion", "markdown", "confluence"];
12
12
  const SUPPORTED_PAGE_KEYS = [
13
13
  "system_overview",
14
14
  "module_catalog",
@@ -161,6 +161,48 @@ export function validateConfig(config) {
161
161
  }
162
162
  }
163
163
 
164
+ // Validate Discord configuration (optional)
165
+ if (config.discord !== undefined) {
166
+ if (typeof config.discord !== "object" || Array.isArray(config.discord)) {
167
+ errors.push("discord must be an object");
168
+ } else {
169
+ // Validate notifyOn
170
+ if (config.discord.notifyOn !== undefined) {
171
+ const validOptions = ["always", "significant", "never"];
172
+ if (!validOptions.includes(config.discord.notifyOn)) {
173
+ errors.push(`discord.notifyOn must be one of: ${validOptions.join(", ")}`);
174
+ }
175
+ }
176
+
177
+ // Validate significantThreshold
178
+ if (config.discord.significantThreshold !== undefined) {
179
+ if (typeof config.discord.significantThreshold !== "number") {
180
+ errors.push("discord.significantThreshold must be a number");
181
+ } else if (config.discord.significantThreshold < 0 || config.discord.significantThreshold > 100) {
182
+ errors.push("discord.significantThreshold must be between 0 and 100");
183
+ }
184
+ }
185
+
186
+ // Validate branches filter
187
+ if (config.discord.branches !== undefined) {
188
+ if (!Array.isArray(config.discord.branches)) {
189
+ errors.push("discord.branches must be an array");
190
+ } else {
191
+ config.discord.branches.forEach((branch, idx) => {
192
+ if (typeof branch !== "string") {
193
+ errors.push(`discord.branches[${idx}] must be a string`);
194
+ }
195
+ });
196
+ }
197
+ }
198
+
199
+ // Validate enabled flag
200
+ if (config.discord.enabled !== undefined && typeof config.discord.enabled !== "boolean") {
201
+ errors.push("discord.enabled must be a boolean");
202
+ }
203
+ }
204
+ }
205
+
164
206
  // Validate feature flags (optional)
165
207
  if (config.features !== undefined) {
166
208
  if (typeof config.features !== "object" || Array.isArray(config.features)) {
@@ -1,15 +1,32 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import yaml from "js-yaml";
4
- import { validateConfig } from "./config-schema.js";
4
+ import { validateConfig as validateSchema } from "./config-schema.js";
5
+ import { validateConfig } from "../utils/validate.js";
6
+ import { warn, error } from "../utils/logger.js";
5
7
 
6
8
  export async function loadConfig(configPath) {
7
9
  const absoluteConfigPath = path.resolve(configPath);
8
10
  const raw = await fs.readFile(absoluteConfigPath, "utf8");
9
11
  const cfg = yaml.load(raw);
10
12
 
11
- // Validate config against schema
12
- validateConfig(cfg);
13
+ // Validate config against schema (structure)
14
+ validateSchema(cfg);
15
+
16
+ // Validate config for security (injection, secrets, etc.)
17
+ const securityResult = validateConfig(cfg);
18
+
19
+ // Log warnings
20
+ if (securityResult.warnings.length > 0) {
21
+ securityResult.warnings.forEach(w => warn(`Config warning: ${w}`));
22
+ }
23
+
24
+ // Throw error if invalid
25
+ if (!securityResult.valid) {
26
+ error("Configuration validation failed:");
27
+ securityResult.errors.forEach(e => error(` - ${e}`));
28
+ throw new Error(`Invalid configuration: ${securityResult.errors.join(", ")}`);
29
+ }
13
30
 
14
31
  cfg.__configPath = absoluteConfigPath;
15
32
  cfg.__repoRoot = path.dirname(absoluteConfigPath);