@hua-labs/tap 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-bridge-runner.d.mts +10 -1
- package/dist/bridges/codex-bridge-runner.mjs +233 -121
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +2728 -991
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +139 -5
- package/dist/index.mjs +528 -69
- 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,218 @@
|
|
|
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
4
|
|
|
5
5
|
// src/state.ts
|
|
6
|
-
import * as
|
|
7
|
-
import * as
|
|
6
|
+
import * as fs3 from "fs";
|
|
7
|
+
import * as path3 from "path";
|
|
8
8
|
import * as crypto from "crypto";
|
|
9
9
|
|
|
10
10
|
// src/config/resolve.ts
|
|
11
|
+
import * as fs2 from "fs";
|
|
12
|
+
import * as path2 from "path";
|
|
13
|
+
|
|
14
|
+
// src/utils.ts
|
|
11
15
|
import * as fs from "fs";
|
|
12
16
|
import * as path from "path";
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
var VALID_RUNTIMES = ["claude", "codex", "gemini"];
|
|
18
|
+
function isValidRuntime(name) {
|
|
19
|
+
return VALID_RUNTIMES.includes(name);
|
|
20
|
+
}
|
|
21
|
+
function detectPlatform() {
|
|
22
|
+
return process.platform;
|
|
23
|
+
}
|
|
24
|
+
var _noGitWarned = false;
|
|
25
|
+
function _setNoGitWarned() {
|
|
26
|
+
_noGitWarned = true;
|
|
27
|
+
}
|
|
17
28
|
function findRepoRoot(startDir = process.cwd()) {
|
|
18
29
|
let dir = path.resolve(startDir);
|
|
19
30
|
while (true) {
|
|
20
31
|
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
21
|
-
if (fs.existsSync(path.join(dir, "package.json")))
|
|
32
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
33
|
+
if (!_noGitWarned) {
|
|
34
|
+
_setNoGitWarned();
|
|
35
|
+
logWarn(
|
|
36
|
+
"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."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
22
41
|
const parent = path.dirname(dir);
|
|
23
42
|
if (parent === dir) break;
|
|
24
43
|
dir = parent;
|
|
25
44
|
}
|
|
45
|
+
if (!_noGitWarned) {
|
|
46
|
+
_setNoGitWarned();
|
|
47
|
+
logWarn(
|
|
48
|
+
"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."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return process.cwd();
|
|
52
|
+
}
|
|
53
|
+
function resolveCommsDir(args, repoRoot) {
|
|
54
|
+
const idx = args.indexOf("--comms-dir");
|
|
55
|
+
if (idx !== -1 && args[idx + 1]) {
|
|
56
|
+
return path.resolve(args[idx + 1]);
|
|
57
|
+
}
|
|
58
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
59
|
+
return config.commsDir;
|
|
60
|
+
}
|
|
61
|
+
function createAdapterContext(commsDir, repoRoot) {
|
|
62
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
63
|
+
return {
|
|
64
|
+
commsDir: path.resolve(commsDir),
|
|
65
|
+
repoRoot: path.resolve(repoRoot),
|
|
66
|
+
stateDir: config.stateDir,
|
|
67
|
+
platform: detectPlatform()
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function parseArgs(args) {
|
|
71
|
+
const positional = [];
|
|
72
|
+
const flags = {};
|
|
73
|
+
for (let i = 0; i < args.length; i++) {
|
|
74
|
+
const arg = args[i];
|
|
75
|
+
if (arg.startsWith("--")) {
|
|
76
|
+
const key = arg.slice(2);
|
|
77
|
+
const next = args[i + 1];
|
|
78
|
+
if (next && !next.startsWith("--")) {
|
|
79
|
+
flags[key] = next;
|
|
80
|
+
i++;
|
|
81
|
+
} else {
|
|
82
|
+
flags[key] = true;
|
|
83
|
+
}
|
|
84
|
+
} else if (arg.startsWith("-")) {
|
|
85
|
+
flags[arg.slice(1)] = true;
|
|
86
|
+
} else {
|
|
87
|
+
positional.push(arg);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { positional, flags };
|
|
91
|
+
}
|
|
92
|
+
var _jsonMode = false;
|
|
93
|
+
function setJsonMode(enabled) {
|
|
94
|
+
_jsonMode = enabled;
|
|
95
|
+
}
|
|
96
|
+
function log(message) {
|
|
97
|
+
if (!_jsonMode) console.log(` ${message}`);
|
|
98
|
+
}
|
|
99
|
+
function logSuccess(message) {
|
|
100
|
+
if (!_jsonMode) console.log(` + ${message}`);
|
|
101
|
+
}
|
|
102
|
+
function logWarn(message) {
|
|
103
|
+
if (!_jsonMode) console.log(` ! ${message}`);
|
|
104
|
+
}
|
|
105
|
+
function logError(message) {
|
|
106
|
+
if (!_jsonMode) console.error(` x ${message}`);
|
|
107
|
+
}
|
|
108
|
+
function logHeader(message) {
|
|
109
|
+
if (!_jsonMode) console.log(`
|
|
110
|
+
${message}
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
function resolveInstanceId(identifier, state) {
|
|
114
|
+
if (state.instances[identifier]) {
|
|
115
|
+
return { ok: true, instanceId: identifier };
|
|
116
|
+
}
|
|
117
|
+
if (isValidRuntime(identifier)) {
|
|
118
|
+
const matches = Object.values(state.instances).filter(
|
|
119
|
+
(inst) => inst.runtime === identifier
|
|
120
|
+
);
|
|
121
|
+
if (matches.length === 1) {
|
|
122
|
+
return { ok: true, instanceId: matches[0].instanceId };
|
|
123
|
+
}
|
|
124
|
+
if (matches.length > 1) {
|
|
125
|
+
const ids = matches.map((m) => m.instanceId).join(", ");
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
code: "TAP_INSTANCE_AMBIGUOUS",
|
|
129
|
+
message: `Multiple ${identifier} instances found: ${ids}. Specify one explicitly.`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
136
|
+
message: `Instance not found: ${identifier}`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function buildInstanceId(runtime, name) {
|
|
140
|
+
return name ? `${runtime}-${name}` : runtime;
|
|
141
|
+
}
|
|
142
|
+
function findPortConflict(state, port, excludeInstanceId) {
|
|
143
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
144
|
+
if (id !== excludeInstanceId && inst.port === port) return id;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/config/resolve.ts
|
|
150
|
+
var SHARED_CONFIG_FILE = "tap-config.json";
|
|
151
|
+
var LOCAL_CONFIG_FILE = "tap-config.local.json";
|
|
152
|
+
var LEGACY_CONFIG_FILE = ".tap-config";
|
|
153
|
+
var DEFAULT_RUNTIME_COMMAND = "node";
|
|
154
|
+
var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
155
|
+
function findRepoRoot2(startDir = process.cwd()) {
|
|
156
|
+
let dir = path2.resolve(startDir);
|
|
157
|
+
while (true) {
|
|
158
|
+
if (fs2.existsSync(path2.join(dir, ".git"))) return dir;
|
|
159
|
+
if (fs2.existsSync(path2.join(dir, "package.json"))) {
|
|
160
|
+
if (!_noGitWarned) {
|
|
161
|
+
_setNoGitWarned();
|
|
162
|
+
console.error(
|
|
163
|
+
"[tap] warning: No .git directory found. Resolved via package.json. Use --comms-dir to specify explicitly."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return dir;
|
|
167
|
+
}
|
|
168
|
+
const parent = path2.dirname(dir);
|
|
169
|
+
if (parent === dir) break;
|
|
170
|
+
dir = parent;
|
|
171
|
+
}
|
|
172
|
+
if (!_noGitWarned) {
|
|
173
|
+
_setNoGitWarned();
|
|
174
|
+
console.error(
|
|
175
|
+
"[tap] warning: No git repository found. Using cwd as root. Run 'git init' or use --comms-dir."
|
|
176
|
+
);
|
|
177
|
+
}
|
|
26
178
|
return process.cwd();
|
|
27
179
|
}
|
|
28
180
|
function loadJsonFile(filePath) {
|
|
29
|
-
if (!
|
|
181
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
30
182
|
try {
|
|
31
|
-
const raw =
|
|
183
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
32
184
|
return JSON.parse(raw);
|
|
33
185
|
} catch {
|
|
34
186
|
return null;
|
|
35
187
|
}
|
|
36
188
|
}
|
|
37
189
|
function loadSharedConfig(repoRoot) {
|
|
38
|
-
return loadJsonFile(
|
|
190
|
+
return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
|
|
39
191
|
}
|
|
40
192
|
function loadLocalConfig(repoRoot) {
|
|
41
|
-
return loadJsonFile(
|
|
193
|
+
return loadJsonFile(path2.join(repoRoot, LOCAL_CONFIG_FILE));
|
|
194
|
+
}
|
|
195
|
+
function readLegacyShellValue(configText, key) {
|
|
196
|
+
const match = configText.match(new RegExp(`^${key}="?(.+?)"?$`, "m"));
|
|
197
|
+
return match?.[1]?.trim() || null;
|
|
198
|
+
}
|
|
199
|
+
function loadLegacyShellConfig(repoRoot) {
|
|
200
|
+
const filePath = path2.join(repoRoot, LEGACY_CONFIG_FILE);
|
|
201
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
202
|
+
try {
|
|
203
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
204
|
+
const commsDir = readLegacyShellValue(raw, "TAP_COMMS_DIR");
|
|
205
|
+
if (!commsDir) return null;
|
|
206
|
+
return { commsDir };
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
42
210
|
}
|
|
43
211
|
function resolveConfig(overrides = {}, startDir) {
|
|
44
|
-
const repoRoot =
|
|
212
|
+
const repoRoot = findRepoRoot2(startDir);
|
|
45
213
|
const shared = loadSharedConfig(repoRoot) ?? {};
|
|
46
214
|
const local = loadLocalConfig(repoRoot) ?? {};
|
|
215
|
+
const legacy = loadLegacyShellConfig(repoRoot) ?? {};
|
|
47
216
|
const sources = {
|
|
48
217
|
repoRoot: "auto",
|
|
49
218
|
commsDir: "auto",
|
|
@@ -53,10 +222,10 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
53
222
|
};
|
|
54
223
|
let commsDir;
|
|
55
224
|
if (overrides.commsDir) {
|
|
56
|
-
commsDir =
|
|
225
|
+
commsDir = resolvePath(repoRoot, overrides.commsDir);
|
|
57
226
|
sources.commsDir = "cli-flag";
|
|
58
227
|
} else if (process.env.TAP_COMMS_DIR) {
|
|
59
|
-
commsDir =
|
|
228
|
+
commsDir = resolvePath(repoRoot, process.env.TAP_COMMS_DIR);
|
|
60
229
|
sources.commsDir = "env";
|
|
61
230
|
} else if (local.commsDir) {
|
|
62
231
|
commsDir = resolvePath(repoRoot, local.commsDir);
|
|
@@ -64,15 +233,18 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
64
233
|
} else if (shared.commsDir) {
|
|
65
234
|
commsDir = resolvePath(repoRoot, shared.commsDir);
|
|
66
235
|
sources.commsDir = "shared-config";
|
|
236
|
+
} else if (legacy.commsDir) {
|
|
237
|
+
commsDir = resolvePath(repoRoot, legacy.commsDir);
|
|
238
|
+
sources.commsDir = "legacy-shell-config";
|
|
67
239
|
} else {
|
|
68
|
-
commsDir =
|
|
240
|
+
commsDir = path2.join(repoRoot, "tap-comms");
|
|
69
241
|
}
|
|
70
242
|
let stateDir;
|
|
71
243
|
if (overrides.stateDir) {
|
|
72
|
-
stateDir =
|
|
244
|
+
stateDir = resolvePath(repoRoot, overrides.stateDir);
|
|
73
245
|
sources.stateDir = "cli-flag";
|
|
74
246
|
} else if (process.env.TAP_STATE_DIR) {
|
|
75
|
-
stateDir =
|
|
247
|
+
stateDir = resolvePath(repoRoot, process.env.TAP_STATE_DIR);
|
|
76
248
|
sources.stateDir = "env";
|
|
77
249
|
} else if (local.stateDir) {
|
|
78
250
|
stateDir = resolvePath(repoRoot, local.stateDir);
|
|
@@ -81,7 +253,7 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
81
253
|
stateDir = resolvePath(repoRoot, shared.stateDir);
|
|
82
254
|
sources.stateDir = "shared-config";
|
|
83
255
|
} else {
|
|
84
|
-
stateDir =
|
|
256
|
+
stateDir = path2.join(repoRoot, ".tap-comms");
|
|
85
257
|
}
|
|
86
258
|
let runtimeCommand;
|
|
87
259
|
if (overrides.runtimeCommand) {
|
|
@@ -121,7 +293,21 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
121
293
|
};
|
|
122
294
|
}
|
|
123
295
|
function resolvePath(repoRoot, p) {
|
|
124
|
-
|
|
296
|
+
const normalized = normalizeTapPath(p);
|
|
297
|
+
return path2.isAbsolute(normalized) ? normalized : path2.resolve(repoRoot, normalized);
|
|
298
|
+
}
|
|
299
|
+
function normalizeTapPath(input) {
|
|
300
|
+
const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
|
|
301
|
+
if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
302
|
+
return trimmed;
|
|
303
|
+
}
|
|
304
|
+
if (process.platform === "win32") {
|
|
305
|
+
const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
|
|
306
|
+
if (match) {
|
|
307
|
+
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return trimmed;
|
|
125
311
|
}
|
|
126
312
|
|
|
127
313
|
// src/state.ts
|
|
@@ -132,10 +318,10 @@ function getStateDir(repoRoot) {
|
|
|
132
318
|
return config.stateDir;
|
|
133
319
|
}
|
|
134
320
|
function getStatePath(repoRoot) {
|
|
135
|
-
return
|
|
321
|
+
return path3.join(getStateDir(repoRoot), STATE_FILE);
|
|
136
322
|
}
|
|
137
323
|
function stateExists(repoRoot) {
|
|
138
|
-
return
|
|
324
|
+
return fs3.existsSync(getStatePath(repoRoot));
|
|
139
325
|
}
|
|
140
326
|
function migrateStateV1toV2(v1) {
|
|
141
327
|
const instances = {};
|
|
@@ -163,8 +349,8 @@ function migrateStateV1toV2(v1) {
|
|
|
163
349
|
}
|
|
164
350
|
function loadState(repoRoot) {
|
|
165
351
|
const statePath = getStatePath(repoRoot);
|
|
166
|
-
if (!
|
|
167
|
-
const raw =
|
|
352
|
+
if (!fs3.existsSync(statePath)) return null;
|
|
353
|
+
const raw = fs3.readFileSync(statePath, "utf-8");
|
|
168
354
|
const parsed = JSON.parse(raw);
|
|
169
355
|
if (parsed.schemaVersion === 1 || parsed.runtimes) {
|
|
170
356
|
const migrated = migrateStateV1toV2(parsed);
|
|
@@ -175,11 +361,11 @@ function loadState(repoRoot) {
|
|
|
175
361
|
}
|
|
176
362
|
function saveState(repoRoot, state) {
|
|
177
363
|
const stateDir = getStateDir(repoRoot);
|
|
178
|
-
|
|
364
|
+
fs3.mkdirSync(stateDir, { recursive: true });
|
|
179
365
|
const statePath = getStatePath(repoRoot);
|
|
180
366
|
const tmp = `${statePath}.tmp.${process.pid}`;
|
|
181
|
-
|
|
182
|
-
|
|
367
|
+
fs3.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
|
|
368
|
+
fs3.renameSync(tmp, statePath);
|
|
183
369
|
}
|
|
184
370
|
function createInitialState(commsDir, repoRoot, packageVersion) {
|
|
185
371
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -187,8 +373,8 @@ function createInitialState(commsDir, repoRoot, packageVersion) {
|
|
|
187
373
|
schemaVersion: SCHEMA_VERSION,
|
|
188
374
|
createdAt: now,
|
|
189
375
|
updatedAt: now,
|
|
190
|
-
commsDir:
|
|
191
|
-
repoRoot:
|
|
376
|
+
commsDir: path3.resolve(commsDir),
|
|
377
|
+
repoRoot: path3.resolve(repoRoot),
|
|
192
378
|
packageVersion,
|
|
193
379
|
instances: {}
|
|
194
380
|
};
|
|
@@ -217,186 +403,85 @@ function getInstalledInstances(state) {
|
|
|
217
403
|
);
|
|
218
404
|
}
|
|
219
405
|
function ensureBackupDir(stateDir, instanceId) {
|
|
220
|
-
const backupDir =
|
|
221
|
-
|
|
406
|
+
const backupDir = path3.join(stateDir, "backups", instanceId);
|
|
407
|
+
fs3.mkdirSync(backupDir, { recursive: true });
|
|
222
408
|
return backupDir;
|
|
223
409
|
}
|
|
224
410
|
function backupFile(filePath, backupDir) {
|
|
225
|
-
const basename3 =
|
|
411
|
+
const basename3 = path3.basename(filePath);
|
|
226
412
|
const hash = fileHash(filePath);
|
|
227
|
-
const backupPath =
|
|
228
|
-
|
|
413
|
+
const backupPath = path3.join(backupDir, `${basename3}.${hash}.bak`);
|
|
414
|
+
fs3.copyFileSync(filePath, backupPath);
|
|
229
415
|
return backupPath;
|
|
230
416
|
}
|
|
231
417
|
function fileHash(filePath) {
|
|
232
|
-
if (!
|
|
233
|
-
const content =
|
|
418
|
+
if (!fs3.existsSync(filePath)) return "";
|
|
419
|
+
const content = fs3.readFileSync(filePath);
|
|
234
420
|
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
235
421
|
}
|
|
236
422
|
|
|
237
|
-
// src/
|
|
238
|
-
import * as
|
|
239
|
-
import * as
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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;
|
|
423
|
+
// src/version.ts
|
|
424
|
+
import * as fs4 from "fs";
|
|
425
|
+
import * as path4 from "path";
|
|
426
|
+
import { fileURLToPath } from "url";
|
|
427
|
+
var FALLBACK_VERSION = "0.0.0";
|
|
428
|
+
function resolvePackageVersion(metaUrl = import.meta.url) {
|
|
429
|
+
const moduleDir = path4.dirname(fileURLToPath(metaUrl));
|
|
430
|
+
const packageJsonPath = path4.join(moduleDir, "..", "package.json");
|
|
431
|
+
try {
|
|
432
|
+
const parsed = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
|
|
433
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
434
|
+
return parsed.version;
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
255
437
|
}
|
|
256
|
-
return
|
|
438
|
+
return FALLBACK_VERSION;
|
|
257
439
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
440
|
+
var version = resolvePackageVersion();
|
|
441
|
+
|
|
442
|
+
// src/permissions.ts
|
|
443
|
+
import * as fs5 from "fs";
|
|
444
|
+
import * as path5 from "path";
|
|
445
|
+
import * as os from "os";
|
|
446
|
+
|
|
447
|
+
// src/toml.ts
|
|
448
|
+
function splitLines(content) {
|
|
449
|
+
return content.replace(/\r\n/g, "\n").split("\n");
|
|
265
450
|
}
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
commsDir: path3.resolve(commsDir),
|
|
270
|
-
repoRoot: path3.resolve(repoRoot),
|
|
271
|
-
stateDir: config.stateDir,
|
|
272
|
-
platform: detectPlatform()
|
|
273
|
-
};
|
|
451
|
+
function tableHeader(selector) {
|
|
452
|
+
return `[${selector}]`;
|
|
274
453
|
}
|
|
275
|
-
function
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
i++;
|
|
286
|
-
} else {
|
|
287
|
-
flags[key] = true;
|
|
454
|
+
function findTableRange(lines, selector) {
|
|
455
|
+
const header = tableHeader(selector);
|
|
456
|
+
for (let i = 0; i < lines.length; i++) {
|
|
457
|
+
if (lines[i].trim() !== header) continue;
|
|
458
|
+
let end = lines.length;
|
|
459
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
460
|
+
const trimmed = lines[j].trim();
|
|
461
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
462
|
+
end = j;
|
|
463
|
+
break;
|
|
288
464
|
}
|
|
289
|
-
} else if (arg.startsWith("-")) {
|
|
290
|
-
flags[arg.slice(1)] = true;
|
|
291
|
-
} else {
|
|
292
|
-
positional.push(arg);
|
|
293
465
|
}
|
|
466
|
+
return { start: i, end };
|
|
294
467
|
}
|
|
295
|
-
return
|
|
296
|
-
}
|
|
297
|
-
var _jsonMode = false;
|
|
298
|
-
function setJsonMode(enabled) {
|
|
299
|
-
_jsonMode = enabled;
|
|
468
|
+
return null;
|
|
300
469
|
}
|
|
301
|
-
function
|
|
302
|
-
|
|
470
|
+
function escapeBasicString(value) {
|
|
471
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
303
472
|
}
|
|
304
|
-
function
|
|
305
|
-
if (
|
|
473
|
+
function renderValue(value) {
|
|
474
|
+
if (Array.isArray(value)) {
|
|
475
|
+
return `[${value.map((item) => `"${escapeBasicString(item)}"`).join(", ")}]`;
|
|
476
|
+
}
|
|
477
|
+
return `"${escapeBasicString(value)}"`;
|
|
306
478
|
}
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// src/version.ts
|
|
355
|
-
var version = "0.1.0";
|
|
356
|
-
|
|
357
|
-
// src/permissions.ts
|
|
358
|
-
import * as fs4 from "fs";
|
|
359
|
-
import * as path4 from "path";
|
|
360
|
-
import * as os from "os";
|
|
361
|
-
|
|
362
|
-
// src/toml.ts
|
|
363
|
-
function splitLines(content) {
|
|
364
|
-
return content.replace(/\r\n/g, "\n").split("\n");
|
|
365
|
-
}
|
|
366
|
-
function tableHeader(selector) {
|
|
367
|
-
return `[${selector}]`;
|
|
368
|
-
}
|
|
369
|
-
function findTableRange(lines, selector) {
|
|
370
|
-
const header = tableHeader(selector);
|
|
371
|
-
for (let i = 0; i < lines.length; i++) {
|
|
372
|
-
if (lines[i].trim() !== header) continue;
|
|
373
|
-
let end = lines.length;
|
|
374
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
375
|
-
const trimmed = lines[j].trim();
|
|
376
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
377
|
-
end = j;
|
|
378
|
-
break;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return { start: i, end };
|
|
382
|
-
}
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
function escapeBasicString(value) {
|
|
386
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
387
|
-
}
|
|
388
|
-
function renderValue(value) {
|
|
389
|
-
if (Array.isArray(value)) {
|
|
390
|
-
return `[${value.map((item) => `"${escapeBasicString(item)}"`).join(", ")}]`;
|
|
391
|
-
}
|
|
392
|
-
return `"${escapeBasicString(value)}"`;
|
|
393
|
-
}
|
|
394
|
-
function extractTomlTable(content, selector) {
|
|
395
|
-
const lines = splitLines(content);
|
|
396
|
-
const range = findTableRange(lines, selector);
|
|
397
|
-
if (!range) return null;
|
|
398
|
-
return `${lines.slice(range.start, range.end).join("\n")}
|
|
399
|
-
`;
|
|
479
|
+
function extractTomlTable(content, selector) {
|
|
480
|
+
const lines = splitLines(content);
|
|
481
|
+
const range = findTableRange(lines, selector);
|
|
482
|
+
if (!range) return null;
|
|
483
|
+
return `${lines.slice(range.start, range.end).join("\n")}
|
|
484
|
+
`;
|
|
400
485
|
}
|
|
401
486
|
function removeTomlTable(content, selector) {
|
|
402
487
|
const lines = splitLines(content);
|
|
@@ -486,13 +571,13 @@ var CLAUDE_DENY_RULES = [
|
|
|
486
571
|
];
|
|
487
572
|
function applyClaudePermissions(repoRoot, mode) {
|
|
488
573
|
const warnings = [];
|
|
489
|
-
const claudeDir =
|
|
490
|
-
const settingsPath =
|
|
491
|
-
|
|
574
|
+
const claudeDir = path5.join(repoRoot, ".claude");
|
|
575
|
+
const settingsPath = path5.join(claudeDir, "settings.local.json");
|
|
576
|
+
fs5.mkdirSync(claudeDir, { recursive: true });
|
|
492
577
|
let settings = {};
|
|
493
|
-
if (
|
|
578
|
+
if (fs5.existsSync(settingsPath)) {
|
|
494
579
|
try {
|
|
495
|
-
settings = JSON.parse(
|
|
580
|
+
settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
|
|
496
581
|
} catch {
|
|
497
582
|
warnings.push(
|
|
498
583
|
".claude/settings.local.json was invalid JSON. Starting fresh."
|
|
@@ -506,8 +591,8 @@ function applyClaudePermissions(repoRoot, mode) {
|
|
|
506
591
|
const cleaned = existingDeny.filter((r) => !tapRuleSet.has(r));
|
|
507
592
|
settings.deny = cleaned;
|
|
508
593
|
const tmp2 = `${settingsPath}.tmp.${process.pid}`;
|
|
509
|
-
|
|
510
|
-
|
|
594
|
+
fs5.writeFileSync(tmp2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
595
|
+
fs5.renameSync(tmp2, settingsPath);
|
|
511
596
|
logWarn("Claude: full mode \u2014 tap deny rules removed. Use with caution.");
|
|
512
597
|
warnings.push("Full permission mode: tap deny rules removed.");
|
|
513
598
|
return { applied: true, warnings };
|
|
@@ -515,18 +600,18 @@ function applyClaudePermissions(repoRoot, mode) {
|
|
|
515
600
|
const newDeny = [.../* @__PURE__ */ new Set([...existingDeny, ...CLAUDE_DENY_RULES])];
|
|
516
601
|
settings.deny = newDeny;
|
|
517
602
|
const tmp = `${settingsPath}.tmp.${process.pid}`;
|
|
518
|
-
|
|
519
|
-
|
|
603
|
+
fs5.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
604
|
+
fs5.renameSync(tmp, settingsPath);
|
|
520
605
|
logSuccess(
|
|
521
606
|
`Claude: ${CLAUDE_DENY_RULES.length} deny rules applied to .claude/settings.local.json`
|
|
522
607
|
);
|
|
523
608
|
return { applied: true, warnings };
|
|
524
609
|
}
|
|
525
610
|
function findCodexConfigPath() {
|
|
526
|
-
return
|
|
611
|
+
return path5.join(os.homedir(), ".codex", "config.toml");
|
|
527
612
|
}
|
|
528
613
|
function canonicalizeTrustPath(targetPath) {
|
|
529
|
-
let resolved =
|
|
614
|
+
let resolved = path5.resolve(targetPath).replace(/\//g, "\\");
|
|
530
615
|
const driveRoot = /^[A-Za-z]:\\$/;
|
|
531
616
|
if (!driveRoot.test(resolved)) {
|
|
532
617
|
resolved = resolved.replace(/\\+$/g, "");
|
|
@@ -536,10 +621,10 @@ function canonicalizeTrustPath(targetPath) {
|
|
|
536
621
|
function applyCodexPermissions(repoRoot, commsDir, mode) {
|
|
537
622
|
const warnings = [];
|
|
538
623
|
const configPath = findCodexConfigPath();
|
|
539
|
-
|
|
624
|
+
fs5.mkdirSync(path5.dirname(configPath), { recursive: true });
|
|
540
625
|
let content = "";
|
|
541
|
-
if (
|
|
542
|
-
content =
|
|
626
|
+
if (fs5.existsSync(configPath)) {
|
|
627
|
+
content = fs5.readFileSync(configPath, "utf-8");
|
|
543
628
|
}
|
|
544
629
|
const trustTargets = getCodexWritableRoots(repoRoot, commsDir);
|
|
545
630
|
if (mode === "full") {
|
|
@@ -601,8 +686,8 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
|
|
|
601
686
|
);
|
|
602
687
|
}
|
|
603
688
|
const tmp = `${configPath}.tmp.${process.pid}`;
|
|
604
|
-
|
|
605
|
-
|
|
689
|
+
fs5.writeFileSync(tmp, content, "utf-8");
|
|
690
|
+
fs5.renameSync(tmp, configPath);
|
|
606
691
|
const modeLabel = mode === "full" ? "danger-full-access" : "workspace-write, network=full";
|
|
607
692
|
logSuccess(
|
|
608
693
|
`Codex: sandbox=${modeLabel}, ${trustTargets.length} path(s) trusted`
|
|
@@ -611,12 +696,12 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
|
|
|
611
696
|
}
|
|
612
697
|
function getCodexWritableRoots(repoRoot, commsDir) {
|
|
613
698
|
const roots = [repoRoot, commsDir];
|
|
614
|
-
const parent =
|
|
699
|
+
const parent = path5.dirname(repoRoot);
|
|
615
700
|
for (let i = 1; i <= 4; i++) {
|
|
616
|
-
const wtPath =
|
|
617
|
-
if (
|
|
701
|
+
const wtPath = path5.join(parent, `hua-wt-${i}`);
|
|
702
|
+
if (fs5.existsSync(wtPath)) roots.push(wtPath);
|
|
618
703
|
}
|
|
619
|
-
return [...new Set(roots.map((r) =>
|
|
704
|
+
return [...new Set(roots.map((r) => path5.resolve(r)))];
|
|
620
705
|
}
|
|
621
706
|
function buildPermissionSummary(mode, repoRoot, commsDir) {
|
|
622
707
|
const trustedPaths = getCodexWritableRoots(repoRoot, commsDir);
|
|
@@ -654,7 +739,7 @@ function parsePermissionMode(args) {
|
|
|
654
739
|
return "safe";
|
|
655
740
|
}
|
|
656
741
|
async function initCommand(args) {
|
|
657
|
-
const repoRoot =
|
|
742
|
+
const repoRoot = findRepoRoot();
|
|
658
743
|
const commsDir = resolveCommsDir(args, repoRoot);
|
|
659
744
|
const permMode = parsePermissionMode(args);
|
|
660
745
|
if (stateExists(repoRoot) && !args.includes("--force")) {
|
|
@@ -670,13 +755,13 @@ async function initCommand(args) {
|
|
|
670
755
|
logHeader("@hua-labs/tap init");
|
|
671
756
|
log(`Comms directory: ${commsDir}`);
|
|
672
757
|
for (const dir of COMMS_DIRS) {
|
|
673
|
-
const dirPath =
|
|
674
|
-
|
|
758
|
+
const dirPath = path6.join(commsDir, dir);
|
|
759
|
+
fs6.mkdirSync(dirPath, { recursive: true });
|
|
675
760
|
logSuccess(`Created ${dir}/`);
|
|
676
761
|
}
|
|
677
|
-
const gitignorePath =
|
|
678
|
-
if (!
|
|
679
|
-
|
|
762
|
+
const gitignorePath = path6.join(commsDir, ".gitignore");
|
|
763
|
+
if (!fs6.existsSync(gitignorePath)) {
|
|
764
|
+
fs6.writeFileSync(
|
|
680
765
|
gitignorePath,
|
|
681
766
|
["tap.db", ".lock", "*.tmp.*", ".DS_Store"].join("\n") + "\n",
|
|
682
767
|
"utf-8"
|
|
@@ -685,12 +770,12 @@ async function initCommand(args) {
|
|
|
685
770
|
}
|
|
686
771
|
const { config } = resolveConfig({}, repoRoot);
|
|
687
772
|
const stateDir = config.stateDir;
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const stateDirRel =
|
|
773
|
+
fs6.mkdirSync(path6.join(stateDir, "pids"), { recursive: true });
|
|
774
|
+
fs6.mkdirSync(path6.join(stateDir, "logs"), { recursive: true });
|
|
775
|
+
fs6.mkdirSync(path6.join(stateDir, "backups"), { recursive: true });
|
|
776
|
+
const stateDirRel = path6.relative(repoRoot, stateDir);
|
|
692
777
|
logSuccess(`Created ${stateDirRel}/ state directory`);
|
|
693
|
-
const repoGitignore =
|
|
778
|
+
const repoGitignore = path6.join(repoRoot, ".gitignore");
|
|
694
779
|
const gitignoreEntries = [
|
|
695
780
|
{ entry: stateDirRel.replace(/\\/g, "/") + "/", label: "tap-comms state" },
|
|
696
781
|
{
|
|
@@ -698,11 +783,11 @@ async function initCommand(args) {
|
|
|
698
783
|
label: "tap-comms local config (machine-specific)"
|
|
699
784
|
}
|
|
700
785
|
];
|
|
701
|
-
if (
|
|
702
|
-
const content =
|
|
786
|
+
if (fs6.existsSync(repoGitignore)) {
|
|
787
|
+
const content = fs6.readFileSync(repoGitignore, "utf-8");
|
|
703
788
|
for (const { entry, label } of gitignoreEntries) {
|
|
704
789
|
if (!content.includes(entry)) {
|
|
705
|
-
|
|
790
|
+
fs6.appendFileSync(repoGitignore, `
|
|
706
791
|
# ${label}
|
|
707
792
|
${entry}
|
|
708
793
|
`);
|
|
@@ -742,64 +827,178 @@ ${entry}
|
|
|
742
827
|
}
|
|
743
828
|
|
|
744
829
|
// src/adapters/claude.ts
|
|
745
|
-
import * as
|
|
746
|
-
import * as
|
|
830
|
+
import * as fs8 from "fs";
|
|
831
|
+
import * as path8 from "path";
|
|
747
832
|
import { execSync } from "child_process";
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
833
|
+
|
|
834
|
+
// src/adapters/common.ts
|
|
835
|
+
import * as fs7 from "fs";
|
|
836
|
+
import * as os2 from "os";
|
|
837
|
+
import * as path7 from "path";
|
|
838
|
+
import { spawnSync } from "child_process";
|
|
839
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
840
|
+
function probeCommand(candidates) {
|
|
841
|
+
for (const candidate of candidates) {
|
|
842
|
+
const result = spawnSync(candidate, ["--version"], {
|
|
843
|
+
encoding: "utf-8",
|
|
844
|
+
shell: process.platform === "win32"
|
|
845
|
+
});
|
|
846
|
+
if (result.status === 0) {
|
|
847
|
+
const version2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || null;
|
|
848
|
+
return { command: candidate, version: version2 };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return { command: null, version: null };
|
|
751
852
|
}
|
|
752
|
-
function
|
|
853
|
+
function getHomeDir() {
|
|
854
|
+
return os2.homedir();
|
|
855
|
+
}
|
|
856
|
+
function toForwardSlashPath(filePath) {
|
|
857
|
+
return path7.resolve(filePath).replace(/\\/g, "/");
|
|
858
|
+
}
|
|
859
|
+
function canWriteOrCreate(filePath) {
|
|
753
860
|
try {
|
|
754
|
-
|
|
755
|
-
|
|
861
|
+
if (fs7.existsSync(filePath)) {
|
|
862
|
+
fs7.accessSync(filePath, fs7.constants.W_OK);
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
const parent = path7.dirname(filePath);
|
|
866
|
+
fs7.mkdirSync(parent, { recursive: true });
|
|
867
|
+
fs7.accessSync(parent, fs7.constants.W_OK);
|
|
868
|
+
return true;
|
|
756
869
|
} catch {
|
|
757
|
-
return
|
|
870
|
+
return false;
|
|
758
871
|
}
|
|
759
872
|
}
|
|
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) {
|
|
873
|
+
function findLocalTapCommsSource(ctx) {
|
|
771
874
|
const candidates = [
|
|
772
|
-
|
|
875
|
+
path7.join(
|
|
773
876
|
ctx.repoRoot,
|
|
774
877
|
"packages",
|
|
775
878
|
"tap-plugin",
|
|
776
879
|
"channels",
|
|
777
880
|
"tap-comms.ts"
|
|
778
881
|
),
|
|
779
|
-
|
|
882
|
+
path7.join(
|
|
780
883
|
ctx.repoRoot,
|
|
781
884
|
"node_modules",
|
|
782
885
|
"@hua-labs",
|
|
886
|
+
"tap-plugin",
|
|
783
887
|
"channels",
|
|
784
888
|
"tap-comms.ts"
|
|
785
889
|
)
|
|
786
890
|
];
|
|
787
|
-
for (const
|
|
788
|
-
if (
|
|
891
|
+
for (const candidate of candidates) {
|
|
892
|
+
if (fs7.existsSync(candidate)) return candidate;
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
function findBundledTapCommsSource(metaUrl = import.meta.url) {
|
|
897
|
+
const moduleDir = path7.dirname(fileURLToPath2(metaUrl));
|
|
898
|
+
const candidates = [
|
|
899
|
+
path7.join(moduleDir, "mcp-server.mjs"),
|
|
900
|
+
path7.join(moduleDir, "..", "mcp-server.mjs"),
|
|
901
|
+
path7.join(moduleDir, "..", "mcp-server.ts")
|
|
902
|
+
];
|
|
903
|
+
for (const candidate of candidates) {
|
|
904
|
+
if (fs7.existsSync(candidate)) return candidate;
|
|
905
|
+
}
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
function findTapCommsServerEntry(ctx, metaUrl = import.meta.url) {
|
|
909
|
+
return findBundledTapCommsSource(metaUrl) ?? findLocalTapCommsSource(ctx);
|
|
910
|
+
}
|
|
911
|
+
function findPreferredBunCommand() {
|
|
912
|
+
const home = getHomeDir();
|
|
913
|
+
const candidates = process.platform === "win32" ? [path7.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path7.join(home, ".bun", "bin", "bun"), "bun"];
|
|
914
|
+
for (const candidate of candidates) {
|
|
915
|
+
if (path7.isAbsolute(candidate) && !fs7.existsSync(candidate)) continue;
|
|
916
|
+
const result = spawnSync(candidate, ["--version"], {
|
|
917
|
+
encoding: "utf-8",
|
|
918
|
+
shell: process.platform === "win32"
|
|
919
|
+
});
|
|
920
|
+
if (result.status === 0) {
|
|
921
|
+
return path7.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
|
|
922
|
+
}
|
|
789
923
|
}
|
|
790
924
|
return null;
|
|
791
925
|
}
|
|
926
|
+
function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
927
|
+
const sourcePath = findTapCommsServerEntry(ctx);
|
|
928
|
+
const bunCommand = findPreferredBunCommand();
|
|
929
|
+
const warnings = [];
|
|
930
|
+
const issues = [];
|
|
931
|
+
const env = {
|
|
932
|
+
TAP_AGENT_NAME: "<set-per-session>",
|
|
933
|
+
TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
|
|
934
|
+
};
|
|
935
|
+
if (instanceId) {
|
|
936
|
+
env.TAP_AGENT_ID = instanceId;
|
|
937
|
+
}
|
|
938
|
+
if (!sourcePath) {
|
|
939
|
+
issues.push(
|
|
940
|
+
"tap-comms MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
|
|
941
|
+
);
|
|
942
|
+
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
943
|
+
}
|
|
944
|
+
const isBundled = sourcePath.endsWith(".mjs");
|
|
945
|
+
let command = bunCommand;
|
|
946
|
+
if (!command && isBundled) {
|
|
947
|
+
command = process.execPath;
|
|
948
|
+
warnings.push(
|
|
949
|
+
"bun not found; using node to run the compiled MCP server. Install bun for better performance."
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (!command) {
|
|
953
|
+
issues.push(
|
|
954
|
+
"bun is required to run the repo-local tap-comms MCP server (.ts source). Install bun: https://bun.sh"
|
|
955
|
+
);
|
|
956
|
+
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
command: isBundled && command === process.execPath ? toForwardSlashPath(command) : command,
|
|
960
|
+
args: [toForwardSlashPath(sourcePath)],
|
|
961
|
+
env,
|
|
962
|
+
sourcePath,
|
|
963
|
+
warnings,
|
|
964
|
+
issues
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/adapters/claude.ts
|
|
969
|
+
var MCP_SERVER_KEY = "tap-comms";
|
|
970
|
+
function findMcpJsonPath(ctx) {
|
|
971
|
+
return path8.join(ctx.repoRoot, ".mcp.json");
|
|
972
|
+
}
|
|
973
|
+
function findClaudeCommand() {
|
|
974
|
+
try {
|
|
975
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
976
|
+
return "claude";
|
|
977
|
+
} catch {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function buildMcpServerEntry(ctx) {
|
|
982
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
983
|
+
if (!managed.command) return null;
|
|
984
|
+
return {
|
|
985
|
+
type: "stdio",
|
|
986
|
+
command: managed.command,
|
|
987
|
+
args: managed.args,
|
|
988
|
+
env: managed.env
|
|
989
|
+
};
|
|
990
|
+
}
|
|
792
991
|
var claudeAdapter = {
|
|
793
992
|
runtime: "claude",
|
|
794
993
|
async probe(ctx) {
|
|
795
994
|
const warnings = [];
|
|
796
995
|
const issues = [];
|
|
797
996
|
const configPath = findMcpJsonPath(ctx);
|
|
798
|
-
const configExists =
|
|
997
|
+
const configExists = fs8.existsSync(configPath);
|
|
799
998
|
const runtimeCommand = findClaudeCommand();
|
|
800
999
|
const canWrite = configExists ? (() => {
|
|
801
1000
|
try {
|
|
802
|
-
|
|
1001
|
+
fs8.accessSync(configPath, fs8.constants.W_OK);
|
|
803
1002
|
return true;
|
|
804
1003
|
} catch {
|
|
805
1004
|
return false;
|
|
@@ -810,13 +1009,10 @@ var claudeAdapter = {
|
|
|
810
1009
|
"Claude CLI not found in PATH. Config will be created but may need manual setup."
|
|
811
1010
|
);
|
|
812
1011
|
}
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
if (!fs6.existsSync(ctx.commsDir)) {
|
|
1012
|
+
const managed = buildManagedMcpServerSpec(ctx);
|
|
1013
|
+
warnings.push(...managed.warnings);
|
|
1014
|
+
issues.push(...managed.issues);
|
|
1015
|
+
if (!fs8.existsSync(ctx.commsDir)) {
|
|
820
1016
|
issues.push(
|
|
821
1017
|
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
822
1018
|
);
|
|
@@ -840,7 +1036,7 @@ var claudeAdapter = {
|
|
|
840
1036
|
const operations = [];
|
|
841
1037
|
const ownedArtifacts = [];
|
|
842
1038
|
if (probe.configExists) {
|
|
843
|
-
const raw =
|
|
1039
|
+
const raw = fs8.readFileSync(configPath, "utf-8");
|
|
844
1040
|
try {
|
|
845
1041
|
const config = JSON.parse(raw);
|
|
846
1042
|
if (config.mcpServers?.[MCP_SERVER_KEY]) {
|
|
@@ -857,7 +1053,7 @@ var claudeAdapter = {
|
|
|
857
1053
|
const serverEntry = buildMcpServerEntry(ctx);
|
|
858
1054
|
if (!serverEntry) {
|
|
859
1055
|
warnings.push(
|
|
860
|
-
"tap-comms MCP server not found
|
|
1056
|
+
"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
1057
|
);
|
|
862
1058
|
return {
|
|
863
1059
|
runtime: "claude",
|
|
@@ -899,9 +1095,9 @@ var claudeAdapter = {
|
|
|
899
1095
|
try {
|
|
900
1096
|
if (op.type === "set" || op.type === "merge") {
|
|
901
1097
|
let config = {};
|
|
902
|
-
if (
|
|
1098
|
+
if (fs8.existsSync(op.path)) {
|
|
903
1099
|
backupFile(op.path, plan.backupDir);
|
|
904
|
-
const raw =
|
|
1100
|
+
const raw = fs8.readFileSync(op.path, "utf-8");
|
|
905
1101
|
try {
|
|
906
1102
|
config = JSON.parse(raw);
|
|
907
1103
|
} catch {
|
|
@@ -914,12 +1110,12 @@ var claudeAdapter = {
|
|
|
914
1110
|
setNestedKey(config, op.key, op.value);
|
|
915
1111
|
}
|
|
916
1112
|
const tmp = `${op.path}.tmp.${process.pid}`;
|
|
917
|
-
|
|
1113
|
+
fs8.writeFileSync(
|
|
918
1114
|
tmp,
|
|
919
1115
|
JSON.stringify(config, null, 2) + "\n",
|
|
920
1116
|
"utf-8"
|
|
921
1117
|
);
|
|
922
|
-
|
|
1118
|
+
fs8.renameSync(tmp, op.path);
|
|
923
1119
|
changedFiles.push(op.path);
|
|
924
1120
|
appliedOps++;
|
|
925
1121
|
}
|
|
@@ -948,12 +1144,12 @@ var claudeAdapter = {
|
|
|
948
1144
|
if (configPath) {
|
|
949
1145
|
checks.push({
|
|
950
1146
|
name: "Config file exists",
|
|
951
|
-
passed:
|
|
952
|
-
message:
|
|
1147
|
+
passed: fs8.existsSync(configPath),
|
|
1148
|
+
message: fs8.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
953
1149
|
});
|
|
954
|
-
if (
|
|
1150
|
+
if (fs8.existsSync(configPath)) {
|
|
955
1151
|
try {
|
|
956
|
-
const raw =
|
|
1152
|
+
const raw = fs8.readFileSync(configPath, "utf-8");
|
|
957
1153
|
const config = JSON.parse(raw);
|
|
958
1154
|
checks.push({ name: "Config is valid JSON", passed: true });
|
|
959
1155
|
const entry = config.mcpServers?.[MCP_SERVER_KEY];
|
|
@@ -963,7 +1159,7 @@ var claudeAdapter = {
|
|
|
963
1159
|
message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
|
|
964
1160
|
});
|
|
965
1161
|
if (entry) {
|
|
966
|
-
const hasCommsDir = entry.env?.TAP_COMMS_DIR === ctx.commsDir;
|
|
1162
|
+
const hasCommsDir = normalizeTapCommsDir(entry.env?.TAP_COMMS_DIR) === normalizeTapCommsDir(ctx.commsDir);
|
|
967
1163
|
checks.push({
|
|
968
1164
|
name: "TAP_COMMS_DIR configured",
|
|
969
1165
|
passed: hasCommsDir,
|
|
@@ -981,8 +1177,8 @@ var claudeAdapter = {
|
|
|
981
1177
|
}
|
|
982
1178
|
checks.push({
|
|
983
1179
|
name: "Comms directory exists",
|
|
984
|
-
passed:
|
|
985
|
-
message:
|
|
1180
|
+
passed: fs8.existsSync(ctx.commsDir),
|
|
1181
|
+
message: fs8.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
986
1182
|
});
|
|
987
1183
|
const cmd = findClaudeCommand();
|
|
988
1184
|
checks.push({
|
|
@@ -1014,174 +1210,62 @@ function setNestedKey(obj, keyPath, value) {
|
|
|
1014
1210
|
}
|
|
1015
1211
|
current[keys[keys.length - 1]] = value;
|
|
1016
1212
|
}
|
|
1213
|
+
function normalizeTapCommsDir(value) {
|
|
1214
|
+
return typeof value === "string" ? path8.resolve(value).replace(/\\/g, "/") : "";
|
|
1215
|
+
}
|
|
1017
1216
|
|
|
1018
1217
|
// src/adapters/codex.ts
|
|
1019
|
-
import * as
|
|
1020
|
-
import * as
|
|
1021
|
-
import { fileURLToPath } from "url";
|
|
1218
|
+
import * as fs10 from "fs";
|
|
1219
|
+
import * as path10 from "path";
|
|
1220
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1022
1221
|
|
|
1023
1222
|
// src/artifact-backups.ts
|
|
1024
1223
|
import * as crypto2 from "crypto";
|
|
1025
|
-
import * as
|
|
1026
|
-
import * as
|
|
1224
|
+
import * as fs9 from "fs";
|
|
1225
|
+
import * as path9 from "path";
|
|
1027
1226
|
function selectorHash(selector) {
|
|
1028
1227
|
return crypto2.createHash("sha256").update(selector).digest("hex").slice(0, 12);
|
|
1029
1228
|
}
|
|
1030
1229
|
function artifactBackupPath(backupDir, kind, selector) {
|
|
1031
1230
|
const safeKind = kind.replace(/[^a-z-]/gi, "-");
|
|
1032
|
-
return
|
|
1231
|
+
return path9.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
|
|
1033
1232
|
}
|
|
1034
1233
|
function writeArtifactBackup(backupPath, payload) {
|
|
1035
|
-
|
|
1234
|
+
fs9.mkdirSync(path9.dirname(backupPath), { recursive: true });
|
|
1036
1235
|
const tmp = `${backupPath}.tmp.${process.pid}`;
|
|
1037
|
-
|
|
1038
|
-
|
|
1236
|
+
fs9.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
1237
|
+
fs9.renameSync(tmp, backupPath);
|
|
1039
1238
|
}
|
|
1040
1239
|
function readArtifactBackup(backupPath) {
|
|
1041
|
-
if (!
|
|
1240
|
+
if (!fs9.existsSync(backupPath)) return null;
|
|
1042
1241
|
try {
|
|
1043
|
-
const raw =
|
|
1242
|
+
const raw = fs9.readFileSync(backupPath, "utf-8");
|
|
1044
1243
|
return JSON.parse(raw);
|
|
1045
1244
|
} catch {
|
|
1046
1245
|
return null;
|
|
1047
1246
|
}
|
|
1048
1247
|
}
|
|
1049
1248
|
|
|
1050
|
-
// src/adapters/
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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 };
|
|
1249
|
+
// src/adapters/codex.ts
|
|
1250
|
+
var MCP_SELECTOR = "mcp_servers.tap-comms";
|
|
1251
|
+
var ENV_SELECTOR = "mcp_servers.tap-comms.env";
|
|
1252
|
+
function findCodexConfigPath2() {
|
|
1253
|
+
return path10.join(getHomeDir(), ".codex", "config.toml");
|
|
1067
1254
|
}
|
|
1068
|
-
function
|
|
1069
|
-
|
|
1255
|
+
function canonicalizeTrustPath2(targetPath) {
|
|
1256
|
+
let resolved = path10.resolve(targetPath).replace(/\//g, "\\");
|
|
1257
|
+
const driveRoot = /^[A-Za-z]:\\$/;
|
|
1258
|
+
if (!driveRoot.test(resolved)) {
|
|
1259
|
+
resolved = resolved.replace(/\\+$/g, "");
|
|
1260
|
+
}
|
|
1261
|
+
return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
|
|
1070
1262
|
}
|
|
1071
|
-
function
|
|
1072
|
-
return
|
|
1263
|
+
function trustSelector(targetPath) {
|
|
1264
|
+
return `projects.'${canonicalizeTrustPath2(targetPath)}'`;
|
|
1073
1265
|
}
|
|
1074
|
-
function
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
// src/adapters/codex.ts
|
|
1166
|
-
var MCP_SELECTOR = "mcp_servers.tap-comms";
|
|
1167
|
-
var ENV_SELECTOR = "mcp_servers.tap-comms.env";
|
|
1168
|
-
function findCodexConfigPath2() {
|
|
1169
|
-
return path9.join(getHomeDir(), ".codex", "config.toml");
|
|
1170
|
-
}
|
|
1171
|
-
function canonicalizeTrustPath2(targetPath) {
|
|
1172
|
-
let resolved = path9.resolve(targetPath).replace(/\//g, "\\");
|
|
1173
|
-
const driveRoot = /^[A-Za-z]:\\$/;
|
|
1174
|
-
if (!driveRoot.test(resolved)) {
|
|
1175
|
-
resolved = resolved.replace(/\\+$/g, "");
|
|
1176
|
-
}
|
|
1177
|
-
return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
|
|
1178
|
-
}
|
|
1179
|
-
function trustSelector(targetPath) {
|
|
1180
|
-
return `projects.'${canonicalizeTrustPath2(targetPath)}'`;
|
|
1181
|
-
}
|
|
1182
|
-
function getTrustTargets(ctx) {
|
|
1183
|
-
const targets = [ctx.repoRoot, process.cwd()];
|
|
1184
|
-
return [...new Set(targets.map((value) => path9.resolve(value)))];
|
|
1266
|
+
function getTrustTargets(ctx) {
|
|
1267
|
+
const targets = [ctx.repoRoot, process.cwd()];
|
|
1268
|
+
return [...new Set(targets.map((value) => path10.resolve(value)))];
|
|
1185
1269
|
}
|
|
1186
1270
|
function buildManagedArtifacts(configPath, ctx) {
|
|
1187
1271
|
const artifacts = [
|
|
@@ -1198,14 +1282,14 @@ function buildManagedArtifacts(configPath, ctx) {
|
|
|
1198
1282
|
return artifacts;
|
|
1199
1283
|
}
|
|
1200
1284
|
function readConfigOrEmpty(configPath) {
|
|
1201
|
-
if (!
|
|
1202
|
-
return
|
|
1285
|
+
if (!fs10.existsSync(configPath)) return "";
|
|
1286
|
+
return fs10.readFileSync(configPath, "utf-8");
|
|
1203
1287
|
}
|
|
1204
1288
|
function writeTomlFile(filePath, content) {
|
|
1205
|
-
|
|
1289
|
+
fs10.mkdirSync(path10.dirname(filePath), { recursive: true });
|
|
1206
1290
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
1207
|
-
|
|
1208
|
-
|
|
1291
|
+
fs10.writeFileSync(tmp, content, "utf-8");
|
|
1292
|
+
fs10.renameSync(tmp, filePath);
|
|
1209
1293
|
}
|
|
1210
1294
|
function verifyManagedToml(content, ctx, configPath) {
|
|
1211
1295
|
const checks = [];
|
|
@@ -1214,8 +1298,8 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
1214
1298
|
const envTable = extractTomlTable(content, ENV_SELECTOR);
|
|
1215
1299
|
checks.push({
|
|
1216
1300
|
name: "Codex config exists",
|
|
1217
|
-
passed:
|
|
1218
|
-
message:
|
|
1301
|
+
passed: fs10.existsSync(configPath),
|
|
1302
|
+
message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
1219
1303
|
});
|
|
1220
1304
|
checks.push({
|
|
1221
1305
|
name: "tap-comms MCP table present",
|
|
@@ -1255,7 +1339,7 @@ var codexAdapter = {
|
|
|
1255
1339
|
const warnings = [];
|
|
1256
1340
|
const issues = [];
|
|
1257
1341
|
const configPath = findCodexConfigPath2();
|
|
1258
|
-
const configExists =
|
|
1342
|
+
const configExists = fs10.existsSync(configPath);
|
|
1259
1343
|
const runtimeProbe = probeCommand(
|
|
1260
1344
|
ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
|
|
1261
1345
|
);
|
|
@@ -1264,7 +1348,7 @@ var codexAdapter = {
|
|
|
1264
1348
|
"Codex CLI not found in PATH. Config can still be written, but runtime verification will be limited."
|
|
1265
1349
|
);
|
|
1266
1350
|
}
|
|
1267
|
-
if (!
|
|
1351
|
+
if (!fs10.existsSync(ctx.commsDir)) {
|
|
1268
1352
|
issues.push(
|
|
1269
1353
|
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
1270
1354
|
);
|
|
@@ -1325,7 +1409,7 @@ var codexAdapter = {
|
|
|
1325
1409
|
const configPath = plan.operations[0]?.path ?? findCodexConfigPath2();
|
|
1326
1410
|
const warnings = [];
|
|
1327
1411
|
const changedFiles = [];
|
|
1328
|
-
const managed = buildManagedMcpServerSpec(ctx);
|
|
1412
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
1329
1413
|
warnings.push(...managed.warnings);
|
|
1330
1414
|
if (managed.issues.length > 0 || !managed.command) {
|
|
1331
1415
|
return {
|
|
@@ -1340,7 +1424,7 @@ var codexAdapter = {
|
|
|
1340
1424
|
};
|
|
1341
1425
|
}
|
|
1342
1426
|
const existingContent = readConfigOrEmpty(configPath);
|
|
1343
|
-
if (
|
|
1427
|
+
if (fs10.existsSync(configPath) && existingContent) {
|
|
1344
1428
|
backupFile(configPath, plan.backupDir);
|
|
1345
1429
|
}
|
|
1346
1430
|
const artifactsWithBackups = plan.ownedArtifacts.map((artifact) => {
|
|
@@ -1415,8 +1499,8 @@ var codexAdapter = {
|
|
|
1415
1499
|
const checks = verifyManagedToml(content, ctx, configPath);
|
|
1416
1500
|
checks.push({
|
|
1417
1501
|
name: "Comms directory exists",
|
|
1418
|
-
passed:
|
|
1419
|
-
message:
|
|
1502
|
+
passed: fs10.existsSync(ctx.commsDir),
|
|
1503
|
+
message: fs10.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
1420
1504
|
});
|
|
1421
1505
|
checks.push({
|
|
1422
1506
|
name: "Codex CLI found",
|
|
@@ -1439,12 +1523,12 @@ var codexAdapter = {
|
|
|
1439
1523
|
return "app-server";
|
|
1440
1524
|
},
|
|
1441
1525
|
resolveBridgeScript(ctx) {
|
|
1442
|
-
const distDir =
|
|
1526
|
+
const distDir = path10.dirname(fileURLToPath3(import.meta.url));
|
|
1443
1527
|
const candidates = [
|
|
1444
1528
|
// 1. Relative to bundled CLI (npm install / npx)
|
|
1445
|
-
|
|
1529
|
+
path10.join(distDir, "bridges", "codex-bridge-runner.mjs"),
|
|
1446
1530
|
// 2. Monorepo development — dist inside repo
|
|
1447
|
-
|
|
1531
|
+
path10.join(
|
|
1448
1532
|
ctx.repoRoot,
|
|
1449
1533
|
"packages",
|
|
1450
1534
|
"tap-comms",
|
|
@@ -1453,7 +1537,7 @@ var codexAdapter = {
|
|
|
1453
1537
|
"codex-bridge-runner.mjs"
|
|
1454
1538
|
),
|
|
1455
1539
|
// 3. Source file — dev mode with strip-types
|
|
1456
|
-
|
|
1540
|
+
path10.join(
|
|
1457
1541
|
ctx.repoRoot,
|
|
1458
1542
|
"packages",
|
|
1459
1543
|
"tap-comms",
|
|
@@ -1463,30 +1547,30 @@ var codexAdapter = {
|
|
|
1463
1547
|
)
|
|
1464
1548
|
];
|
|
1465
1549
|
for (const candidate of candidates) {
|
|
1466
|
-
if (
|
|
1550
|
+
if (fs10.existsSync(candidate)) return candidate;
|
|
1467
1551
|
}
|
|
1468
1552
|
return null;
|
|
1469
1553
|
}
|
|
1470
1554
|
};
|
|
1471
1555
|
|
|
1472
1556
|
// src/adapters/gemini.ts
|
|
1473
|
-
import * as
|
|
1474
|
-
import * as
|
|
1557
|
+
import * as fs11 from "fs";
|
|
1558
|
+
import * as path11 from "path";
|
|
1475
1559
|
var GEMINI_SELECTOR = "mcpServers.tap-comms";
|
|
1476
1560
|
function candidateConfigPaths(ctx) {
|
|
1477
1561
|
const home = getHomeDir();
|
|
1478
1562
|
return [
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1563
|
+
path11.join(ctx.repoRoot, ".gemini", "settings.json"),
|
|
1564
|
+
path11.join(home, ".gemini", "settings.json"),
|
|
1565
|
+
path11.join(home, ".gemini", "antigravity", "mcp_config.json")
|
|
1482
1566
|
];
|
|
1483
1567
|
}
|
|
1484
1568
|
function chooseGeminiConfigPath(ctx) {
|
|
1485
1569
|
const [workspaceConfig, homeConfig, antigravityConfig] = candidateConfigPaths(ctx);
|
|
1486
|
-
if (
|
|
1487
|
-
if (
|
|
1488
|
-
if (
|
|
1489
|
-
const raw =
|
|
1570
|
+
if (fs11.existsSync(workspaceConfig)) return workspaceConfig;
|
|
1571
|
+
if (fs11.existsSync(homeConfig)) return homeConfig;
|
|
1572
|
+
if (fs11.existsSync(antigravityConfig)) {
|
|
1573
|
+
const raw = fs11.readFileSync(antigravityConfig, "utf-8").trim();
|
|
1490
1574
|
if (raw) {
|
|
1491
1575
|
try {
|
|
1492
1576
|
JSON.parse(raw);
|
|
@@ -1498,8 +1582,8 @@ function chooseGeminiConfigPath(ctx) {
|
|
|
1498
1582
|
return workspaceConfig;
|
|
1499
1583
|
}
|
|
1500
1584
|
function readJsonFile(filePath) {
|
|
1501
|
-
if (!
|
|
1502
|
-
const raw =
|
|
1585
|
+
if (!fs11.existsSync(filePath)) return {};
|
|
1586
|
+
const raw = fs11.readFileSync(filePath, "utf-8").trim();
|
|
1503
1587
|
if (!raw) return {};
|
|
1504
1588
|
return JSON.parse(raw);
|
|
1505
1589
|
}
|
|
@@ -1530,8 +1614,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
|
|
|
1530
1614
|
const entry = readNestedKey(config, GEMINI_SELECTOR);
|
|
1531
1615
|
checks.push({
|
|
1532
1616
|
name: "Gemini config exists",
|
|
1533
|
-
passed:
|
|
1534
|
-
message:
|
|
1617
|
+
passed: fs11.existsSync(configPath),
|
|
1618
|
+
message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
1535
1619
|
});
|
|
1536
1620
|
checks.push({
|
|
1537
1621
|
name: "tap-comms entry present",
|
|
@@ -1540,8 +1624,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
|
|
|
1540
1624
|
});
|
|
1541
1625
|
checks.push({
|
|
1542
1626
|
name: "Comms directory exists",
|
|
1543
|
-
passed:
|
|
1544
|
-
message:
|
|
1627
|
+
passed: fs11.existsSync(ctx.commsDir),
|
|
1628
|
+
message: fs11.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
1545
1629
|
});
|
|
1546
1630
|
if (entry?.env && typeof entry.env === "object") {
|
|
1547
1631
|
checks.push({
|
|
@@ -1558,7 +1642,7 @@ var geminiAdapter = {
|
|
|
1558
1642
|
const warnings = [];
|
|
1559
1643
|
const issues = [];
|
|
1560
1644
|
const configPath = chooseGeminiConfigPath(ctx);
|
|
1561
|
-
const configExists =
|
|
1645
|
+
const configExists = fs11.existsSync(configPath);
|
|
1562
1646
|
const runtimeProbe = probeCommand(
|
|
1563
1647
|
ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
|
|
1564
1648
|
);
|
|
@@ -1567,10 +1651,12 @@ var geminiAdapter = {
|
|
|
1567
1651
|
"Gemini CLI not found in PATH. Config can still be written, but runtime verification will be limited."
|
|
1568
1652
|
);
|
|
1569
1653
|
}
|
|
1570
|
-
if (!
|
|
1571
|
-
issues.push(
|
|
1654
|
+
if (!fs11.existsSync(ctx.commsDir)) {
|
|
1655
|
+
issues.push(
|
|
1656
|
+
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
1657
|
+
);
|
|
1572
1658
|
}
|
|
1573
|
-
const managed = buildManagedMcpServerSpec(ctx);
|
|
1659
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
1574
1660
|
warnings.push(...managed.warnings);
|
|
1575
1661
|
issues.push(...managed.issues);
|
|
1576
1662
|
return {
|
|
@@ -1599,7 +1685,9 @@ var geminiAdapter = {
|
|
|
1599
1685
|
conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
|
|
1600
1686
|
}
|
|
1601
1687
|
} catch {
|
|
1602
|
-
warnings.push(
|
|
1688
|
+
warnings.push(
|
|
1689
|
+
`${configPath} exists but is not valid JSON. It will be replaced.`
|
|
1690
|
+
);
|
|
1603
1691
|
}
|
|
1604
1692
|
}
|
|
1605
1693
|
operations.push({
|
|
@@ -1621,7 +1709,7 @@ var geminiAdapter = {
|
|
|
1621
1709
|
const configPath = plan.operations[0]?.path ?? chooseGeminiConfigPath(ctx);
|
|
1622
1710
|
const warnings = [];
|
|
1623
1711
|
const changedFiles = [];
|
|
1624
|
-
const managed = buildManagedMcpServerSpec(ctx);
|
|
1712
|
+
const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
|
|
1625
1713
|
warnings.push(...managed.warnings);
|
|
1626
1714
|
if (managed.issues.length > 0 || !managed.command) {
|
|
1627
1715
|
return {
|
|
@@ -1637,20 +1725,26 @@ var geminiAdapter = {
|
|
|
1637
1725
|
}
|
|
1638
1726
|
let config = {};
|
|
1639
1727
|
let previousValue = void 0;
|
|
1640
|
-
if (
|
|
1641
|
-
if (
|
|
1728
|
+
if (fs11.existsSync(configPath)) {
|
|
1729
|
+
if (fs11.readFileSync(configPath, "utf-8").trim()) {
|
|
1642
1730
|
backupFile(configPath, plan.backupDir);
|
|
1643
1731
|
}
|
|
1644
1732
|
try {
|
|
1645
1733
|
config = readJsonFile(configPath);
|
|
1646
1734
|
} catch {
|
|
1647
|
-
warnings.push(
|
|
1735
|
+
warnings.push(
|
|
1736
|
+
`${configPath} was invalid JSON. Created backup and starting fresh.`
|
|
1737
|
+
);
|
|
1648
1738
|
config = {};
|
|
1649
1739
|
}
|
|
1650
1740
|
previousValue = readNestedKey(config, GEMINI_SELECTOR);
|
|
1651
1741
|
}
|
|
1652
1742
|
const artifact = plan.ownedArtifacts[0];
|
|
1653
|
-
const backupPath = artifactBackupPath(
|
|
1743
|
+
const backupPath = artifactBackupPath(
|
|
1744
|
+
plan.backupDir,
|
|
1745
|
+
artifact.kind,
|
|
1746
|
+
artifact.selector
|
|
1747
|
+
);
|
|
1654
1748
|
writeArtifactBackup(backupPath, {
|
|
1655
1749
|
kind: "json-path",
|
|
1656
1750
|
selector: artifact.selector,
|
|
@@ -1662,10 +1756,10 @@ var geminiAdapter = {
|
|
|
1662
1756
|
args: managed.args,
|
|
1663
1757
|
env: managed.env
|
|
1664
1758
|
});
|
|
1665
|
-
|
|
1759
|
+
fs11.mkdirSync(path11.dirname(configPath), { recursive: true });
|
|
1666
1760
|
const tmp = `${configPath}.tmp.${process.pid}`;
|
|
1667
|
-
|
|
1668
|
-
|
|
1761
|
+
fs11.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1762
|
+
fs11.renameSync(tmp, configPath);
|
|
1669
1763
|
changedFiles.push(configPath);
|
|
1670
1764
|
return {
|
|
1671
1765
|
success: true,
|
|
@@ -1736,19 +1830,22 @@ function getAdapter(runtime) {
|
|
|
1736
1830
|
}
|
|
1737
1831
|
|
|
1738
1832
|
// src/engine/bridge.ts
|
|
1739
|
-
import * as
|
|
1740
|
-
import * as
|
|
1741
|
-
import
|
|
1833
|
+
import * as fs13 from "fs";
|
|
1834
|
+
import * as net from "net";
|
|
1835
|
+
import * as path13 from "path";
|
|
1836
|
+
import { randomBytes } from "crypto";
|
|
1837
|
+
import { spawn, spawnSync as spawnSync2, execSync as execSync3 } from "child_process";
|
|
1838
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1742
1839
|
|
|
1743
1840
|
// src/runtime/resolve-node.ts
|
|
1744
|
-
import * as
|
|
1745
|
-
import * as
|
|
1841
|
+
import * as fs12 from "fs";
|
|
1842
|
+
import * as path12 from "path";
|
|
1746
1843
|
import { execSync as execSync2 } from "child_process";
|
|
1747
1844
|
function readNodeVersion(repoRoot) {
|
|
1748
|
-
const nvFile =
|
|
1749
|
-
if (!
|
|
1845
|
+
const nvFile = path12.join(repoRoot, ".node-version");
|
|
1846
|
+
if (!fs12.existsSync(nvFile)) return null;
|
|
1750
1847
|
try {
|
|
1751
|
-
const raw =
|
|
1848
|
+
const raw = fs12.readFileSync(nvFile, "utf-8").trim();
|
|
1752
1849
|
return raw.length > 0 ? raw.replace(/^v/, "") : null;
|
|
1753
1850
|
} catch {
|
|
1754
1851
|
return null;
|
|
@@ -1758,16 +1855,16 @@ function fnmCandidateDirs() {
|
|
|
1758
1855
|
if (process.platform === "win32") {
|
|
1759
1856
|
return [
|
|
1760
1857
|
process.env.FNM_DIR,
|
|
1761
|
-
process.env.APPDATA ?
|
|
1762
|
-
process.env.LOCALAPPDATA ?
|
|
1763
|
-
process.env.USERPROFILE ?
|
|
1858
|
+
process.env.APPDATA ? path12.join(process.env.APPDATA, "fnm") : null,
|
|
1859
|
+
process.env.LOCALAPPDATA ? path12.join(process.env.LOCALAPPDATA, "fnm") : null,
|
|
1860
|
+
process.env.USERPROFILE ? path12.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
|
|
1764
1861
|
].filter(Boolean);
|
|
1765
1862
|
}
|
|
1766
1863
|
return [
|
|
1767
1864
|
process.env.FNM_DIR,
|
|
1768
|
-
process.env.HOME ?
|
|
1769
|
-
process.env.HOME ?
|
|
1770
|
-
process.env.XDG_DATA_HOME ?
|
|
1865
|
+
process.env.HOME ? path12.join(process.env.HOME, ".local", "share", "fnm") : null,
|
|
1866
|
+
process.env.HOME ? path12.join(process.env.HOME, ".fnm") : null,
|
|
1867
|
+
process.env.XDG_DATA_HOME ? path12.join(process.env.XDG_DATA_HOME, "fnm") : null
|
|
1771
1868
|
].filter(Boolean);
|
|
1772
1869
|
}
|
|
1773
1870
|
function nodeExecutableName() {
|
|
@@ -1777,14 +1874,14 @@ function probeFnmNode(desiredVersion) {
|
|
|
1777
1874
|
const dirs = fnmCandidateDirs();
|
|
1778
1875
|
const exe = nodeExecutableName();
|
|
1779
1876
|
for (const baseDir of dirs) {
|
|
1780
|
-
const candidate =
|
|
1877
|
+
const candidate = path12.join(
|
|
1781
1878
|
baseDir,
|
|
1782
1879
|
"node-versions",
|
|
1783
1880
|
`v${desiredVersion}`,
|
|
1784
1881
|
"installation",
|
|
1785
1882
|
exe
|
|
1786
1883
|
);
|
|
1787
|
-
if (!
|
|
1884
|
+
if (!fs12.existsSync(candidate)) continue;
|
|
1788
1885
|
try {
|
|
1789
1886
|
const v = execSync2(`"${candidate}" --version`, {
|
|
1790
1887
|
encoding: "utf-8",
|
|
@@ -1810,107 +1907,881 @@ function detectNodeMajorVersion(command) {
|
|
|
1810
1907
|
return null;
|
|
1811
1908
|
}
|
|
1812
1909
|
}
|
|
1813
|
-
function checkStripTypesSupport(command) {
|
|
1814
|
-
const major = detectNodeMajorVersion(command);
|
|
1815
|
-
if (major !== null && major >= 22) return true;
|
|
1910
|
+
function checkStripTypesSupport(command) {
|
|
1911
|
+
const major = detectNodeMajorVersion(command);
|
|
1912
|
+
if (major !== null && major >= 22) return true;
|
|
1913
|
+
try {
|
|
1914
|
+
execSync2(`"${command}" --experimental-strip-types -e ""`, {
|
|
1915
|
+
timeout: 5e3,
|
|
1916
|
+
stdio: "pipe"
|
|
1917
|
+
});
|
|
1918
|
+
return true;
|
|
1919
|
+
} catch {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
function findTsxFallback(repoRoot) {
|
|
1924
|
+
const candidates = [
|
|
1925
|
+
path12.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
|
|
1926
|
+
path12.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
|
|
1927
|
+
path12.join(repoRoot, "node_modules", ".bin", "tsx")
|
|
1928
|
+
];
|
|
1929
|
+
for (const c of candidates) {
|
|
1930
|
+
if (fs12.existsSync(c)) return c;
|
|
1931
|
+
}
|
|
1932
|
+
return null;
|
|
1933
|
+
}
|
|
1934
|
+
function getFnmBinDir(repoRoot) {
|
|
1935
|
+
const desiredVersion = readNodeVersion(repoRoot);
|
|
1936
|
+
if (!desiredVersion) return null;
|
|
1937
|
+
const nodePath = probeFnmNode(desiredVersion);
|
|
1938
|
+
if (!nodePath) return null;
|
|
1939
|
+
return path12.dirname(nodePath);
|
|
1940
|
+
}
|
|
1941
|
+
function resolveNodeRuntime(configCommand, repoRoot) {
|
|
1942
|
+
if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
|
|
1943
|
+
return {
|
|
1944
|
+
command: configCommand,
|
|
1945
|
+
supportsStripTypes: false,
|
|
1946
|
+
source: "bun",
|
|
1947
|
+
majorVersion: null
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
const desiredVersion = readNodeVersion(repoRoot);
|
|
1951
|
+
if (desiredVersion) {
|
|
1952
|
+
const fnmNode = probeFnmNode(desiredVersion);
|
|
1953
|
+
if (fnmNode) {
|
|
1954
|
+
const major2 = detectNodeMajorVersion(fnmNode);
|
|
1955
|
+
return {
|
|
1956
|
+
command: fnmNode,
|
|
1957
|
+
supportsStripTypes: checkStripTypesSupport(fnmNode),
|
|
1958
|
+
source: "fnm",
|
|
1959
|
+
majorVersion: major2
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
const major = detectNodeMajorVersion(configCommand);
|
|
1964
|
+
if (major !== null) {
|
|
1965
|
+
return {
|
|
1966
|
+
command: configCommand,
|
|
1967
|
+
supportsStripTypes: checkStripTypesSupport(configCommand),
|
|
1968
|
+
source: major === detectNodeMajorVersion("node") ? "path" : "config",
|
|
1969
|
+
majorVersion: major
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
const tsx = findTsxFallback(repoRoot);
|
|
1973
|
+
if (tsx) {
|
|
1974
|
+
return {
|
|
1975
|
+
command: tsx,
|
|
1976
|
+
supportsStripTypes: false,
|
|
1977
|
+
source: "tsx-fallback",
|
|
1978
|
+
majorVersion: null
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
return {
|
|
1982
|
+
command: configCommand,
|
|
1983
|
+
supportsStripTypes: false,
|
|
1984
|
+
source: "path",
|
|
1985
|
+
majorVersion: null
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
|
|
1989
|
+
const fnmBin = getFnmBinDir(repoRoot);
|
|
1990
|
+
if (!fnmBin) return { ...baseEnv };
|
|
1991
|
+
const pathKey = process.platform === "win32" ? "Path" : "PATH";
|
|
1992
|
+
const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
|
|
1993
|
+
return {
|
|
1994
|
+
...baseEnv,
|
|
1995
|
+
[pathKey]: `${fnmBin}${path12.delimiter}${currentPath}`
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/engine/bridge.ts
|
|
2000
|
+
var DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
|
|
2001
|
+
var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
2002
|
+
var APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
2003
|
+
var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
|
|
2004
|
+
var APP_SERVER_HEALTH_RETRY_MS = 250;
|
|
2005
|
+
var APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
|
|
2006
|
+
var APP_SERVER_AUTH_FILE_MODE = 384;
|
|
2007
|
+
function appServerLogFilePath(stateDir, instanceId) {
|
|
2008
|
+
return path13.join(stateDir, "logs", `app-server-${instanceId}.log`);
|
|
2009
|
+
}
|
|
2010
|
+
function appServerGatewayLogFilePath(stateDir, instanceId) {
|
|
2011
|
+
return path13.join(stateDir, "logs", `app-server-gateway-${instanceId}.log`);
|
|
2012
|
+
}
|
|
2013
|
+
function appServerGatewayTokenFilePath(stateDir, instanceId) {
|
|
2014
|
+
return path13.join(
|
|
2015
|
+
stateDir,
|
|
2016
|
+
"secrets",
|
|
2017
|
+
`app-server-gateway-${instanceId}.token`
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
function stderrLogFilePath(logPath) {
|
|
2021
|
+
return `${logPath}.stderr`;
|
|
2022
|
+
}
|
|
2023
|
+
function writeProtectedTextFile(filePath, content) {
|
|
2024
|
+
fs13.mkdirSync(path13.dirname(filePath), { recursive: true });
|
|
2025
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
2026
|
+
fs13.writeFileSync(tmp, content, {
|
|
2027
|
+
encoding: "utf-8",
|
|
2028
|
+
mode: APP_SERVER_AUTH_FILE_MODE
|
|
2029
|
+
});
|
|
2030
|
+
fs13.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
|
|
2031
|
+
fs13.renameSync(tmp, filePath);
|
|
2032
|
+
fs13.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
|
|
2033
|
+
}
|
|
2034
|
+
function removeFileIfExists(filePath) {
|
|
2035
|
+
if (!filePath || !fs13.existsSync(filePath)) {
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
try {
|
|
2039
|
+
fs13.unlinkSync(filePath);
|
|
2040
|
+
} catch {
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
function getWebSocketCtor() {
|
|
2044
|
+
const candidate = globalThis.WebSocket;
|
|
2045
|
+
return typeof candidate === "function" ? candidate : null;
|
|
2046
|
+
}
|
|
2047
|
+
function delay(ms) {
|
|
2048
|
+
return new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
2049
|
+
}
|
|
2050
|
+
function isLoopbackHost(hostname) {
|
|
2051
|
+
return hostname === "127.0.0.1" || hostname === "localhost";
|
|
2052
|
+
}
|
|
2053
|
+
function resolveCodexCommand(platform) {
|
|
2054
|
+
const candidates = platform === "win32" ? ["codex.cmd", "codex.exe", "codex", "codex.ps1"] : ["codex"];
|
|
2055
|
+
return probeCommand(candidates).command;
|
|
2056
|
+
}
|
|
2057
|
+
function formatCodexAppServerCommand(command, url) {
|
|
2058
|
+
return `${command} app-server --listen ${url}`;
|
|
2059
|
+
}
|
|
2060
|
+
function resolvePowerShellCommand() {
|
|
2061
|
+
return probeCommand(["pwsh", "powershell", "powershell.exe"]).command ?? "powershell";
|
|
2062
|
+
}
|
|
2063
|
+
function resolveAuthGatewayScript(repoRoot) {
|
|
2064
|
+
const moduleDir = path13.dirname(fileURLToPath4(import.meta.url));
|
|
2065
|
+
const candidates = [
|
|
2066
|
+
path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
|
|
2067
|
+
path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.ts"),
|
|
2068
|
+
path13.join(
|
|
2069
|
+
repoRoot,
|
|
2070
|
+
"packages",
|
|
2071
|
+
"tap-comms",
|
|
2072
|
+
"dist",
|
|
2073
|
+
"bridges",
|
|
2074
|
+
"codex-app-server-auth-gateway.mjs"
|
|
2075
|
+
),
|
|
2076
|
+
path13.join(
|
|
2077
|
+
repoRoot,
|
|
2078
|
+
"packages",
|
|
2079
|
+
"tap-comms",
|
|
2080
|
+
"src",
|
|
2081
|
+
"bridges",
|
|
2082
|
+
"codex-app-server-auth-gateway.ts"
|
|
2083
|
+
)
|
|
2084
|
+
];
|
|
2085
|
+
for (const candidate of candidates) {
|
|
2086
|
+
if (fs13.existsSync(candidate)) {
|
|
2087
|
+
return candidate;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
function getBridgeRuntimeStateDir(repoRoot, instanceId) {
|
|
2093
|
+
return path13.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`);
|
|
2094
|
+
}
|
|
2095
|
+
async function allocateLoopbackPort(hostname) {
|
|
2096
|
+
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2097
|
+
return await new Promise((resolve11, reject) => {
|
|
2098
|
+
const server = net.createServer();
|
|
2099
|
+
server.unref();
|
|
2100
|
+
server.once("error", reject);
|
|
2101
|
+
server.listen(0, bindHost, () => {
|
|
2102
|
+
const address = server.address();
|
|
2103
|
+
if (!address || typeof address === "string") {
|
|
2104
|
+
server.close(() => {
|
|
2105
|
+
reject(new Error("Failed to allocate a loopback port"));
|
|
2106
|
+
});
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const port = address.port;
|
|
2110
|
+
server.close((error) => {
|
|
2111
|
+
if (error) {
|
|
2112
|
+
reject(error);
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
resolve11(port);
|
|
2116
|
+
});
|
|
2117
|
+
});
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
function buildProtectedAppServerUrl(publicUrl, token) {
|
|
2121
|
+
const url = new URL(publicUrl);
|
|
2122
|
+
url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
|
|
2123
|
+
return url.toString().replace(/\/(?=\?|$)/, "");
|
|
2124
|
+
}
|
|
2125
|
+
function readGatewayTokenFromPath(tokenPath) {
|
|
2126
|
+
return fs13.readFileSync(tokenPath, "utf8").trim();
|
|
2127
|
+
}
|
|
2128
|
+
function readGatewayToken(auth) {
|
|
2129
|
+
if (!auth) {
|
|
2130
|
+
return null;
|
|
2131
|
+
}
|
|
2132
|
+
const legacyToken = auth.token;
|
|
2133
|
+
if (legacyToken?.trim()) {
|
|
2134
|
+
return legacyToken.trim();
|
|
2135
|
+
}
|
|
2136
|
+
if (!auth.tokenPath || !fs13.existsSync(auth.tokenPath)) {
|
|
2137
|
+
return null;
|
|
2138
|
+
}
|
|
2139
|
+
const fileToken = readGatewayTokenFromPath(auth.tokenPath);
|
|
2140
|
+
return fileToken || null;
|
|
2141
|
+
}
|
|
2142
|
+
function materializeGatewayTokenFile(stateDir, instanceId, publicUrl, auth) {
|
|
2143
|
+
if (auth.tokenPath && fs13.existsSync(auth.tokenPath)) {
|
|
2144
|
+
return auth;
|
|
2145
|
+
}
|
|
2146
|
+
const token = readGatewayToken(auth);
|
|
2147
|
+
if (!token) {
|
|
2148
|
+
throw new Error(`Missing auth gateway token for ${instanceId}`);
|
|
2149
|
+
}
|
|
2150
|
+
const tokenPath = appServerGatewayTokenFilePath(stateDir, instanceId);
|
|
2151
|
+
writeProtectedTextFile(tokenPath, `${token}
|
|
2152
|
+
`);
|
|
2153
|
+
return {
|
|
2154
|
+
...auth,
|
|
2155
|
+
protectedUrl: buildProtectedAppServerUrl(publicUrl, "***"),
|
|
2156
|
+
tokenPath
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
async function createManagedAppServerAuth(options) {
|
|
2160
|
+
const publicUrl = new URL(options.publicUrl);
|
|
2161
|
+
const upstreamUrl = new URL(options.publicUrl);
|
|
2162
|
+
upstreamUrl.port = String(await allocateLoopbackPort(publicUrl.hostname));
|
|
2163
|
+
upstreamUrl.search = "";
|
|
2164
|
+
upstreamUrl.hash = "";
|
|
2165
|
+
const gatewayScript = resolveAuthGatewayScript(options.repoRoot);
|
|
2166
|
+
if (!gatewayScript) {
|
|
2167
|
+
throw new Error("Auth gateway script not found");
|
|
2168
|
+
}
|
|
2169
|
+
const token = randomBytes(24).toString("base64url");
|
|
2170
|
+
const tokenPath = appServerGatewayTokenFilePath(
|
|
2171
|
+
options.stateDir,
|
|
2172
|
+
options.instanceId
|
|
2173
|
+
);
|
|
2174
|
+
writeProtectedTextFile(tokenPath, `${token}
|
|
2175
|
+
`);
|
|
2176
|
+
const protectedUrl = buildProtectedAppServerUrl(options.publicUrl, "***");
|
|
2177
|
+
const gatewayLogPath = appServerGatewayLogFilePath(
|
|
2178
|
+
options.stateDir,
|
|
2179
|
+
options.instanceId
|
|
2180
|
+
);
|
|
2181
|
+
fs13.mkdirSync(path13.dirname(gatewayLogPath), { recursive: true });
|
|
2182
|
+
rotateLog(gatewayLogPath);
|
|
2183
|
+
const runtime = resolveNodeRuntime(process.execPath, options.repoRoot);
|
|
2184
|
+
const gatewayArgs = [];
|
|
2185
|
+
if (gatewayScript.endsWith(".ts")) {
|
|
2186
|
+
if (!runtime.supportsStripTypes) {
|
|
2187
|
+
throw new Error(
|
|
2188
|
+
"Current Node runtime cannot start the auth gateway from TypeScript source. Rebuild @hua-labs/tap or use Node 22.6+."
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
gatewayArgs.push("--experimental-strip-types");
|
|
2192
|
+
}
|
|
2193
|
+
gatewayArgs.push(gatewayScript);
|
|
2194
|
+
const gatewayEnv = {
|
|
2195
|
+
...buildRuntimeEnv(options.repoRoot),
|
|
2196
|
+
TAP_GATEWAY_LISTEN_URL: options.publicUrl,
|
|
2197
|
+
TAP_GATEWAY_UPSTREAM_URL: upstreamUrl.toString().replace(/\/$/, ""),
|
|
2198
|
+
TAP_GATEWAY_TOKEN_FILE: tokenPath
|
|
2199
|
+
};
|
|
2200
|
+
let gatewayPid;
|
|
2201
|
+
{
|
|
2202
|
+
let logFd = null;
|
|
2203
|
+
try {
|
|
2204
|
+
if (options.platform === "win32") {
|
|
2205
|
+
gatewayPid = startWindowsDetachedProcess(
|
|
2206
|
+
runtime.command,
|
|
2207
|
+
gatewayArgs,
|
|
2208
|
+
options.repoRoot,
|
|
2209
|
+
gatewayLogPath,
|
|
2210
|
+
gatewayEnv
|
|
2211
|
+
);
|
|
2212
|
+
} else {
|
|
2213
|
+
logFd = fs13.openSync(gatewayLogPath, "a");
|
|
2214
|
+
const child = spawn(runtime.command, gatewayArgs, {
|
|
2215
|
+
cwd: options.repoRoot,
|
|
2216
|
+
detached: true,
|
|
2217
|
+
stdio: ["ignore", logFd, logFd],
|
|
2218
|
+
env: gatewayEnv,
|
|
2219
|
+
windowsHide: true
|
|
2220
|
+
});
|
|
2221
|
+
child.unref();
|
|
2222
|
+
gatewayPid = child.pid ?? null;
|
|
2223
|
+
}
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
removeFileIfExists(tokenPath);
|
|
2226
|
+
throw error;
|
|
2227
|
+
} finally {
|
|
2228
|
+
if (logFd != null) {
|
|
2229
|
+
fs13.closeSync(logFd);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (gatewayPid == null) {
|
|
2234
|
+
removeFileIfExists(tokenPath);
|
|
2235
|
+
throw new Error("Failed to spawn app-server auth gateway");
|
|
2236
|
+
}
|
|
2237
|
+
return {
|
|
2238
|
+
mode: "query-token",
|
|
2239
|
+
protectedUrl,
|
|
2240
|
+
upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
|
|
2241
|
+
tokenPath,
|
|
2242
|
+
gatewayPid,
|
|
2243
|
+
gatewayLogPath
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
function canReuseManagedAppServer(appServer) {
|
|
2247
|
+
if (!appServer?.managed) {
|
|
2248
|
+
return false;
|
|
2249
|
+
}
|
|
2250
|
+
if (appServer.pid != null && !isProcessAlive(appServer.pid)) {
|
|
2251
|
+
return false;
|
|
2252
|
+
}
|
|
2253
|
+
const auth = appServer.auth;
|
|
2254
|
+
if (auth) {
|
|
2255
|
+
if (!auth.protectedUrl) {
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
if (!readGatewayToken(auth)) {
|
|
2259
|
+
return false;
|
|
2260
|
+
}
|
|
2261
|
+
if (auth.gatewayPid != null && !isProcessAlive(auth.gatewayPid)) {
|
|
2262
|
+
return false;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
return true;
|
|
2266
|
+
}
|
|
2267
|
+
function markAppServerHealthy(appServer) {
|
|
2268
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2269
|
+
return {
|
|
2270
|
+
...appServer,
|
|
2271
|
+
healthy: true,
|
|
2272
|
+
lastCheckedAt: checkedAt,
|
|
2273
|
+
lastHealthyAt: checkedAt
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
function findReusableManagedAppServer(stateDir, publicUrl) {
|
|
2277
|
+
const pidDir = path13.join(stateDir, "pids");
|
|
2278
|
+
if (!fs13.existsSync(pidDir)) {
|
|
2279
|
+
return null;
|
|
2280
|
+
}
|
|
2281
|
+
for (const name of fs13.readdirSync(pidDir)) {
|
|
2282
|
+
if (!name.startsWith("bridge-") || !name.endsWith(".json")) {
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
try {
|
|
2286
|
+
const raw = fs13.readFileSync(path13.join(pidDir, name), "utf-8");
|
|
2287
|
+
const parsed = JSON.parse(raw);
|
|
2288
|
+
if (parsed.appServer?.url !== publicUrl) {
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
if (canReuseManagedAppServer(parsed.appServer)) {
|
|
2292
|
+
return markAppServerHealthy(parsed.appServer);
|
|
2293
|
+
}
|
|
2294
|
+
} catch {
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
return null;
|
|
2298
|
+
}
|
|
2299
|
+
function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
|
|
2300
|
+
const ext = path13.extname(command).toLowerCase();
|
|
2301
|
+
const stderrLogPath = stderrLogFilePath(logPath);
|
|
2302
|
+
const stdoutFd = fs13.openSync(logPath, "a");
|
|
2303
|
+
const stderrFd = fs13.openSync(stderrLogPath, "a");
|
|
2304
|
+
try {
|
|
2305
|
+
const child = ext === ".ps1" ? spawn(
|
|
2306
|
+
resolvePowerShellCommand(),
|
|
2307
|
+
["-NoLogo", "-NoProfile", "-File", command, ...args],
|
|
2308
|
+
{
|
|
2309
|
+
cwd: repoRoot,
|
|
2310
|
+
detached: true,
|
|
2311
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
2312
|
+
env,
|
|
2313
|
+
windowsHide: true
|
|
2314
|
+
}
|
|
2315
|
+
) : spawn(command, args, {
|
|
2316
|
+
cwd: repoRoot,
|
|
2317
|
+
detached: true,
|
|
2318
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
2319
|
+
env,
|
|
2320
|
+
windowsHide: true,
|
|
2321
|
+
shell: ext === ".cmd" || ext === ".bat"
|
|
2322
|
+
});
|
|
2323
|
+
child.unref();
|
|
2324
|
+
return child.pid ?? null;
|
|
2325
|
+
} finally {
|
|
2326
|
+
fs13.closeSync(stdoutFd);
|
|
2327
|
+
fs13.closeSync(stderrFd);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
|
|
2331
|
+
return startWindowsDetachedProcess(
|
|
2332
|
+
command,
|
|
2333
|
+
["app-server", "--listen", url],
|
|
2334
|
+
repoRoot,
|
|
2335
|
+
logPath
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
function findListeningProcessId(url, platform) {
|
|
2339
|
+
if (platform !== "win32") {
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
let port;
|
|
2343
|
+
try {
|
|
2344
|
+
const parsed = new URL(url);
|
|
2345
|
+
port = parsed.port ? Number.parseInt(parsed.port, 10) : null;
|
|
2346
|
+
} catch {
|
|
2347
|
+
return null;
|
|
2348
|
+
}
|
|
2349
|
+
if (port == null || !Number.isFinite(port)) {
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
const result = spawnSync2(
|
|
2353
|
+
resolvePowerShellCommand(),
|
|
2354
|
+
[
|
|
2355
|
+
"-NoLogo",
|
|
2356
|
+
"-NoProfile",
|
|
2357
|
+
"-Command",
|
|
2358
|
+
[
|
|
2359
|
+
`$port = ${port}`,
|
|
2360
|
+
"$processId = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess",
|
|
2361
|
+
"if ($processId) { $processId }"
|
|
2362
|
+
].join("; ")
|
|
2363
|
+
],
|
|
2364
|
+
{
|
|
2365
|
+
encoding: "utf-8",
|
|
2366
|
+
windowsHide: true
|
|
2367
|
+
}
|
|
2368
|
+
);
|
|
2369
|
+
if (result.status !== 0) {
|
|
2370
|
+
return null;
|
|
2371
|
+
}
|
|
2372
|
+
const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
|
|
2373
|
+
return Number.isFinite(parsedPid) ? parsedPid : null;
|
|
2374
|
+
}
|
|
2375
|
+
function resolveAppServerUrl(baseUrl, port) {
|
|
2376
|
+
const resolvedBase = (baseUrl ?? DEFAULT_APP_SERVER_URL2).replace(/\/$/, "");
|
|
2377
|
+
if (port == null) {
|
|
2378
|
+
return resolvedBase;
|
|
2379
|
+
}
|
|
2380
|
+
try {
|
|
2381
|
+
const parsed = new URL(resolvedBase);
|
|
2382
|
+
parsed.port = String(port);
|
|
2383
|
+
return parsed.toString().replace(/\/$/, "");
|
|
2384
|
+
} catch {
|
|
2385
|
+
return resolvedBase;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
async function isTcpPortAvailable(hostname, port) {
|
|
2389
|
+
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2390
|
+
return await new Promise((resolve11) => {
|
|
2391
|
+
const server = net.createServer();
|
|
2392
|
+
server.unref();
|
|
2393
|
+
server.once("error", () => resolve11(false));
|
|
2394
|
+
server.listen(port, bindHost, () => {
|
|
2395
|
+
server.close((error) => resolve11(!error));
|
|
2396
|
+
});
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, excludeInstanceId) {
|
|
2400
|
+
let hostname = "127.0.0.1";
|
|
2401
|
+
try {
|
|
2402
|
+
hostname = new URL(baseUrl ?? DEFAULT_APP_SERVER_URL2).hostname;
|
|
2403
|
+
} catch {
|
|
2404
|
+
}
|
|
2405
|
+
const maxAttempts = 1e3;
|
|
2406
|
+
let port = basePort;
|
|
2407
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1, port += 1) {
|
|
2408
|
+
const claimedInState = Object.entries(state.instances).some(
|
|
2409
|
+
([id, inst]) => id !== excludeInstanceId && inst.port === port
|
|
2410
|
+
);
|
|
2411
|
+
if (claimedInState) {
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
if (!isLoopbackHost(hostname)) {
|
|
2415
|
+
return port;
|
|
2416
|
+
}
|
|
2417
|
+
if (await isTcpPortAvailable(hostname, port)) {
|
|
2418
|
+
return port;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
throw new Error(
|
|
2422
|
+
`Failed to find a free app-server port starting at ${basePort}`
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
2426
|
+
const WebSocket = getWebSocketCtor();
|
|
2427
|
+
if (!WebSocket) {
|
|
2428
|
+
return false;
|
|
2429
|
+
}
|
|
2430
|
+
return new Promise((resolve11) => {
|
|
2431
|
+
let settled = false;
|
|
2432
|
+
let socket = null;
|
|
2433
|
+
const finish = (healthy) => {
|
|
2434
|
+
if (settled) {
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
settled = true;
|
|
2438
|
+
clearTimeout(timer);
|
|
2439
|
+
try {
|
|
2440
|
+
socket?.close();
|
|
2441
|
+
} catch {
|
|
2442
|
+
}
|
|
2443
|
+
resolve11(healthy);
|
|
2444
|
+
};
|
|
2445
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2446
|
+
try {
|
|
2447
|
+
socket = new WebSocket(url);
|
|
2448
|
+
socket.addEventListener("open", () => finish(true), { once: true });
|
|
2449
|
+
socket.addEventListener("error", () => finish(false), { once: true });
|
|
2450
|
+
socket.addEventListener("close", () => finish(false), { once: true });
|
|
2451
|
+
} catch {
|
|
2452
|
+
finish(false);
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
async function waitForAppServerHealth(url, timeoutMs) {
|
|
2457
|
+
const deadline = Date.now() + timeoutMs;
|
|
2458
|
+
while (Date.now() < deadline) {
|
|
2459
|
+
if (await checkAppServerHealth(url)) {
|
|
2460
|
+
return true;
|
|
2461
|
+
}
|
|
2462
|
+
await delay(APP_SERVER_HEALTH_RETRY_MS);
|
|
2463
|
+
}
|
|
2464
|
+
return false;
|
|
2465
|
+
}
|
|
2466
|
+
async function terminateProcess(pid, platform) {
|
|
2467
|
+
if (!isProcessAlive(pid)) {
|
|
2468
|
+
return false;
|
|
2469
|
+
}
|
|
2470
|
+
try {
|
|
2471
|
+
if (platform === "win32") {
|
|
2472
|
+
execSync3(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
|
|
2473
|
+
} else {
|
|
2474
|
+
process.kill(pid, "SIGTERM");
|
|
2475
|
+
await delay(2e3);
|
|
2476
|
+
if (isProcessAlive(pid)) {
|
|
2477
|
+
process.kill(pid, "SIGKILL");
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
} catch {
|
|
2481
|
+
}
|
|
2482
|
+
return !isProcessAlive(pid);
|
|
2483
|
+
}
|
|
2484
|
+
async function stopManagedAppServer(appServer, platform) {
|
|
2485
|
+
if (!appServer.managed) {
|
|
2486
|
+
return false;
|
|
2487
|
+
}
|
|
2488
|
+
let stopped = false;
|
|
2489
|
+
if (appServer.auth?.gatewayPid != null) {
|
|
2490
|
+
stopped = await terminateProcess(appServer.auth.gatewayPid, platform) || stopped;
|
|
2491
|
+
}
|
|
2492
|
+
if (appServer.pid != null) {
|
|
2493
|
+
stopped = await terminateProcess(appServer.pid, platform) || stopped;
|
|
2494
|
+
}
|
|
2495
|
+
removeFileIfExists(appServer.auth?.tokenPath);
|
|
2496
|
+
return stopped;
|
|
2497
|
+
}
|
|
2498
|
+
async function ensureCodexAppServer(options) {
|
|
2499
|
+
const effectiveUrl = resolveAppServerUrl(options.appServerUrl);
|
|
2500
|
+
const fallbackManualCommand = formatCodexAppServerCommand(
|
|
2501
|
+
"codex",
|
|
2502
|
+
effectiveUrl
|
|
2503
|
+
);
|
|
2504
|
+
if (options.existingAppServer?.url === effectiveUrl && canReuseManagedAppServer(options.existingAppServer)) {
|
|
2505
|
+
return markAppServerHealthy(options.existingAppServer);
|
|
2506
|
+
}
|
|
2507
|
+
const sharedManaged = findReusableManagedAppServer(
|
|
2508
|
+
options.stateDir,
|
|
2509
|
+
effectiveUrl
|
|
2510
|
+
);
|
|
2511
|
+
if (sharedManaged) {
|
|
2512
|
+
return sharedManaged;
|
|
2513
|
+
}
|
|
2514
|
+
let parsedUrl;
|
|
1816
2515
|
try {
|
|
1817
|
-
|
|
1818
|
-
timeout: 5e3,
|
|
1819
|
-
stdio: "pipe"
|
|
1820
|
-
});
|
|
1821
|
-
return true;
|
|
2516
|
+
parsedUrl = new URL(effectiveUrl);
|
|
1822
2517
|
} catch {
|
|
1823
|
-
|
|
2518
|
+
throw new Error(
|
|
2519
|
+
`Invalid app-server URL: ${effectiveUrl}
|
|
2520
|
+
Start it manually:
|
|
2521
|
+
${fallbackManualCommand}`
|
|
2522
|
+
);
|
|
1824
2523
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
];
|
|
1832
|
-
for (const c of candidates) {
|
|
1833
|
-
if (fs11.existsSync(c)) return c;
|
|
2524
|
+
if (!isLoopbackHost(parsedUrl.hostname)) {
|
|
2525
|
+
throw new Error(
|
|
2526
|
+
`Auto-start only supports loopback app-server URLs. Current URL: ${effectiveUrl}
|
|
2527
|
+
Start it manually:
|
|
2528
|
+
${fallbackManualCommand}`
|
|
2529
|
+
);
|
|
1834
2530
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2531
|
+
if (await checkAppServerHealth(effectiveUrl)) {
|
|
2532
|
+
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.";
|
|
2533
|
+
throw new Error(`${effectiveUrl}: ${hint}`);
|
|
2534
|
+
}
|
|
2535
|
+
const resolvedCommand = resolveCodexCommand(options.platform);
|
|
2536
|
+
if (!resolvedCommand) {
|
|
2537
|
+
throw new Error(
|
|
2538
|
+
`Codex CLI not found in PATH.
|
|
2539
|
+
Start the app-server manually:
|
|
2540
|
+
${fallbackManualCommand}`
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
const logPath = appServerLogFilePath(options.stateDir, options.instanceId);
|
|
2544
|
+
fs13.mkdirSync(path13.dirname(logPath), { recursive: true });
|
|
2545
|
+
rotateLog(logPath);
|
|
2546
|
+
if (options.noAuth) {
|
|
2547
|
+
const manualCommand2 = formatCodexAppServerCommand("codex", effectiveUrl);
|
|
2548
|
+
let pid2;
|
|
2549
|
+
if (options.platform === "win32") {
|
|
2550
|
+
try {
|
|
2551
|
+
pid2 = startWindowsCodexAppServer(
|
|
2552
|
+
resolvedCommand,
|
|
2553
|
+
effectiveUrl,
|
|
2554
|
+
options.repoRoot,
|
|
2555
|
+
logPath
|
|
2556
|
+
);
|
|
2557
|
+
} catch (err) {
|
|
2558
|
+
throw new Error(
|
|
2559
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
2560
|
+
Start it manually:
|
|
2561
|
+
${manualCommand2}`,
|
|
2562
|
+
{ cause: err }
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
} else {
|
|
2566
|
+
const logFd = fs13.openSync(logPath, "a");
|
|
2567
|
+
try {
|
|
2568
|
+
const child = spawn(
|
|
2569
|
+
resolvedCommand,
|
|
2570
|
+
["app-server", "--listen", effectiveUrl],
|
|
2571
|
+
{
|
|
2572
|
+
cwd: options.repoRoot,
|
|
2573
|
+
detached: true,
|
|
2574
|
+
stdio: ["ignore", logFd, logFd],
|
|
2575
|
+
env: process.env,
|
|
2576
|
+
windowsHide: true
|
|
2577
|
+
}
|
|
2578
|
+
);
|
|
2579
|
+
child.unref();
|
|
2580
|
+
pid2 = child.pid ?? null;
|
|
2581
|
+
} catch (err) {
|
|
2582
|
+
throw new Error(
|
|
2583
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
2584
|
+
Start it manually:
|
|
2585
|
+
${manualCommand2}`,
|
|
2586
|
+
{ cause: err }
|
|
2587
|
+
);
|
|
2588
|
+
} finally {
|
|
2589
|
+
fs13.closeSync(logFd);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (pid2 == null) {
|
|
2593
|
+
throw new Error(
|
|
2594
|
+
`Failed to spawn Codex app-server.
|
|
2595
|
+
Start it manually:
|
|
2596
|
+
${manualCommand2}`
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
const healthy2 = await waitForAppServerHealth(
|
|
2600
|
+
effectiveUrl,
|
|
2601
|
+
APP_SERVER_START_TIMEOUT_MS
|
|
2602
|
+
);
|
|
2603
|
+
if (!healthy2) {
|
|
2604
|
+
await terminateProcess(pid2, options.platform);
|
|
2605
|
+
throw new Error(
|
|
2606
|
+
`Codex app-server did not become healthy at ${effectiveUrl}.
|
|
2607
|
+
Check ${logPath}
|
|
2608
|
+
Or start it manually:
|
|
2609
|
+
${manualCommand2}`
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
pid2 = findListeningProcessId(effectiveUrl, options.platform) ?? pid2;
|
|
2613
|
+
const healthyAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1846
2614
|
return {
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2615
|
+
url: effectiveUrl,
|
|
2616
|
+
pid: pid2,
|
|
2617
|
+
managed: true,
|
|
2618
|
+
healthy: true,
|
|
2619
|
+
lastCheckedAt: healthyAt2,
|
|
2620
|
+
lastHealthyAt: healthyAt2,
|
|
2621
|
+
logPath,
|
|
2622
|
+
manualCommand: manualCommand2,
|
|
2623
|
+
auth: null
|
|
1851
2624
|
};
|
|
1852
2625
|
}
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2626
|
+
const auth = await createManagedAppServerAuth({
|
|
2627
|
+
instanceId: options.instanceId,
|
|
2628
|
+
stateDir: options.stateDir,
|
|
2629
|
+
repoRoot: options.repoRoot,
|
|
2630
|
+
platform: options.platform,
|
|
2631
|
+
publicUrl: effectiveUrl
|
|
2632
|
+
});
|
|
2633
|
+
const manualCommand = formatCodexAppServerCommand("codex", auth.upstreamUrl);
|
|
2634
|
+
let pid;
|
|
2635
|
+
if (options.platform === "win32") {
|
|
2636
|
+
try {
|
|
2637
|
+
pid = startWindowsCodexAppServer(
|
|
2638
|
+
resolvedCommand,
|
|
2639
|
+
auth.upstreamUrl,
|
|
2640
|
+
options.repoRoot,
|
|
2641
|
+
logPath
|
|
2642
|
+
);
|
|
2643
|
+
} catch (err) {
|
|
2644
|
+
if (auth.gatewayPid != null) {
|
|
2645
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2646
|
+
}
|
|
2647
|
+
removeFileIfExists(auth.tokenPath);
|
|
2648
|
+
throw new Error(
|
|
2649
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
2650
|
+
Start it manually:
|
|
2651
|
+
${manualCommand}`,
|
|
2652
|
+
{ cause: err }
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
} else {
|
|
2656
|
+
const logFd = fs13.openSync(logPath, "a");
|
|
2657
|
+
try {
|
|
2658
|
+
const child = spawn(
|
|
2659
|
+
resolvedCommand,
|
|
2660
|
+
["app-server", "--listen", auth.upstreamUrl],
|
|
2661
|
+
{
|
|
2662
|
+
cwd: options.repoRoot,
|
|
2663
|
+
detached: true,
|
|
2664
|
+
stdio: ["ignore", logFd, logFd],
|
|
2665
|
+
env: process.env,
|
|
2666
|
+
windowsHide: true
|
|
2667
|
+
}
|
|
2668
|
+
);
|
|
2669
|
+
child.unref();
|
|
2670
|
+
pid = child.pid ?? null;
|
|
2671
|
+
} catch (err) {
|
|
2672
|
+
if (auth.gatewayPid != null) {
|
|
2673
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2674
|
+
}
|
|
2675
|
+
removeFileIfExists(auth.tokenPath);
|
|
2676
|
+
throw new Error(
|
|
2677
|
+
`Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
|
|
2678
|
+
Start it manually:
|
|
2679
|
+
${manualCommand}`,
|
|
2680
|
+
{ cause: err }
|
|
2681
|
+
);
|
|
2682
|
+
} finally {
|
|
2683
|
+
fs13.closeSync(logFd);
|
|
1864
2684
|
}
|
|
1865
2685
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2686
|
+
if (pid == null) {
|
|
2687
|
+
if (auth.gatewayPid != null) {
|
|
2688
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2689
|
+
}
|
|
2690
|
+
removeFileIfExists(auth.tokenPath);
|
|
2691
|
+
throw new Error(
|
|
2692
|
+
`Failed to spawn Codex app-server.
|
|
2693
|
+
Start it manually:
|
|
2694
|
+
${manualCommand}`
|
|
2695
|
+
);
|
|
1874
2696
|
}
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2697
|
+
const healthy = await waitForAppServerHealth(
|
|
2698
|
+
auth.upstreamUrl,
|
|
2699
|
+
APP_SERVER_START_TIMEOUT_MS
|
|
2700
|
+
);
|
|
2701
|
+
if (!healthy) {
|
|
2702
|
+
await terminateProcess(pid, options.platform);
|
|
2703
|
+
if (auth.gatewayPid != null) {
|
|
2704
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2705
|
+
}
|
|
2706
|
+
removeFileIfExists(auth.tokenPath);
|
|
2707
|
+
throw new Error(
|
|
2708
|
+
`Codex app-server did not become healthy at ${auth.upstreamUrl}.
|
|
2709
|
+
Check ${logPath}
|
|
2710
|
+
Or start it manually:
|
|
2711
|
+
${manualCommand}`
|
|
2712
|
+
);
|
|
1883
2713
|
}
|
|
2714
|
+
const gatewayToken = readGatewayToken(auth);
|
|
2715
|
+
if (!gatewayToken) {
|
|
2716
|
+
await terminateProcess(pid, options.platform);
|
|
2717
|
+
if (auth.gatewayPid != null) {
|
|
2718
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2719
|
+
}
|
|
2720
|
+
removeFileIfExists(auth.tokenPath);
|
|
2721
|
+
throw new Error("Tap auth gateway token is missing after startup.");
|
|
2722
|
+
}
|
|
2723
|
+
const gatewayHealthy = await waitForAppServerHealth(
|
|
2724
|
+
buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
|
|
2725
|
+
APP_SERVER_GATEWAY_START_TIMEOUT_MS
|
|
2726
|
+
);
|
|
2727
|
+
if (!gatewayHealthy) {
|
|
2728
|
+
await terminateProcess(pid, options.platform);
|
|
2729
|
+
if (auth.gatewayPid != null) {
|
|
2730
|
+
await terminateProcess(auth.gatewayPid, options.platform);
|
|
2731
|
+
}
|
|
2732
|
+
removeFileIfExists(auth.tokenPath);
|
|
2733
|
+
throw new Error(
|
|
2734
|
+
`Tap auth gateway did not become healthy at ${effectiveUrl}.
|
|
2735
|
+
Check ${auth.gatewayLogPath ?? "the gateway log"} and ${logPath}.`
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
const healthyAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2739
|
+
pid = findListeningProcessId(auth.upstreamUrl, options.platform) ?? pid;
|
|
1884
2740
|
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}`
|
|
2741
|
+
url: effectiveUrl,
|
|
2742
|
+
pid,
|
|
2743
|
+
managed: true,
|
|
2744
|
+
healthy: true,
|
|
2745
|
+
lastCheckedAt: healthyAt,
|
|
2746
|
+
lastHealthyAt: healthyAt,
|
|
2747
|
+
logPath,
|
|
2748
|
+
manualCommand,
|
|
2749
|
+
auth
|
|
1899
2750
|
};
|
|
1900
2751
|
}
|
|
1901
|
-
|
|
1902
|
-
// src/engine/bridge.ts
|
|
1903
2752
|
function pidFilePath(stateDir, instanceId) {
|
|
1904
|
-
return
|
|
2753
|
+
return path13.join(stateDir, "pids", `bridge-${instanceId}.json`);
|
|
1905
2754
|
}
|
|
1906
2755
|
function logFilePath(stateDir, instanceId) {
|
|
1907
|
-
return
|
|
2756
|
+
return path13.join(stateDir, "logs", `bridge-${instanceId}.log`);
|
|
2757
|
+
}
|
|
2758
|
+
function runtimeHeartbeatFilePath(runtimeStateDir) {
|
|
2759
|
+
return path13.join(runtimeStateDir, "heartbeat.json");
|
|
2760
|
+
}
|
|
2761
|
+
function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
|
|
2762
|
+
if (!runtimeStateDir) {
|
|
2763
|
+
return null;
|
|
2764
|
+
}
|
|
2765
|
+
const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
|
|
2766
|
+
if (!fs13.existsSync(heartbeatPath)) {
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2769
|
+
try {
|
|
2770
|
+
const raw = fs13.readFileSync(heartbeatPath, "utf-8");
|
|
2771
|
+
const parsed = JSON.parse(raw);
|
|
2772
|
+
return typeof parsed.updatedAt === "string" ? parsed.updatedAt : null;
|
|
2773
|
+
} catch {
|
|
2774
|
+
return null;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
function resolveHeartbeatTimestamp(state) {
|
|
2778
|
+
return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
|
|
1908
2779
|
}
|
|
1909
2780
|
function loadBridgeState(stateDir, instanceId) {
|
|
1910
2781
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
1911
|
-
if (!
|
|
2782
|
+
if (!fs13.existsSync(pidPath)) return null;
|
|
1912
2783
|
try {
|
|
1913
|
-
const raw =
|
|
2784
|
+
const raw = fs13.readFileSync(pidPath, "utf-8");
|
|
1914
2785
|
return JSON.parse(raw);
|
|
1915
2786
|
} catch {
|
|
1916
2787
|
return null;
|
|
@@ -1918,15 +2789,16 @@ function loadBridgeState(stateDir, instanceId) {
|
|
|
1918
2789
|
}
|
|
1919
2790
|
function saveBridgeState(stateDir, instanceId, state) {
|
|
1920
2791
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2792
|
+
const serializable = JSON.parse(JSON.stringify(state));
|
|
2793
|
+
if (serializable.appServer?.auth) {
|
|
2794
|
+
delete serializable.appServer.auth.token;
|
|
2795
|
+
}
|
|
2796
|
+
writeProtectedTextFile(pidPath, JSON.stringify(serializable, null, 2));
|
|
1925
2797
|
}
|
|
1926
2798
|
function clearBridgeState(stateDir, instanceId) {
|
|
1927
2799
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
1928
|
-
if (
|
|
1929
|
-
|
|
2800
|
+
if (fs13.existsSync(pidPath)) {
|
|
2801
|
+
fs13.unlinkSync(pidPath);
|
|
1930
2802
|
}
|
|
1931
2803
|
}
|
|
1932
2804
|
function isProcessAlive(pid) {
|
|
@@ -1964,31 +2836,61 @@ async function startBridge(options) {
|
|
|
1964
2836
|
`Bridge for ${instanceId} is already running (PID: ${existing.pid})`
|
|
1965
2837
|
);
|
|
1966
2838
|
}
|
|
2839
|
+
const previousBridgeState = loadBridgeState(stateDir, instanceId);
|
|
2840
|
+
const previousAppServer = previousBridgeState?.appServer ?? null;
|
|
1967
2841
|
clearBridgeState(stateDir, instanceId);
|
|
1968
2842
|
const logPath = logFilePath(stateDir, instanceId);
|
|
1969
|
-
|
|
2843
|
+
fs13.mkdirSync(path13.dirname(logPath), { recursive: true });
|
|
1970
2844
|
rotateLog(logPath);
|
|
1971
|
-
|
|
1972
|
-
const repoRoot = options.repoRoot ??
|
|
2845
|
+
let logFd = null;
|
|
2846
|
+
const repoRoot = options.repoRoot ?? path13.resolve(stateDir, "..");
|
|
2847
|
+
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
1973
2848
|
const resolved = resolveNodeRuntime(
|
|
1974
2849
|
options.runtimeCommand ?? "node",
|
|
1975
2850
|
repoRoot
|
|
1976
2851
|
);
|
|
1977
2852
|
const command = resolved.command;
|
|
1978
2853
|
const runtimeEnv = buildRuntimeEnv(repoRoot);
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2854
|
+
const effectiveAppServerUrl = resolveAppServerUrl(options.appServerUrl, port);
|
|
2855
|
+
let appServer = null;
|
|
2856
|
+
let bridgeAppServerUrl = effectiveAppServerUrl;
|
|
2857
|
+
if (runtime === "codex" && options.manageAppServer) {
|
|
2858
|
+
appServer = await ensureCodexAppServer({
|
|
2859
|
+
instanceId,
|
|
2860
|
+
stateDir,
|
|
2861
|
+
repoRoot,
|
|
2862
|
+
platform: options.platform,
|
|
2863
|
+
appServerUrl: effectiveAppServerUrl,
|
|
2864
|
+
existingAppServer: previousAppServer,
|
|
2865
|
+
noAuth: options.noAuth
|
|
2866
|
+
});
|
|
2867
|
+
if (appServer.auth) {
|
|
2868
|
+
appServer = {
|
|
2869
|
+
...appServer,
|
|
2870
|
+
auth: materializeGatewayTokenFile(
|
|
2871
|
+
stateDir,
|
|
2872
|
+
instanceId,
|
|
2873
|
+
effectiveAppServerUrl,
|
|
2874
|
+
appServer.auth
|
|
2875
|
+
)
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
bridgeAppServerUrl = effectiveAppServerUrl;
|
|
2879
|
+
}
|
|
2880
|
+
try {
|
|
2881
|
+
const bridgeEnv = {
|
|
1983
2882
|
...runtimeEnv,
|
|
1984
2883
|
TAP_COMMS_DIR: commsDir,
|
|
2884
|
+
TAP_STATE_DIR: runtimeStateDir,
|
|
1985
2885
|
TAP_BRIDGE_RUNTIME: runtime,
|
|
1986
2886
|
TAP_BRIDGE_INSTANCE_ID: instanceId,
|
|
2887
|
+
TAP_AGENT_ID: instanceId,
|
|
1987
2888
|
TAP_AGENT_NAME: resolvedAgent,
|
|
1988
2889
|
CODEX_TAP_AGENT_NAME: resolvedAgent,
|
|
1989
2890
|
TAP_RESOLVED_NODE: resolved.command,
|
|
1990
2891
|
TAP_STRIP_TYPES: resolved.supportsStripTypes ? "1" : "0",
|
|
1991
|
-
...
|
|
2892
|
+
...bridgeAppServerUrl ? { CODEX_APP_SERVER_URL: bridgeAppServerUrl } : {},
|
|
2893
|
+
...appServer?.auth?.tokenPath ? { TAP_GATEWAY_TOKEN_FILE: appServer.auth.tokenPath } : {},
|
|
1992
2894
|
...port != null ? { TAP_BRIDGE_PORT: String(port) } : {},
|
|
1993
2895
|
...options.headless?.enabled ? {
|
|
1994
2896
|
TAP_HEADLESS: "true",
|
|
@@ -1996,7 +2898,6 @@ async function startBridge(options) {
|
|
|
1996
2898
|
TAP_MAX_REVIEW_ROUNDS: String(options.headless.maxRounds),
|
|
1997
2899
|
TAP_QUALITY_FLOOR: options.headless.qualitySeverityFloor
|
|
1998
2900
|
} : {},
|
|
1999
|
-
// Bridge script operational flags
|
|
2000
2901
|
...options.busyMode ? { TAP_BUSY_MODE: options.busyMode } : {},
|
|
2001
2902
|
...options.pollSeconds != null ? { TAP_POLL_SECONDS: String(options.pollSeconds) } : {},
|
|
2002
2903
|
...options.reconnectSeconds != null ? { TAP_RECONNECT_SECONDS: String(options.reconnectSeconds) } : {},
|
|
@@ -2008,20 +2909,55 @@ async function startBridge(options) {
|
|
|
2008
2909
|
...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
|
|
2009
2910
|
...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
|
|
2010
2911
|
...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
|
|
2912
|
+
};
|
|
2913
|
+
let bridgePid = null;
|
|
2914
|
+
if (options.platform === "win32") {
|
|
2915
|
+
bridgePid = startWindowsDetachedProcess(
|
|
2916
|
+
command,
|
|
2917
|
+
[bridgeScript],
|
|
2918
|
+
repoRoot,
|
|
2919
|
+
logPath,
|
|
2920
|
+
bridgeEnv
|
|
2921
|
+
);
|
|
2922
|
+
} else {
|
|
2923
|
+
logFd = fs13.openSync(logPath, "a");
|
|
2924
|
+
const child = spawn(command, [bridgeScript], {
|
|
2925
|
+
detached: true,
|
|
2926
|
+
stdio: ["ignore", logFd, logFd],
|
|
2927
|
+
env: bridgeEnv,
|
|
2928
|
+
windowsHide: true
|
|
2929
|
+
});
|
|
2930
|
+
child.unref();
|
|
2931
|
+
bridgePid = child.pid ?? null;
|
|
2011
2932
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2933
|
+
if (logFd != null) {
|
|
2934
|
+
fs13.closeSync(logFd);
|
|
2935
|
+
logFd = null;
|
|
2936
|
+
}
|
|
2937
|
+
if (!bridgePid) {
|
|
2938
|
+
throw new Error(`Failed to spawn bridge process for ${instanceId}`);
|
|
2939
|
+
}
|
|
2940
|
+
const state = {
|
|
2941
|
+
pid: bridgePid,
|
|
2942
|
+
statePath: pidFilePath(stateDir, instanceId),
|
|
2943
|
+
lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2944
|
+
appServer,
|
|
2945
|
+
runtimeStateDir
|
|
2946
|
+
};
|
|
2947
|
+
saveBridgeState(stateDir, instanceId, state);
|
|
2948
|
+
return state;
|
|
2949
|
+
} catch (err) {
|
|
2950
|
+
if (logFd != null) {
|
|
2951
|
+
try {
|
|
2952
|
+
fs13.closeSync(logFd);
|
|
2953
|
+
} catch {
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
if (appServer?.managed) {
|
|
2957
|
+
await stopManagedAppServer(appServer, options.platform);
|
|
2958
|
+
}
|
|
2959
|
+
throw err;
|
|
2960
|
+
}
|
|
2025
2961
|
}
|
|
2026
2962
|
async function stopBridge(options) {
|
|
2027
2963
|
const { instanceId, stateDir, platform } = options;
|
|
@@ -2034,37 +2970,33 @@ async function stopBridge(options) {
|
|
|
2034
2970
|
return false;
|
|
2035
2971
|
}
|
|
2036
2972
|
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
|
-
}
|
|
2973
|
+
await terminateProcess(state.pid, platform);
|
|
2046
2974
|
} catch {
|
|
2047
2975
|
}
|
|
2048
2976
|
clearBridgeState(stateDir, instanceId);
|
|
2049
2977
|
return true;
|
|
2050
2978
|
}
|
|
2051
2979
|
function rotateLog(logPath) {
|
|
2052
|
-
if (!
|
|
2980
|
+
if (!fs13.existsSync(logPath)) return;
|
|
2053
2981
|
try {
|
|
2054
|
-
const stats =
|
|
2982
|
+
const stats = fs13.statSync(logPath);
|
|
2055
2983
|
if (stats.size === 0) return;
|
|
2056
2984
|
const prevPath = `${logPath}.prev`;
|
|
2057
|
-
|
|
2985
|
+
fs13.renameSync(logPath, prevPath);
|
|
2058
2986
|
} catch {
|
|
2059
2987
|
}
|
|
2060
2988
|
}
|
|
2061
2989
|
function getHeartbeatAge(stateDir, instanceId) {
|
|
2062
2990
|
const state = loadBridgeState(stateDir, instanceId);
|
|
2063
|
-
|
|
2064
|
-
|
|
2991
|
+
const heartbeat = resolveHeartbeatTimestamp(state);
|
|
2992
|
+
if (!heartbeat) return null;
|
|
2993
|
+
const heartbeatTime = new Date(heartbeat).getTime();
|
|
2065
2994
|
if (isNaN(heartbeatTime)) return null;
|
|
2066
2995
|
return Math.floor((Date.now() - heartbeatTime) / 1e3);
|
|
2067
2996
|
}
|
|
2997
|
+
function getBridgeHeartbeatTimestamp(stateDir, instanceId) {
|
|
2998
|
+
return resolveHeartbeatTimestamp(loadBridgeState(stateDir, instanceId));
|
|
2999
|
+
}
|
|
2068
3000
|
function getBridgeStatus(stateDir, instanceId) {
|
|
2069
3001
|
const state = loadBridgeState(stateDir, instanceId);
|
|
2070
3002
|
if (!state) return "stopped";
|
|
@@ -2084,7 +3016,7 @@ async function addCommand(args) {
|
|
|
2084
3016
|
ok: false,
|
|
2085
3017
|
command: "add",
|
|
2086
3018
|
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>]",
|
|
3019
|
+
message: "Missing runtime argument. Usage: npx @hua-labs/tap add <claude|codex|gemini> [--name <name>] [--port <port>] [--agent-name <name>] [--headless] [--role <role>]",
|
|
2088
3020
|
warnings: [],
|
|
2089
3021
|
data: {}
|
|
2090
3022
|
};
|
|
@@ -2104,6 +3036,7 @@ async function addCommand(args) {
|
|
|
2104
3036
|
const instanceId = buildInstanceId(runtime, instanceName);
|
|
2105
3037
|
const portStr = typeof flags["port"] === "string" ? flags["port"] : void 0;
|
|
2106
3038
|
const port = portStr ? parseInt(portStr, 10) : null;
|
|
3039
|
+
const agentNameFlag = typeof flags["agent-name"] === "string" ? flags["agent-name"] : null;
|
|
2107
3040
|
const force = flags["force"] === true;
|
|
2108
3041
|
const headlessFlag = flags["headless"] === true;
|
|
2109
3042
|
const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
|
|
@@ -2150,7 +3083,7 @@ async function addCommand(args) {
|
|
|
2150
3083
|
data: {}
|
|
2151
3084
|
};
|
|
2152
3085
|
}
|
|
2153
|
-
const repoRoot =
|
|
3086
|
+
const repoRoot = findRepoRoot();
|
|
2154
3087
|
const state = loadState(repoRoot);
|
|
2155
3088
|
if (!state) {
|
|
2156
3089
|
return {
|
|
@@ -2194,7 +3127,7 @@ async function addCommand(args) {
|
|
|
2194
3127
|
logHeader(`@hua-labs/tap add ${instanceId}`);
|
|
2195
3128
|
if (instanceName) log(`Instance name: ${instanceName}`);
|
|
2196
3129
|
if (port !== null) log(`Port: ${port}`);
|
|
2197
|
-
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
3130
|
+
const ctx = { ...createAdapterContext(state.commsDir, repoRoot), instanceId };
|
|
2198
3131
|
const adapter = getAdapter(runtime);
|
|
2199
3132
|
const warnings = [];
|
|
2200
3133
|
log("Probing runtime...");
|
|
@@ -2226,13 +3159,15 @@ async function addCommand(args) {
|
|
|
2226
3159
|
log(`Artifacts: ${plan.ownedArtifacts.length}`);
|
|
2227
3160
|
for (const w of plan.warnings) logWarn(w);
|
|
2228
3161
|
if (plan.operations.length === 0) {
|
|
3162
|
+
const failureMessage = probe.issues[0] ?? plan.warnings[0] ?? probe.warnings[0] ?? "No operations to apply. Runtime not configured.";
|
|
3163
|
+
const failureCode = /MCP server/i.test(failureMessage) ? "TAP_LOCAL_SERVER_MISSING" : "TAP_PATCH_FAILED";
|
|
2229
3164
|
return {
|
|
2230
|
-
ok:
|
|
3165
|
+
ok: false,
|
|
2231
3166
|
command: "add",
|
|
2232
3167
|
runtime,
|
|
2233
3168
|
instanceId,
|
|
2234
|
-
code:
|
|
2235
|
-
message:
|
|
3169
|
+
code: failureCode,
|
|
3170
|
+
message: failureMessage,
|
|
2236
3171
|
warnings,
|
|
2237
3172
|
data: { planOps: 0 }
|
|
2238
3173
|
};
|
|
@@ -2280,10 +3215,10 @@ async function addCommand(args) {
|
|
|
2280
3215
|
logWarn("Bridge script not found. Bridge not started.");
|
|
2281
3216
|
warnings.push("Bridge script not found. Run bridge manually.");
|
|
2282
3217
|
} else {
|
|
2283
|
-
const
|
|
2284
|
-
if (!
|
|
3218
|
+
const resolvedAgentName = agentNameFlag || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
|
|
3219
|
+
if (!resolvedAgentName) {
|
|
2285
3220
|
logWarn(
|
|
2286
|
-
"No agent name set
|
|
3221
|
+
"No agent name set. Bridge not started. Use: npx @hua-labs/tap bridge start <instance> --agent-name <name>"
|
|
2287
3222
|
);
|
|
2288
3223
|
warnings.push("Bridge not auto-started: no agent name available.");
|
|
2289
3224
|
} else {
|
|
@@ -2297,7 +3232,7 @@ async function addCommand(args) {
|
|
|
2297
3232
|
commsDir: ctx.commsDir,
|
|
2298
3233
|
bridgeScript,
|
|
2299
3234
|
platform: ctx.platform,
|
|
2300
|
-
agentName:
|
|
3235
|
+
agentName: resolvedAgentName,
|
|
2301
3236
|
runtimeCommand: resolvedCfg.runtimeCommand,
|
|
2302
3237
|
appServerUrl: resolvedCfg.appServerUrl,
|
|
2303
3238
|
repoRoot,
|
|
@@ -2313,10 +3248,11 @@ async function addCommand(args) {
|
|
|
2313
3248
|
}
|
|
2314
3249
|
}
|
|
2315
3250
|
}
|
|
3251
|
+
const existingAgentName = state.instances[instanceId]?.agentName ?? null;
|
|
2316
3252
|
const instanceState = {
|
|
2317
3253
|
instanceId,
|
|
2318
3254
|
runtime,
|
|
2319
|
-
agentName:
|
|
3255
|
+
agentName: agentNameFlag ?? existingAgentName,
|
|
2320
3256
|
port,
|
|
2321
3257
|
installed: true,
|
|
2322
3258
|
configPath: probe.configPath ?? "",
|
|
@@ -2382,7 +3318,7 @@ function instanceStatusLine(inst, status) {
|
|
|
2382
3318
|
return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
|
|
2383
3319
|
}
|
|
2384
3320
|
async function statusCommand(_args) {
|
|
2385
|
-
const repoRoot =
|
|
3321
|
+
const repoRoot = findRepoRoot();
|
|
2386
3322
|
const state = loadState(repoRoot);
|
|
2387
3323
|
if (!state) {
|
|
2388
3324
|
return {
|
|
@@ -2395,7 +3331,7 @@ async function statusCommand(_args) {
|
|
|
2395
3331
|
};
|
|
2396
3332
|
}
|
|
2397
3333
|
logHeader("@hua-labs/tap status");
|
|
2398
|
-
log(`Version: ${
|
|
3334
|
+
log(`Version: ${version}`);
|
|
2399
3335
|
log(`Comms dir: ${state.commsDir}`);
|
|
2400
3336
|
log(`Repo root: ${state.repoRoot}`);
|
|
2401
3337
|
log(`Schema: v${state.schemaVersion}`);
|
|
@@ -2452,7 +3388,7 @@ async function statusCommand(_args) {
|
|
|
2452
3388
|
message: `${installed.length} instance(s) installed`,
|
|
2453
3389
|
warnings: [],
|
|
2454
3390
|
data: {
|
|
2455
|
-
version
|
|
3391
|
+
version,
|
|
2456
3392
|
commsDir: state.commsDir,
|
|
2457
3393
|
repoRoot: state.repoRoot,
|
|
2458
3394
|
instances
|
|
@@ -2461,7 +3397,7 @@ async function statusCommand(_args) {
|
|
|
2461
3397
|
}
|
|
2462
3398
|
|
|
2463
3399
|
// src/engine/rollback.ts
|
|
2464
|
-
import * as
|
|
3400
|
+
import * as fs14 from "fs";
|
|
2465
3401
|
async function rollbackRuntime(_instanceId, runtimeState) {
|
|
2466
3402
|
const errors = [];
|
|
2467
3403
|
const restoredFiles = [];
|
|
@@ -2490,7 +3426,7 @@ async function rollbackRuntime(_instanceId, runtimeState) {
|
|
|
2490
3426
|
};
|
|
2491
3427
|
}
|
|
2492
3428
|
function rollbackArtifact(artifact) {
|
|
2493
|
-
if (!
|
|
3429
|
+
if (!fs14.existsSync(artifact.path)) {
|
|
2494
3430
|
return { restored: false, error: `File not found: ${artifact.path}` };
|
|
2495
3431
|
}
|
|
2496
3432
|
switch (artifact.kind) {
|
|
@@ -2508,7 +3444,7 @@ function rollbackArtifact(artifact) {
|
|
|
2508
3444
|
}
|
|
2509
3445
|
}
|
|
2510
3446
|
function rollbackJsonPath(artifact) {
|
|
2511
|
-
const raw =
|
|
3447
|
+
const raw = fs14.readFileSync(artifact.path, "utf-8");
|
|
2512
3448
|
let config;
|
|
2513
3449
|
try {
|
|
2514
3450
|
config = JSON.parse(raw);
|
|
@@ -2534,18 +3470,18 @@ function rollbackJsonPath(artifact) {
|
|
|
2534
3470
|
cleanEmptyParents(config, artifact.selector);
|
|
2535
3471
|
}
|
|
2536
3472
|
const tmp = `${artifact.path}.tmp.${process.pid}`;
|
|
2537
|
-
|
|
2538
|
-
|
|
3473
|
+
fs14.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
3474
|
+
fs14.renameSync(tmp, artifact.path);
|
|
2539
3475
|
return { restored: true };
|
|
2540
3476
|
}
|
|
2541
3477
|
function rollbackTomlTable(artifact) {
|
|
2542
|
-
const content =
|
|
3478
|
+
const content = fs14.readFileSync(artifact.path, "utf-8");
|
|
2543
3479
|
const backup = artifact.backupPath ? readArtifactBackup(artifact.backupPath) : null;
|
|
2544
3480
|
if (backup?.kind === "toml-table" && backup.selector === artifact.selector) {
|
|
2545
3481
|
const nextContent = backup.existed ? replaceTomlTable(content, artifact.selector, backup.content ?? "") : removeTomlTable(content, artifact.selector);
|
|
2546
3482
|
const tmp2 = `${artifact.path}.tmp.${process.pid}`;
|
|
2547
|
-
|
|
2548
|
-
|
|
3483
|
+
fs14.writeFileSync(tmp2, nextContent, "utf-8");
|
|
3484
|
+
fs14.renameSync(tmp2, artifact.path);
|
|
2549
3485
|
return { restored: true };
|
|
2550
3486
|
}
|
|
2551
3487
|
if (!extractTomlTable(content, artifact.selector)) {
|
|
@@ -2555,13 +3491,13 @@ function rollbackTomlTable(artifact) {
|
|
|
2555
3491
|
};
|
|
2556
3492
|
}
|
|
2557
3493
|
const tmp = `${artifact.path}.tmp.${process.pid}`;
|
|
2558
|
-
|
|
2559
|
-
|
|
3494
|
+
fs14.writeFileSync(tmp, removeTomlTable(content, artifact.selector), "utf-8");
|
|
3495
|
+
fs14.renameSync(tmp, artifact.path);
|
|
2560
3496
|
return { restored: true };
|
|
2561
3497
|
}
|
|
2562
3498
|
function rollbackFile(artifact) {
|
|
2563
|
-
if (
|
|
2564
|
-
|
|
3499
|
+
if (fs14.existsSync(artifact.path)) {
|
|
3500
|
+
fs14.unlinkSync(artifact.path);
|
|
2565
3501
|
return { restored: true };
|
|
2566
3502
|
}
|
|
2567
3503
|
return { restored: false, error: `File not found: ${artifact.path}` };
|
|
@@ -2622,7 +3558,7 @@ async function removeCommand(args) {
|
|
|
2622
3558
|
data: {}
|
|
2623
3559
|
};
|
|
2624
3560
|
}
|
|
2625
|
-
const repoRoot =
|
|
3561
|
+
const repoRoot = findRepoRoot();
|
|
2626
3562
|
const state = loadState(repoRoot);
|
|
2627
3563
|
if (!state) {
|
|
2628
3564
|
return {
|
|
@@ -2708,7 +3644,7 @@ async function removeCommand(args) {
|
|
|
2708
3644
|
}
|
|
2709
3645
|
|
|
2710
3646
|
// src/commands/bridge.ts
|
|
2711
|
-
import * as
|
|
3647
|
+
import * as path14 from "path";
|
|
2712
3648
|
function formatAge(seconds) {
|
|
2713
3649
|
if (seconds < 60) return `${seconds}s ago`;
|
|
2714
3650
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
@@ -2720,6 +3656,7 @@ Usage:
|
|
|
2720
3656
|
|
|
2721
3657
|
Subcommands:
|
|
2722
3658
|
start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
|
|
3659
|
+
start --all Start all registered app-server instances
|
|
2723
3660
|
stop <instance> Stop bridge for an instance
|
|
2724
3661
|
stop Stop all running bridges
|
|
2725
3662
|
status Show bridge status for all instances
|
|
@@ -2727,6 +3664,8 @@ Subcommands:
|
|
|
2727
3664
|
|
|
2728
3665
|
Options:
|
|
2729
3666
|
--agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
|
|
3667
|
+
Saved to state \u2014 only needed on first start
|
|
3668
|
+
--all Start all registered app-server instances
|
|
2730
3669
|
--busy-mode <steer|wait> How to handle active turns (default: steer)
|
|
2731
3670
|
--poll-seconds <n> Inbox poll interval (default: 5)
|
|
2732
3671
|
--reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
|
|
@@ -2734,17 +3673,98 @@ Options:
|
|
|
2734
3673
|
--thread-id <id> Resume specific thread
|
|
2735
3674
|
--ephemeral Use ephemeral thread (no persistence)
|
|
2736
3675
|
--process-existing-messages Process all existing inbox messages
|
|
3676
|
+
--no-server Skip app-server auto-start and connect only
|
|
3677
|
+
--no-auth Skip auth gateway (app-server listens directly, localhost only)
|
|
3678
|
+
|
|
3679
|
+
Port Assignment:
|
|
3680
|
+
Ports are auto-assigned from 4501 on first bridge start if not set via --port
|
|
3681
|
+
during 'tap add'. Auto-assigned ports are saved to state for future starts.
|
|
2737
3682
|
|
|
2738
3683
|
Examples:
|
|
2739
3684
|
npx @hua-labs/tap bridge start codex --agent-name myAgent
|
|
3685
|
+
npx @hua-labs/tap bridge start --all
|
|
3686
|
+
npx @hua-labs/tap bridge start codex --agent-name myAgent --no-server
|
|
2740
3687
|
npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer --busy-mode steer
|
|
2741
3688
|
npx @hua-labs/tap bridge stop codex
|
|
2742
3689
|
npx @hua-labs/tap bridge stop
|
|
2743
3690
|
npx @hua-labs/tap bridge status
|
|
2744
3691
|
`.trim();
|
|
3692
|
+
function formatAppServerState(appServer) {
|
|
3693
|
+
const ownership = appServer.managed ? "managed" : "external";
|
|
3694
|
+
const pid = appServer.pid != null ? ` pid:${appServer.pid}` : "";
|
|
3695
|
+
const health = appServer.healthy ? "healthy" : "unhealthy";
|
|
3696
|
+
const auth = appServer.auth != null ? `, auth gateway:${appServer.auth.gatewayPid ?? "-"} -> ${appServer.auth.upstreamUrl}` : "";
|
|
3697
|
+
return `${health}, ${ownership}${pid}, ${appServer.url}${auth}`;
|
|
3698
|
+
}
|
|
3699
|
+
function redactProtectedUrl(url) {
|
|
3700
|
+
try {
|
|
3701
|
+
const parsed = new URL(url);
|
|
3702
|
+
if (parsed.searchParams.has("tap_token")) {
|
|
3703
|
+
parsed.searchParams.set("tap_token", "***");
|
|
3704
|
+
}
|
|
3705
|
+
return parsed.toString().replace(/\/$/, "");
|
|
3706
|
+
} catch {
|
|
3707
|
+
return url.replace(/tap_token=[^&]+/g, "tap_token=***");
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
function loadCurrentBridgeState(stateDir, instanceId, fallback) {
|
|
3711
|
+
return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
|
|
3712
|
+
}
|
|
3713
|
+
function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
|
|
3714
|
+
const shared = [];
|
|
3715
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
3716
|
+
if (id === currentInstanceId || !inst?.installed) {
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3719
|
+
const instanceId = id;
|
|
3720
|
+
if (getBridgeStatus(stateDir, instanceId) !== "running") {
|
|
3721
|
+
continue;
|
|
3722
|
+
}
|
|
3723
|
+
const bridgeState = loadCurrentBridgeState(
|
|
3724
|
+
stateDir,
|
|
3725
|
+
instanceId,
|
|
3726
|
+
inst.bridge
|
|
3727
|
+
);
|
|
3728
|
+
if (bridgeState?.appServer?.url === appServerUrl) {
|
|
3729
|
+
shared.push(instanceId);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
return shared;
|
|
3733
|
+
}
|
|
3734
|
+
function transferManagedAppServerOwnership(state, stateDir, recipientId, appServer) {
|
|
3735
|
+
const recipient = state.instances[recipientId];
|
|
3736
|
+
if (!recipient) {
|
|
3737
|
+
return false;
|
|
3738
|
+
}
|
|
3739
|
+
const bridgeState = loadCurrentBridgeState(
|
|
3740
|
+
stateDir,
|
|
3741
|
+
recipientId,
|
|
3742
|
+
recipient.bridge
|
|
3743
|
+
);
|
|
3744
|
+
if (!bridgeState) {
|
|
3745
|
+
return false;
|
|
3746
|
+
}
|
|
3747
|
+
const transferredAppServer = {
|
|
3748
|
+
...appServer,
|
|
3749
|
+
managed: true,
|
|
3750
|
+
healthy: true,
|
|
3751
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3752
|
+
lastHealthyAt: appServer.lastHealthyAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3753
|
+
};
|
|
3754
|
+
const updatedBridge = {
|
|
3755
|
+
...bridgeState,
|
|
3756
|
+
appServer: transferredAppServer
|
|
3757
|
+
};
|
|
3758
|
+
saveBridgeState(stateDir, recipientId, updatedBridge);
|
|
3759
|
+
state.instances[recipientId] = {
|
|
3760
|
+
...recipient,
|
|
3761
|
+
bridge: updatedBridge
|
|
3762
|
+
};
|
|
3763
|
+
return true;
|
|
3764
|
+
}
|
|
2745
3765
|
async function bridgeStart(identifier, agentName, flags = {}) {
|
|
2746
|
-
const repoRoot =
|
|
2747
|
-
|
|
3766
|
+
const repoRoot = findRepoRoot();
|
|
3767
|
+
let state = loadState(repoRoot);
|
|
2748
3768
|
if (!state) {
|
|
2749
3769
|
return {
|
|
2750
3770
|
ok: false,
|
|
@@ -2767,7 +3787,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2767
3787
|
};
|
|
2768
3788
|
}
|
|
2769
3789
|
const instanceId = resolved.instanceId;
|
|
2770
|
-
|
|
3790
|
+
let instance = state.instances[instanceId];
|
|
2771
3791
|
if (!instance?.installed) {
|
|
2772
3792
|
return {
|
|
2773
3793
|
ok: false,
|
|
@@ -2794,6 +3814,13 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2794
3814
|
data: { bridgeMode: mode }
|
|
2795
3815
|
};
|
|
2796
3816
|
}
|
|
3817
|
+
const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
|
|
3818
|
+
if (agentName && agentName !== instance.agentName) {
|
|
3819
|
+
instance = { ...instance, agentName };
|
|
3820
|
+
const updatedState = updateInstanceState(state, instanceId, instance);
|
|
3821
|
+
saveState(repoRoot, updatedState);
|
|
3822
|
+
state = updatedState;
|
|
3823
|
+
}
|
|
2797
3824
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
2798
3825
|
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
2799
3826
|
if (!bridgeScript) {
|
|
@@ -2810,19 +3837,63 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2810
3837
|
}
|
|
2811
3838
|
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
2812
3839
|
const runtimeCommand = resolvedConfig.runtimeCommand;
|
|
2813
|
-
const
|
|
3840
|
+
const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
|
|
3841
|
+
let effectivePort = instance.port;
|
|
3842
|
+
if (effectivePort == null && manageAppServer) {
|
|
3843
|
+
effectivePort = await findNextAvailableAppServerPort(
|
|
3844
|
+
state,
|
|
3845
|
+
resolvedConfig.appServerUrl,
|
|
3846
|
+
4501,
|
|
3847
|
+
instanceId
|
|
3848
|
+
);
|
|
3849
|
+
instance = { ...instance, port: effectivePort };
|
|
3850
|
+
const updatedState = updateInstanceState(state, instanceId, instance);
|
|
3851
|
+
saveState(repoRoot, updatedState);
|
|
3852
|
+
state = updatedState;
|
|
3853
|
+
}
|
|
3854
|
+
const appServerUrl = resolveAppServerUrl(
|
|
3855
|
+
resolvedConfig.appServerUrl,
|
|
3856
|
+
effectivePort ?? void 0
|
|
3857
|
+
);
|
|
2814
3858
|
logHeader(`@hua-labs/tap bridge start ${instanceId}`);
|
|
2815
3859
|
log(`Bridge script: ${bridgeScript}`);
|
|
2816
3860
|
log(`Bridge mode: ${mode}`);
|
|
2817
3861
|
log(`Runtime cmd: ${runtimeCommand}`);
|
|
2818
3862
|
log(`App server: ${appServerUrl}`);
|
|
2819
|
-
if (
|
|
3863
|
+
if (effectivePort != null) log(`Port: ${effectivePort}`);
|
|
3864
|
+
if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
|
|
3865
|
+
const noAuth = flags["no-auth"] === true;
|
|
3866
|
+
if (!manageAppServer && instance.runtime === "codex") {
|
|
3867
|
+
log("Auto server: disabled (--no-server)");
|
|
3868
|
+
}
|
|
3869
|
+
if (noAuth && manageAppServer) {
|
|
3870
|
+
log("Auth gateway: disabled (--no-auth)");
|
|
3871
|
+
}
|
|
2820
3872
|
const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
|
|
2821
3873
|
if (willBeHeadless) {
|
|
2822
3874
|
const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
|
|
2823
3875
|
log(`Headless: ${role}`);
|
|
2824
3876
|
}
|
|
2825
3877
|
try {
|
|
3878
|
+
if (!manageAppServer && instance.runtime === "codex") {
|
|
3879
|
+
log("Checking app-server health...");
|
|
3880
|
+
const healthy = await checkAppServerHealth(appServerUrl);
|
|
3881
|
+
if (healthy) {
|
|
3882
|
+
logSuccess("App server reachable");
|
|
3883
|
+
} else {
|
|
3884
|
+
logError(`App server not reachable at ${appServerUrl}`);
|
|
3885
|
+
return {
|
|
3886
|
+
ok: false,
|
|
3887
|
+
command: "bridge",
|
|
3888
|
+
instanceId,
|
|
3889
|
+
runtime: instance.runtime,
|
|
3890
|
+
code: "TAP_BRIDGE_START_FAILED",
|
|
3891
|
+
message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
|
|
3892
|
+
warnings: [],
|
|
3893
|
+
data: {}
|
|
3894
|
+
};
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
2826
3897
|
const busyModeRaw = flags["busy-mode"];
|
|
2827
3898
|
if (busyModeRaw !== void 0 && busyModeRaw !== "steer" && busyModeRaw !== "wait") {
|
|
2828
3899
|
return {
|
|
@@ -2871,11 +3942,13 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2871
3942
|
commsDir: ctx.commsDir,
|
|
2872
3943
|
bridgeScript,
|
|
2873
3944
|
platform: ctx.platform,
|
|
2874
|
-
agentName,
|
|
3945
|
+
agentName: resolvedAgentName,
|
|
2875
3946
|
runtimeCommand,
|
|
2876
3947
|
appServerUrl,
|
|
2877
3948
|
repoRoot,
|
|
2878
|
-
port:
|
|
3949
|
+
port: effectivePort ?? void 0,
|
|
3950
|
+
manageAppServer,
|
|
3951
|
+
noAuth,
|
|
2879
3952
|
headless,
|
|
2880
3953
|
busyMode,
|
|
2881
3954
|
pollSeconds,
|
|
@@ -2886,7 +3959,25 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2886
3959
|
processExistingMessages
|
|
2887
3960
|
});
|
|
2888
3961
|
logSuccess(`Bridge started (PID: ${bridge.pid})`);
|
|
2889
|
-
log(`Log: ${
|
|
3962
|
+
log(`Log: ${path14.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
|
|
3963
|
+
if (bridge.appServer) {
|
|
3964
|
+
log(`App server: ${formatAppServerState(bridge.appServer)}`);
|
|
3965
|
+
if (bridge.appServer.logPath) {
|
|
3966
|
+
log(`Server log: ${bridge.appServer.logPath}`);
|
|
3967
|
+
}
|
|
3968
|
+
if (bridge.appServer.auth) {
|
|
3969
|
+
log(
|
|
3970
|
+
`Protected: ${redactProtectedUrl(bridge.appServer.auth.protectedUrl)}`
|
|
3971
|
+
);
|
|
3972
|
+
if (bridge.appServer.auth.gatewayLogPath) {
|
|
3973
|
+
log(`Gateway log: ${bridge.appServer.auth.gatewayLogPath}`);
|
|
3974
|
+
}
|
|
3975
|
+
log(`TUI connect: ${bridge.appServer.auth.upstreamUrl}`);
|
|
3976
|
+
}
|
|
3977
|
+
if (bridge.appServer.managed && !bridge.appServer.auth) {
|
|
3978
|
+
log(`TUI connect: ${bridge.appServer.url}`);
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
2890
3981
|
const updated = { ...instance, bridge };
|
|
2891
3982
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
2892
3983
|
saveState(repoRoot, newState);
|
|
@@ -2898,25 +3989,93 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
2898
3989
|
code: "TAP_BRIDGE_START_OK",
|
|
2899
3990
|
message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
|
|
2900
3991
|
warnings: [],
|
|
2901
|
-
data: { pid: bridge.pid }
|
|
3992
|
+
data: { pid: bridge.pid, appServer: bridge.appServer ?? null }
|
|
2902
3993
|
};
|
|
2903
3994
|
} catch (err) {
|
|
2904
3995
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2905
3996
|
logError(msg);
|
|
2906
3997
|
return {
|
|
2907
|
-
ok: false,
|
|
3998
|
+
ok: false,
|
|
3999
|
+
command: "bridge",
|
|
4000
|
+
instanceId,
|
|
4001
|
+
runtime: instance.runtime,
|
|
4002
|
+
code: "TAP_BRIDGE_START_FAILED",
|
|
4003
|
+
message: msg,
|
|
4004
|
+
warnings: [],
|
|
4005
|
+
data: {}
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
async function bridgeStartAll(flags = {}) {
|
|
4010
|
+
const repoRoot = findRepoRoot();
|
|
4011
|
+
const state = loadState(repoRoot);
|
|
4012
|
+
if (!state) {
|
|
4013
|
+
return {
|
|
4014
|
+
ok: false,
|
|
4015
|
+
command: "bridge",
|
|
4016
|
+
code: "TAP_NOT_INITIALIZED",
|
|
4017
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
4018
|
+
warnings: [],
|
|
4019
|
+
data: {}
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
const instanceIds = Object.keys(state.instances);
|
|
4023
|
+
const appServerInstances = instanceIds.filter((id) => {
|
|
4024
|
+
const inst = state.instances[id];
|
|
4025
|
+
if (!inst?.installed) return false;
|
|
4026
|
+
const adapter = getAdapter(inst.runtime);
|
|
4027
|
+
return adapter.bridgeMode() === "app-server";
|
|
4028
|
+
});
|
|
4029
|
+
if (appServerInstances.length === 0) {
|
|
4030
|
+
return {
|
|
4031
|
+
ok: true,
|
|
2908
4032
|
command: "bridge",
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
code: "TAP_BRIDGE_START_FAILED",
|
|
2912
|
-
message: msg,
|
|
4033
|
+
code: "TAP_NO_OP",
|
|
4034
|
+
message: "No app-server instances found to start.",
|
|
2913
4035
|
warnings: [],
|
|
2914
4036
|
data: {}
|
|
2915
4037
|
};
|
|
2916
4038
|
}
|
|
4039
|
+
logHeader("@hua-labs/tap bridge start --all");
|
|
4040
|
+
log(
|
|
4041
|
+
`Found ${appServerInstances.length} app-server instance(s): ${appServerInstances.join(", ")}`
|
|
4042
|
+
);
|
|
4043
|
+
log("");
|
|
4044
|
+
const started = [];
|
|
4045
|
+
const failed = [];
|
|
4046
|
+
const warnings = [];
|
|
4047
|
+
for (const instanceId of appServerInstances) {
|
|
4048
|
+
const inst = state.instances[instanceId];
|
|
4049
|
+
const storedName = inst?.agentName ?? void 0;
|
|
4050
|
+
if (!storedName) {
|
|
4051
|
+
const msg = `${instanceId}: skipped \u2014 no stored agent-name. Set it first: tap bridge start ${instanceId} --agent-name <name>`;
|
|
4052
|
+
log(msg);
|
|
4053
|
+
warnings.push(msg);
|
|
4054
|
+
continue;
|
|
4055
|
+
}
|
|
4056
|
+
log(`Starting ${instanceId} (agent: ${storedName})...`);
|
|
4057
|
+
const result = await bridgeStart(instanceId, storedName, flags);
|
|
4058
|
+
if (result.ok) {
|
|
4059
|
+
started.push(instanceId);
|
|
4060
|
+
logSuccess(`${instanceId} started`);
|
|
4061
|
+
} else {
|
|
4062
|
+
failed.push(instanceId);
|
|
4063
|
+
logError(`${instanceId}: ${result.message}`);
|
|
4064
|
+
}
|
|
4065
|
+
log("");
|
|
4066
|
+
}
|
|
4067
|
+
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(", ")}`;
|
|
4068
|
+
return {
|
|
4069
|
+
ok: failed.length === 0 && started.length > 0,
|
|
4070
|
+
command: "bridge",
|
|
4071
|
+
code: started.length > 0 ? "TAP_BRIDGE_START_OK" : "TAP_BRIDGE_START_FAILED",
|
|
4072
|
+
message,
|
|
4073
|
+
warnings,
|
|
4074
|
+
data: { started, failed }
|
|
4075
|
+
};
|
|
2917
4076
|
}
|
|
2918
4077
|
async function bridgeStopOne(identifier) {
|
|
2919
|
-
const repoRoot =
|
|
4078
|
+
const repoRoot = findRepoRoot();
|
|
2920
4079
|
const state = loadState(repoRoot);
|
|
2921
4080
|
if (!state) {
|
|
2922
4081
|
return {
|
|
@@ -2941,20 +4100,64 @@ async function bridgeStopOne(identifier) {
|
|
|
2941
4100
|
}
|
|
2942
4101
|
const instanceId = resolved.instanceId;
|
|
2943
4102
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
4103
|
+
const instance = state.instances[instanceId];
|
|
4104
|
+
const bridgeState = loadCurrentBridgeState(
|
|
4105
|
+
ctx.stateDir,
|
|
4106
|
+
instanceId,
|
|
4107
|
+
instance?.bridge
|
|
4108
|
+
);
|
|
4109
|
+
const appServer = bridgeState?.appServer ?? null;
|
|
2944
4110
|
logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
|
|
2945
4111
|
const stopped = await stopBridge({
|
|
2946
4112
|
instanceId,
|
|
2947
4113
|
stateDir: ctx.stateDir,
|
|
2948
4114
|
platform: ctx.platform
|
|
2949
4115
|
});
|
|
4116
|
+
let appServerStopped = false;
|
|
4117
|
+
let appServerTransferredTo = null;
|
|
2950
4118
|
if (stopped) {
|
|
2951
4119
|
logSuccess(`Bridge for ${instanceId} stopped`);
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
4120
|
+
} else {
|
|
4121
|
+
log(`No running bridge for ${instanceId}`);
|
|
4122
|
+
}
|
|
4123
|
+
if (appServer?.managed) {
|
|
4124
|
+
const sharedUsers = getSharedAppServerUsers(
|
|
4125
|
+
state,
|
|
4126
|
+
ctx.stateDir,
|
|
4127
|
+
instanceId,
|
|
4128
|
+
appServer.url
|
|
4129
|
+
);
|
|
4130
|
+
if (sharedUsers.length > 0) {
|
|
4131
|
+
const recipient = sharedUsers[0];
|
|
4132
|
+
if (transferManagedAppServerOwnership(
|
|
4133
|
+
state,
|
|
4134
|
+
ctx.stateDir,
|
|
4135
|
+
recipient,
|
|
4136
|
+
appServer
|
|
4137
|
+
)) {
|
|
4138
|
+
appServerTransferredTo = recipient;
|
|
4139
|
+
log(`Managed app-server ownership moved to ${recipient}`);
|
|
4140
|
+
} else {
|
|
4141
|
+
log(
|
|
4142
|
+
`Managed app-server left running at ${appServer.url} because ownership transfer failed`
|
|
4143
|
+
);
|
|
4144
|
+
}
|
|
4145
|
+
} else {
|
|
4146
|
+
appServerStopped = await stopManagedAppServer(appServer, ctx.platform);
|
|
4147
|
+
if (appServerStopped) {
|
|
4148
|
+
const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID: ${appServer.auth.gatewayPid}` : "";
|
|
4149
|
+
logSuccess(
|
|
4150
|
+
`Managed app-server stopped (PID: ${appServer.pid ?? "-"}${gatewayNote})`
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
2957
4153
|
}
|
|
4154
|
+
}
|
|
4155
|
+
if (instance) {
|
|
4156
|
+
const updated = { ...instance, bridge: null };
|
|
4157
|
+
const newState = updateInstanceState(state, instanceId, updated);
|
|
4158
|
+
saveState(repoRoot, newState);
|
|
4159
|
+
}
|
|
4160
|
+
if (stopped) {
|
|
2958
4161
|
return {
|
|
2959
4162
|
ok: true,
|
|
2960
4163
|
command: "bridge",
|
|
@@ -2962,16 +4165,12 @@ async function bridgeStopOne(identifier) {
|
|
|
2962
4165
|
code: "TAP_BRIDGE_STOP_OK",
|
|
2963
4166
|
message: `Bridge for ${instanceId} stopped`,
|
|
2964
4167
|
warnings: [],
|
|
2965
|
-
data: {
|
|
4168
|
+
data: {
|
|
4169
|
+
appServerStopped,
|
|
4170
|
+
appServerTransferredTo
|
|
4171
|
+
}
|
|
2966
4172
|
};
|
|
2967
4173
|
}
|
|
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
4174
|
return {
|
|
2976
4175
|
ok: true,
|
|
2977
4176
|
command: "bridge",
|
|
@@ -2979,11 +4178,14 @@ async function bridgeStopOne(identifier) {
|
|
|
2979
4178
|
code: "TAP_BRIDGE_NOT_RUNNING",
|
|
2980
4179
|
message: `No running bridge for ${instanceId}`,
|
|
2981
4180
|
warnings: [],
|
|
2982
|
-
data: {
|
|
4181
|
+
data: {
|
|
4182
|
+
appServerStopped,
|
|
4183
|
+
appServerTransferredTo
|
|
4184
|
+
}
|
|
2983
4185
|
};
|
|
2984
4186
|
}
|
|
2985
4187
|
async function bridgeStopAll() {
|
|
2986
|
-
const repoRoot =
|
|
4188
|
+
const repoRoot = findRepoRoot();
|
|
2987
4189
|
const state = loadState(repoRoot);
|
|
2988
4190
|
if (!state) {
|
|
2989
4191
|
return {
|
|
@@ -2998,9 +4200,22 @@ async function bridgeStopAll() {
|
|
|
2998
4200
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
2999
4201
|
const instanceIds = Object.keys(state.instances);
|
|
3000
4202
|
const stopped = [];
|
|
4203
|
+
const managedAppServers = /* @__PURE__ */ new Map();
|
|
3001
4204
|
logHeader("@hua-labs/tap bridge stop (all)");
|
|
3002
4205
|
let stateChanged = false;
|
|
3003
4206
|
for (const instanceId of instanceIds) {
|
|
4207
|
+
const bridgeState = loadCurrentBridgeState(
|
|
4208
|
+
ctx.stateDir,
|
|
4209
|
+
instanceId,
|
|
4210
|
+
state.instances[instanceId]?.bridge
|
|
4211
|
+
);
|
|
4212
|
+
const appServer = bridgeState?.appServer;
|
|
4213
|
+
if (appServer?.managed && appServer.pid != null) {
|
|
4214
|
+
managedAppServers.set(
|
|
4215
|
+
`${appServer.url}:${appServer.pid}:${appServer.auth?.gatewayPid ?? "-"}`,
|
|
4216
|
+
appServer
|
|
4217
|
+
);
|
|
4218
|
+
}
|
|
3004
4219
|
const didStop = await stopBridge({
|
|
3005
4220
|
instanceId,
|
|
3006
4221
|
stateDir: ctx.stateDir,
|
|
@@ -3016,6 +4231,16 @@ async function bridgeStopAll() {
|
|
|
3016
4231
|
stateChanged = true;
|
|
3017
4232
|
}
|
|
3018
4233
|
}
|
|
4234
|
+
const stoppedAppServers = [];
|
|
4235
|
+
for (const appServer of managedAppServers.values()) {
|
|
4236
|
+
if (await stopManagedAppServer(appServer, ctx.platform)) {
|
|
4237
|
+
stoppedAppServers.push(appServer.pid);
|
|
4238
|
+
const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID ${appServer.auth.gatewayPid}` : "";
|
|
4239
|
+
logSuccess(
|
|
4240
|
+
`Stopped app-server PID ${appServer.pid} (${appServer.url}${gatewayNote})`
|
|
4241
|
+
);
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
3019
4244
|
if (stateChanged) {
|
|
3020
4245
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3021
4246
|
saveState(repoRoot, state);
|
|
@@ -3028,11 +4253,11 @@ async function bridgeStopAll() {
|
|
|
3028
4253
|
code: stopped.length > 0 ? "TAP_BRIDGE_STOP_OK" : "TAP_BRIDGE_NOT_RUNNING",
|
|
3029
4254
|
message,
|
|
3030
4255
|
warnings: [],
|
|
3031
|
-
data: { stopped }
|
|
4256
|
+
data: { stopped, stoppedAppServers }
|
|
3032
4257
|
};
|
|
3033
4258
|
}
|
|
3034
4259
|
function bridgeStatusAll() {
|
|
3035
|
-
const repoRoot =
|
|
4260
|
+
const repoRoot = findRepoRoot();
|
|
3036
4261
|
const state = loadState(repoRoot);
|
|
3037
4262
|
if (!state) {
|
|
3038
4263
|
return {
|
|
@@ -3067,7 +4292,8 @@ function bridgeStatusAll() {
|
|
|
3067
4292
|
runtime: inst.runtime,
|
|
3068
4293
|
pid: null,
|
|
3069
4294
|
port: inst.port,
|
|
3070
|
-
lastHeartbeat: null
|
|
4295
|
+
lastHeartbeat: null,
|
|
4296
|
+
appServer: null
|
|
3071
4297
|
};
|
|
3072
4298
|
continue;
|
|
3073
4299
|
}
|
|
@@ -3075,7 +4301,7 @@ function bridgeStatusAll() {
|
|
|
3075
4301
|
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
3076
4302
|
const age = getHeartbeatAge(stateDir, instanceId);
|
|
3077
4303
|
const pid = bridgeState?.pid ?? null;
|
|
3078
|
-
const heartbeat =
|
|
4304
|
+
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
3079
4305
|
const pidStr = pid ? String(pid) : "-";
|
|
3080
4306
|
const portStr = inst.port ? String(inst.port) : "-";
|
|
3081
4307
|
const ageStr = age !== null ? formatAge(age) : "-";
|
|
@@ -3083,12 +4309,24 @@ function bridgeStatusAll() {
|
|
|
3083
4309
|
log(
|
|
3084
4310
|
`${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${statusColor.padEnd(10)} ${pidStr.padEnd(8)} ${portStr.padEnd(6)} ${ageStr}`
|
|
3085
4311
|
);
|
|
4312
|
+
if (bridgeState?.appServer) {
|
|
4313
|
+
log(` App server: ${formatAppServerState(bridgeState.appServer)}`);
|
|
4314
|
+
if (bridgeState.appServer.logPath) {
|
|
4315
|
+
log(` Server log: ${bridgeState.appServer.logPath}`);
|
|
4316
|
+
}
|
|
4317
|
+
if (bridgeState.appServer.auth) {
|
|
4318
|
+
log(
|
|
4319
|
+
` Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
3086
4323
|
bridges[instanceId] = {
|
|
3087
4324
|
status,
|
|
3088
4325
|
runtime: inst.runtime,
|
|
3089
4326
|
pid,
|
|
3090
4327
|
port: inst.port,
|
|
3091
|
-
lastHeartbeat: heartbeat
|
|
4328
|
+
lastHeartbeat: heartbeat,
|
|
4329
|
+
appServer: bridgeState?.appServer ?? null
|
|
3092
4330
|
};
|
|
3093
4331
|
}
|
|
3094
4332
|
if (instanceIds.length === 0) {
|
|
@@ -3105,7 +4343,7 @@ function bridgeStatusAll() {
|
|
|
3105
4343
|
};
|
|
3106
4344
|
}
|
|
3107
4345
|
function bridgeStatusOne(identifier) {
|
|
3108
|
-
const repoRoot =
|
|
4346
|
+
const repoRoot = findRepoRoot();
|
|
3109
4347
|
const state = loadState(repoRoot);
|
|
3110
4348
|
if (!state) {
|
|
3111
4349
|
return {
|
|
@@ -3162,7 +4400,8 @@ function bridgeStatusOne(identifier) {
|
|
|
3162
4400
|
bridgeMode: inst.bridgeMode,
|
|
3163
4401
|
pid: null,
|
|
3164
4402
|
port: inst.port,
|
|
3165
|
-
lastHeartbeat: null
|
|
4403
|
+
lastHeartbeat: null,
|
|
4404
|
+
appServer: null
|
|
3166
4405
|
}
|
|
3167
4406
|
};
|
|
3168
4407
|
}
|
|
@@ -3171,15 +4410,45 @@ function bridgeStatusOne(identifier) {
|
|
|
3171
4410
|
const status = getBridgeStatus(stateDir, instanceId);
|
|
3172
4411
|
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
3173
4412
|
const age = getHeartbeatAge(stateDir, instanceId);
|
|
4413
|
+
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
3174
4414
|
log(`Status: ${status}`);
|
|
3175
4415
|
if (bridgeState) {
|
|
3176
4416
|
log(`PID: ${bridgeState.pid}`);
|
|
3177
4417
|
log(
|
|
3178
|
-
`Heartbeat: ${
|
|
4418
|
+
`Heartbeat: ${heartbeat ?? "-"}${age !== null ? ` (${formatAge(age)})` : ""}`
|
|
3179
4419
|
);
|
|
3180
4420
|
log(
|
|
3181
|
-
`Log: ${
|
|
4421
|
+
`Log: ${path14.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
|
|
3182
4422
|
);
|
|
4423
|
+
if (bridgeState.appServer) {
|
|
4424
|
+
log(`App server: ${bridgeState.appServer.url}`);
|
|
4425
|
+
log(`Server PID: ${bridgeState.appServer.pid ?? "-"}`);
|
|
4426
|
+
log(
|
|
4427
|
+
`Server mode: ${bridgeState.appServer.managed ? "managed" : "external"}`
|
|
4428
|
+
);
|
|
4429
|
+
log(
|
|
4430
|
+
`Health: ${bridgeState.appServer.healthy ? "healthy" : "unhealthy"}`
|
|
4431
|
+
);
|
|
4432
|
+
log(`Checked: ${bridgeState.appServer.lastCheckedAt}`);
|
|
4433
|
+
if (bridgeState.appServer.logPath) {
|
|
4434
|
+
log(`Server log: ${bridgeState.appServer.logPath}`);
|
|
4435
|
+
}
|
|
4436
|
+
if (bridgeState.appServer.auth) {
|
|
4437
|
+
log(`Auth: ${bridgeState.appServer.auth.mode}`);
|
|
4438
|
+
log(
|
|
4439
|
+
`Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
|
|
4440
|
+
);
|
|
4441
|
+
log(`Upstream: ${bridgeState.appServer.auth.upstreamUrl}`);
|
|
4442
|
+
log(`TUI connect: ${bridgeState.appServer.auth.upstreamUrl}`);
|
|
4443
|
+
log(`Gateway PID: ${bridgeState.appServer.auth.gatewayPid ?? "-"}`);
|
|
4444
|
+
if (bridgeState.appServer.auth.gatewayLogPath) {
|
|
4445
|
+
log(`Gateway log: ${bridgeState.appServer.auth.gatewayLogPath}`);
|
|
4446
|
+
}
|
|
4447
|
+
} else if (bridgeState.appServer.managed) {
|
|
4448
|
+
log(`Auth: none (--no-auth)`);
|
|
4449
|
+
log(`TUI connect: ${bridgeState.appServer.url}`);
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
3183
4452
|
}
|
|
3184
4453
|
log("");
|
|
3185
4454
|
return {
|
|
@@ -3195,7 +4464,8 @@ function bridgeStatusOne(identifier) {
|
|
|
3195
4464
|
bridgeMode: inst.bridgeMode,
|
|
3196
4465
|
pid: bridgeState?.pid ?? null,
|
|
3197
4466
|
port: inst.port,
|
|
3198
|
-
lastHeartbeat:
|
|
4467
|
+
lastHeartbeat: heartbeat,
|
|
4468
|
+
appServer: bridgeState?.appServer ?? null
|
|
3199
4469
|
}
|
|
3200
4470
|
};
|
|
3201
4471
|
}
|
|
@@ -3217,12 +4487,29 @@ async function bridgeCommand(args) {
|
|
|
3217
4487
|
}
|
|
3218
4488
|
switch (subcommand) {
|
|
3219
4489
|
case "start": {
|
|
4490
|
+
const wantsAll = flags["all"] === true || identifierArg === "--all";
|
|
4491
|
+
const hasInstance = identifierArg && identifierArg !== "--all";
|
|
4492
|
+
if (wantsAll && hasInstance) {
|
|
4493
|
+
return {
|
|
4494
|
+
ok: false,
|
|
4495
|
+
command: "bridge",
|
|
4496
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4497
|
+
message: `Cannot combine <instance> with --all. Use either:
|
|
4498
|
+
tap bridge start ${identifierArg}
|
|
4499
|
+
tap bridge start --all`,
|
|
4500
|
+
warnings: [],
|
|
4501
|
+
data: {}
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
4504
|
+
if (wantsAll) {
|
|
4505
|
+
return bridgeStartAll(flags);
|
|
4506
|
+
}
|
|
3220
4507
|
if (!identifierArg) {
|
|
3221
4508
|
return {
|
|
3222
4509
|
ok: false,
|
|
3223
4510
|
command: "bridge",
|
|
3224
4511
|
code: "TAP_INVALID_ARGUMENT",
|
|
3225
|
-
message: "Missing instance. Usage: npx @hua-labs/tap bridge start <instance>",
|
|
4512
|
+
message: "Missing instance. Usage: npx @hua-labs/tap bridge start <instance> or --all",
|
|
3226
4513
|
warnings: [],
|
|
3227
4514
|
data: {}
|
|
3228
4515
|
};
|
|
@@ -3253,41 +4540,268 @@ async function bridgeCommand(args) {
|
|
|
3253
4540
|
}
|
|
3254
4541
|
}
|
|
3255
4542
|
|
|
4543
|
+
// src/engine/dashboard.ts
|
|
4544
|
+
import * as fs15 from "fs";
|
|
4545
|
+
import * as path15 from "path";
|
|
4546
|
+
import { execSync as execSync4 } from "child_process";
|
|
4547
|
+
function collectAgents(commsDir) {
|
|
4548
|
+
const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
|
|
4549
|
+
if (!fs15.existsSync(heartbeatsPath)) return [];
|
|
4550
|
+
try {
|
|
4551
|
+
const raw = fs15.readFileSync(heartbeatsPath, "utf-8");
|
|
4552
|
+
const data = JSON.parse(raw);
|
|
4553
|
+
return Object.entries(data).map(([name, info]) => ({
|
|
4554
|
+
name: info.agent ?? name,
|
|
4555
|
+
status: info.status ?? null,
|
|
4556
|
+
lastActivity: info.lastActivity ?? info.timestamp ?? null,
|
|
4557
|
+
joinedAt: info.joinedAt ?? null
|
|
4558
|
+
}));
|
|
4559
|
+
} catch {
|
|
4560
|
+
return [];
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
function collectBridges(repoRoot) {
|
|
4564
|
+
const state = loadState(repoRoot);
|
|
4565
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
4566
|
+
const stateDir = config.stateDir;
|
|
4567
|
+
const bridges = [];
|
|
4568
|
+
if (state) {
|
|
4569
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
4570
|
+
if (!inst?.installed) continue;
|
|
4571
|
+
if (inst.bridgeMode !== "app-server") continue;
|
|
4572
|
+
const instanceId = id;
|
|
4573
|
+
const status = getBridgeStatus(stateDir, instanceId);
|
|
4574
|
+
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
4575
|
+
const age = getHeartbeatAge(stateDir, instanceId);
|
|
4576
|
+
bridges.push({
|
|
4577
|
+
instanceId: id,
|
|
4578
|
+
runtime: inst.runtime,
|
|
4579
|
+
status,
|
|
4580
|
+
pid: bridgeState?.pid ?? null,
|
|
4581
|
+
port: inst.port ?? null,
|
|
4582
|
+
heartbeatAge: age,
|
|
4583
|
+
headless: inst.headless?.enabled ?? false
|
|
4584
|
+
});
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
const tmpDir = path15.join(repoRoot, ".tmp");
|
|
4588
|
+
if (fs15.existsSync(tmpDir)) {
|
|
4589
|
+
try {
|
|
4590
|
+
const dirs = fs15.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
|
|
4591
|
+
for (const dir of dirs) {
|
|
4592
|
+
const daemonPath = path15.join(tmpDir, dir, "bridge-daemon.json");
|
|
4593
|
+
if (!fs15.existsSync(daemonPath)) continue;
|
|
4594
|
+
try {
|
|
4595
|
+
const raw = fs15.readFileSync(daemonPath, "utf-8");
|
|
4596
|
+
const daemon = JSON.parse(raw);
|
|
4597
|
+
const alreadyCovered = bridges.some(
|
|
4598
|
+
(b) => b.pid === daemon.pid && b.pid !== null
|
|
4599
|
+
);
|
|
4600
|
+
if (alreadyCovered) continue;
|
|
4601
|
+
const agentFile = path15.join(tmpDir, dir, "agent-name.txt");
|
|
4602
|
+
const agentName = fs15.existsSync(agentFile) ? fs15.readFileSync(agentFile, "utf-8").trim() : dir;
|
|
4603
|
+
const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
|
|
4604
|
+
const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
|
|
4605
|
+
const port = portMatch ? parseInt(portMatch[1], 10) : null;
|
|
4606
|
+
bridges.push({
|
|
4607
|
+
instanceId: agentName,
|
|
4608
|
+
runtime: "codex",
|
|
4609
|
+
status: running ? "running" : "stale",
|
|
4610
|
+
pid: daemon.pid ?? null,
|
|
4611
|
+
port,
|
|
4612
|
+
heartbeatAge: null,
|
|
4613
|
+
headless: false
|
|
4614
|
+
});
|
|
4615
|
+
} catch {
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
} catch {
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
return bridges;
|
|
4622
|
+
}
|
|
4623
|
+
function collectPRs() {
|
|
4624
|
+
try {
|
|
4625
|
+
const output = execSync4(
|
|
4626
|
+
"gh pr list --state all --limit 10 --json number,title,author,state,url",
|
|
4627
|
+
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
4628
|
+
);
|
|
4629
|
+
const prs = JSON.parse(output);
|
|
4630
|
+
return prs.map((pr) => ({
|
|
4631
|
+
number: pr.number,
|
|
4632
|
+
title: pr.title,
|
|
4633
|
+
author: pr.author.login,
|
|
4634
|
+
state: pr.state,
|
|
4635
|
+
url: pr.url
|
|
4636
|
+
}));
|
|
4637
|
+
} catch {
|
|
4638
|
+
return [];
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
function collectWarnings(bridges, agents) {
|
|
4642
|
+
const warnings = [];
|
|
4643
|
+
for (const bridge of bridges) {
|
|
4644
|
+
if (bridge.status === "stale") {
|
|
4645
|
+
warnings.push({
|
|
4646
|
+
level: "warn",
|
|
4647
|
+
message: `Bridge ${bridge.instanceId} is stale (PID ${bridge.pid} dead)`
|
|
4648
|
+
});
|
|
4649
|
+
}
|
|
4650
|
+
if (bridge.status === "running" && bridge.heartbeatAge !== null && bridge.heartbeatAge > 60) {
|
|
4651
|
+
warnings.push({
|
|
4652
|
+
level: "warn",
|
|
4653
|
+
message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
|
|
4654
|
+
});
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
if (bridges.length === 0) {
|
|
4658
|
+
warnings.push({
|
|
4659
|
+
level: "warn",
|
|
4660
|
+
message: "No bridges configured"
|
|
4661
|
+
});
|
|
4662
|
+
}
|
|
4663
|
+
if (agents.length === 0) {
|
|
4664
|
+
warnings.push({
|
|
4665
|
+
level: "warn",
|
|
4666
|
+
message: "No agent heartbeats found"
|
|
4667
|
+
});
|
|
4668
|
+
}
|
|
4669
|
+
return warnings;
|
|
4670
|
+
}
|
|
4671
|
+
function collectDashboardSnapshot(repoRoot, commsDirOverride) {
|
|
4672
|
+
const { config } = resolveConfig(
|
|
4673
|
+
commsDirOverride ? { commsDir: commsDirOverride } : {},
|
|
4674
|
+
repoRoot
|
|
4675
|
+
);
|
|
4676
|
+
const resolved = config;
|
|
4677
|
+
const agents = collectAgents(resolved.commsDir);
|
|
4678
|
+
const bridges = collectBridges(resolved.repoRoot);
|
|
4679
|
+
const prs = collectPRs();
|
|
4680
|
+
const warnings = collectWarnings(bridges, agents);
|
|
4681
|
+
return {
|
|
4682
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4683
|
+
repoRoot: resolved.repoRoot,
|
|
4684
|
+
commsDir: resolved.commsDir,
|
|
4685
|
+
agents,
|
|
4686
|
+
bridges,
|
|
4687
|
+
prs,
|
|
4688
|
+
warnings
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
// src/commands/up.ts
|
|
4693
|
+
var UP_HELP = `
|
|
4694
|
+
Usage:
|
|
4695
|
+
tap-comms up [bridge-start options]
|
|
4696
|
+
|
|
4697
|
+
Description:
|
|
4698
|
+
Start all registered app-server bridge daemons with one command.
|
|
4699
|
+
This is the orchestration entrypoint for headless/background TAP operation.
|
|
4700
|
+
|
|
4701
|
+
Examples:
|
|
4702
|
+
npx @hua-labs/tap up
|
|
4703
|
+
npx @hua-labs/tap up --no-auth
|
|
4704
|
+
npx @hua-labs/tap up --busy-mode wait
|
|
4705
|
+
`.trim();
|
|
4706
|
+
async function upCommand(args) {
|
|
4707
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4708
|
+
log(UP_HELP);
|
|
4709
|
+
return {
|
|
4710
|
+
ok: true,
|
|
4711
|
+
command: "up",
|
|
4712
|
+
code: "TAP_NO_OP",
|
|
4713
|
+
message: UP_HELP,
|
|
4714
|
+
warnings: [],
|
|
4715
|
+
data: {}
|
|
4716
|
+
};
|
|
4717
|
+
}
|
|
4718
|
+
const repoRoot = findRepoRoot();
|
|
4719
|
+
const result = await bridgeCommand(["start", "--all", ...args]);
|
|
4720
|
+
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
4721
|
+
const activeBridges = snapshot.bridges.filter(
|
|
4722
|
+
(bridge) => bridge.status === "running"
|
|
4723
|
+
).length;
|
|
4724
|
+
if (!result.ok) {
|
|
4725
|
+
return {
|
|
4726
|
+
...result,
|
|
4727
|
+
command: "up",
|
|
4728
|
+
data: {
|
|
4729
|
+
...result.data,
|
|
4730
|
+
snapshot
|
|
4731
|
+
}
|
|
4732
|
+
};
|
|
4733
|
+
}
|
|
4734
|
+
return {
|
|
4735
|
+
ok: true,
|
|
4736
|
+
command: "up",
|
|
4737
|
+
code: "TAP_UP_OK",
|
|
4738
|
+
message: `tap up: ${activeBridges} bridge(s) running`,
|
|
4739
|
+
warnings: result.warnings,
|
|
4740
|
+
data: {
|
|
4741
|
+
...result.data,
|
|
4742
|
+
snapshot
|
|
4743
|
+
}
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
// src/commands/down.ts
|
|
4748
|
+
var DOWN_HELP = `
|
|
4749
|
+
Usage:
|
|
4750
|
+
tap-comms down
|
|
4751
|
+
|
|
4752
|
+
Description:
|
|
4753
|
+
Stop all running bridge daemons and managed app-servers.
|
|
4754
|
+
|
|
4755
|
+
Examples:
|
|
4756
|
+
npx @hua-labs/tap down
|
|
4757
|
+
`.trim();
|
|
4758
|
+
async function downCommand(args) {
|
|
4759
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4760
|
+
log(DOWN_HELP);
|
|
4761
|
+
return {
|
|
4762
|
+
ok: true,
|
|
4763
|
+
command: "down",
|
|
4764
|
+
code: "TAP_NO_OP",
|
|
4765
|
+
message: DOWN_HELP,
|
|
4766
|
+
warnings: [],
|
|
4767
|
+
data: {}
|
|
4768
|
+
};
|
|
4769
|
+
}
|
|
4770
|
+
const repoRoot = findRepoRoot();
|
|
4771
|
+
const result = await bridgeCommand(["stop"]);
|
|
4772
|
+
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
4773
|
+
if (!result.ok) {
|
|
4774
|
+
return {
|
|
4775
|
+
...result,
|
|
4776
|
+
command: "down",
|
|
4777
|
+
data: {
|
|
4778
|
+
...result.data,
|
|
4779
|
+
snapshot
|
|
4780
|
+
}
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
return {
|
|
4784
|
+
ok: true,
|
|
4785
|
+
command: "down",
|
|
4786
|
+
code: "TAP_DOWN_OK",
|
|
4787
|
+
message: `tap down: ${snapshot.bridges.filter((bridge) => bridge.status === "running").length} bridge(s) still running`,
|
|
4788
|
+
warnings: result.warnings,
|
|
4789
|
+
data: {
|
|
4790
|
+
...result.data,
|
|
4791
|
+
snapshot
|
|
4792
|
+
}
|
|
4793
|
+
};
|
|
4794
|
+
}
|
|
4795
|
+
|
|
3256
4796
|
// 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
|
-
}
|
|
4797
|
+
import * as path16 from "path";
|
|
4798
|
+
import { spawn as spawn2 } from "child_process";
|
|
3285
4799
|
async function serveCommand(args) {
|
|
3286
|
-
const repoRoot =
|
|
4800
|
+
const repoRoot = findRepoRoot();
|
|
3287
4801
|
let commsDir;
|
|
3288
4802
|
const commsDirIdx = args.indexOf("--comms-dir");
|
|
3289
4803
|
if (commsDirIdx !== -1 && args[commsDirIdx + 1]) {
|
|
3290
|
-
commsDir =
|
|
4804
|
+
commsDir = path16.resolve(args[commsDirIdx + 1]);
|
|
3291
4805
|
}
|
|
3292
4806
|
if (!commsDir && process.env.TAP_COMMS_DIR) {
|
|
3293
4807
|
commsDir = process.env.TAP_COMMS_DIR;
|
|
@@ -3308,37 +4822,29 @@ async function serveCommand(args) {
|
|
|
3308
4822
|
data: {}
|
|
3309
4823
|
};
|
|
3310
4824
|
}
|
|
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) {
|
|
4825
|
+
const ctx = createAdapterContext(commsDir, repoRoot);
|
|
4826
|
+
const managed = buildManagedMcpServerSpec(ctx);
|
|
4827
|
+
if (!managed.command || !managed.sourcePath) {
|
|
4828
|
+
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
4829
|
return {
|
|
3324
4830
|
ok: false,
|
|
3325
4831
|
command: "serve",
|
|
3326
|
-
code: "TAP_SERVE_NO_SERVER",
|
|
3327
|
-
message:
|
|
4832
|
+
code: managed.sourcePath ? "TAP_SERVE_BUN_REQUIRED" : "TAP_SERVE_NO_SERVER",
|
|
4833
|
+
message: fallbackMessage,
|
|
3328
4834
|
warnings: [],
|
|
3329
4835
|
data: {}
|
|
3330
4836
|
};
|
|
3331
4837
|
}
|
|
3332
|
-
const child = spawn2(
|
|
4838
|
+
const child = spawn2(managed.command, managed.args, {
|
|
3333
4839
|
stdio: "inherit",
|
|
3334
4840
|
env: {
|
|
3335
4841
|
...process.env,
|
|
3336
4842
|
TAP_COMMS_DIR: commsDir
|
|
3337
4843
|
}
|
|
3338
4844
|
});
|
|
3339
|
-
return new Promise((
|
|
4845
|
+
return new Promise((resolve11) => {
|
|
3340
4846
|
child.on("error", (err) => {
|
|
3341
|
-
|
|
4847
|
+
resolve11({
|
|
3342
4848
|
ok: false,
|
|
3343
4849
|
command: "serve",
|
|
3344
4850
|
code: "TAP_INTERNAL_ERROR",
|
|
@@ -3348,7 +4854,7 @@ async function serveCommand(args) {
|
|
|
3348
4854
|
});
|
|
3349
4855
|
});
|
|
3350
4856
|
child.on("exit", (code) => {
|
|
3351
|
-
|
|
4857
|
+
resolve11({
|
|
3352
4858
|
ok: code === 0,
|
|
3353
4859
|
command: "serve",
|
|
3354
4860
|
code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
|
|
@@ -3361,8 +4867,8 @@ async function serveCommand(args) {
|
|
|
3361
4867
|
}
|
|
3362
4868
|
|
|
3363
4869
|
// src/commands/init-worktree.ts
|
|
3364
|
-
import * as
|
|
3365
|
-
import * as
|
|
4870
|
+
import * as fs16 from "fs";
|
|
4871
|
+
import * as path17 from "path";
|
|
3366
4872
|
import { execSync as execSync5 } from "child_process";
|
|
3367
4873
|
var INIT_WORKTREE_HELP = `
|
|
3368
4874
|
Usage:
|
|
@@ -3399,7 +4905,7 @@ function run(cmd, opts) {
|
|
|
3399
4905
|
}
|
|
3400
4906
|
}
|
|
3401
4907
|
function toAbsolute(p) {
|
|
3402
|
-
const resolved =
|
|
4908
|
+
const resolved = path17.resolve(p);
|
|
3403
4909
|
return resolved.replace(/\\/g, "/");
|
|
3404
4910
|
}
|
|
3405
4911
|
function probeBun(candidate) {
|
|
@@ -3430,18 +4936,18 @@ function findBun() {
|
|
|
3430
4936
|
}
|
|
3431
4937
|
}
|
|
3432
4938
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3433
|
-
const bunHome =
|
|
4939
|
+
const bunHome = path17.join(
|
|
3434
4940
|
home,
|
|
3435
4941
|
".bun",
|
|
3436
4942
|
"bin",
|
|
3437
4943
|
process.platform === "win32" ? "bun.exe" : "bun"
|
|
3438
4944
|
);
|
|
3439
|
-
if (
|
|
4945
|
+
if (fs16.existsSync(bunHome) && probeBun(bunHome)) return bunHome;
|
|
3440
4946
|
return null;
|
|
3441
4947
|
}
|
|
3442
4948
|
function step1CreateWorktree(opts) {
|
|
3443
4949
|
log("Step 1/9: Creating worktree...");
|
|
3444
|
-
if (
|
|
4950
|
+
if (fs16.existsSync(opts.worktreePath)) {
|
|
3445
4951
|
logWarn(`Directory already exists: ${opts.worktreePath}`);
|
|
3446
4952
|
try {
|
|
3447
4953
|
run("git rev-parse --git-dir", { cwd: opts.worktreePath });
|
|
@@ -3503,22 +5009,22 @@ function step2MergeMain(opts, warnings) {
|
|
|
3503
5009
|
}
|
|
3504
5010
|
function step3CopyPermissions(opts, warnings) {
|
|
3505
5011
|
log("Step 3/9: Copying permissions...");
|
|
3506
|
-
const srcSettings =
|
|
5012
|
+
const srcSettings = path17.join(
|
|
3507
5013
|
opts.repoRoot,
|
|
3508
5014
|
".claude",
|
|
3509
5015
|
"settings.local.json"
|
|
3510
5016
|
);
|
|
3511
|
-
const destDir =
|
|
3512
|
-
const destSettings =
|
|
3513
|
-
if (!
|
|
5017
|
+
const destDir = path17.join(opts.worktreePath, ".claude");
|
|
5018
|
+
const destSettings = path17.join(destDir, "settings.local.json");
|
|
5019
|
+
if (!fs16.existsSync(srcSettings)) {
|
|
3514
5020
|
warn(
|
|
3515
5021
|
warnings,
|
|
3516
5022
|
"No .claude/settings.local.json found in main repo. Skipping."
|
|
3517
5023
|
);
|
|
3518
5024
|
return;
|
|
3519
5025
|
}
|
|
3520
|
-
|
|
3521
|
-
|
|
5026
|
+
fs16.mkdirSync(destDir, { recursive: true });
|
|
5027
|
+
fs16.copyFileSync(srcSettings, destSettings);
|
|
3522
5028
|
logSuccess("Copied settings.local.json");
|
|
3523
5029
|
try {
|
|
3524
5030
|
run("git update-index --skip-worktree .claude/settings.local.json", {
|
|
@@ -3543,7 +5049,7 @@ function step4GenerateMcpJson(opts, warnings) {
|
|
|
3543
5049
|
const wtAbs = toAbsolute(opts.worktreePath);
|
|
3544
5050
|
const bunAbs = toAbsolute(bunPath);
|
|
3545
5051
|
const commsAbs = toAbsolute(opts.commsDir);
|
|
3546
|
-
const channelEntry =
|
|
5052
|
+
const channelEntry = path17.join(
|
|
3547
5053
|
wtAbs,
|
|
3548
5054
|
"packages/tap-plugin/channels/tap-comms.ts"
|
|
3549
5055
|
);
|
|
@@ -3560,8 +5066,8 @@ function step4GenerateMcpJson(opts, warnings) {
|
|
|
3560
5066
|
}
|
|
3561
5067
|
}
|
|
3562
5068
|
};
|
|
3563
|
-
const mcpPath =
|
|
3564
|
-
|
|
5069
|
+
const mcpPath = path17.join(opts.worktreePath, ".mcp.json");
|
|
5070
|
+
fs16.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
3565
5071
|
logSuccess(`.mcp.json generated (absolute paths + cwd)`);
|
|
3566
5072
|
log(` bun: ${bunAbs}`);
|
|
3567
5073
|
log(` comms: ${commsAbs}`);
|
|
@@ -3599,275 +5105,126 @@ function step6BuildEslintPlugin(opts, warnings) {
|
|
|
3599
5105
|
}
|
|
3600
5106
|
function step7VerifyComms(opts, warnings) {
|
|
3601
5107
|
log("Step 7/9: Verifying comms directory...");
|
|
3602
|
-
if (!
|
|
5108
|
+
if (!fs16.existsSync(opts.commsDir)) {
|
|
3603
5109
|
warn(warnings, `Comms directory not found: ${opts.commsDir}`);
|
|
3604
|
-
warn(warnings, "Create it or run: npx @hua-labs/tap init");
|
|
3605
|
-
return;
|
|
3606
|
-
}
|
|
3607
|
-
const requiredDirs = ["inbox", "findings", "reviews", "letters"];
|
|
3608
|
-
for (const dir of requiredDirs) {
|
|
3609
|
-
const dirPath =
|
|
3610
|
-
if (!
|
|
3611
|
-
|
|
3612
|
-
logSuccess(`Created ${dir}/`);
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
logSuccess(`Comms verified: ${opts.commsDir}`);
|
|
3616
|
-
}
|
|
3617
|
-
function step8VerifyBun(warnings) {
|
|
3618
|
-
log("Step 8/9: Verifying bun...");
|
|
3619
|
-
const bunPath = findBun();
|
|
3620
|
-
if (!bunPath) {
|
|
3621
|
-
warn(warnings, "bun not found in PATH.");
|
|
3622
|
-
warn(warnings, "Install: curl -fsSL https://bun.sh/install | bash");
|
|
3623
|
-
return;
|
|
3624
|
-
}
|
|
3625
|
-
try {
|
|
3626
|
-
const version2 = run(`"${bunPath}" --version`);
|
|
3627
|
-
logSuccess(`bun ${version2} found: ${bunPath}`);
|
|
3628
|
-
} catch {
|
|
3629
|
-
warn(warnings, "bun found but version check failed.");
|
|
3630
|
-
}
|
|
3631
|
-
}
|
|
3632
|
-
function step9Ready(opts) {
|
|
3633
|
-
logHeader("Ready!");
|
|
3634
|
-
log(`Worktree: ${toAbsolute(opts.worktreePath)}`);
|
|
3635
|
-
log(`Branch: ${opts.branch}`);
|
|
3636
|
-
log(`Comms: ${toAbsolute(opts.commsDir)}`);
|
|
3637
|
-
if (opts.mission) log(`Mission: ${opts.mission}`);
|
|
3638
|
-
log("");
|
|
3639
|
-
log("Next steps:");
|
|
3640
|
-
log(` cd ${opts.worktreePath}`);
|
|
3641
|
-
log(" claude # Start Claude Code session");
|
|
3642
|
-
log("");
|
|
3643
|
-
}
|
|
3644
|
-
async function initWorktreeCommand(args) {
|
|
3645
|
-
const { flags } = parseArgs(args);
|
|
3646
|
-
if (flags["help"] === true || flags["h"] === true) {
|
|
3647
|
-
log(INIT_WORKTREE_HELP);
|
|
3648
|
-
return {
|
|
3649
|
-
ok: true,
|
|
3650
|
-
command: "init-worktree",
|
|
3651
|
-
code: "TAP_NO_OP",
|
|
3652
|
-
message: "init-worktree help",
|
|
3653
|
-
warnings: [],
|
|
3654
|
-
data: {}
|
|
3655
|
-
};
|
|
3656
|
-
}
|
|
3657
|
-
const worktreePath = typeof flags["path"] === "string" ? flags["path"] : void 0;
|
|
3658
|
-
if (!worktreePath) {
|
|
3659
|
-
return {
|
|
3660
|
-
ok: false,
|
|
3661
|
-
command: "init-worktree",
|
|
3662
|
-
code: "TAP_INVALID_ARGUMENT",
|
|
3663
|
-
message: "Missing --path. Usage: npx @hua-labs/tap init-worktree --path ../hua-wt-3",
|
|
3664
|
-
warnings: [],
|
|
3665
|
-
data: {}
|
|
3666
|
-
};
|
|
3667
|
-
}
|
|
3668
|
-
const repoRoot = findRepoRoot2();
|
|
3669
|
-
const { config } = resolveConfig({}, repoRoot);
|
|
3670
|
-
const branch = typeof flags["branch"] === "string" ? flags["branch"] : path15.basename(path15.resolve(worktreePath));
|
|
3671
|
-
const base = typeof flags["base"] === "string" ? flags["base"] : "origin/main";
|
|
3672
|
-
const mission = typeof flags["mission"] === "string" ? flags["mission"] : void 0;
|
|
3673
|
-
const commsDir = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : config.commsDir;
|
|
3674
|
-
const skipInstall = flags["skip-install"] === true;
|
|
3675
|
-
const opts = {
|
|
3676
|
-
worktreePath: path15.resolve(worktreePath),
|
|
3677
|
-
branch,
|
|
3678
|
-
base,
|
|
3679
|
-
mission,
|
|
3680
|
-
commsDir: path15.resolve(commsDir),
|
|
3681
|
-
skipInstall,
|
|
3682
|
-
repoRoot
|
|
3683
|
-
};
|
|
3684
|
-
logHeader(`@hua-labs/tap init-worktree`);
|
|
3685
|
-
log(`Path: ${opts.worktreePath}`);
|
|
3686
|
-
log(`Branch: ${opts.branch}`);
|
|
3687
|
-
log(`Base: ${opts.base}`);
|
|
3688
|
-
log(`Comms: ${opts.commsDir}`);
|
|
3689
|
-
if (mission) log(`Mission: ${mission}`);
|
|
3690
|
-
log("");
|
|
3691
|
-
const warnings = [];
|
|
3692
|
-
const created = step1CreateWorktree(opts);
|
|
3693
|
-
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
|
-
});
|
|
5110
|
+
warn(warnings, "Create it or run: npx @hua-labs/tap init");
|
|
5111
|
+
return;
|
|
5112
|
+
}
|
|
5113
|
+
const requiredDirs = ["inbox", "findings", "reviews", "letters"];
|
|
5114
|
+
for (const dir of requiredDirs) {
|
|
5115
|
+
const dirPath = path17.join(opts.commsDir, dir);
|
|
5116
|
+
if (!fs16.existsSync(dirPath)) {
|
|
5117
|
+
fs16.mkdirSync(dirPath, { recursive: true });
|
|
5118
|
+
logSuccess(`Created ${dir}/`);
|
|
3837
5119
|
}
|
|
3838
5120
|
}
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
5121
|
+
logSuccess(`Comms verified: ${opts.commsDir}`);
|
|
5122
|
+
}
|
|
5123
|
+
function step8VerifyBun(warnings) {
|
|
5124
|
+
log("Step 8/9: Verifying bun...");
|
|
5125
|
+
const bunPath = findBun();
|
|
5126
|
+
if (!bunPath) {
|
|
5127
|
+
warn(warnings, "bun not found in PATH.");
|
|
5128
|
+
warn(warnings, "Install: curl -fsSL https://bun.sh/install | bash");
|
|
5129
|
+
return;
|
|
3844
5130
|
}
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
5131
|
+
try {
|
|
5132
|
+
const version2 = run(`"${bunPath}" --version`);
|
|
5133
|
+
logSuccess(`bun ${version2} found: ${bunPath}`);
|
|
5134
|
+
} catch {
|
|
5135
|
+
warn(warnings, "bun found but version check failed.");
|
|
3850
5136
|
}
|
|
3851
|
-
return warnings;
|
|
3852
5137
|
}
|
|
3853
|
-
function
|
|
3854
|
-
|
|
3855
|
-
|
|
5138
|
+
function step9Ready(opts) {
|
|
5139
|
+
logHeader("Ready!");
|
|
5140
|
+
log(`Worktree: ${toAbsolute(opts.worktreePath)}`);
|
|
5141
|
+
log(`Branch: ${opts.branch}`);
|
|
5142
|
+
log(`Comms: ${toAbsolute(opts.commsDir)}`);
|
|
5143
|
+
if (opts.mission) log(`Mission: ${opts.mission}`);
|
|
5144
|
+
log("");
|
|
5145
|
+
log("Next steps:");
|
|
5146
|
+
log(` cd ${opts.worktreePath}`);
|
|
5147
|
+
log(" claude # Start Claude Code session");
|
|
5148
|
+
log("");
|
|
5149
|
+
}
|
|
5150
|
+
async function initWorktreeCommand(args) {
|
|
5151
|
+
const { flags } = parseArgs(args);
|
|
5152
|
+
if (flags["help"] === true || flags["h"] === true) {
|
|
5153
|
+
log(INIT_WORKTREE_HELP);
|
|
5154
|
+
return {
|
|
5155
|
+
ok: true,
|
|
5156
|
+
command: "init-worktree",
|
|
5157
|
+
code: "TAP_NO_OP",
|
|
5158
|
+
message: "init-worktree help",
|
|
5159
|
+
warnings: [],
|
|
5160
|
+
data: {}
|
|
5161
|
+
};
|
|
5162
|
+
}
|
|
5163
|
+
const worktreePath = typeof flags["path"] === "string" ? flags["path"] : void 0;
|
|
5164
|
+
if (!worktreePath) {
|
|
5165
|
+
return {
|
|
5166
|
+
ok: false,
|
|
5167
|
+
command: "init-worktree",
|
|
5168
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
5169
|
+
message: "Missing --path. Usage: npx @hua-labs/tap init-worktree --path ../hua-wt-3",
|
|
5170
|
+
warnings: [],
|
|
5171
|
+
data: {}
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5174
|
+
const repoRoot = findRepoRoot();
|
|
5175
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
5176
|
+
const branch = typeof flags["branch"] === "string" ? flags["branch"] : path17.basename(path17.resolve(worktreePath));
|
|
5177
|
+
const base = typeof flags["base"] === "string" ? flags["base"] : "origin/main";
|
|
5178
|
+
const mission = typeof flags["mission"] === "string" ? flags["mission"] : void 0;
|
|
5179
|
+
const commsDir = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : config.commsDir;
|
|
5180
|
+
const skipInstall = flags["skip-install"] === true;
|
|
5181
|
+
const opts = {
|
|
5182
|
+
worktreePath: path17.resolve(worktreePath),
|
|
5183
|
+
branch,
|
|
5184
|
+
base,
|
|
5185
|
+
mission,
|
|
5186
|
+
commsDir: path17.resolve(commsDir),
|
|
5187
|
+
skipInstall,
|
|
3856
5188
|
repoRoot
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
5189
|
+
};
|
|
5190
|
+
logHeader(`@hua-labs/tap init-worktree`);
|
|
5191
|
+
log(`Path: ${opts.worktreePath}`);
|
|
5192
|
+
log(`Branch: ${opts.branch}`);
|
|
5193
|
+
log(`Base: ${opts.base}`);
|
|
5194
|
+
log(`Comms: ${opts.commsDir}`);
|
|
5195
|
+
if (mission) log(`Mission: ${mission}`);
|
|
5196
|
+
log("");
|
|
5197
|
+
const warnings = [];
|
|
5198
|
+
const created = step1CreateWorktree(opts);
|
|
5199
|
+
if (!created) {
|
|
5200
|
+
return {
|
|
5201
|
+
ok: false,
|
|
5202
|
+
command: "init-worktree",
|
|
5203
|
+
code: "TAP_PATCH_FAILED",
|
|
5204
|
+
message: "Failed to create worktree.",
|
|
5205
|
+
warnings,
|
|
5206
|
+
data: {}
|
|
5207
|
+
};
|
|
5208
|
+
}
|
|
5209
|
+
step2MergeMain(opts, warnings);
|
|
5210
|
+
step3CopyPermissions(opts, warnings);
|
|
5211
|
+
step4GenerateMcpJson(opts, warnings);
|
|
5212
|
+
step5Install(opts, warnings);
|
|
5213
|
+
step6BuildEslintPlugin(opts, warnings);
|
|
5214
|
+
step7VerifyComms(opts, warnings);
|
|
5215
|
+
step8VerifyBun(warnings);
|
|
5216
|
+
step9Ready(opts);
|
|
3863
5217
|
return {
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
5218
|
+
ok: true,
|
|
5219
|
+
command: "init-worktree",
|
|
5220
|
+
code: "TAP_INIT_OK",
|
|
5221
|
+
message: `Worktree initialized: ${opts.worktreePath}`,
|
|
5222
|
+
warnings,
|
|
5223
|
+
data: {
|
|
5224
|
+
path: opts.worktreePath,
|
|
5225
|
+
branch: opts.branch,
|
|
5226
|
+
commsDir: opts.commsDir
|
|
5227
|
+
}
|
|
3871
5228
|
};
|
|
3872
5229
|
}
|
|
3873
5230
|
|
|
@@ -3967,7 +5324,7 @@ async function dashboardCommand(args) {
|
|
|
3967
5324
|
const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : "5";
|
|
3968
5325
|
const intervalSeconds = Math.max(2, parseInt(intervalStr, 10) || 5);
|
|
3969
5326
|
const commsDirOverride = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : void 0;
|
|
3970
|
-
const repoRoot =
|
|
5327
|
+
const repoRoot = findRepoRoot();
|
|
3971
5328
|
if (watchMode) {
|
|
3972
5329
|
const run2 = () => {
|
|
3973
5330
|
const snapshot2 = collectDashboardSnapshot(repoRoot, commsDirOverride);
|
|
@@ -4015,6 +5372,371 @@ async function dashboardCommand(args) {
|
|
|
4015
5372
|
};
|
|
4016
5373
|
}
|
|
4017
5374
|
|
|
5375
|
+
// src/commands/doctor.ts
|
|
5376
|
+
import {
|
|
5377
|
+
existsSync as existsSync16,
|
|
5378
|
+
mkdirSync as mkdirSync10,
|
|
5379
|
+
readdirSync as readdirSync3,
|
|
5380
|
+
readFileSync as readFileSync14,
|
|
5381
|
+
statSync as statSync2,
|
|
5382
|
+
unlinkSync as unlinkSync3
|
|
5383
|
+
} from "fs";
|
|
5384
|
+
import { join as join17 } from "path";
|
|
5385
|
+
var PASS = "pass";
|
|
5386
|
+
var WARN = "warn";
|
|
5387
|
+
var FAIL = "fail";
|
|
5388
|
+
function countFiles(dir, ext = ".md") {
|
|
5389
|
+
if (!existsSync16(dir)) return 0;
|
|
5390
|
+
try {
|
|
5391
|
+
return readdirSync3(dir).filter((f) => f.endsWith(ext)).length;
|
|
5392
|
+
} catch {
|
|
5393
|
+
return 0;
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
function recentFileCount(dir, withinMs) {
|
|
5397
|
+
if (!existsSync16(dir)) return 0;
|
|
5398
|
+
const cutoff = Date.now() - withinMs;
|
|
5399
|
+
let count = 0;
|
|
5400
|
+
try {
|
|
5401
|
+
for (const f of readdirSync3(dir)) {
|
|
5402
|
+
if (!f.endsWith(".md")) continue;
|
|
5403
|
+
try {
|
|
5404
|
+
if (statSync2(join17(dir, f)).mtimeMs > cutoff) count++;
|
|
5405
|
+
} catch {
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
} catch {
|
|
5409
|
+
}
|
|
5410
|
+
return count;
|
|
5411
|
+
}
|
|
5412
|
+
function checkComms(commsDir) {
|
|
5413
|
+
const checks = [];
|
|
5414
|
+
checks.push({
|
|
5415
|
+
name: "comms directory",
|
|
5416
|
+
status: existsSync16(commsDir) ? PASS : FAIL,
|
|
5417
|
+
message: existsSync16(commsDir) ? commsDir : `Not found: ${commsDir}`,
|
|
5418
|
+
fix: existsSync16(commsDir) ? void 0 : () => {
|
|
5419
|
+
mkdirSync10(commsDir, { recursive: true });
|
|
5420
|
+
return `Created ${commsDir}`;
|
|
5421
|
+
}
|
|
5422
|
+
});
|
|
5423
|
+
for (const [subdir, required] of [
|
|
5424
|
+
["inbox", true],
|
|
5425
|
+
["reviews", false],
|
|
5426
|
+
["findings", false]
|
|
5427
|
+
]) {
|
|
5428
|
+
const dir = join17(commsDir, subdir);
|
|
5429
|
+
const exists = existsSync16(dir);
|
|
5430
|
+
checks.push({
|
|
5431
|
+
name: `${subdir} directory`,
|
|
5432
|
+
status: exists ? PASS : required ? FAIL : WARN,
|
|
5433
|
+
message: exists ? subdir === "findings" ? `${countFiles(dir)} findings` : subdir === "inbox" ? `${countFiles(dir)} messages` : "exists" : `Missing${required ? "" : " (optional)"}`,
|
|
5434
|
+
fix: exists ? void 0 : () => {
|
|
5435
|
+
mkdirSync10(dir, { recursive: true });
|
|
5436
|
+
return `Created ${dir}`;
|
|
5437
|
+
}
|
|
5438
|
+
});
|
|
5439
|
+
}
|
|
5440
|
+
const heartbeats = join17(commsDir, "heartbeats.json");
|
|
5441
|
+
if (existsSync16(heartbeats)) {
|
|
5442
|
+
try {
|
|
5443
|
+
const store = JSON.parse(readFileSync14(heartbeats, "utf-8"));
|
|
5444
|
+
const agents = Object.keys(store);
|
|
5445
|
+
const now = Date.now();
|
|
5446
|
+
const active = agents.filter((a) => {
|
|
5447
|
+
const ts = store[a]?.lastActivity;
|
|
5448
|
+
return ts && now - new Date(ts).getTime() < 10 * 60 * 1e3;
|
|
5449
|
+
});
|
|
5450
|
+
checks.push({
|
|
5451
|
+
name: "heartbeats",
|
|
5452
|
+
status: active.length > 0 ? PASS : WARN,
|
|
5453
|
+
message: `${active.length} active / ${agents.length} total`
|
|
5454
|
+
});
|
|
5455
|
+
} catch {
|
|
5456
|
+
checks.push({
|
|
5457
|
+
name: "heartbeats",
|
|
5458
|
+
status: WARN,
|
|
5459
|
+
message: "File exists but unreadable"
|
|
5460
|
+
});
|
|
5461
|
+
}
|
|
5462
|
+
} else {
|
|
5463
|
+
checks.push({
|
|
5464
|
+
name: "heartbeats",
|
|
5465
|
+
status: WARN,
|
|
5466
|
+
message: "No heartbeats file"
|
|
5467
|
+
});
|
|
5468
|
+
}
|
|
5469
|
+
return checks;
|
|
5470
|
+
}
|
|
5471
|
+
function checkInstances(repoRoot, stateDir) {
|
|
5472
|
+
const checks = [];
|
|
5473
|
+
const state = loadState(repoRoot);
|
|
5474
|
+
if (!state) {
|
|
5475
|
+
checks.push({
|
|
5476
|
+
name: "tap state",
|
|
5477
|
+
status: FAIL,
|
|
5478
|
+
message: "Not initialized. Run: tap init"
|
|
5479
|
+
});
|
|
5480
|
+
return checks;
|
|
5481
|
+
}
|
|
5482
|
+
checks.push({
|
|
5483
|
+
name: "tap state",
|
|
5484
|
+
status: PASS,
|
|
5485
|
+
message: `v${state.schemaVersion}, ${getInstalledInstances(state).length} instance(s)`
|
|
5486
|
+
});
|
|
5487
|
+
const installed = getInstalledInstances(state);
|
|
5488
|
+
for (const id of installed) {
|
|
5489
|
+
const inst = state.instances[id];
|
|
5490
|
+
if (!inst) continue;
|
|
5491
|
+
if (inst.bridgeMode === "app-server") {
|
|
5492
|
+
const running = isBridgeRunning(stateDir, id);
|
|
5493
|
+
const bridgeState = loadBridgeState(stateDir, id);
|
|
5494
|
+
const heartbeatAge = getHeartbeatAge(stateDir, id);
|
|
5495
|
+
let status;
|
|
5496
|
+
let message;
|
|
5497
|
+
let fix;
|
|
5498
|
+
if (running && bridgeState) {
|
|
5499
|
+
if (heartbeatAge !== null && heartbeatAge > 120) {
|
|
5500
|
+
status = WARN;
|
|
5501
|
+
message = `PID ${bridgeState.pid} alive but heartbeat stale (${Math.round(heartbeatAge)}s ago)`;
|
|
5502
|
+
} else {
|
|
5503
|
+
status = PASS;
|
|
5504
|
+
message = `PID ${bridgeState.pid}, port ${inst.port ?? "auto"}`;
|
|
5505
|
+
}
|
|
5506
|
+
} else if (bridgeState && !running) {
|
|
5507
|
+
status = WARN;
|
|
5508
|
+
message = `Stale PID ${bridgeState.pid} (process dead)`;
|
|
5509
|
+
fix = () => {
|
|
5510
|
+
const appServer = bridgeState.appServer;
|
|
5511
|
+
if (appServer?.managed) {
|
|
5512
|
+
for (const pid of [appServer.auth?.gatewayPid, appServer.pid]) {
|
|
5513
|
+
if (pid) {
|
|
5514
|
+
try {
|
|
5515
|
+
process.kill(pid);
|
|
5516
|
+
} catch {
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
const pidPath = join17(stateDir, "pids", `bridge-${id}.json`);
|
|
5522
|
+
try {
|
|
5523
|
+
unlinkSync3(pidPath);
|
|
5524
|
+
} catch {
|
|
5525
|
+
}
|
|
5526
|
+
const currentState = loadState(repoRoot);
|
|
5527
|
+
if (currentState?.instances[id]) {
|
|
5528
|
+
currentState.instances[id].bridge = null;
|
|
5529
|
+
currentState.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5530
|
+
saveState(repoRoot, currentState);
|
|
5531
|
+
}
|
|
5532
|
+
return `Cleaned stale bridge + managed processes for ${id}`;
|
|
5533
|
+
};
|
|
5534
|
+
} else {
|
|
5535
|
+
status = WARN;
|
|
5536
|
+
message = "Not running";
|
|
5537
|
+
}
|
|
5538
|
+
checks.push({ name: `bridge: ${id}`, status, message, fix });
|
|
5539
|
+
} else {
|
|
5540
|
+
checks.push({
|
|
5541
|
+
name: `instance: ${id}`,
|
|
5542
|
+
status: PASS,
|
|
5543
|
+
message: `${inst.runtime} (${inst.bridgeMode})`
|
|
5544
|
+
});
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
return checks;
|
|
5548
|
+
}
|
|
5549
|
+
function checkMessageLifecycle(commsDir) {
|
|
5550
|
+
const checks = [];
|
|
5551
|
+
const inbox = join17(commsDir, "inbox");
|
|
5552
|
+
if (!existsSync16(inbox)) {
|
|
5553
|
+
checks.push({
|
|
5554
|
+
name: "message flow",
|
|
5555
|
+
status: FAIL,
|
|
5556
|
+
message: "No inbox"
|
|
5557
|
+
});
|
|
5558
|
+
return checks;
|
|
5559
|
+
}
|
|
5560
|
+
const total = countFiles(inbox);
|
|
5561
|
+
const recent1h = recentFileCount(inbox, 60 * 60 * 1e3);
|
|
5562
|
+
const recent10m = recentFileCount(inbox, 10 * 60 * 1e3);
|
|
5563
|
+
checks.push({
|
|
5564
|
+
name: "message flow",
|
|
5565
|
+
status: recent10m > 0 ? PASS : total > 0 ? WARN : FAIL,
|
|
5566
|
+
message: `${total} total, ${recent1h} in last 1h, ${recent10m} in last 10m`
|
|
5567
|
+
});
|
|
5568
|
+
const receiptsPath = join17(commsDir, "receipts", "receipts.json");
|
|
5569
|
+
if (existsSync16(receiptsPath)) {
|
|
5570
|
+
try {
|
|
5571
|
+
const receipts = JSON.parse(readFileSync14(receiptsPath, "utf-8"));
|
|
5572
|
+
const receiptCount = Object.keys(receipts).length;
|
|
5573
|
+
checks.push({
|
|
5574
|
+
name: "read receipts",
|
|
5575
|
+
status: PASS,
|
|
5576
|
+
message: `${receiptCount} receipts tracked`
|
|
5577
|
+
});
|
|
5578
|
+
} catch {
|
|
5579
|
+
checks.push({
|
|
5580
|
+
name: "read receipts",
|
|
5581
|
+
status: WARN,
|
|
5582
|
+
message: "File exists but unreadable"
|
|
5583
|
+
});
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
return checks;
|
|
5587
|
+
}
|
|
5588
|
+
function checkMcpServer(repoRoot) {
|
|
5589
|
+
const checks = [];
|
|
5590
|
+
const mcpJson = join17(repoRoot, ".mcp.json");
|
|
5591
|
+
if (existsSync16(mcpJson)) {
|
|
5592
|
+
try {
|
|
5593
|
+
const config = JSON.parse(readFileSync14(mcpJson, "utf-8"));
|
|
5594
|
+
const hasTapComms = config?.mcpServers?.["tap-comms"];
|
|
5595
|
+
checks.push({
|
|
5596
|
+
name: "MCP config (.mcp.json)",
|
|
5597
|
+
status: hasTapComms ? PASS : WARN,
|
|
5598
|
+
message: hasTapComms ? `command: ${hasTapComms.command}` : "tap-comms not configured"
|
|
5599
|
+
});
|
|
5600
|
+
if (hasTapComms?.args?.[0]) {
|
|
5601
|
+
const mcpScript = hasTapComms.args[0];
|
|
5602
|
+
checks.push({
|
|
5603
|
+
name: "MCP server script",
|
|
5604
|
+
status: existsSync16(mcpScript) ? PASS : FAIL,
|
|
5605
|
+
message: existsSync16(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
|
|
5606
|
+
});
|
|
5607
|
+
}
|
|
5608
|
+
} catch {
|
|
5609
|
+
checks.push({
|
|
5610
|
+
name: "MCP config (.mcp.json)",
|
|
5611
|
+
status: WARN,
|
|
5612
|
+
message: "File exists but invalid JSON"
|
|
5613
|
+
});
|
|
5614
|
+
}
|
|
5615
|
+
} else {
|
|
5616
|
+
checks.push({
|
|
5617
|
+
name: "MCP config (.mcp.json)",
|
|
5618
|
+
status: WARN,
|
|
5619
|
+
message: "Not found \u2014 MCP channel notifications won't work"
|
|
5620
|
+
});
|
|
5621
|
+
}
|
|
5622
|
+
return checks;
|
|
5623
|
+
}
|
|
5624
|
+
function renderCheck(check, fixMode) {
|
|
5625
|
+
const icons = {
|
|
5626
|
+
pass: "[OK]",
|
|
5627
|
+
warn: "[!!]",
|
|
5628
|
+
fail: "[XX]",
|
|
5629
|
+
skip: "[--]"
|
|
5630
|
+
};
|
|
5631
|
+
const icon = icons[check.status] || "[??]";
|
|
5632
|
+
const fixable = fixMode && check.fix ? " (fixable)" : "";
|
|
5633
|
+
const msg = check.message ? ` \u2014 ${check.message}${fixable}` : "";
|
|
5634
|
+
return ` ${icon} ${check.name}${msg}`;
|
|
5635
|
+
}
|
|
5636
|
+
async function doctorCommand(args) {
|
|
5637
|
+
const repoRoot = findRepoRoot();
|
|
5638
|
+
const overrides = {};
|
|
5639
|
+
let fixMode = false;
|
|
5640
|
+
for (let i = 0; i < args.length; i++) {
|
|
5641
|
+
if (args[i] === "--comms-dir" && args[i + 1]) {
|
|
5642
|
+
overrides.commsDir = args[i + 1];
|
|
5643
|
+
}
|
|
5644
|
+
if (args[i] === "--fix") {
|
|
5645
|
+
fixMode = true;
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
const { config } = resolveConfig(overrides, repoRoot);
|
|
5649
|
+
const state = loadState(repoRoot);
|
|
5650
|
+
const commsDir = overrides.commsDir ? config.commsDir : state?.commsDir ?? config.commsDir;
|
|
5651
|
+
logHeader(`@hua-labs/tap doctor (v${version})${fixMode ? " --fix" : ""}`);
|
|
5652
|
+
function runAllChecks() {
|
|
5653
|
+
const checks = [];
|
|
5654
|
+
checks.push(...checkComms(commsDir));
|
|
5655
|
+
checks.push(...checkInstances(repoRoot, config.stateDir));
|
|
5656
|
+
checks.push(...checkMessageLifecycle(commsDir));
|
|
5657
|
+
checks.push(...checkMcpServer(repoRoot));
|
|
5658
|
+
return checks;
|
|
5659
|
+
}
|
|
5660
|
+
const initialChecks = runAllChecks();
|
|
5661
|
+
for (const section of ["Comms", "Instances", "Messages", "MCP"]) {
|
|
5662
|
+
const sectionChecks = {
|
|
5663
|
+
Comms: initialChecks.filter(
|
|
5664
|
+
(c) => [
|
|
5665
|
+
"comms directory",
|
|
5666
|
+
"inbox directory",
|
|
5667
|
+
"reviews directory",
|
|
5668
|
+
"findings directory",
|
|
5669
|
+
"heartbeats"
|
|
5670
|
+
].includes(c.name)
|
|
5671
|
+
),
|
|
5672
|
+
Instances: initialChecks.filter(
|
|
5673
|
+
(c) => c.name.startsWith("bridge:") || c.name.startsWith("instance:") || c.name === "tap state"
|
|
5674
|
+
),
|
|
5675
|
+
Messages: initialChecks.filter(
|
|
5676
|
+
(c) => ["message flow", "read receipts"].includes(c.name)
|
|
5677
|
+
),
|
|
5678
|
+
MCP: initialChecks.filter(
|
|
5679
|
+
(c) => c.name.startsWith("MCP") || c.name === "MCP server script"
|
|
5680
|
+
)
|
|
5681
|
+
}[section];
|
|
5682
|
+
if (sectionChecks.length > 0) {
|
|
5683
|
+
log(`${section}:`);
|
|
5684
|
+
for (const c of sectionChecks) log(renderCheck(c, fixMode));
|
|
5685
|
+
log("");
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
const fixed = [];
|
|
5689
|
+
let finalChecks = initialChecks;
|
|
5690
|
+
if (fixMode) {
|
|
5691
|
+
const fixable = initialChecks.filter(
|
|
5692
|
+
(c) => (c.status === "warn" || c.status === "fail") && c.fix
|
|
5693
|
+
);
|
|
5694
|
+
if (fixable.length > 0) {
|
|
5695
|
+
log("Fixes:");
|
|
5696
|
+
for (const c of fixable) {
|
|
5697
|
+
try {
|
|
5698
|
+
const desc = c.fix();
|
|
5699
|
+
fixed.push(desc);
|
|
5700
|
+
logSuccess(` ${desc}`);
|
|
5701
|
+
} catch (err) {
|
|
5702
|
+
logWarn(
|
|
5703
|
+
` Failed to fix ${c.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
5704
|
+
);
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
log("");
|
|
5708
|
+
log("Re-verifying...");
|
|
5709
|
+
finalChecks = runAllChecks();
|
|
5710
|
+
const postFails = finalChecks.filter((c) => c.status === "fail").length;
|
|
5711
|
+
const postWarns = finalChecks.filter((c) => c.status === "warn").length;
|
|
5712
|
+
log(
|
|
5713
|
+
` ${postFails === 0 ? "All clear" : `${postFails} remaining failures, ${postWarns} warnings`}`
|
|
5714
|
+
);
|
|
5715
|
+
} else {
|
|
5716
|
+
log("Nothing to fix.");
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
const passes = finalChecks.filter((c) => c.status === "pass").length;
|
|
5720
|
+
const warns = finalChecks.filter((c) => c.status === "warn").length;
|
|
5721
|
+
const fails = finalChecks.filter((c) => c.status === "fail").length;
|
|
5722
|
+
log("");
|
|
5723
|
+
log(
|
|
5724
|
+
`${finalChecks.length} checks: ${passes} passed, ${warns} warnings, ${fails} failures` + (fixed.length > 0 ? ` (${fixed.length} fixed)` : "")
|
|
5725
|
+
);
|
|
5726
|
+
return {
|
|
5727
|
+
ok: fails === 0,
|
|
5728
|
+
command: "doctor",
|
|
5729
|
+
code: fails === 0 ? "TAP_STATUS_OK" : "TAP_VERIFY_FAILED",
|
|
5730
|
+
message: `${passes} passed, ${warns} warnings, ${fails} failures`,
|
|
5731
|
+
warnings: finalChecks.filter((c) => c.status === "warn").map((c) => `${c.name}: ${c.message}`),
|
|
5732
|
+
data: {
|
|
5733
|
+
checks: finalChecks.map(({ fix, ...rest }) => rest),
|
|
5734
|
+
summary: { total: finalChecks.length, passes, warns, fails },
|
|
5735
|
+
fixed
|
|
5736
|
+
}
|
|
5737
|
+
};
|
|
5738
|
+
}
|
|
5739
|
+
|
|
4018
5740
|
// src/output.ts
|
|
4019
5741
|
function emitResult(result, jsonMode) {
|
|
4020
5742
|
if (jsonMode) {
|
|
@@ -4053,7 +5775,10 @@ Commands:
|
|
|
4053
5775
|
remove <instance> Remove an instance and rollback config
|
|
4054
5776
|
status Show installed instances and bridge status
|
|
4055
5777
|
bridge <sub> [inst] Manage bridges (start, stop, status)
|
|
5778
|
+
up Start all registered bridge daemons
|
|
5779
|
+
down Stop all running bridge daemons
|
|
4056
5780
|
dashboard Show unified ops dashboard
|
|
5781
|
+
doctor Diagnose tap infrastructure health
|
|
4057
5782
|
serve Start tap-comms MCP server (stdio)
|
|
4058
5783
|
version Show version
|
|
4059
5784
|
|
|
@@ -4077,7 +5802,10 @@ function normalizeCommandName(command) {
|
|
|
4077
5802
|
case "remove":
|
|
4078
5803
|
case "status":
|
|
4079
5804
|
case "bridge":
|
|
5805
|
+
case "up":
|
|
5806
|
+
case "down":
|
|
4080
5807
|
case "dashboard":
|
|
5808
|
+
case "doctor":
|
|
4081
5809
|
case "serve":
|
|
4082
5810
|
return command;
|
|
4083
5811
|
default:
|
|
@@ -4127,9 +5855,18 @@ async function main() {
|
|
|
4127
5855
|
case "bridge":
|
|
4128
5856
|
result = await bridgeCommand(commandArgs);
|
|
4129
5857
|
break;
|
|
5858
|
+
case "up":
|
|
5859
|
+
result = await upCommand(commandArgs);
|
|
5860
|
+
break;
|
|
5861
|
+
case "down":
|
|
5862
|
+
result = await downCommand(commandArgs);
|
|
5863
|
+
break;
|
|
4130
5864
|
case "dashboard":
|
|
4131
5865
|
result = await dashboardCommand(commandArgs);
|
|
4132
5866
|
break;
|
|
5867
|
+
case "doctor":
|
|
5868
|
+
result = await doctorCommand(commandArgs);
|
|
5869
|
+
break;
|
|
4133
5870
|
case "serve": {
|
|
4134
5871
|
const serveResult = await serveCommand(commandArgs);
|
|
4135
5872
|
if (!serveResult.ok) {
|