@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 +82 -2
- package/README.md +2 -2
- package/docs/ROADMAP.md +1 -1
- package/docs/STABILITY.md +3 -3
- package/package.json +1 -1
- package/src/ai/generate-sections.js +2 -2
- package/src/ai/provider.js +13 -2
- package/src/cli.js +9 -9
- package/src/init.js +888 -41
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
|
|
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
|
|
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` |
|
|
169
|
-
| `npx @chappibunny/repolens init --
|
|
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
|
|
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 |
|
|
20
|
-
| `init --
|
|
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
|
-
| `--
|
|
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
|
@@ -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}
|
|
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(
|
|
79
|
+
warn(`AI generation failed: ${result.error || "unknown error"}`);
|
|
80
80
|
return fallbackFn();
|
|
81
81
|
}
|
|
82
82
|
|
package/src/ai/provider.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
138
|
-
info(`${fmt.cyan("โ")} ${fmt.green("repolens demo")}
|
|
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
|
-
--
|
|
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 #
|
|
254
|
-
repolens init --
|
|
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
|
-
|
|
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
|
|
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",
|
|
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", "
|
|
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
|
-
"
|
|
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
|
|
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๐
|
|
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(
|
|
961
|
+
const projectName = (await ask(`๐ฆ Project name (${defaultName}): `)).trim() || defaultName;
|
|
571
962
|
|
|
572
963
|
// 2. Publishers
|
|
573
|
-
info("\
|
|
574
|
-
PUBLISHER_CHOICES.forEach((p, i) =>
|
|
575
|
-
|
|
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.
|
|
584
|
-
const
|
|
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(`
|
|
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
|
|
595
|
-
|
|
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
|
-
//
|
|
600
|
-
info("\
|
|
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((
|
|
603
|
-
|
|
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 :
|
|
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
|
-
//
|
|
609
|
-
|
|
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
|
-
//
|
|
613
|
-
|
|
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
|
|
616
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
}
|