chesspal 0.2.4 → 0.3.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 +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 +9 -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 +29 -16
- 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 +95 -42
- 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 +111 -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/opener.css +65 -0
- package/dist/page/table/opener.js +59 -0
- package/dist/page/talk/chat.css +10 -3
- package/dist/page/talk/talk.css +38 -26
- package/dist/page/talk/talk.js +4 -2
- package/dist/page/text.js +83 -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 +60 -0
- package/dist/server/page/chat.js +79 -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 +47 -7
- package/dist/server/page/page.js +69 -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 +39 -0
- package/dist/server/page/table.js +102 -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 +31 -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 +25 -0
- package/dist/server/table/hosts/play.js +152 -0
- package/dist/server/table/hosts/seat.d.ts +25 -0
- package/dist/server/table/hosts/seat.js +74 -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 +230 -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,49 @@
|
|
|
1
|
+
// The AI hosts the table opens a chat in (D-104): Claude Code and Codex, each with the start message, and the model and effort the player chose on the page
|
|
2
|
+
// for the profile in use; "as the host" passes nothing, and the host's own settings stand.
|
|
3
|
+
import { speaksItalian } from "../../../commands/index.js";
|
|
4
|
+
/** The hosts the page offers, by their command's name. */
|
|
5
|
+
export const hostNames = ["claude", "codex"];
|
|
6
|
+
/**
|
|
7
|
+
* Each host's choices, as tried in each (Priya): a value outside them would fail only at the chat's first turn. Claude Code's are its aliases; Codex's, the
|
|
8
|
+
* models a ChatGPT account lists, which change with its releases.
|
|
9
|
+
*/
|
|
10
|
+
export const hostChoices = {
|
|
11
|
+
claude: { models: ["fable", "opus", "sonnet", "haiku"], efforts: ["low", "medium", "high", "xhigh", "max"] },
|
|
12
|
+
codex: { models: ["gpt-6.1-sol", "gpt-6-astra", "gpt-6.1-terra", "gpt-6-luna"], efforts: ["low", "medium", "high", "xhigh", "max"] },
|
|
13
|
+
};
|
|
14
|
+
/** Whether a word names a host the page offers. */
|
|
15
|
+
export function isHostName(name) {
|
|
16
|
+
return hostNames.includes(name);
|
|
17
|
+
}
|
|
18
|
+
/** The message a chat opened by the table starts with, in the player's language: the AI answers it by calling status, as every chat does. */
|
|
19
|
+
export function startMessage(language) {
|
|
20
|
+
return speaksItalian(language) ? "Giochiamo!" : "Let's play!";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The start message and the choices the host knows: first for Claude Code, whose options may take the word after; last for Codex, where "resume" is no
|
|
24
|
+
* subcommand, kept from its update screen, which an Enter would accept.
|
|
25
|
+
*/
|
|
26
|
+
export function hostArgs(host, message, choice = {}) {
|
|
27
|
+
const { model, effort } = offered(host, choice);
|
|
28
|
+
if (host === "claude") {
|
|
29
|
+
return [message, ...(model === undefined ? [] : ["--model", model]), ...(effort === undefined ? [] : ["--effort", effort])];
|
|
30
|
+
}
|
|
31
|
+
return ["-c", "check_for_update_on_startup=false", ...(model === undefined ? [] : ["-m", model]), ...(effort === undefined ? [] : ["-c", `model_reasoning_effort=${effort}`]), message];
|
|
32
|
+
}
|
|
33
|
+
/** The settings' keys that keep a host's choice in the profile: claudeModel, claudeEffort, codexModel, codexEffort. */
|
|
34
|
+
export function choiceKeys(host) {
|
|
35
|
+
return { model: `${host}Model`, effort: `${host}Effort` };
|
|
36
|
+
}
|
|
37
|
+
/** A host's choice as the profile's settings keep it: the values the host offers alone, the rest is the host's own. */
|
|
38
|
+
export function choiceIn(settings, host) {
|
|
39
|
+
const keys = choiceKeys(host);
|
|
40
|
+
const text = (value) => (typeof value === "string" ? value : undefined);
|
|
41
|
+
return offered(host, { model: text(settings?.[keys.model]), effort: text(settings?.[keys.effort]) });
|
|
42
|
+
}
|
|
43
|
+
// The choice kept to the values the host offers.
|
|
44
|
+
function offered(host, choice) {
|
|
45
|
+
const { models, efforts } = hostChoices[host];
|
|
46
|
+
const model = choice.model !== undefined && models.includes(choice.model) ? choice.model : undefined;
|
|
47
|
+
const effort = choice.effort !== undefined && efforts.includes(choice.effort) ? choice.effort : undefined;
|
|
48
|
+
return { model, effort };
|
|
49
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The AI hosts a table opens a chat in (D-104): their choices, the terminal they open in, and `chesspal play`, which starts one there.
|
|
2
|
+
export { terminalChats } from "./chats.js";
|
|
3
|
+
export { isHostName } from "./hosts.js";
|
|
4
|
+
export { hostLaunch, playHost } from "./play.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Context } from "../../../commands/index.ts";
|
|
2
|
+
import { type HostName } from "./hosts.ts";
|
|
3
|
+
/** What `chesspal play` starts: the host's program, its arguments, folder and environment, and whether it needs cmd, as a .cmd shim does on Windows. */
|
|
4
|
+
export interface HostLaunch {
|
|
5
|
+
readonly program: string;
|
|
6
|
+
readonly args: readonly string[];
|
|
7
|
+
readonly folder: string;
|
|
8
|
+
readonly env: NodeJS.ProcessEnv;
|
|
9
|
+
readonly shell: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Runs the host in the table's folder, in this terminal, until it ends: its exit code; started again, clean, when the table asks for a new game (D-57).
|
|
13
|
+
* Ctrl+C is the host's: this process ignores it, or the terminal would close with the chat.
|
|
14
|
+
*/
|
|
15
|
+
export declare function playHost(context: Context, host: HostName): Promise<number>;
|
|
16
|
+
/**
|
|
17
|
+
* Runs the host until it ends, or until `again`, asked every half second, says to start it again: then it is ended with what it started, killed when it
|
|
18
|
+
* does not end. Its exit code, and whether to start it again.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runHost(launch: HostLaunch, again: () => boolean, platform?: string, every?: number): Promise<{
|
|
21
|
+
code: number;
|
|
22
|
+
again: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
/** How the host starts for the profile in use: found on the PATH, with its choice; it throws, saying how to install it, when it is not there. */
|
|
25
|
+
export declare function hostLaunch(context: Context, host: HostName, env?: NodeJS.ProcessEnv, exists?: (path: string) => boolean): HostLaunch;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// `chesspal play claude` (D-104): the chat the table's button opens, in the terminal the table opened for it, or one the player typed it in: the host started
|
|
2
|
+
// in the table's folder with the start message, and the model and effort chosen on the page for the profile in use; started again for a new game.
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { win32 } from "node:path";
|
|
6
|
+
import { tableFolder } from "../../../home/index.js";
|
|
7
|
+
import { setUpHosts } from "../../../host-files/index.js";
|
|
8
|
+
import { activeProfile, gameFolder, languageIn, profileSettings } from "../../../profiles/index.js";
|
|
9
|
+
import { programPath } from "./find.js";
|
|
10
|
+
import { choiceIn, hostArgs, startMessage } from "./hosts.js";
|
|
11
|
+
import { askedAgain, leaveSeat, refreshSeat, seatTimes, takeSeat } from "./seat.js";
|
|
12
|
+
import { cmdQuoted } from "./terminal.js";
|
|
13
|
+
/**
|
|
14
|
+
* Runs the host in the table's folder, in this terminal, until it ends: its exit code; started again, clean, when the table asks for a new game (D-57).
|
|
15
|
+
* Ctrl+C is the host's: this process ignores it, or the terminal would close with the chat.
|
|
16
|
+
*/
|
|
17
|
+
export async function playHost(context, host) {
|
|
18
|
+
const folder = tableFolder(context.home());
|
|
19
|
+
takeSeat(folder, { pid: process.pid, host });
|
|
20
|
+
process.on("SIGINT", () => undefined);
|
|
21
|
+
// A window closed with its X ends this process without its finally: the seat goes first.
|
|
22
|
+
process.on("SIGHUP", () => {
|
|
23
|
+
leaveSeat(folder, process.pid);
|
|
24
|
+
process.exit(129);
|
|
25
|
+
});
|
|
26
|
+
const terminal = savedTerminal(context.platform);
|
|
27
|
+
let refreshed = Date.now();
|
|
28
|
+
const asked = () => {
|
|
29
|
+
if (Date.now() - refreshed >= seatTimes.refresh) {
|
|
30
|
+
refreshed = Date.now();
|
|
31
|
+
refreshSeat(folder, process.pid);
|
|
32
|
+
}
|
|
33
|
+
return askedAgain(folder, process.pid);
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
for (;;) {
|
|
37
|
+
// A request left from a second click while the host before was ending is not for this one.
|
|
38
|
+
askedAgain(folder, process.pid);
|
|
39
|
+
const ran = await runHost(hostLaunch(context, host), asked, context.platform).catch((error) => {
|
|
40
|
+
throw new Error(`${hostTitle[host]} did not start: ${error.message}`);
|
|
41
|
+
});
|
|
42
|
+
if (!ran.again) {
|
|
43
|
+
return ran.code;
|
|
44
|
+
}
|
|
45
|
+
terminal.restore();
|
|
46
|
+
process.stdout.write(cleanScreen);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
leaveSeat(folder, process.pid);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Runs the host until it ends, or until `again`, asked every half second, says to start it again: then it is ended with what it started, killed when it
|
|
55
|
+
* does not end. Its exit code, and whether to start it again.
|
|
56
|
+
*/
|
|
57
|
+
export function runHost(launch, again, platform = process.platform, every = 500) {
|
|
58
|
+
return new Promise((done, failed) => {
|
|
59
|
+
const words = [launch.program, ...launch.args];
|
|
60
|
+
const command = launch.shell ? [words.map(cmdQuoted).join(" ")] : words;
|
|
61
|
+
const chat = spawn(command[0], command.slice(1), { cwd: launch.folder, env: launch.env, stdio: "inherit", shell: launch.shell });
|
|
62
|
+
let ended = false;
|
|
63
|
+
let killing;
|
|
64
|
+
const watch = setInterval(() => {
|
|
65
|
+
if (!ended && again()) {
|
|
66
|
+
ended = true;
|
|
67
|
+
endTree(chat, platform);
|
|
68
|
+
killing = setTimeout(() => chat.kill("SIGKILL"), killAfter);
|
|
69
|
+
}
|
|
70
|
+
}, every);
|
|
71
|
+
const stop = () => {
|
|
72
|
+
clearInterval(watch);
|
|
73
|
+
clearTimeout(killing);
|
|
74
|
+
};
|
|
75
|
+
chat.once("error", (error) => {
|
|
76
|
+
stop();
|
|
77
|
+
failed(error);
|
|
78
|
+
});
|
|
79
|
+
chat.once("exit", (code) => {
|
|
80
|
+
stop();
|
|
81
|
+
done({ code: code ?? 1, again: ended || again() });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** How the host starts for the profile in use: found on the PATH, with its choice; it throws, saying how to install it, when it is not there. */
|
|
86
|
+
export function hostLaunch(context, host, env = process.env, exists) {
|
|
87
|
+
const home = context.home();
|
|
88
|
+
const folder = tableFolder(home);
|
|
89
|
+
const found = programPath(host, context.platform, env, exists);
|
|
90
|
+
if (found === undefined) {
|
|
91
|
+
throw new Error(`${hostTitle[host]} is not installed on this computer, or not on its PATH: install it (${installHint[host]}), then try again.`);
|
|
92
|
+
}
|
|
93
|
+
setUpHosts(folder, context.platform);
|
|
94
|
+
const choice = choiceIn(profileSettings(home, activeProfile(home).id), host);
|
|
95
|
+
const args = hostArgs(host, startMessage(languageIn(gameFolder(home), context.language)), choice);
|
|
96
|
+
// An effort the player set for every chat would win over the one chosen on the page: spawn leaves out a variable that is undefined.
|
|
97
|
+
const effort = host === "claude" && choice.effort !== undefined ? { CLAUDE_CODE_EFFORT_LEVEL: undefined } : {};
|
|
98
|
+
const childEnv = { ...env, ...Object.fromEntries(sessionMarks.map((name) => [name, undefined])), ...effort };
|
|
99
|
+
const script = npmScript(host, found, context.platform, exists ?? existsSync);
|
|
100
|
+
if (script !== undefined) {
|
|
101
|
+
return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false };
|
|
102
|
+
}
|
|
103
|
+
return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found) };
|
|
104
|
+
}
|
|
105
|
+
const hostTitle = { claude: "Claude Code", codex: "Codex" };
|
|
106
|
+
// A host that has not ended this long after it was asked is killed.
|
|
107
|
+
const killAfter = 3_000;
|
|
108
|
+
// A host ended in the middle leaves the modes it set: colours, scroll region, synchronized output, bracketed paste, focus and mouse reports, kitty's
|
|
109
|
+
// keyboard flags, its own screen, a hidden cursor (Priya). All off, and the screen cleared, as a new chat.
|
|
110
|
+
const cleanScreen = "\x1b[0m\x1b[r\x1b[?2026l\x1b[?2004l\x1b[?1004l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[<99u\x1b[?1049l\x1b[?25h\x1b[2J\x1b[3J\x1b[H";
|
|
111
|
+
// The host and what it started, its MCP server among them: on Windows its whole tree, which taskkill ends, never the console (Priya); else the host,
|
|
112
|
+
// which ends its own. A taskkill that failed leaves it to the kill that follows.
|
|
113
|
+
function endTree(chat, platform) {
|
|
114
|
+
if (platform !== "win32" || chat.pid === undefined) {
|
|
115
|
+
chat.kill("SIGTERM");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const taskkill = win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe");
|
|
119
|
+
const ended = spawnSync(taskkill, ["/T", "/F", "/PID", String(chat.pid)], { stdio: "ignore", windowsHide: true });
|
|
120
|
+
if (ended.status !== 0) {
|
|
121
|
+
chat.kill();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The terminal's settings before the first host, which one ended in the middle leaves raw, for the next to take as its own; stty keeps them on macOS and
|
|
126
|
+
* Linux, while on Windows each host sets the console's itself.
|
|
127
|
+
*/
|
|
128
|
+
function savedTerminal(platform) {
|
|
129
|
+
const saved = platform === "win32" || !process.stdin.isTTY ? undefined : spawnSync("stty", ["-g"], { stdio: ["inherit", "pipe", "ignore"], encoding: "utf8" });
|
|
130
|
+
const settings = saved?.status === 0 ? saved.stdout.trim() : undefined;
|
|
131
|
+
return {
|
|
132
|
+
restore: () => {
|
|
133
|
+
if (settings !== undefined) {
|
|
134
|
+
spawnSync("stty", [settings], { stdio: ["inherit", "ignore", "ignore"] });
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// What marks a chat's own session, which a table started from inside a chat would hand down, and the host take for its own (Priya).
|
|
140
|
+
const sessionMarks = ["CLAUDECODE", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION", "CLAUDE_CODE_SESSION_ATTENDED", "CLAUDE_PID", "CLAUDE_EFFORT", "AI_AGENT", "TRACEPARENT"];
|
|
141
|
+
const installHint = { claude: "https://claude.com/claude-code", codex: "npm install -g @openai/codex" };
|
|
142
|
+
/**
|
|
143
|
+
* The script behind npm's Windows shim of Codex, which Node runs as the shim would, without cmd and its quoting of the start message (Priya); none for
|
|
144
|
+
* another program, or when it is not beside the shim.
|
|
145
|
+
*/
|
|
146
|
+
function npmScript(host, program, platform, exists) {
|
|
147
|
+
if (host !== "codex" || platform !== "win32" || !/\.cmd$/i.test(program)) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const script = win32.join(win32.dirname(program), "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
151
|
+
return exists(script) ? script : undefined;
|
|
152
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The play process in the seat: its process id and its host. */
|
|
2
|
+
export interface Seat {
|
|
3
|
+
readonly pid: number;
|
|
4
|
+
readonly host: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Takes the seat for this play process, over a request left for one before it; it throws while another one holds it. A seat is held while its play
|
|
8
|
+
* refreshes it: one closed with its window, or stopped, lets it go within `heldFor`, whatever process takes its id after it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function takeSeat(folder: string, seat: Seat, now?: Date): void;
|
|
11
|
+
/** Tells the table this play process still holds the seat; nothing when it lost it. */
|
|
12
|
+
export declare function refreshSeat(folder: string, pid: number, now?: Date): void;
|
|
13
|
+
/** Leaves the seat, when this play process still holds it. */
|
|
14
|
+
export declare function leaveSeat(folder: string, pid: number): void;
|
|
15
|
+
/** Whether a play process holds the seat. */
|
|
16
|
+
export declare function seatTaken(folder: string, now?: Date): boolean;
|
|
17
|
+
/** Asks the play process in the seat to start its host again: whether one holds it, and was asked. */
|
|
18
|
+
export declare function askAgain(folder: string, now?: Date): boolean;
|
|
19
|
+
/** Whether the table asked this play process to start its host again; the request is taken. */
|
|
20
|
+
export declare function askedAgain(folder: string, pid: number): boolean;
|
|
21
|
+
/** How often a play process refreshes its seat, and how long the seat stays held without it. */
|
|
22
|
+
export declare const seatTimes: {
|
|
23
|
+
refresh: number;
|
|
24
|
+
heldFor: number;
|
|
25
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The seat `chesspal play` takes at the table (D-104): the play process notes itself in the table's folder while its host runs, one at a time, and the table
|
|
2
|
+
// asks it there to start its host again, a clean chat for a new game (D-57).
|
|
3
|
+
import { mkdirSync, readFileSync, rmSync, statSync, utimesSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { writeAtomically } from "../../../games/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Takes the seat for this play process, over a request left for one before it; it throws while another one holds it. A seat is held while its play
|
|
8
|
+
* refreshes it: one closed with its window, or stopped, lets it go within `heldFor`, whatever process takes its id after it.
|
|
9
|
+
*/
|
|
10
|
+
export function takeSeat(folder, seat, now = new Date()) {
|
|
11
|
+
const held = seatHeld(folder, now);
|
|
12
|
+
if (held !== undefined && held.pid !== seat.pid) {
|
|
13
|
+
throw new Error(`A chat in ${held.host} already plays at the table, in another window: play there, or close it, then try again.`);
|
|
14
|
+
}
|
|
15
|
+
// chesspal play may come before the table has ever been opened.
|
|
16
|
+
mkdirSync(folder, { recursive: true });
|
|
17
|
+
rmSync(join(folder, againFile), { force: true });
|
|
18
|
+
writeAtomically(join(folder, seatFile), `${JSON.stringify(seat)}\n`);
|
|
19
|
+
}
|
|
20
|
+
/** Tells the table this play process still holds the seat; nothing when it lost it. */
|
|
21
|
+
export function refreshSeat(folder, pid, now = new Date()) {
|
|
22
|
+
if (seatIn(folder)?.pid === pid) {
|
|
23
|
+
utimesSync(join(folder, seatFile), now, now);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Leaves the seat, when this play process still holds it. */
|
|
27
|
+
export function leaveSeat(folder, pid) {
|
|
28
|
+
if (seatIn(folder)?.pid === pid) {
|
|
29
|
+
rmSync(join(folder, seatFile), { force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Whether a play process holds the seat. */
|
|
33
|
+
export function seatTaken(folder, now = new Date()) {
|
|
34
|
+
return seatHeld(folder, now) !== undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Asks the play process in the seat to start its host again: whether one holds it, and was asked. */
|
|
37
|
+
export function askAgain(folder, now = new Date()) {
|
|
38
|
+
const held = seatHeld(folder, now);
|
|
39
|
+
if (held === undefined) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
writeAtomically(join(folder, againFile), `${JSON.stringify({ pid: held.pid })}\n`);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/** Whether the table asked this play process to start its host again; the request is taken. */
|
|
46
|
+
export function askedAgain(folder, pid) {
|
|
47
|
+
const asked = jsonIn(join(folder, againFile))?.pid === pid;
|
|
48
|
+
if (asked) {
|
|
49
|
+
rmSync(join(folder, againFile), { force: true });
|
|
50
|
+
}
|
|
51
|
+
return asked;
|
|
52
|
+
}
|
|
53
|
+
/** How often a play process refreshes its seat, and how long the seat stays held without it. */
|
|
54
|
+
export const seatTimes = { refresh: 5_000, heldFor: 15_000 };
|
|
55
|
+
const seatFile = "play.json";
|
|
56
|
+
const againFile = "play-again.json";
|
|
57
|
+
function seatHeld(folder, now) {
|
|
58
|
+
const seat = seatIn(folder);
|
|
59
|
+
// A file's time is finer than a Date's: it is compared to the millisecond, as the Date is.
|
|
60
|
+
const refreshed = statSync(join(folder, seatFile), { throwIfNoEntry: false })?.mtimeMs;
|
|
61
|
+
return seat !== undefined && refreshed !== undefined && now.getTime() - Math.floor(refreshed) < seatTimes.heldFor ? seat : undefined;
|
|
62
|
+
}
|
|
63
|
+
function seatIn(folder) {
|
|
64
|
+
const seat = jsonIn(join(folder, seatFile));
|
|
65
|
+
return typeof seat?.pid === "number" && typeof seat.host === "string" ? { pid: seat.pid, host: seat.host } : undefined;
|
|
66
|
+
}
|
|
67
|
+
function jsonIn(path) {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Opens a terminal window in `folder` running `command`, apart from the table, which it outlives: whether a terminal started. */
|
|
2
|
+
export declare function openTerminal(folder: string, command: readonly string[], platform: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
3
|
+
/** The terminals to try, in order, each as the program and its arguments; those not installed are left out. */
|
|
4
|
+
export declare function terminals(folder: string, command: readonly string[], platform: string, env: NodeJS.ProcessEnv, exists?: (path: string) => boolean): string[][];
|
|
5
|
+
/** A word for cmd.exe's line, in double quotes when it holds a space or & | < > ^ ( ); cmd reads % and ! even inside them, which our words never hold. */
|
|
6
|
+
export declare function cmdQuoted(word: string): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// A terminal window the table opens for a chat (D-104), in a folder, running a command: Windows Terminal, else the console Windows starts; macOS's Terminal;
|
|
2
|
+
// on Linux the terminal the desktop names, else a common one.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { programPath } from "./find.js";
|
|
5
|
+
/** Opens a terminal window in `folder` running `command`, apart from the table, which it outlives: whether a terminal started. */
|
|
6
|
+
export async function openTerminal(folder, command, platform, env = process.env) {
|
|
7
|
+
for (const [program, ...args] of terminals(folder, command, platform, env)) {
|
|
8
|
+
if (program !== undefined && await started(program, args, folder)) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
/** The terminals to try, in order, each as the program and its arguments; those not installed are left out. */
|
|
15
|
+
export function terminals(folder, command, platform, env, exists) {
|
|
16
|
+
const installed = (name) => programPath(name, platform, env, exists);
|
|
17
|
+
if (platform === "win32") {
|
|
18
|
+
const windowsTerminal = installed("wt");
|
|
19
|
+
// start takes its first quoted word as the window's title, an empty one; cmd gets its line verbatim, each word quoted where it holds & or a space.
|
|
20
|
+
const windowsConsole = ["cmd.exe", "/d", "/s", "/c", `"start "" /D ${[folder, ...command].map(cmdQuoted).join(" ")}"`];
|
|
21
|
+
// wt splits its command line at every ;, and reads \; as one.
|
|
22
|
+
const wtWords = [folder, ...command].map((word) => word.replaceAll(";", "\\;"));
|
|
23
|
+
return windowsTerminal === undefined ? [windowsConsole] : [[windowsTerminal, "-w", "new", "-d", wtWords[0], "--title", "Chesspal", ...wtWords.slice(1)], windowsConsole];
|
|
24
|
+
}
|
|
25
|
+
if (platform === "darwin") {
|
|
26
|
+
const script = `cd ${shellQuoted(folder)} && exec ${command.map(shellQuoted).join(" ")}`;
|
|
27
|
+
return [["osascript", "-e", `tell application "Terminal" to do script "${appleQuoted(script)}"`, "-e", `tell application "Terminal" to activate`]];
|
|
28
|
+
}
|
|
29
|
+
const linux = [
|
|
30
|
+
["x-terminal-emulator", "-e", ...command],
|
|
31
|
+
["gnome-terminal", `--working-directory=${folder}`, "--", ...command],
|
|
32
|
+
["konsole", "--workdir", folder, "-e", ...command],
|
|
33
|
+
["xfce4-terminal", `--working-directory=${folder}`, "-x", ...command],
|
|
34
|
+
["xterm", "-e", ...command],
|
|
35
|
+
];
|
|
36
|
+
return linux.flatMap(([name, ...args]) => {
|
|
37
|
+
const path = installed(name ?? "");
|
|
38
|
+
return path === undefined ? [] : [[path, ...args]];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** A word for cmd.exe's line, in double quotes when it holds a space or & | < > ^ ( ); cmd reads % and ! even inside them, which our words never hold. */
|
|
42
|
+
export function cmdQuoted(word) {
|
|
43
|
+
return /[\s&|<>^()!%]/.test(word) ? `"${word}"` : word;
|
|
44
|
+
}
|
|
45
|
+
// A program started apart from the table: whether it started. cmd's line is quoted already.
|
|
46
|
+
function started(program, args, folder) {
|
|
47
|
+
return new Promise((done) => {
|
|
48
|
+
const opened = spawn(program, args, { cwd: folder, detached: true, stdio: "ignore", windowsVerbatimArguments: program === "cmd.exe" });
|
|
49
|
+
opened.once("spawn", () => {
|
|
50
|
+
opened.unref();
|
|
51
|
+
done(true);
|
|
52
|
+
});
|
|
53
|
+
opened.once("error", () => done(false));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// A word for the shell, in single quotes, a quote in it closed and escaped.
|
|
57
|
+
function shellQuoted(word) {
|
|
58
|
+
return `'${word.replaceAll("'", "'\\''")}'`;
|
|
59
|
+
}
|
|
60
|
+
// Text inside an AppleScript string.
|
|
61
|
+
function appleQuoted(text) {
|
|
62
|
+
return text.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** What the icon starts: Node with the arguments of `chesspal` alone, in a folder. */
|
|
2
|
+
export interface IconCommand {
|
|
3
|
+
readonly node: string;
|
|
4
|
+
readonly args: readonly string[];
|
|
5
|
+
readonly folder: string;
|
|
6
|
+
}
|
|
7
|
+
/** The folders the icon goes in: the desktop and the system's menu of applications; the tests give their own. */
|
|
8
|
+
export interface IconPlaces {
|
|
9
|
+
readonly desktop: string;
|
|
10
|
+
readonly menu: string;
|
|
11
|
+
}
|
|
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 declare function makeIcons(platform: string, command: IconCommand, places?: IconPlaces): Promise<string[]>;
|
|
17
|
+
/**
|
|
18
|
+
* 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
|
|
19
|
+
* terminal, where `chesspal` alone could ask about the icon unseen; `open` never asks.
|
|
20
|
+
*/
|
|
21
|
+
export declare function iconCommand(home: string): IconCommand;
|
|
22
|
+
/** 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. */
|
|
23
|
+
export declare function iconAsked(home: string, machine?: string): boolean;
|
|
24
|
+
/** Notes that the command line asked on this machine, whatever the answer. */
|
|
25
|
+
export declare function noteIconAsked(home: string, machine?: string): void;
|
|
26
|
+
/** Notes the icon made on this machine, its command and files, which asks no more there. */
|
|
27
|
+
export declare function noteIconMade(home: string, command: IconCommand, files: readonly string[], machine?: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* 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
|
|
30
|
+
* there: then it is made again. One the player removed stays removed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function iconToRemake(home: string, command: IconCommand, machine?: string, exists?: (path: string) => boolean): boolean;
|
|
@@ -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[]>;
|