@chappibunny/repolens 1.0.0 → 1.2.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,13 +2,75 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 1.2.0
6
+
7
+ ### ✨ Config Migration
8
+
9
+ - **`repolens migrate` now patches `.repolens.yml`**: Automatically adds `configVersion: 1` to legacy config files that are missing it. Creates a `.repolens.yml.backup` before modifying. Supports both `.yml` and `.yaml` extensions, dry-run mode, and graceful handling of parse errors or missing configs.
10
+
11
+ ### � Bug Fixes
12
+
13
+ - **GitHub Wiki empty pages**: Fixed branch mismatch where `git init` created a `main` branch but GitHub Wiki serves from `master`. Now explicitly initializes with `master` and pushes to `refs/heads/master`. Also added content validation to skip pages with empty content.
14
+
15
+ ### �📝 Documentation
16
+
17
+ - Updated all documentation to reflect v1.2.0 version references
18
+ - Updated TROUBLESHOOTING.md with `migrate` auto-fix hint for missing `configVersion`
19
+ - Updated KNOWN_ISSUES.md planned improvements (removed shipped features)
20
+ - Updated ROADMAP.md with v1.1.0 shipped items
21
+
22
+ ## 1.1.0
23
+
24
+ ### ✨ GitHub Wiki Publisher UX Enhancement
25
+
26
+ Major upgrade to the wiki output quality — better information hierarchy, audience-aware navigation, and richer page structure.
27
+
28
+ - **Audience-grouped Home page**: Pages organized by reader (Stakeholders, Engineers, New Contributors, Change Tracking) instead of a flat list
29
+ - **Page descriptions**: Each link on Home includes a one-line summary of the page's purpose
30
+ - **Status rail**: Home page now shows a metadata table (project, branch, page count, publisher, source)
31
+ - **Recommended reading order**: Guided path through the docs for first-time readers
32
+ - **Grouped sidebar**: Navigation split into Overview and Architecture sections (plus Custom Pages)
33
+ - **Page metadata headers**: Every page gets a `[← Home](Home)` back-link, audience tag, and branch indicator
34
+ - **Cleaner footer**: Compact format with branch context and Home link
35
+ - **New constants**: `PAGE_DESCRIPTIONS`, `AUDIENCE_GROUPS`, `SIDEBAR_GROUPS`, `PAGE_AUDIENCE`
36
+ - **New helpers**: `getPageDisplayTitle()`, `getCustomPageKeys()`, `wikiLink()`, `pageHeader()`
37
+
38
+ ### 🐛 Bug Fixes
39
+ - **Temperature still sent to GPT-5**: `DEFAULT_TEMPERATURE = 0.2` always leaked into API requests even after v1.0.1 fixes — the fallback chain (`temperature ?? aiConfig.temperature ?? DEFAULT_TEMPERATURE`) guaranteed it was never `undefined`. Removed the default entirely; temperature is now only sent when explicitly configured via `REPOLENS_AI_TEMPERATURE` env var or `ai.temperature` in config.
40
+
41
+ ## 1.0.1
42
+
43
+ ### 🐛 Bug Fixes
44
+ - **GPT-5 API compatibility**: Use `max_completion_tokens` instead of deprecated `max_tokens` parameter
45
+ - **GPT-5 temperature handling**: Omit `temperature` from API requests for models that only support the default value (e.g. gpt-5-mini)
46
+ - Removed all hardcoded `temperature: 0.2` overrides from AI section generators
47
+ - Removed `REPOLENS_AI_TEMPERATURE` from CI workflow and `.env.example`
48
+ - Updated `init` scaffolding to omit temperature from default config
49
+
50
+ ### 🔧 Improvements
51
+ - Upgraded GitHub Actions: checkout v4.2.2, setup-node v4.2.0, Node.js 22
52
+
5
53
  ## 1.0.0
6
54
 
7
55
  ### 🎉 Stable Release
8
56
 
9
57
  RepoLens v1.0.0 marks the first stable release with a frozen public API. All CLI commands, configuration schema, and plugin interfaces are now covered by semantic versioning guarantees. See [STABILITY.md](STABILITY.md) for the full contract.
10
58
 
59
+ ### ✨ New Features
60
+ - **GitHub Wiki Publisher**: Publish documentation directly to your repository's Wiki tab
61
+ - Git-based: clones wiki repo, writes pages, commits and pushes
62
+ - Generates `Home.md` index, `_Sidebar.md` navigation, `_Footer.md`
63
+ - Branch filtering via `github_wiki.branches` in `.repolens.yml`
64
+ - Auto-detects repository from `GITHUB_REPOSITORY` env or git remote
65
+ - Token sanitization in error messages (never leaks `GITHUB_TOKEN`)
66
+ - Config: `sidebar` and `footer` toggles
67
+ - 17 tests covering publishing, branch filtering, config validation, and security
68
+
11
69
  ### 🐛 Bug Fixes
70
+ - **GPT-5 API compatibility**: Use `max_completion_tokens` instead of deprecated `max_tokens` parameter
71
+ - **GPT-5 temperature handling**: Omit `temperature` from API requests for models that only support the default value (e.g. gpt-5-mini)
72
+ - **Git identity in wiki publisher**: Set committer name/email (`RepoLens Bot`) so CI runners can commit
73
+ - **Publisher allowlist**: Added `github_wiki` to `validate.js` publisher validation (was only in `config-schema.js`)
12
74
  - **Doctor false-success**: `repolens doctor` now correctly exits with code 2 when `runDoctor()` reports failures (previously always printed "validation passed")
13
75
  - **Feedback exit code**: `repolens feedback` now exits with code 1 when feedback fails to send (previously exited 0)
14
76
  - **Unknown flags**: `repolens --unknown-flag` now prints an error instead of silently running publish
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  AI-assisted documentation intelligence system that generates architecture docs for engineers AND readable system docs for stakeholders
18
18
 
19
- **Current Status**: v1.0.0 — Stable Release
19
+ **Current Status**: v1.2.0 — Stable Release
20
20
 
21
21
  RepoLens automatically generates and maintains living architecture documentation by analyzing your repository structure, extracting meaningful insights from your package.json, and creating visual dependency graphs. Run it once, or let it auto-update on every push.
22
22
 
@@ -124,7 +124,7 @@ RepoLens automatically detects:
124
124
  ✅ **Deterministic Fallback** - Always generates docs even if AI unavailable
125
125
  ✅ **Business Domain Inference** - Automatically maps code to business functions
126
126
  ✅ **Data Flow Analysis** - Understands how information moves through your system
127
- ✅ **Multiple Publishers** - Output to Notion, Confluence, Markdown, or all three
127
+ ✅ **Multiple Publishers** - Output to Notion, Confluence, GitHub Wiki, Markdown, or all four
128
128
  ✅ **Branch-Aware** - Prevent doc conflicts across branches
