@chappibunny/repolens 1.9.12 → 1.10.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,35 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 1.10.0
6
+
7
+ ### ✨ Interactive Init is Now Default
8
+
9
+ - **`repolens init` is now fully interactive**: No more `--interactive` flag needed — the wizard runs by default
10
+ - **Use `--quick` to skip the wizard**: For CI or minimal scaffolding, use `repolens init --quick`
11
+ - **Comprehensive wizard**: Configures ALL publishers (Notion, Confluence, GitHub Wiki), not just Notion
12
+ - Collects credentials for each selected publisher
13
+ - Guides AI provider selection with environment validation
14
+ - Discord webhook configuration
15
+ - Branch filtering for Notion/Confluence publishing
16
+ - Shows clear summary of GitHub Actions secrets needed
17
+ - **Credentials written to `.env`**: All collected credentials are saved locally (gitignored)
18
+ - **Better validation**: Shows specific status for each credential (✓ set vs ○ needed)
19
+
20
+ ### 🤖 AI Setup from CLI
21
+
22
+ - **Browser auto-open**: Wizard offers to open API key signup pages (OpenAI, Anthropic, Google) in your browser
23
+ - **API key validation**: Tests your AI key immediately after you paste it — confirms it works before saving
24
+ - **GitHub Token testing**: Validates existing `GITHUB_TOKEN` works with GitHub Models
25
+ - **Clear feedback**: Shows ✓ valid / ⚠ invalid with specific error messages
26
+ - **Writes all config**: `.env` now includes `REPOLENS_AI_ENABLED=true` and `REPOLENS_AI_PROVIDER=...`
27
+
28
+ ### 🔧 AI Diagnostics
29
+
30
+ - **Better error messages**: AI failures now show the actual reason (e.g., "Missing API key (expected REPOLENS_AI_API_KEY)")
31
+ - **Config logging**: First AI call logs the provider, model, and key prefix being used
32
+ - **Workflow template updated**: Default workflow now shows both GitHub Models (free) AND OpenAI options with clear comments
33
+
5
34
  ## 1.9.12
6
35
 
7
36
  ### 🐛 Bug Fixes
@@ -149,7 +178,7 @@ Users without AI API keys now get production-quality documentation instead of sp
149
178
  - **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
179
  - **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
180
  - **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`.
181
+ - **Demo AI upsell**: `repolens demo` now shows a hint about GitHub Models (free) when AI is not enabled, guiding users to `repolens init`.
153
182
  - **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
183
  - **Doctor validation**: `repolens doctor` now checks for `GITHUB_TOKEN` when provider is `github`, and `REPOLENS_AI_API_KEY` for other providers.
155
184
 
@@ -449,7 +478,7 @@ RepoLens v1.0.0 marks the first stable release with a frozen public API. All CLI
449
478
  ## 0.7.0
450
479
 
451
480
  ### ✨ 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
481
+ - **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
482
  - **Watch Mode**: `repolens watch` — watches source directories for changes and regenerates Markdown docs with 500ms debounce (no API calls)
454
483
  - **Enhanced Error Messages**: Centralized error catalog with actionable guidance — every error now shows what went wrong, why, and how to fix it
455
484
  - **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.10.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,14 +1,15 @@
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"];
7
8
  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)" },
9
+ { value: "github", label: "GitHub Models (free in GitHub Actions)", signupUrl: null },
10
+ { value: "openai_compatible", label: "OpenAI / Compatible (GPT-5, GPT-4o, etc.)", signupUrl: "https://platform.openai.com/api-keys" },
11
+ { value: "anthropic", label: "Anthropic (Claude)", signupUrl: "https://console.anthropic.com/settings/keys" },
12
+ { value: "google", label: "Google (Gemini)", signupUrl: "https://aistudio.google.com/app/apikey" },
12
13
  ];
13
14
  const SCAN_PRESETS = {
14
15
  nextjs: {
@@ -89,10 +90,14 @@ jobs:
89
90
  CONFLUENCE_API_TOKEN: \${{ secrets.CONFLUENCE_API_TOKEN }}
90
91
  CONFLUENCE_SPACE_KEY: \${{ secrets.CONFLUENCE_SPACE_KEY }}
91
92
  CONFLUENCE_PARENT_PAGE_ID: \${{ secrets.CONFLUENCE_PARENT_PAGE_ID }}
92
- # AI-enhanced docs via GitHub Models (free)
93
+ # AI-enhanced docs: Choose ONE option below
93
94
  REPOLENS_AI_ENABLED: true
95
+ # Option A: GitHub Models (free — uses GITHUB_TOKEN)
94
96
  REPOLENS_AI_PROVIDER: github
95
97
  GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
98
+ # Option B: OpenAI / Anthropic / Google (comment out Option A, uncomment below)
99
+ # REPOLENS_AI_API_KEY: \${{ secrets.REPOLENS_AI_API_KEY }}
100
+ # REPOLENS_AI_PROVIDER: openai_compatible # or: anthropic, google
96
101
  run: npx @chappibunny/repolens@latest publish
97
102
  `;
