@triflux/core 10.53.0 → 10.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/codex-session-hook.mjs +1 -17
- package/hub/cli-adapter-base.mjs +3 -424
- package/hub/index.mjs +0 -5
- package/hub/lib/hosts-compat.mjs +18 -64
- package/hub/lib/private-tmp.mjs +35 -0
- package/hub/lib/process-utils.mjs +19 -220
- package/hub/lib/prompt-tmp.mjs +6 -24
- package/hub/lib/spawn-trace.mjs +112 -5
- package/hub/lib/timeout-defaults.mjs +34 -6
- package/hub/lib/worker-lifecycle.mjs +3 -0
- package/hub/platform.mjs +3 -1
- package/hub/team/agent-map.json +29 -0
- package/hub/team/intervention.mjs +6 -4
- package/hub/team/nativeProxy.mjs +2 -48
- package/hub/team/pane.mjs +329 -0
- package/hub/team/psmux.mjs +1765 -0
- package/hub/team/retry-state-machine.mjs +1 -1
- package/hub/team/session.mjs +541 -0
- package/hud/constants.mjs +0 -3
- package/hud/providers/claude.mjs +35 -15
- package/hud/providers/codex.mjs +169 -42
- package/hud/utils.mjs +0 -23
- package/package.json +2 -2
- package/scripts/lib/backup-rotation.mjs +60 -0
- package/scripts/lib/codex-profile-config.mjs +20 -7
- package/scripts/lib/codex-recovery.sh +2 -3
- package/scripts/lib/install-blob-history.json +3196 -0
- package/scripts/lib/install-retire.mjs +203 -0
- package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
- package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
- package/scripts/lib/machine-profile.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +1 -42
- package/scripts/lib/mcp-guard-engine.mjs +198 -19
- package/scripts/lib/mcp-manifest.mjs +0 -40
- package/scripts/lib/process-utils.mjs +19 -0
- package/scripts/lib/stable-node.mjs +35 -0
- package/scripts/lib/test-env.mjs +20 -0
- package/scripts/lib/tmr-install.mjs +183 -0
- package/hooks/agy-session-hook.mjs +0 -155
- package/hooks/claude-cwd-projection-refresh.mjs +0 -11
- package/hooks/hook-orchestrator.mjs +0 -11
- package/hooks/lib/resolve-root.mjs +0 -62
- package/hub/codex-adapter.mjs +0 -193
- package/hub/codex-preflight.mjs +0 -219
- package/hub/lib/cto-env.mjs +0 -70
- package/scripts/lib/remote-spawn-transfer.mjs +0 -226
- package/scripts/lib/session-presence.mjs +0 -151
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
import { argv, exit, stdin, stdout } from "node:process";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { writeCodexSessionRecord } from "../hub/lib/codex-session-registry.mjs";
|
|
6
|
-
import {
|
|
7
|
-
emitParticipantSessionStarted,
|
|
8
|
-
shouldSkipInteractiveRegistration,
|
|
9
|
-
} from "../scripts/lib/session-presence.mjs";
|
|
10
6
|
|
|
11
7
|
function parsePayload(stdinData) {
|
|
12
8
|
try {
|
|
@@ -72,23 +68,11 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
|
72
68
|
? normalizeMode(opts.argvMode ?? argv[2], parsed.payload)
|
|
73
69
|
: "";
|
|
74
70
|
const writeSessionRecord = opts.writeSessionRecord || writeCodexSessionRecord;
|
|
75
|
-
const emitSessionStarted =
|
|
76
|
-
opts.emitSessionStarted || emitParticipantSessionStarted;
|
|
77
71
|
|
|
78
72
|
try {
|
|
79
73
|
await runHookSideEffectsWithStdoutSuppressed(async () => {
|
|
80
74
|
if (mode === "register" || mode === "heartbeat") {
|
|
81
|
-
|
|
82
|
-
writeSessionRecord(parsed.payload);
|
|
83
|
-
} catch {}
|
|
84
|
-
}
|
|
85
|
-
if (
|
|
86
|
-
mode === "register" &&
|
|
87
|
-
!shouldSkipInteractiveRegistration(parsed.payload, opts)
|
|
88
|
-
) {
|
|
89
|
-
try {
|
|
90
|
-
await emitSessionStarted(stdinData);
|
|
91
|
-
} catch {}
|
|
75
|
+
writeSessionRecord(parsed.payload);
|
|
92
76
|
}
|
|
93
77
|
});
|
|
94
78
|
} catch {
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -1,427 +1,6 @@
|
|
|
1
|
-
// hub/cli-adapter-base.mjs
|
|
2
|
-
// Phase 2: codex-adapter.mjs에서 추출한 재사용 가능 유틸리티
|
|
3
|
-
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
-
|
|
7
|
-
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
8
|
-
import { writePromptToTmpFile } from "./lib/prompt-tmp.mjs";
|
|
9
|
-
import {
|
|
10
|
-
createActivityLifecycle,
|
|
11
|
-
isActivityLifecycleEnabled,
|
|
12
|
-
resolveHardCeilingMs,
|
|
13
|
-
resolveStallInterventionMs,
|
|
14
|
-
} from "./lib/worker-lifecycle.mjs";
|
|
15
|
-
import { IS_WINDOWS, killProcess } from "./platform.mjs";
|
|
16
|
-
import { createInterventionLadder } from "./team/intervention.mjs";
|
|
17
|
-
|
|
18
|
-
// ── Shell utilities ─────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
export function normalizePathForShell(value) {
|
|
21
|
-
return IS_WINDOWS ? String(value).replace(/\\/g, "/") : String(value);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function shellQuote(value) {
|
|
25
|
-
return JSON.stringify(String(value));
|
|
26
|
-
}
|
|
1
|
+
// hub/cli-adapter-base.mjs: CLI 실행 명령용 셸 유틸리티
|
|
27
2
|
|
|
3
|
+
// PowerShell 은 ‘ ’ ‚ ‛ 도 작은따옴표로 읽는다. 따옴표 문자를 모두 겹쳐 써야 인용이 안 끝난다.
|
|
28
4
|
export function escapePwshSingleQuoted(value) {
|
|
29
|
-
return String(value).replace(/'/g, "
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const CODEX_MCP_TRANSPORT_EXIT_CODE = 70;
|
|
33
|
-
export const CODEX_MCP_EXECUTION_EXIT_CODE = 1;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* long-form 플래그 기반 명령 빌더.
|
|
37
|
-
*
|
|
38
|
-
* macOS 회귀 fix (codex v0.130.0 oh-my-codex hook lifecycle): 매우 긴
|
|
39
|
-
* argv-inline prompt 가 SessionStart Failed 를 유발하므로 default 가 prompt
|
|
40
|
-
* 를 stdin 으로 전달하는 패턴. 셸별 분기:
|
|
41
|
-
* - Unix (bash/zsh): `codex exec ... < '/tmp/triflux-codex-prompt/prompt-*.txt'`
|
|
42
|
-
* - Windows (pwsh7): `Get-Content -Raw 'path' | codex exec ...`
|
|
43
|
-
* (pwsh7 의 `<` 는 reserved future syntax 라 호환성 보장 안 됨)
|
|
44
|
-
*
|
|
45
|
-
* 환경 변수 `TFX_CODEX_STDIN_PROMPT=0` 이면 legacy argv-inline 으로 회귀.
|
|
46
|
-
* 결정론 보장이 필요한 launcher path 는 호출 시 `stdinPrompt: false` 명시.
|
|
47
|
-
*
|
|
48
|
-
* @param {string} prompt
|
|
49
|
-
* @param {string|null} resultFile — null이면 --output-last-message 생략
|
|
50
|
-
* @param {{ profile?: string, codexHome?: string, disallowUltra?: boolean, enforceCanonicalProfile?: boolean, skipGitRepoCheck?: boolean, sandboxBypass?: boolean, cwd?: string, mcpServers?: string[], stdinPrompt?: boolean }} [opts]
|
|
51
|
-
* @returns {string} 실행할 셸 커맨드
|
|
52
|
-
*/
|
|
53
|
-
export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
54
|
-
const {
|
|
55
|
-
profile,
|
|
56
|
-
skipGitRepoCheck = true,
|
|
57
|
-
sandboxBypass = true,
|
|
58
|
-
mcpServers,
|
|
59
|
-
stdinPrompt,
|
|
60
|
-
codexHome,
|
|
61
|
-
disallowUltra,
|
|
62
|
-
enforceCanonicalProfile,
|
|
63
|
-
} = opts;
|
|
64
|
-
|
|
65
|
-
const parts = ["codex", "exec"];
|
|
66
|
-
// Select the effort profile via `-c` config overrides instead of
|
|
67
|
-
// `--profile <name>`. codex 0.134+ rejects `--profile X` whenever config.toml
|
|
68
|
-
// still contains an inline [profiles.X] table (and codex re-injects such
|
|
69
|
-
// tables when it rewrites config.toml), so the `-c model=.. -c
|
|
70
|
-
// model_reasoning_effort=..` form is immune and mutates no config.
|
|
71
|
-
const profileOverrides = profile
|
|
72
|
-
? codexProfileConfigOverrides(profile, {
|
|
73
|
-
codexHome,
|
|
74
|
-
disallowUltra,
|
|
75
|
-
enforceCanonicalProfile,
|
|
76
|
-
})
|
|
77
|
-
: [];
|
|
78
|
-
|
|
79
|
-
if (sandboxBypass) parts.push("--dangerously-bypass-approvals-and-sandbox");
|
|
80
|
-
if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
|
|
81
|
-
if (resultFile) parts.push("--output-last-message", resultFile);
|
|
82
|
-
parts.push("--color", "never");
|
|
83
|
-
for (const override of profileOverrides)
|
|
84
|
-
parts.push("-c", shellQuote(override));
|
|
85
|
-
// `codex exec`는 --cwd를 받지 않아 child process의 cwd로 제어한다.
|
|
86
|
-
if (Array.isArray(mcpServers)) {
|
|
87
|
-
for (const server of mcpServers) {
|
|
88
|
-
parts.push("-c", `mcp_servers.${server}.enabled=true`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const useStdin = resolveStdinPromptMode(stdinPrompt);
|
|
93
|
-
const hasPrompt = typeof prompt === "string" && prompt.length > 0;
|
|
94
|
-
if (useStdin && hasPrompt) {
|
|
95
|
-
const promptFile = writePromptToTmpFile(prompt);
|
|
96
|
-
if (IS_WINDOWS) {
|
|
97
|
-
// pwsh7 호환: `<` redirect 는 reserved future syntax 라 동작 불안정.
|
|
98
|
-
// Get-Content -Raw stdin pipe 로 prompt 를 codex stdin 에 주입.
|
|
99
|
-
return `Get-Content -Raw '${escapePwshSingleQuoted(promptFile)}' | ${parts.join(" ")}`;
|
|
100
|
-
}
|
|
101
|
-
return `${parts.join(" ")} < ${shellQuote(promptFile)}`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
parts.push(JSON.stringify(prompt));
|
|
105
|
-
return parts.join(" ");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function resolveStdinPromptMode(explicit) {
|
|
109
|
-
if (typeof explicit === "boolean") return explicit;
|
|
110
|
-
// Default ON to fix macOS codex hook regression. Opt out via env=0.
|
|
111
|
-
const env = process.env.TFX_CODEX_STDIN_PROMPT;
|
|
112
|
-
if (env === "0" || env === "false") return false;
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── Sleep ───────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
export function sleep(ms) {
|
|
119
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── Result factory ──────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
export function createResult(ok, extra = {}) {
|
|
125
|
-
return {
|
|
126
|
-
ok,
|
|
127
|
-
output: "",
|
|
128
|
-
stderr: "",
|
|
129
|
-
exitCode: null,
|
|
130
|
-
duration: 0,
|
|
131
|
-
retried: false,
|
|
132
|
-
fellBack: false,
|
|
133
|
-
failureMode: ok ? null : "crash",
|
|
134
|
-
...extra,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function appendWarnings(stderr, warnings = []) {
|
|
139
|
-
const text = warnings.map((item) => `[preflight] ${item}`).join("\n");
|
|
140
|
-
return [stderr, text].filter(Boolean).join("\n");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ── Attempt execution ───────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* preflight 뒤 buildAttemptsFn 이 만든 시도를 순서대로 실행한다.
|
|
147
|
-
*
|
|
148
|
-
* @param {object} params
|
|
149
|
-
* @param {(prompt: string, workdir: string, preflight: object, attempt: object) => Promise<object>} params.runFn
|
|
150
|
-
* @param {(opts: object) => Promise<object>} params.preflightFn
|
|
151
|
-
* @param {(opts: object, preflight: object) => object[]} params.buildAttemptsFn
|
|
152
|
-
* @param {object} params.opts — caller-supplied execute options
|
|
153
|
-
* @returns {Promise<object>} createResult-shaped result
|
|
154
|
-
*/
|
|
155
|
-
export async function executeWithAttempts({
|
|
156
|
-
runFn,
|
|
157
|
-
preflightFn,
|
|
158
|
-
buildAttemptsFn,
|
|
159
|
-
opts = {},
|
|
160
|
-
}) {
|
|
161
|
-
const { withRetry } = await import("./workers/worker-utils.mjs");
|
|
162
|
-
|
|
163
|
-
const preflight = await preflightFn(opts);
|
|
164
|
-
if (!preflight.ok) {
|
|
165
|
-
return createResult(false, {
|
|
166
|
-
stderr: appendWarnings("", preflight.warnings),
|
|
167
|
-
fellBack: opts.fallbackToClaude !== false,
|
|
168
|
-
failureMode: "crash",
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const attempts = buildAttemptsFn(opts, preflight);
|
|
173
|
-
let attemptIndex = 0;
|
|
174
|
-
let lastResult = createResult(false);
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
lastResult = await withRetry(
|
|
178
|
-
async () => {
|
|
179
|
-
const result = await runFn(
|
|
180
|
-
opts.prompt || "",
|
|
181
|
-
opts.workdir || process.cwd(),
|
|
182
|
-
preflight,
|
|
183
|
-
attempts[attemptIndex],
|
|
184
|
-
);
|
|
185
|
-
const current = {
|
|
186
|
-
...result,
|
|
187
|
-
stderr: appendWarnings(result.stderr, preflight.warnings),
|
|
188
|
-
retried: attemptIndex > 0,
|
|
189
|
-
};
|
|
190
|
-
const canRetry = !current.ok && attemptIndex < attempts.length - 1;
|
|
191
|
-
attemptIndex += 1;
|
|
192
|
-
if (!canRetry) return current;
|
|
193
|
-
const error = new Error("retry");
|
|
194
|
-
error.retryable = true;
|
|
195
|
-
error.result = current;
|
|
196
|
-
throw error;
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
maxAttempts: attempts.length,
|
|
200
|
-
baseDelayMs: 250,
|
|
201
|
-
maxDelayMs: 750,
|
|
202
|
-
shouldRetry: (error) => error?.retryable === true,
|
|
203
|
-
},
|
|
204
|
-
);
|
|
205
|
-
} catch (error) {
|
|
206
|
-
lastResult =
|
|
207
|
-
error?.result ||
|
|
208
|
-
createResult(false, { stderr: String(error?.message || error) });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (lastResult.ok) return lastResult;
|
|
212
|
-
return {
|
|
213
|
-
...lastResult,
|
|
214
|
-
retried: attempts.length > 1,
|
|
215
|
-
fellBack: opts.fallbackToClaude !== false,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ── Process termination ─────────────────────────────────────────
|
|
220
|
-
|
|
221
|
-
export async function terminateChild(pid, opts = {}) {
|
|
222
|
-
if (!pid) return;
|
|
223
|
-
const graceMs = opts.graceMs ?? 5000;
|
|
224
|
-
killProcess(pid, { signal: "SIGTERM", tree: true, timeout: graceMs });
|
|
225
|
-
await sleep(graceMs);
|
|
226
|
-
killProcess(pid, {
|
|
227
|
-
signal: "SIGKILL",
|
|
228
|
-
tree: true,
|
|
229
|
-
force: true,
|
|
230
|
-
timeout: graceMs,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ── Process execution with stall detection ──────────────────────
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Spawn a CLI process with timeout + stall detection.
|
|
238
|
-
*
|
|
239
|
-
* @param {string} command — shell command to run
|
|
240
|
-
* @param {string} workdir — cwd for the child process
|
|
241
|
-
* @param {number} timeout — legacy wall-clock duration (TFX_ACTIVITY_LIFECYCLE=0 only)
|
|
242
|
-
* @param {object} [opts]
|
|
243
|
-
* @param {string} [opts.resultFile] — file to read output from (if CLI writes there)
|
|
244
|
-
* @param {function} [opts.inferStallMode] — (stdout, stderr) => string. Default: () => 'timeout'
|
|
245
|
-
* @param {number} [opts.stallCheckIntervalMs] — stall check interval (default 10_000)
|
|
246
|
-
* @param {number} [opts.stallThresholdMs] — inactivity threshold
|
|
247
|
-
* @param {number} [opts.hardCeilingMs] — activity-independent maximum duration
|
|
248
|
-
* @param {(context: object) => Promise<boolean|string>|boolean|string} [opts.onStallIntervene]
|
|
249
|
-
* T5 intervention seam; true or "resolved" keeps the process alive
|
|
250
|
-
* @param {object} [opts.deps] — clock/process/timer overrides for deterministic tests
|
|
251
|
-
* @returns {Promise<object>} createResult-shaped object
|
|
252
|
-
*/
|
|
253
|
-
export async function runProcess(command, workdir, timeout, opts = {}) {
|
|
254
|
-
const deps = opts.deps || {};
|
|
255
|
-
const now = deps.now || Date.now;
|
|
256
|
-
const spawnProcess = deps.spawn || spawn;
|
|
257
|
-
const scheduleTimeout = deps.setTimeout || setTimeout;
|
|
258
|
-
const cancelTimeout = deps.clearTimeout || clearTimeout;
|
|
259
|
-
const scheduleInterval = deps.setInterval || setInterval;
|
|
260
|
-
const cancelInterval = deps.clearInterval || clearInterval;
|
|
261
|
-
const terminateProcess = deps.terminateChild || terminateChild;
|
|
262
|
-
const startedAt = now();
|
|
263
|
-
const inferStallMode = opts.inferStallMode || (() => "timeout");
|
|
264
|
-
const stallCheckIntervalMs = opts.stallCheckIntervalMs ?? 10_000;
|
|
265
|
-
const legacyLifecycle = !isActivityLifecycleEnabled();
|
|
266
|
-
const stallThresholdMs =
|
|
267
|
-
opts.stallThresholdMs ??
|
|
268
|
-
(legacyLifecycle ? 30_000 : resolveStallInterventionMs());
|
|
269
|
-
const hardCeilingMs = opts.hardCeilingMs ?? resolveHardCeilingMs();
|
|
270
|
-
const earlyClassifyMs = opts.earlyClassifyMs ?? 30_000;
|
|
271
|
-
const resultFile = opts.resultFile || null;
|
|
272
|
-
|
|
273
|
-
let stdout = "";
|
|
274
|
-
let stderr = "";
|
|
275
|
-
let exitCode = null;
|
|
276
|
-
let failureMode = null;
|
|
277
|
-
let child;
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
child = spawnProcess(command, {
|
|
281
|
-
cwd: workdir,
|
|
282
|
-
shell: true,
|
|
283
|
-
windowsHide: true,
|
|
284
|
-
});
|
|
285
|
-
} catch (error) {
|
|
286
|
-
return createResult(false, {
|
|
287
|
-
stderr: String(error?.message || error),
|
|
288
|
-
duration: now() - startedAt,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const resultFileSignature = () => {
|
|
293
|
-
if (!resultFile) return "";
|
|
294
|
-
try {
|
|
295
|
-
const info = statSync(resultFile);
|
|
296
|
-
return `${info.size}:${info.mtimeMs}`;
|
|
297
|
-
} catch {
|
|
298
|
-
return "";
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
let lastBytes = 0;
|
|
303
|
-
let lastResultFileSignature = resultFileSignature();
|
|
304
|
-
let lastChange = now();
|
|
305
|
-
const activitySignature = () =>
|
|
306
|
-
`${Buffer.byteLength(stdout) + Buffer.byteLength(stderr)}:${resultFileSignature()}`;
|
|
307
|
-
let ladder = null;
|
|
308
|
-
const lifecycle = createActivityLifecycle({
|
|
309
|
-
enabled: !legacyLifecycle,
|
|
310
|
-
interventionMs: stallThresholdMs,
|
|
311
|
-
hardCeilingMs,
|
|
312
|
-
now,
|
|
313
|
-
onIntervene: async (context) => {
|
|
314
|
-
if (typeof opts.onStallIntervene === "function") {
|
|
315
|
-
const result = await opts.onStallIntervene(context);
|
|
316
|
-
return result === true || result === "resolved";
|
|
317
|
-
}
|
|
318
|
-
ladder ??= createInterventionLadder({
|
|
319
|
-
target: {
|
|
320
|
-
pid: child.pid,
|
|
321
|
-
cli: opts.cli,
|
|
322
|
-
codexHome: opts.codexHome,
|
|
323
|
-
rolloutFile: opts.rolloutFile,
|
|
324
|
-
},
|
|
325
|
-
readActivitySignature: activitySignature,
|
|
326
|
-
deps:
|
|
327
|
-
typeof opts.resumeHandler === "function"
|
|
328
|
-
? { resumeHandler: opts.resumeHandler }
|
|
329
|
-
: {},
|
|
330
|
-
});
|
|
331
|
-
const result = await ladder.intervene();
|
|
332
|
-
return result?.outcome === "reactivated" || result?.outcome === "resumed";
|
|
333
|
-
},
|
|
334
|
-
});
|
|
335
|
-
const touch = () => {
|
|
336
|
-
lastChange = now();
|
|
337
|
-
lifecycle.observe(activitySignature());
|
|
338
|
-
};
|
|
339
|
-
child.stdout?.on("data", (chunk) => {
|
|
340
|
-
stdout += String(chunk);
|
|
341
|
-
touch();
|
|
342
|
-
});
|
|
343
|
-
child.stderr?.on("data", (chunk) => {
|
|
344
|
-
stderr += String(chunk);
|
|
345
|
-
touch();
|
|
346
|
-
});
|
|
347
|
-
child.on("error", (error) => {
|
|
348
|
-
stderr += String(error?.message || error);
|
|
349
|
-
failureMode ||= "crash";
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
const stopFor = async (mode) => {
|
|
353
|
-
if (failureMode) return;
|
|
354
|
-
failureMode = mode;
|
|
355
|
-
await terminateProcess(child.pid);
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
const timeoutTimer = legacyLifecycle
|
|
359
|
-
? scheduleTimeout(() => {
|
|
360
|
-
void stopFor("timeout");
|
|
361
|
-
}, timeout)
|
|
362
|
-
: null;
|
|
363
|
-
const stallTimer = scheduleInterval(() => {
|
|
364
|
-
const size = Buffer.byteLength(stdout) + Buffer.byteLength(stderr);
|
|
365
|
-
const currentResultFileSignature = resultFileSignature();
|
|
366
|
-
if (
|
|
367
|
-
size !== lastBytes ||
|
|
368
|
-
currentResultFileSignature !== lastResultFileSignature
|
|
369
|
-
) {
|
|
370
|
-
lastBytes = size;
|
|
371
|
-
lastResultFileSignature = currentResultFileSignature;
|
|
372
|
-
touch();
|
|
373
|
-
}
|
|
374
|
-
if (legacyLifecycle) {
|
|
375
|
-
if (now() - lastChange >= stallThresholdMs)
|
|
376
|
-
void stopFor(inferStallMode(stdout, stderr));
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
const quietMs = now() - lastChange;
|
|
380
|
-
const mode = inferStallMode(stdout, stderr);
|
|
381
|
-
if (
|
|
382
|
-
quietMs >= earlyClassifyMs &&
|
|
383
|
-
(mode === "rate_limited" || mode === "auth_stall")
|
|
384
|
-
) {
|
|
385
|
-
void stopFor(inferStallMode(stdout, stderr));
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
void lifecycle
|
|
389
|
-
.check({
|
|
390
|
-
pid: child.pid,
|
|
391
|
-
command,
|
|
392
|
-
workdir,
|
|
393
|
-
mode,
|
|
394
|
-
stdoutTail: stdout.slice(-2000),
|
|
395
|
-
stderrTail: stderr.slice(-2000),
|
|
396
|
-
})
|
|
397
|
-
.then((reason) => {
|
|
398
|
-
if (!reason) return;
|
|
399
|
-
void stopFor(reason === "hard_ceiling" ? "timeout" : mode);
|
|
400
|
-
});
|
|
401
|
-
}, stallCheckIntervalMs);
|
|
402
|
-
timeoutTimer?.unref?.();
|
|
403
|
-
stallTimer?.unref?.();
|
|
404
|
-
|
|
405
|
-
await new Promise((resolve) =>
|
|
406
|
-
child.on("close", (code) => {
|
|
407
|
-
exitCode = code;
|
|
408
|
-
resolve();
|
|
409
|
-
}),
|
|
410
|
-
);
|
|
411
|
-
if (timeoutTimer) cancelTimeout(timeoutTimer);
|
|
412
|
-
cancelInterval(stallTimer);
|
|
413
|
-
|
|
414
|
-
const fileOutput =
|
|
415
|
-
resultFile && existsSync(resultFile)
|
|
416
|
-
? readFileSync(resultFile, "utf8")
|
|
417
|
-
: "";
|
|
418
|
-
const output = fileOutput || stdout;
|
|
419
|
-
const ok = failureMode == null && exitCode === 0;
|
|
420
|
-
return createResult(ok, {
|
|
421
|
-
output,
|
|
422
|
-
stderr,
|
|
423
|
-
exitCode,
|
|
424
|
-
duration: now() - startedAt,
|
|
425
|
-
failureMode: ok ? null : failureMode || "crash",
|
|
426
|
-
});
|
|
5
|
+
return String(value).replace(/['\u2018\u2019\u201A\u201B]/g, "$&$&");
|
|
427
6
|
}
|
package/hub/index.mjs
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
// @triflux/core — barrel export (auto-generated by pack.mjs)
|
|
2
|
-
// Adapters
|
|
3
|
-
export { buildExecCommand, sleep, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
|
|
4
|
-
export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript } from './codex-adapter.mjs';
|
|
5
|
-
export { getCodexVersion, runPreflight } from './codex-preflight.mjs';
|
|
6
|
-
|
|
7
2
|
// Platform
|
|
8
3
|
export { IS_WINDOWS, IS_MAC, IS_LINUX, TEMP_DIR, normalizePath, whichCommand, killProcess, pipePath } from './platform.mjs';
|
|
9
4
|
|
package/hub/lib/hosts-compat.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
// cwd 를 따르면 다른 저장소의 hosts.json 을 사용자 설정으로 복사하게 된다. 패키지 루트로 고정한다.
|
|
7
|
+
const PACKAGE_ROOT = resolve(
|
|
8
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
9
|
+
"..",
|
|
10
|
+
"..",
|
|
11
|
+
);
|
|
12
|
+
const LEGACY_HOSTS_SEGMENTS = ["references", "hosts.json"];
|
|
10
13
|
|
|
11
14
|
let migrated = false;
|
|
12
15
|
|
|
@@ -27,13 +30,14 @@ export function userStateHostsPath() {
|
|
|
27
30
|
return join(homedir(), ".config", "triflux", "hosts.json");
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
function legacyHostsPath(repoRoot) {
|
|
34
|
+
return join(repoRoot || PACKAGE_ROOT, ...LEGACY_HOSTS_SEGMENTS);
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
function candidatePaths(repoRoot) {
|
|
31
|
-
const
|
|
32
|
-
const repoRootCandidates = HOSTS_LOCATIONS.map((segments) =>
|
|
33
|
-
join(root, ...segments),
|
|
34
|
-
);
|
|
38
|
+
const legacyPath = legacyHostsPath(repoRoot);
|
|
35
39
|
const userPath = userStateHostsPath();
|
|
36
|
-
return userPath ? [userPath,
|
|
40
|
+
return userPath ? [userPath, legacyPath] : [legacyPath];
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export function migrateLegacyHosts(repoRoot) {
|
|
@@ -52,14 +56,11 @@ export function migrateLegacyHosts(repoRoot) {
|
|
|
52
56
|
return { migrated: false, from: null, to, reason: "already-exists" };
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
HOSTS_LOCATIONS.map((segments) => join(root, ...segments)).find((path) =>
|
|
58
|
-
existsSync(path),
|
|
59
|
-
) || null;
|
|
60
|
-
if (!from) {
|
|
59
|
+
const legacyPath = legacyHostsPath(repoRoot);
|
|
60
|
+
if (!existsSync(legacyPath)) {
|
|
61
61
|
return { migrated: false, from: null, to, reason: "not-found" };
|
|
62
62
|
}
|
|
63
|
+
from = legacyPath;
|
|
63
64
|
|
|
64
65
|
mkdirSync(dirname(to), { recursive: true });
|
|
65
66
|
copyFileSync(from, to);
|
|
@@ -270,50 +271,3 @@ export function resolveHost(nameOrAlias, repoRoot) {
|
|
|
270
271
|
export function readHost(nameOrAlias, repoRoot) {
|
|
271
272
|
return resolveHost(nameOrAlias, repoRoot)?.host ?? null;
|
|
272
273
|
}
|
|
273
|
-
|
|
274
|
-
export function selfTestFixtures() {
|
|
275
|
-
const v1 = normalizeHost(
|
|
276
|
-
{
|
|
277
|
-
description: "legacy",
|
|
278
|
-
aliases: ["desk"],
|
|
279
|
-
default_dir: "~/Desktop/Projects",
|
|
280
|
-
os: "win32",
|
|
281
|
-
ssh_user: "SSAFY",
|
|
282
|
-
tailscale: { ip: "100.64.0.1", dns: "desk.ts.net" },
|
|
283
|
-
capabilities: ["codex", "claude"],
|
|
284
|
-
},
|
|
285
|
-
"ultra4",
|
|
286
|
-
);
|
|
287
|
-
const v2 = normalizeHost(
|
|
288
|
-
{
|
|
289
|
-
description: "modern",
|
|
290
|
-
aliases: ["mac"],
|
|
291
|
-
default_dir: "~/projects",
|
|
292
|
-
os: "darwin kernel",
|
|
293
|
-
ssh: { user: "tellang" },
|
|
294
|
-
tailscale: {
|
|
295
|
-
ip: "100.64.0.2",
|
|
296
|
-
dns: "mac.ts.net",
|
|
297
|
-
ssh_mode: "ssh-over-vpn",
|
|
298
|
-
},
|
|
299
|
-
capabilities_v2: { codex: true, claude: true, high_memory: true },
|
|
300
|
-
last_probe: { ok: true, ts: "2026-04-18T12:34:56Z", latency_ms: 143 },
|
|
301
|
-
},
|
|
302
|
-
"m2",
|
|
303
|
-
);
|
|
304
|
-
return {
|
|
305
|
-
v1,
|
|
306
|
-
v2,
|
|
307
|
-
checks: {
|
|
308
|
-
v1_os: v1.os === "windows",
|
|
309
|
-
v1_ssh_user: v1.ssh.user === "SSAFY" && v1.ssh_user === "SSAFY",
|
|
310
|
-
v2_os: v2.os === "darwin",
|
|
311
|
-
v2_caps: v2.capabilities.includes("high-memory"),
|
|
312
|
-
v2_probe: v2.last_probe?.ok === true && v2.last_probe?.latency_ms === 143,
|
|
313
|
-
},
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (process.argv.includes("--self-test")) {
|
|
318
|
-
process.stdout.write(`${JSON.stringify(selfTestFixtures(), null, 2)}\n`);
|
|
319
|
-
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { chmodSync, lstatSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// 공용 /tmp 의 고정 이름은 다른 사용자가 먼저 만들거나 읽을 수 있다.
|
|
6
|
+
// uid 를 넣은 0700 루트를 쓰고, 남의 것이거나 링크면 쓰지 않는다.
|
|
7
|
+
// Windows 는 TEMP 가 이미 사용자별이라 그대로 쓴다.
|
|
8
|
+
// scripts/tfx-route.sh 의 resolve_tmp_dir 가 같은 규칙을 셸로 따른다.
|
|
9
|
+
export function privateTmpRoot({
|
|
10
|
+
base = tmpdir(),
|
|
11
|
+
uid = process.getuid?.(),
|
|
12
|
+
} = {}) {
|
|
13
|
+
if (uid === undefined) return base;
|
|
14
|
+
return ensureOwnedDir(join(base, `triflux-${uid}`), uid);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 루트가 예전에 느슨했다면 그 사이 남이 하위에 링크를 심었을 수 있어 하위도 같은 검사를 한다.
|
|
18
|
+
export function privateTmpDir(name, { base, uid = process.getuid?.() } = {}) {
|
|
19
|
+
const dir = join(privateTmpRoot({ base, uid }), name);
|
|
20
|
+
if (uid === undefined) {
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
return ensureOwnedDir(dir, uid);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ensureOwnedDir(dir, uid) {
|
|
28
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
29
|
+
const st = lstatSync(dir);
|
|
30
|
+
if (!st.isDirectory() || st.uid !== uid) {
|
|
31
|
+
throw new Error(`unsafe temp dir (not owned by uid ${uid}): ${dir}`);
|
|
32
|
+
}
|
|
33
|
+
if ((st.mode & 0o777) !== 0o700) chmodSync(dir, 0o700);
|
|
34
|
+
return dir;
|
|
35
|
+
}
|