@hua-labs/tap 0.2.1 → 0.2.2
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/README.md +2 -2
- package/dist/bridges/codex-app-server-bridge.mjs +8 -1
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +102 -29
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +458 -51
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +63 -1
- package/dist/index.mjs +304 -16
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +461 -198
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -10,6 +10,8 @@ interface AdapterContext {
|
|
|
10
10
|
platform: Platform;
|
|
11
11
|
/** Instance ID for TAP_AGENT_ID env injection. Set by 'tap add'. */
|
|
12
12
|
instanceId?: string;
|
|
13
|
+
/** Agent name from state. Injected as TAP_AGENT_NAME in MCP config. */
|
|
14
|
+
agentName?: string;
|
|
13
15
|
}
|
|
14
16
|
interface ProbeResult {
|
|
15
17
|
installed: boolean;
|
|
@@ -129,6 +131,10 @@ interface InstanceState {
|
|
|
129
131
|
bridge: BridgeState | null;
|
|
130
132
|
/** Headless mode configuration. null = interactive (default). */
|
|
131
133
|
headless: HeadlessConfig | null;
|
|
134
|
+
/** Whether bridge manages its own app-server process. Saved for restart mode preservation. */
|
|
135
|
+
manageAppServer?: boolean;
|
|
136
|
+
/** Whether bridge runs without auth gateway. Saved for restart mode preservation. */
|
|
137
|
+
noAuth?: boolean;
|
|
132
138
|
warnings: string[];
|
|
133
139
|
}
|
|
134
140
|
/** @deprecated Use InstanceState. Kept for v1 state migration. */
|
|
@@ -199,6 +205,8 @@ interface TapSharedConfig {
|
|
|
199
205
|
appServerUrl?: string;
|
|
200
206
|
/** GitHub URL for the comms repository (used by `tap comms pull/push`). */
|
|
201
207
|
commsRepoUrl?: string;
|
|
208
|
+
/** Control tower agent name. Used for auto-notify on new agent join (M111). */
|
|
209
|
+
towerName?: string;
|
|
202
210
|
}
|
|
203
211
|
/**
|
|
204
212
|
* Local config (tap-config.local.json) — gitignored, machine-specific overrides.
|
|
@@ -214,6 +222,7 @@ interface TapResolvedConfig {
|
|
|
214
222
|
stateDir: string;
|
|
215
223
|
runtimeCommand: string;
|
|
216
224
|
appServerUrl: string;
|
|
225
|
+
towerName: string | null;
|
|
217
226
|
}
|
|
218
227
|
/** Config resolution source for diagnostics. */
|
|
219
228
|
type ConfigSource = "cli-flag" | "env" | "local-config" | "shared-config" | "legacy-shell-config" | "auto";
|
|
@@ -239,6 +248,46 @@ declare function resolveConfig(overrides?: ConfigOverrides, startDir?: string):
|
|
|
239
248
|
declare function saveSharedConfig(repoRoot: string, config: TapSharedConfig): void;
|
|
240
249
|
declare function saveLocalConfig(repoRoot: string, config: TapLocalConfig): void;
|
|
241
250
|
|
|
251
|
+
interface BridgeStartOptions {
|
|
252
|
+
instanceId: InstanceId;
|
|
253
|
+
runtime: RuntimeName;
|
|
254
|
+
stateDir: string;
|
|
255
|
+
commsDir: string;
|
|
256
|
+
bridgeScript: string;
|
|
257
|
+
platform: Platform;
|
|
258
|
+
agentName?: string;
|
|
259
|
+
runtimeCommand?: string;
|
|
260
|
+
appServerUrl?: string;
|
|
261
|
+
repoRoot?: string;
|
|
262
|
+
port?: number;
|
|
263
|
+
/** Headless configuration. Passed as env vars to the bridge process. */
|
|
264
|
+
headless?: HeadlessConfig | null;
|
|
265
|
+
/** Bridge script operational flags (forwarded to codex-app-server-bridge.ts) */
|
|
266
|
+
busyMode?: "steer" | "wait";
|
|
267
|
+
pollSeconds?: number;
|
|
268
|
+
reconnectSeconds?: number;
|
|
269
|
+
messageLookbackMinutes?: number;
|
|
270
|
+
threadId?: string;
|
|
271
|
+
ephemeral?: boolean;
|
|
272
|
+
processExistingMessages?: boolean;
|
|
273
|
+
manageAppServer?: boolean;
|
|
274
|
+
/** Skip auth gateway — app-server listens directly on the public port (localhost only). */
|
|
275
|
+
noAuth?: boolean;
|
|
276
|
+
}
|
|
277
|
+
interface RestartBridgeOptions extends BridgeStartOptions {
|
|
278
|
+
/** Max seconds to wait for active turn to complete before killing. Default: 30 */
|
|
279
|
+
drainTimeoutSeconds?: number;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Graceful bridge restart: wait for active turn → cleanup → stop → start.
|
|
283
|
+
* Prevents message loss during restart by draining active work first
|
|
284
|
+
* and replaying unprocessed messages on the new instance.
|
|
285
|
+
*
|
|
286
|
+
* For headless instances: drain phase cleans up headless dispatch files
|
|
287
|
+
* to prevent the new bridge from re-injecting completed review requests.
|
|
288
|
+
* (별 finding: eager marking + replay collision)
|
|
289
|
+
*/
|
|
290
|
+
declare function restartBridge(options: RestartBridgeOptions): Promise<BridgeState>;
|
|
242
291
|
declare function rotateLog(logPath: string): void;
|
|
243
292
|
/**
|
|
244
293
|
* Update the heartbeat timestamp for a running bridge.
|
|
@@ -355,6 +404,19 @@ declare function startAgents(options?: AgentControlOptions): Promise<AgentContro
|
|
|
355
404
|
* Always operates on the cwd-based repo (same as CLI commands).
|
|
356
405
|
*/
|
|
357
406
|
declare function stopAgents(): Promise<AgentControlResult>;
|
|
407
|
+
interface HealthReport {
|
|
408
|
+
ok: boolean;
|
|
409
|
+
timestamp: string;
|
|
410
|
+
bridges: DashboardSnapshot["bridges"];
|
|
411
|
+
agents: DashboardSnapshot["agents"];
|
|
412
|
+
warnings: DashboardSnapshot["warnings"];
|
|
413
|
+
headless: Record<string, unknown>[];
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Health check that combines dashboard snapshot with headless state.
|
|
417
|
+
* Consumed by monitoring tools (Uptime Kuma, cron, autopilot).
|
|
418
|
+
*/
|
|
419
|
+
declare function getHealthReport(options?: StateApiOptions): HealthReport;
|
|
358
420
|
/**
|
|
359
421
|
* Resolve tap configuration for API consumers.
|
|
360
422
|
* Returns paths and settings without requiring CLI args.
|
|
@@ -431,4 +493,4 @@ declare function resolveNodeRuntime(configCommand: string, repoRoot: string): Re
|
|
|
431
493
|
*/
|
|
432
494
|
declare function buildRuntimeEnv(repoRoot: string, baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
433
495
|
|
|
434
|
-
export { type AdapterContext, type AgentControlOptions, type AgentControlResult, type AgentInfo, type AppServerAuthState, type AppServerState, type ApplyResult, type ArtifactKind, type BridgeInfo, type BridgeMode, type BridgeState, type CommandCode, type CommandName, type CommandResult, type ConfigOverrides, type ConfigResolution, type ConfigSource, type DashboardSnapshot, type DashboardWarning, type EventStreamOptions, type HttpServerOptions, type InstanceId, type InstanceState, LOCAL_CONFIG_FILE, type OwnedArtifact, type PRInfo, type PatchOp, type PatchOpType, type PatchPlan, type Platform, type ProbeResult, type ResolvedRuntime, type RuntimeAdapter, type RuntimeName, type RuntimeSource, type RuntimeState, SHARED_CONFIG_FILE, type StateApiOptions, type TapLocalConfig, type TapResolvedConfig, type TapSharedConfig, type TapState, type TapStateV1, type VerifyCheck, type VerifyResult, buildRuntimeEnv, collectDashboardSnapshot, createInitialState, getConfig, getDashboardSnapshot, getFnmBinDir, getHeartbeatAge, loadLocalConfig, loadSharedConfig, loadState, probeFnmNode, readNodeVersion, resolveConfig, resolveNodeRuntime, rotateLog, saveLocalConfig, saveSharedConfig, saveState, startAgents, startHttpServer, stateExists, stopAgents, streamEvents, updateBridgeHeartbeat, version };
|
|
496
|
+
export { type AdapterContext, type AgentControlOptions, type AgentControlResult, type AgentInfo, type AppServerAuthState, type AppServerState, type ApplyResult, type ArtifactKind, type BridgeInfo, type BridgeMode, type BridgeState, type CommandCode, type CommandName, type CommandResult, type ConfigOverrides, type ConfigResolution, type ConfigSource, type DashboardSnapshot, type DashboardWarning, type EventStreamOptions, type HealthReport, type HttpServerOptions, type InstanceId, type InstanceState, LOCAL_CONFIG_FILE, type OwnedArtifact, type PRInfo, type PatchOp, type PatchOpType, type PatchPlan, type Platform, type ProbeResult, type ResolvedRuntime, type RuntimeAdapter, type RuntimeName, type RuntimeSource, type RuntimeState, SHARED_CONFIG_FILE, type StateApiOptions, type TapLocalConfig, type TapResolvedConfig, type TapSharedConfig, type TapState, type TapStateV1, type VerifyCheck, type VerifyResult, buildRuntimeEnv, collectDashboardSnapshot, createInitialState, getConfig, getDashboardSnapshot, getFnmBinDir, getHealthReport, getHeartbeatAge, loadLocalConfig, loadSharedConfig, loadState, probeFnmNode, readNodeVersion, resolveConfig, resolveNodeRuntime, restartBridge, rotateLog, saveLocalConfig, saveSharedConfig, saveState, startAgents, startHttpServer, stateExists, stopAgents, streamEvents, updateBridgeHeartbeat, version };
|
package/dist/index.mjs
CHANGED
|
@@ -199,7 +199,8 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
199
199
|
commsDir: "auto",
|
|
200
200
|
stateDir: "auto",
|
|
201
201
|
runtimeCommand: "auto",
|
|
202
|
-
appServerUrl: "auto"
|
|
202
|
+
appServerUrl: "auto",
|
|
203
|
+
towerName: "auto"
|
|
203
204
|
};
|
|
204
205
|
let commsDir;
|
|
205
206
|
if (overrides.commsDir) {
|
|
@@ -268,8 +269,16 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
268
269
|
} else {
|
|
269
270
|
appServerUrl = DEFAULT_APP_SERVER_URL;
|
|
270
271
|
}
|
|
272
|
+
const towerName = local.towerName ?? shared.towerName ?? null;
|
|
271
273
|
return {
|
|
272
|
-
config: {
|
|
274
|
+
config: {
|
|
275
|
+
repoRoot,
|
|
276
|
+
commsDir,
|
|
277
|
+
stateDir,
|
|
278
|
+
runtimeCommand,
|
|
279
|
+
appServerUrl,
|
|
280
|
+
towerName
|
|
281
|
+
},
|
|
273
282
|
sources
|
|
274
283
|
};
|
|
275
284
|
}
|
|
@@ -469,6 +478,10 @@ function canWriteOrCreate(filePath) {
|
|
|
469
478
|
return false;
|
|
470
479
|
}
|
|
471
480
|
}
|
|
481
|
+
function isEphemeralPath(p) {
|
|
482
|
+
const normalized = p.replace(/\\/g, "/").toLowerCase();
|
|
483
|
+
return normalized.includes("/_npx/") || normalized.includes("\\_npx\\") || normalized.includes("/fnm_multishells/") || normalized.includes("\\fnm_multishells\\") || normalized.includes("/tmp/") || normalized.includes("\\temp\\");
|
|
484
|
+
}
|
|
472
485
|
function findLocalTapCommsSource(ctx) {
|
|
473
486
|
const candidates = [
|
|
474
487
|
path5.join(
|
|
@@ -528,8 +541,10 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
528
541
|
const warnings = [];
|
|
529
542
|
const issues = [];
|
|
530
543
|
const env = {
|
|
531
|
-
TAP_AGENT_NAME: "<set-per-session>",
|
|
532
|
-
TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
|
|
544
|
+
TAP_AGENT_NAME: ctx.agentName ?? "<set-per-session>",
|
|
545
|
+
TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir),
|
|
546
|
+
TAP_STATE_DIR: toForwardSlashPath(ctx.stateDir),
|
|
547
|
+
TAP_REPO_ROOT: toForwardSlashPath(ctx.repoRoot)
|
|
533
548
|
};
|
|
534
549
|
if (instanceId) {
|
|
535
550
|
env.TAP_AGENT_ID = instanceId;
|
|
@@ -542,11 +557,29 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
542
557
|
}
|
|
543
558
|
const isBundled = sourcePath.endsWith(".mjs");
|
|
544
559
|
let command = bunCommand;
|
|
560
|
+
let args = [toForwardSlashPath(sourcePath)];
|
|
545
561
|
if (!command && isBundled) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
562
|
+
const isEphemeralSource = isEphemeralPath(sourcePath);
|
|
563
|
+
const isEphemeralNode = isEphemeralPath(process.execPath);
|
|
564
|
+
if (isEphemeralSource) {
|
|
565
|
+
command = "npx";
|
|
566
|
+
args = ["@hua-labs/tap", "serve"];
|
|
567
|
+
warnings.push(
|
|
568
|
+
"Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
|
|
569
|
+
);
|
|
570
|
+
} else if (isEphemeralNode) {
|
|
571
|
+
command = "node";
|
|
572
|
+
warnings.push(
|
|
573
|
+
"Detected ephemeral node path. Using `node` from PATH for MCP config stability."
|
|
574
|
+
);
|
|
575
|
+
} else {
|
|
576
|
+
command = toForwardSlashPath(process.execPath);
|
|
577
|
+
}
|
|
578
|
+
if (!isEphemeralSource) {
|
|
579
|
+
warnings.push(
|
|
580
|
+
"bun not found; using node to run the compiled MCP server. Install bun for better performance."
|
|
581
|
+
);
|
|
582
|
+
}
|
|
550
583
|
}
|
|
551
584
|
if (!command) {
|
|
552
585
|
issues.push(
|
|
@@ -555,8 +588,8 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
555
588
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
556
589
|
}
|
|
557
590
|
return {
|
|
558
|
-
command
|
|
559
|
-
args
|
|
591
|
+
command,
|
|
592
|
+
args,
|
|
560
593
|
env,
|
|
561
594
|
sourcePath,
|
|
562
595
|
warnings,
|
|
@@ -1558,6 +1591,40 @@ function isBridgeRunning(stateDir, instanceId) {
|
|
|
1558
1591
|
if (!state) return false;
|
|
1559
1592
|
return isProcessAlive(state.pid);
|
|
1560
1593
|
}
|
|
1594
|
+
function resolveAgentName(instanceId, explicit, context) {
|
|
1595
|
+
if (explicit) return explicit;
|
|
1596
|
+
try {
|
|
1597
|
+
const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
|
|
1598
|
+
const state = loadState(repoRoot);
|
|
1599
|
+
const stateAgent = state?.instances[instanceId]?.agentName;
|
|
1600
|
+
if (stateAgent) return stateAgent;
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
return process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME || null;
|
|
1604
|
+
}
|
|
1605
|
+
function inferRestartMode(bridgeState, flags, savedMode) {
|
|
1606
|
+
const wasManaged = bridgeState?.appServer != null;
|
|
1607
|
+
const hadAuth = bridgeState?.appServer?.auth != null;
|
|
1608
|
+
const manageAppServer = flags?.noServer === true ? false : flags?.noServer === void 0 ? savedMode?.manageAppServer ?? wasManaged : true;
|
|
1609
|
+
const noAuth = flags?.noAuth === true ? true : flags?.noAuth === void 0 ? savedMode?.noAuth ?? !hadAuth : false;
|
|
1610
|
+
return { manageAppServer, noAuth };
|
|
1611
|
+
}
|
|
1612
|
+
function cleanupHeadlessDispatch(inboxDir, agentName) {
|
|
1613
|
+
const removed = [];
|
|
1614
|
+
if (!fs7.existsSync(inboxDir)) return removed;
|
|
1615
|
+
const normalizedAgent = agentName.replace(/-/g, "_");
|
|
1616
|
+
const marker = `-headless-${normalizedAgent}-review-`;
|
|
1617
|
+
try {
|
|
1618
|
+
for (const file of fs7.readdirSync(inboxDir)) {
|
|
1619
|
+
if (file.includes(marker)) {
|
|
1620
|
+
fs7.unlinkSync(path7.join(inboxDir, file));
|
|
1621
|
+
removed.push(file);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
} catch {
|
|
1625
|
+
}
|
|
1626
|
+
return removed;
|
|
1627
|
+
}
|
|
1561
1628
|
async function startBridge(options) {
|
|
1562
1629
|
const {
|
|
1563
1630
|
instanceId,
|
|
@@ -1568,7 +1635,10 @@ async function startBridge(options) {
|
|
|
1568
1635
|
agentName,
|
|
1569
1636
|
port
|
|
1570
1637
|
} = options;
|
|
1571
|
-
const resolvedAgent = agentName
|
|
1638
|
+
const resolvedAgent = resolveAgentName(instanceId, agentName, {
|
|
1639
|
+
repoRoot: options.repoRoot,
|
|
1640
|
+
stateDir
|
|
1641
|
+
});
|
|
1572
1642
|
if (!resolvedAgent) {
|
|
1573
1643
|
throw new Error(
|
|
1574
1644
|
`No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
|
|
@@ -1720,6 +1790,35 @@ async function stopBridge(options) {
|
|
|
1720
1790
|
clearBridgeState(stateDir, instanceId);
|
|
1721
1791
|
return true;
|
|
1722
1792
|
}
|
|
1793
|
+
async function restartBridge(options) {
|
|
1794
|
+
const { instanceId, stateDir, platform } = options;
|
|
1795
|
+
const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
|
|
1796
|
+
const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
|
|
1797
|
+
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
1798
|
+
const heartbeatPath = path7.join(runtimeStateDir, "heartbeat.json");
|
|
1799
|
+
if (fs7.existsSync(heartbeatPath)) {
|
|
1800
|
+
const startWait = Date.now();
|
|
1801
|
+
while (Date.now() - startWait < drainTimeout) {
|
|
1802
|
+
try {
|
|
1803
|
+
const hb = JSON.parse(fs7.readFileSync(heartbeatPath, "utf-8"));
|
|
1804
|
+
if (!hb.activeTurnId) break;
|
|
1805
|
+
} catch {
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (options.headless?.enabled && options.commsDir) {
|
|
1812
|
+
const agentName = options.agentName ?? instanceId;
|
|
1813
|
+
cleanupHeadlessDispatch(path7.join(options.commsDir, "inbox"), agentName);
|
|
1814
|
+
}
|
|
1815
|
+
await stopBridge({ instanceId, stateDir, platform });
|
|
1816
|
+
const restartOptions = {
|
|
1817
|
+
...options,
|
|
1818
|
+
processExistingMessages: true
|
|
1819
|
+
};
|
|
1820
|
+
return startBridge(restartOptions);
|
|
1821
|
+
}
|
|
1723
1822
|
function rotateLog(logPath) {
|
|
1724
1823
|
if (!fs7.existsSync(logPath)) return;
|
|
1725
1824
|
try {
|
|
@@ -1763,6 +1862,7 @@ var init_bridge = __esm({
|
|
|
1763
1862
|
"use strict";
|
|
1764
1863
|
init_common();
|
|
1765
1864
|
init_runtime();
|
|
1865
|
+
init_state();
|
|
1766
1866
|
DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
|
|
1767
1867
|
APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
1768
1868
|
APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
@@ -3230,7 +3330,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3230
3330
|
log(`TUI connect: ${bridge.appServer.url}`);
|
|
3231
3331
|
}
|
|
3232
3332
|
}
|
|
3233
|
-
const updated = { ...instance, bridge };
|
|
3333
|
+
const updated = { ...instance, bridge, manageAppServer, noAuth };
|
|
3234
3334
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
3235
3335
|
saveState(repoRoot, newState);
|
|
3236
3336
|
return {
|
|
@@ -3721,6 +3821,121 @@ function bridgeStatusOne(identifier) {
|
|
|
3721
3821
|
}
|
|
3722
3822
|
};
|
|
3723
3823
|
}
|
|
3824
|
+
async function bridgeRestart(identifier, flags) {
|
|
3825
|
+
const repoRoot = findRepoRoot();
|
|
3826
|
+
const state = loadState(repoRoot);
|
|
3827
|
+
if (!state) {
|
|
3828
|
+
return {
|
|
3829
|
+
ok: false,
|
|
3830
|
+
command: "bridge",
|
|
3831
|
+
code: "TAP_NOT_INITIALIZED",
|
|
3832
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
3833
|
+
warnings: [],
|
|
3834
|
+
data: {}
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
const resolved = resolveInstanceId(identifier, state);
|
|
3838
|
+
if (!resolved.ok) {
|
|
3839
|
+
return {
|
|
3840
|
+
ok: false,
|
|
3841
|
+
command: "bridge",
|
|
3842
|
+
code: resolved.code,
|
|
3843
|
+
message: resolved.message,
|
|
3844
|
+
warnings: [],
|
|
3845
|
+
data: {}
|
|
3846
|
+
};
|
|
3847
|
+
}
|
|
3848
|
+
const instanceId = resolved.instanceId;
|
|
3849
|
+
const inst = state.instances[instanceId];
|
|
3850
|
+
if (!inst) {
|
|
3851
|
+
return {
|
|
3852
|
+
ok: false,
|
|
3853
|
+
command: "bridge",
|
|
3854
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
3855
|
+
message: `Instance not found: ${instanceId}`,
|
|
3856
|
+
warnings: [],
|
|
3857
|
+
data: {}
|
|
3858
|
+
};
|
|
3859
|
+
}
|
|
3860
|
+
const adapter = getAdapter(inst.runtime);
|
|
3861
|
+
const ctx = {
|
|
3862
|
+
...createAdapterContext(state.commsDir, repoRoot),
|
|
3863
|
+
instanceId
|
|
3864
|
+
};
|
|
3865
|
+
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
3866
|
+
if (!bridgeScript) {
|
|
3867
|
+
return {
|
|
3868
|
+
ok: false,
|
|
3869
|
+
command: "bridge",
|
|
3870
|
+
instanceId,
|
|
3871
|
+
code: "TAP_BRIDGE_SCRIPT_MISSING",
|
|
3872
|
+
message: `Bridge script not found for ${instanceId}`,
|
|
3873
|
+
warnings: [],
|
|
3874
|
+
data: {}
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
3878
|
+
const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
|
|
3879
|
+
const drainTimeout = parseInt(drainStr, 10) || 30;
|
|
3880
|
+
logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
|
|
3881
|
+
log(`Drain timeout: ${drainTimeout}s`);
|
|
3882
|
+
try {
|
|
3883
|
+
const currentBridgeState = loadBridgeState(ctx.stateDir, instanceId);
|
|
3884
|
+
const { manageAppServer, noAuth } = inferRestartMode(
|
|
3885
|
+
currentBridgeState,
|
|
3886
|
+
{
|
|
3887
|
+
noServer: flags["no-server"] === true ? true : void 0,
|
|
3888
|
+
noAuth: flags["no-auth"] === true ? true : void 0
|
|
3889
|
+
},
|
|
3890
|
+
{
|
|
3891
|
+
manageAppServer: inst.manageAppServer,
|
|
3892
|
+
noAuth: inst.noAuth
|
|
3893
|
+
}
|
|
3894
|
+
);
|
|
3895
|
+
const bridge = await restartBridge({
|
|
3896
|
+
instanceId,
|
|
3897
|
+
runtime: inst.runtime,
|
|
3898
|
+
stateDir: ctx.stateDir,
|
|
3899
|
+
commsDir: ctx.commsDir,
|
|
3900
|
+
bridgeScript,
|
|
3901
|
+
platform: ctx.platform,
|
|
3902
|
+
agentName: inst.agentName ?? void 0,
|
|
3903
|
+
runtimeCommand: resolvedConfig.runtimeCommand,
|
|
3904
|
+
appServerUrl: resolvedConfig.appServerUrl,
|
|
3905
|
+
repoRoot,
|
|
3906
|
+
port: inst.port ?? void 0,
|
|
3907
|
+
headless: inst.headless,
|
|
3908
|
+
drainTimeoutSeconds: drainTimeout,
|
|
3909
|
+
manageAppServer,
|
|
3910
|
+
noAuth
|
|
3911
|
+
});
|
|
3912
|
+
logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
|
|
3913
|
+
const updated = { ...inst, bridge, manageAppServer, noAuth };
|
|
3914
|
+
const newState = updateInstanceState(state, instanceId, updated);
|
|
3915
|
+
saveState(repoRoot, newState);
|
|
3916
|
+
return {
|
|
3917
|
+
ok: true,
|
|
3918
|
+
command: "bridge",
|
|
3919
|
+
instanceId,
|
|
3920
|
+
code: "TAP_BRIDGE_START_OK",
|
|
3921
|
+
message: `Bridge for ${instanceId} restarted (PID: ${bridge.pid})`,
|
|
3922
|
+
warnings: [],
|
|
3923
|
+
data: { pid: bridge.pid }
|
|
3924
|
+
};
|
|
3925
|
+
} catch (err) {
|
|
3926
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3927
|
+
logError(msg);
|
|
3928
|
+
return {
|
|
3929
|
+
ok: false,
|
|
3930
|
+
command: "bridge",
|
|
3931
|
+
instanceId,
|
|
3932
|
+
code: "TAP_BRIDGE_START_FAILED",
|
|
3933
|
+
message: msg,
|
|
3934
|
+
warnings: [],
|
|
3935
|
+
data: {}
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3724
3939
|
async function bridgeCommand(args) {
|
|
3725
3940
|
const { positional, flags } = parseArgs(args);
|
|
3726
3941
|
const subcommand = positional[0];
|
|
@@ -3780,12 +3995,25 @@ async function bridgeCommand(args) {
|
|
|
3780
3995
|
}
|
|
3781
3996
|
return bridgeStatusAll();
|
|
3782
3997
|
}
|
|
3998
|
+
case "restart": {
|
|
3999
|
+
if (!identifierArg) {
|
|
4000
|
+
return {
|
|
4001
|
+
ok: false,
|
|
4002
|
+
command: "bridge",
|
|
4003
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4004
|
+
message: "Missing instance. Usage: npx @hua-labs/tap bridge restart <instance>",
|
|
4005
|
+
warnings: [],
|
|
4006
|
+
data: {}
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
return bridgeRestart(identifierArg, flags);
|
|
4010
|
+
}
|
|
3783
4011
|
default:
|
|
3784
4012
|
return {
|
|
3785
4013
|
ok: false,
|
|
3786
4014
|
command: "bridge",
|
|
3787
4015
|
code: "TAP_INVALID_ARGUMENT",
|
|
3788
|
-
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
|
|
4016
|
+
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
|
|
3789
4017
|
warnings: [],
|
|
3790
4018
|
data: {}
|
|
3791
4019
|
};
|
|
@@ -4003,6 +4231,9 @@ init_dashboard();
|
|
|
4003
4231
|
init_dashboard();
|
|
4004
4232
|
init_utils();
|
|
4005
4233
|
init_config();
|
|
4234
|
+
init_state();
|
|
4235
|
+
import * as fs13 from "fs";
|
|
4236
|
+
import * as path14 from "path";
|
|
4006
4237
|
function getDashboardSnapshot(options) {
|
|
4007
4238
|
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
4008
4239
|
return collectDashboardSnapshot(repoRoot, options?.commsDir);
|
|
@@ -4049,6 +4280,60 @@ async function stopAgents() {
|
|
|
4049
4280
|
commandResult: result
|
|
4050
4281
|
};
|
|
4051
4282
|
}
|
|
4283
|
+
function getHealthReport(options) {
|
|
4284
|
+
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
4285
|
+
const snapshot = collectDashboardSnapshot(repoRoot, options?.commsDir);
|
|
4286
|
+
const headlessStates = [];
|
|
4287
|
+
try {
|
|
4288
|
+
const state = loadState(repoRoot);
|
|
4289
|
+
const activeMatchers = /* @__PURE__ */ new Set();
|
|
4290
|
+
if (state) {
|
|
4291
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
4292
|
+
if (inst?.installed && inst.bridgeMode === "app-server") {
|
|
4293
|
+
activeMatchers.add(id);
|
|
4294
|
+
if (inst.agentName) activeMatchers.add(inst.agentName);
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
const tmpDir = path14.join(repoRoot, ".tmp");
|
|
4299
|
+
if (fs13.existsSync(tmpDir)) {
|
|
4300
|
+
for (const dir of fs13.readdirSync(tmpDir)) {
|
|
4301
|
+
if (!dir.startsWith("codex-app-server-bridge")) continue;
|
|
4302
|
+
const suffix = dir.replace("codex-app-server-bridge-", "");
|
|
4303
|
+
if (activeMatchers.size > 0) {
|
|
4304
|
+
let matched = false;
|
|
4305
|
+
for (const matcher of activeMatchers) {
|
|
4306
|
+
if (suffix === matcher || suffix.startsWith(matcher)) {
|
|
4307
|
+
matched = true;
|
|
4308
|
+
break;
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
if (!matched) continue;
|
|
4312
|
+
}
|
|
4313
|
+
const hsPath = path14.join(tmpDir, dir, "headless-state.json");
|
|
4314
|
+
if (!fs13.existsSync(hsPath)) continue;
|
|
4315
|
+
try {
|
|
4316
|
+
const hs = JSON.parse(fs13.readFileSync(hsPath, "utf-8"));
|
|
4317
|
+
headlessStates.push({ instanceDir: dir, ...hs });
|
|
4318
|
+
} catch {
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
} catch {
|
|
4323
|
+
}
|
|
4324
|
+
const hasFailures = snapshot.warnings.some((w) => w.level === "error");
|
|
4325
|
+
const hasBridgeDown = snapshot.bridges.some(
|
|
4326
|
+
(b) => b.status === "stale" || b.status === "stopped"
|
|
4327
|
+
);
|
|
4328
|
+
return {
|
|
4329
|
+
ok: !hasFailures && !hasBridgeDown,
|
|
4330
|
+
timestamp: snapshot.generatedAt,
|
|
4331
|
+
bridges: snapshot.bridges,
|
|
4332
|
+
agents: snapshot.agents,
|
|
4333
|
+
warnings: snapshot.warnings,
|
|
4334
|
+
headless: headlessStates
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4052
4337
|
function getConfig(options) {
|
|
4053
4338
|
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
4054
4339
|
const { config } = resolveConfig({}, repoRoot);
|
|
@@ -4104,8 +4389,9 @@ async function handleEvents(req, res, apiOptions) {
|
|
|
4104
4389
|
}
|
|
4105
4390
|
res.end();
|
|
4106
4391
|
}
|
|
4107
|
-
function handleHealth(res) {
|
|
4108
|
-
|
|
4392
|
+
function handleHealth(res, apiOptions) {
|
|
4393
|
+
const report = getHealthReport(apiOptions);
|
|
4394
|
+
jsonResponse(res, report);
|
|
4109
4395
|
}
|
|
4110
4396
|
async function startHttpServer(options) {
|
|
4111
4397
|
const port = options?.port ?? 4580;
|
|
@@ -4136,7 +4422,7 @@ async function startHttpServer(options) {
|
|
|
4136
4422
|
handleConfig(res, apiOptions);
|
|
4137
4423
|
return;
|
|
4138
4424
|
case "/health":
|
|
4139
|
-
handleHealth(res);
|
|
4425
|
+
handleHealth(res, apiOptions);
|
|
4140
4426
|
return;
|
|
4141
4427
|
}
|
|
4142
4428
|
}
|
|
@@ -4192,6 +4478,7 @@ export {
|
|
|
4192
4478
|
getConfig,
|
|
4193
4479
|
getDashboardSnapshot,
|
|
4194
4480
|
getFnmBinDir,
|
|
4481
|
+
getHealthReport,
|
|
4195
4482
|
getHeartbeatAge,
|
|
4196
4483
|
loadLocalConfig,
|
|
4197
4484
|
loadSharedConfig,
|
|
@@ -4200,6 +4487,7 @@ export {
|
|
|
4200
4487
|
readNodeVersion,
|
|
4201
4488
|
resolveConfig,
|
|
4202
4489
|
resolveNodeRuntime,
|
|
4490
|
+
restartBridge,
|
|
4203
4491
|
rotateLog,
|
|
4204
4492
|
saveLocalConfig,
|
|
4205
4493
|
saveSharedConfig,
|