@triflux/core 10.1.0 → 10.26.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/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +191 -31
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +262 -34
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +1051 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +22 -21
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/schema/delegator-tools.schema.json +1 -1
- package/hub/delegator/service.mjs +93 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +82 -108
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +78 -30
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +23 -17
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +190 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +157 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +210 -52
- package/hud/renderers.mjs +440 -127
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +37 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +49 -20
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +117 -12
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// hub/lib/prompt-tmp.mjs
|
|
2
|
+
//
|
|
3
|
+
// macOS `codex exec` 회귀 fix 보조 helper. codex CLI v0.130.0 의 macOS hook
|
|
4
|
+
// lifecycle (oh-my-codex/codex-native-hook.js) 이 매우 긴 argv inline prompt
|
|
5
|
+
// (4KB+) 를 받으면 SessionStart Failed 로 즉시 exit. PR #243 의 stdin EAGAIN
|
|
6
|
+
// async stream fix 후속 회귀로 추정.
|
|
7
|
+
//
|
|
8
|
+
// 대안: prompt 를 임시 파일에 쓰고 `< /tmp/...` stdin redirect 로 전달.
|
|
9
|
+
// Manual `echo prompt | codex exec ...` (stdin pipe) 가 정상 작동하는 fact 와
|
|
10
|
+
// 정합.
|
|
11
|
+
//
|
|
12
|
+
// cleanup 정책: best-effort. OS `/tmp` 가 자동 cleanup 하고, 이 모듈은
|
|
13
|
+
// 매 호출마다 unique path 사용 (timestamp + pid + counter). orphan 파일은
|
|
14
|
+
// 추후 doctor 가 prune.
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
const PROMPT_TMP_DIR = join(tmpdir(), "triflux-codex-prompt");
|
|
21
|
+
let counter = 0;
|
|
22
|
+
|
|
23
|
+
export function writePromptToTmpFile(prompt) {
|
|
24
|
+
mkdirSync(PROMPT_TMP_DIR, { recursive: true });
|
|
25
|
+
const id = `${Date.now()}-${process.pid}-${counter++}`;
|
|
26
|
+
const file = join(PROMPT_TMP_DIR, `prompt-${id}.txt`);
|
|
27
|
+
writeFileSync(file, String(prompt ?? ""), "utf8");
|
|
28
|
+
return file;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getPromptTmpDir() {
|
|
32
|
+
return PROMPT_TMP_DIR;
|
|
33
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const HIGH_PATTERNS = Object.freeze([
|
|
2
|
+
/^hub\//,
|
|
3
|
+
/^scripts\//,
|
|
4
|
+
/^\.claude\/rules\//,
|
|
5
|
+
/^bin\//,
|
|
6
|
+
/^\.github\//,
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const MEDIUM_PATTERNS = Object.freeze([
|
|
10
|
+
/package\.json$/,
|
|
11
|
+
/\.ya?ml$/,
|
|
12
|
+
/\.toml$/,
|
|
13
|
+
/^config\//,
|
|
14
|
+
/^hooks\//,
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export const LOW_PATTERNS = Object.freeze([/\.md$/, /\.txt$/, /\.test\./]);
|
|
18
|
+
|
|
19
|
+
export function classifyRiskTier({ changedFiles }) {
|
|
20
|
+
const files = Array.isArray(changedFiles)
|
|
21
|
+
? changedFiles.filter((file) => typeof file === "string" && file.length > 0)
|
|
22
|
+
: [];
|
|
23
|
+
|
|
24
|
+
if (files.length === 0) {
|
|
25
|
+
return "low";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (matchesAny(files, HIGH_PATTERNS)) {
|
|
29
|
+
return "high";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (files.length > 1) {
|
|
33
|
+
return "medium";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (matchesAny(files, MEDIUM_PATTERNS)) {
|
|
37
|
+
return "medium";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (matchesPattern(files[0], LOW_PATTERNS)) {
|
|
41
|
+
return "low";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return "medium";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function matchesAny(files, patterns) {
|
|
48
|
+
return files.some((file) => matchesPattern(file, patterns));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function matchesPattern(file, patterns) {
|
|
52
|
+
return patterns.some((pattern) => pattern.test(file));
|
|
53
|
+
}
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { createWriteStream, mkdirSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const LOG_DIR = join(homedir(), ".triflux", "logs");
|
|
8
|
+
const DEDUPE_WINDOW_MS = 5_000;
|
|
9
|
+
const RATE_WINDOW_MS = 1_000;
|
|
10
|
+
// multi-worker headless dispatch 가 1 초 안에 ~25-30+ psmux/tmux 명령을 호출한다.
|
|
11
|
+
// 2-worker dispatchBatch 가 default 30 limit 을 초과해 `rate_limit` throw 로 mac
|
|
12
|
+
// 호환성 회귀 (smoke test 발견, 2026-05-15). 100 으로 상향 — 폭주 안전망은
|
|
13
|
+
// 여전히 (default 100/sec) 유지, env override 로 보수 설정 가능.
|
|
14
|
+
export let MAX_SPAWN_PER_SEC = resolvePositiveInteger(
|
|
15
|
+
process.env.TRIFLUX_MAX_SPAWN_RATE,
|
|
16
|
+
100,
|
|
17
|
+
);
|
|
18
|
+
export const MAX_TOTAL_DESCENDANTS = resolvePositiveInteger(
|
|
19
|
+
process.env.TRIFLUX_MAX_DESCENDANTS,
|
|
20
|
+
50,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
let logDay = "";
|
|
24
|
+
let logStream = null;
|
|
25
|
+
let traceSequence = 0;
|
|
26
|
+
|
|
27
|
+
const recentSpawnTimes = [];
|
|
28
|
+
const dedupeEntries = new Map();
|
|
29
|
+
const activeChildren = new Map();
|
|
30
|
+
|
|
31
|
+
function resolvePositiveInteger(...values) {
|
|
32
|
+
for (const value of values) {
|
|
33
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
34
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function nowIso() {
|
|
43
|
+
return new Date().toISOString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getLogPath(day = nowIso().slice(0, 10)) {
|
|
47
|
+
return join(LOG_DIR, `spawn-trace-${day}.jsonl`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureLogStream() {
|
|
51
|
+
const day = nowIso().slice(0, 10);
|
|
52
|
+
if (logStream && logDay === day) {
|
|
53
|
+
return logStream;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
57
|
+
|
|
58
|
+
if (logStream) {
|
|
59
|
+
try {
|
|
60
|
+
logStream.end();
|
|
61
|
+
} catch {
|
|
62
|
+
/* ignore */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logDay = day;
|
|
67
|
+
logStream = createWriteStream(getLogPath(day), { flags: "a" });
|
|
68
|
+
logStream.on("error", () => {
|
|
69
|
+
/* ignore logging failures */
|
|
70
|
+
});
|
|
71
|
+
return logStream;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function appendTrace(data, { sync = false } = {}) {
|
|
75
|
+
const entry = {
|
|
76
|
+
ts: nowIso(),
|
|
77
|
+
session_id: process.env.TRIFLUX_SESSION_ID ?? null,
|
|
78
|
+
parent_pid: process.pid,
|
|
79
|
+
...data,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
ensureLogStream().write(`${JSON.stringify(entry)}\n`);
|
|
84
|
+
} catch {
|
|
85
|
+
if (!sync) {
|
|
86
|
+
/* ignore logging failures */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function nextTraceId() {
|
|
92
|
+
traceSequence += 1;
|
|
93
|
+
return `spawn-trace-${Date.now()}-${traceSequence}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function trimRecentSpawnTimes(now) {
|
|
97
|
+
while (
|
|
98
|
+
recentSpawnTimes.length > 0 &&
|
|
99
|
+
now - recentSpawnTimes[0] >= RATE_WINDOW_MS
|
|
100
|
+
) {
|
|
101
|
+
recentSpawnTimes.shift();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function trimDedupeEntries(now) {
|
|
106
|
+
for (const [key, ts] of dedupeEntries.entries()) {
|
|
107
|
+
if (now - ts >= DEDUPE_WINDOW_MS) {
|
|
108
|
+
dedupeEntries.delete(key);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stripTraceOptions(options) {
|
|
114
|
+
if (!options || typeof options !== "object") {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { reason: _reason, dedupe: _dedupe, ...rest } = options;
|
|
119
|
+
return rest;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getReason(options) {
|
|
123
|
+
if (!options || typeof options !== "object") {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return options.reason ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getDedupeKey(options) {
|
|
131
|
+
if (!options || typeof options !== "object") {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return typeof options.dedupe === "string" && options.dedupe.trim()
|
|
136
|
+
? options.dedupe.trim()
|
|
137
|
+
: null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getCwd(options) {
|
|
141
|
+
if (!options || typeof options !== "object") {
|
|
142
|
+
return process.cwd();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return options.cwd || process.cwd();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createPolicyError(reasonCode, message, meta = {}) {
|
|
149
|
+
const error = new Error(message);
|
|
150
|
+
error.code = "TRIFLUX_SPAWN_BLOCKED";
|
|
151
|
+
error.reasonCode = reasonCode;
|
|
152
|
+
Object.assign(error, meta);
|
|
153
|
+
return error;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getMaxSpawnPerSec() {
|
|
157
|
+
return MAX_SPAWN_PER_SEC;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function reload() {
|
|
161
|
+
MAX_SPAWN_PER_SEC = resolvePositiveInteger(
|
|
162
|
+
process.env.TRIFLUX_MAX_SPAWN_RATE,
|
|
163
|
+
30,
|
|
164
|
+
);
|
|
165
|
+
return getMaxSpawnPerSec();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function logBlocked(traceId, command, args, options, error, extra = {}) {
|
|
169
|
+
appendTrace({
|
|
170
|
+
event: "blocked",
|
|
171
|
+
trace_id: traceId,
|
|
172
|
+
command,
|
|
173
|
+
args,
|
|
174
|
+
cwd: getCwd(options),
|
|
175
|
+
reason: getReason(options),
|
|
176
|
+
warning: error.message,
|
|
177
|
+
warning_code: error.reasonCode || error.code || "unknown",
|
|
178
|
+
...extra,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function enforceGuards(command, args, options) {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
trimRecentSpawnTimes(now);
|
|
185
|
+
trimDedupeEntries(now);
|
|
186
|
+
const maxSpawnPerSec = getMaxSpawnPerSec();
|
|
187
|
+
|
|
188
|
+
const dedupeKey = getDedupeKey(options);
|
|
189
|
+
if (dedupeKey) {
|
|
190
|
+
const lastSeenAt = dedupeEntries.get(dedupeKey);
|
|
191
|
+
if (lastSeenAt != null && now - lastSeenAt < DEDUPE_WINDOW_MS) {
|
|
192
|
+
return createPolicyError(
|
|
193
|
+
"dedupe",
|
|
194
|
+
`spawn-trace dedupe blocked for key "${dedupeKey}"`,
|
|
195
|
+
{ dedupeKey },
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (recentSpawnTimes.length >= maxSpawnPerSec) {
|
|
201
|
+
return createPolicyError(
|
|
202
|
+
"rate_limit",
|
|
203
|
+
`spawn-trace rate limit exceeded (${maxSpawnPerSec}/sec)`,
|
|
204
|
+
{ maxPerSec: maxSpawnPerSec },
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (activeChildren.size >= MAX_TOTAL_DESCENDANTS) {
|
|
209
|
+
return createPolicyError(
|
|
210
|
+
"max_descendants",
|
|
211
|
+
`spawn-trace max descendants exceeded (${MAX_TOTAL_DESCENDANTS})`,
|
|
212
|
+
{ maxDescendants: MAX_TOTAL_DESCENDANTS },
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
recentSpawnTimes.push(now);
|
|
217
|
+
if (dedupeKey) {
|
|
218
|
+
dedupeEntries.set(dedupeKey, now);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function trackChild(child, meta) {
|
|
225
|
+
if (!child || typeof child.once !== "function") {
|
|
226
|
+
return child;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
activeChildren.set(child, meta);
|
|
230
|
+
|
|
231
|
+
let finalized = false;
|
|
232
|
+
const finalize = (event, payload = {}) => {
|
|
233
|
+
if (finalized) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
finalized = true;
|
|
238
|
+
activeChildren.delete(child);
|
|
239
|
+
|
|
240
|
+
appendTrace({
|
|
241
|
+
event,
|
|
242
|
+
trace_id: meta.traceId,
|
|
243
|
+
command: meta.command,
|
|
244
|
+
args: meta.args,
|
|
245
|
+
cwd: meta.cwd,
|
|
246
|
+
reason: meta.reason,
|
|
247
|
+
child_pid: child.pid ?? null,
|
|
248
|
+
duration_ms: Date.now() - meta.startedAt,
|
|
249
|
+
...payload,
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
child.once("exit", (code, signal) => {
|
|
254
|
+
finalize("exit", {
|
|
255
|
+
exit_code: code,
|
|
256
|
+
signal: signal ?? null,
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
child.once("error", (error) => {
|
|
261
|
+
finalize("error", {
|
|
262
|
+
error: error.message,
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return child;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function createRejectedChild(command, args, error) {
|
|
270
|
+
const child = new EventEmitter();
|
|
271
|
+
child.pid = undefined;
|
|
272
|
+
child.stdin = null;
|
|
273
|
+
child.stdout = null;
|
|
274
|
+
child.stderr = null;
|
|
275
|
+
child.stdio = [null, null, null];
|
|
276
|
+
child.spawnfile = String(command);
|
|
277
|
+
child.spawnargs = [String(command), ...args.map((arg) => String(arg))];
|
|
278
|
+
child.kill = () => false;
|
|
279
|
+
child.killed = false;
|
|
280
|
+
child.connected = false;
|
|
281
|
+
child.exitCode = 1;
|
|
282
|
+
child.signalCode = null;
|
|
283
|
+
|
|
284
|
+
queueMicrotask(() => {
|
|
285
|
+
child.emit("error", error);
|
|
286
|
+
child.emit("exit", 1, null);
|
|
287
|
+
child.emit("close", 1, null);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return child;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function normalizeSpawnArgs(args, options) {
|
|
294
|
+
if (Array.isArray(args)) {
|
|
295
|
+
return {
|
|
296
|
+
argsList: [...args],
|
|
297
|
+
options,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
argsList: [],
|
|
303
|
+
options: args,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function normalizeExecFileArgs(args, options, callback) {
|
|
308
|
+
let argsList = [];
|
|
309
|
+
let normalizedOptions;
|
|
310
|
+
let normalizedCallback;
|
|
311
|
+
|
|
312
|
+
if (typeof args === "function") {
|
|
313
|
+
normalizedCallback = args;
|
|
314
|
+
} else if (Array.isArray(args)) {
|
|
315
|
+
argsList = [...args];
|
|
316
|
+
if (typeof options === "function") {
|
|
317
|
+
normalizedCallback = options;
|
|
318
|
+
} else {
|
|
319
|
+
normalizedOptions = options;
|
|
320
|
+
if (typeof callback === "function") {
|
|
321
|
+
normalizedCallback = callback;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
normalizedOptions = args;
|
|
326
|
+
if (typeof options === "function") {
|
|
327
|
+
normalizedCallback = options;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
argsList,
|
|
333
|
+
options: normalizedOptions,
|
|
334
|
+
callback: normalizedCallback,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function wait(ms) {
|
|
339
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function spawn(command, args, options) {
|
|
343
|
+
const { argsList, options: normalizedOptions } = normalizeSpawnArgs(
|
|
344
|
+
args,
|
|
345
|
+
options,
|
|
346
|
+
);
|
|
347
|
+
const traceId = nextTraceId();
|
|
348
|
+
const blockedError = enforceGuards(command, argsList, normalizedOptions);
|
|
349
|
+
if (blockedError) {
|
|
350
|
+
logBlocked(traceId, command, argsList, normalizedOptions, blockedError);
|
|
351
|
+
throw blockedError;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const startedAt = Date.now();
|
|
355
|
+
const child = childProcess.spawn(
|
|
356
|
+
command,
|
|
357
|
+
argsList,
|
|
358
|
+
stripTraceOptions(normalizedOptions),
|
|
359
|
+
);
|
|
360
|
+
appendTrace({
|
|
361
|
+
event: "spawn",
|
|
362
|
+
trace_id: traceId,
|
|
363
|
+
command,
|
|
364
|
+
args: argsList,
|
|
365
|
+
cwd: getCwd(normalizedOptions),
|
|
366
|
+
reason: getReason(normalizedOptions),
|
|
367
|
+
child_pid: child.pid ?? null,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
return trackChild(child, {
|
|
371
|
+
traceId,
|
|
372
|
+
startedAt,
|
|
373
|
+
command,
|
|
374
|
+
args: argsList,
|
|
375
|
+
cwd: getCwd(normalizedOptions),
|
|
376
|
+
reason: getReason(normalizedOptions),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function spawnWithBackoff(command, args, options, maxRetries = 1) {
|
|
381
|
+
const retryLimit =
|
|
382
|
+
Number.isInteger(maxRetries) && maxRetries >= 0 ? maxRetries : 1;
|
|
383
|
+
let originalRateLimitError = null;
|
|
384
|
+
|
|
385
|
+
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
386
|
+
try {
|
|
387
|
+
return spawn(command, args, options);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (error?.reasonCode !== "rate_limit") {
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
originalRateLimitError ??= error;
|
|
394
|
+
|
|
395
|
+
if (attempt >= retryLimit) {
|
|
396
|
+
throw originalRateLimitError;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
await wait(RATE_WINDOW_MS);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
throw originalRateLimitError;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function execFile(file, args, options, callback) {
|
|
407
|
+
const normalized = normalizeExecFileArgs(args, options, callback);
|
|
408
|
+
const traceId = nextTraceId();
|
|
409
|
+
const blockedError = enforceGuards(
|
|
410
|
+
file,
|
|
411
|
+
normalized.argsList,
|
|
412
|
+
normalized.options,
|
|
413
|
+
);
|
|
414
|
+
if (blockedError) {
|
|
415
|
+
logBlocked(
|
|
416
|
+
traceId,
|
|
417
|
+
file,
|
|
418
|
+
normalized.argsList,
|
|
419
|
+
normalized.options,
|
|
420
|
+
blockedError,
|
|
421
|
+
);
|
|
422
|
+
if (typeof normalized.callback === "function") {
|
|
423
|
+
queueMicrotask(() => normalized.callback(blockedError, "", ""));
|
|
424
|
+
return createRejectedChild(file, normalized.argsList, blockedError);
|
|
425
|
+
}
|
|
426
|
+
throw blockedError;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const startedAt = Date.now();
|
|
430
|
+
const wrappedCallback =
|
|
431
|
+
typeof normalized.callback === "function"
|
|
432
|
+
? (error, stdout, stderr) => normalized.callback(error, stdout, stderr)
|
|
433
|
+
: undefined;
|
|
434
|
+
|
|
435
|
+
const child = childProcess.execFile(
|
|
436
|
+
file,
|
|
437
|
+
normalized.argsList,
|
|
438
|
+
stripTraceOptions(normalized.options),
|
|
439
|
+
wrappedCallback,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
appendTrace({
|
|
443
|
+
event: "spawn",
|
|
444
|
+
trace_id: traceId,
|
|
445
|
+
command: file,
|
|
446
|
+
args: normalized.argsList,
|
|
447
|
+
cwd: getCwd(normalized.options),
|
|
448
|
+
reason: getReason(normalized.options),
|
|
449
|
+
child_pid: child.pid ?? null,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return trackChild(child, {
|
|
453
|
+
traceId,
|
|
454
|
+
startedAt,
|
|
455
|
+
command: file,
|
|
456
|
+
args: normalized.argsList,
|
|
457
|
+
cwd: getCwd(normalized.options),
|
|
458
|
+
reason: getReason(normalized.options),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function execFileSync(file, args, options) {
|
|
463
|
+
const normalized = Array.isArray(args)
|
|
464
|
+
? { argsList: [...args], options }
|
|
465
|
+
: { argsList: [], options: args };
|
|
466
|
+
const traceId = nextTraceId();
|
|
467
|
+
const blockedError = enforceGuards(
|
|
468
|
+
file,
|
|
469
|
+
normalized.argsList,
|
|
470
|
+
normalized.options,
|
|
471
|
+
);
|
|
472
|
+
if (blockedError) {
|
|
473
|
+
logBlocked(
|
|
474
|
+
traceId,
|
|
475
|
+
file,
|
|
476
|
+
normalized.argsList,
|
|
477
|
+
normalized.options,
|
|
478
|
+
blockedError,
|
|
479
|
+
{
|
|
480
|
+
sync: true,
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
throw blockedError;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const startedAt = Date.now();
|
|
487
|
+
appendTrace(
|
|
488
|
+
{
|
|
489
|
+
event: "spawn",
|
|
490
|
+
trace_id: traceId,
|
|
491
|
+
command: file,
|
|
492
|
+
args: normalized.argsList,
|
|
493
|
+
cwd: getCwd(normalized.options),
|
|
494
|
+
reason: getReason(normalized.options),
|
|
495
|
+
sync: true,
|
|
496
|
+
},
|
|
497
|
+
{ sync: true },
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
const result = childProcess.execFileSync(
|
|
502
|
+
file,
|
|
503
|
+
normalized.argsList,
|
|
504
|
+
stripTraceOptions(normalized.options),
|
|
505
|
+
);
|
|
506
|
+
appendTrace(
|
|
507
|
+
{
|
|
508
|
+
event: "exit",
|
|
509
|
+
trace_id: traceId,
|
|
510
|
+
command: file,
|
|
511
|
+
args: normalized.argsList,
|
|
512
|
+
cwd: getCwd(normalized.options),
|
|
513
|
+
reason: getReason(normalized.options),
|
|
514
|
+
exit_code: 0,
|
|
515
|
+
duration_ms: Date.now() - startedAt,
|
|
516
|
+
sync: true,
|
|
517
|
+
},
|
|
518
|
+
{ sync: true },
|
|
519
|
+
);
|
|
520
|
+
return result;
|
|
521
|
+
} catch (error) {
|
|
522
|
+
appendTrace(
|
|
523
|
+
{
|
|
524
|
+
event: "exit",
|
|
525
|
+
trace_id: traceId,
|
|
526
|
+
command: file,
|
|
527
|
+
args: normalized.argsList,
|
|
528
|
+
cwd: getCwd(normalized.options),
|
|
529
|
+
reason: getReason(normalized.options),
|
|
530
|
+
exit_code: error?.status ?? null,
|
|
531
|
+
signal: error?.signal ?? null,
|
|
532
|
+
duration_ms: Date.now() - startedAt,
|
|
533
|
+
error: error?.message,
|
|
534
|
+
sync: true,
|
|
535
|
+
},
|
|
536
|
+
{ sync: true },
|
|
537
|
+
);
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export const ChildProcess = childProcess.ChildProcess;
|
|
543
|
+
export const _forkChild = childProcess._forkChild;
|
|
544
|
+
export const exec = childProcess.exec;
|
|
545
|
+
export const execSync = childProcess.execSync;
|
|
546
|
+
export const fork = childProcess.fork;
|
|
547
|
+
export const spawnSync = childProcess.spawnSync;
|
|
548
|
+
|
|
549
|
+
export default {
|
|
550
|
+
...childProcess,
|
|
551
|
+
spawn,
|
|
552
|
+
spawnWithBackoff,
|
|
553
|
+
execFile,
|
|
554
|
+
execFileSync,
|
|
555
|
+
get MAX_SPAWN_PER_SEC() {
|
|
556
|
+
return MAX_SPAWN_PER_SEC;
|
|
557
|
+
},
|
|
558
|
+
MAX_TOTAL_DESCENDANTS,
|
|
559
|
+
getMaxSpawnPerSec,
|
|
560
|
+
reload,
|
|
561
|
+
};
|