cajonzero 0.1.1 → 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/README.md CHANGED
@@ -10,27 +10,27 @@ npm install cajonzero
10
10
  npx cj0 --help
11
11
  ```
12
12
 
13
- ## 本地运行
13
+ ## 本地运行(本仓库)
14
14
 
15
15
  ```bash
16
16
  npm install
17
17
  npm run build # 构建大厅与管理后台
18
- npm run demo # 启动在 http://127.0.0.1:8787
18
+ npm run demo # http://127.0.0.1:8787
19
19
  ```
20
20
 
21
- 打开页面,注册账号后选择地图即可进入房间。
22
- 管理后台地址是 `/admin/login`,演示环境的令牌为 `cajonzero-demo-admin-token`。
21
+ 管理后台地址 `/admin/login`,演示令牌 `cajonzero-demo-admin-token`。
23
22
 
24
- ## 创建自己的服务
23
+ ## 创建项目
25
24
 
26
25
  ```bash
27
- cj0 create my-server
26
+ npx cj0 create my-server
28
27
  cd my-server
29
28
  npm install
30
- npm run dev # 本地预览
29
+ npx cj0 setup # 交互配置 ENV:vars 写 wrangler.jsonc,密钥写 .dev.vars
30
+ npm run dev
31
31
  ```
32
32
 
33
- 部署前需要完成以下配置:
33
+ ## 部署
34
34
 
35
35
  ```bash
36
36
  npx wrangler kv namespace create CAJONZERO_KV # 把返回的 id 填进 wrangler.jsonc
@@ -41,8 +41,8 @@ npm run deploy
41
41
 
42
42
  ## 配置地图
43
43
 
44
- 地图文件需要单独上传,不随 worker 打包。把解包后的 `.boxplay` 目录上传到
45
- OSS/S3/CDN,然后在 `cajonzero/maps.json` 中登记:
44
+ 地图文件单独上传,不随 worker 打包。把解包后的 `.boxplay` 目录上传到
45
+ OSS/S3/CDN(需允许 CORS),然后在 `cajonzero/maps.json` 中登记:
46
46
 
47
47
  ```json
48
48
  {
@@ -58,17 +58,15 @@ OSS/S3/CDN,然后在 `cajonzero/maps.json` 中登记:
58
58
  }
59
59
  ```
60
60
 
61
- 地图文件由客户端和房间直接向 CDN 拉取,服务端不做缓存。
62
- 更新地图后请刷新 CDN 缓存,或改用带版本的路径。
61
+ `root` 必须以 `/` 结尾;更新地图后刷新 CDN 缓存或改用带版本的路径。
63
62
  房间参数、地图拉取策略等配置位于 `cajonzero/config.json`。
64
63
 
65
64
  ## 常用命令
66
65
 
67
66
  ```bash
68
- cj0 serve [--port <port>] [--host <host>] # 本地启动
69
67
  cj0 create <folder> # 生成部署项目
68
+ cj0 setup [--config <file>] # 交互配置必需/可选的 ENV
69
+ cj0 serve [--port <port>] [--host <host>] # 本地启动
70
70
  npm test # 运行测试
71
71
  npm run ci:gates # 完整检查(类型/测试/构建)
72
72
  ```
73
-
74
- 详细说明见 [`docs/`](./docs/)。
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";
@@ -51,7 +53,7 @@ var SCAFFOLD_ENTRY_CONTENT = [
51
53
  " };",
52
54
  " },",
53
55
  "});",
54
- 'export { Room } from "cajonzero";',
56
+ 'export { Room } from "cajonzero/serve";',
55
57
  ""
56
58
  ].join("\n");
57
59
  var SCAFFOLD_CONFIG_CONTENT = `${JSON.stringify(
@@ -156,82 +158,49 @@ function readme(options) {
156
158
  return [
157
159
  `# ${options.packageName}`,
158
160
  "",
159
- "A serverless (Cloudflare Workers) BOX3 **play** server, scaffolded by",
160
- "[`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",
161
163
  "",
162
- "This project contains **no engine source code** \u2014 configuration plus a short",
163
- "worker entry that hands `cajonzero/config.json` and `cajonzero/maps.json` to the",
164
- "runtime:",
165
- "",
166
- "```ts",
167
- 'import config from "../cajonzero/config.json";',
168
- 'import maps from "../cajonzero/maps.json";',
169
- 'import { serve } from "cajonzero/serve";',
170
- "",
171
- "export default serve({",
172
- " config,",
173
- " maps,",
174
- " storage: (env) => /* any unstorage driver over env bindings */,",
175
- " sessionSecret: (env) => env.CAJONZERO_SESSION_SECRET,",
176
- " // adminToken / environment / rooms / settings \u2026",
177
- "});",
178
- 'export { Room } from "cajonzero";',
179
- "```",
180
- "",
181
- "## Quick start",
164
+ "## \u5FEB\u901F\u5F00\u59CB",
182
165
  "",
