@blogic-cz/agent-tools 0.8.2 → 0.8.4
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/package.json +1 -1
- package/src/gh-tool/pr/review.ts +109 -41
- package/src/gh-tool/service.ts +6 -2
- package/src/shared/index.ts +2 -0
- package/src/shared/log-transform.ts +240 -0
package/package.json
CHANGED
package/src/gh-tool/pr/review.ts
CHANGED
|
@@ -19,10 +19,10 @@ import { viewPR } from "./core";
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
|
|
21
21
|
const REVIEW_THREADS_QUERY = `
|
|
22
|
-
query($owner: String!, $name: String!, $pr: Int
|
|
22
|
+
query($owner: String!, $name: String!, $pr: Int!, $after: String) {
|
|
23
23
|
repository(owner: $owner, name: $name) {
|
|
24
24
|
pullRequest(number: $pr) {
|
|
25
|
-
reviewThreads(first: 100) {
|
|
25
|
+
reviewThreads(first: 100, after: $after) {
|
|
26
26
|
nodes {
|
|
27
27
|
id
|
|
28
28
|
isResolved
|
|
@@ -37,6 +37,10 @@ const REVIEW_THREADS_QUERY = `
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
pageInfo {
|
|
41
|
+
hasNextPage
|
|
42
|
+
endCursor
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
}
|
|
@@ -100,6 +104,10 @@ type ThreadsQueryResult = {
|
|
|
100
104
|
pullRequest: {
|
|
101
105
|
reviewThreads: {
|
|
102
106
|
nodes: ThreadNode[];
|
|
107
|
+
pageInfo: {
|
|
108
|
+
hasNextPage: boolean;
|
|
109
|
+
endCursor: string | null;
|
|
110
|
+
};
|
|
103
111
|
};
|
|
104
112
|
};
|
|
105
113
|
};
|
|
@@ -156,6 +164,78 @@ type ReviewCommentById = {
|
|
|
156
164
|
pull_request_url: string;
|
|
157
165
|
};
|
|
158
166
|
|
|
167
|
+
const REST_PAGE_SIZE = 100;
|
|
168
|
+
|
|
169
|
+
const parseJson = <T>(
|
|
170
|
+
stdout: string,
|
|
171
|
+
command: string,
|
|
172
|
+
parseFailurePrefix: string,
|
|
173
|
+
): Effect.Effect<T, GitHubCommandError> =>
|
|
174
|
+
Effect.try({
|
|
175
|
+
try: () => JSON.parse(stdout) as T,
|
|
176
|
+
catch: (error) =>
|
|
177
|
+
new GitHubCommandError({
|
|
178
|
+
command,
|
|
179
|
+
exitCode: 0,
|
|
180
|
+
stderr: `${parseFailurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
|
|
181
|
+
message: `${parseFailurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const fetchAllRestPages = Effect.fn("pr.fetchAllRestPages")(function* <T>(
|
|
186
|
+
endpoint: string,
|
|
187
|
+
command: string,
|
|
188
|
+
parseFailurePrefix: string,
|
|
189
|
+
) {
|
|
190
|
+
const service = yield* GitHubService;
|
|
191
|
+
|
|
192
|
+
const results: T[] = [];
|
|
193
|
+
let page = 1;
|
|
194
|
+
|
|
195
|
+
while (true) {
|
|
196
|
+
const separator = endpoint.includes("?") ? "&" : "?";
|
|
197
|
+
const result = yield* service.runGh([
|
|
198
|
+
"api",
|
|
199
|
+
`${endpoint}${separator}per_page=${REST_PAGE_SIZE}&page=${page}`,
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
const rawPage = yield* parseJson<T[]>(result.stdout, command, parseFailurePrefix);
|
|
203
|
+
results.push(...rawPage);
|
|
204
|
+
|
|
205
|
+
if (rawPage.length < REST_PAGE_SIZE) {
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
page += 1;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const fetchAllThreadNodes = Effect.fn("pr.fetchAllThreadNodes")(function* (pr: number) {
|
|
214
|
+
const service = yield* GitHubService;
|
|
215
|
+
const repoInfo = yield* service.getRepoInfo();
|
|
216
|
+
|
|
217
|
+
const nodes: ThreadNode[] = [];
|
|
218
|
+
let after: string | null = null;
|
|
219
|
+
|
|
220
|
+
while (true) {
|
|
221
|
+
const response = (yield* service.runGraphQL(REVIEW_THREADS_QUERY, {
|
|
222
|
+
owner: repoInfo.owner,
|
|
223
|
+
name: repoInfo.name,
|
|
224
|
+
pr,
|
|
225
|
+
after,
|
|
226
|
+
})) as ThreadsQueryResult;
|
|
227
|
+
|
|
228
|
+
const page = response.repository.pullRequest.reviewThreads;
|
|
229
|
+
nodes.push(...page.nodes);
|
|
230
|
+
|
|
231
|
+
if (!page.pageInfo.hasNextPage || page.pageInfo.endCursor === null) {
|
|
232
|
+
return nodes;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
after = page.pageInfo.endCursor;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
159
239
|
// ---------------------------------------------------------------------------
|
|
160
240
|
// Handlers
|
|
161
241
|
// ---------------------------------------------------------------------------
|
|
@@ -176,18 +256,8 @@ export const fetchThreads = Effect.fn("pr.fetchThreads")(function* (
|
|
|
176
256
|
pr: number | null,
|
|
177
257
|
unresolvedOnly: boolean,
|
|
178
258
|
) {
|
|
179
|
-
const service = yield* GitHubService;
|
|
180
|
-
const repoInfo = yield* service.getRepoInfo();
|
|
181
|
-
|
|
182
259
|
const resolvedPr = pr ?? (yield* viewPR(null)).number;
|
|
183
|
-
|
|
184
|
-
const response = (yield* service.runGraphQL(REVIEW_THREADS_QUERY, {
|
|
185
|
-
owner: repoInfo.owner,
|
|
186
|
-
name: repoInfo.name,
|
|
187
|
-
pr: resolvedPr,
|
|
188
|
-
})) as ThreadsQueryResult;
|
|
189
|
-
|
|
190
|
-
const threads = response.repository.pullRequest.reviewThreads.nodes;
|
|
260
|
+
const threads = yield* fetchAllThreadNodes(resolvedPr);
|
|
191
261
|
|
|
192
262
|
const mapped: ReviewThread[] = threads
|
|
193
263
|
.map((node) => {
|
|
@@ -223,21 +293,11 @@ export const fetchComments = Effect.fn("pr.fetchComments")(function* (
|
|
|
223
293
|
|
|
224
294
|
const resolvedPr = pr ?? (yield* viewPR(null)).number;
|
|
225
295
|
|
|
226
|
-
const
|
|
227
|
-
"api",
|
|
296
|
+
const raw = yield* fetchAllRestPages<RawReviewComment>(
|
|
228
297
|
`repos/${repoInfo.owner}/${repoInfo.name}/pulls/${resolvedPr}/comments`,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
try: () => JSON.parse(result.stdout) as RawReviewComment[],
|
|
233
|
-
catch: (error) =>
|
|
234
|
-
new GitHubCommandError({
|
|
235
|
-
command: "gh-tool pr comments",
|
|
236
|
-
exitCode: 0,
|
|
237
|
-
stderr: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
|
|
238
|
-
message: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
|
|
239
|
-
}),
|
|
240
|
-
});
|
|
298
|
+
"gh-tool pr comments",
|
|
299
|
+
"Failed to parse response",
|
|
300
|
+
);
|
|
241
301
|
|
|
242
302
|
const comments: ReviewComment[] = raw.map((c) => ({
|
|
243
303
|
id: c.id,
|
|
@@ -272,21 +332,11 @@ export const fetchIssueComments = Effect.fn("pr.fetchIssueComments")(function* (
|
|
|
272
332
|
|
|
273
333
|
const resolvedPr = pr ?? (yield* viewPR(null)).number;
|
|
274
334
|
|
|
275
|
-
const
|
|
276
|
-
"api",
|
|
335
|
+
const raw = yield* fetchAllRestPages<RawIssueComment>(
|
|
277
336
|
`repos/${repoInfo.owner}/${repoInfo.name}/issues/${resolvedPr}/comments`,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
try: () => JSON.parse(result.stdout) as RawIssueComment[],
|
|
282
|
-
catch: (error) =>
|
|
283
|
-
new GitHubCommandError({
|
|
284
|
-
command: "gh-tool pr issue-comments",
|
|
285
|
-
exitCode: 0,
|
|
286
|
-
stderr: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
|
|
287
|
-
message: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
|
|
288
|
-
}),
|
|
289
|
-
});
|
|
337
|
+
"gh-tool pr issue-comments",
|
|
338
|
+
"Failed to parse response",
|
|
339
|
+
);
|
|
290
340
|
|
|
291
341
|
let comments = raw.map(mapRawIssueComment);
|
|
292
342
|
|
|
@@ -388,12 +438,30 @@ export const fetchDiscussionSummary = Effect.fn("pr.fetchDiscussionSummary")(fun
|
|
|
388
438
|
: current,
|
|
389
439
|
);
|
|
390
440
|
|
|
441
|
+
const repliedThreadCommentIds = new Set(
|
|
442
|
+
reviewComments
|
|
443
|
+
.filter((comment) => comment.inReplyToId !== null)
|
|
444
|
+
.map((comment) => comment.inReplyToId),
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const unrepliedReviewThreadsCount = threads.filter(
|
|
448
|
+
(thread) => !repliedThreadCommentIds.has(thread.commentId),
|
|
449
|
+
).length;
|
|
450
|
+
|
|
391
451
|
return {
|
|
392
452
|
issueCommentsCount: issueComments.length,
|
|
393
453
|
latestIssueComment,
|
|
394
454
|
reviewCommentsCount: reviewComments.length,
|
|
395
455
|
reviewThreadsCount: threads.length,
|
|
456
|
+
repliedReviewThreadsCount: threads.length - unrepliedReviewThreadsCount,
|
|
457
|
+
unrepliedReviewThreadsCount,
|
|
458
|
+
resolvedUnrepliedReviewThreadsCount: threads.filter(
|
|
459
|
+
(thread) => thread.isResolved && !repliedThreadCommentIds.has(thread.commentId),
|
|
460
|
+
).length,
|
|
396
461
|
unresolvedReviewThreadsCount: threads.filter((thread) => !thread.isResolved).length,
|
|
462
|
+
unresolvedUnrepliedReviewThreadsCount: threads.filter(
|
|
463
|
+
(thread) => !thread.isResolved && !repliedThreadCommentIds.has(thread.commentId),
|
|
464
|
+
).length,
|
|
397
465
|
};
|
|
398
466
|
});
|
|
399
467
|
|
package/src/gh-tool/service.ts
CHANGED
|
@@ -22,7 +22,7 @@ export class GitHubService extends ServiceMap.Service<
|
|
|
22
22
|
readonly runGhJson: <T>(args: string[]) => Effect.Effect<T, GhError>;
|
|
23
23
|
readonly runGraphQL: (
|
|
24
24
|
query: string,
|
|
25
|
-
variables: Record<string, string | number>,
|
|
25
|
+
variables: Record<string, string | number | null>,
|
|
26
26
|
) => Effect.Effect<unknown, GhError>;
|
|
27
27
|
readonly getRepoInfo: () => Effect.Effect<RepoInfo, GhError>;
|
|
28
28
|
}
|
|
@@ -135,11 +135,15 @@ export class GitHubService extends ServiceMap.Service<
|
|
|
135
135
|
|
|
136
136
|
const runGraphQL = Effect.fn("GitHubService.runGraphQL")(function* (
|
|
137
137
|
query: string,
|
|
138
|
-
variables: Record<string, string | number>,
|
|
138
|
+
variables: Record<string, string | number | null>,
|
|
139
139
|
) {
|
|
140
140
|
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
141
141
|
|
|
142
142
|
for (const [key, value] of Object.entries(variables)) {
|
|
143
|
+
if (value === null) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
143
147
|
if (typeof value === "number") {
|
|
144
148
|
args.push("-F", `${key}=${value}`);
|
|
145
149
|
} else {
|
package/src/shared/index.ts
CHANGED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { deduplicateLines } from "./transform";
|
|
2
|
+
|
|
3
|
+
type LogLevel = "ERROR" | "WARN" | "INFO" | "DEBUG";
|
|
4
|
+
|
|
5
|
+
type ParsedLogLine = {
|
|
6
|
+
level: LogLevel;
|
|
7
|
+
timestamp?: string;
|
|
8
|
+
message: string;
|
|
9
|
+
originalIndex: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const TIMESTAMP_FIELDS = ["timestamp", "ts", "time", "@timestamp", "datetime"] as const;
|
|
13
|
+
const LEVEL_FIELDS = ["level", "severity", "lvl", "log.level"] as const;
|
|
14
|
+
const MESSAGE_FIELDS = ["message", "msg", "log", "text"] as const;
|
|
15
|
+
const ERROR_FIELDS = ["error", "err", "stack", "exception", "error.message"] as const;
|
|
16
|
+
|
|
17
|
+
const LEVEL_ORDER: ReadonlyArray<LogLevel> = ["ERROR", "WARN", "INFO", "DEBUG"];
|
|
18
|
+
|
|
19
|
+
function getByPath(obj: unknown, path: string): unknown {
|
|
20
|
+
if (typeof obj !== "object" || obj === null) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!path.includes(".")) {
|
|
25
|
+
return (obj as Record<string, unknown>)[path];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parts = path.split(".");
|
|
29
|
+
let current: unknown = obj;
|
|
30
|
+
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
if (typeof current !== "object" || current === null) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
current = (current as Record<string, unknown>)[part];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return current;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toText(value: unknown): string | undefined {
|
|
42
|
+
if (typeof value === "string") {
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (value && typeof value === "object") {
|
|
52
|
+
if ("message" in value) {
|
|
53
|
+
const nestedMessage = toText((value as Record<string, unknown>).message);
|
|
54
|
+
if (nestedMessage) {
|
|
55
|
+
return nestedMessage;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatTimestamp(value: unknown): string | undefined {
|
|
70
|
+
if (typeof value === "number" && value > 1_000_000_000_000) {
|
|
71
|
+
return new Date(value).toISOString();
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === "number" && value > 1_000_000_000) {
|
|
74
|
+
return new Date(value * 1000).toISOString();
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function firstMappedField(
|
|
80
|
+
obj: unknown,
|
|
81
|
+
fields: ReadonlyArray<string>,
|
|
82
|
+
isTimestamp = false,
|
|
83
|
+
): string | undefined {
|
|
84
|
+
for (const field of fields) {
|
|
85
|
+
const raw = getByPath(obj, field);
|
|
86
|
+
if (isTimestamp) {
|
|
87
|
+
const formatted = formatTimestamp(raw);
|
|
88
|
+
if (formatted) return formatted;
|
|
89
|
+
}
|
|
90
|
+
const value = toText(raw);
|
|
91
|
+
if (value) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Pino numeric levels: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal
|
|
100
|
+
function normalizeLevel(rawLevel: string | undefined): LogLevel {
|
|
101
|
+
const normalized = rawLevel?.trim().toLowerCase();
|
|
102
|
+
|
|
103
|
+
if (!normalized) {
|
|
104
|
+
return "INFO";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const numericLevel = Number(normalized);
|
|
108
|
+
if (Number.isFinite(numericLevel)) {
|
|
109
|
+
if (numericLevel >= 50) return "ERROR";
|
|
110
|
+
if (numericLevel >= 40) return "WARN";
|
|
111
|
+
if (numericLevel >= 30) return "INFO";
|
|
112
|
+
return "DEBUG";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (normalized === "error" || normalized === "err" || normalized === "fatal") {
|
|
116
|
+
return "ERROR";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (normalized === "warn" || normalized === "warning") {
|
|
120
|
+
return "WARN";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (normalized === "info") {
|
|
124
|
+
return "INFO";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (normalized === "debug" || normalized === "trace") {
|
|
128
|
+
return "DEBUG";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return "INFO";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatLine({ level, timestamp, message }: ParsedLogLine): string {
|
|
135
|
+
if (timestamp) {
|
|
136
|
+
return `[${timestamp}] [${level}] ${message}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `[${level}] ${message}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseJsonLogLine(line: string, originalIndex: number): ParsedLogLine {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(line) as unknown;
|
|
145
|
+
const timestamp = firstMappedField(parsed, TIMESTAMP_FIELDS, true);
|
|
146
|
+
const level = normalizeLevel(firstMappedField(parsed, LEVEL_FIELDS));
|
|
147
|
+
const message =
|
|
148
|
+
firstMappedField(parsed, MESSAGE_FIELDS) ??
|
|
149
|
+
firstMappedField(parsed, ERROR_FIELDS) ??
|
|
150
|
+
line.trim();
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
level,
|
|
154
|
+
timestamp,
|
|
155
|
+
message,
|
|
156
|
+
originalIndex,
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
return {
|
|
160
|
+
level: "INFO",
|
|
161
|
+
message: line.trim(),
|
|
162
|
+
originalIndex,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isJsonMode(lines: ReadonlyArray<string>): boolean {
|
|
168
|
+
const firstNonEmpty = lines.find((line) => line.trim().length > 0);
|
|
169
|
+
if (!firstNonEmpty) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(firstNonEmpty) as unknown;
|
|
175
|
+
return typeof parsed === "object" && parsed !== null;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function groupHeader(level: LogLevel, count: number): string {
|
|
182
|
+
switch (level) {
|
|
183
|
+
case "ERROR":
|
|
184
|
+
return `--- errors (${count}) ---`;
|
|
185
|
+
case "WARN":
|
|
186
|
+
return `--- warnings (${count}) ---`;
|
|
187
|
+
case "INFO":
|
|
188
|
+
return `--- info (${count}) ---`;
|
|
189
|
+
case "DEBUG":
|
|
190
|
+
return `--- debug (${count}) ---`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function transformJsonLines(lines: ReadonlyArray<string>): string {
|
|
195
|
+
const parsedLines = lines
|
|
196
|
+
.map((line, originalIndex) => ({ line, originalIndex }))
|
|
197
|
+
.filter(({ line }) => line.trim().length > 0)
|
|
198
|
+
.map(({ line, originalIndex }) => parseJsonLogLine(line, originalIndex));
|
|
199
|
+
|
|
200
|
+
if (parsedLines.length === 0) {
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const ordered = LEVEL_ORDER.flatMap((level) =>
|
|
205
|
+
parsedLines
|
|
206
|
+
.filter((entry) => entry.level === level)
|
|
207
|
+
.sort((a, b) => a.originalIndex - b.originalIndex),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const output: Array<string> = [];
|
|
211
|
+
|
|
212
|
+
for (const level of LEVEL_ORDER) {
|
|
213
|
+
const group = ordered.filter((entry) => entry.level === level);
|
|
214
|
+
if (group.length === 0) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
output.push(groupHeader(level, group.length));
|
|
219
|
+
output.push(...group.map(formatLine));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return deduplicateLines(output.join("\n"));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function transformLogOutput(rawOutput: string): string {
|
|
226
|
+
if (rawOutput.length === 0) {
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const lines = rawOutput.split(/\r?\n/);
|
|
231
|
+
|
|
232
|
+
if (isJsonMode(lines)) {
|
|
233
|
+
return transformJsonLines(lines);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return deduplicateLines(rawOutput, {
|
|
237
|
+
normalizeTimestamps: true,
|
|
238
|
+
normalizeUUIDs: true,
|
|
239
|
+
});
|
|
240
|
+
}
|