@uzuhq/code-cli 0.5.2 → 0.5.4

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/cli.js CHANGED
@@ -16,6 +16,7 @@ import { buildServerLogic } from './build-server-logic.js';
16
16
  import { create2dGame } from './create-2d-game.js';
17
17
  import { uploadIconsToCfImages } from './cf-images-upload.js';
18
18
  import { runDevCommand } from './dev.js';
19
+ import { readInstalledSdkVersion } from './sdk-version.js';
19
20
  import { DEFAULT_ENV, authBaseURL, resolveEnv, studioHost } from './auth/env.js';
20
21
  import { runLoginFlow } from './auth/login-flow.js';
21
22
  import { saveCredentials, clearCredentials, credentialsPath } from './auth/config.js';
@@ -23,27 +24,6 @@ import { getLoginIdToken, getValidIdToken } from './auth/token-cache.js';
23
24
  import { PUBLISH_TOKEN_ENV, createPublishToken, listPublishTokens, revokePublishToken, } from './auth/publish-token.js';
24
25
  // publish / login / logout 共通の env オプション。既定 dev・help には出さない (誤って本番へ publish しないため)。
25
26
  const envOption = () => new Option('--env <env>', '接続先環境 (dev | stg | prd)').default(DEFAULT_ENV).hideHelp();
26
- /**
27
- * ゲームの node_modules から SDK の実バージョンを読む。
28
- * 開発者の自己申告ではなく install 済み実体から測ることで、backend に記録される
29
- * sdkVersion (ホスト側のサポート範囲判定に使う) の信頼性を担保する。
30
- * @uzupj/uzu-sdk は旧パッケージ名 (未移行 scenario 向けの両対応)。
31
- */
32
- const readInstalledSdkVersion = (cwd) => {
33
- const candidates = [
34
- resolve(cwd, 'node_modules', '@uzuhq', 'code-sdk', 'package.json'),
35
- resolve(cwd, 'node_modules', '@uzupj', 'uzu-sdk', 'package.json'),
36
- ];
37
- const pkgPath = candidates.find(existsSync);
38
- if (!pkgPath) {
39
- throw new Error('@uzuhq/code-sdk が node_modules に見つかりません。依存を install してから publish してください。');
40
- }
41
- const version = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
42
- if (!version) {
43
- throw new Error(`${pkgPath} に version がありません。`);
44
- }
45
- return version;
46
- };
47
27
  /** publish に使われている uzu-cli 自身のバージョン (dist/../package.json)。 */
