@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/CHANGELOG.md +131 -0
- package/README.md +414 -64
- package/package.json +16 -4
- package/src/ai/provider.js +48 -45
- package/src/cli.js +117 -9
- package/src/core/config-schema.js +43 -1
- package/src/core/config.js +20 -3
- package/src/core/scan.js +184 -3
- package/src/init.js +46 -4
- package/src/integrations/discord.js +261 -0
- package/src/migrate.js +7 -0
- package/src/publishers/confluence.js +428 -0
- package/src/publishers/index.js +112 -4
- package/src/publishers/notion.js +20 -16
- package/src/publishers/publish.js +1 -1
- package/src/renderers/render.js +32 -2
- package/src/utils/branch.js +32 -0
- package/src/utils/logger.js +21 -4
- package/src/utils/metrics.js +361 -0
- package/src/utils/rate-limit.js +289 -0
- package/src/utils/secrets.js +240 -0
- package/src/utils/telemetry.js +375 -0
- package/src/utils/validate.js +382 -0
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
|
-
|
|
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
|
|
247
|
-
` - notion
|
|
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}`);
|