@zaisma/cli 0.7.0 → 0.9.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/package.json +1 -1
- package/zaisma.mjs +117 -3
package/package.json
CHANGED
package/zaisma.mjs
CHANGED
|
@@ -49,6 +49,7 @@ const COMMAND_ERROR_CODES = {
|
|
|
49
49
|
file_exists: "database export won't write over a file that's already there.",
|
|
50
50
|
needs_newer_node: "Reading or writing a .sqlite file needs Node 22.13 or newer.",
|
|
51
51
|
not_a_database: "The .sqlite file to import isn't a SQLite database.",
|
|
52
|
+
unknown_link: "links remove: the game has no link by that name.",
|
|
52
53
|
server_unreachable: "Zaisma couldn't be reached: no connection, or a wrong ZAISMA_API.",
|
|
53
54
|
unexpected_answer: "Zaisma answered without JSON, like a page from a network in between.",
|
|
54
55
|
};
|
|
@@ -82,6 +83,21 @@ const COMMANDS = {
|
|
|
82
83
|
},
|
|
83
84
|
unschedule: { about: "Cancel a publish-later", arguments: [{ name: "slug" }, { name: "version_id" }], flags: {} },
|
|
84
85
|
visibility: { about: "Set who can see a game: everyone (listed), people with the link (unlisted), or only you and tester links (private, draft)", arguments: [{ name: "slug" }, { name: "visibility", choices: VISIBILITIES }], flags: {} },
|
|
86
|
+
"allowed-hosts": {
|
|
87
|
+
about: "The hosts a game's server can call with fetch(), like discord.com or *.example.com; given hosts, set the whole list",
|
|
88
|
+
arguments: [{ name: "slug" }, { name: "hosts", isRest: true, isOptional: true }],
|
|
89
|
+
flags: { none: { hasValue: false, about: "Set the list to none: the game server calls no other servers" } },
|
|
90
|
+
},
|
|
91
|
+
links: { about: "A game's registered links, which it opens with Zaisma.links.open: each name, address and who it opens for", arguments: [{ name: "slug" }], flags: {} },
|
|
92
|
+
"links set": {
|
|
93
|
+
about: "Add a link, or change the one with the same name",
|
|
94
|
+
arguments: [{ name: "slug" }, { name: "link_name" }, { name: "destination_url" }],
|
|
95
|
+
flags: { "on-game-page": { hasValue: false, about: "Show it on the game page too" } },
|
|
96
|
+
},
|
|
97
|
+
"links remove": { about: "Remove a link: the game can't open it any more", arguments: [{ name: "slug" }, { name: "link_name" }], flags: {} },
|
|
98
|
+
secrets: { about: "A game's secrets, which its game server reads as server.secrets and room.secrets: their names and when each was set, never the values", arguments: [{ name: "slug" }], flags: {} },
|
|
99
|
+
"secrets set": { about: "Set a secret for a game's server, replacing one of the same name: the value piped in, or typed without showing; it's never shown again", arguments: [{ name: "slug" }, { name: "secret_name" }], flags: {} },
|
|
100
|
+
"secrets delete": { about: "Delete one of a game's secrets", arguments: [{ name: "slug" }, { name: "secret_name" }], flags: {} },
|
|
85
101
|
forum: {
|
|
86
102
|
about: "Your forum topics and messages to Zaisma; one topic with every post you can see; or a search",
|
|
87
103
|
arguments: [{ name: "topic_id", isOptional: true }],
|
|
@@ -159,7 +175,7 @@ const COMMANDS = {
|
|
|
159
175
|
help: { about: "This help; with --json, every command, flag, exit code and error code", arguments: [], flags: {} },
|
|
160
176
|
};
|
|
161
177
|
/** Commands with a second word, like zaisma forum post. */
|
|
162
|
-
const SUBCOMMANDS = { forum: ["post", "message", "reply"], database: ["query", "batch", "export", "import"], saves: ["get", "change", "replace", "reset", "copies", "copy", "restore"] };
|
|
178
|
+
const SUBCOMMANDS = { links: ["set", "remove"], forum: ["post", "message", "reply"], database: ["query", "batch", "export", "import"], saves: ["get", "change", "replace", "reset", "copies", "copy", "restore"], secrets: ["set", "delete"] };
|
|
163
179
|
/** Flags every command takes. */
|
|
164
180
|
const GLOBAL_FLAGS = {
|
|
165
181
|
json: { hasValue: false, about: "Print the API's answer as JSON, as-is (on failure its { error }), and nothing else" },
|
|
@@ -838,6 +854,97 @@ function* sqliteDumpStatements(sqlite) {
|
|
|
838
854
|
yield "COMMIT;\n";
|
|
839
855
|
}
|
|
840
856
|
|
|
857
|
+
/** The hosts a game's server can call (game_server_allowed_hosts on the game), as the MCP's get_game and update_game. */
|
|
858
|
+
async function allowedHosts(values, flags) {
|
|
859
|
+
const given = (values.hosts ?? "").split(/[\s,]+/).filter(Boolean);
|
|
860
|
+
if (given.length && flags.none) refuse("unknown_field", { message: "Hosts and --none were both given.", field: "--none", fix: `Give the whole list, like zaisma allowed-hosts ${values.slug} discord.com api.github.com, or --none for none.` });
|
|
861
|
+
const answer = given.length || flags.none ? await api("PATCH", `/games/${values.slug}`, { game_server_allowed_hosts: given }) : await api("GET", `/games/${values.slug}`);
|
|
862
|
+
if (isJson) return printJson(answer);
|
|
863
|
+
const hosts = answer.game.game_server_allowed_hosts;
|
|
864
|
+
console.log(hosts.length ? `${values.slug}'s game server can call: ${hosts.join(", ")}` : `${values.slug}'s game server calls no other servers. List some with: zaisma allowed-hosts ${values.slug} discord.com`);
|
|
865
|
+
if (given.length || flags.none) console.log("It works within a minute.");
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** Who a link opens for, in words (who_can_open). */
|
|
869
|
+
const LINK_AUDIENCE_WORDS = { everyone: "opens for everyone", signed_in_adults: "opens for signed-in adults", nobody: "opens for nobody" };
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* A game's registered links (links on the game), as the MCP's get_game and update_game: the API takes
|
|
873
|
+
* the whole list, so set and remove change it and send it back.
|
|
874
|
+
*/
|
|
875
|
+
async function links(command, values, flags) {
|
|
876
|
+
let answer = await api("GET", `/games/${values.slug}`);
|
|
877
|
+
if (command !== "links") {
|
|
878
|
+
const kept = answer.game.links.map((link) => ({ link_name: link.link_name, destination_url: link.destination_url, is_on_game_page: link.is_on_game_page }));
|
|
879
|
+
const others = kept.filter((link) => link.link_name !== values.link_name);
|
|
880
|
+
if (command === "links remove" && others.length === kept.length) {
|
|
881
|
+
refuse("unknown_link", { message: `${values.slug} has no link named ${values.link_name}.`, field: "link_name", fix: `zaisma links ${values.slug} lists its links.` }, EXIT_CODES.failed);
|
|
882
|
+
}
|
|
883
|
+
const changed = command === "links set" ? kept.map((link) => (link.link_name === values.link_name ? { ...link, destination_url: values.destination_url, is_on_game_page: Boolean(flags["on-game-page"]) } : link)) : others;
|
|
884
|
+
if (command === "links set" && !kept.some((link) => link.link_name === values.link_name)) changed.push({ link_name: values.link_name, destination_url: values.destination_url, is_on_game_page: Boolean(flags["on-game-page"]) });
|
|
885
|
+
answer = await api("PATCH", `/games/${values.slug}`, { links: changed });
|
|
886
|
+
}
|
|
887
|
+
if (isJson) return printJson(answer);
|
|
888
|
+
const gameLinks = answer.game.links;
|
|
889
|
+
if (!gameLinks.length) return console.log(`${values.slug} has no links. Add one with: zaisma links set ${values.slug} store https://kit.itch.io/${values.slug}`);
|
|
890
|
+
for (const link of gameLinks) {
|
|
891
|
+
console.log(`${link.link_name.padEnd(20)} ${link.destination_url} (${LINK_AUDIENCE_WORDS[link.who_can_open]}${link.is_on_game_page ? ", on the game page" : ""})`);
|
|
892
|
+
if (link.review_reason) console.log(`${"".padEnd(20)} ${link.review_reason.message} ${link.review_reason.fix}`);
|
|
893
|
+
}
|
|
894
|
+
if (command === "links set") console.log(`The game opens it with Zaisma.links.open("${values.link_name}").`);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** A game's secrets (…/games/<slug>/secrets…), as the MCP's list_secrets, set_secret and delete_secret. */
|
|
898
|
+
async function secrets(command, values) {
|
|
899
|
+
if (command === "secrets") {
|
|
900
|
+
const answer = await api("GET", `/games/${values.slug}/secrets`);
|
|
901
|
+
if (isJson) return printJson(answer);
|
|
902
|
+
if (!answer.secrets.length) console.log(`No secrets yet. Set one with: zaisma secrets set ${values.slug} DISCORD_WEBHOOK_URL`);
|
|
903
|
+
for (const secret of answer.secrets) console.log(`${secret.name.padEnd(32)} set ${when(secret.set_at)}`);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const path = `/games/${values.slug}/secrets/${encodeURIComponent(values.secret_name)}`;
|
|
907
|
+
if (command === "secrets delete") {
|
|
908
|
+
const answer = await api("DELETE", path);
|
|
909
|
+
if (isJson) return printJson(answer);
|
|
910
|
+
return console.log(`Deleted ${values.secret_name} from ${values.slug}. Running rooms lose it within a minute.`);
|
|
911
|
+
}
|
|
912
|
+
const secretValue = await readSecretValue(values.secret_name);
|
|
913
|
+
if (!secretValue) refuse("missing_field", { message: `No value was given for ${values.secret_name}.`, field: "secret_value", fix: `Pipe it in, like printf %s "$TOKEN" | zaisma secrets set ${values.slug} ${values.secret_name}, or run it in a terminal and type it.` });
|
|
914
|
+
const answer = await api("PUT", path, { secret_value: secretValue });
|
|
915
|
+
if (isJson) return printJson(answer);
|
|
916
|
+
console.log(`Set ${values.secret_name} for ${values.slug}. Its game server reads it as server.secrets.${values.secret_name}; running rooms get it within a minute.`);
|
|
917
|
+
if (answer.next_steps.some((step) => step.tool === "update_game")) console.log(`It calls no other servers yet: list the hosts it uses this with, like zaisma allowed-hosts ${values.slug} discord.com`);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** A secret's value: piped in (without the line break at its end), or typed in a terminal without showing it. */
|
|
921
|
+
async function readSecretValue(secretName) {
|
|
922
|
+
if (!process.stdin.isTTY) {
|
|
923
|
+
const chunks = [];
|
|
924
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
925
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
926
|
+
}
|
|
927
|
+
process.stderr.write(`${secretName} (it isn't shown; Enter when done): `);
|
|
928
|
+
process.stdin.setRawMode(true);
|
|
929
|
+
process.stdin.setEncoding("utf8");
|
|
930
|
+
let typed = "";
|
|
931
|
+
try {
|
|
932
|
+
for await (const chunk of process.stdin) {
|
|
933
|
+
for (const character of chunk) {
|
|
934
|
+
if (character === "\r" || character === "\n") return typed;
|
|
935
|
+
if (character === "\u0003") process.exit(130);
|
|
936
|
+
if (character === "\u007f" || character === "\b") typed = typed.slice(0, -1);
|
|
937
|
+
else typed += character;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return typed;
|
|
941
|
+
} finally {
|
|
942
|
+
process.stdin.setRawMode(false);
|
|
943
|
+
process.stdin.pause();
|
|
944
|
+
process.stderr.write("\n");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
841
948
|
/** A game's cloud saves (…/games/<slug>/saves…), as the MCP's list_saves, get_save, change_save, replace_save, reset_save, list_save_copies, get_save_copy and restore_save_copy. */
|
|
842
949
|
async function saves(command, values, flags) {
|
|
843
950
|
const savePath = () => `/games/${values.slug}/saves/${encodeURIComponent(values.player_id)}`;
|
|
@@ -941,7 +1048,11 @@ async function run({ command, values, flags, globalFlags, rest }) {
|
|
|
941
1048
|
if (isJson) return printAnswer(answer);
|
|
942
1049
|
const { games, unread_topic_count: unreadTopicCount } = answer;
|
|
943
1050
|
if (!games.length) console.log('No games yet. Make one with: zaisma new "My Game"');
|
|
944
|
-
for (const game of games)
|
|
1051
|
+
for (const game of games) {
|
|
1052
|
+
console.log(`${game.slug.padEnd(28)} ${game.visibility.padEnd(9)} ${game.review_status.padEnd(8)} ${game.title}${game.review_reason ? ` [${reasonCode(game)}]` : ""}`);
|
|
1053
|
+
// What needs attention in it, most urgent first.
|
|
1054
|
+
for (const alert of game.alerts ?? []) console.log(` ! ${alert.message}`);
|
|
1055
|
+
}
|
|
945
1056
|
if (unreadTopicCount) console.log(`\nZaisma replied to ${unreadTopicCount} of your forum topics. See them with: zaisma forum`);
|
|
946
1057
|
return EXIT_CODES.done;
|
|
947
1058
|
}
|
|
@@ -987,7 +1098,10 @@ async function run({ command, values, flags, globalFlags, rest }) {
|
|
|
987
1098
|
console.log(`${answer.game.title} is now ${answer.game.visibility}.`);
|
|
988
1099
|
return EXIT_CODES.done;
|
|
989
1100
|
}
|
|
990
|
-
if (command === "
|
|
1101
|
+
if (command === "allowed-hosts") await allowedHosts(values, flags);
|
|
1102
|
+
else if (command === "links" || command.startsWith("links ")) await links(command, values, flags);
|
|
1103
|
+
else if (command === "secrets" || command.startsWith("secrets ")) await secrets(command, values);
|
|
1104
|
+
else if (command === "database" || command.startsWith("database ")) await database(command, values, flags);
|
|
991
1105
|
else if (command === "saves" || command.startsWith("saves ")) await saves(command, values, flags);
|
|
992
1106
|
else await forum(command, values, flags);
|
|
993
1107
|
return EXIT_CODES.done;
|