cajonzero 0.1.0 → 0.1.2

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/dist/bin/cj0.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { mkdirSync, existsSync, statSync, writeFileSync, readdirSync, readFileSync } from 'fs';
3
- import path from 'path';
2
+ import { mkdirSync, existsSync, readFileSync, writeFileSync, statSync, readdirSync } from 'fs';
3
+ import path3 from 'path';
4
+ import { createInterface } from 'readline/promises';
4
5
  import { fileURLToPath } from 'url';
6
+ import { randomBytes } from 'crypto';
5
7
 
6
8
  // src/scaffold/templates.ts
7
9
  var SCAFFOLD_ENTRY_PATH = "src/index.ts";
@@ -34,7 +36,6 @@ var SCAFFOLD_ENTRY_CONTENT = [
34
36
  " sessionSecret: (env) => env.CAJONZERO_SESSION_SECRET,",
35
37
  " adminToken: (env) => env.BOX_PLAY_ADMIN_TOKEN,",
36
38
  " environment: (env) => env.CAJONZERO_ENV,",
37
- " mapSeedKv: (env) => env.CAJONZERO_KV,",
38
39
  " rooms: (env) => env.ROOMS,",
39
40
  " settings: (env) => {",
40
41
  " const e = vars(env);",
@@ -52,7 +53,7 @@ var SCAFFOLD_ENTRY_CONTENT = [
52
53
  " };",
53
54
  " },",
54
55
  "});",
55
- 'export { Room } from "cajonzero";',
56
+ 'export { Room } from "cajonzero/serve";',
56
57
  ""
57
58
  ].join("\n");
58
59
  var SCAFFOLD_CONFIG_CONTENT = `${JSON.stringify(
@@ -61,7 +62,6 @@ var SCAFFOLD_CONFIG_CONTENT = `${JSON.stringify(
61
62
  admin: { path: "/admin" },
62
63
  room: { tickRate: 64, emptyGraceMs: 48e4 },
63
64
  maps: {
64
- cacheTtlSeconds: 86400,
65
65
  maxDownloadBytes: 33554432,
66
66
  concurrency: 8,
67
67
  requestTimeoutMs: 15e3
@@ -126,7 +126,7 @@ function wranglerConfig(options) {
126
126
  " },",
127
127
  ' "kv_namespaces": [',
128
128
  " {",
129
- " // Player/account data + map-script KV + resolved map-seed cache.",
129
+ " // Player/account data + map-script KV.",
130
130
  ' "binding": "CAJONZERO_KV",',
131
131
  " // Replace with `wrangler kv namespace create CAJONZERO_KV` output.",
132
132
  ` "id": ${JSON.stringify(SCAFFOLD_KV_PLACEHOLDER_ID)}`,
@@ -158,82 +158,49 @@ function readme(options) {
158
158
  return [
159
159
  `# ${options.packageName}`,
160
160
  "",
161
- "A serverless (Cloudflare Workers) BOX3 **play** server, scaffolded by",
162
- "[`cajonzero`](https://www.npmjs.com/package/cajonzero).",
161
+ "\u7531 cajonzero \u751F\u6210\u7684 Cloudflare Workers \u670D\u52A1\u3002\u9879\u76EE\u53EA\u6709\u914D\u7F6E\u548C\u4E00\u4E2A\u5F88\u8584\u7684 worker",
162
+ "\u5165\u53E3\uFF0C\u6CA1\u6709\u5F15\u64CE\u6E90\u7801\u3002",
163
163
  "",
164
- "This project contains **no engine source code** \u2014 configuration plus a short",
165
- "worker entry that hands `cajonzero/config.json` and `cajonzero/maps.json` to the",
166
- "runtime:",
167
- "",
168
- "```ts",
169
- 'import config from "../cajonzero/config.json";',
170
- 'import maps from "../cajonzero/maps.json";',
171
- 'import { serve } from "cajonzero/serve";',
172
- "",
173
- "export default serve({",
174
- " config,",
175
- " maps,",
176
- " storage: (env) => /* any unstorage driver over env bindings */,",
177
- " sessionSecret: (env) => env.CAJONZERO_SESSION_SECRET,",
178
- " // adminToken / environment / mapSeedKv / rooms / settings \u2026",
179
- "});",
180
- 'export { Room } from "cajonzero";',
181
- "```",
182
- "",
183
- "## Quick start",
164
+ "## \u5FEB\u901F\u5F00\u59CB",
184
165
  "",
185
166
  "```bash",
186
- "npm install # also syncs cajonzero's prebuilt lobby/console into dist/ui",
187
- "npm run dev # wrangler dev \u2192 http://localhost:8787",
167
+ "npm install",
168
+ "npx cj0 setup # \u4EA4\u4E92\u5F0F\u914D\u7F6E\u5FC5\u9700/\u53EF\u9009\u7684 ENV",
169
+ "npm run dev # http://localhost:8787",
188
170
  "```",
189
171
  "",
190
- "`npm run build` validates the deploy by bundling the worker without",
191
- "publishing (`wrangler deploy --dry-run`).",
192
- "",
193
- "## Environment",
194
- "",
195
- "Every value the runtime consumes is passed explicitly in `src/index.ts`;",
196
- "the package never guesses env var names.",
197
- "",
198
- "| Name | Kind | Required | Purpose |",
199
- "|---|---|---|---|",
200
- "| `CAJONZERO_KV` | KV binding | yes | player/account data + map-seed cache |",
201
- "| `ROOMS` | Durable Object binding | yes | room actors |",
202
- "| `CAJONZERO_SESSION_SECRET` | secret | yes | signs session proofs and login cookies |",
203
- "| `BOX_PLAY_ADMIN_TOKEN` | secret | yes | owner console and `/api/admin/*` |",
204
- "| `CAJONZERO_ENV` | var | no | deployment label (`development` relaxes cookie `Secure`) |",
205
- "| `CAJONZERO_PLAYER_LIMIT` | var | no | maximum players per room |",
206
- "| `CAJONZERO_ZERO_PLAYER_TIMEOUT_MS` | var | no | empty-room grace window |",
207
- "| `CAJONZERO_AUTOSAVE_INTERVAL_MS` | var | no | snapshot cadence |",
208
- "| `CAJONZERO_DEBUG` | var | no | verbose logging |",
209
- "| `CAJONZERO_TELEPORT_ACCESS` | var | no | allow non-team containers |",
210
- "| `CAJONZERO_PAID_ACCESS` | var | no | restrict joins to paid containers |",
211
- "| `CAJONZERO_SCRIPT_HEAP_MB` | var | no | QuickJS heap per script host |",
212
- "| `CAJONZERO_SCRIPT_TIMEOUT_MS` | var | no | script invocation timeout |",
213
- "| `CAJONZERO_DB_SIZE_LIMIT` | var | no | map-script storage budget |",
172
+ "## \u73AF\u5883\u53D8\u91CF",
214
173
  "",
215
- "Create the namespace and set the secrets before deploying:",
174
+ "`cj0 setup` \u628A\u975E\u5BC6\u94A5\u5199\u5165 `wrangler.jsonc` \u7684 `vars`\uFF0C\u628A\u5BC6\u94A5\u5199\u5165 `.dev.vars`\uFF08\u672C\u5730\uFF09\u3002",
175
+ "\u90E8\u7F72\u65F6\u5BC6\u94A5\u8981\u5355\u72EC\u8BBE\u7F6E\uFF1A",
216
176
  "",
217
177
  "```bash",
218
- "npx wrangler kv namespace create CAJONZERO_KV # paste the id into wrangler.jsonc",
178
+ "npx wrangler kv namespace create CAJONZERO_KV # id \u586B\u8FDB wrangler.jsonc",
219
179
  "npx wrangler secret put CAJONZERO_SESSION_SECRET",
220
180
  "npx wrangler secret put BOX_PLAY_ADMIN_TOKEN",
221
181
  "```",
222
182
  "",
223
- "## Maps (object storage)",
224
- "",
225
- "Maps are **not** uploaded with the worker and are never unpacked at runtime.",
226
- "Unpack the `.boxplay` yourself and upload the resulting directory to OSS/S3/CDN",
227
- "with the archive layout preserved:",
183
+ "| \u540D\u79F0 | \u7C7B\u578B | \u5FC5\u9700 | \u8BF4\u660E |",
184
+ "|---|---|---|---|",
185
+ "| `CAJONZERO_KV` | KV \u7ED1\u5B9A | \u662F | \u8D26\u53F7/\u73A9\u5BB6\u6570\u636E + \u5730\u56FE\u811A\u672C KV |",
186
+ "| `ROOMS` | Durable Object \u7ED1\u5B9A | \u662F | \u623F\u95F4\u5B9E\u4F8B |",
187
+ "| `CAJONZERO_SESSION_SECRET` | \u5BC6\u94A5 | \u662F | \u4F1A\u8BDD\u51ED\u8BC1\u4E0E Cookie \u7B7E\u540D |",
188
+ "| `BOX_PLAY_ADMIN_TOKEN` | \u5BC6\u94A5 | \u662F | \u7BA1\u7406\u540E\u53F0\u4EE4\u724C |",
189
+ "| `CAJONZERO_ENV` | \u53D8\u91CF | \u5426 | \u90E8\u7F72\u540D\u79F0\uFF08`development` \u65F6 Cookie \u4E0D\u52A0 Secure\uFF09 |",
190
+ "| `CAJONZERO_PLAYER_LIMIT` | \u53D8\u91CF | \u5426 | \u5355\u623F\u95F4\u73A9\u5BB6\u4E0A\u9650 |",
191
+ "| `CAJONZERO_ZERO_PLAYER_TIMEOUT_MS` | \u53D8\u91CF | \u5426 | \u7A7A\u623F\u95F4\u4FDD\u7559\u65F6\u95F4 |",
192
+ "| `CAJONZERO_AUTOSAVE_INTERVAL_MS` | \u53D8\u91CF | \u5426 | \u81EA\u52A8\u5B58\u6863\u95F4\u9694 |",
193
+ "| `CAJONZERO_DEBUG` | \u53D8\u91CF | \u5426 | \u8C03\u8BD5\u65E5\u5FD7 |",
194
+ "| `CAJONZERO_TELEPORT_ACCESS` | \u53D8\u91CF | \u5426 | \u5141\u8BB8\u4F20\u9001\u7C7B\u5BB9\u5668 |",
195
+ "| `CAJONZERO_PAID_ACCESS` | \u53D8\u91CF | \u5426 | \u4EC5\u4ED8\u8D39\u5BB9\u5668\u53EF\u52A0\u5165 |",
196
+ "| `CAJONZERO_SCRIPT_HEAP_MB` | \u53D8\u91CF | \u5426 | \u811A\u672C\u5806\u4E0A\u9650 |",
197
+ "| `CAJONZERO_SCRIPT_TIMEOUT_MS` | \u53D8\u91CF | \u5426 | \u5355\u6B21\u811A\u672C\u8D85\u65F6 |",
198
+ "| `CAJONZERO_DB_SIZE_LIMIT` | \u53D8\u91CF | \u5426 | \u5730\u56FE\u811A\u672C\u5B58\u50A8\u4E0A\u9650 |",
228
199
  "",
229
- "```text",
230
- "<root>/bundle.json",
231
- "<root>/worlds/<worldId>/manifest.json",
232
- "<root>/worlds/<worldId>/data/project-state.json",
233
- "<root>/worlds/<worldId>/data/blobs/<cid>",
234
- "```",
200
+ "## \u5730\u56FE",
235
201
  "",
236
- "Then add an entry to `cajonzero/maps.json`:",
202
+ "\u5730\u56FE\u4E0D\u968F worker \u4E0A\u4F20\u3002\u628A\u89E3\u5305\u540E\u7684 `.boxplay` \u76EE\u5F55\u4F20\u5230 OSS/S3/CDN\uFF08\u9700\u5141\u8BB8 CORS\uFF09\uFF0C",
203
+ "\u518D\u5728 `cajonzero/maps.json` \u767B\u8BB0\uFF1A",
237
204
  "",
238
205
  "```json",
239
206
  "{",
@@ -243,32 +210,21 @@ function readme(options) {
243
210
  ' "slug": "omega",',
244
211
  ' "root": "https://cdn.example.com/cajonzero/omega/",',
245
212
  ' "worldId": "project-omega",',
246
- ' "name": "Omega City Reloaded",',
247
- ' "version": "1.0.4",',
248
- ' "etag": "v1.0.4"',
213
+ ' "name": "Omega City Reloaded"',
249
214
  " }",
250
215
  " ]",
251
216
  "}",
252
217
  "```",
253
218
  "",
254
- "- `root` must be the unpacked map directory and end with `/`.",
255
- "- `etag` (or `version`) is the seed-cache revision: bump it whenever you",
256
- " re-upload the map, otherwise the cached world stays stale.",
257
- "- The bucket may be public-read; the server fetches it, so no CORS is needed",
258
- " for gameplay. Enable CORS only if the browser loads assets from it.",
219
+ "`root` \u5FC5\u987B\u4EE5 `/` \u7ED3\u5C3E\uFF1B\u66F4\u65B0\u5730\u56FE\u540E\u5237\u65B0 CDN \u7F13\u5B58\u6216\u6362\u8DEF\u5F84\u3002",
259
220
  "",
260
- "## Where things live",
261
- "",
262
- "- **Play lobby** \u2014 the worker serves the lobby at `/`.",
263
- "- **Admin console** \u2014 at `/admin` (sign in with `BOX_PLAY_ADMIN_TOKEN`) for",
264
- " player bans and runtime settings; its JSON API is under `/api/admin/*`.",
265
- "- **Map catalog** \u2014 `GET /api/maps` returns the entries from `maps.json`.",
266
- "",
267
- "## Deploy",
221
+ "## \u90E8\u7F72",
268
222
  "",
269
223
  "```bash",
270
- "npm run deploy # wrangler deploy",
224
+ "npm run deploy",
271
225
  "```",
226
+ "",
227
+ "\u5927\u5385\u5728 `/`\uFF0C\u7BA1\u7406\u540E\u53F0\u5728 `/admin`\u3002",
272
228
  ""
273
229
  ].join("\n");
274
230
  }
@@ -310,7 +266,7 @@ function readCajonzeroPackage(manifestPath) {
310
266
  return { version, wranglerRange, unstorageRange };
311
267
  }
312
268
  function toPackageName(folder) {
313
- const base = path.basename(folder);
269
+ const base = path3.basename(folder);
314
270
  const slug = base.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-._]+/, "").replace(/[-._]+$/, "");
315
271
  return slug.length > 0 ? slug : "cajonzero-app";
316
272
  }
@@ -325,7 +281,7 @@ function createProject(folder, options = {}) {
325
281
  throw new ScaffoldError("INVALID_TARGET", "create requires a target folder");
326
282
  }
327
283
  const cwd = options.cwd ?? process.cwd();
328
- const target = path.resolve(cwd, name);
284
+ const target = path3.resolve(cwd, name);
329
285
  if (existsSync(target) && !statSync(target).isDirectory()) {
330
286
  throw new ScaffoldError("INVALID_TARGET", `target exists and is not a directory: ${target}`);
331
287
  }
@@ -348,12 +304,280 @@ function createProject(folder, options = {}) {
348
304
  });
349
305
  mkdirSync(target, { recursive: true });
350
306
  for (const file of files) {
351
- const fullPath = path.join(target, file.path);
352
- mkdirSync(path.dirname(fullPath), { recursive: true });
307
+ const fullPath = path3.join(target, file.path);
308
+ mkdirSync(path3.dirname(fullPath), { recursive: true });
353
309
  writeFileSync(fullPath, file.content, "utf8");
354
310
  }
355
311
  return { target, files: files.map((file) => file.path) };
356
312
  }
313
+ var SETTING_FIELDS = [
314
+ {
315
+ name: "CAJONZERO_PLAYER_LIMIT",
316
+ label: "\u5355\u623F\u95F4\u73A9\u5BB6\u4E0A\u9650",
317
+ kind: "var",
318
+ required: false,
319
+ hint: "\u9ED8\u8BA4 100"
320
+ },
321
+ {
322
+ name: "CAJONZERO_ZERO_PLAYER_TIMEOUT_MS",
323
+ label: "\u7A7A\u623F\u95F4\u4FDD\u7559\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09",
324
+ kind: "var",
325
+ required: false,
326
+ hint: "\u9ED8\u8BA4 480000\uFF088 \u5206\u949F\uFF09"
327
+ },
328
+ {
329
+ name: "CAJONZERO_AUTOSAVE_INTERVAL_MS",
330
+ label: "\u81EA\u52A8\u5B58\u6863\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09",
331
+ kind: "var",
332
+ required: false,
333
+ hint: "\u9ED8\u8BA4 300000"
334
+ },
335
+ {
336
+ name: "CAJONZERO_DEBUG",
337
+ label: "\u8C03\u8BD5\u65E5\u5FD7\uFF08true/false\uFF09",
338
+ kind: "var",
339
+ required: false,
340
+ hint: "\u9ED8\u8BA4 false"
341
+ },
342
+ {
343
+ name: "CAJONZERO_TELEPORT_ACCESS",
344
+ label: "\u5141\u8BB8\u4F20\u9001\u7C7B\u5BB9\u5668\uFF08true/false\uFF09",
345
+ kind: "var",
346
+ required: false,
347
+ hint: "\u9ED8\u8BA4 false"
348
+ },
349
+ {
350
+ name: "CAJONZERO_PAID_ACCESS",
351
+ label: "\u4EC5\u4ED8\u8D39\u5BB9\u5668\u53EF\u52A0\u5165\uFF08true/false\uFF09",
352
+ kind: "var",
353
+ required: false,
354
+ hint: "\u9ED8\u8BA4 false"
355
+ },
356
+ {
357
+ name: "CAJONZERO_SCRIPT_HEAP_MB",
358
+ label: "\u811A\u672C\u5806\u4E0A\u9650\uFF08MB\uFF09",
359
+ kind: "var",
360
+ required: false,
361
+ hint: "\u9ED8\u8BA4 256"
362
+ },
363
+ {
364
+ name: "CAJONZERO_SCRIPT_TIMEOUT_MS",
365
+ label: "\u5355\u6B21\u811A\u672C\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
366
+ kind: "var",
367
+ required: false,
368
+ hint: "\u9ED8\u8BA4 10000"
369
+ },
370
+ {
371
+ name: "CAJONZERO_DB_SIZE_LIMIT",
372
+ label: "\u5730\u56FE\u811A\u672C\u5B58\u50A8\u4E0A\u9650\uFF08\u5B57\u8282\uFF09",
373
+ kind: "var",
374
+ required: false,
375
+ hint: "\u9ED8\u8BA4 134217728"
376
+ }
377
+ ];
378
+ var SETUP_FIELDS = [
379
+ {
380
+ name: "CAJONZERO_SESSION_SECRET",
381
+ label: "\u4F1A\u8BDD\u7B7E\u540D\u5BC6\u94A5",
382
+ kind: "secret",
383
+ required: true,
384
+ hint: "\u7559\u7A7A\u81EA\u52A8\u751F\u6210\uFF1B\u672C\u5730\u5199\u5165 .dev.vars"
385
+ },
386
+ {
387
+ name: "BOX_PLAY_ADMIN_TOKEN",
388
+ label: "\u7BA1\u7406\u540E\u53F0\u4EE4\u724C",
389
+ kind: "secret",
390
+ required: true,
391
+ hint: "\u7559\u7A7A\u81EA\u52A8\u751F\u6210\uFF1B\u672C\u5730\u5199\u5165 .dev.vars"
392
+ },
393
+ {
394
+ name: "CAJONZERO_ENV",
395
+ label: "\u90E8\u7F72\u540D\u79F0",
396
+ kind: "var",
397
+ required: true,
398
+ default: "development",
399
+ hint: "development \u65F6 Cookie \u4E0D\u52A0 Secure"
400
+ },
401
+ ...SETTING_FIELDS
402
+ ];
403
+ function generateSecret() {
404
+ return randomBytes(32).toString("hex");
405
+ }
406
+ function generateAdminToken() {
407
+ return randomBytes(24).toString("base64url");
408
+ }
409
+ function suggestedValue(field) {
410
+ if (field.default !== void 0) return field.default;
411
+ if (field.kind === "secret") {
412
+ return field.name === "BOX_PLAY_ADMIN_TOKEN" ? generateAdminToken() : generateSecret();
413
+ }
414
+ return void 0;
415
+ }
416
+ function parseDevVars(text) {
417
+ const out = {};
418
+ for (const line of text.split(/\r?\n/)) {
419
+ const trimmed = line.trim();
420
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
421
+ const eq = trimmed.indexOf("=");
422
+ if (eq <= 0) continue;
423
+ const key = trimmed.slice(0, eq).trim();
424
+ let value = trimmed.slice(eq + 1).trim();
425
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
426
+ value = value.slice(1, -1);
427
+ }
428
+ out[key] = value;
429
+ }
430
+ return out;
431
+ }
432
+ function encodeDevVar(value) {
433
+ return /[\s#'"]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
434
+ }
435
+ function serializeDevVars(values) {
436
+ const lines = Object.entries(values).map(([key, value]) => `${key}=${encodeDevVar(value)}`);
437
+ return `${lines.join("\n")}
438
+ `;
439
+ }
440
+ function findVarsBlock(raw) {
441
+ const match = /"vars"\s*:\s*\{/.exec(raw);
442
+ if (match === null) return null;
443
+ const open = raw.indexOf("{", match.index);
444
+ let depth = 0;
445
+ let inString = false;
446
+ let escaped = false;
447
+ for (let i = open; i < raw.length; i += 1) {
448
+ const ch = raw[i];
449
+ if (inString) {
450
+ if (escaped) escaped = false;
451
+ else if (ch === "\\") escaped = true;
452
+ else if (ch === '"') inString = false;
453
+ continue;
454
+ }
455
+ if (ch === '"') inString = true;
456
+ else if (ch === "{") depth += 1;
457
+ else if (ch === "}") {
458
+ depth -= 1;
459
+ if (depth === 0) {
460
+ const lineStart = raw.lastIndexOf("\n", match.index) + 1;
461
+ const indent = /^\s*/.exec(raw.slice(lineStart, match.index))?.[0] ?? " ";
462
+ return { start: match.index, open, close: i, indent };
463
+ }
464
+ }
465
+ }
466
+ return null;
467
+ }
468
+ function parseJsoncObject(text) {
469
+ let out = "";
470
+ let inString = false;
471
+ let escaped = false;
472
+ for (let i = 0; i < text.length; i += 1) {
473
+ const ch = text[i];
474
+ if (inString) {
475
+ out += ch;
476
+ if (escaped) escaped = false;
477
+ else if (ch === "\\") escaped = true;
478
+ else if (ch === '"') inString = false;
479
+ continue;
480
+ }
481
+ if (ch === '"') {
482
+ inString = true;
483
+ out += ch;
484
+ continue;
485
+ }
486
+ if (ch === "/" && text[i + 1] === "/") {
487
+ while (i < text.length && text[i] !== "\n") i += 1;
488
+ out += "\n";
489
+ continue;
490
+ }
491
+ if (ch === "/" && text[i + 1] === "*") {
492
+ i += 2;
493
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
494
+ i += 1;
495
+ continue;
496
+ }
497
+ out += ch;
498
+ }
499
+ out = out.replace(/,(\s*[}\]])/g, "$1");
500
+ try {
501
+ const parsed = JSON.parse(out);
502
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
503
+ } catch {
504
+ return {};
505
+ }
506
+ }
507
+ function setWranglerVars(raw, vars) {
508
+ const managed = new Set(SETUP_FIELDS.filter((f) => f.kind === "var").map((f) => f.name));
509
+ const block = findVarsBlock(raw);
510
+ const existing = block === null ? {} : parseJsoncObject(`{${raw.slice(block.open + 1, block.close)}}`);
511
+ const merged = {};
512
+ for (const [key, value] of Object.entries(existing)) {
513
+ if (!managed.has(key)) merged[key] = value;
514
+ }
515
+ for (const field of SETUP_FIELDS) {
516
+ if (field.kind !== "var") continue;
517
+ const value = vars[field.name];
518
+ if (value !== void 0 && value.length > 0) merged[field.name] = value;
519
+ }
520
+ const indent = block?.indent ?? " ";
521
+ const inner = `${indent} `;
522
+ const body = Object.entries(merged).map(([key, value]) => `${inner}${JSON.stringify(key)}: ${JSON.stringify(value)}`).join(",\n");
523
+ const bodyText = `"vars": {
524
+ ${body}${body.length > 0 ? "\n" : ""}${indent}}`;
525
+ if (block !== null) {
526
+ return `${raw.slice(0, block.start)}${bodyText}${raw.slice(block.close + 1)}`;
527
+ }
528
+ const open = raw.indexOf("{");
529
+ if (open < 0) throw new Error("wrangler config is not a JSON object");
530
+ return `${raw.slice(0, open + 1)}
531
+ ${indent}${bodyText},
532
+ ${raw.slice(open + 1)}`;
533
+ }
534
+ function readSetupValues(cwd, configPath, devVarsPath) {
535
+ const out = {};
536
+ if (existsSync(devVarsPath)) {
537
+ Object.assign(out, parseDevVars(readFileSync(devVarsPath, "utf8")));
538
+ }
539
+ if (existsSync(configPath)) {
540
+ const raw = readFileSync(configPath, "utf8");
541
+ const block = findVarsBlock(raw);
542
+ if (block !== null) {
543
+ const parsed = parseJsoncObject(`{${raw.slice(block.open + 1, block.close)}}`);
544
+ for (const [key, value] of Object.entries(parsed)) {
545
+ if (typeof value === "string") out[key] = value;
546
+ }
547
+ }
548
+ }
549
+ return out;
550
+ }
551
+ function applySetup(options) {
552
+ const cwd = options.cwd ?? process.cwd();
553
+ const configPath = path3.resolve(cwd, options.configPath ?? "wrangler.jsonc");
554
+ const devVarsPath = path3.join(path3.dirname(configPath), ".dev.vars");
555
+ if (!existsSync(configPath)) {
556
+ throw new Error(`\u627E\u4E0D\u5230 ${path3.basename(configPath)}\uFF0C\u8BF7\u5728 cj0 create \u751F\u6210\u7684\u9879\u76EE\u76EE\u5F55\u4E2D\u8FD0\u884C`);
557
+ }
558
+ const vars = {};
559
+ const secrets = {};
560
+ for (const field of SETUP_FIELDS) {
561
+ const value = options.values[field.name]?.trim();
562
+ if (value === void 0 || value.length === 0) continue;
563
+ if (field.kind === "secret") secrets[field.name] = value;
564
+ else vars[field.name] = value;
565
+ }
566
+ const raw = readFileSync(configPath, "utf8");
567
+ const next = setWranglerVars(raw, vars);
568
+ if (next !== raw) writeFileSync(configPath, next, "utf8");
569
+ const existingDev = existsSync(devVarsPath) ? parseDevVars(readFileSync(devVarsPath, "utf8")) : {};
570
+ const managedSecrets = new Set(
571
+ SETUP_FIELDS.filter((f) => f.kind === "secret").map((f) => f.name)
572
+ );
573
+ const mergedDev = {};
574
+ for (const [key, value] of Object.entries(existingDev)) {
575
+ if (!managedSecrets.has(key)) mergedDev[key] = value;
576
+ }
577
+ Object.assign(mergedDev, secrets);
578
+ writeFileSync(devVarsPath, serializeDevVars(mergedDev), "utf8");
579
+ return { configPath, devVarsPath, vars, secrets };
580
+ }
357
581
 
358
582
  // src/bin/cj0.ts
359
583
  var USAGE = `cj0 \u2014 serverless (Cloudflare) play-only BOX3 engine
@@ -363,6 +587,7 @@ Usage:
363
587
 
364
588
  Commands:
365
589
  create <folder> Scaffold a deployable project
590
+ setup [--config <file>] Configure required/optional ENV values
366
591
  serve [--port <port>] [--host <host>] Run the cajonzero server locally (workerd)
367
592
 
368
593
  Serve options:
@@ -412,10 +637,94 @@ function runCreate(args) {
412
637
  process.stdout.write(
413
638
  `created project in ${result.target} (${result.files.length} files)
414
639
  ${result.files.join(", ")}
415
- next: cd ${folder} && npm install && npm run dev
640
+ next: cd ${folder} && npm install && npx cj0 setup && npm run dev
416
641
  `
417
642
  );
418
643
  }
644
+ function parseSetupFlags(args) {
645
+ let config = DEFAULT_CONFIG;
646
+ for (let i = 0; i < args.length; i += 1) {
647
+ const arg = args[i];
648
+ if (arg === void 0) continue;
649
+ if (arg === "--config" || arg.startsWith("--config=")) {
650
+ config = readFlagValue(args, i, "--config");
651
+ if (!arg.includes("=")) i += 1;
652
+ } else {
653
+ throw new Error(`unexpected setup option "${arg}"`);
654
+ }
655
+ }
656
+ return { config };
657
+ }
658
+ async function runSetup(args) {
659
+ const { config } = parseSetupFlags(args);
660
+ const cwd = process.cwd();
661
+ const configPath = path3.resolve(cwd, config);
662
+ const devVarsPath = path3.join(path3.dirname(configPath), ".dev.vars");
663
+ const current = readSetupValues(cwd, configPath, devVarsPath);
664
+ if (!process.stdin.isTTY) {
665
+ const values2 = {};
666
+ for (const field of SETUP_FIELDS) {
667
+ if (!field.required) continue;
668
+ const value = current[field.name] ?? suggestedValue(field) ?? "";
669
+ if (value.length > 0) values2[field.name] = value;
670
+ }
671
+ const result2 = applySetup({ cwd, configPath: config, values: values2 });
672
+ process.stdout.write(
673
+ `cajonzero setup\uFF08\u975E\u4EA4\u4E92\uFF09\uFF1Avars=${Object.keys(result2.vars).join(", ") || "\u65E0"} secrets=${Object.keys(result2.secrets).join(", ") || "\u65E0"}
674
+ `
675
+ );
676
+ return;
677
+ }
678
+ const values = {};
679
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
680
+ try {
681
+ process.stdout.write("cajonzero setup \u2014 \u914D\u7F6E\u5FC5\u9700\u4E0E\u53EF\u9009\u7684 ENV\n\n");
682
+ for (const field of SETUP_FIELDS) {
683
+ if (!field.required) continue;
684
+ const existing = current[field.name];
685
+ const hint = field.kind === "secret" ? existing !== void 0 ? "\u5DF2\u914D\u7F6E\uFF0C\u56DE\u8F66\u4FDD\u7559" : "\u56DE\u8F66\u81EA\u52A8\u751F\u6210" : field.hint;
686
+ const label = `? ${field.label}\uFF08${field.name}\uFF09${hint !== void 0 ? ` [${hint}]` : ""}: `;
687
+ const answer = (await rl.question(label)).trim();
688
+ const value = answer.length > 0 ? answer : existing ?? suggestedValue(field) ?? "";
689
+ if (value.length > 0) values[field.name] = value;
690
+ }
691
+ const configureOptional = (await rl.question("? \u914D\u7F6E\u53EF\u9009\u8BBE\u7F6E\u9879\uFF1F(y/N): ")).trim().toLowerCase();
692
+ if (configureOptional === "y" || configureOptional === "yes") {
693
+ process.stdout.write(" \uFF08\u56DE\u8F66\u4FDD\u7559\u5F53\u524D\u503C\uFF0C\u8F93\u5165 - \u6E05\u9664\uFF09\n");
694
+ for (const field of SETUP_FIELDS) {
695
+ if (field.required) continue;
696
+ const existing = current[field.name];
697
+ const shown = existing ?? "";
698
+ const currentLabel = shown.length > 0 ? ` \u5F53\u524D ${shown}` : "";
699
+ const hint = field.hint !== void 0 ? ` [${field.hint}]` : "";
700
+ const answer = (await rl.question(`? ${field.label}\uFF08${field.name}\uFF09${hint}${currentLabel}: `)).trim();
701
+ if (answer === "-") continue;
702
+ const value = answer.length > 0 ? answer : shown;
703
+ if (value.length > 0) values[field.name] = value;
704
+ }
705
+ }
706
+ } finally {
707
+ rl.close();
708
+ }
709
+ const result = applySetup({ cwd, configPath: config, values });
710
+ const varNames = Object.keys(result.vars);
711
+ const secretNames = Object.keys(result.secrets);
712
+ process.stdout.write(`
713
+ \u5DF2\u66F4\u65B0 ${path3.relative(cwd, result.configPath)}\uFF1A
714
+ `);
715
+ process.stdout.write(` vars: ${varNames.length > 0 ? varNames.join(", ") : "\uFF08\u65E0\uFF09"}
716
+ `);
717
+ process.stdout.write(`\u5DF2\u5199\u5165 .dev.vars\uFF1A${secretNames.length > 0 ? secretNames.join(", ") : "\uFF08\u65E0\uFF09"}
718
+ `);
719
+ process.stdout.write("\n\u672C\u5730\u5F00\u53D1\uFF1Anpm run dev\n");
720
+ if (secretNames.length > 0) {
721
+ process.stdout.write("\u90E8\u7F72\u524D\u8BBE\u7F6E Cloudflare secret\uFF1A\n");
722
+ for (const name of secretNames) {
723
+ process.stdout.write(` npx wrangler secret put ${name}
724
+ `);
725
+ }
726
+ }
727
+ }
419
728
  function readFlagValue(args, index, flag) {
420
729
  const inline = args[index];
421
730
  if (inline !== void 0 && inline.startsWith(`${flag}=`)) {
@@ -464,8 +773,8 @@ async function runServe(args) {
464
773
  }
465
774
  const assetsDir = config.assets?.directory;
466
775
  if (typeof assetsDir === "string" && assetsDir.length > 0) {
467
- const configDir = path.dirname(path.resolve(process.cwd(), flags.config));
468
- mkdirSync(path.resolve(configDir, assetsDir), { recursive: true });
776
+ const configDir = path3.dirname(path3.resolve(process.cwd(), flags.config));
777
+ mkdirSync(path3.resolve(configDir, assetsDir), { recursive: true });
469
778
  }
470
779
  const secrets = localDevVars();
471
780
  const worker = await unstable_dev(entry, {
@@ -524,6 +833,9 @@ async function main(argv) {
524
833
  case "create":
525
834
  runCreate(rest);
526
835
  return;
836
+ case "setup":
837
+ await runSetup(rest);
838
+ return;
527
839
  default:
528
840
  process.stderr.write(`cajonzero: unknown command "${command}"
529
841
 
@@ -1,4 +1,4 @@
1
- import { T as TimestampedClientInput, P as NetPublic, Q as NetSecret } from './providers-CJgLgWh4.js';
1
+ import { T as TimestampedClientInput, N as NetPublic, O as NetSecret } from './providers-BX7e4pNW.js';
2
2
 
3
3
  /**
4
4
  * cajonzero — mudb protocol envelope (message-id tables + `raw` channel).
@@ -235,8 +235,6 @@ declare class NetCodec {
235
235
  */
236
236
  /** Map delivery + seed loading policy. */
237
237
  interface MapsConfig {
238
- /** How long a resolved world seed stays in KV. `0` disables the cache. */
239
- readonly cacheTtlSeconds: number;
240
238
  /** Hard cap for a single map file fetched from the CDN (bytes). */
241
239
  readonly maxDownloadBytes: number;
242
240
  /** Parallel blob fetches while resolving a seed. */