qualink 0.5.0 → 0.6.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.
@@ -1,4 +1,6 @@
1
+ import { stat } from "node:fs/promises";
1
2
  import { defineCommand } from "citty";
3
+ import { formatBytes } from "../utils/format.js";
2
4
  import { CliError } from "./cli-error.js";
3
5
  import { commonArgs, isDryRun } from "./common-args.js";
4
6
  import { parseCommonMetadata } from "./parse-metadata.js";
@@ -16,6 +18,11 @@ export function createCollectorCommand(config) {
16
18
  async run({ args }) {
17
19
  try {
18
20
  const parsedArgs = args;
21
+ const inputPath = typeof parsedArgs.input === "string" ? parsedArgs.input : undefined;
22
+ if (inputPath) {
23
+ const fileStat = await stat(inputPath);
24
+ process.stderr.write(` read: ${inputPath} (${formatBytes(fileStat.size)})\n`);
25
+ }
19
26
  const metadata = parseCommonMetadata(parsedArgs);
20
27
  const { metricType, documents } = await config.collect(parsedArgs, metadata);
21
28
  await sendToSink(metricType, parsedArgs, documents);
package/dist/cli/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { relative } from "node:path";
1
2
  import { defineCommand, runMain } from "citty";
2
3
  import { CliError } from "./cli-error.js";
3
4
  import { biomeCommand, coverageDotnetCommand, coverageJsCommand, eslintCommand, junitCommand, lighthouseCommand, metaCommand, pipelineCommand, sarifCommand, } from "./commands/index.js";
@@ -54,6 +55,11 @@ const collectCommand = defineCommand({
54
55
  async function runDirMode(dir, args) {
55
56
  const metadata = parseCommonMetadata(args);
56
57
  const discovered = await discoverFiles(dir);
58
+ for (const [collectorKey, files] of discovered) {
59
+ for (const filePath of files) {
60
+ process.stderr.write(` scan: ${relative(dir, filePath)} → ${collectorKey}\n`);
61
+ }
62
+ }
57
63
  const accumulated = new Map();
58
64
  const counts = new Map();
59
65
  for (const [collectorKey, files] of discovered) {
@@ -78,6 +84,11 @@ async function runConfigMode(configValue, args) {
78
84
  const metadata = parseCommonMetadata(args);
79
85
  const entries = await parseConfig(configValue);
80
86
  const resolved = await resolveConfig(entries, ".");
87
+ for (const entry of resolved) {
88
+ for (const filePath of entry.files) {
89
+ process.stderr.write(` scan: ${filePath} → ${entry.type}\n`);
90
+ }
91
+ }
81
92
  const accumulated = new Map();
82
93
  const counts = new Map();
83
94
  for (const entry of resolved) {
@@ -1,3 +1,4 @@
1
+ import { INDEX_BY_TYPE } from "../sinks/elastic.js";
1
2
  import { createSink } from "../sinks/index.js";
2
3
  import { CliError } from "./cli-error.js";
3
4
  import { argValue, envOrArg, isDryRun } from "./common-args.js";
@@ -65,8 +66,22 @@ export async function sendToSink(metricType, args, documents) {
65
66
  sinkConfig = sinkConfigBase;
66
67
  }
67
68
  const sink = createSink(sinkConfig);
68
- await sink.send({
69
+ const { durationMs } = await sink.send({
69
70
  metricType,
70
71
  documents,
71
72
  });
73
+ const count = documents.length;
74
+ const ms = Math.round(durationMs);
75
+ if (sinkKind === "elastic") {
76
+ const index = INDEX_BY_TYPE[metricType];
77
+ const url = sinkConfig.elasticUrl ?? "";
78
+ process.stderr.write(` sent: ${count} document(s) → elastic ${index} (${url}) ${ms}ms\n`);
79
+ }
80
+ else if (sinkKind === "loki") {
81
+ const url = sinkConfig.lokiUrl ?? "";
82
+ process.stderr.write(` sent: ${count} document(s) → loki (${url}) ${ms}ms\n`);
83
+ }
84
+ else {
85
+ process.stderr.write(` sent: ${count} document(s) → stdout\n`);
86
+ }
72
87
  }
@@ -1,11 +1,12 @@
1
- import type { NormalizedDocument } from "../types.js";
2
- import type { SendInput, Sink } from "./types.js";
1
+ import type { MetricType, NormalizedDocument } from "../types.js";
2
+ import type { SendInput, SendResult, Sink } from "./types.js";
3
3
  interface ElasticSinkOptions {
4
4
  url: string;
5
5
  apiKey: string;
6
6
  retryMax: number;
7
7
  retryBackoffMs: number;
8
8
  }
9
+ export declare const INDEX_BY_TYPE: Record<MetricType, string>;
9
10
  export declare function buildBulkBody(indexName: string, documents: NormalizedDocument[]): string;
10
11
  export declare class ElasticSink implements Sink {
11
12
  private readonly url;
@@ -13,6 +14,6 @@ export declare class ElasticSink implements Sink {
13
14
  private readonly retryMax;
14
15
  private readonly retryBackoffMs;
15
16
  constructor(options: ElasticSinkOptions);
16
- send(input: SendInput): Promise<void>;
17
+ send(input: SendInput): Promise<SendResult>;
17
18
  }
18
19
  export {};
@@ -1,5 +1,5 @@
1
1
  import { isRecord } from "../utils/guards.js";
2
- const INDEX_BY_TYPE = {
2
+ export const INDEX_BY_TYPE = {
3
3
  biome: "codequality-biome",
4
4
  eslint: "codequality-eslint",
5
5
  lighthouse: "codequality-lighthouse",
@@ -63,8 +63,9 @@ export class ElasticSink {
63
63
  }
64
64
  async send(input) {
65
65
  if (input.documents.length === 0) {
66
- return;
66
+ return { durationMs: 0 };
67
67
  }
68
+ const start = performance.now();
68
69
  const indexName = INDEX_BY_TYPE[input.metricType];
69
70
  let documents = input.documents;
70
71
  let attempt = 0;
@@ -99,7 +100,7 @@ export class ElasticSink {
99
100
  if (nonRetryableErrors.length > 0) {
100
101
  throw new Error(`Elastic bulk request completed with ${nonRetryableErrors.length} non-retryable item error(s)`);
101
102
  }
102
- return;
103
+ return { durationMs: performance.now() - start };
103
104
  }
104
105
  if (attempt > this.retryMax) {
105
106
  const deadLetterBody = buildBulkBody(indexName, retryable);
@@ -110,7 +111,7 @@ export class ElasticSink {
110
111
  await sleep(this.retryBackoffMs * attempt);
111
112
  continue;
112
113
  }
113
- return;
114
+ return { durationMs: performance.now() - start };
114
115
  }
115
116
  }
116
117
  }
@@ -1,5 +1,5 @@
1
1
  import type { NormalizedDocument } from "../types.js";
2
- import type { SendInput, Sink } from "./types.js";
2
+ import type { SendInput, SendResult, Sink } from "./types.js";
3
3
  export interface LokiSinkOptions {
4
4
  url: string;
5
5
  username?: string | undefined;
@@ -25,6 +25,6 @@ export declare class LokiSink implements Sink {
25
25
  private readonly retryMax;
26
26
  private readonly retryBackoffMs;
27
27
  constructor(options: LokiSinkOptions);
28
- send(input: SendInput): Promise<void>;
28
+ send(input: SendInput): Promise<SendResult>;
29
29
  }
30
30
  export {};
@@ -48,8 +48,9 @@ export class LokiSink {
48
48
  }
49
49
  async send(input) {
50
50
  if (input.documents.length === 0) {
51
- return;
51
+ return { durationMs: 0 };
52
52
  }
53
+ const start = performance.now();
53
54
  const payload = buildLokiPayload(input.documents);
54
55
  const body = JSON.stringify(payload);
55
56
  const headers = {
@@ -71,7 +72,7 @@ export class LokiSink {
71
72
  body,
72
73
  });
73
74
  if (response.status === 204 || response.ok) {
74
- return;
75
+ return { durationMs: performance.now() - start };
75
76
  }
76
77
  const responseText = await response.text();
77
78
  if (!isRetryableStatus(response.status) || attempt > this.retryMax) {
@@ -1,4 +1,4 @@
1
- import type { SendInput, Sink } from "./types.js";
1
+ import type { SendInput, SendResult, Sink } from "./types.js";
2
2
  export declare class StdoutSink implements Sink {
3
- send(input: SendInput): Promise<void>;
3
+ send(input: SendInput): Promise<SendResult>;
4
4
  }
@@ -6,5 +6,6 @@ export class StdoutSink {
6
6
  documents: input.documents,
7
7
  };
8
8
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
9
+ return { durationMs: 0 };
9
10
  }
10
11
  }
@@ -3,6 +3,9 @@ export interface SendInput {
3
3
  metricType: MetricType;
4
4
  documents: NormalizedDocument[];
5
5
  }
6
+ export interface SendResult {
7
+ durationMs: number;
8
+ }
6
9
  export interface Sink {
7
- send(input: SendInput): Promise<void>;
10
+ send(input: SendInput): Promise<SendResult>;
8
11
  }
@@ -0,0 +1 @@
1
+ export declare function formatBytes(bytes: number): string;
@@ -0,0 +1,11 @@
1
+ const UNITS = ["B", "kB", "MB", "GB"];
2
+ export function formatBytes(bytes) {
3
+ let value = bytes;
4
+ let unitIndex = 0;
5
+ while (value >= 1000 && unitIndex < UNITS.length - 1) {
6
+ value /= 1000;
7
+ unitIndex++;
8
+ }
9
+ const formatted = unitIndex === 0 ? value.toString() : value.toFixed(1).replace(/\.0$/, "");
10
+ return `${formatted} ${UNITS[unitIndex]}`;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualink",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Collect, normalize, and relay code quality metrics from CI",
5
5
  "license": "MIT",
6
6
  "type": "module",