@hua-labs/tap 0.1.1 → 0.2.1
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 +123 -152
- package/dist/bridges/codex-app-server-auth-gateway.d.mts +8 -0
- package/dist/bridges/codex-app-server-auth-gateway.mjs +183 -0
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -0
- package/dist/bridges/codex-app-server-bridge.d.mts +55 -0
- package/dist/bridges/codex-app-server-bridge.mjs +1358 -0
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -0
- package/dist/bridges/codex-bridge-runner.d.mts +11 -1
- package/dist/bridges/codex-bridge-runner.mjs +271 -127
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +2885 -905
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +167 -5
- package/dist/index.mjs +3872 -96
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.d.mts +2 -0
- package/dist/mcp-server.mjs +22174 -0
- package/dist/mcp-server.mjs.map +1 -0
- package/package.json +7 -4
package/dist/cli.mjs
CHANGED
|
@@ -1,49 +1,219 @@
|
|
|
1
1
|
// src/commands/init.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
2
|
+
import * as fs6 from "fs";
|
|
3
|
+
import * as path6 from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
4
5
|
|
|
5
6
|
// src/state.ts
|
|
6
|
-
import * as
|
|
7
|
-
import * as
|
|
7
|
+
import * as fs3 from "fs";
|
|
8
|
+
import * as path3 from "path";
|
|
8
9
|
import * as crypto from "crypto";
|
|
9
10
|
|
|
10
11
|
// src/config/resolve.ts
|
|
12
|
+
import * as fs2 from "fs";
|
|
13
|
+
import * as path2 from "path";
|
|
14
|
+
|
|
15
|
+
// src/utils.ts
|
|
11
16
|
import * as fs from "fs";
|
|
12
17
|
import * as path from "path";
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
var VALID_RUNTIMES = ["claude", "codex", "gemini"];
|
|
19
|
+
function isValidRuntime(name) {
|
|
20
|
+
return VALID_RUNTIMES.includes(name);
|
|
21
|
+
}
|
|
22
|
+
function detectPlatform() {
|
|
23
|
+
return process.platform;
|
|
24
|
+
}
|
|
25
|
+
var _noGitWarned = false;
|
|
26
|
+
function _setNoGitWarned() {
|
|
27
|
+
_noGitWarned = true;
|
|
28
|
+
}
|
|
17
29
|
function findRepoRoot(startDir = process.cwd()) {
|
|
18
30
|
let dir = path.resolve(startDir);
|
|
19
31
|
while (true) {
|
|
20
32
|
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
21
|
-
if (fs.existsSync(path.join(dir, "package.json")))
|
|
33
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
34
|
+
if (!_noGitWarned) {
|
|
35
|
+
_setNoGitWarned();
|
|
36
|
+
logWarn(
|
|
37
|
+
"No .git directory found. Resolved repo root via package.json \u2014 comms directory may be created in an unexpected location. Use --comms-dir to specify explicitly."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
22
42
|
const parent = path.dirname(dir);
|
|
23
43
|
if (parent === dir) break;
|
|
24
44
|
dir = parent;
|
|
25
45
|
}
|
|
46
|
+
if (!_noGitWarned) {
|
|
47
|
+
_setNoGitWarned();
|
|
48
|
+
logWarn(
|
|
49
|
+
"No git repository or package.json found. Using current directory as root. Run 'git init' first, or use --comms-dir to specify the comms path."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return process.cwd();
|
|
53
|
+
}
|
|
54
|
+
function resolveCommsDir(args, repoRoot) {
|
|
55
|
+
const idx = args.indexOf("--comms-dir");
|
|
56
|
+
if (idx !== -1 && args[idx + 1]) {
|
|
57
|
+
return path.resolve(args[idx + 1]);
|
|
58
|
+
}
|
|
59
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
60
|
+
return config.commsDir;
|
|
61
|
+
}
|
|
62
|
+
function createAdapterContext(commsDir, repoRoot) {
|
|
63
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
64
|
+
return {
|
|
65
|
+
commsDir: path.resolve(commsDir),
|
|
66
|
+
repoRoot: path.resolve(repoRoot),
|
|
67
|
+
stateDir: config.stateDir,
|
|
68
|
+
platform: detectPlatform()
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseArgs(args) {
|
|
72
|
+
const positional = [];
|
|
73
|
+
const flags = {};
|
|
74
|
+
for (let i = 0; i < args.length; i++) {
|
|
75
|
+
const arg = args[i];
|
|
76
|
+
if (arg.startsWith("--")) {
|
|
77
|
+
const key = arg.slice(2);
|
|
78
|
+
const next = args[i + 1];
|
|
79
|
+
if (next && !next.startsWith("--")) {
|
|
80
|
+
flags[key] = next;
|
|
81
|
+
i++;
|
|
82
|
+
} else {
|
|
83
|
+
flags[key] = true;
|
|
84
|
+
}
|
|
85
|
+
} else if (arg.startsWith("-")) {
|
|
86
|
+
flags[arg.slice(1)] = true;
|
|
87
|
+
} else {
|
|
88
|
+
positional.push(arg);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { positional, flags };
|
|
92
|
+
}
|
|
93
|
+
var _jsonMode = false;
|
|
94
|
+
function setJsonMode(enabled) {
|
|
95
|
+
_jsonMode = enabled;
|
|
96
|
+
}
|
|
97
|
+
function log(message) {
|
|
98
|
+
if (!_jsonMode) console.log(` ${message}`);
|
|
99
|
+
}
|
|
100
|
+
function logSuccess(message) {
|
|
101
|
+
if (!_jsonMode) console.log(` + ${message}`);
|
|
102
|
+
}
|
|
103
|
+
function logWarn(message) {
|
|
104
|
+
if (!_jsonMode) console.log(` ! ${message}`);
|
|
105
|
+
}
|
|
106
|
+
function logError(message) {
|
|
107
|
+
if (!_jsonMode) console.error(` x ${message}`);
|
|
108
|
+
}
|
|
109
|
+
function logHeader(message) {
|
|
110
|
+
if (!_jsonMode) console.log(`
|
|
111
|
+
${message}
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
114
|
+
function resolveInstanceId(identifier, state) {
|
|
115
|
+
if (state.instances[identifier]) {
|
|
116
|
+
return { ok: true, instanceId: identifier };
|
|
117
|
+
}
|
|
118
|
+
if (isValidRuntime(identifier)) {
|
|
119
|
+
const matches = Object.values(state.instances).filter(
|
|
120
|
+
(inst) => inst.runtime === identifier
|
|
121
|
+
);
|
|
122
|
+
if (matches.length === 1) {
|
|
123
|
+
return { ok: true, instanceId: matches[0].instanceId };
|
|
124
|
+
}
|
|
125
|
+
if (matches.length > 1) {
|
|
126
|
+
const ids = matches.map((m) => m.instanceId).join(", ");
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
code: "TAP_INSTANCE_AMBIGUOUS",
|
|
130
|
+
message: `Multiple ${identifier} instances found: ${ids}. Specify one explicitly.`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
137
|
+
message: `Instance not found: ${identifier}`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function buildInstanceId(runtime, name) {
|
|
141
|
+
return name ? `${runtime}-${name}` : runtime;
|
|
142
|
+
}
|
|
143
|
+
function findPortConflict(state, port, excludeInstanceId) {
|
|
144
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
145
|
+
if (id !== excludeInstanceId && inst.port === port) return id;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/config/resolve.ts
|
|
151
|
+
var SHARED_CONFIG_FILE = "tap-config.json";
|
|
152
|
+
var LOCAL_CONFIG_FILE = "tap-config.local.json";
|
|
153
|
+
var LEGACY_CONFIG_FILE = ".tap-config";
|
|
154
|
+
var DEFAULT_RUNTIME_COMMAND = "node";
|
|
155
|
+
var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
156
|
+
function findRepoRoot2(startDir = process.cwd()) {
|
|
157
|
+
let dir = path2.resolve(startDir);
|
|
158
|
+
while (true) {
|
|
159
|
+
if (fs2.existsSync(path2.join(dir, ".git"))) return dir;
|
|
160
|
+
if (fs2.existsSync(path2.join(dir, "package.json"))) {
|
|
161
|
+
if (!_noGitWarned) {
|
|
162
|
+
_setNoGitWarned();
|
|
163
|
+
console.error(
|
|
164
|
+
"[tap] warning: No .git directory found. Resolved via package.json. Use --comms-dir to specify explicitly."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return dir;
|
|
168
|
+
}
|
|
169
|
+
const parent = path2.dirname(dir);
|
|
170
|
+
if (parent === dir) break;
|
|
171
|
+
dir = parent;
|
|
172
|
+
}
|
|
173
|
+
if (!_noGitWarned) {
|
|
174
|
+
_setNoGitWarned();
|
|
175
|
+
console.error(
|
|
176
|
+
"[tap] warning: No git repository found. Using cwd as root. Run 'git init' or use --comms-dir."
|
|
177
|
+
);
|
|
178
|
+
}
|
|
26
179
|
return process.cwd();
|
|
27
180
|
}
|
|
28
181
|
function loadJsonFile(filePath) {
|
|
29
|
-
if (!
|
|
182
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
30
183
|
try {
|
|
31
|
-
const raw =
|
|
184
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
32
185
|
return JSON.parse(raw);
|
|
33
186
|
} catch {
|
|
34
187
|
return null;
|
|
35
188
|
}
|
|
36
189
|
}
|
|
37
|
-
function
|
|
38
|
-
return loadJsonFile(
|
|
190
|
+
function loadSharedConfig2(repoRoot) {
|
|
191
|
+
return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
|
|
39
192
|
}
|
|
40
193
|
function loadLocalConfig(repoRoot) {
|
|
41
|
-
return loadJsonFile(
|
|
194
|
+
return loadJsonFile(path2.join(repoRoot, LOCAL_CONFIG_FILE));
|
|
195
|
+
}
|
|
196
|
+
function readLegacyShellValue(configText, key) {
|
|
197
|
+
const match = configText.match(new RegExp(`^${key}="?(.+?)"?$`, "m"));
|
|
198
|
+
return match?.[1]?.trim() || null;
|
|
199
|
+
}
|
|
200
|
+
function loadLegacyShellConfig(repoRoot) {
|
|
201
|
+
const filePath = path2.join(repoRoot, LEGACY_CONFIG_FILE);
|
|
202
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
203
|
+
try {
|
|
204
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
205
|
+
const commsDir = readLegacyShellValue(raw, "TAP_COMMS_DIR");
|
|
206
|
+
if (!commsDir) return null;
|
|
207
|
+
return { commsDir };
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
42
211
|
}
|
|
43
212
|
function resolveConfig(overrides = {}, startDir) {
|
|
44
|
-
const repoRoot =
|
|
45
|
-
const shared =
|
|
213
|
+
const repoRoot = findRepoRoot2(startDir);
|
|
214
|
+
const shared = loadSharedConfig2(repoRoot) ?? {};
|
|
46
215
|
const local = loadLocalConfig(repoRoot) ?? {};
|
|
216
|
+
const legacy = loadLegacyShellConfig(repoRoot) ?? {};
|
|
47
217
|
const sources = {
|
|
48
218
|
repoRoot: "auto",
|
|
49
219
|
commsDir: "auto",
|
|
@@ -53,10 +223,10 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
53
223
|
};
|
|
54
224
|
let commsDir;
|
|
55
225
|
if (overrides.commsDir) {
|
|
56
|
-
commsDir =
|
|
226
|
+
commsDir = resolvePath(repoRoot, overrides.commsDir);
|
|
57
227
|
sources.commsDir = "cli-flag";
|
|
58
228
|
} else if (process.env.TAP_COMMS_DIR) {
|
|
59
|
-
commsDir =
|
|
229
|
+
commsDir = resolvePath(repoRoot, process.env.TAP_COMMS_DIR);
|
|
60
230
|
sources.commsDir = "env";
|
|
61
231
|
} else if (local.commsDir) {
|
|
62
232
|
commsDir = resolvePath(repoRoot, local.commsDir);
|
|
@@ -64,15 +234,18 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
64
234
|
} else if (shared.commsDir) {
|
|
65
235
|
commsDir = resolvePath(repoRoot, shared.commsDir);
|
|
66
236
|
sources.commsDir = "shared-config";
|
|
237
|
+
} else if (legacy.commsDir) {
|
|
238
|
+
commsDir = resolvePath(repoRoot, legacy.commsDir);
|
|
239
|
+
sources.commsDir = "legacy-shell-config";
|
|
67
240
|
} else {
|
|
68
|
-
commsDir =
|
|
241
|
+
commsDir = path2.join(repoRoot, "tap-comms");
|
|
69
242
|
}
|
|
70
243
|
let stateDir;
|
|
71
244
|
if (overrides.stateDir) {
|
|
72
|
-
stateDir =
|
|
245
|
+
stateDir = resolvePath(repoRoot, overrides.stateDir);
|
|
73
246
|
sources.stateDir = "cli-flag";
|
|
74
247
|
} else if (process.env.TAP_STATE_DIR) {
|
|
75
|
-
stateDir =
|
|
248
|
+
stateDir = resolvePath(repoRoot, process.env.TAP_STATE_DIR);
|
|
76
249
|
sources.stateDir = "env";
|
|
77
250
|
} else if (local.stateDir) {
|
|
78
251
|
stateDir = resolvePath(repoRoot, local.stateDir);
|
|
@@ -81,7 +254,7 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
81
254
|
stateDir = resolvePath(repoRoot, shared.stateDir);
|
|
82
255
|
sources.stateDir = "shared-config";
|
|
83
256
|
} else {
|
|
84
|
-
stateDir =
|
|
257
|
+
stateDir = path2.join(repoRoot, ".tap-comms");
|
|
85
258
|
}
|
|
86
259
|
let runtimeCommand;
|
|
87
260
|
if (overrides.runtimeCommand) {
|
|
@@ -120,8 +293,28 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
120
293
|
sources
|
|
121
294
|
};
|
|
122
295
|
}
|
|
296
|
+
function saveSharedConfig(repoRoot, config) {
|
|
297
|
+
const filePath = path2.join(repoRoot, SHARED_CONFIG_FILE);
|
|
298
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
299
|
+
fs2.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
300
|
+
fs2.renameSync(tmp, filePath);
|
|
301
|
+
}
|
|
123
302
|
function resolvePath(repoRoot, p) {
|
|
124
|
-
|
|
303
|
+
const normalized = normalizeTapPath(p);
|
|
304
|
+
return path2.isAbsolute(normalized) ? normalized : path2.resolve(repoRoot, normalized);
|
|
305
|
+
}
|
|
306
|
+
function normalizeTapPath(input) {
|
|
307
|
+
const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
|
|
308
|
+
if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
309
|
+
return trimmed;
|
|
310
|
+
}
|
|
311
|
+
if (process.platform === "win32") {
|
|
312
|
+
const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
|
|
313
|
+
if (match) {
|
|
314
|
+
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return trimmed;
|
|
125
318
|
}
|
|
126
319
|
|
|
127
320
|
// src/state.ts
|
|
@@ -132,10 +325,10 @@ function getStateDir(repoRoot) {
|
|
|
132
325
|
return config.stateDir;
|
|
133
326
|
}
|
|
134
327
|
function getStatePath(repoRoot) {
|
|
135
|
-
return
|
|
328
|
+
return path3.join(getStateDir(repoRoot), STATE_FILE);
|
|
136
329
|
}
|
|
137
330
|
function stateExists(repoRoot) {
|
|
138
|
-
return
|
|
331
|
+
return fs3.existsSync(getStatePath(repoRoot));
|
|
139
332
|
}
|
|
140
333
|
function migrateStateV1toV2(v1) {
|
|
141
334
|
const instances = {};
|
|
@@ -163,8 +356,8 @@ function migrateStateV1toV2(v1) {
|
|
|
163
356
|
}
|
|
164
357
|
function loadState(repoRoot) {
|
|
165
358
|
const statePath = getStatePath(repoRoot);
|
|
166
|
-
if (!
|
|
167
|
-
const raw =
|
|
359
|
+
if (!fs3.existsSync(statePath)) return null;
|
|
360
|
+
const raw = fs3.readFileSync(statePath, "utf-8");
|
|
168
361
|
const parsed = JSON.parse(raw);
|
|
169
362
|
if (parsed.schemaVersion === 1 || parsed.runtimes) {
|
|
170
363
|
const migrated = migrateStateV1toV2(parsed);
|
|
@@ -175,11 +368,11 @@ function loadState(repoRoot) {
|
|
|
175
368
|
}
|
|
176
369
|
function saveState(repoRoot, state) {
|
|
177
370
|
const stateDir = getStateDir(repoRoot);
|
|
178
|
-
|
|
371
|
+
fs3.mkdirSync(stateDir, { recursive: true });
|
|
179
372
|
const statePath = getStatePath(repoRoot);
|
|
180
373
|
const tmp = `${statePath}.tmp.${process.pid}`;
|
|
181
|
-
|
|
182
|
-
|
|
374
|
+
fs3.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
|
|
375
|
+
fs3.renameSync(tmp, statePath);
|
|
183
376
|
}
|
|
184
377
|
function createInitialState(commsDir, repoRoot, packageVersion) {
|
|
185
378
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -187,8 +380,8 @@ function createInitialState(commsDir, repoRoot, packageVersion) {
|
|
|
187
380
|
schemaVersion: SCHEMA_VERSION,
|
|
188
381
|
createdAt: now,
|
|
189
382
|
updatedAt: now,
|
|
190
|
-
commsDir:
|
|
191
|
-
repoRoot:
|
|
383
|
+
commsDir: path3.resolve(commsDir),
|
|
384
|
+
repoRoot: path3.resolve(repoRoot),
|
|
192
385
|
packageVersion,
|
|
193
386
|
instances: {}
|
|
194
387
|
};
|
|
@@ -217,146 +410,45 @@ function getInstalledInstances(state) {
|
|
|
217
410
|
);
|
|
218
411
|
}
|
|
219
412
|
function ensureBackupDir(stateDir, instanceId) {
|
|
220
|
-
const backupDir =
|
|
221
|
-
|
|
413
|
+
const backupDir = path3.join(stateDir, "backups", instanceId);
|
|
414
|
+
fs3.mkdirSync(backupDir, { recursive: true });
|
|
222
415
|
return backupDir;
|
|
223
416
|
}
|
|
224
417
|
function backupFile(filePath, backupDir) {
|
|
225
|
-
const basename3 =
|
|
418
|
+
const basename3 = path3.basename(filePath);
|
|
226
419
|
const hash = fileHash(filePath);
|
|
227
|
-
const backupPath =
|
|
228
|
-
|
|
420
|
+
const backupPath = path3.join(backupDir, `${basename3}.${hash}.bak`);
|
|
421
|
+
fs3.copyFileSync(filePath, backupPath);
|
|
229
422
|
return backupPath;
|
|
230
423
|
}
|
|
231
|
-
function fileHash(filePath) {
|
|
232
|
-
if (!
|
|
233
|
-
const content =
|
|
234
|
-
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// src/utils.ts
|
|
238
|
-
import * as fs3 from "fs";
|
|
239
|
-
import * as path3 from "path";
|
|
240
|
-
var VALID_RUNTIMES = ["claude", "codex", "gemini"];
|
|
241
|
-
function isValidRuntime(name) {
|
|
242
|
-
return VALID_RUNTIMES.includes(name);
|
|
243
|
-
}
|
|
244
|
-
function detectPlatform() {
|
|
245
|
-
return process.platform;
|
|
246
|
-
}
|
|
247
|
-
function findRepoRoot2(startDir = process.cwd()) {
|
|
248
|
-
let dir = path3.resolve(startDir);
|
|
249
|
-
while (true) {
|
|
250
|
-
if (fs3.existsSync(path3.join(dir, ".git"))) return dir;
|
|
251
|
-
if (fs3.existsSync(path3.join(dir, "package.json"))) return dir;
|
|
252
|
-
const parent = path3.dirname(dir);
|
|
253
|
-
if (parent === dir) break;
|
|
254
|
-
dir = parent;
|
|
255
|
-
}
|
|
256
|
-
return process.cwd();
|
|
257
|
-
}
|
|
258
|
-
function resolveCommsDir(args, repoRoot) {
|
|
259
|
-
const idx = args.indexOf("--comms-dir");
|
|
260
|
-
if (idx !== -1 && args[idx + 1]) {
|
|
261
|
-
return path3.resolve(args[idx + 1]);
|
|
262
|
-
}
|
|
263
|
-
const { config } = resolveConfig({}, repoRoot);
|
|
264
|
-
return config.commsDir;
|
|
265
|
-
}
|
|
266
|
-
function createAdapterContext(commsDir, repoRoot) {
|
|
267
|
-
const { config } = resolveConfig({}, repoRoot);
|
|
268
|
-
return {
|
|
269
|
-
commsDir: path3.resolve(commsDir),
|
|
270
|
-
repoRoot: path3.resolve(repoRoot),
|
|
271
|
-
stateDir: config.stateDir,
|
|
272
|
-
platform: detectPlatform()
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
function parseArgs(args) {
|
|
276
|
-
const positional = [];
|
|
277
|
-
const flags = {};
|
|
278
|
-
for (let i = 0; i < args.length; i++) {
|
|
279
|
-
const arg = args[i];
|
|
280
|
-
if (arg.startsWith("--")) {
|
|
281
|
-
const key = arg.slice(2);
|
|
282
|
-
const next = args[i + 1];
|
|
283
|
-
if (next && !next.startsWith("--")) {
|
|
284
|
-
flags[key] = next;
|
|
285
|
-
i++;
|
|
286
|
-
} else {
|
|
287
|
-
flags[key] = true;
|
|
288
|
-
}
|
|
289
|
-
} else if (arg.startsWith("-")) {
|
|
290
|
-
flags[arg.slice(1)] = true;
|
|
291
|
-
} else {
|
|
292
|
-
positional.push(arg);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return { positional, flags };
|
|
296
|
-
}
|
|
297
|
-
var _jsonMode = false;
|
|
298
|
-
function setJsonMode(enabled) {
|
|
299
|
-
_jsonMode = enabled;
|
|
300
|
-
}
|
|
301
|
-
function log(message) {
|
|
302
|
-
if (!_jsonMode) console.log(` ${message}`);
|
|
303
|
-
}
|
|
304
|
-
function logSuccess(message) {
|
|
305
|
-
if (!_jsonMode) console.log(` + ${message}`);
|
|
306
|
-
}
|
|
307
|
-
function logWarn(message) {
|
|
308
|
-
if (!_jsonMode) console.log(` ! ${message}`);
|
|
309
|
-
}
|
|
310
|
-
function logError(message) {
|
|
311
|
-
if (!_jsonMode) console.error(` x ${message}`);
|
|
312
|
-
}
|
|
313
|
-
function logHeader(message) {
|
|
314
|
-
if (!_jsonMode) console.log(`
|
|
315
|
-
${message}
|
|
316
|
-
`);
|
|
317
|
-
}
|
|
318
|
-
function resolveInstanceId(identifier, state) {
|
|
319
|
-
if (state.instances[identifier]) {
|
|
320
|
-
return { ok: true, instanceId: identifier };
|
|
321
|
-
}
|
|
322
|
-
if (isValidRuntime(identifier)) {
|
|
323
|
-
const matches = Object.values(state.instances).filter(
|
|
324
|
-
(inst) => inst.runtime === identifier
|
|
325
|
-
);
|
|
326
|
-
if (matches.length === 1) {
|
|
327
|
-
return { ok: true, instanceId: matches[0].instanceId };
|
|
328
|
-
}
|
|
329
|
-
if (matches.length > 1) {
|
|
330
|
-
const ids = matches.map((m) => m.instanceId).join(", ");
|
|
331
|
-
return {
|
|
332
|
-
ok: false,
|
|
333
|
-
code: "TAP_INSTANCE_AMBIGUOUS",
|
|
334
|
-
message: `Multiple ${identifier} instances found: ${ids}. Specify one explicitly.`
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return {
|
|
339
|
-
ok: false,
|
|
340
|
-
code: "TAP_INSTANCE_NOT_FOUND",
|
|
341
|
-
message: `Instance not found: ${identifier}`
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
function buildInstanceId(runtime, name) {
|
|
345
|
-
return name ? `${runtime}-${name}` : runtime;
|
|
346
|
-
}
|
|
347
|
-
function findPortConflict(state, port, excludeInstanceId) {
|
|
348
|
-
for (const [id, inst] of Object.entries(state.instances)) {
|
|
349
|
-
if (id !== excludeInstanceId && inst.port === port) return id;
|
|
350
|
-
}
|
|
351
|
-
return null;
|
|
424
|
+
function fileHash(filePath) {
|
|
425
|
+
if (!fs3.existsSync(filePath)) return "";
|
|
426
|
+
const content = fs3.readFileSync(filePath);
|
|
427
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
352
428
|
}
|
|
353
429
|
|
|
354
430
|
// src/version.ts
|
|
355
|
-
var version = "0.1.0";
|
|
356
|
-
|
|
357
|
-
// src/permissions.ts
|
|
358
431
|
import * as fs4 from "fs";
|
|
359
432
|
import * as path4 from "path";
|
|
433
|
+
import { fileURLToPath } from "url";
|
|
434
|
+
var FALLBACK_VERSION = "0.0.0";
|
|
435
|
+
function resolvePackageVersion(metaUrl = import.meta.url) {
|
|
436
|
+
const moduleDir = path4.dirname(fileURLToPath(metaUrl));
|
|
437
|
+
const packageJsonPath = path4.join(moduleDir, "..", "package.json");
|
|
438
|
+
try {
|
|
439
|
+
const parsed = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
|
|
440
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
441
|
+
return parsed.version;
|
|
442
|
+
}
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
return FALLBACK_VERSION;
|
|
446
|
+
}
|
|
447
|
+
var version = resolvePackageVersion();
|
|
448
|
+
|
|
449
|
+
// src/permissions.ts
|
|
450
|
+
import * as fs5 from "fs";
|
|
451
|
+
import * as path5 from "path";
|
|
360
452
|
import * as os from "os";
|
|
361
453
|
|
|
362
454
|
// src/toml.ts
|
|
@@ -486,13 +578,13 @@ var CLAUDE_DENY_RULES = [
|
|
|
486
578
|
];
|
|
487
579
|
function applyClaudePermissions(repoRoot, mode) {
|
|
488
580
|
const warnings = [];
|
|
489
|
-
const claudeDir =
|
|
490
|
-
const settingsPath =
|
|
491
|
-
|
|
581
|
+
const claudeDir = path5.join(repoRoot, ".claude");
|
|
582
|
+
const settingsPath = path5.join(claudeDir, "settings.local.json");
|
|
583
|
+
fs5.mkdirSync(claudeDir, { recursive: true });
|
|
492
584
|
let settings = {};
|
|
493
|
-
if (
|
|
585
|
+
if (fs5.existsSync(settingsPath)) {
|
|
494
586
|
try {
|
|
495
|
-
settings = JSON.parse(
|
|
587
|
+
settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
|
|
496
588
|
} catch {
|
|
497
589
|
warnings.push(
|
|
498
590
|
".claude/settings.local.json was invalid JSON. Starting fresh."
|
|
@@ -506,8 +598,8 @@ function applyClaudePermissions(repoRoot, mode) {
|
|
|
506
598
|
const cleaned = existingDeny.filter((r) => !tapRuleSet.has(r));
|
|
507
599
|
settings.deny = cleaned;
|
|
508
600
|
const tmp2 = `${settingsPath}.tmp.${process.pid}`;
|
|
509
|
-
|
|
510
|
-
|
|
601
|
+
fs5.writeFileSync(tmp2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
602
|
+
fs5.renameSync(tmp2, settingsPath);
|
|
511
603
|
logWarn("Claude: full mode \u2014 tap deny rules removed. Use with caution.");
|
|
512
604
|
warnings.push("Full permission mode: tap deny rules removed.");
|
|
513
605
|
return { applied: true, warnings };
|
|
@@ -515,18 +607,18 @@ function applyClaudePermissions(repoRoot, mode) {
|
|
|
515
607
|
const newDeny = [.../* @__PURE__ */ new Set([...existingDeny, ...CLAUDE_DENY_RULES])];
|
|
516
608
|
settings.deny = newDeny;
|
|
517
609
|
const tmp = `${settingsPath}.tmp.${process.pid}`;
|
|
518
|
-
|
|
519
|
-
|
|
610
|
+
fs5.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
611
|
+
fs5.renameSync(tmp, settingsPath);
|
|
520
612
|
logSuccess(
|
|
521
613
|
`Claude: ${CLAUDE_DENY_RULES.length} deny rules applied to .claude/settings.local.json`
|
|
522
614
|
);
|
|
523
615
|
return { applied: true, warnings };
|
|
524
616
|
}
|
|
525
617
|
function findCodexConfigPath() {
|
|
526
|
-
return
|
|
618
|
+
return path5.join(os.homedir(), ".codex", "config.toml");
|
|
527
619
|
}
|
|
528
620
|
function canonicalizeTrustPath(targetPath) {
|
|
529
|
-
let resolved =
|
|
621
|
+
let resolved = path5.resolve(targetPath).replace(/\//g, "\\");
|
|
530
622
|
const driveRoot = /^[A-Za-z]:\\$/;
|
|
531
623
|
if (!driveRoot.test(resolved)) {
|
|
532
624
|
resolved = resolved.replace(/\\+$/g, "");
|
|
@@ -536,10 +628,10 @@ function canonicalizeTrustPath(targetPath) {
|
|
|
536
628
|
function applyCodexPermissions(repoRoot, commsDir, mode) {
|
|
537
629
|
const warnings = [];
|
|
538
630
|
const configPath = findCodexConfigPath();
|
|
539
|
-
|
|
631
|
+
fs5.mkdirSync(path5.dirname(configPath), { recursive: true });
|
|
540
632
|
let content = "";
|
|
541
|
-
if (
|
|
542
|
-
content =
|
|
633
|
+
if (fs5.existsSync(configPath)) {
|
|
634
|
+
content = fs5.readFileSync(configPath, "utf-8");
|
|
543
635
|
}
|
|
544
636
|
const trustTargets = getCodexWritableRoots(repoRoot, commsDir);
|
|
545
637
|
if (mode === "full") {
|
|
@@ -601,8 +693,8 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
|
|
|
601
693
|
);
|
|
602
694
|
}
|
|
603
695
|
const tmp = `${configPath}.tmp.${process.pid}`;
|
|
604
|
-
|
|
605
|
-
|
|
696
|
+
fs5.writeFileSync(tmp, content, "utf-8");
|
|
697
|
+
fs5.renameSync(tmp, configPath);
|
|
606
698
|
const modeLabel = mode === "full" ? "danger-full-access" : "workspace-write, network=full";
|
|
607
699
|
logSuccess(
|
|
608
700
|
`Codex: sandbox=${modeLabel}, ${trustTargets.length} path(s) trusted`
|
|
@@ -611,12 +703,12 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
|
|
|
611
703
|
}
|
|
612
704
|
function getCodexWritableRoots(repoRoot, commsDir) {
|
|
613
705
|
const roots = [repoRoot, commsDir];
|
|
614
|
-
const parent =
|
|
706
|
+
const parent = path5.dirname(repoRoot);
|
|
615
707
|
for (let i = 1; i <= 4; i++) {
|
|
616
|
-
const wtPath =
|
|
617
|
-
if (
|
|
708
|
+
const wtPath = path5.join(parent, `hua-wt-${i}`);
|
|
709
|
+
if (fs5.existsSync(wtPath)) roots.push(wtPath);
|
|
618
710
|
}
|
|
619
|
-
return [...new Set(roots.map((r) =>
|
|
711
|
+
return [...new Set(roots.map((r) => path5.resolve(r)))];
|
|
620
712
|
}
|
|
621
713
|
function buildPermissionSummary(mode, repoRoot, commsDir) {
|
|
622
714
|
const trustedPaths = getCodexWritableRoots(repoRoot, commsDir);
|
|
@@ -654,7 +746,7 @@ function parsePermissionMode(args) {
|
|
|
654
746
|
return "safe";
|
|
655
747
|
}
|
|
656
748
|
async function initCommand(args) {
|
|
657
|
-
const repoRoot =
|
|
749
|
+
const repoRoot = findRepoRoot();
|
|
658
750
|
const commsDir = resolveCommsDir(args, repoRoot);
|
|
659
751
|
const permMode = parsePermissionMode(args);
|
|
660
752
|
if (stateExists(repoRoot) && !args.includes("--force")) {
|
|
@@ -668,15 +760,73 @@ async function initCommand(args) {
|
|
|
668
760
|
};
|
|
669
761
|
}
|
|
670
762
|
logHeader("@hua-labs/tap init");
|
|
763
|
+
const commsRepoIdx = args.indexOf("--comms-repo");
|
|
764
|
+
const commsRepoUrl = commsRepoIdx !== -1 && args[commsRepoIdx + 1] ? args[commsRepoIdx + 1] : void 0;
|
|
765
|
+
if (commsRepoUrl) {
|
|
766
|
+
if (fs6.existsSync(commsDir) && fs6.readdirSync(commsDir).length > 0) {
|
|
767
|
+
const gitDir = path6.join(commsDir, ".git");
|
|
768
|
+
if (fs6.existsSync(gitDir)) {
|
|
769
|
+
log(`Comms directory exists: ${commsDir}`);
|
|
770
|
+
logSuccess("Comms directory is already a git repo \u2014 linking only");
|
|
771
|
+
} else {
|
|
772
|
+
logError(`Comms directory exists but is not a git repo: ${commsDir}`);
|
|
773
|
+
return {
|
|
774
|
+
ok: false,
|
|
775
|
+
command: "init",
|
|
776
|
+
code: "TAP_INIT_CLONE_FAILED",
|
|
777
|
+
message: `Comms directory "${commsDir}" exists but is not a git repo. Remove it or use --force to reinitialize.`,
|
|
778
|
+
warnings: [],
|
|
779
|
+
data: { commsDir, commsRepoUrl }
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
log(`Cloning comms repo: ${commsRepoUrl}`);
|
|
784
|
+
try {
|
|
785
|
+
execSync(`git clone "${commsRepoUrl}" "${commsDir}"`, {
|
|
786
|
+
stdio: "pipe",
|
|
787
|
+
encoding: "utf-8"
|
|
788
|
+
});
|
|
789
|
+
logSuccess(`Cloned comms repo to ${commsDir}`);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
792
|
+
logError(`Failed to clone comms repo: ${msg}`);
|
|
793
|
+
return {
|
|
794
|
+
ok: false,
|
|
795
|
+
command: "init",
|
|
796
|
+
code: "TAP_INIT_CLONE_FAILED",
|
|
797
|
+
message: `Failed to clone comms repo: ${msg}`,
|
|
798
|
+
warnings: [],
|
|
799
|
+
data: { commsRepoUrl }
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
{
|
|
805
|
+
const sharedConfig = loadSharedConfig2(repoRoot) ?? {};
|
|
806
|
+
let configChanged = false;
|
|
807
|
+
if (commsRepoUrl) {
|
|
808
|
+
sharedConfig.commsRepoUrl = commsRepoUrl;
|
|
809
|
+
configChanged = true;
|
|
810
|
+
}
|
|
811
|
+
const commsDirRelative = path6.relative(repoRoot, commsDir);
|
|
812
|
+
if (commsDirRelative && commsDirRelative !== "tap-comms") {
|
|
813
|
+
sharedConfig.commsDir = commsDirRelative;
|
|
814
|
+
configChanged = true;
|
|
815
|
+
}
|
|
816
|
+
if (configChanged) {
|
|
817
|
+
saveSharedConfig(repoRoot, sharedConfig);
|
|
818
|
+
logSuccess("Saved comms config to tap-config.json");
|
|
819
|
+
}
|
|
820
|
+
}
|
|
671
821
|
log(`Comms directory: ${commsDir}`);
|
|
672
822
|
for (const dir of COMMS_DIRS) {
|
|
673
|
-
const dirPath =
|
|
674
|
-
|
|
823
|
+
const dirPath = path6.join(commsDir, dir);
|
|
824
|
+
fs6.mkdirSync(dirPath, { recursive: true });
|
|
675
825
|
logSuccess(`Created ${dir}/`);
|
|
676
826
|
}
|
|
677
|
-
const gitignorePath =
|
|
678
|
-
if (!
|
|
679
|
-
|
|
827
|
+
const gitignorePath = path6.join(commsDir, ".gitignore");
|
|
828
|
+
if (!fs6.existsSync(gitignorePath)) {
|
|
829
|
+
fs6.writeFileSync(
|
|
680
830
|
gitignorePath,
|
|
681
831
|
["tap.db", ".lock", "*.tmp.*", ".DS_Store"].join("\n") + "\n",
|
|
682
832
|
"utf-8"
|
|
@@ -685,12 +835,12 @@ async function initCommand(args) {
|
|
|
685
835
|
}
|
|
686
836
|
const { config } = resolveConfig({}, repoRoot);
|
|
687
837
|
const stateDir = config.stateDir;
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const stateDirRel =
|
|
838
|
+
fs6.mkdirSync(path6.join(stateDir, "pids"), { recursive: true });
|
|
839
|
+
fs6.mkdirSync(path6.join(stateDir, "logs"), { recursive: true });
|
|
840
|
+
fs6.mkdirSync(path6.join(stateDir, "backups"), { recursive: true });
|
|
841
|
+
const stateDirRel = path6.relative(repoRoot, stateDir);
|
|
692
842
|
logSuccess(`Created ${stateDirRel}/ state directory`);
|
|
693
|
-
const repoGitignore =
|
|
843
|
+
const repoGitignore = path6.join(repoRoot, ".gitignore");
|
|
694
844
|
const gitignoreEntries = [
|
|
695
845
|
{ entry: stateDirRel.replace(/\\/g, "/") + "/", label: "tap-comms state" },
|
|
696
846
|
{
|
|
@@ -698,11 +848,11 @@ async function initCommand(args) {
|
|
|
698
848
|
label: "tap-comms local config (machine-specific)"
|
|
699
849
|
}
|
|
700
850
|
];
|
|
701
|
-
if (
|
|
702
|
-
const content =
|
|
851
|
+
if (fs6.existsSync(repoGitignore)) {
|
|
852
|
+
const content = fs6.readFileSync(repoGitignore, "utf-8");
|
|
703
853
|
for (const { entry, label } of gitignoreEntries) {
|
|
704
854
|
if (!content.includes(entry)) {
|
|
705
|
-
|
|
855
|
+
fs6.appendFileSync(repoGitignore, `
|
|
706
856
|
# ${label}
|
|
707
857
|
${entry}
|
|
708
858
|
`);
|
|
@@ -742,64 +892,178 @@ ${entry}
|
|
|
742
892
|
}
|
|
743
893
|
|
|
744
894
|
// src/adapters/claude.ts
|
|
745
|
-
import * as
|
|
746
|
-
import * as
|
|
747
|
-
import { execSync } from "child_process";
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
895
|
+
import * as fs8 from "fs";
|
|
896
|
+
import * as path8 from "path";
|
|
897
|
+
import { execSync as execSync2 } from "child_process";
|
|
898
|
+
|
|
899
|
+
// src/adapters/common.ts
|
|
900
|
+
import * as fs7 from "fs";
|
|
901
|
+
import * as os2 from "os";
|
|
902
|
+
import * as path7 from "path";
|
|
903
|
+
import { spawnSync } from "child_process";
|
|
904
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
905
|
+
function probeCommand(candidates) {
|
|
906
|
+
for (const candidate of candidates) {
|
|
907
|
+
const result = spawnSync(candidate, ["--version"], {
|
|
908
|
+
encoding: "utf-8",
|
|
909
|
+
shell: process.platform === "win32"
|
|
910
|
+
});
|
|
911
|
+
if (result.status === 0) {
|
|
912
|
+
const version2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || null;
|
|
913
|
+
return { command: candidate, version: version2 };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return { command: null, version: null };
|
|
751
917
|
}
|
|
752
|
-
function
|
|
918
|
+
function getHomeDir() {
|
|
919
|
+
return os2.homedir();
|
|
920
|
+
}
|
|
921
|
+
function toForwardSlashPath(filePath) {
|
|
922
|
+
return path7.resolve(filePath).replace(/\\/g, "/");
|
|
923
|
+
}
|
|
924
|
+
function canWriteOrCreate(filePath) {
|
|
753
925
|
try {
|
|
754
|
-
|
|
755
|
-
|
|
926
|
+
if (fs7.existsSync(filePath)) {
|
|
927
|
+
fs7.accessSync(filePath, fs7.constants.W_OK);
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
const parent = path7.dirname(filePath);
|
|
931
|
+
fs7.mkdirSync(parent, { recursive: true });
|
|
932
|
+
fs7.accessSync(parent, fs7.constants.W_OK);
|
|
933
|
+
return true;
|
|
756
934
|
} catch {
|
|
757
|
-
return
|
|
935
|
+
return false;
|
|
758
936
|
}
|
|
759
937
|
}
|
|
760
|
-
function
|
|
761
|
-
const localChannels = findLocalChannels(ctx);
|
|
762
|
-
if (!localChannels) return null;
|
|
763
|
-
return {
|
|
764
|
-
type: "stdio",
|
|
765
|
-
command: "npx",
|
|
766
|
-
args: ["bun", localChannels],
|
|
767
|
-
env: { TAP_COMMS_DIR: ctx.commsDir }
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
function findLocalChannels(ctx) {
|
|
938
|
+
function findLocalTapCommsSource(ctx) {
|
|
771
939
|
const candidates = [
|
|
772
|
-
|
|
940
|
+
path7.join(
|
|
773
941
|
ctx.repoRoot,
|
|
774
942
|
"packages",
|
|
775
943
|
"tap-plugin",
|
|
776
944
|
"channels",
|
|
777
945
|
"tap-comms.ts"
|
|
778
946
|
),
|
|
779
|
-
|
|
947
|
+
path7.join(
|
|
780
948
|
ctx.repoRoot,
|
|
781
949
|
"node_modules",
|
|
782
950
|
"@hua-labs",
|
|
951
|
+
"tap-plugin",
|
|
783
952
|
"channels",
|
|
784
953
|
"tap-comms.ts"
|
|
785
954
|
)
|
|
786
955
|
];
|
|
787
|
-
for (const
|
|
788
|
-
if (
|
|
956
|
+
for (const candidate of candidates) {
|
|
957
|
+
if (fs7.existsSync(candidate)) return candidate;
|
|
958
|
+
}
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
function findBundledTapCommsSource(metaUrl = import.meta.url) {
|
|
962
|
+
const moduleDir = path7.dirname(fileURLToPath2(metaUrl));
|
|
963
|
+
const candidates = [
|
|
964
|
+
path7.join(moduleDir, "mcp-server.mjs"),
|
|
965
|
+
path7.join(moduleDir, "..", "mcp-server.mjs"),
|
|
966
|
+
path7.join(moduleDir, "..", "mcp-server.ts")
|
|
967
|
+
];
|
|
968
|
+
for (const candidate of candidates) {
|
|
969
|
+
if (fs7.existsSync(candidate)) return candidate;
|
|
970
|
+
}
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
function findTapCommsServerEntry(ctx, metaUrl = import.meta.url) {
|
|
974
|
+
return findBundledTapCommsSource(metaUrl) ?? findLocalTapCommsSource(ctx);
|
|
975
|
+
}
|
|
976
|
+
function findPreferredBunCommand() {
|
|
977
|
+
const home = getHomeDir();
|
|
978
|
+
const candidates = process.platform === "win32" ? [path7.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path7.join(home, ".bun", "bin", "bun"), "bun"];
|
|
979
|
+
for (const candidate of candidates) {
|
|
980
|
+
if (path7.isAbsolute(candidate) && !fs7.existsSync(candidate)) continue;
|
|
981
|
+
const result = spawnSync(candidate, ["--version"], {
|
|
982
|
+
encoding: "utf-8",
|
|
983
|
+
shell: process.platform === "win32"
|
|
984
|
+
});
|
|
985
|
+
if (result.status === 0) {
|
|
986
|
+
return path7.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
|
|
987
|
+
}
|
|
789
988
|
}
|
|
790
989
|
return null;
|
|
791
990
|
}
|
|
991
|
+
function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
992
|
+
const sourcePath = findTapCommsServerEntry(ctx);
|
|
993
|
+
const bunCommand = findPreferredBunCommand();
|
|
994
|
+
const warnings = [];
|
|
995
|
+
const issues = [];
|
|
996
|
+
const env = {
|
|
997
|
+
TAP_AGENT_NAME: "<set-per-session>",
|
|
998
|
+
TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
|
|
999
|
+
};
|
|
1000
|
+
if (instanceId) {
|
|
1001
|
+
env.TAP_AGENT_ID = instanceId;
|
|
1002
|
+
}
|
|
1003
|
+
if (!sourcePath) {
|
|
1004
|
+
issues.push(
|
|
1005
|
+
"tap-comms MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
|
|
1006
|
+
);
|
|
1007
|
+
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
1008
|
+
}
|
|
1009
|
+
const isBundled = sourcePath.endsWith(".mjs");
|
|
1010
|
+
let command = bunCommand;
|
|
1011
|
+
if (!command && isBundled) {
|
|
1012
|
+
command = process.execPath;
|
|
1013
|
+
warnings.push(
|
|
1014
|
+
"bun not found; using node to run the compiled MCP server. Install bun for better performance."
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
if (!command) {
|
|
1018
|
+
issues.push(
|
|
1019
|
+
"bun is required to run the repo-local tap-comms MCP server (.ts source). Install bun: https://bun.sh"
|
|
1020
|
+
);
|
|
1021
|
+
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
command: isBundled && command === process.execPath ? toForwardSlashPath(command) : command,
|
|
1025
|
+
args: [toForwardSlashPath(sourcePath)],
|
|
1026
|
+
env,
|
|
1027
|
+
sourcePath,
|
|
1028
|
+
warnings,
|
|
1029
|
+
issues
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/adapters/claude.ts
|
|
1034
|
+
var MCP_SERVER_KEY = "tap-comms";
|
|
1035
|
+
function findMcpJsonPath(ctx) {
|
|
1036
|
+
return path8.join(ctx.repoRoot, ".mcp.json");
|
|
1037
|
+
}
|
|
1038
|
+
function findClaudeCommand() {
|
|
1039
|
+
try {
|
|
1040
|
+
execSync2("claude --version", { stdio: "pipe" });
|
|
1041
|
+
return "claude";
|
|
1042
|
+
} catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function buildMcpServerEntry(ctx) {
|
|
1047
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
1048
|
+
if (!managed.command) return null;
|
|
1049
|
+
return {
|
|
1050
|
+
type: "stdio",
|
|
1051
|
+
command: managed.command,
|
|
1052
|
+
args: managed.args,
|
|
1053
|
+
env: managed.env
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
792
1056
|
var claudeAdapter = {
|
|
793
1057
|
runtime: "claude",
|
|
794
1058
|
async probe(ctx) {
|
|
795
1059
|
const warnings = [];
|
|
796
1060
|
const issues = [];
|
|
797
1061
|
const configPath = findMcpJsonPath(ctx);
|
|
798
|
-
const configExists =
|
|
1062
|
+
const configExists = fs8.existsSync(configPath);
|
|
799
1063
|
const runtimeCommand = findClaudeCommand();
|
|
800
1064
|
const canWrite = configExists ? (() => {
|
|
801
1065
|
try {
|
|
802
|
-
|
|
1066
|
+
fs8.accessSync(configPath, fs8.constants.W_OK);
|
|
803
1067
|
return true;
|
|
804
1068
|
} catch {
|
|
805
1069
|
return false;
|
|
@@ -810,13 +1074,10 @@ var claudeAdapter = {
|
|
|
810
1074
|
"Claude CLI not found in PATH. Config will be created but may need manual setup."
|
|
811
1075
|
);
|
|
812
1076
|
}
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
if (!fs6.existsSync(ctx.commsDir)) {
|
|
1077
|
+
const managed = buildManagedMcpServerSpec(ctx);
|
|
1078
|
+
warnings.push(...managed.warnings);
|
|
1079
|
+
issues.push(...managed.issues);
|
|
1080
|
+
if (!fs8.existsSync(ctx.commsDir)) {
|
|
820
1081
|
issues.push(
|
|
821
1082
|
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
822
1083
|
);
|
|
@@ -840,7 +1101,7 @@ var claudeAdapter = {
|
|
|
840
1101
|
const operations = [];
|
|
841
1102
|
const ownedArtifacts = [];
|
|
842
1103
|
if (probe.configExists) {
|
|
843
|
-
const raw =
|
|
1104
|
+
const raw = fs8.readFileSync(configPath, "utf-8");
|
|
844
1105
|
try {
|
|
845
1106
|
const config = JSON.parse(raw);
|
|
846
1107
|
if (config.mcpServers?.[MCP_SERVER_KEY]) {
|
|
@@ -857,7 +1118,7 @@ var claudeAdapter = {
|
|
|
857
1118
|
const serverEntry = buildMcpServerEntry(ctx);
|
|
858
1119
|
if (!serverEntry) {
|
|
859
1120
|
warnings.push(
|
|
860
|
-
"tap-comms MCP server not found
|
|
1121
|
+
"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."
|
|
861
1122
|
);
|
|
862
1123
|
return {
|
|
863
1124
|
runtime: "claude",
|
|
@@ -899,9 +1160,9 @@ var claudeAdapter = {
|
|
|
899
1160
|
try {
|
|
900
1161
|
if (op.type === "set" || op.type === "merge") {
|
|
901
1162
|
let config = {};
|
|
902
|
-
if (
|
|
1163
|
+
if (fs8.existsSync(op.path)) {
|
|
903
1164
|
backupFile(op.path, plan.backupDir);
|
|
904
|
-
const raw =
|
|
1165
|
+
const raw = fs8.readFileSync(op.path, "utf-8");
|
|
905
1166
|
try {
|
|
906
1167
|
config = JSON.parse(raw);
|
|
907
1168
|
} catch {
|
|
@@ -914,12 +1175,12 @@ var claudeAdapter = {
|
|
|
914
1175
|
setNestedKey(config, op.key, op.value);
|
|
915
1176
|
}
|
|
916
1177
|
const tmp = `${op.path}.tmp.${process.pid}`;
|
|
917
|
-
|
|
1178
|
+
fs8.writeFileSync(
|
|
918
1179
|
tmp,
|
|
919
1180
|
JSON.stringify(config, null, 2) + "\n",
|
|
920
1181
|
"utf-8"
|
|
921
1182
|
);
|
|
922
|
-
|
|
1183
|
+
fs8.renameSync(tmp, op.path);
|
|
923
1184
|
changedFiles.push(op.path);
|
|
924
1185
|
appliedOps++;
|
|
925
1186
|
}
|
|
@@ -948,12 +1209,12 @@ var claudeAdapter = {
|
|
|
948
1209
|
if (configPath) {
|
|
949
1210
|
checks.push({
|
|
950
1211
|
name: "Config file exists",
|
|
951
|
-
passed:
|
|
952
|
-
message:
|
|
1212
|
+
passed: fs8.existsSync(configPath),
|
|
1213
|
+
message: fs8.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
953
1214
|
});
|
|
954
|
-
if (
|
|
1215
|
+
if (fs8.existsSync(configPath)) {
|
|
955
1216
|
try {
|
|
956
|
-
const raw =
|
|
1217
|
+
const raw = fs8.readFileSync(configPath, "utf-8");
|
|
957
1218
|
const config = JSON.parse(raw);
|
|
958
1219
|
checks.push({ name: "Config is valid JSON", passed: true });
|
|
959
1220
|
const entry = config.mcpServers?.[MCP_SERVER_KEY];
|
|
@@ -963,7 +1224,7 @@ var claudeAdapter = {
|
|
|
963
1224
|
message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
|
|
964
1225
|
});
|
|
965
1226
|
if (entry) {
|
|
966
|
-
const hasCommsDir = entry.env?.TAP_COMMS_DIR === ctx.commsDir;
|
|
1227
|
+
const hasCommsDir = normalizeTapCommsDir(entry.env?.TAP_COMMS_DIR) === normalizeTapCommsDir(ctx.commsDir);
|
|
967
1228
|
checks.push({
|
|
968
1229
|
name: "TAP_COMMS_DIR configured",
|
|
969
1230
|
passed: hasCommsDir,
|
|
@@ -981,8 +1242,8 @@ var claudeAdapter = {
|
|
|
981
1242
|
}
|
|
982
1243
|
checks.push({
|
|
983
1244
|
name: "Comms directory exists",
|
|
984
|
-
passed:
|
|
985
|
-
message:
|
|
1245
|
+
passed: fs8.existsSync(ctx.commsDir),
|
|
1246
|
+
message: fs8.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
986
1247
|
});
|
|
987
1248
|
const cmd = findClaudeCommand();
|
|
988
1249
|
checks.push({
|
|
@@ -1014,162 +1275,50 @@ function setNestedKey(obj, keyPath, value) {
|
|
|
1014
1275
|
}
|
|
1015
1276
|
current[keys[keys.length - 1]] = value;
|
|
1016
1277
|
}
|
|
1278
|
+
function normalizeTapCommsDir(value) {
|
|
1279
|
+
return typeof value === "string" ? path8.resolve(value).replace(/\\/g, "/") : "";
|
|
1280
|
+
}
|
|
1017
1281
|
|
|
1018
1282
|
// src/adapters/codex.ts
|
|
1019
|
-
import * as
|
|
1020
|
-
import * as
|
|
1021
|
-
import { fileURLToPath } from "url";
|
|
1283
|
+
import * as fs10 from "fs";
|
|
1284
|
+
import * as path10 from "path";
|
|
1285
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1022
1286
|
|
|
1023
1287
|
// src/artifact-backups.ts
|
|
1024
1288
|
import * as crypto2 from "crypto";
|
|
1025
|
-
import * as
|
|
1026
|
-
import * as
|
|
1289
|
+
import * as fs9 from "fs";
|
|
1290
|
+
import * as path9 from "path";
|
|
1027
1291
|
function selectorHash(selector) {
|
|
1028
1292
|
return crypto2.createHash("sha256").update(selector).digest("hex").slice(0, 12);
|
|
1029
1293
|
}
|
|
1030
1294
|
function artifactBackupPath(backupDir, kind, selector) {
|
|
1031
1295
|
const safeKind = kind.replace(/[^a-z-]/gi, "-");
|
|
1032
|
-
return
|
|
1296
|
+
return path9.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
|
|
1033
1297
|
}
|
|
1034
1298
|
function writeArtifactBackup(backupPath, payload) {
|
|
1035
|
-
|
|
1299
|
+
fs9.mkdirSync(path9.dirname(backupPath), { recursive: true });
|
|
1036
1300
|
const tmp = `${backupPath}.tmp.${process.pid}`;
|
|
1037
|
-
|
|
1038
|
-
|
|
1301
|
+
fs9.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
1302
|
+
fs9.renameSync(tmp, backupPath);
|
|
1039
1303
|
}
|
|
1040
1304
|
function readArtifactBackup(backupPath) {
|
|
1041
|
-
if (!
|
|
1305
|
+
if (!fs9.existsSync(backupPath)) return null;
|
|
1042
1306
|
try {
|
|
1043
|
-
const raw =
|
|
1307
|
+
const raw = fs9.readFileSync(backupPath, "utf-8");
|
|
1044
1308
|
return JSON.parse(raw);
|
|
1045
1309
|
} catch {
|
|
1046
1310
|
return null;
|
|
1047
1311
|
}
|
|
1048
1312
|
}
|
|
1049
1313
|
|
|
1050
|
-
// src/adapters/common.ts
|
|
1051
|
-
import * as fs8 from "fs";
|
|
1052
|
-
import * as os2 from "os";
|
|
1053
|
-
import * as path8 from "path";
|
|
1054
|
-
import { spawnSync } from "child_process";
|
|
1055
|
-
function probeCommand(candidates) {
|
|
1056
|
-
for (const candidate of candidates) {
|
|
1057
|
-
const result = spawnSync(candidate, ["--version"], {
|
|
1058
|
-
encoding: "utf-8",
|
|
1059
|
-
shell: process.platform === "win32"
|
|
1060
|
-
});
|
|
1061
|
-
if (result.status === 0) {
|
|
1062
|
-
const version2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || null;
|
|
1063
|
-
return { command: candidate, version: version2 };
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
return { command: null, version: null };
|
|
1067
|
-
}
|
|
1068
|
-
function getHomeDir() {
|
|
1069
|
-
return os2.homedir();
|
|
1070
|
-
}
|
|
1071
|
-
function toForwardSlashPath(filePath) {
|
|
1072
|
-
return path8.resolve(filePath).replace(/\\/g, "/");
|
|
1073
|
-
}
|
|
1074
|
-
function canWriteOrCreate(filePath) {
|
|
1075
|
-
try {
|
|
1076
|
-
if (fs8.existsSync(filePath)) {
|
|
1077
|
-
fs8.accessSync(filePath, fs8.constants.W_OK);
|
|
1078
|
-
return true;
|
|
1079
|
-
}
|
|
1080
|
-
const parent = path8.dirname(filePath);
|
|
1081
|
-
fs8.mkdirSync(parent, { recursive: true });
|
|
1082
|
-
fs8.accessSync(parent, fs8.constants.W_OK);
|
|
1083
|
-
return true;
|
|
1084
|
-
} catch {
|
|
1085
|
-
return false;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
function findLocalTapCommsSource(ctx) {
|
|
1089
|
-
const candidates = [
|
|
1090
|
-
path8.join(
|
|
1091
|
-
ctx.repoRoot,
|
|
1092
|
-
"packages",
|
|
1093
|
-
"tap-plugin",
|
|
1094
|
-
"channels",
|
|
1095
|
-
"tap-comms.ts"
|
|
1096
|
-
),
|
|
1097
|
-
path8.join(
|
|
1098
|
-
ctx.repoRoot,
|
|
1099
|
-
"node_modules",
|
|
1100
|
-
"@hua-labs",
|
|
1101
|
-
"tap-plugin",
|
|
1102
|
-
"channels",
|
|
1103
|
-
"tap-comms.ts"
|
|
1104
|
-
)
|
|
1105
|
-
];
|
|
1106
|
-
for (const candidate of candidates) {
|
|
1107
|
-
if (fs8.existsSync(candidate)) return candidate;
|
|
1108
|
-
}
|
|
1109
|
-
return null;
|
|
1110
|
-
}
|
|
1111
|
-
function findPreferredBunCommand() {
|
|
1112
|
-
const home = getHomeDir();
|
|
1113
|
-
const candidates = process.platform === "win32" ? [path8.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path8.join(home, ".bun", "bin", "bun"), "bun"];
|
|
1114
|
-
for (const candidate of candidates) {
|
|
1115
|
-
if (path8.isAbsolute(candidate) && !fs8.existsSync(candidate)) continue;
|
|
1116
|
-
const result = spawnSync(candidate, ["--version"], {
|
|
1117
|
-
encoding: "utf-8",
|
|
1118
|
-
shell: process.platform === "win32"
|
|
1119
|
-
});
|
|
1120
|
-
if (result.status === 0) {
|
|
1121
|
-
return path8.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
return null;
|
|
1125
|
-
}
|
|
1126
|
-
function buildManagedMcpServerSpec(ctx) {
|
|
1127
|
-
const sourcePath = findLocalTapCommsSource(ctx);
|
|
1128
|
-
const bunCommand = findPreferredBunCommand();
|
|
1129
|
-
const warnings = [];
|
|
1130
|
-
const issues = [];
|
|
1131
|
-
if (sourcePath && bunCommand) {
|
|
1132
|
-
return {
|
|
1133
|
-
command: bunCommand,
|
|
1134
|
-
args: [toForwardSlashPath(sourcePath)],
|
|
1135
|
-
env: {
|
|
1136
|
-
TAP_AGENT_NAME: "<set-per-session>",
|
|
1137
|
-
TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
|
|
1138
|
-
},
|
|
1139
|
-
sourcePath,
|
|
1140
|
-
warnings,
|
|
1141
|
-
issues
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
if (!sourcePath) {
|
|
1145
|
-
issues.push(
|
|
1146
|
-
"tap-comms MCP server source not found. v1 requires a repo-local tap-plugin/channels installation."
|
|
1147
|
-
);
|
|
1148
|
-
}
|
|
1149
|
-
if (!bunCommand) {
|
|
1150
|
-
issues.push("bun is required to run the repo-local tap-comms MCP server.");
|
|
1151
|
-
}
|
|
1152
|
-
return {
|
|
1153
|
-
command: null,
|
|
1154
|
-
args: [],
|
|
1155
|
-
env: {
|
|
1156
|
-
TAP_AGENT_NAME: "<set-per-session>",
|
|
1157
|
-
TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
|
|
1158
|
-
},
|
|
1159
|
-
sourcePath,
|
|
1160
|
-
warnings,
|
|
1161
|
-
issues
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
1314
|
// src/adapters/codex.ts
|
|
1166
1315
|
var MCP_SELECTOR = "mcp_servers.tap-comms";
|
|
1167
1316
|
var ENV_SELECTOR = "mcp_servers.tap-comms.env";
|
|
1168
1317
|
function findCodexConfigPath2() {
|
|
1169
|
-
return
|
|
1318
|
+
return path10.join(getHomeDir(), ".codex", "config.toml");
|
|
1170
1319
|
}
|
|
1171
1320
|
function canonicalizeTrustPath2(targetPath) {
|
|
1172
|
-
let resolved =
|
|
1321
|
+
let resolved = path10.resolve(targetPath).replace(/\//g, "\\");
|
|
1173
1322
|
const driveRoot = /^[A-Za-z]:\\$/;
|
|
1174
1323
|
if (!driveRoot.test(resolved)) {
|
|
1175
1324
|
resolved = resolved.replace(/\\+$/g, "");
|
|
@@ -1181,7 +1330,7 @@ function trustSelector(targetPath) {
|
|
|
1181
1330
|
}
|
|
1182
1331
|
function getTrustTargets(ctx) {
|
|
1183
1332
|
const targets = [ctx.repoRoot, process.cwd()];
|
|
1184
|
-
return [...new Set(targets.map((value) =>
|
|
1333
|
+
return [...new Set(targets.map((value) => path10.resolve(value)))];
|
|
1185
1334
|
}
|
|
1186
1335
|
function buildManagedArtifacts(configPath, ctx) {
|
|
1187
1336
|
const artifacts = [
|
|
@@ -1198,14 +1347,14 @@ function buildManagedArtifacts(configPath, ctx) {
|
|
|
1198
1347
|
return artifacts;
|
|
1199
1348
|
}
|
|
1200
1349
|
function readConfigOrEmpty(configPath) {
|
|
1201
|
-
if (!
|
|
1202
|
-
return
|
|
1350
|
+
if (!fs10.existsSync(configPath)) return "";
|
|
1351
|
+
return fs10.readFileSync(configPath, "utf-8");
|
|
1203
1352
|
}
|
|
1204
1353
|
function writeTomlFile(filePath, content) {
|
|
1205
|
-
|
|
1354
|
+
fs10.mkdirSync(path10.dirname(filePath), { recursive: true });
|
|
1206
1355
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
1207
|
-
|
|
1208
|
-
|
|
1356
|
+
fs10.writeFileSync(tmp, content, "utf-8");
|
|
1357
|
+
fs10.renameSync(tmp, filePath);
|
|
1209
1358
|
}
|
|
1210
1359
|
function verifyManagedToml(content, ctx, configPath) {
|
|
1211
1360
|
const checks = [];
|
|
@@ -1214,8 +1363,8 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
1214
1363
|
const envTable = extractTomlTable(content, ENV_SELECTOR);
|
|
1215
1364
|
checks.push({
|
|
1216
1365
|
name: "Codex config exists",
|
|
1217
|
-
passed:
|
|
1218
|
-
message:
|
|
1366
|
+
passed: fs10.existsSync(configPath),
|
|
1367
|
+
message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
1219
1368
|
});
|
|
1220
1369
|
checks.push({
|
|
1221
1370
|
name: "tap-comms MCP table present",
|
|
@@ -1255,7 +1404,7 @@ var codexAdapter = {
|
|
|
1255
1404
|
const warnings = [];
|
|
1256
1405
|
const issues = [];
|
|
1257
1406
|
const configPath = findCodexConfigPath2();
|
|
1258
|
-
const configExists =
|
|
1407
|
+
const configExists = fs10.existsSync(configPath);
|
|
1259
1408
|
const runtimeProbe = probeCommand(
|
|
1260
1409
|
ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
|
|
1261
1410
|
);
|
|
@@ -1264,7 +1413,7 @@ var codexAdapter = {
|
|
|
1264
1413
|
"Codex CLI not found in PATH. Config can still be written, but runtime verification will be limited."
|
|
1265
1414
|
);
|
|
1266
1415
|
}
|
|
1267
|
-
if (!
|
|
1416
|
+
if (!fs10.existsSync(ctx.commsDir)) {
|
|
1268
1417
|
issues.push(
|
|
1269
1418
|
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
1270
1419
|
);
|
|
@@ -1325,7 +1474,7 @@ var codexAdapter = {
|
|
|
1325
1474
|
const configPath = plan.operations[0]?.path ?? findCodexConfigPath2();
|
|
1326
1475
|
const warnings = [];
|
|
1327
1476
|
const changedFiles = [];
|
|
1328
|
-
const managed = buildManagedMcpServerSpec(ctx);
|
|
1477
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
1329
1478
|
warnings.push(...managed.warnings);
|
|
1330
1479
|
if (managed.issues.length > 0 || !managed.command) {
|
|
1331
1480
|
return {
|
|
@@ -1340,7 +1489,7 @@ var codexAdapter = {
|
|
|
1340
1489
|
};
|
|
1341
1490
|
}
|
|
1342
1491
|
const existingContent = readConfigOrEmpty(configPath);
|
|
1343
|
-
if (
|
|
1492
|
+
if (fs10.existsSync(configPath) && existingContent) {
|
|
1344
1493
|
backupFile(configPath, plan.backupDir);
|
|
1345
1494
|
}
|
|
1346
1495
|
const artifactsWithBackups = plan.ownedArtifacts.map((artifact) => {
|
|
@@ -1415,8 +1564,8 @@ var codexAdapter = {
|
|
|
1415
1564
|
const checks = verifyManagedToml(content, ctx, configPath);
|
|
1416
1565
|
checks.push({
|
|
1417
1566
|
name: "Comms directory exists",
|
|
1418
|
-
passed:
|
|
1419
|
-
message:
|
|
1567
|
+
passed: fs10.existsSync(ctx.commsDir),
|
|
1568
|
+
message: fs10.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
1420
1569
|
});
|
|
1421
1570
|
checks.push({
|
|
1422
1571
|
name: "Codex CLI found",
|
|
@@ -1439,12 +1588,12 @@ var codexAdapter = {
|
|
|
1439
1588
|
return "app-server";
|
|
1440
1589
|
},
|
|
1441
1590
|
resolveBridgeScript(ctx) {
|
|
1442
|
-
const distDir =
|
|
1591
|
+
const distDir = path10.dirname(fileURLToPath3(import.meta.url));
|
|
1443
1592
|
const candidates = [
|
|
1444
1593
|
// 1. Relative to bundled CLI (npm install / npx)
|
|
1445
|
-
|
|
1594
|
+
path10.join(distDir, "bridges", "codex-bridge-runner.mjs"),
|
|
1446
1595
|
// 2. Monorepo development — dist inside repo
|
|
1447
|
-
|
|
1596
|
+
path10.join(
|
|
1448
1597
|
ctx.repoRoot,
|
|
1449
1598
|
"packages",
|
|
1450
1599
|
"tap-comms",
|
|
@@ -1453,7 +1602,7 @@ var codexAdapter = {
|
|
|
1453
1602
|
"codex-bridge-runner.mjs"
|
|
1454
1603
|
),
|
|
1455
1604
|
// 3. Source file — dev mode with strip-types
|
|
1456
|
-
|
|
1605
|
+
path10.join(
|
|
1457
1606
|
ctx.repoRoot,
|
|
1458
1607
|
"packages",
|
|
1459
1608
|
"tap-comms",
|
|
@@ -1463,30 +1612,30 @@ var codexAdapter = {
|
|
|
1463
1612
|
)
|
|
1464
1613
|
];
|
|
1465
1614
|
for (const candidate of candidates) {
|
|
1466
|
-
if (
|
|
1615
|
+
if (fs10.existsSync(candidate)) return candidate;
|
|
1467
1616
|
}
|
|
1468
1617
|
return null;
|
|
1469
1618
|
}
|
|
1470
1619
|
};
|
|
1471
1620
|
|
|
1472
1621
|
// src/adapters/gemini.ts
|
|
1473
|
-
import * as
|
|
1474
|
-
import * as
|
|
1622
|
+
import * as fs11 from "fs";
|
|
1623
|
+
import * as path11 from "path";
|
|
1475
1624
|
var GEMINI_SELECTOR = "mcpServers.tap-comms";
|
|
1476
1625
|
function candidateConfigPaths(ctx) {
|
|
1477
1626
|
const home = getHomeDir();
|
|
1478
1627
|
return [
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1628
|
+
path11.join(ctx.repoRoot, ".gemini", "settings.json"),
|
|
1629
|
+
path11.join(home, ".gemini", "settings.json"),
|
|
1630
|
+
path11.join(home, ".gemini", "antigravity", "mcp_config.json")
|
|
1482
1631
|
];
|
|
1483
1632
|
}
|
|
1484
1633
|
function chooseGeminiConfigPath(ctx) {
|
|
1485
1634
|
const [workspaceConfig, homeConfig, antigravityConfig] = candidateConfigPaths(ctx);
|
|
1486
|
-
if (
|
|
1487
|
-
if (
|
|
1488
|
-
if (
|
|
1489
|
-
const raw =
|
|
1635
|
+
if (fs11.existsSync(workspaceConfig)) return workspaceConfig;
|
|
1636
|
+
if (fs11.existsSync(homeConfig)) return homeConfig;
|
|
1637
|
+
if (fs11.existsSync(antigravityConfig)) {
|
|
1638
|
+
const raw = fs11.readFileSync(antigravityConfig, "utf-8").trim();
|
|
1490
1639
|
if (raw) {
|
|
1491
1640
|
try {
|
|
1492
1641
|
JSON.parse(raw);
|
|
@@ -1498,8 +1647,8 @@ function chooseGeminiConfigPath(ctx) {
|
|
|
1498
1647
|
return workspaceConfig;
|
|
1499
1648
|
}
|
|
1500
1649
|
function readJsonFile(filePath) {
|
|
1501
|
-
if (!
|
|
1502
|
-
const raw =
|
|
1650
|
+
if (!fs11.existsSync(filePath)) return {};
|
|
1651
|
+
const raw = fs11.readFileSync(filePath, "utf-8").trim();
|
|
1503
1652
|
if (!raw) return {};
|
|
1504
1653
|
return JSON.parse(raw);
|
|
1505
1654
|
}
|
|
@@ -1530,8 +1679,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
|
|
|
1530
1679
|
const entry = readNestedKey(config, GEMINI_SELECTOR);
|
|
1531
1680
|
checks.push({
|
|
1532
1681
|
name: "Gemini config exists",
|
|
1533
|
-
passed:
|
|
1534
|
-
message:
|
|
1682
|
+
passed: fs11.existsSync(configPath),
|
|
1683
|
+
message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
1535
1684
|
});
|
|
1536
1685
|
checks.push({
|
|
1537
1686
|
name: "tap-comms entry present",
|
|
@@ -1540,8 +1689,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
|
|
|
1540
1689
|
});
|
|
1541
1690
|
checks.push({
|
|
1542
1691
|
name: "Comms directory exists",
|
|
1543
|
-
passed:
|
|
1544
|
-
message:
|
|
1692
|
+
passed: fs11.existsSync(ctx.commsDir),
|
|
1693
|
+
message: fs11.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
1545
1694
|
});
|
|
1546
1695
|
if (entry?.env && typeof entry.env === "object") {
|
|
1547
1696
|
checks.push({
|
|
@@ -1558,7 +1707,7 @@ var geminiAdapter = {
|
|
|
1558
1707
|
const warnings = [];
|
|
1559
1708
|
const issues = [];
|
|
1560
1709
|
const configPath = chooseGeminiConfigPath(ctx);
|
|
1561
|
-
const configExists =
|
|
1710
|
+
const configExists = fs11.existsSync(configPath);
|
|
1562
1711
|
const runtimeProbe = probeCommand(
|
|
1563
1712
|
ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
|
|
1564
1713
|
);
|
|
@@ -1567,10 +1716,12 @@ var geminiAdapter = {
|
|
|
1567
1716
|
"Gemini CLI not found in PATH. Config can still be written, but runtime verification will be limited."
|
|
1568
1717
|
);
|
|
1569
1718
|
}
|
|
1570
|
-
if (!
|
|
1571
|
-
issues.push(
|
|
1719
|
+
if (!fs11.existsSync(ctx.commsDir)) {
|
|
1720
|
+
issues.push(
|
|
1721
|
+
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
1722
|
+
);
|
|
1572
1723
|
}
|
|
1573
|
-
const managed = buildManagedMcpServerSpec(ctx);
|
|
1724
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
1574
1725
|
warnings.push(...managed.warnings);
|
|
1575
1726
|
issues.push(...managed.issues);
|
|
1576
1727
|
return {
|
|
@@ -1599,7 +1750,9 @@ var geminiAdapter = {
|
|
|
1599
1750
|
conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
|
|
1600
1751
|
}
|
|
1601
1752
|
} catch {
|
|
1602
|
-
warnings.push(
|
|
1753
|
+
warnings.push(
|
|
1754
|
+
`${configPath} exists but is not valid JSON. It will be replaced.`
|
|
1755
|
+
);
|
|
1603
1756
|
}
|
|
1604
1757
|
}
|
|
1605
1758
|
operations.push({
|
|
@@ -1621,7 +1774,7 @@ var geminiAdapter = {
|
|
|
1621
1774
|
const configPath = plan.operations[0]?.path ?? chooseGeminiConfigPath(ctx);
|
|
1622
1775
|
const warnings = [];
|
|
1623
1776
|
const changedFiles = [];
|
|
1624
|
-
const managed = buildManagedMcpServerSpec(ctx);
|
|
1777
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
1625
1778
|
warnings.push(...managed.warnings);
|
|
1626
1779
|
if (managed.issues.length > 0 || !managed.command) {
|
|
1627
1780
|
return {
|
|
@@ -1637,20 +1790,26 @@ var geminiAdapter = {
|
|
|
1637
1790
|
}
|
|
1638
1791
|
let config = {};
|
|
1639
1792
|
let previousValue = void 0;
|
|
1640
|
-
if (
|
|
1641
|
-
if (
|
|
1793
|
+
if (fs11.existsSync(configPath)) {
|
|
1794
|
+
if (fs11.readFileSync(configPath, "utf-8").trim()) {
|
|
1642
1795
|
backupFile(configPath, plan.backupDir);
|
|
1643
1796
|
}
|
|
1644
1797
|
try {
|
|
1645
1798
|
config = readJsonFile(configPath);
|
|
1646
1799
|
} catch {
|
|
1647
|
-
warnings.push(
|
|
1800
|
+
warnings.push(
|
|
1801
|
+
`${configPath} was invalid JSON. Created backup and starting fresh.`
|
|
1802
|
+
);
|
|
1648
1803
|
config = {};
|
|
1649
1804
|
}
|
|
1650
1805
|
previousValue = readNestedKey(config, GEMINI_SELECTOR);
|
|
1651
1806
|
}
|
|
1652
1807
|
const artifact = plan.ownedArtifacts[0];
|
|
1653
|
-
const backupPath = artifactBackupPath(
|
|
1808
|
+
const backupPath = artifactBackupPath(
|
|
1809
|
+
plan.backupDir,
|
|
1810
|
+
artifact.kind,
|
|
1811
|
+
artifact.selector
|
|
1812
|
+
);
|
|
1654
1813
|
writeArtifactBackup(backupPath, {
|
|
1655
1814
|
kind: "json-path",
|
|
1656
1815
|
selector: artifact.selector,
|
|
@@ -1662,10 +1821,10 @@ var geminiAdapter = {
|
|
|
1662
1821
|
args: managed.args,
|
|
1663
1822
|
env: managed.env
|
|
1664
1823
|
});
|
|
1665
|
-
|
|
1824
|
+
fs11.mkdirSync(path11.dirname(configPath), { recursive: true });
|
|
1666
1825
|
const tmp = `${configPath}.tmp.${process.pid}`;
|
|
1667
|
-
|
|
1668
|
-
|
|
1826
|
+
fs11.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1827
|
+
fs11.renameSync(tmp, configPath);
|
|
1669
1828
|
changedFiles.push(configPath);
|
|
1670
1829
|
return {
|
|
1671
1830
|
success: true,
|
|
@@ -1736,19 +1895,22 @@ function getAdapter(runtime) {
|
|
|
1736
1895
|
}
|
|
1737
1896
|
|
|
1738
1897
|
// src/engine/bridge.ts
|
|
1739
|
-
import * as
|
|
1740
|
-
import * as
|
|
1741
|
-
import
|
|
1898
|
+
import * as fs13 from "fs";
|
|
1899
|
+
import * as net from "net";
|
|
1900
|
+
import * as path13 from "path";
|
|
1901
|
+
import { randomBytes } from "crypto";
|
|
1902
|
+
import { spawn, spawnSync as spawnSync2, execSync as execSync4 } from "child_process";
|
|
1903
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1742
1904
|
|
|
1743
1905
|
// src/runtime/resolve-node.ts
|
|
1744
|
-
import * as
|
|
1745
|
-
import * as
|
|
1746
|
-
import { execSync as
|
|
1906
|
+
import * as fs12 from "fs";
|
|
1907
|
+
import * as path12 from "path";
|
|
1908
|
+
import { execSync as execSync3 } from "child_process";
|
|
1747
1909
|
function readNodeVersion(repoRoot) {
|
|
1748
|
-
const nvFile =
|
|
1749
|
-
if (!
|
|
1910
|
+
const nvFile = path12.join(repoRoot, ".node-version");
|
|
1911
|
+
if (!fs12.existsSync(nvFile)) return null;
|
|
1750
1912
|
try {
|
|
1751
|
-
const raw =
|
|
1913
|
+
const raw = fs12.readFileSync(nvFile, "utf-8").trim();
|
|
1752
1914
|
return raw.length > 0 ? raw.replace(/^v/, "") : null;
|
|
1753
1915
|
} catch {
|
|
1754
1916
|
return null;
|
|
@@ -1758,16 +1920,16 @@ function fnmCandidateDirs() {
|
|
|
1758
1920
|
if (process.platform === "win32") {
|
|
1759
1921
|
return [
|
|
1760
1922
|
process.env.FNM_DIR,
|
|
1761
|
-
process.env.APPDATA ?
|
|
1762
|
-
process.env.LOCALAPPDATA ?
|
|
1763
|
-
process.env.USERPROFILE ?
|
|
1923
|
+
process.env.APPDATA ? path12.join(process.env.APPDATA, "fnm") : null,
|
|
1924
|
+
process.env.LOCALAPPDATA ? path12.join(process.env.LOCALAPPDATA, "fnm") : null,
|
|
1925
|
+
process.env.USERPROFILE ? path12.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
|
|
1764
1926
|
].filter(Boolean);
|
|
1765
1927
|
}
|
|
1766
1928
|
return [
|
|
1767
1929
|
process.env.FNM_DIR,
|
|
1768
|
-
process.env.HOME ?
|
|
1769
|
-
process.env.HOME ?
|
|
1770
|
-
process.env.XDG_DATA_HOME ?
|
|
1930
|
+
process.env.HOME ? path12.join(process.env.HOME, ".local", "share", "fnm") : null,
|
|
1931
|
+
process.env.HOME ? path12.join(process.env.HOME, ".fnm") : null,
|
|
1932
|
+
process.env.XDG_DATA_HOME ? path12.join(process.env.XDG_DATA_HOME, "fnm") : null
|
|
1771
1933
|
].filter(Boolean);
|
|
1772
1934
|
}
|
|
1773
1935
|
function nodeExecutableName() {
|
|
@@ -1777,16 +1939,16 @@ function probeFnmNode(desiredVersion) {
|
|
|
1777
1939
|
const dirs = fnmCandidateDirs();
|
|
1778
1940
|
const exe = nodeExecutableName();
|
|
1779
1941
|
for (const baseDir of dirs) {
|
|
1780
|
-
const candidate =
|
|
1942
|
+
const candidate = path12.join(
|
|
1781
1943
|
baseDir,
|
|
1782
1944
|
"node-versions",
|
|
1783
1945
|
`v${desiredVersion}`,
|
|
1784
1946
|
"installation",
|
|
1785
1947
|
exe
|
|
1786
1948
|
);
|
|
1787
|
-
if (!
|
|
1949
|
+
if (!fs12.existsSync(candidate)) continue;
|
|
1788
1950
|
try {
|
|
1789
|
-
const v =
|
|
1951
|
+
const v = execSync3(`"${candidate}" --version`, {
|
|
1790
1952
|
encoding: "utf-8",
|
|
1791
1953
|
timeout: 5e3
|
|
1792
1954
|
}).trim();
|
|
@@ -1800,7 +1962,7 @@ function probeFnmNode(desiredVersion) {
|
|
|
1800
1962
|
}
|
|
1801
1963
|
function detectNodeMajorVersion(command) {
|
|
1802
1964
|
try {
|
|
1803
|
-
const version2 =
|
|
1965
|
+
const version2 = execSync3(`"${command}" --version`, {
|
|
1804
1966
|
encoding: "utf-8",
|
|
1805
1967
|
timeout: 5e3
|
|
1806
1968
|
}).trim();
|
|
@@ -1814,7 +1976,7 @@ function checkStripTypesSupport(command) {
|
|
|
1814
1976
|
const major = detectNodeMajorVersion(command);
|
|
1815
1977
|
if (major !== null && major >= 22) return true;
|
|
1816
1978
|
try {
|
|
1817
|
-
|
|
1979
|
+
execSync3(`"${command}" --experimental-strip-types -e ""`, {
|
|
1818
1980
|
timeout: 5e3,
|
|
1819
1981
|
stdio: "pipe"
|
|
1820
1982
|
});
|
|
@@ -1825,12 +1987,12 @@ function checkStripTypesSupport(command) {
|
|
|
1825
1987
|
}
|
|
1826
1988
|
function findTsxFallback(repoRoot) {
|
|
1827
1989
|
const candidates = [
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1990
|
+
path12.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
|
|
1991
|
+
path12.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
|
|
1992
|
+
path12.join(repoRoot, "node_modules", ".bin", "tsx")
|
|
1831
1993
|
];
|
|
1832
1994
|
for (const c of candidates) {
|
|
1833
|
-
if (
|
|
1995
|
+
if (fs12.existsSync(c)) return c;
|
|
1834
1996
|
}
|
|
1835
1997
|
return null;
|
|
1836
1998
|
}
|
|
@@ -1839,78 +2001,852 @@ function getFnmBinDir(repoRoot) {
|
|
|
1839
2001
|
if (!desiredVersion) return null;
|
|
1840
2002
|
const nodePath = probeFnmNode(desiredVersion);
|
|
1841
2003
|
if (!nodePath) return null;
|
|
1842
|
-
return
|
|
2004
|
+
return path12.dirname(nodePath);
|
|
1843
2005
|
}
|
|
1844
2006
|
function resolveNodeRuntime(configCommand, repoRoot) {
|
|
1845
2007
|
if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
|
|
1846
2008
|
return {
|
|
1847
|
-
command: configCommand,
|
|
1848
|
-
supportsStripTypes: false,
|
|
1849
|
-
source: "bun",
|
|
1850
|
-
majorVersion: null
|
|
2009
|
+
command: configCommand,
|
|
2010
|
+
supportsStripTypes: false,
|
|
2011
|
+
source: "bun",
|
|
2012
|
+
majorVersion: null
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
const desiredVersion = readNodeVersion(repoRoot);
|
|
2016
|
+
if (desiredVersion) {
|
|
2017
|
+
const fnmNode = probeFnmNode(desiredVersion);
|
|
2018
|
+
if (fnmNode) {
|
|
2019
|
+
const major2 = detectNodeMajorVersion(fnmNode);
|
|
2020
|
+
return {
|
|
2021
|
+
command: fnmNode,
|
|
2022
|
+
supportsStripTypes: checkStripTypesSupport(fnmNode),
|
|
2023
|
+
source: "fnm",
|
|
2024
|
+
majorVersion: major2
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
const major = detectNodeMajorVersion(configCommand);
|
|
2029
|
+
if (major !== null) {
|
|
2030
|
+
return {
|
|
2031
|
+
command: configCommand,
|
|
2032
|
+
supportsStripTypes: checkStripTypesSupport(configCommand),
|
|
2033
|
+
source: major === detectNodeMajorVersion("node") ? "path" : "config",
|
|
2034
|
+
majorVersion: major
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
const tsx = findTsxFallback(repoRoot);
|
|
2038
|
+
if (tsx) {
|
|
2039
|
+
return {
|
|
2040
|
+
command: tsx,
|
|
2041
|
+
supportsStripTypes: false,
|
|
2042
|
+
source: "tsx-fallback",
|
|
2043
|
+
majorVersion: null
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
return {
|
|
2047
|
+
command: configCommand,
|
|
2048
|
+
supportsStripTypes: false,
|
|
2049
|
+
source: "path",
|
|
2050
|
+
majorVersion: null
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
|
|
2054
|
+
const fnmBin = getFnmBinDir(repoRoot);
|
|
2055
|
+
if (!fnmBin) return { ...baseEnv };
|
|
2056
|
+
const pathKey = process.platform === "win32" ? "Path" : "PATH";
|
|
2057
|
+
const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
|
|
2058
|
+
return {
|
|
2059
|
+
...baseEnv,
|
|
2060
|
+
[pathKey]: `${fnmBin}${path12.delimiter}${currentPath}`
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// src/engine/bridge.ts
|
|
2065
|
+
var DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
|
|
2066
|
+
var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
2067
|
+
var APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
2068
|
+
var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
|
|
2069
|
+
var APP_SERVER_HEALTH_RETRY_MS = 250;
|
|
2070
|
+
var APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
|
|
2071
|
+
var APP_SERVER_AUTH_FILE_MODE = 384;
|
|
2072
|
+
function appServerLogFilePath(stateDir, instanceId) {
|
|
2073
|
+
return path13.join(stateDir, "logs", `app-server-${instanceId}.log`);
|
|
2074
|
+
}
|
|
2075
|
+
function appServerGatewayLogFilePath(stateDir, instanceId) {
|
|
2076
|
+
return path13.join(stateDir, "logs", `app-server-gateway-${instanceId}.log`);
|
|
2077
|
+
}
|
|
2078
|
+
function appServerGatewayTokenFilePath(stateDir, instanceId) {
|
|
2079
|
+
return path13.join(
|
|
2080
|
+
stateDir,
|
|
2081
|
+
"secrets",
|
|
2082
|
+
`app-server-gateway-${instanceId}.token`
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
function stderrLogFilePath(logPath) {
|
|
2086
|
+
return `${logPath}.stderr`;
|
|
2087
|
+
}
|
|
2088
|
+
function writeProtectedTextFile(filePath, content) {
|
|
2089
|
+
fs13.mkdirSync(path13.dirname(filePath), { recursive: true });
|
|
2090
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
2091
|
+
fs13.writeFileSync(tmp, content, {
|
|
2092
|
+
encoding: "utf-8",
|
|
2093
|
+
mode: APP_SERVER_AUTH_FILE_MODE
|
|
2094
|
+
});
|
|
2095
|
+
fs13.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
|
|
2096
|
+
fs13.renameSync(tmp, filePath);
|
|
2097
|
+
fs13.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
|
|
2098
|
+
}
|
|
2099
|
+
function removeFileIfExists(filePath) {
|
|
2100
|
+
if (!filePath || !fs13.existsSync(filePath)) {
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
try {
|
|
2104
|
+
fs13.unlinkSync(filePath);
|
|
2105
|
+
} catch {
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function getWebSocketCtor() {
|
|
2109
|
+
const candidate = globalThis.WebSocket;
|
|
2110
|
+
return typeof candidate === "function" ? candidate : null;
|
|
2111
|
+
}
|
|
2112
|
+
function delay(ms) {
|
|
2113
|
+
return new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
2114
|
+
}
|
|
2115
|
+
function isLoopbackHost(hostname) {
|
|
2116
|
+
return hostname === "127.0.0.1" || hostname === "localhost";
|
|
2117
|
+
}
|
|
2118
|
+
function resolveCodexCommand(platform) {
|
|
2119
|
+
const candidates = platform === "win32" ? ["codex.cmd", "codex.exe", "codex", "codex.ps1"] : ["codex"];
|
|
2120
|
+
return probeCommand(candidates).command;
|
|
2121
|
+
}
|
|
2122
|
+
function formatCodexAppServerCommand(command, url) {
|
|
2123
|
+
return `${command} app-server --listen ${url}`;
|
|
2124
|
+
}
|
|
2125
|
+
function resolvePowerShellCommand() {
|
|
2126
|
+
return probeCommand(["pwsh", "powershell", "powershell.exe"]).command ?? "powershell";
|
|
2127
|
+
}
|
|
2128
|
+
function resolveAuthGatewayScript(repoRoot) {
|
|
2129
|
+
const moduleDir = path13.dirname(fileURLToPath4(import.meta.url));
|
|
2130
|
+
const candidates = [
|
|
2131
|
+
path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
|
|
2132
|
+
path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.ts"),
|
|
2133
|
+
path13.join(
|
|
2134
|
+
repoRoot,
|
|
2135
|
+
"packages",
|
|
2136
|
+
"tap-comms",
|
|
2137
|
+
"dist",
|
|
2138
|
+
"bridges",
|
|
2139
|
+
"codex-app-server-auth-gateway.mjs"
|
|
2140
|
+
),
|
|
2141
|
+
path13.join(
|
|
2142
|
+
repoRoot,
|
|
2143
|
+
"packages",
|
|
2144
|
+
"tap-comms",
|
|
2145
|
+
"src",
|
|
2146
|
+
"bridges",
|
|
2147
|
+
"codex-app-server-auth-gateway.ts"
|
|
2148
|
+
)
|
|
2149
|
+
];
|
|
2150
|
+
for (const candidate of candidates) {
|
|
2151
|
+
if (fs13.existsSync(candidate)) {
|
|
2152
|
+
return candidate;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
function getBridgeRuntimeStateDir(repoRoot, instanceId) {
|
|
2158
|
+
return path13.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`);
|
|
2159
|
+
}
|
|
2160
|
+
async function allocateLoopbackPort(hostname) {
|
|
2161
|
+
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2162
|
+
return await new Promise((resolve11, reject) => {
|
|
2163
|
+
const server = net.createServer();
|
|
2164
|
+
server.unref();
|
|
2165
|
+
server.once("error", reject);
|
|
2166
|
+
server.listen(0, bindHost, () => {
|
|
2167
|
+
const address = server.address();
|
|
2168
|
+
if (!address || typeof address === "string") {
|
|
2169
|
+
server.close(() => {
|
|
2170
|
+
reject(new Error("Failed to allocate a loopback port"));
|
|
2171
|
+
});
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const port = address.port;
|
|
2175
|
+
server.close((error) => {
|
|
2176
|
+
if (error) {
|
|
2177
|
+
reject(error);
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
resolve11(port);
|
|
2181
|
+
});
|
|
2182
|
+
});
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
function buildProtectedAppServerUrl(publicUrl, token) {
|
|
2186
|
+
const url = new URL(publicUrl);
|
|
2187
|
+
url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
|
|
2188
|
+
return url.toString().replace(/\/(?=\?|$)/, "");
|
|
2189
|
+
}
|
|
2190
|
+
function readGatewayTokenFromPath(tokenPath) {
|
|
2191
|
+
return fs13.readFileSync(tokenPath, "utf8").trim();
|
|
2192
|
+
}
|
|
2193
|
+
function readGatewayToken(auth) {
|
|
2194
|
+
if (!auth) {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
const legacyToken = auth.token;
|
|
2198
|
+
if (legacyToken?.trim()) {
|
|
2199
|
+
return legacyToken.trim();
|
|
2200
|
+
}
|
|
2201
|
+
if (!auth.tokenPath || !fs13.existsSync(auth.tokenPath)) {
|
|
2202
|
+
return null;
|
|
2203
|
+
}
|
|
2204
|
+
const fileToken = readGatewayTokenFromPath(auth.tokenPath);
|
|
2205
|
+
return fileToken || null;
|
|
2206
|
+
}
|
|
2207
|
+
function materializeGatewayTokenFile(stateDir, instanceId, publicUrl, auth) {
|
|
2208
|
+
if (auth.tokenPath && fs13.existsSync(auth.tokenPath)) {
|
|
2209
|
+
return auth;
|
|
2210
|
+
}
|
|
2211
|
+
const token = readGatewayToken(auth);
|
|
2212
|
+
if (!token) {
|
|
2213
|
+
throw new Error(`Missing auth gateway token for ${instanceId}`);
|
|
2214
|
+
}
|
|
2215
|
+
const tokenPath = appServerGatewayTokenFilePath(stateDir, instanceId);
|
|
2216
|
+
writeProtectedTextFile(tokenPath, `${token}
|
|
2217
|
+
`);
|
|
2218
|
+
return {
|
|
2219
|
+
...auth,
|
|
2220
|
+
protectedUrl: buildProtectedAppServerUrl(publicUrl, "***"),
|
|
2221
|
+
tokenPath
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
async function createManagedAppServerAuth(options) {
|
|
2225
|
+
const publicUrl = new URL(options.publicUrl);
|
|
2226
|
+
const upstreamUrl = new URL(options.publicUrl);
|
|
2227
|
+
upstreamUrl.port = String(await allocateLoopbackPort(publicUrl.hostname));
|
|
2228
|
+
upstreamUrl.search = "";
|
|
2229
|
+
upstreamUrl.hash = "";
|
|
2230
|
+
const gatewayScript = resolveAuthGatewayScript(options.repoRoot);
|
|
2231
|
+
if (!gatewayScript) {
|
|
2232
|
+
throw new Error("Auth gateway script not found");
|
|
2233
|
+
}
|
|
2234
|
+
const token = randomBytes(24).toString("base64url");
|
|
2235
|
+
const tokenPath = appServerGatewayTokenFilePath(
|
|
2236
|
+
options.stateDir,
|
|
2237
|
+
options.instanceId
|
|
2238
|
+
);
|
|
2239
|
+
writeProtectedTextFile(tokenPath, `${token}
|
|
2240
|
+
`);
|
|
2241
|
+
const protectedUrl = buildProtectedAppServerUrl(options.publicUrl, "***");
|
|
2242
|
+
const gatewayLogPath = appServerGatewayLogFilePath(
|
|
2243
|
+
options.stateDir,
|
|
2244
|
+
options.instanceId
|
|
2245
|
+
);
|
|
2246
|
+
fs13.mkdirSync(path13.dirname(gatewayLogPath), { recursive: true });
|
|
2247
|
+
rotateLog(gatewayLogPath);
|
|
2248
|
+
const runtime = resolveNodeRuntime(process.execPath, options.repoRoot);
|
|
2249
|
+
const gatewayArgs = [];
|
|
2250
|
+
if (gatewayScript.endsWith(".ts")) {
|
|
2251
|
+
if (!runtime.supportsStripTypes) {
|
|
2252
|
+
throw new Error(
|
|
2253
|
+
"Current Node runtime cannot start the auth gateway from TypeScript source. Rebuild @hua-labs/tap or use Node 22.6+."
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
gatewayArgs.push("--experimental-strip-types");
|
|
2257
|
+
}
|
|
2258
|
+
gatewayArgs.push(gatewayScript);
|
|
2259
|
+
const gatewayEnv = {
|
|
2260
|
+
...buildRuntimeEnv(options.repoRoot),
|
|
2261
|
+
TAP_GATEWAY_LISTEN_URL: options.publicUrl,
|
|
2262
|
+
TAP_GATEWAY_UPSTREAM_URL: upstreamUrl.toString().replace(/\/$/, ""),
|
|
2263
|
+
TAP_GATEWAY_TOKEN_FILE: tokenPath
|
|
2264
|
+
};
|
|
2265
|
+
let gatewayPid;
|
|
2266
|
+
{
|
|
2267
|
+
let logFd = null;
|
|
2268
|
+
try {
|
|
2269
|
+
if (options.platform === "win32") {
|
|
2270
|
+
gatewayPid = startWindowsDetachedProcess(
|
|
2271
|
+
runtime.command,
|
|
2272
|
+
gatewayArgs,
|
|
2273
|
+
options.repoRoot,
|
|
2274
|
+
gatewayLogPath,
|
|
2275
|
+
gatewayEnv
|
|
2276
|
+
);
|
|
2277
|
+
} else {
|
|
2278
|
+
logFd = fs13.openSync(gatewayLogPath, "a");
|
|
2279
|
+
const child = spawn(runtime.command, gatewayArgs, {
|
|
2280
|
+
cwd: options.repoRoot,
|
|
2281
|
+
detached: true,
|
|
2282
|
+
stdio: ["ignore", logFd, logFd],
|
|
2283
|
+
env: gatewayEnv,
|
|
2284
|
+
windowsHide: true
|
|
2285
|
+
});
|
|
2286
|
+
child.unref();
|
|
2287
|
+
gatewayPid = child.pid ?? null;
|
|
2288
|
+
}
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
removeFileIfExists(tokenPath);
|
|
2291
|
+
throw error;
|
|
2292
|
+
} finally {
|
|
2293
|
+
if (logFd != null) {
|
|
2294
|
+
fs13.closeSync(logFd);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
if (gatewayPid == null) {
|
|
2299
|
+
removeFileIfExists(tokenPath);
|
|
2300
|
+
throw new Error("Failed to spawn app-server auth gateway");
|
|
2301
|
+
}
|
|
2302
|
+
return {
|
|
2303
|
+
mode: "query-token",
|
|
2304
|
+
protectedUrl,
|
|
2305
|
+
upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
|
|
2306
|
+
tokenPath,
|
|
2307
|
+
gatewayPid,
|
|
2308
|
+
gatewayLogPath
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
function canReuseManagedAppServer(appServer) {
|
|
2312
|
+
if (!appServer?.managed) {
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
if (appServer.pid != null && !isProcessAlive(appServer.pid)) {
|
|
2316
|
+
return false;
|
|
2317
|
+
}
|
|
2318
|
+
const auth = appServer.auth;
|
|
2319
|
+
if (auth) {
|
|
2320
|
+
if (!auth.protectedUrl) {
|
|
2321
|
+
return false;
|
|
2322
|
+
}
|
|
2323
|
+
if (!readGatewayToken(auth)) {
|
|
2324
|
+
return false;
|
|
2325
|
+
}
|
|
2326
|
+
if (auth.gatewayPid != null && !isProcessAlive(auth.gatewayPid)) {
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return true;
|
|
2331
|
+
}
|
|
2332
|
+
function markAppServerHealthy(appServer) {
|
|
2333
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2334
|
+
return {
|
|
2335
|
+
...appServer,
|
|
2336
|
+
healthy: true,
|
|
2337
|
+
lastCheckedAt: checkedAt,
|
|
2338
|
+
lastHealthyAt: checkedAt
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function findReusableManagedAppServer(stateDir, publicUrl) {
|
|
2342
|
+
const pidDir = path13.join(stateDir, "pids");
|
|
2343
|
+
if (!fs13.existsSync(pidDir)) {
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
for (const name of fs13.readdirSync(pidDir)) {
|
|
2347
|
+
if (!name.startsWith("bridge-") || !name.endsWith(".json")) {
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
try {
|
|
2351
|
+
const raw = fs13.readFileSync(path13.join(pidDir, name), "utf-8");
|
|
2352
|
+
const parsed = JSON.parse(raw);
|
|
2353
|
+
if (parsed.appServer?.url !== publicUrl) {
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
if (canReuseManagedAppServer(parsed.appServer)) {
|
|
2357
|
+
return markAppServerHealthy(parsed.appServer);
|
|
2358
|
+
}
|
|
2359
|
+
} catch {
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return null;
|
|
2363
|
+
}
|
|
2364
|
+
function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
|
|
2365
|
+
const ext = path13.extname(command).toLowerCase();
|
|
2366
|
+
const stderrLogPath = stderrLogFilePath(logPath);
|
|
2367
|
+
const stdoutFd = fs13.openSync(logPath, "a");
|
|
2368
|
+
const stderrFd = fs13.openSync(stderrLogPath, "a");
|
|
2369
|
+
try {
|
|
2370
|
+
const child = ext === ".ps1" ? spawn(
|
|
2371
|
+
resolvePowerShellCommand(),
|
|
2372
|
+
["-NoLogo", "-NoProfile", "-File", command, ...args],
|
|
2373
|
+
{
|
|
2374
|
+
cwd: repoRoot,
|
|
2375
|
+
detached: true,
|
|
2376
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
2377
|
+
env,
|
|
2378
|
+
windowsHide: true
|
|
2379
|
+
}
|
|
2380
|
+
) : spawn(command, args, {
|
|
2381
|
+
cwd: repoRoot,
|
|
2382
|
+
detached: true,
|
|
2383
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
2384
|
+
env,
|
|
2385
|
+
windowsHide: true,
|
|
2386
|
+
shell: ext === ".cmd" || ext === ".bat"
|
|
2387
|
+
});
|
|
2388
|
+
child.unref();
|
|
2389
|
+
return child.pid ?? null;
|
|
2390
|
+
} finally {
|
|
2391
|
+
fs13.closeSync(stdoutFd);
|
|
2392
|
+
fs13.closeSync(stderrFd);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
|
|
2396
|
+
return startWindowsDetachedProcess(
|
|
2397
|
+
command,
|
|
2398
|
+
["app-server", "--listen", url],
|
|
2399
|
+
repoRoot,
|
|
2400
|
+
logPath
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
function findListeningProcessId(url, platform) {
|
|
2404
|
+
if (platform !== "win32") {
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
let port;
|
|
2408
|
+
try {
|
|
2409
|
+
const parsed = new URL(url);
|
|
2410
|
+
port = parsed.port ? Number.parseInt(parsed.port, 10) : null;
|
|
2411
|
+
} catch {
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
if (port == null || !Number.isFinite(port)) {
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
const result = spawnSync2(
|
|
2418
|
+
resolvePowerShellCommand(),
|
|
2419
|
+
[
|
|
2420
|
+
"-NoLogo",
|
|
2421
|
+
"-NoProfile",
|
|
2422
|
+
"-Command",
|
|
2423
|
+
[
|
|
2424
|
+
`$port = ${port}`,
|
|
2425
|
+
"$processId = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess",
|
|
2426
|
+
"if ($processId) { $processId }"
|
|
2427
|
+
].join("; ")
|
|
2428
|
+
],
|
|
2429
|
+
{
|
|
2430
|
+
encoding: "utf-8",
|
|
2431
|
+
windowsHide: true
|
|
2432
|
+
}
|
|
2433
|
+
);
|
|
2434
|
+
if (result.status !== 0) {
|
|
2435
|
+
return null;
|
|
2436
|
+
}
|
|
2437
|
+
const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
|
|
2438
|
+
return Number.isFinite(parsedPid) ? parsedPid : null;
|
|
2439
|
+
}
|
|
2440
|
+
function resolveAppServerUrl(baseUrl, port) {
|
|
2441
|
+
const resolvedBase = (baseUrl ?? DEFAULT_APP_SERVER_URL2).replace(/\/$/, "");
|
|
2442
|
+
if (port == null) {
|
|
2443
|
+
return resolvedBase;
|
|
2444
|
+
}
|
|
2445
|
+
try {
|
|
2446
|
+
const parsed = new URL(resolvedBase);
|
|
2447
|
+
parsed.port = String(port);
|
|
2448
|
+
return parsed.toString().replace(/\/$/, "");
|
|
2449
|
+
} catch {
|
|
2450
|
+
return resolvedBase;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
async function isTcpPortAvailable(hostname, port) {
|
|
2454
|
+
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2455
|
+
return await new Promise((resolve11) => {
|
|
2456
|
+
const server = net.createServer();
|
|
2457
|
+
server.unref();
|
|
2458
|
+
server.once("error", () => resolve11(false));
|
|
2459
|
+
server.listen(port, bindHost, () => {
|
|
2460
|
+
server.close((error) => resolve11(!error));
|
|
2461
|
+
});
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, excludeInstanceId) {
|
|
2465
|
+
let hostname = "127.0.0.1";
|
|
2466
|
+
try {
|
|
2467
|
+
hostname = new URL(baseUrl ?? DEFAULT_APP_SERVER_URL2).hostname;
|
|
2468
|
+
} catch {
|
|
2469
|
+
}
|
|
2470
|
+
const maxAttempts = 1e3;
|
|
2471
|
+
let port = basePort;
|
|
2472
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1, port += 1) {
|
|
2473
|
+
const claimedInState = Object.entries(state.instances).some(
|
|
2474
|
+
([id, inst]) => id !== excludeInstanceId && inst.port === port
|
|
2475
|
+
);
|
|
2476
|
+
if (claimedInState) {
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
if (!isLoopbackHost(hostname)) {
|
|
2480
|
+
return port;
|
|
2481
|
+
}
|
|
2482
|
+
if (await isTcpPortAvailable(hostname, port)) {
|
|
2483
|
+
return port;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
throw new Error(
|
|
2487
|
+
`Failed to find a free app-server port starting at ${basePort}`
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
2491
|
+
const WebSocket = getWebSocketCtor();
|
|
2492
|
+
if (!WebSocket) {
|
|
2493
|
+
return false;
|
|
2494
|
+
}
|
|
2495
|
+
return new Promise((resolve11) => {
|
|
2496
|
+
let settled = false;
|
|
2497
|
+
let socket = null;
|
|
2498
|
+
const finish = (healthy) => {
|
|
2499
|
+
if (settled) {
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
settled = true;
|
|
2503
|
+
clearTimeout(timer);
|
|
2504
|
+
try {
|
|
2505
|
+
socket?.close();
|
|
2506
|
+
} catch {
|
|
2507
|
+
}
|
|
2508
|
+
resolve11(healthy);
|
|
2509
|
+
};
|
|
2510
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2511
|
+
try {
|
|
2512
|
+
socket = new WebSocket(url);
|
|
2513
|
+
socket.addEventListener("open", () => finish(true), { once: true });
|
|
2514
|
+
socket.addEventListener("error", () => finish(false), { once: true });
|
|
2515
|
+
socket.addEventListener("close", () => finish(false), { once: true });
|
|
2516
|
+
} catch {
|
|
2517
|
+
finish(false);
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
async function waitForAppServerHealth(url, timeoutMs) {
|
|
2522
|
+
const deadline = Date.now() + timeoutMs;
|
|
2523
|
+
while (Date.now() < deadline) {
|
|
2524
|
+
if (await checkAppServerHealth(url)) {
|
|
2525
|
+
return true;
|
|
2526
|
+
}
|
|
2527
|
+
await delay(APP_SERVER_HEALTH_RETRY_MS);
|
|
2528
|
+
}
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
async function terminateProcess(pid, platform) {
|
|
2532
|
+
if (!isProcessAlive(pid)) {
|
|
2533
|
+
return false;
|
|
2534
|
+
}
|
|
2535
|
+
try {
|
|
2536
|
+
if (platform === "win32") {
|
|
2537
|
+
execSync4(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
|
|
2538
|
+
} else {
|
|
2539
|
+
process.kill(pid, "SIGTERM");
|
|
2540
|
+
await delay(2e3);
|
|
2541
|
+
if (isProcessAlive(pid)) {
|
|
2542
|
+
process.kill(pid, "SIGKILL");
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
} catch {
|
|
2546
|
+
}
|
|
2547
|
+
return !isProcessAlive(pid);
|
|
2548
|
+
}
|
|
2549
|
+
async function stopManagedAppServer(appServer, platform) {
|
|
2550
|
+
if (!appServer.managed) {
|
|
2551
|
+
return false;
|
|
2552
|
+
}
|
|
2553
|
+
let stopped = false;
|
|
2554
|
+
if (appServer.auth?.gatewayPid != null) {
|
|
2555
|
+
stopped = await terminateProcess(appServer.auth.gatewayPid, platform) || stopped;
|
|
2556
|
+
}
|
|
2557
|
+
if (appServer.pid != null) {
|
|
2558
|
+
stopped = await terminateProcess(appServer.pid, platform) || stopped;
|
|
2559
|
+
}
|
|
2560
|
+
removeFileIfExists(appServer.auth?.tokenPath);
|
|
2561
|
+
return stopped;
|
|
2562
|
+
}
|
|
2563
|
+
async function ensureCodexAppServer(options) {
|
|
2564
|
+
const effectiveUrl = resolveAppServerUrl(options.appServerUrl);
|
|
2565
|
+
const fallbackManualCommand = formatCodexAppServerCommand(
|
|
2566
|
+
"codex",
|
|
2567
|
+
effectiveUrl
|
|
2568
|
+
);
|
|
2569
|
+
if (options.existingAppServer?.url === effectiveUrl && canReuseManagedAppServer(options.existingAppServer)) {
|
|
2570
|
+
return markAppServerHealthy(options.existingAppServer);
|
|
2571
|
+
}
|
|
2572
|
+
const sharedManaged = findReusableManagedAppServer(
|
|
2573
|
+
options.stateDir,
|
|
2574
|
+
effectiveUrl
|
|
2575
|
+
);
|
|
2576
|
+
if (sharedManaged) {
|
|
2577
|
+
return sharedManaged;
|
|
2578
|
+
}
|
|
2579
|
+
let parsedUrl;
|
|
2580
|
+
try {
|
|
2581
|
+
parsedUrl = new URL(effectiveUrl);
|
|
2582
|
+
} catch {
|
|
2583
|
+
throw new Error(
|
|
2584
|
+
`Invalid app-server URL: ${effectiveUrl}
|
|
2585
|
+
Start it manually:
|
|
2586
|
+
${fallbackManualCommand}`
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
if (!isLoopbackHost(parsedUrl.hostname)) {
|
|
2590
|
+
throw new Error(
|
|
2591
|
+
`Auto-start only supports loopback app-server URLs. Current URL: ${effectiveUrl}
|
|
2592
|
+
Start it manually:
|
|
2593
|
+
${fallbackManualCommand}`
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
if (await checkAppServerHealth(effectiveUrl)) {
|
|
2597
|
+
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.";
|
|
2598
|
+
throw new Error(`${effectiveUrl}: ${hint}`);
|
|
2599
|
+
}
|
|
2600
|
+
const resolvedCommand = resolveCodexCommand(options.platform);
|
|
2601
|
+
if (!resolvedCommand) {
|
|
2602
|
+
throw new Error(
|
|
2603
|
+
`Codex CLI not found in PATH.
|
|
2604
|
+
Start the app-server manually:
|
|
2605
|
+
${fallbackManualCommand}`
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
const logPath = appServerLogFilePath(options.stateDir, options.instanceId);
|
|
2609
|
+
fs13.mkdirSync(path13.dirname(logPath), { recursive: true });
|
|
2610
|
+
rotateLog(logPath);
|
|
2611
|
+
if (options.noAuth) {
|
|
2612
|
+
const manualCommand2 = formatCodexAppServerCommand("codex", effectiveUrl);
|
|
2613
|
+
let pid2;
|
|
2614
|
+
if (options.platform === "win32") {
|
|
2615
|
+
try {
|
|
2616
|
+
pid2 = startWindowsCodexAppServer(
|
|
2617
|
+
resolvedCommand,
|
|
2618
|
+
effectiveUrl,
|
|
2619
|
+
options.repoRoot,
|
|
2620
|
+
logPath
|
|
2621
|
+
);
|
|
2622
|
+
} catch (err) {
|
|
2623
|
+
throw new Error(
|
|
2624
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
2625
|
+
Start it manually:
|
|
2626
|
+
${manualCommand2}`,
|
|
2627
|
+
{ cause: err }
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
} else {
|
|
2631
|
+
const logFd = fs13.openSync(logPath, "a");
|
|
2632
|
+
try {
|
|
2633
|
+
const child = spawn(
|
|
2634
|
+
resolvedCommand,
|
|
2635
|
+
["app-server", "--listen", effectiveUrl],
|
|
2636
|
+
{
|
|
2637
|
+
cwd: options.repoRoot,
|
|
2638
|
+
detached: true,
|
|
2639
|
+
stdio: ["ignore", logFd, logFd],
|
|
2640
|
+
env: process.env,
|
|
2641
|
+
windowsHide: true
|
|
2642
|
+
}
|
|
2643
|
+
);
|
|
2644
|
+
child.unref();
|
|
2645
|
+
pid2 = child.pid ?? null;
|
|
2646
|
+
} catch (err) {
|
|
2647
|
+
throw new Error(
|
|
2648
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
2649
|
+
Start it manually:
|
|
2650
|
+
${manualCommand2}`,
|
|
2651
|
+
{ cause: err }
|
|
2652
|
+
);
|
|
2653
|
+
} finally {
|
|
2654
|
+
fs13.closeSync(logFd);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
if (pid2 == null) {
|
|
2658
|
+
throw new Error(
|
|
2659
|
+
`Failed to spawn Codex app-server.
|
|
2660
|
+
Start it manually:
|
|
2661
|
+
${manualCommand2}`
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
const healthy2 = await waitForAppServerHealth(
|
|
2665
|
+
effectiveUrl,
|
|
2666
|
+
APP_SERVER_START_TIMEOUT_MS
|
|
2667
|
+
);
|
|
2668
|
+
if (!healthy2) {
|
|
2669
|
+
await terminateProcess(pid2, options.platform);
|
|
2670
|
+
throw new Error(
|
|
2671
|
+
`Codex app-server did not become healthy at ${effectiveUrl}.
|
|
2672
|
+
Check ${logPath}
|
|
2673
|
+
Or start it manually:
|
|
2674
|
+
${manualCommand2}`
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
pid2 = findListeningProcessId(effectiveUrl, options.platform) ?? pid2;
|
|
2678
|
+
const healthyAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2679
|
+
return {
|
|
2680
|
+
url: effectiveUrl,
|
|
2681
|
+
pid: pid2,
|
|
2682
|
+
managed: true,
|
|
2683
|
+
healthy: true,
|
|
2684
|
+
lastCheckedAt: healthyAt2,
|
|
2685
|
+
lastHealthyAt: healthyAt2,
|
|
2686
|
+
logPath,
|
|
2687
|
+
manualCommand: manualCommand2,
|
|
2688
|
+
auth: null
|
|
1851
2689
|
};
|
|
1852
2690
|
}
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2691
|
+
const auth = await createManagedAppServerAuth({
|
|
2692
|
+
instanceId: options.instanceId,
|
|
2693
|
+
stateDir: options.stateDir,
|
|
2694
|
+
repoRoot: options.repoRoot,
|
|
2695
|
+
platform: options.platform,
|
|
2696
|
+
publicUrl: effectiveUrl
|
|
2697
|
+
});
|
|
2698
|
+
const manualCommand = formatCodexAppServerCommand("codex", auth.upstreamUrl);
|
|
2699
|
+
let pid;
|
|
2700
|
+
if (options.platform === "win32") {
|
|
2701
|
+
try {
|
|
2702
|
+
pid = startWindowsCodexAppServer(
|
|
2703
|
+
resolvedCommand,
|
|
2704
|
+
auth.upstreamUrl,
|
|
2705
|
+
options.repoRoot,
|
|
2706
|
+
logPath
|
|
2707
|
+
);
|
|
2708
|
+
} catch (err) {
|
|
2709
|
+
if (auth.gatewayPid != null) {
|
|
2710
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2711
|
+
}
|
|
2712
|
+
removeFileIfExists(auth.tokenPath);
|
|
2713
|
+
throw new Error(
|
|
2714
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
2715
|
+
Start it manually:
|
|
2716
|
+
${manualCommand}`,
|
|
2717
|
+
{ cause: err }
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
} else {
|
|
2721
|
+
const logFd = fs13.openSync(logPath, "a");
|
|
2722
|
+
try {
|
|
2723
|
+
const child = spawn(
|
|
2724
|
+
resolvedCommand,
|
|
2725
|
+
["app-server", "--listen", auth.upstreamUrl],
|
|
2726
|
+
{
|
|
2727
|
+
cwd: options.repoRoot,
|
|
2728
|
+
detached: true,
|
|
2729
|
+
stdio: ["ignore", logFd, logFd],
|
|
2730
|
+
env: process.env,
|
|
2731
|
+
windowsHide: true
|
|
2732
|
+
}
|
|
2733
|
+
);
|
|
2734
|
+
child.unref();
|
|
2735
|
+
pid = child.pid ?? null;
|
|
2736
|
+
} catch (err) {
|
|
2737
|
+
if (auth.gatewayPid != null) {
|
|
2738
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2739
|
+
}
|
|
2740
|
+
removeFileIfExists(auth.tokenPath);
|
|
2741
|
+
throw new Error(
|
|
2742
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
2743
|
+
Start it manually:
|
|
2744
|
+
${manualCommand}`,
|
|
2745
|
+
{ cause: err }
|
|
2746
|
+
);
|
|
2747
|
+
} finally {
|
|
2748
|
+
fs13.closeSync(logFd);
|
|
1864
2749
|
}
|
|
1865
2750
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2751
|
+
if (pid == null) {
|
|
2752
|
+
if (auth.gatewayPid != null) {
|
|
2753
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2754
|
+
}
|
|
2755
|
+
removeFileIfExists(auth.tokenPath);
|
|
2756
|
+
throw new Error(
|
|
2757
|
+
`Failed to spawn Codex app-server.
|
|
2758
|
+
Start it manually:
|
|
2759
|
+
${manualCommand}`
|
|
2760
|
+
);
|
|
1874
2761
|
}
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2762
|
+
const healthy = await waitForAppServerHealth(
|
|
2763
|
+
auth.upstreamUrl,
|
|
2764
|
+
APP_SERVER_START_TIMEOUT_MS
|
|
2765
|
+
);
|
|
2766
|
+
if (!healthy) {
|
|
2767
|
+
await terminateProcess(pid, options.platform);
|
|
2768
|
+
if (auth.gatewayPid != null) {
|
|
2769
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2770
|
+
}
|
|
2771
|
+
removeFileIfExists(auth.tokenPath);
|
|
2772
|
+
throw new Error(
|
|
2773
|
+
`Codex app-server did not become healthy at ${auth.upstreamUrl}.
|
|
2774
|
+
Check ${logPath}
|
|
2775
|
+
Or start it manually:
|
|
2776
|
+
${manualCommand}`
|
|
2777
|
+
);
|
|
1883
2778
|
}
|
|
2779
|
+
const gatewayToken = readGatewayToken(auth);
|
|
2780
|
+
if (!gatewayToken) {
|
|
2781
|
+
await terminateProcess(pid, options.platform);
|
|
2782
|
+
if (auth.gatewayPid != null) {
|
|
2783
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2784
|
+
}
|
|
2785
|
+
removeFileIfExists(auth.tokenPath);
|
|
2786
|
+
throw new Error("Tap auth gateway token is missing after startup.");
|
|
2787
|
+
}
|
|
2788
|
+
const gatewayHealthy = await waitForAppServerHealth(
|
|
2789
|
+
buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
|
|
2790
|
+
APP_SERVER_GATEWAY_START_TIMEOUT_MS
|
|
2791
|
+
);
|
|
2792
|
+
if (!gatewayHealthy) {
|
|
2793
|
+
await terminateProcess(pid, options.platform);
|
|
2794
|
+
if (auth.gatewayPid != null) {
|
|
2795
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2796
|
+
}
|
|
2797
|
+
removeFileIfExists(auth.tokenPath);
|
|
2798
|
+
throw new Error(
|
|
2799
|
+
`Tap auth gateway did not become healthy at ${effectiveUrl}.
|
|
2800
|
+
Check ${auth.gatewayLogPath ?? "the gateway log"} and ${logPath}.`
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
const healthyAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2804
|
+
pid = findListeningProcessId(auth.upstreamUrl, options.platform) ?? pid;
|
|
1884
2805
|
return {
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
const pathKey = process.platform === "win32" ? "Path" : "PATH";
|
|
1895
|
-
const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
|
|
1896
|
-
return {
|
|
1897
|
-
...baseEnv,
|
|
1898
|
-
[pathKey]: `${fnmBin}${path11.delimiter}${currentPath}`
|
|
2806
|
+
url: effectiveUrl,
|
|
2807
|
+
pid,
|
|
2808
|
+
managed: true,
|
|
2809
|
+
healthy: true,
|
|
2810
|
+
lastCheckedAt: healthyAt,
|
|
2811
|
+
lastHealthyAt: healthyAt,
|
|
2812
|
+
logPath,
|
|
2813
|
+
manualCommand,
|
|
2814
|
+
auth
|
|
1899
2815
|
};
|
|
1900
2816
|
}
|
|
1901
|
-
|
|
1902
|
-
// src/engine/bridge.ts
|
|
1903
2817
|
function pidFilePath(stateDir, instanceId) {
|
|
1904
|
-
return
|
|
2818
|
+
return path13.join(stateDir, "pids", `bridge-${instanceId}.json`);
|
|
1905
2819
|
}
|
|
1906
2820
|
function logFilePath(stateDir, instanceId) {
|
|
1907
|
-
return
|
|
2821
|
+
return path13.join(stateDir, "logs", `bridge-${instanceId}.log`);
|
|
2822
|
+
}
|
|
2823
|
+
function runtimeHeartbeatFilePath(runtimeStateDir) {
|
|
2824
|
+
return path13.join(runtimeStateDir, "heartbeat.json");
|
|
2825
|
+
}
|
|
2826
|
+
function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
|
|
2827
|
+
if (!runtimeStateDir) {
|
|
2828
|
+
return null;
|
|
2829
|
+
}
|
|
2830
|
+
const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
|
|
2831
|
+
if (!fs13.existsSync(heartbeatPath)) {
|
|
2832
|
+
return null;
|
|
2833
|
+
}
|
|
2834
|
+
try {
|
|
2835
|
+
const raw = fs13.readFileSync(heartbeatPath, "utf-8");
|
|
2836
|
+
const parsed = JSON.parse(raw);
|
|
2837
|
+
return typeof parsed.updatedAt === "string" ? parsed.updatedAt : null;
|
|
2838
|
+
} catch {
|
|
2839
|
+
return null;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
function resolveHeartbeatTimestamp(state) {
|
|
2843
|
+
return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
|
|
1908
2844
|
}
|
|
1909
2845
|
function loadBridgeState(stateDir, instanceId) {
|
|
1910
2846
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
1911
|
-
if (!
|
|
2847
|
+
if (!fs13.existsSync(pidPath)) return null;
|
|
1912
2848
|
try {
|
|
1913
|
-
const raw =
|
|
2849
|
+
const raw = fs13.readFileSync(pidPath, "utf-8");
|
|
1914
2850
|
return JSON.parse(raw);
|
|
1915
2851
|
} catch {
|
|
1916
2852
|
return null;
|
|
@@ -1918,15 +2854,16 @@ function loadBridgeState(stateDir, instanceId) {
|
|
|
1918
2854
|
}
|
|
1919
2855
|
function saveBridgeState(stateDir, instanceId, state) {
|
|
1920
2856
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2857
|
+
const serializable = JSON.parse(JSON.stringify(state));
|
|
2858
|
+
if (serializable.appServer?.auth) {
|
|
2859
|
+
delete serializable.appServer.auth.token;
|
|
2860
|
+
}
|
|
2861
|
+
writeProtectedTextFile(pidPath, JSON.stringify(serializable, null, 2));
|
|
1925
2862
|
}
|
|
1926
2863
|
function clearBridgeState(stateDir, instanceId) {
|
|
1927
2864
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
1928
|
-
if (
|
|
1929
|
-
|
|
2865
|
+
if (fs13.existsSync(pidPath)) {
|
|
2866
|
+
fs13.unlinkSync(pidPath);
|
|
1930
2867
|
}
|
|
1931
2868
|
}
|
|
1932
2869
|
function isProcessAlive(pid) {
|
|
@@ -1964,31 +2901,61 @@ async function startBridge(options) {
|
|
|
1964
2901
|
`Bridge for ${instanceId} is already running (PID: ${existing.pid})`
|
|
1965
2902
|
);
|
|
1966
2903
|
}
|
|
2904
|
+
const previousBridgeState = loadBridgeState(stateDir, instanceId);
|
|
2905
|
+
const previousAppServer = previousBridgeState?.appServer ?? null;
|
|
1967
2906
|
clearBridgeState(stateDir, instanceId);
|
|
1968
2907
|
const logPath = logFilePath(stateDir, instanceId);
|
|
1969
|
-
|
|
2908
|
+
fs13.mkdirSync(path13.dirname(logPath), { recursive: true });
|
|
1970
2909
|
rotateLog(logPath);
|
|
1971
|
-
|
|
1972
|
-
const repoRoot = options.repoRoot ??
|
|
2910
|
+
let logFd = null;
|
|
2911
|
+
const repoRoot = options.repoRoot ?? path13.resolve(stateDir, "..");
|
|
2912
|
+
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
1973
2913
|
const resolved = resolveNodeRuntime(
|
|
1974
2914
|
options.runtimeCommand ?? "node",
|
|
1975
2915
|
repoRoot
|
|
1976
2916
|
);
|
|
1977
2917
|
const command = resolved.command;
|
|
1978
2918
|
const runtimeEnv = buildRuntimeEnv(repoRoot);
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2919
|
+
const effectiveAppServerUrl = resolveAppServerUrl(options.appServerUrl, port);
|
|
2920
|
+
let appServer = null;
|
|
2921
|
+
let bridgeAppServerUrl = effectiveAppServerUrl;
|
|
2922
|
+
if (runtime === "codex" && options.manageAppServer) {
|
|
2923
|
+
appServer = await ensureCodexAppServer({
|
|
2924
|
+
instanceId,
|
|
2925
|
+
stateDir,
|
|
2926
|
+
repoRoot,
|
|
2927
|
+
platform: options.platform,
|
|
2928
|
+
appServerUrl: effectiveAppServerUrl,
|
|
2929
|
+
existingAppServer: previousAppServer,
|
|
2930
|
+
noAuth: options.noAuth
|
|
2931
|
+
});
|
|
2932
|
+
if (appServer.auth) {
|
|
2933
|
+
appServer = {
|
|
2934
|
+
...appServer,
|
|
2935
|
+
auth: materializeGatewayTokenFile(
|
|
2936
|
+
stateDir,
|
|
2937
|
+
instanceId,
|
|
2938
|
+
effectiveAppServerUrl,
|
|
2939
|
+
appServer.auth
|
|
2940
|
+
)
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
bridgeAppServerUrl = effectiveAppServerUrl;
|
|
2944
|
+
}
|
|
2945
|
+
try {
|
|
2946
|
+
const bridgeEnv = {
|
|
1983
2947
|
...runtimeEnv,
|
|
1984
2948
|
TAP_COMMS_DIR: commsDir,
|
|
2949
|
+
TAP_STATE_DIR: runtimeStateDir,
|
|
1985
2950
|
TAP_BRIDGE_RUNTIME: runtime,
|
|
1986
2951
|
TAP_BRIDGE_INSTANCE_ID: instanceId,
|
|
2952
|
+
TAP_AGENT_ID: instanceId,
|
|
1987
2953
|
TAP_AGENT_NAME: resolvedAgent,
|
|
1988
2954
|
CODEX_TAP_AGENT_NAME: resolvedAgent,
|
|
1989
2955
|
TAP_RESOLVED_NODE: resolved.command,
|
|
1990
2956
|
TAP_STRIP_TYPES: resolved.supportsStripTypes ? "1" : "0",
|
|
1991
|
-
...
|
|
2957
|
+
...bridgeAppServerUrl ? { CODEX_APP_SERVER_URL: bridgeAppServerUrl } : {},
|
|
2958
|
+
...appServer?.auth?.tokenPath ? { TAP_GATEWAY_TOKEN_FILE: appServer.auth.tokenPath } : {},
|
|
1992
2959
|
...port != null ? { TAP_BRIDGE_PORT: String(port) } : {},
|
|
1993
2960
|
...options.headless?.enabled ? {
|
|
1994
2961
|
TAP_HEADLESS: "true",
|
|
@@ -1996,7 +2963,6 @@ async function startBridge(options) {
|
|
|
1996
2963
|
TAP_MAX_REVIEW_ROUNDS: String(options.headless.maxRounds),
|
|
1997
2964
|
TAP_QUALITY_FLOOR: options.headless.qualitySeverityFloor
|
|
1998
2965
|
} : {},
|
|
1999
|
-
// Bridge script operational flags
|
|
2000
2966
|
...options.busyMode ? { TAP_BUSY_MODE: options.busyMode } : {},
|
|
2001
2967
|
...options.pollSeconds != null ? { TAP_POLL_SECONDS: String(options.pollSeconds) } : {},
|
|
2002
2968
|
...options.reconnectSeconds != null ? { TAP_RECONNECT_SECONDS: String(options.reconnectSeconds) } : {},
|
|
@@ -2008,20 +2974,55 @@ async function startBridge(options) {
|
|
|
2008
2974
|
...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
|
|
2009
2975
|
...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
|
|
2010
2976
|
...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
|
|
2977
|
+
};
|
|
2978
|
+
let bridgePid = null;
|
|
2979
|
+
if (options.platform === "win32") {
|
|
2980
|
+
bridgePid = startWindowsDetachedProcess(
|
|
2981
|
+
command,
|
|
2982
|
+
[bridgeScript],
|
|
2983
|
+
repoRoot,
|
|
2984
|
+
logPath,
|
|
2985
|
+
bridgeEnv
|
|
2986
|
+
);
|
|
2987
|
+
} else {
|
|
2988
|
+
logFd = fs13.openSync(logPath, "a");
|
|
2989
|
+
const child = spawn(command, [bridgeScript], {
|
|
2990
|
+
detached: true,
|
|
2991
|
+
stdio: ["ignore", logFd, logFd],
|
|
2992
|
+
env: bridgeEnv,
|
|
2993
|
+
windowsHide: true
|
|
2994
|
+
});
|
|
2995
|
+
child.unref();
|
|
2996
|
+
bridgePid = child.pid ?? null;
|
|
2011
2997
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2998
|
+
if (logFd != null) {
|
|
2999
|
+
fs13.closeSync(logFd);
|
|
3000
|
+
logFd = null;
|
|
3001
|
+
}
|
|
3002
|
+
if (!bridgePid) {
|
|
3003
|
+
throw new Error(`Failed to spawn bridge process for ${instanceId}`);
|
|
3004
|
+
}
|
|
3005
|
+
const state = {
|
|
3006
|
+
pid: bridgePid,
|
|
3007
|
+
statePath: pidFilePath(stateDir, instanceId),
|
|
3008
|
+
lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3009
|
+
appServer,
|
|
3010
|
+
runtimeStateDir
|
|
3011
|
+
};
|
|
3012
|
+
saveBridgeState(stateDir, instanceId, state);
|
|
3013
|
+
return state;
|
|
3014
|
+
} catch (err) {
|
|
3015
|
+
if (logFd != null) {
|
|
3016
|
+
try {
|
|
3017
|
+
fs13.closeSync(logFd);
|
|
3018
|
+
} catch {
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
if (appServer?.managed) {
|
|
3022
|
+
await stopManagedAppServer(appServer, options.platform);
|
|
3023
|
+
}
|
|
3024
|
+
throw err;
|
|
3025
|
+
}
|
|
2025
3026
|
}
|
|
2026
3027
|
async function stopBridge(options) {
|
|
2027
3028
|
const { instanceId, stateDir, platform } = options;
|
|
@@ -2034,37 +3035,33 @@ async function stopBridge(options) {
|
|
|
2034
3035
|
return false;
|
|
2035
3036
|
}
|
|
2036
3037
|
try {
|
|
2037
|
-
|
|
2038
|
-
execSync3(`taskkill /PID ${state.pid} /F /T`, { stdio: "pipe" });
|
|
2039
|
-
} else {
|
|
2040
|
-
process.kill(state.pid, "SIGTERM");
|
|
2041
|
-
await new Promise((resolve10) => setTimeout(resolve10, 2e3));
|
|
2042
|
-
if (isProcessAlive(state.pid)) {
|
|
2043
|
-
process.kill(state.pid, "SIGKILL");
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
3038
|
+
await terminateProcess(state.pid, platform);
|
|
2046
3039
|
} catch {
|
|
2047
3040
|
}
|
|
2048
3041
|
clearBridgeState(stateDir, instanceId);
|
|
2049
3042
|
return true;
|
|
2050
3043
|
}
|
|
2051
3044
|
function rotateLog(logPath) {
|
|
2052
|
-
if (!
|
|
3045
|
+
if (!fs13.existsSync(logPath)) return;
|
|
2053
3046
|
try {
|
|
2054
|
-
const stats =
|
|
3047
|
+
const stats = fs13.statSync(logPath);
|
|
2055
3048
|
if (stats.size === 0) return;
|
|
2056
3049
|
const prevPath = `${logPath}.prev`;
|
|
2057
|
-
|
|
3050
|
+
fs13.renameSync(logPath, prevPath);
|
|
2058
3051
|
} catch {
|
|
2059
3052
|
}
|
|
2060
3053
|
}
|
|
2061
3054
|
function getHeartbeatAge(stateDir, instanceId) {
|
|
2062
3055
|
const state = loadBridgeState(stateDir, instanceId);
|
|
2063
|
-
|
|
2064
|
-
|
|
3056
|
+
const heartbeat = resolveHeartbeatTimestamp(state);
|
|
3057
|
+
if (!heartbeat) return null;
|
|
3058
|
+
const heartbeatTime = new Date(heartbeat).getTime();
|
|
2065
3059
|
if (isNaN(heartbeatTime)) return null;
|
|
2066
3060
|
return Math.floor((Date.now() - heartbeatTime) / 1e3);
|
|
2067
3061
|
}
|
|
3062
|
+
function getBridgeHeartbeatTimestamp(stateDir, instanceId) {
|
|
3063
|
+
return resolveHeartbeatTimestamp(loadBridgeState(stateDir, instanceId));
|
|
3064
|
+
}
|
|
2068
3065
|
function getBridgeStatus(stateDir, instanceId) {
|
|
2069
3066
|
const state = loadBridgeState(stateDir, instanceId);
|
|
2070
3067
|
if (!state) return "stopped";
|
|
@@ -2084,7 +3081,7 @@ async function addCommand(args) {
|
|
|
2084
3081
|
ok: false,
|
|
2085
3082
|
command: "add",
|
|
2086
3083
|
code: "TAP_INVALID_ARGUMENT",
|
|
2087
|
-
message: "Missing runtime argument. Usage: npx @hua-labs/tap add <claude|codex|gemini> [--name <name>] [--port <port>] [--headless] [--role <role>]",
|
|
3084
|
+
message: "Missing runtime argument. Usage: npx @hua-labs/tap add <claude|codex|gemini> [--name <name>] [--port <port>] [--agent-name <name>] [--headless] [--role <role>]",
|
|
2088
3085
|
warnings: [],
|
|
2089
3086
|
data: {}
|
|
2090
3087
|
};
|
|
@@ -2104,6 +3101,7 @@ async function addCommand(args) {
|
|
|
2104
3101
|
const instanceId = buildInstanceId(runtime, instanceName);
|
|
2105
3102
|
const portStr = typeof flags["port"] === "string" ? flags["port"] : void 0;
|
|
2106
3103
|
const port = portStr ? parseInt(portStr, 10) : null;
|
|
3104
|
+
const agentNameFlag = typeof flags["agent-name"] === "string" ? flags["agent-name"] : null;
|
|
2107
3105
|
const force = flags["force"] === true;
|
|
2108
3106
|
const headlessFlag = flags["headless"] === true;
|
|
2109
3107
|
const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
|
|
@@ -2150,7 +3148,7 @@ async function addCommand(args) {
|
|
|
2150
3148
|
data: {}
|
|
2151
3149
|
};
|
|
2152
3150
|
}
|
|
2153
|
-
const repoRoot =
|
|
3151
|
+
const repoRoot = findRepoRoot();
|
|
2154
3152
|
const state = loadState(repoRoot);
|
|
2155
3153
|
if (!state) {
|
|
2156
3154
|
return {
|
|
@@ -2194,7 +3192,7 @@ async function addCommand(args) {
|
|
|
2194
3192
|
logHeader(`@hua-labs/tap add ${instanceId}`);
|
|
2195
3193
|
if (instanceName) log(`Instance name: ${instanceName}`);
|
|
2196
3194
|
if (port !== null) log(`Port: ${port}`);
|
|
2197
|
-
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
3195
|
+
const ctx = { ...createAdapterContext(state.commsDir, repoRoot), instanceId };
|
|
2198
3196
|
const adapter = getAdapter(runtime);
|
|
2199
3197
|
const warnings = [];
|
|
2200
3198
|
log("Probing runtime...");
|
|
@@ -2226,13 +3224,15 @@ async function addCommand(args) {
|
|
|
2226
3224
|
log(`Artifacts: ${plan.ownedArtifacts.length}`);
|
|
2227
3225
|
for (const w of plan.warnings) logWarn(w);
|
|
2228
3226
|
if (plan.operations.length === 0) {
|
|
3227
|
+
const failureMessage = probe.issues[0] ?? plan.warnings[0] ?? probe.warnings[0] ?? "No operations to apply. Runtime not configured.";
|
|
3228
|
+
const failureCode = /MCP server/i.test(failureMessage) ? "TAP_LOCAL_SERVER_MISSING" : "TAP_PATCH_FAILED";
|
|
2229
3229
|
return {
|
|
2230
|
-
ok:
|
|
3230
|
+
ok: false,
|
|
2231
3231
|
command: "add",
|
|
2232
3232
|
runtime,
|
|
2233
3233
|
instanceId,
|
|
2234
|
-
code:
|
|
2235
|
-
message:
|
|
3234
|
+
code: failureCode,
|
|
3235
|
+
message: failureMessage,
|
|
2236
3236
|
warnings,
|
|
2237
3237
|
data: { planOps: 0 }
|
|
2238
3238
|
};
|
|
@@ -2280,10 +3280,10 @@ async function addCommand(args) {
|
|
|
2280
3280
|
logWarn("Bridge script not found. Bridge not started.");
|
|
2281
3281
|
warnings.push("Bridge script not found. Run bridge manually.");
|
|
2282
3282
|
} else {
|
|
2283
|
-
const
|
|
2284
|
-
if (!
|
|
3283
|
+
const resolvedAgentName = agentNameFlag || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
|
|
3284
|
+
if (!resolvedAgentName) {
|
|
2285
3285
|
logWarn(
|
|
2286
|
-
"No agent name set
|
|
3286
|
+
"No agent name set. Bridge not started. Use: npx @hua-labs/tap bridge start <instance> --agent-name <name>"
|
|
2287
3287
|
);
|
|
2288
3288
|
warnings.push("Bridge not auto-started: no agent name available.");
|
|
2289
3289
|
} else {
|
|
@@ -2297,7 +3297,7 @@ async function addCommand(args) {
|
|
|
2297
3297
|
commsDir: ctx.commsDir,
|
|
2298
3298
|
bridgeScript,
|
|
2299
3299
|
platform: ctx.platform,
|
|
2300
|
-
agentName:
|
|
3300
|
+
agentName: resolvedAgentName,
|
|
2301
3301
|
runtimeCommand: resolvedCfg.runtimeCommand,
|
|
2302
3302
|
appServerUrl: resolvedCfg.appServerUrl,
|
|
2303
3303
|
repoRoot,
|
|
@@ -2313,10 +3313,11 @@ async function addCommand(args) {
|
|
|
2313
3313
|
}
|
|
2314
3314
|
}
|
|
2315
3315
|
}
|
|
3316
|
+
const existingAgentName = state.instances[instanceId]?.agentName ?? null;
|
|
2316
3317
|
const instanceState = {
|
|
2317
3318
|
instanceId,
|
|
2318
3319
|
runtime,
|
|
2319
|
-
agentName:
|
|
3320
|
+
agentName: agentNameFlag ?? existingAgentName,
|
|
2320
3321
|
port,
|
|
2321
3322
|
installed: true,
|
|
2322
3323
|
configPath: probe.configPath ?? "",
|
|
@@ -2382,7 +3383,7 @@ function instanceStatusLine(inst, status) {
|
|
|
2382
3383
|
return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
|
|
2383
3384
|
}
|
|
2384
3385
|
async function statusCommand(_args) {
|
|
2385
|
-
const repoRoot =
|
|
3386
|
+
const repoRoot = findRepoRoot();
|
|
2386
3387
|
const state = loadState(repoRoot);
|
|
2387
3388
|
if (!state) {
|
|
2388
3389
|
return {
|
|
@@ -2395,7 +3396,7 @@ async function statusCommand(_args) {
|
|
|
2395
3396
|
};
|
|
2396
3397
|
}
|
|
2397
3398
|
logHeader("@hua-labs/tap status");
|
|
2398
|
-
log(`Version: ${
|
|
3399
|
+
log(`Version: ${version}`);
|
|
2399
3400
|
log(`Comms dir: ${state.commsDir}`);
|
|
2400
3401
|
log(`Repo root: ${state.repoRoot}`);
|
|
2401
3402
|
log(`Schema: v${state.schemaVersion}`);
|
|
@@ -2452,7 +3453,7 @@ async function statusCommand(_args) {
|
|
|
2452
3453
|
message: `${installed.length} instance(s) installed`,
|
|
2453
3454
|
warnings: [],
|
|
2454
3455
|
data: {
|
|
2455
|
-
version
|
|
3456
|
+
version,
|
|
2456
3457
|
commsDir: state.commsDir,
|
|
2457
3458
|
repoRoot: state.repoRoot,
|
|
2458
3459
|
instances
|
|
@@ -2461,7 +3462,7 @@ async function statusCommand(_args) {
|
|
|
2461
3462
|
}
|
|
2462
3463
|
|
|
2463
3464
|
// src/engine/rollback.ts
|
|
2464
|
-
import * as
|
|
3465
|
+
import * as fs14 from "fs";
|
|
2465
3466
|
async function rollbackRuntime(_instanceId, runtimeState) {
|
|
2466
3467
|
const errors = [];
|
|
2467
3468
|
const restoredFiles = [];
|
|
@@ -2490,7 +3491,7 @@ async function rollbackRuntime(_instanceId, runtimeState) {
|
|
|
2490
3491
|
};
|
|
2491
3492
|
}
|
|
2492
3493
|
function rollbackArtifact(artifact) {
|
|
2493
|
-
if (!
|
|
3494
|
+
if (!fs14.existsSync(artifact.path)) {
|
|
2494
3495
|
return { restored: false, error: `File not found: ${artifact.path}` };
|
|
2495
3496
|
}
|
|
2496
3497
|
switch (artifact.kind) {
|
|
@@ -2508,7 +3509,7 @@ function rollbackArtifact(artifact) {
|
|
|
2508
3509
|
}
|
|
2509
3510
|
}
|
|
2510
3511
|
function rollbackJsonPath(artifact) {
|
|
2511
|
-
const raw =
|
|
3512
|
+
const raw = fs14.readFileSync(artifact.path, "utf-8");
|
|
2512
3513
|
let config;
|
|
2513
3514
|
try {
|
|
2514
3515
|
config = JSON.parse(raw);
|
|
@@ -2534,18 +3535,18 @@ function rollbackJsonPath(artifact) {
|
|
|
2534
3535
|
cleanEmptyParents(config, artifact.selector);
|
|
2535
3536
|
}
|
|
2536
3537
|
const tmp = `${artifact.path}.tmp.${process.pid}`;
|
|
2537
|
-
|
|
2538
|
-
|
|
3538
|
+
fs14.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
3539
|
+
fs14.renameSync(tmp, artifact.path);
|
|
2539
3540
|
return { restored: true };
|
|
2540
3541
|
}
|
|
2541
3542
|
function rollbackTomlTable(artifact) {
|
|
2542
|
-
const content =
|
|
3543
|
+
const content = fs14.readFileSync(artifact.path, "utf-8");
|
|
2543
3544
|
const backup = artifact.backupPath ? readArtifactBackup(artifact.backupPath) : null;
|
|
2544
3545
|
if (backup?.kind === "toml-table" && backup.selector === artifact.selector) {
|
|
2545
3546
|
const nextContent = backup.existed ? replaceTomlTable(content, artifact.selector, backup.content ?? "") : removeTomlTable(content, artifact.selector);
|
|
2546
3547
|
const tmp2 = `${artifact.path}.tmp.${process.pid}`;
|
|
2547
|
-
|
|
2548
|
-
|
|
3548
|
+
fs14.writeFileSync(tmp2, nextContent, "utf-8");
|
|
3549
|
+
fs14.renameSync(tmp2, artifact.path);
|
|
2549
3550
|
return { restored: true };
|
|
2550
3551
|
}
|
|
2551
3552
|
if (!extractTomlTable(content, artifact.selector)) {
|
|
@@ -2555,13 +3556,13 @@ function rollbackTomlTable(artifact) {
|
|
|
2555
3556
|
};
|
|
2556
3557
|
}
|
|
2557
3558
|
const tmp = `${artifact.path}.tmp.${process.pid}`;
|
|
2558
|
-
|
|
2559
|
-
|
|
3559
|
+
fs14.writeFileSync(tmp, removeTomlTable(content, artifact.selector), "utf-8");
|
|
3560
|
+
fs14.renameSync(tmp, artifact.path);
|
|
2560
3561
|
return { restored: true };
|
|
2561
3562
|
}
|
|
2562
3563
|
function rollbackFile(artifact) {
|
|
2563
|
-
if (
|
|
2564
|
-
|
|
3564
|
+
if (fs14.existsSync(artifact.path)) {
|
|
3565
|
+
fs14.unlinkSync(artifact.path);
|
|
2565
3566
|
return { restored: true };
|
|
2566
3567
|
}
|
|
2567
3568
|
return { restored: false, error: `File not found: ${artifact.path}` };
|
|
@@ -2622,7 +3623,7 @@ async function removeCommand(args) {
|
|
|
2622
3623
|
data: {}
|
|
2623
3624
|
};
|
|
2624
3625
|
}
|
|
2625
|
-
const repoRoot =
|
|
3626
|
+
const repoRoot = findRepoRoot();
|
|
2626
3627
|
const state = loadState(repoRoot);
|
|
2627
3628
|
if (!state) {
|
|
2628
3629
|
return {
|
|
@@ -2708,7 +3709,7 @@ async function removeCommand(args) {
|
|
|
2708
3709
|
}
|
|
2709
3710
|
|
|
2710
3711
|
// src/commands/bridge.ts
|
|
2711
|
-
import * as
|
|
3712
|
+
import * as path14 from "path";
|
|
2712
3713
|
function formatAge(seconds) {
|
|
2713
3714
|
if (seconds < 60) return `${seconds}s ago`;
|
|
2714
3715
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
@@ -2720,6 +3721,7 @@ Usage:
|
|
|
2720
3721
|
|
|
2721
3722
|
Subcommands:
|
|
2722
3723
|
start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
|
|
3724
|
+
start --all Start all registered app-server instances
|
|
2723
3725
|
stop <instance> Stop bridge for an instance
|
|
2724
3726
|
stop Stop all running bridges
|
|
2725
3727
|
status Show bridge status for all instances
|
|
@@ -2727,6 +3729,8 @@ Subcommands:
|
|
|
2727
3729
|
|
|
2728
3730
|
Options:
|
|
2729
3731
|
--agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
|
|
3732
|
+
Saved to state \u2014 only needed on first start
|
|
3733
|
+
--all Start all registered app-server instances
|
|
2730
3734
|
--busy-mode <steer|wait> How to handle active turns (default: steer)
|
|
2731
3735
|
--poll-seconds <n> Inbox poll interval (default: 5)
|
|
2732
3736
|
--reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
|
|
@@ -2734,17 +3738,98 @@ Options:
|
|
|
2734
3738
|
--thread-id <id> Resume specific thread
|
|
2735
3739
|
--ephemeral Use ephemeral thread (no persistence)
|
|
2736
3740
|
--process-existing-messages Process all existing inbox messages
|
|
3741
|
+
--no-server Skip app-server auto-start and connect only
|
|
3742
|
+
--no-auth Skip auth gateway (app-server listens directly, localhost only)
|
|
3743
|
+
|
|
3744
|
+
Port Assignment:
|
|
3745
|
+
Ports are auto-assigned from 4501 on first bridge start if not set via --port
|
|
3746
|
+
during 'tap add'. Auto-assigned ports are saved to state for future starts.
|
|
2737
3747
|
|
|
2738
3748
|
Examples:
|
|
2739
3749
|
npx @hua-labs/tap bridge start codex --agent-name myAgent
|
|
3750
|
+
npx @hua-labs/tap bridge start --all
|
|
3751
|
+
npx @hua-labs/tap bridge start codex --agent-name myAgent --no-server
|
|
2740
3752
|
npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer --busy-mode steer
|
|
2741
3753
|
npx @hua-labs/tap bridge stop codex
|
|
2742
3754
|
npx @hua-labs/tap bridge stop
|
|
2743
3755
|
npx @hua-labs/tap bridge status
|
|
2744
3756
|
`.trim();
|
|
3757
|
+
function formatAppServerState(appServer) {
|
|
3758
|
+
const ownership = appServer.managed ? "managed" : "external";
|
|
3759
|
+
const pid = appServer.pid != null ? ` pid:${appServer.pid}` : "";
|
|
3760
|
+
const health = appServer.healthy ? "healthy" : "unhealthy";
|
|
3761
|
+
const auth = appServer.auth != null ? `, auth gateway:${appServer.auth.gatewayPid ?? "-"} -> ${appServer.auth.upstreamUrl}` : "";
|
|
3762
|
+
return `${health}, ${ownership}${pid}, ${appServer.url}${auth}`;
|
|
3763
|
+
}
|
|
3764
|
+
function redactProtectedUrl(url) {
|
|
3765
|
+
try {
|
|
3766
|
+
const parsed = new URL(url);
|
|
3767
|
+
if (parsed.searchParams.has("tap_token")) {
|
|
3768
|
+
parsed.searchParams.set("tap_token", "***");
|
|
3769
|
+
}
|
|
3770
|
+
return parsed.toString().replace(/\/$/, "");
|
|
3771
|
+
} catch {
|
|
3772
|
+
return url.replace(/tap_token=[^&]+/g, "tap_token=***");
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
function loadCurrentBridgeState(stateDir, instanceId, fallback) {
|
|
3776
|
+
return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
|
|
3777
|
+
}
|
|
3778
|
+
function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
|
|
3779
|
+
const shared = [];
|
|
3780
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
3781
|
+
if (id === currentInstanceId || !inst?.installed) {
|
|
3782
|
+
continue;
|
|
3783
|
+
}
|
|
3784
|
+
const instanceId = id;
|
|
3785
|
+
if (getBridgeStatus(stateDir, instanceId) !== "running") {
|
|
3786
|
+
continue;
|
|
3787
|
+
}
|
|
3788
|
+
const bridgeState = loadCurrentBridgeState(
|
|
3789
|
+
stateDir,
|
|
3790
|
+
instanceId,
|
|
3791
|
+
inst.bridge
|
|
3792
|
+
);
|
|
3793
|
+
if (bridgeState?.appServer?.url === appServerUrl) {
|
|
3794
|
+
shared.push(instanceId);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
return shared;
|
|
3798
|
+
}
|
|
3799
|
+
function transferManagedAppServerOwnership(state, stateDir, recipientId, appServer) {
|
|
3800
|
+
const recipient = state.instances[recipientId];
|
|
3801
|
+
if (!recipient) {
|
|
3802
|
+
return false;
|
|
3803
|
+
}
|
|
3804
|
+
const bridgeState = loadCurrentBridgeState(
|
|
3805
|
+
stateDir,
|
|
3806
|
+
recipientId,
|
|
3807
|
+
recipient.bridge
|
|
3808
|
+
);
|
|
3809
|
+
if (!bridgeState) {
|
|
3810
|
+
return false;
|
|
3811
|
+
}
|
|
3812
|
+
const transferredAppServer = {
|
|
3813
|
+
...appServer,
|
|
3814
|
+
managed: true,
|
|
3815
|
+
healthy: true,
|
|
3816
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3817
|
+
lastHealthyAt: appServer.lastHealthyAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3818
|
+
};
|
|
3819
|
+
const updatedBridge = {
|
|
3820
|
+
...bridgeState,
|
|
3821
|
+
appServer: transferredAppServer
|
|
3822
|
+
};
|
|
3823
|
+
saveBridgeState(stateDir, recipientId, updatedBridge);
|
|
3824
|
+
state.instances[recipientId] = {
|
|
3825
|
+
...recipient,
|
|
3826
|
+
bridge: updatedBridge
|
|
3827
|
+
};
|
|
3828
|
+
return true;
|
|
3829
|
+
}
|
|
2745
3830
|
async function bridgeStart(identifier, agentName, flags = {}) {
|
|
2746
|
-
const repoRoot =
|
|
2747
|
-
|
|
3831
|
+
const repoRoot = findRepoRoot();
|
|
3832
|
+
let state = loadState(repoRoot);
|
|
2748
3833
|
if (!state) {
|
|
2749
3834
|
return {
|
|
2750
3835
|
ok: false,
|
|
@@ -2767,7 +3852,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2767
3852
|
};
|
|
2768
3853
|
}
|
|
2769
3854
|
const instanceId = resolved.instanceId;
|
|
2770
|
-
|
|
3855
|
+
let instance = state.instances[instanceId];
|
|
2771
3856
|
if (!instance?.installed) {
|
|
2772
3857
|
return {
|
|
2773
3858
|
ok: false,
|
|
@@ -2794,6 +3879,13 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2794
3879
|
data: { bridgeMode: mode }
|
|
2795
3880
|
};
|
|
2796
3881
|
}
|
|
3882
|
+
const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
|
|
3883
|
+
if (agentName && agentName !== instance.agentName) {
|
|
3884
|
+
instance = { ...instance, agentName };
|
|
3885
|
+
const updatedState = updateInstanceState(state, instanceId, instance);
|
|
3886
|
+
saveState(repoRoot, updatedState);
|
|
3887
|
+
state = updatedState;
|
|
3888
|
+
}
|
|
2797
3889
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
2798
3890
|
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
2799
3891
|
if (!bridgeScript) {
|
|
@@ -2810,19 +3902,63 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2810
3902
|
}
|
|
2811
3903
|
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
2812
3904
|
const runtimeCommand = resolvedConfig.runtimeCommand;
|
|
2813
|
-
const
|
|
3905
|
+
const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
|
|
3906
|
+
let effectivePort = instance.port;
|
|
3907
|
+
if (effectivePort == null && manageAppServer) {
|
|
3908
|
+
effectivePort = await findNextAvailableAppServerPort(
|
|
3909
|
+
state,
|
|
3910
|
+
resolvedConfig.appServerUrl,
|
|
3911
|
+
4501,
|
|
3912
|
+
instanceId
|
|
3913
|
+
);
|
|
3914
|
+
instance = { ...instance, port: effectivePort };
|
|
3915
|
+
const updatedState = updateInstanceState(state, instanceId, instance);
|
|
3916
|
+
saveState(repoRoot, updatedState);
|
|
3917
|
+
state = updatedState;
|
|
3918
|
+
}
|
|
3919
|
+
const appServerUrl = resolveAppServerUrl(
|
|
3920
|
+
resolvedConfig.appServerUrl,
|
|
3921
|
+
effectivePort ?? void 0
|
|
3922
|
+
);
|
|
2814
3923
|
logHeader(`@hua-labs/tap bridge start ${instanceId}`);
|
|
2815
3924
|
log(`Bridge script: ${bridgeScript}`);
|
|
2816
3925
|
log(`Bridge mode: ${mode}`);
|
|
2817
3926
|
log(`Runtime cmd: ${runtimeCommand}`);
|
|
2818
3927
|
log(`App server: ${appServerUrl}`);
|
|
2819
|
-
if (
|
|
3928
|
+
if (effectivePort != null) log(`Port: ${effectivePort}`);
|
|
3929
|
+
if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
|
|
3930
|
+
const noAuth = flags["no-auth"] === true;
|
|
3931
|
+
if (!manageAppServer && instance.runtime === "codex") {
|
|
3932
|
+
log("Auto server: disabled (--no-server)");
|
|
3933
|
+
}
|
|
3934
|
+
if (noAuth && manageAppServer) {
|
|
3935
|
+
log("Auth gateway: disabled (--no-auth)");
|
|
3936
|
+
}
|
|
2820
3937
|
const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
|
|
2821
3938
|
if (willBeHeadless) {
|
|
2822
3939
|
const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
|
|
2823
3940
|
log(`Headless: ${role}`);
|
|
2824
3941
|
}
|
|
2825
3942
|
try {
|
|
3943
|
+
if (!manageAppServer && instance.runtime === "codex") {
|
|
3944
|
+
log("Checking app-server health...");
|
|
3945
|
+
const healthy = await checkAppServerHealth(appServerUrl);
|
|
3946
|
+
if (healthy) {
|
|
3947
|
+
logSuccess("App server reachable");
|
|
3948
|
+
} else {
|
|
3949
|
+
logError(`App server not reachable at ${appServerUrl}`);
|
|
3950
|
+
return {
|
|
3951
|
+
ok: false,
|
|
3952
|
+
command: "bridge",
|
|
3953
|
+
instanceId,
|
|
3954
|
+
runtime: instance.runtime,
|
|
3955
|
+
code: "TAP_BRIDGE_START_FAILED",
|
|
3956
|
+
message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
|
|
3957
|
+
warnings: [],
|
|
3958
|
+
data: {}
|
|
3959
|
+
};
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
2826
3962
|
const busyModeRaw = flags["busy-mode"];
|
|
2827
3963
|
if (busyModeRaw !== void 0 && busyModeRaw !== "steer" && busyModeRaw !== "wait") {
|
|
2828
3964
|
return {
|
|
@@ -2871,11 +4007,13 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2871
4007
|
commsDir: ctx.commsDir,
|
|
2872
4008
|
bridgeScript,
|
|
2873
4009
|
platform: ctx.platform,
|
|
2874
|
-
agentName,
|
|
4010
|
+
agentName: resolvedAgentName,
|
|
2875
4011
|
runtimeCommand,
|
|
2876
4012
|
appServerUrl,
|
|
2877
4013
|
repoRoot,
|
|
2878
|
-
port:
|
|
4014
|
+
port: effectivePort ?? void 0,
|
|
4015
|
+
manageAppServer,
|
|
4016
|
+
noAuth,
|
|
2879
4017
|
headless,
|
|
2880
4018
|
busyMode,
|
|
2881
4019
|
pollSeconds,
|
|
@@ -2886,7 +4024,25 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2886
4024
|
processExistingMessages
|
|
2887
4025
|
});
|
|
2888
4026
|
logSuccess(`Bridge started (PID: ${bridge.pid})`);
|
|
2889
|
-
log(`Log: ${
|
|
4027
|
+
log(`Log: ${path14.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
|
|
4028
|
+
if (bridge.appServer) {
|
|
4029
|
+
log(`App server: ${formatAppServerState(bridge.appServer)}`);
|
|
4030
|
+
if (bridge.appServer.logPath) {
|
|
4031
|
+
log(`Server log: ${bridge.appServer.logPath}`);
|
|
4032
|
+
}
|
|
4033
|
+
if (bridge.appServer.auth) {
|
|
4034
|
+
log(
|
|
4035
|
+
`Protected: ${redactProtectedUrl(bridge.appServer.auth.protectedUrl)}`
|
|
4036
|
+
);
|
|
4037
|
+
if (bridge.appServer.auth.gatewayLogPath) {
|
|
4038
|
+
log(`Gateway log: ${bridge.appServer.auth.gatewayLogPath}`);
|
|
4039
|
+
}
|
|
4040
|
+
log(`TUI connect: ${bridge.appServer.auth.upstreamUrl}`);
|
|
4041
|
+
}
|
|
4042
|
+
if (bridge.appServer.managed && !bridge.appServer.auth) {
|
|
4043
|
+
log(`TUI connect: ${bridge.appServer.url}`);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
2890
4046
|
const updated = { ...instance, bridge };
|
|
2891
4047
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
2892
4048
|
saveState(repoRoot, newState);
|
|
@@ -2898,7 +4054,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2898
4054
|
code: "TAP_BRIDGE_START_OK",
|
|
2899
4055
|
message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
|
|
2900
4056
|
warnings: [],
|
|
2901
|
-
data: { pid: bridge.pid }
|
|
4057
|
+
data: { pid: bridge.pid, appServer: bridge.appServer ?? null }
|
|
2902
4058
|
};
|
|
2903
4059
|
} catch (err) {
|
|
2904
4060
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2915,8 +4071,76 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2915
4071
|
};
|
|
2916
4072
|
}
|
|
2917
4073
|
}
|
|
4074
|
+
async function bridgeStartAll(flags = {}) {
|
|
4075
|
+
const repoRoot = findRepoRoot();
|
|
4076
|
+
const state = loadState(repoRoot);
|
|
4077
|
+
if (!state) {
|
|
4078
|
+
return {
|
|
4079
|
+
ok: false,
|
|
4080
|
+
command: "bridge",
|
|
4081
|
+
code: "TAP_NOT_INITIALIZED",
|
|
4082
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
4083
|
+
warnings: [],
|
|
4084
|
+
data: {}
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
4087
|
+
const instanceIds = Object.keys(state.instances);
|
|
4088
|
+
const appServerInstances = instanceIds.filter((id) => {
|
|
4089
|
+
const inst = state.instances[id];
|
|
4090
|
+
if (!inst?.installed) return false;
|
|
4091
|
+
const adapter = getAdapter(inst.runtime);
|
|
4092
|
+
return adapter.bridgeMode() === "app-server";
|
|
4093
|
+
});
|
|
4094
|
+
if (appServerInstances.length === 0) {
|
|
4095
|
+
return {
|
|
4096
|
+
ok: true,
|
|
4097
|
+
command: "bridge",
|
|
4098
|
+
code: "TAP_NO_OP",
|
|
4099
|
+
message: "No app-server instances found to start.",
|
|
4100
|
+
warnings: [],
|
|
4101
|
+
data: {}
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
logHeader("@hua-labs/tap bridge start --all");
|
|
4105
|
+
log(
|
|
4106
|
+
`Found ${appServerInstances.length} app-server instance(s): ${appServerInstances.join(", ")}`
|
|
4107
|
+
);
|
|
4108
|
+
log("");
|
|
4109
|
+
const started = [];
|
|
4110
|
+
const failed = [];
|
|
4111
|
+
const warnings = [];
|
|
4112
|
+
for (const instanceId of appServerInstances) {
|
|
4113
|
+
const inst = state.instances[instanceId];
|
|
4114
|
+
const storedName = inst?.agentName ?? void 0;
|
|
4115
|
+
if (!storedName) {
|
|
4116
|
+
const msg = `${instanceId}: skipped \u2014 no stored agent-name. Set it first: tap bridge start ${instanceId} --agent-name <name>`;
|
|
4117
|
+
log(msg);
|
|
4118
|
+
warnings.push(msg);
|
|
4119
|
+
continue;
|
|
4120
|
+
}
|
|
4121
|
+
log(`Starting ${instanceId} (agent: ${storedName})...`);
|
|
4122
|
+
const result = await bridgeStart(instanceId, storedName, flags);
|
|
4123
|
+
if (result.ok) {
|
|
4124
|
+
started.push(instanceId);
|
|
4125
|
+
logSuccess(`${instanceId} started`);
|
|
4126
|
+
} else {
|
|
4127
|
+
failed.push(instanceId);
|
|
4128
|
+
logError(`${instanceId}: ${result.message}`);
|
|
4129
|
+
}
|
|
4130
|
+
log("");
|
|
4131
|
+
}
|
|
4132
|
+
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(", ")}`;
|
|
4133
|
+
return {
|
|
4134
|
+
ok: failed.length === 0 && started.length > 0,
|
|
4135
|
+
command: "bridge",
|
|
4136
|
+
code: started.length > 0 ? "TAP_BRIDGE_START_OK" : "TAP_BRIDGE_START_FAILED",
|
|
4137
|
+
message,
|
|
4138
|
+
warnings,
|
|
4139
|
+
data: { started, failed }
|
|
4140
|
+
};
|
|
4141
|
+
}
|
|
2918
4142
|
async function bridgeStopOne(identifier) {
|
|
2919
|
-
const repoRoot =
|
|
4143
|
+
const repoRoot = findRepoRoot();
|
|
2920
4144
|
const state = loadState(repoRoot);
|
|
2921
4145
|
if (!state) {
|
|
2922
4146
|
return {
|
|
@@ -2941,20 +4165,64 @@ async function bridgeStopOne(identifier) {
|
|
|
2941
4165
|
}
|
|
2942
4166
|
const instanceId = resolved.instanceId;
|
|
2943
4167
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
4168
|
+
const instance = state.instances[instanceId];
|
|
4169
|
+
const bridgeState = loadCurrentBridgeState(
|
|
4170
|
+
ctx.stateDir,
|
|
4171
|
+
instanceId,
|
|
4172
|
+
instance?.bridge
|
|
4173
|
+
);
|
|
4174
|
+
const appServer = bridgeState?.appServer ?? null;
|
|
2944
4175
|
logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
|
|
2945
4176
|
const stopped = await stopBridge({
|
|
2946
4177
|
instanceId,
|
|
2947
4178
|
stateDir: ctx.stateDir,
|
|
2948
4179
|
platform: ctx.platform
|
|
2949
4180
|
});
|
|
4181
|
+
let appServerStopped = false;
|
|
4182
|
+
let appServerTransferredTo = null;
|
|
4183
|
+
if (stopped) {
|
|
4184
|
+
logSuccess(`Bridge for ${instanceId} stopped`);
|
|
4185
|
+
} else {
|
|
4186
|
+
log(`No running bridge for ${instanceId}`);
|
|
4187
|
+
}
|
|
4188
|
+
if (appServer?.managed) {
|
|
4189
|
+
const sharedUsers = getSharedAppServerUsers(
|
|
4190
|
+
state,
|
|
4191
|
+
ctx.stateDir,
|
|
4192
|
+
instanceId,
|
|
4193
|
+
appServer.url
|
|
4194
|
+
);
|
|
4195
|
+
if (sharedUsers.length > 0) {
|
|
4196
|
+
const recipient = sharedUsers[0];
|
|
4197
|
+
if (transferManagedAppServerOwnership(
|
|
4198
|
+
state,
|
|
4199
|
+
ctx.stateDir,
|
|
4200
|
+
recipient,
|
|
4201
|
+
appServer
|
|
4202
|
+
)) {
|
|
4203
|
+
appServerTransferredTo = recipient;
|
|
4204
|
+
log(`Managed app-server ownership moved to ${recipient}`);
|
|
4205
|
+
} else {
|
|
4206
|
+
log(
|
|
4207
|
+
`Managed app-server left running at ${appServer.url} because ownership transfer failed`
|
|
4208
|
+
);
|
|
4209
|
+
}
|
|
4210
|
+
} else {
|
|
4211
|
+
appServerStopped = await stopManagedAppServer(appServer, ctx.platform);
|
|
4212
|
+
if (appServerStopped) {
|
|
4213
|
+
const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID: ${appServer.auth.gatewayPid}` : "";
|
|
4214
|
+
logSuccess(
|
|
4215
|
+
`Managed app-server stopped (PID: ${appServer.pid ?? "-"}${gatewayNote})`
|
|
4216
|
+
);
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
if (instance) {
|
|
4221
|
+
const updated = { ...instance, bridge: null };
|
|
4222
|
+
const newState = updateInstanceState(state, instanceId, updated);
|
|
4223
|
+
saveState(repoRoot, newState);
|
|
4224
|
+
}
|
|
2950
4225
|
if (stopped) {
|
|
2951
|
-
logSuccess(`Bridge for ${instanceId} stopped`);
|
|
2952
|
-
const instance2 = state.instances[instanceId];
|
|
2953
|
-
if (instance2) {
|
|
2954
|
-
const updated = { ...instance2, bridge: null };
|
|
2955
|
-
const newState = updateInstanceState(state, instanceId, updated);
|
|
2956
|
-
saveState(repoRoot, newState);
|
|
2957
|
-
}
|
|
2958
4226
|
return {
|
|
2959
4227
|
ok: true,
|
|
2960
4228
|
command: "bridge",
|
|
@@ -2962,16 +4230,12 @@ async function bridgeStopOne(identifier) {
|
|
|
2962
4230
|
code: "TAP_BRIDGE_STOP_OK",
|
|
2963
4231
|
message: `Bridge for ${instanceId} stopped`,
|
|
2964
4232
|
warnings: [],
|
|
2965
|
-
data: {
|
|
4233
|
+
data: {
|
|
4234
|
+
appServerStopped,
|
|
4235
|
+
appServerTransferredTo
|
|
4236
|
+
}
|
|
2966
4237
|
};
|
|
2967
4238
|
}
|
|
2968
|
-
log(`No running bridge for ${instanceId}`);
|
|
2969
|
-
const instance = state.instances[instanceId];
|
|
2970
|
-
if (instance?.bridge) {
|
|
2971
|
-
const updated = { ...instance, bridge: null };
|
|
2972
|
-
const newState = updateInstanceState(state, instanceId, updated);
|
|
2973
|
-
saveState(repoRoot, newState);
|
|
2974
|
-
}
|
|
2975
4239
|
return {
|
|
2976
4240
|
ok: true,
|
|
2977
4241
|
command: "bridge",
|
|
@@ -2979,11 +4243,14 @@ async function bridgeStopOne(identifier) {
|
|
|
2979
4243
|
code: "TAP_BRIDGE_NOT_RUNNING",
|
|
2980
4244
|
message: `No running bridge for ${instanceId}`,
|
|
2981
4245
|
warnings: [],
|
|
2982
|
-
data: {
|
|
4246
|
+
data: {
|
|
4247
|
+
appServerStopped,
|
|
4248
|
+
appServerTransferredTo
|
|
4249
|
+
}
|
|
2983
4250
|
};
|
|
2984
4251
|
}
|
|
2985
4252
|
async function bridgeStopAll() {
|
|
2986
|
-
const repoRoot =
|
|
4253
|
+
const repoRoot = findRepoRoot();
|
|
2987
4254
|
const state = loadState(repoRoot);
|
|
2988
4255
|
if (!state) {
|
|
2989
4256
|
return {
|
|
@@ -2998,9 +4265,22 @@ async function bridgeStopAll() {
|
|
|
2998
4265
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
2999
4266
|
const instanceIds = Object.keys(state.instances);
|
|
3000
4267
|
const stopped = [];
|
|
4268
|
+
const managedAppServers = /* @__PURE__ */ new Map();
|
|
3001
4269
|
logHeader("@hua-labs/tap bridge stop (all)");
|
|
3002
4270
|
let stateChanged = false;
|
|
3003
4271
|
for (const instanceId of instanceIds) {
|
|
4272
|
+
const bridgeState = loadCurrentBridgeState(
|
|
4273
|
+
ctx.stateDir,
|
|
4274
|
+
instanceId,
|
|
4275
|
+
state.instances[instanceId]?.bridge
|
|
4276
|
+
);
|
|
4277
|
+
const appServer = bridgeState?.appServer;
|
|
4278
|
+
if (appServer?.managed && appServer.pid != null) {
|
|
4279
|
+
managedAppServers.set(
|
|
4280
|
+
`${appServer.url}:${appServer.pid}:${appServer.auth?.gatewayPid ?? "-"}`,
|
|
4281
|
+
appServer
|
|
4282
|
+
);
|
|
4283
|
+
}
|
|
3004
4284
|
const didStop = await stopBridge({
|
|
3005
4285
|
instanceId,
|
|
3006
4286
|
stateDir: ctx.stateDir,
|
|
@@ -3016,6 +4296,16 @@ async function bridgeStopAll() {
|
|
|
3016
4296
|
stateChanged = true;
|
|
3017
4297
|
}
|
|
3018
4298
|
}
|
|
4299
|
+
const stoppedAppServers = [];
|
|
4300
|
+
for (const appServer of managedAppServers.values()) {
|
|
4301
|
+
if (await stopManagedAppServer(appServer, ctx.platform)) {
|
|
4302
|
+
stoppedAppServers.push(appServer.pid);
|
|
4303
|
+
const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID ${appServer.auth.gatewayPid}` : "";
|
|
4304
|
+
logSuccess(
|
|
4305
|
+
`Stopped app-server PID ${appServer.pid} (${appServer.url}${gatewayNote})`
|
|
4306
|
+
);
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
3019
4309
|
if (stateChanged) {
|
|
3020
4310
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3021
4311
|
saveState(repoRoot, state);
|
|
@@ -3028,11 +4318,11 @@ async function bridgeStopAll() {
|
|
|
3028
4318
|
code: stopped.length > 0 ? "TAP_BRIDGE_STOP_OK" : "TAP_BRIDGE_NOT_RUNNING",
|
|
3029
4319
|
message,
|
|
3030
4320
|
warnings: [],
|
|
3031
|
-
data: { stopped }
|
|
4321
|
+
data: { stopped, stoppedAppServers }
|
|
3032
4322
|
};
|
|
3033
4323
|
}
|
|
3034
4324
|
function bridgeStatusAll() {
|
|
3035
|
-
const repoRoot =
|
|
4325
|
+
const repoRoot = findRepoRoot();
|
|
3036
4326
|
const state = loadState(repoRoot);
|
|
3037
4327
|
if (!state) {
|
|
3038
4328
|
return {
|
|
@@ -3067,7 +4357,8 @@ function bridgeStatusAll() {
|
|
|
3067
4357
|
runtime: inst.runtime,
|
|
3068
4358
|
pid: null,
|
|
3069
4359
|
port: inst.port,
|
|
3070
|
-
lastHeartbeat: null
|
|
4360
|
+
lastHeartbeat: null,
|
|
4361
|
+
appServer: null
|
|
3071
4362
|
};
|
|
3072
4363
|
continue;
|
|
3073
4364
|
}
|
|
@@ -3075,7 +4366,7 @@ function bridgeStatusAll() {
|
|
|
3075
4366
|
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
3076
4367
|
const age = getHeartbeatAge(stateDir, instanceId);
|
|
3077
4368
|
const pid = bridgeState?.pid ?? null;
|
|
3078
|
-
const heartbeat =
|
|
4369
|
+
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
3079
4370
|
const pidStr = pid ? String(pid) : "-";
|
|
3080
4371
|
const portStr = inst.port ? String(inst.port) : "-";
|
|
3081
4372
|
const ageStr = age !== null ? formatAge(age) : "-";
|
|
@@ -3083,12 +4374,24 @@ function bridgeStatusAll() {
|
|
|
3083
4374
|
log(
|
|
3084
4375
|
`${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${statusColor.padEnd(10)} ${pidStr.padEnd(8)} ${portStr.padEnd(6)} ${ageStr}`
|
|
3085
4376
|
);
|
|
4377
|
+
if (bridgeState?.appServer) {
|
|
4378
|
+
log(` App server: ${formatAppServerState(bridgeState.appServer)}`);
|
|
4379
|
+
if (bridgeState.appServer.logPath) {
|
|
4380
|
+
log(` Server log: ${bridgeState.appServer.logPath}`);
|
|
4381
|
+
}
|
|
4382
|
+
if (bridgeState.appServer.auth) {
|
|
4383
|
+
log(
|
|
4384
|
+
` Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
|
|
4385
|
+
);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
3086
4388
|
bridges[instanceId] = {
|
|
3087
4389
|
status,
|
|
3088
4390
|
runtime: inst.runtime,
|
|
3089
4391
|
pid,
|
|
3090
4392
|
port: inst.port,
|
|
3091
|
-
lastHeartbeat: heartbeat
|
|
4393
|
+
lastHeartbeat: heartbeat,
|
|
4394
|
+
appServer: bridgeState?.appServer ?? null
|
|
3092
4395
|
};
|
|
3093
4396
|
}
|
|
3094
4397
|
if (instanceIds.length === 0) {
|
|
@@ -3105,7 +4408,7 @@ function bridgeStatusAll() {
|
|
|
3105
4408
|
};
|
|
3106
4409
|
}
|
|
3107
4410
|
function bridgeStatusOne(identifier) {
|
|
3108
|
-
const repoRoot =
|
|
4411
|
+
const repoRoot = findRepoRoot();
|
|
3109
4412
|
const state = loadState(repoRoot);
|
|
3110
4413
|
if (!state) {
|
|
3111
4414
|
return {
|
|
@@ -3162,7 +4465,8 @@ function bridgeStatusOne(identifier) {
|
|
|
3162
4465
|
bridgeMode: inst.bridgeMode,
|
|
3163
4466
|
pid: null,
|
|
3164
4467
|
port: inst.port,
|
|
3165
|
-
lastHeartbeat: null
|
|
4468
|
+
lastHeartbeat: null,
|
|
4469
|
+
appServer: null
|
|
3166
4470
|
}
|
|
3167
4471
|
};
|
|
3168
4472
|
}
|
|
@@ -3171,123 +4475,398 @@ function bridgeStatusOne(identifier) {
|
|
|
3171
4475
|
const status = getBridgeStatus(stateDir, instanceId);
|
|
3172
4476
|
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
3173
4477
|
const age = getHeartbeatAge(stateDir, instanceId);
|
|
4478
|
+
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
3174
4479
|
log(`Status: ${status}`);
|
|
3175
4480
|
if (bridgeState) {
|
|
3176
4481
|
log(`PID: ${bridgeState.pid}`);
|
|
3177
4482
|
log(
|
|
3178
|
-
`Heartbeat: ${
|
|
4483
|
+
`Heartbeat: ${heartbeat ?? "-"}${age !== null ? ` (${formatAge(age)})` : ""}`
|
|
3179
4484
|
);
|
|
3180
4485
|
log(
|
|
3181
|
-
`Log: ${
|
|
4486
|
+
`Log: ${path14.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
|
|
4487
|
+
);
|
|
4488
|
+
if (bridgeState.appServer) {
|
|
4489
|
+
log(`App server: ${bridgeState.appServer.url}`);
|
|
4490
|
+
log(`Server PID: ${bridgeState.appServer.pid ?? "-"}`);
|
|
4491
|
+
log(
|
|
4492
|
+
`Server mode: ${bridgeState.appServer.managed ? "managed" : "external"}`
|
|
4493
|
+
);
|
|
4494
|
+
log(
|
|
4495
|
+
`Health: ${bridgeState.appServer.healthy ? "healthy" : "unhealthy"}`
|
|
4496
|
+
);
|
|
4497
|
+
log(`Checked: ${bridgeState.appServer.lastCheckedAt}`);
|
|
4498
|
+
if (bridgeState.appServer.logPath) {
|
|
4499
|
+
log(`Server log: ${bridgeState.appServer.logPath}`);
|
|
4500
|
+
}
|
|
4501
|
+
if (bridgeState.appServer.auth) {
|
|
4502
|
+
log(`Auth: ${bridgeState.appServer.auth.mode}`);
|
|
4503
|
+
log(
|
|
4504
|
+
`Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
|
|
4505
|
+
);
|
|
4506
|
+
log(`Upstream: ${bridgeState.appServer.auth.upstreamUrl}`);
|
|
4507
|
+
log(`TUI connect: ${bridgeState.appServer.auth.upstreamUrl}`);
|
|
4508
|
+
log(`Gateway PID: ${bridgeState.appServer.auth.gatewayPid ?? "-"}`);
|
|
4509
|
+
if (bridgeState.appServer.auth.gatewayLogPath) {
|
|
4510
|
+
log(`Gateway log: ${bridgeState.appServer.auth.gatewayLogPath}`);
|
|
4511
|
+
}
|
|
4512
|
+
} else if (bridgeState.appServer.managed) {
|
|
4513
|
+
log(`Auth: none (--no-auth)`);
|
|
4514
|
+
log(`TUI connect: ${bridgeState.appServer.url}`);
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
log("");
|
|
4519
|
+
return {
|
|
4520
|
+
ok: true,
|
|
4521
|
+
command: "bridge",
|
|
4522
|
+
instanceId,
|
|
4523
|
+
runtime: inst.runtime,
|
|
4524
|
+
code: "TAP_BRIDGE_STATUS_OK",
|
|
4525
|
+
message: `${instanceId} bridge: ${status}`,
|
|
4526
|
+
warnings: [],
|
|
4527
|
+
data: {
|
|
4528
|
+
status,
|
|
4529
|
+
bridgeMode: inst.bridgeMode,
|
|
4530
|
+
pid: bridgeState?.pid ?? null,
|
|
4531
|
+
port: inst.port,
|
|
4532
|
+
lastHeartbeat: heartbeat,
|
|
4533
|
+
appServer: bridgeState?.appServer ?? null
|
|
4534
|
+
}
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
async function bridgeCommand(args) {
|
|
4538
|
+
const { positional, flags } = parseArgs(args);
|
|
4539
|
+
const subcommand = positional[0];
|
|
4540
|
+
const identifierArg = positional[1];
|
|
4541
|
+
const agentName = typeof flags["agent-name"] === "string" ? flags["agent-name"] : void 0;
|
|
4542
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
4543
|
+
log(BRIDGE_HELP);
|
|
4544
|
+
return {
|
|
4545
|
+
ok: true,
|
|
4546
|
+
command: "bridge",
|
|
4547
|
+
code: "TAP_NO_OP",
|
|
4548
|
+
message: BRIDGE_HELP,
|
|
4549
|
+
warnings: [],
|
|
4550
|
+
data: {}
|
|
4551
|
+
};
|
|
4552
|
+
}
|
|
4553
|
+
switch (subcommand) {
|
|
4554
|
+
case "start": {
|
|
4555
|
+
const wantsAll = flags["all"] === true || identifierArg === "--all";
|
|
4556
|
+
const hasInstance = identifierArg && identifierArg !== "--all";
|
|
4557
|
+
if (wantsAll && hasInstance) {
|
|
4558
|
+
return {
|
|
4559
|
+
ok: false,
|
|
4560
|
+
command: "bridge",
|
|
4561
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4562
|
+
message: `Cannot combine <instance> with --all. Use either:
|
|
4563
|
+
tap bridge start ${identifierArg}
|
|
4564
|
+
tap bridge start --all`,
|
|
4565
|
+
warnings: [],
|
|
4566
|
+
data: {}
|
|
4567
|
+
};
|
|
4568
|
+
}
|
|
4569
|
+
if (wantsAll) {
|
|
4570
|
+
return bridgeStartAll(flags);
|
|
4571
|
+
}
|
|
4572
|
+
if (!identifierArg) {
|
|
4573
|
+
return {
|
|
4574
|
+
ok: false,
|
|
4575
|
+
command: "bridge",
|
|
4576
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4577
|
+
message: "Missing instance. Usage: npx @hua-labs/tap bridge start <instance> or --all",
|
|
4578
|
+
warnings: [],
|
|
4579
|
+
data: {}
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
4582
|
+
return bridgeStart(identifierArg, agentName, flags);
|
|
4583
|
+
}
|
|
4584
|
+
case "stop": {
|
|
4585
|
+
if (!identifierArg) {
|
|
4586
|
+
return bridgeStopAll();
|
|
4587
|
+
}
|
|
4588
|
+
return bridgeStopOne(identifierArg);
|
|
4589
|
+
}
|
|
4590
|
+
case "status": {
|
|
4591
|
+
if (identifierArg) {
|
|
4592
|
+
return bridgeStatusOne(identifierArg);
|
|
4593
|
+
}
|
|
4594
|
+
return bridgeStatusAll();
|
|
4595
|
+
}
|
|
4596
|
+
default:
|
|
4597
|
+
return {
|
|
4598
|
+
ok: false,
|
|
4599
|
+
command: "bridge",
|
|
4600
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4601
|
+
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
|
|
4602
|
+
warnings: [],
|
|
4603
|
+
data: {}
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
// src/engine/dashboard.ts
|
|
4609
|
+
import * as fs15 from "fs";
|
|
4610
|
+
import * as path15 from "path";
|
|
4611
|
+
import { execSync as execSync5 } from "child_process";
|
|
4612
|
+
function collectAgents(commsDir) {
|
|
4613
|
+
const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
|
|
4614
|
+
if (!fs15.existsSync(heartbeatsPath)) return [];
|
|
4615
|
+
try {
|
|
4616
|
+
const raw = fs15.readFileSync(heartbeatsPath, "utf-8");
|
|
4617
|
+
const data = JSON.parse(raw);
|
|
4618
|
+
return Object.entries(data).map(([name, info]) => ({
|
|
4619
|
+
name: info.agent ?? name,
|
|
4620
|
+
status: info.status ?? null,
|
|
4621
|
+
lastActivity: info.lastActivity ?? info.timestamp ?? null,
|
|
4622
|
+
joinedAt: info.joinedAt ?? null
|
|
4623
|
+
}));
|
|
4624
|
+
} catch {
|
|
4625
|
+
return [];
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
function collectBridges(repoRoot) {
|
|
4629
|
+
const state = loadState(repoRoot);
|
|
4630
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
4631
|
+
const stateDir = config.stateDir;
|
|
4632
|
+
const bridges = [];
|
|
4633
|
+
if (state) {
|
|
4634
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
4635
|
+
if (!inst?.installed) continue;
|
|
4636
|
+
if (inst.bridgeMode !== "app-server") continue;
|
|
4637
|
+
const instanceId = id;
|
|
4638
|
+
const status = getBridgeStatus(stateDir, instanceId);
|
|
4639
|
+
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
4640
|
+
const age = getHeartbeatAge(stateDir, instanceId);
|
|
4641
|
+
bridges.push({
|
|
4642
|
+
instanceId: id,
|
|
4643
|
+
runtime: inst.runtime,
|
|
4644
|
+
status,
|
|
4645
|
+
pid: bridgeState?.pid ?? null,
|
|
4646
|
+
port: inst.port ?? null,
|
|
4647
|
+
heartbeatAge: age,
|
|
4648
|
+
headless: inst.headless?.enabled ?? false
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
const tmpDir = path15.join(repoRoot, ".tmp");
|
|
4653
|
+
if (fs15.existsSync(tmpDir)) {
|
|
4654
|
+
try {
|
|
4655
|
+
const dirs = fs15.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
|
|
4656
|
+
for (const dir of dirs) {
|
|
4657
|
+
const daemonPath = path15.join(tmpDir, dir, "bridge-daemon.json");
|
|
4658
|
+
if (!fs15.existsSync(daemonPath)) continue;
|
|
4659
|
+
try {
|
|
4660
|
+
const raw = fs15.readFileSync(daemonPath, "utf-8");
|
|
4661
|
+
const daemon = JSON.parse(raw);
|
|
4662
|
+
const alreadyCovered = bridges.some(
|
|
4663
|
+
(b) => b.pid === daemon.pid && b.pid !== null
|
|
4664
|
+
);
|
|
4665
|
+
if (alreadyCovered) continue;
|
|
4666
|
+
const agentFile = path15.join(tmpDir, dir, "agent-name.txt");
|
|
4667
|
+
const agentName = fs15.existsSync(agentFile) ? fs15.readFileSync(agentFile, "utf-8").trim() : dir;
|
|
4668
|
+
const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
|
|
4669
|
+
const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
|
|
4670
|
+
const port = portMatch ? parseInt(portMatch[1], 10) : null;
|
|
4671
|
+
bridges.push({
|
|
4672
|
+
instanceId: agentName,
|
|
4673
|
+
runtime: "codex",
|
|
4674
|
+
status: running ? "running" : "stale",
|
|
4675
|
+
pid: daemon.pid ?? null,
|
|
4676
|
+
port,
|
|
4677
|
+
heartbeatAge: null,
|
|
4678
|
+
headless: false
|
|
4679
|
+
});
|
|
4680
|
+
} catch {
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
} catch {
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
return bridges;
|
|
4687
|
+
}
|
|
4688
|
+
function collectPRs() {
|
|
4689
|
+
try {
|
|
4690
|
+
const output = execSync5(
|
|
4691
|
+
"gh pr list --state all --limit 10 --json number,title,author,state,url",
|
|
4692
|
+
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
3182
4693
|
);
|
|
4694
|
+
const prs = JSON.parse(output);
|
|
4695
|
+
return prs.map((pr) => ({
|
|
4696
|
+
number: pr.number,
|
|
4697
|
+
title: pr.title,
|
|
4698
|
+
author: pr.author.login,
|
|
4699
|
+
state: pr.state,
|
|
4700
|
+
url: pr.url
|
|
4701
|
+
}));
|
|
4702
|
+
} catch {
|
|
4703
|
+
return [];
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
function collectWarnings(bridges, agents) {
|
|
4707
|
+
const warnings = [];
|
|
4708
|
+
for (const bridge of bridges) {
|
|
4709
|
+
if (bridge.status === "stale") {
|
|
4710
|
+
warnings.push({
|
|
4711
|
+
level: "warn",
|
|
4712
|
+
message: `Bridge ${bridge.instanceId} is stale (PID ${bridge.pid} dead)`
|
|
4713
|
+
});
|
|
4714
|
+
}
|
|
4715
|
+
if (bridge.status === "running" && bridge.heartbeatAge !== null && bridge.heartbeatAge > 60) {
|
|
4716
|
+
warnings.push({
|
|
4717
|
+
level: "warn",
|
|
4718
|
+
message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
if (bridges.length === 0) {
|
|
4723
|
+
warnings.push({
|
|
4724
|
+
level: "warn",
|
|
4725
|
+
message: "No bridges configured"
|
|
4726
|
+
});
|
|
4727
|
+
}
|
|
4728
|
+
if (agents.length === 0) {
|
|
4729
|
+
warnings.push({
|
|
4730
|
+
level: "warn",
|
|
4731
|
+
message: "No agent heartbeats found"
|
|
4732
|
+
});
|
|
4733
|
+
}
|
|
4734
|
+
return warnings;
|
|
4735
|
+
}
|
|
4736
|
+
function collectDashboardSnapshot(repoRoot, commsDirOverride) {
|
|
4737
|
+
const { config } = resolveConfig(
|
|
4738
|
+
commsDirOverride ? { commsDir: commsDirOverride } : {},
|
|
4739
|
+
repoRoot
|
|
4740
|
+
);
|
|
4741
|
+
const resolved = config;
|
|
4742
|
+
const agents = collectAgents(resolved.commsDir);
|
|
4743
|
+
const bridges = collectBridges(resolved.repoRoot);
|
|
4744
|
+
const prs = collectPRs();
|
|
4745
|
+
const warnings = collectWarnings(bridges, agents);
|
|
4746
|
+
return {
|
|
4747
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4748
|
+
repoRoot: resolved.repoRoot,
|
|
4749
|
+
commsDir: resolved.commsDir,
|
|
4750
|
+
agents,
|
|
4751
|
+
bridges,
|
|
4752
|
+
prs,
|
|
4753
|
+
warnings
|
|
4754
|
+
};
|
|
4755
|
+
}
|
|
4756
|
+
|
|
4757
|
+
// src/commands/up.ts
|
|
4758
|
+
var UP_HELP = `
|
|
4759
|
+
Usage:
|
|
4760
|
+
tap-comms up [bridge-start options]
|
|
4761
|
+
|
|
4762
|
+
Description:
|
|
4763
|
+
Start all registered app-server bridge daemons with one command.
|
|
4764
|
+
This is the orchestration entrypoint for headless/background TAP operation.
|
|
4765
|
+
|
|
4766
|
+
Examples:
|
|
4767
|
+
npx @hua-labs/tap up
|
|
4768
|
+
npx @hua-labs/tap up --no-auth
|
|
4769
|
+
npx @hua-labs/tap up --busy-mode wait
|
|
4770
|
+
`.trim();
|
|
4771
|
+
async function upCommand(args) {
|
|
4772
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4773
|
+
log(UP_HELP);
|
|
4774
|
+
return {
|
|
4775
|
+
ok: true,
|
|
4776
|
+
command: "up",
|
|
4777
|
+
code: "TAP_NO_OP",
|
|
4778
|
+
message: UP_HELP,
|
|
4779
|
+
warnings: [],
|
|
4780
|
+
data: {}
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
const repoRoot = findRepoRoot();
|
|
4784
|
+
const result = await bridgeCommand(["start", "--all", ...args]);
|
|
4785
|
+
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
4786
|
+
const activeBridges = snapshot.bridges.filter(
|
|
4787
|
+
(bridge) => bridge.status === "running"
|
|
4788
|
+
).length;
|
|
4789
|
+
if (!result.ok) {
|
|
4790
|
+
return {
|
|
4791
|
+
...result,
|
|
4792
|
+
command: "up",
|
|
4793
|
+
data: {
|
|
4794
|
+
...result.data,
|
|
4795
|
+
snapshot
|
|
4796
|
+
}
|
|
4797
|
+
};
|
|
3183
4798
|
}
|
|
3184
|
-
log("");
|
|
3185
4799
|
return {
|
|
3186
4800
|
ok: true,
|
|
3187
|
-
command: "
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
message: `${instanceId} bridge: ${status}`,
|
|
3192
|
-
warnings: [],
|
|
4801
|
+
command: "up",
|
|
4802
|
+
code: "TAP_UP_OK",
|
|
4803
|
+
message: `tap up: ${activeBridges} bridge(s) running`,
|
|
4804
|
+
warnings: result.warnings,
|
|
3193
4805
|
data: {
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
pid: bridgeState?.pid ?? null,
|
|
3197
|
-
port: inst.port,
|
|
3198
|
-
lastHeartbeat: bridgeState?.lastHeartbeat ?? null
|
|
4806
|
+
...result.data,
|
|
4807
|
+
snapshot
|
|
3199
4808
|
}
|
|
3200
4809
|
};
|
|
3201
4810
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
4811
|
+
|
|
4812
|
+
// src/commands/down.ts
|
|
4813
|
+
var DOWN_HELP = `
|
|
4814
|
+
Usage:
|
|
4815
|
+
tap-comms down
|
|
4816
|
+
|
|
4817
|
+
Description:
|
|
4818
|
+
Stop all running bridge daemons and managed app-servers.
|
|
4819
|
+
|
|
4820
|
+
Examples:
|
|
4821
|
+
npx @hua-labs/tap down
|
|
4822
|
+
`.trim();
|
|
4823
|
+
async function downCommand(args) {
|
|
4824
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4825
|
+
log(DOWN_HELP);
|
|
3209
4826
|
return {
|
|
3210
4827
|
ok: true,
|
|
3211
|
-
command: "
|
|
4828
|
+
command: "down",
|
|
3212
4829
|
code: "TAP_NO_OP",
|
|
3213
|
-
message:
|
|
4830
|
+
message: DOWN_HELP,
|
|
3214
4831
|
warnings: [],
|
|
3215
4832
|
data: {}
|
|
3216
4833
|
};
|
|
3217
4834
|
}
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
};
|
|
3229
|
-
}
|
|
3230
|
-
return bridgeStart(identifierArg, agentName, flags);
|
|
3231
|
-
}
|
|
3232
|
-
case "stop": {
|
|
3233
|
-
if (!identifierArg) {
|
|
3234
|
-
return bridgeStopAll();
|
|
3235
|
-
}
|
|
3236
|
-
return bridgeStopOne(identifierArg);
|
|
3237
|
-
}
|
|
3238
|
-
case "status": {
|
|
3239
|
-
if (identifierArg) {
|
|
3240
|
-
return bridgeStatusOne(identifierArg);
|
|
4835
|
+
const repoRoot = findRepoRoot();
|
|
4836
|
+
const result = await bridgeCommand(["stop"]);
|
|
4837
|
+
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
4838
|
+
if (!result.ok) {
|
|
4839
|
+
return {
|
|
4840
|
+
...result,
|
|
4841
|
+
command: "down",
|
|
4842
|
+
data: {
|
|
4843
|
+
...result.data,
|
|
4844
|
+
snapshot
|
|
3241
4845
|
}
|
|
3242
|
-
|
|
3243
|
-
}
|
|
3244
|
-
default:
|
|
3245
|
-
return {
|
|
3246
|
-
ok: false,
|
|
3247
|
-
command: "bridge",
|
|
3248
|
-
code: "TAP_INVALID_ARGUMENT",
|
|
3249
|
-
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
|
|
3250
|
-
warnings: [],
|
|
3251
|
-
data: {}
|
|
3252
|
-
};
|
|
4846
|
+
};
|
|
3253
4847
|
}
|
|
4848
|
+
return {
|
|
4849
|
+
ok: true,
|
|
4850
|
+
command: "down",
|
|
4851
|
+
code: "TAP_DOWN_OK",
|
|
4852
|
+
message: `tap down: ${snapshot.bridges.filter((bridge) => bridge.status === "running").length} bridge(s) still running`,
|
|
4853
|
+
warnings: result.warnings,
|
|
4854
|
+
data: {
|
|
4855
|
+
...result.data,
|
|
4856
|
+
snapshot
|
|
4857
|
+
}
|
|
4858
|
+
};
|
|
3254
4859
|
}
|
|
3255
4860
|
|
|
3256
4861
|
// src/commands/serve.ts
|
|
3257
|
-
import * as
|
|
3258
|
-
import
|
|
3259
|
-
import { execSync as execSync4, spawn as spawn2 } from "child_process";
|
|
3260
|
-
function findServerEntry(repoRoot) {
|
|
3261
|
-
const candidates = [
|
|
3262
|
-
path14.join(repoRoot, "packages", "tap-plugin", "channels", "tap-comms.ts"),
|
|
3263
|
-
path14.join(
|
|
3264
|
-
repoRoot,
|
|
3265
|
-
"node_modules",
|
|
3266
|
-
"@hua-labs",
|
|
3267
|
-
"tap-plugin",
|
|
3268
|
-
"channels",
|
|
3269
|
-
"tap-comms.ts"
|
|
3270
|
-
)
|
|
3271
|
-
];
|
|
3272
|
-
for (const p of candidates) {
|
|
3273
|
-
if (fs14.existsSync(p)) return p;
|
|
3274
|
-
}
|
|
3275
|
-
return null;
|
|
3276
|
-
}
|
|
3277
|
-
function isBunInstalled() {
|
|
3278
|
-
try {
|
|
3279
|
-
execSync4("bun --version", { stdio: "pipe" });
|
|
3280
|
-
return true;
|
|
3281
|
-
} catch {
|
|
3282
|
-
return false;
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
4862
|
+
import * as path16 from "path";
|
|
4863
|
+
import { spawn as spawn2 } from "child_process";
|
|
3285
4864
|
async function serveCommand(args) {
|
|
3286
|
-
const repoRoot =
|
|
4865
|
+
const repoRoot = findRepoRoot();
|
|
3287
4866
|
let commsDir;
|
|
3288
4867
|
const commsDirIdx = args.indexOf("--comms-dir");
|
|
3289
4868
|
if (commsDirIdx !== -1 && args[commsDirIdx + 1]) {
|
|
3290
|
-
commsDir =
|
|
4869
|
+
commsDir = path16.resolve(args[commsDirIdx + 1]);
|
|
3291
4870
|
}
|
|
3292
4871
|
if (!commsDir && process.env.TAP_COMMS_DIR) {
|
|
3293
4872
|
commsDir = process.env.TAP_COMMS_DIR;
|
|
@@ -3308,37 +4887,29 @@ async function serveCommand(args) {
|
|
|
3308
4887
|
data: {}
|
|
3309
4888
|
};
|
|
3310
4889
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
code: "TAP_SERVE_BUN_REQUIRED",
|
|
3316
|
-
message: "bun is required to run the tap-comms MCP server. Install: https://bun.sh",
|
|
3317
|
-
warnings: [],
|
|
3318
|
-
data: {}
|
|
3319
|
-
};
|
|
3320
|
-
}
|
|
3321
|
-
const serverEntry = findServerEntry(repoRoot);
|
|
3322
|
-
if (!serverEntry) {
|
|
4890
|
+
const ctx = createAdapterContext(commsDir, repoRoot);
|
|
4891
|
+
const managed = buildManagedMcpServerSpec(ctx);
|
|
4892
|
+
if (!managed.command || !managed.sourcePath) {
|
|
4893
|
+
const fallbackMessage = managed.issues[0] ?? "tap-comms MCP server not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/.";
|
|
3323
4894
|
return {
|
|
3324
4895
|
ok: false,
|
|
3325
4896
|
command: "serve",
|
|
3326
|
-
code: "TAP_SERVE_NO_SERVER",
|
|
3327
|
-
message:
|
|
4897
|
+
code: managed.sourcePath ? "TAP_SERVE_BUN_REQUIRED" : "TAP_SERVE_NO_SERVER",
|
|
4898
|
+
message: fallbackMessage,
|
|
3328
4899
|
warnings: [],
|
|
3329
4900
|
data: {}
|
|
3330
4901
|
};
|
|
3331
4902
|
}
|
|
3332
|
-
const child = spawn2(
|
|
4903
|
+
const child = spawn2(managed.command, managed.args, {
|
|
3333
4904
|
stdio: "inherit",
|
|
3334
4905
|
env: {
|
|
3335
4906
|
...process.env,
|
|
3336
4907
|
TAP_COMMS_DIR: commsDir
|
|
3337
4908
|
}
|
|
3338
4909
|
});
|
|
3339
|
-
return new Promise((
|
|
4910
|
+
return new Promise((resolve11) => {
|
|
3340
4911
|
child.on("error", (err) => {
|
|
3341
|
-
|
|
4912
|
+
resolve11({
|
|
3342
4913
|
ok: false,
|
|
3343
4914
|
command: "serve",
|
|
3344
4915
|
code: "TAP_INTERNAL_ERROR",
|
|
@@ -3348,7 +4919,7 @@ async function serveCommand(args) {
|
|
|
3348
4919
|
});
|
|
3349
4920
|
});
|
|
3350
4921
|
child.on("exit", (code) => {
|
|
3351
|
-
|
|
4922
|
+
resolve11({
|
|
3352
4923
|
ok: code === 0,
|
|
3353
4924
|
command: "serve",
|
|
3354
4925
|
code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
|
|
@@ -3361,9 +4932,9 @@ async function serveCommand(args) {
|
|
|
3361
4932
|
}
|
|
3362
4933
|
|
|
3363
4934
|
// src/commands/init-worktree.ts
|
|
3364
|
-
import * as
|
|
3365
|
-
import * as
|
|
3366
|
-
import { execSync as
|
|
4935
|
+
import * as fs16 from "fs";
|
|
4936
|
+
import * as path17 from "path";
|
|
4937
|
+
import { execSync as execSync6 } from "child_process";
|
|
3367
4938
|
var INIT_WORKTREE_HELP = `
|
|
3368
4939
|
Usage:
|
|
3369
4940
|
tap-comms init-worktree [options]
|
|
@@ -3387,7 +4958,7 @@ function warn(warnings, message) {
|
|
|
3387
4958
|
}
|
|
3388
4959
|
function run(cmd, opts) {
|
|
3389
4960
|
try {
|
|
3390
|
-
return
|
|
4961
|
+
return execSync6(cmd, {
|
|
3391
4962
|
cwd: opts?.cwd,
|
|
3392
4963
|
encoding: "utf-8",
|
|
3393
4964
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -3399,12 +4970,12 @@ function run(cmd, opts) {
|
|
|
3399
4970
|
}
|
|
3400
4971
|
}
|
|
3401
4972
|
function toAbsolute(p) {
|
|
3402
|
-
const resolved =
|
|
4973
|
+
const resolved = path17.resolve(p);
|
|
3403
4974
|
return resolved.replace(/\\/g, "/");
|
|
3404
4975
|
}
|
|
3405
4976
|
function probeBun(candidate) {
|
|
3406
4977
|
try {
|
|
3407
|
-
const out =
|
|
4978
|
+
const out = execSync6(`"${candidate}" --version`, {
|
|
3408
4979
|
encoding: "utf-8",
|
|
3409
4980
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3410
4981
|
timeout: 5e3
|
|
@@ -3418,7 +4989,7 @@ function findBun() {
|
|
|
3418
4989
|
const candidates = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
|
|
3419
4990
|
for (const name of candidates) {
|
|
3420
4991
|
try {
|
|
3421
|
-
const out =
|
|
4992
|
+
const out = execSync6(
|
|
3422
4993
|
process.platform === "win32" ? `where ${name}` : `which ${name}`,
|
|
3423
4994
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
|
|
3424
4995
|
).trim();
|
|
@@ -3430,18 +5001,18 @@ function findBun() {
|
|
|
3430
5001
|
}
|
|
3431
5002
|
}
|
|
3432
5003
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3433
|
-
const bunHome =
|
|
5004
|
+
const bunHome = path17.join(
|
|
3434
5005
|
home,
|
|
3435
5006
|
".bun",
|
|
3436
5007
|
"bin",
|
|
3437
5008
|
process.platform === "win32" ? "bun.exe" : "bun"
|
|
3438
5009
|
);
|
|
3439
|
-
if (
|
|
5010
|
+
if (fs16.existsSync(bunHome) && probeBun(bunHome)) return bunHome;
|
|
3440
5011
|
return null;
|
|
3441
5012
|
}
|
|
3442
5013
|
function step1CreateWorktree(opts) {
|
|
3443
5014
|
log("Step 1/9: Creating worktree...");
|
|
3444
|
-
if (
|
|
5015
|
+
if (fs16.existsSync(opts.worktreePath)) {
|
|
3445
5016
|
logWarn(`Directory already exists: ${opts.worktreePath}`);
|
|
3446
5017
|
try {
|
|
3447
5018
|
run("git rev-parse --git-dir", { cwd: opts.worktreePath });
|
|
@@ -3503,22 +5074,22 @@ function step2MergeMain(opts, warnings) {
|
|
|
3503
5074
|
}
|
|
3504
5075
|
function step3CopyPermissions(opts, warnings) {
|
|
3505
5076
|
log("Step 3/9: Copying permissions...");
|
|
3506
|
-
const srcSettings =
|
|
5077
|
+
const srcSettings = path17.join(
|
|
3507
5078
|
opts.repoRoot,
|
|
3508
5079
|
".claude",
|
|
3509
5080
|
"settings.local.json"
|
|
3510
5081
|
);
|
|
3511
|
-
const destDir =
|
|
3512
|
-
const destSettings =
|
|
3513
|
-
if (!
|
|
5082
|
+
const destDir = path17.join(opts.worktreePath, ".claude");
|
|
5083
|
+
const destSettings = path17.join(destDir, "settings.local.json");
|
|
5084
|
+
if (!fs16.existsSync(srcSettings)) {
|
|
3514
5085
|
warn(
|
|
3515
5086
|
warnings,
|
|
3516
5087
|
"No .claude/settings.local.json found in main repo. Skipping."
|
|
3517
5088
|
);
|
|
3518
5089
|
return;
|
|
3519
5090
|
}
|
|
3520
|
-
|
|
3521
|
-
|
|
5091
|
+
fs16.mkdirSync(destDir, { recursive: true });
|
|
5092
|
+
fs16.copyFileSync(srcSettings, destSettings);
|
|
3522
5093
|
logSuccess("Copied settings.local.json");
|
|
3523
5094
|
try {
|
|
3524
5095
|
run("git update-index --skip-worktree .claude/settings.local.json", {
|
|
@@ -3543,7 +5114,7 @@ function step4GenerateMcpJson(opts, warnings) {
|
|
|
3543
5114
|
const wtAbs = toAbsolute(opts.worktreePath);
|
|
3544
5115
|
const bunAbs = toAbsolute(bunPath);
|
|
3545
5116
|
const commsAbs = toAbsolute(opts.commsDir);
|
|
3546
|
-
const channelEntry =
|
|
5117
|
+
const channelEntry = path17.join(
|
|
3547
5118
|
wtAbs,
|
|
3548
5119
|
"packages/tap-plugin/channels/tap-comms.ts"
|
|
3549
5120
|
);
|
|
@@ -3560,8 +5131,8 @@ function step4GenerateMcpJson(opts, warnings) {
|
|
|
3560
5131
|
}
|
|
3561
5132
|
}
|
|
3562
5133
|
};
|
|
3563
|
-
const mcpPath =
|
|
3564
|
-
|
|
5134
|
+
const mcpPath = path17.join(opts.worktreePath, ".mcp.json");
|
|
5135
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
3565
5136
|
logSuccess(`.mcp.json generated (absolute paths + cwd)`);
|
|
3566
5137
|
log(` bun: ${bunAbs}`);
|
|
3567
5138
|
log(` comms: ${commsAbs}`);
|
|
@@ -3599,16 +5170,16 @@ function step6BuildEslintPlugin(opts, warnings) {
|
|
|
3599
5170
|
}
|
|
3600
5171
|
function step7VerifyComms(opts, warnings) {
|
|
3601
5172
|
log("Step 7/9: Verifying comms directory...");
|
|
3602
|
-
if (!
|
|
5173
|
+
if (!fs16.existsSync(opts.commsDir)) {
|
|
3603
5174
|
warn(warnings, `Comms directory not found: ${opts.commsDir}`);
|
|
3604
5175
|
warn(warnings, "Create it or run: npx @hua-labs/tap init");
|
|
3605
5176
|
return;
|
|
3606
5177
|
}
|
|
3607
5178
|
const requiredDirs = ["inbox", "findings", "reviews", "letters"];
|
|
3608
5179
|
for (const dir of requiredDirs) {
|
|
3609
|
-
const dirPath =
|
|
3610
|
-
if (!
|
|
3611
|
-
|
|
5180
|
+
const dirPath = path17.join(opts.commsDir, dir);
|
|
5181
|
+
if (!fs16.existsSync(dirPath)) {
|
|
5182
|
+
fs16.mkdirSync(dirPath, { recursive: true });
|
|
3612
5183
|
logSuccess(`Created ${dir}/`);
|
|
3613
5184
|
}
|
|
3614
5185
|
}
|
|
@@ -3665,19 +5236,19 @@ async function initWorktreeCommand(args) {
|
|
|
3665
5236
|
data: {}
|
|
3666
5237
|
};
|
|
3667
5238
|
}
|
|
3668
|
-
const repoRoot =
|
|
5239
|
+
const repoRoot = findRepoRoot();
|
|
3669
5240
|
const { config } = resolveConfig({}, repoRoot);
|
|
3670
|
-
const branch = typeof flags["branch"] === "string" ? flags["branch"] :
|
|
5241
|
+
const branch = typeof flags["branch"] === "string" ? flags["branch"] : path17.basename(path17.resolve(worktreePath));
|
|
3671
5242
|
const base = typeof flags["base"] === "string" ? flags["base"] : "origin/main";
|
|
3672
5243
|
const mission = typeof flags["mission"] === "string" ? flags["mission"] : void 0;
|
|
3673
5244
|
const commsDir = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : config.commsDir;
|
|
3674
5245
|
const skipInstall = flags["skip-install"] === true;
|
|
3675
5246
|
const opts = {
|
|
3676
|
-
worktreePath:
|
|
5247
|
+
worktreePath: path17.resolve(worktreePath),
|
|
3677
5248
|
branch,
|
|
3678
5249
|
base,
|
|
3679
5250
|
mission,
|
|
3680
|
-
commsDir:
|
|
5251
|
+
commsDir: path17.resolve(commsDir),
|
|
3681
5252
|
skipInstall,
|
|
3682
5253
|
repoRoot
|
|
3683
5254
|
};
|
|
@@ -3691,183 +5262,34 @@ async function initWorktreeCommand(args) {
|
|
|
3691
5262
|
const warnings = [];
|
|
3692
5263
|
const created = step1CreateWorktree(opts);
|
|
3693
5264
|
if (!created) {
|
|
3694
|
-
return {
|
|
3695
|
-
ok: false,
|
|
3696
|
-
command: "init-worktree",
|
|
3697
|
-
code: "TAP_PATCH_FAILED",
|
|
3698
|
-
message: "Failed to create worktree.",
|
|
3699
|
-
warnings,
|
|
3700
|
-
data: {}
|
|
3701
|
-
};
|
|
3702
|
-
}
|
|
3703
|
-
step2MergeMain(opts, warnings);
|
|
3704
|
-
step3CopyPermissions(opts, warnings);
|
|
3705
|
-
step4GenerateMcpJson(opts, warnings);
|
|
3706
|
-
step5Install(opts, warnings);
|
|
3707
|
-
step6BuildEslintPlugin(opts, warnings);
|
|
3708
|
-
step7VerifyComms(opts, warnings);
|
|
3709
|
-
step8VerifyBun(warnings);
|
|
3710
|
-
step9Ready(opts);
|
|
3711
|
-
return {
|
|
3712
|
-
ok: true,
|
|
3713
|
-
command: "init-worktree",
|
|
3714
|
-
code: "TAP_INIT_OK",
|
|
3715
|
-
message: `Worktree initialized: ${opts.worktreePath}`,
|
|
3716
|
-
warnings,
|
|
3717
|
-
data: {
|
|
3718
|
-
path: opts.worktreePath,
|
|
3719
|
-
branch: opts.branch,
|
|
3720
|
-
commsDir: opts.commsDir
|
|
3721
|
-
}
|
|
3722
|
-
};
|
|
3723
|
-
}
|
|
3724
|
-
|
|
3725
|
-
// src/engine/dashboard.ts
|
|
3726
|
-
import * as fs16 from "fs";
|
|
3727
|
-
import * as path16 from "path";
|
|
3728
|
-
import { execSync as execSync6 } from "child_process";
|
|
3729
|
-
function collectAgents(commsDir) {
|
|
3730
|
-
const heartbeatsPath = path16.join(commsDir, "heartbeats.json");
|
|
3731
|
-
if (!fs16.existsSync(heartbeatsPath)) return [];
|
|
3732
|
-
try {
|
|
3733
|
-
const raw = fs16.readFileSync(heartbeatsPath, "utf-8");
|
|
3734
|
-
const data = JSON.parse(raw);
|
|
3735
|
-
return Object.entries(data).map(([name, info]) => ({
|
|
3736
|
-
name: info.agent ?? name,
|
|
3737
|
-
status: info.status ?? null,
|
|
3738
|
-
lastActivity: info.lastActivity ?? info.timestamp ?? null,
|
|
3739
|
-
joinedAt: info.joinedAt ?? null
|
|
3740
|
-
}));
|
|
3741
|
-
} catch {
|
|
3742
|
-
return [];
|
|
3743
|
-
}
|
|
3744
|
-
}
|
|
3745
|
-
function collectBridges(repoRoot) {
|
|
3746
|
-
const state = loadState(repoRoot);
|
|
3747
|
-
const { config } = resolveConfig({}, repoRoot);
|
|
3748
|
-
const stateDir = config.stateDir;
|
|
3749
|
-
const bridges = [];
|
|
3750
|
-
if (state) {
|
|
3751
|
-
for (const [id, inst] of Object.entries(state.instances)) {
|
|
3752
|
-
if (!inst?.installed) continue;
|
|
3753
|
-
if (inst.bridgeMode !== "app-server") continue;
|
|
3754
|
-
const instanceId = id;
|
|
3755
|
-
const status = getBridgeStatus(stateDir, instanceId);
|
|
3756
|
-
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
3757
|
-
const age = getHeartbeatAge(stateDir, instanceId);
|
|
3758
|
-
bridges.push({
|
|
3759
|
-
instanceId: id,
|
|
3760
|
-
runtime: inst.runtime,
|
|
3761
|
-
status,
|
|
3762
|
-
pid: bridgeState?.pid ?? null,
|
|
3763
|
-
port: inst.port ?? null,
|
|
3764
|
-
heartbeatAge: age,
|
|
3765
|
-
headless: inst.headless?.enabled ?? false
|
|
3766
|
-
});
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
const tmpDir = path16.join(repoRoot, ".tmp");
|
|
3770
|
-
if (fs16.existsSync(tmpDir)) {
|
|
3771
|
-
try {
|
|
3772
|
-
const dirs = fs16.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
|
|
3773
|
-
for (const dir of dirs) {
|
|
3774
|
-
const daemonPath = path16.join(tmpDir, dir, "bridge-daemon.json");
|
|
3775
|
-
if (!fs16.existsSync(daemonPath)) continue;
|
|
3776
|
-
try {
|
|
3777
|
-
const raw = fs16.readFileSync(daemonPath, "utf-8");
|
|
3778
|
-
const daemon = JSON.parse(raw);
|
|
3779
|
-
const alreadyCovered = bridges.some(
|
|
3780
|
-
(b) => b.pid === daemon.pid && b.pid !== null
|
|
3781
|
-
);
|
|
3782
|
-
if (alreadyCovered) continue;
|
|
3783
|
-
const agentFile = path16.join(tmpDir, dir, "agent-name.txt");
|
|
3784
|
-
const agentName = fs16.existsSync(agentFile) ? fs16.readFileSync(agentFile, "utf-8").trim() : dir;
|
|
3785
|
-
const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
|
|
3786
|
-
const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
|
|
3787
|
-
const port = portMatch ? parseInt(portMatch[1], 10) : null;
|
|
3788
|
-
bridges.push({
|
|
3789
|
-
instanceId: agentName,
|
|
3790
|
-
runtime: "codex",
|
|
3791
|
-
status: running ? "running" : "stale",
|
|
3792
|
-
pid: daemon.pid ?? null,
|
|
3793
|
-
port,
|
|
3794
|
-
heartbeatAge: null,
|
|
3795
|
-
headless: false
|
|
3796
|
-
});
|
|
3797
|
-
} catch {
|
|
3798
|
-
}
|
|
3799
|
-
}
|
|
3800
|
-
} catch {
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
return bridges;
|
|
3804
|
-
}
|
|
3805
|
-
function collectPRs() {
|
|
3806
|
-
try {
|
|
3807
|
-
const output = execSync6(
|
|
3808
|
-
"gh pr list --state all --limit 10 --json number,title,author,state,url",
|
|
3809
|
-
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
3810
|
-
);
|
|
3811
|
-
const prs = JSON.parse(output);
|
|
3812
|
-
return prs.map((pr) => ({
|
|
3813
|
-
number: pr.number,
|
|
3814
|
-
title: pr.title,
|
|
3815
|
-
author: pr.author.login,
|
|
3816
|
-
state: pr.state,
|
|
3817
|
-
url: pr.url
|
|
3818
|
-
}));
|
|
3819
|
-
} catch {
|
|
3820
|
-
return [];
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
function collectWarnings(bridges, agents) {
|
|
3824
|
-
const warnings = [];
|
|
3825
|
-
for (const bridge of bridges) {
|
|
3826
|
-
if (bridge.status === "stale") {
|
|
3827
|
-
warnings.push({
|
|
3828
|
-
level: "warn",
|
|
3829
|
-
message: `Bridge ${bridge.instanceId} is stale (PID ${bridge.pid} dead)`
|
|
3830
|
-
});
|
|
3831
|
-
}
|
|
3832
|
-
if (bridge.status === "running" && bridge.heartbeatAge !== null && bridge.heartbeatAge > 60) {
|
|
3833
|
-
warnings.push({
|
|
3834
|
-
level: "warn",
|
|
3835
|
-
message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
|
|
3836
|
-
});
|
|
3837
|
-
}
|
|
3838
|
-
}
|
|
3839
|
-
if (bridges.length === 0) {
|
|
3840
|
-
warnings.push({
|
|
3841
|
-
level: "warn",
|
|
3842
|
-
message: "No bridges configured"
|
|
3843
|
-
});
|
|
3844
|
-
}
|
|
3845
|
-
if (agents.length === 0) {
|
|
3846
|
-
warnings.push({
|
|
3847
|
-
level: "warn",
|
|
3848
|
-
message: "No agent heartbeats found"
|
|
3849
|
-
});
|
|
5265
|
+
return {
|
|
5266
|
+
ok: false,
|
|
5267
|
+
command: "init-worktree",
|
|
5268
|
+
code: "TAP_PATCH_FAILED",
|
|
5269
|
+
message: "Failed to create worktree.",
|
|
5270
|
+
warnings,
|
|
5271
|
+
data: {}
|
|
5272
|
+
};
|
|
3850
5273
|
}
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
);
|
|
3858
|
-
|
|
3859
|
-
const agents = collectAgents(resolved.commsDir);
|
|
3860
|
-
const bridges = collectBridges(resolved.repoRoot);
|
|
3861
|
-
const prs = collectPRs();
|
|
3862
|
-
const warnings = collectWarnings(bridges, agents);
|
|
5274
|
+
step2MergeMain(opts, warnings);
|
|
5275
|
+
step3CopyPermissions(opts, warnings);
|
|
5276
|
+
step4GenerateMcpJson(opts, warnings);
|
|
5277
|
+
step5Install(opts, warnings);
|
|
5278
|
+
step6BuildEslintPlugin(opts, warnings);
|
|
5279
|
+
step7VerifyComms(opts, warnings);
|
|
5280
|
+
step8VerifyBun(warnings);
|
|
5281
|
+
step9Ready(opts);
|
|
3863
5282
|
return {
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
5283
|
+
ok: true,
|
|
5284
|
+
command: "init-worktree",
|
|
5285
|
+
code: "TAP_INIT_OK",
|
|
5286
|
+
message: `Worktree initialized: ${opts.worktreePath}`,
|
|
5287
|
+
warnings,
|
|
5288
|
+
data: {
|
|
5289
|
+
path: opts.worktreePath,
|
|
5290
|
+
branch: opts.branch,
|
|
5291
|
+
commsDir: opts.commsDir
|
|
5292
|
+
}
|
|
3871
5293
|
};
|
|
3872
5294
|
}
|
|
3873
5295
|
|
|
@@ -3967,7 +5389,7 @@ async function dashboardCommand(args) {
|
|
|
3967
5389
|
const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : "5";
|
|
3968
5390
|
const intervalSeconds = Math.max(2, parseInt(intervalStr, 10) || 5);
|
|
3969
5391
|
const commsDirOverride = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : void 0;
|
|
3970
|
-
const repoRoot =
|
|
5392
|
+
const repoRoot = findRepoRoot();
|
|
3971
5393
|
if (watchMode) {
|
|
3972
5394
|
const run2 = () => {
|
|
3973
5395
|
const snapshot2 = collectDashboardSnapshot(repoRoot, commsDirOverride);
|
|
@@ -4015,6 +5437,544 @@ async function dashboardCommand(args) {
|
|
|
4015
5437
|
};
|
|
4016
5438
|
}
|
|
4017
5439
|
|
|
5440
|
+
// src/commands/doctor.ts
|
|
5441
|
+
import {
|
|
5442
|
+
existsSync as existsSync16,
|
|
5443
|
+
mkdirSync as mkdirSync10,
|
|
5444
|
+
readdirSync as readdirSync4,
|
|
5445
|
+
readFileSync as readFileSync14,
|
|
5446
|
+
statSync as statSync2,
|
|
5447
|
+
unlinkSync as unlinkSync3
|
|
5448
|
+
} from "fs";
|
|
5449
|
+
import { join as join17 } from "path";
|
|
5450
|
+
var PASS = "pass";
|
|
5451
|
+
var WARN = "warn";
|
|
5452
|
+
var FAIL = "fail";
|
|
5453
|
+
function countFiles(dir, ext = ".md") {
|
|
5454
|
+
if (!existsSync16(dir)) return 0;
|
|
5455
|
+
try {
|
|
5456
|
+
return readdirSync4(dir).filter((f) => f.endsWith(ext)).length;
|
|
5457
|
+
} catch {
|
|
5458
|
+
return 0;
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
function recentFileCount(dir, withinMs) {
|
|
5462
|
+
if (!existsSync16(dir)) return 0;
|
|
5463
|
+
const cutoff = Date.now() - withinMs;
|
|
5464
|
+
let count = 0;
|
|
5465
|
+
try {
|
|
5466
|
+
for (const f of readdirSync4(dir)) {
|
|
5467
|
+
if (!f.endsWith(".md")) continue;
|
|
5468
|
+
try {
|
|
5469
|
+
if (statSync2(join17(dir, f)).mtimeMs > cutoff) count++;
|
|
5470
|
+
} catch {
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
} catch {
|
|
5474
|
+
}
|
|
5475
|
+
return count;
|
|
5476
|
+
}
|
|
5477
|
+
function loadBridgeRuntimeHeartbeat(bridgeState) {
|
|
5478
|
+
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
5479
|
+
if (!runtimeStateDir) {
|
|
5480
|
+
return null;
|
|
5481
|
+
}
|
|
5482
|
+
const heartbeatPath = join17(runtimeStateDir, "heartbeat.json");
|
|
5483
|
+
if (!existsSync16(heartbeatPath)) {
|
|
5484
|
+
return null;
|
|
5485
|
+
}
|
|
5486
|
+
try {
|
|
5487
|
+
return JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
|
|
5488
|
+
} catch {
|
|
5489
|
+
return null;
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
function checkComms(commsDir) {
|
|
5493
|
+
const checks = [];
|
|
5494
|
+
checks.push({
|
|
5495
|
+
name: "comms directory",
|
|
5496
|
+
status: existsSync16(commsDir) ? PASS : FAIL,
|
|
5497
|
+
message: existsSync16(commsDir) ? commsDir : `Not found: ${commsDir}`,
|
|
5498
|
+
fix: existsSync16(commsDir) ? void 0 : () => {
|
|
5499
|
+
mkdirSync10(commsDir, { recursive: true });
|
|
5500
|
+
return `Created ${commsDir}`;
|
|
5501
|
+
}
|
|
5502
|
+
});
|
|
5503
|
+
for (const [subdir, required] of [
|
|
5504
|
+
["inbox", true],
|
|
5505
|
+
["reviews", false],
|
|
5506
|
+
["findings", false]
|
|
5507
|
+
]) {
|
|
5508
|
+
const dir = join17(commsDir, subdir);
|
|
5509
|
+
const exists = existsSync16(dir);
|
|
5510
|
+
checks.push({
|
|
5511
|
+
name: `${subdir} directory`,
|
|
5512
|
+
status: exists ? PASS : required ? FAIL : WARN,
|
|
5513
|
+
message: exists ? subdir === "findings" ? `${countFiles(dir)} findings` : subdir === "inbox" ? `${countFiles(dir)} messages` : "exists" : `Missing${required ? "" : " (optional)"}`,
|
|
5514
|
+
fix: exists ? void 0 : () => {
|
|
5515
|
+
mkdirSync10(dir, { recursive: true });
|
|
5516
|
+
return `Created ${dir}`;
|
|
5517
|
+
}
|
|
5518
|
+
});
|
|
5519
|
+
}
|
|
5520
|
+
const heartbeats = join17(commsDir, "heartbeats.json");
|
|
5521
|
+
if (existsSync16(heartbeats)) {
|
|
5522
|
+
try {
|
|
5523
|
+
const store = JSON.parse(readFileSync14(heartbeats, "utf-8"));
|
|
5524
|
+
const agents = Object.keys(store);
|
|
5525
|
+
const now = Date.now();
|
|
5526
|
+
const active = agents.filter((a) => {
|
|
5527
|
+
const ts = store[a]?.lastActivity;
|
|
5528
|
+
return ts && now - new Date(ts).getTime() < 10 * 60 * 1e3;
|
|
5529
|
+
});
|
|
5530
|
+
checks.push({
|
|
5531
|
+
name: "heartbeats",
|
|
5532
|
+
status: active.length > 0 ? PASS : WARN,
|
|
5533
|
+
message: `${active.length} active / ${agents.length} total`
|
|
5534
|
+
});
|
|
5535
|
+
} catch {
|
|
5536
|
+
checks.push({
|
|
5537
|
+
name: "heartbeats",
|
|
5538
|
+
status: WARN,
|
|
5539
|
+
message: "File exists but unreadable"
|
|
5540
|
+
});
|
|
5541
|
+
}
|
|
5542
|
+
} else {
|
|
5543
|
+
checks.push({
|
|
5544
|
+
name: "heartbeats",
|
|
5545
|
+
status: WARN,
|
|
5546
|
+
message: "No heartbeats file"
|
|
5547
|
+
});
|
|
5548
|
+
}
|
|
5549
|
+
return checks;
|
|
5550
|
+
}
|
|
5551
|
+
function checkInstances(repoRoot, stateDir) {
|
|
5552
|
+
const checks = [];
|
|
5553
|
+
const state = loadState(repoRoot);
|
|
5554
|
+
if (!state) {
|
|
5555
|
+
checks.push({
|
|
5556
|
+
name: "tap state",
|
|
5557
|
+
status: FAIL,
|
|
5558
|
+
message: "Not initialized. Run: tap init"
|
|
5559
|
+
});
|
|
5560
|
+
return checks;
|
|
5561
|
+
}
|
|
5562
|
+
checks.push({
|
|
5563
|
+
name: "tap state",
|
|
5564
|
+
status: PASS,
|
|
5565
|
+
message: `v${state.schemaVersion}, ${getInstalledInstances(state).length} instance(s)`
|
|
5566
|
+
});
|
|
5567
|
+
const installed = getInstalledInstances(state);
|
|
5568
|
+
for (const id of installed) {
|
|
5569
|
+
const inst = state.instances[id];
|
|
5570
|
+
if (!inst) continue;
|
|
5571
|
+
if (inst.bridgeMode === "app-server") {
|
|
5572
|
+
const running = isBridgeRunning(stateDir, id);
|
|
5573
|
+
const bridgeState = loadBridgeState(stateDir, id);
|
|
5574
|
+
const heartbeatAge = getHeartbeatAge(stateDir, id);
|
|
5575
|
+
const runtimeHeartbeat = loadBridgeRuntimeHeartbeat(bridgeState);
|
|
5576
|
+
let status;
|
|
5577
|
+
let message;
|
|
5578
|
+
let fix;
|
|
5579
|
+
if (running && bridgeState) {
|
|
5580
|
+
if (heartbeatAge !== null && heartbeatAge > 120) {
|
|
5581
|
+
status = WARN;
|
|
5582
|
+
message = `PID ${bridgeState.pid} alive but heartbeat stale (${Math.round(heartbeatAge)}s ago)`;
|
|
5583
|
+
} else {
|
|
5584
|
+
status = PASS;
|
|
5585
|
+
message = `PID ${bridgeState.pid}, port ${inst.port ?? "auto"}`;
|
|
5586
|
+
}
|
|
5587
|
+
} else if (bridgeState && !running) {
|
|
5588
|
+
status = WARN;
|
|
5589
|
+
message = `Stale PID ${bridgeState.pid} (process dead)`;
|
|
5590
|
+
fix = () => {
|
|
5591
|
+
const appServer = bridgeState.appServer;
|
|
5592
|
+
if (appServer?.managed) {
|
|
5593
|
+
for (const pid of [appServer.auth?.gatewayPid, appServer.pid]) {
|
|
5594
|
+
if (pid) {
|
|
5595
|
+
try {
|
|
5596
|
+
process.kill(pid);
|
|
5597
|
+
} catch {
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5602
|
+
const pidPath = join17(stateDir, "pids", `bridge-${id}.json`);
|
|
5603
|
+
try {
|
|
5604
|
+
unlinkSync3(pidPath);
|
|
5605
|
+
} catch {
|
|
5606
|
+
}
|
|
5607
|
+
const currentState = loadState(repoRoot);
|
|
5608
|
+
if (currentState?.instances[id]) {
|
|
5609
|
+
currentState.instances[id].bridge = null;
|
|
5610
|
+
currentState.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5611
|
+
saveState(repoRoot, currentState);
|
|
5612
|
+
}
|
|
5613
|
+
return `Cleaned stale bridge + managed processes for ${id}`;
|
|
5614
|
+
};
|
|
5615
|
+
} else {
|
|
5616
|
+
status = WARN;
|
|
5617
|
+
message = "Not running";
|
|
5618
|
+
}
|
|
5619
|
+
const lastRuntimeError = runtimeHeartbeat?.lastError?.trim();
|
|
5620
|
+
if (lastRuntimeError) {
|
|
5621
|
+
status = status === FAIL ? FAIL : WARN;
|
|
5622
|
+
message = `${message}; bridge last error: ${lastRuntimeError}`;
|
|
5623
|
+
}
|
|
5624
|
+
checks.push({ name: `bridge: ${id}`, status, message, fix });
|
|
5625
|
+
} else {
|
|
5626
|
+
checks.push({
|
|
5627
|
+
name: `instance: ${id}`,
|
|
5628
|
+
status: PASS,
|
|
5629
|
+
message: `${inst.runtime} (${inst.bridgeMode})`
|
|
5630
|
+
});
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
return checks;
|
|
5634
|
+
}
|
|
5635
|
+
function checkMessageLifecycle(commsDir) {
|
|
5636
|
+
const checks = [];
|
|
5637
|
+
const inbox = join17(commsDir, "inbox");
|
|
5638
|
+
if (!existsSync16(inbox)) {
|
|
5639
|
+
checks.push({
|
|
5640
|
+
name: "message flow",
|
|
5641
|
+
status: FAIL,
|
|
5642
|
+
message: "No inbox"
|
|
5643
|
+
});
|
|
5644
|
+
return checks;
|
|
5645
|
+
}
|
|
5646
|
+
const total = countFiles(inbox);
|
|
5647
|
+
const recent1h = recentFileCount(inbox, 60 * 60 * 1e3);
|
|
5648
|
+
const recent10m = recentFileCount(inbox, 10 * 60 * 1e3);
|
|
5649
|
+
checks.push({
|
|
5650
|
+
name: "message flow",
|
|
5651
|
+
status: recent10m > 0 ? PASS : total > 0 ? WARN : FAIL,
|
|
5652
|
+
message: `${total} total, ${recent1h} in last 1h, ${recent10m} in last 10m`
|
|
5653
|
+
});
|
|
5654
|
+
const receiptsPath = join17(commsDir, "receipts", "receipts.json");
|
|
5655
|
+
if (existsSync16(receiptsPath)) {
|
|
5656
|
+
try {
|
|
5657
|
+
const receipts = JSON.parse(readFileSync14(receiptsPath, "utf-8"));
|
|
5658
|
+
const receiptCount = Object.keys(receipts).length;
|
|
5659
|
+
checks.push({
|
|
5660
|
+
name: "read receipts",
|
|
5661
|
+
status: PASS,
|
|
5662
|
+
message: `${receiptCount} receipts tracked`
|
|
5663
|
+
});
|
|
5664
|
+
} catch {
|
|
5665
|
+
checks.push({
|
|
5666
|
+
name: "read receipts",
|
|
5667
|
+
status: WARN,
|
|
5668
|
+
message: "File exists but unreadable"
|
|
5669
|
+
});
|
|
5670
|
+
}
|
|
5671
|
+
}
|
|
5672
|
+
return checks;
|
|
5673
|
+
}
|
|
5674
|
+
function checkMcpServer(repoRoot) {
|
|
5675
|
+
const checks = [];
|
|
5676
|
+
const mcpJson = join17(repoRoot, ".mcp.json");
|
|
5677
|
+
if (existsSync16(mcpJson)) {
|
|
5678
|
+
try {
|
|
5679
|
+
const config = JSON.parse(readFileSync14(mcpJson, "utf-8"));
|
|
5680
|
+
const hasTapComms = config?.mcpServers?.["tap-comms"];
|
|
5681
|
+
checks.push({
|
|
5682
|
+
name: "MCP config (.mcp.json)",
|
|
5683
|
+
status: hasTapComms ? PASS : WARN,
|
|
5684
|
+
message: hasTapComms ? `command: ${hasTapComms.command}` : "tap-comms not configured"
|
|
5685
|
+
});
|
|
5686
|
+
if (hasTapComms?.args?.[0]) {
|
|
5687
|
+
const mcpScript = hasTapComms.args[0];
|
|
5688
|
+
checks.push({
|
|
5689
|
+
name: "MCP server script",
|
|
5690
|
+
status: existsSync16(mcpScript) ? PASS : FAIL,
|
|
5691
|
+
message: existsSync16(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
|
|
5692
|
+
});
|
|
5693
|
+
}
|
|
5694
|
+
} catch {
|
|
5695
|
+
checks.push({
|
|
5696
|
+
name: "MCP config (.mcp.json)",
|
|
5697
|
+
status: WARN,
|
|
5698
|
+
message: "File exists but invalid JSON"
|
|
5699
|
+
});
|
|
5700
|
+
}
|
|
5701
|
+
} else {
|
|
5702
|
+
checks.push({
|
|
5703
|
+
name: "MCP config (.mcp.json)",
|
|
5704
|
+
status: WARN,
|
|
5705
|
+
message: "Not found \u2014 MCP channel notifications won't work"
|
|
5706
|
+
});
|
|
5707
|
+
}
|
|
5708
|
+
return checks;
|
|
5709
|
+
}
|
|
5710
|
+
function renderCheck(check, fixMode) {
|
|
5711
|
+
const icons = {
|
|
5712
|
+
pass: "[OK]",
|
|
5713
|
+
warn: "[!!]",
|
|
5714
|
+
fail: "[XX]",
|
|
5715
|
+
skip: "[--]"
|
|
5716
|
+
};
|
|
5717
|
+
const icon = icons[check.status] || "[??]";
|
|
5718
|
+
const fixable = fixMode && check.fix ? " (fixable)" : "";
|
|
5719
|
+
const msg = check.message ? ` \u2014 ${check.message}${fixable}` : "";
|
|
5720
|
+
return ` ${icon} ${check.name}${msg}`;
|
|
5721
|
+
}
|
|
5722
|
+
async function doctorCommand(args) {
|
|
5723
|
+
const repoRoot = findRepoRoot();
|
|
5724
|
+
const overrides = {};
|
|
5725
|
+
let fixMode = false;
|
|
5726
|
+
for (let i = 0; i < args.length; i++) {
|
|
5727
|
+
if (args[i] === "--comms-dir" && args[i + 1]) {
|
|
5728
|
+
overrides.commsDir = args[i + 1];
|
|
5729
|
+
}
|
|
5730
|
+
if (args[i] === "--fix") {
|
|
5731
|
+
fixMode = true;
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
const { config } = resolveConfig(overrides, repoRoot);
|
|
5735
|
+
const state = loadState(repoRoot);
|
|
5736
|
+
const commsDir = overrides.commsDir ? config.commsDir : state?.commsDir ?? config.commsDir;
|
|
5737
|
+
logHeader(`@hua-labs/tap doctor (v${version})${fixMode ? " --fix" : ""}`);
|
|
5738
|
+
function runAllChecks() {
|
|
5739
|
+
const checks = [];
|
|
5740
|
+
checks.push(...checkComms(commsDir));
|
|
5741
|
+
checks.push(...checkInstances(repoRoot, config.stateDir));
|
|
5742
|
+
checks.push(...checkMessageLifecycle(commsDir));
|
|
5743
|
+
checks.push(...checkMcpServer(repoRoot));
|
|
5744
|
+
return checks;
|
|
5745
|
+
}
|
|
5746
|
+
const initialChecks = runAllChecks();
|
|
5747
|
+
for (const section of ["Comms", "Instances", "Messages", "MCP"]) {
|
|
5748
|
+
const sectionChecks = {
|
|
5749
|
+
Comms: initialChecks.filter(
|
|
5750
|
+
(c) => [
|
|
5751
|
+
"comms directory",
|
|
5752
|
+
"inbox directory",
|
|
5753
|
+
"reviews directory",
|
|
5754
|
+
"findings directory",
|
|
5755
|
+
"heartbeats"
|
|
5756
|
+
].includes(c.name)
|
|
5757
|
+
),
|
|
5758
|
+
Instances: initialChecks.filter(
|
|
5759
|
+
(c) => c.name.startsWith("bridge:") || c.name.startsWith("instance:") || c.name === "tap state"
|
|
5760
|
+
),
|
|
5761
|
+
Messages: initialChecks.filter(
|
|
5762
|
+
(c) => ["message flow", "read receipts"].includes(c.name)
|
|
5763
|
+
),
|
|
5764
|
+
MCP: initialChecks.filter(
|
|
5765
|
+
(c) => c.name.startsWith("MCP") || c.name === "MCP server script"
|
|
5766
|
+
)
|
|
5767
|
+
}[section];
|
|
5768
|
+
if (sectionChecks.length > 0) {
|
|
5769
|
+
log(`${section}:`);
|
|
5770
|
+
for (const c of sectionChecks) log(renderCheck(c, fixMode));
|
|
5771
|
+
log("");
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
const fixed = [];
|
|
5775
|
+
let finalChecks = initialChecks;
|
|
5776
|
+
if (fixMode) {
|
|
5777
|
+
const fixable = initialChecks.filter(
|
|
5778
|
+
(c) => (c.status === "warn" || c.status === "fail") && c.fix
|
|
5779
|
+
);
|
|
5780
|
+
if (fixable.length > 0) {
|
|
5781
|
+
log("Fixes:");
|
|
5782
|
+
for (const c of fixable) {
|
|
5783
|
+
try {
|
|
5784
|
+
const desc = c.fix();
|
|
5785
|
+
fixed.push(desc);
|
|
5786
|
+
logSuccess(` ${desc}`);
|
|
5787
|
+
} catch (err) {
|
|
5788
|
+
logWarn(
|
|
5789
|
+
` Failed to fix ${c.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
5790
|
+
);
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
log("");
|
|
5794
|
+
log("Re-verifying...");
|
|
5795
|
+
finalChecks = runAllChecks();
|
|
5796
|
+
const postFails = finalChecks.filter((c) => c.status === "fail").length;
|
|
5797
|
+
const postWarns = finalChecks.filter((c) => c.status === "warn").length;
|
|
5798
|
+
log(
|
|
5799
|
+
` ${postFails === 0 ? "All clear" : `${postFails} remaining failures, ${postWarns} warnings`}`
|
|
5800
|
+
);
|
|
5801
|
+
} else {
|
|
5802
|
+
log("Nothing to fix.");
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
const passes = finalChecks.filter((c) => c.status === "pass").length;
|
|
5806
|
+
const warns = finalChecks.filter((c) => c.status === "warn").length;
|
|
5807
|
+
const fails = finalChecks.filter((c) => c.status === "fail").length;
|
|
5808
|
+
log("");
|
|
5809
|
+
log(
|
|
5810
|
+
`${finalChecks.length} checks: ${passes} passed, ${warns} warnings, ${fails} failures` + (fixed.length > 0 ? ` (${fixed.length} fixed)` : "")
|
|
5811
|
+
);
|
|
5812
|
+
return {
|
|
5813
|
+
ok: fails === 0,
|
|
5814
|
+
command: "doctor",
|
|
5815
|
+
code: fails === 0 ? "TAP_STATUS_OK" : "TAP_VERIFY_FAILED",
|
|
5816
|
+
message: `${passes} passed, ${warns} warnings, ${fails} failures`,
|
|
5817
|
+
warnings: finalChecks.filter((c) => c.status === "warn").map((c) => `${c.name}: ${c.message}`),
|
|
5818
|
+
data: {
|
|
5819
|
+
checks: finalChecks.map(({ fix, ...rest }) => rest),
|
|
5820
|
+
summary: { total: finalChecks.length, passes, warns, fails },
|
|
5821
|
+
fixed
|
|
5822
|
+
}
|
|
5823
|
+
};
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
// src/commands/comms.ts
|
|
5827
|
+
import { execSync as execSync7 } from "child_process";
|
|
5828
|
+
import * as fs17 from "fs";
|
|
5829
|
+
import * as path18 from "path";
|
|
5830
|
+
var COMMS_HELP = `
|
|
5831
|
+
Usage:
|
|
5832
|
+
tap-comms comms <subcommand>
|
|
5833
|
+
|
|
5834
|
+
Subcommands:
|
|
5835
|
+
pull Pull latest changes from comms remote repo
|
|
5836
|
+
push Commit and push comms changes to remote repo
|
|
5837
|
+
|
|
5838
|
+
Examples:
|
|
5839
|
+
npx @hua-labs/tap comms pull
|
|
5840
|
+
npx @hua-labs/tap comms push
|
|
5841
|
+
`.trim();
|
|
5842
|
+
function isGitRepo(dir) {
|
|
5843
|
+
return fs17.existsSync(path18.join(dir, ".git"));
|
|
5844
|
+
}
|
|
5845
|
+
function commsPull(commsDir) {
|
|
5846
|
+
logHeader("tap comms pull");
|
|
5847
|
+
if (!isGitRepo(commsDir)) {
|
|
5848
|
+
logError(`${commsDir} is not a git repository`);
|
|
5849
|
+
return {
|
|
5850
|
+
ok: false,
|
|
5851
|
+
command: "comms",
|
|
5852
|
+
code: "TAP_COMMS_NOT_REPO",
|
|
5853
|
+
message: `Comms directory is not a git repo. Use 'tap init --comms-repo <url>' to set up.`,
|
|
5854
|
+
warnings: [],
|
|
5855
|
+
data: { commsDir }
|
|
5856
|
+
};
|
|
5857
|
+
}
|
|
5858
|
+
try {
|
|
5859
|
+
const output = execSync7("git pull --rebase", {
|
|
5860
|
+
cwd: commsDir,
|
|
5861
|
+
encoding: "utf-8",
|
|
5862
|
+
stdio: "pipe"
|
|
5863
|
+
});
|
|
5864
|
+
logSuccess("Comms pull complete");
|
|
5865
|
+
if (output.trim()) log(output.trim());
|
|
5866
|
+
return {
|
|
5867
|
+
ok: true,
|
|
5868
|
+
command: "comms",
|
|
5869
|
+
code: "TAP_COMMS_PULL_OK",
|
|
5870
|
+
message: "Comms pull complete",
|
|
5871
|
+
warnings: [],
|
|
5872
|
+
data: { commsDir }
|
|
5873
|
+
};
|
|
5874
|
+
} catch (err) {
|
|
5875
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5876
|
+
logError(`Pull failed: ${msg}`);
|
|
5877
|
+
return {
|
|
5878
|
+
ok: false,
|
|
5879
|
+
command: "comms",
|
|
5880
|
+
code: "TAP_COMMS_PULL_FAILED",
|
|
5881
|
+
message: `Pull failed: ${msg}`,
|
|
5882
|
+
warnings: [],
|
|
5883
|
+
data: { commsDir }
|
|
5884
|
+
};
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
function commsPush(commsDir) {
|
|
5888
|
+
logHeader("tap comms push");
|
|
5889
|
+
if (!isGitRepo(commsDir)) {
|
|
5890
|
+
logError(`${commsDir} is not a git repository`);
|
|
5891
|
+
return {
|
|
5892
|
+
ok: false,
|
|
5893
|
+
command: "comms",
|
|
5894
|
+
code: "TAP_COMMS_NOT_REPO",
|
|
5895
|
+
message: `Comms directory is not a git repo. Use 'tap init --comms-repo <url>' to set up.`,
|
|
5896
|
+
warnings: [],
|
|
5897
|
+
data: { commsDir }
|
|
5898
|
+
};
|
|
5899
|
+
}
|
|
5900
|
+
try {
|
|
5901
|
+
execSync7("git add -A", { cwd: commsDir, stdio: "pipe" });
|
|
5902
|
+
const status = execSync7("git status --porcelain", {
|
|
5903
|
+
cwd: commsDir,
|
|
5904
|
+
encoding: "utf-8",
|
|
5905
|
+
stdio: "pipe"
|
|
5906
|
+
}).trim();
|
|
5907
|
+
if (!status) {
|
|
5908
|
+
log("Nothing to push \u2014 comms directory is clean");
|
|
5909
|
+
return {
|
|
5910
|
+
ok: true,
|
|
5911
|
+
command: "comms",
|
|
5912
|
+
code: "TAP_COMMS_PUSH_OK",
|
|
5913
|
+
message: "Nothing to push",
|
|
5914
|
+
warnings: [],
|
|
5915
|
+
data: { commsDir, changed: false }
|
|
5916
|
+
};
|
|
5917
|
+
}
|
|
5918
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5919
|
+
execSync7(`git commit -m "chore(comms): sync ${timestamp}"`, {
|
|
5920
|
+
cwd: commsDir,
|
|
5921
|
+
stdio: "pipe"
|
|
5922
|
+
});
|
|
5923
|
+
execSync7("git push", { cwd: commsDir, stdio: "pipe" });
|
|
5924
|
+
logSuccess("Comms push complete");
|
|
5925
|
+
return {
|
|
5926
|
+
ok: true,
|
|
5927
|
+
command: "comms",
|
|
5928
|
+
code: "TAP_COMMS_PUSH_OK",
|
|
5929
|
+
message: "Comms push complete",
|
|
5930
|
+
warnings: [],
|
|
5931
|
+
data: { commsDir, changed: true }
|
|
5932
|
+
};
|
|
5933
|
+
} catch (err) {
|
|
5934
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5935
|
+
logError(`Push failed: ${msg}`);
|
|
5936
|
+
return {
|
|
5937
|
+
ok: false,
|
|
5938
|
+
command: "comms",
|
|
5939
|
+
code: "TAP_COMMS_PUSH_FAILED",
|
|
5940
|
+
message: `Push failed: ${msg}`,
|
|
5941
|
+
warnings: [],
|
|
5942
|
+
data: { commsDir }
|
|
5943
|
+
};
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
async function commsCommand(args) {
|
|
5947
|
+
const subcommand = args[0];
|
|
5948
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
5949
|
+
log(COMMS_HELP);
|
|
5950
|
+
return {
|
|
5951
|
+
ok: true,
|
|
5952
|
+
command: "comms",
|
|
5953
|
+
code: "TAP_NO_OP",
|
|
5954
|
+
message: COMMS_HELP,
|
|
5955
|
+
warnings: [],
|
|
5956
|
+
data: {}
|
|
5957
|
+
};
|
|
5958
|
+
}
|
|
5959
|
+
const repoRoot = findRepoRoot();
|
|
5960
|
+
const commsDir = resolveCommsDir(args, repoRoot);
|
|
5961
|
+
switch (subcommand) {
|
|
5962
|
+
case "pull":
|
|
5963
|
+
return commsPull(commsDir);
|
|
5964
|
+
case "push":
|
|
5965
|
+
return commsPush(commsDir);
|
|
5966
|
+
default:
|
|
5967
|
+
return {
|
|
5968
|
+
ok: false,
|
|
5969
|
+
command: "comms",
|
|
5970
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
5971
|
+
message: `Unknown comms subcommand: ${subcommand}. Use pull or push.`,
|
|
5972
|
+
warnings: [],
|
|
5973
|
+
data: {}
|
|
5974
|
+
};
|
|
5975
|
+
}
|
|
5976
|
+
}
|
|
5977
|
+
|
|
4018
5978
|
// src/output.ts
|
|
4019
5979
|
function emitResult(result, jsonMode) {
|
|
4020
5980
|
if (jsonMode) {
|
|
@@ -4053,7 +6013,11 @@ Commands:
|
|
|
4053
6013
|
remove <instance> Remove an instance and rollback config
|
|
4054
6014
|
status Show installed instances and bridge status
|
|
4055
6015
|
bridge <sub> [inst] Manage bridges (start, stop, status)
|
|
6016
|
+
up Start all registered bridge daemons
|
|
6017
|
+
down Stop all running bridge daemons
|
|
6018
|
+
comms <pull|push> Sync comms directory with remote repo
|
|
4056
6019
|
dashboard Show unified ops dashboard
|
|
6020
|
+
doctor Diagnose tap infrastructure health
|
|
4057
6021
|
serve Start tap-comms MCP server (stdio)
|
|
4058
6022
|
version Show version
|
|
4059
6023
|
|
|
@@ -4077,7 +6041,11 @@ function normalizeCommandName(command) {
|
|
|
4077
6041
|
case "remove":
|
|
4078
6042
|
case "status":
|
|
4079
6043
|
case "bridge":
|
|
6044
|
+
case "up":
|
|
6045
|
+
case "down":
|
|
6046
|
+
case "comms":
|
|
4080
6047
|
case "dashboard":
|
|
6048
|
+
case "doctor":
|
|
4081
6049
|
case "serve":
|
|
4082
6050
|
return command;
|
|
4083
6051
|
default:
|
|
@@ -4127,9 +6095,21 @@ async function main() {
|
|
|
4127
6095
|
case "bridge":
|
|
4128
6096
|
result = await bridgeCommand(commandArgs);
|
|
4129
6097
|
break;
|
|
6098
|
+
case "up":
|
|
6099
|
+
result = await upCommand(commandArgs);
|
|
6100
|
+
break;
|
|
6101
|
+
case "down":
|
|
6102
|
+
result = await downCommand(commandArgs);
|
|
6103
|
+
break;
|
|
6104
|
+
case "comms":
|
|
6105
|
+
result = await commsCommand(commandArgs);
|
|
6106
|
+
break;
|
|
4130
6107
|
case "dashboard":
|
|
4131
6108
|
result = await dashboardCommand(commandArgs);
|
|
4132
6109
|
break;
|
|
6110
|
+
case "doctor":
|
|
6111
|
+
result = await doctorCommand(commandArgs);
|
|
6112
|
+
break;
|
|
4133
6113
|
case "serve": {
|
|
4134
6114
|
const serveResult = await serveCommand(commandArgs);
|
|
4135
6115
|
if (!serveResult.ok) {
|