@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 +300 -39
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
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/
|
|
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:
|
|
5146
|
-
const fullPath = [...
|
|
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,
|
|
5260
|
+
constructor(parent, value, path11, key) {
|
|
5263
5261
|
this._cachedPath = [];
|
|
5264
5262
|
this.parent = parent;
|
|
5265
5263
|
this.data = value;
|
|
5266
|
-
this._path =
|
|
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/
|
|
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 = (
|
|
9441
|
-
if (cache.has(
|
|
9442
|
-
const normalized = normalizePath(
|
|
9443
|
-
cache.set(
|
|
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/
|
|
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 (
|
|
9819
|
-
if (!cache.has(
|
|
9820
|
-
cache.set(
|
|
9816
|
+
return (path11) => {
|
|
9817
|
+
if (!cache.has(path11)) {
|
|
9818
|
+
cache.set(path11, normalizePath2(path11, workspaceRoot));
|
|
9821
9819
|
}
|
|
9822
|
-
return cache.get(
|
|
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
|
-
|
|
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/
|
|
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 =
|
|
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(
|
|
11469
|
-
console.log(
|
|
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(
|
|
11474
|
-
console.log(
|
|
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(
|
|
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(
|
|
11487
|
-
console.log(
|
|
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(
|
|
11492
|
-
console.log(
|
|
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(
|
|
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(
|
|
11506
|
-
console.log(
|
|
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 ??
|
|
11511
|
-
console.log(` ${
|
|
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);
|