48
28
  const readOwnCliVersion = () => {
49
29
  const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
@@ -290,6 +290,17 @@ export class GameRoom {
290
290
  this.ensureTickLoop();
291
291
  }
292
292
  async handleMessage(ws, msg) {
293
+ // SDK は生文字列で送受信する (本番の setWebSocketAutoResponse と同じ wire 形式)。
294
+ // JSON.parse より前に返さないと heartbeat が落ちて 35 秒ごとに全席が再接続する。
295
+ if (msg === '__ping') {
296
+ try {
297
+ ws.send('__pong');
298
+ }
299
+ catch {
300
+ /* disconnected */
301
+ }
302
+ return;
303
+ }
293
304
  const attachment = this.attachments.get(ws);
294
305
  if (!attachment)
295
306
  return;
@@ -302,10 +313,6 @@ export class GameRoom {
302
313
  return;
303
314
  }
304
315
  const msgType = parsed.type;
305
- if (msgType === '__ping') {
306
- this.sendTo(ws, { type: '__pong' });
307
- return;
308
- }
309
316
  console.log(`[GameRoom] ⬅ recv from=${senderId} type=${msgType}`);
310
317
  if (msgType === '__action') {
311
318
  if (!this.gameState) {
@@ -25,6 +25,17 @@ export class RelayRoom {
25
25
  ws.send(JSON.stringify({ type: '__room_init', myId: playerId }));
26
26
  }
27
27
  handleMessage(ws, msg) {
28
+ // SDK は生文字列で送受信する (本番の setWebSocketAutoResponse と同じ wire 形式)。
29
+ // JSON.parse より前に返さないと heartbeat が落ちて 35 秒ごとに全席が再接続する。
30
+ if (msg === '__ping') {
31
+ try {
32
+ ws.send('__pong');
33
+ }
34
+ catch {
35
+ /* disconnected */
36
+ }
37
+ return;
38
+ }
28
39
  const attachment = this.attachments.get(ws);
29
40
  if (!attachment)
30
41
  return;
@@ -36,15 +47,6 @@ export class RelayRoom {
36
47
  catch {
37
48
  return;
38
49
  }
39
- if (parsed.type === '__ping') {
40
- try {
41
- ws.send(JSON.stringify({ type: '__pong' }));
42
- }
43
- catch {
44
- /* disconnected */
45
- }
46
- return;
47
- }
48
50
  console.log(`[RelayRoom] ⬅ recv from=${senderId}`, JSON.stringify(parsed));
49
51
  const outData = JSON.stringify({ ...parsed, __from: senderId });
50
52
  if (parsed.__to && typeof parsed.__to === 'string') {
@@ -78,6 +78,17 @@ export class SyncRoom {
78
78
  }
79
79
  }
80
80
  handleMessage(ws, msg) {
81
+ // SDK は生文字列で送受信する (本番の setWebSocketAutoResponse と同じ wire 形式)。
82
+ // JSON.parse より前に返さないと heartbeat が落ちて 35 秒ごとに全席が再接続する。
83
+ if (msg === '__ping') {
84
+ try {
85
+ ws.send('__pong');
86
+ }
87
+ catch {
88
+ /* disconnected */
89
+ }
90
+ return;
91
+ }
81
92
  const attachment = this.attachments.get(ws);
82
93
  if (!attachment)
83
94
  return;
@@ -90,15 +101,6 @@ export class SyncRoom {
90
101
  return;
91
102
  }
92
103
  const msgType = parsed.type;
93
- if (msgType === '__ping') {
94
- try {
95
- ws.send(JSON.stringify({ type: '__pong' }));
96
- }
97
- catch {
98
- /* disconnected */
99
- }
100
- return;
101
- }
102
104
  console.log(`[SyncRoom] ⬅ recv from=${senderId} type=${msgType}`);
103
105
  if (msgType === '__init_state') {
104
106
  if (this.stateInitialized) {
@@ -33,6 +33,14 @@ const PHONE_SVG = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" x
33
33
  <rect x="6.5" y="2" width="11" height="20" rx="2.5" stroke="white" stroke-width="2"/>
34
34
  <line x1="10" y1="18.5" x2="14" y2="18.5" stroke="white" stroke-width="2" stroke-linecap="round"/>
35
35
  </svg>`;
36
+ // 擬似ノッチ表示トグル。 createNotchDecor が実際に描く装飾 (横向き端末・左の
37
+ // Dynamic Island・下中央のホームインジケータ) をそのまま象る。 隣の PHONE_SVG
38
+ // (縦向き) とは向きで見分ける。
39
+ const NOTCH_SVG = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
40
+ <rect x="2" y="4.5" width="20" height="15" rx="3.5" stroke="white" stroke-width="1.8"/>
41
+ <rect x="4.8" y="8.5" width="2.2" height="7" rx="1.1" fill="white"/>
42
+ <line x1="11" y1="17" x2="17" y2="17" stroke="white" stroke-width="1.6" stroke-linecap="round"/>
43
+ </svg>`;
36
44
  /** buildIframeUrl に渡す HUD の避け領域 (Flutter HudInsets と同じ計算)。 */
37
45
  function hudInsetParams(hasMenu, actionCount) {
38
46
  let x = HUD_LEFT;
@@ -429,19 +437,12 @@ function openPlayerSwitcher(opts, currentIndex, anchor) {
429
437
  };
430
438
  setTimeout(() => document.addEventListener('click', close), 0);
431
439
  }
432
- /**
433
- * 各 iframe (= 各プレイヤー画面) の左上に載せる overlay chrome。
434
- * 本番アプリ (Flutter GamePlayScreen) と同じ仕様:
435
- * SafeArea top:8 / left:12 → [UZU ボタン 44] [gap 6] [ActionBar ピル: chat, mic]。
436
- * UZU ボタンは dev メニュー (State/Reset/Emulator) + その player の「スマホで開く」を開く。
437
- * chat / mic は本番の見た目に合わせた dev 表示 (機能は SDK 側が担うため配線しない)。
438
- */
439
440
  function createCellChrome(opts, playerIndex,
440
441
  // facade=true (実機の単体表示) では見た目だけ揃える。 admin channel が無いので
441
442
  // UZU メニュー / 実機 QR は配線せず、 アイコンはタップ無効 (ハリボテ)。
442
443
  facade = false,
443
- // grid では no-op のマイクアイコンを擬似ノッチ表示トグルに転用する。
444
- onMicClick,
444
+ // grid では no-op のマイク枠を擬似ノッチ表示トグルに転用する。
445
+ notchToggle,
445
446
  // 単体表示 (実機) では UZU ボタンをプレイヤー切替に転用する。
446
447
  onUzuClick) {
447
448
  const wrap = document.createElement('div');
@@ -489,17 +490,24 @@ onUzuClick) {
489
490
  'border-radius:18px',
490
491
  'display:flex;align-items:center',
491
492
  ].join(';');
492
- // 1つめ: マイク。 本番は音声トグルだが harness では no-op なので、 grid では
493
- // 擬似ノッチ表示のトグルに転用する (単体表示 facade では装飾のみ)。
494
- const mic = document.createElement(onMicClick ? 'button' : 'div');
495
- mic.innerHTML = MIC_SVG;
496
- mic.title = onMicClick ? '擬似ノッチ表示 ON/OFF' : 'マイク (dev 表示のみ)';
497
- mic.style.cssText = onMicClick
493
+ // 1つめ: 本番は音声トグルだが harness には音声が無い。 grid ではこの枠を擬似ノッチ
494
+ // 表示のトグルに転用し、 アイコンも端末フレームへ差し替える (facade は本番の見た目)。
495
+ const micSlot = document.createElement(notchToggle ? 'button' : 'div');
496
+ micSlot.innerHTML = notchToggle ? NOTCH_SVG : MIC_SVG;
497
+ micSlot.style.cssText = notchToggle
498
498
  ? 'padding:0 10px;height:36px;background:none;border:none;cursor:pointer;display:flex;align-items:center;'
499
499
  : 'padding:0 10px;display:flex;align-items:center;';
500
- if (onMicClick)
501
- mic.onclick = onMicClick;
502
- bar.appendChild(mic);
500
+ if (notchToggle) {
501
+ micSlot.onclick = notchToggle.toggle;
502
+ notchToggle.register((on) => {
503
+ micSlot.style.opacity = on ? '1' : '0.4';
504
+ micSlot.title = `擬似ノッチ表示: ${on ? 'ON' : 'OFF'}`;
505
+ });
506
+ }
507
+ else {
508
+ micSlot.title = 'マイク (dev 表示のみ)';
509
+ }
510
+ bar.appendChild(micSlot);
503
511
  // 2つめ: 携帯 (その player を実機で開く QR)。 facade では装飾のみ。
504
512
  const phone = document.createElement('button');
505
513
  phone.innerHTML = PHONE_SVG;
@@ -528,7 +536,8 @@ export function mountIframeGrid(opts) {
528
536
  const iframeSplashes = new Map();
529
537
  const iframePredictionWarns = new Map();
530
538
  // 擬似ノッチ (各 cell 共通トグル)。 iframe を再生成せず inset だけ切り替える。
531
- let notchSimOn = true;
539
+ // 既定は OFF: ノッチ帯のぶん画面が縮むので、 SafeArea を確認したいときだけ出す。
540
+ let notchSimOn = false;
532
541
  const notchAppliers = [];
533
542
  const toggleNotchSim = () => {
534
543
  notchSimOn = !notchSimOn;
@@ -575,6 +584,7 @@ export function mountIframeGrid(opts) {
575
584
  iframeStatusDots.clear();
576
585
  iframeSplashes.clear();
577
586
  iframePredictionWarns.clear();
587
+ notchAppliers.length = 0;
578
588
  for (let i = 0; i < opts.seats.length; i++) {
579
589
  const seat = seatAt(opts, i);
580
590
  const cell = document.createElement('div');
@@ -616,7 +626,13 @@ export function mountIframeGrid(opts) {
616
626
  screen.appendChild(splash);
617
627
  // admin は端末を模す必要が無いので overlay chrome / 擬似ノッチを載せず素の画面にする。
618
628
  if (seat.kind !== 'admin') {
619
- screen.appendChild(createCellChrome(opts, i, false, toggleNotchSim));
629
+ screen.appendChild(createCellChrome(opts, i, false, {
630
+ toggle: toggleNotchSim,
631
+ register: (apply) => {
632
+ apply(notchSimOn);
633
+ notchAppliers.push(apply);
634
+ },
635
+ }));
620
636
  }
621
637
  // 先読み警告の内訳パネル。 バッジクリックで開閉する。 画面の上に重ねるので
622
638
  // ゲームの見た目は普段どおりのまま、 必要なときだけ前に出る。
@@ -0,0 +1,41 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+ /** 新パッケージ名を先に見る。@uzupj/uzu-sdk は旧名 (未移行 scenario 向けの両対応)。 */
4
+ const SDK_PACKAGE_PATHS = [
5
+ ['@uzuhq', 'code-sdk'],
6
+ ['@uzupj', 'uzu-sdk'],
7
+ ];
8
+ /**
9
+ * ゲームの node_modules から SDK の実バージョンを読む。
10
+ * 開発者の自己申告ではなく install 済み実体から測ることで、backend に記録される
11
+ * sdkVersion (ホスト側のサポート範囲判定に使う) の信頼性を担保する。
12
+ *
13
+ * cwd の node_modules だけでなく親方向へ辿るのは、npm workspaces が依存の実体を
14
+ * リポジトリ root へ hoist するため。manifest.json のある packages/<game>/ で publish しても、
15
+ * そこに node_modules は無い。Node のモジュール解決と同じく「近い node_modules が勝つ」。
16
+ */
17
+ export const readInstalledSdkVersion = (cwd) => {
18
+ const pkgPath = findSdkPackageJson(cwd);
19
+ if (!pkgPath) {
20
+ throw new Error('@uzuhq/code-sdk が node_modules に見つかりません。依存を install してから publish してください。');
21
+ }
22
+ const version = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
23
+ if (!version) {
24
+ throw new Error(`${pkgPath} に version がありません。`);
25
+ }
26
+ return version;
27
+ };
28
+ const findSdkPackageJson = (cwd) => {
29
+ let dir = resolve(cwd);
30
+ for (;;) {
31
+ const found = SDK_PACKAGE_PATHS.map((segments) => resolve(dir, 'node_modules', ...segments, 'package.json')).find(existsSync);
32
+ if (found) {
33
+ return found;
34
+ }
35
+ const parent = dirname(dir);
36
+ if (parent === dir) {
37
+ return undefined;
38
+ }
39
+ dir = parent;
40
+ }
41
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uzuhq/code-cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "UZU ゲーム開発 CLI - ビルド・パブリッシュ・プロジェクト作成ツール",
5
5
  "type": "module",
6
6
  "bin": {