triflux 3.2.0-dev.7 → 3.2.0-dev.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/bin/triflux.mjs +557 -251
- package/hooks/keyword-rules.json +16 -0
- package/hub/bridge.mjs +410 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +512 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +59 -1459
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +12 -80
- package/hub/team/nativeProxy.mjs +121 -47
- package/hub/team/pane.mjs +66 -43
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +354 -291
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +41 -52
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +4 -2
- package/package.json +4 -1
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +128 -70
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +415 -80
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +53 -62
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# psmux-steering-prototype.sh
|
|
3
|
+
# Windows psmux 환경에서 lead/codex-worker/gemini-worker pane을 만들고
|
|
4
|
+
# send-keys + pipe-pane 기반으로 실시간 CLI 스티어링을 실험하는 프로토타입.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
PSMUX_BIN="${PSMUX_BIN:-psmux}"
|
|
9
|
+
SESSION_NAME="${PSMUX_SESSION_NAME:-triflux-steering}"
|
|
10
|
+
WINDOW_NAME="${PSMUX_WINDOW_NAME:-control}"
|
|
11
|
+
PANE_LEAD="lead"
|
|
12
|
+
PANE_CODEX="codex-worker"
|
|
13
|
+
PANE_GEMINI="gemini-worker"
|
|
14
|
+
SHELL_COMMAND="${PSMUX_SHELL_COMMAND:-powershell.exe -NoLogo}"
|
|
15
|
+
CAPTURE_ROOT="${PSMUX_CAPTURE_ROOT:-${TMPDIR:-/tmp}/psmux-steering}"
|
|
16
|
+
CAPTURE_DIR="${CAPTURE_ROOT}/${SESSION_NAME}"
|
|
17
|
+
CAPTURE_HELPER_PATH="${CAPTURE_ROOT}/pipe-pane-capture.ps1"
|
|
18
|
+
COMPLETION_PREFIX="__TRIFLUX_DONE__:"
|
|
19
|
+
POLL_INTERVAL_SEC="${PSMUX_POLL_INTERVAL_SEC:-1}"
|
|
20
|
+
|
|
21
|
+
usage() {
|
|
22
|
+
cat <<'EOF'
|
|
23
|
+
Usage:
|
|
24
|
+
scripts/psmux-steering-prototype.sh start
|
|
25
|
+
scripts/psmux-steering-prototype.sh demo
|
|
26
|
+
scripts/psmux-steering-prototype.sh attach
|
|
27
|
+
scripts/psmux-steering-prototype.sh send <pane-name> <command text>
|
|
28
|
+
scripts/psmux-steering-prototype.sh send-no-enter <pane-name> <text>
|
|
29
|
+
scripts/psmux-steering-prototype.sh steer-ps <pane-name> <powershell command>
|
|
30
|
+
scripts/psmux-steering-prototype.sh wait <pane-name> <regex> [timeout-sec]
|
|
31
|
+
scripts/psmux-steering-prototype.sh logs
|
|
32
|
+
scripts/psmux-steering-prototype.sh cleanup
|
|
33
|
+
|
|
34
|
+
Pane names:
|
|
35
|
+
lead | codex-worker | gemini-worker
|
|
36
|
+
|
|
37
|
+
Environment overrides:
|
|
38
|
+
PSMUX_BIN
|
|
39
|
+
PSMUX_SESSION_NAME
|
|
40
|
+
PSMUX_WINDOW_NAME
|
|
41
|
+
PSMUX_SHELL_COMMAND
|
|
42
|
+
PSMUX_CAPTURE_ROOT
|
|
43
|
+
PSMUX_POLL_INTERVAL_SEC
|
|
44
|
+
EOF
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
log() {
|
|
48
|
+
printf '[psmux-steering] %s\n' "$*"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
die() {
|
|
52
|
+
printf '[psmux-steering] ERROR: %s\n' "$*" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
require_psmux() {
|
|
57
|
+
command -v "$PSMUX_BIN" >/dev/null 2>&1 || die "Cannot find '$PSMUX_BIN' in PATH."
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
session_target() {
|
|
61
|
+
printf '%s:%s' "$SESSION_NAME" "$WINDOW_NAME"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pane_target_from_index() {
|
|
65
|
+
local pane_index="$1"
|
|
66
|
+
printf '%s.%s' "$(session_target)" "$pane_index"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
log_file_for() {
|
|
70
|
+
local pane_name="$1"
|
|
71
|
+
printf '%s/%s.log' "$CAPTURE_DIR" "$pane_name"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
to_windows_path() {
|
|
75
|
+
local path_value="$1"
|
|
76
|
+
|
|
77
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
78
|
+
cygpath -aw "$path_value"
|
|
79
|
+
return 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
printf '%s\n' "$path_value"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ensure_capture_helper() {
|
|
86
|
+
mkdir -p "$CAPTURE_ROOT"
|
|
87
|
+
|
|
88
|
+
cat >"$CAPTURE_HELPER_PATH" <<'EOF'
|
|
89
|
+
param(
|
|
90
|
+
[Parameter(Mandatory = $true)][string]$Path
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
$parent = Split-Path -Parent $Path
|
|
94
|
+
if ($parent) {
|
|
95
|
+
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
$reader = [Console]::In
|
|
99
|
+
while (($line = $reader.ReadLine()) -ne $null) {
|
|
100
|
+
Add-Content -LiteralPath $Path -Value $line -Encoding utf8
|
|
101
|
+
}
|
|
102
|
+
EOF
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
session_exists() {
|
|
106
|
+
"$PSMUX_BIN" has-session -t "$SESSION_NAME" >/dev/null 2>&1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolve_pane_target() {
|
|
110
|
+
local pane_name="$1"
|
|
111
|
+
local pane_index
|
|
112
|
+
|
|
113
|
+
pane_index="$("$PSMUX_BIN" list-panes -t "$(session_target)" -F '#{pane_index} #{pane_title}' \
|
|
114
|
+
| awk -v wanted="$pane_name" '$2 == wanted { print $1; exit }')"
|
|
115
|
+
|
|
116
|
+
[[ -n "$pane_index" ]] || return 1
|
|
117
|
+
pane_target_from_index "$pane_index"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
require_pane_target() {
|
|
121
|
+
local pane_name="$1"
|
|
122
|
+
local pane_target
|
|
123
|
+
|
|
124
|
+
pane_target="$(resolve_pane_target "$pane_name")"
|
|
125
|
+
[[ -n "$pane_target" ]] || die "Pane '$pane_name' not found in session '$SESSION_NAME'."
|
|
126
|
+
printf '%s\n' "$pane_target"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
set_pane_title() {
|
|
130
|
+
local pane_target="$1"
|
|
131
|
+
local pane_name="$2"
|
|
132
|
+
|
|
133
|
+
"$PSMUX_BIN" select-pane -t "$pane_target" -T "$pane_name" >/dev/null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
start_capture_for_pane() {
|
|
137
|
+
local pane_name="$1"
|
|
138
|
+
local pane_target log_file helper_windows_path log_windows_path
|
|
139
|
+
|
|
140
|
+
pane_target="$(require_pane_target "$pane_name")"
|
|
141
|
+
log_file="$(log_file_for "$pane_name")"
|
|
142
|
+
ensure_capture_helper
|
|
143
|
+
helper_windows_path="$(to_windows_path "$CAPTURE_HELPER_PATH")"
|
|
144
|
+
log_windows_path="$(to_windows_path "$log_file")"
|
|
145
|
+
|
|
146
|
+
mkdir -p "$CAPTURE_DIR"
|
|
147
|
+
: >"$log_file"
|
|
148
|
+
|
|
149
|
+
"$PSMUX_BIN" pipe-pane -t "$pane_target" >/dev/null 2>&1 || true
|
|
150
|
+
"$PSMUX_BIN" pipe-pane -t "$pane_target" powershell.exe -NoLogo -NoProfile -File "$helper_windows_path" "$log_windows_path" >/dev/null
|
|
151
|
+
refresh_snapshot_for_pane "$pane_name"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
start_capture_for_all_panes() {
|
|
155
|
+
start_capture_for_pane "$PANE_LEAD"
|
|
156
|
+
start_capture_for_pane "$PANE_CODEX"
|
|
157
|
+
start_capture_for_pane "$PANE_GEMINI"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
stop_capture_for_pane() {
|
|
161
|
+
local pane_name="$1"
|
|
162
|
+
local pane_target
|
|
163
|
+
|
|
164
|
+
pane_target="$(resolve_pane_target "$pane_name" || true)"
|
|
165
|
+
[[ -n "$pane_target" ]] || return 0
|
|
166
|
+
"$PSMUX_BIN" pipe-pane -t "$pane_target" >/dev/null 2>&1 || true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
refresh_snapshot_for_pane() {
|
|
170
|
+
local pane_name="$1"
|
|
171
|
+
local pane_target log_file
|
|
172
|
+
|
|
173
|
+
pane_target="$(require_pane_target "$pane_name")"
|
|
174
|
+
log_file="$(log_file_for "$pane_name")"
|
|
175
|
+
mkdir -p "$CAPTURE_DIR"
|
|
176
|
+
|
|
177
|
+
# Detached Windows sessions may not flush pipe-pane reliably yet.
|
|
178
|
+
# Overwriting the log with a fresh capture-pane snapshot keeps
|
|
179
|
+
# completion detection deterministic for the prototype.
|
|
180
|
+
"$PSMUX_BIN" capture-pane -t "$pane_target" -p >"$log_file"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
send_keys_to_pane() {
|
|
184
|
+
local pane_name="$1"
|
|
185
|
+
local text="$2"
|
|
186
|
+
local submit="${3:-1}"
|
|
187
|
+
local pane_target
|
|
188
|
+
|
|
189
|
+
pane_target="$(require_pane_target "$pane_name")"
|
|
190
|
+
"$PSMUX_BIN" send-keys -t "$pane_target" -l "$text"
|
|
191
|
+
if [[ "$submit" != "0" ]]; then
|
|
192
|
+
"$PSMUX_BIN" send-keys -t "$pane_target" C-m
|
|
193
|
+
fi
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
dispatch_powershell_command() {
|
|
197
|
+
local pane_name="$1"
|
|
198
|
+
local command_text="$2"
|
|
199
|
+
local token wrapped
|
|
200
|
+
|
|
201
|
+
token="${pane_name}-$(date +%s)-$RANDOM"
|
|
202
|
+
wrapped="${command_text}; \$trifluxExit = if (\$null -ne \$LASTEXITCODE) { [int]\$LASTEXITCODE } else { 0 }; Write-Output \"${COMPLETION_PREFIX}${token}:\$trifluxExit\""
|
|
203
|
+
|
|
204
|
+
send_keys_to_pane "$pane_name" "$wrapped" 1
|
|
205
|
+
printf '%s\n' "$token"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
wait_for_pattern() {
|
|
209
|
+
local pane_name="$1"
|
|
210
|
+
local pattern="$2"
|
|
211
|
+
local timeout_sec="${3:-300}"
|
|
212
|
+
local log_file deadline
|
|
213
|
+
|
|
214
|
+
log_file="$(log_file_for "$pane_name")"
|
|
215
|
+
[[ -f "$log_file" ]] || die "Log file for pane '$pane_name' does not exist. Start capture first."
|
|
216
|
+
|
|
217
|
+
deadline=$((SECONDS + timeout_sec))
|
|
218
|
+
while (( SECONDS <= deadline )); do
|
|
219
|
+
refresh_snapshot_for_pane "$pane_name"
|
|
220
|
+
if grep -Eq -- "$pattern" "$log_file"; then
|
|
221
|
+
return 0
|
|
222
|
+
fi
|
|
223
|
+
sleep "$POLL_INTERVAL_SEC"
|
|
224
|
+
done
|
|
225
|
+
|
|
226
|
+
return 1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
wait_for_completion_token() {
|
|
230
|
+
local pane_name="$1"
|
|
231
|
+
local token="$2"
|
|
232
|
+
local timeout_sec="${3:-300}"
|
|
233
|
+
local pattern
|
|
234
|
+
|
|
235
|
+
pattern="${COMPLETION_PREFIX}${token}:[0-9]+"
|
|
236
|
+
wait_for_pattern "$pane_name" "$pattern" "$timeout_sec"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
print_log_locations() {
|
|
240
|
+
mkdir -p "$CAPTURE_DIR"
|
|
241
|
+
printf '%s\t%s\n' "$PANE_LEAD" "$(log_file_for "$PANE_LEAD")"
|
|
242
|
+
printf '%s\t%s\n' "$PANE_CODEX" "$(log_file_for "$PANE_CODEX")"
|
|
243
|
+
printf '%s\t%s\n' "$PANE_GEMINI" "$(log_file_for "$PANE_GEMINI")"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
create_session_layout() {
|
|
247
|
+
local lead_index codex_index gemini_index
|
|
248
|
+
|
|
249
|
+
require_psmux
|
|
250
|
+
|
|
251
|
+
if session_exists; then
|
|
252
|
+
die "Session '$SESSION_NAME' already exists. Run cleanup first or set PSMUX_SESSION_NAME."
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
mkdir -p "$CAPTURE_DIR"
|
|
256
|
+
|
|
257
|
+
lead_index="$("$PSMUX_BIN" new-session -d -P -F '#{pane_index}' -s "$SESSION_NAME" -n "$WINDOW_NAME" -- $SHELL_COMMAND)"
|
|
258
|
+
codex_index="$("$PSMUX_BIN" split-window -h -P -F '#{pane_index}' -t "$(session_target)" -- $SHELL_COMMAND)"
|
|
259
|
+
gemini_index="$("$PSMUX_BIN" split-window -v -P -F '#{pane_index}' -t "$(pane_target_from_index "$codex_index")" -- $SHELL_COMMAND)"
|
|
260
|
+
|
|
261
|
+
set_pane_title "$(pane_target_from_index "$lead_index")" "$PANE_LEAD"
|
|
262
|
+
set_pane_title "$(pane_target_from_index "$codex_index")" "$PANE_CODEX"
|
|
263
|
+
set_pane_title "$(pane_target_from_index "$gemini_index")" "$PANE_GEMINI"
|
|
264
|
+
|
|
265
|
+
"$PSMUX_BIN" select-layout -t "$(session_target)" tiled >/dev/null
|
|
266
|
+
"$PSMUX_BIN" select-pane -t "$(pane_target_from_index "$lead_index")" >/dev/null
|
|
267
|
+
|
|
268
|
+
start_capture_for_all_panes
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
show_start_summary() {
|
|
272
|
+
log "Session created: $SESSION_NAME"
|
|
273
|
+
log "Window: $WINDOW_NAME"
|
|
274
|
+
log "Attach with: $PSMUX_BIN attach -t $SESSION_NAME"
|
|
275
|
+
print_log_locations
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
run_demo() {
|
|
279
|
+
local lead_token codex_token gemini_token
|
|
280
|
+
|
|
281
|
+
create_session_layout
|
|
282
|
+
|
|
283
|
+
lead_token="$(dispatch_powershell_command "$PANE_LEAD" 'Write-Host "lead pane ready"')"
|
|
284
|
+
codex_token="$(dispatch_powershell_command "$PANE_CODEX" 'Write-Host "codex-worker pane ready"')"
|
|
285
|
+
gemini_token="$(dispatch_powershell_command "$PANE_GEMINI" 'Write-Host "gemini-worker pane ready"')"
|
|
286
|
+
|
|
287
|
+
wait_for_completion_token "$PANE_LEAD" "$lead_token" 30 || die "Lead pane demo command timed out."
|
|
288
|
+
wait_for_completion_token "$PANE_CODEX" "$codex_token" 30 || die "Codex pane demo command timed out."
|
|
289
|
+
wait_for_completion_token "$PANE_GEMINI" "$gemini_token" 30 || die "Gemini pane demo command timed out."
|
|
290
|
+
|
|
291
|
+
show_start_summary
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
cleanup() {
|
|
295
|
+
stop_capture_for_pane "$PANE_LEAD"
|
|
296
|
+
stop_capture_for_pane "$PANE_CODEX"
|
|
297
|
+
stop_capture_for_pane "$PANE_GEMINI"
|
|
298
|
+
|
|
299
|
+
if session_exists; then
|
|
300
|
+
"$PSMUX_BIN" kill-session -t "$SESSION_NAME" >/dev/null 2>&1 || true
|
|
301
|
+
fi
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
main() {
|
|
305
|
+
local action="${1:-demo}"
|
|
306
|
+
|
|
307
|
+
case "$action" in
|
|
308
|
+
start)
|
|
309
|
+
create_session_layout
|
|
310
|
+
show_start_summary
|
|
311
|
+
;;
|
|
312
|
+
demo)
|
|
313
|
+
run_demo
|
|
314
|
+
;;
|
|
315
|
+
attach)
|
|
316
|
+
require_psmux
|
|
317
|
+
"$PSMUX_BIN" attach -t "$SESSION_NAME"
|
|
318
|
+
;;
|
|
319
|
+
send)
|
|
320
|
+
[[ $# -ge 3 ]] || die "Usage: $0 send <pane-name> <command text>"
|
|
321
|
+
shift
|
|
322
|
+
local pane_name="$1"
|
|
323
|
+
shift
|
|
324
|
+
send_keys_to_pane "$pane_name" "$*" 1
|
|
325
|
+
;;
|
|
326
|
+
send-no-enter)
|
|
327
|
+
[[ $# -ge 3 ]] || die "Usage: $0 send-no-enter <pane-name> <text>"
|
|
328
|
+
shift
|
|
329
|
+
local pane_name="$1"
|
|
330
|
+
shift
|
|
331
|
+
send_keys_to_pane "$pane_name" "$*" 0
|
|
332
|
+
;;
|
|
333
|
+
steer-ps)
|
|
334
|
+
[[ $# -ge 3 ]] || die "Usage: $0 steer-ps <pane-name> <powershell command>"
|
|
335
|
+
shift
|
|
336
|
+
local pane_name="$1"
|
|
337
|
+
shift
|
|
338
|
+
dispatch_powershell_command "$pane_name" "$*"
|
|
339
|
+
;;
|
|
340
|
+
wait)
|
|
341
|
+
[[ $# -ge 3 ]] || die "Usage: $0 wait <pane-name> <regex> [timeout-sec]"
|
|
342
|
+
shift
|
|
343
|
+
local pane_name="$1"
|
|
344
|
+
local pattern="$2"
|
|
345
|
+
local timeout_sec="${3:-300}"
|
|
346
|
+
if wait_for_pattern "$pane_name" "$pattern" "$timeout_sec"; then
|
|
347
|
+
log "Matched pattern for pane '$pane_name': $pattern"
|
|
348
|
+
else
|
|
349
|
+
die "Timed out waiting for pane '$pane_name' pattern: $pattern"
|
|
350
|
+
fi
|
|
351
|
+
;;
|
|
352
|
+
logs)
|
|
353
|
+
print_log_locations
|
|
354
|
+
;;
|
|
355
|
+
cleanup)
|
|
356
|
+
cleanup
|
|
357
|
+
;;
|
|
358
|
+
-h|--help|help)
|
|
359
|
+
usage
|
|
360
|
+
;;
|
|
361
|
+
*)
|
|
362
|
+
usage
|
|
363
|
+
die "Unknown action: $action"
|
|
364
|
+
;;
|
|
365
|
+
esac
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
main "$@"
|
package/scripts/setup.mjs
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
// - hud-qos-status.mjs를 ~/.claude/hud/에 동기화
|
|
5
5
|
// - skills/를 ~/.claude/skills/에 동기화
|
|
6
6
|
|
|
7
|
-
import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, chmodSync, unlinkSync } from "fs";
|
|
8
|
-
import { join, dirname } from "path";
|
|
9
|
-
import { homedir } from "os";
|
|
10
|
-
import { spawn } from "child_process";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
12
|
-
|
|
13
|
-
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
|
+
import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, chmodSync, unlinkSync } from "fs";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
14
14
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
15
15
|
const CODEX_DIR = join(homedir(), ".codex");
|
|
16
16
|
const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
@@ -45,6 +45,36 @@ const SYNC_MAP = [
|
|
|
45
45
|
dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
|
|
46
46
|
label: "tfx-route-post.mjs",
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
src: join(PLUGIN_ROOT, "scripts", "tfx-route-worker.mjs"),
|
|
50
|
+
dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
|
|
51
|
+
label: "tfx-route-worker.mjs",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "codex-mcp.mjs"),
|
|
55
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "codex-mcp.mjs"),
|
|
56
|
+
label: "hub/workers/codex-mcp.mjs",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "interface.mjs"),
|
|
60
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
|
|
61
|
+
label: "hub/workers/interface.mjs",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "gemini-worker.mjs"),
|
|
65
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
|
|
66
|
+
label: "hub/workers/gemini-worker.mjs",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "claude-worker.mjs"),
|
|
70
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
|
|
71
|
+
label: "hub/workers/claude-worker.mjs",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "factory.mjs"),
|
|
75
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
|
|
76
|
+
label: "hub/workers/factory.mjs",
|
|
77
|
+
},
|
|
48
78
|
{
|
|
49
79
|
src: join(PLUGIN_ROOT, "hud", "hud-qos-status.mjs"),
|
|
50
80
|
dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
|
|
@@ -52,24 +82,24 @@ const SYNC_MAP = [
|
|
|
52
82
|
},
|
|
53
83
|
];
|
|
54
84
|
|
|
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
|
-
}
|
|
85
|
+
function getVersion(filePath) {
|
|
86
|
+
try {
|
|
87
|
+
const content = readFileSync(filePath, "utf8");
|
|
88
|
+
const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
|
|
89
|
+
return match ? match[1] : null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function shouldSyncTextFile(src, dst) {
|
|
96
|
+
if (!existsSync(dst)) return true;
|
|
97
|
+
try {
|
|
98
|
+
return readFileSync(src, "utf8") !== readFileSync(dst, "utf8");
|
|
99
|
+
} catch {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
73
103
|
|
|
74
104
|
function escapeRegExp(value) {
|
|
75
105
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -124,13 +154,41 @@ for (const { src, dst, label } of SYNC_MAP) {
|
|
|
124
154
|
copyFileSync(src, dst);
|
|
125
155
|
try { chmodSync(dst, 0o755); } catch {}
|
|
126
156
|
synced++;
|
|
127
|
-
} else {
|
|
128
|
-
if (shouldSyncTextFile(src, dst)) {
|
|
129
|
-
copyFileSync(src, dst);
|
|
130
|
-
try { chmodSync(dst, 0o755); } catch {}
|
|
131
|
-
synced++;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
157
|
+
} else {
|
|
158
|
+
if (shouldSyncTextFile(src, dst)) {
|
|
159
|
+
copyFileSync(src, dst);
|
|
160
|
+
try { chmodSync(dst, 0o755); } catch {}
|
|
161
|
+
synced++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Worker 의존성 동기화 (MCP SDK + transitive deps) ──
|
|
167
|
+
|
|
168
|
+
const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
|
|
169
|
+
const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
|
|
170
|
+
const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
|
|
171
|
+
|
|
172
|
+
// native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
|
|
173
|
+
const SKIP_PACKAGES = new Set(["better-sqlite3", "prebuild-install", "node-abi", "node-addon-api"]);
|
|
174
|
+
|
|
175
|
+
if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
|
|
176
|
+
try {
|
|
177
|
+
const { cpSync } = await import("fs");
|
|
178
|
+
for (const entry of readdirSync(srcNodeModules)) {
|
|
179
|
+
if (SKIP_PACKAGES.has(entry)) continue;
|
|
180
|
+
|
|
181
|
+
const src = join(srcNodeModules, entry);
|
|
182
|
+
const dst = join(workerNodeModules, entry);
|
|
183
|
+
if (existsSync(dst)) continue;
|
|
184
|
+
|
|
185
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
186
|
+
cpSync(src, dst, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
synced++;
|
|
189
|
+
} catch {
|
|
190
|
+
// best effort: 의존성 복사 실패 시 exec fallback으로 동작
|
|
191
|
+
}
|
|
134
192
|
}
|
|
135
193
|
|
|
136
194
|
// ── 스킬 동기화 ──
|
|
@@ -300,36 +358,36 @@ if (codexProfilesAdded > 0) {
|
|
|
300
358
|
synced++;
|
|
301
359
|
}
|
|
302
360
|
|
|
303
|
-
// ── MCP 인벤토리 백그라운드 갱신 ──
|
|
304
|
-
|
|
305
|
-
const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
|
|
306
|
-
if (existsSync(mcpCheck)) {
|
|
307
|
-
const child = spawn(process.execPath, [mcpCheck], {
|
|
361
|
+
// ── MCP 인벤토리 백그라운드 갱신 ──
|
|
362
|
+
|
|
363
|
+
const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
|
|
364
|
+
if (existsSync(mcpCheck)) {
|
|
365
|
+
const child = spawn(process.execPath, [mcpCheck], {
|
|
308
366
|
detached: true,
|
|
309
367
|
stdio: "ignore",
|
|
310
368
|
});
|
|
311
|
-
child.unref(); // 부모 프로세스와 분리 — 비동기 실행
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// ── Hub 헬스체크 + 자동 기동 (세션 시작 백그라운드) ──
|
|
315
|
-
// setup 훅이 포그라운드 지연을 만들지 않도록 별도 detached 프로세스로 처리한다.
|
|
316
|
-
const hubEnsure = join(PLUGIN_ROOT, "scripts", "hub-ensure.mjs");
|
|
317
|
-
const isPostinstall = process.env.npm_lifecycle_event === "postinstall";
|
|
318
|
-
const isCi = /^(1|true)$/i.test(process.env.CI || "");
|
|
319
|
-
const disableHubAutostart = process.env.TFX_DISABLE_HUB_AUTOSTART === "1";
|
|
320
|
-
|
|
321
|
-
if (!isPostinstall && !isCi && !disableHubAutostart && existsSync(hubEnsure)) {
|
|
322
|
-
try {
|
|
323
|
-
const child = spawn(process.execPath, [hubEnsure], {
|
|
324
|
-
env: process.env,
|
|
325
|
-
detached: true,
|
|
326
|
-
stdio: "ignore",
|
|
327
|
-
});
|
|
328
|
-
child.unref();
|
|
329
|
-
} catch {
|
|
330
|
-
// best effort: 실패해도 setup 흐름은 지속
|
|
331
|
-
}
|
|
332
|
-
}
|
|
369
|
+
child.unref(); // 부모 프로세스와 분리 — 비동기 실행
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── Hub 헬스체크 + 자동 기동 (세션 시작 백그라운드) ──
|
|
373
|
+
// setup 훅이 포그라운드 지연을 만들지 않도록 별도 detached 프로세스로 처리한다.
|
|
374
|
+
const hubEnsure = join(PLUGIN_ROOT, "scripts", "hub-ensure.mjs");
|
|
375
|
+
const isPostinstall = process.env.npm_lifecycle_event === "postinstall";
|
|
376
|
+
const isCi = /^(1|true)$/i.test(process.env.CI || "");
|
|
377
|
+
const disableHubAutostart = process.env.TFX_DISABLE_HUB_AUTOSTART === "1";
|
|
378
|
+
|
|
379
|
+
if (!isPostinstall && !isCi && !disableHubAutostart && existsSync(hubEnsure)) {
|
|
380
|
+
try {
|
|
381
|
+
const child = spawn(process.execPath, [hubEnsure], {
|
|
382
|
+
env: process.env,
|
|
383
|
+
detached: true,
|
|
384
|
+
stdio: "ignore",
|
|
385
|
+
});
|
|
386
|
+
child.unref();
|
|
387
|
+
} catch {
|
|
388
|
+
// best effort: 실패해도 setup 흐름은 지속
|
|
389
|
+
}
|
|
390
|
+
}
|
|
333
391
|
|
|
334
392
|
// ── postinstall 배너 (npm install 시에만 출력) ──
|
|
335
393
|
|
|
@@ -357,23 +415,23 @@ ${B}╚════════════════════════
|
|
|
357
415
|
${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
|
|
358
416
|
${G}✓${R} HUD statusLine → settings.json
|
|
359
417
|
|
|
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})
|
|
418
|
+
${B}Commands:${R}
|
|
419
|
+
${C}triflux${R} setup 파일 동기화 + HUD 설정
|
|
420
|
+
${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
|
|
421
|
+
${C}triflux${R} list 설치된 스킬 목록
|
|
422
|
+
${C}triflux${R} update 최신 안정 버전으로 업데이트
|
|
423
|
+
${C}triflux${R} update --dev dev 채널로 업데이트 (${D}dev 별칭 지원${R})
|
|
366
424
|
|
|
367
425
|
${B}Shortcuts:${R}
|
|
368
426
|
${C}tfx${R} triflux 축약
|
|
369
427
|
${C}tfx-setup${R} triflux setup
|
|
370
428
|
${C}tfx-doctor${R} triflux doctor
|
|
371
429
|
|
|
372
|
-
${B}Skills (Claude Code):${R}
|
|
373
|
-
${C}/tfx-auto${R} "작업" 자동 분류 + 병렬 실행
|
|
374
|
-
${C}/tfx-auto-codex${R} "작업" Codex 리드 + Gemini 유지
|
|
375
|
-
${C}/tfx-codex${R} "작업" Codex 전용 모드
|
|
376
|
-
${C}/tfx-gemini${R} "작업" Gemini 전용 모드
|
|
430
|
+
${B}Skills (Claude Code):${R}
|
|
431
|
+
${C}/tfx-auto${R} "작업" 자동 분류 + 병렬 실행
|
|
432
|
+
${C}/tfx-auto-codex${R} "작업" Codex 리드 + Gemini 유지
|
|
433
|
+
${C}/tfx-codex${R} "작업" Codex 전용 모드
|
|
434
|
+
${C}/tfx-gemini${R} "작업" Gemini 전용 모드
|
|
377
435
|
${C}/tfx-setup${R} HUD 설정 + 진단
|
|
378
436
|
|
|
379
437
|
${Y}!${R} 세션 재시작 후 스킬이 활성화됩니다
|