beads-enhanced-ui 0.1.0

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.
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveWorkspaceDatabase } from './db.js';
4
+ import { debug } from './logging.js';
5
+
6
+ /**
7
+ * Watch the resolved workspace database target and invoke a callback after a
8
+ * debounce window.
9
+ *
10
+ * For SQLite workspaces this watches the DB file's parent directory and filters
11
+ * by file name. For non-SQLite backends (for example Dolt), this watches the
12
+ * workspace `.beads` directory.
13
+ *
14
+ * @param {string} root_dir - Project root directory (starting point for resolution).
15
+ * @param {() => void} onChange - Called when changes are detected.
16
+ * @param {{ debounce_ms?: number, cooldown_ms?: number, explicit_db?: string }} [options]
17
+ * @returns {{ close: () => void, rebind: (opts?: { root_dir?: string, explicit_db?: string }) => void, path: string }}
18
+ */
19
+ export function watchDb(root_dir, onChange, options = {}) {
20
+ const debounce_ms = options.debounce_ms ?? 250;
21
+ const cooldown_ms = options.cooldown_ms ?? 1000;
22
+ const log = debug('watcher');
23
+
24
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
25
+ let timer;
26
+ /** @type {fs.FSWatcher | undefined} */
27
+ let watcher;
28
+ let cooldown_until = 0;
29
+ let current_path = '';
30
+ let current_dir = '';
31
+ let current_file = '';
32
+
33
+ /**
34
+ * Schedule the debounced onChange callback.
35
+ */
36
+ const schedule = () => {
37
+ if (timer) {
38
+ clearTimeout(timer);
39
+ }
40
+ timer = setTimeout(() => {
41
+ onChange();
42
+ cooldown_until = Date.now() + cooldown_ms;
43
+ }, debounce_ms);
44
+ timer.unref();
45
+ };
46
+
47
+ /**
48
+ * Attach a watcher to the directory containing the resolved DB path.
49
+ *
50
+ * @param {string} base_dir
51
+ * @param {string | undefined} explicit_db
52
+ */
53
+ const bind = (base_dir, explicit_db) => {
54
+ const resolved = resolveWorkspaceDatabase({ cwd: base_dir, explicit_db });
55
+ current_path = resolved.path;
56
+ if (pathIsDirectory(current_path)) {
57
+ current_dir = current_path;
58
+ current_file = '';
59
+ } else {
60
+ current_dir = path.dirname(current_path);
61
+ current_file = path.basename(current_path);
62
+ }
63
+ if (!resolved.exists) {
64
+ log(
65
+ 'resolved workspace database missing: %s – Hint: set --db, export BEADS_DB, or run `bd init` in your workspace.',
66
+ current_path
67
+ );
68
+ }
69
+
70
+ // (Re)create watcher
71
+ try {
72
+ watcher = fs.watch(
73
+ current_dir,
74
+ { persistent: true },
75
+ (event_type, filename) => {
76
+ if (current_file && filename && String(filename) !== current_file) {
77
+ return;
78
+ }
79
+ if (event_type === 'change' || event_type === 'rename') {
80
+ if (Date.now() < cooldown_until) {
81
+ return;
82
+ }
83
+ log('fs %s %s', event_type, filename || '');
84
+ schedule();
85
+ }
86
+ }
87
+ );
88
+ } catch (err) {
89
+ log('unable to watch directory %s %o', current_dir, err);
90
+ }
91
+ };
92
+
93
+ // initial bind
94
+ bind(root_dir, options.explicit_db);
95
+
96
+ return {
97
+ get path() {
98
+ return current_path;
99
+ },
100
+ close() {
101
+ if (timer) {
102
+ clearTimeout(timer);
103
+ timer = undefined;
104
+ }
105
+ watcher?.close();
106
+ },
107
+ /**
108
+ * Re-resolve and reattach watcher when root_dir or explicit_db changes.
109
+ *
110
+ * @param {{ root_dir?: string, explicit_db?: string }} [opts]
111
+ */
112
+ rebind(opts = {}) {
113
+ const next_root = opts.root_dir ? String(opts.root_dir) : root_dir;
114
+ const next_explicit = opts.explicit_db ?? options.explicit_db;
115
+ const next_resolved = resolveWorkspaceDatabase({
116
+ cwd: next_root,
117
+ explicit_db: next_explicit
118
+ });
119
+ const next_path = next_resolved.path;
120
+ if (next_path !== current_path) {
121
+ // swap watcher
122
+ watcher?.close();
123
+ cooldown_until = 0;
124
+ bind(next_root, next_explicit);
125
+ }
126
+ }
127
+ };
128
+ }
129
+
130
+ /**
131
+ * @param {string} file_path
132
+ */
133
+ function pathIsDirectory(file_path) {
134
+ try {
135
+ return fs.statSync(file_path).isDirectory();
136
+ } catch {
137
+ return false;
138
+ }
139
+ }