@mandujs/cli 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.ko.md +234 -234
  2. package/README.md +354 -354
  3. package/package.json +2 -2
  4. package/src/commands/contract.ts +173 -173
  5. package/src/commands/dev.ts +8 -68
  6. package/src/commands/doctor.ts +27 -27
  7. package/src/commands/guard-arch.ts +303 -303
  8. package/src/commands/guard-check.ts +3 -3
  9. package/src/commands/monitor.ts +300 -300
  10. package/src/commands/openapi.ts +107 -107
  11. package/src/commands/registry.ts +367 -357
  12. package/src/commands/routes.ts +228 -228
  13. package/src/commands/start.ts +184 -0
  14. package/src/errors/codes.ts +35 -35
  15. package/src/errors/index.ts +2 -2
  16. package/src/errors/messages.ts +143 -143
  17. package/src/hooks/index.ts +17 -17
  18. package/src/hooks/preaction.ts +256 -256
  19. package/src/main.ts +37 -34
  20. package/src/terminal/banner.ts +166 -166
  21. package/src/terminal/help.ts +306 -306
  22. package/src/terminal/index.ts +71 -71
  23. package/src/terminal/output.ts +295 -295
  24. package/src/terminal/palette.ts +30 -30
  25. package/src/terminal/progress.ts +327 -327
  26. package/src/terminal/stream-writer.ts +214 -214
  27. package/src/terminal/table.ts +354 -354
  28. package/src/terminal/theme.ts +142 -142
  29. package/src/util/bun.ts +6 -6
  30. package/src/util/fs.ts +23 -23
  31. package/src/util/handlers.ts +96 -0
  32. package/src/util/manifest.ts +52 -52
  33. package/src/util/output.ts +22 -22
  34. package/src/util/port.ts +71 -71
  35. package/templates/default/AGENTS.md +96 -96
  36. package/templates/default/app/api/health/route.ts +13 -13
  37. package/templates/default/app/globals.css +49 -49
  38. package/templates/default/app/layout.tsx +27 -27
  39. package/templates/default/app/page.tsx +38 -38
  40. package/templates/default/package.json +1 -0
  41. package/templates/default/src/client/shared/lib/utils.ts +16 -16
  42. package/templates/default/src/client/shared/ui/button.tsx +57 -57
  43. package/templates/default/src/client/shared/ui/card.tsx +78 -78
  44. package/templates/default/src/client/shared/ui/index.ts +21 -21
  45. package/templates/default/src/client/shared/ui/input.tsx +24 -24
  46. package/templates/default/tests/example.test.ts +58 -58
  47. package/templates/default/tests/helpers.ts +52 -52
  48. package/templates/default/tests/setup.ts +9 -9
  49. package/templates/default/tsconfig.json +12 -14
  50. package/templates/default/apps/server/main.ts +0 -67
  51. package/templates/default/apps/web/entry.tsx +0 -35
