@chappibunny/repolens 1.10.0 โ 1.11.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 +51 -0
- package/package.json +1 -1
- package/src/init.js +484 -41
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RepoLens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.11.0
|
|
6
|
+
|
|
7
|
+
### ๐ง Smart URL Parsing in Wizard
|
|
8
|
+
|
|
9
|
+
The init wizard now intelligently parses URLs you paste, automatically extracting the right values:
|
|
10
|
+
|
|
11
|
+
**Confluence:**
|
|
12
|
+
- Paste a full page URL โ wizard extracts base URL, space key, AND page ID
|
|
13
|
+
- No more confusion between base URL vs page URL
|
|
14
|
+
- Space key clearly explained (examples: `DOCS`, `ENG`, `~username` for personal)
|
|
15
|
+
- Validates credentials immediately by testing connection to your space
|
|
16
|
+
|
|
17
|
+
**Notion:**
|
|
18
|
+
- Paste a full Notion page URL โ wizard extracts the 32-char page ID automatically
|
|
19
|
+
- Tests connection immediately and confirms your integration has access
|
|
20
|
+
- Clear step-by-step instructions with browser auto-open for integrations page
|
|
21
|
+
|
|
22
|
+
### ๐ Multi-Language Scan Presets
|
|
23
|
+
|
|
24
|
+
The wizard now supports **8 language/framework presets**:
|
|
25
|
+
|
|
26
|
+
| # | Preset | Languages | Best For |
|
|
27
|
+
|---|--------|-----------|----------|
|
|
28
|
+
| 1 | **Universal** | All (JS, TS, Python, Go, Rust, Java, Ruby, PHP, C#, Swift, Kotlin, Scala, Vue, Svelte) | Polyglot projects |
|
|
29
|
+
| 2 | Next.js / React | TypeScript, JavaScript | React frontends |
|
|
30
|
+
| 3 | Express / Node.js | TypeScript, JavaScript | Node.js backends |
|
|
31
|
+
| 4 | **Python** | Python | Django, Flask, FastAPI |
|
|
32
|
+
| 5 | **Go** | Go | Standard Go layout |
|
|
33
|
+
| 6 | **Rust** | Rust | Cargo projects |
|
|
34
|
+
| 7 | **Java/Kotlin/Scala** | JVM languages | Maven/Gradle |
|
|
35
|
+
| 8 | JavaScript/TypeScript | JS/TS only | Legacy JS projects |
|
|
36
|
+
|
|
37
|
+
**Universal is now the default** โ no more "0 modules detected" because of language mismatch!
|
|
38
|
+
|
|
39
|
+
### ๐ Critical .env Reminder
|
|
40
|
+
|
|
41
|
+
After collecting credentials, the wizard now prominently displays:
|
|
42
|
+
```
|
|
43
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
44
|
+
โ IMPORTANT: Your credentials are in .env but not loaded yet โ
|
|
45
|
+
โ Run this BEFORE 'repolens publish': โ
|
|
46
|
+
โ โ
|
|
47
|
+
โ source .env โ
|
|
48
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### ๐งช New Tests
|
|
52
|
+
|
|
53
|
+
- 13 new tests for URL parsing functions (`parseConfluenceUrl`, `parseNotionInput`)
|
|
54
|
+
- Total: 393 tests passing
|
|
55
|
+
|
|
5
56
|
## 1.10.0
|
|
6
57
|
|
|
7
58
|
### โจ Interactive Init is Now Default
|
package/package.json
CHANGED
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
|
|