sonar-sweep 0.1.0 → 0.2.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 (55) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.d.mts +4 -0
  3. package/dist/cli/index.mjs +3 -0
  4. package/dist/cli-DYHj3O_0.mjs +650 -0
  5. package/dist/cli.d.mts +1 -0
  6. package/dist/cli.mjs +14 -0
  7. package/dist/index.d.mts +263 -0
  8. package/dist/index.mjs +3 -0
  9. package/dist/issue-transition-C1WIMpYQ.mjs +338 -0
  10. package/package.json +36 -11
  11. package/dist/cli/commands/issue-accept.d.ts +0 -13
  12. package/dist/cli/commands/issue-accept.js +0 -49
  13. package/dist/cli/commands/issue-accept.js.map +0 -1
  14. package/dist/cli/commands/pr-coverage.d.ts +0 -16
  15. package/dist/cli/commands/pr-coverage.js +0 -79
  16. package/dist/cli/commands/pr-coverage.js.map +0 -1
  17. package/dist/cli/commands/pr-issues.d.ts +0 -15
  18. package/dist/cli/commands/pr-issues.js +0 -78
  19. package/dist/cli/commands/pr-issues.js.map +0 -1
  20. package/dist/cli/commands/pr-report.d.ts +0 -13
  21. package/dist/cli/commands/pr-report.js +0 -94
  22. package/dist/cli/commands/pr-report.js.map +0 -1
  23. package/dist/cli/commands/pr-review.d.ts +0 -16
  24. package/dist/cli/commands/pr-review.js +0 -93
  25. package/dist/cli/commands/pr-review.js.map +0 -1
  26. package/dist/cli/index.d.ts +0 -1
  27. package/dist/cli/index.js +0 -28
  28. package/dist/cli/index.js.map +0 -1
  29. package/dist/cli/project-key.d.ts +0 -1
  30. package/dist/cli/project-key.js +0 -32
  31. package/dist/cli/project-key.js.map +0 -1
  32. package/dist/cli.d.ts +0 -2
  33. package/dist/cli.js +0 -8
  34. package/dist/cli.js.map +0 -1
  35. package/dist/index.d.ts +0 -6
  36. package/dist/index.js +0 -7
  37. package/dist/index.js.map +0 -1
  38. package/dist/service/issue-transition.d.ts +0 -12
  39. package/dist/service/issue-transition.js +0 -20
  40. package/dist/service/issue-transition.js.map +0 -1
  41. package/dist/service/pr-coverage.d.ts +0 -22
  42. package/dist/service/pr-coverage.js +0 -48
  43. package/dist/service/pr-coverage.js.map +0 -1
  44. package/dist/service/pr-issues.d.ts +0 -29
  45. package/dist/service/pr-issues.js +0 -45
  46. package/dist/service/pr-issues.js.map +0 -1
  47. package/dist/service/pr-report.d.ts +0 -27
  48. package/dist/service/pr-report.js +0 -59
  49. package/dist/service/pr-report.js.map +0 -1
  50. package/dist/service/pr-review.d.ts +0 -39
  51. package/dist/service/pr-review.js +0 -81
  52. package/dist/service/pr-review.js.map +0 -1
  53. package/dist/service/sonarcloud-client.d.ts +0 -100
  54. package/dist/service/sonarcloud-client.js +0 -112
  55. package/dist/service/sonarcloud-client.js.map +0 -1
