@liendev/lien 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5140,8 +5140,8 @@ function getErrorMap() {
5140
5140
 
5141
5141
  // ../../node_modules/zod/v3/helpers/parseUtil.js
5142
5142
  var makeIssue = (params) => {
5143
- const { data, path: path10, errorMaps, issueData } = params;
5144
- const fullPath = [...path10, ...issueData.path || []];
5143
+ const { data, path: path11, errorMaps, issueData } = params;
5144
+ const fullPath = [...path11, ...issueData.path || []];
5145
5145
  const fullIssue = {
5146
5146
  ...issueData,
5147
5147
  path: fullPath
@@ -5257,11 +5257,11 @@ var errorUtil;
5257
5257
 
5258
5258
  // ../../node_modules/zod/v3/types.js
5259
5259
  var ParseInputLazyPath = class {
5260
- constructor(parent, value, path10, key) {
5260
+ constructor(parent, value, path11, key) {
5261
5261
  this._cachedPath = [];
5262
5262
  this.parent = parent;
5263
5263
  this.data = value;
5264
- this._path = path10;
5264
+ this._path = path11;
5265
5265
  this._key = key;
5266
5266
  }
5267
5267
  get path() {
@@ -9435,10 +9435,10 @@ async function findRelatedChunks(filepaths, fileChunksMap, ctx) {
9435
9435
  }
9436
9436
  function createPathCache(workspaceRoot) {
9437
9437
  const cache = /* @__PURE__ */ new Map();
9438
- const normalize = (path10) => {
9439
- if (cache.has(path10)) return cache.get(path10);
9440
- const normalized = normalizePath(path10, workspaceRoot);
9441
- cache.set(path10, normalized);
9438
+ const normalize = (path11) => {
9439
+ if (cache.has(path11)) return cache.get(path11);
9440
+ const normalized = normalizePath(path11, workspaceRoot);
9441
+ cache.set(path11, normalized);
9442
9442
  return normalized;
9443
9443
  };
9444
9444
  return { normalize, cache };
@@ -9813,11 +9813,11 @@ async function scanChunksPaginated(vectorDB, crossRepo, log, normalizePathCached
9813
9813
  function createPathNormalizer() {
9814
9814
  const workspaceRoot = process.cwd().replace(/\\/g, "/");
9815
9815
  const cache = /* @__PURE__ */ new Map();
9816
- return (path10) => {
9817
- if (!cache.has(path10)) {
9818
- cache.set(path10, normalizePath2(path10, workspaceRoot));
9816
+ return (path11) => {
9817
+ if (!cache.has(path11)) {
9818
+ cache.set(path11, normalizePath2(path11, workspaceRoot));
9819
9819
  }
9820
- return cache.get(path10);
9820
+ return cache.get(path11);
9821
9821
  };
9822
9822
  }
9823
9823
  function groupChunksByFile(chunks) {
@@ -11418,12 +11418,275 @@ async function complexityCommand(options) {
11418
11418
  }
11419
11419
  }
11420
11420
 
11421
- // src/cli/config.ts
11421
+ // src/cli/review.ts
11422
+ import { execFile } from "child_process";
11423
+ import { promisify } from "util";
11422
11424
  import chalk8 from "chalk";
11425
+ import ora3 from "ora";
11426
+ import fs7 from "fs";
11423
11427
  import path9 from "path";
11428
+ import {
11429
+ performChunkOnlyIndex,
11430
+ analyzeComplexityFromChunks
11431
+ } from "@liendev/parser";
11432
+ import {
11433
+ ReviewEngine,
11434
+ loadConfig,
11435
+ loadPlugins,
11436
+ resolveLLMApiKey,
11437
+ getPluginConfig,
11438
+ OpenRouterLLMClient,
11439
+ TerminalAdapter,
11440
+ SARIFAdapter,
11441
+ filterAnalyzableFiles,
11442
+ consoleLogger
11443
+ } from "@liendev/review";
11444
+ var execFileAsync = promisify(execFile);
11445
+ var VALID_FAIL_ON2 = ["error", "warning"];
11446
+ var VALID_FORMATS3 = ["text", "json", "sarif"];
11447
+ function validateOptions(options, rootDir) {
11448
+ if (options.failOn && !VALID_FAIL_ON2.includes(options.failOn)) {
11449
+ console.error(
11450
+ chalk8.red(
11451
+ `Error: Invalid --fail-on value "${options.failOn}". Must be either 'error' or 'warning'`
11452
+ )
11453
+ );
11454
+ process.exit(1);
11455
+ }
11456
+ if (!VALID_FORMATS3.includes(options.format)) {
11457
+ console.error(
11458
+ chalk8.red(
11459
+ `Error: Invalid --format value "${options.format}". Must be one of: text, json, sarif`
11460
+ )
11461
+ );
11462
+ process.exit(1);
11463
+ }
11464
+ if (options.files) {
11465
+ const missing = options.files.filter((file) => {
11466
+ const fullPath = path9.isAbsolute(file) ? file : path9.join(rootDir, file);
11467
+ return !fs7.existsSync(fullPath);
11468
+ });
11469
+ if (missing.length > 0) {
11470
+ console.error(chalk8.red(`Error: File${missing.length > 1 ? "s" : ""} not found:`));
11471
+ missing.forEach((file) => console.error(chalk8.red(` - ${file}`)));
11472
+ process.exit(1);
11473
+ }
11474
+ }
11475
+ }
11476
+ async function getDefaultBranch(rootDir) {
11477
+ for (const remote of ["origin", "upstream"]) {
11478
+ try {
11479
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--verify", `${remote}/main`], {
11480
+ cwd: rootDir,
11481
+ timeout: 5e3
11482
+ });
11483
+ if (stdout.trim()) return `${remote}/main`;
11484
+ } catch {
11485
+ }
11486
+ try {
11487
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--verify", `${remote}/master`], {
11488
+ cwd: rootDir,
11489
+ timeout: 5e3
11490
+ });
11491
+ if (stdout.trim()) return `${remote}/master`;
11492
+ } catch {
11493
+ }
11494
+ }
11495
+ return "HEAD";
11496
+ }
11497
+ async function getGitChangedFiles(rootDir) {
11498
+ const defaultBranch = await getDefaultBranch(rootDir);
11499
+ try {
11500
+ const { stdout } = await execFileAsync(
11501
+ "git",
11502
+ ["diff", "--name-only", `${defaultBranch}...HEAD`],
11503
+ { cwd: rootDir, timeout: 1e4 }
11504
+ );
11505
+ const files = stdout.trim().split("\n").filter(Boolean);
11506
+ const { stdout: statusOut } = await execFileAsync("git", ["diff", "--name-only", "HEAD"], {
11507
+ cwd: rootDir,
11508
+ timeout: 1e4
11509
+ });
11510
+ const workingFiles = statusOut.trim().split("\n").filter(Boolean);
11511
+ const allFiles = /* @__PURE__ */ new Set([...files, ...workingFiles]);
11512
+ return Array.from(allFiles).filter((f) => fs7.existsSync(path9.join(rootDir, f)));
11513
+ } catch {
11514
+ try {
11515
+ const { stdout } = await execFileAsync("git", ["ls-files"], { cwd: rootDir, timeout: 1e4 });
11516
+ return stdout.trim().split("\n").filter(Boolean);
11517
+ } catch {
11518
+ return [];
11519
+ }
11520
+ }
11521
+ }
11522
+ async function isGitRepo3(rootDir) {
11523
+ try {
11524
+ await fs7.promises.access(path9.join(rootDir, ".git"));
11525
+ return true;
11526
+ } catch {
11527
+ return false;
11528
+ }
11529
+ }
11530
+ function createLogger(verbose) {
11531
+ if (verbose) return consoleLogger;
11532
+ return {
11533
+ info: () => {
11534
+ },
11535
+ warning: (msg) => console.error(chalk8.yellow(`Warning: ${msg}`)),
11536
+ error: (msg) => console.error(chalk8.red(`Error: ${msg}`)),
11537
+ debug: () => {
11538
+ }
11539
+ };
11540
+ }
11541
+ async function resolveFilesToReview(options, rootDir, spinner) {
11542
+ if (options.files) return options.files;
11543
+ if (!await isGitRepo3(rootDir)) {
11544
+ console.error(chalk8.red("Error: Not a git repository"));
11545
+ console.log(chalk8.yellow("Use --files to analyze specific files"));
11546
+ process.exit(1);
11547
+ }
11548
+ spinner?.start("Detecting changed files...");
11549
+ const changedFiles = await getGitChangedFiles(rootDir);
11550
+ const analyzable = filterAnalyzableFiles(changedFiles);
11551
+ if (analyzable.length === 0) {
11552
+ spinner?.stop();
11553
+ console.log(chalk8.yellow("No changed files found. Use --files to analyze specific files."));
11554
+ return null;
11555
+ }
11556
+ spinner?.succeed(`Found ${analyzable.length} changed file${analyzable.length === 1 ? "" : "s"}`);
11557
+ return analyzable;
11558
+ }
11559
+ function resolveLLMClient(options, config, logger) {
11560
+ const noLlm = options.noLlm ?? false;
11561
+ const apiKey = noLlm ? void 0 : resolveLLMApiKey(config);
11562
+ if (!apiKey && !noLlm && options.format === "text") {
11563
+ console.log(
11564
+ chalk8.dim(
11565
+ "No LLM API key found. Running without LLM (plugins that require LLM will be skipped).\nSet OPENROUTER_API_KEY or configure .lien/review.yml for LLM-enriched reviews."
11566
+ )
11567
+ );
11568
+ }
11569
+ const model = options.model ?? config.llm.model;
11570
+ const llm = apiKey && !noLlm ? new OpenRouterLLMClient({ apiKey, model, logger }) : void 0;
11571
+ if (llm && options.model) {
11572
+ console.log(chalk8.dim(`Using model: ${model}`));
11573
+ }
11574
+ return { llm, model };
11575
+ }
11576
+ async function presentResults(findings, options, adapterContext) {
11577
+ if (options.format === "sarif") {
11578
+ await new SARIFAdapter().present(findings, adapterContext);
11579
+ } else if (options.format === "json") {
11580
+ console.log(JSON.stringify(findings, null, 2));
11581
+ } else {
11582
+ await new TerminalAdapter().present(findings, adapterContext);
11583
+ }
11584
+ }
11585
+ async function indexAndAnalyze(rootDir, filesToReview, logger, spinner) {
11586
+ spinner?.start("Indexing files...");
11587
+ const indexResult = await performChunkOnlyIndex(rootDir, { filesToIndex: filesToReview });
11588
+ if (!indexResult.success || indexResult.chunks.length === 0) {
11589
+ spinner?.fail("Failed to index files");
11590
+ if (indexResult.error) logger.error(indexResult.error);
11591
+ process.exit(2);
11592
+ }
11593
+ spinner?.succeed(
11594
+ `Indexed ${indexResult.filesIndexed} file${indexResult.filesIndexed === 1 ? "" : "s"} (${indexResult.chunksCreated} chunks)`
11595
+ );
11596
+ spinner?.start("Analyzing complexity...");
11597
+ const complexityReport = analyzeComplexityFromChunks(indexResult.chunks, filesToReview);
11598
+ spinner?.succeed(
11599
+ `Complexity: ${complexityReport.summary.totalViolations} violation${complexityReport.summary.totalViolations === 1 ? "" : "s"}`
11600
+ );
11601
+ return { chunks: indexResult.chunks, complexityReport };
11602
+ }
11603
+ async function runReviewEngine(config, chunks, filesToReview, complexityReport, llm, logger, verbose, pluginFilter) {
11604
+ const plugins = await loadPlugins(config);
11605
+ const engine = new ReviewEngine({ verbose });
11606
+ for (const plugin of plugins) {
11607
+ engine.register(plugin);
11608
+ }
11609
+ const pluginConfigs = {};
11610
+ for (const plugin of plugins) {
11611
+ const pluginConfig = getPluginConfig(config, plugin.id);
11612
+ if (Object.keys(pluginConfig).length > 0) {
11613
+ pluginConfigs[plugin.id] = pluginConfig;
11614
+ }
11615
+ }
11616
+ return engine.run(
11617
+ {
11618
+ chunks,
11619
+ changedFiles: filesToReview,
11620
+ complexityReport,
11621
+ baselineReport: null,
11622
+ deltas: null,
11623
+ pluginConfigs,
11624
+ config: {},
11625
+ llm,
11626
+ logger
11627
+ },
11628
+ pluginFilter
11629
+ );
11630
+ }
11631
+ async function reviewCommand(options) {
11632
+ const rootDir = process.cwd();
11633
+ try {
11634
+ validateOptions(options, rootDir);
11635
+ const verbose = options.verbose ?? false;
11636
+ const logger = createLogger(verbose);
11637
+ const spinner = options.format === "text" ? ora3() : null;
11638
+ const filesToReview = await resolveFilesToReview(options, rootDir, spinner);
11639
+ if (!filesToReview) return;
11640
+ const config = loadConfig(rootDir);
11641
+ const { llm, model } = resolveLLMClient(options, config, logger);
11642
+ const { chunks, complexityReport } = await indexAndAnalyze(
11643
+ rootDir,
11644
+ filesToReview,
11645
+ logger,
11646
+ spinner
11647
+ );
11648
+ spinner?.start("Running review plugins...");
11649
+ const findings = await runReviewEngine(
11650
+ config,
11651
+ chunks,
11652
+ filesToReview,
11653
+ complexityReport,
11654
+ llm,
11655
+ logger,
11656
+ verbose,
11657
+ options.plugin
11658
+ );
11659
+ spinner?.succeed(
11660
+ `Review complete: ${findings.length} finding${findings.length === 1 ? "" : "s"}`
11661
+ );
11662
+ await presentResults(findings, options, {
11663
+ complexityReport,
11664
+ baselineReport: null,
11665
+ deltas: null,
11666
+ deltaSummary: null,
11667
+ logger,
11668
+ llmUsage: llm?.getUsage(),
11669
+ model
11670
+ });
11671
+ if (options.failOn) {
11672
+ const hasMatching = options.failOn === "error" ? findings.some((f) => f.severity === "error") : findings.some((f) => f.severity === "error" || f.severity === "warning");
11673
+ if (hasMatching) process.exit(1);
11674
+ }
11675
+ } catch (error) {
11676
+ console.error(
11677
+ chalk8.red("Error running review:"),
11678
+ error instanceof Error ? error.message : String(error)
11679
+ );
11680
+ process.exit(2);
11681
+ }
11682
+ }
11683
+
11684
+ // src/cli/config.ts
11685
+ import chalk9 from "chalk";
11686
+ import path10 from "path";
11424
11687
  import os3 from "os";
11425
11688
  import { loadGlobalConfig, mergeGlobalConfig } from "@liendev/core";
11426
- var CONFIG_PATH = path9.join(os3.homedir(), ".lien", "config.json");
11689
+ var CONFIG_PATH = path10.join(os3.homedir(), ".lien", "config.json");
11427
11690
  var ALLOWED_KEYS = {
11428
11691
  backend: {
11429
11692
  values: ["lancedb", "qdrant"],
@@ -11462,50 +11725,50 @@ function buildPartialConfig(key, value) {
11462
11725
  async function configSetCommand(key, value) {
11463
11726
  const allowed = ALLOWED_KEYS[key];
11464
11727
  if (!allowed) {
11465
- console.error(chalk8.red(`Unknown config key: "${key}"`));
11466
- console.log(chalk8.dim("Valid keys:"), Object.keys(ALLOWED_KEYS).join(", "));
11728
+ console.error(chalk9.red(`Unknown config key: "${key}"`));
11729
+ console.log(chalk9.dim("Valid keys:"), Object.keys(ALLOWED_KEYS).join(", "));
11467
11730
  process.exit(1);
11468
11731
  }
11469
11732
  if (allowed.values.length > 0 && !allowed.values.includes(value)) {
11470
- console.error(chalk8.red(`Invalid value "${value}" for ${key}`));
11471
- console.log(chalk8.dim("Valid values:"), allowed.values.join(", "));
11733
+ console.error(chalk9.red(`Invalid value "${value}" for ${key}`));
11734
+ console.log(chalk9.dim("Valid values:"), allowed.values.join(", "));
11472
11735
  process.exit(1);
11473
11736
  }
11474
11737
  if (key === "qdrant.apiKey") {
11475
11738
  const existing = await loadGlobalConfig();
11476
11739
  if (!existing.qdrant?.url) {
11477
- console.error(chalk8.red("Set qdrant.url first before setting qdrant.apiKey"));
11740
+ console.error(chalk9.red("Set qdrant.url first before setting qdrant.apiKey"));
11478
11741
  process.exit(1);
11479
11742
  }
11480
11743
  }
11481
11744
  const partial = buildPartialConfig(key, value);
11482
11745
  await mergeGlobalConfig(partial);
11483
- console.log(chalk8.green(`Set ${key} = ${value}`));
11484
- console.log(chalk8.dim(`Config: ${CONFIG_PATH}`));
11746
+ console.log(chalk9.green(`Set ${key} = ${value}`));
11747
+ console.log(chalk9.dim(`Config: ${CONFIG_PATH}`));
11485
11748
  }
11486
11749
  async function configGetCommand(key) {
11487
11750
  if (!ALLOWED_KEYS[key]) {
11488
- console.error(chalk8.red(`Unknown config key: "${key}"`));
11489
- console.log(chalk8.dim("Valid keys:"), Object.keys(ALLOWED_KEYS).join(", "));
11751
+ console.error(chalk9.red(`Unknown config key: "${key}"`));
11752
+ console.log(chalk9.dim("Valid keys:"), Object.keys(ALLOWED_KEYS).join(", "));
11490
11753
  process.exit(1);
11491
11754
  }
11492
11755
  const config = await loadGlobalConfig();
11493
11756
  const value = getConfigValue(config, key);
11494
11757
  if (value === void 0) {
11495
- console.log(chalk8.dim(`${key}: (not set)`));
11758
+ console.log(chalk9.dim(`${key}: (not set)`));
11496
11759
  } else {
11497
11760
  console.log(`${key}: ${value}`);
11498
11761
  }
11499
11762
  }
11500
11763
  async function configListCommand() {
11501
11764
  const config = await loadGlobalConfig();
11502
- console.log(chalk8.bold("Global Configuration"));
11503
- console.log(chalk8.dim(`File: ${CONFIG_PATH}
11765
+ console.log(chalk9.bold("Global Configuration"));
11766
+ console.log(chalk9.dim(`File: ${CONFIG_PATH}
11504
11767
  `));
11505
11768
  for (const [key, meta] of Object.entries(ALLOWED_KEYS)) {
11506
11769
  const value = getConfigValue(config, key);
11507
- const display = value ?? chalk8.dim("(not set)");
11508
- console.log(` ${chalk8.cyan(key)}: ${display} ${chalk8.dim(`\u2014 ${meta.description}`)}`);
11770
+ const display = value ?? chalk9.dim("(not set)");
11771
+ console.log(` ${chalk9.cyan(key)}: ${display} ${chalk9.dim(`\u2014 ${meta.description}`)}`);
11509
11772
  }
11510
11773
  }
11511
11774
 
@@ -11539,6 +11802,7 @@ program.command("serve").description(
11539
11802
  ).option("-r, --root <path>", "Root directory to serve (defaults to current directory)").action(serveCommand);
11540
11803
  program.command("status").description("Show indexing status and statistics").option("-v, --verbose", "Show detailed settings").option("--format <type>", "Output format: text, json", "text").action(statusCommand);
11541
11804
  program.command("complexity").description("Analyze code complexity").option("--files <paths...>", "Specific files to analyze").option("--format <type>", "Output format: text, json, sarif", "text").option("--fail-on <severity>", "Exit 1 if violations: error, warning").action(complexityCommand);
11805
+ program.command("review").description("Run pluggable code review on changed files").option("--files <paths...>", "Specific files to analyze (skips git diff)").option("--format <type>", "Output format: text, json, sarif", "text").option("--fail-on <severity>", "Exit 1 if findings match: error, warning").option("--no-llm", "Skip plugins that require LLM").option("--model <name>", "LLM model to use (overrides config)").option("-v, --verbose", "Show detailed logging").option("--plugin <name>", "Run only a specific plugin").action(reviewCommand);
11542
11806
  var configCmd = program.command("config").description("Manage global configuration (~/.lien/config.json)");
11543
11807
  configCmd.command("set <key> <value>").description("Set a global config value").action(configSetCommand);
11544
11808
  configCmd.command("get <key>").description("Get a config value").action(configGetCommand);