98
103
 
@@ -283,6 +288,117 @@ async function dirExists(dirPath) {
283
288
  }
284
289
  }
285
290
 
291
+ /**
292
+ * Open a URL in the default browser (cross-platform).
293
+ */
294
+ function openUrl(url) {
295
+ const platform = process.platform;
296
+ let cmd;
297
+ if (platform === "darwin") {
298
+ cmd = `open "${url}"`;
299
+ } else if (platform === "win32") {
300
+ cmd = `start "" "${url}"`;
301
+ } else {
302
+ cmd = `xdg-open "${url}"`;
303
+ }
304
+ return new Promise((resolve) => {
305
+ exec(cmd, (err) => {
306
+ if (err) {
307
+ warn(`Could not open browser: ${err.message}`);
308
+ resolve(false);
309
+ } else {
310
+ resolve(true);
311
+ }
312
+ });
313
+ });
314
+ }
315
+
316
+ /**
317
+ * Test an AI API key by making a minimal request.
318
+ * Returns { success: true } or { success: false, error: string }.
319
+ */
320
+ async function testAIKey(provider, apiKey) {
321
+ try {
322
+ const timeout = 15000;
323
+ const controller = new AbortController();
324
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
325
+
326
+ let url, headers, body;
327
+
328
+ if (provider === "github") {
329
+ url = "https://models.inference.ai.azure.com/chat/completions";
330
+ headers = {
331
+ "Content-Type": "application/json",
332
+ "Authorization": `Bearer ${apiKey}`,
333
+ };
334
+ body = JSON.stringify({
335
+ model: "gpt-4o-mini",
336
+ messages: [{ role: "user", content: "Say OK" }],
337
+ max_tokens: 5,
338
+ });
339
+ } else if (provider === "openai_compatible") {
340
+ url = "https://api.openai.com/v1/chat/completions";
341
+ headers = {
342
+ "Content-Type": "application/json",
343
+ "Authorization": `Bearer ${apiKey}`,
344
+ };
345
+ body = JSON.stringify({
346
+ model: "gpt-4o-mini",
347
+ messages: [{ role: "user", content: "Say OK" }],
348
+ max_tokens: 5,
349
+ });
350
+ } else if (provider === "anthropic") {
351
+ url = "https://api.anthropic.com/v1/messages";
352
+ headers = {
353
+ "Content-Type": "application/json",
354
+ "x-api-key": apiKey,
355
+ "anthropic-version": "2023-06-01",
356
+ };
357
+ body = JSON.stringify({
358
+ model: "claude-sonnet-4-20250514",
359
+ max_tokens: 5,
360
+ messages: [{ role: "user", content: "Say OK" }],
361
+ });
362
+ } else if (provider === "google") {
363
+ url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
364
+ headers = { "Content-Type": "application/json" };
365
+ body = JSON.stringify({
366
+ contents: [{ parts: [{ text: "Say OK" }] }],
367
+ generationConfig: { maxOutputTokens: 5 },
368
+ });
369
+ } else {
370
+ return { success: false, error: "Unknown provider" };
371
+ }
372
+
373
+ const response = await fetch(url, {
374
+ method: "POST",
375
+ headers,
376
+ body,
377
+ signal: controller.signal,
378
+ });
379
+
380
+ clearTimeout(timeoutId);
381
+
382
+ if (response.ok) {
383
+ return { success: true };
384
+ }
385
+
386
+ const errorBody = await response.text().catch(() => "");
387
+ if (response.status === 401 || response.status === 403) {
388
+ return { success: false, error: "Invalid API key" };
389
+ }
390
+ if (response.status === 429) {
391
+ return { success: false, error: "Rate limited — but key is valid" };
392
+ }
393
+ return { success: false, error: `API error ${response.status}: ${errorBody.slice(0, 100)}` };
394
+ } catch (err) {
395
+ if (err.name === "AbortError") {
396
+ return { success: false, error: "Request timed out" };
397
+ }
398
+ return { success: false, error: err.message };
399
+ }
400
+ }
401
+
286
402
  async function detectRepoStructure(repoRoot) {
287
403
  const detectedRoots = [];
288
404
 
@@ -482,7 +598,8 @@ async function promptNotionCredentials() {
482
598
  });