129
129
  ✅ **GitHub Actions** - Autonomous operation on every push
130
130
  ✅ **Team Notifications** - Discord integration with rich embeds (NEW in v0.6.0)
@@ -230,10 +230,12 @@ npm link
230
230
  Install from a specific version:
231
231
 
232
232
  ```bash
233
- npm install https://github.com/CHAPIBUNNY/repolens/releases/download/v1.0.0/chappibunny-repolens-1.0.0.tgz
233
+ npm install https://github.com/CHAPIBUNNY/repolens/releases/download/v1.2.0/chappibunny-repolens-1.2.0.tgz
234
234
  ```
235
235
  </details>
236
236
 
237
+ > **Having install problems?** See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for solutions to common issues like `ENOTEMPTY`, `npm ci` failures, and stale versions.
238
+
237
239
  ---
238
240
 
239
241
  ## 🎓 Complete Onboarding Guide
@@ -313,6 +315,14 @@ publishers:
313
315
  ```
314
316
  Maximum visibility: Notion for async collaboration, Confluence for enterprise docs, Markdown for local backups.
315
317
 
318
+ **GitHub Wiki** (ideal for open source):
319
+ ```yaml
320
+ publishers:
321
+ - github_wiki
322
+ - markdown
323
+ ```
324
+ Docs live alongside your code — accessible from the repo's Wiki tab. Requires `GITHUB_TOKEN`.
325
+
316
326
  ### Step 3: Enable AI Features (Optional)
317
327
 
318
328
  **AI-enhanced documentation adds natural language explanations for non-technical audiences.**
@@ -336,7 +346,6 @@ REPOLENS_AI_API_KEY=sk-xxxxxxxxxxxxx
336
346
  # Optional: Customize provider
337
347
  REPOLENS_AI_BASE_URL=https://api.openai.com/v1
338
348
  REPOLENS_AI_MODEL=gpt-5-mini
339
- REPOLENS_AI_TEMPERATURE=0.3
340
349
  REPOLENS_AI_MAX_TOKENS=2000
341
350
  ```
342
351
 
@@ -346,7 +355,6 @@ REPOLENS_AI_MAX_TOKENS=2000
346
355
  ai:
347
356
  enabled: true # Enable AI features
348
357
  mode: hybrid # hybrid, full, or off
349
- temperature: 0.3 # Lower = more focused (0.0-1.0)
350
358
  max_tokens: 2000 # Token limit per request
351
359
 
352
360
  features:
@@ -944,11 +952,14 @@ features:
944
952
  | `configVersion` | number | **Yes** | Schema version (must be `1`) |
945
953
  | `project.name` | string | Yes | Project name |
946
954
  | `project.docs_title_prefix` | string | No | Prefix for documentation titles (default: project name) |
947
- | `publishers` | array | Yes | Output targets: `notion`, `confluence`, `markdown` (+ plugin publishers) |
955
+ | `publishers` | array | Yes | Output targets: `notion`, `confluence`, `github_wiki`, `markdown` (+ plugin publishers) |
948
956
  | `plugins` | array | No | Plugin paths or npm package names |
949
957
  | `notion.branches` | array | No | Branch whitelist for Notion publishing. Supports globs. |
950
958
  | `notion.includeBranchInTitle` | boolean | No | Add `[branch-name]` to titles (default: `true`) |
951
959
  | `confluence.branches` | array | No | Branch whitelist for Confluence publishing. Supports globs. |
960
+ | `github_wiki.branches` | array | No | Branch whitelist for GitHub Wiki publishing. Supports globs. |
961
+ | `github_wiki.sidebar` | boolean | No | Generate `_Sidebar.md` navigation (default: `true`) |
962
+ | `github_wiki.footer` | boolean | No | Generate `_Footer.md` (default: `true`) |
952
963
  | `discord.enabled` | boolean | No | Enable Discord notifications (default: `true` if webhook set) |
953
964
  | `discord.notifyOn` | string | No | Notification policy: `always`, `significant`, `never` (default: `significant`) |
954
965
  | `discord.significantThreshold` | number | No | Change % threshold for notifications (default: `10`) |
@@ -962,8 +973,8 @@ features:
962
973
  | `features` | object | No | Feature flags (boolean values) |
963
974
  | `ai.enabled` | boolean | No | Enable AI-powered documentation |
964
975
  | `ai.mode` | string | No | AI mode: `hybrid`, `full`, or `off` |
965
- | `ai.temperature` | number | No | Generation temperature (0\u20132) |
966
- | `ai.max_tokens` | number | No | Max tokens per request (>0) |
976
+ | `ai.temperature` | number | No | Generation temperature (0–2). Not supported by all models (e.g. gpt-5-mini ignores it) |
977
+ | `ai.max_tokens` | number | No | Max completion tokens per request (>0) |
967
978
 
968
979
  ---
969
980
 
@@ -993,6 +1004,13 @@ Optional for Discord notifications:
993
1004
  |----------|----------|-------------|
994
1005
  | `DISCORD_WEBHOOK_URL` | No | Discord webhook URL for team notifications |
995
1006
 
1007
+ Required for GitHub Wiki publisher:
1008
+
1009
+ | Variable | Required | Description |
1010
+ |----------|----------|-------------|
1011
+ | `GITHUB_TOKEN` | Yes | Personal access token or Actions `${{ secrets.GITHUB_TOKEN }}` |
1012
+ | `GITHUB_REPOSITORY` | No | `owner/repo` (auto-detected from git remote in Actions) |
1013
+
996
1014
  **Local Development:** Create `.env` file in project root
997
1015
  **GitHub Actions:** Add as repository secrets in Settings → Secrets and variables → Actions
998
1016
 
@@ -1077,7 +1095,7 @@ npm test
1077
1095
  - Integration workflows
1078
1096
  - Doctor command validation
1079
1097
 
1080
- **Coverage:** 163 tests passing across 14 test files
1098
+ **Coverage:** 180 tests passing across 15 test files
1081
1099
 
1082
1100
  ### Test Package Installation Locally
1083
1101
 
@@ -1088,7 +1106,7 @@ Simulates the full user installation experience:
1088
1106
  npm pack
1089
1107
 
1090
1108
  # Install globally from tarball
1091
- npm install -g chappibunny-repolens-1.0.0.tgz
1109
+ npm install -g chappibunny-repolens-1.2.0.tgz
1092
1110
 
1093
1111
  # Verify
1094
1112
  repolens --version
@@ -1137,6 +1155,7 @@ repolens/
1137
1155
  │ │ ├── publish.js # Publishing pipeline
1138
1156
  │ │ ├── notion.js # Notion API integration
