@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.
@@ -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();
@@ -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
 
@@ -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
  */
@@ -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
- console.warn(
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
- console.warn(
226
+ warn(
225
227
  `AI API retry ${attempt}/${maxRetries} after ${Math.round(delay)}ms (${error.message})`
226
228
  );
227
229
  }
@@ -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: "https://082083dbf5899ed7e65dfd9b8dc72f90@o4511014913703936.ingest.de.sentry.io/4511014919209040", // TODO: Replace with actual 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: "https://082083dbf5899ed7e65dfd9b8dc72f90@o4511014913703936.ingest.de.sentry.io/4511014919209040",
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
- console.log("");
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
- console.log("");
121
+ info("");
122
122
  }
123
123
 
124
124
  export async function forceCheckForUpdates() {