@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.
- package/CHANGELOG.md +44 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +133 -2
- package/dist/args.d.ts +36 -0
- package/dist/args.js +139 -0
- package/dist/args.js.map +1 -0
- package/dist/connector.d.ts +44 -0
- package/dist/connector.js +90 -0
- package/dist/connector.js.map +1 -0
- package/dist/dev.d.ts +86 -0
- package/dist/dev.js +254 -0
- package/dist/dev.js.map +1 -0
- package/dist/discover.d.ts +65 -0
- package/dist/discover.js +110 -0
- package/dist/discover.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +21 -0
- package/dist/main.js.map +1 -0
- package/dist/run.d.ts +69 -0
- package/dist/run.js +446 -0
- package/dist/run.js.map +1 -0
- package/dist/server-process.d.ts +82 -0
- package/dist/server-process.js +322 -0
- package/dist/server-process.js.map +1 -0
- package/dist/tail.d.ts +28 -0
- package/dist/tail.js +57 -0
- package/dist/tail.js.map +1 -0
- package/dist/watch.d.ts +47 -0
- package/dist/watch.js +146 -0
- package/dist/watch.js.map +1 -0
- package/package.json +42 -4
- package/src/args.ts +158 -0
- package/src/connector.ts +107 -0
- package/src/dev.ts +303 -0
- package/src/discover.ts +137 -0
- package/src/main.ts +22 -0
- package/src/run.ts +538 -0
- package/src/server-process.ts +368 -0
- package/src/tail.ts +66 -0
- package/src/watch.ts +167 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The program's MCP server, stood in for by a process.
|
|
3
|
+
*
|
|
4
|
+
* `serve` of the SDK takes anything that can `connect` a transport and `close`
|
|
5
|
+
* the session, and does everything else of a pencere program: the question to the
|
|
6
|
+
* terminal, the token, the bundle, ⌃C and `bye`, the host starting the view again
|
|
7
|
+
* (§10.7) and the program reopening it (§10.8). It connects once for each view it
|
|
8
|
+
* opens. This is that server for a program that has none of its own: it starts the
|
|
9
|
+
* command for each connection and carries the JSON-RPC messages between the
|
|
10
|
+
* terminal's stream and the command's stdio, line by line, untouched. A server
|
|
11
|
+
* that has been connected to a view once has done its `initialize`; the next view
|
|
12
|
+
* is a new session, and so a new process, which is also what a stdio server
|
|
13
|
+
* expects: one `initialize` for the life of the process.
|
|
14
|
+
*/
|
|
15
|
+
import { CLOSE_TIMING, ServerProcess } from "./server-process.js";
|
|
16
|
+
export class ProcessConnector {
|
|
17
|
+
#spec;
|
|
18
|
+
#options;
|
|
19
|
+
#current;
|
|
20
|
+
#ending = new Set();
|
|
21
|
+
#starts = 0;
|
|
22
|
+
constructor(spec, options) {
|
|
23
|
+
this.#spec = spec;
|
|
24
|
+
this.#options = options;
|
|
25
|
+
}
|
|
26
|
+
/** How many processes this connector has started: one for each view that was opened. */
|
|
27
|
+
get starts() {
|
|
28
|
+
return this.#starts;
|
|
29
|
+
}
|
|
30
|
+
/** @throws {StartError} when the command cannot be started. */
|
|
31
|
+
async connect(transport) {
|
|
32
|
+
// One process for one session: the last is gone before the next begins.
|
|
33
|
+
await this.close();
|
|
34
|
+
const { tail, unexpectedExit, timing } = this.#options;
|
|
35
|
+
const server = new ServerProcess(this.#spec, tail, timing ?? CLOSE_TIMING);
|
|
36
|
+
server.onmessage = (message) => {
|
|
37
|
+
// A view that is gone has no use for the answer, and says so by failing here.
|
|
38
|
+
transport.send(message).catch(() => { });
|
|
39
|
+
};
|
|
40
|
+
server.onerror = (error) => tail.push(`pencere: ${error.message}\n`);
|
|
41
|
+
server.onexit = (exit) => {
|
|
42
|
+
if (!exit.expected)
|
|
43
|
+
unexpectedExit(exit);
|
|
44
|
+
};
|
|
45
|
+
transport.onmessage = (message) => {
|
|
46
|
+
// A server that is gone cannot be written to, and says so by ending (`unexpectedExit`).
|
|
47
|
+
server.send(message).catch(() => { });
|
|
48
|
+
};
|
|
49
|
+
transport.onclose = () => {
|
|
50
|
+
void server.close();
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
await transport.start();
|
|
54
|
+
await server.start();
|
|
55
|
+
this.#starts += 1;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
transport.onmessage = undefined;
|
|
59
|
+
transport.onclose = undefined;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
this.#current = { server, transport };
|
|
63
|
+
}
|
|
64
|
+
/** Ends the process of the session that is connected, if there is one, and resolves when it is gone. */
|
|
65
|
+
async close() {
|
|
66
|
+
const current = this.#current;
|
|
67
|
+
this.#current = undefined;
|
|
68
|
+
if (current === undefined)
|
|
69
|
+
return;
|
|
70
|
+
const ending = current.server.close().then(() => current.transport.close());
|
|
71
|
+
this.#ending.add(ending);
|
|
72
|
+
try {
|
|
73
|
+
await ending;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
this.#ending.delete(ending);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolves when every process this connector started has ended. `serve` lets go of
|
|
81
|
+
* a session without waiting for it (a signal must not wait for a stubborn server),
|
|
82
|
+
* and a program that is about to leave has to.
|
|
83
|
+
*/
|
|
84
|
+
async drain() {
|
|
85
|
+
await this.close();
|
|
86
|
+
while (this.#ending.size > 0)
|
|
87
|
+
await Promise.all([...this.#ending]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=connector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector.js","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"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"}
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pencere dev`: what a change to the files comes to.
|
|
3
|
+
*
|
|
4
|
+
* `pencere run` shows a view once. `pencere dev` watches the files of the view and
|
|
5
|
+
* the files the server reads, and when they change it has the SDK close the view
|
|
6
|
+
* and open it again (`ViewControl.reopen`, specification §10.8): the old view and
|
|
7
|
+
* the server of its session are gone, the server is started again as for any view,
|
|
8
|
+
* and the new view's files are checked against what the terminal already holds,
|
|
9
|
+
* so only the files that changed cross the stream. Nothing of that is this file's:
|
|
10
|
+
* what is, is deciding *when*.
|
|
11
|
+
*
|
|
12
|
+
* - **A burst is one reopening.** A build writes many files; the change is acted
|
|
13
|
+
* on once the files have been quiet for a moment (`settle`), and a change that
|
|
14
|
+
* comes while a reopening is under way is acted on after it, again after a
|
|
15
|
+
* moment of quiet.
|
|
16
|
+
* - **Only what changed.** A change to the view's directory is a reopening when
|
|
17
|
+
* the files it holds are different from what the view shows (a file saved as it
|
|
18
|
+
* was, or one that is not part of a view, changes nothing). A change to
|
|
19
|
+
* `--watch`'s paths is a reopening when what the paths hold has moved (their
|
|
20
|
+
* size or modification time): the events of one write that arrive late are not
|
|
21
|
+
* a second write.
|
|
22
|
+
* - **A half-built view waits.** A build that has emptied its output directory
|
|
23
|
+
* leaves something that is not a view; that is not shown, and the next change,
|
|
24
|
+
* which is the build writing more, is looked at in its turn.
|
|
25
|
+
* - **A view that is not there yet waits too.** A change that comes before the
|
|
26
|
+
* first view is up is asked again a few times, a moment apart.
|
|
27
|
+
*
|
|
28
|
+
* What the person gets to know is said after the view has closed (`notes`): the
|
|
29
|
+
* screen is the view's while it is up. A `serve` that ended because the program ended it
|
|
30
|
+
* (its server fell) is not the end of the reloading: the program starts `serve` again, and
|
|
31
|
+
* the next change is what it waits for.
|
|
32
|
+
*/
|
|
33
|
+
import type { Bundle, ReopenOptions, ReopenResult } from "@pencere/sdk";
|
|
34
|
+
/** How long `Reloader` waits, and how often it asks again. */
|
|
35
|
+
export interface ReloadTiming {
|
|
36
|
+
/** The quiet, in milliseconds, after which the changes so far are acted on. */
|
|
37
|
+
readonly settle: number;
|
|
38
|
+
/** How long to wait, in milliseconds, before asking again for a view that is not up yet. */
|
|
39
|
+
readonly retry: number;
|
|
40
|
+
/** How many times to ask again before leaving the change for the next one to carry. */
|
|
41
|
+
readonly tries: number;
|
|
42
|
+
}
|
|
43
|
+
/** What `Reloader` needs of the program's hand on its view: `ViewControl`'s `reopen`. */
|
|
44
|
+
export interface Reopener {
|
|
45
|
+
reopen(next: ReopenOptions): Promise<ReopenResult>;
|
|
46
|
+
}
|
|
47
|
+
export interface ReloaderOptions {
|
|
48
|
+
readonly control: Reopener;
|
|
49
|
+
/** The directory the view's files are in (`--view`), absent when the terminal reads the server's own resource. */
|
|
50
|
+
readonly view?: string;
|
|
51
|
+
/** What the view was first opened with. */
|
|
52
|
+
readonly bundle?: Bundle;
|
|
53
|
+
/** What the server's own files (`--watch`) hold, in a form that changes when they do. Without it every change to them counts. */
|
|
54
|
+
readonly fingerprint?: () => Promise<string>;
|
|
55
|
+
readonly timing: ReloadTiming;
|
|
56
|
+
/** Reads the view's files. `bundleFromDirectory`, unless a test lends another. */
|
|
57
|
+
readonly load?: (directory: string) => Promise<Bundle>;
|
|
58
|
+
}
|
|
59
|
+
/** Closes the view and opens it again when what it is made of, or the server, changed. */
|
|
60
|
+
export declare class Reloader {
|
|
61
|
+
#private;
|
|
62
|
+
constructor(options: ReloaderOptions);
|
|
63
|
+
/**
|
|
64
|
+
* Notes what the view's files and the server's own hold now, as the view that is about to open
|
|
65
|
+
* sees them, and says which view's files those are. Call once the files are being watched: what
|
|
66
|
+
* was saved before is in what is read, and what is saved after tells of itself, but a save in
|
|
67
|
+
* between, with the files read first and the watching begun after, would be neither. A view
|
|
68
|
+
* directory that cannot be read just then (a build that is half done) leaves what the reloader
|
|
69
|
+
* was given, and the change that made it unreadable is looked at in its turn.
|
|
70
|
+
*/
|
|
71
|
+
start(): Promise<Bundle | undefined>;
|
|
72
|
+
/** How many times the view has been reopened. */
|
|
73
|
+
get reopened(): number;
|
|
74
|
+
/** What to tell the person once the view has closed. */
|
|
75
|
+
get notes(): readonly string[];
|
|
76
|
+
/** Something under the view's directory changed. */
|
|
77
|
+
viewChanged(): void;
|
|
78
|
+
/** Something of the server's own files changed. */
|
|
79
|
+
serverChanged(): void;
|
|
80
|
+
/** Something to tell the person afterwards that is not a change: a watcher that failed. */
|
|
81
|
+
warn(text: string): void;
|
|
82
|
+
/** No more reopenings: the view has ended, or cannot be reopened. */
|
|
83
|
+
stop(): void;
|
|
84
|
+
/** Resolves when nothing is waiting to be acted on and nothing is being acted on. */
|
|
85
|
+
settled(): Promise<void>;
|
|
86
|
+
}
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pencere dev`: what a change to the files comes to.
|
|
3
|
+
*
|
|
4
|
+
* `pencere run` shows a view once. `pencere dev` watches the files of the view and
|
|
5
|
+
* the files the server reads, and when they change it has the SDK close the view
|
|
6
|
+
* and open it again (`ViewControl.reopen`, specification §10.8): the old view and
|
|
7
|
+
* the server of its session are gone, the server is started again as for any view,
|
|
8
|
+
* and the new view's files are checked against what the terminal already holds,
|
|
9
|
+
* so only the files that changed cross the stream. Nothing of that is this file's:
|
|
10
|
+
* what is, is deciding *when*.
|
|
11
|
+
*
|
|
12
|
+
* - **A burst is one reopening.** A build writes many files; the change is acted
|
|
13
|
+
* on once the files have been quiet for a moment (`settle`), and a change that
|
|
14
|
+
* comes while a reopening is under way is acted on after it, again after a
|
|
15
|
+
* moment of quiet.
|
|
16
|
+
* - **Only what changed.** A change to the view's directory is a reopening when
|
|
17
|
+
* the files it holds are different from what the view shows (a file saved as it
|
|
18
|
+
* was, or one that is not part of a view, changes nothing). A change to
|
|
19
|
+
* `--watch`'s paths is a reopening when what the paths hold has moved (their
|
|
20
|
+
* size or modification time): the events of one write that arrive late are not
|
|
21
|
+
* a second write.
|
|
22
|
+
* - **A half-built view waits.** A build that has emptied its output directory
|
|
23
|
+
* leaves something that is not a view; that is not shown, and the next change,
|
|
24
|
+
* which is the build writing more, is looked at in its turn.
|
|
25
|
+
* - **A view that is not there yet waits too.** A change that comes before the
|
|
26
|
+
* first view is up is asked again a few times, a moment apart.
|
|
27
|
+
*
|
|
28
|
+
* What the person gets to know is said after the view has closed (`notes`): the
|
|
29
|
+
* screen is the view's while it is up. A `serve` that ended because the program ended it
|
|
30
|
+
* (its server fell) is not the end of the reloading: the program starts `serve` again, and
|
|
31
|
+
* the next change is what it waits for.
|
|
32
|
+
*/
|
|
33
|
+
import { BundleError, bundleFromDirectory } from "@pencere/sdk";
|
|
34
|
+
function merged(a, b) {
|
|
35
|
+
return a === undefined ? b : { view: a.view || b.view, server: a.server || b.server };
|
|
36
|
+
}
|
|
37
|
+
/** Whether `bundle` holds the files `shown` does, byte for byte, under the same entry. */
|
|
38
|
+
function sameFiles(bundle, shown) {
|
|
39
|
+
return shown !== undefined && JSON.stringify(bundle.manifest) === JSON.stringify(shown.manifest);
|
|
40
|
+
}
|
|
41
|
+
function why(error) {
|
|
42
|
+
if (error instanceof BundleError)
|
|
43
|
+
return error.message;
|
|
44
|
+
const code = error?.code;
|
|
45
|
+
if (code === "ENOENT")
|
|
46
|
+
return "a file went away while it was read";
|
|
47
|
+
return error instanceof Error ? error.message : String(error);
|
|
48
|
+
}
|
|
49
|
+
/** Closes the view and opens it again when what it is made of, or the server, changed. */
|
|
50
|
+
export class Reloader {
|
|
51
|
+
#options;
|
|
52
|
+
#notes = [];
|
|
53
|
+
#waiting = [];
|
|
54
|
+
/** The files the view shows now. */
|
|
55
|
+
#shown;
|
|
56
|
+
/** What the server's files held when the view was last opened. */
|
|
57
|
+
#serverPrint;
|
|
58
|
+
/** Changes to act on once the files are quiet. */
|
|
59
|
+
#pending;
|
|
60
|
+
/** Changes that could not be acted on, which the next change carries along. */
|
|
61
|
+
#held;
|
|
62
|
+
#timer;
|
|
63
|
+
#running;
|
|
64
|
+
#stopped = false;
|
|
65
|
+
#tries = 0;
|
|
66
|
+
/** Why the view's files could not be read at the last change, while that is so. */
|
|
67
|
+
#problem;
|
|
68
|
+
#reopened = 0;
|
|
69
|
+
constructor(options) {
|
|
70
|
+
this.#options = options;
|
|
71
|
+
this.#shown = options.bundle;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Notes what the view's files and the server's own hold now, as the view that is about to open
|
|
75
|
+
* sees them, and says which view's files those are. Call once the files are being watched: what
|
|
76
|
+
* was saved before is in what is read, and what is saved after tells of itself, but a save in
|
|
77
|
+
* between, with the files read first and the watching begun after, would be neither. A view
|
|
78
|
+
* directory that cannot be read just then (a build that is half done) leaves what the reloader
|
|
79
|
+
* was given, and the change that made it unreadable is looked at in its turn.
|
|
80
|
+
*/
|
|
81
|
+
async start() {
|
|
82
|
+
this.#serverPrint = await this.#options.fingerprint?.();
|
|
83
|
+
const { view } = this.#options;
|
|
84
|
+
if (view !== undefined) {
|
|
85
|
+
try {
|
|
86
|
+
this.#shown = await (this.#options.load ?? bundleFromDirectory)(view);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// What it was given stays, and the next change looks again.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return this.#shown;
|
|
93
|
+
}
|
|
94
|
+
/** How many times the view has been reopened. */
|
|
95
|
+
get reopened() {
|
|
96
|
+
return this.#reopened;
|
|
97
|
+
}
|
|
98
|
+
/** What to tell the person once the view has closed. */
|
|
99
|
+
get notes() {
|
|
100
|
+
const notes = [...this.#notes];
|
|
101
|
+
if (this.#problem !== undefined)
|
|
102
|
+
notes.push(`the last change to the view's files could not be shown (${this.#problem}); the view is as the change before it left it`);
|
|
103
|
+
else if (this.#held !== undefined)
|
|
104
|
+
notes.push("the last change was not shown, because there was no view to show it in");
|
|
105
|
+
return notes;
|
|
106
|
+
}
|
|
107
|
+
/** Something under the view's directory changed. */
|
|
108
|
+
viewChanged() {
|
|
109
|
+
this.#note({ view: true, server: false });
|
|
110
|
+
}
|
|
111
|
+
/** Something of the server's own files changed. */
|
|
112
|
+
serverChanged() {
|
|
113
|
+
this.#note({ view: false, server: true });
|
|
114
|
+
}
|
|
115
|
+
/** Something to tell the person afterwards that is not a change: a watcher that failed. */
|
|
116
|
+
warn(text) {
|
|
117
|
+
this.#notes.push(text);
|
|
118
|
+
}
|
|
119
|
+
/** No more reopenings: the view has ended, or cannot be reopened. */
|
|
120
|
+
stop() {
|
|
121
|
+
this.#stopped = true;
|
|
122
|
+
this.#disarm();
|
|
123
|
+
this.#pending = undefined;
|
|
124
|
+
this.#release();
|
|
125
|
+
}
|
|
126
|
+
/** Resolves when nothing is waiting to be acted on and nothing is being acted on. */
|
|
127
|
+
settled() {
|
|
128
|
+
if (this.#idle())
|
|
129
|
+
return Promise.resolve();
|
|
130
|
+
return new Promise((resolve) => this.#waiting.push(resolve));
|
|
131
|
+
}
|
|
132
|
+
#idle() {
|
|
133
|
+
return this.#timer === undefined && this.#running === undefined && this.#pending === undefined;
|
|
134
|
+
}
|
|
135
|
+
#release() {
|
|
136
|
+
if (!this.#idle())
|
|
137
|
+
return;
|
|
138
|
+
for (const resolve of this.#waiting.splice(0))
|
|
139
|
+
resolve();
|
|
140
|
+
}
|
|
141
|
+
#note(change) {
|
|
142
|
+
if (this.#stopped)
|
|
143
|
+
return;
|
|
144
|
+
// What could not be acted on before is carried along with this one.
|
|
145
|
+
const carried = merged(this.#pending, change);
|
|
146
|
+
this.#pending = this.#held === undefined ? carried : merged(carried, this.#held);
|
|
147
|
+
this.#held = undefined;
|
|
148
|
+
this.#tries = 0;
|
|
149
|
+
this.#arm(this.#options.timing.settle);
|
|
150
|
+
}
|
|
151
|
+
#arm(delay) {
|
|
152
|
+
this.#disarm();
|
|
153
|
+
this.#timer = setTimeout(() => {
|
|
154
|
+
this.#timer = undefined;
|
|
155
|
+
this.#kick();
|
|
156
|
+
}, delay);
|
|
157
|
+
}
|
|
158
|
+
#disarm() {
|
|
159
|
+
if (this.#timer !== undefined)
|
|
160
|
+
clearTimeout(this.#timer);
|
|
161
|
+
this.#timer = undefined;
|
|
162
|
+
}
|
|
163
|
+
#kick() {
|
|
164
|
+
if (this.#running !== undefined || this.#stopped)
|
|
165
|
+
return;
|
|
166
|
+
this.#running = this.#pass().finally(() => {
|
|
167
|
+
this.#running = undefined;
|
|
168
|
+
// What came while this ran has had its moment of quiet only if nothing came since.
|
|
169
|
+
if (!this.#stopped && this.#pending !== undefined && this.#timer === undefined)
|
|
170
|
+
this.#arm(this.#options.timing.settle);
|
|
171
|
+
this.#release();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async #pass() {
|
|
175
|
+
const change = this.#pending;
|
|
176
|
+
this.#pending = undefined;
|
|
177
|
+
if (change === undefined)
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
await this.#act(change);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
// Looking at the files must not end the view the person is looking at.
|
|
184
|
+
this.#notes.push(`could not look at the files: ${why(error)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async #act(change) {
|
|
188
|
+
const { view, fingerprint } = this.#options;
|
|
189
|
+
let serverPrint;
|
|
190
|
+
let serverMoved = false;
|
|
191
|
+
if (change.server) {
|
|
192
|
+
serverPrint = await fingerprint?.();
|
|
193
|
+
serverMoved = serverPrint === undefined || serverPrint !== this.#serverPrint;
|
|
194
|
+
}
|
|
195
|
+
let bundle;
|
|
196
|
+
let viewMoved = false;
|
|
197
|
+
if (change.view && view !== undefined) {
|
|
198
|
+
try {
|
|
199
|
+
bundle = await (this.#options.load ?? bundleFromDirectory)(view);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// A build that is half done: the next change is the rest of it.
|
|
203
|
+
this.#problem = why(error);
|
|
204
|
+
this.#held = merged(this.#held, change);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.#problem = undefined;
|
|
208
|
+
viewMoved = !sameFiles(bundle, this.#shown);
|
|
209
|
+
}
|
|
210
|
+
if (!viewMoved && !serverMoved)
|
|
211
|
+
return;
|
|
212
|
+
const result = await this.#options.control.reopen(viewMoved && bundle !== undefined ? { bundle } : {});
|
|
213
|
+
switch (result.kind) {
|
|
214
|
+
case "reopened":
|
|
215
|
+
this.#reopened += 1;
|
|
216
|
+
if (viewMoved)
|
|
217
|
+
this.#shown = bundle;
|
|
218
|
+
if (serverMoved)
|
|
219
|
+
this.#serverPrint = serverPrint;
|
|
220
|
+
return;
|
|
221
|
+
case "unsupported":
|
|
222
|
+
this.#notes.push("this terminal does not reopen a view when asked, so `pencere dev` ran the view as `pencere run` does and the changes were not shown: close it and run it again to see them");
|
|
223
|
+
this.stop();
|
|
224
|
+
return;
|
|
225
|
+
case "no-view":
|
|
226
|
+
if (result.reason === "aborted") {
|
|
227
|
+
// `serve` was ended by the program's own hand, which is not the terminal refusing:
|
|
228
|
+
// the server fell, and what comes next is the program's to decide. The files that
|
|
229
|
+
// were asked for are what came of it, and the same files are not tried again.
|
|
230
|
+
if (viewMoved)
|
|
231
|
+
this.#shown = bundle;
|
|
232
|
+
if (serverMoved)
|
|
233
|
+
this.#serverPrint = serverPrint;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// The new view was refused or could not start: `serve` says why as it ends.
|
|
237
|
+
this.stop();
|
|
238
|
+
return;
|
|
239
|
+
case "unavailable": {
|
|
240
|
+
const { retry, tries } = this.#options.timing;
|
|
241
|
+
if (this.#tries < tries) {
|
|
242
|
+
this.#tries += 1;
|
|
243
|
+
this.#pending = merged(this.#pending, change);
|
|
244
|
+
this.#arm(retry);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.#held = merged(this.#held, change);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=dev.js.map
|
package/dist/dev.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../src/dev.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tool's view to show, when the person did not say.
|
|
3
|
+
*
|
|
4
|
+
* The server is started once, for as long as it takes to ask it: `initialize` and
|
|
5
|
+
* `tools/list`, as any MCP client does. The tool that names a view is the one
|
|
6
|
+
* whose `_meta.ui.resourceUri` does (MCP Apps, Resource Discovery; the flat
|
|
7
|
+
* `_meta["ui/resourceUri"]` of the first drafts, which the extension's own
|
|
8
|
+
* libraries still read, counts too). One such tool is the answer. None, or several,
|
|
9
|
+
* is for the person to settle, and the answer says which tools there were. The list is read page
|
|
10
|
+
* by page, as far as the terminal reads it ({@link MAX_TOOL_PAGES}).
|
|
11
|
+
*/
|
|
12
|
+
import type { CloseTiming, ServerCommand } from "./server-process.js";
|
|
13
|
+
import type { Tail } from "./tail.js";
|
|
14
|
+
/**
|
|
15
|
+
* The most pages of tools that are read: what the terminal reads of the same server, so that a
|
|
16
|
+
* tool the terminal would not find is not the tool this finds, and a server that never stops
|
|
17
|
+
* listing does not keep this asking.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_TOOL_PAGES = 16;
|
|
20
|
+
/** What the server has with it. */
|
|
21
|
+
export type Discovery = {
|
|
22
|
+
readonly kind: "found";
|
|
23
|
+
readonly tool: string;
|
|
24
|
+
}
|
|
25
|
+
/** No tool names a view. `tools` is every tool the server has. */
|
|
26
|
+
| {
|
|
27
|
+
readonly kind: "none";
|
|
28
|
+
readonly tools: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/** Several do: their names. */
|
|
31
|
+
| {
|
|
32
|
+
readonly kind: "many";
|
|
33
|
+
readonly tools: readonly string[];
|
|
34
|
+
};
|
|
35
|
+
export interface DiscoverOptions {
|
|
36
|
+
/** The version `pencere` calls itself, in the handshake. */
|
|
37
|
+
readonly version: string;
|
|
38
|
+
/** How long the server has to answer each question, in milliseconds. */
|
|
39
|
+
readonly timeout: number;
|
|
40
|
+
/** The person interrupted: the server is let go and the question ends. */
|
|
41
|
+
readonly signal?: AbortSignal;
|
|
42
|
+
readonly timing?: CloseTiming;
|
|
43
|
+
}
|
|
44
|
+
/** The server was started and did not answer as an MCP server does. */
|
|
45
|
+
export declare class DiscoveryError extends Error {
|
|
46
|
+
constructor(message: string, options?: ErrorOptions);
|
|
47
|
+
}
|
|
48
|
+
/** The person interrupted while the server was being asked. */
|
|
49
|
+
export declare class Interrupted extends Error {
|
|
50
|
+
constructor();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The `ui://` resource a tool names, in either of the two places MCP Apps has had it. An address
|
|
54
|
+
* of another scheme is no view: the terminal reads `ui://` resources only, and a tool it would
|
|
55
|
+
* refuse is not a tool to show.
|
|
56
|
+
*/
|
|
57
|
+
export declare function viewOf(tool: {
|
|
58
|
+
readonly _meta?: unknown;
|
|
59
|
+
}): string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* @throws {StartError} when the command cannot be started
|
|
62
|
+
* @throws {DiscoveryError} when it started and did not answer as an MCP server
|
|
63
|
+
* @throws {Interrupted} when `signal` aborted
|
|
64
|
+
*/
|
|
65
|
+
export declare function discoverTool(spec: ServerCommand, tail: Tail, options: DiscoverOptions): Promise<Discovery>;
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tool's view to show, when the person did not say.
|
|
3
|
+
*
|
|
4
|
+
* The server is started once, for as long as it takes to ask it: `initialize` and
|
|
5
|
+
* `tools/list`, as any MCP client does. The tool that names a view is the one
|
|
6
|
+
* whose `_meta.ui.resourceUri` does (MCP Apps, Resource Discovery; the flat
|
|
7
|
+
* `_meta["ui/resourceUri"]` of the first drafts, which the extension's own
|
|
8
|
+
* libraries still read, counts too). One such tool is the answer. None, or several,
|
|
9
|
+
* is for the person to settle, and the answer says which tools there were. The list is read page
|
|
10
|
+
* by page, as far as the terminal reads it ({@link MAX_TOOL_PAGES}).
|
|
11
|
+
*/
|
|
12
|
+
import { Client } from "@modelcontextprotocol/client";
|
|
13
|
+
import { ServerProcess, StartError } from "./server-process.js";
|
|
14
|
+
import { plain } from "./tail.js";
|
|
15
|
+
/**
|
|
16
|
+
* The most pages of tools that are read: what the terminal reads of the same server, so that a
|
|
17
|
+
* tool the terminal would not find is not the tool this finds, and a server that never stops
|
|
18
|
+
* listing does not keep this asking.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_TOOL_PAGES = 16;
|
|
21
|
+
/** The server was started and did not answer as an MCP server does. */
|
|
22
|
+
export class DiscoveryError extends Error {
|
|
23
|
+
constructor(message, options) {
|
|
24
|
+
super(message, options);
|
|
25
|
+
this.name = "DiscoveryError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** The person interrupted while the server was being asked. */
|
|
29
|
+
export class Interrupted extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("interrupted");
|
|
32
|
+
this.name = "Interrupted";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The `ui://` resource a tool names, in either of the two places MCP Apps has had it. An address
|
|
37
|
+
* of another scheme is no view: the terminal reads `ui://` resources only, and a tool it would
|
|
38
|
+
* refuse is not a tool to show.
|
|
39
|
+
*/
|
|
40
|
+
export function viewOf(tool) {
|
|
41
|
+
const meta = tool._meta;
|
|
42
|
+
if (typeof meta !== "object" || meta === null)
|
|
43
|
+
return undefined;
|
|
44
|
+
const record = meta;
|
|
45
|
+
const ui = record.ui;
|
|
46
|
+
if (typeof ui === "object" && ui !== null) {
|
|
47
|
+
const uri = ui.resourceUri;
|
|
48
|
+
if (typeof uri === "string" && uri.startsWith("ui://"))
|
|
49
|
+
return uri;
|
|
50
|
+
}
|
|
51
|
+
const flat = record["ui/resourceUri"];
|
|
52
|
+
return typeof flat === "string" && flat.startsWith("ui://") ? flat : undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @throws {StartError} when the command cannot be started
|
|
56
|
+
* @throws {DiscoveryError} when it started and did not answer as an MCP server
|
|
57
|
+
* @throws {Interrupted} when `signal` aborted
|
|
58
|
+
*/
|
|
59
|
+
export async function discoverTool(spec, tail, options) {
|
|
60
|
+
const { signal, timeout } = options;
|
|
61
|
+
if (signal?.aborted)
|
|
62
|
+
throw new Interrupted();
|
|
63
|
+
const server = new ServerProcess(spec, tail, options.timing);
|
|
64
|
+
const client = new Client({ name: "pencere", version: options.version });
|
|
65
|
+
// Closing the process fails whatever the client is waiting for.
|
|
66
|
+
const abort = () => void server.close();
|
|
67
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
68
|
+
try {
|
|
69
|
+
await client.connect(server, { timeout });
|
|
70
|
+
const tools = [];
|
|
71
|
+
let cursor;
|
|
72
|
+
// A server that says it has no tools is asked for none, as `listTools` would not ask it.
|
|
73
|
+
const asking = client.getServerCapabilities()?.tools !== undefined;
|
|
74
|
+
for (let pages = 1; asking; pages += 1) {
|
|
75
|
+
// A page at a time, as the terminal reads them: `listTools` without a cursor reads
|
|
76
|
+
// them all, to a limit of its own, which is not the terminal's.
|
|
77
|
+
const page = await client.request({ method: "tools/list", params: cursor === undefined ? {} : { cursor } }, { timeout });
|
|
78
|
+
tools.push(...page.tools);
|
|
79
|
+
cursor = page.nextCursor;
|
|
80
|
+
if (cursor === undefined)
|
|
81
|
+
break;
|
|
82
|
+
if (pages >= MAX_TOOL_PAGES)
|
|
83
|
+
throw new DiscoveryError(`the server has more than ${MAX_TOOL_PAGES} pages of tools`);
|
|
84
|
+
}
|
|
85
|
+
const withView = tools.filter((tool) => viewOf(tool) !== undefined).map((tool) => tool.name);
|
|
86
|
+
const [first, ...others] = withView;
|
|
87
|
+
if (first === undefined)
|
|
88
|
+
return { kind: "none", tools: tools.map((tool) => tool.name) };
|
|
89
|
+
return others.length === 0 ? { kind: "found", tool: first } : { kind: "many", tools: withView };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (signal?.aborted)
|
|
93
|
+
throw new Interrupted();
|
|
94
|
+
// A process that could not be started says so itself, wherever the client put it.
|
|
95
|
+
if (error instanceof StartError)
|
|
96
|
+
throw error;
|
|
97
|
+
if (error instanceof Error && error.cause instanceof StartError)
|
|
98
|
+
throw error.cause;
|
|
99
|
+
if (error instanceof DiscoveryError)
|
|
100
|
+
throw error;
|
|
101
|
+
// What the server said is shown as text: an error message is its to write.
|
|
102
|
+
throw new DiscoveryError(`"${spec.command}" did not answer as an MCP server that speaks stdio does: ${plain(error instanceof Error ? error.message : String(error))}`, { cause: error });
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
signal?.removeEventListener("abort", abort);
|
|
106
|
+
await client.close().catch(() => { });
|
|
107
|
+
await server.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"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"}
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** The `pencere` command. */
|
|
3
|
+
import { run, systemEnvironment } from "./run.js";
|
|
4
|
+
// A terminal that is gone (ssh dropped, a window closed) fails what is written to it and read
|
|
5
|
+
// from it, as an `error` of the stream. Nothing listens for it by default, and an `error` nobody
|
|
6
|
+
// listens for ends the process in the middle of letting go of the server, which is then left running.
|
|
7
|
+
for (const stream of [process.stdin, process.stdout, process.stderr])
|
|
8
|
+
stream.on("error", () => { });
|
|
9
|
+
const finish = await run(process.argv.slice(2), systemEnvironment());
|
|
10
|
+
if ("signal" in finish) {
|
|
11
|
+
// What the person sent (or the terminal, going away) ended the view and the server, and now
|
|
12
|
+
// ends this process as it would have ended any other: the shell reads the signal in the
|
|
13
|
+
// status (130, 143, 129).
|
|
14
|
+
process.kill(process.pid, finish.signal);
|
|
15
|
+
// The signal is delivered at once; this is for a platform where it is not.
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
17
|
+
process.exit({ SIGINT: 130, SIGTERM: 143, SIGHUP: 129 }[finish.signal]);
|
|
18
|
+
}
|
|
19
|
+
// A server's pipes or a stream the terminal left open must not keep the process.
|
|
20
|
+
process.exit(finish.code);
|
|
21
|
+
//# sourceMappingURL=main.js.map
|