@chappibunny/repolens 0.9.0 → 1.1.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.
@@ -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",
@@ -39,17 +39,17 @@ class ValidationError extends Error {
39
39
  export function validateConfig(config) {
40
40
  const errors = [];
41
41
 
42
- // Check schema version
43
- if (config.configVersion !== undefined) {
44
- if (typeof config.configVersion !== "number") {
45
- errors.push("configVersion must be a number");
46
- } else if (config.configVersion > CURRENT_SCHEMA_VERSION) {
47
- errors.push(
48
- `Config schema version ${config.configVersion} is not supported. ` +
49
- `This version of RepoLens supports schema version ${CURRENT_SCHEMA_VERSION}. ` +
50
- `Please upgrade RepoLens or downgrade your config.`
51
- );
52
- }
42
+ // Check schema version (required for v1.0+)
43
+ if (config.configVersion === undefined || config.configVersion === null) {
44
+ errors.push("Missing required field: configVersion (must be 1)");
45
+ } else if (typeof config.configVersion !== "number") {
46
+ errors.push("configVersion must be a number");
47
+ } else if (config.configVersion > CURRENT_SCHEMA_VERSION) {
48
+ errors.push(
49
+ `Config schema version ${config.configVersion} is not supported. ` +
50
+ `This version of RepoLens supports schema version ${CURRENT_SCHEMA_VERSION}. ` +
51
+ `Please upgrade RepoLens or downgrade your config.`
52
+ );
53
53
  }
54
54
 
55
55
  // Validate project section
@@ -173,6 +173,51 @@ export function validateConfig(config) {
173
173
  }
174
174
  }
175
175
 
176
+ // Validate Confluence configuration (optional)
177
+ if (config.confluence !== undefined) {
178
+ if (typeof config.confluence !== "object" || Array.isArray(config.confluence)) {
179
+ errors.push("confluence must be an object");
180
+ } else {
181
+ // Validate branches filter
182
+ if (config.confluence.branches !== undefined) {
183
+ if (!Array.isArray(config.confluence.branches)) {
184
+ errors.push("confluence.branches must be an array");
185
+ } else {
186
+ config.confluence.branches.forEach((branch, idx) => {
187
+ if (typeof branch !== "string") {
188
+ errors.push(`confluence.branches[${idx}] must be a string`);
189
+ }
190
+ });
191
+ }
192
+ }
193
+ }
194
+ }
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
+
176
221
  // Validate Discord configuration (optional)
177
222
  if (config.discord !== undefined) {
178
223
  if (typeof config.discord !== "object" || Array.isArray(config.discord)) {
@@ -241,9 +286,13 @@ export function validateConfig(config) {
241
286
  }
242
287
  if (config.ai.temperature !== undefined && typeof config.ai.temperature !== "number") {
243
288
  errors.push("ai.temperature must be a number");
289
+ } else if (typeof config.ai.temperature === "number" && (config.ai.temperature < 0 || config.ai.temperature > 2)) {
290
+ errors.push("ai.temperature must be between 0 and 2");
244
291
  }
245
292
  if (config.ai.max_tokens !== undefined && typeof config.ai.max_tokens !== "number") {
246
293
  errors.push("ai.max_tokens must be a number");
294
+ } else if (typeof config.ai.max_tokens === "number" && config.ai.max_tokens <= 0) {
295
+ errors.push("ai.max_tokens must be greater than 0");
247
296
  }
248
297
  }
249
298
  }
package/src/init.js CHANGED
@@ -94,7 +94,7 @@ NOTION_VERSION=2022-06-28
94
94
 
95
95
  # Confluence Publishing
96
96
  CONFLUENCE_URL=https://your-company.atlassian.net/wiki
97
- CONFLUENCE_EMAIL=trades@rabitaitrades.com
97
+ CONFLUENCE_EMAIL=your-email@example.com
98
98
  CONFLUENCE_API_TOKEN=
99
99
  CONFLUENCE_SPACE_KEY=DOCS
100
100
  CONFLUENCE_PARENT_PAGE_ID=
@@ -104,8 +104,7 @@ CONFLUENCE_PARENT_PAGE_ID=
104
104
  # REPOLENS_AI_ENABLED=true
105
105
  # REPOLENS_AI_API_KEY=sk-...
106
106
  # REPOLENS_AI_BASE_URL=https://api.openai.com/v1
107
- # REPOLENS_AI_MODEL=gpt-4-turbo-preview
108
- # REPOLENS_AI_TEMPERATURE=0.3
107
+ # REPOLENS_AI_MODEL=gpt-5-mini
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
@@ -234,7 +234,7 @@ export async function runMigrate(targetDir = process.cwd(), options = {}) {
234
234
  console.log("\n📝 Next steps:");
235
235
  console.log(" 1. Review the changes: git diff .github/workflows/");
236
236
  console.log(" 2. Test locally: npx @chappibunny/repolens@latest publish");
237
- console.log(" 3. Commit: git add .github/workflows/ && git commit -m 'chore: migrate RepoLens workflow to v0.4.0'");
237
+ console.log(" 3. Commit: git add .github/workflows/ && git commit -m 'chore: migrate RepoLens workflow to latest format'");
238
238
  console.log(" 4. Push: git push");
239
239
  console.log("\n💡 Tip: Backups saved as *.backup - delete them once verified");
240
240
  } else {
@@ -259,6 +259,6 @@ export async function runMigrate(targetDir = process.cwd(), options = {}) {
259
259
  async function printMigrationBanner() {
260
260
  console.log("\n" + "=".repeat(60));
261
261
  console.log("🔄 RepoLens Workflow Migration Tool");
262
- console.log(" Upgrading to v0.4.0 format");
262
+ console.log(" Upgrading to latest format");
263
263
  console.log("=".repeat(60));
264
264
  }
@@ -0,0 +1,454 @@
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
+ git("init", tmpDir);
396
+ git(`remote add origin ${cloneUrl}`, tmpDir);
397
+ }
398
+
399
+ const pageKeys = Object.keys(renderedPages);
400
+
401
+ // Write Home.md
402
+ const home = generateHome(pageKeys, projectName, branch, repo);
403
+ await fs.writeFile(path.join(tmpDir, "Home.md"), home, "utf8");
404
+
405
+ // Write each rendered page with metadata header
406
+ for (const [key, markdown] of Object.entries(renderedPages)) {
407
+ const fileName = pageFileName(key);
408
+ const header = pageHeader(key, branch);
409
+ await fs.writeFile(path.join(tmpDir, fileName), header + markdown, "utf8");
410
+ }
411
+
412
+ // Generate sidebar
413
+ if (includeSidebar) {
414
+ const sidebar = generateSidebar(pageKeys, projectName, branch);
415
+ await fs.writeFile(path.join(tmpDir, "_Sidebar.md"), sidebar, "utf8");
416
+ }
417
+
418
+ // Generate footer
419
+ if (includeFooter) {
420
+ const footer = generateFooter(branch);
421
+ await fs.writeFile(path.join(tmpDir, "_Footer.md"), footer, "utf8");
422
+ }
423
+
424
+ // Stage, commit, push
425
+ git("config user.name \"RepoLens Bot\"", tmpDir);
426
+ git("config user.email \"repolens@users.noreply.github.com\"", tmpDir);
427
+ git("add -A", tmpDir);
428
+
429
+ try {
430
+ execSync("git diff --cached --quiet", { cwd: tmpDir, stdio: "pipe" });
431
+ info("GitHub Wiki is already up to date — no changes to push.");
432
+ return;
433
+ } catch {
434
+ // There are staged changes — continue to commit
435
+ }
436
+
437
+ const branchLabel = branch !== "main" && branch !== "master" ? ` [${branch}]` : "";
438
+ const commitMsg = `docs: update RepoLens documentation${branchLabel}`;
439
+ git(`commit -m "${commitMsg}"`, tmpDir);
440
+
441
+ git("push origin HEAD", tmpDir);
442
+ info(`GitHub Wiki published: https://github.com/${repo}/wiki`);
443
+
444
+ } finally {
445
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Check if GitHub Wiki publishing secrets are available.
451
+ */
452
+ export function hasGitHubWikiSecrets() {
453
+ return !!process.env.GITHUB_TOKEN;
454
+ }
@@ -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();
@@ -90,8 +112,8 @@ export async function publishDocs(cfg, renderedPages, scanResult, pluginManager
90
112
  await publisher.publish(cfg, renderedPages);
91
113
  publishedTo.push(key);
92
114
  } catch (err) {
115
+ warn(`Plugin publisher "${key}" failed: ${err.message}`);
93
116
  publishStatus = "failure";
94
- throw err;
95
117
  }
96
118
  }
97
119
  }
@@ -1,7 +1,7 @@
1
1
  import fetch from "node-fetch";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { log } from "../utils/logger.js";
4
+ import { log, warn } from "../utils/logger.js";
5
5
  import { fetchWithRetry } from "../utils/retry.js";
6
6
  import { executeNotionRequest } from "../utils/rate-limit.js";
7
7
  import { createRepoLensError } from "../utils/errors.js";
@@ -243,7 +243,7 @@ function parseInlineRichText(text) {
243
243
  function markdownToNotionBlocks(markdown) {
244
244
  // Safety check: handle undefined/null markdown
245
245
  if (!markdown || typeof markdown !== 'string') {
246
- console.warn(`Warning: markdownToNotionBlocks received invalid markdown: ${typeof markdown}`);
246
+ warn(`markdownToNotionBlocks received invalid markdown: ${typeof markdown}`);
247
247
  return [];
248
248
  }
249
249
 
@@ -1,5 +1,6 @@
1
1
  import { ensurePage, replacePageContent } from "./notion.js";
2
2
  import { getCurrentBranch, getBranchQualifiedTitle } from "../utils/branch.js";
3
+ import { warn } from "../utils/logger.js";
3
4
 
4
5
  export async function publishToNotion(cfg, renderedPages) {
5
6
  const parentPageId = process.env.NOTION_PARENT_PAGE_ID;
@@ -22,7 +23,7 @@ export async function publishToNotion(cfg, renderedPages) {
22
23
 
23
24
  // Skip if content not generated (e.g., disabled feature or generation error)
24
25
  if (!markdown) {
25
- console.log(`⚠️ Skipping ${page.key}: No content generated`);
26
+ warn(`Skipping ${page.key}: No content generated`);
26
27
  continue;
27
28
  }
28
29