@liendev/lien 0.40.0 → 0.42.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 +292 -28
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
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:
|
|
5144
|
-
const fullPath = [...
|
|
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,
|
|
5260
|
+
constructor(parent, value, path11, key) {
|
|
5261
5261
|
this._cachedPath = [];
|
|
5262
5262
|
this.parent = parent;
|
|
5263
5263
|
this.data = value;
|
|
5264
|
-
this._path =
|
|
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 = (
|
|
9439
|
-
if (cache.has(
|
|
9440
|
-
const normalized = normalizePath(
|
|
9441
|
-
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);
|
|
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 (
|
|
9817
|
-
if (!cache.has(
|
|
9818
|
-
cache.set(
|
|
9816
|
+
return (path11) => {
|
|
9817
|
+
if (!cache.has(path11)) {
|
|
9818
|
+
cache.set(path11, normalizePath2(path11, workspaceRoot));
|
|
9819
9819
|
}
|
|
9820
|
-
return cache.get(
|
|
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/
|
|
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 =
|
|
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(
|
|
11466
|
-
console.log(
|
|
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(
|
|
11471
|
-
console.log(
|
|
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(
|
|
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(
|
|
11484
|
-
console.log(
|
|
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(
|
|
11489
|
-
console.log(
|
|
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(
|
|
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(
|
|
11503
|
-
console.log(
|
|
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 ??
|
|
11508
|
-
console.log(` ${
|
|
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);
|