docs: document ci dependency docs scripts

This commit is contained in:
Peter Steinberger
2026-06-04 23:42:32 -04:00
parent 6b0ffa2106
commit 980c91d293
20 changed files with 215 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Checks core web-fetch surfaces for provider-owned Firecrawl coupling.
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { collectSourceFileContents } from "./lib/source-file-scan-cache.mjs"; import { collectSourceFileContents } from "./lib/source-file-scan-cache.mjs";
@@ -34,6 +35,9 @@ const suspiciousPatterns = [
let webFetchProviderViolationsPromise; let webFetchProviderViolationsPromise;
/**
* Collects web-fetch provider boundary violations in core source files.
*/
export async function collectWebFetchProviderBoundaryViolations() { export async function collectWebFetchProviderBoundaryViolations() {
if (!webFetchProviderViolationsPromise) { if (!webFetchProviderViolationsPromise) {
webFetchProviderViolationsPromise = (async () => { webFetchProviderViolationsPromise = (async () => {
@@ -81,6 +85,9 @@ export async function collectWebFetchProviderBoundaryViolations() {
return await webFetchProviderViolationsPromise; return await webFetchProviderViolationsPromise;
} }
/**
* Runs the web-fetch provider boundary check.
*/
export async function main(argv, io) { export async function main(argv, io) {
const args = argv ?? process.argv.slice(2); const args = argv ?? process.argv.slice(2);
const json = args.includes("--json"); const json = args.includes("--json");

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Inventories core web-search surfaces that still mention bundled providers.
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -153,6 +154,9 @@ function scanGenericCoreImports(lines, relativeFile, inventory) {
} }
} }
/**
* Collects web-search provider boundary inventory from core source files.
*/
export async function collectWebSearchProviderBoundaryInventory() { export async function collectWebSearchProviderBoundaryInventory() {
if (!webSearchProviderInventoryPromise) { if (!webSearchProviderInventoryPromise) {
webSearchProviderInventoryPromise = (async () => { webSearchProviderInventoryPromise = (async () => {
@@ -184,6 +188,9 @@ export async function collectWebSearchProviderBoundaryInventory() {
return await webSearchProviderInventoryPromise; return await webSearchProviderInventoryPromise;
} }
/**
* Reads the expected web-search provider boundary inventory baseline.
*/
export async function readExpectedInventory() { export async function readExpectedInventory() {
try { try {
return JSON.parse(await fs.readFile(baselinePath, "utf8")); return JSON.parse(await fs.readFile(baselinePath, "utf8"));
@@ -195,6 +202,9 @@ export async function readExpectedInventory() {
} }
} }
/**
* Diffs expected and actual web-search provider boundary inventory entries.
*/
export function diffInventory(expected, actual) { export function diffInventory(expected, actual) {
return diffInventoryEntries(expected, actual, compareInventoryEntries); return diffInventoryEntries(expected, actual, compareInventoryEntries);
} }
@@ -219,6 +229,9 @@ function formatEntry(entry) {
return `${entry.provider} ${entry.file}:${entry.line} ${entry.reason}`; return `${entry.provider} ${entry.file}:${entry.line} ${entry.reason}`;
} }
/**
* Runs the web-search provider boundary baseline check.
*/
export async function runWebSearchProviderBoundaryCheck(argv, io) { export async function runWebSearchProviderBoundaryCheck(argv, io) {
return await runBaselineInventoryCheck({ return await runBaselineInventoryCheck({
argv: argv ?? process.argv.slice(2), argv: argv ?? process.argv.slice(2),
@@ -231,6 +244,9 @@ export async function runWebSearchProviderBoundaryCheck(argv, io) {
}); });
} }
/**
* Entrypoint wrapper for the web-search provider boundary check.
*/
export async function main(argv, io) { export async function main(argv, io) {
const exitCode = await runWebSearchProviderBoundaryCheck(argv, io); const exitCode = await runWebSearchProviderBoundaryCheck(argv, io);
if (!io && exitCode !== 0) { if (!io && exitCode !== 0) {

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Ensures webhook handlers authenticate before reading request bodies.
import path from "node:path"; import path from "node:path";
import ts from "typescript"; import ts from "typescript";
import { bundledPluginCallsite, bundledPluginFile } from "./lib/bundled-plugin-paths.mjs"; import { bundledPluginCallsite, bundledPluginFile } from "./lib/bundled-plugin-paths.mjs";
@@ -29,6 +30,9 @@ function getCalleeName(expression) {
return null; return null;
} }
/**
* Finds request body reads that occur before webhook auth validation.
*/
export function findBlockedWebhookBodyReadLines(content, fileName = "source.ts") { export function findBlockedWebhookBodyReadLines(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true); const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const lines = []; const lines = [];
@@ -45,6 +49,9 @@ export function findBlockedWebhookBodyReadLines(content, fileName = "source.ts")
return lines; return lines;
} }
/**
* Runs the webhook auth/body-order guard.
*/
export async function main() { export async function main() {
await runCallsiteGuard({ await runCallsiteGuard({
importMetaUrl: import.meta.url, importMetaUrl: import.meta.url,

View File

@@ -1,7 +1,11 @@
// Runs the repository check lanes selected by CLI arguments.
import { performance } from "node:perf_hooks"; import { performance } from "node:perf_hooks";
import { printTimingSummary } from "./lib/check-timing-summary.mjs"; import { printTimingSummary } from "./lib/check-timing-summary.mjs";
import { runManagedCommand } from "./lib/managed-child-process.mjs"; import { runManagedCommand } from "./lib/managed-child-process.mjs";
/**
* Returns command usage text for the aggregate check runner.
*/
export function usage() { export function usage() {
return [ return [
"Usage: node scripts/check.mjs [--timed] [--include-architecture] [--include-test-types]", "Usage: node scripts/check.mjs [--timed] [--include-architecture] [--include-test-types]",
@@ -16,6 +20,9 @@ export function usage() {
].join("\n"); ].join("\n");
} }
/**
* Parses aggregate check runner arguments.
*/
export function parseCheckArgs(argv) { export function parseCheckArgs(argv) {
const args = { const args = {
help: false, help: false,
@@ -39,6 +46,9 @@ export function parseCheckArgs(argv) {
return args; return args;
} }
/**
* Runs selected repository check lanes.
*/
export async function main(argv = process.argv.slice(2)) { export async function main(argv = process.argv.slice(2)) {
let args; let args;
try { try {
@@ -158,6 +168,9 @@ async function runSerial(commands) {
return results; return results;
} }
/**
* Runs one managed check command and returns timing/status details.
*/
export async function runCommand(command, runManagedCommandImpl = runManagedCommand) { export async function runCommand(command, runManagedCommandImpl = runManagedCommand) {
const startedAt = performance.now(); const startedAt = performance.now();
let status = 1; let status = 1;

View File

@@ -1,3 +1,4 @@
// Determines CI scope from changed paths.
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs"; import { appendFileSync } from "node:fs";
import { isDirectRunUrl } from "./lib/direct-run.mjs"; import { isDirectRunUrl } from "./lib/direct-run.mjs";
@@ -64,6 +65,9 @@ const NODE_FAST_SCOPE_RE = new RegExp(
* @param {string[]} changedPaths * @param {string[]} changedPaths
* @returns {ChangedScope} * @returns {ChangedScope}
*/ */
/**
* Detects high-level CI scope from changed file paths.
*/
export function detectChangedScope(changedPaths) { export function detectChangedScope(changedPaths) {
if (!Array.isArray(changedPaths) || changedPaths.length === 0) { if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
return { return {
@@ -158,6 +162,9 @@ export function detectChangedScope(changedPaths) {
* @param {string[]} changedPaths * @param {string[]} changedPaths
* @returns {NodeFastScope} * @returns {NodeFastScope}
*/ */
/**
* Detects whether node-fast CI can cover the changed paths.
*/
export function detectNodeFastScope(changedPaths) { export function detectNodeFastScope(changedPaths) {
if (!Array.isArray(changedPaths) || changedPaths.length === 0) { if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
return { runFastOnly: false, runPluginContracts: false, runCiRouting: false }; return { runFastOnly: false, runPluginContracts: false, runCiRouting: false };
@@ -207,6 +214,9 @@ function detectInstallSmokeScopeForPath(path) {
* @param {string[]} changedPaths * @param {string[]} changedPaths
* @returns {InstallSmokeScope} * @returns {InstallSmokeScope}
*/ */
/**
* Detects whether install-smoke CI should run for changed paths.
*/
export function detectInstallSmokeScope(changedPaths) { export function detectInstallSmokeScope(changedPaths) {
if (!Array.isArray(changedPaths) || changedPaths.length === 0) { if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
return { runFastInstallSmoke: true, runFullInstallSmoke: true }; return { runFastInstallSmoke: true, runFullInstallSmoke: true };
@@ -232,6 +242,9 @@ export function detectInstallSmokeScope(changedPaths) {
* @param {string} [cwd] * @param {string} [cwd]
* @returns {string[]} * @returns {string[]}
*/ */
/**
* Lists changed paths for CI base/head inputs.
*/
export function listChangedPaths( export function listChangedPaths(
base, base,
head = "HEAD", head = "HEAD",
@@ -263,6 +276,9 @@ export function listChangedPaths(
* @param {string} [outputPath] * @param {string} [outputPath]
* @param {InstallSmokeScope} [installSmokeScope] * @param {InstallSmokeScope} [installSmokeScope]
*/ */
/**
* Writes CI scope decisions to GitHub Actions output.
*/
export function writeGitHubOutput( export function writeGitHubOutput(
scope, scope,
outputPath = process.env.GITHUB_OUTPUT, outputPath = process.env.GITHUB_OUTPUT,

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Summarizes GitHub Actions run/job timings for CI analysis.
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { parsePositiveInt } from "./lib/numeric-options.mjs"; import { parsePositiveInt } from "./lib/numeric-options.mjs";
@@ -46,6 +47,9 @@ function normalizeRunJob(job) {
}; };
} }
/**
* Flattens paginated GitHub run job responses.
*/
export function collectRunJobsFromPages(pages) { export function collectRunJobsFromPages(pages) {
return pages.flatMap((page) => (Array.isArray(page.jobs) ? page.jobs.map(normalizeRunJob) : [])); return pages.flatMap((page) => (Array.isArray(page.jobs) ? page.jobs.map(normalizeRunJob) : []));
} }
@@ -118,6 +122,9 @@ function collectRunTimingContext(run) {
return { created, jobs, updated }; return { created, jobs, updated };
} }
/**
* Summarizes longest jobs and total timing for a workflow run.
*/
export function summarizeRunTimings(run, limit = 15) { export function summarizeRunTimings(run, limit = 15) {
const { created, jobs, updated } = collectRunTimingContext(run); const { created, jobs, updated } = collectRunTimingContext(run);
const byDuration = [...jobs] const byDuration = [...jobs]
@@ -142,6 +149,9 @@ export function summarizeRunTimings(run, limit = 15) {
}; };
} }
/**
* Summarizes pnpm store warmup overlap near run start.
*/
export function summarizePnpmStoreWarmupBarrier(run, windowSeconds = 5) { export function summarizePnpmStoreWarmupBarrier(run, windowSeconds = 5) {
const { jobs } = collectRunTimingContext(run); const { jobs } = collectRunTimingContext(run);
const preflight = jobs.find((job) => job.name === "preflight"); const preflight = jobs.find((job) => job.name === "preflight");
@@ -182,6 +192,9 @@ export function summarizePnpmStoreWarmupBarrier(run, windowSeconds = 5) {
}; };
} }
/**
* Selects the latest main push CI run, optionally matching a head SHA.
*/
export function selectLatestMainPushCiRun(runs, headSha = null) { export function selectLatestMainPushCiRun(runs, headSha = null) {
const pushRuns = runs.filter((run) => run.event === "push"); const pushRuns = runs.filter((run) => run.event === "push");
if (headSha) { if (headSha) {
@@ -337,6 +350,9 @@ function printSection(title, jobs, metric) {
} }
} }
/**
* Parses CI run timing CLI arguments.
*/
export function parseRunTimingArgs(args) { export function parseRunTimingArgs(args) {
let explicitRunId; let explicitRunId;
let limit = 15; let limit = 15;

View File

@@ -1,3 +1,4 @@
// Finds duplicate PRs after merge and closes overlapping candidates.
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
@@ -10,6 +11,9 @@ Closes explicit duplicate PRs after a landed PR, after verifying the landed PR i
each duplicate has either a shared referenced issue or overlapping changed hunks. Defaults to dry-run.`; each duplicate has either a shared referenced issue or overlapping changed hunks. Defaults to dry-run.`;
} }
/**
* Parses comma-separated PR numbers from CLI/env input.
*/
export function parsePrNumberList(value) { export function parsePrNumberList(value) {
return [ return [
...new Set( ...new Set(
@@ -27,6 +31,9 @@ export function parsePrNumberList(value) {
]; ];
} }
/**
* Parses duplicate PR close workflow arguments.
*/
export function parseArgs(argv, env = process.env) { export function parseArgs(argv, env = process.env) {
const args = { const args = {
apply: false, apply: false,
@@ -109,6 +116,9 @@ function intersectSets(left, right) {
return [...left].filter((value) => right.has(value)); return [...left].filter((value) => right.has(value));
} }
/**
* Parses changed hunk ranges from unified diff text.
*/
export function parseUnifiedDiffRanges(diffText) { export function parseUnifiedDiffRanges(diffText) {
const ranges = new Map(); const ranges = new Map();
let currentPath = null; let currentPath = null;
@@ -136,6 +146,9 @@ export function parseUnifiedDiffRanges(diffText) {
return ranges; return ranges;
} }
/**
* Reports whether two PR diffs touch overlapping hunks.
*/
export function hasOverlappingHunks(leftRanges, rightRanges) { export function hasOverlappingHunks(leftRanges, rightRanges) {
for (const [path, left] of leftRanges) { for (const [path, left] of leftRanges) {
const right = rightRanges.get(path) ?? []; const right = rightRanges.get(path) ?? [];
@@ -182,6 +195,9 @@ Evidence: ${formatEvidence(evidence)}.
Closing #${candidate.number} as a duplicate.`; Closing #${candidate.number} as a duplicate.`;
} }
/**
* Builds the close/skip plan for duplicate PR candidates.
*/
export function buildDuplicateClosePlan({ candidates, diffs, landed, repo }) { export function buildDuplicateClosePlan({ candidates, diffs, landed, repo }) {
if (landed.state !== "MERGED" || !landed.mergedAt) { if (landed.state !== "MERGED" || !landed.mergedAt) {
throw new Error(`#${landed.number} is not merged`); throw new Error(`#${landed.number} is not merged`);
@@ -246,6 +262,9 @@ function loadDiff(repo, number, runGh) {
return runGh(["pr", "diff", String(number), "--repo", repo, "--color=never"]); return runGh(["pr", "diff", String(number), "--repo", repo, "--color=never"]);
} }
/**
* Applies labels/comments/closes for planned duplicate PR actions.
*/
export function applyClosePlan({ labels = DEFAULT_LABELS, plan, repo, runGh }) { export function applyClosePlan({ labels = DEFAULT_LABELS, plan, repo, runGh }) {
for (const item of plan) { for (const item of plan) {
if (item.action !== "close") { if (item.action !== "close") {
@@ -261,6 +280,9 @@ export function applyClosePlan({ labels = DEFAULT_LABELS, plan, repo, runGh }) {
} }
} }
/**
* Runs the duplicate PR close workflow.
*/
export function runDuplicateCloseWorkflow(args, runGh = defaultRunGh) { export function runDuplicateCloseWorkflow(args, runGh = defaultRunGh) {
const landed = loadPr(args.repo, args.landedPr, runGh); const landed = loadPr(args.repo, args.landedPr, runGh);
const candidates = args.duplicates.map((number) => loadPr(args.repo, number, runGh)); const candidates = args.duplicates.map((number) => loadPr(args.repo, number, runGh));

View File

@@ -1,3 +1,4 @@
// Copies bundled plugin metadata into generated runtime locations.
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
@@ -30,6 +31,9 @@ function shouldCopyBundledPluginMetadata(id, env, buildablePluginDirs) {
return env.OPENCLAW_BUILD_PRIVATE_QA === "1"; return env.OPENCLAW_BUILD_PRIVATE_QA === "1";
} }
/**
* Rewrites package extension entries for bundled metadata output.
*/
export function rewritePackageExtensions(entries) { export function rewritePackageExtensions(entries) {
if (!Array.isArray(entries)) { if (!Array.isArray(entries)) {
return undefined; return undefined;
@@ -234,6 +238,9 @@ function copyDeclaredPluginSkillPaths(params) {
* env?: NodeJS.ProcessEnv; * env?: NodeJS.ProcessEnv;
* }} [params] * }} [params]
*/ */
/**
* Copies bundled plugin metadata and package extension files.
*/
export function copyBundledPluginMetadata(params = {}) { export function copyBundledPluginMetadata(params = {}) {
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd(); const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
const env = params.env ?? process.env; const env = params.env ?? process.env;

View File

@@ -1,8 +1,12 @@
// Copies the CommonJS plugin SDK root alias into dist output.
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { resolve } from "node:path"; import { resolve } from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs"; import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";
/**
* Copies the plugin SDK root alias source into the configured output path.
*/
export function copyPluginSdkRootAlias(params = {}) { export function copyPluginSdkRootAlias(params = {}) {
const cwd = params.cwd ?? process.cwd(); const cwd = params.cwd ?? process.cwd();
const source = resolve(cwd, "src/plugin-sdk/root-alias.cjs"); const source = resolve(cwd, "src/plugin-sdk/root-alias.cjs");

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
// Resolves and delegates to the repo-local or PATH crabbox binary.
import { spawn, spawnSync } from "node:child_process"; import { spawn, spawnSync } from "node:child_process";
import { import {
accessSync, accessSync,

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Builds dependency change reports from lockfile and manifest diffs.
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
@@ -41,6 +42,9 @@ function versionsFor(payload, packageName) {
return new Set(payload[packageName] ?? []); return new Set(payload[packageName] ?? []);
} }
/**
* Creates a structured dependency diff report from base/head payloads.
*/
export function createDependencyChangesReport({ export function createDependencyChangesReport({
basePayload, basePayload,
headPayload, headPayload,
@@ -184,10 +188,16 @@ function readGitFile(ref, filePath, cwd) {
}); });
} }
/**
* Reports whether a path is a dependency-related file.
*/
export function isDependencyFile(filePath) { export function isDependencyFile(filePath) {
return DEPENDENCY_FILE_PATTERNS.some((pattern) => pattern.test(filePath)); return DEPENDENCY_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
} }
/**
* Returns git pathspecs used for dependency diff collection.
*/
export function dependencyDiffPathspecs() { export function dependencyDiffPathspecs() {
return [...DEPENDENCY_DIFF_PATHS]; return [...DEPENDENCY_DIFF_PATHS];
} }
@@ -276,6 +286,9 @@ async function writeArtifact(filePath, content) {
await writeFile(filePath, content, "utf8"); await writeFile(filePath, content, "utf8");
} }
/**
* Generates and writes dependency change report artifacts.
*/
export async function runDependencyChangesReport(options) { export async function runDependencyChangesReport(options) {
const headLockfileText = await readFile(path.join(options.rootDir, options.headLockfile), "utf8"); const headLockfileText = await readFile(path.join(options.rootDir, options.headLockfile), "utf8");
const baseLockfileText = options.baseRef const baseLockfileText = options.baseRef
@@ -293,6 +306,9 @@ export async function runDependencyChangesReport(options) {
}); });
} }
/**
* Runs the dependency changes report CLI.
*/
export async function main(argv = process.argv.slice(2)) { export async function main(argv = process.argv.slice(2)) {
const options = parseArgs(argv); const options = parseArgs(argv);
const report = await runDependencyChangesReport(options); const report = await runDependencyChangesReport(options);

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Reports dependency ownership, closure, and risk surface from lockfile data.
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
@@ -36,6 +37,9 @@ function normalizeDependencies(record = {}) {
return entries.toSorted((left, right) => left.name.localeCompare(right.name)); return entries.toSorted((left, right) => left.name.localeCompare(right.name));
} }
/**
* Extracts the package name from a pnpm lockfile package key.
*/
export function packageNameFromLockKey(lockKey) { export function packageNameFromLockKey(lockKey) {
const peerSuffixIndex = lockKey.indexOf("("); const peerSuffixIndex = lockKey.indexOf("(");
const baseKey = peerSuffixIndex >= 0 ? lockKey.slice(0, peerSuffixIndex) : lockKey; const baseKey = peerSuffixIndex >= 0 ? lockKey.slice(0, peerSuffixIndex) : lockKey;
@@ -143,6 +147,9 @@ function collectReportTarget({ repoRoot, packageJson, ownershipPath }) {
}; };
} }
/**
* Collects dependency ownership and transitive surface metadata.
*/
export function collectDependencyOwnershipSurfaceReport(params = {}) { export function collectDependencyOwnershipSurfaceReport(params = {}) {
const repoRoot = path.resolve(params.repoRoot ?? process.cwd()); const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
const packageJson = readJson(path.join(repoRoot, "package.json")); const packageJson = readJson(path.join(repoRoot, "package.json"));
@@ -262,6 +269,9 @@ export function collectDependencyOwnershipSurfaceReport(params = {}) {
}; };
} }
/**
* Collects policy errors from a dependency ownership surface report.
*/
export function collectDependencyOwnershipSurfaceCheckErrors(report) { export function collectDependencyOwnershipSurfaceCheckErrors(report) {
return report.ownershipGaps.map( return report.ownershipGaps.map(
(name) => `root dependency '${name}' is missing from ${DEFAULT_OWNERSHIP_PATH}`, (name) => `root dependency '${name}' is missing from ${DEFAULT_OWNERSHIP_PATH}`,
@@ -289,6 +299,9 @@ function pluralize(count, singular, plural = `${singular}s`) {
return `${count} ${count === 1 ? singular : plural}`; return `${count} ${count === 1 ? singular : plural}`;
} }
/**
* Renders a dependency ownership surface report as Markdown.
*/
export function renderDependencyOwnershipSurfaceMarkdownReport(report) { export function renderDependencyOwnershipSurfaceMarkdownReport(report) {
const lines = [ const lines = [
"# Dependency Ownership and Install Surface Report", "# Dependency Ownership and Install Surface Report",

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Checks resolved dependencies against npm advisory data and gate policy.
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import process from "node:process"; import process from "node:process";
@@ -124,6 +125,9 @@ function sortFindings(findings) {
}); });
} }
/**
* Classifies npm advisory findings into report-only findings and hard blockers.
*/
export function classifyVulnerabilityFindings({ allAdvisories, productionAdvisories }) { export function classifyVulnerabilityFindings({ allAdvisories, productionAdvisories }) {
const allFindings = flattenAdvisories(allAdvisories, "all"); const allFindings = flattenAdvisories(allAdvisories, "all");
const productionFindings = flattenAdvisories(productionAdvisories, "production"); const productionFindings = flattenAdvisories(productionAdvisories, "production");
@@ -150,6 +154,9 @@ function countPayloadVersions(payload) {
return Object.values(payload).reduce((sum, versions) => sum + versions.length, 0); return Object.values(payload).reduce((sum, versions) => sum + versions.length, 0);
} }
/**
* Runs the dependency vulnerability gate against pnpm-lock.yaml.
*/
export async function runDependencyVulnerabilityGate({ export async function runDependencyVulnerabilityGate({
rootDir = process.cwd(), rootDir = process.cwd(),
fetchImpl = fetch, fetchImpl = fetch,
@@ -195,6 +202,9 @@ export async function runDependencyVulnerabilityGate({
}; };
} }
/**
* Renders the dependency vulnerability gate report as Markdown.
*/
export function renderDependencyVulnerabilityGateMarkdownReport(report) { export function renderDependencyVulnerabilityGateMarkdownReport(report) {
const lines = [ const lines = [
"# npm Advisory Vulnerability Gate: Resolved Dependency Graph", "# npm Advisory Vulnerability Gate: Resolved Dependency Graph",

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Audits docs links, routes, redirects, and Mintlify anchors.
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import fs from "node:fs"; import fs from "node:fs";
import os from "node:os"; import os from "node:os";
@@ -48,7 +49,11 @@ function normalizeSlashes(p) {
return p.replace(/\\/g, "/"); return p.replace(/\\/g, "/");
} }
/** @param {string} p */ /**
* Normalizes a docs route by stripping query, hash, and edge slashes.
*
* @param {string} p
*/
export function normalizeRoute(p) { export function normalizeRoute(p) {
const [withoutFragment] = p.split("#"); const [withoutFragment] = p.split("#");
const [withoutQuery] = withoutFragment.split("?"); const [withoutQuery] = withoutFragment.split("?");
@@ -276,6 +281,8 @@ export function sanitizeDocsConfigForEnglishOnly(value) {
} }
/** /**
* Prepares a docs directory, mirroring ClawHub docs when available.
*
* @param {string} [sourceDir] * @param {string} [sourceDir]
* @param {{ * @param {{
* resolveClawHubRepoPathImpl?: typeof resolveClawHubRepoPath; * resolveClawHubRepoPathImpl?: typeof resolveClawHubRepoPath;
@@ -310,6 +317,9 @@ export function prepareMirroredDocsDir(sourceDir = DOCS_DIR, options = {}) {
} }
} }
/**
* Creates an English-only temporary docs tree for Mintlify anchor checks.
*/
export function prepareAnchorAuditDocsDir(sourceDir = DOCS_DIR) { export function prepareAnchorAuditDocsDir(sourceDir = DOCS_DIR) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docs-anchor-audit-")); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docs-anchor-audit-"));
try { try {
@@ -406,6 +416,9 @@ export function resolveMintlifyAnchorAuditInvocation(params) {
return createMintlifyPnpmRunnerSpawnSpec(params); return createMintlifyPnpmRunnerSpawnSpec(params);
} }
/**
* Audits local docs links against route, file, and redirect indexes.
*/
export function auditDocsLinks(options = {}) { export function auditDocsLinks(options = {}) {
const docsDir = options.docsDir ?? DOCS_DIR; const docsDir = options.docsDir ?? DOCS_DIR;
const index = buildAuditIndex(docsDir, { const index = buildAuditIndex(docsDir, {
@@ -537,6 +550,8 @@ export function auditDocsLinks(options = {}) {
} }
/** /**
* Runs the docs link audit CLI.
*
* @param {{ * @param {{
* args?: string[]; * args?: string[];
* comSpec?: string; * comSpec?: string;

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Lists source docs pages and routes for docs-aware tooling.
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path"; import { join, relative } from "node:path";

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Syncs source docs into the generated publish tree.
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
@@ -298,6 +299,9 @@ function getGitHeadSha(repoPath) {
} }
} }
/**
* Resolves the local ClawHub repository path used for docs mirroring.
*/
export function resolveClawHubRepoPath(value = "", options = {}) { export function resolveClawHubRepoPath(value = "", options = {}) {
const required = options.required !== false; const required = options.required !== false;
const candidates = [ const candidates = [
@@ -579,6 +583,9 @@ function rewriteClawHubMarkdownLinks(raw, relativeSourcePath, source) {
}); });
} }
/**
* Mirrors ClawHub docs into the target docs tree.
*/
export function syncClawHubDocsTree(targetDocsDir, options = {}) { export function syncClawHubDocsTree(targetDocsDir, options = {}) {
const repoPath = resolveClawHubRepoPath(options.repoPath || "", { const repoPath = resolveClawHubRepoPath(options.repoPath || "", {
required: options.required !== false, required: options.required !== false,

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Ensures CLI startup benchmark assets are built before checks.
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import path from "node:path"; import path from "node:path";
@@ -25,10 +26,16 @@ function positiveEnvInt(name, env, fallback) {
return value; return value;
} }
/**
* Resolves the CLI startup build timeout from environment.
*/
export function resolveCliStartupBuildTimeoutMs(env = process.env) { export function resolveCliStartupBuildTimeoutMs(env = process.env) {
return positiveEnvInt("OPENCLAW_CLI_STARTUP_BUILD_TIMEOUT_MS", env, DEFAULT_BUILD_TIMEOUT_MS); return positiveEnvInt("OPENCLAW_CLI_STARTUP_BUILD_TIMEOUT_MS", env, DEFAULT_BUILD_TIMEOUT_MS);
} }
/**
* Reports whether required CLI startup build outputs exist.
*/
export function hasCliStartupBuild(params = {}) { export function hasCliStartupBuild(params = {}) {
const rootDir = params.rootDir ?? repoRoot; const rootDir = params.rootDir ?? repoRoot;
const exists = params.existsSync ?? existsSync; const exists = params.existsSync ?? existsSync;
@@ -36,6 +43,9 @@ export function hasCliStartupBuild(params = {}) {
return hasEntry && exists(path.join(rootDir, startupMetadataPath)); return hasEntry && exists(path.join(rootDir, startupMetadataPath));
} }
/**
* Builds CLI startup assets when required outputs are missing.
*/
export function ensureCliStartupBuild(params = {}) { export function ensureCliStartupBuild(params = {}) {
const rootDir = params.rootDir ?? repoRoot; const rootDir = params.rootDir ?? repoRoot;
if (hasCliStartupBuild({ rootDir, existsSync: params.existsSync })) { if (hasCliStartupBuild({ rootDir, existsSync: params.existsSync })) {

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Ensures memory extension runtime entries are built before checks.
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import { existsSync, readdirSync } from "node:fs"; import { existsSync, readdirSync } from "node:fs";
import path from "node:path"; import path from "node:path";
@@ -27,6 +28,9 @@ function positiveEnvInt(name, env, fallback) {
return value; return value;
} }
/**
* Resolves the extension memory build timeout from environment.
*/
export function resolveExtensionMemoryBuildTimeoutMs(env = process.env) { export function resolveExtensionMemoryBuildTimeoutMs(env = process.env) {
return positiveEnvInt( return positiveEnvInt(
"OPENCLAW_EXTENSION_MEMORY_BUILD_TIMEOUT_MS", "OPENCLAW_EXTENSION_MEMORY_BUILD_TIMEOUT_MS",
@@ -52,6 +56,9 @@ function collectExpectedExtensionMemoryEntryIds(rootDir, env) {
} }
} }
/**
* Reports whether built memory extension entries exist.
*/
export function hasBuiltExtensionMemoryEntries(params = {}) { export function hasBuiltExtensionMemoryEntries(params = {}) {
const rootDir = params.rootDir ?? repoRoot; const rootDir = params.rootDir ?? repoRoot;
const exists = params.existsSync ?? existsSync; const exists = params.existsSync ?? existsSync;
@@ -80,6 +87,9 @@ export function hasBuiltExtensionMemoryEntries(params = {}) {
); );
} }
/**
* Builds memory extension entries when required outputs are missing.
*/
export function ensureExtensionMemoryBuild(params = {}) { export function ensureExtensionMemoryBuild(params = {}) {
const rootDir = params.rootDir ?? repoRoot; const rootDir = params.rootDir ?? repoRoot;
if ( if (

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
// Ensures Playwright Chromium is installed or a usable system browser is available.
import { spawnSync as spawnSyncImpl } from "node:child_process"; import { spawnSync as spawnSyncImpl } from "node:child_process";
import { existsSync as existsSyncImpl, realpathSync } from "node:fs"; import { existsSync as existsSyncImpl, realpathSync } from "node:fs";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
@@ -18,6 +19,9 @@ const playwrightInstallWithDepsArgs = [
"chromium", "chromium",
]; ];
const executableOverrideEnvKey = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH"; const executableOverrideEnvKey = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH";
/**
* System Chromium executable paths used before downloading Playwright browsers.
*/
export const systemChromiumExecutableCandidates = [ export const systemChromiumExecutableCandidates = [
"/snap/bin/chromium", "/snap/bin/chromium",
"/usr/bin/chromium-browser", "/usr/bin/chromium-browser",
@@ -26,6 +30,9 @@ export const systemChromiumExecutableCandidates = [
"/usr/bin/google-chrome-stable", "/usr/bin/google-chrome-stable",
]; ];
/**
* Checks whether a Chromium executable can start enough to print its version.
*/
export function canRunChromiumExecutable(executablePath, spawnSync = spawnSyncImpl) { export function canRunChromiumExecutable(executablePath, spawnSync = spawnSyncImpl) {
const result = spawnSync(executablePath, ["--version"], { const result = spawnSync(executablePath, ["--version"], {
stdio: "ignore", stdio: "ignore",
@@ -33,6 +40,9 @@ export function canRunChromiumExecutable(executablePath, spawnSync = spawnSyncIm
return result.status === 0; return result.status === 0;
} }
/**
* Resolves the first runnable system Chromium executable path.
*/
export function resolveSystemChromiumExecutablePath( export function resolveSystemChromiumExecutablePath(
existsSync = existsSyncImpl, existsSync = existsSyncImpl,
spawnSync = spawnSyncImpl, spawnSync = spawnSyncImpl,
@@ -44,6 +54,9 @@ export function resolveSystemChromiumExecutablePath(
); );
} }
/**
* Builds the pnpm runner invocation for Playwright browser install.
*/
export function resolvePlaywrightInstallRunner(options = {}) { export function resolvePlaywrightInstallRunner(options = {}) {
const env = options.env ?? process.env; const env = options.env ?? process.env;
return resolvePnpmRunner({ return resolvePnpmRunner({
@@ -59,6 +72,9 @@ function isTruthyEnvFlag(value) {
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
} }
/**
* Reports whether Linux system dependencies should be installed with Chromium.
*/
export function shouldInstallPlaywrightSystemDependencies(options = {}) { export function shouldInstallPlaywrightSystemDependencies(options = {}) {
const env = options.env ?? process.env; const env = options.env ?? process.env;
const platform = options.platform ?? process.platform; const platform = options.platform ?? process.platform;
@@ -76,6 +92,9 @@ export function shouldInstallPlaywrightSystemDependencies(options = {}) {
); );
} }
/**
* Checks whether this module is the direct script entrypoint.
*/
export function isDirectScriptExecution( export function isDirectScriptExecution(
argvEntry = process.argv[1], argvEntry = process.argv[1],
modulePath = fileURLToPath(import.meta.url), modulePath = fileURLToPath(import.meta.url),
@@ -91,6 +110,9 @@ export function isDirectScriptExecution(
} }
} }
/**
* Ensures a runnable Chromium exists for Playwright-based UI tests.
*/
export function ensurePlaywrightChromium(options = {}) { export function ensurePlaywrightChromium(options = {}) {
const env = options.env ?? process.env; const env = options.env ?? process.env;
const executableOverride = const executableOverride =

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Formats docs Markdown/MDX and repairs Mintlify accordion indentation.
import { execFileSync, spawnSync } from "node:child_process"; import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs"; import fs from "node:fs";
import os from "node:os"; import os from "node:os";