@mandujs/cli 0.9.20 → 0.9.22
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/README.md +15 -0
- package/package.json +2 -2
- package/src/commands/monitor.ts +280 -0
- package/src/main.ts +35 -17
- package/templates/default/package.json +6 -6
package/README.md
CHANGED
|
@@ -109,6 +109,7 @@ That's it!
|
|
|
109
109
|
|---------|-------------|
|
|
110
110
|
| `mandu doctor` | Analyze Guard failures + suggest patches |
|
|
111
111
|
| `mandu watch` | Real-time file monitoring |
|
|
112
|
+
| `mandu monitor` | MCP Activity Monitor log stream |
|
|
112
113
|
| `mandu brain setup` | Configure sLLM (optional) |
|
|
113
114
|
| `mandu brain status` | Check Brain status |
|
|
114
115
|
|
|
@@ -256,6 +257,16 @@ bunx mandu guard arch --preset fsd
|
|
|
256
257
|
| `--no-llm` | Template mode (no LLM) |
|
|
257
258
|
| `--output <path>` | Output file path |
|
|
258
259
|
|
|
260
|
+
### `mandu monitor`
|
|
261
|
+
|
|
262
|
+
| Option | Description |
|
|
263
|
+
|--------|-------------|
|
|
264
|
+
| `--format <f>` | Output: console, agent, json |
|
|
265
|
+
| `--summary` | Print summary (JSON log only) |
|
|
266
|
+
| `--since <d>` | Summary window: 5m, 30s, 1h |
|
|
267
|
+
| `--follow <bool>` | Follow mode (default: true) |
|
|
268
|
+
| `--file <path>` | Use custom log file |
|
|
269
|
+
|
|
259
270
|
---
|
|
260
271
|
|
|
261
272
|
## Examples
|
|
@@ -287,6 +298,10 @@ bunx mandu change rollback
|
|
|
287
298
|
bunx mandu doctor
|
|
288
299
|
bunx mandu doctor --format json
|
|
289
300
|
|
|
301
|
+
# Monitor
|
|
302
|
+
bunx mandu monitor
|
|
303
|
+
bunx mandu monitor --summary --since 5m
|
|
304
|
+
|
|
290
305
|
# Build
|
|
291
306
|
bunx mandu build --minify --sourcemap
|
|
292
307
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.22",
|
|
4
4
|
"description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/main.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@mandujs/core": "0.9.
|
|
35
|
+
"@mandujs/core": "0.9.39"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"bun": ">=1.0.0"
|
|
@@ -0,0 +1,280 @@
|
|
|
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, type OutputFormat } from "../util/output";
|
|
6
|
+
|
|
7
|
+
type MonitorOutput = "console" | "json";
|
|
8
|
+
|
|
9
|
+
export interface MonitorOptions {
|
|
10
|
+
format?: OutputFormat;
|
|
11
|
+
follow?: boolean;
|
|
12
|
+
summary?: boolean;
|
|
13
|
+
since?: string;
|
|
14
|
+
file?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface MonitorEvent {
|
|
18
|
+
ts?: string;
|
|
19
|
+
type?: string;
|
|
20
|
+
severity?: "info" | "warn" | "error";
|
|
21
|
+
source?: string;
|
|
22
|
+
message?: string;
|
|
23
|
+
data?: Record<string, unknown>;
|
|
24
|
+
count?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseDuration(value?: string): number | undefined {
|
|
28
|
+
if (!value) return undefined;
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
const match = trimmed.match(/^(\d+)(ms|s|m|h|d)?$/);
|
|
31
|
+
if (!match) return undefined;
|
|
32
|
+
const amount = Number(match[1]);
|
|
33
|
+
const unit = match[2] ?? "m";
|
|
34
|
+
switch (unit) {
|
|
35
|
+
case "ms":
|
|
36
|
+
return amount;
|
|
37
|
+
case "s":
|
|
38
|
+
return amount * 1000;
|
|
39
|
+
case "m":
|
|
40
|
+
return amount * 60 * 1000;
|
|
41
|
+
case "h":
|
|
42
|
+
return amount * 60 * 60 * 1000;
|
|
43
|
+
case "d":
|
|
44
|
+
return amount * 24 * 60 * 60 * 1000;
|
|
45
|
+
default:
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatTime(ts?: string): string {
|
|
51
|
+
const date = ts ? new Date(ts) : new Date();
|
|
52
|
+
return date.toLocaleTimeString("ko-KR", { hour12: false });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatEventForConsole(event: MonitorEvent): string {
|
|
56
|
+
const time = formatTime(event.ts);
|
|
57
|
+
const countSuffix = event.count && event.count > 1 ? ` x${event.count}` : "";
|
|
58
|
+
const type = event.type ?? "event";
|
|
59
|
+
|
|
60
|
+
if (type === "tool.call") {
|
|
61
|
+
const tag = (event.data?.tag as string | undefined) ?? "TOOL";
|
|
62
|
+
const argsSummary = event.data?.argsSummary as string | undefined;
|
|
63
|
+
return `${time} → [${tag}]${argsSummary ?? ""}${countSuffix}`;
|
|
64
|
+
}
|
|
65
|
+
if (type === "tool.error") {
|
|
66
|
+
const tag = (event.data?.tag as string | undefined) ?? "TOOL";
|
|
67
|
+
const argsSummary = event.data?.argsSummary as string | undefined;
|
|
68
|
+
const message = event.message ?? "ERROR";
|
|
69
|
+
return `${time} ✗ [${tag}]${argsSummary ?? ""}${countSuffix}\n ${message}`;
|
|
70
|
+
}
|
|
71
|
+
if (type === "tool.result") {
|
|
72
|
+
const tag = (event.data?.tag as string | undefined) ?? "TOOL";
|
|
73
|
+
const summary = event.data?.summary as string | undefined;
|
|
74
|
+
return `${time} ✓ [${tag}]${summary ?? ""}${countSuffix}`;
|
|
75
|
+
}
|
|
76
|
+
if (type === "watch.warning") {
|
|
77
|
+
const ruleId = event.data?.ruleId as string | undefined;
|
|
78
|
+
const file = event.data?.file as string | undefined;
|
|
79
|
+
const message = event.message ?? "";
|
|
80
|
+
const icon = event.severity === "info" ? "ℹ" : "⚠";
|
|
81
|
+
return `${time} ${icon} [WATCH:${ruleId ?? "UNKNOWN"}] ${file ?? ""}${countSuffix}\n ${message}`;
|
|
82
|
+
}
|
|
83
|
+
if (type === "monitor.summary") {
|
|
84
|
+
return `${time} · SUMMARY ${event.message ?? ""}`;
|
|
85
|
+
}
|
|
86
|
+
if (type === "system.event") {
|
|
87
|
+
const category = event.data?.category as string | undefined;
|
|
88
|
+
return `${time} [${category ?? "SYSTEM"}] ${event.message ?? ""}${countSuffix}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return `${time} [${type}] ${event.message ?? ""}${countSuffix}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function resolveLogFile(
|
|
95
|
+
rootDir: string,
|
|
96
|
+
output: MonitorOutput,
|
|
97
|
+
explicit?: string
|
|
98
|
+
): Promise<string | null> {
|
|
99
|
+
if (explicit) return explicit;
|
|
100
|
+
|
|
101
|
+
const manduDir = path.join(rootDir, ".mandu");
|
|
102
|
+
const jsonPath = path.join(manduDir, "activity.jsonl");
|
|
103
|
+
const logPath = path.join(manduDir, "activity.log");
|
|
104
|
+
|
|
105
|
+
const hasJson = await pathExists(jsonPath);
|
|
106
|
+
const hasLog = await pathExists(logPath);
|
|
107
|
+
|
|
108
|
+
if (output === "json") {
|
|
109
|
+
if (hasJson) return jsonPath;
|
|
110
|
+
if (hasLog) return logPath;
|
|
111
|
+
} else {
|
|
112
|
+
if (hasLog) return logPath;
|
|
113
|
+
if (hasJson) return jsonPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function readSummary(
|
|
120
|
+
filePath: string,
|
|
121
|
+
sinceMs: number
|
|
122
|
+
): Promise<{
|
|
123
|
+
windowMs: number;
|
|
124
|
+
total: number;
|
|
125
|
+
bySeverity: { info: number; warn: number; error: number };
|
|
126
|
+
byType: Record<string, number>;
|
|
127
|
+
}> {
|
|
128
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
129
|
+
const lines = content.split("\n").filter(Boolean);
|
|
130
|
+
const cutoff = Date.now() - sinceMs;
|
|
131
|
+
const counts = { total: 0, info: 0, warn: 0, error: 0 };
|
|
132
|
+
const byType: Record<string, number> = {};
|
|
133
|
+
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
try {
|
|
136
|
+
const event = JSON.parse(line) as MonitorEvent;
|
|
137
|
+
if (!event.ts) continue;
|
|
138
|
+
const ts = new Date(event.ts).getTime();
|
|
139
|
+
if (Number.isNaN(ts) || ts < cutoff) continue;
|
|
140
|
+
const count = event.count ?? 1;
|
|
141
|
+
counts.total += count;
|
|
142
|
+
if (event.severity) {
|
|
143
|
+
counts[event.severity] += count;
|
|
144
|
+
}
|
|
145
|
+
const type = event.type ?? "event";
|
|
146
|
+
byType[type] = (byType[type] ?? 0) + count;
|
|
147
|
+
} catch {
|
|
148
|
+
// ignore parse errors
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { windowMs: sinceMs, total: counts.total, bySeverity: counts, byType };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function printSummaryConsole(summary: {
|
|
156
|
+
windowMs: number;
|
|
157
|
+
total: number;
|
|
158
|
+
bySeverity: { info: number; warn: number; error: number };
|
|
159
|
+
byType: Record<string, number>;
|
|
160
|
+
}): void {
|
|
161
|
+
const seconds = Math.round(summary.windowMs / 1000);
|
|
162
|
+
const topTypes = Object.entries(summary.byType)
|
|
163
|
+
.sort((a, b) => b[1] - a[1])
|
|
164
|
+
.slice(0, 5)
|
|
165
|
+
.map(([type, count]) => `${type}=${count}`)
|
|
166
|
+
.join(", ");
|
|
167
|
+
|
|
168
|
+
console.log(`Summary (last ${seconds}s)`);
|
|
169
|
+
console.log(` total=${summary.total}`);
|
|
170
|
+
console.log(` error=${summary.bySeverity.error} warn=${summary.bySeverity.warn} info=${summary.bySeverity.info}`);
|
|
171
|
+
if (topTypes) {
|
|
172
|
+
console.log(` top=${topTypes}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function outputChunk(
|
|
177
|
+
chunk: string,
|
|
178
|
+
isJson: boolean,
|
|
179
|
+
output: MonitorOutput
|
|
180
|
+
): void {
|
|
181
|
+
if (!isJson || output === "json") {
|
|
182
|
+
process.stdout.write(chunk);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const lines = chunk.split("\n").filter(Boolean);
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
try {
|
|
189
|
+
const event = JSON.parse(line) as MonitorEvent;
|
|
190
|
+
const formatted = formatEventForConsole(event);
|
|
191
|
+
process.stdout.write(`${formatted}\n`);
|
|
192
|
+
} catch {
|
|
193
|
+
process.stdout.write(`${line}\n`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function followFile(
|
|
199
|
+
filePath: string,
|
|
200
|
+
isJson: boolean,
|
|
201
|
+
output: MonitorOutput,
|
|
202
|
+
startAtEnd: boolean
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
let position = 0;
|
|
205
|
+
let buffer = "";
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const stat = await fs.stat(filePath);
|
|
209
|
+
position = startAtEnd ? stat.size : 0;
|
|
210
|
+
} catch {
|
|
211
|
+
position = 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const fd = await fs.open(filePath, "r");
|
|
215
|
+
|
|
216
|
+
fsSync.watchFile(
|
|
217
|
+
filePath,
|
|
218
|
+
{ interval: 500 },
|
|
219
|
+
async (curr) => {
|
|
220
|
+
if (curr.size < position) {
|
|
221
|
+
position = 0;
|
|
222
|
+
buffer = "";
|
|
223
|
+
}
|
|
224
|
+
if (curr.size === position) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const length = curr.size - position;
|
|
229
|
+
const chunk = Buffer.alloc(length);
|
|
230
|
+
await fd.read(chunk, 0, length, position);
|
|
231
|
+
position = curr.size;
|
|
232
|
+
buffer += chunk.toString("utf-8");
|
|
233
|
+
|
|
234
|
+
const lines = buffer.split("\n");
|
|
235
|
+
buffer = lines.pop() ?? "";
|
|
236
|
+
if (lines.length > 0) {
|
|
237
|
+
outputChunk(lines.join("\n"), isJson, output);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function monitor(options: MonitorOptions = {}): Promise<boolean> {
|
|
244
|
+
const rootDir = resolveFromCwd(".");
|
|
245
|
+
const resolved = resolveOutputFormat(options.format);
|
|
246
|
+
const output: MonitorOutput = resolved === "json" || resolved === "agent" ? "json" : "console";
|
|
247
|
+
const filePath = await resolveLogFile(rootDir, output, options.file);
|
|
248
|
+
|
|
249
|
+
if (!filePath) {
|
|
250
|
+
console.error("❌ activity log 파일을 찾을 수 없습니다. (.mandu/activity.log 또는 activity.jsonl)");
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const isJson = filePath.endsWith(".jsonl");
|
|
255
|
+
const follow = options.follow !== false;
|
|
256
|
+
|
|
257
|
+
if (options.summary) {
|
|
258
|
+
if (!isJson) {
|
|
259
|
+
console.error("⚠️ summary는 JSON 로그(activity.jsonl)에서만 가능합니다.");
|
|
260
|
+
} else {
|
|
261
|
+
const windowMs = parseDuration(options.since) ?? 5 * 60 * 1000;
|
|
262
|
+
const summary = await readSummary(filePath, windowMs);
|
|
263
|
+
if (output === "json") {
|
|
264
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
265
|
+
} else {
|
|
266
|
+
printSummaryConsole(summary);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!follow) return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!follow) {
|
|
273
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
274
|
+
outputChunk(content, isJson, output);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
await followFile(filePath, isJson, output, true);
|
|
279
|
+
return new Promise(() => {});
|
|
280
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -19,9 +19,10 @@ import {
|
|
|
19
19
|
changePrune,
|
|
20
20
|
} from "./commands/change";
|
|
21
21
|
import { doctor } from "./commands/doctor";
|
|
22
|
-
import { watch } from "./commands/watch";
|
|
23
|
-
import { brainSetup, brainStatus } from "./commands/brain";
|
|
24
|
-
import { routesGenerate, routesList, routesWatch } from "./commands/routes";
|
|
22
|
+
import { watch } from "./commands/watch";
|
|
23
|
+
import { brainSetup, brainStatus } from "./commands/brain";
|
|
24
|
+
import { routesGenerate, routesList, routesWatch } from "./commands/routes";
|
|
25
|
+
import { monitor } from "./commands/monitor";
|
|
25
26
|
|
|
26
27
|
const HELP_TEXT = `
|
|
27
28
|
🥟 Mandu CLI - Agent-Native Fullstack Framework
|
|
@@ -47,9 +48,10 @@ Commands:
|
|
|
47
48
|
generate Spec에서 코드 생성 (레거시)
|
|
48
49
|
|
|
49
50
|
doctor Guard 실패 분석 + 패치 제안 (Brain)
|
|
50
|
-
watch 실시간 파일 감시 - 경고만 (Brain)
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
watch 실시간 파일 감시 - 경고만 (Brain)
|
|
52
|
+
monitor MCP Activity Monitor 로그 스트림
|
|
53
|
+
|
|
54
|
+
brain setup sLLM 설정 (선택)
|
|
53
55
|
brain status Brain 상태 확인
|
|
54
56
|
|
|
55
57
|
contract create <routeId> 라우트에 대한 Contract 생성
|
|
@@ -83,8 +85,12 @@ Options:
|
|
|
83
85
|
--show-trend guard arch 트렌드 분석 표시
|
|
84
86
|
--minify build 시 코드 압축
|
|
85
87
|
--sourcemap build 시 소스맵 생성
|
|
86
|
-
--watch build/guard arch 파일 감시 모드
|
|
87
|
-
--
|
|
88
|
+
--watch build/guard arch 파일 감시 모드
|
|
89
|
+
--summary monitor 요약 출력 (JSON 로그에서만)
|
|
90
|
+
--since <duration> monitor 요약 기간 (예: 5m, 30s, 1h)
|
|
91
|
+
--follow <bool> monitor follow 모드 (기본: true)
|
|
92
|
+
--file <path> monitor 로그 파일 직접 지정
|
|
93
|
+
--message <msg> change begin 시 설명 메시지
|
|
88
94
|
--id <id> change rollback 시 특정 변경 ID
|
|
89
95
|
--keep <n> change prune 시 유지할 스냅샷 수 (기본: 5)
|
|
90
96
|
--output <path> openapi/doctor 출력 경로
|
|
@@ -107,8 +113,10 @@ Examples:
|
|
|
107
113
|
bunx mandu guard
|
|
108
114
|
bunx mandu guard arch --preset fsd
|
|
109
115
|
bunx mandu guard arch --watch
|
|
110
|
-
bunx mandu guard arch --ci --format json
|
|
111
|
-
bunx mandu
|
|
116
|
+
bunx mandu guard arch --ci --format json
|
|
117
|
+
bunx mandu monitor
|
|
118
|
+
bunx mandu monitor --summary --since 5m
|
|
119
|
+
bunx mandu doctor
|
|
112
120
|
bunx mandu brain setup --model codellama
|
|
113
121
|
bunx mandu contract create users
|
|
114
122
|
bunx mandu openapi generate --output docs/api.json
|
|
@@ -375,13 +383,23 @@ async function main(): Promise<void> {
|
|
|
375
383
|
});
|
|
376
384
|
break;
|
|
377
385
|
|
|
378
|
-
case "watch":
|
|
379
|
-
success = await watch({
|
|
380
|
-
status: options.status === "true",
|
|
381
|
-
debounce: options.debounce ? Number(options.debounce) : undefined,
|
|
382
|
-
});
|
|
383
|
-
break;
|
|
384
|
-
|
|
386
|
+
case "watch":
|
|
387
|
+
success = await watch({
|
|
388
|
+
status: options.status === "true",
|
|
389
|
+
debounce: options.debounce ? Number(options.debounce) : undefined,
|
|
390
|
+
});
|
|
391
|
+
break;
|
|
392
|
+
|
|
393
|
+
case "monitor":
|
|
394
|
+
success = await monitor({
|
|
395
|
+
format: options.format as any,
|
|
396
|
+
summary: options.summary === "true",
|
|
397
|
+
since: options.since,
|
|
398
|
+
follow: options.follow === "false" ? false : true,
|
|
399
|
+
file: options.file,
|
|
400
|
+
});
|
|
401
|
+
break;
|
|
402
|
+
|
|
385
403
|
case "brain": {
|
|
386
404
|
const subCommand = args[1];
|
|
387
405
|
switch (subCommand) {
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"test": "bun test"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@mandujs/core": "^0.9.
|
|
12
|
-
"react": "^
|
|
13
|
-
"react-dom": "^
|
|
11
|
+
"@mandujs/core": "^0.9.39",
|
|
12
|
+
"react": "^19.2.0",
|
|
13
|
+
"react-dom": "^19.2.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@mandujs/cli": "^0.9.
|
|
17
|
-
"@types/react": "^
|
|
18
|
-
"@types/react-dom": "^
|
|
16
|
+
"@mandujs/cli": "^0.9.21",
|
|
17
|
+
"@types/react": "^19.2.0",
|
|
18
|
+
"@types/react-dom": "^19.2.0",
|
|
19
19
|
"typescript": "^5.0.0"
|
|
20
20
|
}
|
|
21
21
|
}
|