@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
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", "routes", "controllers", "models"],
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
- "src/**/*.{ts,tsx,js,jsx,md}",
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(" Get your integration token from: https://www.notion.so/my-integrations");
713
- info(" Find page ID in the URL: notion.so/workspace/PAGE_ID_HERE\n");
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
- credentials.notion = {
718
- token: (await ask(" NOTION_TOKEN: ")).trim(),
719
- parentPageId: (await ask(" NOTION_PARENT_PAGE_ID: ")).trim(),
720
- };
721
- if (!credentials.notion.token || !credentials.notion.parentPageId) {
722
- warn(" Incomplete credentials. You'll need to set them manually.");
723
- delete credentials.notion;
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
- info(" โœ“ Notion credentials collected");
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(" Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens");
735
- info(" Find space key in the URL: confluence.atlassian.net/wiki/spaces/SPACE_KEY/...\n");
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
- credentials.confluence = {
740
- url: (await ask(" CONFLUENCE_URL (e.g., https://company.atlassian.net/wiki): ")).trim(),
741
- email: (await ask(" CONFLUENCE_EMAIL: ")).trim(),
742
- apiToken: (await ask(" CONFLUENCE_API_TOKEN: ")).trim(),
743
- spaceKey: (await ask(" CONFLUENCE_SPACE_KEY: ")).trim(),
744
- parentPageId: (await ask(" CONFLUENCE_PARENT_PAGE_ID (optional): ")).trim(),
745
- };
746
- const conf = credentials.confluence;
747
- if (!conf.url || !conf.email || !conf.apiToken || !conf.spaceKey) {
748
- warn(" Incomplete credentials. You'll need to set them manually.");
749
- delete credentials.confluence;
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
- info(" โœ“ Confluence credentials collected");
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๐Ÿ“‚ Scan Preset (determines which files to analyze):");
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((p, i) => info(` ${i + 1}. ${p}`));
857
- const presetInput = (await ask(`Preset [3] (default: 3 generic): `)).trim() || "3";
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 : 2];
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: ${presetKey} preset`);
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
- info(" 1. Test locally: npx @chappibunny/repolens publish");
1215
- info(" 2. Add GitHub secrets (see above)");
1216
- info(" 3. Commit and push to trigger workflow");
1217
- info(" 4. Run 'npx @chappibunny/repolens doctor' to validate setup");
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