@@ -0,0 +1,263 @@
1
+ //#region src/service/sonarcloud-client.d.ts
2
+ type SonarCloudClientOptions = {
3
+ baseUrl?: string;
4
+ fetchImpl?: typeof fetch;
5
+ token: string;
6
+ };
7
+ type QualityGateCondition = {
8
+ actualValue?: string;
9
+ comparator: string;
10
+ errorThreshold?: string;
11
+ metricKey: string;
12
+ status: string;
13
+ };
14
+ type ProjectStatusResponse = {
15
+ projectStatus: {
16
+ conditions: Array<QualityGateCondition>;
17
+ status: string;
18
+ };
19
+ };
20
+ type IssuesFacetValue = {
21
+ count: number;
22
+ val: string;
23
+ };
24
+ type IssuesSearchResponse = {
25
+ components?: Array<SonarIssueComponent>;
26
+ facets: Array<{
27
+ property: string;
28
+ values: Array<IssuesFacetValue>;
29
+ }>;
30
+ issues?: Array<SonarIssue>;
31
+ paging?: {
32
+ pageIndex: number;
33
+ pageSize: number;
34
+ total: number;
35
+ };
36
+ total: number;
37
+ };
38
+ type SonarIssue = {
39
+ component: string;
40
+ effort?: string;
41
+ issueStatus?: string;
42
+ key: string;
43
+ line?: number;
44
+ message: string;
45
+ rule: string;
46
+ severity: string;
47
+ status: string;
48
+ type: string;
49
+ };
50
+ type SonarIssueComponent = {
51
+ key: string;
52
+ longName?: string;
53
+ path?: string;
54
+ };
55
+ type SonarHotspot = {
56
+ author?: string;
57
+ component: string;
58
+ creationDate?: string;
59
+ key: string;
60
+ line?: number;
61
+ message: string;
62
+ project: string;
63
+ resolution?: string;
64
+ securityCategory: string;
65
+ status: string;
66
+ updateDate?: string;
67
+ vulnerabilityProbability: string;
68
+ };
69
+ type HotspotsSearchResponse = {
70
+ components?: Array<SonarIssueComponent>;
71
+ hotspots: Array<SonarHotspot>;
72
+ paging: {
73
+ pageIndex: number;
74
+ pageSize: number;
75
+ total: number;
76
+ };
77
+ };
78
+ type HotspotResolution = "SAFE" | "ACKNOWLEDGED" | "FIXED";
79
+ type MeasuresResponse = {
80
+ component: {
81
+ measures: Array<{
82
+ metric: string;
83
+ periods?: Array<{
84
+ index: number;
85
+ value: string;
86
+ }>;
87
+ value?: string;
88
+ }>;
89
+ };
90
+ };
91
+ type ComponentTreeResponse = {
92
+ components: Array<{
93
+ key: string;
94
+ measures: Array<{
95
+ metric: string;
96
+ periods?: Array<{
97
+ index: number;
98
+ value: string;
99
+ }>;
100
+ value?: string;
101
+ }>;
102
+ name: string;
103
+ path?: string;
104
+ }>;
105
+ paging: {
106
+ pageIndex: number;
107
+ pageSize: number;
108
+ total: number;
109
+ };
110
+ };
111
+ declare class SonarCloudClient {
112
+ private readonly token;
113
+ private readonly baseUrl;
114
+ private readonly fetchImpl;
115
+ constructor(options: SonarCloudClientOptions);
116
+ getProjectStatus(projectKey: string, pullRequest: string): Promise<ProjectStatusResponse>;
117
+ getIssuesFacets(projectKey: string, pullRequest: string): Promise<IssuesSearchResponse>;
118
+ getPullRequestIssues(projectKey: string, pullRequest: string, page?: number, pageSize?: number): Promise<IssuesSearchResponse>;
119
+ getMeasures(projectKey: string, pullRequest: string): Promise<MeasuresResponse>;
120
+ getCoverageComponentTree(projectKey: string, pullRequest: string, page?: number, pageSize?: number): Promise<ComponentTreeResponse>;
121
+ getSourceRaw(componentKey: string, pullRequest: string): Promise<string>;
122
+ doIssueTransition(issueKey: string, transition: string, comment?: string): Promise<void>;
123
+ getHotspots(projectKey: string, pullRequest: string, status?: "TO_REVIEW" | "REVIEWED", page?: number, pageSize?: number): Promise<HotspotsSearchResponse>;
124
+ changeHotspotStatus(hotspotKey: string, status: "REVIEWED" | "TO_REVIEW", resolution?: HotspotResolution, comment?: string): Promise<void>;
125
+ private get;
126
+ private post;
127
+ }
128
+ //#endregion
129
+ //#region src/service/pr-report.d.ts
130
+ type PullRequestReport = {
131
+ analysisUrl: string;
132
+ failingQualityGateConditions: Array<{
133
+ actualValue?: string;
134
+ comparator: string;
135
+ errorThreshold?: string;
136
+ metricKey: string;
137
+ }>;
138
+ issueCounts: {
139
+ acceptedIssues: number;
140
+ newIssues: number;
141
+ };
142
+ measures: {
143
+ coverageOnNewCode: number;
144
+ duplicationOnNewCode: number;
145
+ securityHotspots: number;
146
+ };
147
+ projectKey: string;
148
+ pullRequest: string;
149
+ qualityGateStatus: string;
150
+ };
151
+ type PullRequestReportInput = {
152
+ projectKey: string;
153
+ pullRequest: string;
154
+ };
155
+ declare function getPullRequestReport(clientOptions: SonarCloudClientOptions, input: PullRequestReportInput): Promise<PullRequestReport>;
156
+ //#endregion
157
+ //#region src/service/pr-issues.d.ts
158
+ type PullRequestIssue = {
159
+ effort?: string;
160
+ file: string;
161
+ issueStatus: string;
162
+ key: string;
163
+ line?: number;
164
+ message: string;
165
+ rule: string;
166
+ severity: string;
167
+ status: string;
168
+ type: string;
169
+ };
170
+ type PullRequestIssuesReport = {
171
+ analysisUrl: string;
172
+ issues: Array<PullRequestIssue>;
173
+ page: number;
174
+ pageSize: number;
175
+ projectKey: string;
176
+ pullRequest: string;
177
+ total: number;
178
+ };
179
+ type PullRequestIssuesInput = {
180
+ page?: number;
181
+ pageSize?: number;
182
+ projectKey: string;
183
+ pullRequest: string;
184
+ };
185
+ declare function getPullRequestIssues(clientOptions: SonarCloudClientOptions, input: PullRequestIssuesInput): Promise<PullRequestIssuesReport>;
186
+ //#endregion
187
+ //#region src/service/pr-coverage.d.ts
188
+ type PullRequestCoverageFile = {
189
+ coverageOnNewCode: number;
190
+ file: string;
191
+ linesToCover: number;
192
+ uncoveredLines: number;
193
+ };
194
+ type PullRequestCoverageReport = {
195
+ analysisUrl: string;
196
+ files: Array<PullRequestCoverageFile>;
197
+ projectKey: string;
198
+ pullRequest: string;
199
+ threshold: number;
200
+ };
201
+ type PullRequestCoverageInput = {
202
+ includePassing?: boolean;
203
+ maxFiles?: number;
204
+ projectKey: string;
205
+ pullRequest: string;
206
+ threshold?: number;
207
+ };
208
+ declare function getPullRequestCoverage(clientOptions: SonarCloudClientOptions, input: PullRequestCoverageInput): Promise<PullRequestCoverageReport>;
209
+ //#endregion
210
+ //#region src/service/pr-review.d.ts
211
+ type PullRequestIssueReview = {
212
+ effort?: string;
213
+ file: string;
214
+ issueStatus: string;
215
+ issueUrl: string;
216
+ key: string;
217
+ line?: number;
218
+ message: string;
219
+ rule: string;
220
+ severity: string;
221
+ snippet?: {
222
+ endLine: number;
223
+ lines: Array<{
224
+ highlight: boolean;
225
+ line: number;
226
+ text: string;
227
+ }>;
228
+ startLine: number;
229
+ };
230
+ sourceError?: string;
231
+ status: string;
232
+ type: string;
233
+ };
234
+ type PullRequestReviewReport = {
235
+ analysisUrl: string;
236
+ issues: Array<PullRequestIssueReview>;
237
+ projectKey: string;
238
+ pullRequest: string;
239
+ total: number;
240
+ };
241
+ type PullRequestReviewInput = {
242
+ contextLines?: number;
243
+ page?: number;
244
+ pageSize?: number;
245
+ projectKey: string;
246
+ pullRequest: string;
247
+ };
248
+ declare function getPullRequestReview(clientOptions: SonarCloudClientOptions, input: PullRequestReviewInput): Promise<PullRequestReviewReport>;
249
+ //#endregion
250
+ //#region src/service/issue-transition.d.ts
251
+ type IssueTransitionInput = {
252
+ comment?: string;
253
+ issueKey: string;
254
+ transition: "accept" | "wontfix" | "falsepositive" | "confirm" | "reopen" | "resolve";
255
+ };
256
+ type IssueTransitionResult = {
257
+ applied: true;
258
+ issueKey: string;
259
+ transition: string;
260
+ };
261
+ declare function transitionIssue(clientOptions: SonarCloudClientOptions, input: IssueTransitionInput): Promise<IssueTransitionResult>;
262
+ //#endregion
263
+ export { ComponentTreeResponse, HotspotResolution, HotspotsSearchResponse, IssueTransitionInput, IssueTransitionResult, IssuesFacetValue, IssuesSearchResponse, MeasuresResponse, ProjectStatusResponse, PullRequestCoverageFile, PullRequestCoverageInput, PullRequestCoverageReport, PullRequestIssue, PullRequestIssueReview, PullRequestIssuesInput, PullRequestIssuesReport, PullRequestReport, PullRequestReportInput, PullRequestReviewInput, PullRequestReviewReport, QualityGateCondition, SonarCloudClient, SonarCloudClientOptions, SonarHotspot, SonarIssue, SonarIssueComponent, getPullRequestCoverage, getPullRequestIssues, getPullRequestReport, getPullRequestReview, transitionIssue };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { a as getPullRequestReport, i as getPullRequestIssues, n as getPullRequestReview, o as SonarCloudClient, r as getPullRequestCoverage, t as transitionIssue } from "./issue-transition-C1WIMpYQ.mjs";
2
+
3
+ export { SonarCloudClient, getPullRequestCoverage, getPullRequestIssues, getPullRequestReport, getPullRequestReview, transitionIssue };
@@ -0,0 +1,338 @@
1
+ //#region src/service/sonarcloud-client.ts
2
+ var SonarCloudClient = class {
3
+ token;
4
+ baseUrl;
5
+ fetchImpl;
6
+ constructor(options) {
7
+ if (!options.token?.trim()) throw new Error("Missing Sonar token");
8
+ this.token = options.token.trim();
9
+ this.baseUrl = (options.baseUrl ?? "https://sonarcloud.io").replace(/\/$/, "");
10
+ this.fetchImpl = options.fetchImpl ?? fetch;
11
+ }
12
+ async getProjectStatus(projectKey, pullRequest) {
13
+ return this.get("/api/qualitygates/project_status", {
14
+ projectKey,
15
+ pullRequest
16
+ });
17
+ }
18
+ async getIssuesFacets(projectKey, pullRequest) {
19
+ return this.get("/api/issues/search", {
20
+ componentKeys: projectKey,
21
+ facets: "issueStatuses",
22
+ inNewCodePeriod: "true",
23
+ ps: "1",
24
+ pullRequest
25
+ });
26
+ }
27
+ async getPullRequestIssues(projectKey, pullRequest, page = 1, pageSize = 100) {
28
+ return this.get("/api/issues/search", {
29
+ componentKeys: projectKey,
30
+ inNewCodePeriod: "true",
31
+ p: String(page),
32
+ ps: String(pageSize),
33
+ pullRequest,
34
+ resolved: "false"
35
+ });
36
+ }
37
+ async getMeasures(projectKey, pullRequest) {
38
+ return this.get("/api/measures/component", {
39
+ component: projectKey,
40
+ metricKeys: "new_security_hotspots,new_coverage,new_duplicated_lines_density",
41
+ pullRequest
42
+ });
43
+ }
44
+ async getCoverageComponentTree(projectKey, pullRequest, page = 1, pageSize = 100) {
45
+ return this.get("/api/measures/component_tree", {
46
+ component: projectKey,
47
+ metricKeys: "new_coverage,new_lines_to_cover,new_uncovered_lines",
48
+ p: String(page),
49
+ ps: String(pageSize),
50
+ pullRequest,
51
+ qualifiers: "FIL"
52
+ });
53
+ }
54
+ async getSourceRaw(componentKey, pullRequest) {
55
+ const url = new URL("/api/sources/raw", this.baseUrl);
56
+ url.searchParams.set("key", componentKey);
57
+ url.searchParams.set("pullRequest", pullRequest);
58
+ const response = await this.fetchImpl(url, {
59
+ headers: { Authorization: `Bearer ${this.token}` },
60
+ method: "GET"
61
+ });
62
+ if (!response.ok) {
63
+ await response.text();
64
+ throw new Error(`Sonar API request failed (${response.status}) for /api/sources/raw: \${body}`);
65
+ }
66
+ return response.text();
67
+ }
68
+ async doIssueTransition(issueKey, transition, comment) {
69
+ await this.post("/api/issues/do_transition", {
70
+ issue: issueKey,
71
+ transition,
72
+ ...comment ? { comment } : {}
73
+ });
74
+ }
75
+ async getHotspots(projectKey, pullRequest, status = "TO_REVIEW", page = 1, pageSize = 100) {
76
+ return this.get("/api/hotspots/search", {
77
+ p: String(page),
78
+ projectKey,
79
+ ps: String(pageSize),
80
+ pullRequest,
81
+ status
82
+ });
83
+ }
84
+ async changeHotspotStatus(hotspotKey, status, resolution, comment) {
85
+ await this.post("/api/hotspots/change_status", {
86
+ hotspot: hotspotKey,
87
+ status,
88
+ ...resolution ? { resolution } : {},
89
+ ...comment ? { comment } : {}
90
+ });
91
+ }
92
+ async get(path, query) {
93
+ const url = new URL(path, this.baseUrl);
94
+ for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value);
95
+ const response = await this.fetchImpl(url, {
96
+ headers: { Authorization: `Bearer ${this.token}` },
97
+ method: "GET"
98
+ });
99
+ if (!response.ok) {
100
+ await response.text();
101
+ throw new Error(`Sonar API request failed (\${response.status}) for \${path}: \${body}`);
102
+ }
103
+ return await response.json();
104
+ }
105
+ async post(path, form) {
106
+ const url = new URL(path, this.baseUrl);
107
+ const body = new URLSearchParams(form);
108
+ const response = await this.fetchImpl(url, {
109
+ body,
110
+ headers: {
111
+ Authorization: `Bearer \${this.token}`,
112
+ "content-type": "application/x-www-form-urlencoded"
113
+ },
114
+ method: "POST"
115
+ });
116
+ if (!response.ok) {
117
+ await response.text();
118
+ throw new Error(`Sonar API request failed (\${response.status}) for \${path}: \${responseBody}`);
119
+ }
120
+ }
121
+ };
122
+
123
+ //#endregion
124
+ //#region src/service/pr-report.ts
125
+ async function getPullRequestReport(clientOptions, input) {
126
+ const projectKey = input.projectKey.trim();
127
+ const pullRequest = input.pullRequest.trim();
128
+ if (!projectKey) throw new Error("Missing projectKey");
129
+ if (!pullRequest) throw new Error("Missing pullRequest");
130
+ const baseUrl = (clientOptions.baseUrl ?? "https://sonarcloud.io").replace(/\/$/, "");
131
+ const client = new SonarCloudClient(clientOptions);
132
+ const [projectStatusResponse, issuesResponse, measuresResponse] = await Promise.all([
133
+ client.getProjectStatus(projectKey, pullRequest),
134
+ client.getIssuesFacets(projectKey, pullRequest),
135
+ client.getMeasures(projectKey, pullRequest)
136
+ ]);
137
+ const issueStatuses = issuesResponse.facets.find((facet) => facet.property === "issueStatuses");
138
+ const newIssues = sumCounts(issueStatuses?.values, [
139
+ "OPEN",
140
+ "CONFIRMED",
141
+ "REOPENED"
142
+ ]);
143
+ const acceptedIssues = sumCounts(issueStatuses?.values, ["ACCEPTED"]);
144
+ return {
145
+ analysisUrl: `${baseUrl}/dashboard?id=${encodeURIComponent(projectKey)}&pullRequest=${encodeURIComponent(pullRequest)}`,
146
+ failingQualityGateConditions: projectStatusResponse.projectStatus.conditions.filter((condition) => condition.status !== "OK").map((condition) => ({
147
+ actualValue: condition.actualValue,
148
+ comparator: condition.comparator,
149
+ errorThreshold: condition.errorThreshold,
150
+ metricKey: condition.metricKey
151
+ })),
152
+ issueCounts: {
153
+ acceptedIssues,
154
+ newIssues
155
+ },
156
+ measures: {
157
+ coverageOnNewCode: measureValue(measuresResponse.component.measures, "new_coverage"),
158
+ duplicationOnNewCode: measureValue(measuresResponse.component.measures, "new_duplicated_lines_density"),
159
+ securityHotspots: measureValue(measuresResponse.component.measures, "new_security_hotspots")
160
+ },
161
+ projectKey,
162
+ pullRequest,
163
+ qualityGateStatus: projectStatusResponse.projectStatus.status
164
+ };
165
+ }
166
+ function sumCounts(values, keys) {
167
+ if (!values) return 0;
168
+ return values.filter((item) => keys.includes(item.val)).reduce((total, item) => total + item.count, 0);
169
+ }
170
+ function measureValue(measures, metric) {
171
+ const measure = measures.find((item) => item.metric === metric);
172
+ const rawValue = measure?.periods?.[0]?.value ?? measure?.value ?? "0";
173
+ const parsed = Number.parseFloat(rawValue);
174
+ return Number.isFinite(parsed) ? parsed : 0;
175
+ }
176
+
177
+ //#endregion
178
+ //#region src/service/pr-issues.ts
179
+ async function getPullRequestIssues(clientOptions, input) {
180
+ const projectKey = input.projectKey.trim();
181
+ const pullRequest = input.pullRequest.trim();
182
+ if (!projectKey) throw new Error("Missing projectKey");
183
+ if (!pullRequest) throw new Error("Missing pullRequest");
184
+ const page = input.page ?? 1;
185
+ const pageSize = input.pageSize ?? 100;
186
+ const baseUrl = (clientOptions.baseUrl ?? "https://sonarcloud.io").replace(/\/$/, "");
187
+ const response = await new SonarCloudClient(clientOptions).getPullRequestIssues(projectKey, pullRequest, page, pageSize);
188
+ const componentPathByKey = new Map((response.components ?? []).map((component) => [component.key, component.path ?? component.longName ?? component.key]));
189
+ const issues = (response.issues ?? []).map((issue) => {
190
+ const file = componentPathByKey.get(issue.component) ?? issue.component.split(":").slice(1).join(":") ?? issue.component;
191
+ return {
192
+ effort: issue.effort,
193
+ file,
194
+ issueStatus: issue.issueStatus ?? issue.status,
195
+ key: issue.key,
196
+ line: issue.line,
197
+ message: issue.message,
198
+ rule: issue.rule,
199
+ severity: issue.severity,
200
+ status: issue.status,
201
+ type: issue.type
202
+ };
203
+ });
204
+ return {
205
+ analysisUrl: `${baseUrl}/dashboard?id=${encodeURIComponent(projectKey)}&pullRequest=${encodeURIComponent(pullRequest)}`,
206
+ issues,
207
+ page: response.paging?.pageIndex ?? page,
208
+ pageSize: response.paging?.pageSize ?? pageSize,
209
+ projectKey,
210
+ pullRequest,
211
+ total: response.total
212
+ };
213
+ }
214
+
215
+ //#endregion
216
+ //#region src/service/pr-coverage.ts
217
+ async function getPullRequestCoverage(clientOptions, input) {
218
+ const projectKey = input.projectKey.trim();
219
+ const pullRequest = input.pullRequest.trim();
220
+ if (!projectKey) throw new Error("Missing projectKey");
221
+ if (!pullRequest) throw new Error("Missing pullRequest");
222
+ const threshold = input.threshold ?? 80;
223
+ const includePassing = input.includePassing ?? false;
224
+ const maxFiles = Math.max(1, input.maxFiles ?? 20);
225
+ const baseUrl = (clientOptions.baseUrl ?? "https://sonarcloud.io").replace(/\/$/, "");
226
+ const files = (await new SonarCloudClient(clientOptions).getCoverageComponentTree(projectKey, pullRequest, 1, 500)).components.map((component) => {
227
+ const file = component.path ?? component.name;
228
+ return {
229
+ coverageOnNewCode: value(component.measures, "new_coverage"),
230
+ file,
231
+ linesToCover: value(component.measures, "new_lines_to_cover"),
232
+ uncoveredLines: value(component.measures, "new_uncovered_lines")
233
+ };
234
+ }).filter((item) => item.linesToCover > 0).filter((item) => includePassing || item.coverageOnNewCode < threshold).sort((a, b) => a.coverageOnNewCode - b.coverageOnNewCode).slice(0, maxFiles);
235
+ return {
236
+ analysisUrl: `${baseUrl}/dashboard?id=${encodeURIComponent(projectKey)}&pullRequest=${encodeURIComponent(pullRequest)}`,
237
+ files,
238
+ projectKey,
239
+ pullRequest,
240
+ threshold
241
+ };
242
+ }
243
+ function value(measures, metric) {
244
+ const measure = measures.find((item) => item.metric === metric);
245
+ const raw = measure?.periods?.[0]?.value ?? measure?.value ?? "0";
246
+ const parsed = Number.parseFloat(raw);
247
+ return Number.isFinite(parsed) ? parsed : 0;
248
+ }
249
+
250
+ //#endregion
251
+ //#region src/service/pr-review.ts
252
+ async function getPullRequestReview(clientOptions, input) {
253
+ const projectKey = input.projectKey.trim();
254
+ const pullRequest = input.pullRequest.trim();
255
+ if (!projectKey) throw new Error("Missing projectKey");
256
+ if (!pullRequest) throw new Error("Missing pullRequest");
257
+ const page = input.page ?? 1;
258
+ const pageSize = input.pageSize ?? 100;
259
+ const contextLines = Math.max(0, input.contextLines ?? 3);
260
+ const baseUrl = (clientOptions.baseUrl ?? "https://sonarcloud.io").replace(/\/$/, "");
261
+ const client = new SonarCloudClient(clientOptions);
262
+ const response = await client.getPullRequestIssues(projectKey, pullRequest, page, pageSize);
263
+ const componentPathByKey = new Map((response.components ?? []).map((component) => [component.key, component.path ?? component.longName ?? component.key]));
264
+ const issues = await Promise.all((response.issues ?? []).map(async (issue) => {
265
+ const file = componentPathByKey.get(issue.component) ?? issue.component.split(":").slice(1).join(":") ?? issue.component;
266
+ const result = {
267
+ effort: issue.effort,
268
+ file,
269
+ issueStatus: issue.issueStatus ?? issue.status,
270
+ issueUrl: buildIssueUrl(baseUrl, projectKey, pullRequest, issue.key),
271
+ key: issue.key,
272
+ line: issue.line,
273
+ message: issue.message,
274
+ rule: issue.rule,
275
+ severity: issue.severity,
276
+ status: issue.status,
277
+ type: issue.type
278
+ };
279
+ if (!issue.line) return result;
280
+ try {
281
+ result.snippet = makeSnippet(await client.getSourceRaw(issue.component, pullRequest), issue.line, contextLines);
282
+ } catch (error) {
283
+ result.sourceError = error instanceof Error ? error.message : String(error);
284
+ }
285
+ return result;
286
+ }));
287
+ return {
288
+ analysisUrl: `${baseUrl}/dashboard?id=${encodeURIComponent(projectKey)}&pullRequest=${encodeURIComponent(pullRequest)}`,
289
+ issues,
290
+ projectKey,
291
+ pullRequest,
292
+ total: response.total
293
+ };
294
+ }
295
+ function buildIssueUrl(baseUrl, projectKey, pullRequest, issueKey) {
296
+ const url = new URL("/project/issues", baseUrl);
297
+ url.searchParams.set("id", projectKey);
298
+ url.searchParams.set("pullRequest", pullRequest);
299
+ url.searchParams.set("issues", issueKey);
300
+ url.searchParams.set("open", issueKey);
301
+ return String(url);
302
+ }
303
+ function makeSnippet(source, issueLine, contextLines) {
304
+ const lines = source.split(/\r?\n/);
305
+ const startLine = Math.max(1, issueLine - contextLines);
306
+ const endLine = Math.min(lines.length, issueLine + contextLines);
307
+ return {
308
+ endLine,
309
+ lines: lines.slice(startLine - 1, endLine).map((text, index) => {
310
+ const line = startLine + index;
311
+ return {
312
+ highlight: line === issueLine,
313
+ line,
314
+ text
315
+ };
316
+ }),
317
+ startLine
318
+ };
319
+ }
320
+
321
+ //#endregion
322
+ //#region src/service/issue-transition.ts
323
+ async function transitionIssue(clientOptions, input) {
324
+ const issueKey = input.issueKey.trim();
325
+ const transition = input.transition.trim();
326
+ const comment = input.comment?.trim();
327
+ if (!issueKey) throw new Error("Missing issueKey");
328
+ if (!transition) throw new Error("Missing transition");
329
+ await new SonarCloudClient(clientOptions).doIssueTransition(issueKey, transition, comment);
330
+ return {
331
+ applied: true,
332
+ issueKey,
333
+ transition
334
+ };
335
+ }
336
+
337
+ //#endregion
338
+ export { getPullRequestReport as a, getPullRequestIssues as i, getPullRequestReview as n, SonarCloudClient as o, getPullRequestCoverage as r, transitionIssue as t };
package/package.json CHANGED
@@ -1,34 +1,59 @@
1
1
  {
2
2
  "name": "sonar-sweep",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for fetching SonarQube Cloud pull request details",
5
- "type": "module",
5
+ "homepage": "https://github.com/clentfort/sonar-sweep#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/clentfort/sonar-sweep/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "Christian Lentfort",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/clentfort/sonar-sweep.git"
14
+ },
6
15
  "bin": {
7
- "sonar-sweep": "dist/cli.js"
16
+ "sonar-sweep": "dist/cli.mjs"
8
17
  },
