@codacy/analysis-runner 0.12.0 → 0.13.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/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # @codacy/analysis-runner
2
2
 
3
3
  ## Table of Contents
4
+
4
5
  - [Overview](#overview)
6
+ - [Quick start](#quick-start)
5
7
  - [API](#api)
6
8
  - [Pipeline steps](#pipeline-steps)
7
9
  - [Output formats](#output-formats)
@@ -13,17 +15,90 @@
13
15
 
14
16
  Core orchestration engine for the Codacy Analysis CLI. Discovers tools, checks capabilities, calls `analyze()` on each adapter, aggregates results, and transforms output. Usable programmatically by both the CLI and IDE integrations.
15
17
 
18
+ ## Quick start
19
+
20
+ Tool adapters live in a companion preset package, **`@codacy/analysis-adapters`**. A
21
+ consumer installs both and registers the built-in adapters **once** before running
22
+ anything tool-related — the runner's adapter registry starts empty and nothing
23
+ auto-populates it.
24
+
25
+ ```ts
26
+ import { registerBuiltinAdapters } from "@codacy/analysis-adapters";
27
+ import { analyze, createNullLogger } from "@codacy/analysis-runner";
28
+
29
+ // Register all built-in tools + cloud-only descriptors (call once at startup).
30
+ registerBuiltinAdapters();
31
+
32
+ // Run analysis. `repositoryRoot` must be a git work tree.
33
+ const result = await analyze({
34
+ repositoryRoot: "/abs/path/to/repo",
35
+ // files: ["src/index.ts"], // optional: scope to specific files
36
+ logger: createNullLogger(), // or bring your own Logger (see below)
37
+ });
38
+
39
+ // Issue paths are repo-relative; line/column are 1-based.
40
+ for (const issue of result.issues) {
41
+ console.log(
42
+ `${issue.filePath}:${issue.line}:${issue.column} ${issue.patternId} ${issue.message}`,
43
+ );
44
+ }
45
+ ```
46
+
47
+ To register only a subset of tools, import `builtinAdapterEntries` from the preset and
48
+ call `registerLazyAdapter(descriptor, load)` for the ones you want.
49
+
50
+ ### Creating a config
51
+
52
+ `analyze()` reads `.codacy/codacy.config.json`. Generate one programmatically with the
53
+ `init*` family — they build a `CodacyConfig` from the registered adapters; `writeCodacyConfig`
54
+ persists it:
55
+
56
+ ```ts
57
+ import {
58
+ getRegisteredAdapters,
59
+ initLocalConfig, // local-config detection only (offline)
60
+ // initDefaultConfig / initAutoConfig / initRemoteConfig also available
61
+ writeCodacyConfig,
62
+ } from "@codacy/analysis-runner";
63
+
64
+ const adapters = await getRegisteredAdapters();
65
+ const { config } = await initLocalConfig(repositoryRoot, adapters);
66
+ await writeCodacyConfig(repositoryRoot, config);
67
+ ```
68
+
69
+ `initAutoConfig` additionally takes the registered descriptors (`getRegisteredDescriptors()`)
70
+ and the preset's `loadUnsupportedPatterns` to gate stack-specific patterns; `initRemoteConfig`
71
+ pulls the config from Codacy Cloud (pass an API token — see credential helpers below).
72
+
73
+ ### Custom logging & cancellation
74
+
75
+ - Pass your own `Logger` (the `@codacy/tooling` contract: `debug` / `info` / `warn` / `error`)
76
+ to route progress into your UI, or `createNullLogger()` for silence. Do **not** use
77
+ `createLogger()` in an embedder — it writes to files/stderr. Drive progress UI from the
78
+ `onToolStart` / `onToolComplete` / `onToolProgress` callbacks on `AnalyzeOptions`.
79
+ - Pass `signal: AbortSignal` on `AnalyzeOptions` to cancel an in-flight run; signal-honoring
80
+ adapters return partial results and remaining tools are marked cancelled.
81
+
16
82
  ## API
17
83
 
18
- | Export | Purpose |
19
- |--------|---------|
20
- | `analyze(options)` | Main entry point — runs the full analysis pipeline |
21
- | `registerAdapter(adapter)` | Register a tool adapter |
22
- | `clearAdapters()` | Reset adapter registry (for tests) |
23
- | `getRegisteredAdapters()` | List registered adapters |
24
- | `formatOutput(result, format)` | Transform AnalysisResult into a formatted string |
25
- | `buildToolConfig()` | Assemble CodacyToolConfig from CodacyConfig + local config |
26
- | `readCodacyConfig()` / `writeCodacyConfig()` | Read/write `.codacy/codacy.config.json` |
84
+ | Export | Purpose |
85
+ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
86
+ | `analyze(options)` | Main entry point — runs the full analysis pipeline. Options include `repositoryRoot`, `tools`, `files`, `logger`, `signal`, and `onTool*` progress callbacks |
87
+ | `registerAdapter(adapter)` / `registerLazyAdapter(descriptor, load)` | Register a tool adapter (eager / lazy) |
88
+ | `clearAdapters()` | Reset adapter registry (for tests) |
89
+ | `getRegisteredAdapters()` / `getRegisteredDescriptors()` | List registered adapters / descriptors |
90
+ | `initLocalConfig()` / `initDefaultConfig()` / `initAutoConfig()` / `initRemoteConfig()` | Build a `CodacyConfig` (local, default, stack-aware, or Cloud-sourced) |
91
+ | `readCodacyConfig()` / `writeCodacyConfig()` | Read/write `.codacy/codacy.config.json` |
92
+ | `buildToolConfig()` | Assemble CodacyToolConfig from CodacyConfig + local config |
93
+ | `discover(path)` | Scan a repo and report languages, frameworks, libraries, notable files |
94
+ | `formatOutput(result, format)` | Transform AnalysisResult into a formatted string |
95
+ | `createLogger()` / `createNullLogger()` | File/stderr logger (CLI) / no-op logger (embedders) |
96
+ | `saveCredentials()` / `loadCredentials()` / `deleteCredentials()` / `resolveApiToken()` | Machine-stored API-token storage in `~/.codacy/credentials` |
97
+ | `validateApiToken()` / `configureApiToken()` / `listIgnoredFiles()` | Codacy Cloud API helpers |
98
+
99
+ > Adapter **registration** lives in the companion preset `@codacy/analysis-adapters`
100
+ > (`registerBuiltinAdapters`, `builtinAdapterEntries`), not here — the runner stays
101
+ > tool-agnostic.
27
102
 
28
103
  ## Pipeline steps
29
104
 
@@ -38,18 +113,18 @@ Core orchestration engine for the Codacy Analysis CLI. Discovers tools, checks c
38
113
 
39
114
  ## Output formats
40
115
 
41
- | Format | Description |
42
- |--------|-------------|
43
- | `json` | Pretty-printed AnalysisResult (default) |
44
- | `sarif` | SARIF v2.1.0 document |
45
- | `container` | One JSON object per line (Codacy Docker runners) |
46
- | `text` | Human-readable terminal summary with colors and grouping |
116
+ | Format | Description |
117
+ | ----------- | -------------------------------------------------------- |
118
+ | `json` | Pretty-printed AnalysisResult (default) |
119
+ | `sarif` | SARIF v2.1.0 document |
120
+ | `container` | One JSON object per line (Codacy Docker runners) |
121
+ | `text` | Human-readable terminal summary with colors and grouping |
47
122
 
48
123
  ## Execution modes
49
124
 
50
- | Mode | Behavior |
51
- |------|----------|
52
- | `standard` | Skip unavailable tools, report them (default) |
53
- | `strict` | Fail if any configured tool is unavailable |
54
- | `auto-install` | Attempt to install missing tools before analysis |
55
- | `inspect` | Dry-run: report capability without running analysis |
125
+ | Mode | Behavior |
126
+ | -------------- | --------------------------------------------------- |
127
+ | `standard` | Skip unavailable tools, report them (default) |
128
+ | `strict` | Fail if any configured tool is unavailable |
129
+ | `auto-install` | Attempt to install missing tools before analysis |
130
+ | `inspect` | Dry-run: report capability without running analysis |
package/dist/index.d.ts CHANGED
@@ -51,6 +51,15 @@ interface AnalyzeOptions {
51
51
  configFile?: string;
52
52
  /** Logger for pipeline progress messages (install status, etc.). Defaults to no-op. */
53
53
  logger?: Logger;
54
+ /**
55
+ * Optional caller abort signal for cancelling an in-flight analysis (e.g. an
56
+ * embedder like VSCode or an MCP server). When aborted, `analyze()` stops
57
+ * launching new tools, in-flight signal-honoring adapters return partial
58
+ * results, and remaining tools are marked cancelled. If the signal is already
59
+ * aborted when `analyze()` is called, it returns immediately without doing any
60
+ * work.
61
+ */
62
+ signal?: AbortSignal;
54
63
  /** Called when a tool starts executing. */
55
64
  onToolStart?: (toolId: string, displayName: string) => void;
56
65
  /** Called when a tool finishes executing. */
@@ -659,6 +668,15 @@ interface LoggerOptions {
659
668
  */
660
669
  onStderrMessage?: (text: string, level: LogLevel) => void;
661
670
  }
671
+ /**
672
+ * Creates a no-op Logger that discards all messages.
673
+ *
674
+ * Intended for embedders (VSCode extension, MCP server, other library
675
+ * consumers) that call `analyze()` programmatically and do not want the
676
+ * pipeline writing to a log file or stderr. Passing this — or simply omitting
677
+ * `options.logger`, which defaults to it — keeps analysis completely silent.
678
+ */
679
+ declare function createNullLogger(): Logger;
662
680
  /**
663
681
  * Removes old log files beyond the retention limit.
664
682
  *
@@ -677,6 +695,50 @@ declare function rotateLogFiles(logsDir: string, maxFiles: number): void;
677
695
  */
678
696
  declare function createLogger(options?: LoggerOptions): Logger;
679
697
 
698
+ /**
699
+ * Credential storage — encrypts and persists API tokens to ~/.codacy/credentials,
700
+ * and resolves an API token from the environment or stored credentials.
701
+ *
702
+ * Lives in the runner so that any embedder (CLI, MCP server, IDE integration) can
703
+ * reuse credential storage. The interactive prompt stays in the CLI.
704
+ *
705
+ * Replicates the encryption scheme from codacy-cloud-cli — the byte format,
706
+ * algorithm, key derivation, and file location are SHARED, so credentials written
707
+ * by one tool are readable by the other. DO NOT change any of the following:
708
+ * - File location: ~/.codacy/credentials
709
+ * - Machine key: [hostname, username, homedir, platform].join("|")
710
+ * - Key derivation: PBKDF2 (100k iterations, SHA-512, 32-byte key)
711
+ * - Encryption: AES-256-GCM with random salt (32B) and IV (16B)
712
+ * - Storage: JSON with hex-encoded { salt, iv, authTag, encrypted }
713
+ * - File permissions: 0o600 (owner read/write only)
714
+ *
715
+ * All crypto uses Node.js built-in `crypto` — no external dependencies.
716
+ */
717
+ /**
718
+ * Encrypts and saves an API token to ~/.codacy/credentials.
719
+ * Creates the ~/.codacy/ directory if it doesn't exist.
720
+ */
721
+ declare function saveCredentials(token: string): Promise<void>;
722
+ /**
723
+ * Reads and decrypts the stored API token from ~/.codacy/credentials.
724
+ * Returns null if the file is missing, corrupt, or cannot be decrypted.
725
+ */
726
+ declare function loadCredentials(): Promise<string | null>;
727
+ /**
728
+ * Deletes the stored credentials file.
729
+ * Returns true if the file existed and was deleted, false otherwise.
730
+ */
731
+ declare function deleteCredentials(): Promise<boolean>;
732
+ /**
733
+ * Resolves a Codacy API token from the environment or stored credentials.
734
+ *
735
+ * Resolution order:
736
+ * 1. CODACY_API_TOKEN environment variable
737
+ * 2. Encrypted credentials saved via `codacy-analysis login`
738
+ * 3. Throws with instructions if neither source has a token.
739
+ */
740
+ declare function resolveApiToken(): Promise<string>;
741
+
680
742
  /**
681
743
  * Repository discovery — scans a codebase and identifies languages, file patterns,
682
744
  * notable files, frameworks, and libraries.
@@ -699,4 +761,21 @@ interface DiscoverOptions {
699
761
  */
700
762
  declare function discover(repositoryRoot: string, options?: DiscoverOptions): Promise<DiscoveryResult>;
701
763
 
702
- export { type AdapterFactory, type AnalyzeOptions, type AutoFilters, type CategoryFilter, type CodacyYamlConfig, type CodacyYamlEngineConfig, type ConfigOperation, type DiscoverOptions, type ExecutionMode, type IgnoredFilesResult, type InitAutoStats, type InitDefaultStats, type InitLocalStats, type InitRemoteStats, type LoggerOptions, type PatternMetadataLookup, type SeverityFilter, analyze, applyCodacyYamlExcludes, applyConfigOperation, buildToolConfig, clearAdapters, computePatternSummary, configureApiToken, createCodacyConfig, createLogger, detectLanguages, diffConfigs, discover, formatOutput, formatTextSummary, getAdapter, getRegisteredAdapters, getRegisteredDescriptors, initAutoConfig, initContainerConfig, initDefaultConfig, initLocalConfig, initRemoteConfig, intersectConfigs, listDiffFiles, listIgnoredFiles, listPrFiles, listStagedFiles, mergeConfigs, parseAutoFilters, readCodacyConfig, readCodacyYaml, registerAdapter, registerLazyAdapter, registerUnsupportedTool, resolveConfigPath, resolveEngineToAdapter, rotateLogFiles, validateApiToken, writeCodacyConfig, writeCodacyConfigFile };
764
+ /**
765
+ * Discovery catalog — static knowledge base for the discover command.
766
+ *
767
+ * Contains manifest file parsers, a curated dependency classification catalog,
768
+ * and notable file detection rules. All parsers are pure functions that accept
769
+ * raw file content and return dependency name arrays. They never throw.
770
+ */
771
+
772
+ /**
773
+ * Every display name discovery can emit from a dependency/file/package.json signal.
774
+ * This is the authoritative set a framework-mapping key must belong to (together
775
+ * with detected language names) for its patterns to ever gate on. The drift-guard
776
+ * test uses it to catch mapping keys that no signal can produce (e.g. a rename
777
+ * like react-intl → formatjs that updates the mapping but not the catalog).
778
+ */
779
+ declare function getKnownDisplayNames(): Set<string>;
780
+
781
+ export { type AdapterFactory, type AnalyzeOptions, type AutoFilters, type CategoryFilter, type CodacyYamlConfig, type CodacyYamlEngineConfig, type ConfigOperation, type DiscoverOptions, type ExecutionMode, type IgnoredFilesResult, type InitAutoStats, type InitDefaultStats, type InitLocalStats, type InitRemoteStats, type LoggerOptions, type PatternMetadataLookup, type SeverityFilter, analyze, applyCodacyYamlExcludes, applyConfigOperation, buildToolConfig, clearAdapters, computePatternSummary, configureApiToken, createCodacyConfig, createLogger, createNullLogger, deleteCredentials, detectLanguages, diffConfigs, discover, formatOutput, formatTextSummary, getAdapter, getKnownDisplayNames, getRegisteredAdapters, getRegisteredDescriptors, initAutoConfig, initContainerConfig, initDefaultConfig, initLocalConfig, initRemoteConfig, intersectConfigs, listDiffFiles, listIgnoredFiles, listPrFiles, listStagedFiles, loadCredentials, mergeConfigs, parseAutoFilters, readCodacyConfig, readCodacyYaml, registerAdapter, registerLazyAdapter, registerUnsupportedTool, resolveApiToken, resolveConfigPath, resolveEngineToAdapter, rotateLogFiles, saveCredentials, validateApiToken, writeCodacyConfig, writeCodacyConfigFile };
package/dist/index.js CHANGED
@@ -39,12 +39,15 @@ __export(index_exports, {
39
39
  configureApiToken: () => configureApiToken,
40
40
  createCodacyConfig: () => createCodacyConfig,
41
41
  createLogger: () => createLogger,
42
+ createNullLogger: () => createNullLogger,
43
+ deleteCredentials: () => deleteCredentials,
42
44
  detectLanguages: () => detectLanguages,
43
45
  diffConfigs: () => diffConfigs,
44
46
  discover: () => discover,
45
47
  formatOutput: () => formatOutput,
46
48
  formatTextSummary: () => formatTextSummary,
47
49
  getAdapter: () => getAdapter,
50
+ getKnownDisplayNames: () => getKnownDisplayNames,
48
51
  getRegisteredAdapters: () => getRegisteredAdapters,
49
52
  getRegisteredDescriptors: () => getRegisteredDescriptors,
50
53
  initAutoConfig: () => initAutoConfig,
@@ -57,6 +60,7 @@ __export(index_exports, {
57
60
  listIgnoredFiles: () => listIgnoredFiles,
58
61
  listPrFiles: () => listPrFiles,
59
62
  listStagedFiles: () => listStagedFiles,
63
+ loadCredentials: () => loadCredentials,
60
64
  mergeConfigs: () => mergeConfigs,
61
65
  parseAutoFilters: () => parseAutoFilters,
62
66
  readCodacyConfig: () => readCodacyConfig,
@@ -64,9 +68,11 @@ __export(index_exports, {
64
68
  registerAdapter: () => registerAdapter,
65
69
  registerLazyAdapter: () => registerLazyAdapter,
66
70
  registerUnsupportedTool: () => registerUnsupportedTool,
71
+ resolveApiToken: () => resolveApiToken,
67
72
  resolveConfigPath: () => resolveConfigPath,
68
73
  resolveEngineToAdapter: () => resolveEngineToAdapter,
69
74
  rotateLogFiles: () => rotateLogFiles,
75
+ saveCredentials: () => saveCredentials,
70
76
  validateApiToken: () => validateApiToken,
71
77
  writeCodacyConfig: () => writeCodacyConfig,
72
78
  writeCodacyConfigFile: () => writeCodacyConfigFile
@@ -74,8 +80,8 @@ __export(index_exports, {
74
80
  module.exports = __toCommonJS(index_exports);
75
81
 
76
82
  // src/analyze.ts
77
- var import_os = __toESM(require("os"));
78
- var import_path3 = __toESM(require("path"));
83
+ var import_os2 = __toESM(require("os"));
84
+ var import_path4 = __toESM(require("path"));
79
85
  var import_child_process3 = require("child_process");
80
86
  var import_util = require("util");
81
87
  var import_tooling3 = require("@codacy/tooling");
@@ -418,6 +424,61 @@ function detectDefaultBranch(repositoryRoot) {
418
424
 
419
425
  // src/files.ts
420
426
  var MAX_FILE_SIZE_BYTES = 150 * 1024;
427
+ var DEPENDENCY_MANIFEST_FILES = /* @__PURE__ */ new Set([
428
+ // JavaScript / TypeScript
429
+ "package.json",
430
+ "package-lock.json",
431
+ "npm-shrinkwrap.json",
432
+ "yarn.lock",
433
+ "pnpm-lock.yaml",
434
+ "bun.lock",
435
+ // Python
436
+ "requirements.txt",
437
+ "Pipfile",
438
+ "Pipfile.lock",
439
+ "poetry.lock",
440
+ "uv.lock",
441
+ // PHP
442
+ "composer.json",
443
+ "composer.lock",
444
+ // Ruby
445
+ "Gemfile.lock",
446
+ // Rust
447
+ "Cargo.toml",
448
+ "Cargo.lock",
449
+ // Go
450
+ "go.mod",
451
+ "go.sum",
452
+ // .NET
453
+ "packages.config",
454
+ "packages.lock.json",
455
+ "Directory.Packages.props",
456
+ "Packages.props",
457
+ // Java / Scala
458
+ "pom.xml",
459
+ "gradle.lockfile",
460
+ "build.gradle",
461
+ "build.gradle.kts",
462
+ "build.sbt.lock",
463
+ // Dart / Flutter
464
+ "pubspec.yaml",
465
+ "pubspec.lock",
466
+ // Elixir
467
+ "mix.lock",
468
+ // Swift / CocoaPods
469
+ "Package.resolved",
470
+ "Podfile.lock",
471
+ // C / C++ (Conan)
472
+ "conan.lock",
473
+ // Erlang
474
+ "rebar.lock"
475
+ ]);
476
+ var DEPENDENCY_MANIFEST_SUFFIXES = [".deps.json"];
477
+ function isDependencyManifest(relativePath) {
478
+ const base = relativePath.split("/").pop() ?? "";
479
+ if (DEPENDENCY_MANIFEST_FILES.has(base)) return true;
480
+ return DEPENDENCY_MANIFEST_SUFFIXES.some((suffix) => base.endsWith(suffix));
481
+ }
421
482
  function listRepoFiles(repositoryRoot) {
422
483
  try {
423
484
  const tracked = (0, import_child_process2.execSync)("git ls-files", {
@@ -449,6 +510,15 @@ function filterOversizedFiles(files, repositoryRoot, logger) {
449
510
  try {
450
511
  const stat = import_fs.default.statSync(import_path2.default.join(repositoryRoot, f));
451
512
  if (stat.size > MAX_FILE_SIZE_BYTES) {
513
+ if (isDependencyManifest(f)) {
514
+ logger?.debug("Keeping oversized dependency manifest", {
515
+ file: f,
516
+ sizeKB: Math.round(stat.size / 1024),
517
+ limitKB: MAX_FILE_SIZE_BYTES / 1024
518
+ });
519
+ result.push(f);
520
+ continue;
521
+ }
452
522
  skipped.push({ filePath: f, sizeBytes: stat.size });
453
523
  logger?.debug("Skipping oversized file", {
454
524
  file: f,
@@ -504,7 +574,43 @@ function filterByTargetFiles(allFiles, targetPatterns) {
504
574
  // src/executor.ts
505
575
  var import_tooling2 = require("@codacy/tooling");
506
576
  var HARD_TIMEOUT_GRACE_MS = 2e3;
507
- async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
577
+ function anySignal(a, b) {
578
+ if (typeof AbortSignal.any === "function") return AbortSignal.any([a, b]);
579
+ const composite = new AbortController();
580
+ if (a.aborted || b.aborted) {
581
+ composite.abort();
582
+ } else {
583
+ const onAbort = () => composite.abort();
584
+ a.addEventListener("abort", onAbort, { once: true });
585
+ b.addEventListener("abort", onAbort, { once: true });
586
+ }
587
+ return composite.signal;
588
+ }
589
+ function cancelledResult(adapter) {
590
+ const now = (/* @__PURE__ */ new Date()).toISOString();
591
+ return {
592
+ metadata: { startedAt: now, completedAt: now, durationMs: 0 },
593
+ issues: [],
594
+ errors: [
595
+ {
596
+ toolId: adapter.id,
597
+ phase: "toolInvoke",
598
+ kind: "Cancelled",
599
+ message: `Analysis cancelled by caller; ${adapter.id} was not run`,
600
+ level: "error"
601
+ }
602
+ ],
603
+ summary: {
604
+ toolId: adapter.id,
605
+ status: "skipped",
606
+ issueCount: 0,
607
+ errorCount: 1,
608
+ durationMs: 0,
609
+ filesAnalyzed: 0
610
+ }
611
+ };
612
+ }
613
+ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs, callerSignal) {
508
614
  const startTime = Date.now();
509
615
  ctx.logger.debug("runTool started", {
510
616
  toolId: adapter.id,
@@ -512,7 +618,7 @@ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
512
618
  timeoutMs
513
619
  });
514
620
  const abortController = new AbortController();
515
- ctx.signal = abortController.signal;
621
+ ctx.signal = callerSignal ? anySignal(callerSignal, abortController.signal) : abortController.signal;
516
622
  const abortTimer = setTimeout(() => abortController.abort(), timeoutMs);
517
623
  abortTimer.unref?.();
518
624
  let hardTimer;
@@ -533,8 +639,14 @@ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
533
639
  return result;
534
640
  } catch (err) {
535
641
  const durationMs = Date.now() - startTime;
536
- const isTimeout = err instanceof Error && err.message.includes("TIMEOUT");
537
- if (isTimeout) {
642
+ const isCancelled = callerSignal?.aborted === true;
643
+ const isTimeout = !isCancelled && err instanceof Error && err.message.includes("TIMEOUT");
644
+ if (isCancelled) {
645
+ ctx.logger.error("Tool cancelled by caller", {
646
+ toolId: adapter.id,
647
+ elapsedMs: durationMs
648
+ });
649
+ } else if (isTimeout) {
538
650
  ctx.logger.error("Tool timed out", { toolId: adapter.id, timeoutMs, elapsedMs: durationMs });
539
651
  } else {
540
652
  ctx.logger.error("Tool execution failed", {
@@ -544,6 +656,8 @@ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
544
656
  });
545
657
  }
546
658
  const now = /* @__PURE__ */ new Date();
659
+ const kind = isCancelled ? "Cancelled" : isTimeout ? "Timeout" : "InvocationError";
660
+ const message = isCancelled ? `Tool ${adapter.id} cancelled by caller` : isTimeout ? `Tool ${adapter.id} exceeded timeout of ${(0, import_tooling2.formatDuration)(timeoutMs)}` : `Tool ${adapter.id} failed: ${err instanceof Error ? err.message : String(err)}`;
547
661
  return {
548
662
  metadata: {
549
663
  startedAt: new Date(now.getTime() - durationMs).toISOString(),
@@ -555,8 +669,8 @@ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
555
669
  {
556
670
  toolId: adapter.id,
557
671
  phase: "toolInvoke",
558
- kind: isTimeout ? "Timeout" : "InvocationError",
559
- message: isTimeout ? `Tool ${adapter.id} exceeded timeout of ${(0, import_tooling2.formatDuration)(timeoutMs)}` : `Tool ${adapter.id} failed: ${err instanceof Error ? err.message : String(err)}`,
672
+ kind,
673
+ message,
560
674
  level: "error"
561
675
  }
562
676
  ],
@@ -574,15 +688,19 @@ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
574
688
  if (hardTimer) clearTimeout(hardTimer);
575
689
  }
576
690
  }
577
- async function runToolsSequential(tools, timeoutMs, callbacks) {
691
+ async function runToolsSequential(tools, timeoutMs, callbacks, callerSignal) {
578
692
  const results = [];
579
693
  for (const { adapter, ctx, codacyToolConfig } of tools) {
694
+ if (callerSignal?.aborted) {
695
+ results.push(cancelledResult(adapter));
696
+ continue;
697
+ }
580
698
  if (callbacks?.onToolProgress) {
581
699
  const cb = callbacks.onToolProgress;
582
700
  ctx.onProgress = (message) => cb(adapter.id, adapter.displayName, message);
583
701
  }
584
702
  callbacks?.onToolStart?.(adapter.id, adapter.displayName);
585
- const result = await runTool(adapter, ctx, codacyToolConfig, timeoutMs);
703
+ const result = await runTool(adapter, ctx, codacyToolConfig, timeoutMs, callerSignal);
586
704
  callbacks?.onToolComplete?.(
587
705
  adapter.id,
588
706
  adapter.displayName,
@@ -593,19 +711,23 @@ async function runToolsSequential(tools, timeoutMs, callbacks) {
593
711
  }
594
712
  return results;
595
713
  }
596
- async function runToolsParallel(tools, timeoutMs, concurrency, callbacks) {
714
+ async function runToolsParallel(tools, timeoutMs, concurrency, callbacks, callerSignal) {
597
715
  const results = new Array(tools.length);
598
716
  let nextIndex = 0;
599
717
  async function worker() {
600
718
  while (nextIndex < tools.length) {
601
719
  const index = nextIndex++;
602
720
  const { adapter, ctx, codacyToolConfig } = tools[index];
721
+ if (callerSignal?.aborted) {
722
+ results[index] = cancelledResult(adapter);
723
+ continue;
724
+ }
603
725
  if (callbacks?.onToolProgress) {
604
726
  const cb = callbacks.onToolProgress;
605
727
  ctx.onProgress = (message) => cb(adapter.id, adapter.displayName, message);
606
728
  }
607
729
  callbacks?.onToolStart?.(adapter.id, adapter.displayName);
608
- results[index] = await runTool(adapter, ctx, codacyToolConfig, timeoutMs);
730
+ results[index] = await runTool(adapter, ctx, codacyToolConfig, timeoutMs, callerSignal);
609
731
  callbacks?.onToolComplete?.(
610
732
  adapter.id,
611
733
  adapter.displayName,
@@ -619,6 +741,124 @@ async function runToolsParallel(tools, timeoutMs, concurrency, callbacks) {
619
741
  return results;
620
742
  }
621
743
 
744
+ // src/logger.ts
745
+ var import_fs2 = __toESM(require("fs"));
746
+ var import_path3 = __toESM(require("path"));
747
+ var import_os = __toESM(require("os"));
748
+ var LEVEL_ORDER = {
749
+ debug: 0,
750
+ info: 1,
751
+ warning: 2,
752
+ error: 3
753
+ };
754
+ function createNullLogger() {
755
+ return {
756
+ debug: () => {
757
+ },
758
+ info: () => {
759
+ },
760
+ warn: () => {
761
+ },
762
+ error: () => {
763
+ }
764
+ };
765
+ }
766
+ function rotateLogFiles(logsDir, maxFiles) {
767
+ let entries2;
768
+ try {
769
+ entries2 = import_fs2.default.readdirSync(logsDir).filter((f) => f.startsWith("analysis-") && f.endsWith(".log"));
770
+ } catch {
771
+ return;
772
+ }
773
+ if (entries2.length <= maxFiles) return;
774
+ entries2.sort();
775
+ const toRemove = entries2.slice(0, entries2.length - maxFiles);
776
+ for (const f of toRemove) {
777
+ try {
778
+ import_fs2.default.unlinkSync(import_path3.default.join(logsDir, f));
779
+ } catch {
780
+ }
781
+ }
782
+ }
783
+ var fileSeq = 0;
784
+ function fileTimestamp() {
785
+ return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${fileSeq++}`;
786
+ }
787
+ function enrichMessage(message, fields) {
788
+ if (!fields) return message;
789
+ const parts = [];
790
+ if (fields.toolId) parts.push(String(fields.toolId));
791
+ if (fields.version) parts.push(`v${fields.version}`);
792
+ if (fields.installation) parts.push(String(fields.installation));
793
+ if (fields.configSource) parts.push(String(fields.configSource));
794
+ if (typeof fields.fileCount === "number") parts.push(`${fields.fileCount} files`);
795
+ if (typeof fields.issueCount === "number") parts.push(`${fields.issueCount} issues`);
796
+ if (typeof fields.errorCount === "number" && fields.errorCount > 0)
797
+ parts.push(`${fields.errorCount} errors`);
798
+ if (typeof fields.totalIssues === "number") parts.push(`${fields.totalIssues} issues`);
799
+ if (typeof fields.totalErrors === "number" && fields.totalErrors > 0)
800
+ parts.push(`${fields.totalErrors} errors`);
801
+ if (typeof fields.durationMs === "number")
802
+ parts.push(`${(fields.durationMs / 1e3).toFixed(1)}s`);
803
+ if (fields.reason) parts.push(String(fields.reason));
804
+ if (fields.error) parts.push(String(fields.error));
805
+ if (typeof fields.found === "boolean") parts.push(fields.found ? "found" : "not found");
806
+ if (fields.path && !fields.version) parts.push(String(fields.path));
807
+ if (parts.length === 0) return message;
808
+ return `${message} (${parts.join(", ")})`;
809
+ }
810
+ function createLogger(options = {}) {
811
+ const minLevel = LEVEL_ORDER[options.level ?? "info"];
812
+ const fileEnabled = options.fileEnabled ?? true;
813
+ const logsDir = options.logsDir ?? import_path3.default.join(import_os.default.homedir(), ".codacy", "logs");
814
+ const maxFileSize = options.maxFileSize ?? 10 * 1024 * 1024;
815
+ const maxFiles = options.maxFiles ?? 5;
816
+ const onStderrMessage = options.onStderrMessage;
817
+ let logFd = null;
818
+ let bytesWritten = 0;
819
+ if (fileEnabled) {
820
+ import_fs2.default.mkdirSync(logsDir, { recursive: true });
821
+ rotateLogFiles(logsDir, maxFiles);
822
+ const logFilePath = import_path3.default.join(logsDir, `analysis-${fileTimestamp()}.log`);
823
+ logFd = import_fs2.default.openSync(logFilePath, "a");
824
+ }
825
+ function log(level, message, fields) {
826
+ const numLevel = LEVEL_ORDER[level];
827
+ if (numLevel < minLevel) return;
828
+ if (fileEnabled && logFd !== null) {
829
+ const entry = JSON.stringify({
830
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
831
+ level,
832
+ message,
833
+ ...fields
834
+ });
835
+ const buf = Buffer.from(entry + "\n", "utf-8");
836
+ import_fs2.default.writeSync(logFd, buf);
837
+ bytesWritten += buf.length;
838
+ if (bytesWritten >= maxFileSize) {
839
+ import_fs2.default.closeSync(logFd);
840
+ rotateLogFiles(logsDir, maxFiles);
841
+ const newPath = import_path3.default.join(logsDir, `analysis-${fileTimestamp()}.log`);
842
+ logFd = import_fs2.default.openSync(newPath, "a");
843
+ bytesWritten = 0;
844
+ }
845
+ }
846
+ const enriched = enrichMessage(message, fields);
847
+ if (onStderrMessage) {
848
+ onStderrMessage(enriched, level);
849
+ } else {
850
+ process.stderr.write(` ${enriched}
851
+ `);
852
+ }
853
+ }
854
+ return {
855
+ debug: (msg, fields) => log("debug", msg, fields),
856
+ info: (msg, fields) => log("info", msg, fields),
857
+ warn: (msg, fields) => log("warning", msg, fields),
858
+ error: (msg, fields) => log("error", msg, fields)
859
+ };
860
+ }
861
+
622
862
  // src/summary.ts
623
863
  var NOISY_THRESHOLD = 500;
624
864
  var SEVERITY_ORDER = ["Error", "High", "Warning", "Info"];
@@ -731,16 +971,6 @@ function computePatternSummary(issues, lookup) {
731
971
 
732
972
  // src/analyze.ts
733
973
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
734
- var nullLogger = {
735
- debug: () => {
736
- },
737
- info: () => {
738
- },
739
- warn: () => {
740
- },
741
- error: () => {
742
- }
743
- };
744
974
  var CHECK_CONCURRENCY = 12;
745
975
  async function mapWithConcurrency(items, concurrency, task) {
746
976
  const results = new Array(items.length);
@@ -760,7 +990,7 @@ async function analyze(options) {
760
990
  const mode = options.mode || "standard";
761
991
  const toolTimeout = options.toolTimeout || 9e5;
762
992
  const parallelTools = options.parallelTools || 1;
763
- const logger = options.logger ?? nullLogger;
993
+ const logger = options.logger ?? createNullLogger();
764
994
  const phaseTimings = {
765
995
  availabilityMs: 0,
766
996
  fileDiscoveryMs: 0,
@@ -773,6 +1003,26 @@ async function analyze(options) {
773
1003
  unavailable: [],
774
1004
  checkedAt: (/* @__PURE__ */ new Date()).toISOString()
775
1005
  };
1006
+ if (options.signal?.aborted) {
1007
+ logger.info("Analysis cancelled by caller before it started");
1008
+ return buildResult(
1009
+ startedAt,
1010
+ options.repositoryRoot,
1011
+ mode,
1012
+ emptyCapability,
1013
+ [],
1014
+ [
1015
+ {
1016
+ toolId: "",
1017
+ phase: "toolInvoke",
1018
+ kind: "Cancelled",
1019
+ message: "Analysis cancelled by caller before it started",
1020
+ level: "error"
1021
+ }
1022
+ ],
1023
+ []
1024
+ );
1025
+ }
776
1026
  try {
777
1027
  await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], {
778
1028
  cwd: options.repositoryRoot
@@ -836,11 +1086,11 @@ async function analyze(options) {
836
1086
  const requestedTools = options.tools ?? codacyConfig.tools.map((t) => t.toolId);
837
1087
  const adapters = await getRegisteredAdapters(requestedTools);
838
1088
  logger.info("Pipeline started", { mode, toolCount: adapters.length, parallelTools, toolTimeout });
839
- const globalDir = import_path3.default.join(import_os.default.homedir(), ".codacy");
1089
+ const globalDir = import_path4.default.join(import_os2.default.homedir(), ".codacy");
840
1090
  const baseCtx = {
841
1091
  repositoryRoot: options.repositoryRoot,
842
1092
  globalDir,
843
- platform: { os: import_os.default.platform(), arch: import_os.default.arch() },
1093
+ platform: { os: import_os2.default.platform(), arch: import_os2.default.arch() },
844
1094
  logger,
845
1095
  runner: "local"
846
1096
  };
@@ -1202,9 +1452,15 @@ async function analyze(options) {
1202
1452
  };
1203
1453
  let results;
1204
1454
  if (parallelTools > 1 && toolRuns.length > 1) {
1205
- results = await runToolsParallel(toolRuns, toolTimeout, parallelTools, callbacks);
1455
+ results = await runToolsParallel(
1456
+ toolRuns,
1457
+ toolTimeout,
1458
+ parallelTools,
1459
+ callbacks,
1460
+ options.signal
1461
+ );
1206
1462
  } else {
1207
- results = await runToolsSequential(toolRuns, toolTimeout, callbacks);
1463
+ results = await runToolsSequential(toolRuns, toolTimeout, callbacks, options.signal);
1208
1464
  }
1209
1465
  phaseTimings.toolExecutionMs = Date.now() - toolExecStartMs;
1210
1466
  const allIssues = normalizeIssues(results.flatMap((r) => r.issues));
@@ -1245,8 +1501,8 @@ function makeToolContext(base, adapter, targetFiles) {
1245
1501
  return {
1246
1502
  ...base,
1247
1503
  targetFiles,
1248
- toolInstallDir: import_path3.default.join(base.globalDir, "tools", adapter.id),
1249
- generatedConfigDir: import_path3.default.join(base.repositoryRoot, ".codacy", "generated", adapter.id)
1504
+ toolInstallDir: import_path4.default.join(base.globalDir, "tools", adapter.id),
1505
+ generatedConfigDir: import_path4.default.join(base.repositoryRoot, ".codacy", "generated", adapter.id)
1250
1506
  };
1251
1507
  }
1252
1508
  function normalizeIssues(issues) {
@@ -1285,7 +1541,7 @@ function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors
1285
1541
  }
1286
1542
 
1287
1543
  // src/format.ts
1288
- var import_path4 = __toESM(require("path"));
1544
+ var import_path5 = __toESM(require("path"));
1289
1545
  var import_tooling4 = require("@codacy/tooling");
1290
1546
  var import_ansis = __toESM(require("ansis"));
1291
1547
  var import_cli_table3 = __toESM(require("cli-table3"));
@@ -1711,7 +1967,7 @@ function formatFilesCell(filesRouted) {
1711
1967
  }
1712
1968
  function formatConfigCell(tool, repositoryRoot) {
1713
1969
  if (tool.configSource === "local" && tool.configPath) {
1714
- const rawPath = import_path4.default.isAbsolute(tool.configPath) ? import_path4.default.relative(repositoryRoot, tool.configPath) : tool.configPath;
1970
+ const rawPath = import_path5.default.isAbsolute(tool.configPath) ? import_path5.default.relative(repositoryRoot, tool.configPath) : tool.configPath;
1715
1971
  return rawPath.startsWith("./") ? rawPath : `./${rawPath}`;
1716
1972
  }
1717
1973
  if (tool.configSource === "codacy") return "Codacy";
@@ -1973,13 +2229,13 @@ function applyConfigOperation(op, source, dest) {
1973
2229
 
1974
2230
  // src/init.ts
1975
2231
  var import_promises4 = __toESM(require("fs/promises"));
1976
- var import_path7 = __toESM(require("path"));
2232
+ var import_path8 = __toESM(require("path"));
1977
2233
  var import_child_process4 = require("child_process");
1978
2234
  var import_tooling5 = require("@codacy/tooling");
1979
2235
 
1980
2236
  // src/discover.ts
1981
2237
  var import_promises2 = __toESM(require("fs/promises"));
1982
- var import_path5 = __toESM(require("path"));
2238
+ var import_path6 = __toESM(require("path"));
1983
2239
 
1984
2240
  // src/discover-catalog.ts
1985
2241
  var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
@@ -2009,6 +2265,10 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2009
2265
  ["@hapi/hapi", { displayName: "Hapi", kind: "framework" }],
2010
2266
  ["hono", { displayName: "Hono", kind: "framework" }],
2011
2267
  ["electron", { displayName: "Electron", kind: "framework" }],
2268
+ // AWS CDK — the v2 umbrella package `aws-cdk-lib`, the CLI package `aws-cdk`,
2269
+ // and the v1 `@aws-cdk/*` construct packages (prefix rule below).
2270
+ ["aws-cdk-lib", { displayName: "AWS CDK", kind: "framework" }],
2271
+ ["aws-cdk", { displayName: "AWS CDK", kind: "framework" }],
2012
2272
  // --- JavaScript / TypeScript — Libraries ---
2013
2273
  ["lodash", { displayName: "Lodash", kind: "library" }],
2014
2274
  ["underscore", { displayName: "Underscore.js", kind: "library" }],
@@ -2061,9 +2321,20 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2061
2321
  ["pino", { displayName: "Pino", kind: "library" }],
2062
2322
  ["winston", { displayName: "Winston", kind: "library" }],
2063
2323
  ["i18next", { displayName: "i18next", kind: "library" }],
2064
- ["react-intl", { displayName: "react-intl", kind: "library" }],
2324
+ // react-intl is the React binding of the FormatJS ecosystem; eslint-plugin-formatjs
2325
+ // lints both, so we report the umbrella name "formatjs" (matches the mapping key).
2326
+ ["react-intl", { displayName: "formatjs", kind: "library" }],
2065
2327
  ["vue-i18n", { displayName: "vue-i18n", kind: "library" }],
2066
2328
  ["next-intl", { displayName: "next-intl", kind: "library" }],
2329
+ // Type checker (eslint-plugin-flowtype). Also detected via a `.flowconfig` file.
2330
+ ["flow-bin", { displayName: "Flow", kind: "library" }],
2331
+ // Legacy JS libraries with dedicated ESLint plugins.
2332
+ ["backbone", { displayName: "Backbone.js", kind: "library" }],
2333
+ ["chai", { displayName: "Chai", kind: "library" }],
2334
+ ["codeceptjs", { displayName: "CodeceptJS", kind: "library" }],
2335
+ ["webdriverio", { displayName: "WebdriverIO", kind: "library" }],
2336
+ ["react-relay", { displayName: "Relay", kind: "library" }],
2337
+ ["relay-runtime", { displayName: "Relay", kind: "library" }],
2067
2338
  // --- JavaScript / TypeScript — Tools ---
2068
2339
  ["typescript", { displayName: "TypeScript", kind: "tool" }],
2069
2340
  ["eslint", { displayName: "ESLint", kind: "tool" }],
@@ -2275,7 +2546,9 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2275
2546
  ["cats-core", { displayName: "Cats", kind: "library" }],
2276
2547
  ["circe-core", { displayName: "Circe", kind: "library" }],
2277
2548
  // --- Dart — Frameworks ---
2278
- // `flutter` and `angular` appear as plain keys in pubspec.yaml.
2549
+ // `flutter` and `angular` appear as plain keys in pubspec.yaml. NOTE: `angular`
2550
+ // here is Dart's AngularDart. The npm package `angular` is AngularJS (1.x) — a
2551
+ // different framework — so package.json overrides this via MANIFEST_SCOPED_CATALOG.
2279
2552
  ["flutter", { displayName: "Flutter", kind: "framework" }],
2280
2553
  ["flutter_test", { displayName: "Flutter", kind: "framework" }],
2281
2554
  ["angular", { displayName: "Angular", kind: "framework" }],
@@ -2312,10 +2585,13 @@ var DEPENDENCY_PREFIX_RULES = [
2312
2585
  classification: { displayName: "Testing Library", kind: "library" }
2313
2586
  },
2314
2587
  { prefix: "@tanstack/", classification: { displayName: "TanStack", kind: "library" } },
2588
+ { prefix: "@wdio/", classification: { displayName: "WebdriverIO", kind: "library" } },
2589
+ // AWS CDK v1 construct libraries are published under the @aws-cdk/ scope.
2590
+ { prefix: "@aws-cdk/", classification: { displayName: "AWS CDK", kind: "framework" } },
2315
2591
  { prefix: "@reduxjs/", classification: { displayName: "Redux", kind: "library" } },
2316
2592
  { prefix: "@prisma/", classification: { displayName: "Prisma", kind: "library" } },
2317
2593
  { prefix: "@apollo/", classification: { displayName: "Apollo", kind: "library" } },
2318
- { prefix: "@formatjs/", classification: { displayName: "react-intl", kind: "library" } },
2594
+ { prefix: "@formatjs/", classification: { displayName: "formatjs", kind: "library" } },
2319
2595
  { prefix: "@lingui/", classification: { displayName: "Lingui", kind: "library" } },
2320
2596
  { prefix: "@emotion/", classification: { displayName: "Emotion", kind: "library" } },
2321
2597
  { prefix: "@mui/", classification: { displayName: "Material UI", kind: "library" } },
@@ -2358,7 +2634,14 @@ var DEPENDENCY_PREFIX_RULES = [
2358
2634
  { prefix: "MSTest.", classification: { displayName: "MSTest", kind: "library" } },
2359
2635
  { prefix: "Serilog", classification: { displayName: "Serilog", kind: "library" } }
2360
2636
  ];
2361
- function classifyDependency(name) {
2637
+ var MANIFEST_SCOPED_CATALOG = /* @__PURE__ */ new Map([
2638
+ ["package.json", /* @__PURE__ */ new Map([["angular", { displayName: "AngularJS", kind: "framework" }]])]
2639
+ ]);
2640
+ function classifyDependency(name, manifestBasename) {
2641
+ if (manifestBasename) {
2642
+ const scoped = MANIFEST_SCOPED_CATALOG.get(manifestBasename)?.get(name);
2643
+ if (scoped) return scoped;
2644
+ }
2362
2645
  const exact = DEPENDENCY_CATALOG.get(name);
2363
2646
  if (exact) return exact;
2364
2647
  for (const rule of DEPENDENCY_PREFIX_RULES) {
@@ -2366,6 +2649,91 @@ function classifyDependency(name) {
2366
2649
  }
2367
2650
  return void 0;
2368
2651
  }
2652
+ function pathBasename(filePath) {
2653
+ return filePath.split("/").pop() ?? filePath;
2654
+ }
2655
+ var FILE_SIGNAL_RULES = [
2656
+ {
2657
+ name: ".flowconfig",
2658
+ matches: (f) => pathBasename(f) === ".flowconfig",
2659
+ classification: { displayName: "Flow", kind: "library" }
2660
+ },
2661
+ {
2662
+ name: "cdk.json",
2663
+ matches: (f) => pathBasename(f) === "cdk.json",
2664
+ classification: { displayName: "AWS CDK", kind: "framework" }
2665
+ },
2666
+ {
2667
+ name: ".meteor/release",
2668
+ matches: (f) => f === ".meteor/release" || f.endsWith("/.meteor/release"),
2669
+ classification: { displayName: "Meteor", kind: "framework" }
2670
+ },
2671
+ {
2672
+ name: "sfdx-project.json",
2673
+ matches: (f) => pathBasename(f) === "sfdx-project.json",
2674
+ classification: { displayName: "Salesforce", kind: "platform" }
2675
+ },
2676
+ {
2677
+ name: "lwc.config.json",
2678
+ matches: (f) => pathBasename(f) === "lwc.config.json",
2679
+ classification: { displayName: "Lightning Web Components", kind: "platform" }
2680
+ },
2681
+ {
2682
+ name: "suitecloud.config.js",
2683
+ matches: (f) => pathBasename(f) === "suitecloud.config.js",
2684
+ classification: { displayName: "SuiteScript", kind: "platform" }
2685
+ }
2686
+ ];
2687
+ function detectFileSignals(files) {
2688
+ const result = [];
2689
+ for (const rule of FILE_SIGNAL_RULES) {
2690
+ const hit = files.find((f) => rule.matches(f));
2691
+ if (!hit) continue;
2692
+ result.push({
2693
+ name: rule.name,
2694
+ displayName: rule.classification.displayName,
2695
+ kind: rule.classification.kind,
2696
+ source: "file",
2697
+ manifestFile: hit
2698
+ });
2699
+ }
2700
+ return result;
2701
+ }
2702
+ var PACKAGE_JSON_SIGNAL_DISPLAY_NAMES = ["ESLint Plugin"];
2703
+ function isEslintPluginName(name) {
2704
+ return /^eslint-plugin-/.test(name) || /^@[^/]+\/eslint-plugin(-.*)?$/.test(name);
2705
+ }
2706
+ function detectPackageJsonSignals(content) {
2707
+ let pkg;
2708
+ try {
2709
+ pkg = JSON.parse(content);
2710
+ } catch {
2711
+ return [];
2712
+ }
2713
+ const name = pkg && typeof pkg.name === "string" ? pkg.name : "";
2714
+ if (name && isEslintPluginName(name)) {
2715
+ return [
2716
+ {
2717
+ name,
2718
+ displayName: "ESLint Plugin",
2719
+ kind: "tool",
2720
+ source: "manifest",
2721
+ manifestFile: "package.json"
2722
+ }
2723
+ ];
2724
+ }
2725
+ return [];
2726
+ }
2727
+ function getKnownDisplayNames() {
2728
+ const names = /* @__PURE__ */ new Set();
2729
+ for (const { displayName } of DEPENDENCY_CATALOG.values()) names.add(displayName);
2730
+ for (const scoped of MANIFEST_SCOPED_CATALOG.values())
2731
+ for (const { displayName } of scoped.values()) names.add(displayName);
2732
+ for (const rule of DEPENDENCY_PREFIX_RULES) names.add(rule.classification.displayName);
2733
+ for (const rule of FILE_SIGNAL_RULES) names.add(rule.classification.displayName);
2734
+ for (const name of PACKAGE_JSON_SIGNAL_DISPLAY_NAMES) names.add(name);
2735
+ return names;
2736
+ }
2369
2737
  var NOTABLE_ANY_DEPTH = /* @__PURE__ */ new Set([
2370
2738
  "Dockerfile",
2371
2739
  "docker-compose.yml",
@@ -2735,7 +3103,8 @@ async function discover(repositoryRoot, options) {
2735
3103
  let frameworks = [];
2736
3104
  let libraries = [];
2737
3105
  if (!options?.skipDependencies) {
2738
- const allDeps = await extractDependencies(repositoryRoot, files);
3106
+ const manifestDeps = await extractDependencies(repositoryRoot, files);
3107
+ const allDeps = [...manifestDeps, ...detectFileSignals(files)];
2739
3108
  frameworks = deduplicateDeps(allDeps.filter((d) => d.kind === "framework"));
2740
3109
  libraries = deduplicateDeps(
2741
3110
  allDeps.filter((d) => d.kind === "library" || d.kind === "platform" || d.kind === "tool")
@@ -2748,7 +3117,7 @@ async function discover(repositoryRoot, options) {
2748
3117
  frameworks,
2749
3118
  libraries,
2750
3119
  metadata: {
2751
- repositoryRoot: import_path5.default.resolve(repositoryRoot),
3120
+ repositoryRoot: import_path6.default.resolve(repositoryRoot),
2752
3121
  totalFiles: files.length,
2753
3122
  scannedAt: (/* @__PURE__ */ new Date()).toISOString()
2754
3123
  }
@@ -2757,7 +3126,7 @@ async function discover(repositoryRoot, options) {
2757
3126
  function extractFilePatterns(files) {
2758
3127
  const extensions = /* @__PURE__ */ new Set();
2759
3128
  for (const file of files) {
2760
- const ext = import_path5.default.extname(file).toLowerCase();
3129
+ const ext = import_path6.default.extname(file).toLowerCase();
2761
3130
  if (ext) {
2762
3131
  extensions.add(`*${ext}`);
2763
3132
  }
@@ -2790,13 +3159,14 @@ async function extractDependencies(repositoryRoot, files) {
2790
3159
  if (!parser) continue;
2791
3160
  let content;
2792
3161
  try {
2793
- content = await import_promises2.default.readFile(import_path5.default.join(repositoryRoot, filePath), "utf-8");
3162
+ content = await import_promises2.default.readFile(import_path6.default.join(repositoryRoot, filePath), "utf-8");
2794
3163
  } catch {
2795
3164
  continue;
2796
3165
  }
3166
+ const basename = filePath.split("/").pop() ?? filePath;
2797
3167
  const rawDeps = parser(content);
2798
3168
  for (const raw of rawDeps) {
2799
- const classification = classifyDependency(raw.name);
3169
+ const classification = classifyDependency(raw.name, basename);
2800
3170
  if (classification) {
2801
3171
  allDeps.push({
2802
3172
  name: raw.name,
@@ -2808,6 +3178,11 @@ async function extractDependencies(repositoryRoot, files) {
2808
3178
  });
2809
3179
  }
2810
3180
  }
3181
+ if (basename === "package.json") {
3182
+ for (const signal of detectPackageJsonSignals(content)) {
3183
+ allDeps.push({ ...signal, manifestFile: filePath });
3184
+ }
3185
+ }
2811
3186
  }
2812
3187
  return allDeps;
2813
3188
  }
@@ -2825,14 +3200,14 @@ function deduplicateDeps(deps) {
2825
3200
 
2826
3201
  // src/codacy-yaml.ts
2827
3202
  var import_promises3 = __toESM(require("fs/promises"));
2828
- var import_path6 = __toESM(require("path"));
3203
+ var import_path7 = __toESM(require("path"));
2829
3204
  var import_yaml = require("yaml");
2830
3205
  async function readCodacyYaml(repositoryRoot) {
2831
3206
  const candidates = [".codacy.yaml", ".codacy.yml"];
2832
3207
  let raw = null;
2833
3208
  for (const filename of candidates) {
2834
3209
  try {
2835
- raw = await import_promises3.default.readFile(import_path6.default.join(repositoryRoot, filename), "utf-8");
3210
+ raw = await import_promises3.default.readFile(import_path7.default.join(repositoryRoot, filename), "utf-8");
2836
3211
  break;
2837
3212
  } catch {
2838
3213
  }
@@ -3090,13 +3465,13 @@ var getQueryString = (params) => {
3090
3465
  };
3091
3466
  var getUrl = (config, options) => {
3092
3467
  const encoder = config.ENCODE_PATH || encodeURI;
3093
- const path9 = options.url.replace("{api-version}", config.VERSION).replace(/{(.*?)}/g, (substring, group) => {
3468
+ const path10 = options.url.replace("{api-version}", config.VERSION).replace(/{(.*?)}/g, (substring, group) => {
3094
3469
  if (options.path?.hasOwnProperty(group)) {
3095
3470
  return encoder(String(options.path[group]));
3096
3471
  }
3097
3472
  return substring;
3098
3473
  });
3099
- const url = `${config.BASE}${path9}`;
3474
+ const url = `${config.BASE}${path10}`;
3100
3475
  if (options.query) {
3101
3476
  return `${url}${getQueryString(options.query)}`;
3102
3477
  }
@@ -6490,8 +6865,8 @@ async function detectLanguages(repositoryRoot, precomputedFiles) {
6490
6865
  }
6491
6866
  const detected = /* @__PURE__ */ new Set();
6492
6867
  for (const file of files) {
6493
- const basename = import_path7.default.basename(file);
6494
- const ext = import_path7.default.extname(file).toLowerCase();
6868
+ const basename = import_path8.default.basename(file);
6869
+ const ext = import_path8.default.extname(file).toLowerCase();
6495
6870
  for (const [lang, config] of Object.entries(import_tooling5.LANGUAGE_CONFIG)) {
6496
6871
  if (config.extensions.includes(ext)) {
6497
6872
  detected.add(lang);
@@ -6508,12 +6883,12 @@ async function detectLocalConfigFiles(repositoryRoot, adapters) {
6508
6883
  adapters.map(async (adapter) => {
6509
6884
  const result = await adapter.checkLocalConfigurationFile({
6510
6885
  repositoryRoot,
6511
- generatedConfigDir: import_path7.default.join(repositoryRoot, ".codacy", "generated"),
6512
- globalDir: import_path7.default.join(process.env.HOME ?? "~", ".codacy"),
6513
- toolInstallDir: import_path7.default.join(process.env.HOME ?? "~", ".codacy", "tools", adapter.id),
6886
+ generatedConfigDir: import_path8.default.join(repositoryRoot, ".codacy", "generated"),
6887
+ globalDir: import_path8.default.join(process.env.HOME ?? "~", ".codacy"),
6888
+ toolInstallDir: import_path8.default.join(process.env.HOME ?? "~", ".codacy", "tools", adapter.id),
6514
6889
  targetFiles: [],
6515
6890
  platform: { os: process.platform, arch: process.arch },
6516
- logger: silentLogger,
6891
+ logger: createNullLogger(),
6517
6892
  runner: "local"
6518
6893
  });
6519
6894
  return { toolId: adapter.id, result };
@@ -6526,16 +6901,6 @@ async function detectLocalConfigFiles(repositoryRoot, adapters) {
6526
6901
  }
6527
6902
  return found;
6528
6903
  }
6529
- var silentLogger = {
6530
- debug: () => {
6531
- },
6532
- info: () => {
6533
- },
6534
- warn: () => {
6535
- },
6536
- error: () => {
6537
- }
6538
- };
6539
6904
  async function enrichWithCodacyYaml(config, repositoryRoot, adapters) {
6540
6905
  const yamlConfig = await readCodacyYaml(repositoryRoot);
6541
6906
  if (!yamlConfig) return { config, codacyYamlApplied: false };
@@ -6714,7 +7079,7 @@ async function initRemoteConfig(repositoryRoot, provider, organization, reposito
6714
7079
  };
6715
7080
  }
6716
7081
  async function initContainerConfig(repositoryRoot, adapters) {
6717
- const codacyrcPath = import_path7.default.join(repositoryRoot, ".codacyrc");
7082
+ const codacyrcPath = import_path8.default.join(repositoryRoot, ".codacyrc");
6718
7083
  let raw;
6719
7084
  try {
6720
7085
  raw = await import_promises4.default.readFile(codacyrcPath, "utf-8");
@@ -7045,110 +7410,68 @@ function parseAutoFilters(raw) {
7045
7410
  return { filters: { severities, categories }, unknown };
7046
7411
  }
7047
7412
 
7048
- // src/logger.ts
7049
- var import_fs2 = __toESM(require("fs"));
7050
- var import_path8 = __toESM(require("path"));
7051
- var import_os2 = __toESM(require("os"));
7052
- var LEVEL_ORDER = {
7053
- debug: 0,
7054
- info: 1,
7055
- warning: 2,
7056
- error: 3
7057
- };
7058
- function rotateLogFiles(logsDir, maxFiles) {
7059
- let entries2;
7413
+ // src/credentials.ts
7414
+ var import_crypto = __toESM(require("crypto"));
7415
+ var import_promises5 = __toESM(require("fs/promises"));
7416
+ var import_os3 = __toESM(require("os"));
7417
+ var import_path9 = __toESM(require("path"));
7418
+ function credentialsPath() {
7419
+ return import_path9.default.join(import_os3.default.homedir(), ".codacy", "credentials");
7420
+ }
7421
+ function deriveKey(salt) {
7422
+ const info = import_os3.default.userInfo();
7423
+ const machineKey = [import_os3.default.hostname(), info.username, import_os3.default.homedir(), import_os3.default.platform()].join("|");
7424
+ return import_crypto.default.pbkdf2Sync(machineKey, salt, 1e5, 32, "sha512");
7425
+ }
7426
+ async function saveCredentials(token) {
7427
+ const credentialsFile = credentialsPath();
7428
+ const salt = import_crypto.default.randomBytes(32);
7429
+ const iv = import_crypto.default.randomBytes(16);
7430
+ const key = deriveKey(salt);
7431
+ const cipher = import_crypto.default.createCipheriv("aes-256-gcm", key, iv);
7432
+ const encrypted = Buffer.concat([cipher.update(token, "utf-8"), cipher.final()]);
7433
+ const authTag = cipher.getAuthTag();
7434
+ const payload = JSON.stringify({
7435
+ salt: salt.toString("hex"),
7436
+ iv: iv.toString("hex"),
7437
+ authTag: authTag.toString("hex"),
7438
+ encrypted: encrypted.toString("hex")
7439
+ });
7440
+ await import_promises5.default.mkdir(import_path9.default.dirname(credentialsFile), { recursive: true });
7441
+ await import_promises5.default.writeFile(credentialsFile, payload, { encoding: "utf-8", mode: 384 });
7442
+ }
7443
+ async function loadCredentials() {
7060
7444
  try {
7061
- entries2 = import_fs2.default.readdirSync(logsDir).filter((f) => f.startsWith("analysis-") && f.endsWith(".log"));
7445
+ const raw = await import_promises5.default.readFile(credentialsPath(), "utf-8");
7446
+ const data = JSON.parse(raw);
7447
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null;
7448
+ const salt = Buffer.from(data.salt, "hex");
7449
+ const iv = Buffer.from(data.iv, "hex");
7450
+ const authTag = Buffer.from(data.authTag, "hex");
7451
+ const encrypted = Buffer.from(data.encrypted, "hex");
7452
+ const key = deriveKey(salt);
7453
+ const decipher = import_crypto.default.createDecipheriv("aes-256-gcm", key, iv);
7454
+ decipher.setAuthTag(authTag);
7455
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
7456
+ return decrypted.toString("utf-8");
7062
7457
  } catch {
7063
- return;
7064
- }
7065
- if (entries2.length <= maxFiles) return;
7066
- entries2.sort();
7067
- const toRemove = entries2.slice(0, entries2.length - maxFiles);
7068
- for (const f of toRemove) {
7069
- try {
7070
- import_fs2.default.unlinkSync(import_path8.default.join(logsDir, f));
7071
- } catch {
7072
- }
7458
+ return null;
7073
7459
  }
7074
7460
  }
7075
- var fileSeq = 0;
7076
- function fileTimestamp() {
7077
- return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${fileSeq++}`;
7078
- }
7079
- function enrichMessage(message, fields) {
7080
- if (!fields) return message;
7081
- const parts = [];
7082
- if (fields.toolId) parts.push(String(fields.toolId));
7083
- if (fields.version) parts.push(`v${fields.version}`);
7084
- if (fields.installation) parts.push(String(fields.installation));
7085
- if (fields.configSource) parts.push(String(fields.configSource));
7086
- if (typeof fields.fileCount === "number") parts.push(`${fields.fileCount} files`);
7087
- if (typeof fields.issueCount === "number") parts.push(`${fields.issueCount} issues`);
7088
- if (typeof fields.errorCount === "number" && fields.errorCount > 0)
7089
- parts.push(`${fields.errorCount} errors`);
7090
- if (typeof fields.totalIssues === "number") parts.push(`${fields.totalIssues} issues`);
7091
- if (typeof fields.totalErrors === "number" && fields.totalErrors > 0)
7092
- parts.push(`${fields.totalErrors} errors`);
7093
- if (typeof fields.durationMs === "number")
7094
- parts.push(`${(fields.durationMs / 1e3).toFixed(1)}s`);
7095
- if (fields.reason) parts.push(String(fields.reason));
7096
- if (fields.error) parts.push(String(fields.error));
7097
- if (typeof fields.found === "boolean") parts.push(fields.found ? "found" : "not found");
7098
- if (fields.path && !fields.version) parts.push(String(fields.path));
7099
- if (parts.length === 0) return message;
7100
- return `${message} (${parts.join(", ")})`;
7101
- }
7102
- function createLogger(options = {}) {
7103
- const minLevel = LEVEL_ORDER[options.level ?? "info"];
7104
- const fileEnabled = options.fileEnabled ?? true;
7105
- const logsDir = options.logsDir ?? import_path8.default.join(import_os2.default.homedir(), ".codacy", "logs");
7106
- const maxFileSize = options.maxFileSize ?? 10 * 1024 * 1024;
7107
- const maxFiles = options.maxFiles ?? 5;
7108
- const onStderrMessage = options.onStderrMessage;
7109
- let logFd = null;
7110
- let bytesWritten = 0;
7111
- if (fileEnabled) {
7112
- import_fs2.default.mkdirSync(logsDir, { recursive: true });
7113
- rotateLogFiles(logsDir, maxFiles);
7114
- const logFilePath = import_path8.default.join(logsDir, `analysis-${fileTimestamp()}.log`);
7115
- logFd = import_fs2.default.openSync(logFilePath, "a");
7116
- }
7117
- function log(level, message, fields) {
7118
- const numLevel = LEVEL_ORDER[level];
7119
- if (numLevel < minLevel) return;
7120
- if (fileEnabled && logFd !== null) {
7121
- const entry = JSON.stringify({
7122
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7123
- level,
7124
- message,
7125
- ...fields
7126
- });
7127
- const buf = Buffer.from(entry + "\n", "utf-8");
7128
- import_fs2.default.writeSync(logFd, buf);
7129
- bytesWritten += buf.length;
7130
- if (bytesWritten >= maxFileSize) {
7131
- import_fs2.default.closeSync(logFd);
7132
- rotateLogFiles(logsDir, maxFiles);
7133
- const newPath = import_path8.default.join(logsDir, `analysis-${fileTimestamp()}.log`);
7134
- logFd = import_fs2.default.openSync(newPath, "a");
7135
- bytesWritten = 0;
7136
- }
7137
- }
7138
- const enriched = enrichMessage(message, fields);
7139
- if (onStderrMessage) {
7140
- onStderrMessage(enriched, level);
7141
- } else {
7142
- process.stderr.write(` ${enriched}
7143
- `);
7144
- }
7461
+ async function deleteCredentials() {
7462
+ try {
7463
+ await import_promises5.default.unlink(credentialsPath());
7464
+ return true;
7465
+ } catch {
7466
+ return false;
7145
7467
  }
7146
- return {
7147
- debug: (msg, fields) => log("debug", msg, fields),
7148
- info: (msg, fields) => log("info", msg, fields),
7149
- warn: (msg, fields) => log("warning", msg, fields),
7150
- error: (msg, fields) => log("error", msg, fields)
7151
- };
7468
+ }
7469
+ async function resolveApiToken() {
7470
+ const envToken = process.env.CODACY_API_TOKEN;
7471
+ if (envToken) return envToken;
7472
+ const stored = await loadCredentials();
7473
+ if (stored) return stored;
7474
+ throw new Error("No API token found. Set CODACY_API_TOKEN or run 'codacy-analysis login'.");
7152
7475
  }
7153
7476
  // Annotate the CommonJS export names for ESM import in node:
7154
7477
  0 && (module.exports = {
@@ -7161,12 +7484,15 @@ function createLogger(options = {}) {
7161
7484
  configureApiToken,
7162
7485
  createCodacyConfig,
7163
7486
  createLogger,
7487
+ createNullLogger,
7488
+ deleteCredentials,
7164
7489
  detectLanguages,
7165
7490
  diffConfigs,
7166
7491
  discover,
7167
7492
  formatOutput,
7168
7493
  formatTextSummary,
7169
7494
  getAdapter,
7495
+ getKnownDisplayNames,
7170
7496
  getRegisteredAdapters,
7171
7497
  getRegisteredDescriptors,
7172
7498
  initAutoConfig,
@@ -7179,6 +7505,7 @@ function createLogger(options = {}) {
7179
7505
  listIgnoredFiles,
7180
7506
  listPrFiles,
7181
7507
  listStagedFiles,
7508
+ loadCredentials,
7182
7509
  mergeConfigs,
7183
7510
  parseAutoFilters,
7184
7511
  readCodacyConfig,
@@ -7186,9 +7513,11 @@ function createLogger(options = {}) {
7186
7513
  registerAdapter,
7187
7514
  registerLazyAdapter,
7188
7515
  registerUnsupportedTool,
7516
+ resolveApiToken,
7189
7517
  resolveConfigPath,
7190
7518
  resolveEngineToAdapter,
7191
7519
  rotateLogFiles,
7520
+ saveCredentials,
7192
7521
  validateApiToken,
7193
7522
  writeCodacyConfig,
7194
7523
  writeCodacyConfigFile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/analysis-runner",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Core orchestration engine for Codacy analysis",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,11 +12,11 @@
12
12
  "cli-table3": "^0.6",
13
13
  "minimatch": "10.2.4",
14
14
  "yaml": "^2.6.0",
15
- "@codacy/tooling": "0.12.0"
15
+ "@codacy/tooling": "0.13.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@codacy/openapi-typescript-codegen": "0.0.9",
19
- "@codacy/tools-dummy-1": "0.12.0"
19
+ "@codacy/tools-dummy-1": "0.13.0"
20
20
  },
21
21
  "license": "MIT",
22
22
  "author": "Codacy <support@codacy.com> (https://www.codacy.com)",