spectater 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +113 -2
- package/bin/spectater.js +376 -0
- package/package.json +59 -4
- package/skills/spectater/SKILL.md +13 -0
- package/src/agent.js +184 -0
- package/src/changes.js +82 -0
- package/src/config.js +44 -0
- package/src/discovery.js +158 -0
- package/src/files.js +25 -0
- package/src/hooks.js +145 -0
- package/src/markdown.js +120 -0
- package/src/queue.js +29 -0
- package/src/review.js +402 -0
- package/src/server.js +291 -0
- package/src/storage.js +381 -0
- package/src/watch.js +113 -0
- package/web/app.js +705 -0
- package/web/favicon.svg +14 -0
- package/web/index.html +158 -0
- package/web/style.css +647 -0
- package/web/theme.js +45 -0
package/src/watch.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
import { lstat, readdir, realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { within } from './discovery.js';
|
|
5
|
+
|
|
6
|
+
export async function watchChange(review, report) {
|
|
7
|
+
const root = review.state.changeRoot;
|
|
8
|
+
const watches = new Map();
|
|
9
|
+
let timer,
|
|
10
|
+
running,
|
|
11
|
+
dirty = false,
|
|
12
|
+
closed = false;
|
|
13
|
+
const status = (error) =>
|
|
14
|
+
report({
|
|
15
|
+
type: 'watch',
|
|
16
|
+
automatic: !error,
|
|
17
|
+
error: error
|
|
18
|
+
? `Automatic updates unavailable: ${error.message} Use Refresh source to retry.`
|
|
19
|
+
: undefined,
|
|
20
|
+
});
|
|
21
|
+
async function register() {
|
|
22
|
+
const directories = new Map();
|
|
23
|
+
async function visit(directory) {
|
|
24
|
+
const stat = await lstat(directory);
|
|
25
|
+
if (
|
|
26
|
+
stat.isSymbolicLink() ||
|
|
27
|
+
!stat.isDirectory() ||
|
|
28
|
+
(await realpath(directory)) !== directory ||
|
|
29
|
+
(directory !== root && !within(root, directory))
|
|
30
|
+
)
|
|
31
|
+
throw new Error('Selected change directory moved or uses a symlink.');
|
|
32
|
+
if (directories.size >= 256) throw new Error('Selected change exceeds 256 directories.');
|
|
33
|
+
directories.set(directory, `${stat.dev}:${stat.ino}`);
|
|
34
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
35
|
+
if (entry.isDirectory() && !entry.isSymbolicLink())
|
|
36
|
+
await visit(path.join(directory, entry.name));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
await visit(root);
|
|
40
|
+
if (closed) return;
|
|
41
|
+
for (const [directory, item] of watches) {
|
|
42
|
+
if (directories.get(directory) !== item.identity) {
|
|
43
|
+
item.watcher.close();
|
|
44
|
+
watches.delete(directory);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [directory, identity] of directories) {
|
|
48
|
+
if (watches.has(directory)) continue;
|
|
49
|
+
const watcher = watch(directory, schedule);
|
|
50
|
+
watcher.on('error', (error) => {
|
|
51
|
+
watcher.close();
|
|
52
|
+
watches.delete(directory);
|
|
53
|
+
status(error);
|
|
54
|
+
});
|
|
55
|
+
watches.set(directory, { identity, watcher });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function update() {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
timer = undefined;
|
|
61
|
+
if (closed) return;
|
|
62
|
+
if (running) {
|
|
63
|
+
dirty = true;
|
|
64
|
+
return running;
|
|
65
|
+
}
|
|
66
|
+
running = (async () => {
|
|
67
|
+
try {
|
|
68
|
+
await register();
|
|
69
|
+
if (!closed) await review.refresh();
|
|
70
|
+
if (!closed) status();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!closed) status(error);
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
75
|
+
await running;
|
|
76
|
+
running = undefined;
|
|
77
|
+
if (dirty && !closed) {
|
|
78
|
+
dirty = false;
|
|
79
|
+
schedule();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function schedule() {
|
|
83
|
+
if (closed) return;
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
timer = setTimeout(update, 150);
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await register();
|
|
89
|
+
status();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
status(error);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
async retry() {
|
|
95
|
+
try {
|
|
96
|
+
await register();
|
|
97
|
+
const state = await review.refresh();
|
|
98
|
+
status();
|
|
99
|
+
return state;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
status(error);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
async close() {
|
|
106
|
+
closed = true;
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
for (const { watcher } of watches.values()) watcher.close();
|
|
109
|
+
watches.clear();
|
|
110
|
+
await running;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|