triflux 10.3.4 → 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/bin/tfx-doctor-tui.mjs +1 -1
- package/bin/tfx-doctor.mjs +6 -1
- package/bin/tfx-profile.mjs +1 -1
- package/bin/tfx-setup-tui.mjs +1 -1
- package/bin/tfx-setup.mjs +6 -1
- package/bin/triflux.mjs +2396 -1140
- package/hooks/agent-route-guard.mjs +12 -8
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +19 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +108 -24
- package/hooks/hook-registry.json +11 -0
- package/hooks/keyword-rules.json +6 -10
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/pipeline-stop.mjs +3 -6
- package/hooks/safety-guard.mjs +99 -28
- package/hooks/session-start-fast.mjs +143 -0
- package/hooks/subagent-verifier.mjs +5 -4
- package/hub/account-broker.mjs +256 -60
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +95 -57
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +67 -30
- package/hub/bridge.mjs +0 -1
- package/hub/cli-adapter-base.mjs +200 -48
- package/hub/codex-adapter.mjs +76 -96
- package/hub/codex-compat.mjs +3 -3
- package/hub/codex-preflight.mjs +63 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +3 -3
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +5 -5
- package/hub/fullcycle.mjs +33 -17
- package/hub/gemini-adapter.mjs +69 -94
- package/hub/hitl.mjs +89 -30
- package/hub/intent.mjs +161 -38
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/mcp-response-cache.mjs +66 -32
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +35 -37
- package/hub/lib/process-utils.mjs +106 -37
- package/hub/lib/spawn-trace.mjs +527 -0
- package/hub/lib/ssh-command.mjs +34 -4
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +57 -48
- package/hub/promote-penalties.mjs +25 -7
- package/hub/quality/deslop.mjs +70 -49
- package/hub/research.mjs +32 -25
- package/hub/router.mjs +240 -107
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/server.mjs +4 -4
- package/hub/session-fingerprint.mjs +126 -60
- package/hub/state.mjs +84 -43
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +22 -7
- package/hub/team/ansi.mjs +186 -122
- package/hub/team/backend.mjs +28 -10
- package/hub/team/cli/commands/attach.mjs +29 -9
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +32 -11
- package/hub/team/cli/commands/focus.mjs +38 -11
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +11 -4
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -31
- package/hub/team/cli/commands/start/parse-args.mjs +38 -11
- package/hub/team/cli/commands/start/start-headless.mjs +112 -36
- package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
- package/hub/team/cli/commands/start/start-mux.mjs +70 -21
- package/hub/team/cli/commands/start/start-wt.mjs +29 -12
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +30 -8
- package/hub/team/cli/services/attach-fallback.mjs +31 -11
- package/hub/team/cli/services/hub-client.mjs +42 -14
- package/hub/team/cli/services/member-selector.mjs +11 -4
- package/hub/team/cli/services/native-control.mjs +48 -21
- package/hub/team/cli/services/runtime-mode.mjs +2 -1
- package/hub/team/cli/services/state-store.mjs +25 -8
- package/hub/team/cli/services/task-model.mjs +16 -6
- package/hub/team/conductor-mesh-bridge.mjs +24 -23
- package/hub/team/conductor.mjs +8 -4
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +41 -21
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/handoff.mjs +31 -15
- package/hub/team/headless.mjs +2 -1
- package/hub/team/health-probe.mjs +69 -54
- package/hub/team/launcher-template.mjs +16 -13
- package/hub/team/native-supervisor.mjs +65 -21
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +119 -76
- package/hub/team/orchestrator.mjs +9 -4
- package/hub/team/pane.mjs +12 -7
- package/hub/team/process-cleanup.mjs +25 -16
- package/hub/team/psmux.mjs +491 -201
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +117 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +3 -1
- package/hub/team/session.mjs +98 -34
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-locks.mjs +15 -13
- package/hub/team/swarm-planner.mjs +32 -21
- package/hub/team/swarm-reconciler.mjs +48 -23
- package/hub/team/tui-lite.mjs +266 -68
- package/hub/team/tui-remote-adapter.mjs +14 -10
- package/hub/team/tui-viewer.mjs +99 -43
- package/hub/team/tui.mjs +708 -271
- package/hub/team/worktree-lifecycle.mjs +152 -58
- package/hub/team/wt-manager.mjs +24 -14
- package/hub/token-mode.mjs +71 -71
- package/hub/tray.mjs +66 -23
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-mcp.mjs +192 -141
- package/hub/workers/delegator-mcp.mjs +507 -333
- package/hub/workers/factory.mjs +8 -8
- package/hub/workers/gemini-worker.mjs +115 -84
- package/hub/workers/interface.mjs +6 -1
- package/hub/workers/worker-utils.mjs +21 -14
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +162 -26
- package/hud/context-monitor.mjs +82 -41
- package/hud/hud-qos-status.mjs +129 -49
- package/hud/mission-board.mjs +6 -3
- package/hud/providers/claude.mjs +226 -115
- package/hud/providers/codex.mjs +62 -22
- package/hud/providers/gemini.mjs +168 -56
- package/hud/renderers.mjs +384 -119
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +78 -38
- package/mesh/index.mjs +11 -5
- package/mesh/mesh-budget.mjs +18 -9
- package/mesh/mesh-heartbeat.mjs +1 -1
- package/mesh/mesh-queue.mjs +3 -5
- package/mesh/mesh-router.mjs +5 -4
- package/package.json +2 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
- package/scripts/__tests__/keyword-detector.test.mjs +77 -28
- package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
- package/scripts/__tests__/remote-spawn.test.mjs +10 -4
- package/scripts/__tests__/session-start-fast.test.mjs +36 -0
- package/scripts/__tests__/skill-template.test.mjs +98 -50
- package/scripts/__tests__/smoke.test.mjs +1 -1
- package/scripts/__tests__/spawn-trace.test.mjs +102 -0
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
- package/scripts/cache-doctor.mjs +11 -4
- package/scripts/cache-warmup.mjs +96 -37
- package/scripts/claudemd-sync.mjs +27 -17
- package/scripts/codex-gateway-preflight.mjs +52 -37
- package/scripts/codex-mcp-gateway-sync.mjs +59 -39
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/config-audit.mjs +232 -0
- package/scripts/convert-to-tmpl.mjs +54 -0
- package/scripts/cross-review-gate.mjs +35 -12
- package/scripts/cross-review-tracker.mjs +21 -8
- package/scripts/demo.mjs +35 -17
- package/scripts/doctor-diagnose.mjs +284 -0
- package/scripts/gen-skill-docs.mjs +7 -2
- package/scripts/gen-skill-manifest.mjs +2 -1
- package/scripts/headless-guard.mjs +86 -48
- package/scripts/hub-ensure.mjs +45 -26
- package/scripts/keyword-detector.mjs +41 -20
- package/scripts/keyword-rules-expander.mjs +47 -30
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +6 -3
- package/scripts/lib/env-probe.mjs +47 -28
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/keyword-rules.mjs +43 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +377 -239
- package/scripts/lib/mcp-guard-engine.mjs +194 -79
- package/scripts/lib/mcp-manifest.mjs +23 -13
- package/scripts/lib/mcp-server-catalog.mjs +300 -63
- package/scripts/lib/psmux-info.mjs +11 -6
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-template.mjs +30 -7
- package/scripts/mcp-check.mjs +58 -39
- package/scripts/mcp-gateway-config.mjs +83 -39
- package/scripts/mcp-gateway-ensure.mjs +43 -35
- package/scripts/mcp-gateway-integration-test.mjs +70 -58
- package/scripts/mcp-gateway-start.mjs +126 -60
- package/scripts/mcp-gateway-verify.mjs +24 -22
- package/scripts/mcp-safety-guard.mjs +44 -11
- package/scripts/notion-read.mjs +199 -84
- package/scripts/pack.mjs +94 -89
- package/scripts/preflight-cache.mjs +27 -10
- package/scripts/preinstall.mjs +42 -13
- package/scripts/remote-spawn.mjs +309 -94
- package/scripts/run.cjs +8 -5
- package/scripts/session-spawn-helper.mjs +130 -39
- package/scripts/session-stale-cleanup.mjs +123 -0
- package/scripts/setup.mjs +941 -492
- package/scripts/test-lock.mjs +20 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
- package/scripts/tfx-batch-stats.mjs +32 -11
- package/scripts/tfx-gate-activate.mjs +11 -4
- package/scripts/tfx-route-post.mjs +87 -20
- package/scripts/tfx-route-worker.mjs +57 -51
- package/scripts/tfx-route.sh +41 -124
- package/scripts/tmp-cleanup.mjs +21 -7
- package/scripts/token-snapshot.mjs +204 -85
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/_templates/base.md +1 -6
- package/skills/merge-worktree/SKILL.md.tmpl +144 -0
- package/skills/shared/telemetry-segment.md +6 -0
- package/skills/star-prompt/SKILL.md.tmpl +222 -0
- package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
- package/skills/tfx-analysis/skill.json +1 -6
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
- package/skills/tfx-auto-codex/skill.json +1 -3
- package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
- package/skills/tfx-autopilot/skill.json +1 -5
- package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
- package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
- package/skills/tfx-autoroute/skill.json +1 -7
- package/skills/tfx-codex/SKILL.md +1 -0
- package/skills/tfx-codex/skill.json +1 -3
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
- package/skills/tfx-codex-swarm/evals/evals.json +1 -1
- package/skills/tfx-codex-swarm/skill.json +1 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
- package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
- package/skills/tfx-debate/SKILL.md.tmpl +192 -0
- package/skills/tfx-debate/skill.json +1 -7
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
- package/skills/tfx-deep-analysis/skill.json +1 -5
- package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
- package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
- package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
- package/skills/tfx-deep-qa/skill.json +1 -6
- package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
- package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
- package/skills/tfx-doctor/SKILL.md +21 -0
- package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
- package/skills/tfx-doctor/skill.json +1 -3
- package/skills/tfx-find/SKILL.md +1 -0
- package/skills/tfx-forge/SKILL.md.tmpl +187 -0
- package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
- package/skills/tfx-fullcycle/skill.json +1 -6
- package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
- package/skills/tfx-gemini/skill.json +1 -3
- package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
- package/skills/tfx-hooks/skill.json +1 -3
- package/skills/tfx-hub/SKILL.md.tmpl +212 -0
- package/skills/tfx-hub/skill.json +1 -3
- package/skills/tfx-index/SKILL.md +1 -0
- package/skills/tfx-index/skill.json +1 -6
- package/skills/tfx-interview/SKILL.md.tmpl +285 -0
- package/skills/tfx-multi/SKILL.md.tmpl +183 -0
- package/skills/tfx-multi/skill.json +1 -3
- package/skills/tfx-panel/SKILL.md.tmpl +189 -0
- package/skills/tfx-panel/skill.json +1 -7
- package/skills/tfx-persist/SKILL.md.tmpl +270 -0
- package/skills/tfx-persist/skill.json +1 -7
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-plan/skill.json +1 -6
- package/skills/tfx-profile/SKILL.md.tmpl +239 -0
- package/skills/tfx-profile/skill.json +1 -3
- package/skills/tfx-prune/SKILL.md.tmpl +200 -0
- package/skills/tfx-prune/skill.json +1 -7
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
- package/skills/tfx-psmux-rules/skill.json +1 -4
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-qa/skill.json +1 -6
- package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
- package/skills/tfx-ralph/skill.json +1 -4
- package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
- package/skills/tfx-remote-setup/skill.json +1 -3
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
- package/skills/tfx-remote-spawn/references/hosts.json +16 -0
- package/skills/tfx-remote-spawn/skill.json +1 -4
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +1 -0
- package/skills/tfx-review/skill.json +1 -6
- package/skills/tfx-setup/SKILL.md.tmpl +504 -0
- package/skills/tfx-setup/skill.json +1 -3
- package/skills/tfx-swarm/SKILL.md +22 -0
- package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
- package/tui/codex-profile.mjs +88 -33
- package/tui/core.mjs +45 -15
- package/tui/doctor.mjs +75 -28
- package/tui/gemini-profile.mjs +74 -29
- package/tui/monitor-data.mjs +8 -4
- package/tui/monitor.mjs +71 -27
- package/tui/setup.mjs +133 -42
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tfx-swarm
|
|
3
|
+
description: "tfx-swarm — 다중 기기 x 다중 모델 스웜 오케스트레이션"
|
|
4
|
+
triggers:
|
|
5
|
+
- swarm
|
|
6
|
+
- 스웜
|
|
7
|
+
- 병렬 실행
|
|
8
|
+
- codex-swarm
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# {{SKILL_NAME}} — 다중 기기 x 다중 모델 스웜 오케스트레이션
|
|
12
|
+
|
|
13
|
+
{{> base}}
|
|
14
|
+
|
|
15
|
+
> **Multi-Machine x Multi-Model Swarm.** PRD 하나로 로컬과 원격 머신에서
|
|
16
|
+
> Claude + Codex + Gemini를 병렬 실행하고, file-lease로 충돌을 방지하며,
|
|
17
|
+
> 결과를 자동 통합한다. triflux의 킬러 스킬.
|
|
18
|
+
|
|
19
|
+
## 트리거
|
|
20
|
+
|
|
21
|
+
- `swarm`, `스웜`, `병렬 실행`, `다중 워커`, `PRD 실행`, `swarm launch`
|
|
22
|
+
- `codex-swarm` (backward compat -> 이 스킬로 라우팅)
|
|
23
|
+
|
|
24
|
+
## 핵심 기능
|
|
25
|
+
|
|
26
|
+
| 기능 | 설명 |
|
|
27
|
+
|------|------|
|
|
28
|
+
| **다중 모델** | shard별 `agent: codex\|gemini\|claude` 지정. 작업 특성에 맞는 모델 배치 |
|
|
29
|
+
| **다중 기기** | shard별 `host: <ssh-host>` 지정. 로컬/원격 혼합 실행 |
|
|
30
|
+
| **File Lease** | shard별 파일 소유권. 동일 파일 동시 수정 방지 |
|
|
31
|
+
| **Redundant Execution** | critical shard는 다른 모델로 이중 실행 후 reconcile |
|
|
32
|
+
| **자동 통합** | 의존 순서대로 merge. 원격 shard는 SSH 경유 fetch |
|
|
33
|
+
| **장애 폴백** | rate limit -> 다른 모델로 자동 전환 (codex<->gemini<->claude) |
|
|
34
|
+
|
|
35
|
+
## 전제조건
|
|
36
|
+
|
|
37
|
+
- psmux >= 3.3.0
|
|
38
|
+
- Hub 실행 중 (`curl -sf http://127.0.0.1:27888/status`)
|
|
39
|
+
- Codex CLI 또는 Gemini CLI (사용할 agent에 따라)
|
|
40
|
+
- 원격 shard 사용 시: SSH 키 인증 + 원격 머신에 Claude Code 설치
|
|
41
|
+
- 프로젝트에 `docs/prd/` 디렉토리 존재
|
|
42
|
+
|
|
43
|
+
## 실행 흐름
|
|
44
|
+
|
|
45
|
+
### Step 1: PRD 탐색
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
find docs/prd -name '*.md' -not -name '_template.md' -not -path '*/archived/*' | sort
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
AskUserQuestion으로 실행할 PRD 선택. 복수 선택 시 각각 독립 shard.
|
|
52
|
+
|
|
53
|
+
### Step 2: 원격/모델 구성 확인
|
|
54
|
+
|
|
55
|
+
PRD 파싱 후, shard에 `host:` 필드가 있으면 AskUserQuestion으로 원격 실행 여부를 확인한다.
|
|
56
|
+
|
|
57
|
+
AskUserQuestion:
|
|
58
|
+
|
|
59
|
+
> PRD에 원격 호스트가 지정된 shard가 있습니다:
|
|
60
|
+
> - `{shard_name}` -> `{host}` ({agent})
|
|
61
|
+
>
|
|
62
|
+
> 원격 실행을 어떻게 할까요?
|
|
63
|
+
|
|
64
|
+
Options:
|
|
65
|
+
- A) 원격 포함 실행 (PRD 그대로) — 로컬+원격 혼합
|
|
66
|
+
- B) 전부 로컬에서 실행 — host 필드 무시
|
|
67
|
+
- C) 원격만 실행 — 로컬 shard 건너뛰기
|
|
68
|
+
|
|
69
|
+
PRD에 다중 agent가 지정된 경우에도 AskUserQuestion:
|
|
70
|
+
|
|
71
|
+
> PRD에 여러 모델이 지정되어 있습니다:
|
|
72
|
+
> - codex: {N}개 shard
|
|
73
|
+
> - gemini: {N}개 shard
|
|
74
|
+
> - claude: {N}개 shard
|
|
75
|
+
>
|
|
76
|
+
> 모델 배치를 어떻게 할까요?
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
- A) PRD 그대로 — shard별 지정 모델 사용 (권장)
|
|
80
|
+
- B) 전부 Codex로 — 단일 모델
|
|
81
|
+
- C) 전부 Gemini로 — 단일 모델
|
|
82
|
+
|
|
83
|
+
### Step 3: 계획 생성
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
import { planSwarm } from '../../hub/team/swarm-planner.mjs';
|
|
87
|
+
|
|
88
|
+
const swarmPlan = planSwarm(selectedPrdPath);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
계획을 사용자에게 보여주고 승인 요청:
|
|
92
|
+
- shard 수, 파일 배분, lease 맵, merge 순서
|
|
93
|
+
- 원격 shard 표시 (host 정보)
|
|
94
|
+
- 모델 배분 표시 (agent 정보)
|
|
95
|
+
- critical shard 표시 (redundant execution 대상)
|
|
96
|
+
|
|
97
|
+
### Step 4: Hypervisor 실행
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
import { createSwarmHypervisor } from '../../hub/team/swarm-hypervisor.mjs';
|
|
101
|
+
|
|
102
|
+
const hyper = createSwarmHypervisor({
|
|
103
|
+
workdir: process.cwd(),
|
|
104
|
+
logsDir: join(process.cwd(), '.triflux', 'swarm-logs'),
|
|
105
|
+
maxRestarts: 2,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const run = hyper.launch(swarmPlan);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
실행 중 상태 모니터링:
|
|
112
|
+
- `hyper.on('shardLaunched', ...)` -> 진행 표시
|
|
113
|
+
- `hyper.on('shardCompleted', ...)` -> 완료/실패 표시
|
|
114
|
+
- `hyper.on('warning', ...)` -> 경고 (파일 충돌 등)
|
|
115
|
+
|
|
116
|
+
원격 shard 실행 시:
|
|
117
|
+
1. `probeRemoteEnv(host)` -> 원격 환경 감지 (OS, shell, Claude 경로)
|
|
118
|
+
2. conductor가 `remote: true` 설정으로 SSH 경유 세션 실행
|
|
119
|
+
3. 완료 후 `fetchRemoteShard()`로 원격 브랜치를 로컬로 fetch
|
|
120
|
+
|
|
121
|
+
### Step 5: 결과 검증 + 통합
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const status = hyper.getStatus();
|
|
125
|
+
// status.completedShards, status.failedShards, status.workers
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
통합 결과를 사용자에게 보고:
|
|
129
|
+
- 성공: merged shard 목록 (로컬/원격 구분 표시)
|
|
130
|
+
- 실패: 충돌/실패 shard 목록 + 수동 해결 안내
|
|
131
|
+
|
|
132
|
+
### Step 6: 정리
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
await hyper.shutdown('completed');
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## PRD 예제: 다중 기기 x 다중 모델
|
|
139
|
+
|
|
140
|
+
```markdown
|
|
141
|
+
## Shard: auth-refactor
|
|
142
|
+
- agent: codex
|
|
143
|
+
- files: src/auth.mjs, src/middleware/jwt.mjs
|
|
144
|
+
- prompt: JWT 인증 미들웨어 리팩터링
|
|
145
|
+
|
|
146
|
+
## Shard: ui-dashboard
|
|
147
|
+
- agent: gemini
|
|
148
|
+
- files: src/ui/dashboard.mjs, src/ui/charts.mjs
|
|
149
|
+
- prompt: 대시보드 UI 개선
|
|
150
|
+
|
|
151
|
+
## Shard: security-audit
|
|
152
|
+
- agent: claude
|
|
153
|
+
- host: ryzen5-7600
|
|
154
|
+
- files: src/security.mjs
|
|
155
|
+
- critical: true
|
|
156
|
+
- prompt: 보안 취약점 감사
|
|
157
|
+
|
|
158
|
+
## Shard: perf-optimization
|
|
159
|
+
- agent: codex
|
|
160
|
+
- host: m2
|
|
161
|
+
- files: src/engine/optimizer.mjs
|
|
162
|
+
- depends: auth-refactor
|
|
163
|
+
- prompt: 성능 최적화 (auth 리팩터 완료 후)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
위 PRD 하나로: Codex(로컬) + Gemini(로컬) + Claude(ryzen5-7600) + Codex(m2) 4개 워커가 병렬 실행된다.
|
|
167
|
+
security-audit는 `critical: true`이므로 다른 모델로 이중 실행 후 reconcile.
|
|
168
|
+
|
|
169
|
+
## PRD Shard 필드 레퍼런스
|
|
170
|
+
|
|
171
|
+
| 필드 | 필수 | 기본값 | 설명 |
|
|
172
|
+
|------|------|--------|------|
|
|
173
|
+
| `agent` | - | `codex` | 실행 모델: `codex`, `gemini`, `claude` |
|
|
174
|
+
| `host` | - | (로컬) | SSH 호스트. 미지정 시 로컬 실행 |
|
|
175
|
+
| `files` | O | - | shard가 수정할 파일 목록 (file-lease 대상) |
|
|
176
|
+
| `depends` | - | - | 의존하는 shard 이름. 해당 shard 완료 후 실행 |
|
|
177
|
+
| `critical` | - | `false` | `true`면 다른 모델로 이중 실행 + reconcile |
|
|
178
|
+
| `mcp` | - | - | 필요한 MCP 서버 목록 |
|
|
179
|
+
| `prompt` | O | - | shard 실행 프롬프트. `\|`로 멀티라인 |
|
|
180
|
+
|
|
181
|
+
## 제약
|
|
182
|
+
|
|
183
|
+
- `codex exec` / `gemini -p` 직접 호출 금지 (headless-guard)
|
|
184
|
+
- MAX_CONCURRENCY 기본 4 (WT 프리징 방지)
|
|
185
|
+
- WT 조작 간 sleep 2s (race-guard)
|
|
186
|
+
- config.toml과 CLI 플래그 중복 지정 금지
|
|
187
|
+
|
|
188
|
+
## Redundant Execution
|
|
189
|
+
|
|
190
|
+
critical shard (hard lease 또는 고위험 파일)에 대해:
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
import { shouldRunRedundant, reconcile } from '../../hub/team/swarm-reconciler.mjs';
|
|
194
|
+
|
|
195
|
+
if (shouldRunRedundant(shard)) {
|
|
196
|
+
// primary + verifier 이중 실행
|
|
197
|
+
const decision = await reconcile(primaryResult, verifierResult);
|
|
198
|
+
// decision.selected: 'primary' | 'verifier' | 'hitl'
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
HITL fallback 시 AskUserQuestion으로 사용자에게 선택 요청.
|
|
203
|
+
|
|
204
|
+
## 장애 처리
|
|
205
|
+
|
|
206
|
+
| 장애 | 분류 | 대응 |
|
|
207
|
+
|------|------|------|
|
|
208
|
+
| 워커 크래시 | F1 | conductor auto-restart (최대 2회) |
|
|
209
|
+
| Rate limit | F2 | 다른 모델로 자동 전환 (codex->gemini->claude) |
|
|
210
|
+
| Stall | F3 | health probe 감지 -> kill -> restart |
|
|
211
|
+
| File lease 위반 | F4 | 워커 변경 revert, shard 실패 처리 |
|
|
212
|
+
| Merge 충돌 | F5 | 충돌 해결 재시도 |
|
|
213
|
+
|
|
214
|
+
## 기존 스킬과의 관계
|
|
215
|
+
|
|
216
|
+
- **tfx-codex-swarm**: deprecated. 이 스킬로 통합됨 (backward compat alias)
|
|
217
|
+
- **tfx-remote-spawn**: 단독 원격 세션 관리(list, attach, send)용으로 유지. swarm은 다중 shard 병렬 관리
|
|
218
|
+
- **tfx-multi**: Claude Native Teams 기반 로컬 오케스트레이션. swarm은 PRD 기반 worktree 분할
|
package/tui/codex-profile.mjs
CHANGED
|
@@ -1,32 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// tui/codex-profile.mjs — Interactive Codex Profile Manager
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
copyFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
5
10
|
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
6
12
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
BOLD,
|
|
14
|
+
box,
|
|
15
|
+
CYAN,
|
|
16
|
+
clear,
|
|
17
|
+
confirm,
|
|
18
|
+
DIM,
|
|
19
|
+
divider,
|
|
20
|
+
fail,
|
|
21
|
+
GREEN,
|
|
22
|
+
info,
|
|
23
|
+
input,
|
|
24
|
+
label,
|
|
25
|
+
ok,
|
|
26
|
+
onExit,
|
|
27
|
+
RED,
|
|
28
|
+
RESET,
|
|
29
|
+
select,
|
|
30
|
+
showCursor,
|
|
31
|
+
table,
|
|
32
|
+
WHITE,
|
|
33
|
+
warn,
|
|
34
|
+
YELLOW,
|
|
11
35
|
} from "./core.mjs";
|
|
12
36
|
|
|
13
37
|
const CODEX_DIR = join(homedir(), ".codex");
|
|
14
38
|
const CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
15
39
|
|
|
16
40
|
const KNOWN_MODELS = [
|
|
17
|
-
{ label: "gpt-5.4",
|
|
18
|
-
{ label: "gpt-5.3-codex",
|
|
41
|
+
{ label: "gpt-5.4", hint: "최신 플래그십" },
|
|
42
|
+
{ label: "gpt-5.3-codex", hint: "코딩 특화" },
|
|
19
43
|
{ label: "gpt-5.1-codex-mini", hint: "경량 Spark" },
|
|
20
|
-
{ label: "o3",
|
|
21
|
-
{ label: "o4-mini",
|
|
22
|
-
{ label: "직접 입력",
|
|
44
|
+
{ label: "o3", hint: "추론 특화" },
|
|
45
|
+
{ label: "o4-mini", hint: "추론 경량" },
|
|
46
|
+
{ label: "직접 입력", hint: "" },
|
|
23
47
|
];
|
|
24
48
|
|
|
25
49
|
const EFFORT_LEVELS = [
|
|
26
|
-
{ label: "low",
|
|
50
|
+
{ label: "low", hint: "빠른 응답, 최소 추론" },
|
|
27
51
|
{ label: "medium", hint: "균형 잡힌 추론" },
|
|
28
|
-
{ label: "high",
|
|
29
|
-
{ label: "xhigh",
|
|
52
|
+
{ label: "high", hint: "깊은 추론" },
|
|
53
|
+
{ label: "xhigh", hint: "최대 추론 (느림)" },
|
|
30
54
|
];
|
|
31
55
|
|
|
32
56
|
// ── TOML Parsing ──
|
|
@@ -175,7 +199,10 @@ function showStatus(config) {
|
|
|
175
199
|
|
|
176
200
|
console.log();
|
|
177
201
|
label("기본 모델", `${WHITE}${defaults.model || "미설정"}${RESET}`);
|
|
178
|
-
label(
|
|
202
|
+
label(
|
|
203
|
+
"기본 Effort",
|
|
204
|
+
`${WHITE}${defaults.model_reasoning_effort || "미설정"}${RESET}`,
|
|
205
|
+
);
|
|
179
206
|
console.log();
|
|
180
207
|
|
|
181
208
|
if (profiles.length === 0) {
|
|
@@ -208,7 +235,9 @@ function effortColor(effort) {
|
|
|
208
235
|
|
|
209
236
|
async function pickModel(current) {
|
|
210
237
|
const idx = KNOWN_MODELS.findIndex((m) => m.label === current);
|
|
211
|
-
const choice = await select("모델 선택", KNOWN_MODELS, {
|
|
238
|
+
const choice = await select("모델 선택", KNOWN_MODELS, {
|
|
239
|
+
initial: Math.max(0, idx),
|
|
240
|
+
});
|
|
212
241
|
if (!choice) return null;
|
|
213
242
|
if (choice.value.label === "직접 입력") {
|
|
214
243
|
return await input("모델 ID", current || "");
|
|
@@ -218,7 +247,9 @@ async function pickModel(current) {
|
|
|
218
247
|
|
|
219
248
|
async function pickEffort(current) {
|
|
220
249
|
const idx = EFFORT_LEVELS.findIndex((e) => e.label === current);
|
|
221
|
-
const choice = await select("Reasoning Effort 선택", EFFORT_LEVELS, {
|
|
250
|
+
const choice = await select("Reasoning Effort 선택", EFFORT_LEVELS, {
|
|
251
|
+
initial: Math.max(0, idx),
|
|
252
|
+
});
|
|
222
253
|
if (!choice) return null;
|
|
223
254
|
return choice.value.label;
|
|
224
255
|
}
|
|
@@ -240,7 +271,9 @@ async function editProfile(config) {
|
|
|
240
271
|
|
|
241
272
|
const profile = profiles[picked.index];
|
|
242
273
|
console.log();
|
|
243
|
-
info(
|
|
274
|
+
info(
|
|
275
|
+
`현재: ${BOLD}${profile.name}${RESET} → ${profile.model} / ${profile.model_reasoning_effort}`,
|
|
276
|
+
);
|
|
244
277
|
|
|
245
278
|
const newModel = await pickModel(profile.model);
|
|
246
279
|
if (newModel === null) return config;
|
|
@@ -249,7 +282,9 @@ async function editProfile(config) {
|
|
|
249
282
|
if (newEffort === null) return config;
|
|
250
283
|
|
|
251
284
|
console.log();
|
|
252
|
-
info(
|
|
285
|
+
info(
|
|
286
|
+
`변경: ${profile.model} → ${BOLD}${newModel}${RESET}, ${profile.model_reasoning_effort} → ${BOLD}${newEffort}${RESET}`,
|
|
287
|
+
);
|
|
253
288
|
|
|
254
289
|
if (!(await confirm("저장하시겠습니까?"))) return config;
|
|
255
290
|
|
|
@@ -259,7 +294,7 @@ async function editProfile(config) {
|
|
|
259
294
|
if (!["name", "model", "model_reasoning_effort"].includes(k)) props[k] = v;
|
|
260
295
|
}
|
|
261
296
|
|
|
262
|
-
|
|
297
|
+
const raw = writeProfile(config.raw, profile.name, props);
|
|
263
298
|
save(raw);
|
|
264
299
|
ok(`${profile.name} 프로파일 저장 완료`);
|
|
265
300
|
return readConfig();
|
|
@@ -268,7 +303,9 @@ async function editProfile(config) {
|
|
|
268
303
|
async function editDefault(config) {
|
|
269
304
|
const { defaults } = config;
|
|
270
305
|
info(`현재 기본 모델: ${BOLD}${defaults.model || "미설정"}${RESET}`);
|
|
271
|
-
info(
|
|
306
|
+
info(
|
|
307
|
+
`현재 기본 Effort: ${BOLD}${defaults.model_reasoning_effort || "미설정"}${RESET}`,
|
|
308
|
+
);
|
|
272
309
|
|
|
273
310
|
const newModel = await pickModel(defaults.model);
|
|
274
311
|
if (newModel === null) return config;
|
|
@@ -277,7 +314,9 @@ async function editDefault(config) {
|
|
|
277
314
|
if (newEffort === null) return config;
|
|
278
315
|
|
|
279
316
|
console.log();
|
|
280
|
-
info(
|
|
317
|
+
info(
|
|
318
|
+
`변경: ${defaults.model} → ${BOLD}${newModel}${RESET}, ${defaults.model_reasoning_effort} → ${BOLD}${newEffort}${RESET}`,
|
|
319
|
+
);
|
|
281
320
|
|
|
282
321
|
if (!(await confirm("저장하시겠습니까?"))) return config;
|
|
283
322
|
|
|
@@ -307,7 +346,10 @@ async function addProfile(config) {
|
|
|
307
346
|
info(`추가: ${BOLD}${name}${RESET} → ${model} / ${effort}`);
|
|
308
347
|
if (!(await confirm("저장하시겠습니까?"))) return config;
|
|
309
348
|
|
|
310
|
-
const raw = writeProfile(config.raw, name, {
|
|
349
|
+
const raw = writeProfile(config.raw, name, {
|
|
350
|
+
model,
|
|
351
|
+
model_reasoning_effort: effort,
|
|
352
|
+
});
|
|
311
353
|
save(raw);
|
|
312
354
|
ok(`${name} 프로파일 추가 완료`);
|
|
313
355
|
return readConfig();
|
|
@@ -325,7 +367,12 @@ async function removeProfile(config) {
|
|
|
325
367
|
if (!picked) return config;
|
|
326
368
|
|
|
327
369
|
const name = profiles[picked.index].name;
|
|
328
|
-
if (
|
|
370
|
+
if (
|
|
371
|
+
!(await confirm(
|
|
372
|
+
`${RED}${name}${RESET} 프로파일을 삭제하시겠습니까?`,
|
|
373
|
+
false,
|
|
374
|
+
))
|
|
375
|
+
) {
|
|
329
376
|
return config;
|
|
330
377
|
}
|
|
331
378
|
|
|
@@ -350,11 +397,11 @@ function save(content) {
|
|
|
350
397
|
// ── Main Loop ──
|
|
351
398
|
|
|
352
399
|
const MENU = [
|
|
353
|
-
{ label: "프로파일 모델 변경",
|
|
354
|
-
{ label: "기본 모델 변경",
|
|
355
|
-
{ label: "프로파일 추가",
|
|
356
|
-
{ label: "프로파일 삭제",
|
|
357
|
-
{ label: "종료",
|
|
400
|
+
{ label: "프로파일 모델 변경", hint: "모델/effort 수정" },
|
|
401
|
+
{ label: "기본 모델 변경", hint: "top-level default" },
|
|
402
|
+
{ label: "프로파일 추가", hint: "새 프로파일 생성" },
|
|
403
|
+
{ label: "프로파일 삭제", hint: "기존 프로파일 제거" },
|
|
404
|
+
{ label: "종료", hint: "Ctrl+C" },
|
|
358
405
|
];
|
|
359
406
|
|
|
360
407
|
async function main() {
|
|
@@ -384,10 +431,18 @@ async function main() {
|
|
|
384
431
|
|
|
385
432
|
console.log();
|
|
386
433
|
switch (choice.index) {
|
|
387
|
-
case 0:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
case
|
|
434
|
+
case 0:
|
|
435
|
+
config = await editProfile(config);
|
|
436
|
+
break;
|
|
437
|
+
case 1:
|
|
438
|
+
config = await editDefault(config);
|
|
439
|
+
break;
|
|
440
|
+
case 2:
|
|
441
|
+
config = await addProfile(config);
|
|
442
|
+
break;
|
|
443
|
+
case 3:
|
|
444
|
+
config = await removeProfile(config);
|
|
445
|
+
break;
|
|
391
446
|
}
|
|
392
447
|
|
|
393
448
|
console.log();
|
package/tui/core.mjs
CHANGED
|
@@ -45,7 +45,9 @@ export function box(title, width = 50) {
|
|
|
45
45
|
const left = Math.floor((inner - padded.length) / 2);
|
|
46
46
|
const right = inner - left - padded.length;
|
|
47
47
|
console.log(` ${DIM}┌${"─".repeat(inner)}┐${RESET}`);
|
|
48
|
-
console.log(
|
|
48
|
+
console.log(
|
|
49
|
+
` ${DIM}│${RESET}${" ".repeat(left)}${BOLD}${AMBER}${padded}${RESET}${" ".repeat(right)}${DIM}│${RESET}`,
|
|
50
|
+
);
|
|
49
51
|
console.log(` ${DIM}└${"─".repeat(inner)}┘${RESET}`);
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -58,8 +60,8 @@ export function table(headers, rows, { indent = 2 } = {}) {
|
|
|
58
60
|
const widths = headers.map((h, i) =>
|
|
59
61
|
Math.max(
|
|
60
62
|
stripAnsi(h).length,
|
|
61
|
-
...rows.map((r) => stripAnsi(String(r[i] ?? "")).length)
|
|
62
|
-
)
|
|
63
|
+
...rows.map((r) => stripAnsi(String(r[i] ?? "")).length),
|
|
64
|
+
),
|
|
63
65
|
);
|
|
64
66
|
|
|
65
67
|
const top = widths.map((w) => "─".repeat(w + 2)).join("┬");
|
|
@@ -84,10 +86,18 @@ export function table(headers, rows, { indent = 2 } = {}) {
|
|
|
84
86
|
console.log(`${pad}└${bot}┘`);
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
export function ok(msg) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
export function
|
|
89
|
+
export function ok(msg) {
|
|
90
|
+
console.log(` ${GREEN}✓${RESET} ${msg}`);
|
|
91
|
+
}
|
|
92
|
+
export function warn(msg) {
|
|
93
|
+
console.log(` ${YELLOW}⚠${RESET} ${msg}`);
|
|
94
|
+
}
|
|
95
|
+
export function fail(msg) {
|
|
96
|
+
console.log(` ${RED}✗${RESET} ${msg}`);
|
|
97
|
+
}
|
|
98
|
+
export function info(msg) {
|
|
99
|
+
console.log(` ${CYAN}ℹ${RESET} ${msg}`);
|
|
100
|
+
}
|
|
91
101
|
|
|
92
102
|
export function label(key, value) {
|
|
93
103
|
console.log(` ${DIM}${key}:${RESET} ${BOLD}${value}${RESET}`);
|
|
@@ -102,9 +112,14 @@ export async function select(title, options, { initial = 0 } = {}) {
|
|
|
102
112
|
const o = typeof options[i] === "string" ? options[i] : options[i].label;
|
|
103
113
|
console.log(` ${DIM}${i + 1}.${RESET} ${o}`);
|
|
104
114
|
}
|
|
105
|
-
const answer = await input(
|
|
115
|
+
const answer = await input(
|
|
116
|
+
`선택 (1-${options.length})`,
|
|
117
|
+
String(initial + 1),
|
|
118
|
+
);
|
|
106
119
|
const idx = parseInt(answer, 10) - 1;
|
|
107
|
-
return idx >= 0 && idx < options.length
|
|
120
|
+
return idx >= 0 && idx < options.length
|
|
121
|
+
? { index: idx, value: options[idx] }
|
|
122
|
+
: null;
|
|
108
123
|
}
|
|
109
124
|
|
|
110
125
|
readline.emitKeypressEvents(process.stdin);
|
|
@@ -116,7 +131,8 @@ export async function select(title, options, { initial = 0 } = {}) {
|
|
|
116
131
|
const total = options.length;
|
|
117
132
|
|
|
118
133
|
const getLabel = (o) => (typeof o === "string" ? o : o.label);
|
|
119
|
-
const getHint = (o) =>
|
|
134
|
+
const getHint = (o) =>
|
|
135
|
+
typeof o === "object" && o.hint ? ` ${DIM}${o.hint}${RESET}` : "";
|
|
120
136
|
|
|
121
137
|
const render = (first = false) => {
|
|
122
138
|
if (!first) moveUp(total);
|
|
@@ -167,8 +183,13 @@ export async function select(title, options, { initial = 0 } = {}) {
|
|
|
167
183
|
// ── Input: Confirm ──
|
|
168
184
|
|
|
169
185
|
export async function confirm(message, defaultYes = true) {
|
|
170
|
-
const hint = defaultYes
|
|
171
|
-
|
|
186
|
+
const hint = defaultYes
|
|
187
|
+
? `${BOLD}Y${RESET}${DIM}/n${RESET}`
|
|
188
|
+
: `${DIM}y/${RESET}${BOLD}N${RESET}`;
|
|
189
|
+
const rl = readline.createInterface({
|
|
190
|
+
input: process.stdin,
|
|
191
|
+
output: process.stdout,
|
|
192
|
+
});
|
|
172
193
|
|
|
173
194
|
return new Promise((resolve) => {
|
|
174
195
|
rl.question(` ${CYAN}?${RESET} ${message} [${hint}] `, (answer) => {
|
|
@@ -184,7 +205,10 @@ export async function confirm(message, defaultYes = true) {
|
|
|
184
205
|
|
|
185
206
|
export async function input(message, defaultValue = "") {
|
|
186
207
|
const hint = defaultValue ? ` ${DIM}(${defaultValue})${RESET}` : "";
|
|
187
|
-
const rl = readline.createInterface({
|
|
208
|
+
const rl = readline.createInterface({
|
|
209
|
+
input: process.stdin,
|
|
210
|
+
output: process.stdout,
|
|
211
|
+
});
|
|
188
212
|
|
|
189
213
|
return new Promise((resolve) => {
|
|
190
214
|
rl.question(` ${CYAN}?${RESET} ${message}${hint}: `, (answer) => {
|
|
@@ -202,7 +226,9 @@ export function spinner(message) {
|
|
|
202
226
|
hideCursor();
|
|
203
227
|
const id = setInterval(() => {
|
|
204
228
|
clearLine();
|
|
205
|
-
process.stdout.write(
|
|
229
|
+
process.stdout.write(
|
|
230
|
+
` ${CYAN}${frames[i++ % frames.length]}${RESET} ${message}`,
|
|
231
|
+
);
|
|
206
232
|
}, 80);
|
|
207
233
|
|
|
208
234
|
return {
|
|
@@ -230,7 +256,11 @@ export function sleep(ms) {
|
|
|
230
256
|
|
|
231
257
|
// graceful exit
|
|
232
258
|
export function onExit(fn) {
|
|
233
|
-
const handler = () => {
|
|
259
|
+
const handler = () => {
|
|
260
|
+
showCursor();
|
|
261
|
+
fn?.();
|
|
262
|
+
process.exit(0);
|
|
263
|
+
};
|
|
234
264
|
process.on("SIGINT", handler);
|
|
235
265
|
process.on("SIGTERM", handler);
|
|
236
266
|
}
|