@triflux/remote 10.50.2 → 10.52.0
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/cto/brief.mjs +0 -20
- package/cto/collect.mjs +1 -200
- package/cto/current.schema.json +2 -23
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -187
- package/hub/index.mjs +0 -17
- package/hub/team/agent-map.json +0 -1
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +2 -39
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +42 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +2 -7
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +108 -1296
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +3 -34
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/session.mjs +7 -91
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +11 -58
- package/hub/team/tui.mjs +8 -306
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +6 -84
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/hub-client.mjs +0 -264
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-intent.mjs +0 -216
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/tools.mjs +0 -900
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -1,76 +1,12 @@
|
|
|
1
|
-
import { execSync, spawn } from "node:child_process";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
|
-
import {
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { resolveHubPortForContext } from "../../hub/hub-lifecycle.mjs";
|
|
4
|
+
import { join } from "node:path";
|
|
8
5
|
import { whichCommand, whichCommandAsync } from "@triflux/core/hub/platform.mjs";
|
|
9
6
|
|
|
10
|
-
const HUB_DEFAULT_PORT = 27888;
|
|
11
|
-
const DEFAULT_STATUS_URL = "http://127.0.0.1:27888/status";
|
|
12
|
-
const _sab = new Int32Array(new SharedArrayBuffer(4));
|
|
13
7
|
const CLI_PROBE_CACHE = new Map();
|
|
14
8
|
const CLI_PROBE_PROMISES = new Map();
|
|
15
9
|
|
|
16
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
-
const DEFAULT_PKG_ROOT = join(dirname(__filename), "..", "..");
|
|
18
|
-
|
|
19
|
-
function sleepSync(ms) {
|
|
20
|
-
Atomics.wait(_sab, 0, 0, ms);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function fetchHubStatus({
|
|
24
|
-
execSyncFn = execSync,
|
|
25
|
-
statusUrl = resolveDefaultStatusUrl(),
|
|
26
|
-
timeout = 3000,
|
|
27
|
-
} = {}) {
|
|
28
|
-
const response = execSyncFn(`curl -sf ${statusUrl}`, {
|
|
29
|
-
timeout,
|
|
30
|
-
encoding: "utf8",
|
|
31
|
-
windowsHide: true,
|
|
32
|
-
});
|
|
33
|
-
const data = JSON.parse(response);
|
|
34
|
-
return {
|
|
35
|
-
ok: true,
|
|
36
|
-
state: data?.hub?.state || "unknown",
|
|
37
|
-
pid: data?.pid,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function resolveDefaultStatusUrl(
|
|
42
|
-
env = process.env,
|
|
43
|
-
cwd = process.cwd(),
|
|
44
|
-
) {
|
|
45
|
-
const port = resolveHubPortForContext({
|
|
46
|
-
env,
|
|
47
|
-
cwd,
|
|
48
|
-
defaultPort: HUB_DEFAULT_PORT,
|
|
49
|
-
});
|
|
50
|
-
return `http://127.0.0.1:${port}/status`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function resolveStatusUrlForContext({
|
|
54
|
-
statusUrl,
|
|
55
|
-
env = process.env,
|
|
56
|
-
cwd = process.cwd(),
|
|
57
|
-
} = {}) {
|
|
58
|
-
try {
|
|
59
|
-
const url = new URL(String(statusUrl));
|
|
60
|
-
url.port = String(
|
|
61
|
-
resolveHubPortForContext({
|
|
62
|
-
port: url.port,
|
|
63
|
-
env,
|
|
64
|
-
cwd,
|
|
65
|
-
defaultPort: HUB_DEFAULT_PORT,
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
return url.toString();
|
|
69
|
-
} catch {
|
|
70
|
-
return resolveDefaultStatusUrl(env, cwd);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
10
|
function normalizeCliName(name) {
|
|
75
11
|
return String(name ?? "").trim() || null;
|
|
76
12
|
}
|
|
@@ -125,7 +61,7 @@ async function resolveCliProbe(name, options = {}) {
|
|
|
125
61
|
return toCliResult(path);
|
|
126
62
|
}
|
|
127
63
|
|
|
128
|
-
|
|
64
|
+
async function checkCli(name, options = {}) {
|
|
129
65
|
const cliName = normalizeCliName(name);
|
|
130
66
|
if (!cliName) return { ok: false };
|
|
131
67
|
|
|
@@ -168,11 +104,6 @@ export async function probeClis(names, options = {}) {
|
|
|
168
104
|
return mapCliResults(cliNames, results);
|
|
169
105
|
}
|
|
170
106
|
|
|
171
|
-
export function resetCliProbeCache() {
|
|
172
|
-
CLI_PROBE_CACHE.clear();
|
|
173
|
-
CLI_PROBE_PROMISES.clear();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
107
|
export function detectCodexAuthState({
|
|
177
108
|
homeDir = homedir(),
|
|
178
109
|
existsSyncFn = existsSync,
|
|
@@ -237,63 +168,3 @@ export function detectCodexPlan(options = {}) {
|
|
|
237
168
|
const { plan, source } = detectCodexAuthState(options);
|
|
238
169
|
return { plan, source };
|
|
239
170
|
}
|
|
240
|
-
|
|
241
|
-
export function checkHub({
|
|
242
|
-
env = process.env,
|
|
243
|
-
cwd = process.cwd(),
|
|
244
|
-
pkgRoot = DEFAULT_PKG_ROOT,
|
|
245
|
-
statusUrl = resolveDefaultStatusUrl(env, cwd),
|
|
246
|
-
restart = true,
|
|
247
|
-
requestTimeoutMs = 3000,
|
|
248
|
-
pollAttempts = 8,
|
|
249
|
-
pollIntervalMs = 500,
|
|
250
|
-
execSyncFn = execSync,
|
|
251
|
-
spawnFn = spawn,
|
|
252
|
-
existsSyncFn = existsSync,
|
|
253
|
-
sleepSyncFn = sleepSync,
|
|
254
|
-
} = {}) {
|
|
255
|
-
const guardedStatusUrl = resolveStatusUrlForContext({ statusUrl, env, cwd });
|
|
256
|
-
try {
|
|
257
|
-
return fetchHubStatus({
|
|
258
|
-
execSyncFn,
|
|
259
|
-
statusUrl: guardedStatusUrl,
|
|
260
|
-
timeout: requestTimeoutMs,
|
|
261
|
-
});
|
|
262
|
-
} catch {}
|
|
263
|
-
|
|
264
|
-
if (!restart) return { ok: false, state: "unreachable", restart: "disabled" };
|
|
265
|
-
|
|
266
|
-
const serverPath = join(pkgRoot, "hub", "server.mjs");
|
|
267
|
-
if (!existsSyncFn(serverPath))
|
|
268
|
-
return { ok: false, state: "unreachable", restart: "no_server" };
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
const child = spawnFn(process.execPath, [serverPath], {
|
|
272
|
-
env: { ...env, TFX_HUB_PORT: String(new URL(guardedStatusUrl).port) },
|
|
273
|
-
detached: true,
|
|
274
|
-
stdio: "ignore",
|
|
275
|
-
windowsHide: true,
|
|
276
|
-
});
|
|
277
|
-
child.unref();
|
|
278
|
-
} catch {
|
|
279
|
-
return { ok: false, state: "unreachable", restart: "spawn_failed" };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
for (let i = 0; i < pollAttempts; i++) {
|
|
283
|
-
sleepSyncFn(pollIntervalMs);
|
|
284
|
-
try {
|
|
285
|
-
const status = fetchHubStatus({
|
|
286
|
-
execSyncFn,
|
|
287
|
-
statusUrl: guardedStatusUrl,
|
|
288
|
-
timeout: Math.min(requestTimeoutMs, 1000),
|
|
289
|
-
});
|
|
290
|
-
if (status.state === "healthy") {
|
|
291
|
-
return { ...status, restarted: true };
|
|
292
|
-
}
|
|
293
|
-
} catch {}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return { ok: false, state: "unreachable", restart: "timeout" };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export { DEFAULT_PKG_ROOT, DEFAULT_STATUS_URL };
|
|
@@ -30,36 +30,10 @@ const DEFAULT_GEMINI_PROFILES = {
|
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
// 1회 마이그레이션 테이블: 옛 ID 형식 모델값 → agy display name.
|
|
34
|
-
// null = 카탈로그에서 빠진 모델(2.5 계열, 3 Flash) → 해당 프로필 제거 대상.
|
|
35
|
-
const LEGACY_MODEL_MIGRATION = {
|
|
36
|
-
"gemini-3.1-pro-preview": "Gemini 3.1 Pro (High)",
|
|
37
|
-
"gemini-3-flash-preview": null,
|
|
38
|
-
"Gemini 3 Flash": null,
|
|
39
|
-
"gemini-2.5-pro": null,
|
|
40
|
-
"gemini-2.5-flash": null,
|
|
41
|
-
"gemini-2.5-flash-lite": null,
|
|
42
|
-
};
|
|
43
|
-
// 과거 setup 이 자동 생성했던 프로필 이름. 정책상 더 쓰지 않으므로 제거한다.
|
|
44
|
-
// 사용자가 직접 이름 붙인 프로필은 건드리지 않는다.
|
|
45
|
-
const LEGACY_PROFILE_NAMES = [
|
|
46
|
-
"pro25",
|
|
47
|
-
"flash25",
|
|
48
|
-
"lite25",
|
|
49
|
-
"flash3",
|
|
50
|
-
"pro31",
|
|
51
|
-
"pro31_low",
|
|
52
|
-
"flash35",
|
|
53
|
-
"flash35_high",
|
|
54
|
-
"flash35_low",
|
|
55
|
-
];
|
|
56
|
-
// 과거 setup 이 자동 생성한 기본 model 값. 사용자가 고른 값이 아니므로 새 기본으로 올린다.
|
|
57
|
-
const LEGACY_DEFAULT_MODELS = new Set(["Gemini 3.5 Flash (Medium)"]);
|
|
58
|
-
|
|
59
33
|
// ── 용도별 effort (SSOT) ──
|
|
60
34
|
// 한 세대(3.8 Flash)만 쓰고 역할에 따라 effort 를 나눈다. 키는 tfx 에이전트/역할 이름
|
|
61
35
|
// (agent-map.json 의 에이전트, tfx multi --assign 의 role). bash(tfx-route.sh)와
|
|
62
|
-
// 실행
|
|
36
|
+
// 실행 모드(execution-mode)가 이 표를 읽는다.
|
|
63
37
|
// High : 판단이 결과를 좌우하는 역할 (검토, 설계, 디버깅, 분석)
|
|
64
38
|
// Medium : 생성/작성/실행 (기본값)
|
|
65
39
|
// Low : 분류, 요약, 추출, 번역처럼 짧고 기계적인 작업
|
|
@@ -182,40 +156,6 @@ function ensureGeminiProfiles({
|
|
|
182
156
|
)
|
|
183
157
|
cfg.profiles = {};
|
|
184
158
|
|
|
185
|
-
// ── 1회 마이그레이션: deprecated 2.5 프로필 prune + 옛 ID 형식 → display name ──
|
|
186
|
-
// merge-only 로직만으로는 기존 사용자 파일에 남은 stale 프로필/옛 모델 ID 가
|
|
187
|
-
// 정리되지 않으므로, 신규 default 를 채우기 전에 마이그레이션을 먼저 적용한다.
|
|
188
|
-
let migrated = false;
|
|
189
|
-
for (const legacy of LEGACY_PROFILE_NAMES) {
|
|
190
|
-
if (cfg.profiles[legacy]) {
|
|
191
|
-
delete cfg.profiles[legacy];
|
|
192
|
-
migrated = true;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
for (const [pname, pval] of Object.entries(cfg.profiles)) {
|
|
196
|
-
const mid = typeof pval === "string" ? pval : pval?.model;
|
|
197
|
-
if (mid && Object.hasOwn(LEGACY_MODEL_MIGRATION, mid)) {
|
|
198
|
-
const repl = LEGACY_MODEL_MIGRATION[mid];
|
|
199
|
-
if (repl === null) {
|
|
200
|
-
delete cfg.profiles[pname];
|
|
201
|
-
} else if (typeof pval === "string") {
|
|
202
|
-
cfg.profiles[pname] = repl;
|
|
203
|
-
} else {
|
|
204
|
-
cfg.profiles[pname].model = repl;
|
|
205
|
-
}
|
|
206
|
-
migrated = true;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (
|
|
210
|
-
typeof cfg.model === "string" &&
|
|
211
|
-
(Object.hasOwn(LEGACY_MODEL_MIGRATION, cfg.model) ||
|
|
212
|
-
LEGACY_DEFAULT_MODELS.has(cfg.model))
|
|
213
|
-
) {
|
|
214
|
-
// 옛 ID 형식/이전 세대 자동 생성 기본값은 새 기본(DEFAULT)으로 정규화한다.
|
|
215
|
-
cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
216
|
-
migrated = true;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
159
|
let added = 0;
|
|
220
160
|
for (const [name, value] of Object.entries(
|
|
221
161
|
DEFAULT_GEMINI_PROFILES.profiles,
|
|
@@ -227,12 +167,7 @@ function ensureGeminiProfiles({
|
|
|
227
167
|
}
|
|
228
168
|
if (!cfg.model) cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
229
169
|
|
|
230
|
-
if (added > 0
|
|
231
|
-
if (migrated) {
|
|
232
|
-
try {
|
|
233
|
-
copyFileSync(profilesPath, profilesPath + `.bak.${Date.now()}`);
|
|
234
|
-
} catch {}
|
|
235
|
-
}
|
|
170
|
+
if (added > 0) {
|
|
236
171
|
writeFileSync(profilesPath, JSON.stringify(cfg, null, 2) + "\n", {
|
|
237
172
|
encoding: "utf8",
|
|
238
173
|
mode: 0o600,
|
|
@@ -260,8 +195,6 @@ function ensureGeminiProfiles({
|
|
|
260
195
|
export {
|
|
261
196
|
DEFAULT_GEMINI_PROFILES,
|
|
262
197
|
ensureGeminiProfiles,
|
|
263
|
-
GEMINI_DEFAULT_PURPOSE_PROFILE,
|
|
264
|
-
GEMINI_PROFILE_BY_PURPOSE,
|
|
265
198
|
resolveGeminiModel,
|
|
266
199
|
resolveGeminiProfileForPurpose,
|
|
267
200
|
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
readFileSync,
|
|
10
10
|
realpathSync,
|
|
11
11
|
renameSync,
|
|
12
|
+
rmSync,
|
|
12
13
|
statSync,
|
|
13
14
|
unlinkSync,
|
|
14
15
|
writeFileSync,
|
|
@@ -781,3 +782,285 @@ export function cleanupLegacyMcp({
|
|
|
781
782
|
}
|
|
782
783
|
return result;
|
|
783
784
|
}
|
|
785
|
+
|
|
786
|
+
const HUB_SERVER = "tfx-hub";
|
|
787
|
+
const HUB_TASK = "TrifluxHubEnsure";
|
|
788
|
+
const HUB_CONFIG_FILES = [
|
|
789
|
+
[".claude.json", "json"],
|
|
790
|
+
[".claude/settings.json", "json"],
|
|
791
|
+
[".claude/mcp.json", "json"],
|
|
792
|
+
[".codex/config.json", "json"],
|
|
793
|
+
[".codex/config.toml", "toml"],
|
|
794
|
+
[".gemini/settings.json", "json"],
|
|
795
|
+
[".gemini/config/mcp_config.json", "json"],
|
|
796
|
+
];
|
|
797
|
+
|
|
798
|
+
// 허브가 등록한 항목은 loopback 의 /mcp 주소다. 다른 모양이면 사용자 항목으로 본다.
|
|
799
|
+
function isHubEntry(config) {
|
|
800
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
801
|
+
return false;
|
|
802
|
+
return [config.url, config.httpUrl, config.serverUrl].some((value) => {
|
|
803
|
+
try {
|
|
804
|
+
const url = new URL(value);
|
|
805
|
+
return (
|
|
806
|
+
url.protocol === "http:" &&
|
|
807
|
+
["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) &&
|
|
808
|
+
url.pathname === "/mcp"
|
|
809
|
+
);
|
|
810
|
+
} catch {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function removeHubFromJson(original, file, warnings) {
|
|
817
|
+
const data = JSON.parse(original);
|
|
818
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
819
|
+
throw new Error(`${file}: JSON 객체가 아닙니다`);
|
|
820
|
+
const groups = [
|
|
821
|
+
data.mcpServers,
|
|
822
|
+
...Object.values(data.projects ?? {}).map((project) => project?.mcpServers),
|
|
823
|
+
];
|
|
824
|
+
let count = 0;
|
|
825
|
+
for (const entries of groups) {
|
|
826
|
+
if (
|
|
827
|
+
!entries ||
|
|
828
|
+
typeof entries !== "object" ||
|
|
829
|
+
Array.isArray(entries) ||
|
|
830
|
+
!(HUB_SERVER in entries)
|
|
831
|
+
)
|
|
832
|
+
continue;
|
|
833
|
+
if (!isHubEntry(entries[HUB_SERVER])) {
|
|
834
|
+
warnings.push(`${file}: ${HUB_SERVER} 항목의 소유를 확인할 수 없어 보존`);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
delete entries[HUB_SERVER];
|
|
838
|
+
count++;
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
output: count ? `${JSON.stringify(data, null, 2)}\n` : original,
|
|
842
|
+
count,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function removeHubFromToml(original, file, warnings) {
|
|
847
|
+
const data = toml.parse(original);
|
|
848
|
+
const entry = data.mcp_servers?.[HUB_SERVER];
|
|
849
|
+
if (!entry) return { output: original, count: 0 };
|
|
850
|
+
if (!isHubEntry(entry)) {
|
|
851
|
+
warnings.push(`${file}: ${HUB_SERVER} 항목의 소유를 확인할 수 없어 보존`);
|
|
852
|
+
return { output: original, count: 0 };
|
|
853
|
+
}
|
|
854
|
+
// [mcp_servers.tfx-hub] 와 그 하위 표만 다음 표 머리글 전까지 걷어낸다.
|
|
855
|
+
const hubHeader = new RegExp(
|
|
856
|
+
`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*(?:"${HUB_SERVER}"|'${HUB_SERVER}'|${HUB_SERVER})\\s*(?:\\.[^\\]]*)?\\]`,
|
|
857
|
+
);
|
|
858
|
+
const kept = [];
|
|
859
|
+
let skipping = false;
|
|
860
|
+
let trailing = [];
|
|
861
|
+
for (const line of original.split(/(?<=\n)/)) {
|
|
862
|
+
if (/^\s*\[/.test(line)) {
|
|
863
|
+
skipping = hubHeader.test(line);
|
|
864
|
+
if (!skipping) {
|
|
865
|
+
while (trailing[0]?.trim() === "" && kept.at(-1)?.trim() === "")
|
|
866
|
+
trailing.shift();
|
|
867
|
+
kept.push(...trailing);
|
|
868
|
+
}
|
|
869
|
+
trailing = [];
|
|
870
|
+
} else if (skipping) {
|
|
871
|
+
trailing = /^\s*(?:#.*)?\r?\n?$/.test(line) ? [...trailing, line] : [];
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (!skipping) kept.push(line);
|
|
875
|
+
}
|
|
876
|
+
const output = kept.join("");
|
|
877
|
+
delete data.mcp_servers[HUB_SERVER];
|
|
878
|
+
if (!Object.keys(data.mcp_servers).length) delete data.mcp_servers;
|
|
879
|
+
try {
|
|
880
|
+
if (!isDeepStrictEqual(toml.parse(output), data))
|
|
881
|
+
throw new Error("TOML 구조 불일치");
|
|
882
|
+
} catch {
|
|
883
|
+
warnings.push(`${file}: 이주 결과 TOML 검증 실패, 파일 보존`);
|
|
884
|
+
return { output: original, count: 0 };
|
|
885
|
+
}
|
|
886
|
+
return { output, count: 1 };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function hubCommandLine(pid, platform, run) {
|
|
890
|
+
const result =
|
|
891
|
+
platform === "win32"
|
|
892
|
+
? runPowerShell(
|
|
893
|
+
run,
|
|
894
|
+
`(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CommandLine`,
|
|
895
|
+
)
|
|
896
|
+
: tryRun(run, "ps", ["-p", String(pid), "-o", "command="]);
|
|
897
|
+
if (result.ok) return result.output.trim() || null;
|
|
898
|
+
// ps 는 프로세스가 없을 때 status 1 로 끝난다. PowerShell 실패는 확인 불가다.
|
|
899
|
+
return platform !== "win32" && result.status === 1 ? null : undefined;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function stopHub(home, platform, run, result) {
|
|
903
|
+
const pidFile = join(home, ".claude/cache/tfx-hub/hub.pid");
|
|
904
|
+
const target = fileTarget(pidFile);
|
|
905
|
+
if (!target) return;
|
|
906
|
+
let pid;
|
|
907
|
+
try {
|
|
908
|
+
pid = Number(JSON.parse(readFileSync(target, "utf8")).pid);
|
|
909
|
+
} catch {
|
|
910
|
+
/* 아래에서 보존 처리 */
|
|
911
|
+
}
|
|
912
|
+
if (!Number.isInteger(pid) || pid <= 1) {
|
|
913
|
+
result.warnings.push(`${pidFile}: pid 를 읽을 수 없어 보존`);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const command = hubCommandLine(pid, platform, run);
|
|
917
|
+
if (command === undefined) {
|
|
918
|
+
result.warnings.push(`${pidFile}: pid ${pid} 확인 실패, 종료하지 않음`);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (command !== null) {
|
|
922
|
+
if (!/[/\\]hub[/\\]server\.mjs(?:["'\s]|$)/.test(command)) {
|
|
923
|
+
// 허브가 아닌 프로세스는 건드리지 않고 우리 파일만 지운다.
|
|
924
|
+
unlinkSync(target);
|
|
925
|
+
result.changed = true;
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const stopped =
|
|
929
|
+
platform === "win32"
|
|
930
|
+
? tryRun(run, "taskkill", ["/F", "/T", "/PID", String(pid)])
|
|
931
|
+
: tryRun(run, "kill", ["-TERM", String(pid)]);
|
|
932
|
+
let gone = false;
|
|
933
|
+
for (let attempt = 0; stopped.ok && attempt < 10 && !gone; attempt++) {
|
|
934
|
+
gone = hubCommandLine(pid, platform, run) === null;
|
|
935
|
+
if (!gone)
|
|
936
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
937
|
+
}
|
|
938
|
+
if (!gone) {
|
|
939
|
+
result.warnings.push(`${pidFile}: pid ${pid} 종료 확인 실패, 파일 보존`);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
result.hubStopped = true;
|
|
943
|
+
}
|
|
944
|
+
unlinkSync(target);
|
|
945
|
+
result.changed = true;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function removeHubTask(run, result) {
|
|
949
|
+
const query = tryRun(run, "schtasks.exe", [
|
|
950
|
+
"/Query",
|
|
951
|
+
"/TN",
|
|
952
|
+
HUB_TASK,
|
|
953
|
+
"/FO",
|
|
954
|
+
"LIST",
|
|
955
|
+
"/V",
|
|
956
|
+
]);
|
|
957
|
+
if (!query.ok) return;
|
|
958
|
+
if (!query.output.includes("hub-ensure.mjs")) {
|
|
959
|
+
result.warnings.push(`${HUB_TASK}: hub-ensure 작업이 아니라 보존`);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const removed = tryRun(run, "schtasks.exe", [
|
|
963
|
+
"/Delete",
|
|
964
|
+
"/TN",
|
|
965
|
+
HUB_TASK,
|
|
966
|
+
"/F",
|
|
967
|
+
]);
|
|
968
|
+
if (removed.ok) result.changed = true;
|
|
969
|
+
else result.warnings.push(`${HUB_TASK}: 예약 작업 삭제 실패`);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** cwd 의 프로젝트 MCP 파일 중 제거된 허브가 만든 tfx-hub 항목이 있는 파일을 돌려준다. */
|
|
973
|
+
export function findProjectHubEntries(cwd = process.cwd()) {
|
|
974
|
+
return [".mcp.json", ".claude/mcp.json"].flatMap((relative) => {
|
|
975
|
+
const file = join(cwd, relative);
|
|
976
|
+
try {
|
|
977
|
+
const data = JSON.parse(readFileSync(file, "utf8"));
|
|
978
|
+
return isHubEntry(data?.mcpServers?.[HUB_SERVER]) ? [file] : [];
|
|
979
|
+
} catch {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** 제거된 허브의 설정 항목, 실행 중인 프로세스, 예약 작업, 설치본 스냅샷(gemini, codex)을 정리한다. */
|
|
986
|
+
export function cleanupTfxHub({
|
|
987
|
+
home = homedir(),
|
|
988
|
+
platform = osPlatform(),
|
|
989
|
+
run = defaultRun,
|
|
990
|
+
pluginRoot,
|
|
991
|
+
} = {}) {
|
|
992
|
+
const result = {
|
|
993
|
+
ok: true,
|
|
994
|
+
changed: false,
|
|
995
|
+
removed: 0,
|
|
996
|
+
backups: [],
|
|
997
|
+
warnings: [],
|
|
998
|
+
hubStopped: false,
|
|
999
|
+
};
|
|
1000
|
+
const injected = run !== defaultRun;
|
|
1001
|
+
// 테스트에서는 실제 홈과 시스템 명령을 건드리지 않는다.
|
|
1002
|
+
if (
|
|
1003
|
+
!injected &&
|
|
1004
|
+
(process.env.NODE_TEST_CONTEXT || process.env.TRIFLUX_TEST_HOME)
|
|
1005
|
+
) {
|
|
1006
|
+
result.skipped = true;
|
|
1007
|
+
return result;
|
|
1008
|
+
}
|
|
1009
|
+
for (const [relative, kind] of HUB_CONFIG_FILES) {
|
|
1010
|
+
const file = join(home, relative);
|
|
1011
|
+
try {
|
|
1012
|
+
const target = fileTarget(file);
|
|
1013
|
+
if (!target) continue;
|
|
1014
|
+
const original = readFileSync(target, "utf8");
|
|
1015
|
+
const plan =
|
|
1016
|
+
kind === "toml"
|
|
1017
|
+
? removeHubFromToml(original, file, result.warnings)
|
|
1018
|
+
: removeHubFromJson(original, file, result.warnings);
|
|
1019
|
+
if (!plan.count) continue;
|
|
1020
|
+
if (
|
|
1021
|
+
fileTarget(file) !== target ||
|
|
1022
|
+
readFileSync(target, "utf8") !== original
|
|
1023
|
+
) {
|
|
1024
|
+
result.warnings.push(`${file}: 검사 후 변경되어 건너뜀`);
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
result.backups.push(writeAtomic(file, target, plan.output));
|
|
1028
|
+
result.removed += plan.count;
|
|
1029
|
+
result.changed = true;
|
|
1030
|
+
} catch {
|
|
1031
|
+
result.warnings.push(`${file}: 설정 형식 또는 읽기 실패`);
|
|
1032
|
+
result.ok = false;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const realSystem =
|
|
1036
|
+
injected ||
|
|
1037
|
+
(resolve(home) === resolve(homedir()) && platform === osPlatform());
|
|
1038
|
+
if (realSystem) {
|
|
1039
|
+
try {
|
|
1040
|
+
stopHub(home, platform, run, result);
|
|
1041
|
+
if (platform === "win32") removeHubTask(run, result);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
result.ok = false;
|
|
1044
|
+
result.warnings.push(`허브 프로세스 정리 실패: ${error.message}`);
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
result.warnings.push("격리 HOME: 허브 프로세스와 예약 작업 정리는 건너뜀");
|
|
1048
|
+
}
|
|
1049
|
+
for (const name of pluginRoot
|
|
1050
|
+
? ["gemini-snapshots", "codex-snapshots"]
|
|
1051
|
+
: []) {
|
|
1052
|
+
const snapshots = join(pluginRoot, "references", name);
|
|
1053
|
+
// EBUSY, EPERM, 읽기 전용 설치 디렉터리에서도 setup 이 멈추지 않게 한다.
|
|
1054
|
+
try {
|
|
1055
|
+
if (lstatSync(snapshots, { throwIfNoEntry: false })?.isDirectory()) {
|
|
1056
|
+
rmSync(snapshots, { recursive: true, force: true });
|
|
1057
|
+
result.changed = true;
|
|
1058
|
+
}
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
result.warnings.push(
|
|
1061
|
+
`${snapshots}: 삭제 실패 (${error.code ?? error.message})`,
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return result;
|
|
1066
|
+
}
|
|
@@ -14,7 +14,7 @@ import { readFileSync } from "node:fs";
|
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join, resolve } from "node:path";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
const MACHINE_PROFILE_FILENAME = "machine-profile.env";
|
|
18
18
|
|
|
19
19
|
export const MACHINE_PROFILE_KEYS = Object.freeze([
|
|
20
20
|
"TFX_MACHINE_PROFILE_VERSION",
|
|
@@ -28,9 +28,9 @@ export const MACHINE_PROFILE_KEYS = Object.freeze([
|
|
|
28
28
|
"TFX_STALL_KILL",
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
const MACHINE_PROFILE_KEY_SET = new Set(MACHINE_PROFILE_KEYS);
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const MACHINE_PROFILE_VALUE_RE = /^[A-Za-z0-9_.-]+$/u;
|
|
34
34
|
|
|
35
35
|
// CLI 이름과 disable 키의 대응. 이 두 개가 TFX_DISABLE_* SSOT 의 전부다.
|
|
36
36
|
const CLI_DISABLE_KEYS = Object.freeze({
|
|
@@ -248,20 +248,6 @@ export function normalizeCliName(cli) {
|
|
|
248
248
|
return CLI_ALIASES[key] || null;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
/**
|
|
252
|
-
* 세션 문맥 주입용 한 줄. 형식은 고정이며 소비자가 파싱하지 않고 그대로 보여준다.
|
|
253
|
-
*
|
|
254
|
-
* @param {ReturnType<typeof resolveCliPolicy>} policy
|
|
255
|
-
* @returns {string}
|
|
256
|
-
*/
|
|
257
|
-
export function formatCliPolicyLine(policy) {
|
|
258
|
-
const codex = policy?.codexDisabled ? "off" : "on";
|
|
259
|
-
const antigravity = policy?.antigravityDisabled ? "off" : "on";
|
|
260
|
-
const codexSource = policy?.source?.codex || "default";
|
|
261
|
-
const antigravitySource = policy?.source?.antigravity || "default";
|
|
262
|
-
return `cli-policy: codex=${codex} antigravity=${antigravity} (SSOT: TFX_DISABLE_*; codex<-${codexSource}, antigravity<-${antigravitySource})`;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
251
|
/**
|
|
266
252
|
* 차단된 CLI 실행 요청에 쓸 오류를 만든다. throw 는 호출자가 한다. 정책상 허용된
|
|
267
253
|
* CLI 이거나 모르는 이름이면 null 을 준다.
|