@yuu1111/quality-check 0.5.1 → 0.7.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/cli.js ADDED
@@ -0,0 +1,861 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/cli.ts
5
+ import { tmpdir } from "os";
6
+ import { join as join2, resolve as resolve2 } from "path";
7
+
8
+ // ../shared/src/baseline.ts
9
+ import { existsSync, readFileSync, writeFileSync } from "fs";
10
+
11
+ // ../shared/src/json.ts
12
+ function isJsonObject(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+
16
+ // ../shared/src/baseline.ts
17
+ function entryKey(entry) {
18
+ return [entry.engine ?? "", entry.rule, entry.file, entry.text].join("\x00");
19
+ }
20
+ function compareEntries(left, right) {
21
+ const leftKey = entryKey(left);
22
+ const rightKey = entryKey(right);
23
+ if (leftKey === rightKey) {
24
+ return 0;
25
+ }
26
+ return leftKey < rightKey ? -1 : 1;
27
+ }
28
+ function toEntry(finding) {
29
+ const entry = {
30
+ count: 1,
31
+ file: finding.file,
32
+ rule: finding.rule,
33
+ text: finding.text
34
+ };
35
+ if (finding.engine !== undefined) {
36
+ entry.engine = finding.engine;
37
+ }
38
+ return entry;
39
+ }
40
+ function createBaseline(findings) {
41
+ const entries = new Map;
42
+ for (const finding of findings) {
43
+ const key = entryKey(finding);
44
+ const existing = entries.get(key);
45
+ if (existing) {
46
+ existing.count += 1;
47
+ continue;
48
+ }
49
+ entries.set(key, toEntry(finding));
50
+ }
51
+ return { entries: [...entries.values()].sort(compareEntries), version: 1 };
52
+ }
53
+ function compareWithBaseline(findings, baseline) {
54
+ const remaining = new Map;
55
+ for (const entry of baseline.entries) {
56
+ const key = entryKey(entry);
57
+ remaining.set(key, (remaining.get(key) ?? 0) + entry.count);
58
+ }
59
+ const added = [];
60
+ for (const finding of findings) {
61
+ const key = entryKey(finding);
62
+ const count = remaining.get(key) ?? 0;
63
+ if (count > 0) {
64
+ remaining.set(key, count - 1);
65
+ continue;
66
+ }
67
+ added.push(finding);
68
+ }
69
+ const resolved = baseline.entries.filter((entry) => (remaining.get(entryKey(entry)) ?? 0) > 0);
70
+ return { added, resolved };
71
+ }
72
+ function isBaselineEntry(value) {
73
+ if (!isJsonObject(value)) {
74
+ return false;
75
+ }
76
+ return (value.engine === undefined || typeof value.engine === "string") && typeof value.file === "string" && typeof value.rule === "string" && typeof value.text === "string" && typeof value.count === "number";
77
+ }
78
+ function readBaseline(path, label) {
79
+ if (!existsSync(path)) {
80
+ return { entries: [], version: 1 };
81
+ }
82
+ const value = JSON.parse(readFileSync(path, "utf8"));
83
+ if (!isJsonObject(value) || !Array.isArray(value.entries) || !value.entries.every(isBaselineEntry)) {
84
+ throw new Error(`${path} is not a ${label} baseline`);
85
+ }
86
+ return { entries: value.entries, version: 1 };
87
+ }
88
+ function writeBaseline(path, baseline) {
89
+ writeFileSync(path, `${JSON.stringify(baseline, null, "\t")}
90
+ `);
91
+ }
92
+
93
+ // ../shared/src/cli.ts
94
+ function addValue(values, name, value) {
95
+ const list = values.get(name);
96
+ if (list === undefined) {
97
+ values.set(name, [value]);
98
+ return;
99
+ }
100
+ list.push(value);
101
+ }
102
+ function addTarget(targets, argument) {
103
+ if (argument.startsWith("-")) {
104
+ throw new Error(`unknown option: ${argument}`);
105
+ }
106
+ targets.push(argument);
107
+ }
108
+ function optionName(argument) {
109
+ const separator = argument.indexOf("=");
110
+ return separator === -1 ? argument.slice(2) : argument.slice(2, separator);
111
+ }
112
+ function inlineValue(argument) {
113
+ const separator = argument.indexOf("=");
114
+ return separator === -1 ? undefined : argument.slice(separator + 1);
115
+ }
116
+ function consumeValue(values, argv, index, name) {
117
+ const value = argv[index + 1];
118
+ if (value === undefined) {
119
+ return 0;
120
+ }
121
+ addValue(values, name, value);
122
+ return 1;
123
+ }
124
+ function parseArgv(argv, spec) {
125
+ const flags = new Set;
126
+ const values = new Map;
127
+ const valueNames = new Set(spec.values ?? []);
128
+ const flagNames = new Set(spec.flags ?? []);
129
+ const targets = [];
130
+ for (let index = 0;index < argv.length; index += 1) {
131
+ const argument = argv[index] ?? "";
132
+ if (!argument.startsWith("--")) {
133
+ addTarget(targets, argument);
134
+ continue;
135
+ }
136
+ const name = optionName(argument);
137
+ if (valueNames.has(name)) {
138
+ const inline = inlineValue(argument);
139
+ if (inline !== undefined) {
140
+ addValue(values, name, inline);
141
+ continue;
142
+ }
143
+ index += consumeValue(values, argv, index, name);
144
+ continue;
145
+ }
146
+ if (flagNames.has(name) && argument.indexOf("=") === -1) {
147
+ flags.add(name);
148
+ continue;
149
+ }
150
+ throw new Error(`unknown option: ${argument}`);
151
+ }
152
+ return { flags, targets, values };
153
+ }
154
+ function wantsHelp(argv) {
155
+ return argv.includes("--help") || argv.includes("-h");
156
+ }
157
+ function runCli(main) {
158
+ async function execute() {
159
+ try {
160
+ process.exit(await main(process.argv.slice(2)));
161
+ } catch (error) {
162
+ console.error(error instanceof Error ? error.message : String(error));
163
+ process.exit(2);
164
+ }
165
+ }
166
+ execute();
167
+ }
168
+
169
+ // ../shared/src/color.ts
170
+ var ANSI_CODES = {
171
+ error: "31",
172
+ header: "36",
173
+ muted: "2",
174
+ pass: "32",
175
+ warn: "33"
176
+ };
177
+ function plainPainter(text, _tone) {
178
+ return text;
179
+ }
180
+ function ansiPainter() {
181
+ return (text, tone) => `\x1B[${ANSI_CODES[tone]}m${text}\x1B[0m`;
182
+ }
183
+ function colorEnabled(stream, env) {
184
+ if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") {
185
+ return false;
186
+ }
187
+ if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "") {
188
+ return env.FORCE_COLOR !== "0";
189
+ }
190
+ return stream.isTTY === true;
191
+ }
192
+
193
+ // src/config.ts
194
+ import { existsSync as existsSync2 } from "fs";
195
+ import { resolve } from "path";
196
+ import { pathToFileURL } from "url";
197
+ var ENGINE_NAMES = [
198
+ "biome",
199
+ "typecheck",
200
+ "knip",
201
+ "comment-check",
202
+ "document-style-check",
203
+ "tsdoc-check"
204
+ ];
205
+ var DEFAULT_CONFIG_FILES = [
206
+ "quality.config.ts",
207
+ "quality.config.mts",
208
+ "quality.config.js",
209
+ "quality.config.mjs"
210
+ ];
211
+ var DEFAULT_BASELINE_FILE = "quality-baseline.json";
212
+ function isJsonObject2(value) {
213
+ return typeof value === "object" && value !== null && !Array.isArray(value);
214
+ }
215
+ function isEngineName(value) {
216
+ return ENGINE_NAMES.includes(value);
217
+ }
218
+ var ENGINE_OPTION_KEYS = {
219
+ biome: ["args", "ignore", "targets"],
220
+ typecheck: ["args", "ignore", "projects", "targets"],
221
+ knip: ["args", "ignore", "targets"],
222
+ "comment-check": ["args", "enable", "ignore", "targets"],
223
+ "document-style-check": ["args", "enable", "ignore", "targets"],
224
+ "tsdoc-check": ["args", "error", "ignore", "targets"]
225
+ };
226
+ function readStringArray(value, field) {
227
+ if (value === undefined) {
228
+ return;
229
+ }
230
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry === "")) {
231
+ throw new Error(`${field} must be an array of non-empty strings`);
232
+ }
233
+ return [...value];
234
+ }
235
+ function readBaseline2(value, source) {
236
+ if (value === undefined || value === false) {
237
+ return value;
238
+ }
239
+ if (typeof value === "string" && value !== "") {
240
+ return value;
241
+ }
242
+ throw new Error(`${source}: baseline must be a path string or false`);
243
+ }
244
+ function parseEngines(value, source) {
245
+ if (!isJsonObject2(value)) {
246
+ throw new Error(`${source} must export an engines object`);
247
+ }
248
+ const engines = {};
249
+ for (const [name, enabled] of Object.entries(value)) {
250
+ if (!isEngineName(name)) {
251
+ throw new Error(`${source}: unknown engine: ${name}`);
252
+ }
253
+ if (typeof enabled !== "boolean") {
254
+ throw new Error(`${source}: engines.${name} must be a boolean \u8D77\u52D5\u6761\u4EF6\u306Fconfig\u3078\u7F6E\u304F`);
255
+ }
256
+ engines[name] = enabled;
257
+ }
258
+ if (ENGINE_NAMES.every((name) => !engines[name])) {
259
+ throw new Error(`${source} must enable at least one engine`);
260
+ }
261
+ return engines;
262
+ }
263
+ function parseEngineExtras(value, source, name) {
264
+ const extras = {};
265
+ if (name === "comment-check" || name === "document-style-check") {
266
+ const enable = readStringArray(value.enable, `${source}: config.${name}.enable`);
267
+ if (enable !== undefined) {
268
+ extras.enable = enable;
269
+ }
270
+ }
271
+ if (name === "tsdoc-check") {
272
+ const error = readStringArray(value.error, `${source}: config.${name}.error`);
273
+ if (error !== undefined) {
274
+ extras.error = error;
275
+ }
276
+ }
277
+ if (name === "typecheck") {
278
+ const projects = readStringArray(value.projects, `${source}: config.${name}.projects`);
279
+ if (projects !== undefined) {
280
+ if (projects.length === 0) {
281
+ throw new Error(`${source}: config.${name}.projects must not be empty`);
282
+ }
283
+ extras.projects = projects;
284
+ }
285
+ }
286
+ return extras;
287
+ }
288
+ function parseEngineOptions(value, source, name) {
289
+ const unknown = Object.keys(value).filter((key) => !ENGINE_OPTION_KEYS[name].includes(key));
290
+ if (unknown.length > 0) {
291
+ throw new Error(`${source}: config.${name} has an unknown option: ${unknown[0]}`);
292
+ }
293
+ const options = {};
294
+ const ignore = readStringArray(value.ignore, `${source}: config.${name}.ignore`);
295
+ if (ignore !== undefined) {
296
+ options.ignore = ignore;
297
+ }
298
+ const targets = readStringArray(value.targets, `${source}: config.${name}.targets`);
299
+ if (targets !== undefined) {
300
+ options.targets = targets;
301
+ }
302
+ const args = readStringArray(value.args, `${source}: config.${name}.args`);
303
+ if (args !== undefined) {
304
+ options.args = args;
305
+ }
306
+ return { ...options, ...parseEngineExtras(value, source, name) };
307
+ }
308
+ function parseEngineConfig(value, source) {
309
+ if (value === undefined) {
310
+ return;
311
+ }
312
+ if (!isJsonObject2(value)) {
313
+ throw new Error(`${source}: config must be an object`);
314
+ }
315
+ const config = {};
316
+ for (const [name, options] of Object.entries(value)) {
317
+ if (!isEngineName(name)) {
318
+ throw new Error(`${source}: unknown engine in config: ${name}`);
319
+ }
320
+ if (!isJsonObject2(options)) {
321
+ throw new Error(`${source}: config.${name} must be an object`);
322
+ }
323
+ config[name] = parseEngineOptions(options, source, name);
324
+ }
325
+ return config;
326
+ }
327
+ function parseConfig(value, source) {
328
+ if (!isJsonObject2(value)) {
329
+ throw new Error(`${source} must export a config object`);
330
+ }
331
+ const config = {
332
+ engines: parseEngines(value.engines, source)
333
+ };
334
+ const engineConfig = parseEngineConfig(value.config, source);
335
+ if (engineConfig !== undefined) {
336
+ config.config = engineConfig;
337
+ }
338
+ const baseline = readBaseline2(value.baseline, source);
339
+ if (baseline !== undefined) {
340
+ config.baseline = baseline;
341
+ }
342
+ return config;
343
+ }
344
+ function enabledEngines(config) {
345
+ return ENGINE_NAMES.filter((name) => Boolean(config.engines[name]));
346
+ }
347
+ function engineConfig(config, name) {
348
+ if (!config.engines[name]) {
349
+ return null;
350
+ }
351
+ return config.config?.[name] ?? {};
352
+ }
353
+ function findConfigFile(cwd) {
354
+ for (const name of DEFAULT_CONFIG_FILES) {
355
+ const candidate = resolve(cwd, name);
356
+ if (existsSync2(candidate)) {
357
+ return candidate;
358
+ }
359
+ }
360
+ return null;
361
+ }
362
+ async function loadConfig(path) {
363
+ const module = await import(pathToFileURL(path).href);
364
+ const value = isJsonObject2(module) && "default" in module ? module.default : module;
365
+ return parseConfig(value, path);
366
+ }
367
+
368
+ // ../shared/src/findings.ts
369
+ function formatLocation(finding) {
370
+ return `${finding.file}:${finding.line}:${finding.column}`;
371
+ }
372
+
373
+ // src/engines.ts
374
+ import { existsSync as existsSync3 } from "fs";
375
+ import { dirname, join } from "path";
376
+ var ENGINE_BINS = {
377
+ biome: "biome",
378
+ "comment-check": "comment-check",
379
+ "document-style-check": "document-style-check",
380
+ knip: "knip",
381
+ "tsdoc-check": "tsdoc-check",
382
+ typecheck: "tsc"
383
+ };
384
+ var WINDOWS_SHIMS = [".exe", ".cmd", ".bat", ""];
385
+ var POSIX_SHIMS = [""];
386
+ var ENGINE_LIMITS = {
387
+ biome: { ignore: "biome.json holds its settings" },
388
+ typecheck: {
389
+ ignore: "tsconfig.json holds its settings",
390
+ targets: "tsconfig.json and projects hold its settings"
391
+ },
392
+ knip: {
393
+ ignore: "knip.ts holds its settings",
394
+ targets: "knip analyzes the whole project"
395
+ },
396
+ "comment-check": {},
397
+ "document-style-check": {},
398
+ "tsdoc-check": {}
399
+ };
400
+ function skippedEngineOptions(name, options) {
401
+ if (options === undefined) {
402
+ return [];
403
+ }
404
+ const limits = ENGINE_LIMITS[name];
405
+ const skipped = [];
406
+ for (const key of ["ignore", "targets"]) {
407
+ const reason = limits[key];
408
+ if (reason !== undefined && options[key] !== undefined) {
409
+ skipped.push(`${key} skipped (${reason})`);
410
+ }
411
+ }
412
+ return skipped;
413
+ }
414
+ function isFindingEngine(name) {
415
+ return name === "comment-check" || name === "document-style-check" || name === "tsdoc-check";
416
+ }
417
+ function resolveExecutable(name, cwd) {
418
+ const shims = process.platform === "win32" ? WINDOWS_SHIMS : POSIX_SHIMS;
419
+ const bin = ENGINE_BINS[name];
420
+ let directory = cwd;
421
+ for (;; ) {
422
+ for (const shim of shims) {
423
+ const candidate = join(directory, "node_modules", ".bin", `${bin}${shim}`);
424
+ if (existsSync3(candidate)) {
425
+ return candidate;
426
+ }
427
+ }
428
+ const parent = dirname(directory);
429
+ if (parent === directory) {
430
+ break;
431
+ }
432
+ directory = parent;
433
+ }
434
+ const onPath = Bun.which(bin);
435
+ return onPath ?? null;
436
+ }
437
+ function colorArguments(name, color) {
438
+ if (!color) {
439
+ return [];
440
+ }
441
+ if (name === "biome") {
442
+ return ["--colors=force"];
443
+ }
444
+ if (name === "typecheck") {
445
+ return ["--pretty"];
446
+ }
447
+ return [];
448
+ }
449
+ function buildTypecheckCommand(executable, context, project) {
450
+ const options = engineConfig(context.config, "typecheck") ?? {};
451
+ return [
452
+ executable,
453
+ "--noEmit",
454
+ ...project === undefined ? [] : ["--project", project],
455
+ ...colorArguments("typecheck", context.color),
456
+ ...options.args ?? []
457
+ ];
458
+ }
459
+ function enableArguments(options) {
460
+ return (options?.enable ?? []).flatMap((rule) => ["--enable", rule]);
461
+ }
462
+ function buildEngineCommand(name, executable, context) {
463
+ const options = engineConfig(context.config, name) ?? {};
464
+ const limits = ENGINE_LIMITS[name];
465
+ const extra = options.args ?? [];
466
+ const ignores = limits.ignore === undefined ? [...options.ignore ?? [], ...context.overrides.ignore] : [];
467
+ const requested = context.overrides.targets.length > 0 ? context.overrides.targets : options.targets ?? ["."];
468
+ const targets = limits.targets === undefined ? requested : [];
469
+ const ignoreArguments = ignores.flatMap((ignore) => ["--ignore", ignore]);
470
+ const rules = engineConfig(context.config, "tsdoc-check")?.error ?? [];
471
+ const errorArguments = rules.flatMap((rule) => ["--error", rule]);
472
+ if (name === "biome") {
473
+ return [
474
+ executable,
475
+ "check",
476
+ ...colorArguments(name, context.color),
477
+ ...targets,
478
+ ...extra
479
+ ];
480
+ }
481
+ if (name === "typecheck") {
482
+ return buildTypecheckCommand(executable, context, undefined);
483
+ }
484
+ if (name === "knip") {
485
+ return [executable, ...extra];
486
+ }
487
+ if (name === "comment-check") {
488
+ return [
489
+ executable,
490
+ "--json",
491
+ "--baseline",
492
+ context.rawBaseline,
493
+ ...enableArguments(engineConfig(context.config, "comment-check")),
494
+ ...targets,
495
+ ...ignoreArguments,
496
+ ...extra
497
+ ];
498
+ }
499
+ if (name === "document-style-check") {
500
+ return [
501
+ executable,
502
+ "lint",
503
+ "--json",
504
+ ...enableArguments(engineConfig(context.config, "document-style-check")),
505
+ ...targets,
506
+ ...ignoreArguments,
507
+ ...extra
508
+ ];
509
+ }
510
+ return [
511
+ executable,
512
+ "--json",
513
+ ...errorArguments,
514
+ ...targets,
515
+ ...ignoreArguments,
516
+ ...extra
517
+ ];
518
+ }
519
+ function buildEngineCommands(name, executable, context) {
520
+ const projects = name === "typecheck" ? engineConfig(context.config, "typecheck")?.projects ?? [] : [];
521
+ if (projects.length === 0) {
522
+ return [buildEngineCommand(name, executable, context)];
523
+ }
524
+ return projects.map((project) => buildTypecheckCommand(executable, context, project));
525
+ }
526
+ async function runEngineProcess(command, options) {
527
+ const child = Bun.spawn({
528
+ cmd: command,
529
+ cwd: options.cwd,
530
+ stderr: "pipe",
531
+ stdout: "pipe"
532
+ });
533
+ const [stdout, stderr, exitCode] = await Promise.all([
534
+ new Response(child.stdout).text(),
535
+ new Response(child.stderr).text(),
536
+ child.exited
537
+ ]);
538
+ return { exitCode, stderr, stdout };
539
+ }
540
+
541
+ // src/report.ts
542
+ function describeFinding(finding, paint, tone) {
543
+ return `${formatLocation(finding)} ${finding.rule} ${paint(finding.severity, tone)} ${finding.text}`;
544
+ }
545
+ function describeCounts(result) {
546
+ if (!isFindingEngine(result.name)) {
547
+ return `exit ${result.exitCode}`;
548
+ }
549
+ return `${result.reported.length} new, ${result.resolved} resolved, ${result.warnings.length} warnings`;
550
+ }
551
+ function describeStatus(result, paint) {
552
+ if (result.status === "error") {
553
+ return paint("error", "error");
554
+ }
555
+ const tone = result.status === "passed" ? "pass" : "error";
556
+ return `${paint(result.status, tone)} (${describeCounts(result)})`;
557
+ }
558
+ function formatEngineSection(result, paint = plainPainter) {
559
+ const lines = [paint(`== ${result.name} ==`, "header")];
560
+ for (const skipped of result.skipped) {
561
+ lines.push(paint(`${result.name}: ${skipped}`, "muted"));
562
+ }
563
+ if (result.output !== "" && !isFindingEngine(result.name)) {
564
+ lines.push(result.output);
565
+ }
566
+ for (const finding of result.reported) {
567
+ lines.push(describeFinding(finding, paint, "error"));
568
+ }
569
+ for (const finding of result.warnings) {
570
+ lines.push(describeFinding(finding, paint, "warn"));
571
+ }
572
+ if (result.message !== undefined) {
573
+ lines.push(paint(result.message, "error"));
574
+ }
575
+ lines.push(`${result.name}: ${describeStatus(result, paint)}`);
576
+ return lines.join(`
577
+ `);
578
+ }
579
+ function formatSummary(results, paint = plainPainter) {
580
+ const failed = results.filter((result) => result.status !== "passed").map((result) => result.name);
581
+ const passed = results.filter((result) => result.status === "passed").map((result) => result.name);
582
+ if (failed.length === 0) {
583
+ return paint(`quality-check: ${results.length} engines passed`, "pass");
584
+ }
585
+ const lines = [
586
+ paint(`quality-check: ${failed.length} of ${results.length} engines failed`, "error"),
587
+ ` ${paint("failed:", "error")} ${failed.join(", ")}`
588
+ ];
589
+ if (passed.length > 0) {
590
+ lines.push(` ${paint("passed:", "pass")} ${passed.join(", ")}`);
591
+ }
592
+ return lines.join(`
593
+ `);
594
+ }
595
+ function toJsonReport(results) {
596
+ return {
597
+ engines: results.map((result) => ({
598
+ detected: result.detected.length,
599
+ exitCode: result.exitCode,
600
+ message: result.message ?? null,
601
+ name: result.name,
602
+ reported: result.reported,
603
+ resolved: result.resolved,
604
+ skipped: result.skipped,
605
+ status: result.status,
606
+ warnings: result.warnings
607
+ })),
608
+ failed: results.filter((result) => result.status !== "passed").map((result) => result.name)
609
+ };
610
+ }
611
+
612
+ // src/findings.ts
613
+ function readFinding(engine, value, severity, textField) {
614
+ if (!isJsonObject(value)) {
615
+ throw new Error(`${engine} printed an unexpected finding`);
616
+ }
617
+ const { column, file, line, rule } = value;
618
+ const text = value[textField];
619
+ if (typeof rule !== "string" || typeof file !== "string" || typeof line !== "number" || typeof column !== "number" || typeof text !== "string") {
620
+ throw new Error(`${engine} printed an unexpected finding`);
621
+ }
622
+ return { column, engine, file, line, rule, severity, text };
623
+ }
624
+ function readEntries(engine, value, field) {
625
+ if (!Array.isArray(value)) {
626
+ throw new Error(`${engine} printed JSON without a ${field} array`);
627
+ }
628
+ return value;
629
+ }
630
+ function parseJson(engine, stdout) {
631
+ let value;
632
+ try {
633
+ value = JSON.parse(stdout);
634
+ } catch {
635
+ throw new Error(`${engine} did not print JSON`);
636
+ }
637
+ if (!isJsonObject(value)) {
638
+ throw new Error(`${engine} printed an unexpected JSON value`);
639
+ }
640
+ return value;
641
+ }
642
+ function parseFindings(engine, stdout) {
643
+ const value = parseJson(engine, stdout);
644
+ if (engine === "comment-check") {
645
+ return {
646
+ errors: readEntries(engine, value.added, "added").map((entry) => readFinding(engine, entry, "error", "text")),
647
+ warnings: []
648
+ };
649
+ }
650
+ return {
651
+ errors: readEntries(engine, value.errors, "errors").map((entry) => readFinding(engine, entry, "error", "message")),
652
+ warnings: readEntries(engine, value.warnings, "warnings").map((entry) => readFinding(engine, entry, "warning", "message"))
653
+ };
654
+ }
655
+
656
+ // src/run.ts
657
+ function baseResult(name, exitCode, output) {
658
+ return {
659
+ detected: [],
660
+ exitCode,
661
+ name,
662
+ output,
663
+ reported: [],
664
+ resolved: 0,
665
+ skipped: [],
666
+ warnings: []
667
+ };
668
+ }
669
+ function joinOutput(result) {
670
+ return [result.stdout, result.stderr].map((part) => part.trimEnd()).filter((part) => part !== "").join(`
671
+ `);
672
+ }
673
+ function describeError(error) {
674
+ return error instanceof Error ? error.message : String(error);
675
+ }
676
+ function combineExitCode(codes) {
677
+ if (codes.some((code) => code === 2)) {
678
+ return 2;
679
+ }
680
+ const failed = codes.find((code) => code !== 0);
681
+ return failed === undefined ? 0 : failed;
682
+ }
683
+ async function runCommands(commands, options, runner) {
684
+ const executed = [];
685
+ for (const command of commands) {
686
+ executed.push({ command, result: await runner(command, options) });
687
+ }
688
+ const labeled = commands.length > 1;
689
+ const output = executed.map(({ command, result }) => {
690
+ const text = joinOutput(result);
691
+ if (!labeled) {
692
+ return text;
693
+ }
694
+ const header = `$ ${command.join(" ")}`;
695
+ return text === "" ? header : `${header}
696
+ ${text}`;
697
+ }).filter((block) => block !== "").join(`
698
+
699
+ `);
700
+ const exitCode = combineExitCode(executed.map((entry) => entry.result.exitCode));
701
+ return { exitCode, output };
702
+ }
703
+ async function runFindingEngine(name, options, executable, context, runner) {
704
+ const result = await runner(buildEngineCommand(name, executable, context), {
705
+ cwd: options.cwd
706
+ });
707
+ const output = joinOutput(result);
708
+ if (result.exitCode === 2) {
709
+ return {
710
+ ...baseResult(name, 2, output),
711
+ message: `${name} could not finish`,
712
+ status: "error"
713
+ };
714
+ }
715
+ let parsed;
716
+ try {
717
+ parsed = parseFindings(name, result.stdout);
718
+ } catch (error) {
719
+ return {
720
+ ...baseResult(name, result.exitCode, output),
721
+ message: describeError(error),
722
+ status: "error"
723
+ };
724
+ }
725
+ const comparison = options.baseline === null ? { added: parsed.errors, resolved: [] } : compareWithBaseline(parsed.errors, options.baseline);
726
+ return {
727
+ ...baseResult(name, result.exitCode, output),
728
+ detected: parsed.errors,
729
+ reported: comparison.added,
730
+ resolved: comparison.resolved.length,
731
+ status: comparison.added.length > 0 ? "failed" : "passed",
732
+ warnings: parsed.warnings
733
+ };
734
+ }
735
+ async function runProcessEngine(name, options, executable, context, runner) {
736
+ const commands = buildEngineCommands(name, executable, context);
737
+ const { exitCode, output } = await runCommands(commands, { cwd: options.cwd }, runner);
738
+ if (exitCode === 2) {
739
+ return {
740
+ ...baseResult(name, 2, output),
741
+ message: `${name} could not finish`,
742
+ status: "error"
743
+ };
744
+ }
745
+ return {
746
+ ...baseResult(name, exitCode, output),
747
+ status: exitCode === 0 ? "passed" : "failed"
748
+ };
749
+ }
750
+ async function executeEngine(name, options, context, runner) {
751
+ const executable = (options.resolve ?? resolveExecutable)(name, options.cwd);
752
+ if (executable === null) {
753
+ return {
754
+ ...baseResult(name, null, ""),
755
+ message: `${ENGINE_BINS[name]} is not installed`,
756
+ status: "error"
757
+ };
758
+ }
759
+ if (isFindingEngine(name)) {
760
+ return await runFindingEngine(name, options, executable, context, runner);
761
+ }
762
+ return await runProcessEngine(name, options, executable, context, runner);
763
+ }
764
+ async function runEngine(name, options, context, runner) {
765
+ const result = await executeEngine(name, options, context, runner);
766
+ return {
767
+ ...result,
768
+ skipped: skippedEngineOptions(name, options.config.config?.[name])
769
+ };
770
+ }
771
+ async function runEngines(options) {
772
+ const runner = options.runner ?? runEngineProcess;
773
+ const context = {
774
+ color: options.color,
775
+ config: options.config,
776
+ overrides: options.overrides,
777
+ rawBaseline: options.rawBaseline
778
+ };
779
+ const results = [];
780
+ for (const name of enabledEngines(options.config)) {
781
+ results.push(await runEngine(name, options, context, runner));
782
+ }
783
+ return results;
784
+ }
785
+
786
+ // src/cli.ts
787
+ var USAGE = "Usage: quality-check [--config <path>] [--baseline <path>] [--ignore <path>] [--update-baseline] [--json] [path...]";
788
+ function parseArguments(argv) {
789
+ const parsed = parseArgv(argv, {
790
+ flags: ["json", "update-baseline"],
791
+ values: ["baseline", "config", "ignore"]
792
+ });
793
+ return {
794
+ baselinePath: parsed.values.get("baseline")?.at(-1),
795
+ configPath: parsed.values.get("config")?.at(-1),
796
+ ignores: [...parsed.values.get("ignore") ?? []],
797
+ json: parsed.flags.has("json"),
798
+ targets: parsed.targets,
799
+ update: parsed.flags.has("update-baseline")
800
+ };
801
+ }
802
+ function resolveBaselinePath(options, config, cwd) {
803
+ if (options.baselinePath !== undefined) {
804
+ return resolve2(cwd, options.baselinePath);
805
+ }
806
+ if (config.baseline === false) {
807
+ return null;
808
+ }
809
+ return resolve2(cwd, config.baseline ?? DEFAULT_BASELINE_FILE);
810
+ }
811
+ function resolveConfigPath(options, cwd) {
812
+ if (options.configPath !== undefined) {
813
+ return resolve2(cwd, options.configPath);
814
+ }
815
+ return findConfigFile(cwd);
816
+ }
817
+ async function main(argv) {
818
+ if (wantsHelp(argv)) {
819
+ console.log(USAGE);
820
+ return 0;
821
+ }
822
+ const options = parseArguments(argv);
823
+ const color = colorEnabled(process.stdout, process.env);
824
+ const paint = color ? ansiPainter() : plainPainter;
825
+ const cwd = process.cwd();
826
+ const configPath = resolveConfigPath(options, cwd);
827
+ if (configPath === null) {
828
+ console.error(`quality.config.ts not found in ${cwd}`);
829
+ return 2;
830
+ }
831
+ const config = await loadConfig(configPath);
832
+ const baselinePath = resolveBaselinePath(options, config, cwd);
833
+ const results = await runEngines({
834
+ baseline: options.update || baselinePath === null ? null : readBaseline(baselinePath, "quality"),
835
+ color,
836
+ config,
837
+ cwd,
838
+ overrides: { ignore: options.ignores, targets: options.targets },
839
+ rawBaseline: join2(tmpdir(), `quality-check-raw-${process.pid}.json`)
840
+ });
841
+ if (options.update) {
842
+ if (baselinePath === null) {
843
+ console.error("baseline is disabled by the configuration");
844
+ return 2;
845
+ }
846
+ const baseline = createBaseline(results.flatMap((result) => result.detected));
847
+ writeBaseline(baselinePath, baseline);
848
+ console.log(paint(`Recorded ${baseline.entries.length} entries in ${baselinePath}`, "pass"));
849
+ return 0;
850
+ }
851
+ if (options.json) {
852
+ console.log(JSON.stringify(toJsonReport(results), null, "\t"));
853
+ } else {
854
+ for (const result of results) {
855
+ console.log(formatEngineSection(result, paint));
856
+ }
857
+ console.log(formatSummary(results, paint));
858
+ }
859
+ return results.some((result) => result.status !== "passed") ? 1 : 0;
860
+ }
861
+ runCli(main);