sesame-kit 0.4.0 → 0.4.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.ja.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  # sesame-kit — SESAME クラウド CLI & ライブラリ (非公式)
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/sesame-kit)](https://www.npmjs.com/package/sesame-kit) [![license](https://img.shields.io/npm/l/sesame-kit)](./LICENSE) [![node](https://img.shields.io/node/v/sesame-kit)](https://nodejs.org)
6
+
5
7
  > English: [README.md](./README.md)
6
8
 
7
9
  > **ステータス** — pre-1.0 でありバグが残っている可能性があります。実運用で概ね安定が確認できた時点で 1.0 にします。依存する場合はバージョンを固定してください。
@@ -36,14 +38,17 @@
36
38
  要件は Node.js 18 以上 (ESM / `node:` プロトコルを使用)。
37
39
 
38
40
  ```bash
39
- git clone https://github.com/FukumotoIkuma/sesame-kit.git
40
- cd sesame-kit
41
- npm install
42
- npm link # グローバルに `sesame` コマンドを公開
43
- # あるいは: node bin/sesame.js ...
41
+ npm install -g sesame-kit # グローバル CLI: `sesame ...`
42
+ npx sesame-kit --help # インストールせず実行
43
+ npm install sesame-kit # プロジェクトにライブラリとして追加
44
44
  ```
45
45
 
46
- ライブラリとして使う場合は `npm link sesame-kit`、または `npm install /path/to/sesame-kit`。
46
+ ソースから:
47
+
48
+ ```bash
49
+ git clone https://github.com/FukumotoIkuma/sesame-kit.git
50
+ cd sesame-kit && npm install && npm link
51
+ ```
47
52
 
48
53
  ---
49
54
 
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  # sesame-kit — SESAME cloud CLI & library (unofficial)
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/sesame-kit)](https://www.npmjs.com/package/sesame-kit) [![license](https://img.shields.io/npm/l/sesame-kit)](./LICENSE) [![node](https://img.shields.io/node/v/sesame-kit)](https://nodejs.org)
6
+
5
7
  A Node.js CLI and library that drives the SESAME cloud WebSocket API using the same Cognito consumer client as the official SESAME iOS / Android apps. It covers lock control, Hub3 IR (emit and learn), device management, history, and battery level. With `sesame serve` it exposes every feature as JSON-RPC so you can drive SESAME from any language.
6
8
 
7
9
  > 日本語版: [README.ja.md](./README.ja.md)
@@ -36,14 +38,17 @@ A Node.js port of the official biz3 admin web app ([CANDY-HOUSE/biz.candyhouse.c
36
38
  Requires Node.js 18+ (uses ESM and the `node:` protocol).
37
39
 
38
40
  ```bash
39
- git clone https://github.com/FukumotoIkuma/sesame-kit.git
40
- cd sesame-kit
41
- npm install
42
- npm link # expose the `sesame` command globally
43
- # or: node bin/sesame.js ...
41
+ npm install -g sesame-kit # global CLI: `sesame ...`
42
+ npx sesame-kit --help # or run without installing
43
+ npm install sesame-kit # or as a library in your project
44
44
  ```
45
45
 
46
- To use it as a library: `npm link sesame-kit`, or `npm install /path/to/sesame-kit`.
46
+ From source:
47
+
48
+ ```bash
49
+ git clone https://github.com/FukumotoIkuma/sesame-kit.git
50
+ cd sesame-kit && npm install && npm link
51
+ ```
47
52
 
48
53
  ---
49
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sesame-kit",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "SESAME cloud CLI & library (lock control + Hub3 IR + device management). Node.js port of biz3 React app with the Cognito consumer client swapped in for long-lived sessions.",
5
5
  "author": "Ikuma Fukumoto",
6
6
  "license": "MIT",
@@ -85,7 +85,8 @@
85
85
  "build:rpc-schema": "node scripts/gen-rpc-schema.mjs",
86
86
  "build:grpc-proto": "node scripts/gen-grpc-proto.mjs",
87
87
  "build": "tsc -p tsconfig.json && node scripts/gen-rpc-schema.mjs && node scripts/gen-grpc-proto.mjs",
88
- "prepack": "tsc -p tsconfig.json && node scripts/gen-rpc-schema.mjs && node scripts/gen-grpc-proto.mjs"
88
+ "prepack": "tsc -p tsconfig.json && node scripts/gen-rpc-schema.mjs && node scripts/gen-grpc-proto.mjs",
89
+ "release": "bash scripts/release.sh"
89
90
  },
90
91
  "dependencies": {
91
92
  "@aws-sdk/client-cognito-identity-provider": "^3.1057.0",
package/src/cli.js CHANGED
@@ -1508,6 +1508,10 @@ export async function run(argv = process.argv) {
1508
1508
  .name("sesame")
1509
1509
  .description("SESAME cloud CLI: lock control + Hub3 IR + device management (port of biz3 React with Consumer Cognito client)")
1510
1510
  .version(getPkgVersion(), "-V, --version")
1511
+ // 引数不足/未知オプション時に usage を出す (commander 既定はエラー1行のみで不親切)。
1512
+ // この前に設定すると後で追加する全サブコマンドへ継承される。--json 時は writeErr 側で抑止。
1513
+ .showHelpAfterError()
1514
+ .showSuggestionAfterError()
1511
1515
  .option("--config-dir <path>", "設定ディレクトリ上書き (default: ~/.config/sesame-hub3)")
1512
1516
  .option("--debug", "詳細ログ")
1513
1517
  .option("--json", "JSON 出力");
@@ -1751,9 +1755,11 @@ devices だけで完結します (手入力は呼び名のみ):
1751
1755
  // BLE 権限/電源エラーは macOS なら該当設定ペインを自動で開いて誘導する。
1752
1756
  if (maybeHandleBleError(err)) { finishCli(); return; }
1753
1757
  const code = (typeof err.exitCode === "number" && err.exitCode !== 0) ? err.exitCode : 1;
1754
- // commander の usage エラーは非 JSON 時すでに stderr へ整形済み (usage 付き) なので二重出力を避ける。
1755
- if (typeof err.code === "string" && err.code.startsWith("commander.") && !CLI_JSON) {
1756
- process.exitCode = code; finishCli(); return;
1758
+ // commander の usage エラー。非 JSON 時は commander が stderr に整形済み (usage 付き) なので二重出力を避ける。
1759
+ if (typeof err.code === "string" && err.code.startsWith("commander.")) {
1760
+ if (!CLI_JSON) { process.exitCode = code; finishCli(); return; }
1761
+ // --json: commander のメッセージ先頭 "error: " を剥がして封筒に載せる (error が二重にならないように)。
1762
+ die((err.message || "usage error").replace(/^error:\s*/i, ""), code); return;
1757
1763
  }
1758
1764
  die(withStaleHint(err.message || String(err)), code);
1759
1765
  }
package/src/session-ui.js CHANGED
@@ -5,6 +5,9 @@
5
5
  // 状態が変わった瞬間に描き直す Ink (React for CLI) で実装する。BLE の mechStatus publish や
6
6
  // バックグラウンド接続の完了を `bus` の "update" イベントで受け、React state を更新して再描画する。
7
7
  //
8
+ // 操作: ↑↓ 移動 / → か Enter で決定 / ← か Esc で戻る / q で終了。
9
+ // アクション実行後はホームに戻らず、その操作メニューに留まる (続けて操作できる)。
10
+ //
8
11
  // JSX は使わない (本リポは src を素の ESM で実行しビルド工程が無いため)。React.createElement = h。
9
12
 
10
13
  import React from "react";
@@ -43,6 +46,7 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
43
46
  const [numVal, setNumVal] = React.useState(""); // autolock 秒数 / LED duty 入力
44
47
  const [selRemote, setSelRemote] = React.useState(null); // IR: 選択中リモコン
45
48
  const [irKeys, setIrKeys] = React.useState(null); // IR: 取得したキー一覧 (null=未取得)
49
+ const [hi, setHi] = React.useState(null); // → 決定用: ハイライト中の項目
46
50
 
47
51
  // BLE onStatus / 背景接続完了 → 再描画。
48
52
  React.useEffect(() => {
@@ -51,18 +55,82 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
51
55
  return () => bus.off("update", on);
52
56
  }, [bus]);
53
57
 
58
+ // hi (→ 決定用ハイライト) は各 SelectInput の onHighlight が先頭項目で更新する。
59
+ // SelectInput を描画しない可能性のある mode (空の IR リスト) に入る時だけ明示的にクリアし、
60
+ // 前メニューの項目が残って → が誤爆しないようにする (下の selectAction / selectIrRemote 参照)。
61
+
54
62
  const backToActions = () => { setMode("actions"); };
55
63
 
56
- // q / Esc。Esc は深い階層では1つ戻る、devices(or single actions)では終了。
64
+ // ---- 各メニューの選択ハンドラ (SelectInput.onSelect 決定の両方から呼ぶ) ----
65
+ const selectDevice = (it) => {
66
+ if (!it) return;
67
+ if (it.value === "__quit") { exit(); return; }
68
+ setSelName(it.value); setMsg(""); setMode("actions");
69
+ };
70
+ const selectAction = (it) => {
71
+ if (!it) return;
72
+ if (it.value === "__back") { if (single) exit(); else { setMode("devices"); setMsg(""); } return; }
73
+ if (it.value === "autolock") { setNumVal(""); setMode("autolock"); return; }
74
+ if (it.value === "led") { setNumVal(""); setMode("led"); return; }
75
+ if (it.value === "ir") { setHi(null); setSelRemote(null); setIrKeys(null); setMode("ir-remote"); return; }
76
+ runExec(it.value, devices.get(selName));
77
+ };
78
+ const selectIrRemote = (it) => {
79
+ if (!it) return;
80
+ if (it.value === "__back") { backToActions(); return; }
81
+ setHi(null); setSelRemote(it.value); setIrKeys(null); setMode("ir-key");
82
+ Promise.resolve(listKeysFor ? listKeysFor(it.value) : []).then(setIrKeys).catch(() => setIrKeys([]));
83
+ };
84
+ const selectIrKey = (it) => {
85
+ if (!it) return;
86
+ if (it.value === "__back") { setMode("ir-remote"); return; }
87
+ runExec("ir", devices.get(selName), { remote: selRemote, key: it.value });
88
+ };
89
+
90
+ // ← / Esc で 1 つ戻る。Esc は最上位 (devices / single の actions) では終了する。
91
+ // ← は最上位では何もしない (誤操作で終了しないように)。
92
+ const goBack = (allowExitAtTop) => {
93
+ if (mode === "actions") {
94
+ if (single) { if (allowExitAtTop) exit(); }
95
+ else { setMode("devices"); setMsg(""); }
96
+ } else if (mode === "ir-key") setMode("ir-remote");
97
+ else if (mode === "devices") { if (allowExitAtTop) exit(); }
98
+ else backToActions(); // autolock / led / ir-remote
99
+ };
100
+
101
+ // 現在 mode のメニュー項目 (render と → 決定で共有。順序が両者で一致する)。
102
+ const menuItems = () => {
103
+ if (mode === "devices") return [...names.map((n) => ({ label: n, value: n })), { label: "終了", value: "__quit" }];
104
+ if (mode === "actions") return [...actionsFor(devices.get(selName)), { label: single ? "終了" : "← 戻る", value: "__back" }];
105
+ if (mode === "ir-remote") {
106
+ const r = hub3RemotesFor ? hub3RemotesFor(devices.get(selName)) : [];
107
+ return r.length ? [...r, { label: "← 戻る", value: "__back" }] : [];
108
+ }
109
+ if (mode === "ir-key") return (irKeys && irKeys.length) ? [...irKeys, { label: "← 戻る", value: "__back" }] : [];
110
+ return [];
111
+ };
112
+
113
+ // → で決定。ink-select-input の onHighlight は初期項目では発火しないため、移動前は hi=null。
114
+ // その場合は先頭 (= 既定でハイライトされている項目) にフォールバックする。
115
+ const goForward = () => {
116
+ const it = hi || menuItems()[0];
117
+ if (!it) return;
118
+ if (mode === "devices") selectDevice(it);
119
+ else if (mode === "actions") selectAction(it);
120
+ else if (mode === "ir-remote") selectIrRemote(it);
121
+ else if (mode === "ir-key") selectIrKey(it);
122
+ };
123
+
124
+ const isList = mode === "devices" || mode === "actions" || mode === "ir-remote" || mode === "ir-key";
125
+
57
126
  useInput((input, key) => {
58
127
  if (mode === "busy") return;
59
- if (input === "q") { exit(); return; }
60
- if (key.escape) {
61
- if (mode === "actions") { if (single) exit(); else { setMode("devices"); setMsg(""); } }
62
- else if (mode === "ir-key") { setMode("ir-remote"); }
63
- else if (mode === "devices") exit();
64
- else { backToActions(); } // autolock / led / ir-remote
65
- }
128
+ // q は**メニュー系のみ**で終了。autolock/LED の数値入力中は文字として TextInput へ渡す。
129
+ if (input === "q" && isList) { exit(); return; }
130
+ if (key.escape) { goBack(true); return; } // Esc: 戻る (最上位では終了)
131
+ // / は数値入力 (autolock/led) ではテキストカーソル移動に使うので、リスト系のみで奪う。
132
+ if (isList && key.leftArrow) { goBack(false); return; } // ←: 戻る (最上位では何もしない)
133
+ if (isList && key.rightArrow) { goForward(); return; } // →: 決定
66
134
  });
67
135
 
68
136
  const runExec = (op, d, extra) => {
@@ -70,10 +138,10 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
70
138
  exec(op, d, extra)
71
139
  .then((m) => setMsg(m))
72
140
  .catch((e) => setMsg(`error: ${e?.message || e}`))
73
- .finally(() => setMode(single ? "actions" : "devices"));
141
+ .finally(() => setMode("actions")); // ホームに戻らず操作メニューに留まる (続けて操作できる)
74
142
  };
75
143
 
76
- // ヘッダ: 全デバイスの現在状態 (ライブ)
144
+ // ヘッダ: 全デバイスの現在状態 (ライブ) + 操作ヒント。
77
145
  const header = h(
78
146
  Box,
79
147
  { flexDirection: "column" },
@@ -85,6 +153,7 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
85
153
  return h(Text, { key: n, color: d.ble ? "green" : undefined },
86
154
  ` ${n} [${label}·${tag}]: ${fmtState(d)}`);
87
155
  }),
156
+ h(Text, { dimColor: true }, " ↑↓ 移動 → 決定 ← 戻る q 終了"),
88
157
  msg ? h(Text, { color: "yellow" }, msg) : null,
89
158
  );
90
159
  const box = (...kids) => h(Box, { flexDirection: "column" }, header, ...kids);
@@ -116,66 +185,30 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
116
185
  const d = devices.get(selName);
117
186
  const remotes = (hub3RemotesFor ? hub3RemotesFor(d) : []);
118
187
  if (remotes.length === 0) {
119
- return box(h(Text, null, `${selName}: 登録リモコンがありません ( sesame remote add で登録 )Esc で戻る`));
188
+ return box(h(Text, null, `${selName}: 登録リモコンがありません ( sesame remote add で登録 )。← / Esc で戻る`));
120
189
  }
121
- const items = [...remotes, { label: "← 戻る", value: "__back" }];
122
190
  return box(h(Text, null, `${selName} の IR: リモコン選択`),
123
- h(SelectInput, {
124
- items,
125
- onSelect: (it) => {
126
- if (it.value === "__back") { backToActions(); return; }
127
- setSelRemote(it.value); setIrKeys(null); setMode("ir-key");
128
- Promise.resolve(listKeysFor ? listKeysFor(it.value) : []).then(setIrKeys).catch(() => setIrKeys([]));
129
- },
130
- }),
191
+ h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectIrRemote }),
131
192
  );
