@chappibunny/repolens 1.12.2 → 1.13.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/CHANGELOG.md +100 -0
- package/README.md +25 -36
- package/bin/repolens.js +21 -1
- package/package.json +20 -11
- package/src/ai/provider.js +1 -2
- package/src/analyzers/monorepo-detector.js +8 -20
- package/src/cli-args.js +60 -0
- package/src/cli.js +80 -100
- package/src/core/config-schema.js +47 -1
- package/src/core/config.js +5 -2
- package/src/core/diff.js +43 -20
- package/src/core/scan.js +24 -4
- package/src/delivery/comment.js +5 -2
- package/src/docs/write-doc-set.js +9 -7
- package/src/doctor.js +16 -3
- package/src/help.js +54 -0
- package/src/index.js +13 -0
- package/src/init.js +99 -62
- package/src/integrations/discord.js +16 -2
- package/src/migrate.js +2 -2
- package/src/publishers/confluence.js +17 -3
- package/src/publishers/github-wiki.js +61 -32
- package/src/publishers/index.js +24 -9
- package/src/publishers/markdown.js +3 -2
- package/src/publishers/notion.js +10 -4
- package/src/publishers/obsidian.js +716 -0
- package/src/utils/branch.js +37 -42
- package/src/utils/github-repo.js +8 -0
- package/src/utils/interactive.js +20 -0
- package/src/utils/logger.js +5 -1
- package/src/utils/retry.js +30 -6
- package/src/utils/safe-path.js +50 -0
- package/src/utils/secrets.js +3 -1
- package/src/utils/telemetry.js +34 -6
- package/src/utils/update-check.js +23 -6
- package/src/utils/validate.js +70 -1
- package/src/utils/yaml.js +27 -0
- package/src/watch.js +1 -1
- package/docs/AI.md +0 -329
- package/docs/ARCHITECTURE.md +0 -145
- package/docs/CONFIGURATION.md +0 -121
- package/docs/DEVELOPMENT.md +0 -79
- package/docs/ENVIRONMENT.md +0 -79
- package/docs/INSTALLATION.md +0 -45
- package/docs/KNOWN_ISSUES.md +0 -309
- package/docs/MIGRATION.md +0 -311
- package/docs/ONBOARDING.md +0 -382
- package/docs/PRODUCTION_CHECKLIST.md +0 -470
- package/docs/RELEASE.md +0 -76
- package/docs/ROADMAP.md +0 -151
- package/docs/STABILITY.md +0 -265
- package/docs/TELEMETRY.md +0 -166
- package/docs/TROUBLESHOOTING.md +0 -377
package/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { loadConfig } from "./core/config.js";
|
|
7
7
|
import { scanRepo } from "./core/scan.js";
|
|
@@ -29,6 +29,9 @@ import { generateDocumentSet } from "./docs/generate-doc-set.js";
|
|
|
29
29
|
import { writeDocumentSet } from "./docs/write-doc-set.js";
|
|
30
30
|
import { loadPlugins } from "./plugins/loader.js";
|
|
31
31
|
import { PluginManager } from "./plugins/manager.js";
|
|
32
|
+
import { printHelp } from "./help.js";
|
|
33
|
+
import { parseCliArgs } from "./cli-args.js";
|
|
34
|
+
import { isNonInteractiveEnvironment } from "./utils/interactive.js";
|
|
32
35
|
import {
|
|
33
36
|
initTelemetry,
|
|
34
37
|
captureError,
|
|
@@ -219,56 +222,33 @@ async function findConfig(startDir = process.cwd()) {
|
|
|
219
222
|
}
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
function printHelp() {
|
|
223
|
-
console.log(`
|
|
224
|
-
RepoLens — Repository Intelligence CLI
|
|
225
|
-
|
|
226
|
-
Usage:
|
|
227
|
-
repolens <command> [options]
|
|
228
|
-
|
|
229
|
-
Commands:
|
|
230
|
-
init Scaffold RepoLens files in your repository
|
|
231
|
-
doctor Validate your RepoLens setup
|
|
232
|
-
migrate Upgrade workflow files to current format
|
|
233
|
-
publish Scan, render, and publish documentation
|
|
234
|
-
demo Generate local docs without API keys (quick preview)
|
|
235
|
-
watch Watch for file changes and regenerate docs
|
|
236
|
-
uninstall Remove all RepoLens-generated files from your repository
|
|
237
|
-
feedback Send feedback to the RepoLens team
|
|
238
|
-
version Print the current RepoLens version
|
|
239
|
-
|
|
240
|
-
Options:
|
|
241
|
-
--config Path to .repolens.yml (auto-discovered if not provided)
|
|
242
|
-
--target Target repository path for init/doctor/migrate
|
|
243
|
-
--quick Run init with minimal prompts (skip wizard)
|
|
244
|
-
--dry-run Preview migration changes without applying them
|
|
245
|
-
--force Skip interactive confirmation for migration
|
|
246
|
-
--verbose Enable verbose logging
|
|
247
|
-
--version Print version
|
|
248
|
-
--help Show this help message
|
|
249
|
-
|
|
250
|
-
Examples:
|
|
251
|
-
repolens init # Full interactive wizard (recommended)
|
|
252
|
-
repolens init --quick # Minimal setup, skip wizard
|
|
253
|
-
repolens init --target /tmp/my-repo
|
|
254
|
-
repolens doctor --target /tmp/my-repo
|
|
255
|
-
repolens migrate # Migrate workflows in current directory
|
|
256
|
-
repolens migrate --dry-run # Preview changes without applying
|
|
257
|
-
repolens publish # Auto-discovers .repolens.yml
|
|
258
|
-
repolens publish --config /path/.repolens.yml # Explicit config path
|
|
259
|
-
repolens demo # Quick local preview (no API keys)
|
|
260
|
-
repolens watch # Watch mode (Markdown only)
|
|
261
|
-
repolens uninstall # Remove all RepoLens files
|
|
262
|
-
repolens uninstall --target /tmp/my-repo # Uninstall from specific repo
|
|
263
|
-
repolens --version
|
|
264
|
-
`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
225
|
async function main() {
|
|
268
|
-
const
|
|
226
|
+
const parsed = parseCliArgs(process.argv.slice(2));
|
|
227
|
+
|
|
228
|
+
// Help and version return before telemetry and the update check.
|
|
229
|
+
// bin/repolens.js answers --version/--help without loading this module.
|
|
230
|
+
if (parsed.help) {
|
|
231
|
+
printHelp();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
269
234
|
|
|
270
|
-
|
|
271
|
-
|
|
235
|
+
if (parsed.version) {
|
|
236
|
+
const version = await getPackageVersion();
|
|
237
|
+
console.log(version);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (parsed.errors.length > 0) {
|
|
242
|
+
for (const message of parsed.errors) error(message);
|
|
243
|
+
error("Run 'repolens help' for usage information.");
|
|
244
|
+
process.exit(EXIT_VALIDATION);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const command = parsed.command;
|
|
248
|
+
|
|
249
|
+
// Initialize telemetry (opt-in via REPOLENS_TELEMETRY_ENABLED=true).
|
|
250
|
+
// @sentry/node is optional; init is a no-op when it is not installed.
|
|
251
|
+
await initTelemetry();
|
|
272
252
|
setContext("cli", {
|
|
273
253
|
command: command || "publish",
|
|
274
254
|
args: process.argv.slice(2),
|
|
@@ -279,17 +259,6 @@ async function main() {
|
|
|
279
259
|
// Check for updates (non-blocking, runs in background)
|
|
280
260
|
checkForUpdates().catch(() => {/* Silently fail */});
|
|
281
261
|
|
|
282
|
-
if (command === "--help" || command === "-h" || command === "help") {
|
|
283
|
-
printHelp();
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (command === "--version" || command === "-v" || command === "version") {
|
|
288
|
-
const version = await getPackageVersion();
|
|
289
|
-
console.log(version);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
262
|
if (command === "init") {
|
|
294
263
|
await printBanner();
|
|
295
264
|
const targetDir = getArg("--target") || process.cwd();
|
|
@@ -392,13 +361,6 @@ async function main() {
|
|
|
392
361
|
return;
|
|
393
362
|
}
|
|
394
363
|
|
|
395
|
-
// Reject unknown flags/commands before falling through to publish
|
|
396
|
-
if (command && command.startsWith("--") && !["--help", "-h", "--version", "-v"].includes(command)) {
|
|
397
|
-
error(`Unknown option: ${command}`);
|
|
398
|
-
error("Run 'repolens help' for usage information.");
|
|
399
|
-
process.exit(EXIT_ERROR);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
364
|
if (command === "publish" || !command) {
|
|
403
365
|
await printBanner();
|
|
404
366
|
|
|
@@ -461,7 +423,7 @@ async function main() {
|
|
|
461
423
|
process.exit(EXIT_ERROR);
|
|
462
424
|
}
|
|
463
425
|
|
|
464
|
-
const rawDiff = getGitDiff("origin/main");
|
|
426
|
+
const rawDiff = getGitDiff("origin/main", cfg.__repoRoot || process.cwd());
|
|
465
427
|
const diffData = buildArchitectureDiffData(rawDiff);
|
|
466
428
|
|
|
467
429
|
try {
|
|
@@ -534,10 +496,12 @@ async function main() {
|
|
|
534
496
|
const commandTimer = startTimer("demo");
|
|
535
497
|
const targetDir = getArg("--target") || process.cwd();
|
|
536
498
|
|
|
537
|
-
// Try to load existing config, otherwise use sensible defaults
|
|
499
|
+
// Try to load existing config, otherwise use sensible defaults.
|
|
500
|
+
// Start at --target so a demo of another directory does not pick up
|
|
501
|
+
// the config in the shell's current directory.
|
|
538
502
|
let cfg;
|
|
539
503
|
try {
|
|
540
|
-
const configPath = getArg("--config") || await findConfig();
|
|
504
|
+
const configPath = getArg("--config") || await findConfig(targetDir);
|
|
541
505
|
info(`Using config: ${configPath}`);
|
|
542
506
|
cfg = await loadConfig(configPath);
|
|
543
507
|
} catch {
|
|
@@ -580,7 +544,7 @@ async function main() {
|
|
|
580
544
|
stopTimer(scanTimer);
|
|
581
545
|
info(`Detected ${scan.modules?.length || 0} modules, ${scan.filesCount || 0} files`);
|
|
582
546
|
|
|
583
|
-
const rawDiff = getGitDiff("origin/main");
|
|
547
|
+
const rawDiff = getGitDiff("origin/main", cfg.__repoRoot || targetDir);
|
|
584
548
|
|
|
585
549
|
info("Generating documentation set...");
|
|
586
550
|
const renderTimer = startTimer("render");
|
|
@@ -675,7 +639,7 @@ async function main() {
|
|
|
675
639
|
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST;
|
|
676
640
|
|
|
677
641
|
if (isCI || isTest) {
|
|
678
|
-
info("\
|
|
642
|
+
info("\nNon-interactive environment: removing RepoLens files without a prompt. Pass --force to hide this notice.");
|
|
679
643
|
} else {
|
|
680
644
|
const confirmed = await promptYesNo(`\nRemove ${found.length} item${found.length === 1 ? "" : "s"}? This cannot be undone.`, false);
|
|
681
645
|
|
|
@@ -717,6 +681,11 @@ async function main() {
|
|
|
717
681
|
|
|
718
682
|
if (command === "feedback") {
|
|
719
683
|
await printBanner();
|
|
684
|
+
if (isNonInteractiveEnvironment()) {
|
|
685
|
+
error("feedback needs an interactive terminal.");
|
|
686
|
+
await closeTelemetry();
|
|
687
|
+
process.exit(EXIT_VALIDATION);
|
|
688
|
+
}
|
|
720
689
|
info("Send feedback to the RepoLens team");
|
|
721
690
|
info("Your feedback is anonymous and helps us improve RepoLens.\n");
|
|
722
691
|
|
|
@@ -754,6 +723,8 @@ async function main() {
|
|
|
754
723
|
captureError(err, { command: "feedback" });
|
|
755
724
|
error("Failed to send feedback:");
|
|
756
725
|
error(err.message);
|
|
726
|
+
await closeTelemetry();
|
|
727
|
+
process.exit(EXIT_ERROR);
|
|
757
728
|
}
|
|
758
729
|
|
|
759
730
|
await closeTelemetry();
|
|
@@ -765,32 +736,41 @@ async function main() {
|
|
|
765
736
|
process.exit(EXIT_ERROR);
|
|
766
737
|
}
|
|
767
738
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
739
|
+
export function runCli() {
|
|
740
|
+
return main().catch(async (err) => {
|
|
741
|
+
// Capture unexpected errors
|
|
742
|
+
captureError(err, {
|
|
743
|
+
type: "uncaught",
|
|
744
|
+
command: process.argv[2] || "unknown"
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
console.error("\n❌ RepoLens encountered an unexpected error:\n");
|
|
748
|
+
|
|
749
|
+
if (err.code === "ENOENT") {
|
|
750
|
+
error(formatError("FILE_NOT_FOUND", err.path));
|
|
751
|
+
} else if (err.code === "EACCES") {
|
|
752
|
+
error(formatError("FILE_PERMISSION_DENIED", err.path));
|
|
753
|
+
} else if (err.message) {
|
|
754
|
+
error(err.message);
|
|
755
|
+
} else {
|
|
756
|
+
error(err);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (process.env.VERBOSE || process.argv.includes("--verbose")) {
|
|
760
|
+
console.error("\nStack trace:");
|
|
761
|
+
console.error(err.stack);
|
|
762
|
+
} else {
|
|
763
|
+
console.error("\nRun with --verbose for full error details.");
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
await closeTelemetry();
|
|
767
|
+
process.exit(EXIT_ERROR);
|
|
773
768
|
});
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
error(err.message);
|
|
783
|
-
} else {
|
|
784
|
-
error(err);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (process.env.VERBOSE || process.argv.includes("--verbose")) {
|
|
788
|
-
console.error("\nStack trace:");
|
|
789
|
-
console.error(err.stack);
|
|
790
|
-
} else {
|
|
791
|
-
console.error("\nRun with --verbose for full error details.");
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
await closeTelemetry();
|
|
795
|
-
process.exit(EXIT_ERROR);
|
|
796
|
-
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Run only when this file is the process entry (node src/cli.js).
|
|
772
|
+
// bin/repolens.js imports runCli() explicitly so a package import does not start the CLI.
|
|
773
|
+
const entryArg = process.argv[1];
|
|
774
|
+
if (entryArg && pathToFileURL(path.resolve(entryArg)).href === import.meta.url) {
|
|
775
|
+
runCli();
|
|
776
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const CURRENT_SCHEMA_VERSION = 1;
|
|
11
|
-
const SUPPORTED_PUBLISHERS = ["notion", "markdown", "confluence", "github_wiki"];
|
|
11
|
+
const SUPPORTED_PUBLISHERS = ["notion", "markdown", "confluence", "github_wiki", "obsidian"];
|
|
12
12
|
const SUPPORTED_PAGE_KEYS = [
|
|
13
13
|
"system_overview",
|
|
14
14
|
"module_catalog",
|
|
@@ -218,6 +218,51 @@ export function validateConfig(config) {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
// Validate Obsidian configuration (optional, local vault — no network)
|
|
222
|
+
if (config.obsidian !== undefined) {
|
|
223
|
+
if (typeof config.obsidian !== "object" || Array.isArray(config.obsidian) || config.obsidian === null) {
|
|
224
|
+
errors.push("obsidian must be an object");
|
|
225
|
+
} else {
|
|
226
|
+
if (config.obsidian.vault !== undefined) {
|
|
227
|
+
if (typeof config.obsidian.vault !== "string" || config.obsidian.vault.trim() === "") {
|
|
228
|
+
errors.push("obsidian.vault must be a non-empty string");
|
|
229
|
+
} else if (config.obsidian.vault.includes("\0")) {
|
|
230
|
+
errors.push("obsidian.vault contains a null byte");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (config.obsidian.folder !== undefined) {
|
|
234
|
+
if (typeof config.obsidian.folder !== "string" || config.obsidian.folder.trim() === "") {
|
|
235
|
+
errors.push("obsidian.folder must be a non-empty string");
|
|
236
|
+
} else if (config.obsidian.folder.includes("\0")) {
|
|
237
|
+
errors.push("obsidian.folder contains a null byte");
|
|
238
|
+
} else if (
|
|
239
|
+
config.obsidian.folder.startsWith("/") ||
|
|
240
|
+
config.obsidian.folder.startsWith("\\") ||
|
|
241
|
+
/^[A-Za-z]:[\\/]/.test(config.obsidian.folder) ||
|
|
242
|
+
config.obsidian.folder.split(/[\\/]/).some((part) => part === "..")
|
|
243
|
+
) {
|
|
244
|
+
errors.push("obsidian.folder must be a relative path inside the vault");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (config.obsidian.branches !== undefined) {
|
|
248
|
+
if (!Array.isArray(config.obsidian.branches)) {
|
|
249
|
+
errors.push("obsidian.branches must be an array");
|
|
250
|
+
} else {
|
|
251
|
+
config.obsidian.branches.forEach((branch, idx) => {
|
|
252
|
+
if (typeof branch !== "string") {
|
|
253
|
+
errors.push(`obsidian.branches[${idx}] must be a string`);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const flag of ["index", "frontmatter", "wikilinks"]) {
|
|
259
|
+
if (config.obsidian[flag] !== undefined && typeof config.obsidian[flag] !== "boolean") {
|
|
260
|
+
errors.push(`obsidian.${flag} must be a boolean`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
221
266
|
// Validate Discord configuration (optional)
|
|
222
267
|
if (config.discord !== undefined) {
|
|
223
268
|
if (typeof config.discord !== "object" || Array.isArray(config.discord)) {
|
|
@@ -379,6 +424,7 @@ const VALID_CONFIG_KEYS = [
|
|
|
379
424
|
"notion",
|
|
380
425
|
"confluence",
|
|
381
426
|
"github_wiki",
|
|
427
|
+
"obsidian",
|
|
382
428
|
"discord",
|
|
383
429
|
"features",
|
|
384
430
|
"ai",
|
package/src/core/config.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import yaml from "js-yaml";
|
|
4
3
|
import { validateConfig as validateSchema } from "./config-schema.js";
|
|
5
4
|
import { validateConfig } from "../utils/validate.js";
|
|
6
5
|
import { warn, error } from "../utils/logger.js";
|
|
6
|
+
import { loadYaml } from "../utils/yaml.js";
|
|
7
7
|
|
|
8
8
|
export async function loadConfig(configPath) {
|
|
9
9
|
const absoluteConfigPath = path.resolve(configPath);
|
|
10
10
|
const raw = await fs.readFile(absoluteConfigPath, "utf8");
|
|
11
|
-
const cfg =
|
|
11
|
+
const cfg = loadYaml(raw);
|
|
12
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) {
|
|
13
|
+
throw new Error("Configuration must be a YAML mapping");
|
|
14
|
+
}
|
|
12
15
|
|
|
13
16
|
// Validate config against schema (structure)
|
|
14
17
|
validateSchema(cfg);
|
package/src/core/diff.js
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { warn } from "../utils/logger.js";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const SAFE_REF = /^[A-Za-z0-9._~^/@+-]+$/;
|
|
5
|
+
|
|
6
|
+
function emptyDiff() {
|
|
7
|
+
return { added: [], removed: [], modified: [] };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Classify one `git diff --name-status` line.
|
|
12
|
+
* Renames and copies (`R100`, `C75`) record the destination path.
|
|
13
|
+
*/
|
|
14
|
+
export function classifyDiffLine(line) {
|
|
15
|
+
const parts = String(line || "").split("\t");
|
|
16
|
+
const status = parts[0];
|
|
17
|
+
if (!status) return null;
|
|
18
|
+
|
|
19
|
+
const file = status.startsWith("R") || status.startsWith("C")
|
|
20
|
+
? parts[2] || parts[1]
|
|
21
|
+
: parts[1];
|
|
22
|
+
if (!file) return null;
|
|
23
|
+
|
|
24
|
+
if (status === "A") return { bucket: "added", file };
|
|
25
|
+
if (status === "D") return { bucket: "removed", file };
|
|
26
|
+
return { bucket: "modified", file };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getGitDiff(baseRef = "origin/main", cwd = process.cwd()) {
|
|
30
|
+
if (typeof baseRef !== "string" || baseRef.startsWith("-") || !SAFE_REF.test(baseRef)) {
|
|
31
|
+
warn("git diff skipped: base ref contains unsupported characters.");
|
|
32
|
+
return emptyDiff();
|
|
33
|
+
}
|
|
34
|
+
|
|
5
35
|
let output = "";
|
|
6
36
|
|
|
7
37
|
try {
|
|
8
|
-
|
|
9
|
-
|
|
38
|
+
// Pipe stderr so a missing repo or ref does not dump git's usage text.
|
|
39
|
+
output = execFileSync("git", ["diff", "--name-status", baseRef], {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
cwd,
|
|
42
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
43
|
});
|
|
11
44
|
} catch (error) {
|
|
12
45
|
warn("git diff failed, returning empty diff.");
|
|
13
|
-
return
|
|
14
|
-
added: [],
|
|
15
|
-
removed: [],
|
|
16
|
-
modified: []
|
|
17
|
-
};
|
|
46
|
+
return emptyDiff();
|
|
18
47
|
}
|
|
19
48
|
|
|
20
49
|
const lines = output.split("\n").filter(Boolean);
|
|
@@ -24,17 +53,11 @@ export function getGitDiff(baseRef = "origin/main") {
|
|
|
24
53
|
const modified = [];
|
|
25
54
|
|
|
26
55
|
for (const line of lines) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
added.push(file);
|
|
33
|
-
} else if (status === "D") {
|
|
34
|
-
removed.push(file);
|
|
35
|
-
} else {
|
|
36
|
-
modified.push(file);
|
|
37
|
-
}
|
|
56
|
+
const classified = classifyDiffLine(line);
|
|
57
|
+
if (!classified) continue;
|
|
58
|
+
if (classified.bucket === "added") added.push(classified.file);
|
|
59
|
+
else if (classified.bucket === "removed") removed.push(classified.file);
|
|
60
|
+
else modified.push(classified.file);
|
|
38
61
|
}
|
|
39
62
|
|
|
40
63
|
return {
|
package/src/core/scan.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { info, warn } from "../utils/logger.js";
|
|
5
5
|
import { trackScan } from "../utils/telemetry.js";
|
|
6
6
|
import { detectMonorepo } from "../analyzers/monorepo-detector.js";
|
|
7
|
+
import { isInsideRoot } from "../utils/safe-path.js";
|
|
7
8
|
|
|
8
9
|
const norm = (p) => p.replace(/\\/g, "/");
|
|
9
10
|
|
|
@@ -71,8 +72,20 @@ function isNextPage(file) {
|
|
|
71
72
|
return false;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
const MAX_SOURCE_BYTES = 1_000_000;
|
|
76
|
+
|
|
74
77
|
async function readFileSafe(file) {
|
|
75
78
|
try {
|
|
79
|
+
const stat = await fs.stat(file);
|
|
80
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return "";
|
|
81
|
+
const fh = await fs.open(file, "r");
|
|
82
|
+
try {
|
|
83
|
+
const sample = Buffer.alloc(Math.min(8192, stat.size));
|
|
84
|
+
await fh.read(sample, 0, sample.length, 0);
|
|
85
|
+
if (sample.includes(0)) return "";
|
|
86
|
+
} finally {
|
|
87
|
+
await fh.close();
|
|
88
|
+
}
|
|
76
89
|
return await fs.readFile(file, "utf8");
|
|
77
90
|
} catch {
|
|
78
91
|
return "";
|
|
@@ -466,11 +479,18 @@ function isValidRoutePath(p) {
|
|
|
466
479
|
export async function scanRepo(cfg) {
|
|
467
480
|
const repoRoot = cfg.__repoRoot;
|
|
468
481
|
|
|
469
|
-
const
|
|
470
|
-
|
|
482
|
+
const rootResolved = path.resolve(repoRoot);
|
|
483
|
+
const files = (await fg(cfg.scan.include, {
|
|
484
|
+
cwd: rootResolved,
|
|
471
485
|
ignore: cfg.scan.ignore,
|
|
472
|
-
onlyFiles: true
|
|
473
|
-
|
|
486
|
+
onlyFiles: true,
|
|
487
|
+
followSymbolicLinks: false,
|
|
488
|
+
absolute: false
|
|
489
|
+
})).filter((file) => isInsideRoot(rootResolved, path.resolve(rootResolved, file)));
|
|
490
|
+
|
|
491
|
+
if (files.length === 0) {
|
|
492
|
+
warn("No files matched the scan patterns. Documentation will be mostly empty. Check scan.include in .repolens.yml.");
|
|
493
|
+
}
|
|
474
494
|
|
|
475
495
|
// Performance guardrails
|
|
476
496
|
if (files.length > MAX_FILES_LIMIT) {
|
package/src/delivery/comment.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
1
|
import { log } from "../utils/logger.js";
|
|
3
2
|
import { fetchWithRetry } from "../utils/retry.js";
|
|
3
|
+
import { assertGitHubRepo } from "../utils/github-repo.js";
|
|
4
|
+
import { sanitizeSecrets } from "../utils/secrets.js";
|
|
4
5
|
|
|
5
6
|
function safe(text) {
|
|
6
7
|
return String(text || "").trim();
|
|
@@ -72,7 +73,8 @@ async function githubRequest(method, url, body) {
|
|
|
72
73
|
|
|
73
74
|
if (!res.ok) {
|
|
74
75
|
const text = await res.text();
|
|
75
|
-
|
|
76
|
+
const clipped = sanitizeSecrets(String(text || "").replace(/\s+/g, " ").slice(0, 500));
|
|
77
|
+
throw new Error(`GitHub API error ${res.status}: ${clipped}`);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
return await res.json();
|
|
@@ -114,6 +116,7 @@ export async function upsertPrComment(diff) {
|
|
|
114
116
|
return;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
assertGitHubRepo(repoFull);
|
|
117
120
|
const [owner, repo] = repoFull.split("/");
|
|
118
121
|
const body = buildBody(diff);
|
|
119
122
|
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { info } from "../utils/logger.js";
|
|
6
|
+
import { resolveOutputDir, safeFileName } from "../utils/safe-path.js";
|
|
6
7
|
|
|
7
8
|
export async function writeDocumentSet(docSet, targetDir = process.cwd()) {
|
|
8
9
|
const { documents, artifacts, config } = docSet;
|
|
9
10
|
|
|
10
|
-
// Determine output directory
|
|
11
|
-
const outputDir =
|
|
12
|
-
targetDir,
|
|
11
|
+
// Determine output directory. Reject absolute paths and `..` escapes.
|
|
12
|
+
const outputDir = resolveOutputDir(
|
|
13
|
+
targetDir,
|
|
13
14
|
config.documentation?.output_dir || ".repolens"
|
|
14
15
|
);
|
|
15
16
|
|
|
@@ -20,9 +21,10 @@ export async function writeDocumentSet(docSet, targetDir = process.cwd()) {
|
|
|
20
21
|
|
|
21
22
|
// Write each document
|
|
22
23
|
for (const doc of documents) {
|
|
23
|
-
const
|
|
24
|
+
const fileName = safeFileName(doc.filename);
|
|
25
|
+
const filePath = path.join(outputDir, fileName);
|
|
24
26
|
await fs.writeFile(filePath, doc.content, "utf8");
|
|
25
|
-
info(`✓ Wrote ${
|
|
27
|
+
info(`✓ Wrote ${fileName}`);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
// Write artifacts if enabled
|
|
@@ -59,12 +61,12 @@ export async function writeDocumentSet(docSet, targetDir = process.cwd()) {
|
|
|
59
61
|
return {
|
|
60
62
|
outputDir,
|
|
61
63
|
documentCount: documents.length,
|
|
62
|
-
files: documents.map(d => d.filename)
|
|
64
|
+
files: documents.map(d => safeFileName(d.filename))
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export async function readPreviousDocumentSet(targetDir = process.cwd(), config) {
|
|
67
|
-
const outputDir =
|
|
69
|
+
const outputDir = resolveOutputDir(
|
|
68
70
|
targetDir,
|
|
69
71
|
config.documentation?.output_dir || ".repolens"
|
|
70
72
|
);
|
package/src/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import { loadYaml } from "./utils/yaml.js";
|
|
4
4
|
import { loadConfig } from "./core/config.js";
|
|
5
5
|
import { detectConfigTypos } from "./core/config-schema.js";
|
|
6
6
|
import { info } from "./utils/logger.js";
|
|
@@ -118,7 +118,7 @@ export async function runDoctor(targetDir = process.cwd()) {
|
|
|
118
118
|
let rawConfig = null;
|
|
119
119
|
try {
|
|
120
120
|
const rawYaml = await fs.readFile(repolensConfigPath, "utf8");
|
|
121
|
-
rawConfig =
|
|
121
|
+
rawConfig = loadYaml(rawYaml);
|
|
122
122
|
|
|
123
123
|
// Check for config key typos before validation
|
|
124
124
|
if (rawConfig && typeof rawConfig === "object") {
|
|
@@ -204,6 +204,14 @@ export async function runDoctor(targetDir = process.cwd()) {
|
|
|
204
204
|
);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
if (cfg.publishers.includes("obsidian")) {
|
|
208
|
+
if (typeof cfg.obsidian?.vault === "string" && cfg.obsidian.vault.trim() !== "") {
|
|
209
|
+
ok(`Obsidian vault configured (${cfg.obsidian.vault}) — local publisher, no credentials`);
|
|
210
|
+
} else {
|
|
211
|
+
warn("obsidian.vault is not set — required to publish into an Obsidian vault");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
207
215
|
if (cfg.ai?.enabled || process.env.REPOLENS_AI_ENABLED === "true") {
|
|
208
216
|
const aiProvider = process.env.REPOLENS_AI_PROVIDER || "openai_compatible";
|
|
209
217
|
if (aiProvider === "github") {
|
|
@@ -218,7 +226,12 @@ export async function runDoctor(targetDir = process.cwd()) {
|
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
if (envChecks.length === 0) {
|
|
221
|
-
|
|
229
|
+
const localPublishers = [];
|
|
230
|
+
if (cfg.publishers.includes("markdown")) localPublishers.push("Markdown");
|
|
231
|
+
if (cfg.publishers.includes("obsidian")) localPublishers.push("Obsidian");
|
|
232
|
+
ok(localPublishers.length > 0
|
|
233
|
+
? `No publisher-specific env vars required (${localPublishers.join(" and ")} only)`
|
|
234
|
+
: "No publisher-specific env vars required");
|
|
222
235
|
}
|
|
223
236
|
|
|
224
237
|
for (const check of envChecks) {
|
package/src/help.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function printHelp() {
|
|
2
|
+
console.log(`
|
|
3
|
+
RepoLens — Repository Intelligence CLI
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
repolens <command> [options]
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
init Scaffold RepoLens files in your repository
|
|
10
|
+
doctor Validate your RepoLens setup
|
|
11
|
+
migrate Upgrade workflow files to current format
|
|
12
|
+
publish Scan, render, and publish documentation
|
|
13
|
+
demo Generate local docs without API keys (quick preview)
|
|
14
|
+
watch Watch for file changes and regenerate docs
|
|
15
|
+
uninstall Remove RepoLens-generated files from a repository
|
|
16
|
+
feedback Send feedback to the RepoLens team
|
|
17
|
+
version Print the current RepoLens version
|
|
18
|
+
help Show this help message
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--config <path> Path to .repolens.yml (auto-discovered if omitted)
|
|
22
|
+
--target <path> Repository path for init, doctor, migrate, demo, uninstall
|
|
23
|
+
--quick Scaffold without the wizard (also --non-interactive)
|
|
24
|
+
--dry-run Preview migration changes without writing them
|
|
25
|
+
--force Apply migrate or uninstall without a confirmation prompt
|
|
26
|
+
--verbose Print debug logs and full stacks on unexpected errors
|
|
27
|
+
--version, -v Print version and exit
|
|
28
|
+
--help, -h Show this help message
|
|
29
|
+
|
|
30
|
+
Non-interactive sessions (CI, pipes, or REPOLENS_NON_INTERACTIVE):
|
|
31
|
+
init falls back to --quick instead of waiting on the wizard
|
|
32
|
+
yes/no prompts use their default and do not wait (uninstall defaults to no
|
|
33
|
+
on a closed terminal outside CI; CI still removes files and tells you so)
|
|
34
|
+
feedback exits with code 2 because it needs a terminal
|
|
35
|
+
|
|
36
|
+
Exit codes:
|
|
37
|
+
0 success (including a cancelled wizard)
|
|
38
|
+
1 runtime error, or an unknown command
|
|
39
|
+
2 invalid flags, missing config, or a failed doctor check
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
repolens init
|
|
43
|
+
repolens init --quick
|
|
44
|
+
repolens init --target /tmp/my-repo
|
|
45
|
+
repolens doctor --target /tmp/my-repo
|
|
46
|
+
repolens migrate --dry-run
|
|
47
|
+
repolens publish
|
|
48
|
+
repolens publish --config /path/.repolens.yml
|
|
49
|
+
repolens demo
|
|
50
|
+
repolens watch
|
|
51
|
+
repolens uninstall --target /tmp/my-repo
|
|
52
|
+
repolens --version
|
|
53
|
+
`);
|
|
54
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic entry for @chappibunny/repolens.
|
|
3
|
+
*
|
|
4
|
+
* Importing this module does not start the CLI. The `repolens` binary
|
|
5
|
+
* lives in bin/repolens.js and calls runCli() from src/cli.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { loadConfig } from "./core/config.js";
|
|
9
|
+
export { scanRepo } from "./core/scan.js";
|
|
10
|
+
export { generateDocumentSet } from "./docs/generate-doc-set.js";
|
|
11
|
+
export { writeDocumentSet, readPreviousDocumentSet } from "./docs/write-doc-set.js";
|
|
12
|
+
export { publishDocs } from "./publishers/index.js";
|
|
13
|
+
export { runDoctor } from "./doctor.js";
|