@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,2059 @@
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.ReplHandler = void 0;
40
+ const readline_1 = __importDefault(require("readline"));
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const types_1 = require("../types");
43
+ const rest_client_1 = require("../dmdata/rest-client");
44
+ const config_1 = require("../config");
45
+ const notifier_1 = require("../engine/notification/notifier");
46
+ const formatter_1 = require("../ui/formatter");
47
+ const themeModule = __importStar(require("../ui/theme"));
48
+ const sound_player_1 = require("../engine/notification/sound-player");
49
+ const log = __importStar(require("../logger"));
50
+ const logger_1 = require("../logger");
51
+ const waiting_tips_1 = require("./waiting-tips");
52
+ const test_samples_1 = require("./test-samples");
53
+ /** カテゴリ表示名 */
54
+ const CATEGORY_LABELS = {
55
+ info: "情報",
56
+ status: "ステータス",
57
+ settings: "設定",
58
+ operation: "操作",
59
+ };
60
+ /** EEW ログ記録項目の表示ラベル */
61
+ const EEW_LOG_FIELD_LABELS = {
62
+ hypocenter: "震源情報",
63
+ originTime: "発生時刻",
64
+ coordinates: "緯度・経度",
65
+ magnitude: "M値・深さ",
66
+ forecastIntensity: "最大予測震度",
67
+ maxLgInt: "最大予測長周期階級",
68
+ forecastAreas: "予測震度地域リスト",
69
+ lgIntensity: "地域別長周期階級",
70
+ isPlum: "PLUM法フラグ",
71
+ hasArrived: "主要動到達フラグ",
72
+ diff: "差分情報",
73
+ maxIntChangeReason: "震度変化理由",
74
+ };
75
+ /** EEW ログ記録項目のグループ定義 */
76
+ const EEW_LOG_FIELD_GROUPS = [
77
+ { label: "震源", fields: ["hypocenter", "originTime", "coordinates"] },
78
+ { label: "規模", fields: ["magnitude"] },
79
+ { label: "変化", fields: ["diff", "maxIntChangeReason"] },
80
+ { label: "予測概要", fields: ["forecastIntensity", "maxLgInt"] },
81
+ { label: "予測地域", fields: ["forecastAreas", "lgIntensity", "isPlum", "hasArrived"] },
82
+ ];
83
+ class StatusLine {
84
+ pulseOn = true;
85
+ connectedAt = null;
86
+ lastMessageTime = null;
87
+ clockMode = "elapsed";
88
+ tick() {
89
+ this.pulseOn = !this.pulseOn;
90
+ }
91
+ setConnected(connected) {
92
+ if (connected) {
93
+ this.connectedAt = Date.now();
94
+ this.lastMessageTime = null;
95
+ }
96
+ else {
97
+ this.connectedAt = null;
98
+ this.lastMessageTime = null;
99
+ }
100
+ }
101
+ markMessageReceived() {
102
+ this.lastMessageTime = Date.now();
103
+ }
104
+ setClockMode(mode) {
105
+ this.clockMode = mode;
106
+ }
107
+ getClockMode() {
108
+ return this.clockMode;
109
+ }
110
+ buildPrefix(options) {
111
+ const suffix = options?.noSuffix ? "" : chalk_1.default.gray("]> ");
112
+ if (this.connectedAt == null) {
113
+ return (chalk_1.default.gray("FlEq [") + chalk_1.default.gray("○ --:--:--") + suffix);
114
+ }
115
+ const dot = this.pulseOn ? chalk_1.default.cyan("●") : chalk_1.default.gray("○");
116
+ const timeStr = this.clockMode === "clock"
117
+ ? formatCurrentTime()
118
+ : (0, formatter_1.formatElapsedTime)(Date.now() - (this.lastMessageTime ?? this.connectedAt));
119
+ return (chalk_1.default.gray("FlEq [") +
120
+ dot +
121
+ chalk_1.default.gray(" ") +
122
+ chalk_1.default.white(timeStr) +
123
+ suffix);
124
+ }
125
+ /** ログ出力用プレフィックス */
126
+ buildLogPrefix() {
127
+ if (this.connectedAt == null) {
128
+ return (chalk_1.default.gray("FlEq [") + chalk_1.default.gray("○ --:--:--") + chalk_1.default.gray("]> "));
129
+ }
130
+ const dot = this.pulseOn ? chalk_1.default.cyan("●") : chalk_1.default.gray("○");
131
+ const timeStr = this.clockMode === "clock"
132
+ ? formatCurrentTime()
133
+ : (0, formatter_1.formatElapsedTime)(Date.now() - (this.lastMessageTime ?? this.connectedAt));
134
+ return (chalk_1.default.gray("FlEq [") +
135
+ dot +
136
+ chalk_1.default.gray(" ") +
137
+ chalk_1.default.white(timeStr) +
138
+ chalk_1.default.gray("]> "));
139
+ }
140
+ getLastMessageTime() {
141
+ return this.lastMessageTime;
142
+ }
143
+ }
144
+ /** 現在時刻を HH:mm:ss 形式で返す */
145
+ function formatCurrentTime() {
146
+ const now = new Date();
147
+ const pad = (n) => String(n).padStart(2, "0");
148
+ return `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
149
+ }
150
+ /** 震度キーから対応するロール名を返す */
151
+ function getIntensityRole(key) {
152
+ const map = {
153
+ "1": "intensity1", "2": "intensity2", "3": "intensity3", "4": "intensity4",
154
+ "5弱": "intensity5Lower", "5強": "intensity5Upper",
155
+ "6弱": "intensity6Lower", "6強": "intensity6Upper", "7": "intensity7",
156
+ };
157
+ return map[key] ?? null;
158
+ }
159
+ /** 長周期階級キーから対応するロール名を返す */
160
+ function getLgIntRole(key) {
161
+ const map = {
162
+ "0": "lgInt0", "1": "lgInt1", "2": "lgInt2", "3": "lgInt3", "4": "lgInt4",
163
+ };
164
+ return map[key] ?? null;
165
+ }
166
+ /** 構造的型ガード: backup 機能を持つ ConnectionManager か */
167
+ function hasBackupSupport(m) {
168
+ return ("startBackup" in m &&
169
+ "stopBackup" in m &&
170
+ "isBackupRunning" in m &&
171
+ "getBackupStatus" in m);
172
+ }
173
+ /** レーベンシュタイン距離 (typo候補用) */
174
+ function levenshtein(a, b) {
175
+ const m = a.length;
176
+ const n = b.length;
177
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
178
+ for (let i = 0; i <= m; i++)
179
+ dp[i][0] = i;
180
+ for (let j = 0; j <= n; j++)
181
+ dp[0][j] = j;
182
+ for (let i = 1; i <= m; i++) {
183
+ for (let j = 1; j <= n; j++) {
184
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
185
+ }
186
+ }
187
+ return dp[m][n];
188
+ }
189
+ class ReplHandler {
190
+ config;
191
+ wsManager;
192
+ notifier;
193
+ eewLogger;
194
+ onQuit;
195
+ rl = null;
196
+ commands;
197
+ stopping = false;
198
+ statusLine;
199
+ statusTimer = null;
200
+ commandRunning = false;
201
+ tipIntervalMs;
202
+ nextTipAt = null;
203
+ tipIndex = 0;
204
+ statusProviders;
205
+ detailProviders;
206
+ constructor(config, wsManager, notifier, eewLogger, onQuit, statusProviders = [], detailProviders = []) {
207
+ this.config = config;
208
+ this.wsManager = wsManager;
209
+ this.notifier = notifier;
210
+ this.eewLogger = eewLogger;
211
+ this.onQuit = onQuit;
212
+ this.statusProviders = statusProviders;
213
+ this.detailProviders = detailProviders;
214
+ this.statusLine = new StatusLine();
215
+ this.statusLine.setClockMode(this.config.promptClock);
216
+ this.tipIntervalMs = this.config.waitTipIntervalMin * 60 * 1000;
217
+ this.tipIndex = Math.floor(Math.random() * waiting_tips_1.WAITING_TIPS.length);
218
+ this.commands = {
219
+ help: {
220
+ description: "コマンド一覧を表示 (例: help status)",
221
+ detail: "引数なしで一覧表示。help <command> でコマンドの詳細を表示。",
222
+ category: "info",
223
+ handler: (args) => this.handleHelp(args),
224
+ },
225
+ "?": {
226
+ description: "help のエイリアス",
227
+ category: "info",
228
+ handler: (args) => this.handleHelp(args),
229
+ },
230
+ history: {
231
+ description: "地震履歴を取得・表示 (例: history 5)",
232
+ detail: "dmdata.jp API から直近の地震履歴を取得します。\n 引数: 件数 (1〜100, デフォルト10)\n 例: history 20",
233
+ category: "info",
234
+ handler: (args) => this.handleHistory(args),
235
+ },
236
+ colors: {
237
+ description: "カラーパレット・震度色の一覧を表示",
238
+ detail: "CUD (カラーユニバーサルデザイン) パレットと、\n 震度・長周期地震動階級・フレームレベルに対応する色を確認できます。",
239
+ category: "info",
240
+ handler: () => this.handleColors(),
241
+ },
242
+ detail: {
243
+ description: "直近の情報を再表示 (例: detail tsunami, detail volcano)",
244
+ detail: "引数なし: 津波情報を再表示 (デフォルト)\n detail tsunami: 津波情報を再表示\n detail volcano: 火山警報状態を再表示",
245
+ category: "info",
246
+ subcommands: {
247
+ tsunami: { description: "津波情報を再表示" },
248
+ volcano: { description: "火山警報状態を再表示" },
249
+ },
250
+ handler: (args) => this.handleDetail(args),
251
+ },
252
+ status: {
253
+ description: "WebSocket 接続状態を表示",
254
+ detail: "現在の WebSocket 接続状態、SocketID、再接続試行回数を表示します。",
255
+ category: "status",
256
+ handler: () => this.handleStatus(),
257
+ },
258
+ config: {
259
+ description: "現在の設定を表示",
260
+ detail: "Configファイルに保存された設定を一覧表示します。",
261
+ category: "status",
262
+ handler: () => this.handleConfig(),
263
+ },
264
+ contract: {
265
+ description: "契約区分一覧を表示",
266
+ detail: "dmdata.jp で契約している区分を API から取得して表示します。",
267
+ category: "status",
268
+ handler: () => this.handleContract(),
269
+ },
270
+ socket: {
271
+ description: "接続中のソケット一覧を表示",
272
+ detail: "dmdata.jp で現在開いているソケット一覧を表示します。",
273
+ category: "status",
274
+ handler: () => this.handleSocket(),
275
+ },
276
+ notify: {
277
+ description: "通知設定の表示・切替 (例: notify eew on)",
278
+ detail: "引数なし: 現在の通知設定を一覧表示\n notify <category>: トグル切替\n notify <category> on: 有効にする\n notify <category> off: 無効にする\n notify all:on / all:off: 一括操作\n カテゴリ: eew, earthquake, tsunami, seismicText, nankaiTrough, lgObservation, volcano",
279
+ category: "settings",
280
+ subcommands: {
281
+ "<category>": { description: "トグル切替 / on / off" },
282
+ "all:on": { description: "全カテゴリを有効にする" },
283
+ "all:off": { description: "全カテゴリを無効にする" },
284
+ },
285
+ handler: (args) => this.handleNotify(args),
286
+ },
287
+ eewlog: {
288
+ description: "EEWログ記録の設定 (例: eewlog on / eewlog fields)",
289
+ detail: "eewlog: 現在のログ記録設定を表示\n eewlog on: ログ記録を有効にする\n eewlog off: ログ記録を無効にする\n eewlog fields: 記録項目の一覧表示 (グループ別)\n eewlog fields <field>: 項目のトグル切替\n eewlog fields <field> on/off: 項目の有効/無効\n [震源] hypocenter, originTime, coordinates\n [規模] magnitude\n [変化] diff, maxIntChangeReason\n [予測概要] forecastIntensity, maxLgInt\n [予測地域] forecastAreas, lgIntensity, isPlum, hasArrived",
290
+ category: "settings",
291
+ subcommands: {
292
+ on: { description: "ログ記録を有効にする" },
293
+ off: { description: "ログ記録を無効にする" },
294
+ fields: { description: "記録項目の一覧・切替" },
295
+ },
296
+ handler: (args) => this.handleEewLog(args),
297
+ },
298
+ tablewidth: {
299
+ description: "テーブル幅の表示・変更 (例: tablewidth 80 / tablewidth auto)",
300
+ detail: "引数なし: 現在のテーブル幅を表示\n tablewidth <40〜200>: テーブル幅を固定値に変更\n tablewidth auto: ターミナル幅に自動追従 (デフォルト)\n 変更は即座に反映され、Configファイルに保存されます。",
301
+ category: "settings",
302
+ subcommands: {
303
+ "<40-200>": { description: "テーブル幅を固定値に変更" },
304
+ auto: { description: "ターミナル幅に自動追従" },
305
+ },
306
+ handler: (args) => this.handleTableWidth(args),
307
+ },
308
+ infotext: {
309
+ description: "お知らせ電文の全文/省略切替 (例: infotext full)",
310
+ detail: "infotext full: 全文表示\n infotext short: 省略表示 (デフォルト)",
311
+ category: "settings",
312
+ subcommands: {
313
+ full: { description: "全文表示" },
314
+ short: { description: "省略表示 (デフォルト)" },
315
+ },
316
+ handler: (args) => this.handleInfoText(args),
317
+ },
318
+ tipinterval: {
319
+ description: "待機中ヒント表示間隔の表示・変更 (例: tipinterval 15)",
320
+ detail: "tipinterval: 現在のヒント間隔(分)を表示\n tipinterval <0〜1440>: ヒント間隔を分で変更 (0で無効)",
321
+ category: "settings",
322
+ subcommands: {
323
+ "<0-1440>": { description: "ヒント間隔を分で変更 (0で無効)" },
324
+ },
325
+ handler: (args) => this.handleTipInterval(args),
326
+ },
327
+ mode: {
328
+ description: "表示モード切替 (例: mode compact)",
329
+ detail: "mode: 現在のモードを表示\n mode normal: フルフレーム表示 (デフォルト)\n mode compact: 1行サマリー表示\n 長時間モニタリング時は compact がおすすめです。",
330
+ category: "settings",
331
+ subcommands: {
332
+ normal: { description: "フルフレーム表示 (デフォルト)" },
333
+ compact: { description: "1行サマリー表示" },
334
+ },
335
+ handler: (args) => this.handleMode(args),
336
+ },
337
+ clock: {
338
+ description: "プロンプト時計の切替 (例: clock / clock elapsed)",
339
+ detail: "clock: 経過時間/現在時刻をトグル切替\n clock elapsed: 経過時間表示 (デフォルト)\n clock now: 現在時刻表示",
340
+ category: "settings",
341
+ subcommands: {
342
+ elapsed: { description: "経過時間表示 (デフォルト)" },
343
+ now: { description: "現在時刻表示" },
344
+ },
345
+ handler: (args) => this.handleClock(args),
346
+ },
347
+ sound: {
348
+ description: "通知音の ON/OFF 切替",
349
+ detail: "sound: 現在の状態を表示\n sound on: 通知音を有効にする\n sound off: 通知音を無効にする",
350
+ category: "settings",
351
+ subcommands: {
352
+ on: { description: "通知音を有効にする" },
353
+ off: { description: "通知音を無効にする" },
354
+ },
355
+ handler: (args) => this.handleSound(args),
356
+ },
357
+ theme: {
358
+ description: "カラーテーマの表示・管理 (例: theme path / theme reload)",
359
+ detail: "theme: テーマ概要を表示\n theme path: theme.json のパスを表示\n theme show: 全パレット色・全ロールスタイルを一覧表示\n theme reset: デフォルト theme.json を書き出し\n theme reload: theme.json を再読込\n theme validate: theme.json を検証",
360
+ category: "settings",
361
+ subcommands: {
362
+ path: { description: "theme.json のパスを表示" },
363
+ show: { description: "全パレット色・ロールスタイル一覧" },
364
+ reset: { description: "デフォルト theme.json を書き出し" },
365
+ reload: { description: "theme.json を再読込" },
366
+ validate: { description: "theme.json を検証" },
367
+ },
368
+ handler: (args) => this.handleTheme(args),
369
+ },
370
+ mute: {
371
+ description: "通知を一時ミュート (例: mute 30m)",
372
+ detail: "mute: 現在のミュート状態を表示\n mute <duration>: 指定時間ミュート (例: 30m, 1h, 90s)\n mute off: ミュート解除",
373
+ category: "settings",
374
+ subcommands: {
375
+ "<duration>": { description: "指定時間ミュート (例: 30m, 1h)" },
376
+ off: { description: "ミュート解除" },
377
+ },
378
+ handler: (args) => this.handleMute(args),
379
+ },
380
+ fold: {
381
+ description: "観測点の表示件数制限 (例: fold 10 / fold off)",
382
+ detail: "fold: 現在の設定を表示\n fold <N>: 上位N件に制限\n fold off: 全件表示に戻す",
383
+ category: "settings",
384
+ subcommands: {
385
+ "<N>": { description: "観測点を上位N件に制限 (1〜999)" },
386
+ off: { description: "全件表示に戻す" },
387
+ },
388
+ handler: (args) => this.handleFold(args),
389
+ },
390
+ limit: {
391
+ description: "省略表示の上限設定 (例: limit volcanoAlertLines 15)",
392
+ detail: "limit: 現在の省略設定を一覧表示\n limit <key> <N>: 上限値を変更 (1〜999)\n limit <key> default: デフォルト値に戻す\n limit reset: 全項目をデフォルトに戻す",
393
+ category: "settings",
394
+ subcommands: {
395
+ "<key> <N>": { description: "上限値を変更 (1〜999)" },
396
+ "<key> default": { description: "デフォルト値に戻す" },
397
+ reset: { description: "全項目をデフォルトに戻す" },
398
+ },
399
+ handler: (args) => this.handleLimit(args),
400
+ },
401
+ test: {
402
+ description: "テスト機能",
403
+ detail: "test sound [level]: サウンドテスト\n test table [type] [番号]: 表示形式テスト",
404
+ category: "operation",
405
+ subcommands: {
406
+ sound: {
407
+ description: "サウンドテスト",
408
+ detail: "引数なし: 利用可能なサウンドレベル一覧を表示\n test sound <level>: 指定レベルのサウンドを再生\n レベル: critical, warning, normal, info, cancel",
409
+ },
410
+ table: {
411
+ description: "表示形式テスト",
412
+ detail: "引数なし: 利用可能な電文タイプ一覧を表示\n test table <type>: バリエーション一覧を表示\n test table <type> <番号>: 指定バリエーションを表示\n タイプ: earthquake, eew, tsunami, seismicText, nankaiTrough, lgObservation, volcano",
413
+ },
414
+ },
415
+ handler: (args) => this.handleTest(args),
416
+ },
417
+ clear: {
418
+ description: "ターミナル画面をクリア",
419
+ category: "operation",
420
+ handler: () => this.handleClear(),
421
+ },
422
+ backup: {
423
+ description: "EEW副回線の起動/停止 (例: backup on)",
424
+ detail: "backup: 副回線の状態を表示\n backup on: 副回線を起動\n backup off: 副回線を停止",
425
+ category: "operation",
426
+ subcommands: {
427
+ on: { description: "副回線を起動" },
428
+ off: { description: "副回線を停止" },
429
+ },
430
+ handler: (args) => this.handleBackup(args),
431
+ },
432
+ retry: {
433
+ description: "WebSocket 再接続を試行",
434
+ detail: "切断中の場合に手動で再接続を試みます。",
435
+ category: "operation",
436
+ handler: () => this.handleRetry(),
437
+ },
438
+ quit: {
439
+ description: "アプリケーションを終了",
440
+ category: "operation",
441
+ handler: () => this.handleQuit(),
442
+ },
443
+ exit: {
444
+ description: "quit のエイリアス",
445
+ category: "operation",
446
+ handler: () => this.handleQuit(),
447
+ },
448
+ };
449
+ }
450
+ /** REPL を開始する */
451
+ start() {
452
+ // ロガーのプレフィックスを StatusLine に連動させる
453
+ (0, logger_1.setLogPrefixBuilder)(() => this.statusLine.buildLogPrefix());
454
+ // ログ出力前にプロンプト行をクリアし、出力後に再描画する
455
+ (0, logger_1.setLogHooks)({
456
+ beforeLog: () => {
457
+ if (process.stdout.isTTY && this.rl) {
458
+ readline_1.default.cursorTo(process.stdout, 0);
459
+ readline_1.default.clearLine(process.stdout, 0);
460
+ }
461
+ },
462
+ afterLog: () => {
463
+ if (this.rl && !this.stopping) {
464
+ this.rl.setPrompt(this.buildPromptString());
465
+ this.rl.prompt();
466
+ }
467
+ },
468
+ });
469
+ this.rl = readline_1.default.createInterface({
470
+ input: process.stdin,
471
+ output: process.stdout,
472
+ prompt: this.buildPromptString(),
473
+ });
474
+ if (process.stdout.isTTY) {
475
+ this.resetTipSchedule();
476
+ this.statusTimer = setInterval(() => {
477
+ this.statusLine.tick();
478
+ this.maybeShowWaitingTip();
479
+ if (!this.commandRunning && this.rl && this.rl.line.length === 0) {
480
+ readline_1.default.cursorTo(process.stdout, 0);
481
+ readline_1.default.clearLine(process.stdout, 0);
482
+ this.rl.setPrompt(this.buildPromptString());
483
+ this.rl.prompt();
484
+ }
485
+ }, 1000);
486
+ }
487
+ this.rl.on("line", (line) => {
488
+ this.commandRunning = true;
489
+ const trimmed = line.trim();
490
+ if (trimmed.length === 0) {
491
+ this.commandRunning = false;
492
+ this.prompt();
493
+ return;
494
+ }
495
+ const [rawCmd, ...rest] = trimmed.split(/\s+/);
496
+ const args = rest.join(" ");
497
+ const entry = this.resolveCommand(rawCmd);
498
+ if (entry == null) {
499
+ // typo候補を検索
500
+ const suggestion = this.findSuggestion(rawCmd);
501
+ if (suggestion) {
502
+ console.log(chalk_1.default.yellow(` 不明なコマンド: ${rawCmd}`) + chalk_1.default.gray(` — もしかして: ${chalk_1.default.white(suggestion)}`));
503
+ }
504
+ else {
505
+ console.log(chalk_1.default.yellow(` 不明なコマンド: ${rawCmd}`) + chalk_1.default.gray(" (help で一覧を表示)"));
506
+ }
507
+ this.commandRunning = false;
508
+ this.prompt();
509
+ return;
510
+ }
511
+ try {
512
+ const result = entry.handler(args);
513
+ if (result instanceof Promise) {
514
+ result
515
+ .catch((err) => {
516
+ log.error(`コマンド実行エラー: ${err instanceof Error ? err.message : err}`);
517
+ })
518
+ .finally(() => {
519
+ this.commandRunning = false;
520
+ if (!this.stopping)
521
+ this.prompt();
522
+ });
523
+ }
524
+ else {
525
+ this.commandRunning = false;
526
+ if (!this.stopping)
527
+ this.prompt();
528
+ }
529
+ }
530
+ catch (err) {
531
+ log.error(`コマンド実行エラー: ${err instanceof Error ? err.message : err}`);
532
+ this.commandRunning = false;
533
+ if (!this.stopping)
534
+ this.prompt();
535
+ }
536
+ });
537
+ this.rl.on("close", () => {
538
+ if (!this.stopping) {
539
+ this.handleQuit();
540
+ }
541
+ });
542
+ this.prompt();
543
+ }
544
+ /** REPL を停止する */
545
+ stop() {
546
+ this.stopping = true;
547
+ (0, logger_1.setLogPrefixBuilder)(null);
548
+ (0, logger_1.setLogHooks)(null);
549
+ if (this.statusTimer) {
550
+ clearInterval(this.statusTimer);
551
+ this.statusTimer = null;
552
+ }
553
+ if (this.rl) {
554
+ this.rl.close();
555
+ this.rl = null;
556
+ }
557
+ }
558
+ /** プロンプトを再表示する (データ出力後・接続状態変化時に呼ぶ) */
559
+ refreshPrompt() {
560
+ if (this.rl && !this.commandRunning) {
561
+ this.prompt();
562
+ }
563
+ }
564
+ /** WebSocket 接続状態をプロンプトに反映 */
565
+ setConnected(connected) {
566
+ this.statusLine.setConnected(connected);
567
+ if (this.rl) {
568
+ this.rl.setPrompt(this.buildPromptString());
569
+ }
570
+ }
571
+ /** 電文表示の前処理(入力中の文字をクリアし、プロンプト行をクリア) */
572
+ beforeDisplayMessage() {
573
+ if (process.stdout.isTTY && this.rl) {
574
+ this.clearInput();
575
+ readline_1.default.cursorTo(process.stdout, 0);
576
+ readline_1.default.clearLine(process.stdout, 0);
577
+ }
578
+ }
579
+ /** 電文表示の後処理(受信時刻更新・プロンプト再描画) */
580
+ afterDisplayMessage() {
581
+ this.statusLine.markMessageReceived();
582
+ this.resetTipSchedule();
583
+ this.prompt();
584
+ }
585
+ buildPromptString() {
586
+ if (!process.stdout.isTTY) {
587
+ return chalk_1.default.gray("FlEq> ");
588
+ }
589
+ const base = this.statusLine.buildPrefix({ noSuffix: true });
590
+ const status = this.wsManager.getStatus();
591
+ // ステータスプロバイダーからセグメント収集 → priority 順ソート
592
+ const segments = this.statusProviders
593
+ .map((p) => p.getPromptStatus())
594
+ .filter((s) => s != null)
595
+ .sort((a, b) => a.priority - b.priority);
596
+ const parts = segments.map((s) => s.text);
597
+ if (status.connected && status.heartbeatDeadlineAt != null) {
598
+ const sec = Math.max(0, Math.ceil((status.heartbeatDeadlineAt - Date.now()) / 1000));
599
+ const palette = themeModule.getPalette();
600
+ const pingColor = sec <= 29
601
+ ? chalk_1.default.rgb(...palette.vermillion)
602
+ : sec <= 69
603
+ ? chalk_1.default.rgb(...palette.yellow)
604
+ : chalk_1.default.white;
605
+ parts.push(pingColor(`ping in ${sec}s`));
606
+ }
607
+ if (parts.length === 0) {
608
+ return `${base}${chalk_1.default.gray("]> ")}`;
609
+ }
610
+ return `${base}${chalk_1.default.gray(" | ")}${parts.join(chalk_1.default.gray(" | "))}${chalk_1.default.gray("]> ")}`;
611
+ }
612
+ prompt() {
613
+ if (this.rl) {
614
+ this.rl.setPrompt(this.buildPromptString());
615
+ this.rl.prompt();
616
+ }
617
+ }
618
+ /** 入力中の文字をクリアしてプロンプト行を再描画する */
619
+ clearInput() {
620
+ if (!this.rl || !process.stdout.isTTY)
621
+ return;
622
+ if (this.rl.line.length === 0)
623
+ return;
624
+ readline_1.default.cursorTo(process.stdout, 0);
625
+ readline_1.default.clearLine(process.stdout, 0);
626
+ // readline 内部バッファをリセット (Node.js 実行時は書き込み可能)
627
+ this.rl.line = "";
628
+ this.rl.cursor = 0;
629
+ }
630
+ /** コマンド名のエイリアス (短縮形 → 正式名) */
631
+ static COMMAND_ALIASES = {
632
+ hist: "history",
633
+ cols: "colors",
634
+ det: "detail",
635
+ stat: "status",
636
+ conf: "config",
637
+ cont: "contract",
638
+ sock: "socket",
639
+ noti: "notify",
640
+ ewlg: "eewlog",
641
+ tw: "tablewidth",
642
+ itxt: "infotext",
643
+ tint: "tipinterval",
644
+ snd: "sound",
645
+ thm: "theme",
646
+ bkup: "backup",
647
+ lim: "limit",
648
+ cls: "clear",
649
+ };
650
+ /** 通知カテゴリ名のエイリアス (短縮形 → 正式名) */
651
+ static CATEGORY_ALIASES = {
652
+ eq: "earthquake",
653
+ tsu: "tsunami",
654
+ st: "seismicText",
655
+ nt: "nankaiTrough",
656
+ lgob: "lgObservation",
657
+ };
658
+ /** カテゴリ名を解決する (case-insensitive + エイリアス) */
659
+ static resolveNotifyCategory(input) {
660
+ const lower = input.toLowerCase();
661
+ // 正式名で完全一致 (case-insensitive)
662
+ for (const cat of Object.keys(notifier_1.NOTIFY_CATEGORY_LABELS)) {
663
+ if (cat.toLowerCase() === lower)
664
+ return cat;
665
+ }
666
+ // エイリアスで検索
667
+ return ReplHandler.CATEGORY_ALIASES[lower] ?? null;
668
+ }
669
+ /** test table 電文タイプ名のエイリアス (短縮形 → 正式名) */
670
+ static TABLE_TYPE_ALIASES = {
671
+ eq: "earthquake",
672
+ tsu: "tsunami",
673
+ st: "seismicText",
674
+ nt: "nankaiTrough",
675
+ lgob: "lgObservation",
676
+ vc: "volcano",
677
+ };
678
+ /** test table の電文タイプ名を解決する (case-insensitive + エイリアス) */
679
+ static resolveTestTableType(input) {
680
+ const lower = input.toLowerCase();
681
+ // 正式名で完全一致 (case-insensitive)
682
+ for (const key of Object.keys(test_samples_1.TEST_TABLES)) {
683
+ if (key.toLowerCase() === lower)
684
+ return key;
685
+ }
686
+ // エイリアスで検索
687
+ return ReplHandler.TABLE_TYPE_ALIASES[lower] ?? null;
688
+ }
689
+ /** コマンド名を解決する (case-insensitive + エイリアス) */
690
+ resolveCommand(input) {
691
+ const lower = input.toLowerCase();
692
+ // 正式名で完全一致 (case-insensitive)
693
+ for (const [name, entry] of Object.entries(this.commands)) {
694
+ if (name.toLowerCase() === lower)
695
+ return entry;
696
+ }
697
+ // エイリアスで検索
698
+ const canonical = ReplHandler.COMMAND_ALIASES[lower];
699
+ if (canonical != null)
700
+ return this.commands[canonical];
701
+ return undefined;
702
+ }
703
+ /** typo候補を検索 (距離2以内で最も近いコマンドを返す) */
704
+ findSuggestion(input) {
705
+ let bestCmd = null;
706
+ let bestDist = 3; // 距離2以内を候補にする
707
+ const displayed = new Set();
708
+ for (const name of Object.keys(this.commands)) {
709
+ if (name === "?" || name === "exit")
710
+ continue;
711
+ if (displayed.has(name))
712
+ continue;
713
+ displayed.add(name);
714
+ const dist = levenshtein(input.toLowerCase(), name.toLowerCase());
715
+ if (dist < bestDist) {
716
+ bestDist = dist;
717
+ bestCmd = name;
718
+ }
719
+ }
720
+ return bestCmd;
721
+ }
722
+ /** 設定変更可能なコマンドの現在値と設定可能な値を返す */
723
+ getCurrentSettingValues() {
724
+ const notifySettings = this.notifier.getSettings();
725
+ const onCount = Object.values(notifySettings).filter(Boolean).length;
726
+ const totalCount = Object.keys(notifySettings).length;
727
+ const muteInfo = this.notifier.isMuted()
728
+ ? `, ミュート中`
729
+ : "";
730
+ return {
731
+ sound: {
732
+ current: this.notifier.getSoundEnabled() ? "ON" : "OFF",
733
+ options: "on / off",
734
+ },
735
+ tablewidth: {
736
+ current: this.config.tableWidth == null
737
+ ? `auto (${process.stdout.columns ?? 60})`
738
+ : `${this.config.tableWidth} (固定)`,
739
+ options: "40〜200 / auto",
740
+ },
741
+ infotext: {
742
+ current: this.config.infoFullText ? "full" : "short",
743
+ options: "full / short",
744
+ },
745
+ tipinterval: {
746
+ current: this.config.waitTipIntervalMin === 0
747
+ ? "無効"
748
+ : `${this.config.waitTipIntervalMin}分`,
749
+ options: "0〜1440 (0で無効)",
750
+ },
751
+ mode: {
752
+ current: (0, formatter_1.getDisplayMode)(),
753
+ options: "normal / compact",
754
+ },
755
+ clock: {
756
+ current: this.statusLine.getClockMode() === "clock" ? "現在時刻" : "経過時間",
757
+ options: "elapsed / now",
758
+ },
759
+ notify: {
760
+ current: `${onCount}/${totalCount} ON${muteInfo}`,
761
+ options: "eew, earthquake, tsunami, seismicText, nankaiTrough, lgObservation",
762
+ },
763
+ mute: {
764
+ current: this.notifier.isMuted()
765
+ ? `残り ${formatDuration(this.notifier.muteRemaining())}`
766
+ : "OFF",
767
+ options: "<duration> (例: 30m, 1h, 90s) / off",
768
+ },
769
+ eewlog: {
770
+ current: this.eewLogger.isEnabled()
771
+ ? (() => {
772
+ const fields = this.eewLogger.getFields();
773
+ const onCount = Object.values(fields).filter(Boolean).length;
774
+ return `ON (${onCount}/${Object.keys(fields).length}項目)`;
775
+ })()
776
+ : "OFF",
777
+ options: "on / off / fields",
778
+ },
779
+ limit: {
780
+ current: (() => {
781
+ const t = this.config.truncation;
782
+ const d = types_1.DEFAULT_CONFIG.truncation;
783
+ const changed = Object.keys(d)
784
+ .filter((k) => t[k] !== d[k]).length;
785
+ return changed > 0 ? `${changed}項目変更済` : "デフォルト";
786
+ })(),
787
+ options: "limit で詳細表示",
788
+ },
789
+ };
790
+ }
791
+ // ── コマンドハンドラ ──
792
+ handleDetail(args) {
793
+ const sub = args.trim().toLowerCase();
794
+ // 引数なし or "tsunami" → 津波情報を再表示
795
+ if (sub === "" || sub === "tsunami") {
796
+ const provider = this.detailProviders.find((p) => p.category === "tsunami");
797
+ if (provider == null || !provider.hasDetail()) {
798
+ console.log(chalk_1.default.gray(" 現在、継続中の津波情報はありません。"));
799
+ }
800
+ else {
801
+ provider.showDetail();
802
+ }
803
+ return;
804
+ }
805
+ // 未知のサブコマンド
806
+ console.log(chalk_1.default.yellow(` 不明なサブコマンド: ${sub}`) + chalk_1.default.gray(" (利用可能: tsunami)"));
807
+ }
808
+ handleHelp(args) {
809
+ const trimmed = args.trim();
810
+ // help <command> [subcommand] — 詳細表示
811
+ if (trimmed.length > 0) {
812
+ const parts = trimmed.split(/\s+/);
813
+ const entry = this.resolveCommand(parts[0]);
814
+ if (entry == null) {
815
+ console.log(chalk_1.default.yellow(` 不明なコマンド: ${parts[0]}`));
816
+ return;
817
+ }
818
+ // サブコマンド解決
819
+ if (parts.length > 1 && entry.subcommands) {
820
+ const sub = entry.subcommands[parts[1]];
821
+ if (sub == null) {
822
+ console.log(chalk_1.default.yellow(` 不明なサブコマンド: ${parts[0]} ${parts[1]}`));
823
+ return;
824
+ }
825
+ console.log();
826
+ console.log(chalk_1.default.cyan.bold(` ${parts[0]} ${parts[1]}`) + chalk_1.default.gray(` — ${sub.description}`));
827
+ if (sub.detail) {
828
+ console.log();
829
+ for (const line of sub.detail.split("\n")) {
830
+ console.log(chalk_1.default.white(` ${line}`));
831
+ }
832
+ }
833
+ console.log();
834
+ return;
835
+ }
836
+ console.log();
837
+ console.log(chalk_1.default.cyan.bold(` ${parts[0]}`) + chalk_1.default.gray(` — ${entry.description}`));
838
+ if (entry.detail) {
839
+ console.log();
840
+ for (const line of entry.detail.split("\n")) {
841
+ console.log(chalk_1.default.white(` ${line}`));
842
+ }
843
+ }
844
+ // サブコマンド一覧
845
+ if (entry.subcommands) {
846
+ console.log();
847
+ const subNames = Object.keys(entry.subcommands).sort();
848
+ for (let i = 0; i < subNames.length; i++) {
849
+ const subName = subNames[i];
850
+ const sub = entry.subcommands[subName];
851
+ const prefix = i < subNames.length - 1 ? "├─" : "└─";
852
+ console.log(chalk_1.default.gray(` ${prefix} `) + chalk_1.default.white(subName.padEnd(10)) + chalk_1.default.gray(sub.description));
853
+ }
854
+ }
855
+ console.log();
856
+ return;
857
+ }
858
+ // help — カテゴリ別一覧
859
+ console.log();
860
+ console.log(chalk_1.default.cyan.bold(" 利用可能なコマンド:"));
861
+ const currentValues = this.getCurrentSettingValues();
862
+ const displayed = new Set();
863
+ const categoryOrder = ["info", "status", "settings", "operation"];
864
+ for (const category of categoryOrder) {
865
+ console.log();
866
+ console.log(chalk_1.default.cyan(` [${CATEGORY_LABELS[category]}]`));
867
+ const commandNames = Object.keys(this.commands)
868
+ .filter((name) => name !== "exit" && name !== "?" && this.commands[name].category === category)
869
+ .sort();
870
+ for (const name of commandNames) {
871
+ const entry = this.commands[name];
872
+ if (displayed.has(entry.description))
873
+ continue;
874
+ displayed.add(entry.description);
875
+ const setting = currentValues[name];
876
+ const valueSuffix = setting != null
877
+ ? chalk_1.default.gray(" [") + chalk_1.default.yellow(setting.current) + chalk_1.default.gray("]") +
878
+ (setting.options ? chalk_1.default.gray(` (${setting.options})`) : "")
879
+ : "";
880
+ // エイリアス (短縮形) を逆引き
881
+ const alias = Object.entries(ReplHandler.COMMAND_ALIASES)
882
+ .find(([, v]) => v === name)?.[0];
883
+ const aliasSuffix = alias != null ? chalk_1.default.gray(` (${alias})`) : "";
884
+ console.log(chalk_1.default.white(` ${name.padEnd(14)}`) + chalk_1.default.gray(entry.description) + aliasSuffix + valueSuffix);
885
+ // サブコマンドツリー表示
886
+ if (entry.subcommands) {
887
+ const subNames = Object.keys(entry.subcommands).sort();
888
+ for (let i = 0; i < subNames.length; i++) {
889
+ const subName = subNames[i];
890
+ const sub = entry.subcommands[subName];
891
+ const prefix = i < subNames.length - 1 ? "├─" : "└─";
892
+ console.log(chalk_1.default.gray(` ${prefix} `) + chalk_1.default.white(subName.padEnd(10)) + chalk_1.default.gray(sub.description));
893
+ }
894
+ }
895
+ }
896
+ }
897
+ console.log();
898
+ console.log(chalk_1.default.gray(" エイリアス: ") + chalk_1.default.white("?") + chalk_1.default.gray(" → help, ") + chalk_1.default.white("exit") + chalk_1.default.gray(" → quit (コマンド名の大文字小文字は区別しません)"));
899
+ console.log();
900
+ }
901
+ async handleHistory(args) {
902
+ const MAX_HISTORY = 100;
903
+ const raw = args.length > 0 ? parseInt(args, 10) : 10;
904
+ if (isNaN(raw) || raw <= 0) {
905
+ console.log(chalk_1.default.yellow(" 件数は正の整数で指定してください"));
906
+ return;
907
+ }
908
+ const limit = Math.min(raw, MAX_HISTORY);
909
+ console.log(chalk_1.default.gray(" 地震履歴を取得中..."));
910
+ const res = await (0, rest_client_1.listEarthquakes)(this.config.apiKey, limit);
911
+ if (res.items.length === 0) {
912
+ console.log(chalk_1.default.gray(" 該当する地震情報はありません"));
913
+ return;
914
+ }
915
+ // カラム幅定義
916
+ const COL = { time: 18, hypo: 16, mag: 6, depth: 8, int: 8 };
917
+ const hLine = (l, m, r, h) => chalk_1.default.gray(` ${l}${h.repeat(COL.time + 2)}${m}${h.repeat(COL.hypo + 2)}${m}${h.repeat(COL.mag + 2)}${m}${h.repeat(COL.depth + 2)}${m}${h.repeat(COL.int + 2)}${r}`);
918
+ console.log();
919
+ console.log(hLine("┌", "┬", "┐", "─"));
920
+ console.log(chalk_1.default.gray(" │ ") +
921
+ chalk_1.default.cyan((0, formatter_1.visualPadEnd)("発生時刻", COL.time)) + chalk_1.default.gray(" │ ") +
922
+ chalk_1.default.cyan((0, formatter_1.visualPadEnd)("震源地", COL.hypo)) + chalk_1.default.gray(" │ ") +
923
+ chalk_1.default.cyan((0, formatter_1.visualPadEnd)("規模", COL.mag)) + chalk_1.default.gray(" │ ") +
924
+ chalk_1.default.cyan((0, formatter_1.visualPadEnd)("深さ", COL.depth)) + chalk_1.default.gray(" │ ") +
925
+ chalk_1.default.cyan((0, formatter_1.visualPadEnd)("最大震度", COL.int)) + chalk_1.default.gray(" │"));
926
+ console.log(hLine("├", "┼", "┤", "─"));
927
+ // 最新が一番下に来るように逆順で表示
928
+ const items = [...res.items].reverse();
929
+ for (const item of items) {
930
+ const time = formatShortTime(item.originTime || item.arrivalTime);
931
+ const hypo = truncate(item.hypocenter?.name || "不明", COL.hypo);
932
+ const mag = item.magnitude?.value != null ? `M${item.magnitude.value}` : "M---";
933
+ const depth = formatDepth(item);
934
+ const maxInt = item.maxInt != null ? item.maxInt : "---";
935
+ const intColor = item.maxInt != null ? (0, formatter_1.intensityColor)(item.maxInt) : chalk_1.default.gray;
936
+ console.log(chalk_1.default.gray(" │ ") +
937
+ chalk_1.default.white((0, formatter_1.visualPadEnd)(time, COL.time)) + chalk_1.default.gray(" │ ") +
938
+ chalk_1.default.white((0, formatter_1.visualPadEnd)(hypo, COL.hypo)) + chalk_1.default.gray(" │ ") +
939
+ chalk_1.default.yellow((0, formatter_1.visualPadEnd)(mag, COL.mag)) + chalk_1.default.gray(" │ ") +
940
+ chalk_1.default.white((0, formatter_1.visualPadEnd)(depth, COL.depth)) + chalk_1.default.gray(" │ ") +
941
+ intColor((0, formatter_1.visualPadEnd)(maxInt, COL.int)) + chalk_1.default.gray(" │"));
942
+ }
943
+ console.log(hLine("└", "┴", "┘", "─"));
944
+ console.log();
945
+ }
946
+ handleStatus() {
947
+ const status = this.wsManager.getStatus();
948
+ console.log();
949
+ console.log(chalk_1.default.cyan.bold(" WebSocket 接続状態:"));
950
+ console.log(chalk_1.default.white(" 状態: ") +
951
+ (status.connected
952
+ ? chalk_1.default.green.bold("接続中")
953
+ : chalk_1.default.red.bold("切断")));
954
+ if (status.socketId != null) {
955
+ console.log(chalk_1.default.white(" SocketID: ") + chalk_1.default.white(String(status.socketId)));
956
+ }
957
+ if (status.reconnectAttempt > 0) {
958
+ console.log(chalk_1.default.white(" 再接続試行: ") +
959
+ chalk_1.default.yellow(`#${status.reconnectAttempt}`));
960
+ }
961
+ // 副回線情報
962
+ if (hasBackupSupport(this.wsManager)) {
963
+ if (this.wsManager.isBackupRunning()) {
964
+ const backupStatus = this.wsManager.getBackupStatus();
965
+ console.log(chalk_1.default.white(" 副回線: ") +
966
+ (backupStatus?.connected
967
+ ? chalk_1.default.green.bold("接続中")
968
+ : chalk_1.default.yellow("再接続中")));
969
+ if (backupStatus?.socketId != null) {
970
+ console.log(chalk_1.default.white(" 副回線 SocketID: ") + chalk_1.default.white(String(backupStatus.socketId)));
971
+ }
972
+ }
973
+ else {
974
+ console.log(chalk_1.default.white(" 副回線: ") + chalk_1.default.gray("未起動"));
975
+ }
976
+ }
977
+ console.log();
978
+ }
979
+ async handleBackup(args) {
980
+ if (!hasBackupSupport(this.wsManager)) {
981
+ console.log(chalk_1.default.yellow(" この構成では副回線は利用できません"));
982
+ return;
983
+ }
984
+ const sub = args.trim().toLowerCase();
985
+ if (sub === "") {
986
+ // 状態表示
987
+ if (this.wsManager.isBackupRunning()) {
988
+ const bs = this.wsManager.getBackupStatus();
989
+ console.log(chalk_1.default.white(" 副回線: ") +
990
+ (bs?.connected ? chalk_1.default.green.bold("接続中") : chalk_1.default.yellow("再接続中")));
991
+ if (bs?.socketId != null) {
992
+ console.log(chalk_1.default.white(" SocketID: ") + chalk_1.default.white(String(bs.socketId)));
993
+ }
994
+ }
995
+ else {
996
+ console.log(chalk_1.default.white(" 副回線: ") + chalk_1.default.gray("未起動"));
997
+ }
998
+ return;
999
+ }
1000
+ if (sub === "on") {
1001
+ const result = await this.wsManager.startBackup();
1002
+ if (result !== "started") {
1003
+ // 起動失敗時は Config を変更しない
1004
+ return;
1005
+ }
1006
+ // 成功時のみ Config に永続化
1007
+ const fileConfig = (0, config_1.loadConfig)();
1008
+ fileConfig.backup = true;
1009
+ (0, config_1.saveConfig)(fileConfig);
1010
+ this.config.backup = true;
1011
+ return;
1012
+ }
1013
+ if (sub === "off") {
1014
+ this.wsManager.stopBackup();
1015
+ // Config に永続化
1016
+ const fileConfig = (0, config_1.loadConfig)();
1017
+ fileConfig.backup = false;
1018
+ (0, config_1.saveConfig)(fileConfig);
1019
+ this.config.backup = false;
1020
+ return;
1021
+ }
1022
+ console.log(chalk_1.default.yellow(" 使い方: backup / backup on / backup off"));
1023
+ }
1024
+ handleColors() {
1025
+ const termWidth = process.stdout.columns || 80;
1026
+ const palette = themeModule.getPalette();
1027
+ const PALETTE_USAGE = {
1028
+ gray: "低優先度・補助テキスト",
1029
+ sky: "通常・長周期階級1",
1030
+ blue: "震度3",
1031
+ blueGreen: "震度4・津波なし",
1032
+ yellow: "震度5弱・M3+",
1033
+ orange: "警告レベル",
1034
+ vermillion: "危険レベル",
1035
+ raspberry: "取消・キャンセル",
1036
+ darkRed: "最高警戒 (背景用)",
1037
+ };
1038
+ // ── CUD カラーパレット ──
1039
+ console.log();
1040
+ console.log(chalk_1.default.cyan.bold(" CUD カラーパレット:"));
1041
+ if (themeModule.isCustomized()) {
1042
+ console.log(chalk_1.default.gray(" (カスタムテーマ適用中)"));
1043
+ }
1044
+ console.log();
1045
+ for (const name of themeModule.getPaletteNames()) {
1046
+ const rgb = palette[name];
1047
+ const swatch = chalk_1.default.rgb(rgb[0], rgb[1], rgb[2])("██");
1048
+ const rgbStr = `(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
1049
+ console.log(` ${swatch} ` +
1050
+ chalk_1.default.white(name.padEnd(12)) +
1051
+ chalk_1.default.gray(rgbStr.padEnd(16)) +
1052
+ chalk_1.default.gray(PALETTE_USAGE[name] ?? ""));
1053
+ }
1054
+ // ── 震度カラー (マルチカラム) ──
1055
+ console.log();
1056
+ console.log(chalk_1.default.cyan.bold(" 震度カラー:"));
1057
+ console.log();
1058
+ const intensityKeys = ["1", "2", "3", "4", "5弱", "5強", "6弱", "6強", "7"];
1059
+ const intensities = intensityKeys.map((key) => {
1060
+ const label = `震度${key}`;
1061
+ const style = (0, formatter_1.intensityColor)(key);
1062
+ const role = getIntensityRole(key);
1063
+ const resolved = role ? themeModule.getRole(role) : null;
1064
+ return { label, key, style, resolved };
1065
+ });
1066
+ this.printColorGrid(termWidth, intensities, (item) => {
1067
+ if (item.resolved?.bg && item.resolved?.fg) {
1068
+ return this.renderFgBgItem(item.label, item.resolved.fg, item.resolved.bg, item.style);
1069
+ }
1070
+ return { cell: `${item.style("██")} ${item.style(item.label)}`, visualLen: (0, formatter_1.visualWidth)(item.label) + 3 };
1071
+ });
1072
+ // ── 長周期地震動階級カラー (マルチカラム) ──
1073
+ console.log();
1074
+ console.log(chalk_1.default.cyan.bold(" 長周期地震動階級カラー:"));
1075
+ console.log();
1076
+ const lgIntKeys = ["0", "1", "2", "3", "4"];
1077
+ const lgInts = lgIntKeys.map((key) => {
1078
+ const label = `階級${key}`;
1079
+ const style = (0, formatter_1.lgIntensityColor)(key);
1080
+ const role = getLgIntRole(key);
1081
+ const resolved = role ? themeModule.getRole(role) : null;
1082
+ return { label, key, style, resolved };
1083
+ });
1084
+ this.printColorGrid(termWidth, lgInts, (item) => {
1085
+ if (item.resolved?.bg && item.resolved?.fg) {
1086
+ return this.renderFgBgItem(item.label, item.resolved.fg, item.resolved.bg, item.style);
1087
+ }
1088
+ return { cell: `${item.style("██")} ${item.style(item.label)}`, visualLen: (0, formatter_1.visualWidth)(item.label) + 3 };
1089
+ });
1090
+ // ── フレームレベル (マルチカラム) ──
1091
+ console.log();
1092
+ console.log(chalk_1.default.cyan.bold(" フレームレベル:"));
1093
+ console.log();
1094
+ const frameRoles = [
1095
+ { name: "critical", role: "frameCritical", label: "[緊急] 二重線" },
1096
+ { name: "warning", role: "frameWarning", label: "[警告] 二重線" },
1097
+ { name: "normal", role: "frameNormal", label: "[情報] 通常" },
1098
+ { name: "info", role: "frameInfo", label: "[通知] 通常" },
1099
+ { name: "cancel", role: "frameCancel", label: "[取消] 通常" },
1100
+ ];
1101
+ this.printColorGrid(termWidth, frameRoles, (lv) => {
1102
+ const style = themeModule.getRoleChalk(lv.role);
1103
+ const text = `${lv.name} ${lv.label}`;
1104
+ return { cell: `${style("██")} ${style(text)}`, visualLen: (0, formatter_1.visualWidth)(text) + 3 };
1105
+ });
1106
+ console.log();
1107
+ }
1108
+ handleTheme(args) {
1109
+ const sub = args.trim().toLowerCase();
1110
+ if (sub === "" || sub === "info") {
1111
+ // テーマ概要
1112
+ const palette = themeModule.getPalette();
1113
+ console.log();
1114
+ console.log(chalk_1.default.cyan.bold(" カラーテーマ:"));
1115
+ console.log();
1116
+ // パレットスウォッチ (1行に全色)
1117
+ const swatches = themeModule.getPaletteNames().map((name) => {
1118
+ const rgb = palette[name];
1119
+ return chalk_1.default.rgb(rgb[0], rgb[1], rgb[2])("██");
1120
+ });
1121
+ console.log(` ${swatches.join(" ")}`);
1122
+ console.log();
1123
+ console.log(chalk_1.default.white(` theme.json: `) + chalk_1.default.gray(themeModule.getThemePath()));
1124
+ console.log(chalk_1.default.white(` カスタマイズ: `) + (themeModule.isCustomized() ? chalk_1.default.green("あり") : chalk_1.default.gray("なし (デフォルト)")));
1125
+ console.log();
1126
+ console.log(chalk_1.default.gray(" サブコマンド: theme path / show / reset / reload / validate"));
1127
+ console.log();
1128
+ return;
1129
+ }
1130
+ if (sub === "path") {
1131
+ console.log(` ${themeModule.getThemePath()}`);
1132
+ return;
1133
+ }
1134
+ if (sub === "show") {
1135
+ this.handleThemeShow();
1136
+ return;
1137
+ }
1138
+ if (sub === "reset") {
1139
+ this.handleThemeReset();
1140
+ return;
1141
+ }
1142
+ if (sub === "reload") {
1143
+ const warnings = themeModule.reloadTheme();
1144
+ if (warnings.length === 0) {
1145
+ console.log(chalk_1.default.green(" テーマを再読込しました"));
1146
+ }
1147
+ else {
1148
+ console.log(chalk_1.default.yellow(" テーマを再読込しました (警告あり):"));
1149
+ for (const w of warnings) {
1150
+ console.log(chalk_1.default.yellow(` ${w}`));
1151
+ }
1152
+ }
1153
+ return;
1154
+ }
1155
+ if (sub === "validate") {
1156
+ const { valid, warnings } = themeModule.validateThemeFile();
1157
+ if (valid && warnings.length === 0) {
1158
+ console.log(chalk_1.default.green(" theme.json に問題はありません"));
1159
+ }
1160
+ else if (valid) {
1161
+ console.log(chalk_1.default.yellow(" theme.json の検証結果:"));
1162
+ for (const w of warnings) {
1163
+ console.log(chalk_1.default.yellow(` ${w}`));
1164
+ }
1165
+ }
1166
+ else {
1167
+ console.log(chalk_1.default.red(" theme.json に問題があります:"));
1168
+ for (const w of warnings) {
1169
+ console.log(chalk_1.default.red(` ${w}`));
1170
+ }
1171
+ }
1172
+ return;
1173
+ }
1174
+ console.log(chalk_1.default.yellow(` 不明なサブコマンド: ${args.trim()}`));
1175
+ console.log(chalk_1.default.gray(" 使い方: theme / theme path / theme show / theme reset / theme reload / theme validate"));
1176
+ }
1177
+ handleThemeShow() {
1178
+ const termWidth = process.stdout.columns || 80;
1179
+ const palette = themeModule.getPalette();
1180
+ console.log();
1181
+ console.log(chalk_1.default.cyan.bold(" パレット:"));
1182
+ console.log();
1183
+ for (const name of themeModule.getPaletteNames()) {
1184
+ const rgb = palette[name];
1185
+ const swatch = chalk_1.default.rgb(rgb[0], rgb[1], rgb[2])("██");
1186
+ const hex = themeModule.rgbToHex(rgb);
1187
+ console.log(` ${swatch} ${chalk_1.default.white(name.padEnd(12))} ${chalk_1.default.gray(hex)}`);
1188
+ }
1189
+ console.log();
1190
+ console.log(chalk_1.default.cyan.bold(" ロール:"));
1191
+ console.log();
1192
+ const roleNames = themeModule.getRoleNames();
1193
+ const maxNameLen = Math.max(...roleNames.map((n) => n.length));
1194
+ for (const name of roleNames) {
1195
+ const style = themeModule.getRoleChalk(name);
1196
+ const resolved = themeModule.getRole(name);
1197
+ const parts = [];
1198
+ if (resolved.fg)
1199
+ parts.push(`fg: ${themeModule.rgbToHex(resolved.fg)}`);
1200
+ if (resolved.bg)
1201
+ parts.push(`bg: ${themeModule.rgbToHex(resolved.bg)}`);
1202
+ if (resolved.bold)
1203
+ parts.push("bold");
1204
+ const preview = style("Sample");
1205
+ console.log(` ${chalk_1.default.white(name.padEnd(maxNameLen + 1))} ${preview} ${chalk_1.default.gray(parts.join(", "))}`);
1206
+ }
1207
+ console.log();
1208
+ }
1209
+ handleThemeReset() {
1210
+ if (!this.rl)
1211
+ return;
1212
+ const rl = this.rl;
1213
+ rl.question(chalk_1.default.yellow(" デフォルトの theme.json を書き出しますか? (y/N) "), (answer) => {
1214
+ if (answer.trim().toLowerCase() === "y") {
1215
+ try {
1216
+ const warnings = themeModule.resetTheme();
1217
+ console.log(chalk_1.default.green(` theme.json を書き出しました: ${themeModule.getThemePath()}`));
1218
+ if (warnings.length > 0) {
1219
+ for (const w of warnings) {
1220
+ console.log(chalk_1.default.yellow(` ${w}`));
1221
+ }
1222
+ }
1223
+ }
1224
+ catch (err) {
1225
+ const msg = err instanceof Error ? err.message : "不明なエラー";
1226
+ console.log(chalk_1.default.red(` theme.json の書き出しに失敗しました: ${msg}`));
1227
+ }
1228
+ }
1229
+ else {
1230
+ console.log(chalk_1.default.gray(" キャンセルしました"));
1231
+ }
1232
+ rl.setPrompt(this.buildPromptString());
1233
+ rl.prompt();
1234
+ });
1235
+ }
1236
+ /**
1237
+ * fg/bg 分離表示用のセルを生成する。
1238
+ * 文字色 ██ と背景色 ██ を横に並べ、ラベルは実際の表示スタイル(fg+bg)で表示する。
1239
+ */
1240
+ renderFgBgItem(label, fg, bg, style) {
1241
+ const fgBlock = chalk_1.default.rgb(fg[0], fg[1], fg[2])("██");
1242
+ const bgBlock = chalk_1.default.bgRgb(bg[0], bg[1], bg[2])(" ");
1243
+ // "██ ██ label" → swatch(2) + space(1) + swatch(2) + space(1) + label
1244
+ return { cell: `${fgBlock} ${bgBlock} ${style(label)}`, visualLen: (0, formatter_1.visualWidth)(label) + 6 };
1245
+ }
1246
+ /**
1247
+ * 色付きアイテムをターミナル幅に応じたマルチカラムで出力する。
1248
+ * renderFn は各アイテムから { cell, visualLen } を返す。
1249
+ */
1250
+ printColorGrid(termWidth, items, renderFn) {
1251
+ const rendered = items.map(renderFn);
1252
+ // 最大表示幅 + マージンでカラム幅を決定
1253
+ const maxVisual = Math.max(...rendered.map((r) => r.visualLen));
1254
+ const colWidth = maxVisual + 3; // 右余白
1255
+ const indent = 2;
1256
+ const cols = Math.max(1, Math.floor((termWidth - indent) / colWidth));
1257
+ let line = "";
1258
+ let col = 0;
1259
+ for (const r of rendered) {
1260
+ const pad = colWidth - r.visualLen;
1261
+ line += r.cell + " ".repeat(Math.max(0, pad));
1262
+ col++;
1263
+ if (col >= cols) {
1264
+ console.log(`${" ".repeat(indent)}${line}`);
1265
+ line = "";
1266
+ col = 0;
1267
+ }
1268
+ }
1269
+ if (line.length > 0) {
1270
+ console.log(`${" ".repeat(indent)}${line}`);
1271
+ }
1272
+ }
1273
+ handleConfig() {
1274
+ (0, config_1.printConfig)();
1275
+ }
1276
+ async handleContract() {
1277
+ console.log(chalk_1.default.gray(" 契約情報を取得中..."));
1278
+ const classifications = await (0, rest_client_1.listContracts)(this.config.apiKey);
1279
+ console.log();
1280
+ console.log(chalk_1.default.cyan.bold(" 契約済み区分:"));
1281
+ if (classifications.length === 0) {
1282
+ console.log(chalk_1.default.gray(" (なし)"));
1283
+ }
1284
+ else {
1285
+ for (const c of classifications) {
1286
+ console.log(chalk_1.default.white(` - ${c}`));
1287
+ }
1288
+ }
1289
+ console.log();
1290
+ }
1291
+ async handleSocket() {
1292
+ console.log(chalk_1.default.gray(" ソケット情報を取得中..."));
1293
+ const res = await (0, rest_client_1.listSockets)(this.config.apiKey);
1294
+ console.log();
1295
+ console.log(chalk_1.default.cyan.bold(" 接続中のソケット:"));
1296
+ if (res.items.length === 0) {
1297
+ console.log(chalk_1.default.gray(" (なし)"));
1298
+ }
1299
+ else {
1300
+ for (const s of res.items) {
1301
+ console.log(chalk_1.default.white(` id=${s.id}`) +
1302
+ chalk_1.default.gray(` status=${s.status}`) +
1303
+ chalk_1.default.gray(` app=${s.appName || "---"}`) +
1304
+ chalk_1.default.gray(` start=${s.start}`));
1305
+ }
1306
+ }
1307
+ console.log();
1308
+ }
1309
+ handleNotify(args) {
1310
+ const trimmed = args.trim();
1311
+ // 引数なし → 一覧表示
1312
+ if (trimmed.length === 0) {
1313
+ const settings = this.notifier.getSettings();
1314
+ console.log();
1315
+ console.log(chalk_1.default.cyan.bold(" 通知設定:"));
1316
+ if (this.notifier.isMuted()) {
1317
+ const remaining = this.notifier.muteRemaining();
1318
+ console.log(chalk_1.default.yellow(` (ミュート中: 残り ${formatDuration(remaining)})`));
1319
+ }
1320
+ console.log();
1321
+ for (const [cat, label] of Object.entries(notifier_1.NOTIFY_CATEGORY_LABELS)) {
1322
+ const enabled = settings[cat];
1323
+ const status = enabled
1324
+ ? chalk_1.default.green("ON")
1325
+ : chalk_1.default.red("OFF");
1326
+ // 短縮形を逆引き
1327
+ const alias = Object.entries(ReplHandler.CATEGORY_ALIASES)
1328
+ .find(([, v]) => v === cat)?.[0];
1329
+ const aliasPart = alias != null ? chalk_1.default.gray(` (${alias})`) : "";
1330
+ console.log(chalk_1.default.white(` ${cat.padEnd(14)}`) +
1331
+ chalk_1.default.gray(`${label} `) +
1332
+ status +
1333
+ aliasPart);
1334
+ }
1335
+ console.log();
1336
+ console.log(chalk_1.default.gray(" 使い方: notify <category> [on|off] / notify all:on / notify all:off"));
1337
+ console.log();
1338
+ return;
1339
+ }
1340
+ // all:on / all:off (case-insensitive)
1341
+ const lower = trimmed.toLowerCase();
1342
+ if (lower === "all:on" || lower === "aon") {
1343
+ this.notifier.setAll(true);
1344
+ console.log(chalk_1.default.green(" 全通知を有効にしました"));
1345
+ return;
1346
+ }
1347
+ if (lower === "all:off" || lower === "aoff") {
1348
+ this.notifier.setAll(false);
1349
+ console.log(chalk_1.default.yellow(" 全通知を無効にしました"));
1350
+ return;
1351
+ }
1352
+ // カテゴリ指定 (+ 任意の on/off)
1353
+ const parts = trimmed.split(/\s+/);
1354
+ const cat = ReplHandler.resolveNotifyCategory(parts[0]);
1355
+ const action = parts[1]?.toLowerCase();
1356
+ if (cat == null) {
1357
+ console.log(chalk_1.default.yellow(` 不明なカテゴリ: ${parts[0]}`) +
1358
+ chalk_1.default.gray(` (有効: ${Object.keys(notifier_1.NOTIFY_CATEGORY_LABELS).join(", ")})`));
1359
+ return;
1360
+ }
1361
+ let newState;
1362
+ if (action === "on") {
1363
+ const settings = this.notifier.getSettings();
1364
+ if (settings[cat]) {
1365
+ console.log(` ${notifier_1.NOTIFY_CATEGORY_LABELS[cat]} (${cat}): 既に ${chalk_1.default.green("ON")} です`);
1366
+ return;
1367
+ }
1368
+ newState = this.notifier.toggleCategory(cat);
1369
+ }
1370
+ else if (action === "off") {
1371
+ const settings = this.notifier.getSettings();
1372
+ if (!settings[cat]) {
1373
+ console.log(` ${notifier_1.NOTIFY_CATEGORY_LABELS[cat]} (${cat}): 既に ${chalk_1.default.red("OFF")} です`);
1374
+ return;
1375
+ }
1376
+ newState = this.notifier.toggleCategory(cat);
1377
+ }
1378
+ else {
1379
+ newState = this.notifier.toggleCategory(cat);
1380
+ }
1381
+ const label = notifier_1.NOTIFY_CATEGORY_LABELS[cat];
1382
+ const status = newState ? chalk_1.default.green("ON") : chalk_1.default.red("OFF");
1383
+ console.log(` ${label} (${cat}): ${status}`);
1384
+ }
1385
+ handleTableWidth(args) {
1386
+ const trimmed = args.trim();
1387
+ if (trimmed.length === 0) {
1388
+ if (this.config.tableWidth == null) {
1389
+ const cols = process.stdout.columns ?? 60;
1390
+ console.log(` 現在のテーブル幅: auto (ターミナル幅: ${cols})`);
1391
+ }
1392
+ else {
1393
+ console.log(` 現在のテーブル幅: ${this.config.tableWidth} (固定)`);
1394
+ }
1395
+ console.log(chalk_1.default.gray(" 使い方: tablewidth <40〜200> / tablewidth auto"));
1396
+ return;
1397
+ }
1398
+ if (trimmed.toLowerCase() === "auto") {
1399
+ this.config.tableWidth = null;
1400
+ (0, formatter_1.clearFrameWidth)();
1401
+ const config = (0, config_1.loadConfig)();
1402
+ delete config.tableWidth;
1403
+ (0, config_1.saveConfig)(config);
1404
+ const cols = process.stdout.columns ?? 60;
1405
+ console.log(` テーブル幅を auto に変更しました。(現在のターミナル幅: ${cols})`);
1406
+ return;
1407
+ }
1408
+ const width = Number(trimmed);
1409
+ if (isNaN(width) || !Number.isInteger(width) || width < 40 || width > 200) {
1410
+ console.log(chalk_1.default.yellow(" tableWidth は 40〜200 の整数、または auto を指定してください。"));
1411
+ return;
1412
+ }
1413
+ this.config.tableWidth = width;
1414
+ (0, formatter_1.setFrameWidth)(width);
1415
+ const config = (0, config_1.loadConfig)();
1416
+ config.tableWidth = width;
1417
+ (0, config_1.saveConfig)(config);
1418
+ console.log(` テーブル幅を ${width} に変更しました。`);
1419
+ }
1420
+ handleInfoText(args) {
1421
+ const trimmed = args.trim();
1422
+ if (trimmed.length === 0) {
1423
+ const current = this.config.infoFullText ? "full (全文表示)" : "short (省略表示)";
1424
+ console.log(` お知らせ電文表示: ${current}`);
1425
+ console.log(chalk_1.default.gray(" 使い方: infotext full / infotext short"));
1426
+ return;
1427
+ }
1428
+ const infoLower = trimmed.toLowerCase();
1429
+ if (infoLower === "full") {
1430
+ this.config.infoFullText = true;
1431
+ (0, formatter_1.setInfoFullText)(true);
1432
+ const config = (0, config_1.loadConfig)();
1433
+ config.infoFullText = true;
1434
+ (0, config_1.saveConfig)(config);
1435
+ console.log(" お知らせ電文を全文表示に変更しました。");
1436
+ }
1437
+ else if (infoLower === "short") {
1438
+ this.config.infoFullText = false;
1439
+ (0, formatter_1.setInfoFullText)(false);
1440
+ const config = (0, config_1.loadConfig)();
1441
+ config.infoFullText = false;
1442
+ (0, config_1.saveConfig)(config);
1443
+ console.log(" お知らせ電文を省略表示に変更しました。");
1444
+ }
1445
+ else {
1446
+ console.log(chalk_1.default.yellow(" full または short を指定してください。"));
1447
+ }
1448
+ }
1449
+ handleMode(args) {
1450
+ const trimmed = args.trim();
1451
+ if (trimmed.length === 0) {
1452
+ const current = (0, formatter_1.getDisplayMode)();
1453
+ console.log(` 表示モード: ${current}`);
1454
+ console.log(chalk_1.default.gray(" 使い方: mode normal / mode compact"));
1455
+ return;
1456
+ }
1457
+ const modeLower = trimmed.toLowerCase();
1458
+ if (modeLower !== "normal" && modeLower !== "compact") {
1459
+ console.log(chalk_1.default.yellow(` 無効なモード: ${trimmed}`) + chalk_1.default.gray(" (normal / compact)"));
1460
+ return;
1461
+ }
1462
+ const mode = modeLower;
1463
+ this.config.displayMode = mode;
1464
+ (0, formatter_1.setDisplayMode)(mode);
1465
+ const config = (0, config_1.loadConfig)();
1466
+ config.displayMode = mode;
1467
+ (0, config_1.saveConfig)(config);
1468
+ console.log(` 表示モードを ${mode} に変更しました。`);
1469
+ }
1470
+ handleClock(args) {
1471
+ const trimmed = args.trim();
1472
+ if (trimmed.length === 0) {
1473
+ // トグル
1474
+ const current = this.statusLine.getClockMode();
1475
+ const next = current === "elapsed" ? "clock" : "elapsed";
1476
+ this.statusLine.setClockMode(next);
1477
+ this.config.promptClock = next;
1478
+ const config = (0, config_1.loadConfig)();
1479
+ config.promptClock = next;
1480
+ (0, config_1.saveConfig)(config);
1481
+ const label = next === "clock" ? "現在時刻" : "経過時間";
1482
+ console.log(` プロンプト時計を ${label} に切り替えました。`);
1483
+ return;
1484
+ }
1485
+ const clockLower = trimmed.toLowerCase();
1486
+ if (clockLower === "elapsed") {
1487
+ this.statusLine.setClockMode("elapsed");
1488
+ this.config.promptClock = "elapsed";
1489
+ const config = (0, config_1.loadConfig)();
1490
+ config.promptClock = "elapsed";
1491
+ (0, config_1.saveConfig)(config);
1492
+ console.log(" プロンプト時計を 経過時間 に変更しました。");
1493
+ }
1494
+ else if (clockLower === "now") {
1495
+ this.statusLine.setClockMode("clock");
1496
+ this.config.promptClock = "clock";
1497
+ const config = (0, config_1.loadConfig)();
1498
+ config.promptClock = "clock";
1499
+ (0, config_1.saveConfig)(config);
1500
+ console.log(" プロンプト時計を 現在時刻 に変更しました。");
1501
+ }
1502
+ else {
1503
+ console.log(chalk_1.default.yellow(" elapsed または now を指定してください。"));
1504
+ }
1505
+ }
1506
+ handleTipInterval(args) {
1507
+ const trimmed = args.trim();
1508
+ if (trimmed.length === 0) {
1509
+ console.log(` 待機中ヒント間隔: ${this.config.waitTipIntervalMin}分`);
1510
+ console.log(chalk_1.default.gray(" 使い方: tipinterval <0〜1440> (0で無効)"));
1511
+ return;
1512
+ }
1513
+ const min = Number(trimmed);
1514
+ if (isNaN(min) || !Number.isInteger(min) || min < 0 || min > 1440) {
1515
+ console.log(chalk_1.default.yellow(" tipinterval は 0〜1440 の整数を指定してください。"));
1516
+ return;
1517
+ }
1518
+ this.config.waitTipIntervalMin = min;
1519
+ this.tipIntervalMs = min * 60 * 1000;
1520
+ this.resetTipSchedule();
1521
+ const config = (0, config_1.loadConfig)();
1522
+ config.waitTipIntervalMin = min;
1523
+ (0, config_1.saveConfig)(config);
1524
+ if (min === 0) {
1525
+ console.log(" 待機中ヒントを無効化しました。");
1526
+ return;
1527
+ }
1528
+ console.log(` 待機中ヒント間隔を ${min}分 に変更しました。`);
1529
+ }
1530
+ handleSound(args) {
1531
+ const trimmed = args.trim();
1532
+ if (trimmed.length === 0) {
1533
+ const current = this.notifier.getSoundEnabled();
1534
+ const status = current ? chalk_1.default.green("ON") : chalk_1.default.red("OFF");
1535
+ console.log(` 通知音: ${status}`);
1536
+ console.log(chalk_1.default.gray(" 使い方: sound on / sound off"));
1537
+ return;
1538
+ }
1539
+ const soundLower = trimmed.toLowerCase();
1540
+ if (soundLower === "on") {
1541
+ this.notifier.setSoundEnabled(true);
1542
+ console.log(` 通知音を ${chalk_1.default.green("ON")} にしました。`);
1543
+ }
1544
+ else if (soundLower === "off") {
1545
+ this.notifier.setSoundEnabled(false);
1546
+ console.log(` 通知音を ${chalk_1.default.red("OFF")} にしました。`);
1547
+ }
1548
+ else {
1549
+ console.log(chalk_1.default.yellow(" on または off を指定してください。"));
1550
+ }
1551
+ }
1552
+ handleFold(args) {
1553
+ const trimmed = args.trim();
1554
+ if (trimmed.length === 0) {
1555
+ const current = (0, formatter_1.getMaxObservations)();
1556
+ if (current == null) {
1557
+ console.log(" 観測点表示: 全件表示");
1558
+ }
1559
+ else {
1560
+ console.log(` 観測点表示: 上位 ${current} 件に制限`);
1561
+ }
1562
+ console.log(chalk_1.default.gray(" 使い方: fold <N> / fold off"));
1563
+ return;
1564
+ }
1565
+ if (trimmed.toLowerCase() === "off") {
1566
+ (0, formatter_1.setMaxObservations)(null);
1567
+ this.config.maxObservations = null;
1568
+ const config = (0, config_1.loadConfig)();
1569
+ delete config.maxObservations;
1570
+ (0, config_1.saveConfig)(config);
1571
+ console.log(" 観測点表示を全件表示に戻しました。");
1572
+ return;
1573
+ }
1574
+ const n = Number(trimmed);
1575
+ if (isNaN(n) || !Number.isInteger(n) || n < 1 || n > 999) {
1576
+ console.log(chalk_1.default.yellow(" 1〜999 の整数、または off を指定してください。"));
1577
+ return;
1578
+ }
1579
+ (0, formatter_1.setMaxObservations)(n);
1580
+ this.config.maxObservations = n;
1581
+ const config = (0, config_1.loadConfig)();
1582
+ config.maxObservations = n;
1583
+ (0, config_1.saveConfig)(config);
1584
+ console.log(` 観測点表示を上位 ${n} 件に制限しました。`);
1585
+ }
1586
+ /** 省略上限キーの日本語ラベル */
1587
+ static TRUNCATION_LABELS = {
1588
+ seismicTextLines: "地震テキスト本文",
1589
+ nankaiTroughLines: "南海トラフ本文",
1590
+ volcanoAlertLines: "火山警報本文",
1591
+ volcanoEruptionLines: "火山観測報本文",
1592
+ volcanoTextLines: "火山解説情報本文",
1593
+ volcanoAshfallQuickLines: "降灰速報(VFVO54)本文",
1594
+ volcanoAshfallDetailLines: "降灰詳細(VFVO55)本文",
1595
+ volcanoAshfallRegularLines: "降灰定時(VFVO53)本文",
1596
+ volcanoPreventionLines: "火山警報 防災事項",
1597
+ volcanoMunicipalities: "火山警報 対象市町村",
1598
+ ashfallAreasQuick: "降灰速報(VFVO54) 地域数",
1599
+ ashfallAreasOther: "降灰予報 地域数",
1600
+ ashfallPeriodsQuick: "降灰速報(VFVO54) 時間帯数",
1601
+ ashfallPeriodsOther: "降灰予報 時間帯数",
1602
+ plumeWindSampleRows: "噴煙流向報 風向データ行数",
1603
+ tsunamiCompactForecastAreas: "津波compact 予報地域数",
1604
+ };
1605
+ handleLimit(args) {
1606
+ const trimmed = args.trim();
1607
+ if (trimmed.length === 0) {
1608
+ // 全項目一覧表示
1609
+ const current = (0, formatter_1.getTruncation)();
1610
+ const defaults = types_1.DEFAULT_CONFIG.truncation;
1611
+ console.log(" 省略表示の上限設定:");
1612
+ console.log();
1613
+ const linesKeys = [
1614
+ "seismicTextLines", "nankaiTroughLines",
1615
+ "volcanoAlertLines", "volcanoEruptionLines", "volcanoTextLines",
1616
+ "volcanoAshfallQuickLines", "volcanoAshfallDetailLines", "volcanoAshfallRegularLines",
1617
+ "volcanoPreventionLines",
1618
+ ];
1619
+ const countKeys = [
1620
+ "volcanoMunicipalities",
1621
+ "ashfallAreasQuick", "ashfallAreasOther",
1622
+ "ashfallPeriodsQuick", "ashfallPeriodsOther",
1623
+ "plumeWindSampleRows", "tsunamiCompactForecastAreas",
1624
+ ];
1625
+ const printGroup = (label, keys) => {
1626
+ console.log(chalk_1.default.gray(` [${label}]`));
1627
+ for (const key of keys) {
1628
+ const val = current[key];
1629
+ const def = defaults[key];
1630
+ const changed = val !== def;
1631
+ const valStr = changed ? chalk_1.default.yellow(String(val)) : String(val);
1632
+ const desc = ReplHandler.TRUNCATION_LABELS[key];
1633
+ console.log(` ${key.padEnd(30)} ${valStr.padStart(changed ? 14 : 4)} ${chalk_1.default.gray(`(default: ${def})`)} ${chalk_1.default.gray(desc)}`);
1634
+ }
1635
+ };
1636
+ printGroup("本文行数", linesKeys);
1637
+ console.log();
1638
+ printGroup("件数", countKeys);
1639
+ console.log();
1640
+ console.log(chalk_1.default.gray(" 使い方: limit <key> <N> / limit <key> default / limit reset"));
1641
+ console.log(chalk_1.default.gray(" ※ infotext full 時は本文行数制限は無効になります"));
1642
+ return;
1643
+ }
1644
+ // limit reset
1645
+ if (trimmed.toLowerCase() === "reset") {
1646
+ const defaults = { ...types_1.DEFAULT_CONFIG.truncation };
1647
+ (0, formatter_1.setTruncation)(defaults);
1648
+ this.config.truncation = defaults;
1649
+ const config = (0, config_1.loadConfig)();
1650
+ delete config.truncation;
1651
+ (0, config_1.saveConfig)(config);
1652
+ console.log(" 省略上限設定を全てデフォルトに戻しました。");
1653
+ return;
1654
+ }
1655
+ // limit <key> <value|default>
1656
+ const parts = trimmed.split(/\s+/);
1657
+ if (parts.length < 2) {
1658
+ console.log(chalk_1.default.yellow(" 使い方: limit <key> <N> / limit <key> default / limit reset"));
1659
+ return;
1660
+ }
1661
+ const [keyStr, valueStr] = parts;
1662
+ if (!config_1.VALID_TRUNCATION_KEYS.includes(keyStr)) {
1663
+ console.log(chalk_1.default.yellow(` 不明なキー: ${keyStr}`));
1664
+ console.log(chalk_1.default.gray(" 有効なキー: limit で一覧表示"));
1665
+ return;
1666
+ }
1667
+ const tKey = keyStr;
1668
+ if (valueStr.toLowerCase() === "default") {
1669
+ // デフォルト値に戻す
1670
+ const defaults = types_1.DEFAULT_CONFIG.truncation;
1671
+ const newTrunc = { ...(0, formatter_1.getTruncation)(), [tKey]: defaults[tKey] };
1672
+ (0, formatter_1.setTruncation)(newTrunc);
1673
+ this.config.truncation = newTrunc;
1674
+ const config = (0, config_1.loadConfig)();
1675
+ if (config.truncation != null) {
1676
+ delete config.truncation[tKey];
1677
+ if (Object.keys(config.truncation).length === 0) {
1678
+ delete config.truncation;
1679
+ }
1680
+ }
1681
+ (0, config_1.saveConfig)(config);
1682
+ console.log(` ${tKey} をデフォルト (${defaults[tKey]}) に戻しました。`);
1683
+ return;
1684
+ }
1685
+ const num = Number(valueStr);
1686
+ if (isNaN(num) || !Number.isInteger(num) || num < 1 || num > 999) {
1687
+ console.log(chalk_1.default.yellow(" 1〜999 の整数、または default を指定してください。"));
1688
+ return;
1689
+ }
1690
+ const newTrunc = { ...(0, formatter_1.getTruncation)(), [tKey]: num };
1691
+ (0, formatter_1.setTruncation)(newTrunc);
1692
+ this.config.truncation = newTrunc;
1693
+ const config = (0, config_1.loadConfig)();
1694
+ if (config.truncation == null)
1695
+ config.truncation = {};
1696
+ config.truncation[tKey] = num;
1697
+ (0, config_1.saveConfig)(config);
1698
+ console.log(` ${tKey} を ${num} に変更しました。`);
1699
+ }
1700
+ handleMute(args) {
1701
+ const trimmed = args.trim();
1702
+ if (trimmed.length === 0) {
1703
+ if (this.notifier.isMuted()) {
1704
+ const remaining = this.notifier.muteRemaining();
1705
+ console.log(` ミュート中: 残り ${formatDuration(remaining)}`);
1706
+ }
1707
+ else {
1708
+ console.log(" ミュートなし");
1709
+ }
1710
+ console.log(chalk_1.default.gray(" 使い方: mute <duration> (例: 30m, 1h, 90s) / mute off"));
1711
+ return;
1712
+ }
1713
+ if (trimmed.toLowerCase() === "off") {
1714
+ this.notifier.unmute();
1715
+ console.log(" ミュートを解除しました。");
1716
+ return;
1717
+ }
1718
+ const ms = parseDuration(trimmed);
1719
+ if (ms == null || ms <= 0) {
1720
+ console.log(chalk_1.default.yellow(" 無効な時間指定です。例: 30m, 1h, 90s"));
1721
+ return;
1722
+ }
1723
+ this.notifier.mute(ms);
1724
+ console.log(` 通知を ${formatDuration(ms)} ミュートしました。`);
1725
+ }
1726
+ handleEewLog(args) {
1727
+ const trimmed = args.trim();
1728
+ // 引数なし → 現在の設定を表示
1729
+ if (trimmed.length === 0) {
1730
+ const enabled = this.eewLogger.isEnabled();
1731
+ const status = enabled ? chalk_1.default.green("ON") : chalk_1.default.red("OFF");
1732
+ console.log();
1733
+ console.log(chalk_1.default.cyan.bold(" EEW ログ記録:") + ` ${status}`);
1734
+ if (enabled) {
1735
+ console.log();
1736
+ const fields = this.eewLogger.getFields();
1737
+ for (const group of EEW_LOG_FIELD_GROUPS) {
1738
+ console.log(chalk_1.default.cyan(` [${group.label}]`));
1739
+ for (const field of group.fields) {
1740
+ const fieldEnabled = fields[field];
1741
+ const fieldStatus = fieldEnabled ? chalk_1.default.green("ON") : chalk_1.default.red("OFF");
1742
+ console.log(chalk_1.default.white(` ${field.padEnd(22)}`) +
1743
+ chalk_1.default.gray(`${EEW_LOG_FIELD_LABELS[field]} `) +
1744
+ fieldStatus);
1745
+ }
1746
+ }
1747
+ }
1748
+ console.log();
1749
+ console.log(chalk_1.default.gray(" 使い方: eewlog on/off / eewlog fields / eewlog fields <field> [on|off]"));
1750
+ console.log();
1751
+ return;
1752
+ }
1753
+ // on / off (case-insensitive)
1754
+ const eewlogLower = trimmed.toLowerCase();
1755
+ if (eewlogLower === "on") {
1756
+ this.eewLogger.setEnabled(true);
1757
+ this.config.eewLog = true;
1758
+ const config = (0, config_1.loadConfig)();
1759
+ config.eewLog = true;
1760
+ (0, config_1.saveConfig)(config);
1761
+ console.log(` EEW ログ記録を ${chalk_1.default.green("ON")} にしました。`);
1762
+ return;
1763
+ }
1764
+ if (eewlogLower === "off") {
1765
+ this.eewLogger.setEnabled(false);
1766
+ this.config.eewLog = false;
1767
+ const config = (0, config_1.loadConfig)();
1768
+ config.eewLog = false;
1769
+ (0, config_1.saveConfig)(config);
1770
+ console.log(` EEW ログ記録を ${chalk_1.default.red("OFF")} にしました。`);
1771
+ return;
1772
+ }
1773
+ // fields サブコマンド (case-insensitive: "fields" or "fld")
1774
+ if (eewlogLower === "fields" || eewlogLower === "fld") {
1775
+ const fields = this.eewLogger.getFields();
1776
+ console.log();
1777
+ console.log(chalk_1.default.cyan.bold(" EEW ログ記録項目:"));
1778
+ console.log();
1779
+ for (const group of EEW_LOG_FIELD_GROUPS) {
1780
+ console.log(chalk_1.default.cyan(` [${group.label}]`));
1781
+ for (const field of group.fields) {
1782
+ const fieldEnabled = fields[field];
1783
+ const fieldStatus = fieldEnabled ? chalk_1.default.green("ON") : chalk_1.default.red("OFF");
1784
+ console.log(chalk_1.default.white(` ${field.padEnd(22)}`) +
1785
+ chalk_1.default.gray(`${EEW_LOG_FIELD_LABELS[field]} `) +
1786
+ fieldStatus);
1787
+ }
1788
+ }
1789
+ console.log();
1790
+ return;
1791
+ }
1792
+ // fields <field> [on|off] (case-insensitive prefix)
1793
+ if (eewlogLower.startsWith("fields ") || eewlogLower.startsWith("fld ")) {
1794
+ const fieldsPrefixLen = eewlogLower.startsWith("fld ") ? 4 : 7;
1795
+ const parts = trimmed.slice(fieldsPrefixLen).trim().split(/\s+/);
1796
+ const fieldName = parts[0];
1797
+ const action = parts[1]?.toLowerCase();
1798
+ if (!config_1.VALID_EEW_LOG_FIELDS.includes(fieldName)) {
1799
+ console.log(chalk_1.default.yellow(` 不明な項目: ${parts[0]}`) +
1800
+ chalk_1.default.gray(` (有効: ${config_1.VALID_EEW_LOG_FIELDS.join(", ")})`));
1801
+ return;
1802
+ }
1803
+ let newState;
1804
+ const fields = this.eewLogger.getFields();
1805
+ if (action === "on") {
1806
+ if (fields[fieldName]) {
1807
+ console.log(` ${EEW_LOG_FIELD_LABELS[fieldName]} (${fieldName}): 既に ${chalk_1.default.green("ON")} です`);
1808
+ return;
1809
+ }
1810
+ newState = this.eewLogger.toggleField(fieldName);
1811
+ }
1812
+ else if (action === "off") {
1813
+ if (!fields[fieldName]) {
1814
+ console.log(` ${EEW_LOG_FIELD_LABELS[fieldName]} (${fieldName}): 既に ${chalk_1.default.red("OFF")} です`);
1815
+ return;
1816
+ }
1817
+ newState = this.eewLogger.toggleField(fieldName);
1818
+ }
1819
+ else {
1820
+ newState = this.eewLogger.toggleField(fieldName);
1821
+ }
1822
+ const label = EEW_LOG_FIELD_LABELS[fieldName];
1823
+ const status = newState ? chalk_1.default.green("ON") : chalk_1.default.red("OFF");
1824
+ console.log(` ${label} (${fieldName}): ${status}`);
1825
+ // 設定を永続化
1826
+ const config = (0, config_1.loadConfig)();
1827
+ config.eewLogFields = this.eewLogger.getFields();
1828
+ (0, config_1.saveConfig)(config);
1829
+ return;
1830
+ }
1831
+ console.log(chalk_1.default.yellow(" 使い方: eewlog on/off / eewlog fields / eewlog fields <field> [on|off]"));
1832
+ }
1833
+ handleTest(args) {
1834
+ const parts = args.trim().split(/\s+/).filter(Boolean);
1835
+ const sub = parts[0] ?? "";
1836
+ if (sub === "") {
1837
+ const testEntry = this.commands["test"];
1838
+ console.log();
1839
+ console.log(chalk_1.default.cyan.bold(" test サブコマンド:"));
1840
+ if (testEntry.subcommands) {
1841
+ for (const [name, sc] of Object.entries(testEntry.subcommands)) {
1842
+ console.log(chalk_1.default.white(` ${name.padEnd(14)}`) + chalk_1.default.gray(sc.description));
1843
+ }
1844
+ }
1845
+ console.log();
1846
+ console.log(chalk_1.default.gray(" 詳細: help test <subcommand>"));
1847
+ console.log();
1848
+ return;
1849
+ }
1850
+ const subLower = sub.toLowerCase();
1851
+ if (subLower === "sound" || subLower === "snd") {
1852
+ this.handleTestSound(parts.slice(1).join(" "));
1853
+ return;
1854
+ }
1855
+ if (subLower === "table" || subLower === "tbl") {
1856
+ this.handleTestTable(parts.slice(1).join(" "));
1857
+ return;
1858
+ }
1859
+ console.log(chalk_1.default.yellow(` 不明なサブコマンド: ${sub}`) + chalk_1.default.gray(" (sound / table)"));
1860
+ }
1861
+ handleTestSound(args) {
1862
+ const level = args.trim().toLowerCase();
1863
+ if (level === "") {
1864
+ console.log();
1865
+ console.log(chalk_1.default.cyan.bold(" 利用可能なサウンドレベル:"));
1866
+ for (const l of sound_player_1.SOUND_LEVELS) {
1867
+ console.log(chalk_1.default.white(` ${l}`));
1868
+ }
1869
+ console.log();
1870
+ console.log(chalk_1.default.gray(" 使い方: test sound <level>"));
1871
+ console.log();
1872
+ return;
1873
+ }
1874
+ if (!(0, sound_player_1.isSoundLevel)(level)) {
1875
+ console.log(chalk_1.default.yellow(` 不明なサウンドレベル: ${level}`));
1876
+ console.log(chalk_1.default.gray(` 有効な値: ${sound_player_1.SOUND_LEVELS.join(", ")}`));
1877
+ return;
1878
+ }
1879
+ console.log(chalk_1.default.gray(` サウンドテスト: ${level} を再生中...`));
1880
+ (0, sound_player_1.playSound)(level);
1881
+ }
1882
+ handleTestTable(args) {
1883
+ const parts = args.trim().split(/\s+/).filter(Boolean);
1884
+ const type = parts[0] ?? "";
1885
+ const variantArg = parts[1];
1886
+ if (type === "") {
1887
+ console.log();
1888
+ console.log(chalk_1.default.cyan.bold(" 利用可能な電文タイプ:"));
1889
+ // エイリアス逆引きマップ (正式名 → 短縮形[])
1890
+ const aliasReverse = {};
1891
+ for (const [alias, canonical] of Object.entries(ReplHandler.TABLE_TYPE_ALIASES)) {
1892
+ (aliasReverse[canonical] ??= []).push(alias);
1893
+ }
1894
+ for (const [key, entry] of Object.entries(test_samples_1.TEST_TABLES)) {
1895
+ const count = entry.variants.length;
1896
+ const aliases = aliasReverse[key];
1897
+ const aliasText = aliases != null ? ` (${aliases.join(", ")})` : "";
1898
+ const nameCol = `${key}${aliasText}`;
1899
+ console.log(chalk_1.default.white(` ${nameCol.padEnd(24)}`) +
1900
+ chalk_1.default.gray(`${entry.label}`) +
1901
+ chalk_1.default.gray(` (${count}件)`));
1902
+ }
1903
+ console.log();
1904
+ console.log(chalk_1.default.gray(" 使い方: test table <type> [番号]"));
1905
+ console.log();
1906
+ return;
1907
+ }
1908
+ const resolvedType = ReplHandler.resolveTestTableType(type);
1909
+ const entry = resolvedType != null ? test_samples_1.TEST_TABLES[resolvedType] : undefined;
1910
+ if (entry == null) {
1911
+ console.log(chalk_1.default.yellow(` 不明な電文タイプ: ${type}`));
1912
+ console.log(chalk_1.default.gray(` 有効な値: ${Object.keys(test_samples_1.TEST_TABLES).join(", ")}`));
1913
+ return;
1914
+ }
1915
+ // 番号指定なし → バリエーション一覧を表示
1916
+ if (variantArg == null) {
1917
+ console.log();
1918
+ console.log(chalk_1.default.cyan.bold(` ${entry.label} バリエーション:`));
1919
+ for (const [i, v] of entry.variants.entries()) {
1920
+ console.log(chalk_1.default.white(` ${String(i + 1).padEnd(4)}`) + chalk_1.default.gray(v.label));
1921
+ }
1922
+ console.log();
1923
+ console.log(chalk_1.default.gray(` 使い方: test table ${resolvedType} <番号>`));
1924
+ console.log();
1925
+ return;
1926
+ }
1927
+ const variantNum = parseInt(variantArg, 10);
1928
+ if (isNaN(variantNum) || variantNum < 1 || variantNum > entry.variants.length) {
1929
+ console.log(chalk_1.default.yellow(` 不明な番号: ${variantArg} (1〜${entry.variants.length})`));
1930
+ return;
1931
+ }
1932
+ const variant = entry.variants[variantNum - 1];
1933
+ console.log(chalk_1.default.gray(` 表示テスト: ${entry.label} #${variantNum} ${variant.label}`));
1934
+ variant.run();
1935
+ }
1936
+ handleClear() {
1937
+ if (process.stdout.isTTY) {
1938
+ // readline 互換のクリア (内部カーソル状態を壊さない)
1939
+ readline_1.default.cursorTo(process.stdout, 0, 0);
1940
+ readline_1.default.clearScreenDown(process.stdout);
1941
+ }
1942
+ else {
1943
+ console.clear();
1944
+ }
1945
+ }
1946
+ async handleRetry() {
1947
+ const status = this.wsManager.getStatus();
1948
+ if (status.connected) {
1949
+ console.log(chalk_1.default.gray(" 既に接続中です。"));
1950
+ return;
1951
+ }
1952
+ console.log(chalk_1.default.gray(" 再接続を試行中..."));
1953
+ try {
1954
+ await this.wsManager.connect();
1955
+ }
1956
+ catch (err) {
1957
+ log.error(`再接続に失敗しました: ${err instanceof Error ? err.message : err}`);
1958
+ }
1959
+ }
1960
+ async handleQuit() {
1961
+ this.stop();
1962
+ await this.onQuit();
1963
+ }
1964
+ maybeShowWaitingTip() {
1965
+ if (!this.rl || this.commandRunning)
1966
+ return;
1967
+ if (this.tipIntervalMs <= 0 || this.nextTipAt == null)
1968
+ return;
1969
+ const status = this.wsManager.getStatus();
1970
+ if (!status.connected)
1971
+ return;
1972
+ const lastMessageAt = this.statusLine.getLastMessageTime();
1973
+ if (lastMessageAt != null && Date.now() - lastMessageAt < 10_000)
1974
+ return;
1975
+ if (Date.now() < this.nextTipAt)
1976
+ return;
1977
+ this.clearInput();
1978
+ const tip = waiting_tips_1.WAITING_TIPS[this.tipIndex % waiting_tips_1.WAITING_TIPS.length];
1979
+ this.tipIndex++;
1980
+ do {
1981
+ this.nextTipAt += this.tipIntervalMs;
1982
+ } while (this.nextTipAt <= Date.now());
1983
+ console.log(chalk_1.default.gray(` ${tip}`));
1984
+ }
1985
+ resetTipSchedule() {
1986
+ if (this.tipIntervalMs <= 0) {
1987
+ this.nextTipAt = null;
1988
+ return;
1989
+ }
1990
+ this.nextTipAt = Date.now() + this.tipIntervalMs;
1991
+ }
1992
+ }
1993
+ exports.ReplHandler = ReplHandler;
1994
+ /** ISO 文字列を "MM-DD HH:mm:ss" に整形 (テーブル用短縮形) */
1995
+ function formatShortTime(iso) {
1996
+ const d = new Date(iso);
1997
+ if (isNaN(d.getTime()))
1998
+ return iso;
1999
+ const pad = (n) => String(n).padStart(2, "0");
2000
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
2001
+ }
2002
+ /** 文字列を視覚幅で指定幅に切り詰める */
2003
+ function truncate(str, maxWidth) {
2004
+ if (maxWidth <= 0)
2005
+ return "";
2006
+ if ((0, formatter_1.visualWidth)(str) <= maxWidth)
2007
+ return str;
2008
+ const ellipsis = "…";
2009
+ const ellipsisWidth = (0, formatter_1.visualWidth)(ellipsis);
2010
+ if (maxWidth <= ellipsisWidth)
2011
+ return ellipsis;
2012
+ const targetWidth = maxWidth - ellipsisWidth;
2013
+ let result = "";
2014
+ let width = 0;
2015
+ for (const ch of str) {
2016
+ const chWidth = (0, formatter_1.visualWidth)(ch);
2017
+ if (width + chWidth > targetWidth)
2018
+ break;
2019
+ result += ch;
2020
+ width += chWidth;
2021
+ }
2022
+ return result + ellipsis;
2023
+ }
2024
+ /** GdEarthquakeItem から深さ文字列を生成 */
2025
+ function formatDepth(item) {
2026
+ if (item.hypocenter?.depth?.value != null) {
2027
+ const val = item.hypocenter.depth.value;
2028
+ const unit = item.hypocenter.depth.unit || "km";
2029
+ return `${val}${unit}`;
2030
+ }
2031
+ return "---";
2032
+ }
2033
+ /** 時間文字列をミリ秒に変換 (例: "30m" → 1800000, "1h" → 3600000, "90s" → 90000) */
2034
+ function parseDuration(input) {
2035
+ const match = input.match(/^(\d+)(s|m|h)$/);
2036
+ if (!match)
2037
+ return null;
2038
+ const value = parseInt(match[1], 10);
2039
+ const unit = match[2];
2040
+ switch (unit) {
2041
+ case "s": return value * 1000;
2042
+ case "m": return value * 60 * 1000;
2043
+ case "h": return value * 60 * 60 * 1000;
2044
+ default: return null;
2045
+ }
2046
+ }
2047
+ /** ミリ秒を人間可読な時間文字列に変換 */
2048
+ function formatDuration(ms) {
2049
+ const totalSec = Math.ceil(ms / 1000);
2050
+ if (totalSec < 60)
2051
+ return `${totalSec}秒`;
2052
+ const min = Math.floor(totalSec / 60);
2053
+ const sec = totalSec % 60;
2054
+ if (min < 60)
2055
+ return sec > 0 ? `${min}分${sec}秒` : `${min}分`;
2056
+ const hour = Math.floor(min / 60);
2057
+ const remMin = min % 60;
2058
+ return remMin > 0 ? `${hour}時間${remMin}分` : `${hour}時間`;
2059
+ }