@ton-ai-core/vibecode-linter
Version:
Advanced TypeScript linter with Git integration, dependency analysis, and comprehensive error reporting
263 lines • 12.1 kB
JavaScript
// CHANGE: Introduce explicit preflight checks for peer dependencies and execution context
// WHY: Provide actionable, English diagnostics before running linters; avoid MODULE_NOT_FOUND at runtime
// QUOTE(ТЗ): "При вызове команды писать внятно что необходимо сделать"
// REF: REQ-CLI-PREFLIGHT-PEERS
// SOURCE: n/a
import * as fs from "node:fs";
import { createRequire } from "node:module";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { match } from "ts-pattern";
// CHANGE: Define __dirname and require equivalents for ES modules
// WHY: package.json has "type": "module", __dirname and require are not available in ES modules
// QUOTE(ERROR): "ReferenceError: __dirname is not defined", "ReferenceError: require is not defined"
// REF: ES module migration
// SOURCE: Node.js ES modules documentation
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
/**
* Check if the current working directory looks like a project root (has package.json).
*
* Invariant: users should run CLI from the project root to ensure peer dependency resolution.
*/
export function hasPackageJson(cwd) {
// CHANGE: explicit existence check
// WHY: Ensures we emit a clear message if user runs outside project root
// QUOTE(ТЗ): "При вызове команды писать внятно что необходимо сделать"
// REF: REQ-CLI-PREFLIGHT-PEERS
const pkg = path.join(cwd, "package.json");
try {
const stat = fs.statSync(pkg);
return stat.isFile();
}
catch {
return false;
}
}
/**
* Resolve a module from a specific cwd without mutating global module paths.
*
* Postcondition: returns true if resolution succeeds, false otherwise.
*/
export function canResolveFromCwd(moduleName, cwd) {
try {
// CHANGE: bounded resolution via explicit paths
// WHY: Mirrors how Node resolves peer deps in consumer projects
// QUOTE(ТЗ): "Использовать версию инструмента проекта"
// REF: REQ-CLI-PREFLIGHT-PEERS
require.resolve(moduleName, { paths: [cwd] });
return true;
}
catch {
return false;
}
}
/**
* Detect if running from an isolated npx cache directory (advisory).
*
* Invariant: Running from ~/.npm/_npx/** may hide project devDependencies from Node's resolver.
*/
export function isNpxIsolatedProcess(currentDir, argv0, execPath) {
// CHANGE: heuristic detection for isolation
// WHY: Provide clear guidance about using locally installed CLI
// QUOTE(ТЗ): "Писать внятные подсказки при проблемах запуска"
// REF: REQ-CLI-PREFLIGHT-PEERS
const markers = ["/.npm/_npx/", "\\.npm\\_npx\\"]; // POSIX and Windows
const haystack = [currentDir, argv0, execPath].join("|");
return markers.some((m) => haystack.includes(m));
}
// CHANGE: Extract conditional push into helper to reduce cyclomatic complexity
// WHY: Satisfy ESLint complexity limit by delegating branching
// QUOTE(ТЗ): "Исправить все ошибки линтера"
// REF: REQ-LINT-FIX
/**
* Append an issue to the accumulator if the condition holds.
*
* Invariant: pushes only when condition is true.
*/
function pushIssueIf(condition, code, out) {
if (condition) {
out.push(code);
}
}
/**
* Run preflight checks and return a structured result.
*
* Preconditions:
* - None
*
* Postconditions:
* - ok === true iff no blocking issues are found (missing TypeScript, missing Biome, or no package.json)
* - issues contains "npxIsolated" as advisory when detected
*
* Complexity:
* - O(1) fs checks and sync require.resolve attempts; no file traversal
*/
export function runPreflight(cwd = process.cwd()) {
const issues = [];
const hasPkg = hasPackageJson(cwd);
if (!hasPkg) {
issues.push("noPackageJson");
}
// Only attempt peer resolution if cwd looks like a project
// (still attempt though, because user may be in subfolder below a root with hoisted node_modules)
// CHANGE: Detect TypeScript presence via package.json resolution as well
// WHY: allow robust detection during tests and in projects where TypeScript is present without a resolvable JS entry
// QUOTE(ТЗ): "Инварианты должны быть проверяемыми без ложных отрицаний"
// REF: REQ-CLI-PREFLIGHT-PEERS
const hasTs = canResolveFromCwd("typescript", cwd) ||
canResolveFromCwd("typescript/package.json", cwd);
if (!hasTs) {
issues.push("missingTypescript");
}
// CHANGE: Detect Biome presence via package.json resolution as it is a CLI-first package
// WHY: '@biomejs/biome' may not expose a require-able JS entry; resolving its package.json is a robust presence check
// QUOTE(ТЗ): "При вызове команды писать внятно что необходимо сделать"
// REF: REQ-CLI-PREFLIGHT-PEERS
const hasBiome = canResolveFromCwd("@biomejs/biome", cwd) ||
canResolveFromCwd("@biomejs/biome/package.json", cwd);
if (!hasBiome) {
issues.push("missingBiome");
}
pushIssueIf(isNpxIsolatedProcess(__dirname, process.argv[1] ?? "", process.execPath), "npxIsolated", issues);
// Blocking if any of the required peers or project context is missing
const blocking = issues.some((c) => c === "missingTypescript" ||
c === "missingBiome" ||
c === "noPackageJson");
return { ok: !blocking, issues };
}
// CHANGE: Helper to print blocking issues (missing peers, no package.json)
// WHY: Reduce size/complexity of printPreflightReport by delegating detailed output
// QUOTE(LINT): "Function has too many lines (max 50)"
// REF: ESLint max-lines-per-function
/**
* Print detailed guidance for the "noPackageJson" blocking issue.
*
* Invariant: Users should execute the CLI from a directory containing package.json
*/
// CHANGE: extract small printer to reduce lines in dispatcher
// WHY: satisfy max-lines-per-function without losing clarity
// QUOTE(LINT): "Function has too many lines (max 50)"
// REF: ESLint max-lines-per-function
function printNoPackageJson() {
console.error(" • Current directory does not contain package.json.");
console.error(" Why: vibecode-linter resolves peer tools (TypeScript, Biome) from your project.");
console.error(" Action: Run this command from your project root (where package.json is located).\n");
}
/**
* Print detailed guidance for the "missingTypescript" blocking issue.
*
* Invariant: TypeScript must be installed in the consumer project
*/
// CHANGE: extract small printer to reduce lines in dispatcher
// WHY: satisfy max-lines-per-function without losing clarity
// QUOTE(LINT): "Function has too many lines (max 50)"
// REF: ESLint max-lines-per-function
function printMissingTypescript() {
console.error(" • TypeScript (typescript) is not installed in this project.");
console.error(" Why: vibecode-linter uses your project's TypeScript as a peer dependency.");
console.error(" Install:");
console.error(" npm install --save-dev typescript");
console.error(" Verify:");
console.error(" npx tsc --version\n");
}
/**
* Print detailed guidance for the "missingBiome" blocking issue.
*
* Invariant: Biome CLI must be installed in the consumer project
*/
// CHANGE: extract small printer to reduce lines in dispatcher
// WHY: satisfy max-lines-per-function without losing clarity
// QUOTE(LINT): "Function has too many lines (max 50)"
// REF: ESLint max-lines-per-function
function printMissingBiome() {
console.error(" • Biome CLI (@biomejs/biome) is not installed in this project.");
console.error(" Why: vibecode-linter runs Biome as an external tool from your node_modules.");
console.error(" Install:");
console.error(" npm install --save-dev @biomejs/biome");
console.error(" Verify:");
console.error(" npx biome --version\n");
}
/**
* Print blocking issues dispatcher.
*
* CHANGE: delegate per-issue printing to compact helpers
* WHY: keep this function small to satisfy linting constraints
* QUOTE(LINT): "Function has too many lines (max 50)"
* REF: ESLint max-lines-per-function
*/
function printBlockingIssues(blockingIssues) {
console.error("\n[ERROR] Environment preflight failed. Please resolve the following issues:\n");
for (const issue of blockingIssues) {
// CHANGE: Replace switch with ts-pattern exhaustive match
// WHY: Functional paradigm forbids switch; ts-pattern ensures total, type-safe branching
// QUOTE(TЗ): "Исправить все ошибки линтера" — запрет switch, использовать ts-pattern match()
// REF: REQ-LINT-FIX, ESLint no-restricted-syntax
// FORMAT THEOREME: Пусть I — конечное множество {noPackageJson, missingTypescript, missingBiome, npxIsolated}.
// Определим g: I → Unit c поведением печати. Матч реализует тотально определенную функцию g, покрывая все элементы I.
match(issue)
.with("noPackageJson", () => {
printNoPackageJson();
})
.with("missingTypescript", () => {
printMissingTypescript();
})
.with("missingBiome", () => {
printMissingBiome();
})
.with("npxIsolated", () => {
// Advisory-only: handled as warning elsewhere; not a blocker
})
.exhaustive();
}
}
// CHANGE: Helper to print advisory warnings (non-blocking hints)
// WHY: Keep printPreflightReport concise
// QUOTE(LINT): "Function has too many lines (max 50)"
// REF: ESLint max-lines-per-function
function printAdvisoryWarnings(advisories) {
if (advisories.length === 0)
return;
// Currently the only advisory is npxIsolated
if (advisories.includes("npxIsolated")) {
console.warn("\n[WARN] Detected isolated npx execution environment.");
console.warn(" Running via a fresh npx cache may not see your project's devDependencies.");
console.warn(" Recommended:");
console.warn(" npm install --save-dev @ton-ai-core/vibecode-linter");
console.warn(" npx @ton-ai-core/vibecode-linter <path>\n");
}
}
/**
* Print actionable, English guidance based on preflight issues.
*
* No side-effects other than stdout/stderr. Does not exit the process.
*/
export function printPreflightReport(issues) {
// CHANGE: Keep this function concise by delegating detailed output to helpers
// WHY: Satisfy max-lines-per-function while preserving actionable guidance
// QUOTE(LINT): "Function has too many lines (max 50)"
// REF: ESLint max-lines-per-function
if (issues.length === 0)
return;
const blockingIssues = issues.filter((c) => c !== "npxIsolated");
const advisories = issues.filter((c) => c === "npxIsolated");
if (blockingIssues.length > 0) {
printBlockingIssues(blockingIssues);
}
printAdvisoryWarnings(advisories);
}
/**
* Convenience helper to both run and print preflight diagnostics.
*
* @returns PreflightResult for the caller to decide whether to proceed or exit.
*/
export function checkAndReportPreflight(cwd = process.cwd()) {
const result = runPreflight(cwd);
printPreflightReport(result.issues);
return result;
}
// CHANGE: This module is designed to be used by the CLI entry before any linter execution.
// WHY: Enforce invariants early to prevent runtime MODULE_NOT_FOUND and provide clear guidance.
// QUOTE(ТЗ): "Любое решение строится на инвариантах и проверяемых источниках"
// REF: CLINE-RULES
//# sourceMappingURL=preflight.js.map