@sayue_ltr/fleq 1.49.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +898 -0
  2. package/LICENSE +21 -0
  3. package/README.md +535 -0
  4. package/assets/icons/.gitkeep +0 -0
  5. package/assets/sounds/.gitkeep +0 -0
  6. package/assets/sounds/cancel.mp3 +0 -0
  7. package/assets/sounds/critical.mp3 +0 -0
  8. package/assets/sounds/info.mp3 +0 -0
  9. package/assets/sounds/normal.mp3 +0 -0
  10. package/assets/sounds/warning.mp3 +0 -0
  11. package/dist/config.js +638 -0
  12. package/dist/dmdata/connection-manager.js +2 -0
  13. package/dist/dmdata/endpoint-selector.js +185 -0
  14. package/dist/dmdata/multi-connection-manager.js +158 -0
  15. package/dist/dmdata/rest-client.js +281 -0
  16. package/dist/dmdata/telegram-parser.js +704 -0
  17. package/dist/dmdata/volcano-parser.js +647 -0
  18. package/dist/dmdata/ws-client.js +336 -0
  19. package/dist/engine/cli/cli-init.js +266 -0
  20. package/dist/engine/cli/cli-run.js +121 -0
  21. package/dist/engine/cli/cli.js +121 -0
  22. package/dist/engine/eew/eew-logger.js +355 -0
  23. package/dist/engine/eew/eew-tracker.js +229 -0
  24. package/dist/engine/messages/message-router.js +261 -0
  25. package/dist/engine/messages/tsunami-state.js +96 -0
  26. package/dist/engine/messages/volcano-state.js +131 -0
  27. package/dist/engine/messages/volcano-vfvo53-aggregator.js +173 -0
  28. package/dist/engine/monitor/monitor.js +118 -0
  29. package/dist/engine/monitor/repl-coordinator.js +63 -0
  30. package/dist/engine/monitor/shutdown.js +114 -0
  31. package/dist/engine/notification/node-notifier-loader.js +19 -0
  32. package/dist/engine/notification/notifier.js +338 -0
  33. package/dist/engine/notification/sound-player.js +230 -0
  34. package/dist/engine/notification/volcano-presentation.js +166 -0
  35. package/dist/engine/startup/config-resolver.js +139 -0
  36. package/dist/engine/startup/tsunami-initializer.js +91 -0
  37. package/dist/engine/startup/update-checker.js +229 -0
  38. package/dist/engine/startup/volcano-initializer.js +89 -0
  39. package/dist/index.js +24 -0
  40. package/dist/logger.js +95 -0
  41. package/dist/types.js +61 -0
  42. package/dist/ui/earthquake-formatter.js +871 -0
  43. package/dist/ui/eew-formatter.js +335 -0
  44. package/dist/ui/formatter.js +689 -0
  45. package/dist/ui/repl.js +2059 -0
  46. package/dist/ui/test-samples.js +880 -0
  47. package/dist/ui/theme.js +516 -0
  48. package/dist/ui/volcano-formatter.js +667 -0
  49. package/dist/ui/waiting-tips.js +227 -0
  50. package/dist/utils/intensity.js +13 -0
  51. package/dist/utils/secrets.js +14 -0
  52. package/package.json +69 -0
