@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,338 @@
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.Notifier = exports.NOTIFY_CATEGORY_LABELS = void 0;
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs"));
39
+ const types_1 = require("../../types");
40
+ const config_1 = require("../../config");
41
+ const sound_player_1 = require("./sound-player");
42
+ const nodeNotifierLoader = __importStar(require("./node-notifier-loader"));
43
+ const intensityUtils = __importStar(require("../../utils/intensity"));
44
+ const log = __importStar(require("../../logger"));
45
+ /** 通知アイコンのパス (assets/icons/icon.png が存在する場合に使用) */
46
+ const NOTIFY_ICON_PATH = path.resolve(__dirname, "../../../assets/icons/icon.png");
47
+ /** 通知アプリ名 */
48
+ const NOTIFY_APP_NAME = "FlEq";
49
+ /** 通知カテゴリと日本語ラベルの対応 */
50
+ exports.NOTIFY_CATEGORY_LABELS = {
51
+ eew: "緊急地震速報",
52
+ earthquake: "地震情報",
53
+ tsunami: "津波情報",
54
+ seismicText: "地震活動テキスト",
55
+ nankaiTrough: "南海トラフ関連",
56
+ lgObservation: "長周期地震動",
57
+ volcano: "火山情報",
58
+ };
59
+ class Notifier {
60
+ settings;
61
+ soundEnabled;
62
+ muteUntil = null;
63
+ constructor() {
64
+ const fileConfig = (0, config_1.loadConfig)();
65
+ this.settings = {
66
+ ...types_1.DEFAULT_CONFIG.notify,
67
+ ...fileConfig.notify,
68
+ };
69
+ this.soundEnabled = fileConfig.sound ?? types_1.DEFAULT_CONFIG.sound;
70
+ }
71
+ /** 指定ミリ秒間、通知をミュートする */
72
+ mute(durationMs) {
73
+ this.muteUntil = Date.now() + durationMs;
74
+ }
75
+ /** ミュートを解除する */
76
+ unmute() {
77
+ this.muteUntil = null;
78
+ }
79
+ /** 現在ミュート中かどうか */
80
+ isMuted() {
81
+ if (this.muteUntil == null)
82
+ return false;
83
+ if (Date.now() >= this.muteUntil) {
84
+ this.muteUntil = null;
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+ /** ミュート残り時間 (ms)。ミュート中でなければ 0 */
90
+ muteRemaining() {
91
+ if (this.muteUntil == null)
92
+ return 0;
93
+ const remaining = this.muteUntil - Date.now();
94
+ if (remaining <= 0) {
95
+ this.muteUntil = null;
96
+ return 0;
97
+ }
98
+ return remaining;
99
+ }
100
+ /** カテゴリのトグル → 新しい状態を返す */
101
+ toggleCategory(cat) {
102
+ this.settings[cat] = !this.settings[cat];
103
+ this.persist();
104
+ return this.settings[cat];
105
+ }
106
+ /** 一括 ON/OFF */
107
+ setAll(enabled) {
108
+ for (const key of Object.keys(this.settings)) {
109
+ this.settings[key] = enabled;
110
+ }
111
+ this.persist();
112
+ }
113
+ /** 現在の設定を返す */
114
+ getSettings() {
115
+ return { ...this.settings };
116
+ }
117
+ /** 通知音が有効かどうか */
118
+ getSoundEnabled() {
119
+ return this.soundEnabled;
120
+ }
121
+ /** 通知音の有効/無効を切り替える */
122
+ setSoundEnabled(enabled) {
123
+ this.soundEnabled = enabled;
124
+ this.persist();
125
+ }
126
+ // ── 電文タイプ別通知 ──
127
+ notifyEew(info, result) {
128
+ if (!this.settings.eew)
129
+ return;
130
+ // 通知条件: 第1報 / 予報→警報切替 / 取消報 / 最終報
131
+ const isUpgradeToWarning = result.previousInfo?.isWarning === false && info.isWarning === true;
132
+ const isFinal = info.nextAdvisory != null;
133
+ if (!result.isNew && !isUpgradeToWarning && !result.isCancelled && !isFinal) {
134
+ return;
135
+ }
136
+ if (result.isCancelled) {
137
+ this.send("[取消] 緊急地震速報", "緊急地震速報は取り消されました", "cancel");
138
+ return;
139
+ }
140
+ const soundLevel = info.isWarning ? "critical" : "warning";
141
+ const title = info.isWarning
142
+ ? "緊急地震速報(警報)"
143
+ : "緊急地震速報(予報)";
144
+ const maxInt = info.forecastIntensity?.areas?.[0]
145
+ ? this.findMaxForecastInt(info)
146
+ : "不明";
147
+ const body = info.earthquake
148
+ ? `${info.earthquake.hypocenterName} / M${info.earthquake.magnitude} / 最大予測震度${maxInt}`
149
+ : title;
150
+ this.send(title, body, soundLevel);
151
+ }
152
+ notifyEarthquake(info) {
153
+ if (!this.settings.earthquake)
154
+ return;
155
+ if (info.infoType === "取消") {
156
+ this.send(`[取消] ${info.title}`, "この情報は取り消されました", "cancel");
157
+ return;
158
+ }
159
+ const soundLevel = this.earthquakeSoundLevel(info);
160
+ const parts = [];
161
+ if (info.earthquake) {
162
+ parts.push(info.earthquake.hypocenterName);
163
+ parts.push(`M${info.earthquake.magnitude}`);
164
+ }
165
+ if (info.intensity) {
166
+ parts.push(`最大震度${info.intensity.maxInt}`);
167
+ }
168
+ this.send(info.title, parts.length > 0 ? parts.join(" / ") : (info.headline ?? info.title), soundLevel);
169
+ }
170
+ notifyTsunami(info) {
171
+ if (!this.settings.tsunami)
172
+ return;
173
+ if (info.infoType === "取消") {
174
+ this.send(`[取消] ${info.title}`, "この情報は取り消されました", "cancel");
175
+ return;
176
+ }
177
+ const soundLevel = this.tsunamiSoundLevel(info);
178
+ const parts = [];
179
+ if (info.forecast && info.forecast.length > 0) {
180
+ const kinds = [...new Set(info.forecast.map((f) => f.kind))];
181
+ parts.push(kinds.join("・"));
182
+ const areas = info.forecast.slice(0, 3).map((f) => f.areaName);
183
+ parts.push(areas.join(", "));
184
+ }
185
+ if (info.headline) {
186
+ parts.push(info.headline);
187
+ }
188
+ this.send(info.title, parts.length > 0 ? parts.join(" / ") : info.title, soundLevel);
189
+ }
190
+ notifySeismicText(info) {
191
+ if (!this.settings.seismicText)
192
+ return;
193
+ if (info.infoType === "取消") {
194
+ this.send(`[取消] ${info.title}`, "この情報は取り消されました", "cancel");
195
+ return;
196
+ }
197
+ const body = info.headline ?? info.bodyText.slice(0, 80);
198
+ this.send(info.title, body, "info");
199
+ }
200
+ notifyNankaiTrough(info) {
201
+ if (!this.settings.nankaiTrough)
202
+ return;
203
+ if (info.infoType === "取消") {
204
+ this.send(`[取消] ${info.title}`, "この情報は取り消されました", "cancel");
205
+ return;
206
+ }
207
+ const body = info.headline ?? info.bodyText.slice(0, 80);
208
+ this.send(info.title, body, "warning");
209
+ }
210
+ notifyLgObservation(info) {
211
+ if (!this.settings.lgObservation)
212
+ return;
213
+ if (info.infoType === "取消") {
214
+ this.send(`[取消] ${info.title}`, "この情報は取り消されました", "cancel");
215
+ return;
216
+ }
217
+ const soundLevel = this.lgObservationSoundLevel(info);
218
+ const parts = [];
219
+ if (info.earthquake) {
220
+ parts.push(info.earthquake.hypocenterName);
221
+ }
222
+ if (info.maxLgInt) {
223
+ parts.push(`長周期階級${info.maxLgInt}`);
224
+ }
225
+ if (info.maxInt) {
226
+ parts.push(`最大震度${info.maxInt}`);
227
+ }
228
+ this.send(info.title, parts.length > 0 ? parts.join(" / ") : info.title, soundLevel);
229
+ }
230
+ notifyVolcano(info, presentation) {
231
+ if (!this.settings.volcano)
232
+ return;
233
+ if (info.infoType === "取消") {
234
+ this.send(`[取消] ${info.title}`, "この情報は取り消されました", "cancel");
235
+ return;
236
+ }
237
+ this.send(info.title, presentation.summary, presentation.soundLevel);
238
+ }
239
+ notifyVolcanoBatch(batch, presentation) {
240
+ if (!this.settings.volcano)
241
+ return;
242
+ this.send("降灰予報(定時)", presentation.summary, presentation.soundLevel);
243
+ }
244
+ // ── 内部メソッド ──
245
+ _notifier = null;
246
+ getNotifier() {
247
+ if (this._notifier == null) {
248
+ this._notifier = nodeNotifierLoader.loadNodeNotifier();
249
+ if (this._notifier == null) {
250
+ log.debug("node-notifier の読み込みに失敗しました");
251
+ }
252
+ }
253
+ return this._notifier;
254
+ }
255
+ send(title, message, level) {
256
+ if (this.isMuted())
257
+ return;
258
+ try {
259
+ const nn = this.getNotifier();
260
+ if (nn) {
261
+ nn.notify({
262
+ title,
263
+ message,
264
+ sound: false,
265
+ appID: NOTIFY_APP_NAME,
266
+ ...(fs.existsSync(NOTIFY_ICON_PATH) ? { icon: NOTIFY_ICON_PATH } : {}),
267
+ });
268
+ }
269
+ }
270
+ catch (err) {
271
+ if (err instanceof Error) {
272
+ log.debug(`通知送信エラー: ${err.message}`);
273
+ }
274
+ }
275
+ if (this.soundEnabled && level) {
276
+ (0, sound_player_1.playSound)(level);
277
+ }
278
+ }
279
+ earthquakeSoundLevel(info) {
280
+ if (!info.intensity)
281
+ return "normal";
282
+ if (intensityUtils.intensityToRank(info.intensity.maxInt) >= 4)
283
+ return "warning";
284
+ return "normal";
285
+ }
286
+ /** 津波情報のサウンドレベルを判定 */
287
+ tsunamiSoundLevel(info) {
288
+ if (!info.forecast || info.forecast.length === 0)
289
+ return "normal";
290
+ const kinds = info.forecast.map((f) => f.kind);
291
+ // 注意報・警報・大津波警報のいずれかが含まれていれば critical
292
+ if (kinds.some((k) => k.includes("津波") && !k.includes("解除")))
293
+ return "critical";
294
+ // 解除のみの場合は warning
295
+ if (kinds.some((k) => k.includes("解除")))
296
+ return "warning";
297
+ return "normal";
298
+ }
299
+ /** 長周期地震動観測のサウンドレベルを判定 */
300
+ lgObservationSoundLevel(info) {
301
+ if (!info.maxLgInt)
302
+ return "normal";
303
+ if (info.maxLgInt === "4" || info.maxLgInt === "3")
304
+ return "critical";
305
+ if (info.maxLgInt === "2" || info.maxLgInt === "1")
306
+ return "warning";
307
+ return "normal";
308
+ }
309
+ persist() {
310
+ try {
311
+ const config = (0, config_1.loadConfig)();
312
+ config.notify = { ...this.settings };
313
+ config.sound = this.soundEnabled;
314
+ (0, config_1.saveConfig)(config);
315
+ }
316
+ catch (err) {
317
+ if (err instanceof Error) {
318
+ log.warn(`通知設定の保存に失敗しました: ${err.message}`);
319
+ }
320
+ }
321
+ }
322
+ findMaxForecastInt(info) {
323
+ if (!info.forecastIntensity?.areas || info.forecastIntensity.areas.length === 0) {
324
+ return "不明";
325
+ }
326
+ let maxLabel = info.forecastIntensity.areas[0].intensity;
327
+ let maxRank = intensityUtils.intensityToRank(maxLabel);
328
+ for (const area of info.forecastIntensity.areas) {
329
+ const rank = intensityUtils.intensityToRank(area.intensity);
330
+ if (rank > maxRank) {
331
+ maxRank = rank;
332
+ maxLabel = area.intensity;
333
+ }
334
+ }
335
+ return maxLabel;
336
+ }
337
+ }
338
+ exports.Notifier = Notifier;
@@ -0,0 +1,230 @@
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.SOUND_LEVELS = void 0;
37
+ exports.isSoundLevel = isSoundLevel;
38
+ exports.playSound = playSound;
39
+ const child_process_1 = require("child_process");
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const log = __importStar(require("../../logger"));
43
+ /** 通知音レベル一覧 (型導出の信頼できる唯一のソース) */
44
+ exports.SOUND_LEVELS = ["critical", "warning", "normal", "info", "cancel"];
45
+ /** 文字列が有効な SoundLevel かを判定する型ガード */
46
+ function isSoundLevel(value) {
47
+ return exports.SOUND_LEVELS.includes(value);
48
+ }
49
+ /** カスタム効果音ディレクトリ (プロジェクトルート/assets/sounds/) */
50
+ const CUSTOM_SOUNDS_DIR = path.resolve(__dirname, "..", "..", "..", "assets", "sounds");
51
+ /** カスタム効果音のファイル名 (拡張子なし — mp3 → wav の順で探索) */
52
+ const CUSTOM_SOUND_FILES = {
53
+ critical: "critical",
54
+ warning: "warning",
55
+ normal: "normal",
56
+ info: "info",
57
+ cancel: "cancel",
58
+ };
59
+ /** サポートする拡張子 (優先順) */
60
+ const SUPPORTED_EXTENSIONS = [".mp3", ".wav"];
61
+ /** Windows システムサウンドフォールバック */
62
+ const WINDOWS_SOUNDS = {
63
+ critical: "Windows Critical Stop.wav",
64
+ warning: "Windows Exclamation.wav",
65
+ normal: "Windows Notify Calendar.wav",
66
+ info: "Windows Notify Email.wav",
67
+ cancel: "Windows Recycle.wav",
68
+ };
69
+ /** macOS システムサウンドフォールバック */
70
+ const MACOS_SOUNDS = {
71
+ critical: "Sosumi.aiff",
72
+ warning: "Basso.aiff",
73
+ normal: "Glass.aiff",
74
+ info: "Tink.aiff",
75
+ cancel: "Pop.aiff",
76
+ };
77
+ /** Linux canberra イベント名フォールバック */
78
+ const LINUX_CANBERRA_EVENTS = {
79
+ critical: "dialog-error",
80
+ warning: "dialog-warning",
81
+ normal: "message-new-instant",
82
+ info: "dialog-information",
83
+ cancel: "bell",
84
+ };
85
+ /**
86
+ * カスタム効果音ファイルのパスを返す。見つからなければ null。
87
+ * mp3 → wav の順で探索する。
88
+ */
89
+ function findCustomSound(level) {
90
+ const baseName = CUSTOM_SOUND_FILES[level];
91
+ for (const ext of SUPPORTED_EXTENSIONS) {
92
+ const filePath = path.join(CUSTOM_SOUNDS_DIR, baseName + ext);
93
+ if (fs.existsSync(filePath)) {
94
+ return filePath;
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ /**
100
+ * 通知音を再生する (fire-and-forget)。
101
+ * カスタム効果音ファイルがあればそちらを優先、なければ OS システムサウンドにフォールバック。
102
+ * 再生失敗はログに記録するのみで例外は投げない。
103
+ */
104
+ function playSound(level) {
105
+ try {
106
+ const customPath = findCustomSound(level);
107
+ const platform = process.platform;
108
+ if (customPath) {
109
+ playCustomSound(customPath, platform);
110
+ }
111
+ else if (platform === "win32") {
112
+ playSystemSoundWindows(level);
113
+ }
114
+ else if (platform === "darwin") {
115
+ playSystemSoundMacOS(level);
116
+ }
117
+ else {
118
+ playSystemSoundLinux(level);
119
+ }
120
+ }
121
+ catch (err) {
122
+ if (err instanceof Error) {
123
+ log.debug(`通知音の再生に失敗しました: ${err.message}`);
124
+ }
125
+ }
126
+ }
127
+ // ── カスタム効果音再生 ──
128
+ function playCustomSound(filePath, platform) {
129
+ if (platform === "win32") {
130
+ playCustomSoundWindows(filePath);
131
+ }
132
+ else if (platform === "darwin") {
133
+ playCustomSoundMacOS(filePath);
134
+ }
135
+ else {
136
+ playCustomSoundLinux(filePath);
137
+ }
138
+ }
139
+ /** Windows: winmm.dll mciSendString で mp3/wav を同期再生 */
140
+ function playCustomSoundWindows(filePath) {
141
+ // MediaPlayer (WPF) は非同期ロードのためタイミング問題が起きやすい。
142
+ // mciSendString は同期 (wait) で確実に再生できる。
143
+ const escaped = filePath.replace(/'/g, "''");
144
+ const psCommand = [
145
+ `Add-Type -Namespace Win32 -Name Mci -MemberDefinition '[DllImport("winmm.dll",CharSet=[System.Runtime.InteropServices.CharSet]::Unicode)]public static extern int mciSendStringW(string cmd,System.Text.StringBuilder ret,int retLen,System.IntPtr hwnd);';`,
146
+ `[Win32.Mci]::mciSendStringW('open "' + '${escaped}' + '" type mpegvideo alias fleqsnd',$null,0,[IntPtr]::Zero)|Out-Null;`,
147
+ `[Win32.Mci]::mciSendStringW('play fleqsnd wait',$null,0,[IntPtr]::Zero)|Out-Null;`,
148
+ `[Win32.Mci]::mciSendStringW('close fleqsnd',$null,0,[IntPtr]::Zero)|Out-Null`,
149
+ ].join(" ");
150
+ (0, child_process_1.exec)(`powershell -NoProfile -Command "${psCommand}"`, (err) => {
151
+ if (err) {
152
+ log.debug(`Windows カスタム通知音の再生に失敗しました: ${err.message}`);
153
+ }
154
+ });
155
+ }
156
+ /** macOS: afplay で mp3/wav を再生 */
157
+ function playCustomSoundMacOS(filePath) {
158
+ (0, child_process_1.execFile)("afplay", [filePath], (err) => {
159
+ if (err) {
160
+ log.debug(`macOS カスタム通知音の再生に失敗しました: ${err.message}`);
161
+ }
162
+ });
163
+ }
164
+ /** Linux: ffplay → paplay → aplay のフォールバック */
165
+ function playCustomSoundLinux(filePath) {
166
+ const ext = path.extname(filePath).toLowerCase();
167
+ if (ext === ".mp3") {
168
+ // mp3 は ffplay で再生
169
+ (0, child_process_1.execFile)("ffplay", ["-nodisp", "-autoexit", "-loglevel", "quiet", filePath], (err) => {
170
+ if (err) {
171
+ log.debug(`Linux ffplay での再生に失敗しました: ${err.message}`);
172
+ printBell();
173
+ }
174
+ });
175
+ }
176
+ else {
177
+ // wav は paplay → aplay のフォールバック
178
+ (0, child_process_1.execFile)("paplay", [filePath], (err) => {
179
+ if (err) {
180
+ (0, child_process_1.execFile)("aplay", ["-q", filePath], (err2) => {
181
+ if (err2) {
182
+ log.debug(`Linux 通知音の再生に失敗しました: ${err2.message}`);
183
+ printBell();
184
+ }
185
+ });
186
+ }
187
+ });
188
+ }
189
+ }
190
+ // ── システムサウンドフォールバック ──
191
+ function playSystemSoundWindows(level) {
192
+ const soundFile = WINDOWS_SOUNDS[level];
193
+ const soundPath = path.join(process.env.SYSTEMROOT || "C:\\Windows", "Media", soundFile);
194
+ const psCommand = `(New-Object System.Media.SoundPlayer '${soundPath}').PlaySync()`;
195
+ (0, child_process_1.exec)(`powershell -NoProfile -Command "${psCommand}"`, (err) => {
196
+ if (err) {
197
+ log.debug(`Windows 通知音の再生に失敗しました: ${err.message}`);
198
+ }
199
+ });
200
+ }
201
+ function playSystemSoundMacOS(level) {
202
+ const soundFile = MACOS_SOUNDS[level];
203
+ const soundPath = `/System/Library/Sounds/${soundFile}`;
204
+ (0, child_process_1.execFile)("afplay", [soundPath], (err) => {
205
+ if (err) {
206
+ log.debug(`macOS 通知音の再生に失敗しました: ${err.message}`);
207
+ }
208
+ });
209
+ }
210
+ function playSystemSoundLinux(level) {
211
+ const eventName = LINUX_CANBERRA_EVENTS[level];
212
+ if (eventName === "bell") {
213
+ printBell();
214
+ return;
215
+ }
216
+ (0, child_process_1.execFile)("canberra-gtk-play", ["-i", eventName], (err) => {
217
+ if (err) {
218
+ log.debug(`canberra-gtk-play 失敗、bell にフォールバック: ${err.message}`);
219
+ printBell();
220
+ }
221
+ });
222
+ }
223
+ function printBell() {
224
+ try {
225
+ process.stdout.write("\x07");
226
+ }
227
+ catch {
228
+ // ignore
229
+ }
230
+ }