183
166
  "```bash",
184
- "npm install # also syncs cajonzero's prebuilt lobby/console into dist/ui",
185
- "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",
186
170
  "```",
187
171
  "",
188
- "`npm run build` validates the deploy by bundling the worker without",
189
- "publishing (`wrangler deploy --dry-run`).",
190
- "",
191
- "## Environment",
192
- "",
193
- "Every value the runtime consumes is passed explicitly in `src/index.ts`;",
194
- "the package never guesses env var names.",
195
- "",
196
- "| Name | Kind | Required | Purpose |",
197
- "|---|---|---|---|",
198
- "| `CAJONZERO_KV` | KV binding | yes | player/account data + map-script KV |",
199
- "| `ROOMS` | Durable Object binding | yes | room actors |",
200
- "| `CAJONZERO_SESSION_SECRET` | secret | yes | signs session proofs and login cookies |",
201
- "| `BOX_PLAY_ADMIN_TOKEN` | secret | yes | owner console and `/api/admin/*` |",
202
- "| `CAJONZERO_ENV` | var | no | deployment label (`development` relaxes cookie `Secure`) |",
203
- "| `CAJONZERO_PLAYER_LIMIT` | var | no | maximum players per room |",
204
- "| `CAJONZERO_ZERO_PLAYER_TIMEOUT_MS` | var | no | empty-room grace window |",
205
- "| `CAJONZERO_AUTOSAVE_INTERVAL_MS` | var | no | snapshot cadence |",
206
- "| `CAJONZERO_DEBUG` | var | no | verbose logging |",
207
- "| `CAJONZERO_TELEPORT_ACCESS` | var | no | allow non-team containers |",
208
- "| `CAJONZERO_PAID_ACCESS` | var | no | restrict joins to paid containers |",
209
- "| `CAJONZERO_SCRIPT_HEAP_MB` | var | no | QuickJS heap per script host |",
210
- "| `CAJONZERO_SCRIPT_TIMEOUT_MS` | var | no | script invocation timeout |",
211
- "| `CAJONZERO_DB_SIZE_LIMIT` | var | no | map-script storage budget |",
172
+ "## \u73AF\u5883\u53D8\u91CF",
212
173
  "",
213
- "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",
214
176
  "",
215
177
  "```bash",
216
- "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",
217
179
  "npx wrangler secret put CAJONZERO_SESSION_SECRET",
218
180
  "npx wrangler secret put BOX_PLAY_ADMIN_TOKEN",
219
181
  "```",
220
182
  "",
221
- "## Maps (object storage)",
222
- "",
223
- "Maps are **not** uploaded with the worker and are never unpacked at runtime.",
224
- "Unpack the `.boxplay` yourself and upload the resulting directory to OSS/S3/CDN",
225
- "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 |",
226
199
  "",
227
- "```text",
228
- "<root>/bundle.json",
229
- "<root>/worlds/<worldId>/manifest.json",
230
- "<root>/worlds/<worldId>/data/project-state.json",
231
- "<root>/worlds/<worldId>/data/blobs/<cid>",
232
- "```",
200
+ "## \u5730\u56FE",
233
201
  "",
234
- "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",
235
204
  "",
236
205
  "```json",
237
206
  "{",
@@ -241,30 +210,21 @@ function readme(options) {
241
210
  ' "slug": "omega",',
242
211
  ' "root": "https://cdn.example.com/cajonzero/omega/",',
243
212
  ' "worldId": "project-omega",',
244
- ' "name": "Omega City Reloaded",',
245
- ' "version": "1.0.4"',
213
+ ' "name": "Omega City Reloaded"',
246
214
  " }",
247
215
  " ]",
248
216
  "}",
249
217
  "```",
250
218
  "",
251
- "- `root` must be the unpacked map directory and end with `/`.",
252
- "- The browser loads the map files directly, so the bucket must allow CORS",
253
- " (`Access-Control-Allow-Origin`) for this deployment's origin.",
254
- "- Re-uploaded maps need a CDN cache refresh (or a versioned path).",
219
+ "`root` \u5FC5\u987B\u4EE5 `/` \u7ED3\u5C3E\uFF1B\u66F4\u65B0\u5730\u56FE\u540E\u5237\u65B0 CDN \u7F13\u5B58\u6216\u6362\u8DEF\u5F84\u3002",
255
220
  "",
256
- "## Where things live",
257
- "",
258
- "- **Play lobby** \u2014 the worker serves the lobby at `/`.",
259
- "- **Admin console** \u2014 at `/admin` (sign in with `BOX_PLAY_ADMIN_TOKEN`) for",
260
- " player bans and runtime settings; its JSON API is under `/api/admin/*`.",
261
- "- **Map catalog** \u2014 `GET /api/maps` returns the entries from `maps.json`.",
262
- "",
263
- "## Deploy",
221
+ "## \u90E8\u7F72",
264
222
  "",
265
223
  "```bash",
266
- "npm run deploy # wrangler deploy",
224
+ "npm run deploy",
267
225
  "```",
226
+ "",
227
+ "\u5927\u5385\u5728 `/`\uFF0C\u7BA1\u7406\u540E\u53F0\u5728 `/admin`\u3002",
268
228
  ""
269
229
  ].join("\n");
