@hua-labs/tap 0.1.1 → 0.2.1

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