1139
1157
  │ │ ├── confluence.js # Confluence REST API integration
1158
+ │ │ ├── github-wiki.js # GitHub Wiki publisher (git-based)
1140
1159
  │ │ └── markdown.js # Local Markdown generation
1141
1160
  │ ├── integrations/
1142
1161
  │ │ └── discord.js # Discord webhook notifications
@@ -1156,7 +1175,7 @@ repolens/
1156
1175
  │ ├── telemetry.js # Opt-in error tracking + performance timers
1157
1176
  │ ├── errors.js # Enhanced error messages with guidance
1158
1177
  │ └── update-check.js # Version update notifications
1159
- ├── tests/ # Vitest test suite (163 tests across 14 files)
1178
+ ├── tests/ # Vitest test suite (180 tests across 15 files)
1160
1179
  ├── .repolens.yml # Dogfooding config
1161
1180
  ├── package.json
1162
1181
  ├── CHANGELOG.md
@@ -1211,14 +1230,14 @@ See [RELEASE.md](./RELEASE.md) for detailed workflow.
1211
1230
 
1212
1231
  ## 🗺️ Roadmap
1213
1232
 
1214
- **Current Status:** v1.0.0 — Stable Release
1233
+ **Current Status:** v1.2.0 — Stable Release
1215
1234
 
1216
1235
  ### v1.0 — Complete ✅
1217
1236
 
1218
1237
  - [x] CLI commands: `init`, `doctor`, `publish`, `migrate`, `watch`, `feedback`, `version`, `help`
1219
1238
  - [x] Config schema v1 with validation (frozen)
1220
1239
  - [x] Auto-discovery of `.repolens.yml`
1221
- - [x] Publishers: Notion + Confluence + Markdown
1240
+ - [x] Publishers: Notion + Confluence + GitHub Wiki + Markdown
1222
1241
  - [x] Branch-aware publishing with filtering
1223
1242
  - [x] Smart tech stack detection from package.json
1224
1243
  - [x] Unicode dependency diagrams (no external deps)
@@ -1227,7 +1246,7 @@ See [RELEASE.md](./RELEASE.md) for detailed workflow.
1227
1246
  - [x] GitHub Actions automation (publish + release)
1228
1247
  - [x] PR architecture diff comments
1229
1248
  - [x] Performance guardrails (10k warning, 50k limit)
1230
- - [x] Comprehensive test suite (163 tests across 14 files)
1249
+ - [x] Comprehensive test suite (180 tests across 15 files)
1231
1250
  - [x] Security hardening (secret detection, injection prevention, fuzzing)
1232
1251
  - [x] Discord notifications with rich embeds
1233
1252
  - [x] Documentation coverage & health scoring
@@ -1250,7 +1269,7 @@ See [RELEASE.md](./RELEASE.md) for detailed workflow.
1250
1269
 
1251
1270
  ### Future
1252
1271
 
1253
- - [ ] Additional publishers (GitHub Wiki, Obsidian)
1272
+ - [ ] Additional publishers (Obsidian)
1254
1273
  - [ ] VS Code extension
1255
1274
  - [ ] GitHub App
1256
1275
 
@@ -1266,6 +1285,8 @@ MIT
1266
1285
 
1267
1286
  ## 💬 Support & Contact
1268
1287
 
