@triflux/remote 10.35.1 → 10.35.3

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/collect.mjs CHANGED
@@ -17,6 +17,7 @@ import { basename, dirname, join, relative } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
 
19
19
  import { renderBrief } from "./brief.mjs";
20
+ import { resolveLakeRootDir } from "./lake-root.mjs";
20
21
 
21
22
  const SCHEMA_VERSION = "cto-lake.v1";
22
23
  const SOURCE_REGISTRY = [
@@ -785,7 +786,7 @@ function hasFlag(args, flag) {
785
786
  }
786
787
 
787
788
  export async function runCollect(args = [], opts = {}) {
788
- const rootDir = opts.rootDir || process.cwd();
789
+ const rootDir = opts.rootDir || resolveLakeRootDir(process.cwd());
789
790
  const lakeRoot = opts.lakeRoot || join(rootDir, ".triflux", "lake");
790
791
  const stdout = opts.stdout || process.stdout;
791
792
  const stderr = opts.stderr || process.stderr;
@@ -0,0 +1,44 @@
1
+ // cto/lake-root.mjs - resolve the git repo toplevel for the CTO lake.
2
+ //
3
+ // CTO lake (.triflux/lake/current.json) 는 repo 루트에 하나만 둔다. 그런데
4
+ // runStatus/runCollect/runDashboard 는 process.cwd() 를 기본 rootDir 로 쓰므로,
5
+ // repo 루트가 아닌 하위 폴더(예: packages/triflux)에서 `tfx cto` 를 부르면 lake
6
+ // 를 못 찾아 "run tfx cto collect" 가 반복되고, collect 는 엉뚱한 하위 폴더에
7
+ // 새 .triflux/lake 를 만든다. cwd 에서 `.git` 마커를 위로 탐색해 toplevel 로
8
+ // 올려 이 불일치를 없앤다.
9
+ //
10
+ // git worktree 는 자체 `.git` 파일을 가지므로 worktree 루트에서 멈춘다 — lake
11
+ // 격리(워크트리별 collect)가 그대로 유지된다. `.git` 을 못 찾으면 cwd 를 그대로
12
+ // 반환해 기존 동작을 보존한다.
13
+
14
+ import { existsSync as defaultExistsSync } from "node:fs";
15
+ import { dirname, join, parse } from "node:path";
16
+
17
+ const MAX_DEPTH = 64;
18
+
19
+ /**
20
+ * cwd 에서 가장 가까운 git toplevel(`.git` 디렉토리 또는 파일이 있는 폴더)을
21
+ * 찾아 반환한다. 못 찾으면 cwd 를 그대로 돌려준다(기존 동작 보존).
22
+ *
23
+ * @param {string} cwd 시작 디렉토리(보통 process.cwd())
24
+ * @param {object} [opts]
25
+ * @param {(path: string) => boolean} [opts.existsSync] 테스트용 주입 seam
26
+ * @returns {string}
27
+ */
28
+ export function resolveLakeRootDir(cwd, opts = {}) {
29
+ const exists = opts?.existsSync || defaultExistsSync;
30
+ // 비문자열(객체/숫자 등 truthy 포함) 또는 빈 문자열이면 항상 "" 를 반환해
31
+ // @returns {string} 계약을 지킨다 — truthy 비문자열을 그대로 누설하지 않는다.
32
+ if (typeof cwd !== "string" || !cwd) return "";
33
+
34
+ let dir = cwd;
35
+ const { root } = parse(dir);
36
+ for (let depth = 0; depth < MAX_DEPTH; depth++) {
37
+ if (exists(join(dir, ".git"))) return dir;
38
+ if (dir === root) break;
39
+ const parent = dirname(dir);
40
+ if (parent === dir) break;
41
+ dir = parent;
42
+ }
43
+ return cwd;
44
+ }
package/cto/status.mjs CHANGED
@@ -2,6 +2,8 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
5
+ import { resolveLakeRootDir } from "./lake-root.mjs";
6
+
5
7
  const SCHEMA_VERSION = "cto-lake.v1";
6
8
  const SYNAPSE_TIMEOUT_MS = 1500;
7
9
 
@@ -304,7 +306,7 @@ function renderHumanStatus(status) {
304
306
  }
305
307
 
306
308
  export async function runStatus(args = [], opts = {}) {
307
- const rootDir = opts.rootDir || process.cwd();
309
+ const rootDir = opts.rootDir || resolveLakeRootDir(process.cwd());
308
310
  const lakeRoot = opts.lakeRoot || join(rootDir, ".triflux", "lake");
309
311
  const stdout = opts.stdout || process.stdout;
310
312
  const jsonOut = opts.json === true || hasFlag(args, "--json");
@@ -47,6 +47,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
47
47
  class WebViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler {
48
48
  var webView: WKWebView!
49
49
  var port: String = "27888"
50
+ let canonicalPort = "27888"
51
+ var consecutiveFailures = 0
50
52
  var retryWorkItem: DispatchWorkItem?
51
53
  var onFocusComplete: (() -> Void)?
52
54
 
@@ -101,6 +103,14 @@ class WebViewController: NSViewController, WKNavigationDelegate, WKScriptMessage
101
103
 
102
104
  func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
103
105
  logError("Failed to load: \(error.localizedDescription)")
106
+ consecutiveFailures += 1
107
+ if consecutiveFailures >= 3 && port != canonicalPort {
108
+ port = canonicalPort
109
+ consecutiveFailures = 0
110
+ logError("Switching to canonical port \(canonicalPort) after repeated failures")
111
+ loadTray()
112
+ return
113
+ }
104
114
  let retry = DispatchWorkItem { [weak self] in
105
115
  self?.loadTray()
106
116
  }
@@ -111,6 +121,7 @@ class WebViewController: NSViewController, WKNavigationDelegate, WKScriptMessage
111
121
  func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
112
122
  retryWorkItem?.cancel()
113
123
  retryWorkItem = nil
124
+ consecutiveFailures = 0
114
125
  logError("Successfully loaded URL")
115
126
  }
116
127
 
@@ -1,6 +1,8 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
+ import { resolveLakeRootDir } from "../../cto/lake-root.mjs";
5
+
4
6
  const DEFAULT_DEBOUNCE_MS = 120_000;
5
7
  const OFF_VALUES = new Set(["0", "false", "off", "no"]);
6
8
 
@@ -45,7 +47,7 @@ export function createCtoAutoCollector(opts = {}) {
45
47
  const cwd = typeof session?.cwd === "string" ? session.cwd : "";
46
48
  const worktree =
47
49
  typeof session?.worktreePath === "string" ? session.worktreePath : "";
48
- const projectRoot = worktree || cwd;
50
+ const projectRoot = resolveLakeRootDir(worktree || cwd);
49
51
  if (!projectRoot)
50
52
  return { triggered: false, reason: "missing-project-root" };
51
53
 
@@ -61,11 +63,19 @@ export function createCtoAutoCollector(opts = {}) {
61
63
  { sessionId, err: String(error?.message || error) },
62
64
  "cto.auto_collect.query_failed",
63
65
  );
64
- return { triggered: false, reason: "query-failed" };
65
- }
66
- if (!Array.isArray(peers) || peers.length < 1) {
67
- return { triggered: false, reason: "no-peers" };
66
+ // peer 조회 실패는 collect 막지 않는다 — peer 정보는 이제 라벨 용도뿐이라
67
+ // registry 일시 장애에도 solo 로 진행해 collect 회복력을 유지한다.
68
+ peers = [];
68
69
  }
70
+ // 단일 세션(peer 0개)도 허용한다 — 사용자 선택. peer 유무와 무관하게
71
+ // 아래 debounce + fresh-lake 게이트가 collect 빈도를 제어한다.
72
+ //
73
+ // 주의: peerCount 는 best-effort 라벨이다. querySessions 는 raw
74
+ // cwd/worktreePath 로 매칭하지만 projectRoot 는 resolved git toplevel 이라,
75
+ // 같은 repo 의 다른 하위 폴더 세션은 peer 로 안 잡혀 triggered-solo 로 보고될
76
+ // 수 있다. 즉 라벨은 repo-scoped 가 아니라 exact-cwd-scoped 다 — 향후 peer
77
+ // 기반 로직을 이 라벨 위에 다시 올릴 때 주의한다.
78
+ const peerCount = Array.isArray(peers) ? peers.length : 0;
69
79
 
70
80
  const currentTime = now();
71
81
  const last = lastCollectMs.get(projectRoot) || 0;
@@ -92,9 +102,9 @@ export function createCtoAutoCollector(opts = {}) {
92
102
  inFlight.add(promise);
93
103
  return {
94
104
  triggered: true,
95
- reason: "triggered",
105
+ reason: peerCount > 0 ? "triggered" : "triggered-solo",
96
106
  projectRoot,
97
- peerCount: peers.length,
107
+ peerCount,
98
108
  };
99
109
  }
100
110
 
package/hub/tray.mjs CHANGED
@@ -9,12 +9,26 @@ import { existsSync, readFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { dirname, join, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
+ import { resolveHubPortForContext } from "./hub-lifecycle.mjs";
12
13
  import { IS_MAC, IS_WINDOWS } from "@triflux/core/hub/platform.mjs";
13
14
  import { ensureHubForTray } from "./tray-lifecycle.mjs";
14
15
 
15
16
  const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
16
17
  const DEFAULT_HUB_PORT = "27888";
17
18
 
19
+ export function resolveTrayHubPort({
20
+ env = process.env,
21
+ cwd = process.cwd(),
22
+ } = {}) {
23
+ return String(
24
+ resolveHubPortForContext({
25
+ env,
26
+ cwd,
27
+ defaultPort: Number(DEFAULT_HUB_PORT),
28
+ }),
29
+ );
30
+ }
31
+
18
32
  function sleep(ms) {
19
33
  return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
20
34
  }
@@ -416,7 +430,7 @@ async function shutdown(reason = "shutdown") {
416
430
  export async function startTray() {
417
431
  if (IS_MAC) {
418
432
  const trayScript = fileURLToPath(import.meta.url);
419
- const port = process.env.TFX_HUB_PORT || DEFAULT_HUB_PORT;
433
+ const port = resolveTrayHubPort();
420
434
  const serverPath = join(dirname(trayScript), "server.mjs");
421
435
  await ensureHubForTray({ port, serverPath });
422
436
  const reaped = reapExistingMacTrayProcesses({ scriptPath: trayScript });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.35.1",
3
+ "version": "10.35.3",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",