@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.
- package/package.json +1 -1
- package/zaisma.mjs +68 -2
package/package.json
CHANGED
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
|
|
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);
|