triflux 9.7.7 → 9.7.9
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +2 -2
- package/hooks/pipeline-stop.mjs +2 -2
- package/hub/team/ansi.mjs +1 -0
- package/hub/team/cli/commands/start/index.mjs +1 -0
- package/hub/team/cli/commands/start/parse-args.mjs +1 -1
- package/hub/team/cli/help.mjs +1 -0
- package/hub/team/dashboard-open.mjs +150 -0
- package/hub/team/tui-lite.mjs +109 -5
- package/hub/team/tui-viewer.mjs +14 -0
- package/package.json +1 -1
- package/scripts/cache-warmup.mjs +47 -5
- package/scripts/lib/env-probe.mjs +36 -6
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "triflux",
|
|
11
11
|
"description": "CLI-first multi-model orchestrator for Claude Code. Routes tasks to Codex, Gemini, and Claude CLIs with automatic triage, DAG-based parallel execution, headless psmux sessions, and cost-optimized routing. Includes 41 skills, HUD status bar, hook orchestrator, and shell-based CLI routing.",
|
|
12
|
-
"version": "9.7.
|
|
12
|
+
"version": "9.7.9",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "tellang"
|
|
15
15
|
},
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
]
|
|
31
31
|
}
|
|
32
32
|
],
|
|
33
|
-
"version": "9.7.
|
|
33
|
+
"version": "9.7.9"
|
|
34
34
|
}
|
package/bin/triflux.mjs
CHANGED
|
@@ -160,12 +160,12 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
160
160
|
},
|
|
161
161
|
},
|
|
162
162
|
multi: {
|
|
163
|
-
usage: "tfx multi [--dashboard-layout single|split-2col|split-3col|auto] <subcommand|task>",
|
|
163
|
+
usage: "tfx multi [--dashboard-layout lite|single|split-2col|split-3col|auto] <subcommand|task>",
|
|
164
164
|
description: "멀티-CLI 팀 모드",
|
|
165
165
|
options: [
|
|
166
166
|
{ name: "--dashboard", type: "boolean", description: "headless dashboard viewer 표시 (기본값: 켜짐)" },
|
|
167
167
|
{ name: "--no-dashboard", type: "boolean", description: "headless dashboard viewer 비활성화" },
|
|
168
|
-
{ name: "--dashboard-layout", type: "string", description: "dashboard viewer 레이아웃 선택: single|split-2col|split-3col|auto" },
|
|
168
|
+
{ name: "--dashboard-layout", type: "string", description: "dashboard viewer 레이아웃 선택: lite|single|split-2col|split-3col|auto" },
|
|
169
169
|
],
|
|
170
170
|
subcommands: {
|
|
171
171
|
status: {
|
package/hooks/pipeline-stop.mjs
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
// 파이프라인이 없으면 정상 종료를 허용한다.
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { PLUGIN_ROOT } from "./lib/resolve-root.mjs";
|
|
10
9
|
|
|
11
10
|
let getPipelineStateDbPath;
|
|
12
11
|
let ensurePipelineTable;
|
|
@@ -22,7 +21,8 @@ try {
|
|
|
22
21
|
process.exit(0);
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
const
|
|
24
|
+
const PROJECT_ROOT = process.env.CLAUDE_CWD || process.cwd();
|
|
25
|
+
const HUB_DB_PATH = getPipelineStateDbPath(PROJECT_ROOT);
|
|
26
26
|
const TERMINAL = new Set(["complete", "failed"]);
|
|
27
27
|
|
|
28
28
|
async function checkActivePipelines() {
|
package/hub/team/ansi.mjs
CHANGED
|
@@ -22,6 +22,7 @@ export function moveDown(n = 1) { return `${ESC}[${n}B`; }
|
|
|
22
22
|
// ── 줄 제어 ──
|
|
23
23
|
export const clearLine = `${ESC}[2K`;
|
|
24
24
|
export const clearToEnd = `${ESC}[K`;
|
|
25
|
+
export const eraseBelow = `${ESC}[J`;
|
|
25
26
|
|
|
26
27
|
// ── 색상 (triflux 디자인 시스템) ──
|
|
27
28
|
export const RESET = `${ESC}[0m`;
|
|
@@ -16,6 +16,7 @@ function printStartUsage() {
|
|
|
16
16
|
console.log(` 사용법: ${WHITE}tfx multi "작업 설명"${RESET}`);
|
|
17
17
|
console.log(` ${WHITE}tfx multi --agents codex,gemini --lead claude "작업"${RESET}`);
|
|
18
18
|
console.log(` ${WHITE}tfx multi --teammate-mode headless "작업"${RESET} ${DIM}(psmux 헤드리스, 기본)${RESET}`);
|
|
19
|
+
console.log(` ${WHITE}tfx multi --dashboard-layout lite "작업"${RESET} ${DIM}(dashboard-lite 기본 뷰)${RESET}`);
|
|
19
20
|
console.log(` ${WHITE}tfx multi --dashboard-layout auto "작업"${RESET} ${DIM}(dashboard viewer 레이아웃 자동)${RESET}`);
|
|
20
21
|
console.log(` ${WHITE}tfx multi --dashboard-anchor window "작업"${RESET} ${DIM}(dashboard anchor: window|tab, 기본 window)${RESET}`);
|
|
21
22
|
console.log(` ${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}`);
|
|
@@ -50,7 +50,7 @@ export function parseTeamArgs(args = []) {
|
|
|
50
50
|
let timeoutSec = 300;
|
|
51
51
|
let verbose = false;
|
|
52
52
|
let dashboard = true;
|
|
53
|
-
let dashboardLayout = "
|
|
53
|
+
let dashboardLayout = "lite";
|
|
54
54
|
let dashboardSize = 0.40;
|
|
55
55
|
let dashboardAnchor = "window";
|
|
56
56
|
let mcpProfile = "";
|
package/hub/team/cli/help.mjs
CHANGED
|
@@ -11,6 +11,7 @@ export function renderTeamHelp() {
|
|
|
11
11
|
${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}
|
|
12
12
|
${WHITE}tfx multi --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
|
|
13
13
|
${WHITE}tfx multi --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
|
|
14
|
+
${WHITE}tfx multi --dashboard-layout lite "작업"${RESET} ${DIM}(dashboard-lite 기본 뷰)${RESET}
|
|
14
15
|
${WHITE}tfx multi --dashboard-layout auto "작업"${RESET} ${DIM}(dashboard viewer 레이아웃 자동 결정)${RESET}
|
|
15
16
|
${WHITE}tfx multi --dashboard-size 0.4 "작업"${RESET} ${DIM}(대시보드 분할 비율 0.2~0.8, 기본 0.50)${RESET}
|
|
16
17
|
${WHITE}tfx multi --dashboard-anchor window "작업"${RESET} ${DIM}(대시보드 고정 위치: window|tab, 기본 window)${RESET}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { psmuxExec } from "./psmux.mjs";
|
|
4
|
+
import {
|
|
5
|
+
detectMultiplexer,
|
|
6
|
+
focusWtPane,
|
|
7
|
+
hasWindowsTerminal,
|
|
8
|
+
resolveAttachCommand,
|
|
9
|
+
tmuxExec,
|
|
10
|
+
} from "./session.mjs";
|
|
11
|
+
|
|
12
|
+
function sanitizeWindowTitle(value, fallback = "triflux") {
|
|
13
|
+
const text = String(value || "").replace(/[\r\n]+/g, " ").trim();
|
|
14
|
+
return text || fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sanitizeSessionName(value) {
|
|
18
|
+
return String(value || "").replace(/[^a-zA-Z0-9_\-]/g, "") || "tfx-session";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sanitizeWorkingDirectory(value) {
|
|
22
|
+
const text = String(value || "").replace(/[\r\n\x00-\x1f]/g, "").trim();
|
|
23
|
+
return text || process.cwd();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseWorkerNumber(value) {
|
|
27
|
+
const text = String(value || "").trim();
|
|
28
|
+
const workerMatch = text.match(/^worker-(\d+)$/i);
|
|
29
|
+
if (workerMatch) return Number.parseInt(workerMatch[1], 10);
|
|
30
|
+
const paneMatch = text.match(/:(\d+)$/);
|
|
31
|
+
if (paneMatch) return Number.parseInt(paneMatch[1], 10);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function decideDashboardOpenMode({ openAll = false, hasWtSession = !!process.env.WT_SESSION } = {}) {
|
|
36
|
+
if (openAll) return hasWtSession ? "tab" : "window";
|
|
37
|
+
return hasWtSession ? "split" : "window";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function spawnWindowsTerminal(spec, opts = {}) {
|
|
41
|
+
if (!hasWindowsTerminal()) return false;
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
mode = "window",
|
|
45
|
+
title = "triflux",
|
|
46
|
+
cwd = process.cwd(),
|
|
47
|
+
split = { orientation: "H", size: 0.50 },
|
|
48
|
+
} = opts;
|
|
49
|
+
|
|
50
|
+
const safeTitle = sanitizeWindowTitle(title);
|
|
51
|
+
const safeCwd = sanitizeWorkingDirectory(cwd);
|
|
52
|
+
const orientation = split?.orientation === "V" ? "V" : "H";
|
|
53
|
+
const size = Number.isFinite(split?.size) ? Math.min(0.8, Math.max(0.2, split.size)) : 0.50;
|
|
54
|
+
const baseArgs = ["--profile", "triflux", "--title", safeTitle, "-d", safeCwd, "--", spec.command, ...spec.args];
|
|
55
|
+
const args = mode === "split"
|
|
56
|
+
? ["-w", "0", "sp", `-${orientation}`, "-s", String(size), ...baseArgs]
|
|
57
|
+
: mode === "tab"
|
|
58
|
+
? ["-w", "0", "nt", ...baseArgs]
|
|
59
|
+
: ["-w", "new", ...baseArgs];
|
|
60
|
+
|
|
61
|
+
const child = spawn("wt.exe", args, {
|
|
62
|
+
detached: true,
|
|
63
|
+
stdio: "ignore",
|
|
64
|
+
windowsHide: false,
|
|
65
|
+
});
|
|
66
|
+
child.unref();
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function focusManagedPane(target, opts = {}) {
|
|
71
|
+
const { teammateMode = "", layout = "1xN" } = opts;
|
|
72
|
+
const paneRef = String(target || "");
|
|
73
|
+
|
|
74
|
+
if (teammateMode === "wt" || paneRef.startsWith("wt:")) {
|
|
75
|
+
const paneIndex = parseWorkerNumber(paneRef);
|
|
76
|
+
return paneIndex != null && focusWtPane(paneIndex, { layout });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!paneRef) return false;
|
|
80
|
+
try {
|
|
81
|
+
if (detectMultiplexer() === "psmux") psmuxExec(["select-pane", "-t", paneRef]);
|
|
82
|
+
else tmuxExec(`select-pane -t ${paneRef}`);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function openHeadlessDashboardTarget(sessionName, opts = {}) {
|
|
90
|
+
const {
|
|
91
|
+
worker = null,
|
|
92
|
+
openAll = false,
|
|
93
|
+
cwd = process.cwd(),
|
|
94
|
+
title,
|
|
95
|
+
} = opts;
|
|
96
|
+
|
|
97
|
+
const safeSession = sanitizeSessionName(sessionName);
|
|
98
|
+
const workerNumber = worker == null ? null : parseWorkerNumber(worker);
|
|
99
|
+
|
|
100
|
+
if (!openAll && workerNumber != null) {
|
|
101
|
+
try {
|
|
102
|
+
psmuxExec(["select-pane", "-t", `${safeSession}:0.${workerNumber}`]);
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return spawnWindowsTerminal(
|
|
107
|
+
{ command: "psmux", args: ["attach-session", "-t", safeSession] },
|
|
108
|
+
{
|
|
109
|
+
mode: decideDashboardOpenMode({ openAll }),
|
|
110
|
+
title: title || (openAll ? `▲ ${safeSession}` : `▲ ${safeSession}:${workerNumber ?? "all"}`),
|
|
111
|
+
cwd,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function openDashboardRuntimeTarget(runtime, opts = {}) {
|
|
117
|
+
const {
|
|
118
|
+
teammateMode = "",
|
|
119
|
+
sessionName = "",
|
|
120
|
+
targetPane = "",
|
|
121
|
+
layout = "1xN",
|
|
122
|
+
openAll = false,
|
|
123
|
+
cwd = process.cwd(),
|
|
124
|
+
title = "",
|
|
125
|
+
} = { ...runtime, ...opts };
|
|
126
|
+
|
|
127
|
+
if (teammateMode === "headless") {
|
|
128
|
+
return openHeadlessDashboardTarget(sessionName, {
|
|
129
|
+
worker: openAll ? null : targetPane,
|
|
130
|
+
openAll,
|
|
131
|
+
cwd,
|
|
132
|
+
title,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if ((teammateMode === "wt" || String(targetPane).startsWith("wt:")) && !openAll) {
|
|
137
|
+
return focusManagedPane(targetPane, { teammateMode: "wt", layout });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (!openAll && targetPane) focusManagedPane(targetPane, { teammateMode, layout });
|
|
142
|
+
return spawnWindowsTerminal(resolveAttachCommand(sessionName), {
|
|
143
|
+
mode: decideDashboardOpenMode({ openAll }),
|
|
144
|
+
title: title || `▲ ${sanitizeSessionName(sessionName)}`,
|
|
145
|
+
cwd,
|
|
146
|
+
});
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
package/hub/team/tui-lite.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { altScreenOff, altScreenOn, BG, bold, box, clearScreen, color, cursorHide, cursorHome, cursorShow, dim, FG, MOCHA, padRight, progressBar, statusBadge, stripAnsi, truncate, wcswidth } from "./ansi.mjs";
|
|
1
|
+
import { altScreenOff, altScreenOn, BG, bold, box, clearScreen, clearToEnd, color, cursorHide, cursorHome, cursorShow, dim, eraseBelow, FG, MOCHA, padRight, progressBar, statusBadge, stripAnsi, truncate, wcswidth } from "./ansi.mjs";
|
|
2
2
|
|
|
3
3
|
const FALLBACK_COLUMNS = 100, FALLBACK_ROWS = 24;
|
|
4
4
|
const VALID_TABS = new Set(["log", "detail", "files"]);
|
|
@@ -154,8 +154,11 @@ function buildDetail(workerName, worker, width, tab, helpVisible) {
|
|
|
154
154
|
return frame(
|
|
155
155
|
[
|
|
156
156
|
bold("tui-lite"),
|
|
157
|
-
"
|
|
158
|
-
"
|
|
157
|
+
"j/k or arrows: worker selection",
|
|
158
|
+
"Enter: open selected worker",
|
|
159
|
+
"Shift+Enter: open all workers",
|
|
160
|
+
"l: tabs log/detail/files",
|
|
161
|
+
"1-9: direct select, q: quit",
|
|
159
162
|
"toggleDetail(false) 로 상세 패널 숨김",
|
|
160
163
|
],
|
|
161
164
|
width,
|
|
@@ -185,11 +188,14 @@ function buildDetail(workerName, worker, width, tab, helpVisible) {
|
|
|
185
188
|
export function createLiteDashboard(opts = {}) {
|
|
186
189
|
const {
|
|
187
190
|
stream = process.stdout,
|
|
191
|
+
input = process.stdin,
|
|
188
192
|
refreshMs = 1000,
|
|
189
193
|
columns,
|
|
190
194
|
rows,
|
|
191
195
|
layout = "auto",
|
|
192
196
|
forceTTY = false,
|
|
197
|
+
onOpenSelectedWorker,
|
|
198
|
+
onOpenAllWorkers,
|
|
193
199
|
} = opts;
|
|
194
200
|
|
|
195
201
|
const isTTY = forceTTY || !!stream?.isTTY;
|
|
@@ -203,6 +209,8 @@ export function createLiteDashboard(opts = {}) {
|
|
|
203
209
|
let detailExpanded = true;
|
|
204
210
|
let focusTab = "log";
|
|
205
211
|
let helpVisible = false;
|
|
212
|
+
let inputAttached = false;
|
|
213
|
+
let rawModeEnabled = false;
|
|
206
214
|
|
|
207
215
|
const write = (text) => { if (!closed) stream.write(text); };
|
|
208
216
|
const workerNames = () => [...workers.keys()].sort();
|
|
@@ -210,6 +218,95 @@ export function createLiteDashboard(opts = {}) {
|
|
|
210
218
|
const viewportRows = () => Math.max(10, rows || stream?.rows || process.stdout?.rows || FALLBACK_ROWS);
|
|
211
219
|
const ensureSelection = (names) => { if (names.length && (!selectedWorker || !workers.has(selectedWorker))) selectedWorker = names[0]; };
|
|
212
220
|
|
|
221
|
+
function selectRelative(offset) {
|
|
222
|
+
const names = workerNames();
|
|
223
|
+
if (names.length === 0) return;
|
|
224
|
+
ensureSelection(names);
|
|
225
|
+
const idx = Math.max(0, names.indexOf(selectedWorker));
|
|
226
|
+
selectedWorker = names[(idx + offset + names.length) % names.length];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function triggerOpenSelected() {
|
|
230
|
+
if (typeof onOpenSelectedWorker !== "function" || !selectedWorker || !workers.has(selectedWorker)) return;
|
|
231
|
+
try {
|
|
232
|
+
const result = onOpenSelectedWorker(selectedWorker, workers.get(selectedWorker), new Map(workers));
|
|
233
|
+
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function triggerOpenAll() {
|
|
238
|
+
if (typeof onOpenAllWorkers !== "function") return;
|
|
239
|
+
try {
|
|
240
|
+
const result = onOpenAllWorkers(selectedWorker, workers.get(selectedWorker), new Map(workers));
|
|
241
|
+
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function handleInput(chunk) {
|
|
246
|
+
const key = String(chunk);
|
|
247
|
+
if (key === "\u0003") return;
|
|
248
|
+
|
|
249
|
+
if (helpVisible) {
|
|
250
|
+
helpVisible = false;
|
|
251
|
+
render();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (key === "j" || key === "\u001b[B") {
|
|
256
|
+
selectRelative(1);
|
|
257
|
+
render();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (key === "k" || key === "\u001b[A") {
|
|
261
|
+
selectRelative(-1);
|
|
262
|
+
render();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (key === "\r" || key === "\n") {
|
|
266
|
+
triggerOpenSelected();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (key === "\x1b[13;2u" || key === "\x1b[27;13;2~" || key === "\x1b\r" || key === "\x1b\n") {
|
|
270
|
+
triggerOpenAll();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (key === "l") {
|
|
274
|
+
const tabs = ["log", "detail", "files"];
|
|
275
|
+
focusTab = tabs[(tabs.indexOf(focusTab) + 1) % tabs.length];
|
|
276
|
+
render();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (key === "h" || key === "?") {
|
|
280
|
+
helpVisible = true;
|
|
281
|
+
render();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (key === "q") {
|
|
285
|
+
close();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (/^[1-9]$/.test(key)) {
|
|
289
|
+
const names = workerNames();
|
|
290
|
+
const target = names[Number.parseInt(key, 10) - 1];
|
|
291
|
+
if (target) {
|
|
292
|
+
selectedWorker = target;
|
|
293
|
+
render();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function attachInput() {
|
|
299
|
+
if (inputAttached) return;
|
|
300
|
+
if (!isTTY || !input?.isTTY || typeof input?.on !== "function") return;
|
|
301
|
+
inputAttached = true;
|
|
302
|
+
if (typeof input.setRawMode === "function") {
|
|
303
|
+
input.setRawMode(true);
|
|
304
|
+
rawModeEnabled = true;
|
|
305
|
+
}
|
|
306
|
+
if (typeof input.resume === "function") input.resume();
|
|
307
|
+
input.on("data", handleInput);
|
|
308
|
+
}
|
|
309
|
+
|
|
213
310
|
function buildRows() {
|
|
214
311
|
const names = workerNames();
|
|
215
312
|
ensureSelection(names);
|
|
@@ -235,15 +332,22 @@ export function createLiteDashboard(opts = {}) {
|
|
|
235
332
|
|
|
236
333
|
function render() {
|
|
237
334
|
if (closed) return;
|
|
335
|
+
attachInput();
|
|
238
336
|
frameCount++;
|
|
239
337
|
const rowsOut = buildRows();
|
|
240
|
-
if (isTTY)
|
|
241
|
-
|
|
338
|
+
if (isTTY) {
|
|
339
|
+
const width = viewportColumns();
|
|
340
|
+
const padded = rowsOut.map((line) => padRight(String(line ?? ""), width) + clearToEnd);
|
|
341
|
+
write(cursorHome + padded.join("\n") + eraseBelow);
|
|
342
|
+
} else write(`${rowsOut.join("\n")}\n`);
|
|
242
343
|
}
|
|
243
344
|
|
|
244
345
|
function close() {
|
|
245
346
|
if (closed) return;
|
|
246
347
|
if (timer) clearInterval(timer);
|
|
348
|
+
if (inputAttached && typeof input?.off === "function") input.off("data", handleInput);
|
|
349
|
+
if (rawModeEnabled && typeof input?.setRawMode === "function") input.setRawMode(false);
|
|
350
|
+
if (inputAttached && typeof input?.pause === "function") input.pause();
|
|
247
351
|
if (isTTY) write(cursorShow + altScreenOff);
|
|
248
352
|
closed = true;
|
|
249
353
|
}
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { createLogDashboard } from "./tui.mjs";
|
|
11
11
|
import { createLiteDashboard } from "./tui-lite.mjs";
|
|
12
|
+
import { openHeadlessDashboardTarget } from "./dashboard-open.mjs";
|
|
12
13
|
import { processHandoff } from "./handoff.mjs";
|
|
13
14
|
import { statusBadge } from "./ansi.mjs";
|
|
14
15
|
|
|
@@ -53,6 +54,15 @@ const tui = tuiFactory({
|
|
|
53
54
|
input: process.stdin,
|
|
54
55
|
columns: process.stdout.columns || parseInt(process.env.COLUMNS, 10) || 120,
|
|
55
56
|
layout: LAYOUT,
|
|
57
|
+
onOpenSelectedWorker: (workerName) => openHeadlessDashboardTarget(SESSION, {
|
|
58
|
+
worker: workerName,
|
|
59
|
+
openAll: false,
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
}),
|
|
62
|
+
onOpenAllWorkers: () => openHeadlessDashboardTarget(SESSION, {
|
|
63
|
+
openAll: true,
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
}),
|
|
56
66
|
});
|
|
57
67
|
const startTime = Date.now();
|
|
58
68
|
tui.setStartTime(startTime);
|
|
@@ -221,6 +231,8 @@ function makeWorkerState(paneIdx) {
|
|
|
221
231
|
handoff: null,
|
|
222
232
|
progress: 0,
|
|
223
233
|
activityAt: Date.now(),
|
|
234
|
+
title: "",
|
|
235
|
+
cli: "codex",
|
|
224
236
|
};
|
|
225
237
|
}
|
|
226
238
|
|
|
@@ -278,6 +290,8 @@ function ingest() {
|
|
|
278
290
|
let cli = "codex";
|
|
279
291
|
if (pane.title.includes("gemini") || pane.title.includes("🔵")) cli = "gemini";
|
|
280
292
|
else if (pane.title.includes("claude") || pane.title.includes("🟠")) cli = "claude";
|
|
293
|
+
ws.title = pane.title;
|
|
294
|
+
ws.cli = cli;
|
|
281
295
|
|
|
282
296
|
const resultData = checkResultFile(paneName);
|
|
283
297
|
if (resultData?.processed && !resultData.processed.fallback) {
|
package/package.json
CHANGED
package/scripts/cache-warmup.mjs
CHANGED
|
@@ -14,10 +14,12 @@ import { homedir } from "node:os";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
|
|
16
16
|
import { readPreflightCache } from "./preflight-cache.mjs";
|
|
17
|
-
import { checkCli, checkHub,
|
|
17
|
+
import { checkCli, checkHub, detectCodexAuthState } from "./lib/env-probe.mjs";
|
|
18
18
|
import { SEARCH_SERVER_ORDER, MCP_SERVER_DOMAIN_TAGS } from "./lib/mcp-server-catalog.mjs";
|
|
19
19
|
|
|
20
20
|
export const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
21
|
+
const WARMUP_METADATA_FILE = ["state", "warmup-metadata.json"];
|
|
22
|
+
const AUTH_SENSITIVE_TARGETS = new Set(["codexSkills", "tierEnvironment", "searchEngines"]);
|
|
21
23
|
|
|
22
24
|
export const CACHE_TARGETS = Object.freeze({
|
|
23
25
|
codexSkills: Object.freeze({
|
|
@@ -83,6 +85,21 @@ export function resolveTargetPath(target, { cwd = process.cwd() } = {}) {
|
|
|
83
85
|
return join(omcDir, ...spec.file);
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
function resolveWarmupMetadataPath({ cwd = process.cwd() } = {}) {
|
|
89
|
+
const { omcDir } = resolveRootDirs(cwd);
|
|
90
|
+
return join(omcDir, ...WARMUP_METADATA_FILE);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readWarmupMetadata(options = {}) {
|
|
94
|
+
const metadataPath = resolveWarmupMetadataPath(options);
|
|
95
|
+
if (!existsSync(metadataPath)) return null;
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
function resolveTtlMs(target, options = {}) {
|
|
87
104
|
if (Number.isFinite(options.ttlByTarget?.[target])) {
|
|
88
105
|
return Math.max(0, Math.trunc(options.ttlByTarget[target]));
|
|
@@ -188,6 +205,7 @@ export function probeTierEnvironment(options = {}) {
|
|
|
188
205
|
const homeDir = resolveHomeDir(options.homeDir);
|
|
189
206
|
const preflight = options.preflight ?? readPreflightCache();
|
|
190
207
|
const execSyncFn = options.execSyncFn || execSync;
|
|
208
|
+
const codexAuth = preflight?.codex_plan ?? detectCodexAuthState({ homeDir });
|
|
191
209
|
|
|
192
210
|
const codexCheck = preflight?.codex || checkCli("codex", { execSyncFn });
|
|
193
211
|
const geminiCheck = preflight?.gemini || checkCli("gemini", { execSyncFn });
|
|
@@ -198,8 +216,6 @@ export function probeTierEnvironment(options = {}) {
|
|
|
198
216
|
pollAttempts: options.hubRestart === true ? 8 : 0,
|
|
199
217
|
execSyncFn,
|
|
200
218
|
});
|
|
201
|
-
const codexPlan = preflight?.codex_plan || detectCodexPlan({ homeDir });
|
|
202
|
-
|
|
203
219
|
const checks = {
|
|
204
220
|
psmux: false,
|
|
205
221
|
hub: !!hubCheck?.ok,
|
|
@@ -241,7 +257,9 @@ export function probeTierEnvironment(options = {}) {
|
|
|
241
257
|
tier,
|
|
242
258
|
checks,
|
|
243
259
|
available_agents: agents,
|
|
244
|
-
codex_plan:
|
|
260
|
+
codex_plan: codexAuth.source == null
|
|
261
|
+
? { plan: codexAuth.plan }
|
|
262
|
+
: { plan: codexAuth.plan, source: codexAuth.source },
|
|
245
263
|
source: {
|
|
246
264
|
preflight: !!preflight,
|
|
247
265
|
home_dir: homeDir,
|
|
@@ -250,6 +268,21 @@ export function probeTierEnvironment(options = {}) {
|
|
|
250
268
|
};
|
|
251
269
|
}
|
|
252
270
|
|
|
271
|
+
function getCodexAuthFingerprint(options = {}) {
|
|
272
|
+
if (typeof options.preflight?.codex_plan?.fingerprint === "string") {
|
|
273
|
+
return options.preflight.codex_plan.fingerprint;
|
|
274
|
+
}
|
|
275
|
+
return detectCodexAuthState({ homeDir: resolveHomeDir(options.homeDir) }).fingerprint;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function hasAuthFingerprintChanged(target, options = {}) {
|
|
279
|
+
if (!AUTH_SENSITIVE_TARGETS.has(target)) return false;
|
|
280
|
+
const nextFingerprint = getCodexAuthFingerprint(options);
|
|
281
|
+
const previousFingerprint = readWarmupMetadata(options)?.codex_auth_fingerprint || null;
|
|
282
|
+
if (previousFingerprint === null) return false;
|
|
283
|
+
return previousFingerprint !== nextFingerprint;
|
|
284
|
+
}
|
|
285
|
+
|
|
253
286
|
export function extractProjectMeta(options = {}) {
|
|
254
287
|
const cwd = options.cwd || process.cwd();
|
|
255
288
|
const execSyncFn = options.execSyncFn || execSync;
|
|
@@ -422,7 +455,7 @@ export function checkSearchEngines(options = {}) {
|
|
|
422
455
|
|
|
423
456
|
function buildTarget(target, options = {}) {
|
|
424
457
|
const filePath = resolveTargetPath(target, options);
|
|
425
|
-
if (!options.force && isFresh(target, options)) {
|
|
458
|
+
if (!options.force && isFresh(target, options) && !hasAuthFingerprintChanged(target, options)) {
|
|
426
459
|
return { target, status: "skipped", file: filePath, reason: "fresh" };
|
|
427
460
|
}
|
|
428
461
|
|
|
@@ -476,6 +509,15 @@ export function buildAll(options = {}) {
|
|
|
476
509
|
const built = results.filter((result) => result.status === "built").length;
|
|
477
510
|
const skipped = results.filter((result) => result.status === "skipped").length;
|
|
478
511
|
const failed = results.filter((result) => result.status === "failed").length;
|
|
512
|
+
const authFingerprint = getCodexAuthFingerprint(options);
|
|
513
|
+
|
|
514
|
+
if (failed === 0) {
|
|
515
|
+
writeJSON(resolveWarmupMetadataPath(options), {
|
|
516
|
+
updated_at: new Date(options.now ?? Date.now()).toISOString(),
|
|
517
|
+
codex_auth_fingerprint: authFingerprint,
|
|
518
|
+
targets,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
479
521
|
|
|
480
522
|
return {
|
|
481
523
|
ok: failed === 0,
|
|
@@ -3,6 +3,7 @@ import { join, dirname } from "node:path";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_STATUS_URL = "http://127.0.0.1:27888/status";
|
|
8
9
|
const _sab = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -48,29 +49,58 @@ export function checkCli(name, { execSyncFn = execSync } = {}) {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
export function
|
|
52
|
+
export function detectCodexAuthState({
|
|
52
53
|
homeDir = homedir(),
|
|
53
54
|
existsSyncFn = existsSync,
|
|
54
55
|
readFileSyncFn = readFileSync,
|
|
55
56
|
} = {}) {
|
|
56
57
|
try {
|
|
57
58
|
const authPath = join(homeDir, ".codex", "auth.json");
|
|
58
|
-
if (!existsSyncFn(authPath)) return { plan: "unknown", source: "no_auth" };
|
|
59
|
+
if (!existsSyncFn(authPath)) return { plan: "unknown", source: "no_auth", fingerprint: "no_auth" };
|
|
59
60
|
|
|
60
61
|
const auth = JSON.parse(readFileSyncFn(authPath, "utf8"));
|
|
61
|
-
if (auth.auth_mode !== "chatgpt")
|
|
62
|
+
if (auth.auth_mode !== "chatgpt") {
|
|
63
|
+
const fingerprint = createHash("sha256")
|
|
64
|
+
.update(JSON.stringify({
|
|
65
|
+
auth_mode: auth.auth_mode || "api_key",
|
|
66
|
+
has_api_key: Boolean(auth.api_key || auth.apiKey),
|
|
67
|
+
}))
|
|
68
|
+
.digest("hex");
|
|
69
|
+
return { plan: "api", source: "api_key", fingerprint };
|
|
70
|
+
}
|
|
62
71
|
|
|
63
72
|
const token = auth.tokens?.id_token || auth.tokens?.access_token;
|
|
64
|
-
if (!token)
|
|
73
|
+
if (!token) {
|
|
74
|
+
return {
|
|
75
|
+
plan: "unknown",
|
|
76
|
+
source: "no_token",
|
|
77
|
+
fingerprint: createHash("sha256")
|
|
78
|
+
.update(JSON.stringify({ auth_mode: auth.auth_mode || "chatgpt", token: null }))
|
|
79
|
+
.digest("hex"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
65
82
|
|
|
66
83
|
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
|
|
67
84
|
const plan = payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type || "unknown";
|
|
68
|
-
|
|
85
|
+
const fingerprint = createHash("sha256")
|
|
86
|
+
.update(JSON.stringify({
|
|
87
|
+
auth_mode: auth.auth_mode || "chatgpt",
|
|
88
|
+
plan,
|
|
89
|
+
sub: payload?.sub || null,
|
|
90
|
+
exp: payload?.exp || null,
|
|
91
|
+
}))
|
|
92
|
+
.digest("hex");
|
|
93
|
+
return { plan, source: "jwt", fingerprint };
|
|
69
94
|
} catch {
|
|
70
|
-
return { plan: "unknown", source: "error" };
|
|
95
|
+
return { plan: "unknown", source: "error", fingerprint: "error" };
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
|
|
99
|
+
export function detectCodexPlan(options = {}) {
|
|
100
|
+
const { plan, source } = detectCodexAuthState(options);
|
|
101
|
+
return { plan, source };
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
export function checkHub({
|
|
75
105
|
pkgRoot = DEFAULT_PKG_ROOT,
|
|
76
106
|
statusUrl = DEFAULT_STATUS_URL,
|