@@ -1,300 +1,300 @@
1
- import fs from "fs/promises";
2
- import fsSync from "fs";
3
- import path from "path";
4
- import { resolveFromCwd, pathExists } from "../util/fs";
5
- import { resolveOutputFormat } from "../util/output";
6
-
7
- type MonitorOutput = "console" | "json";
8
-
9
- export interface MonitorOptions {
10
- follow?: boolean;
11
- summary?: boolean;
12
- since?: string;
13
- file?: string;
14
- }
15
-
16
- interface MonitorEvent {
17
- ts?: string;
18
- type?: string;
19
- severity?: "info" | "warn" | "error";
20
- source?: string;
21
- message?: string;
22
- data?: Record<string, unknown>;
23
- count?: number;
24
- }
25
-
26
- function parseDuration(value?: string): number | undefined {
27
- if (!value) return undefined;
28
- const trimmed = value.trim();
29
- const match = trimmed.match(/^(\d+)(ms|s|m|h|d)?$/);
30
- if (!match) return undefined;
31
- const amount = Number(match[1]);
32
- const unit = match[2] ?? "m";
33
- switch (unit) {
34
- case "ms":
35
- return amount;
36
- case "s":
37
- return amount * 1000;
38
- case "m":
39
- return amount * 60 * 1000;
40
- case "h":
41
- return amount * 60 * 60 * 1000;
42
- case "d":
43
- return amount * 24 * 60 * 60 * 1000;
44
- default:
45
- return undefined;
46
- }
47
- }
48
-
49
- function formatTime(ts?: string): string {
50
- const date = ts ? new Date(ts) : new Date();
51
- return date.toLocaleTimeString("ko-KR", { hour12: false });
52
- }
53
-
54
- function formatEventForConsole(event: MonitorEvent): string {
55
- const time = formatTime(event.ts);
56
- const countSuffix = event.count && event.count > 1 ? ` x${event.count}` : "";
57
- const type = event.type ?? "event";
58
-
59
- if (type === "tool.call") {
60
- const tag = (event.data?.tag as string | undefined) ?? "TOOL";
61
- const argsSummary = event.data?.argsSummary as string | undefined;
62
- return `${time} → [${tag}]${argsSummary ?? ""}${countSuffix}`;
63
- }
64
- if (type === "tool.error") {
65
- const tag = (event.data?.tag as string | undefined) ?? "TOOL";
66
- const argsSummary = event.data?.argsSummary as string | undefined;
67
- const message = event.message ?? "ERROR";
68
- return `${time} ✗ [${tag}]${argsSummary ?? ""}${countSuffix}\n ${message}`;
69
- }
70
- if (type === "tool.result") {
71
- const tag = (event.data?.tag as string | undefined) ?? "TOOL";
72
- const summary = event.data?.summary as string | undefined;
73
- return `${time} ✓ [${tag}]${summary ?? ""}${countSuffix}`;
74
- }
75
- if (type === "watch.warning") {
76
- const ruleId = event.data?.ruleId as string | undefined;
77
- const file = event.data?.file as string | undefined;
78
- const message = event.message ?? "";
79
- const icon = event.severity === "info" ? "ℹ" : "⚠";
80
- return `${time} ${icon} [WATCH:${ruleId ?? "UNKNOWN"}] ${file ?? ""}${countSuffix}\n ${message}`;
81
- }
82
- if (type === "guard.violation") {
83
- const ruleId = event.data?.ruleId as string | undefined;
84
- const file = event.data?.file as string | undefined;
85
- const line = event.data?.line as number | undefined;
86
- const message = event.message ?? (event.data?.message as string | undefined) ?? "";
87
- const location = line ? `${file}:${line}` : file ?? "";
88
- return `${time} 🚨 [GUARD:${ruleId ?? "UNKNOWN"}] ${location}${countSuffix}\n ${message}`;
89
- }
90
- if (type === "guard.summary") {
91
- const count = event.data?.count as number | undefined;
92
- const passed = event.data?.passed as boolean | undefined;
93
- return `${time} 🧱 [GUARD] ${passed ? "PASSED" : "FAILED"} (${count ?? 0} violations)`;
94
- }
95
- if (type === "routes.change") {
96
- const action = event.data?.action as string | undefined;
97
- const routeId = event.data?.routeId as string | undefined;
98
- const pattern = event.data?.pattern as string | undefined;
99
- const kind = event.data?.kind as string | undefined;
100
- const detail = [routeId, pattern, kind].filter(Boolean).join(" ");
101
- return `${time} 🛣️ [ROUTES:${action ?? "change"}] ${detail}${countSuffix}`;
102
- }
103
- if (type === "monitor.summary") {
104
- return `${time} · SUMMARY ${event.message ?? ""}`;
105
- }
106
- if (type === "system.event") {
107
- const category = event.data?.category as string | undefined;
108
- return `${time} [${category ?? "SYSTEM"}] ${event.message ?? ""}${countSuffix}`;
109
- }
110
-
111
- return `${time} [${type}] ${event.message ?? ""}${countSuffix}`;
112
- }
113
-
114
- async function resolveLogFile(
115
- rootDir: string,
116
- output: MonitorOutput,
117
- explicit?: string
118
- ): Promise<string | null> {
119
- if (explicit) return explicit;
120
-
121
- const manduDir = path.join(rootDir, ".mandu");
122
- const jsonPath = path.join(manduDir, "activity.jsonl");
123
- const logPath = path.join(manduDir, "activity.log");
124
-
125
- const hasJson = await pathExists(jsonPath);
126
- const hasLog = await pathExists(logPath);
127
-
128
- if (output === "json") {
129
- if (hasJson) return jsonPath;
130
- if (hasLog) return logPath;
131
- } else {
132
- if (hasLog) return logPath;
133
- if (hasJson) return jsonPath;
134
- }
135
-
136
- return null;
137
- }
138
-
139
- async function readSummary(
140
- filePath: string,
141
- sinceMs: number
142
- ): Promise<{
143
- windowMs: number;
144
- total: number;
145
- bySeverity: { info: number; warn: number; error: number };
146
- byType: Record<string, number>;
147
- }> {
148
- const content = await fs.readFile(filePath, "utf-8");
149
- const lines = content.split("\n").filter(Boolean);
150
- const cutoff = Date.now() - sinceMs;
151
- const counts = { total: 0, info: 0, warn: 0, error: 0 };
152
- const byType: Record<string, number> = {};
153
-
154
- for (const line of lines) {
155
- try {
156
- const event = JSON.parse(line) as MonitorEvent;
157
- if (!event.ts) continue;
158
- const ts = new Date(event.ts).getTime();
159
- if (Number.isNaN(ts) || ts < cutoff) continue;
160
- const count = event.count ?? 1;
161
- counts.total += count;
162
- if (event.severity) {
163
- counts[event.severity] += count;
164
- }
165
- const type = event.type ?? "event";
166
- byType[type] = (byType[type] ?? 0) + count;
167
- } catch {
168
- // ignore parse errors
169
- }
170
- }
171
-
172
- return { windowMs: sinceMs, total: counts.total, bySeverity: counts, byType };
173
- }
174
-
175
- function printSummaryConsole(summary: {
176
- windowMs: number;
177
- total: number;
178
- bySeverity: { info: number; warn: number; error: number };
179
- byType: Record<string, number>;
180
- }): void {
181
- const seconds = Math.round(summary.windowMs / 1000);
182
- const topTypes = Object.entries(summary.byType)
183
- .sort((a, b) => b[1] - a[1])
184
- .slice(0, 5)
185
- .map(([type, count]) => `${type}=${count}`)
186
- .join(", ");
187
-
188
- console.log(`Summary (last ${seconds}s)`);
189
- console.log(` total=${summary.total}`);
190
- console.log(` error=${summary.bySeverity.error} warn=${summary.bySeverity.warn} info=${summary.bySeverity.info}`);
191
- if (topTypes) {
192
- console.log(` top=${topTypes}`);
193
- }
194
- }
195
-
196
- function outputChunk(
197
- chunk: string,
198
- isJson: boolean,
199
- output: MonitorOutput
200
- ): void {
201
- if (!isJson || output === "json") {
202
- process.stdout.write(chunk);
203
- return;
204
- }
205
-
206
- const lines = chunk.split("\n").filter(Boolean);
207
- for (const line of lines) {
208
- try {
209
- const event = JSON.parse(line) as MonitorEvent;
210
- const formatted = formatEventForConsole(event);
211
- process.stdout.write(`${formatted}\n`);
212
- } catch {
213
- process.stdout.write(`${line}\n`);
214
- }
215
- }
216
- }
217
-
218
- async function followFile(
219
- filePath: string,
220
- isJson: boolean,
221
- output: MonitorOutput,
222
- startAtEnd: boolean
223
- ): Promise<void> {
224
- let position = 0;
225
- let buffer = "";
226
-
227
- try {
228
- const stat = await fs.stat(filePath);
229
- position = startAtEnd ? stat.size : 0;
230
- } catch {
231
- position = 0;
232
- }
233
-
234
- const fd = await fs.open(filePath, "r");
235
-
236
- fsSync.watchFile(
237
- filePath,
238
- { interval: 500 },
239
- async (curr) => {
240
- if (curr.size < position) {
241
- position = 0;
242
- buffer = "";
243
- }
244
- if (curr.size === position) {
245
- return;
246
- }
247
-
248
- const length = curr.size - position;
249
- const chunk = Buffer.alloc(length);
250
- await fd.read(chunk, 0, length, position);
251
- position = curr.size;
252
- buffer += chunk.toString("utf-8");
253
-
254
- const lines = buffer.split("\n");
255
- buffer = lines.pop() ?? "";
256
- if (lines.length > 0) {
257
- outputChunk(lines.join("\n"), isJson, output);
258
- }
259
- }
260
- );
261
- }
262
-
263
- export async function monitor(options: MonitorOptions = {}): Promise<boolean> {
264
- const rootDir = resolveFromCwd(".");
265
- const resolved = resolveOutputFormat();
266
- const output: MonitorOutput = resolved === "json" || resolved === "agent" ? "json" : "console";
267
- const filePath = await resolveLogFile(rootDir, output, options.file);
268
-
269
- if (!filePath) {
270
- console.error("❌ activity log 파일을 찾을 수 없습니다. (.mandu/activity.log 또는 activity.jsonl)");
271
- return false;
272
- }
273
-
274
- const isJson = filePath.endsWith(".jsonl");
275
- const follow = options.follow !== false;
276
-
277
- if (options.summary) {
278
- if (!isJson) {
279
- console.error("⚠️ summary는 JSON 로그(activity.jsonl)에서만 가능합니다.");
280
- } else {
281
- const windowMs = parseDuration(options.since) ?? 5 * 60 * 1000;
282
- const summary = await readSummary(filePath, windowMs);
283
- if (output === "json") {
284
- console.log(JSON.stringify(summary, null, 2));
285
- } else {
286
- printSummaryConsole(summary);
287
- }
288
- }
289
- if (!follow) return true;
290
- }
291
-
292
- if (!follow) {
293
- const content = await fs.readFile(filePath, "utf-8");
294
- outputChunk(content, isJson, output);
295
- return true;
296
- }
297
-
298
- await followFile(filePath, isJson, output, true);
299
- return new Promise(() => {});
300
- }
1
+ import fs from "fs/promises";
2
+ import fsSync from "fs";
3
+ import path from "path";
4
+ import { resolveFromCwd, pathExists } from "../util/fs";
5
+ import { resolveOutputFormat } from "../util/output";
6
+
7
+ type MonitorOutput = "console" | "json";
8
+
9
+ export interface MonitorOptions {
10
+ follow?: boolean;
11
+ summary?: boolean;
12
+ since?: string;
13
+ file?: string;
14
+ }
15
+
16
+ interface MonitorEvent {
17
+ ts?: string;
18
+ type?: string;
19
+ severity?: "info" | "warn" | "error";
20
+ source?: string;
21
+ message?: string;
22
+ data?: Record<string, unknown>;
23
+ count?: number;
24
+ }
25
+
26
+ function parseDuration(value?: string): number | undefined {
27
+ if (!value) return undefined;
28
+ const trimmed = value.trim();
29
+ const match = trimmed.match(/^(\d+)(ms|s|m|h|d)?$/);
30
+ if (!match) return undefined;
31
+ const amount = Number(match[1]);
32
+ const unit = match[2] ?? "m";
33
+ switch (unit) {
34
+ case "ms":
35
+ return amount;
36
+ case "s":
37
+ return amount * 1000;
38
+ case "m":
39
+ return amount * 60 * 1000;
40
+ case "h":
41
+ return amount * 60 * 60 * 1000;
42
+ case "d":
43
+ return amount * 24 * 60 * 60 * 1000;
44
+ default:
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ function formatTime(ts?: string): string {
50
+ const date = ts ? new Date(ts) : new Date();
51
+ return date.toLocaleTimeString("ko-KR", { hour12: false });
52
+ }
53
+
54
+ function formatEventForConsole(event: MonitorEvent): string {
55
+ const time = formatTime(event.ts);
56
+ const countSuffix = event.count && event.count > 1 ? ` x${event.count}` : "";
57
+ const type = event.type ?? "event";
58
+
59
+ if (type === "tool.call") {
60
+ const tag = (event.data?.tag as string | undefined) ?? "TOOL";
61
+ const argsSummary = event.data?.argsSummary as string | undefined;
62
+ return `${time} → [${tag}]${argsSummary ?? ""}${countSuffix}`;
63
+ }
64
+ if (type === "tool.error") {
65
+ const tag = (event.data?.tag as string | undefined) ?? "TOOL";
66
+ const argsSummary = event.data?.argsSummary as string | undefined;
67
+ const message = event.message ?? "ERROR";
68
+ return `${time} ✗ [${tag}]${argsSummary ?? ""}${countSuffix}\n ${message}`;
69
+ }
70
+ if (type === "tool.result") {
71
+ const tag = (event.data?.tag as string | undefined) ?? "TOOL";
72
+ const summary = event.data?.summary as string | undefined;
73
+ return `${time} ✓ [${tag}]${summary ?? ""}${countSuffix}`;
74
+ }
75
+ if (type === "watch.warning") {
76
+ const ruleId = event.data?.ruleId as string | undefined;
77
+ const file = event.data?.file as string | undefined;
78
+ const message = event.message ?? "";
79
+ const icon = event.severity === "info" ? "ℹ" : "⚠";
80
+ return `${time} ${icon} [WATCH:${ruleId ?? "UNKNOWN"}] ${file ?? ""}${countSuffix}\n ${message}`;
81
+ }
82
+ if (type === "guard.violation") {
83
+ const ruleId = event.data?.ruleId as string | undefined;
84
+ const file = event.data?.file as string | undefined;
85
+ const line = event.data?.line as number | undefined;
86
+ const message = event.message ?? (event.data?.message as string | undefined) ?? "";
87
+ const location = line ? `${file}:${line}` : file ?? "";
88
+ return `${time} 🚨 [GUARD:${ruleId ?? "UNKNOWN"}] ${location}${countSuffix}\n ${message}`;
89
+ }
90
+ if (type === "guard.summary") {
91
+ const count = event.data?.count as number | undefined;
92
+ const passed = event.data?.passed as boolean | undefined;
93
+ return `${time} 🧱 [GUARD] ${passed ? "PASSED" : "FAILED"} (${count ?? 0} violations)`;
94
+ }
95
+ if (type === "routes.change") {
96
+ const action = event.data?.action as string | undefined;
97
+ const routeId = event.data?.routeId as string | undefined;
98
+ const pattern = event.data?.pattern as string | undefined;
99
+ const kind = event.data?.kind as string | undefined;
100
+ const detail = [routeId, pattern, kind].filter(Boolean).join(" ");
101
+ return `${time} 🛣️ [ROUTES:${action ?? "change"}] ${detail}${countSuffix}`;
102
+ }
103
+ if (type === "monitor.summary") {
104
+ return `${time} · SUMMARY ${event.message ?? ""}`;
105
+ }
106
+ if (type === "system.event") {
107
+ const category = event.data?.category as string | undefined;
108
+ return `${time} [${category ?? "SYSTEM"}] ${event.message ?? ""}${countSuffix}`;
109
+ }
110
+
111
+ return `${time} [${type}] ${event.message ?? ""}${countSuffix}`;
112
+ }
113
+
114
+ async function resolveLogFile(
115
+ rootDir: string,
116
+ output: MonitorOutput,
117
+ explicit?: string
118
+ ): Promise<string | null> {
119
+ if (explicit) return explicit;
120
+
121
+ const manduDir = path.join(rootDir, ".mandu");
122
+ const jsonPath = path.join(manduDir, "activity.jsonl");
123
+ const logPath = path.join(manduDir, "activity.log");
124
+
125
+ const hasJson = await pathExists(jsonPath);
126
+ const hasLog = await pathExists(logPath);
127
+
128
+ if (output === "json") {
129
+ if (hasJson) return jsonPath;
130
+ if (hasLog) return logPath;
131
+ } else {
132
+ if (hasLog) return logPath;
133
+ if (hasJson) return jsonPath;
134
+ }
135
+
136
+ return null;
137
+ }
138
+
139
+ async function readSummary(
140
+ filePath: string,
141
+ sinceMs: number
142
+ ): Promise<{
143
+ windowMs: number;
144
+ total: number;
145
+ bySeverity: { info: number; warn: number; error: number };
146
+ byType: Record<string, number>;
147
+ }> {
148
+ const content = await fs.readFile(filePath, "utf-8");
149
+ const lines = content.split("\n").filter(Boolean);
150
+ const cutoff = Date.now() - sinceMs;
151
+ const counts = { total: 0, info: 0, warn: 0, error: 0 };
152
+ const byType: Record<string, number> = {};
153
+
154
+ for (const line of lines) {
155
+ try {
156
+ const event = JSON.parse(line) as MonitorEvent;
157
+ if (!event.ts) continue;
158
+ const ts = new Date(event.ts).getTime();
159
+ if (Number.isNaN(ts) || ts < cutoff) continue;
160
+ const count = event.count ?? 1;
161
+ counts.total += count;
162
+ if (event.severity) {
163
+ counts[event.severity] += count;
164
+ }
165
+ const type = event.type ?? "event";
166
+ byType[type] = (byType[type] ?? 0) + count;
167
+ } catch {
168
+ // ignore parse errors
169
+ }
170
+ }
171
+
172
+ return { windowMs: sinceMs, total: counts.total, bySeverity: counts, byType };
173
+ }
174
+
175
+ function printSummaryConsole(summary: {
176
+ windowMs: number;
177
+ total: number;
178
+ bySeverity: { info: number; warn: number; error: number };
179
+ byType: Record<string, number>;
180
+ }): void {
181
+ const seconds = Math.round(summary.windowMs / 1000);
182
+ const topTypes = Object.entries(summary.byType)
183
+ .sort((a, b) => b[1] - a[1])
184
+ .slice(0, 5)
185
+ .map(([type, count]) => `${type}=${count}`)
186
+ .join(", ");
187
+
188
+ console.log(`Summary (last ${seconds}s)`);
189
+ console.log(` total=${summary.total}`);
190
+ console.log(` error=${summary.bySeverity.error} warn=${summary.bySeverity.warn} info=${summary.bySeverity.info}`);
191
+ if (topTypes) {
192
+ console.log(` top=${topTypes}`);
193
+ }
194
+ }
195
+
196
+ function outputChunk(
197
+ chunk: string,
198
+ isJson: boolean,
199
+ output: MonitorOutput
200
+ ): void {
201
+ if (!isJson || output === "json") {
202
+ process.stdout.write(chunk);
203
+ return;
204
+ }
205
+
206
+ const lines = chunk.split("\n").filter(Boolean);
207
+ for (const line of lines) {
208
+ try {
209
+ const event = JSON.parse(line) as MonitorEvent;
210
+ const formatted = formatEventForConsole(event);
211
+ process.stdout.write(`${formatted}\n`);
212
+ } catch {
213
+ process.stdout.write(`${line}\n`);
214
+ }
215
+ }
216
+ }
217
+
218
+ async function followFile(
219
+ filePath: string,
220
+ isJson: boolean,
221
+ output: MonitorOutput,
222
+ startAtEnd: boolean
223
+ ): Promise<void> {
224
+ let position = 0;
225
+ let buffer = "";
226
+
227
+ try {
228
+ const stat = await fs.stat(filePath);
229
+ position = startAtEnd ? stat.size : 0;
230
+ } catch {
231
+ position = 0;
232
+ }
233
+
234
+ const fd = await fs.open(filePath, "r");
235
+
236
+ fsSync.watchFile(
237
+ filePath,
238
+ { interval: 500 },
239
+ async (curr) => {
240
+ if (curr.size < position) {
241
+ position = 0;
242
+ buffer = "";
243
+ }
244
+ if (curr.size === position) {
245
+ return;
246
+ }
247
+
248
+ const length = curr.size - position;
249
+ const chunk = Buffer.alloc(length);
250
+ await fd.read(chunk, 0, length, position);
251
+ position = curr.size;
252
+ buffer += chunk.toString("utf-8");
253
+
254
+ const lines = buffer.split("\n");
255
+ buffer = lines.pop() ?? "";
256
+ if (lines.length > 0) {
257
+ outputChunk(lines.join("\n"), isJson, output);
258
+ }
259
+ }
260
+ );
261
+ }
262
+
263
+ export async function monitor(options: MonitorOptions = {}): Promise<boolean> {
264
+ const rootDir = resolveFromCwd(".");
265
+ const resolved = resolveOutputFormat();
266
+ const output: MonitorOutput = resolved === "json" || resolved === "agent" ? "json" : "console";
267
+ const filePath = await resolveLogFile(rootDir, output, options.file);
268
+
269
+ if (!filePath) {
270
+ console.error("❌ activity log 파일을 찾을 수 없습니다. (.mandu/activity.log 또는 activity.jsonl)");
271
+ return false;
272
+ }
273
+
274
+ const isJson = filePath.endsWith(".jsonl");
275
+ const follow = options.follow !== false;
276
+
277
+ if (options.summary) {
278
+ if (!isJson) {
279
+ console.error("⚠️ summary는 JSON 로그(activity.jsonl)에서만 가능합니다.");
280
+ } else {
281
+ const windowMs = parseDuration(options.since) ?? 5 * 60 * 1000;
282
+ const summary = await readSummary(filePath, windowMs);
283
+ if (output === "json") {
284
+ console.log(JSON.stringify(summary, null, 2));
285
+ } else {
286
+ printSummaryConsole(summary);
287
+ }
288
+ }
289
+ if (!follow) return true;
290
+ }
291
+
292
+ if (!follow) {
293
+ const content = await fs.readFile(filePath, "utf-8");
294
+ outputChunk(content, isJson, output);
295
+ return true;
296
+ }
297
+
298
+ await followFile(filePath, isJson, output, true);
299
+ return new Promise(() => {});
300
+ }