132
193
  }
133
194
 
134
195
  // IR: キー選択 (非同期取得中はローディング表示)。
135
196
  if (mode === "ir-key") {
136
- const d = devices.get(selName);
137
197
  if (irKeys === null) return box(h(Text, null, `${selRemote}: キー取得中...`));
138
- if (irKeys.length === 0) return box(h(Text, null, `${selRemote}: キーがありません ( sesame remote sync-keys )Esc で戻る`));
139
- const items = [...irKeys, { label: "← 戻る", value: "__back" }];
198
+ if (irKeys.length === 0) return box(h(Text, null, `${selRemote}: キーがありません ( sesame remote sync-keys )。← / Esc で戻る`));
140
199
  return box(h(Text, null, `${selRemote} のキー選択 (送信)`),
141
- h(SelectInput, {
142
- items,
143
- onSelect: (it) => {
144
- if (it.value === "__back") { setMode("ir-remote"); return; }
145
- runExec("ir", d, { remote: selRemote, key: it.value });
146
- },
147
- }),
200
+ h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectIrKey }),
148
201
  );
149
202
  }
150
203
 
151
204
  if (mode === "actions") {
152
- const d = devices.get(selName);
153
- const items = [...actionsFor(d)];
154
- items.push({ label: single ? "終了" : "← 戻る", value: "__back" });
155
205
  return box(h(Text, null, `${selName} の操作:`),
156
- h(SelectInput, {
157
- items,
158
- onSelect: (it) => {
159
- if (it.value === "__back") { if (single) exit(); else { setMode("devices"); setMsg(""); } return; }
160
- if (it.value === "autolock") { setNumVal(""); setMode("autolock"); return; }
161
- if (it.value === "led") { setNumVal(""); setMode("led"); return; }
162
- if (it.value === "ir") { setSelRemote(null); setIrKeys(null); setMode("ir-remote"); return; }
163
- runExec(it.value, d);
164
- },
165
- }),
206
+ h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectAction }),
166
207
  );
