triflux 3.3.0-dev.1 → 3.3.0-dev.5
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 +169 -39
- package/hooks/hooks.json +5 -0
- package/hub/assign-callbacks.mjs +136 -0
- package/hub/bridge.mjs +283 -97
- package/hub/pipe.mjs +104 -0
- package/hub/router.mjs +322 -1
- package/hub/schema.sql +40 -7
- package/hub/server.mjs +151 -53
- package/hub/store.mjs +293 -1
- package/hub/team/cli-team-status.mjs +17 -3
- package/hub/team/native-supervisor.mjs +62 -22
- package/hub/team/native.mjs +86 -10
- package/hub/team/psmux.mjs +555 -115
- package/hub/tools.mjs +101 -26
- package/hub/workers/delegator-mcp.mjs +1045 -0
- package/hub/workers/factory.mjs +3 -0
- package/hub/workers/interface.mjs +2 -2
- package/hud/hud-qos-status.mjs +1735 -1790
- package/package.json +60 -60
- package/scripts/__tests__/keyword-detector.test.mjs +3 -3
- package/scripts/__tests__/smoke.test.mjs +34 -0
- package/scripts/hub-ensure.mjs +21 -3
- package/scripts/lib/mcp-filter.mjs +637 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/mcp-check.mjs +126 -88
- package/scripts/setup.mjs +15 -10
- package/scripts/test-tfx-route-no-claude-native.mjs +10 -2
- package/scripts/tfx-route.sh +434 -179
package/hub/team/psmux.mjs
CHANGED
|
@@ -1,10 +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";
|
|
6
9
|
const GIT_BASH = process.env.GIT_BASH_PATH || "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
7
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
|
+
})();
|
|
8
21
|
|
|
9
22
|
function quoteArg(value) {
|
|
10
23
|
const str = String(value);
|
|
@@ -12,10 +25,143 @@ function quoteArg(value) {
|
|
|
12
25
|
return `"${str.replace(/"/g, '\\"')}"`;
|
|
13
26
|
}
|
|
14
27
|
|
|
28
|
+
function sanitizePathPart(value) {
|
|
29
|
+
return String(value).replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "_");
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
function toPaneTitle(index) {
|
|
16
33
|
return index === 0 ? "lead" : `worker-${index}`;
|
|
17
34
|
}
|
|
18
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
|
+
|
|
19
165
|
function parsePaneList(output) {
|
|
20
166
|
return output
|
|
21
167
|
.split("\n")
|
|
@@ -60,44 +206,123 @@ function parseSessionSummaries(output) {
|
|
|
60
206
|
.filter(Boolean);
|
|
61
207
|
}
|
|
62
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
|
+
|
|
63
229
|
function collectSessionPanes(sessionName) {
|
|
64
|
-
const output = psmuxExec(
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
]);
|
|
67
237
|
return parsePaneList(output);
|
|
68
238
|
}
|
|
69
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
|
+
|
|
70
298
|
function psmux(args, opts = {}) {
|
|
71
|
-
|
|
72
|
-
|
|
299
|
+
const normalizedArgs = normalizePsmuxArgs(args);
|
|
300
|
+
try {
|
|
301
|
+
const result = childProcess.execFileSync(PSMUX_BIN, normalizedArgs, {
|
|
73
302
|
encoding: "utf8",
|
|
74
|
-
timeout:
|
|
303
|
+
timeout: PSMUX_TIMEOUT_MS,
|
|
75
304
|
stdio: ["pipe", "pipe", "pipe"],
|
|
76
305
|
windowsHide: true,
|
|
77
306
|
...opts,
|
|
78
307
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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;
|
|
85
319
|
}
|
|
86
|
-
|
|
87
|
-
const result = execSync(`${quoteArg(PSMUX_BIN)} ${args}`, {
|
|
88
|
-
encoding: "utf8",
|
|
89
|
-
timeout: 10000,
|
|
90
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
91
|
-
windowsHide: true,
|
|
92
|
-
...opts,
|
|
93
|
-
});
|
|
94
|
-
return result != null ? result.trim() : "";
|
|
95
320
|
}
|
|
96
321
|
|
|
97
322
|
/** psmux 실행 가능 여부 확인 */
|
|
98
323
|
export function hasPsmux() {
|
|
99
324
|
try {
|
|
100
|
-
|
|
325
|
+
childProcess.execFileSync(PSMUX_BIN, ["-V"], {
|
|
101
326
|
stdio: "ignore",
|
|
102
327
|
timeout: 3000,
|
|
103
328
|
windowsHide: true,
|
|
@@ -130,45 +355,73 @@ export function createPsmuxSession(sessionName, opts = {}) {
|
|
|
130
355
|
const layout = opts.layout === "1xN" || opts.layout === "Nx1" ? opts.layout : "2x2";
|
|
131
356
|
const paneCount = Math.max(
|
|
132
357
|
1,
|
|
133
|
-
Number.isFinite(opts.paneCount) ? Math.trunc(opts.paneCount) : 4
|
|
358
|
+
Number.isFinite(opts.paneCount) ? Math.trunc(opts.paneCount) : 4,
|
|
134
359
|
);
|
|
135
360
|
const limitedPaneCount = layout === "2x2" ? Math.min(paneCount, 4) : paneCount;
|
|
136
361
|
const sessionTarget = `${sessionName}:0`;
|
|
137
362
|
|
|
138
|
-
const leadPane = psmuxExec(
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
]);
|
|
141
376
|
|
|
142
377
|
if (layout === "2x2" && limitedPaneCount >= 3) {
|
|
143
|
-
const rightPane = psmuxExec(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
]);
|
|
149
396
|
if (limitedPaneCount >= 4) {
|
|
150
|
-
psmuxExec(
|
|
151
|
-
|
|
152
|
-
|
|
397
|
+
psmuxExec([
|
|
398
|
+
"split-window",
|
|
399
|
+
"-v",
|
|
400
|
+
"-P",
|
|
401
|
+
"-F",
|
|
402
|
+
"#{session_name}:#{window_index}.#{pane_index}",
|
|
403
|
+
"-t",
|
|
404
|
+
leadPane,
|
|
405
|
+
]);
|
|
153
406
|
}
|
|
154
|
-
psmuxExec(
|
|
407
|
+
psmuxExec(["select-layout", "-t", sessionTarget, "tiled"]);
|
|
155
408
|
} else if (layout === "1xN") {
|
|
156
|
-
for (let
|
|
157
|
-
psmuxExec(
|
|
409
|
+
for (let index = 1; index < limitedPaneCount; index += 1) {
|
|
410
|
+
psmuxExec(["split-window", "-h", "-t", sessionTarget]);
|
|
158
411
|
}
|
|
159
|
-
psmuxExec(
|
|
412
|
+
psmuxExec(["select-layout", "-t", sessionTarget, "even-horizontal"]);
|
|
160
413
|
} else {
|
|
161
|
-
for (let
|
|
162
|
-
psmuxExec(
|
|
414
|
+
for (let index = 1; index < limitedPaneCount; index += 1) {
|
|
415
|
+
psmuxExec(["split-window", "-v", "-t", sessionTarget]);
|
|
163
416
|
}
|
|
164
|
-
psmuxExec(
|
|
417
|
+
psmuxExec(["select-layout", "-t", sessionTarget, "even-vertical"]);
|
|
165
418
|
}
|
|
166
419
|
|
|
167
|
-
psmuxExec(
|
|
420
|
+
psmuxExec(["select-pane", "-t", leadPane]);
|
|
168
421
|
|
|
169
422
|
const panes = collectSessionPanes(sessionName).slice(0, limitedPaneCount);
|
|
170
423
|
panes.forEach((pane, index) => {
|
|
171
|
-
psmuxExec(
|
|
424
|
+
psmuxExec(["select-pane", "-t", pane, "-T", toPaneTitle(index)]);
|
|
172
425
|
});
|
|
173
426
|
|
|
174
427
|
return { sessionName, panes };
|
|
@@ -180,7 +433,7 @@ export function createPsmuxSession(sessionName, opts = {}) {
|
|
|
180
433
|
*/
|
|
181
434
|
export function killPsmuxSession(sessionName) {
|
|
182
435
|
try {
|
|
183
|
-
psmuxExec(
|
|
436
|
+
psmuxExec(["kill-session", "-t", sessionName], { stdio: "ignore" });
|
|
184
437
|
} catch {
|
|
185
438
|
// 이미 종료된 세션 — 무시
|
|
186
439
|
}
|
|
@@ -193,7 +446,7 @@ export function killPsmuxSession(sessionName) {
|
|
|
193
446
|
*/
|
|
194
447
|
export function psmuxSessionExists(sessionName) {
|
|
195
448
|
try {
|
|
196
|
-
psmuxExec(
|
|
449
|
+
psmuxExec(["has-session", "-t", sessionName], { stdio: "ignore" });
|
|
197
450
|
return true;
|
|
198
451
|
} catch {
|
|
199
452
|
return false;
|
|
@@ -206,7 +459,7 @@ export function psmuxSessionExists(sessionName) {
|
|
|
206
459
|
*/
|
|
207
460
|
export function listPsmuxSessions() {
|
|
208
461
|
try {
|
|
209
|
-
return parseSessionSummaries(psmuxExec("list-sessions"))
|
|
462
|
+
return parseSessionSummaries(psmuxExec(["list-sessions"]))
|
|
210
463
|
.map((session) => session.sessionName)
|
|
211
464
|
.filter((sessionName) => sessionName.startsWith("tfx-multi-"));
|
|
212
465
|
} catch {
|
|
@@ -222,7 +475,7 @@ export function listPsmuxSessions() {
|
|
|
222
475
|
*/
|
|
223
476
|
export function capturePsmuxPane(target, lines = 5) {
|
|
224
477
|
try {
|
|
225
|
-
const full = psmuxExec(
|
|
478
|
+
const full = psmuxExec(["capture-pane", "-t", target, "-p"]);
|
|
226
479
|
const nonEmpty = full.split("\n").filter((line) => line.trim() !== "");
|
|
227
480
|
return nonEmpty.slice(-lines).join("\n");
|
|
228
481
|
} catch {
|
|
@@ -235,7 +488,7 @@ export function capturePsmuxPane(target, lines = 5) {
|
|
|
235
488
|
* @param {string} sessionName
|
|
236
489
|
*/
|
|
237
490
|
export function attachPsmuxSession(sessionName) {
|
|
238
|
-
const result = spawnSync(PSMUX_BIN, ["attach-session", "-t", sessionName], {
|
|
491
|
+
const result = childProcess.spawnSync(PSMUX_BIN, ["attach-session", "-t", sessionName], {
|
|
239
492
|
stdio: "inherit",
|
|
240
493
|
timeout: 0,
|
|
241
494
|
windowsHide: false,
|
|
@@ -252,7 +505,7 @@ export function attachPsmuxSession(sessionName) {
|
|
|
252
505
|
*/
|
|
253
506
|
export function getPsmuxSessionAttachedCount(sessionName) {
|
|
254
507
|
try {
|
|
255
|
-
const session = parseSessionSummaries(psmuxExec("list-sessions"))
|
|
508
|
+
const session = parseSessionSummaries(psmuxExec(["list-sessions"]))
|
|
256
509
|
.find((entry) => entry.sessionName === sessionName);
|
|
257
510
|
return session ? session.attachedCount : null;
|
|
258
511
|
} catch {
|
|
@@ -272,30 +525,166 @@ export function configurePsmuxKeybindings(sessionName, opts = {}) {
|
|
|
272
525
|
const cond = `#{==:#{session_name},${sessionName}}`;
|
|
273
526
|
const target = `${sessionName}:0`;
|
|
274
527
|
const bindNext = inProcess
|
|
275
|
-
?
|
|
276
|
-
:
|
|
528
|
+
? "select-pane -t :.+ \\; resize-pane -Z"
|
|
529
|
+
: "select-pane -t :.+";
|
|
277
530
|
const bindPrev = inProcess
|
|
278
|
-
?
|
|
279
|
-
:
|
|
531
|
+
? "select-pane -t :.- \\; resize-pane -Z"
|
|
532
|
+
: "select-pane -t :.-";
|
|
280
533
|
|
|
281
|
-
// psmux는 세션별 서버이므로 -t target으로 세션 컨텍스트를 전달해야
|
|
282
|
-
const bindSafe = (
|
|
283
|
-
try {
|
|
534
|
+
// psmux는 세션별 서버이므로 -t target으로 세션 컨텍스트를 전달해야 한다.
|
|
535
|
+
const bindSafe = (args) => {
|
|
536
|
+
try {
|
|
537
|
+
psmuxExec(["-t", target, ...args]);
|
|
538
|
+
} catch {
|
|
539
|
+
// 미지원 시 무시
|
|
540
|
+
}
|
|
284
541
|
};
|
|
285
542
|
|
|
286
|
-
bindSafe(
|
|
287
|
-
bindSafe(
|
|
288
|
-
bindSafe(
|
|
289
|
-
bindSafe(
|
|
290
|
-
bindSafe(
|
|
291
|
-
bindSafe(
|
|
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"]);
|
|
292
549
|
|
|
293
550
|
if (taskListCommand) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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})를 호출하세요.`);
|
|
298
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
|
+
};
|
|
299
688
|
}
|
|
300
689
|
|
|
301
690
|
// ─── 하이브리드 모드 워커 관리 함수 ───
|
|
@@ -314,23 +703,28 @@ export function spawnWorker(sessionName, workerName, cmd) {
|
|
|
314
703
|
|
|
315
704
|
// remain-on-exit: 종료된 pane이 즉시 사라지는 것 방지
|
|
316
705
|
try {
|
|
317
|
-
psmuxExec(
|
|
318
|
-
} catch {
|
|
706
|
+
psmuxExec(["set-option", "-t", sessionName, "remain-on-exit", "on"]);
|
|
707
|
+
} catch {
|
|
708
|
+
// 미지원 시 무시
|
|
709
|
+
}
|
|
319
710
|
|
|
320
711
|
// Windows: pane 기본셸이 PowerShell → Git Bash로 래핑
|
|
321
|
-
// psmux가 이스케이프 시퀀스를 처리하므로 포워드
|
|
712
|
+
// psmux가 이스케이프 시퀀스를 처리하므로 포워드 슬래시 경로를 사용한다.
|
|
322
713
|
const shellCmd = IS_WINDOWS
|
|
323
|
-
? `& '${GIT_BASH.replace(/\\/g,
|
|
714
|
+
? `& '${GIT_BASH.replace(/\\/g, "/")}' -l -c '${cmd.replace(/'/g, "'\\''")}'`
|
|
324
715
|
: cmd;
|
|
325
716
|
|
|
326
717
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
"
|
|
330
|
-
|
|
718
|
+
const paneTarget = psmuxExec([
|
|
719
|
+
"split-window",
|
|
720
|
+
"-t",
|
|
721
|
+
sessionName,
|
|
722
|
+
"-P",
|
|
723
|
+
"-F",
|
|
724
|
+
"#{session_name}:#{window_index}.#{pane_index}",
|
|
331
725
|
shellCmd,
|
|
332
726
|
]);
|
|
333
|
-
|
|
727
|
+
psmuxExec(["select-pane", "-t", paneTarget, "-T", workerName]);
|
|
334
728
|
return { paneId: paneTarget, workerName };
|
|
335
729
|
} catch (err) {
|
|
336
730
|
throw new Error(`워커 생성 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
|
|
@@ -348,24 +742,16 @@ export function getWorkerStatus(sessionName, workerName) {
|
|
|
348
742
|
throw new Error("psmux가 설치되어 있지 않습니다.");
|
|
349
743
|
}
|
|
350
744
|
try {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (title === workerName) {
|
|
358
|
-
const isDead = dead === "1";
|
|
359
|
-
return {
|
|
360
|
-
status: isDead ? "exited" : "running",
|
|
361
|
-
exitCode: isDead ? parseInt(deadStatus, 10) || 0 : null,
|
|
362
|
-
paneId,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
throw new Error(`워커를 찾을 수 없습니다: ${workerName}`);
|
|
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
|
+
};
|
|
367
751
|
} catch (err) {
|
|
368
|
-
if (err.message.includes("
|
|
752
|
+
if (err.message.includes("Pane을 찾을 수 없습니다")) {
|
|
753
|
+
throw new Error(`워커를 찾을 수 없습니다: ${workerName}`);
|
|
754
|
+
}
|
|
369
755
|
throw new Error(`워커 상태 조회 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
|
|
370
756
|
}
|
|
371
757
|
}
|
|
@@ -385,26 +771,30 @@ export function killWorker(sessionName, workerName) {
|
|
|
385
771
|
|
|
386
772
|
// 이미 종료된 워커 → pane 정리만 수행
|
|
387
773
|
if (status === "exited") {
|
|
388
|
-
try {
|
|
774
|
+
try {
|
|
775
|
+
psmuxExec(["kill-pane", "-t", paneId]);
|
|
776
|
+
} catch {
|
|
777
|
+
// 무시
|
|
778
|
+
}
|
|
389
779
|
return { killed: true };
|
|
390
780
|
}
|
|
391
781
|
|
|
392
782
|
// running → C-c 우아한 종료 시도
|
|
393
783
|
try {
|
|
394
|
-
psmuxExec(
|
|
784
|
+
psmuxExec(["send-keys", "-t", paneId, "C-c"]);
|
|
395
785
|
} catch {
|
|
396
786
|
// send-keys 실패 무시
|
|
397
787
|
}
|
|
398
|
-
|
|
399
|
-
|
|
788
|
+
|
|
789
|
+
sleepMs(1000);
|
|
790
|
+
|
|
400
791
|
try {
|
|
401
|
-
psmuxExec(
|
|
792
|
+
psmuxExec(["kill-pane", "-t", paneId]);
|
|
402
793
|
} catch {
|
|
403
794
|
// 이미 종료된 pane — 무시
|
|
404
795
|
}
|
|
405
796
|
return { killed: true };
|
|
406
797
|
} catch (err) {
|
|
407
|
-
// 워커를 찾을 수 없음 → 이미 종료된 것으로 간주
|
|
408
798
|
if (err.message.includes("워커를 찾을 수 없습니다")) {
|
|
409
799
|
return { killed: true };
|
|
410
800
|
}
|
|
@@ -425,7 +815,7 @@ export function captureWorkerOutput(sessionName, workerName, lines = 50) {
|
|
|
425
815
|
}
|
|
426
816
|
try {
|
|
427
817
|
const { paneId } = getWorkerStatus(sessionName, workerName);
|
|
428
|
-
return psmuxExec(
|
|
818
|
+
return psmuxExec(["capture-pane", "-t", paneId, "-p", "-S", `-${lines}`]);
|
|
429
819
|
} catch (err) {
|
|
430
820
|
if (err.message.includes("워커를 찾을 수 없습니다")) throw err;
|
|
431
821
|
throw new Error(`출력 캡처 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
|
|
@@ -435,7 +825,7 @@ export function captureWorkerOutput(sessionName, workerName, lines = 50) {
|
|
|
435
825
|
// ─── CLI 진입점 ───
|
|
436
826
|
|
|
437
827
|
if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
438
|
-
const [
|
|
828
|
+
const [, , cmd, ...args] = process.argv;
|
|
439
829
|
|
|
440
830
|
// CLI 인자 파싱 헬퍼
|
|
441
831
|
function getArg(name) {
|
|
@@ -453,8 +843,7 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
453
843
|
console.error("사용법: node psmux.mjs spawn --session <세션> --name <워커명> --cmd <커맨드>");
|
|
454
844
|
process.exit(1);
|
|
455
845
|
}
|
|
456
|
-
|
|
457
|
-
console.log(JSON.stringify(result, null, 2));
|
|
846
|
+
console.log(JSON.stringify(spawnWorker(session, name, workerCmd), null, 2));
|
|
458
847
|
break;
|
|
459
848
|
}
|
|
460
849
|
case "status": {
|
|
@@ -464,8 +853,7 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
464
853
|
console.error("사용법: node psmux.mjs status --session <세션> --name <워커명>");
|
|
465
854
|
process.exit(1);
|
|
466
855
|
}
|
|
467
|
-
|
|
468
|
-
console.log(JSON.stringify(result, null, 2));
|
|
856
|
+
console.log(JSON.stringify(getWorkerStatus(session, name), null, 2));
|
|
469
857
|
break;
|
|
470
858
|
}
|
|
471
859
|
case "kill": {
|
|
@@ -475,8 +863,7 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
475
863
|
console.error("사용법: node psmux.mjs kill --session <세션> --name <워커명>");
|
|
476
864
|
process.exit(1);
|
|
477
865
|
}
|
|
478
|
-
|
|
479
|
-
console.log(JSON.stringify(result, null, 2));
|
|
866
|
+
console.log(JSON.stringify(killWorker(session, name), null, 2));
|
|
480
867
|
break;
|
|
481
868
|
}
|
|
482
869
|
case "output": {
|
|
@@ -490,13 +877,66 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
490
877
|
console.log(captureWorkerOutput(session, name, lines));
|
|
491
878
|
break;
|
|
492
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
|
+
}
|
|
493
929
|
default:
|
|
494
|
-
console.error("사용법: node psmux.mjs spawn|status|kill|output [args]");
|
|
930
|
+
console.error("사용법: node psmux.mjs spawn|status|kill|output|capture-start|dispatch|wait-pattern|wait-completion [args]");
|
|
495
931
|
console.error("");
|
|
496
|
-
console.error(" spawn
|
|
497
|
-
console.error(" status
|
|
498
|
-
console.error(" kill
|
|
499
|
-
console.error(" output
|
|
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 <초>]");
|
|
500
940
|
process.exit(1);
|
|
501
941
|
}
|
|
502
942
|
} catch (err) {
|