@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.
@@ -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 scanRepo(cfg) {
479
+ export async function listMatchedFiles(cfg) {
480
480
  const repoRoot = cfg.__repoRoot;
481
-
482
481
  const rootResolved = path.resolve(repoRoot);
483
- const files = (await fg(cfg.scan.include, {
482
+ const files = await fg(cfg.scan.include, {
484
483
  cwd: rootResolved,
485
- ignore: cfg.scan.ignore,
484
+ ignore: cfg.scan?.ignore || [],
486
485
  onlyFiles: true,
487
486
  followSymbolicLinks: false,
488
487
  absolute: false
489
- })).filter((file) => isInsideRoot(rootResolved, path.resolve(rootResolved, file)));
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. Documentation will be mostly empty. Check scan.include in .repolens.yml.");
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
- process.exitCode = 1;
263
- return { ok: false };
282
+ activeFailures = null;
283
+ return { ok: false, failures };
264
284
  }
265
285
 
266
286
  ok("RepoLens doctor passed.");
267
- return { ok: true };
287
+ activeFailures = null;
288
+ return { ok: true, failures };
268
289
  }
package/src/help.js CHANGED
@@ -1,10 +1,240 @@
1
- export function printHelp() {
2
- console.log(`
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
- --quick Scaffold without the wizard (also --non-interactive)
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 this help message
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
- Exit codes:
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
  }