openclaw/tools/copilot-enforcement/check-copilot-usage.ts
2026-01-26 11:05:20 +05:30

187 lines
4.3 KiB
TypeScript

import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
type FindingKind = "import" | "dependency";
type Finding = {
file: string;
kind: FindingKind;
specifier: string;
line: number | null;
};
const CODE_EXTENSIONS = new Set([
".ts",
".tsx",
".js",
".jsx",
".mjs",
".cjs",
".mts",
".cts",
]);
const EXCLUDED_DIRS = new Set([
".git",
"node_modules",
"dist",
"vendor",
"coverage",
".turbo",
".next",
"build",
]);
const COPILOT_IMPORT_PATTERN = /(?:from\s+|require\(|import\()\s*["'](@github\/copilot[^"']*)["']/g;
const COPILOT_SIDE_EFFECT_PATTERN = /import\s*["'](@github\/copilot[^"']*)["']/g;
export async function scanCopilotUsage(rootDir: string): Promise<Finding[]> {
const findings: Finding[] = [];
await walkDirectory(rootDir, async (filePath) => {
const relativePath = path.relative(rootDir, filePath);
if (path.basename(filePath) === "package.json") {
const packageFindings = await scanPackageJson(filePath, relativePath);
findings.push(...packageFindings);
return;
}
if (!CODE_EXTENSIONS.has(path.extname(filePath))) {
return;
}
const content = await readFile(filePath, "utf8");
findings.push(...scanSourceFile(content, relativePath));
});
return findings;
}
async function walkDirectory(
dirPath: string,
onFile: (filePath: string) => Promise<void>
): Promise<void> {
const entries = await readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
if (EXCLUDED_DIRS.has(entry.name)) {
continue;
}
await walkDirectory(path.join(dirPath, entry.name), onFile);
continue;
}
if (entry.isFile()) {
await onFile(path.join(dirPath, entry.name));
}
}
}
function scanSourceFile(content: string, relativePath: string): Finding[] {
const findings: Finding[] = [];
for (const pattern of [COPILOT_IMPORT_PATTERN, COPILOT_SIDE_EFFECT_PATTERN]) {
for (const match of content.matchAll(pattern)) {
const specifier = match[1];
if (specifier === "@github/copilot-sdk") {
continue;
}
if (specifier.startsWith("@github/copilot")) {
findings.push({
file: relativePath,
kind: "import",
specifier,
line: getLineNumber(content, match.index ?? 0),
});
}
}
}
return findings;
}
async function scanPackageJson(
filePath: string,
relativePath: string
): Promise<Finding[]> {
const findings: Finding[] = [];
const content = await readFile(filePath, "utf8");
try {
const json = JSON.parse(content) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};
const dependencyBlocks = [
json.dependencies,
json.devDependencies,
json.peerDependencies,
json.optionalDependencies,
];
for (const block of dependencyBlocks) {
if (!block) {
continue;
}
for (const name of Object.keys(block)) {
if (name === "@github/copilot-sdk") {
continue;
}
if (name.startsWith("@github/copilot")) {
findings.push({
file: relativePath,
kind: "dependency",
specifier: name,
line: null,
});
}
}
}
} catch {
return findings;
}
return findings;
}
function getLineNumber(content: string, index: number): number {
return content.slice(0, index).split("\n").length;
}
function formatFindings(findings: Finding[]): string {
return findings
.map((finding) => {
const line = finding.line ? `:${finding.line}` : "";
return `${finding.file}${line} - ${finding.kind} ${finding.specifier}`;
})
.join("\n");
}
async function runCli(): Promise<void> {
const rootDir = process.cwd();
const findings = await scanCopilotUsage(rootDir);
if (findings.length === 0) {
console.log("Copilot SDK check passed.");
return;
}
console.error("Non-SDK Copilot usage detected:\n" + formatFindings(findings));
process.exitCode = 1;
}
const isCli =
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isCli) {
runCli();
}