@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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Language profiles used by `init --quick` and config-less `demo`.
|
|
6
|
+
* Globs are repository-wide so a Python package that lives next to setup.py
|
|
7
|
+
* is scanned even when it is not under src/.
|
|
8
|
+
*/
|
|
9
|
+
export const LANGUAGE_PROFILES = [
|
|
10
|
+
{
|
|
11
|
+
id: "python",
|
|
12
|
+
label: "Python",
|
|
13
|
+
markers: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "poetry.lock"],
|
|
14
|
+
extensions: [".py"],
|
|
15
|
+
include: ["**/*.py"],
|
|
16
|
+
candidateRoots: ["src", "app", "lib", "api", "core", "services"],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "go",
|
|
20
|
+
label: "Go",
|
|
21
|
+
markers: ["go.mod"],
|
|
22
|
+
extensions: [".go"],
|
|
23
|
+
include: ["**/*.go"],
|
|
24
|
+
candidateRoots: ["cmd", "internal", "pkg", "api"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "rust",
|
|
28
|
+
label: "Rust",
|
|
29
|
+
markers: ["Cargo.toml"],
|
|
30
|
+
extensions: [".rs"],
|
|
31
|
+
include: ["**/*.rs"],
|
|
32
|
+
candidateRoots: ["src", "crates"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "java",
|
|
36
|
+
label: "Java",
|
|
37
|
+
markers: ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"],
|
|
38
|
+
extensions: [".java", ".kt", ".kts", ".scala"],
|
|
39
|
+
include: ["**/*.{java,kt,kts,scala}"],
|
|
40
|
+
candidateRoots: ["src/main/java", "src/main/kotlin", "src/main/scala", "src", "app"],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "javascript",
|
|
44
|
+
label: "JavaScript",
|
|
45
|
+
markers: ["package.json", "tsconfig.json", "jsconfig.json"],
|
|
46
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".vue", ".svelte"],
|
|
47
|
+
include: ["**/*.{js,jsx,mjs,cjs,ts,tsx,vue,svelte}"],
|
|
48
|
+
candidateRoots: ["src", "app", "lib", "pages", "components", "packages"],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "ruby",
|
|
52
|
+
label: "Ruby",
|
|
53
|
+
markers: ["Gemfile"],
|
|
54
|
+
extensions: [".rb"],
|
|
55
|
+
include: ["**/*.rb"],
|
|
56
|
+
candidateRoots: ["app", "lib"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "php",
|
|
60
|
+
label: "PHP",
|
|
61
|
+
markers: ["composer.json"],
|
|
62
|
+
extensions: [".php"],
|
|
63
|
+
include: ["**/*.php"],
|
|
64
|
+
candidateRoots: ["src", "app", "lib"],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "csharp",
|
|
68
|
+
label: "C#",
|
|
69
|
+
markers: ["*.csproj", "*.sln"],
|
|
70
|
+
extensions: [".cs"],
|
|
71
|
+
include: ["**/*.cs"],
|
|
72
|
+
candidateRoots: ["src"],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "swift",
|
|
76
|
+
label: "Swift",
|
|
77
|
+
markers: ["Package.swift"],
|
|
78
|
+
extensions: [".swift"],
|
|
79
|
+
include: ["**/*.swift"],
|
|
80
|
+
candidateRoots: ["Sources", "src"],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "cpp",
|
|
84
|
+
label: "C",
|
|
85
|
+
markers: ["CMakeLists.txt"],
|
|
86
|
+
extensions: [".c", ".cc", ".cpp", ".h", ".hpp"],
|
|
87
|
+
include: ["**/*.{c,cc,cpp,h,hpp}"],
|
|
88
|
+
candidateRoots: ["src", "include"],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const EXT_TO_LANGUAGE = new Map();
|
|
93
|
+
for (const profile of LANGUAGE_PROFILES) {
|
|
94
|
+
for (const ext of profile.extensions) {
|
|
95
|
+
if (!EXT_TO_LANGUAGE.has(ext)) EXT_TO_LANGUAGE.set(ext, profile.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const SKIP_DIRS = new Set([
|
|
100
|
+
".git",
|
|
101
|
+
".hg",
|
|
102
|
+
".svn",
|
|
103
|
+
".repolens",
|
|
104
|
+
".next",
|
|
105
|
+
".venv",
|
|
106
|
+
"venv",
|
|
107
|
+
"node_modules",
|
|
108
|
+
"vendor",
|
|
109
|
+
"target",
|
|
110
|
+
"dist",
|
|
111
|
+
"build",
|
|
112
|
+
"out",
|
|
113
|
+
"coverage",
|
|
114
|
+
"__pycache__",
|
|
115
|
+
".tox",
|
|
116
|
+
".mypy_cache",
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const UNIVERSAL_CANDIDATE_ROOTS = ["src", "app", "lib", "cmd", "internal", "pkg", "packages"];
|
|
120
|
+
|
|
121
|
+
export const UNIVERSAL_INCLUDE = [
|
|
122
|
+
"**/*.{js,jsx,mjs,cjs,ts,tsx,vue,svelte,py,go,rs,java,kt,kts,scala,rb,php,cs,swift,c,cc,cpp,h,hpp}",
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const MAX_WALK_ENTRIES = 8000;
|
|
126
|
+
const MAX_WALK_DEPTH = 4;
|
|
127
|
+
|
|
128
|
+
function markerMatches(marker, rootNames) {
|
|
129
|
+
if (marker.startsWith("*.")) {
|
|
130
|
+
const suffix = marker.slice(1);
|
|
131
|
+
return rootNames.some((name) => name.endsWith(suffix));
|
|
132
|
+
}
|
|
133
|
+
return rootNames.includes(marker);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function rootRank(root) {
|
|
137
|
+
const base = root.split("/").pop() || root;
|
|
138
|
+
if (/^(tests?|specs?|__tests__|testdata)$/i.test(base)) return 2;
|
|
139
|
+
if (/^(docs|examples?|benchmarks?)$/i.test(base)) return 1;
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function compareRoots(a, b) {
|
|
144
|
+
const rank = rootRank(a) - rootRank(b);
|
|
145
|
+
if (rank !== 0) return rank;
|
|
146
|
+
return a.localeCompare(b);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function dirExists(dirPath) {
|
|
150
|
+
try {
|
|
151
|
+
const stat = await fs.stat(dirPath);
|
|
152
|
+
return stat.isDirectory();
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function walkSourceTree(repoRoot) {
|
|
159
|
+
const extCounts = new Map();
|
|
160
|
+
const topLevelExts = new Map();
|
|
161
|
+
const rootNames = [];
|
|
162
|
+
let seen = 0;
|
|
163
|
+
|
|
164
|
+
async function walk(dir, depth) {
|
|
165
|
+
if (depth > MAX_WALK_DEPTH || seen >= MAX_WALK_ENTRIES) return;
|
|
166
|
+
let entries;
|
|
167
|
+
try {
|
|
168
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
169
|
+
} catch {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
if (seen >= MAX_WALK_ENTRIES) return;
|
|
175
|
+
seen += 1;
|
|
176
|
+
if (entry.isSymbolicLink()) continue;
|
|
177
|
+
|
|
178
|
+
if (entry.isDirectory()) {
|
|
179
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
180
|
+
await walk(path.join(dir, entry.name), depth + 1);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!entry.isFile()) continue;
|
|
185
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
186
|
+
if (EXT_TO_LANGUAGE.has(ext)) {
|
|
187
|
+
extCounts.set(ext, (extCounts.get(ext) || 0) + 1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const relDir = path.relative(repoRoot, dir);
|
|
191
|
+
if (!relDir) {
|
|
192
|
+
rootNames.push(entry.name);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const top = relDir.split(path.sep)[0];
|
|
197
|
+
if (!topLevelExts.has(top)) topLevelExts.set(top, new Set());
|
|
198
|
+
if (ext) topLevelExts.get(top).add(ext);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await walk(repoRoot, 0);
|
|
203
|
+
return { extCounts, topLevelExts, rootNames };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Detect languages and the scan globs / module roots that fit this repository.
|
|
208
|
+
* Always returns at least one include pattern and one module root.
|
|
209
|
+
*/
|
|
210
|
+
export async function detectRepoLanguages(repoRoot) {
|
|
211
|
+
const root = path.resolve(repoRoot);
|
|
212
|
+
const { extCounts, topLevelExts, rootNames } = await walkSourceTree(root);
|
|
213
|
+
|
|
214
|
+
const presentIds = new Set();
|
|
215
|
+
for (const ext of extCounts.keys()) {
|
|
216
|
+
const id = EXT_TO_LANGUAGE.get(ext);
|
|
217
|
+
if (id) presentIds.add(id);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const detected = LANGUAGE_PROFILES.filter((profile) => {
|
|
221
|
+
if (presentIds.has(profile.id)) return true;
|
|
222
|
+
return profile.markers.some((marker) => markerMatches(marker, rootNames));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const include = [];
|
|
226
|
+
const seenInclude = new Set();
|
|
227
|
+
const source = detected.length > 0 ? detected.flatMap((profile) => profile.include) : UNIVERSAL_INCLUDE;
|
|
228
|
+
for (const pattern of source) {
|
|
229
|
+
if (seenInclude.has(pattern)) continue;
|
|
230
|
+
seenInclude.add(pattern);
|
|
231
|
+
include.push(pattern);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const sourceExts = new Set();
|
|
235
|
+
const profilesForRoots = detected.length > 0 ? detected : LANGUAGE_PROFILES;
|
|
236
|
+
for (const profile of profilesForRoots) {
|
|
237
|
+
if (detected.length > 0 || presentIds.has(profile.id)) {
|
|
238
|
+
for (const ext of profile.extensions) sourceExts.add(ext);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (detected.length === 0) {
|
|
242
|
+
for (const ext of EXT_TO_LANGUAGE.keys()) sourceExts.add(ext);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const moduleRoots = [];
|
|
246
|
+
for (const [dir, exts] of topLevelExts) {
|
|
247
|
+
if ([...exts].some((ext) => sourceExts.has(ext))) moduleRoots.push(dir);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (moduleRoots.length === 0) {
|
|
251
|
+
const candidates = detected.length > 0
|
|
252
|
+
? [...new Set(detected.flatMap((profile) => profile.candidateRoots))]
|
|
253
|
+
: UNIVERSAL_CANDIDATE_ROOTS;
|
|
254
|
+
for (const candidate of candidates) {
|
|
255
|
+
if (await dirExists(path.join(root, candidate))) moduleRoots.push(candidate);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (moduleRoots.length === 0) moduleRoots.push(".");
|
|
260
|
+
|
|
261
|
+
moduleRoots.sort(compareRoots);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
languages: detected.map((profile) => ({ id: profile.id, label: profile.label })),
|
|
265
|
+
include,
|
|
266
|
+
moduleRoots,
|
|
267
|
+
};
|
|
268
|
+
}
|
package/src/core/scan.js
CHANGED
|
@@ -476,20 +476,25 @@ function isValidRoutePath(p) {
|
|
|
476
476
|
return p.startsWith("/") || /^[a-zA-Z0-9]/.test(p);
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
export async function
|
|
479
|
+
export async function listMatchedFiles(cfg) {
|
|
480
480
|
const repoRoot = cfg.__repoRoot;
|
|
481
|
-
|
|
482
481
|
const rootResolved = path.resolve(repoRoot);
|
|
483
|
-
const files =
|
|
482
|
+
const files = await fg(cfg.scan.include, {
|
|
484
483
|
cwd: rootResolved,
|
|
485
|
-
ignore: cfg.scan
|
|
484
|
+
ignore: cfg.scan?.ignore || [],
|
|
486
485
|
onlyFiles: true,
|
|
487
486
|
followSymbolicLinks: false,
|
|
488
487
|
absolute: false
|
|
489
|
-
})
|
|
488
|
+
});
|
|
489
|
+
return files.filter((file) => isInsideRoot(rootResolved, path.resolve(rootResolved, file)));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export async function scanRepo(cfg) {
|
|
493
|
+
const repoRoot = cfg.__repoRoot;
|
|
494
|
+
const files = await listMatchedFiles(cfg);
|
|
490
495
|
|
|
491
496
|
if (files.length === 0) {
|
|
492
|
-
warn("No files matched the scan patterns.
|
|
497
|
+
warn("No files matched the scan patterns. Check scan.include in .repolens.yml.");
|
|
493
498
|
}
|
|
494
499
|
|
|
495
500
|
// Performance guardrails
|
package/src/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { loadYaml } from "./utils/yaml.js";
|
|
4
4
|
import { loadConfig } from "./core/config.js";
|
|
5
|
+
import { listMatchedFiles } from "./core/scan.js";
|
|
5
6
|
import { detectConfigTypos } from "./core/config-schema.js";
|
|
6
7
|
import { info } from "./utils/logger.js";
|
|
7
8
|
import { forceCheckForUpdates } from "./utils/update-check.js";
|
|
@@ -56,8 +57,11 @@ function ok(message) {
|
|
|
56
57
|
info(`✅ ${message}`);
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
let activeFailures = null;
|
|
61
|
+
|
|
59
62
|
function fail(message) {
|
|
60
63
|
info(`❌ ${message}`);
|
|
64
|
+
if (activeFailures) activeFailures.push(message);
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
function warn(message) {
|
|
@@ -66,6 +70,8 @@ function warn(message) {
|
|
|
66
70
|
|
|
67
71
|
export async function runDoctor(targetDir = process.cwd()) {
|
|
68
72
|
const repoRoot = path.resolve(targetDir);
|
|
73
|
+
const failures = [];
|
|
74
|
+
activeFailures = failures;
|
|
69
75
|
|
|
70
76
|
const repolensConfigPath = path.join(repoRoot, ".repolens.yml");
|
|
71
77
|
const workflowPath = path.join(repoRoot, ".github", "workflows", "repolens.yml");
|
|
@@ -172,6 +178,20 @@ export async function runDoctor(targetDir = process.cwd()) {
|
|
|
172
178
|
fail("outputs.pages is missing or empty");
|
|
173
179
|
hasFailures = true;
|
|
174
180
|
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const matched = await listMatchedFiles(cfg);
|
|
184
|
+
if (matched.length === 0) {
|
|
185
|
+
const patterns = (cfg.scan?.include || []).join(", ") || "(none)";
|
|
186
|
+
fail(`0 files matched scan.include (${patterns}). publish and demo would write empty documentation. Update scan.include, or run \`repolens init --quick\` so the globs match this repository's language.`);
|
|
187
|
+
hasFailures = true;
|
|
188
|
+
} else {
|
|
189
|
+
ok(`scan.include matches ${matched.length} file(s)`);
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
fail(`Could not scan repository: ${error.message}`);
|
|
193
|
+
hasFailures = true;
|
|
194
|
+
}
|
|
175
195
|
}
|
|
176
196
|
|
|
177
197
|
// Validate environment variables for configured publishers
|
|
@@ -259,10 +279,11 @@ export async function runDoctor(targetDir = process.cwd()) {
|
|
|
259
279
|
|
|
260
280
|
if (hasFailures) {
|
|
261
281
|
fail("RepoLens doctor found blocking issues.");
|
|
262
|
-
|
|
263
|
-
return { ok: false };
|
|
282
|
+
activeFailures = null;
|
|
283
|
+
return { ok: false, failures };
|
|
264
284
|
}
|
|
265
285
|
|
|
266
286
|
ok("RepoLens doctor passed.");
|
|
267
|
-
|
|
287
|
+
activeFailures = null;
|
|
288
|
+
return { ok: true, failures };
|
|
268
289
|
}
|
package/src/help.js
CHANGED
|
@@ -1,10 +1,240 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const EXIT_CODES = `Exit codes:
|
|
2
|
+
0 success (including a cancelled wizard, or help with no other work)
|
|
3
|
+
1 runtime error, or an unknown command
|
|
4
|
+
2 invalid flags, missing config, target directory not found,
|
|
5
|
+
a failed doctor check (including 0 files matched),
|
|
6
|
+
demo or publish scanned 0 files, watch has nothing to watch
|
|
7
|
+
(missing module_roots, or scan.include matches no files),
|
|
8
|
+
publish --strict is missing publisher credentials,
|
|
9
|
+
or feedback has no terminal`;
|
|
10
|
+
|
|
11
|
+
const COMMAND_HELP = {
|
|
12
|
+
init: `
|
|
13
|
+
RepoLens init — scaffold RepoLens files
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
repolens init [--target <path>] [--quick]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--target <path> Repository to scaffold (default: current directory)
|
|
20
|
+
--quick Skip the wizard. Markdown publisher only.
|
|
21
|
+
Detects the repo language and writes matching globs.
|
|
22
|
+
--non-interactive Same as --quick
|
|
23
|
+
--verbose Print debug logs
|
|
24
|
+
--help, -h Show this help
|
|
25
|
+
|
|
26
|
+
\`repolens init\` opens the wizard, which can add Notion, Confluence, GitHub
|
|
27
|
+
Wiki, and Obsidian. \`repolens init --quick\` writes a Markdown-only config.
|
|
28
|
+
It detects Python, Go, Rust, Java/Kotlin, JavaScript/TypeScript, Ruby, PHP,
|
|
29
|
+
C#, Swift, and C/C++ (and combinations) and sets scan.include plus
|
|
30
|
+
module_roots from the files it finds. A repository with no recognized
|
|
31
|
+
language gets multi-language globs, not JavaScript-only defaults.
|
|
32
|
+
|
|
33
|
+
The target directory must already exist.
|
|
34
|
+
|
|
35
|
+
${EXIT_CODES}
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
repolens init
|
|
39
|
+
repolens init --quick
|
|
40
|
+
repolens init --target /path/to/repo --quick
|
|
41
|
+
`,
|
|
42
|
+
|
|
43
|
+
doctor: `
|
|
44
|
+
RepoLens doctor — validate a RepoLens setup
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
repolens doctor [--target <path>]
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--target <path> Repository to validate (default: current directory)
|
|
51
|
+
--verbose Print debug logs
|
|
52
|
+
--help, -h Show this help
|
|
53
|
+
|
|
54
|
+
Checks .repolens.yml, the GitHub workflow, publishers, scan.include,
|
|
55
|
+
module_roots, and outputs.pages. A scan that matches 0 files is a blocking
|
|
56
|
+
failure: publish and demo would otherwise write empty docs.
|
|
57
|
+
|
|
58
|
+
The target directory must already exist.
|
|
59
|
+
|
|
60
|
+
${EXIT_CODES}
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
repolens doctor
|
|
64
|
+
repolens doctor --target /path/to/repo
|
|
65
|
+
`,
|
|
66
|
+
|
|
67
|
+
migrate: `
|
|
68
|
+
RepoLens migrate — upgrade workflow files
|
|
69
|
+
|
|
70
|
+
Usage:
|
|
71
|
+
repolens migrate [--target <path>] [--dry-run] [--force]
|
|
72
|
+
|
|
73
|
+
Options:
|
|
74
|
+
--target <path> Repository to migrate (default: current directory)
|
|
75
|
+
--dry-run Preview changes without writing them
|
|
76
|
+
--force Apply changes without a confirmation prompt
|
|
77
|
+
--verbose Print debug logs
|
|
78
|
+
--help, -h Show this help
|
|
79
|
+
|
|
80
|
+
The target directory must already exist.
|
|
81
|
+
|
|
82
|
+
${EXIT_CODES}
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
repolens migrate --dry-run
|
|
86
|
+
repolens migrate --force
|
|
87
|
+
repolens migrate --target /path/to/repo
|
|
88
|
+
`,
|
|
89
|
+
|
|
90
|
+
publish: `
|
|
91
|
+
RepoLens publish — scan, render, and publish documentation
|
|
92
|
+
|
|
93
|
+
Usage:
|
|
94
|
+
repolens publish [--config <path>] [--strict]
|
|
95
|
+
|
|
96
|
+
Options:
|
|
97
|
+
--config <path> Path to .repolens.yml (auto-discovered if omitted)
|
|
98
|
+
--strict Exit 2 when a configured publisher is missing
|
|
99
|
+
credentials (Notion, Confluence, GitHub Wiki) or
|
|
100
|
+
obsidian.vault. Markdown needs no credentials.
|
|
101
|
+
--verbose Print debug logs
|
|
102
|
+
--help, -h Show this help
|
|
103
|
+
|
|
104
|
+
Without --strict, a publisher with missing credentials is skipped.
|
|
105
|
+
A scan that matches 0 files exits 2 and does not write empty docs.
|
|
106
|
+
\`publish\` does not accept --dry-run.
|
|
107
|
+
|
|
108
|
+
${EXIT_CODES}
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
repolens publish
|
|
112
|
+
repolens publish --config /path/.repolens.yml
|
|
113
|
+
repolens publish --strict
|
|
114
|
+
`,
|
|
115
|
+
|
|
116
|
+
demo: `
|
|
117
|
+
RepoLens demo — local preview without API keys
|
|
118
|
+
|
|
119
|
+
Usage:
|
|
120
|
+
repolens demo [--target <path>] [--config <path>]
|
|
121
|
+
|
|
122
|
+
Options:
|
|
123
|
+
--target <path> Repository to preview (default: current directory)
|
|
124
|
+
--config <path> Path to .repolens.yml (discovered from --target)
|
|
125
|
+
--verbose Print debug logs
|
|
126
|
+
--help, -h Show this help
|
|
127
|
+
|
|
128
|
+
With no config, demo detects the repository language and scans matching
|
|
129
|
+
files (Python, Go, Rust, Java, JavaScript, and others). It does not publish
|
|
130
|
+
to Notion or Confluence. A scan that matches 0 files exits 2 and does not
|
|
131
|
+
write empty docs.
|
|
132
|
+
|
|
133
|
+
The target directory must already exist.
|
|
134
|
+
|
|
135
|
+
${EXIT_CODES}
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
repolens demo
|
|
139
|
+
repolens demo --target /path/to/repo
|
|
140
|
+
`,
|
|
141
|
+
|
|
142
|
+
watch: `
|
|
143
|
+
RepoLens watch — regenerate Markdown when files change
|
|
144
|
+
|
|
145
|
+
Usage:
|
|
146
|
+
repolens watch [--config <path>]
|
|
147
|
+
|
|
148
|
+
Options:
|
|
149
|
+
--config <path> Path to .repolens.yml (auto-discovered if omitted)
|
|
150
|
+
--verbose Print debug logs
|
|
151
|
+
--help, -h Show this help
|
|
152
|
+
|
|
153
|
+
Watches directories listed in module_roots and regenerates Markdown when
|
|
154
|
+
files change. Exits 2 when none of those directories exist, and when
|
|
155
|
+
scan.include matches no files (including after init --quick in an empty
|
|
156
|
+
directory). It does not wait for changes in that case. Press Ctrl+C to stop
|
|
157
|
+
when a watch is running.
|
|
158
|
+
|
|
159
|
+
${EXIT_CODES}
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
repolens watch
|
|
163
|
+
repolens watch --config /path/.repolens.yml
|
|
164
|
+
`,
|
|
165
|
+
|
|
166
|
+
uninstall: `
|
|
167
|
+
RepoLens uninstall — remove RepoLens-generated files
|
|
168
|
+
|
|
169
|
+
Usage:
|
|
170
|
+
repolens uninstall [--target <path>] [--force]
|
|
171
|
+
|
|
172
|
+
Options:
|
|
173
|
+
--target <path> Repository to clean (default: current directory)
|
|
174
|
+
--force Remove files without a confirmation prompt
|
|
175
|
+
--verbose Print debug logs
|
|
176
|
+
--help, -h Show this help
|
|
177
|
+
|
|
178
|
+
Removes .repolens/, .repolens.yml, .github/workflows/repolens.yml,
|
|
179
|
+
.env.example, and README.repolens.md when those files are present.
|
|
180
|
+
Outside CI, a closed terminal defaults to keeping the files.
|
|
181
|
+
The target directory must already exist.
|
|
182
|
+
|
|
183
|
+
${EXIT_CODES}
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
repolens uninstall --target /path/to/repo
|
|
187
|
+
repolens uninstall --force
|
|
188
|
+
`,
|
|
189
|
+
|
|
190
|
+
feedback: `
|
|
191
|
+
RepoLens feedback — send feedback to the RepoLens team
|
|
192
|
+
|
|
193
|
+
Usage:
|
|
194
|
+
repolens feedback
|
|
195
|
+
|
|
196
|
+
Options:
|
|
197
|
+
--verbose Print debug logs
|
|
198
|
+
--help, -h Show this help
|
|
199
|
+
|
|
200
|
+
Needs an interactive terminal. CI, pipes, and REPOLENS_NON_INTERACTIVE
|
|
201
|
+
exit 2.
|
|
202
|
+
|
|
203
|
+
${EXIT_CODES}
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
repolens feedback
|
|
207
|
+
`,
|
|
208
|
+
|
|
209
|
+
mcp: `
|
|
210
|
+
RepoLens mcp — stdio MCP server
|
|
211
|
+
|
|
212
|
+
Usage:
|
|
213
|
+
repolens mcp
|
|
214
|
+
|
|
215
|
+
Options:
|
|
216
|
+
--help, -h Show this help
|
|
217
|
+
|
|
218
|
+
Starts the Model Context Protocol server on stdio. \`repolens-mcp\` is the
|
|
219
|
+
same server. Logs go to stderr. The workspace is REPOLENS_WORKSPACE or the
|
|
220
|
+
current directory. Paths cannot leave that directory.
|
|
221
|
+
|
|
222
|
+
${EXIT_CODES}
|
|
223
|
+
|
|
224
|
+
Examples:
|
|
225
|
+
repolens mcp
|
|
226
|
+
`,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const GLOBAL_HELP = `
|
|
3
230
|
RepoLens — Repository Intelligence CLI
|
|
4
231
|
|
|
5
232
|
Usage:
|
|
6
233
|
repolens <command> [options]
|
|
7
234
|
|
|
235
|
+
Run \`repolens <command> --help\` for that command's flags.
|
|
236
|
+
With no command, RepoLens prints this help and does not publish.
|
|
237
|
+
|
|
8
238
|
Commands:
|
|
9
239
|
init Scaffold RepoLens files in your repository
|
|
10
240
|
doctor Validate your RepoLens setup
|
|
@@ -20,37 +250,54 @@ Commands:
|
|
|
20
250
|
|
|
21
251
|
Options:
|
|
22
252
|
--config <path> Path to .repolens.yml (auto-discovered if omitted)
|
|
23
|
-
--target <path> Repository path for init, doctor, migrate, demo, uninstall
|
|
24
|
-
|
|
253
|
+
--target <path> Repository path for init, doctor, migrate, demo, uninstall.
|
|
254
|
+
Must already exist.
|
|
255
|
+
--quick Scaffold without the wizard (also --non-interactive).
|
|
256
|
+
Markdown only; globs follow the detected language.
|
|
257
|
+
--strict publish: exit 2 when publisher credentials are missing
|
|
25
258
|
--dry-run Preview migration changes without writing them
|
|
26
259
|
--force Apply migrate or uninstall without a confirmation prompt
|
|
27
260
|
--verbose Print debug logs and full stacks on unexpected errors
|
|
28
261
|
--version, -v Print version and exit
|
|
29
|
-
--help, -h Show
|
|
262
|
+
--help, -h Show help. After a command, show that command's help.
|
|
30
263
|
|
|
31
264
|
Non-interactive sessions (CI, pipes, or REPOLENS_NON_INTERACTIVE):
|
|
32
265
|
init falls back to --quick instead of waiting on the wizard
|
|
33
266
|
yes/no prompts use their default and do not wait (uninstall defaults to no
|
|
34
267
|
on a closed terminal outside CI; CI still removes files and tells you so)
|
|
35
268
|
feedback exits with code 2 because it needs a terminal
|
|
269
|
+
the ASCII banner is hidden when stdout is not a terminal or CI is set
|
|
36
270
|
|
|
37
|
-
|
|
38
|
-
0 success (including a cancelled wizard)
|
|
39
|
-
1 runtime error, or an unknown command
|
|
40
|
-
2 invalid flags, missing config, or a failed doctor check
|
|
271
|
+
${EXIT_CODES}
|
|
41
272
|
|
|
42
273
|
Examples:
|
|
274
|
+
repolens
|
|
43
275
|
repolens init
|
|
44
276
|
repolens init --quick
|
|
277
|
+
repolens init --help
|
|
45
278
|
repolens init --target /tmp/my-repo
|
|
46
279
|
repolens doctor --target /tmp/my-repo
|
|
47
280
|
repolens migrate --dry-run
|
|
48
281
|
repolens publish
|
|
282
|
+
repolens publish --strict
|
|
49
283
|
repolens publish --config /path/.repolens.yml
|
|
50
284
|
repolens demo
|
|
51
285
|
repolens watch
|
|
52
286
|
repolens uninstall --target /tmp/my-repo
|
|
53
287
|
repolens mcp
|
|
54
288
|
repolens --version
|
|
55
|
-
|
|
289
|
+
`;
|
|
290
|
+
|
|
291
|
+
const KNOWN_COMMANDS = new Set([...Object.keys(COMMAND_HELP), "version", "help"]);
|
|
292
|
+
|
|
293
|
+
export function printHelp(command) {
|
|
294
|
+
if (command && COMMAND_HELP[command]) {
|
|
295
|
+
console.log(COMMAND_HELP[command]);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (command && !KNOWN_COMMANDS.has(command)) {
|
|
299
|
+
console.log(`Unknown command: ${command}`);
|
|
300
|
+
console.log("Run 'repolens help' for the command list.\n");
|
|
301
|
+
}
|
|
302
|
+
console.log(GLOBAL_HELP);
|
|
56
303
|
}
|