483
599
 
484
600
  try {
485
- info("\n📝 Notion Setup (optional)");
601
+ info("\n📝 Quick Setup — Notion Publishing");
602
+ info("(Use 'repolens init' without --quick for full wizard)\n");
486
603
  const useNotion = await rl.question("Would you like to publish to Notion? (Y/n): ");
487
604
 
488
605
  if (useNotion.toLowerCase() === 'n') {
@@ -564,15 +681,20 @@ async function runInteractiveWizard(repoRoot) {
564
681
 
565
682
  try {
566
683
  info("\n🧙 Interactive Configuration Wizard\n");
684
+ info("This wizard will help you configure RepoLens for your project.");
685
+ info("Press Enter to accept defaults shown in parentheses.\n");
567
686
 
568
687
  // 1. Project name
569
688
  const defaultName = path.basename(repoRoot) || "my-project";
570
- const projectName = (await ask(`Project name (${defaultName}): `)).trim() || defaultName;
689
+ const projectName = (await ask(`📦 Project name (${defaultName}): `)).trim() || defaultName;
571
690
 
572
691
  // 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";
692
+ info("\n📤 Select publishers (comma-separated numbers):");
693
+ PUBLISHER_CHOICES.forEach((p, i) => {
694
+ const desc = PUBLISHER_DESCRIPTIONS[p] || "";
695
+ info(` ${i + 1}. ${p}${desc ? ` — ${desc}` : ""}`);
696
+ });
697
+ const pubInput = (await ask(`Publishers [1] (default: 1 markdown): `)).trim() || "1";
576
698
  const publishers = pubInput
577
699
  .split(",")
578
700
  .map((n) => parseInt(n.trim(), 10))
@@ -580,24 +702,156 @@ async function runInteractiveWizard(repoRoot) {
580
702
  .map((n) => PUBLISHER_CHOICES[n - 1]);
581
703
  if (publishers.length === 0) publishers.push("markdown");
582
704
 
583
- // 3. AI
584
- const enableAi = (await ask("\nEnable AI-enhanced documentation? (y/N): ")).trim().toLowerCase() === "y";
705
+ // 3. Collect credentials for each publisher
706
+ const credentials = {};
707
+ const githubSecretsNeeded = [];
708
+
709
+ // Notion setup
710
+ if (publishers.includes("notion")) {
711
+ 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");
714
+
715
+ const setupNow = (await ask(" Configure Notion credentials now? (Y/n): ")).trim().toLowerCase();
716
+ 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;
724
+ } else {
725
+ info(" ✓ Notion credentials collected");
726
+ }
727
+ }
728
+ githubSecretsNeeded.push("NOTION_TOKEN", "NOTION_PARENT_PAGE_ID");
729
+ }
730
+
731
+ // Confluence setup
732
+ if (publishers.includes("confluence")) {
733
+ 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");
736
+
737
+ const setupNow = (await ask(" Configure Confluence credentials now? (Y/n): ")).trim().toLowerCase();
738
+ 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;
750
+ } else {
751
+ info(" ✓ Confluence credentials collected");
752
+ }
753
+ }
754
+ githubSecretsNeeded.push("CONFLUENCE_URL", "CONFLUENCE_EMAIL", "CONFLUENCE_API_TOKEN", "CONFLUENCE_SPACE_KEY", "CONFLUENCE_PARENT_PAGE_ID");
755
+ }
756
+
757
+ // GitHub Wiki setup
758
+ if (publishers.includes("github_wiki")) {
759
+ info("\n📝 GitHub Wiki Setup");
760
+ info(" Requires GITHUB_TOKEN with repo scope.");
761
+ if (process.env.GITHUB_TOKEN) {
762
+ info(" ✓ GITHUB_TOKEN is set in your environment");
763
+ } else {
764
+ warn(" GITHUB_TOKEN not found in environment.");
765
+ info(" For local use: export GITHUB_TOKEN=your_token");
766
+ info(" For GitHub Actions: Uses ${{ secrets.GITHUB_TOKEN }} automatically");
767
+ }
768
+ githubSecretsNeeded.push("GITHUB_TOKEN");
769
+ }
770
+
771
+ // 4. AI Configuration
772
+ info("\n🤖 AI-Enhanced Documentation");
773
+ info(" Adds natural language explanations for non-technical stakeholders.");
774
+ const enableAi = (await ask(" Enable AI features? (Y/n): ")).trim().toLowerCase() !== "n";
775
+
585
776
  let aiProvider = null;
777
+ let aiApiKey = null;
778
+
586
779
  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";
780
+ info("\n Select AI provider:");
781
+ AI_PROVIDERS.forEach((p, i) => info(` ${i + 1}. ${p.label}`));
782
+ const aiInput = (await ask(` Provider [1] (default: 1 GitHub Models — free): `)).trim() || "1";
590
783
  const idx = parseInt(aiInput, 10);
591
784
  const chosen = AI_PROVIDERS[(idx >= 1 && idx <= AI_PROVIDERS.length) ? idx - 1 : 0];
592
785
  aiProvider = chosen.value;
786
+
593
787
  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.");
788
+ info("\n ✨ GitHub Models is free and uses your GITHUB_TOKEN.");
789
+
790
+ // Check for existing token
791
+ const existingToken = process.env.GITHUB_TOKEN;
792
+ if (existingToken) {
793
+ info(" Testing your GITHUB_TOKEN...");
794
+ const testResult = await testAIKey("github", existingToken);
795
+ if (testResult.success) {
796
+ info(" ✓ GITHUB_TOKEN is valid — AI will work locally and in Actions");
797
+ credentials.ai = { provider: "github", useGitHubToken: true };
798
+ } else {
799
+ warn(` ⚠ GITHUB_TOKEN test failed: ${testResult.error}`);
800
+ info(" AI will still work in GitHub Actions with ${{ secrets.GITHUB_TOKEN }}");
801
+ }
802
+ } else {
803
+ info(" No GITHUB_TOKEN found in environment.");
804
+ info(" In GitHub Actions: Works automatically with ${{ secrets.GITHUB_TOKEN }}");
805
+ info(" For local testing: export GITHUB_TOKEN=your_personal_access_token");
806
+ }
807
+ githubSecretsNeeded.push("GITHUB_TOKEN");
808
+ credentials.ai = { ...(credentials.ai || {}), provider: "github", enabled: true };
809
+ } else {
810
+ // Non-GitHub provider: help them get an API key
811
+ info(`\n ${chosen.label} requires an API key.`);
812
+
813
+ // Offer to open signup URL
814
+ if (chosen.signupUrl) {
815
+ const openBrowser = (await ask(` Open ${chosen.value} signup page in browser? (Y/n): `)).trim().toLowerCase();
816
+ if (openBrowser !== "n") {
817
+ info(` Opening ${chosen.signupUrl}...`);
818
+ await openUrl(chosen.signupUrl);
819
+ info(" Create an API key, then paste it below.\n");
820
+ }
821
+ }
822
+
823
+ const keyInput = (await ask(` Paste your API key (or press Enter to skip): `)).trim();
824
+ if (keyInput) {
825
+ info(" Testing your API key...");
826
+ const testResult = await testAIKey(aiProvider, keyInput);
827
+
828
+ if (testResult.success) {
829
+ info(" ✓ API key is valid!");
830
+ aiApiKey = keyInput;
831
+ credentials.ai = { apiKey: keyInput, provider: aiProvider, enabled: true };
832
+ } else if (testResult.error === "Rate limited — but key is valid") {
833
+ info(" ✓ API key is valid (rate limited, but will work)");
834
+ aiApiKey = keyInput;
835
+ credentials.ai = { apiKey: keyInput, provider: aiProvider, enabled: true };
836
+ } else {
837
+ warn(` ⚠ API key test failed: ${testResult.error}`);
838
+ const useAnyway = (await ask(` Save this key anyway? (y/N): `)).trim().toLowerCase();
839
+ if (useAnyway === "y") {
840
+ aiApiKey = keyInput;
841
+ credentials.ai = { apiKey: keyInput, provider: aiProvider, enabled: true };
842
+ } else {
843
+ warn(" Skipping AI configuration. You can set REPOLENS_AI_API_KEY later.");
844
+ }
845
+ }
846
+ } else {
847
+ warn(" No API key provided. Set REPOLENS_AI_API_KEY in .env or GitHub secrets.");
848
+ }
849
+ githubSecretsNeeded.push("REPOLENS_AI_API_KEY");
596
850
  }
