@lark-apaas/fullstack-cli 1.1.7 → 1.1.8

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.
@@ -0,0 +1,9 @@
1
+ type LogType = 'server' | 'trace' | 'server-std' | 'client-std' | 'browser' | 'client';
2
+ interface ReadLogsOptions {
3
+ logDir: string;
4
+ type: LogType;
5
+ maxLines?: number;
6
+ }
7
+ export declare function readLatestLogLines(options: ReadLogsOptions): Promise<string[]>;
8
+ export declare function run(options: ReadLogsOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,368 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export async function readLatestLogLines(options) {
4
+ const maxLines = options.maxLines ?? 200;
5
+ const filePath = resolveLogFilePath(options.logDir, options.type);
6
+ if (!fileExists(filePath)) {
7
+ throw new Error(`Log file not found: ${filePath}`);
8
+ }
9
+ let lines = readFileTailLines(filePath, maxLines * 5);
10
+ if (options.type === 'server-std') {
11
+ lines = extractServerStdSegment(lines, maxLines);
12
+ }
13
+ else if (options.type === 'client-std') {
14
+ lines = extractClientStdSegment(lines, maxLines);
15
+ }
16
+ else if (options.type === 'server' || options.type === 'trace') {
17
+ lines = readJsonLinesLastPid(filePath, maxLines);
18
+ }
19
+ else if (options.type === 'browser' || options.type === 'client') {
20
+ lines = readJsonLinesTail(filePath, maxLines);
21
+ }
22
+ return lines;
23
+ }
24
+ function resolveLogFilePath(logDir, type) {
25
+ const base = path.isAbsolute(logDir) ? logDir : path.join(process.cwd(), logDir);
26
+ if (type === 'server') {
27
+ return path.join(base, 'server.log');
28
+ }
29
+ if (type === 'trace') {
30
+ return path.join(base, 'trace.log');
31
+ }
32
+ if (type === 'server-std') {
33
+ return path.join(base, 'server.std.log');
34
+ }
35
+ if (type === 'client-std') {
36
+ return path.join(base, 'client.std.log');
37
+ }
38
+ if (type === 'browser') {
39
+ return path.join(base, 'browser.log');
40
+ }
41
+ return path.join(base, 'client.log');
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();
355
+ }
356
+ export async function run(options) {
357
+ try {
358
+ const lines = await readLatestLogLines(options);
359
+ for (const line of lines) {
360
+ process.stdout.write(line + '\n');
361
+ }
362
+ }
363
+ catch (error) {
364
+ const message = error instanceof Error ? error.message : String(error);
365
+ console.error(`[read-logs] ${message}`);
366
+ process.exitCode = 1;
367
+ }
368
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { readLatestLogLines } from './read-logs';
6
+ async function writeFile(dir, fileName, content) {
7
+ await fs.promises.mkdir(dir, { recursive: true });
8
+ await fs.promises.writeFile(path.join(dir, fileName), content, 'utf8');
9
+ }
10
+ describe('readLatestLogLines', () => {
11
+ let tmpDir;
12
+ beforeEach(async () => {
13
+ tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'fullstack-cli-logs-'));
14
+ });
15
+ afterEach(async () => {
16
+ await fs.promises.rm(tmpDir, { recursive: true, force: true });
17
+ });
18
+ it('returns only the last server-std segment', async () => {
19
+ const content = [
20
+ '[2026-01-05 10:00:00] [server] > app@1.0.0 dev:server',
21
+ '[2026-01-05 10:00:01] [server] booting',
22
+ '[2026-01-05 10:01:00] [server] > app@1.0.0 dev:server',
23
+ '[2026-01-05 10:01:01] [server] started',
24
+ '[2026-01-05 10:01:02] [server] ready',
25
+ ].join('\n');
26
+ await writeFile(tmpDir, 'server.std.log', content);
27
+ const lines = await readLatestLogLines({
28
+ logDir: tmpDir,
29
+ type: 'server-std',
30
+ maxLines: 200,
31
+ });
32
+ expect(lines).toEqual(['> app@1.0.0 dev:server', 'started', 'ready']);
33
+ });
34
+ it('returns only the last client-std segment by hot reload markers', async () => {
35
+ const content = [
36
+ '[2026-01-05 10:00:00] [client] > app@1.0.0 dev:client',
37
+ '[2026-01-05 10:00:05] [client] compiled successfully',
38
+ '[2026-01-05 10:00:06] [client] initial done',
39
+ '[2026-01-05 10:02:00] [client] compiled successfully',
40
+ '[2026-01-05 10:02:01] [client] hmr update',
41
+ '[2026-01-05 10:02:02] [client] updated',
42
+ ].join('\n');
43
+ await writeFile(tmpDir, 'client.std.log', content);
44
+ const lines = await readLatestLogLines({
45
+ logDir: tmpDir,
46
+ type: 'client-std',
47
+ maxLines: 200,
48
+ });
49
+ expect(lines).toEqual(['compiled successfully', 'hmr update', 'updated']);
50
+ });
51
+ it('returns only the last server pid segment for server.log', async () => {
52
+ const content = [
53
+ JSON.stringify({ pid: 100, time: '2026-01-05T10:00:00.000Z', level: 'INFO', message: 'old' }),
54
+ JSON.stringify({ pid: 100, time: '2026-01-05T10:00:01.000Z', level: 'INFO', message: 'old2' }),
55
+ JSON.stringify({ pid: 200, time: '2026-01-05T11:00:00.000Z', level: 'INFO', message: 'new' }),
56
+ JSON.stringify({ pid: 200, time: '2026-01-05T11:00:01.000Z', level: 'INFO', message: 'new2' }),
57
+ ].join('\n');
58
+ await writeFile(tmpDir, 'server.log', content);
59
+ const lines = await readLatestLogLines({
60
+ logDir: tmpDir,
61
+ type: 'server',
62
+ maxLines: 200,
63
+ });
64
+ expect(lines).toEqual([
65
+ JSON.stringify({ pid: 200, time: '2026-01-05T11:00:00.000Z', level: 'INFO', message: 'new' }),
66
+ JSON.stringify({ pid: 200, time: '2026-01-05T11:00:01.000Z', level: 'INFO', message: 'new2' }),
67
+ ]);
68
+ });
69
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "CLI tool for fullstack template management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@lark-apaas/devtool-kits": "^1.2.10",
34
+ "@lark-apaas/devtool-kits": "^1.2.11",
35
35
  "@vercel/nft": "^0.30.3",
36
36
  "cac": "^6.7.14",
37
37
  "dotenv": "^16.0.0",