167
208
  }
168
209
 
169
210
  // mode === "devices"
170
- const items = names.map((n) => ({ label: n, value: n }));
171
- items.push({ label: "終了", value: "__quit" });
172
211
  return box(h(Text, null, "操作するデバイス:"),
173
- h(SelectInput, {
174
- items,
175
- onSelect: (it) => {
176
- if (it.value === "__quit") { exit(); return; }
177
- setSelName(it.value); setMsg(""); setMode("actions");
178
- },
179
- }),
212
+ h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectDevice }),
180
213
  );
181
214
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.js"],"names":[],"mappings":"AA+9CA,oDAiQC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.js"],"names":[],"mappings":"AA+9CA,oDAuQC"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-ui.d.ts","sourceRoot":"","sources":["../src/session-ui.js"],"names":[],"mappings":"AAgBA;;;;;;;;;;GAUG;AACH,oCATW;IACN,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,GAAG,EAAC,MAAM,GAAC,IAAI,CAAA;KAAC,CAAC,CAAC;IACtD,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,EAAE,OAAO,aAAa,EAAE,YAAY,CAAC;IACxC,IAAI,EAAE,CAAC,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,MAAM,EAAE,OAAO,CAAC,EAAC,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,UAAU,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,KAAK,CAAC;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,KAAK,EAAC,MAAM,CAAA;KAAC,CAAC,CAAC;IACjE,QAAQ,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,MAAM,CAAC;CACnC,iBAKH;AAED;;;;;;;;;QAoJC"}
1
+ {"version":3,"file":"session-ui.d.ts","sourceRoot":"","sources":["../src/session-ui.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;GAUG;AACH,oCATW;IACN,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,GAAG,EAAC,MAAM,GAAC,IAAI,CAAA;KAAC,CAAC,CAAC;IACtD,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,EAAE,OAAO,aAAa,EAAE,YAAY,CAAC;IACxC,IAAI,EAAE,CAAC,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,MAAM,EAAE,OAAO,CAAC,EAAC,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,UAAU,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,KAAK,CAAC;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,KAAK,EAAC,MAAM,CAAA;KAAC,CAAC,CAAC;IACjE,QAAQ,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,MAAM,CAAC;CACnC,iBAKH;AAED;;;;;;;;;QAkLC"}