@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.
- package/CHANGELOG.md +898 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/assets/icons/.gitkeep +0 -0
- package/assets/sounds/.gitkeep +0 -0
- package/assets/sounds/cancel.mp3 +0 -0
- package/assets/sounds/critical.mp3 +0 -0
- package/assets/sounds/info.mp3 +0 -0
- package/assets/sounds/normal.mp3 +0 -0
- package/assets/sounds/warning.mp3 +0 -0
- package/dist/config.js +638 -0
- package/dist/dmdata/connection-manager.js +2 -0
- package/dist/dmdata/endpoint-selector.js +185 -0
- package/dist/dmdata/multi-connection-manager.js +158 -0
- package/dist/dmdata/rest-client.js +281 -0
- package/dist/dmdata/telegram-parser.js +704 -0
- package/dist/dmdata/volcano-parser.js +647 -0
- package/dist/dmdata/ws-client.js +336 -0
- package/dist/engine/cli/cli-init.js +266 -0
- package/dist/engine/cli/cli-run.js +121 -0
- package/dist/engine/cli/cli.js +121 -0
- package/dist/engine/eew/eew-logger.js +355 -0
- package/dist/engine/eew/eew-tracker.js +229 -0
- package/dist/engine/messages/message-router.js +261 -0
- package/dist/engine/messages/tsunami-state.js +96 -0
- package/dist/engine/messages/volcano-state.js +131 -0
- package/dist/engine/messages/volcano-vfvo53-aggregator.js +173 -0
- package/dist/engine/monitor/monitor.js +118 -0
- package/dist/engine/monitor/repl-coordinator.js +63 -0
- package/dist/engine/monitor/shutdown.js +114 -0
- package/dist/engine/notification/node-notifier-loader.js +19 -0
- package/dist/engine/notification/notifier.js +338 -0
- package/dist/engine/notification/sound-player.js +230 -0
- package/dist/engine/notification/volcano-presentation.js +166 -0
- package/dist/engine/startup/config-resolver.js +139 -0
- package/dist/engine/startup/tsunami-initializer.js +91 -0
- package/dist/engine/startup/update-checker.js +229 -0
- package/dist/engine/startup/volcano-initializer.js +89 -0
- package/dist/index.js +24 -0
- package/dist/logger.js +95 -0
- package/dist/types.js +61 -0
- package/dist/ui/earthquake-formatter.js +871 -0
- package/dist/ui/eew-formatter.js +335 -0
- package/dist/ui/formatter.js +689 -0
- package/dist/ui/repl.js +2059 -0
- package/dist/ui/test-samples.js +880 -0
- package/dist/ui/theme.js +516 -0
- package/dist/ui/volcano-formatter.js +667 -0
- package/dist/ui/waiting-tips.js +227 -0
- package/dist/utils/intensity.js +13 -0
- package/dist/utils/secrets.js +14 -0
- package/package.json +69 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.VALID_TRUNCATION_KEYS = exports.VALID_EEW_LOG_FIELDS = exports.VALID_CLASSIFICATIONS = exports.ConfigError = void 0;
|
|
37
|
+
exports.resolveConfigDir = resolveConfigDir;
|
|
38
|
+
exports.getConfigDir = getConfigDir;
|
|
39
|
+
exports.getConfigPath = getConfigPath;
|
|
40
|
+
exports.loadConfig = loadConfig;
|
|
41
|
+
exports.saveConfig = saveConfig;
|
|
42
|
+
exports.setConfigValue = setConfigValue;
|
|
43
|
+
exports.unsetConfigValue = unsetConfigValue;
|
|
44
|
+
exports.printConfig = printConfig;
|
|
45
|
+
exports.printConfigKeys = printConfigKeys;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
49
|
+
const types_1 = require("./types");
|
|
50
|
+
const secretUtils = __importStar(require("./utils/secrets"));
|
|
51
|
+
const log = __importStar(require("./logger"));
|
|
52
|
+
/** 設定エラー */
|
|
53
|
+
class ConfigError extends Error {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "ConfigError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.ConfigError = ConfigError;
|
|
60
|
+
/** 旧Configファイルのディレクトリ (マイグレーション用) */
|
|
61
|
+
const OLD_CONFIG_DIR = path.join(os.homedir(), ".config", "dmdata-monitor");
|
|
62
|
+
/** レガシーConfigディレクトリ (macOS/Windows でのマイグレーション用) */
|
|
63
|
+
const LEGACY_CONFIG_DIR = path.join(os.homedir(), ".config", "fleq");
|
|
64
|
+
/**
|
|
65
|
+
* OS・環境変数・ホームディレクトリからConfigディレクトリを解決する純粋関数。
|
|
66
|
+
* テスト時に platform / env / homedir を差し替えられる。
|
|
67
|
+
*
|
|
68
|
+
* 優先順位:
|
|
69
|
+
* 1. 環境変数 XDG_CONFIG_HOME (設定されている場合)
|
|
70
|
+
* 2. OS 別のデフォルトパス:
|
|
71
|
+
* - macOS: ~/Library/Application Support/fleq
|
|
72
|
+
* - Windows: %APPDATA%/fleq
|
|
73
|
+
* - Linux 等: ~/.config/fleq (XDG 標準)
|
|
74
|
+
*/
|
|
75
|
+
function resolveConfigDir(platform = process.platform, env = process.env, homedir = os.homedir()) {
|
|
76
|
+
if (env.XDG_CONFIG_HOME) {
|
|
77
|
+
return path.join(env.XDG_CONFIG_HOME, "fleq");
|
|
78
|
+
}
|
|
79
|
+
switch (platform) {
|
|
80
|
+
case "darwin":
|
|
81
|
+
return path.join(homedir, "Library", "Application Support", "fleq");
|
|
82
|
+
case "win32":
|
|
83
|
+
return path.join(env.APPDATA || path.join(homedir, "AppData", "Roaming"), "fleq");
|
|
84
|
+
default:
|
|
85
|
+
return path.join(homedir, ".config", "fleq");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** 現在のプロセス環境で設定ディレクトリを返す (resolveConfigDir のショートハンド) */
|
|
89
|
+
function getConfigDir() {
|
|
90
|
+
return resolveConfigDir();
|
|
91
|
+
}
|
|
92
|
+
/** Configファイルのディレクトリ */
|
|
93
|
+
const CONFIG_DIR = getConfigDir();
|
|
94
|
+
/** Configファイルのパス */
|
|
95
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
96
|
+
/** Configファイルの権限を可能な範囲で 0600 に寄せる */
|
|
97
|
+
function hardenConfigPermissions(filePath) {
|
|
98
|
+
try {
|
|
99
|
+
fs.chmodSync(filePath, 0o600);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err instanceof Error) {
|
|
103
|
+
log.warn(`Configファイル権限の調整に失敗しました: ${err.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 旧パスから新パスへ設定ファイルをマイグレーションする。
|
|
109
|
+
*
|
|
110
|
+
* 移行元の優先順位 (先にヒットした方を移行):
|
|
111
|
+
* 1. ~/.config/fleq/config.json (macOS/Windows でレガシーパスに保存されていた場合)
|
|
112
|
+
* 2. ~/.config/dmdata-monitor/config.json (旧アプリ名)
|
|
113
|
+
*/
|
|
114
|
+
function migrateConfigIfNeeded() {
|
|
115
|
+
if (fs.existsSync(CONFIG_PATH))
|
|
116
|
+
return;
|
|
117
|
+
// パス比較は正規化して行う (symlink・末尾スラッシュ・大文字小文字の差異を吸収)
|
|
118
|
+
const isSamePath = (a, b) => path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
|
|
119
|
+
// 移行元候補 (優先順)
|
|
120
|
+
const migrationSources = [
|
|
121
|
+
// レガシーパス (macOS/Windows で ~/.config/fleq/ に保存されていた場合)
|
|
122
|
+
...(!isSamePath(LEGACY_CONFIG_DIR, CONFIG_DIR)
|
|
123
|
+
? [path.join(LEGACY_CONFIG_DIR, "config.json")]
|
|
124
|
+
: []),
|
|
125
|
+
// 旧アプリ名
|
|
126
|
+
path.join(OLD_CONFIG_DIR, "config.json"),
|
|
127
|
+
];
|
|
128
|
+
for (const sourcePath of migrationSources) {
|
|
129
|
+
if (fs.existsSync(sourcePath)) {
|
|
130
|
+
try {
|
|
131
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
132
|
+
// COPYFILE_EXCL: 既にファイルが存在する場合はエラーにして上書きを防ぐ
|
|
133
|
+
fs.copyFileSync(sourcePath, CONFIG_PATH, fs.constants.COPYFILE_EXCL);
|
|
134
|
+
hardenConfigPermissions(CONFIG_PATH);
|
|
135
|
+
log.info(`設定ファイルを移行しました: ${sourcePath} → ${CONFIG_PATH}`);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
// 別プロセスが先に作成した場合は成功扱い
|
|
139
|
+
const e = err;
|
|
140
|
+
if (e.code === "EEXIST")
|
|
141
|
+
return;
|
|
142
|
+
if (err instanceof Error) {
|
|
143
|
+
log.warn(`設定ファイルの移行に失敗しました: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** 有効な分類区分 */
|
|
151
|
+
exports.VALID_CLASSIFICATIONS = [
|
|
152
|
+
"telegram.earthquake",
|
|
153
|
+
"eew.forecast",
|
|
154
|
+
"eew.warning",
|
|
155
|
+
"telegram.volcano",
|
|
156
|
+
];
|
|
157
|
+
/** 有効なテストモード */
|
|
158
|
+
const VALID_TEST_MODES = ["no", "including", "only"];
|
|
159
|
+
/** 有効な表示モード */
|
|
160
|
+
const VALID_DISPLAY_MODES = ["normal", "compact"];
|
|
161
|
+
/** 有効なプロンプト時計モード */
|
|
162
|
+
const VALID_PROMPT_CLOCKS = ["elapsed", "clock"];
|
|
163
|
+
/** 有効な EEW ログ記録項目 */
|
|
164
|
+
exports.VALID_EEW_LOG_FIELDS = [
|
|
165
|
+
"hypocenter",
|
|
166
|
+
"originTime",
|
|
167
|
+
"coordinates",
|
|
168
|
+
"magnitude",
|
|
169
|
+
"forecastIntensity",
|
|
170
|
+
"maxLgInt",
|
|
171
|
+
"forecastAreas",
|
|
172
|
+
"lgIntensity",
|
|
173
|
+
"isPlum",
|
|
174
|
+
"hasArrived",
|
|
175
|
+
"diff",
|
|
176
|
+
"maxIntChangeReason",
|
|
177
|
+
];
|
|
178
|
+
/** 有効な通知カテゴリ */
|
|
179
|
+
const VALID_NOTIFY_CATEGORIES = [
|
|
180
|
+
"eew",
|
|
181
|
+
"earthquake",
|
|
182
|
+
"tsunami",
|
|
183
|
+
"seismicText",
|
|
184
|
+
"nankaiTrough",
|
|
185
|
+
"lgObservation",
|
|
186
|
+
"volcano",
|
|
187
|
+
];
|
|
188
|
+
/** 有効な省略上限キー */
|
|
189
|
+
exports.VALID_TRUNCATION_KEYS = [
|
|
190
|
+
"seismicTextLines",
|
|
191
|
+
"nankaiTroughLines",
|
|
192
|
+
"volcanoAlertLines",
|
|
193
|
+
"volcanoEruptionLines",
|
|
194
|
+
"volcanoTextLines",
|
|
195
|
+
"volcanoAshfallQuickLines",
|
|
196
|
+
"volcanoAshfallDetailLines",
|
|
197
|
+
"volcanoAshfallRegularLines",
|
|
198
|
+
"volcanoPreventionLines",
|
|
199
|
+
"volcanoMunicipalities",
|
|
200
|
+
"ashfallAreasQuick",
|
|
201
|
+
"ashfallAreasOther",
|
|
202
|
+
"ashfallPeriodsQuick",
|
|
203
|
+
"ashfallPeriodsOther",
|
|
204
|
+
"plumeWindSampleRows",
|
|
205
|
+
"tsunamiCompactForecastAreas",
|
|
206
|
+
];
|
|
207
|
+
/** 設定可能なキーと説明 */
|
|
208
|
+
const CONFIG_KEYS = {
|
|
209
|
+
apiKey: "dmdata.jp APIキー",
|
|
210
|
+
classifications: "受信区分 (カンマ区切り: telegram.earthquake,eew.forecast,eew.warning)",
|
|
211
|
+
testMode: 'テスト電文モード: "no" | "including" | "only"',
|
|
212
|
+
appName: "アプリケーション名",
|
|
213
|
+
maxReconnectDelaySec: "再接続の最大待機秒数",
|
|
214
|
+
keepExistingConnections: "既存のWebSocket接続を維持するか (true/false)",
|
|
215
|
+
tableWidth: 'テーブル表示幅 (40〜200 / "auto" でターミナル幅に自動追従)',
|
|
216
|
+
infoFullText: "お知らせ電文の全文表示 (true/false)",
|
|
217
|
+
displayMode: '表示モード: "normal" | "compact"',
|
|
218
|
+
promptClock: 'プロンプト時計: "elapsed" (経過時間) | "clock" (現在時刻)',
|
|
219
|
+
waitTipIntervalMin: "待機中ヒント表示間隔 (分, 0で無効)",
|
|
220
|
+
sound: "通知音の有効/無効 (true/false)",
|
|
221
|
+
eewLog: "EEWログ記録の有効/無効 (true/false)",
|
|
222
|
+
maxObservations: '観測点の最大表示件数 (1〜999 / "off" で全件表示)',
|
|
223
|
+
backup: "EEW副回線の有効/無効 (true/false)",
|
|
224
|
+
truncation: "省略表示の上限設定 (truncation.<key> で個別設定)",
|
|
225
|
+
};
|
|
226
|
+
/** Configファイルのパスを返す */
|
|
227
|
+
function getConfigPath() {
|
|
228
|
+
return CONFIG_PATH;
|
|
229
|
+
}
|
|
230
|
+
/** Configファイルを読み込む */
|
|
231
|
+
function loadConfig() {
|
|
232
|
+
migrateConfigIfNeeded();
|
|
233
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
hardenConfigPermissions(CONFIG_PATH);
|
|
238
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
239
|
+
const parsed = JSON.parse(raw);
|
|
240
|
+
if (typeof parsed !== "object" || parsed == null || Array.isArray(parsed)) {
|
|
241
|
+
log.warn("Configファイルの形式が不正です。無視します。");
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
return validateConfig(parsed);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
if (err instanceof SyntaxError) {
|
|
248
|
+
log.warn("ConfigファイルのJSONパースに失敗しました。無視します。");
|
|
249
|
+
}
|
|
250
|
+
else if (err instanceof Error) {
|
|
251
|
+
log.warn(`Configファイルの読み込みに失敗しました: ${err.message}`);
|
|
252
|
+
}
|
|
253
|
+
return {};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Configファイルに書き込む (APIキーを含むため 0600 で保存) */
|
|
257
|
+
function saveConfig(config) {
|
|
258
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
259
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
260
|
+
}
|
|
261
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
|
|
262
|
+
encoding: "utf-8",
|
|
263
|
+
mode: 0o600,
|
|
264
|
+
});
|
|
265
|
+
hardenConfigPermissions(CONFIG_PATH);
|
|
266
|
+
}
|
|
267
|
+
/** パースした値をバリデーションして ConfigFile にする */
|
|
268
|
+
function validateConfig(raw) {
|
|
269
|
+
const config = {};
|
|
270
|
+
applyApiKey(config, raw.apiKey);
|
|
271
|
+
applyClassifications(config, raw.classifications);
|
|
272
|
+
applyTestMode(config, raw.testMode);
|
|
273
|
+
applyAppName(config, raw.appName);
|
|
274
|
+
applyReconnectDelay(config, raw.maxReconnectDelaySec);
|
|
275
|
+
applyBooleanField(config, "keepExistingConnections", raw.keepExistingConnections);
|
|
276
|
+
applyTableWidth(config, raw.tableWidth);
|
|
277
|
+
applyBooleanField(config, "infoFullText", raw.infoFullText);
|
|
278
|
+
applyDisplayMode(config, raw.displayMode);
|
|
279
|
+
applyPromptClock(config, raw.promptClock);
|
|
280
|
+
applyWaitTipInterval(config, raw.waitTipIntervalMin);
|
|
281
|
+
applyBooleanField(config, "sound", raw.sound);
|
|
282
|
+
applyBooleanField(config, "eewLog", raw.eewLog);
|
|
283
|
+
applyEewLogFields(config, raw.eewLogFields);
|
|
284
|
+
applyNotifySettings(config, raw.notify);
|
|
285
|
+
applyMaxObservations(config, raw.maxObservations);
|
|
286
|
+
applyBooleanField(config, "backup", raw.backup);
|
|
287
|
+
applyTruncation(config, raw.truncation);
|
|
288
|
+
return config;
|
|
289
|
+
}
|
|
290
|
+
function applyApiKey(config, value) {
|
|
291
|
+
if (typeof value === "string" && value.length > 0) {
|
|
292
|
+
config.apiKey = value;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function applyClassifications(config, value) {
|
|
296
|
+
if (typeof value === "string") {
|
|
297
|
+
const parsed = parseClassifications(value);
|
|
298
|
+
if (parsed.length > 0) {
|
|
299
|
+
config.classifications = parsed;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else if (Array.isArray(value)) {
|
|
303
|
+
const validClassifications = value.filter((c) => typeof c === "string" &&
|
|
304
|
+
exports.VALID_CLASSIFICATIONS.includes(c));
|
|
305
|
+
if (validClassifications.length > 0) {
|
|
306
|
+
config.classifications = validClassifications;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function applyTestMode(config, value) {
|
|
311
|
+
if (typeof value === "string" &&
|
|
312
|
+
VALID_TEST_MODES.includes(value)) {
|
|
313
|
+
config.testMode = value;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function applyAppName(config, value) {
|
|
317
|
+
if (typeof value === "string" && value.length > 0) {
|
|
318
|
+
config.appName = value;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function applyReconnectDelay(config, value) {
|
|
322
|
+
if (typeof value === "number" && value > 0) {
|
|
323
|
+
config.maxReconnectDelaySec = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function applyBooleanField(config, field, value) {
|
|
327
|
+
if (typeof value === "boolean") {
|
|
328
|
+
config[field] = value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function applyTableWidth(config, value) {
|
|
332
|
+
if (typeof value === "number" && value >= 40 && value <= 200) {
|
|
333
|
+
config.tableWidth = value;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function applyDisplayMode(config, value) {
|
|
337
|
+
if (typeof value === "string" &&
|
|
338
|
+
VALID_DISPLAY_MODES.includes(value)) {
|
|
339
|
+
config.displayMode = value;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function applyPromptClock(config, value) {
|
|
343
|
+
if (typeof value === "string" &&
|
|
344
|
+
VALID_PROMPT_CLOCKS.includes(value)) {
|
|
345
|
+
config.promptClock = value;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function applyWaitTipInterval(config, value) {
|
|
349
|
+
if (typeof value === "number" &&
|
|
350
|
+
Number.isInteger(value) &&
|
|
351
|
+
value >= 0 &&
|
|
352
|
+
value <= 1440) {
|
|
353
|
+
config.waitTipIntervalMin = value;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function applyNotifySettings(config, value) {
|
|
357
|
+
if (typeof value !== "object" || value == null || Array.isArray(value)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const notifyRaw = value;
|
|
361
|
+
const notify = {};
|
|
362
|
+
for (const [key, val] of Object.entries(notifyRaw)) {
|
|
363
|
+
if (VALID_NOTIFY_CATEGORIES.includes(key) &&
|
|
364
|
+
typeof val === "boolean") {
|
|
365
|
+
notify[key] = val;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (Object.keys(notify).length > 0) {
|
|
369
|
+
config.notify = notify;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function applyMaxObservations(config, value) {
|
|
373
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 999) {
|
|
374
|
+
config.maxObservations = value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function applyTruncation(config, value) {
|
|
378
|
+
if (typeof value !== "object" || value == null || Array.isArray(value)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const raw = value;
|
|
382
|
+
const truncation = {};
|
|
383
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
384
|
+
if (exports.VALID_TRUNCATION_KEYS.includes(key) &&
|
|
385
|
+
typeof val === "number" &&
|
|
386
|
+
Number.isInteger(val) &&
|
|
387
|
+
val >= 1 &&
|
|
388
|
+
val <= 999) {
|
|
389
|
+
truncation[key] = val;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (Object.keys(truncation).length > 0) {
|
|
393
|
+
config.truncation = truncation;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function applyEewLogFields(config, value) {
|
|
397
|
+
if (typeof value !== "object" || value == null || Array.isArray(value)) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const raw = value;
|
|
401
|
+
const fields = {};
|
|
402
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
403
|
+
if (exports.VALID_EEW_LOG_FIELDS.includes(key) &&
|
|
404
|
+
typeof val === "boolean") {
|
|
405
|
+
fields[key] = val;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (Object.keys(fields).length > 0) {
|
|
409
|
+
config.eewLogFields = fields;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** カンマ区切りの分類区分文字列をパースする */
|
|
413
|
+
function parseClassifications(input) {
|
|
414
|
+
return input
|
|
415
|
+
.split(",")
|
|
416
|
+
.map((s) => s.trim())
|
|
417
|
+
.filter((s) => exports.VALID_CLASSIFICATIONS.includes(s));
|
|
418
|
+
}
|
|
419
|
+
/** 設定値を1件セットする。無効な入力の場合は ConfigError をスローする。 */
|
|
420
|
+
function setConfigValue(key, value) {
|
|
421
|
+
// truncation.xxx ドットキー対応
|
|
422
|
+
if (key.startsWith("truncation.")) {
|
|
423
|
+
const subKey = key.slice("truncation.".length);
|
|
424
|
+
if (!exports.VALID_TRUNCATION_KEYS.includes(subKey)) {
|
|
425
|
+
throw new ConfigError(`不明な truncation キー: ${subKey}\n有効なキー: ${exports.VALID_TRUNCATION_KEYS.join(", ")}`);
|
|
426
|
+
}
|
|
427
|
+
const num = Number(value);
|
|
428
|
+
if (isNaN(num) || !Number.isInteger(num) || num < 1 || num > 999) {
|
|
429
|
+
throw new ConfigError(`${subKey} は 1〜999 の整数を指定してください。`);
|
|
430
|
+
}
|
|
431
|
+
const config = loadConfig();
|
|
432
|
+
if (config.truncation == null)
|
|
433
|
+
config.truncation = {};
|
|
434
|
+
config.truncation[subKey] = num;
|
|
435
|
+
saveConfig(config);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (!(key in CONFIG_KEYS)) {
|
|
439
|
+
throw new ConfigError(`不明な設定キー: ${key}\n有効なキー: ${Object.keys(CONFIG_KEYS).join(", ")}`);
|
|
440
|
+
}
|
|
441
|
+
const config = loadConfig();
|
|
442
|
+
switch (key) {
|
|
443
|
+
case "apiKey":
|
|
444
|
+
config.apiKey = value;
|
|
445
|
+
break;
|
|
446
|
+
case "classifications": {
|
|
447
|
+
const parsed = parseClassifications(value);
|
|
448
|
+
if (parsed.length === 0) {
|
|
449
|
+
throw new ConfigError(`無効な区分: ${value}\n有効な値: ${exports.VALID_CLASSIFICATIONS.join(", ")}`);
|
|
450
|
+
}
|
|
451
|
+
config.classifications = parsed;
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case "testMode":
|
|
455
|
+
if (!VALID_TEST_MODES.includes(value)) {
|
|
456
|
+
throw new ConfigError(`無効なテストモード: ${value}\n有効な値: ${VALID_TEST_MODES.join(", ")}`);
|
|
457
|
+
}
|
|
458
|
+
config.testMode = value;
|
|
459
|
+
break;
|
|
460
|
+
case "appName":
|
|
461
|
+
config.appName = value;
|
|
462
|
+
break;
|
|
463
|
+
case "maxReconnectDelaySec": {
|
|
464
|
+
const num = Number(value);
|
|
465
|
+
if (isNaN(num) || num <= 0) {
|
|
466
|
+
throw new ConfigError("maxReconnectDelaySec は正の数値を指定してください。");
|
|
467
|
+
}
|
|
468
|
+
config.maxReconnectDelaySec = num;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case "keepExistingConnections":
|
|
472
|
+
if (value !== "true" && value !== "false") {
|
|
473
|
+
throw new ConfigError("keepExistingConnections は true または false を指定してください。");
|
|
474
|
+
}
|
|
475
|
+
config.keepExistingConnections = value === "true";
|
|
476
|
+
break;
|
|
477
|
+
case "tableWidth": {
|
|
478
|
+
if (value === "auto") {
|
|
479
|
+
delete config.tableWidth;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
const tw = Number(value);
|
|
483
|
+
if (isNaN(tw) || !Number.isInteger(tw) || tw < 40 || tw > 200) {
|
|
484
|
+
throw new ConfigError("tableWidth は 40〜200 の整数、または auto を指定してください。");
|
|
485
|
+
}
|
|
486
|
+
config.tableWidth = tw;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case "infoFullText":
|
|
490
|
+
if (value !== "true" && value !== "false") {
|
|
491
|
+
throw new ConfigError("infoFullText は true または false を指定してください。");
|
|
492
|
+
}
|
|
493
|
+
config.infoFullText = value === "true";
|
|
494
|
+
break;
|
|
495
|
+
case "displayMode":
|
|
496
|
+
if (!VALID_DISPLAY_MODES.includes(value)) {
|
|
497
|
+
throw new ConfigError(`無効な表示モード: ${value}\n有効な値: ${VALID_DISPLAY_MODES.join(", ")}`);
|
|
498
|
+
}
|
|
499
|
+
config.displayMode = value;
|
|
500
|
+
break;
|
|
501
|
+
case "promptClock":
|
|
502
|
+
if (!VALID_PROMPT_CLOCKS.includes(value)) {
|
|
503
|
+
throw new ConfigError(`無効なプロンプト時計: ${value}\n有効な値: ${VALID_PROMPT_CLOCKS.join(", ")}`);
|
|
504
|
+
}
|
|
505
|
+
config.promptClock = value;
|
|
506
|
+
break;
|
|
507
|
+
case "waitTipIntervalMin": {
|
|
508
|
+
const min = Number(value);
|
|
509
|
+
if (isNaN(min) || !Number.isInteger(min) || min < 0 || min > 1440) {
|
|
510
|
+
throw new ConfigError("waitTipIntervalMin は 0〜1440 の整数を指定してください。");
|
|
511
|
+
}
|
|
512
|
+
config.waitTipIntervalMin = min;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
case "sound":
|
|
516
|
+
if (value !== "true" && value !== "false") {
|
|
517
|
+
throw new ConfigError("sound は true または false を指定してください。");
|
|
518
|
+
}
|
|
519
|
+
config.sound = value === "true";
|
|
520
|
+
break;
|
|
521
|
+
case "eewLog":
|
|
522
|
+
if (value !== "true" && value !== "false") {
|
|
523
|
+
throw new ConfigError("eewLog は true または false を指定してください。");
|
|
524
|
+
}
|
|
525
|
+
config.eewLog = value === "true";
|
|
526
|
+
break;
|
|
527
|
+
case "backup":
|
|
528
|
+
if (value !== "true" && value !== "false") {
|
|
529
|
+
throw new ConfigError("backup は true または false を指定してください。");
|
|
530
|
+
}
|
|
531
|
+
config.backup = value === "true";
|
|
532
|
+
break;
|
|
533
|
+
case "maxObservations": {
|
|
534
|
+
if (value === "off") {
|
|
535
|
+
delete config.maxObservations;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
const mo = Number(value);
|
|
539
|
+
if (isNaN(mo) || !Number.isInteger(mo) || mo < 1 || mo > 999) {
|
|
540
|
+
throw new ConfigError('maxObservations は 1〜999 の整数、または "off" を指定してください。');
|
|
541
|
+
}
|
|
542
|
+
config.maxObservations = mo;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
saveConfig(config);
|
|
547
|
+
}
|
|
548
|
+
/** 設定値を1件削除する。無効なキーの場合は ConfigError をスローする。 */
|
|
549
|
+
function unsetConfigValue(key) {
|
|
550
|
+
// truncation ドットキー対応
|
|
551
|
+
if (key === "truncation") {
|
|
552
|
+
const config = loadConfig();
|
|
553
|
+
delete config.truncation;
|
|
554
|
+
saveConfig(config);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (key.startsWith("truncation.")) {
|
|
558
|
+
const subKey = key.slice("truncation.".length);
|
|
559
|
+
if (!exports.VALID_TRUNCATION_KEYS.includes(subKey)) {
|
|
560
|
+
throw new ConfigError(`不明な truncation キー: ${subKey}\n有効なキー: ${exports.VALID_TRUNCATION_KEYS.join(", ")}`);
|
|
561
|
+
}
|
|
562
|
+
const config = loadConfig();
|
|
563
|
+
if (config.truncation != null) {
|
|
564
|
+
delete config.truncation[subKey];
|
|
565
|
+
// 空になったらオブジェクトごと削除
|
|
566
|
+
if (Object.keys(config.truncation).length === 0) {
|
|
567
|
+
delete config.truncation;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
saveConfig(config);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (!(key in CONFIG_KEYS)) {
|
|
574
|
+
throw new ConfigError(`不明な設定キー: ${key}\n有効なキー: ${Object.keys(CONFIG_KEYS).join(", ")}`);
|
|
575
|
+
}
|
|
576
|
+
const config = loadConfig();
|
|
577
|
+
delete config[key];
|
|
578
|
+
saveConfig(config);
|
|
579
|
+
}
|
|
580
|
+
/** 現在の設定を整形して表示する */
|
|
581
|
+
function printConfig() {
|
|
582
|
+
const config = loadConfig();
|
|
583
|
+
const configPath = getConfigPath();
|
|
584
|
+
console.log();
|
|
585
|
+
console.log(` Config: ${configPath}`);
|
|
586
|
+
console.log();
|
|
587
|
+
if (Object.keys(config).length === 0) {
|
|
588
|
+
console.log(" (設定なし)");
|
|
589
|
+
console.log();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
for (const [key, description] of Object.entries(CONFIG_KEYS)) {
|
|
593
|
+
if (key === "truncation") {
|
|
594
|
+
const trunc = config.truncation;
|
|
595
|
+
if (trunc != null && Object.keys(trunc).length > 0) {
|
|
596
|
+
console.log(` truncation:`);
|
|
597
|
+
console.log(` # ${description}`);
|
|
598
|
+
for (const [tk, tv] of Object.entries(trunc)) {
|
|
599
|
+
const def = types_1.DEFAULT_CONFIG.truncation[tk];
|
|
600
|
+
const marker = tv !== def ? " *" : "";
|
|
601
|
+
console.log(` ${tk} = ${tv}${marker}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
const val = config[key];
|
|
607
|
+
if (val !== undefined) {
|
|
608
|
+
const displayValue = key === "apiKey" ? secretUtils.maskApiKey(String(val)) : formatValue(val);
|
|
609
|
+
console.log(` ${key} = ${displayValue}`);
|
|
610
|
+
console.log(` # ${description}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
console.log();
|
|
614
|
+
}
|
|
615
|
+
/** 設定可能なキー一覧を表示する */
|
|
616
|
+
function printConfigKeys() {
|
|
617
|
+
console.log();
|
|
618
|
+
console.log(" 設定可能なキー:");
|
|
619
|
+
console.log();
|
|
620
|
+
for (const [key, description] of Object.entries(CONFIG_KEYS)) {
|
|
621
|
+
console.log(` ${key}`);
|
|
622
|
+
console.log(` ${description}`);
|
|
623
|
+
if (key === "truncation") {
|
|
624
|
+
for (const tk of exports.VALID_TRUNCATION_KEYS) {
|
|
625
|
+
const def = types_1.DEFAULT_CONFIG.truncation[tk];
|
|
626
|
+
console.log(` truncation.${tk} (default: ${def})`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
console.log();
|
|
631
|
+
}
|
|
632
|
+
/** 値を表示用にフォーマットする */
|
|
633
|
+
function formatValue(val) {
|
|
634
|
+
if (Array.isArray(val)) {
|
|
635
|
+
return val.join(", ");
|
|
636
|
+
}
|
|
637
|
+
return String(val);
|
|
638
|
+
}
|