qualink 0.2.0 → 0.4.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.
Files changed (48) hide show
  1. package/README.md +79 -0
  2. package/dist/cli/command-factory.d.ts +23 -23
  3. package/dist/cli/commands/biome.d.ts +23 -23
  4. package/dist/cli/commands/coverage-dotnet.d.ts +23 -23
  5. package/dist/cli/commands/coverage-js.d.ts +23 -23
  6. package/dist/cli/commands/eslint.d.ts +23 -23
  7. package/dist/cli/commands/index.d.ts +1 -0
  8. package/dist/cli/commands/index.js +1 -0
  9. package/dist/cli/commands/lighthouse.d.ts +23 -23
  10. package/dist/cli/commands/lighthouse.js +4 -1
  11. package/dist/cli/commands/meta.d.ts +23 -23
  12. package/dist/cli/commands/pipeline.d.ts +76 -0
  13. package/dist/cli/commands/pipeline.js +35 -0
  14. package/dist/cli/commands/sarif.d.ts +23 -23
  15. package/dist/cli/common-args.d.ts +2 -2
  16. package/dist/cli/common-args.js +1 -1
  17. package/dist/cli/detect-ci.d.ts +2 -0
  18. package/dist/cli/detect-ci.js +47 -0
  19. package/dist/cli/detect-project.js +31 -5
  20. package/dist/cli/detect-solution.d.ts +2 -0
  21. package/dist/cli/detect-solution.js +70 -0
  22. package/dist/cli/index.js +123 -1
  23. package/dist/cli/multi-collect/config.d.ts +23 -0
  24. package/dist/cli/multi-collect/config.js +105 -0
  25. package/dist/cli/multi-collect/discover.d.ts +11 -0
  26. package/dist/cli/multi-collect/discover.js +84 -0
  27. package/dist/cli/multi-collect/patterns.d.ts +16 -0
  28. package/dist/cli/multi-collect/patterns.js +20 -0
  29. package/dist/cli/multi-collect/resolve-metadata.d.ts +17 -0
  30. package/dist/cli/multi-collect/resolve-metadata.js +147 -0
  31. package/dist/cli/multi-collect/run-collector.d.ts +11 -0
  32. package/dist/cli/multi-collect/run-collector.js +75 -0
  33. package/dist/cli/parse-metadata.d.ts +1 -0
  34. package/dist/cli/parse-metadata.js +3 -3
  35. package/dist/collectors/eslint.d.ts +2 -0
  36. package/dist/collectors/eslint.js +32 -3
  37. package/dist/collectors/index.d.ts +1 -0
  38. package/dist/collectors/index.js +1 -0
  39. package/dist/collectors/lighthouse.d.ts +4 -1
  40. package/dist/collectors/lighthouse.js +89 -1
  41. package/dist/collectors/pipeline.d.ts +13 -0
  42. package/dist/collectors/pipeline.js +31 -0
  43. package/dist/normalize.js +1 -1
  44. package/dist/sinks/elastic.js +1 -0
  45. package/dist/types.d.ts +32 -4
  46. package/package.json +6 -6
  47. package/dist/cli/detect-package.d.ts +0 -3
  48. package/dist/cli/detect-package.js +0 -42
@@ -2,10 +2,10 @@ import { asOptionalString } from "../utils/assert.js";
2
2
  import { CliError } from "./cli-error.js";
3
3
  import { argValue, envOrArg } from "./common-args.js";
4
4
  import { detectBranch, detectCommitSha, detectPipelineProvider, detectPipelineRunId, } from "./detect-ci.js";
5
- import { detectPackageName } from "./detect-package.js";
6
5
  import { detectProjectName } from "./detect-project.js";
7
6
  import { detectRepo } from "./detect-repo.js";
