@chappibunny/repolens 1.9.12 โ†’ 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 CHANGED
@@ -2,6 +2,86 @@
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
+
56
+ ## 1.10.0
57
+
58
+ ### โœจ Interactive Init is Now Default
59
+
60
+ - **`repolens init` is now fully interactive**: No more `--interactive` flag needed โ€” the wizard runs by default
61
+ - **Use `--quick` to skip the wizard**: For CI or minimal scaffolding, use `repolens init --quick`
62
+ - **Comprehensive wizard**: Configures ALL publishers (Notion, Confluence, GitHub Wiki), not just Notion
63
+ - Collects credentials for each selected publisher
64
+ - Guides AI provider selection with environment validation
65
+ - Discord webhook configuration
66
+ - Branch filtering for Notion/Confluence publishing
67
+ - Shows clear summary of GitHub Actions secrets needed
68
+ - **Credentials written to `.env`**: All collected credentials are saved locally (gitignored)
69
+ - **Better validation**: Shows specific status for each credential (โœ“ set vs โ—‹ needed)
70
+
71
+ ### ๐Ÿค– AI Setup from CLI
72
+
73
+ - **Browser auto-open**: Wizard offers to open API key signup pages (OpenAI, Anthropic, Google) in your browser
74
+ - **API key validation**: Tests your AI key immediately after you paste it โ€” confirms it works before saving
75
+ - **GitHub Token testing**: Validates existing `GITHUB_TOKEN` works with GitHub Models
76
+ - **Clear feedback**: Shows โœ“ valid / โš  invalid with specific error messages
77
+ - **Writes all config**: `.env` now includes `REPOLENS_AI_ENABLED=true` and `REPOLENS_AI_PROVIDER=...`
78
+
79
+ ### ๐Ÿ”ง AI Diagnostics
80
+
81
+ - **Better error messages**: AI failures now show the actual reason (e.g., "Missing API key (expected REPOLENS_AI_API_KEY)")
82
+ - **Config logging**: First AI call logs the provider, model, and key prefix being used
83
+ - **Workflow template updated**: Default workflow now shows both GitHub Models (free) AND OpenAI options with clear comments
84
+
5
85
  ## 1.9.12
6
86
 
7
87
  ### ๐Ÿ› Bug Fixes
@@ -149,7 +229,7 @@ Users without AI API keys now get production-quality documentation instead of sp
149
229
  - **Zero-config AI in CI**: When `ai.provider: github` is set in `.repolens.yml`, RepoLens uses the default `GITHUB_TOKEN` injected by GitHub Actions. No secrets to create or manage.
150
230
  - **Config-driven AI settings**: `ai.enabled`, `ai.provider`, `ai.model`, `ai.temperature`, and `ai.base_url` in `.repolens.yml` are now fully respected at runtime (env vars still take precedence). Previously these config values were ignored.
151
231
  - **Init wizard fixes**: Provider selection now uses correct runtime values (`github`, `openai_compatible`, `anthropic`, `google`) instead of mismatched labels. The wizard now emits `ai.provider` to the generated YAML. Added `github_wiki` to publisher choices.
152
- - **Demo AI upsell**: `repolens demo` now shows a hint about GitHub Models (free) when AI is not enabled, guiding users to `repolens init --interactive`.
232
+ - **Demo AI upsell**: `repolens demo` now shows a hint about GitHub Models (free) when AI is not enabled, guiding users to `repolens init`.
153
233
  - **Uninstall command**: `repolens uninstall` removes all RepoLens-generated files (`.repolens/`, `.repolens.yml`, workflow, `.env.example`, `README.repolens.md`) with confirmation prompt and `--force` flag.
154
234
  - **Doctor validation**: `repolens doctor` now checks for `GITHUB_TOKEN` when provider is `github`, and `REPOLENS_AI_API_KEY` for other providers.
155
235
 
@@ -449,7 +529,7 @@ RepoLens v1.0.0 marks the first stable release with a frozen public API. All CLI
449
529
  ## 0.7.0
450
530
 
451
531
  ### โœจ New Features
452
- - **Interactive Init Wizard**: `repolens init --interactive` โ€” step-by-step configuration wizard with scan presets (Next.js, Express, generic), publisher selection, AI provider setup, and branch filtering
532
+ - **Interactive Init Wizard**: `repolens init` โ€” step-by-step configuration wizard with scan presets (Next.js, Express, generic), publisher selection, AI provider setup, and branch filtering (now default behavior; use `--quick` to skip)
453
533
  - **Watch Mode**: `repolens watch` โ€” watches source directories for changes and regenerates Markdown docs with 500ms debounce (no API calls)
454
534
  - **Enhanced Error Messages**: Centralized error catalog with actionable guidance โ€” every error now shows what went wrong, why, and how to fix it
455
535
  - **Performance Monitoring**: Scan, render, and publish timing summary printed after every `publish` run
package/README.md CHANGED
@@ -165,8 +165,8 @@ Step-by-step setup for publishers, AI features, Notion, Confluence, GitHub Wiki,
165
165
 
166
166
  | Command | Description |
167
167
  |---|---|
168
- | `npx @chappibunny/repolens init` | Scaffold config + GitHub Actions workflow |
169
- | `npx @chappibunny/repolens init --interactive` | Step-by-step configuration wizard |
168
+ | `npx @chappibunny/repolens init` | **Interactive wizard** โ€” configure publishers, AI, credentials |
169
+ | `npx @chappibunny/repolens init --quick` | Minimal setup, skip wizard |
170
170
  | `npx @chappibunny/repolens publish` | Scan, generate, and publish documentation |
171
171
  | `npx @chappibunny/repolens demo` | Quick local preview โ€” no API keys needed |
