@mandujs/core 0.9.11 → 0.9.12
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/watcher/rules.ts +1 -1
- package/src/watcher/watcher.ts +134 -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.12",
|
|
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/watcher/rules.ts
CHANGED
|
@@ -227,7 +227,7 @@ export async function validateFile(
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
// Default: generate warning for pattern match
|
|
230
|
-
if (rule.id === "GENERATED_DIRECT_EDIT" || rule.id === "WRONG_SLOT_LOCATION") {
|
|
230
|
+
if (rule.id === "GENERATED_DIRECT_EDIT" || rule.id === "WRONG_SLOT_LOCATION" || rule.id === "ISLAND_FIRST_MODIFIED") {
|
|
231
231
|
warnings.push({
|
|
232
232
|
ruleId: rule.id,
|
|
233
233
|
file: relativePath,
|
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.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,83 @@ 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
|
+
// Auto-open terminal window to tail the log
|
|
109
|
+
this.openLogTerminal(this.logFile, rootDir);
|
|
110
|
+
|
|
111
|
+
// Build sets for fast lookup
|
|
112
|
+
const ignoredSet = new Set(ignoreDirs || []);
|
|
113
|
+
const extSet = new Set(watchExtensions || []);
|
|
114
|
+
|
|
115
|
+
// Start chokidar watcher
|
|
116
|
+
this.chokidarWatcher = chokidar.watch(rootDir, {
|
|
117
|
+
ignored: (filePath, stats) => {
|
|
118
|
+
const basename = path.basename(filePath);
|
|
119
|
+
// Ignore directories in the ignore list
|
|
120
|
+
if (ignoredSet.has(basename)) return true;
|
|
121
|
+
// For files, only watch matching extensions
|
|
122
|
+
if (stats?.isFile() && extSet.size > 0) {
|
|
123
|
+
const ext = path.extname(filePath);
|
|
124
|
+
return !extSet.has(ext);
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
},
|
|
128
|
+
ignoreInitial: true,
|
|
129
|
+
awaitWriteFinish: {
|
|
130
|
+
stabilityThreshold: debounceMs ?? 300,
|
|
131
|
+
pollInterval: 100,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Count initial files
|
|
136
|
+
this.chokidarWatcher.on("ready", () => {
|
|
137
|
+
const watched = this.chokidarWatcher?.getWatched() ?? {};
|
|
138
|
+
let count = 0;
|
|
139
|
+
for (const files of Object.values(watched)) {
|
|
140
|
+
count += files.length;
|
|
141
|
+
}
|
|
142
|
+
this._fileCount = count;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Handle events (v5 passes absolute paths when watching absolute rootDir)
|
|
146
|
+
this.chokidarWatcher.on("change", (filePath) => {
|
|
147
|
+
this.processFileEvent("modify", filePath);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
this.chokidarWatcher.on("add", (filePath) => {
|
|
151
|
+
this._fileCount++;
|
|
152
|
+
this.processFileEvent("create", filePath);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
this.chokidarWatcher.on("unlink", (filePath) => {
|
|
156
|
+
this._fileCount = Math.max(0, this._fileCount - 1);
|
|
157
|
+
this.processFileEvent("delete", filePath);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
this.chokidarWatcher.on("error", (error) => {
|
|
161
|
+
console.error(`[Watch] Error:`, error.message);
|
|
162
|
+
});
|
|
82
163
|
|
|
83
164
|
this._active = true;
|
|
84
165
|
this._startedAt = new Date();
|
|
@@ -87,22 +168,28 @@ export class FileWatcher {
|
|
|
87
168
|
/**
|
|
88
169
|
* Stop watching
|
|
89
170
|
*/
|
|
90
|
-
stop(): void {
|
|
171
|
+
async stop(): Promise<void> {
|
|
91
172
|
if (!this._active) {
|
|
92
173
|
return;
|
|
93
174
|
}
|
|
94
175
|
|
|
95
|
-
// Close
|
|
96
|
-
|
|
97
|
-
|
|
176
|
+
// Close tail terminal process
|
|
177
|
+
if (this.tailProcess) {
|
|
178
|
+
this.tailProcess.kill();
|
|
179
|
+
this.tailProcess = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Close log stream
|
|
183
|
+
if (this.logStream) {
|
|
184
|
+
this.logStream.end();
|
|
185
|
+
this.logStream = null;
|
|
98
186
|
}
|
|
99
|
-
this.watchers.clear();
|
|
100
187
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
188
|
+
// Close chokidar watcher (async in v5)
|
|
189
|
+
if (this.chokidarWatcher) {
|
|
190
|
+
await this.chokidarWatcher.close();
|
|
191
|
+
this.chokidarWatcher = null;
|
|
104
192
|
}
|
|
105
|
-
this.debounceTimers.clear();
|
|
106
193
|
|
|
107
194
|
this._active = false;
|
|
108
195
|
this._fileCount = 0;
|
|
@@ -144,94 +231,44 @@ export class FileWatcher {
|
|
|
144
231
|
}
|
|
145
232
|
|
|
146
233
|
/**
|
|
147
|
-
*
|
|
234
|
+
* Open a new terminal window tailing the log file
|
|
148
235
|
*/
|
|
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
|
-
|
|
236
|
+
private openLogTerminal(logFile: string, cwd: string): void {
|
|
158
237
|
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
|
-
}
|
|
238
|
+
if (process.platform === "win32") {
|
|
239
|
+
// Windows: open new cmd window with PowerShell Get-Content -Wait
|
|
240
|
+
this.tailProcess = spawn("cmd", [
|
|
241
|
+
"/c", "start",
|
|
242
|
+
"Mandu Watch",
|
|
243
|
+
"powershell", "-NoExit", "-Command",
|
|
244
|
+
`[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; chcp 65001 | Out-Null; Get-Content '${logFile}' -Wait -Encoding UTF8`,
|
|
245
|
+
], { cwd, detached: true, stdio: "ignore" });
|
|
246
|
+
} else if (process.platform === "darwin") {
|
|
247
|
+
// macOS: open new Terminal.app tab
|
|
248
|
+
this.tailProcess = spawn("osascript", [
|
|
249
|
+
"-e", `tell application "Terminal" to do script "tail -f '${logFile}'"`,
|
|
250
|
+
], { detached: true, stdio: "ignore" });
|
|
251
|
+
} else {
|
|
252
|
+
// Linux: try common terminal emulators
|
|
253
|
+
this.tailProcess = spawn("x-terminal-emulator", [
|
|
254
|
+
"-e", `tail -f '${logFile}'`,
|
|
255
|
+
], { cwd, detached: true, stdio: "ignore" });
|
|
180
256
|
}
|
|
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);
|
|
257
|
+
this.tailProcess?.unref();
|
|
258
|
+
} catch {
|
|
259
|
+
// Terminal auto-open failed silently — user can still tail manually
|
|
206
260
|
}
|
|
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
261
|
}
|
|
215
262
|
|
|
216
263
|
/**
|
|
217
|
-
* Process a
|
|
264
|
+
* Process a file event
|
|
218
265
|
*/
|
|
219
266
|
private async processFileEvent(
|
|
220
|
-
|
|
267
|
+
event: "create" | "modify" | "delete",
|
|
221
268
|
filePath: string
|
|
222
269
|
): Promise<void> {
|
|
223
270
|
const { rootDir } = this.config;
|
|
224
271
|
|
|
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
272
|
// Validate file against rules
|
|
236
273
|
try {
|
|
237
274
|
const warnings = await validateFile(filePath, event, rootDir);
|
|
@@ -251,6 +288,11 @@ export class FileWatcher {
|
|
|
251
288
|
* Emit a warning to all handlers
|
|
252
289
|
*/
|
|
253
290
|
private emitWarning(warning: WatchWarning): void {
|
|
291
|
+
// Write to log file
|
|
292
|
+
if (this.logStream) {
|
|
293
|
+
this.logStream.write(formatWarning(warning));
|
|
294
|
+
}
|
|
295
|
+
|
|
254
296
|
// Add to recent warnings
|
|
255
297
|
this.recentWarnings.push(warning);
|
|
256
298
|
|
|
@@ -322,9 +364,9 @@ export async function startWatcher(config: WatcherConfig): Promise<FileWatcher>
|
|
|
322
364
|
/**
|
|
323
365
|
* Stop the global watcher
|
|
324
366
|
*/
|
|
325
|
-
export function stopWatcher(): void {
|
|
367
|
+
export async function stopWatcher(): Promise<void> {
|
|
326
368
|
if (globalWatcher) {
|
|
327
|
-
globalWatcher.stop();
|
|
369
|
+
await globalWatcher.stop();
|
|
328
370
|
globalWatcher = null;
|
|
329
371
|
}
|
|
330
372
|
}
|