@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 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.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
+ }
@@ -162,6 +162,8 @@ export interface WatchWarning {
162
162
  timestamp: Date;
163
163
  /** Event type that triggered the warning */
164
164
  event: "create" | "modify" | "delete";
165
+ /** Warning level */
166
+ level?: "info" | "warn";
165
167
  }
166
168
 
167
169
  /**
@@ -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
  }
@@ -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.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 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,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
- // 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
+ // 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 all watchers
96
- for (const [, watcher] of this.watchers) {
97
- watcher.close();
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
- // Clear debounce timers
102
- for (const [, timer] of this.debounceTimers) {
103
- clearTimeout(timer);
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
- * Watch a directory recursively
233
+ * Open a new terminal window tailing the log file
148
234
  */
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
-
235
+ private openLogTerminal(logFile: string, cwd: string): void {
158
236
  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
- }
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
- } 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);
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 debounced file event
263
+ * Process a file event
218
264
  */
219
265
  private async processFileEvent(
220
- eventType: string,
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
  }