172
172
  | `npx @chappibunny/repolens doctor` | Validate your setup |
package/docs/ROADMAP.md CHANGED
@@ -56,7 +56,7 @@ Everything below is live, tested, and available on npm.
56
56
  - โœ… User feedback via `repolens feedback` command
57
57
 
58
58
  ### Polish & Reliability (v0.7.0)
59
- - โœ… Interactive configuration wizard (`repolens init --interactive`)
59
+ - โœ… Interactive configuration wizard (now default for `repolens init`)
60
60
  - โœ… Watch mode for local development (`repolens watch`)
61
61
  - โœ… Enhanced error messages with actionable guidance (centralized error catalog)
62
62
  - โœ… Performance monitoring (scan/render/publish timing summary)
package/docs/STABILITY.md CHANGED
@@ -16,8 +16,8 @@ RepoLens follows semantic versioning (semver):
16
16
 
17
17
  | Command | Status | Description |
18
18
  |---------|--------|-------------|
19
- | `init` | Stable | Scaffold configuration and GitHub Actions workflow |
20
- | `init --interactive` | Stable | Step-by-step configuration wizard |
19
+ | `init` | Stable | **Interactive wizard** โ€” configure publishers, AI, credentials |
20
+ | `init --quick` | Stable | Minimal scaffolding, skip wizard |
21
21
  | `doctor` | Stable | Validate repository setup |
22
22
  | `publish` | Stable | Scan, generate, and publish documentation |
23
23
  | `demo` | Stable | Generate local docs without API keys (quick preview) |
@@ -33,7 +33,7 @@ RepoLens follows semantic versioning (semver):
33
33
  |--------|-------|--------|-------------|
34
34
  | `--config <path>` | โ€” | Stable | Path to `.repolens.yml` |
35
35
  | `--target <path>` | โ€” | Stable | Target repository path (init, doctor, migrate) |
36
- | `--interactive` | โ€” | Stable | Interactive mode for init |
36
+ | `--quick` | โ€” | Stable | Skip interactive wizard for init |
37
37
  | `--dry-run` | โ€” | Stable | Preview changes without applying (migrate) |
38
38
  | `--force` | โ€” | Stable | Skip confirmation prompts (migrate) |
39
39
  | `--verbose` | โ€” | Stable | Enable verbose logging |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.9.12",
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",
@@ -63,7 +63,7 @@ async function generateWithStructuredFallback(key, promptText, maxTokens, fallba
63
63
  if (md) return sanitizeAIOutput(md);
64
64
  }
65
65
  // If structured mode failed, fall through to plain-text
66
- warn(`Structured AI failed for ${key}, trying plain-text mode...`);
66
+ warn(`Structured AI failed for ${key}: ${result.error || "invalid/empty response"}`);
67
67
  }
68
68
 
69
69
  // Plain-text AI fallback
@@ -76,7 +76,7 @@ async function generateWithStructuredFallback(key, promptText, maxTokens, fallba
76
76
  });
77
77
 
78
78
  if (!result.success) {
79
- warn("AI generation failed, using fallback");
79
+ warn(`AI generation failed: ${result.error || "unknown error"}`);
80
80
  return fallbackFn();
81
81
  }
82
82
 
@@ -33,6 +33,9 @@ const AI_PRESETS = {
33
33
  },
34
34
  };
35
35
 
