Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/chunking/merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ export function mergeViolations(
): SemiObjectiveItem[] {
const all = chunkViolations.flat();

// Deduplicate using composite key (quoted_text + description + analysis)
// Deduplicate using composite key (quoted_text + description + message)
const seen = new Set<string>();
return all.filter((v) => {
const key = [
v.quoted_text?.toLowerCase().trim() || "",
v.description?.toLowerCase().trim() || "",
v.analysis?.toLowerCase().trim() || "",
v.message?.toLowerCase().trim() || "",
].join("|");
if (seen.has(key)) return false;
seen.add(key);
Expand Down
146 changes: 92 additions & 54 deletions src/cli/commands.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import type { Command } from 'commander';
import { existsSync } from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { createProvider } from '../providers/provider-factory';
import { PerplexitySearchProvider } from '../providers/perplexity-provider';
import type { SearchProvider } from '../providers/search-provider';
import { loadConfig } from '../boundaries/config-loader';
import { loadStyleGuide } from '../boundaries/style-guide-loader';
import { loadRuleFile, type PromptFile } from '../prompts/prompt-loader';
import { RulePackLoader } from '../boundaries/rule-pack-loader';
import { PresetLoader } from '../config/preset-loader';
import { printGlobalSummary, printTokenUsage } from '../output/reporter';
import { DefaultRequestBuilder } from '../providers/request-builder';
import { loadDirective } from '../prompts/directive-loader';
import { resolveTargets } from '../scan/file-resolver';
import { parseCliOptions, parseEnvironment } from '../boundaries/index';
import { handleUnknownError } from '../errors/index';
import { evaluateFiles } from './orchestrator';
import { OutputFormat } from './types';
import { DEFAULT_CONFIG_FILENAME, STYLE_GUIDE_FILENAME, ZERO_CONFIG_PACK_NAME, ZERO_CONFIG_PROMPT_ID } from '../config/constants';
import { Severity, Type } from '../evaluators/types';
import type { Command } from "commander";
import { existsSync } from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import { createProvider } from "../providers/provider-factory";
import { PerplexitySearchProvider } from "../providers/perplexity-provider";
import type { SearchProvider } from "../providers/search-provider";
import { loadConfig } from "../boundaries/config-loader";
import { loadStyleGuide } from "../boundaries/style-guide-loader";
import { loadRuleFile, type PromptFile } from "../prompts/prompt-loader";
import { RulePackLoader } from "../boundaries/rule-pack-loader";
import { PresetLoader } from "../config/preset-loader";
import { printGlobalSummary, printTokenUsage } from "../output/reporter";
import { DefaultRequestBuilder } from "../providers/request-builder";
import { loadDirective } from "../prompts/directive-loader";
import { resolveTargets } from "../scan/file-resolver";
import { parseCliOptions, parseEnvironment } from "../boundaries/index";
import { handleUnknownError } from "../errors/index";
import { evaluateFiles } from "./orchestrator";
import { OutputFormat } from "./types";
import {
DEFAULT_CONFIG_FILENAME,
STYLE_GUIDE_FILENAME,
ZERO_CONFIG_PACK_NAME,
ZERO_CONFIG_PROMPT_ID,
} from "../config/constants";
import { Severity, Type } from "../evaluators/types";

// eslint-disable-next-line @typescript-eslint/naming-convention
const __filename = fileURLToPath(import.meta.url);
Expand All @@ -33,12 +38,23 @@ const __dirname = dirname(__filename);
*/
export function registerMainCommand(program: Command): void {
program
.option('-v, --verbose', 'Enable verbose logging')
.option('--show-prompt', 'Print full prompt and injected content')
.option('--show-prompt-trunc', 'Print truncated prompt/content previews (500 chars)')
.option('--output <format>', 'Output format: line (default), json, or vale-json, rdjson', 'line')
.option('--config <path>', `Path to custom ${DEFAULT_CONFIG_FILENAME} config file`)
.argument('[paths...]', 'files or directories to check (required)')
.option("-v, --verbose", "Enable verbose logging")
.option("--show-prompt", "Print full prompt and injected content")
.option(
"--show-prompt-trunc",
"Print truncated prompt/content previews (500 chars)"
)
.option("--suggest", "Show fix suggestions for each issue")
.option(
"--output <format>",
"Output format: line (default), json, or vale-json, rdjson",
"line"
)
.option(
"--config <path>",
`Path to custom ${DEFAULT_CONFIG_FILENAME} config file`
)
.argument("[paths...]", "files or directories to check (required)")
.action(async (paths: string[] = []) => {
// Require explicit paths to prevent accidental full directory scans
// Users must provide specific files, directories, or wildcards (e.g., `vectorlint *`)
Expand All @@ -52,7 +68,7 @@ export function registerMainCommand(program: Command): void {
try {
cliOptions = parseCliOptions(program.opts());
} catch (e: unknown) {
const err = handleUnknownError(e, 'Parsing CLI options');
const err = handleUnknownError(e, "Parsing CLI options");
console.error(`Error: ${err.message}`);
process.exit(1);
}
Expand All @@ -62,9 +78,9 @@ export function registerMainCommand(program: Command): void {
try {
env = parseEnvironment();
} catch (e: unknown) {
const err = handleUnknownError(e, 'Validating environment variables');
const err = handleUnknownError(e, "Validating environment variables");
console.error(`Error: ${err.message}`);
console.error('Please set these in your .env file or environment.');
console.error("Please set these in your .env file or environment.");
process.exit(1);
}

Expand All @@ -73,15 +89,17 @@ export function registerMainCommand(program: Command): void {
try {
directive = loadDirective();
} catch (e: unknown) {
const err = handleUnknownError(e, 'Loading directive');
const err = handleUnknownError(e, "Loading directive");
console.error(`Error: ${err.message}`);
process.exit(1);
}

// Load style guide (VECTORLINT.md)
const styleGuide = loadStyleGuide(process.cwd());
if (styleGuide.content && cliOptions.verbose) {
console.log(`[vectorlint] Loaded style guide from ${STYLE_GUIDE_FILENAME} (${styleGuide.tokenEstimate} estimated tokens)`);
console.log(
`[vectorlint] Loaded style guide from ${STYLE_GUIDE_FILENAME} (${styleGuide.tokenEstimate} estimated tokens)`
);
}

const provider = createProvider(
Expand All @@ -104,7 +122,7 @@ export function registerMainCommand(program: Command): void {
try {
config = loadConfig(process.cwd(), cliOptions.config);
} catch (e: unknown) {
const err = handleUnknownError(e, 'Loading configuration');
const err = handleUnknownError(e, "Loading configuration");
console.error(`Error: ${err.message}`);
process.exit(1);
}
Expand All @@ -118,15 +136,19 @@ export function registerMainCommand(program: Command): void {

const prompts: PromptFile[] = [];
try {
const presetsDir = path.resolve(__dirname, '../presets');
const presetsDir = path.resolve(__dirname, "../presets");
const presetLoader = new PresetLoader(presetsDir);
const loader = new RulePackLoader(presetLoader);

const packs = await loader.listAllPacks(rulesPath);

if (packs.length === 0 && cliOptions.verbose) {
console.warn(`[vectorlint] Warning: No rule packs (subdirectories) found in ${rulesPath} or presets.`);
console.warn(`[vectorlint] Please organize your rules into subdirectories or use a valid preset.`);
console.warn(
`[vectorlint] Warning: No rule packs (subdirectories) found in ${rulesPath} or presets.`
);
console.warn(
`[vectorlint] Please organize your rules into subdirectories or use a valid preset.`
);
}

for (const pack of packs) {
Expand All @@ -136,7 +158,8 @@ export function registerMainCommand(program: Command): void {
for (const filePath of rulePaths) {
const result = loadRuleFile(filePath, pack.name);
if (result.warning) {
if (cliOptions.verbose) console.warn(`[vectorlint] ${result.warning}`);
if (cliOptions.verbose)
console.warn(`[vectorlint] ${result.warning}`);
}
if (result.prompt) {
prompts.push(result.prompt);
Expand All @@ -147,21 +170,32 @@ export function registerMainCommand(program: Command): void {
if (prompts.length === 0) {
if (styleGuide.content) {
if (cliOptions.verbose) {
console.log('[vectorlint] No rules found, but VECTORLINT.md exists. Running in zero-config mode.');
console.log(
"[vectorlint] No rules found, but VECTORLINT.md exists. Running in zero-config mode."
);
}

prompts.push(createStyleGuidePrompt(styleGuide.path || path.resolve(process.cwd(), STYLE_GUIDE_FILENAME)));
prompts.push(
createStyleGuidePrompt(
styleGuide.path ||
path.resolve(process.cwd(), STYLE_GUIDE_FILENAME)
)
);
} else {
if (rulesPath) {
console.error(`Error: no .md rules found in ${rulesPath} or presets.`);
console.error(
`Error: no .md rules found in ${rulesPath} or presets.`
);
} else {
console.error('Error: no rules found. Either set RulesPath in config or configure RunRules with a valid preset.');
console.error(
"Error: no rules found. Either set RulesPath in config or configure RunRules with a valid preset."
);
}
process.exit(1);
}
}
} catch (e: unknown) {
const err = handleUnknownError(e, 'Loading prompts');
const err = handleUnknownError(e, "Loading prompts");
console.error(`Error: failed to load prompts: ${err.message}`);
process.exit(1);
}
Expand All @@ -177,26 +211,27 @@ export function registerMainCommand(program: Command): void {
configDir: config.configDir,
});
} catch (e: unknown) {
const err = handleUnknownError(e, 'Resolving target files');
const err = handleUnknownError(e, "Resolving target files");
console.error(`Error: failed to resolve target files: ${err.message}`);
process.exit(1);
}

if (targets.length === 0) {
console.error('Error: no target files found to evaluate.');
console.error("Error: no target files found to evaluate.");
process.exit(1);
}



// Create search provider if API key is available
const searchProvider: SearchProvider | undefined = process.env.PERPLEXITY_API_KEY
const searchProvider: SearchProvider | undefined = process.env
.PERPLEXITY_API_KEY
? new PerplexitySearchProvider({ debug: false })
: undefined;

const outputFormat = cliOptions.output as OutputFormat;
if (!Object.values(OutputFormat).includes(outputFormat)) {
console.error(`Error: Invalid output format '${cliOptions.output}'. Valid options: line, json, vale-json, rdjson`);
console.error(
`Error: Invalid output format '${cliOptions.output}'. Valid options: line, json, vale-json, rdjson`
);
process.exit(1);
}

Expand All @@ -210,14 +245,15 @@ export function registerMainCommand(program: Command): void {
verbose: cliOptions.verbose,
outputFormat: outputFormat,
scanPaths: config.scanPaths,
suggest: cliOptions.suggest,
pricing: {
inputPricePerMillion: env.INPUT_PRICE_PER_MILLION,
outputPricePerMillion: env.OUTPUT_PRICE_PER_MILLION,
},
});

// Print global summary (only for line format)
if (cliOptions.output === 'line') {
if (cliOptions.output === "line") {
if (result.tokenUsage) {
printTokenUsage(result.tokenUsage);
}
Expand All @@ -230,7 +266,9 @@ export function registerMainCommand(program: Command): void {
}

// Exit with appropriate code
process.exit(result.hadOperationalErrors || result.hadSeverityErrors ? 1 : 0);
process.exit(
result.hadOperationalErrors || result.hadSeverityErrors ? 1 : 0
);
});
}

Expand All @@ -239,11 +277,11 @@ function createStyleGuidePrompt(fullPath: string): PromptFile {
id: ZERO_CONFIG_PROMPT_ID,
filename: STYLE_GUIDE_FILENAME,
fullPath,
body: 'Evaluate the provided content against the attached Global Style Guide. Report any violations of the rules defined in the style guide.',
body: "Evaluate the provided content against the attached Global Style Guide. Report any violations of the rules defined in the style guide.",
pack: ZERO_CONFIG_PACK_NAME,
meta: {
id: ZERO_CONFIG_PROMPT_ID,
name: 'Style Guide Compliance',
name: "Style Guide Compliance",
evaluator: Type.BASE,
severity: Severity.WARNING,
},
Expand Down
Loading