@liendev/lien 0.39.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
@@ -3828,13 +3828,11 @@ import {
3828
3828
  getCurrentBranch,
3829
3829
  getCurrentCommit,
3830
3830
  readVersionFile,
3831
- extractRepoId,
3832
3831
  DEFAULT_CONCURRENCY,
3833
3832
  DEFAULT_EMBEDDING_BATCH_SIZE,
3834
- DEFAULT_CHUNK_SIZE,
3835
- DEFAULT_CHUNK_OVERLAP,
3836
3833
  DEFAULT_GIT_POLL_INTERVAL_MS
3837
3834
  } from "@liendev/core";
3835
+ import { extractRepoId, DEFAULT_CHUNK_SIZE, DEFAULT_CHUNK_OVERLAP } from "@liendev/parser";
3838
3836
  var VALID_FORMATS = ["text", "json"];
3839
3837
  async function getFileStats(filePath) {
3840
3838
  try {
@@ -4251,7 +4249,7 @@ import {
4251
4249
  detectEcosystems,
4252
4250
  getEcosystemExcludePatterns,
4253
4251
  ALWAYS_IGNORE_PATTERNS
4254
- } from "@liendev/core";
4252
+ } from "@liendev/parser";
4255
4253
  var FileWatcher = class {
4256
4254
  watcher = null;
4257
4255
  rootDir;
@@ -5142,8 +5140,8 @@ function getErrorMap() {
5142
5140
 
5143
5141
  // ../../node_modules/zod/v3/helpers/parseUtil.js
5144
5142
  var makeIssue = (params) => {
5145
- const { data, path: path10, errorMaps, issueData } = params;
5146
- const fullPath = [...path10, ...issueData.path || []];
5143
+ const { data, path: path11, errorMaps, issueData } = params;
5144
+ const fullPath = [...path11, ...issueData.path || []];
5147
5145
  const fullIssue = {
5148
5146
  ...issueData,
5149
5147
  path: fullPath
@@ -5259,11 +5257,11 @@ var errorUtil;
5259
5257
 
5260
5258
  // ../../node_modules/zod/v3/types.js
5261
5259
  var ParseInputLazyPath = class {
5262
- constructor(parent, value, path10, key) {
5260
+ constructor(parent, value, path11, key) {
5263
5261
  this._cachedPath = [];
5264
5262
  this.parent = parent;
5265
5263
  this.data = value;
5266
- this._path = path10;
5264
+ this._path = path11;
5267
5265
  this._key = key;
5268
5266
  }
5269
5267
  get path() {
@@ -9392,7 +9390,7 @@ import {
9392
9390
  getCanonicalPath,
9393
9391
  isTestFile,
9394
9392
  MAX_CHUNKS_PER_FILE
9395
- } from "@liendev/core";
9393
+ } from "@liendev/parser";
9396
9394
  var SCAN_LIMIT = 1e4;
9397
9395
  async function searchFileChunks(filepaths, ctx) {
9398
9396
  const { vectorDB, workspaceRoot } = ctx;
@@ -9437,10 +9435,10 @@ async function findRelatedChunks(filepaths, fileChunksMap, ctx) {
9437
9435
  }
9438
9436
  function createPathCache(workspaceRoot) {
9439
9437
  const cache = /* @__PURE__ */ new Map();
9440
- const normalize = (path10) => {
9441
- if (cache.has(path10)) return cache.get(path10);
9442
- const normalized = normalizePath(path10, workspaceRoot);
9443
- 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);
9444
9442
  return normalized;
9445
9443
  };
9446
9444
  return { normalize, cache };
@@ -9648,7 +9646,7 @@ import {
9648
9646
  matchesFile as matchesFile2,
9649
9647
  getCanonicalPath as getCanonicalPath2,
9650
9648
  isTestFile as isTestFile2
9651
- } from "@liendev/core";
9649
+ } from "@liendev/parser";
9652
9650
  var COMPLEXITY_THRESHOLDS = {
9653
9651
  HIGH_COMPLEXITY_DEPENDENT: 10,
9654
9652
  // Individual file is complex
@@ -9815,11 +9813,11 @@ async function scanChunksPaginated(vectorDB, crossRepo, log, normalizePathCached
9815
9813
  function createPathNormalizer() {
9816
9814
  const workspaceRoot = process.cwd().replace(/\\/g, "/");
9817
9815
  const cache = /* @__PURE__ */ new Map();
9818
- return (path10) => {
9819
- if (!cache.has(path10)) {
9820
- cache.set(path10, normalizePath2(path10, workspaceRoot));
9816
+ return (path11) => {
9817
+ if (!cache.has(path11)) {
9818
+ cache.set(path11, normalizePath2(path11, workspaceRoot));
9821
9819
  }
9822
- return cache.get(path10);
9820
+ return cache.get(path11);
9823
9821
  };
9824
9822
  }
9825
9823
  function groupChunksByFile(chunks) {
@@ -10566,9 +10564,9 @@ import {
10566
10564
  indexMultipleFiles as indexMultipleFiles2,
10567
10565
  isGitAvailable,
10568
10566
  isGitRepo as isGitRepo2,
10569
- DEFAULT_GIT_POLL_INTERVAL_MS as DEFAULT_GIT_POLL_INTERVAL_MS2,
10570
- createGitignoreFilter as createGitignoreFilter2
10567
+ DEFAULT_GIT_POLL_INTERVAL_MS as DEFAULT_GIT_POLL_INTERVAL_MS2
10571
10568
  } from "@liendev/core";
10569
+ import { createGitignoreFilter as createGitignoreFilter2 } from "@liendev/parser";
10572
10570
 
10573
10571
  // src/mcp/file-change-handler.ts
10574
10572
  import fs3 from "fs/promises";
@@ -10576,10 +10574,9 @@ import {
10576
10574
  indexMultipleFiles,
10577
10575
  indexSingleFile,
10578
10576
  ManifestManager,
10579
- computeContentHash,
10580
- normalizeToRelativePath as normalizeToRelativePath2,
10581
- createGitignoreFilter
10577
+ normalizeToRelativePath as normalizeToRelativePath2
10582
10578
  } from "@liendev/core";
10579
+ import { computeContentHash, createGitignoreFilter } from "@liendev/parser";
10583
10580
  async function handleFileDeletion(filepath, vectorDB, manifest, log) {
10584
10581
  log(`\u{1F5D1}\uFE0F File deleted: ${filepath}`);
10585
10582
  try {
@@ -11421,12 +11418,275 @@ async function complexityCommand(options) {
11421
11418
  }
11422
11419
  }
11423
11420
 
11424
- // src/cli/config.ts
11421
+ // src/cli/review.ts
11422
+ import { execFile } from "child_process";
11423
+ import { promisify } from "util";
11425
11424
  import chalk8 from "chalk";
11425
+ import ora3 from "ora";
11426
+ import fs7 from "fs";
11426
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";
11427
11687
  import os3 from "os";
11428
11688
  import { loadGlobalConfig, mergeGlobalConfig } from "@liendev/core";
11429
- var CONFIG_PATH = path9.join(os3.homedir(), ".lien", "config.json");
11689
+ var CONFIG_PATH = path10.join(os3.homedir(), ".lien", "config.json");
11430
11690
  var ALLOWED_KEYS = {
11431
11691
  backend: {
11432
11692
  values: ["lancedb", "qdrant"],
@@ -11465,50 +11725,50 @@ function buildPartialConfig(key, value) {
11465
11725
  async function configSetCommand(key, value) {
11466
11726
  const allowed = ALLOWED_KEYS[key];
11467
11727
  if (!allowed) {
11468
- console.error(chalk8.red(`Unknown config key: "${key}"`));
11469
- 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(", "));
11470
11730
  process.exit(1);
11471
11731
  }
11472
11732
  if (allowed.values.length > 0 && !allowed.values.includes(value)) {
11473
- console.error(chalk8.red(`Invalid value "${value}" for ${key}`));
11474
- 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(", "));
11475
11735
  process.exit(1);
11476
11736
  }
11477
11737
  if (key === "qdrant.apiKey") {
11478
11738
  const existing = await loadGlobalConfig();
11479
11739
  if (!existing.qdrant?.url) {
11480
- 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"));
11481
11741
  process.exit(1);
11482
11742
  }
11483
11743
  }
11484
11744
  const partial = buildPartialConfig(key, value);
11485
11745
  await mergeGlobalConfig(partial);
11486
- console.log(chalk8.green(`Set ${key} = ${value}`));
11487
- console.log(chalk8.dim(`Config: ${CONFIG_PATH}`));
11746
+ console.log(chalk9.green(`Set ${key} = ${value}`));
11747
+ console.log(chalk9.dim(`Config: ${CONFIG_PATH}`));
11488
11748
  }
11489
11749
  async function configGetCommand(key) {
11490
11750
  if (!ALLOWED_KEYS[key]) {
11491
- console.error(chalk8.red(`Unknown config key: "${key}"`));
11492
- 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(", "));
11493
11753
  process.exit(1);
11494
11754
  }
11495
11755
  const config = await loadGlobalConfig();
11496
11756
  const value = getConfigValue(config, key);
11497
11757
  if (value === void 0) {
11498
- console.log(chalk8.dim(`${key}: (not set)`));
11758
+ console.log(chalk9.dim(`${key}: (not set)`));
11499
11759
  } else {
11500
11760
  console.log(`${key}: ${value}`);
11501
11761
  }
11502
11762
  }
11503
11763
  async function configListCommand() {
11504
11764
  const config = await loadGlobalConfig();
11505
- console.log(chalk8.bold("Global Configuration"));
11506
- console.log(chalk8.dim(`File: ${CONFIG_PATH}
11765
+ console.log(chalk9.bold("Global Configuration"));
11766
+ console.log(chalk9.dim(`File: ${CONFIG_PATH}
11507
11767
  `));
11508
11768
  for (const [key, meta] of Object.entries(ALLOWED_KEYS)) {
11509
11769
  const value = getConfigValue(config, key);
11510
- const display = value ?? chalk8.dim("(not set)");
11511
- 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}`)}`);
11512
11772
  }
11513
11773
  }
11514
11774
 
@@ -11542,6 +11802,7 @@ program.command("serve").description(
11542
11802
  ).option("-r, --root <path>", "Root directory to serve (defaults to current directory)").action(serveCommand);
11543
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);
11544
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);
11545
11806
  var configCmd = program.command("config").description("Manage global configuration (~/.lien/config.json)");
11546
11807
  configCmd.command("set <key> <value>").description("Set a global config value").action(configSetCommand);
11547
11808
  configCmd.command("get <key>").description("Get a config value").action(configGetCommand);