@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.
- package/CHANGELOG.md +76 -0
- package/README.md +63 -33
- package/package.json +1 -1
- package/src/ai/generate-sections.js +0 -6
- package/src/ai/provider.js +30 -19
- package/src/cli.js +35 -15
- package/src/core/config-schema.js +61 -12
- package/src/init.js +2 -5
- package/src/migrate.js +2 -2
- package/src/publishers/github-wiki.js +454 -0
- package/src/publishers/index.js +24 -2
- package/src/publishers/notion.js +2 -2
- package/src/publishers/publish.js +2 -1
- package/src/utils/branch.js +27 -0
- package/src/utils/rate-limit.js +4 -2
- package/src/utils/telemetry.js +6 -2
- package/src/utils/update-check.js +2 -2
- package/src/utils/validate.js +33 -24
|
@@ -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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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=
|
|
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-
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/publishers/index.js
CHANGED
|
@@ -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 {
|
|
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
|
}
|
package/src/publishers/notion.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
+
warn(`Skipping ${page.key}: No content generated`);
|
|
26
27
|
continue;
|
|
27
28
|
}
|
|
28
29
|
|