@planu/cli 4.1.2 → 4.1.3
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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## [4.1.3] - 2026-05-21
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- Make the TypeScript file watcher fallback portable across operating systems by avoiding non-portable recursive `fs.watch` mode.
|
|
5
|
+
- Keep the native-core fallback path non-fatal when a platform-specific binary is unavailable.
|
|
6
|
+
|
|
7
|
+
### Tests
|
|
8
|
+
- Add coverage to prevent reintroducing recursive watcher mode in the portable fallback.
|
|
9
|
+
|
|
10
|
+
|
|
1
11
|
## [4.1.2] - 2026-05-21
|
|
2
12
|
|
|
3
13
|
### Bug Fixes
|
|
@@ -180,6 +180,7 @@ const getNative = () => process.env.DISABLE_NATIVE_CORE === '1' ? null : nativeM
|
|
|
180
180
|
const PROJECT_ID_RE = /^[a-f0-9]{16,64}$/;
|
|
181
181
|
const SPEC_ANNOTATION_RE = /@spec\s+(SPEC-\d+)(?:\s+AC:([\d,]+))?(?:\s+(.*))?/;
|
|
182
182
|
const SCANNABLE_EXTS = new Set(['.ts', '.js', '.tsx', '.jsx', '.rs', '.py', '.go']);
|
|
183
|
+
const MAX_TS_WATCHERS = 128;
|
|
183
184
|
const IGNORED_DIRS = new Set([
|
|
184
185
|
'node_modules',
|
|
185
186
|
'.git',
|
|
@@ -671,11 +672,41 @@ export function startNativeWatcher(rootPath, callback) {
|
|
|
671
672
|
n.startProjectWatcher(rootPath, callback);
|
|
672
673
|
return;
|
|
673
674
|
}
|
|
674
|
-
// TS fallback:
|
|
675
|
+
// TS fallback: watch each existing directory. Node's `recursive: true` is not
|
|
676
|
+
// portable across all OS/libc combinations, and unsupported platforms must not
|
|
677
|
+
// crash the MCP server when the native watcher is unavailable.
|
|
675
678
|
const fs = nodeFs;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
+
let watcherCount = 0;
|
|
680
|
+
const watchDir = (absDir, relDir) => {
|
|
681
|
+
if (watcherCount >= MAX_TS_WATCHERS) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const watcher = fs.watch(absDir, {}, (event, filename) => {
|
|
685
|
+
callback(null, `${event}:${join(relDir, filename?.toString() ?? '')}`);
|
|
686
|
+
});
|
|
687
|
+
watcherCount++;
|
|
688
|
+
watcher.on('error', (err) => {
|
|
689
|
+
callback(err, '');
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
const walkDirs = (absDir, relDir) => {
|
|
693
|
+
watchDir(absDir, relDir);
|
|
694
|
+
let entries;
|
|
695
|
+
try {
|
|
696
|
+
entries = readdirSync(absDir, { withFileTypes: true });
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
for (const entry of entries) {
|
|
702
|
+
if (!entry.isDirectory() || IGNORED_DIRS.has(entry.name)) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const childRel = join(relDir, entry.name);
|
|
706
|
+
walkDirs(join(absDir, entry.name), childRel);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
walkDirs(rootPath, '');
|
|
679
710
|
}
|
|
680
711
|
export const isNativeActive = () => nativeModule !== null && process.env.DISABLE_NATIVE_CORE !== '1';
|
|
681
712
|
export const nativeLoadDiagnostic = () => loadDiagnostic;
|
|
@@ -18,9 +18,11 @@ export declare function matchesGlobPattern(filePath: string, pattern: string): b
|
|
|
18
18
|
export declare function matchesPatterns(filePath: string, include?: string[], exclude?: string[]): boolean;
|
|
19
19
|
export declare class FileWatcher extends EventEmitter {
|
|
20
20
|
private watcher;
|
|
21
|
+
private readonly watchers;
|
|
21
22
|
private readonly rootDir;
|
|
22
23
|
private readonly include;
|
|
23
24
|
private readonly exclude;
|
|
25
|
+
private readonly maxWatchers;
|
|
24
26
|
/** Track known files to distinguish create vs modify */
|
|
25
27
|
private readonly knownFiles;
|
|
26
28
|
private closed;
|
|
@@ -42,6 +44,10 @@ export declare class FileWatcher extends EventEmitter {
|
|
|
42
44
|
* Graceful shutdown: release all handles.
|
|
43
45
|
*/
|
|
44
46
|
close(): void;
|
|
47
|
+
private startTypeScriptWatcher;
|
|
48
|
+
private watchDirectory;
|
|
49
|
+
private watchSubdirectories;
|
|
50
|
+
private emitWatcherError;
|
|
45
51
|
private handleFsEvent;
|
|
46
52
|
private determineEventType;
|
|
47
53
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// engine/hooks/file-watcher.ts — File system watcher with normalized events (SPEC-129)
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { watch } from 'node:fs';
|
|
3
|
+
import { readdirSync, watch } from 'node:fs';
|
|
4
4
|
import { stat } from 'node:fs/promises';
|
|
5
5
|
import { normalize, join } from 'node:path';
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
@@ -73,15 +73,22 @@ export function matchesPatterns(filePath, include, exclude) {
|
|
|
73
73
|
// Default excludes
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
75
75
|
const DEFAULT_EXCLUDES = ['data/**', 'node_modules/**', '.git/**'];
|
|
76
|
+
const DEFAULT_MAX_TS_WATCHERS = 128;
|
|
77
|
+
function maxTypeScriptWatchers() {
|
|
78
|
+
const raw = Number(process.env.PLANU_MAX_TS_WATCHERS ?? DEFAULT_MAX_TS_WATCHERS);
|
|
79
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_MAX_TS_WATCHERS;
|
|
80
|
+
}
|
|
76
81
|
// ---------------------------------------------------------------------------
|
|
77
82
|
// FileWatcher
|
|
78
83
|
// ---------------------------------------------------------------------------
|
|
79
84
|
import { isNativeActive, startNativeWatcher } from '../core-bridge.js';
|
|
80
85
|
export class FileWatcher extends EventEmitter {
|
|
81
86
|
watcher = null;
|
|
87
|
+
watchers = new Set();
|
|
82
88
|
rootDir;
|
|
83
89
|
include;
|
|
84
90
|
exclude;
|
|
91
|
+
maxWatchers = maxTypeScriptWatchers();
|
|
85
92
|
/** Track known files to distinguish create vs modify */
|
|
86
93
|
knownFiles = new Set();
|
|
87
94
|
closed = false;
|
|
@@ -112,21 +119,11 @@ export class FileWatcher extends EventEmitter {
|
|
|
112
119
|
});
|
|
113
120
|
}
|
|
114
121
|
else {
|
|
115
|
-
this.
|
|
116
|
-
if (!filename || this.closed) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
// Normalize path separator
|
|
120
|
-
const relPath = filename.replace(/\\/g, '/');
|
|
121
|
-
void this.handleFsEvent(relPath);
|
|
122
|
-
});
|
|
123
|
-
this.watcher.on('error', (err) => {
|
|
124
|
-
this.emit('error', err);
|
|
125
|
-
});
|
|
122
|
+
this.startTypeScriptWatcher();
|
|
126
123
|
}
|
|
127
124
|
}
|
|
128
125
|
catch (err) {
|
|
129
|
-
this.
|
|
126
|
+
this.emitWatcherError(err);
|
|
130
127
|
}
|
|
131
128
|
}
|
|
132
129
|
/**
|
|
@@ -148,15 +145,71 @@ export class FileWatcher extends EventEmitter {
|
|
|
148
145
|
*/
|
|
149
146
|
close() {
|
|
150
147
|
this.closed = true;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
this.watcher = null;
|
|
148
|
+
for (const watcher of this.watchers) {
|
|
149
|
+
watcher.close();
|
|
154
150
|
}
|
|
151
|
+
this.watchers.clear();
|
|
152
|
+
this.watcher = null;
|
|
155
153
|
this.removeAllListeners();
|
|
156
154
|
}
|
|
157
155
|
// ---------------------------------------------------------------------------
|
|
158
156
|
// Private
|
|
159
157
|
// ---------------------------------------------------------------------------
|
|
158
|
+
startTypeScriptWatcher() {
|
|
159
|
+
this.watchDirectory(this.rootDir, '');
|
|
160
|
+
this.watchSubdirectories(this.rootDir, '');
|
|
161
|
+
}
|
|
162
|
+
watchDirectory(absDir, relDir) {
|
|
163
|
+
const watcher = watch(absDir, {}, (_eventType, filename) => {
|
|
164
|
+
if (!filename || this.closed) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const relPath = join(relDir, filename).replace(/\\/g, '/');
|
|
168
|
+
void this.handleFsEvent(relPath);
|
|
169
|
+
});
|
|
170
|
+
watcher.on('error', (err) => {
|
|
171
|
+
this.emitWatcherError(err);
|
|
172
|
+
});
|
|
173
|
+
this.watchers.add(watcher);
|
|
174
|
+
this.watcher ??= watcher;
|
|
175
|
+
}
|
|
176
|
+
watchSubdirectories(absDir, relDir) {
|
|
177
|
+
if (this.watchers.size >= this.maxWatchers) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let entries;
|
|
181
|
+
try {
|
|
182
|
+
entries = readdirSync(absDir, { withFileTypes: true });
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
if (!entry.isDirectory()) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const childRel = join(relDir, entry.name).replace(/\\/g, '/');
|
|
192
|
+
if (this.exclude.some((p) => matchesGlobPattern(childRel, p))) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const childAbs = join(absDir, entry.name);
|
|
196
|
+
try {
|
|
197
|
+
this.watchDirectory(childAbs, childRel);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (this.watchers.size >= this.maxWatchers) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.watchSubdirectories(childAbs, childRel);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
emitWatcherError(err) {
|
|
209
|
+
if (this.listenerCount('error') > 0) {
|
|
210
|
+
this.emit('error', err);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
160
213
|
async handleFsEvent(relPath) {
|
|
161
214
|
try {
|
|
162
215
|
// Check exclude patterns first
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.3",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"packageName": "@planu/core"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@planu/core-darwin-arm64": "4.1.
|
|
36
|
-
"@planu/core-darwin-x64": "4.1.
|
|
37
|
-
"@planu/core-linux-arm64-gnu": "4.1.
|
|
38
|
-
"@planu/core-linux-arm64-musl": "4.1.
|
|
39
|
-
"@planu/core-linux-x64-gnu": "4.1.
|
|
40
|
-
"@planu/core-linux-x64-musl": "4.1.
|
|
35
|
+
"@planu/core-darwin-arm64": "4.1.3",
|
|
36
|
+
"@planu/core-darwin-x64": "4.1.3",
|
|
37
|
+
"@planu/core-linux-arm64-gnu": "4.1.3",
|
|
38
|
+
"@planu/core-linux-arm64-musl": "4.1.3",
|
|
39
|
+
"@planu/core-linux-x64-gnu": "4.1.3",
|
|
40
|
+
"@planu/core-linux-x64-musl": "4.1.3"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=24.0.0"
|