36
+ // Module-level flag to log config only once
37
+ let hasLoggedConfig = false;
38
+
36
39
  export async function generateText({ system, user, temperature, maxTokens, config, jsonMode, jsonSchema }) {
37
40
  // Check if AI is enabled (env var takes precedence, then config)
38
41
  const aiConfig = config?.ai || {};
@@ -69,14 +72,22 @@ export async function generateText({ system, user, temperature, maxTokens, confi
69
72
 
70
73
  // Validate configuration
71
74
  if (!apiKey) {
72
- warn("REPOLENS_AI_API_KEY not set. AI features disabled.");
75
+ const keySource = provider === "github" ? "GITHUB_TOKEN or REPOLENS_AI_API_KEY" : "REPOLENS_AI_API_KEY";
76
+ warn(`AI: No API key found. Expected ${keySource} in environment.`);
73
77
  return {
74
78
  success: false,
75
- error: "Missing API key",
79
+ error: `Missing API key (expected ${keySource})`,
76
80
  fallback: true
77
81
  };
78
82
  }
79
83
 
84
+ // Log configuration once per run
85
+ if (!hasLoggedConfig) {
86
+ const keyPreview = apiKey.substring(0, 8) + "...";
87
+ info(`AI Config: provider=${provider}, model=${model}, key=${keyPreview}`);
88
+ hasLoggedConfig = true;
89
+ }
90
+
80
91
  if (!baseUrl && provider === "openai_compatible") {
81
92
  warn("REPOLENS_AI_BASE_URL not set. Using OpenAI default.");
82
93
  }
package/src/cli.js CHANGED
@@ -134,10 +134,8 @@ function showPostGenerationAINotice() {
134
134
  info(`${fmt.cyan("โ”‚")} ${fmt.yellow("โ€ข")} Developer Onboarding โ€” getting started guide for new hires ${fmt.cyan("โ”‚")}`);
135
135
  info(`${fmt.cyan("โ”‚")} ${fmt.cyan("โ”‚")}`);
136
136
  info(`${fmt.cyan("โ”‚")} ${fmt.boldGreen("๐Ÿ†“ Enable for FREE with GitHub Models:")} ${fmt.cyan("โ”‚")}`);
137
- info(`${fmt.cyan("โ”‚")} ${fmt.green("export GITHUB_TOKEN=<your-token>")} ${fmt.cyan("โ”‚")}`);
138
- info(`${fmt.cyan("โ”‚")} ${fmt.green("repolens demo")} ${fmt.cyan("โ”‚")}`);
139
- info(`${fmt.cyan("โ”‚")} ${fmt.cyan("โ”‚")}`);
140
- info(`${fmt.cyan("โ”‚")} Or run: ${fmt.brightCyan("repolens init --interactive")} โ†’ select GitHub Models ${fmt.cyan("โ”‚")}`);
137
+ info(`${fmt.cyan("โ”‚")} Run: ${fmt.brightCyan("repolens init")} โ†’ the wizard will guide you through setup ${fmt.cyan("โ”‚")}`);
138
+ info(`${fmt.cyan("โ”‚")} Or quick preview: ${fmt.green("export GITHUB_TOKEN=<your-token> && repolens demo")} ${fmt.cyan("โ”‚")}`);
141
139
  info(`${fmt.cyan("โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜")}`);
142
140
  }
143
141
 
@@ -242,7 +240,7 @@ Commands:
242
240
  Options:
243
241
  --config Path to .repolens.yml (auto-discovered if not provided)
244
242
  --target Target repository path for init/doctor/migrate
245
- --interactive Run init with step-by-step configuration wizard
243
+ --quick Run init with minimal prompts (skip wizard)
246
244
  --dry-run Preview migration changes without applying them
247
245
  --force Skip interactive confirmation for migration
248
246
  --verbose Enable verbose logging
@@ -250,8 +248,8 @@ Options:
250
248
  --help Show this help message
251
249
 
252
250
  Examples:
253
- repolens init # Quick setup with auto-detection
254
- repolens init --interactive # Step-by-step wizard
251
+ repolens init # Full interactive wizard (recommended)
252
+ repolens init --quick # Minimal setup, skip wizard
255
253
  repolens init --target /tmp/my-repo
256
254
  repolens doctor --target /tmp/my-repo
257
255
  repolens migrate # Migrate workflows in current directory
@@ -295,7 +293,9 @@ async function main() {
295
293
  if (command === "init") {
296
294
  await printBanner();
297
295
  const targetDir = getArg("--target") || process.cwd();
298
- const interactive = process.argv.includes("--interactive");
296
+ // Interactive is now the default; use --quick for minimal scaffolding
297
+ const quick = process.argv.includes("--quick") || process.argv.includes("--non-interactive");
298
+ const interactive = !quick;
299
299
  info(`Initializing RepoLens in: ${targetDir}`);
300
300
 
301
301
  const timer = startTimer("init");
@@ -598,7 +598,7 @@ async function main() {
598
598
 
599
599
  if (aiResult.enabled && aiResult.wasPrompted) {
600
600
  info(`\n๐Ÿค– AI-enhanced docs were generated using ${fmt.boldGreen("GitHub Models (FREE)")}`);
601
- info(" To keep AI enabled permanently, run: repolens init --interactive");
601
+ info(" To keep AI enabled permanently, run: repolens init");
602
602
  } else if (aiResult.noToken) {
603
603
  // No GITHUB_TOKEN - show instructions
604
604
  showPostGenerationAINotice();
package/src/init.js CHANGED
@@ -1,17 +1,245 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
+ import { exec } from "node:child_process";
4
5
  import { info, warn } from "./utils/logger.js";
5
6
 
6
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
+ }
7
218
  const AI_PROVIDERS = [
8
- { value: "github", label: "GitHub Models (free in GitHub Actions)" },
9
- { value: "openai_compatible", label: "OpenAI / Compatible (GPT-5, GPT-4o, etc.)" },
10
- { value: "anthropic", label: "Anthropic (Claude)" },
11
- { value: "google", label: "Google (Gemini)" },
219
+ { value: "github", label: "GitHub Models (free in GitHub Actions)", signupUrl: null },
220
+ { value: "openai_compatible", label: "OpenAI / Compatible (GPT-5, GPT-4o, etc.)", signupUrl: "https://platform.openai.com/api-keys" },
221
+ { value: "anthropic", label: "Anthropic (Claude)", signupUrl: "https://console.anthropic.com/settings/keys" },
222
+ { value: "google", label: "Google (Gemini)", signupUrl: "https://aistudio.google.com/app/apikey" },
12
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
+
13
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
14
240
  nextjs: {
241
+ label: "Next.js / React",
242
+ description: "React, Next.js, and frontend TypeScript projects",
15
243
  include: [
16
244
  "src/**/*.{ts,tsx,js,jsx}",
17
245
  "app/**/*.{ts,tsx,js,jsx}",
@@ -22,20 +250,65 @@ const SCAN_PRESETS = {
22
250
  roots: ["src", "app", "pages", "lib", "components"],
23
251
  },
24
252
  express: {
253
+ label: "Express / Node.js",
254
+ description: "Node.js backend APIs and Express servers",
25
255
  include: [
26
- "src/**/*.{ts,js}",
256
+ "src/**/*.{ts,js,mjs,cjs}",
27
257
  "routes/**/*.{ts,js}",
28
258
  "controllers/**/*.{ts,js}",
29
259
  "models/**/*.{ts,js}",
30
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",
31
292
  ],
32
- roots: ["src", "routes", "controllers", "models"],
293
+ roots: ["src", "lib", "bin", "examples"],
33
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
34
307
  generic: {
308
+ label: "JavaScript/TypeScript only",
309
+ description: "Traditional JS/TS projects",
35
310
  include: [
36
- "src/**/*.{ts,tsx,js,jsx,md}",
37
- "app/**/*.{ts,tsx,js,jsx,md}",
38
- "lib/**/*.{ts,tsx,js,jsx,md}",
311
+ "**/*.{ts,tsx,js,jsx,mjs,cjs}",
39
312
  ],
40
313
  roots: ["src", "app", "lib"],
41
314
  },
@@ -89,10 +362,14 @@ jobs:
89
362
  CONFLUENCE_API_TOKEN: \${{ secrets.CONFLUENCE_API_TOKEN }}
90
363
  CONFLUENCE_SPACE_KEY: \${{ secrets.CONFLUENCE_SPACE_KEY }}
91
364
  CONFLUENCE_PARENT_PAGE_ID: \${{ secrets.CONFLUENCE_PARENT_PAGE_ID }}
92
- # AI-enhanced docs via GitHub Models (free)
365
+ # AI-enhanced docs: Choose ONE option below
93
366
  REPOLENS_AI_ENABLED: true
367
+ # Option A: GitHub Models (free โ€” uses GITHUB_TOKEN)
94
368
  REPOLENS_AI_PROVIDER: github
95
369
  GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
370
+ # Option B: OpenAI / Anthropic / Google (comment out Option A, uncomment below)
371
+ # REPOLENS_AI_API_KEY: \${{ secrets.REPOLENS_AI_API_KEY }}
372
+ # REPOLENS_AI_PROVIDER: openai_compatible # or: anthropic, google
96
373
  run: npx @chappibunny/repolens@latest publish
97
374
  `;
98
375
 
@@ -283,6 +560,117 @@ async function dirExists(dirPath) {
283
560
  }
284
561
  }
285
562
 
563
+ /**
564
+ * Open a URL in the default browser (cross-platform).
565
+ */
566
+ function openUrl(url) {
567
+ const platform = process.platform;
568
+ let cmd;
569
+ if (platform === "darwin") {
570
+ cmd = `open "${url}"`;
571
+ } else if (platform === "win32") {
572
+ cmd = `start "" "${url}"`;
573
+ } else {
574
+ cmd = `xdg-open "${url}"`;
575
+ }
576
+ return new Promise((resolve) => {
577
+ exec(cmd, (err) => {
578
+ if (err) {
579
+ warn(`Could not open browser: ${err.message}`);
580
+ resolve(false);
581
+ } else {
582
+ resolve(true);
583
+ }
584
+ });
585
+ });
586
+ }
587
+
588
+ /**
589
+ * Test an AI API key by making a minimal request.
590
+ * Returns { success: true } or { success: false, error: string }.
591
+ */
592
+ async function testAIKey(provider, apiKey) {
593
+ try {
594
+ const timeout = 15000;
595
+ const controller = new AbortController();
596
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
597
+
598
+ let url, headers, body;
599
+
600
+ if (provider === "github") {
601
+ url = "https://models.inference.ai.azure.com/chat/completions";
602
+ headers = {
603
+ "Content-Type": "application/json",
604
+ "Authorization": `Bearer ${apiKey}`,
605
+ };
606
+ body = JSON.stringify({
607
+ model: "gpt-4o-mini",
608
+ messages: [{ role: "user", content: "Say OK" }],
609
+ max_tokens: 5,
610
+ });
611
+ } else if (provider === "openai_compatible") {
612
+ url = "https://api.openai.com/v1/chat/completions";
613
+ headers = {
614
+ "Content-Type": "application/json",
615
+ "Authorization": `Bearer ${apiKey}`,
616
+ };
617
+ body = JSON.stringify({
618
+ model: "gpt-4o-mini",
619
+ messages: [{ role: "user", content: "Say OK" }],
620
+ max_tokens: 5,
621
+ });
622
+ } else if (provider === "anthropic") {
623
+ url = "https://api.anthropic.com/v1/messages";
624
+ headers = {
625
+ "Content-Type": "application/json",
626
+ "x-api-key": apiKey,
627
+ "anthropic-version": "2023-06-01",
628
+ };
629
+ body = JSON.stringify({
630
+ model: "claude-sonnet-4-20250514",
631
+ max_tokens: 5,
632
+ messages: [{ role: "user", content: "Say OK" }],
633
+ });
634
+ } else if (provider === "google") {
635
+ url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
636
+ headers = { "Content-Type": "application/json" };
637
+ body = JSON.stringify({
638
+ contents: [{ parts: [{ text: "Say OK" }] }],
639
+ generationConfig: { maxOutputTokens: 5 },
640
+ });
641
+ } else {
642
+ return { success: false, error: "Unknown provider" };
643
+ }
644
+
645
+ const response = await fetch(url, {
646
+ method: "POST",
647
+ headers,
648
+ body,
649
+ signal: controller.signal,
650
+ });
651
+
652
+ clearTimeout(timeoutId);
653
+
654
+ if (response.ok) {
655
+ return { success: true };
656
+ }
657
+
658
+ const errorBody = await response.text().catch(() => "");
659
+ if (response.status === 401 || response.status === 403) {
660
+ return { success: false, error: "Invalid API key" };
661
+ }
662
+ if (response.status === 429) {
663
+ return { success: false, error: "Rate limited โ€” but key is valid" };
664
+ }
665
+ return { success: false, error: `API error ${response.status}: ${errorBody.slice(0, 100)}` };
666
+ } catch (err) {
667
+ if (err.name === "AbortError") {
668
+ return { success: false, error: "Request timed out" };
669
+ }
670
+ return { success: false, error: err.message };
671
+ }
672
+ }
673
+
286
674
  async function detectRepoStructure(repoRoot) {
287
675
  const detectedRoots = [];
288
676
 
@@ -482,7 +870,8 @@ async function promptNotionCredentials() {
482
870
  });
483
871
 
484
872
  try {
485
- info("\n๐Ÿ“ Notion Setup (optional)");
873
+ info("\n๐Ÿ“ Quick Setup โ€” Notion Publishing");
874
+ info("(Use 'repolens init' without --quick for full wizard)\n");
486
875
  const useNotion = await rl.question("Would you like to publish to Notion? (Y/n): ");
487
876
 
488
877
  if (useNotion.toLowerCase() === 'n') {
@@ -564,15 +953,20 @@ async function runInteractiveWizard(repoRoot) {
564
953
 
565
954
  try {
566
955
  info("\n๐Ÿง™ Interactive Configuration Wizard\n");
956
+ info("This wizard will help you configure RepoLens for your project.");
957
+ info("Press Enter to accept defaults shown in parentheses.\n");
567
958
 
568
959
  // 1. Project name
569
960
  const defaultName = path.basename(repoRoot) || "my-project";
570
- const projectName = (await ask(`Project name (${defaultName}): `)).trim() || defaultName;
961
+ const projectName = (await ask(`๐Ÿ“ฆ Project name (${defaultName}): `)).trim() || defaultName;
571
962
 
572
963
  // 2. Publishers
573
- info("\nSelect publishers (comma-separated numbers):");
574
- PUBLISHER_CHOICES.forEach((p, i) => info(` ${i + 1}. ${p}`));
575
- const pubInput = (await ask(`Publishers [1,2,3] (default: 1): `)).trim() || "1";
964
+ info("\n๐Ÿ“ค Select publishers (comma-separated numbers):");
965
+ PUBLISHER_CHOICES.forEach((p, i) => {
966
+ const desc = PUBLISHER_DESCRIPTIONS[p] || "";
967
+ info(` ${i + 1}. ${p}${desc ? ` โ€” ${desc}` : ""}`);
968
+ });
969
+ const pubInput = (await ask(`Publishers [1] (default: 1 markdown): `)).trim() || "1";
576
970
  const publishers = pubInput
577
971
  .split(",")
578
972
  .map((n) => parseInt(n.trim(), 10))
@@ -580,45 +974,388 @@ async function runInteractiveWizard(repoRoot) {
580
974
  .map((n) => PUBLISHER_CHOICES[n - 1]);
581
975
  if (publishers.length === 0) publishers.push("markdown");
582
976
 
583
- // 3. AI
584
- const enableAi = (await ask("\nEnable AI-enhanced documentation? (y/N): ")).trim().toLowerCase() === "y";
977
+ // 3. Collect credentials for each publisher
978
+ const credentials = {};
979
+ const githubSecretsNeeded = [];
980
+
981
+ // Notion setup
982
+ if (publishers.includes("notion")) {
983
+ info("\n๐Ÿ“ Notion Setup");
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(" โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜");
993
+
994
+ const setupNow = (await ask("\n Configure Notion credentials now? (Y/n): ")).trim().toLowerCase();
995
+ if (setupNow !== "n") {
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.");
1007
+ } else {
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
+ }
1051
+ }
1052
+ }
1053
+ githubSecretsNeeded.push("NOTION_TOKEN", "NOTION_PARENT_PAGE_ID");
1054
+ }
1055
+
1056
+ // Confluence setup
1057
+ if (publishers.includes("confluence")) {
1058
+ info("\n๐Ÿ“ Confluence Setup");
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(" โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜");
1066
+
1067
+ const setupNow = (await ask("\n Configure Confluence credentials now? (Y/n): ")).trim().toLowerCase();
1068
+ if (setupNow !== "n") {
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.");
1138
+ } else {
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
+ }
1160
+ }
1161
+ }
1162
+ githubSecretsNeeded.push("CONFLUENCE_URL", "CONFLUENCE_EMAIL", "CONFLUENCE_API_TOKEN", "CONFLUENCE_SPACE_KEY", "CONFLUENCE_PARENT_PAGE_ID");
1163
+ }
1164
+
1165
+ // GitHub Wiki setup
1166
+ if (publishers.includes("github_wiki")) {
1167
+ info("\n๐Ÿ“ GitHub Wiki Setup");
1168
+ info(" Requires GITHUB_TOKEN with repo scope.");
1169
+ if (process.env.GITHUB_TOKEN) {
1170
+ info(" โœ“ GITHUB_TOKEN is set in your environment");
1171
+ } else {
1172
+ warn(" GITHUB_TOKEN not found in environment.");
1173
+ info(" For local use: export GITHUB_TOKEN=your_token");
1174
+ info(" For GitHub Actions: Uses ${{ secrets.GITHUB_TOKEN }} automatically");
1175
+ }
1176
+ githubSecretsNeeded.push("GITHUB_TOKEN");
1177
+ }
1178
+
1179
+ // 4. AI Configuration
1180
+ info("\n๐Ÿค– AI-Enhanced Documentation");
1181
+ info(" Adds natural language explanations for non-technical stakeholders.");
1182
+ const enableAi = (await ask(" Enable AI features? (Y/n): ")).trim().toLowerCase() !== "n";
1183
+
585
1184
  let aiProvider = null;
1185
+ let aiApiKey = null;
1186
+
586
1187
  if (enableAi) {
587
- info("Select AI provider:");
588
- AI_PROVIDERS.forEach((p, i) => info(` ${i + 1}. ${p.label}`));
589
- const aiInput = (await ask(`Provider [1] (default: 1 GitHub Models โ€” free): `)).trim() || "1";
1188
+ info("\n Select AI provider:");
1189
+ AI_PROVIDERS.forEach((p, i) => info(` ${i + 1}. ${p.label}`));
1190
+ const aiInput = (await ask(` Provider [1] (default: 1 GitHub Models โ€” free): `)).trim() || "1";
590
1191
  const idx = parseInt(aiInput, 10);
591
1192
  const chosen = AI_PROVIDERS[(idx >= 1 && idx <= AI_PROVIDERS.length) ? idx - 1 : 0];
592
1193
  aiProvider = chosen.value;
1194
+
593
1195
  if (aiProvider === "github") {
594
- info("\n โœจ Great choice! GitHub Models uses your existing GITHUB_TOKEN โ€” no extra API key needed.");
595
- info(" Works automatically in GitHub Actions with the free tier.");
1196
+ info("\n โœจ GitHub Models is free and uses your GITHUB_TOKEN.");
1197
+
1198
+ // Check for existing token
1199
+ const existingToken = process.env.GITHUB_TOKEN;
1200
+ if (existingToken) {
1201
+ info(" Testing your GITHUB_TOKEN...");
1202
+ const testResult = await testAIKey("github", existingToken);
1203
+ if (testResult.success) {
1204
+ info(" โœ“ GITHUB_TOKEN is valid โ€” AI will work locally and in Actions");
1205
+ credentials.ai = { provider: "github", useGitHubToken: true };
1206
+ } else {
1207
+ warn(` โš  GITHUB_TOKEN test failed: ${testResult.error}`);
1208
+ info(" AI will still work in GitHub Actions with ${{ secrets.GITHUB_TOKEN }}");
1209
+ }
1210
+ } else {
1211
+ info(" No GITHUB_TOKEN found in environment.");
1212
+ info(" In GitHub Actions: Works automatically with ${{ secrets.GITHUB_TOKEN }}");
1213
+ info(" For local testing: export GITHUB_TOKEN=your_personal_access_token");
1214
+ }
1215
+ githubSecretsNeeded.push("GITHUB_TOKEN");
1216
+ credentials.ai = { ...(credentials.ai || {}), provider: "github", enabled: true };
1217
+ } else {
1218
+ // Non-GitHub provider: help them get an API key
1219
+ info(`\n ${chosen.label} requires an API key.`);
1220
+
1221
+ // Offer to open signup URL
1222
+ if (chosen.signupUrl) {
1223
+ const openBrowser = (await ask(` Open ${chosen.value} signup page in browser? (Y/n): `)).trim().toLowerCase();
1224
+ if (openBrowser !== "n") {
1225
+ info(` Opening ${chosen.signupUrl}...`);
1226
+ await openUrl(chosen.signupUrl);
1227
+ info(" Create an API key, then paste it below.\n");
1228
+ }
1229
+ }
1230
+
1231
+ const keyInput = (await ask(` Paste your API key (or press Enter to skip): `)).trim();
1232
+ if (keyInput) {
1233
+ info(" Testing your API key...");
1234
+ const testResult = await testAIKey(aiProvider, keyInput);
1235
+
1236
+ if (testResult.success) {
1237
+ info(" โœ“ API key is valid!");
1238
+ aiApiKey = keyInput;
1239
+ credentials.ai = { apiKey: keyInput, provider: aiProvider, enabled: true };
1240
+ } else if (testResult.error === "Rate limited โ€” but key is valid") {
1241
+ info(" โœ“ API key is valid (rate limited, but will work)");
1242
+ aiApiKey = keyInput;
1243
+ credentials.ai = { apiKey: keyInput, provider: aiProvider, enabled: true };
1244
+ } else {
1245
+ warn(` โš  API key test failed: ${testResult.error}`);
1246
+ const useAnyway = (await ask(` Save this key anyway? (y/N): `)).trim().toLowerCase();
1247
+ if (useAnyway === "y") {
1248
+ aiApiKey = keyInput;
1249
+ credentials.ai = { apiKey: keyInput, provider: aiProvider, enabled: true };
1250
+ } else {
1251
+ warn(" Skipping AI configuration. You can set REPOLENS_AI_API_KEY later.");
1252
+ }
1253
+ }
1254
+ } else {
1255
+ warn(" No API key provided. Set REPOLENS_AI_API_KEY in .env or GitHub secrets.");
1256
+ }
1257
+ githubSecretsNeeded.push("REPOLENS_AI_API_KEY");
596
1258
  }
597
1259
  }
598
1260
 
599
- // 4. Scan preset
600
- info("\nScan preset:");
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
+
601
1265
  const presetKeys = Object.keys(SCAN_PRESETS);
602
- presetKeys.forEach((p, i) => info(` ${i + 1}. ${p}`));
603
- 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);
604
1279
  const presetIdx = parseInt(presetInput, 10);
605
- const presetKey = presetKeys[(presetIdx >= 1 && presetIdx <= presetKeys.length) ? presetIdx - 1 : 2];
1280
+ const presetKey = presetKeys[(presetIdx >= 1 && presetIdx <= presetKeys.length) ? presetIdx - 1 : defaultPresetIdx - 1];
606
1281
  const preset = SCAN_PRESETS[presetKey];
1282
+
1283
+ info(` โœ“ Selected: ${preset.label}`);
607
1284
 
608
- // 5. Branch filtering
609
- const branchInput = (await ask("\nBranches allowed to publish to Notion/Confluence (comma-separated, default: main): ")).trim() || "main";
1285
+ // 6. Branch filtering
1286
+ info("\n๐ŸŒฟ Branch Filtering");
1287
+ info(" Limits which branches can publish to Notion/Confluence.");
1288
+ const branchInput = (await ask(" Allowed branches (comma-separated, default: main): ")).trim() || "main";
610
1289
  const branches = branchInput.split(",").map((b) => b.trim()).filter(Boolean);
611
1290
 
612
- // 6. Discord
613
- const enableDiscord = (await ask("\nEnable Discord notifications? (y/N): ")).trim().toLowerCase() === "y";
1291
+ // 7. Discord notifications
1292
+ info("\n๐Ÿ”” Discord Notifications");
1293
+ const enableDiscord = (await ask(" Enable Discord notifications? (y/N): ")).trim().toLowerCase() === "y";
1294
+
1295
+ let discordWebhook = null;
1296
+ if (enableDiscord) {
1297
+ info(" Get webhook URL from: Server Settings > Integrations > Webhooks");
1298
+ discordWebhook = (await ask(" DISCORD_WEBHOOK_URL (leave blank to skip): ")).trim() || null;
1299
+ if (discordWebhook) {
1300
+ credentials.discord = { webhookUrl: discordWebhook };
1301
+ }
1302
+ githubSecretsNeeded.push("DISCORD_WEBHOOK_URL");
1303
+ }
1304
+
1305
+ // Summary
1306
+ info("\n" + "โ•".repeat(60));
1307
+ info("๐Ÿ“‹ Configuration Summary");
1308
+ info("โ•".repeat(60));
1309
+ info(` Project: ${projectName}`);
1310
+ info(` Publishers: ${publishers.join(", ")}`);
1311
+ info(` AI: ${enableAi ? `Enabled (${aiProvider})` : "Disabled"}`);
1312
+ info(` Scan: ${preset.label}`);
1313
+ info(` Branches: ${branches.join(", ")}`);
1314
+ info(` Discord: ${enableDiscord ? "Enabled" : "Disabled"}`);
1315
+
1316
+ // GitHub secrets summary
1317
+ const uniqueSecrets = [...new Set(githubSecretsNeeded)];
1318
+ if (uniqueSecrets.length > 0) {
1319
+ info("\n๐Ÿ“Œ GitHub Actions Secrets Required:");
1320
+ info(" Add these at: https://github.com/YOUR_ORG/YOUR_REPO/settings/secrets/actions");
1321
+ for (const secret of uniqueSecrets) {
1322
+ const status = credentials[secret.toLowerCase().split("_")[0]] ? "โœ“" : "โ—‹";
1323
+ info(` ${status} ${secret}`);
1324
+ }
1325
+ }
614
1326
 
615
- info("\nโœ“ Wizard complete. Generating config...\n");
616
- return { projectName, publishers, enableAi, aiProvider, preset, branches, enableDiscord };
1327
+ info("\n" + "โ•".repeat(60));
1328
+
1329
+ const proceed = (await ask("\nProceed with this configuration? (Y/n): ")).trim().toLowerCase();
1330
+ if (proceed === "n") {
1331
+ info("Configuration cancelled.");
1332
+ return null;
1333
+ }
1334
+
1335
+ info("\nโœ“ Wizard complete. Generating files...\n");
1336
+ return {
1337
+ projectName,
1338
+ publishers,
1339
+ enableAi,
1340
+ aiProvider,
1341
+ preset,
1342
+ branches,
1343
+ enableDiscord,
1344
+ credentials,
1345
+ githubSecretsNeeded: uniqueSecrets
1346
+ };
617
1347
  } finally {
618
1348
  rl.close();
619
1349
  }
620
1350
  }
621
1351
 
1352
+ const PUBLISHER_DESCRIPTIONS = {
1353
+ markdown: "Local files in .repolens/",
1354
+ notion: "Notion workspace pages",
1355
+ confluence: "Atlassian Confluence pages",
1356
+ github_wiki: "Repository wiki pages",
1357
+ };
1358
+
622
1359
  /**
623
1360
  * Build a .repolens.yml from wizard answers.
624
1361
  */
@@ -719,16 +1456,73 @@ function buildWizardConfig(answers) {
719
1456
  return lines.join("\n");
720
1457
  }
721
1458
 
1459
+ /**
1460
+ * Build .env content from wizard credentials.
1461
+ */
1462
+ function buildEnvFromCredentials(credentials) {
1463
+ const lines = [];
1464
+
1465
+ if (credentials.notion) {
1466
+ lines.push("# Notion Publishing");
1467
+ lines.push(`NOTION_TOKEN=${credentials.notion.token}`);
1468
+ lines.push(`NOTION_PARENT_PAGE_ID=${credentials.notion.parentPageId}`);
1469
+ lines.push(`NOTION_VERSION=2022-06-28`);
1470
+ lines.push("");
1471
+ }
1472
+
1473
+ if (credentials.confluence) {
1474
+ lines.push("# Confluence Publishing");
1475
+ lines.push(`CONFLUENCE_URL=${credentials.confluence.url}`);
1476
+ lines.push(`CONFLUENCE_EMAIL=${credentials.confluence.email}`);
1477
+ lines.push(`CONFLUENCE_API_TOKEN=${credentials.confluence.apiToken}`);
1478
+ lines.push(`CONFLUENCE_SPACE_KEY=${credentials.confluence.spaceKey}`);
1479
+ if (credentials.confluence.parentPageId) {
1480
+ lines.push(`CONFLUENCE_PARENT_PAGE_ID=${credentials.confluence.parentPageId}`);
1481
+ }
1482
+ lines.push("");
1483
+ }
1484
+
1485
+ if (credentials.ai?.enabled) {
1486
+ lines.push("# AI Configuration");
1487
+ lines.push(`REPOLENS_AI_ENABLED=true`);
1488
+ if (credentials.ai.provider) {
1489
+ lines.push(`REPOLENS_AI_PROVIDER=${credentials.ai.provider}`);
1490
+ }
1491
+ if (credentials.ai.apiKey) {
1492
+ lines.push(`REPOLENS_AI_API_KEY=${credentials.ai.apiKey}`);
1493
+ }
1494
+ if (credentials.ai.provider === "github") {
1495
+ lines.push("# GitHub Models uses GITHUB_TOKEN (set separately or auto-available in Actions)");
1496
+ }
1497
+ lines.push("");
1498
+ }
1499
+
1500
+ if (credentials.discord) {
1501
+ lines.push("# Discord Notifications");
1502
+ lines.push(`DISCORD_WEBHOOK_URL=${credentials.discord.webhookUrl}`);
1503
+ lines.push("");
1504
+ }
1505
+
1506
+ return lines.join("\n");
1507
+ }
1508
+
1509
+ // Export helper functions for testing
1510
+ export { parseConfluenceUrl, parseNotionInput };
1511
+
722
1512
  export async function runInit(targetDir = process.cwd(), options = {}) {
723
1513
  const repoRoot = path.resolve(targetDir);
724
1514
 
725
1515
  // Ensure target directory exists
726
1516
  await fs.mkdir(repoRoot, { recursive: true });
727
1517
 
728
- // Interactive wizard if --interactive flag is set
1518
+ // Interactive wizard is now the default (--quick skips it)
729
1519
  let wizardAnswers = null;
730
1520
  if (options.interactive) {
731
1521
  wizardAnswers = await runInteractiveWizard(repoRoot);
1522
+ if (!wizardAnswers) {
1523
+ // User cancelled the wizard
1524
+ return;
1525
+ }
732
1526
  }
733
1527
 
734
1528
  // Prompt for Notion credentials interactively (only in non-wizard mode)
@@ -765,7 +1559,7 @@ export async function runInit(targetDir = process.cwd(), options = {}) {
765
1559
  for (const root of detectedRoots) {
766
1560
  info(` - ${root}`);
767
1561
  }
768
- } else {
1562
+ } else if (!wizardAnswers) {
769
1563
  info(`No known roots detected. Falling back to default config.`);
770
1564
  }
771
1565
 
@@ -792,18 +1586,25 @@ export async function runInit(targetDir = process.cwd(), options = {}) {
792
1586
  info(`Skipped existing ${envExamplePath}`);
793
1587
  }
794
1588
 
795
- // Create .env file with collected credentials
796
- if (notionCredentials && !envExists) {
1589
+ // Create .env file with collected credentials (wizard mode)
1590
+ if (wizardAnswers?.credentials && Object.keys(wizardAnswers.credentials).length > 0 && !envExists) {
1591
+ const envContent = buildEnvFromCredentials(wizardAnswers.credentials);
1592
+ if (envContent.trim()) {
1593
+ await fs.writeFile(envPath, envContent, "utf8");
1594
+ info(`โœ… Created ${envPath} with your credentials`);
1595
+ await ensureEnvInGitignore(repoRoot);
1596
+ }
1597
+ }
1598
+ // Legacy: Create .env file with Notion credentials (non-wizard mode)
1599
+ else if (notionCredentials && !envExists) {
797
1600
  const envContent = `NOTION_TOKEN=${notionCredentials.token}
798
1601
  NOTION_PARENT_PAGE_ID=${notionCredentials.parentPageId}
799
1602
  NOTION_VERSION=2022-06-28
800
1603
  `;
801
1604
  await fs.writeFile(envPath, envContent, "utf8");
802
1605
  info(`โœ… Created ${envPath} with your Notion credentials`);
803
-
804
- // Ensure .env is in .gitignore
805
1606
  await ensureEnvInGitignore(repoRoot);
806
- } else if (notionCredentials && envExists) {
1607
+ } else if ((notionCredentials || wizardAnswers?.credentials) && envExists) {
807
1608
  warn(`Skipped existing ${envPath} - your credentials were not overwritten`);
808
1609
  }
809
1610
 
@@ -815,7 +1616,53 @@ NOTION_VERSION=2022-06-28
815
1616
  }
816
1617
 
817
1618
  info("\nโœจ RepoLens initialization complete!\n");
818
- if (hasGitHubToken && !wizardAnswers) {
1619
+
1620
+ // Wizard mode: Show tailored summary
1621
+ if (wizardAnswers) {
1622
+ info("๐Ÿ“ Files created:");
1623
+ info(" โ€ข .repolens.yml โ€” Configuration");
1624
+ info(" โ€ข .github/workflows/repolens.yml โ€” GitHub Actions workflow");
1625
+ info(" โ€ข .env.example โ€” Template for credentials");
1626
+ if (wizardAnswers.credentials && Object.keys(wizardAnswers.credentials).length > 0) {
1627
+ info(" โ€ข .env โ€” Your credentials (gitignored)");
1628
+ }
1629
+ info(" โ€ข README.repolens.md โ€” Getting started guide");
1630
+
1631
+ if (wizardAnswers.githubSecretsNeeded && wizardAnswers.githubSecretsNeeded.length > 0) {
1632
+ info("\n๐Ÿ” GitHub Actions Secrets:");
1633
+ info(" Add at: Settings โ†’ Secrets โ†’ Actions");
1634
+ for (const secret of wizardAnswers.githubSecretsNeeded) {
1635
+ info(` โ€ข ${secret}`);
1636
+ }
1637
+ }
1638
+
1639
+ // Show .env sourcing instructions if credentials were collected
1640
+ const hasLocalCredentials = wizardAnswers.credentials && Object.keys(wizardAnswers.credentials).length > 0;
1641
+
1642
+ info("\n๐Ÿš€ Next steps:");
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`);
1661
+ return;
1662
+ }
1663
+
1664
+ // Non-wizard mode: Original output
1665
+ if (hasGitHubToken && !wizardAnswers) {
819
1666
  info("๐Ÿค– Detected GITHUB_TOKEN โ€” AI-enhanced docs enabled via GitHub Models (free)");
820
1667
  info(" Your workflow and config are pre-configured. No extra setup needed.\n");
821
1668
  }