270
230
  }
@@ -306,7 +266,7 @@ function readCajonzeroPackage(manifestPath) {
306
266
  return { version, wranglerRange, unstorageRange };
307
267
  }
308
268
  function toPackageName(folder) {
309
- const base = path.basename(folder);
269
+ const base = path3.basename(folder);
310
270
  const slug = base.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-._]+/, "").replace(/[-._]+$/, "");
311
271
  return slug.length > 0 ? slug : "cajonzero-app";
312
272
  }
@@ -321,7 +281,7 @@ function createProject(folder, options = {}) {
321
281
  throw new ScaffoldError("INVALID_TARGET", "create requires a target folder");
322
282
  }
323
283
  const cwd = options.cwd ?? process.cwd();
324
- const target = path.resolve(cwd, name);
284
+ const target = path3.resolve(cwd, name);
325
285
  if (existsSync(target) && !statSync(target).isDirectory()) {
326
286
  throw new ScaffoldError("INVALID_TARGET", `target exists and is not a directory: ${target}`);
327
287
  }
@@ -344,12 +304,280 @@ function createProject(folder, options = {}) {
344
304
  });
345
305
  mkdirSync(target, { recursive: true });
346
306
  for (const file of files) {
347
- const fullPath = path.join(target, file.path);
348
- mkdirSync(path.dirname(fullPath), { recursive: true });
307
+ const fullPath = path3.join(target, file.path);
308
+ mkdirSync(path3.dirname(fullPath), { recursive: true });
349
309
  writeFileSync(fullPath, file.content, "utf8");
350
310
  }
351
311
  return { target, files: files.map((file) => file.path) };
352
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
+ }
353
581
 
354
582
  // src/bin/cj0.ts
355
583
  var USAGE = `cj0 \u2014 serverless (Cloudflare) play-only BOX3 engine
@@ -359,6 +587,7 @@ Usage:
359
587
 
360
588
  Commands:
361
589
  create <folder> Scaffold a deployable project
590
+ setup [--config <file>] Configure required/optional ENV values
362
591
  serve [--port <port>] [--host <host>] Run the cajonzero server locally (workerd)
363
592
 
364
593
  Serve options:
@@ -408,10 +637,94 @@ function runCreate(args) {
408
637
  process.stdout.write(
409
638
  `created project in ${result.target} (${result.files.length} files)
410
639
  ${result.files.join(", ")}
411
- next: cd ${folder} && npm install && npm run dev
640
+ next: cd ${folder} && npm install && npx cj0 setup && npm run dev
412
641
  `
413
642
  );
414
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
+ }
415
728
  function readFlagValue(args, index, flag) {
416
729
  const inline = args[index];
417
730
  if (inline !== void 0 && inline.startsWith(`${flag}=`)) {
@@ -460,8 +773,8 @@ async function runServe(args) {
460
773
  }
461
774
  const assetsDir = config.assets?.directory;
462
775
  if (typeof assetsDir === "string" && assetsDir.length > 0) {
463
- const configDir = path.dirname(path.resolve(process.cwd(), flags.config));
464
- 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 });
465
778
  }
466
779
  const secrets = localDevVars();
467
780
  const worker = await unstable_dev(entry, {
@@ -520,6 +833,9 @@ async function main(argv) {
520
833
  case "create":
521
834
  runCreate(rest);
522
835
  return;
836
+ case "setup":
837
+ await runSetup(rest);
838
+ return;
523
839
  default:
524
840
  process.stderr.write(`cajonzero: unknown command "${command}"
525
841
 
@@ -1,4 +1,4 @@
1
- import { T as TimestampedClientInput, N as NetPublic, O as NetSecret } from './providers-Dh-umPQ6.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).