chesspal 0.3.5 → 0.4.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/README.md +1 -0
- package/dist/cli/cli.js +17 -4
- package/dist/cli/command-line.js +22 -12
- package/dist/cli/icon.d.ts +2 -0
- package/dist/cli/icon.js +11 -1
- package/dist/cli/prompt-hook.d.ts +17 -0
- package/dist/cli/prompt-hook.js +100 -0
- package/dist/commands/analysis/analyse.d.ts +2 -2
- package/dist/commands/analysis/analyse.js +10 -16
- package/dist/commands/analysis/annotate.d.ts +8 -0
- package/dist/commands/analysis/annotate.js +19 -3
- package/dist/commands/answers/agents.d.ts +17 -7
- package/dist/commands/answers/agents.js +46 -16
- package/dist/commands/answers/clock.js +2 -1
- package/dist/commands/answers/drawings.js +6 -2
- package/dist/commands/answers/index.d.ts +4 -3
- package/dist/commands/answers/index.js +4 -3
- package/dist/commands/answers/lines.d.ts +5 -3
- package/dist/commands/answers/lines.js +17 -9
- package/dist/commands/answers/opening-book.d.ts +13 -7
- package/dist/commands/answers/opening-book.js +34 -38
- package/dist/commands/answers/opening-tags.d.ts +4 -0
- package/dist/commands/answers/opening-tags.js +11 -0
- package/dist/commands/answers/play-move.d.ts +3 -1
- package/dist/commands/answers/play-move.js +9 -6
- package/dist/commands/answers/say.js +2 -1
- package/dist/commands/answers/summary.d.ts +7 -3
- package/dist/commands/answers/summary.js +16 -5
- package/dist/commands/answers/view.d.ts +1 -1
- package/dist/commands/answers/view.js +2 -2
- package/dist/commands/game/ai-move.js +4 -4
- package/dist/commands/game/coach.js +7 -6
- package/dist/commands/game/end.js +2 -2
- package/dist/commands/game/export.d.ts +1 -1
- package/dist/commands/game/index.d.ts +1 -1
- package/dist/commands/game/index.js +1 -1
- package/dist/commands/game/look.d.ts +7 -3
- package/dist/commands/game/look.js +9 -4
- package/dist/commands/game/new.js +42 -12
- package/dist/commands/game/player-move.d.ts +16 -0
- package/dist/commands/game/player-move.js +69 -19
- package/dist/commands/game/ponder.js +7 -4
- package/dist/commands/game/position.d.ts +2 -1
- package/dist/commands/game/position.js +9 -4
- package/dist/commands/game/status.d.ts +4 -2
- package/dist/commands/game/status.js +24 -14
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/index.js +3 -3
- package/dist/commands/profiles/past-games.d.ts +7 -2
- package/dist/commands/profiles/past-games.js +45 -18
- package/dist/commands/registry/context.d.ts +2 -0
- package/dist/commands/registry/context.js +5 -0
- package/dist/commands/registry/index.d.ts +1 -1
- package/dist/commands/registry/index.js +1 -1
- package/dist/commands/talk/ai-say.d.ts +1 -1
- package/dist/commands/talk/ai-say.js +7 -6
- package/dist/core/game.d.ts +13 -0
- package/dist/core/game.js +29 -3
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +3 -3
- package/dist/core/openings.d.ts +20 -6
- package/dist/core/openings.js +45 -22
- package/dist/core/pgn.d.ts +4 -0
- package/dist/core/pgn.js +35 -6
- package/dist/games/analysis.d.ts +7 -4
- package/dist/games/analysis.js +5 -3
- package/dist/games/archive.d.ts +25 -5
- package/dist/games/archive.js +52 -19
- package/dist/games/chat-start.d.ts +2 -2
- package/dist/games/chat-start.js +6 -1
- package/dist/games/current-game.d.ts +2 -2
- package/dist/games/current-game.js +3 -2
- package/dist/games/index.d.ts +3 -2
- package/dist/games/index.js +3 -2
- package/dist/games/position-shown.d.ts +24 -0
- package/dist/games/position-shown.js +51 -0
- package/dist/games/thoughts.d.ts +12 -4
- package/dist/games/thoughts.js +27 -10
- package/dist/home/index.d.ts +1 -0
- package/dist/home/index.js +1 -0
- package/dist/home/same-path.d.ts +7 -0
- package/dist/home/same-path.js +24 -0
- package/dist/host-files/host-files.d.ts +5 -0
- package/dist/host-files/host-files.js +21 -7
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/anchor.js +15 -11
- package/dist/page/app/app.js +49 -14
- package/dist/page/app/served.js +30 -38
- package/dist/page/app/watch.js +97 -0
- package/dist/page/avatars/torretta.css +108 -0
- package/dist/page/base.css +1 -1
- package/dist/page/board/board.css +11 -8
- package/dist/page/board/marks.js +2 -2
- package/dist/page/board/pointer.js +2 -2
- package/dist/page/dialogs/analyses.js +5 -3
- package/dist/page/dialogs/dialogs.css +3 -1
- package/dist/page/dialogs/rows.js +72 -0
- package/dist/page/dialogs/site-games.js +3 -1
- package/dist/page/game/column.js +10 -8
- package/dist/page/game/game.css +137 -16
- package/dist/page/game/move-list.js +7 -5
- package/dist/page/game/plies.js +4 -0
- package/dist/page/game/review-progress.js +11 -0
- package/dist/page/guide/drawings/drawings.css +0 -1
- package/dist/page/guide/en.md +2 -2
- package/dist/page/guide/it.md +2 -2
- package/dist/page/helps/helps.css +5 -5
- package/dist/page/helps/helps.js +3 -181
- package/dist/page/helps/rows.js +16 -0
- package/dist/page/helps/strategy.js +105 -0
- package/dist/page/helps/tactics.js +69 -0
- package/dist/page/layout/header.js +49 -6
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/layout.css +49 -17
- package/dist/page/layout/window.js +45 -6
- package/dist/page/lead/lead.js +41 -4
- package/dist/page/legend/legend.css +4 -16
- package/dist/page/legend/legend.js +28 -15
- package/dist/page/profile/settings.js +1 -0
- package/dist/page/sounds/sounds.js +1 -1
- package/dist/page/style.css +1 -0
- package/dist/page/table/action.js +42 -0
- package/dist/page/table/table.css +57 -39
- package/dist/page/table/window.js +67 -67
- package/dist/page/talk/talk.css +52 -118
- package/dist/page/talk/talk.js +65 -20
- package/dist/page/talk/voice.js +92 -21
- package/dist/page/text.js +34 -8
- package/dist/page/theme.css +13 -0
- package/dist/profiles/profiles.js +5 -1
- package/dist/server/bell/bell.d.ts +24 -29
- package/dist/server/bell/bell.js +79 -133
- package/dist/server/bell/index.d.ts +1 -1
- package/dist/server/bell/index.js +2 -2
- package/dist/server/mcp/mcp.js +49 -7
- package/dist/server/page/analyses.js +15 -4
- package/dist/server/page/chat.d.ts +5 -1
- package/dist/server/page/chat.js +5 -1
- package/dist/server/page/files.js +3 -1
- package/dist/server/page/game.d.ts +5 -0
- package/dist/server/page/game.js +95 -15
- package/dist/server/page/http.d.ts +1 -1
- package/dist/server/page/page.d.ts +14 -9
- package/dist/server/page/page.js +111 -26
- package/dist/server/page/table.d.ts +40 -5
- package/dist/server/page/table.js +65 -25
- package/dist/server/page/update.d.ts +1 -1
- package/dist/server/page/update.js +4 -4
- package/dist/server/page/windows.d.ts +22 -0
- package/dist/server/page/windows.js +45 -0
- package/dist/server/ports/index.d.ts +1 -0
- package/dist/server/ports/index.js +2 -0
- package/dist/server/ports/ports.d.ts +78 -0
- package/dist/server/ports/ports.js +222 -0
- package/dist/server/table/browser.d.ts +22 -6
- package/dist/server/table/browser.js +98 -20
- package/dist/server/table/hosts/chats.d.ts +5 -0
- package/dist/server/table/hosts/chats.js +11 -4
- package/dist/server/table/hosts/hooks.d.ts +6 -0
- package/dist/server/table/hosts/hooks.js +38 -0
- package/dist/server/table/hosts/hosts.d.ts +13 -5
- package/dist/server/table/hosts/hosts.js +16 -10
- package/dist/server/table/hosts/index.d.ts +1 -0
- package/dist/server/table/hosts/index.js +1 -0
- package/dist/server/table/hosts/play.d.ts +7 -3
- package/dist/server/table/hosts/play.js +63 -9
- package/dist/server/table/icon/index.d.ts +2 -0
- package/dist/server/table/icon/index.js +16 -2
- package/dist/server/table/index.d.ts +2 -2
- package/dist/server/table/index.js +1 -1
- package/dist/server/table/launch.d.ts +5 -4
- package/dist/server/table/launch.js +18 -5
- package/dist/server/table/link.js +4 -1
- package/dist/server/table/table.d.ts +7 -0
- package/dist/server/table/table.js +40 -12
- package/dist/sites/index.d.ts +1 -1
- package/dist/sites/index.js +1 -1
- package/dist/sites/{chesscom.d.ts → ratings/chesscom.d.ts} +2 -2
- package/dist/sites/{chesscom.js → ratings/chesscom.js} +1 -1
- package/dist/sites/{choice.d.ts → ratings/choice.d.ts} +1 -1
- package/dist/sites/{choice.js → ratings/choice.js} +1 -1
- package/dist/sites/ratings/index.d.ts +1 -0
- package/dist/sites/ratings/index.js +2 -0
- package/dist/sites/{lichess.d.ts → ratings/lichess.d.ts} +2 -2
- package/dist/sites/{lichess.js → ratings/lichess.js} +1 -1
- package/dist/sites/{sites.d.ts → ratings/ratings.d.ts} +2 -2
- package/dist/sites/{sites.js → ratings/ratings.js} +1 -1
- package/instructions/opponent.md +1 -1
- package/instructions/start.md +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
-
import { gameAt, samePosition } from "../../commands/index.js";
|
|
2
|
+
import { analystOf, gameAt, samePosition } from "../../commands/index.js";
|
|
3
3
|
import { endOf, gameInAnalysis } from "../../games/index.js";
|
|
4
4
|
import { gameFolder } from "../../profiles/index.js";
|
|
5
5
|
import { tellWatchers } from "./events.js";
|
|
6
6
|
import { bodyOf, isJson, jsonOf, runRoute, sendJson } from "./http.js";
|
|
7
|
+
import { giveOpening, releaseOpening } from "./windows.js";
|
|
7
8
|
/** What the table's page shows of its chat: whether one is at the table, with which host, the hosts it can open one in, and one opening. */
|
|
8
9
|
export function tableShown(served) {
|
|
9
10
|
const { table, attached } = served;
|
|
@@ -21,7 +22,8 @@ export function tableShown(served) {
|
|
|
21
22
|
...(held === undefined ? {} : { started: held }),
|
|
22
23
|
hosts: table.chats?.hosts() ?? [],
|
|
23
24
|
...(now === undefined ? {} : { opening: now }),
|
|
24
|
-
...(served.
|
|
25
|
+
...(served.chatWait === undefined ? {} : { waiting: served.chatWait.for }),
|
|
26
|
+
...(served.waitRefused === undefined ? {} : { refused: served.waitRefused }),
|
|
25
27
|
},
|
|
26
28
|
};
|
|
27
29
|
}
|
|
@@ -36,6 +38,9 @@ export function answerTable(request, response, url, served) {
|
|
|
36
38
|
sendJson(response, 200, { quitting: true });
|
|
37
39
|
served.table.quit();
|
|
38
40
|
}
|
|
41
|
+
else if (path === "/api/table/open") {
|
|
42
|
+
runRoute(response, () => openWindow(request, response, served));
|
|
43
|
+
}
|
|
39
44
|
else if (path === "/api/table/chat") {
|
|
40
45
|
runRoute(response, () => openChat(request, response, served));
|
|
41
46
|
}
|
|
@@ -50,7 +55,7 @@ export function answerTable(request, response, url, served) {
|
|
|
50
55
|
}
|
|
51
56
|
else {
|
|
52
57
|
sendJson(response, 404, {
|
|
53
|
-
error: `${path} is no route of the table's: it answers POST /api/table/chat, /api/table/restart, /api/table/waiting, /api/table/update and /api/table/quit.`,
|
|
58
|
+
error: `${path} is no route of the table's: it answers POST /api/table/open, /api/table/chat, /api/table/restart, /api/table/waiting, /api/table/update and /api/table/quit.`,
|
|
54
59
|
});
|
|
55
60
|
}
|
|
56
61
|
return true;
|
|
@@ -72,26 +77,47 @@ export function relaunchChat(served, how = "again", host) {
|
|
|
72
77
|
const folder = served.attached?.chat.folder;
|
|
73
78
|
// Another host may first ask whether it trusts the table's folder: the pages say so, as for a chat opened.
|
|
74
79
|
const switching = how === "restart" && host !== undefined && host !== hostAtTable(served);
|
|
75
|
-
if (folder === undefined || served.table?.chats?.relaunch(folder, host) !== true) {
|
|
80
|
+
if (folder === undefined || heldChat(served) === undefined || served.table?.chats?.relaunch(folder, host) !== true) {
|
|
76
81
|
return false;
|
|
77
82
|
}
|
|
78
83
|
noteOpening(served, switching ? "chat" : how);
|
|
79
84
|
return true;
|
|
80
85
|
}
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Whether a new game that wants a clean chat waits for the chat at the table to end its answer, then starts it again (D-57, D-107): `chesspal play` holds
|
|
88
|
+
* it, it is at work, and no analyst of it is; with an analyst at work the player is asked for /clear, as with a chat opened by hand.
|
|
89
|
+
*/
|
|
90
|
+
export function chatToWaitFor(served) {
|
|
91
|
+
return heldChat(served) !== undefined && !chatAtRest(served) && !analystAtWork(served);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Does what the page asked while the chat was at work, now that it has ended its answer. A chat gone meanwhile takes a start again with it; a new game
|
|
95
|
+
* starts without it, and loses nothing: the next chat at the table is a clean one.
|
|
96
|
+
*/
|
|
97
|
+
export function startWhenAtRest(served) {
|
|
98
|
+
const wanted = served.chatWait;
|
|
99
|
+
const gone = heldChat(served) === undefined;
|
|
100
|
+
if (wanted === undefined || (!gone && !chatAtRest(served))) {
|
|
85
101
|
return;
|
|
86
102
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
else if (chatAtRest(served)) {
|
|
91
|
-
served.restartWanted = undefined;
|
|
92
|
-
relaunchChat(served, "restart", wanted.host);
|
|
103
|
+
served.chatWait = undefined;
|
|
104
|
+
if (!gone || wanted.for === "new-game") {
|
|
105
|
+
startWaited(served, wanted);
|
|
93
106
|
}
|
|
94
107
|
}
|
|
108
|
+
/** A new game that waited for the chat could not start (D-107): the pages say why, each once. */
|
|
109
|
+
export function refuseWaited(served, refused) {
|
|
110
|
+
served.waitRefused = { ...refused, number: (served.waitRefused?.number ?? 0) + 1 };
|
|
111
|
+
tellWatchers(served.watchers);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* chesspal asks to open the table's window, the newest one staying and the others closing; or, with `release`, lets go of the opening it was given when no
|
|
115
|
+
* window that reaches the table opened, so that the next chesspal opens its own.
|
|
116
|
+
*/
|
|
117
|
+
async function openWindow(request, response, served) {
|
|
118
|
+
const release = jsonOf(await bodyOf(request))?.release;
|
|
119
|
+
sendJson(response, 200, release === undefined ? giveOpening(served) : releaseOpening(served, release));
|
|
120
|
+
}
|
|
95
121
|
// A terminal takes a few seconds to open and its chat to attach: a second click meanwhile would open a second chat, which serves a page of its own.
|
|
96
122
|
const openingFor = 30_000;
|
|
97
123
|
/**
|
|
@@ -138,34 +164,38 @@ async function restartChat(request, response, served) {
|
|
|
138
164
|
sendJson(response, 200, { restarting: relaunchChat(served, "restart", offered.name) });
|
|
139
165
|
}
|
|
140
166
|
else {
|
|
141
|
-
served.
|
|
167
|
+
served.chatWait = { for: "restart", host: offered.name };
|
|
142
168
|
tellWatchers(served.watchers);
|
|
143
169
|
sendJson(response, 200, { waiting: true });
|
|
144
170
|
}
|
|
145
171
|
}
|
|
146
172
|
/**
|
|
147
|
-
* Ends a start again the page asked while the chat was at work (D-107): `now` starts it at once, cutting the answer
|
|
148
|
-
* never end; else the request is let go. Asked by the table's own page alone.
|
|
173
|
+
* Ends a start again or a new game the page asked while the chat was at work (D-107): `now` starts it at once, cutting the answer the chat gives, as after
|
|
174
|
+
* an Esc, when it would never end; else the request is let go, and nothing changes. Asked by the table's own page alone.
|
|
149
175
|
*/
|
|
150
176
|
async function endWaiting(request, response, served) {
|
|
151
177
|
const now = jsonOf(await bodyOf(request))?.now;
|
|
152
|
-
const wanted = served.
|
|
178
|
+
const wanted = served.chatWait;
|
|
153
179
|
if (!fromTablePage(request, served)) {
|
|
154
|
-
sendJson(response, 403, { error: "Only the table's own page ends
|
|
180
|
+
sendJson(response, 403, { error: "Only the table's own page ends what waits for the chat." });
|
|
155
181
|
}
|
|
156
182
|
else if (typeof now !== "boolean") {
|
|
157
|
-
sendJson(response, 400, { error: "Say now: true to start the chat again at once, cutting its answer, or now: false to let the request go." });
|
|
183
|
+
sendJson(response, 400, { error: "Say now: true to start the chat again or the new game at once, cutting its answer, or now: false to let the request go." });
|
|
158
184
|
}
|
|
159
185
|
else if (wanted === undefined) {
|
|
160
|
-
sendJson(response, 409, { error: "
|
|
186
|
+
sendJson(response, 409, { error: "Nothing waits for the chat: ask for a start again from the chat's window, or for a new game." });
|
|
161
187
|
}
|
|
162
188
|
else {
|
|
163
|
-
served.
|
|
164
|
-
const restarting = now &&
|
|
189
|
+
served.chatWait = undefined;
|
|
190
|
+
const restarting = now && startWaited(served, wanted);
|
|
165
191
|
tellWatchers(served.watchers);
|
|
166
192
|
sendJson(response, 200, now ? { restarting } : { waiting: false });
|
|
167
193
|
}
|
|
168
194
|
}
|
|
195
|
+
// What waited for the chat to end its answer, done now: the chat starts again with the host asked, or the new game starts with it starting again clean.
|
|
196
|
+
function startWaited(served, wanted) {
|
|
197
|
+
return wanted.for === "new-game" ? wanted.start() : relaunchChat(served, "restart", wanted.host);
|
|
198
|
+
}
|
|
169
199
|
/**
|
|
170
200
|
* Once npm has installed the newer Chesspal (D-108), and no game goes on and the chat is at rest, as a game or a chat may have started while npm worked:
|
|
171
201
|
* the chat `chesspal play` holds starts again from it, clean, and once it has left the table, the table closes to start again from it too.
|
|
@@ -252,14 +282,24 @@ function hostAtTable(served) {
|
|
|
252
282
|
return served.attached === undefined ? undefined : served.table?.chats?.hosts().find((one) => said.includes(one.name))?.name;
|
|
253
283
|
}
|
|
254
284
|
// The chat at the table when `chesspal play` holds it, which the table can start again: the model and effort it started with.
|
|
285
|
+
// The chat at the table when it is the one `chesspal play` holds: in its folder and started by it, not another opened there by hand.
|
|
255
286
|
function heldChat(served) {
|
|
256
287
|
const { attached } = served;
|
|
257
|
-
|
|
288
|
+
const held = attached === undefined ? undefined : served.table?.chats?.held(attached.chat.folder);
|
|
289
|
+
return held === undefined || held.seat !== attached?.chat.seat ? undefined : { model: held.model, effort: held.effort };
|
|
258
290
|
}
|
|
259
291
|
// The chat waits for the doorbell, out of a turn, and no analyst of it is at work: starting it again loses nothing.
|
|
260
292
|
function chatAtRest(served) {
|
|
261
293
|
const { bell } = served;
|
|
262
|
-
return bell.listening && !bell.inTurn &&
|
|
294
|
+
return bell.listening && !bell.inTurn && !analystAtWork(served);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Whether the chat's analyst is at work, on `game` when named: one whose chat started again since it took its game, or that has written nothing for half an
|
|
298
|
+
* hour, is gone (D-109). The chat's starts are in its own folder: on the table, the attached chat's.
|
|
299
|
+
*/
|
|
300
|
+
export function analystAtWork(served, game = gameInAnalysis(gameFolder(served.context.home()))?.analyst?.game) {
|
|
301
|
+
const chat = { ...served.context, folder: served.attached?.chat.folder ?? served.folder };
|
|
302
|
+
return game !== undefined && analystOf(chat, game) !== undefined;
|
|
263
303
|
}
|
|
264
304
|
// The pages show a chat opening from now until it attaches; one that has not by then lets them open another. The opening noted.
|
|
265
305
|
function noteOpening(served, how) {
|
|
@@ -8,7 +8,7 @@ export interface UpdateShown {
|
|
|
8
8
|
readonly installing?: true;
|
|
9
9
|
readonly installed?: true;
|
|
10
10
|
}
|
|
11
|
-
/** Asks npm for Chesspal's latest version now and
|
|
11
|
+
/** Asks npm for Chesspal's latest version now and every hour after, the pages told when it is newer than this one; how to stop. None in the tests. */
|
|
12
12
|
export declare function watchForUpdates(served: Served, every?: number): () => void;
|
|
13
13
|
/** The newer Chesspal the page shows, when npm has one. */
|
|
14
14
|
export declare function updateShown(served: Served): {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// A newer Chesspal on npm (D-108): the page's server asks as it starts and
|
|
1
|
+
// A newer Chesspal on npm (D-108): the page's server asks as it starts and every hour after; the table's page offers to install it, a chat's own page says
|
|
2
2
|
// the command to type.
|
|
3
3
|
import { answerTime, askSite } from "../../sites/index.js";
|
|
4
4
|
import { isOlder, version } from "../../version.js";
|
|
5
5
|
import { tellWatchers } from "./events.js";
|
|
6
|
-
/** Asks npm for Chesspal's latest version now and
|
|
7
|
-
export function watchForUpdates(served, every =
|
|
6
|
+
/** Asks npm for Chesspal's latest version now and every hour after, the pages told when it is newer than this one; how to stop. None in the tests. */
|
|
7
|
+
export function watchForUpdates(served, every = anHour) {
|
|
8
8
|
const fetch = served.context.fetch;
|
|
9
9
|
if (fetch === undefined) {
|
|
10
10
|
return () => undefined;
|
|
@@ -37,4 +37,4 @@ export async function latestVersion(fetch) {
|
|
|
37
37
|
return answer?.status === 200 && typeof said === "string" && /^\d+\.\d+\.\d+$/.test(said) ? said : undefined;
|
|
38
38
|
}
|
|
39
39
|
const latestUrl = "https://registry.npmjs.org/chesspal/latest";
|
|
40
|
-
const
|
|
40
|
+
const anHour = 60 * 60 * 1000;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Served } from "./page.ts";
|
|
2
|
+
/** What the table keeps of its windows: the opening last given, the one that has not connected yet, and the newest window connected. */
|
|
3
|
+
export interface TableWindows {
|
|
4
|
+
given: number;
|
|
5
|
+
pending?: {
|
|
6
|
+
readonly opening: number;
|
|
7
|
+
readonly at: number;
|
|
8
|
+
} | undefined;
|
|
9
|
+
newest: number;
|
|
10
|
+
}
|
|
11
|
+
/** How long, in milliseconds, an opening given waits for its window to connect before another may open one. */
|
|
12
|
+
export declare const openingWait = 10000;
|
|
13
|
+
/** Answers `chesspal` asking to open a window: its opening's number, or none while another opening has not connected yet. */
|
|
14
|
+
export declare function giveOpening(served: Served): {
|
|
15
|
+
opening: number | null;
|
|
16
|
+
};
|
|
17
|
+
/** Lets go of an opening whose window will never reach the table, as no browser started or only a tab did: whether it was the one pending. */
|
|
18
|
+
export declare function releaseOpening(served: Served, opening: unknown): {
|
|
19
|
+
released: boolean;
|
|
20
|
+
};
|
|
21
|
+
/** A page connects to the events with the opening of its window, when the table opened it: every page hears which window is the newest. */
|
|
22
|
+
export declare function windowConnected(served: Served, url: URL): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** How long, in milliseconds, an opening given waits for its window to connect before another may open one. */
|
|
2
|
+
export const openingWait = 10_000;
|
|
3
|
+
/** Answers `chesspal` asking to open a window: its opening's number, or none while another opening has not connected yet. */
|
|
4
|
+
export function giveOpening(served) {
|
|
5
|
+
const windows = windowsOf(served);
|
|
6
|
+
const now = served.context.now().getTime();
|
|
7
|
+
if (windows.pending !== undefined && now - windows.pending.at < openingWait) {
|
|
8
|
+
return { opening: null };
|
|
9
|
+
}
|
|
10
|
+
windows.given = Math.max(now, windows.given + 1);
|
|
11
|
+
windows.pending = { opening: windows.given, at: now };
|
|
12
|
+
return { opening: windows.given };
|
|
13
|
+
}
|
|
14
|
+
/** Lets go of an opening whose window will never reach the table, as no browser started or only a tab did: whether it was the one pending. */
|
|
15
|
+
export function releaseOpening(served, opening) {
|
|
16
|
+
const windows = windowsOf(served);
|
|
17
|
+
const released = windows.pending !== undefined && windows.pending.opening === opening;
|
|
18
|
+
if (released) {
|
|
19
|
+
windows.pending = undefined;
|
|
20
|
+
}
|
|
21
|
+
return { released };
|
|
22
|
+
}
|
|
23
|
+
/** A page connects to the events with the opening of its window, when the table opened it: every page hears which window is the newest. */
|
|
24
|
+
export function windowConnected(served, url) {
|
|
25
|
+
const opening = Number(url.searchParams.get("window"));
|
|
26
|
+
if (served.table === undefined || !Number.isSafeInteger(opening) || opening <= 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const windows = windowsOf(served);
|
|
30
|
+
windows.newest = Math.max(windows.newest, opening);
|
|
31
|
+
windows.given = Math.max(windows.given, opening);
|
|
32
|
+
if (windows.pending !== undefined && windows.pending.opening <= opening) {
|
|
33
|
+
windows.pending = undefined;
|
|
34
|
+
}
|
|
35
|
+
tellNewest(served.watchers, windows.newest);
|
|
36
|
+
}
|
|
37
|
+
function windowsOf(served) {
|
|
38
|
+
served.windows ??= { given: 0, newest: 0 };
|
|
39
|
+
return served.windows;
|
|
40
|
+
}
|
|
41
|
+
function tellNewest(watchers, newest) {
|
|
42
|
+
for (const watcher of watchers) {
|
|
43
|
+
watcher.write(`data: window ${newest}\n\n`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { askPage, type BellProbe, bellProbe, chatFolder, type ChatHook, findChatPage, notedFolder, notedPagePorts, pageAt, pageNote, pagePorts, probeOf, probeWait, serverHome, type TableChat, tellChatAtWork } from "./ports.ts";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Where a folder's page answers, and how a hook finds its chat's page and tells it the chat is at work: light, for the prompt hook of every message.
|
|
2
|
+
export { askPage, bellProbe, chatFolder, findChatPage, notedFolder, notedPagePorts, pageAt, pageNote, pagePorts, probeOf, probeWait, serverHome, tellChatAtWork } from "./ports.js";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whose page answers at a port, for the hook to find its own: the chat's session, the player's folder, whether a browser has the page open, Chesspal's
|
|
3
|
+
* version, and whether the table serves it (D-104), with the session and the folder of the chat attached to it, never its own process's.
|
|
4
|
+
*/
|
|
5
|
+
export declare function bellProbe(folder: string, open?: boolean, table?: TableChat): BellProbe;
|
|
6
|
+
/** The chat attached to the table, as its probe says it: its session, null for none or for Codex, and its folder; neither while no chat is attached. */
|
|
7
|
+
export interface TableChat {
|
|
8
|
+
session: string | null;
|
|
9
|
+
chat?: string;
|
|
10
|
+
}
|
|
11
|
+
/** What a page says of itself at `/api/bell?probe`. */
|
|
12
|
+
export interface BellProbe {
|
|
13
|
+
session: string | null;
|
|
14
|
+
folder: string;
|
|
15
|
+
open: boolean;
|
|
16
|
+
version?: string;
|
|
17
|
+
table?: true;
|
|
18
|
+
/** The folder of the chat attached to the table. */
|
|
19
|
+
chat?: string;
|
|
20
|
+
}
|
|
21
|
+
/** What the page at a port of this computer says of itself; undefined when none answers there in `wait`. */
|
|
22
|
+
export declare function pageAt(port: number, wait?: number): Promise<BellProbe | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* The ports a folder's page tries first: one in each of a few ranges, by its real path, since Windows may reserve a range and the hook may name the folder
|
|
25
|
+
* through a link. The hook looks for it there.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pagePorts(folder: string): number[];
|
|
28
|
+
/** The ports noted for a folder's page in the data folder: none when it has no notes or cannot be read. A note may be stale: the probe tells. */
|
|
29
|
+
export declare function notedPagePorts(home: string, folder: string): number[];
|
|
30
|
+
/** The file where a page off its folder's usual ports notes its port in the data folder: `pages/<port>.json`, with the page's folder. */
|
|
31
|
+
export declare function pageNote(home: string, port: number): string;
|
|
32
|
+
/** The folder whose page noted a port; none when no note reads well. */
|
|
33
|
+
export declare function notedFolder(home: string, port: number): string | undefined;
|
|
34
|
+
/** What a hook reads of its host's input to find its chat's page: the chat's session and the folder it runs in. */
|
|
35
|
+
export interface ChatHook {
|
|
36
|
+
session?: string;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
}
|
|
39
|
+
/** The page of a hook's chat, in the chat's folder: its port, or why none was found. */
|
|
40
|
+
export type ChatPage = {
|
|
41
|
+
folder: string;
|
|
42
|
+
port: number;
|
|
43
|
+
} | {
|
|
44
|
+
folder: string;
|
|
45
|
+
port?: undefined;
|
|
46
|
+
why: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Finds the page of the hook's chat: the one of its session among those of its folder, or the table the chat is attached to (D-104); else the only one,
|
|
50
|
+
* or the only one a browser has open. Its folder's usual ports first: a page of the chat's session there spares reading the data folder, some 40 ms on
|
|
51
|
+
* Windows. With `ports` given, only those are tried. Each page has `wait` to say whose it is.
|
|
52
|
+
*/
|
|
53
|
+
export declare function findChatPage(hook: ChatHook, ports?: number[], wait?: number): Promise<ChatPage>;
|
|
54
|
+
/**
|
|
55
|
+
* Tells the page of the hook's chat that the player's message started its turn (the prompt hook): whether the page heard it. Quick or not at all: each
|
|
56
|
+
* page has a quarter of a second to answer, and none is asked twice, so that the prompt never waits on a page that is busy or gone.
|
|
57
|
+
*/
|
|
58
|
+
export declare function tellChatAtWork(hook: ChatHook, ports?: number[]): Promise<boolean>;
|
|
59
|
+
/** What the page at a port of this computer answers, once it ends: the body of a 200, none for any other answer or none in `wait`; `refused` when nobody listens there at all. */
|
|
60
|
+
export interface PageAnswer {
|
|
61
|
+
refused: boolean;
|
|
62
|
+
text?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Asks the page at a port of this computer, on a connection of its own: one that waits for the bell waits for hours, with no `wait`. */
|
|
65
|
+
export declare function askPage(port: number, path: string, wait?: number): Promise<PageAnswer>;
|
|
66
|
+
/** The page's words for what it says of itself; undefined for anything else. */
|
|
67
|
+
export declare function probeOf(said: string | undefined): BellProbe | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* The data folder the chat's server is given in its folder's settings, as CHESSPAL_HOME in a playtest: the hook runs in the host's environment, not the
|
|
70
|
+
* server's (Federico). Claude Code's .mcp.json first, then Codex's config.
|
|
71
|
+
*/
|
|
72
|
+
export declare function serverHome(folder: string): string | undefined;
|
|
73
|
+
/** The chat's folder, where its server started, as a hook finds it: Claude Code names it, and the hook's cwd follows the cd's of the chat. */
|
|
74
|
+
export declare function chatFolder(hook: {
|
|
75
|
+
cwd?: string;
|
|
76
|
+
}): string;
|
|
77
|
+
/** How long a port may take to say whose page it is: one of this computer answers at once. */
|
|
78
|
+
export declare const probeWait = 1000;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Where a folder's page answers, and how a hook finds its chat's page: on its folder's usual ports, at a port it noted in the data folder, or at the table
|
|
2
|
+
// (D-104). Light on purpose: the prompt hook loads it on every message of the chat.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import { get } from "node:http";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { dataFolder, realPath, samePath, tableFolder } from "../../home/index.js";
|
|
8
|
+
import { version } from "../../version.js";
|
|
9
|
+
/**
|
|
10
|
+
* Whose page answers at a port, for the hook to find its own: the chat's session, the player's folder, whether a browser has the page open, Chesspal's
|
|
11
|
+
* version, and whether the table serves it (D-104), with the session and the folder of the chat attached to it, never its own process's.
|
|
12
|
+
*/
|
|
13
|
+
export function bellProbe(folder, open = false, table) {
|
|
14
|
+
const session = table === undefined ? process.env.CLAUDE_CODE_SESSION_ID ?? null : table.session;
|
|
15
|
+
return { session, folder: resolve(folder), open, version, ...(table === undefined ? {} : { table: true, ...(table.chat === undefined ? {} : { chat: table.chat }) }) };
|
|
16
|
+
}
|
|
17
|
+
/** What the page at a port of this computer says of itself; undefined when none answers there in `wait`. */
|
|
18
|
+
export async function pageAt(port, wait = probeWait) {
|
|
19
|
+
return probeOf((await askPage(port, "/api/bell?probe", wait)).text);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The ports a folder's page tries first: one in each of a few ranges, by its real path, since Windows may reserve a range and the hook may name the folder
|
|
23
|
+
* through a link. The hook looks for it there.
|
|
24
|
+
*/
|
|
25
|
+
export function pagePorts(folder) {
|
|
26
|
+
const offset = createHash("sha256").update(realPath(folder).toLowerCase()).digest()[0] % 10;
|
|
27
|
+
return [5870, 6870, 7870].map((base) => base + portShift() + offset);
|
|
28
|
+
}
|
|
29
|
+
/** The ports noted for a folder's page in the data folder: none when it has no notes or cannot be read. A note may be stale: the probe tells. */
|
|
30
|
+
export function notedPagePorts(home, folder) {
|
|
31
|
+
let names;
|
|
32
|
+
try {
|
|
33
|
+
names = readdirSync(join(home, notesFolder));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return names.flatMap((name) => {
|
|
39
|
+
const port = Number(/^(\d+)\.json$/.exec(name)?.[1]);
|
|
40
|
+
const noted = Number.isInteger(port) ? notedFolder(home, port) : undefined;
|
|
41
|
+
return noted !== undefined && samePath(noted, folder) ? [port] : [];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** The file where a page off its folder's usual ports notes its port in the data folder: `pages/<port>.json`, with the page's folder. */
|
|
45
|
+
export function pageNote(home, port) {
|
|
46
|
+
return join(home, notesFolder, `${port}.json`);
|
|
47
|
+
}
|
|
48
|
+
/** The folder whose page noted a port; none when no note reads well. */
|
|
49
|
+
export function notedFolder(home, port) {
|
|
50
|
+
try {
|
|
51
|
+
const { folder } = JSON.parse(readFileSync(pageNote(home, port), "utf8"));
|
|
52
|
+
return typeof folder === "string" ? folder : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Finds the page of the hook's chat: the one of its session among those of its folder, or the table the chat is attached to (D-104); else the only one,
|
|
60
|
+
* or the only one a browser has open. Its folder's usual ports first: a page of the chat's session there spares reading the data folder, some 40 ms on
|
|
61
|
+
* Windows. With `ports` given, only those are tried. Each page has `wait` to say whose it is.
|
|
62
|
+
*/
|
|
63
|
+
export async function findChatPage(hook, ports, wait = probeWait) {
|
|
64
|
+
const folder = chatFolder(hook);
|
|
65
|
+
const probed = (tried) => Promise.all(tried.map(async (port) => ({ port, said: await pageAt(port, wait) })));
|
|
66
|
+
const usual = await probed(ports ?? pagePorts(folder));
|
|
67
|
+
const own = usual.find(({ said }) => hook.session !== undefined && said?.session === hook.session && samePath(said.folder, folder));
|
|
68
|
+
if (own !== undefined) {
|
|
69
|
+
return { folder, port: own.port };
|
|
70
|
+
}
|
|
71
|
+
// The data folder holds the table's ports and those a page of the folder noted, off its usual ones.
|
|
72
|
+
const home = ports === undefined ? homeOf(folder) : undefined;
|
|
73
|
+
const noted = home === undefined ? [] : notedPagePorts(home, folder).filter((port) => !usual.some((one) => one.port === port));
|
|
74
|
+
const [more, table] = await Promise.all([probed(noted), home === undefined ? undefined : tableOf(home, folder, hook.session, wait)]);
|
|
75
|
+
const probes = [...usual, ...more];
|
|
76
|
+
const pages = probes.flatMap(({ port, said }) => (said !== undefined && samePath(said.folder, folder) ? [{ port, said }] : []));
|
|
77
|
+
// A chat opened in the table's own folder finds the table among the pages of its folder already.
|
|
78
|
+
const ours = table === undefined || pages.some(({ port }) => port === table.port) ? pages : [...pages, table];
|
|
79
|
+
const port = chosenPage(ours, hook.session)?.port;
|
|
80
|
+
if (port !== undefined) {
|
|
81
|
+
return { folder, port };
|
|
82
|
+
}
|
|
83
|
+
const others = probes.flatMap((one) => (one.said === undefined ? [] : [`${one.port} is ${one.said.folder}'s`]));
|
|
84
|
+
const open = ours.filter(({ said }) => said.open).length;
|
|
85
|
+
const found = ours.length > 1 ? `${ours.length} pages of it, none of this chat's session, ${open} open in a browser` : "no page of it";
|
|
86
|
+
return { folder, why: `${found} at ports ${probes.map((one) => one.port).join(", ")}${others.length > 0 ? `; ${others.join(", ")}` : ""}` };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Tells the page of the hook's chat that the player's message started its turn (the prompt hook): whether the page heard it. Quick or not at all: each
|
|
90
|
+
* page has a quarter of a second to answer, and none is asked twice, so that the prompt never waits on a page that is busy or gone.
|
|
91
|
+
*/
|
|
92
|
+
export async function tellChatAtWork(hook, ports) {
|
|
93
|
+
try {
|
|
94
|
+
const found = await findChatPage(hook, ports, promptWait);
|
|
95
|
+
return found.port !== undefined && (await askPage(found.port, "/api/bell/prompt", promptWait)).text === "at work";
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Asks the page at a port of this computer, on a connection of its own: one that waits for the bell waits for hours, with no `wait`. */
|
|
102
|
+
export function askPage(port, path, wait) {
|
|
103
|
+
return new Promise((done) => {
|
|
104
|
+
// Node's shared connection ends a request idle for five seconds.
|
|
105
|
+
const request = get({ host: "127.0.0.1", port, path, agent: false }, (response) => {
|
|
106
|
+
let text = "";
|
|
107
|
+
response.setEncoding("utf8");
|
|
108
|
+
response.on("data", (chunk) => {
|
|
109
|
+
text += chunk;
|
|
110
|
+
});
|
|
111
|
+
response.on("end", () => done(response.statusCode === 200 ? { refused: false, text } : { refused: false }));
|
|
112
|
+
response.on("error", () => done({ refused: false }));
|
|
113
|
+
});
|
|
114
|
+
if (wait !== undefined) {
|
|
115
|
+
giveUpAfterTurns(request, wait);
|
|
116
|
+
}
|
|
117
|
+
request.on("error", (error) => done({ refused: error.code === "ECONNREFUSED" }));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** The page's words for what it says of itself; undefined for anything else. */
|
|
121
|
+
export function probeOf(said) {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = said === undefined ? undefined : JSON.parse(said);
|
|
124
|
+
return parsed === undefined || typeof parsed.folder !== "string" ? undefined : {
|
|
125
|
+
session: typeof parsed.session === "string" ? parsed.session : null,
|
|
126
|
+
folder: parsed.folder,
|
|
127
|
+
open: parsed.open === true,
|
|
128
|
+
...(typeof parsed.version === "string" ? { version: parsed.version } : {}),
|
|
129
|
+
...(parsed.table === true ? { table: true } : {}),
|
|
130
|
+
...(typeof parsed.chat === "string" ? { chat: parsed.chat } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The data folder the chat's server is given in its folder's settings, as CHESSPAL_HOME in a playtest: the hook runs in the host's environment, not the
|
|
139
|
+
* server's (Federico). Claude Code's .mcp.json first, then Codex's config.
|
|
140
|
+
*/
|
|
141
|
+
export function serverHome(folder) {
|
|
142
|
+
const chosen = [claudeHome(folder), codexHome(folder)].find((one) => one !== undefined && one.trim() !== "");
|
|
143
|
+
return chosen === undefined ? undefined : resolve(chosen.trim());
|
|
144
|
+
}
|
|
145
|
+
/** The chat's folder, where its server started, as a hook finds it: Claude Code names it, and the hook's cwd follows the cd's of the chat. */
|
|
146
|
+
export function chatFolder(hook) {
|
|
147
|
+
return resolve(process.env.CLAUDE_PROJECT_DIR ?? hook.cwd ?? process.cwd());
|
|
148
|
+
}
|
|
149
|
+
/** How long a port may take to say whose page it is: one of this computer answers at once. */
|
|
150
|
+
export const probeWait = 1000;
|
|
151
|
+
// How long a page has to answer the prompt hook, which holds the player's message meanwhile.
|
|
152
|
+
const promptWait = 250;
|
|
153
|
+
// Where the pages off their folder's usual ports note them, in the data folder: `<port>.json`, with the page's folder.
|
|
154
|
+
const notesFolder = "pages";
|
|
155
|
+
/**
|
|
156
|
+
* CHESSPAL_PORT_SHIFT, for the tests: a multiple of ten under a thousand moves every range of ports by it, so that runs side by side never share one.
|
|
157
|
+
* Any other value moves nothing.
|
|
158
|
+
*/
|
|
159
|
+
function portShift() {
|
|
160
|
+
const shift = Number(process.env.CHESSPAL_PORT_SHIFT ?? 0);
|
|
161
|
+
return Number.isInteger(shift) && shift >= 0 && shift < 1000 && shift % 10 === 0 ? shift : 0;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The data folder of the chat's server, as the hook finds it; none when it cannot be found. Finding it costs some tens of milliseconds on Windows, the
|
|
165
|
+
* registry read.
|
|
166
|
+
*/
|
|
167
|
+
function homeOf(folder) {
|
|
168
|
+
try {
|
|
169
|
+
return serverHome(folder) ?? dataFolder();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** The table of the data folder, when the chat attached to it is the hook's: of its folder, or of its session; none when no table has this chat. */
|
|
176
|
+
async function tableOf(home, folder, session, wait) {
|
|
177
|
+
const probes = await Promise.all(pagePorts(tableFolder(home)).map(async (port) => ({ port, said: await pageAt(port, wait) })));
|
|
178
|
+
return probes.find((one) => one.said?.table === true && ((one.said.chat !== undefined && samePath(one.said.chat, folder)) || (session !== undefined && one.said.session === session)));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The chat's page among those of its folder: the one of its session; else the only one, or the only one a browser has open, where the player plays. Codex
|
|
182
|
+
* gives the page no session, and a chat closed may leave its page behind for a while.
|
|
183
|
+
*/
|
|
184
|
+
function chosenPage(pages, session) {
|
|
185
|
+
const open = pages.filter(({ said }) => said.open);
|
|
186
|
+
return pages.find(({ said }) => session !== undefined && said.session === session) ?? (pages.length === 1 ? pages[0] : open.length === 1 ? open[0] : undefined);
|
|
187
|
+
}
|
|
188
|
+
function claudeHome(folder) {
|
|
189
|
+
try {
|
|
190
|
+
const config = JSON.parse(readFileSync(join(folder, ".mcp.json"), "utf8"));
|
|
191
|
+
const home = config.mcpServers?.chesspal?.env?.CHESSPAL_HOME;
|
|
192
|
+
return typeof home === "string" ? home : undefined;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// The setting as init keeps it in Codex's table: a basic string, whose escapes are JSON's for a path, or a literal one.
|
|
199
|
+
function codexHome(folder) {
|
|
200
|
+
try {
|
|
201
|
+
const config = readFileSync(join(folder, ".codex", "config.toml"), "utf8");
|
|
202
|
+
const [, basic, literal] = /\bCHESSPAL_HOME\s*=\s*(?:("(?:[^"\\]|\\.)*")|'([^']*)')/.exec(config) ?? [];
|
|
203
|
+
return basic === undefined ? literal : JSON.parse(basic);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Ends a request still unanswered after `wait`, counted in ten turns of this process's clock: after a stall (a busy computer, one waking up), a timer
|
|
211
|
+
* fires before the answer waiting is read, and a page of this process, or one that answered meanwhile, would be taken for one that does not answer.
|
|
212
|
+
*/
|
|
213
|
+
function giveUpAfterTurns(request, wait) {
|
|
214
|
+
let turns = 0;
|
|
215
|
+
const timer = setInterval(() => {
|
|
216
|
+
turns++;
|
|
217
|
+
if (turns === 10) {
|
|
218
|
+
request.destroy();
|
|
219
|
+
}
|
|
220
|
+
}, wait / 10);
|
|
221
|
+
request.on("close", () => clearInterval(timer));
|
|
222
|
+
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/** What the search for a browser asks the system: whether a file is there, and what a command prints, nothing when it fails. */
|
|
2
|
+
export interface BrowserSystem {
|
|
3
|
+
exists: (path: string) => boolean;
|
|
4
|
+
output: (command: string, args: string[]) => Promise<string>;
|
|
5
|
+
}
|
|
6
|
+
/** How the table's page opened: in a window of its own, marked with its opening; in a tab of the default browser, which is not; or not at all. */
|
|
7
|
+
export type Opened = "window" | "tab" | "none";
|
|
8
|
+
/**
|
|
9
|
+
* Opens an address in a window of its own, marked with the table's opening, else in a tab of the default browser, without waiting for it: how it opened.
|
|
10
|
+
* A tab is not marked: a page cannot close a tab it did not open.
|
|
11
|
+
*/
|
|
12
|
+
export declare function openInBrowser(url: string, platform: string, opening?: number): Promise<Opened>;
|
|
13
|
+
/**
|
|
14
|
+
* The address of the window the table opens, marked so that the page sizes it as it opens, with its opening, so that it closes once a newer window has
|
|
15
|
+
* opened (page/layout/window.ts).
|
|
16
|
+
*/
|
|
17
|
+
export declare function openedAddress(url: string, opening?: number): string;
|
|
18
|
+
/**
|
|
19
|
+
* The browser that opens the table's window: the default one when it is a Chromium that takes --app and is where it installs itself, so that the window
|
|
20
|
+
* opens in the browser the player uses; else Chrome or Edge, the first there is; none when neither is.
|
|
21
|
+
*/
|
|
22
|
+
export declare function appBrowser(platform: string, env: NodeJS.ProcessEnv, system?: BrowserSystem): Promise<string | undefined>;
|