@hua-labs/tap 0.2.1 → 0.2.3
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 +456 -52
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +63 -1
- package/dist/index.mjs +302 -17
- 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;
|
|
@@ -541,9 +556,25 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
541
556
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
542
557
|
}
|
|
543
558
|
const isBundled = sourcePath.endsWith(".mjs");
|
|
559
|
+
const isEphemeralSource = isEphemeralPath(sourcePath);
|
|
544
560
|
let command = bunCommand;
|
|
545
|
-
|
|
546
|
-
|
|
561
|
+
let args = [toForwardSlashPath(sourcePath)];
|
|
562
|
+
if (isEphemeralSource && isBundled) {
|
|
563
|
+
command = "npx";
|
|
564
|
+
args = ["@hua-labs/tap", "serve"];
|
|
565
|
+
warnings.push(
|
|
566
|
+
"Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
|
|
567
|
+
);
|
|
568
|
+
} else if (!command && isBundled) {
|
|
569
|
+
const isEphemeralNode = isEphemeralPath(process.execPath);
|
|
570
|
+
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
|
+
}
|
|
547
578
|
warnings.push(
|
|
548
579
|
"bun not found; using node to run the compiled MCP server. Install bun for better performance."
|
|
549
580
|
);
|
|
@@ -555,8 +586,8 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
555
586
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
556
587
|
}
|
|
557
588
|
return {
|
|
558
|
-
command
|
|
559
|
-
args
|
|
589
|
+
command,
|
|
590
|
+
args,
|
|
560
591
|
env,
|
|
561
592
|
sourcePath,
|
|
562
593
|
warnings,
|
|
@@ -1558,6 +1589,40 @@ function isBridgeRunning(stateDir, instanceId) {
|
|
|
1558
1589
|
if (!state) return false;
|
|
1559
1590
|
return isProcessAlive(state.pid);
|
|
1560
1591
|
}
|
|
1592
|
+
function resolveAgentName(instanceId, explicit, context) {
|
|
1593
|
+
if (explicit) return explicit;
|
|
1594
|
+
try {
|
|
1595
|
+
const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
|
|
1596
|
+
const state = loadState(repoRoot);
|
|
1597
|
+
const stateAgent = state?.instances[instanceId]?.agentName;
|
|
1598
|
+
if (stateAgent) return stateAgent;
|
|
1599
|
+
} catch {
|
|
1600
|
+
}
|
|
1601
|
+
return process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME || null;
|
|
1602
|
+
}
|
|
1603
|
+
function inferRestartMode(bridgeState, flags, savedMode) {
|
|
1604
|
+
const wasManaged = bridgeState?.appServer != null;
|
|
1605
|
+
const hadAuth = bridgeState?.appServer?.auth != null;
|
|
1606
|
+
const manageAppServer = flags?.noServer === true ? false : flags?.noServer === void 0 ? savedMode?.manageAppServer ?? wasManaged : true;
|
|
1607
|
+
const noAuth = flags?.noAuth === true ? true : flags?.noAuth === void 0 ? savedMode?.noAuth ?? !hadAuth : false;
|
|
1608
|
+
return { manageAppServer, noAuth };
|
|
1609
|
+
}
|
|
1610
|
+
function cleanupHeadlessDispatch(inboxDir, agentName) {
|
|
1611
|
+
const removed = [];
|
|
1612
|
+
if (!fs7.existsSync(inboxDir)) return removed;
|
|
1613
|
+
const normalizedAgent = agentName.replace(/-/g, "_");
|
|
1614
|
+
const marker = `-headless-${normalizedAgent}-review-`;
|
|
1615
|
+
try {
|
|
1616
|
+
for (const file of fs7.readdirSync(inboxDir)) {
|
|
1617
|
+
if (file.includes(marker)) {
|
|
1618
|
+
fs7.unlinkSync(path7.join(inboxDir, file));
|
|
1619
|
+
removed.push(file);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
} catch {
|
|
1623
|
+
}
|
|
1624
|
+
return removed;
|
|
1625
|
+
}
|
|
1561
1626
|
async function startBridge(options) {
|
|
1562
1627
|
const {
|
|
1563
1628
|
instanceId,
|
|
@@ -1568,7 +1633,10 @@ async function startBridge(options) {
|
|
|
1568
1633
|
agentName,
|
|
1569
1634
|
port
|
|
1570
1635
|
} = options;
|
|
1571
|
-
const resolvedAgent = agentName
|
|
1636
|
+
const resolvedAgent = resolveAgentName(instanceId, agentName, {
|
|
1637
|
+
repoRoot: options.repoRoot,
|
|
1638
|
+
stateDir
|
|
1639
|
+
});
|
|
1572
1640
|
if (!resolvedAgent) {
|
|
1573
1641
|
throw new Error(
|
|
1574
1642
|
`No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
|
|
@@ -1720,6 +1788,35 @@ async function stopBridge(options) {
|
|
|
1720
1788
|
clearBridgeState(stateDir, instanceId);
|
|
1721
1789
|
return true;
|
|
1722
1790
|
}
|
|
1791
|
+
async function restartBridge(options) {
|
|
1792
|
+
const { instanceId, stateDir, platform } = options;
|
|
1793
|
+
const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
|
|
1794
|
+
const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
|
|
1795
|
+
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
1796
|
+
const heartbeatPath = path7.join(runtimeStateDir, "heartbeat.json");
|
|
1797
|
+
if (fs7.existsSync(heartbeatPath)) {
|
|
1798
|
+
const startWait = Date.now();
|
|
1799
|
+
while (Date.now() - startWait < drainTimeout) {
|
|
1800
|
+
try {
|
|
1801
|
+
const hb = JSON.parse(fs7.readFileSync(heartbeatPath, "utf-8"));
|
|
1802
|
+
if (!hb.activeTurnId) break;
|
|
1803
|
+
} catch {
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1806
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (options.headless?.enabled && options.commsDir) {
|
|
1810
|
+
const agentName = options.agentName ?? instanceId;
|
|
1811
|
+
cleanupHeadlessDispatch(path7.join(options.commsDir, "inbox"), agentName);
|
|
1812
|
+
}
|
|
1813
|
+
await stopBridge({ instanceId, stateDir, platform });
|
|
1814
|
+
const restartOptions = {
|
|
1815
|
+
...options,
|
|
1816
|
+
processExistingMessages: true
|
|
1817
|
+
};
|
|
1818
|
+
return startBridge(restartOptions);
|
|
1819
|
+
}
|
|
1723
1820
|
function rotateLog(logPath) {
|
|
1724
1821
|
if (!fs7.existsSync(logPath)) return;
|
|
1725
1822
|
try {
|
|
@@ -1763,6 +1860,7 @@ var init_bridge = __esm({
|
|
|
1763
1860
|
"use strict";
|
|
1764
1861
|
init_common();
|
|
1765
1862
|
init_runtime();
|
|
1863
|
+
init_state();
|
|
1766
1864
|
DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
|
|
1767
1865
|
APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
1768
1866
|
APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
@@ -2396,13 +2494,12 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
2396
2494
|
});
|
|
2397
2495
|
}
|
|
2398
2496
|
if (mainTable && managed.command) {
|
|
2497
|
+
const expectedArgs = managed.args.map((a) => `"${a.replace(/\\/g, "\\\\")}"`).join(", ");
|
|
2399
2498
|
checks.push({
|
|
2400
2499
|
name: "Managed command configured",
|
|
2401
2500
|
passed: mainTable.includes(
|
|
2402
2501
|
`command = "${managed.command.replace(/\\/g, "\\\\")}"`
|
|
2403
|
-
) && mainTable.includes(
|
|
2404
|
-
`args = ["${managed.args[0]?.replace(/\\/g, "\\\\") ?? ""}"]`
|
|
2405
|
-
),
|
|
2502
|
+
) && mainTable.includes(`args = [${expectedArgs}]`),
|
|
2406
2503
|
message: "Managed tap-comms command/args do not match expected values"
|
|
2407
2504
|
});
|
|
2408
2505
|
}
|
|
@@ -3230,7 +3327,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3230
3327
|
log(`TUI connect: ${bridge.appServer.url}`);
|
|
3231
3328
|
}
|
|
3232
3329
|
}
|
|
3233
|
-
const updated = { ...instance, bridge };
|
|
3330
|
+
const updated = { ...instance, bridge, manageAppServer, noAuth };
|
|
3234
3331
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
3235
3332
|
saveState(repoRoot, newState);
|
|
3236
3333
|
return {
|
|
@@ -3721,6 +3818,121 @@ function bridgeStatusOne(identifier) {
|
|
|
3721
3818
|
}
|
|
3722
3819
|
};
|
|
3723
3820
|
}
|
|
3821
|
+
async function bridgeRestart(identifier, flags) {
|
|
3822
|
+
const repoRoot = findRepoRoot();
|
|
3823
|
+
const state = loadState(repoRoot);
|
|
3824
|
+
if (!state) {
|
|
3825
|
+
return {
|
|
3826
|
+
ok: false,
|
|
3827
|
+
command: "bridge",
|
|
3828
|
+
code: "TAP_NOT_INITIALIZED",
|
|
3829
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
3830
|
+
warnings: [],
|
|
3831
|
+
data: {}
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
const resolved = resolveInstanceId(identifier, state);
|
|
3835
|
+
if (!resolved.ok) {
|
|
3836
|
+
return {
|
|
3837
|
+
ok: false,
|
|
3838
|
+
command: "bridge",
|
|
3839
|
+
code: resolved.code,
|
|
3840
|
+
message: resolved.message,
|
|
3841
|
+
warnings: [],
|
|
3842
|
+
data: {}
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
const instanceId = resolved.instanceId;
|
|
3846
|
+
const inst = state.instances[instanceId];
|
|
3847
|
+
if (!inst) {
|
|
3848
|
+
return {
|
|
3849
|
+
ok: false,
|
|
3850
|
+
command: "bridge",
|
|
3851
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
3852
|
+
message: `Instance not found: ${instanceId}`,
|
|
3853
|
+
warnings: [],
|
|
3854
|
+
data: {}
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
const adapter = getAdapter(inst.runtime);
|
|
3858
|
+
const ctx = {
|
|
3859
|
+
...createAdapterContext(state.commsDir, repoRoot),
|
|
3860
|
+
instanceId
|
|
3861
|
+
};
|
|
3862
|
+
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
3863
|
+
if (!bridgeScript) {
|
|
3864
|
+
return {
|
|
3865
|
+
ok: false,
|
|
3866
|
+
command: "bridge",
|
|
3867
|
+
instanceId,
|
|
3868
|
+
code: "TAP_BRIDGE_SCRIPT_MISSING",
|
|
3869
|
+
message: `Bridge script not found for ${instanceId}`,
|
|
3870
|
+
warnings: [],
|
|
3871
|
+
data: {}
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
3875
|
+
const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
|
|
3876
|
+
const drainTimeout = parseInt(drainStr, 10) || 30;
|
|
3877
|
+
logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
|
|
3878
|
+
log(`Drain timeout: ${drainTimeout}s`);
|
|
3879
|
+
try {
|
|
3880
|
+
const currentBridgeState = loadBridgeState(ctx.stateDir, instanceId);
|
|
3881
|
+
const { manageAppServer, noAuth } = inferRestartMode(
|
|
3882
|
+
currentBridgeState,
|
|
3883
|
+
{
|
|
3884
|
+
noServer: flags["no-server"] === true ? true : void 0,
|
|
3885
|
+
noAuth: flags["no-auth"] === true ? true : void 0
|
|
3886
|
+
},
|
|
3887
|
+
{
|
|
3888
|
+
manageAppServer: inst.manageAppServer,
|
|
3889
|
+
noAuth: inst.noAuth
|
|
3890
|
+
}
|
|
3891
|
+
);
|
|
3892
|
+
const bridge = await restartBridge({
|
|
3893
|
+
instanceId,
|
|
3894
|
+
runtime: inst.runtime,
|
|
3895
|
+
stateDir: ctx.stateDir,
|
|
3896
|
+
commsDir: ctx.commsDir,
|
|
3897
|
+
bridgeScript,
|
|
3898
|
+
platform: ctx.platform,
|
|
3899
|
+
agentName: inst.agentName ?? void 0,
|
|
3900
|
+
runtimeCommand: resolvedConfig.runtimeCommand,
|
|
3901
|
+
appServerUrl: resolvedConfig.appServerUrl,
|
|
3902
|
+
repoRoot,
|
|
3903
|
+
port: inst.port ?? void 0,
|
|
3904
|
+
headless: inst.headless,
|
|
3905
|
+
drainTimeoutSeconds: drainTimeout,
|
|
3906
|
+
manageAppServer,
|
|
3907
|
+
noAuth
|
|
3908
|
+
});
|
|
3909
|
+
logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
|
|
3910
|
+
const updated = { ...inst, bridge, manageAppServer, noAuth };
|
|
3911
|
+
const newState = updateInstanceState(state, instanceId, updated);
|
|
3912
|
+
saveState(repoRoot, newState);
|
|
3913
|
+
return {
|
|
3914
|
+
ok: true,
|
|
3915
|
+
command: "bridge",
|
|
3916
|
+
instanceId,
|
|
3917
|
+
code: "TAP_BRIDGE_START_OK",
|
|
3918
|
+
message: `Bridge for ${instanceId} restarted (PID: ${bridge.pid})`,
|
|
3919
|
+
warnings: [],
|
|
3920
|
+
data: { pid: bridge.pid }
|
|
3921
|
+
};
|
|
3922
|
+
} catch (err) {
|
|
3923
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3924
|
+
logError(msg);
|
|
3925
|
+
return {
|
|
3926
|
+
ok: false,
|
|
3927
|
+
command: "bridge",
|
|
3928
|
+
instanceId,
|
|
3929
|
+
code: "TAP_BRIDGE_START_FAILED",
|
|
3930
|
+
message: msg,
|
|
3931
|
+
warnings: [],
|
|
3932
|
+
data: {}
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3724
3936
|
async function bridgeCommand(args) {
|
|
3725
3937
|
const { positional, flags } = parseArgs(args);
|
|
3726
3938
|
const subcommand = positional[0];
|
|
@@ -3780,12 +3992,25 @@ async function bridgeCommand(args) {
|
|
|
3780
3992
|
}
|
|
3781
3993
|
return bridgeStatusAll();
|
|
3782
3994
|
}
|
|
3995
|
+
case "restart": {
|
|
3996
|
+
if (!identifierArg) {
|
|
3997
|
+
return {
|
|
3998
|
+
ok: false,
|
|
3999
|
+
command: "bridge",
|
|
4000
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4001
|
+
message: "Missing instance. Usage: npx @hua-labs/tap bridge restart <instance>",
|
|
4002
|
+
warnings: [],
|
|
4003
|
+
data: {}
|
|
4004
|
+
};
|
|
4005
|
+
}
|
|
4006
|
+
return bridgeRestart(identifierArg, flags);
|
|
4007
|
+
}
|
|
3783
4008
|
default:
|
|
3784
4009
|
return {
|
|
3785
4010
|
ok: false,
|
|
3786
4011
|
command: "bridge",
|
|
3787
4012
|
code: "TAP_INVALID_ARGUMENT",
|
|
3788
|
-
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
|
|
4013
|
+
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
|
|
3789
4014
|
warnings: [],
|
|
3790
4015
|
data: {}
|
|
3791
4016
|
};
|
|
@@ -4003,6 +4228,9 @@ init_dashboard();
|
|
|
4003
4228
|
init_dashboard();
|
|
4004
4229
|
init_utils();
|
|
4005
4230
|
init_config();
|
|
4231
|
+
init_state();
|
|
4232
|
+
import * as fs13 from "fs";
|
|
4233
|
+
import * as path14 from "path";
|
|
4006
4234
|
function getDashboardSnapshot(options) {
|
|
4007
4235
|
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
4008
4236
|
return collectDashboardSnapshot(repoRoot, options?.commsDir);
|
|
@@ -4049,6 +4277,60 @@ async function stopAgents() {
|
|
|
4049
4277
|
commandResult: result
|
|
4050
4278
|
};
|
|
4051
4279
|
}
|
|
4280
|
+
function getHealthReport(options) {
|
|
4281
|
+
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
4282
|
+
const snapshot = collectDashboardSnapshot(repoRoot, options?.commsDir);
|
|
4283
|
+
const headlessStates = [];
|
|
4284
|
+
try {
|
|
4285
|
+
const state = loadState(repoRoot);
|
|
4286
|
+
const activeMatchers = /* @__PURE__ */ new Set();
|
|
4287
|
+
if (state) {
|
|
4288
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
4289
|
+
if (inst?.installed && inst.bridgeMode === "app-server") {
|
|
4290
|
+
activeMatchers.add(id);
|
|
4291
|
+
if (inst.agentName) activeMatchers.add(inst.agentName);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
const tmpDir = path14.join(repoRoot, ".tmp");
|
|
4296
|
+
if (fs13.existsSync(tmpDir)) {
|
|
4297
|
+
for (const dir of fs13.readdirSync(tmpDir)) {
|
|
4298
|
+
if (!dir.startsWith("codex-app-server-bridge")) continue;
|
|
4299
|
+
const suffix = dir.replace("codex-app-server-bridge-", "");
|
|
4300
|
+
if (activeMatchers.size > 0) {
|
|
4301
|
+
let matched = false;
|
|
4302
|
+
for (const matcher of activeMatchers) {
|
|
4303
|
+
if (suffix === matcher || suffix.startsWith(matcher)) {
|
|
4304
|
+
matched = true;
|
|
4305
|
+
break;
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
if (!matched) continue;
|
|
4309
|
+
}
|
|
4310
|
+
const hsPath = path14.join(tmpDir, dir, "headless-state.json");
|
|
4311
|
+
if (!fs13.existsSync(hsPath)) continue;
|
|
4312
|
+
try {
|
|
4313
|
+
const hs = JSON.parse(fs13.readFileSync(hsPath, "utf-8"));
|
|
4314
|
+
headlessStates.push({ instanceDir: dir, ...hs });
|
|
4315
|
+
} catch {
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
} catch {
|
|
4320
|
+
}
|
|
4321
|
+
const hasFailures = snapshot.warnings.some((w) => w.level === "error");
|
|
4322
|
+
const hasBridgeDown = snapshot.bridges.some(
|
|
4323
|
+
(b) => b.status === "stale" || b.status === "stopped"
|
|
4324
|
+
);
|
|
4325
|
+
return {
|
|
4326
|
+
ok: !hasFailures && !hasBridgeDown,
|
|
4327
|
+
timestamp: snapshot.generatedAt,
|
|
4328
|
+
bridges: snapshot.bridges,
|
|
4329
|
+
agents: snapshot.agents,
|
|
4330
|
+
warnings: snapshot.warnings,
|
|
4331
|
+
headless: headlessStates
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4052
4334
|
function getConfig(options) {
|
|
4053
4335
|
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
4054
4336
|
const { config } = resolveConfig({}, repoRoot);
|
|
@@ -4104,8 +4386,9 @@ async function handleEvents(req, res, apiOptions) {
|
|
|
4104
4386
|
}
|
|
4105
4387
|
res.end();
|
|
4106
4388
|
}
|
|
4107
|
-
function handleHealth(res) {
|
|
4108
|
-
|
|
4389
|
+
function handleHealth(res, apiOptions) {
|
|
4390
|
+
const report = getHealthReport(apiOptions);
|
|
4391
|
+
jsonResponse(res, report);
|
|
4109
4392
|
}
|
|
4110
4393
|
async function startHttpServer(options) {
|
|
4111
4394
|
const port = options?.port ?? 4580;
|
|
@@ -4136,7 +4419,7 @@ async function startHttpServer(options) {
|
|
|
4136
4419
|
handleConfig(res, apiOptions);
|
|
4137
4420
|
return;
|
|
4138
4421
|
case "/health":
|
|
4139
|
-
handleHealth(res);
|
|
4422
|
+
handleHealth(res, apiOptions);
|
|
4140
4423
|
return;
|
|
4141
4424
|
}
|
|
4142
4425
|
}
|
|
@@ -4192,6 +4475,7 @@ export {
|
|
|
4192
4475
|
getConfig,
|
|
4193
4476
|
getDashboardSnapshot,
|
|
4194
4477
|
getFnmBinDir,
|
|
4478
|
+
getHealthReport,
|
|
4195
4479
|
getHeartbeatAge,
|
|
4196
4480
|
loadLocalConfig,
|
|
4197
4481
|
loadSharedConfig,
|
|
@@ -4200,6 +4484,7 @@ export {
|
|
|
4200
4484
|
readNodeVersion,
|
|
4201
4485
|
resolveConfig,
|
|
4202
4486
|
resolveNodeRuntime,
|
|
4487
|
+
restartBridge,
|
|
4203
4488
|
rotateLog,
|
|
4204
4489
|
saveLocalConfig,
|
|
4205
4490
|
saveSharedConfig,
|