@zaisma/cli 0.1.0 → 0.2.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 (3) hide show
  1. package/README.md +4 -2
  2. package/package.json +1 -1
  3. package/zaisma.mjs +104 -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. Everything here uses the [developer API](https://github.com/zaisma/zaisma/blob/main/docs/DEVELOPER-API.md).
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? `zaisma feedback send --kind bug --title "…" --text "…"` tells us, and answers with known issues that match. `zaisma feedback` lists yours and the known issues, `zaisma feedback <id>` shows one with our replies, and `zaisma feedback add <id> --text "…"` adds to it (`--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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaisma/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.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
@@ -17,13 +17,20 @@ 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"] Patch notes, shown with the update
21
- [--publish] Go live as soon as it passes review
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"] Publish later, at this time (your computer's time zone)
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 feedback [--search "words"] Your feedback to Zaisma, and the known issues
28
+ zaisma feedback <id> One item with every post, or a known issue
29
+ zaisma feedback send --kind bug|gap|feature|question --title "…" --text "…" [--game <slug>]
30
+ Tell Zaisma about a bug, a gap, an idea or a question
31
+ zaisma feedback add <id> --text "…" [--reopen]
32
+ Add to an item, reopen it, or add your case to a known issue
33
+ [--by-me] Mark a post as written by you; otherwise it says your AI wrote it
27
34
 
28
35
  Set ZAISMA_KEY to an API key from https://zaisma.com/account (under Game developers).
29
36
  Without installing: npx @zaisma/cli <command>. Installed (npm i -g @zaisma/cli): zaisma <command>.`;
@@ -50,7 +57,7 @@ async function api(method, path, body) {
50
57
  if (!KEY) fail("set ZAISMA_KEY to an API key from https://zaisma.com/account first.");
51
58
  const res = await fetch(`${API}/api/dev${path}`, {
52
59
  method,
53
- headers: { authorization: `Bearer ${KEY}`, ...(body && !(body instanceof Uint8Array) ? { "content-type": "application/json" } : {}) },
60
+ headers: { authorization: `Bearer ${KEY}`, "x-zaisma-client": "cli", ...(body && !(body instanceof Uint8Array) ? { "content-type": "application/json" } : {}) },
54
61
  body: body instanceof Uint8Array ? body : body ? JSON.stringify(body) : undefined,
55
62
  });
56
63
  const data = await res.json().catch(() => ({}));
@@ -86,22 +93,22 @@ function sha256(file) {
86
93
  }
87
94
 
88
95
  async function sendInParts(f, progress) {
89
- const start = await api("POST", `/files/${f.sha256}/parts`, { bytes: f.bytes });
90
- if (start.stored === "already") return;
96
+ const start = await api("POST", `/files/${f.sha256}/parts`, { size_bytes: f.size_bytes });
97
+ if (start.storage_status === "already_stored") return;
91
98
  const file = await open(f.full);
92
99
  const parts = [];
93
100
  try {
94
- for (let n = 1, offset = 0; offset < f.bytes; n++, offset += start.part_bytes) {
95
- const chunk = new Uint8Array(Math.min(start.part_bytes, f.bytes - offset));
101
+ for (let n = 1, offset = 0; offset < f.size_bytes; n++, offset += start.part_size_bytes) {
102
+ const chunk = new Uint8Array(Math.min(start.part_size_bytes, f.size_bytes - offset));
96
103
  await file.read(chunk, 0, chunk.length, offset);
97
- const part = await api("PUT", `/files/${f.sha256}/parts/${n}?upload=${encodeURIComponent(start.upload)}`, chunk);
98
- parts.push({ part: part.part, etag: part.etag });
104
+ const part = await api("PUT", `/files/${f.sha256}/parts/${n}?upload_id=${encodeURIComponent(start.upload_id)}`, chunk);
105
+ parts.push({ part_number: part.part_number, etag: part.etag });
99
106
  progress(offset + chunk.length);
100
107
  }
101
108
  } finally {
102
109
  await file.close();
103
110
  }
104
- await api("POST", `/files/${f.sha256}/parts/done`, { upload: start.upload, parts });
111
+ await api("POST", `/files/${f.sha256}/parts/done`, { upload_id: start.upload_id, parts });
105
112
  }
106
113
 
107
114
  const size = (n) => (n >= 1048576 ? `${(n / 1048576).toFixed(1)} MB` : `${Math.ceil(n / 1024)} KB`);
@@ -116,12 +123,12 @@ async function upload(folder, opts) {
116
123
  console.log(`Checking ${paths.length} files…`);
117
124
  const files = [];
118
125
  for (const full of paths) {
119
- files.push({ path: relative(folder, full).split(sep).join("/"), sha256: await sha256(full), bytes: (await stat(full)).size, full });
126
+ files.push({ path: relative(folder, full).split(sep).join("/"), sha256: await sha256(full), size_bytes: (await stat(full)).size, full });
120
127
  }
121
128
  const list = files.map(({ full: _, ...f }) => f);
122
129
  const plan = await api("POST", `/games/${slug}/uploads`, { files: list });
123
- const todo = files.filter((f, i) => plan.missing.includes(f.sha256) && files.findIndex((g) => g.sha256 === f.sha256) === i);
124
- const total = todo.reduce((n, f) => n + f.bytes, 0);
130
+ const todo = files.filter((f, i) => plan.missing_file_hashes.includes(f.sha256) && files.findIndex((g) => g.sha256 === f.sha256) === i);
131
+ const total = todo.reduce((n, f) => n + f.size_bytes, 0);
125
132
  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
133
 
127
134
  let sent = 0;
@@ -129,75 +136,138 @@ async function upload(folder, opts) {
129
136
  await Promise.all(
130
137
  Array.from({ length: 4 }, async () => {
131
138
  for (let f = queue.shift(); f; f = queue.shift()) {
132
- if (f.bytes > DIRECT_BYTES) await sendInParts(f, (n) => process.stdout.write(`\r ${size(sent + n)} of ${size(total)} `));
139
+ if (f.size_bytes > DIRECT_BYTES) await sendInParts(f, (n) => process.stdout.write(`\r ${size(sent + n)} of ${size(total)} `));
133
140
  else await api("PUT", `/files/${f.sha256}`, new Uint8Array(await readFile(f.full)));
134
- sent += f.bytes;
141
+ sent += f.size_bytes;
135
142
  process.stdout.write(`\r ${size(sent)} of ${size(total)} `);
136
143
  }
137
144
  }),
138
145
  );
139
146
  if (todo.length) process.stdout.write("\n");
140
147
 
141
- const { version } = await api("POST", `/games/${slug}/versions`, { files: list, notes: typeof opts.notes === "string" ? opts.notes : "", publish: opts.publish ? "when-approved" : undefined });
148
+ 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
149
  console.log(`Version ${version.id} is being scanned. This usually takes under a minute…`);
143
150
 
144
151
  for (let i = 0; i < 200; i++) {
145
152
  await sleep(3000);
146
153
  const { version: v } = await api("GET", `/games/${slug}/versions/${version.id}`);
147
- if (v.scan === "scanning") continue;
148
- if (v.scan === "approved") {
149
- if (v.live) console.log(`Passed and live: ${API}/g/${slug}`);
154
+ if (v.scan_status === "scanning") continue;
155
+ if (v.scan_status === "approved") {
156
+ if (v.is_live) console.log(`Passed and live: ${API}/g/${slug}`);
150
157
  else {
151
158
  console.log(`Passed. Your private preview: ${v.preview_url}`);
152
159
  console.log(`Publish it with: zaisma publish ${slug} ${v.id}`);
153
160
  }
154
161
  } else {
155
- console.log(v.scan === "held" ? "Held: a person will look at it, usually within a day. What the scan found:" : "Rejected. What the scan found:");
156
- for (const f of v.findings ?? []) console.log(` - ${f.detail}${f.path ? ` (${f.path})` : ""}`);
157
- if (v.scan === "rejected") process.exit(1);
162
+ 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:");
163
+ for (const f of v.findings) console.log(` - ${f.explanation}${f.path ? ` (${f.path})` : ""}`);
164
+ if (v.scan_status === "rejected") process.exit(1);
158
165
  }
159
166
  return;
160
167
  }
161
168
  console.log(`Still scanning. Check later with: zaisma versions ${slug}`);
162
169
  }
163
170
 
171
+ const day = (ms) => new Date(ms).toISOString().slice(0, 10);
172
+
173
+ function printKnown(k) {
174
+ console.log(`${k.id} ${k.status.padEnd(8)} ${k.title} (${k.developers} developer${k.developers === 1 ? "" : "s"}${k.roadmap ? `, roadmap: ${k.roadmap}` : ""})`);
175
+ if (k.workaround) console.log(` Until it's fixed: ${k.workaround}`);
176
+ }
177
+
178
+ /** Text from --text, or piped in. */
179
+ async function readText(opts) {
180
+ if (typeof opts.text === "string") return opts.text;
181
+ if (process.stdin.isTTY) return "";
182
+ const chunks = [];
183
+ for await (const c of process.stdin) chunks.push(c);
184
+ return Buffer.concat(chunks).toString("utf8").trim();
185
+ }
186
+
187
+ /** Feedback to Zaisma. Posts say an AI wrote them unless --by-me says the developer did. */
188
+ async function feedback(opts) {
189
+ const [sub, id] = opts._;
190
+ const by = opts["by-me"] ? { author: "person" } : {};
191
+ if (sub === "send") {
192
+ const text = await readText(opts);
193
+ if (typeof opts.kind !== "string" || typeof opts.title !== "string" || !text) fail('usage: zaisma feedback send --kind bug|gap|feature|question --title "…" --text "…" [--game <slug>]');
194
+ const out = await api("POST", "/feedback", { kind: opts.kind, title: opts.title, text, ...(typeof opts.game === "string" ? { game: opts.game } : {}), ...by });
195
+ console.log(`Sent (${out.feedback.id}). Zaisma will reply here: zaisma feedback ${out.feedback.id}`);
196
+ if (out.secrets_removed) console.log(`${out.secrets_removed} secret${out.secrets_removed === 1 ? " was" : "s were"} blanked before it was stored.`);
197
+ if (out.matches.length) {
198
+ console.log("\nKnown issues that look like the same problem:");
199
+ for (const k of out.matches) printKnown(k);
200
+ }
201
+ return;
202
+ }
203
+ if (sub === "add") {
204
+ const text = await readText(opts);
205
+ if (!id || !text) fail('usage: zaisma feedback add <id> --text "…" [--reopen]');
206
+ const out = await api("POST", `/feedback/${id}/posts`, { text, ...(opts.reopen ? { reopen: true } : {}), ...by });
207
+ console.log(`Added to ${out.feedback.id} (${out.feedback.status}).`);
208
+ return;
209
+ }
210
+ if (sub) {
211
+ const out = await api("GET", `/feedback/${sub}`);
212
+ if (!out.feedback) return printKnown(out.known_issue);
213
+ const f = out.feedback;
214
+ console.log(`${f.title}\n${f.kind}, ${f.status}${f.game ? `, ${f.game}` : ""}`);
215
+ if (f.known_issue) printKnown(f.known_issue);
216
+ for (const p of f.posts) console.log(`\n${day(p.at)} ${p.by}${p.kind === "reopen" ? " (reopened)" : ""}:\n${p.text}${p.picture_url ? `\n[screenshot] ${p.picture_url}` : ""}`);
217
+ return;
218
+ }
219
+ const q = typeof opts.search === "string" ? `?q=${encodeURIComponent(opts.search)}` : "";
220
+ const { mine, known_issues } = await api("GET", `/feedback${q}`);
221
+ console.log(mine.length ? "Yours (* has a reply you haven't read):" : "You haven't sent any feedback yet.");
222
+ for (const f of mine) console.log(`${f.id} ${f.status.padEnd(8)} ${f.news ? "* " : " "}${f.title}`);
223
+ if (known_issues.length) {
224
+ console.log("\nKnown issues:");
225
+ for (const k of known_issues) printKnown(k);
226
+ }
227
+ }
228
+
164
229
  async function main() {
165
230
  const [command, ...rest] = process.argv.slice(2);
166
231
  const opts = args(rest);
167
232
  if (!command || command === "help" || command === "--help" || command === "-h") return console.log(HELP);
233
+ // Flags renamed to match the API's names (docs/NAMING.md). Said out loud, so an old one isn't quietly ignored.
234
+ const renamed = { notes: "patch-notes", publish: "publish-when-approved", at: "scheduled-publish-at" };
235
+ for (const [old, now] of Object.entries(renamed)) if (old in opts) fail(`--${old} is now --${now}.`);
168
236
 
169
237
  if (command === "games") {
170
- const { games } = await api("GET", "/games");
238
+ const { games, unread_topic_count } = await api("GET", "/games");
171
239
  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.review.padEnd(8)} ${g.title}`);
240
+ for (const g of games) console.log(`${g.slug.padEnd(28)} ${g.visibility.padEnd(9)} ${g.review_status.padEnd(8)} ${g.title}`);
241
+ if (unread_topic_count) console.log(`\nZaisma replied to ${unread_topic_count} of your forum topics. See them with: zaisma feedback`);
173
242
  return;
174
243
  }
175
244
  if (command === "new") {
176
245
  const title = opts._.join(" ");
177
246
  if (!title) fail('usage: zaisma new "My Game" [--slug my-game]');
178
247
  const { game } = await api("POST", "/games", { title, ...(typeof opts.slug === "string" ? { slug: opts.slug } : {}) });
179
- return console.log(`Made ${game.url} (a draft). Upload it with: zaisma upload <folder> --game ${game.slug}`);
248
+ return console.log(`Made ${game.page_url} (a draft). Upload it with: zaisma upload <folder> --game ${game.slug}`);
180
249
  }
181
250
  if (command === "upload") return upload(opts._[0], opts);
182
251
  if (command === "versions") {
183
252
  const slug = opts._[0];
184
253
  if (!slug) fail("usage: zaisma versions <slug>");
185
254
  const { versions } = await api("GET", `/games/${slug}`);
186
- for (const v of versions) console.log(`${v.id} ${new Date(v.created_at).toISOString().slice(0, 16).replace("T", " ")} ${(v.live ? "live" : v.scan).padEnd(9)} ${v.preview_url ?? ""}${v.publish_at ? ` goes live ${new Date(v.publish_at).toLocaleString()}` : ""}`);
255
+ 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
256
  return;
188
257
  }
189
258
  if (command === "publish") {
190
259
  const [slug, id] = opts._;
191
260
  if (!slug || !id) fail("usage: zaisma publish <slug> <version>");
192
- if (typeof opts.at === "string") {
193
- const at = new Date(opts.at.includes("T") || /z$|[+-]\d\d:?\d\d$/i.test(opts.at) ? opts.at : opts.at.replace(" ", "T"));
194
- if (Number.isNaN(at.getTime())) fail('give --at as a date and time, like "2026-11-01 17:00".');
195
- const { version } = await api("POST", `/games/${slug}/versions/${id}/publish`, { at: at.getTime() });
196
- console.log(`Version ${id} will go live at ${new Date(version.publish_at).toLocaleString()}${version.scan === "scanning" ? ", if it has passed the scan by then" : ""}.`);
261
+ const later = opts["scheduled-publish-at"];
262
+ if (typeof later === "string") {
263
+ const at = new Date(later.includes("T") || /z$|[+-]\d\d:?\d\d$/i.test(later) ? later : later.replace(" ", "T"));
264
+ if (Number.isNaN(at.getTime())) fail('give --scheduled-publish-at as a date and time, like "2026-11-01 17:00".');
265
+ const { version } = await api("POST", `/games/${slug}/versions/${id}/publish`, { scheduled_publish_at: at.toISOString() });
266
+ 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
267
  return;
198
268
  }
199
269
  const { game } = await api("POST", `/games/${slug}/versions/${id}/publish`);
200
- console.log(`Version ${id} is live at ${game.url}`);
270
+ console.log(`Version ${id} is live at ${game.page_url}`);
201
271
  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
272
  return;
203
273
  }
@@ -213,6 +283,7 @@ async function main() {
213
283
  const { game } = await api("PATCH", `/games/${slug}`, { visibility: opts.visibility });
214
284
  return console.log(`${game.title} is now ${game.visibility}.`);
215
285
  }
286
+ if (command === "feedback") return feedback(opts);
216
287
  fail(`unknown command "${command}". Run zaisma help.`);
217
288
  }
218
289