@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/src/core/scan.js CHANGED
@@ -2,6 +2,7 @@ import fg from "fast-glob";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { info, warn } from "../utils/logger.js";
5
+ import { trackScan } from "../utils/telemetry.js";
5
6
 
6
7
  const norm = (p) => p.replace(/\\/g, "/");
7
8
 
@@ -17,6 +18,25 @@ function isNextRoute(file) {
17
18
  );
18
19
  }
19
20
 
21
+ function isExpressRoute(content) {
22
+ // Detect Express.js route patterns
23
+ const expressPatterns = [
24
+ /(?:app|router)\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
25
+ /(?:app|router)\.route\s*\(\s*['"`]([^'"`]+)['"`]/gi
26
+ ];
27
+ return expressPatterns.some(pattern => pattern.test(content));
28
+ }
29
+
30
+ function isReactRouterFile(content) {
31
+ // Detect React Router patterns
32
+ return content.includes("<Route") || content.includes("createBrowserRouter") || content.includes("createRoutesFromElements");
33
+ }
34
+
35
+ function isVueRouterFile(content) {
36
+ // Detect Vue Router patterns
37
+ return (content.includes("createRouter") || content.includes("VueRouter")) && content.includes("routes");
38
+ }
39
+
20
40
  function isNextPage(file) {
21
41
  const f = norm(file);
22
42
 
@@ -162,6 +182,113 @@ async function extractRepoMetadata(repoRoot) {
162
182
  return metadata;
163
183
  }
164
184
 
185
+ function extractExpressRoutes(content) {
186
+ const routes = [];
187
+ const seenRoutes = new Set();
188
+
189
+ // Match app.METHOD(path) or router.METHOD(path)
190
+ const methodPattern = /(?:app|router)\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
191
+ let match;
192
+
193
+ while ((match = methodPattern.exec(content)) !== null) {
194
+ const [, method, path] = match;
195
+ const routeKey = `${method.toUpperCase()}:${path}`;
196
+
197
+ if (!seenRoutes.has(routeKey)) {
198
+ seenRoutes.add(routeKey);
199
+
200
+ const existing = routes.find(r => r.path === path);
201
+ if (existing) {
202
+ if (!existing.methods.includes(method.toUpperCase())) {
203
+ existing.methods.push(method.toUpperCase());
204
+ }
205
+ } else {
206
+ routes.push({
207
+ path,
208
+ methods: [method.toUpperCase()]
209
+ });
210
+ }
211
+ }
212
+ }
213
+
214
+ // Match app.route(path).METHOD()
215
+ const routePattern = /(?:app|router)\.route\s*\(\s*['"`]([^'"`]+)['"`]\s*\)([.\s\w()]*)/gi;
216
+ while ((match = routePattern.exec(content)) !== null) {
217
+ const [, path, chain] = match;
218
+ const methods = [];
219
+
220
+ if (chain.includes(".get(")) methods.push("GET");
221
+ if (chain.includes(".post(")) methods.push("POST");
222
+ if (chain.includes(".put(")) methods.push("PUT");
223
+ if (chain.includes(".patch(")) methods.push("PATCH");
224
+ if (chain.includes(".delete(")) methods.push("DELETE");
225
+
226
+ if (methods.length > 0) {
227
+ const existing = routes.find(r => r.path === path);
228
+ if (existing) {
229
+ methods.forEach(m => {
230
+ if (!existing.methods.includes(m)) existing.methods.push(m);
231
+ });
232
+ } else {
233
+ routes.push({ path, methods });
234
+ }
235
+ }
236
+ }
237
+
238
+ return routes;
239
+ }
240
+
241
+ function extractReactRoutes(content, file) {
242
+ const routes = [];
243
+
244
+ // Match <Route path="..." />
245
+ const routePattern = /<Route\s+[^>]*path\s*=\s*['"`]([^'"`]+)['"`][^>]*\/?>/gi;
246
+ let match;
247
+
248
+ while ((match = routePattern.exec(content)) !== null) {
249
+ const [, path] = match;
250
+ routes.push({
251
+ file,
252
+ path,
253
+ framework: "React Router"
254
+ });
255
+ }
256
+
257
+ // Match path: "..." in route objects
258
+ const objectPattern = /path\s*:\s*['"`]([^'"`]+)['"`]/gi;
259
+ while ((match = objectPattern.exec(content)) !== null) {
260
+ const [, path] = match;
261
+ if (!routes.some(r => r.path === path)) {
262
+ routes.push({
263
+ file,
264
+ path,
265
+ framework: "React Router"
266
+ });
267
+ }
268
+ }
269
+
270
+ return routes;
271
+ }
272
+
273
+ function extractVueRoutes(content, file) {
274
+ const routes = [];
275
+
276
+ // Match path: '...' or path: "..." in Vue router definitions
277
+ const pathPattern = /path\s*:\s*['"`]([^'"`]+)['"`]/gi;
278
+ let match;
279
+
280
+ while ((match = pathPattern.exec(content)) !== null) {
281
+ const [, path] = match;
282
+ routes.push({
283
+ file,
284
+ path,
285
+ framework: "Vue Router"
286
+ });
287
+ }
288
+
289
+ return routes;
290
+ }
291
+
165
292
  export async function scanRepo(cfg) {
166
293
  const repoRoot = cfg.__repoRoot;
167
294
 
@@ -197,6 +324,7 @@ export async function scanRepo(cfg) {
197
324
  const apiFiles = files.filter(isNextRoute);
198
325
  const api = [];
199
326
 
327
+ // Extract Next.js API routes
200
328
  for (const file of apiFiles) {
201
329
  const absoluteFile = path.join(repoRoot, file);
202
330
  const content = await readFileSafe(absoluteFile);
@@ -211,23 +339,71 @@ export async function scanRepo(cfg) {
211
339
  api.push({
212
340
  file,
213
341
  path: routePathFromFile(file),
214
- methods: methods.length ? methods : ["UNKNOWN"]
342
+ methods: methods.length ? methods : ["UNKNOWN"],
343
+ framework: "Next.js"
215
344
  });
216
345
  }
217
346
 
347
+ // Extract Express.js routes
348
+ for (const file of files) {
349
+ if (file.endsWith(".ts") || file.endsWith(".js")) {
350
+ const absoluteFile = path.join(repoRoot, file);
351
+ const content = await readFileSafe(absoluteFile);
352
+
353
+ if (isExpressRoute(content)) {
354
+ const expressRoutes = extractExpressRoutes(content);
355
+ for (const route of expressRoutes) {
356
+ api.push({
357
+ file,
358
+ path: route.path,
359
+ methods: route.methods,
360
+ framework: "Express"
361
+ });
362
+ }
363
+ }
364
+ }
365
+ }
366
+
218
367
  const pageFiles = files.filter(isNextPage);
219
368
  const pages = pageFiles.map((file) => ({
220
369
  file,
221
- path: routePathFromFile(file)
370
+ path: routePathFromFile(file),
371
+ framework: "Next.js"
222
372
  }));
223
373
 
374
+ // Extract React Router routes
375
+ for (const file of files) {
376
+ if (file.endsWith(".tsx") || file.endsWith(".jsx")) {
377
+ const absoluteFile = path.join(repoRoot, file);
378
+ const content = await readFileSafe(absoluteFile);
379
+
380
+ if (isReactRouterFile(content)) {
381
+ const reactRoutes = extractReactRoutes(content, file);
382
+ pages.push(...reactRoutes);
383
+ }
384
+ }
385
+ }
386
+
387
+ // Extract Vue Router routes
388
+ for (const file of files) {
389
+ if (file.endsWith(".ts") || file.endsWith(".js") || file.endsWith(".vue")) {
390
+ const absoluteFile = path.join(repoRoot, file);
391
+ const content = await readFileSafe(absoluteFile);
392
+
393
+ if (isVueRouterFile(content)) {
394
+ const vueRoutes = extractVueRoutes(content, file);
395
+ pages.push(...vueRoutes);
396
+ }
397
+ }
398
+ }
399
+
224
400
  // Extract repository metadata
225
401
  const metadata = await extractRepoMetadata(repoRoot);
226
402
 
227
403
  // Detect external API integrations
228
404
  const externalApis = await detectExternalApis(files, repoRoot);
229
405
 
230
- return {
406
+ const scanResult = {
231
407
  filesCount: files.length,
232
408
  modules,
233
409
  api,
@@ -235,6 +411,11 @@ export async function scanRepo(cfg) {
235
411
  metadata,
236
412
  externalApis
237
413
  };
414
+
415
+ // Track scan metrics
416
+ trackScan(scanResult);
417
+
418
+ return scanResult;
238
419
  }
239
420
 
240
421
  async function detectExternalApis(files, repoRoot) {
package/src/init.js CHANGED
@@ -46,6 +46,11 @@ jobs:
46
46
  env:
47
47
  NOTION_TOKEN: \${{ secrets.NOTION_TOKEN }}
48
48
  NOTION_PARENT_PAGE_ID: \${{ secrets.NOTION_PARENT_PAGE_ID }}
49
+ CONFLUENCE_URL: \${{ secrets.CONFLUENCE_URL }}
50
+ CONFLUENCE_EMAIL: \${{ secrets.CONFLUENCE_EMAIL }}
51
+ CONFLUENCE_API_TOKEN: \${{ secrets.CONFLUENCE_API_TOKEN }}
52
+ CONFLUENCE_SPACE_KEY: \${{ secrets.CONFLUENCE_SPACE_KEY }}
53
+ CONFLUENCE_PARENT_PAGE_ID: \${{ secrets.CONFLUENCE_PARENT_PAGE_ID }}
49
54
  run: npx @chappibunny/repolens@latest publish
50
55
  `;
51
56
 
@@ -54,6 +59,13 @@ NOTION_TOKEN=
54
59
  NOTION_PARENT_PAGE_ID=
55
60
  NOTION_VERSION=2022-06-28
56
61
 
62
+ # Confluence Publishing
63
+ CONFLUENCE_URL=https://your-company.atlassian.net/wiki
64
+ CONFLUENCE_EMAIL=your@email.com
65
+ CONFLUENCE_API_TOKEN=
66
+ CONFLUENCE_SPACE_KEY=DOCS
67
+ CONFLUENCE_PARENT_PAGE_ID=
68
+
57
69
  # AI-Assisted Documentation (Optional)
58
70
  # Enable AI features for natural language explanations
59
71
  # REPOLENS_AI_ENABLED=true
@@ -66,7 +78,7 @@ NOTION_VERSION=2022-06-28
66
78
 
67
79
  const DEFAULT_REPOLENS_README = `# RepoLens Documentation
68
80
 
69
- This repository is configured to use [RepoLens](https://github.com/CHAPIBUNNY/repolens) for automatic architecture documentation.
81
+ This repository is configured to use [RepoLens](https://github.com/CHAPIBUNNY/repolens) (@chappibunny/repolens) for automatic architecture documentation.
70
82
 
71
83
  ## šŸ“‹ What RepoLens Created
72
84
 
@@ -93,14 +105,37 @@ If you configured Notion credentials during setup, documentation will publish au
93
105
  - \`NOTION_TOKEN\` — Get from https://www.notion.so/my-integrations
94
106
  - \`NOTION_PARENT_PAGE_ID\` — The page where docs will be published
95
107
 
108
+ ### Confluence Publishing
109
+
110
+ To publish documentation to Atlassian Confluence:
111
+
112
+ 1. Copy \`.env.example\` to \`.env\`
113
+ 2. Generate an API token: https://id.atlassian.com/manage-profile/security/api-tokens
114
+ 3. Add your credentials:
115
+ - \`CONFLUENCE_URL\` — Your Confluence base URL (e.g., https://your-company.atlassian.net/wiki)
116
+ - \`CONFLUENCE_EMAIL\` — Your Atlassian account email
117
+ - \`CONFLUENCE_API_TOKEN\` — API token from step 2
118
+ - \`CONFLUENCE_SPACE_KEY\` — Space key (e.g., DOCS, ENG)
119
+ - \`CONFLUENCE_PARENT_PAGE_ID\` — (Optional) Parent page ID for nested docs
120
+
96
121
  ### GitHub Actions
97
122
 
98
123
  For automated publishing on every push:
99
124
 
100
125
  1. Go to repository Settings → Secrets → Actions
101
- 2. Add secrets:
126
+ 2. Add secrets for your chosen publisher(s):
127
+
128
+ **For Notion:**
102
129
  - \`NOTION_TOKEN\`
103
130
  - \`NOTION_PARENT_PAGE_ID\`
131
+
132
+ **For Confluence:**
133
+ - \`CONFLUENCE_URL\`
134
+ - \`CONFLUENCE_EMAIL\`
135
+ - \`CONFLUENCE_API_TOKEN\`
136
+ - \`CONFLUENCE_SPACE_KEY\`
137
+ - \`CONFLUENCE_PARENT_PAGE_ID\` (optional)
138
+
104
139
  3. Push to main branch
105
140
 
106
141
  ## šŸ“Š Generated Documentation
@@ -243,8 +278,9 @@ function buildRepoLensConfig(projectName, detectedRoots) {
243
278
  ` docs_title_prefix: "RepoLens"`,
244
279
  ``,
245
280
  `publishers:`,
246
- ` - markdown # Always generate local Markdown files`,
247
- ` - notion # Auto-detected: publishes if NOTION_TOKEN is set`,
281
+ ` - markdown # Always generate local Markdown files`,
282
+ ` - notion # Auto-detected: publishes if NOTION_TOKEN is set`,
283
+ ` - confluence # Auto-detected: publishes if CONFLUENCE_URL is set`,
248
284
  ``,
249
285
  `# Optional: Configure Notion publishing behavior`,
250
286
  `# notion:`,
@@ -253,6 +289,12 @@ function buildRepoLensConfig(projectName, detectedRoots) {
253
289
  `# - staging # Or add specific branches`,
254
290
  `# includeBranchInTitle: false # Clean titles without [branch-name]`,
255
291
  ``,
292
+ `# Optional: Configure Confluence publishing behavior`,
293
+ `# confluence:`,
294
+ `# branches:`,
295
+ `# - main # Only publish from main branch`,
296
+ `# - develop # Or add specific branches`,
297
+ ``,
256
298
  `# Optional: GitHub integration for SVG diagram hosting`,
257
299
  `# github:`,
258
300
  `# owner: "your-username"`,
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Discord Integration for RepoLens
3
+ * Sends rich embed notifications to Discord channels via webhooks
4
+ */
5
+
6
+ import fetch from "node-fetch";
7
+ import { info, warn, error } from "../utils/logger.js";
8
+
9
+ /**
10
+ * Send a notification to Discord webhook
11
+ * @param {string} webhookUrl - Discord webhook URL
12
+ * @param {object} payload - Notification payload
13
+ * @returns {Promise<boolean>} - Success status
14
+ */
15
+ export async function sendDiscordNotification(webhookUrl, payload) {
16
+ if (!webhookUrl) {
17
+ warn("Discord webhook URL not configured, skipping notification");
18
+ return false;
19
+ }
20
+
21
+ try {
22
+ const embed = buildEmbed(payload);
23
+ const body = {
24
+ username: "RepoLens",
25
+ avatar_url: "https://raw.githubusercontent.com/CHAPIBUNNY/repolens/main/.github/repolens-icon.png",
26
+ embeds: [embed],
27
+ };
28
+
29
+ const response = await fetch(webhookUrl, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify(body),
33
+ });
34
+
35
+ if (!response.ok) {
36
+ const text = await response.text();
37
+ error(`Discord webhook failed (${response.status}): ${text}`);
38
+ return false;
39
+ }
40
+
41
+ info("āœ“ Discord notification sent");
42
+ return true;
43
+ } catch (err) {
44
+ error(`Discord notification failed: ${err.message}`);
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Build Discord embed from payload
51
+ * @param {object} payload - Notification data
52
+ * @returns {object} - Discord embed object
53
+ */
54
+ function buildEmbed(payload) {
55
+ const {
56
+ title,
57
+ description,
58
+ color,
59
+ fields = [],
60
+ timestamp = new Date().toISOString(),
61
+ footer,
62
+ url,
63
+ } = payload;
64
+
65
+ // Color mapping: success (green), warning (yellow), error (red), info (blue)
66
+ const colorMap = {
67
+ success: 0x27ae60,
68
+ warning: 0xf39c12,
69
+ error: 0xe74c3c,
70
+ info: 0x3498db,
71
+ };
72
+
73
+ return {
74
+ title: title || "RABITAI Documentation Updated",
75
+ description: description || "Documentation has been regenerated",
76
+ color: typeof color === "string" ? colorMap[color] || colorMap.info : color || colorMap.info,
77
+ fields: fields.map((field) => ({
78
+ name: field.name,
79
+ value: field.value,
80
+ inline: field.inline !== false, // Default to inline
81
+ })),
82
+ timestamp,
83
+ footer: footer
84
+ ? { text: footer }
85
+ : { text: "RABITAI 🐰" },
86
+ url: url || undefined,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Build documentation update notification
92
+ * @param {object} options - Notification options
93
+ * @returns {object} - Notification payload
94
+ */
95
+ export function buildDocUpdateNotification(options) {
96
+ const {
97
+ branch,
98
+ commitSha,
99
+ commitMessage,
100
+ filesScanned,
101
+ modulesDetected,
102
+ coverage,
103
+ notionUrl,
104
+ changePercent,
105
+ } = options;
106
+
107
+ const fields = [];
108
+
109
+ // Branch and commit
110
+ if (branch) {
111
+ fields.push({
112
+ name: "šŸ“Œ Branch",
113
+ value: `\`${branch}\``,
114
+ inline: true,
115
+ });
116
+ }
117
+
118
+ if (commitSha) {
119
+ fields.push({
120
+ name: "šŸ”– Commit",
121
+ value: `\`${commitSha.substring(0, 7)}\``,
122
+ inline: true,
123
+ });
124
+ }
125
+
126
+ // Repository stats
127
+ if (filesScanned !== undefined) {
128
+ fields.push({
129
+ name: "šŸ“ Files Scanned",
130
+ value: filesScanned.toLocaleString(),
131
+ inline: true,
132
+ });
133
+ }
134
+
135
+ if (modulesDetected !== undefined) {
136
+ fields.push({
137
+ name: "šŸ“¦ Modules Detected",
138
+ value: modulesDetected.toLocaleString(),
139
+ inline: true,
140
+ });
141
+ }
142
+
143
+ // Coverage
144
+ if (coverage !== undefined) {
145
+ const coverageEmoji = coverage >= 80 ? "🟢" : coverage >= 60 ? "🟔" : "šŸ”“";
146
+ fields.push({
147
+ name: `${coverageEmoji} Coverage`,
148
+ value: `${coverage.toFixed(1)}%`,
149
+ inline: true,
150
+ });
151
+ }
152
+
153
+ // Change percent
154
+ if (changePercent !== undefined) {
155
+ const changeEmoji = changePercent >= 20 ? "āš ļø" : "šŸ“Š";
156
+ fields.push({
157
+ name: `${changeEmoji} Changes`,
158
+ value: `${changePercent.toFixed(1)}%`,
159
+ inline: true,
160
+ });
161
+ }
162
+
163
+ // Links
164
+ const links = [];
165
+ if (notionUrl) {
166
+ links.push(`[šŸ“š Notion Docs](${notionUrl})`);
167
+ }
168
+
169
+ if (links.length > 0) {
170
+ fields.push({
171
+ name: "šŸ”— Quick Links",
172
+ value: links.join(" • "),
173
+ inline: false,
174
+ });
175
+ }
176
+
177
+ // Determine color based on change magnitude
178
+ let color = "success";
179
+ if (changePercent !== undefined) {
180
+ if (changePercent >= 20) {
181
+ color = "warning";
182
+ } else if (changePercent >= 50) {
183
+ color = "error";
184
+ }
185
+ }
186
+
187
+ return {
188
+ title: "šŸ“ Documentation Updated",
189
+ description: commitMessage
190
+ ? `*${commitMessage.split("\n")[0].substring(0, 100)}${commitMessage.length > 100 ? "..." : ""}*`
191
+ : "Architecture documentation has been regenerated",
192
+ color,
193
+ fields,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Build error notification
199
+ * @param {object} options - Error options
200
+ * @returns {object} - Notification payload
201
+ */
202
+ export function buildErrorNotification(options) {
203
+ const { errorMessage, command, branch, commitSha } = options;
204
+
205
+ const fields = [];
206
+
207
+ if (command) {
208
+ fields.push({
209
+ name: "āš™ļø Command",
210
+ value: `\`${command}\``,
211
+ inline: true,
212
+ });
213
+ }
214
+
215
+ if (branch) {
216
+ fields.push({
217
+ name: "šŸ“Œ Branch",
218
+ value: `\`${branch}\``,
219
+ inline: true,
220
+ });
221
+ }
222
+
223
+ if (commitSha) {
224
+ fields.push({
225
+ name: "šŸ”– Commit",
226
+ value: `\`${commitSha.substring(0, 7)}\``,
227
+ inline: true,
228
+ });
229
+ }
230
+
231
+ if (errorMessage) {
232
+ fields.push({
233
+ name: "āŒ Error",
234
+ value: `\`\`\`${errorMessage.substring(0, 500)}\`\`\``,
235
+ inline: false,
236
+ });
237
+ }
238
+
239
+ return {
240
+ title: "🚨 RABITAI Error",
241
+ description: "Documentation generation failed",
242
+ color: "error",
243
+ fields,
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Should send notification based on change threshold
249
+ * @param {number} changePercent - Percentage of changes
250
+ * @param {string} notifyOn - Notification policy (always, significant, never)
251
+ * @param {number} significantThreshold - Threshold for significant changes (default: 10)
252
+ * @returns {boolean} - Whether to send notification
253
+ */
254
+ export function shouldNotify(changePercent, notifyOn = "significant", significantThreshold = 10) {
255
+ if (notifyOn === "always") return true;
256
+ if (notifyOn === "never") return false;
257
+ if (notifyOn === "significant") {
258
+ return changePercent === undefined || changePercent >= significantThreshold;
259
+ }
260
+ return false;
261
+ }
package/src/migrate.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { info, warn, error as logError } from "./utils/logger.js";
4
+ import { trackMigration } from "./utils/telemetry.js";
4
5
 
5
6
  /**
6
7
  * Detect legacy workflow patterns that need migration
@@ -242,6 +243,12 @@ export async function runMigrate(targetDir = process.cwd(), options = {}) {
242
243
  } else {
243
244
  console.log("\n✨ All workflows are up to date!");
244
245
  }
246
+
247
+ // Track migration metrics
248
+ trackMigration(migratedCount, skippedCount);
249
+
250
+ // Return results for telemetry tracking
251
+ return { migratedCount, skippedCount };
245
252
 
246
253
  } catch (err) {
247
254
  logError(`Migration failed: ${err.message}`);