@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 CHANGED
@@ -1,52 +1,53 @@
1
- {
2
- "name": "@mandujs/core",
3
- "version": "0.9.11",
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
- "ollama": "^0.6.3"
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
+ }
@@ -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,
@@ -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 native file system watching for efficiency.
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 watchers: Map<string, fs.FSWatcher> = new Map();
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
- // Watch the root directory and subdirectories
81
- await this.watchDirectory(rootDir);
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 all watchers
96
- for (const [, watcher] of this.watchers) {
97
- watcher.close();
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
- // Clear debounce timers
102
- for (const [, timer] of this.debounceTimers) {
103
- clearTimeout(timer);
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
- * Watch a directory recursively
234
+ * Open a new terminal window tailing the log file
148
235
  */
149
- private async watchDirectory(dir: string): Promise<void> {
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
- // Watch this directory
160
- const watcher = fs.watch(dir, (eventType, filename) => {
161
- if (filename) {
162
- this.handleFileEvent(eventType, path.join(dir, filename));
163
- }
164
- });
165
-
166
- watcher.on("error", (error) => {
167
- console.error(`[Watch] Error watching ${dir}:`, error.message);
168
- });
169
-
170
- this.watchers.set(dir, watcher);
171
- this._fileCount++;
172
-
173
- // Recursively watch subdirectories
174
- const entries = fs.readdirSync(dir, { withFileTypes: true });
175
-
176
- for (const entry of entries) {
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
- } catch (error) {
182
- // Directory might not exist or be accessible
183
- console.error(
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 debounced file event
264
+ * Process a file event
218
265
  */
219
266
  private async processFileEvent(
220
- eventType: string,
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
  }