triflux 3.2.0-dev.6 → 3.2.0-dev.8

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/README.ko.md CHANGED
@@ -212,11 +212,13 @@ tfx doctor
212
212
  |--------|------|
213
213
  | `tfx setup` | 스크립트 + HUD + 스킬 동기화 |
214
214
  | `tfx doctor` | CLI 진단 + 이슈 추적 |
215
- | `tfx update` | 최신 버전으로 업데이트 |
216
- | `tfx list` | 설치된 스킬 목록 |
217
- | `tfx version` | 버전 표시 |
218
-
219
- 축약: `tfx` = `triflux`, `tfl` = `triflux`
215
+ | `tfx update` | 최신 안정 버전으로 업데이트 |
216
+ | `tfx list` | 설치된 스킬 목록 |
217
+ | `tfx version` | 버전 표시 |
218
+
219
+ 축약: `tfx` = `triflux`, `tfl` = `triflux`
220
+
221
+ dev 채널 업데이트: `tfx update --dev` (`dev` 별칭 지원)
220
222
 
221
223
  ### HUD 상태 표시줄
222
224
 
package/README.md CHANGED
@@ -212,11 +212,13 @@ tfx doctor
212
212
  |---------|-------------|
213
213
  | `tfx setup` | Sync scripts + HUD + skills |
214
214
  | `tfx doctor` | CLI diagnostics + issue tracker |
215
- | `tfx update` | Update to latest version |
216
- | `tfx list` | List installed skills |
217
- | `tfx version` | Show version info |
218
-
219
- Shortcuts: `tfx` = `triflux`, `tfl` = `triflux`
215
+ | `tfx update` | Update to latest stable version |
216
+ | `tfx list` | List installed skills |
217
+ | `tfx version` | Show version info |
218
+
219
+ Shortcuts: `tfx` = `triflux`, `tfl` = `triflux`
220
+
221
+ Dev channel update: `tfx update --dev` (`dev` alias supported)
220
222
 
221
223
  ### HUD Status Bar
222
224
 
package/bin/triflux.mjs CHANGED
@@ -3,9 +3,10 @@
3
3
  import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync } from "fs";
4
4
  import { join, dirname } from "path";
5
5
  import { homedir } from "os";
6
- import { execSync, spawn } from "child_process";
7
-
8
- const PKG_ROOT = dirname(dirname(new URL(import.meta.url).pathname)).replace(/^\/([A-Z]:)/, "$1");
6
+ import { execSync, spawn } from "child_process";
7
+ import { fileURLToPath } from "url";
8
+
9
+ const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
9
10
  const CLAUDE_DIR = join(homedir(), ".claude");
10
11
  const CODEX_DIR = join(homedir(), ".codex");
11
12
  const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
@@ -67,7 +68,7 @@ function which(cmd) {
67
68
  } catch { return null; }
68
69
  }
69
70
 
70
- function whichInShell(cmd, shell) {
71
+ function whichInShell(cmd, shell) {
71
72
  const cmds = {
72
73
  bash: `bash -c "source ~/.bashrc 2>/dev/null && command -v ${cmd} 2>/dev/null"`,
73
74
  cmd: `cmd /c where ${cmd} 2>nul`,
@@ -82,8 +83,12 @@ function whichInShell(cmd, shell) {
82
83
  stdio: ["pipe", "pipe", "ignore"],
83
84
  }).trim();
84
85
  return result.split(/\r?\n/)[0] || null;
85
- } catch { return null; }
86
- }
86
+ } catch { return null; }
87
+ }
88
+
89
+ function isDevUpdateRequested(argv = process.argv) {
90
+ return argv.includes("--dev") || argv.includes("@dev") || argv.includes("dev");
91
+ }
87
92
 
88
93
  function checkShellAvailable(shell) {
89
94
  const cmds = { bash: "bash --version", cmd: "cmd /c echo ok", pwsh: "pwsh -NoProfile -c echo ok" };
@@ -676,10 +681,10 @@ function cmdDoctor(options = {}) {
676
681
  }
677
682
  }