9
- "main": "dist/index.js",
10
- "types": "dist/index.d.ts",
11
18
  "files": [
12
19
  "dist",
13
20
  "README.md"
14
21
  ],
15
- "engines": {
16
- "node": ">=20"
22
+ "type": "module",
23
+ "main": "dist/index.mjs",
24
+ "types": "dist/index.d.mts",
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
17
28
  },
18
29
  "scripts": {
19
- "build": "tsc -p tsconfig.json",
30
+ "build": "tsdown src/index.ts src/cli.ts src/cli/index.ts --format esm --dts",
20
31
  "test": "vitest run",
21
- "test:e2e": "npm run build && vitest run --config vitest.e2e.config.ts",
22
- "type-check": "tsc -p tsconfig.json --noEmit",
23
- "start": "node dist/cli.js"
32
+ "test:e2e": "run-s build \"test:e2e:run {@}\" --",
33
+ "test:e2e:run": "vitest run --config vitest.e2e.config.ts",
34
+ "type-check": "tsgo --noEmit",
35
+ "lint": "oxlint .",
36
+ "format": "oxfmt --write .",
37
+ "start": "node dist/cli.mjs",
38
+ "prepare": "husky"
24
39
  },
25
40
  "dependencies": {
26
41
  "yargs": "17.7.2"
27
42
  },
