@chappibunny/repolens 1.14.2 → 1.15.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/src/init.js CHANGED
@@ -4,6 +4,8 @@ import { createInterface } from "node:readline/promises";
4
4
  import { spawn } from "node:child_process";
5
5
  import { info, warn } from "./utils/logger.js";
6
6
  import { isNonInteractiveEnvironment } from "./utils/interactive.js";
7
+ import { detectRepoLanguages } from "./core/languages.js";
8
+ import { repolensCommand } from "./utils/invocation.js";
7
9
 
8
10
  const PUBLISHER_CHOICES = ["markdown", "notion", "confluence", "github_wiki", "obsidian"];
9
11
 
@@ -315,21 +317,6 @@ const SCAN_PRESETS = {
315
317
  },
316
318
  };
317
319
 
318
- const DETECTABLE_ROOTS = [
319
- "app",
320
- "src/app",
321
- "components",
322
- "src/components",
323
- "lib",
324
- "src/lib",
325
- "hooks",
326
- "src/hooks",
327
- "store",
328
- "src/store",
329
- "pages",
330
- "src/pages"
331
- ];
332
-
333
320
  const DEFAULT_GITHUB_WORKFLOW = `name: RepoLens Documentation
334
321
 
335
322
  on:
@@ -422,7 +409,8 @@ This repository is configured to use [RepoLens](https://github.com/CHAPIBUNNY/re
422
409
  ### Local Testing
423
410
 
424
411
  \`\`\`bash
425
- # Test documentation generation locally
412
+ # Test documentation generation locally.
413
+ # Use \`repolens publish\` when the CLI is on your PATH.
426
414
  npx @chappibunny/repolens publish
427
415
  \`\`\`
428
416
 
@@ -557,15 +545,6 @@ async function fileExists(filePath) {
557
545
  }
558
546
  }
559
547
 
560
- async function dirExists(dirPath) {
561
- try {
562
- const stat = await fs.stat(dirPath);
563
- return stat.isDirectory();
564
- } catch {
565
- return false;
566
- }
567
- }
568
-
569
548
  /**
570
549
  * Open a URL in the default browser (cross-platform).
571
550
  */
@@ -680,23 +659,6 @@ async function testAIKey(provider, apiKey) {
680
659
  }
681
660
  }
682
661
 
683
- async function detectRepoStructure(repoRoot) {
684
- const detectedRoots = [];
685
-
686
- for (const relativePath of DETECTABLE_ROOTS) {
687
- const absolutePath = path.join(repoRoot, relativePath);
688
- if (await dirExists(absolutePath)) {
689
- detectedRoots.push(relativePath);
690
- }
691
- }
692
-
693
- return detectedRoots;
694
- }
695
-
696
- function buildIncludePatterns(detectedRoots) {
697
- return detectedRoots.map((root) => `${root}/**/*.{ts,tsx,js,jsx,md}`);
698
- }
699
-
700
662
  function buildIgnorePatterns() {
701
663
  return [
702
664
  "tools/repolens/**",
@@ -717,12 +679,18 @@ function buildIgnorePatterns() {
717
679
  "**/esm/**",
718
680
  "**/imp/**",
719
681
  "**/types/**",
720
- "**/load-testing/**"
682
+ "**/load-testing/**",
683
+ "**/__pycache__/**",
684
+ "**/.venv/**",
685
+ "**/venv/**",
686
+ "**/vendor/**",
687
+ "**/target/**"
721
688
  ];
722
689
  }
723
690
 
724
- function buildRepoLensConfig(projectName, detectedRoots, options = {}) {
725
- const includePatterns = buildIncludePatterns(detectedRoots);
691
+ function buildRepoLensConfig(projectName, detected, options = {}) {
692
+ const includePatterns = detected?.include?.length ? detected.include : ["**/*.{js,jsx,ts,tsx,py,go,rs,java}"];
693
+ const moduleRoots = detected?.moduleRoots?.length ? detected.moduleRoots : ["."];
726
694
  const ignorePatterns = buildIgnorePatterns();
727
695
  const enableAI = options.enableAI || false;
728
696
 
@@ -734,9 +702,7 @@ function buildRepoLensConfig(projectName, detectedRoots, options = {}) {
734
702
  ` docs_title_prefix: "RepoLens"`,
735
703
  ``,
736
704
  `publishers:`,
737
- ` - markdown # Always generate local Markdown files`,
738
- ` - notion # Auto-detected: publishes if NOTION_TOKEN is set`,
739
- ` - confluence # Auto-detected: publishes if CONFLUENCE_URL is set`,
705
+ ` - markdown # Local files. The wizard (repolens init) can add Notion, Confluence, GitHub Wiki, and Obsidian.`,
740
706
  ``,
