@mandujs/core 0.9.11 → 0.9.13
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/package.json +53 -52
- package/src/brain/types.ts +2 -0
- package/src/watcher/rules.ts +21 -1
- package/src/watcher/watcher.ts +133 -92
package/package.json
CHANGED
|
@@ -1,52 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./src/index.ts",
|
|
7
|
-
"types": "./src/index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./src/index.ts",
|
|
10
|
-
"./client": "./src/client/index.ts",
|
|
11
|
-
"./*": "./src/*"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"src/**/*"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"test": "bun test",
|
|
18
|
-
"test:hydration": "bun test tests/hydration",
|
|
19
|
-
"test:watch": "bun test --watch"
|
|
20
|
-
},
|
|
21
|
-
"devDependencies": {
|
|
22
|
-
"@happy-dom/global-registrator": "^15.0.0"
|
|
23
|
-
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"mandu",
|
|
26
|
-
"framework",
|
|
27
|
-
"agent",
|
|
28
|
-
"ai",
|
|
29
|
-
"code-generation"
|
|
30
|
-
],
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/konamgil/mandu.git",
|
|
34
|
-
"directory": "packages/core"
|
|
35
|
-
},
|
|
36
|
-
"author": "konamgil",
|
|
37
|
-
"license": "MIT",
|
|
38
|
-
"publishConfig": {
|
|
39
|
-
"access": "public"
|
|
40
|
-
},
|
|
41
|
-
"engines": {
|
|
42
|
-
"bun": ">=1.0.0"
|
|
43
|
-
},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"react": ">=18.0.0",
|
|
46
|
-
"react-dom": ">=18.0.0",
|
|
47
|
-
"zod": ">=3.0.0"
|
|
48
|
-
},
|
|
49
|
-
"dependencies": {
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@mandujs/core",
|
|
3
|
+
"version": "0.9.13",
|
|
4
|
+
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./client": "./src/client/index.ts",
|
|
11
|
+
"./*": "./src/*"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src/**/*"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"test:hydration": "bun test tests/hydration",
|
|
19
|
+
"test:watch": "bun test --watch"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@happy-dom/global-registrator": "^15.0.0"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mandu",
|
|
26
|
+
"framework",
|
|
27
|
+
"agent",
|
|
28
|
+
"ai",
|
|
29
|
+
"code-generation"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/konamgil/mandu.git",
|
|
34
|
+
"directory": "packages/core"
|
|
35
|
+
},
|
|
36
|
+
"author": "konamgil",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"bun": ">=1.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": ">=18.0.0",
|
|
46
|
+
"react-dom": ">=18.0.0",
|
|
47
|
+
"zod": ">=3.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"chokidar": "^5.0.0",
|
|
51
|
+
"ollama": "^0.6.3"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/brain/types.ts
CHANGED
package/src/watcher/rules.ts
CHANGED
|
@@ -63,6 +63,14 @@ export const MVP_RULES: ArchRule[] = [
|
|
|
63
63
|
message: "Generated 파일에서 금지된 모듈이 import되었습니다.",
|
|
64
64
|
forbiddenImports: ["fs", "child_process", "cluster", "worker_threads"],
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
id: "SLOT_MODIFIED",
|
|
68
|
+
name: "Slot File Modified",
|
|
69
|
+
description: "Slot 파일이 수정되었습니다",
|
|
70
|
+
pattern: "spec/slots/*.slot.ts",
|
|
71
|
+
action: "warn",
|
|
72
|
+
message: "Slot 수정 감지. mandu_validate_slot 또는 mandu_guard_check로 검증하세요.",
|
|
73
|
+
},
|
|
66
74
|
{
|
|
67
75
|
id: "ISLAND_FIRST_MODIFIED",
|
|
68
76
|
name: "Island-First ComponentModule Modified",
|
|
@@ -227,13 +235,25 @@ export async function validateFile(
|
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
// Default: generate warning for pattern match
|
|
230
|
-
if (rule.id === "GENERATED_DIRECT_EDIT" || rule.id === "WRONG_SLOT_LOCATION") {
|
|
238
|
+
if (rule.id === "GENERATED_DIRECT_EDIT" || rule.id === "WRONG_SLOT_LOCATION" || rule.id === "ISLAND_FIRST_MODIFIED") {
|
|
239
|
+
warnings.push({
|
|
240
|
+
ruleId: rule.id,
|
|
241
|
+
file: relativePath,
|
|
242
|
+
message: rule.message,
|
|
243
|
+
timestamp: new Date(),
|
|
244
|
+
event,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Slot modified: info level notification
|
|
249
|
+
if (rule.id === "SLOT_MODIFIED" && event !== "delete") {
|
|
231
250
|
warnings.push({
|
|
232
251
|
ruleId: rule.id,
|
|
233
252
|
file: relativePath,
|
|
234
253
|
message: rule.message,
|
|
235
254
|
timestamp: new Date(),
|
|
236
255
|
event,
|
|
256
|
+
level: "info",
|
|
237
257
|
});
|
|
238
258
|
}
|
|
239
259
|
}
|
package/src/watcher/watcher.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Brain v0.1 - File Watcher
|
|
3
3
|
*
|
|
4
4
|
* Watches for file changes and triggers warnings (no blocking).
|
|
5
|
-
* Uses
|
|
5
|
+
* Uses chokidar for reliable cross-platform file system watching.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
@@ -13,6 +13,17 @@ import type {
|
|
|
13
13
|
import { validateFile, MVP_RULES } from "./rules";
|
|
14
14
|
import path from "path";
|
|
15
15
|
import fs from "fs";
|
|
16
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
17
|
+
import chokidar, { type FSWatcher } from "chokidar";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format a warning for log output
|
|
21
|
+
*/
|
|
22
|
+
function formatWarning(warning: WatchWarning): string {
|
|
23
|
+
const time = new Date().toLocaleTimeString("ko-KR", { hour12: false });
|
|
24
|
+
const icon = warning.level === "info" ? "[INFO]" : warning.event === "delete" ? "[DEL]" : "[WARN]";
|
|
25
|
+
return `${time} ${icon} ${warning.ruleId}\n ${warning.file}\n ${warning.message}\n`;
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
29
|
* Watcher configuration
|
|
@@ -47,13 +58,15 @@ const DEFAULT_CONFIG: Partial<WatcherConfig> = {
|
|
|
47
58
|
*/
|
|
48
59
|
export class FileWatcher {
|
|
49
60
|
private config: WatcherConfig;
|
|
50
|
-
private
|
|
61
|
+
private chokidarWatcher: FSWatcher | null = null;
|
|
51
62
|
private handlers: Set<WatchEventHandler> = new Set();
|
|
52
63
|
private recentWarnings: WatchWarning[] = [];
|
|
53
|
-
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
54
64
|
private _active: boolean = false;
|
|
55
65
|
private _startedAt: Date | null = null;
|
|
56
66
|
private _fileCount: number = 0;
|
|
67
|
+
private logFile: string | null = null;
|
|
68
|
+
private logStream: fs.WriteStream | null = null;
|
|
69
|
+
private tailProcess: ChildProcess | null = null;
|
|
57
70
|
|
|
58
71
|
constructor(config: WatcherConfig) {
|
|
59
72
|
this.config = {
|
|
@@ -70,15 +83,82 @@ export class FileWatcher {
|
|
|
70
83
|
return;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
const { rootDir } = this.config;
|
|
86
|
+
const { rootDir, ignoreDirs, watchExtensions, debounceMs } = this.config;
|
|
74
87
|
|
|
75
88
|
// Verify root directory exists
|
|
76
89
|
if (!fs.existsSync(rootDir)) {
|
|
77
90
|
throw new Error(`Root directory does not exist: ${rootDir}`);
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
//
|
|
81
|
-
|
|
93
|
+
// Setup log file at .mandu/watch.log
|
|
94
|
+
const manduDir = path.join(rootDir, ".mandu");
|
|
95
|
+
if (!fs.existsSync(manduDir)) {
|
|
96
|
+
fs.mkdirSync(manduDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
this.logFile = path.join(manduDir, "watch.log");
|
|
99
|
+
this.logStream = fs.createWriteStream(this.logFile, { flags: "w" });
|
|
100
|
+
const startTime = new Date().toLocaleTimeString("ko-KR", { hour12: false });
|
|
101
|
+
this.logStream.write(
|
|
102
|
+
`${"=".repeat(50)}\n` +
|
|
103
|
+
` Mandu Watch - ${startTime}\n` +
|
|
104
|
+
` Root: ${rootDir}\n` +
|
|
105
|
+
`${"=".repeat(50)}\n\n`
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Terminal is now handled by ActivityMonitor in MCP server
|
|
109
|
+
|
|
110
|
+
// Build sets for fast lookup
|
|
111
|
+
const ignoredSet = new Set(ignoreDirs || []);
|
|
112
|
+
const extSet = new Set(watchExtensions || []);
|
|
113
|
+
|
|
114
|
+
// Start chokidar watcher
|
|
115
|
+
this.chokidarWatcher = chokidar.watch(rootDir, {
|
|
116
|
+
ignored: (filePath, stats) => {
|
|
117
|
+
const basename = path.basename(filePath);
|
|
118
|
+
// Ignore directories in the ignore list
|
|
119
|
+
if (ignoredSet.has(basename)) return true;
|
|
120
|
+
// For files, only watch matching extensions
|
|
121
|
+
if (stats?.isFile() && extSet.size > 0) {
|
|
122
|
+
const ext = path.extname(filePath);
|
|
123
|
+
return !extSet.has(ext);
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
},
|
|
127
|
+
ignoreInitial: true,
|
|
128
|
+
awaitWriteFinish: {
|
|
129
|
+
stabilityThreshold: debounceMs ?? 300,
|
|
130
|
+
pollInterval: 100,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Count initial files
|
|
135
|
+
this.chokidarWatcher.on("ready", () => {
|
|
136
|
+
const watched = this.chokidarWatcher?.getWatched() ?? {};
|
|
137
|
+
let count = 0;
|
|
138
|
+
for (const files of Object.values(watched)) {
|
|
139
|
+
count += files.length;
|
|
140
|
+
}
|
|
141
|
+
this._fileCount = count;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Handle events (v5 passes absolute paths when watching absolute rootDir)
|
|
145
|
+
this.chokidarWatcher.on("change", (filePath) => {
|
|
146
|
+
this.processFileEvent("modify", filePath);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
this.chokidarWatcher.on("add", (filePath) => {
|
|
150
|
+
this._fileCount++;
|
|
151
|
+
this.processFileEvent("create", filePath);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
this.chokidarWatcher.on("unlink", (filePath) => {
|
|
155
|
+
this._fileCount = Math.max(0, this._fileCount - 1);
|
|
156
|
+
this.processFileEvent("delete", filePath);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.chokidarWatcher.on("error", (error) => {
|
|
160
|
+
console.error(`[Watch] Error:`, error.message);
|
|
161
|
+
});
|
|
82
162
|
|
|
83
163
|
this._active = true;
|
|
84
164
|
this._startedAt = new Date();
|
|
@@ -87,22 +167,28 @@ export class FileWatcher {
|
|
|
87
167
|
/**
|
|
88
168
|
* Stop watching
|
|
89
169
|
*/
|
|
90
|
-
stop(): void {
|
|
170
|
+
async stop(): Promise<void> {
|
|
91
171
|
if (!this._active) {
|
|
92
172
|
return;
|
|
93
173
|
}
|
|
94
174
|
|
|
95
|
-
// Close
|
|
96
|
-
|
|
97
|
-
|
|
175
|
+
// Close tail terminal process
|
|
176
|
+
if (this.tailProcess) {
|
|
177
|
+
this.tailProcess.kill();
|
|
178
|
+
this.tailProcess = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Close log stream
|
|
182
|
+
if (this.logStream) {
|
|
183
|
+
this.logStream.end();
|
|
184
|
+
this.logStream = null;
|
|
98
185
|
}
|
|
99
|
-
this.watchers.clear();
|
|
100
186
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
187
|
+
// Close chokidar watcher (async in v5)
|
|
188
|
+
if (this.chokidarWatcher) {
|
|
189
|
+
await this.chokidarWatcher.close();
|
|
190
|
+
this.chokidarWatcher = null;
|
|
104
191
|
}
|
|
105
|
-
this.debounceTimers.clear();
|
|
106
192
|
|
|
107
193
|
this._active = false;
|
|
108
194
|
this._fileCount = 0;
|
|
@@ -144,94 +230,44 @@ export class FileWatcher {
|
|
|
144
230
|
}
|
|
145
231
|
|
|
146
232
|
/**
|
|
147
|
-
*
|
|
233
|
+
* Open a new terminal window tailing the log file
|
|
148
234
|
*/
|
|
149
|
-
private
|
|
150
|
-
const { ignoreDirs } = this.config;
|
|
151
|
-
|
|
152
|
-
// Skip ignored directories
|
|
153
|
-
const dirName = path.basename(dir);
|
|
154
|
-
if (ignoreDirs?.includes(dirName)) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
235
|
+
private openLogTerminal(logFile: string, cwd: string): void {
|
|
158
236
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (entry.isDirectory() && !ignoreDirs?.includes(entry.name)) {
|
|
178
|
-
await this.watchDirectory(path.join(dir, entry.name));
|
|
179
|
-
}
|
|
237
|
+
if (process.platform === "win32") {
|
|
238
|
+
// Windows: open new cmd window with PowerShell Get-Content -Wait
|
|
239
|
+
this.tailProcess = spawn("cmd", [
|
|
240
|
+
"/c", "start",
|
|
241
|
+
"Mandu Watch",
|
|
242
|
+
"powershell", "-NoExit", "-Command",
|
|
243
|
+
`[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; chcp 65001 | Out-Null; Get-Content '${logFile}' -Wait -Encoding UTF8`,
|
|
244
|
+
], { cwd, detached: true, stdio: "ignore" });
|
|
245
|
+
} else if (process.platform === "darwin") {
|
|
246
|
+
// macOS: open new Terminal.app tab
|
|
247
|
+
this.tailProcess = spawn("osascript", [
|
|
248
|
+
"-e", `tell application "Terminal" to do script "tail -f '${logFile}'"`,
|
|
249
|
+
], { detached: true, stdio: "ignore" });
|
|
250
|
+
} else {
|
|
251
|
+
// Linux: try common terminal emulators
|
|
252
|
+
this.tailProcess = spawn("x-terminal-emulator", [
|
|
253
|
+
"-e", `tail -f '${logFile}'`,
|
|
254
|
+
], { cwd, detached: true, stdio: "ignore" });
|
|
180
255
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
`[Watch] Failed to watch ${dir}:`,
|
|
185
|
-
error instanceof Error ? error.message : error
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Handle a file system event
|
|
192
|
-
*/
|
|
193
|
-
private handleFileEvent(eventType: string, filePath: string): void {
|
|
194
|
-
const { debounceMs, watchExtensions } = this.config;
|
|
195
|
-
|
|
196
|
-
// Check file extension
|
|
197
|
-
const ext = path.extname(filePath);
|
|
198
|
-
if (watchExtensions && !watchExtensions.includes(ext)) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Debounce events for the same file
|
|
203
|
-
const existingTimer = this.debounceTimers.get(filePath);
|
|
204
|
-
if (existingTimer) {
|
|
205
|
-
clearTimeout(existingTimer);
|
|
256
|
+
this.tailProcess?.unref();
|
|
257
|
+
} catch {
|
|
258
|
+
// Terminal auto-open failed silently — user can still tail manually
|
|
206
259
|
}
|
|
207
|
-
|
|
208
|
-
const timer = setTimeout(() => {
|
|
209
|
-
this.debounceTimers.delete(filePath);
|
|
210
|
-
this.processFileEvent(eventType, filePath);
|
|
211
|
-
}, debounceMs);
|
|
212
|
-
|
|
213
|
-
this.debounceTimers.set(filePath, timer);
|
|
214
260
|
}
|
|
215
261
|
|
|
216
262
|
/**
|
|
217
|
-
* Process a
|
|
263
|
+
* Process a file event
|
|
218
264
|
*/
|
|
219
265
|
private async processFileEvent(
|
|
220
|
-
|
|
266
|
+
event: "create" | "modify" | "delete",
|
|
221
267
|
filePath: string
|
|
222
268
|
): Promise<void> {
|
|
223
269
|
const { rootDir } = this.config;
|
|
224
270
|
|
|
225
|
-
// Determine event type
|
|
226
|
-
let event: "create" | "modify" | "delete";
|
|
227
|
-
|
|
228
|
-
if (eventType === "rename") {
|
|
229
|
-
// Check if file exists to determine create vs delete
|
|
230
|
-
event = fs.existsSync(filePath) ? "create" : "delete";
|
|
231
|
-
} else {
|
|
232
|
-
event = "modify";
|
|
233
|
-
}
|
|
234
|
-
|
|
235
271
|
// Validate file against rules
|
|
236
272
|
try {
|
|
237
273
|
const warnings = await validateFile(filePath, event, rootDir);
|
|
@@ -251,6 +287,11 @@ export class FileWatcher {
|
|
|
251
287
|
* Emit a warning to all handlers
|
|
252
288
|
*/
|
|
253
289
|
private emitWarning(warning: WatchWarning): void {
|
|
290
|
+
// Write to log file
|
|
291
|
+
if (this.logStream) {
|
|
292
|
+
this.logStream.write(formatWarning(warning));
|
|
293
|
+
}
|
|
294
|
+
|
|
254
295
|
// Add to recent warnings
|
|
255
296
|
this.recentWarnings.push(warning);
|
|
256
297
|
|
|
@@ -322,9 +363,9 @@ export async function startWatcher(config: WatcherConfig): Promise<FileWatcher>
|
|
|
322
363
|
/**
|
|
323
364
|
* Stop the global watcher
|
|
324
365
|
*/
|
|
325
|
-
export function stopWatcher(): void {
|
|
366
|
+
export async function stopWatcher(): Promise<void> {
|
|
326
367
|
if (globalWatcher) {
|
|
327
|
-
globalWatcher.stop();
|
|
368
|
+
await globalWatcher.stop();
|
|
328
369
|
globalWatcher = null;
|
|
329
370
|
}
|
|
330
371
|
}
|