@triflux/remote 10.0.0-alpha.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/hub/pipe.mjs +579 -0
- package/hub/public/dashboard.html +355 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/server.mjs +1124 -0
- package/hub/store-adapter.mjs +851 -0
- package/hub/store.mjs +897 -0
- package/hub/team/agent-map.json +11 -0
- package/hub/team/ansi.mjs +379 -0
- package/hub/team/backend.mjs +90 -0
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +106 -0
- package/hub/team/cli/commands/start/parse-args.mjs +130 -0
- package/hub/team/cli/commands/start/start-headless.mjs +109 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/cli/help.mjs +42 -0
- package/hub/team/cli/index.mjs +41 -0
- package/hub/team/cli/manifest.mjs +29 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +208 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +117 -0
- package/hub/team/cli/services/runtime-mode.mjs +62 -0
- package/hub/team/cli/services/state-store.mjs +48 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard-layout.mjs +33 -0
- package/hub/team/dashboard-open.mjs +153 -0
- package/hub/team/dashboard.mjs +274 -0
- package/hub/team/handoff.mjs +303 -0
- package/hub/team/headless.mjs +1149 -0
- package/hub/team/native-supervisor.mjs +392 -0
- package/hub/team/native.mjs +649 -0
- package/hub/team/nativeProxy.mjs +681 -0
- package/hub/team/orchestrator.mjs +161 -0
- package/hub/team/pane.mjs +153 -0
- package/hub/team/psmux.mjs +1354 -0
- package/hub/team/routing.mjs +223 -0
- package/hub/team/session.mjs +611 -0
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +361 -0
- package/hub/team/tui-lite.mjs +380 -0
- package/hub/team/tui-viewer.mjs +463 -0
- package/hub/team/tui.mjs +1245 -0
- package/hub/tools.mjs +554 -0
- package/hub/tray.mjs +376 -0
- package/hub/workers/claude-worker.mjs +475 -0
- package/hub/workers/codex-mcp.mjs +504 -0
- package/hub/workers/delegator-mcp.mjs +1076 -0
- package/hub/workers/factory.mjs +21 -0
- package/hub/workers/gemini-worker.mjs +373 -0
- package/hub/workers/interface.mjs +52 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/package.json +31 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"executor": "codex", "build-fixer": "codex", "debugger": "codex", "deep-executor": "codex",
|
|
3
|
+
"architect": "codex", "planner": "codex", "critic": "codex", "analyst": "codex",
|
|
4
|
+
"code-reviewer": "codex", "security-reviewer": "codex", "quality-reviewer": "codex",
|
|
5
|
+
"scientist": "codex", "scientist-deep": "codex", "document-specialist": "codex",
|
|
6
|
+
"spark": "codex",
|
|
7
|
+
"designer": "gemini", "writer": "gemini",
|
|
8
|
+
"explore": "claude",
|
|
9
|
+
"verifier": "codex", "test-engineer": "codex", "qa-tester": "codex",
|
|
10
|
+
"codex": "codex", "gemini": "gemini", "claude": "claude"
|
|
11
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// hub/team/ansi.mjs — Zero-dependency ANSI escape 유틸리티
|
|
2
|
+
// TUI 대시보드 렌더링을 위한 최소 헬퍼.
|
|
3
|
+
//
|
|
4
|
+
// wcwidth 지원: emoji/CJK wide=2셀, combining mark=0셀, ANSI escape=0셀
|
|
5
|
+
// 외부 의존성 없이 Unicode 범위 기반으로 구현.
|
|
6
|
+
|
|
7
|
+
export const ESC = "\x1b";
|
|
8
|
+
|
|
9
|
+
// ── 화면 ──
|
|
10
|
+
export const altScreenOn = `${ESC}[?1049h`;
|
|
11
|
+
export const altScreenOff = `${ESC}[?1049l`;
|
|
12
|
+
export const clearScreen = `${ESC}[2J`;
|
|
13
|
+
export const cursorHome = `${ESC}[H`;
|
|
14
|
+
export const cursorHide = `${ESC}[?25l`;
|
|
15
|
+
export const cursorShow = `${ESC}[?25h`;
|
|
16
|
+
|
|
17
|
+
// ── 커서 이동 ──
|
|
18
|
+
export function moveTo(row, col) { return `${ESC}[${row};${col}H`; }
|
|
19
|
+
export function moveUp(n = 1) { return `${ESC}[${n}A`; }
|
|
20
|
+
export function moveDown(n = 1) { return `${ESC}[${n}B`; }
|
|
21
|
+
|
|
22
|
+
// ── 줄 제어 ──
|
|
23
|
+
export const clearLine = `${ESC}[2K`;
|
|
24
|
+
export const clearToEnd = `${ESC}[K`;
|
|
25
|
+
export const eraseBelow = `${ESC}[J`;
|
|
26
|
+
|
|
27
|
+
// ── 색상 (triflux 디자인 시스템) ──
|
|
28
|
+
export const RESET = `${ESC}[0m`;
|
|
29
|
+
export const BOLD = `${ESC}[1m`;
|
|
30
|
+
export const DIM = `${ESC}[2m`;
|
|
31
|
+
|
|
32
|
+
export const FG = {
|
|
33
|
+
white: `${ESC}[97m`,
|
|
34
|
+
black: `${ESC}[30m`,
|
|
35
|
+
red: `${ESC}[31m`,
|
|
36
|
+
green: `${ESC}[32m`,
|
|
37
|
+
yellow: `${ESC}[33m`,
|
|
38
|
+
blue: `${ESC}[34m`,
|
|
39
|
+
magenta: `${ESC}[35m`,
|
|
40
|
+
cyan: `${ESC}[36m`,
|
|
41
|
+
gray: `${ESC}[90m`,
|
|
42
|
+
// triflux 브랜드
|
|
43
|
+
codex: `${ESC}[38;2;16;163;127m`, // #10a37f codex green
|
|
44
|
+
gemini: `${ESC}[38;5;39m`, // blue
|
|
45
|
+
claude: `${ESC}[38;2;232;112;64m`, // orange
|
|
46
|
+
triflux: `${ESC}[38;5;214m`, // amber
|
|
47
|
+
accent: `${ESC}[38;5;75m`, // light blue (Catppuccin blue)
|
|
48
|
+
muted: `${ESC}[38;5;245m`, // gray
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const BG = {
|
|
52
|
+
black: `${ESC}[40m`,
|
|
53
|
+
red: `${ESC}[41m`,
|
|
54
|
+
green: `${ESC}[42m`,
|
|
55
|
+
yellow: `${ESC}[43m`,
|
|
56
|
+
blue: `${ESC}[44m`,
|
|
57
|
+
header: `${ESC}[48;5;236m`, // dark gray
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── 색상 헬퍼 ──
|
|
61
|
+
export function color(text, fg, bg) {
|
|
62
|
+
const prefix = (fg || "") + (bg || "");
|
|
63
|
+
return prefix ? `${prefix}${text}${RESET}` : text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function bold(text) { return `${BOLD}${text}${RESET}`; }
|
|
67
|
+
export function dim(text) { return `${DIM}${text}${RESET}`; }
|
|
68
|
+
|
|
69
|
+
export function lerpRgb(a, b, t) {
|
|
70
|
+
return {
|
|
71
|
+
r: Math.round(a.r + (b.r - a.r) * t),
|
|
72
|
+
g: Math.round(a.g + (b.g - a.g) * t),
|
|
73
|
+
b: Math.round(a.b + (b.b - a.b) * t),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function rgbSeq(rgb, mode = 38) {
|
|
78
|
+
return `${ESC}[${mode};2;${rgb.r};${rgb.g};${rgb.b}m`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function brightenRgb(rgb, amount = 0.3) {
|
|
82
|
+
return lerpRgb(rgb, { r: 255, g: 255, b: 255 }, amount);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseRgbSeq(seq) {
|
|
86
|
+
const match = typeof seq === "string"
|
|
87
|
+
? seq.match(/\x1b\[(?:38|48);2;(\d+);(\d+);(\d+)m/)
|
|
88
|
+
: null;
|
|
89
|
+
if (!match) return null;
|
|
90
|
+
return {
|
|
91
|
+
r: Number.parseInt(match[1], 10),
|
|
92
|
+
g: Number.parseInt(match[2], 10),
|
|
93
|
+
b: Number.parseInt(match[3], 10),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function reapplyBackground(text, bgSeq) {
|
|
98
|
+
if (!bgSeq) return text;
|
|
99
|
+
return `${bgSeq}${String(text).replaceAll(RESET, `${RESET}${bgSeq}`)}${RESET}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── 박스 그리기 (유니코드 테두리) ──
|
|
103
|
+
const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
|
|
104
|
+
|
|
105
|
+
function borderHighlightCell(width, totalRows, highlightPos) {
|
|
106
|
+
if (!Number.isFinite(highlightPos)) return null;
|
|
107
|
+
const perimeter = 2 * (width - 2) + 2 * totalRows;
|
|
108
|
+
if (perimeter <= 0) return null;
|
|
109
|
+
let pos = Math.floor(highlightPos) % perimeter;
|
|
110
|
+
if (pos < 0) pos += perimeter;
|
|
111
|
+
|
|
112
|
+
if (pos < width - 2) return { row: 0, col: pos + 1 };
|
|
113
|
+
pos -= width - 2;
|
|
114
|
+
if (pos < totalRows) return { row: pos, col: width - 1 };
|
|
115
|
+
pos -= totalRows;
|
|
116
|
+
if (pos < width - 2) return { row: totalRows - 1, col: width - 2 - pos };
|
|
117
|
+
pos -= width - 2;
|
|
118
|
+
return { row: totalRows - 1 - pos, col: 0 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderBorderChar(glyph, row, col, highlightCell, borderSeq, highlightSeq) {
|
|
122
|
+
if (highlightCell && highlightCell.row === row && highlightCell.col === col) {
|
|
123
|
+
return `${highlightSeq}${glyph}${RESET}`;
|
|
124
|
+
}
|
|
125
|
+
return borderSeq ? `${borderSeq}${glyph}${RESET}` : glyph;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function box(lines, width, borderColor = "", options = {}) {
|
|
129
|
+
const isFn = typeof borderColor === "function";
|
|
130
|
+
const totalRows = lines.length + 2;
|
|
131
|
+
const bc = isFn ? (row) => borderColor(row, totalRows) : () => borderColor;
|
|
132
|
+
const rst = (isFn || borderColor) ? RESET : "";
|
|
133
|
+
const highlightCell = borderHighlightCell(width, totalRows, options.highlightPos);
|
|
134
|
+
const highlightSeq = options.highlightColor
|
|
135
|
+
|| (() => {
|
|
136
|
+
const parsed = parseRgbSeq(typeof borderColor === "string" ? borderColor : "");
|
|
137
|
+
return parsed ? rgbSeq(brightenRgb(parsed, 0.45)) : `${BOLD}${FG.white}`;
|
|
138
|
+
})();
|
|
139
|
+
const topChars = [BOX.tl, ...Array.from({ length: width - 2 }, () => BOX.h), BOX.tr];
|
|
140
|
+
const botChars = [BOX.bl, ...Array.from({ length: width - 2 }, () => BOX.h), BOX.br];
|
|
141
|
+
const top = topChars
|
|
142
|
+
.map((glyph, col) => renderBorderChar(glyph, 0, col, highlightCell, bc(0), highlightSeq))
|
|
143
|
+
.join("");
|
|
144
|
+
const bot = botChars
|
|
145
|
+
.map((glyph, col) => renderBorderChar(glyph, totalRows - 1, col, highlightCell, bc(totalRows - 1), highlightSeq))
|
|
146
|
+
.join("");
|
|
147
|
+
const mid = `${bc(Math.floor(totalRows / 2))}${BOX.ml}${BOX.h.repeat(width - 2)}${BOX.mr}${rst}`;
|
|
148
|
+
const body = lines.map((l, i) => {
|
|
149
|
+
const row = i + 1;
|
|
150
|
+
const content = options.titleFlashBg && i === 0
|
|
151
|
+
? reapplyBackground(padRight(l, width - 4), options.titleFlashBg)
|
|
152
|
+
: padRight(l, width - 4);
|
|
153
|
+
return `${renderBorderChar(BOX.v, row, 0, highlightCell, bc(row), highlightSeq)} ${content} ${renderBorderChar(BOX.v, row, width - 1, highlightCell, bc(row), highlightSeq)}`;
|
|
154
|
+
});
|
|
155
|
+
return { top, body, bot, mid };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── wcwidth 구현 (외부 의존성 없음) ──
|
|
159
|
+
// Unicode 코드포인트의 터미널 표시 너비를 반환: 0(combining), 1(일반), 2(wide)
|
|
160
|
+
function charWidth(cp) {
|
|
161
|
+
// combining / zero-width 범위
|
|
162
|
+
if (
|
|
163
|
+
cp === 0 || cp === 0xAD ||
|
|
164
|
+
(cp >= 0x0300 && cp <= 0x036F) || // Combining Diacritical Marks
|
|
165
|
+
(cp >= 0x0610 && cp <= 0x061A) ||
|
|
166
|
+
(cp >= 0x064B && cp <= 0x065F) ||
|
|
167
|
+
(cp >= 0x1AB0 && cp <= 0x1AFF) ||
|
|
168
|
+
(cp >= 0x1DC0 && cp <= 0x1DFF) ||
|
|
169
|
+
(cp >= 0x20D0 && cp <= 0x20FF) || // Combining Diacritical Marks for Symbols
|
|
170
|
+
(cp >= 0xFE20 && cp <= 0xFE2F) // Combining Half Marks
|
|
171
|
+
) return 0;
|
|
172
|
+
|
|
173
|
+
// Wide: CJK Unified Ideographs, Hangul, Fullwidth, emoji 주요 블록
|
|
174
|
+
if (
|
|
175
|
+
(cp >= 0x1100 && cp <= 0x115F) || // Hangul Jamo
|
|
176
|
+
(cp >= 0x2E80 && cp <= 0x2EFF) || // CJK Radicals Supplement
|
|
177
|
+
(cp >= 0x2F00 && cp <= 0x2FFF) ||
|
|
178
|
+
(cp >= 0x3000 && cp <= 0x303F) || // CJK Symbols and Punctuation
|
|
179
|
+
(cp >= 0x3040 && cp <= 0x309F) || // Hiragana
|
|
180
|
+
(cp >= 0x30A0 && cp <= 0x30FF) || // Katakana
|
|
181
|
+
(cp >= 0x3100 && cp <= 0x312F) ||
|
|
182
|
+
(cp >= 0x3130 && cp <= 0x318F) || // Hangul Compatibility Jamo
|
|
183
|
+
(cp >= 0x3190 && cp <= 0x319F) ||
|
|
184
|
+
(cp >= 0x31C0 && cp <= 0x31EF) ||
|
|
185
|
+
(cp >= 0x3200 && cp <= 0x32FF) ||
|
|
186
|
+
(cp >= 0x3300 && cp <= 0x33FF) ||
|
|
187
|
+
(cp >= 0x3400 && cp <= 0x4DBF) ||
|
|
188
|
+
(cp >= 0x4E00 && cp <= 0x9FFF) || // CJK Unified Ideographs
|
|
189
|
+
(cp >= 0xA000 && cp <= 0xA48F) ||
|
|
190
|
+
(cp >= 0xA490 && cp <= 0xA4CF) ||
|
|
191
|
+
(cp >= 0xA960 && cp <= 0xA97F) ||
|
|
192
|
+
(cp >= 0xAC00 && cp <= 0xD7AF) || // Hangul Syllables
|
|
193
|
+
(cp >= 0xF900 && cp <= 0xFAFF) || // CJK Compatibility Ideographs
|
|
194
|
+
(cp >= 0xFE10 && cp <= 0xFE1F) ||
|
|
195
|
+
(cp >= 0xFE30 && cp <= 0xFE4F) ||
|
|
196
|
+
(cp >= 0xFF00 && cp <= 0xFF60) || // Fullwidth Forms
|
|
197
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) ||
|
|
198
|
+
(cp >= 0x1B000 && cp <= 0x1B0FF) ||
|
|
199
|
+
(cp >= 0x1F004 && cp <= 0x1F0CF) ||
|
|
200
|
+
(cp >= 0x1F200 && cp <= 0x1F2FF) ||
|
|
201
|
+
(cp >= 0x1F300 && cp <= 0x1F64F) || // Misc Symbols, Emoticons
|
|
202
|
+
(cp >= 0x1F680 && cp <= 0x1F6FF) || // Transport & Map
|
|
203
|
+
(cp >= 0x1F900 && cp <= 0x1FAFF) || // Supplemental Symbols
|
|
204
|
+
(cp >= 0x20000 && cp <= 0x2FFFD) ||
|
|
205
|
+
(cp >= 0x30000 && cp <= 0x3FFFD)
|
|
206
|
+
) return 2;
|
|
207
|
+
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 문자열의 터미널 표시 너비 계산 (ANSI escape 제외, wcwidth 적용)
|
|
212
|
+
export function wcswidth(str) {
|
|
213
|
+
const plain = stripAnsi(str);
|
|
214
|
+
let width = 0;
|
|
215
|
+
for (const char of plain) {
|
|
216
|
+
width += charWidth(char.codePointAt(0));
|
|
217
|
+
}
|
|
218
|
+
return width;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── 텍스트 유틸 ──
|
|
222
|
+
export function stripAnsi(str) {
|
|
223
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(\x07|\x1b\\)/g, "");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// wcwidth-aware padRight: ANSI + wide char 보정 포함
|
|
227
|
+
export function padRight(str, len) {
|
|
228
|
+
const w = wcswidth(str);
|
|
229
|
+
const pad = Math.max(0, len - w);
|
|
230
|
+
return str + " ".repeat(pad);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// wcwidth-aware truncate: wide char 경계에서 자름
|
|
234
|
+
export function truncate(str, maxLen) {
|
|
235
|
+
const plain = stripAnsi(str);
|
|
236
|
+
const w = wcswidth(plain);
|
|
237
|
+
if (w <= maxLen) return str;
|
|
238
|
+
|
|
239
|
+
let acc = 0;
|
|
240
|
+
let i = 0;
|
|
241
|
+
for (const char of plain) {
|
|
242
|
+
const cw = charWidth(char.codePointAt(0));
|
|
243
|
+
if (acc + cw > maxLen - 1) break;
|
|
244
|
+
acc += cw;
|
|
245
|
+
i += char.length;
|
|
246
|
+
}
|
|
247
|
+
return plain.slice(0, i) + "…";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// wcwidth-aware clip: 정확히 width 셀에 맞게 자르고 패딩 (wide char 경계 보정)
|
|
251
|
+
export function clip(str, width) {
|
|
252
|
+
const plain = stripAnsi(str);
|
|
253
|
+
let acc = 0;
|
|
254
|
+
let i = 0;
|
|
255
|
+
for (const char of plain) {
|
|
256
|
+
const cw = charWidth(char.codePointAt(0));
|
|
257
|
+
if (acc + cw > width) {
|
|
258
|
+
// wide char이 경계를 넘으면 공백으로 채움
|
|
259
|
+
const result = plain.slice(0, i) + " ".repeat(width - acc);
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
acc += cw;
|
|
263
|
+
i += char.length;
|
|
264
|
+
}
|
|
265
|
+
return str + " ".repeat(width - acc);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Catppuccin Mocha 색상 상수 ──
|
|
269
|
+
export const MOCHA = {
|
|
270
|
+
ok: `${ESC}[38;5;114m`, // #a6e3a1 green
|
|
271
|
+
partial: `${ESC}[38;5;216m`, // #fab387 peach
|
|
272
|
+
fail: `${ESC}[38;5;210m`, // #f38ba8 red
|
|
273
|
+
thinking: `${ESC}[38;5;183m`, // #cba6f7 mauve
|
|
274
|
+
executing: `${ESC}[38;5;117m`, // #74c7ec sky
|
|
275
|
+
border: `${ESC}[38;5;238m`, // #45475a surface1
|
|
276
|
+
text: `${ESC}[38;2;205;214;244m`, // #cdd6f4 catppuccin text
|
|
277
|
+
subtext: `${ESC}[38;2;166;173;200m`, // #a6adc8 subtext0
|
|
278
|
+
overlay: `${ESC}[38;2;108;112;134m`, // #6c7086 overlay0
|
|
279
|
+
blue: `${ESC}[38;2;137;180;250m`, // #89b4fa blue
|
|
280
|
+
yellow: `${ESC}[38;2;249;226;175m`, // #f9e2af yellow
|
|
281
|
+
red: `${ESC}[38;2;243;139;168m`, // #f38ba8 red (truecolor)
|
|
282
|
+
surface0: `${ESC}[38;2;49;50;68m`, // #313244 surface0
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const MOCHA_RGB = {
|
|
286
|
+
ok: { r: 166, g: 227, b: 161 },
|
|
287
|
+
partial: { r: 250, g: 179, b: 135 },
|
|
288
|
+
fail: { r: 243, g: 139, b: 168 },
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// ── badge 헬퍼 ──
|
|
292
|
+
// statusBadge(status) → ANSI 색상 문자열
|
|
293
|
+
export function statusBadge(status) {
|
|
294
|
+
switch (status) {
|
|
295
|
+
case "ok":
|
|
296
|
+
case "completed":
|
|
297
|
+
case "done":
|
|
298
|
+
return `${MOCHA.ok}✓ ${status}${RESET}`;
|
|
299
|
+
case "partial":
|
|
300
|
+
case "in_progress":
|
|
301
|
+
case "running":
|
|
302
|
+
return `${MOCHA.partial}◑ ${status}${RESET}`;
|
|
303
|
+
case "fail":
|
|
304
|
+
case "failed":
|
|
305
|
+
case "error":
|
|
306
|
+
return `${MOCHA.fail}✗ ${status}${RESET}`;
|
|
307
|
+
case "thinking":
|
|
308
|
+
return `${MOCHA.thinking}⠿ ${status}${RESET}`;
|
|
309
|
+
case "executing":
|
|
310
|
+
return `${MOCHA.executing}▶ ${status}${RESET}`;
|
|
311
|
+
default:
|
|
312
|
+
return `${FG.muted}· ${status}${RESET}`;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── 진행률 바 ──
|
|
317
|
+
// progressBar(percent, width, time) — percent: 0~100, time 전달 시 shimmer sweep
|
|
318
|
+
export function progressBar(percent, width = 20, time) {
|
|
319
|
+
const ratio = Math.max(0, Math.min(100, percent)) / 100;
|
|
320
|
+
const filled = Math.round(ratio * width);
|
|
321
|
+
const empty = width - filled;
|
|
322
|
+
const fillRgb = percent >= 100 ? MOCHA_RGB.ok : percent >= 50 ? MOCHA_RGB.partial : MOCHA_RGB.fail;
|
|
323
|
+
const fillColor = rgbSeq(fillRgb);
|
|
324
|
+
let fillText = "█".repeat(filled);
|
|
325
|
+
|
|
326
|
+
if (filled > 0 && Number.isFinite(time)) {
|
|
327
|
+
const shinePos = Math.min(
|
|
328
|
+
filled - 1,
|
|
329
|
+
Math.floor(((((time % 2000) + 2000) % 2000) / 2000) * filled),
|
|
330
|
+
);
|
|
331
|
+
const shineColor = rgbSeq(brightenRgb(fillRgb, 0.3));
|
|
332
|
+
fillText = Array.from({ length: filled }, (_, idx) =>
|
|
333
|
+
idx === shinePos ? `${shineColor}█${RESET}` : `${fillColor}█${RESET}`
|
|
334
|
+
).join("");
|
|
335
|
+
} else if (filled > 0) {
|
|
336
|
+
fillText = `${fillColor}${fillText}${RESET}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const emptyText = empty > 0 ? `${MOCHA.border}${"░".repeat(empty)}${RESET}` : "";
|
|
340
|
+
return `${fillText}${emptyText}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── 애니메이션 진행률 바 (shimmer sweep) ──
|
|
344
|
+
export function animatedProgressBar(percent, width = 20, tick = 0) {
|
|
345
|
+
const ratio = Math.max(0, Math.min(100, percent)) / 100;
|
|
346
|
+
const filled = Math.round(ratio * width);
|
|
347
|
+
const empty = width - filled;
|
|
348
|
+
if (filled === 0 || percent >= 100) return progressBar(percent, width);
|
|
349
|
+
const baseClr = percent >= 50 ? MOCHA.partial : MOCHA.fail;
|
|
350
|
+
const pos = tick % (filled + 3);
|
|
351
|
+
let bar = "";
|
|
352
|
+
for (let i = 0; i < filled; i++) {
|
|
353
|
+
const d = Math.abs(i - pos);
|
|
354
|
+
if (d === 0) bar += `${ESC}[97m█`;
|
|
355
|
+
else if (d === 1) bar += `${baseClr}▓`;
|
|
356
|
+
else bar += `${baseClr}█`;
|
|
357
|
+
}
|
|
358
|
+
return `${bar}${MOCHA.border}${"░".repeat(empty)}${RESET}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── 상태 아이콘 ──
|
|
362
|
+
export const STATUS_ICON = {
|
|
363
|
+
running: `${MOCHA.partial}⏳${RESET}`,
|
|
364
|
+
completed: `${MOCHA.ok}✓${RESET}`,
|
|
365
|
+
failed: `${MOCHA.fail}✗${RESET}`,
|
|
366
|
+
pending: `${FG.gray}⏸${RESET}`,
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export const CLI_ICON = {
|
|
370
|
+
codex: `${FG.codex}⚪${RESET}`,
|
|
371
|
+
gemini: `${FG.gemini}🔵${RESET}`,
|
|
372
|
+
claude: `${FG.claude}🟠${RESET}`,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// ── 로딩 도트 (braille spinner) ──
|
|
376
|
+
const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
377
|
+
export function loadingDots(tick = 0, clr = MOCHA.thinking) {
|
|
378
|
+
return `${clr}${BRAILLE_FRAMES[tick % BRAILLE_FRAMES.length]}${RESET}`;
|
|
379
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// hub/team/backend.mjs — CLI 백엔드 추상화 레이어
|
|
2
|
+
// 각 CLI(codex/gemini/claude)의 명령 빌드 로직을 클래스로 캡슐화한다.
|
|
3
|
+
// v7.2.2
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
|
|
6
|
+
import { buildExecArgs } from "../codex-adapter.mjs";
|
|
7
|
+
|
|
8
|
+
const _require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
// ── 백엔드 클래스 ──────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export class CodexBackend {
|
|
13
|
+
name() { return "codex"; }
|
|
14
|
+
command() { return "codex"; }
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} prompt — 프롬프트 (프롬프트 파일 경로가 아닌 PowerShell 표현식)
|
|
18
|
+
* @param {string} resultFile — 결과 저장 경로
|
|
19
|
+
* @param {object} [opts]
|
|
20
|
+
* @returns {string} PowerShell 명령 (cls 제외)
|
|
21
|
+
*/
|
|
22
|
+
buildArgs(prompt, resultFile, opts = {}) {
|
|
23
|
+
return buildExecArgs({ prompt, resultFile, ...opts });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
env() { return {}; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class GeminiBackend {
|
|
30
|
+
name() { return "gemini"; }
|
|
31
|
+
command() { return "gemini"; }
|
|
32
|
+
|
|
33
|
+
buildArgs(prompt, resultFile, opts = {}) {
|
|
34
|
+
return `gemini --prompt ${prompt} --output-format text > '${resultFile}' 2>'${resultFile}.err'`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
env() { return {}; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ClaudeBackend {
|
|
41
|
+
name() { return "claude"; }
|
|
42
|
+
command() { return "claude"; }
|
|
43
|
+
|
|
44
|
+
buildArgs(prompt, resultFile, opts = {}) {
|
|
45
|
+
return `claude --print ${prompt} --output-format text > '${resultFile}' 2>&1`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
env() { return {}; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── 레지스트리 ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/** @type {Map<string, CodexBackend|GeminiBackend|ClaudeBackend>} */
|
|
54
|
+
const backends = new Map([
|
|
55
|
+
["codex", new CodexBackend()],
|
|
56
|
+
["gemini", new GeminiBackend()],
|
|
57
|
+
["claude", new ClaudeBackend()],
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 백엔드 이름으로 조회한다.
|
|
62
|
+
* @param {string} name — "codex" | "gemini" | "claude"
|
|
63
|
+
* @returns {CodexBackend|GeminiBackend|ClaudeBackend}
|
|
64
|
+
* @throws {Error} 등록되지 않은 이름
|
|
65
|
+
*/
|
|
66
|
+
export function getBackend(name) {
|
|
67
|
+
const b = backends.get(name);
|
|
68
|
+
if (!b) throw new Error(`지원하지 않는 CLI: ${name}`);
|
|
69
|
+
return b;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 에이전트명 또는 CLI명을 Backend로 해석한다.
|
|
74
|
+
* agent-map.json을 통해 에이전트명 → CLI명으로 변환 후 레지스트리에서 조회한다.
|
|
75
|
+
* @param {string} agentOrCli — "executor", "codex", "designer" 등
|
|
76
|
+
* @returns {CodexBackend|GeminiBackend|ClaudeBackend}
|
|
77
|
+
*/
|
|
78
|
+
export function getBackendForAgent(agentOrCli) {
|
|
79
|
+
const agentMap = _require("./agent-map.json");
|
|
80
|
+
const cliName = agentMap[agentOrCli] || agentOrCli;
|
|
81
|
+
return getBackend(cliName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 등록된 모든 백엔드를 반환한다.
|
|
86
|
+
* @returns {Array<CodexBackend|GeminiBackend|ClaudeBackend>}
|
|
87
|
+
*/
|
|
88
|
+
export function listBackends() {
|
|
89
|
+
return Array.from(backends.values());
|
|
90
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { attachSession } from "../../session.mjs";
|
|
2
|
+
import { DIM, RESET } from "../../shared.mjs";
|
|
3
|
+
import { buildManualAttachCommand, launchAttachInWindowsTerminal, wantsWtAttachFallback } from "../services/attach-fallback.mjs";
|
|
4
|
+
import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
|
|
5
|
+
import { loadTeamState } from "../services/state-store.mjs";
|
|
6
|
+
import { fail, ok, warn } from "../render.mjs";
|
|
7
|
+
|
|
8
|
+
export async function teamAttach(args = []) {
|
|
9
|
+
const state = loadTeamState();
|
|
10
|
+
if (!state || !isTeamAlive(state)) {
|
|
11
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (isNativeMode(state)) {
|
|
15
|
+
console.log(`\n ${DIM}in-process 모드는 별도 attach가 없습니다.${RESET}\n ${DIM}상태 확인: tfx multi status${RESET}\n`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (isWtMode(state)) {
|
|
19
|
+
console.log(`\n ${DIM}wt 모드는 attach 개념이 없습니다 (Windows Terminal pane가 독립 실행됨).${RESET}\n ${DIM}재실행/정리는: tfx multi stop${RESET}\n`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
attachSession(state.sessionName);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const allowWt = wantsWtAttachFallback(args);
|
|
27
|
+
if (allowWt && await launchAttachInWindowsTerminal(state.sessionName)) {
|
|
28
|
+
warn(`현재 터미널에서 attach 실패: ${error.message}`);
|
|
29
|
+
ok("Windows Terminal split-pane로 attach 재시도 창을 열었습니다.");
|
|
30
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}\n`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
fail(`attach 실패: ${error.message}`);
|
|
34
|
+
warn(allowWt ? "WT 분할창 attach 자동 검증 실패 (session_attached 증가 없음)" : "자동 WT 분할은 기본 비활성입니다. 필요 시 --wt 옵션으로 실행하세요.");
|
|
35
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}\n`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { injectPrompt, sendKeys } from "../../pane.mjs";
|
|
2
|
+
import { DIM, RESET, WHITE, YELLOW } from "../../shared.mjs";
|
|
3
|
+
import { publishLeadControl } from "../services/hub-client.mjs";
|
|
4
|
+
import { resolveMember } from "../services/member-selector.mjs";
|
|
5
|
+
import { nativeRequest } from "../services/native-control.mjs";
|
|
6
|
+
import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
|
|
7
|
+
import { loadTeamState } from "../services/state-store.mjs";
|
|
8
|
+
import { ok, warn } from "../render.mjs";
|
|
9
|
+
|
|
10
|
+
export async function teamControl(args = []) {
|
|
11
|
+
const state = loadTeamState();
|
|
12
|
+
if (!state || !isTeamAlive(state)) {
|
|
13
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const member = resolveMember(state, args[0]);
|
|
18
|
+
const command = String(args[1] || "").toLowerCase();
|
|
19
|
+
const reason = args.slice(2).join(" ");
|
|
20
|
+
if (!member || !new Set(["interrupt", "stop", "pause", "resume"]).has(command)) {
|
|
21
|
+
console.log(`\n 사용법: ${WHITE}tfx multi control <lead|이름|번호> <interrupt|stop|pause|resume> [사유]${RESET}\n`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (isWtMode(state)) {
|
|
25
|
+
console.log(`\n ${YELLOW}⚠${RESET} wt 모드는 Hub direct/control 주입 경로가 비활성입니다.\n ${DIM}수동 제어: 해당 pane에서 직접 명령/인터럽트를 수행하세요.${RESET}\n`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let directOk = false;
|
|
30
|
+
if (isNativeMode(state)) {
|
|
31
|
+
directOk = !!(await nativeRequest(state, "/control", { member: member.name, command, reason }))?.ok;
|
|
32
|
+
} else {
|
|
33
|
+
injectPrompt(member.pane, `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`);
|
|
34
|
+
if (command === "interrupt") sendKeys(member.pane, "C-c");
|
|
35
|
+
directOk = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const published = await publishLeadControl(state, member, command, reason);
|
|
39
|
+
if (directOk && published) ok(`${member.name} 제어 전송 (${command}, direct + hub)`);
|
|
40
|
+
else if (directOk) ok(`${member.name} 제어 전송 (${command}, direct only)`);
|
|
41
|
+
else warn(`${member.name} 제어 전송 실패 (${command})`);
|
|
42
|
+
console.log("");
|
|
43
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AMBER, BOLD, DIM, RESET } from "../../shared.mjs";
|
|
2
|
+
import {
|
|
3
|
+
capturePaneOutput,
|
|
4
|
+
detectMultiplexer,
|
|
5
|
+
getSessionAttachedCount,
|
|
6
|
+
hasWindowsTerminal,
|
|
7
|
+
hasWindowsTerminalSession,
|
|
8
|
+
listSessions,
|
|
9
|
+
} from "../../session.mjs";
|
|
10
|
+
import { getHubInfo, nativeGetStatus } from "../services/hub-client.mjs";
|
|
11
|
+
import { isNativeMode, isTeamAlive, isWtMode } from "../services/runtime-mode.mjs";
|
|
12
|
+
import { loadTeamState, TEAM_PROFILE } from "../services/state-store.mjs";
|
|
13
|
+
import { formatCompletionSuffix } from "../render.mjs";
|
|
14
|
+
|
|
15
|
+
export async function teamDebug(args = []) {
|
|
16
|
+
const state = loadTeamState();
|
|
17
|
+
const flagIndex = args.findIndex((arg) => arg === "--lines" || arg === "-n");
|
|
18
|
+
const lines = flagIndex === -1 ? 20 : Math.max(3, parseInt(args[flagIndex + 1] || "20", 10) || 20);
|
|
19
|
+
const hub = await getHubInfo();
|
|
20
|
+
|
|
21
|
+
console.log(`\n ${AMBER}${BOLD}⬡ Team Debug${RESET}\n`);
|
|
22
|
+
console.log(` platform: ${process.platform}`);
|
|
23
|
+
console.log(` node: ${process.version}`);
|
|
24
|
+
console.log(` tty: stdout=${!!process.stdout.isTTY}, stdin=${!!process.stdin.isTTY}`);
|
|
25
|
+
console.log(` mux: ${detectMultiplexer() || "none"}`);
|
|
26
|
+
console.log(` hub-pid: ${hub ? `${hub.pid}` : "-"}`);
|
|
27
|
+
console.log(` hub-url: ${hub?.url || "-"}`);
|
|
28
|
+
const sessions = listSessions();
|
|
29
|
+
console.log(` sessions: ${sessions.length ? sessions.join(", ") : "-"}`);
|
|
30
|
+
|
|
31
|
+
if (!state) {
|
|
32
|
+
console.log(`\n ${DIM}team-state 없음 (활성 세션 없음)${RESET}\n`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`\n ${BOLD}state${RESET}`);
|
|
37
|
+
console.log(` session: ${state.sessionName}`);
|
|
38
|
+
console.log(` profile: ${state.profile || TEAM_PROFILE}`);
|
|
39
|
+
console.log(` mode: ${state.teammateMode || "tmux"}`);
|
|
40
|
+
console.log(` lead: ${state.lead}`);
|
|
41
|
+
console.log(` agents: ${(state.agents || []).join(", ")}`);
|
|
42
|
+
console.log(` alive: ${isTeamAlive(state) ? "yes" : "no"}`);
|
|
43
|
+
console.log(` attached: ${getSessionAttachedCount(state.sessionName) ?? "-"}`);
|
|
44
|
+
|
|
45
|
+
if (isWtMode(state)) {
|
|
46
|
+
console.log(`\n ${BOLD}wt-session${RESET}`);
|
|
47
|
+
console.log(` window: ${state?.wt?.windowId ?? 0}`);
|
|
48
|
+
console.log(` layout: ${state?.wt?.layout || state?.layout || "-"}`);
|
|
49
|
+
console.log(` panes: ${state?.wt?.paneCount ?? (state.members || []).length}`);
|
|
50
|
+
console.log(` wt.exe: ${hasWindowsTerminal() ? "yes" : "no"}`);
|
|
51
|
+
console.log(` WT_SESSION:${hasWindowsTerminalSession() ? "yes" : "no"}`);
|
|
52
|
+
console.log("");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isNativeMode(state)) {
|
|
57
|
+
console.log(`\n ${BOLD}native-members${RESET}`);
|
|
58
|
+
const members = (await nativeGetStatus(state))?.data?.members || [];
|
|
59
|
+
if (!members.length) console.log(` ${DIM}(no data)${RESET}`);
|
|
60
|
+
for (const member of members) console.log(` - ${member.name}: ${member.status}${formatCompletionSuffix(member)}${member.lastPreview ? ` ${DIM}${member.lastPreview}${RESET}` : ""}`);
|
|
61
|
+
console.log("");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`\n ${BOLD}pane-tail${RESET} ${DIM}(last ${lines} lines)${RESET}`);
|
|
66
|
+
if (!(state.members || []).length) console.log(` ${DIM}(members 없음)${RESET}`);
|
|
67
|
+
for (const member of state.members || []) {
|
|
68
|
+
console.log(`\n [${member.name}] ${member.pane}`);
|
|
69
|
+
for (const line of (capturePaneOutput(member.pane, lines) || "(empty)").split("\n").slice(-lines)) {
|
|
70
|
+
console.log(` ${line}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
console.log("");
|
|
74
|
+
}
|