@triflux/remote 10.0.0-alpha.1
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/hub/pipe.mjs +579 -0
- package/hub/public/dashboard.html +355 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/server.mjs +1124 -0
- package/hub/store-adapter.mjs +851 -0
- package/hub/store.mjs +897 -0
- package/hub/team/agent-map.json +11 -0
- package/hub/team/ansi.mjs +379 -0
- package/hub/team/backend.mjs +90 -0
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +106 -0
- package/hub/team/cli/commands/start/parse-args.mjs +130 -0
- package/hub/team/cli/commands/start/start-headless.mjs +109 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/cli/help.mjs +42 -0
- package/hub/team/cli/index.mjs +41 -0
- package/hub/team/cli/manifest.mjs +29 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +208 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +117 -0
- package/hub/team/cli/services/runtime-mode.mjs +62 -0
- package/hub/team/cli/services/state-store.mjs +48 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard-layout.mjs +33 -0
- package/hub/team/dashboard-open.mjs +153 -0
- package/hub/team/dashboard.mjs +274 -0
- package/hub/team/handoff.mjs +303 -0
- package/hub/team/headless.mjs +1149 -0
- package/hub/team/native-supervisor.mjs +392 -0
- package/hub/team/native.mjs +649 -0
- package/hub/team/nativeProxy.mjs +681 -0
- package/hub/team/orchestrator.mjs +161 -0
- package/hub/team/pane.mjs +153 -0
- package/hub/team/psmux.mjs +1354 -0
- package/hub/team/routing.mjs +223 -0
- package/hub/team/session.mjs +611 -0
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +361 -0
- package/hub/team/tui-lite.mjs +380 -0
- package/hub/team/tui-viewer.mjs +463 -0
- package/hub/team/tui.mjs +1245 -0
- package/hub/tools.mjs +554 -0
- package/hub/tray.mjs +376 -0
- package/hub/workers/claude-worker.mjs +475 -0
- package/hub/workers/codex-mcp.mjs +504 -0
- package/hub/workers/delegator-mcp.mjs +1076 -0
- package/hub/workers/factory.mjs +21 -0
- package/hub/workers/gemini-worker.mjs +373 -0
- package/hub/workers/interface.mjs +52 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/package.json +31 -0
package/hub/tray.mjs
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import _SysTrayModule from "systray2";
|
|
4
|
+
const SysTray = _SysTrayModule.default || _SysTrayModule;
|
|
5
|
+
import { exec } from "node:child_process";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { IS_WINDOWS } from "./platform.mjs";
|
|
11
|
+
|
|
12
|
+
const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
|
|
13
|
+
const DEFAULT_HUB_PORT = "27888";
|
|
14
|
+
|
|
15
|
+
function getHubBaseUrl() {
|
|
16
|
+
if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/+$/, "");
|
|
17
|
+
try {
|
|
18
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
19
|
+
if (info.port) return `http://${info.host || "127.0.0.1"}:${info.port}`;
|
|
20
|
+
} catch {}
|
|
21
|
+
const port = process.env.TFX_HUB_PORT || DEFAULT_HUB_PORT;
|
|
22
|
+
return `http://127.0.0.1:${port}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getDashboardUrl() {
|
|
26
|
+
return `${getHubBaseUrl()}/dashboard`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getHubStatusUrl() {
|
|
30
|
+
return `${getHubBaseUrl()}/status`;
|
|
31
|
+
}
|
|
32
|
+
const POLL_INTERVAL_MS = 10_000;
|
|
33
|
+
const HUB_TIMEOUT_MS = 3_000;
|
|
34
|
+
const AIMD_INITIAL = 3;
|
|
35
|
+
const AIMD_MIN = 1;
|
|
36
|
+
const AIMD_MAX = 10;
|
|
37
|
+
const AIMD_WINDOW_MS = 30 * 60 * 1000;
|
|
38
|
+
|
|
39
|
+
const CACHE_DIR = join(homedir(), ".claude", "cache");
|
|
40
|
+
const BATCH_EVENTS_FILE = join(CACHE_DIR, "batch-events.jsonl");
|
|
41
|
+
const CODEX_RATE_LIMITS_FILE = join(CACHE_DIR, "codex-rate-limits-cache.json");
|
|
42
|
+
const GEMINI_QUOTA_FILE = join(CACHE_DIR, "gemini-quota-cache.json");
|
|
43
|
+
const CLAUDE_USAGE_FILE = join(CACHE_DIR, "claude-usage-cache.json");
|
|
44
|
+
|
|
45
|
+
const TRAY_ICON_BASE64 = "AAABAAEAICAAAAEAIAADAQAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAgAAAAIAgGAAAAc3p69AAAAMpJREFUeJzV1UEKgzAQheEcwnXP4a17gl6n6yy7U1IIqDSTeW/m0TYwK8X/E6OW8m9rWW5Pa74SlWLYeBgRDcOQ7V42a+SIGSALkwKIQKDnroJQmy4bAgO8EDnAA4EBkV3do7VWeAOfAO0Cx/EC+vnMG2QCLND12Pp4vUcK+DQ9fBxqI47uDI23oV7F2fNVxF2A64zCTBwCWGE2Pv0WzKKp8ba8wWg4DIiGKUBWdBjP+i+E4z8BUCJccRUCimdC6HAGIiWOYiRR5doBauXshzcEs0UAAAAASUVORK5CYII=";
|
|
46
|
+
|
|
47
|
+
function clampPercent(value) {
|
|
48
|
+
if (!Number.isFinite(Number(value))) return null;
|
|
49
|
+
return Math.max(0, Math.min(100, Math.round(Number(value))));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readJson(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readLines(filePath) {
|
|
61
|
+
try {
|
|
62
|
+
return readFileSync(filePath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isSuccessResult(result) {
|
|
69
|
+
return result === "success" || result === "success_with_warnings";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getAimdBatchSize(now = Date.now()) {
|
|
73
|
+
const sinceMs = now - AIMD_WINDOW_MS;
|
|
74
|
+
const events = readLines(BATCH_EVENTS_FILE)
|
|
75
|
+
.map((line) => {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(line);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.filter((event) => event && Number(event.ts) >= sinceMs);
|
|
83
|
+
|
|
84
|
+
if (events.length === 0) return AIMD_INITIAL;
|
|
85
|
+
|
|
86
|
+
let batchSize = AIMD_INITIAL;
|
|
87
|
+
for (const event of events) {
|
|
88
|
+
if (isSuccessResult(event.result)) {
|
|
89
|
+
batchSize = Math.min(AIMD_MAX, batchSize + 1);
|
|
90
|
+
} else {
|
|
91
|
+
batchSize = Math.max(AIMD_MIN, Math.floor(batchSize * 0.5));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return batchSize;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getCodexPercent() {
|
|
98
|
+
const data = readJson(CODEX_RATE_LIMITS_FILE);
|
|
99
|
+
const buckets = data?.buckets && typeof data.buckets === "object" ? data.buckets : null;
|
|
100
|
+
const primaryBucket = buckets?.codex ?? Object.values(buckets ?? {})[0] ?? null;
|
|
101
|
+
return clampPercent(primaryBucket?.primary?.used_percent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pickGeminiBucket(data) {
|
|
105
|
+
const buckets = Array.isArray(data?.buckets) ? data.buckets : [];
|
|
106
|
+
if (buckets.length === 0) return null;
|
|
107
|
+
|
|
108
|
+
const preferredModels = [
|
|
109
|
+
"gemini-3-flash-preview",
|
|
110
|
+
"gemini-2.5-flash",
|
|
111
|
+
"gemini-3-flash",
|
|
112
|
+
"gemini-2.5-flash-lite",
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
for (const modelId of preferredModels) {
|
|
116
|
+
const match = buckets.find((bucket) => bucket?.modelId === modelId);
|
|
117
|
+
if (match) return match;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return buckets.find((bucket) => String(bucket?.modelId ?? "").includes("flash")) ?? buckets[0];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getGeminiPercent() {
|
|
124
|
+
const data = readJson(GEMINI_QUOTA_FILE);
|
|
125
|
+
const bucket = pickGeminiBucket(data);
|
|
126
|
+
if (!bucket) return null;
|
|
127
|
+
return clampPercent((1 - Number(bucket.remainingFraction ?? 1)) * 100);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getClaudePercent() {
|
|
131
|
+
const data = readJson(CLAUDE_USAGE_FILE);
|
|
132
|
+
return clampPercent(data?.data?.fiveHourPercent);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function getHubStatusLabel() {
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(getHubStatusUrl(), {
|
|
138
|
+
signal: AbortSignal.timeout(HUB_TIMEOUT_MS),
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
141
|
+
|
|
142
|
+
const data = await response.json();
|
|
143
|
+
const state = typeof data?.hub?.state === "string" ? data.hub.state : "connected";
|
|
144
|
+
const sessions = Number.isFinite(Number(data?.sessions)) ? Number(data.sessions) : null;
|
|
145
|
+
return sessions == null ? `Hub: ${state}` : `Hub: ${state} | S:${sessions}`;
|
|
146
|
+
} catch {
|
|
147
|
+
return "Hub 미연결";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatTooltipPercent(value) {
|
|
152
|
+
return value == null ? "--%" : `${value}%`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatMenuPercent(value) {
|
|
156
|
+
return value == null ? "--%" : `${value}%`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildTooltip(snapshot) {
|
|
160
|
+
const hubTag = snapshot.hubLabel.startsWith("Hub 미") ? "H:off" : "H:on";
|
|
161
|
+
return `tfx AIMD:${snapshot.aimd}/10 | C:${formatTooltipPercent(snapshot.claude)} X:${formatTooltipPercent(snapshot.codex)} G:${formatTooltipPercent(snapshot.gemini)} ${hubTag}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildUsageTitle(snapshot) {
|
|
165
|
+
return `C: ${formatMenuPercent(snapshot.claude)} | X: ${formatMenuPercent(snapshot.codex)} | G: ${formatMenuPercent(snapshot.gemini)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function findChromePath() {
|
|
169
|
+
const candidates = [
|
|
170
|
+
join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
171
|
+
join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
172
|
+
join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
173
|
+
];
|
|
174
|
+
for (const p of candidates) {
|
|
175
|
+
try { if (existsSync(p)) return p; } catch {}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function openDashboard() {
|
|
181
|
+
const url = getDashboardUrl();
|
|
182
|
+
const shell = process.env.ComSpec || "cmd.exe";
|
|
183
|
+
const chrome = findChromePath();
|
|
184
|
+
if (chrome) {
|
|
185
|
+
// Chrome --app: 주소바/탭 없는 앱 윈도우로 열기
|
|
186
|
+
exec(`start "" "${chrome}" "--app=${url}"`, { shell, windowsHide: true }, (err) => {
|
|
187
|
+
if (err) {
|
|
188
|
+
exec(`start "" "${url}"`, { shell, windowsHide: true }, () => {});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
exec(`start "" "${url}"`, { shell, windowsHide: true }, () => {});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const openDashboardItem = {
|
|
197
|
+
title: "대시보드 열기",
|
|
198
|
+
tooltip: "브라우저에서 대시보드 열기",
|
|
199
|
+
enabled: true,
|
|
200
|
+
click: openDashboard,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const aimdItem = {
|
|
204
|
+
title: "AIMD: 3/10",
|
|
205
|
+
tooltip: "최근 30분 AIMD 동시 워커",
|
|
206
|
+
enabled: true,
|
|
207
|
+
click: openDashboard,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const quotaItem = {
|
|
211
|
+
title: "C: --% | X: --% | G: --%",
|
|
212
|
+
tooltip: "Claude | Codex | Gemini 사용률",
|
|
213
|
+
enabled: true,
|
|
214
|
+
click: openDashboard,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const hubItem = {
|
|
218
|
+
title: "Hub 미연결",
|
|
219
|
+
tooltip: "Hub 연결 상태",
|
|
220
|
+
enabled: true,
|
|
221
|
+
click: openDashboard,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const refreshItem = {
|
|
225
|
+
title: "새로고침",
|
|
226
|
+
tooltip: "캐시 재읽기",
|
|
227
|
+
enabled: true,
|
|
228
|
+
click: () => {
|
|
229
|
+
void scheduleRefresh();
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const exitItem = {
|
|
234
|
+
title: "종료",
|
|
235
|
+
tooltip: "트레이 종료",
|
|
236
|
+
enabled: true,
|
|
237
|
+
click: () => {
|
|
238
|
+
void shutdown("menu-exit");
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const menu = {
|
|
243
|
+
icon: TRAY_ICON_BASE64,
|
|
244
|
+
title: "tfx",
|
|
245
|
+
tooltip: "tfx AIMD:3/10 | C:--% X:--% G:--% H:off",
|
|
246
|
+
items: [
|
|
247
|
+
openDashboardItem,
|
|
248
|
+
SysTray.separator,
|
|
249
|
+
aimdItem,
|
|
250
|
+
quotaItem,
|
|
251
|
+
hubItem,
|
|
252
|
+
SysTray.separator,
|
|
253
|
+
refreshItem,
|
|
254
|
+
exitItem,
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
let systray = null;
|
|
259
|
+
let pollTimer = null;
|
|
260
|
+
let refreshPromise = null;
|
|
261
|
+
let shuttingDown = false;
|
|
262
|
+
|
|
263
|
+
async function refreshMenu() {
|
|
264
|
+
const snapshot = {
|
|
265
|
+
aimd: getAimdBatchSize(),
|
|
266
|
+
codex: getCodexPercent(),
|
|
267
|
+
gemini: getGeminiPercent(),
|
|
268
|
+
claude: getClaudePercent(),
|
|
269
|
+
hubLabel: await getHubStatusLabel(),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
aimdItem.title = `AIMD: ${snapshot.aimd}/10`;
|
|
273
|
+
quotaItem.title = buildUsageTitle(snapshot);
|
|
274
|
+
hubItem.title = snapshot.hubLabel;
|
|
275
|
+
|
|
276
|
+
if (systray) {
|
|
277
|
+
await systray.sendAction({ type: "update-item", item: aimdItem });
|
|
278
|
+
await systray.sendAction({ type: "update-item", item: quotaItem });
|
|
279
|
+
await systray.sendAction({ type: "update-item", item: hubItem });
|
|
280
|
+
await systray.sendAction({ type: "update-item-and-title", item: { title: buildTooltip(snapshot) } });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function scheduleRefresh() {
|
|
285
|
+
if (refreshPromise) return refreshPromise;
|
|
286
|
+
refreshPromise = refreshMenu().catch((error) => {
|
|
287
|
+
console.error(`[tfx-tray] refresh failed: ${error.message}`);
|
|
288
|
+
}).finally(() => {
|
|
289
|
+
refreshPromise = null;
|
|
290
|
+
});
|
|
291
|
+
return refreshPromise;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function shutdown(reason = "shutdown") {
|
|
295
|
+
if (shuttingDown) return;
|
|
296
|
+
shuttingDown = true;
|
|
297
|
+
|
|
298
|
+
if (pollTimer) {
|
|
299
|
+
clearInterval(pollTimer);
|
|
300
|
+
pollTimer = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
if (systray && !systray.killed) {
|
|
305
|
+
await systray.kill(false);
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error(`[tfx-tray] ${reason} cleanup failed: ${error.message}`);
|
|
309
|
+
} finally {
|
|
310
|
+
systray = null;
|
|
311
|
+
process.exit(0);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function startTray() {
|
|
316
|
+
if (!IS_WINDOWS) {
|
|
317
|
+
throw new Error("tray command is only supported on Windows.");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
systray = new SysTray({
|
|
321
|
+
menu,
|
|
322
|
+
debug: false,
|
|
323
|
+
copyDir: false,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await systray.ready();
|
|
327
|
+
|
|
328
|
+
systray.onError((error) => {
|
|
329
|
+
console.error(`[tfx-tray] ${error.message}`);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
systray.onExit((code, signal) => {
|
|
333
|
+
if (shuttingDown) return;
|
|
334
|
+
const detail = signal ? `signal ${signal}` : `code ${code ?? 0}`;
|
|
335
|
+
console.error(`[tfx-tray] tray exited unexpectedly (${detail})`);
|
|
336
|
+
process.exit(typeof code === "number" ? code : 1);
|
|
337
|
+
});
|
|
338
|
+
await systray.onClick((action) => {
|
|
339
|
+
if (action.item?.click) {
|
|
340
|
+
action.item.click();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (action.item?.__id === openDashboardItem.__id) {
|
|
345
|
+
openDashboard();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await scheduleRefresh();
|
|
350
|
+
|
|
351
|
+
pollTimer = setInterval(() => {
|
|
352
|
+
void scheduleRefresh();
|
|
353
|
+
}, POLL_INTERVAL_MS);
|
|
354
|
+
pollTimer.unref();
|
|
355
|
+
|
|
356
|
+
process.on("SIGINT", () => {
|
|
357
|
+
void shutdown("SIGINT");
|
|
358
|
+
});
|
|
359
|
+
process.on("SIGTERM", () => {
|
|
360
|
+
void shutdown("SIGTERM");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
systray,
|
|
365
|
+
stop: shutdown,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const selfRun = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
|
|
370
|
+
|
|
371
|
+
if (selfRun) {
|
|
372
|
+
startTray().catch((error) => {
|
|
373
|
+
console.error(`[tfx-tray] start failed: ${error.message}`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
});
|
|
376
|
+
}
|