pi-soly 1.3.0 → 1.4.1

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/switch/watcher.ts DELETED
@@ -1,112 +0,0 @@
1
- // =============================================================================
2
- // watcher.ts — Hot-reload the rotor cycle when rotor .md files change
3
- // =============================================================================
4
- //
5
- // Watches all known rotor home dirs (project + user) and triggers a cycle
6
- // refresh + brief notify when a .md file is added/removed/changed. The
7
- // watcher is debounced (editors save in bursts) and stops cleanly on
8
- // extension reload.
9
- //
10
- // Why: previously, adding a new rotor .md to `.agents/` only took effect on
11
- // the next Ctrl+Tab. With this watcher, the new rotor appears in the next
12
- // pill render — no user action required.
13
- // =============================================================================
14
-
15
- import * as fs from "node:fs";
16
- import * as os from "node:os";
17
- import * as path from "node:path";
18
- import { rotorHomeDirs } from "./core.js";
19
-
20
- /** Debounce window for file events (editors save in bursts). */
21
- const DEBOUNCE_MS = 200;
22
- /** Coalesce window for the "rotors reloaded" notify. */
23
- const NOTIFY_COALESCE_MS = 500;
24
-
25
- export interface WatcherOptions {
26
- /** Called when rotors change (debounced). */
27
- onChange: () => void;
28
- /** Called with a debounced message about what changed. */
29
- onNotify?: (message: string) => void;
30
- /** Override HOME for tests. */
31
- home?: string;
32
- }
33
-
34
- export interface WatcherHandle {
35
- stop: () => void;
36
- }
37
-
38
- /** Watch all rotor home dirs for *.md add/remove/change. */
39
- export function watchRotors(cwd: string | undefined, opts: WatcherOptions): WatcherHandle {
40
- const home = opts.home ?? process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
41
- const dirs = rotorHomeDirs(cwd).map((d) => d.replace(/^~/, home));
42
-
43
- const watchers: fs.FSWatcher[] = [];
44
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
45
- let notifyTimer: ReturnType<typeof setTimeout> | null = null;
46
- let pendingReasons: string[] = [];
47
- let stopped = false;
48
-
49
- const fire = () => {
50
- if (stopped) return;
51
- opts.onChange();
52
- };
53
-
54
- const scheduleNotify = (reason: string) => {
55
- pendingReasons.push(reason);
56
- if (notifyTimer) clearTimeout(notifyTimer);
57
- notifyTimer = setTimeout(() => {
58
- const reasons = [...new Set(pendingReasons)];
59
- pendingReasons = [];
60
- notifyTimer = null;
61
- if (opts.onNotify) {
62
- const summary =
63
- reasons.length === 1
64
- ? reasons[0]!
65
- : `${reasons.length} changes (${reasons.slice(0, 3).join(", ")}${reasons.length > 3 ? "…" : ""})`;
66
- opts.onNotify(`rotors reloaded (${summary})`);
67
- }
68
- }, NOTIFY_COALESCE_MS);
69
- };
70
-
71
- const onEvent = (event: "add" | "change" | "unlink", filename: string | null) => {
72
- if (stopped) return;
73
- if (!filename || !filename.endsWith(".md")) return;
74
- // Skip dotfiles (frontmatter dumps, etc.)
75
- if (filename.startsWith(".")) return;
76
- // Coalesce
77
- if (debounceTimer) clearTimeout(debounceTimer);
78
- debounceTimer = setTimeout(() => {
79
- debounceTimer = null;
80
- scheduleNotify(event);
81
- fire();
82
- }, DEBOUNCE_MS);
83
- };
84
-
85
- for (const dir of dirs) {
86
- // Ensure dir exists before watching (fs.watch errors on non-existent)
87
- try {
88
- fs.mkdirSync(dir, { recursive: true });
89
- } catch { /* ignore */ }
90
- try {
91
- const w = fs.watch(dir, { persistent: false }, (_eventType, filename) => {
92
- onEvent(_eventType as "add" | "change" | "unlink", filename);
93
- });
94
- watchers.push(w);
95
- } catch (err) {
96
- // Some dirs may not exist or be unwatchable. Skip silently.
97
- // eslint-disable-next-line no-console
98
- console.error(`[pi-soly] cannot watch ${dir}: ${(err as Error).message}`);
99
- }
100
- }
101
-
102
- return {
103
- stop: () => {
104
- stopped = true;
105
- if (debounceTimer) clearTimeout(debounceTimer);
106
- if (notifyTimer) clearTimeout(notifyTimer);
107
- for (const w of watchers) {
108
- try { w.close(); } catch { /* ignore */ }
109
- }
110
- },
111
- };
112
- }