@pencere/cli 0.0.0-stage → 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,322 @@
1
+ /**
2
+ * An MCP server that is a process: JSON-RPC messages, one to a line, on its stdin
3
+ * and stdout (MCP's stdio transport), seen from the client's side.
4
+ *
5
+ * This is the one place a server is started. Discovery asks a server what it has
6
+ * with it, and the adapter that stands in for the program's MCP server on the
7
+ * terminal's stream (`connector.ts`) forwards to it, so both see the same
8
+ * environment, the same process group and the same ending.
9
+ *
10
+ * Why this and not `StdioClientTransport` of the MCP client package: that one
11
+ * passes a server six environment variables and no more, which would make a
12
+ * server start differently under pencere than under the chat client whose
13
+ * configuration the person copied; and it cannot start the server in a process
14
+ * group of its own. The server must have one. ⌃C is a signal of the terminal to
15
+ * the foreground group, the pencere program's own, and a server that took it too
16
+ * would die before the program had said `bye`, and a Python server would print a
17
+ * traceback for what the person meant as "close this". The server is ended by the
18
+ * program that started it: its stdin closes, which every stdio server takes as
19
+ * the end, then SIGTERM and SIGKILL to its whole group, which catches what a
20
+ * wrapper (`npx`, `uv run`, `sh -c`) started. A server that leaves on its own when its
21
+ * stdin closes, or falls, leaves what it started behind in the group, and that gets the
22
+ * same two endings: the group is the program's to end, not only the process that leads it.
23
+ */
24
+ import { ReadBuffer, serializeMessage } from "@modelcontextprotocol/client";
25
+ import { spawn } from "node:child_process";
26
+ export const CLOSE_TIMING = { stdin: 1500, term: 1500, kill: 2000 };
27
+ /** After a process ended, how long to wait for the last of its output before letting go of its pipes. */
28
+ const PIPE_GRACE = 250;
29
+ /** How often the process table is asked whether a group is gone, in milliseconds. */
30
+ const GROUP_POLL = 20;
31
+ const LIMITS = { line: 10 * 1024 * 1024 };
32
+ /** The process could not be started. */
33
+ export class StartError extends Error {
34
+ command;
35
+ constructor(command, cause) {
36
+ super(`cannot start "${command}": ${reason(cause)}`, { cause });
37
+ this.name = "StartError";
38
+ this.command = command;
39
+ }
40
+ }
41
+ function reason(error) {
42
+ const code = error.code;
43
+ if (code === "ENOENT")
44
+ return "no such file or directory (is it installed, and on PATH?)";
45
+ if (code === "EACCES")
46
+ return "permission denied";
47
+ return error.message;
48
+ }
49
+ function toError(value) {
50
+ return value instanceof Error ? value : new Error(String(value));
51
+ }
52
+ /** Resolves `true` when `promise` settles within `ms`, `false` when the time is up first. */
53
+ async function within(promise, ms) {
54
+ let timer;
55
+ const late = new Promise((resolve) => {
56
+ timer = setTimeout(() => resolve(false), ms);
57
+ });
58
+ try {
59
+ return await Promise.race([promise.then(() => true), late]);
60
+ }
61
+ finally {
62
+ clearTimeout(timer);
63
+ }
64
+ }
65
+ /** Whether anything is left of the process group `pid` leads. */
66
+ function groupExists(pid) {
67
+ try {
68
+ process.kill(-pid, 0);
69
+ return true;
70
+ }
71
+ catch {
72
+ // Nothing there (ESRCH), or nothing that can be signalled: either way nothing to wait for.
73
+ return false;
74
+ }
75
+ }
76
+ /** Resolves `true` when the group is gone within `ms`, `false` when something is left. */
77
+ async function groupGone(pid, ms) {
78
+ const deadline = Date.now() + ms;
79
+ while (groupExists(pid)) {
80
+ if (Date.now() >= deadline)
81
+ return false;
82
+ await new Promise((resolve) => setTimeout(resolve, GROUP_POLL));
83
+ }
84
+ return true;
85
+ }
86
+ /** The process of a stdio MCP server, as a transport for an MCP client, and a tool to end it. */
87
+ export class ServerProcess {
88
+ onclose;
89
+ onerror;
90
+ onmessage;
91
+ /** The process has ended and what it wrote has been read: once, before `onclose`. */
92
+ onexit;
93
+ #spec;
94
+ #tail;
95
+ #timing;
96
+ #read;
97
+ #child;
98
+ #closing = false;
99
+ #shutdown;
100
+ /** The process has exited: what it did is known, and what is left is its pipes. */
101
+ #exited = false;
102
+ #ended = false;
103
+ /** The ending of what is left in the process's group, begun when the process ended. */
104
+ #sweeping = Promise.resolve();
105
+ #done;
106
+ #finished;
107
+ constructor(spec, tail, timing = CLOSE_TIMING, limits = LIMITS) {
108
+ this.#spec = spec;
109
+ this.#tail = tail;
110
+ this.#timing = timing;
111
+ this.#read = new ReadBuffer({ maxBufferSize: limits.line });
112
+ this.#done = new Promise((resolve) => {
113
+ this.#finished = resolve;
114
+ });
115
+ }
116
+ /** Resolves once the process has ended (never, if it was never started). */
117
+ get ended() {
118
+ return this.#done;
119
+ }
120
+ get pid() {
121
+ return this.#child?.pid;
122
+ }
123
+ /** Starts the process; it is running when this resolves. @throws {StartError} */
124
+ start() {
125
+ if (this.#child !== undefined)
126
+ return Promise.reject(new Error("the server process was started already"));
127
+ return new Promise((resolve, reject) => {
128
+ const child = spawn(this.#spec.command, [...this.#spec.args], {
129
+ stdio: ["pipe", "pipe", "pipe"],
130
+ // A process group of its own: see the description of this file.
131
+ detached: true,
132
+ shell: false,
133
+ });
134
+ this.#child = child;
135
+ let spawned = false;
136
+ // Not `once`: after the process runs, an error is a signal that was not sent or a write that
137
+ // did not go through, and there can be more than one; an `error` nobody listens for ends the program.
138
+ child.on("error", (error) => {
139
+ // Before `spawn` this is a program that could not be started; after, a signal
140
+ // or a write that did not go through.
141
+ if (spawned)
142
+ this.onerror?.(error);
143
+ else {
144
+ this.#ended = true;
145
+ reject(new StartError(this.#spec.command, error));
146
+ }
147
+ });
148
+ child.once("spawn", () => {
149
+ spawned = true;
150
+ resolve();
151
+ });
152
+ // A write to a process that has gone fails with EPIPE, and `send` tells the caller
153
+ // so; the process's end is told by `onexit`, once, and is not an error as well.
154
+ child.stdin.on("error", () => { });
155
+ child.stdout.on("error", (error) => this.onerror?.(error));
156
+ child.stdout.on("data", (chunk) => this.#data(chunk));
157
+ child.stderr.on("error", () => { });
158
+ child.stderr.on("data", (chunk) => this.#tail.push(chunk));
159
+ let status;
160
+ let expected = false;
161
+ let timer;
162
+ /** What the process did, as it is known the moment it is: asked to end or not is decided then. */
163
+ const exited = (code, signal) => {
164
+ if (status !== undefined)
165
+ return;
166
+ status = { code, signal };
167
+ expected = this.#closing;
168
+ this.#exited = true;
169
+ };
170
+ const end = () => {
171
+ if (!spawned || this.#ended)
172
+ return;
173
+ clearTimeout(timer);
174
+ this.#ended = true;
175
+ // The last of its output is read or is not coming: a grandchild may hold the pipes.
176
+ for (const stream of [child.stdin, child.stdout, child.stderr])
177
+ stream?.destroy();
178
+ this.#read.clear();
179
+ const exit = { ...(status ?? { code: null, signal: null }), expected };
180
+ this.#finished(exit);
181
+ this.onexit?.(exit);
182
+ this.onclose?.();
183
+ // Whatever the leader's end was, what it started is not ended with it: right now, with
184
+ // the group's number still its, what is left in the group is the program's to end.
185
+ this.#sweeping = this.#sweep();
186
+ };
187
+ child.once("exit", (code, signal) => {
188
+ exited(code, signal);
189
+ timer = setTimeout(end, PIPE_GRACE);
190
+ });
191
+ child.once("close", (code, signal) => {
192
+ exited(code, signal);
193
+ end();
194
+ });
195
+ });
196
+ }
197
+ /**
198
+ * What the server wrote to stdout: whole lines are messages, and the rest waits for its end. The
199
+ * chunk is read a line at a time, so that a line past the limit costs the lines around it
200
+ * nothing: the buffer that gives up on it gives up on what it holds, which is that line.
201
+ */
202
+ #data(chunk) {
203
+ let from = 0;
204
+ while (from < chunk.length) {
205
+ const newline = chunk.indexOf(0x0a, from);
206
+ const to = newline === -1 ? chunk.length : newline + 1;
207
+ this.#line(chunk.subarray(from, to));
208
+ from = to;
209
+ }
210
+ }
211
+ #line(piece) {
212
+ try {
213
+ this.#read.append(piece);
214
+ }
215
+ catch (error) {
216
+ // A line past the limit is dropped, and the next one is read as it comes.
217
+ this.onerror?.(toError(error));
218
+ return;
219
+ }
220
+ for (;;) {
221
+ let message;
222
+ try {
223
+ message = this.#read.readMessage();
224
+ }
225
+ catch (error) {
226
+ // A line that is JSON and is not a message: it is gone, and the lines after it are not.
227
+ this.onerror?.(toError(error));
228
+ continue;
229
+ }
230
+ if (message === null)
231
+ return;
232
+ try {
233
+ this.onmessage?.(message);
234
+ }
235
+ catch (error) {
236
+ this.onerror?.(toError(error));
237
+ }
238
+ }
239
+ }
240
+ send(message) {
241
+ const stdin = this.#child?.stdin;
242
+ if (this.#ended || this.#closing || !stdin?.writable)
243
+ return Promise.reject(new Error("the server process is not running"));
244
+ return new Promise((resolve, reject) => {
245
+ stdin.write(serializeMessage(message), (error) => error === null || error === undefined ? resolve() : reject(error));
246
+ });
247
+ }
248
+ /**
249
+ * Ends the process, as asked: its stdin closes, and if it has not left by itself
250
+ * its group gets SIGTERM and then SIGKILL. Resolves when it is gone. A process that
251
+ * is already gone, or was never started, is a close that has nothing to do.
252
+ */
253
+ close() {
254
+ this.#shutdown ??= this.#shutDown();
255
+ return this.#shutdown;
256
+ }
257
+ async #shutDown() {
258
+ const child = this.#child;
259
+ if (child === undefined)
260
+ return;
261
+ if (!this.#ended) {
262
+ // Exited by itself already: closing it is waiting for its pipes, which is not what it
263
+ // was that ended it.
264
+ if (this.#exited)
265
+ await this.#done;
266
+ else
267
+ await this.#end(child);
268
+ }
269
+ await this.#sweeping;
270
+ }
271
+ /** Asks the process to end, as its stdin closing does, and then makes it. */
272
+ async #end(child) {
273
+ this.#closing = true;
274
+ try {
275
+ child.stdin?.end();
276
+ }
277
+ catch {
278
+ // Closed already: the process is on its way out.
279
+ }
280
+ if (await within(this.#done, this.#timing.stdin))
281
+ return;
282
+ this.#signal("SIGTERM");
283
+ if (await within(this.#done, this.#timing.term))
284
+ return;
285
+ this.#signal("SIGKILL");
286
+ await within(this.#done, this.#timing.kill);
287
+ }
288
+ /**
289
+ * The process has ended: what is left in its group (a wrapper's child, `sh -c 'server &'`) is
290
+ * ended as a leader that would not leave is, SIGTERM and then SIGKILL. Called as the process
291
+ * ends, so that the group's number is still its and has not been given to another.
292
+ */
293
+ async #sweep() {
294
+ const pid = this.#child?.pid;
295
+ if (pid === undefined || !groupExists(pid))
296
+ return;
297
+ this.#signal("SIGTERM");
298
+ if (await groupGone(pid, this.#timing.term))
299
+ return;
300
+ this.#signal("SIGKILL");
301
+ await groupGone(pid, this.#timing.kill);
302
+ }
303
+ /** To the process's whole group; to the process alone where a group cannot be signalled. */
304
+ #signal(signal) {
305
+ const child = this.#child;
306
+ const pid = child?.pid;
307
+ if (child === undefined || pid === undefined)
308
+ return;
309
+ try {
310
+ process.kill(-pid, signal);
311
+ }
312
+ catch {
313
+ try {
314
+ child.kill(signal);
315
+ }
316
+ catch {
317
+ // Gone between the question and the signal.
318
+ }
319
+ }
320
+ }
321
+ }
322
+ //# sourceMappingURL=server-process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-process.js","sourceRoot":"","sources":["../src/server-process.ts"],"names":[],"mappings":"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"}
package/dist/tail.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The end of what a server wrote to its stderr.
3
+ *
4
+ * While a view is up the terminal's screen is the view's, so what the server says
5
+ * on stderr would be lost under the alternate screen. It is kept here instead,
6
+ * a bounded amount, the end of it (the cause of a failure is the last thing a
7
+ * server says), and shown on the screen the person gets back, if the server failed.
8
+ */
9
+ /** How much is kept: enough for a stack trace, not for a server that never stops talking. */
10
+ export declare const TAIL_LIMIT: number;
11
+ export declare class Tail {
12
+ #private;
13
+ constructor(limit?: number);
14
+ /** Adds what the server wrote; a character cut between two chunks is kept whole. */
15
+ push(chunk: Uint8Array | string): void;
16
+ /** What is kept, as the person is shown it: text, not terminal commands (see {@link plain}). */
17
+ text(): string;
18
+ get empty(): boolean;
19
+ /** Forgets what is kept: it has been shown, and what comes next is about the next run of the server. */
20
+ clear(): void;
21
+ }
22
+ /**
23
+ * Text a server wrote, made safe to put on the person's terminal: what is not a
24
+ * character to read (an escape, which could be a command to the terminal, among
25
+ * them) is written out as `\u001b`, and a carriage return is a line end. Line ends and
26
+ * tabs stay.
27
+ */
28
+ export declare function plain(text: string): string;
package/dist/tail.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The end of what a server wrote to its stderr.
3
+ *
4
+ * While a view is up the terminal's screen is the view's, so what the server says
5
+ * on stderr would be lost under the alternate screen. It is kept here instead,
6
+ * a bounded amount, the end of it (the cause of a failure is the last thing a
7
+ * server says), and shown on the screen the person gets back, if the server failed.
8
+ */
9
+ import { StringDecoder } from "node:string_decoder";
10
+ /** How much is kept: enough for a stack trace, not for a server that never stops talking. */
11
+ export const TAIL_LIMIT = 16 * 1024;
12
+ export class Tail {
13
+ #limit;
14
+ #decoder = new StringDecoder("utf8");
15
+ #text = "";
16
+ #dropped = false;
17
+ constructor(limit = TAIL_LIMIT) {
18
+ this.#limit = limit;
19
+ }
20
+ /** Adds what the server wrote; a character cut between two chunks is kept whole. */
21
+ push(chunk) {
22
+ this.#text += typeof chunk === "string" ? chunk : this.#decoder.write(Buffer.from(chunk));
23
+ if (this.#text.length <= this.#limit)
24
+ return;
25
+ this.#dropped = true;
26
+ const cut = this.#text.length - this.#limit;
27
+ // Whole lines: a line that begins in what was dropped is not worth showing half of.
28
+ const newline = this.#text.indexOf("\n", cut);
29
+ const whole = newline === -1 ? "" : this.#text.slice(newline + 1);
30
+ // A single line longer than the limit has no whole line to keep: its end is.
31
+ this.#text = whole === "" ? this.#text.slice(cut) : whole;
32
+ }
33
+ /** What is kept, as the person is shown it: text, not terminal commands (see {@link plain}). */
34
+ text() {
35
+ return (this.#dropped ? "[earlier output left out]\n" : "") + plain(this.#text);
36
+ }
37
+ get empty() {
38
+ return this.#text === "";
39
+ }
40
+ /** Forgets what is kept: it has been shown, and what comes next is about the next run of the server. */
41
+ clear() {
42
+ this.#text = "";
43
+ this.#dropped = false;
44
+ }
45
+ }
46
+ /**
47
+ * Text a server wrote, made safe to put on the person's terminal: what is not a
48
+ * character to read (an escape, which could be a command to the terminal, among
49
+ * them) is written out as `\u001b`, and a carriage return is a line end. Line ends and
50
+ * tabs stay.
51
+ */
52
+ export function plain(text) {
53
+ return text.replace(/\r\n?/g, "\n").replace(
54
+ // C0 other than tab and line feed, DEL and C1.
55
+ /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
56
+ }
57
+ //# sourceMappingURL=tail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tail.js","sourceRoot":"","sources":["../src/tail.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,6FAA6F;AAC7F,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,GAAG,IAAI,CAAC;AAEpC,MAAM,OAAO,IAAI;IACN,MAAM,CAAS;IACf,QAAQ,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,KAAK,GAAG,EAAE,CAAC;IACX,QAAQ,GAAG,KAAK,CAAC;IAEjB,YAAY,KAAK,GAAW,UAAU;QACpC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,oFAAoF;IACpF,IAAI,CAAC,KAA0B;QAC7B,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1F,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QAC7C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC5C,oFAAoF;QACpF,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QAClE,6EAA6E;QAC7E,IAAI,CAAC,KAAK,GAAG,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAC5D,CAAC;IAED,gGAAgG;IAChG,IAAI;QACF,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClF,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC;IAC3B,CAAC;IAED,wGAAwG;IACxG,KAAK;QACH,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;IACxB,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,IAAY;IAChC,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,OAAO;IACzC,+CAA+C;IAC/C,4CAA4C,EAC5C,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAC7D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Watching files with `node:fs`, for `pencere dev`.
3
+ *
4
+ * `fs.watch` says that something under a path changed, not what, and says it more
5
+ * than once for one write; that is enough, because `Reloader` waits for the files
6
+ * to be quiet and looks at what they hold before it acts. A directory is watched
7
+ * recursively; a file is watched through the directory it is in, because an
8
+ * editor that saves by writing a new file and renaming it over the old one leaves
9
+ * a watch on the file itself watching a file that is gone.
10
+ */
11
+ /** What a watched path is for: the view's files, or the server's own. */
12
+ export type Role = "view" | "server";
13
+ /** A path to watch and what a change to it means. */
14
+ export interface Root {
15
+ readonly path: string;
16
+ readonly role: Role;
17
+ }
18
+ /** A set of watches, to end. */
19
+ export interface Watching {
20
+ close(): void;
21
+ }
22
+ /** Starts watching `roots`; `onChange` is told the role of a root something changed under. */
23
+ export type Watch = (roots: readonly Root[], onChange: (role: Role) => void, onError: (message: string) => void) => Watching;
24
+ /** A path that cannot be watched: it is not there, or the system would not. */
25
+ export declare class WatchError extends Error {
26
+ readonly missing: boolean;
27
+ constructor(message: string, missing: boolean);
28
+ }
29
+ /**
30
+ * Whether a path under a watched directory is nothing a person edits: a name that
31
+ * begins with a dot (a view's own walk leaves them out too), the packages a
32
+ * project installs and the bytecode a language writes beside its source, which a
33
+ * server writes by running and so would end every run with a change.
34
+ */
35
+ export declare function isNoise(relative: string): boolean;
36
+ /**
37
+ * Watches `roots` with `fs.watch`.
38
+ *
39
+ * @throws {WatchError} when a root cannot be watched; what was started before it is ended.
40
+ */
41
+ export declare const watchRoots: Watch;
42
+ /**
43
+ * What the paths hold, in a form that changes when they do: the size and the
44
+ * modification time of every file under them, noise left out. A path that is not
45
+ * there is part of it, so that a file that went away is a change too.
46
+ */
47
+ export declare function fingerprintOf(paths: readonly string[]): Promise<string>;
package/dist/watch.js ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Watching files with `node:fs`, for `pencere dev`.
3
+ *
4
+ * `fs.watch` says that something under a path changed, not what, and says it more
5
+ * than once for one write; that is enough, because `Reloader` waits for the files
6
+ * to be quiet and looks at what they hold before it acts. A directory is watched
7
+ * recursively; a file is watched through the directory it is in, because an
8
+ * editor that saves by writing a new file and renaming it over the old one leaves
9
+ * a watch on the file itself watching a file that is gone.
10
+ */
11
+ import { statSync, watch as watchPath } from "node:fs";
12
+ import { readdir, stat } from "node:fs/promises";
13
+ import { basename, dirname, join } from "node:path";
14
+ /** A path that cannot be watched: it is not there, or the system would not. */
15
+ export class WatchError extends Error {
16
+ missing;
17
+ constructor(message, missing) {
18
+ super(message);
19
+ this.name = "WatchError";
20
+ this.missing = missing;
21
+ }
22
+ }
23
+ /**
24
+ * Whether a path under a watched directory is nothing a person edits: a name that
25
+ * begins with a dot (a view's own walk leaves them out too), the packages a
26
+ * project installs and the bytecode a language writes beside its source, which a
27
+ * server writes by running and so would end every run with a change.
28
+ */
29
+ export function isNoise(relative) {
30
+ return relative
31
+ .split(/[\\/]/)
32
+ .some((part) => part.startsWith(".") || part === "node_modules" || part === "__pycache__");
33
+ }
34
+ function describe(error) {
35
+ const code = error?.code;
36
+ if (code === "ENOENT")
37
+ return "no such file or directory";
38
+ if (code === "EACCES")
39
+ return "permission denied";
40
+ if (code === "EMFILE" || code === "ENOSPC")
41
+ return "the system has no more watches to give";
42
+ return error instanceof Error ? error.message : String(error);
43
+ }
44
+ /**
45
+ * Watches `roots` with `fs.watch`.
46
+ *
47
+ * @throws {WatchError} when a root cannot be watched; what was started before it is ended.
48
+ */
49
+ export const watchRoots = (roots, onChange, onError) => {
50
+ const watchers = [];
51
+ const close = () => {
52
+ for (const watcher of watchers.splice(0))
53
+ watcher.close();
54
+ };
55
+ try {
56
+ for (const root of roots)
57
+ watchers.push(watchRoot(root, onChange, onError));
58
+ }
59
+ catch (error) {
60
+ close();
61
+ throw error;
62
+ }
63
+ return { close };
64
+ };
65
+ function watchRoot(root, onChange, onError) {
66
+ let isDirectory;
67
+ try {
68
+ isDirectory = statSync(root.path).isDirectory();
69
+ }
70
+ catch (error) {
71
+ const missing = error?.code === "ENOENT";
72
+ throw new WatchError(`cannot watch ${root.path}: ${describe(error)}`, missing);
73
+ }
74
+ const heard = (name) => {
75
+ // `null` is a system that does not say which file: it may be this one.
76
+ if (name === null)
77
+ return onChange(root.role);
78
+ const text = name.toString();
79
+ if (isDirectory ? !isNoise(text) : text === basename(root.path))
80
+ onChange(root.role);
81
+ };
82
+ let watcher;
83
+ try {
84
+ watcher = isDirectory
85
+ ? watchPath(root.path, { recursive: true }, (_event, name) => heard(name))
86
+ : watchPath(dirname(root.path), (_event, name) => heard(name));
87
+ }
88
+ catch (error) {
89
+ throw new WatchError(`cannot watch ${root.path}: ${describe(error)}`, false);
90
+ }
91
+ watcher.on("error", (error) => {
92
+ watcher.close();
93
+ onError(`stopped watching ${root.path}: ${describe(error)}`);
94
+ });
95
+ return watcher;
96
+ }
97
+ /**
98
+ * What the paths hold, in a form that changes when they do: the size and the
99
+ * modification time of every file under them, noise left out. A path that is not
100
+ * there is part of it, so that a file that went away is a change too.
101
+ */
102
+ export async function fingerprintOf(paths) {
103
+ const lines = [];
104
+ const visit = async (path) => {
105
+ let info;
106
+ try {
107
+ info = await stat(path);
108
+ }
109
+ catch {
110
+ lines.push(`${path} missing`);
111
+ return;
112
+ }
113
+ if (!info.isDirectory()) {
114
+ lines.push(`${path} ${info.size} ${info.mtimeMs}`);
115
+ return;
116
+ }
117
+ let entries;
118
+ try {
119
+ entries = await readdir(path, { withFileTypes: true });
120
+ }
121
+ catch {
122
+ lines.push(`${path} unreadable`);
123
+ return;
124
+ }
125
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
126
+ for (const entry of entries) {
127
+ // A link to a directory is not followed, so that a loop cannot trap the walk.
128
+ if (isNoise(entry.name) ||
129
+ (entry.isSymbolicLink() && (await isDirectoryLink(join(path, entry.name)))))
130
+ continue;
131
+ await visit(join(path, entry.name));
132
+ }
133
+ };
134
+ for (const path of paths)
135
+ await visit(path);
136
+ return lines.join("\n");
137
+ }
138
+ async function isDirectoryLink(path) {
139
+ try {
140
+ return (await stat(path)).isDirectory();
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ }
146
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"}