@zaisma/cli 0.1.0 → 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 +4 -2
- package/package.json +1 -1
- package/zaisma.mjs +134 -33
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ Run it with `npx @zaisma/cli <command>`, or install it once with `npm install -g
|
|
|
6
6
|
|
|
7
7
|
1. Make an API key at https://zaisma.com/account, under Game developers, and set it: `export ZAISMA_KEY=zsk_…`
|
|
8
8
|
2. Make a game: `zaisma new "Sky Hopper"`
|
|
9
|
-
3. Upload your build folder (the one with `index.html`): `zaisma upload ./build --game sky-hopper --notes "First version"`. Only new or changed files are sent. Once the scan passes you get a private preview link.
|
|
9
|
+
3. Upload your build folder (the one with `index.html`): `zaisma upload ./build --game sky-hopper --patch-notes "First version"`. Only new or changed files are sent. Once the scan passes you get a private preview link.
|
|
10
10
|
4. Publish it: `zaisma publish sky-hopper <version>`, then show it to everyone: `zaisma show sky-hopper --visibility listed`
|
|
11
11
|
|
|
12
|
-
`zaisma versions <slug>` lists versions and their preview links; `zaisma publish` with an older version rolls back, and `--at "2026-11-01 17:00"` publishes later (`zaisma unschedule` cancels). Files over 95 MB are sent in parts automatically, up to 500 MB each.
|
|
12
|
+
`zaisma versions <slug>` lists versions and their preview links; `zaisma publish` with an older version rolls back, and `--scheduled-publish-at "2026-11-01 17:00"` publishes later (`zaisma unschedule` cancels). Files over 95 MB are sent in parts automatically, up to 500 MB each.
|
|
13
|
+
|
|
14
|
+
Found a bug in Zaisma, or missing something? Search the developer forum first with `zaisma forum --search "too_big"`. `zaisma forum post --kind bug --title "…" --category mcp --text "…"` starts a public topic, and `zaisma forum message …` sends Zaisma a private one (logs, an unreleased game). `zaisma forum` lists your topics and messages, `zaisma forum <topic id>` shows one with its replies, and `zaisma forum reply <topic id> --text "…"` replies (`--reopen` if a fixed one is back). Posts say an AI wrote them unless you add `--by-me`. Everything here uses the [developer API](https://github.com/zaisma/zaisma/blob/main/docs/DEVELOPER-API.md).
|
package/package.json
CHANGED
package/zaisma.mjs
CHANGED
|
@@ -17,13 +17,24 @@ const HELP = `Usage:
|
|
|
17
17
|
zaisma games List your games
|
|
18
18
|
zaisma new <title> [--slug link-name] Make a game (it starts as a draft)
|
|
19
19
|
zaisma upload <folder> --game <slug> Upload a version; only changed files are sent
|
|
20
|
-
[--notes "What changed"]
|
|
21
|
-
[--publish]
|
|
20
|
+
[--patch-notes "What changed"] Patch notes, shown with the update
|
|
21
|
+
[--publish-when-approved] Go live as soon as it passes review
|
|
22
22
|
zaisma versions <slug> List versions and preview links
|
|
23
23
|
zaisma publish <slug> <version> Publish a draft, or roll back to an older version
|
|
24
|
-
[--at "2026-11-01 17:00"]
|
|
24
|
+
[--scheduled-publish-at "2026-11-01 17:00"] Publish later, at this time (your computer's time zone)
|
|
25
25
|
zaisma unschedule <slug> <version> Cancel a publish-later
|
|
26
26
|
zaisma show <slug> [--visibility listed|unlisted|private|draft]
|
|
27
|
+
zaisma forum [--search "words"] Your forum topics and messages to Zaisma, or search the forum
|
|
28
|
+
zaisma forum <topic id> A topic with every post you can see
|
|
29
|
+
zaisma forum post --kind bug|gap|feature|question|talk --title "…" --category <category> --text "…"
|
|
30
|
+
Start a public topic in the developer forum
|
|
31
|
+
zaisma forum message --kind bug|gap|feature|question --title "…" --text "…" [--category <category>] [--game <slug>]
|
|
32
|
+
A private message: only you and Zaisma see it
|
|
33
|
+
zaisma forum reply <topic id> --text "…" [--reopen]
|
|
34
|
+
Reply, or reopen your topic when something fixed is back
|
|
35
|
+
[--by-me] Mark a post as written by you; otherwise it says your AI wrote it
|
|
36
|
+
Categories: publishing, scans, pages, rooms, servers, passes, sdk, saves, stats, mcp, api, cli,
|
|
37
|
+
accounts, dashboard, reports, rules, docs, other. Markdown works in --text, or pipe the text in.
|
|
27
38
|
|
|
28
39
|
Set ZAISMA_KEY to an API key from https://zaisma.com/account (under Game developers).
|
|
29
40
|
Without installing: npx @zaisma/cli <command>. Installed (npm i -g @zaisma/cli): zaisma <command>.`;
|
|
@@ -50,7 +61,7 @@ async function api(method, path, body) {
|
|
|
50
61
|
if (!KEY) fail("set ZAISMA_KEY to an API key from https://zaisma.com/account first.");
|
|
51
62
|
const res = await fetch(`${API}/api/dev${path}`, {
|
|
52
63
|
method,
|
|
53
|
-
headers: { authorization: `Bearer ${KEY}`, ...(body && !(body instanceof Uint8Array) ? { "content-type": "application/json" } : {}) },
|
|
64
|
+
headers: { authorization: `Bearer ${KEY}`, "x-zaisma-client": "cli", ...(body && !(body instanceof Uint8Array) ? { "content-type": "application/json" } : {}) },
|
|
54
65
|
body: body instanceof Uint8Array ? body : body ? JSON.stringify(body) : undefined,
|
|
55
66
|
});
|
|
56
67
|
const data = await res.json().catch(() => ({}));
|
|
@@ -86,22 +97,22 @@ function sha256(file) {
|
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
async function sendInParts(f, progress) {
|
|
89
|
-
const start = await api("POST", `/files/${f.sha256}/parts`, {
|
|
90
|
-
if (start.
|
|
100
|
+
const start = await api("POST", `/files/${f.sha256}/parts`, { size_bytes: f.size_bytes });
|
|
101
|
+
if (start.storage_status === "already_stored") return;
|
|
91
102
|
const file = await open(f.full);
|
|
92
103
|
const parts = [];
|
|
93
104
|
try {
|
|
94
|
-
for (let n = 1, offset = 0; offset < f.
|
|
95
|
-
const chunk = new Uint8Array(Math.min(start.
|
|
105
|
+
for (let n = 1, offset = 0; offset < f.size_bytes; n++, offset += start.part_size_bytes) {
|
|
106
|
+
const chunk = new Uint8Array(Math.min(start.part_size_bytes, f.size_bytes - offset));
|
|
96
107
|
await file.read(chunk, 0, chunk.length, offset);
|
|
97
|
-
const part = await api("PUT", `/files/${f.sha256}/parts/${n}?
|
|
98
|
-
parts.push({
|
|
108
|
+
const part = await api("PUT", `/files/${f.sha256}/parts/${n}?upload_id=${encodeURIComponent(start.upload_id)}`, chunk);
|
|
109
|
+
parts.push({ part_number: part.part_number, etag: part.etag });
|
|
99
110
|
progress(offset + chunk.length);
|
|
100
111
|
}
|
|
101
112
|
} finally {
|
|
102
113
|
await file.close();
|
|
103
114
|
}
|
|
104
|
-
await api("POST", `/files/${f.sha256}/parts/done`, {
|
|
115
|
+
await api("POST", `/files/${f.sha256}/parts/done`, { upload_id: start.upload_id, parts });
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
const size = (n) => (n >= 1048576 ? `${(n / 1048576).toFixed(1)} MB` : `${Math.ceil(n / 1024)} KB`);
|
|
@@ -116,12 +127,12 @@ async function upload(folder, opts) {
|
|
|
116
127
|
console.log(`Checking ${paths.length} files…`);
|
|
117
128
|
const files = [];
|
|
118
129
|
for (const full of paths) {
|
|
119
|
-
files.push({ path: relative(folder, full).split(sep).join("/"), sha256: await sha256(full),
|
|
130
|
+
files.push({ path: relative(folder, full).split(sep).join("/"), sha256: await sha256(full), size_bytes: (await stat(full)).size, full });
|
|
120
131
|
}
|
|
121
132
|
const list = files.map(({ full: _, ...f }) => f);
|
|
122
133
|
const plan = await api("POST", `/games/${slug}/uploads`, { files: list });
|
|
123
|
-
const todo = files.filter((f, i) => plan.
|
|
124
|
-
const total = todo.reduce((n, f) => n + f.
|
|
134
|
+
const todo = files.filter((f, i) => plan.missing_file_hashes.includes(f.sha256) && files.findIndex((g) => g.sha256 === f.sha256) === i);
|
|
135
|
+
const total = todo.reduce((n, f) => n + f.size_bytes, 0);
|
|
125
136
|
console.log(todo.length ? `Sending ${todo.length} new or changed files (${size(total)})…` : "Nothing changed since your last upload; making a version from the stored files.");
|
|
126
137
|
|
|
127
138
|
let sent = 0;
|
|
@@ -129,75 +140,163 @@ async function upload(folder, opts) {
|
|
|
129
140
|
await Promise.all(
|
|
130
141
|
Array.from({ length: 4 }, async () => {
|
|
131
142
|
for (let f = queue.shift(); f; f = queue.shift()) {
|
|
132
|
-
if (f.
|
|
143
|
+
if (f.size_bytes > DIRECT_BYTES) await sendInParts(f, (n) => process.stdout.write(`\r ${size(sent + n)} of ${size(total)} `));
|
|
133
144
|
else await api("PUT", `/files/${f.sha256}`, new Uint8Array(await readFile(f.full)));
|
|
134
|
-
sent += f.
|
|
145
|
+
sent += f.size_bytes;
|
|
135
146
|
process.stdout.write(`\r ${size(sent)} of ${size(total)} `);
|
|
136
147
|
}
|
|
137
148
|
}),
|
|
138
149
|
);
|
|
139
150
|
if (todo.length) process.stdout.write("\n");
|
|
140
151
|
|
|
141
|
-
const { version } = await api("POST", `/games/${slug}/versions`, { files: list,
|
|
152
|
+
const { version } = await api("POST", `/games/${slug}/versions`, { files: list, patch_notes: typeof opts["patch-notes"] === "string" ? opts["patch-notes"] : "", publish_when_approved: opts["publish-when-approved"] === true });
|
|
142
153
|
console.log(`Version ${version.id} is being scanned. This usually takes under a minute…`);
|
|
143
154
|
|
|
144
155
|
for (let i = 0; i < 200; i++) {
|
|
145
156
|
await sleep(3000);
|
|
146
157
|
const { version: v } = await api("GET", `/games/${slug}/versions/${version.id}`);
|
|
147
|
-
if (v.
|
|
148
|
-
if (v.
|
|
149
|
-
if (v.
|
|
158
|
+
if (v.scan_status === "scanning") continue;
|
|
159
|
+
if (v.scan_status === "approved") {
|
|
160
|
+
if (v.is_live) console.log(`Passed and live: ${API}/g/${slug}`);
|
|
150
161
|
else {
|
|
151
162
|
console.log(`Passed. Your private preview: ${v.preview_url}`);
|
|
152
163
|
console.log(`Publish it with: zaisma publish ${slug} ${v.id}`);
|
|
153
164
|
}
|
|
154
165
|
} else {
|
|
155
|
-
console.log(v.
|
|
156
|
-
for (const f of v.findings
|
|
157
|
-
if (v.
|
|
166
|
+
console.log(v.scan_status === "held" ? "Held: a person will look at it, usually within a day. What the scan found:" : "Rejected. What the scan found:");
|
|
167
|
+
for (const f of v.findings) console.log(` - ${f.explanation}${f.path ? ` (${f.path})` : ""}`);
|
|
168
|
+
if (v.scan_status === "rejected") process.exit(1);
|
|
158
169
|
}
|
|
159
170
|
return;
|
|
160
171
|
}
|
|
161
172
|
console.log(`Still scanning. Check later with: zaisma versions ${slug}`);
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
const day = (at) => at.slice(0, 10);
|
|
176
|
+
|
|
177
|
+
function printTopic(topic) {
|
|
178
|
+
const count = topic.is_known_issue ? `, ${topic.affected_developer_count} developer${topic.affected_developer_count === 1 ? "" : "s"} affected` : topic.vote_count ? `, ${topic.vote_count} vote${topic.vote_count === 1 ? "" : "s"}` : "";
|
|
179
|
+
console.log(`${topic.id} ${topic.topic_status.padEnd(13)} ${topic.is_unread ? "* " : " "}${topic.title} (${topic.is_known_issue ? "known issue" : topic.kind}${count})`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Text from --text, or piped in. */
|
|
183
|
+
async function readText(opts) {
|
|
184
|
+
if (typeof opts.text === "string") return opts.text;
|
|
185
|
+
if (process.stdin.isTTY) return "";
|
|
186
|
+
const chunks = [];
|
|
187
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
188
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The developer forum. Posts say an AI wrote them unless --by-me says the developer did. */
|
|
192
|
+
async function forum(opts) {
|
|
193
|
+
const [sub, id] = opts._;
|
|
194
|
+
const by = opts["by-me"] ? { author_kind: "developer" } : {};
|
|
195
|
+
const sent = (out) => {
|
|
196
|
+
if (out.check_note) console.log(out.check_note);
|
|
197
|
+
if (out.removed_secret_count) console.log(`${out.removed_secret_count} secret${out.removed_secret_count === 1 ? " was" : "s were"} blanked before it was stored.`);
|
|
198
|
+
};
|
|
199
|
+
if (sub === "post" || sub === "message") {
|
|
200
|
+
const text = await readText(opts);
|
|
201
|
+
const isMessage = sub === "message";
|
|
202
|
+
const usage = isMessage
|
|
203
|
+
? 'usage: zaisma forum message --kind bug|gap|feature|question --title "…" --text "…" [--category <category>] [--game <slug>]'
|
|
204
|
+
: 'usage: zaisma forum post --kind bug|gap|feature|question|talk --title "…" --category <category> --text "…"';
|
|
205
|
+
if (typeof opts.kind !== "string" || typeof opts.title !== "string" || !text || (!isMessage && typeof opts.category !== "string")) fail(usage);
|
|
206
|
+
const out = await api("POST", "/forum", {
|
|
207
|
+
kind: opts.kind,
|
|
208
|
+
title: opts.title,
|
|
209
|
+
text_markdown: text,
|
|
210
|
+
...(typeof opts.category === "string" ? { category: opts.category } : {}),
|
|
211
|
+
...(isMessage ? { is_public: false, ...(typeof opts.game === "string" ? { game_slug: opts.game } : {}) } : {}),
|
|
212
|
+
...by,
|
|
213
|
+
});
|
|
214
|
+
console.log(`${isMessage ? "Sent to Zaisma" : "Posted"} (${out.topic.id}). Replies show here: zaisma forum ${out.topic.id}`);
|
|
215
|
+
sent(out);
|
|
216
|
+
if (out.similar_topics.length) {
|
|
217
|
+
console.log("\nTopics that look like the same thing (vote or reply there if one is):");
|
|
218
|
+
for (const topic of out.similar_topics) printTopic(topic);
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (sub === "reply") {
|
|
223
|
+
const text = await readText(opts);
|
|
224
|
+
if (!id || !text) fail('usage: zaisma forum reply <topic id> --text "…" [--reopen]');
|
|
225
|
+
const out = await api("POST", `/forum/${id}/posts`, { text_markdown: text, ...(opts.reopen ? { reopen: true } : {}), ...by });
|
|
226
|
+
console.log(`Replied in ${id}.`);
|
|
227
|
+
sent(out);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (sub) {
|
|
231
|
+
const { topic, posts } = await api("GET", `/forum/${sub}`);
|
|
232
|
+
console.log(`${topic.title}\n${topic.is_known_issue ? "known issue" : topic.kind}, ${topic.topic_status}${topic.is_public ? "" : ", a message to Zaisma"}${topic.game_slug ? `, ${topic.game_slug}` : ""}`);
|
|
233
|
+
if (topic.workaround) console.log(`Until it's fixed: ${topic.workaround}`);
|
|
234
|
+
if (topic.merged_into_topic) console.log(`Part of the known issue ${topic.merged_into_topic.id}: ${topic.merged_into_topic.title}`);
|
|
235
|
+
const names = { developer: "", ai: "'s AI", zaisma: "" };
|
|
236
|
+
for (const post of posts) {
|
|
237
|
+
const waiting = post.check_status === "pending" ? " (being checked)" : post.check_status === "held" ? " (waiting for a person at Zaisma)" : "";
|
|
238
|
+
console.log(`\n${day(post.created_at)} ${post.author.name}${names[post.author.kind]}${post.kind === "reopen" ? " (reopened)" : ""}${post.is_answer ? " (the answer)" : ""}${waiting}:\n${post.text_markdown}`);
|
|
239
|
+
for (const image of post.media) console.log(`[image] ${image.image_url}`);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (typeof opts.search === "string") {
|
|
244
|
+
const { topics } = await api("GET", `/forum?detail=brief&search_text=${encodeURIComponent(opts.search)}`);
|
|
245
|
+
if (!topics.length) console.log("Nothing matches that. Post it with: zaisma forum post");
|
|
246
|
+
for (const topic of topics) printTopic(topic);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const [yours, messages] = await Promise.all([api("GET", "/forum?detail=brief&tab=yours"), api("GET", "/forum?detail=brief&tab=messages")]);
|
|
250
|
+
console.log(yours.topics.length ? "Your topics (* has something new):" : "You haven't posted in the forum yet.");
|
|
251
|
+
for (const topic of yours.topics) printTopic(topic);
|
|
252
|
+
if (messages.topics.length) {
|
|
253
|
+
console.log("\nYour messages to Zaisma:");
|
|
254
|
+
for (const topic of messages.topics) printTopic(topic);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
164
258
|
async function main() {
|
|
165
259
|
const [command, ...rest] = process.argv.slice(2);
|
|
166
260
|
const opts = args(rest);
|
|
167
261
|
if (!command || command === "help" || command === "--help" || command === "-h") return console.log(HELP);
|
|
262
|
+
// Flags renamed to match the API's names (docs/NAMING.md). Said out loud, so an old one isn't quietly ignored.
|
|
263
|
+
const renamed = { notes: "patch-notes", publish: "publish-when-approved", at: "scheduled-publish-at" };
|
|
264
|
+
for (const [old, now] of Object.entries(renamed)) if (old in opts) fail(`--${old} is now --${now}.`);
|
|
168
265
|
|
|
169
266
|
if (command === "games") {
|
|
170
|
-
const { games } = await api("GET", "/games");
|
|
267
|
+
const { games, unread_topic_count } = await api("GET", "/games");
|
|
171
268
|
if (!games.length) return console.log('No games yet. Make one with: zaisma new "My Game"');
|
|
172
|
-
for (const g of games) console.log(`${g.slug.padEnd(28)} ${g.visibility.padEnd(9)} ${g.
|
|
269
|
+
for (const g of games) console.log(`${g.slug.padEnd(28)} ${g.visibility.padEnd(9)} ${g.review_status.padEnd(8)} ${g.title}`);
|
|
270
|
+
if (unread_topic_count) console.log(`\nZaisma replied to ${unread_topic_count} of your forum topics. See them with: zaisma forum`);
|
|
173
271
|
return;
|
|
174
272
|
}
|
|
175
273
|
if (command === "new") {
|
|
176
274
|
const title = opts._.join(" ");
|
|
177
275
|
if (!title) fail('usage: zaisma new "My Game" [--slug my-game]');
|
|
178
276
|
const { game } = await api("POST", "/games", { title, ...(typeof opts.slug === "string" ? { slug: opts.slug } : {}) });
|
|
179
|
-
return console.log(`Made ${game.
|
|
277
|
+
return console.log(`Made ${game.page_url} (a draft). Upload it with: zaisma upload <folder> --game ${game.slug}`);
|
|
180
278
|
}
|
|
181
279
|
if (command === "upload") return upload(opts._[0], opts);
|
|
182
280
|
if (command === "versions") {
|
|
183
281
|
const slug = opts._[0];
|
|
184
282
|
if (!slug) fail("usage: zaisma versions <slug>");
|
|
185
283
|
const { versions } = await api("GET", `/games/${slug}`);
|
|
186
|
-
for (const v of versions) console.log(`${v.id} ${
|
|
284
|
+
for (const v of versions) console.log(`${v.id} ${v.created_at.slice(0, 16).replace("T", " ")} ${(v.is_live ? "live" : v.scan_status).padEnd(9)} ${v.preview_url ?? ""}${v.scheduled_publish_at ? ` goes live ${new Date(v.scheduled_publish_at).toLocaleString()}` : ""}`);
|
|
187
285
|
return;
|
|
188
286
|
}
|
|
189
287
|
if (command === "publish") {
|
|
190
288
|
const [slug, id] = opts._;
|
|
191
289
|
if (!slug || !id) fail("usage: zaisma publish <slug> <version>");
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
290
|
+
const later = opts["scheduled-publish-at"];
|
|
291
|
+
if (typeof later === "string") {
|
|
292
|
+
const at = new Date(later.includes("T") || /z$|[+-]\d\d:?\d\d$/i.test(later) ? later : later.replace(" ", "T"));
|
|
293
|
+
if (Number.isNaN(at.getTime())) fail('give --scheduled-publish-at as a date and time, like "2026-11-01 17:00".');
|
|
294
|
+
const { version } = await api("POST", `/games/${slug}/versions/${id}/publish`, { scheduled_publish_at: at.toISOString() });
|
|
295
|
+
console.log(`Version ${id} will go live at ${new Date(version.scheduled_publish_at).toLocaleString()}${version.scan_status === "scanning" ? ", if it has passed the scan by then" : ""}.`);
|
|
197
296
|
return;
|
|
198
297
|
}
|
|
199
298
|
const { game } = await api("POST", `/games/${slug}/versions/${id}/publish`);
|
|
200
|
-
console.log(`Version ${id} is live at ${game.
|
|
299
|
+
console.log(`Version ${id} is live at ${game.page_url}`);
|
|
201
300
|
if (game.visibility === "draft") console.log(`The game is still a draft, so only you can see it. Show it with: zaisma show ${slug} --visibility listed`);
|
|
202
301
|
return;
|
|
203
302
|
}
|
|
@@ -213,6 +312,8 @@ async function main() {
|
|
|
213
312
|
const { game } = await api("PATCH", `/games/${slug}`, { visibility: opts.visibility });
|
|
214
313
|
return console.log(`${game.title} is now ${game.visibility}.`);
|
|
215
314
|
}
|
|
315
|
+
if (command === "forum") return forum(opts);
|
|
316
|
+
if (command === "feedback") fail("zaisma feedback is now zaisma forum: zaisma forum lists your topics, zaisma forum message sends Zaisma a private message.");
|
|
216
317
|
fail(`unknown command "${command}". Run zaisma help.`);
|
|
217
318
|
}
|
|
218
319
|
|