import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { dirname, join, normalize, relative } from "node:path";
const roots = ["README.md", "README_zh.md", "CONTRIBUTING.md", "docs", "apps/web/README.md", "apps/tui/README.md"].filter(existsSync);
const files = roots.flatMap((root) => collectDocFiles(root)).sort();
const brokenLinks = [];
const forbiddenPublicLinks = [];
const sensitiveMatches = [];
const markdownLinkPattern = /\[[^\]]*]\(([^)]+)\)/g;
for (const file of files) {
const text = readFileSync(file, "utf8");
scanLinks(file, text);
scanSensitiveContent(file, text);
}
if (brokenLinks.length > 0) {
console.error("Documentation link smoke failed:");
for (const link of brokenLinks) {
console.error(`- ${link.file}: ${link.rawTarget} -> ${link.resolvedPath}`);
}
}
if (forbiddenPublicLinks.length > 0) {
console.error("Public documentation must not link to .docs-internal:");
for (const link of forbiddenPublicLinks) {
console.error(`- ${link.file}: ${link.rawTarget}`);
}
}
if (sensitiveMatches.length > 0) {
console.error("Documentation sensitive-content smoke failed:");
for (const match of sensitiveMatches) {
console.error(`- ${match.file}: ${match.label} at line ${match.line}`);
}
}
if (brokenLinks.length > 0 || forbiddenPublicLinks.length > 0 || sensitiveMatches.length > 0) {
process.exit(1);
}
console.log(`Documentation link smoke OK: files=${files.length}`);
function collectDocFiles(path) {
if (!existsSync(path)) {
return [];
}
const stat = statSync(path);
if (stat.isFile()) {
return isDocFile(path) ? [path] : [];
}
if (!stat.isDirectory()) {
return [];
}
return readdirSync(path)
.flatMap((entry) => collectDocFiles(join(path, entry)));
}
function isDocFile(path) {
return path.endsWith(".md") || path.endsWith(".html");
}
function scanLinks(file, text) {
let match;
while ((match = markdownLinkPattern.exec(text)) !== null) {
const rawTarget = match[1]?.trim() ?? "";
if (shouldSkip(rawTarget)) {
continue;
}
const targetPath = rawTarget.split("#")[0] ?? "";
if (!targetPath) {
continue;
}
const resolvedPath = normalize(join(dirname(file), targetPath));
if (isPublicDoc(file) && pointsToInternalDocs(resolvedPath)) {
forbiddenPublicLinks.push({ file, rawTarget });
}
if (!existsSync(resolvedPath)) {
brokenLinks.push({ file, rawTarget, resolvedPath });
}
}
}
function scanSensitiveContent(file, text) {
const checks = [
{ label: "source-sensitive product reference", pattern: /\bDB-GPT(?:-like)?\b/i },
{ label: "source-sensitive reference repo", pattern: /\bReference repo\b/i },
{ label: "source-sensitive wording", pattern: /借鉴|对标|复刻/ },
{ label: "personal Unix path", pattern: /\/home\/[A-Za-z0-9._-]+|\/Users\/[A-Za-z0-9._-]+/ },
{ label: "personal Windows path", pattern: /C:\\Users\\/i },
{ label: "OpenAI-style secret key", pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
{ label: "AWS access key", pattern: /\bAKIA[0-9A-Z]{16}\b/ },
{
label: "literal credential assignment",
pattern: /\b(?:API_KEY|TOKEN|SECRET|PASSWORD|PASSWD)\s*=\s*(?!replace-with-your-key|你的_API_Key|<[^>]+>|\.\.\.)\S+/i
}
];
const publicOnlyChecks = [
{ label: "public release placeholder wording", pattern: /待确认|未确认|未核实|占位|后续补充|可能支持/ }
];
const activeChecks = isPublicDoc(file) ? [...checks, ...publicOnlyChecks] : checks;
const lines = text.split("\n");
for (const [index, line] of lines.entries()) {
for (const check of activeChecks) {
if (check.pattern.test(line)) {
sensitiveMatches.push({ file, label: check.label, line: index + 1 });
}
}
}
}
function isPublicDoc(file) {
return file === "README.md" || file === "README_zh.md" || file.startsWith(`docs${"/"}`);
}
function pointsToInternalDocs(path) {
return relative(".docs-internal", path) === "" || !relative(".docs-internal", path).startsWith("..");
}
function shouldSkip(rawTarget) {
return (
!rawTarget ||
rawTarget.startsWith("#") ||
rawTarget.startsWith("http:") ||
rawTarget.startsWith("https:") ||
rawTarget.startsWith("mailto:") ||
rawTarget.startsWith("app://")
);
}