sesame-kit 0.4.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.
Files changed (170) hide show
  1. package/LICENSE +26 -0
  2. package/LICENSE.biz3 +21 -0
  3. package/README.ja.md +225 -0
  4. package/README.md +222 -0
  5. package/bin/sesame.js +8 -0
  6. package/clients/js/sesame-client.mjs +208 -0
  7. package/clients/python/pyproject.toml +5 -0
  8. package/clients/python/sesame_client.py +323 -0
  9. package/clients/python/setup.cfg +11 -0
  10. package/docs/architecture.ja.md +132 -0
  11. package/docs/architecture.md +105 -0
  12. package/docs/commands.ja.md +316 -0
  13. package/docs/commands.md +308 -0
  14. package/docs/library.ja.md +152 -0
  15. package/docs/library.md +152 -0
  16. package/docs/migration.ja.md +13 -0
  17. package/docs/migration.md +13 -0
  18. package/package.json +114 -0
  19. package/src/access.js +375 -0
  20. package/src/account.js +36 -0
  21. package/src/auth.js +248 -0
  22. package/src/ble/devicemodel.js +164 -0
  23. package/src/ble/index.js +185 -0
  24. package/src/ble/protocol.js +319 -0
  25. package/src/ble/session.js +235 -0
  26. package/src/ble/transport.js +279 -0
  27. package/src/cli/access.js +373 -0
  28. package/src/cli/company.js +104 -0
  29. package/src/cli/iot.js +400 -0
  30. package/src/cli/org.js +788 -0
  31. package/src/cli/presetir.js +188 -0
  32. package/src/cli/schedule.js +83 -0
  33. package/src/cli/serve.js +308 -0
  34. package/src/cli.js +1815 -0
  35. package/src/client.js +957 -0
  36. package/src/company.js +147 -0
  37. package/src/config.js +575 -0
  38. package/src/crypto.js +162 -0
  39. package/src/devices.js +228 -0
  40. package/src/index.js +55 -0
  41. package/src/iot.js +513 -0
  42. package/src/ir.js +341 -0
  43. package/src/itemcodes.js +29 -0
  44. package/src/lock.js +194 -0
  45. package/src/org.js +803 -0
  46. package/src/paths.js +30 -0
  47. package/src/presetir.js +525 -0
  48. package/src/prompts.js +74 -0
  49. package/src/schedule.js +108 -0
  50. package/src/serve/daemon.js +251 -0
  51. package/src/serve/framing/grpc.js +145 -0
  52. package/src/serve/framing/http.js +144 -0
  53. package/src/serve/framing/ndjson.js +75 -0
  54. package/src/serve/framing/socket.js +73 -0
  55. package/src/serve/framing/stdio.js +28 -0
  56. package/src/serve/framing/token.js +36 -0
  57. package/src/serve/framing/ws.js +56 -0
  58. package/src/serve/grpc-methods.generated.json +378 -0
  59. package/src/serve/jsonrpc.js +164 -0
  60. package/src/serve/registry.js +226 -0
  61. package/src/serve/rpc-params.generated.json +1746 -0
  62. package/src/serve/sesame.proto +470 -0
  63. package/src/session-ui.js +181 -0
  64. package/src/sharekey.js +130 -0
  65. package/src/tokens.js +53 -0
  66. package/src/transport.js +634 -0
  67. package/src/util.js +26 -0
  68. package/types/access.d.ts +193 -0
  69. package/types/access.d.ts.map +1 -0
  70. package/types/account.d.ts +13 -0
  71. package/types/account.d.ts.map +1 -0
  72. package/types/auth.d.ts +80 -0
  73. package/types/auth.d.ts.map +1 -0
  74. package/types/ble/devicemodel.d.ts +212 -0
  75. package/types/ble/devicemodel.d.ts.map +1 -0
  76. package/types/ble/index.d.ts +160 -0
  77. package/types/ble/index.d.ts.map +1 -0
  78. package/types/ble/protocol.d.ts +201 -0
  79. package/types/ble/protocol.d.ts.map +1 -0
  80. package/types/ble/session.d.ts +129 -0
  81. package/types/ble/session.d.ts.map +1 -0
  82. package/types/ble/transport.d.ts +67 -0
  83. package/types/ble/transport.d.ts.map +1 -0
  84. package/types/cli/access.d.ts +6 -0
  85. package/types/cli/access.d.ts.map +1 -0
  86. package/types/cli/company.d.ts +6 -0
  87. package/types/cli/company.d.ts.map +1 -0
  88. package/types/cli/iot.d.ts +6 -0
  89. package/types/cli/iot.d.ts.map +1 -0
  90. package/types/cli/org.d.ts +6 -0
  91. package/types/cli/org.d.ts.map +1 -0
  92. package/types/cli/presetir.d.ts +6 -0
  93. package/types/cli/presetir.d.ts.map +1 -0
  94. package/types/cli/schedule.d.ts +6 -0
  95. package/types/cli/schedule.d.ts.map +1 -0
  96. package/types/cli/serve.d.ts +2 -0
  97. package/types/cli/serve.d.ts.map +1 -0
  98. package/types/cli.d.ts +2 -0
  99. package/types/cli.d.ts.map +1 -0
  100. package/types/client.d.ts +463 -0
  101. package/types/client.d.ts.map +1 -0
  102. package/types/company.d.ts +94 -0
  103. package/types/company.d.ts.map +1 -0
  104. package/types/config.d.ts +111 -0
  105. package/types/config.d.ts.map +1 -0
  106. package/types/crypto.d.ts +61 -0
  107. package/types/crypto.d.ts.map +1 -0
  108. package/types/devices.d.ts +116 -0
  109. package/types/devices.d.ts.map +1 -0
  110. package/types/index.d.ts +23 -0
  111. package/types/index.d.ts.map +1 -0
  112. package/types/iot.d.ts +312 -0
  113. package/types/iot.d.ts.map +1 -0
  114. package/types/ir.d.ts +147 -0
  115. package/types/ir.d.ts.map +1 -0
  116. package/types/itemcodes.d.ts +21 -0
  117. package/types/itemcodes.d.ts.map +1 -0
  118. package/types/lock.d.ts +89 -0
  119. package/types/lock.d.ts.map +1 -0
  120. package/types/org.d.ts +468 -0
  121. package/types/org.d.ts.map +1 -0
  122. package/types/paths.d.ts +10 -0
  123. package/types/paths.d.ts.map +1 -0
  124. package/types/presetir.d.ts +286 -0
  125. package/types/presetir.d.ts.map +1 -0
  126. package/types/prompts.d.ts +39 -0
  127. package/types/prompts.d.ts.map +1 -0
  128. package/types/schedule.d.ts +71 -0
  129. package/types/schedule.d.ts.map +1 -0
  130. package/types/serve/daemon.d.ts +133 -0
  131. package/types/serve/daemon.d.ts.map +1 -0
  132. package/types/serve/framing/grpc.d.ts +14 -0
  133. package/types/serve/framing/grpc.d.ts.map +1 -0
  134. package/types/serve/framing/http.d.ts +14 -0
  135. package/types/serve/framing/http.d.ts.map +1 -0
  136. package/types/serve/framing/ndjson.d.ts +19 -0
  137. package/types/serve/framing/ndjson.d.ts.map +1 -0
  138. package/types/serve/framing/socket.d.ts +14 -0
  139. package/types/serve/framing/socket.d.ts.map +1 -0
  140. package/types/serve/framing/stdio.d.ts +11 -0
  141. package/types/serve/framing/stdio.d.ts.map +1 -0
  142. package/types/serve/framing/token.d.ts +11 -0
  143. package/types/serve/framing/token.d.ts.map +1 -0
  144. package/types/serve/framing/ws.d.ts +13 -0
  145. package/types/serve/framing/ws.d.ts.map +1 -0
  146. package/types/serve/jsonrpc.d.ts +118 -0
  147. package/types/serve/jsonrpc.d.ts.map +1 -0
  148. package/types/serve/registry.d.ts +41 -0
  149. package/types/serve/registry.d.ts.map +1 -0
  150. package/types/session-ui.d.ts +36 -0
  151. package/types/session-ui.d.ts.map +1 -0
  152. package/types/sharekey.d.ts +35 -0
  153. package/types/sharekey.d.ts.map +1 -0
  154. package/types/tokens.d.ts +20 -0
  155. package/types/tokens.d.ts.map +1 -0
  156. package/types/transport.d.ts +138 -0
  157. package/types/transport.d.ts.map +1 -0
  158. package/types/util.d.ts +20 -0
  159. package/types/util.d.ts.map +1 -0
  160. package/vendor/biz3/README.md +37 -0
  161. package/vendor/biz3/constants/cmdCode.d.ts +48 -0
  162. package/vendor/biz3/constants/cmdCode.d.ts.map +1 -0
  163. package/vendor/biz3/constants/cmdCode.js +92 -0
  164. package/vendor/biz3/constants/messageConstants.d.ts +28 -0
  165. package/vendor/biz3/constants/messageConstants.d.ts.map +1 -0
  166. package/vendor/biz3/constants/messageConstants.js +30 -0
  167. package/vendor/biz3/constants/sesameDeviceModel.d.ts +75 -0
  168. package/vendor/biz3/constants/sesameDeviceModel.d.ts.map +1 -0
  169. package/vendor/biz3/constants/sesameDeviceModel.js +77 -0
  170. package/vendor/biz3/package.json +5 -0