597
851
  }
598
852
 
599
- // 4. Scan preset
600
- info("\nScan preset:");
853
+ // 5. Scan preset
854
+ info("\n📂 Scan Preset (determines which files to analyze):");
601
855
  const presetKeys = Object.keys(SCAN_PRESETS);
602
856
  presetKeys.forEach((p, i) => info(` ${i + 1}. ${p}`));
603
857
  const presetInput = (await ask(`Preset [3] (default: 3 generic): `)).trim() || "3";
@@ -605,20 +859,80 @@ async function runInteractiveWizard(repoRoot) {
605
859
  const presetKey = presetKeys[(presetIdx >= 1 && presetIdx <= presetKeys.length) ? presetIdx - 1 : 2];
606
860
  const preset = SCAN_PRESETS[presetKey];
607
861
 
608
- // 5. Branch filtering
609
- const branchInput = (await ask("\nBranches allowed to publish to Notion/Confluence (comma-separated, default: main): ")).trim() || "main";
862
+ // 6. Branch filtering
863
+ info("\n🌿 Branch Filtering");
864
+ info(" Limits which branches can publish to Notion/Confluence.");
865
+ const branchInput = (await ask(" Allowed branches (comma-separated, default: main): ")).trim() || "main";
610
866
  const branches = branchInput.split(",").map((b) => b.trim()).filter(Boolean);
611
867
 
612
- // 6. Discord
613
- const enableDiscord = (await ask("\nEnable Discord notifications? (y/N): ")).trim().toLowerCase() === "y";
868
+ // 7. Discord notifications
869
+ info("\n🔔 Discord Notifications");
870
+ const enableDiscord = (await ask(" Enable Discord notifications? (y/N): ")).trim().toLowerCase() === "y";
871
+
872
+ let discordWebhook = null;
873
+ if (enableDiscord) {
874
+ info(" Get webhook URL from: Server Settings > Integrations > Webhooks");
875
+ discordWebhook = (await ask(" DISCORD_WEBHOOK_URL (leave blank to skip): ")).trim() || null;
876
+ if (discordWebhook) {
877
+ credentials.discord = { webhookUrl: discordWebhook };
878
+ }
879
+ githubSecretsNeeded.push("DISCORD_WEBHOOK_URL");
880
+ }
614
881
 
615
- info("\n✓ Wizard complete. Generating config...\n");
616
- return { projectName, publishers, enableAi, aiProvider, preset, branches, enableDiscord };
882
+ // Summary
883
+ info("\n" + "═".repeat(60));
884
+ info("📋 Configuration Summary");
885
+ info("═".repeat(60));
886
+ info(` Project: ${projectName}`);
887
+ info(` Publishers: ${publishers.join(", ")}`);
888
+ info(` AI: ${enableAi ? `Enabled (${aiProvider})` : "Disabled"}`);
889
+ info(` Scan: ${presetKey} preset`);
890
+ info(` Branches: ${branches.join(", ")}`);
891
+ info(` Discord: ${enableDiscord ? "Enabled" : "Disabled"}`);
892
+
893
+ // GitHub secrets summary
894
+ const uniqueSecrets = [...new Set(githubSecretsNeeded)];
895
+ if (uniqueSecrets.length > 0) {
896
+ info("\n📌 GitHub Actions Secrets Required:");
897
+ info(" Add these at: https://github.com/YOUR_ORG/YOUR_REPO/settings/secrets/actions");
898
+ for (const secret of uniqueSecrets) {
899
+ const status = credentials[secret.toLowerCase().split("_")[0]] ? "✓" : "○";
900
+ info(` ${status} ${secret}`);
901
+ }
902
+ }
903
+
904
+ info("\n" + "═".repeat(60));
905
+
906
+ const proceed = (await ask("\nProceed with this configuration? (Y/n): ")).trim().toLowerCase();
907
+ if (proceed === "n") {
908
+ info("Configuration cancelled.");
909
+ return null;
910
+ }
911
+
912
+ info("\n✓ Wizard complete. Generating files...\n");
913
+ return {
914
+ projectName,
915
+ publishers,
916
+ enableAi,
917
+ aiProvider,
918
+ preset,
919
+ branches,
920
+ enableDiscord,
921
+ credentials,
922
+ githubSecretsNeeded: uniqueSecrets
923
+ };
617
924
  } finally {
618
925
  rl.close();
619
926
  }
