@zaisma/cli 0.7.0 → 0.8.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/zaisma.mjs +79 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaisma/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Upload and publish browser games on Zaisma from the command line.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://zaisma.com",
package/zaisma.mjs CHANGED
@@ -82,6 +82,14 @@ const COMMANDS = {
82
82
  },
83
83
  unschedule: { about: "Cancel a publish-later", arguments: [{ name: "slug" }, { name: "version_id" }], flags: {} },
84
84
  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: {} },
85
+ "allowed-hosts": {
86
+ about: "The hosts a game's server can call with fetch(), like discord.com or *.example.com; given hosts, set the whole list",
87
+ arguments: [{ name: "slug" }, { name: "hosts", isRest: true, isOptional: true }],
88
+ flags: { none: { hasValue: false, about: "Set the list to none: the game server calls no other servers" } },
89
+ },
90
+ 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: {} },
91
+ "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: {} },
92
+ "secrets delete": { about: "Delete one of a game's secrets", arguments: [{ name: "slug" }, { name: "secret_name" }], flags: {} },
85
93
  forum: {
86
94
  about: "Your forum topics and messages to Zaisma; one topic with every post you can see; or a search",
87
95
  arguments: [{ name: "topic_id", isOptional: true }],
@@ -159,7 +167,7 @@ const COMMANDS = {
159
167
  help: { about: "This help; with --json, every command, flag, exit code and error code", arguments: [], flags: {} },
160
168
  };
161
169
  /** 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"] };
170
+ const SUBCOMMANDS = { forum: ["post", "message", "reply"], database: ["query", "batch", "export", "import"], saves: ["get", "change", "replace", "reset", "copies", "copy", "restore"], secrets: ["set", "delete"] };
163
171
  /** Flags every command takes. */
164
172
  const GLOBAL_FLAGS = {
165
173
  json: { hasValue: false, about: "Print the API's answer as JSON, as-is (on failure its { error }), and nothing else" },
@@ -838,6 +846,68 @@ function* sqliteDumpStatements(sqlite) {
838
846
  yield "COMMIT;\n";
839
847
  }
840
848
 
849
+ /** The hosts a game's server can call (game_server_allowed_hosts on the game), as the MCP's get_game and update_game. */
850
+ async function allowedHosts(values, flags) {
851
+ const given = (values.hosts ?? "").split(/[\s,]+/).filter(Boolean);
852
+ 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.` });
853
+ const answer = given.length || flags.none ? await api("PATCH", `/games/${values.slug}`, { game_server_allowed_hosts: given }) : await api("GET", `/games/${values.slug}`);
854
+ if (isJson) return printJson(answer);
855
+ const hosts = answer.game.game_server_allowed_hosts;
856
+ 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`);
857
+ if (given.length || flags.none) console.log("It works within a minute.");
858
+ }
859
+
860
+ /** A game's secrets (…/games/<slug>/secrets…), as the MCP's list_secrets, set_secret and delete_secret. */
861
+ async function secrets(command, values) {
862
+ if (command === "secrets") {
863
+ const answer = await api("GET", `/games/${values.slug}/secrets`);
864
+ if (isJson) return printJson(answer);
865
+ if (!answer.secrets.length) console.log(`No secrets yet. Set one with: zaisma secrets set ${values.slug} DISCORD_WEBHOOK_URL`);
866
+ for (const secret of answer.secrets) console.log(`${secret.name.padEnd(32)} set ${when(secret.set_at)}`);
867
+ return;
868
+ }
869
+ const path = `/games/${values.slug}/secrets/${encodeURIComponent(values.secret_name)}`;
870
+ if (command === "secrets delete") {
871
+ const answer = await api("DELETE", path);
872
+ if (isJson) return printJson(answer);
873
+ return console.log(`Deleted ${values.secret_name} from ${values.slug}. Running rooms lose it within a minute.`);
874
+ }
875
+ const secretValue = await readSecretValue(values.secret_name);
876
+ 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.` });
877
+ const answer = await api("PUT", path, { secret_value: secretValue });
878
+ if (isJson) return printJson(answer);
879
+ 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.`);
880
+ 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`);
881
+ }
882
+
883
+ /** A secret's value: piped in (without the line break at its end), or typed in a terminal without showing it. */
884
+ async function readSecretValue(secretName) {
885
+ if (!process.stdin.isTTY) {
886
+ const chunks = [];
887
+ for await (const chunk of process.stdin) chunks.push(chunk);
888
+ return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
889
+ }
890
+ process.stderr.write(`${secretName} (it isn't shown; Enter when done): `);
891
+ process.stdin.setRawMode(true);
892
+ process.stdin.setEncoding("utf8");
893
+ let typed = "";
894
+ try {
895
+ for await (const chunk of process.stdin) {
896
+ for (const character of chunk) {
897
+ if (character === "\r" || character === "\n") return typed;
898
+ if (character === "\u0003") process.exit(130);
899
+ if (character === "\u007f" || character === "\b") typed = typed.slice(0, -1);
900
+ else typed += character;
901
+ }
902
+ }
903
+ return typed;
904
+ } finally {
905
+ process.stdin.setRawMode(false);
906
+ process.stdin.pause();
907
+ process.stderr.write("\n");
908
+ }
909
+ }
910
+
841
911
  /** 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
912
  async function saves(command, values, flags) {
843
913
  const savePath = () => `/games/${values.slug}/saves/${encodeURIComponent(values.player_id)}`;
@@ -941,7 +1011,11 @@ async function run({ command, values, flags, globalFlags, rest }) {
941
1011
  if (isJson) return printAnswer(answer);
942
1012
  const { games, unread_topic_count: unreadTopicCount } = answer;
943
1013
  if (!games.length) console.log('No games yet. Make one with: zaisma new "My Game"');
944
- for (const game of games) console.log(`${game.slug.padEnd(28)} ${game.visibility.padEnd(9)} ${game.review_status.padEnd(8)} ${game.title}${game.review_reason ? ` [${reasonCode(game)}]` : ""}`);
1014
+ for (const game of games) {
1015
+ console.log(`${game.slug.padEnd(28)} ${game.visibility.padEnd(9)} ${game.review_status.padEnd(8)} ${game.title}${game.review_reason ? ` [${reasonCode(game)}]` : ""}`);
1016
+ // What needs attention in it, most urgent first.
1017
+ for (const alert of game.alerts ?? []) console.log(` ! ${alert.message}`);
1018
+ }
945
1019
  if (unreadTopicCount) console.log(`\nZaisma replied to ${unreadTopicCount} of your forum topics. See them with: zaisma forum`);
946
1020
  return EXIT_CODES.done;
947
1021
  }
@@ -987,7 +1061,9 @@ async function run({ command, values, flags, globalFlags, rest }) {
987
1061
  console.log(`${answer.game.title} is now ${answer.game.visibility}.`);
988
1062
  return EXIT_CODES.done;
989
1063
  }
990
- if (command === "database" || command.startsWith("database ")) await database(command, values, flags);
1064
+ if (command === "allowed-hosts") await allowedHosts(values, flags);
1065
+ else if (command === "secrets" || command.startsWith("secrets ")) await secrets(command, values);
1066
+ else if (command === "database" || command.startsWith("database ")) await database(command, values, flags);
991
1067
  else if (command === "saves" || command.startsWith("saves ")) await saves(command, values, flags);
992
1068
  else await forum(command, values, flags);
993
1069
  return EXIT_CODES.done;