@shepai/cli 1.34.0 → 1.35.1
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/packages/core/src/domain/factories/index.d.ts +1 -0
- package/dist/packages/core/src/domain/factories/index.d.ts.map +1 -1
- package/dist/packages/core/src/domain/factories/index.js +1 -0
- package/dist/packages/core/src/domain/factories/spec-yaml-parser.d.ts +29 -0
- package/dist/packages/core/src/domain/factories/spec-yaml-parser.d.ts.map +1 -0
- package/dist/packages/core/src/domain/factories/spec-yaml-parser.js +118 -0
- package/dist/packages/core/src/domain/generated/output.d.ts +221 -12
- package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/agent/logs.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/agent/logs.command.js +9 -124
- package/dist/src/presentation/cli/commands/feat/index.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/index.js +3 -1
- package/dist/src/presentation/cli/commands/feat/logs.command.d.ts +15 -0
- package/dist/src/presentation/cli/commands/feat/logs.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/feat/logs.command.js +52 -0
- package/dist/src/presentation/cli/commands/log-viewer.d.ts +23 -0
- package/dist/src/presentation/cli/commands/log-viewer.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/log-viewer.js +135 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/cache/.previewinfo +1 -1
- package/web/.next/cache/.rscinfo +1 -1
- package/web/.next/cache/.tsbuildinfo +1 -1
- package/web/.next/cache/config.json +3 -3
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +1 -1
- package/web/.next/required-server-files.json +1 -1
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_12059e3e.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__f12b60be._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__f12b60be._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/src/presentation/web/server.js +1 -1
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- /package/web/.next/static/{HHW4FOl5T41Xe5-ljK9pc → _6QZh55r5rZlPal4MMGV8}/_buildManifest.js +0 -0
- /package/web/.next/static/{HHW4FOl5T41Xe5-ljK9pc → _6QZh55r5rZlPal4MMGV8}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{HHW4FOl5T41Xe5-ljK9pc → _6QZh55r5rZlPal4MMGV8}/_ssgManifest.js +0 -0
|
@@ -11,41 +11,9 @@
|
|
|
11
11
|
import { Command } from 'commander';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
|
-
import {
|
|
15
|
-
import { open } from 'node:fs/promises';
|
|
16
|
-
import { colors, messages } from '../../ui/index.js';
|
|
14
|
+
import { messages } from '../../ui/index.js';
|
|
17
15
|
import { resolveAgentRun } from './resolve-run.js';
|
|
18
|
-
|
|
19
|
-
function readTailLines(filePath, n) {
|
|
20
|
-
const stat = statSync(filePath);
|
|
21
|
-
// For small files (< 64KB), just read the whole thing
|
|
22
|
-
if (stat.size < 65536) {
|
|
23
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
24
|
-
const lines = content.split('\n');
|
|
25
|
-
return lines.slice(Math.max(0, lines.length - n)).join('\n');
|
|
26
|
-
}
|
|
27
|
-
// For large files, read from the end in chunks
|
|
28
|
-
const fd = openSync(filePath, 'r');
|
|
29
|
-
const chunkSize = Math.min(stat.size, 8192);
|
|
30
|
-
let found = 0;
|
|
31
|
-
let pos = stat.size;
|
|
32
|
-
let tail = '';
|
|
33
|
-
const buf = Buffer.alloc(chunkSize);
|
|
34
|
-
while (pos > 0 && found <= n) {
|
|
35
|
-
const readSize = Math.min(chunkSize, pos);
|
|
36
|
-
pos -= readSize;
|
|
37
|
-
readSync(fd, buf, 0, readSize, pos);
|
|
38
|
-
const chunk = buf.toString('utf-8', 0, readSize);
|
|
39
|
-
tail = chunk + tail;
|
|
40
|
-
for (const ch of chunk) {
|
|
41
|
-
if (ch === '\n')
|
|
42
|
-
found++;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
closeSync(fd);
|
|
46
|
-
const lines = tail.split('\n');
|
|
47
|
-
return lines.slice(Math.max(0, lines.length - n)).join('\n');
|
|
48
|
-
}
|
|
16
|
+
import { viewLog } from '../log-viewer.js';
|
|
49
17
|
export function createLogsCommand() {
|
|
50
18
|
return new Command('logs')
|
|
51
19
|
.description('View agent run logs')
|
|
@@ -61,97 +29,14 @@ export function createLogsCommand() {
|
|
|
61
29
|
return;
|
|
62
30
|
}
|
|
63
31
|
const logPath = join(homedir(), '.shep', 'logs', `worker-${resolved.run.id}.log`);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
32
|
+
const ok = await viewLog({
|
|
33
|
+
logPath,
|
|
34
|
+
follow: opts.follow,
|
|
35
|
+
lines: parseInt(opts.lines, 10),
|
|
36
|
+
label: `run ${resolved.run.id.substring(0, 8)}`,
|
|
37
|
+
});
|
|
38
|
+
if (!ok) {
|
|
67
39
|
process.exitCode = 1;
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const stat = statSync(logPath);
|
|
71
|
-
if (stat.size === 0) {
|
|
72
|
-
messages.info(`Log file is empty for run ${colors.accent(resolved.run.id.substring(0, 8))}`);
|
|
73
|
-
if (!opts.follow)
|
|
74
|
-
return;
|
|
75
|
-
// In follow mode, continue — the file will grow
|
|
76
|
-
}
|
|
77
|
-
const requestedLines = parseInt(opts.lines, 10);
|
|
78
|
-
if (opts.follow) {
|
|
79
|
-
// Print existing content first
|
|
80
|
-
if (stat.size > 0) {
|
|
81
|
-
if (requestedLines > 0) {
|
|
82
|
-
process.stdout.write(readTailLines(logPath, requestedLines));
|
|
83
|
-
if (!process.stdout.destroyed)
|
|
84
|
-
process.stdout.write('\n');
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
// Stream existing content without loading entire file into memory
|
|
88
|
-
await new Promise((resolve, reject) => {
|
|
89
|
-
const stream = createReadStream(logPath, { encoding: 'utf-8' });
|
|
90
|
-
stream.pipe(process.stdout, { end: false });
|
|
91
|
-
stream.on('end', resolve);
|
|
92
|
-
stream.on('error', reject);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Follow new content using fs.watch (event-driven, no polling)
|
|
97
|
-
let position = stat.size;
|
|
98
|
-
const handle = await open(logPath, 'r');
|
|
99
|
-
// Reusable 4KB buffer for incremental reads
|
|
100
|
-
const readBuf = Buffer.alloc(4096);
|
|
101
|
-
const readNewContent = async () => {
|
|
102
|
-
try {
|
|
103
|
-
const currentStat = statSync(logPath);
|
|
104
|
-
while (position < currentStat.size) {
|
|
105
|
-
const bytesToRead = Math.min(readBuf.length, currentStat.size - position);
|
|
106
|
-
const { bytesRead } = await handle.read(readBuf, 0, bytesToRead, position);
|
|
107
|
-
if (bytesRead === 0)
|
|
108
|
-
break;
|
|
109
|
-
process.stdout.write(readBuf.toString('utf-8', 0, bytesRead));
|
|
110
|
-
position += bytesRead;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// File may have been deleted/rotated
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
// Use fs.watch for efficient OS-level file change notifications
|
|
118
|
-
const watcher = watch(logPath, { persistent: true }, () => {
|
|
119
|
-
readNewContent();
|
|
120
|
-
});
|
|
121
|
-
// Fallback poll every 2s in case fs.watch misses events (NFS, etc.)
|
|
122
|
-
const fallbackInterval = setInterval(readNewContent, 2000);
|
|
123
|
-
const cleanup = async () => {
|
|
124
|
-
watcher.close();
|
|
125
|
-
clearInterval(fallbackInterval);
|
|
126
|
-
await handle.close();
|
|
127
|
-
};
|
|
128
|
-
process.on('SIGINT', async () => {
|
|
129
|
-
await cleanup();
|
|
130
|
-
process.exit(0);
|
|
131
|
-
});
|
|
132
|
-
process.on('SIGTERM', async () => {
|
|
133
|
-
await cleanup();
|
|
134
|
-
process.exit(0);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
// Print mode
|
|
139
|
-
if (requestedLines > 0) {
|
|
140
|
-
process.stdout.write(readTailLines(logPath, requestedLines));
|
|
141
|
-
process.stdout.write('\n');
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
// Stream the file — don't load it all into memory
|
|
145
|
-
await new Promise((resolve, reject) => {
|
|
146
|
-
const stream = createReadStream(logPath, { encoding: 'utf-8' });
|
|
147
|
-
stream.pipe(process.stdout, { end: false });
|
|
148
|
-
stream.on('end', () => {
|
|
149
|
-
process.stdout.write('\n');
|
|
150
|
-
resolve();
|
|
151
|
-
});
|
|
152
|
-
stream.on('error', reject);
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
40
|
}
|
|
156
41
|
}
|
|
157
42
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAY3C"}
|
|
@@ -18,6 +18,7 @@ import { createResumeCommand } from './resume.command.js';
|
|
|
18
18
|
import { createReviewCommand } from './review.command.js';
|
|
19
19
|
import { createApproveCommand } from './approve.command.js';
|
|
20
20
|
import { createRejectCommand } from './reject.command.js';
|
|
21
|
+
import { createLogsCommand } from './logs.command.js';
|
|
21
22
|
/**
|
|
22
23
|
* Create the feat command group
|
|
23
24
|
*/
|
|
@@ -31,5 +32,6 @@ export function createFeatCommand() {
|
|
|
31
32
|
.addCommand(createResumeCommand())
|
|
32
33
|
.addCommand(createReviewCommand())
|
|
33
34
|
.addCommand(createApproveCommand())
|
|
34
|
-
.addCommand(createRejectCommand())
|
|
35
|
+
.addCommand(createRejectCommand())
|
|
36
|
+
.addCommand(createLogsCommand());
|
|
35
37
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Logs Command
|
|
3
|
+
*
|
|
4
|
+
* View log output for a feature's agent run with efficient streaming.
|
|
5
|
+
* Resolves the feature by ID, finds its associated agent run, then
|
|
6
|
+
* displays the worker log file.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* shep feat logs <id> # Print full log
|
|
10
|
+
* shep feat logs -f <id> # Follow (tail -f) using fs.watch
|
|
11
|
+
* shep feat logs -n 50 <id> # Last 50 lines
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
export declare function createLogsCommand(): Command;
|
|
15
|
+
//# sourceMappingURL=logs.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.command.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/logs.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,iBAAiB,IAAI,OAAO,CAkC3C"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Logs Command
|
|
3
|
+
*
|
|
4
|
+
* View log output for a feature's agent run with efficient streaming.
|
|
5
|
+
* Resolves the feature by ID, finds its associated agent run, then
|
|
6
|
+
* displays the worker log file.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* shep feat logs <id> # Print full log
|
|
10
|
+
* shep feat logs -f <id> # Follow (tail -f) using fs.watch
|
|
11
|
+
* shep feat logs -n 50 <id> # Last 50 lines
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { container } from '../../../../../packages/core/src/infrastructure/di/container.js';
|
|
17
|
+
import { ShowFeatureUseCase } from '../../../../../packages/core/src/application/use-cases/features/show-feature.use-case.js';
|
|
18
|
+
import { messages } from '../../ui/index.js';
|
|
19
|
+
import { viewLog } from '../log-viewer.js';
|
|
20
|
+
export function createLogsCommand() {
|
|
21
|
+
return new Command('logs')
|
|
22
|
+
.description('View feature agent logs')
|
|
23
|
+
.argument('<id>', 'Feature ID (or prefix)')
|
|
24
|
+
.option('-f, --follow', 'Follow log output (like tail -f)')
|
|
25
|
+
.option('-n, --lines <count>', 'Number of lines to show from the end', '0')
|
|
26
|
+
.action(async (id, opts) => {
|
|
27
|
+
try {
|
|
28
|
+
const useCase = container.resolve(ShowFeatureUseCase);
|
|
29
|
+
const feature = await useCase.execute(id);
|
|
30
|
+
if (!feature.agentRunId) {
|
|
31
|
+
messages.error(`Feature "${feature.name}" has no agent run`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const logPath = join(homedir(), '.shep', 'logs', `worker-${feature.agentRunId}.log`);
|
|
36
|
+
const ok = await viewLog({
|
|
37
|
+
logPath,
|
|
38
|
+
follow: opts.follow,
|
|
39
|
+
lines: parseInt(opts.lines, 10),
|
|
40
|
+
label: `feature "${feature.name}"`,
|
|
41
|
+
});
|
|
42
|
+
if (!ok) {
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
48
|
+
messages.error('Failed to read feature logs', err);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Log Viewer
|
|
3
|
+
*
|
|
4
|
+
* Displays a log file with support for follow mode and tail lines.
|
|
5
|
+
* Used by both `shep agent logs` and `shep feat logs`.
|
|
6
|
+
*/
|
|
7
|
+
export interface LogViewerOptions {
|
|
8
|
+
/** Absolute path to the log file */
|
|
9
|
+
logPath: string;
|
|
10
|
+
/** Follow log output (like tail -f) */
|
|
11
|
+
follow?: boolean;
|
|
12
|
+
/** Number of lines to show from the end (0 = all) */
|
|
13
|
+
lines: number;
|
|
14
|
+
/** Label for error messages (e.g. "run abc123", "feature my-feat") */
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Display a log file to stdout with optional follow mode and tail lines.
|
|
19
|
+
*
|
|
20
|
+
* @returns false if the log file doesn't exist or is empty (in non-follow mode)
|
|
21
|
+
*/
|
|
22
|
+
export declare function viewLog(opts: LogViewerOptions): Promise<boolean>;
|
|
23
|
+
//# sourceMappingURL=log-viewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-viewer.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/log-viewer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;CACf;AAiCD;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CA6FtE"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Log Viewer
|
|
3
|
+
*
|
|
4
|
+
* Displays a log file with support for follow mode and tail lines.
|
|
5
|
+
* Used by both `shep agent logs` and `shep feat logs`.
|
|
6
|
+
*/
|
|
7
|
+
import { closeSync, createReadStream, existsSync, openSync, readFileSync, readSync, statSync, watch, } from 'node:fs';
|
|
8
|
+
import { open } from 'node:fs/promises';
|
|
9
|
+
import { messages } from '../ui/index.js';
|
|
10
|
+
/** Read the last N lines from a file without loading it all into memory */
|
|
11
|
+
function readTailLines(filePath, n) {
|
|
12
|
+
const stat = statSync(filePath);
|
|
13
|
+
// For small files (< 64KB), just read the whole thing
|
|
14
|
+
if (stat.size < 65536) {
|
|
15
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
return lines.slice(Math.max(0, lines.length - n)).join('\n');
|
|
18
|
+
}
|
|
19
|
+
// For large files, read from the end in chunks
|
|
20
|
+
const fd = openSync(filePath, 'r');
|
|
21
|
+
const chunkSize = Math.min(stat.size, 8192);
|
|
22
|
+
let found = 0;
|
|
23
|
+
let pos = stat.size;
|
|
24
|
+
let tail = '';
|
|
25
|
+
const buf = Buffer.alloc(chunkSize);
|
|
26
|
+
while (pos > 0 && found <= n) {
|
|
27
|
+
const readSize = Math.min(chunkSize, pos);
|
|
28
|
+
pos -= readSize;
|
|
29
|
+
readSync(fd, buf, 0, readSize, pos);
|
|
30
|
+
const chunk = buf.toString('utf-8', 0, readSize);
|
|
31
|
+
tail = chunk + tail;
|
|
32
|
+
for (const ch of chunk) {
|
|
33
|
+
if (ch === '\n')
|
|
34
|
+
found++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
closeSync(fd);
|
|
38
|
+
const lines = tail.split('\n');
|
|
39
|
+
return lines.slice(Math.max(0, lines.length - n)).join('\n');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Display a log file to stdout with optional follow mode and tail lines.
|
|
43
|
+
*
|
|
44
|
+
* @returns false if the log file doesn't exist or is empty (in non-follow mode)
|
|
45
|
+
*/
|
|
46
|
+
export async function viewLog(opts) {
|
|
47
|
+
const { logPath, follow, lines: requestedLines, label } = opts;
|
|
48
|
+
if (!existsSync(logPath)) {
|
|
49
|
+
messages.error(`No log file found for ${label}`);
|
|
50
|
+
messages.info(`Expected: ${logPath}`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const stat = statSync(logPath);
|
|
54
|
+
if (stat.size === 0) {
|
|
55
|
+
messages.info(`Log file is empty for ${label}`);
|
|
56
|
+
if (!follow)
|
|
57
|
+
return false;
|
|
58
|
+
// In follow mode, continue — the file will grow
|
|
59
|
+
}
|
|
60
|
+
if (follow) {
|
|
61
|
+
// Print existing content first
|
|
62
|
+
if (stat.size > 0) {
|
|
63
|
+
if (requestedLines > 0) {
|
|
64
|
+
process.stdout.write(readTailLines(logPath, requestedLines));
|
|
65
|
+
if (!process.stdout.destroyed)
|
|
66
|
+
process.stdout.write('\n');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
const stream = createReadStream(logPath, { encoding: 'utf-8' });
|
|
71
|
+
stream.pipe(process.stdout, { end: false });
|
|
72
|
+
stream.on('end', resolve);
|
|
73
|
+
stream.on('error', reject);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Follow new content using fs.watch (event-driven, no polling)
|
|
78
|
+
let position = stat.size;
|
|
79
|
+
const handle = await open(logPath, 'r');
|
|
80
|
+
const readBuf = Buffer.alloc(4096);
|
|
81
|
+
const readNewContent = async () => {
|
|
82
|
+
try {
|
|
83
|
+
const currentStat = statSync(logPath);
|
|
84
|
+
while (position < currentStat.size) {
|
|
85
|
+
const bytesToRead = Math.min(readBuf.length, currentStat.size - position);
|
|
86
|
+
const { bytesRead } = await handle.read(readBuf, 0, bytesToRead, position);
|
|
87
|
+
if (bytesRead === 0)
|
|
88
|
+
break;
|
|
89
|
+
process.stdout.write(readBuf.toString('utf-8', 0, bytesRead));
|
|
90
|
+
position += bytesRead;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// File may have been deleted/rotated
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const watcher = watch(logPath, { persistent: true }, () => {
|
|
98
|
+
readNewContent();
|
|
99
|
+
});
|
|
100
|
+
// Fallback poll every 2s in case fs.watch misses events (NFS, etc.)
|
|
101
|
+
const fallbackInterval = setInterval(readNewContent, 2000);
|
|
102
|
+
const cleanup = async () => {
|
|
103
|
+
watcher.close();
|
|
104
|
+
clearInterval(fallbackInterval);
|
|
105
|
+
await handle.close();
|
|
106
|
+
};
|
|
107
|
+
process.on('SIGINT', async () => {
|
|
108
|
+
await cleanup();
|
|
109
|
+
process.exit(0);
|
|
110
|
+
});
|
|
111
|
+
process.on('SIGTERM', async () => {
|
|
112
|
+
await cleanup();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Print mode
|
|
118
|
+
if (requestedLines > 0) {
|
|
119
|
+
process.stdout.write(readTailLines(logPath, requestedLines));
|
|
120
|
+
process.stdout.write('\n');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
await new Promise((resolve, reject) => {
|
|
124
|
+
const stream = createReadStream(logPath, { encoding: 'utf-8' });
|
|
125
|
+
stream.pipe(process.stdout, { end: false });
|
|
126
|
+
stream.on('end', () => {
|
|
127
|
+
process.stdout.write('\n');
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
stream.on('error', reject);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|