620
927
  }
621
928
 
929
+ const PUBLISHER_DESCRIPTIONS = {
930
+ markdown: "Local files in .repolens/",
931
+ notion: "Notion workspace pages",
932
+ confluence: "Atlassian Confluence pages",
933
+ github_wiki: "Repository wiki pages",
934
+ };
935
+
622
936
  /**
623
937
  * Build a .repolens.yml from wizard answers.
624
938
  */
@@ -719,16 +1033,70 @@ function buildWizardConfig(answers) {
719
1033
  return lines.join("\n");
720
1034
  }
721
1035
 
1036
+ /**
1037
+ * Build .env content from wizard credentials.
1038
+ */
1039
+ function buildEnvFromCredentials(credentials) {
1040
+ const lines = [];
1041
+
1042
+ if (credentials.notion) {
1043
+ lines.push("# Notion Publishing");
1044
+ lines.push(`NOTION_TOKEN=${credentials.notion.token}`);
1045
+ lines.push(`NOTION_PARENT_PAGE_ID=${credentials.notion.parentPageId}`);
1046
+ lines.push(`NOTION_VERSION=2022-06-28`);
1047
+ lines.push("");
1048
+ }
1049
+
1050
+ if (credentials.confluence) {
1051
+ lines.push("# Confluence Publishing");
1052
+ lines.push(`CONFLUENCE_URL=${credentials.confluence.url}`);
1053
+ lines.push(`CONFLUENCE_EMAIL=${credentials.confluence.email}`);
1054
+ lines.push(`CONFLUENCE_API_TOKEN=${credentials.confluence.apiToken}`);
1055
+ lines.push(`CONFLUENCE_SPACE_KEY=${credentials.confluence.spaceKey}`);
1056
+ if (credentials.confluence.parentPageId) {
1057
+ lines.push(`CONFLUENCE_PARENT_PAGE_ID=${credentials.confluence.parentPageId}`);
1058
+ }
1059
+ lines.push("");
1060
+ }
1061
+
1062
+ if (credentials.ai?.enabled) {
1063
+ lines.push("# AI Configuration");
1064
+ lines.push(`REPOLENS_AI_ENABLED=true`);
1065
+ if (credentials.ai.provider) {
1066
+ lines.push(`REPOLENS_AI_PROVIDER=${credentials.ai.provider}`);
1067
+ }
1068
+ if (credentials.ai.apiKey) {
1069
+ lines.push(`REPOLENS_AI_API_KEY=${credentials.ai.apiKey}`);
1070
+ }
1071
+ if (credentials.ai.provider === "github") {
1072
+ lines.push("# GitHub Models uses GITHUB_TOKEN (set separately or auto-available in Actions)");
1073
+ }
1074
+ lines.push("");
1075
+ }
1076
+
1077
+ if (credentials.discord) {
1078
+ lines.push("# Discord Notifications");
1079
+ lines.push(`DISCORD_WEBHOOK_URL=${credentials.discord.webhookUrl}`);
1080
+ lines.push("");
1081
+ }
1082
+
1083
+ return lines.join("\n");
1084
+ }
1085
+
722
1086
  export async function runInit(targetDir = process.cwd(), options = {}) {
723
1087
  const repoRoot = path.resolve(targetDir);
724
1088
 
725
1089
  // Ensure target directory exists
726
1090
  await fs.mkdir(repoRoot, { recursive: true });
727
1091
 
728
- // Interactive wizard if --interactive flag is set
1092
+ // Interactive wizard is now the default (--quick skips it)
729
1093
  let wizardAnswers = null;
730
1094
  if (options.interactive) {
731
1095
  wizardAnswers = await runInteractiveWizard(repoRoot);
1096
+ if (!wizardAnswers) {
1097
+ // User cancelled the wizard
1098
+ return;
1099
+ }
732
1100
  }
733
1101
 
734
1102
  // Prompt for Notion credentials interactively (only in non-wizard mode)
@@ -765,7 +1133,7 @@ export async function runInit(targetDir = process.cwd(), options = {}) {
765
1133
  for (const root of detectedRoots) {
766
1134
  info(` - ${root}`);
767
1135
  }
768
- } else {
1136
+ } else if (!wizardAnswers) {
769
1137
  info(`No known roots detected. Falling back to default config.`);
770
1138
  }
771
1139
 
@@ -792,18 +1160,25 @@ export async function runInit(targetDir = process.cwd(), options = {}) {
792
1160
  info(`Skipped existing ${envExamplePath}`);
793
1161
  }
794
1162
 
795
- // Create .env file with collected credentials
796
- if (notionCredentials && !envExists) {
1163
+ // Create .env file with collected credentials (wizard mode)
1164
+ if (wizardAnswers?.credentials && Object.keys(wizardAnswers.credentials).length > 0 && !envExists) {
1165
+ const envContent = buildEnvFromCredentials(wizardAnswers.credentials);
1166
+ if (envContent.trim()) {
1167
+ await fs.writeFile(envPath, envContent, "utf8");
1168
+ info(`✅ Created ${envPath} with your credentials`);
1169
+ await ensureEnvInGitignore(repoRoot);
1170
+ }
1171
+ }
1172
+ // Legacy: Create .env file with Notion credentials (non-wizard mode)
1173
+ else if (notionCredentials && !envExists) {
797
1174
  const envContent = `NOTION_TOKEN=${notionCredentials.token}
798
1175
  NOTION_PARENT_PAGE_ID=${notionCredentials.parentPageId}
799
1176
  NOTION_VERSION=2022-06-28
800
1177
  `;
801
1178
  await fs.writeFile(envPath, envContent, "utf8");
802
1179
  info(`✅ Created ${envPath} with your Notion credentials`);
803
-
804
- // Ensure .env is in .gitignore
805
1180
  await ensureEnvInGitignore(repoRoot);
806
- } else if (notionCredentials && envExists) {
1181
+ } else if ((notionCredentials || wizardAnswers?.credentials) && envExists) {
807
1182
  warn(`Skipped existing ${envPath} - your credentials were not overwritten`);
808
1183
  }
809
1184
 
@@ -815,7 +1190,36 @@ NOTION_VERSION=2022-06-28
815
1190
  }
816
1191
 
817
1192
  info("\n✨ RepoLens initialization complete!\n");
818
- if (hasGitHubToken && !wizardAnswers) {
1193
+
1194
+ // Wizard mode: Show tailored summary
1195
+ if (wizardAnswers) {
1196
+ info("📁 Files created:");
1197
+ info(" • .repolens.yml — Configuration");
1198
+ info(" • .github/workflows/repolens.yml — GitHub Actions workflow");
1199
+ info(" • .env.example — Template for credentials");
1200
+ if (wizardAnswers.credentials && Object.keys(wizardAnswers.credentials).length > 0) {
1201
+ info(" • .env — Your credentials (gitignored)");
1202
+ }
1203
+ info(" • README.repolens.md — Getting started guide");
1204
+
1205
+ if (wizardAnswers.githubSecretsNeeded && wizardAnswers.githubSecretsNeeded.length > 0) {
1206
+ info("\n🔐 GitHub Actions Secrets:");
1207
+ info(" Add at: Settings → Secrets → Actions");
1208
+ for (const secret of wizardAnswers.githubSecretsNeeded) {
1209
+ info(` • ${secret}`);
1210
+ }
1211
+ }
1212
+
1213
+ 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");
1218
+ return;
1219
+ }
1220
+
1221
+ // Non-wizard mode: Original output
1222
+ if (hasGitHubToken && !wizardAnswers) {
819
1223
  info("🤖 Detected GITHUB_TOKEN — AI-enhanced docs enabled via GitHub Models (free)");
820
1224
  info(" Your workflow and config are pre-configured. No extra setup needed.\n");
821
1225
  }