@chappibunny/repolens 1.14.3 → 1.15.1
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 +43 -0
- package/README.md +24 -11
- package/bin/repolens-mcp.js +1 -1
- package/bin/repolens.js +8 -5
- package/llms-install.md +2 -0
- package/package.json +1 -1
- package/src/analyzers/complexity-analyzer.js +149 -10
- package/src/cli-args.js +1 -0
- package/src/cli.js +67 -9
- package/src/core/diff.js +15 -1
- package/src/core/languages.js +268 -0
- package/src/core/scan.js +11 -6
- package/src/doctor.js +24 -3
- package/src/help.js +257 -10
- package/src/init.js +49 -82
- package/src/publishers/index.js +33 -0
- package/src/renderers/renderAnalysis.js +2 -2
- package/src/utils/banner.js +9 -0
- package/src/utils/invocation.js +54 -0
- package/src/watch.js +43 -26
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,
|
|
725
|
-
const includePatterns =
|
|
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 #
|
|
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
|
-
|
|
803
|
-
|
|
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
|
-
|
|
822
|
-
|
|
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
|
-
|
|
1556
|
-
|
|
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
|
-
//
|
|
1572
|
-
const notionCredentials = wizardAnswers
|
|
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
|
|
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,
|
|
1557
|
+
: buildRepoLensConfig(projectName, detected, { enableAI: hasGitHubToken });
|
|
1597
1558
|
|
|
1598
1559
|
info(`Detected project name: ${projectName}`);
|
|
1599
1560
|
|
|
1600
|
-
if (
|
|
1601
|
-
|
|
1602
|
-
|
|
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(
|
|
1664
|
+
info(` 2. ${publishCmd} ← Test locally`);
|
|
1698
1665
|
} else {
|
|
1699
|
-
info(
|
|
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 '
|
|
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(
|
|
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 '
|
|
1715
|
+
info(` ${hasGitHubToken ? "3" : "4"}. Run '${publishCmd}' to test locally`);
|
|
1749
1716
|
info(` ${hasGitHubToken ? "4" : "5"}. Commit the generated files`);
|
|
1750
1717
|
}
|
|
1751
1718
|
}
|
package/src/publishers/index.js
CHANGED
|
@@ -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();
|
|
@@ -593,7 +593,7 @@ export function renderCodeHealth(healthResult) {
|
|
|
593
593
|
lines.push("");
|
|
594
594
|
lines.push("| Factor | Weight | How It's Measured |");
|
|
595
595
|
lines.push("|--------|--------|-------------------|");
|
|
596
|
-
lines.push("| Complexity | –1/pt above 10, –2/pt above 30 | Cyclomatic complexity
|
|
596
|
+
lines.push("| Complexity | –1/pt above 10, –2/pt above 30 | Cyclomatic complexity for JavaScript/TypeScript, Python, and Go (regex, not AST) |");
|
|
597
597
|
lines.push("| Function complexity | –2/pt above 8 per function | Highest complexity function in the file |");
|
|
598
598
|
lines.push("| Coupling | –1/5 units above 20 | Fan-in × Fan-out (import connections) |");
|
|
599
599
|
lines.push("| Documentation | –10 if <50%, –5 if <80% | JSDoc/TSDoc coverage of exports |");
|
|
@@ -602,7 +602,7 @@ export function renderCodeHealth(healthResult) {
|
|
|
602
602
|
|
|
603
603
|
lines.push("---");
|
|
604
604
|
lines.push("");
|
|
605
|
-
lines.push("*Generated by RepoLens code health analysis. Complexity is regex-based and may differ from AST-based tools. Scores are relative indicators, not absolute quality measures.*");
|
|
605
|
+
lines.push("*Generated by RepoLens code health analysis. Complexity covers JavaScript/TypeScript, Python, and Go. It is regex-based and may differ from AST-based tools. Scores are relative indicators, not absolute quality measures.*");
|
|
606
606
|
lines.push("");
|
|
607
607
|
|
|
608
608
|
return lines.join("\n");
|
|
@@ -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
|
@@ -37,45 +37,62 @@ async function rebuild(configPath) {
|
|
|
37
37
|
* @param {string} configPath - Resolved path to .repolens.yml
|
|
38
38
|
*/
|
|
39
39
|
export async function runWatch(configPath) {
|
|
40
|
-
info("Starting watch mode (Markdown only)...");
|
|
41
|
-
info("Press Ctrl+C to stop.\n");
|
|
42
|
-
|
|
43
|
-
// Initial build
|
|
44
|
-
await rebuild(configPath);
|
|
45
|
-
|
|
46
|
-
// Determine directories to watch from config
|
|
47
40
|
const cfg = await loadConfig(configPath);
|
|
48
41
|
const repoRoot = cfg.__repoRoot || process.cwd();
|
|
49
42
|
const moduleRoots = cfg.module_roots || ["src", "app", "lib"];
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const onChange = () => {
|
|
54
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
55
|
-
debounceTimer = setTimeout(() => rebuild(configPath), DEBOUNCE_MS);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const watchers = [];
|
|
44
|
+
const watchable = [];
|
|
59
45
|
for (const root of moduleRoots) {
|
|
60
46
|
const dirPath = path.resolve(repoRoot, root);
|
|
61
47
|
try {
|
|
62
48
|
fs.accessSync(dirPath);
|
|
63
|
-
|
|
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}/`);
|
|
49
|
+
watchable.push({ root, dirPath });
|
|
71
50
|
} catch {
|
|
72
51
|
// Directory doesn't exist, skip
|
|
73
52
|
}
|
|
74
53
|
}
|
|
75
54
|
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
55
|
+
if (watchable.length === 0) {
|
|
56
|
+
const hint = "No directories to watch. Check module_roots in .repolens.yml — none of the configured roots exist.";
|
|
57
|
+
warn(hint);
|
|
58
|
+
error(hint);
|
|
59
|
+
return { ok: false, message: hint };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A root can exist (including "." after init --quick in an empty directory)
|
|
63
|
+
// while scan.include still matches nothing. Do not wait forever in that case.
|
|
64
|
+
const scan = await scanRepo(cfg);
|
|
65
|
+
if ((scan?.filesCount || 0) === 0) {
|
|
66
|
+
const patterns = (cfg.scan?.include || []).join(", ") || "(none)";
|
|
67
|
+
const hint = `No files matched scan.include (${patterns}). Nothing to watch. Check scan.include and module_roots in .repolens.yml. Add source files or update the globs, then run watch again.`;
|
|
68
|
+
warn(hint);
|
|
69
|
+
error(hint);
|
|
70
|
+
return { ok: false, message: hint };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
info("Starting watch mode (Markdown only)...");
|
|
74
|
+
info("Press Ctrl+C to stop.\n");
|
|
75
|
+
|
|
76
|
+
// Initial build only when there is something to watch.
|
|
77
|
+
await rebuild(configPath);
|
|
78
|
+
|
|
79
|
+
let debounceTimer = null;
|
|
80
|
+
|
|
81
|
+
const onChange = () => {
|
|
82
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
83
|
+
debounceTimer = setTimeout(() => rebuild(configPath), DEBOUNCE_MS);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const watchers = [];
|
|
87
|
+
for (const { root, dirPath } of watchable) {
|
|
88
|
+
const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
|
|
89
|
+
if (filename && !filename.includes("node_modules")) {
|
|
90
|
+
info(`Change detected: ${root}/${filename}`);
|
|
91
|
+
onChange();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
watchers.push(watcher);
|
|
95
|
+
info(`Watching: ${root}/`);
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
info("\nWaiting for changes...\n");
|