@@ -0,0 +1,336 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.WebSocketManager = void 0;
40
+ const ws_1 = __importDefault(require("ws"));
41
+ const rest_client_1 = require("./rest-client");
42
+ const endpoint_selector_1 = require("./endpoint-selector");
43
+ const log = __importStar(require("../logger"));
44
+ /** サーバーからの ping が途絶えたとみなすまでのミリ秒 */
45
+ const HEARTBEAT_TIMEOUT_MS = 90_000;
46
+ /** 再接続ジッターの最大値 (ミリ秒) */
47
+ const RECONNECT_JITTER_MS = 1_000;
48
+ /** 受信オブジェクトが WsDataMessage の必須フィールドを持つか確認 */
49
+ function isWsDataMessage(parsed) {
50
+ if (typeof parsed !== "object" || parsed == null)
51
+ return false;
52
+ const msg = parsed;
53
+ if (typeof msg["id"] !== "string")
54
+ return false;
55
+ if (typeof msg["head"] !== "object" || msg["head"] == null)
56
+ return false;
57
+ const head = msg["head"];
58
+ return typeof head["type"] === "string";
59
+ }
60
+ function isWsStartMessage(parsed) {
61
+ if (typeof parsed !== "object" || parsed == null)
62
+ return false;
63
+ const msg = parsed;
64
+ return typeof msg["socketId"] === "number" && Array.isArray(msg["classifications"]);
65
+ }
66
+ function isWsPingMessage(parsed) {
67
+ if (typeof parsed !== "object" || parsed == null)
68
+ return false;
69
+ const msg = parsed;
70
+ return typeof msg["pingId"] === "string";
71
+ }
72
+ class WebSocketManager {
73
+ config;
74
+ events;
75
+ ws = null;
76
+ reconnectAttempt = 0;
77
+ reconnectTimer = null;
78
+ heartbeatTimer = null;
79
+ shouldRun = true;
80
+ socketId = null;
81
+ previousSocketId = null;
82
+ heartbeatDeadlineAt = null;
83
+ endpointSelector = new endpoint_selector_1.EndpointSelector();
84
+ /** connect 世代番号 — close() 時にインクリメントして in-flight connect を無効化する */
85
+ connectSeq = 0;
86
+ constructor(config, events) {
87
+ this.config = config;
88
+ this.events = events;
89
+ }
90
+ /** 接続を開始する */
91
+ async connect() {
92
+ this.shouldRun = true;
93
+ const seq = ++this.connectSeq;
94
+ await this.doConnect(seq);
95
+ }
96
+ /** 接続状態を返す */
97
+ getStatus() {
98
+ return {
99
+ connected: this.ws != null && this.ws.readyState === ws_1.default.OPEN,
100
+ socketId: this.socketId,
101
+ reconnectAttempt: this.reconnectAttempt,
102
+ heartbeatDeadlineAt: this.heartbeatDeadlineAt,
103
+ };
104
+ }
105
+ /** 接続を停止する */
106
+ close() {
107
+ this.shouldRun = false;
108
+ this.connectSeq++;
109
+ this.clearTimers();
110
+ if (this.ws) {
111
+ this.ws.close(1000, "client shutdown");
112
+ this.ws = null;
113
+ }
114
+ this.heartbeatDeadlineAt = null;
115
+ }
116
+ clearTimers() {
117
+ if (this.reconnectTimer) {
118
+ clearTimeout(this.reconnectTimer);
119
+ this.reconnectTimer = null;
120
+ }
121
+ if (this.heartbeatTimer) {
122
+ clearTimeout(this.heartbeatTimer);
123
+ this.heartbeatTimer = null;
124
+ }
125
+ }
126
+ async doConnect(seq) {
127
+ try {
128
+ log.info("Socket Start を実行中...");
129
+ const startRes = await (0, rest_client_1.prepareAndStartSocket)(this.config, this.previousSocketId ?? undefined);
130
+ // close() が呼ばれていたら接続を中断
131
+ if (!this.shouldRun || seq !== this.connectSeq) {
132
+ log.debug("接続中断: close() が呼ばれたため新しいソケットを作成しません");
133
+ return;
134
+ }
135
+ if (!startRes.websocket) {
136
+ throw new Error("WebSocket URL が取得できませんでした");
137
+ }
138
+ const wsUrl = this.endpointSelector.resolveUrl(startRes.websocket.url);
139
+ log.info(`WebSocket に接続中: ${wsUrl.replace(/ticket=.*/, "ticket=***")}`);
140
+ const socket = new ws_1.default(wsUrl, ["dmdata.v2"]);
141
+ this.ws = socket;
142
+ socket.on("open", () => {
143
+ // 古いソケットのイベントが遅延到着した場合はスキップ
144
+ if (this.ws !== socket)
145
+ return;
146
+ this.reconnectAttempt = 0;
147
+ this.previousSocketId = null;
148
+ this.endpointSelector.recordConnected(wsUrl);
149
+ log.info("WebSocket 接続成功");
150
+ this.resetHeartbeat();
151
+ this.events.onConnected();
152
+ });
153
+ socket.on("message", (raw) => {
154
+ if (this.ws !== socket)
155
+ return;
156
+ this.handleMessage(raw);
157
+ });
158
+ socket.on("close", (code, reason) => {
159
+ // 古いソケット or 既に処理済みならスキップ
160
+ if (this.ws !== socket)
161
+ return;
162
+ const reasonStr = reason.toString() || `code=${code}`;
163
+ log.warn(`WebSocket 切断: ${reasonStr}`);
164
+ this.clearTimers();
165
+ this.ws = null;
166
+ this.previousSocketId = this.socketId;
167
+ this.socketId = null;
168
+ this.heartbeatDeadlineAt = null;
169
+ this.endpointSelector.recordDisconnected();
170
+ this.events.onDisconnected(reasonStr);
171
+ this.scheduleReconnect();
172
+ });
173
+ socket.on("error", (err) => {
174
+ log.error(`WebSocket エラー: ${err.message}`);
175
+ // 古いソケットのエラーは無視
176
+ if (this.ws !== socket)
177
+ return;
178
+ try {
179
+ socket.close();
180
+ }
181
+ catch {
182
+ // close() 自体の失敗は無視
183
+ }
184
+ this.clearTimers();
185
+ this.ws = null;
186
+ this.previousSocketId = this.socketId;
187
+ this.socketId = null;
188
+ this.heartbeatDeadlineAt = null;
189
+ this.endpointSelector.recordDisconnected();
190
+ this.events.onDisconnected(`error: ${err.message}`);
191
+ this.scheduleReconnect();
192
+ });
193
+ }
194
+ catch (err) {
195
+ log.error(`接続失敗: ${err instanceof Error ? err.message : err}`);
196
+ this.scheduleReconnect();
197
+ }
198
+ }
199
+ /** WebSocket.Data を文字列に安全に変換する */
200
+ static normalizeWsData(raw) {
201
+ if (typeof raw === "string")
202
+ return raw;
203
+ if (Buffer.isBuffer(raw))
204
+ return raw.toString("utf-8");
205
+ if (raw instanceof ArrayBuffer)
206
+ return Buffer.from(raw).toString("utf-8");
207
+ if (Array.isArray(raw))
208
+ return Buffer.concat(raw).toString("utf-8");
209
+ return String(raw);
210
+ }
211
+ handleMessage(raw) {
212
+ let parsed;
213
+ try {
214
+ const text = WebSocketManager.normalizeWsData(raw);
215
+ parsed = JSON.parse(text);
216
+ }
217
+ catch {
218
+ log.error("受信データのJSONパースに失敗");
219
+ return;
220
+ }
221
+ if (typeof parsed !== "object" || parsed == null) {
222
+ log.warn("受信データが不正な形式です");
223
+ return;
224
+ }
225
+ const messageObject = parsed;
226
+ const messageType = typeof messageObject["type"] === "string" ? messageObject["type"] : null;
227
+ switch (messageType) {
228
+ case "start":
229
+ this.handleStartMessage(parsed);
230
+ break;
231
+ case "ping":
232
+ this.handlePingMessage(parsed);
233
+ break;
234
+ case "pong":
235
+ log.debug("Pong 受信");
236
+ break;
237
+ case "data":
238
+ this.handleDataMessage(parsed);
239
+ break;
240
+ case "error":
241
+ this.logServerError(messageObject);
242
+ break;
243
+ default:
244
+ log.debug(`未知のメッセージタイプ: ${messageType ?? "(型なし)"}`);
245
+ }
246
+ }
247
+ handleStartMessage(parsed) {
248
+ if (!isWsStartMessage(parsed)) {
249
+ log.warn("start メッセージのスキーマが不正です");
250
+ return;
251
+ }
252
+ this.socketId = parsed.socketId;
253
+ log.info(`セッション開始: socketId=${parsed.socketId}`);
254
+ log.info(`区分: [${parsed.classifications.join(", ")}]`);
255
+ }
256
+ handlePingMessage(parsed) {
257
+ if (!isWsPingMessage(parsed)) {
258
+ log.warn("ping メッセージのスキーマが不正です");
259
+ return;
260
+ }
261
+ this.resetHeartbeat();
262
+ this.sendPong(parsed.pingId);
263
+ }
264
+ handleDataMessage(parsed) {
265
+ if (!isWsDataMessage(parsed)) {
266
+ log.warn("data メッセージのスキーマが不正です (id/head/head.type が欠落)");
267
+ return;
268
+ }
269
+ this.resetHeartbeat();
270
+ log.debug(`データ受信: type=${parsed.head.type}, id=${parsed.id.slice(0, 16)}...`);
271
+ this.events.onData(parsed);
272
+ }
273
+ logServerError(messageObject) {
274
+ const errorObj = messageObject["error"];
275
+ let errMsg;
276
+ let errCode;
277
+ if (typeof errorObj === "object" && errorObj != null) {
278
+ // error がオブジェクト形式: { error: { message, code } }
279
+ const e = errorObj;
280
+ errMsg = String(e["message"] ?? "unknown");
281
+ errCode = String(e["code"] ?? "unknown");
282
+ }
283
+ else if (typeof errorObj === "string") {
284
+ // error が文字列形式: { error: "Closed by user.", code: 4808 }
285
+ errMsg = errorObj;
286
+ errCode = String(messageObject["code"] ?? "unknown");
287
+ }
288
+ else {
289
+ errMsg = JSON.stringify(messageObject);
290
+ errCode = "unknown";
291
+ }
292
+ log.error(`サーバーエラー: ${errMsg} (code=${errCode})`);
293
+ }
294
+ sendPong(pingId) {
295
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
296
+ this.ws.send(JSON.stringify({ type: "pong", pingId }));
297
+ log.debug(`Pong 送信: pingId=${pingId}`);
298
+ }
299
+ }
300
+ /** ハートビートタイマーをリセット (ping/data 受信時に呼ぶ) */
301
+ resetHeartbeat() {
302
+ if (this.heartbeatTimer) {
303
+ clearTimeout(this.heartbeatTimer);
304
+ }
305
+ this.heartbeatDeadlineAt = Date.now() + HEARTBEAT_TIMEOUT_MS;
306
+ this.heartbeatTimer = setTimeout(() => {
307
+ log.warn(`ハートビートタイムアウト: ${HEARTBEAT_TIMEOUT_MS / 1000}秒間 ping を受信していません`);
308
+ this.heartbeatDeadlineAt = null;
309
+ if (this.ws) {
310
+ this.ws.close(4000, "heartbeat timeout");
311
+ }
312
+ }, HEARTBEAT_TIMEOUT_MS);
313
+ }
314
+ /** 指数バックオフで再接続をスケジュール */
315
+ scheduleReconnect() {
316
+ if (!this.shouldRun)
317
+ return;
318
+ // 既にタイマーがスケジュール済みなら重複を防止
319
+ if (this.reconnectTimer) {
320
+ log.debug("再接続タイマーは既にスケジュール済みです");
321
+ return;
322
+ }
323
+ this.reconnectAttempt++;
324
+ // 指数バックオフ: 1, 2, 4, 8, ... 秒(上限あり)+ ジッター
325
+ const baseDelay = Math.min(Math.pow(2, this.reconnectAttempt - 1) * 1000, this.config.maxReconnectDelaySec * 1000);
326
+ const jitter = Math.random() * RECONNECT_JITTER_MS;
327
+ const delay = baseDelay + jitter;
328
+ log.info(`${(delay / 1000).toFixed(1)}秒後に再接続します (試行 #${this.reconnectAttempt})`);
329
+ const currentSeq = this.connectSeq;
330
+ this.reconnectTimer = setTimeout(async () => {
331
+ this.reconnectTimer = null;
332
+ await this.doConnect(currentSeq);
333
+ }, delay);
334
+ }
335
+ }
336
+ exports.WebSocketManager = WebSocketManager;
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.runInit = runInit;
40
+ const readline_1 = __importDefault(require("readline"));
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const config_1 = require("../../config");
43
+ const rest_client_1 = require("../../dmdata/rest-client");
44
+ const secretUtils = __importStar(require("../../utils/secrets"));
45
+ const log = __importStar(require("../../logger"));
46
+ /** 区分選択肢メタデータ */
47
+ const CLASSIFICATION_OPTIONS = [
48
+ {
49
+ value: "telegram.earthquake",
50
+ label: "地震・津波関連",
51
+ description: "地震情報、津波情報、震源・震度情報など",
52
+ },
53
+ {
54
+ value: "eew.forecast",
55
+ label: "緊急地震速報(予報)",
56
+ description: "予報レベルのEEWを受信します",
57
+ },
58
+ {
59
+ value: "eew.warning",
60
+ label: "緊急地震速報(警報)",
61
+ description: "警報レベルのEEWを受信します",
62
+ },
63
+ {
64
+ value: "telegram.volcano",
65
+ label: "火山関連",
66
+ description: "噴火警報、噴火速報、降灰予報、火山の状況に関する解説情報など",
67
+ },
68
+ ];
69
+ /** テストモード選択肢メタデータ */
70
+ const TEST_MODE_OPTIONS = [
71
+ {
72
+ value: "no",
73
+ label: "受信しない",
74
+ description: "通常運用向け。テスト電文は無視します",
75
+ },
76
+ {
77
+ value: "including",
78
+ label: "通常電文 + テスト電文",
79
+ description: "動作確認したいとき向けです",
80
+ },
81
+ {
82
+ value: "only",
83
+ label: "テスト電文のみ",
84
+ description: "本番電文を混ぜずに検証したいとき向けです",
85
+ },
86
+ ];
87
+ /** readline を使ったテキスト入力ヘルパー */
88
+ function askText(rl, prompt) {
89
+ return new Promise((resolve) => {
90
+ rl.question(prompt, (answer) => resolve(answer.trim()));
91
+ });
92
+ }
93
+ /** Y/n 確認ヘルパー (デフォルト Yes) */
94
+ async function askConfirm(rl, prompt) {
95
+ const answer = await askText(rl, prompt);
96
+ if (answer === "")
97
+ return true;
98
+ return answer.toLowerCase().startsWith("y");
99
+ }
100
+ /** 番号選択ヘルパー (単一選択、1-indexed) */
101
+ async function askSingleChoice(rl, options, defaultValue) {
102
+ const defaultIdx = options.findIndex((o) => o.value === defaultValue);
103
+ const defaultNum = defaultIdx >= 0 ? defaultIdx + 1 : 1;
104
+ const input = await askText(rl, ` 選択 [${chalk_1.default.gray(String(defaultNum))}]: `);
105
+ if (input === "") {
106
+ return options[defaultNum - 1].value;
107
+ }
108
+ const num = parseInt(input, 10);
109
+ if (isNaN(num) || num < 1 || num > options.length) {
110
+ console.log(chalk_1.default.yellow(` → 無効な入力です。既定値を使用します。`));
111
+ return options[defaultNum - 1].value;
112
+ }
113
+ return options[num - 1].value;
114
+ }
115
+ /** 番号選択ヘルパー (複数選択、スペース区切り、1-indexed) */
116
+ async function askMultiChoice(rl, options, defaultValues) {
117
+ const defaultNums = defaultValues
118
+ .map((v) => options.findIndex((o) => o.value === v) + 1)
119
+ .filter((n) => n > 0);
120
+ const defaultDisplay = defaultNums.join(" ");
121
+ const input = await askText(rl, ` 選択 [${chalk_1.default.gray(defaultDisplay)}]: `);
122
+ if (input === "") {
123
+ return defaultValues.length > 0 ? defaultValues : options.map((o) => o.value);
124
+ }
125
+ const nums = input
126
+ .split(/[\s,]+/)
127
+ .map((s) => parseInt(s, 10))
128
+ .filter((n) => !isNaN(n) && n >= 1 && n <= options.length);
129
+ if (nums.length === 0) {
130
+ console.log(chalk_1.default.yellow(` → 無効な入力です。既定値を使用します。`));
131
+ return defaultValues.length > 0 ? defaultValues : options.map((o) => o.value);
132
+ }
133
+ return [...new Set(nums)].sort((a, b) => a - b).map((n) => options[n - 1].value);
134
+ }
135
+ /** 区分の表示用ラベルを取得 */
136
+ function classificationLabel(value) {
137
+ const found = CLASSIFICATION_OPTIONS.find((o) => o.value === value);
138
+ return found ? found.label : value;
139
+ }
140
+ /** テストモードの表示用ラベルを取得 */
141
+ function testModeLabel(value) {
142
+ const found = TEST_MODE_OPTIONS.find((o) => o.value === value);
143
+ return found ? found.label : value;
144
+ }
145
+ /** インタラクティブに初期設定を行う */
146
+ async function runInit() {
147
+ const existingConfig = (0, config_1.loadConfig)();
148
+ console.log();
149
+ console.log(chalk_1.default.cyan.bold(" fleq 初期設定"));
150
+ console.log(chalk_1.default.gray(" ─────────────────────────────"));
151
+ console.log(chalk_1.default.gray(" dmdata.jp のAPIキーと受信設定を行います。"));
152
+ console.log(chalk_1.default.gray(" Enter で既定値を採用します。"));
153
+ console.log();
154
+ if (existingConfig.apiKey) {
155
+ console.log(chalk_1.default.gray(" 既存の設定が見つかりました。空のままEnterで既存の値を維持します。"));
156
+ console.log();
157
+ }
158
+ const rl = readline_1.default.createInterface({
159
+ input: process.stdin,
160
+ output: process.stdout,
161
+ });
162
+ try {
163
+ // ── [1/4] APIキー ──
164
+ console.log(chalk_1.default.white.bold(" [1/4] dmdata.jp APIキー"));
165
+ console.log(chalk_1.default.gray(" 取得先: https://manager.dmdata.jp/control/apikey"));
166
+ console.log(chalk_1.default.gray(" ヒント: マイページの「APIキー」から発行・確認できます"));
167
+ if (existingConfig.apiKey) {
168
+ console.log(chalk_1.default.gray(` 現在: ${secretUtils.maskApiKey(existingConfig.apiKey)}`));
169
+ }
170
+ console.log();
171
+ const apiKeyInput = await askText(rl, " APIキー: ");
172
+ const apiKey = apiKeyInput.length > 0 ? apiKeyInput : existingConfig.apiKey;
173
+ if (!apiKey) {
174
+ log.error("APIキーは必須です。");
175
+ rl.close();
176
+ process.exit(1);
177
+ }
178
+ // ── [2/4] 契約確認 ──
179
+ console.log();
180
+ console.log(chalk_1.default.white.bold(" [2/4] 契約確認"));
181
+ console.log(chalk_1.default.gray(" 契約状況を確認中..."));
182
+ let contractedClassifications = [];
183
+ try {
184
+ contractedClassifications = await (0, rest_client_1.listContracts)(apiKey);
185
+ if (contractedClassifications.length > 0) {
186
+ const labels = contractedClassifications.map((c) => classificationLabel(c));
187
+ console.log(chalk_1.default.green(` 契約済み: ${labels.join(", ")}`));
188
+ }
189
+ else {
190
+ console.log(chalk_1.default.yellow(" 契約済みの区分が見つかりません。"));
191
+ }
192
+ }
193
+ catch (err) {
194
+ log.warn(`契約確認に失敗しました: ${err instanceof Error ? err.message : err}`);
195
+ console.log(chalk_1.default.yellow(" APIキーの確認ができませんでした。既定値を使用します。"));
196
+ }
197
+ // ── [3/4] 受信区分 ──
198
+ console.log();
199
+ console.log(chalk_1.default.white.bold(" [3/4] 受信区分"));
200
+ console.log(chalk_1.default.gray(" 受信したい情報を選んでください。"));
201
+ console.log(chalk_1.default.gray(" 番号をスペース区切りで入力します。Enter で既定値を採用します。"));
202
+ console.log();
203
+ // デフォルト値: 既存config → 契約済み区分 → 全区分
204
+ const defaultClassifications = existingConfig.classifications != null && existingConfig.classifications.length > 0
205
+ ? existingConfig.classifications
206
+ : contractedClassifications.filter((c) => config_1.VALID_CLASSIFICATIONS.includes(c)).length > 0
207
+ ? contractedClassifications.filter((c) => config_1.VALID_CLASSIFICATIONS.includes(c))
208
+ : [...config_1.VALID_CLASSIFICATIONS];
209
+ // 選択肢表示
210
+ for (let i = 0; i < CLASSIFICATION_OPTIONS.length; i++) {
211
+ const opt = CLASSIFICATION_OPTIONS[i];
212
+ const isDefault = defaultClassifications.includes(opt.value);
213
+ const checkbox = isDefault ? chalk_1.default.green("[x]") : chalk_1.default.gray("[ ]");
214
+ console.log(` ${checkbox} ${chalk_1.default.white(`${i + 1}. ${opt.label}`)}`);
215
+ console.log(chalk_1.default.gray(` ${opt.description}`));
216
+ }
217
+ console.log();
218
+ const selectedClassifications = await askMultiChoice(rl, CLASSIFICATION_OPTIONS, defaultClassifications);
219
+ // ── [4/4] テストモード ──
220
+ console.log();
221
+ console.log(chalk_1.default.white.bold(" [4/4] テスト電文"));
222
+ console.log(chalk_1.default.gray(" テスト配信をどう扱うか選んでください。"));
223
+ console.log();
224
+ const currentTestMode = existingConfig.testMode ?? "no";
225
+ for (let i = 0; i < TEST_MODE_OPTIONS.length; i++) {
226
+ const opt = TEST_MODE_OPTIONS[i];
227
+ const marker = opt.value === currentTestMode ? chalk_1.default.green("→") : " ";
228
+ console.log(` ${marker} ${chalk_1.default.white(`${i + 1}. ${opt.label}`)}`);
229
+ console.log(chalk_1.default.gray(` ${opt.description}`));
230
+ }
231
+ console.log();
232
+ const selectedTestMode = await askSingleChoice(rl, TEST_MODE_OPTIONS, currentTestMode);
233
+ // ── 確認 ──
234
+ console.log();
235
+ console.log(chalk_1.default.white.bold(" 設定内容"));
236
+ console.log(chalk_1.default.gray(" ─────────────────────────────"));
237
+ console.log(` APIキー: ${chalk_1.default.white(secretUtils.maskApiKey(apiKey))}`);
238
+ console.log(` 受信区分: ${chalk_1.default.white(selectedClassifications.map((c) => classificationLabel(c)).join(", "))}`);
239
+ console.log(` テスト電文: ${chalk_1.default.white(testModeLabel(selectedTestMode))}`);
240
+ console.log();
241
+ const confirmed = await askConfirm(rl, ` この内容で保存しますか? [${chalk_1.default.white("Y")}/n]: `);
242
+ if (!confirmed) {
243
+ console.log();
244
+ console.log(chalk_1.default.yellow(" 設定を保存せずに終了します。"));
245
+ console.log();
246
+ return;
247
+ }
248
+ // ── 保存 ──
249
+ const config = {
250
+ ...existingConfig,
251
+ apiKey,
252
+ classifications: selectedClassifications,
253
+ testMode: selectedTestMode,
254
+ };
255
+ (0, config_1.saveConfig)(config);
256
+ console.log();
257
+ console.log(chalk_1.default.green(" 設定を保存しました。"));
258
+ console.log(chalk_1.default.gray(` ファイル: ${(0, config_1.getConfigPath)()}`));
259
+ console.log();
260
+ console.log(chalk_1.default.white(" fleq を実行してモニタリングを開始できます。"));
261
+ console.log();
262
+ }
263
+ finally {
264
+ rl.close();
265
+ }
266
+ }