@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,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveVolcanoPresentation = resolveVolcanoPresentation;
4
+ exports.resolveVolcanoBatchPresentation = resolveVolcanoBatchPresentation;
5
+ /** 火山電文の表示レベル・通知レベル・通知要約を判定する */
6
+ function resolveVolcanoPresentation(info, volcanoState) {
7
+ // 全種別共通: 取消
8
+ if (info.infoType === "取消") {
9
+ return {
10
+ frameLevel: "cancel",
11
+ soundLevel: "cancel",
12
+ summary: "この情報は取り消されました",
13
+ };
14
+ }
15
+ switch (info.kind) {
16
+ case "eruption":
17
+ return resolveEruption(info);
18
+ case "alert":
19
+ return resolveAlert(info, volcanoState);
20
+ case "ashfall":
21
+ return resolveAshfall(info);
22
+ case "text":
23
+ return resolveText(info);
24
+ case "plume":
25
+ return resolvePlume(info);
26
+ }
27
+ }
28
+ // ── 個別判定 ──
29
+ function resolveEruption(info) {
30
+ const summary = buildEruptionSummary(info);
31
+ // VFVO56: 噴火速報 → critical
32
+ if (info.isFlashReport) {
33
+ return { frameLevel: "critical", soundLevel: "critical", summary };
34
+ }
35
+ // VFVO52: 爆発(51) / 噴火多発(56) or 噴煙 ≥ 3000m → warning
36
+ if (info.phenomenonCode === "51" ||
37
+ info.phenomenonCode === "56" ||
38
+ (info.plumeHeight != null && info.plumeHeight >= 3000)) {
39
+ return { frameLevel: "warning", soundLevel: "normal", summary };
40
+ }
41
+ // 噴火(52) / 噴火したもよう(62) 軽微
42
+ return { frameLevel: "normal", soundLevel: "info", summary };
43
+ }
44
+ function resolveAlert(info, volcanoState) {
45
+ const summary = buildAlertSummary(info);
46
+ // 海上警報 (VFSVii)
47
+ if (info.isMarine) {
48
+ // Code 31 = 海上警報
49
+ if (info.alertLevelCode === "31" || info.alertLevelCode === "36") {
50
+ return { frameLevel: "warning", soundLevel: "warning", summary };
51
+ }
52
+ // Code 33 = 海上予報
53
+ return { frameLevel: "normal", soundLevel: "normal", summary };
54
+ }
55
+ // 引下げ・解除
56
+ if (info.action === "lower" || info.action === "release") {
57
+ return { frameLevel: "normal", soundLevel: "normal", summary };
58
+ }
59
+ const level = info.alertLevel ?? 0;
60
+ const isRenotification = volcanoState.isRenotification(info);
61
+ // 引上げ
62
+ if (info.action === "raise" || info.action === "issue") {
63
+ if (level >= 4) {
64
+ return { frameLevel: "critical", soundLevel: "critical", summary };
65
+ }
66
+ if (level >= 2) {
67
+ return { frameLevel: "warning", soundLevel: "warning", summary };
68
+ }
69
+ return { frameLevel: "normal", soundLevel: "normal", summary };
70
+ }
71
+ // 継続
72
+ if (level >= 4) {
73
+ return {
74
+ frameLevel: isRenotification ? "warning" : "critical",
75
+ soundLevel: "normal",
76
+ summary,
77
+ };
78
+ }
79
+ if (level >= 2) {
80
+ return {
81
+ frameLevel: isRenotification ? "normal" : "warning",
82
+ soundLevel: isRenotification ? "info" : "normal",
83
+ summary,
84
+ };
85
+ }
86
+ // レベル1継続
87
+ return { frameLevel: "normal", soundLevel: "info", summary };
88
+ }
89
+ function resolveAshfall(info) {
90
+ const summary = buildAshfallSummary(info);
91
+ switch (info.type) {
92
+ case "VFVO54": // 降灰速報
93
+ return { frameLevel: "warning", soundLevel: "warning", summary };
94
+ case "VFVO55": // 降灰詳細
95
+ return { frameLevel: "normal", soundLevel: "normal", summary };
96
+ case "VFVO53": // 降灰定時
97
+ return { frameLevel: "info", soundLevel: "info", summary };
98
+ }
99
+ }
100
+ function resolveText(info) {
101
+ if (info.kind !== "text") {
102
+ return { frameLevel: "info", soundLevel: "info", summary: info.title };
103
+ }
104
+ const summary = info.headline ?? info.title;
105
+ // VFVO51 臨時
106
+ if (info.isExtraordinary) {
107
+ return { frameLevel: "warning", soundLevel: "normal", summary };
108
+ }
109
+ // 通常
110
+ return { frameLevel: "info", soundLevel: "info", summary };
111
+ }
112
+ function resolvePlume(info) {
113
+ const summary = `${info.volcanoName} 推定噴煙流向報`;
114
+ return { frameLevel: "normal", soundLevel: "info", summary };
115
+ }
116
+ // ── 要約テキスト生成 ──
117
+ function buildAlertSummary(info) {
118
+ const parts = [info.volcanoName];
119
+ if (info.alertLevel != null) {
120
+ parts.push(`Lv${info.alertLevel}`);
121
+ }
122
+ parts.push(info.warningKind);
123
+ return parts.join(" / ");
124
+ }
125
+ function buildEruptionSummary(info) {
126
+ const parts = [info.volcanoName, info.phenomenonName];
127
+ if (info.plumeHeight != null) {
128
+ parts.push(`噴煙${info.plumeHeight}m`);
129
+ }
130
+ else if (info.plumeHeightUnknown) {
131
+ parts.push("噴煙高度不明");
132
+ }
133
+ return parts.join(" / ");
134
+ }
135
+ // ── バッチ presentation ──
136
+ /** VFVO53 バッチの表示レベル・通知レベル・通知要約を判定する */
137
+ function resolveVolcanoBatchPresentation(batch) {
138
+ return {
139
+ frameLevel: "info",
140
+ soundLevel: "info",
141
+ summary: buildBatchSummary(batch),
142
+ };
143
+ }
144
+ function buildBatchSummary(batch) {
145
+ const count = batch.items.length;
146
+ const MAX_NAMES = 3;
147
+ const names = batch.items.slice(0, MAX_NAMES).map((i) => i.volcanoName);
148
+ const rest = count - MAX_NAMES;
149
+ const nameList = rest > 0 ? `${names.join("、")} +${rest}` : names.join("、");
150
+ return `${count}火山: ${nameList}`;
151
+ }
152
+ function buildAshfallSummary(info) {
153
+ const parts = [info.volcanoName];
154
+ const subKindLabel = {
155
+ scheduled: "定時",
156
+ rapid: "速報",
157
+ detailed: "詳細",
158
+ }[info.subKind];
159
+ parts.push(`降灰予報(${subKindLabel})`);
160
+ // 最初の時間帯の最も深刻なエリアを追加
161
+ if (info.ashForecasts.length > 0 && info.ashForecasts[0].areas.length > 0) {
162
+ const topArea = info.ashForecasts[0].areas[0];
163
+ parts.push(topArea.ashName);
164
+ }
165
+ return parts.join(" / ");
166
+ }
@@ -0,0 +1,139 @@
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.resolveConfig = resolveConfig;
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const types_1 = require("../../types");
42
+ const config_1 = require("../../config");
43
+ const log = __importStar(require("../../logger"));
44
+ /**
45
+ * CLI引数 → 環境変数 → .env → Configファイル → デフォルト値 の優先順位で設定を解決する。
46
+ * 致命的なバリデーションエラー時は process.exit(1) する。
47
+ */
48
+ function resolveConfig(opts) {
49
+ // Configファイル読み込み
50
+ const fileConfig = (0, config_1.loadConfig)();
51
+ log.debug(`Config: ${(0, config_1.getConfigPath)()}`);
52
+ // APIキー (CLI > 環境変数 > Configファイル)
53
+ const apiKey = opts.apiKey || process.env.DMDATA_API_KEY || fileConfig.apiKey;
54
+ if (!apiKey) {
55
+ log.error("APIキーが指定されていません。");
56
+ console.log();
57
+ console.log(chalk_1.default.white(" 以下のいずれかの方法で設定してください:"));
58
+ console.log(chalk_1.default.gray(" 1. ") + chalk_1.default.white("fleq init") + chalk_1.default.gray(" — インタラクティブセットアップ"));
59
+ console.log(chalk_1.default.gray(" 2. ") + chalk_1.default.white("fleq config set apiKey <key>") + chalk_1.default.gray(" — 直接設定"));
60
+ console.log(chalk_1.default.gray(" 3. ") + chalk_1.default.white("fleq --api-key <key>") + chalk_1.default.gray(" — CLI引数で指定"));
61
+ console.log(chalk_1.default.gray(" 4. ") + chalk_1.default.white("DMDATA_API_KEY=<key>") + chalk_1.default.gray(" — 環境変数で指定"));
62
+ console.log();
63
+ process.exit(1);
64
+ }
65
+ // 分類区分の解析 (CLI > Configファイル > デフォルト)
66
+ let classifications;
67
+ if (opts.classifications != null) {
68
+ const allTokens = opts.classifications.split(",").map((s) => s.trim());
69
+ const valid = [];
70
+ const invalid = [];
71
+ for (const token of allTokens) {
72
+ if (config_1.VALID_CLASSIFICATIONS.includes(token)) {
73
+ valid.push(token);
74
+ }
75
+ else if (token.length > 0) {
76
+ invalid.push(token);
77
+ }
78
+ }
79
+ if (invalid.length > 0) {
80
+ log.warn(`無効な区分を無視しました: ${invalid.join(", ")}`);
81
+ log.warn(`有効な値: ${config_1.VALID_CLASSIFICATIONS.join(", ")}`);
82
+ }
83
+ classifications = valid;
84
+ }
85
+ else if (fileConfig.classifications != null) {
86
+ classifications = fileConfig.classifications;
87
+ // v1.48 で追加された telegram.volcano が含まれていない場合、案内を表示
88
+ if (!classifications.includes("telegram.volcano")) {
89
+ log.info("火山情報の受信には telegram.volcano の追加が必要です: " +
90
+ chalk_1.default.white("fleq config set classifications " + [...classifications, "telegram.volcano"].join(",")));
91
+ }
92
+ }
93
+ else {
94
+ classifications = types_1.DEFAULT_CONFIG.classifications;
95
+ }
96
+ if (classifications.length === 0) {
97
+ log.error(`有効な区分が指定されていません。`);
98
+ log.error(`有効な値: ${config_1.VALID_CLASSIFICATIONS.join(", ")}`);
99
+ process.exit(1);
100
+ }
101
+ // テストモード (CLI > Configファイル > デフォルト)
102
+ const testMode = opts.test != null
103
+ ? opts.test
104
+ : fileConfig.testMode ?? types_1.DEFAULT_CONFIG.testMode;
105
+ if (!["no", "including", "only"].includes(testMode)) {
106
+ log.error(`無効なテストモード: ${testMode} (有効な値: no, including, only)`);
107
+ process.exit(1);
108
+ }
109
+ // 表示モード (CLI > Configファイル > デフォルト)
110
+ const displayModeRaw = opts.mode ?? fileConfig.displayMode ?? types_1.DEFAULT_CONFIG.displayMode;
111
+ if (displayModeRaw !== "normal" && displayModeRaw !== "compact") {
112
+ log.error(`無効な表示モード: ${displayModeRaw} (有効な値: normal, compact)`);
113
+ process.exit(1);
114
+ }
115
+ return {
116
+ apiKey,
117
+ classifications,
118
+ testMode,
119
+ appName: fileConfig.appName ?? types_1.DEFAULT_CONFIG.appName,
120
+ maxReconnectDelaySec: fileConfig.maxReconnectDelaySec ?? types_1.DEFAULT_CONFIG.maxReconnectDelaySec,
121
+ keepExistingConnections: opts.closeOthers === true
122
+ ? false
123
+ : (opts.keepExisting ??
124
+ fileConfig.keepExistingConnections ??
125
+ types_1.DEFAULT_CONFIG.keepExistingConnections),
126
+ tableWidth: fileConfig.tableWidth ?? null,
127
+ infoFullText: fileConfig.infoFullText ?? types_1.DEFAULT_CONFIG.infoFullText,
128
+ displayMode: displayModeRaw,
129
+ promptClock: fileConfig.promptClock ?? types_1.DEFAULT_CONFIG.promptClock,
130
+ waitTipIntervalMin: fileConfig.waitTipIntervalMin ?? types_1.DEFAULT_CONFIG.waitTipIntervalMin,
131
+ notify: { ...types_1.DEFAULT_CONFIG.notify, ...fileConfig.notify },
132
+ sound: fileConfig.sound ?? types_1.DEFAULT_CONFIG.sound,
133
+ eewLog: fileConfig.eewLog ?? types_1.DEFAULT_CONFIG.eewLog,
134
+ eewLogFields: { ...types_1.DEFAULT_CONFIG.eewLogFields, ...fileConfig.eewLogFields },
135
+ maxObservations: fileConfig.maxObservations ?? types_1.DEFAULT_CONFIG.maxObservations,
136
+ backup: fileConfig.backup ?? types_1.DEFAULT_CONFIG.backup,
137
+ truncation: { ...types_1.DEFAULT_CONFIG.truncation, ...fileConfig.truncation },
138
+ };
139
+ }
@@ -0,0 +1,91 @@
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.restoreTsunamiState = restoreTsunamiState;
37
+ const rest_client_1 = require("../../dmdata/rest-client");
38
+ const telegram_parser_1 = require("../../dmdata/telegram-parser");
39
+ const log = __importStar(require("../../logger"));
40
+ /** TelegramListItem を WsDataMessage 互換の形に変換する (body は呼び出し側で確認済み前提) */
41
+ function toWsDataMessage(item, body) {
42
+ return {
43
+ type: "data",
44
+ version: "2.0",
45
+ classification: item.classification,
46
+ id: item.id,
47
+ passing: [],
48
+ head: item.head,
49
+ xmlReport: item.xmlReport,
50
+ format: item.format,
51
+ compression: item.compression,
52
+ encoding: item.encoding,
53
+ body,
54
+ };
55
+ }
56
+ /**
57
+ * 起動時に最新の VTSE41 電文を取得し、津波警報状態を復元する。
58
+ * エラー時は警告ログのみ出力し、アプリの起動を妨げない。
59
+ */
60
+ async function restoreTsunamiState(apiKey, tsunamiState) {
61
+ try {
62
+ const res = await (0, rest_client_1.listTelegrams)(apiKey, "VTSE41", 1);
63
+ if (res.items.length === 0) {
64
+ log.debug("VTSE41 電文なし: 津波状態の復元をスキップ");
65
+ return null;
66
+ }
67
+ const item = res.items[0];
68
+ if (!item.body) {
69
+ log.debug("VTSE41 電文に body が含まれていません: 津波状態の復元をスキップ");
70
+ return null;
71
+ }
72
+ const msg = toWsDataMessage(item, item.body);
73
+ const info = (0, telegram_parser_1.parseTsunamiTelegram)(msg);
74
+ if (info == null) {
75
+ log.debug("VTSE41 電文のパースに失敗: 津波状態の復元をスキップ");
76
+ return null;
77
+ }
78
+ tsunamiState.update(info);
79
+ // 状態が実際にセットされた場合のみログ出力
80
+ if (tsunamiState.getLevel() != null) {
81
+ log.info(`津波警報状態を復元しました: ${tsunamiState.getLevel()}`);
82
+ return info;
83
+ }
84
+ log.debug("最新の VTSE41 は警報なし (取消または津波予報のみ)");
85
+ return null;
86
+ }
87
+ catch (err) {
88
+ log.warn(`津波状態の復元に失敗しました: ${err instanceof Error ? err.message : err}`);
89
+ return null;
90
+ }
91
+ }
@@ -0,0 +1,229 @@
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.isUpdateCheckDisabled = isUpdateCheckDisabled;
40
+ exports.isNewerVersion = isNewerVersion;
41
+ exports.checkForUpdates = checkForUpdates;
42
+ const https = __importStar(require("https"));
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const chalk_1 = __importDefault(require("chalk"));
46
+ const log = __importStar(require("../../logger"));
47
+ const config_1 = require("../../config");
48
+ /** チェック結果のキャッシュファイルパス */
49
+ const CACHE_DIR = (0, config_1.getConfigDir)();
50
+ const CACHE_PATH = path.join(CACHE_DIR, ".update-check");
51
+ /** チェック間隔: 24時間 */
52
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
53
+ /** 各ソースへのリクエストタイムアウト: 3秒 */
54
+ const REQUEST_TIMEOUT_MS = 3000;
55
+ /** GitHub リポジトリ (owner/repo) */
56
+ const GITHUB_REPO = "Lateo2580/FlEq";
57
+ /** 環境変数で更新確認を無効化しているか */
58
+ function isUpdateCheckDisabled(env = process.env) {
59
+ const raw = env.FLEQ_NO_UPDATE_CHECK;
60
+ if (raw == null)
61
+ return false;
62
+ return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
63
+ }
64
+ /** キャッシュを読み込む。無効ならnullを返す */
65
+ function readCache() {
66
+ try {
67
+ if (!fs.existsSync(CACHE_PATH))
68
+ return null;
69
+ const raw = fs.readFileSync(CACHE_PATH, "utf-8");
70
+ const parsed = JSON.parse(raw);
71
+ if (typeof parsed === "object" &&
72
+ parsed != null &&
73
+ "lastCheck" in parsed &&
74
+ "latestVersion" in parsed &&
75
+ typeof parsed.lastCheck === "number" &&
76
+ typeof parsed.latestVersion === "string") {
77
+ return parsed;
78
+ }
79
+ return null;
80
+ }
81
+ catch (err) {
82
+ log.debug(`update check cache read failed: ${err instanceof Error ? err.message : String(err)}`);
83
+ return null;
84
+ }
85
+ }
86
+ /** キャッシュを書き込む */
87
+ function writeCache(cache) {
88
+ try {
89
+ if (!fs.existsSync(CACHE_DIR)) {
90
+ fs.mkdirSync(CACHE_DIR, { recursive: true, mode: 0o700 });
91
+ }
92
+ fs.writeFileSync(CACHE_PATH, JSON.stringify(cache), { encoding: "utf-8" });
93
+ }
94
+ catch (err) {
95
+ log.debug(`update check cache write failed: ${err instanceof Error ? err.message : String(err)}`);
96
+ }
97
+ }
98
+ /** 指定 URL から JSON を取得し、バージョン文字列を抽出する */
99
+ function fetchJson(url, extractVersion) {
100
+ return new Promise((resolve, reject) => {
101
+ const req = https.get(url, { timeout: REQUEST_TIMEOUT_MS, headers: { "User-Agent": "fleq-update-checker" } }, (res) => {
102
+ if (res.statusCode !== 200) {
103
+ res.resume();
104
+ reject(new Error(`HTTP ${res.statusCode}`));
105
+ return;
106
+ }
107
+ let data = "";
108
+ res.setEncoding("utf-8");
109
+ res.on("data", (chunk) => {
110
+ data += chunk;
111
+ });
112
+ res.on("end", () => {
113
+ try {
114
+ const parsed = JSON.parse(data);
115
+ const version = extractVersion(parsed);
116
+ if (version != null) {
117
+ resolve(version);
118
+ }
119
+ else {
120
+ reject(new Error("Unexpected response format"));
121
+ }
122
+ }
123
+ catch {
124
+ reject(new Error("JSON parse error"));
125
+ }
126
+ });
127
+ });
128
+ req.on("error", reject);
129
+ req.on("timeout", () => {
130
+ req.destroy();
131
+ reject(new Error("Request timeout"));
132
+ });
133
+ });
134
+ }
135
+ /** npm registry から最新バージョンを取得する */
136
+ function fetchFromNpm(packageName) {
137
+ return fetchJson(`https://registry.npmjs.org/${packageName}/latest`, (data) => {
138
+ if (typeof data === "object" &&
139
+ data != null &&
140
+ "version" in data &&
141
+ typeof data.version === "string") {
142
+ return data.version;
143
+ }
144
+ return null;
145
+ });
146
+ }
147
+ /** GitHub Releases API から最新バージョンを取得する */
148
+ function fetchFromGitHub() {
149
+ return fetchJson(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, (data) => {
150
+ if (typeof data === "object" &&
151
+ data != null &&
152
+ "tag_name" in data &&
153
+ typeof data.tag_name === "string") {
154
+ // tag_name は "v1.49.0" 形式。normalizeVersion が v プレフィックスを処理する
155
+ return data.tag_name;
156
+ }
157
+ return null;
158
+ });
159
+ }
160
+ /**
161
+ * npm registry → GitHub Releases の順でフォールバックしながら最新バージョンを取得する。
162
+ * 両方失敗した場合のみ reject する。
163
+ */
164
+ function fetchLatestVersion(packageName) {
165
+ return fetchFromNpm(packageName).catch((npmErr) => {
166
+ log.debug(`npm registry failed: ${npmErr instanceof Error ? npmErr.message : String(npmErr)}, trying GitHub`);
167
+ return fetchFromGitHub();
168
+ });
169
+ }
170
+ /**
171
+ * バージョン文字列を [major, minor, patch] に正規化する。
172
+ * 不正な形式の場合は null を返す。
173
+ */
174
+ function normalizeVersion(v) {
175
+ const m = v.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
176
+ if (m == null)
177
+ return null;
178
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
179
+ }
180
+ /**
181
+ * セマンティックバージョンを比較する。
182
+ * latest が current より新しければ true を返す。
183
+ * いずれかが不正な形式の場合は false を返す。
184
+ */
185
+ function isNewerVersion(current, latest) {
186
+ const c = normalizeVersion(current);
187
+ const l = normalizeVersion(latest);
188
+ if (c == null || l == null)
189
+ return false;
190
+ if (l[0] !== c[0])
191
+ return l[0] > c[0];
192
+ if (l[1] !== c[1])
193
+ return l[1] > c[1];
194
+ return l[2] > c[2];
195
+ }
196
+ /**
197
+ * 新しいバージョンが利用可能か非同期でチェックし、あればコンソールに通知する。
198
+ * 起動をブロックしないよう、エラーは全て黙って無視する。
199
+ */
200
+ function checkForUpdates(packageName, currentVersion) {
201
+ if (isUpdateCheckDisabled()) {
202
+ log.debug("update check skipped by FLEQ_NO_UPDATE_CHECK");
203
+ return;
204
+ }
205
+ // キャッシュが有効なら registry にアクセスしない
206
+ const cache = readCache();
207
+ if (cache != null && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
208
+ if (isNewerVersion(currentVersion, cache.latestVersion)) {
209
+ printUpdateNotice(currentVersion, cache.latestVersion, packageName);
210
+ }
211
+ return;
212
+ }
213
+ // 非同期で最新バージョンを取得
214
+ fetchLatestVersion(packageName)
215
+ .then((latestVersion) => {
216
+ writeCache({ lastCheck: Date.now(), latestVersion });
217
+ if (isNewerVersion(currentVersion, latestVersion)) {
218
+ printUpdateNotice(currentVersion, latestVersion, packageName);
219
+ }
220
+ })
221
+ .catch((err) => {
222
+ log.debug(`update check failed: ${err instanceof Error ? err.message : String(err)}`);
223
+ });
224
+ }
225
+ /** 更新通知を表示する */
226
+ function printUpdateNotice(current, latest, packageName) {
227
+ log.warn(`Update available: v${current} → v${latest} ` +
228
+ chalk_1.default.gray(`npm install -g ${packageName}@latest`));
229
+ }