triflux 10.16.0 → 10.17.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/bin/triflux.mjs +21 -22
- package/hooks/keyword-rules.json +12 -0
- package/hooks/safety-guard.mjs +3 -0
- package/hub/cli-adapter-base.mjs +10 -5
- package/hub/lib/hosts-compat.mjs +19 -4
- package/hub/lib/ssh-command.mjs +4 -10
- package/hub/router.mjs +20 -3
- package/hub/server.mjs +8 -1
- package/hub/team/agent-map.json +2 -0
- package/hub/team/codex-review.mjs +14 -9
- package/hub/team/conductor.mjs +8 -4
- package/hub/team/psmux.mjs +1 -1
- package/hub/team/worktree-lifecycle.mjs +14 -1
- package/hub/workers/codex-app-server-worker.mjs +4 -6
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -1
- package/hub/workers/worker-utils.mjs +1 -2
- package/package.json +1 -1
- package/scripts/codex-mcp-gateway-sync.mjs +22 -0
- package/scripts/doctor-diagnose.mjs +24 -11
- package/scripts/lib/mcp-guard-engine.mjs +20 -0
- package/scripts/setup.mjs +29 -1
- package/scripts/sync-hub-mcp-settings.mjs +38 -1
- package/scripts/test-lock.mjs +134 -36
package/bin/triflux.mjs
CHANGED
|
@@ -5073,29 +5073,28 @@ function startHubAfterUpdate(info) {
|
|
|
5073
5073
|
function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
|
|
5074
5074
|
section("MCP 자동 등록");
|
|
5075
5075
|
|
|
5076
|
-
// Codex — config.json에 기본 disabled 엔트리로
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
warn(`Codex 등록 실패: ${e.message}`);
|
|
5076
|
+
// Codex — config.json에 기본 disabled 엔트리로 등록.
|
|
5077
|
+
// Hub startup must keep the MCP config fresh even on CI/dev machines where
|
|
5078
|
+
// the Codex CLI binary itself is not installed.
|
|
5079
|
+
try {
|
|
5080
|
+
const result = ensureCodexHubServerConfig({
|
|
5081
|
+
mcpUrl,
|
|
5082
|
+
createIfMissing: true,
|
|
5083
|
+
enabled: codexEnabled,
|
|
5084
|
+
});
|
|
5085
|
+
if (!result.ok) throw new Error(result.reason || "unknown");
|
|
5086
|
+
const suffix = which("codex") ? "" : " (CLI 미설치)";
|
|
5087
|
+
if (result.changed) {
|
|
5088
|
+
ok(
|
|
5089
|
+
`Codex: config.json에 등록 완료 (${codexEnabled ? "enabled" : "기본 disabled"})${suffix}`,
|
|
5090
|
+
);
|
|
5091
|
+
} else {
|
|
5092
|
+
ok(
|
|
5093
|
+
`Codex: 이미 등록됨 (${codexEnabled ? "enabled" : "기본 disabled"})${suffix}`,
|
|
5094
|
+
);
|
|
5096
5095
|
}
|
|
5097
|
-
}
|
|
5098
|
-
|
|
5096
|
+
} catch (e) {
|
|
5097
|
+
warn(`Codex 등록 실패: ${e.message}`);
|
|
5099
5098
|
}
|
|
5100
5099
|
|
|
5101
5100
|
// Gemini — settings.json 직접 수정
|
package/hooks/keyword-rules.json
CHANGED
|
@@ -97,6 +97,18 @@
|
|
|
97
97
|
"source": "(?:정리해|슬롭|클린업)",
|
|
98
98
|
"flags": "i"
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
"source": "(?:병렬|동시에|parallel|concurrent)",
|
|
102
|
+
"flags": "i"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"source": "(?:점검|진단|확인해)",
|
|
106
|
+
"flags": "i"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"source": "(?:계속해|이어서|진행해)",
|
|
110
|
+
"flags": "i"
|
|
111
|
+
},
|
|
100
112
|
{
|
|
101
113
|
"source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b",
|
|
102
114
|
"flags": "i"
|
package/hooks/safety-guard.mjs
CHANGED
|
@@ -123,9 +123,12 @@ function getWindowsHostIds() {
|
|
|
123
123
|
ids.add(name);
|
|
124
124
|
if (cfg.tailscale?.ip) ids.add(cfg.tailscale.ip);
|
|
125
125
|
if (cfg.tailscale?.dns) ids.add(cfg.tailscale.dns);
|
|
126
|
+
if (cfg.ssh?.host) ids.add(cfg.ssh.host);
|
|
126
127
|
if (cfg.ssh?.user) {
|
|
127
128
|
ids.add(`${cfg.ssh.user}@${name}`);
|
|
128
129
|
if (cfg.tailscale?.ip) ids.add(`${cfg.ssh.user}@${cfg.tailscale.ip}`);
|
|
130
|
+
if (cfg.tailscale?.dns) ids.add(`${cfg.ssh.user}@${cfg.tailscale.dns}`);
|
|
131
|
+
if (cfg.ssh?.host) ids.add(`${cfg.ssh.user}@${cfg.ssh.host}`);
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
} catch {
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -72,6 +72,11 @@ let _cachedVersion = null;
|
|
|
72
72
|
*/
|
|
73
73
|
export function getCodexVersion() {
|
|
74
74
|
if (_cachedVersion !== null) return _cachedVersion;
|
|
75
|
+
const override = Number(process.env.TFX_CODEX_VERSION_MINOR);
|
|
76
|
+
if (Number.isFinite(override) && override > 0) {
|
|
77
|
+
_cachedVersion = override;
|
|
78
|
+
return _cachedVersion;
|
|
79
|
+
}
|
|
75
80
|
try {
|
|
76
81
|
const out = execSync("codex --version", {
|
|
77
82
|
encoding: "utf8",
|
|
@@ -80,7 +85,10 @@ export function getCodexVersion() {
|
|
|
80
85
|
const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
81
86
|
_cachedVersion = match ? Number.parseInt(match[2], 10) : 0;
|
|
82
87
|
} catch {
|
|
83
|
-
|
|
88
|
+
// Command builders should remain stable in CI even when the real Codex
|
|
89
|
+
// CLI is absent. Runtime preflight still reports/install-gates Codex
|
|
90
|
+
// separately; this fallback only selects the modern argv shape.
|
|
91
|
+
_cachedVersion = 117;
|
|
84
92
|
}
|
|
85
93
|
return _cachedVersion;
|
|
86
94
|
}
|
|
@@ -182,10 +190,7 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
182
190
|
// ── Sleep ───────────────────────────────────────────────────────
|
|
183
191
|
|
|
184
192
|
export function sleep(ms) {
|
|
185
|
-
return new Promise((resolve) =>
|
|
186
|
-
const timer = setTimeout(resolve, ms);
|
|
187
|
-
timer.unref?.();
|
|
188
|
-
});
|
|
193
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
// ── Result factory ──────────────────────────────────────────────
|
package/hub/lib/hosts-compat.mjs
CHANGED
|
@@ -133,8 +133,20 @@ function normalizeLastProbe(rawProbe) {
|
|
|
133
133
|
return Object.keys(probe).length > 0 ? probe : null;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function normalizeResources(rawHost) {
|
|
137
|
+
const rawResources =
|
|
138
|
+
rawHost.resources && typeof rawHost.resources === "object"
|
|
139
|
+
? rawHost.resources
|
|
140
|
+
: {};
|
|
141
|
+
const rawSpecs =
|
|
142
|
+
rawHost.specs && typeof rawHost.specs === "object" ? rawHost.specs : {};
|
|
143
|
+
return { ...rawResources, ...rawSpecs };
|
|
144
|
+
}
|
|
145
|
+
|
|
136
146
|
export function normalizeHost(rawHost = {}, name = "") {
|
|
137
147
|
const sshUser = rawHost.ssh_user || rawHost.ssh?.user || rawHost.user || null;
|
|
148
|
+
const sshHost = rawHost.ssh?.host || rawHost.host || null;
|
|
149
|
+
const resources = normalizeResources(rawHost);
|
|
138
150
|
const tailscale = {
|
|
139
151
|
ip: rawHost.tailscale?.ip || null,
|
|
140
152
|
dns: rawHost.tailscale?.dns || null,
|
|
@@ -167,15 +179,14 @@ export function normalizeHost(rawHost = {}, name = "") {
|
|
|
167
179
|
ssh: {
|
|
168
180
|
...(rawHost.ssh && typeof rawHost.ssh === "object" ? rawHost.ssh : {}),
|
|
169
181
|
user: sshUser,
|
|
182
|
+
host: sshHost,
|
|
170
183
|
},
|
|
171
184
|
tailscale,
|
|
172
185
|
capabilities,
|
|
173
186
|
capabilities_v2,
|
|
174
187
|
last_probe: normalizeLastProbe(rawHost.last_probe),
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
? { ...rawHost.specs }
|
|
178
|
-
: {},
|
|
188
|
+
resources,
|
|
189
|
+
specs: { ...resources },
|
|
179
190
|
raw: { ...rawHost },
|
|
180
191
|
};
|
|
181
192
|
}
|
|
@@ -234,6 +245,7 @@ export function resolveHost(nameOrAlias, repoRoot) {
|
|
|
234
245
|
...host.aliases,
|
|
235
246
|
host.tailscale.ip,
|
|
236
247
|
host.tailscale.dns,
|
|
248
|
+
host.ssh.host,
|
|
237
249
|
host.ssh_user ? `${host.ssh_user}@${name}` : null,
|
|
238
250
|
host.ssh_user && host.tailscale.ip
|
|
239
251
|
? `${host.ssh_user}@${host.tailscale.ip}`
|
|
@@ -241,6 +253,9 @@ export function resolveHost(nameOrAlias, repoRoot) {
|
|
|
241
253
|
host.ssh_user && host.tailscale.dns
|
|
242
254
|
? `${host.ssh_user}@${host.tailscale.dns}`
|
|
243
255
|
: null,
|
|
256
|
+
host.ssh_user && host.ssh.host
|
|
257
|
+
? `${host.ssh_user}@${host.ssh.host}`
|
|
258
|
+
: null,
|
|
244
259
|
]);
|
|
245
260
|
for (const alias of aliases) {
|
|
246
261
|
if (alias && String(alias).toLowerCase() === lowered) {
|
package/hub/lib/ssh-command.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// PowerShell 호스트에 bash 문법(2>/dev/null, &&, $())을 보내는 사고를 방지한다.
|
|
3
3
|
// 모든 SSH 명령 생성 코드에서 이 유틸리티를 사용할 것.
|
|
4
4
|
|
|
5
|
-
import { readHosts } from "./hosts-compat.mjs";
|
|
5
|
+
import { readHost, readHosts, resolveHost } from "./hosts-compat.mjs";
|
|
6
6
|
|
|
7
7
|
/** hosts.json 캐시 (프로세스 수명 동안 유지) */
|
|
8
8
|
let hostsCache = null;
|
|
@@ -28,7 +28,7 @@ function loadHostsCache(repoRoot) {
|
|
|
28
28
|
export function detectHostOs(hostAlias, repoRoot) {
|
|
29
29
|
loadHostsCache(repoRoot);
|
|
30
30
|
|
|
31
|
-
const hostCfg =
|
|
31
|
+
const hostCfg = resolveHost(hostAlias, repoRoot)?.host;
|
|
32
32
|
if (hostCfg?.os === "windows") return "windows";
|
|
33
33
|
if (hostCfg?.os) return "posix";
|
|
34
34
|
|
|
@@ -163,7 +163,7 @@ export function validateCommandForOs(command, os) {
|
|
|
163
163
|
*/
|
|
164
164
|
export function getHostConfig(hostAlias, repoRoot) {
|
|
165
165
|
loadHostsCache(repoRoot);
|
|
166
|
-
return
|
|
166
|
+
return readHost(hostAlias, repoRoot);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/**
|
|
@@ -175,13 +175,7 @@ export function getHostConfig(hostAlias, repoRoot) {
|
|
|
175
175
|
export function resolveHostAlias(alias, repoRoot) {
|
|
176
176
|
loadHostsCache(repoRoot);
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
for (const [name, cfg] of Object.entries(hostsCache.hosts || {})) {
|
|
181
|
-
if (cfg.aliases.includes(alias)) return name;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return null;
|
|
178
|
+
return resolveHost(alias, repoRoot)?.name ?? null;
|
|
185
179
|
}
|
|
186
180
|
|
|
187
181
|
/**
|
package/hub/router.mjs
CHANGED
|
@@ -60,6 +60,21 @@ function normalizeAgentTopics(store, agentId, runtimeTopics) {
|
|
|
60
60
|
return Array.from(topics);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function waitForEmitterOnce(emitter, eventName, timeoutMs) {
|
|
64
|
+
let timer = null;
|
|
65
|
+
const timeout = Math.max(0, Math.min(Number(timeoutMs) || 0, 30000));
|
|
66
|
+
return Promise.race([
|
|
67
|
+
once(emitter, eventName),
|
|
68
|
+
new Promise((_, reject) => {
|
|
69
|
+
timer = setTimeout(() => {
|
|
70
|
+
reject(new Error(`timed out waiting for ${eventName}`));
|
|
71
|
+
}, timeout);
|
|
72
|
+
}),
|
|
73
|
+
]).finally(() => {
|
|
74
|
+
if (timer) clearTimeout(timer);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
/**
|
|
64
79
|
* 라우터 생성
|
|
65
80
|
* @param {object} store
|
|
@@ -525,9 +540,11 @@ export function createRouter(store) {
|
|
|
525
540
|
}
|
|
526
541
|
|
|
527
542
|
try {
|
|
528
|
-
const [response] = await
|
|
529
|
-
|
|
530
|
-
|
|
543
|
+
const [response] = await waitForEmitterOnce(
|
|
544
|
+
responseEmitter,
|
|
545
|
+
cid,
|
|
546
|
+
await_response_ms,
|
|
547
|
+
);
|
|
531
548
|
return {
|
|
532
549
|
ok: true,
|
|
533
550
|
data: {
|
package/hub/server.mjs
CHANGED
|
@@ -326,7 +326,14 @@ async function syncHubMcpSettingsIfAvailable({ hubUrl }) {
|
|
|
326
326
|
if (typeof mod?.syncCodexHubUrl === "function") {
|
|
327
327
|
await mod.syncCodexHubUrl({ hubUrl });
|
|
328
328
|
}
|
|
329
|
-
|
|
329
|
+
const allowProjectSync =
|
|
330
|
+
process.env.TFX_PROJECT_MCP_SYNC === "1" ||
|
|
331
|
+
!(
|
|
332
|
+
process.env.NODE_ENV === "test" ||
|
|
333
|
+
process.env.CI === "true" ||
|
|
334
|
+
process.env.TFX_TEST === "1"
|
|
335
|
+
);
|
|
336
|
+
if (allowProjectSync && typeof mod?.syncProjectMcpJson === "function") {
|
|
330
337
|
await mod.syncProjectMcpJson({ hubUrl, projectRoot: PROJECT_ROOT });
|
|
331
338
|
}
|
|
332
339
|
} catch (error) {
|
package/hub/team/agent-map.json
CHANGED
|
@@ -55,21 +55,22 @@ export function expandRange({ ref = "HEAD", base } = {}) {
|
|
|
55
55
|
* - `HEAD` with no prior commit returns an empty string
|
|
56
56
|
* - optional `file` scopes the diff to a single path via `-- <file>`
|
|
57
57
|
*
|
|
58
|
-
* @param {{ ref?: string, base?: string, file?: string }} opts
|
|
58
|
+
* @param {{ ref?: string, base?: string, file?: string, runner?: (cmd: string, args: string[]) => string }} opts
|
|
59
59
|
* @returns {{ diff: string, range: string, file?: string }}
|
|
60
60
|
*/
|
|
61
|
-
export function resolveReviewDiff({
|
|
61
|
+
export function resolveReviewDiff({
|
|
62
|
+
ref = "HEAD",
|
|
63
|
+
base,
|
|
64
|
+
file,
|
|
65
|
+
runner = _defaultGitRunner,
|
|
66
|
+
} = {}) {
|
|
62
67
|
const range = expandRange({ ref, base });
|
|
63
68
|
|
|
64
69
|
const args = ["log", "-p", "--stat", "--no-color", range];
|
|
65
70
|
if (file) {
|
|
66
71
|
args.push("--", file);
|
|
67
72
|
}
|
|
68
|
-
const diff =
|
|
69
|
-
encoding: "utf8",
|
|
70
|
-
windowsHide: true,
|
|
71
|
-
maxBuffer: 50 * 1024 * 1024,
|
|
72
|
-
});
|
|
73
|
+
const diff = runner("git", args);
|
|
73
74
|
return { diff, range, file };
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -306,11 +307,15 @@ export async function runCodexReview({
|
|
|
306
307
|
timeoutMs = 180_000,
|
|
307
308
|
sandbox = "read-only",
|
|
308
309
|
env = process.env,
|
|
310
|
+
_deps = {},
|
|
309
311
|
} = {}) {
|
|
310
312
|
let range;
|
|
311
313
|
let diff;
|
|
312
314
|
try {
|
|
313
|
-
({ diff, range } = resolveReviewDiff
|
|
315
|
+
({ diff, range } = (_deps.resolveReviewDiff || resolveReviewDiff)({
|
|
316
|
+
ref,
|
|
317
|
+
base,
|
|
318
|
+
}));
|
|
314
319
|
} catch (err) {
|
|
315
320
|
return {
|
|
316
321
|
ok: false,
|
|
@@ -353,7 +358,7 @@ export async function runCodexReview({
|
|
|
353
358
|
};
|
|
354
359
|
}
|
|
355
360
|
|
|
356
|
-
return _runCodexOnPrompt({
|
|
361
|
+
return (_deps.runCodexOnPrompt || _runCodexOnPrompt)({
|
|
357
362
|
prompt,
|
|
358
363
|
range,
|
|
359
364
|
diffBytes,
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -57,7 +57,7 @@ export const STATES = Object.freeze({
|
|
|
57
57
|
/** 유효한 상태 전이 테이블 */
|
|
58
58
|
const TRANSITIONS = Object.freeze({
|
|
59
59
|
[STATES.INIT]: [STATES.STARTING],
|
|
60
|
-
[STATES.STARTING]: [STATES.HEALTHY, STATES.FAILED],
|
|
60
|
+
[STATES.STARTING]: [STATES.HEALTHY, STATES.FAILED, STATES.COMPLETED],
|
|
61
61
|
[STATES.HEALTHY]: [
|
|
62
62
|
STATES.STALLED,
|
|
63
63
|
STATES.INPUT_WAIT,
|
|
@@ -337,7 +337,6 @@ export function createConductor(opts = {}) {
|
|
|
337
337
|
forceKill(pid);
|
|
338
338
|
resolve();
|
|
339
339
|
}, graceMs);
|
|
340
|
-
timer.unref?.();
|
|
341
340
|
child.once("exit", () => {
|
|
342
341
|
clearTimeout(timer);
|
|
343
342
|
resolve();
|
|
@@ -629,6 +628,7 @@ export function createConductor(opts = {}) {
|
|
|
629
628
|
});
|
|
630
629
|
|
|
631
630
|
if (TERMINAL_STATES.has(session.state)) return;
|
|
631
|
+
if (shuttingDown) return;
|
|
632
632
|
|
|
633
633
|
if (code === 0 && !signal) {
|
|
634
634
|
transition(session, STATES.COMPLETED, "exit_0");
|
|
@@ -675,7 +675,7 @@ export function createConductor(opts = {}) {
|
|
|
675
675
|
session: session.id,
|
|
676
676
|
error: err.message,
|
|
677
677
|
});
|
|
678
|
-
if (!TERMINAL_STATES.has(session.state)) {
|
|
678
|
+
if (!shuttingDown && !TERMINAL_STATES.has(session.state)) {
|
|
679
679
|
handleFailure(session, `child_error:${err.message}`);
|
|
680
680
|
}
|
|
681
681
|
});
|
|
@@ -866,6 +866,8 @@ export function createConductor(opts = {}) {
|
|
|
866
866
|
host,
|
|
867
867
|
});
|
|
868
868
|
|
|
869
|
+
if (shuttingDown || TERMINAL_STATES.has(session.state)) return;
|
|
870
|
+
|
|
869
871
|
// 원격 세션은 broker lease를 사용하지 않으므로 release 불필요
|
|
870
872
|
// (spawnSession에서 config.remote === true일 때 lease 건너뜀)
|
|
871
873
|
if (code === 0) {
|
|
@@ -900,7 +902,9 @@ export function createConductor(opts = {}) {
|
|
|
900
902
|
session: session.id,
|
|
901
903
|
error: err.message,
|
|
902
904
|
});
|
|
903
|
-
|
|
905
|
+
if (!shuttingDown && !TERMINAL_STATES.has(session.state)) {
|
|
906
|
+
handleFailure(session, `spawn_error:${err.message}`);
|
|
907
|
+
}
|
|
904
908
|
});
|
|
905
909
|
}
|
|
906
910
|
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -940,7 +940,7 @@ function killOrphanMcpProcesses(sessionName) {
|
|
|
940
940
|
// (e.g. `<session>2-worker-1.txt`).
|
|
941
941
|
const escSession = escapeRegex(safeSessionUnix);
|
|
942
942
|
const pids = childProcess
|
|
943
|
-
.execFileSync("pgrep", ["-f", `tfx-headless/${escSession}[
|
|
943
|
+
.execFileSync("pgrep", ["-f", `tfx-headless/${escSession}[-./]`], {
|
|
944
944
|
encoding: "utf8",
|
|
945
945
|
timeout: 5000,
|
|
946
946
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -58,7 +58,20 @@ function git(args, cwd) {
|
|
|
58
58
|
execFile(
|
|
59
59
|
"git",
|
|
60
60
|
args,
|
|
61
|
-
{
|
|
61
|
+
{
|
|
62
|
+
cwd,
|
|
63
|
+
windowsHide: true,
|
|
64
|
+
timeout: 30_000,
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || "Triflux",
|
|
68
|
+
GIT_AUTHOR_EMAIL:
|
|
69
|
+
process.env.GIT_AUTHOR_EMAIL || "triflux@example.invalid",
|
|
70
|
+
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || "Triflux",
|
|
71
|
+
GIT_COMMITTER_EMAIL:
|
|
72
|
+
process.env.GIT_COMMITTER_EMAIL || "triflux@example.invalid",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
62
75
|
(err, stdout, stderr) => {
|
|
63
76
|
if (err) {
|
|
64
77
|
const msg = `git ${args[0]} failed: ${stderr?.trim() || err.message}`;
|
|
@@ -876,7 +876,6 @@ export class CodexAppServerWorker {
|
|
|
876
876
|
raw: null,
|
|
877
877
|
});
|
|
878
878
|
}, timeoutMs);
|
|
879
|
-
timer.unref?.();
|
|
880
879
|
});
|
|
881
880
|
|
|
882
881
|
const result = await Promise.race([resultPromise, timeoutPromise]);
|
|
@@ -952,8 +951,7 @@ export class CodexAppServerWorker {
|
|
|
952
951
|
});
|
|
953
952
|
client.close("closing");
|
|
954
953
|
const deadline = new Promise((resolve) => {
|
|
955
|
-
|
|
956
|
-
t.unref?.();
|
|
954
|
+
setTimeout(resolve, UNSUBSCRIBE_DEADLINE_MS);
|
|
957
955
|
});
|
|
958
956
|
await Promise.race([unsubPromise, deadline]);
|
|
959
957
|
}
|
|
@@ -971,12 +969,12 @@ export class CodexAppServerWorker {
|
|
|
971
969
|
try {
|
|
972
970
|
if (child.exitCode === null && !child.killed) child.kill("SIGTERM");
|
|
973
971
|
} catch {}
|
|
974
|
-
|
|
972
|
+
if (child.exitCode === null && !child.killed) {
|
|
973
|
+
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
975
974
|
try {
|
|
976
975
|
if (child.exitCode === null && !child.killed) child.kill("SIGKILL");
|
|
977
976
|
} catch {}
|
|
978
|
-
}
|
|
979
|
-
killTimer.unref?.();
|
|
977
|
+
}
|
|
980
978
|
}
|
|
981
979
|
}
|
|
982
980
|
|
|
@@ -25,8 +25,7 @@ export function createWorkerError(message, details = {}) {
|
|
|
25
25
|
|
|
26
26
|
function sleep(delayMs) {
|
|
27
27
|
return new Promise((resolve) => {
|
|
28
|
-
|
|
29
|
-
timer.unref?.();
|
|
28
|
+
setTimeout(resolve, Math.max(0, delayMs));
|
|
30
29
|
});
|
|
31
30
|
}
|
|
32
31
|
|
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@ import { SERVERS } from "./mcp-gateway-start.mjs";
|
|
|
21
21
|
|
|
22
22
|
const CODEX_CONFIG = join(homedir(), ".codex", "config.toml");
|
|
23
23
|
const BACKUP_SUFFIX = ".pre-gateway.bak";
|
|
24
|
+
const CODEX_CONFIG_SYNC_OPT_IN = "TFX_CODEX_CONFIG_SYNC";
|
|
24
25
|
|
|
25
26
|
// gateway 서버 → SSE URL 매핑
|
|
26
27
|
const GATEWAY_MAP = new Map(
|
|
@@ -75,9 +76,25 @@ function _buildStdioEntry(name, block) {
|
|
|
75
76
|
return `[mcp_servers.${name}]\n${block}\n`;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
function isProtectedCodexConfigMutationEnv(env = process.env) {
|
|
80
|
+
return env.NODE_ENV === "test" || env.CI === "true" || env.TFX_TEST === "1";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function shouldSkipCodexConfigMutation() {
|
|
84
|
+
return (
|
|
85
|
+
process.env[CODEX_CONFIG_SYNC_OPT_IN] !== "1" &&
|
|
86
|
+
isProtectedCodexConfigMutationEnv()
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
// ── enable: stdio → SSE ──
|
|
79
91
|
|
|
80
92
|
export function enableGateway() {
|
|
93
|
+
if (shouldSkipCodexConfigMutation()) {
|
|
94
|
+
console.log("[SKIP] protected environment; config.toml not modified");
|
|
95
|
+
return { changed: 0, skipped: 0, reason: "protected-env" };
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
if (!existsSync(CODEX_CONFIG)) {
|
|
82
99
|
console.log("[SKIP] ~/.codex/config.toml not found");
|
|
83
100
|
return { changed: 0, skipped: 0 };
|
|
@@ -151,6 +168,11 @@ export function enableGateway() {
|
|
|
151
168
|
// ── disable: SSE → stdio 복원 ──
|
|
152
169
|
|
|
153
170
|
export function disableGateway() {
|
|
171
|
+
if (shouldSkipCodexConfigMutation()) {
|
|
172
|
+
console.log("[SKIP] protected environment; config.toml not restored");
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
154
176
|
const backupPath = CODEX_CONFIG + BACKUP_SUFFIX;
|
|
155
177
|
if (!existsSync(backupPath)) {
|
|
156
178
|
console.log("[SKIP] No backup found — nothing to restore");
|
|
@@ -258,6 +258,28 @@ function generateSummary(stats, sysInfo, hookTimings, traceCount) {
|
|
|
258
258
|
return lines.join("\n");
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
function createZipArchive(bundleDir, zipPath) {
|
|
262
|
+
if (platform() === "win32") {
|
|
263
|
+
execFileSync(
|
|
264
|
+
"powershell.exe",
|
|
265
|
+
[
|
|
266
|
+
"-NoProfile",
|
|
267
|
+
"-NonInteractive",
|
|
268
|
+
"-Command",
|
|
269
|
+
`Compress-Archive -Path '${bundleDir}\\*' -DestinationPath '${zipPath}' -Force`,
|
|
270
|
+
],
|
|
271
|
+
{ timeout: 30000, windowsHide: true },
|
|
272
|
+
);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
execFileSync("zip", ["-qr", zipPath, "."], {
|
|
277
|
+
cwd: bundleDir,
|
|
278
|
+
timeout: 30000,
|
|
279
|
+
windowsHide: true,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
261
283
|
export async function diagnose({ json = false } = {}) {
|
|
262
284
|
mkdirSync(DIAG_DIR, { recursive: true });
|
|
263
285
|
|
|
@@ -319,19 +341,10 @@ export async function diagnose({ json = false } = {}) {
|
|
|
319
341
|
const summary = generateSummary(stats, sysInfo, hookTimings, traces.length);
|
|
320
342
|
writeFileSync(join(bundleDir, "summary.txt"), summary);
|
|
321
343
|
|
|
322
|
-
// 7. zip via
|
|
344
|
+
// 7. zip via platform archive tool
|
|
323
345
|
const zipPath = `${bundleDir}.zip`;
|
|
324
346
|
try {
|
|
325
|
-
|
|
326
|
-
"powershell.exe",
|
|
327
|
-
[
|
|
328
|
-
"-NoProfile",
|
|
329
|
-
"-NonInteractive",
|
|
330
|
-
"-Command",
|
|
331
|
-
`Compress-Archive -Path '${bundleDir}\\*' -DestinationPath '${zipPath}' -Force`,
|
|
332
|
-
],
|
|
333
|
-
{ timeout: 30000, windowsHide: true },
|
|
334
|
-
);
|
|
347
|
+
createZipArchive(bundleDir, zipPath);
|
|
335
348
|
} catch (err) {
|
|
336
349
|
// fallback: leave the directory unzipped
|
|
337
350
|
if (json) {
|
|
@@ -98,6 +98,17 @@ function isCodexConfig(filePath) {
|
|
|
98
98
|
return normalized.endsWith("/.codex/config.toml");
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function isProtectedCodexConfigMutationEnv(env = process.env) {
|
|
102
|
+
return env.NODE_ENV === "test" || env.CI === "true" || env.TFX_TEST === "1";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function shouldSkipCodexConfigMutation() {
|
|
106
|
+
return (
|
|
107
|
+
process.env.TFX_CODEX_CONFIG_SYNC !== "1" &&
|
|
108
|
+
isProtectedCodexConfigMutationEnv()
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
function detectClient(filePath) {
|
|
102
113
|
const normalized = normalizeForMatch(filePath);
|
|
103
114
|
if (normalized.endsWith("/.gemini/settings.json")) return "gemini";
|
|
@@ -448,6 +459,15 @@ function updateJsonConfig(filePath, updates = [], removals = []) {
|
|
|
448
459
|
|
|
449
460
|
function updateCodexConfig(filePath, updates = [], removals = []) {
|
|
450
461
|
const resolvedPath = resolveFilePath(filePath);
|
|
462
|
+
if (shouldSkipCodexConfigMutation()) {
|
|
463
|
+
return {
|
|
464
|
+
modified: false,
|
|
465
|
+
filePath: resolvedPath,
|
|
466
|
+
skipped: true,
|
|
467
|
+
reason: "protected-env",
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
451
471
|
let raw = existsSync(resolvedPath) ? readFileSync(resolvedPath, "utf8") : "";
|
|
452
472
|
|
|
453
473
|
for (const name of removals) {
|
package/scripts/setup.mjs
CHANGED
|
@@ -581,6 +581,15 @@ function ensureHooksInSettings({ settingsPath, registryPath }) {
|
|
|
581
581
|
}
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
function isProtectedCodexConfigMutationEnv(env = process.env) {
|
|
585
|
+
return (
|
|
586
|
+
env.NODE_ENV === "test" ||
|
|
587
|
+
env.CI === "true" ||
|
|
588
|
+
env.TFX_TEST === "1" ||
|
|
589
|
+
Boolean(env.TRIFLUX_TEST_HOME)
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
584
593
|
/**
|
|
585
594
|
* Codex config.json에 tfx-hub MCP 서버 엔트리를 보장한다.
|
|
586
595
|
* @param {{ mcpUrl: string, createIfMissing?: boolean, enabled?: boolean }} opts
|
|
@@ -594,7 +603,19 @@ function ensureCodexHubServerConfig({
|
|
|
594
603
|
}) {
|
|
595
604
|
try {
|
|
596
605
|
const codexConfigDir = join(homedir(), ".codex");
|
|
597
|
-
const
|
|
606
|
+
const hasExplicitConfigFile =
|
|
607
|
+
typeof configFile === "string" && configFile.length > 0;
|
|
608
|
+
const configPath = hasExplicitConfigFile
|
|
609
|
+
? configFile
|
|
610
|
+
: join(codexConfigDir, "config.json");
|
|
611
|
+
|
|
612
|
+
if (
|
|
613
|
+
!hasExplicitConfigFile &&
|
|
614
|
+
process.env.TFX_CODEX_CONFIG_SYNC !== "1" &&
|
|
615
|
+
isProtectedCodexConfigMutationEnv()
|
|
616
|
+
) {
|
|
617
|
+
return { ok: true, changed: false, reason: "protected-env" };
|
|
618
|
+
}
|
|
598
619
|
|
|
599
620
|
if (!existsSync(configPath)) {
|
|
600
621
|
if (!createIfMissing)
|
|
@@ -642,6 +663,13 @@ const REQUIRED_TOP_LEVEL_SETTINGS = [
|
|
|
642
663
|
|
|
643
664
|
function ensureCodexProfiles() {
|
|
644
665
|
try {
|
|
666
|
+
if (
|
|
667
|
+
process.env.TFX_CODEX_CONFIG_SYNC !== "1" &&
|
|
668
|
+
isProtectedCodexConfigMutationEnv()
|
|
669
|
+
) {
|
|
670
|
+
return { ok: true, changed: 0, reason: "protected-env" };
|
|
671
|
+
}
|
|
672
|
+
|
|
645
673
|
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
646
674
|
|
|
647
675
|
const original = existsSync(CODEX_CONFIG_PATH)
|
|
@@ -12,6 +12,7 @@ const CODEX_CONFIG_FILE = [".codex", "config.toml"];
|
|
|
12
12
|
const TFX_HUB_SECTION = "tfx-hub";
|
|
13
13
|
const CODEX_DEFAULT_HUB_URL = "http://127.0.0.1:27888/mcp";
|
|
14
14
|
const FILE_LOCKS = new Map();
|
|
15
|
+
const CODEX_CONFIG_SYNC_OPT_IN = "TFX_CODEX_CONFIG_SYNC";
|
|
15
16
|
|
|
16
17
|
// Windows 에서 process.env.HOME 만 set 하고 USERPROFILE 은 그대로 둔 fixture 환경
|
|
17
18
|
// (e.g. integration test) 에서 production path 로 새는 것을 방지하려면 platform
|
|
@@ -40,6 +41,29 @@ function getCodexConfigPath(codexConfigPath) {
|
|
|
40
41
|
return join(resolveHome(), ...CODEX_CONFIG_FILE);
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function isProtectedCodexConfigEnv(env = process.env) {
|
|
45
|
+
return (
|
|
46
|
+
env.NODE_ENV === "test" ||
|
|
47
|
+
env.CI === "true" ||
|
|
48
|
+
env.TFX_TEST === "1" ||
|
|
49
|
+
Boolean(env.TRIFLUX_TEST_HOME)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shouldSkipImplicitCodexConfigWrite({
|
|
54
|
+
codexConfigPath,
|
|
55
|
+
allowProtectedEnvWrite,
|
|
56
|
+
env = process.env,
|
|
57
|
+
}) {
|
|
58
|
+
if (allowProtectedEnvWrite || env[CODEX_CONFIG_SYNC_OPT_IN] === "1") {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (typeof codexConfigPath === "string" && codexConfigPath.length > 0) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return isProtectedCodexConfigEnv(env);
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
export function getProjectMcpJsonPaths(projectRoot) {
|
|
44
68
|
const root =
|
|
45
69
|
typeof projectRoot === "string" && projectRoot.length > 0
|
|
@@ -533,6 +557,7 @@ export async function syncCodexHubUrl({
|
|
|
533
557
|
codexConfigPath,
|
|
534
558
|
dryRun = false,
|
|
535
559
|
logger = console,
|
|
560
|
+
allowProtectedEnvWrite = false,
|
|
536
561
|
}) {
|
|
537
562
|
const result = {
|
|
538
563
|
updated: [],
|
|
@@ -540,8 +565,20 @@ export async function syncCodexHubUrl({
|
|
|
540
565
|
errors: [],
|
|
541
566
|
};
|
|
542
567
|
|
|
568
|
+
const filePath = getCodexConfigPath(codexConfigPath);
|
|
569
|
+
if (
|
|
570
|
+
shouldSkipImplicitCodexConfigWrite({
|
|
571
|
+
codexConfigPath,
|
|
572
|
+
allowProtectedEnvWrite,
|
|
573
|
+
})
|
|
574
|
+
) {
|
|
575
|
+
log(logger, "info", `[codex-mcp-sync] skipped: ${filePath}`);
|
|
576
|
+
result.skipped.push(filePath);
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
|
|
543
580
|
const outcome = await syncCodexConfigFile({
|
|
544
|
-
filePath
|
|
581
|
+
filePath,
|
|
545
582
|
hubUrl,
|
|
546
583
|
dryRun,
|
|
547
584
|
logger,
|
package/scripts/test-lock.mjs
CHANGED
|
@@ -8,13 +8,107 @@
|
|
|
8
8
|
// 사용: node scripts/test-lock.mjs [-- ...node --test args]
|
|
9
9
|
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
unlinkSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
13
20
|
|
|
14
|
-
const
|
|
21
|
+
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
22
|
+
const LOCK_DIR = join(dirname(SCRIPT_PATH), "..", ".test-lock");
|
|
15
23
|
const LOCK_FILE = join(LOCK_DIR, "pid.lock");
|
|
16
24
|
const STALE_THRESHOLD_MS = 10 * 60 * 1000; // 10분 넘으면 stale
|
|
17
25
|
|
|
26
|
+
function toPosixPath(path) {
|
|
27
|
+
return path.replace(/\\/g, "/");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stripLeadingDotSlash(pattern) {
|
|
31
|
+
return pattern.startsWith("./") ? pattern.slice(2) : pattern;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function globToRegExp(pattern) {
|
|
35
|
+
let source = "^";
|
|
36
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
37
|
+
const char = pattern[i];
|
|
38
|
+
if (char === "*") {
|
|
39
|
+
if (pattern[i + 1] === "*") {
|
|
40
|
+
if (pattern[i + 2] === "/") {
|
|
41
|
+
source += "(?:.*/)?";
|
|
42
|
+
i += 2;
|
|
43
|
+
} else {
|
|
44
|
+
source += ".*";
|
|
45
|
+
i += 1;
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
source += "[^/]*";
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
source += /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
|
|
53
|
+
}
|
|
54
|
+
source += "$";
|
|
55
|
+
return new RegExp(source);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function staticGlobPrefix(pattern) {
|
|
59
|
+
const firstGlob = pattern.indexOf("*");
|
|
60
|
+
if (firstGlob === -1) return pattern;
|
|
61
|
+
const slash = pattern.lastIndexOf("/", firstGlob);
|
|
62
|
+
if (slash === -1) return ".";
|
|
63
|
+
return pattern.slice(0, slash) || ".";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function walkFiles(dir) {
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const files = [];
|
|
75
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
76
|
+
const fullPath = join(dir, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
files.push(...walkFiles(fullPath));
|
|
79
|
+
} else if (entry.isFile()) {
|
|
80
|
+
files.push(fullPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function expandGlobArg(arg, cwd) {
|
|
87
|
+
const normalizedPattern = stripLeadingDotSlash(toPosixPath(arg));
|
|
88
|
+
const matchPattern = isAbsolute(arg)
|
|
89
|
+
? toPosixPath(resolve(arg))
|
|
90
|
+
: normalizedPattern;
|
|
91
|
+
const matcher = globToRegExp(matchPattern);
|
|
92
|
+
const prefix = staticGlobPrefix(normalizedPattern);
|
|
93
|
+
const root = isAbsolute(arg) ? resolve(prefix) : resolve(cwd, prefix);
|
|
94
|
+
|
|
95
|
+
return walkFiles(root)
|
|
96
|
+
.map((file) => {
|
|
97
|
+
if (isAbsolute(arg)) return toPosixPath(resolve(file));
|
|
98
|
+
return toPosixPath(relative(cwd, file));
|
|
99
|
+
})
|
|
100
|
+
.filter((file) => matcher.test(file))
|
|
101
|
+
.sort((a, b) => a.localeCompare(b));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function expandTestArgs(args, { cwd = process.cwd() } = {}) {
|
|
105
|
+
return args.flatMap((arg) => {
|
|
106
|
+
if (arg.startsWith("-") || !arg.includes("*")) return [arg];
|
|
107
|
+
const expanded = expandGlobArg(arg, cwd);
|
|
108
|
+
return expanded.length > 0 ? expanded : [arg];
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
18
112
|
function readLock() {
|
|
19
113
|
try {
|
|
20
114
|
const content = readFileSync(LOCK_FILE, "utf8").trim();
|
|
@@ -56,36 +150,40 @@ function releaseLock() {
|
|
|
56
150
|
}
|
|
57
151
|
}
|
|
58
152
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
process.on("
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
153
|
+
export function main(argv = process.argv.slice(2)) {
|
|
154
|
+
acquireLock();
|
|
155
|
+
|
|
156
|
+
// cleanup on exit
|
|
157
|
+
process.on("exit", releaseLock);
|
|
158
|
+
process.on("SIGINT", () => {
|
|
159
|
+
releaseLock();
|
|
160
|
+
process.exit(130);
|
|
161
|
+
});
|
|
162
|
+
process.on("SIGTERM", () => {
|
|
163
|
+
releaseLock();
|
|
164
|
+
process.exit(143);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// forward args after -- to node --test
|
|
168
|
+
const args = expandTestArgs(argv);
|
|
169
|
+
// stdio split (issue #192 F1): when prepare.mjs spawns this lock with
|
|
170
|
+
// ["ignore","pipe","pipe"], full inherit cascades the parent stdin=ignore
|
|
171
|
+
// to grand-child node --test, breaking ConPTY assumptions on Windows and
|
|
172
|
+
// surfacing as EXIT=1 (false-failed). Pipe stdin only — stdout/stderr stay
|
|
173
|
+
// inherited so the grand-child still streams to whoever attached to us.
|
|
174
|
+
const child = spawn(process.execPath, args, {
|
|
175
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
176
|
+
env: { ...process.env, TEST_LOCK_PID: String(process.pid) },
|
|
177
|
+
});
|
|
178
|
+
// Close stdin immediately so node --test never blocks waiting for input.
|
|
179
|
+
child.stdin?.end();
|
|
180
|
+
|
|
181
|
+
child.on("exit", (code) => {
|
|
182
|
+
releaseLock();
|
|
183
|
+
process.exit(code ?? 1);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (process.argv[1] && resolve(process.argv[1]) === SCRIPT_PATH) {
|
|
188
|
+
main();
|
|
189
|
+
}
|