28
43
  "devDependencies": {
44
+ "@nkzw/oxlint-config": "^1.0.1",
29
45
  "@types/node": "22.13.10",
30
46
  "@types/yargs": "17.0.33",
47
+ "@typescript/native-preview": "^7.0.0-dev.20260225.1",
48
+ "husky": "^9.1.7",
49
+ "npm-run-all2": "^8.0.4",
50
+ "oxfmt": "^0.35.0",
51
+ "oxlint": "^1.50.0",
52
+ "tsdown": "^0.20.3",
31
53
  "typescript": "5.8.2",
32
54
  "vitest": "4.0.15"
55
+ },
56
+ "engines": {
57
+ "node": ">=20"
33
58
  }
34
59
  }
@@ -1,13 +0,0 @@
1
- import type { Argv } from 'yargs';
2
- type IssueAcceptArgs = {
3
- token: string;
4
- baseUrl: string;
5
- issueKey: string;
6
- comment?: string;
7
- json: boolean;
8
- };
9
- export declare const command = "issue-accept <issueKey>";
10
- export declare const describe = "Mark a Sonar issue as accepted (transition=accept)";
11
- export declare function builder(yargs: Argv): Argv<IssueAcceptArgs>;
12
- export declare function handler(args: IssueAcceptArgs): Promise<void>;
13
- export {};