@hua-labs/tap 0.2.5 → 0.3.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/LICENSE +21 -0
- package/README.md +280 -194
- package/dist/bridges/codex-app-server-bridge.d.mts +9 -1
- package/dist/bridges/codex-app-server-bridge.mjs +182 -6
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +8 -16
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/bridges/gemini-ide-companion-runner.d.mts +1 -0
- package/dist/bridges/gemini-ide-companion-runner.mjs +22501 -0
- package/dist/bridges/gemini-ide-companion-runner.mjs.map +1 -0
- package/dist/cli.mjs +1938 -989
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +76 -22
- package/dist/index.mjs +26727 -3768
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +331 -205
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +65 -65
- /package/bin/{tap-comms.mjs → tap.mjs} +0 -0
package/dist/cli.mjs
CHANGED
|
@@ -979,11 +979,42 @@ function probeCommand(candidates) {
|
|
|
979
979
|
});
|
|
980
980
|
if (result.status === 0) {
|
|
981
981
|
const version2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || null;
|
|
982
|
-
|
|
982
|
+
const absolutePath = resolveCommandPath(candidate);
|
|
983
|
+
return { command: absolutePath ?? candidate, version: version2 };
|
|
983
984
|
}
|
|
984
985
|
}
|
|
985
986
|
return { command: null, version: null };
|
|
986
987
|
}
|
|
988
|
+
function resolveCommandPath(command) {
|
|
989
|
+
if (path7.isAbsolute(command)) return command;
|
|
990
|
+
const whichCmd = process.platform === "win32" ? "where.exe" : "which";
|
|
991
|
+
try {
|
|
992
|
+
const result = spawnSync2(whichCmd, [command], {
|
|
993
|
+
encoding: "utf-8",
|
|
994
|
+
windowsHide: true
|
|
995
|
+
});
|
|
996
|
+
if (result.status !== 0) return null;
|
|
997
|
+
const lines = result.stdout.trim().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
998
|
+
if (lines.length === 0) return null;
|
|
999
|
+
if (process.platform === "win32") {
|
|
1000
|
+
const candidateExt = path7.extname(command).toLowerCase();
|
|
1001
|
+
if (candidateExt) {
|
|
1002
|
+
const extMatch = lines.find(
|
|
1003
|
+
(l) => path7.extname(l).toLowerCase() === candidateExt && fs7.existsSync(l)
|
|
1004
|
+
);
|
|
1005
|
+
if (extMatch) return extMatch;
|
|
1006
|
+
}
|
|
1007
|
+
const executableMatch = lines.find(
|
|
1008
|
+
(l) => /\.(cmd|exe|ps1)$/i.test(l) && fs7.existsSync(l)
|
|
1009
|
+
);
|
|
1010
|
+
if (executableMatch) return executableMatch;
|
|
1011
|
+
}
|
|
1012
|
+
const firstValid = lines.find((l) => fs7.existsSync(l));
|
|
1013
|
+
return firstValid ?? null;
|
|
1014
|
+
} catch {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
987
1018
|
function getHomeDir() {
|
|
988
1019
|
return os2.homedir();
|
|
989
1020
|
}
|
|
@@ -2020,192 +2051,16 @@ function getAdapter(runtime) {
|
|
|
2020
2051
|
return adapter;
|
|
2021
2052
|
}
|
|
2022
2053
|
|
|
2023
|
-
// src/engine/bridge.ts
|
|
2024
|
-
import * as fs13 from "fs";
|
|
2025
|
-
import * as net from "net";
|
|
2026
|
-
import * as os3 from "os";
|
|
2027
|
-
import * as path13 from "path";
|
|
2028
|
-
import { randomBytes } from "crypto";
|
|
2029
|
-
import { spawn, spawnSync as spawnSync3, execSync as execSync3 } from "child_process";
|
|
2030
|
-
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2031
|
-
|
|
2032
|
-
// src/runtime/resolve-node.ts
|
|
2033
|
-
import * as fs12 from "fs";
|
|
2054
|
+
// src/engine/bridge-paths.ts
|
|
2034
2055
|
import * as path12 from "path";
|
|
2035
|
-
import { execSync as execSync2 } from "child_process";
|
|
2036
|
-
function readNodeVersion(repoRoot) {
|
|
2037
|
-
const nvFile = path12.join(repoRoot, ".node-version");
|
|
2038
|
-
if (!fs12.existsSync(nvFile)) return null;
|
|
2039
|
-
try {
|
|
2040
|
-
const raw = fs12.readFileSync(nvFile, "utf-8").trim();
|
|
2041
|
-
return raw.length > 0 ? raw.replace(/^v/, "") : null;
|
|
2042
|
-
} catch {
|
|
2043
|
-
return null;
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
function fnmCandidateDirs() {
|
|
2047
|
-
if (process.platform === "win32") {
|
|
2048
|
-
return [
|
|
2049
|
-
process.env.FNM_DIR,
|
|
2050
|
-
process.env.APPDATA ? path12.join(process.env.APPDATA, "fnm") : null,
|
|
2051
|
-
process.env.LOCALAPPDATA ? path12.join(process.env.LOCALAPPDATA, "fnm") : null,
|
|
2052
|
-
process.env.USERPROFILE ? path12.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
|
|
2053
|
-
].filter(Boolean);
|
|
2054
|
-
}
|
|
2055
|
-
return [
|
|
2056
|
-
process.env.FNM_DIR,
|
|
2057
|
-
process.env.HOME ? path12.join(process.env.HOME, ".local", "share", "fnm") : null,
|
|
2058
|
-
process.env.HOME ? path12.join(process.env.HOME, ".fnm") : null,
|
|
2059
|
-
process.env.XDG_DATA_HOME ? path12.join(process.env.XDG_DATA_HOME, "fnm") : null
|
|
2060
|
-
].filter(Boolean);
|
|
2061
|
-
}
|
|
2062
|
-
function nodeExecutableName() {
|
|
2063
|
-
return process.platform === "win32" ? "node.exe" : "node";
|
|
2064
|
-
}
|
|
2065
|
-
function probeFnmNode(desiredVersion) {
|
|
2066
|
-
const dirs = fnmCandidateDirs();
|
|
2067
|
-
const exe = nodeExecutableName();
|
|
2068
|
-
for (const baseDir of dirs) {
|
|
2069
|
-
const candidate = path12.join(
|
|
2070
|
-
baseDir,
|
|
2071
|
-
"node-versions",
|
|
2072
|
-
`v${desiredVersion}`,
|
|
2073
|
-
"installation",
|
|
2074
|
-
exe
|
|
2075
|
-
);
|
|
2076
|
-
if (!fs12.existsSync(candidate)) continue;
|
|
2077
|
-
try {
|
|
2078
|
-
const v = execSync2(`"${candidate}" --version`, {
|
|
2079
|
-
encoding: "utf-8",
|
|
2080
|
-
timeout: 5e3
|
|
2081
|
-
}).trim();
|
|
2082
|
-
if (v.startsWith(`v${desiredVersion.split(".")[0]}.`)) {
|
|
2083
|
-
return candidate;
|
|
2084
|
-
}
|
|
2085
|
-
} catch {
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return null;
|
|
2089
|
-
}
|
|
2090
|
-
function detectNodeMajorVersion(command) {
|
|
2091
|
-
try {
|
|
2092
|
-
const version2 = execSync2(`"${command}" --version`, {
|
|
2093
|
-
encoding: "utf-8",
|
|
2094
|
-
timeout: 5e3
|
|
2095
|
-
}).trim();
|
|
2096
|
-
const match = version2.match(/^v?(\d+)\./);
|
|
2097
|
-
return match ? parseInt(match[1], 10) : null;
|
|
2098
|
-
} catch {
|
|
2099
|
-
return null;
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
function checkStripTypesSupport(command) {
|
|
2103
|
-
const major = detectNodeMajorVersion(command);
|
|
2104
|
-
if (major !== null && major >= 22) return true;
|
|
2105
|
-
try {
|
|
2106
|
-
execSync2(`"${command}" --experimental-strip-types -e ""`, {
|
|
2107
|
-
timeout: 5e3,
|
|
2108
|
-
stdio: "pipe"
|
|
2109
|
-
});
|
|
2110
|
-
return true;
|
|
2111
|
-
} catch {
|
|
2112
|
-
return false;
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
function findTsxFallback(repoRoot) {
|
|
2116
|
-
const candidates = [
|
|
2117
|
-
path12.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
|
|
2118
|
-
path12.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
|
|
2119
|
-
path12.join(repoRoot, "node_modules", ".bin", "tsx")
|
|
2120
|
-
];
|
|
2121
|
-
for (const c of candidates) {
|
|
2122
|
-
if (fs12.existsSync(c)) return c;
|
|
2123
|
-
}
|
|
2124
|
-
return null;
|
|
2125
|
-
}
|
|
2126
|
-
function getFnmBinDir(repoRoot) {
|
|
2127
|
-
const desiredVersion = readNodeVersion(repoRoot);
|
|
2128
|
-
if (!desiredVersion) return null;
|
|
2129
|
-
const nodePath = probeFnmNode(desiredVersion);
|
|
2130
|
-
if (!nodePath) return null;
|
|
2131
|
-
return path12.dirname(nodePath);
|
|
2132
|
-
}
|
|
2133
|
-
function resolveNodeRuntime(configCommand, repoRoot) {
|
|
2134
|
-
if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
|
|
2135
|
-
return {
|
|
2136
|
-
command: configCommand,
|
|
2137
|
-
supportsStripTypes: false,
|
|
2138
|
-
source: "bun",
|
|
2139
|
-
majorVersion: null
|
|
2140
|
-
};
|
|
2141
|
-
}
|
|
2142
|
-
const desiredVersion = readNodeVersion(repoRoot);
|
|
2143
|
-
if (desiredVersion) {
|
|
2144
|
-
const fnmNode = probeFnmNode(desiredVersion);
|
|
2145
|
-
if (fnmNode) {
|
|
2146
|
-
const major2 = detectNodeMajorVersion(fnmNode);
|
|
2147
|
-
return {
|
|
2148
|
-
command: fnmNode,
|
|
2149
|
-
supportsStripTypes: checkStripTypesSupport(fnmNode),
|
|
2150
|
-
source: "fnm",
|
|
2151
|
-
majorVersion: major2
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
const major = detectNodeMajorVersion(configCommand);
|
|
2156
|
-
if (major !== null) {
|
|
2157
|
-
return {
|
|
2158
|
-
command: configCommand,
|
|
2159
|
-
supportsStripTypes: checkStripTypesSupport(configCommand),
|
|
2160
|
-
source: major === detectNodeMajorVersion("node") ? "path" : "config",
|
|
2161
|
-
majorVersion: major
|
|
2162
|
-
};
|
|
2163
|
-
}
|
|
2164
|
-
const tsx = findTsxFallback(repoRoot);
|
|
2165
|
-
if (tsx) {
|
|
2166
|
-
return {
|
|
2167
|
-
command: tsx,
|
|
2168
|
-
supportsStripTypes: false,
|
|
2169
|
-
source: "tsx-fallback",
|
|
2170
|
-
majorVersion: null
|
|
2171
|
-
};
|
|
2172
|
-
}
|
|
2173
|
-
return {
|
|
2174
|
-
command: configCommand,
|
|
2175
|
-
supportsStripTypes: false,
|
|
2176
|
-
source: "path",
|
|
2177
|
-
majorVersion: null
|
|
2178
|
-
};
|
|
2179
|
-
}
|
|
2180
|
-
function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
|
|
2181
|
-
const fnmBin = getFnmBinDir(repoRoot);
|
|
2182
|
-
if (!fnmBin) return { ...baseEnv };
|
|
2183
|
-
const pathKey = process.platform === "win32" ? "Path" : "PATH";
|
|
2184
|
-
const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
|
|
2185
|
-
return {
|
|
2186
|
-
...baseEnv,
|
|
2187
|
-
[pathKey]: `${fnmBin}${path12.delimiter}${currentPath}`
|
|
2188
|
-
};
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
// src/engine/bridge.ts
|
|
2192
|
-
var DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
|
|
2193
|
-
var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
2194
|
-
var APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
2195
|
-
var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
|
|
2196
|
-
var APP_SERVER_HEALTH_RETRY_MS = 250;
|
|
2197
|
-
var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
|
|
2198
|
-
var APP_SERVER_AUTH_FILE_MODE = 384;
|
|
2199
|
-
var WINDOWS_SPAWN_WRAPPER_PREFIX = "tap-spawn-";
|
|
2200
|
-
var WINDOWS_SPAWN_WRAPPER_STALE_MS = 60 * 60 * 1e3;
|
|
2201
2056
|
function appServerLogFilePath(stateDir, instanceId) {
|
|
2202
|
-
return
|
|
2057
|
+
return path12.join(stateDir, "logs", `app-server-${instanceId}.log`);
|
|
2203
2058
|
}
|
|
2204
2059
|
function appServerGatewayLogFilePath(stateDir, instanceId) {
|
|
2205
|
-
return
|
|
2060
|
+
return path12.join(stateDir, "logs", `app-server-gateway-${instanceId}.log`);
|
|
2206
2061
|
}
|
|
2207
2062
|
function appServerGatewayTokenFilePath(stateDir, instanceId) {
|
|
2208
|
-
return
|
|
2063
|
+
return path12.join(
|
|
2209
2064
|
stateDir,
|
|
2210
2065
|
"secrets",
|
|
2211
2066
|
`app-server-gateway-${instanceId}.token`
|
|
@@ -2214,23 +2069,40 @@ function appServerGatewayTokenFilePath(stateDir, instanceId) {
|
|
|
2214
2069
|
function stderrLogFilePath(logPath) {
|
|
2215
2070
|
return `${logPath}.stderr`;
|
|
2216
2071
|
}
|
|
2072
|
+
function pidFilePath(stateDir, instanceId) {
|
|
2073
|
+
return path12.join(stateDir, "pids", `bridge-${instanceId}.json`);
|
|
2074
|
+
}
|
|
2075
|
+
function logFilePath(stateDir, instanceId) {
|
|
2076
|
+
return path12.join(stateDir, "logs", `bridge-${instanceId}.log`);
|
|
2077
|
+
}
|
|
2078
|
+
function runtimeHeartbeatFilePath(runtimeStateDir) {
|
|
2079
|
+
return path12.join(runtimeStateDir, "heartbeat.json");
|
|
2080
|
+
}
|
|
2081
|
+
function runtimeThreadStateFilePath(runtimeStateDir) {
|
|
2082
|
+
return path12.join(runtimeStateDir, "thread.json");
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// src/engine/bridge-file-io.ts
|
|
2086
|
+
import * as fs12 from "fs";
|
|
2087
|
+
import * as path13 from "path";
|
|
2088
|
+
var APP_SERVER_AUTH_FILE_MODE = 384;
|
|
2217
2089
|
function writeProtectedTextFile(filePath, content) {
|
|
2218
|
-
|
|
2090
|
+
fs12.mkdirSync(path13.dirname(filePath), { recursive: true });
|
|
2219
2091
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
2220
|
-
|
|
2092
|
+
fs12.writeFileSync(tmp, content, {
|
|
2221
2093
|
encoding: "utf-8",
|
|
2222
2094
|
mode: APP_SERVER_AUTH_FILE_MODE
|
|
2223
2095
|
});
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2096
|
+
fs12.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
|
|
2097
|
+
fs12.renameSync(tmp, filePath);
|
|
2098
|
+
fs12.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
|
|
2227
2099
|
}
|
|
2228
2100
|
function removeFileIfExists(filePath) {
|
|
2229
|
-
if (!filePath || !
|
|
2101
|
+
if (!filePath || !fs12.existsSync(filePath)) {
|
|
2230
2102
|
return;
|
|
2231
2103
|
}
|
|
2232
2104
|
try {
|
|
2233
|
-
|
|
2105
|
+
fs12.unlinkSync(filePath);
|
|
2234
2106
|
} catch {
|
|
2235
2107
|
}
|
|
2236
2108
|
}
|
|
@@ -2240,83 +2112,155 @@ function toPowerShellSingleQuotedString(value) {
|
|
|
2240
2112
|
function toPowerShellStringArrayLiteral(values) {
|
|
2241
2113
|
return `@(${values.map(toPowerShellSingleQuotedString).join(", ")})`;
|
|
2242
2114
|
}
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
} catch {
|
|
2248
|
-
return;
|
|
2249
|
-
}
|
|
2250
|
-
for (const entry of entries) {
|
|
2251
|
-
if (!entry.startsWith(WINDOWS_SPAWN_WRAPPER_PREFIX) || !/\.(cmd|ps1)$/i.test(entry)) {
|
|
2252
|
-
continue;
|
|
2253
|
-
}
|
|
2254
|
-
const wrapperPath = path13.join(os3.tmpdir(), entry);
|
|
2255
|
-
try {
|
|
2256
|
-
const stats = fs13.statSync(wrapperPath);
|
|
2257
|
-
if (now - stats.mtimeMs < WINDOWS_SPAWN_WRAPPER_STALE_MS) {
|
|
2258
|
-
continue;
|
|
2259
|
-
}
|
|
2260
|
-
fs13.unlinkSync(wrapperPath);
|
|
2261
|
-
} catch {
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
function buildWindowsDetachedWrapperScript(command, args, logPath, stderrLogPath, env) {
|
|
2266
|
-
const lines = ["$ErrorActionPreference = 'Stop'"];
|
|
2267
|
-
for (const [key, value] of Object.entries(env)) {
|
|
2268
|
-
if (value !== void 0 && value !== process.env[key]) {
|
|
2269
|
-
lines.push(
|
|
2270
|
-
`[Environment]::SetEnvironmentVariable(${toPowerShellSingleQuotedString(key)}, ${toPowerShellSingleQuotedString(value)}, 'Process')`
|
|
2271
|
-
);
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
lines.push(
|
|
2275
|
-
`$logPath = ${toPowerShellSingleQuotedString(logPath)}`,
|
|
2276
|
-
`$stderrLogPath = ${toPowerShellSingleQuotedString(stderrLogPath)}`,
|
|
2277
|
-
`$commandPath = ${toPowerShellSingleQuotedString(command)}`,
|
|
2278
|
-
`$commandArgs = ${toPowerShellStringArrayLiteral(args)}`,
|
|
2279
|
-
"$exitCode = 1",
|
|
2280
|
-
"try {",
|
|
2281
|
-
" & $commandPath @commandArgs >> $logPath 2>> $stderrLogPath",
|
|
2282
|
-
" $exitCode = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 0 }",
|
|
2283
|
-
"} finally {",
|
|
2284
|
-
" Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue",
|
|
2285
|
-
"}",
|
|
2286
|
-
"exit $exitCode"
|
|
2287
|
-
);
|
|
2288
|
-
return `${lines.join("\r\n")}\r
|
|
2289
|
-
`;
|
|
2290
|
-
}
|
|
2115
|
+
|
|
2116
|
+
// src/engine/bridge-port-network.ts
|
|
2117
|
+
import * as net from "net";
|
|
2118
|
+
var DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
|
|
2291
2119
|
function getWebSocketCtor() {
|
|
2292
2120
|
const candidate = globalThis.WebSocket;
|
|
2293
2121
|
return typeof candidate === "function" ? candidate : null;
|
|
2294
2122
|
}
|
|
2295
2123
|
function delay(ms) {
|
|
2296
|
-
return new Promise((
|
|
2124
|
+
return new Promise((resolve14) => setTimeout(resolve14, ms));
|
|
2297
2125
|
}
|
|
2298
2126
|
function isLoopbackHost(hostname) {
|
|
2299
2127
|
return hostname === "127.0.0.1" || hostname === "localhost";
|
|
2300
2128
|
}
|
|
2301
|
-
function
|
|
2302
|
-
const
|
|
2303
|
-
return
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2129
|
+
async function allocateLoopbackPort(hostname) {
|
|
2130
|
+
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2131
|
+
return await new Promise((resolve14, reject) => {
|
|
2132
|
+
const server = net.createServer();
|
|
2133
|
+
server.unref();
|
|
2134
|
+
server.once("error", reject);
|
|
2135
|
+
server.listen(0, bindHost, () => {
|
|
2136
|
+
const address = server.address();
|
|
2137
|
+
if (!address || typeof address === "string") {
|
|
2138
|
+
server.close(() => {
|
|
2139
|
+
reject(new Error("Failed to allocate a loopback port"));
|
|
2140
|
+
});
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
const port = address.port;
|
|
2144
|
+
server.close((error) => {
|
|
2145
|
+
if (error) {
|
|
2146
|
+
reject(error);
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
resolve14(port);
|
|
2150
|
+
});
|
|
2151
|
+
});
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
async function isTcpPortAvailable(hostname, port) {
|
|
2155
|
+
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2156
|
+
return await new Promise((resolve14) => {
|
|
2157
|
+
const server = net.createServer();
|
|
2158
|
+
server.unref();
|
|
2159
|
+
server.once("error", () => resolve14(false));
|
|
2160
|
+
server.listen(port, bindHost, () => {
|
|
2161
|
+
server.close((error) => resolve14(!error));
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
async function waitForPortRelease(url, timeoutMs = 1e4, intervalMs = 500) {
|
|
2166
|
+
let hostname;
|
|
2167
|
+
let port;
|
|
2168
|
+
try {
|
|
2169
|
+
const parsed = new URL(url);
|
|
2170
|
+
hostname = parsed.hostname;
|
|
2171
|
+
port = parseInt(parsed.port, 10);
|
|
2172
|
+
} catch {
|
|
2173
|
+
return true;
|
|
2174
|
+
}
|
|
2175
|
+
if (!port || !Number.isFinite(port)) return true;
|
|
2176
|
+
const deadline = Date.now() + timeoutMs;
|
|
2177
|
+
while (Date.now() < deadline) {
|
|
2178
|
+
if (await isTcpPortAvailable(hostname, port)) {
|
|
2179
|
+
return true;
|
|
2180
|
+
}
|
|
2181
|
+
await delay(intervalMs);
|
|
2182
|
+
}
|
|
2183
|
+
return false;
|
|
2184
|
+
}
|
|
2185
|
+
async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, excludeInstanceId) {
|
|
2186
|
+
let hostname = "127.0.0.1";
|
|
2187
|
+
try {
|
|
2188
|
+
hostname = new URL(baseUrl ?? DEFAULT_APP_SERVER_URL2).hostname;
|
|
2189
|
+
} catch {
|
|
2190
|
+
}
|
|
2191
|
+
const maxAttempts = 1e3;
|
|
2192
|
+
let port = basePort;
|
|
2193
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1, port += 1) {
|
|
2194
|
+
const claimedInState = Object.entries(state.instances).some(
|
|
2195
|
+
([id, inst]) => id !== excludeInstanceId && inst.port === port
|
|
2196
|
+
);
|
|
2197
|
+
if (claimedInState) {
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
if (!isLoopbackHost(hostname)) {
|
|
2201
|
+
return port;
|
|
2202
|
+
}
|
|
2203
|
+
if (await isTcpPortAvailable(hostname, port)) {
|
|
2204
|
+
return port;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
throw new Error(
|
|
2208
|
+
`Failed to find a free app-server port starting at ${basePort}`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// src/engine/bridge-codex-command.ts
|
|
2213
|
+
import * as fs13 from "fs";
|
|
2214
|
+
import * as path14 from "path";
|
|
2215
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2216
|
+
function resolveCodexCommand(platform) {
|
|
2217
|
+
const candidates = platform === "win32" ? ["codex.cmd", "codex.exe", "codex", "codex.ps1"] : ["codex"];
|
|
2218
|
+
const resolved = probeCommand(candidates).command;
|
|
2219
|
+
if (!resolved) return null;
|
|
2220
|
+
if (platform === "win32" && resolved.endsWith(".cmd")) {
|
|
2221
|
+
const unwrapped = unwrapNpmCmdShim(resolved);
|
|
2222
|
+
if (unwrapped) return unwrapped;
|
|
2223
|
+
}
|
|
2224
|
+
return resolved;
|
|
2225
|
+
}
|
|
2226
|
+
function unwrapNpmCmdShim(cmdPath) {
|
|
2227
|
+
let content;
|
|
2228
|
+
try {
|
|
2229
|
+
content = fs13.readFileSync(cmdPath, "utf-8");
|
|
2230
|
+
} catch {
|
|
2231
|
+
return null;
|
|
2232
|
+
}
|
|
2233
|
+
const match = content.match(
|
|
2234
|
+
/"%_prog%"\s+"(%dp0%\\[^"]+)"\s+%\*/
|
|
2235
|
+
);
|
|
2236
|
+
if (!match) return null;
|
|
2237
|
+
const dp0 = path14.dirname(cmdPath);
|
|
2238
|
+
const scriptRelative = match[1].replace(/%dp0%\\/g, "");
|
|
2239
|
+
const scriptPath = path14.resolve(dp0, scriptRelative);
|
|
2240
|
+
if (!fs13.existsSync(scriptPath)) return null;
|
|
2241
|
+
const localNode = path14.join(dp0, "node.exe");
|
|
2242
|
+
const nodeCommand = fs13.existsSync(localNode) ? localNode : probeCommand(["node.exe", "node"]).command ?? "node";
|
|
2243
|
+
return `${nodeCommand}\0${scriptPath}`;
|
|
2244
|
+
}
|
|
2245
|
+
function splitResolvedCommand(resolved) {
|
|
2246
|
+
const parts = resolved.split("\0");
|
|
2247
|
+
if (parts.length === 2) {
|
|
2248
|
+
return { command: parts[0], prefixArgs: [parts[1]] };
|
|
2249
|
+
}
|
|
2250
|
+
return { command: resolved, prefixArgs: [] };
|
|
2251
|
+
}
|
|
2252
|
+
function resolvePowerShellCommand() {
|
|
2253
|
+
return probeCommand(["pwsh", "powershell", "powershell.exe"]).command ?? "powershell";
|
|
2310
2254
|
}
|
|
2311
2255
|
function resolveAuthGatewayScript(repoRoot) {
|
|
2312
|
-
const moduleDir =
|
|
2256
|
+
const moduleDir = path14.dirname(fileURLToPath4(import.meta.url));
|
|
2313
2257
|
const candidates = [
|
|
2314
2258
|
// Bundled: dist/bridges/ sibling (npm install / built package)
|
|
2315
|
-
|
|
2259
|
+
path14.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
|
|
2316
2260
|
// Source: src/bridges/ sibling (monorepo dev with ts runner)
|
|
2317
|
-
|
|
2261
|
+
path14.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
|
|
2318
2262
|
// Monorepo dist fallback
|
|
2319
|
-
|
|
2263
|
+
path14.join(
|
|
2320
2264
|
repoRoot,
|
|
2321
2265
|
"packages",
|
|
2322
2266
|
"tap-comms",
|
|
@@ -2324,7 +2268,7 @@ function resolveAuthGatewayScript(repoRoot) {
|
|
|
2324
2268
|
"bridges",
|
|
2325
2269
|
"codex-app-server-auth-gateway.mjs"
|
|
2326
2270
|
),
|
|
2327
|
-
|
|
2271
|
+
path14.join(
|
|
2328
2272
|
repoRoot,
|
|
2329
2273
|
"packages",
|
|
2330
2274
|
"tap-comms",
|
|
@@ -2338,41 +2282,661 @@ function resolveAuthGatewayScript(repoRoot) {
|
|
|
2338
2282
|
return candidate;
|
|
2339
2283
|
}
|
|
2340
2284
|
}
|
|
2341
|
-
return null;
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2285
|
+
return null;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/engine/bridge-windows-spawn.ts
|
|
2289
|
+
import * as fs14 from "fs";
|
|
2290
|
+
import * as os3 from "os";
|
|
2291
|
+
import * as path15 from "path";
|
|
2292
|
+
import { randomBytes } from "crypto";
|
|
2293
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2294
|
+
var WINDOWS_SPAWN_WRAPPER_PREFIX = "tap-spawn-";
|
|
2295
|
+
var WINDOWS_SPAWN_WRAPPER_STALE_MS = 60 * 60 * 1e3;
|
|
2296
|
+
function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
|
|
2297
|
+
let entries;
|
|
2298
|
+
try {
|
|
2299
|
+
entries = fs14.readdirSync(os3.tmpdir());
|
|
2300
|
+
} catch {
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
for (const entry of entries) {
|
|
2304
|
+
if (!entry.startsWith(WINDOWS_SPAWN_WRAPPER_PREFIX) || !/\.(cmd|ps1)$/i.test(entry)) {
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
const wrapperPath = path15.join(os3.tmpdir(), entry);
|
|
2308
|
+
try {
|
|
2309
|
+
const stats = fs14.statSync(wrapperPath);
|
|
2310
|
+
if (now - stats.mtimeMs < WINDOWS_SPAWN_WRAPPER_STALE_MS) {
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
fs14.unlinkSync(wrapperPath);
|
|
2314
|
+
} catch {
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
function buildWindowsDetachedWrapperScript(command, args, logPath, stderrLogPath, env) {
|
|
2319
|
+
const lines = ["$ErrorActionPreference = 'Stop'"];
|
|
2320
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2321
|
+
if (value !== void 0 && value !== process.env[key]) {
|
|
2322
|
+
lines.push(
|
|
2323
|
+
`[Environment]::SetEnvironmentVariable(${toPowerShellSingleQuotedString(key)}, ${toPowerShellSingleQuotedString(value)}, 'Process')`
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
lines.push(
|
|
2328
|
+
`$logPath = ${toPowerShellSingleQuotedString(logPath)}`,
|
|
2329
|
+
`$stderrLogPath = ${toPowerShellSingleQuotedString(stderrLogPath)}`,
|
|
2330
|
+
`$commandPath = ${toPowerShellSingleQuotedString(command)}`,
|
|
2331
|
+
`$commandArgs = ${toPowerShellStringArrayLiteral(args)}`,
|
|
2332
|
+
"$exitCode = 1",
|
|
2333
|
+
"try {",
|
|
2334
|
+
" & $commandPath @commandArgs >> $logPath 2>> $stderrLogPath",
|
|
2335
|
+
" $exitCode = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 0 }",
|
|
2336
|
+
"} finally {",
|
|
2337
|
+
" Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue",
|
|
2338
|
+
"}",
|
|
2339
|
+
"exit $exitCode"
|
|
2340
|
+
);
|
|
2341
|
+
return `${lines.join("\r\n")}\r
|
|
2342
|
+
`;
|
|
2343
|
+
}
|
|
2344
|
+
function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
|
|
2345
|
+
const stderrLogPath = stderrLogFilePath(logPath);
|
|
2346
|
+
const powerShellCommand = resolvePowerShellCommand();
|
|
2347
|
+
cleanupStaleWindowsSpawnWrappers();
|
|
2348
|
+
const wrapperPath = path15.join(
|
|
2349
|
+
os3.tmpdir(),
|
|
2350
|
+
`${WINDOWS_SPAWN_WRAPPER_PREFIX}${randomBytes(4).toString("hex")}.ps1`
|
|
2351
|
+
);
|
|
2352
|
+
fs14.writeFileSync(
|
|
2353
|
+
wrapperPath,
|
|
2354
|
+
buildWindowsDetachedWrapperScript(
|
|
2355
|
+
command,
|
|
2356
|
+
args,
|
|
2357
|
+
logPath,
|
|
2358
|
+
stderrLogPath,
|
|
2359
|
+
env
|
|
2360
|
+
)
|
|
2361
|
+
);
|
|
2362
|
+
const psCommand = [
|
|
2363
|
+
"$p = Start-Process",
|
|
2364
|
+
`-FilePath ${toPowerShellSingleQuotedString(powerShellCommand)}`,
|
|
2365
|
+
`-ArgumentList ${toPowerShellStringArrayLiteral(["-NoLogo", "-NoProfile", "-File", wrapperPath])}`,
|
|
2366
|
+
`-WorkingDirectory ${toPowerShellSingleQuotedString(repoRoot)}`,
|
|
2367
|
+
"-WindowStyle Hidden",
|
|
2368
|
+
"-PassThru",
|
|
2369
|
+
"; Write-Output $p.Id"
|
|
2370
|
+
].join(" ");
|
|
2371
|
+
const result = spawnSync3(
|
|
2372
|
+
powerShellCommand,
|
|
2373
|
+
["-NoLogo", "-NoProfile", "-Command", psCommand],
|
|
2374
|
+
{
|
|
2375
|
+
encoding: "utf-8",
|
|
2376
|
+
windowsHide: true
|
|
2377
|
+
}
|
|
2378
|
+
);
|
|
2379
|
+
if (result.status !== 0) {
|
|
2380
|
+
removeFileIfExists(wrapperPath);
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
const pid = parseInt(result.stdout.trim(), 10);
|
|
2384
|
+
if (!Number.isFinite(pid)) {
|
|
2385
|
+
removeFileIfExists(wrapperPath);
|
|
2386
|
+
return null;
|
|
2387
|
+
}
|
|
2388
|
+
return pid;
|
|
2389
|
+
}
|
|
2390
|
+
function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
|
|
2391
|
+
const { command: exe, prefixArgs } = splitResolvedCommand(command);
|
|
2392
|
+
return startWindowsDetachedProcess(
|
|
2393
|
+
exe,
|
|
2394
|
+
[...prefixArgs, "app-server", "--listen", url],
|
|
2395
|
+
repoRoot,
|
|
2396
|
+
logPath
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
function findListeningProcessId(url, platform) {
|
|
2400
|
+
if (platform !== "win32") {
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
let port;
|
|
2404
|
+
try {
|
|
2405
|
+
const parsed = new URL(url);
|
|
2406
|
+
port = parsed.port ? Number.parseInt(parsed.port, 10) : null;
|
|
2407
|
+
} catch {
|
|
2408
|
+
return null;
|
|
2409
|
+
}
|
|
2410
|
+
if (port == null || !Number.isFinite(port)) {
|
|
2411
|
+
return null;
|
|
2412
|
+
}
|
|
2413
|
+
const result = spawnSync3(
|
|
2414
|
+
resolvePowerShellCommand(),
|
|
2415
|
+
[
|
|
2416
|
+
"-NoLogo",
|
|
2417
|
+
"-NoProfile",
|
|
2418
|
+
"-Command",
|
|
2419
|
+
[
|
|
2420
|
+
`$port = ${port}`,
|
|
2421
|
+
"$processId = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess",
|
|
2422
|
+
"if ($processId) { $processId }"
|
|
2423
|
+
].join("; ")
|
|
2424
|
+
],
|
|
2425
|
+
{
|
|
2426
|
+
encoding: "utf-8",
|
|
2427
|
+
windowsHide: true
|
|
2428
|
+
}
|
|
2429
|
+
);
|
|
2430
|
+
if (result.status !== 0) {
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
|
|
2434
|
+
return Number.isFinite(parsedPid) ? parsedPid : null;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// src/engine/bridge-unix-spawn.ts
|
|
2438
|
+
import * as fs15 from "fs";
|
|
2439
|
+
import { spawn, spawnSync as spawnSync4 } from "child_process";
|
|
2440
|
+
function startUnixDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
|
|
2441
|
+
const stderrPath = stderrLogFilePath(logPath);
|
|
2442
|
+
let logFd = null;
|
|
2443
|
+
let stderrFd = null;
|
|
2444
|
+
try {
|
|
2445
|
+
logFd = fs15.openSync(logPath, "a");
|
|
2446
|
+
stderrFd = fs15.openSync(stderrPath, "a");
|
|
2447
|
+
const child = spawn(command, args, {
|
|
2448
|
+
cwd: repoRoot,
|
|
2449
|
+
detached: true,
|
|
2450
|
+
stdio: ["ignore", logFd, stderrFd],
|
|
2451
|
+
env,
|
|
2452
|
+
windowsHide: true
|
|
2453
|
+
});
|
|
2454
|
+
child.unref();
|
|
2455
|
+
return child.pid ?? null;
|
|
2456
|
+
} finally {
|
|
2457
|
+
if (logFd != null) {
|
|
2458
|
+
fs15.closeSync(logFd);
|
|
2459
|
+
}
|
|
2460
|
+
if (stderrFd != null) {
|
|
2461
|
+
fs15.closeSync(stderrFd);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
function startUnixCodexAppServer(command, url, repoRoot, logPath) {
|
|
2466
|
+
const { command: exe, prefixArgs } = splitResolvedCommand(command);
|
|
2467
|
+
return startUnixDetachedProcess(
|
|
2468
|
+
exe,
|
|
2469
|
+
[...prefixArgs, "app-server", "--listen", url],
|
|
2470
|
+
repoRoot,
|
|
2471
|
+
logPath
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
function findUnixListeningProcessId(url, platform) {
|
|
2475
|
+
if (platform === "win32") {
|
|
2476
|
+
return null;
|
|
2477
|
+
}
|
|
2478
|
+
let port;
|
|
2479
|
+
try {
|
|
2480
|
+
const parsed = new URL(url);
|
|
2481
|
+
port = parsed.port ? Number.parseInt(parsed.port, 10) : null;
|
|
2482
|
+
} catch {
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
if (port == null || !Number.isFinite(port)) {
|
|
2486
|
+
return null;
|
|
2487
|
+
}
|
|
2488
|
+
const result = spawnSync4(
|
|
2489
|
+
"lsof",
|
|
2490
|
+
["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"],
|
|
2491
|
+
{
|
|
2492
|
+
encoding: "utf-8",
|
|
2493
|
+
windowsHide: true
|
|
2494
|
+
}
|
|
2495
|
+
);
|
|
2496
|
+
if (!result || result.status !== 0) {
|
|
2497
|
+
return null;
|
|
2498
|
+
}
|
|
2499
|
+
const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
|
|
2500
|
+
return Number.isFinite(parsedPid) ? parsedPid : null;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// src/engine/bridge-process-control.ts
|
|
2504
|
+
import { execSync as execSync2 } from "child_process";
|
|
2505
|
+
function isProcessAlive(pid) {
|
|
2506
|
+
try {
|
|
2507
|
+
process.kill(pid, 0);
|
|
2508
|
+
return true;
|
|
2509
|
+
} catch {
|
|
2510
|
+
return false;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
async function terminateProcess(pid, platform) {
|
|
2514
|
+
if (!isProcessAlive(pid)) {
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
try {
|
|
2518
|
+
if (platform === "win32") {
|
|
2519
|
+
execSync2(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
|
|
2520
|
+
} else {
|
|
2521
|
+
process.kill(pid, "SIGTERM");
|
|
2522
|
+
await delay(2e3);
|
|
2523
|
+
if (isProcessAlive(pid)) {
|
|
2524
|
+
process.kill(pid, "SIGKILL");
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
} catch {
|
|
2528
|
+
}
|
|
2529
|
+
return !isProcessAlive(pid);
|
|
2530
|
+
}
|
|
2531
|
+
async function stopManagedAppServer(appServer, platform) {
|
|
2532
|
+
if (!appServer.managed) {
|
|
2533
|
+
return false;
|
|
2534
|
+
}
|
|
2535
|
+
let stopped = false;
|
|
2536
|
+
if (appServer.auth?.gatewayPid != null) {
|
|
2537
|
+
stopped = await terminateProcess(appServer.auth.gatewayPid, platform) || stopped;
|
|
2538
|
+
}
|
|
2539
|
+
if (appServer.pid != null) {
|
|
2540
|
+
stopped = await terminateProcess(appServer.pid, platform) || stopped;
|
|
2541
|
+
}
|
|
2542
|
+
removeFileIfExists(appServer.auth?.tokenPath);
|
|
2543
|
+
return stopped;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// src/engine/bridge-config.ts
|
|
2547
|
+
import * as fs16 from "fs";
|
|
2548
|
+
import * as path16 from "path";
|
|
2549
|
+
function resolveAgentName(instanceId, explicit, context) {
|
|
2550
|
+
if (explicit) return explicit;
|
|
2551
|
+
try {
|
|
2552
|
+
const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
|
|
2553
|
+
const state = loadState(repoRoot);
|
|
2554
|
+
const stateAgent = state?.instances[instanceId]?.agentName;
|
|
2555
|
+
if (stateAgent) return stateAgent;
|
|
2556
|
+
} catch {
|
|
2557
|
+
}
|
|
2558
|
+
return process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME || null;
|
|
2559
|
+
}
|
|
2560
|
+
function inferRestartMode(bridgeState, flags, savedMode) {
|
|
2561
|
+
const wasManaged = bridgeState?.appServer != null;
|
|
2562
|
+
const hadAuth = bridgeState?.appServer?.auth != null;
|
|
2563
|
+
const manageAppServer = flags?.noServer === true ? false : flags?.noServer === void 0 ? savedMode?.manageAppServer ?? wasManaged : true;
|
|
2564
|
+
const noAuth = flags?.noAuth === true ? true : flags?.noAuth === void 0 ? savedMode?.noAuth ?? !hadAuth : false;
|
|
2565
|
+
return { manageAppServer, noAuth };
|
|
2566
|
+
}
|
|
2567
|
+
function cleanupHeadlessDispatch(inboxDir, agentName) {
|
|
2568
|
+
const removed = [];
|
|
2569
|
+
if (!fs16.existsSync(inboxDir)) return removed;
|
|
2570
|
+
const normalizedAgent = agentName.replace(/-/g, "_");
|
|
2571
|
+
const marker = `-headless-${normalizedAgent}-review-`;
|
|
2572
|
+
try {
|
|
2573
|
+
for (const file of fs16.readdirSync(inboxDir)) {
|
|
2574
|
+
if (file.includes(marker)) {
|
|
2575
|
+
fs16.unlinkSync(path16.join(inboxDir, file));
|
|
2576
|
+
removed.push(file);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
} catch {
|
|
2580
|
+
}
|
|
2581
|
+
return removed;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/engine/bridge-state.ts
|
|
2585
|
+
import * as fs17 from "fs";
|
|
2586
|
+
function loadRuntimeBridgeHeartbeat(bridgeState) {
|
|
2587
|
+
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
2588
|
+
if (!runtimeStateDir) {
|
|
2589
|
+
return null;
|
|
2590
|
+
}
|
|
2591
|
+
const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
|
|
2592
|
+
if (!fs17.existsSync(heartbeatPath)) {
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2595
|
+
try {
|
|
2596
|
+
return JSON.parse(
|
|
2597
|
+
fs17.readFileSync(heartbeatPath, "utf-8")
|
|
2598
|
+
);
|
|
2599
|
+
} catch {
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function loadRuntimeBridgeThreadState(bridgeState) {
|
|
2604
|
+
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
2605
|
+
if (!runtimeStateDir) {
|
|
2606
|
+
return null;
|
|
2607
|
+
}
|
|
2608
|
+
const threadPath = runtimeThreadStateFilePath(runtimeStateDir);
|
|
2609
|
+
if (!fs17.existsSync(threadPath)) {
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
2612
|
+
try {
|
|
2613
|
+
const parsed = JSON.parse(
|
|
2614
|
+
fs17.readFileSync(threadPath, "utf-8")
|
|
2615
|
+
);
|
|
2616
|
+
return parsed.threadId ? parsed : null;
|
|
2617
|
+
} catch {
|
|
2618
|
+
return null;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
function loadBridgeState(stateDir, instanceId) {
|
|
2622
|
+
const pidPath = pidFilePath(stateDir, instanceId);
|
|
2623
|
+
if (!fs17.existsSync(pidPath)) return null;
|
|
2624
|
+
try {
|
|
2625
|
+
const raw = fs17.readFileSync(pidPath, "utf-8");
|
|
2626
|
+
return JSON.parse(raw);
|
|
2627
|
+
} catch {
|
|
2628
|
+
return null;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
function saveBridgeState(stateDir, instanceId, state) {
|
|
2632
|
+
const pidPath = pidFilePath(stateDir, instanceId);
|
|
2633
|
+
const serializable = JSON.parse(JSON.stringify(state));
|
|
2634
|
+
if (serializable.appServer?.auth) {
|
|
2635
|
+
delete serializable.appServer.auth.token;
|
|
2636
|
+
}
|
|
2637
|
+
writeProtectedTextFile(pidPath, JSON.stringify(serializable, null, 2));
|
|
2638
|
+
}
|
|
2639
|
+
function clearBridgeState(stateDir, instanceId) {
|
|
2640
|
+
const pidPath = pidFilePath(stateDir, instanceId);
|
|
2641
|
+
if (fs17.existsSync(pidPath)) {
|
|
2642
|
+
fs17.unlinkSync(pidPath);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
function isBridgeRunning(stateDir, instanceId) {
|
|
2646
|
+
const state = loadBridgeState(stateDir, instanceId);
|
|
2647
|
+
if (!state) return false;
|
|
2648
|
+
return isProcessAlive(state.pid);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/engine/bridge-observability.ts
|
|
2652
|
+
import * as fs18 from "fs";
|
|
2653
|
+
function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
|
|
2654
|
+
const heartbeat = loadRuntimeBridgeHeartbeat({ runtimeStateDir });
|
|
2655
|
+
return typeof heartbeat?.updatedAt === "string" ? heartbeat.updatedAt : null;
|
|
2656
|
+
}
|
|
2657
|
+
function resolveHeartbeatTimestamp(state) {
|
|
2658
|
+
return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
|
|
2659
|
+
}
|
|
2660
|
+
function getHeartbeatAge(stateDir, instanceId) {
|
|
2661
|
+
const state = loadBridgeState(stateDir, instanceId);
|
|
2662
|
+
const heartbeat = resolveHeartbeatTimestamp(state);
|
|
2663
|
+
if (!heartbeat) return null;
|
|
2664
|
+
const heartbeatTime = new Date(heartbeat).getTime();
|
|
2665
|
+
if (isNaN(heartbeatTime)) return null;
|
|
2666
|
+
return Math.floor((Date.now() - heartbeatTime) / 1e3);
|
|
2667
|
+
}
|
|
2668
|
+
function getBridgeHeartbeatTimestamp(stateDir, instanceId) {
|
|
2669
|
+
return resolveHeartbeatTimestamp(loadBridgeState(stateDir, instanceId));
|
|
2670
|
+
}
|
|
2671
|
+
function getBridgeStatus(stateDir, instanceId) {
|
|
2672
|
+
const state = loadBridgeState(stateDir, instanceId);
|
|
2673
|
+
if (!state) return "stopped";
|
|
2674
|
+
if (!isProcessAlive(state.pid)) {
|
|
2675
|
+
clearBridgeState(stateDir, instanceId);
|
|
2676
|
+
return "stale";
|
|
2677
|
+
}
|
|
2678
|
+
return "running";
|
|
2679
|
+
}
|
|
2680
|
+
function getTurnInfo(stateDir, instanceId, stuckThresholdSeconds = 300) {
|
|
2681
|
+
const state = loadBridgeState(stateDir, instanceId);
|
|
2682
|
+
if (!state) return null;
|
|
2683
|
+
const heartbeat = loadRuntimeBridgeHeartbeat(state);
|
|
2684
|
+
if (!heartbeat) return null;
|
|
2685
|
+
const activeTurnId = heartbeat.activeTurnId ?? null;
|
|
2686
|
+
const lastTurnStatus = heartbeat.lastTurnStatus ?? null;
|
|
2687
|
+
const turnTimestamp = heartbeat.turnStartedAt ?? null;
|
|
2688
|
+
const updatedAt = turnTimestamp ?? heartbeat.updatedAt ?? null;
|
|
2689
|
+
let ageSeconds = null;
|
|
2690
|
+
if (turnTimestamp) {
|
|
2691
|
+
const ts = new Date(turnTimestamp).getTime();
|
|
2692
|
+
if (!isNaN(ts)) {
|
|
2693
|
+
ageSeconds = Math.floor((Date.now() - ts) / 1e3);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
const stuck = activeTurnId !== null && ageSeconds !== null && ageSeconds > stuckThresholdSeconds;
|
|
2697
|
+
return { activeTurnId, lastTurnStatus, updatedAt, ageSeconds, stuck };
|
|
2698
|
+
}
|
|
2699
|
+
function isTurnStuck(stateDir, instanceId, thresholdSeconds = 300) {
|
|
2700
|
+
const info = getTurnInfo(stateDir, instanceId, thresholdSeconds);
|
|
2701
|
+
return info?.stuck ?? false;
|
|
2702
|
+
}
|
|
2703
|
+
function rotateLog(logPath) {
|
|
2704
|
+
if (!fs18.existsSync(logPath)) return;
|
|
2705
|
+
try {
|
|
2706
|
+
const stats = fs18.statSync(logPath);
|
|
2707
|
+
if (stats.size === 0) return;
|
|
2708
|
+
const prevPath = `${logPath}.prev`;
|
|
2709
|
+
fs18.renameSync(logPath, prevPath);
|
|
2710
|
+
} catch {
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
// src/engine/bridge-app-server-health.ts
|
|
2715
|
+
var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
2716
|
+
var APP_SERVER_HEALTH_RETRY_MS = 250;
|
|
2717
|
+
var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
|
|
2718
|
+
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
|
|
2719
|
+
const WebSocket = getWebSocketCtor();
|
|
2720
|
+
if (!WebSocket) {
|
|
2721
|
+
return false;
|
|
2722
|
+
}
|
|
2723
|
+
return new Promise((resolve14) => {
|
|
2724
|
+
let settled = false;
|
|
2725
|
+
let socket = null;
|
|
2726
|
+
const finish = (healthy) => {
|
|
2727
|
+
if (settled) {
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
settled = true;
|
|
2731
|
+
clearTimeout(timer);
|
|
2732
|
+
try {
|
|
2733
|
+
socket?.close();
|
|
2734
|
+
} catch {
|
|
2735
|
+
}
|
|
2736
|
+
resolve14(healthy);
|
|
2737
|
+
};
|
|
2738
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2739
|
+
try {
|
|
2740
|
+
const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
|
|
2741
|
+
socket = new WebSocket(url, protocols);
|
|
2742
|
+
socket.addEventListener("open", () => finish(true), { once: true });
|
|
2743
|
+
socket.addEventListener("error", () => finish(false), { once: true });
|
|
2744
|
+
socket.addEventListener("close", () => finish(false), { once: true });
|
|
2745
|
+
} catch {
|
|
2746
|
+
finish(false);
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
async function waitForAppServerHealth(url, timeoutMs, gatewayToken) {
|
|
2751
|
+
const deadline = Date.now() + timeoutMs;
|
|
2752
|
+
while (Date.now() < deadline) {
|
|
2753
|
+
if (await checkAppServerHealth(url, APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken)) {
|
|
2754
|
+
return true;
|
|
2755
|
+
}
|
|
2756
|
+
await delay(APP_SERVER_HEALTH_RETRY_MS);
|
|
2757
|
+
}
|
|
2758
|
+
return false;
|
|
2759
|
+
}
|
|
2760
|
+
function markAppServerHealthy(appServer) {
|
|
2761
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2762
|
+
return {
|
|
2763
|
+
...appServer,
|
|
2764
|
+
healthy: true,
|
|
2765
|
+
lastCheckedAt: checkedAt,
|
|
2766
|
+
lastHealthyAt: checkedAt
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// src/engine/bridge-app-server-auth.ts
|
|
2771
|
+
import * as fs20 from "fs";
|
|
2772
|
+
import * as path18 from "path";
|
|
2773
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2774
|
+
|
|
2775
|
+
// src/runtime/resolve-node.ts
|
|
2776
|
+
import * as fs19 from "fs";
|
|
2777
|
+
import * as path17 from "path";
|
|
2778
|
+
import { execSync as execSync3 } from "child_process";
|
|
2779
|
+
function readNodeVersion(repoRoot) {
|
|
2780
|
+
const nvFile = path17.join(repoRoot, ".node-version");
|
|
2781
|
+
if (!fs19.existsSync(nvFile)) return null;
|
|
2782
|
+
try {
|
|
2783
|
+
const raw = fs19.readFileSync(nvFile, "utf-8").trim();
|
|
2784
|
+
return raw.length > 0 ? raw.replace(/^v/, "") : null;
|
|
2785
|
+
} catch {
|
|
2786
|
+
return null;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
function fnmCandidateDirs() {
|
|
2790
|
+
if (process.platform === "win32") {
|
|
2791
|
+
return [
|
|
2792
|
+
process.env.FNM_DIR,
|
|
2793
|
+
process.env.APPDATA ? path17.join(process.env.APPDATA, "fnm") : null,
|
|
2794
|
+
process.env.LOCALAPPDATA ? path17.join(process.env.LOCALAPPDATA, "fnm") : null,
|
|
2795
|
+
process.env.USERPROFILE ? path17.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
|
|
2796
|
+
].filter(Boolean);
|
|
2797
|
+
}
|
|
2798
|
+
return [
|
|
2799
|
+
process.env.FNM_DIR,
|
|
2800
|
+
process.env.HOME ? path17.join(process.env.HOME, ".local", "share", "fnm") : null,
|
|
2801
|
+
process.env.HOME ? path17.join(process.env.HOME, ".fnm") : null,
|
|
2802
|
+
process.env.XDG_DATA_HOME ? path17.join(process.env.XDG_DATA_HOME, "fnm") : null
|
|
2803
|
+
].filter(Boolean);
|
|
2804
|
+
}
|
|
2805
|
+
function nodeExecutableName() {
|
|
2806
|
+
return process.platform === "win32" ? "node.exe" : "node";
|
|
2807
|
+
}
|
|
2808
|
+
function probeFnmNode(desiredVersion) {
|
|
2809
|
+
const dirs = fnmCandidateDirs();
|
|
2810
|
+
const exe = nodeExecutableName();
|
|
2811
|
+
for (const baseDir of dirs) {
|
|
2812
|
+
const candidate = path17.join(
|
|
2813
|
+
baseDir,
|
|
2814
|
+
"node-versions",
|
|
2815
|
+
`v${desiredVersion}`,
|
|
2816
|
+
"installation",
|
|
2817
|
+
exe
|
|
2818
|
+
);
|
|
2819
|
+
if (!fs19.existsSync(candidate)) continue;
|
|
2820
|
+
try {
|
|
2821
|
+
const v = execSync3(`"${candidate}" --version`, {
|
|
2822
|
+
encoding: "utf-8",
|
|
2823
|
+
timeout: 5e3
|
|
2824
|
+
}).trim();
|
|
2825
|
+
if (v.startsWith(`v${desiredVersion.split(".")[0]}.`)) {
|
|
2826
|
+
return candidate;
|
|
2827
|
+
}
|
|
2828
|
+
} catch {
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return null;
|
|
2832
|
+
}
|
|
2833
|
+
function detectNodeMajorVersion(command) {
|
|
2834
|
+
try {
|
|
2835
|
+
const version2 = execSync3(`"${command}" --version`, {
|
|
2836
|
+
encoding: "utf-8",
|
|
2837
|
+
timeout: 5e3
|
|
2838
|
+
}).trim();
|
|
2839
|
+
const match = version2.match(/^v?(\d+)\./);
|
|
2840
|
+
return match ? parseInt(match[1], 10) : null;
|
|
2841
|
+
} catch {
|
|
2842
|
+
return null;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
function checkStripTypesSupport(command) {
|
|
2846
|
+
const major = detectNodeMajorVersion(command);
|
|
2847
|
+
if (major !== null && major >= 22) return true;
|
|
2848
|
+
try {
|
|
2849
|
+
execSync3(`"${command}" --experimental-strip-types -e ""`, {
|
|
2850
|
+
timeout: 5e3,
|
|
2851
|
+
stdio: "pipe"
|
|
2852
|
+
});
|
|
2853
|
+
return true;
|
|
2854
|
+
} catch {
|
|
2855
|
+
return false;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
function findTsxFallback(repoRoot) {
|
|
2859
|
+
const candidates = [
|
|
2860
|
+
path17.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
|
|
2861
|
+
path17.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
|
|
2862
|
+
path17.join(repoRoot, "node_modules", ".bin", "tsx")
|
|
2863
|
+
];
|
|
2864
|
+
for (const c of candidates) {
|
|
2865
|
+
if (fs19.existsSync(c)) return c;
|
|
2866
|
+
}
|
|
2867
|
+
return null;
|
|
2868
|
+
}
|
|
2869
|
+
function getFnmBinDir(repoRoot) {
|
|
2870
|
+
const desiredVersion = readNodeVersion(repoRoot);
|
|
2871
|
+
if (!desiredVersion) return null;
|
|
2872
|
+
const nodePath = probeFnmNode(desiredVersion);
|
|
2873
|
+
if (!nodePath) return null;
|
|
2874
|
+
return path17.dirname(nodePath);
|
|
2875
|
+
}
|
|
2876
|
+
function resolveNodeRuntime(configCommand, repoRoot) {
|
|
2877
|
+
if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
|
|
2878
|
+
return {
|
|
2879
|
+
command: configCommand,
|
|
2880
|
+
supportsStripTypes: false,
|
|
2881
|
+
source: "bun",
|
|
2882
|
+
majorVersion: null
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
const desiredVersion = readNodeVersion(repoRoot);
|
|
2886
|
+
if (desiredVersion) {
|
|
2887
|
+
const fnmNode = probeFnmNode(desiredVersion);
|
|
2888
|
+
if (fnmNode) {
|
|
2889
|
+
const major2 = detectNodeMajorVersion(fnmNode);
|
|
2890
|
+
return {
|
|
2891
|
+
command: fnmNode,
|
|
2892
|
+
supportsStripTypes: checkStripTypesSupport(fnmNode),
|
|
2893
|
+
source: "fnm",
|
|
2894
|
+
majorVersion: major2
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
const major = detectNodeMajorVersion(configCommand);
|
|
2899
|
+
if (major !== null) {
|
|
2900
|
+
return {
|
|
2901
|
+
command: configCommand,
|
|
2902
|
+
supportsStripTypes: checkStripTypesSupport(configCommand),
|
|
2903
|
+
source: major === detectNodeMajorVersion("node") ? "path" : "config",
|
|
2904
|
+
majorVersion: major
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
const tsx = findTsxFallback(repoRoot);
|
|
2908
|
+
if (tsx) {
|
|
2909
|
+
return {
|
|
2910
|
+
command: tsx,
|
|
2911
|
+
supportsStripTypes: false,
|
|
2912
|
+
source: "tsx-fallback",
|
|
2913
|
+
majorVersion: null
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
return {
|
|
2917
|
+
command: configCommand,
|
|
2918
|
+
supportsStripTypes: false,
|
|
2919
|
+
source: "path",
|
|
2920
|
+
majorVersion: null
|
|
2921
|
+
};
|
|
2345
2922
|
}
|
|
2346
|
-
|
|
2347
|
-
const
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
server.close(() => {
|
|
2356
|
-
reject(new Error("Failed to allocate a loopback port"));
|
|
2357
|
-
});
|
|
2358
|
-
return;
|
|
2359
|
-
}
|
|
2360
|
-
const port = address.port;
|
|
2361
|
-
server.close((error) => {
|
|
2362
|
-
if (error) {
|
|
2363
|
-
reject(error);
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
resolve13(port);
|
|
2367
|
-
});
|
|
2368
|
-
});
|
|
2369
|
-
});
|
|
2923
|
+
function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
|
|
2924
|
+
const fnmBin = getFnmBinDir(repoRoot);
|
|
2925
|
+
if (!fnmBin) return { ...baseEnv };
|
|
2926
|
+
const pathKey = process.platform === "win32" ? "Path" : "PATH";
|
|
2927
|
+
const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
|
|
2928
|
+
return {
|
|
2929
|
+
...baseEnv,
|
|
2930
|
+
[pathKey]: `${fnmBin}${path17.delimiter}${currentPath}`
|
|
2931
|
+
};
|
|
2370
2932
|
}
|
|
2933
|
+
|
|
2934
|
+
// src/engine/bridge-app-server-auth.ts
|
|
2371
2935
|
function buildProtectedAppServerUrl(publicUrl, _token) {
|
|
2372
2936
|
return publicUrl;
|
|
2373
2937
|
}
|
|
2374
2938
|
function readGatewayTokenFromPath(tokenPath) {
|
|
2375
|
-
return
|
|
2939
|
+
return fs20.readFileSync(tokenPath, "utf8").trim();
|
|
2376
2940
|
}
|
|
2377
2941
|
function readGatewayToken(auth) {
|
|
2378
2942
|
if (!auth) {
|
|
@@ -2382,14 +2946,14 @@ function readGatewayToken(auth) {
|
|
|
2382
2946
|
if (legacyToken?.trim()) {
|
|
2383
2947
|
return legacyToken.trim();
|
|
2384
2948
|
}
|
|
2385
|
-
if (!auth.tokenPath || !
|
|
2949
|
+
if (!auth.tokenPath || !fs20.existsSync(auth.tokenPath)) {
|
|
2386
2950
|
return null;
|
|
2387
2951
|
}
|
|
2388
2952
|
const fileToken = readGatewayTokenFromPath(auth.tokenPath);
|
|
2389
2953
|
return fileToken || null;
|
|
2390
2954
|
}
|
|
2391
2955
|
function materializeGatewayTokenFile(stateDir, instanceId, publicUrl, auth) {
|
|
2392
|
-
if (auth.tokenPath &&
|
|
2956
|
+
if (auth.tokenPath && fs20.existsSync(auth.tokenPath)) {
|
|
2393
2957
|
return auth;
|
|
2394
2958
|
}
|
|
2395
2959
|
const token = readGatewayToken(auth);
|
|
@@ -2415,7 +2979,7 @@ async function createManagedAppServerAuth(options) {
|
|
|
2415
2979
|
if (!gatewayScript) {
|
|
2416
2980
|
throw new Error("Auth gateway script not found");
|
|
2417
2981
|
}
|
|
2418
|
-
const token =
|
|
2982
|
+
const token = randomBytes2(24).toString("base64url");
|
|
2419
2983
|
const tokenPath = appServerGatewayTokenFilePath(
|
|
2420
2984
|
options.stateDir,
|
|
2421
2985
|
options.instanceId
|
|
@@ -2427,7 +2991,7 @@ async function createManagedAppServerAuth(options) {
|
|
|
2427
2991
|
options.stateDir,
|
|
2428
2992
|
options.instanceId
|
|
2429
2993
|
);
|
|
2430
|
-
|
|
2994
|
+
fs20.mkdirSync(path18.dirname(gatewayLogPath), { recursive: true });
|
|
2431
2995
|
rotateLog(gatewayLogPath);
|
|
2432
2996
|
const runtime = resolveNodeRuntime(process.execPath, options.repoRoot);
|
|
2433
2997
|
const gatewayArgs = [];
|
|
@@ -2447,37 +3011,23 @@ async function createManagedAppServerAuth(options) {
|
|
|
2447
3011
|
TAP_GATEWAY_TOKEN_FILE: tokenPath
|
|
2448
3012
|
};
|
|
2449
3013
|
let gatewayPid;
|
|
2450
|
-
{
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
env: gatewayEnv,
|
|
2468
|
-
windowsHide: true
|
|
2469
|
-
});
|
|
2470
|
-
child.unref();
|
|
2471
|
-
gatewayPid = child.pid ?? null;
|
|
2472
|
-
}
|
|
2473
|
-
} catch (error) {
|
|
2474
|
-
removeFileIfExists(tokenPath);
|
|
2475
|
-
throw error;
|
|
2476
|
-
} finally {
|
|
2477
|
-
if (logFd != null) {
|
|
2478
|
-
fs13.closeSync(logFd);
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
3014
|
+
try {
|
|
3015
|
+
gatewayPid = options.platform === "win32" ? startWindowsDetachedProcess(
|
|
3016
|
+
runtime.command,
|
|
3017
|
+
gatewayArgs,
|
|
3018
|
+
options.repoRoot,
|
|
3019
|
+
gatewayLogPath,
|
|
3020
|
+
gatewayEnv
|
|
3021
|
+
) : startUnixDetachedProcess(
|
|
3022
|
+
runtime.command,
|
|
3023
|
+
gatewayArgs,
|
|
3024
|
+
options.repoRoot,
|
|
3025
|
+
gatewayLogPath,
|
|
3026
|
+
gatewayEnv
|
|
3027
|
+
);
|
|
3028
|
+
} catch (error) {
|
|
3029
|
+
removeFileIfExists(tokenPath);
|
|
3030
|
+
throw error;
|
|
2481
3031
|
}
|
|
2482
3032
|
if (gatewayPid == null) {
|
|
2483
3033
|
removeFileIfExists(tokenPath);
|
|
@@ -2510,259 +3060,70 @@ function canReuseManagedAppServer(appServer) {
|
|
|
2510
3060
|
if (auth.gatewayPid != null && !isProcessAlive(auth.gatewayPid)) {
|
|
2511
3061
|
return false;
|
|
2512
3062
|
}
|
|
2513
|
-
}
|
|
2514
|
-
return true;
|
|
2515
|
-
}
|
|
2516
|
-
function markAppServerHealthy(appServer) {
|
|
2517
|
-
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2518
|
-
return {
|
|
2519
|
-
...appServer,
|
|
2520
|
-
healthy: true,
|
|
2521
|
-
lastCheckedAt: checkedAt,
|
|
2522
|
-
lastHealthyAt: checkedAt
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
function findReusableManagedAppServer(stateDir, publicUrl) {
|
|
2526
|
-
const pidDir = path13.join(stateDir, "pids");
|
|
2527
|
-
if (!fs13.existsSync(pidDir)) {
|
|
2528
|
-
return null;
|
|
2529
|
-
}
|
|
2530
|
-
for (const name of fs13.readdirSync(pidDir)) {
|
|
2531
|
-
if (!name.startsWith("bridge-") || !name.endsWith(".json")) {
|
|
2532
|
-
continue;
|
|
2533
|
-
}
|
|
2534
|
-
try {
|
|
2535
|
-
const raw = fs13.readFileSync(path13.join(pidDir, name), "utf-8");
|
|
2536
|
-
const parsed = JSON.parse(raw);
|
|
2537
|
-
if (parsed.appServer?.url !== publicUrl) {
|
|
2538
|
-
continue;
|
|
2539
|
-
}
|
|
2540
|
-
if (canReuseManagedAppServer(parsed.appServer)) {
|
|
2541
|
-
return markAppServerHealthy(parsed.appServer);
|
|
2542
|
-
}
|
|
2543
|
-
} catch {
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
return null;
|
|
2547
|
-
}
|
|
2548
|
-
function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
|
|
2549
|
-
const stderrLogPath = stderrLogFilePath(logPath);
|
|
2550
|
-
const powerShellCommand = resolvePowerShellCommand();
|
|
2551
|
-
cleanupStaleWindowsSpawnWrappers();
|
|
2552
|
-
const wrapperPath = path13.join(
|
|
2553
|
-
os3.tmpdir(),
|
|
2554
|
-
`${WINDOWS_SPAWN_WRAPPER_PREFIX}${randomBytes(4).toString("hex")}.ps1`
|
|
2555
|
-
);
|
|
2556
|
-
fs13.writeFileSync(
|
|
2557
|
-
wrapperPath,
|
|
2558
|
-
buildWindowsDetachedWrapperScript(
|
|
2559
|
-
command,
|
|
2560
|
-
args,
|
|
2561
|
-
logPath,
|
|
2562
|
-
stderrLogPath,
|
|
2563
|
-
env
|
|
2564
|
-
)
|
|
2565
|
-
);
|
|
2566
|
-
const psCommand = [
|
|
2567
|
-
"$p = Start-Process",
|
|
2568
|
-
`-FilePath ${toPowerShellSingleQuotedString(powerShellCommand)}`,
|
|
2569
|
-
`-ArgumentList ${toPowerShellStringArrayLiteral(["-NoLogo", "-NoProfile", "-File", wrapperPath])}`,
|
|
2570
|
-
`-WorkingDirectory ${toPowerShellSingleQuotedString(repoRoot)}`,
|
|
2571
|
-
"-WindowStyle Hidden",
|
|
2572
|
-
"-PassThru",
|
|
2573
|
-
"; Write-Output $p.Id"
|
|
2574
|
-
].join(" ");
|
|
2575
|
-
const result = spawnSync3(
|
|
2576
|
-
powerShellCommand,
|
|
2577
|
-
["-NoLogo", "-NoProfile", "-Command", psCommand],
|
|
2578
|
-
{
|
|
2579
|
-
encoding: "utf-8",
|
|
2580
|
-
windowsHide: true
|
|
2581
|
-
}
|
|
2582
|
-
);
|
|
2583
|
-
if (result.status !== 0) {
|
|
2584
|
-
removeFileIfExists(wrapperPath);
|
|
2585
|
-
return null;
|
|
2586
|
-
}
|
|
2587
|
-
const pid = parseInt(result.stdout.trim(), 10);
|
|
2588
|
-
if (!Number.isFinite(pid)) {
|
|
2589
|
-
removeFileIfExists(wrapperPath);
|
|
2590
|
-
return null;
|
|
2591
|
-
}
|
|
2592
|
-
return pid;
|
|
2593
|
-
}
|
|
2594
|
-
function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
|
|
2595
|
-
return startWindowsDetachedProcess(
|
|
2596
|
-
command,
|
|
2597
|
-
["app-server", "--listen", url],
|
|
2598
|
-
repoRoot,
|
|
2599
|
-
logPath
|
|
2600
|
-
);
|
|
2601
|
-
}
|
|
2602
|
-
function findListeningProcessId(url, platform) {
|
|
2603
|
-
if (platform !== "win32") {
|
|
2604
|
-
return null;
|
|
2605
|
-
}
|
|
2606
|
-
let port;
|
|
2607
|
-
try {
|
|
2608
|
-
const parsed = new URL(url);
|
|
2609
|
-
port = parsed.port ? Number.parseInt(parsed.port, 10) : null;
|
|
2610
|
-
} catch {
|
|
2611
|
-
return null;
|
|
2612
|
-
}
|
|
2613
|
-
if (port == null || !Number.isFinite(port)) {
|
|
2614
|
-
return null;
|
|
2615
|
-
}
|
|
2616
|
-
const result = spawnSync3(
|
|
2617
|
-
resolvePowerShellCommand(),
|
|
2618
|
-
[
|
|
2619
|
-
"-NoLogo",
|
|
2620
|
-
"-NoProfile",
|
|
2621
|
-
"-Command",
|
|
2622
|
-
[
|
|
2623
|
-
`$port = ${port}`,
|
|
2624
|
-
"$processId = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess",
|
|
2625
|
-
"if ($processId) { $processId }"
|
|
2626
|
-
].join("; ")
|
|
2627
|
-
],
|
|
2628
|
-
{
|
|
2629
|
-
encoding: "utf-8",
|
|
2630
|
-
windowsHide: true
|
|
2631
|
-
}
|
|
2632
|
-
);
|
|
2633
|
-
if (result.status !== 0) {
|
|
2634
|
-
return null;
|
|
2635
|
-
}
|
|
2636
|
-
const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
|
|
2637
|
-
return Number.isFinite(parsedPid) ? parsedPid : null;
|
|
2638
|
-
}
|
|
2639
|
-
function resolveAppServerUrl(baseUrl, port) {
|
|
2640
|
-
const resolvedBase = (baseUrl ?? DEFAULT_APP_SERVER_URL2).replace(/\/$/, "");
|
|
2641
|
-
if (port == null) {
|
|
2642
|
-
return resolvedBase;
|
|
2643
|
-
}
|
|
2644
|
-
try {
|
|
2645
|
-
const parsed = new URL(resolvedBase);
|
|
2646
|
-
parsed.port = String(port);
|
|
2647
|
-
return parsed.toString().replace(/\/$/, "");
|
|
2648
|
-
} catch {
|
|
2649
|
-
return resolvedBase;
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
async function isTcpPortAvailable(hostname, port) {
|
|
2653
|
-
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2654
|
-
return await new Promise((resolve13) => {
|
|
2655
|
-
const server = net.createServer();
|
|
2656
|
-
server.unref();
|
|
2657
|
-
server.once("error", () => resolve13(false));
|
|
2658
|
-
server.listen(port, bindHost, () => {
|
|
2659
|
-
server.close((error) => resolve13(!error));
|
|
2660
|
-
});
|
|
2661
|
-
});
|
|
2662
|
-
}
|
|
2663
|
-
async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, excludeInstanceId) {
|
|
2664
|
-
let hostname = "127.0.0.1";
|
|
2665
|
-
try {
|
|
2666
|
-
hostname = new URL(baseUrl ?? DEFAULT_APP_SERVER_URL2).hostname;
|
|
2667
|
-
} catch {
|
|
2668
|
-
}
|
|
2669
|
-
const maxAttempts = 1e3;
|
|
2670
|
-
let port = basePort;
|
|
2671
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1, port += 1) {
|
|
2672
|
-
const claimedInState = Object.entries(state.instances).some(
|
|
2673
|
-
([id, inst]) => id !== excludeInstanceId && inst.port === port
|
|
2674
|
-
);
|
|
2675
|
-
if (claimedInState) {
|
|
2676
|
-
continue;
|
|
2677
|
-
}
|
|
2678
|
-
if (!isLoopbackHost(hostname)) {
|
|
2679
|
-
return port;
|
|
2680
|
-
}
|
|
2681
|
-
if (await isTcpPortAvailable(hostname, port)) {
|
|
2682
|
-
return port;
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
2685
|
-
throw new Error(
|
|
2686
|
-
`Failed to find a free app-server port starting at ${basePort}`
|
|
2687
|
-
);
|
|
2688
|
-
}
|
|
2689
|
-
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
|
|
2690
|
-
const WebSocket = getWebSocketCtor();
|
|
2691
|
-
if (!WebSocket) {
|
|
2692
|
-
return false;
|
|
2693
|
-
}
|
|
2694
|
-
return new Promise((resolve13) => {
|
|
2695
|
-
let settled = false;
|
|
2696
|
-
let socket = null;
|
|
2697
|
-
const finish = (healthy) => {
|
|
2698
|
-
if (settled) {
|
|
2699
|
-
return;
|
|
2700
|
-
}
|
|
2701
|
-
settled = true;
|
|
2702
|
-
clearTimeout(timer);
|
|
2703
|
-
try {
|
|
2704
|
-
socket?.close();
|
|
2705
|
-
} catch {
|
|
2706
|
-
}
|
|
2707
|
-
resolve13(healthy);
|
|
2708
|
-
};
|
|
2709
|
-
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2710
|
-
try {
|
|
2711
|
-
const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
|
|
2712
|
-
socket = new WebSocket(url, protocols);
|
|
2713
|
-
socket.addEventListener("open", () => finish(true), { once: true });
|
|
2714
|
-
socket.addEventListener("error", () => finish(false), { once: true });
|
|
2715
|
-
socket.addEventListener("close", () => finish(false), { once: true });
|
|
2716
|
-
} catch {
|
|
2717
|
-
finish(false);
|
|
2718
|
-
}
|
|
2719
|
-
});
|
|
3063
|
+
}
|
|
3064
|
+
return true;
|
|
2720
3065
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
3066
|
+
|
|
3067
|
+
// src/engine/bridge-app-server-lifecycle.ts
|
|
3068
|
+
import * as fs21 from "fs";
|
|
3069
|
+
import * as path19 from "path";
|
|
3070
|
+
var DEFAULT_APP_SERVER_URL3 = "ws://127.0.0.1:4501";
|
|
3071
|
+
var APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
3072
|
+
var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
|
|
3073
|
+
function isAppServerUsedByOtherBridge(stateDir, excludeInstanceId, appServer) {
|
|
3074
|
+
const pidDir = path19.join(stateDir, "pids");
|
|
3075
|
+
if (!fs21.existsSync(pidDir)) return false;
|
|
3076
|
+
for (const name of fs21.readdirSync(pidDir)) {
|
|
3077
|
+
if (!name.startsWith("bridge-") || !name.endsWith(".json")) continue;
|
|
3078
|
+
const otherId = name.slice("bridge-".length, -".json".length);
|
|
3079
|
+
if (otherId === excludeInstanceId) continue;
|
|
3080
|
+
try {
|
|
3081
|
+
const raw = fs21.readFileSync(path19.join(pidDir, name), "utf-8");
|
|
3082
|
+
const state = JSON.parse(raw);
|
|
3083
|
+
if (state.appServer?.url === appServer.url && state.appServer?.pid === appServer.pid && isProcessAlive(state.pid)) {
|
|
3084
|
+
return true;
|
|
3085
|
+
}
|
|
3086
|
+
} catch {
|
|
3087
|
+
continue;
|
|
2730
3088
|
}
|
|
2731
|
-
await delay(APP_SERVER_HEALTH_RETRY_MS);
|
|
2732
3089
|
}
|
|
2733
3090
|
return false;
|
|
2734
3091
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
3092
|
+
function findReusableManagedAppServer(stateDir, publicUrl) {
|
|
3093
|
+
const pidDir = path19.join(stateDir, "pids");
|
|
3094
|
+
if (!fs21.existsSync(pidDir)) {
|
|
3095
|
+
return null;
|
|
2738
3096
|
}
|
|
2739
|
-
|
|
2740
|
-
if (
|
|
2741
|
-
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
3097
|
+
for (const name of fs21.readdirSync(pidDir)) {
|
|
3098
|
+
if (!name.startsWith("bridge-") || !name.endsWith(".json")) {
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
try {
|
|
3102
|
+
const raw = fs21.readFileSync(path19.join(pidDir, name), "utf-8");
|
|
3103
|
+
const parsed = JSON.parse(raw);
|
|
3104
|
+
if (parsed.appServer?.url !== publicUrl) {
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
if (canReuseManagedAppServer(parsed.appServer)) {
|
|
3108
|
+
return markAppServerHealthy(parsed.appServer);
|
|
2747
3109
|
}
|
|
3110
|
+
} catch {
|
|
2748
3111
|
}
|
|
2749
|
-
} catch {
|
|
2750
3112
|
}
|
|
2751
|
-
return
|
|
3113
|
+
return null;
|
|
2752
3114
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
let stopped = false;
|
|
2758
|
-
if (appServer.auth?.gatewayPid != null) {
|
|
2759
|
-
stopped = await terminateProcess(appServer.auth.gatewayPid, platform) || stopped;
|
|
3115
|
+
function resolveAppServerUrl(baseUrl, port) {
|
|
3116
|
+
const resolvedBase = (baseUrl ?? DEFAULT_APP_SERVER_URL3).replace(/\/$/, "");
|
|
3117
|
+
if (port == null) {
|
|
3118
|
+
return resolvedBase;
|
|
2760
3119
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
3120
|
+
try {
|
|
3121
|
+
const parsed = new URL(resolvedBase);
|
|
3122
|
+
parsed.port = String(port);
|
|
3123
|
+
return parsed.toString().replace(/\/$/, "");
|
|
3124
|
+
} catch {
|
|
3125
|
+
return resolvedBase;
|
|
2763
3126
|
}
|
|
2764
|
-
removeFileIfExists(appServer.auth?.tokenPath);
|
|
2765
|
-
return stopped;
|
|
2766
3127
|
}
|
|
2767
3128
|
async function ensureCodexAppServer(options) {
|
|
2768
3129
|
const effectiveUrl = resolveAppServerUrl(options.appServerUrl);
|
|
@@ -2810,7 +3171,7 @@ Start the app-server manually:
|
|
|
2810
3171
|
);
|
|
2811
3172
|
}
|
|
2812
3173
|
const logPath = appServerLogFilePath(options.stateDir, options.instanceId);
|
|
2813
|
-
|
|
3174
|
+
fs21.mkdirSync(path19.dirname(logPath), { recursive: true });
|
|
2814
3175
|
rotateLog(logPath);
|
|
2815
3176
|
if (options.noAuth) {
|
|
2816
3177
|
const manualCommand2 = formatCodexAppServerCommand("codex", effectiveUrl);
|
|
@@ -2832,21 +3193,13 @@ Start it manually:
|
|
|
2832
3193
|
);
|
|
2833
3194
|
}
|
|
2834
3195
|
} else {
|
|
2835
|
-
const logFd = fs13.openSync(logPath, "a");
|
|
2836
3196
|
try {
|
|
2837
|
-
|
|
3197
|
+
pid2 = startUnixCodexAppServer(
|
|
2838
3198
|
resolvedCommand,
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
detached: true,
|
|
2843
|
-
stdio: ["ignore", logFd, logFd],
|
|
2844
|
-
env: process.env,
|
|
2845
|
-
windowsHide: true
|
|
2846
|
-
}
|
|
3199
|
+
effectiveUrl,
|
|
3200
|
+
options.repoRoot,
|
|
3201
|
+
logPath
|
|
2847
3202
|
);
|
|
2848
|
-
child.unref();
|
|
2849
|
-
pid2 = child.pid ?? null;
|
|
2850
3203
|
} catch (err) {
|
|
2851
3204
|
throw new Error(
|
|
2852
3205
|
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -2854,8 +3207,6 @@ Start it manually:
|
|
|
2854
3207
|
${manualCommand2}`,
|
|
2855
3208
|
{ cause: err }
|
|
2856
3209
|
);
|
|
2857
|
-
} finally {
|
|
2858
|
-
fs13.closeSync(logFd);
|
|
2859
3210
|
}
|
|
2860
3211
|
}
|
|
2861
3212
|
if (pid2 == null) {
|
|
@@ -2878,7 +3229,7 @@ Or start it manually:
|
|
|
2878
3229
|
${manualCommand2}`
|
|
2879
3230
|
);
|
|
2880
3231
|
}
|
|
2881
|
-
pid2 = findListeningProcessId(effectiveUrl, options.platform) ?? pid2;
|
|
3232
|
+
pid2 = (options.platform === "win32" ? findListeningProcessId(effectiveUrl, options.platform) : findUnixListeningProcessId(effectiveUrl, options.platform)) ?? pid2;
|
|
2882
3233
|
const healthyAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2883
3234
|
return {
|
|
2884
3235
|
url: effectiveUrl,
|
|
@@ -2922,21 +3273,13 @@ Start it manually:
|
|
|
2922
3273
|
);
|
|
2923
3274
|
}
|
|
2924
3275
|
} else {
|
|
2925
|
-
const logFd = fs13.openSync(logPath, "a");
|
|
2926
3276
|
try {
|
|
2927
|
-
|
|
3277
|
+
pid = startUnixCodexAppServer(
|
|
2928
3278
|
resolvedCommand,
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
detached: true,
|
|
2933
|
-
stdio: ["ignore", logFd, logFd],
|
|
2934
|
-
env: process.env,
|
|
2935
|
-
windowsHide: true
|
|
2936
|
-
}
|
|
3279
|
+
auth.upstreamUrl,
|
|
3280
|
+
options.repoRoot,
|
|
3281
|
+
logPath
|
|
2937
3282
|
);
|
|
2938
|
-
child.unref();
|
|
2939
|
-
pid = child.pid ?? null;
|
|
2940
3283
|
} catch (err) {
|
|
2941
3284
|
if (auth.gatewayPid != null) {
|
|
2942
3285
|
await terminateProcess(auth.gatewayPid, options.platform);
|
|
@@ -2948,8 +3291,6 @@ Start it manually:
|
|
|
2948
3291
|
${manualCommand}`,
|
|
2949
3292
|
{ cause: err }
|
|
2950
3293
|
);
|
|
2951
|
-
} finally {
|
|
2952
|
-
fs13.closeSync(logFd);
|
|
2953
3294
|
}
|
|
2954
3295
|
}
|
|
2955
3296
|
if (pid == null) {
|
|
@@ -3006,7 +3347,7 @@ Check ${auth.gatewayLogPath ?? "the gateway log"} and ${logPath}.`
|
|
|
3006
3347
|
);
|
|
3007
3348
|
}
|
|
3008
3349
|
const healthyAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3009
|
-
pid = findListeningProcessId(auth.upstreamUrl, options.platform) ?? pid;
|
|
3350
|
+
pid = (options.platform === "win32" ? findListeningProcessId(auth.upstreamUrl, options.platform) : findUnixListeningProcessId(auth.upstreamUrl, options.platform)) ?? pid;
|
|
3010
3351
|
return {
|
|
3011
3352
|
url: effectiveUrl,
|
|
3012
3353
|
pid,
|
|
@@ -3019,130 +3360,15 @@ Check ${auth.gatewayLogPath ?? "the gateway log"} and ${logPath}.`
|
|
|
3019
3360
|
auth
|
|
3020
3361
|
};
|
|
3021
3362
|
}
|
|
3022
|
-
function
|
|
3023
|
-
return
|
|
3024
|
-
}
|
|
3025
|
-
function logFilePath(stateDir, instanceId) {
|
|
3026
|
-
return path13.join(stateDir, "logs", `bridge-${instanceId}.log`);
|
|
3027
|
-
}
|
|
3028
|
-
function runtimeHeartbeatFilePath(runtimeStateDir) {
|
|
3029
|
-
return path13.join(runtimeStateDir, "heartbeat.json");
|
|
3030
|
-
}
|
|
3031
|
-
function runtimeThreadStateFilePath(runtimeStateDir) {
|
|
3032
|
-
return path13.join(runtimeStateDir, "thread.json");
|
|
3033
|
-
}
|
|
3034
|
-
function loadRuntimeBridgeHeartbeat(bridgeState) {
|
|
3035
|
-
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
3036
|
-
if (!runtimeStateDir) {
|
|
3037
|
-
return null;
|
|
3038
|
-
}
|
|
3039
|
-
const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
|
|
3040
|
-
if (!fs13.existsSync(heartbeatPath)) {
|
|
3041
|
-
return null;
|
|
3042
|
-
}
|
|
3043
|
-
try {
|
|
3044
|
-
return JSON.parse(
|
|
3045
|
-
fs13.readFileSync(heartbeatPath, "utf-8")
|
|
3046
|
-
);
|
|
3047
|
-
} catch {
|
|
3048
|
-
return null;
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
function loadRuntimeBridgeThreadState(bridgeState) {
|
|
3052
|
-
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
3053
|
-
if (!runtimeStateDir) {
|
|
3054
|
-
return null;
|
|
3055
|
-
}
|
|
3056
|
-
const threadPath = runtimeThreadStateFilePath(runtimeStateDir);
|
|
3057
|
-
if (!fs13.existsSync(threadPath)) {
|
|
3058
|
-
return null;
|
|
3059
|
-
}
|
|
3060
|
-
try {
|
|
3061
|
-
const parsed = JSON.parse(
|
|
3062
|
-
fs13.readFileSync(threadPath, "utf-8")
|
|
3063
|
-
);
|
|
3064
|
-
return parsed.threadId ? parsed : null;
|
|
3065
|
-
} catch {
|
|
3066
|
-
return null;
|
|
3067
|
-
}
|
|
3068
|
-
}
|
|
3069
|
-
function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
|
|
3070
|
-
const heartbeat = loadRuntimeBridgeHeartbeat({ runtimeStateDir });
|
|
3071
|
-
return typeof heartbeat?.updatedAt === "string" ? heartbeat.updatedAt : null;
|
|
3072
|
-
}
|
|
3073
|
-
function resolveHeartbeatTimestamp(state) {
|
|
3074
|
-
return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
|
|
3075
|
-
}
|
|
3076
|
-
function loadBridgeState(stateDir, instanceId) {
|
|
3077
|
-
const pidPath = pidFilePath(stateDir, instanceId);
|
|
3078
|
-
if (!fs13.existsSync(pidPath)) return null;
|
|
3079
|
-
try {
|
|
3080
|
-
const raw = fs13.readFileSync(pidPath, "utf-8");
|
|
3081
|
-
return JSON.parse(raw);
|
|
3082
|
-
} catch {
|
|
3083
|
-
return null;
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
3086
|
-
function saveBridgeState(stateDir, instanceId, state) {
|
|
3087
|
-
const pidPath = pidFilePath(stateDir, instanceId);
|
|
3088
|
-
const serializable = JSON.parse(JSON.stringify(state));
|
|
3089
|
-
if (serializable.appServer?.auth) {
|
|
3090
|
-
delete serializable.appServer.auth.token;
|
|
3091
|
-
}
|
|
3092
|
-
writeProtectedTextFile(pidPath, JSON.stringify(serializable, null, 2));
|
|
3093
|
-
}
|
|
3094
|
-
function clearBridgeState(stateDir, instanceId) {
|
|
3095
|
-
const pidPath = pidFilePath(stateDir, instanceId);
|
|
3096
|
-
if (fs13.existsSync(pidPath)) {
|
|
3097
|
-
fs13.unlinkSync(pidPath);
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
function isProcessAlive(pid) {
|
|
3101
|
-
try {
|
|
3102
|
-
process.kill(pid, 0);
|
|
3103
|
-
return true;
|
|
3104
|
-
} catch {
|
|
3105
|
-
return false;
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
function isBridgeRunning(stateDir, instanceId) {
|
|
3109
|
-
const state = loadBridgeState(stateDir, instanceId);
|
|
3110
|
-
if (!state) return false;
|
|
3111
|
-
return isProcessAlive(state.pid);
|
|
3112
|
-
}
|
|
3113
|
-
function resolveAgentName(instanceId, explicit, context) {
|
|
3114
|
-
if (explicit) return explicit;
|
|
3115
|
-
try {
|
|
3116
|
-
const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
|
|
3117
|
-
const state = loadState(repoRoot);
|
|
3118
|
-
const stateAgent = state?.instances[instanceId]?.agentName;
|
|
3119
|
-
if (stateAgent) return stateAgent;
|
|
3120
|
-
} catch {
|
|
3121
|
-
}
|
|
3122
|
-
return process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME || null;
|
|
3123
|
-
}
|
|
3124
|
-
function inferRestartMode(bridgeState, flags, savedMode) {
|
|
3125
|
-
const wasManaged = bridgeState?.appServer != null;
|
|
3126
|
-
const hadAuth = bridgeState?.appServer?.auth != null;
|
|
3127
|
-
const manageAppServer = flags?.noServer === true ? false : flags?.noServer === void 0 ? savedMode?.manageAppServer ?? wasManaged : true;
|
|
3128
|
-
const noAuth = flags?.noAuth === true ? true : flags?.noAuth === void 0 ? savedMode?.noAuth ?? !hadAuth : false;
|
|
3129
|
-
return { manageAppServer, noAuth };
|
|
3363
|
+
function formatCodexAppServerCommand(command, url) {
|
|
3364
|
+
return `${command} app-server --listen ${url}`;
|
|
3130
3365
|
}
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
for (const file of fs13.readdirSync(inboxDir)) {
|
|
3138
|
-
if (file.includes(marker)) {
|
|
3139
|
-
fs13.unlinkSync(path13.join(inboxDir, file));
|
|
3140
|
-
removed.push(file);
|
|
3141
|
-
}
|
|
3142
|
-
}
|
|
3143
|
-
} catch {
|
|
3144
|
-
}
|
|
3145
|
-
return removed;
|
|
3366
|
+
|
|
3367
|
+
// src/engine/bridge-startup.ts
|
|
3368
|
+
import * as fs22 from "fs";
|
|
3369
|
+
import * as path20 from "path";
|
|
3370
|
+
function getBridgeRuntimeStateDir(repoRoot, instanceId) {
|
|
3371
|
+
return path20.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`);
|
|
3146
3372
|
}
|
|
3147
3373
|
async function startBridge(options) {
|
|
3148
3374
|
const {
|
|
@@ -3173,10 +3399,9 @@ async function startBridge(options) {
|
|
|
3173
3399
|
const previousAppServer = previousBridgeState?.appServer ?? null;
|
|
3174
3400
|
clearBridgeState(stateDir, instanceId);
|
|
3175
3401
|
const logPath = logFilePath(stateDir, instanceId);
|
|
3176
|
-
|
|
3402
|
+
fs22.mkdirSync(path20.dirname(logPath), { recursive: true });
|
|
3177
3403
|
rotateLog(logPath);
|
|
3178
|
-
|
|
3179
|
-
const repoRoot = options.repoRoot ?? path13.resolve(stateDir, "..");
|
|
3404
|
+
const repoRoot = options.repoRoot ?? path20.resolve(stateDir, "..");
|
|
3180
3405
|
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
3181
3406
|
const resolved = resolveNodeRuntime(
|
|
3182
3407
|
options.runtimeCommand ?? "node",
|
|
@@ -3244,30 +3469,19 @@ async function startBridge(options) {
|
|
|
3244
3469
|
...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
|
|
3245
3470
|
...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
|
|
3246
3471
|
};
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
stdio: ["ignore", logFd, logFd],
|
|
3261
|
-
env: bridgeEnv,
|
|
3262
|
-
windowsHide: true
|
|
3263
|
-
});
|
|
3264
|
-
child.unref();
|
|
3265
|
-
bridgePid = child.pid ?? null;
|
|
3266
|
-
}
|
|
3267
|
-
if (logFd != null) {
|
|
3268
|
-
fs13.closeSync(logFd);
|
|
3269
|
-
logFd = null;
|
|
3270
|
-
}
|
|
3472
|
+
const bridgePid = options.platform === "win32" ? startWindowsDetachedProcess(
|
|
3473
|
+
command,
|
|
3474
|
+
[bridgeScript],
|
|
3475
|
+
repoRoot,
|
|
3476
|
+
logPath,
|
|
3477
|
+
bridgeEnv
|
|
3478
|
+
) : startUnixDetachedProcess(
|
|
3479
|
+
command,
|
|
3480
|
+
[bridgeScript],
|
|
3481
|
+
repoRoot,
|
|
3482
|
+
logPath,
|
|
3483
|
+
bridgeEnv
|
|
3484
|
+
);
|
|
3271
3485
|
if (!bridgePid) {
|
|
3272
3486
|
throw new Error(`Failed to spawn bridge process for ${instanceId}`);
|
|
3273
3487
|
}
|
|
@@ -3281,18 +3495,23 @@ async function startBridge(options) {
|
|
|
3281
3495
|
saveBridgeState(stateDir, instanceId, state);
|
|
3282
3496
|
return state;
|
|
3283
3497
|
} catch (err) {
|
|
3284
|
-
if (logFd != null) {
|
|
3285
|
-
try {
|
|
3286
|
-
fs13.closeSync(logFd);
|
|
3287
|
-
} catch {
|
|
3288
|
-
}
|
|
3289
|
-
}
|
|
3290
3498
|
if (appServer?.managed) {
|
|
3291
|
-
|
|
3499
|
+
const shared = isAppServerUsedByOtherBridge(
|
|
3500
|
+
stateDir,
|
|
3501
|
+
instanceId,
|
|
3502
|
+
appServer
|
|
3503
|
+
);
|
|
3504
|
+
if (!shared) {
|
|
3505
|
+
await stopManagedAppServer(appServer, options.platform);
|
|
3506
|
+
}
|
|
3292
3507
|
}
|
|
3293
3508
|
throw err;
|
|
3294
3509
|
}
|
|
3295
3510
|
}
|
|
3511
|
+
|
|
3512
|
+
// src/engine/bridge-orchestrator.ts
|
|
3513
|
+
import * as fs23 from "fs";
|
|
3514
|
+
import * as path21 from "path";
|
|
3296
3515
|
async function stopBridge(options) {
|
|
3297
3516
|
const { instanceId, stateDir, platform } = options;
|
|
3298
3517
|
const state = loadBridgeState(stateDir, instanceId);
|
|
@@ -3315,59 +3534,28 @@ async function restartBridge(options) {
|
|
|
3315
3534
|
const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
|
|
3316
3535
|
const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
|
|
3317
3536
|
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
3318
|
-
const heartbeatPath =
|
|
3319
|
-
if (
|
|
3537
|
+
const heartbeatPath = path21.join(runtimeStateDir, "heartbeat.json");
|
|
3538
|
+
if (fs23.existsSync(heartbeatPath)) {
|
|
3320
3539
|
const startWait = Date.now();
|
|
3321
3540
|
while (Date.now() - startWait < drainTimeout) {
|
|
3322
3541
|
try {
|
|
3323
|
-
const hb = JSON.parse(
|
|
3542
|
+
const hb = JSON.parse(fs23.readFileSync(heartbeatPath, "utf-8"));
|
|
3324
3543
|
if (!hb.activeTurnId) break;
|
|
3325
3544
|
} catch {
|
|
3326
3545
|
break;
|
|
3327
3546
|
}
|
|
3328
|
-
await new Promise((
|
|
3547
|
+
await new Promise((resolve14) => setTimeout(resolve14, 1e3));
|
|
3329
3548
|
}
|
|
3330
3549
|
}
|
|
3331
3550
|
if (options.headless?.enabled && options.commsDir) {
|
|
3332
3551
|
const agentName = options.agentName ?? instanceId;
|
|
3333
|
-
cleanupHeadlessDispatch(
|
|
3552
|
+
cleanupHeadlessDispatch(path21.join(options.commsDir, "inbox"), agentName);
|
|
3334
3553
|
}
|
|
3335
3554
|
await stopBridge({ instanceId, stateDir, platform });
|
|
3336
|
-
|
|
3555
|
+
return startBridge({
|
|
3337
3556
|
...options,
|
|
3338
3557
|
processExistingMessages: true
|
|
3339
|
-
};
|
|
3340
|
-
return startBridge(restartOptions);
|
|
3341
|
-
}
|
|
3342
|
-
function rotateLog(logPath) {
|
|
3343
|
-
if (!fs13.existsSync(logPath)) return;
|
|
3344
|
-
try {
|
|
3345
|
-
const stats = fs13.statSync(logPath);
|
|
3346
|
-
if (stats.size === 0) return;
|
|
3347
|
-
const prevPath = `${logPath}.prev`;
|
|
3348
|
-
fs13.renameSync(logPath, prevPath);
|
|
3349
|
-
} catch {
|
|
3350
|
-
}
|
|
3351
|
-
}
|
|
3352
|
-
function getHeartbeatAge(stateDir, instanceId) {
|
|
3353
|
-
const state = loadBridgeState(stateDir, instanceId);
|
|
3354
|
-
const heartbeat = resolveHeartbeatTimestamp(state);
|
|
3355
|
-
if (!heartbeat) return null;
|
|
3356
|
-
const heartbeatTime = new Date(heartbeat).getTime();
|
|
3357
|
-
if (isNaN(heartbeatTime)) return null;
|
|
3358
|
-
return Math.floor((Date.now() - heartbeatTime) / 1e3);
|
|
3359
|
-
}
|
|
3360
|
-
function getBridgeHeartbeatTimestamp(stateDir, instanceId) {
|
|
3361
|
-
return resolveHeartbeatTimestamp(loadBridgeState(stateDir, instanceId));
|
|
3362
|
-
}
|
|
3363
|
-
function getBridgeStatus(stateDir, instanceId) {
|
|
3364
|
-
const state = loadBridgeState(stateDir, instanceId);
|
|
3365
|
-
if (!state) return "stopped";
|
|
3366
|
-
if (!isProcessAlive(state.pid)) {
|
|
3367
|
-
clearBridgeState(stateDir, instanceId);
|
|
3368
|
-
return "stale";
|
|
3369
|
-
}
|
|
3370
|
-
return "running";
|
|
3558
|
+
});
|
|
3371
3559
|
}
|
|
3372
3560
|
|
|
3373
3561
|
// src/commands/add.ts
|
|
@@ -3858,7 +4046,7 @@ async function statusCommand(args) {
|
|
|
3858
4046
|
}
|
|
3859
4047
|
|
|
3860
4048
|
// src/engine/rollback.ts
|
|
3861
|
-
import * as
|
|
4049
|
+
import * as fs24 from "fs";
|
|
3862
4050
|
async function rollbackRuntime(_instanceId, runtimeState) {
|
|
3863
4051
|
const errors = [];
|
|
3864
4052
|
const restoredFiles = [];
|
|
@@ -3887,7 +4075,7 @@ async function rollbackRuntime(_instanceId, runtimeState) {
|
|
|
3887
4075
|
};
|
|
3888
4076
|
}
|
|
3889
4077
|
function rollbackArtifact(artifact) {
|
|
3890
|
-
if (!
|
|
4078
|
+
if (!fs24.existsSync(artifact.path)) {
|
|
3891
4079
|
return { restored: false, error: `File not found: ${artifact.path}` };
|
|
3892
4080
|
}
|
|
3893
4081
|
switch (artifact.kind) {
|
|
@@ -3905,7 +4093,7 @@ function rollbackArtifact(artifact) {
|
|
|
3905
4093
|
}
|
|
3906
4094
|
}
|
|
3907
4095
|
function rollbackJsonPath(artifact) {
|
|
3908
|
-
const raw =
|
|
4096
|
+
const raw = fs24.readFileSync(artifact.path, "utf-8");
|
|
3909
4097
|
let config;
|
|
3910
4098
|
try {
|
|
3911
4099
|
config = JSON.parse(raw);
|
|
@@ -3931,18 +4119,18 @@ function rollbackJsonPath(artifact) {
|
|
|
3931
4119
|
cleanEmptyParents(config, artifact.selector);
|
|
3932
4120
|
}
|
|
3933
4121
|
const tmp = `${artifact.path}.tmp.${process.pid}`;
|
|
3934
|
-
|
|
3935
|
-
|
|
4122
|
+
fs24.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
4123
|
+
fs24.renameSync(tmp, artifact.path);
|
|
3936
4124
|
return { restored: true };
|
|
3937
4125
|
}
|
|
3938
4126
|
function rollbackTomlTable(artifact) {
|
|
3939
|
-
const content =
|
|
4127
|
+
const content = fs24.readFileSync(artifact.path, "utf-8");
|
|
3940
4128
|
const backup = artifact.backupPath ? readArtifactBackup(artifact.backupPath) : null;
|
|
3941
4129
|
if (backup?.kind === "toml-table" && backup.selector === artifact.selector) {
|
|
3942
4130
|
const nextContent = backup.existed ? replaceTomlTable(content, artifact.selector, backup.content ?? "") : removeTomlTable(content, artifact.selector);
|
|
3943
4131
|
const tmp2 = `${artifact.path}.tmp.${process.pid}`;
|
|
3944
|
-
|
|
3945
|
-
|
|
4132
|
+
fs24.writeFileSync(tmp2, nextContent, "utf-8");
|
|
4133
|
+
fs24.renameSync(tmp2, artifact.path);
|
|
3946
4134
|
return { restored: true };
|
|
3947
4135
|
}
|
|
3948
4136
|
if (!extractTomlTable(content, artifact.selector)) {
|
|
@@ -3952,13 +4140,13 @@ function rollbackTomlTable(artifact) {
|
|
|
3952
4140
|
};
|
|
3953
4141
|
}
|
|
3954
4142
|
const tmp = `${artifact.path}.tmp.${process.pid}`;
|
|
3955
|
-
|
|
3956
|
-
|
|
4143
|
+
fs24.writeFileSync(tmp, removeTomlTable(content, artifact.selector), "utf-8");
|
|
4144
|
+
fs24.renameSync(tmp, artifact.path);
|
|
3957
4145
|
return { restored: true };
|
|
3958
4146
|
}
|
|
3959
4147
|
function rollbackFile(artifact) {
|
|
3960
|
-
if (
|
|
3961
|
-
|
|
4148
|
+
if (fs24.existsSync(artifact.path)) {
|
|
4149
|
+
fs24.unlinkSync(artifact.path);
|
|
3962
4150
|
return { restored: true };
|
|
3963
4151
|
}
|
|
3964
4152
|
return { restored: false, error: `File not found: ${artifact.path}` };
|
|
@@ -4068,8 +4256,8 @@ async function removeCommand(args) {
|
|
|
4068
4256
|
};
|
|
4069
4257
|
}
|
|
4070
4258
|
const instanceId = resolved.instanceId;
|
|
4071
|
-
const
|
|
4072
|
-
if (!
|
|
4259
|
+
const instance = state.instances[instanceId];
|
|
4260
|
+
if (!instance?.installed) {
|
|
4073
4261
|
return {
|
|
4074
4262
|
ok: true,
|
|
4075
4263
|
command: "remove",
|
|
@@ -4081,7 +4269,7 @@ async function removeCommand(args) {
|
|
|
4081
4269
|
};
|
|
4082
4270
|
}
|
|
4083
4271
|
logHeader(`@hua-labs/tap remove ${instanceId}`);
|
|
4084
|
-
if (
|
|
4272
|
+
if (instance.bridge) {
|
|
4085
4273
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
4086
4274
|
const stopped = await stopBridge({
|
|
4087
4275
|
instanceId,
|
|
@@ -4094,7 +4282,7 @@ async function removeCommand(args) {
|
|
|
4094
4282
|
log(`No running bridge for ${instanceId}`);
|
|
4095
4283
|
}
|
|
4096
4284
|
}
|
|
4097
|
-
const result = await rollbackRuntime(instanceId,
|
|
4285
|
+
const result = await rollbackRuntime(instanceId, instance);
|
|
4098
4286
|
if (result.success) {
|
|
4099
4287
|
logSuccess(`Rolled back ${result.restoredCount} artifact(s)`);
|
|
4100
4288
|
for (const f of result.restoredFiles) logSuccess(`Restored: ${f}`);
|
|
@@ -4106,7 +4294,7 @@ async function removeCommand(args) {
|
|
|
4106
4294
|
ok: true,
|
|
4107
4295
|
command: "remove",
|
|
4108
4296
|
instanceId,
|
|
4109
|
-
runtime:
|
|
4297
|
+
runtime: instance.runtime,
|
|
4110
4298
|
code: "TAP_REMOVE_OK",
|
|
4111
4299
|
message: `${instanceId} removed successfully`,
|
|
4112
4300
|
warnings: [],
|
|
@@ -4121,7 +4309,7 @@ async function removeCommand(args) {
|
|
|
4121
4309
|
ok: false,
|
|
4122
4310
|
command: "remove",
|
|
4123
4311
|
instanceId,
|
|
4124
|
-
runtime:
|
|
4312
|
+
runtime: instance.runtime,
|
|
4125
4313
|
code: "TAP_ROLLBACK_FAILED",
|
|
4126
4314
|
message: "Rollback had errors. State preserved for retry.",
|
|
4127
4315
|
warnings: result.errors,
|
|
@@ -4130,7 +4318,7 @@ async function removeCommand(args) {
|
|
|
4130
4318
|
}
|
|
4131
4319
|
|
|
4132
4320
|
// src/commands/bridge.ts
|
|
4133
|
-
import * as
|
|
4321
|
+
import * as path22 from "path";
|
|
4134
4322
|
function formatAge(seconds) {
|
|
4135
4323
|
if (seconds < 60) return `${seconds}s ago`;
|
|
4136
4324
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
@@ -4147,6 +4335,7 @@ Subcommands:
|
|
|
4147
4335
|
stop Stop all running bridges
|
|
4148
4336
|
status Show bridge status for all instances
|
|
4149
4337
|
status <instance> Show bridge status for a specific instance
|
|
4338
|
+
watch Monitor bridges and auto-restart stuck/stale ones
|
|
4150
4339
|
|
|
4151
4340
|
Options:
|
|
4152
4341
|
--agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
|
|
@@ -4203,7 +4392,7 @@ function formatThreadSummary(threadId, cwd) {
|
|
|
4203
4392
|
return cwd ? `${threadId} (${cwd})` : threadId;
|
|
4204
4393
|
}
|
|
4205
4394
|
function normalizeComparablePath(value) {
|
|
4206
|
-
return
|
|
4395
|
+
return path22.resolve(value).replace(/\\/g, "/").toLowerCase();
|
|
4207
4396
|
}
|
|
4208
4397
|
function sameOptionalPath(left, right) {
|
|
4209
4398
|
if (!left || !right) {
|
|
@@ -4288,37 +4477,37 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4288
4477
|
};
|
|
4289
4478
|
}
|
|
4290
4479
|
const instanceId = resolved.instanceId;
|
|
4291
|
-
let
|
|
4292
|
-
if (!
|
|
4480
|
+
let instance = state.instances[instanceId];
|
|
4481
|
+
if (!instance?.installed) {
|
|
4293
4482
|
return {
|
|
4294
4483
|
ok: false,
|
|
4295
4484
|
command: "bridge",
|
|
4296
4485
|
instanceId,
|
|
4297
|
-
runtime:
|
|
4486
|
+
runtime: instance?.runtime,
|
|
4298
4487
|
code: "TAP_INSTANCE_NOT_FOUND",
|
|
4299
|
-
message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${
|
|
4488
|
+
message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance?.runtime ?? identifier}`,
|
|
4300
4489
|
warnings: [],
|
|
4301
4490
|
data: {}
|
|
4302
4491
|
};
|
|
4303
4492
|
}
|
|
4304
|
-
const adapter = getAdapter(
|
|
4493
|
+
const adapter = getAdapter(instance.runtime);
|
|
4305
4494
|
const mode = adapter.bridgeMode();
|
|
4306
4495
|
if (mode !== "app-server") {
|
|
4307
4496
|
return {
|
|
4308
4497
|
ok: true,
|
|
4309
4498
|
command: "bridge",
|
|
4310
4499
|
instanceId,
|
|
4311
|
-
runtime:
|
|
4500
|
+
runtime: instance.runtime,
|
|
4312
4501
|
code: "TAP_NO_OP",
|
|
4313
4502
|
message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
|
|
4314
4503
|
warnings: [],
|
|
4315
4504
|
data: { bridgeMode: mode }
|
|
4316
4505
|
};
|
|
4317
4506
|
}
|
|
4318
|
-
const resolvedAgentName = agentName ??
|
|
4319
|
-
if (agentName && agentName !==
|
|
4320
|
-
|
|
4321
|
-
const updatedState = updateInstanceState(state, instanceId,
|
|
4507
|
+
const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
|
|
4508
|
+
if (agentName && agentName !== instance.agentName) {
|
|
4509
|
+
instance = { ...instance, agentName };
|
|
4510
|
+
const updatedState = updateInstanceState(state, instanceId, instance);
|
|
4322
4511
|
saveState(repoRoot, updatedState);
|
|
4323
4512
|
state = updatedState;
|
|
4324
4513
|
}
|
|
@@ -4329,7 +4518,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4329
4518
|
ok: false,
|
|
4330
4519
|
command: "bridge",
|
|
4331
4520
|
instanceId,
|
|
4332
|
-
runtime:
|
|
4521
|
+
runtime: instance.runtime,
|
|
4333
4522
|
code: "TAP_BRIDGE_SCRIPT_MISSING",
|
|
4334
4523
|
message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
|
|
4335
4524
|
warnings: [],
|
|
@@ -4338,8 +4527,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4338
4527
|
}
|
|
4339
4528
|
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
4340
4529
|
const runtimeCommand = resolvedConfig.runtimeCommand;
|
|
4341
|
-
const manageAppServer =
|
|
4342
|
-
let effectivePort =
|
|
4530
|
+
const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
|
|
4531
|
+
let effectivePort = instance.port;
|
|
4343
4532
|
if (effectivePort == null && manageAppServer) {
|
|
4344
4533
|
effectivePort = await findNextAvailableAppServerPort(
|
|
4345
4534
|
state,
|
|
@@ -4347,8 +4536,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4347
4536
|
4501,
|
|
4348
4537
|
instanceId
|
|
4349
4538
|
);
|
|
4350
|
-
|
|
4351
|
-
const updatedState = updateInstanceState(state, instanceId,
|
|
4539
|
+
instance = { ...instance, port: effectivePort };
|
|
4540
|
+
const updatedState = updateInstanceState(state, instanceId, instance);
|
|
4352
4541
|
saveState(repoRoot, updatedState);
|
|
4353
4542
|
state = updatedState;
|
|
4354
4543
|
}
|
|
@@ -4364,19 +4553,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4364
4553
|
if (effectivePort != null) log(`Port: ${effectivePort}`);
|
|
4365
4554
|
if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
|
|
4366
4555
|
const noAuth = flags["no-auth"] === true;
|
|
4367
|
-
if (!manageAppServer &&
|
|
4556
|
+
if (!manageAppServer && instance.runtime === "codex") {
|
|
4368
4557
|
log("Auto server: disabled (--no-server)");
|
|
4369
4558
|
}
|
|
4370
4559
|
if (noAuth && manageAppServer) {
|
|
4371
4560
|
log("Auth gateway: disabled (--no-auth)");
|
|
4372
4561
|
}
|
|
4373
|
-
const willBeHeadless = flags["headless"] === true ||
|
|
4562
|
+
const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
|
|
4374
4563
|
if (willBeHeadless) {
|
|
4375
|
-
const role = (typeof flags["role"] === "string" ? flags["role"] : null) ??
|
|
4564
|
+
const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
|
|
4376
4565
|
log(`Headless: ${role}`);
|
|
4377
4566
|
}
|
|
4378
4567
|
try {
|
|
4379
|
-
if (!manageAppServer &&
|
|
4568
|
+
if (!manageAppServer && instance.runtime === "codex") {
|
|
4380
4569
|
log("Checking app-server health...");
|
|
4381
4570
|
const healthy = await checkAppServerHealth(appServerUrl);
|
|
4382
4571
|
if (healthy) {
|
|
@@ -4387,7 +4576,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4387
4576
|
ok: false,
|
|
4388
4577
|
command: "bridge",
|
|
4389
4578
|
instanceId,
|
|
4390
|
-
runtime:
|
|
4579
|
+
runtime: instance.runtime,
|
|
4391
4580
|
code: "TAP_BRIDGE_START_FAILED",
|
|
4392
4581
|
message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
|
|
4393
4582
|
warnings: [],
|
|
@@ -4401,7 +4590,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4401
4590
|
ok: false,
|
|
4402
4591
|
command: "bridge",
|
|
4403
4592
|
instanceId,
|
|
4404
|
-
runtime:
|
|
4593
|
+
runtime: instance.runtime,
|
|
4405
4594
|
code: "TAP_INVALID_ARGUMENT",
|
|
4406
4595
|
message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
|
|
4407
4596
|
warnings: [],
|
|
@@ -4434,7 +4623,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4434
4623
|
ok: false,
|
|
4435
4624
|
command: "bridge",
|
|
4436
4625
|
instanceId,
|
|
4437
|
-
runtime:
|
|
4626
|
+
runtime: instance.runtime,
|
|
4438
4627
|
code: "TAP_INVALID_ARGUMENT",
|
|
4439
4628
|
message: err instanceof Error ? err.message : String(err),
|
|
4440
4629
|
warnings: [],
|
|
@@ -4452,7 +4641,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4452
4641
|
ok: false,
|
|
4453
4642
|
command: "bridge",
|
|
4454
4643
|
instanceId,
|
|
4455
|
-
runtime:
|
|
4644
|
+
runtime: instance.runtime,
|
|
4456
4645
|
code: "TAP_INVALID_ARGUMENT",
|
|
4457
4646
|
message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
|
|
4458
4647
|
warnings: [],
|
|
@@ -4464,32 +4653,43 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4464
4653
|
role: roleArg ?? "reviewer",
|
|
4465
4654
|
maxRounds: 5,
|
|
4466
4655
|
qualitySeverityFloor: "high"
|
|
4467
|
-
} :
|
|
4468
|
-
const
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4656
|
+
} : instance.headless;
|
|
4657
|
+
const previousWarmup = process.env.TAP_COLD_START_WARMUP;
|
|
4658
|
+
process.env.TAP_COLD_START_WARMUP = "true";
|
|
4659
|
+
let bridge;
|
|
4660
|
+
try {
|
|
4661
|
+
bridge = await startBridge({
|
|
4662
|
+
instanceId,
|
|
4663
|
+
runtime: instance.runtime,
|
|
4664
|
+
stateDir: ctx.stateDir,
|
|
4665
|
+
commsDir: ctx.commsDir,
|
|
4666
|
+
bridgeScript,
|
|
4667
|
+
platform: ctx.platform,
|
|
4668
|
+
agentName: resolvedAgentName,
|
|
4669
|
+
runtimeCommand,
|
|
4670
|
+
appServerUrl,
|
|
4671
|
+
repoRoot,
|
|
4672
|
+
port: effectivePort ?? void 0,
|
|
4673
|
+
manageAppServer,
|
|
4674
|
+
noAuth,
|
|
4675
|
+
headless,
|
|
4676
|
+
busyMode,
|
|
4677
|
+
pollSeconds,
|
|
4678
|
+
reconnectSeconds,
|
|
4679
|
+
messageLookbackMinutes,
|
|
4680
|
+
threadId,
|
|
4681
|
+
ephemeral,
|
|
4682
|
+
processExistingMessages
|
|
4683
|
+
});
|
|
4684
|
+
} finally {
|
|
4685
|
+
if (previousWarmup === void 0) {
|
|
4686
|
+
delete process.env.TAP_COLD_START_WARMUP;
|
|
4687
|
+
} else {
|
|
4688
|
+
process.env.TAP_COLD_START_WARMUP = previousWarmup;
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4491
4691
|
logSuccess(`Bridge started (PID: ${bridge.pid})`);
|
|
4492
|
-
log(`Log: ${
|
|
4692
|
+
log(`Log: ${path22.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
|
|
4493
4693
|
if (bridge.appServer) {
|
|
4494
4694
|
log(`App server: ${formatAppServerState(bridge.appServer)}`);
|
|
4495
4695
|
if (bridge.appServer.logPath) {
|
|
@@ -4508,14 +4708,14 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4508
4708
|
log(`TUI connect: ${bridge.appServer.url}`);
|
|
4509
4709
|
}
|
|
4510
4710
|
}
|
|
4511
|
-
const updated = { ...
|
|
4711
|
+
const updated = { ...instance, bridge, manageAppServer, noAuth };
|
|
4512
4712
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
4513
4713
|
saveState(repoRoot, newState);
|
|
4514
4714
|
return {
|
|
4515
4715
|
ok: true,
|
|
4516
4716
|
command: "bridge",
|
|
4517
4717
|
instanceId,
|
|
4518
|
-
runtime:
|
|
4718
|
+
runtime: instance.runtime,
|
|
4519
4719
|
code: "TAP_BRIDGE_START_OK",
|
|
4520
4720
|
message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
|
|
4521
4721
|
warnings: [],
|
|
@@ -4528,7 +4728,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4528
4728
|
ok: false,
|
|
4529
4729
|
command: "bridge",
|
|
4530
4730
|
instanceId,
|
|
4531
|
-
runtime:
|
|
4731
|
+
runtime: instance.runtime,
|
|
4532
4732
|
code: "TAP_BRIDGE_START_FAILED",
|
|
4533
4733
|
message: msg,
|
|
4534
4734
|
warnings: [],
|
|
@@ -4630,11 +4830,11 @@ async function bridgeStopOne(identifier) {
|
|
|
4630
4830
|
}
|
|
4631
4831
|
const instanceId = resolved.instanceId;
|
|
4632
4832
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
4633
|
-
const
|
|
4833
|
+
const instance = state.instances[instanceId];
|
|
4634
4834
|
const bridgeState = loadCurrentBridgeState(
|
|
4635
4835
|
ctx.stateDir,
|
|
4636
4836
|
instanceId,
|
|
4637
|
-
|
|
4837
|
+
instance?.bridge
|
|
4638
4838
|
);
|
|
4639
4839
|
const appServer = bridgeState?.appServer ?? null;
|
|
4640
4840
|
logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
|
|
@@ -4679,11 +4879,17 @@ async function bridgeStopOne(identifier) {
|
|
|
4679
4879
|
logSuccess(
|
|
4680
4880
|
`Managed app-server stopped (PID: ${appServer.pid ?? "-"}${gatewayNote})`
|
|
4681
4881
|
);
|
|
4882
|
+
const released = await waitForPortRelease(appServer.url, 5e3);
|
|
4883
|
+
if (!released) {
|
|
4884
|
+
log(
|
|
4885
|
+
`Warning: port for ${appServer.url} still in use after stop \u2014 next start may need a different port`
|
|
4886
|
+
);
|
|
4887
|
+
}
|
|
4682
4888
|
}
|
|
4683
4889
|
}
|
|
4684
4890
|
}
|
|
4685
|
-
if (
|
|
4686
|
-
const updated = { ...
|
|
4891
|
+
if (instance) {
|
|
4892
|
+
const updated = { ...instance, bridge: null };
|
|
4687
4893
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
4688
4894
|
saveState(repoRoot, newState);
|
|
4689
4895
|
}
|
|
@@ -4755,22 +4961,29 @@ async function bridgeStopAll() {
|
|
|
4755
4961
|
logSuccess(`Stopped bridge for ${instanceId}`);
|
|
4756
4962
|
stopped.push(instanceId);
|
|
4757
4963
|
}
|
|
4758
|
-
const
|
|
4759
|
-
if (
|
|
4760
|
-
state.instances[instanceId] = { ...
|
|
4964
|
+
const instance = state.instances[instanceId];
|
|
4965
|
+
if (instance?.bridge) {
|
|
4966
|
+
state.instances[instanceId] = { ...instance, bridge: null };
|
|
4761
4967
|
stateChanged = true;
|
|
4762
4968
|
}
|
|
4763
4969
|
}
|
|
4764
4970
|
const stoppedAppServers = [];
|
|
4971
|
+
const releasePorts = [];
|
|
4765
4972
|
for (const appServer of managedAppServers.values()) {
|
|
4766
4973
|
if (await stopManagedAppServer(appServer, ctx.platform)) {
|
|
4767
4974
|
stoppedAppServers.push(appServer.pid);
|
|
4975
|
+
releasePorts.push(appServer.url);
|
|
4768
4976
|
const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID ${appServer.auth.gatewayPid}` : "";
|
|
4769
4977
|
logSuccess(
|
|
4770
4978
|
`Stopped app-server PID ${appServer.pid} (${appServer.url}${gatewayNote})`
|
|
4771
4979
|
);
|
|
4772
4980
|
}
|
|
4773
4981
|
}
|
|
4982
|
+
if (releasePorts.length > 0) {
|
|
4983
|
+
await Promise.all(
|
|
4984
|
+
releasePorts.map((url) => waitForPortRelease(url, 5e3))
|
|
4985
|
+
);
|
|
4986
|
+
}
|
|
4774
4987
|
if (stateChanged) {
|
|
4775
4988
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4776
4989
|
saveState(repoRoot, state);
|
|
@@ -4786,6 +4999,124 @@ async function bridgeStopAll() {
|
|
|
4786
4999
|
data: { stopped, stoppedAppServers }
|
|
4787
5000
|
};
|
|
4788
5001
|
}
|
|
5002
|
+
async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
|
|
5003
|
+
const repoRoot = findRepoRoot();
|
|
5004
|
+
const state = loadState(repoRoot);
|
|
5005
|
+
if (!state) {
|
|
5006
|
+
return {
|
|
5007
|
+
ok: false,
|
|
5008
|
+
command: "bridge",
|
|
5009
|
+
code: "TAP_NOT_INITIALIZED",
|
|
5010
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
5011
|
+
warnings: [],
|
|
5012
|
+
data: {}
|
|
5013
|
+
};
|
|
5014
|
+
}
|
|
5015
|
+
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
5016
|
+
const stateDir = resolvedCfg.stateDir;
|
|
5017
|
+
const instanceIds = Object.keys(state.instances);
|
|
5018
|
+
logHeader("@hua-labs/tap bridge watch");
|
|
5019
|
+
log(
|
|
5020
|
+
`Checking ${instanceIds.length} instance(s), stuck threshold: ${stuckThresholdSeconds}s`
|
|
5021
|
+
);
|
|
5022
|
+
const restarted = [];
|
|
5023
|
+
const cleaned = [];
|
|
5024
|
+
const healthy = [];
|
|
5025
|
+
const warnings = [];
|
|
5026
|
+
for (const instanceId of instanceIds) {
|
|
5027
|
+
const inst = state.instances[instanceId];
|
|
5028
|
+
if (!inst?.installed || inst.bridgeMode !== "app-server") continue;
|
|
5029
|
+
const status = getBridgeStatus(stateDir, instanceId);
|
|
5030
|
+
if (status === "stale") {
|
|
5031
|
+
log(`${instanceId}: stale (process dead) \u2014 cleaning up`);
|
|
5032
|
+
cleaned.push(instanceId);
|
|
5033
|
+
continue;
|
|
5034
|
+
}
|
|
5035
|
+
if (status === "stopped") {
|
|
5036
|
+
log(`${instanceId}: stopped`);
|
|
5037
|
+
continue;
|
|
5038
|
+
}
|
|
5039
|
+
if (isTurnStuck(stateDir, instanceId, stuckThresholdSeconds)) {
|
|
5040
|
+
const turnInfo = getTurnInfo(stateDir, instanceId, stuckThresholdSeconds);
|
|
5041
|
+
const ageStr = turnInfo?.ageSeconds != null ? formatAge(turnInfo.ageSeconds) : "?";
|
|
5042
|
+
log(
|
|
5043
|
+
`${instanceId}: \u26A0 STUCK turn ${turnInfo?.activeTurnId?.slice(0, 8)}... (${ageStr}) \u2014 restarting`
|
|
5044
|
+
);
|
|
5045
|
+
const adapter = getAdapter(inst.runtime);
|
|
5046
|
+
const ctx = {
|
|
5047
|
+
...createAdapterContext(state.commsDir, repoRoot),
|
|
5048
|
+
instanceId
|
|
5049
|
+
};
|
|
5050
|
+
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
5051
|
+
if (!bridgeScript) {
|
|
5052
|
+
warnings.push(
|
|
5053
|
+
`${instanceId}: cannot restart \u2014 bridge script not found`
|
|
5054
|
+
);
|
|
5055
|
+
continue;
|
|
5056
|
+
}
|
|
5057
|
+
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
5058
|
+
const { manageAppServer, noAuth } = inferRestartMode(bridgeState, {});
|
|
5059
|
+
const previousWarmup = process.env.TAP_COLD_START_WARMUP;
|
|
5060
|
+
process.env.TAP_COLD_START_WARMUP = "true";
|
|
5061
|
+
try {
|
|
5062
|
+
const newBridgeState = await restartBridge({
|
|
5063
|
+
instanceId,
|
|
5064
|
+
runtime: inst.runtime,
|
|
5065
|
+
stateDir: ctx.stateDir,
|
|
5066
|
+
commsDir: ctx.commsDir,
|
|
5067
|
+
bridgeScript,
|
|
5068
|
+
platform: ctx.platform,
|
|
5069
|
+
agentName: inst.agentName ?? void 0,
|
|
5070
|
+
runtimeCommand: resolvedCfg.runtimeCommand,
|
|
5071
|
+
appServerUrl: resolvedCfg.appServerUrl,
|
|
5072
|
+
repoRoot,
|
|
5073
|
+
port: inst.port ?? void 0,
|
|
5074
|
+
headless: inst.headless,
|
|
5075
|
+
drainTimeoutSeconds: 30,
|
|
5076
|
+
manageAppServer,
|
|
5077
|
+
noAuth
|
|
5078
|
+
});
|
|
5079
|
+
const updatedInst = { ...inst, bridge: newBridgeState };
|
|
5080
|
+
const updatedState = updateInstanceState(
|
|
5081
|
+
state,
|
|
5082
|
+
instanceId,
|
|
5083
|
+
updatedInst
|
|
5084
|
+
);
|
|
5085
|
+
saveState(repoRoot, updatedState);
|
|
5086
|
+
restarted.push(instanceId);
|
|
5087
|
+
logSuccess(`${instanceId}: restarted`);
|
|
5088
|
+
} catch (err) {
|
|
5089
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5090
|
+
warnings.push(`${instanceId}: restart failed \u2014 ${msg}`);
|
|
5091
|
+
logError(`${instanceId}: restart failed \u2014 ${msg}`);
|
|
5092
|
+
} finally {
|
|
5093
|
+
if (previousWarmup === void 0) {
|
|
5094
|
+
delete process.env.TAP_COLD_START_WARMUP;
|
|
5095
|
+
} else {
|
|
5096
|
+
process.env.TAP_COLD_START_WARMUP = previousWarmup;
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
} else {
|
|
5100
|
+
healthy.push(instanceId);
|
|
5101
|
+
log(`${instanceId}: healthy`);
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
const message = [
|
|
5105
|
+
restarted.length > 0 ? `Restarted: ${restarted.join(", ")}` : null,
|
|
5106
|
+
cleaned.length > 0 ? `Cleaned stale: ${cleaned.join(", ")}` : null,
|
|
5107
|
+
healthy.length > 0 ? `Healthy: ${healthy.join(", ")}` : null
|
|
5108
|
+
].filter(Boolean).join(". ") || "No app-server bridges found";
|
|
5109
|
+
log("");
|
|
5110
|
+
log(message);
|
|
5111
|
+
return {
|
|
5112
|
+
ok: true,
|
|
5113
|
+
command: "bridge",
|
|
5114
|
+
code: restarted.length > 0 ? "TAP_BRIDGE_WATCH_RESTARTED" : "TAP_BRIDGE_WATCH_OK",
|
|
5115
|
+
message,
|
|
5116
|
+
warnings,
|
|
5117
|
+
data: { restarted, cleaned, healthy }
|
|
5118
|
+
};
|
|
5119
|
+
}
|
|
4789
5120
|
function bridgeStatusAll() {
|
|
4790
5121
|
const repoRoot = findRepoRoot();
|
|
4791
5122
|
const state = loadState(repoRoot);
|
|
@@ -4866,6 +5197,19 @@ function bridgeStatusAll() {
|
|
|
4866
5197
|
` Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
|
|
4867
5198
|
);
|
|
4868
5199
|
}
|
|
5200
|
+
const turnInfo = getTurnInfo(stateDir, instanceId);
|
|
5201
|
+
if (turnInfo?.activeTurnId) {
|
|
5202
|
+
const ageStr2 = turnInfo.ageSeconds != null ? formatAge(turnInfo.ageSeconds) : "?";
|
|
5203
|
+
if (turnInfo.stuck) {
|
|
5204
|
+
log(
|
|
5205
|
+
` \u26A0 STUCK: turn ${turnInfo.activeTurnId.slice(0, 8)}... active ${ageStr2} (threshold: 5m)`
|
|
5206
|
+
);
|
|
5207
|
+
} else {
|
|
5208
|
+
log(
|
|
5209
|
+
` Turn: ${turnInfo.activeTurnId.slice(0, 8)}... active ${ageStr2}`
|
|
5210
|
+
);
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
4869
5213
|
bridges[instanceId] = {
|
|
4870
5214
|
status,
|
|
4871
5215
|
runtime: inst.runtime,
|
|
@@ -4984,7 +5328,7 @@ function bridgeStatusOne(identifier) {
|
|
|
4984
5328
|
);
|
|
4985
5329
|
}
|
|
4986
5330
|
log(
|
|
4987
|
-
`Log: ${
|
|
5331
|
+
`Log: ${path22.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
|
|
4988
5332
|
);
|
|
4989
5333
|
if (bridgeState.appServer) {
|
|
4990
5334
|
log(`App server: ${bridgeState.appServer.url}`);
|
|
@@ -5102,7 +5446,7 @@ async function bridgeRestart(identifier, flags) {
|
|
|
5102
5446
|
ok: false,
|
|
5103
5447
|
command: "bridge",
|
|
5104
5448
|
instanceId,
|
|
5105
|
-
runtime:
|
|
5449
|
+
runtime: inst.runtime,
|
|
5106
5450
|
code: "TAP_INVALID_ARGUMENT",
|
|
5107
5451
|
message: err instanceof Error ? err.message : String(err),
|
|
5108
5452
|
warnings: [],
|
|
@@ -5124,23 +5468,34 @@ async function bridgeRestart(identifier, flags) {
|
|
|
5124
5468
|
noAuth: inst.noAuth
|
|
5125
5469
|
}
|
|
5126
5470
|
);
|
|
5127
|
-
const
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5471
|
+
const previousColdStartWarmup = process.env.TAP_COLD_START_WARMUP;
|
|
5472
|
+
process.env.TAP_COLD_START_WARMUP = "true";
|
|
5473
|
+
let bridge;
|
|
5474
|
+
try {
|
|
5475
|
+
bridge = await restartBridge({
|
|
5476
|
+
instanceId,
|
|
5477
|
+
runtime: inst.runtime,
|
|
5478
|
+
stateDir: ctx.stateDir,
|
|
5479
|
+
commsDir: ctx.commsDir,
|
|
5480
|
+
bridgeScript,
|
|
5481
|
+
platform: ctx.platform,
|
|
5482
|
+
agentName: inst.agentName ?? void 0,
|
|
5483
|
+
runtimeCommand: resolvedConfig.runtimeCommand,
|
|
5484
|
+
appServerUrl: resolvedConfig.appServerUrl,
|
|
5485
|
+
repoRoot,
|
|
5486
|
+
port: inst.port ?? void 0,
|
|
5487
|
+
headless: inst.headless,
|
|
5488
|
+
drainTimeoutSeconds: drainTimeout,
|
|
5489
|
+
manageAppServer,
|
|
5490
|
+
noAuth
|
|
5491
|
+
});
|
|
5492
|
+
} finally {
|
|
5493
|
+
if (previousColdStartWarmup === void 0) {
|
|
5494
|
+
delete process.env.TAP_COLD_START_WARMUP;
|
|
5495
|
+
} else {
|
|
5496
|
+
process.env.TAP_COLD_START_WARMUP = previousColdStartWarmup;
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5144
5499
|
logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
|
|
5145
5500
|
const updated = { ...inst, bridge, manageAppServer, noAuth };
|
|
5146
5501
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
@@ -5227,6 +5582,13 @@ async function bridgeCommand(args) {
|
|
|
5227
5582
|
}
|
|
5228
5583
|
return bridgeStatusAll();
|
|
5229
5584
|
}
|
|
5585
|
+
case "watch": {
|
|
5586
|
+
const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : void 0;
|
|
5587
|
+
const interval = intervalStr ? parseInt(intervalStr, 10) : 30;
|
|
5588
|
+
const stuckThresholdStr = typeof flags["stuck-threshold"] === "string" ? flags["stuck-threshold"] : void 0;
|
|
5589
|
+
const stuckThreshold = stuckThresholdStr ? parseInt(stuckThresholdStr, 10) : 300;
|
|
5590
|
+
return bridgeWatch(interval, stuckThreshold);
|
|
5591
|
+
}
|
|
5230
5592
|
case "restart": {
|
|
5231
5593
|
if (!identifierArg) {
|
|
5232
5594
|
return {
|
|
@@ -5253,14 +5615,14 @@ async function bridgeCommand(args) {
|
|
|
5253
5615
|
}
|
|
5254
5616
|
|
|
5255
5617
|
// src/engine/dashboard.ts
|
|
5256
|
-
import * as
|
|
5257
|
-
import * as
|
|
5618
|
+
import * as fs25 from "fs";
|
|
5619
|
+
import * as path23 from "path";
|
|
5258
5620
|
import { execSync as execSync4 } from "child_process";
|
|
5259
5621
|
function collectAgents(commsDir) {
|
|
5260
|
-
const heartbeatsPath =
|
|
5261
|
-
if (!
|
|
5622
|
+
const heartbeatsPath = path23.join(commsDir, "heartbeats.json");
|
|
5623
|
+
if (!fs25.existsSync(heartbeatsPath)) return [];
|
|
5262
5624
|
try {
|
|
5263
|
-
const raw =
|
|
5625
|
+
const raw = fs25.readFileSync(heartbeatsPath, "utf-8");
|
|
5264
5626
|
const data = JSON.parse(raw);
|
|
5265
5627
|
return Object.entries(data).map(([name, info]) => ({
|
|
5266
5628
|
name: info.agent ?? name,
|
|
@@ -5296,22 +5658,22 @@ function collectBridges(repoRoot) {
|
|
|
5296
5658
|
});
|
|
5297
5659
|
}
|
|
5298
5660
|
}
|
|
5299
|
-
const tmpDir =
|
|
5300
|
-
if (
|
|
5661
|
+
const tmpDir = path23.join(repoRoot, ".tmp");
|
|
5662
|
+
if (fs25.existsSync(tmpDir)) {
|
|
5301
5663
|
try {
|
|
5302
|
-
const dirs =
|
|
5664
|
+
const dirs = fs25.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
|
|
5303
5665
|
for (const dir of dirs) {
|
|
5304
|
-
const daemonPath =
|
|
5305
|
-
if (!
|
|
5666
|
+
const daemonPath = path23.join(tmpDir, dir, "bridge-daemon.json");
|
|
5667
|
+
if (!fs25.existsSync(daemonPath)) continue;
|
|
5306
5668
|
try {
|
|
5307
|
-
const raw =
|
|
5669
|
+
const raw = fs25.readFileSync(daemonPath, "utf-8");
|
|
5308
5670
|
const daemon = JSON.parse(raw);
|
|
5309
5671
|
const alreadyCovered = bridges.some(
|
|
5310
5672
|
(b) => b.pid === daemon.pid && b.pid !== null
|
|
5311
5673
|
);
|
|
5312
5674
|
if (alreadyCovered) continue;
|
|
5313
|
-
const agentFile =
|
|
5314
|
-
const agentName =
|
|
5675
|
+
const agentFile = path23.join(tmpDir, dir, "agent-name.txt");
|
|
5676
|
+
const agentName = fs25.existsSync(agentFile) ? fs25.readFileSync(agentFile, "utf-8").trim() : dir;
|
|
5315
5677
|
const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
|
|
5316
5678
|
const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
|
|
5317
5679
|
const port = portMatch ? parseInt(portMatch[1], 10) : null;
|
|
@@ -5517,7 +5879,7 @@ async function downCommand(args) {
|
|
|
5517
5879
|
}
|
|
5518
5880
|
|
|
5519
5881
|
// src/commands/serve.ts
|
|
5520
|
-
import * as
|
|
5882
|
+
import * as path24 from "path";
|
|
5521
5883
|
import { spawn as spawn2 } from "child_process";
|
|
5522
5884
|
var SERVE_HELP = `
|
|
5523
5885
|
Usage:
|
|
@@ -5551,7 +5913,7 @@ async function serveCommand(args) {
|
|
|
5551
5913
|
let commsDir;
|
|
5552
5914
|
const commsDirIdx = args.indexOf("--comms-dir");
|
|
5553
5915
|
if (commsDirIdx !== -1 && args[commsDirIdx + 1]) {
|
|
5554
|
-
commsDir =
|
|
5916
|
+
commsDir = path24.resolve(args[commsDirIdx + 1]);
|
|
5555
5917
|
}
|
|
5556
5918
|
if (!commsDir && process.env.TAP_COMMS_DIR) {
|
|
5557
5919
|
commsDir = process.env.TAP_COMMS_DIR;
|
|
@@ -5594,9 +5956,9 @@ async function serveCommand(args) {
|
|
|
5594
5956
|
TAP_COMMS_DIR: commsDir
|
|
5595
5957
|
}
|
|
5596
5958
|
});
|
|
5597
|
-
return new Promise((
|
|
5959
|
+
return new Promise((resolve14) => {
|
|
5598
5960
|
child.on("error", (err) => {
|
|
5599
|
-
|
|
5961
|
+
resolve14({
|
|
5600
5962
|
ok: false,
|
|
5601
5963
|
command: "serve",
|
|
5602
5964
|
code: "TAP_INTERNAL_ERROR",
|
|
@@ -5606,7 +5968,7 @@ async function serveCommand(args) {
|
|
|
5606
5968
|
});
|
|
5607
5969
|
});
|
|
5608
5970
|
child.on("exit", (code) => {
|
|
5609
|
-
|
|
5971
|
+
resolve14({
|
|
5610
5972
|
ok: code === 0,
|
|
5611
5973
|
command: "serve",
|
|
5612
5974
|
code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
|
|
@@ -5619,8 +5981,8 @@ async function serveCommand(args) {
|
|
|
5619
5981
|
}
|
|
5620
5982
|
|
|
5621
5983
|
// src/commands/init-worktree.ts
|
|
5622
|
-
import * as
|
|
5623
|
-
import * as
|
|
5984
|
+
import * as fs26 from "fs";
|
|
5985
|
+
import * as path25 from "path";
|
|
5624
5986
|
import { execSync as execSync5 } from "child_process";
|
|
5625
5987
|
var INIT_WORKTREE_HELP = `
|
|
5626
5988
|
Usage:
|
|
@@ -5657,7 +6019,7 @@ function run(cmd, opts) {
|
|
|
5657
6019
|
}
|
|
5658
6020
|
}
|
|
5659
6021
|
function toAbsolute(p) {
|
|
5660
|
-
const resolved =
|
|
6022
|
+
const resolved = path25.resolve(p);
|
|
5661
6023
|
return resolved.replace(/\\/g, "/");
|
|
5662
6024
|
}
|
|
5663
6025
|
function probeBun(candidate) {
|
|
@@ -5688,18 +6050,18 @@ function findBun() {
|
|
|
5688
6050
|
}
|
|
5689
6051
|
}
|
|
5690
6052
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
5691
|
-
const bunHome =
|
|
6053
|
+
const bunHome = path25.join(
|
|
5692
6054
|
home,
|
|
5693
6055
|
".bun",
|
|
5694
6056
|
"bin",
|
|
5695
6057
|
process.platform === "win32" ? "bun.exe" : "bun"
|
|
5696
6058
|
);
|
|
5697
|
-
if (
|
|
6059
|
+
if (fs26.existsSync(bunHome) && probeBun(bunHome)) return bunHome;
|
|
5698
6060
|
return null;
|
|
5699
6061
|
}
|
|
5700
6062
|
function step1CreateWorktree(opts) {
|
|
5701
6063
|
log("Step 1/9: Creating worktree...");
|
|
5702
|
-
if (
|
|
6064
|
+
if (fs26.existsSync(opts.worktreePath)) {
|
|
5703
6065
|
logWarn(`Directory already exists: ${opts.worktreePath}`);
|
|
5704
6066
|
try {
|
|
5705
6067
|
run("git rev-parse --git-dir", { cwd: opts.worktreePath });
|
|
@@ -5761,22 +6123,22 @@ function step2MergeMain(opts, warnings) {
|
|
|
5761
6123
|
}
|
|
5762
6124
|
function step3CopyPermissions(opts, warnings) {
|
|
5763
6125
|
log("Step 3/9: Copying permissions...");
|
|
5764
|
-
const srcSettings =
|
|
6126
|
+
const srcSettings = path25.join(
|
|
5765
6127
|
opts.repoRoot,
|
|
5766
6128
|
".claude",
|
|
5767
6129
|
"settings.local.json"
|
|
5768
6130
|
);
|
|
5769
|
-
const destDir =
|
|
5770
|
-
const destSettings =
|
|
5771
|
-
if (!
|
|
6131
|
+
const destDir = path25.join(opts.worktreePath, ".claude");
|
|
6132
|
+
const destSettings = path25.join(destDir, "settings.local.json");
|
|
6133
|
+
if (!fs26.existsSync(srcSettings)) {
|
|
5772
6134
|
warn(
|
|
5773
6135
|
warnings,
|
|
5774
6136
|
"No .claude/settings.local.json found in main repo. Skipping."
|
|
5775
6137
|
);
|
|
5776
6138
|
return;
|
|
5777
6139
|
}
|
|
5778
|
-
|
|
5779
|
-
|
|
6140
|
+
fs26.mkdirSync(destDir, { recursive: true });
|
|
6141
|
+
fs26.copyFileSync(srcSettings, destSettings);
|
|
5780
6142
|
logSuccess("Copied settings.local.json");
|
|
5781
6143
|
try {
|
|
5782
6144
|
run("git update-index --skip-worktree .claude/settings.local.json", {
|
|
@@ -5801,7 +6163,7 @@ function step4GenerateMcpJson(opts, warnings) {
|
|
|
5801
6163
|
const wtAbs = toAbsolute(opts.worktreePath);
|
|
5802
6164
|
const bunAbs = toAbsolute(bunPath);
|
|
5803
6165
|
const commsAbs = toAbsolute(opts.commsDir);
|
|
5804
|
-
const channelEntry =
|
|
6166
|
+
const channelEntry = path25.join(
|
|
5805
6167
|
wtAbs,
|
|
5806
6168
|
"packages/tap-plugin/channels/tap-comms.ts"
|
|
5807
6169
|
);
|
|
@@ -5818,8 +6180,8 @@ function step4GenerateMcpJson(opts, warnings) {
|
|
|
5818
6180
|
}
|
|
5819
6181
|
}
|
|
5820
6182
|
};
|
|
5821
|
-
const mcpPath =
|
|
5822
|
-
|
|
6183
|
+
const mcpPath = path25.join(opts.worktreePath, ".mcp.json");
|
|
6184
|
+
fs26.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
5823
6185
|
logSuccess(`.mcp.json generated (absolute paths + cwd)`);
|
|
5824
6186
|
log(` bun: ${bunAbs}`);
|
|
5825
6187
|
log(` comms: ${commsAbs}`);
|
|
@@ -5857,16 +6219,16 @@ function step6BuildEslintPlugin(opts, warnings) {
|
|
|
5857
6219
|
}
|
|
5858
6220
|
function step7VerifyComms(opts, warnings) {
|
|
5859
6221
|
log("Step 7/9: Verifying comms directory...");
|
|
5860
|
-
if (!
|
|
6222
|
+
if (!fs26.existsSync(opts.commsDir)) {
|
|
5861
6223
|
warn(warnings, `Comms directory not found: ${opts.commsDir}`);
|
|
5862
6224
|
warn(warnings, "Create it or run: npx @hua-labs/tap init");
|
|
5863
6225
|
return;
|
|
5864
6226
|
}
|
|
5865
6227
|
const requiredDirs = ["inbox", "findings", "reviews", "letters"];
|
|
5866
6228
|
for (const dir of requiredDirs) {
|
|
5867
|
-
const dirPath =
|
|
5868
|
-
if (!
|
|
5869
|
-
|
|
6229
|
+
const dirPath = path25.join(opts.commsDir, dir);
|
|
6230
|
+
if (!fs26.existsSync(dirPath)) {
|
|
6231
|
+
fs26.mkdirSync(dirPath, { recursive: true });
|
|
5870
6232
|
logSuccess(`Created ${dir}/`);
|
|
5871
6233
|
}
|
|
5872
6234
|
}
|
|
@@ -5925,17 +6287,17 @@ async function initWorktreeCommand(args) {
|
|
|
5925
6287
|
}
|
|
5926
6288
|
const repoRoot = findRepoRoot();
|
|
5927
6289
|
const { config } = resolveConfig({}, repoRoot);
|
|
5928
|
-
const branch = typeof flags["branch"] === "string" ? flags["branch"] :
|
|
6290
|
+
const branch = typeof flags["branch"] === "string" ? flags["branch"] : path25.basename(path25.resolve(worktreePath));
|
|
5929
6291
|
const base = typeof flags["base"] === "string" ? flags["base"] : "origin/main";
|
|
5930
6292
|
const mission = typeof flags["mission"] === "string" ? flags["mission"] : void 0;
|
|
5931
6293
|
const commsDir = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : config.commsDir;
|
|
5932
6294
|
const skipInstall = flags["skip-install"] === true;
|
|
5933
6295
|
const opts = {
|
|
5934
|
-
worktreePath:
|
|
6296
|
+
worktreePath: path25.resolve(worktreePath),
|
|
5935
6297
|
branch,
|
|
5936
6298
|
base,
|
|
5937
6299
|
mission,
|
|
5938
|
-
commsDir:
|
|
6300
|
+
commsDir: path25.resolve(commsDir),
|
|
5939
6301
|
skipInstall,
|
|
5940
6302
|
repoRoot
|
|
5941
6303
|
};
|
|
@@ -6156,18 +6518,18 @@ async function dashboardCommand(args) {
|
|
|
6156
6518
|
|
|
6157
6519
|
// src/commands/doctor.ts
|
|
6158
6520
|
import {
|
|
6159
|
-
existsSync as
|
|
6160
|
-
mkdirSync as
|
|
6161
|
-
readdirSync as
|
|
6162
|
-
readFileSync as
|
|
6163
|
-
renameSync as
|
|
6164
|
-
statSync as
|
|
6165
|
-
unlinkSync as
|
|
6166
|
-
writeFileSync as
|
|
6521
|
+
existsSync as existsSync23,
|
|
6522
|
+
mkdirSync as mkdirSync13,
|
|
6523
|
+
readdirSync as readdirSync6,
|
|
6524
|
+
readFileSync as readFileSync18,
|
|
6525
|
+
renameSync as renameSync11,
|
|
6526
|
+
statSync as statSync3,
|
|
6527
|
+
unlinkSync as unlinkSync6,
|
|
6528
|
+
writeFileSync as writeFileSync13
|
|
6167
6529
|
} from "fs";
|
|
6168
6530
|
import { homedir as homedir3 } from "os";
|
|
6169
|
-
import { spawnSync as
|
|
6170
|
-
import { dirname as
|
|
6531
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
6532
|
+
import { dirname as dirname15, join as join23, resolve as resolve13 } from "path";
|
|
6171
6533
|
var PASS = "pass";
|
|
6172
6534
|
var WARN = "warn";
|
|
6173
6535
|
var FAIL = "fail";
|
|
@@ -6177,7 +6539,7 @@ var CODEX_ENV_DRIFT_KEYS = [
|
|
|
6177
6539
|
"TAP_REPO_ROOT"
|
|
6178
6540
|
];
|
|
6179
6541
|
function normalizeComparablePath2(value) {
|
|
6180
|
-
return
|
|
6542
|
+
return resolve13(value).replace(/\\/g, "/").toLowerCase();
|
|
6181
6543
|
}
|
|
6182
6544
|
function samePath(left, right) {
|
|
6183
6545
|
return normalizeComparablePath2(left) === normalizeComparablePath2(right);
|
|
@@ -6195,10 +6557,10 @@ function appendWarningMessage(message, extra) {
|
|
|
6195
6557
|
return message.includes(extra) ? message : `${message}; ${extra}`;
|
|
6196
6558
|
}
|
|
6197
6559
|
function findCodexConfigPath3() {
|
|
6198
|
-
return
|
|
6560
|
+
return join23(homedir3(), ".codex", "config.toml");
|
|
6199
6561
|
}
|
|
6200
6562
|
function canonicalizeTrustPath3(targetPath) {
|
|
6201
|
-
let resolved =
|
|
6563
|
+
let resolved = resolve13(targetPath).replace(/\//g, "\\");
|
|
6202
6564
|
const driveRoot = /^[A-Za-z]:\\$/;
|
|
6203
6565
|
if (!driveRoot.test(resolved)) {
|
|
6204
6566
|
resolved = resolved.replace(/\\+$/g, "");
|
|
@@ -6209,19 +6571,19 @@ function trustSelector2(targetPath) {
|
|
|
6209
6571
|
return `projects.'${canonicalizeTrustPath3(targetPath)}'`;
|
|
6210
6572
|
}
|
|
6211
6573
|
function writeTomlAtomically(filePath, content) {
|
|
6212
|
-
const dir =
|
|
6213
|
-
|
|
6574
|
+
const dir = dirname15(filePath);
|
|
6575
|
+
mkdirSync13(dir, { recursive: true });
|
|
6214
6576
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
6215
|
-
|
|
6216
|
-
|
|
6577
|
+
writeFileSync13(tmp, content, "utf-8");
|
|
6578
|
+
renameSync11(tmp, filePath);
|
|
6217
6579
|
}
|
|
6218
6580
|
function hasInstalledCodexInstance(state) {
|
|
6219
|
-
return
|
|
6220
|
-
(
|
|
6581
|
+
return state ? Object.values(state.instances).some(
|
|
6582
|
+
(instance) => instance.runtime === "codex" && instance.installed
|
|
6221
6583
|
) : false;
|
|
6222
6584
|
}
|
|
6223
6585
|
function getCodexTrustTargets(repoRoot) {
|
|
6224
|
-
return [...new Set([repoRoot, process.cwd()].map((value) =>
|
|
6586
|
+
return [...new Set([repoRoot, process.cwd()].map((value) => resolve13(value)))];
|
|
6225
6587
|
}
|
|
6226
6588
|
function buildCodexDoctorSpec(repoRoot, commsDir) {
|
|
6227
6589
|
const state = loadState(repoRoot);
|
|
@@ -6246,8 +6608,11 @@ function repairCodexConfig(repoRoot, commsDir) {
|
|
|
6246
6608
|
spec.managed.issues[0] ?? "Unable to resolve the managed tap MCP server for Codex."
|
|
6247
6609
|
);
|
|
6248
6610
|
}
|
|
6249
|
-
const existingContent =
|
|
6250
|
-
const existingTapEnvTable = extractTomlTable(
|
|
6611
|
+
const existingContent = existsSync23(spec.configPath) ? readFileSync18(spec.configPath, "utf-8") : "";
|
|
6612
|
+
const existingTapEnvTable = extractTomlTable(
|
|
6613
|
+
existingContent,
|
|
6614
|
+
"mcp_servers.tap.env"
|
|
6615
|
+
);
|
|
6251
6616
|
const existingLegacyEnvTable = extractTomlTable(
|
|
6252
6617
|
existingContent,
|
|
6253
6618
|
"mcp_servers.tap-comms.env"
|
|
@@ -6305,22 +6670,22 @@ function repairCodexConfig(repoRoot, commsDir) {
|
|
|
6305
6670
|
return `Repaired Codex config at ${spec.configPath}. Restart Codex to reload MCP settings.`;
|
|
6306
6671
|
}
|
|
6307
6672
|
function countFiles(dir, ext = ".md") {
|
|
6308
|
-
if (!
|
|
6673
|
+
if (!existsSync23(dir)) return 0;
|
|
6309
6674
|
try {
|
|
6310
|
-
return
|
|
6675
|
+
return readdirSync6(dir).filter((f) => f.endsWith(ext)).length;
|
|
6311
6676
|
} catch {
|
|
6312
6677
|
return 0;
|
|
6313
6678
|
}
|
|
6314
6679
|
}
|
|
6315
6680
|
function recentFileCount(dir, withinMs) {
|
|
6316
|
-
if (!
|
|
6681
|
+
if (!existsSync23(dir)) return 0;
|
|
6317
6682
|
const cutoff = Date.now() - withinMs;
|
|
6318
6683
|
let count = 0;
|
|
6319
6684
|
try {
|
|
6320
|
-
for (const f of
|
|
6685
|
+
for (const f of readdirSync6(dir)) {
|
|
6321
6686
|
if (!f.endsWith(".md")) continue;
|
|
6322
6687
|
try {
|
|
6323
|
-
if (
|
|
6688
|
+
if (statSync3(join23(dir, f)).mtimeMs > cutoff) count++;
|
|
6324
6689
|
} catch {
|
|
6325
6690
|
}
|
|
6326
6691
|
}
|
|
@@ -6332,10 +6697,10 @@ function checkComms(commsDir) {
|
|
|
6332
6697
|
const checks = [];
|
|
6333
6698
|
checks.push({
|
|
6334
6699
|
name: "comms directory",
|
|
6335
|
-
status:
|
|
6336
|
-
message:
|
|
6337
|
-
fix:
|
|
6338
|
-
|
|
6700
|
+
status: existsSync23(commsDir) ? PASS : FAIL,
|
|
6701
|
+
message: existsSync23(commsDir) ? commsDir : `Not found: ${commsDir}`,
|
|
6702
|
+
fix: existsSync23(commsDir) ? void 0 : () => {
|
|
6703
|
+
mkdirSync13(commsDir, { recursive: true });
|
|
6339
6704
|
return `Created ${commsDir}`;
|
|
6340
6705
|
}
|
|
6341
6706
|
});
|
|
@@ -6344,22 +6709,22 @@ function checkComms(commsDir) {
|
|
|
6344
6709
|
["reviews", false],
|
|
6345
6710
|
["findings", false]
|
|
6346
6711
|
]) {
|
|
6347
|
-
const dir =
|
|
6348
|
-
const exists =
|
|
6712
|
+
const dir = join23(commsDir, subdir);
|
|
6713
|
+
const exists = existsSync23(dir);
|
|
6349
6714
|
checks.push({
|
|
6350
6715
|
name: `${subdir} directory`,
|
|
6351
6716
|
status: exists ? PASS : required ? FAIL : WARN,
|
|
6352
6717
|
message: exists ? subdir === "findings" ? `${countFiles(dir)} findings` : subdir === "inbox" ? `${countFiles(dir)} messages` : "exists" : `Missing${required ? "" : " (optional)"}`,
|
|
6353
6718
|
fix: exists ? void 0 : () => {
|
|
6354
|
-
|
|
6719
|
+
mkdirSync13(dir, { recursive: true });
|
|
6355
6720
|
return `Created ${dir}`;
|
|
6356
6721
|
}
|
|
6357
6722
|
});
|
|
6358
6723
|
}
|
|
6359
|
-
const heartbeats =
|
|
6360
|
-
if (
|
|
6724
|
+
const heartbeats = join23(commsDir, "heartbeats.json");
|
|
6725
|
+
if (existsSync23(heartbeats)) {
|
|
6361
6726
|
try {
|
|
6362
|
-
const store = JSON.parse(
|
|
6727
|
+
const store = JSON.parse(readFileSync18(heartbeats, "utf-8"));
|
|
6363
6728
|
const agents = Object.keys(store);
|
|
6364
6729
|
const now = Date.now();
|
|
6365
6730
|
const active = agents.filter((a) => {
|
|
@@ -6433,15 +6798,21 @@ function checkInstances(repoRoot, stateDir) {
|
|
|
6433
6798
|
for (const pid of [appServer.auth?.gatewayPid, appServer.pid]) {
|
|
6434
6799
|
if (pid) {
|
|
6435
6800
|
try {
|
|
6436
|
-
process.
|
|
6801
|
+
if (process.platform === "win32") {
|
|
6802
|
+
spawnSync5("taskkill", ["/PID", String(pid), "/F", "/T"], {
|
|
6803
|
+
stdio: "pipe"
|
|
6804
|
+
});
|
|
6805
|
+
} else {
|
|
6806
|
+
process.kill(pid);
|
|
6807
|
+
}
|
|
6437
6808
|
} catch {
|
|
6438
6809
|
}
|
|
6439
6810
|
}
|
|
6440
6811
|
}
|
|
6441
6812
|
}
|
|
6442
|
-
const pidPath =
|
|
6813
|
+
const pidPath = join23(stateDir, "pids", `bridge-${id}.json`);
|
|
6443
6814
|
try {
|
|
6444
|
-
|
|
6815
|
+
unlinkSync6(pidPath);
|
|
6445
6816
|
} catch {
|
|
6446
6817
|
}
|
|
6447
6818
|
const currentState = loadState(repoRoot);
|
|
@@ -6495,8 +6866,8 @@ function checkInstances(repoRoot, stateDir) {
|
|
|
6495
6866
|
}
|
|
6496
6867
|
function checkMessageLifecycle(commsDir) {
|
|
6497
6868
|
const checks = [];
|
|
6498
|
-
const inbox =
|
|
6499
|
-
if (!
|
|
6869
|
+
const inbox = join23(commsDir, "inbox");
|
|
6870
|
+
if (!existsSync23(inbox)) {
|
|
6500
6871
|
checks.push({
|
|
6501
6872
|
name: "message flow",
|
|
6502
6873
|
status: FAIL,
|
|
@@ -6512,10 +6883,10 @@ function checkMessageLifecycle(commsDir) {
|
|
|
6512
6883
|
status: recent10m > 0 ? PASS : total > 0 ? WARN : FAIL,
|
|
6513
6884
|
message: `${total} total, ${recent1h} in last 1h, ${recent10m} in last 10m`
|
|
6514
6885
|
});
|
|
6515
|
-
const receiptsPath =
|
|
6516
|
-
if (
|
|
6886
|
+
const receiptsPath = join23(commsDir, "receipts", "receipts.json");
|
|
6887
|
+
if (existsSync23(receiptsPath)) {
|
|
6517
6888
|
try {
|
|
6518
|
-
const receipts = JSON.parse(
|
|
6889
|
+
const receipts = JSON.parse(readFileSync18(receiptsPath, "utf-8"));
|
|
6519
6890
|
const receiptCount = Object.keys(receipts).length;
|
|
6520
6891
|
checks.push({
|
|
6521
6892
|
name: "read receipts",
|
|
@@ -6534,8 +6905,8 @@ function checkMessageLifecycle(commsDir) {
|
|
|
6534
6905
|
}
|
|
6535
6906
|
function checkMcpServer(repoRoot) {
|
|
6536
6907
|
const checks = [];
|
|
6537
|
-
const mcpJson =
|
|
6538
|
-
if (!
|
|
6908
|
+
const mcpJson = join23(repoRoot, ".mcp.json");
|
|
6909
|
+
if (!existsSync23(mcpJson)) {
|
|
6539
6910
|
checks.push({
|
|
6540
6911
|
name: "MCP config (.mcp.json)",
|
|
6541
6912
|
status: WARN,
|
|
@@ -6545,7 +6916,7 @@ function checkMcpServer(repoRoot) {
|
|
|
6545
6916
|
}
|
|
6546
6917
|
let config;
|
|
6547
6918
|
try {
|
|
6548
|
-
config = JSON.parse(
|
|
6919
|
+
config = JSON.parse(readFileSync18(mcpJson, "utf-8"));
|
|
6549
6920
|
} catch {
|
|
6550
6921
|
checks.push({
|
|
6551
6922
|
name: "MCP config (.mcp.json)",
|
|
@@ -6573,6 +6944,14 @@ function checkMcpServer(repoRoot) {
|
|
|
6573
6944
|
return checks;
|
|
6574
6945
|
}
|
|
6575
6946
|
const hasTapComms = hasTap ?? hasOldKey;
|
|
6947
|
+
if (!hasTapComms) {
|
|
6948
|
+
checks.push({
|
|
6949
|
+
name: "MCP config (.mcp.json)",
|
|
6950
|
+
status: FAIL,
|
|
6951
|
+
message: "No tap or tap-comms key found in .mcp.json"
|
|
6952
|
+
});
|
|
6953
|
+
return checks;
|
|
6954
|
+
}
|
|
6576
6955
|
checks.push({
|
|
6577
6956
|
name: "MCP config (.mcp.json)",
|
|
6578
6957
|
status: PASS,
|
|
@@ -6580,10 +6959,10 @@ function checkMcpServer(repoRoot) {
|
|
|
6580
6959
|
});
|
|
6581
6960
|
if (hasTapComms.command) {
|
|
6582
6961
|
const cmd = hasTapComms.command;
|
|
6583
|
-
let cmdAvailable =
|
|
6962
|
+
let cmdAvailable = existsSync23(cmd);
|
|
6584
6963
|
if (!cmdAvailable) {
|
|
6585
6964
|
try {
|
|
6586
|
-
const result =
|
|
6965
|
+
const result = spawnSync5(cmd, ["--version"], {
|
|
6587
6966
|
stdio: "pipe",
|
|
6588
6967
|
timeout: 5e3,
|
|
6589
6968
|
shell: process.platform === "win32"
|
|
@@ -6602,8 +6981,8 @@ function checkMcpServer(repoRoot) {
|
|
|
6602
6981
|
const mcpScript = hasTapComms.args[0];
|
|
6603
6982
|
checks.push({
|
|
6604
6983
|
name: "MCP server script",
|
|
6605
|
-
status:
|
|
6606
|
-
message:
|
|
6984
|
+
status: existsSync23(mcpScript) ? PASS : FAIL,
|
|
6985
|
+
message: existsSync23(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
|
|
6607
6986
|
});
|
|
6608
6987
|
if (mcpScript.endsWith(".mjs") && hasTapComms.command && !hasTapComms.command.includes("bun")) {
|
|
6609
6988
|
checks.push({
|
|
@@ -6636,8 +7015,8 @@ function checkMcpServer(repoRoot) {
|
|
|
6636
7015
|
} else {
|
|
6637
7016
|
checks.push({
|
|
6638
7017
|
name: "MCP TAP_COMMS_DIR",
|
|
6639
|
-
status:
|
|
6640
|
-
message:
|
|
7018
|
+
status: existsSync23(envCommsDir) ? PASS : FAIL,
|
|
7019
|
+
message: existsSync23(envCommsDir) ? envCommsDir : `Directory not found: ${envCommsDir}`
|
|
6641
7020
|
});
|
|
6642
7021
|
}
|
|
6643
7022
|
checks.push({
|
|
@@ -6654,7 +7033,7 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
6654
7033
|
}
|
|
6655
7034
|
const checks = [];
|
|
6656
7035
|
const fixHint = 'Run "tap doctor --fix" or "tap add codex --force".';
|
|
6657
|
-
if (!
|
|
7036
|
+
if (!existsSync23(spec.configPath)) {
|
|
6658
7037
|
checks.push({
|
|
6659
7038
|
name: "MCP config (~/.codex/config.toml)",
|
|
6660
7039
|
status: WARN,
|
|
@@ -6663,15 +7042,13 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
6663
7042
|
});
|
|
6664
7043
|
return checks;
|
|
6665
7044
|
}
|
|
6666
|
-
const content =
|
|
7045
|
+
const content = readFileSync18(spec.configPath, "utf-8");
|
|
6667
7046
|
const tapTable = extractTomlTable(content, "mcp_servers.tap");
|
|
6668
7047
|
const tapEnvTable = extractTomlTable(content, "mcp_servers.tap.env");
|
|
6669
7048
|
const legacyTable = extractTomlTable(content, "mcp_servers.tap-comms");
|
|
6670
7049
|
const legacyEnvTable = extractTomlTable(content, "mcp_servers.tap-comms.env");
|
|
6671
7050
|
const selectedMain = parseTomlAssignments(tapTable ?? "");
|
|
6672
|
-
const selectedEnv = parseTomlAssignments(
|
|
6673
|
-
tapEnvTable ?? legacyEnvTable ?? ""
|
|
6674
|
-
);
|
|
7051
|
+
const selectedEnv = parseTomlAssignments(tapEnvTable ?? legacyEnvTable ?? "");
|
|
6675
7052
|
const issues = [];
|
|
6676
7053
|
if (legacyTable || legacyEnvTable) {
|
|
6677
7054
|
issues.push('legacy "tap-comms" key present');
|
|
@@ -6731,8 +7108,8 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
6731
7108
|
}
|
|
6732
7109
|
function checkBridgeTurnHealth(repoRoot) {
|
|
6733
7110
|
const checks = [];
|
|
6734
|
-
const tmpDir =
|
|
6735
|
-
if (!
|
|
7111
|
+
const tmpDir = join23(repoRoot, ".tmp");
|
|
7112
|
+
if (!existsSync23(tmpDir)) return checks;
|
|
6736
7113
|
const state = loadState(repoRoot);
|
|
6737
7114
|
const activeMatchers = /* @__PURE__ */ new Set();
|
|
6738
7115
|
if (state) {
|
|
@@ -6745,7 +7122,7 @@ function checkBridgeTurnHealth(repoRoot) {
|
|
|
6745
7122
|
}
|
|
6746
7123
|
let dirs;
|
|
6747
7124
|
try {
|
|
6748
|
-
dirs =
|
|
7125
|
+
dirs = readdirSync6(tmpDir).filter((d) => {
|
|
6749
7126
|
if (!d.startsWith("codex-app-server-bridge")) return false;
|
|
6750
7127
|
const suffix = d.replace("codex-app-server-bridge-", "");
|
|
6751
7128
|
if (activeMatchers.size === 0) return true;
|
|
@@ -6758,11 +7135,11 @@ function checkBridgeTurnHealth(repoRoot) {
|
|
|
6758
7135
|
return checks;
|
|
6759
7136
|
}
|
|
6760
7137
|
for (const dir of dirs) {
|
|
6761
|
-
const heartbeatPath =
|
|
6762
|
-
if (!
|
|
7138
|
+
const heartbeatPath = join23(tmpDir, dir, "heartbeat.json");
|
|
7139
|
+
if (!existsSync23(heartbeatPath)) continue;
|
|
6763
7140
|
let heartbeat;
|
|
6764
7141
|
try {
|
|
6765
|
-
heartbeat = JSON.parse(
|
|
7142
|
+
heartbeat = JSON.parse(readFileSync18(heartbeatPath, "utf-8"));
|
|
6766
7143
|
} catch {
|
|
6767
7144
|
checks.push({
|
|
6768
7145
|
name: `turn: ${dir}`,
|
|
@@ -6985,9 +7362,9 @@ async function doctorCommand(args) {
|
|
|
6985
7362
|
}
|
|
6986
7363
|
|
|
6987
7364
|
// src/commands/comms.ts
|
|
6988
|
-
import { execSync as execSync6, spawnSync as
|
|
6989
|
-
import * as
|
|
6990
|
-
import * as
|
|
7365
|
+
import { execSync as execSync6, spawnSync as spawnSync6 } from "child_process";
|
|
7366
|
+
import * as fs27 from "fs";
|
|
7367
|
+
import * as path26 from "path";
|
|
6991
7368
|
var COMMS_HELP = `
|
|
6992
7369
|
Usage:
|
|
6993
7370
|
tap comms <subcommand>
|
|
@@ -7001,7 +7378,7 @@ Examples:
|
|
|
7001
7378
|
npx @hua-labs/tap comms push
|
|
7002
7379
|
`.trim();
|
|
7003
7380
|
function isGitRepo(dir) {
|
|
7004
|
-
return
|
|
7381
|
+
return fs27.existsSync(path26.join(dir, ".git"));
|
|
7005
7382
|
}
|
|
7006
7383
|
function commsPull(commsDir) {
|
|
7007
7384
|
logHeader("tap comms pull");
|
|
@@ -7077,7 +7454,7 @@ function commsPush(commsDir) {
|
|
|
7077
7454
|
};
|
|
7078
7455
|
}
|
|
7079
7456
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7080
|
-
const commitResult =
|
|
7457
|
+
const commitResult = spawnSync6(
|
|
7081
7458
|
"git",
|
|
7082
7459
|
["commit", "-m", `chore(comms): sync ${timestamp}`],
|
|
7083
7460
|
{ cwd: commsDir, stdio: "pipe", encoding: "utf-8" }
|
|
@@ -7148,6 +7525,568 @@ async function commsCommand(args) {
|
|
|
7148
7525
|
}
|
|
7149
7526
|
}
|
|
7150
7527
|
|
|
7528
|
+
// src/commands/watch.ts
|
|
7529
|
+
var WATCH_HELP = `
|
|
7530
|
+
Usage:
|
|
7531
|
+
tap watch [options]
|
|
7532
|
+
|
|
7533
|
+
Description:
|
|
7534
|
+
Monitor all bridges and auto-restart stuck/stale ones.
|
|
7535
|
+
Single-pass by default. Use --loop for continuous monitoring.
|
|
7536
|
+
|
|
7537
|
+
Options:
|
|
7538
|
+
--stuck-threshold <seconds> Turn stuck threshold (default: 300)
|
|
7539
|
+
--interval <seconds> Loop interval (default: 60)
|
|
7540
|
+
--loop Run continuously instead of single-pass
|
|
7541
|
+
--max-rounds <n> Max loop iterations (default: unlimited)
|
|
7542
|
+
|
|
7543
|
+
Examples:
|
|
7544
|
+
npx @hua-labs/tap watch # single check
|
|
7545
|
+
npx @hua-labs/tap watch --loop # continuous
|
|
7546
|
+
npx @hua-labs/tap watch --loop --interval 30 # check every 30s
|
|
7547
|
+
npx @hua-labs/tap watch --stuck-threshold 120 # 2 min threshold
|
|
7548
|
+
`.trim();
|
|
7549
|
+
function delay2(ms) {
|
|
7550
|
+
return new Promise((resolve14) => setTimeout(resolve14, ms));
|
|
7551
|
+
}
|
|
7552
|
+
async function watchCommand(args) {
|
|
7553
|
+
const { flags } = parseArgs(args);
|
|
7554
|
+
if (flags["help"] === true || flags["h"] === true) {
|
|
7555
|
+
log(WATCH_HELP);
|
|
7556
|
+
return {
|
|
7557
|
+
ok: true,
|
|
7558
|
+
command: "watch",
|
|
7559
|
+
code: "TAP_NO_OP",
|
|
7560
|
+
message: WATCH_HELP,
|
|
7561
|
+
warnings: [],
|
|
7562
|
+
data: {}
|
|
7563
|
+
};
|
|
7564
|
+
}
|
|
7565
|
+
const stuckThresholdStr = typeof flags["stuck-threshold"] === "string" ? flags["stuck-threshold"] : void 0;
|
|
7566
|
+
const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : void 0;
|
|
7567
|
+
const loop = flags["loop"] === true;
|
|
7568
|
+
const maxRoundsStr = typeof flags["max-rounds"] === "string" ? flags["max-rounds"] : void 0;
|
|
7569
|
+
let stuckThreshold;
|
|
7570
|
+
let interval;
|
|
7571
|
+
let maxRounds;
|
|
7572
|
+
try {
|
|
7573
|
+
stuckThreshold = parseIntFlag(stuckThresholdStr, "--stuck-threshold", 30, 3600) ?? 300;
|
|
7574
|
+
interval = parseIntFlag(intervalStr, "--interval", 5, 3600) ?? 60;
|
|
7575
|
+
maxRounds = parseIntFlag(maxRoundsStr, "--max-rounds", 1, 1e4) ?? null;
|
|
7576
|
+
} catch (err) {
|
|
7577
|
+
return {
|
|
7578
|
+
ok: false,
|
|
7579
|
+
command: "watch",
|
|
7580
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
7581
|
+
message: err instanceof Error ? err.message : String(err),
|
|
7582
|
+
warnings: [],
|
|
7583
|
+
data: {}
|
|
7584
|
+
};
|
|
7585
|
+
}
|
|
7586
|
+
const bridgeArgs = ["watch", "--stuck-threshold", String(stuckThreshold)];
|
|
7587
|
+
if (!loop) {
|
|
7588
|
+
return bridgeCommand(bridgeArgs);
|
|
7589
|
+
}
|
|
7590
|
+
logHeader("@hua-labs/tap watch (loop mode)");
|
|
7591
|
+
log(`Interval: ${interval}s, Stuck threshold: ${stuckThreshold}s`);
|
|
7592
|
+
if (maxRounds != null) {
|
|
7593
|
+
log(`Max rounds: ${maxRounds}`);
|
|
7594
|
+
}
|
|
7595
|
+
log("");
|
|
7596
|
+
let round = 0;
|
|
7597
|
+
let failedRounds = 0;
|
|
7598
|
+
const allRestarted = [];
|
|
7599
|
+
const allWarnings = [];
|
|
7600
|
+
while (maxRounds == null || round < maxRounds) {
|
|
7601
|
+
round++;
|
|
7602
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
7603
|
+
log(`[${timestamp}] Round ${round}`);
|
|
7604
|
+
const result = await bridgeCommand(bridgeArgs);
|
|
7605
|
+
if (!result.ok) {
|
|
7606
|
+
failedRounds++;
|
|
7607
|
+
allWarnings.push(`Round ${round}: ${result.message}`);
|
|
7608
|
+
}
|
|
7609
|
+
if (result.data?.restarted) {
|
|
7610
|
+
const restarted = result.data.restarted;
|
|
7611
|
+
allRestarted.push(...restarted);
|
|
7612
|
+
}
|
|
7613
|
+
if (result.warnings?.length) {
|
|
7614
|
+
allWarnings.push(...result.warnings);
|
|
7615
|
+
}
|
|
7616
|
+
if (maxRounds != null && round >= maxRounds) break;
|
|
7617
|
+
await delay2(interval * 1e3);
|
|
7618
|
+
}
|
|
7619
|
+
const allOk = failedRounds === 0;
|
|
7620
|
+
const message = [
|
|
7621
|
+
`Completed ${round} round(s)`,
|
|
7622
|
+
failedRounds > 0 ? `${failedRounds} failed` : null,
|
|
7623
|
+
allRestarted.length > 0 ? `Total restarts: ${allRestarted.length} (${allRestarted.join(", ")})` : "No restarts needed"
|
|
7624
|
+
].filter(Boolean).join(". ");
|
|
7625
|
+
return {
|
|
7626
|
+
ok: allOk,
|
|
7627
|
+
command: "watch",
|
|
7628
|
+
code: !allOk ? "TAP_WATCH_FAILED" : allRestarted.length > 0 ? "TAP_WATCH_RESTARTED" : "TAP_WATCH_OK",
|
|
7629
|
+
message,
|
|
7630
|
+
warnings: allWarnings,
|
|
7631
|
+
data: { rounds: round, restarted: allRestarted }
|
|
7632
|
+
};
|
|
7633
|
+
}
|
|
7634
|
+
|
|
7635
|
+
// src/commands/gui.ts
|
|
7636
|
+
import * as http from "http";
|
|
7637
|
+
|
|
7638
|
+
// src/engine/missions.ts
|
|
7639
|
+
import * as fs28 from "fs";
|
|
7640
|
+
import * as path27 from "path";
|
|
7641
|
+
function parseStatus(raw) {
|
|
7642
|
+
const trimmed = raw.trim();
|
|
7643
|
+
if (trimmed.includes("active")) return "active";
|
|
7644
|
+
if (trimmed.includes("completed")) return "completed";
|
|
7645
|
+
return "planned";
|
|
7646
|
+
}
|
|
7647
|
+
function parseRow(line) {
|
|
7648
|
+
if (!line.startsWith("|") || !line.endsWith("|")) return null;
|
|
7649
|
+
const cells = line.split("|").slice(1, -1).map((c) => c.trim());
|
|
7650
|
+
if (cells.length < 4) return null;
|
|
7651
|
+
const [idCell, missionCell, branchCell, statusCell, ownerCell] = cells;
|
|
7652
|
+
if (/^[-: ]+$/.test(idCell ?? "")) return null;
|
|
7653
|
+
const id = (idCell ?? "").replace(/[^\w]/g, "");
|
|
7654
|
+
if (!id || !/^M\d+$/i.test(id)) return null;
|
|
7655
|
+
const titleMatch = missionCell?.match(/\[([^\]]+)\]/);
|
|
7656
|
+
const title = titleMatch ? titleMatch[1] : (missionCell ?? "").trim();
|
|
7657
|
+
if (!title) return null;
|
|
7658
|
+
const branchMatch = branchCell?.match(/`([^`]+)`/);
|
|
7659
|
+
const branch = branchMatch ? branchMatch[1] : null;
|
|
7660
|
+
const status = parseStatus(statusCell ?? "");
|
|
7661
|
+
const rawOwner = (ownerCell ?? "").trim();
|
|
7662
|
+
const owner = rawOwner === "" || rawOwner === "\u2014" || rawOwner === "\uBBF8\uBC30\uC815" ? null : rawOwner;
|
|
7663
|
+
return { id: id.toUpperCase(), title, branch, status, owner };
|
|
7664
|
+
}
|
|
7665
|
+
function parseMissionsFile(repoRoot) {
|
|
7666
|
+
const missionsPath = path27.join(repoRoot, "docs", "missions", "MISSIONS.md");
|
|
7667
|
+
let content;
|
|
7668
|
+
try {
|
|
7669
|
+
content = fs28.readFileSync(missionsPath, "utf-8");
|
|
7670
|
+
} catch {
|
|
7671
|
+
return [];
|
|
7672
|
+
}
|
|
7673
|
+
const missions = [];
|
|
7674
|
+
for (const line of content.split("\n")) {
|
|
7675
|
+
const mission = parseRow(line);
|
|
7676
|
+
if (!mission) continue;
|
|
7677
|
+
missions.push(mission);
|
|
7678
|
+
}
|
|
7679
|
+
return missions;
|
|
7680
|
+
}
|
|
7681
|
+
|
|
7682
|
+
// src/engine/pull-requests.ts
|
|
7683
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
7684
|
+
function runGhPrList(repoRoot, extraArgs) {
|
|
7685
|
+
try {
|
|
7686
|
+
const result = spawnSync7(
|
|
7687
|
+
"gh",
|
|
7688
|
+
[
|
|
7689
|
+
"pr",
|
|
7690
|
+
"list",
|
|
7691
|
+
"--json",
|
|
7692
|
+
"number,title,state,author,headRefName,url,mergedAt",
|
|
7693
|
+
...extraArgs
|
|
7694
|
+
],
|
|
7695
|
+
{ cwd: repoRoot, encoding: "utf-8", timeout: 1e4 }
|
|
7696
|
+
);
|
|
7697
|
+
if (result.error || result.status !== 0) return null;
|
|
7698
|
+
const raw = result.stdout.trim();
|
|
7699
|
+
if (!raw) return null;
|
|
7700
|
+
return JSON.parse(raw);
|
|
7701
|
+
} catch {
|
|
7702
|
+
return null;
|
|
7703
|
+
}
|
|
7704
|
+
}
|
|
7705
|
+
function mapEntry(entry) {
|
|
7706
|
+
const state = entry.state?.toLowerCase();
|
|
7707
|
+
return {
|
|
7708
|
+
number: entry.number,
|
|
7709
|
+
title: entry.title ?? "",
|
|
7710
|
+
state: state === "merged" ? "merged" : state === "closed" ? "closed" : "open",
|
|
7711
|
+
author: entry.author?.login ?? "",
|
|
7712
|
+
branch: entry.headRefName ?? "",
|
|
7713
|
+
url: entry.url ?? "",
|
|
7714
|
+
mergedAt: entry.mergedAt ?? null
|
|
7715
|
+
};
|
|
7716
|
+
}
|
|
7717
|
+
function fetchOpenPrs(repoRoot) {
|
|
7718
|
+
const entries = runGhPrList(repoRoot, ["--limit", "50"]);
|
|
7719
|
+
if (!entries) return [];
|
|
7720
|
+
return entries.map(mapEntry);
|
|
7721
|
+
}
|
|
7722
|
+
function fetchMergedPrs(repoRoot, limit = 20) {
|
|
7723
|
+
const entries = runGhPrList(repoRoot, [
|
|
7724
|
+
"--state",
|
|
7725
|
+
"merged",
|
|
7726
|
+
"--limit",
|
|
7727
|
+
String(limit)
|
|
7728
|
+
]);
|
|
7729
|
+
if (!entries) return [];
|
|
7730
|
+
return entries.map(mapEntry).sort((a, b) => {
|
|
7731
|
+
if (!a.mergedAt || !b.mergedAt) return 0;
|
|
7732
|
+
return new Date(b.mergedAt).getTime() - new Date(a.mergedAt).getTime();
|
|
7733
|
+
});
|
|
7734
|
+
}
|
|
7735
|
+
function fetchPrs(repoRoot) {
|
|
7736
|
+
return {
|
|
7737
|
+
open: fetchOpenPrs(repoRoot),
|
|
7738
|
+
merged: fetchMergedPrs(repoRoot)
|
|
7739
|
+
};
|
|
7740
|
+
}
|
|
7741
|
+
|
|
7742
|
+
// src/commands/gui.ts
|
|
7743
|
+
var GUI_HELP = `
|
|
7744
|
+
Usage:
|
|
7745
|
+
tap gui [options]
|
|
7746
|
+
|
|
7747
|
+
Description:
|
|
7748
|
+
Start a local web dashboard showing bridge status, agents, and turn info.
|
|
7749
|
+
|
|
7750
|
+
Options:
|
|
7751
|
+
--port <n> Dashboard port (default: 3847)
|
|
7752
|
+
--help, -h Show help
|
|
7753
|
+
|
|
7754
|
+
Examples:
|
|
7755
|
+
npx @hua-labs/tap gui
|
|
7756
|
+
npx @hua-labs/tap gui --port 8080
|
|
7757
|
+
`.trim();
|
|
7758
|
+
function esc(str) {
|
|
7759
|
+
if (!str) return "-";
|
|
7760
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7761
|
+
}
|
|
7762
|
+
function buildHtml(snapshot, turnData) {
|
|
7763
|
+
const agentRows = snapshot.agents.map(
|
|
7764
|
+
(a) => `<tr><td>${esc(a.name)}</td><td class="${a.status === "active" ? "ok" : "warn"}">${esc(a.status)}</td><td>${a.lastActivity ? esc(new Date(a.lastActivity).toLocaleTimeString()) : "-"}</td></tr>`
|
|
7765
|
+
).join("\n");
|
|
7766
|
+
const bridgeRows = snapshot.bridges.map((b) => {
|
|
7767
|
+
const turn = turnData[b.instanceId];
|
|
7768
|
+
const turnCell = turn?.activeTurnId ? `<span class="${turn.stuck ? "stuck" : "ok"}">${esc(turn.activeTurnId.slice(0, 8))}... ${turn.stuck ? "\u26A0 STUCK" : ""} ${turn.ageSeconds != null ? `(${turn.ageSeconds}s)` : ""}</span>` : "-";
|
|
7769
|
+
const statusClass = b.status === "running" ? "ok" : b.status === "stale" ? "stuck" : "off";
|
|
7770
|
+
return `<tr><td>${esc(b.instanceId)}</td><td>${esc(b.runtime)}</td><td class="${statusClass}">${esc(b.status)}</td><td>${b.pid ?? "-"}</td><td>${b.port ?? "-"}</td><td>${b.heartbeatAge != null ? `${b.heartbeatAge}s ago` : "-"}</td><td>${turnCell}</td></tr>`;
|
|
7771
|
+
}).join("\n");
|
|
7772
|
+
const warningRows = snapshot.warnings.map(
|
|
7773
|
+
(w) => `<tr><td class="warn">${esc(w.level)}</td><td>${esc(w.message)}</td></tr>`
|
|
7774
|
+
).join("\n");
|
|
7775
|
+
return `<!DOCTYPE html>
|
|
7776
|
+
<html lang="en">
|
|
7777
|
+
<head>
|
|
7778
|
+
<meta charset="utf-8">
|
|
7779
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7780
|
+
<title>tap dashboard</title>
|
|
7781
|
+
<style>
|
|
7782
|
+
body { font-family: system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; margin: 0; padding: 20px; }
|
|
7783
|
+
h1 { color: #58a6ff; font-size: 1.4em; }
|
|
7784
|
+
h2 { color: #8b949e; font-size: 1.1em; margin-top: 24px; }
|
|
7785
|
+
table { border-collapse: collapse; width: 100%; margin: 8px 0; }
|
|
7786
|
+
th, td { text-align: left; padding: 6px 12px; border-bottom: 1px solid #21262d; }
|
|
7787
|
+
th { color: #8b949e; font-size: 0.85em; text-transform: uppercase; }
|
|
7788
|
+
.ok { color: #3fb950; }
|
|
7789
|
+
.warn { color: #d29922; }
|
|
7790
|
+
.stuck { color: #f85149; font-weight: bold; }
|
|
7791
|
+
.off { color: #8b949e; }
|
|
7792
|
+
.meta { color: #8b949e; font-size: 0.85em; }
|
|
7793
|
+
.refresh { color: #8b949e; font-size: 0.8em; margin-top: 16px; }
|
|
7794
|
+
</style>
|
|
7795
|
+
</head>
|
|
7796
|
+
<body>
|
|
7797
|
+
<h1>tap dashboard</h1>
|
|
7798
|
+
<p class="meta">${esc(snapshot.generatedAt)} · ${esc(snapshot.repoRoot)}</p>
|
|
7799
|
+
|
|
7800
|
+
<h2>Agents</h2>
|
|
7801
|
+
<table>
|
|
7802
|
+
<tr><th>Name</th><th>Status</th><th>Last Activity</th></tr>
|
|
7803
|
+
${agentRows || '<tr><td colspan="3" class="off">No agents</td></tr>'}
|
|
7804
|
+
</table>
|
|
7805
|
+
|
|
7806
|
+
<h2>Bridges</h2>
|
|
7807
|
+
<table>
|
|
7808
|
+
<tr><th>Instance</th><th>Runtime</th><th>Status</th><th>PID</th><th>Port</th><th>Heartbeat</th><th>Turn</th></tr>
|
|
7809
|
+
${bridgeRows || '<tr><td colspan="7" class="off">No bridges</td></tr>'}
|
|
7810
|
+
</table>
|
|
7811
|
+
|
|
7812
|
+
${warningRows ? `<h2>Warnings</h2><table><tr><th>Level</th><th>Message</th></tr>${warningRows}</table>` : ""}
|
|
7813
|
+
|
|
7814
|
+
<p class="refresh" id="status">Connecting to live updates...</p>
|
|
7815
|
+
<script>
|
|
7816
|
+
const es = new EventSource('/api/events');
|
|
7817
|
+
const statusEl = document.getElementById('status');
|
|
7818
|
+
let lastReloadAt = Date.now();
|
|
7819
|
+
es.onmessage = (e) => {
|
|
7820
|
+
statusEl.textContent = 'Live \u2014 updated ' + new Date().toLocaleTimeString();
|
|
7821
|
+
statusEl.style.color = '#3fb950';
|
|
7822
|
+
const elapsed = Date.now() - lastReloadAt;
|
|
7823
|
+
if (elapsed >= 9000) { lastReloadAt = Date.now(); location.reload(); }
|
|
7824
|
+
};
|
|
7825
|
+
es.onerror = () => {
|
|
7826
|
+
statusEl.textContent = 'Disconnected \u2014 will retry...';
|
|
7827
|
+
statusEl.style.color = '#f85149';
|
|
7828
|
+
};
|
|
7829
|
+
</script>
|
|
7830
|
+
<p class="refresh"><a href="/missions" style="color:#58a6ff;">Mission Kanban</a> · <a href="/prs" style="color:#58a6ff;">PR Board</a></p>
|
|
7831
|
+
</body>
|
|
7832
|
+
</html>`;
|
|
7833
|
+
}
|
|
7834
|
+
function buildMissionsHtml(repoRoot) {
|
|
7835
|
+
const missions = parseMissionsFile(repoRoot);
|
|
7836
|
+
const byStatus = {
|
|
7837
|
+
active: missions.filter((m) => m.status === "active"),
|
|
7838
|
+
planned: missions.filter((m) => m.status === "planned"),
|
|
7839
|
+
completed: missions.filter((m) => m.status === "completed")
|
|
7840
|
+
};
|
|
7841
|
+
function card(m) {
|
|
7842
|
+
return `<div class="card">
|
|
7843
|
+
<div class="card-id">${esc(m.id)}</div>
|
|
7844
|
+
<div class="card-title">${esc(m.title)}</div>
|
|
7845
|
+
${m.owner ? `<div class="card-meta">Owner: ${esc(m.owner)}</div>` : ""}
|
|
7846
|
+
${m.branch ? `<div class="card-meta card-branch">${esc(m.branch)}</div>` : ""}
|
|
7847
|
+
</div>`;
|
|
7848
|
+
}
|
|
7849
|
+
function column(label, headerClass, items) {
|
|
7850
|
+
return `<div class="column">
|
|
7851
|
+
<div class="col-header ${headerClass}">${label} <span class="badge">${items.length}</span></div>
|
|
7852
|
+
<div class="col-body">
|
|
7853
|
+
${items.length ? items.map(card).join("\n ") : '<div class="empty">No missions</div>'}
|
|
7854
|
+
</div>
|
|
7855
|
+
</div>`;
|
|
7856
|
+
}
|
|
7857
|
+
return `<!DOCTYPE html>
|
|
7858
|
+
<html lang="en">
|
|
7859
|
+
<head>
|
|
7860
|
+
<meta charset="utf-8">
|
|
7861
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7862
|
+
<title>tap \u2014 mission kanban</title>
|
|
7863
|
+
<meta http-equiv="refresh" content="30">
|
|
7864
|
+
<style>
|
|
7865
|
+
body { font-family: system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; margin: 0; padding: 20px; }
|
|
7866
|
+
h1 { color: #58a6ff; font-size: 1.4em; }
|
|
7867
|
+
a { color: #58a6ff; text-decoration: none; }
|
|
7868
|
+
a:hover { text-decoration: underline; }
|
|
7869
|
+
.meta { color: #8b949e; font-size: 0.85em; }
|
|
7870
|
+
.refresh { color: #8b949e; font-size: 0.8em; margin-top: 16px; }
|
|
7871
|
+
.board { display: flex; gap: 16px; margin-top: 16px; align-items: flex-start; flex-wrap: wrap; }
|
|
7872
|
+
.column { flex: 1; min-width: 240px; background: #161b22; border: 1px solid #21262d; border-radius: 6px; overflow: hidden; }
|
|
7873
|
+
.col-header { padding: 10px 14px; font-size: 0.85em; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; display: flex; justify-content: space-between; align-items: center; }
|
|
7874
|
+
.col-header.active { color: #3fb950; border-bottom: 2px solid #3fb950; }
|
|
7875
|
+
.col-header.planned { color: #d29922; border-bottom: 2px solid #d29922; }
|
|
7876
|
+
.col-header.completed { color: #8b949e; border-bottom: 2px solid #8b949e; }
|
|
7877
|
+
.badge { background: #21262d; color: #c9d1d9; border-radius: 10px; padding: 1px 7px; font-size: 0.8em; }
|
|
7878
|
+
.col-body { padding: 8px; display: flex; flex-direction: column; gap: 8px; }
|
|
7879
|
+
.card { background: #0d1117; border: 1px solid #21262d; border-radius: 4px; padding: 10px 12px; }
|
|
7880
|
+
.card-id { font-size: 0.75em; color: #58a6ff; font-weight: 600; margin-bottom: 4px; }
|
|
7881
|
+
.card-title { font-size: 0.9em; color: #e6edf3; line-height: 1.4; }
|
|
7882
|
+
.card-meta { font-size: 0.75em; color: #8b949e; margin-top: 4px; }
|
|
7883
|
+
.card-branch { font-family: ui-monospace, monospace; color: #6e7681; }
|
|
7884
|
+
.empty { color: #6e7681; font-size: 0.85em; padding: 8px 4px; }
|
|
7885
|
+
</style>
|
|
7886
|
+
</head>
|
|
7887
|
+
<body>
|
|
7888
|
+
<h1>mission kanban</h1>
|
|
7889
|
+
<p class="meta"><a href="/">← Dashboard</a> · ${esc(repoRoot)}</p>
|
|
7890
|
+
<div class="board">
|
|
7891
|
+
${column("Active", "active", byStatus.active)}
|
|
7892
|
+
${column("Planned", "planned", byStatus.planned)}
|
|
7893
|
+
${column("Completed", "completed", byStatus.completed)}
|
|
7894
|
+
</div>
|
|
7895
|
+
<p class="refresh">Auto-refresh every 30s</p>
|
|
7896
|
+
</body>
|
|
7897
|
+
</html>`;
|
|
7898
|
+
}
|
|
7899
|
+
function buildPrsHtml(repoRoot) {
|
|
7900
|
+
const { open, merged } = fetchPrs(repoRoot);
|
|
7901
|
+
function prRow(pr) {
|
|
7902
|
+
return `<tr>
|
|
7903
|
+
<td><a href="${esc(pr.url)}" target="_blank" rel="noopener" style="color:#58a6ff;">#${pr.number}</a></td>
|
|
7904
|
+
<td>${esc(pr.title)}</td>
|
|
7905
|
+
<td>${esc(pr.author)}</td>
|
|
7906
|
+
<td class="branch">${esc(pr.branch)}</td>
|
|
7907
|
+
</tr>`;
|
|
7908
|
+
}
|
|
7909
|
+
const openRows = open.map(prRow).join("\n");
|
|
7910
|
+
const mergedRows = merged.map(
|
|
7911
|
+
(pr) => `<tr>
|
|
7912
|
+
<td><a href="${esc(pr.url)}" target="_blank" rel="noopener" style="color:#58a6ff;">#${pr.number}</a></td>
|
|
7913
|
+
<td>${esc(pr.title)}</td>
|
|
7914
|
+
<td>${esc(pr.author)}</td>
|
|
7915
|
+
</tr>`
|
|
7916
|
+
).join("\n");
|
|
7917
|
+
return `<!DOCTYPE html>
|
|
7918
|
+
<html lang="en">
|
|
7919
|
+
<head>
|
|
7920
|
+
<meta charset="utf-8">
|
|
7921
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7922
|
+
<title>tap \u2014 pr board</title>
|
|
7923
|
+
<meta http-equiv="refresh" content="60">
|
|
7924
|
+
<style>
|
|
7925
|
+
body { font-family: system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; margin: 0; padding: 20px; }
|
|
7926
|
+
h1 { color: #58a6ff; font-size: 1.4em; }
|
|
7927
|
+
h2 { color: #8b949e; font-size: 1.1em; margin-top: 24px; }
|
|
7928
|
+
a { color: #58a6ff; text-decoration: none; }
|
|
7929
|
+
a:hover { text-decoration: underline; }
|
|
7930
|
+
table { border-collapse: collapse; width: 100%; margin: 8px 0; }
|
|
7931
|
+
th, td { text-align: left; padding: 6px 12px; border-bottom: 1px solid #21262d; }
|
|
7932
|
+
th { color: #8b949e; font-size: 0.85em; text-transform: uppercase; }
|
|
7933
|
+
.branch { font-family: ui-monospace, monospace; font-size: 0.85em; color: #6e7681; }
|
|
7934
|
+
.meta { color: #8b949e; font-size: 0.85em; }
|
|
7935
|
+
.refresh { color: #8b949e; font-size: 0.8em; margin-top: 16px; }
|
|
7936
|
+
.off { color: #8b949e; }
|
|
7937
|
+
</style>
|
|
7938
|
+
</head>
|
|
7939
|
+
<body>
|
|
7940
|
+
<h1>pr board</h1>
|
|
7941
|
+
<p class="meta"><a href="/">← Dashboard</a> · ${esc(repoRoot)}</p>
|
|
7942
|
+
|
|
7943
|
+
<h2>Open PRs <span style="color:#3fb950;">(${open.length})</span></h2>
|
|
7944
|
+
<table>
|
|
7945
|
+
<tr><th>#</th><th>Title</th><th>Author</th><th>Branch</th></tr>
|
|
7946
|
+
${openRows || '<tr><td colspan="4" class="off">No open PRs</td></tr>'}
|
|
7947
|
+
</table>
|
|
7948
|
+
|
|
7949
|
+
<h2>Recently Merged <span style="color:#8b949e;">(${merged.length})</span></h2>
|
|
7950
|
+
<table>
|
|
7951
|
+
<tr><th>#</th><th>Title</th><th>Author</th></tr>
|
|
7952
|
+
${mergedRows || '<tr><td colspan="3" class="off">No merged PRs</td></tr>'}
|
|
7953
|
+
</table>
|
|
7954
|
+
|
|
7955
|
+
<p class="refresh">Auto-refresh every 60s</p>
|
|
7956
|
+
</body>
|
|
7957
|
+
</html>`;
|
|
7958
|
+
}
|
|
7959
|
+
async function guiCommand(args) {
|
|
7960
|
+
const { flags } = parseArgs(args);
|
|
7961
|
+
if (flags["help"] === true || flags["h"] === true) {
|
|
7962
|
+
log(GUI_HELP);
|
|
7963
|
+
return {
|
|
7964
|
+
ok: true,
|
|
7965
|
+
command: "gui",
|
|
7966
|
+
code: "TAP_NO_OP",
|
|
7967
|
+
message: GUI_HELP,
|
|
7968
|
+
warnings: [],
|
|
7969
|
+
data: {}
|
|
7970
|
+
};
|
|
7971
|
+
}
|
|
7972
|
+
const portStr = typeof flags["port"] === "string" ? flags["port"] : void 0;
|
|
7973
|
+
let port;
|
|
7974
|
+
try {
|
|
7975
|
+
port = parseIntFlag(portStr, "--port", 1024, 65535) ?? 3847;
|
|
7976
|
+
} catch (err) {
|
|
7977
|
+
return {
|
|
7978
|
+
ok: false,
|
|
7979
|
+
command: "gui",
|
|
7980
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
7981
|
+
message: err instanceof Error ? err.message : String(err),
|
|
7982
|
+
warnings: [],
|
|
7983
|
+
data: {}
|
|
7984
|
+
};
|
|
7985
|
+
}
|
|
7986
|
+
const repoRoot = findRepoRoot();
|
|
7987
|
+
const server = http.createServer((req, res) => {
|
|
7988
|
+
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
7989
|
+
const state = loadState(repoRoot);
|
|
7990
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
7991
|
+
const turnData = {};
|
|
7992
|
+
if (state) {
|
|
7993
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
7994
|
+
if (!inst?.installed || inst.bridgeMode !== "app-server") continue;
|
|
7995
|
+
turnData[id] = getTurnInfo(config.stateDir, id);
|
|
7996
|
+
}
|
|
7997
|
+
}
|
|
7998
|
+
const jsonHeaders = {
|
|
7999
|
+
"Content-Type": "application/json",
|
|
8000
|
+
"Access-Control-Allow-Origin": "*"
|
|
8001
|
+
};
|
|
8002
|
+
if (req.url === "/api/snapshot") {
|
|
8003
|
+
res.writeHead(200, jsonHeaders);
|
|
8004
|
+
res.end(JSON.stringify({ ...snapshot, turns: turnData }, null, 2));
|
|
8005
|
+
return;
|
|
8006
|
+
}
|
|
8007
|
+
if (req.url === "/api/events") {
|
|
8008
|
+
res.writeHead(200, {
|
|
8009
|
+
"Content-Type": "text/event-stream",
|
|
8010
|
+
"Cache-Control": "no-cache",
|
|
8011
|
+
Connection: "keep-alive",
|
|
8012
|
+
"Access-Control-Allow-Origin": "*"
|
|
8013
|
+
});
|
|
8014
|
+
const sendEvent = () => {
|
|
8015
|
+
const s = collectDashboardSnapshot(repoRoot);
|
|
8016
|
+
const st = loadState(repoRoot);
|
|
8017
|
+
const cfg = resolveConfig({}, repoRoot).config;
|
|
8018
|
+
const td = {};
|
|
8019
|
+
if (st) {
|
|
8020
|
+
for (const [id, inst] of Object.entries(st.instances)) {
|
|
8021
|
+
if (!inst?.installed || inst.bridgeMode !== "app-server") continue;
|
|
8022
|
+
td[id] = getTurnInfo(cfg.stateDir, id);
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
res.write(`data: ${JSON.stringify({ ...s, turns: td })}
|
|
8026
|
+
|
|
8027
|
+
`);
|
|
8028
|
+
};
|
|
8029
|
+
sendEvent();
|
|
8030
|
+
const interval = setInterval(sendEvent, 5e3);
|
|
8031
|
+
req.on("close", () => clearInterval(interval));
|
|
8032
|
+
return;
|
|
8033
|
+
}
|
|
8034
|
+
if (req.url === "/api/missions") {
|
|
8035
|
+
const missions = parseMissionsFile(repoRoot);
|
|
8036
|
+
res.writeHead(200, jsonHeaders);
|
|
8037
|
+
res.end(JSON.stringify(missions, null, 2));
|
|
8038
|
+
return;
|
|
8039
|
+
}
|
|
8040
|
+
if (req.url === "/missions") {
|
|
8041
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
8042
|
+
res.end(buildMissionsHtml(repoRoot));
|
|
8043
|
+
return;
|
|
8044
|
+
}
|
|
8045
|
+
if (req.url === "/api/prs") {
|
|
8046
|
+
const prs = fetchPrs(repoRoot);
|
|
8047
|
+
res.writeHead(200, jsonHeaders);
|
|
8048
|
+
res.end(JSON.stringify(prs, null, 2));
|
|
8049
|
+
return;
|
|
8050
|
+
}
|
|
8051
|
+
if (req.url === "/prs") {
|
|
8052
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
8053
|
+
res.end(buildPrsHtml(repoRoot));
|
|
8054
|
+
return;
|
|
8055
|
+
}
|
|
8056
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
8057
|
+
res.end(buildHtml(snapshot, turnData));
|
|
8058
|
+
});
|
|
8059
|
+
return new Promise((resolve14) => {
|
|
8060
|
+
server.on("error", (err) => {
|
|
8061
|
+
if (err.code === "EADDRINUSE") {
|
|
8062
|
+
resolve14({
|
|
8063
|
+
ok: false,
|
|
8064
|
+
command: "gui",
|
|
8065
|
+
code: "TAP_PORT_IN_USE",
|
|
8066
|
+
message: `Port ${port} is already in use. Try: tap gui --port <other>`,
|
|
8067
|
+
warnings: [],
|
|
8068
|
+
data: {}
|
|
8069
|
+
});
|
|
8070
|
+
} else {
|
|
8071
|
+
resolve14({
|
|
8072
|
+
ok: false,
|
|
8073
|
+
command: "gui",
|
|
8074
|
+
code: "TAP_GUI_ERROR",
|
|
8075
|
+
message: err.message,
|
|
8076
|
+
warnings: [],
|
|
8077
|
+
data: {}
|
|
8078
|
+
});
|
|
8079
|
+
}
|
|
8080
|
+
});
|
|
8081
|
+
server.listen(port, "127.0.0.1", () => {
|
|
8082
|
+
logHeader("tap gui dashboard");
|
|
8083
|
+
logSuccess(`Dashboard: http://127.0.0.1:${port}`);
|
|
8084
|
+
log(`API: http://127.0.0.1:${port}/api/snapshot`);
|
|
8085
|
+
log("Press Ctrl+C to stop");
|
|
8086
|
+
});
|
|
8087
|
+
});
|
|
8088
|
+
}
|
|
8089
|
+
|
|
7151
8090
|
// src/output.ts
|
|
7152
8091
|
function emitResult(result, jsonMode) {
|
|
7153
8092
|
if (jsonMode) {
|
|
@@ -7240,6 +8179,8 @@ Commands:
|
|
|
7240
8179
|
down Stop all running bridge daemons
|
|
7241
8180
|
comms <pull|push> Sync comms directory with remote repo
|
|
7242
8181
|
dashboard Show unified ops dashboard
|
|
8182
|
+
watch Monitor bridges and auto-restart stuck ones
|
|
8183
|
+
gui Start local web dashboard (http)
|
|
7243
8184
|
doctor Diagnose tap infrastructure health
|
|
7244
8185
|
serve Start tap MCP server (stdio)
|
|
7245
8186
|
version Show version
|
|
@@ -7270,6 +8211,8 @@ function normalizeCommandName(command) {
|
|
|
7270
8211
|
case "dashboard":
|
|
7271
8212
|
case "doctor":
|
|
7272
8213
|
case "serve":
|
|
8214
|
+
case "watch":
|
|
8215
|
+
case "gui":
|
|
7273
8216
|
return command;
|
|
7274
8217
|
default:
|
|
7275
8218
|
return "unknown";
|
|
@@ -7334,6 +8277,12 @@ async function main() {
|
|
|
7334
8277
|
case "doctor":
|
|
7335
8278
|
result = await doctorCommand(commandArgs);
|
|
7336
8279
|
break;
|
|
8280
|
+
case "watch":
|
|
8281
|
+
result = await watchCommand(commandArgs);
|
|
8282
|
+
break;
|
|
8283
|
+
case "gui":
|
|
8284
|
+
result = await guiCommand(commandArgs);
|
|
8285
|
+
break;
|
|
7337
8286
|
case "serve": {
|
|
7338
8287
|
const serveResult = await serveCommand(commandArgs);
|
|
7339
8288
|
if (!serveResult.ok || serveResult.code === "TAP_NO_OP") {
|