@triflux/remote 10.66.0 → 10.67.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.66.0",
3
+ "version": "10.67.1",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import {
3
4
  chmodSync,
4
5
  constants,
@@ -16,7 +17,7 @@ import {
16
17
  } from "node:fs";
17
18
  import { createRequire } from "node:module";
18
19
  import { homedir, platform as osPlatform, userInfo } from "node:os";
19
- import { basename, dirname, join, resolve } from "node:path";
20
+ import { basename, dirname, join, resolve, sep } from "node:path";
20
21
  import { isDeepStrictEqual } from "node:util";
21
22
  import { loadRegistryOrDefault, pinnedArgs } from "./mcp-guard-engine.mjs";
22
23
 
@@ -1137,17 +1138,104 @@ function removeHubTasks(run, result) {
1137
1138
  }
1138
1139
  }
1139
1140
 
1140
- /** cwd 의 프로젝트 MCP 파일 중 제거된 허브가 만든 tfx-hub 항목이 있는 파일을 돌려준다. */
1141
- export function findProjectHubEntries(cwd = process.cwd()) {
1142
- return [".mcp.json", ".claude/mcp.json"].flatMap((relative) => {
1143
- const file = join(cwd, relative);
1141
+ /**
1142
+ * cwd 부터 HOME 바로 아래까지 프로젝트 MCP 파일에서 제거된 허브와 gateway 가 남긴 항목을 찾는다.
1143
+ * Claude Code 는 상위 폴더의 .mcp.json 도 읽는다. HOME 의 파일은 setup 이 정리하므로 보지 않는다.
1144
+ * @returns {{ file: string, names: string[] }[]}
1145
+ */
1146
+ export function findProjectLegacyMcpEntries(
1147
+ cwd = process.cwd(),
1148
+ home = homedir(),
1149
+ ) {
1150
+ // macOS 의 /var 와 /private/var 처럼 링크가 낀 경로도 HOME 에서 멈추도록 실제 경로로 비교한다.
1151
+ const real = (path) => {
1144
1152
  try {
1145
- const data = JSON.parse(readFileSync(file, "utf8"));
1146
- return isHubEntry(data?.mcpServers?.[HUB_SERVER]) ? [file] : [];
1153
+ return realpathSync(path);
1147
1154
  } catch {
1148
- return [];
1155
+ return resolve(path);
1149
1156
  }
1150
- });
1157
+ };
1158
+ const stop = real(home);
1159
+ const start = real(cwd);
1160
+ const dirs = start === stop ? [] : [start];
1161
+ // HOME 밖에서 실행하면 cwd 만 본다. --fix 가 HOME 밖 상위 폴더를 고치지 않게 한다.
1162
+ if (start.startsWith(stop + sep))
1163
+ for (let dir = dirname(start); dir !== stop; dir = dirname(dir))
1164
+ dirs.push(dir);
1165
+ return dirs.flatMap((dir) =>
1166
+ [".mcp.json", ".claude/mcp.json"].flatMap((relative) => {
1167
+ const file = join(dir, relative);
1168
+ let servers;
1169
+ try {
1170
+ servers = JSON.parse(readFileSync(file, "utf8"))?.mcpServers;
1171
+ } catch {
1172
+ return [];
1173
+ }
1174
+ if (!servers || typeof servers !== "object") return [];
1175
+ const names = Object.entries(servers)
1176
+ .filter(([name, config]) =>
1177
+ name === HUB_SERVER
1178
+ ? isHubEntry(config)
1179
+ : [config?.url, config?.httpUrl].some(
1180
+ (value) => ownedUrl(value)?.valid,
1181
+ ),
1182
+ )
1183
+ .map(([name]) => name);
1184
+ return names.length ? [{ file, names }] : [];
1185
+ }),
1186
+ );
1187
+ }
1188
+
1189
+ /**
1190
+ * doctor --fix: 위에서 찾은 프로젝트 파일에서 tfx-hub 를 지우고 gateway 항목은 setup 과 같이 직접 연결로 바꾼다.
1191
+ * 원본은 저장소에 커밋되지 않게 ~/.config/triflux/backups/project-mcp 에 남긴다. 소유를 확인할 수 없는 항목이 있거나 심볼릭 링크인 파일은 건드리지 않는다.
1192
+ */
1193
+ export function cleanupProjectLegacyMcp({
1194
+ cwd = process.cwd(),
1195
+ home = homedir(),
1196
+ env = process.env,
1197
+ } = {}) {
1198
+ const result = { ok: true, changed: [], backups: [], warnings: [] };
1199
+ const backupDir = join(home, ".config/triflux/backups/project-mcp");
1200
+ for (const { file } of findProjectLegacyMcpEntries(cwd, home)) {
1201
+ try {
1202
+ // 파일이나 .claude 가 링크면 다른 클라이언트 설정(agy 등)을 가리킬 수 있고 대상이 바뀔 수도 있어 고치지 않는다.
1203
+ // 폴더 목록은 실제 경로이므로 실제 경로가 같으면 경로 어디에도 링크가 없다.
1204
+ if (realpathSync(file) !== file) {
1205
+ result.warnings.push(`${file}: 심볼릭 링크라 건너뜀`);
1206
+ continue;
1207
+ }
1208
+ const original = readFileSync(file, "utf8");
1209
+ const migrated = patchJson(original, file, home, env, result.warnings);
1210
+ if (migrated.blocked) continue;
1211
+ const plan = removeHubFromJson(migrated.output, file, result.warnings);
1212
+ if (!migrated.count && !plan.count) continue;
1213
+ if (
1214
+ realpathSync(file) !== file ||
1215
+ readFileSync(file, "utf8") !== original
1216
+ ) {
1217
+ result.warnings.push(`${file}: 검사 후 변경되어 건너뜀`);
1218
+ continue;
1219
+ }
1220
+ mkdirSync(backupDir, { recursive: true });
1221
+ chmodSync(backupDir, 0o700);
1222
+ // 경로 전체를 이름에 쓰면 255바이트를 넘을 수 있어 경로 해시로 짓는다. 원래 경로는 결과의 backups 와 짝을 이룬다.
1223
+ const backup = backupFile(
1224
+ join(
1225
+ backupDir,
1226
+ `${createHash("sha256").update(file).digest("hex").slice(0, 16)}-${basename(file)}`,
1227
+ ),
1228
+ original,
1229
+ );
1230
+ writeAtomic(file, file, plan.output, new Map([[file, backup]]));
1231
+ result.backups.push(backup);
1232
+ result.changed.push(file);
1233
+ } catch {
1234
+ result.ok = false;
1235
+ result.warnings.push(`${file}: 설정 형식 또는 읽기 실패`);
1236
+ }
1237
+ }
1238
+ return result;
1151
1239
  }
1152
1240
 
1153
1241
  /** 제거된 허브의 설정 항목, 실행 중인 프로세스, 예약 작업, 설치본 스냅샷(gemini, codex)을 정리한다. */