@triflux/core 10.53.0 → 10.55.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/hooks/codex-session-hook.mjs +1 -17
- package/hub/cli-adapter-base.mjs +3 -424
- package/hub/index.mjs +0 -5
- package/hub/lib/hosts-compat.mjs +18 -64
- package/hub/lib/private-tmp.mjs +35 -0
- package/hub/lib/process-utils.mjs +19 -220
- package/hub/lib/prompt-tmp.mjs +6 -24
- package/hub/lib/spawn-trace.mjs +112 -5
- package/hub/lib/timeout-defaults.mjs +34 -6
- package/hub/lib/worker-lifecycle.mjs +3 -0
- package/hub/platform.mjs +3 -1
- package/hub/team/agent-map.json +29 -0
- package/hub/team/intervention.mjs +6 -4
- package/hub/team/nativeProxy.mjs +2 -48
- package/hub/team/pane.mjs +329 -0
- package/hub/team/psmux.mjs +1765 -0
- package/hub/team/retry-state-machine.mjs +1 -1
- package/hub/team/session.mjs +541 -0
- package/hud/constants.mjs +0 -3
- package/hud/providers/claude.mjs +35 -15
- package/hud/providers/codex.mjs +169 -42
- package/hud/utils.mjs +0 -23
- package/package.json +2 -2
- package/scripts/lib/backup-rotation.mjs +60 -0
- package/scripts/lib/codex-profile-config.mjs +20 -7
- package/scripts/lib/codex-recovery.sh +2 -3
- package/scripts/lib/install-blob-history.json +3196 -0
- package/scripts/lib/install-retire.mjs +203 -0
- package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
- package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
- package/scripts/lib/machine-profile.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +1 -42
- package/scripts/lib/mcp-guard-engine.mjs +198 -19
- package/scripts/lib/mcp-manifest.mjs +0 -40
- package/scripts/lib/process-utils.mjs +19 -0
- package/scripts/lib/stable-node.mjs +35 -0
- package/scripts/lib/test-env.mjs +20 -0
- package/scripts/lib/tmr-install.mjs +183 -0
- package/hooks/agy-session-hook.mjs +0 -155
- package/hooks/claude-cwd-projection-refresh.mjs +0 -11
- package/hooks/hook-orchestrator.mjs +0 -11
- package/hooks/lib/resolve-root.mjs +0 -62
- package/hub/codex-adapter.mjs +0 -193
- package/hub/codex-preflight.mjs +0 -219
- package/hub/lib/cto-env.mjs +0 -70
- package/scripts/lib/remote-spawn-transfer.mjs +0 -226
- package/scripts/lib/session-presence.mjs +0 -151
package/hub/team/nativeProxy.mjs
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
existsSync,
|
|
7
7
|
mkdirSync,
|
|
8
8
|
renameSync,
|
|
9
|
-
rmSync,
|
|
10
9
|
unlinkSync,
|
|
11
10
|
writeFileSync,
|
|
12
11
|
} from "node:fs";
|
|
@@ -23,7 +22,8 @@ import { isPidAlive } from "../lib/process-utils.mjs";
|
|
|
23
22
|
import { IS_WINDOWS } from "../platform.mjs";
|
|
24
23
|
|
|
25
24
|
const TEAM_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
26
|
-
|
|
25
|
+
// Claude Code 와 같은 규칙으로 CLAUDE_CONFIG_DIR 를 따른다. 테스트도 이걸로 실제 ~/.claude 를 피한다.
|
|
26
|
+
const CLAUDE_HOME = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
27
27
|
const TEAMS_ROOT = join(CLAUDE_HOME, "teams");
|
|
28
28
|
const TASKS_ROOT = join(CLAUDE_HOME, "tasks");
|
|
29
29
|
const LOCK_STALE_MS = 30000;
|
|
@@ -232,52 +232,6 @@ export async function resolveTeamPaths(teamName) {
|
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
export async function forceCleanupTeam(teamName) {
|
|
236
|
-
validateTeamName(teamName);
|
|
237
|
-
|
|
238
|
-
let paths;
|
|
239
|
-
try {
|
|
240
|
-
paths = await resolveTeamPaths(teamName);
|
|
241
|
-
} catch {
|
|
242
|
-
paths = {
|
|
243
|
-
team_dir: join(TEAMS_ROOT, teamName),
|
|
244
|
-
config_path: join(TEAMS_ROOT, teamName, "config.json"),
|
|
245
|
-
tasks_dir: join(TASKS_ROOT, teamName),
|
|
246
|
-
lead_session_id: null,
|
|
247
|
-
config: null,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
const config = paths.config || (await readJsonSafe(paths.config_path));
|
|
253
|
-
if (config && Array.isArray(config.members)) {
|
|
254
|
-
atomicWriteJson(paths.config_path, {
|
|
255
|
-
...config,
|
|
256
|
-
members: config.members.map((member) => ({
|
|
257
|
-
...member,
|
|
258
|
-
isActive: false,
|
|
259
|
-
})),
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
} catch {}
|
|
263
|
-
|
|
264
|
-
const cleanupTargets = new Set([
|
|
265
|
-
paths.team_dir,
|
|
266
|
-
join(TASKS_ROOT, teamName),
|
|
267
|
-
paths.tasks_dir,
|
|
268
|
-
]);
|
|
269
|
-
if (paths.lead_session_id) {
|
|
270
|
-
cleanupTargets.add(join(TASKS_ROOT, paths.lead_session_id));
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
for (const targetPath of cleanupTargets) {
|
|
274
|
-
if (!targetPath) continue;
|
|
275
|
-
try {
|
|
276
|
-
rmSync(targetPath, { recursive: true, force: true });
|
|
277
|
-
} catch {}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
235
|
async function collectTaskFiles(tasksDir) {
|
|
282
236
|
if (!existsSync(tasksDir)) return [];
|
|
283
237
|
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// hub/team/pane.mjs — pane별 CLI 실행 + stdin 주입
|
|
2
|
+
// 의존성: child_process, fs, os, path (Node.js 내장)만 사용
|
|
3
|
+
import { unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { privateTmpDir } from "../lib/private-tmp.mjs";
|
|
7
|
+
import { psmuxExec } from "./psmux.mjs";
|
|
8
|
+
import { detectMultiplexer, tmuxExec } from "./session.mjs";
|
|
9
|
+
|
|
10
|
+
function getPsmuxSessionName(target) {
|
|
11
|
+
return String(target).split(":")[0]?.trim() || "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Windows 경로를 멀티플렉서용 경로로 변환 */
|
|
15
|
+
function toMuxPath(p) {
|
|
16
|
+
if (process.platform !== "win32") return p;
|
|
17
|
+
|
|
18
|
+
const mux = detectMultiplexer();
|
|
19
|
+
|
|
20
|
+
// psmux는 Windows 네이티브 경로 그대로 사용
|
|
21
|
+
if (mux === "psmux") return p;
|
|
22
|
+
|
|
23
|
+
const normalized = p.replace(/\\/g, "/");
|
|
24
|
+
const m = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
|
25
|
+
if (!m) return normalized;
|
|
26
|
+
|
|
27
|
+
const drive = m[1].toLowerCase();
|
|
28
|
+
const rest = m[2];
|
|
29
|
+
|
|
30
|
+
// Git Bash/MSYS tmux는 /c/... 경로를 사용
|
|
31
|
+
return `/${drive}/${rest}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 멀티플렉서 커맨드 실행 (session.mjs와 동일 패턴) */
|
|
35
|
+
function muxExec(args, opts = {}) {
|
|
36
|
+
const exec = detectMultiplexer() === "psmux" ? psmuxExec : tmuxExec;
|
|
37
|
+
return exec(args, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
timeout: 10000,
|
|
40
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
41
|
+
windowsHide: true,
|
|
42
|
+
...opts,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const AGENT_TO_CLI = createRequire(import.meta.url)("./agent-map.json");
|
|
47
|
+
|
|
48
|
+
// 역할명(designer)과 별칭(agy)을 CLI 종류로 푼다. 실행과 주입 판정이 같은 값을 본다.
|
|
49
|
+
export function resolveCli(cli) {
|
|
50
|
+
const name = String(cli || "").toLowerCase();
|
|
51
|
+
return AGENT_TO_CLI[name] ?? name;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* CLI 에이전트 시작 커맨드 생성. 역할명과 별칭은 agent-map 으로 CLI 를 고른다.
|
|
56
|
+
* @param {string} cli: CLI 이름, 별칭(agy) 또는 역할명
|
|
57
|
+
* @returns {string} 실행할 셸 커맨드
|
|
58
|
+
*/
|
|
59
|
+
export function buildCliCommand(cli) {
|
|
60
|
+
switch (resolveCli(cli)) {
|
|
61
|
+
case "codex":
|
|
62
|
+
return "codex --dangerously-bypass-approvals-and-sandbox";
|
|
63
|
+
case "antigravity":
|
|
64
|
+
// 실행 파일은 agy 다. antigravity 는 셸 별칭이라 비대화형 셸에 없다.
|
|
65
|
+
return "agy";
|
|
66
|
+
case "claude":
|
|
67
|
+
// interactive 모드
|
|
68
|
+
return "claude";
|
|
69
|
+
default:
|
|
70
|
+
return cli; // 커스텀 CLI 허용
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* pane에 CLI 시작
|
|
76
|
+
* @param {string} target — 예: tfx-multi-abc:0.1
|
|
77
|
+
* @param {string} command — 실행할 커맨드
|
|
78
|
+
*/
|
|
79
|
+
export function startCliInPane(target, command) {
|
|
80
|
+
// CLI 시작도 buffer paste를 재사용해 셸/플랫폼별 quoting 차이를 제거한다.
|
|
81
|
+
injectPrompt(target, command);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* psmux `@file` 참조 주입이 가능한 CLI인지 판정한다.
|
|
86
|
+
*
|
|
87
|
+
* Codex TUI의 @path는 파일 검색 팝업을 열어 Enter 제출을 가로막는다.
|
|
88
|
+
* Gemini CLI의 파일 내용 주입은 유지한다.
|
|
89
|
+
*
|
|
90
|
+
* @param {{ multiplexer: string, useFileRef: boolean, cli: string|null }} args
|
|
91
|
+
*/
|
|
92
|
+
export function shouldUseFileRef({ multiplexer, useFileRef, cli }) {
|
|
93
|
+
return multiplexer === "psmux" && useFileRef && resolveCli(cli) !== "codex";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 동기 sleep — injectPrompt는 sync 경로라 setTimeout을 쓸 수 없다 */
|
|
97
|
+
function sleepMs(ms) {
|
|
98
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// paste 직후 Enter가 TUI paste-burst 감지에 개행으로 흡수되는 것을 피하는 정착 지연
|
|
102
|
+
const PASTE_SETTLE_MS = 350;
|
|
103
|
+
// 제출 확인 재시도 상한
|
|
104
|
+
const SUBMIT_RETRY_LIMIT = 3;
|
|
105
|
+
// TUI 준비 대기 상한 — 배너/composer 렌더 완료 전 주입은 키 입력이 통째로 유실될 수 있음
|
|
106
|
+
const COMPOSER_READY_TIMEOUT_MS = 8000;
|
|
107
|
+
|
|
108
|
+
function capturePaneText(target) {
|
|
109
|
+
try {
|
|
110
|
+
if (detectMultiplexer() === "psmux") {
|
|
111
|
+
return String(
|
|
112
|
+
psmuxExec(["capture-pane", "-t", target, "-p"], { encoding: "utf8" }) ??
|
|
113
|
+
"",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return String(muxExec(["capture-pane", "-t", target, "-p"]) ?? "");
|
|
117
|
+
} catch {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* TUI가 그리기를 마칠 때까지 대기: pane 내용이 비어 있지 않고 연속 두 번의
|
|
124
|
+
* capture가 동일(정적)해질 때까지 폴링한다. CLI 종류와 무관한 준비 신호이며
|
|
125
|
+
* 시간 초과 시 그냥 진행한다 (기존 고정-대기 동작으로 degrade).
|
|
126
|
+
*/
|
|
127
|
+
function waitForComposerReady(target) {
|
|
128
|
+
const deadline = Date.now() + COMPOSER_READY_TIMEOUT_MS;
|
|
129
|
+
let prev = null;
|
|
130
|
+
while (Date.now() < deadline) {
|
|
131
|
+
const text = capturePaneText(target);
|
|
132
|
+
if (text.trim() && prev !== null && text === prev) return;
|
|
133
|
+
prev = text;
|
|
134
|
+
sleepMs(400);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 주입한 프롬프트가 실제 제출됐는지 확인하고, 안 됐으면 Enter를 재전송한다.
|
|
140
|
+
*
|
|
141
|
+
* TUI는 연속 키 입력을 paste burst로 묶으므로 paste 직후의 Enter가 제출 대신
|
|
142
|
+
* composer 개행으로 흡수될 수 있다. transcript의 과거 prompt가 아니라 마지막
|
|
143
|
+
* composer line에서 현재 prompt가 사라졌을 때만 제출로 판정한다. Antigravity는
|
|
144
|
+
* capture 불가 또는 재시도 소진을 성공으로 숨기지 않고 오류로 반환한다.
|
|
145
|
+
*/
|
|
146
|
+
function isAntigravityCli(cli) {
|
|
147
|
+
return resolveCli(cli) === "antigravity";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function promptComposerNeedle(prompt) {
|
|
151
|
+
const firstContentLine = String(prompt || "")
|
|
152
|
+
.split(/\r?\n/u)
|
|
153
|
+
.map((line) => line.trim())
|
|
154
|
+
.find(Boolean);
|
|
155
|
+
return (firstContentLine || "").slice(0, 16);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isComposerPromptLine(line) {
|
|
159
|
+
return /^\s*(?:[│┃]\s*)?[›❯▶▸>]\s*/u.test(String(line || ""));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function composerPromptState(text, needle) {
|
|
163
|
+
const composerLines = String(text || "")
|
|
164
|
+
.split("\n")
|
|
165
|
+
.filter(isComposerPromptLine);
|
|
166
|
+
if (composerLines.length === 0) return "unknown";
|
|
167
|
+
return composerLines.at(-1).includes(needle) ? "present" : "absent";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasBusyMarkerOutsideComposer(text) {
|
|
171
|
+
return String(text || "")
|
|
172
|
+
.split("\n")
|
|
173
|
+
.some(
|
|
174
|
+
(line) =>
|
|
175
|
+
!isComposerPromptLine(line) &&
|
|
176
|
+
/esc to (?:interrupt|cancel)/iu.test(line),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* paste된 prompt가 실제 제출될 때까지 Enter를 bounded retry한다.
|
|
182
|
+
* Antigravity는 capture로 성공을 확인하지 못하면 조용히 idle 상태로 남기지 않고
|
|
183
|
+
* caller에 명확한 오류를 반환한다.
|
|
184
|
+
*/
|
|
185
|
+
export function submitPromptWithVerification({
|
|
186
|
+
cli,
|
|
187
|
+
prompt,
|
|
188
|
+
capture,
|
|
189
|
+
sendEnter,
|
|
190
|
+
sleep = sleepMs,
|
|
191
|
+
settleMs = PASTE_SETTLE_MS,
|
|
192
|
+
retryLimit = SUBMIT_RETRY_LIMIT,
|
|
193
|
+
} = {}) {
|
|
194
|
+
if (typeof capture !== "function" || typeof sendEnter !== "function") {
|
|
195
|
+
throw new TypeError("capture and sendEnter are required");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const strictVerification = isAntigravityCli(cli);
|
|
199
|
+
const needle = promptComposerNeedle(prompt);
|
|
200
|
+
sleep(settleMs);
|
|
201
|
+
|
|
202
|
+
if (!needle) {
|
|
203
|
+
sendEnter();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (let attempt = 1; attempt <= retryLimit; attempt++) {
|
|
208
|
+
sendEnter();
|
|
209
|
+
sleep(400 * attempt);
|
|
210
|
+
const text = String(capture() ?? "");
|
|
211
|
+
if (!text) {
|
|
212
|
+
if (strictVerification) continue;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (hasBusyMarkerOutsideComposer(text)) return;
|
|
216
|
+
const composerState = composerPromptState(text, needle);
|
|
217
|
+
if (composerState === "absent") return;
|
|
218
|
+
if (composerState === "unknown" && !strictVerification) return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (strictVerification) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Antigravity prompt submission failed after ${retryLimit} attempts: prompt remains in composer or submission could not be verified`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function confirmSubmit(target, prompt, cli, sendEnter) {
|
|
229
|
+
submitPromptWithVerification({
|
|
230
|
+
cli,
|
|
231
|
+
prompt,
|
|
232
|
+
capture: () => capturePaneText(target),
|
|
233
|
+
sendEnter,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* pane에 프롬프트 주입
|
|
239
|
+
* @param {string} target — 예: tfx-multi-abc:0.1
|
|
240
|
+
* @param {string} prompt — 주입할 텍스트
|
|
241
|
+
* @param {object} [opts]
|
|
242
|
+
* @param {boolean} [opts.useFileRef] — true면 TUI용 @file 참조 방식 요청 (psmux 전용). Codex에서는 자동으로 paste-buffer 경로로 fallback.
|
|
243
|
+
* @param {'codex'|'antigravity'|'claude'|null} [opts.cli]: 대상 CLI. Codex일 때 @ intercept를 회피하기 위해 paste-buffer 경로를 강제한다.
|
|
244
|
+
*/
|
|
245
|
+
export function injectPrompt(
|
|
246
|
+
target,
|
|
247
|
+
prompt,
|
|
248
|
+
{ useFileRef = false, cli = null } = {},
|
|
249
|
+
) {
|
|
250
|
+
const tmpDir = privateTmpDir("tfx-multi");
|
|
251
|
+
|
|
252
|
+
const safeTarget = target.replace(/[:.]/g, "-");
|
|
253
|
+
const tmpFile = join(tmpDir, `prompt-${safeTarget}-${Date.now()}.txt`);
|
|
254
|
+
|
|
255
|
+
const multiplexer = detectMultiplexer();
|
|
256
|
+
|
|
257
|
+
// psmux + TUI + CLI별 @file 지원: @path를 literal paste하고 Enter로 확정.
|
|
258
|
+
// Codex는 @가 file search popup을 intercept하므로 paste-buffer 경로로 fallback.
|
|
259
|
+
if (shouldUseFileRef({ multiplexer, useFileRef, cli })) {
|
|
260
|
+
writeFileSync(tmpFile, prompt, "utf8");
|
|
261
|
+
const filePath = tmpFile.replace(/\\/g, "/");
|
|
262
|
+
let cleanupScheduled = false;
|
|
263
|
+
try {
|
|
264
|
+
waitForComposerReady(target);
|
|
265
|
+
psmuxExec(["select-pane", "-t", target]);
|
|
266
|
+
psmuxExec(["send-keys", "-t", target, "-l", `@${filePath}`]);
|
|
267
|
+
confirmSubmit(target, `@${filePath}`, cli, () =>
|
|
268
|
+
psmuxExec(["send-keys", "-t", target, "Enter"]),
|
|
269
|
+
);
|
|
270
|
+
// TUI가 파일을 읽을 시간을 주고 정리
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
try {
|
|
273
|
+
unlinkSync(tmpFile);
|
|
274
|
+
} catch {}
|
|
275
|
+
}, 10000);
|
|
276
|
+
cleanupScheduled = true;
|
|
277
|
+
return;
|
|
278
|
+
} finally {
|
|
279
|
+
if (!cleanupScheduled) {
|
|
280
|
+
try {
|
|
281
|
+
unlinkSync(tmpFile);
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
writeFileSync(tmpFile, prompt, "utf8");
|
|
289
|
+
|
|
290
|
+
if (detectMultiplexer() === "psmux") {
|
|
291
|
+
const sessionName = getPsmuxSessionName(target);
|
|
292
|
+
waitForComposerReady(target);
|
|
293
|
+
psmuxExec(["load-buffer", "-t", sessionName, toMuxPath(tmpFile)]);
|
|
294
|
+
psmuxExec(["select-pane", "-t", target]);
|
|
295
|
+
psmuxExec(["paste-buffer", "-t", target]);
|
|
296
|
+
confirmSubmit(target, prompt, cli, () =>
|
|
297
|
+
psmuxExec(["send-keys", "-t", target, "Enter"]),
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// tmux load-buffer → paste-buffer → (정착 지연 + 제출 확인) Enter
|
|
303
|
+
waitForComposerReady(target);
|
|
304
|
+
muxExec(["load-buffer", toMuxPath(tmpFile)]);
|
|
305
|
+
// -p 가 없으면 줄바꿈이 CR(Enter)로 들어가 여러 줄 지시문이 줄마다 제출된다.
|
|
306
|
+
muxExec(["paste-buffer", "-p", "-t", target]);
|
|
307
|
+
confirmSubmit(target, prompt, cli, () =>
|
|
308
|
+
muxExec(["send-keys", "-t", target, "Enter"]),
|
|
309
|
+
);
|
|
310
|
+
} finally {
|
|
311
|
+
try {
|
|
312
|
+
unlinkSync(tmpFile);
|
|
313
|
+
} catch {}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* pane에 키 입력 전송
|
|
319
|
+
* @param {string} target — 예: tfx-multi-abc:0.1
|
|
320
|
+
* @param {string} keys — tmux 키 표현 (예: 'C-c', 'Enter')
|
|
321
|
+
*/
|
|
322
|
+
export function sendKeys(target, keys) {
|
|
323
|
+
muxExec([
|
|
324
|
+
"send-keys",
|
|
325
|
+
"-t",
|
|
326
|
+
target,
|
|
327
|
+
...String(keys).split(/\s+/u).filter(Boolean),
|
|
328
|
+
]);
|
|
329
|
+
}
|