@lark-apaas/fullstack-cli 1.1.8 → 1.1.9-alpha.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/dist/commands/gen-db-schema.js +4 -4
- package/dist/commands/read-logs/client-std.d.ts +2 -0
- package/dist/commands/read-logs/client-std.js +98 -0
- package/dist/commands/read-logs/json-lines.d.ts +3 -0
- package/dist/commands/read-logs/json-lines.js +217 -0
- package/dist/commands/read-logs/server-std.d.ts +1 -0
- package/dist/commands/read-logs/server-std.js +25 -0
- package/dist/commands/read-logs/std-utils.d.ts +5 -0
- package/dist/commands/read-logs/std-utils.js +61 -0
- package/dist/commands/read-logs/tail.d.ts +2 -0
- package/dist/commands/read-logs/tail.js +47 -0
- package/dist/commands/read-logs.d.ts +8 -1
- package/dist/commands/read-logs.js +112 -327
- package/dist/commands/read-logs.test.js +131 -1
- package/package.json +1 -1
|
@@ -73,16 +73,16 @@ export async function run(options = {}) {
|
|
|
73
73
|
console.error('[gen-db-schema] schema.ts not generated');
|
|
74
74
|
throw new Error('drizzle-kit introspect failed to generate schema.ts');
|
|
75
75
|
}
|
|
76
|
-
fs.mkdirSync(path.dirname(SCHEMA_FILE), { recursive: true });
|
|
77
|
-
fs.copyFileSync(generatedSchema, SCHEMA_FILE);
|
|
78
|
-
console.log(`[gen-db-schema] ✓ Copied to ${outputPath}`);
|
|
79
76
|
// 后处理 schema(使用 CommonJS require 方式加载)
|
|
80
77
|
const { postprocessDrizzleSchema } = require('@lark-apaas/devtool-kits');
|
|
81
|
-
const stats = postprocessDrizzleSchema(
|
|
78
|
+
const stats = postprocessDrizzleSchema(generatedSchema);
|
|
82
79
|
if (stats?.unmatchedUnknown?.length) {
|
|
83
80
|
console.warn('[gen-db-schema] Unmatched custom types detected:', stats.unmatchedUnknown);
|
|
84
81
|
}
|
|
85
82
|
console.log('[gen-db-schema] ✓ Postprocessed schema');
|
|
83
|
+
fs.mkdirSync(path.dirname(SCHEMA_FILE), { recursive: true });
|
|
84
|
+
fs.copyFileSync(generatedSchema, SCHEMA_FILE);
|
|
85
|
+
console.log(`[gen-db-schema] ✓ Copied to ${outputPath}`);
|
|
86
86
|
try {
|
|
87
87
|
if (options.enableNestModuleGenerate) {
|
|
88
88
|
const { parseAndGenerateNestResourceTemplate } = require('@lark-apaas/devtool-kits');
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFileTailLines } from './tail.js';
|
|
2
|
+
import { stripPrefixFromStdLine } from './std-utils.js';
|
|
3
|
+
export function readClientStdSegment(filePath, maxLines) {
|
|
4
|
+
const lines = readFileTailLines(filePath, Math.max(maxLines * 5, 2000));
|
|
5
|
+
return extractClientStdSegment(lines, maxLines);
|
|
6
|
+
}
|
|
7
|
+
export function extractClientStdSegment(lines, maxLines) {
|
|
8
|
+
const bodyLines = lines.map(stripPrefixFromStdLine);
|
|
9
|
+
const hotStartMarkers = [
|
|
10
|
+
/file change detected\..*incremental compilation/i,
|
|
11
|
+
/starting incremental compilation/i,
|
|
12
|
+
/starting compilation/i,
|
|
13
|
+
/\bcompiling\b/i,
|
|
14
|
+
/\brecompil/i,
|
|
15
|
+
];
|
|
16
|
+
const hotEndMarkers = [
|
|
17
|
+
/file change detected\..*incremental compilation/i,
|
|
18
|
+
/\bwebpack compiled\b/i,
|
|
19
|
+
/compiled successfully/i,
|
|
20
|
+
/compiled with warnings/i,
|
|
21
|
+
/compiled with errors/i,
|
|
22
|
+
/failed to compile/i,
|
|
23
|
+
/fast refresh/i,
|
|
24
|
+
/\bhmr\b/i,
|
|
25
|
+
/hot update/i,
|
|
26
|
+
/\bhot reload\b/i,
|
|
27
|
+
/\bhmr update\b/i,
|
|
28
|
+
];
|
|
29
|
+
const compiledMarkers = [
|
|
30
|
+
/\bwebpack compiled\b/i,
|
|
31
|
+
/compiled successfully/i,
|
|
32
|
+
/compiled with warnings/i,
|
|
33
|
+
/compiled with errors/i,
|
|
34
|
+
/failed to compile/i,
|
|
35
|
+
];
|
|
36
|
+
let startIndex = -1;
|
|
37
|
+
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
38
|
+
const line = bodyLines[i];
|
|
39
|
+
if (!line)
|
|
40
|
+
continue;
|
|
41
|
+
if (hotStartMarkers.some((re) => re.test(line))) {
|
|
42
|
+
startIndex = i;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (startIndex === -1) {
|
|
47
|
+
let pivotIndex = -1;
|
|
48
|
+
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
49
|
+
const line = bodyLines[i];
|
|
50
|
+
if (!line)
|
|
51
|
+
continue;
|
|
52
|
+
if (hotEndMarkers.some((re) => re.test(line))) {
|
|
53
|
+
pivotIndex = i;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (pivotIndex !== -1) {
|
|
58
|
+
if (compiledMarkers.some((re) => re.test(bodyLines[pivotIndex] ?? ''))) {
|
|
59
|
+
startIndex = pivotIndex;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const searchLimit = 80;
|
|
63
|
+
const from = Math.max(0, pivotIndex - searchLimit);
|
|
64
|
+
for (let i = pivotIndex; i >= from; i -= 1) {
|
|
65
|
+
const line = bodyLines[i];
|
|
66
|
+
if (!line)
|
|
67
|
+
continue;
|
|
68
|
+
if (compiledMarkers.some((re) => re.test(line))) {
|
|
69
|
+
startIndex = i;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (startIndex === -1) {
|
|
74
|
+
startIndex = pivotIndex;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (startIndex === -1) {
|
|
80
|
+
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
81
|
+
const line = bodyLines[i];
|
|
82
|
+
if (!line)
|
|
83
|
+
continue;
|
|
84
|
+
if (/\bdev:client\b/.test(line)) {
|
|
85
|
+
startIndex = i;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const segment = startIndex === -1 ? bodyLines : bodyLines.slice(startIndex);
|
|
91
|
+
if (segment.length === 0) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
if (segment.length <= maxLines) {
|
|
95
|
+
return segment;
|
|
96
|
+
}
|
|
97
|
+
return segment.slice(segment.length - maxLines);
|
|
98
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function readJsonLinesLastPid(filePath: string, maxLines: number): string[];
|
|
2
|
+
export declare function readJsonLinesByTraceId(filePath: string, traceId: string, maxLines: number): string[];
|
|
3
|
+
export declare function readJsonLinesTail(lines: string[], maxLines: number): string[];
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
function normalizePid(value) {
|
|
3
|
+
if (typeof value === 'number') {
|
|
4
|
+
return String(value);
|
|
5
|
+
}
|
|
6
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
return 'unknown';
|
|
10
|
+
}
|
|
11
|
+
export function readJsonLinesLastPid(filePath, maxLines) {
|
|
12
|
+
const stat = fs.statSync(filePath);
|
|
13
|
+
if (stat.size === 0) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const fd = fs.openSync(filePath, 'r');
|
|
17
|
+
const chunkSize = 64 * 1024;
|
|
18
|
+
let position = stat.size;
|
|
19
|
+
let remainder = '';
|
|
20
|
+
let targetPid = null;
|
|
21
|
+
let finished = false;
|
|
22
|
+
const collected = [];
|
|
23
|
+
try {
|
|
24
|
+
while (position > 0 && !finished) {
|
|
25
|
+
const length = Math.min(chunkSize, position);
|
|
26
|
+
position -= length;
|
|
27
|
+
const buffer = Buffer.alloc(length);
|
|
28
|
+
fs.readSync(fd, buffer, 0, length, position);
|
|
29
|
+
let chunk = buffer.toString('utf8');
|
|
30
|
+
if (remainder) {
|
|
31
|
+
chunk += remainder;
|
|
32
|
+
remainder = '';
|
|
33
|
+
}
|
|
34
|
+
const parts = chunk.split('\n');
|
|
35
|
+
remainder = parts.shift() ?? '';
|
|
36
|
+
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
|
37
|
+
const line = parts[i].trim();
|
|
38
|
+
if (!line)
|
|
39
|
+
continue;
|
|
40
|
+
let parsed = null;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(line);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const pid = normalizePid(parsed?.pid);
|
|
48
|
+
if (targetPid === null) {
|
|
49
|
+
targetPid = pid;
|
|
50
|
+
}
|
|
51
|
+
if (pid !== targetPid) {
|
|
52
|
+
finished = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
collected.push(line);
|
|
56
|
+
if (collected.length >= maxLines * 5) {
|
|
57
|
+
finished = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!finished && remainder) {
|
|
63
|
+
const line = remainder.trim();
|
|
64
|
+
if (line) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(line);
|
|
67
|
+
const pid = normalizePid(parsed?.pid);
|
|
68
|
+
if (targetPid === null) {
|
|
69
|
+
targetPid = pid;
|
|
70
|
+
}
|
|
71
|
+
if (pid === targetPid) {
|
|
72
|
+
collected.push(line);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
fs.closeSync(fd);
|
|
83
|
+
}
|
|
84
|
+
const reversed = collected.reverse();
|
|
85
|
+
if (reversed.length <= maxLines) {
|
|
86
|
+
return reversed;
|
|
87
|
+
}
|
|
88
|
+
return reversed.slice(reversed.length - maxLines);
|
|
89
|
+
}
|
|
90
|
+
function normalizeTraceId(value) {
|
|
91
|
+
if (typeof value === 'string') {
|
|
92
|
+
const trimmed = value.trim();
|
|
93
|
+
return trimmed ? trimmed : null;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
96
|
+
return String(value);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function extractTraceId(obj) {
|
|
101
|
+
if (!obj || typeof obj !== 'object')
|
|
102
|
+
return null;
|
|
103
|
+
const record = obj;
|
|
104
|
+
const directKeys = ['trace_id', 'traceId', 'traceID', 'traceid'];
|
|
105
|
+
for (const key of directKeys) {
|
|
106
|
+
const value = normalizeTraceId(record[key]);
|
|
107
|
+
if (value)
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
const meta = record['meta'];
|
|
111
|
+
if (meta && typeof meta === 'object') {
|
|
112
|
+
const metaRecord = meta;
|
|
113
|
+
for (const key of directKeys) {
|
|
114
|
+
const value = normalizeTraceId(metaRecord[key]);
|
|
115
|
+
if (value)
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const attributes = record['attributes'];
|
|
120
|
+
if (attributes && typeof attributes === 'object') {
|
|
121
|
+
const attrRecord = attributes;
|
|
122
|
+
for (const key of ['traceID', 'trace_id', 'traceId', 'traceid']) {
|
|
123
|
+
const value = normalizeTraceId(attrRecord[key]);
|
|
124
|
+
if (value)
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
export function readJsonLinesByTraceId(filePath, traceId, maxLines) {
|
|
131
|
+
const wanted = traceId.trim();
|
|
132
|
+
if (!wanted)
|
|
133
|
+
return [];
|
|
134
|
+
const stat = fs.statSync(filePath);
|
|
135
|
+
if (stat.size === 0) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const fd = fs.openSync(filePath, 'r');
|
|
139
|
+
const chunkSize = 64 * 1024;
|
|
140
|
+
let position = stat.size;
|
|
141
|
+
let remainder = '';
|
|
142
|
+
let processedNonEmpty = 0;
|
|
143
|
+
let finished = false;
|
|
144
|
+
const collected = [];
|
|
145
|
+
const maxProcessed = Math.max(maxLines * 200, 5000);
|
|
146
|
+
try {
|
|
147
|
+
while (position > 0 && !finished) {
|
|
148
|
+
const length = Math.min(chunkSize, position);
|
|
149
|
+
position -= length;
|
|
150
|
+
const buffer = Buffer.alloc(length);
|
|
151
|
+
fs.readSync(fd, buffer, 0, length, position);
|
|
152
|
+
let chunk = buffer.toString('utf8');
|
|
153
|
+
if (remainder) {
|
|
154
|
+
chunk += remainder;
|
|
155
|
+
remainder = '';
|
|
156
|
+
}
|
|
157
|
+
const parts = chunk.split('\n');
|
|
158
|
+
remainder = parts.shift() ?? '';
|
|
159
|
+
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
|
160
|
+
const line = parts[i].trim();
|
|
161
|
+
if (!line)
|
|
162
|
+
continue;
|
|
163
|
+
processedNonEmpty += 1;
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(line);
|
|
166
|
+
const lineTraceId = extractTraceId(parsed);
|
|
167
|
+
if (lineTraceId === wanted) {
|
|
168
|
+
collected.push(line);
|
|
169
|
+
if (collected.length >= maxLines) {
|
|
170
|
+
finished = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (processedNonEmpty >= maxProcessed && collected.length > 0) {
|
|
179
|
+
finished = true;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!finished && remainder) {
|
|
185
|
+
const line = remainder.trim();
|
|
186
|
+
if (line) {
|
|
187
|
+
try {
|
|
188
|
+
const parsed = JSON.parse(line);
|
|
189
|
+
const lineTraceId = extractTraceId(parsed);
|
|
190
|
+
if (lineTraceId === wanted) {
|
|
191
|
+
collected.push(line);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
fs.closeSync(fd);
|
|
202
|
+
}
|
|
203
|
+
return collected.reverse();
|
|
204
|
+
}
|
|
205
|
+
export function readJsonLinesTail(lines, maxLines) {
|
|
206
|
+
const result = [];
|
|
207
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
208
|
+
const line = lines[i].trim();
|
|
209
|
+
if (!line)
|
|
210
|
+
continue;
|
|
211
|
+
result.push(line);
|
|
212
|
+
if (result.length >= maxLines) {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return result.reverse();
|
|
217
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readServerStdSegment(filePath: string, maxLines: number): string[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileTailLines } from './tail.js';
|
|
2
|
+
import { readStdLinesTailFromLastMarker, stripPrefixFromStdLine } from './std-utils.js';
|
|
3
|
+
export function readServerStdSegment(filePath, maxLines) {
|
|
4
|
+
const marker = (line) => {
|
|
5
|
+
if (!line)
|
|
6
|
+
return false;
|
|
7
|
+
if (/\bdev:server\b/.test(line))
|
|
8
|
+
return true;
|
|
9
|
+
if (line.includes('Starting compilation in watch mode'))
|
|
10
|
+
return true;
|
|
11
|
+
if (line.includes('File change detected. Starting incremental compilation'))
|
|
12
|
+
return true;
|
|
13
|
+
if (line.includes('Starting Nest application'))
|
|
14
|
+
return true;
|
|
15
|
+
if (line.includes('Nest application successfully started'))
|
|
16
|
+
return true;
|
|
17
|
+
return false;
|
|
18
|
+
};
|
|
19
|
+
const segment = readStdLinesTailFromLastMarker(filePath, maxLines, marker);
|
|
20
|
+
if (segment.markerFound) {
|
|
21
|
+
return segment.lines;
|
|
22
|
+
}
|
|
23
|
+
const lines = readFileTailLines(filePath, maxLines);
|
|
24
|
+
return lines.map(stripPrefixFromStdLine);
|
|
25
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
export function stripPrefixFromStdLine(line) {
|
|
3
|
+
const match = line.match(/^(\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(server|client)\] )(.*)$/);
|
|
4
|
+
if (!match) {
|
|
5
|
+
return line;
|
|
6
|
+
}
|
|
7
|
+
return match[4] || '';
|
|
8
|
+
}
|
|
9
|
+
export function readStdLinesTailFromLastMarker(filePath, maxLines, isMarker) {
|
|
10
|
+
const stat = fs.statSync(filePath);
|
|
11
|
+
if (stat.size === 0) {
|
|
12
|
+
return { lines: [], markerFound: false };
|
|
13
|
+
}
|
|
14
|
+
const fd = fs.openSync(filePath, 'r');
|
|
15
|
+
const chunkSize = 64 * 1024;
|
|
16
|
+
let position = stat.size;
|
|
17
|
+
let remainder = '';
|
|
18
|
+
let markerFound = false;
|
|
19
|
+
let finished = false;
|
|
20
|
+
const collected = [];
|
|
21
|
+
try {
|
|
22
|
+
while (position > 0 && !finished) {
|
|
23
|
+
const length = Math.min(chunkSize, position);
|
|
24
|
+
position -= length;
|
|
25
|
+
const buffer = Buffer.alloc(length);
|
|
26
|
+
fs.readSync(fd, buffer, 0, length, position);
|
|
27
|
+
let chunk = buffer.toString('utf8');
|
|
28
|
+
if (remainder) {
|
|
29
|
+
chunk += remainder;
|
|
30
|
+
remainder = '';
|
|
31
|
+
}
|
|
32
|
+
const parts = chunk.split('\n');
|
|
33
|
+
remainder = parts.shift() ?? '';
|
|
34
|
+
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
|
35
|
+
const rawLine = parts[i];
|
|
36
|
+
const line = stripPrefixFromStdLine(rawLine);
|
|
37
|
+
if (collected.length < maxLines) {
|
|
38
|
+
collected.push(line);
|
|
39
|
+
}
|
|
40
|
+
if (isMarker(line)) {
|
|
41
|
+
markerFound = true;
|
|
42
|
+
finished = true;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!finished && remainder) {
|
|
48
|
+
const line = stripPrefixFromStdLine(remainder);
|
|
49
|
+
if (collected.length < maxLines) {
|
|
50
|
+
collected.push(line);
|
|
51
|
+
}
|
|
52
|
+
if (isMarker(line)) {
|
|
53
|
+
markerFound = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
fs.closeSync(fd);
|
|
59
|
+
}
|
|
60
|
+
return { lines: collected.reverse(), markerFound };
|
|
61
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
export function fileExists(filePath) {
|
|
3
|
+
try {
|
|
4
|
+
fs.accessSync(filePath, fs.constants.F_OK | fs.constants.R_OK);
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function readFileTailLines(filePath, maxLines) {
|
|
12
|
+
const stat = fs.statSync(filePath);
|
|
13
|
+
if (stat.size === 0) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const fd = fs.openSync(filePath, 'r');
|
|
17
|
+
const chunkSize = 64 * 1024;
|
|
18
|
+
const chunks = [];
|
|
19
|
+
let position = stat.size;
|
|
20
|
+
let collectedLines = 0;
|
|
21
|
+
try {
|
|
22
|
+
while (position > 0 && collectedLines <= maxLines) {
|
|
23
|
+
const length = Math.min(chunkSize, position);
|
|
24
|
+
position -= length;
|
|
25
|
+
const buffer = Buffer.alloc(length);
|
|
26
|
+
fs.readSync(fd, buffer, 0, length, position);
|
|
27
|
+
chunks.unshift(buffer.toString('utf8'));
|
|
28
|
+
const chunkLines = buffer.toString('utf8').split('\n').length - 1;
|
|
29
|
+
collectedLines += chunkLines;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
fs.closeSync(fd);
|
|
34
|
+
}
|
|
35
|
+
const content = chunks.join('');
|
|
36
|
+
const allLines = content.split('\n');
|
|
37
|
+
if (allLines.length === 0) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
if (allLines[allLines.length - 1] === '') {
|
|
41
|
+
allLines.pop();
|
|
42
|
+
}
|
|
43
|
+
if (allLines.length <= maxLines) {
|
|
44
|
+
return allLines;
|
|
45
|
+
}
|
|
46
|
+
return allLines.slice(allLines.length - maxLines);
|
|
47
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
type LogType = 'server' | 'trace' | 'server-std' | 'client-std' | 'browser'
|
|
1
|
+
type LogType = 'server' | 'trace' | 'server-std' | 'client-std' | 'browser';
|
|
2
|
+
export interface ReadLogsJsonResult {
|
|
3
|
+
hasError: boolean;
|
|
4
|
+
message: string;
|
|
5
|
+
logs?: unknown[];
|
|
6
|
+
}
|
|
2
7
|
interface ReadLogsOptions {
|
|
3
8
|
logDir: string;
|
|
4
9
|
type: LogType;
|
|
5
10
|
maxLines?: number;
|
|
11
|
+
traceId?: string;
|
|
6
12
|
}
|
|
7
13
|
export declare function readLatestLogLines(options: ReadLogsOptions): Promise<string[]>;
|
|
14
|
+
export declare function readLogsJsonResult(options: ReadLogsOptions): Promise<ReadLogsJsonResult>;
|
|
8
15
|
export declare function run(options: ReadLogsOptions): Promise<void>;
|
|
9
16
|
export {};
|
|
@@ -1,26 +1,125 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
2
|
+
import { readServerStdSegment } from './read-logs/server-std.js';
|
|
3
|
+
import { readClientStdSegment } from './read-logs/client-std.js';
|
|
4
|
+
import { readJsonLinesByTraceId, readJsonLinesLastPid, readJsonLinesTail } from './read-logs/json-lines.js';
|
|
5
|
+
import { fileExists, readFileTailLines } from './read-logs/tail.js';
|
|
6
|
+
function hasErrorInStdLines(lines) {
|
|
7
|
+
const combined = lines.join('\n');
|
|
8
|
+
if (!combined)
|
|
9
|
+
return false;
|
|
10
|
+
const strong = [
|
|
11
|
+
/compiled with errors/i,
|
|
12
|
+
/failed to compile/i,
|
|
13
|
+
/error: \w+/i,
|
|
14
|
+
/uncaught/i,
|
|
15
|
+
/unhandled/i,
|
|
16
|
+
/eaddrinuse/i,
|
|
17
|
+
/cannot find module/i,
|
|
18
|
+
/module not found/i,
|
|
19
|
+
/ts\d{3,5}:/i,
|
|
20
|
+
];
|
|
21
|
+
if (strong.some((re) => re.test(combined)))
|
|
22
|
+
return true;
|
|
23
|
+
const weakLine = /\b(error|fatal|exception)\b/i;
|
|
24
|
+
const ignorePatterns = [
|
|
25
|
+
/\b0\s+errors?\b/i,
|
|
26
|
+
/Server Error \d{3}/i,
|
|
27
|
+
];
|
|
28
|
+
return lines.some((line) => {
|
|
29
|
+
const text = line.trim();
|
|
30
|
+
if (!text)
|
|
31
|
+
return false;
|
|
32
|
+
if (ignorePatterns.some((re) => re.test(text)))
|
|
33
|
+
return false;
|
|
34
|
+
return weakLine.test(text);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function hasErrorInLogObject(value) {
|
|
38
|
+
if (!value || typeof value !== 'object')
|
|
39
|
+
return false;
|
|
40
|
+
const obj = value;
|
|
41
|
+
const level = typeof obj.level === 'string' ? obj.level.toLowerCase() : '';
|
|
42
|
+
if (level === 'error' || level === 'fatal')
|
|
43
|
+
return true;
|
|
44
|
+
if (level === 'err')
|
|
45
|
+
return true;
|
|
46
|
+
if (level === 'warn' && typeof obj.stack === 'string' && obj.stack.length > 0)
|
|
47
|
+
return true;
|
|
48
|
+
if (typeof obj.stack === 'string' && obj.stack.length > 0)
|
|
49
|
+
return true;
|
|
50
|
+
const statusCode = obj.statusCode;
|
|
51
|
+
const status_code = obj.status_code;
|
|
52
|
+
const meta = obj.meta;
|
|
53
|
+
const metaObj = meta && typeof meta === 'object' ? meta : null;
|
|
54
|
+
const metaStatusCode = metaObj?.statusCode;
|
|
55
|
+
const metaStatus_code = metaObj?.status_code;
|
|
56
|
+
const candidates = [statusCode, status_code, metaStatusCode, metaStatus_code];
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate >= 400) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const message = typeof obj.message === 'string' ? obj.message : '';
|
|
63
|
+
if (message && hasErrorInStdLines([message]))
|
|
64
|
+
return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
3
67
|
export async function readLatestLogLines(options) {
|
|
4
68
|
const maxLines = options.maxLines ?? 200;
|
|
5
69
|
const filePath = resolveLogFilePath(options.logDir, options.type);
|
|
6
70
|
if (!fileExists(filePath)) {
|
|
7
71
|
throw new Error(`Log file not found: ${filePath}`);
|
|
8
72
|
}
|
|
9
|
-
let lines = readFileTailLines(filePath, maxLines * 5);
|
|
10
73
|
if (options.type === 'server-std') {
|
|
11
|
-
|
|
74
|
+
return readServerStdSegment(filePath, maxLines);
|
|
75
|
+
}
|
|
76
|
+
if (options.type === 'client-std') {
|
|
77
|
+
return readClientStdSegment(filePath, maxLines);
|
|
12
78
|
}
|
|
13
|
-
|
|
14
|
-
|
|
79
|
+
const traceId = typeof options.traceId === 'string' ? options.traceId.trim() : '';
|
|
80
|
+
if (traceId) {
|
|
81
|
+
return readJsonLinesByTraceId(filePath, traceId, maxLines);
|
|
15
82
|
}
|
|
16
|
-
|
|
83
|
+
let lines = readFileTailLines(filePath, maxLines * 5);
|
|
84
|
+
if (options.type === 'server' || options.type === 'trace') {
|
|
17
85
|
lines = readJsonLinesLastPid(filePath, maxLines);
|
|
18
86
|
}
|
|
19
|
-
else if (options.type === 'browser'
|
|
20
|
-
lines = readJsonLinesTail(
|
|
87
|
+
else if (options.type === 'browser') {
|
|
88
|
+
lines = readJsonLinesTail(lines, maxLines);
|
|
21
89
|
}
|
|
22
90
|
return lines;
|
|
23
91
|
}
|
|
92
|
+
export async function readLogsJsonResult(options) {
|
|
93
|
+
const lines = await readLatestLogLines(options);
|
|
94
|
+
if (options.type === 'server-std' || options.type === 'client-std') {
|
|
95
|
+
return {
|
|
96
|
+
hasError: hasErrorInStdLines(lines),
|
|
97
|
+
message: lines.join('\n'),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const logs = [];
|
|
101
|
+
let hasError = false;
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (!hasError && hasErrorInStdLines([line])) {
|
|
104
|
+
hasError = true;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(line);
|
|
108
|
+
logs.push(parsed);
|
|
109
|
+
if (!hasError && hasErrorInLogObject(parsed)) {
|
|
110
|
+
hasError = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
hasError,
|
|
119
|
+
message: '',
|
|
120
|
+
logs,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
24
123
|
function resolveLogFilePath(logDir, type) {
|
|
25
124
|
const base = path.isAbsolute(logDir) ? logDir : path.join(process.cwd(), logDir);
|
|
26
125
|
if (type === 'server') {
|
|
@@ -38,331 +137,17 @@ function resolveLogFilePath(logDir, type) {
|
|
|
38
137
|
if (type === 'browser') {
|
|
39
138
|
return path.join(base, 'browser.log');
|
|
40
139
|
}
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
function fileExists(filePath) {
|
|
44
|
-
try {
|
|
45
|
-
fs.accessSync(filePath, fs.constants.F_OK | fs.constants.R_OK);
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function readFileTailLines(filePath, maxLines) {
|
|
53
|
-
const stat = fs.statSync(filePath);
|
|
54
|
-
if (stat.size === 0) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const fd = fs.openSync(filePath, 'r');
|
|
58
|
-
const chunkSize = 64 * 1024;
|
|
59
|
-
const chunks = [];
|
|
60
|
-
let position = stat.size;
|
|
61
|
-
let collectedLines = 0;
|
|
62
|
-
try {
|
|
63
|
-
while (position > 0 && collectedLines <= maxLines) {
|
|
64
|
-
const length = Math.min(chunkSize, position);
|
|
65
|
-
const buffer = Buffer.alloc(length);
|
|
66
|
-
position -= length;
|
|
67
|
-
fs.readSync(fd, buffer, 0, length, position);
|
|
68
|
-
chunks.unshift(buffer.toString('utf8'));
|
|
69
|
-
const chunkLines = buffer.toString('utf8').split('\n').length - 1;
|
|
70
|
-
collectedLines += chunkLines;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
finally {
|
|
74
|
-
fs.closeSync(fd);
|
|
75
|
-
}
|
|
76
|
-
const content = chunks.join('');
|
|
77
|
-
const allLines = content.split('\n');
|
|
78
|
-
if (allLines.length === 0) {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
if (allLines[allLines.length - 1] === '') {
|
|
82
|
-
allLines.pop();
|
|
83
|
-
}
|
|
84
|
-
if (allLines.length <= maxLines) {
|
|
85
|
-
return allLines;
|
|
86
|
-
}
|
|
87
|
-
return allLines.slice(allLines.length - maxLines);
|
|
88
|
-
}
|
|
89
|
-
function stripPrefixFromStdLine(line) {
|
|
90
|
-
const match = line.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(server|client)\] (.*)$/);
|
|
91
|
-
if (!match) {
|
|
92
|
-
return line;
|
|
93
|
-
}
|
|
94
|
-
return match[3] || '';
|
|
95
|
-
}
|
|
96
|
-
function findLastSegmentByMarkers(lines, markers) {
|
|
97
|
-
let startIndex = -1;
|
|
98
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
99
|
-
const line = lines[i];
|
|
100
|
-
if (markers.start(line)) {
|
|
101
|
-
startIndex = i;
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (startIndex === -1) {
|
|
106
|
-
return lines;
|
|
107
|
-
}
|
|
108
|
-
let endIndex = lines.length;
|
|
109
|
-
if (markers.reset) {
|
|
110
|
-
for (let i = startIndex + 1; i < lines.length; i += 1) {
|
|
111
|
-
if (markers.reset(lines[i])) {
|
|
112
|
-
endIndex = i;
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return lines.slice(startIndex, endIndex);
|
|
118
|
-
}
|
|
119
|
-
function extractServerStdSegment(lines, maxLines) {
|
|
120
|
-
const bodyLines = lines.map(stripPrefixFromStdLine);
|
|
121
|
-
let startIndex = -1;
|
|
122
|
-
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
123
|
-
const line = bodyLines[i];
|
|
124
|
-
if (!line)
|
|
125
|
-
continue;
|
|
126
|
-
if (/\bdev:server\b/.test(line)) {
|
|
127
|
-
startIndex = i;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (startIndex !== -1) {
|
|
132
|
-
const segment = bodyLines.slice(startIndex);
|
|
133
|
-
if (segment.length <= maxLines) {
|
|
134
|
-
return segment;
|
|
135
|
-
}
|
|
136
|
-
return segment.slice(segment.length - maxLines);
|
|
137
|
-
}
|
|
138
|
-
const markers = {
|
|
139
|
-
start: (line) => {
|
|
140
|
-
if (!line)
|
|
141
|
-
return false;
|
|
142
|
-
if (line.includes('Starting compilation in watch mode'))
|
|
143
|
-
return true;
|
|
144
|
-
if (line.includes('File change detected. Starting incremental compilation'))
|
|
145
|
-
return true;
|
|
146
|
-
if (line.includes('Starting Nest application'))
|
|
147
|
-
return true;
|
|
148
|
-
if (line.includes('Nest application successfully started'))
|
|
149
|
-
return true;
|
|
150
|
-
return false;
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
const segment = findLastSegmentByMarkers(bodyLines, markers);
|
|
154
|
-
if (segment.length === 0) {
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
157
|
-
if (segment.length <= maxLines) {
|
|
158
|
-
return segment;
|
|
159
|
-
}
|
|
160
|
-
return segment.slice(segment.length - maxLines);
|
|
161
|
-
}
|
|
162
|
-
function extractClientStdSegment(lines, maxLines) {
|
|
163
|
-
const bodyLines = lines.map(stripPrefixFromStdLine);
|
|
164
|
-
const hotStartMarkers = [
|
|
165
|
-
/file change detected\..*incremental compilation/i,
|
|
166
|
-
/starting incremental compilation/i,
|
|
167
|
-
/starting compilation/i,
|
|
168
|
-
/\bcompiling\b/i,
|
|
169
|
-
/\brecompil/i,
|
|
170
|
-
];
|
|
171
|
-
const hotEndMarkers = [
|
|
172
|
-
/file change detected\..*incremental compilation/i,
|
|
173
|
-
/\bwebpack compiled\b/i,
|
|
174
|
-
/compiled successfully/i,
|
|
175
|
-
/compiled with warnings/i,
|
|
176
|
-
/compiled with errors/i,
|
|
177
|
-
/failed to compile/i,
|
|
178
|
-
/fast refresh/i,
|
|
179
|
-
/\bhmr\b/i,
|
|
180
|
-
/hot update/i,
|
|
181
|
-
/\bhot reload\b/i,
|
|
182
|
-
/\bhmr update\b/i,
|
|
183
|
-
];
|
|
184
|
-
const compiledMarkers = [
|
|
185
|
-
/\bwebpack compiled\b/i,
|
|
186
|
-
/compiled successfully/i,
|
|
187
|
-
/compiled with warnings/i,
|
|
188
|
-
/compiled with errors/i,
|
|
189
|
-
/failed to compile/i,
|
|
190
|
-
];
|
|
191
|
-
let startIndex = -1;
|
|
192
|
-
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
193
|
-
const line = bodyLines[i];
|
|
194
|
-
if (!line)
|
|
195
|
-
continue;
|
|
196
|
-
if (hotStartMarkers.some((re) => re.test(line))) {
|
|
197
|
-
startIndex = i;
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (startIndex === -1) {
|
|
202
|
-
let pivotIndex = -1;
|
|
203
|
-
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
204
|
-
const line = bodyLines[i];
|
|
205
|
-
if (!line)
|
|
206
|
-
continue;
|
|
207
|
-
if (hotEndMarkers.some((re) => re.test(line))) {
|
|
208
|
-
pivotIndex = i;
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (pivotIndex !== -1) {
|
|
213
|
-
if (compiledMarkers.some((re) => re.test(bodyLines[pivotIndex] ?? ''))) {
|
|
214
|
-
startIndex = pivotIndex;
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
const searchLimit = 80;
|
|
218
|
-
const from = Math.max(0, pivotIndex - searchLimit);
|
|
219
|
-
for (let i = pivotIndex; i >= from; i -= 1) {
|
|
220
|
-
const line = bodyLines[i];
|
|
221
|
-
if (!line)
|
|
222
|
-
continue;
|
|
223
|
-
if (compiledMarkers.some((re) => re.test(line))) {
|
|
224
|
-
startIndex = i;
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (startIndex === -1) {
|
|
229
|
-
startIndex = pivotIndex;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
if (startIndex === -1) {
|
|
235
|
-
for (let i = bodyLines.length - 1; i >= 0; i -= 1) {
|
|
236
|
-
const line = bodyLines[i];
|
|
237
|
-
if (!line)
|
|
238
|
-
continue;
|
|
239
|
-
if (/\bdev:client\b/.test(line)) {
|
|
240
|
-
startIndex = i;
|
|
241
|
-
break;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
const segment = startIndex === -1 ? bodyLines : bodyLines.slice(startIndex);
|
|
246
|
-
if (segment.length === 0) {
|
|
247
|
-
return [];
|
|
248
|
-
}
|
|
249
|
-
if (segment.length <= maxLines) {
|
|
250
|
-
return segment;
|
|
251
|
-
}
|
|
252
|
-
return segment.slice(segment.length - maxLines);
|
|
253
|
-
}
|
|
254
|
-
function normalizePid(value) {
|
|
255
|
-
if (typeof value === 'number') {
|
|
256
|
-
return String(value);
|
|
257
|
-
}
|
|
258
|
-
if (typeof value === 'string' && value.length > 0) {
|
|
259
|
-
return value;
|
|
260
|
-
}
|
|
261
|
-
return 'unknown';
|
|
262
|
-
}
|
|
263
|
-
function readJsonLinesLastPid(filePath, maxLines) {
|
|
264
|
-
const stat = fs.statSync(filePath);
|
|
265
|
-
if (stat.size === 0) {
|
|
266
|
-
return [];
|
|
267
|
-
}
|
|
268
|
-
const fd = fs.openSync(filePath, 'r');
|
|
269
|
-
const chunkSize = 64 * 1024;
|
|
270
|
-
let position = stat.size;
|
|
271
|
-
let remainder = '';
|
|
272
|
-
let targetPid = null;
|
|
273
|
-
let finished = false;
|
|
274
|
-
const collected = [];
|
|
275
|
-
try {
|
|
276
|
-
while (position > 0 && !finished) {
|
|
277
|
-
const length = Math.min(chunkSize, position);
|
|
278
|
-
position -= length;
|
|
279
|
-
const buffer = Buffer.alloc(length);
|
|
280
|
-
fs.readSync(fd, buffer, 0, length, position);
|
|
281
|
-
let chunk = buffer.toString('utf8');
|
|
282
|
-
if (remainder) {
|
|
283
|
-
chunk += remainder;
|
|
284
|
-
remainder = '';
|
|
285
|
-
}
|
|
286
|
-
const parts = chunk.split('\n');
|
|
287
|
-
remainder = parts.shift() ?? '';
|
|
288
|
-
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
|
289
|
-
const line = parts[i].trim();
|
|
290
|
-
if (!line)
|
|
291
|
-
continue;
|
|
292
|
-
let parsed = null;
|
|
293
|
-
try {
|
|
294
|
-
parsed = JSON.parse(line);
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
const pid = normalizePid(parsed?.pid);
|
|
300
|
-
if (targetPid === null) {
|
|
301
|
-
targetPid = pid;
|
|
302
|
-
}
|
|
303
|
-
if (pid !== targetPid) {
|
|
304
|
-
finished = true;
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
collected.push(line);
|
|
308
|
-
if (collected.length >= maxLines * 5) {
|
|
309
|
-
finished = true;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (!finished && remainder) {
|
|
315
|
-
const line = remainder.trim();
|
|
316
|
-
if (line) {
|
|
317
|
-
try {
|
|
318
|
-
const parsed = JSON.parse(line);
|
|
319
|
-
const pid = normalizePid(parsed?.pid);
|
|
320
|
-
if (targetPid === null) {
|
|
321
|
-
targetPid = pid;
|
|
322
|
-
}
|
|
323
|
-
if (pid === targetPid) {
|
|
324
|
-
collected.push(line);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch {
|
|
328
|
-
return [];
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
finally {
|
|
334
|
-
fs.closeSync(fd);
|
|
335
|
-
}
|
|
336
|
-
const reversed = collected.reverse();
|
|
337
|
-
if (reversed.length <= maxLines) {
|
|
338
|
-
return reversed;
|
|
339
|
-
}
|
|
340
|
-
return reversed.slice(reversed.length - maxLines);
|
|
341
|
-
}
|
|
342
|
-
function readJsonLinesTail(filePath, maxLines) {
|
|
343
|
-
const lines = readFileTailLines(filePath, maxLines);
|
|
344
|
-
const result = [];
|
|
345
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
346
|
-
const line = lines[i].trim();
|
|
347
|
-
if (!line)
|
|
348
|
-
continue;
|
|
349
|
-
result.push(line);
|
|
350
|
-
if (result.length >= maxLines) {
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return result.reverse();
|
|
140
|
+
throw new Error(`Unsupported log type: ${type}`);
|
|
355
141
|
}
|
|
356
142
|
export async function run(options) {
|
|
357
143
|
try {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
process.stdout.write(line + '\n');
|
|
361
|
-
}
|
|
144
|
+
const result = await readLogsJsonResult(options);
|
|
145
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
362
146
|
}
|
|
363
147
|
catch (error) {
|
|
364
148
|
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
-
|
|
149
|
+
const result = { hasError: true, message };
|
|
150
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
366
151
|
process.exitCode = 1;
|
|
367
152
|
}
|
|
368
153
|
}
|
|
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { readLatestLogLines } from './read-logs';
|
|
5
|
+
import { readLatestLogLines, readLogsJsonResult } from './read-logs';
|
|
6
6
|
async function writeFile(dir, fileName, content) {
|
|
7
7
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
8
8
|
await fs.promises.writeFile(path.join(dir, fileName), content, 'utf8');
|
|
@@ -31,6 +31,31 @@ describe('readLatestLogLines', () => {
|
|
|
31
31
|
});
|
|
32
32
|
expect(lines).toEqual(['> app@1.0.0 dev:server', 'started', 'ready']);
|
|
33
33
|
});
|
|
34
|
+
it('returns only the last server-std segment by latest marker', async () => {
|
|
35
|
+
const content = [
|
|
36
|
+
'[2026-01-05 10:00:00] [server] > app@1.0.0 dev:server',
|
|
37
|
+
'[2026-01-05 10:00:01] [server] [10:00:01] Starting compilation in watch mode...',
|
|
38
|
+
'[2026-01-05 10:00:02] [server] other',
|
|
39
|
+
'[2026-01-05 10:00:03] [server] [10:00:03] File change detected. Starting incremental compilation...',
|
|
40
|
+
'[2026-01-05 10:00:04] [server] other2',
|
|
41
|
+
'[2026-01-05 10:00:05] [server] [Nest] 1 - 2026/01/05 10:00:05 LOG [NestFactory] Starting Nest application...',
|
|
42
|
+
'[2026-01-05 10:00:06] [server] after-old',
|
|
43
|
+
'[2026-01-05 10:01:00] [server] [10:01:00] File change detected. Starting incremental compilation...',
|
|
44
|
+
'[2026-01-05 10:01:01] [server] tail-1',
|
|
45
|
+
'[2026-01-05 10:01:02] [server] tail-2',
|
|
46
|
+
].join('\n');
|
|
47
|
+
await writeFile(tmpDir, 'server.std.log', content);
|
|
48
|
+
const lines = await readLatestLogLines({
|
|
49
|
+
logDir: tmpDir,
|
|
50
|
+
type: 'server-std',
|
|
51
|
+
maxLines: 200,
|
|
52
|
+
});
|
|
53
|
+
expect(lines).toEqual([
|
|
54
|
+
'[10:01:00] File change detected. Starting incremental compilation...',
|
|
55
|
+
'tail-1',
|
|
56
|
+
'tail-2',
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
34
59
|
it('returns only the last client-std segment by hot reload markers', async () => {
|
|
35
60
|
const content = [
|
|
36
61
|
'[2026-01-05 10:00:00] [client] > app@1.0.0 dev:client',
|
|
@@ -66,4 +91,109 @@ describe('readLatestLogLines', () => {
|
|
|
66
91
|
JSON.stringify({ pid: 200, time: '2026-01-05T11:00:01.000Z', level: 'INFO', message: 'new2' }),
|
|
67
92
|
]);
|
|
68
93
|
});
|
|
94
|
+
it('filters server.log by traceId when provided', async () => {
|
|
95
|
+
const content = [
|
|
96
|
+
JSON.stringify({ pid: 200, trace_id: 't1', level: 'INFO', message: 'a' }),
|
|
97
|
+
JSON.stringify({ pid: 200, trace_id: 't2', level: 'INFO', message: 'b' }),
|
|
98
|
+
JSON.stringify({ pid: 200, trace_id: 't2', level: 'INFO', message: 'c' }),
|
|
99
|
+
JSON.stringify({ pid: 200, trace_id: 't3', level: 'INFO', message: 'd' }),
|
|
100
|
+
].join('\n');
|
|
101
|
+
await writeFile(tmpDir, 'server.log', content);
|
|
102
|
+
const result = await readLogsJsonResult({
|
|
103
|
+
logDir: tmpDir,
|
|
104
|
+
type: 'server',
|
|
105
|
+
maxLines: 200,
|
|
106
|
+
traceId: 't2',
|
|
107
|
+
});
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
hasError: false,
|
|
110
|
+
message: '',
|
|
111
|
+
logs: [
|
|
112
|
+
{ pid: 200, trace_id: 't2', level: 'INFO', message: 'b' },
|
|
113
|
+
{ pid: 200, trace_id: 't2', level: 'INFO', message: 'c' },
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
it('filters browser.log by traceId when provided', async () => {
|
|
118
|
+
const content = [
|
|
119
|
+
JSON.stringify({ level: 'info', message: 'x', trace_id: 't1' }),
|
|
120
|
+
JSON.stringify({ level: 'info', message: 'y', trace_id: 't2' }),
|
|
121
|
+
JSON.stringify({ level: 'info', message: 'z', trace_id: 't2' }),
|
|
122
|
+
].join('\n');
|
|
123
|
+
await writeFile(tmpDir, 'browser.log', content);
|
|
124
|
+
const result = await readLogsJsonResult({
|
|
125
|
+
logDir: tmpDir,
|
|
126
|
+
type: 'browser',
|
|
127
|
+
maxLines: 200,
|
|
128
|
+
traceId: 't2',
|
|
129
|
+
});
|
|
130
|
+
expect(result).toEqual({
|
|
131
|
+
hasError: false,
|
|
132
|
+
message: '',
|
|
133
|
+
logs: [
|
|
134
|
+
{ level: 'info', message: 'y', trace_id: 't2' },
|
|
135
|
+
{ level: 'info', message: 'z', trace_id: 't2' },
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it('returns JSON result for server-std as message', async () => {
|
|
140
|
+
const content = [
|
|
141
|
+
'[2026-01-05 10:00:00] [server] > app@1.0.0 dev:server',
|
|
142
|
+
'[2026-01-05 10:00:01] [server] booting',
|
|
143
|
+
].join('\n');
|
|
144
|
+
await writeFile(tmpDir, 'server.std.log', content);
|
|
145
|
+
const result = await readLogsJsonResult({
|
|
146
|
+
logDir: tmpDir,
|
|
147
|
+
type: 'server-std',
|
|
148
|
+
maxLines: 200,
|
|
149
|
+
});
|
|
150
|
+
expect(result).toEqual({
|
|
151
|
+
hasError: false,
|
|
152
|
+
message: '> app@1.0.0 dev:server\nbooting',
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
it('returns JSON result for server as logs', async () => {
|
|
156
|
+
const content = [
|
|
157
|
+
JSON.stringify({ pid: 100, message: 'old' }),
|
|
158
|
+
JSON.stringify({ pid: 200, message: 'new' }),
|
|
159
|
+
JSON.stringify({ pid: 200, message: 'new2' }),
|
|
160
|
+
].join('\n');
|
|
161
|
+
await writeFile(tmpDir, 'server.log', content);
|
|
162
|
+
const result = await readLogsJsonResult({
|
|
163
|
+
logDir: tmpDir,
|
|
164
|
+
type: 'server',
|
|
165
|
+
maxLines: 200,
|
|
166
|
+
});
|
|
167
|
+
expect(result).toEqual({
|
|
168
|
+
hasError: false,
|
|
169
|
+
message: '',
|
|
170
|
+
logs: [{ pid: 200, message: 'new' }, { pid: 200, message: 'new2' }],
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it('sets hasError=true for server-std error keywords', async () => {
|
|
174
|
+
const content = [
|
|
175
|
+
'[2026-01-05 10:00:00] [server] > app@1.0.0 dev:server',
|
|
176
|
+
'[2026-01-05 10:00:01] [server] ERROR something bad happened',
|
|
177
|
+
].join('\n');
|
|
178
|
+
await writeFile(tmpDir, 'server.std.log', content);
|
|
179
|
+
const result = await readLogsJsonResult({
|
|
180
|
+
logDir: tmpDir,
|
|
181
|
+
type: 'server-std',
|
|
182
|
+
maxLines: 200,
|
|
183
|
+
});
|
|
184
|
+
expect(result.hasError).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
it('sets hasError=true for server JSON logs with error level', async () => {
|
|
187
|
+
const content = [
|
|
188
|
+
JSON.stringify({ pid: 200, level: 'INFO', message: 'ok' }),
|
|
189
|
+
JSON.stringify({ pid: 200, level: 'ERROR', message: 'failed' }),
|
|
190
|
+
].join('\n');
|
|
191
|
+
await writeFile(tmpDir, 'server.log', content);
|
|
192
|
+
const result = await readLogsJsonResult({
|
|
193
|
+
logDir: tmpDir,
|
|
194
|
+
type: 'server',
|
|
195
|
+
maxLines: 200,
|
|
196
|
+
});
|
|
197
|
+
expect(result.hasError).toBe(true);
|
|
198
|
+
});
|
|
69
199
|
});
|