8
- const DEFAULT_COLLECTOR_VERSION = "0.1.0";
7
+ import { detectSolution } from "./detect-solution.js";
8
+ export const DEFAULT_COLLECTOR_VERSION = "0.1.0";
9
9
  function parseTags(value) {
10
10
  if (typeof value !== "string" || value.trim().length === 0) {
11
11
  return [];
@@ -42,7 +42,7 @@ export function parseCommonMetadata(args) {
42
42
  pipelineRunId,
43
43
  pipelineProvider,
44
44
  environment: environmentRaw,
45
- packageName: asOptionalString(detectPackageName(args)),
45
+ solution: asOptionalString(detectSolution(args)),
46
46
  projectName: asOptionalString(detectProjectName(args)),
47
47
  collectorVersion,
48
48
  };
@@ -1,6 +1,7 @@
1
1
  import type { CommonMetadata, EslintMetricDocument, Language } from "../types.js";
2
2
  interface EslintMessage {
3
3
  ruleId: string | null;
4
+ severity?: number | undefined;
4
5
  }
5
6
  interface EslintFileResult {
6
7
  filePath: string;
@@ -9,6 +10,7 @@ interface EslintFileResult {
9
10
  fixableErrorCount: number;
10
11
  fixableWarningCount: number;
11
12
  messages: EslintMessage[];
13
+ suppressedMessages: EslintMessage[];
12
14
  }
13
15
  export interface EslintCollectorOptions {
14
16
  includeRules: boolean;
@@ -12,16 +12,20 @@ function asEslintResults(input) {
12
12
  if (!Array.isArray(messagesRaw)) {
13
13
  throw new Error("Invalid ESLint messages array");
14
14
  }
15
- const messages = messagesRaw.map((message) => {
15
+ const parseMessages = (raw) => raw.map((message) => {
16
16
  if (!isRecord(message)) {
17
17
  return { ruleId: null };
18
18
  }
19
19
  const ruleValue = message.ruleId;
20
+ const severity = typeof message.severity === "number" ? message.severity : undefined;
20
21
  if (typeof ruleValue !== "string" && ruleValue !== null) {
21
- return { ruleId: null };
22
+ return { ruleId: null, severity };
22
23
  }
23
- return { ruleId: ruleValue };
24
+ return { ruleId: ruleValue, severity };
24
25
  });
26
+ const messages = parseMessages(messagesRaw);
27
+ const suppressedRaw = entry.suppressedMessages;
28
+ const suppressedMessages = Array.isArray(suppressedRaw) ? parseMessages(suppressedRaw) : [];
25
29
  return {
26
30
  filePath: typeof entry.filePath === "string" ? entry.filePath : "unknown",
27
31
  errorCount: typeof entry.errorCount === "number" ? entry.errorCount : 0,
@@ -29,6 +33,7 @@ function asEslintResults(input) {
29
33
  fixableErrorCount: typeof entry.fixableErrorCount === "number" ? entry.fixableErrorCount : 0,
30
34
  fixableWarningCount: typeof entry.fixableWarningCount === "number" ? entry.fixableWarningCount : 0,
31
35
  messages,
36
+ suppressedMessages,
32
37
  };
33
38
  });
34
39
  }
@@ -58,7 +63,10 @@ export function collectEslint(input, metadata, options) {
58
63
  let warnings = 0;
59
64
  let fixableErrors = 0;
60
65
  let fixableWarnings = 0;
66
+ let suppressedErrors = 0;
67
+ let suppressedWarnings = 0;
61
68
  const rules = new Map();
69
+ const suppressedRules = new Map();
62
70
  const files = [];
63
71
  for (const row of rows) {
64
72
  errors += row.errorCount;
@@ -81,6 +89,20 @@ export function collectEslint(input, metadata, options) {
81
89
  const existing = rules.get(message.ruleId) ?? 0;
82
90
  rules.set(message.ruleId, existing + 1);
83
91
  }
92
+ for (const message of row.suppressedMessages) {
93
+ // severity: 2 = error, 1 = warning
94
+ if (message.severity === 2) {
95
+ suppressedErrors++;
96
+ }
97
+ else {
98
+ suppressedWarnings++;
99
+ }
100
+ if (!message.ruleId) {
101
+ continue;
102
+ }
103
+ const existing = suppressedRules.get(message.ruleId) ?? 0;
104
+ suppressedRules.set(message.ruleId, existing + 1);
105
+ }
84
106
  }
85
107
  const languages = options.languages ?? detectEslintLanguages(rows);
86
108
  const doc = {
@@ -94,11 +116,18 @@ export function collectEslint(input, metadata, options) {
94
116
  warnings,
95
117
  fixable_errors: fixableErrors,
96
118
  fixable_warnings: fixableWarnings,
119
+ suppressed_errors: suppressedErrors,
120
+ suppressed_warnings: suppressedWarnings,
97
121
  };
98
122
  if (options.includeRules) {
99
123
  const sortedRules = [...rules.entries()].sort((a, b) => b[1] - a[1]);
100
124
  const slice = options.topRules > 0 ? sortedRules.slice(0, options.topRules) : sortedRules;
101
125
  doc.rules_violated = Object.fromEntries(slice);
126
+ if (suppressedRules.size > 0) {
127
+ const sortedSuppressed = [...suppressedRules.entries()].sort((a, b) => b[1] - a[1]);
128
+ const suppressedSlice = options.topRules > 0 ? sortedSuppressed.slice(0, options.topRules) : sortedSuppressed;
129
+ doc.suppressed_rules = Object.fromEntries(suppressedSlice);
130
+ }
102
131
  }
103
132
  if (files.length > 0 && (options.topFiles > 0 || options.includeAllFiles)) {
104
133
  const sortedFiles = [...files].sort((a, b) => {
@@ -3,4 +3,5 @@ export { collectCoverageDotnet } from "./coverage-dotnet.js";
3
3
  export { collectCoverageJs } from "./coverage-js.js";
4
4
  export { collectEslint } from "./eslint.js";
5
5
  export { collectLighthouse } from "./lighthouse.js";
6
+ export { collectPipeline, normalizeStatus as normalizePipelineStatus } from "./pipeline.js";
6
7
  export { collectSarif } from "./sarif.js";
@@ -3,4 +3,5 @@ export { collectCoverageDotnet } from "./coverage-dotnet.js";
3
3
  export { collectCoverageJs } from "./coverage-js.js";
4
4
  export { collectEslint } from "./eslint.js";
5
5
  export { collectLighthouse } from "./lighthouse.js";
6
+ export { collectPipeline, normalizeStatus as normalizePipelineStatus } from "./pipeline.js";
6
7
  export { collectSarif } from "./sarif.js";
@@ -1,2 +1,5 @@
1
1
  import type { CommonMetadata, LighthouseMetricDocument } from "../types.js";
2
- export declare function collectLighthouse(input: unknown, metadata: CommonMetadata, url: string): LighthouseMetricDocument[];
2
+ export interface LighthouseCollectorOptions {
3
+ includeFilmstrip: boolean;
4
+ }
5
+ export declare function collectLighthouse(input: unknown, metadata: CommonMetadata, url: string, options?: LighthouseCollectorOptions): LighthouseMetricDocument[];
@@ -12,7 +12,42 @@ function readCategoryScore(categories, key) {
12
12
  }
13
13
  return toPctFromUnitScore(score);
14
14
  }
15
- export function collectLighthouse(input, metadata, url) {
15
+ function readAuditNumeric(audits, key) {
16
+ const audit = audits[key];
17
+ if (!isRecord(audit)) {
18
+ return undefined;
19
+ }
20
+ const value = audit.numericValue;
21
+ if (typeof value !== "number" || !Number.isFinite(value)) {
22
+ return undefined;
23
+ }
24
+ return value;
25
+ }
26
+ function extractFilmstrip(audits) {
27
+ const thumbnail = audits["screenshot-thumbnails"];
28
+ if (!isRecord(thumbnail))
29
+ return undefined;
30
+ const details = thumbnail.details;
31
+ if (!isRecord(details))
32
+ return undefined;
33
+ if (details.type !== "filmstrip")
34
+ return undefined;
35
+ const items = details.items;
36
+ if (!Array.isArray(items))
37
+ return undefined;
38
+ const frames = [];
39
+ for (const item of items) {
40
+ if (!isRecord(item))
41
+ continue;
42
+ if (typeof item.timing !== "number" || !Number.isFinite(item.timing))
43
+ continue;
44
+ if (typeof item.data !== "string")
45
+ continue;
46
+ frames.push({ timing: item.timing, data: item.data });
47
+ }
48
+ return frames.length > 0 ? frames : undefined;
49
+ }
50
+ export function collectLighthouse(input, metadata, url, options) {
16
51
  if (!isRecord(input)) {
17
52
  throw new Error("Lighthouse input must be an object");
18
53
  }
@@ -33,5 +68,58 @@ export function collectLighthouse(input, metadata, url) {
33
68
  best_practices: readCategoryScore(categoriesUnknown, "best-practices"),
34
69
  seo: readCategoryScore(categoriesUnknown, "seo"),
35
70
  };
71
+ const audits = input.audits;
72
+ if (isRecord(audits)) {
73
+ const fcp = readAuditNumeric(audits, "first-contentful-paint");
74
+ const lcp = readAuditNumeric(audits, "largest-contentful-paint");
75
+ const tbt = readAuditNumeric(audits, "total-blocking-time");
76
+ const cls = readAuditNumeric(audits, "cumulative-layout-shift");
77
+ const si = readAuditNumeric(audits, "speed-index");
78
+ const tti = readAuditNumeric(audits, "interactive");
79
+ const ttfb = readAuditNumeric(audits, "server-response-time");
80
+ const totalByteWeight = readAuditNumeric(audits, "total-byte-weight");
81
+ const domSize = readAuditNumeric(audits, "dom-size");
82
+ if (fcp !== undefined)
83
+ doc.fcp = fcp;
84
+ if (lcp !== undefined)
85
+ doc.lcp = lcp;
86
+ if (tbt !== undefined)
87
+ doc.tbt = tbt;
88
+ if (cls !== undefined)
89
+ doc.cls = cls;
90
+ if (si !== undefined)
91
+ doc.si = si;
92
+ if (tti !== undefined)
93
+ doc.tti = tti;
94
+ if (ttfb !== undefined)
95
+ doc.ttfb = ttfb;
96
+ if (totalByteWeight !== undefined)
97
+ doc.total_byte_weight = totalByteWeight;
98
+ if (domSize !== undefined)
99
+ doc.dom_size = domSize;
100
+ const auditScores = {};
101
+ const auditValues = {};
102
+ for (const [id, raw] of Object.entries(audits)) {
103
+ if (!isRecord(raw))
104
+ continue;
105
+ if (typeof raw.score === "number" && Number.isFinite(raw.score)) {
106
+ auditScores[id] = toPctFromUnitScore(raw.score);
107
+ }
108
+ if (typeof raw.numericValue === "number" && Number.isFinite(raw.numericValue)) {
109
+ auditValues[id] = raw.numericValue;
110
+ }
111
+ }
112
+ if (Object.keys(auditScores).length > 0) {
113
+ doc.audit_scores = auditScores;
114
+ }
115
+ if (Object.keys(auditValues).length > 0) {
116
+ doc.audit_values = auditValues;
117
+ }
118
+ if (options?.includeFilmstrip) {
119
+ const filmstrip = extractFilmstrip(audits);
120
+ if (filmstrip)
121
+ doc.filmstrip = filmstrip;
122
+ }
123
+ }
36
124
  return [doc];
37
125
  }
@@ -0,0 +1,13 @@
1
+ import type { CommonMetadata, PipelineMetricDocument } from "../types.js";
2
+ export type PipelineStatus = PipelineMetricDocument["pipeline_status"];
3
+ export declare function normalizeStatus(raw: string): PipelineStatus;
4
+ interface PipelineInput {
5
+ status: string;
6
+ pipelineName: string;
7
+ trigger: string;
8
+ durationMs: number | null;
9
+ startTime: string | null;
10
+ stageName: string | null;
11
+ }
12
+ export declare function collectPipeline(input: PipelineInput, metadata: CommonMetadata): PipelineMetricDocument[];
13
+ export {};
@@ -0,0 +1,31 @@
1
+ import { baseDocument } from "../normalize.js";
2
+ const SUCCEEDED_ALIASES = new Set(["succeeded", "success", "pass", "passed"]);
3
+ const FAILED_ALIASES = new Set(["failed", "failure", "fail"]);
4
+ const CANCELED_ALIASES = new Set(["canceled", "cancelled", "aborted", "skipped"]);
5
+ export function normalizeStatus(raw) {
6
+ const lower = raw.toLowerCase();
7
+ if (SUCCEEDED_ALIASES.has(lower))
8
+ return "succeeded";
9
+ if (FAILED_ALIASES.has(lower))
10
+ return "failed";
11
+ if (CANCELED_ALIASES.has(lower))
12
+ return "canceled";
13
+ return "unknown";
14
+ }
15
+ export function collectPipeline(input, metadata) {
16
+ const doc = {
17
+ ...baseDocument({
18
+ metricType: "pipeline",
19
+ tool: metadata.pipelineProvider,
20
+ languages: [],
21
+ metadata,
22
+ }),
23
+ pipeline_name: input.pipelineName,
24
+ pipeline_status: normalizeStatus(input.status),
25
+ pipeline_trigger: input.trigger,
26
+ duration_ms: input.durationMs,
27
+ start_time: input.startTime,
28
+ stage_name: input.stageName,
29
+ };
30
+ return [doc];
31
+ }
package/dist/normalize.js CHANGED
@@ -6,7 +6,7 @@ export function baseDocument(input) {
6
6
  tool: input.tool,
7
7
  languages: input.languages,
8
8
  repo: metadata.repo,
9
- package: metadata.packageName,
9
+ solution: metadata.solution,
10
10
  project: metadata.projectName,
11
11
  category: metadata.category,
12
12
  tags: metadata.tags,
@@ -7,6 +7,7 @@ const INDEX_BY_TYPE = {
7
7
  sarif: "codequality-sarif",
8
8
  "coverage-dotnet": "codequality-coverage-dotnet",
9
9
  meta: "codequality-meta",
10
+ pipeline: "codequality-pipeline",
10
11
  };
11
12
  function sleep(ms) {
12
13
  return new Promise((resolve) => setTimeout(resolve, ms));
package/dist/types.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  export type Language = "js" | "ts" | "csharp" | (string & {});
2
2
  export type Environment = "dev" | "test" | "prod" | "ci";
3
- export type MetricType = "biome" | "eslint" | "lighthouse" | "coverage-js" | "sarif" | "coverage-dotnet" | "meta";
3
+ export type MetricType = "biome" | "eslint" | "lighthouse" | "coverage-js" | "sarif" | "coverage-dotnet" | "meta" | "pipeline";
4
4
  export interface BaseMetricDocument {
5
5
  "@timestamp": string;
6
6
  metric_type: MetricType;
7
7
  tool: string;
8
8
  languages: Language[];
9
9
  repo: string;
10
- package: string | null;
10
+ solution: string | null;
11
11
  project: string | null;
12
12
  category: string | null;
13
13
  tags: string[];
@@ -24,7 +24,10 @@ export interface EslintMetricDocument extends BaseMetricDocument {
24
24
  warnings: number;
25
25
  fixable_errors: number;
26
26
  fixable_warnings: number;
27
+ suppressed_errors: number;
28
+ suppressed_warnings: number;
27
29
  rules_violated?: Record<string, number>;
30
+ suppressed_rules?: Record<string, number>;
28
31
  top_files?: EslintFileIssue[];
29
32
  all_files?: EslintFileIssue[];
30
33
  }
@@ -35,6 +38,10 @@ export interface EslintFileIssue {
35
38
  fixable_errors: number;
36
39
  fixable_warnings: number;
37
40
  }
41
+ export interface LighthouseFilmstripFrame {
42
+ timing: number;
43
+ data: string;
44
+ }
38
45
  export interface LighthouseMetricDocument extends BaseMetricDocument {
39
46
  metric_type: "lighthouse";
40
47
  url: string;
@@ -42,6 +49,18 @@ export interface LighthouseMetricDocument extends BaseMetricDocument {
42
49
  accessibility: number;
43
50
  best_practices: number;
44
51
  seo: number;
52
+ fcp?: number;
53
+ lcp?: number;
54
+ tbt?: number;
55
+ cls?: number;
56
+ si?: number;
57
+ tti?: number;
58
+ ttfb?: number;
59
+ total_byte_weight?: number;
60
+ dom_size?: number;
61
+ audit_scores?: Record<string, number>;
62
+ audit_values?: Record<string, number>;
63
+ filmstrip?: LighthouseFilmstripFrame[];
45
64
  }
46
65
  export interface CoverageMetricDocument extends BaseMetricDocument {
47
66
  lines_total: number;
@@ -90,7 +109,16 @@ export interface DotnetCoverageMetricDocument extends CoverageMetricDocument {
90
109
  export interface MetaMetricDocument extends BaseMetricDocument {
91
110
  metric_type: "meta";
92
111
  }
93
- export type NormalizedDocument = BiomeMetricDocument | EslintMetricDocument | LighthouseMetricDocument | CoverageJsMetricDocument | SarifMetricDocument | DotnetCoverageMetricDocument | MetaMetricDocument;
112
+ export interface PipelineMetricDocument extends BaseMetricDocument {
113
+ metric_type: "pipeline";
114
+ pipeline_name: string;
115
+ pipeline_status: "succeeded" | "failed" | "canceled" | "unknown";
116
+ pipeline_trigger: string;
117
+ duration_ms: number | null;
118
+ start_time: string | null;
119
+ stage_name: string | null;
120
+ }
121
+ export type NormalizedDocument = BiomeMetricDocument | EslintMetricDocument | LighthouseMetricDocument | CoverageJsMetricDocument | SarifMetricDocument | DotnetCoverageMetricDocument | MetaMetricDocument | PipelineMetricDocument;
94
122
  export interface CommonMetadata {
95
123
  repo: string;
96
124
  category: string | null;
@@ -100,7 +128,7 @@ export interface CommonMetadata {
100
128
  pipelineRunId: string;
101
129
  pipelineProvider: string;
102
130
  environment: Environment;
103
- packageName: string | null;
131
+ solution: string | null;
104
132
  projectName: string | null;
105
133
  collectorVersion: string;
106
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualink",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Collect, normalize, and relay code quality metrics from CI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,16 +17,16 @@
17
17
  "node": ">=22"
18
18
  },
19
19
  "dependencies": {
20
- "citty": "^0.1.6",
21
- "fast-xml-parser": "^4.5.1"
20
+ "citty": "^0.2.1",
21
+ "fast-xml-parser": "^5.4.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@biomejs/biome": "^2.4.4",
25
- "@types/node": "^24.0.0",
24
+ "@biomejs/biome": "^2.4.5",
25
+ "@types/node": "^25.3.3",
26
26
  "@vitest/coverage-v8": "^4.0.18",
27
27
  "git-cliff": "^2.12.0",
28
28
  "ts-node": "^10.9.2",
29
- "typescript": "^5.8.2",
29
+ "typescript": "^5.9.3",
30
30
  "vitest": "^4.0.18"
31
31
  },
32
32
  "repository": {
@@ -1,3 +0,0 @@
1
- import { type CommonArgs } from "./common-args.js";
2
- export declare function isInsideWorkspacePackage(): boolean;
3
- export declare function detectPackageName(args: CommonArgs): string | undefined;
@@ -1,42 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { argValue, envOrArg } from "./common-args.js";
4
- import { runGit } from "./git.js";
5
- export function isInsideWorkspacePackage() {
6
- const gitRoot = runGit(["rev-parse", "--show-toplevel"]);
7
- if (!gitRoot) {
8
- return false;
9
- }
10
- return resolve(".") !== resolve(gitRoot);
11
- }
12
- function readPackageJsonName() {
13
- try {
14
- const pkgPath = resolve("package.json");
15
- if (!existsSync(pkgPath)) {
16
- return undefined;
17
- }
18
- const raw = JSON.parse(readFileSync(pkgPath, "utf-8"));
19
- if (typeof raw === "object" && raw !== null && "name" in raw && typeof raw.name === "string") {
20
- return raw.name;
21
- }
22
- }
23
- catch {
24
- // ignore
25
- }
26
- return undefined;
27
- }
28
- export function detectPackageName(args) {
29
- const explicit = envOrArg(argValue(args, "package"), "QUALINK_PACKAGE");
30
- if (explicit) {
31
- return explicit;
32
- }
33
- const pnpmName = process.env.PNPM_PACKAGE_NAME;
34
- if (pnpmName && pnpmName.trim().length > 0) {
35
- return pnpmName;
36
- }
37
- // Auto-detect from ./package.json when running inside a workspace subdirectory
38
- if (isInsideWorkspacePackage()) {
39
- return readPackageJsonName();
40
- }
41
- return undefined;
42
- }