@hua-labs/tap 0.2.0 → 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.d.mts +55 -0
- package/dist/bridges/codex-app-server-bridge.mjs +1365 -0
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -0
- package/dist/bridges/codex-bridge-runner.d.mts +2 -1
- package/dist/bridges/codex-bridge-runner.mjs +143 -38
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +714 -64
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +94 -4
- package/dist/index.mjs +3795 -190
- 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.mjs
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
9
10
|
|
|
10
11
|
// src/utils.ts
|
|
11
12
|
import * as fs from "fs";
|
|
12
13
|
import * as path from "path";
|
|
13
|
-
|
|
14
|
+
function isValidRuntime(name) {
|
|
15
|
+
return VALID_RUNTIMES.includes(name);
|
|
16
|
+
}
|
|
17
|
+
function detectPlatform() {
|
|
18
|
+
return process.platform;
|
|
19
|
+
}
|
|
14
20
|
function _setNoGitWarned() {
|
|
15
21
|
_noGitWarned = true;
|
|
16
22
|
}
|
|
@@ -39,17 +45,94 @@ function findRepoRoot(startDir = process.cwd()) {
|
|
|
39
45
|
}
|
|
40
46
|
return process.cwd();
|
|
41
47
|
}
|
|
42
|
-
|
|
48
|
+
function createAdapterContext(commsDir, repoRoot) {
|
|
49
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
50
|
+
return {
|
|
51
|
+
commsDir: path.resolve(commsDir),
|
|
52
|
+
repoRoot: path.resolve(repoRoot),
|
|
53
|
+
stateDir: config.stateDir,
|
|
54
|
+
platform: detectPlatform()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function parseArgs(args) {
|
|
58
|
+
const positional = [];
|
|
59
|
+
const flags = {};
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const arg = args[i];
|
|
62
|
+
if (arg.startsWith("--")) {
|
|
63
|
+
const key = arg.slice(2);
|
|
64
|
+
const next = args[i + 1];
|
|
65
|
+
if (next && !next.startsWith("--")) {
|
|
66
|
+
flags[key] = next;
|
|
67
|
+
i++;
|
|
68
|
+
} else {
|
|
69
|
+
flags[key] = true;
|
|
70
|
+
}
|
|
71
|
+
} else if (arg.startsWith("-")) {
|
|
72
|
+
flags[arg.slice(1)] = true;
|
|
73
|
+
} else {
|
|
74
|
+
positional.push(arg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { positional, flags };
|
|
78
|
+
}
|
|
79
|
+
function log(message) {
|
|
80
|
+
if (!_jsonMode) console.log(` ${message}`);
|
|
81
|
+
}
|
|
82
|
+
function logSuccess(message) {
|
|
83
|
+
if (!_jsonMode) console.log(` + ${message}`);
|
|
84
|
+
}
|
|
43
85
|
function logWarn(message) {
|
|
44
86
|
if (!_jsonMode) console.log(` ! ${message}`);
|
|
45
87
|
}
|
|
88
|
+
function logError(message) {
|
|
89
|
+
if (!_jsonMode) console.error(` x ${message}`);
|
|
90
|
+
}
|
|
91
|
+
function logHeader(message) {
|
|
92
|
+
if (!_jsonMode) console.log(`
|
|
93
|
+
${message}
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
function resolveInstanceId(identifier, state) {
|
|
97
|
+
if (state.instances[identifier]) {
|
|
98
|
+
return { ok: true, instanceId: identifier };
|
|
99
|
+
}
|
|
100
|
+
if (isValidRuntime(identifier)) {
|
|
101
|
+
const matches = Object.values(state.instances).filter(
|
|
102
|
+
(inst) => inst.runtime === identifier
|
|
103
|
+
);
|
|
104
|
+
if (matches.length === 1) {
|
|
105
|
+
return { ok: true, instanceId: matches[0].instanceId };
|
|
106
|
+
}
|
|
107
|
+
if (matches.length > 1) {
|
|
108
|
+
const ids = matches.map((m) => m.instanceId).join(", ");
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
code: "TAP_INSTANCE_AMBIGUOUS",
|
|
112
|
+
message: `Multiple ${identifier} instances found: ${ids}. Specify one explicitly.`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
119
|
+
message: `Instance not found: ${identifier}`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
var VALID_RUNTIMES, _noGitWarned, _jsonMode;
|
|
123
|
+
var init_utils = __esm({
|
|
124
|
+
"src/utils.ts"() {
|
|
125
|
+
"use strict";
|
|
126
|
+
init_config();
|
|
127
|
+
VALID_RUNTIMES = ["claude", "codex", "gemini"];
|
|
128
|
+
_noGitWarned = false;
|
|
129
|
+
_jsonMode = false;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
46
132
|
|
|
47
133
|
// src/config/resolve.ts
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
var LEGACY_CONFIG_FILE = ".tap-config";
|
|
51
|
-
var DEFAULT_RUNTIME_COMMAND = "node";
|
|
52
|
-
var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
134
|
+
import * as fs2 from "fs";
|
|
135
|
+
import * as path2 from "path";
|
|
53
136
|
function findRepoRoot2(startDir = process.cwd()) {
|
|
54
137
|
let dir = path2.resolve(startDir);
|
|
55
138
|
while (true) {
|
|
@@ -116,7 +199,8 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
116
199
|
commsDir: "auto",
|
|
117
200
|
stateDir: "auto",
|
|
118
201
|
runtimeCommand: "auto",
|
|
119
|
-
appServerUrl: "auto"
|
|
202
|
+
appServerUrl: "auto",
|
|
203
|
+
towerName: "auto"
|
|
120
204
|
};
|
|
121
205
|
let commsDir;
|
|
122
206
|
if (overrides.commsDir) {
|
|
@@ -185,8 +269,16 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
185
269
|
} else {
|
|
186
270
|
appServerUrl = DEFAULT_APP_SERVER_URL;
|
|
187
271
|
}
|
|
272
|
+
const towerName = local.towerName ?? shared.towerName ?? null;
|
|
188
273
|
return {
|
|
189
|
-
config: {
|
|
274
|
+
config: {
|
|
275
|
+
repoRoot,
|
|
276
|
+
commsDir,
|
|
277
|
+
stateDir,
|
|
278
|
+
runtimeCommand,
|
|
279
|
+
appServerUrl,
|
|
280
|
+
towerName
|
|
281
|
+
},
|
|
190
282
|
sources
|
|
191
283
|
};
|
|
192
284
|
}
|
|
@@ -219,10 +311,31 @@ function normalizeTapPath(input) {
|
|
|
219
311
|
}
|
|
220
312
|
return trimmed;
|
|
221
313
|
}
|
|
314
|
+
var SHARED_CONFIG_FILE, LOCAL_CONFIG_FILE, LEGACY_CONFIG_FILE, DEFAULT_RUNTIME_COMMAND, DEFAULT_APP_SERVER_URL;
|
|
315
|
+
var init_resolve = __esm({
|
|
316
|
+
"src/config/resolve.ts"() {
|
|
317
|
+
"use strict";
|
|
318
|
+
init_utils();
|
|
319
|
+
SHARED_CONFIG_FILE = "tap-config.json";
|
|
320
|
+
LOCAL_CONFIG_FILE = "tap-config.local.json";
|
|
321
|
+
LEGACY_CONFIG_FILE = ".tap-config";
|
|
322
|
+
DEFAULT_RUNTIME_COMMAND = "node";
|
|
323
|
+
DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// src/config/index.ts
|
|
328
|
+
var init_config = __esm({
|
|
329
|
+
"src/config/index.ts"() {
|
|
330
|
+
"use strict";
|
|
331
|
+
init_resolve();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
222
334
|
|
|
223
335
|
// src/state.ts
|
|
224
|
-
|
|
225
|
-
|
|
336
|
+
import * as fs3 from "fs";
|
|
337
|
+
import * as path3 from "path";
|
|
338
|
+
import * as crypto from "crypto";
|
|
226
339
|
function getStateDir(repoRoot) {
|
|
227
340
|
const { config } = resolveConfig({}, repoRoot);
|
|
228
341
|
return config.stateDir;
|
|
@@ -289,43 +402,215 @@ function createInitialState(commsDir, repoRoot, packageVersion) {
|
|
|
289
402
|
instances: {}
|
|
290
403
|
};
|
|
291
404
|
}
|
|
405
|
+
function updateInstanceState(state, instanceId, instanceState) {
|
|
406
|
+
return {
|
|
407
|
+
...state,
|
|
408
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
409
|
+
instances: {
|
|
410
|
+
...state.instances,
|
|
411
|
+
[instanceId]: instanceState
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function ensureBackupDir(stateDir, instanceId) {
|
|
416
|
+
const backupDir = path3.join(stateDir, "backups", instanceId);
|
|
417
|
+
fs3.mkdirSync(backupDir, { recursive: true });
|
|
418
|
+
return backupDir;
|
|
419
|
+
}
|
|
420
|
+
function backupFile(filePath, backupDir) {
|
|
421
|
+
const basename2 = path3.basename(filePath);
|
|
422
|
+
const hash = fileHash(filePath);
|
|
423
|
+
const backupPath = path3.join(backupDir, `${basename2}.${hash}.bak`);
|
|
424
|
+
fs3.copyFileSync(filePath, backupPath);
|
|
425
|
+
return backupPath;
|
|
426
|
+
}
|
|
427
|
+
function fileHash(filePath) {
|
|
428
|
+
if (!fs3.existsSync(filePath)) return "";
|
|
429
|
+
const content = fs3.readFileSync(filePath);
|
|
430
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
431
|
+
}
|
|
432
|
+
var STATE_FILE, SCHEMA_VERSION;
|
|
433
|
+
var init_state = __esm({
|
|
434
|
+
"src/state.ts"() {
|
|
435
|
+
"use strict";
|
|
436
|
+
init_config();
|
|
437
|
+
STATE_FILE = "state.json";
|
|
438
|
+
SCHEMA_VERSION = 2;
|
|
439
|
+
}
|
|
440
|
+
});
|
|
292
441
|
|
|
293
|
-
// src/
|
|
294
|
-
import * as
|
|
295
|
-
import * as
|
|
296
|
-
import
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
442
|
+
// src/adapters/common.ts
|
|
443
|
+
import * as fs5 from "fs";
|
|
444
|
+
import * as os from "os";
|
|
445
|
+
import * as path5 from "path";
|
|
446
|
+
import { spawnSync } from "child_process";
|
|
447
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
448
|
+
function probeCommand(candidates) {
|
|
449
|
+
for (const candidate of candidates) {
|
|
450
|
+
const result = spawnSync(candidate, ["--version"], {
|
|
451
|
+
encoding: "utf-8",
|
|
452
|
+
shell: process.platform === "win32"
|
|
453
|
+
});
|
|
454
|
+
if (result.status === 0) {
|
|
455
|
+
const version2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || null;
|
|
456
|
+
return { command: candidate, version: version2 };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return { command: null, version: null };
|
|
460
|
+
}
|
|
461
|
+
function getHomeDir() {
|
|
462
|
+
return os.homedir();
|
|
463
|
+
}
|
|
464
|
+
function toForwardSlashPath(filePath) {
|
|
465
|
+
return path5.resolve(filePath).replace(/\\/g, "/");
|
|
466
|
+
}
|
|
467
|
+
function canWriteOrCreate(filePath) {
|
|
301
468
|
try {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return
|
|
469
|
+
if (fs5.existsSync(filePath)) {
|
|
470
|
+
fs5.accessSync(filePath, fs5.constants.W_OK);
|
|
471
|
+
return true;
|
|
305
472
|
}
|
|
473
|
+
const parent = path5.dirname(filePath);
|
|
474
|
+
fs5.mkdirSync(parent, { recursive: true });
|
|
475
|
+
fs5.accessSync(parent, fs5.constants.W_OK);
|
|
476
|
+
return true;
|
|
306
477
|
} catch {
|
|
478
|
+
return false;
|
|
307
479
|
}
|
|
308
|
-
return FALLBACK_VERSION;
|
|
309
480
|
}
|
|
310
|
-
|
|
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
|
+
}
|
|
485
|
+
function findLocalTapCommsSource(ctx) {
|
|
486
|
+
const candidates = [
|
|
487
|
+
path5.join(
|
|
488
|
+
ctx.repoRoot,
|
|
489
|
+
"packages",
|
|
490
|
+
"tap-plugin",
|
|
491
|
+
"channels",
|
|
492
|
+
"tap-comms.ts"
|
|
493
|
+
),
|
|
494
|
+
path5.join(
|
|
495
|
+
ctx.repoRoot,
|
|
496
|
+
"node_modules",
|
|
497
|
+
"@hua-labs",
|
|
498
|
+
"tap-plugin",
|
|
499
|
+
"channels",
|
|
500
|
+
"tap-comms.ts"
|
|
501
|
+
)
|
|
502
|
+
];
|
|
503
|
+
for (const candidate of candidates) {
|
|
504
|
+
if (fs5.existsSync(candidate)) return candidate;
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
function findBundledTapCommsSource(metaUrl = import.meta.url) {
|
|
509
|
+
const moduleDir = path5.dirname(fileURLToPath2(metaUrl));
|
|
510
|
+
const candidates = [
|
|
511
|
+
path5.join(moduleDir, "mcp-server.mjs"),
|
|
512
|
+
path5.join(moduleDir, "..", "mcp-server.mjs"),
|
|
513
|
+
path5.join(moduleDir, "..", "mcp-server.ts")
|
|
514
|
+
];
|
|
515
|
+
for (const candidate of candidates) {
|
|
516
|
+
if (fs5.existsSync(candidate)) return candidate;
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
function findTapCommsServerEntry(ctx, metaUrl = import.meta.url) {
|
|
521
|
+
return findBundledTapCommsSource(metaUrl) ?? findLocalTapCommsSource(ctx);
|
|
522
|
+
}
|
|
523
|
+
function findPreferredBunCommand() {
|
|
524
|
+
const home = getHomeDir();
|
|
525
|
+
const candidates = process.platform === "win32" ? [path5.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path5.join(home, ".bun", "bin", "bun"), "bun"];
|
|
526
|
+
for (const candidate of candidates) {
|
|
527
|
+
if (path5.isAbsolute(candidate) && !fs5.existsSync(candidate)) continue;
|
|
528
|
+
const result = spawnSync(candidate, ["--version"], {
|
|
529
|
+
encoding: "utf-8",
|
|
530
|
+
shell: process.platform === "win32"
|
|
531
|
+
});
|
|
532
|
+
if (result.status === 0) {
|
|
533
|
+
return path5.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
539
|
+
const sourcePath = findTapCommsServerEntry(ctx);
|
|
540
|
+
const bunCommand = findPreferredBunCommand();
|
|
541
|
+
const warnings = [];
|
|
542
|
+
const issues = [];
|
|
543
|
+
const env = {
|
|
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)
|
|
548
|
+
};
|
|
549
|
+
if (instanceId) {
|
|
550
|
+
env.TAP_AGENT_ID = instanceId;
|
|
551
|
+
}
|
|
552
|
+
if (!sourcePath) {
|
|
553
|
+
issues.push(
|
|
554
|
+
"tap-comms MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
|
|
555
|
+
);
|
|
556
|
+
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
557
|
+
}
|
|
558
|
+
const isBundled = sourcePath.endsWith(".mjs");
|
|
559
|
+
let command = bunCommand;
|
|
560
|
+
let args = [toForwardSlashPath(sourcePath)];
|
|
561
|
+
if (!command && isBundled) {
|
|
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
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (!command) {
|
|
585
|
+
issues.push(
|
|
586
|
+
"bun is required to run the repo-local tap-comms MCP server (.ts source). Install bun: https://bun.sh"
|
|
587
|
+
);
|
|
588
|
+
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
command,
|
|
592
|
+
args,
|
|
593
|
+
env,
|
|
594
|
+
sourcePath,
|
|
595
|
+
warnings,
|
|
596
|
+
issues
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
var init_common = __esm({
|
|
600
|
+
"src/adapters/common.ts"() {
|
|
601
|
+
"use strict";
|
|
602
|
+
}
|
|
603
|
+
});
|
|
311
604
|
|
|
312
|
-
// src/
|
|
605
|
+
// src/runtime/resolve-node.ts
|
|
313
606
|
import * as fs6 from "fs";
|
|
314
|
-
import * as net from "net";
|
|
315
607
|
import * as path6 from "path";
|
|
316
|
-
import { randomBytes } from "crypto";
|
|
317
|
-
import { spawn, spawnSync, execSync as execSync2 } from "child_process";
|
|
318
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
319
|
-
|
|
320
|
-
// src/runtime/resolve-node.ts
|
|
321
|
-
import * as fs5 from "fs";
|
|
322
|
-
import * as path5 from "path";
|
|
323
608
|
import { execSync } from "child_process";
|
|
324
609
|
function readNodeVersion(repoRoot) {
|
|
325
|
-
const nvFile =
|
|
326
|
-
if (!
|
|
610
|
+
const nvFile = path6.join(repoRoot, ".node-version");
|
|
611
|
+
if (!fs6.existsSync(nvFile)) return null;
|
|
327
612
|
try {
|
|
328
|
-
const raw =
|
|
613
|
+
const raw = fs6.readFileSync(nvFile, "utf-8").trim();
|
|
329
614
|
return raw.length > 0 ? raw.replace(/^v/, "") : null;
|
|
330
615
|
} catch {
|
|
331
616
|
return null;
|
|
@@ -335,16 +620,16 @@ function fnmCandidateDirs() {
|
|
|
335
620
|
if (process.platform === "win32") {
|
|
336
621
|
return [
|
|
337
622
|
process.env.FNM_DIR,
|
|
338
|
-
process.env.APPDATA ?
|
|
339
|
-
process.env.LOCALAPPDATA ?
|
|
340
|
-
process.env.USERPROFILE ?
|
|
623
|
+
process.env.APPDATA ? path6.join(process.env.APPDATA, "fnm") : null,
|
|
624
|
+
process.env.LOCALAPPDATA ? path6.join(process.env.LOCALAPPDATA, "fnm") : null,
|
|
625
|
+
process.env.USERPROFILE ? path6.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
|
|
341
626
|
].filter(Boolean);
|
|
342
627
|
}
|
|
343
628
|
return [
|
|
344
629
|
process.env.FNM_DIR,
|
|
345
|
-
process.env.HOME ?
|
|
346
|
-
process.env.HOME ?
|
|
347
|
-
process.env.XDG_DATA_HOME ?
|
|
630
|
+
process.env.HOME ? path6.join(process.env.HOME, ".local", "share", "fnm") : null,
|
|
631
|
+
process.env.HOME ? path6.join(process.env.HOME, ".fnm") : null,
|
|
632
|
+
process.env.XDG_DATA_HOME ? path6.join(process.env.XDG_DATA_HOME, "fnm") : null
|
|
348
633
|
].filter(Boolean);
|
|
349
634
|
}
|
|
350
635
|
function nodeExecutableName() {
|
|
@@ -354,14 +639,14 @@ function probeFnmNode(desiredVersion) {
|
|
|
354
639
|
const dirs = fnmCandidateDirs();
|
|
355
640
|
const exe = nodeExecutableName();
|
|
356
641
|
for (const baseDir of dirs) {
|
|
357
|
-
const candidate =
|
|
642
|
+
const candidate = path6.join(
|
|
358
643
|
baseDir,
|
|
359
644
|
"node-versions",
|
|
360
645
|
`v${desiredVersion}`,
|
|
361
646
|
"installation",
|
|
362
647
|
exe
|
|
363
648
|
);
|
|
364
|
-
if (!
|
|
649
|
+
if (!fs6.existsSync(candidate)) continue;
|
|
365
650
|
try {
|
|
366
651
|
const v = execSync(`"${candidate}" --version`, {
|
|
367
652
|
encoding: "utf-8",
|
|
@@ -402,12 +687,12 @@ function checkStripTypesSupport(command) {
|
|
|
402
687
|
}
|
|
403
688
|
function findTsxFallback(repoRoot) {
|
|
404
689
|
const candidates = [
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
690
|
+
path6.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
|
|
691
|
+
path6.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
|
|
692
|
+
path6.join(repoRoot, "node_modules", ".bin", "tsx")
|
|
408
693
|
];
|
|
409
694
|
for (const c of candidates) {
|
|
410
|
-
if (
|
|
695
|
+
if (fs6.existsSync(c)) return c;
|
|
411
696
|
}
|
|
412
697
|
return null;
|
|
413
698
|
}
|
|
@@ -416,7 +701,7 @@ function getFnmBinDir(repoRoot) {
|
|
|
416
701
|
if (!desiredVersion) return null;
|
|
417
702
|
const nodePath = probeFnmNode(desiredVersion);
|
|
418
703
|
if (!nodePath) return null;
|
|
419
|
-
return
|
|
704
|
+
return path6.dirname(nodePath);
|
|
420
705
|
}
|
|
421
706
|
function resolveNodeRuntime(configCommand, repoRoot) {
|
|
422
707
|
if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
|
|
@@ -472,96 +757,1084 @@ function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
|
|
|
472
757
|
const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
|
|
473
758
|
return {
|
|
474
759
|
...baseEnv,
|
|
475
|
-
[pathKey]: `${fnmBin}${
|
|
760
|
+
[pathKey]: `${fnmBin}${path6.delimiter}${currentPath}`
|
|
476
761
|
};
|
|
477
762
|
}
|
|
763
|
+
var init_resolve_node = __esm({
|
|
764
|
+
"src/runtime/resolve-node.ts"() {
|
|
765
|
+
"use strict";
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// src/runtime/index.ts
|
|
770
|
+
var init_runtime = __esm({
|
|
771
|
+
"src/runtime/index.ts"() {
|
|
772
|
+
"use strict";
|
|
773
|
+
init_resolve_node();
|
|
774
|
+
}
|
|
775
|
+
});
|
|
478
776
|
|
|
479
777
|
// src/engine/bridge.ts
|
|
480
|
-
|
|
778
|
+
import * as fs7 from "fs";
|
|
779
|
+
import * as net from "net";
|
|
780
|
+
import * as path7 from "path";
|
|
781
|
+
import { randomBytes } from "crypto";
|
|
782
|
+
import { spawn, spawnSync as spawnSync2, execSync as execSync2 } from "child_process";
|
|
783
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
784
|
+
function appServerLogFilePath(stateDir, instanceId) {
|
|
785
|
+
return path7.join(stateDir, "logs", `app-server-${instanceId}.log`);
|
|
786
|
+
}
|
|
787
|
+
function appServerGatewayLogFilePath(stateDir, instanceId) {
|
|
788
|
+
return path7.join(stateDir, "logs", `app-server-gateway-${instanceId}.log`);
|
|
789
|
+
}
|
|
790
|
+
function appServerGatewayTokenFilePath(stateDir, instanceId) {
|
|
791
|
+
return path7.join(
|
|
792
|
+
stateDir,
|
|
793
|
+
"secrets",
|
|
794
|
+
`app-server-gateway-${instanceId}.token`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
function stderrLogFilePath(logPath) {
|
|
798
|
+
return `${logPath}.stderr`;
|
|
799
|
+
}
|
|
481
800
|
function writeProtectedTextFile(filePath, content) {
|
|
482
|
-
|
|
801
|
+
fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
483
802
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
484
|
-
|
|
803
|
+
fs7.writeFileSync(tmp, content, {
|
|
485
804
|
encoding: "utf-8",
|
|
486
805
|
mode: APP_SERVER_AUTH_FILE_MODE
|
|
487
806
|
});
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
807
|
+
fs7.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
|
|
808
|
+
fs7.renameSync(tmp, filePath);
|
|
809
|
+
fs7.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
|
|
491
810
|
}
|
|
492
|
-
function
|
|
493
|
-
|
|
811
|
+
function removeFileIfExists(filePath) {
|
|
812
|
+
if (!filePath || !fs7.existsSync(filePath)) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
fs7.unlinkSync(filePath);
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
494
819
|
}
|
|
495
|
-
function
|
|
496
|
-
|
|
820
|
+
function getWebSocketCtor() {
|
|
821
|
+
const candidate = globalThis.WebSocket;
|
|
822
|
+
return typeof candidate === "function" ? candidate : null;
|
|
497
823
|
}
|
|
498
|
-
function
|
|
499
|
-
|
|
500
|
-
|
|
824
|
+
function delay(ms) {
|
|
825
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
826
|
+
}
|
|
827
|
+
function isLoopbackHost(hostname) {
|
|
828
|
+
return hostname === "127.0.0.1" || hostname === "localhost";
|
|
829
|
+
}
|
|
830
|
+
function resolveCodexCommand(platform) {
|
|
831
|
+
const candidates = platform === "win32" ? ["codex.cmd", "codex.exe", "codex", "codex.ps1"] : ["codex"];
|
|
832
|
+
return probeCommand(candidates).command;
|
|
833
|
+
}
|
|
834
|
+
function formatCodexAppServerCommand(command, url) {
|
|
835
|
+
return `${command} app-server --listen ${url}`;
|
|
836
|
+
}
|
|
837
|
+
function resolvePowerShellCommand() {
|
|
838
|
+
return probeCommand(["pwsh", "powershell", "powershell.exe"]).command ?? "powershell";
|
|
839
|
+
}
|
|
840
|
+
function resolveAuthGatewayScript(repoRoot) {
|
|
841
|
+
const moduleDir = path7.dirname(fileURLToPath3(import.meta.url));
|
|
842
|
+
const candidates = [
|
|
843
|
+
path7.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
|
|
844
|
+
path7.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.ts"),
|
|
845
|
+
path7.join(
|
|
846
|
+
repoRoot,
|
|
847
|
+
"packages",
|
|
848
|
+
"tap-comms",
|
|
849
|
+
"dist",
|
|
850
|
+
"bridges",
|
|
851
|
+
"codex-app-server-auth-gateway.mjs"
|
|
852
|
+
),
|
|
853
|
+
path7.join(
|
|
854
|
+
repoRoot,
|
|
855
|
+
"packages",
|
|
856
|
+
"tap-comms",
|
|
857
|
+
"src",
|
|
858
|
+
"bridges",
|
|
859
|
+
"codex-app-server-auth-gateway.ts"
|
|
860
|
+
)
|
|
861
|
+
];
|
|
862
|
+
for (const candidate of candidates) {
|
|
863
|
+
if (fs7.existsSync(candidate)) {
|
|
864
|
+
return candidate;
|
|
865
|
+
}
|
|
501
866
|
}
|
|
502
|
-
|
|
503
|
-
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
function getBridgeRuntimeStateDir(repoRoot, instanceId) {
|
|
870
|
+
return path7.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`);
|
|
871
|
+
}
|
|
872
|
+
async function allocateLoopbackPort(hostname) {
|
|
873
|
+
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
874
|
+
return await new Promise((resolve8, reject) => {
|
|
875
|
+
const server = net.createServer();
|
|
876
|
+
server.unref();
|
|
877
|
+
server.once("error", reject);
|
|
878
|
+
server.listen(0, bindHost, () => {
|
|
879
|
+
const address = server.address();
|
|
880
|
+
if (!address || typeof address === "string") {
|
|
881
|
+
server.close(() => {
|
|
882
|
+
reject(new Error("Failed to allocate a loopback port"));
|
|
883
|
+
});
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const port = address.port;
|
|
887
|
+
server.close((error) => {
|
|
888
|
+
if (error) {
|
|
889
|
+
reject(error);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
resolve8(port);
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
function buildProtectedAppServerUrl(publicUrl, token) {
|
|
898
|
+
const url = new URL(publicUrl);
|
|
899
|
+
url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
|
|
900
|
+
return url.toString().replace(/\/(?=\?|$)/, "");
|
|
901
|
+
}
|
|
902
|
+
function readGatewayTokenFromPath(tokenPath) {
|
|
903
|
+
return fs7.readFileSync(tokenPath, "utf8").trim();
|
|
904
|
+
}
|
|
905
|
+
function readGatewayToken(auth) {
|
|
906
|
+
if (!auth) {
|
|
504
907
|
return null;
|
|
505
908
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
909
|
+
const legacyToken = auth.token;
|
|
910
|
+
if (legacyToken?.trim()) {
|
|
911
|
+
return legacyToken.trim();
|
|
912
|
+
}
|
|
913
|
+
if (!auth.tokenPath || !fs7.existsSync(auth.tokenPath)) {
|
|
511
914
|
return null;
|
|
512
915
|
}
|
|
916
|
+
const fileToken = readGatewayTokenFromPath(auth.tokenPath);
|
|
917
|
+
return fileToken || null;
|
|
513
918
|
}
|
|
514
|
-
function
|
|
515
|
-
|
|
919
|
+
function materializeGatewayTokenFile(stateDir, instanceId, publicUrl, auth) {
|
|
920
|
+
if (auth.tokenPath && fs7.existsSync(auth.tokenPath)) {
|
|
921
|
+
return auth;
|
|
922
|
+
}
|
|
923
|
+
const token = readGatewayToken(auth);
|
|
924
|
+
if (!token) {
|
|
925
|
+
throw new Error(`Missing auth gateway token for ${instanceId}`);
|
|
926
|
+
}
|
|
927
|
+
const tokenPath = appServerGatewayTokenFilePath(stateDir, instanceId);
|
|
928
|
+
writeProtectedTextFile(tokenPath, `${token}
|
|
929
|
+
`);
|
|
930
|
+
return {
|
|
931
|
+
...auth,
|
|
932
|
+
protectedUrl: buildProtectedAppServerUrl(publicUrl, "***"),
|
|
933
|
+
tokenPath
|
|
934
|
+
};
|
|
516
935
|
}
|
|
517
|
-
function
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
936
|
+
async function createManagedAppServerAuth(options) {
|
|
937
|
+
const publicUrl = new URL(options.publicUrl);
|
|
938
|
+
const upstreamUrl = new URL(options.publicUrl);
|
|
939
|
+
upstreamUrl.port = String(await allocateLoopbackPort(publicUrl.hostname));
|
|
940
|
+
upstreamUrl.search = "";
|
|
941
|
+
upstreamUrl.hash = "";
|
|
942
|
+
const gatewayScript = resolveAuthGatewayScript(options.repoRoot);
|
|
943
|
+
if (!gatewayScript) {
|
|
944
|
+
throw new Error("Auth gateway script not found");
|
|
945
|
+
}
|
|
946
|
+
const token = randomBytes(24).toString("base64url");
|
|
947
|
+
const tokenPath = appServerGatewayTokenFilePath(
|
|
948
|
+
options.stateDir,
|
|
949
|
+
options.instanceId
|
|
950
|
+
);
|
|
951
|
+
writeProtectedTextFile(tokenPath, `${token}
|
|
952
|
+
`);
|
|
953
|
+
const protectedUrl = buildProtectedAppServerUrl(options.publicUrl, "***");
|
|
954
|
+
const gatewayLogPath = appServerGatewayLogFilePath(
|
|
955
|
+
options.stateDir,
|
|
956
|
+
options.instanceId
|
|
957
|
+
);
|
|
958
|
+
fs7.mkdirSync(path7.dirname(gatewayLogPath), { recursive: true });
|
|
959
|
+
rotateLog(gatewayLogPath);
|
|
960
|
+
const runtime = resolveNodeRuntime(process.execPath, options.repoRoot);
|
|
961
|
+
const gatewayArgs = [];
|
|
962
|
+
if (gatewayScript.endsWith(".ts")) {
|
|
963
|
+
if (!runtime.supportsStripTypes) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
"Current Node runtime cannot start the auth gateway from TypeScript source. Rebuild @hua-labs/tap or use Node 22.6+."
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
gatewayArgs.push("--experimental-strip-types");
|
|
969
|
+
}
|
|
970
|
+
gatewayArgs.push(gatewayScript);
|
|
971
|
+
const gatewayEnv = {
|
|
972
|
+
...buildRuntimeEnv(options.repoRoot),
|
|
973
|
+
TAP_GATEWAY_LISTEN_URL: options.publicUrl,
|
|
974
|
+
TAP_GATEWAY_UPSTREAM_URL: upstreamUrl.toString().replace(/\/$/, ""),
|
|
975
|
+
TAP_GATEWAY_TOKEN_FILE: tokenPath
|
|
976
|
+
};
|
|
977
|
+
let gatewayPid;
|
|
978
|
+
{
|
|
979
|
+
let logFd = null;
|
|
980
|
+
try {
|
|
981
|
+
if (options.platform === "win32") {
|
|
982
|
+
gatewayPid = startWindowsDetachedProcess(
|
|
983
|
+
runtime.command,
|
|
984
|
+
gatewayArgs,
|
|
985
|
+
options.repoRoot,
|
|
986
|
+
gatewayLogPath,
|
|
987
|
+
gatewayEnv
|
|
988
|
+
);
|
|
989
|
+
} else {
|
|
990
|
+
logFd = fs7.openSync(gatewayLogPath, "a");
|
|
991
|
+
const child = spawn(runtime.command, gatewayArgs, {
|
|
992
|
+
cwd: options.repoRoot,
|
|
993
|
+
detached: true,
|
|
994
|
+
stdio: ["ignore", logFd, logFd],
|
|
995
|
+
env: gatewayEnv,
|
|
996
|
+
windowsHide: true
|
|
997
|
+
});
|
|
998
|
+
child.unref();
|
|
999
|
+
gatewayPid = child.pid ?? null;
|
|
1000
|
+
}
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
removeFileIfExists(tokenPath);
|
|
1003
|
+
throw error;
|
|
1004
|
+
} finally {
|
|
1005
|
+
if (logFd != null) {
|
|
1006
|
+
fs7.closeSync(logFd);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
525
1009
|
}
|
|
1010
|
+
if (gatewayPid == null) {
|
|
1011
|
+
removeFileIfExists(tokenPath);
|
|
1012
|
+
throw new Error("Failed to spawn app-server auth gateway");
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
mode: "query-token",
|
|
1016
|
+
protectedUrl,
|
|
1017
|
+
upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
|
|
1018
|
+
tokenPath,
|
|
1019
|
+
gatewayPid,
|
|
1020
|
+
gatewayLogPath
|
|
1021
|
+
};
|
|
526
1022
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (serializable.appServer?.auth) {
|
|
531
|
-
delete serializable.appServer.auth.token;
|
|
1023
|
+
function canReuseManagedAppServer(appServer) {
|
|
1024
|
+
if (!appServer?.managed) {
|
|
1025
|
+
return false;
|
|
532
1026
|
}
|
|
533
|
-
|
|
1027
|
+
if (appServer.pid != null && !isProcessAlive(appServer.pid)) {
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
const auth = appServer.auth;
|
|
1031
|
+
if (auth) {
|
|
1032
|
+
if (!auth.protectedUrl) {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
if (!readGatewayToken(auth)) {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
if (auth.gatewayPid != null && !isProcessAlive(auth.gatewayPid)) {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return true;
|
|
534
1043
|
}
|
|
535
|
-
function
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
1044
|
+
function markAppServerHealthy(appServer) {
|
|
1045
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1046
|
+
return {
|
|
1047
|
+
...appServer,
|
|
1048
|
+
healthy: true,
|
|
1049
|
+
lastCheckedAt: checkedAt,
|
|
1050
|
+
lastHealthyAt: checkedAt
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function findReusableManagedAppServer(stateDir, publicUrl) {
|
|
1054
|
+
const pidDir = path7.join(stateDir, "pids");
|
|
1055
|
+
if (!fs7.existsSync(pidDir)) {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
for (const name of fs7.readdirSync(pidDir)) {
|
|
1059
|
+
if (!name.startsWith("bridge-") || !name.endsWith(".json")) {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
try {
|
|
1063
|
+
const raw = fs7.readFileSync(path7.join(pidDir, name), "utf-8");
|
|
1064
|
+
const parsed = JSON.parse(raw);
|
|
1065
|
+
if (parsed.appServer?.url !== publicUrl) {
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if (canReuseManagedAppServer(parsed.appServer)) {
|
|
1069
|
+
return markAppServerHealthy(parsed.appServer);
|
|
1070
|
+
}
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
539
1073
|
}
|
|
1074
|
+
return null;
|
|
540
1075
|
}
|
|
541
|
-
function
|
|
1076
|
+
function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
|
|
1077
|
+
const ext = path7.extname(command).toLowerCase();
|
|
1078
|
+
const stderrLogPath = stderrLogFilePath(logPath);
|
|
1079
|
+
const stdoutFd = fs7.openSync(logPath, "a");
|
|
1080
|
+
const stderrFd = fs7.openSync(stderrLogPath, "a");
|
|
542
1081
|
try {
|
|
543
|
-
|
|
544
|
-
|
|
1082
|
+
const child = ext === ".ps1" ? spawn(
|
|
1083
|
+
resolvePowerShellCommand(),
|
|
1084
|
+
["-NoLogo", "-NoProfile", "-File", command, ...args],
|
|
1085
|
+
{
|
|
1086
|
+
cwd: repoRoot,
|
|
1087
|
+
detached: true,
|
|
1088
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
1089
|
+
env,
|
|
1090
|
+
windowsHide: true
|
|
1091
|
+
}
|
|
1092
|
+
) : spawn(command, args, {
|
|
1093
|
+
cwd: repoRoot,
|
|
1094
|
+
detached: true,
|
|
1095
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
1096
|
+
env,
|
|
1097
|
+
windowsHide: true,
|
|
1098
|
+
shell: ext === ".cmd" || ext === ".bat"
|
|
1099
|
+
});
|
|
1100
|
+
child.unref();
|
|
1101
|
+
return child.pid ?? null;
|
|
1102
|
+
} finally {
|
|
1103
|
+
fs7.closeSync(stdoutFd);
|
|
1104
|
+
fs7.closeSync(stderrFd);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
|
|
1108
|
+
return startWindowsDetachedProcess(
|
|
1109
|
+
command,
|
|
1110
|
+
["app-server", "--listen", url],
|
|
1111
|
+
repoRoot,
|
|
1112
|
+
logPath
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
function findListeningProcessId(url, platform) {
|
|
1116
|
+
if (platform !== "win32") {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
let port;
|
|
1120
|
+
try {
|
|
1121
|
+
const parsed = new URL(url);
|
|
1122
|
+
port = parsed.port ? Number.parseInt(parsed.port, 10) : null;
|
|
545
1123
|
} catch {
|
|
546
|
-
return
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
if (port == null || !Number.isFinite(port)) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
const result = spawnSync2(
|
|
1130
|
+
resolvePowerShellCommand(),
|
|
1131
|
+
[
|
|
1132
|
+
"-NoLogo",
|
|
1133
|
+
"-NoProfile",
|
|
1134
|
+
"-Command",
|
|
1135
|
+
[
|
|
1136
|
+
`$port = ${port}`,
|
|
1137
|
+
"$processId = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess",
|
|
1138
|
+
"if ($processId) { $processId }"
|
|
1139
|
+
].join("; ")
|
|
1140
|
+
],
|
|
1141
|
+
{
|
|
1142
|
+
encoding: "utf-8",
|
|
1143
|
+
windowsHide: true
|
|
1144
|
+
}
|
|
1145
|
+
);
|
|
1146
|
+
if (result.status !== 0) {
|
|
1147
|
+
return null;
|
|
547
1148
|
}
|
|
1149
|
+
const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
|
|
1150
|
+
return Number.isFinite(parsedPid) ? parsedPid : null;
|
|
548
1151
|
}
|
|
549
|
-
function
|
|
550
|
-
|
|
1152
|
+
function resolveAppServerUrl(baseUrl, port) {
|
|
1153
|
+
const resolvedBase = (baseUrl ?? DEFAULT_APP_SERVER_URL2).replace(/\/$/, "");
|
|
1154
|
+
if (port == null) {
|
|
1155
|
+
return resolvedBase;
|
|
1156
|
+
}
|
|
551
1157
|
try {
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
fs6.renameSync(logPath, prevPath);
|
|
1158
|
+
const parsed = new URL(resolvedBase);
|
|
1159
|
+
parsed.port = String(port);
|
|
1160
|
+
return parsed.toString().replace(/\/$/, "");
|
|
556
1161
|
} catch {
|
|
1162
|
+
return resolvedBase;
|
|
557
1163
|
}
|
|
558
1164
|
}
|
|
559
|
-
function
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
1165
|
+
async function isTcpPortAvailable(hostname, port) {
|
|
1166
|
+
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
1167
|
+
return await new Promise((resolve8) => {
|
|
1168
|
+
const server = net.createServer();
|
|
1169
|
+
server.unref();
|
|
1170
|
+
server.once("error", () => resolve8(false));
|
|
1171
|
+
server.listen(port, bindHost, () => {
|
|
1172
|
+
server.close((error) => resolve8(!error));
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, excludeInstanceId) {
|
|
1177
|
+
let hostname = "127.0.0.1";
|
|
1178
|
+
try {
|
|
1179
|
+
hostname = new URL(baseUrl ?? DEFAULT_APP_SERVER_URL2).hostname;
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
const maxAttempts = 1e3;
|
|
1183
|
+
let port = basePort;
|
|
1184
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1, port += 1) {
|
|
1185
|
+
const claimedInState = Object.entries(state.instances).some(
|
|
1186
|
+
([id, inst]) => id !== excludeInstanceId && inst.port === port
|
|
1187
|
+
);
|
|
1188
|
+
if (claimedInState) {
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
if (!isLoopbackHost(hostname)) {
|
|
1192
|
+
return port;
|
|
1193
|
+
}
|
|
1194
|
+
if (await isTcpPortAvailable(hostname, port)) {
|
|
1195
|
+
return port;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
`Failed to find a free app-server port starting at ${basePort}`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
1203
|
+
const WebSocket = getWebSocketCtor();
|
|
1204
|
+
if (!WebSocket) {
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
return new Promise((resolve8) => {
|
|
1208
|
+
let settled = false;
|
|
1209
|
+
let socket = null;
|
|
1210
|
+
const finish = (healthy) => {
|
|
1211
|
+
if (settled) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
settled = true;
|
|
1215
|
+
clearTimeout(timer);
|
|
1216
|
+
try {
|
|
1217
|
+
socket?.close();
|
|
1218
|
+
} catch {
|
|
1219
|
+
}
|
|
1220
|
+
resolve8(healthy);
|
|
1221
|
+
};
|
|
1222
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
1223
|
+
try {
|
|
1224
|
+
socket = new WebSocket(url);
|
|
1225
|
+
socket.addEventListener("open", () => finish(true), { once: true });
|
|
1226
|
+
socket.addEventListener("error", () => finish(false), { once: true });
|
|
1227
|
+
socket.addEventListener("close", () => finish(false), { once: true });
|
|
1228
|
+
} catch {
|
|
1229
|
+
finish(false);
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
async function waitForAppServerHealth(url, timeoutMs) {
|
|
1234
|
+
const deadline = Date.now() + timeoutMs;
|
|
1235
|
+
while (Date.now() < deadline) {
|
|
1236
|
+
if (await checkAppServerHealth(url)) {
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
await delay(APP_SERVER_HEALTH_RETRY_MS);
|
|
1240
|
+
}
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
async function terminateProcess(pid, platform) {
|
|
1244
|
+
if (!isProcessAlive(pid)) {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
try {
|
|
1248
|
+
if (platform === "win32") {
|
|
1249
|
+
execSync2(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
|
|
1250
|
+
} else {
|
|
1251
|
+
process.kill(pid, "SIGTERM");
|
|
1252
|
+
await delay(2e3);
|
|
1253
|
+
if (isProcessAlive(pid)) {
|
|
1254
|
+
process.kill(pid, "SIGKILL");
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
return !isProcessAlive(pid);
|
|
1260
|
+
}
|
|
1261
|
+
async function stopManagedAppServer(appServer, platform) {
|
|
1262
|
+
if (!appServer.managed) {
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
let stopped = false;
|
|
1266
|
+
if (appServer.auth?.gatewayPid != null) {
|
|
1267
|
+
stopped = await terminateProcess(appServer.auth.gatewayPid, platform) || stopped;
|
|
1268
|
+
}
|
|
1269
|
+
if (appServer.pid != null) {
|
|
1270
|
+
stopped = await terminateProcess(appServer.pid, platform) || stopped;
|
|
1271
|
+
}
|
|
1272
|
+
removeFileIfExists(appServer.auth?.tokenPath);
|
|
1273
|
+
return stopped;
|
|
1274
|
+
}
|
|
1275
|
+
async function ensureCodexAppServer(options) {
|
|
1276
|
+
const effectiveUrl = resolveAppServerUrl(options.appServerUrl);
|
|
1277
|
+
const fallbackManualCommand = formatCodexAppServerCommand(
|
|
1278
|
+
"codex",
|
|
1279
|
+
effectiveUrl
|
|
1280
|
+
);
|
|
1281
|
+
if (options.existingAppServer?.url === effectiveUrl && canReuseManagedAppServer(options.existingAppServer)) {
|
|
1282
|
+
return markAppServerHealthy(options.existingAppServer);
|
|
1283
|
+
}
|
|
1284
|
+
const sharedManaged = findReusableManagedAppServer(
|
|
1285
|
+
options.stateDir,
|
|
1286
|
+
effectiveUrl
|
|
1287
|
+
);
|
|
1288
|
+
if (sharedManaged) {
|
|
1289
|
+
return sharedManaged;
|
|
1290
|
+
}
|
|
1291
|
+
let parsedUrl;
|
|
1292
|
+
try {
|
|
1293
|
+
parsedUrl = new URL(effectiveUrl);
|
|
1294
|
+
} catch {
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`Invalid app-server URL: ${effectiveUrl}
|
|
1297
|
+
Start it manually:
|
|
1298
|
+
${fallbackManualCommand}`
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
if (!isLoopbackHost(parsedUrl.hostname)) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`Auto-start only supports loopback app-server URLs. Current URL: ${effectiveUrl}
|
|
1304
|
+
Start it manually:
|
|
1305
|
+
${fallbackManualCommand}`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
if (await checkAppServerHealth(effectiveUrl)) {
|
|
1309
|
+
const hint = options.noAuth ? "Stop it first or use --no-server for an unmanaged external app-server." : "A listener is already running, so tap cannot insert the auth gateway there.\nStop it first or use --no-server for an unmanaged external app-server.";
|
|
1310
|
+
throw new Error(`${effectiveUrl}: ${hint}`);
|
|
1311
|
+
}
|
|
1312
|
+
const resolvedCommand = resolveCodexCommand(options.platform);
|
|
1313
|
+
if (!resolvedCommand) {
|
|
1314
|
+
throw new Error(
|
|
1315
|
+
`Codex CLI not found in PATH.
|
|
1316
|
+
Start the app-server manually:
|
|
1317
|
+
${fallbackManualCommand}`
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
const logPath = appServerLogFilePath(options.stateDir, options.instanceId);
|
|
1321
|
+
fs7.mkdirSync(path7.dirname(logPath), { recursive: true });
|
|
1322
|
+
rotateLog(logPath);
|
|
1323
|
+
if (options.noAuth) {
|
|
1324
|
+
const manualCommand2 = formatCodexAppServerCommand("codex", effectiveUrl);
|
|
1325
|
+
let pid2;
|
|
1326
|
+
if (options.platform === "win32") {
|
|
1327
|
+
try {
|
|
1328
|
+
pid2 = startWindowsCodexAppServer(
|
|
1329
|
+
resolvedCommand,
|
|
1330
|
+
effectiveUrl,
|
|
1331
|
+
options.repoRoot,
|
|
1332
|
+
logPath
|
|
1333
|
+
);
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
throw new Error(
|
|
1336
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
1337
|
+
Start it manually:
|
|
1338
|
+
${manualCommand2}`,
|
|
1339
|
+
{ cause: err }
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
} else {
|
|
1343
|
+
const logFd = fs7.openSync(logPath, "a");
|
|
1344
|
+
try {
|
|
1345
|
+
const child = spawn(
|
|
1346
|
+
resolvedCommand,
|
|
1347
|
+
["app-server", "--listen", effectiveUrl],
|
|
1348
|
+
{
|
|
1349
|
+
cwd: options.repoRoot,
|
|
1350
|
+
detached: true,
|
|
1351
|
+
stdio: ["ignore", logFd, logFd],
|
|
1352
|
+
env: process.env,
|
|
1353
|
+
windowsHide: true
|
|
1354
|
+
}
|
|
1355
|
+
);
|
|
1356
|
+
child.unref();
|
|
1357
|
+
pid2 = child.pid ?? null;
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
throw new Error(
|
|
1360
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
1361
|
+
Start it manually:
|
|
1362
|
+
${manualCommand2}`,
|
|
1363
|
+
{ cause: err }
|
|
1364
|
+
);
|
|
1365
|
+
} finally {
|
|
1366
|
+
fs7.closeSync(logFd);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (pid2 == null) {
|
|
1370
|
+
throw new Error(
|
|
1371
|
+
`Failed to spawn Codex app-server.
|
|
1372
|
+
Start it manually:
|
|
1373
|
+
${manualCommand2}`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
const healthy2 = await waitForAppServerHealth(
|
|
1377
|
+
effectiveUrl,
|
|
1378
|
+
APP_SERVER_START_TIMEOUT_MS
|
|
1379
|
+
);
|
|
1380
|
+
if (!healthy2) {
|
|
1381
|
+
await terminateProcess(pid2, options.platform);
|
|
1382
|
+
throw new Error(
|
|
1383
|
+
`Codex app-server did not become healthy at ${effectiveUrl}.
|
|
1384
|
+
Check ${logPath}
|
|
1385
|
+
Or start it manually:
|
|
1386
|
+
${manualCommand2}`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
pid2 = findListeningProcessId(effectiveUrl, options.platform) ?? pid2;
|
|
1390
|
+
const healthyAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1391
|
+
return {
|
|
1392
|
+
url: effectiveUrl,
|
|
1393
|
+
pid: pid2,
|
|
1394
|
+
managed: true,
|
|
1395
|
+
healthy: true,
|
|
1396
|
+
lastCheckedAt: healthyAt2,
|
|
1397
|
+
lastHealthyAt: healthyAt2,
|
|
1398
|
+
logPath,
|
|
1399
|
+
manualCommand: manualCommand2,
|
|
1400
|
+
auth: null
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
const auth = await createManagedAppServerAuth({
|
|
1404
|
+
instanceId: options.instanceId,
|
|
1405
|
+
stateDir: options.stateDir,
|
|
1406
|
+
repoRoot: options.repoRoot,
|
|
1407
|
+
platform: options.platform,
|
|
1408
|
+
publicUrl: effectiveUrl
|
|
1409
|
+
});
|
|
1410
|
+
const manualCommand = formatCodexAppServerCommand("codex", auth.upstreamUrl);
|
|
1411
|
+
let pid;
|
|
1412
|
+
if (options.platform === "win32") {
|
|
1413
|
+
try {
|
|
1414
|
+
pid = startWindowsCodexAppServer(
|
|
1415
|
+
resolvedCommand,
|
|
1416
|
+
auth.upstreamUrl,
|
|
1417
|
+
options.repoRoot,
|
|
1418
|
+
logPath
|
|
1419
|
+
);
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
if (auth.gatewayPid != null) {
|
|
1422
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
1423
|
+
}
|
|
1424
|
+
removeFileIfExists(auth.tokenPath);
|
|
1425
|
+
throw new Error(
|
|
1426
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
1427
|
+
Start it manually:
|
|
1428
|
+
${manualCommand}`,
|
|
1429
|
+
{ cause: err }
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
} else {
|
|
1433
|
+
const logFd = fs7.openSync(logPath, "a");
|
|
1434
|
+
try {
|
|
1435
|
+
const child = spawn(
|
|
1436
|
+
resolvedCommand,
|
|
1437
|
+
["app-server", "--listen", auth.upstreamUrl],
|
|
1438
|
+
{
|
|
1439
|
+
cwd: options.repoRoot,
|
|
1440
|
+
detached: true,
|
|
1441
|
+
stdio: ["ignore", logFd, logFd],
|
|
1442
|
+
env: process.env,
|
|
1443
|
+
windowsHide: true
|
|
1444
|
+
}
|
|
1445
|
+
);
|
|
1446
|
+
child.unref();
|
|
1447
|
+
pid = child.pid ?? null;
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
if (auth.gatewayPid != null) {
|
|
1450
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
1451
|
+
}
|
|
1452
|
+
removeFileIfExists(auth.tokenPath);
|
|
1453
|
+
throw new Error(
|
|
1454
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
1455
|
+
Start it manually:
|
|
1456
|
+
${manualCommand}`,
|
|
1457
|
+
{ cause: err }
|
|
1458
|
+
);
|
|
1459
|
+
} finally {
|
|
1460
|
+
fs7.closeSync(logFd);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
if (pid == null) {
|
|
1464
|
+
if (auth.gatewayPid != null) {
|
|
1465
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
1466
|
+
}
|
|
1467
|
+
removeFileIfExists(auth.tokenPath);
|
|
1468
|
+
throw new Error(
|
|
1469
|
+
`Failed to spawn Codex app-server.
|
|
1470
|
+
Start it manually:
|
|
1471
|
+
${manualCommand}`
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
const healthy = await waitForAppServerHealth(
|
|
1475
|
+
auth.upstreamUrl,
|
|
1476
|
+
APP_SERVER_START_TIMEOUT_MS
|
|
1477
|
+
);
|
|
1478
|
+
if (!healthy) {
|
|
1479
|
+
await terminateProcess(pid, options.platform);
|
|
1480
|
+
if (auth.gatewayPid != null) {
|
|
1481
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
1482
|
+
}
|
|
1483
|
+
removeFileIfExists(auth.tokenPath);
|
|
1484
|
+
throw new Error(
|
|
1485
|
+
`Codex app-server did not become healthy at ${auth.upstreamUrl}.
|
|
1486
|
+
Check ${logPath}
|
|
1487
|
+
Or start it manually:
|
|
1488
|
+
${manualCommand}`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
const gatewayToken = readGatewayToken(auth);
|
|
1492
|
+
if (!gatewayToken) {
|
|
1493
|
+
await terminateProcess(pid, options.platform);
|
|
1494
|
+
if (auth.gatewayPid != null) {
|
|
1495
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
1496
|
+
}
|
|
1497
|
+
removeFileIfExists(auth.tokenPath);
|
|
1498
|
+
throw new Error("Tap auth gateway token is missing after startup.");
|
|
1499
|
+
}
|
|
1500
|
+
const gatewayHealthy = await waitForAppServerHealth(
|
|
1501
|
+
buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
|
|
1502
|
+
APP_SERVER_GATEWAY_START_TIMEOUT_MS
|
|
1503
|
+
);
|
|
1504
|
+
if (!gatewayHealthy) {
|
|
1505
|
+
await terminateProcess(pid, options.platform);
|
|
1506
|
+
if (auth.gatewayPid != null) {
|
|
1507
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
1508
|
+
}
|
|
1509
|
+
removeFileIfExists(auth.tokenPath);
|
|
1510
|
+
throw new Error(
|
|
1511
|
+
`Tap auth gateway did not become healthy at ${effectiveUrl}.
|
|
1512
|
+
Check ${auth.gatewayLogPath ?? "the gateway log"} and ${logPath}.`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
const healthyAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1516
|
+
pid = findListeningProcessId(auth.upstreamUrl, options.platform) ?? pid;
|
|
1517
|
+
return {
|
|
1518
|
+
url: effectiveUrl,
|
|
1519
|
+
pid,
|
|
1520
|
+
managed: true,
|
|
1521
|
+
healthy: true,
|
|
1522
|
+
lastCheckedAt: healthyAt,
|
|
1523
|
+
lastHealthyAt: healthyAt,
|
|
1524
|
+
logPath,
|
|
1525
|
+
manualCommand,
|
|
1526
|
+
auth
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function pidFilePath(stateDir, instanceId) {
|
|
1530
|
+
return path7.join(stateDir, "pids", `bridge-${instanceId}.json`);
|
|
1531
|
+
}
|
|
1532
|
+
function logFilePath(stateDir, instanceId) {
|
|
1533
|
+
return path7.join(stateDir, "logs", `bridge-${instanceId}.log`);
|
|
1534
|
+
}
|
|
1535
|
+
function runtimeHeartbeatFilePath(runtimeStateDir) {
|
|
1536
|
+
return path7.join(runtimeStateDir, "heartbeat.json");
|
|
1537
|
+
}
|
|
1538
|
+
function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
|
|
1539
|
+
if (!runtimeStateDir) {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
|
|
1543
|
+
if (!fs7.existsSync(heartbeatPath)) {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
try {
|
|
1547
|
+
const raw = fs7.readFileSync(heartbeatPath, "utf-8");
|
|
1548
|
+
const parsed = JSON.parse(raw);
|
|
1549
|
+
return typeof parsed.updatedAt === "string" ? parsed.updatedAt : null;
|
|
1550
|
+
} catch {
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
function resolveHeartbeatTimestamp(state) {
|
|
1555
|
+
return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
|
|
1556
|
+
}
|
|
1557
|
+
function loadBridgeState(stateDir, instanceId) {
|
|
1558
|
+
const pidPath = pidFilePath(stateDir, instanceId);
|
|
1559
|
+
if (!fs7.existsSync(pidPath)) return null;
|
|
1560
|
+
try {
|
|
1561
|
+
const raw = fs7.readFileSync(pidPath, "utf-8");
|
|
1562
|
+
return JSON.parse(raw);
|
|
1563
|
+
} catch {
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function saveBridgeState(stateDir, instanceId, state) {
|
|
1568
|
+
const pidPath = pidFilePath(stateDir, instanceId);
|
|
1569
|
+
const serializable = JSON.parse(JSON.stringify(state));
|
|
1570
|
+
if (serializable.appServer?.auth) {
|
|
1571
|
+
delete serializable.appServer.auth.token;
|
|
1572
|
+
}
|
|
1573
|
+
writeProtectedTextFile(pidPath, JSON.stringify(serializable, null, 2));
|
|
1574
|
+
}
|
|
1575
|
+
function clearBridgeState(stateDir, instanceId) {
|
|
1576
|
+
const pidPath = pidFilePath(stateDir, instanceId);
|
|
1577
|
+
if (fs7.existsSync(pidPath)) {
|
|
1578
|
+
fs7.unlinkSync(pidPath);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
function isProcessAlive(pid) {
|
|
1582
|
+
try {
|
|
1583
|
+
process.kill(pid, 0);
|
|
1584
|
+
return true;
|
|
1585
|
+
} catch {
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function isBridgeRunning(stateDir, instanceId) {
|
|
1590
|
+
const state = loadBridgeState(stateDir, instanceId);
|
|
1591
|
+
if (!state) return false;
|
|
1592
|
+
return isProcessAlive(state.pid);
|
|
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
|
+
}
|
|
1628
|
+
async function startBridge(options) {
|
|
1629
|
+
const {
|
|
1630
|
+
instanceId,
|
|
1631
|
+
runtime,
|
|
1632
|
+
stateDir,
|
|
1633
|
+
commsDir,
|
|
1634
|
+
bridgeScript,
|
|
1635
|
+
agentName,
|
|
1636
|
+
port
|
|
1637
|
+
} = options;
|
|
1638
|
+
const resolvedAgent = resolveAgentName(instanceId, agentName, {
|
|
1639
|
+
repoRoot: options.repoRoot,
|
|
1640
|
+
stateDir
|
|
1641
|
+
});
|
|
1642
|
+
if (!resolvedAgent) {
|
|
1643
|
+
throw new Error(
|
|
1644
|
+
`No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
if (isBridgeRunning(stateDir, instanceId)) {
|
|
1648
|
+
const existing = loadBridgeState(stateDir, instanceId);
|
|
1649
|
+
throw new Error(
|
|
1650
|
+
`Bridge for ${instanceId} is already running (PID: ${existing.pid})`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
const previousBridgeState = loadBridgeState(stateDir, instanceId);
|
|
1654
|
+
const previousAppServer = previousBridgeState?.appServer ?? null;
|
|
1655
|
+
clearBridgeState(stateDir, instanceId);
|
|
1656
|
+
const logPath = logFilePath(stateDir, instanceId);
|
|
1657
|
+
fs7.mkdirSync(path7.dirname(logPath), { recursive: true });
|
|
1658
|
+
rotateLog(logPath);
|
|
1659
|
+
let logFd = null;
|
|
1660
|
+
const repoRoot = options.repoRoot ?? path7.resolve(stateDir, "..");
|
|
1661
|
+
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
1662
|
+
const resolved = resolveNodeRuntime(
|
|
1663
|
+
options.runtimeCommand ?? "node",
|
|
1664
|
+
repoRoot
|
|
1665
|
+
);
|
|
1666
|
+
const command = resolved.command;
|
|
1667
|
+
const runtimeEnv = buildRuntimeEnv(repoRoot);
|
|
1668
|
+
const effectiveAppServerUrl = resolveAppServerUrl(options.appServerUrl, port);
|
|
1669
|
+
let appServer = null;
|
|
1670
|
+
let bridgeAppServerUrl = effectiveAppServerUrl;
|
|
1671
|
+
if (runtime === "codex" && options.manageAppServer) {
|
|
1672
|
+
appServer = await ensureCodexAppServer({
|
|
1673
|
+
instanceId,
|
|
1674
|
+
stateDir,
|
|
1675
|
+
repoRoot,
|
|
1676
|
+
platform: options.platform,
|
|
1677
|
+
appServerUrl: effectiveAppServerUrl,
|
|
1678
|
+
existingAppServer: previousAppServer,
|
|
1679
|
+
noAuth: options.noAuth
|
|
1680
|
+
});
|
|
1681
|
+
if (appServer.auth) {
|
|
1682
|
+
appServer = {
|
|
1683
|
+
...appServer,
|
|
1684
|
+
auth: materializeGatewayTokenFile(
|
|
1685
|
+
stateDir,
|
|
1686
|
+
instanceId,
|
|
1687
|
+
effectiveAppServerUrl,
|
|
1688
|
+
appServer.auth
|
|
1689
|
+
)
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
bridgeAppServerUrl = effectiveAppServerUrl;
|
|
1693
|
+
}
|
|
1694
|
+
try {
|
|
1695
|
+
const bridgeEnv = {
|
|
1696
|
+
...runtimeEnv,
|
|
1697
|
+
TAP_COMMS_DIR: commsDir,
|
|
1698
|
+
TAP_STATE_DIR: runtimeStateDir,
|
|
1699
|
+
TAP_BRIDGE_RUNTIME: runtime,
|
|
1700
|
+
TAP_BRIDGE_INSTANCE_ID: instanceId,
|
|
1701
|
+
TAP_AGENT_ID: instanceId,
|
|
1702
|
+
TAP_AGENT_NAME: resolvedAgent,
|
|
1703
|
+
CODEX_TAP_AGENT_NAME: resolvedAgent,
|
|
1704
|
+
TAP_RESOLVED_NODE: resolved.command,
|
|
1705
|
+
TAP_STRIP_TYPES: resolved.supportsStripTypes ? "1" : "0",
|
|
1706
|
+
...bridgeAppServerUrl ? { CODEX_APP_SERVER_URL: bridgeAppServerUrl } : {},
|
|
1707
|
+
...appServer?.auth?.tokenPath ? { TAP_GATEWAY_TOKEN_FILE: appServer.auth.tokenPath } : {},
|
|
1708
|
+
...port != null ? { TAP_BRIDGE_PORT: String(port) } : {},
|
|
1709
|
+
...options.headless?.enabled ? {
|
|
1710
|
+
TAP_HEADLESS: "true",
|
|
1711
|
+
TAP_AGENT_ROLE: options.headless.role,
|
|
1712
|
+
TAP_MAX_REVIEW_ROUNDS: String(options.headless.maxRounds),
|
|
1713
|
+
TAP_QUALITY_FLOOR: options.headless.qualitySeverityFloor
|
|
1714
|
+
} : {},
|
|
1715
|
+
...options.busyMode ? { TAP_BUSY_MODE: options.busyMode } : {},
|
|
1716
|
+
...options.pollSeconds != null ? { TAP_POLL_SECONDS: String(options.pollSeconds) } : {},
|
|
1717
|
+
...options.reconnectSeconds != null ? { TAP_RECONNECT_SECONDS: String(options.reconnectSeconds) } : {},
|
|
1718
|
+
...options.messageLookbackMinutes != null ? {
|
|
1719
|
+
TAP_MESSAGE_LOOKBACK_MINUTES: String(
|
|
1720
|
+
options.messageLookbackMinutes
|
|
1721
|
+
)
|
|
1722
|
+
} : {},
|
|
1723
|
+
...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
|
|
1724
|
+
...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
|
|
1725
|
+
...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
|
|
1726
|
+
};
|
|
1727
|
+
let bridgePid = null;
|
|
1728
|
+
if (options.platform === "win32") {
|
|
1729
|
+
bridgePid = startWindowsDetachedProcess(
|
|
1730
|
+
command,
|
|
1731
|
+
[bridgeScript],
|
|
1732
|
+
repoRoot,
|
|
1733
|
+
logPath,
|
|
1734
|
+
bridgeEnv
|
|
1735
|
+
);
|
|
1736
|
+
} else {
|
|
1737
|
+
logFd = fs7.openSync(logPath, "a");
|
|
1738
|
+
const child = spawn(command, [bridgeScript], {
|
|
1739
|
+
detached: true,
|
|
1740
|
+
stdio: ["ignore", logFd, logFd],
|
|
1741
|
+
env: bridgeEnv,
|
|
1742
|
+
windowsHide: true
|
|
1743
|
+
});
|
|
1744
|
+
child.unref();
|
|
1745
|
+
bridgePid = child.pid ?? null;
|
|
1746
|
+
}
|
|
1747
|
+
if (logFd != null) {
|
|
1748
|
+
fs7.closeSync(logFd);
|
|
1749
|
+
logFd = null;
|
|
1750
|
+
}
|
|
1751
|
+
if (!bridgePid) {
|
|
1752
|
+
throw new Error(`Failed to spawn bridge process for ${instanceId}`);
|
|
1753
|
+
}
|
|
1754
|
+
const state = {
|
|
1755
|
+
pid: bridgePid,
|
|
1756
|
+
statePath: pidFilePath(stateDir, instanceId),
|
|
1757
|
+
lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1758
|
+
appServer,
|
|
1759
|
+
runtimeStateDir
|
|
1760
|
+
};
|
|
1761
|
+
saveBridgeState(stateDir, instanceId, state);
|
|
1762
|
+
return state;
|
|
1763
|
+
} catch (err) {
|
|
1764
|
+
if (logFd != null) {
|
|
1765
|
+
try {
|
|
1766
|
+
fs7.closeSync(logFd);
|
|
1767
|
+
} catch {
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (appServer?.managed) {
|
|
1771
|
+
await stopManagedAppServer(appServer, options.platform);
|
|
1772
|
+
}
|
|
1773
|
+
throw err;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
async function stopBridge(options) {
|
|
1777
|
+
const { instanceId, stateDir, platform } = options;
|
|
1778
|
+
const state = loadBridgeState(stateDir, instanceId);
|
|
1779
|
+
if (!state) {
|
|
1780
|
+
return false;
|
|
1781
|
+
}
|
|
1782
|
+
if (!isProcessAlive(state.pid)) {
|
|
1783
|
+
clearBridgeState(stateDir, instanceId);
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
await terminateProcess(state.pid, platform);
|
|
1788
|
+
} catch {
|
|
1789
|
+
}
|
|
1790
|
+
clearBridgeState(stateDir, instanceId);
|
|
1791
|
+
return true;
|
|
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
|
+
}
|
|
1822
|
+
function rotateLog(logPath) {
|
|
1823
|
+
if (!fs7.existsSync(logPath)) return;
|
|
1824
|
+
try {
|
|
1825
|
+
const stats = fs7.statSync(logPath);
|
|
1826
|
+
if (stats.size === 0) return;
|
|
1827
|
+
const prevPath = `${logPath}.prev`;
|
|
1828
|
+
fs7.renameSync(logPath, prevPath);
|
|
1829
|
+
} catch {
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
function updateBridgeHeartbeat(stateDir, instanceId) {
|
|
1833
|
+
const state = loadBridgeState(stateDir, instanceId);
|
|
1834
|
+
if (!state) return;
|
|
1835
|
+
if (state.pid !== process.pid) return;
|
|
1836
|
+
state.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
|
|
1837
|
+
saveBridgeState(stateDir, instanceId, state);
|
|
565
1838
|
}
|
|
566
1839
|
function getHeartbeatAge(stateDir, instanceId) {
|
|
567
1840
|
const state = loadBridgeState(stateDir, instanceId);
|
|
@@ -571,6 +1844,9 @@ function getHeartbeatAge(stateDir, instanceId) {
|
|
|
571
1844
|
if (isNaN(heartbeatTime)) return null;
|
|
572
1845
|
return Math.floor((Date.now() - heartbeatTime) / 1e3);
|
|
573
1846
|
}
|
|
1847
|
+
function getBridgeHeartbeatTimestamp(stateDir, instanceId) {
|
|
1848
|
+
return resolveHeartbeatTimestamp(loadBridgeState(stateDir, instanceId));
|
|
1849
|
+
}
|
|
574
1850
|
function getBridgeStatus(stateDir, instanceId) {
|
|
575
1851
|
const state = loadBridgeState(stateDir, instanceId);
|
|
576
1852
|
if (!state) return "stopped";
|
|
@@ -580,16 +1856,32 @@ function getBridgeStatus(stateDir, instanceId) {
|
|
|
580
1856
|
}
|
|
581
1857
|
return "running";
|
|
582
1858
|
}
|
|
1859
|
+
var DEFAULT_APP_SERVER_URL2, APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_START_TIMEOUT_MS, APP_SERVER_GATEWAY_START_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS, APP_SERVER_AUTH_QUERY_PARAM, APP_SERVER_AUTH_FILE_MODE;
|
|
1860
|
+
var init_bridge = __esm({
|
|
1861
|
+
"src/engine/bridge.ts"() {
|
|
1862
|
+
"use strict";
|
|
1863
|
+
init_common();
|
|
1864
|
+
init_runtime();
|
|
1865
|
+
init_state();
|
|
1866
|
+
DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
|
|
1867
|
+
APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
1868
|
+
APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
1869
|
+
APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
|
|
1870
|
+
APP_SERVER_HEALTH_RETRY_MS = 250;
|
|
1871
|
+
APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
|
|
1872
|
+
APP_SERVER_AUTH_FILE_MODE = 384;
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
583
1875
|
|
|
584
1876
|
// src/engine/dashboard.ts
|
|
585
|
-
import * as
|
|
586
|
-
import * as
|
|
1877
|
+
import * as fs8 from "fs";
|
|
1878
|
+
import * as path8 from "path";
|
|
587
1879
|
import { execSync as execSync3 } from "child_process";
|
|
588
1880
|
function collectAgents(commsDir) {
|
|
589
|
-
const heartbeatsPath =
|
|
590
|
-
if (!
|
|
1881
|
+
const heartbeatsPath = path8.join(commsDir, "heartbeats.json");
|
|
1882
|
+
if (!fs8.existsSync(heartbeatsPath)) return [];
|
|
591
1883
|
try {
|
|
592
|
-
const raw =
|
|
1884
|
+
const raw = fs8.readFileSync(heartbeatsPath, "utf-8");
|
|
593
1885
|
const data = JSON.parse(raw);
|
|
594
1886
|
return Object.entries(data).map(([name, info]) => ({
|
|
595
1887
|
name: info.agent ?? name,
|
|
@@ -625,22 +1917,22 @@ function collectBridges(repoRoot) {
|
|
|
625
1917
|
});
|
|
626
1918
|
}
|
|
627
1919
|
}
|
|
628
|
-
const tmpDir =
|
|
629
|
-
if (
|
|
1920
|
+
const tmpDir = path8.join(repoRoot, ".tmp");
|
|
1921
|
+
if (fs8.existsSync(tmpDir)) {
|
|
630
1922
|
try {
|
|
631
|
-
const dirs =
|
|
1923
|
+
const dirs = fs8.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
|
|
632
1924
|
for (const dir of dirs) {
|
|
633
|
-
const daemonPath =
|
|
634
|
-
if (!
|
|
1925
|
+
const daemonPath = path8.join(tmpDir, dir, "bridge-daemon.json");
|
|
1926
|
+
if (!fs8.existsSync(daemonPath)) continue;
|
|
635
1927
|
try {
|
|
636
|
-
const raw =
|
|
1928
|
+
const raw = fs8.readFileSync(daemonPath, "utf-8");
|
|
637
1929
|
const daemon = JSON.parse(raw);
|
|
638
1930
|
const alreadyCovered = bridges.some(
|
|
639
1931
|
(b) => b.pid === daemon.pid && b.pid !== null
|
|
640
1932
|
);
|
|
641
1933
|
if (alreadyCovered) continue;
|
|
642
|
-
const agentFile =
|
|
643
|
-
const agentName =
|
|
1934
|
+
const agentFile = path8.join(tmpDir, dir, "agent-name.txt");
|
|
1935
|
+
const agentName = fs8.existsSync(agentFile) ? fs8.readFileSync(agentFile, "utf-8").trim() : dir;
|
|
644
1936
|
const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
|
|
645
1937
|
const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
|
|
646
1938
|
const port = portMatch ? parseInt(portMatch[1], 10) : null;
|
|
@@ -729,51 +2021,2340 @@ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
|
|
|
729
2021
|
warnings
|
|
730
2022
|
};
|
|
731
2023
|
}
|
|
2024
|
+
var init_dashboard = __esm({
|
|
2025
|
+
"src/engine/dashboard.ts"() {
|
|
2026
|
+
"use strict";
|
|
2027
|
+
init_config();
|
|
2028
|
+
init_bridge();
|
|
2029
|
+
init_state();
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
732
2032
|
|
|
733
|
-
// src/
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
2033
|
+
// src/adapters/claude.ts
|
|
2034
|
+
import * as fs9 from "fs";
|
|
2035
|
+
import * as path9 from "path";
|
|
2036
|
+
import { execSync as execSync4 } from "child_process";
|
|
2037
|
+
function findMcpJsonPath(ctx) {
|
|
2038
|
+
return path9.join(ctx.repoRoot, ".mcp.json");
|
|
737
2039
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const onAbort = () => {
|
|
745
|
-
clearTimeout(timer);
|
|
746
|
-
resolve5();
|
|
747
|
-
};
|
|
748
|
-
const timer = setTimeout(() => {
|
|
749
|
-
options?.signal?.removeEventListener("abort", onAbort);
|
|
750
|
-
resolve5();
|
|
751
|
-
}, intervalMs);
|
|
752
|
-
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
753
|
-
});
|
|
2040
|
+
function findClaudeCommand() {
|
|
2041
|
+
try {
|
|
2042
|
+
execSync4("claude --version", { stdio: "pipe" });
|
|
2043
|
+
return "claude";
|
|
2044
|
+
} catch {
|
|
2045
|
+
return null;
|
|
754
2046
|
}
|
|
755
2047
|
}
|
|
756
|
-
function
|
|
757
|
-
const
|
|
758
|
-
|
|
2048
|
+
function buildMcpServerEntry(ctx) {
|
|
2049
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
2050
|
+
if (!managed.command) return null;
|
|
759
2051
|
return {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
2052
|
+
type: "stdio",
|
|
2053
|
+
command: managed.command,
|
|
2054
|
+
args: managed.args,
|
|
2055
|
+
env: managed.env
|
|
764
2056
|
};
|
|
765
2057
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
|
|
2058
|
+
function setNestedKey(obj, keyPath, value) {
|
|
2059
|
+
const keys = keyPath.split(".");
|
|
2060
|
+
let current = obj;
|
|
2061
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
2062
|
+
const key = keys[i];
|
|
2063
|
+
if (typeof current[key] !== "object" || current[key] === null) {
|
|
2064
|
+
current[key] = {};
|
|
2065
|
+
}
|
|
2066
|
+
current = current[key];
|
|
2067
|
+
}
|
|
2068
|
+
current[keys[keys.length - 1]] = value;
|
|
2069
|
+
}
|
|
2070
|
+
function normalizeTapCommsDir(value) {
|
|
2071
|
+
return typeof value === "string" ? path9.resolve(value).replace(/\\/g, "/") : "";
|
|
2072
|
+
}
|
|
2073
|
+
var MCP_SERVER_KEY, claudeAdapter;
|
|
2074
|
+
var init_claude = __esm({
|
|
2075
|
+
"src/adapters/claude.ts"() {
|
|
2076
|
+
"use strict";
|
|
2077
|
+
init_state();
|
|
2078
|
+
init_common();
|
|
2079
|
+
MCP_SERVER_KEY = "tap-comms";
|
|
2080
|
+
claudeAdapter = {
|
|
2081
|
+
runtime: "claude",
|
|
2082
|
+
async probe(ctx) {
|
|
2083
|
+
const warnings = [];
|
|
2084
|
+
const issues = [];
|
|
2085
|
+
const configPath = findMcpJsonPath(ctx);
|
|
2086
|
+
const configExists = fs9.existsSync(configPath);
|
|
2087
|
+
const runtimeCommand = findClaudeCommand();
|
|
2088
|
+
const canWrite = configExists ? (() => {
|
|
2089
|
+
try {
|
|
2090
|
+
fs9.accessSync(configPath, fs9.constants.W_OK);
|
|
2091
|
+
return true;
|
|
2092
|
+
} catch {
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
2095
|
+
})() : true;
|
|
2096
|
+
if (!runtimeCommand) {
|
|
2097
|
+
warnings.push(
|
|
2098
|
+
"Claude CLI not found in PATH. Config will be created but may need manual setup."
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
const managed = buildManagedMcpServerSpec(ctx);
|
|
2102
|
+
warnings.push(...managed.warnings);
|
|
2103
|
+
issues.push(...managed.issues);
|
|
2104
|
+
if (!fs9.existsSync(ctx.commsDir)) {
|
|
2105
|
+
issues.push(
|
|
2106
|
+
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
return {
|
|
2110
|
+
installed: true,
|
|
2111
|
+
// Claude adapter always "installed" — .mcp.json is per-project
|
|
2112
|
+
configPath,
|
|
2113
|
+
configExists,
|
|
2114
|
+
runtimeCommand,
|
|
2115
|
+
version: null,
|
|
2116
|
+
canWrite,
|
|
2117
|
+
warnings,
|
|
2118
|
+
issues
|
|
2119
|
+
};
|
|
2120
|
+
},
|
|
2121
|
+
async plan(ctx, probe) {
|
|
2122
|
+
const configPath = probe.configPath ?? findMcpJsonPath(ctx);
|
|
2123
|
+
const conflicts = [];
|
|
2124
|
+
const warnings = [];
|
|
2125
|
+
const operations = [];
|
|
2126
|
+
const ownedArtifacts = [];
|
|
2127
|
+
if (probe.configExists) {
|
|
2128
|
+
const raw = fs9.readFileSync(configPath, "utf-8");
|
|
2129
|
+
try {
|
|
2130
|
+
const config = JSON.parse(raw);
|
|
2131
|
+
if (config.mcpServers?.[MCP_SERVER_KEY]) {
|
|
2132
|
+
conflicts.push(
|
|
2133
|
+
`Existing "${MCP_SERVER_KEY}" entry in .mcp.json will be overwritten.`
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
} catch {
|
|
2137
|
+
warnings.push(
|
|
2138
|
+
".mcp.json exists but is not valid JSON. Will be overwritten."
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const serverEntry = buildMcpServerEntry(ctx);
|
|
2143
|
+
if (!serverEntry) {
|
|
2144
|
+
warnings.push(
|
|
2145
|
+
"tap-comms MCP server entry not found. Skipping .mcp.json patch. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
|
|
2146
|
+
);
|
|
2147
|
+
return {
|
|
2148
|
+
runtime: "claude",
|
|
2149
|
+
operations: [],
|
|
2150
|
+
ownedArtifacts: [],
|
|
2151
|
+
backupDir: ensureBackupDir(ctx.stateDir, "claude"),
|
|
2152
|
+
restartRequired: false,
|
|
2153
|
+
conflicts,
|
|
2154
|
+
warnings
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
operations.push({
|
|
2158
|
+
type: probe.configExists ? "merge" : "set",
|
|
2159
|
+
path: configPath,
|
|
2160
|
+
key: `mcpServers.${MCP_SERVER_KEY}`,
|
|
2161
|
+
value: serverEntry
|
|
2162
|
+
});
|
|
2163
|
+
ownedArtifacts.push({
|
|
2164
|
+
kind: "json-path",
|
|
2165
|
+
path: configPath,
|
|
2166
|
+
selector: `mcpServers.${MCP_SERVER_KEY}`
|
|
2167
|
+
});
|
|
2168
|
+
const backupDir = ensureBackupDir(ctx.stateDir, "claude");
|
|
2169
|
+
return {
|
|
2170
|
+
runtime: "claude",
|
|
2171
|
+
operations,
|
|
2172
|
+
ownedArtifacts,
|
|
2173
|
+
backupDir,
|
|
2174
|
+
restartRequired: true,
|
|
2175
|
+
conflicts,
|
|
2176
|
+
warnings
|
|
2177
|
+
};
|
|
2178
|
+
},
|
|
2179
|
+
async apply(_ctx, plan) {
|
|
2180
|
+
const changedFiles = [];
|
|
2181
|
+
const warnings = [];
|
|
2182
|
+
let appliedOps = 0;
|
|
2183
|
+
for (const op of plan.operations) {
|
|
2184
|
+
try {
|
|
2185
|
+
if (op.type === "set" || op.type === "merge") {
|
|
2186
|
+
let config = {};
|
|
2187
|
+
if (fs9.existsSync(op.path)) {
|
|
2188
|
+
backupFile(op.path, plan.backupDir);
|
|
2189
|
+
const raw = fs9.readFileSync(op.path, "utf-8");
|
|
2190
|
+
try {
|
|
2191
|
+
config = JSON.parse(raw);
|
|
2192
|
+
} catch {
|
|
2193
|
+
warnings.push(
|
|
2194
|
+
`${op.path} was invalid JSON. Created backup and starting fresh.`
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (op.key) {
|
|
2199
|
+
setNestedKey(config, op.key, op.value);
|
|
2200
|
+
}
|
|
2201
|
+
const tmp = `${op.path}.tmp.${process.pid}`;
|
|
2202
|
+
fs9.writeFileSync(
|
|
2203
|
+
tmp,
|
|
2204
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
2205
|
+
"utf-8"
|
|
2206
|
+
);
|
|
2207
|
+
fs9.renameSync(tmp, op.path);
|
|
2208
|
+
changedFiles.push(op.path);
|
|
2209
|
+
appliedOps++;
|
|
2210
|
+
}
|
|
2211
|
+
} catch (err) {
|
|
2212
|
+
warnings.push(
|
|
2213
|
+
`Failed to apply op on ${op.path}: ${err instanceof Error ? err.message : String(err)}`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
const lastAppliedHash = changedFiles.length > 0 ? fileHash(changedFiles[0]) : "";
|
|
2218
|
+
return {
|
|
2219
|
+
success: appliedOps > 0,
|
|
2220
|
+
appliedOps,
|
|
2221
|
+
backupCreated: true,
|
|
2222
|
+
lastAppliedHash,
|
|
2223
|
+
ownedArtifacts: plan.ownedArtifacts,
|
|
2224
|
+
changedFiles,
|
|
2225
|
+
restartRequired: plan.restartRequired,
|
|
2226
|
+
warnings
|
|
2227
|
+
};
|
|
2228
|
+
},
|
|
2229
|
+
async verify(ctx, plan) {
|
|
2230
|
+
const checks = [];
|
|
2231
|
+
const warnings = [];
|
|
2232
|
+
const configPath = plan.operations[0]?.path;
|
|
2233
|
+
if (configPath) {
|
|
2234
|
+
checks.push({
|
|
2235
|
+
name: "Config file exists",
|
|
2236
|
+
passed: fs9.existsSync(configPath),
|
|
2237
|
+
message: fs9.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
2238
|
+
});
|
|
2239
|
+
if (fs9.existsSync(configPath)) {
|
|
2240
|
+
try {
|
|
2241
|
+
const raw = fs9.readFileSync(configPath, "utf-8");
|
|
2242
|
+
const config = JSON.parse(raw);
|
|
2243
|
+
checks.push({ name: "Config is valid JSON", passed: true });
|
|
2244
|
+
const entry = config.mcpServers?.[MCP_SERVER_KEY];
|
|
2245
|
+
checks.push({
|
|
2246
|
+
name: "tap-comms entry present",
|
|
2247
|
+
passed: !!entry,
|
|
2248
|
+
message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
|
|
2249
|
+
});
|
|
2250
|
+
if (entry) {
|
|
2251
|
+
const hasCommsDir = normalizeTapCommsDir(entry.env?.TAP_COMMS_DIR) === normalizeTapCommsDir(ctx.commsDir);
|
|
2252
|
+
checks.push({
|
|
2253
|
+
name: "TAP_COMMS_DIR configured",
|
|
2254
|
+
passed: hasCommsDir,
|
|
2255
|
+
message: hasCommsDir ? void 0 : `Expected ${ctx.commsDir}`
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
} catch {
|
|
2259
|
+
checks.push({
|
|
2260
|
+
name: "Config is valid JSON",
|
|
2261
|
+
passed: false,
|
|
2262
|
+
message: "Parse error"
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
checks.push({
|
|
2268
|
+
name: "Comms directory exists",
|
|
2269
|
+
passed: fs9.existsSync(ctx.commsDir),
|
|
2270
|
+
message: fs9.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
2271
|
+
});
|
|
2272
|
+
const cmd = findClaudeCommand();
|
|
2273
|
+
checks.push({
|
|
2274
|
+
name: "Claude CLI found",
|
|
2275
|
+
passed: !!cmd,
|
|
2276
|
+
message: cmd ? void 0 : "claude not in PATH (non-blocking)"
|
|
2277
|
+
});
|
|
2278
|
+
if (!cmd) {
|
|
2279
|
+
warnings.push(
|
|
2280
|
+
"Claude CLI not in PATH. Config is ready but cannot verify runtime reads it."
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
const ok = checks.filter((c) => c.name !== "Claude CLI found").every((c) => c.passed);
|
|
2284
|
+
return { ok, checks, restartRequired: true, warnings };
|
|
2285
|
+
},
|
|
2286
|
+
bridgeMode() {
|
|
2287
|
+
return "native-push";
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
|
|
2293
|
+
// src/artifact-backups.ts
|
|
2294
|
+
import * as crypto2 from "crypto";
|
|
2295
|
+
import * as fs10 from "fs";
|
|
2296
|
+
import * as path10 from "path";
|
|
2297
|
+
function selectorHash(selector) {
|
|
2298
|
+
return crypto2.createHash("sha256").update(selector).digest("hex").slice(0, 12);
|
|
2299
|
+
}
|
|
2300
|
+
function artifactBackupPath(backupDir, kind, selector) {
|
|
2301
|
+
const safeKind = kind.replace(/[^a-z-]/gi, "-");
|
|
2302
|
+
return path10.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
|
|
2303
|
+
}
|
|
2304
|
+
function writeArtifactBackup(backupPath, payload) {
|
|
2305
|
+
fs10.mkdirSync(path10.dirname(backupPath), { recursive: true });
|
|
2306
|
+
const tmp = `${backupPath}.tmp.${process.pid}`;
|
|
2307
|
+
fs10.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
2308
|
+
fs10.renameSync(tmp, backupPath);
|
|
2309
|
+
}
|
|
2310
|
+
var init_artifact_backups = __esm({
|
|
2311
|
+
"src/artifact-backups.ts"() {
|
|
2312
|
+
"use strict";
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
// src/toml.ts
|
|
2317
|
+
function splitLines(content) {
|
|
2318
|
+
return content.replace(/\r\n/g, "\n").split("\n");
|
|
2319
|
+
}
|
|
2320
|
+
function tableHeader(selector) {
|
|
2321
|
+
return `[${selector}]`;
|
|
2322
|
+
}
|
|
2323
|
+
function findTableRange(lines, selector) {
|
|
2324
|
+
const header = tableHeader(selector);
|
|
2325
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2326
|
+
if (lines[i].trim() !== header) continue;
|
|
2327
|
+
let end = lines.length;
|
|
2328
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2329
|
+
const trimmed = lines[j].trim();
|
|
2330
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
2331
|
+
end = j;
|
|
2332
|
+
break;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
return { start: i, end };
|
|
2336
|
+
}
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
function escapeBasicString(value) {
|
|
2340
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2341
|
+
}
|
|
2342
|
+
function renderValue(value) {
|
|
2343
|
+
if (Array.isArray(value)) {
|
|
2344
|
+
return `[${value.map((item) => `"${escapeBasicString(item)}"`).join(", ")}]`;
|
|
2345
|
+
}
|
|
2346
|
+
return `"${escapeBasicString(value)}"`;
|
|
2347
|
+
}
|
|
2348
|
+
function extractTomlTable(content, selector) {
|
|
2349
|
+
const lines = splitLines(content);
|
|
2350
|
+
const range = findTableRange(lines, selector);
|
|
2351
|
+
if (!range) return null;
|
|
2352
|
+
return `${lines.slice(range.start, range.end).join("\n")}
|
|
2353
|
+
`;
|
|
2354
|
+
}
|
|
2355
|
+
function replaceTomlTable(content, selector, replacement) {
|
|
2356
|
+
const lines = splitLines(content);
|
|
2357
|
+
const range = findTableRange(lines, selector);
|
|
2358
|
+
const replacementLines = replacement.replace(/\r\n/g, "\n").trimEnd().split("\n");
|
|
2359
|
+
if (!range) {
|
|
2360
|
+
const doc = trimTomlDocument(content);
|
|
2361
|
+
if (!doc) return `${replacement.trimEnd()}
|
|
2362
|
+
`;
|
|
2363
|
+
return `${doc}
|
|
2364
|
+
|
|
2365
|
+
${replacement.trimEnd()}
|
|
2366
|
+
`;
|
|
2367
|
+
}
|
|
2368
|
+
const next = [
|
|
2369
|
+
...lines.slice(0, range.start),
|
|
2370
|
+
...replacementLines,
|
|
2371
|
+
...lines.slice(range.end)
|
|
2372
|
+
];
|
|
2373
|
+
return `${trimTomlDocument(next.join("\n"))}
|
|
2374
|
+
`;
|
|
2375
|
+
}
|
|
2376
|
+
function renderTomlTable(selector, entries, existingContent) {
|
|
2377
|
+
const preserved = parseTomlAssignments(existingContent ?? "");
|
|
2378
|
+
const merged = { ...preserved, ...entries };
|
|
2379
|
+
const lines = [tableHeader(selector)];
|
|
2380
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
2381
|
+
lines.push(`${key} = ${renderValue(value)}`);
|
|
2382
|
+
}
|
|
2383
|
+
return `${lines.join("\n")}
|
|
2384
|
+
`;
|
|
2385
|
+
}
|
|
2386
|
+
function parseTomlAssignments(tableContent) {
|
|
2387
|
+
const lines = splitLines(tableContent);
|
|
2388
|
+
const values = {};
|
|
2389
|
+
for (const rawLine of lines) {
|
|
2390
|
+
const line = rawLine.trim();
|
|
2391
|
+
if (!line || line.startsWith("#") || line.startsWith("[") && line.endsWith("]")) {
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
const match = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
|
|
2395
|
+
if (!match) continue;
|
|
2396
|
+
const [, key, rawValue] = match;
|
|
2397
|
+
const value = rawValue.trim();
|
|
2398
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2399
|
+
const items = value.slice(1, -1).split(",").map((item) => item.trim()).filter(Boolean).map(unquoteTomlString);
|
|
2400
|
+
values[key] = items;
|
|
2401
|
+
continue;
|
|
2402
|
+
}
|
|
2403
|
+
values[key] = unquoteTomlString(value);
|
|
2404
|
+
}
|
|
2405
|
+
return values;
|
|
2406
|
+
}
|
|
2407
|
+
function trimTomlDocument(content) {
|
|
2408
|
+
return content.replace(/\s+$/g, "").replace(/\n{3,}/g, "\n\n");
|
|
2409
|
+
}
|
|
2410
|
+
function unquoteTomlString(value) {
|
|
2411
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2412
|
+
const inner = value.slice(1, -1);
|
|
2413
|
+
return value.startsWith('"') ? inner.replace(/\\"/g, '"').replace(/\\\\/g, "\\") : inner;
|
|
2414
|
+
}
|
|
2415
|
+
return value;
|
|
2416
|
+
}
|
|
2417
|
+
var init_toml = __esm({
|
|
2418
|
+
"src/toml.ts"() {
|
|
2419
|
+
"use strict";
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
// src/adapters/codex.ts
|
|
2424
|
+
import * as fs11 from "fs";
|
|
2425
|
+
import * as path11 from "path";
|
|
2426
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2427
|
+
function findCodexConfigPath() {
|
|
2428
|
+
return path11.join(getHomeDir(), ".codex", "config.toml");
|
|
2429
|
+
}
|
|
2430
|
+
function canonicalizeTrustPath(targetPath) {
|
|
2431
|
+
let resolved = path11.resolve(targetPath).replace(/\//g, "\\");
|
|
2432
|
+
const driveRoot = /^[A-Za-z]:\\$/;
|
|
2433
|
+
if (!driveRoot.test(resolved)) {
|
|
2434
|
+
resolved = resolved.replace(/\\+$/g, "");
|
|
2435
|
+
}
|
|
2436
|
+
return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
|
|
2437
|
+
}
|
|
2438
|
+
function trustSelector(targetPath) {
|
|
2439
|
+
return `projects.'${canonicalizeTrustPath(targetPath)}'`;
|
|
2440
|
+
}
|
|
2441
|
+
function getTrustTargets(ctx) {
|
|
2442
|
+
const targets = [ctx.repoRoot, process.cwd()];
|
|
2443
|
+
return [...new Set(targets.map((value) => path11.resolve(value)))];
|
|
2444
|
+
}
|
|
2445
|
+
function buildManagedArtifacts(configPath, ctx) {
|
|
2446
|
+
const artifacts = [
|
|
2447
|
+
{ kind: "toml-table", path: configPath, selector: MCP_SELECTOR },
|
|
2448
|
+
{ kind: "toml-table", path: configPath, selector: ENV_SELECTOR }
|
|
2449
|
+
];
|
|
2450
|
+
for (const target of getTrustTargets(ctx)) {
|
|
2451
|
+
artifacts.push({
|
|
2452
|
+
kind: "toml-table",
|
|
2453
|
+
path: configPath,
|
|
2454
|
+
selector: trustSelector(target)
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
return artifacts;
|
|
2458
|
+
}
|
|
2459
|
+
function readConfigOrEmpty(configPath) {
|
|
2460
|
+
if (!fs11.existsSync(configPath)) return "";
|
|
2461
|
+
return fs11.readFileSync(configPath, "utf-8");
|
|
2462
|
+
}
|
|
2463
|
+
function writeTomlFile(filePath, content) {
|
|
2464
|
+
fs11.mkdirSync(path11.dirname(filePath), { recursive: true });
|
|
2465
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
2466
|
+
fs11.writeFileSync(tmp, content, "utf-8");
|
|
2467
|
+
fs11.renameSync(tmp, filePath);
|
|
2468
|
+
}
|
|
2469
|
+
function verifyManagedToml(content, ctx, configPath) {
|
|
2470
|
+
const checks = [];
|
|
2471
|
+
const managed = buildManagedMcpServerSpec(ctx);
|
|
2472
|
+
const mainTable = extractTomlTable(content, MCP_SELECTOR);
|
|
2473
|
+
const envTable = extractTomlTable(content, ENV_SELECTOR);
|
|
2474
|
+
checks.push({
|
|
2475
|
+
name: "Codex config exists",
|
|
2476
|
+
passed: fs11.existsSync(configPath),
|
|
2477
|
+
message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
2478
|
+
});
|
|
2479
|
+
checks.push({
|
|
2480
|
+
name: "tap-comms MCP table present",
|
|
2481
|
+
passed: !!mainTable,
|
|
2482
|
+
message: mainTable ? void 0 : `${MCP_SELECTOR} not found`
|
|
2483
|
+
});
|
|
2484
|
+
checks.push({
|
|
2485
|
+
name: "tap-comms env table present",
|
|
2486
|
+
passed: !!envTable,
|
|
2487
|
+
message: envTable ? void 0 : `${ENV_SELECTOR} not found`
|
|
2488
|
+
});
|
|
2489
|
+
for (const target of getTrustTargets(ctx)) {
|
|
2490
|
+
const selector = trustSelector(target);
|
|
2491
|
+
const trustTable = extractTomlTable(content, selector);
|
|
2492
|
+
checks.push({
|
|
2493
|
+
name: `Trust table present: ${canonicalizeTrustPath(target)}`,
|
|
2494
|
+
passed: !!trustTable && trustTable.includes('trust_level = "trusted"'),
|
|
2495
|
+
message: trustTable && trustTable.includes('trust_level = "trusted"') ? void 0 : `${selector} missing trust_level = "trusted"`
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
if (mainTable && managed.command) {
|
|
2499
|
+
checks.push({
|
|
2500
|
+
name: "Managed command configured",
|
|
2501
|
+
passed: mainTable.includes(
|
|
2502
|
+
`command = "${managed.command.replace(/\\/g, "\\\\")}"`
|
|
2503
|
+
) && mainTable.includes(
|
|
2504
|
+
`args = ["${managed.args[0]?.replace(/\\/g, "\\\\") ?? ""}"]`
|
|
2505
|
+
),
|
|
2506
|
+
message: "Managed tap-comms command/args do not match expected values"
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
return checks;
|
|
2510
|
+
}
|
|
2511
|
+
var MCP_SELECTOR, ENV_SELECTOR, codexAdapter;
|
|
2512
|
+
var init_codex = __esm({
|
|
2513
|
+
"src/adapters/codex.ts"() {
|
|
2514
|
+
"use strict";
|
|
2515
|
+
init_state();
|
|
2516
|
+
init_artifact_backups();
|
|
2517
|
+
init_toml();
|
|
2518
|
+
init_common();
|
|
2519
|
+
MCP_SELECTOR = "mcp_servers.tap-comms";
|
|
2520
|
+
ENV_SELECTOR = "mcp_servers.tap-comms.env";
|
|
2521
|
+
codexAdapter = {
|
|
2522
|
+
runtime: "codex",
|
|
2523
|
+
async probe(ctx) {
|
|
2524
|
+
const warnings = [];
|
|
2525
|
+
const issues = [];
|
|
2526
|
+
const configPath = findCodexConfigPath();
|
|
2527
|
+
const configExists = fs11.existsSync(configPath);
|
|
2528
|
+
const runtimeProbe = probeCommand(
|
|
2529
|
+
ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
|
|
2530
|
+
);
|
|
2531
|
+
if (!runtimeProbe.command) {
|
|
2532
|
+
warnings.push(
|
|
2533
|
+
"Codex CLI not found in PATH. Config can still be written, but runtime verification will be limited."
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
if (!fs11.existsSync(ctx.commsDir)) {
|
|
2537
|
+
issues.push(
|
|
2538
|
+
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
const managed = buildManagedMcpServerSpec(ctx);
|
|
2542
|
+
warnings.push(...managed.warnings);
|
|
2543
|
+
issues.push(...managed.issues);
|
|
2544
|
+
return {
|
|
2545
|
+
installed: true,
|
|
2546
|
+
configPath,
|
|
2547
|
+
configExists,
|
|
2548
|
+
runtimeCommand: runtimeProbe.command,
|
|
2549
|
+
version: runtimeProbe.version,
|
|
2550
|
+
canWrite: canWriteOrCreate(configPath),
|
|
2551
|
+
warnings,
|
|
2552
|
+
issues
|
|
2553
|
+
};
|
|
2554
|
+
},
|
|
2555
|
+
async plan(ctx, probe) {
|
|
2556
|
+
const configPath = probe.configPath ?? findCodexConfigPath();
|
|
2557
|
+
const conflicts = [];
|
|
2558
|
+
const warnings = [];
|
|
2559
|
+
const operations = [];
|
|
2560
|
+
const ownedArtifacts = buildManagedArtifacts(configPath, ctx);
|
|
2561
|
+
if (probe.configExists) {
|
|
2562
|
+
const content = readConfigOrEmpty(configPath);
|
|
2563
|
+
if (extractTomlTable(content, MCP_SELECTOR)) {
|
|
2564
|
+
conflicts.push(`Existing ${MCP_SELECTOR} table will be updated.`);
|
|
2565
|
+
}
|
|
2566
|
+
if (extractTomlTable(content, ENV_SELECTOR)) {
|
|
2567
|
+
conflicts.push(`Existing ${ENV_SELECTOR} table will be updated.`);
|
|
2568
|
+
}
|
|
2569
|
+
for (const target of getTrustTargets(ctx)) {
|
|
2570
|
+
const selector = trustSelector(target);
|
|
2571
|
+
if (extractTomlTable(content, selector)) {
|
|
2572
|
+
conflicts.push(`Existing ${selector} table will be updated.`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
for (const artifact of ownedArtifacts) {
|
|
2577
|
+
operations.push({
|
|
2578
|
+
type: probe.configExists ? "merge" : "set",
|
|
2579
|
+
path: configPath,
|
|
2580
|
+
key: artifact.selector
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
return {
|
|
2584
|
+
runtime: "codex",
|
|
2585
|
+
operations,
|
|
2586
|
+
ownedArtifacts,
|
|
2587
|
+
backupDir: ensureBackupDir(ctx.stateDir, "codex"),
|
|
2588
|
+
restartRequired: true,
|
|
2589
|
+
conflicts,
|
|
2590
|
+
warnings
|
|
2591
|
+
};
|
|
2592
|
+
},
|
|
2593
|
+
async apply(ctx, plan) {
|
|
2594
|
+
const configPath = plan.operations[0]?.path ?? findCodexConfigPath();
|
|
2595
|
+
const warnings = [];
|
|
2596
|
+
const changedFiles = [];
|
|
2597
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
2598
|
+
warnings.push(...managed.warnings);
|
|
2599
|
+
if (managed.issues.length > 0 || !managed.command) {
|
|
2600
|
+
return {
|
|
2601
|
+
success: false,
|
|
2602
|
+
appliedOps: 0,
|
|
2603
|
+
backupCreated: false,
|
|
2604
|
+
lastAppliedHash: "",
|
|
2605
|
+
ownedArtifacts: [],
|
|
2606
|
+
changedFiles,
|
|
2607
|
+
restartRequired: false,
|
|
2608
|
+
warnings: [...managed.warnings, ...managed.issues]
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
const existingContent = readConfigOrEmpty(configPath);
|
|
2612
|
+
if (fs11.existsSync(configPath) && existingContent) {
|
|
2613
|
+
backupFile(configPath, plan.backupDir);
|
|
2614
|
+
}
|
|
2615
|
+
const artifactsWithBackups = plan.ownedArtifacts.map((artifact) => {
|
|
2616
|
+
const previousContent = artifact.kind === "toml-table" ? extractTomlTable(existingContent, artifact.selector) : null;
|
|
2617
|
+
const backupPath = artifactBackupPath(
|
|
2618
|
+
plan.backupDir,
|
|
2619
|
+
artifact.kind,
|
|
2620
|
+
artifact.selector
|
|
2621
|
+
);
|
|
2622
|
+
writeArtifactBackup(backupPath, {
|
|
2623
|
+
kind: "toml-table",
|
|
2624
|
+
selector: artifact.selector,
|
|
2625
|
+
existed: previousContent !== null,
|
|
2626
|
+
content: previousContent ?? void 0
|
|
2627
|
+
});
|
|
2628
|
+
return { ...artifact, backupPath };
|
|
2629
|
+
});
|
|
2630
|
+
let nextContent = existingContent;
|
|
2631
|
+
nextContent = replaceTomlTable(
|
|
2632
|
+
nextContent,
|
|
2633
|
+
MCP_SELECTOR,
|
|
2634
|
+
renderTomlTable(
|
|
2635
|
+
MCP_SELECTOR,
|
|
2636
|
+
{
|
|
2637
|
+
command: managed.command,
|
|
2638
|
+
args: managed.args
|
|
2639
|
+
},
|
|
2640
|
+
extractTomlTable(existingContent, MCP_SELECTOR)
|
|
2641
|
+
)
|
|
2642
|
+
);
|
|
2643
|
+
nextContent = replaceTomlTable(
|
|
2644
|
+
nextContent,
|
|
2645
|
+
ENV_SELECTOR,
|
|
2646
|
+
renderTomlTable(
|
|
2647
|
+
ENV_SELECTOR,
|
|
2648
|
+
managed.env,
|
|
2649
|
+
extractTomlTable(existingContent, ENV_SELECTOR)
|
|
2650
|
+
)
|
|
2651
|
+
);
|
|
2652
|
+
for (const target of getTrustTargets(ctx)) {
|
|
2653
|
+
const selector = trustSelector(target);
|
|
2654
|
+
nextContent = replaceTomlTable(
|
|
2655
|
+
nextContent,
|
|
2656
|
+
selector,
|
|
2657
|
+
renderTomlTable(
|
|
2658
|
+
selector,
|
|
2659
|
+
{ trust_level: "trusted" },
|
|
2660
|
+
extractTomlTable(existingContent, selector)
|
|
2661
|
+
)
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
writeTomlFile(configPath, nextContent);
|
|
2665
|
+
changedFiles.push(configPath);
|
|
2666
|
+
return {
|
|
2667
|
+
success: true,
|
|
2668
|
+
appliedOps: plan.operations.length,
|
|
2669
|
+
backupCreated: true,
|
|
2670
|
+
lastAppliedHash: fileHash(configPath),
|
|
2671
|
+
ownedArtifacts: artifactsWithBackups,
|
|
2672
|
+
changedFiles,
|
|
2673
|
+
restartRequired: true,
|
|
2674
|
+
warnings
|
|
2675
|
+
};
|
|
2676
|
+
},
|
|
2677
|
+
async verify(ctx, plan) {
|
|
2678
|
+
const warnings = [];
|
|
2679
|
+
const configPath = plan.operations[0]?.path ?? findCodexConfigPath();
|
|
2680
|
+
const content = readConfigOrEmpty(configPath);
|
|
2681
|
+
const runtimeProbe = probeCommand(
|
|
2682
|
+
ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
|
|
2683
|
+
);
|
|
2684
|
+
const checks = verifyManagedToml(content, ctx, configPath);
|
|
2685
|
+
checks.push({
|
|
2686
|
+
name: "Comms directory exists",
|
|
2687
|
+
passed: fs11.existsSync(ctx.commsDir),
|
|
2688
|
+
message: fs11.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
2689
|
+
});
|
|
2690
|
+
checks.push({
|
|
2691
|
+
name: "Codex CLI found",
|
|
2692
|
+
passed: !!runtimeProbe.command,
|
|
2693
|
+
message: runtimeProbe.command ? void 0 : "codex not in PATH (non-blocking)"
|
|
2694
|
+
});
|
|
2695
|
+
if (!runtimeProbe.command) {
|
|
2696
|
+
warnings.push(
|
|
2697
|
+
"Codex CLI not in PATH. Config is written, but runtime verification is partial."
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
return {
|
|
2701
|
+
ok: checks.filter((check) => check.name !== "Codex CLI found").every((check) => check.passed),
|
|
2702
|
+
checks,
|
|
2703
|
+
restartRequired: true,
|
|
2704
|
+
warnings
|
|
2705
|
+
};
|
|
2706
|
+
},
|
|
2707
|
+
bridgeMode() {
|
|
2708
|
+
return "app-server";
|
|
2709
|
+
},
|
|
2710
|
+
resolveBridgeScript(ctx) {
|
|
2711
|
+
const distDir = path11.dirname(fileURLToPath4(import.meta.url));
|
|
2712
|
+
const candidates = [
|
|
2713
|
+
// 1. Relative to bundled CLI (npm install / npx)
|
|
2714
|
+
path11.join(distDir, "bridges", "codex-bridge-runner.mjs"),
|
|
2715
|
+
// 2. Monorepo development — dist inside repo
|
|
2716
|
+
path11.join(
|
|
2717
|
+
ctx.repoRoot,
|
|
2718
|
+
"packages",
|
|
2719
|
+
"tap-comms",
|
|
2720
|
+
"dist",
|
|
2721
|
+
"bridges",
|
|
2722
|
+
"codex-bridge-runner.mjs"
|
|
2723
|
+
),
|
|
2724
|
+
// 3. Source file — dev mode with strip-types
|
|
2725
|
+
path11.join(
|
|
2726
|
+
ctx.repoRoot,
|
|
2727
|
+
"packages",
|
|
2728
|
+
"tap-comms",
|
|
2729
|
+
"src",
|
|
2730
|
+
"bridges",
|
|
2731
|
+
"codex-bridge-runner.ts"
|
|
2732
|
+
)
|
|
2733
|
+
];
|
|
2734
|
+
for (const candidate of candidates) {
|
|
2735
|
+
if (fs11.existsSync(candidate)) return candidate;
|
|
2736
|
+
}
|
|
2737
|
+
return null;
|
|
2738
|
+
}
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
});
|
|
2742
|
+
|
|
2743
|
+
// src/adapters/gemini.ts
|
|
2744
|
+
import * as fs12 from "fs";
|
|
2745
|
+
import * as path12 from "path";
|
|
2746
|
+
function candidateConfigPaths(ctx) {
|
|
2747
|
+
const home = getHomeDir();
|
|
2748
|
+
return [
|
|
2749
|
+
path12.join(ctx.repoRoot, ".gemini", "settings.json"),
|
|
2750
|
+
path12.join(home, ".gemini", "settings.json"),
|
|
2751
|
+
path12.join(home, ".gemini", "antigravity", "mcp_config.json")
|
|
2752
|
+
];
|
|
2753
|
+
}
|
|
2754
|
+
function chooseGeminiConfigPath(ctx) {
|
|
2755
|
+
const [workspaceConfig, homeConfig, antigravityConfig] = candidateConfigPaths(ctx);
|
|
2756
|
+
if (fs12.existsSync(workspaceConfig)) return workspaceConfig;
|
|
2757
|
+
if (fs12.existsSync(homeConfig)) return homeConfig;
|
|
2758
|
+
if (fs12.existsSync(antigravityConfig)) {
|
|
2759
|
+
const raw = fs12.readFileSync(antigravityConfig, "utf-8").trim();
|
|
2760
|
+
if (raw) {
|
|
2761
|
+
try {
|
|
2762
|
+
JSON.parse(raw);
|
|
2763
|
+
return antigravityConfig;
|
|
2764
|
+
} catch {
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return workspaceConfig;
|
|
2769
|
+
}
|
|
2770
|
+
function readJsonFile(filePath) {
|
|
2771
|
+
if (!fs12.existsSync(filePath)) return {};
|
|
2772
|
+
const raw = fs12.readFileSync(filePath, "utf-8").trim();
|
|
2773
|
+
if (!raw) return {};
|
|
2774
|
+
return JSON.parse(raw);
|
|
2775
|
+
}
|
|
2776
|
+
function setNestedKey2(obj, keyPath, value) {
|
|
2777
|
+
const keys = keyPath.split(".");
|
|
2778
|
+
let current = obj;
|
|
2779
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
2780
|
+
const key = keys[i];
|
|
2781
|
+
if (typeof current[key] !== "object" || current[key] === null) {
|
|
2782
|
+
current[key] = {};
|
|
2783
|
+
}
|
|
2784
|
+
current = current[key];
|
|
2785
|
+
}
|
|
2786
|
+
current[keys[keys.length - 1]] = value;
|
|
2787
|
+
}
|
|
2788
|
+
function readNestedKey(obj, keyPath) {
|
|
2789
|
+
let current = obj;
|
|
2790
|
+
for (const key of keyPath.split(".")) {
|
|
2791
|
+
if (typeof current !== "object" || current === null || !(key in current)) {
|
|
2792
|
+
return void 0;
|
|
2793
|
+
}
|
|
2794
|
+
current = current[key];
|
|
2795
|
+
}
|
|
2796
|
+
return current;
|
|
2797
|
+
}
|
|
2798
|
+
function verifyGeminiConfig(config, configPath, ctx) {
|
|
2799
|
+
const checks = [];
|
|
2800
|
+
const entry = readNestedKey(config, GEMINI_SELECTOR);
|
|
2801
|
+
checks.push({
|
|
2802
|
+
name: "Gemini config exists",
|
|
2803
|
+
passed: fs12.existsSync(configPath),
|
|
2804
|
+
message: fs12.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
2805
|
+
});
|
|
2806
|
+
checks.push({
|
|
2807
|
+
name: "tap-comms entry present",
|
|
2808
|
+
passed: !!entry,
|
|
2809
|
+
message: entry ? void 0 : `${GEMINI_SELECTOR} not found`
|
|
2810
|
+
});
|
|
2811
|
+
checks.push({
|
|
2812
|
+
name: "Comms directory exists",
|
|
2813
|
+
passed: fs12.existsSync(ctx.commsDir),
|
|
2814
|
+
message: fs12.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
2815
|
+
});
|
|
2816
|
+
if (entry?.env && typeof entry.env === "object") {
|
|
2817
|
+
checks.push({
|
|
2818
|
+
name: "TAP_COMMS_DIR configured",
|
|
2819
|
+
passed: entry.env.TAP_COMMS_DIR === ctx.commsDir.replace(/\\/g, "/"),
|
|
2820
|
+
message: `Expected ${ctx.commsDir.replace(/\\/g, "/")}`
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
return checks;
|
|
2824
|
+
}
|
|
2825
|
+
var GEMINI_SELECTOR, geminiAdapter;
|
|
2826
|
+
var init_gemini = __esm({
|
|
2827
|
+
"src/adapters/gemini.ts"() {
|
|
2828
|
+
"use strict";
|
|
2829
|
+
init_state();
|
|
2830
|
+
init_artifact_backups();
|
|
2831
|
+
init_common();
|
|
2832
|
+
GEMINI_SELECTOR = "mcpServers.tap-comms";
|
|
2833
|
+
geminiAdapter = {
|
|
2834
|
+
runtime: "gemini",
|
|
2835
|
+
async probe(ctx) {
|
|
2836
|
+
const warnings = [];
|
|
2837
|
+
const issues = [];
|
|
2838
|
+
const configPath = chooseGeminiConfigPath(ctx);
|
|
2839
|
+
const configExists = fs12.existsSync(configPath);
|
|
2840
|
+
const runtimeProbe = probeCommand(
|
|
2841
|
+
ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
|
|
2842
|
+
);
|
|
2843
|
+
if (!runtimeProbe.command) {
|
|
2844
|
+
warnings.push(
|
|
2845
|
+
"Gemini CLI not found in PATH. Config can still be written, but runtime verification will be limited."
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
if (!fs12.existsSync(ctx.commsDir)) {
|
|
2849
|
+
issues.push(
|
|
2850
|
+
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
2854
|
+
warnings.push(...managed.warnings);
|
|
2855
|
+
issues.push(...managed.issues);
|
|
2856
|
+
return {
|
|
2857
|
+
installed: true,
|
|
2858
|
+
configPath,
|
|
2859
|
+
configExists,
|
|
2860
|
+
runtimeCommand: runtimeProbe.command,
|
|
2861
|
+
version: runtimeProbe.version,
|
|
2862
|
+
canWrite: canWriteOrCreate(configPath),
|
|
2863
|
+
warnings,
|
|
2864
|
+
issues
|
|
2865
|
+
};
|
|
2866
|
+
},
|
|
2867
|
+
async plan(ctx, probe) {
|
|
2868
|
+
const configPath = probe.configPath ?? chooseGeminiConfigPath(ctx);
|
|
2869
|
+
const conflicts = [];
|
|
2870
|
+
const warnings = [];
|
|
2871
|
+
const operations = [];
|
|
2872
|
+
const ownedArtifacts = [
|
|
2873
|
+
{ kind: "json-path", path: configPath, selector: GEMINI_SELECTOR }
|
|
2874
|
+
];
|
|
2875
|
+
if (probe.configExists) {
|
|
2876
|
+
try {
|
|
2877
|
+
const config = readJsonFile(configPath);
|
|
2878
|
+
if (readNestedKey(config, GEMINI_SELECTOR) !== void 0) {
|
|
2879
|
+
conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
|
|
2880
|
+
}
|
|
2881
|
+
} catch {
|
|
2882
|
+
warnings.push(
|
|
2883
|
+
`${configPath} exists but is not valid JSON. It will be replaced.`
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
operations.push({
|
|
2888
|
+
type: probe.configExists ? "merge" : "set",
|
|
2889
|
+
path: configPath,
|
|
2890
|
+
key: GEMINI_SELECTOR
|
|
2891
|
+
});
|
|
2892
|
+
return {
|
|
2893
|
+
runtime: "gemini",
|
|
2894
|
+
operations,
|
|
2895
|
+
ownedArtifacts,
|
|
2896
|
+
backupDir: ensureBackupDir(ctx.stateDir, "gemini"),
|
|
2897
|
+
restartRequired: true,
|
|
2898
|
+
conflicts,
|
|
2899
|
+
warnings
|
|
2900
|
+
};
|
|
2901
|
+
},
|
|
2902
|
+
async apply(ctx, plan) {
|
|
2903
|
+
const configPath = plan.operations[0]?.path ?? chooseGeminiConfigPath(ctx);
|
|
2904
|
+
const warnings = [];
|
|
2905
|
+
const changedFiles = [];
|
|
2906
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
2907
|
+
warnings.push(...managed.warnings);
|
|
2908
|
+
if (managed.issues.length > 0 || !managed.command) {
|
|
2909
|
+
return {
|
|
2910
|
+
success: false,
|
|
2911
|
+
appliedOps: 0,
|
|
2912
|
+
backupCreated: false,
|
|
2913
|
+
lastAppliedHash: "",
|
|
2914
|
+
ownedArtifacts: [],
|
|
2915
|
+
changedFiles,
|
|
2916
|
+
restartRequired: false,
|
|
2917
|
+
warnings: [...managed.warnings, ...managed.issues]
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
let config = {};
|
|
2921
|
+
let previousValue = void 0;
|
|
2922
|
+
if (fs12.existsSync(configPath)) {
|
|
2923
|
+
if (fs12.readFileSync(configPath, "utf-8").trim()) {
|
|
2924
|
+
backupFile(configPath, plan.backupDir);
|
|
2925
|
+
}
|
|
2926
|
+
try {
|
|
2927
|
+
config = readJsonFile(configPath);
|
|
2928
|
+
} catch {
|
|
2929
|
+
warnings.push(
|
|
2930
|
+
`${configPath} was invalid JSON. Created backup and starting fresh.`
|
|
2931
|
+
);
|
|
2932
|
+
config = {};
|
|
2933
|
+
}
|
|
2934
|
+
previousValue = readNestedKey(config, GEMINI_SELECTOR);
|
|
2935
|
+
}
|
|
2936
|
+
const artifact = plan.ownedArtifacts[0];
|
|
2937
|
+
const backupPath = artifactBackupPath(
|
|
2938
|
+
plan.backupDir,
|
|
2939
|
+
artifact.kind,
|
|
2940
|
+
artifact.selector
|
|
2941
|
+
);
|
|
2942
|
+
writeArtifactBackup(backupPath, {
|
|
2943
|
+
kind: "json-path",
|
|
2944
|
+
selector: artifact.selector,
|
|
2945
|
+
existed: previousValue !== void 0,
|
|
2946
|
+
value: previousValue
|
|
2947
|
+
});
|
|
2948
|
+
setNestedKey2(config, GEMINI_SELECTOR, {
|
|
2949
|
+
command: managed.command,
|
|
2950
|
+
args: managed.args,
|
|
2951
|
+
env: managed.env
|
|
2952
|
+
});
|
|
2953
|
+
fs12.mkdirSync(path12.dirname(configPath), { recursive: true });
|
|
2954
|
+
const tmp = `${configPath}.tmp.${process.pid}`;
|
|
2955
|
+
fs12.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2956
|
+
fs12.renameSync(tmp, configPath);
|
|
2957
|
+
changedFiles.push(configPath);
|
|
2958
|
+
return {
|
|
2959
|
+
success: true,
|
|
2960
|
+
appliedOps: plan.operations.length,
|
|
2961
|
+
backupCreated: true,
|
|
2962
|
+
lastAppliedHash: fileHash(configPath),
|
|
2963
|
+
ownedArtifacts: [{ ...artifact, backupPath }],
|
|
2964
|
+
changedFiles,
|
|
2965
|
+
restartRequired: true,
|
|
2966
|
+
warnings
|
|
2967
|
+
};
|
|
2968
|
+
},
|
|
2969
|
+
async verify(ctx, plan) {
|
|
2970
|
+
const warnings = [];
|
|
2971
|
+
const configPath = plan.operations[0]?.path ?? chooseGeminiConfigPath(ctx);
|
|
2972
|
+
const runtimeProbe = probeCommand(
|
|
2973
|
+
ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
|
|
2974
|
+
);
|
|
2975
|
+
let checks;
|
|
2976
|
+
try {
|
|
2977
|
+
const config = readJsonFile(configPath);
|
|
2978
|
+
checks = verifyGeminiConfig(config, configPath, ctx);
|
|
2979
|
+
} catch {
|
|
2980
|
+
checks = [
|
|
2981
|
+
{
|
|
2982
|
+
name: "Gemini config is valid JSON",
|
|
2983
|
+
passed: false,
|
|
2984
|
+
message: "Parse error"
|
|
2985
|
+
}
|
|
2986
|
+
];
|
|
2987
|
+
}
|
|
2988
|
+
checks.push({
|
|
2989
|
+
name: "Gemini CLI found",
|
|
2990
|
+
passed: !!runtimeProbe.command,
|
|
2991
|
+
message: runtimeProbe.command ? void 0 : "gemini not in PATH (non-blocking)"
|
|
2992
|
+
});
|
|
2993
|
+
if (!runtimeProbe.command) {
|
|
2994
|
+
warnings.push(
|
|
2995
|
+
"Gemini CLI not in PATH. Config is written, but runtime verification is partial."
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2998
|
+
return {
|
|
2999
|
+
ok: checks.filter((check) => check.name !== "Gemini CLI found").every((check) => check.passed),
|
|
3000
|
+
checks,
|
|
3001
|
+
restartRequired: true,
|
|
3002
|
+
warnings
|
|
3003
|
+
};
|
|
3004
|
+
},
|
|
3005
|
+
bridgeMode() {
|
|
3006
|
+
return "polling";
|
|
3007
|
+
}
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
|
|
3012
|
+
// src/adapters/index.ts
|
|
3013
|
+
function getAdapter(runtime) {
|
|
3014
|
+
const adapter = adapters[runtime];
|
|
3015
|
+
if (!adapter) {
|
|
3016
|
+
throw new Error(
|
|
3017
|
+
`Adapter for "${runtime}" is not yet available. Supported: ${Object.keys(adapters).join(", ")}`
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
return adapter;
|
|
3021
|
+
}
|
|
3022
|
+
var adapters;
|
|
3023
|
+
var init_adapters = __esm({
|
|
3024
|
+
"src/adapters/index.ts"() {
|
|
3025
|
+
"use strict";
|
|
3026
|
+
init_claude();
|
|
3027
|
+
init_codex();
|
|
3028
|
+
init_gemini();
|
|
3029
|
+
adapters = {
|
|
3030
|
+
claude: claudeAdapter,
|
|
3031
|
+
codex: codexAdapter,
|
|
3032
|
+
gemini: geminiAdapter
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
});
|
|
3036
|
+
|
|
3037
|
+
// src/commands/bridge.ts
|
|
3038
|
+
import * as path13 from "path";
|
|
3039
|
+
function formatAge(seconds) {
|
|
3040
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
3041
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
3042
|
+
return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
|
|
3043
|
+
}
|
|
3044
|
+
function formatAppServerState(appServer) {
|
|
3045
|
+
const ownership = appServer.managed ? "managed" : "external";
|
|
3046
|
+
const pid = appServer.pid != null ? ` pid:${appServer.pid}` : "";
|
|
3047
|
+
const health = appServer.healthy ? "healthy" : "unhealthy";
|
|
3048
|
+
const auth = appServer.auth != null ? `, auth gateway:${appServer.auth.gatewayPid ?? "-"} -> ${appServer.auth.upstreamUrl}` : "";
|
|
3049
|
+
return `${health}, ${ownership}${pid}, ${appServer.url}${auth}`;
|
|
3050
|
+
}
|
|
3051
|
+
function redactProtectedUrl(url) {
|
|
3052
|
+
try {
|
|
3053
|
+
const parsed = new URL(url);
|
|
3054
|
+
if (parsed.searchParams.has("tap_token")) {
|
|
3055
|
+
parsed.searchParams.set("tap_token", "***");
|
|
3056
|
+
}
|
|
3057
|
+
return parsed.toString().replace(/\/$/, "");
|
|
3058
|
+
} catch {
|
|
3059
|
+
return url.replace(/tap_token=[^&]+/g, "tap_token=***");
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
function loadCurrentBridgeState(stateDir, instanceId, fallback) {
|
|
3063
|
+
return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
|
|
3064
|
+
}
|
|
3065
|
+
function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
|
|
3066
|
+
const shared = [];
|
|
3067
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
3068
|
+
if (id === currentInstanceId || !inst?.installed) {
|
|
3069
|
+
continue;
|
|
3070
|
+
}
|
|
3071
|
+
const instanceId = id;
|
|
3072
|
+
if (getBridgeStatus(stateDir, instanceId) !== "running") {
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
const bridgeState = loadCurrentBridgeState(
|
|
3076
|
+
stateDir,
|
|
3077
|
+
instanceId,
|
|
3078
|
+
inst.bridge
|
|
3079
|
+
);
|
|
3080
|
+
if (bridgeState?.appServer?.url === appServerUrl) {
|
|
3081
|
+
shared.push(instanceId);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
return shared;
|
|
3085
|
+
}
|
|
3086
|
+
function transferManagedAppServerOwnership(state, stateDir, recipientId, appServer) {
|
|
3087
|
+
const recipient = state.instances[recipientId];
|
|
3088
|
+
if (!recipient) {
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
const bridgeState = loadCurrentBridgeState(
|
|
3092
|
+
stateDir,
|
|
3093
|
+
recipientId,
|
|
3094
|
+
recipient.bridge
|
|
3095
|
+
);
|
|
3096
|
+
if (!bridgeState) {
|
|
3097
|
+
return false;
|
|
3098
|
+
}
|
|
3099
|
+
const transferredAppServer = {
|
|
3100
|
+
...appServer,
|
|
3101
|
+
managed: true,
|
|
3102
|
+
healthy: true,
|
|
3103
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3104
|
+
lastHealthyAt: appServer.lastHealthyAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3105
|
+
};
|
|
3106
|
+
const updatedBridge = {
|
|
3107
|
+
...bridgeState,
|
|
3108
|
+
appServer: transferredAppServer
|
|
3109
|
+
};
|
|
3110
|
+
saveBridgeState(stateDir, recipientId, updatedBridge);
|
|
3111
|
+
state.instances[recipientId] = {
|
|
3112
|
+
...recipient,
|
|
3113
|
+
bridge: updatedBridge
|
|
3114
|
+
};
|
|
3115
|
+
return true;
|
|
3116
|
+
}
|
|
3117
|
+
async function bridgeStart(identifier, agentName, flags = {}) {
|
|
3118
|
+
const repoRoot = findRepoRoot();
|
|
3119
|
+
let state = loadState(repoRoot);
|
|
3120
|
+
if (!state) {
|
|
3121
|
+
return {
|
|
3122
|
+
ok: false,
|
|
3123
|
+
command: "bridge",
|
|
3124
|
+
code: "TAP_NOT_INITIALIZED",
|
|
3125
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
3126
|
+
warnings: [],
|
|
3127
|
+
data: {}
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
const resolved = resolveInstanceId(identifier, state);
|
|
3131
|
+
if (!resolved.ok) {
|
|
3132
|
+
return {
|
|
3133
|
+
ok: false,
|
|
3134
|
+
command: "bridge",
|
|
3135
|
+
code: resolved.code,
|
|
3136
|
+
message: resolved.message,
|
|
3137
|
+
warnings: [],
|
|
3138
|
+
data: {}
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
const instanceId = resolved.instanceId;
|
|
3142
|
+
let instance = state.instances[instanceId];
|
|
3143
|
+
if (!instance?.installed) {
|
|
3144
|
+
return {
|
|
3145
|
+
ok: false,
|
|
3146
|
+
command: "bridge",
|
|
3147
|
+
instanceId,
|
|
3148
|
+
runtime: instance?.runtime,
|
|
3149
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
3150
|
+
message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance?.runtime ?? identifier}`,
|
|
3151
|
+
warnings: [],
|
|
3152
|
+
data: {}
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
const adapter = getAdapter(instance.runtime);
|
|
3156
|
+
const mode = adapter.bridgeMode();
|
|
3157
|
+
if (mode !== "app-server") {
|
|
3158
|
+
return {
|
|
3159
|
+
ok: true,
|
|
3160
|
+
command: "bridge",
|
|
3161
|
+
instanceId,
|
|
3162
|
+
runtime: instance.runtime,
|
|
3163
|
+
code: "TAP_NO_OP",
|
|
3164
|
+
message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
|
|
3165
|
+
warnings: [],
|
|
3166
|
+
data: { bridgeMode: mode }
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
|
|
3170
|
+
if (agentName && agentName !== instance.agentName) {
|
|
3171
|
+
instance = { ...instance, agentName };
|
|
3172
|
+
const updatedState = updateInstanceState(state, instanceId, instance);
|
|
3173
|
+
saveState(repoRoot, updatedState);
|
|
3174
|
+
state = updatedState;
|
|
3175
|
+
}
|
|
3176
|
+
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
3177
|
+
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
3178
|
+
if (!bridgeScript) {
|
|
3179
|
+
return {
|
|
3180
|
+
ok: false,
|
|
3181
|
+
command: "bridge",
|
|
3182
|
+
instanceId,
|
|
3183
|
+
runtime: instance.runtime,
|
|
3184
|
+
code: "TAP_BRIDGE_SCRIPT_MISSING",
|
|
3185
|
+
message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
|
|
3186
|
+
warnings: [],
|
|
3187
|
+
data: {}
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
3191
|
+
const runtimeCommand = resolvedConfig.runtimeCommand;
|
|
3192
|
+
const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
|
|
3193
|
+
let effectivePort = instance.port;
|
|
3194
|
+
if (effectivePort == null && manageAppServer) {
|
|
3195
|
+
effectivePort = await findNextAvailableAppServerPort(
|
|
3196
|
+
state,
|
|
3197
|
+
resolvedConfig.appServerUrl,
|
|
3198
|
+
4501,
|
|
3199
|
+
instanceId
|
|
3200
|
+
);
|
|
3201
|
+
instance = { ...instance, port: effectivePort };
|
|
3202
|
+
const updatedState = updateInstanceState(state, instanceId, instance);
|
|
3203
|
+
saveState(repoRoot, updatedState);
|
|
3204
|
+
state = updatedState;
|
|
3205
|
+
}
|
|
3206
|
+
const appServerUrl = resolveAppServerUrl(
|
|
3207
|
+
resolvedConfig.appServerUrl,
|
|
3208
|
+
effectivePort ?? void 0
|
|
3209
|
+
);
|
|
3210
|
+
logHeader(`@hua-labs/tap bridge start ${instanceId}`);
|
|
3211
|
+
log(`Bridge script: ${bridgeScript}`);
|
|
3212
|
+
log(`Bridge mode: ${mode}`);
|
|
3213
|
+
log(`Runtime cmd: ${runtimeCommand}`);
|
|
3214
|
+
log(`App server: ${appServerUrl}`);
|
|
3215
|
+
if (effectivePort != null) log(`Port: ${effectivePort}`);
|
|
3216
|
+
if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
|
|
3217
|
+
const noAuth = flags["no-auth"] === true;
|
|
3218
|
+
if (!manageAppServer && instance.runtime === "codex") {
|
|
3219
|
+
log("Auto server: disabled (--no-server)");
|
|
3220
|
+
}
|
|
3221
|
+
if (noAuth && manageAppServer) {
|
|
3222
|
+
log("Auth gateway: disabled (--no-auth)");
|
|
3223
|
+
}
|
|
3224
|
+
const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
|
|
3225
|
+
if (willBeHeadless) {
|
|
3226
|
+
const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
|
|
3227
|
+
log(`Headless: ${role}`);
|
|
3228
|
+
}
|
|
3229
|
+
try {
|
|
3230
|
+
if (!manageAppServer && instance.runtime === "codex") {
|
|
3231
|
+
log("Checking app-server health...");
|
|
3232
|
+
const healthy = await checkAppServerHealth(appServerUrl);
|
|
3233
|
+
if (healthy) {
|
|
3234
|
+
logSuccess("App server reachable");
|
|
3235
|
+
} else {
|
|
3236
|
+
logError(`App server not reachable at ${appServerUrl}`);
|
|
3237
|
+
return {
|
|
3238
|
+
ok: false,
|
|
3239
|
+
command: "bridge",
|
|
3240
|
+
instanceId,
|
|
3241
|
+
runtime: instance.runtime,
|
|
3242
|
+
code: "TAP_BRIDGE_START_FAILED",
|
|
3243
|
+
message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
|
|
3244
|
+
warnings: [],
|
|
3245
|
+
data: {}
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
const busyModeRaw = flags["busy-mode"];
|
|
3250
|
+
if (busyModeRaw !== void 0 && busyModeRaw !== "steer" && busyModeRaw !== "wait") {
|
|
3251
|
+
return {
|
|
3252
|
+
ok: false,
|
|
3253
|
+
command: "bridge",
|
|
3254
|
+
instanceId,
|
|
3255
|
+
runtime: instance.runtime,
|
|
3256
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
3257
|
+
message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
|
|
3258
|
+
warnings: [],
|
|
3259
|
+
data: {}
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
const busyMode = busyModeRaw;
|
|
3263
|
+
const pollSeconds = typeof flags["poll-seconds"] === "string" ? parseInt(flags["poll-seconds"], 10) : void 0;
|
|
3264
|
+
const reconnectSeconds = typeof flags["reconnect-seconds"] === "string" ? parseInt(flags["reconnect-seconds"], 10) : void 0;
|
|
3265
|
+
const messageLookbackMinutes = typeof flags["message-lookback-minutes"] === "string" ? parseInt(flags["message-lookback-minutes"], 10) : void 0;
|
|
3266
|
+
const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
|
|
3267
|
+
const ephemeral = flags["ephemeral"] === true;
|
|
3268
|
+
const processExistingMessages = flags["process-existing-messages"] === true;
|
|
3269
|
+
const headlessFlag = flags["headless"] === true;
|
|
3270
|
+
const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
|
|
3271
|
+
const validRoles = ["reviewer", "validator", "long-running"];
|
|
3272
|
+
if (roleArg && !validRoles.includes(roleArg)) {
|
|
3273
|
+
return {
|
|
3274
|
+
ok: false,
|
|
3275
|
+
command: "bridge",
|
|
3276
|
+
instanceId,
|
|
3277
|
+
runtime: instance.runtime,
|
|
3278
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
3279
|
+
message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
|
|
3280
|
+
warnings: [],
|
|
3281
|
+
data: {}
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
const headless = headlessFlag ? {
|
|
3285
|
+
enabled: true,
|
|
3286
|
+
role: roleArg ?? "reviewer",
|
|
3287
|
+
maxRounds: 5,
|
|
3288
|
+
qualitySeverityFloor: "high"
|
|
3289
|
+
} : instance.headless;
|
|
3290
|
+
const bridge = await startBridge({
|
|
3291
|
+
instanceId,
|
|
3292
|
+
runtime: instance.runtime,
|
|
3293
|
+
stateDir: ctx.stateDir,
|
|
3294
|
+
commsDir: ctx.commsDir,
|
|
3295
|
+
bridgeScript,
|
|
3296
|
+
platform: ctx.platform,
|
|
3297
|
+
agentName: resolvedAgentName,
|
|
3298
|
+
runtimeCommand,
|
|
3299
|
+
appServerUrl,
|
|
3300
|
+
repoRoot,
|
|
3301
|
+
port: effectivePort ?? void 0,
|
|
3302
|
+
manageAppServer,
|
|
3303
|
+
noAuth,
|
|
3304
|
+
headless,
|
|
3305
|
+
busyMode,
|
|
3306
|
+
pollSeconds,
|
|
3307
|
+
reconnectSeconds,
|
|
3308
|
+
messageLookbackMinutes,
|
|
3309
|
+
threadId,
|
|
3310
|
+
ephemeral,
|
|
3311
|
+
processExistingMessages
|
|
3312
|
+
});
|
|
3313
|
+
logSuccess(`Bridge started (PID: ${bridge.pid})`);
|
|
3314
|
+
log(`Log: ${path13.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
|
|
3315
|
+
if (bridge.appServer) {
|
|
3316
|
+
log(`App server: ${formatAppServerState(bridge.appServer)}`);
|
|
3317
|
+
if (bridge.appServer.logPath) {
|
|
3318
|
+
log(`Server log: ${bridge.appServer.logPath}`);
|
|
3319
|
+
}
|
|
3320
|
+
if (bridge.appServer.auth) {
|
|
3321
|
+
log(
|
|
3322
|
+
`Protected: ${redactProtectedUrl(bridge.appServer.auth.protectedUrl)}`
|
|
3323
|
+
);
|
|
3324
|
+
if (bridge.appServer.auth.gatewayLogPath) {
|
|
3325
|
+
log(`Gateway log: ${bridge.appServer.auth.gatewayLogPath}`);
|
|
3326
|
+
}
|
|
3327
|
+
log(`TUI connect: ${bridge.appServer.auth.upstreamUrl}`);
|
|
3328
|
+
}
|
|
3329
|
+
if (bridge.appServer.managed && !bridge.appServer.auth) {
|
|
3330
|
+
log(`TUI connect: ${bridge.appServer.url}`);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
const updated = { ...instance, bridge, manageAppServer, noAuth };
|
|
3334
|
+
const newState = updateInstanceState(state, instanceId, updated);
|
|
3335
|
+
saveState(repoRoot, newState);
|
|
3336
|
+
return {
|
|
3337
|
+
ok: true,
|
|
3338
|
+
command: "bridge",
|
|
3339
|
+
instanceId,
|
|
3340
|
+
runtime: instance.runtime,
|
|
3341
|
+
code: "TAP_BRIDGE_START_OK",
|
|
3342
|
+
message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
|
|
3343
|
+
warnings: [],
|
|
3344
|
+
data: { pid: bridge.pid, appServer: bridge.appServer ?? null }
|
|
3345
|
+
};
|
|
3346
|
+
} catch (err) {
|
|
3347
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3348
|
+
logError(msg);
|
|
3349
|
+
return {
|
|
3350
|
+
ok: false,
|
|
3351
|
+
command: "bridge",
|
|
3352
|
+
instanceId,
|
|
3353
|
+
runtime: instance.runtime,
|
|
3354
|
+
code: "TAP_BRIDGE_START_FAILED",
|
|
3355
|
+
message: msg,
|
|
3356
|
+
warnings: [],
|
|
3357
|
+
data: {}
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
async function bridgeStartAll(flags = {}) {
|
|
3362
|
+
const repoRoot = findRepoRoot();
|
|
3363
|
+
const state = loadState(repoRoot);
|
|
3364
|
+
if (!state) {
|
|
3365
|
+
return {
|
|
3366
|
+
ok: false,
|
|
3367
|
+
command: "bridge",
|
|
3368
|
+
code: "TAP_NOT_INITIALIZED",
|
|
3369
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
3370
|
+
warnings: [],
|
|
3371
|
+
data: {}
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
const instanceIds = Object.keys(state.instances);
|
|
3375
|
+
const appServerInstances = instanceIds.filter((id) => {
|
|
3376
|
+
const inst = state.instances[id];
|
|
3377
|
+
if (!inst?.installed) return false;
|
|
3378
|
+
const adapter = getAdapter(inst.runtime);
|
|
3379
|
+
return adapter.bridgeMode() === "app-server";
|
|
3380
|
+
});
|
|
3381
|
+
if (appServerInstances.length === 0) {
|
|
3382
|
+
return {
|
|
3383
|
+
ok: true,
|
|
3384
|
+
command: "bridge",
|
|
3385
|
+
code: "TAP_NO_OP",
|
|
3386
|
+
message: "No app-server instances found to start.",
|
|
3387
|
+
warnings: [],
|
|
3388
|
+
data: {}
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
logHeader("@hua-labs/tap bridge start --all");
|
|
3392
|
+
log(
|
|
3393
|
+
`Found ${appServerInstances.length} app-server instance(s): ${appServerInstances.join(", ")}`
|
|
3394
|
+
);
|
|
3395
|
+
log("");
|
|
3396
|
+
const started = [];
|
|
3397
|
+
const failed = [];
|
|
3398
|
+
const warnings = [];
|
|
3399
|
+
for (const instanceId of appServerInstances) {
|
|
3400
|
+
const inst = state.instances[instanceId];
|
|
3401
|
+
const storedName = inst?.agentName ?? void 0;
|
|
3402
|
+
if (!storedName) {
|
|
3403
|
+
const msg = `${instanceId}: skipped \u2014 no stored agent-name. Set it first: tap bridge start ${instanceId} --agent-name <name>`;
|
|
3404
|
+
log(msg);
|
|
3405
|
+
warnings.push(msg);
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
log(`Starting ${instanceId} (agent: ${storedName})...`);
|
|
3409
|
+
const result = await bridgeStart(instanceId, storedName, flags);
|
|
3410
|
+
if (result.ok) {
|
|
3411
|
+
started.push(instanceId);
|
|
3412
|
+
logSuccess(`${instanceId} started`);
|
|
3413
|
+
} else {
|
|
3414
|
+
failed.push(instanceId);
|
|
3415
|
+
logError(`${instanceId}: ${result.message}`);
|
|
3416
|
+
}
|
|
3417
|
+
log("");
|
|
3418
|
+
}
|
|
3419
|
+
const message = started.length > 0 ? `Started ${started.length}/${appServerInstances.length} bridge(s): ${started.join(", ")}` + (failed.length > 0 ? `. Failed: ${failed.join(", ")}` : "") : `No bridges started. Failed: ${failed.join(", ")}`;
|
|
3420
|
+
return {
|
|
3421
|
+
ok: failed.length === 0 && started.length > 0,
|
|
3422
|
+
command: "bridge",
|
|
3423
|
+
code: started.length > 0 ? "TAP_BRIDGE_START_OK" : "TAP_BRIDGE_START_FAILED",
|
|
3424
|
+
message,
|
|
3425
|
+
warnings,
|
|
3426
|
+
data: { started, failed }
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
async function bridgeStopOne(identifier) {
|
|
3430
|
+
const repoRoot = findRepoRoot();
|
|
3431
|
+
const state = loadState(repoRoot);
|
|
3432
|
+
if (!state) {
|
|
3433
|
+
return {
|
|
3434
|
+
ok: false,
|
|
3435
|
+
command: "bridge",
|
|
3436
|
+
code: "TAP_NOT_INITIALIZED",
|
|
3437
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
3438
|
+
warnings: [],
|
|
3439
|
+
data: {}
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
const resolved = resolveInstanceId(identifier, state);
|
|
3443
|
+
if (!resolved.ok) {
|
|
3444
|
+
return {
|
|
3445
|
+
ok: false,
|
|
3446
|
+
command: "bridge",
|
|
3447
|
+
code: resolved.code,
|
|
3448
|
+
message: resolved.message,
|
|
3449
|
+
warnings: [],
|
|
3450
|
+
data: {}
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
const instanceId = resolved.instanceId;
|
|
3454
|
+
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
3455
|
+
const instance = state.instances[instanceId];
|
|
3456
|
+
const bridgeState = loadCurrentBridgeState(
|
|
3457
|
+
ctx.stateDir,
|
|
3458
|
+
instanceId,
|
|
3459
|
+
instance?.bridge
|
|
3460
|
+
);
|
|
3461
|
+
const appServer = bridgeState?.appServer ?? null;
|
|
3462
|
+
logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
|
|
3463
|
+
const stopped = await stopBridge({
|
|
3464
|
+
instanceId,
|
|
3465
|
+
stateDir: ctx.stateDir,
|
|
3466
|
+
platform: ctx.platform
|
|
3467
|
+
});
|
|
3468
|
+
let appServerStopped = false;
|
|
3469
|
+
let appServerTransferredTo = null;
|
|
3470
|
+
if (stopped) {
|
|
3471
|
+
logSuccess(`Bridge for ${instanceId} stopped`);
|
|
3472
|
+
} else {
|
|
3473
|
+
log(`No running bridge for ${instanceId}`);
|
|
3474
|
+
}
|
|
3475
|
+
if (appServer?.managed) {
|
|
3476
|
+
const sharedUsers = getSharedAppServerUsers(
|
|
3477
|
+
state,
|
|
3478
|
+
ctx.stateDir,
|
|
3479
|
+
instanceId,
|
|
3480
|
+
appServer.url
|
|
3481
|
+
);
|
|
3482
|
+
if (sharedUsers.length > 0) {
|
|
3483
|
+
const recipient = sharedUsers[0];
|
|
3484
|
+
if (transferManagedAppServerOwnership(
|
|
3485
|
+
state,
|
|
3486
|
+
ctx.stateDir,
|
|
3487
|
+
recipient,
|
|
3488
|
+
appServer
|
|
3489
|
+
)) {
|
|
3490
|
+
appServerTransferredTo = recipient;
|
|
3491
|
+
log(`Managed app-server ownership moved to ${recipient}`);
|
|
3492
|
+
} else {
|
|
3493
|
+
log(
|
|
3494
|
+
`Managed app-server left running at ${appServer.url} because ownership transfer failed`
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
} else {
|
|
3498
|
+
appServerStopped = await stopManagedAppServer(appServer, ctx.platform);
|
|
3499
|
+
if (appServerStopped) {
|
|
3500
|
+
const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID: ${appServer.auth.gatewayPid}` : "";
|
|
3501
|
+
logSuccess(
|
|
3502
|
+
`Managed app-server stopped (PID: ${appServer.pid ?? "-"}${gatewayNote})`
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
if (instance) {
|
|
3508
|
+
const updated = { ...instance, bridge: null };
|
|
3509
|
+
const newState = updateInstanceState(state, instanceId, updated);
|
|
3510
|
+
saveState(repoRoot, newState);
|
|
3511
|
+
}
|
|
3512
|
+
if (stopped) {
|
|
3513
|
+
return {
|
|
3514
|
+
ok: true,
|
|
3515
|
+
command: "bridge",
|
|
3516
|
+
instanceId,
|
|
3517
|
+
code: "TAP_BRIDGE_STOP_OK",
|
|
3518
|
+
message: `Bridge for ${instanceId} stopped`,
|
|
3519
|
+
warnings: [],
|
|
3520
|
+
data: {
|
|
3521
|
+
appServerStopped,
|
|
3522
|
+
appServerTransferredTo
|
|
3523
|
+
}
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
return {
|
|
3527
|
+
ok: true,
|
|
3528
|
+
command: "bridge",
|
|
3529
|
+
instanceId,
|
|
3530
|
+
code: "TAP_BRIDGE_NOT_RUNNING",
|
|
3531
|
+
message: `No running bridge for ${instanceId}`,
|
|
3532
|
+
warnings: [],
|
|
3533
|
+
data: {
|
|
3534
|
+
appServerStopped,
|
|
3535
|
+
appServerTransferredTo
|
|
3536
|
+
}
|
|
3537
|
+
};
|
|
3538
|
+
}
|
|
3539
|
+
async function bridgeStopAll() {
|
|
3540
|
+
const repoRoot = findRepoRoot();
|
|
3541
|
+
const state = loadState(repoRoot);
|
|
3542
|
+
if (!state) {
|
|
3543
|
+
return {
|
|
3544
|
+
ok: false,
|
|
3545
|
+
command: "bridge",
|
|
3546
|
+
code: "TAP_NOT_INITIALIZED",
|
|
3547
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
3548
|
+
warnings: [],
|
|
3549
|
+
data: {}
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
3553
|
+
const instanceIds = Object.keys(state.instances);
|
|
3554
|
+
const stopped = [];
|
|
3555
|
+
const managedAppServers = /* @__PURE__ */ new Map();
|
|
3556
|
+
logHeader("@hua-labs/tap bridge stop (all)");
|
|
3557
|
+
let stateChanged = false;
|
|
3558
|
+
for (const instanceId of instanceIds) {
|
|
3559
|
+
const bridgeState = loadCurrentBridgeState(
|
|
3560
|
+
ctx.stateDir,
|
|
3561
|
+
instanceId,
|
|
3562
|
+
state.instances[instanceId]?.bridge
|
|
3563
|
+
);
|
|
3564
|
+
const appServer = bridgeState?.appServer;
|
|
3565
|
+
if (appServer?.managed && appServer.pid != null) {
|
|
3566
|
+
managedAppServers.set(
|
|
3567
|
+
`${appServer.url}:${appServer.pid}:${appServer.auth?.gatewayPid ?? "-"}`,
|
|
3568
|
+
appServer
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
const didStop = await stopBridge({
|
|
3572
|
+
instanceId,
|
|
3573
|
+
stateDir: ctx.stateDir,
|
|
3574
|
+
platform: ctx.platform
|
|
3575
|
+
});
|
|
3576
|
+
if (didStop) {
|
|
3577
|
+
logSuccess(`Stopped bridge for ${instanceId}`);
|
|
3578
|
+
stopped.push(instanceId);
|
|
3579
|
+
}
|
|
3580
|
+
const instance = state.instances[instanceId];
|
|
3581
|
+
if (instance?.bridge) {
|
|
3582
|
+
state.instances[instanceId] = { ...instance, bridge: null };
|
|
3583
|
+
stateChanged = true;
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
const stoppedAppServers = [];
|
|
3587
|
+
for (const appServer of managedAppServers.values()) {
|
|
3588
|
+
if (await stopManagedAppServer(appServer, ctx.platform)) {
|
|
3589
|
+
stoppedAppServers.push(appServer.pid);
|
|
3590
|
+
const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID ${appServer.auth.gatewayPid}` : "";
|
|
3591
|
+
logSuccess(
|
|
3592
|
+
`Stopped app-server PID ${appServer.pid} (${appServer.url}${gatewayNote})`
|
|
3593
|
+
);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
if (stateChanged) {
|
|
3597
|
+
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3598
|
+
saveState(repoRoot, state);
|
|
3599
|
+
}
|
|
3600
|
+
const message = stopped.length > 0 ? `Stopped ${stopped.length} bridge(s): ${stopped.join(", ")}` : "No running bridges found";
|
|
3601
|
+
log(message);
|
|
3602
|
+
return {
|
|
3603
|
+
ok: true,
|
|
3604
|
+
command: "bridge",
|
|
3605
|
+
code: stopped.length > 0 ? "TAP_BRIDGE_STOP_OK" : "TAP_BRIDGE_NOT_RUNNING",
|
|
3606
|
+
message,
|
|
3607
|
+
warnings: [],
|
|
3608
|
+
data: { stopped, stoppedAppServers }
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
function bridgeStatusAll() {
|
|
3612
|
+
const repoRoot = findRepoRoot();
|
|
3613
|
+
const state = loadState(repoRoot);
|
|
3614
|
+
if (!state) {
|
|
3615
|
+
return {
|
|
3616
|
+
ok: false,
|
|
3617
|
+
command: "bridge",
|
|
3618
|
+
code: "TAP_NOT_INITIALIZED",
|
|
3619
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
3620
|
+
warnings: [],
|
|
3621
|
+
data: {}
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
3625
|
+
const stateDir = resolvedCfg.stateDir;
|
|
3626
|
+
const instanceIds = Object.keys(state.instances);
|
|
3627
|
+
const bridges = {};
|
|
3628
|
+
logHeader("@hua-labs/tap bridge status");
|
|
3629
|
+
log(
|
|
3630
|
+
`${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(10)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Last Heartbeat"}`
|
|
3631
|
+
);
|
|
3632
|
+
log(
|
|
3633
|
+
`${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(10)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(20)}`
|
|
3634
|
+
);
|
|
3635
|
+
for (const instanceId of instanceIds) {
|
|
3636
|
+
const inst = state.instances[instanceId];
|
|
3637
|
+
if (!inst?.installed) continue;
|
|
3638
|
+
if (inst.bridgeMode !== "app-server") {
|
|
3639
|
+
log(
|
|
3640
|
+
`${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${"n/a".padEnd(10)} ${"-".padEnd(8)} ${"-".padEnd(6)} ${inst.bridgeMode} mode`
|
|
3641
|
+
);
|
|
3642
|
+
bridges[instanceId] = {
|
|
3643
|
+
status: "n/a",
|
|
3644
|
+
runtime: inst.runtime,
|
|
3645
|
+
pid: null,
|
|
3646
|
+
port: inst.port,
|
|
3647
|
+
lastHeartbeat: null,
|
|
3648
|
+
appServer: null
|
|
3649
|
+
};
|
|
3650
|
+
continue;
|
|
3651
|
+
}
|
|
3652
|
+
const status = getBridgeStatus(stateDir, instanceId);
|
|
3653
|
+
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
3654
|
+
const age = getHeartbeatAge(stateDir, instanceId);
|
|
3655
|
+
const pid = bridgeState?.pid ?? null;
|
|
3656
|
+
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
3657
|
+
const pidStr = pid ? String(pid) : "-";
|
|
3658
|
+
const portStr = inst.port ? String(inst.port) : "-";
|
|
3659
|
+
const ageStr = age !== null ? formatAge(age) : "-";
|
|
3660
|
+
const statusColor = status === "running" ? "running" : status === "stale" ? "stale!" : "stopped";
|
|
3661
|
+
log(
|
|
3662
|
+
`${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${statusColor.padEnd(10)} ${pidStr.padEnd(8)} ${portStr.padEnd(6)} ${ageStr}`
|
|
3663
|
+
);
|
|
3664
|
+
if (bridgeState?.appServer) {
|
|
3665
|
+
log(` App server: ${formatAppServerState(bridgeState.appServer)}`);
|
|
3666
|
+
if (bridgeState.appServer.logPath) {
|
|
3667
|
+
log(` Server log: ${bridgeState.appServer.logPath}`);
|
|
3668
|
+
}
|
|
3669
|
+
if (bridgeState.appServer.auth) {
|
|
3670
|
+
log(
|
|
3671
|
+
` Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
|
|
3672
|
+
);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
bridges[instanceId] = {
|
|
3676
|
+
status,
|
|
3677
|
+
runtime: inst.runtime,
|
|
3678
|
+
pid,
|
|
3679
|
+
port: inst.port,
|
|
3680
|
+
lastHeartbeat: heartbeat,
|
|
3681
|
+
appServer: bridgeState?.appServer ?? null
|
|
3682
|
+
};
|
|
3683
|
+
}
|
|
3684
|
+
if (instanceIds.length === 0) {
|
|
3685
|
+
log("No instances installed.");
|
|
3686
|
+
}
|
|
3687
|
+
log("");
|
|
3688
|
+
return {
|
|
3689
|
+
ok: true,
|
|
3690
|
+
command: "bridge",
|
|
3691
|
+
code: "TAP_BRIDGE_STATUS_OK",
|
|
3692
|
+
message: `${instanceIds.length} instance(s) checked`,
|
|
3693
|
+
warnings: [],
|
|
3694
|
+
data: { bridges }
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
function bridgeStatusOne(identifier) {
|
|
3698
|
+
const repoRoot = findRepoRoot();
|
|
3699
|
+
const state = loadState(repoRoot);
|
|
3700
|
+
if (!state) {
|
|
3701
|
+
return {
|
|
3702
|
+
ok: false,
|
|
3703
|
+
command: "bridge",
|
|
3704
|
+
code: "TAP_NOT_INITIALIZED",
|
|
3705
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
3706
|
+
warnings: [],
|
|
3707
|
+
data: {}
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
const resolved = resolveInstanceId(identifier, state);
|
|
3711
|
+
if (!resolved.ok) {
|
|
3712
|
+
return {
|
|
3713
|
+
ok: false,
|
|
3714
|
+
command: "bridge",
|
|
3715
|
+
code: resolved.code,
|
|
3716
|
+
message: resolved.message,
|
|
3717
|
+
warnings: [],
|
|
3718
|
+
data: {}
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
const instanceId = resolved.instanceId;
|
|
3722
|
+
const inst = state.instances[instanceId];
|
|
3723
|
+
if (!inst?.installed) {
|
|
3724
|
+
return {
|
|
3725
|
+
ok: false,
|
|
3726
|
+
command: "bridge",
|
|
3727
|
+
instanceId,
|
|
3728
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
3729
|
+
message: `${instanceId} is not installed.`,
|
|
3730
|
+
warnings: [],
|
|
3731
|
+
data: {}
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3734
|
+
logHeader(`@hua-labs/tap bridge status ${instanceId}`);
|
|
3735
|
+
log(`Instance: ${instanceId}`);
|
|
3736
|
+
log(`Runtime: ${inst.runtime}`);
|
|
3737
|
+
log(`Bridge mode: ${inst.bridgeMode}`);
|
|
3738
|
+
if (inst.port) log(`Port: ${inst.port}`);
|
|
3739
|
+
if (inst.bridgeMode !== "app-server") {
|
|
3740
|
+
log(`Status: n/a (${inst.bridgeMode} mode)`);
|
|
3741
|
+
log("");
|
|
3742
|
+
return {
|
|
3743
|
+
ok: true,
|
|
3744
|
+
command: "bridge",
|
|
3745
|
+
instanceId,
|
|
3746
|
+
runtime: inst.runtime,
|
|
3747
|
+
code: "TAP_BRIDGE_STATUS_OK",
|
|
3748
|
+
message: `${instanceId} bridge: n/a (${inst.bridgeMode} mode)`,
|
|
3749
|
+
warnings: [],
|
|
3750
|
+
data: {
|
|
3751
|
+
status: "n/a",
|
|
3752
|
+
bridgeMode: inst.bridgeMode,
|
|
3753
|
+
pid: null,
|
|
3754
|
+
port: inst.port,
|
|
3755
|
+
lastHeartbeat: null,
|
|
3756
|
+
appServer: null
|
|
3757
|
+
}
|
|
3758
|
+
};
|
|
3759
|
+
}
|
|
3760
|
+
const { config: resolvedCfg2 } = resolveConfig({}, repoRoot);
|
|
3761
|
+
const stateDir = resolvedCfg2.stateDir;
|
|
3762
|
+
const status = getBridgeStatus(stateDir, instanceId);
|
|
3763
|
+
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
3764
|
+
const age = getHeartbeatAge(stateDir, instanceId);
|
|
3765
|
+
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
3766
|
+
log(`Status: ${status}`);
|
|
3767
|
+
if (bridgeState) {
|
|
3768
|
+
log(`PID: ${bridgeState.pid}`);
|
|
3769
|
+
log(
|
|
3770
|
+
`Heartbeat: ${heartbeat ?? "-"}${age !== null ? ` (${formatAge(age)})` : ""}`
|
|
3771
|
+
);
|
|
3772
|
+
log(
|
|
3773
|
+
`Log: ${path13.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
|
|
3774
|
+
);
|
|
3775
|
+
if (bridgeState.appServer) {
|
|
3776
|
+
log(`App server: ${bridgeState.appServer.url}`);
|
|
3777
|
+
log(`Server PID: ${bridgeState.appServer.pid ?? "-"}`);
|
|
3778
|
+
log(
|
|
3779
|
+
`Server mode: ${bridgeState.appServer.managed ? "managed" : "external"}`
|
|
3780
|
+
);
|
|
3781
|
+
log(
|
|
3782
|
+
`Health: ${bridgeState.appServer.healthy ? "healthy" : "unhealthy"}`
|
|
3783
|
+
);
|
|
3784
|
+
log(`Checked: ${bridgeState.appServer.lastCheckedAt}`);
|
|
3785
|
+
if (bridgeState.appServer.logPath) {
|
|
3786
|
+
log(`Server log: ${bridgeState.appServer.logPath}`);
|
|
3787
|
+
}
|
|
3788
|
+
if (bridgeState.appServer.auth) {
|
|
3789
|
+
log(`Auth: ${bridgeState.appServer.auth.mode}`);
|
|
3790
|
+
log(
|
|
3791
|
+
`Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
|
|
3792
|
+
);
|
|
3793
|
+
log(`Upstream: ${bridgeState.appServer.auth.upstreamUrl}`);
|
|
3794
|
+
log(`TUI connect: ${bridgeState.appServer.auth.upstreamUrl}`);
|
|
3795
|
+
log(`Gateway PID: ${bridgeState.appServer.auth.gatewayPid ?? "-"}`);
|
|
3796
|
+
if (bridgeState.appServer.auth.gatewayLogPath) {
|
|
3797
|
+
log(`Gateway log: ${bridgeState.appServer.auth.gatewayLogPath}`);
|
|
3798
|
+
}
|
|
3799
|
+
} else if (bridgeState.appServer.managed) {
|
|
3800
|
+
log(`Auth: none (--no-auth)`);
|
|
3801
|
+
log(`TUI connect: ${bridgeState.appServer.url}`);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
log("");
|
|
3806
|
+
return {
|
|
3807
|
+
ok: true,
|
|
3808
|
+
command: "bridge",
|
|
3809
|
+
instanceId,
|
|
3810
|
+
runtime: inst.runtime,
|
|
3811
|
+
code: "TAP_BRIDGE_STATUS_OK",
|
|
3812
|
+
message: `${instanceId} bridge: ${status}`,
|
|
3813
|
+
warnings: [],
|
|
3814
|
+
data: {
|
|
3815
|
+
status,
|
|
3816
|
+
bridgeMode: inst.bridgeMode,
|
|
3817
|
+
pid: bridgeState?.pid ?? null,
|
|
3818
|
+
port: inst.port,
|
|
3819
|
+
lastHeartbeat: heartbeat,
|
|
3820
|
+
appServer: bridgeState?.appServer ?? null
|
|
3821
|
+
}
|
|
3822
|
+
};
|
|
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
|
+
}
|
|
3939
|
+
async function bridgeCommand(args) {
|
|
3940
|
+
const { positional, flags } = parseArgs(args);
|
|
3941
|
+
const subcommand = positional[0];
|
|
3942
|
+
const identifierArg = positional[1];
|
|
3943
|
+
const agentName = typeof flags["agent-name"] === "string" ? flags["agent-name"] : void 0;
|
|
3944
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
3945
|
+
log(BRIDGE_HELP);
|
|
3946
|
+
return {
|
|
3947
|
+
ok: true,
|
|
3948
|
+
command: "bridge",
|
|
3949
|
+
code: "TAP_NO_OP",
|
|
3950
|
+
message: BRIDGE_HELP,
|
|
3951
|
+
warnings: [],
|
|
3952
|
+
data: {}
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
switch (subcommand) {
|
|
3956
|
+
case "start": {
|
|
3957
|
+
const wantsAll = flags["all"] === true || identifierArg === "--all";
|
|
3958
|
+
const hasInstance = identifierArg && identifierArg !== "--all";
|
|
3959
|
+
if (wantsAll && hasInstance) {
|
|
3960
|
+
return {
|
|
3961
|
+
ok: false,
|
|
3962
|
+
command: "bridge",
|
|
3963
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
3964
|
+
message: `Cannot combine <instance> with --all. Use either:
|
|
3965
|
+
tap bridge start ${identifierArg}
|
|
3966
|
+
tap bridge start --all`,
|
|
3967
|
+
warnings: [],
|
|
3968
|
+
data: {}
|
|
3969
|
+
};
|
|
3970
|
+
}
|
|
3971
|
+
if (wantsAll) {
|
|
3972
|
+
return bridgeStartAll(flags);
|
|
3973
|
+
}
|
|
3974
|
+
if (!identifierArg) {
|
|
3975
|
+
return {
|
|
3976
|
+
ok: false,
|
|
3977
|
+
command: "bridge",
|
|
3978
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
3979
|
+
message: "Missing instance. Usage: npx @hua-labs/tap bridge start <instance> or --all",
|
|
3980
|
+
warnings: [],
|
|
3981
|
+
data: {}
|
|
3982
|
+
};
|
|
3983
|
+
}
|
|
3984
|
+
return bridgeStart(identifierArg, agentName, flags);
|
|
3985
|
+
}
|
|
3986
|
+
case "stop": {
|
|
3987
|
+
if (!identifierArg) {
|
|
3988
|
+
return bridgeStopAll();
|
|
3989
|
+
}
|
|
3990
|
+
return bridgeStopOne(identifierArg);
|
|
3991
|
+
}
|
|
3992
|
+
case "status": {
|
|
3993
|
+
if (identifierArg) {
|
|
3994
|
+
return bridgeStatusOne(identifierArg);
|
|
3995
|
+
}
|
|
3996
|
+
return bridgeStatusAll();
|
|
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
|
+
}
|
|
4011
|
+
default:
|
|
4012
|
+
return {
|
|
4013
|
+
ok: false,
|
|
4014
|
+
command: "bridge",
|
|
4015
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4016
|
+
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
|
|
4017
|
+
warnings: [],
|
|
4018
|
+
data: {}
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
var BRIDGE_HELP;
|
|
4023
|
+
var init_bridge2 = __esm({
|
|
4024
|
+
"src/commands/bridge.ts"() {
|
|
4025
|
+
"use strict";
|
|
4026
|
+
init_state();
|
|
4027
|
+
init_bridge();
|
|
4028
|
+
init_config();
|
|
4029
|
+
init_adapters();
|
|
4030
|
+
init_utils();
|
|
4031
|
+
BRIDGE_HELP = `
|
|
4032
|
+
Usage:
|
|
4033
|
+
tap-comms bridge <subcommand> [instance] [options]
|
|
4034
|
+
|
|
4035
|
+
Subcommands:
|
|
4036
|
+
start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
|
|
4037
|
+
start --all Start all registered app-server instances
|
|
4038
|
+
stop <instance> Stop bridge for an instance
|
|
4039
|
+
stop Stop all running bridges
|
|
4040
|
+
status Show bridge status for all instances
|
|
4041
|
+
status <instance> Show bridge status for a specific instance
|
|
4042
|
+
|
|
4043
|
+
Options:
|
|
4044
|
+
--agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
|
|
4045
|
+
Saved to state \u2014 only needed on first start
|
|
4046
|
+
--all Start all registered app-server instances
|
|
4047
|
+
--busy-mode <steer|wait> How to handle active turns (default: steer)
|
|
4048
|
+
--poll-seconds <n> Inbox poll interval (default: 5)
|
|
4049
|
+
--reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
|
|
4050
|
+
--message-lookback-minutes <n> Process messages from last N minutes (default: 10)
|
|
4051
|
+
--thread-id <id> Resume specific thread
|
|
4052
|
+
--ephemeral Use ephemeral thread (no persistence)
|
|
4053
|
+
--process-existing-messages Process all existing inbox messages
|
|
4054
|
+
--no-server Skip app-server auto-start and connect only
|
|
4055
|
+
--no-auth Skip auth gateway (app-server listens directly, localhost only)
|
|
4056
|
+
|
|
4057
|
+
Port Assignment:
|
|
4058
|
+
Ports are auto-assigned from 4501 on first bridge start if not set via --port
|
|
4059
|
+
during 'tap add'. Auto-assigned ports are saved to state for future starts.
|
|
4060
|
+
|
|
4061
|
+
Examples:
|
|
4062
|
+
npx @hua-labs/tap bridge start codex --agent-name myAgent
|
|
4063
|
+
npx @hua-labs/tap bridge start --all
|
|
4064
|
+
npx @hua-labs/tap bridge start codex --agent-name myAgent --no-server
|
|
4065
|
+
npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer --busy-mode steer
|
|
4066
|
+
npx @hua-labs/tap bridge stop codex
|
|
4067
|
+
npx @hua-labs/tap bridge stop
|
|
4068
|
+
npx @hua-labs/tap bridge status
|
|
4069
|
+
`.trim();
|
|
4070
|
+
}
|
|
4071
|
+
});
|
|
4072
|
+
|
|
4073
|
+
// src/commands/up.ts
|
|
4074
|
+
var up_exports = {};
|
|
4075
|
+
__export(up_exports, {
|
|
4076
|
+
upCommand: () => upCommand
|
|
4077
|
+
});
|
|
4078
|
+
async function upCommand(args) {
|
|
4079
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4080
|
+
log(UP_HELP);
|
|
4081
|
+
return {
|
|
4082
|
+
ok: true,
|
|
4083
|
+
command: "up",
|
|
4084
|
+
code: "TAP_NO_OP",
|
|
4085
|
+
message: UP_HELP,
|
|
4086
|
+
warnings: [],
|
|
4087
|
+
data: {}
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
const repoRoot = findRepoRoot();
|
|
4091
|
+
const result = await bridgeCommand(["start", "--all", ...args]);
|
|
4092
|
+
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
4093
|
+
const activeBridges = snapshot.bridges.filter(
|
|
4094
|
+
(bridge) => bridge.status === "running"
|
|
4095
|
+
).length;
|
|
4096
|
+
if (!result.ok) {
|
|
4097
|
+
return {
|
|
4098
|
+
...result,
|
|
4099
|
+
command: "up",
|
|
4100
|
+
data: {
|
|
4101
|
+
...result.data,
|
|
4102
|
+
snapshot
|
|
4103
|
+
}
|
|
4104
|
+
};
|
|
4105
|
+
}
|
|
4106
|
+
return {
|
|
4107
|
+
ok: true,
|
|
4108
|
+
command: "up",
|
|
4109
|
+
code: "TAP_UP_OK",
|
|
4110
|
+
message: `tap up: ${activeBridges} bridge(s) running`,
|
|
4111
|
+
warnings: result.warnings,
|
|
4112
|
+
data: {
|
|
4113
|
+
...result.data,
|
|
4114
|
+
snapshot
|
|
4115
|
+
}
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
var UP_HELP;
|
|
4119
|
+
var init_up = __esm({
|
|
4120
|
+
"src/commands/up.ts"() {
|
|
4121
|
+
"use strict";
|
|
4122
|
+
init_bridge2();
|
|
4123
|
+
init_dashboard();
|
|
4124
|
+
init_utils();
|
|
4125
|
+
UP_HELP = `
|
|
4126
|
+
Usage:
|
|
4127
|
+
tap-comms up [bridge-start options]
|
|
4128
|
+
|
|
4129
|
+
Description:
|
|
4130
|
+
Start all registered app-server bridge daemons with one command.
|
|
4131
|
+
This is the orchestration entrypoint for headless/background TAP operation.
|
|
4132
|
+
|
|
4133
|
+
Examples:
|
|
4134
|
+
npx @hua-labs/tap up
|
|
4135
|
+
npx @hua-labs/tap up --no-auth
|
|
4136
|
+
npx @hua-labs/tap up --busy-mode wait
|
|
4137
|
+
`.trim();
|
|
4138
|
+
}
|
|
4139
|
+
});
|
|
4140
|
+
|
|
4141
|
+
// src/commands/down.ts
|
|
4142
|
+
var down_exports = {};
|
|
4143
|
+
__export(down_exports, {
|
|
4144
|
+
downCommand: () => downCommand
|
|
4145
|
+
});
|
|
4146
|
+
async function downCommand(args) {
|
|
4147
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4148
|
+
log(DOWN_HELP);
|
|
4149
|
+
return {
|
|
4150
|
+
ok: true,
|
|
4151
|
+
command: "down",
|
|
4152
|
+
code: "TAP_NO_OP",
|
|
4153
|
+
message: DOWN_HELP,
|
|
4154
|
+
warnings: [],
|
|
4155
|
+
data: {}
|
|
4156
|
+
};
|
|
4157
|
+
}
|
|
4158
|
+
const repoRoot = findRepoRoot();
|
|
4159
|
+
const result = await bridgeCommand(["stop"]);
|
|
4160
|
+
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
4161
|
+
if (!result.ok) {
|
|
4162
|
+
return {
|
|
4163
|
+
...result,
|
|
4164
|
+
command: "down",
|
|
4165
|
+
data: {
|
|
4166
|
+
...result.data,
|
|
4167
|
+
snapshot
|
|
4168
|
+
}
|
|
4169
|
+
};
|
|
4170
|
+
}
|
|
4171
|
+
return {
|
|
4172
|
+
ok: true,
|
|
4173
|
+
command: "down",
|
|
4174
|
+
code: "TAP_DOWN_OK",
|
|
4175
|
+
message: `tap down: ${snapshot.bridges.filter((bridge) => bridge.status === "running").length} bridge(s) still running`,
|
|
4176
|
+
warnings: result.warnings,
|
|
4177
|
+
data: {
|
|
4178
|
+
...result.data,
|
|
4179
|
+
snapshot
|
|
4180
|
+
}
|
|
4181
|
+
};
|
|
4182
|
+
}
|
|
4183
|
+
var DOWN_HELP;
|
|
4184
|
+
var init_down = __esm({
|
|
4185
|
+
"src/commands/down.ts"() {
|
|
4186
|
+
"use strict";
|
|
4187
|
+
init_bridge2();
|
|
4188
|
+
init_dashboard();
|
|
4189
|
+
init_utils();
|
|
4190
|
+
DOWN_HELP = `
|
|
4191
|
+
Usage:
|
|
4192
|
+
tap-comms down
|
|
4193
|
+
|
|
4194
|
+
Description:
|
|
4195
|
+
Stop all running bridge daemons and managed app-servers.
|
|
4196
|
+
|
|
4197
|
+
Examples:
|
|
4198
|
+
npx @hua-labs/tap down
|
|
4199
|
+
`.trim();
|
|
4200
|
+
}
|
|
4201
|
+
});
|
|
4202
|
+
|
|
4203
|
+
// src/index.ts
|
|
4204
|
+
init_state();
|
|
4205
|
+
|
|
4206
|
+
// src/version.ts
|
|
4207
|
+
import * as fs4 from "fs";
|
|
4208
|
+
import * as path4 from "path";
|
|
4209
|
+
import { fileURLToPath } from "url";
|
|
4210
|
+
var FALLBACK_VERSION = "0.0.0";
|
|
4211
|
+
function resolvePackageVersion(metaUrl = import.meta.url) {
|
|
4212
|
+
const moduleDir = path4.dirname(fileURLToPath(metaUrl));
|
|
4213
|
+
const packageJsonPath = path4.join(moduleDir, "..", "package.json");
|
|
4214
|
+
try {
|
|
4215
|
+
const parsed = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
|
|
4216
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
4217
|
+
return parsed.version;
|
|
4218
|
+
}
|
|
4219
|
+
} catch {
|
|
4220
|
+
}
|
|
4221
|
+
return FALLBACK_VERSION;
|
|
4222
|
+
}
|
|
4223
|
+
var version = resolvePackageVersion();
|
|
4224
|
+
|
|
4225
|
+
// src/index.ts
|
|
4226
|
+
init_config();
|
|
4227
|
+
init_bridge();
|
|
4228
|
+
init_dashboard();
|
|
4229
|
+
|
|
4230
|
+
// src/api/state.ts
|
|
4231
|
+
init_dashboard();
|
|
4232
|
+
init_utils();
|
|
4233
|
+
init_config();
|
|
4234
|
+
init_state();
|
|
4235
|
+
import * as fs13 from "fs";
|
|
4236
|
+
import * as path14 from "path";
|
|
4237
|
+
function getDashboardSnapshot(options) {
|
|
4238
|
+
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
4239
|
+
return collectDashboardSnapshot(repoRoot, options?.commsDir);
|
|
4240
|
+
}
|
|
4241
|
+
async function* streamEvents(options) {
|
|
4242
|
+
const intervalMs = options?.intervalMs ?? 2e3;
|
|
4243
|
+
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
4244
|
+
while (!options?.signal?.aborted) {
|
|
4245
|
+
yield collectDashboardSnapshot(repoRoot, options?.commsDir);
|
|
4246
|
+
await new Promise((resolve8) => {
|
|
4247
|
+
const onAbort = () => {
|
|
4248
|
+
clearTimeout(timer);
|
|
4249
|
+
resolve8();
|
|
4250
|
+
};
|
|
4251
|
+
const timer = setTimeout(() => {
|
|
4252
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
4253
|
+
resolve8();
|
|
4254
|
+
}, intervalMs);
|
|
4255
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
4256
|
+
});
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
async function startAgents(options) {
|
|
4260
|
+
const { upCommand: upCommand2 } = await Promise.resolve().then(() => (init_up(), up_exports));
|
|
4261
|
+
const result = await upCommand2(options?.args ?? []);
|
|
4262
|
+
const repoRoot = findRepoRoot();
|
|
4263
|
+
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
4264
|
+
return {
|
|
4265
|
+
ok: result.ok,
|
|
4266
|
+
message: result.message,
|
|
4267
|
+
snapshot,
|
|
4268
|
+
commandResult: result
|
|
4269
|
+
};
|
|
4270
|
+
}
|
|
4271
|
+
async function stopAgents() {
|
|
4272
|
+
const { downCommand: downCommand2 } = await Promise.resolve().then(() => (init_down(), down_exports));
|
|
4273
|
+
const result = await downCommand2([]);
|
|
4274
|
+
const repoRoot = findRepoRoot();
|
|
4275
|
+
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
4276
|
+
return {
|
|
4277
|
+
ok: result.ok,
|
|
4278
|
+
message: result.message,
|
|
4279
|
+
snapshot,
|
|
4280
|
+
commandResult: result
|
|
4281
|
+
};
|
|
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
|
+
}
|
|
4337
|
+
function getConfig(options) {
|
|
4338
|
+
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
4339
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
4340
|
+
return {
|
|
4341
|
+
repoRoot,
|
|
4342
|
+
commsDir: options?.commsDir ?? config.commsDir,
|
|
4343
|
+
stateDir: config.stateDir,
|
|
4344
|
+
appServerUrl: config.appServerUrl
|
|
4345
|
+
};
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
// src/api/http.ts
|
|
4349
|
+
import {
|
|
4350
|
+
createServer as createServer2
|
|
4351
|
+
} from "http";
|
|
4352
|
+
var CORS_HEADERS = {
|
|
4353
|
+
"Access-Control-Allow-Origin": "http://localhost:3000",
|
|
4354
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
4355
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
4356
|
+
};
|
|
4357
|
+
function jsonResponse(res, data, status = 200) {
|
|
777
4358
|
res.writeHead(status, {
|
|
778
4359
|
"Content-Type": "application/json",
|
|
779
4360
|
...CORS_HEADERS
|
|
@@ -808,8 +4389,9 @@ async function handleEvents(req, res, apiOptions) {
|
|
|
808
4389
|
}
|
|
809
4390
|
res.end();
|
|
810
4391
|
}
|
|
811
|
-
function handleHealth(res) {
|
|
812
|
-
|
|
4392
|
+
function handleHealth(res, apiOptions) {
|
|
4393
|
+
const report = getHealthReport(apiOptions);
|
|
4394
|
+
jsonResponse(res, report);
|
|
813
4395
|
}
|
|
814
4396
|
async function startHttpServer(options) {
|
|
815
4397
|
const port = options?.port ?? 4580;
|
|
@@ -827,47 +4409,66 @@ async function startHttpServer(options) {
|
|
|
827
4409
|
res.end();
|
|
828
4410
|
return;
|
|
829
4411
|
}
|
|
830
|
-
if (req.method !== "GET") {
|
|
831
|
-
jsonResponse(res, { error: "Method not allowed" }, 405);
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
4412
|
try {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
4413
|
+
if (req.method === "GET") {
|
|
4414
|
+
switch (pathname) {
|
|
4415
|
+
case "/api/snapshot":
|
|
4416
|
+
handleSnapshot(res, apiOptions);
|
|
4417
|
+
return;
|
|
4418
|
+
case "/api/events":
|
|
4419
|
+
await handleEvents(req, res, apiOptions);
|
|
4420
|
+
return;
|
|
4421
|
+
case "/api/config":
|
|
4422
|
+
handleConfig(res, apiOptions);
|
|
4423
|
+
return;
|
|
4424
|
+
case "/health":
|
|
4425
|
+
handleHealth(res, apiOptions);
|
|
4426
|
+
return;
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
if (req.method === "POST") {
|
|
4430
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
4431
|
+
if (!contentType.includes("application/json")) {
|
|
4432
|
+
jsonResponse(
|
|
4433
|
+
res,
|
|
4434
|
+
{ error: "Content-Type must be application/json" },
|
|
4435
|
+
415
|
|
4436
|
+
);
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4439
|
+
switch (pathname) {
|
|
4440
|
+
case "/api/start":
|
|
4441
|
+
jsonResponse(res, await startAgents());
|
|
4442
|
+
return;
|
|
4443
|
+
case "/api/stop":
|
|
4444
|
+
jsonResponse(res, await stopAgents());
|
|
4445
|
+
return;
|
|
4446
|
+
}
|
|
850
4447
|
}
|
|
4448
|
+
jsonResponse(res, { error: "Not found" }, 404);
|
|
851
4449
|
} catch (err) {
|
|
852
4450
|
const message = err instanceof Error ? err.message : String(err);
|
|
853
4451
|
jsonResponse(res, { error: message }, 500);
|
|
854
4452
|
}
|
|
855
4453
|
}
|
|
856
4454
|
);
|
|
857
|
-
await new Promise((
|
|
4455
|
+
await new Promise((resolve8, reject) => {
|
|
858
4456
|
server.once("error", reject);
|
|
859
4457
|
server.listen(port, host, () => {
|
|
860
4458
|
server.removeListener("error", reject);
|
|
861
|
-
|
|
4459
|
+
resolve8();
|
|
862
4460
|
});
|
|
863
4461
|
});
|
|
864
4462
|
return {
|
|
865
4463
|
port,
|
|
866
|
-
close: () => new Promise((
|
|
867
|
-
server.close((err) => err ? reject(err) :
|
|
4464
|
+
close: () => new Promise((resolve8, reject) => {
|
|
4465
|
+
server.close((err) => err ? reject(err) : resolve8());
|
|
868
4466
|
})
|
|
869
4467
|
};
|
|
870
4468
|
}
|
|
4469
|
+
|
|
4470
|
+
// src/index.ts
|
|
4471
|
+
init_runtime();
|
|
871
4472
|
export {
|
|
872
4473
|
LOCAL_CONFIG_FILE,
|
|
873
4474
|
SHARED_CONFIG_FILE,
|
|
@@ -877,6 +4478,7 @@ export {
|
|
|
877
4478
|
getConfig,
|
|
878
4479
|
getDashboardSnapshot,
|
|
879
4480
|
getFnmBinDir,
|
|
4481
|
+
getHealthReport,
|
|
880
4482
|
getHeartbeatAge,
|
|
881
4483
|
loadLocalConfig,
|
|
882
4484
|
loadSharedConfig,
|
|
@@ -885,12 +4487,15 @@ export {
|
|
|
885
4487
|
readNodeVersion,
|
|
886
4488
|
resolveConfig,
|
|
887
4489
|
resolveNodeRuntime,
|
|
4490
|
+
restartBridge,
|
|
888
4491
|
rotateLog,
|
|
889
4492
|
saveLocalConfig,
|
|
890
4493
|
saveSharedConfig,
|
|
891
4494
|
saveState,
|
|
4495
|
+
startAgents,
|
|
892
4496
|
startHttpServer,
|
|
893
4497
|
stateExists,
|
|
4498
|
+
stopAgents,
|
|
894
4499
|
streamEvents,
|
|
895
4500
|
updateBridgeHeartbeat,
|
|
896
4501
|
version
|