claudex-setup 1.16.0 → 1.16.2

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/src/watch.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Watch mode - monitors project for Claude Code config changes and re-audits.
3
- * Uses Node.js fs.watch (zero dependencies).
3
+ * Uses Node.js fs.watch (zero dependencies) with a recursive-directory fallback
4
+ * on platforms where native recursive watch is not reliable.
4
5
  */
5
6
 
6
7
  const fs = require('fs');
@@ -13,20 +14,129 @@ const COLORS = {
13
14
  };
14
15
  const c = (text, color) => `${COLORS[color] || ''}${text}${COLORS.reset}`;
15
16
 
16
- const WATCH_PATHS = [
17
+ const FILE_WATCH_PATHS = [
17
18
  'CLAUDE.md',
18
- '.claude',
19
19
  '.gitignore',
20
20
  'package.json',
21
21
  'tsconfig.json',
22
+ ];
23
+
24
+ const DIRECTORY_WATCH_PATHS = [
25
+ '.claude',
22
26
  '.github',
23
27
  ];
24
28
 
29
+ function supportsNativeRecursiveWatch(platform = process.platform) {
30
+ return platform === 'win32' || platform === 'darwin';
31
+ }
32
+
33
+ function statIfExists(fullPath) {
34
+ try {
35
+ return fs.statSync(fullPath);
36
+ } catch (e) {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function listRecursiveDirectories(dir) {
42
+ const directories = [dir];
43
+ let entries = [];
44
+
45
+ try {
46
+ entries = fs.readdirSync(dir, { withFileTypes: true });
47
+ } catch (e) {
48
+ return directories;
49
+ }
50
+
51
+ for (const entry of entries) {
52
+ if (entry.isDirectory()) {
53
+ directories.push(...listRecursiveDirectories(path.join(dir, entry.name)));
54
+ }
55
+ }
56
+
57
+ return directories;
58
+ }
59
+
60
+ function buildWatchPlan(rootDir, platform = process.platform) {
61
+ const plan = [];
62
+ const seen = new Set();
63
+ const recursiveSupported = supportsNativeRecursiveWatch(platform);
64
+
65
+ const addTarget = (fullPath, recursive, source) => {
66
+ const resolved = path.resolve(fullPath);
67
+ const key = `${resolved}|${recursive}`;
68
+ if (seen.has(key)) return;
69
+ seen.add(key);
70
+ plan.push({ path: resolved, recursive, source });
71
+ };
72
+
73
+ addTarget(rootDir, false, 'repo-root');
74
+
75
+ for (const watchPath of FILE_WATCH_PATHS) {
76
+ const fullPath = path.join(rootDir, watchPath);
77
+ const stat = statIfExists(fullPath);
78
+ if (stat && stat.isFile()) {
79
+ addTarget(fullPath, false, watchPath);
80
+ }
81
+ }
82
+
83
+ for (const watchPath of DIRECTORY_WATCH_PATHS) {
84
+ const fullPath = path.join(rootDir, watchPath);
85
+ const stat = statIfExists(fullPath);
86
+ if (!stat || !stat.isDirectory()) continue;
87
+
88
+ if (recursiveSupported) {
89
+ addTarget(fullPath, true, watchPath);
90
+ continue;
91
+ }
92
+
93
+ for (const dir of listRecursiveDirectories(fullPath)) {
94
+ addTarget(dir, false, watchPath);
95
+ }
96
+ }
97
+
98
+ return plan;
99
+ }
100
+
101
+ function registerWatchers(rootDir, watchers, onChange, platform = process.platform) {
102
+ const plan = buildWatchPlan(rootDir, platform);
103
+
104
+ for (const item of plan) {
105
+ const key = `${item.path}|${item.recursive}`;
106
+ if (watchers.has(key)) continue;
107
+
108
+ try {
109
+ const watcher = fs.watch(item.path, { recursive: item.recursive }, (eventType, filename) => {
110
+ onChange(item, eventType, filename);
111
+ });
112
+ watchers.set(key, watcher);
113
+ } catch (e) {
114
+ // Ignore unsupported or transient watch registration failures.
115
+ }
116
+ }
117
+
118
+ return watchers.size;
119
+ }
120
+
121
+ function closeWatchers(watchers) {
122
+ for (const watcher of watchers.values()) {
123
+ try {
124
+ watcher.close();
125
+ } catch (e) {
126
+ // Ignore close errors during shutdown.
127
+ }
128
+ }
129
+ watchers.clear();
130
+ }
131
+
25
132
  async function watch(options) {
133
+ const recursiveSupported = supportsNativeRecursiveWatch();
134
+
26
135
  console.log('');
27
136
  console.log(c(' claudex-setup watch mode', 'bold'));
28
137
  console.log(c(' ═══════════════════════════════════════', 'dim'));
29
138
  console.log(c(` Watching: ${options.dir}`, 'dim'));
139
+ console.log(c(` Mode: ${recursiveSupported ? 'native recursive directories' : 'expanded directory fallback (cross-platform safe)'}`, 'dim'));
30
140
  console.log(c(' Press Ctrl+C to stop', 'dim'));
31
141
  console.log('');
32
142
 
@@ -43,50 +153,64 @@ async function watch(options) {
43
153
  }
44
154
 
45
155
  // Watch relevant paths
46
- const watchers = [];
156
+ const watchers = new Map();
47
157
  let debounceTimer = null;
158
+ let shuttingDown = false;
48
159
 
49
- for (const watchPath of WATCH_PATHS) {
50
- const fullPath = path.join(options.dir, watchPath);
51
- try {
52
- const watcher = fs.watch(fullPath, { recursive: true }, (eventType, filename) => {
53
- // Debounce: wait 500ms after last change
54
- clearTimeout(debounceTimer);
55
- debounceTimer = setTimeout(async () => {
56
- const timestamp = new Date().toLocaleTimeString();
57
- console.log(c(` [${timestamp}] Change detected: ${filename || watchPath}`, 'dim'));
58
-
59
- try {
60
- const result = await audit({ ...options, silent: true });
61
- const delta = lastScore !== null ? result.score - lastScore : 0;
62
- const arrow = delta > 0 ? c(`+${delta}`, 'green') : delta < 0 ? c(String(delta), 'yellow') : '';
63
-
64
- console.log(` Score: ${scoreColor(result.score)} ${arrow} (${result.passed}/${result.passed + result.failed} passing)`);
65
-
66
- if (result.score > lastScore) {
67
- console.log(c(' Nice improvement!', 'green'));
68
- } else if (result.score < lastScore) {
69
- console.log(c(' Score dropped - check what changed.', 'yellow'));
70
- }
71
- lastScore = result.score;
72
- console.log('');
73
- } catch (e) {
74
- // Ignore transient errors during file saves
75
- }
76
- }, 500);
77
- });
78
- watchers.push(watcher);
79
- } catch (e) {
80
- // Path doesn't exist yet - that's fine
81
- }
82
- }
160
+ const cleanupAndExit = () => {
161
+ if (shuttingDown) return;
162
+ shuttingDown = true;
163
+ clearTimeout(debounceTimer);
164
+ closeWatchers(watchers);
165
+ console.log('');
166
+ console.log(c(' Watch mode stopped.', 'dim'));
167
+ process.exit(0);
168
+ };
169
+
170
+ const handleChange = (item, eventType, filename) => {
171
+ clearTimeout(debounceTimer);
172
+ debounceTimer = setTimeout(async () => {
173
+ const changedLabel = filename
174
+ ? String(filename)
175
+ : path.relative(options.dir, item.path) || path.basename(item.path);
176
+ const timestamp = new Date().toLocaleTimeString();
83
177
 
84
- if (watchers.length === 0) {
85
- console.log(c(' No watchable paths found. Create CLAUDE.md or .claude/ to start.', 'yellow'));
178
+ // Pick up newly created directories or newly materialized watch paths.
179
+ registerWatchers(options.dir, watchers, handleChange);
180
+
181
+ console.log(c(` [${timestamp}] Change detected: ${changedLabel}`, 'dim'));
182
+
183
+ try {
184
+ const result = await audit({ ...options, silent: true });
185
+ const delta = lastScore !== null ? result.score - lastScore : 0;
186
+ const arrow = delta > 0 ? c(`+${delta}`, 'green') : delta < 0 ? c(String(delta), 'yellow') : '';
187
+
188
+ console.log(` Score: ${scoreColor(result.score)} ${arrow} (${result.passed}/${result.passed + result.failed} passing)`);
189
+
190
+ if (lastScore !== null && result.score > lastScore) {
191
+ console.log(c(' Nice improvement!', 'green'));
192
+ } else if (lastScore !== null && result.score < lastScore) {
193
+ console.log(c(' Score dropped - check what changed.', 'yellow'));
194
+ }
195
+ lastScore = result.score;
196
+ console.log('');
197
+ } catch (e) {
198
+ // Ignore transient errors during file saves.
199
+ }
200
+ }, 500);
201
+ };
202
+
203
+ registerWatchers(options.dir, watchers, handleChange);
204
+
205
+ if (watchers.size === 0) {
206
+ console.log(c(' Could not register any filesystem watchers in this environment.', 'yellow'));
86
207
  return;
87
208
  }
88
209
 
89
- console.log(c(` Watching ${watchers.length} paths for changes...`, 'dim'));
210
+ process.once('SIGINT', cleanupAndExit);
211
+ process.once('SIGTERM', cleanupAndExit);
212
+
213
+ console.log(c(` Watching ${watchers.size} targets for changes...`, 'dim'));
90
214
  console.log('');
91
215
 
92
216
  // Keep alive
@@ -98,4 +222,8 @@ function scoreColor(score) {
98
222
  return c(`${score}/100`, color);
99
223
  }
100
224
 
101
- module.exports = { watch };
225
+ module.exports = {
226
+ watch,
227
+ buildWatchPlan,
228
+ supportsNativeRecursiveWatch,
229
+ };