@chappibunny/repolens 1.12.2 → 1.12.4
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 +82 -0
- package/README.md +4 -4
- package/bin/repolens.js +21 -1
- package/package.json +19 -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.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 +2 -2
- package/src/help.js +54 -0
- package/src/index.js +13 -0
- package/src/init.js +38 -41
- 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 +4 -9
- package/src/publishers/markdown.js +3 -2
- package/src/publishers/notion.js +10 -4
- package/src/utils/branch.js +23 -44
- 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 +7 -0
- 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/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") {
|
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";
|
package/src/init.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
|
-
import {
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
5
|
import { info, warn } from "./utils/logger.js";
|
|
6
|
+
import { isNonInteractiveEnvironment } from "./utils/interactive.js";
|
|
6
7
|
|
|
7
8
|
const PUBLISHER_CHOICES = ["markdown", "notion", "confluence", "github_wiki"];
|
|
8
9
|
|
|
@@ -337,8 +338,14 @@ on:
|
|
|
337
338
|
- main
|
|
338
339
|
pull_request:
|
|
339
340
|
|
|
341
|
+
# contents: read checks out the repository.
|
|
342
|
+
# pull-requests: write creates and updates the Architecture Diff
|
|
343
|
+
# comment on pull requests (Issues comments API). Repository write
|
|
344
|
+
# is not required for that comment. Add it only on a job that
|
|
345
|
+
# publishes the GitHub Wiki.
|
|
340
346
|
permissions:
|
|
341
347
|
contents: read
|
|
348
|
+
pull-requests: write
|
|
342
349
|
|
|
343
350
|
jobs:
|
|
344
351
|
publish:
|
|
@@ -461,6 +468,8 @@ For automated publishing on every push:
|
|
|
461
468
|
|
|
462
469
|
3. Push to main branch
|
|
463
470
|
|
|
471
|
+
The workflow token is \`contents: read\` plus \`pull-requests: write\`. That is enough for checkout and the Architecture Diff comment. It cannot push commits. GitHub Wiki publishing pushes a separate wiki repository, so add \`contents: write\` on that job only when you enable the wiki publisher.
|
|
472
|
+
|
|
464
473
|
## 📊 Generated Documentation
|
|
465
474
|
|
|
466
475
|
RepoLens generates documentation in two modes:
|
|
@@ -564,24 +573,27 @@ async function dirExists(dirPath) {
|
|
|
564
573
|
* Open a URL in the default browser (cross-platform).
|
|
565
574
|
*/
|
|
566
575
|
function openUrl(url) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
cmd = `start "" "${url}"`;
|
|
573
|
-
} else {
|
|
574
|
-
cmd = `xdg-open "${url}"`;
|
|
576
|
+
let parsed;
|
|
577
|
+
try {
|
|
578
|
+
parsed = new URL(url);
|
|
579
|
+
} catch {
|
|
580
|
+
return Promise.resolve(false);
|
|
575
581
|
}
|
|
582
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
583
|
+
return Promise.resolve(false);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const platform = process.platform;
|
|
587
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "rundll32" : "xdg-open";
|
|
588
|
+
const args = platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
589
|
+
|
|
576
590
|
return new Promise((resolve) => {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
} else {
|
|
582
|
-
resolve(true);
|
|
583
|
-
}
|
|
591
|
+
const child = spawn(cmd, args, { stdio: "ignore" });
|
|
592
|
+
child.on("error", (err) => {
|
|
593
|
+
warn(`Could not open browser: ${err.message}`);
|
|
594
|
+
resolve(false);
|
|
584
595
|
});
|
|
596
|
+
child.on("close", (code) => resolve(code === 0));
|
|
585
597
|
});
|
|
586
598
|
}
|
|
587
599
|
|
|
@@ -844,23 +856,10 @@ function detectProjectName(repoRoot) {
|
|
|
844
856
|
}
|
|
845
857
|
|
|
846
858
|
async function promptNotionCredentials() {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
process.env.CIRCLECI ||
|
|
852
|
-
process.env.JENKINS_HOME ||
|
|
853
|
-
process.env.CODEBUILD_BUILD_ID;
|
|
854
|
-
|
|
855
|
-
const isTest = process.env.NODE_ENV === 'test' || process.env.VITEST;
|
|
856
|
-
|
|
857
|
-
if (isCI) {
|
|
858
|
-
info("⏭️ Skipping interactive prompts (CI environment detected)");
|
|
859
|
-
return null;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
if (isTest) {
|
|
863
|
-
// Skip silently in test mode
|
|
859
|
+
if (isNonInteractiveEnvironment()) {
|
|
860
|
+
if (process.env.NODE_ENV !== "test" && !process.env.VITEST) {
|
|
861
|
+
info("⏭️ Skipping interactive prompts (non-interactive environment)");
|
|
862
|
+
}
|
|
864
863
|
return null;
|
|
865
864
|
}
|
|
866
865
|
|
|
@@ -943,11 +942,6 @@ async function ensureEnvInGitignore(repoRoot) {
|
|
|
943
942
|
* Returns a structured config that replaces the auto-detected defaults.
|
|
944
943
|
*/
|
|
945
944
|
async function runInteractiveWizard(repoRoot) {
|
|
946
|
-
const isCI = process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI ||
|
|
947
|
-
process.env.CIRCLECI || process.env.JENKINS_HOME || process.env.CODEBUILD_BUILD_ID;
|
|
948
|
-
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST;
|
|
949
|
-
if (isCI || isTest) return null;
|
|
950
|
-
|
|
951
945
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
952
946
|
const ask = (q) => rl.question(q);
|
|
953
947
|
|
|
@@ -1515,14 +1509,17 @@ export async function runInit(targetDir = process.cwd(), options = {}) {
|
|
|
1515
1509
|
// Ensure target directory exists
|
|
1516
1510
|
await fs.mkdir(repoRoot, { recursive: true });
|
|
1517
1511
|
|
|
1518
|
-
// Interactive wizard is
|
|
1512
|
+
// Interactive wizard is the default (--quick skips it).
|
|
1513
|
+
// CI, tests, and closed stdin used to return null from the wizard and
|
|
1514
|
+
// abort before any files were written. Fall through to quick setup instead.
|
|
1519
1515
|
let wizardAnswers = null;
|
|
1520
|
-
if (options.interactive) {
|
|
1516
|
+
if (options.interactive && !isNonInteractiveEnvironment()) {
|
|
1521
1517
|
wizardAnswers = await runInteractiveWizard(repoRoot);
|
|
1522
1518
|
if (!wizardAnswers) {
|
|
1523
|
-
// User cancelled the wizard
|
|
1524
1519
|
return;
|
|
1525
1520
|
}
|
|
1521
|
+
} else if (options.interactive) {
|
|
1522
|
+
info("Non-interactive environment detected — running quick setup instead of the wizard.");
|
|
1526
1523
|
}
|
|
1527
1524
|
|
|
1528
1525
|
// Prompt for Notion credentials interactively (only in non-wizard mode)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Sends rich embed notifications to Discord channels via webhooks
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import fetch from "node-fetch";
|
|
7
6
|
import { info, warn, error } from "../utils/logger.js";
|
|
7
|
+
import { sanitizeSecrets } from "../utils/secrets.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Send a notification to Discord webhook
|
|
@@ -26,15 +26,29 @@ export async function sendDiscordNotification(webhookUrl, payload) {
|
|
|
26
26
|
embeds: [embed],
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
let webhook;
|
|
30
|
+
try {
|
|
31
|
+
webhook = new URL(webhookUrl);
|
|
32
|
+
} catch {
|
|
33
|
+
warn("Discord webhook URL is not a valid URL, skipping notification");
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (webhook.protocol !== "https:") {
|
|
37
|
+
warn("Discord webhook URL must use https, skipping notification");
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
const response = await fetch(webhookUrl, {
|
|
30
42
|
method: "POST",
|
|
31
43
|
headers: { "Content-Type": "application/json" },
|
|
32
44
|
body: JSON.stringify(body),
|
|
45
|
+
signal: AbortSignal.timeout(15000),
|
|
46
|
+
redirect: "error",
|
|
33
47
|
});
|
|
34
48
|
|
|
35
49
|
if (!response.ok) {
|
|
36
50
|
const text = await response.text();
|
|
37
|
-
error(`Discord webhook failed (${response.status}): ${text}`);
|
|
51
|
+
error(`Discord webhook failed (${response.status}): ${sanitizeSecrets(String(text).slice(0, 300))}`);
|
|
38
52
|
return false;
|
|
39
53
|
}
|
|
40
54
|
|
package/src/migrate.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 { info, warn, error as logError } from "./utils/logger.js";
|
|
5
5
|
import { trackMigration } from "./utils/telemetry.js";
|
|
6
6
|
|
|
@@ -141,7 +141,7 @@ async function migrateConfigFile(targetDir, options = {}) {
|
|
|
141
141
|
const raw = await fs.readFile(configPath, "utf8");
|
|
142
142
|
let parsed;
|
|
143
143
|
try {
|
|
144
|
-
parsed =
|
|
144
|
+
parsed = loadYaml(raw);
|
|
145
145
|
} catch {
|
|
146
146
|
warn(` ⚠️ Could not parse ${filename} — skipping config migration`);
|
|
147
147
|
return { migrated: false, filePath: configPath, reason: "parse-error" };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import fetch from "node-fetch";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { log, info, warn } from "../utils/logger.js";
|
|
5
4
|
import { fetchWithRetry } from "../utils/retry.js";
|
|
6
5
|
import { getCurrentBranch, getBranchQualifiedTitle } from "../utils/branch.js";
|
|
7
6
|
import { createRepoLensError } from "../utils/errors.js";
|
|
7
|
+
import { sanitizeSecrets } from "../utils/secrets.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Confluence Publisher for RepoLens
|
|
@@ -50,6 +50,19 @@ function getConfluenceBaseUrl() {
|
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = new URL(url);
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error("CONFLUENCE_URL is not a valid URL");
|
|
58
|
+
}
|
|
59
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
60
|
+
throw new Error("CONFLUENCE_URL must use http or https");
|
|
61
|
+
}
|
|
62
|
+
if (parsed.username || parsed.password) {
|
|
63
|
+
throw new Error("CONFLUENCE_URL must not include embedded credentials. Use CONFLUENCE_EMAIL and CONFLUENCE_API_TOKEN.");
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
// Normalize URL (remove trailing slash, ensure /wiki for Cloud)
|
|
54
67
|
let normalized = url.replace(/\/+$/, "");
|
|
55
68
|
|
|
@@ -84,7 +97,8 @@ async function confluenceRequest(method, endpoint, body = null) {
|
|
|
84
97
|
|
|
85
98
|
if (!res.ok) {
|
|
86
99
|
const text = await res.text();
|
|
87
|
-
|
|
100
|
+
const clipped = sanitizeSecrets(String(text || "").replace(/\s+/g, " ").slice(0, 500));
|
|
101
|
+
let errorMsg = `Confluence API error ${res.status}: ${clipped}`;
|
|
88
102
|
|
|
89
103
|
// Provide helpful error messages
|
|
90
104
|
if (res.status === 401) {
|
|
@@ -114,7 +128,7 @@ async function confluenceRequest(method, endpoint, body = null) {
|
|
|
114
128
|
const CACHE_DIR = path.join(process.cwd(), ".cache");
|
|
115
129
|
|
|
116
130
|
function getCacheFile() {
|
|
117
|
-
const branch = getCurrentBranch();
|
|
131
|
+
const branch = getCurrentBranch().replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80) || "unknown";
|
|
118
132
|
return path.join(CACHE_DIR, `confluence-pages-${branch}.json`);
|
|
119
133
|
}
|
|
120
134
|
|