@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.
- package/README.md +4 -2
- package/package.json +1 -1
- 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.
|
|
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
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"]
|
|
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 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`, {
|
|
90
|
-
if (start.
|
|
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.
|
|
95
|
-
const chunk = new Uint8Array(Math.min(start.
|
|
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}?
|
|
98
|
-
parts.push({
|
|
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`, {
|
|
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),
|
|
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.
|
|
124
|
-
const total = todo.reduce((n, f) => n + f.
|
|
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.
|
|
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.
|
|
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,
|
|
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.
|
|
148
|
-
if (v.
|
|
149
|
-
if (v.
|
|
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.
|
|
156
|
-
for (const f of v.findings
|
|
157
|
-
if (v.
|
|
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.
|
|
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.
|
|
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} ${
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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.
|
|
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
|
|