@chappibunny/repolens 0.9.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 +93 -0
- package/README.md +67 -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 +91 -9
- package/src/publishers/github-wiki.js +461 -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
|
@@ -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
|
+
}
|
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
|
|
package/src/utils/branch.js
CHANGED
|
@@ -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
|
*/
|
package/src/utils/rate-limit.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* to prevent abuse and respect API limits.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { warn } from "./logger.js";
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Rate limiter class using token bucket algorithm
|
|
10
12
|
*/
|
|
@@ -193,7 +195,7 @@ export async function executeNotionRequest(fn, options = {}) {
|
|
|
193
195
|
if (options.onRetry) {
|
|
194
196
|
options.onRetry(attempt, maxRetries, delay, error);
|
|
195
197
|
} else {
|
|
196
|
-
|
|
198
|
+
warn(
|
|
197
199
|
`Notion API retry ${attempt}/${maxRetries} after ${Math.round(delay)}ms (${error.message})`
|
|
198
200
|
);
|
|
199
201
|
}
|
|
@@ -221,7 +223,7 @@ export async function executeAIRequest(fn, options = {}) {
|
|
|
221
223
|
if (options.onRetry) {
|
|
222
224
|
options.onRetry(attempt, maxRetries, delay, error);
|
|
223
225
|
} else {
|
|
224
|
-
|
|
226
|
+
warn(
|
|
225
227
|
`AI API retry ${attempt}/${maxRetries} after ${Math.round(delay)}ms (${error.message})`
|
|
226
228
|
);
|
|
227
229
|
}
|
package/src/utils/telemetry.js
CHANGED
|
@@ -33,8 +33,10 @@ export function initTelemetry() {
|
|
|
33
33
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
34
34
|
const version = packageJson.version || "unknown";
|
|
35
35
|
|
|
36
|
+
const dsn = process.env.REPOLENS_SENTRY_DSN || "https://082083dbf5899ed7e65dfd9b8dc72f90@o4511014913703936.ingest.de.sentry.io/4511014919209040";
|
|
37
|
+
|
|
36
38
|
Sentry.init({
|
|
37
|
-
dsn
|
|
39
|
+
dsn,
|
|
38
40
|
|
|
39
41
|
// Release tracking
|
|
40
42
|
release: `repolens@${version}`,
|
|
@@ -412,8 +414,10 @@ function ensureSentryForFeedback() {
|
|
|
412
414
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
413
415
|
const version = packageJson.version || "unknown";
|
|
414
416
|
|
|
417
|
+
const dsn = process.env.REPOLENS_SENTRY_DSN || "https://082083dbf5899ed7e65dfd9b8dc72f90@o4511014913703936.ingest.de.sentry.io/4511014919209040";
|
|
418
|
+
|
|
415
419
|
Sentry.init({
|
|
416
|
-
dsn
|
|
420
|
+
dsn,
|
|
417
421
|
release: `repolens@${version}`,
|
|
418
422
|
environment: process.env.NODE_ENV || "production",
|
|
419
423
|
sampleRate: 1.0, // Always send feedback
|
|
@@ -104,7 +104,7 @@ export async function checkForUpdates() {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function showUpdateMessage(current, latest) {
|
|
107
|
-
|
|
107
|
+
info("");
|
|
108
108
|
warn("┌────────────────────────────────────────────────────────────┐");
|
|
109
109
|
warn("│ 📦 Update Available │");
|
|
110
110
|
warn("├────────────────────────────────────────────────────────────┤");
|
|
@@ -118,7 +118,7 @@ function showUpdateMessage(current, latest) {
|
|
|
118
118
|
warn("│ │");
|
|
119
119
|
warn("│ Release notes: https://github.com/CHAPIBUNNY/repolens │");
|
|
120
120
|
warn("└────────────────────────────────────────────────────────────┘");
|
|
121
|
-
|
|
121
|
+
info("");
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
export async function forceCheckForUpdates() {
|