chesspal 0.2.4 → 0.3.1
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 +11 -12
- package/dist/cli/cli.js +83 -3
- package/dist/cli/command-line.js +26 -9
- package/dist/cli/icon.d.ts +25 -0
- package/dist/cli/icon.js +80 -0
- package/dist/commands/agents.d.ts +19 -2
- package/dist/commands/agents.js +39 -13
- package/dist/commands/ai-say.js +3 -4
- package/dist/commands/analyse-game.d.ts +4 -0
- package/dist/commands/analyse-game.js +46 -13
- package/dist/commands/annotate-game.d.ts +2 -0
- package/dist/commands/annotate-game.js +40 -18
- package/dist/commands/answer-lines.d.ts +2 -0
- package/dist/commands/answer-lines.js +13 -3
- package/dist/commands/coach.js +4 -2
- package/dist/commands/context.d.ts +11 -1
- package/dist/commands/context.js +5 -0
- package/dist/commands/delete-review.js +9 -5
- package/dist/commands/game-summary.d.ts +2 -0
- package/dist/commands/game-summary.js +4 -0
- package/dist/commands/index.d.ts +3 -2
- package/dist/commands/index.js +7 -4
- package/dist/commands/init.js +2 -4
- package/dist/commands/instructions.d.ts +9 -0
- package/dist/commands/instructions.js +30 -0
- package/dist/commands/lead-input.d.ts +1 -1
- package/dist/commands/lead-input.js +7 -4
- package/dist/commands/page-link.js +6 -5
- package/dist/commands/past-games.d.ts +1 -1
- package/dist/commands/past-games.js +12 -5
- package/dist/commands/player-move.js +2 -3
- package/dist/commands/profiles.d.ts +23 -9
- package/dist/commands/profiles.js +76 -35
- package/dist/commands/ratings.d.ts +8 -0
- package/dist/commands/ratings.js +45 -0
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/registry.js +3 -2
- package/dist/commands/say.d.ts +1 -1
- package/dist/commands/say.js +1 -1
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.js +46 -27
- package/dist/commands/study.js +8 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/review.d.ts +2 -0
- package/dist/core/review.js +5 -1
- package/dist/games/ai-model.d.ts +4 -2
- package/dist/games/ai-model.js +10 -1
- package/dist/games/analysis.d.ts +26 -3
- package/dist/games/analysis.js +41 -6
- package/dist/games/archive.d.ts +6 -3
- package/dist/games/archive.js +13 -5
- package/dist/games/current-game.d.ts +10 -1
- package/dist/games/current-game.js +49 -15
- package/dist/games/files.d.ts +5 -0
- package/dist/games/files.js +42 -1
- package/dist/games/index.d.ts +3 -3
- package/dist/games/index.js +3 -3
- package/dist/home/data-folder.d.ts +2 -0
- package/dist/home/data-folder.js +5 -1
- package/dist/home/index.d.ts +1 -1
- package/dist/home/index.js +1 -1
- package/dist/host-files/agents.d.ts +2 -2
- package/dist/host-files/agents.js +12 -6
- package/dist/host-files/host-files.d.ts +5 -3
- package/dist/host-files/host-files.js +15 -8
- package/dist/host-files/index.d.ts +2 -2
- package/dist/host-files/index.js +2 -2
- package/dist/page/app/app.js +49 -20
- package/dist/page/app/title.js +17 -0
- package/dist/page/avatars/torretta.js +4 -4
- package/dist/page/dialogs/analyses.js +4 -4
- package/dist/page/dialogs/dialog.js +4 -1
- package/dist/page/dialogs/dialogs.css +84 -10
- package/dist/page/dialogs/profiles.js +96 -43
- package/dist/page/game/column.js +5 -1
- package/dist/page/game/plies.js +7 -4
- package/dist/page/guide/blocks.js +67 -0
- package/dist/page/guide/en.md +66 -0
- package/dist/page/guide/foot.js +18 -0
- package/dist/page/guide/guide.css +200 -0
- package/dist/page/guide/guide.js +89 -0
- package/dist/page/guide/it.md +66 -0
- package/dist/page/guide/markdown.js +67 -0
- package/dist/page/helps/helps.css +7 -10
- package/dist/page/layout/favicon.svg +13 -0
- package/dist/page/layout/header.js +3 -3
- package/dist/page/layout/icon.icns +0 -0
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/icon.png +0 -0
- package/dist/page/layout/layout.css +85 -4
- package/dist/page/layout/window.js +45 -0
- package/dist/page/main.js +4 -1
- package/dist/page/players/card.js +1 -1
- package/dist/page/profile/profile.js +14 -9
- package/dist/page/profile/settings.js +9 -0
- package/dist/page/style.css +2 -0
- package/dist/page/table/table.css +123 -0
- package/dist/page/table/window.js +130 -0
- package/dist/page/talk/chat.css +23 -5
- package/dist/page/talk/talk.css +38 -26
- package/dist/page/talk/talk.js +6 -4
- package/dist/page/talk/voice.js +35 -9
- package/dist/page/text.js +131 -17
- package/dist/page/theme.css +7 -0
- package/dist/profiles/index.d.ts +2 -1
- package/dist/profiles/index.js +2 -1
- package/dist/profiles/profiles.d.ts +10 -15
- package/dist/profiles/profiles.js +30 -15
- package/dist/profiles/ratings.d.ts +35 -0
- package/dist/profiles/ratings.js +61 -0
- package/dist/server/bell.d.ts +24 -3
- package/dist/server/bell.js +84 -10
- package/dist/server/caller.d.ts +2 -1
- package/dist/server/caller.js +3 -2
- package/dist/server/mcp.d.ts +7 -3
- package/dist/server/mcp.js +100 -52
- package/dist/server/page/analyses.d.ts +2 -0
- package/dist/server/page/analyses.js +20 -5
- package/dist/server/page/attach.d.ts +4 -0
- package/dist/server/page/attach.js +112 -0
- package/dist/server/page/chat.d.ts +65 -0
- package/dist/server/page/chat.js +87 -0
- package/dist/server/page/events.d.ts +7 -0
- package/dist/server/page/events.js +20 -0
- package/dist/server/page/files.js +10 -1
- package/dist/server/page/game.js +43 -23
- package/dist/server/page/page.d.ts +51 -7
- package/dist/server/page/page.js +74 -48
- package/dist/server/page/profiles.d.ts +5 -0
- package/dist/server/page/profiles.js +26 -10
- package/dist/server/page/table.d.ts +58 -0
- package/dist/server/page/table.js +210 -0
- package/dist/server/table/browser.d.ts +6 -0
- package/dist/server/table/browser.js +48 -0
- package/dist/server/table/hosts/chats.d.ts +7 -0
- package/dist/server/table/hosts/chats.js +34 -0
- package/dist/server/table/hosts/find.d.ts +2 -0
- package/dist/server/table/hosts/find.js +20 -0
- package/dist/server/table/hosts/hosts.d.ts +34 -0
- package/dist/server/table/hosts/hosts.js +49 -0
- package/dist/server/table/hosts/index.d.ts +3 -0
- package/dist/server/table/hosts/index.js +4 -0
- package/dist/server/table/hosts/play.d.ts +29 -0
- package/dist/server/table/hosts/play.js +180 -0
- package/dist/server/table/hosts/seat.d.ts +33 -0
- package/dist/server/table/hosts/seat.js +80 -0
- package/dist/server/table/hosts/terminal.d.ts +6 -0
- package/dist/server/table/hosts/terminal.js +63 -0
- package/dist/server/table/icon/index.d.ts +32 -0
- package/dist/server/table/icon/index.js +83 -0
- package/dist/server/table/icon/linux.d.ts +5 -0
- package/dist/server/table/icon/linux.js +47 -0
- package/dist/server/table/icon/macos.d.ts +3 -0
- package/dist/server/table/icon/macos.js +38 -0
- package/dist/server/table/icon/windows.d.ts +3 -0
- package/dist/server/table/icon/windows.js +48 -0
- package/dist/server/table/index.d.ts +6 -0
- package/dist/server/table/index.js +7 -0
- package/dist/server/table/launch.d.ts +19 -0
- package/dist/server/table/launch.js +78 -0
- package/dist/server/table/link.d.ts +10 -0
- package/dist/server/table/link.js +231 -0
- package/dist/server/table/table.d.ts +35 -0
- package/dist/server/table/table.js +94 -0
- package/dist/sites/chesscom.d.ts +4 -0
- package/dist/sites/chesscom.js +25 -0
- package/dist/sites/choice.d.ts +17 -0
- package/dist/sites/choice.js +13 -0
- package/dist/sites/index.d.ts +2 -0
- package/dist/sites/index.js +4 -0
- package/dist/sites/lichess.d.ts +4 -0
- package/dist/sites/lichess.js +32 -0
- package/dist/sites/request.d.ts +11 -0
- package/dist/sites/request.js +15 -0
- package/dist/sites/sites.d.ts +6 -0
- package/dist/sites/sites.js +33 -0
- package/instructions/analysis.md +1 -1
- package/instructions/analyst-agent.md +1 -1
- package/instructions/coach.md +2 -1
- package/instructions/opponent.md +6 -6
- package/instructions/ponder.md +1 -0
- package/instructions/start.md +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Chesspal's icon on the desktop and in the system's menu (D-104): it opens the table as `chesspal` alone does, with no console, on Windows, macOS and Linux.
|
|
2
|
+
// The command line asks once a machine whether to put it there.
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { hostname } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { writeAtomically } from "../../../games/index.js";
|
|
8
|
+
import { tableFolder } from "../../../home/index.js";
|
|
9
|
+
import { linuxIcons } from "./linux.js";
|
|
10
|
+
import { macIcons } from "./macos.js";
|
|
11
|
+
import { windowsIcons } from "./windows.js";
|
|
12
|
+
/**
|
|
13
|
+
* Puts Chesspal's icon on the desktop and in the menu, the system's own folders unless `places` gives others: the files made. It throws when the system
|
|
14
|
+
* refuses, with what went wrong.
|
|
15
|
+
*/
|
|
16
|
+
export function makeIcons(platform, command, places) {
|
|
17
|
+
if (platform === "win32") {
|
|
18
|
+
return windowsIcons(command, iconImage("ico"), places);
|
|
19
|
+
}
|
|
20
|
+
if (platform === "darwin") {
|
|
21
|
+
return Promise.resolve(macIcons(command, iconImage("icns"), places));
|
|
22
|
+
}
|
|
23
|
+
return Promise.resolve(linuxIcons(command, iconImage("png"), places));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What the icon starts: the `chesspal` running now, the same Node and script, as `chesspal open`, in the data folder. Windows gives its hidden console a
|
|
27
|
+
* terminal, where `chesspal` alone could ask about the icon unseen; `open` never asks.
|
|
28
|
+
*/
|
|
29
|
+
export function iconCommand(home) {
|
|
30
|
+
return { node: process.execPath, args: [...process.execArgv, process.argv[1] ?? "", "open"], folder: home };
|
|
31
|
+
}
|
|
32
|
+
/** Whether the command line asked already on this machine whether to put the icon there: once a machine, since the data folder may be on several. */
|
|
33
|
+
export function iconAsked(home, machine = hostname()) {
|
|
34
|
+
return iconNotes(home).asked.includes(machine);
|
|
35
|
+
}
|
|
36
|
+
/** Notes that the command line asked on this machine, whatever the answer. */
|
|
37
|
+
export function noteIconAsked(home, machine = hostname()) {
|
|
38
|
+
const notes = iconNotes(home);
|
|
39
|
+
if (!notes.asked.includes(machine)) {
|
|
40
|
+
writeIconNotes(home, { ...notes, asked: [...notes.asked, machine] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Notes the icon made on this machine, its command and files, which asks no more there. */
|
|
44
|
+
export function noteIconMade(home, command, files, machine = hostname()) {
|
|
45
|
+
const notes = iconNotes(home);
|
|
46
|
+
const asked = notes.asked.includes(machine) ? notes.asked : [...notes.asked, machine];
|
|
47
|
+
writeIconNotes(home, { asked, made: { ...notes.made, [machine]: { command: commandLine(command), files: [...files] } } });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether the icon made on this machine runs another Node or script than `command`, which a Node manager or an update may have taken away, and is still
|
|
51
|
+
* there: then it is made again. One the player removed stays removed.
|
|
52
|
+
*/
|
|
53
|
+
export function iconToRemake(home, command, machine = hostname(), exists = existsSync) {
|
|
54
|
+
const made = iconNotes(home).made[machine];
|
|
55
|
+
return made !== undefined && JSON.stringify(made.command) !== JSON.stringify(commandLine(command)) && made.files.some(exists);
|
|
56
|
+
}
|
|
57
|
+
/** The icon's image, as the build copies it beside the page's files. */
|
|
58
|
+
function iconImage(kind) {
|
|
59
|
+
return fileURLToPath(new URL(`../../../page/layout/icon.${kind}`, import.meta.url));
|
|
60
|
+
}
|
|
61
|
+
function commandLine(command) {
|
|
62
|
+
return [command.node, ...command.args];
|
|
63
|
+
}
|
|
64
|
+
function notesFile(home) {
|
|
65
|
+
return join(tableFolder(home), "icon.json");
|
|
66
|
+
}
|
|
67
|
+
function writeIconNotes(home, notes) {
|
|
68
|
+
mkdirSync(tableFolder(home), { recursive: true });
|
|
69
|
+
writeAtomically(notesFile(home), `${JSON.stringify(notes, null, 2)}\n`);
|
|
70
|
+
}
|
|
71
|
+
// The notes kept; none when the file is missing or cannot be read, which only asks again.
|
|
72
|
+
function iconNotes(home) {
|
|
73
|
+
const file = notesFile(home);
|
|
74
|
+
const texts = (value) => (Array.isArray(value) ? value.filter((text) => typeof text === "string") : []);
|
|
75
|
+
try {
|
|
76
|
+
const read = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
|
|
77
|
+
const made = typeof read.made === "object" && read.made !== null ? read.made : {};
|
|
78
|
+
return { asked: texts(read.asked), made: Object.fromEntries(Object.entries(made).map(([machine, icon]) => [machine, { command: texts(icon?.command), files: texts(icon?.files) }])) };
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return { asked: [], made: {} };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IconCommand, IconPlaces } from "./index.ts";
|
|
2
|
+
/** Makes chesspal.desktop in the user's applications and on the desktop, when there is one, or in `places`: their paths. */
|
|
3
|
+
export declare function linuxIcons(command: IconCommand, image: string, places?: IconPlaces): string[];
|
|
4
|
+
/** The desktop entry: Chesspal's name and icon, and the command it runs with no terminal. */
|
|
5
|
+
export declare function desktopEntry(command: IconCommand, image: string): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The icon on Linux: a desktop entry in the user's applications, which every desktop's menu lists, and a copy on the desktop, marked as trusted where GNOME
|
|
2
|
+
// asks for it.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
/** Makes chesspal.desktop in the user's applications and on the desktop, when there is one, or in `places`: their paths. */
|
|
8
|
+
export function linuxIcons(command, image, places = systemPlaces()) {
|
|
9
|
+
mkdirSync(places.menu, { recursive: true });
|
|
10
|
+
const entry = join(places.menu, "chesspal.desktop");
|
|
11
|
+
writeFileSync(entry, desktopEntry(command, image));
|
|
12
|
+
chmodSync(entry, 0o755);
|
|
13
|
+
if (!existsSync(places.desktop)) {
|
|
14
|
+
return [entry];
|
|
15
|
+
}
|
|
16
|
+
const onDesktop = join(places.desktop, "chesspal.desktop");
|
|
17
|
+
copyFileSync(entry, onDesktop);
|
|
18
|
+
chmodSync(onDesktop, 0o755);
|
|
19
|
+
// GNOME launches a desktop file on the desktop only once it is trusted; elsewhere gio may be missing, and nothing is lost.
|
|
20
|
+
spawnSync("gio", ["set", onDesktop, "metadata::trusted", "true"], { stdio: "ignore" });
|
|
21
|
+
return [onDesktop, entry];
|
|
22
|
+
}
|
|
23
|
+
/** The desktop entry: Chesspal's name and icon, and the command it runs with no terminal. */
|
|
24
|
+
export function desktopEntry(command, image) {
|
|
25
|
+
return `[Desktop Entry]
|
|
26
|
+
Type=Application
|
|
27
|
+
Name=Chesspal
|
|
28
|
+
Comment=Play chess against an AI, and learn with one
|
|
29
|
+
Exec=${[command.node, ...command.args].map(quoted).join(" ")}
|
|
30
|
+
Path=${command.folder}
|
|
31
|
+
Icon=${image}
|
|
32
|
+
Terminal=false
|
|
33
|
+
Categories=Game;BoardGame;
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
// The desktop as the user's dirs name it, and the applications of XDG_DATA_HOME.
|
|
37
|
+
function systemPlaces() {
|
|
38
|
+
const named = spawnSync("xdg-user-dir", ["DESKTOP"], { encoding: "utf8" });
|
|
39
|
+
const desktop = named.status === 0 && named.stdout.trim() !== "" ? named.stdout.trim() : join(homedir(), "Desktop");
|
|
40
|
+
const data = process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share");
|
|
41
|
+
return { desktop, menu: join(data, "applications") };
|
|
42
|
+
}
|
|
43
|
+
// An argument of Exec, in double quotes, with the characters the desktop entry spec escapes there, and % doubled, which Exec reads as a field code; then
|
|
44
|
+
// every backslash doubled, as the spec reads a value's escapes before its quotes (Federico).
|
|
45
|
+
function quoted(argument) {
|
|
46
|
+
return `"${argument.replace(/(["`$\\])/g, "\\$1").replaceAll("%", "%%")}"`.replaceAll("\\", "\\\\");
|
|
47
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { IconCommand, IconPlaces } from "./index.ts";
|
|
2
|
+
/** Makes Chesspal.app in ~/Applications, or in `places`' menu, with its image, and its link on the desktop: their paths. */
|
|
3
|
+
export declare function macIcons(command: IconCommand, image: string, places?: IconPlaces): string[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The icon on macOS: a small application in the user's Applications, which Launchpad and Spotlight list, that runs `chesspal`; on the desktop, a link to it.
|
|
2
|
+
import { chmodSync, copyFileSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
/** Makes Chesspal.app in ~/Applications, or in `places`' menu, with its image, and its link on the desktop: their paths. */
|
|
6
|
+
export function macIcons(command, image, places = { desktop: join(homedir(), "Desktop"), menu: join(homedir(), "Applications") }) {
|
|
7
|
+
const app = join(places.menu, "Chesspal.app");
|
|
8
|
+
const contents = join(app, "Contents");
|
|
9
|
+
const launcher = join(contents, "MacOS", "chesspal");
|
|
10
|
+
mkdirSync(dirname(launcher), { recursive: true });
|
|
11
|
+
mkdirSync(join(contents, "Resources"), { recursive: true });
|
|
12
|
+
copyFileSync(image, join(contents, "Resources", "chesspal.icns"));
|
|
13
|
+
writeFileSync(join(contents, "Info.plist"), infoList);
|
|
14
|
+
writeFileSync(launcher, `#!/bin/sh\ncd ${quoted(command.folder)} && exec ${[command.node, ...command.args].map(quoted).join(" ")}\n`);
|
|
15
|
+
chmodSync(launcher, 0o755);
|
|
16
|
+
const link = join(places.desktop, "Chesspal.app");
|
|
17
|
+
rmSync(link, { force: true });
|
|
18
|
+
symlinkSync(app, link);
|
|
19
|
+
return [link, app];
|
|
20
|
+
}
|
|
21
|
+
// LSUIElement: the launcher shows no icon in the Dock for the moment it runs.
|
|
22
|
+
const infoList = `<?xml version="1.0" encoding="UTF-8"?>
|
|
23
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
24
|
+
<plist version="1.0">
|
|
25
|
+
<dict>
|
|
26
|
+
<key>CFBundleName</key><string>Chesspal</string>
|
|
27
|
+
<key>CFBundleIdentifier</key><string>io.github.stefanocaronia.chesspal</string>
|
|
28
|
+
<key>CFBundleExecutable</key><string>chesspal</string>
|
|
29
|
+
<key>CFBundleIconFile</key><string>chesspal</string>
|
|
30
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
31
|
+
<key>LSUIElement</key><true/>
|
|
32
|
+
</dict>
|
|
33
|
+
</plist>
|
|
34
|
+
`;
|
|
35
|
+
// An argument for the shell, in single quotes, a quote in it closed and escaped.
|
|
36
|
+
function quoted(argument) {
|
|
37
|
+
return `'${argument.replaceAll("'", "'\\''")}'`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { IconCommand, IconPlaces } from "./index.ts";
|
|
2
|
+
/** Makes the shortcuts, on the desktop and in the Start menu as Windows names them for this user, or in `places`: their paths. */
|
|
3
|
+
export declare function windowsIcons(command: IconCommand, image: string, places?: IconPlaces): Promise<string[]>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The icon on Windows: a shortcut on the desktop and in the Start menu, made by PowerShell, the only tool every Windows has for it. It runs Node through
|
|
2
|
+
// `conhost --headless`, the one way found that opens no console window.
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { win32 } from "node:path";
|
|
5
|
+
/** Makes the shortcuts, on the desktop and in the Start menu as Windows names them for this user, or in `places`: their paths. */
|
|
6
|
+
export function windowsIcons(command, image, places) {
|
|
7
|
+
const env = {
|
|
8
|
+
...process.env,
|
|
9
|
+
CHESSPAL_ICON_FOLDERS: places === undefined ? "" : `${places.desktop};${places.menu}`,
|
|
10
|
+
CHESSPAL_ICON_TARGET: win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "conhost.exe"),
|
|
11
|
+
CHESSPAL_ICON_ARGUMENTS: ["--headless", command.node, ...command.args].map(quoted).join(" "),
|
|
12
|
+
CHESSPAL_ICON_FOLDER: command.folder,
|
|
13
|
+
CHESSPAL_ICON_IMAGE: image,
|
|
14
|
+
};
|
|
15
|
+
return new Promise((done, failed) => {
|
|
16
|
+
execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", shortcuts], { env, windowsHide: true }, (error, stdout, stderr) => {
|
|
17
|
+
if (error === null) {
|
|
18
|
+
done(stdout.split(/\r?\n/).filter((line) => line.trim() !== ""));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
failed(new Error(`Windows did not make Chesspal's shortcuts: ${stderr.trim() || error.message}`));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// The values come in the environment, never in the script: a path may hold any character PowerShell reads. Its output in UTF-8, and an error as its
|
|
27
|
+
// message alone, not in the system's language and code page around it.
|
|
28
|
+
const shortcuts = `
|
|
29
|
+
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
30
|
+
trap { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }
|
|
31
|
+
$ErrorActionPreference = 'Stop'
|
|
32
|
+
$shell = New-Object -ComObject WScript.Shell
|
|
33
|
+
$folders = if ($env:CHESSPAL_ICON_FOLDERS) { $env:CHESSPAL_ICON_FOLDERS -split ';' } else { [Environment]::GetFolderPath('Desktop'), [Environment]::GetFolderPath('Programs') }
|
|
34
|
+
foreach ($folder in $folders) {
|
|
35
|
+
$link = $shell.CreateShortcut((Join-Path $folder 'Chesspal.lnk'))
|
|
36
|
+
$link.TargetPath = $env:CHESSPAL_ICON_TARGET
|
|
37
|
+
$link.Arguments = $env:CHESSPAL_ICON_ARGUMENTS
|
|
38
|
+
$link.WorkingDirectory = $env:CHESSPAL_ICON_FOLDER
|
|
39
|
+
$link.IconLocation = $env:CHESSPAL_ICON_IMAGE
|
|
40
|
+
$link.Description = 'Chesspal'
|
|
41
|
+
$link.Save()
|
|
42
|
+
$link.FullName
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
// An argument as Windows reads it from a command line: in quotes when it has a space; a Windows path never holds a quote.
|
|
46
|
+
function quoted(argument) {
|
|
47
|
+
return /\s/.test(argument) ? `"${argument}"` : argument;
|
|
48
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { appBrowser, openedAddress } from "./browser.ts";
|
|
2
|
+
export { hostLaunch, isHostName, playHost, terminalChats } from "./hosts/index.ts";
|
|
3
|
+
export { iconAsked, type IconCommand, iconCommand, type IconPlaces, iconToRemake, makeIcons, noteIconAsked, noteIconMade } from "./icon/index.ts";
|
|
4
|
+
export { noteOpenFailed, openTable, processLauncher, type TableLauncher } from "./launch.ts";
|
|
5
|
+
export { attachTable, tableToJoin } from "./link.ts";
|
|
6
|
+
export { isOlder, startTable, type Table, type TableTimes, tableTimes } from "./table.ts";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The table (D-104): the board page in a process of its own, started by `chesspal` alone, with no chat. Above server/page, which serves the page in either process.
|
|
2
|
+
export { appBrowser, openedAddress } from "./browser.js";
|
|
3
|
+
export { hostLaunch, isHostName, playHost, terminalChats } from "./hosts/index.js";
|
|
4
|
+
export { iconAsked, iconCommand, iconToRemake, makeIcons, noteIconAsked, noteIconMade } from "./icon/index.js";
|
|
5
|
+
export { noteOpenFailed, openTable, processLauncher } from "./launch.js";
|
|
6
|
+
export { attachTable, tableToJoin } from "./link.js";
|
|
7
|
+
export { isOlder, startTable, tableTimes } from "./table.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Context } from "../../commands/index.ts";
|
|
2
|
+
/** What opening the table does outside this process: start the table apart, open the browser, wait. The tests give their own. */
|
|
3
|
+
export interface TableLauncher {
|
|
4
|
+
/** Starts `chesspal table` apart from this process, which it outlives, in the data folder. */
|
|
5
|
+
start(home: string): void;
|
|
6
|
+
/** Opens the page in the browser: whether it could. */
|
|
7
|
+
open(url: string): Promise<boolean>;
|
|
8
|
+
wait(milliseconds: number): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Opens the table's page in the browser, the table started first when none answers, or when the one that does is of another version, which is asked to
|
|
12
|
+
* quit; a table of this version another `chesspal` started meanwhile is as good. What to tell the player, in their language; a table that does not start
|
|
13
|
+
* within ten seconds throws.
|
|
14
|
+
*/
|
|
15
|
+
export declare function openTable(context: Context, launcher?: TableLauncher): Promise<string>;
|
|
16
|
+
/** Notes why the icon could not open the table, whose console nobody sees, in open.log of the table's folder: the last of them. */
|
|
17
|
+
export declare function noteOpenFailed(context: Context, message: string): void;
|
|
18
|
+
/** The launcher of a real `chesspal`: the table started with the same Node and script, the page opened in the default browser. */
|
|
19
|
+
export declare function processLauncher(platform: string, command?: string[]): TableLauncher;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// `chesspal` alone (D-104): the table's page opened in the browser, the table started first, apart from the terminal, when none of this version answers.
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { styleText } from "node:util";
|
|
6
|
+
import { speaksItalian } from "../../commands/index.js";
|
|
7
|
+
import { tableFolder } from "../../home/index.js";
|
|
8
|
+
import { version } from "../../version.js";
|
|
9
|
+
import { openInBrowser } from "./browser.js";
|
|
10
|
+
import { askToQuit, tableAt } from "./table.js";
|
|
11
|
+
/**
|
|
12
|
+
* Opens the table's page in the browser, the table started first when none answers, or when the one that does is of another version, which is asked to
|
|
13
|
+
* quit; a table of this version another `chesspal` started meanwhile is as good. What to tell the player, in their language; a table that does not start
|
|
14
|
+
* within ten seconds throws.
|
|
15
|
+
*/
|
|
16
|
+
export async function openTable(context, launcher = processLauncher(context.platform)) {
|
|
17
|
+
const home = context.home();
|
|
18
|
+
const folder = tableFolder(home);
|
|
19
|
+
const words = speaksItalian(context.language) ? italian : english;
|
|
20
|
+
const deadline = context.now().getTime() + startWait;
|
|
21
|
+
const asked = new Set();
|
|
22
|
+
let started = false;
|
|
23
|
+
for (;;) {
|
|
24
|
+
const table = await tableAt(folder);
|
|
25
|
+
if (table?.version === version) {
|
|
26
|
+
return styleText(["bold", "green"], `♜ ${(await launcher.open(table.url)) ? words.opened(table.url) : words.notOpened(table.url)}`);
|
|
27
|
+
}
|
|
28
|
+
if (table !== undefined && !asked.has(table.port)) {
|
|
29
|
+
asked.add(table.port);
|
|
30
|
+
await askToQuit(table.port);
|
|
31
|
+
}
|
|
32
|
+
else if (table === undefined && !started) {
|
|
33
|
+
started = true;
|
|
34
|
+
launcher.start(home);
|
|
35
|
+
}
|
|
36
|
+
if (context.now().getTime() >= deadline) {
|
|
37
|
+
throw new Error(table === undefined ? words.notStarted : words.notQuit(table.url));
|
|
38
|
+
}
|
|
39
|
+
await launcher.wait(every);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Notes why the icon could not open the table, whose console nobody sees, in open.log of the table's folder: the last of them. */
|
|
43
|
+
export function noteOpenFailed(context, message) {
|
|
44
|
+
try {
|
|
45
|
+
const folder = tableFolder(context.home());
|
|
46
|
+
mkdirSync(folder, { recursive: true });
|
|
47
|
+
writeFileSync(join(folder, "open.log"), `${context.now().toISOString()} ${message}\n`);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// A data folder that cannot be written has no other place for it.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The launcher of a real `chesspal`: the table started with the same Node and script, the page opened in the default browser. */
|
|
54
|
+
export function processLauncher(platform, command = [...process.execArgv, process.argv[1] ?? ""]) {
|
|
55
|
+
return {
|
|
56
|
+
start: (home) => {
|
|
57
|
+
// Its own process group, with no console and nothing of this one's: the terminal can close, the table stays.
|
|
58
|
+
spawn(process.execPath, [...command, "table"], { cwd: home, detached: true, stdio: "ignore", windowsHide: true }).unref();
|
|
59
|
+
},
|
|
60
|
+
open: (url) => openInBrowser(url, platform),
|
|
61
|
+
wait: (milliseconds) => new Promise((done) => setTimeout(done, milliseconds)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// A table answers within ten seconds of its start, an older one quitting first, looked for every tenth of a second.
|
|
65
|
+
const startWait = 10_000;
|
|
66
|
+
const every = 100;
|
|
67
|
+
const english = {
|
|
68
|
+
opened: (url) => `Chesspal's table is open: ${url}`,
|
|
69
|
+
notOpened: (url) => `Chesspal's table is at ${url}: open it in your browser.`,
|
|
70
|
+
notStarted: "Chesspal's table did not start within ten seconds: run `chesspal table` to see why.",
|
|
71
|
+
notQuit: (url) => `The table of another Chesspal version, at ${url}, did not close: close its pages in the browser and wait five minutes, then run chesspal again.`,
|
|
72
|
+
};
|
|
73
|
+
const italian = {
|
|
74
|
+
opened: (url) => `Il tavolo di Chesspal è aperto: ${url}`,
|
|
75
|
+
notOpened: (url) => `Il tavolo di Chesspal è su ${url}: aprilo nel browser.`,
|
|
76
|
+
notStarted: "Il tavolo di Chesspal non è partito in dieci secondi: lancia `chesspal table` per vedere perché.",
|
|
77
|
+
notQuit: (url) => `Il tavolo di un'altra versione di Chesspal, su ${url}, non si è chiuso: chiudi le sue pagine nel browser, aspetta cinque minuti e lancia di nuovo chesspal.`,
|
|
78
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from "../../commands/index.ts";
|
|
2
|
+
import type { Page } from "../page/page.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Attaches the chat to the table of its data folder, when one of its version answers and no other chat is at it: the page the chat then uses, whose `lost`
|
|
5
|
+
* is told once the table is gone or no longer knows the chat. An older table is asked to quit; with none, a newer one or another chat at it, none, and
|
|
6
|
+
* the chat serves its own page.
|
|
7
|
+
*/
|
|
8
|
+
export declare function attachTable(context: Context, lost: () => void): Promise<Page | undefined>;
|
|
9
|
+
/** Whether a table of this version answers for the chat's data folder with no chat at it: a chat that serves its own page goes there at its next tool. */
|
|
10
|
+
export declare function tableToJoin(context: Context): Promise<boolean>;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// A chat attached to the table (D-104): the board page is the table's, and the chat tells it over HTTP, on this machine, what a page it served itself would
|
|
2
|
+
// read from it: that it calls a tool, a copy of what it saw after each, and that the New game window should open.
|
|
3
|
+
import { Agent, request } from "node:http";
|
|
4
|
+
import { aiHostOf, aiModelOf } from "../../games/index.js";
|
|
5
|
+
import { tableFolder } from "../../home/index.js";
|
|
6
|
+
import { outdatedHostFiles } from "../../host-files/index.js";
|
|
7
|
+
import { gameFolder } from "../../profiles/index.js";
|
|
8
|
+
import { version } from "../../version.js";
|
|
9
|
+
import { jsonOf } from "../page/http.js";
|
|
10
|
+
import { askToQuit, isOlder, tableAt } from "./table.js";
|
|
11
|
+
/**
|
|
12
|
+
* Attaches the chat to the table of its data folder, when one of its version answers and no other chat is at it: the page the chat then uses, whose `lost`
|
|
13
|
+
* is told once the table is gone or no longer knows the chat. An older table is asked to quit; with none, a newer one or another chat at it, none, and
|
|
14
|
+
* the chat serves its own page.
|
|
15
|
+
*/
|
|
16
|
+
export async function attachTable(context, lost) {
|
|
17
|
+
const table = await tableOf(context);
|
|
18
|
+
if (table === undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (table.version !== version) {
|
|
22
|
+
if (table.version === undefined || isOlder(table.version, version)) {
|
|
23
|
+
await askToQuit(table.port);
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const link = new TableLink(table.port, context, lost);
|
|
28
|
+
return (await link.attach()) ? tablePage(link, table.url) : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Whether a table of this version answers for the chat's data folder with no chat at it: a chat that serves its own page goes there at its next tool. */
|
|
31
|
+
export async function tableToJoin(context) {
|
|
32
|
+
const table = await tableOf(context);
|
|
33
|
+
return table !== undefined && table.version === version && table.chat === undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The table of the chat's data folder, when one answers; none while the data folder cannot be made. */
|
|
36
|
+
async function tableOf(context) {
|
|
37
|
+
try {
|
|
38
|
+
return await tableAt(tableFolder(context.home()));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// How long the table may take to answer the chat: it answers in a millisecond, so one that takes more than a second is stuck (Federico).
|
|
45
|
+
const answerWait = 1_500;
|
|
46
|
+
/** The page as the attached chat uses it: what a page of its own reads from the chat, the chat sends the table. */
|
|
47
|
+
function tablePage(link, url) {
|
|
48
|
+
return {
|
|
49
|
+
url,
|
|
50
|
+
heard: () => link.copy(true),
|
|
51
|
+
chatCalls: () => link.calls(),
|
|
52
|
+
toolEnded: () => link.copy(false),
|
|
53
|
+
open: () => link.open,
|
|
54
|
+
inUse: () => true,
|
|
55
|
+
askNewGame: () => link.askNewGame(),
|
|
56
|
+
close: async () => link.leave(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** The chat's end of its place at the table: its token, whether a browser has the page open, and the copies it sends, in order, one at a time. */
|
|
60
|
+
class TableLink {
|
|
61
|
+
/** Whether a browser had the page open at the table's last answer. */
|
|
62
|
+
open = false;
|
|
63
|
+
token = "";
|
|
64
|
+
attachment;
|
|
65
|
+
agent = new Agent({ keepAlive: true, maxSockets: 1 });
|
|
66
|
+
news = false;
|
|
67
|
+
copying = false;
|
|
68
|
+
left = false;
|
|
69
|
+
port;
|
|
70
|
+
context;
|
|
71
|
+
lost;
|
|
72
|
+
constructor(port, context, lost) {
|
|
73
|
+
this.port = port;
|
|
74
|
+
this.context = context;
|
|
75
|
+
this.lost = lost;
|
|
76
|
+
}
|
|
77
|
+
/** Attaches: whether the table took the chat. The answer then stays open for as long as both live; when it closes, the chat has lost the table. */
|
|
78
|
+
attach() {
|
|
79
|
+
return new Promise((done) => {
|
|
80
|
+
let text = "";
|
|
81
|
+
let attached = false;
|
|
82
|
+
// A table that takes the chat and then says nothing is no table: the chat serves its own page (Priya).
|
|
83
|
+
const silent = setTimeout(() => this.attachment?.destroy(), answerWait);
|
|
84
|
+
silent.unref();
|
|
85
|
+
this.attachment = request({ ...this.address("/api/chat"), agent: false }, (response) => {
|
|
86
|
+
response.setEncoding("utf8");
|
|
87
|
+
// The first line gives the chat its token; a table that refuses it answers with a status of its own and closes.
|
|
88
|
+
response.on("data", (chunk) => {
|
|
89
|
+
if (attached) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
text += chunk;
|
|
93
|
+
const line = text.indexOf("\n");
|
|
94
|
+
if (line === -1) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const answer = jsonOf(text.slice(0, line));
|
|
98
|
+
clearTimeout(silent);
|
|
99
|
+
if (response.statusCode === 200 && typeof answer?.token === "string") {
|
|
100
|
+
attached = true;
|
|
101
|
+
this.token = answer.token;
|
|
102
|
+
this.open = answer.open === true;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.attachment?.destroy();
|
|
106
|
+
}
|
|
107
|
+
done(attached);
|
|
108
|
+
});
|
|
109
|
+
response.on("close", () => {
|
|
110
|
+
clearTimeout(silent);
|
|
111
|
+
return attached ? this.leave() : done(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
// The answer kept open never keeps the chat's process alive: it ends when its host goes.
|
|
115
|
+
this.attachment.on("socket", (socket) => socket.unref());
|
|
116
|
+
this.attachment.on("error", () => {
|
|
117
|
+
clearTimeout(silent);
|
|
118
|
+
done(false);
|
|
119
|
+
});
|
|
120
|
+
this.attachment.end(JSON.stringify(attachment(this.context)));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** The chat calls a tool: the table hears it before the tool runs, which starts the AI's clock when nobody had heard the player. */
|
|
124
|
+
async calls() {
|
|
125
|
+
this.answered(await this.post("/api/chat/call", {}));
|
|
126
|
+
}
|
|
127
|
+
/** Sends a copy of what the chat saw once the call under way has ended, those asked meanwhile as one; with `news`, the pages hear of it. */
|
|
128
|
+
copy(news) {
|
|
129
|
+
this.news ||= news;
|
|
130
|
+
if (this.copying) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.copying = true;
|
|
134
|
+
setImmediate(() => {
|
|
135
|
+
this.copying = false;
|
|
136
|
+
const body = { ...seenBy(this.context), news: this.news };
|
|
137
|
+
this.news = false;
|
|
138
|
+
void this.post("/api/chat/read", body).then((answer) => this.answered(answer));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Asks the pages open to show the New game window, without waiting: whether one was open, as the table last said. */
|
|
142
|
+
askNewGame() {
|
|
143
|
+
void this.post("/api/chat/new-game", {}).then((answer) => this.answered(answer));
|
|
144
|
+
return this.open;
|
|
145
|
+
}
|
|
146
|
+
/** Leaves the table, once: the chat's process ends, the table is gone, or no longer knows the chat. */
|
|
147
|
+
leave() {
|
|
148
|
+
if (!this.left) {
|
|
149
|
+
this.left = true;
|
|
150
|
+
this.attachment?.destroy();
|
|
151
|
+
this.agent.destroy();
|
|
152
|
+
this.lost();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// The table did not answer, or no longer knows the chat: it is lost. A request it failed alone changes nothing.
|
|
156
|
+
answered(answer) {
|
|
157
|
+
if (answer === "lost") {
|
|
158
|
+
this.leave();
|
|
159
|
+
}
|
|
160
|
+
else if (typeof answer?.open === "boolean") {
|
|
161
|
+
this.open = answer.open;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// A request to the table on the chat's own connection, kept alive, and once more on a new one when the table had just closed the one kept.
|
|
165
|
+
post(path, body, retry = true) {
|
|
166
|
+
if (this.left) {
|
|
167
|
+
return Promise.resolve("lost");
|
|
168
|
+
}
|
|
169
|
+
return new Promise((done) => {
|
|
170
|
+
const asked = request({ ...this.address(path), agent: this.agent }, (response) => {
|
|
171
|
+
const chunks = [];
|
|
172
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
173
|
+
response.on("end", () => done(answerOf(response.statusCode, Buffer.concat(chunks).toString("utf8"))));
|
|
174
|
+
response.on("close", () => done("lost"));
|
|
175
|
+
});
|
|
176
|
+
// A table that stops answering would hold every tool of the chat's: it is lost, and the chat serves its own page (Priya).
|
|
177
|
+
asked.setTimeout(answerWait, () => asked.destroy(new Error("The table did not answer.")));
|
|
178
|
+
asked.on("error", (error) => {
|
|
179
|
+
// The table closes a connection idle for 5 s as Node reuses it: the request never reached it (Federico).
|
|
180
|
+
if (retry && asked.reusedSocket && error.code === "ECONNRESET") {
|
|
181
|
+
void this.post(path, body, false).then(done);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
done("lost");
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
asked.end(JSON.stringify({ ...body, token: this.token }));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
address(path) {
|
|
191
|
+
return { host: "127.0.0.1", port: this.port, path, method: "POST", headers: { "Content-Type": "application/json" } };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** A table that no longer knows the chat answers 409; another status is one request it failed, and the chat stays. */
|
|
195
|
+
function answerOf(status, text) {
|
|
196
|
+
if (status === 409) {
|
|
197
|
+
return "lost";
|
|
198
|
+
}
|
|
199
|
+
return status === 200 ? jsonOf(text) : undefined;
|
|
200
|
+
}
|
|
201
|
+
/** What the chat says of itself as it attaches:its version, session and folder, its host, and whether its folder needs init again. */
|
|
202
|
+
function attachment(context) {
|
|
203
|
+
return {
|
|
204
|
+
version,
|
|
205
|
+
session: process.env.CLAUDE_CODE_SESSION_ID ?? null,
|
|
206
|
+
folder: context.folder,
|
|
207
|
+
host: context.host?.(),
|
|
208
|
+
outdated: outdatedHostFiles(context.folder, context.platform).length > 0,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** What the chat saw, for the table's copy: the game and the position, the words it was given, its host, and who played as the AI (D-82, D-97). */
|
|
212
|
+
function seenBy(context) {
|
|
213
|
+
const folder = gameFolderOf(context);
|
|
214
|
+
return {
|
|
215
|
+
seen: context.seen,
|
|
216
|
+
positionSeen: context.positionSeen,
|
|
217
|
+
positionFirstSeen: context.positionFirstSeen,
|
|
218
|
+
given: [...context.given],
|
|
219
|
+
host: context.host?.(),
|
|
220
|
+
aiModel: folder === undefined ? undefined : aiModelOf(folder),
|
|
221
|
+
aiHost: folder === undefined ? undefined : aiHostOf(folder),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function gameFolderOf(context) {
|
|
225
|
+
try {
|
|
226
|
+
return gameFolder(context.home());
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Context } from "../../commands/index.ts";
|
|
2
|
+
import type { TableChats } from "../page/table.ts";
|
|
3
|
+
/** A table served: its address, and how to close it. */
|
|
4
|
+
export interface Table {
|
|
5
|
+
readonly url: string;
|
|
6
|
+
/** Settled once the table has closed: by itself, asked by another Chesspal version, or by close. */
|
|
7
|
+
readonly closed: Promise<void>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** How long the table stays unused before it closes, and how often it looks, in milliseconds. */
|
|
11
|
+
export interface TableTimes {
|
|
12
|
+
readonly idle: number;
|
|
13
|
+
readonly every: number;
|
|
14
|
+
}
|
|
15
|
+
/** Five minutes with no browser and no chat, looked at every fifteen seconds (D-104). */
|
|
16
|
+
export declare const tableTimes: TableTimes;
|
|
17
|
+
/** A table that answers on its ports: its address, port and version, and the folder of the chat attached to it, when one is. */
|
|
18
|
+
export interface TableFound {
|
|
19
|
+
readonly url: string;
|
|
20
|
+
readonly port: number;
|
|
21
|
+
readonly version: string | undefined;
|
|
22
|
+
readonly chat: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Starts the table on the context's data folder, its folder set up as init does, its page opening chats through `chats`: undefined when another table
|
|
26
|
+
* started at the same time took a lower port, since a table's ports are its lock. It closes once no page has been open, no chat attached and no request
|
|
27
|
+
* come, for `times.idle`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function startTable(context: Context, times?: TableTimes, chats?: TableChats): Promise<Table | undefined>;
|
|
30
|
+
/** The table of a folder that answers on its ports, the lowest of them; its ports are asked together. */
|
|
31
|
+
export declare function tableAt(folder: string): Promise<TableFound | undefined>;
|
|
32
|
+
/** Asks the table at a port to quit; it answers before it goes, and a table that cannot be reached, or does not answer within a second or so, is let be. */
|
|
33
|
+
export declare function askToQuit(port: number): Promise<void>;
|
|
34
|
+
/** Whether a version is older than another, part by part: 0.2.4 is older than 0.10.0. */
|
|
35
|
+
export declare function isOlder(one: string, other: string): boolean;
|