chore: initialize my-pi extension bundle

This commit is contained in:
云服务部-叶林立
2026-08-18 12:30:48 +08:00
parent 7b7b332912
commit 0637139a16
62 changed files with 11055 additions and 1 deletions
+13
View File
@@ -0,0 +1,13 @@
export function stripAnsi(text: string): string {
return text
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
.replace(/\x1b\][0-9;]*(?:\x07|\x1b\\)/g, "")
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
}
export function stripAnsiFast(text: string): string {
if (!text.includes("\x1b")) {
return text;
}
return stripAnsi(text);
}
+155
View File
@@ -0,0 +1,155 @@
import { matchesCommandPatterns } from "./command-detection.js";
interface BuildStats {
compiled: number;
errors: string[][];
warnings: string[];
}
const BUILD_COMMAND_PATTERNS = [
/^cargo\s+(build|check)\b/,
/^bun\s+build\b/,
/^npm\s+run\s+build\b/,
/^yarn\s+build\b/,
/^pnpm\s+build\b/,
/^(?:npx\s+)?tsc\b/,
/^make\b/,
/^cmake\b/,
/^gradle\b/,
/^mvn\b/,
/^go\s+(build|install)\b/,
/^python\s+setup\.py\s+build\b/,
/^pip\s+install\b/,
] as const;
const SKIP_PATTERNS = [
/^\s*Compiling\s+/,
/^\s*Checking\s+/,
/^\s*Downloading\s+/,
/^\s*Downloaded\s+/,
/^\s*Fetching\s+/,
/^\s*Fetched\s+/,
/^\s*Updating\s+/,
/^\s*Updated\s+/,
/^\s*Building\s+/,
/^\s*Generated\s+/,
/^\s*Creating\s+/,
/^\s*Running\s+/,
];
const ERROR_START_PATTERNS = [/^error\[/, /^error:/, /^\[ERROR\]/, /^FAIL/];
const WARNING_PATTERNS = [/^warning:/, /^\[WARNING\]/, /^warn:/];
function isSkipLine(line: string): boolean {
return SKIP_PATTERNS.some((pattern) => pattern.test(line));
}
function isErrorStart(line: string): boolean {
return ERROR_START_PATTERNS.some((pattern) => pattern.test(line));
}
function isWarning(line: string): boolean {
return WARNING_PATTERNS.some((pattern) => pattern.test(line));
}
export function isBuildCommand(command: string | undefined | null): boolean {
return matchesCommandPatterns(command, BUILD_COMMAND_PATTERNS);
}
export function filterBuildOutput(output: string, command: string | undefined | null): string | null {
if (!isBuildCommand(command)) {
return null;
}
const lines = output.split("\n");
const stats: BuildStats = {
compiled: 0,
errors: [],
warnings: [],
};
let inErrorBlock = false;
let currentError: string[] = [];
let blankCount = 0;
for (const line of lines) {
if (line.match(/^\s*(Compiling|Checking|Building)\s+/)) {
stats.compiled++;
continue;
}
if (isSkipLine(line)) {
continue;
}
if (isErrorStart(line)) {
if (inErrorBlock && currentError.length > 0) {
stats.errors.push([...currentError]);
}
inErrorBlock = true;
currentError = [line];
blankCount = 0;
continue;
}
if (isWarning(line)) {
stats.warnings.push(line);
continue;
}
if (!inErrorBlock) {
continue;
}
if (line.trim() === "") {
blankCount++;
if (blankCount >= 2 && currentError.length > 3) {
stats.errors.push([...currentError]);
inErrorBlock = false;
currentError = [];
} else {
currentError.push(line);
}
continue;
}
if (line.match(/^\s/) || line.match(/^-->/)) {
currentError.push(line);
blankCount = 0;
continue;
}
stats.errors.push([...currentError]);
inErrorBlock = false;
currentError = [];
}
if (inErrorBlock && currentError.length > 0) {
stats.errors.push(currentError);
}
if (stats.errors.length === 0 && stats.warnings.length === 0) {
return `[OK] Build successful (${stats.compiled} units compiled)`;
}
const result: string[] = [];
if (stats.errors.length > 0) {
result.push(`[ERROR] ${stats.errors.length} error(s):`);
for (const error of stats.errors.slice(0, 5)) {
result.push(...error.slice(0, 10));
if (error.length > 10) {
result.push(" ...");
}
}
if (stats.errors.length > 5) {
result.push(`... and ${stats.errors.length - 5} more errors`);
}
}
if (stats.warnings.length > 0) {
result.push(`\n[WARN] ${stats.warnings.length} warning(s)`);
}
return result.join("\n");
}
@@ -0,0 +1,53 @@
const ENV_PREFIX_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+)*/;
const CHAIN_OPERATORS = ["&&", "||", ";", "|"] as const;
function sliceFirstSegment(command: string): string {
let cutIndex = -1;
for (const operator of CHAIN_OPERATORS) {
const index = command.indexOf(operator);
if (index === -1) {
continue;
}
if (cutIndex === -1 || index < cutIndex) {
cutIndex = index;
}
}
if (cutIndex === -1) {
return command;
}
return command.slice(0, cutIndex);
}
export function normalizeCommandForDetection(command: string | undefined | null): string | null {
if (typeof command !== "string") {
return null;
}
const firstNonEmptyLine = command
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.length > 0);
if (!firstNonEmptyLine) {
return null;
}
const withoutEnvPrefix = firstNonEmptyLine.replace(ENV_PREFIX_PATTERN, "").trim();
if (!withoutEnvPrefix) {
return null;
}
const firstSegment = sliceFirstSegment(withoutEnvPrefix).trim().toLowerCase();
return firstSegment || null;
}
export function matchesCommandPatterns(
command: string | undefined | null,
patterns: readonly RegExp[],
): boolean {
const normalized = normalizeCommandForDetection(command);
if (!normalized) {
return false;
}
return patterns.some((pattern) => pattern.test(normalized));
}
+231
View File
@@ -0,0 +1,231 @@
import { matchesCommandPatterns, normalizeCommandForDetection } from "./command-detection.js";
const GIT_COMMAND_PATTERNS = [/^git\s+(diff|status|log|show|stash)\b/] as const;
const RAW_GIT_DIFF_PATTERN = /^diff --git /m;
const RAW_GIT_STATUS_PATTERN = /^(?:## |(?:M|A|D|R|C|U|\?| )\S)/m;
export function isGitCommand(command: string | undefined | null): boolean {
return matchesCommandPatterns(command, GIT_COMMAND_PATTERNS);
}
export function compactDiff(output: string, maxLines = 50): string {
const lines = output.split("\n");
const result: string[] = [];
let currentFile = "";
let added = 0;
let removed = 0;
let inHunk = false;
let hunkLines = 0;
const maxHunkLines = 10;
for (const line of lines) {
if (result.length >= maxLines) {
result.push("\n... (more changes truncated)");
break;
}
if (line.startsWith("diff --git")) {
if (currentFile && (added > 0 || removed > 0)) {
result.push(` +${added} -${removed}`);
}
const match = line.match(/diff --git a\/(.+) b\/(.+)/);
currentFile = match?.[2] ?? "unknown";
result.push(`\n> ${currentFile}`);
added = 0;
removed = 0;
inHunk = false;
continue;
}
if (line.startsWith("@@")) {
inHunk = true;
hunkLines = 0;
const hunkInfo = line.match(/@@ .+ @@/)?.[0] ?? "@@";
result.push(` ${hunkInfo}`);
continue;
}
if (!inHunk) {
continue;
}
if (line.startsWith("+") && !line.startsWith("+++")) {
added++;
if (hunkLines < maxHunkLines) {
result.push(` ${line}`);
hunkLines++;
}
} else if (line.startsWith("-") && !line.startsWith("---")) {
removed++;
if (hunkLines < maxHunkLines) {
result.push(` ${line}`);
hunkLines++;
}
} else if (hunkLines < maxHunkLines && !line.startsWith("\\")) {
if (hunkLines > 0) {
result.push(` ${line}`);
hunkLines++;
}
}
if (hunkLines === maxHunkLines) {
result.push(" ... (truncated)");
hunkLines++;
}
}
if (currentFile && (added > 0 || removed > 0)) {
result.push(` +${added} -${removed}`);
}
return result.join("\n");
}
interface StatusStats {
staged: number;
modified: number;
untracked: number;
conflicts: number;
stagedFiles: string[];
modifiedFiles: string[];
untrackedFiles: string[];
}
export function compactStatus(output: string): string {
const lines = output.split("\n");
if (lines.length === 0 || (lines.length === 1 && lines[0]?.trim() === "")) {
return "Clean working tree";
}
const stats: StatusStats = {
staged: 0,
modified: 0,
untracked: 0,
conflicts: 0,
stagedFiles: [],
modifiedFiles: [],
untrackedFiles: [],
};
let branchName = "";
for (const line of lines) {
if (line.startsWith("##")) {
const match = line.match(/## (.+)/);
if (match?.[1]) {
branchName = match[1].split("...")[0] ?? match[1];
}
continue;
}
if (line.length < 3) {
continue;
}
const status = line.slice(0, 2);
const filename = line.slice(3);
const indexStatus = status[0];
const worktreeStatus = status[1];
if (["M", "A", "D", "R", "C"].includes(indexStatus)) {
stats.staged++;
stats.stagedFiles.push(filename);
}
if (indexStatus === "U") {
stats.conflicts++;
}
if (["M", "D"].includes(worktreeStatus)) {
stats.modified++;
stats.modifiedFiles.push(filename);
}
if (status === "??") {
stats.untracked++;
stats.untrackedFiles.push(filename);
}
}
let result = `Branch: ${branchName}\n`;
if (stats.staged > 0) {
result += `Staged: ${stats.staged} files\n`;
for (const file of stats.stagedFiles.slice(0, 5)) {
result += ` ${file}\n`;
}
if (stats.staged > 5) {
result += ` ... +${stats.staged - 5} more\n`;
}
}
if (stats.modified > 0) {
result += `Modified: ${stats.modified} files\n`;
for (const file of stats.modifiedFiles.slice(0, 5)) {
result += ` ${file}\n`;
}
if (stats.modified > 5) {
result += ` ... +${stats.modified - 5} more\n`;
}
}
if (stats.untracked > 0) {
result += `Untracked: ${stats.untracked} files\n`;
for (const file of stats.untrackedFiles.slice(0, 3)) {
result += ` ${file}\n`;
}
if (stats.untracked > 3) {
result += ` ... +${stats.untracked - 3} more\n`;
}
}
if (stats.conflicts > 0) {
result += `Conflicts: ${stats.conflicts} files\n`;
}
return result.trim();
}
export function compactLog(output: string, limit = 20): string {
const lines = output.split("\n");
const result: string[] = [];
for (const line of lines.slice(0, limit)) {
if (line.length > 80) {
result.push(`${line.slice(0, 77)}...`);
} else {
result.push(line);
}
}
if (lines.length > limit) {
result.push(`... and ${lines.length - limit} more commits`);
}
return result.join("\n");
}
export function compactGitOutput(output: string, command: string | undefined | null): string | null {
if (!isGitCommand(command)) {
return null;
}
const normalized = normalizeCommandForDetection(command);
if (!normalized) {
return null;
}
if (normalized.startsWith("git diff")) {
return RAW_GIT_DIFF_PATTERN.test(output) ? compactDiff(output) : null;
}
if (normalized.startsWith("git status")) {
return RAW_GIT_STATUS_PATTERN.test(output) ? compactStatus(output) : null;
}
if (normalized.startsWith("git log")) {
return compactLog(output);
}
return null;
}
+8
View File
@@ -0,0 +1,8 @@
export { stripAnsiFast } from "./ansi.js";
export { truncate } from "./truncate.js";
export { filterBuildOutput } from "./build.js";
export { aggregateTestOutput } from "./test-output.js";
export { aggregateLinterOutput } from "./linter.js";
export { detectLanguage, smartTruncate, filterSourceCode } from "./source.js";
export { compactGitOutput } from "./git.js";
export { groupSearchResults } from "./search.js";
+151
View File
@@ -0,0 +1,151 @@
import { matchesCommandPatterns, normalizeCommandForDetection } from "./command-detection.js";
import { compactPath } from "./path-utils.js";
const LINTER_COMMAND_PATTERNS = [
/^(?:pnpm\s+)?(?:npx\s+)?eslint\b/,
/^(?:npx\s+)?prettier\b/,
/^ruff\b/,
/^pylint\b/,
/^mypy\b/,
/^flake8\b/,
/^black\b/,
/^cargo\s+clippy\b/,
/^golangci-lint\b/,
] as const;
interface Issue {
severity: "ERROR" | "WARNING";
rule: string;
file: string;
line?: number;
message: string;
}
export function isLinterCommand(command: string | undefined | null): boolean {
return matchesCommandPatterns(command, LINTER_COMMAND_PATTERNS);
}
function parseLine(line: string): Issue | null {
const fileLinePattern = /^(.+):(\d+):(\d+):\s*(.+)$/;
const rustPattern = /^(error|warning):\s*(.+?)\s+at\s+(.+):(\d+):(\d+)$/;
const fileLineMatch = line.match(fileLinePattern);
if (fileLineMatch) {
const file = fileLineMatch[1] ?? "unknown";
const lineNumber = Number.parseInt(fileLineMatch[2] ?? "0", 10);
const content = fileLineMatch[4] ?? line;
const severity = /warning/i.test(content) ? "WARNING" : "ERROR";
const rule = content.match(/\[(.+?)\]$/)?.[1] ?? "unknown";
return {
severity,
rule,
file,
line: Number.isNaN(lineNumber) ? undefined : lineNumber,
message: content,
};
}
const rustMatch = line.match(rustPattern);
if (rustMatch) {
const severity = (rustMatch[1]?.toUpperCase() ?? "ERROR") as "ERROR" | "WARNING";
const message = rustMatch[2] ?? line;
const file = rustMatch[3] ?? "unknown";
const lineNumber = Number.parseInt(rustMatch[4] ?? "0", 10);
return {
severity,
rule: "unknown",
file,
line: Number.isNaN(lineNumber) ? undefined : lineNumber,
message,
};
}
return null;
}
function parseIssues(output: string): Issue[] {
const issues: Issue[] = [];
for (const line of output.split("\n")) {
const parsed = parseLine(line);
if (parsed) {
issues.push(parsed);
}
}
return issues;
}
function detectLinterType(command: string | undefined | null): string {
const normalized = normalizeCommandForDetection(command);
if (!normalized) {
return "Linter";
}
if (/(?:^|\s)eslint\b/.test(normalized)) return "ESLint";
if (/^ruff\b/.test(normalized)) return "Ruff";
if (/^pylint\b/.test(normalized)) return "Pylint";
if (/^mypy\b/.test(normalized)) return "MyPy";
if (/^flake8\b/.test(normalized)) return "Flake8";
if (/clippy\b/.test(normalized)) return "Clippy";
if (/^golangci-lint\b/.test(normalized)) return "GolangCI-Lint";
if (/prettier\b/.test(normalized)) return "Prettier";
return "Linter";
}
export function aggregateLinterOutput(output: string, command: string | undefined | null): string | null {
if (!isLinterCommand(command)) {
return null;
}
const linterType = detectLinterType(command);
const issues = parseIssues(output);
if (issues.length === 0) {
return `[OK] ${linterType}: No issues found`;
}
const errors = issues.filter((issue) => issue.severity === "ERROR").length;
const warnings = issues.filter((issue) => issue.severity === "WARNING").length;
const byRule = new Map<string, number>();
for (const issue of issues) {
byRule.set(issue.rule, (byRule.get(issue.rule) ?? 0) + 1);
}
const byFile = new Map<string, Issue[]>();
for (const issue of issues) {
const existing = byFile.get(issue.file) ?? [];
existing.push(issue);
byFile.set(issue.file, existing);
}
let result = `${linterType}: ${errors} errors, ${warnings} warnings in ${byFile.size} files\n`;
result += "═══════════════════════════════════════\n";
result += "Top rules:\n";
const sortedRules = Array.from(byRule.entries())
.sort((left, right) => right[1] - left[1])
.slice(0, 10);
for (const [rule, count] of sortedRules) {
result += ` ${rule} (${count}x)\n`;
}
result += "\nTop files:\n";
const sortedFiles = Array.from(byFile.entries())
.sort((left, right) => right[1].length - left[1].length)
.slice(0, 10);
for (const [file, fileIssues] of sortedFiles) {
result += ` ${compactPath(file, 40)} (${fileIssues.length} issues)\n`;
const fileRules = new Map<string, number>();
for (const issue of fileIssues) {
fileRules.set(issue.rule, (fileRules.get(issue.rule) ?? 0) + 1);
}
const topRules = Array.from(fileRules.entries())
.sort((left, right) => right[1] - left[1])
.slice(0, 3);
for (const [rule, count] of topRules) {
result += ` ${rule} (${count})\n`;
}
}
return result;
}
@@ -0,0 +1,67 @@
function detectPathSeparator(path: string): "/" | "\\" {
return path.includes("\\") && !path.includes("/") ? "\\" : "/";
}
function detectPathPrefix(path: string, separator: "/" | "\\"): string {
if (/^[A-Za-z]:[\\/]/.test(path)) {
return `${path.slice(0, 2)}${separator}`;
}
if (path.startsWith("\\\\") || path.startsWith("//")) {
const parts = path.split(/[\\/]+/).filter((part) => part.length > 0);
if (parts.length >= 2) {
return `${separator}${separator}${parts[0]}${separator}${parts[1]}${separator}`;
}
return `${separator}${separator}`;
}
if (path.startsWith("/") || path.startsWith("\\")) {
return separator;
}
return "";
}
function joinPathSegments(prefix: string, separator: "/" | "\\", segments: string[]): string {
if (segments.length === 0) {
return prefix || "";
}
const joined = segments.join(separator);
return prefix ? `${prefix}${joined}` : joined;
}
export function compactPath(path: string, maxLength: number): string {
if (path.length <= maxLength) {
return path;
}
if (maxLength < 2) {
return path.slice(0, maxLength);
}
const separator = detectPathSeparator(path);
const prefix = detectPathPrefix(path, separator);
const segments = path
.slice(prefix.length)
.split(/[\\/]+/)
.filter((segment) => segment.length > 0);
const lastSegment = segments[segments.length - 1] ?? path.slice(-(maxLength - 1));
const previousSegment = segments[segments.length - 2];
const candidates = [
joinPathSegments(prefix, separator, ["…", ...(previousSegment ? [previousSegment] : []), lastSegment]),
joinPathSegments("", separator, ["…", ...(previousSegment ? [previousSegment] : []), lastSegment]),
joinPathSegments("", separator, ["…", lastSegment]),
`${path.slice(-(maxLength - 1))}`,
];
for (const candidate of candidates) {
if (candidate.length <= maxLength) {
return candidate;
}
}
return `${lastSegment.slice(-(maxLength - 1))}`;
}
+67
View File
@@ -0,0 +1,67 @@
import { compactPath } from "./path-utils.js";
interface SearchResult {
file: string;
lineNumber: string;
content: string;
}
export function groupSearchResults(output: string, maxResults = 50): string | null {
const results: SearchResult[] = [];
for (const line of output.split("\n")) {
if (!line.trim()) {
continue;
}
const match = line.match(/^(.+?):(\d+)?:(.+)$/);
if (!match) {
continue;
}
results.push({
file: match[1] ?? "unknown",
lineNumber: match[2] ?? "?",
content: match[3] ?? "",
});
}
if (results.length === 0) {
return null;
}
const byFile = new Map<string, SearchResult[]>();
for (const result of results) {
const existing = byFile.get(result.file) ?? [];
existing.push(result);
byFile.set(result.file, existing);
}
let outputText = `${results.length} matches in ${byFile.size} files:\n\n`;
const sortedFiles = Array.from(byFile.entries()).sort((left, right) =>
left[0].localeCompare(right[0]),
);
let shown = 0;
for (const [file, matches] of sortedFiles) {
if (shown >= maxResults) {
break;
}
outputText += `> ${compactPath(file, 50)} (${matches.length} matches):\n`;
for (const match of matches.slice(0, 10)) {
let cleaned = match.content.trim();
if (cleaned.length > 70) {
cleaned = `${cleaned.slice(0, 67)}...`;
}
outputText += ` ${match.lineNumber}: ${cleaned}\n`;
shown++;
}
if (matches.length > 10) {
outputText += ` +${matches.length - 10} more\n`;
}
outputText += "\n";
}
if (results.length > shown) {
outputText += `... +${results.length - shown} more\n`;
}
return outputText;
}
+317
View File
@@ -0,0 +1,317 @@
export type Language =
| "typescript"
| "javascript"
| "python"
| "rust"
| "go"
| "java"
| "c"
| "cpp"
| "unknown";
const LANGUAGE_EXTENSIONS: Record<string, Language> = {
".ts": "typescript",
".tsx": "typescript",
".js": "javascript",
".jsx": "javascript",
".mjs": "javascript",
".py": "python",
".pyw": "python",
".rs": "rust",
".go": "go",
".java": "java",
".c": "c",
".h": "c",
".cpp": "cpp",
".hpp": "cpp",
".cc": "cpp",
};
interface CommentPatterns {
line?: string;
blockStart?: string;
blockEnd?: string;
docLine?: string;
docBlockStart?: string;
}
const COMMENT_PATTERNS: Record<Language, CommentPatterns> = {
typescript: { line: "//", blockStart: "/*", blockEnd: "*/", docBlockStart: "/**" },
javascript: { line: "//", blockStart: "/*", blockEnd: "*/", docBlockStart: "/**" },
python: { line: "#", blockStart: '"""', blockEnd: '"""', docBlockStart: '"""' },
rust: { line: "//", blockStart: "/*", blockEnd: "*/", docLine: "///", docBlockStart: "/**" },
go: { line: "//", blockStart: "/*", blockEnd: "*/", docBlockStart: "/**" },
java: { line: "//", blockStart: "/*", blockEnd: "*/", docBlockStart: "/**" },
c: { line: "//", blockStart: "/*", blockEnd: "*/", docBlockStart: "/**" },
cpp: { line: "//", blockStart: "/*", blockEnd: "*/", docBlockStart: "/**" },
unknown: { line: "//", blockStart: "/*", blockEnd: "*/" },
};
const IMPORT_PATTERN = /^(use\s+|import\s+|from\s+|require\(|#include)/;
const SIGNATURE_PATTERN = /^(pub\s+)?(async\s+)?(fn|def|function|func|class|struct|enum|trait|interface|type)\s+\w+/;
const CONST_PATTERN = /^(const|static|let|pub\s+const|pub\s+static)\s+/;
function getCodePortion(line: string, language: Language): string {
const patterns = COMMENT_PATTERNS[language];
let quote: '"' | "'" | "`" | null = null;
let escaped = false;
let code = "";
for (let index = 0; index < line.length; index += 1) {
const character = line[index] ?? "";
if (escaped) {
escaped = false;
continue;
}
if (quote !== null) {
if (character === "\\") {
escaped = true;
continue;
}
if (character === quote) {
quote = null;
}
continue;
}
if (patterns.line && line.startsWith(patterns.line, index)) {
break;
}
if (patterns.blockStart && patterns.blockEnd && line.startsWith(patterns.blockStart, index)) {
const blockEndIndex = line.indexOf(patterns.blockEnd, index + patterns.blockStart.length);
if (blockEndIndex === -1) {
break;
}
index = blockEndIndex + patterns.blockEnd.length - 1;
continue;
}
if (character === '"' || character === "'" || character === "`") {
quote = character;
continue;
}
code += character;
}
return code;
}
function countCodeBraces(line: string, language: Language): { open: number; close: number } {
let open = 0;
let close = 0;
for (const character of getCodePortion(line, language)) {
if (character === "{") {
open += 1;
} else if (character === "}") {
close += 1;
}
}
return { open, close };
}
export function detectLanguage(filePath: string): Language {
const lastDot = filePath.lastIndexOf(".");
if (lastDot === -1) {
return "unknown";
}
const extension = filePath.slice(lastDot).toLowerCase();
return LANGUAGE_EXTENSIONS[extension] ?? "unknown";
}
export function filterMinimal(content: string, language: Language): string {
const patterns = COMMENT_PATTERNS[language];
const lines = content.split("\n");
const result: string[] = [];
let inBlockComment = false;
let inDocstring = false;
let inUserscriptMetadataBlock = false;
const userscriptMetadataStartPattern = /^\/\/\s*==\s*userscript\s*==$/i;
const userscriptMetadataContentPattern = /^\/\/\s*@\w+/;
const userscriptMetadataEndPattern = /^\/\/\s*==\s*\/userscript\s*==$/i;
for (const line of lines) {
const trimmed = line.trim();
const isUserscriptMetadataStart = userscriptMetadataStartPattern.test(trimmed);
const isUserscriptMetadataContent = userscriptMetadataContentPattern.test(trimmed);
const isUserscriptMetadataEnd = userscriptMetadataEndPattern.test(trimmed);
if (isUserscriptMetadataStart) {
inUserscriptMetadataBlock = true;
result.push(line);
continue;
}
if (inUserscriptMetadataBlock) {
result.push(line);
if (isUserscriptMetadataEnd) {
inUserscriptMetadataBlock = false;
} else if (isUserscriptMetadataContent) {
// Preserve metadata key/value lines (e.g. // @name) within the userscript block.
}
continue;
}
if (patterns.blockStart && patterns.blockEnd) {
if (
!inDocstring &&
trimmed.includes(patterns.blockStart) &&
!(patterns.docBlockStart && trimmed.startsWith(patterns.docBlockStart))
) {
inBlockComment = true;
}
if (inBlockComment) {
if (trimmed.includes(patterns.blockEnd)) {
inBlockComment = false;
}
continue;
}
}
if (language === "python" && trimmed.startsWith('"""')) {
inDocstring = !inDocstring;
result.push(line);
continue;
}
if (inDocstring) {
result.push(line);
continue;
}
if (patterns.line && trimmed.startsWith(patterns.line)) {
if (patterns.docLine && trimmed.startsWith(patterns.docLine)) {
result.push(line);
}
continue;
}
if (trimmed.length === 0) {
result.push("");
continue;
}
result.push(line);
}
return result
.join("\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
export function filterAggressive(content: string, language: Language): string {
const minimal = filterMinimal(content, language);
const lines = minimal.split("\n");
const result: string[] = [];
let braceDepth = 0;
let inImplementation = false;
for (const line of lines) {
const trimmed = line.trim();
if (IMPORT_PATTERN.test(trimmed)) {
result.push(line);
continue;
}
if (SIGNATURE_PATTERN.test(trimmed)) {
result.push(line);
inImplementation = true;
braceDepth = 0;
continue;
}
const braces = countCodeBraces(line, language);
const codeTrimmed = getCodePortion(line, language).trim();
if (inImplementation) {
braceDepth += braces.open;
braceDepth -= braces.close;
if (braceDepth <= 1 && (codeTrimmed === "{" || codeTrimmed === "}" || codeTrimmed.endsWith("{"))) {
result.push(line);
}
if (braceDepth <= 0) {
inImplementation = false;
if (trimmed.length > 0 && trimmed !== "}") {
result.push(" // ... implementation");
}
}
continue;
}
if (CONST_PATTERN.test(trimmed)) {
result.push(line);
}
}
return result.join("\n").trim();
}
export function smartTruncate(content: string, maxLines: number, _language: Language): string {
const lines = content.split("\n");
if (lines.length <= maxLines) {
return content;
}
const result: string[] = [];
let keptLines = 0;
let skippedSection = false;
for (const line of lines) {
const trimmed = line.trim();
const isImportant =
SIGNATURE_PATTERN.test(trimmed) ||
IMPORT_PATTERN.test(trimmed) ||
trimmed.startsWith("pub ") ||
trimmed.startsWith("export ") ||
trimmed === "}" ||
trimmed === "{";
if (isImportant || keptLines < maxLines / 2) {
if (skippedSection) {
result.push(` // ... ${lines.length - keptLines} lines omitted`);
skippedSection = false;
}
result.push(line);
keptLines += 1;
} else {
skippedSection = true;
}
if (keptLines >= maxLines - 1) {
break;
}
}
if (skippedSection || keptLines < lines.length) {
result.push(`// ... ${lines.length - keptLines} more lines (total: ${lines.length})`);
}
return result.join("\n");
}
export function filterSourceCode(
content: string,
language: Language,
level: "none" | "minimal" | "aggressive",
): string {
switch (level) {
case "none":
return content;
case "minimal":
return filterMinimal(content, language);
case "aggressive":
return filterAggressive(content, language);
default:
return content;
}
}
@@ -0,0 +1,174 @@
import { matchesCommandPatterns } from "./command-detection.js";
interface TestSummary {
passed: number;
failed: number;
skipped: number;
failures: string[];
}
const TEST_COMMAND_PATTERNS = [
/^npm\s+test\b/,
/^pnpm\s+test\b/,
/^yarn\s+test\b/,
/^bun\s+test\b/,
/^cargo\s+test\b/,
/^go\s+test\b/,
/^pytest\b/,
/^python\s+-m\s+pytest\b/,
/^(?:pnpm\s+)?(?:npx\s+)?vitest\b/,
/^(?:npx\s+)?jest\b/,
/^mocha\b/,
/^ava\b/,
/^tap\b/,
] as const;
const TEST_RESULT_PATTERNS = [
/test result:\s*(\w+)\.\s*(\d+)\s*passed;\s*(\d+)\s*failed;/,
/(\d+)\s*passed(?:,\s*(\d+)\s*failed)?(?:,\s*(\d+)\s*skipped)?/i,
/(\d+)\s*pass(?:,\s*(\d+)\s*fail)?(?:,\s*(\d+)\s*skip)?/i,
/tests?:\s*(\d+)\s*passed(?:,\s*(\d+)\s*failed)?(?:,\s*(\d+)\s*skipped)?/i,
];
const FAILURE_START_PATTERNS = [
/^FAIL\s+/,
/^FAILED\s+/,
/^\s*●\s+/,
/^\s*✕\s+/,
/test\s+\w+\s+\.\.\.\s*FAILED/,
/thread\s+'\w+'\s+panicked/,
];
const FALLBACK_PASS_PATTERN = /(?:\b(?:ok|PASS)\b|[✓✔])/;
const FALLBACK_FAIL_PATTERN = /(?:\b(?:FAIL|fail)\b|[✗✕])/;
function isFailureStart(line: string): boolean {
return FAILURE_START_PATTERNS.some((pattern) => pattern.test(line));
}
function extractTestStats(output: string): Partial<TestSummary> {
for (const pattern of TEST_RESULT_PATTERNS) {
const match = output.match(pattern);
if (!match) {
continue;
}
return {
passed: Number.parseInt(match[1] ?? "0", 10) || 0,
failed: Number.parseInt(match[2] ?? "0", 10) || 0,
skipped: Number.parseInt(match[3] ?? "0", 10) || 0,
};
}
return {};
}
export function isTestCommand(command: string | undefined | null): boolean {
return matchesCommandPatterns(command, TEST_COMMAND_PATTERNS);
}
export function aggregateTestOutput(output: string, command: string | undefined | null): string | null {
if (!isTestCommand(command)) {
return null;
}
const lines = output.split("\n");
const summary: TestSummary = {
passed: 0,
failed: 0,
skipped: 0,
failures: [],
};
const stats = extractTestStats(output);
summary.passed = stats.passed ?? 0;
summary.failed = stats.failed ?? 0;
summary.skipped = stats.skipped ?? 0;
if (summary.passed === 0 && summary.failed === 0) {
for (const line of lines) {
if (FALLBACK_PASS_PATTERN.test(line)) {
summary.passed++;
}
if (FALLBACK_FAIL_PATTERN.test(line)) {
summary.failed++;
}
}
}
if (summary.failed > 0) {
let inFailure = false;
let currentFailure: string[] = [];
let blankCount = 0;
for (const line of lines) {
if (isFailureStart(line)) {
if (inFailure && currentFailure.length > 0) {
summary.failures.push(currentFailure.join("\n"));
}
inFailure = true;
currentFailure = [line];
blankCount = 0;
continue;
}
if (!inFailure) {
continue;
}
if (line.trim() === "") {
blankCount++;
if (blankCount >= 2 && currentFailure.length > 3) {
summary.failures.push(currentFailure.join("\n"));
inFailure = false;
currentFailure = [];
} else {
currentFailure.push(line);
}
continue;
}
if (line.match(/^\s/) || line.match(/^-/)) {
currentFailure.push(line);
blankCount = 0;
continue;
}
summary.failures.push(currentFailure.join("\n"));
inFailure = false;
currentFailure = [];
}
if (inFailure && currentFailure.length > 0) {
summary.failures.push(currentFailure.join("\n"));
}
}
const result: string[] = ["Test Results:"];
result.push(` PASS: ${summary.passed} passed`);
if (summary.failed > 0) {
result.push(` FAIL: ${summary.failed} failed`);
}
if (summary.skipped > 0) {
result.push(` SKIP: ${summary.skipped} skipped`);
}
if (summary.failed > 0 && summary.failures.length > 0) {
result.push("\n Failures:");
for (const failure of summary.failures.slice(0, 5)) {
const failureLines = failure.split("\n");
const firstLine = failureLines[0] ?? "";
result.push(` - ${firstLine.slice(0, 70)}${firstLine.length > 70 ? "..." : ""}`);
for (const detailLine of failureLines.slice(1, 4)) {
if (detailLine.trim()) {
result.push(` ${detailLine.slice(0, 65)}${detailLine.length > 65 ? "..." : ""}`);
}
}
if (failureLines.length > 4) {
result.push(` ... (${failureLines.length - 4} more lines)`);
}
}
if (summary.failures.length > 5) {
result.push(` ... and ${summary.failures.length - 5} more failures`);
}
}
return result.join("\n");
}
@@ -0,0 +1,11 @@
export function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) {
return text;
}
if (maxLength < 3) {
return "...";
}
return `${text.slice(0, maxLength - 3)}...`;
}