package/src/cli/org.js ADDED
@@ -0,0 +1,788 @@
1
+ // `sesame org …` コマンド群。
2
+ //
3
+ // 本体ロジックは src/org.js (biz3 組織管理: employee / employeeGroup / role /
4
+ // deviceGroup / employeeDevice / getDeviceEmployeeKeys)。ここは commander への配線と
5
+ // 入出力整形のみを担う。
6
+ //
7
+ // ctx 契約 (cli.js makeCtx が供給。schedule.js のコメント参照):
8
+ // ctx.withHub(fn) : connect → fn(hub, {opts}) → close。
9
+ // ctx.withAccount(fn) : withHub + 事前に refreshAccount() で実 companyID/subUUID 保証。
10
+ // org の op はほぼ全て companyID 必須なので基本こちらを使う。
11
+ // ctx.out(json, humanFn, jsonObj) : --json 時は jsonObj、それ以外は humanFn()。
12
+ // ctx.die(msg, code) : エラー表示して exit。
13
+ // ctx.canPrompt() : TTY かつ --json なし。
14
+ // ctx.prompts : { selectFromList, promptText, confirm, promptLine }。
15
+ //
16
+ // namespace hub.org.* は companyID / subUUID を自動注入する。companyID は
17
+ // refreshAccount() で実値に更新されるため、companyID を要する op は withAccount を使う。
18
+ // gid のみで companyID を送らない op (getBindDeviceGroup / getBindUserGroup) も、
19
+ // 接続前提を揃えるため withAccount で扱う (実害なし)。
20
+ //
21
+ // 構造化された配列/オブジェクト (items / data / item 等) を要する作成・更新・削除系は
22
+ // --json <文字列> で受け、JSON.parse する。biz3 のネスト差異 (companyID キー名が
23
+ // employee系='companyID' / employeeGroup・deviceGroup='cid' 等、本体 JSDoc 参照) は
24
+ // 本体側で吸収済みなので、ここでは本体の引数名にそのまま合わせる。
25
+
26
+ import { buildShareKeyUrl } from "../sharekey.js";
27
+
28
+ /**
29
+ * @param {import("commander").Command} program
30
+ * @param {object} ctx cli.js makeCtx() が供給する共有コンテキスト
31
+ */
32
+ export function registerOrgCommands(program, ctx) {
33
+ const org = program.command("org").description("組織管理 (biz3: 社員 / 社員グループ / 役割タグ / デバイスグループ / 鍵共有)");
34
+
35
+ // ════════════════════════════════════════════════════════════════════════
36
+ // org employee … (biz3ManageEmployee)
37
+ // ════════════════════════════════════════════════════════════════════════
38
+ const employee = org.command("employee").description("社員管理 (一覧/追加/更新/削除/並替/検索/自己情報)");
39
+
40
+ // sesame org employee ls
41
+ employee.command("ls")
42
+ .description("社員一覧 (getEmployees。pubEmployees push を全 page 集約)")
43
+ .action(() =>
44
+ ctx.withAccount(async (hub, { opts }) => {
45
+ // 戻りは { count, list } (count=totalCount)。
46
+ const { count, list } = await hub.org.getEmployees();
47
+ ctx.out(opts.json, () => {
48
+ if (!Array.isArray(list) || list.length === 0) {
49
+ console.log("(no employees)");
50
+ return;
51
+ }
52
+ console.log(`Found ${list.length} employee(s) (totalCount=${count}):`);
53
+ for (const e of list) {
54
+ const id = e.subUUID ?? "(no-uuid)";
55
+ const name = e.employeeName ?? e.nickname ?? "(no-name)";
56
+ const mail = e.employeeEmail ? `\t${e.employeeEmail}` : "";
57
+ console.log(` ${id}\t${name}${mail}`);
58
+ }
59
+ }, { ok: true, count, employees: list });
60
+ }),
61
+ );
62
+
63
+ // sesame org employee me
64
+ employee.command("me")
65
+ .description("ログイン中の自分自身の社員情報 (getCurrentUserInfo。data 構造は biz3 未確認)")
66
+ .action(() =>
67
+ ctx.withAccount(async (hub, { opts }) => {
68
+ const info = await hub.org.getCurrentUserInfo();
69
+ ctx.out(opts.json, () => {
70
+ console.log(JSON.stringify(info, null, 2));
71
+ }, { ok: true, currentUser: info });
72
+ }),
73
+ );
74
+
75
+ // sesame org employee add --json <items>
76
+ employee.command("add")
77
+ .description("社員を追加 (addEmployees。items は配列で各要素に companyID を含める)")
78
+ .option("--json <items>", 'JSON 配列。例 \'[{"employeeEmail":"a@b.c","employeeName":"山田","tag":[]}]\'')
79
+ .action((cmdOpts) =>
80
+ ctx.withAccount(async (hub, { opts }) => {
81
+ if (!cmdOpts.json) {
82
+ ctx.die('items が必要です: sesame org employee add --json \'[{"employeeEmail":"…","employeeName":"…"}]\'', 2);
83
+ return;
84
+ }
85
+ const items = ctx.parseJson(cmdOpts.json, '[{"employeeEmail":"a@b.c","employeeName":"山田"}]');
86
+ if (items === undefined) return;
87
+ if (!Array.isArray(items)) { ctx.die("--json は配列である必要があります。", 2); return; }
88
+ // 各要素に companyID を補完 (本体 addEmployees は item 内 companyID を期待。biz3 同様)。
89
+ // companyID を後置し、item 内に空文字/null が紛れても必ず有効値が勝つようにする
90
+ // (明示の有効な companyID があればそれを尊重)。
91
+ const withCid = items.map((it) => ({ ...it, companyID: it.companyID || hub.config.companyID }));
92
+ const resp = await hub.org.addEmployees({ items: withCid });
93
+ ctx.out(opts.json, () => {
94
+ console.log(`OK: requested add of ${withCid.length} employee(s)`);
95
+ }, { ok: true, response: resp });
96
+ }),
97
+ );
98
+
99
+ // sesame org employee update --json <data>
100
+ employee.command("update")
101
+ .description("社員情報を更新 (updateEmployee。companyID は自動注入、更新フィールドは --json で渡す)")
102
+ .option("--json <data>", 'JSON オブジェクト。例 \'{"Name":"nickname","Value":"新名"}\'')
103
+ .action((cmdOpts) =>
104
+ ctx.withAccount(async (hub, { opts }) => {
105
+ if (!cmdOpts.json) {
106
+ ctx.die('更新フィールドが必要です: sesame org employee update --json \'{"Name":"…","Value":"…"}\'', 2);
107
+ return;
108
+ }
109
+ const data = ctx.parseJson(cmdOpts.json, '{"Name":"nickname","Value":"新名"}');
110
+ if (data === undefined) return;
111
+ const resp = await hub.org.updateEmployee({ data });
112
+ ctx.out(opts.json, () => {
113
+ console.log("OK: employee updated");
114
+ }, { ok: true, response: resp });
115
+ }),
116
+ );
117
+
118
+ // sesame org employee rm --json <items>
119
+ employee.command("rm")
120
+ .description("社員を削除 (removeEmployees。items は社員オブジェクト/[{subUUID,companyID}] 配列)")
121
+ .option("--json <items>", 'JSON 配列。例 \'[{"subUUID":"…","companyID":"…"}]\'')
122
+ .action((cmdOpts) =>
123
+ ctx.withAccount(async (hub, { opts }) => {
124
+ if (!cmdOpts.json) {
125
+ ctx.die('items が必要です: sesame org employee rm --json \'[{"subUUID":"…"}]\'', 2);
126
+ return;
127
+ }
128
+ const items = ctx.parseJson(cmdOpts.json, '[{"subUUID":"…","companyID":"…"}]');
129
+ if (items === undefined) return;
130
+ if (!Array.isArray(items)) { ctx.die("--json は配列である必要があります。", 2); return; }
131
+ const resp = await hub.org.removeEmployees({ items });
132
+ ctx.out(opts.json, () => {
133
+ console.log(`OK: requested removal of ${items.length} employee(s)`);
134
+ }, { ok: true, response: resp });
135
+ }),
136
+ );
137
+
138
+ // sesame org employee reorder --json <items>
139
+ employee.command("reorder")
140
+ .description("社員の並び順を更新 (reorderEmployees。各要素 {friendUUID, rank})")
141
+ .option("--json <items>", 'JSON 配列。例 \'[{"friendUUID":"…","rank":0},{"friendUUID":"…","rank":-1}]\'')
142
+ .action((cmdOpts) =>
143
+ ctx.withAccount(async (hub, { opts }) => {
144
+ if (!cmdOpts.json) {
145
+ ctx.die('items が必要です: sesame org employee reorder --json \'[{"friendUUID":"…","rank":0}]\'', 2);
146
+ return;
147
+ }
148
+ const items = ctx.parseJson(cmdOpts.json, '[{"friendUUID":"…","rank":0}]');
149
+ if (items === undefined) return;
150
+ if (!Array.isArray(items)) { ctx.die("--json は配列である必要があります。", 2); return; }
151
+ const resp = await hub.org.reorderEmployees({ items });
152
+ ctx.out(opts.json, () => {
153
+ console.log(`OK: reorder requested (${items.length} item(s))`);
154
+ }, { ok: true, response: resp });
155
+ }),
156
+ );
157
+
158
+ // sesame org employee search <keyword>
159
+ employee.command("search <keyword>")
160
+ .description("CS 横断でユーザーを検索 (queryByCS。pubQueryByCS push を全 page 集約)")
161
+ .action((keyword) =>
162
+ ctx.withAccount(async (hub, { opts }) => {
163
+ const list = await hub.org.queryByCS({ keyword });
164
+ ctx.out(opts.json, () => {
165
+ if (!Array.isArray(list) || list.length === 0) {
166
+ console.log("(no matches)");
167
+ return;
168
+ }
169
+ console.log(`Found ${list.length} match(es):`);
170
+ for (const u of list) {
171
+ const id = u.subUUID ?? "(no-uuid)";
172
+ const name = u.employeeName ?? u.nickname ?? "";
173
+ const mail = u.employeeEmail ?? u.email ?? "";
174
+ console.log(` ${id}\t${name}\t${mail}`);
175
+ }
176
+ }, { ok: true, count: Array.isArray(list) ? list.length : 0, results: list });
177
+ }),
178
+ );
179
+
180
+ // sesame org employee confirm <email>
181
+ employee.command("confirm <email>")
182
+ .description("queryByCS で見つけたユーザーを確定 (confirmQueryByCS)。注: biz3 では成功時に現セッションを signout する設計")
183
+ .action((email) =>
184
+ ctx.withAccount(async (hub, { opts }) => {
185
+ // 副作用が重い (biz3 UI は成功時 signout)。対話可能なら確認を取る。
186
+ if (ctx.canPrompt()) {
187
+ const ok = await ctx.prompts.confirm(
188
+ `confirmQueryByCS は biz3 では成功時に現セッションを signout します。続行しますか? (${email})`,
189
+ { defaultYes: false },
190
+ );
191
+ // 正常な中断: die (Error: プレフィックス + process.exit で finally skip) ではなく
192
+ // plain log + return にして withHub の close() を生かす。
193
+ if (!ok) { console.error("中止しました。"); return; }
194
+ }
195
+ const resp = await hub.org.confirmQueryByCS({ email });
196
+ ctx.out(opts.json, () => {
197
+ console.log(`OK: confirmed ${email}`);
198
+ }, { ok: true, response: resp });
199
+ }),
200
+ );
201
+
202
+ // ════════════════════════════════════════════════════════════════════════
203
+ // org group … (社員グループ, biz3ManageEmployeeGroup)
204
+ // ════════════════════════════════════════════════════════════════════════
205
+ const group = org.command("group").description("社員グループ管理 (一覧/追加/更新/削除/メンバー紐付/デバイスグループ連携)");
206
+
207
+ // sesame org group ls
208
+ group.command("ls")
209
+ .description("社員グループ一覧 (getEmployeeGroups)")
210
+ .action(() =>
211
+ ctx.withAccount(async (hub, { opts }) => {
212
+ const list = await hub.org.getEmployeeGroups();
213
+ ctx.out(opts.json, () => {
214
+ if (!Array.isArray(list) || list.length === 0) {
215
+ console.log("(no employee groups)");
216
+ return;
217
+ }
218
+ console.log(`Found ${list.length} employee group(s):`);
219
+ for (const g of list) {
220
+ const id = g.gid ?? g.groupId ?? "(no-id)";
221
+ const name = g.name ?? g.groupName ?? "(no-name)";
222
+ console.log(` ${id}\t${name}`);
223
+ }
224
+ }, { ok: true, count: Array.isArray(list) ? list.length : 0, groups: list });
225
+ }),
226
+ );
227
+
228
+ // sesame org group add --json <item>
229
+ group.command("add")
230
+ .description("社員グループを追加 (addEmployeeGroup。companyID は自動注入、item は --json)")
231
+ .option("--json <item>", 'JSON オブジェクト (グループ名等は biz3 UI 依存で未確認)。例 \'{"name":"営業部"}\'')
232
+ .action((cmdOpts) =>
233
+ ctx.withAccount(async (hub, { opts }) => {
234
+ if (!cmdOpts.json) {
235
+ ctx.die('item が必要です: sesame org group add --json \'{"name":"営業部"}\'', 2);
236
+ return;
237
+ }
238
+ const item = ctx.parseJson(cmdOpts.json, '{"name":"営業部"}');
239
+ if (item === undefined) return;
240
+ const created = await hub.org.addEmployeeGroup({ item });
241
+ ctx.out(opts.json, () => {
242
+ console.log(`OK: added employee group${created?.gid ? ` (${created.gid})` : ""}`);
243
+ }, { ok: true, group: created });
244
+ }),
245
+ );
246
+
247
+ // sesame org group update --json <item>
248
+ group.command("update")
249
+ .description("社員グループを更新 (updateEmployeeGroup。item に gid 等を含める)")
250
+ .option("--json <item>", 'JSON オブジェクト。例 \'{"gid":"…","name":"新名"}\'')
251
+ .action((cmdOpts) =>
252
+ ctx.withAccount(async (hub, { opts }) => {
253
+ if (!cmdOpts.json) {
254
+ ctx.die('item が必要です: sesame org group update --json \'{"gid":"…","name":"…"}\'', 2);
255
+ return;
256
+ }
257
+ const item = ctx.parseJson(cmdOpts.json, '{"gid":"…","name":"新名"}');
258
+ if (item === undefined) return;
259
+ const resp = await hub.org.updateEmployeeGroup({ item });
260
+ ctx.out(opts.json, () => {
261
+ console.log("OK: employee group updated");
262
+ }, { ok: true, response: resp });
263
+ }),
264
+ );
265
+
266
+ // sesame org group rm --json <gids>
267
+ group.command("rm")
268
+ .description("社員グループを削除 (removeEmployeeGroups。gids は配列、要素型は biz3 UI 依存で未確認)")
269
+ .option("--json <gids>", 'JSON 配列。例 \'["gid1","gid2"]\'')
270
+ .action((cmdOpts) =>
271
+ ctx.withAccount(async (hub, { opts }) => {
272
+ if (!cmdOpts.json) {
273
+ ctx.die('gids が必要です: sesame org group rm --json \'["gid1"]\'', 2);
274
+ return;
275
+ }
276
+ const gids = ctx.parseJson(cmdOpts.json, '["gid1","gid2"]');
277
+ if (gids === undefined) return;
278
+ if (!Array.isArray(gids)) { ctx.die("--json は配列である必要があります。", 2); return; }
279
+ const resp = await hub.org.removeEmployeeGroups({ gids });
280
+ ctx.out(opts.json, () => {
281
+ console.log(`OK: requested removal of ${gids.length} group(s)`);
282
+ }, { ok: true, response: resp });
283
+ }),
284
+ );
285
+
286
+ // sesame org group device-groups <gid>
287
+ group.command("device-groups <gid>")
288
+ .description("社員グループに紐づくデバイスグループを取得 (getEmployeeGroupBindDeviceGroup。cid は送らない)")
289
+ .action((gid) =>
290
+ ctx.withAccount(async (hub, { opts }) => {
291
+ const data = await hub.org.getEmployeeGroupBindDeviceGroup({ gid });
292
+ ctx.out(opts.json, () => {
293
+ // data 構造は biz3 未確認。そのまま整形出力。
294
+ console.log(JSON.stringify(data, null, 2));
295
+ }, { ok: true, gid, bindDeviceGroup: data });
296
+ }),
297
+ );
298
+
299
+ // sesame org group add-users <gid> --json <body>
300
+ group.command("add-users <gid>")
301
+ .description("社員グループにユーザーを紐付け (addEmployeeInGroup。uuids/items 両方を --json で渡す)")
302
+ .option("--json <body>", 'JSON {uuids,items}。例 \'{"uuids":["sub1"],"items":[{"subUUID":"sub1"}]}\'')
303
+ .action((gid, cmdOpts) =>
304
+ ctx.withAccount(async (hub, { opts }) => {
305
+ if (!cmdOpts.json) {
306
+ ctx.die('uuids/items が必要です: sesame org group add-users <gid> --json \'{"uuids":[],"items":[]}\'', 2);
307
+ return;
308
+ }
309
+ const body = ctx.parseJson(cmdOpts.json, '{"uuids":["sub1"],"items":[{"subUUID":"sub1"}]}');
310
+ if (body === undefined) return;
311
+ // rm-users と対称に uuids/items の配列性を検証 (undefined のまま送ると曖昧な失敗になる)。
312
+ if (!Array.isArray(body.uuids) || !Array.isArray(body.items)) {
313
+ ctx.die('--json の uuids / items は配列である必要があります。', 2); return;
314
+ }
315
+ const resp = await hub.org.addEmployeeInGroup({ gid, uuids: body.uuids, items: body.items });
316
+ ctx.out(opts.json, () => {
317
+ console.log(`OK: bound users to group ${gid}`);
318
+ }, { ok: true, response: resp });
319
+ }),
320
+ );
321
+
322
+ // sesame org group rm-users <gid> --json <body>
323
+ group.command("rm-users <gid>")
324
+ .description("社員グループからユーザーを解除 (removeEmployeeInGroup。items は {subUUID} に絞り込まれる)")
325
+ .option("--json <body>", 'JSON {uuids,items}。例 \'{"uuids":["sub1"],"items":[{"subUUID":"sub1"}]}\'')
326
+ .action((gid, cmdOpts) =>
327
+ ctx.withAccount(async (hub, { opts }) => {
328
+ if (!cmdOpts.json) {
329
+ ctx.die('uuids/items が必要です: sesame org group rm-users <gid> --json \'{"uuids":[],"items":[]}\'', 2);
330
+ return;
331
+ }
332
+ const body = ctx.parseJson(cmdOpts.json, '{"uuids":["sub1"],"items":[{"subUUID":"sub1"}]}');
333
+ if (body === undefined) return;
334
+ if (!Array.isArray(body.items)) { ctx.die('--json の items は配列である必要があります。', 2); return; }
335
+ const resp = await hub.org.removeEmployeeInGroup({ gid, uuids: body.uuids, items: body.items });
336
+ ctx.out(opts.json, () => {
337
+ console.log(`OK: unbound users from group ${gid}`);
338
+ }, { ok: true, response: resp });
339
+ }),
340
+ );
341
+
342
+ // sesame org group rm-device-group --json <data>
343
+ group.command("rm-device-group")
344
+ .description("社員グループからデバイスグループを解除 (removeEmployeeGroupBindDeviceGroup。data 内容は biz3 未確認)")
345
+ .option("--json <data>", 'JSON オブジェクト (gid 等。biz3 UI 依存で未確認)。')
346
+ .action((cmdOpts) =>
347
+ ctx.withAccount(async (hub, { opts }) => {
348
+ if (!cmdOpts.json) {
349
+ ctx.die('data が必要です: sesame org group rm-device-group --json \'{"gid":"…"}\'', 2);
350
+ return;
351
+ }
352
+ const data = ctx.parseJson(cmdOpts.json, '{"gid":"…"}');
353
+ if (data === undefined) return;
354
+ const resp = await hub.org.removeEmployeeGroupBindDeviceGroup({ data });
355
+ ctx.out(opts.json, () => {
356
+ console.log("OK: unbound device group from employee group");
357
+ }, { ok: true, response: resp });
358
+ }),
359
+ );
360
+
361
+ // ════════════════════════════════════════════════════════════════════════
362
+ // org role … (役割タグ, biz3ManageRole)
363
+ // ════════════════════════════════════════════════════════════════════════
364
+ const role = org.command("role").description("役割タグ管理 (一覧/追加更新/削除)");
365
+
366
+ // sesame org role ls
367
+ role.command("ls")
368
+ .description("役割タグ一覧 (getTags)")
369
+ .action(() =>
370
+ ctx.withAccount(async (hub, { opts }) => {
371
+ const list = await hub.org.getTags();
372
+ ctx.out(opts.json, () => {
373
+ if (!Array.isArray(list) || list.length === 0) {
374
+ console.log("(no role tags)");
375
+ return;
376
+ }
377
+ console.log(`Found ${list.length} role tag(s):`);
378
+ for (const t of list) {
379
+ const id = t.id ?? t.tagId ?? "(no-id)";
380
+ const name = t.name ?? t.tagName ?? "(no-name)";
381
+ console.log(` ${id}\t${name}`);
382
+ }
383
+ }, { ok: true, count: Array.isArray(list) ? list.length : 0, tags: list });
384
+ }),
385
+ );
386
+
387
+ // sesame org role post --json <data>
388
+ role.command("post")
389
+ .description("役割タグを追加/更新 (postTag。companyID は自動注入、data は --json)")
390
+ .option("--json <data>", 'JSON オブジェクト (タグ名等。biz3 UI 依存)。例 \'{"name":"管理者"}\'')
391
+ .action((cmdOpts) =>
392
+ ctx.withAccount(async (hub, { opts }) => {
393
+ if (!cmdOpts.json) {
394
+ ctx.die('data が必要です: sesame org role post --json \'{"name":"管理者"}\'', 2);
395
+ return;
396
+ }
397
+ const data = ctx.parseJson(cmdOpts.json, '{"name":"管理者"}');
398
+ if (data === undefined) return;
399
+ const resp = await hub.org.postTag({ data });
400
+ ctx.out(opts.json, () => {
401
+ console.log("OK: role tag posted");
402
+ }, { ok: true, response: resp });
403
+ }),
404
+ );
405
+
406
+ // sesame org role rm --json <data>
407
+ role.command("rm")
408
+ .description("役割タグを削除 (removeTag。data 内容は biz3 UI 依存)")
409
+ .option("--json <data>", 'JSON オブジェクト。例 \'{"id":"…"}\'')
410
+ .action((cmdOpts) =>
411
+ ctx.withAccount(async (hub, { opts }) => {
412
+ if (!cmdOpts.json) {
413
+ ctx.die('data が必要です: sesame org role rm --json \'{"id":"…"}\'', 2);
414
+ return;
415
+ }
416
+ const data = ctx.parseJson(cmdOpts.json, '{"id":"…"}');
417
+ if (data === undefined) return;
418
+ const resp = await hub.org.removeTag({ data });
419
+ ctx.out(opts.json, () => {
420
+ console.log("OK: role tag removed");
421
+ }, { ok: true, response: resp });
422
+ }),
423
+ );
424
+
425
+ // ════════════════════════════════════════════════════════════════════════
426
+ // org device-group … (デバイスグループ, biz3ManageDeviceGroup)
427
+ // ════════════════════════════════════════════════════════════════════════
428
+ const deviceGroup = org.command("device-group").description("デバイスグループ管理 (一覧/作成/更新/削除/デバイス紐付/社員グループ連携)");
429
+
430
+ // sesame org device-group ls
431
+ deviceGroup.command("ls")
432
+ .description("デバイスグループ一覧 (getDeviceGroups)")
433
+ .action(() =>
434
+ ctx.withAccount(async (hub, { opts }) => {
435
+ const list = await hub.org.getDeviceGroups();
436
+ ctx.out(opts.json, () => {
437
+ if (!Array.isArray(list) || list.length === 0) {
438
+ console.log("(no device groups)");
439
+ return;
440
+ }
441
+ console.log(`Found ${list.length} device group(s):`);
442
+ for (const g of list) {
443
+ const id = g.gid ?? g.groupId ?? "(no-id)";
444
+ const name = g.name ?? g.groupName ?? "(no-name)";
445
+ console.log(` ${id}\t${name}`);
446
+ }
447
+ }, { ok: true, count: Array.isArray(list) ? list.length : 0, groups: list });
448
+ }),
449
+ );
450
+
451
+ // sesame org device-group add <name> [--uuids <json>]
452
+ deviceGroup.command("add <name>")
453
+ .description("デバイスグループを作成 (addDeviceGroup。companyID は自動注入)")
454
+ .option("--uuids <json>", 'JSON 配列の deviceUUID。例 \'["uuid1","uuid2"]\'', "[]")
455
+ .action((name, cmdOpts) =>
456
+ ctx.withAccount(async (hub, { opts }) => {
457
+ const uuids = ctx.parseJson(cmdOpts.uuids, '["uuid1","uuid2"]');
458
+ if (uuids === undefined) return;
459
+ if (!Array.isArray(uuids)) { ctx.die("--uuids は配列である必要があります。", 2); return; }
460
+ const resp = await hub.org.addDeviceGroup({ name, uuids });
461
+ ctx.out(opts.json, () => {
462
+ console.log(`OK: created device group "${name}" (${uuids.length} device(s))`);
463
+ }, { ok: true, response: resp });
464
+ }),
465
+ );
466
+
467
+ // sesame org device-group update --json <item>
468
+ deviceGroup.command("update")
469
+ .description("デバイスグループを更新 (updateDeviceGroup。item に gid 等を含める)")
470
+ .option("--json <item>", 'JSON オブジェクト。例 \'{"gid":"…","name":"新名"}\'')
471
+ .action((cmdOpts) =>
472
+ ctx.withAccount(async (hub, { opts }) => {
473
+ if (!cmdOpts.json) {
474
+ ctx.die('item が必要です: sesame org device-group update --json \'{"gid":"…","name":"…"}\'', 2);
475
+ return;
476
+ }
477
+ const item = ctx.parseJson(cmdOpts.json, '{"gid":"…","name":"新名"}');
478
+ if (item === undefined) return;
479
+ const resp = await hub.org.updateDeviceGroup({ item });
480
+ ctx.out(opts.json, () => {
481
+ console.log("OK: device group updated");
482
+ }, { ok: true, response: resp });
483
+ }),
484
+ );
485
+
486
+ // sesame org device-group rm --json <groupIds>
487
+ deviceGroup.command("rm")
488
+ .description("デバイスグループを削除 (removeDeviceGroups。各要素に cid が自動マージされる)")
489
+ .option("--json <groupIds>", 'JSON オブジェクト配列。例 \'[{"gid":"…"}]\'')
490
+ .action((cmdOpts) =>
491
+ ctx.withAccount(async (hub, { opts }) => {
492
+ if (!cmdOpts.json) {
493
+ ctx.die('groupIds が必要です: sesame org device-group rm --json \'[{"gid":"…"}]\'', 2);
494
+ return;
495
+ }
496
+ const groupIds = ctx.parseJson(cmdOpts.json, '[{"gid":"…"}]');
497
+ if (groupIds === undefined) return;
498
+ if (!Array.isArray(groupIds)) { ctx.die("--json は配列である必要があります。", 2); return; }
499
+ const resp = await hub.org.removeDeviceGroups({ groupIds });
500
+ ctx.out(opts.json, () => {
501
+ console.log(`OK: requested removal of ${groupIds.length} device group(s)`);
502
+ }, { ok: true, response: resp });
503
+ }),
504
+ );
505
+
506
+ // sesame org device-group add-devices <gid> --json <body>
507
+ deviceGroup.command("add-devices <gid>")
508
+ .description("デバイスグループにデバイスを紐付け (addDeviceInGroup。uuids/items を --json で渡す)")
509
+ .option("--json <body>", 'JSON {uuids,items}。例 \'{"uuids":["dev1"],"items":[{"deviceUUID":"dev1"}]}\'')
510
+ .action((gid, cmdOpts) =>
511
+ ctx.withAccount(async (hub, { opts }) => {
512
+ if (!cmdOpts.json) {
513
+ ctx.die('uuids/items が必要です: sesame org device-group add-devices <gid> --json \'{"uuids":[],"items":[]}\'', 2);
514
+ return;
515
+ }
516
+ const body = ctx.parseJson(cmdOpts.json, '{"uuids":["dev1"],"items":[{"deviceUUID":"dev1"}]}');
517
+ if (body === undefined) return;
518
+ // rm-devices と対称に uuids/items の配列性を検証。
519
+ if (!Array.isArray(body.uuids) || !Array.isArray(body.items)) {
520
+ ctx.die('--json の uuids / items は配列である必要があります。', 2); return;
521
+ }
522
+ const resp = await hub.org.addDeviceInGroup({ gid, uuids: body.uuids, items: body.items });
523
+ ctx.out(opts.json, () => {
524
+ console.log(`OK: bound devices to group ${gid}`);
525
+ }, { ok: true, response: resp });
526
+ }),
527
+ );
528
+
529
+ // sesame org device-group rm-devices <gid> --json <body>
530
+ deviceGroup.command("rm-devices <gid>")
531
+ .description("デバイスグループからデバイスを解除 (removeDeviceInGroup。items は {deviceUUID,secretKey} に絞り込まれる)")
532
+ .option("--json <body>", 'JSON {uuids,items}。例 \'{"uuids":["dev1"],"items":[{"deviceUUID":"dev1","secretKey":"…"}]}\'')
533
+ .action((gid, cmdOpts) =>
534
+ ctx.withAccount(async (hub, { opts }) => {
535
+ if (!cmdOpts.json) {
536
+ ctx.die('uuids/items が必要です: sesame org device-group rm-devices <gid> --json \'{"uuids":[],"items":[]}\'', 2);
537
+ return;
538
+ }
539
+ const body = ctx.parseJson(cmdOpts.json, '{"uuids":["dev1"],"items":[{"deviceUUID":"dev1","secretKey":"…"}]}');
540
+ if (body === undefined) return;
541
+ if (!Array.isArray(body.items)) { ctx.die('--json の items は配列である必要があります。', 2); return; }
542
+ const resp = await hub.org.removeDeviceInGroup({ gid, uuids: body.uuids, items: body.items });
543
+ ctx.out(opts.json, () => {
544
+ console.log(`OK: unbound devices from group ${gid}`);
545
+ }, { ok: true, response: resp });
546
+ }),
547
+ );
548
+
549
+ // sesame org device-group user-groups <gid>
550
+ deviceGroup.command("user-groups <gid>")
551
+ .description("デバイスグループにバインド済みの社員グループを取得 (getDeviceGroupBindUserGroup。cid は送らない)")
552
+ .action((gid) =>
553
+ ctx.withAccount(async (hub, { opts }) => {
554
+ const data = await hub.org.getDeviceGroupBindUserGroup({ gid });
555
+ ctx.out(opts.json, () => {
556
+ // data 構造は biz3 未確認。そのまま整形出力。
557
+ console.log(JSON.stringify(data, null, 2));
558
+ }, { ok: true, gid, bindUserGroup: data });
559
+ }),
560
+ );
561
+
562
+ // sesame org device-group rm-user-group --json <data>
563
+ deviceGroup.command("rm-user-group")
564
+ .description("デバイスグループから社員グループを解除 (removeDeviceGroupBindUserGroup。data 内容は biz3 未確認)")
565
+ .option("--json <data>", 'JSON オブジェクト (gid/uuids 等。biz3 UI 依存で未確認)。')
566
+ .action((cmdOpts) =>
567
+ ctx.withAccount(async (hub, { opts }) => {
568
+ if (!cmdOpts.json) {
569
+ ctx.die('data が必要です: sesame org device-group rm-user-group --json \'{"gid":"…"}\'', 2);
570
+ return;
571
+ }
572
+ const data = ctx.parseJson(cmdOpts.json, '{"gid":"…"}');
573
+ if (data === undefined) return;
574
+ const resp = await hub.org.removeDeviceGroupBindUserGroup({ data });
575
+ ctx.out(opts.json, () => {
576
+ console.log("OK: unbound user group from device group");
577
+ }, { ok: true, response: resp });
578
+ }),
579
+ );
580
+
581
+ // ════════════════════════════════════════════════════════════════════════
582
+ // org keys … (デバイス鍵の共有/列挙/取消, biz3ManageEmployeeDevice + getDeviceEmployeeKeys)
583
+ // ════════════════════════════════════════════════════════════════════════
584
+ const keys = org.command("keys").description("デバイス鍵の共有/列挙/取消 (employeeDevice + getDeviceEmployeeKeys)");
585
+
586
+ // sesame org keys device <deviceUUID> [--limit <n>]
587
+ keys.command("device <deviceUUID>")
588
+ .description("デバイス側から鍵保有従業員を列挙 (getDeviceEmployeeKeys。companyID 必須・自動注入)")
589
+ .option("--limit <n>", "0=全件 / 5=非管理モード", (v) => Number(v), 0)
590
+ .action((deviceUUID, cmdOpts) =>
591
+ ctx.withAccount(async (hub, { opts }) => {
592
+ const list = await hub.org.getDeviceEmployeeKeys({ deviceUUID, limit: cmdOpts.limit });
593
+ ctx.out(opts.json, () => {
594
+ if (!Array.isArray(list) || list.length === 0) {
595
+ console.log("(no key holders)");
596
+ return;
597
+ }
598
+ console.log(`Found ${list.length} key holder(s) for ${deviceUUID}:`);
599
+ for (const k of list) {
600
+ const lv = k.keyLevel; // 0=owner,1=manager,2=guest
601
+ const who = k.employeeName ?? k.subUUID ?? "(unknown)";
602
+ const guest = k.guestKeyId && String(k.guestKeyId).length > 0 ? " [guest]" : "";
603
+ console.log(` lv${lv}\t${who}${guest}`);
604
+ }
605
+ }, { ok: true, count: Array.isArray(list) ? list.length : 0, keys: list });
606
+ }),
607
+ );
608
+
609
+ // sesame org keys employee <subUUID>
610
+ keys.command("employee <subUUID>")
611
+ .description("指定従業員が持つデバイス鍵一覧 (getEmployeeDeviceKeys。companyID 不要、data 構造は未確認)")
612
+ .action((subUUID) =>
613
+ ctx.withAccount(async (hub, { opts }) => {
614
+ const data = await hub.org.getEmployeeDeviceKeys({ subUUID });
615
+ ctx.out(opts.json, () => {
616
+ console.log(JSON.stringify(data, null, 2));
617
+ }, { ok: true, subUUID, keys: data });
618
+ }),
619
+ );
620
+
621
+ // sesame org keys share --json <items>
622
+ keys.command("share")
623
+ .description("従業員にデバイス鍵を共有 (shareDeviceKeysToEmployees。items は呼出側で device+user 情報を合成)")
624
+ .option("--json <items>", 'JSON 配列。各要素 {...device,...user,keyLevel,startTime,endTime}。keyLevel 0=owner/1=manager/2=guest')
625
+ .action((cmdOpts) =>
626
+ ctx.withAccount(async (hub, { opts }) => {
627
+ if (!cmdOpts.json) {
628
+ ctx.die('items が必要です: sesame org keys share --json \'[{"deviceUUID":"…","subUUID":"…","keyLevel":1,"startTime":"","endTime":""}]\'', 2);
629
+ return;
630
+ }
631
+ const items = ctx.parseJson(cmdOpts.json, '[{"deviceUUID":"…","subUUID":"…","keyLevel":1,"startTime":"","endTime":""}]');
632
+ if (items === undefined) return;
633
+ if (!Array.isArray(items)) { ctx.die("--json は配列である必要があります。", 2); return; }
634
+ const resp = await hub.org.shareDeviceKeysToEmployees({ items });
635
+ ctx.out(opts.json, () => {
636
+ console.log(`OK: shared keys (${items.length} item(s))`);
637
+ }, { ok: true, response: resp });
638
+ }),
639
+ );
640
+
641
+ // sesame org keys share-group --json <item>
642
+ keys.command("share-group")
643
+ .description("社員グループにデバイスグループ鍵を共有 (shareDeviceGroupKeysToEmployeeGroup。companyID 自動注入)")
644
+ .option("--json <item>", 'JSON {keyLevel,members,devices,mid,dids,startTime,endTime}。keyLevel は文字列 "0"/"1"/"2"')
645
+ .action((cmdOpts) =>
646
+ ctx.withAccount(async (hub, { opts }) => {
647
+ if (!cmdOpts.json) {
648
+ ctx.die('item が必要です: sesame org keys share-group --json \'{"keyLevel":"1","members":[],"devices":[],"mid":"…","dids":[]}\'', 2);
649
+ return;
650
+ }
651
+ const item = ctx.parseJson(cmdOpts.json, '{"keyLevel":"1","members":[],"devices":[],"mid":"…","dids":[]}');
652
+ if (item === undefined) return;
653
+ const resp = await hub.org.shareDeviceGroupKeysToEmployeeGroup({ item });
654
+ ctx.out(opts.json, () => {
655
+ console.log("OK: shared device group keys to employee group");
656
+ }, { ok: true, response: resp });
657
+ }),
658
+ );
659
+
660
+ // sesame org keys rm --json <data>
661
+ keys.command("rm")
662
+ .description("従業員/ゲストのデバイス鍵を削除 (removeEmployeeDeviceKey)。ゲスト鍵は randomTag が必要 (本体 JSDoc 参照)")
663
+ .option("--json <data>", 'JSON。従業員 \'{"subUUID":"…","deviceUUID":"…"}\' / ゲスト \'{"guestKeyId":"…","randomTag":"…","deviceUUID":"…"}\'')
664
+ .action((cmdOpts) =>
665
+ ctx.withAccount(async (hub, { opts }) => {
666
+ if (!cmdOpts.json) {
667
+ ctx.die('data が必要です: sesame org keys rm --json \'{"subUUID":"…","deviceUUID":"…"}\'', 2);
668
+ return;
669
+ }
670
+ const data = ctx.parseJson(cmdOpts.json, '{"subUUID":"…","deviceUUID":"…"}');
671
+ if (data === undefined) return;
672
+ const resp = await hub.org.removeEmployeeDeviceKey({ data });
673
+ ctx.out(opts.json, () => {
674
+ console.log("OK: device key removed");
675
+ }, { ok: true, response: resp });
676
+ }),
677
+ );
678
+
679
+ // sesame org keys update-guest-tag --json <data>
680
+ keys.command("update-guest-tag")
681
+ .description("ゲスト鍵の名称タグを更新 (updateGuestKeyTag)")
682
+ .option("--json <data>", 'JSON {deviceUUID,guestKeyId,keyName}。keyName が新タグ名')
683
+ .action((cmdOpts) =>
684
+ ctx.withAccount(async (hub, { opts }) => {
685
+ if (!cmdOpts.json) {
686
+ ctx.die('data が必要です: sesame org keys update-guest-tag --json \'{"deviceUUID":"…","guestKeyId":"…","keyName":"新名"}\'', 2);
687
+ return;
688
+ }
689
+ const data = ctx.parseJson(cmdOpts.json, '{"deviceUUID":"…","guestKeyId":"…","keyName":"新名"}');
690
+ if (data === undefined) return;
691
+ const resp = await hub.org.updateGuestKeyTag({ data });
692
+ ctx.out(opts.json, () => {
693
+ console.log("OK: guest key tag updated");
694
+ }, { ok: true, response: resp });
695
+ }),
696
+ );
697
+
698
+ // sesame org keys generate-guest-qr --json <data>
699
+ keys.command("generate-guest-qr")
700
+ .description("ゲスト用 guestKeyId を発行 (generateGuestQR)。data はデバイス鍵オブジェクト全体。QR 画像化は本 op 対象外")
701
+ .option("--json <data>", 'JSON のデバイス鍵オブジェクト全体。例 \'{"deviceUUID":"…","secretKey":"…","sesame2PublicKey":"…","keyIndex":0,"deviceModel":"…","keyLevel":0}\'')
702
+ .action((cmdOpts) =>
703
+ ctx.withAccount(async (hub, { opts }) => {
704
+ if (!cmdOpts.json) {
705
+ ctx.die('data が必要です: sesame org keys generate-guest-qr --json \'{"deviceUUID":"…","secretKey":"…"}\'', 2);
706
+ return;
707
+ }
708
+ const data = ctx.parseJson(cmdOpts.json, '{"deviceUUID":"…","secretKey":"…"}');
709
+ if (data === undefined) return;
710
+ const guestKeyId = await hub.org.generateGuestQR({ data });
711
+ ctx.out(opts.json, () => {
712
+ console.log(`OK: guestKeyId = ${guestKeyId}`);
713
+ }, { ok: true, guestKeyId });
714
+ }),
715
+ );
716
+
717
+ // sesame org keys share-url --device <uuid> [--level 0|1|2] [--name …] [--qr]
718
+ // biz3 のゲスト共有 QR と同じ ssm://UI?... 共有 URL を組み立てる (sharekey.buildShareKeyUrl)。
719
+ // level=2 (guest) のときだけ先に generateGuestQR で guestKeyId を発行し secretKey 位置へ差し込む。
720
+ keys.command("share-url")
721
+ .description("デバイス鍵の共有 URL (ssm://UI?...) を生成。SESAME アプリが読む QR の中身そのもの")
722
+ .option("-d, --device <uuid>", "対象 deviceUUID (省略時は対話選択)")
723
+ .option("-l, --level <0|1|2>", "鍵レベル 0=owner / 1=manager / 2=guest (既定 2)", "2")
724
+ .option("--name <name>", "共有時の表示名 (省略時はデバイス名)")
725
+ .option("--json <deviceKey>", "デバイス鍵を JSON で直接指定 (省略時は devices から解決)")
726
+ .option("--qr", "端末に QR を表示 (要 qrcode-terminal: npm i qrcode-terminal)")
727
+ .addHelpText("after", `
728
+ level 2 (guest) のみ generateGuestQR で使い捨て guestKeyId を発行して埋め込みます。
729
+ 0/1 (owner/manager) はデバイス自身の secretKey を共有するため取り扱い注意。
730
+ QR 画像化を省く場合でも、出力された ssm://UI URL を任意の QR 生成器に貼れば共有できます。`)
731
+ .action((cmdOpts) =>
732
+ ctx.withAccount(async (hub, { opts }) => {
733
+ const level = parseInt(cmdOpts.level, 10);
734
+ if (![0, 1, 2].includes(level)) {
735
+ ctx.die("--level は 0 / 1 / 2 のいずれか。", 2);
736
+ return;
737
+ }
738
+ // deviceKey の解決: --json 優先 → --device で devices から検索 → 対話選択。
739
+ let deviceKey;
740
+ if (cmdOpts.json) {
741
+ deviceKey = ctx.parseJson(cmdOpts.json, '{"deviceUUID":"…","secretKey":"…","sesame2PublicKey":"…","keyIndex":"…","deviceModel":"sesame_5"}');
742
+ if (deviceKey === undefined) return;
743
+ } else {
744
+ const list = await hub.listDevices();
745
+ const devs = Array.isArray(list) ? list : [];
746
+ if (cmdOpts.device) {
747
+ deviceKey = devs.find((d) => d.deviceUUID === cmdOpts.device);
748
+ if (!deviceKey) { ctx.die(`deviceUUID ${cmdOpts.device} が devices に見つかりません。`, 2); return; }
749
+ } else if (ctx.canPrompt()) {
750
+ if (devs.length === 0) { ctx.die("共有できるデバイスがありません。", 2); return; }
751
+ deviceKey = await ctx.prompts.selectFromList(
752
+ "共有するデバイスを選択",
753
+ devs,
754
+ (d) => `${d.deviceName || "(no-name)"} ${d.deviceModel || "?"} ${d.deviceUUID}`,
755
+ );
756
+ if (!deviceKey) { console.error("キャンセルしました。"); return; }
757
+ } else {
758
+ ctx.die("--device <uuid> または --json <deviceKey> が必要です (非対話モード)。", 2);
759
+ return;
760
+ }
761
+ }
762
+
763
+ // guest (level 2) のみ使い捨て guestKeyId を発行 (biz3 と同じ。0/1 は deviceKey.secretKey)。
764
+ let guestKeyId;
765
+ if (level === 2) {
766
+ guestKeyId = await hub.org.generateGuestQR({ data: deviceKey });
767
+ }
768
+
769
+ const url = buildShareKeyUrl(deviceKey, { keyLevel: level, guestKeyId, name: cmdOpts.name });
770
+
771
+ // --qr 指定時のみ端末 QR を試みる (qrcode-terminal は任意依存。未導入なら案内のみ)。
772
+ let qrText = null;
773
+ if (cmdOpts.qr && !opts.json) {
774
+ try {
775
+ const { default: qrcodeTerminal } = await import("qrcode-terminal");
776
+ qrcodeTerminal.generate(url, { small: true }, (out) => { qrText = out; });
777
+ } catch {
778
+ qrText = "(qrcode-terminal 未インストール: `npm i qrcode-terminal` で端末 QR 表示)";
779
+ }
780
+ }
781
+
782
+ ctx.out(opts.json, () => {
783
+ console.log(url);
784
+ if (qrText) console.log(`\n${qrText}`);
785
+ }, { ok: true, url, level, guestKeyId: guestKeyId ?? null, deviceUUID: deviceKey.deviceUUID });
786
+ }),
787
+ );
788
+ }