@chappibunny/repolens 1.10.0 โ 1.12.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 +93 -0
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/ai/document-plan.js +16 -0
- package/src/ai/generate-sections.js +6 -6
- package/src/ai/provider.js +27 -3
- package/src/analyzers/complexity-analyzer.js +297 -0
- package/src/analyzers/jsdoc-analyzer.js +354 -0
- package/src/analyzers/security-patterns.js +329 -0
- package/src/docs/generate-doc-set.js +34 -4
- package/src/init.js +484 -41
- package/src/publishers/github-wiki.js +10 -1
- package/src/publishers/markdown.js +3 -1
- package/src/renderers/render.js +96 -1
- package/src/renderers/renderAnalysis.js +184 -0
package/src/init.js
CHANGED
|
@@ -5,14 +5,241 @@ import { exec } from "node:child_process";
|
|
|
5
5
|
import { info, warn } from "./utils/logger.js";
|
|
6
6
|
|
|
7
7
|
const PUBLISHER_CHOICES = ["markdown", "notion", "confluence", "github_wiki"];
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// URL PARSING HELPERS
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a Confluence URL and extract base URL, space key, and page ID.
|
|
15
|
+
* Handles various URL formats:
|
|
16
|
+
* - Full page URL: https://company.atlassian.net/wiki/spaces/DOCS/pages/123456/Page+Title
|
|
17
|
+
* - Space URL: https://company.atlassian.net/wiki/spaces/DOCS
|
|
18
|
+
* - Base URL: https://company.atlassian.net/wiki
|
|
19
|
+
* Returns: { baseUrl, spaceKey, pageId, isFullUrl }
|
|
20
|
+
*/
|
|
21
|
+
function parseConfluenceUrl(input) {
|
|
22
|
+
if (!input) return { baseUrl: null, spaceKey: null, pageId: null, isFullUrl: false };
|
|
23
|
+
|
|
24
|
+
input = input.trim();
|
|
25
|
+
|
|
26
|
+
// Remove query params and hash
|
|
27
|
+
const cleanUrl = input.split("?")[0].split("#")[0];
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(cleanUrl);
|
|
31
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
32
|
+
|
|
33
|
+
// Find '/wiki' position
|
|
34
|
+
const wikiIndex = pathParts.indexOf("wiki");
|
|
35
|
+
if (wikiIndex === -1) {
|
|
36
|
+
// No /wiki in path - might just be base domain
|
|
37
|
+
return {
|
|
38
|
+
baseUrl: `${url.protocol}//${url.host}/wiki`,
|
|
39
|
+
spaceKey: null,
|
|
40
|
+
pageId: null,
|
|
41
|
+
isFullUrl: false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const baseUrl = `${url.protocol}//${url.host}/wiki`;
|
|
46
|
+
let spaceKey = null;
|
|
47
|
+
let pageId = null;
|
|
48
|
+
|
|
49
|
+
// Look for /spaces/SPACE_KEY pattern
|
|
50
|
+
const spacesIndex = pathParts.indexOf("spaces");
|
|
51
|
+
if (spacesIndex !== -1 && pathParts[spacesIndex + 1]) {
|
|
52
|
+
spaceKey = pathParts[spacesIndex + 1];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Look for /pages/PAGE_ID pattern
|
|
56
|
+
const pagesIndex = pathParts.indexOf("pages");
|
|
57
|
+
if (pagesIndex !== -1 && pathParts[pagesIndex + 1]) {
|
|
58
|
+
pageId = pathParts[pagesIndex + 1];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
baseUrl,
|
|
63
|
+
spaceKey,
|
|
64
|
+
pageId,
|
|
65
|
+
isFullUrl: Boolean(spaceKey || pageId),
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
// Not a valid URL - return as-is
|
|
69
|
+
return { baseUrl: input, spaceKey: null, pageId: null, isFullUrl: false };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse a Notion URL or page ID and extract the page ID.
|
|
75
|
+
* Handles various formats:
|
|
76
|
+
* - Full URL: https://www.notion.so/workspace/Page-Title-abc123def456
|
|
77
|
+
* - Short URL: https://notion.so/abc123def456
|
|
78
|
+
* - Just the page ID: abc123def456 or abc123-def456-...
|
|
79
|
+
* Returns: { pageId, isUrl }
|
|
80
|
+
*/
|
|
81
|
+
function parseNotionInput(input) {
|
|
82
|
+
if (!input) return { pageId: null, isUrl: false };
|
|
83
|
+
|
|
84
|
+
input = input.trim();
|
|
85
|
+
|
|
86
|
+
// Check if it looks like a URL
|
|
87
|
+
if (input.includes("notion.so") || input.includes("notion.site")) {
|
|
88
|
+
try {
|
|
89
|
+
const url = new URL(input.startsWith("http") ? input : `https://${input}`);
|
|
90
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
91
|
+
|
|
92
|
+
// The last path segment typically contains the page ID
|
|
93
|
+
// Format: "Page-Title-abc123def456ghi789" - ID is the last 32 hex chars
|
|
94
|
+
const lastPart = pathParts[pathParts.length - 1] || "";
|
|
95
|
+
|
|
96
|
+
// Try to extract the 32-char hex ID from the end
|
|
97
|
+
const idMatch = lastPart.match(/([a-f0-9]{32})$/i);
|
|
98
|
+
if (idMatch) {
|
|
99
|
+
return { pageId: idMatch[1], isUrl: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Try format with dashes: abc123-def456-...
|
|
103
|
+
const dashedMatch = lastPart.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/i);
|
|
104
|
+
if (dashedMatch) {
|
|
105
|
+
return { pageId: dashedMatch[1].replace(/-/g, ""), isUrl: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Last segment might be the ID directly
|
|
109
|
+
const cleanId = lastPart.replace(/-/g, "");
|
|
110
|
+
if (/^[a-f0-9]{32}$/i.test(cleanId)) {
|
|
111
|
+
return { pageId: cleanId, isUrl: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { pageId: null, isUrl: true };
|
|
115
|
+
} catch {
|
|
116
|
+
return { pageId: null, isUrl: false };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Not a URL - might be raw page ID
|
|
121
|
+
const cleanId = input.replace(/-/g, "");
|
|
122
|
+
if (/^[a-f0-9]{32}$/i.test(cleanId)) {
|
|
123
|
+
return { pageId: cleanId, isUrl: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Return as-is (might be invalid)
|
|
127
|
+
return { pageId: input, isUrl: false };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Test Confluence credentials by fetching space info.
|
|
132
|
+
*/
|
|
133
|
+
async function testConfluenceCredentials(url, email, apiToken, spaceKey) {
|
|
134
|
+
try {
|
|
135
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
136
|
+
const endpoint = `${url}/rest/api/space/${spaceKey}`;
|
|
137
|
+
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
140
|
+
|
|
141
|
+
const response = await fetch(endpoint, {
|
|
142
|
+
method: "GET",
|
|
143
|
+
headers: {
|
|
144
|
+
"Authorization": `Basic ${auth}`,
|
|
145
|
+
"Accept": "application/json",
|
|
146
|
+
},
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
|
|
152
|
+
if (response.ok) {
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
return { success: true, spaceName: data.name };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (response.status === 401) {
|
|
158
|
+
return { success: false, error: "Invalid email or API token" };
|
|
159
|
+
}
|
|
160
|
+
if (response.status === 404) {
|
|
161
|
+
return { success: false, error: `Space '${spaceKey}' not found` };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { success: false, error: `API error ${response.status}` };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (err.name === "AbortError") {
|
|
167
|
+
return { success: false, error: "Connection timed out" };
|
|
168
|
+
}
|
|
169
|
+
return { success: false, error: err.message };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Test Notion credentials by fetching page info.
|
|
175
|
+
*/
|
|
176
|
+
async function testNotionCredentials(token, pageId) {
|
|
177
|
+
try {
|
|
178
|
+
const endpoint = `https://api.notion.com/v1/pages/${pageId}`;
|
|
179
|
+
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
182
|
+
|
|
183
|
+
const response = await fetch(endpoint, {
|
|
184
|
+
method: "GET",
|
|
185
|
+
headers: {
|
|
186
|
+
"Authorization": `Bearer ${token}`,
|
|
187
|
+
"Notion-Version": "2022-06-28",
|
|
188
|
+
},
|
|
189
|
+
signal: controller.signal,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
clearTimeout(timeoutId);
|
|
193
|
+
|
|
194
|
+
if (response.ok) {
|
|
195
|
+
return { success: true };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
199
|
+
|
|
200
|
+
if (response.status === 401) {
|
|
201
|
+
return { success: false, error: "Invalid token" };
|
|
202
|
+
}
|
|
203
|
+
if (response.status === 404) {
|
|
204
|
+
return { success: false, error: "Page not found โ make sure your integration has access to this page" };
|
|
205
|
+
}
|
|
206
|
+
if (errorBody.code === "object_not_found") {
|
|
207
|
+
return { success: false, error: "Page not found โ share the page with your integration first" };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { success: false, error: `API error ${response.status}: ${errorBody.message || ""}` };
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (err.name === "AbortError") {
|
|
213
|
+
return { success: false, error: "Connection timed out" };
|
|
214
|
+
}
|
|
215
|
+
return { success: false, error: err.message };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
8
218
|
const AI_PROVIDERS = [
|
|
9
219
|
{ value: "github", label: "GitHub Models (free in GitHub Actions)", signupUrl: null },
|
|
10
220
|
{ value: "openai_compatible", label: "OpenAI / Compatible (GPT-5, GPT-4o, etc.)", signupUrl: "https://platform.openai.com/api-keys" },
|
|
11
221
|
{ value: "anthropic", label: "Anthropic (Claude)", signupUrl: "https://console.anthropic.com/settings/keys" },
|
|
12
222
|
{ value: "google", label: "Google (Gemini)", signupUrl: "https://aistudio.google.com/app/apikey" },
|
|
13
223
|
];
|
|
224
|
+
|
|
225
|
+
// All file extensions we can analyze
|
|
226
|
+
const ALL_EXTENSIONS = "js,ts,jsx,tsx,mjs,cjs,py,go,rs,java,rb,php,cs,swift,kt,scala,vue,svelte";
|
|
227
|
+
|
|
14
228
|
const SCAN_PRESETS = {
|
|
229
|
+
// Universal preset - scans ALL supported languages
|
|
230
|
+
universal: {
|
|
231
|
+
label: "Universal (all languages)",
|
|
232
|
+
description: "Scans all supported file types โ best for polyglot projects",
|
|
233
|
+
include: [
|
|
234
|
+
`**/*.{${ALL_EXTENSIONS}}`,
|
|
235
|
+
],
|
|
236
|
+
roots: ["src", "lib", "app", "pkg", "internal", "cmd", "packages"],
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// JavaScript/TypeScript ecosystems
|
|
15
240
|
nextjs: {
|
|
241
|
+
label: "Next.js / React",
|
|
242
|
+
description: "React, Next.js, and frontend TypeScript projects",
|
|
16
243
|
include: [
|
|
17
244
|
"src/**/*.{ts,tsx,js,jsx}",
|
|
18
245
|
"app/**/*.{ts,tsx,js,jsx}",
|
|
@@ -23,20 +250,65 @@ const SCAN_PRESETS = {
|
|
|
23
250
|
roots: ["src", "app", "pages", "lib", "components"],
|
|
24
251
|
},
|
|
25
252
|
express: {
|
|
253
|
+
label: "Express / Node.js",
|
|
254
|
+
description: "Node.js backend APIs and Express servers",
|
|
26
255
|
include: [
|
|
27
|
-
"src/**/*.{ts,js}",
|
|
256
|
+
"src/**/*.{ts,js,mjs,cjs}",
|
|
28
257
|
"routes/**/*.{ts,js}",
|
|
29
258
|
"controllers/**/*.{ts,js}",
|
|
30
259
|
"models/**/*.{ts,js}",
|
|
31
260
|
"middleware/**/*.{ts,js}",
|
|
261
|
+
"services/**/*.{ts,js}",
|
|
262
|
+
],
|
|
263
|
+
roots: ["src", "routes", "controllers", "models", "services"],
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
// Python ecosystem
|
|
267
|
+
python: {
|
|
268
|
+
label: "Python",
|
|
269
|
+
description: "Django, Flask, FastAPI, and general Python projects",
|
|
270
|
+
include: [
|
|
271
|
+
"**/*.py",
|
|
272
|
+
],
|
|
273
|
+
roots: ["src", "app", "lib", "api", "core", "services", "models", "views", "utils"],
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// Go ecosystem
|
|
277
|
+
golang: {
|
|
278
|
+
label: "Go",
|
|
279
|
+
description: "Go modules with standard layout",
|
|
280
|
+
include: [
|
|
281
|
+
"**/*.go",
|
|
282
|
+
],
|
|
283
|
+
roots: ["cmd", "pkg", "internal", "api", "server", "handlers"],
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// Rust ecosystem
|
|
287
|
+
rust: {
|
|
288
|
+
label: "Rust",
|
|
289
|
+
description: "Cargo projects and Rust libraries",
|
|
290
|
+
include: [
|
|
291
|
+
"**/*.rs",
|
|
32
292
|
],
|
|
33
|
-
roots: ["src", "
|
|
293
|
+
roots: ["src", "lib", "bin", "examples"],
|
|
34
294
|
},
|
|
295
|
+
|
|
296
|
+
// Java/JVM ecosystem
|
|
297
|
+
java: {
|
|
298
|
+
label: "Java / Kotlin / Scala",
|
|
299
|
+
description: "JVM projects (Maven/Gradle)",
|
|
300
|
+
include: [
|
|
301
|
+
"**/*.{java,kt,scala}",
|
|
302
|
+
],
|
|
303
|
+
roots: ["src/main/java", "src/main/kotlin", "src/main/scala", "app", "core", "service"],
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
// Legacy "generic" preserved but improved
|
|
35
307
|
generic: {
|
|
308
|
+
label: "JavaScript/TypeScript only",
|
|
309
|
+
description: "Traditional JS/TS projects",
|
|
36
310
|
include: [
|
|
37
|
-
"
|
|
38
|
-
"app/**/*.{ts,tsx,js,jsx,md}",
|
|
39
|
-
"lib/**/*.{ts,tsx,js,jsx,md}",
|
|
311
|
+
"**/*.{ts,tsx,js,jsx,mjs,cjs}",
|
|
40
312
|
],
|
|
41
313
|
roots: ["src", "app", "lib"],
|
|
42
314
|
},
|
|
@@ -709,20 +981,73 @@ async function runInteractiveWizard(repoRoot) {
|
|
|
709
981
|
// Notion setup
|
|
710
982
|
if (publishers.includes("notion")) {
|
|
711
983
|
info("\n๐ Notion Setup");
|
|
712
|
-
info("
|
|
713
|
-
info("
|
|
984
|
+
info(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
|
|
985
|
+
info(" โ Step 1: Get your Integration Token โ");
|
|
986
|
+
info(" โ โ https://www.notion.so/my-integrations โ");
|
|
987
|
+
info(" โ โ Create new integration โ Copy 'Internal Integration Token'โ");
|
|
988
|
+
info(" โ โ");
|
|
989
|
+
info(" โ Step 2: Share your page with the integration โ");
|
|
990
|
+
info(" โ โ Open the page in Notion โ");
|
|
991
|
+
info(" โ โ Click '...' โ 'Add connections' โ Select your integrationโ");
|
|
992
|
+
info(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
|
|
714
993
|
|
|
715
|
-
const setupNow = (await ask(" Configure Notion credentials now? (Y/n): ")).trim().toLowerCase();
|
|
994
|
+
const setupNow = (await ask("\n Configure Notion credentials now? (Y/n): ")).trim().toLowerCase();
|
|
716
995
|
if (setupNow !== "n") {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
996
|
+
// Token first
|
|
997
|
+
const openNotionPage = (await ask(" Open Notion integrations page in browser? (Y/n): ")).trim().toLowerCase();
|
|
998
|
+
if (openNotionPage !== "n") {
|
|
999
|
+
await openUrl("https://www.notion.so/my-integrations");
|
|
1000
|
+
info(" Opening browser... Create or copy your integration token.\n");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const token = (await ask(" NOTION_TOKEN (paste your secret_... token): ")).trim();
|
|
1004
|
+
|
|
1005
|
+
if (!token) {
|
|
1006
|
+
warn(" No token provided. Skipping Notion setup.");
|
|
724
1007
|
} else {
|
|
725
|
-
|
|
1008
|
+
// Page ID - accept URL or ID
|
|
1009
|
+
info("\n Now paste either:");
|
|
1010
|
+
info(" โข The full Notion page URL, OR");
|
|
1011
|
+
info(" โข Just the 32-character page ID\n");
|
|
1012
|
+
const pageInput = (await ask(" NOTION_PARENT_PAGE_ID (URL or ID): ")).trim();
|
|
1013
|
+
|
|
1014
|
+
const { pageId, isUrl } = parseNotionInput(pageInput);
|
|
1015
|
+
|
|
1016
|
+
if (!pageId) {
|
|
1017
|
+
warn(" Could not extract page ID from input. Please enter the 32-char ID directly.");
|
|
1018
|
+
const retryId = (await ask(" Page ID: ")).trim();
|
|
1019
|
+
if (retryId) {
|
|
1020
|
+
const retryParsed = parseNotionInput(retryId);
|
|
1021
|
+
if (retryParsed.pageId) {
|
|
1022
|
+
credentials.notion = { token, parentPageId: retryParsed.pageId };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} else {
|
|
1026
|
+
if (isUrl) {
|
|
1027
|
+
info(` โ Extracted page ID: ${pageId}`);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Test credentials
|
|
1031
|
+
info(" Testing Notion connection...");
|
|
1032
|
+
const testResult = await testNotionCredentials(token, pageId);
|
|
1033
|
+
|
|
1034
|
+
if (testResult.success) {
|
|
1035
|
+
info(" โ Connection successful! Your integration can access this page.");
|
|
1036
|
+
credentials.notion = { token, parentPageId: pageId };
|
|
1037
|
+
} else {
|
|
1038
|
+
warn(` โ Connection failed: ${testResult.error}`);
|
|
1039
|
+
info(" Common fixes:");
|
|
1040
|
+
info(" โข Make sure you shared the page with your integration");
|
|
1041
|
+
info(" โข Check that the token is correct (starts with 'secret_')");
|
|
1042
|
+
const saveAnyway = (await ask(" Save credentials anyway? (y/N): ")).trim().toLowerCase();
|
|
1043
|
+
if (saveAnyway === "y") {
|
|
1044
|
+
credentials.notion = { token, parentPageId: pageId };
|
|
1045
|
+
info(" โ Credentials saved (verify manually later)");
|
|
1046
|
+
} else {
|
|
1047
|
+
warn(" Skipping Notion setup. Configure manually later.");
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
726
1051
|
}
|
|
727
1052
|
}
|
|
728
1053
|
githubSecretsNeeded.push("NOTION_TOKEN", "NOTION_PARENT_PAGE_ID");
|
|
@@ -731,24 +1056,107 @@ async function runInteractiveWizard(repoRoot) {
|
|
|
731
1056
|
// Confluence setup
|
|
732
1057
|
if (publishers.includes("confluence")) {
|
|
733
1058
|
info("\n๐ Confluence Setup");
|
|
734
|
-
info("
|
|
735
|
-
info("
|
|
1059
|
+
info(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
|
|
1060
|
+
info(" โ You'll need: โ");
|
|
1061
|
+
info(" โ โข Base URL (e.g., https://company.atlassian.net/wiki) โ");
|
|
1062
|
+
info(" โ โข Email address for your Atlassian account โ");
|
|
1063
|
+
info(" โ โข API token from: id.atlassian.com/manage-profile/security โ");
|
|
1064
|
+
info(" โ โข Space key (e.g., DOCS, ENG, ~username for personal) โ");
|
|
1065
|
+
info(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
|
|
736
1066
|
|
|
737
|
-
const setupNow = (await ask(" Configure Confluence credentials now? (Y/n): ")).trim().toLowerCase();
|
|
1067
|
+
const setupNow = (await ask("\n Configure Confluence credentials now? (Y/n): ")).trim().toLowerCase();
|
|
738
1068
|
if (setupNow !== "n") {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1069
|
+
// Open API token page
|
|
1070
|
+
const openAtlassian = (await ask(" Open Atlassian API token page in browser? (Y/n): ")).trim().toLowerCase();
|
|
1071
|
+
if (openAtlassian !== "n") {
|
|
1072
|
+
await openUrl("https://id.atlassian.com/manage-profile/security/api-tokens");
|
|
1073
|
+
info(" Opening browser... Create an API token and copy it.\n");
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Ask for URL first - it can contain space key and page ID
|
|
1077
|
+
info("\n Paste either:");
|
|
1078
|
+
info(" โข A full Confluence page URL (we'll extract the details), OR");
|
|
1079
|
+
info(" โข Just the base URL (e.g., https://company.atlassian.net/wiki)\n");
|
|
1080
|
+
|
|
1081
|
+
const urlInput = (await ask(" CONFLUENCE_URL: ")).trim();
|
|
1082
|
+
const parsed = parseConfluenceUrl(urlInput);
|
|
1083
|
+
|
|
1084
|
+
let baseUrl = parsed.baseUrl;
|
|
1085
|
+
let spaceKey = parsed.spaceKey;
|
|
1086
|
+
let pageId = parsed.pageId;
|
|
1087
|
+
|
|
1088
|
+
if (parsed.isFullUrl) {
|
|
1089
|
+
info(` โ Detected full URL! Extracted:`);
|
|
1090
|
+
info(` โข Base URL: ${baseUrl}`);
|
|
1091
|
+
if (spaceKey) info(` โข Space Key: ${spaceKey}`);
|
|
1092
|
+
if (pageId) info(` โข Page ID: ${pageId}`);
|
|
1093
|
+
|
|
1094
|
+
// Confirm extracted base URL
|
|
1095
|
+
const confirmBase = (await ask(`\n Use '${baseUrl}' as base URL? (Y/n): `)).trim().toLowerCase();
|
|
1096
|
+
if (confirmBase === "n") {
|
|
1097
|
+
baseUrl = (await ask(" Enter base URL: ")).trim();
|
|
1098
|
+
}
|
|
1099
|
+
} else if (!baseUrl || !baseUrl.includes("/wiki")) {
|
|
1100
|
+
warn(" URL should include /wiki (e.g., https://company.atlassian.net/wiki)");
|
|
1101
|
+
baseUrl = (await ask(" Enter base URL (with /wiki): ")).trim();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Email
|
|
1105
|
+
const email = (await ask(" CONFLUENCE_EMAIL (your Atlassian email): ")).trim();
|
|
1106
|
+
|
|
1107
|
+
// API Token
|
|
1108
|
+
const apiToken = (await ask(" CONFLUENCE_API_TOKEN (paste from Atlassian): ")).trim();
|
|
1109
|
+
|
|
1110
|
+
// Space Key - use extracted or ask
|
|
1111
|
+
if (spaceKey) {
|
|
1112
|
+
const confirmSpace = (await ask(`\n Use space key '${spaceKey}'? (Y/n): `)).trim().toLowerCase();
|
|
1113
|
+
if (confirmSpace === "n") {
|
|
1114
|
+
info(" Space key examples: DOCS, ENG, DEV, ~username (personal)");
|
|
1115
|
+
spaceKey = (await ask(" CONFLUENCE_SPACE_KEY: ")).trim();
|
|
1116
|
+
}
|
|
1117
|
+
} else {
|
|
1118
|
+
info("\n Space key is in the URL: /wiki/spaces/SPACE_KEY/...");
|
|
1119
|
+
info(" Examples: DOCS, ENG, DEV, ~username (for personal spaces)");
|
|
1120
|
+
spaceKey = (await ask(" CONFLUENCE_SPACE_KEY: ")).trim();
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Page ID - use extracted or ask
|
|
1124
|
+
if (pageId) {
|
|
1125
|
+
const confirmPage = (await ask(` Use page ID '${pageId}' as parent? (Y/n): `)).trim().toLowerCase();
|
|
1126
|
+
if (confirmPage === "n") {
|
|
1127
|
+
pageId = (await ask(" CONFLUENCE_PARENT_PAGE_ID (optional, press Enter to skip): ")).trim() || null;
|
|
1128
|
+
}
|
|
1129
|
+
} else {
|
|
1130
|
+
info("\n Parent page ID is in the URL: /wiki/spaces/.../pages/PAGE_ID/...");
|
|
1131
|
+
info(" (Optional - leave blank to publish at space root)");
|
|
1132
|
+
pageId = (await ask(" CONFLUENCE_PARENT_PAGE_ID: ")).trim() || null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Validate required fields
|
|
1136
|
+
if (!baseUrl || !email || !apiToken || !spaceKey) {
|
|
1137
|
+
warn(" Missing required fields. Skipping Confluence setup.");
|
|
750
1138
|
} else {
|
|
751
|
-
|
|
1139
|
+
// Test connection
|
|
1140
|
+
info("\n Testing Confluence connection...");
|
|
1141
|
+
const testResult = await testConfluenceCredentials(baseUrl, email, apiToken, spaceKey);
|
|
1142
|
+
|
|
1143
|
+
if (testResult.success) {
|
|
1144
|
+
info(` โ Connection successful! Found space: "${testResult.spaceName}"`);
|
|
1145
|
+
credentials.confluence = { url: baseUrl, email, apiToken, spaceKey, parentPageId: pageId };
|
|
1146
|
+
} else {
|
|
1147
|
+
warn(` โ Connection failed: ${testResult.error}`);
|
|
1148
|
+
info(" Common issues:");
|
|
1149
|
+
info(" โข API token is for your Atlassian account (not Confluence)");
|
|
1150
|
+
info(" โข Space key is case-sensitive (check URL)");
|
|
1151
|
+
info(" โข Make sure you have access to the space");
|
|
1152
|
+
const saveAnyway = (await ask(" Save credentials anyway? (y/N): ")).trim().toLowerCase();
|
|
1153
|
+
if (saveAnyway === "y") {
|
|
1154
|
+
credentials.confluence = { url: baseUrl, email, apiToken, spaceKey, parentPageId: pageId };
|
|
1155
|
+
info(" โ Credentials saved (verify manually later)");
|
|
1156
|
+
} else {
|
|
1157
|
+
warn(" Skipping Confluence setup. Configure manually later.");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
752
1160
|
}
|
|
753
1161
|
}
|
|
754
1162
|
githubSecretsNeeded.push("CONFLUENCE_URL", "CONFLUENCE_EMAIL", "CONFLUENCE_API_TOKEN", "CONFLUENCE_SPACE_KEY", "CONFLUENCE_PARENT_PAGE_ID");
|
|
@@ -850,14 +1258,29 @@ async function runInteractiveWizard(repoRoot) {
|
|
|
850
1258
|
}
|
|
851
1259
|
}
|
|
852
1260
|
|
|
853
|
-
// 5. Scan preset
|
|
854
|
-
info("\n๐
|
|
1261
|
+
// 5. Scan preset - detect language from files in repo
|
|
1262
|
+
info("\n๐ Language/Framework Preset");
|
|
1263
|
+
info(" Determines which file types and directories to scan.\n");
|
|
1264
|
+
|
|
855
1265
|
const presetKeys = Object.keys(SCAN_PRESETS);
|
|
856
|
-
presetKeys.forEach((
|
|
857
|
-
|
|
1266
|
+
presetKeys.forEach((key, i) => {
|
|
1267
|
+
const preset = SCAN_PRESETS[key];
|
|
1268
|
+
const num = (i + 1).toString().padStart(2);
|
|
1269
|
+
info(` ${num}. ${preset.label}`);
|
|
1270
|
+
info(` ${preset.description}`);
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
// Default to universal (index 0) since it works for everything
|
|
1274
|
+
const defaultPresetIdx = 1;
|
|
1275
|
+
const defaultPreset = presetKeys[defaultPresetIdx - 1];
|
|
1276
|
+
const defaultLabel = SCAN_PRESETS[defaultPreset].label;
|
|
1277
|
+
|
|
1278
|
+
const presetInput = (await ask(`\nPreset [${defaultPresetIdx}] (default: ${defaultPresetIdx} ${defaultLabel}): `)).trim() || String(defaultPresetIdx);
|
|
858
1279
|
const presetIdx = parseInt(presetInput, 10);
|
|
859
|
-
const presetKey = presetKeys[(presetIdx >= 1 && presetIdx <= presetKeys.length) ? presetIdx - 1 :
|
|
1280
|
+
const presetKey = presetKeys[(presetIdx >= 1 && presetIdx <= presetKeys.length) ? presetIdx - 1 : defaultPresetIdx - 1];
|
|
860
1281
|
const preset = SCAN_PRESETS[presetKey];
|
|
1282
|
+
|
|
1283
|
+
info(` โ Selected: ${preset.label}`);
|
|
861
1284
|
|
|
862
1285
|
// 6. Branch filtering
|
|
863
1286
|
info("\n๐ฟ Branch Filtering");
|
|
@@ -886,7 +1309,7 @@ async function runInteractiveWizard(repoRoot) {
|
|
|
886
1309
|
info(` Project: ${projectName}`);
|
|
887
1310
|
info(` Publishers: ${publishers.join(", ")}`);
|
|
888
1311
|
info(` AI: ${enableAi ? `Enabled (${aiProvider})` : "Disabled"}`);
|
|
889
|
-
info(` Scan: ${
|
|
1312
|
+
info(` Scan: ${preset.label}`);
|
|
890
1313
|
info(` Branches: ${branches.join(", ")}`);
|
|
891
1314
|
info(` Discord: ${enableDiscord ? "Enabled" : "Disabled"}`);
|
|
892
1315
|
|
|
@@ -1083,6 +1506,9 @@ function buildEnvFromCredentials(credentials) {
|
|
|
1083
1506
|
return lines.join("\n");
|
|
1084
1507
|
}
|
|
1085
1508
|
|
|
1509
|
+
// Export helper functions for testing
|
|
1510
|
+
export { parseConfluenceUrl, parseNotionInput };
|
|
1511
|
+
|
|
1086
1512
|
export async function runInit(targetDir = process.cwd(), options = {}) {
|
|
1087
1513
|
const repoRoot = path.resolve(targetDir);
|
|
1088
1514
|
|
|
@@ -1210,11 +1636,28 @@ NOTION_VERSION=2022-06-28
|
|
|
1210
1636
|
}
|
|
1211
1637
|
}
|
|
1212
1638
|
|
|
1639
|
+
// Show .env sourcing instructions if credentials were collected
|
|
1640
|
+
const hasLocalCredentials = wizardAnswers.credentials && Object.keys(wizardAnswers.credentials).length > 0;
|
|
1641
|
+
|
|
1213
1642
|
info("\n๐ Next steps:");
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1643
|
+
if (hasLocalCredentials) {
|
|
1644
|
+
info(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
|
|
1645
|
+
info(" โ IMPORTANT: Your credentials are in .env but not loaded yet โ");
|
|
1646
|
+
info(" โ Run this BEFORE 'repolens publish': โ");
|
|
1647
|
+
info(" โ โ");
|
|
1648
|
+
info(" โ source .env โ");
|
|
1649
|
+
info(" โ โ");
|
|
1650
|
+
info(" โ This loads your credentials into the current shell. โ");
|
|
1651
|
+
info(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
|
|
1652
|
+
info("");
|
|
1653
|
+
info(" 1. source .env โ Load your credentials");
|
|
1654
|
+
info(" 2. npx @chappibunny/repolens publish โ Test locally");
|
|
1655
|
+
} else {
|
|
1656
|
+
info(" 1. npx @chappibunny/repolens publish โ Test locally");
|
|
1657
|
+
}
|
|
1658
|
+
info(` ${hasLocalCredentials ? "3" : "2"}. Add GitHub secrets (see above)`);
|
|
1659
|
+
info(` ${hasLocalCredentials ? "4" : "3"}. Commit and push to trigger workflow`);
|
|
1660
|
+
info(` ${hasLocalCredentials ? "5" : "4"}. Run 'npx @chappibunny/repolens doctor' to validate setup`);
|
|
1218
1661
|
return;
|
|
1219
1662
|
}
|
|
1220
1663
|
|
|
@@ -37,6 +37,8 @@ const PAGE_ORDER = [
|
|
|
37
37
|
"dependency_graph",
|
|
38
38
|
"architecture_drift",
|
|
39
39
|
"arch_diff",
|
|
40
|
+
"security_hotspots",
|
|
41
|
+
"code_health",
|
|
40
42
|
];
|
|
41
43
|
|
|
42
44
|
const PAGE_TITLES = {
|
|
@@ -56,6 +58,8 @@ const PAGE_TITLES = {
|
|
|
56
58
|
dependency_graph: "Dependency Graph",
|
|
57
59
|
architecture_drift: "Architecture Drift",
|
|
58
60
|
arch_diff: "Architecture Diff",
|
|
61
|
+
security_hotspots: "Security Hotspots",
|
|
62
|
+
code_health: "Code Health",
|
|
59
63
|
};
|
|
60
64
|
|
|
61
65
|
const PAGE_DESCRIPTIONS = {
|
|
@@ -75,6 +79,8 @@ const PAGE_DESCRIPTIONS = {
|
|
|
75
79
|
dependency_graph: "Module and package dependency relationships.",
|
|
76
80
|
architecture_drift: "Detected drift between intended and current architecture patterns.",
|
|
77
81
|
arch_diff: "Architecture-level diff across branches or revisions.",
|
|
82
|
+
security_hotspots: "Security anti-patterns detected with CWE classification and severity ratings.",
|
|
83
|
+
code_health: "Cyclomatic complexity analysis with unified health scores per module.",
|
|
78
84
|
};
|
|
79
85
|
|
|
80
86
|
// Audience-based grouping for Home page
|
|
@@ -90,7 +96,7 @@ const AUDIENCE_GROUPS = [
|
|
|
90
96
|
keys: [
|
|
91
97
|
"architecture_overview", "module_catalog", "api_surface",
|
|
92
98
|
"route_map", "system_map", "graphql_schema", "type_graph",
|
|
93
|
-
"dependency_graph", "architecture_drift",
|
|
99
|
+
"dependency_graph", "architecture_drift", "security_hotspots", "code_health",
|
|
94
100
|
],
|
|
95
101
|
},
|
|
96
102
|
{
|
|
@@ -120,6 +126,7 @@ const SIDEBAR_GROUPS = [
|
|
|
120
126
|
"architecture_overview", "module_catalog", "route_map",
|
|
121
127
|
"api_surface", "system_map", "graphql_schema", "type_graph",
|
|
122
128
|
"dependency_graph", "architecture_drift", "arch_diff", "change_impact",
|
|
129
|
+
"security_hotspots", "code_health",
|
|
123
130
|
],
|
|
124
131
|
},
|
|
125
132
|
];
|
|
@@ -142,6 +149,8 @@ const PAGE_AUDIENCE = {
|
|
|
142
149
|
dependency_graph: "Engineers",
|
|
143
150
|
architecture_drift: "Engineers ยท Tech Leads",
|
|
144
151
|
arch_diff: "Engineers ยท Tech Leads",
|
|
152
|
+
security_hotspots: "Engineers ยท Security",
|
|
153
|
+
code_health: "All Audiences",
|
|
145
154
|
};
|
|
146
155
|
|
|
147
156
|
/**
|
|
@@ -22,7 +22,9 @@ function pageFileName(key) {
|
|
|
22
22
|
graphql_schema: "graphql_schema.md",
|
|
23
23
|
type_graph: "type_graph.md",
|
|
24
24
|
dependency_graph: "dependency_graph.md",
|
|
25
|
-
architecture_drift: "architecture_drift.md"
|
|
25
|
+
architecture_drift: "architecture_drift.md",
|
|
26
|
+
security_hotspots: "security_hotspots.md",
|
|
27
|
+
code_health: "code_health.md"
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
return mapping[key] || `${key}.md`;
|