Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

286 lines
7.8 KiB
JavaScript

import { spawn, spawnSync } from "node:child_process";
import {
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
writeFileSync
} from "node:fs";
import { dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = join(scriptDirectory, "..", "..");
const nodeCommand = process.execPath;
const maxParallel = readPositiveIntegerOption("--max-parallel", 4);
const tailLines = readPositiveIntegerOption("--tail-lines", 80);
const tmpRoot = join(workspaceRoot, ".tmp", "typecheck-runs");
const latestSummaryPath = join(tmpRoot, "latest.json");
const packages = loadTypecheckPackages();
if (packages.length === 0) {
console.log("typecheck found no packages to validate");
process.exit(0);
}
const runId = new Date().toISOString().replace(/[:.]/g, "-");
const runDirectory = join(tmpRoot, runId);
mkdirSync(runDirectory, { recursive: true });
const startedAt = Date.now();
const results = await runPackages(packages, runDirectory);
const durationMs = Date.now() - startedAt;
const failures = results.filter((result) => result.exitCode !== 0);
const summary = {
durationMs,
packageCount: packages.length,
runDirectory,
startedAt: new Date(startedAt).toISOString(),
tailLines,
results
};
writeFileSync(
join(runDirectory, "summary.json"),
`${JSON.stringify(summary, null, 2)}\n`
);
mkdirSync(tmpRoot, { recursive: true });
writeFileSync(latestSummaryPath, `${JSON.stringify(summary, null, 2)}\n`);
if (failures.length === 0) {
console.log(
`typecheck passed ${packages.length} package(s) in ${formatDuration(durationMs)}`
);
process.exit(0);
}
console.error(
`typecheck failed ${failures.length}/${packages.length} package(s) in ${formatDuration(durationMs)}`
);
console.error(
`failed packages: ${failures.map((failure) => failure.name).join(", ")}`
);
const diagnostics = collectDiagnostics(failures);
if (diagnostics.length > 0) {
console.error("\n--- diagnostics ---");
console.error(diagnostics.slice(-tailLines).join("\n"));
if (diagnostics.length > tailLines) {
console.error(`\n(showing last ${tailLines}/${diagnostics.length} lines)`);
}
} else {
for (const failure of failures) {
const tail = tailFile(failure.logPath, tailLines);
const header = tail.truncated
? `${failure.name} tail last ${tailLines} lines (full log: ${failure.logPathRelative})`
: `${failure.name} full log`;
console.error(`\n--- ${header} ---`);
console.error(tail.text);
}
}
console.error(`\nfull logs: ${relative(workspaceRoot, runDirectory)}`);
process.exit(1);
function loadTypecheckPackages() {
return gitLines([
"ls-files",
"apps/*/package.json",
"packages/*/*/package.json",
"services/tuttid/builtin-apps/*/package.json",
"tools/fixtures/*/package.json"
])
.map((packageJsonPath) => {
const packageJson = JSON.parse(
readFileSync(join(workspaceRoot, packageJsonPath), "utf8")
);
return {
name: packageJson.name,
root: dirname(packageJsonPath).replaceAll("\\", "/"),
scripts: packageJson.scripts ?? {}
};
})
.filter((packageInfo) => packageInfo.name && packageInfo.scripts.typecheck)
.sort((left, right) => left.root.localeCompare(right.root));
}
async function runPackages(inputPackages, runDirectory) {
const results = [];
let nextIndex = 0;
const workerCount = Math.max(1, Math.min(maxParallel, inputPackages.length));
await Promise.all(
Array.from({ length: workerCount }, async () => {
while (nextIndex < inputPackages.length) {
const packageInfo = inputPackages[nextIndex++];
results.push(await runPackage(packageInfo, runDirectory));
}
})
);
return results.sort((left, right) => left.index - right.index);
}
function runPackage(packageInfo, runDirectory) {
const index = packages.indexOf(packageInfo);
const logPath = join(
runDirectory,
`${sanitizeFileName(packageInfo.root)}.log`
);
const logStream = createWriteStream(logPath, { flags: "w" });
const startedAt = Date.now();
return new Promise((resolve) => {
const args = [
join(workspaceRoot, "tools", "scripts", "run-tsgo-typecheck.mjs"),
"--package-root",
packageInfo.root
];
const child = spawn(nodeCommand, args, {
cwd: workspaceRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"]
});
logStream.write(`$ ${formatCommand([nodeCommand, ...args])}\n\n`);
child.stdout.on("data", (chunk) => logStream.write(chunk));
child.stderr.on("data", (chunk) => logStream.write(chunk));
child.on("error", (error) => {
logStream.write(`\n[runner] ${error.message}\n`);
logStream.end();
resolve(buildPackageResult(packageInfo, index, logPath, startedAt, 1));
});
child.on("close", (code) => {
logStream.end();
const exitCode = typeof code === "number" ? code : 1;
resolve(
buildPackageResult(packageInfo, index, logPath, startedAt, exitCode)
);
});
});
}
function buildPackageResult(packageInfo, index, logPath, startedAt, exitCode) {
return {
durationMs: Date.now() - startedAt,
exitCode,
index,
name: packageInfo.name,
root: packageInfo.root,
logPath,
logPathRelative: relative(workspaceRoot, logPath)
};
}
function collectDiagnostics(failures) {
const diagnostics = [];
const seen = new Set();
for (const failure of failures) {
if (!existsSync(failure.logPath)) {
continue;
}
const content = readFileSync(failure.logPath, "utf8");
for (const line of content.split("\n")) {
const diagnostic = normalizeDiagnosticLine(line, failure.root);
if (!diagnostic || seen.has(diagnostic)) {
continue;
}
seen.add(diagnostic);
diagnostics.push(diagnostic);
}
}
return diagnostics;
}
function normalizeDiagnosticLine(line, packageRoot) {
const trimmed = line.trim();
if (trimmed.length === 0 || trimmed.startsWith("$ ")) {
return null;
}
const match =
/^(?<file>.+?\.(?:cts|mts|ts|tsx))(?<suffix>\(\d+,\d+\): error TS\d+: .*)$/u.exec(
trimmed
);
if (!match?.groups) {
return trimmed;
}
const absolutePath = join(workspaceRoot, packageRoot, match.groups.file);
return `${relative(workspaceRoot, absolutePath).replaceAll("\\", "/")}${match.groups.suffix}`;
}
function gitLines(args) {
const result = spawnSync("git", args, {
cwd: workspaceRoot,
encoding: "utf8"
});
if (result.status !== 0) {
return [];
}
return result.stdout
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
}
function readOption(name) {
const index = process.argv.indexOf(name);
if (index === -1) {
return null;
}
return process.argv[index + 1] ?? null;
}
function readPositiveIntegerOption(name, defaultValue) {
const value = readOption(name);
if (value === null) {
return defaultValue;
}
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
}
function formatCommand(command) {
return command.map(shellQuote).join(" ");
}
function shellQuote(value) {
if (/^[A-Za-z0-9_./:=@+-]+$/u.test(value)) {
return value;
}
return `'${value.replaceAll("'", "'\\''")}'`;
}
function sanitizeFileName(value) {
return value.replace(/[^A-Za-z0-9_.-]+/gu, "-");
}
function formatDuration(durationMs) {
return `${(durationMs / 1000).toFixed(1)}s`;
}
function tailFile(path, lineCount) {
if (!existsSync(path)) {
return { text: "", truncated: false };
}
const content = readFileSync(path, "utf8");
const lines =
content.length === 0
? []
: content.endsWith("\n")
? content.slice(0, -1).split("\n")
: content.split("\n");
const truncated = lines.length > lineCount;
return {
text: lines.slice(-lineCount).join("\n"),
truncated
};
}