741
707
  `# Docs by RepoLens badge on generated Markdown (default: on)`,
742
708
  `# branding: false`,
@@ -799,13 +765,8 @@ function buildRepoLensConfig(projectName, detectedRoots, options = {}) {
799
765
  lines.push(`scan:`);
800
766
  lines.push(` include:`);
801
767
 
802
- if (includePatterns.length) {
803
- for (const pattern of includePatterns) {
804
- lines.push(` - "${pattern}"`);
805
- }
806
- } else {
807
- lines.push(` - "src/**/*.{ts,tsx,js,jsx,md}"`);
808
- lines.push(` - "app/**/*.{ts,tsx,js,jsx,md}"`);
768
+ for (const pattern of includePatterns) {
769
+ lines.push(` - "${pattern}"`);
809
770
  }
810
771
 
811
772
  lines.push(``);
@@ -818,17 +779,8 @@ function buildRepoLensConfig(projectName, detectedRoots, options = {}) {
818
779
  lines.push(``);
819
780
  lines.push(`module_roots:`);
820
781
 
821
- if (detectedRoots.length) {
822
- for (const root of detectedRoots) {
823
- lines.push(` - "${root}"`);
824
- }
825
- } else {
826
- lines.push(` - "app"`);
827
- lines.push(` - "src/app"`);
828
- lines.push(` - "components"`);
829
- lines.push(` - "src/components"`);
830
- lines.push(` - "lib"`);
831
- lines.push(` - "src/lib"`);
782
+ for (const root of moduleRoots) {
783
+ lines.push(` - "${root}"`);
832
784
  }
833
785
 
834
786
  lines.push(``);
@@ -1552,8 +1504,15 @@ export { parseConfluenceUrl, parseNotionInput };
1552
1504
  export async function runInit(targetDir = process.cwd(), options = {}) {
1553
1505
  const repoRoot = path.resolve(targetDir);
1554
1506
 
1555
- // Ensure target directory exists
1556
- await fs.mkdir(repoRoot, { recursive: true });
1507
+ let targetStat;
1508
+ try {
1509
+ targetStat = await fs.stat(repoRoot);
1510
+ } catch {
1511
+ throw new Error(`Target directory not found: ${targetDir}`);
1512
+ }
1513
+ if (!targetStat.isDirectory()) {
1514
+ throw new Error(`Target directory not found: ${targetDir}`);
1515
+ }
1557
1516
 
1558
1517
  // Interactive wizard is the default (--quick skips it).
1559
1518
  // CI, tests, and closed stdin used to return null from the wizard and
@@ -1568,8 +1527,10 @@ export async function runInit(targetDir = process.cwd(), options = {}) {
1568
1527
  info("Non-interactive environment detected — running quick setup instead of the wizard.");
1569
1528
  }
1570
1529
 
1571
- // Prompt for Notion credentials interactively (only in non-wizard mode)
1572
- const notionCredentials = wizardAnswers ? null : await promptNotionCredentials();
1530
+ // --quick is Markdown only. The wizard collects Notion and Confluence.
1531
+ const notionCredentials = wizardAnswers || options.interactive === false
1532
+ ? null
1533
+ : await promptNotionCredentials();
1573
1534
 
1574
1535
  const repolensConfigPath = path.join(repoRoot, ".repolens.yml");
1575
1536
  const workflowDir = path.join(repoRoot, ".github", "workflows");
@@ -1587,25 +1548,31 @@ export async function runInit(targetDir = process.cwd(), options = {}) {
1587
1548
  const readmeExists = await fileExists(readmePath);
1588
1549
 
1589
1550
  const projectName = wizardAnswers?.projectName || detectProjectName(repoRoot);
1590
- const detectedRoots = wizardAnswers ? [] : await detectRepoStructure(repoRoot);
1551
+ const detected = wizardAnswers ? null : await detectRepoLanguages(repoRoot);
1591
1552
 
1592
1553
  // Auto-detect GITHUB_TOKEN for AI enablement
1593
1554
  const hasGitHubToken = !!process.env.GITHUB_TOKEN;
1594
1555
  const configContent = wizardAnswers
1595
1556
  ? buildWizardConfig(wizardAnswers)
1596
- : buildRepoLensConfig(projectName, detectedRoots, { enableAI: hasGitHubToken });
1557
+ : buildRepoLensConfig(projectName, detected, { enableAI: hasGitHubToken });
1597
1558
 
1598
1559
  info(`Detected project name: ${projectName}`);
1599
1560
 
1600
- if (detectedRoots.length) {
1601
- info(`Detected module roots:`);
1602
- for (const root of detectedRoots) {
1561
+ if (detected) {
1562
+ if (detected.languages.length) {
1563
+ info(`Detected languages: ${detected.languages.map((lang) => lang.label).join(", ")}`);
1564
+ } else {
1565
+ info("No known language detected. Using multi-language scan patterns.");
1566
+ }
1567
+ info("module_roots:");
1568
+ for (const root of detected.moduleRoots) {
1603
1569
  info(` - ${root}`);
1604
1570
  }
1605
- } else if (!wizardAnswers) {
1606
- info(`No known roots detected. Falling back to default config.`);
1607
1571
  }
1608
1572
 
1573
+ const publishCmd = repolensCommand("publish");
1574
+ const doctorCmd = repolensCommand("doctor");
1575
+
1609
1576
  if (!configExists) {
1610
1577
  await fs.writeFile(repolensConfigPath, configContent, "utf8");
1611
1578
  info(`Created ${repolensConfigPath}`);
@@ -1694,13 +1661,13 @@ NOTION_VERSION=2022-06-28
1694
1661
  info(" └────────────────────────────────────────────────────────────┘");
1695
1662
  info("");
1696
1663
  info(" 1. source .env ← Load your credentials");
1697
- info(" 2. npx @chappibunny/repolens publish ← Test locally");
1664
+ info(` 2. ${publishCmd} ← Test locally`);
1698
1665
  } else {
1699
- info(" 1. npx @chappibunny/repolens publish ← Test locally");
1666
+ info(` 1. ${publishCmd} ← Test locally`);
1700
1667
  }
1701
1668
  info(` ${hasLocalCredentials ? "3" : "2"}. Add GitHub secrets (see above)`);
1702
1669
  info(` ${hasLocalCredentials ? "4" : "3"}. Commit and push to trigger workflow`);
1703
- info(` ${hasLocalCredentials ? "5" : "4"}. Run 'npx @chappibunny/repolens doctor' to validate setup`);
1670
+ info(` ${hasLocalCredentials ? "5" : "4"}. Run '${doctorCmd}' to validate setup`);
1704
1671
  return;
1705
1672
  }
1706
1673
 
@@ -1714,7 +1681,7 @@ NOTION_VERSION=2022-06-28
1714
1681
  info(" Your credentials are stored in .env (gitignored)\n");
1715
1682
  info("Next steps:");
1716
1683
  info(" 1. Review .repolens.yml to customize your documentation");
1717
- info(" 2. Run 'npx @chappibunny/repolens publish' to generate your first docs (deterministic mode)");
1684
+ info(` 2. Run '${publishCmd}' to generate your first docs (deterministic mode)`);
1718
1685
  info(" 3. (Optional) Enable AI features:");
1719
1686
  info(" ── FREE: GitHub Models (recommended for GitHub Actions) ──");
1720
1687
  info(" REPOLENS_AI_ENABLED=true");
@@ -1745,7 +1712,7 @@ NOTION_VERSION=2022-06-28
1745
1712
  info(" REPOLENS_AI_API_KEY=sk-...");
1746
1713
  info(" See: https://github.com/CHAPIBUNNY/repolens/blob/main/AI.md");
1747
1714
  }
1748
- info(` ${hasGitHubToken ? "3" : "4"}. Run 'npx @chappibunny/repolens publish' to test locally`);
1715
+ info(` ${hasGitHubToken ? "3" : "4"}. Run '${publishCmd}' to test locally`);
1749
1716
  info(` ${hasGitHubToken ? "4" : "5"}. Commit the generated files`);
1750
1717
  }
1751
1718
  }
@@ -20,6 +20,39 @@ function hasNotionSecrets() {
20
20
  return !!process.env.NOTION_TOKEN && !!process.env.NOTION_PARENT_PAGE_ID;
21
21
  }
22
22
 
23
+ /**
24
+ * Credentials a configured publisher still needs.
25
+ * Markdown has none. Used by `publish --strict`.
26
+ */
27
+ export function missingPublisherCredentials(cfg) {
28
+ const publishers = Array.isArray(cfg?.publishers) ? cfg.publishers : [];
29
+ const missing = [];
30
+
31
+ if (publishers.includes("notion")) {
32
+ if (!process.env.NOTION_TOKEN) missing.push("NOTION_TOKEN (required for notion)");
33
+ if (!process.env.NOTION_PARENT_PAGE_ID) missing.push("NOTION_PARENT_PAGE_ID (required for notion)");
34
+ }
35
+
36
+ if (publishers.includes("confluence")) {
37
+ for (const key of ["CONFLUENCE_URL", "CONFLUENCE_EMAIL", "CONFLUENCE_API_TOKEN", "CONFLUENCE_SPACE_KEY"]) {
38
+ if (!process.env[key]) missing.push(`${key} (required for confluence)`);
39
+ }
40
+ }
41
+
42
+ if (publishers.includes("github_wiki")) {
43
+ if (!process.env.GITHUB_TOKEN) missing.push("GITHUB_TOKEN (required for github_wiki)");
44
+ }
45
+
46
+ if (publishers.includes("obsidian")) {
47
+ const vault = cfg?.obsidian?.vault;
48
+ if (typeof vault !== "string" || vault.trim() === "") {
49
+ missing.push("obsidian.vault (required for obsidian)");
50
+ }
51
+ }
52
+
53
+ return missing;
54
+ }
55
+
23
56
  export async function publishDocs(cfg, renderedPages, scanResult, pluginManager = null) {
24
57
  const publishers = cfg.publishers || ["markdown", "notion"];
25
58
  const currentBranch = getCurrentBranch();
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The ASCII banner is for an interactive terminal.
3
+ * CI logs and pipes get the normal command output only.
4
+ */
5
+ export function shouldPrintBanner({ isTTY = Boolean(process.stdout.isTTY), env = process.env } = {}) {
6
+ if (env && env.CI) return false;
7
+ if (!isTTY) return false;
8
+ return true;
9
+ }
@@ -0,0 +1,54 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * True when `entryPath` is the RepoLens install under npm's global prefix.
7
+ * npx caches live under `_npx` and are not a global install.
8
+ */
9
+ export function isGlobalInstallPath(entryPath, globalPrefix) {
10
+ if (!entryPath || !globalPrefix) return false;
11
+ if (entryPath.includes(`${path.sep}_npx${path.sep}`)) return false;
12
+ let entry = entryPath;
13
+ let prefix = globalPrefix;
14
+ try {
15
+ entry = fs.realpathSync(entryPath);
16
+ } catch {
17
+ entry = path.resolve(entryPath);
18
+ }
19
+ prefix = path.resolve(globalPrefix);
20
+ return entry === prefix || entry.startsWith(prefix + path.sep);
21
+ }
22
+
23
+ let cachedGlobal = null;
24
+
25
+ export function isGlobalInstall() {
26
+ if (cachedGlobal != null) return cachedGlobal;
27
+ const entry = process.argv[1];
28
+ if (!entry) {
29
+ cachedGlobal = false;
30
+ return false;
31
+ }
32
+ try {
33
+ const prefix = execFileSync("npm", ["prefix", "-g"], {
34
+ encoding: "utf8",
35
+ stdio: ["ignore", "pipe", "ignore"],
36
+ timeout: 5000,
37
+ }).trim();
38
+ cachedGlobal = isGlobalInstallPath(entry, prefix);
39
+ } catch {
40
+ cachedGlobal = false;
41
+ }
42
+ return cachedGlobal;
43
+ }
44
+
45
+ /**
46
+ * Command shown in init's next-steps text.
47
+ * Global installs get `repolens publish`. Everything else gets npx.
48
+ */
49
+ export function repolensCommand(subcommand, options = {}) {
50
+ const globalInstall = options.globalInstall ?? isGlobalInstall();
51
+ const bin = globalInstall ? "repolens" : "npx @chappibunny/repolens";
52
+ if (!subcommand) return bin;
53
+ return `${bin} ${subcommand}`;
54
+ }
package/src/watch.js CHANGED
@@ -40,42 +40,48 @@ export async function runWatch(configPath) {
40
40
  info("Starting watch mode (Markdown only)...");
41
41
  info("Press Ctrl+C to stop.\n");
42
42
 
43
- // Initial build
44
- await rebuild(configPath);
45
-
46
- // Determine directories to watch from config
47
43
  const cfg = await loadConfig(configPath);
48
44
  const repoRoot = cfg.__repoRoot || process.cwd();
49
45
  const moduleRoots = cfg.module_roots || ["src", "app", "lib"];
50
46
 
51
- let debounceTimer = null;
52
-
53
- const onChange = () => {
54
- if (debounceTimer) clearTimeout(debounceTimer);
55
- debounceTimer = setTimeout(() => rebuild(configPath), DEBOUNCE_MS);
56
- };
57
-
58
- const watchers = [];
47
+ const watchable = [];
59
48
  for (const root of moduleRoots) {
60
49
  const dirPath = path.resolve(repoRoot, root);
61
50
  try {
62
51
  fs.accessSync(dirPath);
63
- const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
64
- if (filename && !filename.includes("node_modules")) {
65
- info(`Change detected: ${root}/${filename}`);
66
- onChange();
67
- }
68
- });
69
- watchers.push(watcher);
70
- info(`Watching: ${root}/`);
52
+ watchable.push({ root, dirPath });
71
53
  } catch {
72
54
  // Directory doesn't exist, skip
73
55
  }
74
56
  }
75
57
 
76
- if (watchers.length === 0) {
77
- warn("No directories to watch. Check module_roots in .repolens.yml");
78
- return;
58
+ if (watchable.length === 0) {
59
+ const hint = "No directories to watch. Check module_roots in .repolens.yml — none of the configured roots exist.";
60
+ warn(hint);
61
+ error(hint);
62
+ return { ok: false, message: hint };
63
+ }
64
+
65
+ // Initial build only when there is something to watch.
66
+ await rebuild(configPath);
67
+
68
+ let debounceTimer = null;
69
+
70
+ const onChange = () => {
71
+ if (debounceTimer) clearTimeout(debounceTimer);
72
+ debounceTimer = setTimeout(() => rebuild(configPath), DEBOUNCE_MS);
73
+ };
74
+
75
+ const watchers = [];
76
+ for (const { root, dirPath } of watchable) {
77
+ const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
78
+ if (filename && !filename.includes("node_modules")) {
79
+ info(`Change detected: ${root}/${filename}`);
80
+ onChange();
81
+ }
82
+ });
83
+ watchers.push(watcher);
84
+ info(`Watching: ${root}/`);
79
85
  }
80
86
 
81
87
  info("\nWaiting for changes...\n");