678
683
 
679
- function cmdUpdate() {
680
- const isDev = process.argv.includes("--dev");
681
- const tagLabel = isDev ? ` ${YELLOW}@dev${RESET}` : "";
682
- console.log(`\n${BOLD}triflux update${RESET}${tagLabel}\n`);
684
+ function cmdUpdate() {
685
+ const isDev = isDevUpdateRequested(process.argv);
686
+ const tagLabel = isDev ? ` ${YELLOW}--dev${RESET}` : "";
687
+ console.log(`\n${BOLD}triflux update${RESET}${tagLabel}\n`);
683
688
 
684
689
  // 1. 설치 방식 감지
685
690
  const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
@@ -731,12 +736,13 @@ function cmdUpdate() {
731
736
 
732
737
  info(`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`);
733
738
 
734
- // 2. 설치 방식에 따라 업데이트
735
- const oldVer = PKG.version;
736
- let updated = false;
737
-
738
- try {
739
- switch (installMode) {
739
+ // 2. 설치 방식에 따라 업데이트
740
+ const oldVer = PKG.version;
741
+ let updated = false;
742
+ let stoppedHubInfo = null;
743
+
744
+ try {
745
+ switch (installMode) {
740
746
  case "plugin": {
741
747
  const gitDir = pluginPath || PKG_ROOT;
742
748
  const result = execSync("git pull", {
@@ -747,15 +753,19 @@ function cmdUpdate() {
747
753
  ok(`git pull — ${result}`);
748
754
  updated = true;
749
755
  break;
750
- }
751
- case "npm-global": {
752
- const npmCmd = isDev ? "npm install -g triflux@dev" : "npm update -g triflux";
753
- const result = execSync(npmCmd, {
754
- encoding: "utf8",
756
+ }
757
+ case "npm-global": {
758
+ stoppedHubInfo = stopHubForUpdate();
759
+ if (stoppedHubInfo?.pid) {
760
+ info(`실행 중 hub 정지 (PID ${stoppedHubInfo.pid})`);
761
+ }
762
+ const npmCmd = isDev ? "npm install -g triflux@dev" : "npm update -g triflux";
763
+ const result = execSync(npmCmd, {
764
+ encoding: "utf8",
755
765
  timeout: 60000,
756
766
  stdio: ["pipe", "pipe", "ignore"],
757
767
  }).trim().split(/\r?\n/)[0];
758
- ok(`${isDev ? "npm install -g @dev" : "npm update -g"} — ${result || "완료"}`);
768
+ ok(`${isDev ? "npm install -g triflux@dev" : "npm update -g triflux"} — ${result || "완료"}`);
759
769
  updated = true;
760
770
  break;
761
771
  }
@@ -767,7 +777,7 @@ function cmdUpdate() {
767
777
  cwd: process.cwd(),
768
778
  stdio: ["pipe", "pipe", "ignore"],
769
779
  }).trim().split(/\r?\n/)[0];
770
- ok(`npm update — ${result || "완료"}`);
780
+ ok(`${isDev ? "npm install triflux@dev" : "npm update triflux"} — ${result || "완료"}`);
771
781
  updated = true;
772
782
  break;
773
783
  }
@@ -785,11 +795,14 @@ function cmdUpdate() {
785
795
  fail("설치 방식을 감지할 수 없음");
786
796
  info("수동 업데이트: cd <triflux-dir> && git pull");
787
797
  return;
788
- }
789
- } catch (e) {
790
- fail(`업데이트 실패: ${e.message}`);
791
- return;
792
- }
798
+ }
799
+ } catch (e) {
800
+ if (stoppedHubInfo && startHubAfterUpdate(stoppedHubInfo)) {
801
+ info("업데이트 실패 후 hub 재기동 시도");
802
+ }
803
+ fail(`업데이트 실패: ${e.message}`);
804
+ return;
805
+ }
793
806
 
794
807
  // 3. setup 재실행 (tfx-route.sh, HUD, 스킬 동기화)
795
808
  if (updated) {
@@ -807,11 +820,16 @@ function cmdUpdate() {
807
820
  ok(`버전: v${oldVer} (이미 최신)`);
808
821
  }
809
822
 
810
- // setup 재실행
811
- console.log("");
812
- info("setup 재실행 중...");
813
- cmdSetup();
814
- }
823
+ // setup 재실행
824
+ console.log("");
825
+ info("setup 재실행 중...");
826
+ cmdSetup();
827
+
828
+ if (stoppedHubInfo) {
829
+ if (startHubAfterUpdate(stoppedHubInfo)) info("hub 재기동 완료");
830
+ else warn("hub 재기동 실패 — `tfx hub start`로 수동 시작 필요");
831
+ }
832
+ }
815
833
 
816
834
  console.log(`${GREEN}${BOLD}업데이트 완료${RESET}\n`);
817
835
  }
@@ -914,8 +932,8 @@ ${updateNotice}
914
932
  ${WHITE_BRIGHT}tfx doctor${RESET} ${GRAY}CLI 진단 + 이슈 확인${RESET}
915
933
  ${DIM} --fix${RESET} ${GRAY}진단 + 자동 수정${RESET}
916
934
  ${DIM} --reset${RESET} ${GRAY}캐시 전체 초기화${RESET}
917
- ${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 버전으로 업데이트${RESET}
918
- ${DIM} --dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
935
+ ${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
936
+ ${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
919
937
  ${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
920
938
  ${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
921
939
  ${WHITE_BRIGHT}tfx team${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
@@ -989,8 +1007,60 @@ async function cmdCodexTeam() {
989
1007
 
990
1008
  // ── hub 서브커맨드 ──
991
1009
 
992
- const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
993
- const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
1010
+ const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
1011
+ const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
1012
+
1013
+ function sleepMs(ms) {
1014
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1015
+ }
1016
+
1017
+ function stopHubForUpdate() {
1018
+ if (!existsSync(HUB_PID_FILE)) return null;
1019
+ let info = null;
1020
+ try {
1021
+ info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
1022
+ process.kill(info.pid, 0);
1023
+ } catch {
1024
+ try { unlinkSync(HUB_PID_FILE); } catch {}
1025
+ return null;
1026
+ }
1027
+
1028
+ try {
1029
+ if (process.platform === "win32") {
1030
+ execSync(`taskkill /PID ${info.pid} /T /F`, {
1031
+ stdio: ["pipe", "pipe", "ignore"],
1032
+ timeout: 10000,
1033
+ });
1034
+ } else {
1035
+ process.kill(info.pid, "SIGTERM");
1036
+ }
1037
+ } catch {
1038
+ try { process.kill(info.pid, "SIGKILL"); } catch {}
1039
+ }
1040
+
1041
+ sleepMs(300);
1042
+ try { unlinkSync(HUB_PID_FILE); } catch {}
1043
+ return info;
1044
+ }
1045
+
1046
+ function startHubAfterUpdate(info) {
1047
+ if (!info) return false;
1048
+ const serverPath = join(PKG_ROOT, "hub", "server.mjs");
1049
+ if (!existsSync(serverPath)) return false;
1050
+ const port = Number(info?.port) > 0 ? String(info.port) : String(process.env.TFX_HUB_PORT || "27888");
1051
+
1052
+ try {
1053
+ const child = spawn(process.execPath, [serverPath], {
1054
+ env: { ...process.env, TFX_HUB_PORT: port },
1055
+ stdio: "ignore",
1056
+ detached: true,
1057
+ });
1058
+ child.unref();
1059
+ return true;
1060
+ } catch {
1061
+ return false;
1062
+ }
1063
+ }
994
1064
 
995
1065
  // 설치된 CLI에 tfx-hub MCP 서버 자동 등록 (1회 설정, 이후 재실행 불필요)
996
1066
  function autoRegisterMcp(mcpUrl) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "3.2.0-dev.6",
3
+ "version": "3.2.0-dev.8",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,9 +7,10 @@ import { existsSync, readFileSync } from "fs";
7
7
  import { join, dirname } from "path";
8
8
  import { homedir } from "os";
9
9
  import { spawn } from "child_process";
10
+ import { fileURLToPath } from "url";
10
11
 
11
12
  const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
12
- const PLUGIN_ROOT = dirname(dirname(new URL(import.meta.url).pathname)).replace(/^\/([A-Z]:)/, "$1");
13
+ const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
13
14
  const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
14
15
 
15
16
  function formatHostForUrl(host) {
package/scripts/setup.mjs CHANGED
@@ -8,8 +8,9 @@ import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, exis
8
8
  import { join, dirname } from "path";
9
9
  import { homedir } from "os";
10
10
  import { spawn } from "child_process";
11
-
12
- const PLUGIN_ROOT = dirname(dirname(new URL(import.meta.url).pathname)).replace(/^\/([A-Z]:)/, "$1");
11
+ import { fileURLToPath } from "url";
12
+
13
+ const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
13
14
  const CLAUDE_DIR = join(homedir(), ".claude");
14
15
  const CODEX_DIR = join(homedir(), ".codex");
15
16
  const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
@@ -51,15 +52,24 @@ const SYNC_MAP = [
51
52
  },
52
53
  ];
53
54
 
54
- function getVersion(filePath) {
55
- try {
56
- const content = readFileSync(filePath, "utf8");
57
- const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
58
- return match ? match[1] : null;
59
- } catch {
60
- return null;
61
- }
62
- }
55
+ function getVersion(filePath) {
56
+ try {
57
+ const content = readFileSync(filePath, "utf8");
58
+ const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
59
+ return match ? match[1] : null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function shouldSyncTextFile(src, dst) {
66
+ if (!existsSync(dst)) return true;
67
+ try {
68
+ return readFileSync(src, "utf8") !== readFileSync(dst, "utf8");
69
+ } catch {
70
+ return true;
71
+ }
72
+ }
63
73
 
64
74
  function escapeRegExp(value) {
65
75
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -114,15 +124,13 @@ for (const { src, dst, label } of SYNC_MAP) {
114
124
  copyFileSync(src, dst);
115
125
  try { chmodSync(dst, 0o755); } catch {}
116
126
  synced++;
117
- } else {
118
- const srcVersion = getVersion(src);
119
- const dstVersion = getVersion(dst);
120
- if (srcVersion && dstVersion && srcVersion !== dstVersion) {
121
- copyFileSync(src, dst);
122
- try { chmodSync(dst, 0o755); } catch {}
123
- synced++;
124
- }
125
- }
127
+ } else {
128
+ if (shouldSyncTextFile(src, dst)) {
129
+ copyFileSync(src, dst);
130
+ try { chmodSync(dst, 0o755); } catch {}
131
+ synced++;
132
+ }
133
+ }
126
134
  }
127
135
 
128
136
  // ── 스킬 동기화 ──
@@ -349,11 +357,12 @@ ${B}╚════════════════════════
349
357
  ${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
350
358
  ${G}✓${R} HUD statusLine → settings.json
351
359
 
352
- ${B}Commands:${R}
353
- ${C}triflux${R} setup 파일 동기화 + HUD 설정
354
- ${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
355
- ${C}triflux${R} list 설치된 스킬 목록
356
- ${C}triflux${R} update 최신 버전으로 업데이트
360
+ ${B}Commands:${R}
361
+ ${C}triflux${R} setup 파일 동기화 + HUD 설정
362
+ ${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
363
+ ${C}triflux${R} list 설치된 스킬 목록
364
+ ${C}triflux${R} update 최신 안정 버전으로 업데이트
365
+ ${C}triflux${R} update --dev dev 채널로 업데이트 (${D}dev 별칭 지원${R})
357
366
 
358
367
  ${B}Shortcuts:${R}
359
368
  ${C}tfx${R} triflux 축약