@triflux/remote 10.57.3 → 10.59.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.
@@ -34,7 +34,7 @@ export function resolveClaudeHomeDir(
34
34
  const userProfile = nullableEnv(env.USERPROFILE);
35
35
  return userProfile ? path.resolve(userProfile) : os.homedir();
36
36
  }
37
- // POSIX 는 HOME 우선 — sandbox HOME 오버라이드를 추적하는 provenance
37
+ // POSIX 는 HOME 우선, sandbox HOME 오버라이드를 추적하는 provenance
38
38
  // 해석(#382)과 일치 (os.homedir() 도 POSIX 에서는 HOME 을 우선한다).
39
39
  const home = nullableEnv(env.HOME);
40
40
  return home ? path.resolve(home) : os.homedir();
@@ -223,7 +223,7 @@ export function publicClaudeDaemonCandidate(candidate) {
223
223
 
224
224
  function sessionsFromDaemonList(listResponse) {
225
225
  // probe 경로도 list/find 경로와 같은 정규화 row 를 내보내야 한다
226
- // (jobs/sessions, snake_case, dispatch 중첩 변형 흡수 — 8016b724).
226
+ // (jobs/sessions, snake_case, dispatch 중첩 변형 흡수, 8016b724).
227
227
  return extractClaudeAgentSessions(listResponse);
228
228
  }
229
229
 
@@ -501,7 +501,7 @@ export function sendClaudeControlRequest(
501
501
  // 발급한다. control.sock 은 runtime config hash 를 쓰더라도 auth 는 발급 config
502
502
  // 에서 읽어야 한다. 일반 daemon 은 <configDir>/daemon/control.key 를 사용한다.
503
503
  // 어느 경로에도 파일이 없으면 인증 미강제 daemon 으로 보고 auth 를 생략한다
504
- // (fail-open — 구버전 호환).
504
+ // (fail-open, 구버전 호환).
505
505
  export async function readDaemonControlKey(
506
506
  configDir = resolveClaudeConfigDir(),
507
507
  { diagnostics } = {},
@@ -933,7 +933,7 @@ function isClaudeAttachChromeLine(line) {
933
933
  ) ||
934
934
  /^[-─━▔╭╮╰╯│┌┐└┘├┤┬┴┼\s]+$/.test(trimmed) ||
935
935
  /^(c|x|g):\s/.test(trimmed) ||
936
- /CTX:\d+/.test(trimmed) ||
936
+ /ctx:(?:\d+|--)/i.test(trimmed) ||
937
937
  /auto mode on/.test(trimmed)
938
938
  );
939
939
  }
@@ -1720,7 +1720,7 @@ export async function dispatchClaudeDaemonJob({
1720
1720
  { timeoutMs: dispatchTimeoutMs },
1721
1721
  );
1722
1722
  if (dispatch?.ok !== true) {
1723
- // daemon 거부 사유를 보존한다 — generic 메시지만 던지면 EAUTH 같은
1723
+ // daemon 거부 사유를 보존한다, generic 메시지만 던지면 EAUTH 같은
1724
1724
  // 실원인이 묻혀 진단이 어려워진다.
1725
1725
  const reason = dispatch?.error ? `: ${dispatch.error}` : "";
1726
1726
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.57.3",
3
+ "version": "10.59.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -0,0 +1,228 @@
1
+ const AGENT_ENV = [
2
+ ["claude-code", ["CLAUDECODE", "CLAUDE_CODE"]],
3
+ ["codex", ["CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID"]],
4
+ ["gemini-cli", ["GEMINI_CLI"]],
5
+ ["antigravity", ["ANTIGRAVITY_AGENT"]],
6
+ ["opencode", ["OPENCODE_CLIENT"]],
7
+ ["cursor", ["CURSOR_AGENT"]],
8
+ ];
9
+
10
+ const enabled = (value) =>
11
+ Boolean(value) && !/^(0|false|no|off)$/iu.test(value);
12
+
13
+ export function outputOptions(
14
+ args = [],
15
+ { env = process.env, input = process.stdin, output = process.stdout } = {},
16
+ ) {
17
+ const agent =
18
+ AGENT_ENV.find(([, keys]) => keys.some((key) => enabled(env[key])))?.[0] ||
19
+ (enabled(env.AI_AGENT)
20
+ ? env.AI_AGENT === "1"
21
+ ? "ai-agent"
22
+ : env.AI_AGENT
23
+ : null);
24
+ const json = args.includes("--json");
25
+ return {
26
+ json,
27
+ quiet: args.includes("--quiet") || args.includes("-q"),
28
+ verbose: args.includes("--verbose"),
29
+ check: args.includes("--check"),
30
+ agent,
31
+ nonInteractive:
32
+ json ||
33
+ Boolean(agent) ||
34
+ !input?.isTTY ||
35
+ !output?.isTTY ||
36
+ enabled(env.CI) ||
37
+ enabled(env.DOCKER) ||
38
+ env.npm_lifecycle_event === "postinstall",
39
+ color:
40
+ Boolean(output?.isTTY) &&
41
+ env.NO_COLOR === undefined &&
42
+ env.TERM !== "dumb" &&
43
+ (env.FORCE_COLOR === undefined ||
44
+ ["", "true", "1", "2", "3"].includes(env.FORCE_COLOR)),
45
+ };
46
+ }
47
+
48
+ // setup/update JSON 계약: command, outcome, message, alreadyInGoalState,
49
+ // from, to, changed, unchanged, targets, warnings, notices.
50
+ // 해당 결과에만 포함: installMode, installPath, exitCode.
51
+ // 내부 자식 결과에만 포함: files, steps, elapsed.
52
+ export function createCliResult(command, version) {
53
+ return {
54
+ command,
55
+ outcome: "ok",
56
+ message: "",
57
+ alreadyInGoalState: true,
58
+ from: version,
59
+ to: version,
60
+ changed: [],
61
+ unchanged: 0,
62
+ targets: [],
63
+ warnings: [],
64
+ notices: [],
65
+ files: [],
66
+ steps: [],
67
+ };
68
+ }
69
+
70
+ export function recordFile(result, path, change) {
71
+ result.files.push({ path, change });
72
+ if (change === "noop") result.unchanged++;
73
+ else {
74
+ result.changed.push(path);
75
+ result.alreadyInGoalState = false;
76
+ }
77
+ }
78
+
79
+ export function addWarning(result, code, message) {
80
+ if (!result.warnings.some((warning) => warning.message === message))
81
+ result.warnings.push({ code, message });
82
+ }
83
+
84
+ export function addStep(result, message) {
85
+ result.steps.push(message);
86
+ result.alreadyInGoalState = false;
87
+ }
88
+
89
+ export function addNotice(result, message) {
90
+ if (!result.notices.includes(message)) result.notices.push(message);
91
+ }
92
+
93
+ export function commandErrorMessage(error) {
94
+ const message = error.message.split(/\r?\n/)[0];
95
+ const cause = String(error.stderr || "")
96
+ .split(/\r?\n/)
97
+ .find((line) => line.trim())
98
+ ?.trim();
99
+ return cause && !message.includes(cause) ? `${message}: ${cause}` : message;
100
+ }
101
+
102
+ function updateCommand(result, rollback = false) {
103
+ if (["git-local", "plugin"].includes(result.installMode))
104
+ return rollback ? `git checkout v${result.from}` : "git pull";
105
+ const version = rollback ? result.from : "latest";
106
+ return result.installMode === "npm-local"
107
+ ? rollback
108
+ ? `npm i triflux@${version}`
109
+ : "npm update triflux"
110
+ : `npm i -g triflux@${version}`;
111
+ }
112
+
113
+ export function renderCliResult(result, options = {}) {
114
+ const { json, quiet, verbose, check, agent, nonInteractive, color, details } =
115
+ options;
116
+ if (json) {
117
+ const { files, steps, elapsed, ...publicResult } = result;
118
+ return `${JSON.stringify(details ? result : publicResult)}\n`;
119
+ }
120
+ const mark = (symbol) =>
121
+ color
122
+ ? `\x1b[${symbol === "✗" ? 31 : symbol === "!" ? 33 : 32}m${symbol}\x1b[0m`
123
+ : symbol;
124
+ const lines = [];
125
+ if (result.outcome === "failed") {
126
+ lines.push(
127
+ `${mark("✗")} ${result.command === "update" ? "업데이트" : "설정"} 실패: ${result.message}`,
128
+ );
129
+ if (result.command === "update") {
130
+ if (result.installPath) lines.push(`실행 경로: ${result.installPath}`);
131
+ lines.push(`직접 실행: ${updateCommand(result)}`);
132
+ if (result.from !== result.to)
133
+ lines.push(`되돌리기: ${updateCommand(result, true)}`);
134
+ }
135
+ } else if (result.outcome === "skipped") {
136
+ if (!quiet) lines.push(`skipped: ${result.message}`);
137
+ } else if (!quiet) {
138
+ if (result.command === "update") {
139
+ const versionChanged = result.from !== result.to;
140
+ const version = versionChanged
141
+ ? `${result.from} -> ${result.to}`
142
+ : `${result.to} 최신`;
143
+ if (check)
144
+ lines.push(
145
+ versionChanged
146
+ ? `업데이트 있음: ${result.from} -> ${result.to}`
147
+ : `${mark("✓")} triflux ${version}`,
148
+ );
149
+ else {
150
+ const time = `${(result.elapsed / 1000).toFixed(1)}s`;
151
+ const detail =
152
+ !versionChanged && !result.changed.length
153
+ ? `파일 ${result.unchanged}개 일치, ${time}`
154
+ : time;
155
+ lines.push(`${mark("✓")} triflux ${version} (${detail})`);
156
+ if (versionChanged)
157
+ lines.push(
158
+ ` https://github.com/tellang/triflux/releases/tag/v${result.to}`,
159
+ );
160
+ }
161
+ if (result.changed.length)
162
+ lines.push(
163
+ `파일 ${result.changed.length}개 반영 (${result.unchanged}개 그대로)`,
164
+ );
165
+ } else {
166
+ const context = agent
167
+ ? ` (에이전트: ${agent}, 비대화식)`
168
+ : nonInteractive
169
+ ? " (비대화식)"
170
+ : "";
171
+ lines.push(`triflux ${result.to} setup${context}`);
172
+ }
173
+ if (!check) {
174
+ if (result.command === "update" || verbose) {
175
+ for (const { path, change } of result.files) {
176
+ if (change === "noop" && !verbose) continue;
177
+ lines.push(
178
+ ` ${mark(change === "create" ? "+" : change === "noop" ? "✓" : "~")} ${path}`,
179
+ );
180
+ }
181
+ }
182
+ if (
183
+ result.command === "setup" ||
184
+ !result.alreadyInGoalState ||
185
+ result.warnings.length
186
+ ) {
187
+ for (const { name, status, detail } of result.targets)
188
+ lines.push(
189
+ ` ${mark(status === "ok" ? "✓" : status === "skipped" ? "-" : "✗")} ${name.padEnd(8)} ${detail}`,
190
+ );
191
+ }
192
+ for (const step of result.steps) lines.push(` ${mark("✓")} ${step}`);
193
+ }
194
+ }
195
+ for (const warning of result.warnings)
196
+ lines.push(`${mark("!")} ${warning.message}`);
197
+ if (!quiet)
198
+ for (const notice of result.notices) lines.push(`안내: ${notice}`);
199
+ if (!quiet && result.outcome === "ok" && !check) {
200
+ if (result.command === "setup") {
201
+ lines.push(
202
+ `설정 완료${result.warnings.length ? `, 경고 ${result.warnings.length}건` : ""}`,
203
+ "다음: tfx doctor",
204
+ );
205
+ } else if (result.warnings.length || result.from !== result.to) {
206
+ if (result.from !== result.to && result.installPath)
207
+ lines.push(`되돌리기 실행 경로: ${result.installPath}`);
208
+ lines.push(
209
+ `${result.warnings.length ? `경고 ${result.warnings.length}건. ` : ""}${result.from !== result.to ? `되돌리기: ${updateCommand(result, true)}` : ""}`.trim(),
210
+ );
211
+ }
212
+ }
213
+ return lines.length ? `${lines.join("\n")}\n` : "";
214
+ }
215
+
216
+ export function postinstallMessage(version, report) {
217
+ if (!report) return "저장소 체크아웃이라 setup 을 건너뜀. 설정: tfx setup\n";
218
+ const status =
219
+ report.outcome === "failed"
220
+ ? `실패: ${report.message}`
221
+ : report.outcome === "skipped"
222
+ ? `건너뜀: ${report.message}`
223
+ : "완료";
224
+ const warnings = report.warnings
225
+ .map(({ message }) => `! ${message}\n`)
226
+ .join("");
227
+ return `triflux ${version} 설치됨. 설정 ${status}${report.warnings.length ? `, 경고 ${report.warnings.length}건` : ""}. 확인: tfx setup\n${warnings}`;
228
+ }
@@ -1053,7 +1053,7 @@ function listHubProcesses(platform, run) {
1053
1053
 
1054
1054
  // 허브는 제거됐으므로 triflux 패키지의 hub/server.mjs 로 확인된 프로세스는 설치본,
1055
1055
  // 체크아웃, worktree 를 가리지 않고 종료한다(2026-10-08 결정).
1056
- function stopHubs(home, platform, run, result, log) {
1056
+ function stopHubs(home, platform, run, result) {
1057
1057
  const processes = listHubProcesses(platform, run);
1058
1058
  if (!processes) {
1059
1059
  result.warnings.push("허브 프로세스 조회 실패, 종료하지 않음");
@@ -1064,7 +1064,6 @@ function stopHubs(home, platform, run, result, log) {
1064
1064
  if (Number(pid) === process.pid || typeof command !== "string") continue;
1065
1065
  const root = trifluxHubRoot(processArgv(pid, command, platform));
1066
1066
  if (!root) continue;
1067
- log(`허브 종료: pid ${pid} (${root})`);
1068
1067
  const stopped =
1069
1068
  platform === "win32"
1070
1069
  ? tryRun(run, "taskkill", ["/F", "/T", "/PID", String(pid)])
@@ -1157,7 +1156,6 @@ export function cleanupTfxHub({
1157
1156
  platform = osPlatform(),
1158
1157
  run = defaultRun,
1159
1158
  pluginRoot,
1160
- log = () => {},
1161
1159
  backups,
1162
1160
  } = {}) {
1163
1161
  const result = {
@@ -1210,7 +1208,7 @@ export function cleanupTfxHub({
1210
1208
  (resolve(home) === resolve(homedir()) && platform === osPlatform());
1211
1209
  if (realSystem) {
1212
1210
  try {
1213
- stopHubs(home, platform, run, result, log);
1211
+ stopHubs(home, platform, run, result);
1214
1212
  if (platform === "win32") removeHubTasks(run, result);
1215
1213
  } catch (error) {
1216
1214
  result.ok = false;
@@ -0,0 +1,184 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ chmodSync,
5
+ copyFileSync,
6
+ linkSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ mkdtempSync,
10
+ readdirSync,
11
+ rmSync,
12
+ symlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { delimiter, join } from "node:path";
17
+
18
+ // 버전과 체크섬은 이 상수 한 곳에서만 올린다. 받은 파일은 이 sha256 으로만 믿는다.
19
+ export const TMR_RELEASE = Object.freeze({
20
+ version: "0.1.0",
21
+ asset: "tmux-rooms-cli-0.1.0-macos-arm64.zip",
22
+ sha256: "342b34768a6f054d6bda6d3a63de37aeae08c5304d4531eecedf9631805a282f",
23
+ });
24
+ const releaseBase = ({ version }) =>
25
+ `https://github.com/tellang/tmux-rooms/releases/download/v${version}`;
26
+
27
+ export function tmrSupported(platform = process.platform, arch = process.arch) {
28
+ return platform === "darwin" && arch === "arm64";
29
+ }
30
+
31
+ export function tmrBinDir(home) {
32
+ return join(home, ".local", "bin");
33
+ }
34
+
35
+ export function findTmr({ home, env = process.env }) {
36
+ const dirs = [
37
+ tmrBinDir(home),
38
+ ...String(env.PATH || "").split(delimiter),
39
+ ].filter(Boolean);
40
+ // 깨진 링크도 사용자 것으로 보고 건드리지 않는다. 읽을 수 없는 PATH 항목은 건너뛴다.
41
+ for (const dir of dirs)
42
+ for (const name of ["tmuxrooms", "tmr"]) {
43
+ try {
44
+ if (lstatSync(join(dir, name), { throwIfNoEntry: false }))
45
+ return join(dir, name);
46
+ } catch {}
47
+ }
48
+ return null;
49
+ }
50
+
51
+ async function download(url, fetchFn) {
52
+ const response = await fetchFn(url, { signal: AbortSignal.timeout(60_000) });
53
+ if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
54
+ return Buffer.from(await response.arrayBuffer());
55
+ }
56
+
57
+ function sha256(buffer) {
58
+ return createHash("sha256").update(buffer).digest("hex");
59
+ }
60
+
61
+ function extractWithDitto(zipPath, directory) {
62
+ execFileSync("ditto", ["-x", "-k", zipPath, directory], {
63
+ stdio: "ignore",
64
+ timeout: 30_000,
65
+ });
66
+ }
67
+
68
+ /** 체크섬이 맞는 tmuxrooms 를 ~/.local/bin 에 넣고 상대 링크 tmr 을 만든다. */
69
+ export async function installTmr({
70
+ home,
71
+ release = TMR_RELEASE,
72
+ fetchFn = fetch,
73
+ extract = extractWithDitto,
74
+ run = execFileSync,
75
+ symlink = symlinkSync,
76
+ }) {
77
+ const base = releaseBase(release);
78
+ const zip = await download(`${base}/${release.asset}`, fetchFn);
79
+ if (sha256(zip) !== release.sha256)
80
+ throw new Error("zip 체크섬이 고정값과 다르다");
81
+ const sums = (await download(`${base}/SHA256SUMS`, fetchFn))
82
+ .toString("utf8")
83
+ .split(/\r?\n/)
84
+ .map((line) => line.trim().split(/\s+/));
85
+ const listed = sums.find(([, name]) => name === release.asset)?.[0];
86
+ if (listed !== release.sha256)
87
+ throw new Error("SHA256SUMS 의 값이 고정값과 다르다");
88
+
89
+ const work = mkdtempSync(join(tmpdir(), "tfx-tmr-"));
90
+ try {
91
+ const zipPath = join(work, release.asset);
92
+ const out = join(work, "out");
93
+ writeFileSync(zipPath, zip);
94
+ mkdirSync(out);
95
+ extract(zipPath, out);
96
+ const extracted = join(out, "tmuxrooms");
97
+ // zip 에는 실행 파일 하나만 있어야 한다. 다른 구성이면 받은 것을 쓰지 않는다.
98
+ const entries = readdirSync(out);
99
+ if (
100
+ entries.length !== 1 ||
101
+ entries[0] !== "tmuxrooms" ||
102
+ !lstatSync(extracted).isFile()
103
+ )
104
+ throw new Error("zip 구성이 예상과 다르다");
105
+
106
+ const binDir = tmrBinDir(home);
107
+ mkdirSync(binDir, { recursive: true, mode: 0o755 });
108
+ const target = join(binDir, "tmuxrooms");
109
+ // 임시 파일은 이번 시도만 쓰는 디렉터리에 둔다. 정리할 때 남의 파일을 지우지 않는다.
110
+ const staging = mkdtempSync(join(binDir, ".tfx-tmr-"));
111
+ try {
112
+ const temporary = join(staging, "tmuxrooms");
113
+ copyFileSync(extracted, temporary);
114
+ chmodSync(temporary, 0o755);
115
+ // 묻는 사이 다른 쪽이 tmuxrooms 를 만들었으면 덮지 않고 실패한다(EEXIST).
116
+ linkSync(temporary, target);
117
+ } finally {
118
+ rmSync(staging, { recursive: true, force: true });
119
+ }
120
+ const link = join(binDir, "tmr");
121
+ // 이미 있는 tmr 은 사용자 것일 수 있어 건드리지 않는다. 링크를 못 만들어도
122
+ // 체크섬이 맞는 tmuxrooms 는 완전한 설치본이라 지우지 않고 안내만 남긴다.
123
+ let linked = false;
124
+ try {
125
+ if (!lstatSync(link, { throwIfNoEntry: false })) {
126
+ symlink("tmuxrooms", link);
127
+ linked = true;
128
+ }
129
+ } catch {}
130
+
131
+ let runs = true;
132
+ try {
133
+ run(target, ["--help"], { stdio: "ignore", timeout: 10_000 });
134
+ } catch {
135
+ runs = false;
136
+ }
137
+ return { target, link, linked, runs };
138
+ } finally {
139
+ rmSync(work, { recursive: true, force: true });
140
+ }
141
+ }
142
+
143
+ /** setup 동의 단계. macOS arm64 에서 tmr 이 없을 때만 묻고, 물을 수 없으면 안내만 남긴다. */
144
+ export async function offerTmrInstall({
145
+ interactive,
146
+ ask,
147
+ home,
148
+ env = process.env,
149
+ platform = process.platform,
150
+ arch = process.arch,
151
+ install = installTmr,
152
+ log = console.log,
153
+ warn = console.warn,
154
+ notice = console.log,
155
+ }) {
156
+ if (!tmrSupported(platform, arch)) return "not-needed";
157
+ if (findTmr({ home, env })) return "present";
158
+ if (!interactive) {
159
+ notice("tmr 이 없다. tfx setup 에서 설치를 물을 수 있다.");
160
+ return "deferred";
161
+ }
162
+ if (!(await ask("tmr 을 설치할까요? (tmux 세션 목록 도구, ~/.local/bin)"))) {
163
+ notice("tmr 설치를 건너뜀.");
164
+ return "declined";
165
+ }
166
+ try {
167
+ const { target, link, linked, runs } = await install({ home });
168
+ log(`tmr ${TMR_RELEASE.version} 설치: ${target}`);
169
+ if (!linked && !lstatSync(link, { throwIfNoEntry: false }))
170
+ warn(`tmr 링크를 만들지 못했다. 직접 만든다: ln -s tmuxrooms "${link}"`);
171
+ if (!runs) warn(`설치한 ${target} 가 실행되지 않는다.`);
172
+ const binDir = tmrBinDir(home);
173
+ if (
174
+ !String(env.PATH || "")
175
+ .split(delimiter)
176
+ .includes(binDir)
177
+ )
178
+ warn(`${binDir} 가 PATH 에 없다. 셸 설정에 추가한다.`);
179
+ return "installed";
180
+ } catch (error) {
181
+ warn(`tmr 설치 실패: ${error?.message || error}`);
182
+ return "failed";
183
+ }
184
+ }