1288
+ - **Troubleshooting**: [TROUBLESHOOTING.md](TROUBLESHOOTING.md) — installation, config, publishing, AI, and CI/CD issues
1289
+ - **Diagnostics**: Run `npx @chappibunny/repolens doctor` to validate your setup
1269
1290
  - **Issues**: [GitHub Issues](https://github.com/CHAPIBUNNY/repolens/issues)
1270
1291
  - **Discussions**: [GitHub Discussions](https://github.com/CHAPIBUNNY/repolens/discussions)
1271
1292
  - **Email**: Contact repository maintainers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,6 @@ export async function generateExecutiveSummary(context) {
25
25
  const result = await generateText({
26
26
  system: SYSTEM_PROMPT,
27
27
  user: createExecutiveSummaryPrompt(context),
28
- temperature: 0.2,
29
28
  maxTokens: 1500
30
29
  });
31
30
 
@@ -47,7 +46,6 @@ export async function generateSystemOverview(context) {
47
46
  const result = await generateText({
48
47
  system: SYSTEM_PROMPT,
49
48
  user: createSystemOverviewPrompt(context),
50
- temperature: 0.2,
51
49
  maxTokens: 1200
52
50
  });
53
51
 
@@ -68,7 +66,6 @@ export async function generateBusinessDomains(context) {
68
66
  const result = await generateText({
69
67
  system: SYSTEM_PROMPT,
70
68
  user: createBusinessDomainsPrompt(context),
71
- temperature: 0.2,
72
69
  maxTokens: 2000
73
70
  });
74
71
 
@@ -89,7 +86,6 @@ export async function generateArchitectureOverview(context) {
89
86
  const result = await generateText({
90
87
  system: SYSTEM_PROMPT,
91
88
  user: createArchitectureOverviewPrompt(context),
92
- temperature: 0.2,
93
89
  maxTokens: 1800
94
90
  });
95
91
 
@@ -110,7 +106,6 @@ export async function generateDataFlows(flows, context) {
110
106
  const result = await generateText({
111
107
  system: SYSTEM_PROMPT,
112
108
  user: createDataFlowsPrompt(flows, context),
113
- temperature: 0.2,
114
109
  maxTokens: 1800
115
110
  });
116
111
 
@@ -131,7 +126,6 @@ export async function generateDeveloperOnboarding(context) {
131
126
  const result = await generateText({
132
127
  system: SYSTEM_PROMPT,
133
128
  user: createDeveloperOnboardingPrompt(context),
134
- temperature: 0.2,
135
129
  maxTokens: 2200
136
130
  });
137
131
 
@@ -4,7 +4,6 @@ import { warn, info } from "../utils/logger.js";
4
4
  import { executeAIRequest } from "../utils/rate-limit.js";
5
5
 
6
6
  const DEFAULT_TIMEOUT_MS = 60000;
7
- const DEFAULT_TEMPERATURE = 0.2;
8
7
  const DEFAULT_MAX_TOKENS = 2500;
9
8
 
10
9
  export async function generateText({ system, user, temperature, maxTokens, config }) {
@@ -27,8 +26,8 @@ export async function generateText({ system, user, temperature, maxTokens, confi
27
26
  const model = process.env.REPOLENS_AI_MODEL || "gpt-5-mini";
28
27
  const timeoutMs = parseInt(process.env.REPOLENS_AI_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
29
28
 
30
- // Use config values as fallback for temperature/maxTokens
31
- const resolvedTemp = temperature ?? aiConfig.temperature ?? DEFAULT_TEMPERATURE;
29
+ // Use config values as fallback for maxTokens; temperature only when explicitly set
30
+ const resolvedTemp = temperature ?? aiConfig.temperature ?? undefined;
32
31
  const resolvedMaxTokens = maxTokens ?? aiConfig.max_tokens ?? DEFAULT_MAX_TOKENS;
33
32
 
34
33
  // Validate configuration
@@ -81,21 +80,28 @@ async function callOpenAICompatibleAPI({ baseUrl, apiKey, model, system, user, t
81
80
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
82
81
 
83
82
  try {
83
+ // Build request body — omit temperature if not supported by model
84
+ const body = {
85
+ model,
86
+ messages: [
87
+ { role: "system", content: system },
88
+ { role: "user", content: user }
89
+ ],
90
+ max_completion_tokens: maxTokens
91
+ };
92
+ // Only send temperature when explicitly configured — some models
93
+ // (e.g. gpt-5-mini) reject any non-default value
94
+ if (temperature != null) {
95
+ body.temperature = temperature;
96
+ }
97
+
84
98
  const response = await fetch(url, {
85
99
  method: "POST",
86
100
  headers: {
87
101
  "Content-Type": "application/json",
88
102
  "Authorization": `Bearer ${apiKey}`
89
103
  },
90
- body: JSON.stringify({
91
- model,
92
- messages: [
93
- { role: "system", content: system },
94
- { role: "user", content: user }
95
- ],
96
- temperature,
97
- max_tokens: maxTokens
98
- }),
104
+ body: JSON.stringify(body),
99
105
  signal: controller.signal
100
106
  });
101
107
 
@@ -136,7 +142,7 @@ export function getAIConfig() {
136
142
  provider: process.env.REPOLENS_AI_PROVIDER || "openai_compatible",
137
143
  model: process.env.REPOLENS_AI_MODEL || "gpt-5-mini",
138
144
  hasApiKey: !!process.env.REPOLENS_AI_API_KEY,
139
- temperature: parseFloat(process.env.REPOLENS_AI_TEMPERATURE || DEFAULT_TEMPERATURE),
145
+ temperature: process.env.REPOLENS_AI_TEMPERATURE ? parseFloat(process.env.REPOLENS_AI_TEMPERATURE) : undefined,
140
146
  maxTokens: parseInt(process.env.REPOLENS_AI_MAX_TOKENS || DEFAULT_MAX_TOKENS)
141
147
  };
142
148
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  const CURRENT_SCHEMA_VERSION = 1;
11
- const SUPPORTED_PUBLISHERS = ["notion", "markdown", "confluence"];
11
+ const SUPPORTED_PUBLISHERS = ["notion", "markdown", "confluence", "github_wiki"];
12
12
  const SUPPORTED_PAGE_KEYS = [
13
13
  "system_overview",
14
14
  "module_catalog",
@@ -193,6 +193,31 @@ export function validateConfig(config) {
193
193
  }
194
194
  }
195
195
 
196
+ // Validate GitHub Wiki configuration (optional)
197
+ if (config.github_wiki !== undefined) {
198
+ if (typeof config.github_wiki !== "object" || Array.isArray(config.github_wiki)) {
199
+ errors.push("github_wiki must be an object");
200
+ } else {
201
+ if (config.github_wiki.branches !== undefined) {
202
+ if (!Array.isArray(config.github_wiki.branches)) {
203
+ errors.push("github_wiki.branches must be an array");
204
+ } else {
205
+ config.github_wiki.branches.forEach((branch, idx) => {
206
+ if (typeof branch !== "string") {
207
+ errors.push(`github_wiki.branches[${idx}] must be a string`);
208
+ }
209
+ });
210
+ }
211
+ }
212
+ if (config.github_wiki.sidebar !== undefined && typeof config.github_wiki.sidebar !== "boolean") {
213
+ errors.push("github_wiki.sidebar must be a boolean");
214
+ }
215
+ if (config.github_wiki.footer !== undefined && typeof config.github_wiki.footer !== "boolean") {
216
+ errors.push("github_wiki.footer must be a boolean");
217
+ }
218
+ }
219
+ }
220
+
196
221
  // Validate Discord configuration (optional)
197
222
  if (config.discord !== undefined) {
198
223
  if (typeof config.discord !== "object" || Array.isArray(config.discord)) {
package/src/init.js CHANGED
@@ -105,7 +105,6 @@ CONFLUENCE_PARENT_PAGE_ID=
105
105
  # REPOLENS_AI_API_KEY=sk-...
106
106
  # REPOLENS_AI_BASE_URL=https://api.openai.com/v1
107
107
  # REPOLENS_AI_MODEL=gpt-5-mini
108
- # REPOLENS_AI_TEMPERATURE=0.3
109
108
  # REPOLENS_AI_MAX_TOKENS=2000
110
109
  `;
111
110
 
@@ -206,7 +205,6 @@ AI features add natural language explanations for non-technical stakeholders.
206
205
  ai:
207
206
  enabled: true
208
207
  mode: hybrid
209
- temperature: 0.3
210
208
 
211
209
  features:
212
210
  executive_summary: true
@@ -607,7 +605,6 @@ function buildWizardConfig(answers) {
607
605
  lines.push(`ai:`);
608
606
  lines.push(` enabled: true`);
609
607
  lines.push(` mode: hybrid`);
610
- lines.push(` temperature: 0.3`);
611
608
  lines.push(``);
612
609
  lines.push(`features:`);
613
610
  lines.push(` executive_summary: true`);
package/src/migrate.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import yaml from "js-yaml";
3
4
  import { info, warn, error as logError } from "./utils/logger.js";
4
5
  import { trackMigration } from "./utils/telemetry.js";
5
6
 
@@ -105,6 +106,78 @@ $2$3`
105
106
  return migrated;
106
107
  }
107
108
 
109
+ /**
110
+ * Find .repolens.yml config file in the target directory
111
+ */
112
+ async function findConfigFile(targetDir) {
113
+ const candidates = [".repolens.yml", ".repolens.yaml"];
114
+ for (const name of candidates) {
115
+ const filePath = path.join(targetDir, name);
116
+ try {
117
+ await fs.access(filePath);
118
+ return filePath;
119
+ } catch {
120
+ // not found, try next
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Migrate .repolens.yml config file — add configVersion: 1 if missing
128
+ * Returns { migrated: boolean, filePath: string|null }
129
+ */
130
+ async function migrateConfigFile(targetDir, options = {}) {
131
+ const { dryRun = false } = options;
132
+
133
+ const configPath = await findConfigFile(targetDir);
134
+ if (!configPath) {
135
+ return { migrated: false, filePath: null, reason: "no-config" };
136
+ }
137
+
138
+ const filename = path.basename(configPath);
139
+ console.log(`\n📄 Checking config: ${filename}`);
140
+
141
+ const raw = await fs.readFile(configPath, "utf8");
142
+ let parsed;
143
+ try {
144
+ parsed = yaml.load(raw);
145
+ } catch {
146
+ warn(` ⚠️ Could not parse ${filename} — skipping config migration`);
147
+ return { migrated: false, filePath: configPath, reason: "parse-error" };
148
+ }
149
+
150
+ if (!parsed || typeof parsed !== "object") {
151
+ warn(` ⚠️ ${filename} is empty or not a mapping — skipping config migration`);
152
+ return { migrated: false, filePath: configPath, reason: "invalid-structure" };
153
+ }
154
+
155
+ if (parsed.configVersion != null) {
156
+ info(" ✅ configVersion already present");
157
+ return { migrated: false, filePath: configPath, reason: "already-present" };
158
+ }
159
+
160
+ info(" 🔧 Missing configVersion — adding configVersion: 1");
161
+
162
+ // Prepend configVersion: 1 to preserve existing formatting
163
+ const migratedContent = `configVersion: 1\n${raw}`;
164
+
165
+ if (dryRun) {
166
+ info(" 🔍 DRY RUN: No changes written");
167
+ return { migrated: true, filePath: configPath, reason: "dry-run" };
168
+ }
169
+
170
+ // Backup original
171
+ const backupPath = `${configPath}.backup`;
172
+ await fs.writeFile(backupPath, raw, "utf8");
173
+ info(` 💾 Backup saved: ${path.basename(backupPath)}`);
174
+
175
+ await fs.writeFile(configPath, migratedContent, "utf8");
176
+ info(` ✅ Migrated: ${filename}`);
177
+
178
+ return { migrated: true, filePath: configPath, reason: "migrated" };
179
+ }
180
+
108
181
  /**
109
182
  * Find all workflow files in .github/workflows
110
183
  */
@@ -170,18 +243,17 @@ export async function runMigrate(targetDir = process.cwd(), options = {}) {
170
243
 
171
244
  const workflowFiles = await findWorkflowFiles(targetDir);
172
245
 
246
+ let migratedCount = 0;
247
+ let skippedCount = 0;
248
+
173
249
  if (workflowFiles.length === 0) {
174
250
  warn("⚠️ No workflow files found in .github/workflows/");
175
251
  info("\n💡 If you haven't set up GitHub Actions yet:");
176
252
  info(" Run: repolens init");
177
- return;
253
+ } else {
254
+ info(` Found ${workflowFiles.length} workflow file(s)`);
178
255
  }
179
256
 
180
- info(` Found ${workflowFiles.length} workflow file(s)`);
181
-
182
- let migratedCount = 0;
183
- let skippedCount = 0;
184
-
185
257
  for (const workflowPath of workflowFiles) {
186
258
  const filename = path.basename(workflowPath);
187
259
  console.log(`\n📄 Checking: ${filename}`);
@@ -221,6 +293,16 @@ export async function runMigrate(targetDir = process.cwd(), options = {}) {
221
293
  migratedCount++;
222
294
  }
223
295
 
296
+ // === Config File Migration ===
297
+ const configResult = await migrateConfigFile(targetDir, { dryRun });
298
+ let configMigrated = false;
299
+ if (configResult.migrated) {
300
+ migratedCount++;
301
+ configMigrated = true;
302
+ } else if (configResult.reason === "no-config") {
303
+ info("\n📄 No .repolens.yml found — skipping config migration");
304
+ }
305
+
224
306
  // Summary
225
307
  console.log("\n" + "─".repeat(60));
226
308
  console.log("📊 Migration Summary:");
@@ -248,7 +330,7 @@ export async function runMigrate(targetDir = process.cwd(), options = {}) {
248
330
  trackMigration(migratedCount, skippedCount);
249
331
 
250
332
  // Return results for telemetry tracking
251
- return { migratedCount, skippedCount };
333
+ return { migratedCount, skippedCount, configMigrated };
252
334
 
253
335
  } catch (err) {
254
336
  logError(`Migration failed: ${err.message}`);
@@ -0,0 +1,461 @@
1
+ // GitHub Wiki Publisher for RepoLens
2
+ //
3
+ // Publishes rendered documentation to a GitHub repository's wiki.
4
+ // The wiki is a separate git repo at {repo-url}.wiki.git.
5
+ //
6
+ // Environment Variables:
7
+ // - GITHUB_TOKEN: Personal access token or Actions token (required)
8
+ // - GITHUB_REPOSITORY: owner/repo (auto-set in Actions, or detected from git remote)
9
+ //
10
+ // Config (.repolens.yml):
11
+ // github_wiki:
12
+ // branches: [main] # Optional branch filter
13
+ // sidebar: true # Generate _Sidebar.md (default: true)
14
+ // footer: true # Generate _Footer.md (default: true)
15
+
16
+ import { execSync } from "node:child_process";
17
+ import fs from "node:fs/promises";
18
+ import path from "node:path";
19
+ import os from "node:os";
20
+ import { info, warn } from "../utils/logger.js";
21
+ import { getCurrentBranch, getBranchQualifiedTitle, normalizeBranchName } from "../utils/branch.js";
22
+
23
+ const PAGE_ORDER = [
24
+ "executive_summary",
25
+ "system_overview",
26
+ "business_domains",
27
+ "architecture_overview",
28
+ "module_catalog",
29
+ "route_map",
30
+ "api_surface",
31
+ "data_flows",
32
+ "change_impact",
33
+ "system_map",
34
+ "developer_onboarding",
35
+ "graphql_schema",
36
+ "type_graph",
37
+ "dependency_graph",
38
+ "architecture_drift",
39
+ "arch_diff",
40
+ ];
41
+
42
+ const PAGE_TITLES = {
43
+ executive_summary: "Executive Summary",
44
+ system_overview: "System Overview",
45
+ business_domains: "Business Domains",
46
+ architecture_overview: "Architecture Overview",
47
+ module_catalog: "Module Catalog",
48
+ route_map: "Route Map",
49
+ api_surface: "API Surface",
50
+ data_flows: "Data Flows",
51
+ change_impact: "Change Impact",
52
+ system_map: "System Map",
53
+ developer_onboarding: "Developer Onboarding",
54
+ graphql_schema: "GraphQL Schema",
55
+ type_graph: "Type Graph",
56
+ dependency_graph: "Dependency Graph",
57
+ architecture_drift: "Architecture Drift",
58
+ arch_diff: "Architecture Diff",
59
+ };
60
+
61
+ const PAGE_DESCRIPTIONS = {
62
+ executive_summary: "High-level project summary for non-technical readers.",
63
+ system_overview: "Snapshot of stack, scale, structure, and detected capabilities.",
64
+ business_domains: "Functional areas inferred from the repository and business logic.",
65
+ architecture_overview: "Layered technical view of the system and its major components.",
66
+ module_catalog: "Structured inventory of modules, directories, and responsibilities.",
67
+ route_map: "Frontend routes and page structure detected across the application.",
68
+ api_surface: "Detected endpoints, handlers, methods, and API structure.",
69
+ data_flows: "How information moves through the system and its major pathways.",
70
+ change_impact: "Contextual architecture changes and likely downstream effects.",
71
+ system_map: "Visual map of system relationships and dependencies.",
72
+ developer_onboarding: "What a new engineer needs to understand the repository quickly.",
73
+ graphql_schema: "GraphQL types, operations, and schema structure.",
74
+ type_graph: "Type-level relationships and structural coupling across the codebase.",
75
+ dependency_graph: "Module and package dependency relationships.",
76
+ architecture_drift: "Detected drift between intended and current architecture patterns.",
77
+ arch_diff: "Architecture-level diff across branches or revisions.",
78
+ };
79
+
80
+ // Audience-based grouping for Home page
81
+ const AUDIENCE_GROUPS = [
82
+ {
83
+ title: "For Stakeholders",
84
+ emoji: "📊",
85
+ keys: ["executive_summary", "business_domains", "data_flows"],
86
+ },
87
+ {
88
+ title: "For Engineers",
89
+ emoji: "🔧",
90
+ keys: [
91
+ "architecture_overview", "module_catalog", "api_surface",
92
+ "route_map", "system_map", "graphql_schema", "type_graph",
93
+ "dependency_graph", "architecture_drift",
94
+ ],
95
+ },
96
+ {
97
+ title: "For New Contributors",
98
+ emoji: "🚀",
99
+ keys: ["developer_onboarding", "system_overview"],
100
+ },
101
+ {
102
+ title: "Change Tracking",
103
+ emoji: "📋",
104
+ keys: ["change_impact", "arch_diff"],
105
+ },
106
+ ];
107
+
108
+ // Sidebar grouping (compact navigation)
109
+ const SIDEBAR_GROUPS = [
110
+ {
111
+ title: "Overview",
112
+ keys: [
113
+ "executive_summary", "system_overview", "business_domains",
114
+ "data_flows", "developer_onboarding",
115
+ ],
116
+ },
117
+ {
118
+ title: "Architecture",
119
+ keys: [
120
+ "architecture_overview", "module_catalog", "route_map",
121
+ "api_surface", "system_map", "graphql_schema", "type_graph",
122
+ "dependency_graph", "architecture_drift", "arch_diff", "change_impact",
123
+ ],
124
+ },
125
+ ];
126
+
127
+ // Audience labels for page metadata headers
128
+ const PAGE_AUDIENCE = {
129
+ executive_summary: "Stakeholders · Leadership",
130
+ system_overview: "All Audiences",
131
+ business_domains: "Stakeholders · Product",
132
+ architecture_overview: "Engineers · Tech Leads",
133
+ module_catalog: "Engineers",
134
+ route_map: "Engineers",
135
+ api_surface: "Engineers",
136
+ data_flows: "Stakeholders · Engineers",
137
+ change_impact: "Engineers · Tech Leads",
138
+ system_map: "All Audiences",
139
+ developer_onboarding: "New Contributors",
140
+ graphql_schema: "Engineers",
141
+ type_graph: "Engineers",
142
+ dependency_graph: "Engineers",
143
+ architecture_drift: "Engineers · Tech Leads",
144
+ arch_diff: "Engineers · Tech Leads",
145
+ };
146
+
147
+ /**
148
+ * Get the display title for a page key.
149
+ */
150
+ function getPageDisplayTitle(key) {
151
+ return PAGE_TITLES[key] || key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
152
+ }
153
+
154
+ /**
155
+ * Get custom page keys not in the standard order.
156
+ */
157
+ function getCustomPageKeys(pageKeys) {
158
+ return pageKeys.filter((key) => !PAGE_ORDER.includes(key));
159
+ }
160
+
161
+ /**
162
+ * Detect the GitHub repository (owner/repo) from environment or git remote.
163
+ */
164
+ function detectGitHubRepo() {
165
+ if (process.env.GITHUB_REPOSITORY) {
166
+ return process.env.GITHUB_REPOSITORY;
167
+ }
168
+
169
+ try {
170
+ const remoteUrl = execSync("git remote get-url origin", {
171
+ encoding: "utf8",
172
+ stdio: ["pipe", "pipe", "ignore"],
173
+ }).trim();
174
+
175
+ const httpsMatch = remoteUrl.match(/github\.com\/([^/]+\/[^/.]+)/);
176
+ if (httpsMatch) return httpsMatch[1];
177
+
178
+ const sshMatch = remoteUrl.match(/github\.com:([^/]+\/[^/.]+)/);
179
+ if (sshMatch) return sshMatch[1];
180
+ } catch {
181
+ // git command failed
182
+ }
183
+
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Build the authenticated wiki clone URL.
189
+ */
190
+ function buildWikiCloneUrl(repo, token) {
191
+ return `https://x-access-token:${token}@github.com/${repo}.wiki.git`;
192
+ }
193
+
194
+ /**
195
+ * Convert a page key to a wiki-safe filename.
196
+ */
197
+ function pageFileName(key) {
198
+ const title = getPageDisplayTitle(key);
199
+ return title.replace(/\s+/g, "-") + ".md";
200
+ }
201
+
202
+ /**
203
+ * Build a wiki link for a page key.
204
+ */
205
+ function wikiLink(key) {
206
+ const display = getPageDisplayTitle(key);
207
+ const slug = pageFileName(key).replace(/\.md$/, "");
208
+ return `[[${display}|${slug}]]`;
209
+ }
210
+
211
+ /**
212
+ * Generate the Home.md page — audience-grouped with descriptions and status.
213
+ */
214
+ function generateHome(pageKeys, projectName, branch, repo) {
215
+ const title = getBranchQualifiedTitle(projectName + " Documentation", branch);
216
+ const lines = [
217
+ `# ${title}`,
218
+ "",
219
+ `> Architecture documentation for **${projectName}**, auto-generated by [RepoLens](https://github.com/CHAPIBUNNY/repolens).`,
220
+ "",
221
+ "## Status",
222
+ "",
223
+ `| | |`,
224
+ `|---|---|`,
225
+ `| **Project** | ${projectName} |`,
226
+ `| **Branch** | \`${branch}\` |`,
227
+ `| **Pages** | ${pageKeys.length} |`,
228
+ `| **Publisher** | GitHub Wiki |`,
229
+ `| **Source** | [RepoLens](https://github.com/CHAPIBUNNY/repolens) |`,
230
+ "",
231
+ "---",
232
+ "",
233
+ ];
234
+
235
+ // Audience-grouped sections
236
+ for (const group of AUDIENCE_GROUPS) {
237
+ const activeKeys = group.keys.filter((k) => pageKeys.includes(k));
238
+ if (activeKeys.length === 0) continue;
239
+
240
+ lines.push(`## ${group.emoji} ${group.title}`, "");
241
+ for (const key of activeKeys) {
242
+ const desc = PAGE_DESCRIPTIONS[key] || "";
243
+ lines.push(`- ${wikiLink(key)}${desc ? " — " + desc : ""}`);
244
+ }
245
+ lines.push("");
246
+ }
247
+
248
+ // Custom / plugin pages
249
+ const customKeys = getCustomPageKeys(pageKeys);
250
+ if (customKeys.length > 0) {
251
+ lines.push("## 🧩 Custom Pages", "");
252
+ for (const key of customKeys.sort()) {
253
+ const desc = PAGE_DESCRIPTIONS[key] || "";
254
+ lines.push(`- ${wikiLink(key)}${desc ? " — " + desc : ""}`);
255
+ }
256
+ lines.push("");
257
+ }
258
+
259
+ // Recommended reading order
260
+ lines.push(
261
+ "---",
262
+ "",
263
+ "## 📖 Recommended Reading Order",
264
+ "",
265
+ "1. [[Executive Summary|Executive-Summary]] — Start here for a quick overview",
266
+ "2. [[System Overview|System-Overview]] — Understand the stack and scale",
267
+ "3. [[Architecture Overview|Architecture-Overview]] — Deep dive into system design",
268
+ "4. [[Developer Onboarding|Developer-Onboarding]] — Get started contributing",
269
+ "",
270
+ "---",
271
+ "",
272
+ `*This wiki is auto-generated. Manual edits will be overwritten on the next publish.*`,
273
+ );
274
+
275
+ return lines.join("\n");
276
+ }
277
+
278
+ /**
279
+ * Generate _Sidebar.md — grouped navigation.
280
+ */
281
+ function generateSidebar(pageKeys, projectName, branch) {
282
+ const title = getBranchQualifiedTitle(projectName, branch);
283
+ const lines = [`### ${title}`, "", "[[Home]]", ""];
284
+
285
+ for (const group of SIDEBAR_GROUPS) {
286
+ const activeKeys = group.keys.filter((k) => pageKeys.includes(k));
287
+ if (activeKeys.length === 0) continue;
288
+
289
+ lines.push(`**${group.title}**`, "");
290
+ for (const key of activeKeys) {
291
+ lines.push(`- ${wikiLink(key)}`);
292
+ }
293
+ lines.push("");
294
+ }
295
+
296
+ // Custom / plugin pages
297
+ const customKeys = getCustomPageKeys(pageKeys);
298
+ if (customKeys.length > 0) {
299
+ lines.push("**Custom Pages**", "");
300
+ for (const key of customKeys.sort()) {
301
+ lines.push(`- ${wikiLink(key)}`);
302
+ }
303
+ lines.push("");
304
+ }
305
+
306
+ lines.push("---", `*[RepoLens](https://github.com/CHAPIBUNNY/repolens)*`);
307
+ return lines.join("\n");
308
+ }
309
+
310
+ /**
311
+ * Generate _Footer.md.
312
+ */
313
+ function generateFooter(branch) {
314
+ const branchNote = branch !== "main" && branch !== "master"
315
+ ? ` · Branch: \`${branch}\``
316
+ : "";
317
+ return [
318
+ "---",
319
+ `📚 Generated by [RepoLens](https://github.com/CHAPIBUNNY/repolens)${branchNote} · [← Home](Home)`,
320
+ ].join("\n");
321
+ }
322
+
323
+ /**
324
+ * Build a metadata header to prepend to each page.
325
+ */
326
+ function pageHeader(key, branch) {
327
+ const audience = PAGE_AUDIENCE[key] || "All Audiences";
328
+ return [
329
+ `[← Home](Home)`,
330
+ "",
331
+ `> **Audience:** ${audience} · **Branch:** \`${branch}\` · **Generated by** [RepoLens](https://github.com/CHAPIBUNNY/repolens)`,
332
+ "",
333
+ "---",
334
+ "",
335
+ ].join("\n");
336
+ }
337
+
338
+ /**
339
+ * Run a git command in the specified directory.
340
+ * Token is never exposed in error output.
341
+ */
342
+ function git(args, cwd) {
343
+ try {
344
+ return execSync(`git ${args}`, {
345
+ cwd,
346
+ encoding: "utf8",
347
+ stdio: ["pipe", "pipe", "pipe"],
348
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
349
+ });
350
+ } catch (err) {
351
+ const sanitized = (err.stderr || err.message || "").replace(
352
+ /x-access-token:[^\s@]+/g,
353
+ "x-access-token:***"
354
+ );
355
+ throw new Error(`Git command failed: git ${args.split(" ")[0]} — ${sanitized}`);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Publish rendered pages to GitHub Wiki.
361
+ */
362
+ export async function publishToGitHubWiki(cfg, renderedPages) {
363
+ const token = process.env.GITHUB_TOKEN;
364
+ if (!token) {
365
+ throw new Error(
366
+ "Missing GITHUB_TOKEN. Required for GitHub Wiki publishing.\n" +
367
+ "In GitHub Actions, this is available as ${{ secrets.GITHUB_TOKEN }}.\n" +
368
+ "Locally, create a PAT with repo scope: https://github.com/settings/tokens"
369
+ );
370
+ }
371
+
372
+ const repo = detectGitHubRepo();
373
+ if (!repo) {
374
+ throw new Error(
375
+ "Could not detect GitHub repository. Set GITHUB_REPOSITORY=owner/repo\n" +
376
+ "or ensure git remote 'origin' points to a GitHub URL."
377
+ );
378
+ }
379
+
380
+ const branch = getCurrentBranch();
381
+ const wikiConfig = cfg.github_wiki || {};
382
+ const includeSidebar = wikiConfig.sidebar !== false;
383
+ const includeFooter = wikiConfig.footer !== false;
384
+ const projectName = cfg.project?.name || repo.split("/")[1] || "RepoLens";
385
+
386
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "repolens-wiki-"));
387
+
388
+ try {
389
+ const cloneUrl = buildWikiCloneUrl(repo, token);
390
+
391
+ try {
392
+ git(`clone --depth 1 ${cloneUrl} .`, tmpDir);
393
+ } catch {
394
+ warn("Wiki repository not found — initializing. Enable the wiki tab in GitHub repo settings.");
395
+ // GitHub Wiki uses 'master' as default branch; ensure compatibility
396
+ // regardless of the local git init.defaultBranch setting
397
+ git("init -b master", tmpDir);
398
+ git(`remote add origin ${cloneUrl}`, tmpDir);
399
+ }
400
+
401
+ // Only include pages that have actual content
402
+ const populatedPages = Object.entries(renderedPages).filter(
403
+ ([, markdown]) => markdown && markdown.trim().length > 0
404
+ );
405
+ const pageKeys = populatedPages.map(([key]) => key);
406
+
407
+ // Write Home.md
408
+ const home = generateHome(pageKeys, projectName, branch, repo);
409
+ await fs.writeFile(path.join(tmpDir, "Home.md"), home, "utf8");
410
+
411
+ // Write each rendered page with metadata header
412
+ for (const [key, markdown] of populatedPages) {
413
+ const fileName = pageFileName(key);
414
+ const header = pageHeader(key, branch);
415
+ await fs.writeFile(path.join(tmpDir, fileName), header + markdown, "utf8");
416
+ }
417
+
418
+ // Generate sidebar
419
+ if (includeSidebar) {
420
+ const sidebar = generateSidebar(pageKeys, projectName, branch);
421
+ await fs.writeFile(path.join(tmpDir, "_Sidebar.md"), sidebar, "utf8");
422
+ }
423
+
424
+ // Generate footer
425
+ if (includeFooter) {
426
+ const footer = generateFooter(branch);
427
+ await fs.writeFile(path.join(tmpDir, "_Footer.md"), footer, "utf8");
428
+ }
429
+
430
+ // Stage, commit, push
431
+ git("config user.name \"RepoLens Bot\"", tmpDir);
432
+ git("config user.email \"repolens@users.noreply.github.com\"", tmpDir);
433
+ git("add -A", tmpDir);
434
+
435
+ try {
436
+ execSync("git diff --cached --quiet", { cwd: tmpDir, stdio: "pipe" });
437
+ info("GitHub Wiki is already up to date — no changes to push.");
438
+ return;
439
+ } catch {
440
+ // There are staged changes — continue to commit
441
+ }
442
+
443
+ const branchLabel = branch !== "main" && branch !== "master" ? ` [${branch}]` : "";
444
+ const commitMsg = `docs: update RepoLens documentation${branchLabel}`;
445
+ git(`commit -m "${commitMsg}"`, tmpDir);
446
+
447
+ // GitHub Wiki serves from 'master'; push explicitly to master
448
+ git("push origin HEAD:refs/heads/master", tmpDir);
449
+ info(`GitHub Wiki published (${pageKeys.length} pages): https://github.com/${repo}/wiki`);
450
+
451
+ } finally {
452
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Check if GitHub Wiki publishing secrets are available.
458
+ */
459
+ export function hasGitHubWikiSecrets() {
460
+ return !!process.env.GITHUB_TOKEN;
461
+ }
@@ -1,7 +1,8 @@
1
1
  import { publishToNotion } from "./publish.js";
2
2
  import { publishToMarkdown } from "./markdown.js";
3
3
  import { publishToConfluence, hasConfluenceSecrets } from "./confluence.js";
4
- import { shouldPublishToNotion, shouldPublishToConfluence, getCurrentBranch } from "../utils/branch.js";
4
+ import { publishToGitHubWiki, hasGitHubWikiSecrets } from "./github-wiki.js";
5
+ import { shouldPublishToNotion, shouldPublishToConfluence, shouldPublishToGitHubWiki, getCurrentBranch } from "../utils/branch.js";
5
6
  import { info, warn } from "../utils/logger.js";
6
7
  import { trackPublishing } from "../utils/telemetry.js";
7
8
  import { collectMetrics } from "../utils/metrics.js";
@@ -80,6 +81,27 @@ export async function publishDocs(cfg, renderedPages, scanResult, pluginManager
80
81
  }
81
82
  }
82
83
 
84
+ // GitHub Wiki publishing (opt-in if secrets configured)
85
+ if (publishers.includes("github_wiki") || hasGitHubWikiSecrets() && publishers.includes("github_wiki")) {
86
+ if (!hasGitHubWikiSecrets()) {
87
+ info("Skipping GitHub Wiki publish: GITHUB_TOKEN not configured");
88
+ info("In GitHub Actions, add GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} to your env");
89
+ } else if (shouldPublishToGitHubWiki(cfg, currentBranch)) {
90
+ info(`Publishing to GitHub Wiki from branch: ${currentBranch}`);
91
+ try {
92
+ await publishToGitHubWiki(cfg, renderedPages);
93
+ publishedTo.push("github_wiki");
94
+ } catch (err) {
95
+ publishStatus = "failure";
96
+ throw err;
97
+ }
98
+ } else {
99
+ const allowedBranches = cfg.github_wiki?.branches?.join(", ") || "none configured";
100
+ warn(`Skipping GitHub Wiki publish: branch "${currentBranch}" not in allowed list (${allowedBranches})`);
101
+ info("To publish from this branch, add it to github_wiki.branches in .repolens.yml");
102
+ }
103
+ }
104
+
83
105
  // Run plugin publishers
84
106
  if (pluginManager) {
85
107
  const pluginPublishers = pluginManager.getPublishers();
@@ -112,6 +112,33 @@ export function shouldPublishToConfluence(config, currentBranch = getCurrentBran
112
112
  });
113
113
  }
114
114
 
115
+ /**
116
+ * Check if current branch should publish to GitHub Wiki
117
+ * Based on config.github_wiki.branches setting
118
+ */
119
+ export function shouldPublishToGitHubWiki(config, currentBranch = getCurrentBranch()) {
120
+ if (!config.github_wiki) {
121
+ return true;
122
+ }
123
+
124
+ if (!config.github_wiki.branches || config.github_wiki.branches.length === 0) {
125
+ return true;
126
+ }
127
+
128
+ return config.github_wiki.branches.some(pattern => {
129
+ if (pattern === currentBranch) {
130
+ return true;
131
+ }
132
+
133
+ if (pattern.includes("*")) {
134
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
135
+ return regex.test(currentBranch);
136
+ }
137
+
138
+ return false;
139
+ });
140
+ }
141
+
115
142
  /**
116
143
  * Get branch-qualified page title
117
144
  */
@@ -135,7 +135,7 @@ function validatePublishers(publishers) {
135
135
  return errors;
136
136
  }
137
137
 
138
- const validPublishers = ["notion", "markdown", "confluence"];
138
+ const validPublishers = ["notion", "markdown", "confluence", "github_wiki"];
139
139
 
140
140
  if (Array.isArray(publishers)) {
141
141
  for (const pub of publishers) {