@zaisma/cli 0.13.0 → 0.15.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 +68 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaisma/cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.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
@@ -59,6 +59,7 @@ const COMMAND_ERROR_CODES = {
59
59
  repeated_field: "A flag was given twice.",
60
60
  missing_value: "A flag that takes a value was given none.",
61
61
  takes_no_value: "A flag that takes no value was given one.",
62
+ conflicting_flags: "Two flags that don't go together were given, like logs --follow and --cursor.",
62
63
  not_a_folder: "The folder to check or upload isn't a folder.",
63
64
  not_a_file: "The file given (of statements, a dump to import, or a save) can't be read.",
64
65
  file_exists: "database export won't write over a file that's already there.",
@@ -126,6 +127,19 @@ const COMMANDS = {
126
127
  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: {} },
127
128
  "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: {} },
128
129
  "secrets delete": { about: "Delete one of a game's secrets", arguments: [{ name: "slug" }, { name: "secret_name" }], flags: {} },
130
+ logs: {
131
+ about: "What a game's server printed on Zaisma in the last 30 days, oldest first: the newest lines, older ones with --cursor, or new ones as they come with --follow",
132
+ arguments: [{ name: "slug" }],
133
+ flags: {
134
+ follow: { hasValue: false, about: "Keep printing new lines as the game server prints them, every 2 seconds, until Ctrl-C" },
135
+ "room-code": { hasValue: true, placeholder: "<code>", about: "Only lines printed in this room, like K7Q2MX" },
136
+ "minimum-level": { hasValue: true, choices: ["debug", "info", "warn", "error"], about: "debug (everything), info (all but debug), warn (warnings and errors) or error" },
137
+ "source-kind": { hasValue: true, choices: ["room", "request", "schedule", "webhook"], about: "Only lines printed in a room, onRequest, onSchedule or onWebhook" },
138
+ "search-text": { hasValue: true, placeholder: '"words"', about: "Only lines containing this, ignoring case" },
139
+ "maximum-line-count": { hasValue: true, placeholder: "<count>", about: "How many lines at most, up to 500; 100 by default" },
140
+ cursor: { hasValue: true, placeholder: "<next_cursor>", about: "The lines before an earlier answer's: the next_cursor it printed" },
141
+ },
142
+ },
129
143
  forum: {
130
144
  about: "Your forum topics and messages to Zaisma; one topic with every post you can see; or a search",
131
145
  arguments: [{ name: "topic_id", isOptional: true }],
@@ -180,6 +194,12 @@ const COMMANDS = {
180
194
  arguments: [{ name: "slug" }, { name: "file", isOptional: true }],
181
195
  flags: {},
182
196
  },
197
+ "database restore": {
198
+ about: "Put a game's database back as it was at a moment in the last 30 days (an ISO time, like 2026-10-18T14:05:00Z), live; undo it with database undo-restore",
199
+ arguments: [{ name: "slug" }, { name: "restore_to_at" }],
200
+ flags: {},
201
+ },
202
+ "database undo-restore": { about: "Undo a restore of a game's database: back to just before it, live", arguments: [{ name: "slug" }, { name: "database_restore_id" }], flags: {} },
183
203
  saves: {
184
204
  about: "The players with a cloud save in a game, by player ID, 100 a page",
185
205
  arguments: [{ name: "slug" }],
@@ -203,7 +223,7 @@ const COMMANDS = {
203
223
  help: { about: "This help; with --json, every command, flag, exit code and error code", arguments: [], flags: {} },
204
224
  };
205
225
  /** Commands with a second word, like zaisma forum post. */
206
- 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"] };
226
+ const SUBCOMMANDS = { links: ["set", "remove"], forum: ["post", "message", "reply"], database: ["query", "batch", "export", "import", "restore", "undo-restore"], saves: ["get", "change", "replace", "reset", "copies", "copy", "restore"], secrets: ["set", "delete"] };
207
227
  /** Flags every command takes. */
208
228
  const GLOBAL_FLAGS = {
209
229
  json: { hasValue: false, about: "Print the API's answer as JSON, as-is (on failure its { error }), and nothing else" },
@@ -891,10 +911,22 @@ async function readInput() {
891
911
  return Buffer.concat(chunks).toString("utf8").trim();
892
912
  }
893
913
 
894
- /** A game's own database (GET and POST /games/<slug>/database…), as the MCP's get_database, query_database, batch_database, export_database and import_database. */
914
+ /** A game's own database (GET and POST /games/<slug>/database…), as the MCP's get_database, query_database, batch_database, export_database, import_database, restore_database and undo_database_restore. */
895
915
  async function database(command, values, flags) {
896
916
  if (command === "database export") return exportDatabase(values);
897
917
  if (command === "database import") return importDatabase(values);
918
+ if (command === "database restore" || command === "database undo-restore") {
919
+ const isUndo = command === "database undo-restore";
920
+ const answer = isUndo
921
+ ? await api("POST", `/games/${values.slug}/database/restores/${encodeURIComponent(values.database_restore_id)}/undo`)
922
+ : await api("POST", `/games/${values.slug}/database/restores`, { restore_to_at: values.restore_to_at });
923
+ if (isJson) return printJson(answer);
924
+ const restore = answer.database_restore;
925
+ console.log(isUndo ? `Undid restore ${values.database_restore_id}: ${values.slug}'s database is back to ${restore.restore_to_at}.` : `Restored ${values.slug}'s database to ${restore.restore_to_at}.`);
926
+ if (answer.erased_again_player_count) console.log(`Erased ${answer.erased_again_player_count} player${answer.erased_again_player_count === 1 ? "" : "s"} again who had been erased since.`);
927
+ console.log(`Undo it until ${restore.undo_expires_at}: zaisma database undo-restore ${values.slug} ${restore.id}`);
928
+ return;
929
+ }
898
930
  if (command === "database") {
899
931
  const answer = await api("GET", `/games/${values.slug}/database`);
900
932
  if (isJson) return printJson(answer);
@@ -908,6 +940,10 @@ async function database(command, values, flags) {
908
940
  `${use.written_row_count.toLocaleString("en-US")} written (${percent(use.written_row_count, use.free_written_row_count)} of ${use.free_written_row_count.toLocaleString("en-US")}), ` +
909
941
  `${(use.storage_size_bytes / 1024 / 1024).toFixed(1)} MB stored (${percent(use.storage_size_bytes, use.free_storage_size_bytes)} of ${use.free_storage_size_bytes / 1024 / 1024} MB). Resets ${use.reset_date}.`,
910
942
  );
943
+ if (answer.restores.length) {
944
+ console.log("\nRestores (newest first; undo with zaisma database undo-restore <slug> <id>):");
945
+ for (const restore of answer.restores.slice(0, 5)) console.log(`${restore.id} ${restore.restored_at} ${restore.undoes_database_restore_id ? `undid ${restore.undoes_database_restore_id}` : `back to ${restore.restore_to_at}`}`);
946
+ }
911
947
  return;
912
948
  }
913
949
  if (command === "database query") {
@@ -1218,6 +1254,35 @@ async function secrets(command, values) {
1218
1254
  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`);
1219
1255
  }
1220
1256
 
1257
+ /** A game server's logs (GET …/games/<slug>/logs), as the MCP's get_logs; --follow asks for new lines every 2 seconds. */
1258
+ async function logs(values, flags) {
1259
+ if (flags.follow && flags.cursor) refuse("conflicting_flags", { message: "--follow prints new lines, and --cursor older ones.", field: "cursor", fix: `Use one: zaisma logs ${values.slug} --follow, or zaisma logs ${values.slug} --cursor <next_cursor>.` });
1260
+ const filters = {};
1261
+ for (const [flag, input] of [["room-code", "room_code"], ["minimum-level", "minimum_level"], ["source-kind", "source_kind"], ["search-text", "search_text"], ["maximum-line-count", "maximum_line_count"], ["cursor", "cursor"]]) if (flags[flag] !== undefined) filters[input] = flags[flag];
1262
+ const ask = (more) => api("GET", `/games/${values.slug}/logs?${new URLSearchParams({ ...filters, ...more }).toString()}`);
1263
+ const print = (answer) => {
1264
+ if (isJson) {
1265
+ // Followed, each answer is one line of JSON.
1266
+ if (flags.follow) process.stdout.write(`${JSON.stringify(answer)}\n`);
1267
+ else printJson(answer);
1268
+ return;
1269
+ }
1270
+ for (const line of answer.log_lines) console.log(`${line.logged_at.slice(11, 23)} ${line.level.padEnd(5)} ${line.room_code ?? line.source_kind} ${line.text}`);
1271
+ };
1272
+ let answer = await ask({});
1273
+ print(answer);
1274
+ if (!flags.follow) {
1275
+ if (!isJson && !answer.log_lines.length) console.log(`No lines from ${values.slug}'s game server${Object.keys(filters).length ? " that match" : ""} in the last 30 days.`);
1276
+ if (!isJson && answer.next_cursor) console.log(`Older lines: zaisma logs ${values.slug} --cursor ${answer.next_cursor}`);
1277
+ return;
1278
+ }
1279
+ for (;;) {
1280
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1281
+ answer = await ask({ new_lines_cursor: answer.new_lines_cursor });
1282
+ print(answer);
1283
+ }
1284
+ }
1285
+
1221
1286
  /** A secret's value: piped in (without the line break at its end), or typed in a terminal without showing it. */
1222
1287
  async function readSecretValue(secretName) {
1223
1288
  if (!process.stdin.isTTY) {
@@ -1404,6 +1469,7 @@ async function run({ command, values, flags, globalFlags, rest }) {
1404
1469
  else if (command === "own-servers") await ownServers(values, flags);
1405
1470
  else if (command === "links" || command.startsWith("links ")) await links(command, values, flags);
1406
1471
  else if (command === "secrets" || command.startsWith("secrets ")) await secrets(command, values);
1472
+ else if (command === "logs") await logs(values, flags);
1407
1473
  else if (command === "database" || command.startsWith("database ")) await database(command, values, flags);
1408
1474
  else if (command === "saves" || command.startsWith("saves ")) await saves(command, values, flags);
1409
1475
  else await forum(command, values, flags);