triflux 3.2.0-dev.9 → 3.3.0-dev.3
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 +1516 -1386
- package/hooks/hooks.json +22 -0
- package/hooks/keyword-rules.json +4 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +120 -13
- package/hub/pipe.mjs +23 -0
- package/hub/pipeline/index.mjs +121 -0
- package/hub/pipeline/state.mjs +164 -0
- package/hub/pipeline/transitions.mjs +114 -0
- package/hub/router.mjs +322 -1
- package/hub/schema.sql +49 -2
- package/hub/server.mjs +173 -8
- package/hub/store.mjs +259 -1
- package/hub/team/cli-team-control.mjs +381 -381
- package/hub/team/cli-team-start.mjs +474 -470
- package/hub/team/cli-team-status.mjs +238 -238
- package/hub/team/cli.mjs +86 -86
- package/hub/team/native.mjs +144 -6
- package/hub/team/nativeProxy.mjs +51 -38
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +101 -90
- package/hub/team/psmux.mjs +721 -72
- package/hub/team/session.mjs +450 -450
- package/hub/tools.mjs +223 -63
- package/hub/workers/delegator-mcp.mjs +900 -0
- package/hub/workers/factory.mjs +3 -0
- package/hub/workers/interface.mjs +2 -2
- package/hud/hud-qos-status.mjs +89 -144
- package/package.json +1 -1
- package/scripts/__tests__/keyword-detector.test.mjs +11 -11
- package/scripts/__tests__/smoke.test.mjs +34 -0
- package/scripts/hub-ensure.mjs +21 -3
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/setup.mjs +23 -11
- package/scripts/tfx-route.sh +74 -15
- package/skills/{tfx-team → tfx-multi}/SKILL.md +115 -32
package/hub/team/psmux.mjs
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
// hub/team/psmux.mjs — Windows psmux
|
|
2
|
-
// 의존성: child_process (Node.js 내장)만 사용
|
|
3
|
-
import
|
|
1
|
+
// hub/team/psmux.mjs — Windows psmux 세션/키바인딩/캡처/steering 관리
|
|
2
|
+
// 의존성: child_process, fs, os, path (Node.js 내장)만 사용
|
|
3
|
+
import childProcess from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
4
7
|
|
|
5
8
|
const PSMUX_BIN = process.env.PSMUX_BIN || "psmux";
|
|
9
|
+
const GIT_BASH = process.env.GIT_BASH_PATH || "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
10
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
11
|
+
const PSMUX_TIMEOUT_MS = 10000;
|
|
12
|
+
const COMPLETION_PREFIX = "__TRIFLUX_DONE__:";
|
|
13
|
+
const CAPTURE_ROOT = process.env.PSMUX_CAPTURE_ROOT || join(tmpdir(), "psmux-steering");
|
|
14
|
+
const CAPTURE_HELPER_PATH = join(CAPTURE_ROOT, "pipe-pane-capture.ps1");
|
|
15
|
+
const POLL_INTERVAL_MS = (() => {
|
|
16
|
+
const ms = Number.parseInt(process.env.PSMUX_POLL_INTERVAL_MS || "", 10);
|
|
17
|
+
if (Number.isFinite(ms) && ms > 0) return ms;
|
|
18
|
+
const sec = Number.parseFloat(process.env.PSMUX_POLL_INTERVAL_SEC || "1");
|
|
19
|
+
return Number.isFinite(sec) && sec > 0 ? Math.max(100, Math.trunc(sec * 1000)) : 1000;
|
|
20
|
+
})();
|
|
6
21
|
|
|
7
22
|
function quoteArg(value) {
|
|
8
23
|
const str = String(value);
|
|
@@ -10,10 +25,143 @@ function quoteArg(value) {
|
|
|
10
25
|
return `"${str.replace(/"/g, '\\"')}"`;
|
|
11
26
|
}
|
|
12
27
|
|
|
28
|
+
function sanitizePathPart(value) {
|
|
29
|
+
return String(value).replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "_");
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
function toPaneTitle(index) {
|
|
14
33
|
return index === 0 ? "lead" : `worker-${index}`;
|
|
15
34
|
}
|
|
16
35
|
|
|
36
|
+
function sleepMs(ms) {
|
|
37
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tokenizeCommand(command) {
|
|
41
|
+
const source = String(command || "").trim();
|
|
42
|
+
if (!source) return [];
|
|
43
|
+
|
|
44
|
+
const tokens = [];
|
|
45
|
+
let current = "";
|
|
46
|
+
let quote = null;
|
|
47
|
+
|
|
48
|
+
const pushCurrent = () => {
|
|
49
|
+
if (current.length > 0) {
|
|
50
|
+
tokens.push(current);
|
|
51
|
+
current = "";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
56
|
+
const char = source[index];
|
|
57
|
+
const next = source[index + 1];
|
|
58
|
+
|
|
59
|
+
if (quote === "'") {
|
|
60
|
+
if (char === "'") {
|
|
61
|
+
quote = null;
|
|
62
|
+
} else {
|
|
63
|
+
current += char;
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (quote === '"') {
|
|
69
|
+
if (char === '"') {
|
|
70
|
+
quote = null;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (char === "\\" && (next === '"' || next === "\\")) {
|
|
74
|
+
current += next;
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
current += char;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (char === "'" || char === '"') {
|
|
83
|
+
quote = char;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (char === "\\" && next && (/[\s"'\\;]/u.test(next))) {
|
|
88
|
+
current += next;
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (/\s/u.test(char)) {
|
|
94
|
+
pushCurrent();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
current += char;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (quote) {
|
|
102
|
+
throw new Error(`psmux 인자 파싱 실패: 닫히지 않은 인용부호 (${command})`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pushCurrent();
|
|
106
|
+
return tokens;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizePsmuxArgs(args) {
|
|
110
|
+
if (Array.isArray(args)) {
|
|
111
|
+
return args.map((arg) => String(arg));
|
|
112
|
+
}
|
|
113
|
+
return tokenizeCommand(args);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function randomToken(prefix) {
|
|
117
|
+
const base = sanitizePathPart(prefix).replace(/_+/g, "-") || "pane";
|
|
118
|
+
const entropy = Math.random().toString(36).slice(2, 10);
|
|
119
|
+
return `${base}-${Date.now()}-${entropy}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ensurePsmuxInstalled() {
|
|
123
|
+
if (!hasPsmux()) {
|
|
124
|
+
throw new Error("psmux가 설치되어 있지 않습니다.");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getCaptureSessionDir(sessionName) {
|
|
129
|
+
return join(CAPTURE_ROOT, sanitizePathPart(sessionName));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getCaptureLogPath(sessionName, paneName) {
|
|
133
|
+
return join(getCaptureSessionDir(sessionName), `${sanitizePathPart(paneName)}.log`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ensureCaptureHelper() {
|
|
137
|
+
mkdirSync(CAPTURE_ROOT, { recursive: true });
|
|
138
|
+
writeFileSync(
|
|
139
|
+
CAPTURE_HELPER_PATH,
|
|
140
|
+
[
|
|
141
|
+
"param(",
|
|
142
|
+
" [Parameter(Mandatory = $true)][string]$Path",
|
|
143
|
+
")",
|
|
144
|
+
"",
|
|
145
|
+
"$parent = Split-Path -Parent $Path",
|
|
146
|
+
"if ($parent) {",
|
|
147
|
+
" New-Item -ItemType Directory -Force -Path $parent | Out-Null",
|
|
148
|
+
"}",
|
|
149
|
+
"",
|
|
150
|
+
"$reader = [Console]::In",
|
|
151
|
+
"while (($line = $reader.ReadLine()) -ne $null) {",
|
|
152
|
+
" Add-Content -LiteralPath $Path -Value $line -Encoding utf8",
|
|
153
|
+
"}",
|
|
154
|
+
"",
|
|
155
|
+
].join("\n"),
|
|
156
|
+
"utf8",
|
|
157
|
+
);
|
|
158
|
+
return CAPTURE_HELPER_PATH;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readCaptureLog(logPath) {
|
|
162
|
+
return existsSync(logPath) ? readFileSync(logPath, "utf8") : "";
|
|
163
|
+
}
|
|
164
|
+
|
|
17
165
|
function parsePaneList(output) {
|
|
18
166
|
return output
|
|
19
167
|
.split("\n")
|
|
@@ -58,44 +206,123 @@ function parseSessionSummaries(output) {
|
|
|
58
206
|
.filter(Boolean);
|
|
59
207
|
}
|
|
60
208
|
|
|
209
|
+
function parsePaneDetails(output) {
|
|
210
|
+
return output
|
|
211
|
+
.split("\n")
|
|
212
|
+
.map((line) => line.trim())
|
|
213
|
+
.filter(Boolean)
|
|
214
|
+
.map((line) => {
|
|
215
|
+
const [title = "", paneId = "", dead = "0", deadStatus = ""] = line.split("\t");
|
|
216
|
+
const exitCode = dead === "1"
|
|
217
|
+
? Number.parseInt(deadStatus, 10)
|
|
218
|
+
: null;
|
|
219
|
+
return {
|
|
220
|
+
title,
|
|
221
|
+
paneId,
|
|
222
|
+
isDead: dead === "1",
|
|
223
|
+
exitCode: Number.isFinite(exitCode) ? exitCode : dead === "1" ? 0 : null,
|
|
224
|
+
};
|
|
225
|
+
})
|
|
226
|
+
.filter((entry) => entry.paneId);
|
|
227
|
+
}
|
|
228
|
+
|
|
61
229
|
function collectSessionPanes(sessionName) {
|
|
62
|
-
const output = psmuxExec(
|
|
63
|
-
|
|
64
|
-
|
|
230
|
+
const output = psmuxExec([
|
|
231
|
+
"list-panes",
|
|
232
|
+
"-t",
|
|
233
|
+
`${sessionName}:0`,
|
|
234
|
+
"-F",
|
|
235
|
+
"#{pane_index}\t#{session_name}:#{window_index}.#{pane_index}",
|
|
236
|
+
]);
|
|
65
237
|
return parsePaneList(output);
|
|
66
238
|
}
|
|
67
239
|
|
|
240
|
+
function listPaneDetails(sessionName) {
|
|
241
|
+
const output = psmuxExec([
|
|
242
|
+
"list-panes",
|
|
243
|
+
"-t",
|
|
244
|
+
sessionName,
|
|
245
|
+
"-F",
|
|
246
|
+
"#{pane_title}\t#{session_name}:#{window_index}.#{pane_index}\t#{pane_dead}\t#{pane_dead_status}",
|
|
247
|
+
]);
|
|
248
|
+
return parsePaneDetails(output);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolvePane(sessionName, paneNameOrTarget) {
|
|
252
|
+
const wanted = String(paneNameOrTarget);
|
|
253
|
+
const pane = listPaneDetails(sessionName)
|
|
254
|
+
.find((entry) => entry.title === wanted || entry.paneId === wanted);
|
|
255
|
+
if (!pane) {
|
|
256
|
+
throw new Error(`Pane을 찾을 수 없습니다: ${paneNameOrTarget}`);
|
|
257
|
+
}
|
|
258
|
+
return pane;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function refreshCaptureSnapshot(sessionName, paneNameOrTarget) {
|
|
262
|
+
const pane = resolvePane(sessionName, paneNameOrTarget);
|
|
263
|
+
const paneName = pane.title || paneNameOrTarget;
|
|
264
|
+
const logPath = getCaptureLogPath(sessionName, paneName);
|
|
265
|
+
mkdirSync(getCaptureSessionDir(sessionName), { recursive: true });
|
|
266
|
+
const snapshot = psmuxExec(["capture-pane", "-t", pane.paneId, "-p"]);
|
|
267
|
+
writeFileSync(logPath, snapshot, "utf8");
|
|
268
|
+
return { paneId: pane.paneId, paneName, logPath, snapshot };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function disablePipeCapture(paneId) {
|
|
272
|
+
try {
|
|
273
|
+
psmuxExec(["pipe-pane", "-t", paneId]);
|
|
274
|
+
} catch {
|
|
275
|
+
// 기존 pipe가 없으면 무시
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function sendLiteralToPane(paneId, text, submit = true) {
|
|
280
|
+
psmuxExec(["send-keys", "-t", paneId, "-l", text]);
|
|
281
|
+
if (submit) {
|
|
282
|
+
psmuxExec(["send-keys", "-t", paneId, "Enter"]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function toPatternRegExp(pattern) {
|
|
287
|
+
if (pattern instanceof RegExp) {
|
|
288
|
+
const flags = pattern.flags.includes("m") ? pattern.flags : `${pattern.flags}m`;
|
|
289
|
+
return new RegExp(pattern.source, flags);
|
|
290
|
+
}
|
|
291
|
+
return new RegExp(String(pattern), "m");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function escapeRegExp(value) {
|
|
295
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
296
|
+
}
|
|
297
|
+
|
|
68
298
|
function psmux(args, opts = {}) {
|
|
69
|
-
|
|
70
|
-
|
|
299
|
+
const normalizedArgs = normalizePsmuxArgs(args);
|
|
300
|
+
try {
|
|
301
|
+
const result = childProcess.execFileSync(PSMUX_BIN, normalizedArgs, {
|
|
71
302
|
encoding: "utf8",
|
|
72
|
-
timeout:
|
|
303
|
+
timeout: PSMUX_TIMEOUT_MS,
|
|
73
304
|
stdio: ["pipe", "pipe", "pipe"],
|
|
74
305
|
windowsHide: true,
|
|
75
306
|
...opts,
|
|
76
307
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
308
|
+
return result != null ? String(result).trim() : "";
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const stderr = typeof error?.stderr === "string"
|
|
311
|
+
? error.stderr
|
|
312
|
+
: error?.stderr?.toString?.("utf8") || "";
|
|
313
|
+
const stdout = typeof error?.stdout === "string"
|
|
314
|
+
? error.stdout
|
|
315
|
+
: error?.stdout?.toString?.("utf8") || "";
|
|
316
|
+
const wrapped = new Error((stderr || stdout || error.message || "psmux command failed").trim());
|
|
317
|
+
wrapped.status = error.status;
|
|
318
|
+
throw wrapped;
|
|
83
319
|
}
|
|
84
|
-
|
|
85
|
-
const result = execSync(`${quoteArg(PSMUX_BIN)} ${args}`, {
|
|
86
|
-
encoding: "utf8",
|
|
87
|
-
timeout: 10000,
|
|
88
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
89
|
-
windowsHide: true,
|
|
90
|
-
...opts,
|
|
91
|
-
});
|
|
92
|
-
return result != null ? result.trim() : "";
|
|
93
320
|
}
|
|
94
321
|
|
|
95
322
|
/** psmux 실행 가능 여부 확인 */
|
|
96
323
|
export function hasPsmux() {
|
|
97
324
|
try {
|
|
98
|
-
|
|
325
|
+
childProcess.execFileSync(PSMUX_BIN, ["-V"], {
|
|
99
326
|
stdio: "ignore",
|
|
100
327
|
timeout: 3000,
|
|
101
328
|
windowsHide: true,
|
|
@@ -128,45 +355,73 @@ export function createPsmuxSession(sessionName, opts = {}) {
|
|
|
128
355
|
const layout = opts.layout === "1xN" || opts.layout === "Nx1" ? opts.layout : "2x2";
|
|
129
356
|
const paneCount = Math.max(
|
|
130
357
|
1,
|
|
131
|
-
Number.isFinite(opts.paneCount) ? Math.trunc(opts.paneCount) : 4
|
|
358
|
+
Number.isFinite(opts.paneCount) ? Math.trunc(opts.paneCount) : 4,
|
|
132
359
|
);
|
|
133
360
|
const limitedPaneCount = layout === "2x2" ? Math.min(paneCount, 4) : paneCount;
|
|
134
361
|
const sessionTarget = `${sessionName}:0`;
|
|
135
362
|
|
|
136
|
-
const leadPane = psmuxExec(
|
|
137
|
-
|
|
138
|
-
|
|
363
|
+
const leadPane = psmuxExec([
|
|
364
|
+
"new-session",
|
|
365
|
+
"-d",
|
|
366
|
+
"-P",
|
|
367
|
+
"-F",
|
|
368
|
+
"#{session_name}:#{window_index}.#{pane_index}",
|
|
369
|
+
"-s",
|
|
370
|
+
sessionName,
|
|
371
|
+
"-x",
|
|
372
|
+
"220",
|
|
373
|
+
"-y",
|
|
374
|
+
"55",
|
|
375
|
+
]);
|
|
139
376
|
|
|
140
377
|
if (layout === "2x2" && limitedPaneCount >= 3) {
|
|
141
|
-
const rightPane = psmuxExec(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
378
|
+
const rightPane = psmuxExec([
|
|
379
|
+
"split-window",
|
|
380
|
+
"-h",
|
|
381
|
+
"-P",
|
|
382
|
+
"-F",
|
|
383
|
+
"#{session_name}:#{window_index}.#{pane_index}",
|
|
384
|
+
"-t",
|
|
385
|
+
leadPane,
|
|
386
|
+
]);
|
|
387
|
+
psmuxExec([
|
|
388
|
+
"split-window",
|
|
389
|
+
"-v",
|
|
390
|
+
"-P",
|
|
391
|
+
"-F",
|
|
392
|
+
"#{session_name}:#{window_index}.#{pane_index}",
|
|
393
|
+
"-t",
|
|
394
|
+
rightPane,
|
|
395
|
+
]);
|
|
147
396
|
if (limitedPaneCount >= 4) {
|
|
148
|
-
psmuxExec(
|
|
149
|
-
|
|
150
|
-
|
|
397
|
+
psmuxExec([
|
|
398
|
+
"split-window",
|
|
399
|
+
"-v",
|
|
400
|
+
"-P",
|
|
401
|
+
"-F",
|
|
402
|
+
"#{session_name}:#{window_index}.#{pane_index}",
|
|
403
|
+
"-t",
|
|
404
|
+
leadPane,
|
|
405
|
+
]);
|
|
151
406
|
}
|
|
152
|
-
psmuxExec(
|
|
407
|
+
psmuxExec(["select-layout", "-t", sessionTarget, "tiled"]);
|
|
153
408
|
} else if (layout === "1xN") {
|
|
154
|
-
for (let
|
|
155
|
-
psmuxExec(
|
|
409
|
+
for (let index = 1; index < limitedPaneCount; index += 1) {
|
|
410
|
+
psmuxExec(["split-window", "-h", "-t", sessionTarget]);
|
|
156
411
|
}
|
|
157
|
-
psmuxExec(
|
|
412
|
+
psmuxExec(["select-layout", "-t", sessionTarget, "even-horizontal"]);
|
|
158
413
|
} else {
|
|
159
|
-
for (let
|
|
160
|
-
psmuxExec(
|
|
414
|
+
for (let index = 1; index < limitedPaneCount; index += 1) {
|
|
415
|
+
psmuxExec(["split-window", "-v", "-t", sessionTarget]);
|
|
161
416
|
}
|
|
162
|
-
psmuxExec(
|
|
417
|
+
psmuxExec(["select-layout", "-t", sessionTarget, "even-vertical"]);
|
|
163
418
|
}
|
|
164
419
|
|
|
165
|
-
psmuxExec(
|
|
420
|
+
psmuxExec(["select-pane", "-t", leadPane]);
|
|
166
421
|
|
|
167
422
|
const panes = collectSessionPanes(sessionName).slice(0, limitedPaneCount);
|
|
168
423
|
panes.forEach((pane, index) => {
|
|
169
|
-
psmuxExec(
|
|
424
|
+
psmuxExec(["select-pane", "-t", pane, "-T", toPaneTitle(index)]);
|
|
170
425
|
});
|
|
171
426
|
|
|
172
427
|
return { sessionName, panes };
|
|
@@ -178,7 +433,7 @@ export function createPsmuxSession(sessionName, opts = {}) {
|
|
|
178
433
|
*/
|
|
179
434
|
export function killPsmuxSession(sessionName) {
|
|
180
435
|
try {
|
|
181
|
-
psmuxExec(
|
|
436
|
+
psmuxExec(["kill-session", "-t", sessionName], { stdio: "ignore" });
|
|
182
437
|
} catch {
|
|
183
438
|
// 이미 종료된 세션 — 무시
|
|
184
439
|
}
|
|
@@ -191,7 +446,7 @@ export function killPsmuxSession(sessionName) {
|
|
|
191
446
|
*/
|
|
192
447
|
export function psmuxSessionExists(sessionName) {
|
|
193
448
|
try {
|
|
194
|
-
psmuxExec(
|
|
449
|
+
psmuxExec(["has-session", "-t", sessionName], { stdio: "ignore" });
|
|
195
450
|
return true;
|
|
196
451
|
} catch {
|
|
197
452
|
return false;
|
|
@@ -199,14 +454,14 @@ export function psmuxSessionExists(sessionName) {
|
|
|
199
454
|
}
|
|
200
455
|
|
|
201
456
|
/**
|
|
202
|
-
* tfx-
|
|
457
|
+
* tfx-multi- 접두사 psmux 세션 목록
|
|
203
458
|
* @returns {string[]}
|
|
204
459
|
*/
|
|
205
460
|
export function listPsmuxSessions() {
|
|
206
461
|
try {
|
|
207
|
-
return parseSessionSummaries(psmuxExec("list-sessions"))
|
|
462
|
+
return parseSessionSummaries(psmuxExec(["list-sessions"]))
|
|
208
463
|
.map((session) => session.sessionName)
|
|
209
|
-
.filter((sessionName) => sessionName.startsWith("tfx-
|
|
464
|
+
.filter((sessionName) => sessionName.startsWith("tfx-multi-"));
|
|
210
465
|
} catch {
|
|
211
466
|
return [];
|
|
212
467
|
}
|
|
@@ -220,7 +475,7 @@ export function listPsmuxSessions() {
|
|
|
220
475
|
*/
|
|
221
476
|
export function capturePsmuxPane(target, lines = 5) {
|
|
222
477
|
try {
|
|
223
|
-
const full = psmuxExec(
|
|
478
|
+
const full = psmuxExec(["capture-pane", "-t", target, "-p"]);
|
|
224
479
|
const nonEmpty = full.split("\n").filter((line) => line.trim() !== "");
|
|
225
480
|
return nonEmpty.slice(-lines).join("\n");
|
|
226
481
|
} catch {
|
|
@@ -233,7 +488,7 @@ export function capturePsmuxPane(target, lines = 5) {
|
|
|
233
488
|
* @param {string} sessionName
|
|
234
489
|
*/
|
|
235
490
|
export function attachPsmuxSession(sessionName) {
|
|
236
|
-
const result = spawnSync(PSMUX_BIN, ["attach-session", "-t", sessionName], {
|
|
491
|
+
const result = childProcess.spawnSync(PSMUX_BIN, ["attach-session", "-t", sessionName], {
|
|
237
492
|
stdio: "inherit",
|
|
238
493
|
timeout: 0,
|
|
239
494
|
windowsHide: false,
|
|
@@ -250,7 +505,7 @@ export function attachPsmuxSession(sessionName) {
|
|
|
250
505
|
*/
|
|
251
506
|
export function getPsmuxSessionAttachedCount(sessionName) {
|
|
252
507
|
try {
|
|
253
|
-
const session = parseSessionSummaries(psmuxExec("list-sessions"))
|
|
508
|
+
const session = parseSessionSummaries(psmuxExec(["list-sessions"]))
|
|
254
509
|
.find((entry) => entry.sessionName === sessionName);
|
|
255
510
|
return session ? session.attachedCount : null;
|
|
256
511
|
} catch {
|
|
@@ -268,30 +523,424 @@ export function getPsmuxSessionAttachedCount(sessionName) {
|
|
|
268
523
|
export function configurePsmuxKeybindings(sessionName, opts = {}) {
|
|
269
524
|
const { inProcess = false, taskListCommand = "" } = opts;
|
|
270
525
|
const cond = `#{==:#{session_name},${sessionName}}`;
|
|
526
|
+
const target = `${sessionName}:0`;
|
|
271
527
|
const bindNext = inProcess
|
|
272
|
-
?
|
|
273
|
-
:
|
|
528
|
+
? "select-pane -t :.+ \\; resize-pane -Z"
|
|
529
|
+
: "select-pane -t :.+";
|
|
274
530
|
const bindPrev = inProcess
|
|
275
|
-
?
|
|
276
|
-
:
|
|
531
|
+
? "select-pane -t :.- \\; resize-pane -Z"
|
|
532
|
+
: "select-pane -t :.-";
|
|
277
533
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
534
|
+
// psmux는 세션별 서버이므로 -t target으로 세션 컨텍스트를 전달해야 한다.
|
|
535
|
+
const bindSafe = (args) => {
|
|
536
|
+
try {
|
|
537
|
+
psmuxExec(["-t", target, ...args]);
|
|
538
|
+
} catch {
|
|
539
|
+
// 미지원 시 무시
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
bindSafe(["bind-key", "-T", "root", "-n", "S-Down", "if-shell", "-F", cond, bindNext, "send-keys S-Down"]);
|
|
544
|
+
bindSafe(["bind-key", "-T", "root", "-n", "S-Up", "if-shell", "-F", cond, bindPrev, "send-keys S-Up"]);
|
|
545
|
+
bindSafe(["bind-key", "-T", "root", "-n", "S-Right", "if-shell", "-F", cond, bindNext, "send-keys S-Right"]);
|
|
546
|
+
bindSafe(["bind-key", "-T", "root", "-n", "S-Left", "if-shell", "-F", cond, bindPrev, "send-keys S-Left"]);
|
|
547
|
+
bindSafe(["bind-key", "-T", "root", "-n", "BTab", "if-shell", "-F", cond, bindPrev, "send-keys BTab"]);
|
|
548
|
+
bindSafe(["bind-key", "-T", "root", "-n", "Escape", "if-shell", "-F", cond, "send-keys C-c", "send-keys Escape"]);
|
|
284
549
|
|
|
285
550
|
if (taskListCommand) {
|
|
286
|
-
|
|
551
|
+
bindSafe([
|
|
552
|
+
"bind-key",
|
|
553
|
+
"-T",
|
|
554
|
+
"root",
|
|
555
|
+
"-n",
|
|
556
|
+
"C-t",
|
|
557
|
+
"if-shell",
|
|
558
|
+
"-F",
|
|
559
|
+
cond,
|
|
560
|
+
`display-popup -E ${quoteArg(taskListCommand)}`,
|
|
561
|
+
"send-keys C-t",
|
|
562
|
+
]);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ─── steering 기능 ───
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* pane 출력 pipe-pane 캡처를 시작하고 즉시 snapshot을 기록한다.
|
|
570
|
+
* @param {string} sessionName
|
|
571
|
+
* @param {string} paneNameOrTarget
|
|
572
|
+
* @returns {{ paneId: string, paneName: string, logPath: string }}
|
|
573
|
+
*/
|
|
574
|
+
export function startCapture(sessionName, paneNameOrTarget) {
|
|
575
|
+
ensurePsmuxInstalled();
|
|
576
|
+
const pane = resolvePane(sessionName, paneNameOrTarget);
|
|
577
|
+
const paneName = pane.title || paneNameOrTarget;
|
|
578
|
+
const logPath = getCaptureLogPath(sessionName, paneName);
|
|
579
|
+
const helperPath = ensureCaptureHelper();
|
|
580
|
+
mkdirSync(getCaptureSessionDir(sessionName), { recursive: true });
|
|
581
|
+
writeFileSync(logPath, "", "utf8");
|
|
582
|
+
|
|
583
|
+
disablePipeCapture(pane.paneId);
|
|
584
|
+
psmuxExec([
|
|
585
|
+
"pipe-pane",
|
|
586
|
+
"-t",
|
|
587
|
+
pane.paneId,
|
|
588
|
+
`powershell.exe -NoLogo -NoProfile -File ${quoteArg(helperPath)} ${quoteArg(logPath)}`,
|
|
589
|
+
]);
|
|
590
|
+
|
|
591
|
+
refreshCaptureSnapshot(sessionName, pane.paneId);
|
|
592
|
+
return { paneId: pane.paneId, paneName, logPath };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* PowerShell 명령을 pane에 비동기 전송하고 완료 토큰을 반환한다.
|
|
597
|
+
* @param {string} sessionName
|
|
598
|
+
* @param {string} paneNameOrTarget
|
|
599
|
+
* @param {string} commandText
|
|
600
|
+
* @returns {{ paneId: string, paneName: string, token: string, logPath: string }}
|
|
601
|
+
*/
|
|
602
|
+
export function dispatchCommand(sessionName, paneNameOrTarget, commandText) {
|
|
603
|
+
ensurePsmuxInstalled();
|
|
604
|
+
const pane = resolvePane(sessionName, paneNameOrTarget);
|
|
605
|
+
const paneName = pane.title || paneNameOrTarget;
|
|
606
|
+
const logPath = getCaptureLogPath(sessionName, paneName);
|
|
607
|
+
|
|
608
|
+
if (!existsSync(logPath)) {
|
|
609
|
+
startCapture(sessionName, paneName);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const token = randomToken(paneName);
|
|
613
|
+
const wrapped = `${commandText}; $trifluxExit = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }; Write-Output "${COMPLETION_PREFIX}${token}:$trifluxExit"`;
|
|
614
|
+
sendLiteralToPane(pane.paneId, wrapped, true);
|
|
615
|
+
|
|
616
|
+
return { paneId: pane.paneId, paneName, token, logPath };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* pane 캡처 로그에서 정규식 패턴을 polling으로 대기한다.
|
|
621
|
+
* @param {string} sessionName
|
|
622
|
+
* @param {string} paneNameOrTarget
|
|
623
|
+
* @param {string|RegExp} pattern
|
|
624
|
+
* @param {number} timeoutSec
|
|
625
|
+
* @returns {{ matched: boolean, paneId: string, paneName: string, logPath: string, match: string|null }}
|
|
626
|
+
*/
|
|
627
|
+
export function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSec = 300) {
|
|
628
|
+
ensurePsmuxInstalled();
|
|
629
|
+
const pane = resolvePane(sessionName, paneNameOrTarget);
|
|
630
|
+
const paneName = pane.title || paneNameOrTarget;
|
|
631
|
+
const logPath = getCaptureLogPath(sessionName, paneName);
|
|
632
|
+
if (!existsSync(logPath)) {
|
|
633
|
+
throw new Error(`캡처 로그가 없습니다. 먼저 startCapture(${sessionName}, ${paneName})를 호출하세요.`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const deadline = Date.now() + Math.max(0, Math.trunc(timeoutSec * 1000));
|
|
637
|
+
const regex = toPatternRegExp(pattern);
|
|
638
|
+
|
|
639
|
+
while (Date.now() <= deadline) {
|
|
640
|
+
refreshCaptureSnapshot(sessionName, pane.paneId);
|
|
641
|
+
const content = readCaptureLog(logPath);
|
|
642
|
+
const match = regex.exec(content);
|
|
643
|
+
if (match) {
|
|
644
|
+
return {
|
|
645
|
+
matched: true,
|
|
646
|
+
paneId: pane.paneId,
|
|
647
|
+
paneName,
|
|
648
|
+
logPath,
|
|
649
|
+
match: match[0],
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (Date.now() > deadline) {
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
sleepMs(POLL_INTERVAL_MS);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
matched: false,
|
|
661
|
+
paneId: pane.paneId,
|
|
662
|
+
paneName,
|
|
663
|
+
logPath,
|
|
664
|
+
match: null,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* 완료 토큰이 찍힐 때까지 대기하고 exit code를 파싱한다.
|
|
670
|
+
* @param {string} sessionName
|
|
671
|
+
* @param {string} paneNameOrTarget
|
|
672
|
+
* @param {string} token
|
|
673
|
+
* @param {number} timeoutSec
|
|
674
|
+
* @returns {{ matched: boolean, paneId: string, paneName: string, logPath: string, match: string|null, token: string, exitCode: number|null }}
|
|
675
|
+
*/
|
|
676
|
+
export function waitForCompletion(sessionName, paneNameOrTarget, token, timeoutSec = 300) {
|
|
677
|
+
const completionRegex = new RegExp(
|
|
678
|
+
`${escapeRegExp(COMPLETION_PREFIX)}${escapeRegExp(token)}:(\\d+)`,
|
|
679
|
+
"m",
|
|
680
|
+
);
|
|
681
|
+
const result = waitForPattern(sessionName, paneNameOrTarget, completionRegex, timeoutSec);
|
|
682
|
+
const exitMatch = result.match ? completionRegex.exec(result.match) : null;
|
|
683
|
+
return {
|
|
684
|
+
...result,
|
|
685
|
+
token,
|
|
686
|
+
exitCode: exitMatch ? Number.parseInt(exitMatch[1], 10) : null,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ─── 하이브리드 모드 워커 관리 함수 ───
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* psmux 세션의 새 pane에서 워커 실행
|
|
694
|
+
* @param {string} sessionName - 대상 psmux 세션 이름
|
|
695
|
+
* @param {string} workerName - 워커 식별용 pane 타이틀
|
|
696
|
+
* @param {string} cmd - 실행할 커맨드
|
|
697
|
+
* @returns {{ paneId: string, workerName: string }}
|
|
698
|
+
*/
|
|
699
|
+
export function spawnWorker(sessionName, workerName, cmd) {
|
|
700
|
+
if (!hasPsmux()) {
|
|
701
|
+
throw new Error("psmux가 설치되어 있지 않습니다. psmux를 먼저 설치하세요.");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// remain-on-exit: 종료된 pane이 즉시 사라지는 것 방지
|
|
705
|
+
try {
|
|
706
|
+
psmuxExec(["set-option", "-t", sessionName, "remain-on-exit", "on"]);
|
|
707
|
+
} catch {
|
|
708
|
+
// 미지원 시 무시
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Windows: pane 기본셸이 PowerShell → Git Bash로 래핑
|
|
712
|
+
// psmux가 이스케이프 시퀀스를 처리하므로 포워드 슬래시 경로를 사용한다.
|
|
713
|
+
const shellCmd = IS_WINDOWS
|
|
714
|
+
? `& '${GIT_BASH.replace(/\\/g, "/")}' -l -c '${cmd.replace(/'/g, "'\\''")}'`
|
|
715
|
+
: cmd;
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
const paneTarget = psmuxExec([
|
|
719
|
+
"split-window",
|
|
720
|
+
"-t",
|
|
721
|
+
sessionName,
|
|
722
|
+
"-P",
|
|
723
|
+
"-F",
|
|
724
|
+
"#{session_name}:#{window_index}.#{pane_index}",
|
|
725
|
+
shellCmd,
|
|
726
|
+
]);
|
|
727
|
+
psmuxExec(["select-pane", "-t", paneTarget, "-T", workerName]);
|
|
728
|
+
return { paneId: paneTarget, workerName };
|
|
729
|
+
} catch (err) {
|
|
730
|
+
throw new Error(`워커 생성 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* 워커 pane 실행 상태 확인
|
|
736
|
+
* @param {string} sessionName - 대상 psmux 세션 이름
|
|
737
|
+
* @param {string} workerName - 워커 pane 타이틀
|
|
738
|
+
* @returns {{ status: "running"|"exited", exitCode: number|null, paneId: string }}
|
|
739
|
+
*/
|
|
740
|
+
export function getWorkerStatus(sessionName, workerName) {
|
|
741
|
+
if (!hasPsmux()) {
|
|
742
|
+
throw new Error("psmux가 설치되어 있지 않습니다.");
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
const pane = resolvePane(sessionName, workerName);
|
|
746
|
+
return {
|
|
747
|
+
status: pane.isDead ? "exited" : "running",
|
|
748
|
+
exitCode: pane.isDead ? pane.exitCode : null,
|
|
749
|
+
paneId: pane.paneId,
|
|
750
|
+
};
|
|
751
|
+
} catch (err) {
|
|
752
|
+
if (err.message.includes("Pane을 찾을 수 없습니다")) {
|
|
753
|
+
throw new Error(`워커를 찾을 수 없습니다: ${workerName}`);
|
|
754
|
+
}
|
|
755
|
+
throw new Error(`워커 상태 조회 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* 워커 pane 프로세스 강제 종료
|
|
761
|
+
* @param {string} sessionName - 대상 psmux 세션 이름
|
|
762
|
+
* @param {string} workerName - 워커 pane 타이틀
|
|
763
|
+
* @returns {{ killed: boolean }}
|
|
764
|
+
*/
|
|
765
|
+
export function killWorker(sessionName, workerName) {
|
|
766
|
+
if (!hasPsmux()) {
|
|
767
|
+
throw new Error("psmux가 설치되어 있지 않습니다.");
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
const { paneId, status } = getWorkerStatus(sessionName, workerName);
|
|
771
|
+
|
|
772
|
+
// 이미 종료된 워커 → pane 정리만 수행
|
|
773
|
+
if (status === "exited") {
|
|
774
|
+
try {
|
|
775
|
+
psmuxExec(["kill-pane", "-t", paneId]);
|
|
776
|
+
} catch {
|
|
777
|
+
// 무시
|
|
778
|
+
}
|
|
779
|
+
return { killed: true };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// running → C-c 우아한 종료 시도
|
|
783
|
+
try {
|
|
784
|
+
psmuxExec(["send-keys", "-t", paneId, "C-c"]);
|
|
785
|
+
} catch {
|
|
786
|
+
// send-keys 실패 무시
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
sleepMs(1000);
|
|
790
|
+
|
|
287
791
|
try {
|
|
288
|
-
psmuxExec(
|
|
289
|
-
`bind-key -T root -n C-t if-shell -F '${cond}' "display-popup -E '${escaped}'" "send-keys C-t"`
|
|
290
|
-
);
|
|
792
|
+
psmuxExec(["kill-pane", "-t", paneId]);
|
|
291
793
|
} catch {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
794
|
+
// 이미 종료된 pane — 무시
|
|
795
|
+
}
|
|
796
|
+
return { killed: true };
|
|
797
|
+
} catch (err) {
|
|
798
|
+
if (err.message.includes("워커를 찾을 수 없습니다")) {
|
|
799
|
+
return { killed: true };
|
|
800
|
+
}
|
|
801
|
+
throw new Error(`워커 종료 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* 워커 pane 출력 마지막 N줄 캡처
|
|
807
|
+
* @param {string} sessionName - 대상 psmux 세션 이름
|
|
808
|
+
* @param {string} workerName - 워커 pane 타이틀
|
|
809
|
+
* @param {number} lines - 캡처할 줄 수 (기본 50)
|
|
810
|
+
* @returns {string} 캡처된 출력
|
|
811
|
+
*/
|
|
812
|
+
export function captureWorkerOutput(sessionName, workerName, lines = 50) {
|
|
813
|
+
if (!hasPsmux()) {
|
|
814
|
+
throw new Error("psmux가 설치되어 있지 않습니다.");
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
const { paneId } = getWorkerStatus(sessionName, workerName);
|
|
818
|
+
return psmuxExec(["capture-pane", "-t", paneId, "-p", "-S", `-${lines}`]);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
if (err.message.includes("워커를 찾을 수 없습니다")) throw err;
|
|
821
|
+
throw new Error(`출력 캡처 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// ─── CLI 진입점 ───
|
|
826
|
+
|
|
827
|
+
if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
828
|
+
const [, , cmd, ...args] = process.argv;
|
|
829
|
+
|
|
830
|
+
// CLI 인자 파싱 헬퍼
|
|
831
|
+
function getArg(name) {
|
|
832
|
+
const idx = args.indexOf(`--${name}`);
|
|
833
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
switch (cmd) {
|
|
838
|
+
case "spawn": {
|
|
839
|
+
const session = getArg("session");
|
|
840
|
+
const name = getArg("name");
|
|
841
|
+
const workerCmd = getArg("cmd");
|
|
842
|
+
if (!session || !name || !workerCmd) {
|
|
843
|
+
console.error("사용법: node psmux.mjs spawn --session <세션> --name <워커명> --cmd <커맨드>");
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
console.log(JSON.stringify(spawnWorker(session, name, workerCmd), null, 2));
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
case "status": {
|
|
850
|
+
const session = getArg("session");
|
|
851
|
+
const name = getArg("name");
|
|
852
|
+
if (!session || !name) {
|
|
853
|
+
console.error("사용법: node psmux.mjs status --session <세션> --name <워커명>");
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
console.log(JSON.stringify(getWorkerStatus(session, name), null, 2));
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
case "kill": {
|
|
860
|
+
const session = getArg("session");
|
|
861
|
+
const name = getArg("name");
|
|
862
|
+
if (!session || !name) {
|
|
863
|
+
console.error("사용법: node psmux.mjs kill --session <세션> --name <워커명>");
|
|
864
|
+
process.exit(1);
|
|
865
|
+
}
|
|
866
|
+
console.log(JSON.stringify(killWorker(session, name), null, 2));
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
869
|
+
case "output": {
|
|
870
|
+
const session = getArg("session");
|
|
871
|
+
const name = getArg("name");
|
|
872
|
+
const lines = parseInt(getArg("lines") || "50", 10);
|
|
873
|
+
if (!session || !name) {
|
|
874
|
+
console.error("사용법: node psmux.mjs output --session <세션> --name <워커명> [--lines <줄수>]");
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
console.log(captureWorkerOutput(session, name, lines));
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
case "capture-start": {
|
|
881
|
+
const session = getArg("session");
|
|
882
|
+
const name = getArg("name");
|
|
883
|
+
if (!session || !name) {
|
|
884
|
+
console.error("사용법: node psmux.mjs capture-start --session <세션> --name <pane>");
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
console.log(JSON.stringify(startCapture(session, name), null, 2));
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
case "dispatch": {
|
|
891
|
+
const session = getArg("session");
|
|
892
|
+
const name = getArg("name");
|
|
893
|
+
const commandText = getArg("command");
|
|
894
|
+
if (!session || !name || !commandText) {
|
|
895
|
+
console.error("사용법: node psmux.mjs dispatch --session <세션> --name <pane> --command <PowerShell 명령>");
|
|
896
|
+
process.exit(1);
|
|
897
|
+
}
|
|
898
|
+
console.log(JSON.stringify(dispatchCommand(session, name, commandText), null, 2));
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
case "wait-pattern": {
|
|
902
|
+
const session = getArg("session");
|
|
903
|
+
const name = getArg("name");
|
|
904
|
+
const pattern = getArg("pattern");
|
|
905
|
+
const timeoutSec = parseInt(getArg("timeout") || "300", 10);
|
|
906
|
+
if (!session || !name || !pattern) {
|
|
907
|
+
console.error("사용법: node psmux.mjs wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]");
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
const result = waitForPattern(session, name, pattern, timeoutSec);
|
|
911
|
+
console.log(JSON.stringify(result, null, 2));
|
|
912
|
+
if (!result.matched) process.exit(2);
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
case "wait-completion": {
|
|
916
|
+
const session = getArg("session");
|
|
917
|
+
const name = getArg("name");
|
|
918
|
+
const token = getArg("token");
|
|
919
|
+
const timeoutSec = parseInt(getArg("timeout") || "300", 10);
|
|
920
|
+
if (!session || !name || !token) {
|
|
921
|
+
console.error("사용법: node psmux.mjs wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]");
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
const result = waitForCompletion(session, name, token, timeoutSec);
|
|
925
|
+
console.log(JSON.stringify(result, null, 2));
|
|
926
|
+
if (!result.matched) process.exit(2);
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
default:
|
|
930
|
+
console.error("사용법: node psmux.mjs spawn|status|kill|output|capture-start|dispatch|wait-pattern|wait-completion [args]");
|
|
931
|
+
console.error("");
|
|
932
|
+
console.error(" spawn --session <세션> --name <워커명> --cmd <커맨드>");
|
|
933
|
+
console.error(" status --session <세션> --name <워커명>");
|
|
934
|
+
console.error(" kill --session <세션> --name <워커명>");
|
|
935
|
+
console.error(" output --session <세션> --name <워커명> [--lines <줄수>]");
|
|
936
|
+
console.error(" capture-start --session <세션> --name <pane>");
|
|
937
|
+
console.error(" dispatch --session <세션> --name <pane> --command <PowerShell 명령>");
|
|
938
|
+
console.error(" wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]");
|
|
939
|
+
console.error(" wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]");
|
|
940
|
+
process.exit(1);
|
|
295
941
|
}
|
|
942
|
+
} catch (err) {
|
|
943
|
+
console.error(`오류: ${err.message}`);
|
|
944
|
+
process.exit(1);
|
|
296
945
|
}
|
|
297
946
|
}
|